在我们学习Linux的I/O操作和I/O模型的时候,有一个困扰大家很大的一个障碍就是大家会把同步和阻塞混淆起来,把异步和非阻塞混淆起来。
网上的的各种回答要么不清楚,要么根本就是错的,所以为了自己理解的更加透彻,这里自己总结出一些观点和理解。这里只代表本人自己理解,如有不对的地方,还希望能指出。
首先明确一点,同步和异步有两个使用场景,一个是网络通信模型中,还有一个是I/O模型。
网络通信模型同步与异步
网络模型的同步与异步常常指事件处理的同步与异步。比如一个网络交互分为第一步请求,第二步 响应。那么常常有俩种选择,第一种等对方返回结果了我们再进行接下来操作。第二种我们可以设置个回调函数,然后可以不阻塞等待对方返回结果,如果对方返回结果了,回调函数触发,这样就不会阻塞到等待结果那步了。那么这俩种第一种称为同步网络模型,第二种称为异步网络模型。
I/O模型的同步与异步
I/O 模型的同步与异步跟网络模型的同步与异步不同。接下来的全文将用来讲解I/O模型中的问题。
在Unix系统中的I/O操作中一般会分为三步,
- 用户程序调用系统调用进行I/O操作
- 内核准备数据(这个操作用户程序看不到)
- 内核把准备好的数据拷贝到用户缓冲区(这个操作用户也看不到)
对于阻塞和非阻塞的概念。它是相对于用户程序能看到的状态来进行区分的。
当调用一个系统调用后,发起IO请求是否会被阻塞,那么这种I/O称为 阻塞I/O。它会经历准备数据和数据从而内核缓冲区拷贝到用户缓冲区的这一个过程。
当调用一个系统调用后,系统调用直接返回,那么它就是非阻塞I/O。 但是,虽然你返回了,但是,数据还是没有你要真正操作数据,也要等待数据到达用户空间后才能操作,所以你必须经常去循环判断这个数据有没有准备好。
同步和异步
这里的同步和异步指的是,当内核缓冲区有数据的时候,是否需要用户线程来参与内核到用户缓冲区的拷贝过程。
如果,不太理解上面的话,那么我们先来看下Linux下的网络模型有五种:
- 阻塞I/O
- 非阻塞I/O
- I/O多路复用
- 信号驱动I/O
- 异步I/O
阻塞I/O
在Linux中,socket的I/O 默认都是阻塞的,
当应用程序调用recvfrom 的系统调用的时候,内核进入第一个阶段,等待数据。所以在用户态这边整个进程都会阻塞。
当内核准备数据之后,还需要将数据拷贝到用户态内存,然后才会return。之后用户进程才会解除阻塞状态。
非阻塞I/O
设置I/O属性可以修改socket的非阻塞模式,当进行读操作的时候,流程图如下:
当用户进程read操作的时候,如果内核还没数据,就会返回无数据状态,当用户进程角度上,如果是无数据,继续recvfrom等待, 直到内核数据准备好,拷贝到用户控件内存,所以非阻塞I/O需要用户进程一直轮询。
I/O 多路复用
I/O 复用就是我们所说的select, poll 和epoll ,有些地方也称为 事件驱动I/O。
select和poll 这个函数会不断地轮询所负责的所有的socket,当某个socket 有数据到达了,就通知用户进程, epoll 的方式不一样。
信号驱动I/O
异步I/O
用户进程发起read操作后,可以直接干其他的事情,在内核的角度上,它收到一个read之后,立即返回,所以不会对用户进程产生任何阻塞,然后内核会完成等待数据准备完成,然后将数据拷贝到用户内存,当一切都完成后,内核会给用户进程发一个信号,告诉他read 操作完成了
我们注意一下,前面四个模型的从内核缓冲区到 用户缓冲区中,都需要用户线程recvfrom的参与,而且recvfrom 都是被阻塞的, 所以前面四个模型。
阻塞I/O、 非阻塞I/O、I/O复用、信号驱动I/O都是属于同步I/O。
而最后一个在内核缓冲区到用户缓冲区没有用户线程的参与,所以他是属于异步I/O的。
所以我这里大概理解下:
- 当用户线程感知到真正的操作内核缓冲数据的时候,它自己感知到用户第一步准备数据和内核到用户缓冲区的拷贝,也就是感知到有依赖关系的是时候, 那么它是同步I/O。比如I/O复用的recvfrom 。
- 当用户线程不用感知内核缓冲区,而等待I/O完全完成后,通过收到信号通知后,直接可以读取用户缓冲区的数据。那么就是异步的。
这里特别注意一下,信号驱动I/O和异步I/O都有一个信号通知。
信号驱动 IO 是由内核通知我们何时可以启动一个 IO 操作, 而异步 IO 模型是由内核通知我们 IO 操作何时完成.
本文参考:
Linux 网络I/O模型
Unix 网络 IO 模型: 同步异步, 傻傻分不清楚?
理解同步/异步/阻塞/非阻塞IO区别
网络模型同步异步与I/O模型同步异步