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()

相关内容

热门资讯

斗金订购APP贵金属期货投资被...   斗金订购APP的投资者被广告宣传给诱导,注册就送什么现金,然后充值返现金卷等等这些宣传方式,都是...
哈易购APP非法期货交易欺骗投...   哈易购APP宣传可做白银铂金贵金属订购交易,但实际上并没有取得相关交易资质!哈易购APP本质上就...
消息称百度旗下昆仑芯瞄准500... 6 月 29 日消息,据《The Information》昨日援引知情人士消息,百度旗下 AI 芯片...
打造夏日消费新场景 第35届北... 北京商报讯(记者 翟枫瑞)6月29日消息,第35届北京国际燕京啤酒文化节新闻发布会在京举行。本届啤酒...
社保基金持仓数据出炉,一季度增... 最近各大上市公司一季度财报都公开了,咱们国家社保基金的持仓数据也全部曝光。目前社保拿着比亚迪价值44...
36氪首发 | 海思、中兴团队... 作者 | 乔钰杰 编辑 | 袁斯来 硬氪获悉,广州宸思通讯科技有限公司(以下简称“宸思科技”)近日完...
两天蒸发47亿市值!一纸税务通... 一纸税务通知书,能让一家百亿龙头两天蒸发47亿市值。 6月22日,北大荒(600598.SH)公告称...
SK海力士将投资1100万亿韩... SK集团会长崔泰源6月29日在韩国“三大重大计划”发布会上宣布,公司将投资1100万亿韩元扩大半导体...
两只A股,终止上市! 两家A股公司,即将摘牌。 6月29日,退市沪科(600608.SH)公告称,上海证券交易所将在202...
原创 M... 一家成立近十年的自动驾驶公司,在IPO时吸引了14家基石投资者认购近一半的发行股份,其中不乏奔驰、比...
基金忠言|国寿安保滤镜碎,三年... 图片来源:视觉中国 蓝鲸新闻6月29日讯(记者 祁和忠)保险系基金公司国寿安保总经理换人了。 6月2...
三星电机计划加码玻璃基板!相关... 6月29日,玻璃基板概念股午后有所回升, 华工科技(000988.SZ)逼近涨停, 彩虹股份(600...
拉萨海关持续壮大外贸经营主体 ...   新华网拉萨6月28日电(记者蒋梦辰)近日,记者从拉萨海关获悉,今年前5个月,西藏有进出口实绩的外...
机构:二季报临近,医药生物板块... 6月29日,华源证券发布了一篇医药生物行业的研究报告,报告指出,业绩期临近,产业链景气度有望再次迎来...
每日收评科创50放量涨超4.5... 财联社6月29日讯,三大指数全线收红,创业板指探底回升,科创50指数大涨4.61%。沪深两市成交额3...
6月多地土拍结构性升温:深圳单... 进入2026年6月,不少城市核心区地块集中诞生高溢价宗地,热度突出的城市包含深圳、杭州、长沙。 其中...
业绩炸裂!盛达资源半年预盈3.... 6月29日,贵金属矿山龙头盛达资源(000603.SZ)发布 2026 年半年度业绩预告,上半年业绩...
A股午后拉升三大股指收涨:半导... A股三大股指6月29日开盘涨跌互现。早盘沪强深弱,创指一度跌超2%。半导体午后拉升,带动两市上涨,沪...
原创 空... 前言 大家好,我是老金。 这几天,两幅极度割裂的画面放在一起,把我看笑了。 一边是在持续的热浪下,欧...