Unix 5 种 IO 模型概述
程序员文章站
2022-07-12 16:48:47
...
Unix 下有 5 种可用的 I/O 模型:阻塞式 I/O、非阻塞式 I/O、I/O 复用(select 和 poll)、信号驱动式 I/O(SIGIO)和 异步 I/O(POSIX 的 aio_ 系列函数)。其中的每种模型细节会在后面陆续补上,本节只对它们进行大概的描述。
一个输入操作通常包括两个不同的阶段:
(1)等待数据准备好;
(2)从内核向进程复制数据。对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待的分组到达时,它就被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
阻塞式 I/O 模型是最流行的 I/O 模型。所有套接字默认都是阻塞的。为了简单,以数据报套接字为例(下同),阻塞式 I/O 模型上的输入操作情形如下图所示。
进程调用 recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误是系统调用被信号中断。我们说进程在从调用 recvfrom 开始到它返回的整段时间内是被阻塞的。recvfrom 成功返回后,应用进程才开始处理数据报。
在非阻塞式 I/O 模型中,进程把一个套接字设置成非阻塞是在通知内核:当所请求的 I/O 操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。其上的输入操作情形如下图。
图中前三次调用 recvfrom 时没有数据可返回,因此内核转而立即返回一个 EWOULDBLOCK 错误。应用进程像这样持续轮询内核以查看某个操作是否就绪,往往会耗费大量 CPU 时间,不过这种模型偶尔也会遇到,通常是在专门提供某一种功能的系统中才有。
有了 I/O 复用(I/O multiplexing),就可以调用 select 或 poll 函数,阻塞在这两个系统调用的某一个之上,而不是阻塞在真正的 I/O 系统调用上。
这里阻塞于 select 调用,当 select 返回套接字可读这一条件时,就调用 recvfrom 把所读的数据报复制到应用进程缓冲区。相比较于阻塞式 I/O,由于使用 select 需要两个系统调用,似乎 I/O 复用还稍有劣势。不过使用 select 的优势在于可以等待多个描述符就绪。与该模型极为相似的另一种 I/O 模型是在多线程中使用阻塞式 I/O,但它没有使用 select 阻塞在多个文件描述符上,而是使用多个线程(每个文件描述符一个线程),这样每个线程都可以*地调用诸如 recvfrom 之类的阻塞式 I/O 系统调用了。
信号驱动式 I/O 就是让内核在描述符就绪时发送 SIGIO 信号通知我们。
首先开启套接字的信号驱动式 I/O 功能,并通过 sigaction 系统调用安装一个信号处理函数,然后系统调用立即返回,进程继续工作。当数据报准备好读取时,内核就为该进程产生一个 SIGIO 信号,随后就可在信号处理函数中调用 recvfrom 读取数据报,并通知主循环数据已准备好待处理,或者立即通知主循环,让它读取数据报。
异步 I/O 模型中的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到缓冲区)完成后再通知我们。这种模型与信号驱动模型的主要区别在于:信号驱动是由内核通知我们何时可以启动一个 I/O 操作,而异步 I/O 模型是由内核通知我们 I/O 操作何时完成。
这里调用 aio_read 函数(POSIX 异步 I/O 函数以 aio_ 或 lio_ 开头)给内核传递描述符、缓冲区指针、缓冲区大小(与 read 相同的三个参数)和文件偏移,并告诉内核当整个操作完成时如何通知我们。该系统调用会立即返回,而且进程在等待 I/O 完成期间不会被阻塞。
这 5 种 I/O 模型的区别如下图所示。
同步 I/O 和异步 I/O 这两个术语在 POSIX 中的定义如下:
* 同步 I/O 操作导致请求进程阻塞,直到 I/O 操作完成。
* 异步 I/O 操作不导致请求进程阻塞。
根据该定义,可见前 4 种 I/O 模型都是同步 I/O 模型,因为其中真正的 I/O 操作会阻塞进程,而只有异步 I/O 模型与 POSIX 定义的异步 I/O 相匹配。
一个输入操作通常包括两个不同的阶段:
(1)等待数据准备好;
(2)从内核向进程复制数据。对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待的分组到达时,它就被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
阻塞式 I/O 模型是最流行的 I/O 模型。所有套接字默认都是阻塞的。为了简单,以数据报套接字为例(下同),阻塞式 I/O 模型上的输入操作情形如下图所示。
进程调用 recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误是系统调用被信号中断。我们说进程在从调用 recvfrom 开始到它返回的整段时间内是被阻塞的。recvfrom 成功返回后,应用进程才开始处理数据报。
在非阻塞式 I/O 模型中,进程把一个套接字设置成非阻塞是在通知内核:当所请求的 I/O 操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。其上的输入操作情形如下图。
图中前三次调用 recvfrom 时没有数据可返回,因此内核转而立即返回一个 EWOULDBLOCK 错误。应用进程像这样持续轮询内核以查看某个操作是否就绪,往往会耗费大量 CPU 时间,不过这种模型偶尔也会遇到,通常是在专门提供某一种功能的系统中才有。
有了 I/O 复用(I/O multiplexing),就可以调用 select 或 poll 函数,阻塞在这两个系统调用的某一个之上,而不是阻塞在真正的 I/O 系统调用上。
这里阻塞于 select 调用,当 select 返回套接字可读这一条件时,就调用 recvfrom 把所读的数据报复制到应用进程缓冲区。相比较于阻塞式 I/O,由于使用 select 需要两个系统调用,似乎 I/O 复用还稍有劣势。不过使用 select 的优势在于可以等待多个描述符就绪。与该模型极为相似的另一种 I/O 模型是在多线程中使用阻塞式 I/O,但它没有使用 select 阻塞在多个文件描述符上,而是使用多个线程(每个文件描述符一个线程),这样每个线程都可以*地调用诸如 recvfrom 之类的阻塞式 I/O 系统调用了。
信号驱动式 I/O 就是让内核在描述符就绪时发送 SIGIO 信号通知我们。
首先开启套接字的信号驱动式 I/O 功能,并通过 sigaction 系统调用安装一个信号处理函数,然后系统调用立即返回,进程继续工作。当数据报准备好读取时,内核就为该进程产生一个 SIGIO 信号,随后就可在信号处理函数中调用 recvfrom 读取数据报,并通知主循环数据已准备好待处理,或者立即通知主循环,让它读取数据报。
异步 I/O 模型中的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到缓冲区)完成后再通知我们。这种模型与信号驱动模型的主要区别在于:信号驱动是由内核通知我们何时可以启动一个 I/O 操作,而异步 I/O 模型是由内核通知我们 I/O 操作何时完成。
这里调用 aio_read 函数(POSIX 异步 I/O 函数以 aio_ 或 lio_ 开头)给内核传递描述符、缓冲区指针、缓冲区大小(与 read 相同的三个参数)和文件偏移,并告诉内核当整个操作完成时如何通知我们。该系统调用会立即返回,而且进程在等待 I/O 完成期间不会被阻塞。
这 5 种 I/O 模型的区别如下图所示。
同步 I/O 和异步 I/O 这两个术语在 POSIX 中的定义如下:
* 同步 I/O 操作导致请求进程阻塞,直到 I/O 操作完成。
* 异步 I/O 操作不导致请求进程阻塞。
根据该定义,可见前 4 种 I/O 模型都是同步 I/O 模型,因为其中真正的 I/O 操作会阻塞进程,而只有异步 I/O 模型与 POSIX 定义的异步 I/O 相匹配。
上一篇: 获取和设置套接字选项
下一篇: 主机名与 IP 地址的转换(续)