关于IO模型的一个小问题:
不同文章写的标题可能是: “5种 Linux 网络IO模型” 、“5种 Linux IO模型”
解释是一下,这2个是一个东西。Linux 的内核将所有外部设备都看做一个文件来操作(一切皆文件)。
文件IO、网络IO 只是不同应用场景的叫法(最终都由对应的驱动完成对应的操作),对系统层面来说都是一样的。以下内容主要以 网络IO 为主
先看一下 服务器 处理网络请求流程,如图:

Linux IO 操作分成两个阶段:即等待资源阶段 和 使用资源阶段
以 Soeckt IO 为例,如图:

阻塞/非阻塞:等待IO资源(wait for data)
如:等待网络传输数据可用的过程(数据到达内核中)在第1阶段中数据不可用时 I/O请求 一直阻塞,直到数据返回;
在 网络Socket IO 请求过程,当 Socket 读不到数据时,
会一直阻塞在“等待数据准备”的过程中。
在第1阶段中数据不可用时 I/O请求 立即返回,直到被通知资源可用为止。
在 网络Socket IO 请求过程,当 Socket 读不到数据时,立马返回去做别的事;
通过接口循环去“问”一下内核数据是否准备好;
一直询问到 数据到达。
同步/异步:使用IO资源(copy data from kernel to user)
如:从网络上接收到数据,并且拷贝到应用程序的缓冲区里面(把数据从内核复制到用户层)指的是 IO请求 在 读/写动作是阻塞的,直到读取或者写入数据完成。
在 网络IO 中 内核数据 复制 到用户空间的过程是阻塞的。
也就是当前程序的 运行权 由用户空间转移到了内核空间,
相当于当前线程跳到内核里去运行,内核态只是用户态的一个延续。
指的是 I/O请求 在 读/写时立即返回, 读/写动作 由内核来完成;
也就是说,由内核负责数据拷贝到用户提供的缓冲区(用户空间),
最后再通知程序 I/O 请求执行完成。
在 网络IO 中 读写时,是通知的方式告诉内核,
数据的复制完全是由内核来完成,和用户态没有关系。
整个过程可以理解是由2个线程来完成,用户线程通知,内核线程复制数据。
阻塞/非阻塞 & 同步/异步,只是不同视角的不同描述词而已。
在跨模块之间交互时,会从逻辑上,分成“调用方”和“被调用方”。
阻塞/非阻塞是调用方的策略
同步/异步是被调用方的机制
“调用方”选择以阻塞/非阻塞的方式等待“被调用方”的返回结果;
“被调用方”提供同步/异步的方式来告诉“调用方”什么时候 得到结果。
OK,我们先认为上面描述的概念(如果有什么异议,大家留言区讨论)来理解下面 网络IO模型 的描述
Linux下可用的IO模型有5种,分别是:
注:1-4 为什么要加 “同步”,会在最后的时候解释,先看一下介绍
阻塞 IO 模型(Blocking IO)是 linux 默认情况 下所有 IO操作 都是以 阻塞 的方式进行工作的。
如图:

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

如图:

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

IO多路复用 主要是针对非阻塞IO的 改进版;
其特点是通过 管理机制 在一个进程能同时管理等待多个 网络Socket ,
IO多路复用 中有两个非常出名的技术: select / epoll;
这2种技术的好处就在于单个线程可以同时处理多个网络连接的 Socket IO。
(理论上也能同时处理多个 文件描述符(File IO),只是本人没人试过)
如图:

以 select 为例(epoll同理)当 select 管理的 socket描述符,
其中的任意一个进入读 就绪 状态, select 函数就会
返回已经 就绪(上述中 等待资源阶段 已经完成) 的 socket描述符。
select/epoll的优势并不是对于单个连接能处理得更快, 而是在于能处理更多的连接。
select/epoll的优势并不是对于单个连接能处理得更快, 而是在于能处理更多的连接。
select/epoll的优势并不是对于单个连接能处理得更快, 而是在于能处理更多的连接。socket 是“珍珠”,select/epoll 是“线”,
“线”的作用就是把“珍珠”串起来,组织成一串 连续 的任务;
相对于断断续续的单个 socket 任务的处理,select/epoll 本质是批处理思想 的应用。
对应的 时序图:

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

