一、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; }