1. 五种IO模型

举例:钓鱼

钓鱼 = 等 + 钓,对应IO = 等内核准备数据 + 拷贝到用户缓冲区

模型 钓鱼特点 核心特点 等待方式 效率
阻塞 IO 专注钓鱼,鱼漂不动就一直等 全程死等,不做任何事 阻塞等待 最低
非阻塞 IO 不会因为鱼不上钩就卡死在监测鱼漂上 不阻塞,轮询检查状态 非阻塞轮询 比阻塞高,但用户态 CPU 空转
信号驱动 IO 鱼上钩后鱼漂会反向通知他 不主动等,靠信号通知再处理 信号通知 高,不轮询,也不阻塞
IO 多路复用(同步) 架很多支鱼竿(100 只),同时看所有鱼漂 一个线程 / 进程管理多个连接,集中等待 多路复用(select/poll/epoll) 高,单线程处理大量连接
异步 IO 只爱吃鱼,让别人钓鱼后自己离开,鱼钓好直接送过来 全程不参与「等 + 钓」,完成后通知 内核全程处理 最高,完全不阻塞用户

1.1 阻塞IO

在内核将数据准备好之前,系统调用会一直等待,所有的套接字默认都是阻塞方式。阻塞IO是最常见的IO模型

1.2 非阻塞IO

  • 如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码
  • 非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询
  • 这对CPU来说是较大的浪费,一般只有特定场景下才使用

1.3 信号驱动IO

内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作

1.4 IO多路转接

虽然从流程图上看起来和阻塞IO类似,但核心在于IO多路转接能够同时等待多个文件描述符的就绪状态

1.5 异步IO

由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)

2. 高级IO重要概念

2.1 同步通信 vs 异步通信

同步和异步关注的是消息通信机制

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了;换句话说,就是由调用者主动等待这个调用的结果
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果;换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用

这里的同步通信和进程之间的同步是完全不相干的概念

  • 进程 / 线程同步也是进程 / 线程之间直接的制约关系
  • 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系。尤其是在访问临界资源的时候

2.2 阻塞 vs 非阻塞

  • 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程,调用会立即返回,程序可以继续执行后续操作,之后再主动轮询结果或通过其他方式获取返回信息

2.3 总结

  • 怎么拿结果 → 同步 / 异步
  • 线程卡不卡 → 阻塞 / 非阻塞

提到进程 / 线程同步,和这里调用同步不是一回事:

  • 这里的同步 / 异步:面向函数调用、IO、网络通信
  • 操作系统里进程 / 线程同步:面向多线程协作、临界资源竞争(配合互斥使用),只是同名概念,语义无关

3. 其他高级 IO

非阻塞 IO,纪录锁,系统 V 流机制,I/O 多路转接(也叫 I/O 多路复用),readv 和 writev 函数以及存储映射 IO(mmap),这些统称为高级 IO

此处重点讨论的是 I/O 多路转接

4. 非阻塞IO

4.1 fcntl

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

fcntl是文件描述符万能控制函数,cmd 不同,功能完全不同,后面参数也不同

fcntl 5 大功能:

  1. 复制文件描述符:cmd = F_DUPFD
  2. 获得 / 设置 文件描述符标记:cmd = F_GETFD / F_SETFD
  3. 获得 / 设置 文件状态标记:cmd = F_GETFL / F_SETFL,控制的是:文件打开方式 / 状态包含的标志:
    • O_RDONLY / O_WRONLY
    • O_NONBLOCK 非阻塞
    • O_APPEND 追加写
  4. 获得 / 设置 异步 IO 所有权:cmd = F_GETOWN / F_SETOWN
  5. 获得 / 设置 记录锁:cmd = F_GETLK / F_SETLK / F_SETLKW

此处只是用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞

4.2 实现函数SetNoBlock

基于fcntl,实现一个SetNoBlock函数,将文件描述符设置为非阻塞

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>   // perror

// 功能:把文件描述符 fd 设置为非阻塞 IO
void SetNoBlock(int fd) { 
    // 1. 获取当前文件描述符的 文件状态标记(位图)
    int fl = fcntl(fd, F_GETFL); 

    // 2. 如果获取失败(比如 fd 非法),打印错误并返回
    if (fl < 0) { 
        perror("fcntl F_GETFL failed");
        return; 
    }

    // 3. 设置新标记:原有属性 | 非阻塞标记
    // 位或运算:只添加 O_NONBLOCK,不修改原来的属性
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); 
}

为什么要先获取fd?文件描述符会有很多属性,如果直接使用会覆盖/清空原来的属性!再使用 | 追加

4.3 非阻塞 + 轮询

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h> 

// 设置非阻塞
void SetNoBlock(int fd) {
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0) {
        perror("fcntl");
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

int main() {
    SetNoBlock(0);  // 0 代表标准输入 stdin
    while (1) {
        char buf[1024] = {0};
        // 非阻塞 read
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size < 0) {
            // 重点:非阻塞没数据,会触发 EAGAIN/EWOULDBLOCK
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                printf("没有数据,轮询等待...\n");
                sleep(1);
                continue;
            }
            perror("read");
            sleep(1);
            continue;
        }
        // 读到数据
        printf("input:%s\n", buf);
    }
    return 0;
}

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