Linux高级IO模型
Linux 网络高级 IO
五种IO模型
本文将介绍Linux网络编程中的五种常见网络高级IO函数
- 阻塞IO
- 在内核将数据准备好之前, 系统调用会一直等待.
- 所有的套接字, 默认都是阻塞方式.
- 非阻塞IO
- 如果内核还未将数据准备好, 系统调用仍然会直接返回
- 返回EWOULDBLOCK错误码.
- 该IO模式常需要以循环的方式反复尝试读写文件描述符, 这个过程称为
轮询- 但是
轮询会带来大量的CPU资源浪费,所以该IO模式一般只在特定场景下应用- 信号驱动IO
- 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作
- IO多路转接
- 类似于阻塞IO,
- 最核心在于IO多路转接能够同时等待多个文件 描述符的就绪状态
- 异步IO
- 在数据拷贝完成时, 内核会通知应用程序
- 信号驱动则是告诉应用程序何时可以开始拷贝数据
总结:
所有的IO均可看作
等待+拷贝,其中等待的时间常常远高于拷贝,所以高效的IO模式都是尽量减少等待的时间(后面会具体介绍);
高级IO重要概念
同步通信 (synchronous communication) VS 异步通信 (asynchronous communication)
同步和异步关注的是消息通信机制:
- 同步
直接由调用者进行等待,在处理完成之前,调用者会一直等待该处理结果的返回,期间调用者不会进行其他的操作;
- 异步
调用者不会一直进行阻塞式等待,当处理完成后,被调用者会通过一系列的机制(回调函数、信号等)通知调用者,调用者收到对应的完成信号后执行后续操作;
然而,需要注意与多进程/多线程同步/互斥的区别,两者毫不相关:
进程/线程同步也是进程/线程之间直接的制约关系
是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、 传递信息所产生的制约关系. 尤其是在访问临界资源的时候
阻塞 VS 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
- 阻塞调用是指调用结果返回之前,对应的线程会被挂起,直到收到响应信号时才会返回;
- 非阻塞则不同于阻塞,即使未收到响应信号,线程也不会被挂起,而是继续执行接下来的操作;
其他高级IO
非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射 IO(mmap),这些统称为高级IO
本文重点讨论 I/O 多路转接;
非阻塞IO
fcntl
文件描述符控制函数,默认是阻塞 IO
函数原型
1 |
|
参数说明
cmd
F_DUPFD: 复制一个现有的描述符
F_GETFD / F_SETFD: 获得/设置文件描述符标记
F_GETFL / F_SETFL: 获得/设置文件状态标记
F_GETOWN / F_SETOWN: 获得/设置异步I/O所有权
F_GETLK / F_SETLK / F_SETLKW: 获得/设置记录锁
用法实例
——以设置文件描述符为非阻塞模式为例
1 | void SetNoBlock(int fd){ |
细节
这里获取的flag,请注意它是一个位图结构,关于位图结构,可以粗略的将它看作是一个_比特位容器_,每个位置上的0/1值都代表着不同的含义,当然位图的实现依据不同的功能有不同的实现方案,肯定不会只是一个简单的int类型可表示的,这里只是用作理解;
以轮询方式读取标准输入
在我们完成了上述的非阻塞模式设置函数后,我们就可以基于此实现以轮询方式读取标准输入的功能:
1 |
|
I/O多路转接
什么是多路转接:通俗的理解就是多个信号或数据流共享一条通信管道,通过各种机制(如信号)实现数据的有序传递,下面分别介绍几种常见的通信机制:
Select
初识select
系统提供select函数来实现多路复用输入/输出模型
select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
select 函数原型
1 |
|
参数解释
- nfds:最大的可用文件描述符 + 1;
- readfds:监听读事件的文件描述符的集合,这里的 fd_set 即表示一个文件描述符集合;
- writefds:同上,表示被监听的写事件的文件描述符集合;
- exceptfds:同上,表示被监听的异常事件的文件描述符集合;
- timeout:用来设置 select 的超时时间(用来区分非阻塞和阻塞模式)
timeout 取值
- NULL:则表示 select 没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件; ——阻塞等待
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生; ——非阻塞等待
- 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回; ——超时时间内阻塞,超时时间外非阻塞
fd_set
位图结构,对应的位标识所监视的文件描述符;
操作函数(位图结构不是简单类型,不可以通过位运算进行直接操作)
- void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
- int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd的位是否为真
- void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
- void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
返回值
- 执行成功则返回文件描述词状态已改变的个数
- 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
- 当有错误发生时则返回-1,错误原因存于errno,此时参数 readfds, writefds, exceptfds 和 timeout 的值变成不可预测;
常见错误值
- EBADF 文件描述词为无效的或该文件已关闭
- EINTR 此调用被信号所中断
- EINVAL 参数 n 为负值
- ENOMEM 核心内存不足
select 函数执行过程
理解select函数执行过程,需要与其操作函数相结合,这里以一个字节(8位)位图为例:
- 首先重置位图结构——
FD_ZERO,位图变为 (0000 0000);- 再设置需要监听的文件描述符,这里以 fd = 4为例——
FD_SET(4, &fd_set);(0001 0000)- 当监听到对应的文件描述符上由事件就绪时(事件类型由对应的文件描述符集合确定,这里是阻塞等待),fd_set被重新设置,对应位上的值被置为1,而监听的文件描述符若未就绪,则会被置为0;(0001 0000)
- 接收端读取select返回的信号,检索fd_set,执行相应操作;
select 就绪条件
读就绪
套接字 (Sockets)
监听套接字 (Listening Socket, e.g., created by socket, bind, listen) 如果有新的连接请求已完成(pending connection),accept() 将不会阻塞。
已连接的流套接字 (Connected Stream Socket, e.g., TCP)
套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记(SO_RCVLOWAT,默认为1)。这意味着调用 read() 或 recv() 将不会阻塞,并且会返回大于 0 的值(读取到的数据)。
低水平标记
- 接收低水位标记 (Receive Low Water Mark, SO_RCVLOWAT)
- 定义: 当套接字接收缓冲区中累积的数据字节数达到或超过这个标记值时,select/poll/epoll 才会通知应用程序该套接字“可读”。
- 默认值: 通常为1字节。
- 发送低水位标记 (Send Low Water Mark, SO_SNDLOWAT)
- 定义: 当套接字发送缓冲区中的可用空间字节数达到或超过这个标记值时,select/poll/epoll 才会通知应用程序该套接字“可写”。
- 默认值: 通常也比较小,例如允许写入至少一个字节。
为什么需要低水位标记?它的作用是什么?
- 减少不必要的唤醒和系统调用 (针对SO_RCVLOWAT):
- 场景: 想象一个应用,如果每当接收缓冲区有一个字节到达时就被唤醒,它就需要执行一次 read() 系统调用。如果数据是零散到达的(比如,对端一次只发送几个字节),那么应用程序会被频繁唤醒,每次唤醒只处理少量数据。这会导致大量的上下文切换和系统调用开销,效率低下。
- 作用: 通过设置一个较高的 SO_RCVLOWAT(例如,设置为1024字节),应用程序只有在接收缓冲区中累积了足够多的数据(至少1024字节)时才会被通知。这样,一次 read() 调用可以读取更多的数据,减少了唤醒次数和系统调用次数,提高了整体吞吐量和效率。
- 权衡: 设置过高的 SO_RCVLOWAT 可能会增加延迟,因为应用程序需要等待更多数据到达才会被唤醒。
- 控制写入的粒度 (针对SO_SNDLOWAT):
- 场景: 类似地,如果发送缓冲区只有少量空间可用时就通知应用程序可写,应用程序可能会尝试写入少量数据,导致频繁的 write() 调用和可能的网络小包问题(Nagle算法可能会缓解,但不是所有情况)。
- 作用: 通过设置一个合适的 SO_SNDLOWAT,可以确保在发送缓冲区有足够空间容纳一个有意义的数据块时才通知应用程序可写。这有助于应用程序进行更有效率的批量写入。
- 权衡: 对于某些需要低延迟的应用,即使只有少量空间也希望尽快发送数据,此时较小的 SO_SNDLOWAT(甚至默认值)可能更合适。
- 避免“忙等”或过于频繁的轮询:
- 如果没有低水位标记(或者说标记总是1),在数据流速较慢或不稳定的情况下,select/poll/epoll 可能会在只有极少量数据可操作时就返回。如果应用程序逻辑是“只要可读/可写就一直读/写”,可能会导致非常频繁地进入和退出内核态,即使每次操作的数据量很小。
- 低水位标记提供了一种机制,让内核在条件“更有意义”时才通知用户。
- 配合应用层协议:
- 某些应用层协议有最小消息大小或期望一次处理的数据块大小。通过设置合适的低水位标记,可以使得内核通知的时机与应用层的数据处理逻辑更匹配。例如,如果应用总是期望处理至少一个完整的协议单元,可以将 SO_RCVLOWAT 设置为该协议单元的典型大小。
连接的读取半部已经关闭(例如,对端发送了 FIN)。此时调用 read() 或 recv() 将不会阻塞,而是返回 0(表示 EOF)。
套接字上发生了错误(error pending)。此时调用 read() 或 recv() 将不会阻塞,而是返回 -1 并设置 errno 为相应的错误码(例如 ECONNRESET)。这些错误也可以通过 getsockopt(SO_ERROR) 来获取。
数据报套接字 (Datagram Socket, e.g., UDP) 有一个待处理的数据报可供读取。recvfrom() 将不会阻塞。
管道 (Pipes) 或 FIFO
- 管道中有数据可读。read() 将不会阻塞。
- 管道的写端已经关闭。read() 将不会阻塞,而是返回 0 (EOF)。
终端设备 (Terminals) 有未读的数据。
常规文件 (Regular Files) 通常总是被认为是可读的(除非指针在文件末尾)。但 select 主要用于网络和管道这类可能阻塞的 I/O。
写就绪
一个文件描述符被认为是可写的,如果以下任一条件成立:
- 套接字 (Sockets):
- 已连接的流套接字 (Connected Stream Socket, e.g., TCP):
- 套接字发送缓冲区中的可用空间大于等于套接字发送缓冲区低水位标记(SO_SNDLOWAT,默认为1),并且套接字已连接(或者不需要连接,如 UDP)。这意味着调用 write() 或 send() 通常不会阻塞(或者阻塞时间会很短,不会超过发送超时)。
- 连接的写入半部已经关闭。此时调用 write() 或 send() 通常会产生 SIGPIPE 信号,或者返回 -1 并设置 errno 为 EPIPE。注意:即使写入会失败,select 也可能报告其可写,所以需要处理写入错误。
- 一个非阻塞的 connect() 调用已经成功完成。
- 套接字上发生了错误(error pending)。此时调用 write() 或 send() 将不会阻塞,而是返回 -1 并设置 errno。这些错误也可以通过
getsockopt(SO_ERROR)来获取。
- 数据报套接字 (Datagram Socket, e.g., UDP): 通常总是可写的,因为 UDP 是无连接的,发送操作通常不会阻塞,除非内核缓冲区暂时满了。
- 已连接的流套接字 (Connected Stream Socket, e.g., TCP):
- 管道 (Pipes) 或 FIFO:
- 管道中有足够的空间可供写入数据(至少一个字节)。write() 将不会阻塞。
- 管道的读端已经关闭。write() 将会产生 SIGPIPE 信号,或者返回 -1 并设置 errno 为 EPIPE。
- 终端设备 (Terminals): 可以写入。
- 常规文件 (Regular Files): 通常总是被认为是可写的(除非磁盘空间已满或超出配额)。
异常就绪
一个文件描述符被认为有异常条件,如果:
- 套接字 (Sockets):
- 接收到带外数据 (Out-Of-Band, OOB data),仅适用于支持 OOB 数据的协议(如 TCP)。
- 在某些情况下,当套接字上存在待处理的错误时,也可能在 exceptfds 中报告。但更常见的是通过可读或可写条件来发现错误(即读写操作返回-1)。
- 伪终端 (Pseudo-terminals): 在包模式 (packet mode) 下,当主控端检测到从属端的状态改变时。
select 的特点
可监控的文件描述符取决于
sizeof(fd_set)的大小,以我自己的系统为例,sizeof(fd_set)大小为128 bytes,而每一位都可以标识一个文件描述符,所以可监控的文件描述符为128 * 8 = 1024;fd_set 的大小可以调整,具体方法可能涉及重新编译内核;
将 fd 加入监控集的同时,还要再使用一个数据结构 array 保存到 select 监控集合中的 fd
- 一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断;
- 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得 fd 逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数;
select 的缺点
- 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便;
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
用户无法对内核数据进行直接操作,所有的操作都是操作系统通过对应的文件描述符对对应的资源进行操作;
- 每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大;
- select支持的文件描述符数量太小;
select 的使用实例
1 |
|
该程序只检测标准输入(对应的文件描述符为0),当一直不输入时,就会产生超时信息;
poll
鉴于select带来的较大的拷贝开销和遍历成本,又提出了一种新的多路转接方式——poll;
函数原型
1 |
|
参数说明
fds:指向
pollfd结构体的指针1
2
3
4
5
6
7
8
9
10
11
12
13
14// pollfd结构
struct pollfd {
int fd; // 当前结构体所监视的文件描述符
short events; // 当前文件描述符锁关心的时间,具体类型有:
/*
POLLIN:表示文件描述符可以进行读取操作(即有数据可读)。
POLLOUT:表示文件描述符可以进行写入操作(即可以写数据)。
POLLERR:表示文件描述符发生错误。
POLLHUP:表示文件描述符被挂起,通常是连接关闭。
POLLNVAL:表示文件描述符无效。
*/
short revents; // 返回的事件类型,poll 返回时会修改这个字段,告知哪些事件已经发生
// 返回值是 events 字段中感兴趣的事件,或者是一些错误事件
};nfds:表示当前所监视的文件描述符个数,即结构体指针指向的结构体个数
timout:这是
poll等待事件发生的最大时间,单位是毫秒,timeout的值可以是以下几种:- 大于 0:表示等待事件发生的最长时间(毫秒)。
poll会在超时之前返回,或者在事件发生时返回。 - 0:表示非阻塞模式,
poll不会阻塞,立即返回。如果没有事件发生,则revents字段会被设置为 0。 - -1:表示无限期等待,
poll将会一直阻塞直到某个事件发生。
- 大于 0:表示等待事件发生的最长时间(毫秒)。
返回值
- 返回值小于0, 表示出错;
- 返回值等于0, 表示poll函数等待超时;
- 返回值大于0, 表示poll由于监听的文件描述符就绪而返回
socket就绪条件
与select相同;
poll 的优点
较select来说,省略了三套位图结构,而以一个结构体指针类型代替,优化了数据存储的结构;
- 首先针对结构体提供了同一的操作接口,而不是
select在循环中进行赋值操作;- 与
select的位图数组(大小由fd_set限制,而该限制是硬限制,可以通过修改系统设置来更改)不同,poll依赖于结构体数组实现,所以理论上poll没有最大数量的限制;
poll 的缺点
- 尽管poll通过结构体指针实现了操作的统一,但是之后的查询就绪状态依然需要执行遍历操作,尤其是当监视的文件描述符数量较大时,会带来较大的性能消耗;
- 内部使用数组维护,动态扩展和删除性能较差,系统需要重新构建一个
poll_fd结构体数组,由此会带来较大的拷贝资源消耗;
poll 的使用实例(检测标准输入输出——ReadEvent && ListenEvent)
1 |
|
epoll
——Linux 2.6 版本下公认的性能最好的多路 I/O 就绪通知方法
epoll 相关系统调用
epoll_create
创建一个epoll句柄;
函数原型
1 |
|
参数说明
- size:
Linux 2.6.8 之前,
size参数用于指定内核为 epoll 实例分配的事件队列的大小。具体来说,它表示内核分配的事件数组的初始大小,即内核为该 epoll 实例保留的空间大小(以事件数量为单位)。如果事件的数量超过这个初始大小,内核会动态地扩展空间。Linux 2.6.8 之后,
size参数的作用被弃用了;而内核根据实际需求来分配资源;
返回值
成功:返回一个非负整数,表示创建的 epoll 实例的文件描述符。
失败:如果调用失败,返回
-1,并且设置errno以指示错误原因。
epoll_ctl
epoll的事件注册函数;
函数原型
1 |
|
参数说明
epfd:epoll_create 的返回值,即 epoll 的句柄;
什么是句柄?
句柄是对资源的抽象引用,用来间接操作资源而不暴露资源的内部细节。
它通常由操作系统或库分配,并通过特定的 API 来进行资源的管理和访问。
句柄的常见应用包括文件、窗口、数据库连接和图形对象等。
句柄和指针的区别在于,指针直接访问内存,而句柄是资源的抽象标识符,底层实现和资源管理由操作系统或库负责。
op:表示具体的操作,具体分为一下三个宏:
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd;
fd:表示需要监听的文件描述符;
event:表示内核需要监听的具体事件;
struct epoll_event 参数说明
其具体的结构如下:
1 | typedef union epoll_data |
events可以是下列宏的集合(因为是位图结构,所以支持位运算)
EPOLLIN:表示对应的文件描述符可读(包括对端SOCKET正常关闭)
EPOLLOUT:表示对应的文件描述符可写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR:表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂断
EPOLLET :将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的;
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里;
边缘触发 && 水平触发
边缘触发
解释
边缘触发是指当事件从未触发状态变为触发状态时,操作系统会通知应用程序一次。如果事件从未触发状态变为触发状态后,直到事件被清除(处理完成)之前,操作系统不会再通知应用程序。
也就是说,只有事件的“变化”才会触发通知。
特点
- 一次性通知:只有当事件的状态从非触发变为触发时,才会发出通知,之后事件不会再重复通知,除非应用程序明确地清除事件的状态(比如读取数据)。
- 需要不断轮询:如果应用程序没有及时处理事件(例如,未读取网络数据),则需要不断地进行轮询或等待下一次事件通知。
应用场景
边缘触发适用于对事件的处理非常迅速、并且应用程序能够及时清除事件状态的场景。
水平触发
解释
水平触发是指,当事件处于触发状态时,操作系统会持续地通知应用程序,直到事件被处理完成并恢复到非触发状态。与边缘触发不同,水平触发会持续发出通知,直到应用程序处理了事件。
特点
- 持续通知:只要事件的状态仍然是触发状态,操作系统就会持续通知应用程序。应用程序必须处理事件(例如读取数据)才能清除触发状态。
- 不需要不断轮询:应用程序只需等待事件并处理它,而不需要担心遗漏重复的事件通知。
应用场景
水平触发适用于事件状态可能持续存在的场景,且应用程序需要持续收到通知直到事件被处理的情况。
epoll_wait
收集在epoll监控的事件中已经发送的事件;
函数原型
1 |
|
参数说明
epfd:
epoll的文件描述符,表示一个epoll实例,内部维护了多个文件描述符遗迹这些描述符的相关事件;events:
一个指向
epoll_event结构体数组的指针,用于存放epoll_wait返回的就绪事件;在调用epoll_wait时,系统会将就绪的事件写入这个数组。应用程序需要遍历这个数组,并处理每个就绪事件;struct epoll_event的定义1
2
3
4struct epoll_event {
uint32_t events; // 事件类型(例如,EPOLLIN, EPOLLOUT等)
epoll_data_t data; // 用户数据(通常用于存储文件描述符的相关信息)
};events:表示文件描述符的状态,epoll_wait返回的事件类型,可能的值包括:EPOLLIN:表示文件描述符可读。EPOLLOUT:表示文件描述符可写。EPOLLERR:表示文件描述符发生错误。EPOLLHUP:表示文件描述符挂起(例如,连接被断开)。- 其他事件,如
EPOLLRDHUP(TCP连接关闭通知)等。
data:用户定义的数据,通常用于存储与文件描述符相关的信息。通过它可以区分不同的文件描述符或关联的业务逻辑。例如,存储一个指向自定义结构的指针,或者直接存储文件描述符本身。
maxevents
这是
events数组的大小,表示最多可以返回多少个就绪事件。epoll_wait会返回最多maxevents个事件,这个数量不超过数组的大小;timeout
指定
epoll_wait等待事件的最大时间,单位为毫秒。该参数控制epoll_wait的等待行为;timeout = -1:表示无限等待,直到有一个或多个事件就绪为止。epoll_wait会阻塞直到有文件描述符发生变化(例如,变为可读、可写,或者发生错误等)。timeout = 0:表示非阻塞模式,epoll_wait会立即返回,不会等待。它会检查所有注册的文件描述符的状态,如果没有就绪事件,会返回0,表示当前没有文件描述符就绪。timeout > 0:表示最多等待timeout毫秒,如果在指定时间内有就绪事件,epoll_wait会返回;如果没有就绪事件,则在超时后返回0。
常见应用场景:
timeout = -1:常用于需要等待直到事件发生的场景,如网络服务器等待连接或数据到来。timeout = 0:常用于非阻塞模式,通常在多线程或多任务的环境中,用来检查文件描述符的状态而不阻塞。timeout > 0:适用于需要定时轮询事件的场景,例如,应用程序需要定期检查文件描述符的状态,并在超时后执行一些其他任务。
返回值
成功:返回就绪的事件数量,表示有多少个文件描述符的事件发生了。这个数量可以小于或等于
maxevents,如果没有事件发生,则返回0。如果有事件发生但返回的数量小于maxevents,表示没有更多的就绪事件,程序可以继续处理其他任务。失败:返回
-1,并设置errno来指示错误。常见的错误码包括:EINTR:系统调用被信号中断,需要重新调用epoll_wait。EBADF:epfd不是一个有效的文件描述符。EINVAL:无效的参数,可能是events为 NULL 或maxevents非法等。
epoll完整代码实例
1 |
|
三种多路复用技术的对比
| 特性 | select | poll | epoll |
|---|---|---|---|
| FD 存储 | fd_set 位图 | pollfd 结构体数组 | 内核维护 (红黑树管理所有FD,链表管理就绪FD) |
| 最大连接数 | FD_SETSIZE (通常 1024) | 无硬性限制 (受限于内存) | 无硬性限制 (受限于内存, 远大于前两者) |
| 内核/用户拷贝 | 每次调用都拷贝 fd_set | 每次调用都拷贝 pollfd 数组 | epoll_ctl 时拷贝一次, epoll_wait 可能使用共享内存 (mmap) |
| 效率/扫描方式 | 每次轮询所有 FD (O(N)) |
每次轮询所有 FD (O(N)) |
只返回就绪的 FD, 事件驱动 (O(K),K为就绪FD数) |
| 返回就绪FD | 修改传入的 fd_set | 不修改, 通过 revents 成员返回 | 返回就绪 FD 列表 |
| 触发模式 | 水平触发 (LT) | 水平触发 (LT) | 水平触发 (LT) 和边缘触发 (ET) |
| 线程安全 (指对监控集合的修改) | 不安全 (若多线程共享并修改同一个fd_set,需要外部同步) | 相对安全 (每个线程可以有自己的pollfd数组,或对共享数组同步) | epoll_ctl 操作是原子性的,epfd 本身是线程安全的 |
| 可移植性 | 好 (POSIX) | 好 (POSIX) | 差 (Linux 特有) |
- 标题: Linux高级IO模型
- 作者: The Redefine Team
- 创建于 : 2024-12-05 13:26:00
- 更新于 : 2025-06-03 14:35:19
- 链接: https://redefine.ohevan.com/2024/12/05/Linux高级IO模型/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。