深入理解epoll模型:原理、编程接口与触发模式详解
深入理解epoll模型:原理、编程接口与触发模式详解
在高并发场景下,传统的select和poll机制由于设计缺陷无法高效处理大量网络连接。epoll作为Linux内核提供的IO复用机制,通过用户态和内核态的高效协作,能够很好地解决海量并发请求问题。本文将深入解析epoll的工作原理、编程接口以及条件触发和边缘触发的区别。
1. 从内核看epoll机制
select和poll虽然能够实现IO复用的功能,但是由于设计的缺陷,select和poll无法处理海量的网络连接,并且随着网络连接数量的增加,select和poll效率越来越低。
此时急需一种更为高效的IO复用机制解决海量并发请求问题,epoll机制就是为了解决该问题而诞生的。
epoll机制分为两个部分:用户态部分和内核态部分。
用户态部分通过3个系统调用:epoll_create,epoll_ctl,epoll_wait和内核进行交互。内核态部分实现比较复杂,我们将围绕struct eventpoll内核对象来讲解。struct eventpoll对象是epoll机制实现的关键数据结构,包含三个重要成员:rbr(红黑树),rdlist(就绪队列),wq(等待队列)。
- 红黑树:用于记录用户程序注册的epoll事件。
- 等待队列:epoll线程休眠后,用于唤醒epoll线程。
- 就绪队列:socket接收和发送数据后,就绪队列会记录socket读事件和写事件。
用户程序调用epoll_create函数后,会在内核创建struct eventpoll对象,同时会返回一个文件描述符给用户,该文件描述符用于查询进程文件表,找到对应的文件,再通过文件找到struct eventpoll对象。
用户程序通过epoll_ctl函数添加,修改,删除socket事件,注册成功的socket事件会插入红黑树。socket事件添加成功后,epoll才能监听socket读写事件。
如果epoll就绪队列有就绪事件,用户程序调用epoll_wait函数会成功获取到就绪事件。如果没有就绪事件,则epoll线程陷入休眠。
当socket接收到数据后,通过socket等待队列可以唤醒休眠的epoll线程,并将socket封装成epoll就绪事件插入就绪队列。此时epoll线程已经被唤醒,epoll线程可以将就绪事件拷贝至用户程序。
2. epoll编程实战
epoll_create函数
#include <sys/epoll.h>
int epoll_create(int size);
参数:size参数并没有实际意义,但一定要大于0。
返回值:成功返回epoll文件描述符;失败返回-1,并设置errno。
epoll_ctl函数
epoll_ctl函数用于向 epoll 实例中添加、修改或删除文件描述符(通常代表一个网络连接或者文件),并设置这些文件描述符感兴趣的事件类型,如可读、可写或者有异常发生。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- epfd:指向由epoll_create 创建的 epoll 实例的文件描述符。
- op:表示要对目标文件描述符执行的操作,可以是以下几个值之一:
- EPOLL_CTL_ADD:向 epoll 实例中添加一个新的文件描述符。
- EPOLL_CTL_MOD:修改已存在文件描述符的事件类型。
- EPOLL_CTL_DEL:从 epoll 实例中删除一个文件描述符。
- fd:需要操作的目标文件描述符。
- event:指向struct epoll_event结构的指针,该结构指定了需要监听的事件类型。
返回值:成功返回0;失败返回-1,并设置errno。
epoll_event
struct epoll_event {
uint32_t events;
epoll_data_t data;
}
uint32_t events
该字段用于存储事件的类型掩码,常见的事件类型
事件类型 值 (十六进制) 描述
EPOLLIN 0x001 文件描述符可读
EPOLLOUT 0x004 文件描述符可写
EPOLLRDHUP 0x2000 对端关闭连接或半关闭连接
EPOLLPRI 0x010 有紧急数据可读(带外数据)
EPOLLERR 0x008 文件描述符发生错误
EPOLLHUP 0x0100 文件描述符挂起事件
EPOLLET 0x0001 边缘触发模式(Edge Triggered)
EPOLLONESHOT 0x40000000 一次性触发事件,触发后需重新注册data
源码
typedef union epoll_data {
void *ptr; // 指向用户自定义数据的指针
int fd; // 文件描述符
uint32_t u32; // 32位无符号整数
uint64_t u64; // 64位无符号整数
} epoll_data_t;
- ptr:可以指向用户自定义的结构体,方便在事件触发时快速获取与文件描述符相关的上下文信息。
- fd:通常用于存储与事件关联的文件描述符。
- u32 和 u64:可以用于存储其他类型的整数
epoll_wait函数
epoll_wait函数用于等待在 epoll 实例上注册的文件描述符上发生的事件。这个函数会阻塞调用线程,直到有事件发生或超时。
函数原型
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- epfd
类型:int
含义:epoll 文件描述符,是通过 epoll_create 或 epoll_create1 创建的。它用于标识一个 epoll 实例,相当于一个事件集合的句柄。 - events
类型:struct epoll_event *
含义:指向一个 struct epoll_event 类型的数组,用于存储 epoll_wait 返回的就绪事件。epoll_wait 会将就绪的事件及其相关信息填充到这个数组中。
每个 epoll_event 结构体包含两个字段:
uint32_t events:表示事件类型,如 EPOLLIN、EPOLLOUT 等。
epoll_data_t data:联合体,用于存储与事件相关的数据,可以是文件描述符、指针或其他整数值。 - maxevents
类型:int
含义:指定 events 数组的最大容量,即 epoll_wait 最多可以返回的事件数量。必须大于 0。
如果实际就绪的事件数量超过 maxevents,epoll_wait 只会返回前 maxevents 个事件。 - timeout
类型:int
含义:指定 epoll_wait 的超时时间,单位为毫秒。
如果 timeout > 0,表示等待指定的毫秒数。如果在这段时间内没有任何事件发生,函数将返回。
如果 timeout == 0,表示非阻塞模式,epoll_wait 会立即返回,无论是否有事件就绪。
如果 timeout == -1,表示阻塞模式,epoll_wait 会一直等待,直到有事件发生或被信号中断。
3. epoll编程流程
前面我们已经学会了使用epoll 3个接口,接下来我们要实现一个完整epoll编程示例,
该流程是一个epoll编程流程,我们按照这样一个流程去编写代码,思路会很清晰,不容易出错。
int main(int argc, char *argv[]){
structepoll_event ev, events[MAX_EVENTS];//数组
int sock_fd, ret = 0;
int efd = epoll_create(10); //创建epoll实例
ev.data.fd = sock_fd;//连接
ev.events = EPOLLIN;//监听为可读
//注册监听套接字事件
epoll_ctl(efd, EPOLL_CTL_ADD, sock_fd, &ev);
while (1) {
//超时1000毫秒,获取就绪事件
int nfds = epoll_wait(efd, events, MAX_EVENTS, 1000);
if (nfds == -1) return-1; //获取失败退出
elseif (nfds == 0) continue; //超时,继续下一轮事件获取
for (int i = 0; i < nfds; i++) {//轮询就绪事件数组
int fd = events[i].data.fd;
if (fd == sock_fd) { //监听套接字
new_fd = accept(sock_fd, (struct sockaddr *)&peer, &addrlen);
setnonblocking(new_fd); //设置新套接字为非阻塞模式
ev.data.fd = new_fd;
ev.events = EPOLLIN|EPOLLET;
//添加新套接字
epoll_ctl(efd, EPOLL_CTL_ADD, new_fd, &ev);
} else { //业务套接字
if (events[i].events & EPOLLIN) { //EPOLLIN事件
recv(fd, recv_buf, len, 0); //业务套接字接收数据
}
}
}
}
return 0;
}
其实明显相对比
select就像亲自下基层视察的老板,那么epoll这个老板就要显得精明的多了。他可不亲自下基层,他找了个美女秘书,他只要盯着他的秘书看就行了,呸,他只需要听取他的秘书的汇报就行了。汇报啥呢?基层有任何消息,跟秘书说,秘书汇总之后一次性交给老板来处理。这样老板的时间不就大大的提高了嘛。
4. 条件触发和边缘触发
有些人学习 epoll时往往无法正确区分条件触发( Level Trigger )和边缘触发( Edge Trigger ),但只有理解了二者区别才算完整掌握epoll
首先给 出示例帮助各位理解条件触发和边缘触发。 观察如下对话,可以通过对话内容理解条件触发事件的特点
条件触发
儿子:“妈妈,我收到了5000元压岁钱。”
妈妈:“哇,真棒!”
儿子:“我给隔壁家秀熙买了炒年糕,花了2000元。”
妈妈:“嗯,做得好!”
儿子:“妈妈,我还买了玩具,剩下500元。”
妈妈:“用完零花钱就只能挨饿喽!”
儿子:“妈妈,我还留着那500元没动,不会挨饿的。”
妈妈:“哦,很明智嘛。”
儿子:“妈妈,我还留着那500元没动,我要攒起来。”
妈妈:“哦,加油!”边缘触发
儿子 : "妈妈,我收到了 5000元压岁钱 。
妈妈 : "嗯,再接再厉 。 "
儿子 : ‘’• • • • • •‘’
妈妈 :‘‘说,话呀 ! 压岁钱呢 ? 不想回答吗’’
总结
无疑是有记忆和无记忆的区别
水平触发(LT):有记忆
行为特点:
水平触发模式下,只要文件描述符的状态满足条件(例如可读或可写),epoll 就会一直报告该事件,直到状态改变。
例如,如果一个文件描述符可读,epoll_wait 会反复返回该事件,直到数据被读取完,或者文件描述符不再可读。
“有记忆”:LT 模式下,epoll 会持续跟踪文件描述符的状态,只要状态满足条件,就会一直触发事件。即使程序没有立即处理事件,epoll 也会在后续调用中继续报告该事件。边缘触发(ET):无记忆
行为特点:
边缘触发模式下,epoll 只在文件描述符的状态发生变化时报告事件,而不是持续跟踪状态。
例如,如果一个文件描述符从不可读变为可读,epoll_wait 会报告一次事件;但如果程序没有立即读取数据,epoll 不会再次报告该事件,直到下一次状态发生变化(例如数据被读取后又有新数据到来)。
“无记忆”:ET 模式下,epoll 不会持续跟踪文件描述符的状态,只会报告状态的变化。如果程序没有及时处理事件,可能会错过某些状态变化,导致数据丢失或事件未被处理。
LT(水平触发):有记忆,持续跟踪状态,适合简单场景,但可能效率较低。
ET(边缘触发):无记忆,只报告状态变化,适合高性能场景,但需要程序更谨慎地处理事件,确保不遗漏数据。
参考文献:详解epoll