目录
一、epoll 模型
1.1 前导知识
1.1.1 宏 offsetof
1.1.2 手动计算
1.2 epoll 模型
二、 epoll 工作模式
2.1 水平触发
特点:
2.2 边缘触发
特点:
边缘触发模式中的循环读取
结合非阻塞模式的优势
一、epoll 模型
经过了之前的学习,了解到了在 OS 内部,帮我们维护着的 epoll 模型由一棵红黑树与一个就绪队列组成,但是实际上真是如此吗?
其实并不是的,这是我们逻辑上抽象的概念,但事实并非如此。
1.1 前导知识
首先,需要思考一个问题,如果我们知道了结构体的某个成员的地址,能否得到该结构体的起始地址呢?
在这个问题上,C/C++ 为我们提供了一个宏 offsetof ,除此之外,还可以手动计算偏移量得到结构体的起始地址。
1.1.1 宏 offsetof
在C/C++中,如果知道一个结构体成员的地址,可以使用offsetof
宏和指针运算来找到整个结构体的起始地址。具体来说,可以使用以下方法:
- 使用
offsetof
宏来获取结构体成员相对于结构体起始地址的偏移量。 - 使用指针运算,从成员的地址减去这个偏移量。
#include <stdio.h>
#include <stddef.h>
typedef struct
{
int a;
double b;
char c;
} MyStruct;
int main()
{
MyStruct myStruct;
MyStruct* ptrToStruct = &myStruct;
// 获取成员b的地址
double* ptrToMember = &(myStruct.b);
// 计算结构体起始地址
MyStruct* startAddress = (MyStruct*)((char*)ptrToMember - offsetof(MyStruct, b));
// 输出结果
printf("结构体起始地址: %p\n", (void*)startAddress);
printf("原结构体地址: %p\n", (void*)ptrToStruct);
return 0;
}
1.1.2 手动计算
只要知道结构体成员的相对位置,就可以手动计算偏移量找到结构体的起始地址。
-
定义结构体和成员变量: 定义一个包含多个成员的结构体,并定义一个该结构体类型的变量。
-
获取成员的地址: 获取某成员的地址。
-
计算成员相对于结构体起始地址的偏移量: 通过指针运算和类型转换,计算成员在结构体中的偏移量。
-
通过偏移量计算结构体的起始地址: 通过成员地址减去偏移量,计算出结构体的起始地址。
#include <stdio.h>
typedef struct
{
int a;
double b;
char c;
} MyStruct;
int main()
{
MyStruct myStruct;
MyStruct* ptrToStruct = &myStruct;
// 获取成员b的地址
double* ptrToMember = &(myStruct.b);
// 计算成员b的偏移量 (假设结构体对齐方式与平台有关,手动计算)
size_t offset = (size_t)((char*)&(myStruct.b) - (char*)&myStruct);
// 计算结构体起始地址
MyStruct* startAddress = (MyStruct*)((char*)ptrToMember - offset);
// 输出结果
printf("结构体起始地址: %p\n", (void*)startAddress);
printf("原结构体地址: %p\n", (void*)ptrToStruct);
return 0;
}
1.2 epoll 模型
经过前导知识的学习,我们将现在来看一下运用前面知识形成的一个结构:
在这种结构中,next 与 prev 并不指向其他节点的头,而是指向其他节点的同一成员,经过前面的学习,我们知道通过结构体中的某个成员就可以得到结构体的起始地址,就可以访问到结构体的其他成员。
下面正式来看一下, epoll 模型中的节点。
在epoll的实现中,每个被监视的文件描述符(fd)都与一个
struct epitem
结构体实例相关联。这个结构体包含了红黑树节点(struct rb_node rbn
)和双向链表节点(struct list_head rdllink
)。红黑树用于快速查找和管理文件描述符,而双向链表节点则用于构建就绪队列。
struct epitem
{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的 eventpoll 对象
struct epoll_event event; //期待发生的事件类型
}
其中:
rbn
是红黑树节点,用于将该epitem
插入到红黑树中。rdllink
是双向链表节点,用于将该epitem
插入到就绪队列中。
在epoll的实现中,当一个文件描述符准备好进行I/O操作时,相应的 epitem
结构体会从红黑树中取出,并插入到就绪队列中(通常是一个双向链表)。这种设计避免了额外的数据结构,因为 epitem
结构体本身既可以作为红黑树的节点,也可以作为就绪队列的节点。
所以真正的 epoll 模型,应该是类似上图这种形式,就绪队列与红黑树相辅相成。
二、 epoll 工作模式
epoll 有 2 种工作方式-水平触发(LT)和边缘触发(ET)
举例子来说,我们的数据就像是快递, OS 就像是快递小哥。当我们的快递是顺丰快递时,若未取走快递,快递小哥会等着我们并一直打电话催促我们来拿快递;当我们的快递是其他快递时,快递小哥当时给我们打电话,我们可以选择当场去拿,如果当场没去,我们的快递就会被放进菜鸟驿站
上面的顺丰就是水平触发,其他就是边缘触发。
2.1 水平触发
水平触发是 epoll
的默认工作模式。这种模式下,只要文件描述符上有未处理的事件,每次调用 epoll_wait
都会通知你。这意味着,只要有数据可读或空间可写,epoll_wait
就会返回该文件描述符。
特点:
- 持续通知:只要有事件未被处理,
epoll_wait
每次调用都会返回。 - 简单易用:处理方式类似于
poll
和select
,更容易使用和理解。 - 适合阻塞式 I/O:适合处理阻塞式 I/O 操作,不容易漏掉事件。
2.2 边缘触发
边缘触发是一种高效的事件通知模式,但使用起来更加复杂。在这种模式下,只有当文件描述符从无事件到有事件变化时才会通知你。换句话说,只有当文件描述符的状态发生变化时(例如,从不可读变为可读),epoll_wait
才会返回该文件描述符。
特点:
- 一次性通知:只在状态变化时通知一次,如果不一次性处理完所有数据,后续不会再收到通知。
- 高效:避免了重复通知,减少了系统调用次数。
- 适合非阻塞 I/O:通常需要将文件描述符设置为非阻塞模式,避免因为没有新事件而陷入阻塞。
因为边缘触发一次性通知的特点,这倒逼着程序员一次性取走TCP缓冲区的全部数据,但是,如何保证能一次性全部处理完所有数据呢?这就要使用循环读取的方法,但是这时又会产生一个问题:
在阻塞模式下,I/O操作(如 read
或 write
)会一直阻塞,直到有数据可读或可以写入。这意味着在没有数据可读的情况下,read
调用会阻塞程序,直到有数据到达。一旦 I/O 阻塞,程序会因为等待 I/O 操作而停滞不前,这违背了边缘触发模式的设计理念,并会影响到整个事件驱动系统的性能。所以就需要我们将文件描述符设置为非阻塞状态。
边缘触发模式中的循环读取
在边缘触发模式下,epoll 只会在状态变化(如有新数据到达)时通知应用程序。这意味着如果程序没有一次性读取所有数据,后续不会再收到通知。因此,程序员需要使用循环来确保读取所有数据。
结合非阻塞模式的优势
在非阻塞模式下,结合边缘触发模式,程序员可以设计一个高效的事件驱动程序。使用非阻塞模式时,read
调用会立即返回,如果没有数据可读,程序不会阻塞,可以继续处理其他事件。