文章目录
1.初识epoll
epoll是Linux下多路复用I/O接口select/poll的增强版本,它能显著提高程序在大量并发连接时只有少量活跃的情况下的系统CPU利用率。
按照man手册的说法: 是为处理大批量句柄而作了改进的poll.
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.
1.1AI探索
epoll是Linux内核中一种高性能的I/O事件通知机制,最早在Linux 2.5.44内核中引入。它是针对大量并发连接和少量活跃连接场景下的优化,能够显著提高程序在这种情况下的系统CPU利用率。epoll相比于传统的select和poll系统调用,在性能上有显著的提升,特别适用于高并发网络编程场景。以下是对epoll的简要叙述:
一、epoll的特点
- 无文件描述符数量限制:
epoll采用了基于事件的就绪通知机制,没有预定义的文件描述符数量限制,可以支持更大规模的并发连接。其文件描述符上限通常是系统最大可以打开文件的数目,这个数目远大于select和poll的限制(如select通常限制为1024)。 - 高效的事件通知:
epoll使用了内核和用户空间共享的事件数据结构,将文件描述符的事件注册到内核空间。当事件就绪时,内核直接将就绪的事件通知给用户空间,避免了每次调用都需要遍历整个文件描述符数组的性能开销。 - 分离的就绪事件集合:
epoll将就绪的事件从内核空间复制到用户空间,形成一个分离的就绪事件集合。用户可以直接遍历这个集合来处理就绪的事件,而不需要遍历整个文件描述符数组。 - 支持边缘触发和水平触发:
epoll提供了两种模式来处理事件:边缘触发(Edge Triggered, ET)和水平触发(Level Triggered, LT)。ET模式只在状态发生变化时通知应用程序,而LT模式在事件就绪期间一直通知应用程序。 - 较低的内存拷贝开销:
epoll使用内存映射技术,避免了每次调用都需要将事件数据从内核复制到用户空间的开销,从而减少了系统调用的次数和内存拷贝的开销。 - 支持较高精度的超时控制:
epoll的超时参数以毫秒和纳秒为单位,提供了较高精度的超时控制。
二、epoll的实现机制
epoll在Linux内核中使用红黑树(Red-Black Tree)作为其主要的数据结构,用于维护注册的文件描述符集合。红黑树是一种自平衡的二叉搜索树,具有较快的插入、删除和搜索操作的时间复杂度(O(log n))。这使得epoll能够高效地检索和管理大量的文件描述符。
三、epoll的相关函数
epoll提供了几个关键的系统调用函数,包括:
- epoll_create:
创建一个epoll实例,并返回一个文件描述符,用于标识该epoll实例。 - epoll_ctl:
通过epoll_ctl函数可以添加、修改或删除epoll实例中注册的文件描述符及其事件。操作类型包括EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)和EPOLL_CTL_DEL(删除)。 - epoll_wait:
在一段超时时间内等待一组文件描述符上的事件。当检测到有事件发生时,将事件从内核事件表中复制到用户空间,并返回就绪事件的数量。
四、epoll的应用场景
epoll特别适用于需要处理大量并发连接且连接中大部分时间处于非活跃状态的网络应用,如Web服务器、数据库服务器等。通过使用epoll,这些应用可以显著提高性能和资源利用率。
综上所述,epoll是Linux内核中一种高效、可扩展的I/O事件处理机制,特别适用于高并发网络编程场景。它通过优化事件通知机制、减少内存拷贝开销和提供高效的数据结构支持,显著提高了程序在处理大量并发连接时的性能。
1.2拓展认识CMake
CMake是一个跨平台的自动化建构系统,它使用一个名为CMakeLists.txt的文件来描述项目构建的过程。以下是一个CMake使用的简单样例,该样例展示了如何构建一个包含单个源文件(main.cpp)的C++项目。
CMakeLists.txt 文件内容
#设置CMake的最低版本要求
cmake_minimum_required(VERSION 3.10)
#定义项目名称和设置项目支持的语言
project(HelloCMake LANGUAGES CXX)
#设置C++编译选项(可选)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall")
#定义源文件变量
set(SRC_FILES main.cpp)
# 生成可执行文件
add_executable(HelloCMake ${SRC_FILES})
源文件 main.cpp
#include <iostream>
int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}
使用CMake构建项目的步骤
准备项目文件:确保你有一个名为main.cpp的源文件和一个名为CMakeLists.txt的CMake配置文件,内容如上所示。
创建构建目录:在项目根目录下创建一个名为build(或任何你喜欢的名称)的目录。这个目录用于存放CMake生成的构建文件(如Makefile)和编译产物,以避免污染源代码目录。
mkdir build
cd build
运行CMake:在构建目录下,运行cmake命令并指定源代码目录(如果CMakeLists.txt就在当前目录,则可以省略此参数)。这将生成必要的构建文件。
cmake ..
注意:这里的..表示CMakeLists.txt文件位于上一级目录中。
编译项目:使用CMake生成的构建文件来编译项目。这通常是通过在构建目录下运行make(在Linux或macOS上)或相应的构建命令(如在Windows上使用Visual Studio的构建系统)来完成的。
make
这将编译源文件,并生成可执行文件(在这个例子中名为HelloCMake)。
运行可执行文件:在构建目录下,运行生成的可执行文件。
./HelloCMake
如果一切正常,你应该会看到控制台输出“Hello, CMake!”。
注意事项
CMake的版本号(在cmake_minimum_required中指定)应根据你的系统和需求来设置。选择一个适合你的开发环境的版本。
在CMakeLists.txt中,你可以设置各种编译选项、链接库等,以满足你的项目需求。
CMake非常灵活,支持多种编程语言和编译器。上述样例仅展示了C++项目的基本用法。
在实际项目中,你可能需要更复杂的CMakeLists.txt文件,以处理多个源文件、库依赖、测试等。
1.3epoll_create()
epoll_create() :创建一个 epoll 模型。epoll_create() 的参数 size 也已经废弃了,大于 0 就可以了。返回值是一个文件描述符,成功则返回非负文件描述符,失败返回 -1.
自从linux2.6.8之后,size参数是被忽略的.。用完之后, 必须调用close()关闭
功能:
epoll_create函数用于创建一个epoll实例,该实例用于多路复用I/O操作。这个实例在内核中会被映射为一颗红黑树和一个就绪链表,用于管理被监听的文件描述符及其事件。
参数:
size:是epoll实例的大小,即可以监听的文件描述符的数量的一个估计值。但在实际使用中,这个参数在很多系统上已经不再具有实际的含义,可以将其设置为一个大于0的任意值。
返回值:
成功时,返回一个非负整数,代表epoll实例的文件描述符。
失败时,返回-1,并设置errno表示错误原因。
示例:
#include <sys/epoll.h>
#include <stdio.h>
int main() {
int epoll_fd = epoll_create(10); // 10 是一个估计值,实际上已经不再具有实际含义
if (epoll_fd == -1) {
perror("epoll_create");
return -1;
}
// 使用 epoll_fd 进行多路复用的设置和操作
// ...
close(epoll_fd);
return 0;
}
1.4epoll_ctl()
The events member is a bit set composed using the following available event types:
EPOLLIN
The associated file is available for read(2) operations.
EPOLLOUT
The associated file is available for write(2) operations.
EPOLLRDHUP (since Linux 2.6.17)
Stream socket peer closed connection, or shut down writing half of connection. (This flag
is especially useful for writing simple code to detect peer shutdown when using Edge Trig‐
gered monitoring.)
EPOLLPRI
There is urgent data available for read(2) operations.
EPOLLERR
Error condition happened on the associated file descriptor. epoll_wait(2) will always wait
for this event; it is not necessary to set it in events.
EPOLLHUP
Hang up happened on the associated file descriptor. epoll_wait(2) will always wait for this
event; it is not necessary to set it in events.
EPOLLET
Sets the Edge Triggered behavior for the associated file descriptor. The default behavior
for epoll is Level Triggered. See epoll(7) for more detailed information about Edge and
Level Triggered event distribution architectures.
EPOLLONESHOT (since Linux 2.6.2)
Sets the one-shot behavior for the associated file descriptor. This means that after an
event is pulled out with epoll_wait(2) the associated file descriptor is internally disabled
and no other events will be reported by the epoll interface. The user must call epoll_ctl()
with EPOLL_CTL_MOD to rearm the file descriptor with a new event mask.
RETURN VALUE
When successful, epoll_ctl() returns zero. When an error occurs, epoll_ctl() returns -1 and errno
is set appropriately.
应用:向系统里新增一个文件描述符及其要关心的事件,想要修改一个特定的文件描述符关心的事件。
功能:
epoll_ctl函数用于向epoll实例注册、修改或删除要监视的文件描述符及其关联的事件。
参数:
epfd:epoll实例的文件描述符,通过epoll_create创建。
op:要进行的操作,可能的取值有EPOLL_CTL_ADD(注册)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除)。
fd:要进行操作的目标文件描述符。
event:是一个指向struct epoll_event结构体的指针,其中包含要注册或修改的事件信息。如果是删除操作(EPOLL_CTL_DEL),该参数可以为NULL。
返回值:
成功时,返回0。
失败时,返回-1,并设置errno表示错误原因。
示例:
c
#include <sys/epoll.h>
#include <stdio.h>
int main() {
// 创建 epoll 实例
int epoll_fd = epoll_create(10);
// 准备要监视的文件描述符及事件
struct epoll_event ev;
int target_fd = /* ... */; // 你的目标文件描述符
ev.events = EPOLLIN | EPOLLET; // 你要监视的事件类型,这里是读事件和边缘触发
ev.data.fd = target_fd;
// 向 epoll 实例注册文件描述符及事件
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, target_fd, &ev) == -1) {
perror("epoll_ctl");
close(epoll_fd);
return -1;
}
// ...(修改和删除操作类似)
close(epoll_fd);
return 0;
}
1.5epoll_wait()
epoll_wait() :获取已经就绪的文件描述符。第一个参数 epfd 就是 epoll_create() 的返回值;第二个和第三个参数是定义的一个用户级缓冲区,返回已经就绪的 fd 和 事件;最后一个参数的含义和 poll 的 timeout 一模一样,单位为毫秒。而返回值表示已经就绪的文件描述符的个数。
epoll_event 中的 events 表示哪些事件,它的类型是 uint32_t,也就是一个位图,和 poll 中的 events 一样,以位图的形式传递标记位事件;而第二个字段 data 的类型 epoll_data_t 是一个联合体,就是可以选择该联合体字段中的任意一个,通常用来保存的是用户级的数据。
其中 events 可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的;
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里。
带外数据
带外数据(Out-of-Band Data),有时也被称为加速数据(Expedited Data),是指在网络通信中,当连接双方中的一方发生重要事情,需要迅速通知对方时所使用的数据。这种数据在传输时具有比普通数据更高的优先级,能够在已经排队等待发送的普通数据之前发送。以下是关于带外数据的详细叙述:
一、定义与特点
定义:带外数据是映射到现有连接中的,用于快速传递重要信息的数据,它不与普通数据共享同一传输通道,但在逻辑上仍属于同一连接。
特点:
高优先级:带外数据在传输时具有比普通数据更高的优先级,能够确保及时到达接收方。
独立通道:虽然带外数据是映射到现有连接中的,但在逻辑上,它似乎是通过一个独立的通道进行传输的。
面向连接:带外数据主要适用于面向连接的套接字(如TCP套接字),在UDP等无连接协议中通常不支持带外数据。
二、应用场景
紧急通知:当通信一方有重要信息需要立即通知对方时,可以使用带外数据。例如,在实时通信系统中,当一方需要中断当前会话或发送紧急消息时。
交互式应用程序:在需要即时反馈的交互式应用程序中,带外数据可以用于传递控制信息或中断信号。
系统控制:在某些系统中,带外数据可用于传递系统控制命令或中断信号,如UNIX系统中的中断键(Delete或Control-c)和终端流控制符(Control-s和Control-q)。
三、实现方式
TCP紧急模式:在TCP协议中,虽然没有真正意义上的带外数据,但TCP提供了一种称为紧急模式(Urgent Mode)的机制来模拟带外数据的传输。通过在数据段中设置URG位,TCP可以标记某些数据为紧急数据,并通知接收方进行特殊处理。然而,需要注意的是,TCP紧急模式每次只能发送一个字节的紧急数据,这在一定程度上限制了其应用场景。
套接字选项:在某些系统中,可以通过设置套接字选项(如SO_OOBINLINE)来控制带外数据的处理方式。如果设置了该选项,则发送的带外数据将被存储在普通数据流中,与普通数据一起接收和处理。
四、注意事项
互操作性:由于不同系统对带外数据的实现可能存在差异,因此在编写跨平台应用程序时需要特别注意互操作性问题。
使用限制:在某些情况下,带外数据的使用可能受到系统或协议的限制。例如,TCP紧急模式每次只能发送一个字节的紧急数据,这可能会限制其应用场景。
性能影响:虽然带外数据具有高优先级的特性,但其使用可能会对系统性能产生一定影响。因此,在决定使用带外数据时,需要权衡其利弊并谨慎考虑。
综上所述,带外数据是一种在网络通信中用于快速传递重要信息的数据类型。它具有高优先级、独立通道和面向连接等特点,并广泛应用于紧急通知、交互式应用程序和系统控制等场景。然而,在使用带外数据时需要注意互操作性、使用限制和性能影响等问题。
功能:
epoll_wait函数用于等待一个或多个文件描述符上的事件。它是epoll接口的核心,允许单个线程同时监听多个文件描述符,从而高效地处理大量的I/O事件。
参数:
epfd:epoll实例的文件描述符。
events:是一个指向epoll_event结构的数组的指针,该数组用于存储被监听的文件描述符及其相关的事件信息。
maxevents:表示events数组中可以存储的最大事件数量。
timeout:等待事件的超时时间,单位是毫秒。如果设置为-1,表示无限期等待;如果设置为0,则表示立即返回,不会等待任何事件;如果设置为一个正数,则表示等待事件的最大时间。
返回值:
成功时,返回实际ready的文件描述符的数量。
失败时,返回-1,并设置errno表示错误原因。
1.6复习联合体
C语言中的联合体(Union)是一种特殊的数据类型,它允许在相同的内存位置存储不同的数据类型。但是,在任何给定的时间,联合体只能保存这些数据类型中的一个值。这意味着,联合体中的成员共享同一块内存空间,并且这块空间的大小足以容纳联合体中最大的成员。
联合体通常用于需要节省内存但又需要存储不同类型数据的场合。例如,你可能有一个需要同时处理整数和浮点数的场景,但你知道同一时间只会使用到其中的一种类型。这时,使用联合体就可以有效地利用内存。
联合体的定义
联合体使用union关键字来定义,其基本语法如下:
c
union 联合体名 {
成员类型1 成员名1;
成员类型2 成员名2;
...
成员类型N 成员名N;
};
联合体的特点
内存共享:联合体中的所有成员共享同一块内存空间。
大小固定:联合体的大小至少等于其最大成员的大小。
类型安全:使用联合体时需要小心,因为错误的类型访问可能会导致未定义的行为。
初始化:在C99及以后的版本中,联合体可以在定义时进行初始化,但只能初始化第一个成员。
#include <stdio.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
// 初始化(仅适用于第一个成员)
data.i = 10;
printf("Integer: %d\n", data.i);
// 修改为浮点数
data.f = 220.5;
printf("Float: %f\n", data.f);
// 尝试读取int值(可能不是预期的结果)
printf("Integer after float: %d\n", data.i);
// 字符串示例
strcpy(data.str, "Hello, Union!");
printf("String: %s\n", data.str);
return 0;
}
注意事项
访问联合体中的非当前赋值成员是未定义行为(Undefined Behavior, UB),这意味着程序可能表现异常。
使用联合体时要特别注意成员类型的大小和内存布局,以确保正确和有效地使用内存。
在一些应用场景中,结构体(Struct)可能更合适,因为它可以同时存储所有成员,并且每个成员都有自己的内存空间。
应用:判断大小端
#include <stdio.h>
// 定义一个联合体,用于检查字节序
union EndianTest {
unsigned int i;
unsigned char c[sizeof(unsigned int)];
};
// 函数用于判断字节序
void checkEndian() {
union EndianTest test;
test.i = 1; // 初始化整型为1
// 检查第一个字节(最低地址处的字节)
if (test.c[0] == 1) {
// 如果最低地址处的字节是1,则为小端模式
printf("Little-Endian\n");
} else {
// 否则为大端模式
printf("Big-Endian\n");
}
}
int main() {
checkEndian();
return 0;
}
2.epoll底层原理
epoll是Linux内核提供的一种I/O事件通知机制,它通过在用户态和内核态之间建立一个数据结构,使得用户态程序可以在内核态中注册感兴趣的事件,当这些事件发生时,内核会通知用户态程序。
一、创建与初始化:epoll_create
当用户态程序调用epoll_create函数时,内核会创建一个eventpoll对象(也称为epfd),这个对象用于管理所有的事件。
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
eventpoll对象包含了多个关键成员,如自旋锁(用于保护就绪列表)、互斥锁(保证在eventloop使用对应的文件描述符时,文件描述符不会被移除)、等待队列(与进程唤醒有关)、就绪描述符队列(rdllist,用于存储就绪的文件描述符)以及红黑树(rbr,用于组织当前epoll关注的文件描述符)。
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件.
这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度).
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法.
这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.
在epoll中,对于每一个事件,都会建立一个epitem结构体
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem
元素即可.
如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1).
二、添加监听的文件描述符:epoll_ctl
用户态程序通过调用epoll_ctl函数,将需要监控的文件描述符(如socket)添加到eventpoll对象中,设置需要监控的事件类型(如读就绪、写就绪等)。
如果添加的文件描述符在红黑树中已存在,则直接返回;如果不存在,则在红黑树中插入新的节点,并告知内核注册相应的回调函数。
三、等待并处理事件:epoll_wait
用户态程序调用epoll_wait函数等待事件发生。该函数会阻塞当前线程,直到有事件发生时才返回。
当有文件描述符上的事件发生时,内核会将这些文件描述符的引用添加到eventpoll对象的就绪列表(rdllist)中。
epoll_wait函数会检查就绪列表,如果列表不为空,则将事件信息拷贝到用户空间,并清空就绪列表。之后,用户态程序可以根据事件信息进行相应的处理。
四、epoll的优势
支持更多的事件类型:除了传统的文件描述符事件外,还支持网络事件、信号事件等。
支持更大的事件数量:epoll可以支持的事件数量比传统的I/O事件通知机制(如select和poll)更多。
更高的效率:
就绪列表:epoll使用就绪列表来存储就绪的文件描述符,避免了像select那样需要遍历所有文件描述符来检查哪些文件描述符就绪的低效操作。
事件驱动:epoll采用事件驱动的方式,只在事件发生时才通知用户态程序,避免了传统I/O事件通知机制中因轮询导致的效率低下问题。
分离“维护监视队列”和“进程阻塞”:epoll将这两个操作分开,先用epoll_ctl维护监视队列,再调用epoll_wait阻塞进程,以此来提高效率。
检测就绪的时间复杂度为 O(1),只需要看队列是否为空就可以了。获取就绪的时间复杂度为 O(n),需要将就绪队列中的节点一个一个拷贝到应用层。
fd 和 event 没有上限,该红黑树有多大由操作系统说了算
由于该红黑树是操作系统维护的,不需要在用户层由用户维护一个数组这样的数据结构来管理所有的文件描述符及其要关心的事件
epoll_wait() 的返回值 n,表示有 n 个 fd 就绪了,该接口还会将已经就绪的节点放入到它的输出型参数 events 中,就绪事件是连续的,有 n 个!上层用户处理已经就绪的事件,不再需要像以前一样检测有哪些 fd 是非法的,哪些是没有就绪的了;只需要根据返回值 n,遍历 events 即可!
接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
没有数量限制: 文件描述符数目无上限
epoll中使用了内存映射机制?
内存映射机制: 内核直接将就绪队列通过mmap的方式映射到用户态. 避免了拷贝内存这样的额外性能开销.
这种说法是不准确的. 我们定义的struct epoll_event是我们在用户空间中分配好的内存. 势必还是需要将内核的数据拷贝到这个用户空间的内存中的
五、epoll的实现细节
红黑树:用于存储所监控的文件描述符的节点数据,便于快速查找和更新。
就绪链表:用于存储就绪的文件描述符的节点数据,便于快速遍历和处理。
回调机制:当文件描述符上的事件发生时,内核会调用相应的回调函数来处理这些事件,并将文件描述符的引用添加到就绪列表中。
综上所述,epoll的底层原理涉及到了内核中eventpoll对象的创建与管理、文件描述符的添加与监控、事件的等待与处理等多个方面。通过这些机制,epoll实现了高效、可扩展的I/O事件通知功能。
select 和 poll 用数组来管理文件描述符和对应的事件,该数组这个数据结构由用户来维护!
总结:
操作系统在硬件层面,通过硬件中断的方式知道网卡上有数据了,通过网卡驱动上的方法将数据拷贝到网卡驱动上的数据链路层。
操作系统在内部维护一颗红黑树:红黑树中的节点包含的重要字段:int fd 和 uint32_t event,代表内核要关心的文件描述符和要关心的事件。
操作系统会为维护一个就绪队列:一旦红黑树中有特定的一个节点,比如某个节点上的文件描述符的某个事件就绪了,就可以把该节点添加到就绪队列中;该就绪队列中每个节点中的字段包含 int fd 和 uint32_t event,代表已经就绪的文件描述符和已经就绪的事件。
操作系统的底层网卡驱动允许操作系统注册一些回调机制:操作系统内部会提供一个回调函数:网卡通过硬件中断的方式将数据搬到了网卡驱动层。当网卡驱动层中的数据链路层有数据就绪了,主动调用该回调函数,回调函数作用:
- 向上交付
- 交付给 TCP 的接收队列
- 根据fd为键值查找红黑树,确认这个接收队列和哪一个文件描述符关联,再判断该 fd 是否关心了 EPOLLIN 或者 EPOLLOUT 读写事件,如果有,由于数据已经就绪,故构建就绪节点,插入到就绪队列中
- 操作系统把回调函数注册到底层,底层数据一旦就绪就会自动回调执行。对于用户来说,只需要在就绪队列中获取就绪节点即可!整套机制都是由操作系统完成的!
图片来源:epoll模型
eventpoll 为 epoll 对象;epitem 为红黑树的节点。
epoll_create() 创建 epoll 模型本质就是创建红黑树,创建就绪队列以及注册底层的回调机制。epoll 模型怎么让进程找到呢?将epoll 模型放入到 struct file 对象即可,把它也当作文件!因为在 Linux 中一切皆文件!struct file 中也有指针指向 epoll 模型!再把该 struct file 对象添到进程的文件描述符表中即可!
epoll_create() 实质上就是在操作系统中创建 struct file,其中的指针指向整个 epoll 对象,对应的文件描述符就能挂接到进程的文件描述符表中,把该文件描述符返回给用户,用户可以通过该文件描述符找到 struct file 并找到 epoll 模型了。epoll_ctl() 实质上的增加、修改、删除都是在对红黑树进行操作。其中 epoll_wait() 的第二个参数是输出型参数,它会将就绪队列中所有就绪的节点一个一个地放进 struct epoll_event 里。
epoll的使用过程就是三部曲:
调用epoll_create创建一个epoll句柄;
调用epoll_ctl, 将要监控的文件描述符进行注册;
调用epoll_wait, 等待文件描述符就绪
epoll的两种工作模式
(1)水平触发 Level Triggered 工作模式(LT 模式)
epoll 默认所处的工作模式就是 LT 模式。每次有新的连接到来时,如果不处理它,epoll 会每次都通知有连接到来了。一旦有新的连接到来,或者有新的数据到来,上层如果不取走,底层就会一直通知上层,让上层把数据尽快取走,这种模式就叫做 LT 模式。就像示波器中的高电平,一直有效。
当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 仍然会立刻返回并通知socket读事件就绪.直到缓冲区上所有的数据都被处理完, 支持阻塞读写和非阻塞读写
(2)边缘触发 Edge Triggered 工作模式(ET 模式)
ET 模式指的是,数据或者连接,从无到有,从有到多,变化的时候,才会通知一次。ET 模式这种特点倒逼程序员每次通知都必须/最好/不得不把本轮数据全部取走,怎么保证数据全部取走?循环读取,直到读取出错!使用 read() 或者 recv() 在缓冲区中读取数据时,当缓冲区的数据没有,由于它们的读取方式默认是阻塞的,所以此时就会阻塞,服务器就会被挂起!所以在 ET 模式下,所有的 fd 必须是要设置为非阻塞的!
当epoll检测到socket上事件就绪时, 必须立刻处理. 如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候, epoll_wait 不会再返回了.也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会. ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll. 只支持非阻塞的读写
(3)LT 和 ET 的对比
select 和 poll 是工作在 LT 模式下;epoll 既可以支持 LT,也可以支持 ET;LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.另一方面, ET 的代码复杂程度更高了
ET 的工作模式比 LT 的工作模式通知效率更高
通知一次就倒逼上层把全部数据读取走。 ET 模式的 IO 效率也更高:由于上层尽可能的取数据,TCP 就会向对方通告一个更大的窗口,从概率上让对方一次给自己发送更多的数据!
所谓的 LT 模式和 ET 模式,本质就是向就绪队列中放入多个或者一个就绪的事件
ET 模式就一定比 LT 模式的效率高吗?不一定!因为 LT 也可以将所有的 fd 设置为非阻塞,然后循环读取,也就是当通知一次的时候,就把数据全部取走了,就和 ET 一样了!所以谁的效率高不一定,要看具体的实现。epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反.对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.
例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll.
如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型
3.选学
- epoll详解
- Apache与Nginx网络模型
- 惊群效应:epoll的惊群效应及其解决办法可以从以下几个方面进行简要叙述:
一、epoll的惊群效应
定义:
在多线程或多进程环境下,当多个线程或进程同时监听同一个socket的epoll_wait时,一旦有新的连接或事件就绪,操作系统可能无法准确判断应该唤醒哪个线程或进程来处理该事件,因此会同时唤醒多个线程或进程。然而,实际上只有一个线程或进程能够成功处理该事件(如accept新的连接),其他被唤醒的线程或进程将失败,并且通常会收到EAGAIN错误码。这种现象被称为epoll的惊群效应。
危害:
资源浪费:不必要的线程或进程唤醒会导致CPU资源的浪费。
性能下降:多个线程或进程竞争同一资源,增加了系统的负载和响应时间。
二、解决办法
针对epoll的惊群效应,可以采取以下几种解决办法:
单线程/进程监听:
原理:只让一个线程或进程负责监听epoll_wait,当有新的连接或事件到来时,由该线程或进程处理,然后再将任务分配给其他线程或进程处理后续的数据读写请求。
优点:避免了不必要的线程或进程唤醒,减少了资源消耗和性能影响。
示例:在nginx中,采用master/worker模式,但同一时刻只有一个子进程在监听的socket上调用epoll_wait。
使用互斥锁:
原理:在多个线程或进程尝试进入epoll_wait之前,先通过互斥锁(如pthread_mutex_t)进行同步,确保同一时刻只有一个线程或进程能够进入epoll_wait。
实现:nginx中使用了类似的机制,通过accept_mutex来保证同一时刻只有一个子进程能够监听和处理新的连接。
利用内核特性:
Linux 2.6及以后版本:引入了WQ_FLAG_EXCLUSIVE标记位,用于解决accept惊群效应。这个标记位使得在内核层面只唤醒一个等待进程,从而避免了惊群效应。
注意:尽管内核已经提供了解决方案,但应用层仍然需要根据自己的需求和内核版本选择合适的解决方案。
优化事件处理逻辑:
避免不必要的epoll_wait调用:通过合理设计事件处理逻辑,减少不必要的epoll_wait调用,从而降低惊群效应的发生概率。
事件分发:在事件处理过程中,采用高效的事件分发机制,将事件快速分发给相应的处理线程或进程,以减少系统负载和响应时间。
综上所述,epoll的惊群效应是一个在多线程或多进程环境下使用epoll时需要注意的问题。通过采用单线程/进程监听、使用互斥锁、利用内核特性以及优化事件处理逻辑等方法,可以有效地避免或减轻惊群效应带来的资源消耗和性能影响。