拓冰建站拓冰建站
首页 / 资讯中心 / 正文

一次 poll 调用的完整旅程:从 sys_poll 到等待队列,你该带走的 5 个结论

一次 poll 调用的完整旅程从 sys_poll 到等待队列你该带走的 5 个结论【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux你有没有写过这种代码打开一个设备节点然后在 while 里不停 read读到数据就干活读不到就再来一次。top 一看不对劲CPU 打满同事问你在干嘛你说在等数据。问题不在业务在于你根本没资格忙等——内核早就给了你 poll_wait 这条路驱动把进程挂上等待队列设备有动静时内核主动把你叫醒。读完这篇你能把 sys_poll 从进内核到被唤醒的每一步讲清楚。 餐厅叫号就是 poll_wait 的全部逻辑把一次 poll 想成去饭馆你不用守在灶台边看锅而是去前台取个号坐下等短信饭做好了后厨喊你的号。对应到内核里有三个角色。你是食客进程发起 sys_poll内核在它的等待队列上插一张号牌然后让它睡觉。驱动是后厨它手里攥着锅的状态有没有数据、缓冲区满没满。poll_table 是那张号牌上面写着三样东西哪个文件描述符、关心哪类事件可读还是可写、挂到了哪个队列头上。后厨每做一道菜就 broadcast 一次内核拿着号牌逐个核对只对号的人才发起立短信这就是 wake_up 的活。下面跟着一次真实的 sys_poll 走一遍全流程。一次 sys_poll 在内核里走了哪些路入口在 sys_poll真正干活的是 do_poll在 fs/select.c 里。它先干一件容易被忽略的事准备一张工作表poll_initwait 把唤醒回调注册进 poll_table这个回调的名字叫 __pollwait——记住它下一节所有 magic 都从它身上过。然后对列表里的每个文件描述符调 do_poll 内部循环里的 vfs_pollvfs_poll 在 include/linux/poll.h 中定义本质就是转发给驱动的 f_op-poll。也就是说内核通用层从不直接碰设备它只是替你敲门。驱动返回一个掩码比如 EPOLLIN表示现在可读。内核拿这个掩码和你 poll 时要求的 events 做与运算有交集就立刻返回你全程没睡过一个 bit 都没交集内核才把当前进程标成睡眠挂进刚才排好号的队列然后阻塞在定时器或超时上。这里有个细节值得咂摸__pollwait 每次被驱动调用一次就往表里追加一条记录每条记录独立挂到一个等待队列头上。所以你 poll 十个设备就有十条号牌、十个队列任何一个队列被唤醒内核都会回头检查其他九条号牌确认这件事你关不关心。这条链路走完你的进程就睡着了。叫醒它的下一站是驱动侧。驱动侧的两个动作先挂号再验锅以管道的实现 fs/pipe.c 里的 pipe_poll 为例它就是内核自己的标准答案。整个函数只有两步动作顺序却被源码注释用大字标了出来先把进程挂进 poll 表然后才允许读状态。if (filp-f_mode FMODE_READ) poll_wait(filp, pipe-rd_wait, wait); if (filp-f_mode FMODE_WRITE) poll_wait(filp, pipe-wr_wait, wait); /* 挂完号之后才能做下面这个有竞态的检查 */ idx.head_tail READ_ONCE(pipe-head_tail); if (!pipe_empty(idx.head, idx.tail)) mask | EPOLLIN | EPOLLRDNORM;为什么顺序不能反假设你先看锅锅是空的。还没来得及挂号写端叮一声写了一字节。你再挂号——数据永远在那但没人会再来叫你一次你睡死过去。反过来先挂号就算看锅和挂号之间真的漏了状态驱动那边一有动静就 wake_up你的号牌会被兜住。pipe.c 里那段注释说得很直白if something changes and you got it wrong, the poll table entry will wake you up and fix it。这是 poll_wait 的第一铁律。事件到来时的反向唤醒数据到达时驱动调 wake_up_interruptible(pipe-rd_wait) 这类函数内核遍历队列上所有号牌每块号牌都会走一遍 pollwakefs/select.c 中定义它从号牌里取出当年挂表时记下的 filp 和事件掩码拿这次醒来的原因和掩码做与运算对得上才把对应的进程从 TASK_INTERRUPTIBLE 翻成 TASK_RUNNING再走一遍现在到底有没有数据的验锅。对不上号的号牌只被摸一下就放下进程继续睡。这就是为什么惊群没那么可怕同一个队列上挂十个进程十个号牌但只有掩码匹配的才被真正唤醒干活。另外验锅不通过虚假唤醒时驱动返回 0用户空间的 poll 会重新挂进去睡这个睡回去的动作对用户完全透明。写一个驱动 poll 函数4 条硬规则先 poll_wait 后读状态顺序反了就有挂不到事件的窗口pipe.c 的注释就是现成教材。返回掩码与唤醒时机一致你什么时候会调 wake_up就什么时候在 mask 里给对应 bit两边对不上就是 bug。事件路径必须真的调 wake_up数据到了只改标志位不喊人等待者就永久睡眠。善用 wq_has_sleeper 和 wake_up_nr队列空了别白干重活想只叫醒一个处理者用 wake_up_nr 限个数而不是全喊。3 个真实翻车现场现象、根因、解法现场一进程睡死再也没醒。现象是 poll 卡住超时才返回dmesg 干干净净。根因九成是驱动的接收回调里改了数据缓冲区却忘了调 wake_up 系列函数——号牌挂上了后厨没喊号。解法在驱动里全局搜一遍 wake_up确认每个状态翻转点后面都跟了一次唤醒。现场二CPU 又打满了比忙等还惨。现象是进程频繁醒、验锅为空、再睡形成自激循环。根因是唤醒端和掩码端不一致驱动对写缓冲区变了一点点也 wake_up 读端读端被反复白醒。解法把唤醒条件收紧到真正产生可读数据或者按第 4 条限制唤醒数量。现场三一个队列上挂了一堆进程一醒全醒。现象是 N 个进程同时被调度只有一个真正干活。这是典型的被误伤放大。解法驱动用 wake_up_nr 只叫醒一个其余留到下轮内核的 pollwake 掩码比对会帮你挡掉大部分白醒挡不住的就得驱动自己收敛。回到那个 top 里的 100%现在再回头看你最初的场景把 while-read 换掉驱动里老老实实 poll_wait 加 wake_uptop 里那条曲线就平了。想继续挖按这个顺序看include/linux/poll.hpoll_wait 与 poll_table 的定义、fs/select.c__pollwait 与 pollwake 的实现、fs/pipe.c一份可以直接抄作业的驱动侧实现、include/linux/wait.h等待队列基础设施。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门