Unix网络编程补充:epoll
$epoll$ 执行与 $select$ 和 $poll$ 类似的工作:监听多个描述符,当其中存在可用描述符时返回。$epoll$ 支持边缘触发和层级触发两种模式,并且在监听大量描述符的情况下有着很好的性能。
$epoll$ 的核心是 $epoll$ 对象,这是一个内核数据结构,形式上,可以认为是两个集合:
- 监听集合 ( 有时候被称为 $epoll$ 集合 ):描述符集合,进程注册的新的待监听的描述符会被存放在该集合中;
- 可用集合:描述符集合,是监听集合的子集,内核通过该集合返回可用的文件描述符。
1. epoll
操作
#include <sys/epoll.h>
/* 成功时返回0,出错返回-1 */
int epoll_create(int size);
int epoll_create1(int flags);
$epoll_-create$ 创建一个 $epoll$ 对象,返回该对象的描述符。从Linux 2.6.8
开始,$size$ 参数不再有作用,会被忽略,但是必须大于 $0$ ,否则会设置 $EINVAL$ 错误。$epoll_-create1$ 与 $epoll_-create$ 类似,$flags$ 参数的值可以是 $EPOLL_-CLOEXEC$ ,当设置该标志时,该对象描述符会在执行 $execve$ 调用后自动关闭。
#inlcude <sys/epoll.h>
// 成功返回0,出错返回-1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *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_data_t data;
};
$epoll_-ctl$ 从 $epoll$ 描述符中添加、修改或删除事件,$fd$ 指定待监听的描述符。其中 $op$ 可以是:
$op$ | 说明 |
---|---|
$EPOLL_-CTL_-ADD$ | 添加一个事件 |
$EPOLL_-CTL_-MOD$ | 修改一个事件 |
$EPOLL_-CTL_-DEL$ | 删除一个事件,$events$ 参数会被忽略 |
$epoll_-event$ 的 $data$ 成员指定该事件关联的对象,会被保存在内核中,并在可用时返回;$events$ 成员是多个标志位的或的值,用于注册进程感兴趣的事件,当其中的条件满足时会唤醒阻塞进程,同时也作为返回值接收事件状态。
$event$ | 说明 |
---|---|
$EPOLLIN$ | 描述符可读 |
$EPOLLOUT$ | 描述符可写 |
$EPOLLRDHUP$ | 流套接字对端关闭连接或者进入写半关闭状态 |
$EPOLLPRI$ | TCP 套接字带外数据 |
$EPOLLERR$ | 描述符发生错误,不需要作为入参指定也能触发 |
$EPOLLHUP$ | 描述符挂起,一般是描述符未打开或异常关闭导致的,不需要作为入参指定也能触发 |
$EPOLLET$ | 边缘触发模式 ( 默认为层级触发模式 ),仅作为入参,不会被返回 |
$EPOLLONESHOT$ | 单次通知,完成后该描述符会被移除出监听集合 |
$EPOLLWAKEUP$ | 在非 $EPOLLET$ 和 $EPOLLONESHOT$ 的事件上,并且进程拥有 $CAP_-BLOCK_-SUSPEND$ 的能力,可以确保系统不会在等待该事件或者处理该事件时进入休眠状态。仅作为入参,不会被返回 |
当 $epfd$ 或 $fd$ 不可用时,返回 $EBADF$ 错误。当 $fd$ 已存在时,返回 $EEXIST$ 错误。
#include <sys/epoll.h>
/* 成功时返回可用描述符数,出错时返回-1 */
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout, const sigset_t *sigmask);
int epoll_pwait2(int epfd, struct epoll_event *events,
int maxevents, const struct timespec *timeout, const sigset_t *sigmask);
$epoll_-wait$ 等待 $epoll$ 对象上的任意一个或多个描述符变为可用。$events$ 数组用于返回可用描述符,最多返回 $maxevents$ ( 必须大于 $0$ ) 个描述符。$timeout$ 参数指定超时事件,单位为毫秒,如果为 $-1$ ,意味着没有超时时间。文件描述符的状态会作为 $epoll_-event$ 中的 $events$ 成员返回。$epoll_-pwait$ 和 $epoll_-pwait2$ 与 $epoll_-wait$ 类似,其中 $sigmask$ 参数可以指定在等待描述符期间阻塞的信号,类似于以下操作:
sigset_t origmask;
pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = epoll(wait, epfd, &events, maxevents, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);
如果 $epfd$ 不可用,返回 $EBADF$ 错误;如果 $epfd$ 不是一个 $epoll$ 描述符或者 $maxevents$ 不大于 $0$ ,返回 $EINVAL$ 错误;如果 $events$ 指向不可用区域,返回 $EFAULT$ 错误;如果阻塞过程中被中断,返回 $EINTR$ 错误。
2. 边缘触发和层级触发
$epoll$ 允许边缘触发 ( $edge-triggered$ ,$ET$ ) 和层级触发 ( $level-triggered$ ,$LT$ ) 两种行为。假设存在以下情形:
- 一个管道的读描述符 $rfd$ 被注册进 $epoll$ 对象;
- 另一个进程往这个管道写入 $2KB$ 数据;
- $epoll_-wait$ 调用返回,当前进程从管道中读入 $1KB$ 数据;
- 当前进程再次调用 $epoll_-wait$ 。
对于这种情况,$epoll$ 存在两种行为:
- 边缘触发:描述符 $rfd$ 被认为不可用,尽管管道里面还存在数据,直到另一个进程再次向管道中写入数据。边缘触发只有在描述符被监听的时候才会改变事件状态;
- 层级触发:描述符 $rfd$ 可用,$epoll_-wait$ 立即返回。
在上述例子中,假设使用边缘触发并且对端进程在等待先前数据的响应,那么由于 $epoll_-wait$ 的调用,对端可能会无限等待下去。所以对于这种情况,建议使用非阻塞式I/O
,或者多次调用 $read$ 或 $write$ 直到返回 $EAGAIN$ 错误。
#define MAX_EVENTS 10
struct epoll_event, ev, events[MAX_EVENTS];
int listenfd, connfd, nfds, epollfd, val;
/* Code to set up listening socket, 'listenfd',
(socket(), bind(), listen()) omitted. */
epollfd = epoll_create1(0);
if (epollfd = -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listenfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) {
perror("epoll_ctl: listenfd");
exit(EXIT_FAILURE);
}
for (;;) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; n++) {
if (events[n].data.fd == listenfd) {
connfd = accept(listenfd, (struct sockaddr *) &addr, &addrlen);
if (connfd == -1) {
perror("accept");
eixt(EXIT_FAILURE);
}
if ((val = fcntl(connfd, F_GETFL, 0)) == -1) {
perror("fcntl: F_GETFL");
exit(EXIT_FAILURE);
}
if (fcntl(connfd, F_SETFL, val | O_NONBLOCK) == -1) {
perror("fcntl: F_SETFL");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = connfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev) == -1) {
perror("epoll_ctl: connfd");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd);
}
}
}