0%

《操作系统》IO 模型

IO 发生时(以 network IO read 为例)涉及到两个系统对象: 一个是调用这个 IO 的 process(进程)或者 thread(线程),以及两个阶段:

  1. 等待数据准备
  2. 将数据从内核 copy 到 process 中

IO 模型的区别就是在这两个阶段上的差异。

阻塞同步 IO

当调用一个系统调用 read 时,kernel 就开始 IO 的第一个阶段:准备数据,对于 network IO 来说,很多时候数据一开始还没有到达(一个 TCP 包没有接收完整),这个时候 kernel 就要等待足够的数据到来(这也和缓存 IO 还是非缓存 IO 有关,一般都是缓存 IO)

而在用户进程这边,整个进程会被阻塞

当 kernel 一直等到数据准备好了,就会将数据从系统内存 copy 到用户内存,然后 kernel 返回结果,用户进程才结束 block 状态,重新运行起来。

blocking IO 的特点就是两个阶段都被 block。

非阻塞同步 IO

当用户进程发出 read 操作时,如果 kernel 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个 error,比如 EAGAIN 错误(提示用户进程重试)

从用户进程角度来讲,他发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果

用户进程判断结果是 error 时,他就知道数据还没有准备好,于是就再次发送 read 调用。就是轮询过程

直到 kernel 中数据准备好了后,并且再次接收到 read 调用后,将数据 copy 到用户内存。但是这种模型效率很低。

可以通过系统调用将文件描述符设置为 IO 阻塞模型或者 IO 非阻塞模式

多路复用同步 IO

多路复用同步 IO 表示一个线程可以监视多个文件描述符

相比前面的非阻塞同步 IO,非阻塞同步 IO 可以一个线程监视多个文件描述符,但是通过对每个 fd 系统调用返回的 EAGAIN 错误来监视

很明显效率大打折扣(系统调用的代价很高),而多路复用同步 IO 中,不是通过对每个 fd 系统调用返回的 EAGAIN 错误来监视,而是一次性询问内核所有 fd 发生的事件,效率相对非常高

I/O 多路复用需要使用特定的系统调用,最常见的系统调用就是 select,该函数可以同时监听最多 1024 个文件描述符的可读或者可写状态

除了标准的 select 函数之外,操作系统中还提供了一个比较相似的 poll 函数,它使用链表存储文件描述符,摆脱了 1024 的数量上限。

再到后来出现了 epoll

多路复用函数会阻塞的监听一组文件描述符,当文件描述符的状态转变为可读或者可写时,select 会返回可读或者可写事件的个数,应用程序就可以在输入的文件描述符中查找哪些可读或者可写,然后执行相应的操作。

select 在各大操作系统上都提供支持,是保底的多路复用同步 IO 方案

Linux 使用更先进的 Epoll( select、poll 的升级版),Mac 使用 Kqueue ,而 Windows 则是使用 IOCP 的异步 IO 方案

异步 IO

用户进程发起 read 操作之后,立刻就可以开始去做其它的事。

用户发起 read 操作时可以告诉内核以哪种方式通知自己,比如发信号的方式、启动新线程执行 handler 的方式

从 kernel 的角度,当它收到一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block。

kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel 会给用户通知(前面用户进程告诉内核的通知方式),告诉它 read 操作完成了

Linux 提供了 AIO 库函数实现异步,但是不够成熟,用的很少,Linux 上更受欢迎的则是 epoll 的多路复用同步 IO,netpoll、libuv 在 Linux 都使用 epoll

Windows 中使用的 IOCP 就是一种异步 IO 方案




微信关注我,及时接收最新技术文章