背景
阻塞
阻塞这个词来自操作系统的线程/进程的状态模型中,如下图:
一个线程/进程经历的5个状态,创建,就绪,运行,阻塞,终止。
各个状态的转换条件如上图,其中有个阻塞状态,就是说当线程中调用某个函数,需要I/O请求,或者暂时得不到竞争资源的,操作系统会把该线程阻塞起来,避免浪费CPU资源,等到得到了资源,再变成就绪状态,等待CPU调度运行。
UNIX I/O模型
Unix下可用的5种I/O模型:
(1)阻塞式I/O
(2)非阻塞式I/O
(3)I/O复用
(4)信号驱动式I/O
(5)异步I/O
输入操作
一个输入操作通常包括两个不同的阶段:
(1)等待数据准备好
(2)从内核向进程复制数据
而对于一个套接字上的输入操作:
(1)等待数据从网络中到达,当所等待分组到达时,它被复制到内核中的某个缓冲区
(2)把数据从内核缓冲区复制到应用进程缓冲区
1. 阻塞式I/O模型(blocking I/O)
我们把recvfrom函数视为系统调用,因为我们区分了应用程序和内核。
不论它如何实现,一般都会从在应用进程空间中运行切换到内核空间中运行,
一段时间之后再切换回来。
上图中,进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误是系统调用被信号中断。我们说,进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。recvfrom成功返回后,应用进程开始处理数据报。
2. 非阻塞式I/O模型(nonblocking I/O)
前三次调用recvfrom时没有数据可返回,因此内核转而立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回。
当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪。这么做往往耗费大量CPU时间,不过这种模型偶尔也会遇到,通常是在专门提供某一功能的系统中才有。
3. I/O复用模型(I/O multiplexing)
有了I/O复用,我们就可以调用select或poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用之上。
我们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。select的优势在于我们可以等待多个描述符就绪。
4. 信号驱动式I/O模型(signal-driven I/O)
我们可以使用信号,让内核在描述符就绪时发送SIGIO信号通知我们。
我们称这种模型为信号驱动式I/O。
我们首先通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。
无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知。既可以是数据已准备好被处理,也可以是数据报已准备好被读取。
5. 异步I/O模型(asynchronous I/O)
异步I/O模型中,首先告知内核启动某个操作,并让内核在整个操作完成后通知我们。这种模型与信号驱动模型的主要区别在于:信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。
aio_read系统调用立即返回,而且在等待I/O完成期间,我们的进程不被阻塞。
各种I/O模型比较
我们可以看出,前4种模型主要区别在于第一阶段,因为它们的第二阶段是一样的。在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。
相反,异步I/O模型在这两个阶段都要处理,从而不同于其他4种模型。
POSIX把这两个数据定义如下:
(1)同步I/O操作(synchronous I/O operation)导致请求进程阻塞,直到I/O操作完成。
(2)异步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匹配。