本文主要参考https://man7.org/linux/man-pages/man7/epoll.7.html这个Linux/unix线上操作手册。
epoll
epoll API的功能和select,poll等多路io复用接口类似,是用来监听多个 文件描述是否有io事件发生。epoll API的核心概念是epoll实体,从用户空间的的角度来看,epoll实体是操作系统内核的一个数据结构,可以先简单的认为它包含有两个链表:
- interest list,或者叫epoll set,即epoll监听事件集合,是用来保存epoll实体所有监听的文件描述符。它是一个文件描述符集合。比如说,我对某个socket感兴趣,我要监听这个socket,我可以通过epoll API把这个socket的文件描述符加入到一个epoll实体的监听集合里边,委托epoll来监听这个socket的io事件,如果有io事件发生时让epoll来通知。
- ready list,就绪链表,用来保存已经有io事件发生了的文件描述符集合。这个ready list可以认为是interest list的子集,意思是ready list保存的是interest list中已经发生了事件的文件描述符。当内核监听到interest list中的文件描述符有io事件发生后就会把这个文件描述符加入到ready list中。
Linux提供了几个系统调用来创建和管理一个epoll实体,分别是epoll_create,epoll_ctl,epoll_wait.
int epoll_create(int size)
创建一个epoll实体,返回一个新epoll实体相关的文件描述符,size用来告诉内核这个监听的数目一共有多大。size参数只是告诉内核这个 epoll实体会处理的事件大致数目,而不是能够处理的事件的最大个数。在 Linux最新的一些内核版本的实现中,这个 size参数没实际已经没有意义,但是为了兼容最好是填一个正数。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
这个系统调用是用来添加,修改,或者删除epoll对象的interest list监听事件集合条目。epfd是epoll实体的文件描述符;op是操作类型,增删改查等;fd是所监听事件的文件描述符;event则是监听的io事件类型,可读可写等;
其中op的有效值有以下这三种,分别是增删改:
EPOLL_CTL_ADD | 往监听集合中添加一个事件 |
EPOLL_CTL_MOD | 修改监听集合的一个事件 |
EPOLL_CTL_DEL | 删除监听集合的一个事件 |
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 */
};
当一个文件描述符已经就绪的时候,data成员是用来指定内核返回值的(epoll_wait的返回值)。它是一个union,一般是返回就绪事件的fd。
The data member of the epoll_event structure specifies data that the kernel should save and then return (via epoll_wait(2)) when this file descriptor becomes ready.
后面解析的epoll_wait接口也会将返回的数据复制到epoll_event数组,对于epoll_event的data成员,epoll_wait返回的就是调用epoll_ctl注册事件的时候所填入的值。例如,epoll_ctl填的是一个fd,那么epoll_wait返回的也是fd。后面会举一个具体的例子,以redis为例。
events是一个位掩码,一般包含以下这几种类型:
EPOLLIN | 文件描述符可以读 |
EPOLLOUT | 文件描述符可以写 |
EPOLLPRI | 文件描述符有紧急的数据可读(这里应该表示有带外数据到来) |
EPOLLERR | 文件描述符发生错误 |
EPOLLHUP | 文件描述符被挂断 |
EPOLLET | 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的 |
EPOLLONESHOT | 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 |
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
wait for an I/O event on an epoll file descriptor
顾名思义,epoll_wait是用来 等待一个文件描述符io事件发生的。参数events是分配好的epoll_event结构体数组,内核将ready list就绪列表中的事件复制到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告之内核这个events有多大,避免复制的时候越界,当然maxevents必须大于0。
timeout参数用来指定epoll_wait()堵塞的毫秒数。epoll_wait()会一直堵塞直到发生了以下事件:
- 监听的文件描述符发生了一个事件;
- 调用被一个信号处理程序打断,这种情况errno为EINTR;
- 超时退出,timeout指定的时候超时了也没有事件发生也会返回。
Note that the timeout interval will be rounded up to the system clock granularity, and kernel scheduling delays mean that the blocking interval may overrun by a small amount. Specifying a timeout of -1 causes epoll_wait() to block indefinitely, while specifying a timeout equal to zero cause epoll_wait() to return immediately, even if no events are available.
超时时间会被对齐到系统时钟 粒度,加上由于内核调度的延时意味着实际的堵塞时长会稍稍超出timeout。如果timeout为-1,那就一直堵塞到有事件发生。如果timeout等于0,epoll_wait不管有没有事件发生都会马上返回。
The data field of each returned epoll_event structure contains the same data as was specified in the most recent call to epoll_ctl(2) (EPOLL_CTL_ADD, EPOLL_CTL_MOD) for the corresponding open file descriptor.
events数组指针这个参数,其实是个出参,返回的每个epoll_event结构体的data字段的数据就是epoll_ctl接口所指定的。evens是位掩码,用来表面发生的事件类型。
如果成功,epoll_wait会返回发生了事件的文件描述符数量;如果一直到超时结束都没有事件发生就返回0;如果发生错误就返回-1,具体的错误原因可以由errno判断。
epoll_wait这个接口的作用简单讲就是,通过返回值告诉你有多少个事件发生,事件的具体信息保存到了一个数组中,里边保存有事件的fd和事件的类型。这样上层就可以根据fd和事件的类型来进行相应的处理。
redis应用示例
这里以redis3.0中epoll中的实际使用为例子, 文件是src/ae_epoll.c。只有一百多行代码,非常简洁。具体解析见代码注释。
typedef struct aeApiState {
// epoll实体描述符,也就是epoll_creat创建的实体fd
int epfd;
// 事件槽,用来保存epoll_wait返回的数据
struct epoll_event *events;
} aeApiState;
/*
* 创建一个新的 epoll 实例,并将它赋值给 eventLoop,eventLoop这里先不用管,是redis对事件的一个封装
*/
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
if (!state) return -1;
// 初始化事件槽空间,给events数组分配内存
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
if (!state->events) {
zfree(state);
return -1;
}
// 创建 epoll 实例
state->epfd = epoll_create(1024);
if (state->epfd == -1) {
zfree(state->events);
zfree(state);
return -1;
}
// 赋值给 eventLoop
eventLoop->apidata = state;
return 0;
}
上面这段代码主要就是创建一个epoll实体,并为events数据分配内存,后面epoll_wait会用到。 接下来就是要往epoll中注册监听事件。
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee;
/* If the fd was already monitored for some event, we need a MOD
* operation. Otherwise we need an ADD operation.
*
* 如果 fd 没有关联任何事件,那么这是一个 ADD 操作。
*
* 如果已经关联了某个/某些事件,那么这是一个 MOD 操作。
*/
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
//上面是填写操作类型
// 注册事件到 epoll
ee.events = 0;//事件位掩码,指明要监听可读还是可写事件
mask |= eventLoop->events[fd].mask; /* Merge old events */
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.u64 = 0; /* avoid valgrind warning */
ee.data.fd = fd;//这里指的了data为socket的fd,那么后面epoll_wait返回时也是填fd
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}
源码中还有一个删除监听事件的接口,过程都差不多,这里就不讲了,接下来看事件发生时epoll_wai如何处理。
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
//state->events就是第一步申请了内存的那个数组,用来保存返回值
// 如果没有设置等待时间,那就把等待时间设为-1,表示会一直堵塞直到事件发生
retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
// 有至少一个事件就绪
if (retval > 0) {
int j;
// 为已就绪事件设置相应的模式
// 并加入到 eventLoop 的 fired 数组中
numevents = retval;
//挨个把events数组中的epoll_event取出来
for (j = 0; j < numevents; j++) {
int mask = 0;
struct epoll_event *e = state->events+j;
//根据掩码判断事件类型
if (e->events & EPOLLIN) mask |= AE_READABLE;
if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
if (e->events & EPOLLERR) mask |= AE_WRITABLE;
if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
//redis并没有在这里直接处理事件,而是先把事件保存到fired数组,后面统一处理
// 返回已就绪事件个数
return numevents;
}
通过epoll_wait ,我们已经知道了那些fd发生了事件,我们还能通过mask知道事件的类型。例如,我们可以根据fd找到某个socket,如果mask是可读,那么我就掉read函数从这个socket读取数据,如果是可写那就调writ写数据。
redis中,上面已经把事件保存到了fired数组,那么接下来就挨个遍历这个数组对事件进行处理,调用事先注册好的回调函数来处理读写事件。
// 处理文件事件,阻塞时间由 tvp 决定
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
// 从已就绪数组中获取事件
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0;
/* note the fe->mask & mask & ... code: maybe an already processed
* event removed an element that fired and we still didn't
* processed, so we check if the event is still valid. */
// 读事件
if (fe->mask & mask & AE_READABLE) {
// rfired 确保读/写事件只能执行其中一个
rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);//读回调函数
}
// 写事件
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);//写回调函数
}
processed++;
}