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是一种这样的模式,它要求主线程负责监听文件描述符是否有事件发生,有的话就将事情发送给工作线程. 所以这里可以看到几个特点:
总结
同步和异步是类库,框架或者语言层面的事物,比如张三开发了一个类库对I/O多路复用进行了封装,那么我们可以说这个类库支持异步I/O.
阻塞和非阻塞是操作系统进程层面的事物。
Linux I/O模型的发展历程围绕了两个要素
- 让应用程序尽可能的多做事情
- 让CPU尽可能的充分利用