其乐融融的IT技术小站

送分来了,华为一面,介绍下五种 IO 模型

所谓 I/O,就是 Input/Output,输入/输出,在操作系统中,输入输出操作其实并不简单

工作在用户态的应用程序想要读取磁盘中的具体文件内容,就需要经过 System Call(系统调用)陷入内核态

因此,在操作系统中,输入输出操作通常都会包括以下两个阶段:

  1. 准备数据:内核缓冲区准备数据,等待其准备好
  2. 数据拷贝:从内核缓冲区向用户缓冲区复制数据

以网络通信即 Socket 上的输入操作为例,对应的第一阶就是等待数据从网络中到达网卡(对于网络 I/O 来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的 TCP 包。这个时候内核就要等待足够的数据到来),然后从网卡中将数据拷贝到内核缓冲区,这样,数据就准备就完成了;第二阶段就是把数据从内核缓冲区复制到用户缓冲区。

图片

操作系统系统如何去管理输入和输出,从而获取输入和输出的数据?这就是 I/O 模型。

Linux 中有以下五种 I/O 模型:

  • Blocking I/O:阻塞 I/O
  • Non-Blocking I/O:非阻塞 I/O
  • I/O Multiplexing:I/O 多路复用
  • Signal Blocking I/O:信号驱动 I/O
  • Asynchronous I/O:异步 I/O

Blocking I/O

在 Linux 中,默认情况下所有的 Socket 都是 Blocking,它符合人们最常见的思考逻辑。

上面我们介绍了输入输出操作通常都会包括两个阶段,并不是凭空想想,而是对应具体的 I/O 系统调用的,以网络通信为例,Blocking I/O 就对应阻塞的系统调用 recvfrom

第一阶段,准备数据:当用户进程通过系统调用 recvfrom 进行数据读取,操作系统就开始了 I/O 的第一个阶段-准备数据。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞住

解释下 阻塞 的概念:源自操作系统对进程/线程状态的描述概念,其定义为:操作系统把进程/线程从“运行(running)状态” 挂起为 “阻塞(blocked)状态”(又称“等待(waiting)状态”)。当进程/线程处于阻塞状态,则意味着其处于暂停运行状态,暂时不会被 CPU 调度执行

第二阶段,数据拷贝:当内核一直等到数据准备好了,它就会将数据从内核空间中拷贝到用户空间,然后系统调用 recvfrom 返回结果,用户进程才解除阻塞的状态,重新运行起来

图片

在上述步骤中,用户进程调用 recvfrom,该系统调用直到数据准备好且被复制到用户缓冲区中才返回。

从调用 recvfrom 开始,到它返回数据的整段时间,用户进程都是被阻塞住的!这就是 Blocking I/O 的特点,可以简单记忆为 “IO 执行的两个阶段用户进程都被阻塞住了”

recvfrom 成功返回后,用户进程才开始继续处理。

Non-Blocking I/O

参考《Unix 网络编程:第一卷》,书中是这样描述 Non-Blocking I/O 的:

"进程把一个套接字设置成非阻塞是在通知内核,当所请求的 I/O 操作非得把本进程投入睡眠才能完成时,不要把进程投入睡眠,而是返回一个错误"

意思就是,如果某个用户进程进行系统调用 recvform 尝试获取数据,但这时候数据还没准备好:

  • 如果操作系统把这个进程挂起,那就是 Blocking I/O
  • 如果操作系统选择立即给用户进程返回错误信息,那就是 Non-Blocking I/O

如下图所示:

图片

非阻塞的 recvform​ 系统调用之后,如果数据还没准备好,应用进程不会被阻塞住,recvfrom​ 立即返回一个 EWOULDBLOCK​ 错误。用户进程在收到 recvfrom 调用的返回信息之后,可以干点别的事情,然后再发起 recvform 系统调用。

重复上面的过程,不断地进行 recvform 系统调用。这个过程通常被称之为**轮询 (polling)**。轮询检查内核数据,直到数据准备好,再拷贝数据到用户进程,进行数据处理。

需要注意的是,当 recvfrom 系统调用进行拷贝数据的时候,用户进程同样是被阻塞住的。

因此,Non-Blocking I/O 的特点就是用户进程需要不断的主动询问内核数据准备好了没有,可以简单记忆为 “IO 执行的第一阶段用户进程非阻塞,第二阶段用户进程阻塞”

