资讯详情 Linux字符设备驱动poll方法实现:从select到epoll的完整指南
📅 2026/10/18 22:05:12
1. 一个被select和epoll逼出来的驱动接口写字符设备驱动的人迟早会遇到一个尴尬场景用户态程序调了read()但设备数据还没准备好于是线程只能傻乎乎地阻塞在那儿。假如同时要等两个设备的数据问题就更麻烦了——总不能一个线程等一个设备吧这时候用户态会搬出select、poll、epoll这些IO多路复用机制把多个文件描述符交给内核统一监视。但关键问题来了内核凭什么知道你的设备什么时候可读、什么时候可写答案就落在file_operations结构体里那个经常被忽略的成员上——poll方法。我之前刚开始写驱动时也犯过糊涂以为只要实现了read和write就万事大吉结果用户态用select一测立刻傻眼select永远返回超时数据明明已经到缓冲区了read也明明能读到可select就是没有反应。排查到最后发现file_operations里压根没注册.poll回调。从那一刻起我就明白poll方法不是可选项而是驱动接入Linux IO多路复用体系的唯一入口。这篇内容适合正在写或准备写字符设备驱动的开发者尤其是被阻塞IO、非阻塞IO、多路复用这些问题绕晕的人。我会从内核源码的实际调用链讲起把poll方法的工作机制、实现步骤、避坑细节完整拆一遍最后用一个支持非阻塞读写的demo驱动把整个过程串起来。2. 内核把select传到驱动的那一刻发生了什么2.1 从用户态select到驱动poll的完整调用链先把这条调用链捋清楚。用户态调用select(fds, timeout)后内核会遍历fd集合对每个fd调用对应文件系统的poll回调。如果我们把一个字符设备fd传给select最终会走进这个设备的file_operations-poll。这条链路上的关键节点是这样的用户态select()→sys_select()内核VFS层do_select()→ 遍历fd → 调用struct file的f_op-poll(file, pt)驱动层你实现的.poll函数被调用其中需要调用poll_wait()把当前进程挂到设备的等待队列上并返回设备当前的状态掩码回到VFS层do_select()收集所有fd的状态掩码组织成用户态需要的fd_set结果你也许会问poll方法不是每时每刻都被调用的select内部有个循环do_select()会反复遍历fd集合每次遍历都会调用驱动的poll方法。第一次遍历拿到状态掩码发现没有数据可读就把进程挂进设备的等待队列。之后如果没有人唤醒这个进程select线程就会一直睡眠直到超时时间到了或者某个设备唤醒它然后它再重新轮询一遍。我在代码里实测过这样一个行为select的超时是绝对值但驱动返回的poll事件只会在遍历那一刻被采样。所以如果你不想让select的轮询行为太频繁可以在驱动里控制唤醒节奏而不是指望select那边做多少智能调度。2.2 poll方法在file_operations中的正确定义file_operations结构体里poll成员的原型是unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait);返回类型是unsigned int每一位都是一个事件标志位。常用的事件掩码在linux/poll.h中定义POLLIN0x0001设备有数据可读POLLRDNORM0x0040等价于POLLIN表示普通数据可读POLLOUT0x0004设备可写POLLWRNORM0x0100等价于POLLOUTPOLLERR0x0008设备发生错误POLLHUP0x0010设备被挂断注意标准里还有个POLLRDBAND和POLLWRBAND用于带外数据普通驱动用不到别乱加。关键点在于返回值不是随便组合的它必须真实反映当前这一刻设备的状态。比如你的驱动里有个环形缓冲区里面有100字节数据没被读走那这个时刻就应该返回POLLIN | POLLRDNORM。如果缓冲区空了你又希望select阻塞等待就不能返回这两个位而是调用poll_wait把进程挂到等待队列上等数据来时唤醒。曾经有个驱动因为偷懒在poll方法里不管缓冲区状态一律返回POLLIN结果用户态select立即返回配合非阻塞read直接跑成100%CPU忙轮询。这类问题定位起来很快但如果不了解poll的语义往往要折腾好一阵。2.3 poll_table和poll_wait到底干了什么poll方法的第二个参数是struct poll_table_struct *wait这个参数不能忽略因为它是让select和epoll能够睡眠等待的关键。一个常见的错误理解是poll_wait就是用来等待数据的。其实poll_wait的作用不是等待而是注册。它做的事情是把当前发起select的进程挂到你指定的等待队列上。换句话说poll_wait的作用是让自身的等待队列和当前select进程建立关联等于告诉内核将来这个等待队列被唤醒时请顺带唤醒正在select的进程。static unsigned int demo_poll(struct file *filp, struct poll_table_struct *wait) { struct demo_dev *dev filp-private_data; unsigned int mask 0; poll_wait(filp, dev-r_wait, wait); poll_wait(filp, dev-w_wait, wait); if (dev-readable) mask | POLLIN | POLLRDNORM; if (dev-writable) mask | POLLOUT | POLLWRNORM; return mask; }这里我把读等待队列和写等待队列分别挂上去因为读和写是两件独立的事。多队列挂接在poll_wait内部是通过poll_table_entry链表实现的不用自己管理。有个经典的坑是**poll_wait只是把进程挂上等待队列但poll方法一次调用返回后进程可能依然睡眠。**这句废话背后的含义是如果你在poll_wait之后立刻改变了设备状态而唤醒时机恰好没触发select就可能一直等到超时。所以严谨的做法是在poll_wait之后、返回掩码之前重新读取一次设备状态因为从挂接队列到检查返回之间设备状态可能已经被中断或另一个线程改变了。3. 手写demo驱动让sleep和wakeup带动select转动3.1 驱动数据结构等待队列和状态标志理论说再多不如一个能跑的demo。我直接写一个极简字符设备驱动功能很单一用户态可以写入一个字节写入后设备变为可读状态用户态读出那个字节后设备恢复为不可读状态。配合select用户态就能实现等数据到了再读的效果。先定义设备结构#include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/wait.h #include linux/slab.h #include linux/poll.h #include linux/uaccess.h #define DEMO_MAJOR 0 #define DEMO_MINOR 0 #define DEMO_DEV_COUNT 1 struct demo_dev { char data; bool data_ready; wait_queue_head_t r_wait; wait_queue_head_t w_wait; struct cdev cdev; }; static struct demo_dev *demo_devp; static dev_t demo_dev_no;r_wait表示读等待队列用户态select正在等数据可读时进程挂在这里。w_wait表示写等待队列一般用于等待缓冲区有空间可写在这个demo里其实用不到因为写总是成功。但为了完整展示poll的语义我还是保留它。3.2 open和read/write状态翻转如何触发唤醒open函数只做一件事把private_data指向设备结构体。后续所有方法都能通过filp-private_data拿到设备上下文。static int demo_open(struct inode *inode, struct file *filp) { filp-private_data demo_devp; return 0; }write是触发数据可读的开关static ssize_t demo_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { struct demo_dev *dev filp-private_data; char tmp; if (count 1) return -EINVAL; if (copy_from_user(tmp, buf, 1)) return -EFAULT; dev-data tmp; dev-data_ready true; wake_up_interruptible(dev-r_wait); return 1; }read是消耗数据可读状态的开关static ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct demo_dev *dev filp-private_data; char tmp; if (!dev-data_ready) return -EAGAIN; tmp dev-data; dev-data_ready false; if (copy_to_user(buf, tmp, 1)) return -EFAULT; return 1; }注意read里如果data_ready为false直接返回-EAGAIN。假如用户态用的是非阻塞IO这正是期望的行为如果用阻塞IO应该在open时清除O_NONBLOCKread内部调用wait_event_interruptible。不过为了让select能派上用场我选择让read裸奔把是否等待的决策权全交给用户态。wake_up_interruptible(dev-r_wait)这行是关键。当写入发生时唤醒挂在读等待队列上的所有进程其中就包括在select里等待的这个进程。这个唤醒动作做完后select内部会重新调用一遍poll方法此时data_ready已经是truepoll方法返回POLLINselect才真正告诉用户态可以读了。这个唤醒后重新轮询的行为正是多路复用和普通阻塞IO最大的区别。普通阻塞read是唤醒后直接继续读而select是唤醒后回到轮询循环再次询问所有fd的状态然后再把结果返回给用户态。3.3 poll实现和release注意事项poll方法内容非常短但每一行的作用都值得说清楚static unsigned int demo_poll(struct file *filp, struct poll_table_struct *wait) { struct demo_dev *dev filp-private_data; unsigned int mask 0; poll_wait(filp, dev-r_wait, wait); if (dev-data_ready) mask | POLLIN | POLLRDNORM; else mask | POLLOUT | POLLWRNORM; return mask; }这里有个细节值得展开当data_ready为false时我故意返回POLLOUT | POLLWRNORM。因为按poll语义可写表示设备有能力接收数据。read没把数据读走之前缓冲区是满的其实没有这个demo只有1字节容器但data_ready为true时如果再写会覆盖旧数据。为了避免这种情况我把可写当成缓冲区空闲来用data_ready为false时可写为true时不可写。如果希望更贴近真实场景可以把1字节容器换成环形缓冲区用in和out指针判断满空。那样poll方法的逻辑就是缓冲区非空 →POLLIN缓冲区非满 →POLLOUT本质就是这两个判断多路复用系统不在乎你这个缓冲区是1字节还是64KB它只关心你告诉它的非空/非满状态是否准确。release方法很简单static int demo_release(struct inode *inode, struct file *filp) { return 0; }之所以单独提release是因为一个隐藏的坑常在卸载模块时暴露出来如果进程正阻塞在select上模块被卸载poll_wait挂接的等待队列还在用设备结构体里的内存内核访问已释放内存轻则Oops重则系统崩溃。正规做法是模块卸载前先确保没有进程在等待队列上或者在release里清理。对于学习demo保证卸载时没有进程持有fd即可。3.4 file_operations注册和模块入口结构体注册部分没有悬念static struct file_operations demo_fops { .owner THIS_MODULE, .open demo_open, .release demo_release, .read demo_read, .write demo_write, .poll demo_poll, }; static int __init demo_init(void) { int ret; ret alloc_chrdev_region(demo_dev_no, 0, DEMO_DEV_COUNT, demo_poll_dev); if (ret) return ret; demo_devp kzalloc(sizeof(*demo_devp), GFP_KERNEL); if (!demo_devp) { unregister_chrdev_region(demo_dev_no, DEMO_DEV_COUNT); return -ENOMEM; } init_waitqueue_head(demo_devp-r_wait); init_waitqueue_head(demo_devp-w_wait); cdev_init(demo_devp-cdev, demo_fops); ret cdev_add(demo_devp-cdev, demo_dev_no, 1); if (ret) { kfree(demo_devp); unregister_chrdev_region(demo_dev_no, DEMO_DEV_COUNT); return ret; } return 0; } static void __exit demo_exit(void) { cdev_del(demo_devp-cdev); kfree(demo_devp); unregister_chrdev_region(demo_dev_no, DEMO_DEV_COUNT); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL);这里用alloc_chrdev_region动态分配主设备号避免手动指定撞车。实际加载后通过/proc/devices查主设备号再用mknod /dev/demo_poll c 主设备号 0创建节点。4. 用户态select配驱动的实测阻塞、超时和惊群4.1 一个能直接编译的用户态测试程序驱动写完了光看模块加载日志没有说服力我用一段用户态程序实测select的行为。#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include sys/select.h int main(void) { int fd; fd_set rfds; struct timeval tv; char buf[64]; int ret; fd open(/dev/demo_poll, O_RDWR); if (fd 0) { perror(open); return 1; } while (1) { FD_ZERO(rfds); FD_SET(fd, rfds); tv.tv_sec 5; tv.tv_usec 0; ret select(fd 1, rfds, NULL, NULL, tv); if (ret 0) { perror(select); close(fd); return 1; } if (ret 0) { printf(select timeout: no data within 5s\n); continue; } if (FD_ISSET(fd, rfds)) { memset(buf, 0, sizeof(buf)); ret read(fd, buf, sizeof(buf) - 1); if (ret 0) perror(read); else printf(read: %s\n, buf); } } close(fd); return 0; }编译后先跑这个程序它会阻塞在select上。另一个终端执行echo -n A /dev/demo_poll你会发现select立刻返回read读到A。这就是驱动poll方法生效的直接证据。4.2 轮询、超时和事件掩码的边界情况上面的测试程序行为看起来很简单但有几个边界情况值得深挖。第一个是超时行为。struct timeval在select内部用的是相对时间内核会把它转换成jiffies再计算绝对超时点。如果驱动里某个唤醒没触发select会一直睡到超时点然后返回0。很多驱动新手以为poll返回无事件标志select会立即返回这不对。select是否返回0取决于所有fd在轮询循环中始终无事件并且超时时间已到。第二个是事件掩码必须稳定。也就是说如果两次连续的poll调用之间设备状态没变化返回的掩码必须完全一致。内核的do_select会对结果做比较如果第一次返回POLLIN第二次返回0就可能导致事件漏报或重复报告。在我那个demo里data_ready是唯一的状态变量poll只是读取它所以天然稳定。如果你在poll里做了复杂的判断逻辑比如依赖某个会被自身修改的全局标志就要格外小心。第三个是读返回EAGAIN的处理。select说可读但read却返回-EAGAIN这种事在真实驱动中偶有发生。原因多半是两个一是多个进程同时select到了同一个fd你先read把数据抢走了另一个进程read自然扑空二是设备状态在poll返回之后、read执行之前被其他线程改了。因此用户态代码不能假设select说可读就一定能读到必须接受EAGAIN把它当作重新select一次即可的正常情况。4.3 多进程同时等待一个设备的惊群问题当两个进程同时select同一个fd时内核会把这个fd加入两套轮询表两个进程的task_struct都会挂到驱动的r_wait队列上。数据写入后wake_up_interruptible会一次性唤醒这两个进程。但数据只有一个字节只有一个进程最终能读到另一个进程read直接EAGAIN。这就是经典的惊群thundering herd场景。在驱动层面处理方式可以有两种思路一是只唤醒一个等待者用wake_up_interruptible改成wake_up_interruptible_sync配合互斥等待互斥等待会把等待队列条目标记为WQ_FLAG_EXCLUSIVE内核会优先唤醒其中一个。但这个改动不能完全避免竞争因为select内部唤醒后还要重新轮询状态判断仍在驱动的poll方法里。二是在read里加锁或原子操作。如果设备状态操作不是原子的两个进程可能同时读到同一个值。驱动开发中读状态、清状态这两个动作必须做成原子操作或受锁保护。最简单的方案是用spinlock保护data_ready的读写static ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct demo_dev *dev filp-private_data; unsigned long flags; char tmp; spin_lock_irqsave(dev-lock, flags); if (!dev-data_ready) { spin_unlock_irqrestore(dev-lock, flags); return -EAGAIN; } tmp dev-data; dev-data_ready false; spin_unlock_irqrestore(dev-lock, flags); if (copy_to_user(buf, tmp, 1)) return -EFAULT; return 1; }copy_to_user必须在锁外执行因为它可能睡眠。先拷到局部变量再在锁外用copy_to_user这是标准姿势。很多新手把copy_to_user放进临界区死锁或睡眠冲突就是这么来的。5. poll_wait的等待队列语义和经典误用5.1 poll_wait挂上去的是谁再来深挖一下poll_wait背后真正发生的事情。看内核源码poll_wait最终会走到__pollwaitstatic void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p) { struct poll_table_entry *entry poll_get_entry(p); if (!entry) return; entry-filp filp; entry-wait_address wait_address; init_waitqueue_func_entry(entry-wait, pollwake); add_wait_queue(wait_address, entry-wait); }读过这段代码就明白了select并不是把进程的task_struct直接挂到驱动的等待队列上而是构造了一个辅助的等待队列条目回调函数是pollwake。当驱动调用wake_up时pollwake会被执行它在内部找到发起select的进程设置为可运行并触发一次重新轮询。所以在驱动里poll_wait只是注册关系不是阻塞点。进程真正的睡眠是发生在select的do_select循环里由调度器完成。驱动能控制的只是某个事件发生时唤起select重新检查。这也解释了为什么poll方法的实现通常很短——它不应该做任何耗时操作。poll_wait把进程挂到队列上加上几个状态判断就该返回了。如果poll里去读硬件寄存器、等锁或做耗时统计select主线程就会被拖慢多fd轮询的性能会更难看。5.2 驱动里最容易犯的四个poll相关错误第一个错误是忘了调poll_wait。后果是select会立即返回因为内核认为这个fd的事件状态是永久就绪其实不会永久就绪而是行为未定义。没有poll_wait挂接等待队列驱动唤醒机制完全失效select要么忙轮询要么永不唤醒。建议在poll实现的第一行就调用poll_wait即使设备永远可读也把等待队列挂上这样将来实现真正的阻塞读时不会漏掉关联。第二个错误是返回掩码里同时包含读和写事件但实际设备只能做其中一个。比如一个只读设备返回了POLLOUT用户态select就会认为可以写write下去直接返回错误。最典型的问题源于copy-paste从别的驱动抄来poll模板把POLLIN | POLLOUT原封不动返回。第三个错误是状态检查和使用之间没有防重入机制。poll返回POLLIN后数据还没读出另一个内核线程调用了清理函数把状态清了用户态read就会失败。我在测试时就遇到过类似场景中断里不仅置了data_ready还顺手清了一个标志位结果poll的返回值完全错位。第四个错误是在poll中睡眠或调用可能睡眠的函数。do_select遍历fd是全程持锁的驱动poll里如果调用msleep、wait_event或kmalloc(GFP_KERNEL)就可能导致系统挂起或锁序问题。kmalloc要用GFP_ATOMIC一切可能睡眠的调用全部禁止。5.3 内核版本的差异从poll到epoll的兼容性驱动层面实现poll用户态不管用select、poll还是epoll最终都会走到同一个.poll回调。但不同内核版本对poll返回掩码的解析细节有差异。早期内核的poll支持里POLLIN和POLLRDNORM是等价语义POLLOUT和POLLWRNORM也一样。在某些2.6时代的内核里poll事件和epoll事件经过ep_eventpoll翻译对POLLRDNORM的处理路径略有区分。现代内核统一了这些宏的语义驱动返回POLLIN | POLLRDNORM已经是惯例。在linux/poll.h里有段关键注释明确指出驱动必须同时设置常规和带外标志位返回可读时POLLIN和POLLRDNORM一起返回返回可写时POLLOUT和POLLWRNORM一起返回实际操作中我只写POLLIN | POLLRDNORM内核各版本都认识用户态感知不到区别。但如果你写的是FIONREAD这种需要struct file操作的ioctl配合select使用时要格外注意内核源码注释里专门提醒过这个组合的坑。另外提醒一下老内核里有些驱动直接返回POLLIN不带POLLRDNORM。如果用户态恰好用了__poll_t类型做严格位检查可能在某些沙箱环境里行为不一致。保险起见宁可多带一个冗余标志位也不要少带。6. poll和唤醒机制配合的实时性陷阱6.1 wake_up的时机为什么不能太晚poll方法负责上报状态wake_up则负责主动通知select进程状态变了。二者必须配合缺一不可。一个常见场景是硬件中断里收到数据中断处理函数里把数据写入缓冲区然后调用wake_up_interruptible(dev-r_wait)。此时select进程被唤醒重新调用poll方法poll检查缓冲区发现非空返回POLLIN。理论上这个过程可以做到微秒级但有一个隐藏的实时性问题中断里唤醒进程后进程未必立刻运行。如果系统负载高被唤醒的select线程可能在就绪队列里排队几十毫秒数据在缓冲区里躺了许久用户态才读到。这不是驱动的问题是调度器特性。但驱动可以改善如果数据量小且实时性要求高考虑在read阻塞时用wait_event_interruptible配合wake_up_all或者增加一个内核线程直接推送数据而不是依赖用户态select的二次轮询。我在实际项目里踩过类似坑一个低速串口设备中断里唤醒select线程但系统CPU被一个高优先级任务占满select线程迟迟得不到调度串口环形缓冲区溢出数据丢失。后来把read改成中断里直接拷贝到用户态映射缓冲区才彻底解决。这只是案例参考真实场景还是要根据设备的实时性要求选型。6.2 interruptible和uninterruptible的选择驱动里等待队列的唤醒函数最常见的几个是wake_up唤醒独占和非独占等待者不可中断进程不会被唤醒wake_up_interruptible只唤醒可中断进程wake_up_all唤醒该队列上所有进程select/poll在do_select里使用的是pollwake回调这个回调本质上是唤醒可中断进程。所以驱动里跟select配合时用wake_up_interruptible就对了。如果你用了wake_up可能无法唤醒select进程因为select内部进程设置了TASK_INTERRUPTIBLE状态wake_up只处理非中断等待者二者对不上。这个坑我见过不止一次。有人从别处抄来一串代码用的wake_up配合select时偶发不唤醒调试很久才发现是唤醒函数和进程状态不匹配。规则很简单用户态select/poll/epoll对应的内核等待状态必然是可中断的驱动里一律用wake_up_interruptible。6.3 忙轮询和poll方法的死循环再把话说回poll的返回值。有些驱动设计错误地让poll方法永远有事件返回。比如这个写法static unsigned int bad_poll(struct file *filp, struct poll_table_struct *wait) { return POLLIN | POLLOUT; }用户态select只要select这个fd就会立即返回根本不会进入睡眠。你以为写了个驱动实际上把用户态程序拖成了100%CPU忙轮询。现代内核有busy poll机制SO_BUSY_POLL专门用于低延迟网络场景会让select主动忙等一小段时间再进入睡眠。这是应用层行为和驱动poll的返回值语义不冲突。但如果驱动本身poll就永远返回就绪busy poll形同虚设因为没有机会进入睡眠等待。所以写poll方法的守则是**只返回当前真实存在的状态不要为了省事预测或伪造未来状态。**比如缓冲区还有空间就返回POLLOUT空间耗尽就不要再返回有数据就返回POLLIN没数据就只挂等待队列不返回读事件。这套语义是select/epoll能高效运行的基石。7. 从字符设备到更复杂场景poll方法的横向迁移7.1 网络设备、input子系统和misc设备的poll实现poll方法不只存在于字符设备。Linux里大量子系统都依赖类似的事件查询模式。网络设备的poll机制稍有不同它体现在NAPI的poll方法中函数原型是int (*poll)(struct napi_struct *napi, int budget)用于收包调度和file_operations里的poll完全不是一回事。提这个是因为搜索资料时容易混淆看到poll第一反应要先判断是哪个层级的poll。input子系统的input_poll则对应struct input_dev的轮询模式用于没有中断能力的低速输入设备比如老式游戏手柄的扫描。它和file_operations毫无关系但同样需要注册一个周期回调。真正和本文紧密相关的是miscdevice框架下的字符设备。misc_register生成的设备节点依然走file_operations里的poll实现方法完全一致。如果你的驱动使用misc框架只要把.poll函数填进对应的file_operations即可不需要额外处理。所以这里有个有用的经验**poll的实现和驱动的设备模型无关只和file_operations挂钩。**不管用cdev、miscdevice还是proc_createproc文件也支持poll只要file_operations里的poll填对了用户态多路复用就能工作。7.2 内核事件通知机制和poll的配合驱动内部有时需要通知用户态内核有事件发生典型场景包括热插拔设备插入、网络链路状态变化、硬件监测值超过阈值。实现这些功能时poll是一个比netlink更轻量的方案。驱动维护一个事件计数器和等待队列事件发生时计数加一并唤醒等待队列。poll方法判断计数器是否比上次读到的值大是则返回POLLIN。用户态select到事件然后通过read或ioctl把事件数据读走。这种模式在嵌入式设备状态监控里很常见。有个小技巧可以用atomic_t存储事件计数poll方法里用atomic_read判断读取端用atomic_xchg清零。这样既避免锁开销又能保证poll和read之间不会丢失事件。struct demo_event_dev { atomic_t event_count; wait_queue_head_t wait; }; static unsigned int event_poll(struct file *filp, struct poll_table_struct *wait) { struct demo_event_dev *dev filp-private_data; unsigned int mask 0; poll_wait(filp, dev-wait, wait); if (atomic_read(dev-event_count) 0) mask | POLLIN | POLLRDNORM; return mask; }用户在select到事件后read里用atomic_xchg取出并清零计数。这个写法在并发场景下是安全的值得记住。7.3 实测推荐的poll方法模板最后把这段时间反复验证过的poll方法模板贴出来。它适用于大多数准静态字符设备static unsigned int demo_poll(struct file *filp, struct poll_table_struct *wait) { struct demo_dev *dev filp-private_data; unsigned int mask 0; poll_wait(filp, dev-r_wait, wait); poll_wait(filp, dev-w_wait, wait); if (dev-rx_avail) mask | POLLIN | POLLRDNORM; if (dev-tx_avail) mask | POLLOUT | POLLWRNORM; return mask; }只要你维护好rx_avail和tx_avail这两个状态变量并且保证它们的读写是原子的这个模板在绝大多数场景下都能正确工作。这两个变量的更新点通常在read/write/ioctl/中断处理函数里更新完记得调用wake_up_interruptible。顺带一提poll方法在file_operations里有时也被epoll的ep_ptable_queue_proc路径反复调用。epoll对等待队列的处理比select更精细它会在epoll实例内部维护一份回调表所以看起来驱动代码里只是调用了一次poll_wait实际可能被epoll拆成了多次注册。驱动实现层面不需要感知这些但如果用epoll_ctl做边缘触发ET要注意事件报告的语义差异。水平触发LT模式下只要poll返回POLLINepoll每次都会报告边缘触发模式下只有状态从无到有的跳变才报告一次。驱动里的poll实现区分不了这两种模式它只是如实回答当前状态。用户态搭配ET模式时必须把fd设为非阻塞并在收到事件后一次性读完否则很容易丢事件。这个坑在epoll编程里太经典了驱动侧没法帮你兜底。8. 我沿这条链路踩过的坑和最终建议写这个demo的过程中最有价值的部分其实不是代码本身而是整个排查链路。我按时间顺序复盘一遍给后来者一些可复现的排查路径。第一次踩坑是select永远超时。当时驱动结构体里等待队列初始化用了wait_queue_head_t的零值没有调用init_waitqueue_head。poll_wait把进程挂到未初始化的等待队列上add_wait_queue自然异常进程可能被挂进了一个损坏的链表。这个问题的隐蔽性在于模块加载不报错甚至read/write都正常唯独select的表现是挂了但永远不醒。排查方法是用crash工具或/proc状态检查等待队列如果发现进程挂在奇怪的地址上基本就是等待队列没初始化。解决办法其实只要一行init_waitqueue_head。第二次踩坑是poll方法里忘了检查真实状态。当时的驱动很简单poll直接无条件返回POLLIN | POLLOUT用户态测试select立刻返回程序直接打满一个CPU核。这个问题的本质是我不理解poll的语义poll是快照不是通知。把快照伪造成永远有数据代价就是永远忙轮询。这个坑只花了我十分钟就定位了因为用户态程序CPU占用率突然飙到100%实在太显眼。第三次踩坑是真遇到竞争条件。驱动里poll判断card-buffer_count 0返回POLLINread里先检查再拷贝。两个进程同时selectread中间没有锁保护导致一个数据包被两个进程各自读走一半。这种问题在单核性能测试时看不出来一旦多核并发压力上来就现形。最后用自旋锁保护buffer_count和缓冲区的读操作问题才消失。