同IO多路复用和信号驱动IO一样,Linux的epoll(event poll) API可以检查多个文件描述符上的IO就绪状态。epoll API的主要优点如下
- 当检查大量的文件描述符时,epoll的性能扩展性比select和poll高很多
- epoll API即支持水平触发也支持边缘触发。与之相反,select()和 poll()只支持水平触发,而信号驱动 I/O 只支持边缘触发
性能表现上,epoll与信号驱动IO类似。但是epoll有一些胜过信号驱动IO的优点
- 可以避免复杂的信号处理流程(比如信号队列溢出时的处理)
- 灵活性高,可以指定我们希望检查的事件类型(比如,检测套接字文件描述符的读就绪、写就绪或者两种同时指定)
epoll API 是 Linux 系统专有的,在 2.6 版中新增
epoll API的核心数据结构称为epoll实例,它和一个打开的文件描述符相关联。这个文件描述符不是用来做IO操作的。相反,它是内核数据结构的句柄,这些内核数据结构实现了两个目的:
- 记录了在进程中声明过的感兴趣的文件描述符列表—interest list(兴趣列表)。
- 维护了处于 I/O 就绪态的文件描述符列表—ready list(就绪列表)
ready list 中的成员是 interest list 的子集
对于由epoll检测的每一个文件描述符,我们可以指定一个位掩码来表示我们感兴趣的事件。这些位掩码同poll()所使用的位掩码有紧密的关联。
epoll API由下面三个系统调用组成
- 系统调用epoll_create()创建一个epoll实例,返回代表该实例的文件描述符
- 系统调用 epoll_ctl()操作同 epoll 实例相关联的兴趣列表。通过 epoll_ctl(),我们可以增加新的描述符到列表中,将已有的文件描述符从该列表中移除,以及修改代表文件描述符上事件类型的位掩码。
- 系统调用 epoll_wait()返回与 epoll 实例相关联的就绪列表中的成员。
创建epoll实例:epoll_create()
系统调用epoll_create()创建了一个新的epoll实例,其对应的兴趣列表初始化为空
NAME
epoll_create, epoll_create1 - open an epoll file descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
RETURN VALUE
成功时,这些系统调用返回一个非负的文件描述符。
出错时,返回 -1,并设置 errno 以指示错误
参数 size 指定了我们想要通过 epoll 实例来检查的文件描述符个数。该参数并不是一个上限,而是告诉内核应该如何为内部数据结构划分初始大小。(从 Linux 2.6.8 版以来,size 参数被忽略不用,因为内核实现做了修改意味着该参数之前提供的信息已经不再需要了。)
作为函数返回值,epoll_create()返回了代表新创建的 epoll 实例的文件描述符。这个文件描述符在其他几个 epoll 系统调用中用来表示 epoll 实例。当这个文件描述符不再需要时,应该通过 close()来关闭。当所有与 epoll 实例相关的文件描述符都被关闭时,实例被销毁,相关的资源都返还给系统。(多个文件描述符可能引用到相同的 epoll 实例,这是由于调用了 fork()或者 dup()这样类似的函数所致)。
从 2.6.27 版内核以来,Linux 支持了一个新的系统调用 epoll_create1()。该系统调用执行的任务同 epoll_create()一样,但是去掉了无用的参数 size,并增加了一个可用来修改系统调用行为的 flags 参数。目前只支持一个 flag 标志:EPOLL_CLOEXEC,它使得内核在新的文件描述符上启动了执行即关闭(close-on-exec)标志(FD_CLOEXEC)。出于同样的原因,这个标志同 4.3.1 节中描述的 open()的 O_CLOEXEC 标志一样有用。
修改 epoll 的兴趣列表:epoll_ctl()
epoll_ctl()能修改由文件描述符epfd所代表的epoll实例中的兴趣列表
NAME
epoll_ctl - control interface for an epoll descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数fd指定了要修改兴趣列表中的哪一个文件描述符的设定。该参数可以是代表管道、FIFO、套接字、POSIX 消息队列、inotify 实例、终端、设备,甚至是另一个 epoll 实例的文件描述符(例如,我们可以为受检查的描述符建立起一种层次关系)。但是,这里 fd 不能作为普通文件或目录的文件描述符(会出现 EPERM 错误)。
参数 op 用来指定需要执行的操作,它可以是如下几种值。
- EPOLL_CTL_ADD
- 将描述符 fd 添加到 epoll 实例 epfd 中的兴趣列表中去。
- 对于 fd 上我们感兴趣的事件,都指定在 ev 所指向的结构体中。
- 如果我们试图向兴趣列表中添加一个已存在的文件描述符,epoll_ctl()将出现 EEXIST 错误。
- EPOLL_CTL_MOD
- 修改描述符 fd 上设定的事件,需要用到由 ev 所指向的结构体中的信息。
- 如果我们试图修改不在兴趣列表中的文件描述符,epoll_ctl()将出现 ENOENT 错误。
- EPOLL_CTL_DEL
- 将文件描述符 fd 从 epfd 的兴趣列表中移除。该操作忽略参数 ev。
- 如果我们试图移除一个不在 epfd 的兴趣列表中的文件描述符,epoll_ctl()将出现 ENOENT 错误。
- 关闭一个文件描述符会自动将其从所有的 epoll 实例的兴趣列表中移除。
参数 ev 是指向结构体 epoll_event 的指针,结构体的定义如下。
typedef union epoll_data {
void *ptr; // pointer to user-defined data
int fd; // file descriptor
uint32_t u32; // 32-bit integer
uint64_t u64; // 64-bit integer
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
参数 ev 为文件描述符 fd 所做的设置如下。
- 结构体epoll_event 中的events字段是一个位掩码,它指定了我们为待检查的描述符fd上所感兴趣的时间集合
- data字段是一个联合体,当描述符fd成为就绪态时,联合体的成员可以用来指定传回给调用进程的信息
int epfd;
struct epoll_event ev;
epfd = epoll_create(s);
if (epfd == -1){
errExit("epoll_create");
}
ev.data.fd = fd;
ev.events = EPOLLIN;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev) == -1){
errExit("epoll_ctl");
}
max_user_watches 上限
- 因为每个注册到epoll实例上的文件描述符需要占用一小段不能被交换的内核内存空间,因此内核提供了一个接口用来定义每个用户可以注册到epoll实例上的文件描述符总数
- 这个上限值可以通过 max_user_watches 来查看和修改。max_user_watches 是专属于 Linux 系统的/proc/sys/fd/epoll 目录下的一个文件。
- 默认的上限值根据可用的系统内存来计算得出
事件等待:epoll_wait()
epoll_wait() 返回epoll实例中出于就绪态的文件描述符信息。单个epoll_wait()调用能返回多个就绪态文件描述符的信息
NAME
epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *evlist,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
evlist所指向的结构体数组中返回的是有关就绪态文件描述符的信息。 evlist的空间由调用者负责申请,所包含的元素个数在参数maxevents中指定。
在数组evlist中,每个元素返回的都是单个就绪态文件描述符的信息。
- events 字段返回了在该描述符上已经发生的事件掩码。
- data 字段返回的是我们在描述符上使用 cpoll_ctl()注册感兴趣的事件时在 ev.data 中所指定的值。注意,data 字段是唯一可获知同这个事件相关的文件描述符号的途径。因此,当我们调用 epoll_ctl()将文件描述符添加到兴趣列表中时,应该要么将 ev.data.fd 设为文件描述符号,要么将 ev.data.ptr 设为指向包含文件描述符号的结构体
参数 timeout 用来确定 epoll_wait()的阻塞行为,有如下几种。
- 如果 timeout 等于−1,调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生,或者直到捕获到一个信号为止。
- 如果 timeout 等于 0,执行一次非阻塞式的检查,看兴趣列表中的文件描述符上产生了哪个事件。
- 如果 timeout 大于 0,调用将阻塞至多 timeout 毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止
调用成功后,epoll_wait()返回数组 evlist 中的元素个数。如果在 timeout 超时间隔内没有任何文件描述符处于就绪态的话,返回 0。出错时返回−1,并在 errno 中设定错误码以表示错误原因。
在多线程程序中,可以在一个线程中使用epoll_ctl()将文件描述符添加到另一个线程中有epoll_wait()所监视的epoll实例的兴趣列表中去。这些对兴趣列表的修改将立刻得到处理,而epoll_wait()调用将返回有关新添加的文件描述符的就绪信息。
epoll事件
当我们调用epoll_ctl()时可以在ev.events中指定的位掩码以及由epoll_wait()返回的evlist[].events 中的值如下表。除了有一个额外的前缀 E 外,大多数这些位掩码的名称同 poll()中对应的事件掩码名称相同。这种名称上有着对应关系的原因是当我们在 epoll_ctl()中指定输入,或通过 epoll_wait()得到输出时,这些比特位表达的意思同对应的 poll()的事件掩码所表达的意思一样
位掩码 | 作为 epoll_ctl()的输入? | 由 epoll_wait()返回? | 描述 |
---|---|---|---|
EPOLLIN | ● | ● | 可读取非高优先级的数据 |
EPOLLPRI | ● | ● | 可读取高优先级数据 |
EPOLLRDHUP | ● | ● | 套接字对端关闭(始于 Linux 2.6.17 版) |
EPOLLOUT | ● | ● | 普通数据可写 |
EPOLLET | ● | 采用边缘触发事件通知 | |
EPOLLONESHOT | ● | 在完成事件通知之后禁用检查 | |
EPOLLERR | ● | 有错误发生 | |
EPOLLHUP | ● | 出现挂断 |
EPOLLONESHOT 标志
默认情况下,一旦通过 epoll_ctl()的 EPOLL_CTL_ADD 操作将文件描述符添加到 epoll 实例的兴趣列表中后,它会保持激活状态(即,之后对 epoll_wait()的调用会在描述符处于就绪态时通知我们),直到我们显式地通过 epoll_ctl()的 EPOLL_CTL_DEL 操作将其从列表中移除
如果我们希望在某个特定的文件描述符上只得到一次通知,那么可以在传给 epoll_ctl()的ev.events 中指定 EPOLLONESHOT(从 Linux 2.6.2 版开始支持)标志。如果指定了这个标志,那么在下一个 epoll_wait()调用通知我们对应的文件描述符处于就绪态之后,这个描述符就会在兴趣列表中被标记为非激活态,之后的 epoll_wait()调用都不会再通知我们有关这个描述符的状态了。
如果需要,我们可以稍后通过 epoll_ctl()的 EPOLL_CTL_ MOD 操作重新激活对这个文件描述符的检查。(这种情况下不能用 EPOLL_CTL_ADD 操作,因为非激活态的文件描述符仍然还在 epoll 实例的兴趣列表中。
看个例子