网络套接字编程之IO模型详解
本文主要参考自《UNIX网络编程》(第1卷)(套接口API第3版)
Unix下可用的五种I/O模型有:
- 阻塞式I/O
- 非阻塞式I/O
- I/O复用(select和poll)
- 信号驱动式I/O (SIGIO)
- 异步I/O (POSIX的aio_系列函数)
一个输入操作通常包括两个阶段:
- 等待数据准备好
- 将数据从内核复制到进程。
对套接字输入而言,第一步通常为等待数据从网络到达,及到达后将数据复制到内核中某个缓存区。而第二部是将数据从内核缓冲区复制到进程缓冲区。
1. 常用I/O模型介绍
由于TCP采用流式传输,套接字低水位标记(low-water mark)开始起作用,涉及的操作相对复杂。而UDP采用数据报传输,数据准备好的概念相对简单,要么整个数据报收到要么还没有。因此下文主要以UDP recvfrom的调用为例。
1.1 阻塞式I/O模型
默认情况下,所有套接字都是阻塞的。recvfrom的调用过程如下图所示:
如图,进程调用recvfrom,当没有数据到达时,recvfrom函数进入阻塞状态,直到有数据报到达或者发生错误才返回。
1.2 非阻塞I/O模型
进程将套接字设置为非阻塞时,会通知内核:当所请求的I/O操作非得把本进程置于休眠状态才才能完成时,不要把本进程置于休眠状态,而是返回一个错误。即,当没有数据到达时,不要阻塞进程,而是直接返回错误。
如图,前三次recvfrom调用时没有数据可返回,因此内核会立刻返回一个EWOULDBLOCK错误。第四次调用时有数据到达,则将数据复制到进程缓存区,成功返回。此外,当使用如图这种轮询非阻塞时,会占用大量的系统资源、
1.3 I/O复用模型
Unix I/O复用主要基于select或者poll实现,所谓复用,可以理解为同时侦听多个套接字,同时等待多个套接字的数据到达。
如图,阻塞当调用select时发生,当select返回时,说明相应套接字有数据准备好(或发生错误),调用相关套接字即可立刻返回。
1.4 信号驱动式I/O模型
信号驱动式I/O模型,即让内核在描述符就绪时发送SIGIO信号通知我们。
使用信号驱动式I/O首先需要开启套接字驱动式I/O 功能,通过sigaction系统调用安装一个信号处理函数。设置完成后,进程不会被阻塞。当数据报准备好后,内核会为该进程产生一个SIGIO信号。随后可以在相应的回调函数里调用recvfrom函数,也可以同住主循环,使其读取数据。
1.5 异步I/O模型
异步I/O会告知内核启动某个读取操作,并让内核完成整个操作(包括第二步将数据从内核复制到程序缓存区),完成后再通知相应的进程。与信号驱动式I/O的主要区别在于,信号驱动式I/O是内核通知我们何时可以启动I/O操作,而异步I/O是通知我们I/O操作何时完成。
如图,我们调用aio_read函数,向内核传递套接字描述符、缓冲区指针及缓冲区大小(与read相同的参数)和文件偏移,并告知内核当整个操作完成时如何通知我们。之后系统调用会立刻返回,进程不会被阻塞。
2. 各种I/O模型的比较
如前可知,前四种模型区别主要在第一阶段,因为它们第二阶段都是使用recvfrom将数据从内核复制到调用者的缓冲区(阻塞于第二阶段)。而异步I/O模型这两个阶段都要处理。
依照POSIX的定义,异步I/O和同步I/O描述如下:
- 同步I/O操作:导致请求进程阻塞,直到I/O操作完成。
- 异步I/O操作:不导致请求进程阻塞。
五种I/O模型的对比如图所示:
由定义知,前4种都是阻塞式I/O,因为真正的I/O操作(recvfrom)都将阻塞,最后一种才是异步I/O.