I/O Multiplexing -- Linux I/O 多路复用

文章还会涉及到同步 I/O,异步 I/O,阻塞 I/O 和非阻塞 I/O
首先咱们须要理解如下概念:编程

Linux用户态和内核态

在如今操做系统中,CPU一般会在两种不一样的模式下工做:网络

内核态

此模式下,程序代码可以彻底,无限制地访问底层硬件,可以执行任意的 CPU 指令和访问任意的内存地址。内核模式一般留给最底层的,受信任的系统函数来使用。程序在内核模式下崩溃是灾难性的,这甚至可使整台 PC 宕机。数据结构

用户态

在用户模式下,程序代码不可以直接访问硬件和内存。执行在用户态的代码必须委托系统函数去访问硬件和内存。由于有这种隔离机制的保护,程序在用户态下崩溃一般是可恢复的。PC 中大多数程序也是在用户态下执行。多线程

进程切换

指操做系统进程调度切换,从某个进程到另外的进程。切换过程须要保存当前进程的全部状态,包括寄存器状态,关联的内核状态,虚拟内存的配置等,具体会经历如下几个步骤:异步

  • 保存处理器上下文,包括程序计数器和其余寄存器
  • 更新进程控制块 (PCB)
  • 移动进程的 PCB 到合适的队列,例如就绪队列,事件阻塞队列
  • 选择其余进程,并更新他的 PCB
  • 更新内存数据结构
  • 恢复 PCB 上下文

进程阻塞

一个阻塞的进程一般是在等待某个事件,例如信号的释放或者消息的到达。在多任务的系统中,阻塞的进程会经过系统调用去通知调度器本身处于 wait 的状态,以便可以被移除出时序队列。进程若是在 wait 状态下还霸占 CPU 继续执行,这被称为 busy-waiting (空等?)。显然这是不合理的,由于他浪费了 CPU 时钟周期,这本来能够被其余进程使用。因此当一个进程进入了阻塞状态,不该继续占用 CPU 资源。socket

缓冲式 I/O

当咱们写数据(到文件系统),I/O 系统会累积数据到一个中间缓冲区,当缓冲区积累到足够数据时(或者调用flush())才会把数据发送到文件系统,这样减小了文件系统的访问次数。由于对文件系统(磁盘)的访问一般来讲开销很大(对比内存间的拷贝),缓冲式 I/O 可以有效提升性能,尤为是那种屡次的小数据量写操做。如果大数据量的写操做,非缓冲式 I/O 会更好,由于缓冲式 I/O 并不会显著减小(对文件系统)系统调用,却引入的额外的内存拷贝工做,这些数据拷贝操做带来了更高的 CPU 和内存开销。函数

文件描述符 (FD)

在 Unix 及其衍生的操做系统中,文件描述符 (FD) 是一个抽象的指示符 (原文:indicator, handle,多数文章翻译成句柄),用来访问文件或其余 I/O 资源,例如管道,socket等。FD 是 POSIX 编程接口的一部分,是个非负索引值,许多底层的程序都会使用到 FD性能

I/O 模型

当一个读操做发生,会经历如下两个阶段:大数据

  • 数据准备阶段 —— 例如等待网络数据到达,当数据包到达时,他们会拷贝到内核缓冲区中
  • 数据转移阶段 —— 把数据从内核拷贝到用户进程

由于这两个阶段的存在,Linux 提供了如下5种 I/O 模型spa

Blocking I/O Model —— 阻塞式 I/O

阻塞式 I/O 是最多见的 I/O 模式,默认地,全部的 socket 都是阻塞式的
figure_6.1.png
这里咱们用 UDP 协议和 recvfrom 系统调用来举例。上图中,进程调用了 recvfrom,系统函数在有数据报到达并已经拷贝到应用程序缓冲区时,或者有错误发生时才会返回(最多见的错误是被信号中断)。咱们认为进程在 recvfrom 从调用到返回的整个阶段都被阻塞了。当 recvfrom 成功返回,应用程序才会去处理数据报。

Nonblocking I/O Model —— 非阻塞式 I/O

