Linux IO: 系统调用 poll() 实现简析
admin
2024-05-13 04:36:54
0

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) { /* 等待的数据就绪: ready 的值为就绪的 fd 数量 *//* 从 @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[] 空间不够存放所有 @ufds ,则从内核堆* 分配页面,存放剩余的 @ufds 。*/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[] 存放 @ufds 中的 pollfd */unsigned long todo = nfds; /* 总共待安置的 pollfd *//** 1. 建立 poll_list walk 列表。* 将用户空间传递的长度 @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); /* 计算第1个 walk 内存放的 pollfd 数目 */for (;;) {walk->next = NULL;walk->len = len; /* 当前 walk 放置的 pollfd */if (!len)break;/* 拷贝用户空间 pollfd 到内核当前 walk 空间 */if (copy_from_user(walk->entries, ufds + nfds-todo,sizeof(struct pollfd) * walk->len))goto out_fds;todo -= walk->len; /* @ufds 内剩余待放置的 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); /* 从内核堆分配一个页面,作为下一 walk 空间 */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_XXX() 接口触发.*/init_waitqueue_func_entry(&entry->wait, pollwake);entry->wait.private = pwq; /* 私有数据:poll 等待队列项,所在的等待队列 poll_wqueues */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)...pollwake()struct poll_table_entry *entry;entry = container_of(wait, struct poll_table_entry, wait);/* 指定事件类型(POLLIN 等)没有发生, 则不做唤醒动作 */if (key && !((unsigned long)key & entry->key)) return 0;__pollwake(wait, mode, sync, key)struct poll_wqueues *pwq = wait->private;pwq->triggered = 1; /* 标记 poll 等待队列 poll_wqueues 中有数据就绪 */default_wake_function(&dummy_wait, mode, sync, key) /* 唤醒进程 */

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 = ¤t->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()

相关内容

热门资讯

海南自贸港“样板间”抢抓开放机... 中新网海口5月16日电 (记者 王子谦)洋浦经济开发区是海南自贸港“样板间”,也是外界观察自贸港建设...
净利增速2.98%,违规频发!... 近期,中信银行2025年年报与2026年一季报接连公布,报告显示,中信银行总资产站稳10万亿元台阶,...
原创 放... 全网的人几乎都在挤破头往海外大都市扎,可有一个女博主,却偏偏反着来。她拥有五百多万粉丝,本可以继续在...
原创 在... 在中国,买卖虚拟货币,到底行不行? 这个问题,很多人心里都犯嘀咕。有人说,法无禁止即可为;也有人说,...
龙粤慈善事业高质量发展与互联网... 近日,为加快培育数字慈善新生态,助力“善行边疆”活动走深走实,“龙粤慈善事业高质量发展与互联网公开募...
黄金大局已定:不出意外的话,2... 在投资领域,贵金属一直是备受关注的资产类别,尤其是黄金,其价格走势和投资价值牵动着无数投资者的心。随...
后巴菲特时代,伯克希尔哈撒韦新... 【导读】伯克希尔哈撒韦最新持仓公布!清仓亚马逊,建仓达美航空 中国基金报记者 张舟 伯克希尔哈撒韦“...
布朗46分胡金秋20+8 广厦... 【搜狐体育战报】北京时间5月16日CBA季后赛,主场作战的浙江浙商证券以111-102击败深圳马可波...
美联储任命鲍威尔担任临时主席 美国联邦储备委员会理事会5月15日发布公告,任命杰罗姆·鲍威尔担任美联储临时主席,直至凯文·沃什宣誓...
李从悠:白癜风患者,夏季防汗疹... 夏季高温多雨,白癜风患者皮肤屏障受损,出汗后汗液无法及时蒸发,易堵塞毛孔,诱发汗疹(热疹),汗疹引发...
最低涨价60元!4款非标茅台酒... 在飞天茅台涨价之后,部分非标茅台酒也提了价。 5月16日早间,贵州茅台自营渠道i茅台发布公告,宣布对...
邯郸10亿共享智造基金落地,撬... 图片为AI生成 据天眼查App显示,近日邯郸市共享智造股权投资基金(有限合伙)正式登记成立,总出资额...
AI制药行业深度:行业概况、市... 一、AI制药行业概况 1、AI药物研发概述 AI制药是指将NLP、深度神经网络,生成模型等AI技...
世界杯在即:国产彩电的出海故事... 球还没看,彩电先破防了 撰文/ 孟会缘 编辑/ 陈邓新 排版/ Annalee 国产彩电品牌,正深陷...
医疗健康领域投融资日报(5月1... 据亿欧数据统计,昨日(2026年5月15日)共披露16起投融资事件,涉及15家国内企业,1家国外企业...
深圳中创商业咨询携手海旗控股集... 海旗控股集团旗下宁波锦曼程新材料有限公司,自创立以来始终深耕高分子材料领域,秉承推动行业创新与可持续...
原创 关... 前言 大家好,我是老金。 国际地缘博弈的棋盘上,从来没有绝对的秘密,只有刻意或无意的战略试探,近期...
原创 欧... 今天来给大家聊一下最近的欧盟,自从特朗普说要来访华,欧洲的动作有点让人看不懂。从四月中旬到五月初,欧...
心系投资者 携手共行动 ——人... 为落实监管工作要求,切实维护金融消费者合法权益,在 “5・15 全国投资者保护宣传日” 当天,人保寿...
黄仁勋打卡蜜雪冰城 同款产品销... 财联社5月16日讯(记者 沈娇娇)5月15日上午,英伟达CEO黄仁勋现身北京南锣鼓巷,并且进入一家蜜...