目录
3. 信号驱动 IO(Signal Driven IO,SIGIO)
IO 的过程可以分为等待数据和拷贝数据两个阶段。
1. 同步阻塞 IO(Blocking IO)
同步阻塞 IO:进程发起 IO 操作后会被阻塞,知道 IO 操作完成才返回,期间无法处理其他任务。阻塞 IO 是最常见的 IO 模型。
如上图所示,进程调用 recvfrom 向内核发送读请求(读取文件、网络接收数据),内核等待数据就绪(数据从磁盘加载到内存、网络数据到达),数据就绪后,内核将数据拷贝到进程(用户)缓冲区,进程解除阻塞并处理数据。
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>
int main()
{
char buffer[1024];
while(true)
{
std::cout << "请输入数据: ";
fflush(stdout); // 立即将内核缓冲区的数据刷新到标准输出中
ssize_t n = read(0, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n - 1] = 0;
std::cout << "读取的数据:" << buffer << std::endl;
}
else
break;
}
r
如上图,标准输入默认情况下是阻塞模式的,所以每次都会等待用户进行输入之后才进行数据处理。
2. 同步非阻塞 IO(Non-Blocking IO)
2.1 同屋非阻塞 IO 介绍
同步非阻塞 IO:进程发起 IO 请求后立即返回,通过轮询方式查询 IO 操作是否完成,不会阻塞进程。
非阻塞 IO 往往需要轮询的方式反复尝试读写文件描述符,这对 CPU 来说是较大的浪费。
如上图,进程调用 recvfrom 发起读请求,第一次内核中无数据就绪,则返回,然后通过循环调用 recvfrom 发起第二次读请求,内核中依旧无数据就绪,继续返回,知道有数据就绪后,内核将数据拷贝到进程缓冲区中,并进行处理。
fcntl 函数:fcntl(file control)是 Linux 系统中用于操作文件描述符属性的核心系统调用,提供了对文件、套接字、管道等 IO 资源的高级控制能力。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
fd:目标文件描述符。
cmd:操作命令,决定 fcntl 的具体功能。
...:可变参数,根据 cmd 不同可能为整数、指针或结构体。
返回值:成功时根据 cmd 返回不同值,失败返回 -1, 并设置 errno。
下列介绍部分 cmd 参数:
(1)F_GETFL:返回 fd 的状态标志(如 O_EDONLY、O_NONBLOCK)。
(2)F_SETFL:修改 fd 状态标志,常用标志包括:
O_NONBLOCK:设置为非阻塞 IO。
O_APPEND:设置为追加模式。
O_DIRECT:绕过内核缓冲区,直接 IO。
O_ASYNC:启动信号驱动 IO 通知。
2.2 错误码介绍
read 函数用于从文件描述符中读取数据,返回读取的字节数,错误时返回 -1,并设置 errno(错误码)。
下列介绍部分调用 read 函数时发生错误设置的错误码。
(1)EAGAIN:表示操作无法立即完成,需要重试(通常用于文件描述符设置为非阻塞时)。
(2)EWOULDBLOCK:与 EAGAIN 本质相同,是其别名,不同系统可能定义不同,在 POSXI 标准中,EWOULDBLOCK 常用于套接字操作。
(3)EINTR:表示阻塞的系统调用被信号(如 SIGINT、SIGTERM)中断,导致调用未完成而返回错误。
2.3 非阻塞 IO demo 代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{
SetNonBlock(0); // 设置标准输入为非阻塞
char buffer[1024];
while(true)
{
ssize_t n = read(0, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n - 1] = 0;
std::cout << buffer << std::endl;
sleep(1);
}
else if (n < 0) // 底层数据没有准备好,数据读取不算出错
{
// 1. 读取出错或者数据没有就绪
if (errno == EAGAIN || errno == EWOULDBLOCK) // 错误码
{
std::cout << "数据没有就绪... " << std::endl;
sleep(1);
// 其他业务
// ....
continue;
}
else if (errno == EINTR)
continue;
else // 其他错误
break;
}
else
break;
}
return 0;
}
3. 信号驱动 IO(Signal Driven IO,SIGIO)
信号驱动 IO:进程通过注册信号处理函数,当 IO 操作就绪时,内核向进程发送信号(如 SIGIO),进程通过信号回调处理 IO 事件。
如上图,进程将 fd 设置为信号驱动模式,并绑定信号处理函数,内核等待数据就绪后,向进程发送 SIGIO 信号,进程捕获信号,在信号处理函数中完成 IO 操作。
4. IO 多路复用(IO Multiplexing)
IO 多路复用:通过一个进程监控多个文件描述符,当其中一个或多个准备就绪时,进程被唤醒并处理对应的 IO 操作。
在 Linux 中有三种机制进行 IO 多路复用:select、poll、epoll。
select:监控 fd(文件描述符)集合,通过遍历 fd 判断就绪状态,fd 数量受限于 FD_SETSIZE(默认 1024)。该机制支持跨平台,但是效率低,并且监控 fd 数量有限,适合小规模连接。
poll:用链表存储 fd,无数量限制,但仍需遍历所有 fd 进行就绪状态的判断。该机制监控的 fd 没数量限制,效率比 select 高(但是仍需遍历所以 fd,效率也不是特别高)。
epoll:事件驱动模型,内核维护就绪 fd 列表,仅通知就绪事件(LT/ET 模式)。Linux 高新能机制,支持百万级连接,适用于高并发场景(如 Nginx、Redis)。
上述三种机制在之后进行详细介绍。
5. 异步 IO(Asynchronous IO,AIO)
异步 IO:进程发起 IO 请求后立即返回,内核在 IO 操作完成(包括数据拷贝)后通知进程,全程无需进程主动干预。
如上图,进程通过 aio_read 向内核提交 IO 请求,指定回调函数或事件通知方式。内核负责数据读取和拷贝当用户缓冲区,完成后通过信号、回调或事件通知进程,进程处理 IO 结构,无需关注中间过程。