Linux多路转接 epoll
epoll()
解决了 poll()
的部分缺陷,epoll 消除了线性扫描,使用了红黑树结构来存储监听的事件,同时也能避免注册重复文件描述符。
epoll 被公认为 Linux2.6 下最好的多路转接 IO 就绪通知方法。
1. epoll 的工作原理
epoll 将使用的函数增加到了三个,epoll_create()
、epoll_ctl()
、epoll_wait()
。
首先用户要先使用 epoll_create()
注册一个 epoll 句柄,这个函数会在内核建立一个 eventpoll
结构体,这个结构体就代表了 epoll 实例,在这个 eventpoll
中,有两个关键的结构,一个红黑树,用于存储注册的文件描述符和事件信息;一个就绪链表(rdlist),存储发生了事件的文件描述符。
红黑树中的每个节点都是一个 epitem
结构体,这个结构体存储了一个文件描述符及其相关的事件信息。
struct epitem {struct list_head rdllink; // 用于将触发事件的 `epitem` 放入就绪链表struct rb_node rbn; // 红黑树节点,链接到 `epitem` 的红黑树中struct epoll_filefd {struct file *file; // 指向文件对象的指针,用于关联文件描述符int fd; // 文件描述符} ffd;struct eventpoll *ep; // 指向所属的 `epoll` 实例struct list_head fllink; // 链接到 `epoll` 实例的 `allitems` 链表中struct epoll_event event; // 事件信息,包括监听的事件类型和触发的回调
};
当用户使用 epoll_ctl()
向 epoll 实例注册文件描述符时,本质会在这个红黑树中插入一个节点(key 为文件描述符),或使用这个函数来修改已经注册的文件描述符,也是通过红黑树的查找和修改来实现的,所以 epoll 在管理大量文件描述符上有优势。
所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系,即当响应的事件发生时会调用这个回调方法,这个回调方法在内核中叫 ep_poll_callback()
,它会将发生的事件添加到 rdlist
双链表中。所以用户调用 epoll_wait()
时,其只需要在 rdlist
就绪链表中查看是否有 epitem
元素即可。如果 rdlist
不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。 这个操作的时间复杂度是 O(1)。
硬件中断通知操作系统有数据要读,调用
ep_poll_callback()
而不是操作系统主动轮询去读才知道有事件发生。
2. 函数声明
#include <sys/epoll.h>
int epoll_create(int size)
用于创建一个 epoll 的句柄。
size
: 是一个已经被废弃的参数,只要设置大于0
即可。
reval
: 返回值是一个文件描述符,小于0
表示失败。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epfd
: 是 epoll 的句柄,即epoll_create()
的返回值。
op
: 一般有三个操作:
EPOLL_CTL_ADD
:向epfd
中注册新的 fd。EPOLL_CTL__MOD
:修改已经注册的 fd 的监听事件。EPOLL_CTL_DEL
:从epfd
中删除一个 fd。
fd
: 需要监听的文件描述符。
event
: 是事件集,下面给出struct epoll_event
的具体结构。
reval
: 返回值为0
表示成功,返回值为-1
表示失败。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
epoll_wait()
会将就绪的事件,依次严格按照顺序放入我们定义的用户缓冲区数组。
epfd
: 是一个文件描述符,是epoll_create()
的返回值。
*events
: 是一个数组,实质上是传入一个事件缓冲区,从内核捞取就绪事件给用户。
maxevents
: 是数组的最大容量,表示一次最多可以返回的事件数。
timeout
: 表示超时时间,以毫秒为单位;设置0
表示非阻塞等待;设置-1
表示阻塞等待。
reval
: 成功则返回就绪文件描述符的数量,返回值等于0
表示超时,返回值小于0
表示发生错误。
3. struct epoll_event
typedef union epoll_data
{void *ptr; /* 通用指针,用户可以保存自定义数据指针 */int fd; /* 文件描述符 */uint32_t u32; /* 32 位整数 */uint64_t u64; /* 64 位整数 */
} epoll_data_t;struct epoll_event
{uint32_t events; /* 事件掩码,表示感兴趣的或发生的事件 */epoll_data_t data; /* 用户数据,可以是文件描述符或指针 */
};
events
:表示希望监视的事件类型或实际发生的事件类型。它的值是多个 epoll
事件类型的按位或运算结果。
epoll_data_t
: 是一个类型的联合体,允许用户在事件发生时存储自定义数据。可以是一个指针、文件描述符、32 位或 64 位整数,开发者可以根据需要选择其中一种方式存储数据。
常见事件:
EPOLLIN
:表示对应的文件描述符上有数据可读(包括对端 SOCKET 正常关闭)。EPOLLOUT
:表示对应的文件描述符可以写数据。EPOLLPRI
:表示对应的文件描述符有紧急的数据可读(表示有带外数据到来)。EPOLLHUP
:表示对应的文件描述符被挂断。EPOLLET
:将 EPOLL 设置为边沿触发模式(Edge Triggered),这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT
:只监听一次事件,当监听完此事件后,如果还需要继续监听该 socket ,则必须将该 socket 重新加入到 EPOLL 实例中。EPOLLRDHUP
:对端关闭连接。EPOLLERR
:文件描述符发生错误。
4. 水平触发模式和边缘触发模式
4.1 水平触发模式
水平触发(Level Triggered)模式又称 LT 模式,是 epoll 的默认工作模式(也是 select、poll 的工作模式)。LT 模式下,当 epoll 检测到 socket 上事件就绪的时候,可以不立刻进行处理或者只处理一部分,剩下的数据等下次调用 epoll_wait()
再处理。
如果第一次数据没处理完,下次调用
epoll_wait()
会立即返回并通知 socket 事件就绪,直到缓冲区上所有的数据都被处理完,下次调用epoll_wait()
才不会立即返回。
水平触发模式支持文件描述符为阻塞状态和非阻塞状态。
4.2 边缘触发模式
边缘触发模式(Edge Triggered)模式又称 ET 模式,是 epoll 需要手动设置为 EPOLLET
从水平触发模式切换过来的工作模式。ET 模式下,epoll 只通知事件就绪一次,本轮数据没读完,epoll不再通知。所以ET模式一旦就绪,就必须把数据全部读完。所以ET通知的效率更高。
一个事件就绪只通知一次就能全部读取,但 LT 模式下可能需要通知很多次才能全部读取完成。
ET模式要一次性把数据读取完,就要循环读取,直到读取不到。但是循环读取一定会有阻塞问题,所以ET模式下要求所有的文件描述符都使用非阻塞模式。
4.3 LT 和 ET 的优缺点
-
LT 模式的编码要比 ET 模式更简单。
网络通信的主机接收数据和发送 ACK 都要有严格的顺序,LT 模式很容易实现这一点,但在 ET 模式下一次性读取所有就绪的事件,就需要手动编码接收数据和发送 ACK 的顺序的控制。
-
ET在IO上会更高效一点。
因为 ET 模式要上层循环读直到把数据都读走,而 LT 模式可以只读走一部分数据,下次再接着读。ET 模式逼着上层尽快把 TCP 缓冲区的数据尽快取走。所以如果一个服务器处于 ET 模式,接收方的上层尽快把数据取走, TCP 给对方的 ACK 一定有较大的概率给对方通告一个更大的接收窗口,就可以让对方的发送端一次性发送更多的数据包。
-
ET 要求程序员严格编码。
LT 模式通过设计,也可以设计成非阻塞、循环读取,实现 ET 模式同样的功能。但 LT 模式不强制要求文件描述符为非阻塞,而ET 模式能倒闭着程序员设置非阻塞文件描述符。
4.4 epoll使用
todo
4.5 epoll 的优点
-
接口使用方便。
epoll 虽然需要三个函数才能使用,但是使用起来反而更简单高效。
-
数据拷贝轻量。
只在合适的时候调用
EPOLL_CTL_ADD
将文件描述符结构拷贝到内核中, 这个操作并不频繁(而 select/poll 都是每次循环都要进行拷贝)。 -
事件回调机制效率高。
避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,
epoll_wait()
返回直接访问就绪队列就知道哪些文件描述符就绪。这个操作时间复杂度 O(1)。即使文件描述符数目很多,效率也不会受到影响。 -
没有数量限制。
文件描述符数目无上限。