网络I/O模型详解

重要参考资料:W.Richard Stevens 的 UNIX Network Programming Volume 1:The Sockets Networking API,Third Edition(UNIX网络编程-卷1:套接字联网API 第3版),6.2 Chapter:I/O Models。

基于 Unix / Linux 的网络编程的 5 种 I/O 模型。

相关概念

I/O概念

I/O:Input/Output,即输入/输出。操作系统中的 I/O 系统非常复杂和宽泛的概念。本文主要讨论网络I/O。

  • 在硬件层面,I/O是字节在硬盘、网卡、键盘等设备到内存之间流动的过程。

  • 在应用软件的角度上,Input是应用软件经过直接或间接地调用操做系统(kernel)提供的 I/O 接口访问应用进程外部数据的过程。这个过程一般包括两个阶段:第一阶段是在向内核提交需求(调用kernel接口);第二阶段是内核将这部分数据从内核缓冲区复制到应用缓冲区域即成功得到数据(完成任务)。

    Output是应用调用内核IO接口向应用进程外输出数据的过程。一样地一般涉及两个阶段:第一阶段是直接或间接调用内核IO接口请求输出数据;第二阶段内核将数据从应用缓冲区复制到目的地。

    从第一阶段进入第二阶段须要知足必定条件。对于Input,条件一般包括应用须要的数据在内核缓冲区准备好并能够复制到应用缓冲区中。对于Output,条件包括目标文件有足够空间存放数据。

  • 对于网络应用来讲,向网络(Network)输出输入数据与读写文件本质上都是I/O:前者是将数据写到网卡,最终发到目的主机,然后者是将数据写到硬盘。

    在Linux操作系统中,全部外设都被映射成文件,即对外设的操做都被映射成对文件的操做。

    TCP协议中,通讯连接的两端是Socket(套接字,由IP地址和端口组成)。服务端应用进程与客户端进程的通讯就是经过读写Socket文件:服务端进程想要接收客户端发送的数据,就要从本机上与该客户端对应的Socket文件输入流中读取数据,若是想发送数据,就要向该Socket文件输出流写入数据。因此应用间的网络通讯本质上只是I/O的一种。

同步和异步概念

同步和异步的概念描述的是用户线程与内核的交互方式。

  • 同步:是指用户线程发起I/O请求后需要等待或者轮询内核I/O操作完成后才能继续执行。
  • 异步:是指用户线程发起I/O请求后仍继续执行,当内核I/O操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

阻塞与非阻塞概念

阻塞和非阻塞的概念描述的是用户线程调用内核I/O操作(recvfrom调用)的方式。

  • 阻塞是:指I/O操作需要彻底完成后才返回到用户空间。
  • 非阻塞:指I/O操作被调用后立即返回给用户一个状态值,无需等到I/O操作彻底完成。

5种I/O模型

  • 阻塞 I/O(blocking I/O)
  • 非阻塞 I/O(nonblocking I/O)
  • I/O 多路复用(I/O multiplexing (select and poll ))
  • 信号驱动 I/O(signal driven I/O (SIGIO ))
  • 异步 I/O(asynchronous I/O (the POSIX aio_ functions))

一个输入操作通常包括两个不同的阶段:

  1. 等待数据准备好(Waiting for the data to be ready);
  2. 从内核向进程复制数据(Copying the data from the kernel to the process)。

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

记住这两点,这些 I / O Model 的区别就是在这两个阶段上各有不同的处理。

阻塞I/O模型

阻塞I/O模型:最简单、最流行、通常也是默认的 I/O 模型。直接阻塞用户进程直到系统返回数据。是两个阶段都阻塞。

blocking-io

进程调用 recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误是系统调用被信号中断。我们说进程在从调用 recvfrom 开始到它返回的整段时间内是被阻塞的。recvfrom 成功返回后,应用进程开始处理数据报。

这其中包含两部分:一是当应用进程调用 recvfrom 时,kernel 就开始了 I/O 的第一个阶段:准备数据;但对于网络 I/O 来说,数据还没完全到达,还没收到一个完整的数据包,这时候 kernel 要等到完整的数据全部到达。二是在应用进程这边,整个进程都被阻塞,直到 kernel 等到数据准备好,将数据从 kernel 空间拷贝到用户空间,然后 kernel 返回结果,应用进程才解除 block 状态,重新运行起赤。

Blocking I/O 的特殊就是阻塞两个阶段,内核等待数据阻塞,应用进程阻塞。

优点:数据可用立刻返回,没有延迟;调用简单;

缺点:整个过程,用户进程都处于阻塞等待状态。

非阻塞I/O模型

