共计 1662 个字符,预计需要花费 5 分钟才能阅读完成。
select 的不足
在 IO 多路复用中 select(poll)有诸多限制,很多人会说 select 的缺点是在 Linux 内核中,select 所用到的 FD_SET 是有限的,(内核中有个参数__FD_SETSIZE 定义了每个 FD_SET 的句柄个数)。但是更重要的缺点却触及不到:
select 采用的是轮询机制,select 被调用时 fd_set 会被遍历,导致其时间复杂度为 O(n)。
内核 / 用户空间拷贝问题:当有事件发生,且 select 轮询完后,fd_set 会从内核态拷贝到用户态
当并发上来后,轮询的低效率和频繁的内核态用户态切换会导致 select 的性能急剧下降:
epoll 的内部调用流程
(0) 红黑树是在 mmap 出的内存上的,减少了用户空间和内核空间的拷贝 (1) epoll_wait 调用 ep_poll,当 rdlist 为空(无就绪 fd)时挂起当前进程,知道 rdlist 不空时进程才被唤醒。(2) 文件 fd 状态改变(buffer 由不可读变为可读或由不可写变为可写),导致相应 fd 上的回调函数 ep_poll_callback() 被调用。(3) ep_poll_callback 将相应 fd 对应 epitem 加入 rdlist,导致 rdlist 不空,进程被唤醒,epoll_wait 得以继续执行。(4) ep_events_transfer 函数将 rdlist 中的 epitem 拷贝到 txlist 中,并将 rdlist 清空。(5) ep_send_events 函数,它扫描 txlist 中的每个 epitem,调用其关联 fd 对用的 poll 方法。之后将取得的 events 和相应的 fd 发送到用户空间(封装在 struct epoll_event,从 epoll_wait 返回)。(6) 如果这个 epitem 对应的 fd 是 LT 模式监听且取得的 events 是用户所关心的,则将其重新加入回 rdlist。否则(ET 模式)不在加入 rdlist。
ET 模式与 LT 模式的不同
ET 和 LT 模式下的 epitem 都可以通过插入红黑树时的回调(ep_poll_callback)方式加入 rdlist 从而唤醒 epoll_wait,但 LT 模式下的 epitem 还可以通过 txlist(ep_send_events)重新加入 rdlist 唤醒 epoll_wait。所以 ET 模式下,fd 就绪,只会被通知一次,而 LT 模式下只要满足相应读写条件就返回就绪(通过 txlist 加入 rdlist)。
2.1 直接回调插入:fd 状态改变才会触发。
对于读取操作:
(1) 当 buffer 由不可读状态变为可读的时候,即由空变为不空的时候。
(2) 当有新数据到达时,即 buffer 中的待读内容变多的时候。
对于写操作:
(1) 当 buffer 由不可写变为可写的时候,即由满状态变为不满状态的时候。
(2) 当有旧数据被发送走时,即 buffer 中待写的内容变少得时候。
2.2 txlist(ep_send_events):fd 的 events 中有相应的事件(位置 1)即会触发。
对于读操作:
(1) buffer 中有数据可读的时候,即 buffer 不空的时候 fd 的 events 的可读为就置 1。
对于写操作:
(1) buffer 中有空间可写的时候,即 buffer 不满的时候 fd 的 events 的可写位就置 1。
3 总结
与 select 相比,epoll 的回调机制使得资源能够直接使用在有活动的事件上,而不用线性轮询所有的事件。同时 epoll 通过内核与用户空间 mmap 同一块内存,减少了用户空间和内核空间的数据交换,解决了 select 的重要痛点。
另一方面,LT 是 epoll 的默认操作模式,当 epoll_wait 函数检测到有事件发生并将通知应用程序,而应用程序不一定必须立即进行处理,这样 epoll_wait 函数再次检测到此事件的时候还会通知应用程序,直到事件被处理。
而 ET 模式,只要 epoll_wait 函数检测到事件发生,通知应用程序立即进行处理,后续的 epoll_wait 函数将不再检测此事件。因此 ET 模式在很大程度上降低了同一个事件被 epoll 触发的次数,因此效率比 LT 模式高。