要解释I\O的阻塞\非阻塞,同步\异步,实在难以下笔。涉及内容太多,很多知识点我也没有搞清楚,很怕误导别人。算是把看到的认为好的内容搬运过来,整合一下,再把自己不理解的内容抛出来,做个记录,日后想着学懂。
说I/O模型,不得不提《UNINX网络编程》这本书,“第六章:I/O复用”中把Uninx可用的I/O模型分为了5种:
里面有五张对应的图,这里就不放了,因为图不是关键点,只是便于理解的辅助,而真正关键的内容在于图下面的文字说明。
一个输入操作通常包括两个不同阶段:
- 等待数据准备好
- 从内核向用户进程复制数据
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络到达。当所等待的分组到达时,他被复制到内核中的某个缓冲区上。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
我们把recvfrom函数视为系统调用,因为我们正在区分应用进程和内核。不论他怎么实现,一般都会从应用进程空间中运行切换到内核空间中运行,一段时间后再切换回来。
因为书中是以UDP连接举例,所以讨论的都是recvfrom函数。
阻塞式I/O:进程调用recvfrom,系统调用直到数据报到达且被复制到应用进程的缓冲区或者发生错误才返回,所以进程从调用recvfrom开始到他返回的整段时间内都是被阻塞的。
非阻塞式I/O:进程把一个套接字设置为非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入到睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。当数据报没有准备好时,调用recvfrom返回一个错误;当数据报准备好时,调用recvfrom函数,复制数据到进程缓冲区。
I/O复用模型:系统调用阻塞在select或者poll系统调用上,而不是真正阻塞在I/O系统调用上,例如阻塞在recvfrom上。
信号驱动式I/O模型:让内核在描述符准备就绪时,系统为该进程产生一个信号。我们在信号处理函数中调用recvfrom读取数据报。
异步I/O模型:告知内核启动某个操作,并在整个操作完成后通知我们。和信号驱动的区别在于:信号驱动通知我们何时启动I/O操作,而异步I/O通知我们何时I/O完成,这其中包括了数据从内核缓冲区拷贝到用户进程。
通过上面描述,我们可以总结出一个结论:前四种模型区别只在于第一阶段,第二阶段是完全一样的:阻塞在系统调用上,即从内核复制到用户进程。而异步I/O模型是由内核替我们完成了数据复制,我们拿到通知时,数据已经在用户缓冲区了。
对于一个套接字而言,默认都是阻塞状态,这就意味着当一个不能立即完成的套接字调用时,进程会被投入睡眠,等待操作完成。可能产生阻塞的套接字当然不止recvfrom一种,实际有很多:
对于TCP而言,只要缓冲区有一个字节数据可读,即可返回。而对于UDP而言,必须要是完整数据报。
对于阻塞TCP套接字而言,如果发送缓冲区没有空间,将会阻塞直到有空间位置。对于非阻塞的TCP套接字而言,如果发送缓冲区没有空间,直接返回错误,如果发送缓冲区空间不足,返回内核能够复制到缓冲区的字节数。
对于UDP而言,不存在缓冲区,所以不存在因为缓冲区而阻塞的问题,会有其他原因阻塞。
同理,当没有连接到达时,阻塞的套接字调用accept进程会被投入睡眠,非阻塞的套接字调用会直接返回错误。
这个只针对于TCP套接字而言,对于阻塞TCP套接字,connect一定会等到三次握手。非阻塞的TCP套接字,三次握手会发起,但是会返回一个错误。
也就是说,对于套接字而言,阻塞会发生在任何可能产生进程睡眠的系统调用上,不仅仅是说从内核空间拷贝数据。
书中所描述的5种I/O模型,针对的都是输入数据而言,并且所站的角度是从用户程序角度而言。
不能一概认为阻塞\非阻塞,同步\异步就是这5种I/O模型。
实际上阻塞\非阻塞,同步\异步是一个更为广泛的概念,一般描述的都是进程间通信的方式。而实际上套接字也是进程通信的一种方式,也是我们应用层最为常用的。
那么进程间通信是怎么描述的呢?从《操作系统概念》中有如下描述:
进程间通信是通过send和receive两个基本操作完成的,具体的实现方式是有所区别的。消息的传递是有阻塞和非阻塞两种,也被称为同步和异步。
- 阻塞式发送:发送方进程会被一直阻塞,直到消息被接收方进程收到
- 非阻塞式发送:发送方调用send后,立即就可以做其他操作
- 阻塞式接受:接受方调用receive后一直阻塞,直到消息到达可用
- 非阻塞式接受:接受方调用receive后,要么得到一个有效结果,要么得到一个空值
对应到从用户程序角度的套接字的I/O模型上,发送方都是非阻塞的,因为发送方都是通过系统调用把数据复制到发送缓冲区即可,数据是否最终被接收方收到是由网络协议所保证。而我们所说的发送方阻塞,也只是阻塞在数据复制到发送缓冲区而已,而不是是否被接收方接收到。对于接受而言,对应了阻塞式和非阻塞式两种。
从进程的通信角度来看,阻塞/非阻塞与同步/异步是同一个概念。
进程通信,实际也就是数据在进程间的传递。因为操作系统为了保证进程间的独立和安全性考虑,对于操作磁盘、网络等底层的操作都只能由内核来执行。数据传递势必会经过用户空间和内核空间两个部分。那么就会有进程的切换,由用户态切换到内核态,进程进入阻塞状态。众所周知,进程有五种状态:new、running、waiting、ready、terminated。
阻塞状态指的就是进程发起了一个系统调用,由于该系统调用不能立刻完成,内核将进程挂起为waiting状态,释放cpu资源。比如说网络读写,十分耗时,如果进程一只占用CPU则浪费了资源,把CPU资源让给别的进程,提高CPU资源利用率。
那么更明确的可以说,阻塞\非阻塞是指一个操作是否会让进程变为等待状态。那么为什么提到阻塞\非阻塞,就经常说到I/O呢?因为能让一个进程进入阻塞状态,要么是进程自身调用wait或者sleep,要么是进程进行了系统调用,而系统调用通常都涉及到I/O操作。
而系统调用又会涉及到阻塞\非阻塞,系统调用会涉及到CPU与I/O设备进行通信,现代计算机都是采用DMA(直接内存访问)的方式,所以都是非阻塞的。这里的非阻塞指的是CPU不会一直等待I/O。
操作系统对上层应用提供的都是一般都是阻塞操作,即CPU会挂起用户进程。也有非阻塞的方式,与前面介绍的非阻塞I/O相对应,即不会挂起进程,而是直接返回一个错误。这里的非阻塞指的又是用户进程。
那么一个完整的阻塞读操作如下:
写操作同理。
一个完整的非阻塞读操作如下:
如上所述,拷贝数据都是阻塞操作,因此产生了另外一种异步I/O,即数据都拷贝完后,再通知用户进程。实际上这又是另外一个角度的概念了。
总结一下,从进程间通信角度而言,阻塞\非阻塞与同步\异步是用一个概念。
而我们常讨论的是用户进程角度的I/O操作,而一般更具体我们所描述的五种I/O模型,指的是从用户进程角度看的套接字的I/O读取数据模型。
那么问题聚焦到用户进程角度看的套接字的I/O读取数据模型就很简单了。
一般我们实际工程中使用最多的就是I/O复用模型,即select、poll、epoll模型。
这三者区别,以后再讲,但是需要记住的是select有自身的优势,epoll不是更好的poll模型。从某些情况来看,epoll不一定性能好于poll,例如大量短链接或者连接频繁切换读写状态等,epoll会产生额外的系统调用。
参考:《UNINX网络编程》,《怎样理解阻塞非阻塞与同步异步的区别? - 萧萧的回答 - 知乎 https://www.zhihu.com/question/19732473/answer/241673170》