Linux IO: 系统调用 poll() 实现简析

news/2024/10/30 15:26:08/

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 分析背景

本文基于 Linux 4.14 内核源码进行分析。

3. 系统调用 poll() 实现分析

3.1 调用的发起:用户空间

用户侧应用程序在查询某 IO 事件时,poll() 是可选的接口之一。以读取输入事件的代码为例:

struct pollfd pfd;
int timeout;
int ready;pfd.fd = open("/dev/input/event4", O_RDONLY);
pfd.events = POLLIN; /* 等待读取数据 */
timeout = -1; /* 超时时间,单位为毫秒。负数值表示数据就绪前一直等待 */
ready = poll(pfds, nfds, timeout);
if (ready > 0) { /* 等待的数据就绪 *//* 从 @fd 取数据进行处理... */
}

3.2 调用的过程:内核空间

3.2.1 设备的打开过程

打开输入事件文件内核空间过程:

sys_open("/dev/input/event4", O_RDONLY)...joydev_open()struct joydev *joydev =container_of(inode->i_cdev, struct joydev, cdev);struct joydev_client *client;client = kzalloc(sizeof(struct joydev_client), GFP_KERNEL);client->joydev = joydev;joydev_attach_client(joydev, client);joydev_open_device(joydev);file->private_data = client;nonseekable_open(inode, file);

3.2.2 将进程放入设备的 poll 等待队列

