网络 IO - 网络 IO 模式

概念说明

用户空间和内核空间

现代操作系统都是采用虚拟存储器,对于 32 为操作系统而言,它的寻址空间(虚拟存储空间) 为 4G(2 的 32 次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也可以访问底层硬件设备。为了保证内核的安全,不能让用户进程直接操作内核(kernel),操作系统将虚拟空间分为两部分,一部分为内核空间,一部分为用户空间。针对 linux 操作系统而言,将最高的 1G 字节(从虚拟地址的 0xC0000000 到 0xFFFFFFFF)作为内核空间,供内核使用。而较低的 3G 字节(0x00000000 到 0xBFFFFFFF) 作为用户空间,供各个进程使用。

进程切换

为了控制进程的执行,内核必须有能力挂起正在 CPU 上运行的进程,并恢复以前挂起的某个进程的执行,这种行为称为进程的切换。

从一个进程的运行切换到另一个进程上运行,这个过程中经过下面的变化:

  1. 保存处理机上下文,包括程序计数器和其他寄存器
  2. 更新 PCB 信息
  3. 把进程的 PCB 移入相应的队列,如就绪,在某时间阻塞等队列
  4. 选择另一进程执行,并更新其 PCB。
  5. 更新内存管理的数据结构
  6. 恢复处理机上下文

进程切换很消耗资源。
具体过程请参考 进程切换

进程的阻塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败,等待某种操作的完成,新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。因此,进程的阻塞是进程自身的一种行为,也因此只有处于运行态(获得 CPU)的进程才可能转为阻塞状态。当进程进入阻塞状态时,是不占用 CPU 资源的

文件描述符 fd

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一个非负整数。实际上它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

文件描述符的概念往往只适用于 UNIX,Linux 这样的操作系统。

缓存 I/O

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

缓存 I/O 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝所带来的 CPU 以及内存开销是非常大的。

IO 模式

如同前面说的,对于一次 IO(以 read 为例),数据会先被拷贝到操作系统内核的缓冲区中,然后才从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以一个进程的 read 操作会经理两个阶段:

  1. 等待数据准备
  2. 将数据从内核拷贝到进程中

正式因为这两个阶段,linux 系统产生了下面五种网络模式的方案。

  1. 阻塞 I/O (blocking IO)
  2. 非阻塞 I/O (nonblocking IO)
  3. I/O 多路复用 (IO multiplexing)
  4. 信号驱动 I/O (signal driven IO)
  5. 异步 I/O (asynchronous IO)

信号驱动 I/O 在实际中并不常用。

阻塞 I/O 模型

在 Linux 中,默认情况下所有 socket 都是阻塞模型。
一个典型的读操作流程如下:

当用户进程调用了 resvfrom 这个系统调用,
内核就开始了 I/O 的第一个阶段:准备数据(对于网络 I/O 来说,很多时候数据在一开始还没到达。比如没有收到一个完整的 UDP 包,这个时候内核就需要等到全部的数据到来)。这个过程需要等待。也就是数据被拷贝到内核也需要一个过程。当内核一直等到数据准备好了,它就会将数据从内核拷贝到用户内存,然后内核返回结果,用户进程才接触 block 状态,重新运行。

阻塞 I/O 的特点是 I/O 执行的两个阶段都被阻塞了。

非阻塞 I/O

Linux 系统下,可以通过设置使 socket 变为非阻塞模式。当对一个非阻塞模式的 socket 进行读操作时,流程如下:

当用户进程发出 read 操作时,如果内核中的数据还没有准备好,那么它并不会阻塞住用户进程,而是立刻返回一个 error。从用户进程角度讲,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 error 时,他就直到数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户进程的系统调用,那么它就马上将数据拷贝到用户内存,然后返回。

非阻塞 I/O 的特点是用户进程需要不断的询问内核数据是否准备好了(轮询)。

I/O 多路复用

I/O 多路复用的具体实现就是常说的 select,poll,epoll, 有些地方也称这种 I/O 方式为事件驱动 I/O (event driven IO)。selet/epoll 的好处在于单个进程就可以同时处理多个网络连接的 I/O。它的基本原理就是 select,poll,epoll 这几个函数会不断的轮询自己负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。流程图如下:

当用户进程调用了 select,整个进程也会被阻塞。同时内核会监视所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,对应监视它的 select 就会返回。这个时候用户进程调用 read 操作,将数据从内核拷贝到用户进程。

I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select() 函数就可以返回。

从流程图来看,I/O 多路复用的模型和阻塞 I/O 模型没有太大的不同,事实上,I/O 多路复用的性能还更差一些,它需要使用两个系统调用,而阻塞 I/O 只需要一个。但是,I/O 多路复用的优势在于它可以同时处理多个连接。
所以如果处理连接数不是很高的话,使用 select/epoll 的web server 不一定比使用 multi-treading+ blocking IO 的web server 性能更好,可能延迟还更大。select/epoll 的优势并不是对于单个连接能处理的更快,而是在于能处理更多的连接。

在 I/O 多路复用的模型中,对于每个 socket 一般都设置成非阻塞,但是如上图所示,整个用户进程其实一直是被阻塞的,只不过是被 select 整个函数阻塞,而不是被 socket IO 给阻塞。

异步 I/O

Linux 下异步 I/O 其实用的很少。流程如下:

用户进程发起 read 操作之后立刻就可以去做其他事情。从内核角度,当它收到一个异步读的系统调用之后,首先会立刻返回,所以不会对用户进程产生任何的阻塞。然后内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核会给用户进程发送一个信号,告诉它读操作完成了。

总结

阻塞和非阻塞的区别

调用阻塞 I/O 会一直阻塞住对应的进程直到操作完成,而非阻塞 I/O 在内核还在准备数据的时候回立刻返回。

几个例子

用钓鱼来说明这四个 IO 模型
A 用的是老式鱼竿,得一直在旁边守着,等到鱼上钩了就拉杆。
B 的鱼竿有个功能,能够显示是否有鱼上杆,所以 B 可以一般看书一边钓鱼,时不时看看鱼竿有没有鱼上钩,有的话就去拉杆。
C 和 B 的方式是差不多的,但是通知器可以处理放了好几个鱼竿,所以它就放了很多鱼竿。一旦提醒有鱼上钩了,就去拉对应的杆就好了。
D 雇了一个人帮他钓鱼,一旦那个人把鱼钓上来了,就通知他。

同步 IO 和异步 IO 的区别

在说明同步 I/O 和异步 I/O 的区别之前,需要先给出两者的定义。POSIX的定义是这样的:

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
  • An asynchronous I/O operation does not case the requesting process tobe blocked;

两者的区别就在于 synchronous IO 做“IO operation”的时候会将进程阻塞。按照这个定义,之前所说的阻塞 I/O,非阻塞 I/O,IO 多路复用都属于同步 IO。

这里有个注意点,定义中的“IO operation”是指的真实的 IO 操作,就是 resvfrom 这个系统调用。非阻塞 IO 在执行 resvfrom 这个系统调用的时候如果内核数据没有准备好不会阻塞进程,但是内核数据准备好的时候,recvfrom 会将数据从内核拷贝到用户内存中,这个时候进程是被阻塞的。而异步 IO 这个时候进程也是非阻塞的。

各个模型比较如下:

I/O 多路复用之 select,poll,epoll 详解

见另一篇笔记 【网络 IO - 多路复用技术 select,poll,epoll】

参考
Linux IO模式及 select、poll、epoll详解
select、poll、epoll之间的区别总结[整理]