【Linux 33】IO 模型

🌈 一、IO 的概念

⭐ 1. IO 是什么

  • I (input) 输入:将数据从输入设备拷贝到内存中;O (output) 输出:将数据从内存中拷贝到输出设备。
  • 对文件的读写操作就是一种 IO,对应的外设是磁盘;对网络的读写操作也是一种 IO,对应的外设是网卡。

image-20241116170803910

⭐ 2. IO 的过程

  • 任何 IO 的过程都分为如下两个步骤:
    1. :等待 IO 条件就绪 (有数据可读 / 有空间可写)。
    2. 拷贝:第一步实现后,将数据拷贝到内存或外设。
  • 在实际的应用场景中,[等] 所消耗的时间通常要 > [拷贝] 所消耗的时间。
  • 因此,想让 IO 变的高效,就得尽量减少 [等] 的时间。

🌈 二、理解各种 IO 类型

⭐ 1. IO 和钓鱼的相似之处

  • 钓鱼的过程分为 [等] 鱼上钩和将鱼从河里 [拷贝] 到鱼桶两个步骤。
  • 在 IO 时,[等] 消耗的时间往往比 [拷贝] 要多,而钓鱼也符合这个特征。
  • 可以通过如何高效的钓鱼,来分析出如何实现高效的 IO。

⭐ 2. 五个人的五种钓鱼方式

  1. 张三:用 1 根鱼竿钓鱼,将鱼钩抛入水后就死等着有鱼咬钩,除此之外什么也不做,有鱼咬钩就将鱼钓上来。张三处于一动不动的状态。
  2. 李四:用 1 根鱼竿钓鱼,将鱼钩抛入水中后就去刷手机,之后定期观察是否中鱼。中鱼了则将鱼钓上来,否则就继续去刷手机。李四处于一直在动的状态。
  3. 王五:用 1 根鱼竿钓鱼,将鱼钩抛入水中后,在鱼竿顶部绑上一个铃铛,然后去刷手机。铃铛响了就把鱼钓上来,否则就不管鱼竿。王五处于监听铃铛的状态。
  4. 赵六:用 100 根鱼竿钓鱼,将这 100 个鱼竿的鱼钩都抛入水中,定期观察这 100 个鱼竿的浮漂,如果某个鱼竿中鱼了就把鱼钓上来。赵六处于一直在跑的状态。
  5. 田七:他只想吃鱼,不想钓鱼,于是雇佣了姜子牙 (可使用上面 4 种中的任意一种钓鱼方式) 来帮他钓鱼。田七为姜子牙准备好钓鱼要用到的所有工具。让姜子牙在鱼桶被装满时通知田七来拿鱼。田七不参与钓鱼,他只负责发起钓鱼

⭐ 3. 五种钓鱼方式的效率分析

  • 张三、李四、王五的效率相同:他们每人都只拿了 1 根鱼竿,有鱼来咬钩时,每根鱼竿的中鱼概率相等。且他们都是先等鱼咬钩之后再钓鱼。
  • 赵六的钓鱼效率最高:他的钓鱼方式减少了单位时间内 [等] 的时间,增加了 [拷贝] 的时间,是一种高效的钓鱼 (IO) 方式。
  • 田七雇佣的姜子牙可以使用前 4 个人的任意一种钓鱼方式。因此,姜子牙的钓鱼效率取决他使用前 4 种钓鱼方式的哪一种。

⭐ 4. 什么是高效 IO

  • 高效 IO 本质上就是减少单位时间内 [等] 的比重

⭐ 5. 如何区分同步 IO 和异步 IO

  • 田七的这种不参与 IO 的过程的做法被称之为异步 IO,而其他四个人参与 IO 的过程的做法的被称为同步 IO。
  • 因此,区分同步 / 异步 IO 的主要方式就是IO 的发起者有没有参与 IO 的过程

🌈 三、五种 IO 模型概述

  • 上述 5 个人各自的钓鱼方式涵盖了所有 IO 的情况,掌握好这 5 种 IO 模型,以后遇到任何 IO 场景就都能理解。
  1. 阻塞 IO:像张三那样一动不动等鱼来的钓鱼方式。
  2. 非阻塞 IO:像李四那样定时检测是否有鱼上钩的钓鱼方式。
  3. 信号驱动 IO:像王五那样通过监听铃铛来得知是否有鱼上钩的钓鱼方式。
  4. 多路转接 IO:像赵六那样一次性等待多个鱼竿上鱼的钓鱼方式。
  5. 异步 IO:田七 + 姜子牙 (发起 IO + 操作系统)。
  • 在钓鱼这个例子中,人是系统调用,鱼竿是套接字文件,湖是系统内部,鱼是数据,浮漂浮动是数据就绪,钓上鱼是发生拷贝。

⭐ 1. 阻塞 IO

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