注:
信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,
即这种信号通知意味着到达一个数据报,或者返回一个异步错误。但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失。
对应的 时序图:

相比于前面4种IO模型,异步IO 最大的特点 不是顺序执行;
啥意思?当用户进程发现异步IO调用后,就可以处理别的业务逻辑(一点也不会造成程序阻塞),
不用关心数据什么时候来,也不用关心怎么写到指定的用户内存空间。
怎么听上去感觉和 信号驱动IO 差不多?
不是的,在信号驱动IO中数据到达后,
还是需要我们用阻塞的方式来把数据从内核读到程序内在里。
如图:

异步IO 内核会负责:[阶段1;阶段2];
当完成上面2个步骤后,内核会给进程发送一个信号,
告诉进程内核已经完成所有的工作,进程可以使用对应的网络数据了。
通过上面对概念的解释可以发现,在5种IO模型中 阻塞逐渐减少,
只有最后一种 异步IO模型 是完全没有阻塞的 异步IO ,其它4种IO模型都是 同步IO(程序自己负责把数据复制到用户空间),
这也就是为什么在最前面的时候要用 (同步IO) 作为其它4种IO模型的前缀。
如图

其实这里还有一个问题要说明一下:
既然5种IO模型中阻塞越来越少,为什么现在大多数高性能服务却是用的 IO多路复用 模型?
这个问题其实就是对比 [IO多路复用、信号驱动IO、异步IO] 我们一个一个的看。
- 信号驱动IO —— 对于 TCP 通信近乎无用,更适合 UDP 通信;
在处理 UDP 数据包时只存在2种状态:数据到达操作错误。
而对于 TCP 而言,通知的条件为数众多(7种:[完成建立连接请求、发起断开连接请求、完成断开连接请求、有数据到达、有数据写出、半连接关闭、有错误发生]),
每一种情况都需要判别 消耗大量资源,与前3种IO模型相比优势尽失。
最主要是 可靠性太差!信号驱动是使用 队列 来通知任务的,任务过多会造成任务的丢失;并且丢失的任务是不会通知到用户。
- 异步IO —— 在 网络IO 上主要有2大问题:
- Linux的 异步IO接口(AIO) 应用到 socket 操作仍然是按照默认的“阻塞同步”工作方式执行,
并 不是真正的异步(Linux系统功能就没有在 socket 上有异步支持)。- 假设 Linux 系统有完整的支持 socket异步IO,但是还是不能应用到 socket 上,为什么?
因为 不可控制!啥意思,在完整的 异步IO 中,异步IO负责2件事[监听数据到达,把数据给到用户],
也就是这2种事 异步IO 可以决定什么时候/怎么处理数据:可能会马上就处理、也可能等一会儿处理、
处理完了可以选择一会儿通知程序......等等一系列 决策权问题!对于高性能网络服务器来说,这是 绝对不可能 交出去的权力。
注:
信号驱动IO、异步IO这2种IO模型本人也没有在实际的项目和工作中应用过,上面对比给的原因是网上查的资料,再结合个人的理解写出来的,如果有什么不对不好的地方欢迎大家留言讨论。
通过上面的比较,就能看出为什么高性能服务器的网络IO模型只可能是 IO多路复用。
在做网络IO模型决策时,不管选择那一种(软件不管多灵活,都是一个“死物”),
都会因为数据(业务)变化导致,导致其它环节弱点的暴露,
我们能做的只是在所了解的业务中寻找 最大公约数,
找出 “各种代入之间最大的妥协”,即怎么在最小损失的情况下找到最合理的措施。
目前来看只有 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只在 文件系统 完整实现,无法应用网络操作 |
再补一张脑图:

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