【Linux】IO模型
文章目录
- 【Linux】IO模型
- 1、模型概念
- 1.1 IO概念
- 1.2 模型应用
- 2、模型种类
- 2.1 阻塞IO
- 2.2 非阻塞IO
- 2.3 信号驱动IO
- 2.4 IO多路转接
- 2.5 异步IO
- 3、概念对比
- 3.1 同步、异步通信
- 3.2 阻塞、非阻塞
- 4、fcntl
- 4.1 函数原型
- 4.2 指令参数
- 4.3 实现非阻塞IO
- 5、IO多路转接
- 5.1 select
- 5.1.1 作用
- 5.1.2 函数原型
- 5.1.3 fd_set结构
- 5.1.4 timeval结构
- 5.1.5 select执行过程
- 5.1.6 socket就绪条件
- 5.1.7 select缺点
- 5.2 poll
- 5.2.1 作用
- 5.2.2 函数原型
- 5.2.3 socket就绪条件
- 5.2.4 poll优点
- 5.2.5 poll缺点
- 5.3 epoll
- 5.3.1 作用
- 5.3.2 函数原型
- 5.3.3 工作原理
- 5.3.4 epoll优点
- 5.3.5 误区
- 5.3.6 工作方式
- 边缘触发(ET)
- 水平触发(LT)
- 对比
- 5.3.7 使用场景
- 5.3.8 惊群问题
- 5.4 三者对比
1、模型概念
1.1 IO概念
可以用"等待 + 拷贝"这种方式来简单地理解IO模型的基本工作原理。
-
等待: 在IO操作中,通常涉及等待外部事件的发生。例如,从硬盘读取文件时,程序需要等待硬盘返回数据。在网络通信中,程序需要等待数据从网络中到达。这种等待是阻塞的,意味着程序在等待IO操作完成时可能无法执行其他任务。
-
拷贝: 一旦外部事件发生(如数据已经准备好),数据会被拷贝到程序的内存中,以便进一步处理。例如,从文件读取数据后,数据将被拷贝到程序的内存缓冲区。在网络通信中,数据从网络中接收后也会被拷贝到程序的内存。
综合起来,"等待 + 拷贝"的概念很好地概括了IO模型的基本过程:程序等待外部事件完成,一旦完成,数据被拷贝到程序内部进行处理。不同的IO模型会以不同的方式处理这个基本过程,例如阻塞IO模型会在等待时阻塞程序,而异步IO模型会在等待时允许程序继续执行其他任务。但无论哪种模型,都包含了等待和数据拷贝这两个核心步骤。
1.2 模型应用
当处理IO模型时,可以遵循以下思路,从问题分析到实现和使用模型:
-
问题分析: 首先,要明确问题的性质和需求。考虑以下问题:你的应用是需要高并发处理还是低延迟响应?你需要处理文件IO还是网络通信?你是否需要同时处理多个IO操作?
-
选择合适的模型: 根据问题的性质和需求,选择适合的IO模型。考虑IO模型的特点和适用场景,权衡模型之间的优缺点。可以根据并发能力、延迟需求、开发复杂度等来选择合适的模型。
-
模型实现: 根据所选的IO模型,学习和了解相关的编程技术和API。根据模型的要求,编写相应的代码,处理数据的输入输出和事件。确保代码的结构和逻辑与所选模型相符。
-
检验和测试: 在实现模型后,进行充分的测试和调试。测试模型在不同场景下的性能、正确性和并发性。检查代码是否能够正确处理IO操作、处理异常等。
-
适当的使用: 一旦模型通过了测试并满足了需求,开始将其应用于实际项目中。确保在代码中正确使用模型的API和特性。注意模型可能需要特定的初始化、设置和使用方式。
-
错误处理和优化: 在使用模型的过程中,要及时处理可能的错误和异常。如果发现问题,可以根据情况进行优化和改进,以提高性能、减少延迟等。
-
不断改进: IO模型的选择和应用是一个持续优化的过程。根据实际应用和反馈,不断地改进模型的选择和实现,以适应不同的需求和变化。
总之,IO模型的思路涵盖了从问题分析到实际应用的全过程。通过仔细的分析、选择合适的模型、正确的实现、有效的测试和适当的使用,可以使程序能够更好地处理数据输入输出,并达到预期的性能和效果。
2、模型种类
2.1 阻塞IO
阻塞IO模型是一种最简单直接的IO处理模型。在阻塞IO模型中,当程序执行一个IO操作(如读取文件、接收网络数据等)时,程序会一直阻塞(即暂停执行)直到IO操作完成。这意味着程序在等待IO操作完成期间无法执行其他任务。
以下是阻塞IO模型的主要特点和步骤:
-
阻塞等待: 当程序执行一个IO操作时,例如从文件读取数据,程序会阻塞在这个IO操作上,等待数据准备好或者操作完成。这意味着程序在此期间无法执行其他任务,处于等待状态。
-
数据拷贝: 一旦IO操作完成,数据会从外部资源(如文件、网络)拷贝到程序的内部缓冲区。这样,程序就可以从内部缓冲区中读取数据并进行后续的处理。
-
继续执行: 一旦数据被成功拷贝到程序的内部缓冲区,程序会继续执行后续的代码逻辑,对拷贝来的数据进行处理。
阻塞IO模型的优点是它简单直接,易于理解和使用。然而,它也有一些局限性:
-
并发性差: 在阻塞IO模型下,每次进行IO操作时程序都会被阻塞,无法同时处理多个IO操作。这限制了程序的并发性能。
-
资源浪费: 如果一个IO操作花费了较长时间,其他任务可能会长时间等待,造成资源浪费。
-
响应时间较差: 由于阻塞IO会导致程序暂停执行,因此响应时间可能会受到影响,尤其是在需要处理多个IO操作的情况下。
阻塞IO模型在某些场景下仍然是有用的,特别是在简单的、对响应时间要求不高的应用中。然而,在需要高并发能力、低延迟响应或需要同时处理多个IO操作的情况下,可能需要考虑使用其他更复杂的IO模型,如非阻塞IO、多路复用IO或异步IO。
2.2 非阻塞IO
非阻塞IO(Non-blocking IO)是一种相对于阻塞IO的IO处理模型。在非阻塞IO模型中,程序在执行IO操作时不会等待操作完成,而是立即返回。如果操作没有立即完成,程序会继续执行其他任务,而不会被阻塞在IO操作上。
以下是非阻塞IO模型的主要特点和步骤:
-
尝试IO操作: 在进行IO操作时,程序会立即尝试执行操作,不会等待数据准备或操作完成。如果数据尚未准备好,或者操作无法立即完成,操作可能会返回一个指示。
-
轮询状态: 一旦尝试的IO操作返回,程序需要轮询或循环检查操作的状态,以确定操作是否已经完成。这可以通过反复尝试IO操作来实现。
-
数据拷贝和处理: 如果操作成功,数据会从外部资源拷贝到程序的内部缓冲区,然后程序可以对拷贝来的数据进行处理。
-
继续执行: 即使IO操作未完成,程序也会继续执行其他任务,从而实现非阻塞的效果。
非阻塞IO模型的优点是它允许程序在等待IO操作完成期间继续执行其他任务,从而提高了并发性能和响应性能。然而,非阻塞IO模型也有一些挑战和复杂性:
-
轮询开销: 非阻塞IO模型需要程序轮询或循环检查IO操作的状态,这可能会引入一些额外的开销。
-
代码复杂性: 由于需要手动管理IO操作的状态和轮询,代码可能会变得复杂和难以维护。
-
不适于高并发: 虽然非阻塞IO可以提高并发性能,但在高并发场景下可能会导致大量的轮询开销。
非阻塞IO常用于需要高并发性能、低延迟响应或需要同时处理多个IO操作的应用。要实现非阻塞IO,需要使用适当的API和技术,以便轮询IO操作的状态,并在操作就绪时执行相应的操作。
2.3 信号驱动IO
信号驱动IO(Signal-Driven IO)是一种IO模型,它通过使用操作系统信号(通常是信号处理器)来通知程序某个IO操作已经就绪,从而实现非阻塞的效果。信号驱动IO允许程序继续执行其他任务,而不需要轮询或循环检查IO操作的状态。
以下是信号驱动IO模型的主要特点和步骤:
-
注册信号处理器: 在进行IO操作之前,程序需要注册一个信号处理器(Signal Handler),以便在IO操作就绪时接收信号通知。
-
发起IO操作: 程序发起一个非阻塞IO操作,然后继续执行其他任务,而不等待IO操作完成。
-
等待信号: 当IO操作就绪时,操作系统会发送一个信号给程序,通知IO操作已经完成或数据已经准备好。
-
信号处理: 当程序收到信号时,信号处理器会被调用。在信号处理器中,程序可以执行相应的操作,如读取数据、处理数据等。
信号驱动IO模型的优点是它避免了轮询和循环检查IO操作的状态,从而减少了开销和复杂性。然而,信号驱动IO模型也有一些限制和注意事项:
-
不适用于所有IO操作: 不是所有类型的IO操作都适用于信号驱动IO模型。通常它适用于套接字(Socket)和终端设备(Terminal Device)等。
-
信号竞争和处理: 信号驱动IO模型可能涉及到信号竞争和信号处理器的编写,这可能会引入一些复杂性。
-
可移植性: 不同的操作系统可能在信号处理器的处理上有所不同,因此在跨平台应用时需要注意。
信号驱动IO通常用于需要非阻塞IO,同时又不想通过轮询方式来检查IO操作状态的情况。它可以提高并发性能和响应性能,但需要合理地管理信号处理器和处理信号的逻辑。
2.4 IO多路转接
IO多路复用(IO Multiplexing)是一种高效的IO处理模型,它允许程序同时监视多个IO操作的状态,从而在单个线程内处理多个IO操作。这种模型通常使用一个系统调用来等待多个IO事件的就绪,从而避免了阻塞和轮询。其中,IO多路转接是指通过选择(select)、轮询(poll)、事件驱动(epoll)等机制来实现多路复用。
以下是IO多路复用模型的主要特点和步骤:
-
注册文件描述符: 程序将要监视的多个文件描述符(如套接字)注册到IO多路复用机制中。
-
等待事件就绪: 通过系统调用(如
select
、poll
、epoll
)等等待多个IO事件的就绪状态。这个调用会阻塞,直到至少一个文件描述符的IO事件就绪。 -
处理就绪事件: 一旦有文件描述符的IO事件就绪,程序会得到通知,然后可以处理这些就绪的事件。这可以包括读取数据、写入数据、连接等。
-
继续等待: 在处理完就绪的事件后,程序可以再次等待更多的IO事件就绪,从而实现多个IO操作的处理。
IO多路复用的优点是它可以在一个线程内处理多个IO操作,减少了线程开销和资源占用。这可以提高并发性能和响应性能。然而,IO多路复用模型也有一些特定的注意事项:
-
适用性: IO多路复用适用于需要同时处理多个IO操作的场景,特别是并发连接较多的网络通信应用。
-
实现复杂性: 不同的操作系统可能提供不同的IO多路复用机制,实现和使用上可能有一定的复杂性。
-
事件处理: 在处理就绪的事件时,需要检查每个事件的类型,并根据需要执行相应的操作。
IO多路复用通常用于网络服务器、聊天应用等需要高并发处理的场景。在不同的操作系统和编程语言中,可能会有不同的API和机制来实现IO多路复用。
2.5 异步IO
异步IO(Asynchronous IO)是一种IO处理模型,它允许程序在发起IO操作后,不需要等待操作完成,而是可以继续执行其他任务。当IO操作完成后,程序会得到通知,然后可以处理已完成的IO操作。异步IO模型能够在不阻塞程序的情况下进行IO操作,从而提高并发性能和响应性能。
以下是异步IO模型的主要特点和步骤:
-
发起异步IO操作: 程序发起一个异步IO操作,然后继续执行其他任务,不需要等待操作完成。
-
注册回调函数: 在发起异步IO操作时,程序需要注册一个回调函数(Completion Handler)。当IO操作完成时,操作系统会调用这个回调函数,通知程序操作已经完成。
-
继续执行其他任务: 在等待IO操作完成期间,程序可以继续执行其他任务,从而实现非阻塞的效果。
-
处理已完成的IO操作: 当IO操作完成时,操作系统会调用注册的回调函数。在这个回调函数中,程序可以处理已完成的IO操作,读取数据、处理数据等。
异步IO的优点是它能够在IO操作完成前允许程序继续执行其他任务,从而提高了并发性能和响应性能。然而,异步IO模型也有一些注意事项:
-
编程复杂性: 异步IO模型通常需要使用回调函数和事件处理逻辑,可能会使代码变得复杂和难以理解。
-
错误处理: 在异步IO中,错误处理可能会更加复杂,因为需要处理操作失败或超时等情况。
-
事件竞争: 当多个异步IO操作同时完成时,可能会涉及到事件竞争,需要合理地处理这种情况。
异步IO常用于需要高并发性能、低延迟响应、同时处理多个IO操作的应用。要实现异步IO,需要了解所用编程语言或框架提供的异步API和回调机制,并在注册回调函数时处理好相应的逻辑。
3、概念对比
3.1 同步、异步通信
当涉及到同步和异步的概念,重点关注的是消息通信机制。在同步中,调用者发起一个操作后会主动等待操作完成的结果,而在异步中,调用者发起操作后可以继续执行其他任务,待操作完成时通过状态、通知或回调函数等方式得到结果。这两者都涉及到在任务之间的协调和信息交换。
与此同时,进程/线程同步是在多线程或多进程环境中出现的问题,旨在通过锁、信号量、条件变量等机制来管理线程或进程之间的顺序和资源共享,从而防止竞争条件和数据不一致等问题。
因此,同步和异步关注的是消息通信方式,而进程/线程同步是为了解决并发环境中的问题,确保正确的执行顺序和资源的合理共享。这些概念在并发编程和异步操作中都扮演着重要的角色。
3.2 阻塞、非阻塞
阻塞和非阻塞关注的是程序在等待调用结果时的状态,以及调用线程是否会被挂起。
阻塞调用是指在发起一个操作后,当前线程会被挂起(暂停执行),直到操作的结果返回。在阻塞调用中,调用线程会一直等待,直到它能够获取到操作的结果或者能够继续执行下一步操作。
非阻塞调用则是在发起操作后,当前线程不会被挂起。即使无法立即获得结果,调用线程仍然会继续执行,不会等待操作的结果。非阻塞调用在无法立即获取结果时会返回一些指示,以便调用线程可以继续进行其他任务,然后再检查操作结果的就绪状态。
总结来说,阻塞和非阻塞关注的是程序等待调用结果时的行为,阻塞调用会使调用线程等待结果并被挂起,而非阻塞调用则允许调用线程在等待操作结果的过程中继续执行其他任务。
4、fcntl
4.1 函数原型
fcntl
函数的函数原型在不同的编程语言和操作系统中会有所不同,这里以C语言为例,给出一个典型的函数原型:
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* argument */);
fd
:要操作的文件描述符。cmd
:表示要执行的操作,是一个整数常量,用于指定具体的功能。常见的命令参数包括F_GETFL
、F_SETFL
、F_GETLK
、F_SETLK
、F_GETOWN
、F_SETOWN
等等。...
:可选的附加参数,根据不同的命令参数而变化。
请注意,fcntl
的函数原型可能在不同的操作系统中有细微的变化,但通常基本结构是类似的。在使用时,根据具体的编程语言和操作系统,可能需要引入适当的头文件(如 <fcntl.h>
)、传递正确的参数和类型。
4.2 指令参数
fcntl
函数的第二个参数 cmd
是一个表示要执行的操作的整数常量。这个参数决定了具体的功能,下面列举了一些常见的 cmd
参数及其功能介绍:
-
F_DUPFD
: 复制文件描述符。这个命令用于创建一个新的文件描述符,它指向同一个文件。 -
F_GETFD
和F_SETFD
: 获取或设置文件描述符的关闭标志。用于获取或修改文件描述符的关闭标志,即在执行exec
系列函数时是否关闭文件描述符。 -
F_GETFL
和F_SETFL
: 获取或设置文件状态标志。用于获取或修改文件描述符的状态标志,如读写模式、非阻塞标志等。 -
F_GETLK
和F_SETLK
和F_SETLKW
: 获取或设置文件锁。用于获取和设置文件锁,以防止多进程间的文件互斥访问。F_SETLKW
会在获取锁时阻塞,直到锁可用。 -
F_GETOWN
和F_SETOWN
: 获取或设置文件或套接字的属主。通常用于进程间的信号通知,指定进程ID或进程组ID。 -
F_GETSIG
和F_SETSIG
: 获取或设置异步I/O信号。用于获取或设置异步I/O操作时的信号,指示操作完成。 -
F_GETPIPE_SZ
和F_SETPIPE_SZ
: 获取或设置管道缓冲区大小。用于获取或设置管道的缓冲区大小。 -
F_GETLEASE
和F_SETLEASE
: 获取或设置文件租约。用于获取或设置文件租约,控制对文件的访问。 -
F_NOTIFY
: 启用文件通知。用于启用对文件系统事件的通知。
这只是一小部分常见的 cmd
参数及其功能。在使用 fcntl
函数时,要根据具体的需求选择合适的 cmd
参数,并且在不同的操作系统下可能会有一些差异。
4.3 实现非阻塞IO
基于 fcntl
函数,可以设计一个函数来实现非阻塞IO。下面是一个基本的思路和解决方案:
思路:
- 打开文件描述符,确保它是非阻塞的。
- 发起非阻塞IO操作,如果操作未立即完成,程序会立即返回。
- 使用非阻塞IO操作的就绪状态来判断是否继续等待或处理其他任务。
解决方案:
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
// 函数返回值的状态
#define NONBLOCK_IO_SUCCESS 0
#define NONBLOCK_IO_ERROR -1
#define NONBLOCK_IO_TIMEOUT -2
// 设计一个函数实现非阻塞IO
int non_blocking_io(int fd, void *buffer, size_t size, int timeout_ms) {
// 设置文件描述符为非阻塞模式
int flags = fcntl(fd, F_GETFL);
if (flags == -1) {
return NONBLOCK_IO_ERROR;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
return NONBLOCK_IO_ERROR;
}
// 发起非阻塞IO操作
ssize_t bytes_read = read(fd, buffer, size);
if (bytes_read >= 0) {
// IO操作已经完成
return bytes_read;
} else {
// 检查是否是因为IO阻塞或其他错误
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 等待IO操作的就绪状态
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(fd, &read_fds);
struct timeval timeout;
timeout.tv_sec = timeout_ms / 1000;
timeout.tv_usec = (timeout_ms % 1000) * 1000;
int select_result = select(fd + 1, &read_fds, NULL, NULL, &timeout);
if (select_result == 0) {
// 超时,IO操作未完成
return NONBLOCK_IO_TIMEOUT;
} else if (select_result > 0) {
// IO操作已经完成
bytes_read = read(fd, buffer, size);
return bytes_read;
} else {
// 出现错误
return NONBLOCK_IO_ERROR;
}
} else {
// 其他IO错误
return NONBLOCK_IO_ERROR;
}
}
}
int main() {
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
char buffer[1024];
int result = non_blocking_io(fd, buffer, sizeof(buffer), 1000);
if (result == NONBLOCK_IO_SUCCESS) {
printf("IO operation completed.\n");
} else if (result == NONBLOCK_IO_TIMEOUT) {
printf("IO operation timeout.\n");
} else if (result == NONBLOCK_IO_ERROR) {
perror("non_blocking_io");
} else {
printf("Read %d bytes from the file.\n", result);
}
close(fd);
return 0;
}
在上面的例子中,non_blocking_io
函数通过设置文件描述符为非阻塞模式,并使用 select
函数来等待IO操作的就绪状态,实现了非阻塞IO。这是一个简化的实现,可以根据实际需求和操作系统的特点进行进一步的优化和调整。
5、IO多路转接
5.1 select
5.1.1 作用
在IO多路复用中,select
是一种用于监视多个文件描述符的就绪状态的系统调用。它允许程序同时等待多个文件描述符中的一个或多个变为可读、可写或异常就绪,从而避免了阻塞和轮询,提高了程序的效率和响应性。
select
的主要作用是:
-
等待就绪状态: 通过调用
select
,程序可以等待多个文件描述符中的一个或多个变为就绪状态。就绪状态可以是可读、可写或异常情况。 -
避免阻塞:
select
的一个关键优势是它可以在有一个或多个文件描述符就绪之前一直等待,而不会阻塞整个程序。这意味着程序可以继续执行其他任务,而不是一直等待IO操作完成。 -
节省资源: 与轮询不同,
select
允许程序等待多个IO事件的就绪状态,从而避免了无谓的资源消耗。 -
同时处理多路IO: 通过一次调用
select
,程序可以监视多个文件描述符,使得可以同时处理多个IO事件。
基本使用示例:
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(STDIN_FILENO, &read_fds); // 监视标准输入
struct timeval timeout;
timeout.tv_sec = 5; // 设置等待时间为5秒
timeout.tv_usec = 0;
int ready_fds = select(STDIN_FILENO + 1, &read_fds, NULL, NULL, &timeout);
if (ready_fds == -1) {
perror("select");
return 1;
} else if (ready_fds > 0) {
if (FD_ISSET(STDIN_FILENO, &read_fds)) {
printf("Data is available on standard input.\n");
}
} else {
printf("Timeout, no data available.\n");
}
return 0;
}
在上述示例中,select
函数被用于等待标准输入是否有数据可读。它监视一个文件描述符集合,等待指定时间的超时或者就绪状态。这允许程序在等待期间继续执行其他任务,而不会阻塞。
5.1.2 函数原型
在C语言中,select
函数的函数原型如下:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中的参数含义为:
nfds
:需要监视的最大文件描述符值加一(即文件描述符集合中的最大文件描述符加一)。readfds
:指向可读文件描述符集合的指针。writefds
:指向可写文件描述符集合的指针。exceptfds
:指向异常文件描述符集合的指针。timeout
:用于设置超时时间的结构体指针。
timeout
参数用于设置等待超时时间,它可以控制select
函数的行为。具体来说:
如果
timeout
参数为NULL
,则select
将一直被阻塞,直到所监视的文件描述符中的某一个有事件发生。这意味着程序将一直等待,直到有事件发生为止。如果
timeout
参数为0
,则select
将立即返回。在这种情况下,它只检测所监视的文件描述符的状态,然后立即返回结果,不会等待外部事件的发生。这可以用来进行非阻塞的轮询。如果
timeout
参数是一个特定的时间值,比如设置为一段秒数和微秒数,select
将在指定的时间段内等待事件的发生。如果在指定的时间内有事件发生,select
将立即返回。如果超过指定的时间仍然没有事件发生,select
将超时返回。通过合理设置
timeout
参数,你可以控制select
函数的阻塞等待时间,以及在无事件或超时时的处理方式。
select
函数的返回值表示就绪的文件描述符数量,或者在出错时返回 -1
。
需要注意的是,select
函数在某些操作系统中可能已经被更先进的IO多路复用机制取代,如 poll
或 epoll
。在使用时,需要根据操作系统和具体需求来选择合适的IO多路复用函数。
5.1.3 fd_set结构
fd_set
是一个在C语言中用于表示文件描述符集合的数据结构。它通常被用在像 select
、poll
这样的IO多路复用函数中,用于标识待监视的文件描述符的就绪状态。
fd_set
的内部实现通常是一个位图(bitmap),每个位对应一个文件描述符,用于表示该文件描述符是否在集合中。在大多数系统中,fd_set
被定义为一个数组,其中每个数组元素都代表一个文件描述符。
常见的 fd_set
操作函数包括:
-
FD_ZERO(fd_set *set)
: 清空指定的文件描述符集合,将所有位都设置为0,表示没有文件描述符在集合中。 -
FD_SET(int fd, fd_set *set)
: 将指定的文件描述符添加到集合中,即将相应的位设置为1,表示该文件描述符在集合中。 -
FD_CLR(int fd, fd_set *set)
: 从集合中移除指定的文件描述符,即将相应的位设置为0,表示该文件描述符不在集合中。 -
FD_ISSET(int fd, fd_set *set)
: 检查指定的文件描述符是否在集合中,即检查相应的位是否为1,如果是则返回非零值,表示文件描述符在集合中。
因为不同的操作系统可能有不同的实现,fd_set
在不同系统上的限制和行为可能会有所不同。在现代的编程中,一些操作系统已经引入了更先进的IO多路复用机制(如 epoll
、kqueue
等),而不再使用传统的 fd_set
结构。
下面代码片段涉及使用 select
函数来监视文件描述符的就绪状态,特别是用于检查某个文件描述符是否已准备好读取。
fd_set readset;
FD_SET(fd, &readset); // 将文件描述符 fd 添加到 readset 集合中
select(fd + 1, &readset, NULL, NULL, NULL); // 等待就绪状态
if (FD_ISSET(fd, &readset)) {
// 如果文件描述符 fd 在 readset 集合中的状态是就绪的
// 执行相关的操作,表示文件描述符可以进行读取
// ...
}
解释代码的步骤如下:
-
fd_set readset;
:定义一个文件描述符集合,这个集合会被用于传递给select
函数,表示希望监视哪些文件描述符的就绪状态。 -
FD_SET(fd, &readset);
:将希望监视的文件描述符fd
添加到readset
集合中。这表示关心文件描述符fd
是否已准备好进行读取操作。 -
select(fd + 1, &readset, NULL, NULL, NULL);
:调用select
函数来等待监视的文件描述符的就绪状态。fd + 1
是最大的文件描述符值加一,是select
函数所需的nfds
参数。&readset
表示希望监视哪些文件描述符的状态。 -
if (FD_ISSET(fd, &readset)) { ... }
:通过FD_ISSET
宏来检查特定的文件描述符fd
是否在readset
集合中的就绪状态。如果条件满足,表示文件描述符已准备好读取,可以在大括号中执行相应的操作。
这段代码使用了 select
函数来等待特定文件描述符是否已准备好读取,一旦就绪,就执行相关操作。这种机制可以避免阻塞并提高程序的效率。
5.1.4 timeval结构
timeval
是一个在C语言中用于表示时间间隔的数据结构。它通常被用在需要设置超时时间的系统调用中,比如 select
、poll
等。timeval
结构包含两个成员变量,分别表示秒数和微秒数,用于表示一段时间的时长。
timeval
结构的定义通常如下:
struct timeval {
long tv_sec; // 秒数
long tv_usec; // 微秒数
};
-
tv_sec
:表示秒数部分,是一个长整型(long
),用于表示从1970年1月1日午夜(UNIX纪元)起经过的秒数。 -
tv_usec
:表示微秒数部分,是一个长整型(long
),用于表示秒数之外的微秒部分。
在许多系统调用中,你可以使用 timeval
结构来设置超时时间,以控制调用的等待时间。例如,在使用 select
函数等待IO事件就绪时,可以通过设置 timeout
参数为一个 timeval
结构来控制超时时间。
示例用法:
#include <sys/time.h>
int main() {
struct timeval timeout;
timeout.tv_sec = 5; // 设置超时时间为5秒
timeout.tv_usec = 0;
// 在这里可以调用 select 函数并设置 timeout 参数
return 0;
}
在上述示例中,timeout
被设置为5秒,表示程序在调用某个函数时最多等待5秒,超过这个时间就会超时。注意,timeval
结构的使用可能因操作系统和函数调用的要求而有所不同。
5.1.5 select执行过程
理解 select
模型的关键在于理解 fd_set
的概念,这是一个数据结构,用于表示文件描述符的集合,而 select
函数可以监视这些文件描述符的就绪状态。
fd_set
是用于表示文件描述符集合的数据结构,在 select
函数中使用。为方便说明,我们假设 fd_set
的长度为1字节,每个bit对应一个文件描述符。
-
首先,执行
fd_set set; FD_ZERO(&set);
,将set
初始化为0000 0000
,表示没有任何文件描述符处于就绪状态。 -
若有一个文件描述符
fd
为5,执行FD_SET(fd, &set);
,则set
变为0001 0000
,第5个bit被设置为1,表示文件描述符5处于就绪状态。 -
若再加入文件描述符
fd
为2 和fd
为1,执行FD_SET(fd, &set);
,则set
变为0001 0011
,第2和第1个bit都被设置为1,表示文件描述符2和1都处于就绪状态。 -
执行
select(6, &set, 0, 0, 0)
,函数将阻塞等待,监视文件描述符的就绪状态。 -
如果文件描述符1和2上都发生可读事件,
select
函数返回,此时set
变为0000 0011
,只有第2和第1个bit为1,表示文件描述符1和2已经就绪。注意,之前处于就绪状态的文件描述符5的状态被清空,因为没有事件发生。
总之,通过操作 fd_set
,程序可以设置要监视的文件描述符的状态,而 select
函数则会等待这些文件描述符之一变为就绪。一旦有文件描述符就绪,select
返回并更新相应的 fd_set
,使得可以确定哪些文件描述符可以进行IO操作。
5.1.6 socket就绪条件
在网络编程中,“socket 就绪” 指的是套接字(socket)已经准备好进行某种类型的IO操作,比如读取、写入或者异常处理。在使用IO多路复用函数(如 select
、poll
、epoll
等)时,我们监视套接字的就绪状态,以便在合适的时候执行相应的IO操作。
具体来说,在网络编程中,套接字的就绪状态可以分为以下三种情况:
-
读就绪: 套接字读就绪意味着从套接字中可以读取数据。这通常发生在套接字接收缓冲区中有数据可供读取的情况下。
-
写就绪: 套接字写就绪意味着可以向套接字写入数据,而写操作不会被阻塞。这通常发生在套接字发送缓冲区有足够的空间可以容纳写入的数据时。
-
异常就绪: 套接字异常就绪表示发生了异常情况,如带外数据到达。这可能发生在套接字状态出现错误或非正常情况下。
在 select
函数中,可以使用以下 fd_set
宏来检查套接字的就绪状态:
FD_ISSET(fd, &readfds)
:检查套接字fd
是否在可读就绪状态,即是否有数据可读取。FD_ISSET(fd, &writefds)
:检查套接字fd
是否在可写就绪状态,即是否可以写入数据。FD_ISSET(fd, &exceptfds)
:检查套接字fd
是否在异常就绪状态,即是否出现了异常情况。
使用IO多路复用函数时,可以设置对应的 fd_set
集合,然后调用 select
函数来等待套接字的就绪状态,一旦就绪,就可以进行相应的读取、写入或异常处理操作。
5.1.7 select缺点
select
函数的一些缺点在某些情况下可能会影响性能和易用性:
-
手动设置fd集合: 在使用
select
函数时,需要手动设置需要监视的文件描述符集合,这可能会导致代码的可读性较差,尤其是当需要监视多个文件描述符时。 -
内核态拷贝: 每次调用
select
函数时,需要将文件描述符集合从用户态拷贝到内核态,这个开销在文件描述符数量较多时可能变得显著,影响性能。 -
内核遍历: 每次调用
select
函数都需要在内核中遍历传递进来的所有文件描述符,即使其中大部分没有就绪。这会导致在文件描述符数量较多时的性能问题。 -
支持的文件描述符数量限制: 某些系统对
select
函数支持的文件描述符数量有限制,这可能在需要监视大量文件描述符的情况下成为限制。
由于上述缺点,一些现代操作系统引入了更先进的IO多路复用机制,如 poll
、epoll
、kqueue
等,以解决 select
函数的一些性能和限制问题。这些新的机制提供更高效的事件通知和处理方式,避免了一些 select
函数的缺点。在选择合适的IO多路复用机制时,应该考虑应用需求和目标平台的特点。
5.2 poll
5.2.1 作用
poll
是一种IO多路复用机制,用于在一个系统调用中监视多个文件描述符的就绪状态,以便在文件描述符就绪时进行相应的操作。它是对传统的 select
函数的一种改进,解决了一些 select
函数的缺点。
poll
的作用主要有以下几点:
-
监视多个文件描述符:
poll
允许程序同时监视多个文件描述符的就绪状态,无需手动构建文件描述符集合。这简化了代码,提高了可读性。 -
避免手动拷贝和遍历: 与
select
不同,poll
并不需要手动拷贝文件描述符集合到内核态,也不需要在内核中遍历所有文件描述符。这减少了系统调用的开销。 -
没有文件描述符数量限制:
poll
没有像select
那样的文件描述符数量限制,可以支持更多的文件描述符。 -
更好的扩展性:
poll
提供更好的扩展性,它不会因为文件描述符数量增加而导致性能下降。每次调用poll
时,仅会遍历就绪的文件描述符,而不是所有传递进来的文件描述符。 -
更精细的事件类型:
poll
允许指定多种事件类型,如可读、可写和异常等,可以更精细地控制需要监视的事件。
基本使用示例:
#include <poll.h>
int main() {
struct pollfd fds[1];
fds[0].fd = STDIN_FILENO; // 文件描述符为标准输入
fds[0].events = POLLIN; // 监视可读事件
int timeout = 5000; // 超时时间为5秒
int ready_fds = poll(fds, 1, timeout);
if (ready_fds == -1) {
perror("poll");
return 1;
} else if (ready_fds > 0) {
if (fds[0].revents & POLLIN) {
printf("Data is available on standard input.\n");
}
} else {
printf("Timeout, no data available.\n");
}
return 0;
}
在上述示例中,poll
函数用于等待标准输入是否有数据可读。它监视一个文件描述符结构体数组,并在就绪时执行相应的操作。
5.2.2 函数原型
poll
函数是用于实现IO多路复用的系统调用,函数原型如下:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
fds
:一个指向struct pollfd
结构体数组的指针,每个结构体描述一个要监视的文件描述符及其所关注的事件。nfds
:要监视的文件描述符数量,即fds
数组的长度。timeout
:等待超时时间,以毫秒为单位。可以设置为以下几种值:-1
:永远等待,直到有就绪事件发生。0
:立即返回,检查并报告当前状态,不等待就绪事件。- 大于
0
:等待指定毫秒数的时间,然后返回。
poll
函数返回值表示就绪的文件描述符数量,或者在出错时返回 -1
。如果超时时间到达并且没有文件描述符就绪,返回值为 0
。
struct pollfd
结构体用于描述要监视的文件描述符及其关注的事件,定义如下:
struct pollfd {
int fd; // 要监视的文件描述符
short events; // 所关注的事件类型
short revents; // 实际发生的事件类型(由内核填充)
};
其中的成员变量含义如下:
fd
:要监视的文件描述符。events
:所关注的事件类型,可以是以下标志的组合:POLLIN
:可读事件。POLLOUT
:可写事件。POLLPRI
:高优先级数据可读事件。POLLERR
:错误事件。POLLHUP
:挂起事件。POLLNVAL
:无效请求事件。
revents
:实际发生的事件类型,由内核在poll
函数返回时填充。
使用 poll
函数,可以同时监视多个文件描述符,等待其中一个或多个就绪事件发生,并执行相应的操作。
5.2.3 socket就绪条件
同select
5.2.4 poll优点
-
支持大量文件描述符: 与
select
不同,poll
没有文件描述符数量的限制。这意味着你可以同时监视更多的文件描述符,不会受到系统限制。 -
不需要手动管理文件描述符集合: 与
select
相比,你无需手动设置文件描述符集合,而是通过struct pollfd
数组来描述要监视的文件描述符及其关注的事件,这使得代码更易读和管理。 -
避免内核拷贝和遍历:
poll
不需要像select
那样将文件描述符集合从用户态拷贝到内核态,也不需要在内核中遍历所有文件描述符。这降低了系统调用的开销,特别是在文件描述符数量较大时。 -
更好的性能: 相对于
select
,poll
有更好的性能表现,特别是在文件描述符数量较大时,因为它避免了一些select
的缺点。
5.2.5 poll缺点
-
效率问题: 尽管
poll
比select
有更好的性能,但在大规模应用中,仍可能存在效率问题。这是因为poll
在每次调用时需要遍历所有的文件描述符,包括没有就绪的。 -
无法处理大量连接: 对于大规模的连接数,
poll
仍然可能不够高效,因为它需要在每次调用时都遍历所有连接,无法将就绪的连接放在前面。 -
不适用于高并发: 虽然
poll
是相对较好的解决方案,但对于高并发的场景,仍可能不够理想。在这种情况下,更高效的机制如epoll
会更合适。
总体而言,poll
相对于传统的 select
有更好的性能和易用性,但在一些高并发和大规模连接的场景下,仍可能不够高效。在选择使用哪种IO多路复用机制时,你应该根据具体的应用需求和平台特点做出选择。
5.3 epoll
5.3.1 作用
epoll
是一种高效的IO多路复用机制,用于在一个系统调用中监视多个文件描述符的就绪状态,并在就绪时进行相应的操作。它是在Linux系统中引入的,旨在解决传统 select
和 poll
的一些性能限制和问题。
epoll
的作用主要体现在以下几个方面:
-
高效的事件通知: 相比于传统的
select
和poll
,epoll
提供更高效的事件通知机制。它使用了一个事件就绪表,只有就绪的文件描述符才会被返回,避免了遍历整个文件描述符集合的开销。 -
扩展性:
epoll
在大规模并发连接的场景下表现出色。它的性能不会随着文件描述符数量的增加而降低,因为它只返回就绪的文件描述符,不需要遍历所有文件描述符。 -
更多的事件类型:
epoll
支持更多的事件类型,如可读、可写、异常等。这使得你可以更精细地控制需要监视的事件,更灵活地处理IO操作。 -
更好的内存管理:
epoll
使用内核内存映射机制来管理事件就绪表,可以避免频繁的内存拷贝操作,从而提高性能。 -
Edge-Triggered模式:
epoll
提供了ET(Edge-Triggered)模式,这意味着一旦有事件发生,仅在状态发生变化时才会被通知。这与传统的Level-Triggered模式不同,可以减少重复通知。
总之,epoll
提供了一种高效、扩展性好的IO多路复用机制,适用于大规模并发的网络应用。它在性能和功能上都优于传统的 select
和 poll
,在高并发场景下发挥着重要作用。
5.3.2 函数原型
epoll
是基于Linux系统的IO多路复用机制,它的函数原型如下:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
这里是这些函数的简要说明:
-
epoll_create
: 用于创建一个 epoll 实例,返回一个文件描述符用于后续操作。size
参数用于指定内核用于管理的最大事件数。 -
epoll_ctl
: 用于添加、修改或删除要监视的文件描述符以及关联的事件。epfd
是epoll
实例的文件描述符,op
可以是EPOLL_CTL_ADD
、EPOLL_CTL_MOD
或EPOLL_CTL_DEL
,fd
是要监视的文件描述符,event
是一个struct epoll_event
结构体,用于描述关联的事件。 -
epoll_wait
: 用于等待事件的发生,当有就绪事件时,将就绪的事件从内核复制到用户空间,并将就绪事件的数量返回。epfd
是epoll
实例的文件描述符,events
是一个指向struct epoll_event
结构体数组的指针,用于存储就绪事件,maxevents
是events
数组的长度,timeout
是等待超时时间,以毫秒为单位。
struct epoll_event
结构体用于描述事件,定义如下:
struct epoll_event {
__uint32_t events; // 表示关联的事件类型
epoll_data_t data; // 用于用户定义的数据
};
events
字段表示关联的事件类型,可以是以下标志的组合:EPOLLIN
:可读事件。EPOLLOUT
:可写事件。EPOLLPRI
:高优先级数据可读事件。EPOLLERR
:错误事件。EPOLLHUP
:挂起事件。EPOLLRDHUP
:对端关闭连接事件(仅适用于Edge-Triggered模式)。EPOLLONESHOT
:设置为单次触发模式。
data
字段用于用户自定义数据,可以是一个union
,具体取决于应用需求。
通过使用这些 epoll
函数可以创建、管理和等待事件就绪,并在就绪时执行相应的操作,从而实现高效的IO多路复用。
5.3.3 工作原理
epoll
是一种高效的IO多路复用机制,它通过回调机制和底层的数据结构来管理文件描述符、事件以及就绪状态,从而实现在高并发环境中的高效IO操作。以下是 epoll
模型的关键要点:
-
回调机制:
epoll
模型允许为特定的文件描述符设置回调机制。当文件描述符的缓冲区有数据时,将会触发相应的回调。这消除了轮询检测底层是否有数据的需要,提高了效率。 -
红黑树和红黑节点: 创建
epoll
模型时,会创建一颗红黑树作为事件关注的数据结构,每个节点代表一个文件描述符和关注的事件。这解决了用户告知内核需要监视哪些文件描述符和事件的问题。 -
就绪队列和队列节点: 同时,还会创建一个就绪队列作为内核通知用户的数据结构,每个节点表示已经就绪的文件描述符和事件。这解决了内核告知用户文件描述符哪些事件已经就绪的问题。
-
函数作用:
epoll_create
:创建整个epoll
模型,包括回调机制、红黑树和就绪队列。epoll_ctl
:操作红黑树,向其中添加、修改或删除节点,相当于告诉内核需要监视哪些文件描述符和事件。epoll_wait
:检测就绪队列,如果队列非空,直接从中获取已就绪的文件描述符和事件。
-
自动管理: 一旦创建
epoll
模型,用户不需要手动处理哪些文件描述符上的哪些事件已经就绪,整个过程由操作系统自动处理。
总之,epoll
模型通过回调机制、红黑树和就绪队列,实现了高效的文件描述符管理和事件通知,适用于高并发网络环境下的IO多路复用。这种自动化的设计让开发者更专注于处理就绪事件,而不必关心具体的事件检测和管理。
5.3.4 epoll优点
epoll
的优点,特别是与 select
相比具有优势。
epoll
相对于传统的 select
具有诸多优势,可以提高性能和易用性,主要表现在以下方面:
-
接口使用方便: 虽然
epoll
拆分成了三个函数,但由于不需要在每次循环中设置关注的文件描述符,因此反而使用起来更方便高效。它将输入和输出参数分离,使代码更加清晰。 -
数据拷贝轻量:
epoll
只在合适的时候调用EPOLL_CTL_ADD
将文件描述符结构拷贝到内核中,这个操作并不频繁。相比之下,select
和poll
需要在每次循环中进行拷贝,造成额外的开销。 -
事件回调机制:
epoll
采用事件回调机制,避免了使用遍历,将就绪的文件描述符结构加入到就绪队列中。epoll_wait
返回时,直接访问就绪队列,就能知道哪些文件描述符就绪,这个操作的时间复杂度是 O(1)。即使文件描述符数目很多,效率也不会受到影响。 -
没有数量限制: 与
select
和poll
不同,epoll
对文件描述符数目没有上限限制。这使得它适用于大规模连接的高并发环境,而不会受到性能瓶颈。
综上所述,epoll
在接口使用方便、数据拷贝轻量、事件回调机制以及没有数量限制等方面的优点,使得它成为高效的IO多路复用机制,特别适用于需要处理大量并发连接的应用场景。
5.3.5 误区
有些博客说, epoll中使用了内存映射机制
内存映射机制: 内核直接将就绪队列通过mmap的方式映射到用户态. 避免了拷贝内存这样的额外性能开销
这个观点是不准确的。在 epoll
中,并没有直接将就绪队列通过内存映射(mmap
)方式映射到用户态。实际上,epoll
的机制并不涉及直接的内存映射操作。
在 epoll
中,用户空间的程序仍然需要通过系统调用(例如 epoll_wait
)来获取内核中就绪的事件信息。而在这个过程中,内核会将就绪的事件信息填充到用户空间中预先分配的 struct epoll_event
数组中。这个操作涉及到一次数据拷贝,将内核中的事件信息拷贝到用户空间的数据结构中。
所以,尽管 epoll
通过事件回调机制和其他优化措施来提高性能,但它并没有直接使用内存映射机制将内核的就绪队列映射到用户态。事件信息仍然需要通过数据拷贝从内核传递到用户空间,这是 epoll
本身的设计特点。
5.3.6 工作方式
边缘触发(ET)
边缘触发(Edge Triggered,ET)工作模式在 epoll
中的行为,特别是通过在将文件描述符添加到 epoll
描述符时使用 EPOLLET
标志。
边缘触发(ET)是一种 epoll
的工作模式,在这种模式下,当 epoll
检测到某个文件描述符上的事件就绪时,应用程序必须立刻处理该事件。
对于边缘触发模式,以下是一些关键特点:
-
立刻处理: 当
epoll_wait
返回并通知某个文件描述符上的事件就绪时,在边缘触发模式下,应用程序必须立刻对这个事件进行处理,否则不会再次通知相同的事件。 -
不重复通知: 在边缘触发模式下,
epoll_wait
仅在文件描述符上的事件状态发生变化时才会通知应用程序。也就是说,只有从无就绪状态变为就绪状态,或从就绪状态变为无就绪状态时,epoll_wait
才会返回并通知事件就绪。 -
性能优势: 由于边缘触发模式只在事件状态发生变化时通知,相比水平触发模式,它的通知次数较少,从而提高了性能。这在高并发环境中尤为重要,而且较少的通知次数也降低了应用程序的处理负担。
-
仅支持非阻塞读写: 边缘触发模式下只支持非阻塞读写(使用非阻塞IO函数)。由于要求立刻处理事件,阻塞式读写可能会导致事件得不到及时处理。
-
Nginx 默认使用: 由于边缘触发模式在高并发情况下性能更高,Nginx 等高性能服务器通常默认使用边缘触发模式。
总之,边缘触发模式要求立刻处理事件,只在事件状态发生变化时通知应用程序,适合高并发环境下的性能优化。与水平触发模式相比,它能降低通知次数,提高应用程序的效率。
边缘触发(ET)模式和非阻塞文件描述符是两个在处理事件驱动的IO操作时常常一起使用的概念。下面我会解释这两个概念并说明它们之间的关系。
边缘触发(ET)模式:
- 边缘触发是一种
epoll
工作模式,要求应用程序在事件就绪时立刻进行处理。它只在事件状态发生变化时通知应用程序,例如从未就绪变为就绪。这与水平触发(LT)模式不同,LT 在有数据时一直通知应用程序。- ET 模式适合处理高并发环境下的IO操作,因为它减少了不必要的事件通知次数,提高了效率。但也要求应用程序能够及时地处理事件。
非阻塞文件描述符:
- 非阻塞文件描述符是一种处理IO操作的方式。在非阻塞模式下,当执行IO操作(例如读取或写入数据)时,如果当前没有数据可读或写入会立即返回而不是阻塞等待。这使得应用程序能够处理多个文件描述符,而不会因为某一个阻塞导致整个程序停滞。
- 非阻塞模式常用于处理事件驱动的IO操作,特别是在异步的IO多路复用中。
关系:
边缘触发模式和非阻塞文件描述符可以结合使用,以提高事件驱动的IO效率。在边缘触发模式下,要求应用程序在事件就绪时立刻处理。如果应用程序使用非阻塞文件描述符,在事件就绪时可以立即进行非阻塞的IO操作,避免因等待而导致阻塞。这种组合适合高并发、高效率的IO处理,尤其在处理大量连接时表现出色。总之,边缘触发模式和非阻塞文件描述符在事件驱动的IO操作中经常一起使用,帮助提高程序的并发性和效率。应用程序需要及时处理事件和利用非阻塞IO操作,以达到最佳性能。
水平触发(LT)
水平触发(Level Triggered,LT)工作模式在 epoll
中的行为,特别是在默认情况下,epoll
就是采用了这种工作模式。
水平触发(LT)是 epoll
默认的工作模式。在这种模式下,当 epoll
检测到某个文件描述符上的事件就绪时,并不会立刻进行处理,而是会通知应用程序,告诉它文件描述符上的事件已经就绪。
对于水平触发模式,以下是一些关键特点:
-
处理方式: 当
epoll_wait
返回并通知某个文件描述符上的事件就绪时,应用程序可以不立刻对这个事件进行处理,也可以只处理其中的一部分数据。比如,你提到的读取了部分数据后,缓冲区中还有数据未处理。 -
重复通知: 即使应用程序并未处理完所有就绪事件,当下一次调用
epoll_wait
时,如果仍然有未处理完的事件,epoll_wait
仍然会立刻返回并通知相同的文件描述符上的事件就绪。这样会持续通知,直到所有就绪事件都被处理。 -
阻塞和非阻塞: 在水平触发模式下,既支持阻塞式读写(即调用阻塞的IO函数),也支持非阻塞式读写(使用非阻塞IO函数)。不过需要注意的是,无论使用阻塞还是非阻塞方式,水平触发的特性是在有数据到来时会重复通知。
总之,水平触发模式在 epoll
中表现为,文件描述符上的事件只要还有未处理的数据,就会重复通知应用程序,无论是通过阻塞还是非阻塞的方式。这个模式适合需要处理未读完或未写完的数据的情况。
对比
对比水平触发(LT)和边缘触发(ET)两种工作模式在 epoll
中的特点:
-
LT(水平触发):
- LT 是
epoll
的默认工作模式。在 LT 模式下,每当文件描述符上的事件状态就绪时,都会通知应用程序。 - 应用程序可以在事件就绪时不立刻处理,也可以只处理一部分数据。
- 重复通知:在 LT 模式下,即使应用程序没有完全处理就绪事件,每次调用
epoll_wait
都会通知相同的就绪事件。 - 性能:LT 模式下可以通过在每次就绪时立刻处理事件,避免重复通知,从而实现与 ET 模式类似的性能。
- 代码复杂度较低。
- LT 是
-
ET(边缘触发):
- ET 模式要求应用程序在事件就绪时立刻处理。在事件就绪状态发生变化时,才会通知应用程序。
- 应用程序需要确保每次就绪时都立刻处理,否则可能错过事件通知。
- 通知次数较少:ET 模式减少了事件通知次数,提高了性能,特别适合高并发环境。
- 代码复杂度相对较高,需要确保及时处理事件状态的变化。
总结来说,ET 模式要求应用程序立刻处理事件并且减少事件通知次数,适用于高性能、高并发的场景。而在 LT 模式下,通过立刻处理就绪事件,也能实现类似的性能效果。ET 模式的确能够提高性能,但可能增加了代码的复杂性,因为需要确保每次就绪时都立刻处理事件。选择适合的模式要根据应用的实际需求和复杂度来决定。
5.3.7 使用场景
epoll
在高性能IO处理方面的确有很多优点,但要根据具体的场景特点来选择是否使用它:
-
适用场景:
- 高并发连接:当需要处理大量并发连接,而只有一部分连接活跃或者需要处理的事件较少时,
epoll
是一个很好的选择。典型的入口服务器或者需要处理上万个客户端连接的场景,如互联网应用的入口服务器,都适合使用epoll
。 - 异步IO:
epoll
特别适合处理异步IO操作,能够充分利用异步非阻塞的特性,从而提高性能。
- 高并发连接:当需要处理大量并发连接,而只有一部分连接活跃或者需要处理的事件较少时,
-
不适用场景:
- 少量连接:如果应用程序内部通信或者服务器之间通信,只有少数几个连接,使用
epoll
可能会过于复杂,而且并不一定带来性能提升。 - 阻塞IO:如果IO操作主要是阻塞的,而不是事件驱动,使用
epoll
可能不是最佳选择。
- 少量连接:如果应用程序内部通信或者服务器之间通信,只有少数几个连接,使用
总之,epoll
是一种高性能的IO模型,但并不适用于所有场景。在选择使用 epoll
还是其他IO模型时,需要根据应用程序的实际需求、并发连接数量以及事件驱动特点来进行判断。合适的选择能够帮助充分发挥 epoll
的优势,提高程序的性能。
5.3.8 惊群问题
惊群问题是在使用多线程或多进程并发模型中,特别是在网络编程中,可能会遇到的一种性能问题。它会在一些情况下导致性能下降,而 epoll
是为了解决这种问题而设计的。
惊群问题(Thundering Herd Problem):
惊群问题指的是在多个进程或线程等待同一个事件(如文件描述符上的可读事件)时,当该事件就绪时,所有等待的进程或线程都会被唤醒,即使只有一个需要处理这个事件。这种现象可能导致性能下降,因为唤醒了不必要的线程或进程。
解决方案:
epoll
模型的一项重要设计目标就是解决惊群问题。在 epoll
中,同一时刻只会唤醒一个线程或进程来处理就绪事件。其他线程或进程不会被唤醒,从而避免了不必要的竞争和上下文切换。这个机制有效地减轻了惊群问题带来的性能下降。
具体来说,epoll
使用了以下机制来解决惊群问题:
epoll_wait
只会通知一个线程或进程就绪事件,其他等待的线程或进程不会被唤醒。- 使用边缘触发(ET)模式时,
epoll
只会在事件状态发生变化时通知等待的线程或进程,而不会反复通知。
总之,epoll
的设计有效地解决了惊群问题,避免了不必要的资源浪费和性能下降。这是在高并发环境中使用 epoll
的一个重要优势之一。
5.4 三者对比
下面是一个对 select
、poll
和 epoll
的优缺点的比较表格。请注意,每个技术都有其适用的场景和限制,这里提供的是一般情况下的总结。
特性 / 优缺点 | select | poll | epoll |
---|---|---|---|
数据结构 | 数组,最大数量有限 | 数组,最大数量有限 | 红黑树和队列,没有数量限制 |
操作系统支持 | 跨平台支持,但效率较低 | 跨平台支持,效率略高 | 仅在Linux上可用,效率最高 |
文件描述符限制 | 通常有限制,可能影响大规模连接 | 通常有限制,可能影响大规模连接 | 没有上限限制,适用于高并发环境 |
内存拷贝 | 每次循环都需要拷贝文件描述符集合 | 每次循环都需要拷贝文件描述符集合 | 仅在添加和删除文件描述符时拷贝 |
事件检测方式 | Level-Triggered | Level-Triggered | Level-Triggered 或 Edge-Triggered |
事件通知方式 | 循环遍历,效率较低 | 循环遍历,效率较低 | 事件回调机制,效率较高 |
可监视事件类型 | 有限,只支持读写和异常事件 | 有限,只支持读写和异常事件 | 多种事件类型,可细粒度控制 |
扩展性和效率 | 大规模连接下效率低,不推荐使用 | 效率相对较高,适中规模连接 | 大规模连接下效率高,适用于高并发环境 |
使用难度 | 较低,但效率和功能有限 | 较低,但效率和功能有限 | 较高,功能丰富,高效能 |
适用场景 | 小规模连接和跨平台需求 | 适中规模连接和跨平台需求 | 大规模连接和高并发环境,仅在Linux上可用 |