引言
前面写了HTTPS的介绍,步子跨的有点大,有点扯着了的不爽感。这里介绍相应的概念,为后面介绍JavaScript事件循环打个基础。
相关名词介绍
操作系统将设备抽象为文件,对设备的操作转换为对文件的读写,文件的读写操作结构称为文件描述符(Linux)或者文件句柄(Windows)。
操作系统内核对于I/O操作只有阻塞和非阻塞两种方式。
阻塞I/O特点是调用I/O操作之后一定要等到系统内核层面完成所有操作之后,调用才返回。比如读取磁盘文件操作,系统内核在完成磁盘寻到、读取数据、复制数据到内存操作之后,这个调用才结束。阻塞I/O造成CPU等待I/O,浪费等待时间,CPU闲置不能得到充分利用。
非阻塞I/O调用后会立即返回一个文件描述符,调用方的数据再通过文件描述符获取。非阻塞I/O返回后,CPU可以用来处理其他事物,CPU利用率明显提高。非阻塞I/O返回的是当前的调用状态,为了获取完整的数据,程序需要重复调用I/O操作来确认数据是否操作完成。这种重复判断操作是否完成的技术叫轮询。
同步和异步从应用程序的视角来看的状态。
同步操作是应用程序调用操作之后,在操作返回前一直处于等待状态,异步操作不必等待操作完成就可立即返回,应用程序在操作返回前可以继续执行其他操作,在异步操作完成后再处理异步操作的后续操作。
非阻塞调用与异步调用的差别是非阻塞调用会马上返回任何可用的数据,其所读的数据可以等于或少于所要求的,或为零,异步调用所要求的传输应完整地执行,但其具体执行可以是将来某个特定时间。
同步和阻塞有相似的等待特性,同步一定是阻塞的,但阻塞不一定都是同步的,也可以是异步的。
I/O模型
阻塞I/O模型
- 最常用的I/O模型,默认情况下,所有文件操作都是阻塞的
- 从I/O请求发起,到所有请求数据准备好或者出错前,请求发起者(应用程序)一直处于等待状态。
非阻塞I/O模型
- 发起I/O请求,能立即收到一个I/O状态
- 从I/O请求发起,到所有请求数据准备好或者出错之前,请求发起者(应用程序)一直处于不断查询的状态(轮询)。
I/O复用模型
- 如果一个或多个I/O条件满足(输入准备好,数据准备好等),我们就能被通知到的这种能力成为I/O复用。
- 应用程序将提供文件描述符,I/O状态的检测交给I/O复用模型,在I/O可用时接收相应的回调。
- 应用程序在数据准备好之前,处于等待I/O复用模型触发回调。
信号驱动I/O模型
- 需要开启Socket信号驱动I/O功能
- 通过系统调用执行一个信号处理函数(此系统调用立即返回,进程继续执行,非阻塞)
- 当数据准备就绪时,为该进程生成一个I/O信号,通过信号回调通知应用程序读取数据
异步I/O
- 应用程序请求操作系统内核启动某个操作,并在整个操作完成后通知应用程序
- 与信号驱动I/O的区别
- 信号驱动I/O由内核通知应用程序何时开始一个I/O(被通知去哪儿读数据)
- 异步I/O由内核通知应用程序I/O操作何时完成(被通知数据读取完成了)
I/O模型的实现
理想的非阻塞异步I/O
- 应用程序发起非阻塞调用,无需通过遍历或者事件唤醒等方式轮询,直接处理下一个任务
- 在I/O完成后通过信号或者回调将数据传输给应用程序。
- Linux 原生提供的(AIO)就是这种方式
- AIO仅支持内核I/O中的O_DIRECT方式读取,无法利用系统缓存
普遍的异步I/O实现
满足了非阻塞I/O确保获取完整数据的需求,从应用程序的角度看,还是同步的效果,应用程序还是需要等待I/O完全返回,在返回之前需要一直等待。等待过程中要么遍历文件描述符,要么休眠等待事件发生。下面看看I/O复用模型的发展状态,怎样使用阻塞的方式实现异步的效果。
I/O复用模型
- read
- 最原始、性能最低的方式
- 通过重复调用来检查I/O状态来完成完整的数据读取。
- 在得到最终数据前,CPU有效利用率不高(状态检查绝大部分是无效CPU)。
- select
- 标准的I/O复用模型,几乎所有的类Unix系统上都有实现
- 在read的基础上的一种改进方案
- 通过文件描述符上的事件状态来进行判断
- 采用一个1024长度的数组来存储状态,最低可同时维护1024个文件描述符(I/O操作状态)
- pull
- 比起select来说更加优化
- 采用链表的方式突破数组长度的限制
- 避免不需要的检查
- 在文件描述符较多的情况下性能较低(链表的查询效率低)
- epoll
- 是Linux 2.6+上提供的性能更好的I/O复用模型(事件通知机制)
- 利用了事件通知,执行回调的方式而不是遍历查询,所以CPU有效利用率高
- 在进入轮询的时候如果没有检查到I/O事件,将会进行休眠,直到事件发生将它唤醒
- 休眠过程中CPU还是闲置的,利用率还是不够高
- kqueue
- 在FreeBSD 4.1+,OpenBSD 2.9+,NetBSD 2.0 和MacOS X上特有的性能更好的I/O复用模型
- 类似于epoll
- 仅在FreeBSD系统下实现
- eventport
- 在Solaris 10单可用的高性能I/O复用模型
NodeJS的异步I/O方式
单线程同步I/O会因为阻塞I/O导致硬件资源得不到更优的使用;多线程编程模型也因为编程中的死锁、状态同步问题经常出问题。单线程异步是一个值得期待的方式。
- 使用多线程的方式,通过让部分线程进行阻塞I/O或者非阻塞I/O加轮询技术来熟悉数据获取
- 让一个线程进行计算处理,通过线程间的通信将I/O的到的数据进行传递
- 通过线程池和轮询的方式实现异步I/O的效果
- NodeJS在Windows下的异步I/O使用Windows自带的IOCP异步I/O方案
- Windows IOCP 调用异步方法,等待I/O完成的通知,执行回调,无需轮询。
参考资料
- 《深入浅出Node.js》
- 《Netty 权威指南》第二版
- 《深入剖析Nginx》
- 《Unix环境高级编程》第三版
- 《Unix网络编程》 卷一
- 《Windows核心编程》第五版
- 《操作系统概念》第七版
- 聊聊Linux 五种IO模型
- 高性能IO模型浅析
- 也谈IO模型