一个进程盯着几百个 fd,任何一个有动静就去处理 — select,poll,epoll 三代接口解决的都是这件事.上一篇讲阻塞时埋了伏笔,这篇把它收掉:驱动侧的 poll 回调怎么写,epoll 为什么快,水平触发和边缘触发到底差在哪.全部实测,LT 和 ET 的分野一行输出就能看清.
这篇回答几件事:
- 驱动的 .poll 回调做什么,poll_wait 那一句为什么不睡
- epoll 和 poll 的本质区别,快在哪
- LT 和 ET 的行为差异,ET 为什么逼你循环读到 EAGAIN
- 数据到达时,阻塞中的 epoll_wait 是怎么被叫醒的
先写设备.支持多路复用的驱动只需要一个回调:
static __poll_t poll_poll(struct file *f, poll_table *wait)
{
poll_wait(f, &poll_wq, wait); /* 把自己挂进等待队列, 仅注册不睡 */
return data_ready ? (EPOLLIN | EPOLLRDNORM) : 0;
}
两件事:调 poll_wait 登记等待队列,然后报告当前状态.poll_wait 是整个机制里最容易被误解的一句 — 它不阻塞,只把"如果这个设备有动静,请通知我"的回调挂进驱动的等待队列.这句话是驱动和复用层之间的全部契约:驱动数据来了要 wake_up_interruptible(&poll_wq),谁来收,复用层自己安排.设备这边我加了个 ioctl 模拟"数据到达",设置标志并 wake_up.
三代接口先排个队.select 最老,fd_set 是个固定位图,FD_SETSIZE 1024 是硬顶,每次调用还要整张位图在用户态内核态之间拷来拷去.poll 时代去掉了位图,fd 数量随你,但本质没变:do_sys_poll(fs/select.c)每次调用把你给的每个 fd 的 .poll 回调挨个跑一遍,O(n),一百个 fd 就跑一百次,一千个就一千次,大多数次的结果都是"没动静",白跑.epoll 的革命在这:把"注册"和"等待"拆开.epoll_ctl 加进红黑树,只此一次;epoll_wait 只看就绪链表 — 链表平时是空的,谁把 fd 挂进去?就是驱动的 wake_up 触发 poll_wait 当初登记的 ep_poll_callback(fs/eventpoll.c),数据到哪个设备,回调从哪个设备出发,直接把对应 fd 塞进就绪链表并唤醒 epoll_wait.事件驱动的回调代替了遍历,十万 fd 也只和活跃的那几个相关,这才是 epoll 常说的 O(1).
然后是 LT/ET,同一份数据,两种脾气.epoll_ctl 加 fd 时带不带 EPOLLET 的区别,实测一把最清楚 — 放一次数据,连续调两次 epoll_wait:
[LT] 放数据后第一次: n=1 ev=0x1; 第二次: n=1 (水平仍就绪!)
[ET] 放数据后第一次: n=1 ev=0x1; 第二次: n=0 (边沿已消费)
LT 水平触发,只要"可读"这个状态还在,每次 epoll_wait 都如实报告,你不读它就一直喊.ET 边缘触发,只报"状态从无到有"那一瞬间,消费一次就闭嘴,哪怕数据还剩着.ET 省了重复通知,代价是使用纪律:必须一口气把数据读完 — 循环 read 直到返回 EAGAIN,否则残留数据永远不会再有通知,连接就"卡死"了,这是 ET 的经典事故.ET 还必须配非阻塞 fd,阻塞 read 读完一波再读会睡死.默认 LT 不是因为落后,是因为它安全,除非你真的在管十万连接,别急着切 ET.
最后看唤醒链路怎么真正跑通.fork 一个子进程,epoll_wait 带无限超时死等;父进程睡一秒,然后 ioctl 让驱动放数据:
[parent] 1 秒后放数据...
[child] 被唤醒! n=1 ev=0x1
这条链在内核里走了四站:驱动 wake_up_interruptible 摇等待队列 → 队列里挂着的不是进程,是 ep_poll_callback → 回调把 fd 挂进 epoll 就绪链表,顺便唤醒睡着的 epoll_wait → 子进程拿号返回.注意妙处:睡的是 epoll_wait,而摇的是设备的等待队列,中间的路由完全由复用层自动完成 — 这就是 poll_wait 那一句注册的价值,驱动对谁在等,怎么等,一无所知.
最后留几个容易忽略的细节:
- poll 回调里别做重活,它在 do_sys_poll 和 epoll_ctl 的路径上都被同步调用,拿着锁睡一觉全系统卡住.
- 返回的 mask 别只给 EPOLLIN,EPOLLRDNORM 这些常规位一起给,有些用户态库按老习惯检查它们.
- 驱动忘写 wake_up 是另一类经典 bug:poll 状态对了,阻塞的 epoll_wait 永远不醒,表现为"第一次能读到,第二次吊死".
- epoll 也不是银弹:fd 少而活跃率高时,poll 的简单循环反而更快,epoll 的回调链和红黑树都是成本.
- 实验里子进程的 printf 输出一度"消失" — 管道下 stdio 全缓冲,_exit 不 flush,加 fflush 才露面.写这类测试程序的常见暗坑,和内核无关但坑你没商量.
- epoll 的 EPOLLONESHOT 给多线程工作者模式用:通知一次自动摘屏蔽,处理完要 EPOLL_CTL_MOD 重挂,防惊群.
- select 在新代码里别再用了,但读老代码必须懂:FD_SETSIZE 的 1024 在大并发年代踩过无数脚,当年为了它各家发明 poll 的私有变体,最后才统一到 epoll.