用epoll实现的IO多路复用
epoll是Linux下的IO多路复用的实现。这里单开一章是因为它非常有代表性,并且Linux也是目前最广泛被作为服务器的操作系统。细致的了解epoll对整个IO多路复用的工作原理非常有帮助。
与select和poll不同,要使用epoll是需要先创建一下的。
int epfd = epoll_create(10);
epoll_create在内核层创建了一个数据表,记录要注册的fd。接口会返回一个“epoll的文件描述符”指向这个表。注意,接口参数是一个表达要监听事件列表的长度的数值。但不用太在意,因为epoll内部随后会根据事件注册和事件注销动态调整epoll中表格的大小。

为什么epoll要创建一个用文件描述符来指向的表呢?这里有两个好处:
- epoll是有状态的,不像
select和poll那样每次都要重新传入所有要监听的fd,这避免了很多无谓的数据复制。epoll的数据是用接口epoll_ctl来管理的(增、删、改)。 - epoll文件描述符在进程被fork时,子进程是可以继承的。这可以给对多进程共享一份epoll数据,实现并行监听网络请求带来便利。但这超过了本文的讨论范围,就此打住。
epoll创建后,第二步是使用epoll_ctl接口来注册要监听的事件。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
其中第一个参数就是上面创建的表epfd。第二个参数op表示如何对文件名进行操作,共有3种。
EPOLL_CTL_ADD- 注册一个事件EPOLL_CTL_DEL- 取消一个事件的注册EPOLL_CTL_MOD- 修改一个事件的注册
第三个参数是要操作的fd,这里必须是支持NIO的fd(比如socket)。
第四个参数是一个epoll_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 events */
epoll_data_t data; /* User data variable */
};
比方说,想关注一个fd1的读取事件事件,并采用边缘触发(下文会解释什么是边缘触发),大概要这么写:
struct epoll_data ev;
ev.events = EPOLLIN | EPOLLET; // EPOLLIN表示读事件;EPOLLET表示边缘触发
ev.data.fd = fd1;
通过epoll_ctl就可以灵活的注册/取消注册/修改注册某个fd的某些事件。

第三步,使用epoll_wait来等待事件的发生。
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
特别留意,这一步是"block"的。只有当注册的事件至少有一个发生,或者timeout达到时,该调用才会返回。这与select和poll几乎一致。但不一样的地方是evlist,它是epoll_wait的返回数组,里面只包含那些被触发的事件对应的fd,而不是像select和poll那样返回所有注册的fd。

综合起来,一段比较完整的epoll代码大概是这样的。
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int nfds, epfd, fd1, fd2;
// 假设这里有两个socket,fd1和fd2,被初始化好。
// 设置为non blocking
setnonblocking(fd1);
setnonblocking(fd2);
// 创建epoll
epfd = epoll_create(MAX_EVENTS);
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
//注册事件
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = fd1;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd1, &ev) == -1) {
perror("epoll_ctl: error register fd1");
exit(EXIT_FAILURE);
}
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd2, &ev) == -1) {
perror("epoll_ctl: error register fd2");
exit(EXIT_FAILURE);
}
// 监听事件
for (;;) {
nfds = epoll_wait(epdf, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; ++n) { // 处理所有发生IO事件的fd
process_event(events[n].data.fd);
// 如果有必要,可以利用epoll_ctl继续对本fd注册下一次监听,然后重新epoll_wait
}
}
此外,epoll的手册 中也有一个简单的例子。
所有的基于IO多路复用的代码都会遵循这样的写法:注册——监听事件——处理——再注册,无限循环下去。

本文详细介绍了如何使用Linux epoll进行IO多路复用,包括epoll_create创建数据表、epoll_ctl注册和管理事件、epoll_wait等待事件,以及边缘触发的工作原理。通过实例展示了完整的工作流程。
https://www.bilibili.com/video/BV1qL411u7eE?spm_id_from=333.999.0.0
1169

被折叠的 条评论
为什么被折叠?



