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
返回)。
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 进行授权