阻塞非阻塞同步异步 IO
本文内容
1. IO 是什么?
IO 全称是 Input/Output,即输入输出,常见的 IO 有磁盘 IO,网路 IO 等。
当我们需要将内存中的数据持久化时,就需要向磁盘写数据,这个过程就是一次输出(从内存角度来说,将内存中的数据输出到磁盘,从磁盘角度来说则是输入了);反之,我们需要把数据从磁盘中取出时,就是一个输入的过程。这时磁盘就是一个 IO 设备,负责将数据具体的写入和读出。
当我们需要跨主机进行通信时,一个常用的方法就是使用网络通信,而网络通信过程中需要借助网络 IO 设备,比如网卡、路由器、交换机等,它们负责数据流的输入和输出。
对于我们经常使用的应用程序来说,在程序需要使用 IO 操作时,往往只是调用 API,比如:
- 网络 IO:建立连接后,调用
send()
发送数据,recv()
接收数据; - 磁盘 IO:打开文件后,调用
write()
写数据,read()
读数据。
我们都知道,为了安全和稳定,操作系统将地址空间划分为用户空间和内核空间,只有 在内核空间才能访问 IO 设备。
程序都是跑在用户空间的,所以程序调用的这些 API 也只是操作系统提供出来的系统调用函数,所以 只能通过操作系统提供的系统调用来间接的完成 IO 操作。
正是因为用户和内核空间的划分,所以 程序一次 IO 调用,会经历两个过程:
- 内核收到程序的 IO 操作后访问 IO 设备,等待 IO 设备将数据准备好;
- 数据准备好后,内核需要将数据从内核空间拷贝到用户空间(一般读出来的数据会先放到缓冲区),此时程序才能读取到。
理解了 IO 和程序进行 IO 操作的流程,下面就来看看几种常见的 IO,它们分别是 阻塞 IO、非阻塞 IO、同步 IO 和异步 IO。
2. 阻塞和非阻塞 IO
2.1 阻塞 IO
阻塞 IO 是指在发起 IO 调用后,调用方会一直阻塞等待,直到接收到数据 才进行后续的操作。
这个阻塞等待过程包括:用户发起 IO 调用 -> 内核准备好数据 -> 内核将数据拷贝到用户空间 -> 用户接收到数据
2.2 非阻塞 IO
阻塞 IO 是发起系统调用后,一直阻塞直到数据返回,非阻塞 IO 与其相反,发起 IO 调用后,内核在未准备好数据时会立即返回,此时程序可以继续向下执行,只不过一般应用程序都会不断轮询内核,直到取到数据。
不断轮询并不是一种阻塞,毕竟进程还是一直在跑的(一直循环调用)。
需要注意的是,当轮询到内核已准备好数据时,内核需要将数据拷贝到用户空间,这个过程调用方还是阻塞的。
注:EWOULDBLOCK 是一个与非阻塞 I/O 操作相关的错误码,表示在进行非阻塞操作时,暂时无法立即完成该操作,需要进一步等待才能继续进行
3. 同步和异步 IO
前面讲的 阻塞/非阻塞 IO 描述的是调用者发起调用后,是否持续等待直到数据返回。而 同步/异步 IO 描述的是调用者发起调用后,是否需要继续参与这个过程。
其实前面的阻塞/非阻塞 IO,都是一种同步 IO,因为 调用者在发起调用后,还要继续参与这个过程。
非阻塞 IO 在内核拷贝数据时也是阻塞等待的。
而 异步 IO 在发起调用后,内核会立即返回(表示调用成功,并不是返回结果),接着 只需要内核完成整个操作(包括准备数据、拷贝到用户空间),完成后通知应用进程即可,调用者是不需要参与整个过程的。
可以发现,调用者发起调用后,在内核准备数据和拷贝数据的过程中都是不需要参与和等待的。
4. 总结
程序一次 IO 调用,会经历两个过程:
- 内核等待 IO 设备将数据准备好;
- 内核需要将数据从内核空间拷贝到用户空间。
阻塞/非阻塞 IO:
- 阻塞 IO 是发起系统调用后,一直阻塞直到数据返回;
- 非阻塞 IO 是发起 IO 调用后,内核在未准备好数据时会立即返回,此时程序可以继续向下执行,只不过一般应用程序都会不断轮询内核,直到取到数据。
阻塞/非阻塞 IO 都是一种同步 IO,都有一段等待阻塞的过程,在此过程调用者是需要一直参与的。
同步/异步 IO:
- 同步 IO 是 调用者在发起调用后,还要继续参与这个过程;
- 异步 IO 在发起调用后,调用者不需要参与整个过程,内核完成后通知应用进程即可。