/** @ufds: 在其上等待数据的文件句柄列表;* @nfds: @ufds 列表长度;* @timeout_msecs: 超时时间,单位为毫秒。*/
sys_poll(ufds, nfds, timeout_msecs)struct poll_wqueues table;struct timespec64 end_time, *to = NULL;/* 计算超时结束时间点 */if (timeout_msecs >= 0) {to = &end_time;poll_select_set_timeout(to, timeout_msecs / MSEC_PER_SEC,NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));}ret = do_sys_poll(ufds, nfds, to)struct poll_wqueues table;/* 优先使用内核栈 @stack_pps 存放 pollfd 列表 @ufds ,如果 stack_pps[] 空间不够,剩余的 pollfd 存放到内核堆上 */long stack_pps[POLL_STACK_ALLOC/sizeof(long)];struct poll_list *const head = (struct poll_list *)stack_pps;struct poll_list *walk = head; /* 首先用内核栈 @stack_pps[] 存放 pollfd */unsigned long todo = nfds; /* 总共待安置的 pollfd *//** 1. 建立 poll_list 列表。* 将用户空间传递的长度 @nfds 的 pollfd 列表 @ufds , * 放入各 walk (poll_list) 中,其中第1个 walk 使用内* 核栈空间 @stack_pps ,剩余的 walk 都是从内核堆分* 配的1个页面空间。*			   poll_list                      poll_list*			 -------------                ------------------* head --> |     next    | --> ... -->  |       next       | --> NULL*          |-------------|              |------------------|*          |     len     |              |       len        |*          |-------------|              |------------------|*          |  entries[]  |              |    entries[]     |*          | (ufds[0,i]) |              | (ufds[j,nfds-1]) |*           -------------                ------------------*/len = min_t(unsigned int, nfds, N_STACK_PPS);for (;;) {walk->next = NULL;walk->len = len; /* 当前 walk 放置的 pollfd */if (!len)break;if (copy_from_user(walk->entries, ufds + nfds-todo,sizeof(struct pollfd) * walk->len))goto out_fds;todo -= walk->len; /* 剩余待放置的 pollfd 个数 */if (!todo)break;len = min(todo, POLLFD_PER_PAGE); /* 计算下一 walk 待放置的 pollfd 个数 */size = sizeof(struct poll_list) + sizeof(struct pollfd) * len; /* 计算下一 walk 待放置的 pollfd 空间大小 */walk = walk->next = kmalloc(size, GFP_KERNEL); /* 从内核堆分配一个页面 */if (!walk) {err = -ENOMEM;goto out_fds;}}/** 2. * 初始化 poll 等待队列(struct poll_wqueues):* 设置将进程放入 poll 等待队列的回调接口 __pollwait() ,* 然后驱动设备的 poll 接口,通过 poll_wait() 间接的调* 用 __pollwait(), 将进程放置到驱动自身的等待队列。*/poll_initwait(&table)init_poll_funcptr(&pwq->pt, __pollwait)pt->_qproc = qproc;pt->_key   = ~0UL; /* all events enabled */pwq->polling_task = current;pwq->triggered = 0;pwq->error = 0;pwq->table = NULL;pwq->inline_index = 0;/* 3. 调用设备驱动的 poll 接口: 以 poll 输入设备为例 */do_poll(head, &table, end_time)/* 计算剩余的时间 */if (end_time && !timed_out)slack = select_estimate_accuracy(end_time);for (;;) {struct poll_list *walk;/** 遍历所有的 walk 中 pollfd.*/for (walk = list; walk != NULL; walk = walk->next) {struct pollfd * pfd, * pfd_end;pfd = walk->entries;pfd_end = pfd + walk->len;for (; pfd != pfd_end; pfd++) {/* 调用驱动 poll 接口: 如 joydev_poll() */if (do_pollfd(pfd, pt, &can_busy_loop, busy_flag)) {count++; /* 当前 pollfd 成功, 计数加1 */pt->_qproc = NULL;...}}...}pt->_qproc = NULL;if (!count) {count = wait->error;if (signal_pending(current))count = -EINTR; /* 因进程由挂起的信号,中断 poll() 系统调用 */}if (count || timed_out) /* 有 pollfd 的 poll 操作成功 或 超时 */break;.../** poll 失败 && 超时时间还未到达, 进入睡眠等待。* 然后再以下两种情形被唤醒:* . 驱动侧当有数据到达时, 调用 wake_up_poll() 唤醒进程;* . 超时时间到达, 唤醒进程, 此时, 还会再尝试一轮 poll.*/if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack))timed_out = 1;}/* 返回成功 poll 计数 */return count;/* 4. 释放 poll 等待队列(struct poll_wqueues) */poll_freewait(&table);/* 5. 设置 poll 结果 */for (walk = head; walk; walk = walk->next) {struct pollfd *fds = walk->entries;int j;for (j = 0; j < walk->len; j++, ufds++)if (__put_user(fds[j].revents, &ufds->revents))goto out_fds;}/* 6. 设置 poll 成功的设备 fd 数量 */err = fdcount;
out_fds:/* 7. 释放 1. 中建立的 poll_list */walk = head->next;while (walk) {struct poll_list *pos = walk;walk = walk->next;kfree(pos);}return err; /* 返回 poll 成功的设备 fd 数量 */

看一下具体设备驱动的 poll 过程分析:

/* 上接 do_pollfd() */
static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait,bool *can_busy_poll,unsigned int busy_flag)
{int fd = pollfd->fd;struct fd f = fdget(fd);mask = DEFAULT_POLLMASK;pwait->_key = pollfd->events|POLLERR|POLLHUP;pwait->_key |= busy_flag;/* 调用驱动设备的 poll 接口:如 joydev_poll() */mask = f.file->f_op->poll(f.file, pwait)joydev_poll()poll_wait(file, &joydev->wait, wait)/* 将进程放入 poll 等待队列 @wait_address */p->_qproc(filp, wait_address, p) = __pollwait()struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);/* 分配1个 poll_table_entry: 用于将进程放置到等待队列的表项 */struct poll_table_entry *entry = poll_get_entry(pwq);entry->filp = get_file(filp); /* 关联的设备文件句柄 */entry->wait_address = wait_address; /* 关联的等待队列 */entry->key = p->_key; /* pollfd->events | POLLERR | POLLHUP *//* * 设置被唤醒时调用的接口 pollwake(): * 由 poll timeout 超时时触发, 或者驱动数据就绪时调用 wake_up_poll() 触发.*/init_waitqueue_func_entry(&entry->wait, pollwake);entry->wait.private = pwq; /* 私有数据 */add_wait_queue(wait_address, &entry->wait); /* 将进程放置到设备的 poll 等待队列 */if (mask & busy_flag)*can_busy_poll = true;/* Mask out unneeded events. */mask &= pollfd->events | POLLERR | POLLHUP;fdput(f);
}