image-20241116191455917

  • 在调用 recvfrom 函数从某个套接字文件中读取数据时,可能底层数据还未准备好,此时需要等待数据就绪。
  • 当数据就绪后,才能将数据从内核拷贝到用户空间中,最后 recvfrom 函数才能返回。
  • 在调用 recvfrom 函数等待数据就绪期间,在用户看来这个 进程 / 线程 就像是被阻塞住了。

⭐ 2. 非阻塞 IO

  • 即使内核没准备好数据,依然会将系统调用直接返回,并且返回 EWOULDBLOCK 错误码。
  • 非阻塞 IO 通常循环反复的尝试读写文件描述符,这个过程被称之为轮询。这种方式对于 CPU 来说,存在很大的浪费,只有在特定的场景下才用。

image-20241116195437464

  • 阻塞 IO 在 IO 条件没有就绪时,后续检测 IO 条件是否就绪的工作由操作系统发起。
  • 非阻塞 IO 在 IO 条件没有就绪时,后续的检测 IO 条件是否就绪的工作由用户发起。

⭐ 3. 信号驱动 IO

  • 内核会在准备好数据时,发出 SIGIO 信号通知应用程序过来进行 IO 操作。
  • 在底层数据准备好之后,会向当前的 进程 / 线程 发送 SIGIO 信号。因此,可通过 signalsigaction 函数将 SIGIO 信号的处理动作自定义为需要进行 IO 操作,当底层准备好数据时,自动执行对应的 IO 操作。

image-20241116203523260

  • 信号的产生是异步的,但信号驱动 IO 却是同步 IO。这是因为 IO 的发起者只是没有 [等],但却进行了 [拷贝],依然有参与到 IO 的过程中。

⭐ 4. 多路转接 IO

  • 多路转接也被称作多路复用, 这种 IO 方式能够同时等待多个文件描述符的就绪状态

image-20241116204117139

多路转接的思想

  • 由于 IO 的过程分为 [等] 和 [拷贝] 两个步骤,当使用 recvfrom 等接口时,在底层做了两件事:
    1. 第一件事:当 IO 条件未就绪时进行 [等]。
    2. 第二件事:当 IO 条件就绪后进行 [拷贝]。
  • 虽然 recvfrom 等接口也具备 [等] 的能力,但是这些接口一次性只能 [等] 一个文件描述符上的 IO 条件就绪,效率太低。
  • 为了提升 [等] 的效率,系统提供了 selectpollepoll 三种多路转接接口。这些接口的核心工作就是 [等],在 IO 时只需要将 [等] 的工作交给这些接口即可。多路转接的最大意义就是将诸如 recvfrom 这样的接口从 [等] 中解放出来。
  • 多路转接接口可以一次性 [等] 多个文件描述符 (鱼竿),从而将 [等] 的时间重叠。当 IO 条件就绪后,再去调用对应的 recvfrom 等函数 [拷贝] 数据,让这些函数不需要 [等],只需要 [拷贝]。

⭐ 5. 异步 IO

  • 在内核拷贝完数据后,通知应用程序。在这种 IO 模式中,应用程序完全没有参与 IO 的过程。

image-20241116204732068

  • 使用异步 IO 需要调用异步 IO 的接口,将用户空间缓冲区的地址交给 OS,调用完异步 IO 接口后会立即返回。
  • 异步 IO 不需要 IO 发起者进行 [等] 和 [拷贝] 的操作,这两个动作由 OS 完成。当 IO 完成后,OS 就会通知应用程序。
  • 在使用异步 IO 时,可能应用程序已经跑了很远了数据才来,就需要回头处理这些数据。异步 IO 虽然很便捷,但逻辑链很混乱,因此这个不是重点。

🌈 四、高级 IO 重要概念

⭐ 1. 同步通信 VS 异步通信

  • 同步通信:在发出一个系统调用时,在没有得到结果之前,该系统调用函数不会返回。调用函数只要返回,就能获得返回值。由调用者主动等待调用的结果。
  • 异步通信:调用在发出之后立马就返回,没有返回结果。当发出一个异步过程调用后,调用者无法马上得到调用结果;而是在调用发出后,被调用者通过状态、通知 来通知调用者,或通过回调函数处理这个调用。

⭐ 2. 阻塞 VS 非阻塞

  • 阻塞和非阻塞关注的是程序在等待调用结果 (消息、返回值) 时的状态。
  • 阻塞调用:调用结果返回前,会将当前线程挂起,调用线程只有在得到结果之后才会返回。
  • 非阻塞调用:在不能立刻得到结果之前,该调用不会阻塞住当前进程 / 线程。

🌈 五、非阻塞 IO

  • 文件都是默认以阻塞的方式打开。如果想要以非阻塞的方式打开文件,需要在调用 open 函数时,传递 O_NONBLOCKO_NDELAY 参数。

