一、IO理解
1.1 什么是 IO
IO(Input/Output)指的是:计算机系统中数据的输入与输出操作。
在 Linux 中 "一切皆文件",网络套接字、磁盘文件、管道、终端设备等,都对应一个文件描述符(
fd)内核通过
fd来统一管理所有 IO 资源。
1.2 IO 操作的两个阶段
一次 IO 读操作分为两个阶段:
阶段1:等待数据就绪
- 数据从网卡/磁盘到达内核缓冲区(kernel buffer)
- 此时进程可能需要等待
阶段2:数据拷贝
- 内核将数据从内核缓冲区拷贝到用户进程缓冲区(user buffer)
- 拷贝完成后返回
二、五种 IO 模型
2.1 阻塞 IO
用户进程 内核
| |
|-- read(fd, buf) ----------->|
| (进程阻塞) |-- 等待数据到达
| |-- 数据到达内核buffer
| |-- 内核拷贝数据到用户buf
|<------- 返回 --------------- |
| |
所有的 socket 默认都是阻塞模式
在数据未就绪时,进程被挂起,无法做其他事
两个阶段都会阻塞
2.2 非阻塞 IO ★
用户进程 内核
| |
|-- read(fd, buf) ----------->|
|<------- EAGAIN ------------ | (数据未就绪)
| |
| (做其他事或继续轮询) |
| |
|-- read(fd, buf) ----------->|
|<------- 返回数据 ------------| (数据已就绪)
通过 fcntl 设置 O_NONBLOCK 标志
数据未就绪时立即返回 EAGAIN/EWOULDBLOCK
需要不断轮询,浪费 CPU
2.3 信号驱动 IO
用户进程 内核
| |
|-- 注册SIGIO信号处理函数 ----> |
|-- fcntl设置属主和信号 ------->|
| (做其他事) |
| |-- 数据到达
|<------ SIGIO信号 ----------- |
|-- read(fd, buf) ----------->|
|<------- 返回数据 ----------- |
内核在数据就绪时发送 SIGIO 信号通知进程
进程收到信号后再调用 read 读取
实际使用较少
2.4 IO 多路复用 ★
用户进程 内核
| |
|-- select/poll/epoll_wait -->|
| (阻塞在select上) |-- 监控多个fd
| |-- 某个fd就绪
|<------- 返回就绪fd列表 ----- |
| |
|-- read(就绪的fd) ----------> |
|<------- 返回数据 ----------- |
用一个系统调用同时监控多个 fd
当某个 fd 就绪时,再进行对应的读写
select / poll / epoll 三种实现
2.5 异步 IO
用户进程 内核
| |
|-- aio_read / io_uring ----->|
| (做其他事) |-- 等待数据到达
| |-- 数据拷贝到用户buf(内核完成)
|<------ 完成通知/回调 ------- |
| |
真正的异步:发起请求后,内核完成全部工作(包括数据拷贝)后通知进程。
Linux 的实现:POSIX AIO、Linux AIO、io_uring
三、IO重要概念
3.1 阻塞与非阻塞
阻塞IO : 因IO条件不满足(数据从网卡/磁盘还未到达内核缓冲区),而阻塞卡住,直到IO条件就绪。
非阻塞IO : 采用轮询模式判断IO条件是否满足,若检测到IO条件不具备,就出错返回。
3.2 同步与异步
同步IO : 参与了IO的一个阶段或多个阶段过程 (等待数据 + 拷贝数据)
异步IO : 仅"发起IO" 或 "IO工作流"与当前工作流无关
四、非阻塞IO
4.1 fcntl 系统调用
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
复制一个现有的描述符(cmd=F_DUPFD)。
获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)。
获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)。
获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)。
获得/设置记录锁(cmd=F_GETLK、F_SETLK或F_SETLKW)。
4.2 代码示例
设置非阻塞模式
#include <fcntl.h>
#include <unistd.h>
// 获取当前文件状态标志
int flags = fcntl(fd, F_GETFL, 0);
// 设置非阻塞标志
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
char buf[1024];
ssize_t n;
// 将标准输入设置为非阻塞
int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
printf("尝试读取(非阻塞模式)...\n");
while (1)
{
n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if (n > 0)
{
buf[n] = '\0';
printf("读到 %zd 字节: %s", n, buf);
}
else if (n == -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
printf("数据未就绪,稍后再试...\n");
usleep(500000); // 500ms 后重试
}
else
{
perror("read error");
break;
}
}
else if (n == 0)
{
printf("EOF\n");
break;
}
}
return 0;
}