poll_wait 到底干了什么:一次讲透 Linux 内核 poll 等待机制的 5 个关键零件
poll_wait 到底干了什么一次讲透 Linux 内核 poll 等待机制的 5 个关键零件【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux你在 Linux 内核Linux kernel里写了一个字符设备驱动数据偶尔才到一次。最朴素的做法是用户态死循环read()但这样 CPU 被白白烧掉设备没数据时全是空转。真正的解法就藏在include/linux/poll.h的poll_wait()里——驱动把自己挂在等待队列上没事件时进程直接睡死数据一到由内核主动把它叫醒。这篇带你把这条链路从头到尾拆一遍看完你能在白板上复述出poll()系统调用如何变成一次进程睡眠、又如何被wake_up()精准唤醒。没有 poll_wait vs 有了 poll_wait没有 poll_wait忙轮询有了 poll_waitCPU 开销无事件时循环空转一个核打满无事件时进程挂起CPU 归别人用响应延迟取决于轮询间隔事件可能错过一个周期事件发生当拍被唤醒驱动要写的代码用户态自己while(1)只实现一个poll回调 事件时一行wake_up()多设备监控每个设备一个线程或轮流查一次poll()同时挂 N 个 fd收益就一句话把反复问换成有事叫我。一次完整调用旅程先看图再走编号步骤。用户态发起应用调poll(fds, 1, timeout)陷入内核入口do_sys_poll()fs/select.c。准备工具箱内核构造一个poll_wqueues结构里面有个poll_table并把_qproc函数指针指向__pollwait——这是整个机制的挂钩器。逐个 fd 注册对每个 fd 调do_pollfd()→vfs_poll()include/linux/poll.h最终进入驱动实现的f_op-poll(file, pt)。驱动挂钩 报状态驱动的poll回调里第一件事调poll_wait(file, waitq, wait)把当前进程的等待项add_wait_queue()进驱动自己的等待队列然后检查数据是否就绪返回掩码比如EPOLLIN | EPOLLRDNORM。有事件就返回do_poll()发现任何 fd 的掩码非 0立刻把结果填回revents返回用户态根本不睡觉。没事件就睡全 0 时走poll_schedule_timeout()先set_current_state(TASK_INTERRUPTIBLE)再看triggered标志没人置位就schedule_hrtimeout_range()睡到超时或唤醒。事件到达驱动在数据到达的中断/回调路径里调wake_up_interruptible(waitq)等待队列逐个执行每个等待项的pollwake()回调。精准唤醒pollwake()先按entry-key过滤——只关心POLLIN的进程不会因为POLLOUT被叫醒命中就置pwq-triggered 1再default_wake_function()把进程状态改回可运行第 6 步的triggered检查因此短路进程回到第 5 步重查状态并返回。拆开看零件只挑最关键的三块poll_table、__pollwait、pollwake。1.poll_table驱动回调收到的钩子参数include/linux/poll.h 第 37 行typedef struct poll_table_struct { poll_queue_proc _qproc; /* 挂钩函数指向 __pollwait */ __poll_t _key; /* 用户关心的事件掩码 */ } poll_table;为什么这么设计驱动不可能自己创建poll_table——它是poll()系统调用每次运行时的临时对象由do_poll()栈上构造、用完即弃。驱动只拿到一个参数poll_table *wait通过_qproc间接函数调用内核才能决定挂到哪个队列、唤醒时用哪个回调。_key则让驱动知道用户在等什么poll_requested_events()据此做懒启动比如用户只 poll 写就别提前启动 DMA 读。2.poll_wait()__pollwait()注册的真正动作fs/select.c 第 219 行static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p) { struct poll_wqueues *pwq container_of(p, struct poll_wqueues, pt); struct poll_table_entry *entry poll_get_entry(pwq); ... entry-filp get_file(filp); /* 持有 file 引用防释放 */ entry-wait_address wait_address; /* 记驱动队列头便于注销 */ entry-key p-_key; /* 记下关心的事件唤醒时过滤 */ init_waitqueue_func_entry(entry-wait, pollwake); add_wait_queue(wait_address, entry-wait); /* 挂进驱动队列 */ }每挂一个 fd 就分配一个poll_table_entry内含一个wait_queue_entry_tprivate字段回指poll_wqueues——唤醒时靠它找到该叫醒哪个进程。注意get_file()驱动回调返回后poll()可能随时结束不 pin 住file就有 UAF。3.pollwake()唤醒时的双向过滤fs/select.c 第 208 行static int pollwake(wait_queue_entry_t *wait, unsigned mode, int sync, void *key) { struct poll_table_entry *entry; entry container_of(wait, struct poll_table_entry, wait); if (key !(key_to_poll(key) entry-key)) return 0; /* 事件类型不匹配不唤醒 */ return __pollwake(wait, mode, sync, key); }这里就是惊群被压住的地方驱动队列上挂着 100 个等待者但只有entry-key和本次wake_up(..., key)事件类型有交集的那几个会被真正default_wake_function()叫醒。__pollwake里置triggered 1配一条smp_wmb()与睡眠侧poll_schedule_timeout()里的smp_store_mb(triggered, 0)配对堵住事件在挂队列前一刻发生的经典竞态窗口。跟一段真实源码hidraw 驱动选 HID 原始设备/dev/hidrawN键盘鼠标类输入设备的用户接口drivers/hid/hidraw.c它短小且把三个零件都齐了poll 回调drivers/hid/hidraw.c 第 263 行static __poll_t hidraw_poll(struct file *file, poll_table *wait) { struct hidraw_list *list file-private_data; __poll_t mask EPOLLOUT | EPOLLWRNORM; /* hidraw 恒可写 */ poll_wait(file, list-hidraw-wait, wait); /* 挂钩挂到 hidraw-wait */ if (list-head ! list-tail) /* 环形队列有数据 */ mask | EPOLLIN | EPOLLRDNORM; if (!list-hidraw-exist || hidraw_is_revoked(list)) mask | EPOLLERR | EPOLLHUP; return mask; }读路径hidraw_read()同样先poll_wait()挂钩若head tail没数据就wait_event_interruptible()睡在同一个hidraw-wait上。唤醒路径HID 中断把数据塞进环形缓冲后对hidraw-wait执行wake_up_interruptible()——注意它不传key等于传任意事件此时pollwake()的过滤退化为全放行但同一队列上按key注册的 epoll/poll 等待者仍会被entry-key过滤一遍只叫醒真正关心可读事件的人。走一圈应用poll(POLLIN)→hidraw_poll挂钩并返回 0 →do_poll睡 → 鼠标移动触发 HID 中断 →wake_up_interruptible(hidraw-wait)→pollwake命中EPOLLIN→ 进程醒来do_poll重跑hidraw_poll此时head ! tail返回EPOLLIN | EPOLLRDNORMread()拿走数据。踩坑地图现象根因定位手段进程永远睡到超时事件明明发生了事件路径wake_up()的队列头与poll_wait()挂的不是同一个wait_queue_head_t在poll回调和wake_up处各打一行printk比对地址偶发数据到了但 poll 没醒wake_up发生在add_wait_queue之前且没走wq_has_sleeper()复查poll_wait末尾的smp_mb()依赖驱动在唤醒前调wq_has_sleeper()事件路径里补if (wq_has_sleeper(waitq)) recheck()后复测返回EPOLLIN后read()却拿到-EAGAIN或空poll回调检查的条件与read消费的数据源不一致检查的是别的缓冲区/标志位断点确认poll里判定的字段就是read读取的字段CPU 100% 且进程频繁被唤醒又睡下唤醒后条件被别的上下文立刻消费掉如另一个进程先读走形成唤醒-空转循环perf record -g看pollwake/schedule调用栈占比驱动崩溃在poll回调中未判空wait参数——poll()允许传NULL表示只查状态不挂钩回调首行if (wait) poll_wait(...)一页速查最小驱动骨架#include linux/poll.h static DECLARE_WAIT_QUEUE_HEAD(my_waitq); /* 驱动自己的等待队列头 */ static DEFINE_SPINLOCK(my_lock); static int data_ready; /* 事件标志中断里置 1 */ static __poll_t my_poll(struct file *file, poll_table *wait) { __poll_t mask 0; poll_wait(file, my_waitq, wait); /* 1) 先挂钩可传 NULL 吗wait 可为 NULL*/ spin_lock(my_lock); if (data_ready) mask | EPOLLIN | EPOLLRDNORM; /* 2) 再报状态 */ spin_unlock(my_lock); return mask; } static int my_irq_handler(void) { int val 0; spin_lock(my_lock); if (data_ready) { val 1; data_ready 0; /* 消费数据 */ } spin_unlock(my_lock); if (val) wake_up_interruptible(my_waitq); /* 3) 事件时唤醒 */ return 0; }速查表环节函数位置入口do_sys_poll→do_pollfs/select.c挂钩poll_wait→__pollwaitinclude/linux/poll.h、fs/select.c睡眠poll_schedule_timeoutfs/select.c唤醒pollwake→__pollwakefs/select.c驱动示例hidraw_polldrivers/hid/hidraw.c【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考