说在前面
- 环境:windows10
- 参考: UNIX网络编程
- 目录:这里
- 吐槽:爷青回
问题引出
- 在之前的例子中有提到这种情况:
当客户端
在处理两个输入(标准输入
、TCP套接字
)时,若客户端
阻塞在等待输入而服务器进程
终止,那么客户端
将无法读取到TCP套接字
返回的EOF
。见如下代码,// 客户端程序 void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while(Fgets(sendline, MAXLINE, fp) != NULL) // 等待用户输入时,服务器进程终止 { Writen(sockfd, sendline, strlen(sendline)); if(Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } }
I/O复用
便是为了处理类似上述的情况而提出的。这使得内核
能够在发现一个或多个I/O
就绪时通知对应的进程。
I/O模型
阻塞式I/O模型
- 在该模型下,所有
I/O
(包括套接字
)都是阻塞的。以UDP套接字为例,过程如下:
- 上述例子是以UDP为例,这是因为对于UDP来说,数据是否准备好的概念比较简单:数据报的接收状态只有两种,是或者否。但是在TCP中,诸如套接字低水平标记(low-water mark,详见这里)等额外变量的存在,使得数据准备完成这个概念变得复杂。
- 这里,我们将
recvfrom
函数视作系统调用,并将整个过程划分成两个部分,应用进程(用户态)以及内核。不论这个系统调用是如何实现的,它一般都会从应用进程空间中切换到内核空间,之后再切换过来(详见操作系统原理)。 - 上述例子中,进程调用
recvfrom
,其系统调用直到数据报到达并且讲数据从内核复制到应用进程的缓冲区中或发生错误才会返回。最常见的错误是系统调用被信号中断,例如这里。 - 从上图应用进程的角度来看,进程在从调用
recvfrom
开始到它返回的整段时间内(黄色部分)是阻塞的。
非阻塞式I/O模型
- 进程将一个套接字设置成非阻塞是在通知内核:当进程所请求的I/O操作不得不将本进程投入睡眠才能完成时,不要将进程投入睡眠,而是直接返回一个错误。
- 如上图所示,前两次
recvfrom
无数据可返回,因此内核立即返回一个EWOULDBLOCK
错误。在下一次调用时,内核发现有数据报准备完成,便开始将数据复制到应用进程缓冲区,之后recvfrom
返回成功。 - 当一个应用进程像上述过程那样对一个非阻塞描述符循环调用某个操作时,我们称之为轮询(polling)。应用进程会持续轮询内核,以此来确定某个操作是否就绪。但是这种方式通常会耗费大量的CPU时间。
I/O复用模型
- 使用
I/O复用
,应用进程就可以调用select
或者poll
,阻塞在这两个系统调用之一,而不是阻塞在真正的I/O系统调用上。
- 应用进程阻塞于
select
调用,等待数据报套接字可用。当select
返回套接字可读后,应用进程调用recvfrom
把数据报复制到应用进程缓冲区(实际上是内核完成)。 - 对比阻塞调用,I/O复用并不显得有什么优势,并且由于使用
select
,我们需要用到两个系统调用,I/O复用还稍微劣势一些。但是,使用select
的优势在于可以等待多个描述符就绪。
信号驱动式I/O模型
- 使用信号,可以让内核在描述符就绪的时候通知应用进程,这种模型被称为
信号驱动式I/O
。
- 首先开启套接字的信号驱动式I/O功能(后续),并使用
sigaction
系统调用安装一个信号处理函数。该系统调用会立即返回,这样应用进程可以继续运行,而不会阻塞。当数据报准备好后,内核会为进程产生一个SIGIO
信号。应用进程会捕获到该信号。这样,我们既可以在信号处理函数中调用recvfrom
读取数据报,并通知主循环数据已经准备好;也可以立即通知主循环,让其读取数据报。 - 该模型的优势在于等待数据报准备期间,应用进程不会阻塞。
异步I/O模型
-
异步I/O
由POSIX规范定义。演变成当前POSIX规范的各种早期标准所定义的实时函数中存在的差异已经取得一致(早些年的那些函数已经被同化了)。一般来讲,这些函数的工作机制是:通知内核启动某个操作,并且让内核在完成整个操作后(包括内核将数据复制到应用进程缓冲区)通知我们。 -
该模型与信号驱动模型的主要区别在于:信号驱动式I/O是内核告知何时可以启动一个I/O,而异步则是告知一个I/O完成。
-
调用
aio_read
函数(POSIX异步I/O以aio_或lio_开头),给内核传递描述符、缓冲区指针、缓冲区大小(与read
函数相同)和文件偏移(类似lseek
),并告知内核当整个操作完成时如何通知应用进程。 -
该函数会立即返回,这样,应用进程将不会阻塞。
-
上述例子中,假设要求内核在操作完成时会产生某个信号,该信号在数据已经复制到应用进程缓冲区后才产生,不同于信号驱动模型。
对比
对比一
- 前4种模型的区别主要在于第一阶段,他们的第二阶段都是相同的:即应用进程都阻塞在数据从内核复制到调用者的缓冲区。而异步I/O模型在这两个阶段都要处理,从而不同于其他4种模型(异步I/O模型中,应用进程没有阻塞阶段)。
同步、异步I/O对比
- 同步I/O操作导致请求进程阻塞,直到I/O操作完成;(前4种)
- 异步I/O操作不导致请求进程阻塞。