5种Linux 网络IO模型
446
2022.12.15
2022.12.15
发布于 未知归属地

关于IO模型的一个小问题:
不同文章写的标题可能是: “5种 Linux 网络IO模型” 、“5种 Linux IO模型”
解释是一下,这2个是一个东西。

Linux 的内核将所有外部设备都看做一个文件来操作(一切皆文件)。
文件IO、网络IO 只是不同应用场景的叫法(最终都由对应的驱动完成对应的操作),对系统层面来说都是一样的。

以下内容主要以 网络IO 为主


先看一下 服务器 处理网络请求流程,如图:
image.png

0 概念讲解(阻塞/非阻塞 & 同步/异步)

Linux IO 操作分成两个阶段:即等待资源阶段使用资源阶段

以 Soeckt IO 为例,如图:
image.png

  • 当用户进程请求 网络I/O 操作时,该用户进程会执行一个系统调用,将本进程的控制权移交给内核
  • 内核会先进行检查,如果进程所需的数据不在内核空间,则进程会被挂起,等待数据到达(阶段1);内核数据状态好后,内核把数据传送到用户空间内的指定缓冲区(阶段2)。

0.1 等待资源阶段(阶段1)

涉及的概念
阻塞/非阻塞:等待IO资源(wait for data)

如:等待网络传输数据可用的过程(数据到达内核中)

0.1.1 阻塞

在第1阶段中数据不可用时 I/O请求 一直阻塞,直到数据返回;

在 网络Socket IO 请求过程,当 Socket 读不到数据时,
会一直阻塞在“等待数据准备”的过程中。

0.1.2 非阻塞

在第1阶段中数据不可用时 I/O请求 立即返回,直到被通知资源可用为止。

在 网络Socket IO 请求过程,当 Socket 读不到数据时,立马返回去做别的事;
通过接口循环去“问”一下内核数据是否准备好;
一直询问到 数据到达

0.2 使用资源阶段(阶段2)

涉及的概念
同步/异步:使用IO资源(copy data from kernel to user)

如:从网络上接收到数据,并且拷贝到应用程序的缓冲区里面(把数据从内核复制到用户层)

0.2.1 同步。

指的是 IO请求读/写动作是阻塞的,直到读取或者写入数据完成。

在 网络IO 中 内核数据 复制 到用户空间的过程是阻塞的。
也就是当前程序的 运行权 由用户空间转移到了内核空间,
相当于当前线程跳到内核里去运行,内核态只是用户态的一个延续

0.2.2 异步

指的是 I/O请求读/写时立即返回, 读/写动作 由内核来完成
也就是说,由内核负责数据拷贝到用户提供的缓冲区(用户空间),
最后再通知程序 I/O 请求执行完成。

在 网络IO 中 读写时,是通知的方式告诉内核,
数据的复制完全是由内核来完成,和用户态没有关系。
整个过程可以理解是由2个线程来完成,用户线程通知,内核线程复制数据。

小结

阻塞/非阻塞 & 同步/异步,只是不同视角的不同描述词而已。

在跨模块之间交互时,会从逻辑上,分成“调用方”和“被调用方”。

阻塞/非阻塞是调用方策略
同步/异步是被调用方机制

“调用方”选择以阻塞/非阻塞的方式等待“被调用方”的返回结果;
“被调用方”提供同步/异步的方式来告诉“调用方”什么时候 得到结果。

OK,我们先认为上面描述的概念(如果有什么异议,大家留言区讨论)来理解下面 网络IO模型 的描述


Linux下可用的IO模型有5种,分别是:

  1. (同步)阻塞IO
  2. (同步)非阻塞IO
  3. (同步)IO多路复用
  4. (同步)信号驱动IO
  5. 异步IO

注:1-4 为什么要加 “同步”,会在最后的时候解释,先看一下介绍

1 (同步)阻塞IO 模型

阻塞 IO 模型(Blocking IO)是 linux 默认情况 下所有 IO操作 都是以 阻塞 的方式进行工作的。

如图:
image.png

在用户进程中发起 操作,其系统调用直到 数据包到达 并且 复制到应用进程的缓冲区中发生错误 时才返回。
在此期间一直会等待,进程从调用 读 开始到它返回的整段时间都是被阻塞的
即 读 的调用会被阻塞。

对应的 时序图
image.png

2 (同步)非阻塞IO 模型

如图:
image.png

用户进程发出 操作时,如果 内核 中的数据还没有准备好,
此时 读 操作不会把 用户进程 卡住(非阻塞),而是立刻返回一个error;
用户进程需要不断的 主动询问 内核 数据好了没有。

对应的 时序图
image.png

3 (同步)IO多路复用

IO多路复用 主要是针对非阻塞IO的 改进版
其特点是通过 管理机制 在一个进程能同时管理等待多个 网络Socket

IO多路复用 中有两个非常出名的技术: select / epoll
这2种技术的好处就在于单个线程可以同时处理多个网络连接的 Socket IO
(理论上也能同时处理多个 文件描述符(File IO),只是本人没人试过)

如图:
image.png

以 select 为例(epoll同理)当 select 管理的 socket描述符,
其中的任意一个进入读 就绪 状态, select 函数就会
返回已经 就绪(上述中 等待资源阶段 已经完成) 的 socket描述符。

         

select/epoll的优势并不是对于单个连接能处理得更快, 而是在于能处理更多的连接。
select/epoll的优势并不是对于单个连接能处理得更快, 而是在于能处理更多的连接。
select/epoll的优势并不是对于单个连接能处理得更快, 而是在于能处理更多的连接。

socket 是“珍珠”,select/epoll 是“线”,
“线”的作用就是把“珍珠”串起来,组织成一串 连续 的任务;
相对于断断续续的单个 socket 任务的处理,select/epoll 本质是批处理思想 的应用。

