epoll
I/O复用:指应用程序 向 内核 注册一组事件(eg: 新连接到达——监听套接字上的可读事件),然后阻塞在IO复用函数上(select,poll,epoll),内核通过IO复用函数把就绪的事件通知给应用程序。
一、epoll基本API
epoll_create
int epoll_create(int size);
int epoll_create1(int flags);
创建一个内核事件表(epoll实例)
size:一个大于0的整数;之前的内核被设计为用参数size,告知内核期望监听的文件描述字大小,作为内核数据结构初始化的值;从 Linux2.6.8 开始,这个参数就被忽略了 (epoll源码)
返回值:
若成功返回一个大于0的值,表示epoll实例;若返回-1表示出错
flags:
若为0,行为同epoll_create(); 可以设置为EPOLL_CLOEXEC.
EXEC:子进程默认打开父进程所有的文件描述符(包括epoll内核事件表,
CLOEXEC指当前进程 fork 出来的任何子进程 没有访问父进程 epoll 实例的权限。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数功能:向epoll实例中增加/删除监听的事件
参数说明
epfd:epoll句柄
op:操作类型
- EPOLL_CTL_ADD: 向 epoll 实例注册文件描述符对应的事件;
- EPOLL_CTL_DEL:向 epoll 实例删除文件描述符对应的事件;
- EPOLL_CTL_MOD: 修改文件描述符对应的事件。
event:向内核注册的某个具体事件,epoll_event类型
fd:事件对应的文件描述符
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
成员:
events:一系列事件的按位或
常见事件类型
EPOLLIN:表示对应的文件描述字可以读
EPOLLOUT:表示对应的文件描述字可以写
EPOLLRDHUP:表示套接字的一端已经关闭,或者半关闭
EPOLLHUP:表示对应的文件描述字被挂起
EPOLLET:设置为 edge-triggered,默认为 level-triggered。
epoll_data_t:一个联合体
常用的成员是fd,代表用户感兴趣事件对应的描述符
void* ptr 传入用户数据
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
events:
以epoll_event数组形式返回用户空间需要处理的I/O事件
- events成员表示发生的事件类型,事件类型取值和 epoll_ctl 可设置的值一样
- data值即epoll_ctl里设置的data.
返回值: 成功返回的是一个大于0的数,表示就绪事件的个数;返回0表示的是超时时间到;若出错返回-1.
二、ET和LT
LT(Level Trigger)条件触发模式 (默认,效率较高的poll)
-
epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序可以不立即处理该事件。
-
当下一次调用epoll_wait时,epoll_wait还会再次向应用程序报告此事件,直至被处理
ET边缘触发模式
-
epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件 (只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了)
-
必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain
三、使用epoll服务器工作流程
-
服务进程通过
epoll_wait
获取内核就绪事件处理。 -
如果就绪事件是监听套接字(server_fd)上的读事件,调用
accept
获取新连接对应的文件描述符 (client_fd),设置非阻塞,然后 epoll_ctl 监控 client_fd 的可读事件 EPOLLIN。 -
如果就绪事件是已连接套接字上的读事件,
read
读取客户端发送数据,进行逻辑处理。如果read == 0
代表对方请求关闭连接, epoll_ctl 删除 EPOLLIN事件,close
关闭对应 fd 从而完成四次挥手。 -
处理逻辑过程中需要
write
回复客户端,write 内容很大,超出了内核缓冲区,没能实时发送完成所有数据,需要下次继续发送;那么调用epoll_ctl注册client_fd 的EPOLLOUT
可写事件,下次触发事件进行发送。发送完毕后, epoll_ctl 删除 EPOLLOUT 事件。
图片来源:https://wenfh2020.com/2020/04/14/epoll-workflow/
四、性能对比
select、poll、epoll三组IO复用系统调用:
用户设置感兴趣事件的方式:
- select:参数类型
fd_set
,没有将文件描述符和事件类型绑定,仅仅是一个文件描述符集合。因此select需要提供3个fd_set类型的参数,分别传入 可读、可写、异常等事件。 - poll:参数类型
pollfd
,设置文件描述符+事件类型
struct pollfd {
int fd; /* file descriptor */
short events; /*由用户设置感兴趣的事件类型*/
short revents; /* 内核返回就绪事件 */
};
内核如何将结果返回给应用程序:
- select:内核对fd_set在线修改,应用程序需要重置集合
- poll:内核修改pollfd的revents成员
文件描述符数量:
-
select通过数组描述文件描述符集合,最大文件描述符数量有上限,一般是1024,但可以修改源码,重新编译内核,不推荐
-
poll是链表描述,突破了文件描述符上限,最大可以达到系统允许打开的最大文件描述符的数目(65535)。
-
epoll通过红黑树描述,最大可以打开文件的数目,可以通过命令ulimit -n number修改,仅对当前终端有效
应用程序索引就绪文件描述符:
-
select/poll只返回就绪的文件描述符的个数,返回整个用户注册的事件集合(就绪的和未就绪的),若知道是哪个发生了事件,同样需要遍历。时间复杂度O(n)
-
epoll返回就绪事件的个数和,将用户传入的结构体数组events,表示就绪的事件,时间复杂度O(1)