9.Linux网络编程中的五种I/O模型讲解

IO 操作分为两步: 发起 IO 请求等待数据准备, 实际 IO 操作.

例如:

  1. 操作系统的一次写操作分为两步: 将数据从用户空间拷贝到系统空间; 从系统空间往网卡写.
  2. 一次读操作分为两步: 将数据从网卡拷贝到系统空间; 将数据从系统空间拷贝到用户空间.

在这里插入图片描述
当调用 IO 函数 (recv()/recvfrom()) 时, 首先系统先检查是否准备好数据. 如果数据没有准备好, 那么系统就处于等待状态. 当数据准备好后, 将数据从系统缓冲区复制到用户空间, 然后该函数返回.

例如 socket 服务端读取客户端发送的数据时, 应用程序调用 recv() 函数, 这时系统就会判断有没有准备好数据.

当数据准备好后, 将数据从内核拷贝到用户空间, 拷贝完成后函数返回.

等待数据 和 拷贝数据这段时间都是阻塞的.

非阻塞 IO

在这里插入图片描述
非阻塞 IO 通过进程反复调用 IO 函数, 数据没有准备好也会立即返回(返回一个错误标识). 然后不断调用这个 IO 函数, 直到数据准备好为止.

和 阻塞IO 一样应用程序也会调用 recv() 函数(轮询), 不同的是数据没准备好会返回一个错误标识, 这时我们可以利用这个线程来做一些别的业务逻辑, 直到返回成功标识为止.

代码示例:

// 设置非阻塞
int flags = fcntl(socket, F_GETFL, 0);
fcntl(socket, F_SETFL, flags | O_NONBLOCK);
// 等待读入数据
while (bRun) {
   char pbuf[1024];
   len = sizeof(pbuf);
   // rLen返回的是接收到的数据长度,如果<0,则出错,=0则表示正常关闭(graceful shutdown)
   // 这里recv不会阻塞线程执行,如果没有数据,也将直接返回
   int rLen = recv(socket, pbuf, len, 0);
   if ( rLen == 0 ) {
       // 连接正常关闭
       printf("sock shutdown gracefully, sock=%d", socket);
       break;
    } else if (rLen < 0) {
       // 由于是非阻塞,所以需要判断是否是正常的调用返回
       // EAGAIN 和 EWOULDBLOCK 表示连接状态正常, 只是没有数据到来而立即返回
       if ((errno == EAGAIN || errno == EWOULDBLOCK)) {
           //这里可以处理日常事件,如一些计算逻辑、统计等(1)
           continue;
       } else {
           printf("error recv data: %s(errno: %d)\n", strerror(errno),errno);
           close(socket);
           break;
       }
    }
   //接收到数据,进行处理
   printf("recv data len=%d, data=%s", rLen, pbuf);
}

阻塞IO 和 非阻塞IO 都是使用 recv 函数进行读取, 只不过 // 设置非阻塞 设置为 非阻塞IO.

值得注意的是:
阻塞 IO 模型图 和 非阻塞 IO 模型图, 中可以看出都有两个阶段, 分别是 等待数据 和 数据从内核拷贝到用户空间.
而判断阻塞还是非阻塞是从 等待数据 这个阶段进行判断的, 阻塞IO 当调用 recv() 函数时会一直等待数据准备完成, 所以它是阻塞的;
而非阻塞IO 当调用 recv() 函数时如果数据没有准备好那么就会返回一个错误标识, 所以它是非阻塞的.
简单的理解就是 recv() 函数能不能立即返回; 如果数据没有准备好, 当调用 recv() 函数时立即返回一个信息则就是非阻塞的.
第二阶段 数据从内核拷贝到用户空间 都是一样的, 而且这一步是阻塞的. 对于这两个 IO 模型来说, 没必要作过多关注.

多路复用 IO

在这里插入图片描述
从流程上来看, 使用 select 函数进行 IO 请求和同步阻塞模型没有太大的区别, 甚至还多了添加监视 socket, 以及调用 select 函数的额外操作, 效率更差.

但是, 使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 IO 请求. 也就是说多路表示多个 Socket 链接, 复用表示复用一个或多个线程.

例如 可以注册多个 socket, 然后不断地调用 select 读取被激活(读就绪或写就绪)的 socket, 就可以达到在同一个线程内同时处理多个 IO 请求的目的. 而在同步阻塞模型中, 必须通过多线程的方式才能达到这个目的.

虽然 IO 多路复用方式允许同一个线程内处理多个 IO 请求, 但是每个 IO 请求的过程还是阻塞的 (在 select 函数上阻塞), 平均时间甚至比同步阻塞 IO 模型还要长.

select 函数关键调用流程如下: selectcore_sys_select()do_select(). 而 select 函数阻塞的原因是因为在 do_select() 函数里面有一个死循环 for (;;);

退出条件有以下三种:

  • 有就绪的文件描述符.
  • 超时.
  • 中断.

多路 IO 复用模型我个人认为它是 同步阻塞IO模型. 这里的所说的阻塞是指 select 函数执行时线程被阻塞, 而不是指 socket. 一般在使用IO多路复用模型时, socket 都是设置为 NONBLOCK 的.

信号驱动 IO

在这里插入图片描述
首先需要开启 socket 的信号驱动式IO功能, 然后通过 sigaction 系统调用注册 SIGIO 信号处理函数 —— 该系统调用会立即返回.

当数据准备好时, 内核会为该进程产生一个 SIGIO 信号, 这时就可以在信号处理函数中调用 recvfrom 读取数据了.

所以, 信号驱动式 IO 的特点就是在等待数据 ready 期间进程不被阻塞, 当收到信号通知时再阻塞并拷贝数据.

简单理解就是, 数据准备好后回调一个函数, 在函数中获取数据.

异步 IO

在这里插入图片描述
用户进程在发起 aio_read 操作后, 该系统调用立即返回 —— 然后内核会自己等待数据 ready, 并自动将数据拷贝到用户内存. 整个过程完成以后, 内核会给用户进程发送一个信号, 通知IO操作已完成.

异步IO与信号驱动式IO的主要区别是: 信号驱动式 IO 是由内核通知我们何时启动一个 IO 操作, 而异步 IO 是由内核通知我们 IO 操作何时完成.

所以, 异步 IO 的特点是 IO 执行的两个阶段都由内核去完成, 用户进程无需干预, 也不会被阻塞.

五种 IO 模型的比较

在这里插入图片描述
根据上述 5 种IO模型, 前 4 种模型-阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步 I/O 模型, 因为其中真正的 I/O 操作(recvfrom) 将阻塞进程, 在内核数据 copy 到用户空间时都是阻塞的.

总结

阻塞IO 和 非阻塞IO 的区别在于第一步, 发起 IO 请求是否会被阻塞, 如果阻塞直到完成那么就是传统的阻塞IO, 如果不阻塞, 那么就是非阻塞IO.

同步IO 和 异步IO 的区别就在于第二个步骤是否阻塞, 如果实际的 IO 读写阻塞请求进程, 那么就是 同步IO, 因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO, 如果不阻塞, 而是操作系统做完 IO 两个阶段的操作再将结果返回, 那么就是异步IO.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值