非阻塞I/O模型:可以把一个 socket 设置成非阻塞,是为了告诉内核,当所请求的 I/O 操作必须把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误替换。

noblocking-io

前三次调用 recvfrom 都没有数据返回,kernel 立即返回一个 EWOULDBLOCK 错误替代。第四次调用 recvfrom时,数据报已准备好了,会被拷贝到应用进程缓冲区,并且 recvfrom 返回成功,应用进程处理数据。

当一个应用程序像这样对一个非阻塞描述符循环调用 recvfrom 时,它被称为轮询(polling )。 应用程序不断地轮询内核,以查看某些操作是否已就绪。 这通常会耗费大量 CPU 时间,但偶尔会遇到这种模型,通常在专用于一个功能的系统上。

NIO 的交互需要应用进程不断轮循 kernel 数据是否已准备好,kernel 立即返回一个结果。

I/O多路复用模型

有了 I/O 多路复用(I/O multiplexing),就可以调用 select 或 poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的 I/O 系统调用上。

multiplexing-io

阻塞于 select 调用,等待数据报 socket 变得可读。当 select 返回 socket 可读时,我们调用 recvfrom 去拷贝数据报到应用缓冲区。

在两个阶段中,第一阶段阻塞在select操作上,select 侦测多个fd(文件描述符)是否准备就绪,当有fd准备就绪时,select 返回数据可读状态(通知);第二阶段是用户进程收到 select 返回的数据可读状态后,调用recvfrom ,recvfrom 负责从内核空间拷贝数据到用户进程空间,复制完成后返回成功。

此模式也有被称为 event driven I/O(事件驱动I/O)。select/epoll 的好处就在于单个进程就可以同时处理多个网络连接的 I/O。它的基本原理就是 select/epoll 这个 function 会不断的轮询所负责的所有 socket(侦测多个 fd 的可读状态是不就绪,轮询是内核线程在处理),当某个socket 有数据到达了,就通知用户进程。

与 I/O多路复用模型密切相关的另一种 I/O 模型是在多线程中使用阻塞式 I/O。这种模式与上述模型极为相似,但它没有使用 select 阻塞在多个文件描述符上,而是使用多个线程(每个文件描述符一个线程),这样每个线程都可以自由地调用诸如 recvfrom 之类的阻塞式 I/O 系统调用了。

I/O 多路复用模型与 阻塞 I/O 模型非常像。阻塞 I/O 是只调用了系统的 recvfrom,阻塞在 recvfrom 调用;而多路复用 I/O 需要使用两个系统调用(select 和 recvfrom),是阻塞在 select 调用,而不是阻塞在 recvfrom 调用。当 select 阻塞直到 socket 变为可读时,才通知用户进程调用 recvfrom 处理数据。

I/O 多路复用模型的优势是可以处理多个 connection(即,如果连接数不高的话,使用 select/epoll 的 web server 不一定比使用 multi-thread + blocking I/O 的 web server 性能更好,可能延迟还更大)。select/epoll 的优势并不是对单个连接处理的更快,而是在于能处理更多的连接,即多个连接进来时可复用一个线程或少量线程处理(复用)。

I/O 多路复用模型是同步非阻塞

  • 非阻塞是指 I/O 读写,对应的是 recvfrom 操作,调 recvfrom 操作时,数据报已经准备好了,无需阻塞。

  • 同步是指这个执行是在一个线程里处理的。

    在 I/O 多路复用模型中,对于每一个 socket ,一般都设置成为 non-blocking,整个用户进程是一直被阻塞的,只不过用户进程被 select 阻塞,而不是被 socket I/O 阻塞。

我们也可以用信号,让内核在描述符就绪时发送 SIGIO 信号通知用户进程,也称这种为 信号驱动I/O(signal-driven I/O)。而不是用轮循的方式去侦测数描述符的就材状态,避开了大量无效的数据状态轮询操作。

首先为 socket 的开启信号驱动 I/O功能,并通过 sigaction 系统调用安装(注册)一个信号处理器,该系统调用将立即返回,且用户进程继续进行而不被阻塞。

当数据报准备好被读取时,内核为用户进程生成 SIGIO 信号通知用户进程。用户进程可以在信号处理器中调用 recvfrom 读取数据,并通知主循环数据已准备好待处理,或者也可以通知主循环并让它读取数据。

无论如何处理信号,这个模型的优势在于在等待数据报到达期间不会被阻塞。 主循环可以继续执行,只要等待来自信号处理器的通知:数据已准备好待处理,或数据报已准备好被读取。

signal-driven-io

异步I/O模型

异步 I/O 模型:由 POSIX 规范定义,是把各种标准中出现的实时函数中存在的各种差异取得一致共同形成了当前的 POSIX 规范。

