五种 IO 模式的简单介绍 -- 阻塞 IO,非阻塞 IO,信号驱动 IO,IO 多路复用,异步 IO

发布于:2025-06-25 ⋅ 阅读:(20) ⋅ 点赞:(0)

目录

1. 同步阻塞 IO(Blocking IO)

2. 同步非阻塞 IO(Non-Blocking IO)

2.1 同屋非阻塞 IO 介绍

2.2 错误码介绍

2.3 非阻塞 IO demo 代码

3. 信号驱动 IO(Signal Driven IO,SIGIO)

4. IO 多路复用(IO Multiplexing) 

5. 异步 IO(Asynchronous IO,AIO)


        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 结构,无需关注中间过程。 

        


网站公告

今日签到

点亮在社区的每一天
去签到