高山仰止,景行行止
文章目录
- 五种IO模型
- 阻塞I/O
- 非阻塞I/O
- I/O复用
- 信号驱动I/O
- 异步I/O
- 同步通信与异步通信
- 同步通信
- 异步通信
- 非阻塞IO
- 基于fcntl实现setNonblock函数
- 注意事项
- IO多路转接—select
- 文件描述符集合
- timeval结构
- 调用过程
- 返回值
- 缺点和局限性
- IO多路转接—poll
- struct pollfd 结构体
- 返回值
- 优缺点
- IO多路转接—epoll
- 原理和特点
- 函数原型
- epoll_create
- epoll_ctl
- epoll_wait
- struct epoll_event 结构体
- 工作模式
- 水平触发
- 边缘触发
- epoll惊群问题
- 优缺点
- 总结
五种IO模型
在计算机网络中,I/O模型描述了数据在网络套接字和应用程序之间传输的方式。常见的五种I/O模型包括:阻塞I/O、非阻塞I/O、I/O复用、信号驱动I/O和异步I/O。
任何IO过程中,都包含两个步骤:等待和拷贝.。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。为了让IO更高效,最核心的办法就是让等待的时间尽量少
。
阻塞I/O
- 原理:应用程序执行一个I/O操作时,如果数据未准备好,调用将会被阻塞,直到数据准备好为止。在数据被复制到应用程序缓冲区之前,应用程序一直等待,不会返回到主程序。
- 应用场景:适用于单用户和单任务对性能要求不高的应用程序。
非阻塞I/O
- 原理:应用程序执行一个I/O操作时,如果数据未准备好,调用立即返回一个错误。应用程序可以继续做其他事情,它必须不断地询问I/O操作是否完成。
- 应用场景:适用于需要同时处理多个操作或任务的应用程序,但要求应用程序显式管理检查数据状态和重试操作。
I/O复用
- 原理:使用select或poll等系统调用,允许应用程序监视多个文件描述符,一旦某个文件描述符就绪(读就绪或写就绪),能够通知程序进行相应的读写操作。这样,单个进程可以处理多个并发数据流。
- 应用场景:适用于需要处理多个连接或多种网络条件的服务器应用,如Web服务器、数据库服务器。
信号驱动I/O
- 原理:应用程序通过sigaction系统调用预先注册一个信号处理函数,当数据准备就绪时,系统产生一个SIGIO信号,应用程序随后开始I/O操作。
- 应用场景:适用于希望避免轮询但又不希望I/O操作阻塞进程的场景。
异步I/O
- 原理:应用程序发起一个I/O操作后立即返回,不需要等待I/O操作完成。当I/O操作完成后,应用程序会收到一个通知。
- 应用场景:适用于需要高性能I/O处理的应用程序,特别是那些需要处理大量并发I/O操作或者对响应时间有严格要求的应用。
同步通信与异步通信
在计算机网络和系统设计中,同步通信和异步通信是两种基本的数据交换方式,它们在处理数据传输、通信过程控制、资源占用等方面有明显的不同。
同步通信
同步通信要求发送方和接收方在交换信息时必须同时处于就绪状态,即发送方在发送数据后,必须等待接收方的响应,才能继续进行下一步的操作。在同步通信中,双方的交互是实时进行的,这意味着任一方都必须在通信过程中等待对方,直到交换的信息被接收和确认。
- 优点:数据的一致性和可靠性高,易于理解和实现。
- 缺点:可能会导致资源的浪费,因为在等待响应期间,某些资源(如CPU、网络等)可能处于闲置状态。这种方式在处理速度较慢或负载较高的系统中效率不高。
异步通信
异步通信允许发送方在发送数据后不必立即等待响应,而是可以继续执行其他任务。接收方在接收和处理数据后,可以通过回调、事件、消息队列等方式通知发送方结果。在异步通信中,发送方和接收方不需要同时处理交换的信息,减少了等待时间。
- 优点:提高了系统的整体效率和吞吐量,因为发送方在等待响应期间可以处理其他任务。非常适合于处理速度不均或多任务环境。
- 缺点:编程模型更复杂,需要更多的错误处理机制。数据的一致性和顺序控制也更加困难。
注意,这里的同步通信与异步通信和线程的同步与互斥是完全不相干的概念,线程的同步是线程间的一种相互制约,相互协调的关系。
非阻塞IO
一般来说,文件描述符默认都是非阻塞的,如果想将文件描述符设置为非阻塞,可以使用函数fcntl
。fcntl
是 Unix 和类 Unix 系统中的一个重要的系统调用,全称是 “file control”。它提供了对文件描述符的各种控制操作,包括复制文件描述符、获取/设置文件描述符属性、文件锁定等。fcntl
是一个非常灵活的命令,其功能由命令参数指定。
语法
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
- fd: 文件描述符,是一个指向打开文件的引用。
- cmd: 指定要执行的操作类型。
- …: 额外参数,其类型和数量取决于
cmd
参数的值。
常见用途和命令
-
复制文件描述符 (
F_DUPFD
或F_DUPFD_CLOEXEC
)- 创建一个新的文件描述符,作为旧文件描述符的副本。这通常用于重定向标准输入/输出或处理多线程中的文件描述符。
-
获取/设置文件描述符标志 (
F_GETFD
和F_SETFD
)- 获取或设置文件描述符的标志,例如设置
FD_CLOEXEC
标志,使得在执行exec
调用新程序时关闭文件描述符。
- 获取或设置文件描述符的标志,例如设置
-
获取/设置文件状态标志 (
F_GETFL
和F_SETFL
)- 获取或设置文件状态标志,如非阻塞 (
O_NONBLOCK
)、同步 (O_SYNC
) 等。
- 获取或设置文件状态标志,如非阻塞 (
-
文件锁定 (
F_GETLK
,F_SETLK
,F_SETLKW
)- 对文件或文件的一部分进行锁定,避免多个进程同时写入同一文件区域。
F_SETLK
设置锁但如果无法立即获得锁则失败,F_SETLKW
则会等待直到锁被获取,而F_GETLK
可以用来测试锁的状态。
- 对文件或文件的一部分进行锁定,避免多个进程同时写入同一文件区域。
示例
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
int main()
{
std::cout << ">>>>>> ";
fflush(stdout);
char buffer[1024];
fcntl(0, F_SETFL, O_NONBLOCK);
int ret = read(0, buffer, sizeof buffer);
if(ret < 0)
{
std::cout << "read fail\n";
exit(-1);
}
return 0;
}
基于fcntl实现setNonblock函数
void setNonblock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if(fl < 0)
{
std::cout << "fcntl getfl fail\n";
exit(-1);
}
fl |= O_NONBLOCK;
fcntl(fd, F_SETFL, fl);
}
注意事项
fcntl
的行为可能会根据不同的 UNIX 系统变化,特别是在文件锁定和某些命令的支持方面。- 在多线程环境中使用
fcntl
进行文件锁定时要小心,确保不会导致死锁。 - 一些
fcntl
操作可能会因为文件类型而不被支持,比如对某些类型的特殊文件或网络套接字可能不支持文件锁定。
IO多路转接—select
select
函数是一种 I/O 多路复用机制,用于监视多个文件描述符的状态变化,以便在其中至少一个文件描述符就绪时进行读取、写入或异常处理。
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
:表示select
函数的超时时间,如果为NULL
则表示阻塞直到至少有一个文件描述符就绪。
文件描述符集合
fd_set
是一个位图(bitmask),用于表示一组文件描述符。通过宏函数 FD_ZERO
、FD_SET
、FD_CLR
和 FD_ISSET
,可以对 fd_set
进行操作,例如清零、设置、清除和测试文件描述符的状态。
timeval结构
timeval
结构是用于表示时间间隔的数据结构,通常用于设置超时时间或者记录时间间隔的信息。在 POSIX 系统中经常被使用,比如在网络编程中的超时设置,或者在计时功能中的时间间隔记录等。
timeval
结构的定义通常如下所示:
tv_sec
:表示秒数部分,是一个long
类型的整数,用于表示秒数。tv_usec
:表示微秒部分,是一个long
类型的整数,用于表示微秒数(以毫秒为单位)。
timeval
结构主要用于设置超时时间,比如在调用 select
函数时设置超时时间,或者在 setsockopt
函数中设置超时选项。此外,它还可以用于记录时间间隔,比如在计时功能中记录程序执行的时间间隔。
调用过程
-
初始化
fd_set
:通过执行FD_ZERO(&set);
初始化fd_set
变量set
,确保所有位都清零。这个步骤是必要的,因为它保证了set
的起始状态是确定的,没有任何文件描述符被监视。 -
添加文件描述符到
fd_set
:- 当执行
FD_SET(fd, &set);
时,如果fd=5
,则set
的第5位(从0开始计数)被设置为1,set
的位表示变为0001,0000。 - 如果继续添加
fd=2
和fd=1
,则相应的第2位和第1位也被设置为1,set
现在变为0001,0011,表示fd=1
、fd=2
和fd=5
都在监视之列。
- 当执行
-
调用
select
等待事件:select(6, &set, NULL, NULL, NULL);
调用会阻塞等待,直到监视的文件描述符之一(在这个例子中,是fd=1
、fd=2
或fd=5
)上发生可读事件,或者发生了错误或异常条件。参数6
表示监视的文件描述符范围从0到5,这是因为select
的第一个参数应该是监视的文件描述符集中最大文件描述符加1。 -
检查
select
的结果:当select
返回后,set
会被修改以反映哪些文件描述符上发生了事件。如果fd=1
和fd=2
上发生了可读事件,则它们在set
中对应的位保持为1,表示它们准备好被读取。没有事件发生的fd=5
对应的位会被清零,因为select
会修改fd_set
来仅包含那些发生了指定事件的文件描述符。
返回值
- 如果至少有一个文件描述符就绪,
select
函数返回就绪文件描述符的数量。 - 如果超时时间到达,没有文件描述符就绪,
select
返回 0。 - 如果出现错误,
select
返回 -1,并设置errno
变量来指示错误类型。
缺点和局限性
- 文件描述符数量限制:某些系统对单个进程能够监视的文件描述符数量有限制,这会限制
select
函数的使用。 - 效率问题:
select
函数需要遍历所有待监视的文件描述符,效率在文件描述符数量较大时可能不高。 - 没有提供文件描述符状态改变的通知机制,需要不断轮询检查文件描述符的状态。
IO多路转接—poll
poll
函数是一个I/O多路复用的系统调用,允许一个进程监视多个文件描述符,以确定是否可以执行读取、写入或是出错等操作,而无需阻塞等待。
函数原型:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds
:指向一个struct pollfd
数组的指针,该结构体用于描述要监视的文件描述符及其事件。nfds
:表示要监视的文件描述符的数量。timeout
:表示超时时间,以毫秒为单位。如果设置为负数,则poll
会阻塞直到有文件描述符准备就绪;如果设置为0,则poll
立即返回而不阻塞;如果大于0,则表示等待的最大时间。
struct pollfd 结构体
fd
:要监视的文件描述符。events
:要监视的事件,包括POLLIN
(可读)、POLLOUT
(可写)等。revents
:实际发生了的事件,由poll
函数填充。
返回值
-1
:发生错误,可以通过errno
变量查看具体错误原因。0
:超时。> 0
:返回就绪的文件描述符数量。
优缺点
优点:
-
支持大量文件描述符:
poll
能够有效地处理大量的文件描述符,通常没有文件描述符数量的限制。 -
可移植性:
poll
是POSIX标准的一部分,在大多数主流操作系统上都有支持,包括Linux、Windows和Unix等。 -
简单易用:
poll
的接口相对简单,并且易于使用和理解,只需要准备一个struct pollfd
数组来描述要监视的文件描述符和事件。
缺点:
-
效率问题: 尽管
poll
在处理大量文件描述符时比select
效率更高,但在某些情况下仍可能存在效率问题,特别是当文件描述符数量非常大时。 -
性能下降:
poll
是一种轮询机制,需要遍历所有的文件描述符来检查状态变化,因此随着文件描述符数量的增加,性能可能会下降。 -
没有就绪文件描述符数量的返回值:
poll
函数只返回就绪的文件描述符的数量,而不提供就绪的文件描述符的具体信息,需要通过遍历struct pollfd
数组来获取。
IO多路转接—epoll
epoll
是Linux提供的一种I/O多路复用机制,用于有效地处理大量的文件描述符,并且能够高效地监视多个文件描述符的状态变化。
原理和特点
数据结构:
- 红黑树(Red-Black Tree):
epoll
使用红黑树来存储需要监视的文件描述符(FD)。 - 就绪链表(Ready List): 用于存储发生事件的文件描述符。
主要流程:
-
创建
epoll
实例: 用户调用epoll_create
函数创建一个epoll
实例,并返回一个文件描述符,用于操作该实例。 -
添加文件描述符: 用户通过
epoll_ctl
函数将需要监视的文件描述符添加到epoll
实例中。 -
等待事件发生: 用户调用
epoll_wait
函数等待事件发生,内核会阻塞进程直到有文件描述符发生事件。 -
事件通知: 当文件描述符上有事件发生时,内核会将发生事件的文件描述符加入到就绪链表中,并唤醒等待的进程。
-
处理事件: 用户从就绪链表中获取发生事件的文件描述符,并处理相应的事件。
工作原理:
- 当调用
epoll_wait
函数时,内核会遍历红黑树,找到有事件发生的文件描述符,并将其添加到就绪链表中。 - 用户可以通过就绪链表获取发生事件的文件描述符,然后进行相应的处理。
函数原型
epoll_create
、epoll_ctl
和epoll_wait
,它们是使用epoll
进行事件驱动的核心。
epoll_create
int epoll_create(int size);
- 功能: 创建一个
epoll
实例,并返回一个文件描述符。 - 参数:
size
:指定epoll
实例的大小,通常设置为一个大于0的数即可,但实际上内核会忽略这个参数。
- 返回值:
- 如果成功,返回一个新的文件描述符,代表创建的
epoll
实例; - 如果失败,返回-1,并设置errno。
- 如果成功,返回一个新的文件描述符,代表创建的
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 功能: 控制
epoll
实例,用于添加、修改或删除要监视的文件描述符。 - 参数:
epfd
:epoll
实例的文件描述符,由epoll_create
返回。op
:操作类型,可以是EPOLL_CTL_ADD
、EPOLL_CTL_MOD
或EPOLL_CTL_DEL
,分别表示添加、修改或删除文件描述符。fd
:要进行操作的文件描述符。event
:要监视的事件,是一个struct epoll_event
结构体。
- 返回值:
- 如果成功,返回0;
- 如果失败,返回-1,并设置errno。
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 功能: 等待事件发生,返回发生了事件的文件描述符。
- 参数:
epfd
:epoll
实例的文件描述符,由epoll_create
返回。events
:用于存储发生事件的文件描述符和事件的数组。maxevents
:events
数组的最大容量。timeout
:等待超时时间,单位为毫秒,传入-1表示永久等待。
- 返回值:
- 如果成功,返回发生了事件的文件描述符数量;
- 如果超时,返回0;
- 如果出错,返回-1,并设置errno。
这些函数共同构成了epoll
的核心API,允许用户控制监视的文件描述符和等待事件的情况,从而实现高效的事件驱动机制。
struct epoll_event 结构体
events
:要监视的事件,包括EPOLLIN
(可读)、EPOLLOUT
(可写)等。data
:用户数据,通常是文件描述符。
工作模式
epoll
具有两种工作模式:水平触发(Level-Triggered)和边缘触发(Edge-Triggered)
水平触发
- 特点:
- 当文件描述符准备好时,
epoll_wait
返回,并且在下次仍然会触发。 - 如果文件描述符处于就绪状态,但未处理完全部数据,下次
epoll_wait
仍然会返回该文件描述符。
- 当文件描述符准备好时,
- 适用情况:
- 适用于普通的I/O事件处理,适用性广泛,使用比较灵活。
- 优点:
- 兼容性好,适用于大多数场景。
- 缺点:
- 容易出现事件重复通知,可能导致性能损耗。
边缘触发
- 特点:
- 当文件描述符状态发生变化时,
epoll_wait
返回,但只返回一次,需要重新注册。 - 仅在状态变化时通知用户,不会重复通知相同的事件。
- 当文件描述符状态发生变化时,
- 适用情况:
- 适用于需要精确控制事件处理的高性能场景。
- 优点:
- 减少了事件重复通知,提高了效率。
- 缺点:
- 需要用户自行管理文件描述符的状态变化,可能较为复杂。
选择建议:
- 如果对于同一个文件描述符的事件可能会频繁发生而且需要一直关注,可以选择水平触发模式。
- 如果希望系统更高效地处理事件,并且能够减少事件通知次数,可以选择边缘触发模式。
epoll惊群问题
"惊群问题"是在使用多线程或多进程并发处理网络连接时可能遇到的一种性能问题。它的主要原因是内核中的事件通知机制会通知所有等待相同事件的线程或进程,导致多个线程或进程同时被唤醒,竞争处理同一个事件,造成资源浪费和性能下降。
在 epoll
中,惊群问题通常指当一个文件描述符上的事件就绪时,epoll
调用 epoll_wait
返回,然后多个线程或进程被唤醒来处理这个事件,但实际上只有一个线程或进程能够处理该事件,其他的则白白被唤醒了。
解决 epoll
中的惊群问题,可以采取以下措施:
- 多线程下,确保只有一个线程在
epoll_wait
,然后有该线程对事件进行分配给其他线程处理。 - 多进程下,使用一把全局互斥锁,在子进程进行
epoll_wait
前,则先获取锁。
优缺点
优点:
- 高性能:
epoll
使用红黑树和就绪链表管理文件描述符,能够高效地处理大量的并发连接,适用于高性能的网络服务器。 - 支持大规模并发:
epoll
不受文件描述符数量的限制,适用于处理大规模的并发连接。 - 事件驱动:
epoll_wait
函数阻塞进程直到有事件发生,避免了轮询的开销,提高了系统的效率。 - 边缘触发模式:
epoll
支持边缘触发模式,可以减少事件通知次数,提高了系统的性能。
缺点:(基本没什么缺点,挑刺)
- Linux专有:
epoll
是Linux特有的系统调用,在其他操作系统上无法直接使用。 - 复杂性: 使用
epoll
相对于select
和poll
来说,需要更复杂的编程模型和更深入的理解,使用不当容易导致性能问题或者编程错误。 - API变化: 随着Linux内核版本的更新,
epoll
的API可能会发生变化,需要及时了解和适应新的API。
总结
特性 | select | poll | epoll |
---|---|---|---|
跨平台性 | 良好 | 良好 | 仅适用于Linux系统 |
效率 | 低效,性能随文件描述符数量增加而下降 | 低效,性能随文件描述符数量增加而下降 | 高效,性能不随文件描述符数量增加而下降 |
最大文件描述符数 | 有限制 | 无限制 | 无限制 |
编程复杂度 | 简单 | 简单 | 相对较高 |
边缘触发 | 否 | 否 | 是 |
适用场景 | 小规模并发连接 | 小规模并发连接 | 大规模并发连接 |
从上表可以看出,epoll在性能、文件描述符数量无限制和支持边缘触发等方面具有优势,适用于大规模并发连接的场景。select和poll在跨平台性和简单易用性方面具有优势,但在处理大规模并发连接时性能较差。选择合适的I/O多路复用方式应根据具体的应用需求和环境来决定。