若是一个(数据准备阶段) I/O 调用没有完成,内核会当即返回一个错误标记,而不是阻塞这个进程
figure_6.2.png
第一次调用 recvfrom 时并无数据到达,因而内核当即返回了错误标记 EWOULDBLOCK
第四次调用 recvfrom 时数据报已经到达,拷贝到应用程序缓冲区后,recvfrom 成功返回,以后程序会处理这些数据。
像这样,程序在一个非阻塞的 FD 上循环调用 recvfrom 被称为轮询。这一般会浪费 CPU 时钟周期,但这种模型也会偶尔使用到,例如一个系统只专一于某个功能的时候。

I/O Multiplexing Model —— I/O 多路复用

在 Linux I/O 多路复用模型,咱们会阻塞在 select, poll, epoll 这些系统函数中,而不是阻塞在真正的 I/O 调用上。
figure_6.3.png
上图中,咱们阻塞在 select() 函数上,等待 socket 数据可读。select() 返回则表示 socket 数据可读,以后咱们才调用 recvfrom 拷贝数据到应用程序缓冲区

  • 缺点:这里咱们使用了两次系统调用 (select 和 recvfrom),而阻塞式 I/O 只使用了一次 recvfrom
  • 优点:咱们能够监听多个 FD 是否就绪

I/O 多路复用模型与阻塞式 I/O 模式很是类似。阻塞式 I/O 使用多线程(每一个线程负责一个 FD)且每一个线程均可以很自由地调用(阻塞式)系统函数 recvfrom,而非使用 select 负责监听多个 FD。

Signal-Driven I/O Model —— 信号驱动 I/O

告诉内核当某个 FD 就绪时,释放 SIGIO 信号来通知应用程序
figure_6.4.png
咱们首先让 socket 使用信号驱动 I/O 模式,并使用 sigaction 系统函数注册一个信号处理器 (signal handler),该系统函数当即返回,这是非阻塞的。

当数据可读,SIGIO 信号释放出来被进程接收到,咱们能够进行以下操做之一

  • 在 signal handler 中调用 recvfrom 读取数据,随后通知主循环数据已经准备好了
  • signal handler 通知主循环去读取数据

该模型的优点在于,等待数据到达的阶段不会阻塞,主循环能够继续执行其余任务并等待 signal handler 的通知(数据可读或可处理)

Asynchronous I/O Model —— 异步 I/O

异步 I/O 模型告诉内核执行 I/O 操做,等到整个 I/O 操做(包括数据准备阶段和数据转移阶段)完成后再通知咱们。该模式跟信号驱动 I/O 很是类似,主要的区别是:信号驱动 I/O 中,内核通知进程 I/O 操做能够开始(仍需把数据从内核拷贝到进程),而异步 I/O 中内核通知咱们 I/O 操做已经完成(数据已经在进程缓冲区中)
figure_6.5.png
咱们调用了系统函数 aio_read,向内核传递了如下信息:

  • FD, 缓冲区指针,缓冲区大小
  • 文件偏移量
  • I/O 执行完毕的通知方式

aio_read 会当即返回,进程在等待 I/O 操做完成的整个阶段都不会被阻塞。

I/O 模型的比较

figure_6.6.png

前面4种模型的主要区别在第一个阶段(数据准备阶段),第二阶段(数据转移阶段)是同样的:进程都阻塞在数据转移阶段(从内核拷贝到应用程序缓冲区)。异步 I/O 内核负责这两个阶段,不须要应用程序干预。

同步 I/O 和异步 I/O

POSIX 定义以下

  • 同步 I/O 会致使请求 I/O 操做的进程阻塞,直到 I/O 操做完成
  • 异步 I/O 不会致使请求 I/O 操做的进程阻塞

根据这些定义,前面4种 I/O 模型 (blocking, nonblocking, I/O multiplexing, and signal-driven I/O) 都是同步 I/O,由于实际的 I/O 操做都会阻塞进程(举例:信号驱动 I/O 在等待数据时非阻塞,但在数据转移时阻塞了,是同步 I/O),只有异步 I/O 模型符合定义。

select, poll, epoll

施工中

相关文章
相关标签/搜索