在前面学习了select、poll和非阻塞IO之后,再继续学习一下性能更加优越的epoll。下面放置了一张图,这张图来自 The Linux Programming Interface(No Starch Press)。这张图直观地为我们展示了 select、poll、epoll 几种不同的 I/O 复用技术在面对不同文件描述符大小时的表现差异。
从图中可以明显地看到,epoll 的性能是最好的,即使在多达 10000 个文件描述的情况下,其性能的下降和有 10 个文件描述符的情况相比,差别也不是很大。而随着文件描述符的增大,常规的 select 和 poll 方法性能逐渐变得很差。
那么,epoll是如何做到的呢?
一、相关函数介绍
epoll 可以说是和 poll 非常相似的一种 I/O 多路复用技术,epoll 通过监控注册的多个描述字,来进行 I/O 事件的分发处理。不同于 poll 的是,epoll 不仅提供了默认的 level-triggered(条件触发)机制,还提供了性能更为强劲的 edge-triggered(边缘触发)机制,在后面小结会展开分析。
使用 epoll 进行网络程序的编写,需要三个步骤,分别是
- epoll_create
- epoll_ctl
- epoll_wait
下面将逐一分析一下。
1.1 epoll_create
函数原型如下:
int epoll_create(int size);//创建一颗监听红黑树
参数如下:
- size:创建的红黑树的监听节点个数(仅供内核参考),不过现在被自动忽略了,内核可以动安胎分配了,但是该值仍需要一个大于0的整数。
- 返回值:成功时,指向新创建的红黑树的根节点的fd;失败时,返回-1 errno。
1.2 epoll_ctl
函数原型如下:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//操作监听红黑树
参数如下:
epfd:epoll_create函数的返回值。
op:对该监听红黑树所做的操作。
- EPOLL_CTL_ADD:添加fd到监听红黑树。
- EPOLL_CTL_DEL:将一个fd从监听红黑树上摘下(取消监听)。
- EPOLL_CTL_MOD:修改fd在红黑树上的监听事件。
fd:待监听的套接字。
event:本质上是struct epoll_event结构体的地址
typedef union epoll_data { void *ptr; int fd; //对应监听事件的fd uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; epoll_data_t data; };
- 成员event:EPOLLIN、EPOLLOUT、EPOLLEDHUP(套接字的一端关闭,或者半关闭)、EPOLLHUP(对应的文件描述符被挂起)和EPOLLET(ET或者LT)
返回值:成功返回0;失败 -1 errno;
1.3 epoll_wait
函数原型如下:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_wait() 函数类似之前的 poll 和 select 函数,调用者进程被挂起,在等待内核 I/O 事件的分发。
参数如下:
- epfd:epoll_create函数的返回值。
- events:传出参数,是一个结构体数组,存储了满足监听条件的那些fd结构体,事件类型取值和 epoll_ctl 可设置的值一样,这个 epoll_event 结构体里的 data 值就是在 epoll_ctl 那里设置的 data,也就是用户空间和内核空间调用时需要的数据。
- maxevents:大于0的整数,表示epoll_wait 可以返回的最大事件值。
- timeout:epoll_wait 阻塞调用的超时值,如果这个值设置为 -1,表示不超时;如果设置为 0 则立即返回,即使没有任何 I/O 事件发生。
- 返回值:成功时,返回大于0,表事件个数;返回0表示超时时间到了;若出错,则返回-1。
二、示例代码
#define MAXEVENTS 128
char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
int main(int argc, char **argv) {
int listen_fd, socket_fd;
int n, i;
int efd;
struct epoll_event event;
struct epoll_event *events;
listen_fd = tcp_nonblocking_server_listen(SERV_PORT);
efd = epoll_create(1); //创建监听红黑树套接字的树根
if (efd == -1) {
error(1, errno, "epoll create failed");
}
//初始化监听套接字的事件
event.data.fd = listen_fd;
event.events = EPOLLIN | EPOLLET;
if (epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1) { //将listen_fd添加到监听红黑树上
error(1, errno, "epoll_ctl add listen fd failed");
}
/* Buffer where events are returned */
events = calloc(MAXEVENTS, sizeof(event));
while (1) {
n = epoll_wait(efd, events, MAXEVENTS, -1); //实时监听
printf("epoll_wait wakeup\n");
for (i = 0; i < n; i++) {
if ((events[i].events & EPOLLERR) ||
(events[i].events & EPOLLHUP) ||
(!(events[i].events & EPOLLIN))) {
fprintf(stderr, "epoll error\n");
close(events[i].data.fd);
continue;
} else if (listen_fd == events[i].data.fd) { //listen_fd满足读事件,有新的客户端发起连接请求
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *) &ss, &slen);
if (fd < 0) {
error(1, errno, "accept failed");
} else {
make_nonblocking(fd);
event.data.fd = fd; //初始化cfd的监听事件
event.events = EPOLLIN | EPOLLET; //edge-triggered
if (epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1) {
error(1, errno, "epoll_ctl add connection fd failed");
}
}
continue;
} else {
socket_fd = events[i].data.fd;
printf("get event on socket fd == %d \n", socket_fd);
while (1) {
char buf[512];
if ((n = read(socket_fd, buf, sizeof(buf))) < 0) {
if (errno != EAGAIN) {
error(1, errno, "read error");
close(socket_fd);
}
break;
} else if (n == 0) {
close(socket_fd);
break;
} else {
for (i = 0; i < n; ++i) {
buf[i] = rot13_char(buf[i]);
}
if (write(socket_fd, buf, n) < 0) {
error(1, errno, "write error");
}
}
}
}
}
}
free(events);
close(listen_fd);
}
三、ET模式和LT模式
3.1 ET模式------>边沿触发:
缓冲区剩余未读尽的数据不会导致epoll_wait返回(因为是想同的事件),有新的事件满足,才会触发
用法:
struct epoll_event event; event.events = EPOLLIN | EPOLLET
3.2 LT模式-------->水平触发
- 默认采用的模式
- 缓冲区剩余未读尽的数据会导致epoll_wait返回
四、总结
本文主要是对epoll的相关函数进行了介绍,也给出了示例代码。通过代码或者函数的介绍,想必你也大概知道了,为什么epoll比poll模型更加高效,epoll返回的是有事件发生的数组,而poll返回的是准备好的个数,每次poll函数返回都要注册的描述符结合数组,尤其是当数量越大遍历次数越多。
随后分析了ET和LT两种方式,不知道你发现没有,epoll的ET模式是一种高效的模式(也就是其他请求不会卡住),但是只支持非阻塞模式(这个应该很好理解)。