网络&操作系统-linux五种IO模型

用户空间和内核空间

先来将Linux,为了保证内核的安全,操作系统将虚拟空间分为两部分。内核空间和供各个进程使用的用户空间。

进程切换&进程阻塞

进程切换:为了调配各个进程的运行,cpu必须有能力挂起在其上运行的进程,并恢复之前挂起的某个进程的运行。这其中发生了: 1. 保存处理机上下文,包括程序计数器和其他寄存器。 2. 更新PCB信息。 3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。 4. 选择另一个进程执行,并更新其PCB。 5. 更新内存管理的数据结构。 6. 恢复处理机上下文。 总之是十分的耗费资源。

进程阻塞:正在执行的进程,由于期待的某些事情未能发生,如系统资源请求失败,新数据尚未到达,或者无新工作等,则由系统自动执行阻塞原语(Block)。这是进程自身的一种行为。进入阻塞后,是不占用CPU的。

这里插播一下进程的有关概念。在类Unix系统中,进程是由进程控制块(process control block,PCB),进程执行的程序,进程执行时的所用数据,进程运行使用的工作区组成。 PCB记录了操作系统所需的用于描述进程当前情况以及控制进程运行的全部信息。是进程存在的唯一标识。 OS是根据PCB来对并发执行的进程进行控制和管理的.

文件描述符(File descriptor)

指向文件的引用

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。

I/O

缓存I/O

大多数文件系统的默认I/O操作都是缓存I/O。此机制下,数据先被拷贝到内核的缓冲区中,然后才会从缓冲区拷贝到应用程序的地址空间。 缺点:内存开销极大

Linux I/O模型

网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。

当发生一个read操作时,会经历两个阶段:

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

对socket流而言:

  1. 通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。

  2. 把数据从内核缓冲区复制到应用进程缓冲区。

因为这两个阶段,linux产生了以下五种I/O网络模式 - 阻塞 I/O(blocking IO) - 非阻塞 I/O(nonblocking IO) - I/O 多路复用( IO multiplexing) - 信号驱动 I/O( signal driven IO) - 异步 I/O(asynchronous IO)

socket 套接字

先来复习一下套接字,这是计算机网络中进程间数据流的端点,也是操作系统提供的进程间通讯机制 在操作系统中,通常为应用程序提供一组API,成为套接字接口。应用程序可以通过这个来使用网络套接字进行数据交换。 在套接字接口中,IP地址通信端口组成套接字地址(socket address)。本地与远程的套接字地址完成连线后再加上使用的协议(protocol),这个五元组称为套接字对。之后就可以彼此交换数据。 e.g 在同一台计算机上,TCP和UDP协议可以使用同一个port而互不干扰,是因为五元组中的协议不同。

同步阻塞I/O (blocking IO)

在linux中,默认所有的socket都是blocking。

  1. 用户进程进行了系统调用,kernel开始IO第一阶段,准备数据。(对于很多网络IO来说,很多时候数据一开始都没有到达。所以这个时候kernel就需要等待)
  2. 用户进程这边整个进程就会被阻塞(自己选择阻塞)。
  3. kernel数据准备好了之后,会将数据从kernel中拷贝到用户内存。
  4. 用户进程解除block状态,重新运行。

    blocking IO的特点就是在IO执行的两个阶段都被block了

优点

能及时返回数据 无延迟 对内核开发者来说省事

缺点

对用户来说要付出性能的代价

同步非阻塞IO (nonblocking IO)

过一会瞄一眼进度条 轮询(polling)方式 可以通过设置socket变为non-blocking。 在网络IO时候,非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不一样,”非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会 ‘被’ CPU光顾”。

  1. 当用户进程发起read操作时,如果kernel中的数据未准备好,会立刻返回一个error
  2. 用户知道没准备好,先干会别的事情。再次发送read操作。
  3. 如果还没准备好,内核还会返回一个error。
  4. 一旦内核准备好了,就会把数据拷贝到用户内存然后返回。
  5. 需要注意拷贝数据整个过程,进程仍然是属于阻塞的状态 nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有

相比于同步阻塞 优点

能够在任务完成时间内干其他活

缺点

任务完成的响应延迟增大了,任务可能在两次轮询之间完成。减少了整体数据的吞吐量。

IO多路复用(IO multiplexing)

同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,而 “后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,做的事情是一样的)。

IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。

select调用是内核级别的。select和poll调用之后,内核会监视到达的数据。同时会阻塞监视的一批进程。有数据可以读写时就会调用IO操作函数。

select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。(select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。同时系统开销小。)

IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。归为同步阻塞模型

主要应用场景

  1. 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。
  2. 服务器需要同时处理多种网络协议的套接字。

到此为止,前三种IO模式,是用户进程进行系统调用时,处理方式不一样。直接等待,轮询,select或poll轮询。两个阶段过程:

  • 第一个阶段有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。
  • 第二个阶段都是阻塞的。
  • 从整个IO来看,都是顺序进行的,可以归为同步模型。都是进程主动等待且向内核检查状态(向存储设备或者网络)。

信号驱动式IO(signal-driven IO)

首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。 数据准备阶段的异步非阻塞

异步非阻塞IO(asynchronous IO)

相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知(通过signal或者一个基于线程的回调函数)。IO两个阶段,进程都是非阻塞的。 如果进程在忙着做一般的其他事,一般是强行打断,将时间登记一下放进队列,然后返回进程原来在做的事。 如果进程正在内核态忙着做其他别的事,例如以同步阻塞方式读写磁盘,只能把通知挂起,等内核态事情忙完了,快回用户态时,再触发通知。 如果进程被挂起,无事可做sleep了,就把这个进程唤醒,下次有cpu空闲就会调度到这个进程。

异步阻塞IO

通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

reference

聊聊Linux 五种IO模型

Table of Contents