某某茶叶有限公司欢迎您!
金沙棋牌在线 > 服务器&运维 > Linux IO 模型

Linux IO 模型

时间:2020-03-22 23:21

概念说明

用户空间与内核空间

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

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。

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

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

进程的阻塞

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

文件描述符

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

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

缓存 IO

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

缓存 IO 的缺点:

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

引言

本文我们主要了解一下Unix/Linux下5种网络IO模型:blocking IO, nonblocking IO, IO multiplexing, signal driven IO, asynchronous IO以及select/poll/epoll的基本原理,更好的理解在高级语言中的异步编程,但以理解概念为主,并不会涉及到具体的C语言代码编写,如果想要深入的朋友建议阅读Richard Stevens的Unix Network Programming。

缓冲区

缓冲区的引入是为了减少频繁I/O操作而引起频繁的系统调用,当你操作一个流时,更多的是以缓冲区为单位进行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。

Linux下一切皆文件,连外部设备都当作文件;对文件的读写操作会调用内核系统命令,返回文件描述符,简称fd;而对socket的读写操作也会有描述符,socketfd。描述符是一个数字,指向内核中的结构体(文件路径、数据区)。IO操作其实是针对这些内核数据区的读写。

同步与异步 & 阻塞与非阻塞

在进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式,先理解一些概念性的东西。

1.同步与异步

同步与异步同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

典型的异步编程模型比如Node.js。

2016.4.17更新:

POSIX对这两个术语的定义:

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

2. 阻塞与非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

写在前面

为了更好的理解下面提到的Linux下5种网络IO的概念,我们还是有必要先理清几个概念。

几个基本概念

同步 IO VS 异步 IO 的区别:

  • 同步 IO:数据访问的时候进程会阻塞
  • 异步 IO:数据访问的时候进程不会阻塞

阻塞 IO 和非阻塞 IO 的区别:

  • 阻塞 IO:应用程序的调用不会立即返回
  • 非阻塞 IO:应用程序的调用会立即返回

同步 IO 包括:

  • 阻塞 IO Blocking IO
  • 非阻塞 IO NonBlocking IO
  • 多路复用 IO Multiplexing IO
  • 信号驱动 IO Signal Driven IO

阻塞IO

所有的IO操作都是阻塞的,比如recvfrom操作,指到数据包到达并且从内核数据区拷贝到应用数据区或者发生错误才会返回。期间整个线程会被阻塞。

关于阻塞/非阻塞 & 同步/异步更加形象的比喻

老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

  1. 老张把水壶放到火上,立等水开。(同步阻塞) 老张觉得自己有点傻

2. 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞) 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。

  1. 老张把响水壶放到火上,立等水开。(异步阻塞) 老张觉得这样傻等意义不大

4. 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞) 老张觉得自己聪明了。

所谓同步异步,只是对于水壶而言。普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

所谓阻塞非阻塞,仅仅对于老张而言。立等的老张,阻塞;看视的老张,非阻塞。情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

1.程序空间与内核空间

在Linux中,对于一次读取IO的操作,数据并不会直接拷贝到程序的程序缓冲区。它首先会被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的缓冲区。p.s: 最后一句话非常重要,重复一遍。

  1. <code>Waiting for the data to be ready</code>(等待数据到达内核缓冲区)
  2. <code>Copying the data from the kernel to the process</code>(从内核缓冲区拷贝数据到程序缓冲区)

阻塞 IO Blocking IO

Linux 下默认所有的 Socket 都是阻塞 IO

阻塞 IO 分为两个步骤:

  • 步骤 1. 等待数据准备,拷贝到 OS 内核缓存区 (该过程中应用程序进程都会被阻塞)
  • 步骤 2. 从 OS 内核缓存区拷贝到应用程序的地址空间 (该过程中应用程序进程都会被阻塞)

基本步骤如下,其中包括一次系统调用 recvfrom:

图片 1

Blocking IO

非阻塞IO

跟阻塞IO相反,所有的IO操作都立即返回,但是不保证每次都能顺利完成IO操作(比如recvfrom操作,如果没有数据可读,也会立即返回),因此需要通过轮询的方式确保IO操作完成。

Linux下的五种IO模型

  • 阻塞IO(blocking IO)
  • 非阻塞IO (nonblocking IO)
  • IO复用(select 和poll) (IO multiplexing)
  • 信号驱动IO (signal driven IO (SIGIO))
  • 异步IO (asynchronous IO (the POSIX aio_functions))

前四种都是同步,只有最后一种才是异步IO。

2.阻塞与非阻塞

阻塞就是说我们某一个请求不能立即得到返回应答,否则就可以理解为非阻塞。

非阻塞 IO NonBlocking IO

