搞定技术面试 Unix 五种 IO 模型

2,212 阅读9分钟

Unix 五种 IO 模型

  • Unix 的 IO 模型有哪几种?分别是咋样的?
  • 各种IO模型有什么特点?
  • 阻塞和非阻塞的区别?
  • 同步和异步的区别?
  • epoll 为什么比较高效?

IO 模型

Unix 提供了五种 IO 模型,分别是阻塞式IO、非阻塞式IO、IO复用、信号驱动式IO、异步IO

一个IO操作通常包括两个阶段:

  1. 等待数据准备好;
  2. 从内核向进程复制数据;

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

实际应用程序在系统调用完成上面的 2 步操作时,调用方式的阻塞、非阻塞,操作系统在处理应用程序请求时,处理方式的同步、异步处理的不同,可以分为 5 种 I/O 模型。

*注:recvfrom 函数(经 Socket 接收数据),这里把它视为系统调用

阻塞式 IO (Blocking IO)

  • 使用系统调用,并一直阻塞直到内核将数据准备好,之后再由内核缓冲区复制到用户态,在等待内核准备的这段时间什么也干不了
  • 下图函数调用期间,一直被阻塞,直到数据准备好且从内核复制到用户程序才返回,这种IO模型为阻塞式IO。

优点:程序简单,在阻塞等待数据期间进程/线程挂起,基本不会占用 CPU 资源。

缺点:每个连接需要独立的进程/线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销较大,这种模型在实际生产中很少使用。

非阻塞式 IO (Non-blocking IO)

  • 内核在没有准备好数据的时候会返回错误码,而调用程序不会休眠,而是不断轮询询问内核数据是否准备好
  • 下图函数调用时,如果数据没有准备好,不像阻塞式IO那样一直被阻塞,而是返回一个错误码。数据准备好时,函数成功返回。
  • 应用程序对这样一个非阻塞描述符循环调用成为轮询。
  • 非阻塞式IO的轮询会耗费大量cpu,通常在专门提供某一功能的系统中才会使用。通过为套接字的描述符属性设置非阻塞式,可使用该功能

优点:不会阻塞在内核的等待数据过程,每次发起的 I/O 请求可以立即返回,不用阻塞等待,实时性较好。

缺点:轮询将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低,所以一般 Web 服务器不使用这种 I/O 模型。

IO 复用 (I/O multiplexing)

  • 类似与非阻塞,只不过轮询不是由用户线程去执行,而是由内核去轮询,内核监听程序监听到数据准备好后,调用内核函数复制数据到用户态
  • 下图中select这个系统调用,充当代理类的角色,不断轮询注册到它这里的所有需要IO的文件描述符,有结果时,把结果告诉被代理的recvfrom函数,它本尊再亲自出马去拿数据
  • IO多路复用至少有两次系统调用,如果只有一个代理对象,性能上是不如前面的IO模型的,但是由于它可以同时监听很多套接字,所以性能比前两者高

主要是select和epoll。一个线程可以对多个IO端口进行监听,当socket有读写事件时分发到具体的线程进行处理。模型如下所示:

优点:可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程),这样可以大大节省系统资源。

缺点:当连接数较少时效率相比多线程+阻塞 I/O 模型效率较低,可能延迟更大,因为单个连接处理需要 2 次系统调用,占用时间会有增加。

信号驱动式 IO (signal driven I/O (SIGIO))

信号驱动式I/O:首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。

  • 使用信号,内核在数据准备就绪时通过信号来进行通知
  • 首先开启信号驱动io套接字,并使用sigaction系统调用来安装信号处理程序,内核直接返回,不会阻塞用户态
  • 数据准备好时,内核会发送SIGIO信号,收到信号后开始进行io操作

优点:线程并没有在等待数据时被阻塞,可以提高资源的利用率。

缺点:信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。

信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。

但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失。

异步 IO (asynchronous I/O)

  • 异步IO依赖信号处理程序来进行通知
  • 不过异步IO与前面IO模型不同的是:前面的都是数据准备阶段的阻塞与非阻塞,异步IO模型通知的是IO操作已经完成,而不是数据准备完成
  • 异步IO才是真正的非阻塞,主进程只负责做自己的事情,等IO操作完成(数据成功从内核缓存区复制到应用程序缓冲区)时通过回调函数对数据进行处理

相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。异步过程如下图所示:

优点:异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠。

缺点:要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。

五种 IO 模型比较

阻塞式与非阻塞式的比较:调用阻塞IO后进程会一直等待对应的进程完成,而非阻塞IO不会等待对应的进程完成,在kernel还在准备数据的情况下直接返回。两者的最大区别在于被调用方在收到请求到返回结果之前的这段时间内,调用方是否一直在等待。

阻塞是指调用方一直在等待而且别的事情什么都不做;非阻塞是指调用方先去忙别的事情。

阻塞、非阻塞讨论的是调用 IO 的调用者;

同步IO和异步IO的区别:同步IO在做IO操作时,会将进程阻塞掉,阻塞IO、非阻塞IO、IO复用、信号驱动IO都属于同步IO。因为这几种模型,虽然非阻塞IO在轮询是否准备好时是没有阻塞的,但在读取数据阶段,三个模型都发生阻塞。 异步IO是进程发出IO操作请求后,便返回不在理财,直到内核通知进程IO完成,IO再继续执行之前的操作。

同步、异步讨论的是被调用者;

从上图中我们可以看出,越往后,阻塞越少,理论上效率也是最优。

这五种 I/O 模型中,前四种属于同步 I/O,因为其中真正的 I/O 操作(recvfrom)将阻塞进程/线程,只有异步 I/O 模型才与 POSIX 定义的异步 I/O 相匹配。

IO 复用更多介绍

主要有几个函数epoll, select, poll

  1. 当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用。
  2. 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  3. 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  4. 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  5. 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

select

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

epoll

epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

Reference

  1. unix 网络编程 第一卷
  2. 五种IO模型介绍和对比
  3. 一文读懂高性能网络编程中的I/O模型
  4. 聊聊IO多路复用之select、poll、epoll详解