阻塞与非阻塞
如阻塞、非阻塞字面意思一般,我们很容易得到结论就是:阻塞就是调用该方法后会阻塞当前线程,直到方法返回。而非阻塞则相反,调用方法后不会阻塞当前线程,会立即返回结果,比如如果当前还没有任何数据拷贝到内核缓冲区,但又由于需要立即返回结果,那么read()
就会返回-1,以告知用户这次调用读取不到任何数据。
但要更准确来说的话,应该是这样:
这里有三个概念需要注意:对应的硬件,内核缓冲区,用户缓冲区。比如对于
read()
,从对应硬件拷贝数据到内核缓冲区,从内核缓冲区拷贝数据到用户缓冲区,这两个步骤都需要等待。阻塞和非阻塞针对的都是第一个步骤而言的,对于第二个步骤,不论是阻塞IO还是非阻塞IO,当前线程都会进入阻塞状态。
当然这里并不绝对,并不是一定read()
都会有从内核缓冲区到用户缓冲区这一步的实现。如果没有的话,那么非阻塞IO在整个过程中就都不会阻塞了。
由上可知,阻塞和非阻塞IO的阻塞主要是对于第一个步骤进行区分的,和我们理解的阻塞与非阻塞概念不完全一致。
为什么会发生阻塞
在了解完阻塞和非阻塞的详细概念后,我们来说说为什么系统函数的调用就会阻塞线程。
首先要明确的一点是,调用操作系统提供的函数不完全都是系统调用。比如操作系统提供的操作字符串的函数,这些函数并不算是系统调用。
系统调用(system call
)是用户程序与操作系统内核之间进行交互的方式,用户程序通过系统调用向操作系统内核请求一些特权操作,如文件操作、网络操作等。而操作系统提供的函数则包括两种类型:一种是直接在用户空间中运行的函数,另一种是基于系统调用封装的函数库。
我们知道操作系统分为用户态和内核态,这是因为操作系统为了保证安全,会限制我们写的程序只能运行在用户态,在这个层面里,很多操作都会受限,比如说访问硬盘资源。如果说我们要访问系统资源,那么就需要切换到内核态。
我们在自己的程序中调用了系统函数,就涉及到了上下文切换。不同操作系统有不同的方式去处理系统调用,Linux采取的是一对一的方式,但这个上下文切换只涉及到从用户态切换到内核态,线程还是同一个线程。
参考链接:系统调用涉及到线程切换吗?。
对于系统调用read()
这种IO操作,不能立即完成,所以内核将会将该线程挂起(毕竟也是我们要求的等内容全部拷贝过来之后再继续执行),等待IO操作的完成,这样就造成了阻塞。
关于为什么系统调用会造成阻塞的详细解析可以参考这篇答案:怎样理解阻塞非阻塞与同步异步的区别?。
同步与异步
同步指的是发起调用后,没返回结果前不会返回,而异步指的是发起调用后立即返回结果。
那么非阻塞的IO和异步的IO差别在哪呢?上面我们提到非阻塞IO对于读取到的数据会直接返回,那么就存在可能会返回完整的值,可能什么也没有(返回-1),而异步IO,虽然也是直接返回,但是可以通过注册回调函数等方式,告诉操作系统把需要的值完全拷贝之后,用我们注册的回调函数去处理。
非阻塞IO和异步IO确实都没有阻塞主线程,但是在返回内容上有所差异。
多路复用IO为什么算非阻塞而非异步
综上所述,多路复用IO虽然可以一个线程处理多个IO,但是它就不是通过异步告诉每个IO完成后应该执行操作,而是让操作系统去做read()
操作,等需要的内容完全拷贝好之后,再通知我们去处理。
这里强烈推荐读一下这篇文章,详细阐述了多路复用IO的概念:IO多路复用到底是不是异步的?