文章目录
1. 五种IO模型
我们以一个例子:对于钓鱼,可以总体分为等待和鱼上钩两个过程。
假设有五个人去钓场钓鱼:
- A:一直盯着鱼钩,期间不做其他事,直到上鱼。
- B:拿着手机,同时钓鱼和看手机,每隔一段时间去看一看鱼钩。
- C:在鱼漂处挂一个铃铛,专注看手机,直要铃铛一响,就证明上鱼了,此时收杆。
- D:直接架起10个鱼钩,来回走动,观察每个杆的情况。
- E:根本不去钓场,让自己的小弟e去钓鱼,E安排好情况后就去上班了,当e钓到鱼后给E汇报情况即可。
不妨想想,哪个人的钓鱼效率最高?
正如前面的效率问题,只要单位时间等待的时间比重越短,效率最高,
比如钓场的一条鱼一定会上钩,那么对于D来说,钓到鱼的概率就是10/14,而其他人就是1/14,自然D的效率最高,而对于上面五种方式,实际上为五种IO模型:
-
阻塞式 IO (Blocking IO):
- 当应用程序发起一个 IO 请求时,程序会被阻塞直到该 IO 操作完成。
- 在阻塞式 IO 模型中,应用程序在等待 IO 操作完成期间会被挂起,直到数据准备好被读取或写入。
-
非阻塞式 IO (Non-blocking IO):
- 在非阻塞式 IO 模型中,应用程序发起一个 IO 请求后不会被阻塞,而是立即返回。
- 应用程序可以周期性地检查 IO 操作是否完成,从而避免了被阻塞的情况。
- 虽然非阻塞 IO 模型减少了等待时间,但是需要应用程序不断轮询以检查 IO 状态,可能会增加 CPU 开销。
-
多路复用式 IO (I/O Multiplexing):
- 多路复用式 IO 模型通过
select
、poll
或epoll
等机制,允许应用程序同时监视多个 IO 事件。 - 应用程序通过调用这些机制中的一个来等待多个 IO 事件的发生,从而避免了阻塞。
- 当某个 IO 事件发生时,应用程序会被唤醒,并可以处理该 IO 事件。
- 多路复用式 IO 模型通过
-
信号驱动式 IO (Signal-driven IO):
- 在信号驱动式 IO 模型中,应用程序发起一个 IO 请求后,继续执行其他任务。
- 当 IO 操作完成时,操作系统向应用程序发送一个信号来通知 IO 完成,然后应用程序处理该信号并读取或写入数据。
-
异步 IO (Asynchronous IO):
- 异步 IO 模型中,应用程序发起一个 IO 请求后,不需要等待 IO 操作完成,而是可以继续执行其他任务。
- 当 IO 操作完成时,操作系统会通知应用程序,应用程序可以处理已完成的 IO 操作。
我们把IO分为了两个阶段(等 &拷贝),如果两个阶段参加了任意一个(或都参加),就叫做同步IO(A~D),如果没有参加任意一个阶段,则为异步IO(E)
2. 高级IO的重要概念
2.1 同步通信 与 异步通信
上面我们通过例子简单介绍了同步通信与异步通信的概念,这里我们加深一下了解:
-
同步通信:
- 在同步通信中,发送方发送数据后会等待接收方对数据的响应,直到接收到响应后才继续执行后续操作。
- 这意味着发送方和接收方在通信过程中是相互等待的,直到完成数据的传输和处理。
- 同步通信的一个常见例子是阻塞式 I/O,比如传统的文件 I/O 操作和网络套接字的阻塞模式。
-
异步通信:
- 在异步通信中,发送方发送数据后不会立即等待接收方的响应,而是继续执行其他操作。
- 发送方在发送数据后不会阻塞等待,而是通过回调函数、轮询或事件驱动等方式在后续得到接收方的响应或通知。
- 异步通信的一个常见例子是非阻塞式 I/O,比如异步 I/O 操作和事件驱动的网络编程模型。
简单总结,即:
- 在同步通信中,调用者发出请求后会主动等待结果的返回,直到得到结果才继续执行后续操作。
- 在异步通信中,调用者发出请求后不会立即等待结果,而是继续执行其他操作,被调用者则通过状态、通知或回调函数等方式通知调用者结果的情况。
2.2 阻塞与非阻塞
阻塞:
- 阻塞模式下,当程序执行一个操作时,如果该操作无法立即完成,程序将暂停执行,直到操作完成才会继续执行后续操作。
- 阻塞模式下,调用者会一直等待直到操作完成,期间无法进行其他任务。
非阻塞:
- 非阻塞模式下,当程序执行一个操作时,如果该操作无法立即完成,程序将不会暂停执行,而是立即返回一个状态或错误码给调用者。
- 非阻塞模式下,调用者可以继续执行其他任务,不必等待操作完成。
2.3 如何理解四者间的关系?
- 同步通信可以是阻塞或非阻塞,例如:
- 在同步阻塞通信中,发送方发送消息后会阻塞等待接收方响应;
- 而在同步非阻塞通信中,发送方发送消息后可以继续执行其他任务,但会定期检查接收方的响应状态。
2. 异步通信通常是非阻塞的,因为发送方发送消息后不会等待接收方响应,而是继续执行其他任务,接收方处理完消息后再通知发送方。
3. 其他高级IO
下面简单介绍一些高级IO,下文将讨论的是I/O多路转接:
当然,下面是对这些高级 I/O 技术的简单介绍:
3.1 非阻塞 I/O
- 定义:非阻塞 I/O 是一种 I/O 操作模式,在该模式下,I/O 操作不会阻塞当前线程。如果数据无法立即读取或写入,I/O 操作会立即返回,而不是让线程等待直到操作完成。
- 特点:
- 适用于需要处理大量并发连接的场景。
- 常用于网络编程中,例如在网络服务器中处理多个客户端连接。
3.2 纪录锁(Record Lock)
- 定义:纪录锁是一种锁机制,用于对文件中的特定区域进行锁定。这种锁通常用于数据库系统或其他需要锁定特定记录的应用程序。
- 特点:
- 可以锁定文件的部分内容而不是整个文件。
- 提供精细的锁定控制,有助于避免锁争用和提高并发性能。
3.3 系统 V 流机制
- 定义:系统 V 流(Streams)机制是一种用于处理数据流的 I/O 模型,支持对数据流的过滤和转换。通过使用流,可以将数据通过多个处理模块进行处理,每个模块可以独立处理数据的某一部分。
- 特点:
- 提供了一种模块化的数据处理方式。
- 常用于 Unix 系统中的管道和套接字编程中。
3.4 I/O 多路复用(I/O Multiplexing)
- 定义:I/O 多路复用是指在单一线程中同时处理多个 I/O 流。它允许一个线程同时监控多个 I/O 通道(例如套接字),并在某些通道准备好进行读写操作时得到通知。
- 实现方式:
- select:监控多个文件描述符,检查哪些描述符已准备好进行读写。
- poll:类似于
select
,但可以处理更多的文件描述符。 - epoll(Linux):比
select
和poll
更高效,适用于处理大量并发连接的场景。 - kqueue(BSD):与
epoll
类似,用于高效的事件通知。
3.5 readv 和 writev 函数
- 定义:
readv
:从文件描述符中读取数据到多个缓冲区。writev
:从多个缓冲区写入数据到文件描述符。
- 特点:
- 提高了 I/O 操作的效率,特别是对于需要处理多个缓冲区的数据时。
- 允许在单一系统调用中处理多个缓冲区,减少了系统调用的开销。
3.6 存储映射 I/O(mmap)
- 定义:
mmap
是一种将文件内容映射到进程的虚拟内存地址空间的技术。通过mmap
,可以将文件或设备映射到内存中,然后通过内存访问文件内容,而不是通过传统的读写操作。 - 特点:
- 提高了文件 I/O 的效率,因为可以像访问内存一样访问文件内容。
- 支持大文件的高效处理,并且可以用于实现进程间通信(IPC)。
4. 代码理解
非阻塞IO
fcntl()
fcntl
用于对文件描述符进行各种操作,包括复制、关闭、获取/设置文件描述符标志以及对文件描述符的各种属性进行操作。
一般我们用fcntl有以下操作:
- 复制描述符:可以通过
F_DUPFD
操作复制文件描述符。 - 获取/设置文件描述符标志:可以通过
F_GETFL
和F_SETFL
操作来获取和设置文件描述符的状态标志,如非阻塞标志等。 - 获取/设置文件状态标志:可以通过
F_GETFD
和F_SETFD
操作来 获取和设置 - 获得/设置异步I/O所有权:通过F_GETOWN或F_SETOWN
- 取消记录锁:可以通过 F_SETLK 操作取消记录锁。
- 锁定文件:可以通过 F_SETLK 和 F_SETLKW 操作来对文件进行加锁,用于多进程/线程间对文件的访问控制。
下面我们利用fcntl写一个简单的代码例子(利用第三条:获取/设置文件状态标志):
① SetNonBlock(设置进程非阻塞状态)
基于上面介绍的fcntl
,我们来实现一个设置进程非阻塞的代码:
bool SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL); // 获取指定文件描述符 fd 的文件状态标志
if(fl < 0) {
return false;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置文件状态标志为非阻塞
return true;
}
对代码进行解释:
int fl = fcntl(fd, F_GETFL)
:- 用于获取指定文件描述符 fd 的文件状态标志
F_GETFL
参数表示获取文件的状态标志;
fcntl(fd, F_SETFL, fl | O_NONBLOCK)
:- 传入
F_SETFL
参数,表示设置文件的状态标志 - 通过将原来的文件状态标志 fl 与
O_NONBLOCK
进行按位或 。O_NONBLOCK 是一个宏定义,用于表示将文件设置为非阻塞模式。
- 传入
② 轮询方式读取标准输入
首先将标准输入设置为非阻塞,随后在循环内进行读取操作:
int main()
{
SetNonBlock(0); // 将标准输入设置为非阻塞
char buffer[1024];
// 循环读取:
// 在读取数据时,如果返回的字节数 s 大于0,表示成功读取了数据。然后将读取的数据输出到标准输出,并清空缓冲区。
// 如果读取数据失败,则根据 errno 的值进行不同的处理。
while(true)
{
sleep(1);
errno = 0;
// 非阻塞时,以出错形式返回,告知上层:数据未就绪
ssize_t s = read(0, buffer, sizeof(buffer) - 1); // 0: 标准输入
if(s > 0) {
buffer[s-1] = 0;
std::cout << "echo# " << buffer << " errno[---]" << errno << "errstring: " << strerror(errno) << std::endl;
} else {
// 若errno为11,意味着是底层数据未就绪,并非出错
std::cout << "read \"error\" " << "errno: " << errno << "errstring: " << strerror(errno) << std::endl;
}
}
}
执行上面的代码,会发现错误码为11:意味着数据非就绪(非阻塞下),并非一种错误,只需要等待数据就绪。
随后可以完善代码:
int main()
{
SetNonBlock(0); // 设置一次非阻塞
char buffer[1024];
while(true)
{
sleep(2);
errno = 0;
// 非阻塞时,以出错形式返回,告知上层:数据未就绪
ssize_t s = read(0, buffer, sizeof(buffer) - 1); // 0: 标准输入
if(s > 0) {
buffer[s-1] = 0;
std::cout << "echo# " << buffer << " errno[---]" << errno << " | errstring: " << strerror(errno) << std::endl;
} else {
// 若errno为11,意味着是底层数据未就绪,并非出错
// std::cout << "read \"error\" " << "errno:" << errno << " | errstring: " << strerror(errno) << std::endl;
if(errno == EWOULDBLOCK || errno == EAGAIN) // 均为11
{
std::cout << "当前0号fd数据未就绪. try again!" << std::endl;
continue;
}
else if (errno == EINTR)
{
std::cout << "read被信号中断" << std::endl;
continue;
}
else
{
// 错误处理
std::cout << "read error" << std::endl;
}
}
}
}
此时我们执行程序,当未输入数据时,进入的分支是errno == EWOULDBLOCK || errno == EAGAIN
,一旦输入数据,可以正确读取