前面了解了 epoll 的种种优点以及基本的使用框架后,相信你应该跃跃欲试了。不过再此之前,还需要将 epoll 的那个三函数细致的详解一下。
1. epoll 接口
epoll 提供了三个函数:
int epoll_create(int size);
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);
之前也大致讲解了下这些函数的功能,下面就要详细展开了。
1.1 epoll_create
int epoll_create(int size);
- 函数语义
用于创建一个 epoll 对象(epoll instance),同时返回该对象的描述符。
- 参数 size
表示你想监听几个描述符,或者说待会儿你想添加多少个描述符到 epoll 对象中. 从 Linux 2.6.8 内核开始,参数 size 已经没什么用了,但是使用的时候必须大于 0.
1.2 epoll_ctl
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 事件 */
epoll_data_t data; /* 用户数据 */
};
- 函数语义
根据参数 op 决定向 epoll 对象中添加、修改还是删除描述符。
- 参数 epfd
epoll 对象的描述符,由 epoll_create 函数返回。
- 参数 op
它有三个可选值:
值 | 含义 |
---|---|
EPOLL_CTL_ADD | 将参数 fd 指定的描述符添加到 epoll 对象中,同时将其关联到一个 epoll 事件对象——即参数 event 所指定的值 |
EPOLL_CTL_MOD | 修改描述符 fd 所关联的事件对象 event,前提是该 fd 已经添加到了 epoll 对象中 |
EPOLL_CTL_DEL | 将描述符 fd 从 epoll 对象中移除,此时参数 event 被忽略,也可指定为 NULL |
- 参数 event
该参数的类型是 struct epoll_event
结构体指针,结构体定义在上而已经给出了。event 参数关联到参数 fd 上,表示想监听描述符 fd 上的哪种 IO 事件,比如可读事件,可写事件。有关 IO 事件的含义,在上一篇文章已经详细说明了。
这里我们主要关心的是结构体成员 events. 它有下面的值:
值 | 含义 |
---|---|
EPOLLIN | 监听 fd 是否可读 |
EPOLLOUT | 监听 fd 是否可写 |
EPOLLRDHUP | Linux 2.6.17 后可用。监听流式套接字对象是否关闭或半关闭 |
EPOLLPRI | 监听是否有紧急数据可读 |
上面这四个值都需要我们主动去监听才行,然后使用 epoll_wait 函数去等待你关心的这些描述符是否有事件发生。
除此之外,还有一个可选项 EPOLLET,这个不是要监听的事件类型,但是它却要通过 events 成员传递。它表示设置关联的描述符 IO 事件触发模式(大白话就是什么时候应该产生 IO 事件)。
EPOLLET 表示触发模式为边沿触发(Edge Triggered)。如果不指定触发模式,默认情况下为水平触发(Level Triggered)。目前,我们不设置 EPOLLET 选项,让其默认为水平触发。有关触发模式后面会详细讲,这里大家不用关心。
1.3 epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 函数语义
监听所有描述符上是否有事件发生。这些描述符之前都由 epoll_ctl 添加到了由 epfd 参数所引用的 epoll 对象中。
如果所有描述符上都没有 IO 事件发生,该函数会阻塞,直到有事件到来。一旦有事件到来,epoll_wait 函数就返回。同时将所有发生的事件保存到数组中,该数组的地址以及数组大小由你自己通过参数 events 和 maxevents 指定。同时 epoll_wait 会返回发生事件的个数。
- 参数 events
events 数组的结构体类型前面已经详细介绍过了,这里还有补充。
epoll_wait 函数中的 events 是一个输出参数,充当了函数的返回值。如果 epoll_wait 返回了,会把所有发生的事件保存在数组 events 中,如果发生事件的个数比 events 数组的大小还要多……这个没问题,因为参数 maxevents 已经告诉内核,我数组只有这么大,其它的放不下的就下次再给我吧。
返回的 events 数组中,每个元素都表示一个事件,假设 epoll_wait 返回值是 3,就表示有 3 个事件发生了,那么你就挨个处理 events[0]、events[1] 和 events[2] 就行了。
如何知道 events[i] 是哪个描述符发生的事件?注意该结构体还有一个成员,是用户数据,即 data 成员,一般来说你在添加描述符的时候,需要将 data 设置成描述符 fd 的值,当 events 返回的时候,epoll 会帮你把一开始由 epoll_ctl 函数传给它的那个 data 放到这里。
当 epoll 返回时,events[i].events 中就保存了该描述符发生了哪些事件,除了表 2 中的值以外,还有一些异常事件,即使你不主动监听,如果发生了也会主动通知你:
值 | 含义 |
---|---|
EPOLLERR | 描述符有错误,这种非常少见,比如硬件上的问题 |
EPOLLHUP | 关联的描述符有一端挂断,比如管道一端关闭 |
- 参数 timeout
超时参数。
timeout = -1,永远等待。
timeout = 0,立即返回。
timeout > 0,最长等待 timeout 毫秒。
这个参数和 poll 是一样的。
- 返回值
返回值 > 0,表示有几个事件发生。
返回值 = 0,表示超时时间到了。
返回值 < 0,则出错,同时设置 errno 的值。
2. 实验
程序 epoll.c 仍然使用 select 和 poll 中的那个案例,这里只是改成了 epoll 的方式。
2.1 代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#define PERR(msg) do { perror(msg); exit(1); } while(0);
// 信号处理函数,验证 epoll_wait 会被信号打断
void handler(int sig) {
if (sig == SIGINT) {
puts("hello SIGINT");
}
}
// 处理描述符上发生的事件
int process(char* prompt, int fd) {
int n;
char buf[64];
char line[64];
n = read(fd, buf, 63);
if (n < 0) {
// error
PERR("read");
}
else if (n == 0) {
// peer close
sprintf(line, "%s closed\n", prompt);
puts(line);
return 0;
}
else if (n > 0) {
buf[n] = 0;
sprintf(line, "%s say: %s", prompt, buf);
puts(line);
}
return n;
}
int main () {
int i, n, res;
char buf[64];
int fds[3];
int fd;
if (SIG_ERR == signal(SIGINT, handler)) {
PERR("signal");
}
fds[0] = STDIN_FILENO;
fds[1] = open("a.fifo", O_RDONLY);
printf("open pipe: fd = %d\n", fds[1]);
fds[2] = open("b.fifo", O_RDONLY);
printf("open pipe: fd = %d\n", fds[2]);
// 事件数组 evts 用来保存 epoll_wait 返回的事件
struct epoll_event evts[4];
// 创建一个 epoll 实例对象
int epfd = epoll_create(4);
// 添加你所关心的描述符到 epoll 实例对象中
for (i = 0; i < 3; ++i) {
struct epoll_event ev;
ev.data.fd = fds[i]; // 注意这个值必须要指定,不然 epoll_wait 返回了你也不知道是谁发生了事件
ev.events = EPOLLIN; // 想监听可读事件,因为没有指定 EPOLLET 选项,所以默认是水平触发
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fds[i], &ev) < 0) {
PERR("epoll_ctl");
}
}
while(1) {
// 开始等待事件发生,res 表示发生了事件的个数
res = epoll_wait(epfd, evts, 4, -1);
printf("res = %d\n", res);
if (res < 0) {
// error
if (errno == EINTR) {
perror("epoll_wait");
continue;
}
PERR("epoll_wait");
}
else if (res == 0) {
// timeout
continue;
}
// 开始处理所有事件
for (i = 0; i < res; ++i) {
// 这个 fd 就是你一开始通过 event 的 data 成员传进去的。
fd = evts[i].data.fd;
if (evts[i].events & EPOLLIN) {
sprintf(buf, "fd%d", fd);
process(buf, fd);
}
// 这里我们根据没有监听可写事件,所以这种情况不会发生。
if (evts[i].events & EPOLLOUT) {
printf("fd%d can write\n", i);
}
// 下面这两个事件就算你没有监听,也可能会产生,需要单独处理
if (evts[i].events & EPOLLERR) {
printf("fd%d Error\n", i);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}
if (evts[i].events & EPOLLHUP) {
printf("fd%d Hang up\n", i);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}
}
}
}
2.2 编译和运行
- 编译
gcc epoll.c -o epoll
- 运行
图1 运行结果
3. 总结
- 掌握 epoll 的三个函数
- 知道 epoll 的两种模式
练习:当然是写代码了。。。