epoll() 教程 – 学会epoll()只需 3 个简单的步骤
太久没写博客了,最近都在研究一些跟专业无关的东西,什么证券从业资格,区块链,solidity,宏观微观经济,中国通史,明清史,魏晋南北朝史,四大名著。。。偏偏吧吃饭的家伙落下了。罪孽罪孽,赶紧更新一篇技术博客,找回一下感觉。看了下自己的GitHub,最近几个月也是一片空白,说好的要为开源世界留下点什么到现在也没能完成,感慨一下自己没什么毅力,果然成不了大事。
说回到epoll()。最近买了一本关于nginx的书,详细介绍了nginx的架构与底层实现。nginx作为服务器,可以轻松应对上万的并发访问。之所以这么高效,最大的原因就是nginx使用了IO多路复用模型epoll。epoll可以以恒定的时间(O(1))监控文件描述符。
在 Michael Kerrisk 的《The Linux Programming Interface》一书中,第 63.4.5 节提供了一个表,描述了通过一些最常见的轮询方法检查不同数量的文件描述符所需的时间。
如图所示,随着描述符数量的增加,与poll() 或 select() 相比,epoll()有巨大的优势。
本教程将介绍在 Linux 2.6+ 上使用 epoll() 的一些基础知识。P.S. epoll()在Linux2.6+才被引入。
Setp1 创建epoll文件描述符
#include <stdio.h> // for fprintf()
#include <unistd.h> // for close()
#include <sys/epoll.h> // for epoll_create1()
int main()
{
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
fprintf(stderr, "Failed to create epoll file descriptor\n");
return 1;
}
if (close(epoll_fd)) {
fprintf(stderr, "Failed to close epoll file descriptor\n");
return 1;
}
return 0;
}
运行这个程序应该不会有任何文字输出。在这里我们是用 epoll_create1()
创建了一个epoll文件描述符,这个文件描述符与其他Linux文件描述符一样,可以使用 close()
关闭。
有人会问了,为什么这里用的是epoll_create1()
而不是 epoll_create()
? 这里先放一下这两个函数的原型:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
如果你看过epoll的源码(可以参考 https://www.nowcoder.com/discuss/26226 )就知道,epoll_create
的参数size会被忽略,填什么都一样,但是必须大于0。这是因为最初设计epoll_create时,size参数用于通知内核,调用者想要加入到epoll中的文件描述符数量,内核根据size来分配空间,在Linux2.6.8之后size参数就不再需要了,因为内核可以动态分配空间了,但是为了保持向下兼容,需要传参的时候size大于0。
至于epoll_create1
,flags 可以设置为0 或者EPOLL_CLOEXEC,为0时函数表现与epoll_create一致,EPOLL_CLOEXEC标志与open 时的O_CLOEXEC 标志类似,即进程被替换时会关闭文件描述符。
水平触发和边缘出发
水平触发和边缘触发是从电气工程中借用的术语。水平触发比较常见,例如select、poll等都是水平触发的,而epoll不仅支持水平触发,也支持边缘触发。这两者的区别是,水平触发时只要满足条件,就触发一个事件;而边缘触发是只有当状态变化时才会触发事件。
在水平触发的情况下,必须不断的轮询监控每个文件描述符的状态,判断其是否可读或可写。
而边缘触发的情况下,只有在数据到达网卡,也就是说 I/O 状态发生改变时才会触发事件,而且I/O 操作必须一次性的将数据处理完。因为如果没有处理完数据,只有等待下次数据包到达网卡才会再次触发事件。
一般来讲,水平触发是默认设置,更易于使用,在实际开发中一般不会用边缘触发。
Setp2 为epoll添加待观测文件描述符
接下来要做的就是告诉epoll要监视的文件描述符以及要监视的事件类型。
#include <stdio.h> // for fprintf()
#include <unistd.h> // for close()
#include <sys/epoll.h> // for epoll_create1(), epoll_ctl(), struct epoll_event
int main()
{
struct epoll_event event;
int epoll_fd = epoll_create1(0);
if(epoll_fd == -1)
{
fprintf(stderr, "Failed to create epoll file descriptor\n");
return 1;
}
event.events = EPOLLIN;
event.data.fd = 0;
if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event))
{
fprintf(stderr, "Failed to add file descriptor to epoll\n");
close(epoll_fd);
return 1;
}
if(close(epoll_fd))
{
fprintf(stderr, "Failed to close epoll file descriptor\n");
return 1;
}
return 0;
}
这里我添加了一个epoll_event结构的实例,并使用epoll_ctl()将文件描述符0添加到了我们的epoll实例epoll_fd中。event作为我们传入的最后一个参数,让epoll知道我们仅观察输入事件EPOLLIN,并提供一些用户定义的数据,这些数据将随事件返回。
Setp3 收获
好了,现在我们可以使用epoll完成我们的目标了
#define MAX_EVENTS 5
#define READ_SIZE 10
#include <stdio.h> // for fprintf()
#include <unistd.h> // for close(), read()
#include <sys/epoll.h> // for epoll_create1(), epoll_ctl(), struct epoll_event
#include <string.h> // for strncmp
int main()
{
int running = 1, event_count, i;
size_t bytes_read;
char read_buffer[READ_SIZE + 1];
struct epoll_event event, events[MAX_EVENTS];
int epoll_fd = epoll_create1(0);
if(epoll_fd == -1)
{
fprintf(stderr, "Failed to create epoll file descriptor\n");
return 1;
}
event.events = EPOLLIN;
event.data.fd = 0;
if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event))
{
fprintf(stderr, "Failed to add file descriptor to epoll\n");
close(epoll_fd);
return 1;
}
while(running)
{
printf("\nPolling for input...\n");
event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, 30000);
printf("%d ready events\n", event_count);
for(i = 0; i < event_count; i++)
{
printf("Reading file descriptor '%d' -- ", events[i].data.fd);
bytes_read = read(events[i].data.fd, read_buffer, READ_SIZE);
printf("%zd bytes read.\n", bytes_read);
read_buffer[bytes_read] = '\0';
printf("Read '%s'\n", read_buffer);
if(!strncmp(read_buffer, "stop\n", 5))
running = 0;
}
}
if(close(epoll_fd))
{
fprintf(stderr, "Failed to close epoll file descriptor\n");
return 1;
}
return 0;
}
我在这里添加了一些新变量来支持和表达我在做什么。我还添加了一个while循环,该循环将持续从正在监视的文件描述符中读取数据,直到收到一个'stop'。我使用epoll_wait()来等待epoll实例上事件的发生,结果将存储在事件数组中,最多MAX_EVENTS,超时时间为30秒。 epoll_wait()的返回值表示事件数组中有多少个事件数据被填充。除此之外,它还打印出所得到的内容,并执行一些基本的逻辑来完成所有的事情
参考:
https://suchprogramming.com/epoll-in-3-easy-steps/
https://wenku.baidu.com/view/cf03dc2293c69ec3d5bbfd0a79563c1ec5dad7b4.html
https://man7.org/linux/man-pages/man2/epoll_create.2.html