《UNIX网络编程卷1:套接字联网API(第3版)》 - 第6章- I/O复用
I/O模型
在介绍select和poll两个函数之前, 整体回顾下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)**模型。 默认情况下,所有套接字都是阻塞的。 下面以UDP套接字作为例子:
本例中,把 recvfrom
函数视为系统调用
。
进程调用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复用并不显得有什么优势,事实上由于使用select需要两个系统调用(select + recvfrom)
,反而更有劣势。
它的优势在于: select可以等待多个描述符
就绪。
信号驱动式I/O模型
我们也可以使用信号, 让内核在描述符就绪时发送SIGIO
信号通知我们。 这种模式称之为信号驱动式I/O(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_开头),给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移(与lseek类似)
,并告知内核当整个操作完成时如何通知我们 - 该
系统调用
立即返回,而且在等待I/O完成期间,我们的进程不被阻塞
。 - 本例,假设要求内核操作在操作完成时产生某个信号,该信号直到
数据已经复制到应用进程缓冲
区才产生。
各I/O模型的比较
可以看出:前4中模型的主要区别在于第一阶段, 因为它们第二阶段是一样的:在数据从内核复制到调用者缓冲区期间, 进程阻塞于recvfrom调用. ---- 我们将前4中模型定义为同步I/O操作(synchronous I/O operation)。
后一种称之为异步I/O操作(asynchronous I/O operation)
select 函数
该函数允许进程指示内核等待多个事件中的任何一个发生
,并只在有一个或多个事件发生或经历一段时间后才唤醒它。
例
timeout
timeout
,它告知内核等待指定描述符中的任何一个就绪,最大的等待时间。
struct timeval {
long tv_sec; // 秒数
long tv_userc; //毫秒数
}
这个参数有三种可能:
- 永远等待下去: 仅在有一个描述符准备好I/O时才返回。
- 等待一段固定时间:在有一个描述符准备好I/O时返回,但是不超过由该参数指向的timeval结构中的描述和好描述。
- 根本不等待:
检查描述符后立即返回
,这称为轮询(polling)
.该参数必须指向一个timeval结构,且其中的值必须为0.
前两种情形的等待通常会被
进程在等待期间补获的信号中断
,并从信号处理函数返回。
中间三个参数:readset, writeset 和 exceptset指定我们要让内核测试读、写和异常
的描述符。
描述符就绪条件
我们一直在讨论某个描述符准备好I/O(读或写)
或是等待其上一个待处理的异常条件
.
我们对于select返回套接字**“就绪”**的条件必须讨论更明确些:
读就绪
满足下列四个条件中任意一个
,套接字准备好读.
写就绪
满足下列四个条件中任意一个
,套接字准备好写.
异常
如果一个套接字存在带外数据或仍处于带歪标记,那么它有异常条件待处理。