I/O操作 (输入/输出操作, Input/Output) 是指计算机与外部设备就行数据交互的过程.
什么是外部设备: 如键盘, 鼠标, 硬盘, 网卡等.
五种常见的 I/O 模型:
- 阻塞 I/O
- 非阻塞 I/O
- 信号驱动 I/O
- I/O 多路复用
- 异步 I/O
阻塞 I/O
阻塞 I/O 的特点: 当用户发起 I/O 请求后, 进程/线程就会被阻塞, 直到这个 I/O 操作完成.
1. 发起 I/O 请求 ( read/write ).
2. 如果数据为准备好, 那么进程/线程就会被阻塞, 进入等待状态
3. 数据准备好了, 操作系统将数据复制到用户空间, 进程/线程获取到了数据, 就被唤醒继续执行
就像是一个人去钓鱼, 当他甩鱼竿之后, 就一直盯着鱼竿什么都不做, 直到看见有鱼上钩了, 将鱼竿收起来.
阻塞 I/O: 实现简单, 容易理解.
缺点: 但是效率低下, 因为进程/线程会一直等待 I/O 操作完成, 等待期间不会执行其他的任务, 资源的利用率低.
非阻塞 I/O
上面了解了阻塞 I/O 是一直等待 I/O 操作完成形成阻塞.
那么非阻塞 I/O 也就是当进程/线程发起 I/O 请求后, 即使数据没有准备好, 也会立即返回. 不会被阻塞住.
1. 进程/线程发起非阻塞 I/O
2. 如果数据未准备好, 操作系统就会返回一个错误
3. 进程或线程需要不断的轮询, 检查数据是否准备好
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>#define BUFFER_SIZE 1024int main() {char buffer[BUFFER_SIZE];int flags;// 获取标准输入的文件描述符的当前标志flags = fcntl(STDIN_FILENO, F_GETFL, 0);if (flags == -1) {perror("fcntl F_GETFL");return 1;}// 设置标准输入为非阻塞模式if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) == -1) {perror("fcntl F_SETFL");return 1;}while (1) {// 尝试从标准输入读取数据ssize_t n = read(STDIN_FILENO, buffer, BUFFER_SIZE - 1);if (n == -1) {if (errno == EAGAIN || errno == EWOULDBLOCK) {// 没有数据可读,进行其他操作printf("No input available, doing other work...\n");sleep(1);} else {perror("read");break;}} else if (n > 0) {// 读取到数据,处理数据buffer[n] = '\0';printf("Read input: %s", buffer);} else {break;} }return 0;
}
可以看到代码中对于 read 函数的操作需要使用循环不断的检查数据是否准备完成. 这个不断循环的过程就称为轮询.
就像去钓鱼, 我不会一直在那盯着鱼竿, 而是每过一会就去看看有没有鱼上钩, 在这期间可以去看看手机, 刷刷视频.
优点: 如果检测到数据还没准备好, 那么此时程序也可以执行一些耗时不长的任务, 这也是非阻塞 I/O 的一个优点.
缺点: 轮询机制会占用大量的 CPU 资源, 效率低下.
信号驱动 I/O
基于信号通知进程/线程 I/O 操作完成.
1. 程序注册一个信号处理器
2. 当数据准备好了, 操作系统就会发送对应的信号通知程序
3. 程序接收到信号后, 信号处理器就会执行对应操作
钓鱼的时候, 每过一段时间就去看看太麻烦了, 所以在鱼竿上装了一个铃铛, 当有鱼咬钩时, 铃铛就会响. 铃铛响将相当于是信号. 铃铛不响我就继续做我自己的事, 看看手机....
优点: 这也是非阻塞的一个 I/O 模型, 有着非阻塞 I/O 的优点. 通过信号可以准确快速的响应 I/O 事件.
缺点: 那么引入了信号, 程序就必然会变得更加复杂, 容易出现错误.
I/O 多路复用
上面的 I/O 模型中, 都是对某一个 I/O 操作进行管理.
I/O 多路复用则是对于多个 I/O 操作进行管理, 当某个 文件描述符(fd) 准备好了后, 系统就会发送通知, 此时程序就可以对这些 准备好了的 fd 进行操作
1. 通过 select, poll 或 epoll 监控多个文件描述符 (fd) 的状态
2. 当有 fd 准备好了后, 操作系统通知程序, 程序就对这些准备好的 fd 进行操作
上面钓鱼中, 我只带了一根钓鱼竿 , 那么现在我带了50根钓鱼竿, 50根杆同时钓鱼, 效率大幅提升.
异步 I/O
前面的四种都是同步 I/O. I/O = 等待 + 拷贝. 同步 I/O 至少参与了一个过程 (等待或拷贝).
而异步 I/O 则是两个都不参加. 有操作系统完成 I/O 操作, 操作系统完成后通知进程/线程. 进程/线程可以通过回调函数或事件通知机制获取本次 I/O 操作的结果.
1. 用户进程/线程发起异步 I/O 请求, 直接返回
2. 操作系统负责完成 I/O 操作, 并在完成后通知用户进程/线程
3. 用户线程通过回调函数或事件通知机制获取本次 I/O 结果
之前钓鱼, 都是自己在操作, 但是现在我雇佣一个人来帮我钓鱼, 当他钓鱼结束后, 将钓到的鱼交给我就行, 我不用去管钓鱼的事.
当然这一种模型也很复杂的. 对于程序员的要求较高
同步和异步
同步: 必须等待任务完成后, 才能执行下一个任务. 像上面的四种 I/O 模型中, 无论是阻塞还是非阻塞, 我们都在等待它的返回值 (等待它执行的结果). 只有等到了它执行的结果, 程序才会继续向下执行. 异步也是不断地继续轮询, 直到等到执行的结果
异步: 则不会等待这个任务, 而是继续向后执行. 如异步的进行 I/O 请求, 虽然也会直接进行返回, 但是这个返回并没有附带本次操作的结果. 重点并不是返回, 重点是本次请求的结果是否被等待了. 至于本次任务执行的结果, 则是通过其他方法通知给程序 (如: 回调函数等)