Linux的I/O模型

I/O顾名思义就是输入输出,I/O设备可以指网卡,键盘,打印机等,在这里为了方便讨论,I/O专门指网络设备.
之前也看过不少专门讲I/O的帖子和文章,其中有不少帖子有误导的嫌疑,比如打一些不恰当的比喻,所以我有了一种想重新梳理一下I/O模型的冲动。

  • 背景
  • 一些预备知识
  • 同步阻塞
  • 同步非阻塞
  • I/O多路复用
  • 异步非阻塞I/O
  • 其他
  • 总结

背景

既然是模型,说明为了解决一个特定的问题. 那么I/O模型是为了解决什么问题呢?首先排除网络传输问题,那么只能是网卡接受到数据之后,如何给程序消费这个问题了。
程序A想要请求网络数据,程序B也要请求网络数据,以此类推,很有很多程序干类似的事情。

一个程序请求网络数据,会牵涉到四方:

  • 程序本身
  • CPU
  • 操作系统
  • 内存

根据之前提到的”又快又好”的指标,如果这四方都能在程序请求网络数据的过程中,让自身的效率达到最有,那么I/O模型的目的就达到了
是的,这也是I/O模型的终结目的.

一些预备知识

用户空间和内核空间

为了保证内核数据的安全,操作系统将寻址空间分为内核空间和用户空间.可以想象一下,一家公司组织去看文艺演出,前排坐的都是领导,后排坐的都是员工,前排的区域是核心区域.

进程的阻塞

一个进程在执行的过程中,需要等待某件事情的发生,才能继续执行,那么进程有两种选择

  • 占用CPU
  • 不占用CPU

如果进程选择不占用CPU, 那么意味着进程被阻塞了,进入等待状态。
如果进程自己等待某件事情的发生,又不释放CPU的使用权,那么这个进程是自私的。

页缓存 - Page Cache

Linux内核为文件提供了一个缓存,换言之, 从网卡进来的数据,先放到内核的缓存区。接下来的事情就很自然:数据会在在某个场合下拷贝到应用程序的地址空间.

同步和异步的区别

要区分同步和异步,就选取一个好的视角,这个视角就是应用程序. 现在一个程序A, 程序B, 网络请求接口getUser,这个接口执行的时间耗时不确定.

  • 程序A 调用 getUser, 等待了t时间之后,得到了结果,然后继续执行剩下的逻辑, 这就是同步调用.
  • 程序B 调用 getUser,继续执行剩下的逻辑,过t时间之后,以某种方式(回调函数)拿到了结果,这就是异步调用.

在这里,没有操作系统的概念,没有CPU的概念,没有内存的概念,可以看到这是一种调用方式,这种调用方式可以被框架来实现。
而且可以看得出,同步调用符合开发者的认知习惯,因为代码的书写顺序就是代码的执行顺序,所以同步更人性化。默认情况下,我们写的代码就是同步的.

同步阻塞

同步阻塞就是一个进程在等待某件事情发生的时候,让自己进入等待状态,让出CPU的使用权,待数据

  • 从网卡到内核区
  • 然后从内核区和程序地址空间
    再唤醒进程。在这里可以看到进程做了两件事情
  • 干等
  • 让出了CPU
    所以可以看到进程还是让自己充实点的,在等的期间可以干点别的事情。

同步阻塞的优点

实时性好.符合用户的认知模型.

同步阻塞的缺点

效率不高

同步非阻塞

同步非阻塞是同步阻塞的优化,也就是进程别干等了,干点别的事情. 但程序需要每隔一段时间轮训数据有没有达到内核区域,如果达到了内核区域,就将内核区域的数据拷贝到应用程序地址空间,在这拷贝的过程中,进程是被阻塞的.
所以非阻塞强调的是数据从网卡达到内核区这个过程.
同步非阻塞的优点

进程不是纯粹干等了,可以去干点别的事情.

同步非阻塞的缺点

实时性差。因为需要通过轮询才能拿到数据,因为数据有可能在两次轮询间隔期间已经准备就绪了。轮询是有代价的,也需要消耗CPU

I/O多路复用

上面提到了同步非阻塞的缺点就是需要轮询CPU拿到结果,轮询是有代价的.
有两个进程采用同步非阻塞,那么两个进程都需要轮询,如果有n个进程呢?那么n个进程都需要轮询,这种效率是低下的。 如何改进这种效率呢?
一个可行的思路是让一个东西(暂且称为X)统一管理轮询。
统一管理是一种哲学,比如线程池是统一管理线程,连接池是统一管理连接.
Unix下面的select, poll, epoll就是做类似的事情的。

  • select
    • POSIX规定的
    • 调用select函数之前需要将文件描述符从用户态拷贝到内核态.
    • 调用过程是: 应用程序 -> select(轮询) -> I/O数据,在应用程序拿到I/O数据之前,应用程序一直是被select阻塞的,就这样看来,它不比同步阻塞调用高明.
    • 切换一个角度:如果有好多应用程序,那么这个优势就很明显,读取数据由select统一管理. 所以select的特长是处理更多的连接.
  • poll
    • 本质上和select是一样的,但是select的加强版
    • select的文件描述符列表是有限制的,而poll是没有限制的
  • epoll - Linux特有的, 在Linux 2.6 引入.
    • 文件描述符放在内核的一个事件表中,这个事件表是基于红黑树的实现的.
    • 基于事件驱动的I/O机制,只关注有I/O事情发生的文件描述符
    • 相比与select/poll, epoll性能更高
    • epoll的实现是基于Reactor模式

观察select, poll和epoll, 会有这样一条线索

  • 如何又快又好的管理文件描述符
    • 用一种高效的数据结构 - 红黑树
    • 避免文件描述符在用户态和内核态之间的移动
    • 不主动去监控文件描述的变化,而是文件描述符有变化的时候,主动通知消费方 - 好莱坞原则

异步非阻塞I/O

回头来看看同步非阻塞I/O的缺点

  • 进程需要轮询才能拿到结果
  • 数据内核区拷贝到程序地址空间的过程中程序被阻塞了

如果能避免上面那两个问题是不是就完美了? 是的.
异步I/O的宏观视角:

  • 程序A 调用 getUser, 继续执行剩下的逻辑, t时间之后,以某种方式(通常是回调)获取结果。

其他

windows IOCP

libevent

libevent 是一个基于事件驱动的异步I/O库

libuv

libuv也是一个基于事件驱动的异步I/O库,主要用在node.js上.
在linux上, libuv是基于epoll.
在windows上, libuv是基于IOCP

Reactor 模式


Reactor的英文原意是核反应堆,一个核反应堆可以提供很强的能量.
Reactor是一种这样的模式,它要求主线程负责监听文件描述符是否有事件发生,有的话就将事情发送给工作线程. 所以这里可以看到几个特点:

  • 将线程分为两大职责,一种是管理者,一种工作者
  • 管理者负责信息的收集,然后将信息分发给工作者。从实现的角度,体现了事件驱动。从原则的角度,体现了好莱坞原则.

    Proactor 模式

总结

同步和异步是类库,框架或者语言层面的事物,比如张三开发了一个类库对I/O多路复用进行了封装,那么我们可以说这个类库支持异步I/O.
阻塞和非阻塞是操作系统进程层面的事物。
Linux I/O模型的发展历程围绕了两个要素

  • 让应用程序尽可能的多做事情
  • 让CPU尽可能的充分利用