linux epoll
1. epoll 与 select/poll 区别
select由于采用轮询的方式,即轮询所有文件描述符。现实情况中,并发活跃的连接数远小于总连接数(文件描述符列表),select的效率较低。poll与select类似,也是采用轮询的方式,但是poll没有最大文件描列表长度述符限制(默认是FD_SETSIZE= 1024)。
例如,poll函数使用pollfd数组来查询事件,每次都需要将该pollfd数组传递给内核,内核再遍历该数组,查找有事件发生的fd,特别是数组比较大的时候,效率低下。
相比较而言,epoll只在有事件发生时才通知用户程序,效率较高。epoll使用三个高效的数据结构:
mmap: 内核空间和用户空间共享一块内存(epoll_event数组)。epoll_wait过程中,内核将等到的event数据直接写入到epoll_event数组中。红黑树:epoll_ctl将需要监听的文件描述符(针对网络通信就是套接字)时,保存在红黑树中。添加/删除/索引的时间复杂度为O(log n)。rdlist:rdlist是内核存储的就绪事件列表,当有事件发生时比如套接字数据可读,驱动将fd对应的epitem加入到rdlist中(通过fd上的回调函数ep_poll_callback)。epoll_wait从rdlist中取出epitem,更新对应的epoll_event数据,并返回给用户程序。
具体流程如下:
epoll_wait调用ep_poll,当rdlist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。- 文件
fd状态改变(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback()被调用。 ep_poll_callback将相应fd对应epitem加入rdlist,导致rdlist不空,进程被唤醒,epoll_wait得以继续执行。ep_events_transfer函数将rdlist中的epitem拷贝到txlist中,并将rdlist清空。ep_send_events函数(很关键),它扫描txlist中的每个epitem,调用其关联fd对用的poll方法。此时对poll的调用仅仅是取得fd上较新的events(防止之前events被更新),之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。
epoll_wait流程如下:
其中,wq为socket等待队列。
2. ET模式与LT模式区别
LT模式:当fd就绪时,epoll_wait 会一直返回该fd,直到事件被处理。例如,如果一个socket连接有数据可读,epoll_wait 会每次都返回该socket fd,直到数据被完全读取。
ET模式: 当fd从未就绪变为就绪时,epoll_wait 只会返回一次该fd。例如,如果一个socket有数据可读,epoll_wait 只会在数据第一次到达时返回该套接字,之后即使有更多数据到达,也不会再次返回,直到所有数据被读取完并且socket fd再次变为未就绪状态。
3. 设置socket为非阻塞
ET模式下,使用阻塞模式socket,如果数据量较大,需要多次read,最后一次可能没有数据可读,此时read将一直阻塞。使用非阻塞模式socket,read返回EWOULDBLOCK即代表数据读完。
4. epoll 调用流程图
graph TD
A[启动服务器] --> B[创建 epoll 实例]
B --> C[创建监听套接字]
C --> D[设置监听套接字为非阻塞]
D --> E[绑定监听套接字到指定端口]
E --> F[监听连接请求]
F --> G[将监听套接字添加到 epoll 实例中]
G --> H[进入事件循环]
H --> I[调用 epoll_wait 等待事件发生]
I --> J{有事件发生吗?}
J -->|是| K[处理就绪事件]
J -->|否| I
K --> L{事件类型}
L -->|新连接| M[接受新连接]
M --> N[设置新连接套接字为非阻塞]
N --> O[将新连接套接字添加到 epoll 实例中]
L -->|可读事件| P[读取数据]
P --> Q{读取成功吗?}
Q -->|是| R[处理读取的数据]
Q -->|否| S[关闭套接字]
R --> T[将套接字修改为可写事件]
L -->|可写事件| U[写入数据]
U --> V[将套接字修改为可读事件]
S --> H
O --> H
T --> H
V --> H
参考
本文由作者按照 CC BY 4.0 进行授权