对应的 时序图
image.png

4(同步)信号驱动IO

在信号驱动式 I/O 模型中,用户进程通过注册一个 信号处理函数
可以在处理网络数据时 立即返回(不会造成程序阻塞),
并继续原来的非网络IO相关业务;
数据准备好(内核负责)时,
内核就会为用户进程生成一个 SIGIO 信号,进程会收到该信号,
通过信号回调通知应用程序调用recvfrom来读取数据。示意图如下:

image.png

注:
信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,
即这种信号通知意味着到达一个数据报,或者返回一个异步错误。

但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失。

对应的 时序图
image.png

5 异步IO

相比于前面4种IO模型,异步IO 最大的特点 不是顺序执行
啥意思?当用户进程发现异步IO调用后,就可以处理别的业务逻辑(一点也不会造成程序阻塞),
不用关心数据什么时候来,也不用关心怎么写到指定的用户内存空间。

怎么听上去感觉和 信号驱动IO 差不多?
不是的,在信号驱动IO中数据到达后,
还是需要我们用阻塞的方式来把数据从内核读到程序内在里。

如图:
image.png

异步IO 内核会负责:[阶段1;阶段2]
当完成上面2个步骤后,内核会给进程发送一个信号
告诉进程内核已经完成所有的工作,进程可以使用对应的网络数据了。


小结: IO模型对比

通过上面对概念的解释可以发现,在5种IO模型中 阻塞逐渐减少
只有最后一种 异步IO模型 是完全没有阻塞的 异步IO ,其它4种IO模型都是 同步IO(程序自己负责把数据复制到用户空间),
这也就是为什么在最前面的时候要用 (同步IO) 作为其它4种IO模型的前缀。

如图
image.png

其实这里还有一个问题要说明一下:

既然5种IO模型中阻塞越来越少,为什么现在大多数高性能服务却是用的 IO多路复用 模型?

这个问题其实就是对比 [IO多路复用、信号驱动IO、异步IO] 我们一个一个的看。

  • 信号驱动IO —— 对于 TCP 通信近乎无用,更适合 UDP 通信;
    在处理 UDP 数据包时只存在2种状态:数据到达 操作错误
    而对于 TCP 而言,通知的条件为数众多(7种:[完成建立连接请求、发起断开连接请求、完成断开连接请求、有数据到达、有数据写出、半连接关闭、有错误发生]),
    每一种情况都需要判别 消耗大量资源,与前3种IO模型相比优势尽失。
    最主要是 可靠性太差!信号驱动是使用 队列 来通知任务的,任务过多会造成任务的丢失;并且丢失的任务是不会通知到用户。

  • 异步IO —— 在 网络IO 上主要有2大问题:
    1. Linux的 异步IO接口(AIO) 应用到 socket 操作仍然是按照默认的“阻塞同步”工作方式执行,
      不是真正的异步(Linux系统功能就没有在 socket 上有异步支持)。
    2. 假设 Linux 系统有完整的支持 socket异步IO,但是还是不能应用到 socket 上,为什么?
      因为 不可控制!啥意思,在完整的 异步IO 中,异步IO负责2件事[监听数据到达把数据给到用户],
      也就是这2种事 异步IO 可以决定什么时候/怎么处理数据:可能会马上就处理、也可能等一会儿处理、
      处理完了可以选择一会儿通知程序......等等一系列 决策权问题!对于高性能网络服务器来说,这是 绝对不可能 交出去的权力。

注:信号驱动IO异步IO 这2种IO模型本人也没有在实际的项目和工作中应用过,上面对比给的原因是网上查的资料,再结合个人的理解写出来的,如果有什么不对不好的地方欢迎大家留言讨论。

权衡后的全局最优解

通过上面的比较,就能看出为什么高性能服务器的网络IO模型只可能是 IO多路复用

在做网络IO模型决策时,不管选择那一种(软件不管多灵活,都是一个“死物”),
都会因为数据(业务)变化导致,导致其它环节弱点的暴露,
我们能做的只是在所了解的业务中寻找 最大公约数
找出 “各种代入之间最大的妥协”,即怎么在最小损失的情况下找到最合理的措施。

目前来看只有 IO多路复用 是5种IO模型中的 最优解


最后再对比一下5种IO模型的 优/缺点

优点缺点
阻塞IO程序逻辑简单,在阻塞等待数据期间进程/线程挂起,基本不会占用 CPU 资源每个连接需要独立的进程/线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销较大,这种模型在实际应用中很少使用
非阻塞IO不会阻塞在内核的等待数据过程,每次发起的 IO请求 可以立即返回,不用阻塞等待,实时性较好轮询将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率比较低,所以一般高性能服务器不使用这种 IO模型
IO多路复用可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程),这样可以大大节省系统资源高性能服务器主要使用的网络IO模型当连接数较少时效率 相比于 [多线程+阻塞IO模型] 效率较低,可能延迟更大;因为 IO多路复用 本身的 管理成本 即使在单个连接的情况下也是存在的(占用时间会有增加)
信号驱动IO线程并没有在等待数据时被阻塞,可以提高资源的利用率信号IO 在大量 IO操作 时可能会因为信号队列溢出导致没法通知,导致任务丢失
异步IO能够充分利用 DMA(Direct Memory Access) 特性,让 IO操作 与计算重叠要实现真正的异步IO,操作系统需要做大量的工作,目前 Linux 下的异步IO只在 文件系统 完整实现,无法应用网络操作

再补一张脑图:
image.png

其它:
后续我会补上关于 IO多路复用 相关的编程代码,当份工具手册来查阅。


参考:


我是疯子,感谢阅读

评论 (0)