3.2.3 设备数据就绪唤醒 poll 等待队列中的进程

以输入设备事件为例:

joydev_event().../* 输入设备有输入事件来临,唤醒睡眠在设备 poll 等待队列的进程 */wake_up_interruptible(&joydev->wait)

3.3 调用的返回

经由 sys_poll() 系统调用,因请求的设备数据未就绪、而陷入设备 poll 等待队列的进程,在设备数据就绪后,从系统调用 sys_poll() 返回。本来对于 sys_poll() 的返回流程没有什么好说的,但下面的代码返回片段,经常给人带来困惑:

sys_poll()...ret = do_sys_poll(ufds, nfds, to);if (ret == -EINTR) { /* sys_poll() 因信号而中断 */struct restart_block *restart_block;restart_block = &current->restart_block;restart_block->fn = do_restart_poll;restart_block->poll.ufds = ufds;restart_block->poll.nfds = nfds;if (timeout_msecs >= 0) {restart_block->poll.tv_sec = end_time.tv_sec;restart_block->poll.tv_nsec = end_time.tv_nsec;restart_block->poll.has_timeout = 1;} elserestart_block->poll.has_timeout = 0;/** 从这个返回值,可能会经常以为系统调用会自动发起!* 但实际情况往往并非如此,至少在 ARM 平台不会自动* 重新发起 poll() 调用。*/ret = -ERESTART_RESTARTBLOCK;}return ret;	

我们看 ARM 平台对于因信号中断的系统调用是怎么处理的:

do_work_pending()if (thread_flags & _TIF_SIGPENDING) { /* 挂起信号导致系统调用的中断 */int restart = do_signal(regs, syscall)unsigned int retval = 0, continue_addr = 0, restart_addr = 0;int restart = 0;if (syscall) {continue_addr = regs->ARM_pc; /* 紧跟发起系统调用的 swi 指令的下一条指令的地址 */restart_addr = continue_addr - (thumb_mode(regs) ? 2 : 4); /* 如果是返回用户间后,再重新发起系统调用,要将 PC 重新指向 swi 指令 */retval = regs->ARM_r0; /* 系统调用返回值 */switch (retval) {case -ERESTART_RESTARTBLOCK: /* 系统调用返回 ERESTART_RESTARTBLOCK */restart -= 2;...restart++;/* * 由于 R0 已经覆写为系统调动的返回值,我们用在进入系统调用进入内核空间时,* 重复保存的 R0 (系统调用的第1个参数) 来恢复系统调用的第1个参数。*/regs->ARM_r0 = regs->ARM_ORIG_r0;/* 返回用户空间后,重新发起系统调用: 将 User 模式的 PC 重新指向 swi 指令 */regs->ARM_pc = restart_addr;break;}if (get_signal(&ksig)) { /* 取出一个挂起的信号 */if (unlikely(restart) && regs->ARM_pc == restart_addr) {if (retval == -ERESTARTNOHAND ||retval == -ERESTART_RESTARTBLOCK|| (retval == -ERESTARTSYS&& !(ksig.ka.sa.sa_flags & SA_RESTART))) {/* 所以即使 poll() 因信号中断时,设* 置了 restart_block ,且返回了 * -ERESTART_RESTARTBLOCK 了错误码,* ARM 依然将错误码重置为了 -EINTR ,同时也还是从系统调用发起的位置之后继续执行!*/regs->ARM_r0 = -EINTR;regs->ARM_pc = continue_addr;}}}if (unlikely(restart)) {/** Restart without handlers.* Deal with it without leaving* the kernel space.*/return restart; /* 如果走到这里,会重新发起重新调用 */}}

这里返回流程涉及了系统调用和信号处理的细节,可以分别参考博文:
Linux系统调用实现简析
Linux信号处理简析
进行了解。

4. 番外

如果想了解 select() 的实现,可以参考本篇对 poll() 实现的解析,因为它们的实现,有大部分逻辑是相似的。

5. 参考资料

man poll()


http://www.ppmy.cn/news/12757.html

相关文章

React脚手架+组件化开发+组件生命周期+组件通信

react脚手架&#xff08;create-react-app&#xff09; 1.作用&#xff1a; 帮助我们生成一个通用的目录结构&#xff0c;并且已经将我们所需的工程环境配置好 2.依赖环境 脚手架都是使用node编写的&#xff0c;并且都是基于webpack的&#xff1b; 3.安装node 4.安装脚手架 n…

【web安全】——HTTP请求头注入

作者名&#xff1a;Demo不是emo主页面链接&#xff1a; 主页传送门创作初心&#xff1a; 舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座右…

【墙角数枝梅,凌寒独自开】代码改变未来

墙角数枝梅&#xff0c;凌寒独自开 “墙角数枝梅&#xff0c;凌寒独自开。” 出自王安石的《梅花》 诗句是集语言中的精华&#xff0c;浓缩为七言、五言和四言等&#xff0c;寥寥几句道尽人生酸甜苦辣&#xff0c;儿女情长。 而我更愿把现代的程序员称作诗人&#xff0c;语言是…

《Linux Shell脚本攻略》学习笔记-第十二章

12.1 简介 我们可以通过关闭无用的服务、调整内核参数或是添加新的硬件来改善系统性能。 12.2 识别服务 Linux系统可以同时运行数百个任务&#xff0c;其中可能也会有那么一两个你不需要的守护进程。 有三种可以用于启动守护进程和服务的工具&#xff0c;Linux发行版支持其中任…

亚马逊云科技 Build On - 毫厘控制传统行业快速搭建部署智能业务这件事“钱花在刀刃上”——迎云计算更高一层精专形态的下一个时代Serverless

/*本文章总计6052词&#xff0c;可能需要16分钟进行阅读&#xff0c;创作时间近1周&#xff0c;尽量将Serverless干货打包满满&#xff0c;并尝试向您解释清楚。 无论是路过还是看得乐呵的看官姥爷只求能给个&#x1f44d;点赞和⭐收藏&#xff0c;谢谢您嘞❤ P.S.CSDN侧边可以…

Rancher 2022 关键主题与新年展望

作者简介 张智博&#xff0c;SUSE Rancher 大中华区研发总监&#xff0c;一直活跃在研发一线&#xff0c;经历了 OpenStack 到 Kubernetes 的技术变革&#xff0c;在底层操作系统 Linux、虚拟化 KVM 和 Docker 容器技术领域都有丰富的研发和实践经验。 以 Rancher 为核心的 SUS…

c++11 标准模板(STL)(std::forward_list)(十)

定义于头文件 <forward_list> template< class T, class Allocator std::allocator<T> > class forward_list;(1)(C11 起)namespace pmr { template <class T> using forward_list std::forward_list<T, std::pmr::polymorphic_…

机器学习的相关概念与建模流程

文章目录一、机器学习简介1. 机器学习的相关定义2. 一次简单的机器学习任务&#xff1a;鸢尾花分类二、数据与数据集相关概念1. 数据与数据集2. 特征与标签3. 连续变量和离散变量4. 模型类型三、机器学习建模一般流程1. 提出基本模型2. 确定损失函数3. 根据损失函数性质&#x…