之前在服务器进程终止中讨论的情形, TCP 客户端同时要处理两个输入, 一是标准输入, 二是 TCP 套接口. 而此时若是服务器进程被杀死, 服务器尽管正确地给客户发送了 FIN 分节, 但是由于此时客户正阻塞于标准输入 fgets(), 直到读完一行用户输入(也许此时 TCP 服务器已经死透了), 才能看到那个文件结束符.
I/O 复用
因此, 我们需要一个能力, 对于上面两个 I/O, 只要有一个或多个 I/O 条件满足, 都应该正确地通知到, 这个能力被称为 I/O 复用, 由函数 select 和 poll 支持.
I/O 复用的典型应用场景
针对网络应用场景, 有以下情形,
上面讨论过的客户同时处理多个描述字时(一般是交互式输入和网络套接口), 此时必须使用 I/O 复用.
一个 TCP 客户同时处理多个套接口.
一个 TCP 服务器同时处理监听套接口和已连接套接口.
一个服务器同时处理 TCP 和 UDP.
一个服务器同时处理多个服务或者多个协议.
I/O 复用并非只限于网络编程, 许多其他应用也大范围使用这个能力.
I/O 模型概述
首先来看一下 Unix 下我们可用的五个 I/O 模型:
阻塞 I/O
非阻塞 I/O
I/O 复用
信号驱动 I/O(SIGIO)
异步 I/O
一个输入操作一般有两个不同的阶段:
等待数据准备好.
从内核到进程拷贝数据.
对于套接口上的输入操作, 上述步骤具体是:
等待数据到达网络, 分组到达时, 拷贝其到内核缓冲区.
将数据从内核缓冲区拷贝至应用缓冲区.
阻塞 I/O 模型
缺省时, 所有套接口都是阻塞的.《Unix 网络编程》一书中, 前五章的所有例子都使用阻塞 I/O 模型.
为了阐述简单, 这里以 UDP 套接口作为例子, 将函数 recvfrom 视为系统调用, 它会有从应用进程中运行和内核中运行的相互切换.
阻塞 I/O 模型
进程调用 recvfrom, 此系统调用直到数据报到达且拷贝到应用缓冲区或是出错才返回. 此过程可能出现的错误是系统调用被信号中断. 我们说的进程阻塞, 指的是从进程调用 recvfrom 开始到它返回的这段时间, 当进程返回成功提示时, 应用进程开始处理数据报.
非阻塞 I/O 模型
如果一个套接口被设置成非阻塞模式时, 上面说的数据报没有准备好时, 进程不会睡眠, 而是由内核返回一个错误.
仍讨论之前说的 UDP 数据报例子:
数据报没有准备好的时候, 调用函数 recvfrom, 内核立即返回一个 EWOULDBLOCK 错误.
数据报准备好的时候, 调用函数 recvfrom, 数据报从内核拷贝到应用缓冲区, recvfrom 返回成功提示, 进程紧接着会处理数据.
轮询
当一个应用进程对一个非阻塞描述字巡回调用 recvfrom 时, 我们称此过程为轮询(polling). 应用进程连续不断地查询内核, 数据是否准备好, 这对 CPU 时间是极大的浪费.
非阻塞 I/O 模型
I/O 复用模型
在 I/O 复用模型下, 我们不再阻塞于真正的 I/O 系统调用 recvfrom, 而是在 select 和 poll 这两个系统调用之一阻塞.
例如, 阻塞于 select 调用, 等待数据报套接口 (可以是多个中任意一个) 可读, 函数返回对应标识, 此时便可调用 recvfrom 将数据报拷贝到应用缓冲区中.
I/O 复用模型
尽管多了一次系统调用, 但是 select 函数可以等待多个套接口描述字这一点, 是使用 I/O 复用模型的一大理由.
信号驱动 I/O 模型
之前讨论过, 内核可以产生信号, 进程可以调用 sigaction 安装一个信号处理函数. 这个模型的思路是, 数据报准备号以后发送一个信号给进程, 信号处理函数收到信号后, 调用 recvfrom, 进行后续的拷贝和处理.
这一模型的好处是, 任意一个系统调用都是非阻塞的, 主循环可以继续进行, 直到数据报准备好的信号到达.
信号驱动 I/O 模型
异步 I/O 模型
异步 I/O 模型中, 不再调用函数 recvfrom, 而是调用函数 aio_read, 给内核传递描述字, 缓冲区指针, 缓冲区大小, 文件偏移, 并告诉内核当整个操作完成时如何通知. 此调用立即返回, 进程不必阻塞等待 I/O 操作完成.
这里内核在操作完成时也会产生一个信号, 与信号驱动 I/O 模型不同, 这个信号要在数据由内核拷贝到应用缓冲区才产生.
异步 I/O 模型
五种 I/O 模型的比较
除了真正的异步 I/O 模型以外, 其他几种模型, 最后一阶段的处理都是相同的 -- 阻塞于 recvfrom 调用, 将数据从内核拷贝到应用缓冲区.
五个 I/O 模型的比较
来源: https://www.qcloud.com/developer/article/1367571