epoll

Event Poll

由于poll()和select()的局限,Linux2.6内核引入了event poll(epoll)机制。虽然epoll的实现比poll()和select()要复杂的多,epoll解决了前两个都存在的基本性能问题,并增加了一些新的特性

对于poll()和select()每次调用时都需要所有被监听的文件描述符列表。内核必须遍历所有被监视的文件描述符、当这个文件描述符列表变得很大时,每次调用都要遍历列表就编程规模上的瓶颈

epoll把监听注册从实际监听中分离出来,从而结局了这个问题。一个系统调用会初始化epoll上下文,另一个从上下文中加入或删除监视的文件描述符,第三个执行真正的等待事件。

创建新的epoll示例

通过epoll_create1()创建epoll上下文:

1
2
3
# include <sys/epollh>
int epoll_create1 (int flags);
int epoll_create (int size);

调用成功时,epoll_create1()会创建新的epoll实例,并返回和该实例关联的文件描述符。这个文件描述符和真正的文件没有关系,仅仅是为了后续调用epoll而创建的

参数flags支持修改epoll的行为,当前,只有EPOLL_CLOEXEC是个合法的flag,他表示进程被替换时关闭文件描述符

出错时,返回-1,并设置errno为下列值之一:

EINVAL : 参数flags非法

EMFILE : 用户打开的文件数达到上限

ENFILE : 系统打开的文件数达到上限

ENOMEN : 内存不足,无法完成本次操作

epoll_create()时老版本的epoll_create1()的实现,现已废弃。

epoll的标准调用方式如下:

