概述
epoll 是 linux 内核为处理大批量文件描述符而对 poll 进行的改进版本,是 linux 下多路复用 IO 接口 select/poll 的增强版本,显著提高了程序在大量并发连接中只有少量活跃的情况下的CPU利用率
在获取事件时,它无需遍历整个被侦听描述符集,只要遍历被内核 IO 事件异步唤醒而加入 ready 队列的描述符集合就行了
epoll 除了提供 select/poll 所提供的 IO 事件的电平触发,还提供了边沿触发,,这样做可以使得用户空间程序有可能缓存 IO 状态,减少 epoll_wait 或 epoll_pwait 的调用,提高程序效率
实现原理
liunx内部存储需要监控的epoll_event对象,如果有事件触发,返回对应事件
当某个进程调用 epoll_create 函数创建 epoll 专用的文件描述符epfd(连接用户空间和内核空间通道,就像socket 一样,用户看到的是epfd),
Linux 内核会创建为每个(epfd)创建 eventpoll 结构体变量:
struct eventpoll
{
struct rb_root rbr; // 红黑树根节点,存储epoll 中所有事件
struct list_head rdllist; // 双向链表,保存需要epoll_wait 返回的事件
}
这个结构体会在内核空间中分配独立的内存,用于存储使用 epoll_ctl 函数向 epoll 对象中添加进来的事件,
每一个事件都会挂到(存储)红黑树 rbr 上,这样重复添加的时间就可以通过红黑树结构快速识别并避免加入,保证了 epoll_ctl 函数的效率
所有添加到 epoll 中的时间都会与设备驱动程序建立回调关系,
一旦某个事件发生(满足条件是四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满),
则设备驱动程序会调用相应的回调函数,这个回调函数就是 ep_poll_callback,它会把相应事件放到 rdllist 这个双向链表中
这个双向链表的元素是 epitem 结构体类型的:
当向系统中添加一个fd时,就创建一个epitem结构体,这是内核管理epoll的基本数据结构:
struct epitem {
struct rb_node rbn; //用于主结构管理的红黑树
struct list_head rdllink; //事件就绪队列
struct epitem *next; //用于主结构体中的链表
struct epoll_filefd ffd; //这个结构体对应的被监听的文件描述符信息
int nwait; //poll操作中事件的个数
struct list_head pwqlist; //双向链表,保存着被监视文件的等待队列,功能类似于select/poll中的poll_table
struct eventpoll *ep; //该项属于哪个主结构体(多个epitm从属于一个eventpoll)
struct list_head fllink; //双向链表,用来链接被监视的文件描述符对应的struct file。因为file里有f_ep_link,用来保存所有监视这个文件的epoll节点
struct epoll_event event; //注册的感兴趣的事件,也就是用户空间的epoll_event
}
如下图:
事件触发模式
EPOLL事件有两种模型 Level Triggered (LT) 和 Edge Triggered (ET):
LT(level triggered,水平触发模式)是缺省的工作方式,并且同时支持 block 和 non-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。
ET(edge-triggered,边缘触发模式)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,等到下次有新的数据进来的时候才会再次出发就绪事件。
函数原型
int epoll_create(int size);
创建一个 epoll 专用的文件描述符,调用成功返回描述符,否则返回 -1
需要注意的是,该描述符使用完毕后同样需要 close 操作
作用: 需要监听的socket句柄和事件添加到内核
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
调用成功返回0,否则返回 -1
参数说明
- epfd -- epoll_create 返回的 epoll 专用的文件描述符
- op -- epoll_ctl 动作参数取值
fd -- 需要监听的fd- event -- 监听事件类型
struct epoll_event 的定义如下 : typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events 触发的条件 */ epoll_data_t data; /* User data variable */ };
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
When successful, epoll_wait(2) returns the number of file descriptors ready for the requested I/O, or zero if no file descriptor became ready during the requested timeoutmilliseconds. When an error occurs, epoll_wait(2) returns -1 and errno is set appropriately.
返回需要处理的事件数目,如返回 0 表示超时,调用失败返回 –1
参数说明
- epfd -- epoll 专用文件描述符
- events -- 事件触发的事件集合,
- maxevents -- 每次能处理的最大事件数,不能大于 epoll_create 的 size 参数
- timeout -- 超时时间,以毫秒为单位,0 表示立即返回,-1 表示永远阻塞
优势
支持同时打开大量的文件描述符
select 函数对一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是1024,这对于一个服务器来说显然是太少了,虽然修改这个宏之后重新编译系统可以解决这个问题,但是随着 FD_SETSIZE 值的上升,select 函数的性能会显著下降
传统 Apache 服务器对此的解决方案是使用多进程的方式来打开大于 FD_SETSIEZE 的文件描述符,但是开辟进程的效率和资源都有一定的消耗,同时进程间数据同步也远没有线程间数据同步来的高效
epoll 能够打开的 FD 与系统能够持有的 FD 数目是一致的,只受限于系统的内存
IO效率不随 FD 数目增加而线性下降
传统的 select、poll 具有一个致命弱点,每当有数据可读或可写,都需要对整个描述符集合进行扫描,这样如果文件描述符集合很大,而同时又有大量空闲连接,则效率下降会非常明显
使用mmap加速内核与用户空间的消息传递
epoll是通过内核与用户空间mmap同一块内存实现的,这样就可以避免从内核空间通知用户空间的时候不必要的拷贝了
内核微调
内核的 TCP/IP 协议栈使用内存池管理 sk_buff 结构,通过在运行时改变 /proc/sys/net/core/hot_list_length 的值,即可动态调整整个内存池的大小,如 listen 函数所指示的3次握手数据包队列长度也可以根据平台内存动态调整
代码演示
参考文档:
1 http://blog.youkuaiyun.com/justlinux2010/article/details/8510507
2 http://www.cnblogs.com/apprentice89/p/3234677.html
3 http://techlog.cn/article/list/10182604#e