image-20241117191239954

  • 上述设置非阻塞的方法是在打开文件时进行的,如果想将已经打开的文件设置为非阻塞,就需要用到 fcntl 函数。

⭐ 1. fcntl 函数

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

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

1. fcntl 函数参数说明

  • fd:已经打开的文件的文件描述符。
  • cmd:需要进行的操作。
  • ...:可变参数,根据传输的 cmd 值,之后追加的参数也会不同。

2. cmd 参数的可选项及其功能

fcntl 函数的常用功能与功能对应的 cmd 值
复制一个现有的描述符F_DUPFD
获得 / 设置 文件描述符标记F_GETFD 或 F_SETFD
获得 / 设置 文件状态标记F_GETFL 或 F_SETFL
获得 / 设置 异步 IO 的所有权F_GETOWN 或 F_SETOWN
获得 / 设置 记录锁F_GETLK, F_SETLK 或 F_SETLKW

3. fcntl 函数的返回值说明

  • 函数调用成功:返回值取决于具体进行的操作。
  • 函数调用失败:返回 -1,并设置错误码。

⭐ 2. 更改文件描述符为非阻塞

  • 基于 fcntl 实现一个 set_non_block 函数,将文件描述符设置为非阻塞状态。

1. 实现步骤

  1. 调用 fcntl 函数,并将 cmd 的值置为 F_GETFL 来获取文件描述符所对应的文件状态标记 (这是个位图)。
  2. 再次调用 fcntl 函数,并将 cmd 的值置为 F_SETFL 来重新设置文件状态标记 (为文件状态标记添加非阻塞标记 O_NONBLOCK)。

2. 实现代码

// 将文件描述符设置为非阻塞状态
bool set_non_block(int fd)
{
    // 获取文件描述符 fd 所对应的文件状态标记
    int fl = fcntl(fd, F_GETFL);	
    if (fl < 0)
    {
        std::cerr << "fcntl error" << std::endl;
        return false;
    }
    
    // 为获取到的文件状态标记 fl 添加上非阻塞标记
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); 

    return true;
}

⭐ 3. 以非阻塞方式读取标准输入

  • 在调用 read 函数从标准输入文件中读取数据前,可以使用 set_non_block 函数将 0 号文件描述符 (键盘) 设为非阻塞状态。
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>

// 将文件描述符设置为非阻塞状态
bool set_non_block(int fd)
{
    // 获取文件描述符 fd 所对应的文件状态标记
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0)
    {
        std::cerr << "fcntl error" << std::endl;
        return false;
    }

    // 为获取到的文件状态标记 fl 添加上非阻塞标记
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);

    return true;
}

int main()
{
    char buffer[1024];
    set_non_block(0); // 将 0 号文件描述符设为非阻塞撞他

    while (true)
    {
        // 从 0 号文件中读取数据并放入到 buffer 中
        printf("ENTER: ");
        fflush(stdout);
        ssize_t n = ::read(0, buffer, sizeof(buffer) - 1);

        // 打印读取到的内容
        if (n > 0)
        {
            buffer[n] = '\0';
            printf("ECHO: %s\n", buffer);
        }
        else if (0 == n)
        {
            printf("read done\n");
            break;
        }
        else
        {
            perror("read error");
            break;
        }
    }

    return 0;
}
  • 当 read 函数以非阻塞方式从标准输入文件中读取数据时,如果底层数据还没有就绪,read 函数会立刻以出错的形式返回。此时的错误码为 EAGAINEWOULDBLOCK

image-20241117200425556

  • 因此,就需要调整代码,不停调用 read 函数检测底层数据是否就绪。
  • 在 read 函数的返回值是 -1 时,要判断是底层数据不就绪还是真的出错了。
    • 即判断错误码 errno 是否是 EAGAINEWOULDBLOCK,如果错误码不是这两个,则真的出错。
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>

// 将文件描述符设置为非阻塞状态
bool set_non_block(int fd)
{
    // 获取文件描述符 fd 所对应的文件状态标记
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0)
    {
        std::cerr << "fcntl error" << std::endl;
        return false;
    }

    // 为获取到的文件状态标记 fl 添加上非阻塞标记
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);

    return true;
}

int main()
{
    char buffer[1024];
    set_non_block(0); // 将 0 号文件描述符设为非阻塞撞他

    while (true)
    {
        // 从 0 号文件中读取数据并放入到 buffer 中
        printf("ENTER: ");
        fflush(stdout);
        ssize_t n = ::read(0, buffer, sizeof(buffer) - 1);

        // 打印读取到的内容
        if (n > 0)
        {
            buffer[n] = '\0';
            printf("ECHO: %s\n", buffer);
        }
        else if (0 == n)
        {
            printf("read done\n");
            break;
        }
        else
        {
            perror("read error");

            if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                // 底层数据未就绪
                sleep(2);
                continue;
            }
            else
            {
                // 真的出错了
                break;
            }
        }
    }

    return 0;
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值