1
2
3
4
5
6
int epfd;
epfd = epoll_create1(EPOLL_CLOEXEC);
if (epfd < 0)
{
perror ("epoll_creat1);
}

当完成监视后,epoll_create1()返回的文件描述符需要用close()调用关闭

控制epoll

epoll_ctl()函数可以向指定的epoll上下文中加入或删除文件描述符:

1
2
#include <sys/epoll.h>
int poll_ctl (int epfd, int op, int fd, struct epoll_event *event);

头文件<sys/epoll.h>中定义了epoll_event结构体:

1
2
3
4
5
6
7
8
9
10
11
struct epoll_event
{
_u32 events;
unio
{
void *ptr;
int fd;
_u32 u32;
_u64 u64;
} data;
};

epoll_ctl()调用如果执行成功,会控制和文件描述符epfd关联的epoll实例。参数op指定对fd指向的文件所执行的操作。参数event进一步描述epoll更具体的行为

以下是参数op的有效值:

EPOLL_CTL_ADD : 把文件描述符fd所指向的文件添加到epfd指定的epoll监听实例集中,监听event中定义的事件

EPOLL_CTL_DEL : 把文件描述符fd所指向的文件冲epfd指定的epoll监听实例集中删除

EPOLL_CTL_MOD : 使用event指定的更新事件修改在已有fd上的监听行为

epoll_events结构体中的events变量列出了在指定文件描述符上要监听的事件。多个事件可以通过位或运算同时指定。以下是events的有效值:

EPOLLERR : 文件出错。即使没设置,这个事件也被监听

EPOLLET : 在监听文件上开启边缘触发。默认是条件触发

EPOLLHUP : 文件被挂起。即使没设置,这个事件也被监听

EPOLLIN : 文件为阻塞,可读

EPOLLONESHOT : 在事件生成并处理后,文件不会再被监听。必须通过EPOLL_CTL_MOD指定新的事件掩码,以便重新监听文件

EPOLLOUT : 文件为阻塞,可写

EPOLLPRI : 存在高优先级的带外数据可读

event_poll 中的data变量是由用户私有使用。当接收到请求事件后,data会被返回给用户。通常用法是把event.data.fd设置为fd,这样可以很容易查看哪个文件描述符触发了事件。

当成功时,epoll_ctl()返回0。失败时,返回-1,并设置errno为下值:

EBADF : epfd不是有效的epoll实例,或者fd不是有效的文件描述符

EEXIST : op值设置为EPOLL_CTL_ADD,但fd已经与epfd关联

EINVAL : epfd不是epoll实例,epfd和fd相同,或op无效

ENOENT : op值设置为EPOLL_CTL_MOD或EPOLL_CTL_DEL,但是fd没有和epfd关联

ENFILE : 系统打开的文件数达到上限

ENOMEN : 内存不足,无法完成本次操作

EPERM : fd不支持epoll

在下面的例子中,在epoll实例epfd中加入fd所指向文件的监听事件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
struct epoll_event event;
int ret;

event.data.fd = fd;
event.events = EPOLLIN | EPOLLOUT;

ret = epoll_ctl (epfd, EPOLL_CTL_ADD, fd, &event);
if (ret)
{
perror ("epoll_ctl");
}

修改epfd实例中的fd上的一个监听事件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
struct epoll_event event;
int ret;

event.data.fd = fd;
event.events = EPOLLIN;

ret = epoll_ctl (epfd, EPOLL_CTL_MOD, fd, &event);
if (ret)
{
perror ("epoll_ctl");
}

相反,从epoll实例中删除在fd上的一个监听事件,代码如下:

1
2
3
4
5
6
7
8
struct epoll_event event;
int ret;

ret = epoll_ctl (epfd, EPOLL_CTL_DEL, fd, &event);
if (ret)
{
perror ("epoll_ctl");
}

注意,当op设置为EPOLL_CTL_DEL时,由于没有提供事件掩码,event参数可能为NULL,在2.6.9以前的内核中,会检查该参数是否非空,为了和老版本兼容,必须传递一个有效的非空指针,不能只是声明。内核2.6.9版本修复了这个bug

等待epoll事件

系统调用epoll_wait()会等待和指定epoll实例关联的文件描述符上的事件:

1
2
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

当调用epoll_wait()时,等待epoll实例epfd中的文件fd上的事件,时限为timeout毫秒。成功时,events指向描述每个事件的epoll_event结构体的内存,且最多可以有maxevents个事件,返回值是事件个数;出错时,返回-1,并将errno设置为下值:

EBADF : epfd是一个无效的文件描述符

EFAULT : 进程对events所指向的内存没有写权限

EINTR : 系统调用在完成前发生信号中断或超时

EINVAL : epfd不是有效的epoll实例,或者maxevents小于或等于0

如果timeout为0,即使没有事件发生,调用也会立即返回0.如果timeout为-1,调用将一直等待到有事件发生才返回

当调用返回时,epoll_event结构体中的events变量描述了发生的事件。data变量保留了用户在调用epoll_ctl()前的所有内容

一个完整的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define MAX_EVENTS 64

struct epoll_event *events;
int nr_events, i, epfd;

events = malloc (sizeof (struct epoll_event) * MAX_EVENTS);
if (!events)
{
perror ("malloc");
return 1;
}

nr_events = epoll_wait (epfd, events, MAX_EVENTS, -1);
if (nr_events < 0)
{
perror ("epoll_wait");
free (events);
return 1;
}

for (i = 0; i < nr_events; i++)
{
printf ("event = %ld on fd = %d\n",events[i].events, events[i].data.fd);
}
free (events);

边缘触发事件和条件触发事件

如果epoll_ctrl()的参数event中的events项设置为EPOLLET,fd上的监听方式为边缘触发(ET),否则为条件触发(LT)

考虑下面的生产者和消费者在通过UNIX管道通信时的情况

  1. 生产者向管道写入1KB数据
  2. 消费者在管道上调用epoll_wait(),等待管道上有数据并可读

通过条件触发监视时,在步骤2中epoll_wait()会立刻返回,表示管道可读。通过边缘触发监视时,需要在步骤1发生后,步骤2才会返回

条件触发是默认行为,poll()和select()就是采用这种模式,也是大多数开发者所期望的。边缘触发需要不同的编程解决方案,通常是非阻塞I/O,而且需要仔细检查EAGAIN


epoll
https://carl-5535.github.io/2021/03/21/Linux系统编程/epoll/
作者
Carl Chen
发布于
2021年3月21日
许可协议