齐天大圣数据候 2019-10-26
本文代码取自内核版本 4.17
epoll(2) - I/O 事件通知设施。
epoll 是内核在2.6版本后实现的,是对 select(2)/poll(2) 更高效的改进,同时它自身也是一种文件,不恰当的比方可以看作 eventfd + poll。
多路复用也是一直在改进的,经历的几个阶段
#include <sys/epoll.h> 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_create(int size); int epoll_create1(int flags); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_create() - 用来创建一个 epoll 实例,返回一个新的文件描述符。第一个参数 @size
自 2.6.8 开始无意义,但必须大于 0。
epoll_create1() - 参数 @flags
为 0 则等效于 epoll_create(),flags 可以为 EPOLL_CLOEXEC, 在exec新程序时关闭文件描述符。
epoll_ctl() - epoll 的控制接口,用户调用该系统调用来控制监听的文件描述符。参数 @epfd
为 epoll_create() 返回的新文件描述符,参数 @op
为 epoll_ctl() 提供的控制操作:
@event
参数;@fd
为需要控制的文件描述符,参数 @event
为相关联的 struct epoll_event 结构。epoll_wait() - 等待epoll中监听文件描述符就绪的 I/O 事件。参数 @epfd
为epoll实例对应的文件描述符,由 epoll_create() 创建,、
参数 @events
为就绪的事件集合的地址,参数 @maxevents
为需要就绪事件集合的大小,必须大于 0,参数 @timeout
为超时时间,单位为 微秒。
epoll 默认使用水平触发模式,边缘触发模式需要设置 events |= EPOLLET
。
边缘触发模式的特点是边缘触发模式只在关注的文件描述符发生改变时才产生就绪的事件,考虑高低电平的图片,边缘是有一个瞬间的概念,而水平则有一个持续的状态。
这就导致了,边缘触发有可能会丢失需要通知的事件。分析如下
现有 5 个步骤:
如果文件描述符 rfd 使用 EPOLLET 边缘触发模式注册到 epoll 中,那么在执行上面的 5 的时候,尽管管道的读端缓冲区还有数据,epoll_wait(2) 还是可能会挂起,
同时写端可能会基于其已发送的数据期望响应。产生这个情况的原因是边缘触发模式只在关注的文件描述符发生改变时才产生就绪的事件。在上面的步骤中,2 写入的数据,
因此在 rdf 上生成一个事件,由于在 4 中的读取操作不会消耗整个缓冲区数据,故在 5 对 epoll_wait(2) 调用可能发生阻塞。
使用边缘触发模式的程序应该使用非阻塞文件描述符来避免阻塞读写造成处理多个文件描述符时产生的饥饿问题。
所以建议使用的边缘触发模式时遵从一下两点:
在使用边缘触发模式时,在接收到多个数据块的时候会产生多个事件,因此用户可以选择指定 EPOLLONESHOT 标志,在 epoll_wait(2) 收到事件后禁用相关的文件描述符。
而设置 EPOLLONESHOT 标志后,需要用户手动调用 epoll_ctl(2) 重新设置文件描述符。
在示例代码中可以看到边缘触发和水平触发的区别
把 eventfd 注册到 epoll 中,进行两个线程间的通信。使用 eventfd 的 EFD_SEMAPHORE 的标志来模拟 read(2) 读取部分数据。
程序初始值设置 1000,在水平模式下,会先用 1000 次 read(2),把计数器的值消耗为 0,之后 write(2) 写入 cnt,就调用 cnt 次 read(2),总之只要水位(count)不为0就可读。
而 epoll 设置 EPOLLET 后只有发生了 write(2) 操作 epoll_wait(2) 才能产生一个可读事件,而计数器则是逐渐增大。
// 代码取自上一篇文章的 eventfd 示例 #include <unistd.h> #include <pthread.h> #include <poll.h> #include <sys/epoll.h> #include <sys/eventfd.h> #include <stdio.h> int efd; void *run_eventfd_write(void *arg) { uint64_t count = 1; while (1) { printf("write count: %zu\n", count); write(efd, &count, sizeof(count)); count++; sleep(2); // 将睡眠时间调成大于 timeout 时间, } } int main() { unsigned int initval = 1000; int flags = 0; int timeout = 1000; flags |= EFD_SEMAPHORE; // 使计数器器的值每一次减 1 而不清空,保持计数器的值不直接置为 0 efd = eventfd(initval, flags); int epfd = epoll_create(32); struct epoll_event epfds; struct epoll_event ev; ev.data.fd = efd; ev.events = EPOLLIN; ev.events |= EPOLLET; // 对比注释这个行代码的打印输出 epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev); pthread_t pid; pthread_create(&pid, NULL, run_eventfd_write, NULL); while (1) { int ret = epoll_wait(epfd, &epfds, 1, timeout); if (ret > 0) { uint64_t count; read(efd, &count, sizeof(count)); printf("read count: %zu\n", count); } else if (ret == 0) { printf("not avaiable data\n"); } } }
单次命中,内核 2.6.2
引入,当事件就绪被 epoll_wait(2) 返回时,这个事件就不再被关注了。
内核 3.5
引入,如果 EPOLLONESHOT 和 EPOLLET 标志被清除后,并且进程拥有CAP_BLOCK_SUSPEND(阻止系统挂起的特性)权限,这个标志能够保证事件在挂起或者处理的时候,系统不会挂起或休眠。
排他的唤醒,内核 4.5
引入,解决惊群的问题,下一篇文章会分析到。
本文不准备把源码分析放在这里,由于是文件的原因 epoll(2) 对比 poll(2) 和 select(2) 来说要复杂很多,这里抛出几个点来引出源码分析的重点:
刚开始把源码分析放了出来,但是发现写的过于混乱没有重点,代码贴的太多,所以想专门写一文来着分析上面的问题。