ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux 内核 poll_wait 等待队列完整指南:从一次 poll() 调用看懂字符设备 I/O 多路复用

2026/8/29 8:37:40 拓冰建站 浏览量
Linux 内核 poll_wait 等待队列完整指南:从一次 poll() 调用看懂字符设备 I/O 多路复用 Linux 内核 poll_wait 等待队列完整指南从一次 poll() 调用看懂字符设备 I/O 多路复用【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux你的设备明明到了数据进程却还在睡或者用户态拿poll(0)死循环单核被打满。两个病根多半都出在 Linux 内核 poll_wait 等待队列这条链路上没接对。⚙️ 机制全貌三个对象各管一段这套机制里只有三方角色互不重叠wait_queue_head_t——驱动自己的资产。驱动初始化时用DECLARE_WAIT_QUEUE_HEAD建好生命周期跟设备走里面挂的是所有在等这个设备的进程。poll_table——内核塞给你的临时票据。用户态调poll()时fs/select.c里的do_sys_poll会构造一个poll_wqueues把它的_qproc指向内核的__pollwait再经vfs_poll调到你驱动的f_op-poll。你的回调里调一次poll_wait(file, my_wait, wait)内核就把一个poll_table_entry挂进你的队列头并把该节点的唤醒函数绑成pollwake。wake_up 系列——驱动自己事件路径里的敲钟人。事件发生时先置就绪标志再调wake_up_interruptible(my_wait)内核顺着队列逐个调用 entry 的唤醒函数。poll_wait是驱动侧唯一的入口定义在include/linux/poll.h// include/linux/poll.h static inline void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p) { if (p p-_qproc) { p-_qproc(filp, wait_address, p); smp_mb(); /* 与 wq_has_sleeper() 成对防止检查被重排 */ } }一句话记忆驱动负责建队列、置标志、敲钟内核负责登记、过滤、真正唤醒poll_wait是两者的唯一握手点。 一次 poll() 调用的完整生命周期走读用户态poll(fds, 1, timeout)进内核do_sys_poll构造poll_wqueues经vfs_poll调到你注册的 poll 回调。回调第一行poll_wait(file, my_wait, wait)_qproc即__pollwait它分配poll_table_entry、把唤醒函数设为pollwake加进你的队列头。尾部那条smp_mb()保证检查标志不会被重排到挂队列之前。驱动检查就绪标志返回 mask。mask 非零do_sys_poll直接返回mask 为 0 且允许等待poll_schedule_timeout把进程置为可睡眠并schedule()。设备事件到达驱动置位标志后调wake_up_interruptible(my_wait)。wake_up遍历队列调pollwake按事件掩码过滤后交__pollwake收尾// fs/select.c static int __pollwake(wait_queue_entry_t *wait, unsigned mode, int sync, void *key) { struct poll_wqueues *pwq wait-private; smp_wmb(); /* 与睡眠侧屏障配对 */ WRITE_ONCE(pwq-triggered, 1); /* 标记事件命中 */ return default_wake_function(dummy_wait, mode, sync, key); }进程被重新调度后do_sys_poll主循环看到triggered组装revents交还用户态你的read()这时才去取数据。驱动侧回调长这样drivers/scsi/mpt3sas/mpt3sas_ctl.cstatic __poll_t _ctl_poll(struct file *filep, poll_table *wait) { poll_wait(filep, ctl_poll_wait, wait); // 登记本次等待 spin_lock(gioc_lock); list_for_each_entry(ioc, mpt3sas_ioc_list, list) { if (ioc-aen_event_read_flag) { spin_unlock(gioc_lock); return EPOLLIN | EPOLLRDNORM; // 有 AEN 事件可读 } } spin_unlock(gioc_lock); return 0; // 无事件内核会安排睡眠 } 内核真实实现对比队列头放哪、标志放哪同样是标准 poll_wait 等待队列接法两个子系统的取舍完全不同drivers/hid/hidraw.cdrivers/scsi/mpt3sas/mpt3sas_ctl.c队列头作用域每个struct hidraw设备一份全局static一份服务所有适配器事件标志环形缓冲区head ! tail每个适配器的aen_event_read_flag标志读取无锁比较两个索引持gioc_lock自旋锁遍历链表唤醒函数wake_up_interruptiblewake_up_interruptible// drivers/hid/hidraw.cURB 回调把数据塞进环形缓冲后 poll_wait(file, list-hidraw-wait, wait); if (list-head ! list-tail) // 缓冲非空即可读 mask | EPOLLIN | EPOLLRDNORM;// drivers/scsi/mpt3sas/mpt3sas_ctl.c事件处理路径置位后 wake_up_interruptible(ctl_poll_wait); // 唤醒所有等待者对比结论单设备驱动学 hidraw按设备建队列需要多个适配器共享一个通知通道的学 mpt3sas全局队列加锁扫描。唤醒函数选型上用户进程队列用_interruptible变体即可同一队列有多个消费者时再考虑wake_up_interruptible_nr。⚠️ 高频踩坑现象 → 根因 → 修法坑一唤醒风暴。现象一个事件到来N 个 poll 同一设备的进程全部被唤醒空跑只有一个真正消费。根因wake_up会遍历整条队列全量唤醒。修法wake_up_interruptible_nr(wait, 1)只叫醒一个其余由消费端的互斥锁仲裁后再睡回去。坑二丢失唤醒。现象进程偶发永远睡到超时。根因poll 检查完无事件、还没挂队列事件恰好触发并wake_up此时队列里没人标志又已置位进程睡下去后无人再唤醒。修法poll 里的标志检查与事件路径的置位、唤醒走同一把自旋锁或改用prepare_to_waitwq_has_sleeper()惯用法绝不在锁外两次裸查标志。坑三假就绪。现象poll 返回POLLINread()却拿到 0 字节或-EAGAIN。根因poll 和 read 判定就绪用的不是同一份数据或同一把锁两边结论打架。修法poll 的就绪判定做成纯查询与 read 读同一环形缓冲、持同一把锁。坑四忘了调 poll_wait。现象事件从不触发一律等超时。根因进程压根没进任何等待队列你的wake_up敲的是空队列。修法poll 回调第一行必须是poll_waitcode review 时这是必查项。 排查工具箱定位唤醒问题cat /proc/pid/wchan→ 看进程卡在内核哪个函数卡在schedule即睡在等待队列echo 1 /sys/kernel/tracing/events/wait/wakeup/enable→ ftrace 开 wait/wakeup 事件看清谁唤醒了谁bpftrace -e kprobe:default_wake_function { [comm] count(); }→ 按进程统计唤醒次数唤醒风暴一眼现形cat /proc/pid/syscall→ 确认进程是否仍停在 poll、参数处于什么状态 速查清单字符设备 poll 回调必须项队列头用DECLARE_WAIT_QUEUE_HEAD声明按设备建或全局建明确写出并发模型poll 回调第一行调poll_wait(file, wait, wait)事件路径先置标志锁内、后wake_up_interruptiblepoll 与 read 用同一份就绪数据、同一把锁杜绝假就绪多消费者场景用wake_up_interruptible_nr(wait, 1)限流自测时用 ftrace wait/wakeup 事件验证登记 → 睡眠 → 唤醒整链走通【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考