I/O Multiplexing

由于 Non-Blocking I/O 需要不断主动轮询,轮询会消耗大量的 CPU 时间,而后台可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。这就是 I/O Multiplexing。

I/O Multiplexing 引入了新的系统调用 select/poll/epoll(也成为多路复用器),这几个系统调用也是重点,不过本文就不过多阐述了。

具体来说,I/O Multiplexing 就是将多个应用进程的 Socket 注册到一个多路复用器(select/poll/epoll)上,然后使用一个进程来监听该多路复用器,多路复用器会不断的轮询所有注册进来的 Socket,只要有一个 Socket 的数据准备好,就会返回该 Socket。再由应用进程发起真正的 IO 系统调用(也就是 recvfrom,和 Blocking I/O 一样),来完成数据读取。

简单来说,I/O Multiplexing 就是同时阻塞了多个应用进程,而且可以同时对多个 Socket 进行检测,直到有数据可读或可写时,才真正开始 I/O 操作。

图片

比较上图和 Blocking I/O,你会发现 I/O Multiplexing 的 I/O 操作和 Blocking I/O 似乎差不多,事实上,IO 多路复用还更差一些,因为这里需要使用两个系统调用 (select​ 和 recvfrom​),而 Blocking IO 只需要一个系统调用 (recvfrom)。

但是,IO 多路复用的优势并不是对单个连接能处理得更快,而是只需要一个进程就可以同时处理多个 I/O,能同时处理更多的连接。

Signal Blocking I/O

Signal Blocking I/O 就是当用户进程发起 I/O 操作的时候,首先通过系统调用 sigaction​ 向内核注册一个信号处理函数,这个系统调用会立即返回不会阻塞用户进程;当内核数据准备好了就会发送一个 SIGIO 信号给用户进程,这样用户进程就知道内核数据准备好了,可以开始执行 I/O 系统调用了。

图片

和 Non-Blocking I/O 一样,信号驱动 IO 的用户进程在 I/O 的第一阶段准备数据是非阻塞的,在第二阶段数据拷贝是阻塞的

不过信号驱动 IO 基于回调机制,其实现和开发应用难度大,因此在实际中并不常用。

Asynchronous I/O

异步 I/O,先来解释下什么是异步?

POSIX 的定义如下:

  • 同步 I/O 操作(synchronous I/O operation):导致请求进程阻塞,直到 I/O 操作完成
  • 异步 I/O 操作(asynchronous I/O operation):不导致请求进程阻塞

根据这个定义,我们可以做一个分类了,那就是上述四种 I/O 都是同步 I/O!因为它们无一例外都会在第二阶段阻塞住用户进程直到 I/O 操作完成。

这就是为什么你会看见有人把 “阻塞 I/O” 称之为 “同步 阻塞 I/O”,把 “非阻塞 I/O” 称之为 “同步 非阻塞 I/O” 了

而异步 IO 所谓的在整个 I/O 操作期间都不会阻塞用户进程,其通常的工作机制是:

用户进程告知内核启动某个 I/O 操作,并让内核在整个操作(包括将数据从内核复制到用户缓冲区)完成后通知用户进程。

这与 Signal Blocking I/O 的本质区别就是:

  • Signal Blocking I/O 是在数据准备好了之后进行通知,告知应用进程可以启动 I/O 操作进行拷贝数据了
  • Asynchronous I/O 是在整个 I/O 操作完成了之后进行通知,告知应用进程 I/O 操作已经完成了

下图给出了一个异步调用的例子:

图片

用户进程进行异步系统调用 aio_read​ 之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户进程可以去做别的事情。等到数据准备好了,内核直接拷贝数据给用户进程(不需要用户进程再主动发起 recvfrom 系统调用),拷贝完毕后内核才会给用户进程发送通知,告诉用户进程操作已经完成了。

所以,异步 IO 的两个阶段,用户进程都是非阻塞的,用户进程将整个 IO 操作都交由内核完成,内核完成后会发送通知。在此期间,用户进程不需要去检查 IO 操作的状态,也不需要主动的去拷贝数据。

五种 I/O 模型比较

本文理清了五种 I/O 模型,并区分了阻塞/非阻塞、同步和异步的概念

最后上张图对比下,加深印象

图片

赞 ()
分享到:更多 ()

相关推荐

内容页底部广告位3
留言与评论(共有 0 条评论)
   
验证码: