UNIX网络编程 - IO模型
Unix 下可用的 5 种I/O模型:
- 阻塞式 I/O
- 非阻塞式 I/O
- I/O 复用(
select
和poll
) - 信号驱动式 I/O(
SIGIO
) - 异步 I/O(POSIX 的
aio_
系列函数)
一个输入操作通常包括两个不同的阶段:
- 等待数据准备好;
- 从内核向进程复制数据。
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
阻塞式 I/O 模型
最流行的 I/O 模型是阻塞式 I/O(blocking I/O)模型。默认情况下,所有套接字都是阻塞的。
以数据报套接字作为例子:
我们使用 UTP 而不是 TCP 作为例子的原因在于就 UDP 而言,数据准备好读取的概念比较简单:要么整个数据报已经收到,要么还没有。然而对 TCP 来说,诸如套接字低水位标记(low-water mark)等额外变量开始起作用,导致这个概念变得复杂。
我们把 revcfrom
函数 视为系统调用,一般都会从应用进程空间中运行切换到在在内核空间中运行,一段时间之后再切换回来。
进程调用 revcfrom
,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。进程在从调用 recvfrom
开始到它换回的整段时间内是被阻塞的。recvfrom
成功返回后,应用进程开始处理数据报。
非阻塞式 I/O 模型
进程把一个套接字设置成非阻塞是在通知内核:当所请求的 I/O 操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。
前三次调用 recvfrom
时没有数据可返回,因此内核转而立即返回一个 EWOULDBLOCK
错误。第四次调用 recvfrom
时已有一个数据报准备好,它被复制到应用进程缓冲区,于是 recvfrom
成功返回。我们接着处理数据。
当一个应用进程像这样对一个非阻塞描述符循环调用 recvfrom
时,我们称之为 轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪。这么做往往耗费大量 CPU 时间,不过这种模型偶尔也会遇到,通常是在专门提供某一种功能的系统中才有。
I/O 复用模型
有了 I/O 复用(I/O multiplexing),我们就可以调用 select
或 poll
,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正 I/O 系统调用上。
我们阻塞于 select
调用,等待数据报套接字变为可读。当 select
换回套接字可读这一条件时,我们调用 recvfrom
把所读数据报复制到应用进程缓冲区。
比较 阻塞式 I/O 模型和 I/O 复用模型,I/O 复用并不显得有什么优势,事实上由于使用 select
需要连个而不是单个系统调用,I/O 复用还稍有劣势。不过我们稍后看到,使用 select
的优势在于我们可以等待多个描述符就绪。
与 I/O 复用密切相关的另一种 I/O 模型实在多线程中使用阻塞式 I/O。这种模型与上述模型极为相似,但是他没有使用 select
阻塞在多个文件描述附上,而是使用多个线程(每个文件描述符一个线程),这样每个线程都可以自由地调用注入 recvfrom
之类的阻塞式 I/O 系统调用了。
信号驱动式 I/O 模型
我们也可以用信号,让内核在描述符就绪时发送 SIGIO
信号通知我们。我们称这种模型为信号驱动式(signal-driven I/O)。
我们首先开启套接字的信号驱动式 I/O 功能,并通过 sigaction
系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报贮备好读取时,内核就为该进程产生一个 SIGIO
信号。我们随后既可以在信号处理函数中调用 recvfrom
读取数据报,并通知主循环数据已准备好带处理,也可以立即通知主循环,让它读取数据报。
无论如何处理 SIGIO
信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。
异步 I/O 模型
异步 I/O (asynchronous I/O)由 POSIX 规范定义。
一般来说,这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。这种模型与 信号驱动模型 的主要区别在于:信号驱动式 I/O 是由内核通知我们何时可以启动一个 I/O 操作,而异步 I/O 模型是由内核通知我们 I/O 操作何时完成。
我们调用 aio_read
函数(POSIX 异步 I/O 函数以 aio_
或 lio_
开头),给内核传递描述符、缓冲区指针、缓冲区大小(与 read
相同的三个参数)和 文件偏移(与 lseek 类似),并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,而且在等待 I/O 完成期间,我们的进程不被阻塞。
本例子中我们假设要求内核在操作完成时产生某个信号。该信号直到数据已复制到应用进程缓冲区才产生,这一点不同于信号驱动式 I/O 模型。
各种 I/O 模型的比较
前 4 种模型的主要区别在于第一阶段,因为它们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于 recvfrom
调用。相反,异步 I/O 模型在这两个阶段都要处理,从而不同于其他 4 种模型。
同步 I/O 和 异步 I/O 对比
- 同步 I/O 操作(synchronous I/O operation)导致请求进程阻塞,直到 I/O 操作完成。
- 异步 I/O 操作(asynchronous I/O operation) 不导致请求进程阻塞。
根据上述定义,前 4 种模型 - 阻塞式 I/O 模型、非阻塞式 I/O 模型、I/O 复用模型 和 信号驱动式 I/O 模型都是同步 I/O 模型,因为其中真正的 I/O 操作(recvfrom
)讲阻塞进程。只有异步 I/O 模型 与 POSIX 的异步 I/O 相匹配。