可以将 Socket 设置为非阻塞 IO,例如 Java NIO 中可以设置 SocketChannel:channel.configureBlocking(false);

在上述步骤 1 等待数据的过程中,应用程序进程不会被阻塞,而是不断询问 OS 内核数据有没有准备好:

  • 如果数据没有准备好,OS 内核返回一个 error,应用程序进程过一段时间再次询问(该过程中应用程序进程不会被阻塞)
  • 如果数据已经准备好,则进入上述步骤 2,将数据从 OS 内核缓存区拷贝到应用程序的地址空间(该过程中应用程序进程都会被阻塞)

基本步骤如下,其中包括多次系统调用 recvfrom:

图片 2

NonBlocking IO

IO复用

不管是上述的阻塞还是非阻塞,都是针对单描述符来说的,都会导致当前进程/线程的占用(阻塞或者轮询消耗),不能处理其他描述符。而IO复用就是解决整个问题出现的,将多个IO的阻塞复用到同一个select阻塞上。

select/poll:其实就是维护一个fd列表,通过顺序扫描的方式来找出就绪的fd并作对应的IO操作。select缺陷是fd列表大小有限制,取决于内核的FD_SETSIZE,默认是1024;而且在fd列表很大的情况下,顺序扫描的性能也堪忧。

epoll:复用的原理跟select/poll类似,但采用了事件驱动的方式,当fd就绪,则直接调用对应的回调。通过这种方式,来取代顺序扫描下,fd列表大小的增加导致性能直线下降;并且epoll的fd数目是操作系统的最大文件描述符数,而不是FD_SETSIZE。

阻塞IO模型

在这个模型中,应用程序(application)为了执行这个read操作,会调用相应的一个system call,将系统控制权交给kernel,然后就进行等待(这其实就是被阻塞了)。kernel开始执行这个system call,执行完毕后会向应用程序返回响应,应用程序得到响应后,就不再阻塞,并进行后面的工作。

3.同步IO与异步IO

这里先直接引用Stevens(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 cause the requesting process to be blocked.

对于同步与异步,我们可以用一个简单的生活场景来描述。当我们排队在实体店买东西可以视作同步,而网购则可以视作异步。实体店排队这种同步情形显然是非常的浪费时间,等待的这段时间我们被阻塞住了不能干其他的事情,而网购只要我们提交一下订单之后其他什么都不用管了,商品到了,快递员给我们发送一个信号(打电话)我们直接到门口去拿,等待的这段时间我们可以用来撸代码。

p.s: 等你阅读完文章的后面部分,回过头来看异步其实就是将等待的这段时间去处理IO操作,把CPU(我们的大脑)让出来做其他更有价值的事情(撸代码),而不是像同步那样去傻傻地排队。更加详细准确的定义可以在阅读完本文后面部分后参考维基百科。

多路复用 IO Multiplexing IO

  • 单个进程可以同时处理多个网络连接的 IO,即监听多个端口的 IO
  • 适用于连接数很高的情况
  • 实现方式:select,poll,epoll 系统调用
    • 注册多个端口的监听 Socket,比如 8080,8081
    • 当用户进程调用 select 方法后,整个用户进程被阻塞,OS 内核会监听所有注册的 Socket
    • 当任何一个端口的 Socket 中的数据准备好了( 8080 或者 8081),select 方法就会返回
    • 随后用户进程再调用 read 操作,将数据从 OS 内核缓存区拷贝到应用程序的地址空间。
  • 多路复用 IO 类似于 多线程结合阻塞 IO
    • 要实现监听多个端口的 IO,还可以通过多线程的方式,每一个线程负责监听一个端口的 IO
    • 如果处理的连接数不是很高的话,使用 多路复用 IO 不一定比使用 多线程结合阻塞 IO 的服务器性能更好,可能延迟还更大
    • 多路复用 IO 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接

多路复用 IO Multiplexing IO 的优点:

  • 对于耗时短的处理场景高效
  • OS 可以在多个事件源上等待,避免多线程结合阻塞 IO 方式带来的复杂度及性能开销
  • 事务分离,将与应用无关的多路分解和分配机制与应用相关的回调函数分离开

多路复用 IO Multiplexing IO 的缺点:

  • 处理耗时长的操作会造成事务分发的阻塞,影响后续事件的处理。

基本步骤如下,其中包括两次系统调用 select 和 recvfrom:

图片 3

Multiplexing IO

具体的使用,可以参见 Java NIO Buffer, Channel 及 Selector 中所述的 Selector 选择器。

信号驱动IO

开启socket信号驱动IO功能,通过系统调用sigaction注册信号处理函数,当IO就绪了,通过信号触发信号处理,通知应用程序进行IO操作。