通知,这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到应用缓冲区)完成后通知应用进程。

通常,这些函数通过告诉内核开始操作并在整个操作(包括将数据从内核复制到我们的缓冲区)完成时通知我们来工作。

该模型与信号驱动 I/O 模型的主要区别在于:对于信号驱动 I/O,内核会告诉应用何时可以启动 I/O 操作(调用 recvfrom);而对于异步 I/O, 内核告诉应用 I/O 操作何时完成。

应用进程调用 aio_read函数(POSIX异步 I/O 函数以 aio_lio_开头),给内核传递描述符、缓冲区指针,缓冲区大小(与 read 相同的三个参数)和文件偏移(与 lseek 类似),并告诉内核当整个操作完成时如何通知应用。该系统调用立即返回,且在等待 I/O 完成期间,应用进程不被阻塞,kernel 会等待数据准备完成,然后将数据从内核空间拷贝到用户空间,并给通知用户进程 read 操作已完成 。

asynchronous-io

异步IO模型:思路是把应用进程需要先发送询问请求、再发送接收数据请求两个步骤等简化为一个,即只需要向内核发送一次请求,等待数据和数据拷贝到用户线程空间全由内核完成应用进程操作完成。

5种I/O模型比较

图6-6 是 5 种不同 I/O 模型的对比,从图中可以看出,前 4 种模型的主要区别在于第一阶段, 因为它们的第二阶段是一样的:在数据从内核拷贝到用户的缓存冲期间,进程调用 recvfrom 时被阻塞。然而,异步 I/O模型处理这两个阶段与前 4 种模型并不相同。

io-compare

同步I/O与异步I/O

POSIX 对这两个术语的定义如下::

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

    同步I/O(synchronous I/O):同步 I/O 操作会导致请求进程被阻塞,直至 I/O 操作完成。

  • An asynchronous I/O operation does not cause the requesting process to be blocked;

    异步I/O(asynchronous I/O):异步 I/O 操作不会导致请求进程被阻塞。

根据上面的定义,前面 4 种 I/O 模型:nonblocking、I/O multiplexing、和 signal-driven I/O 都属于 synchronous I/O(同步 I/O),因为实际的 I/O 操作 (recvfrom) 会阻塞进程。只有异步 I/O 模型与异步 I/O 定义匹配。

补充:non-blocking I/O 模型,当执行 recvfrom 系统调用且kernel 中数据准备好时,此调用会被阻塞,直到内核将数据拷贝到用户空间完成。

补充:asynchronous I/O 模型,当用户进程发起 I/O 操作之完,内核直接返回,用户进程就再也不用管,直到 kernel 通知说操作已完成,在这个过程,用户进程完全没有被阻塞。

阻塞I/O与非阻塞I/O

  • 阻塞I/O(blocking I/O):会一直阻塞应用进程直到操作完成。
  • 非阻塞I/O(non-blocking I/O):在 kernel 还在准备数据的情况下会立即返回(调用 recvfrom 会立即返回)

注意:这里的 I/O operation 指的是真实的 I/O操作,是指 recvfrom 这个系统调用。

阻塞I/O与I/O多路复用

阻塞 I/O 与 I/O 多路复用的两个阶段都阻塞,主要区别在于 select 阶段。

  • 阻塞 I/O:如果要接收更多的连接,就必须创建更多的线程。

  • I/O 多路复用:在第一阶段大量的连接都可以直接注册到 Selector 复用器上面,同时只要单个或少量的线程来循环处理这些连接事件就可以了,一旦达到 就绪 的条件,就可以立即执行真正的 I/O 操作。

    这是 I/O 多路复用与 阻塞 I/O 最大的不同,也是 I/O 复用的精髓所在。

从应用进程的角度去理解始终是阻塞的,等待数据和将数据拷贝到用户进程这两个阶段都是阻塞的。这一点从应用程序层面比较好理解,比如调用一个以 I/O 复用为基础的 NIO 应用服务,调用端是一直阻塞等待返回结果的。

相关文章

  1. UNIX Network Programming Volume 1, Third Edition: The Sockets Networking API
  2. Chapter 6:6.2 I/O Models 原英文版
  3. I/O models
  4. I/O Models
  5. IO - 同步,异步,阻塞,非阻塞
  6. 怎样理解阻塞非阻塞与同步异步的区别
  7. 详解Java 网络IO编程总结(BIO、NIO、AIO均含完整实例代码)
  8. 5种IO模型 详解 包含select epoll原理
  9. 浅谈高性能IO模型,如何理解四种IO模型呢?
作者

光星

发布于

2022-01-14

更新于

2022-06-17

许可协议

评论