异步I/O与非阻塞I/O

关于Node的介绍,时常会有异步、非阻塞、回调、事件这些术语混合一起推介出来,异步和非阻塞似乎是同一回事,从实践效果来看,异步和非阻塞都达到了并行I/O的目的。但是,从计算机内核I/O而言,异步/同步和阻塞/非阻塞实际上是两码事。

操作系统对异步I/O的支持

操作系统内核对于I/O只有两种方式:阻塞与非阻塞。

在调用阻塞I/O时,应用程序需要等待I/O完成才返回结果,也就是调用之后一定要等到系统内核层面完成所有操作后,调用才结束。如下图所示:

阻塞I/O以读取磁盘上的一段文件为例,系统内核完成磁盘寻道、读取数据、复制数据到内存之后,这个调用才结束。阻塞I/O造成CPU等待I/O,浪费等待时间,CPU的处理能力得不到充分利用。为了提高性能,内核提供了非阻塞I/O。

非阻塞I/O跟阻塞I/O的差别为调用之后会立即返回,如下图所示:

非阻塞I/O返回之后,CPU的时间片可以用来处理其他事务,此时的性能提升是明显的。

但非阻塞I/O也存在一些问题。由于完整的I/O并没有完成,立即返回的并不是业务层期望的数据,而仅仅是当前调用的状态。为了获取完整的数据,应用程序需要重复调用I/O操作来确认是否完成。这种重复调用判断操作是否完成的技术叫做轮询。

任意技术都并非完美的。阻塞I/O造成CPU等待浪费,非阻塞带来的麻烦却是需要轮询去确认是否完全完成数据获取,它会让CPU处理状态判断,是对CPU资源的浪费。轮询技术是一步步演进的,以减小I/O状态判断的CPU损耗。
现存的轮询技术主要有:

  • read
  • select
  • poll
  • epoll
  • kqueue

1,read

read轮询是最原始、性能最低的一种,通过重复调用来检查I/O的状态来完成完整数据的读取。**在得到最终数据前,CPU一直耗用在等待上。**如下:

2,select

select是在read的基础上改进的一种方案,通过对文件描述符上的事件状态来进行判断。select轮询具有一个较弱的限制,那就是由于它采用一个1024长度的数组来存储状态,所以它最多可以同时检查1024个文件描述符。如下:

### 3,poll poll轮询,该方案较select有所改进,采用链表的方式避免数组长度的限制,其次它能避免不需要的检查。但是当文件描述符较多的时候,它的性能还是十分低下的。如下:它与select相似,但性能限制有所改善。

4,epoll

epoll轮询是Linux下效率最高的I/O事件通知机制,在进入轮询的时候如果没有检查到I/O事件,将会进行休眠,直到事件发生将它唤醒。它是真实利用了事件通知、执行回调的方式,而不是遍历查询,所以不会浪费CPU,执行效率较高。如下图:

5,kqueue

kqueue轮询的实现方式与epoll类似,不过它仅在FreeBSD系统下存在。

轮询技术满足了非阻塞I/O确保获取完整数据的需求,但是对于应用程序而言,它仍然只能算是一种同步,因为应用程序仍然需要等待I/O完全返回,依旧花费了很多时间来等待。等待期间,CPU要么用于遍历文件描述符的状态,要么用于休眠等待事件发生。结论是它不够好。

理想的非阻塞异步I/O

尽管epoll已经利用了事件来降低CPU的耗用,但是休眠期间CPU几乎是闲置的,对于当前线程而言利用率不够。

我们期望的完美的异步I/O应该是应用程序发起非阻塞调用,无须通过遍历或者事件唤醒等方式轮询就可以直接处理下一个任务,只需在I/O完成后通过信号或回调将数据传递给应用程序即可。如下图:

幸运的是,在Linux下存在这样一种方式,它原生提供的一种异步I/O方式(AIO)就是通过信号或回调来传递数据的。

但不幸的是,只有Linux下有,而且它还有缺陷——AIO仅支持内核I/O中的O_DIRECT方式读取,导致无法利用系统缓存。

现实的异步I/O

现实比理想要骨感一些,但是要达成异步I/O的目标,并非难事。

前面我们将场景限定在了单线程的状况下,多线程的方式会是另一番风景。通过让部分线程进行阻塞I/O或者非阻塞I/O加轮询技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信将I/O得到的数据进行传递,这就轻松实现了异步I/O(尽管它是模拟的),如下图:

libev的作者Marc Alexander Lehmann实现了一个异步I/O的库:libeio。libeio实质上是采用 线程池与阻塞I/O 模拟异步I/O。最初,Node在*nix平台下采用了libeio配合libev实现I/O部分,实现了异步I/O。在Node v0.9.3中,自行实现了线程池来完成异步I/O。

另一种异步I/O方案是Windows下的IOCP,它在某种程度上提供了理想的异步I/O:**调用异步方法,等待I/O完成之后的通知,执行回调,用户无须考虑轮询。**但是它的内部其实仍然是线程池原理,不同之处在于这些线程池由系统内核接手管理。

IOCP的异步I/O模型与Node的异步调用模型十分近似。在Windows平台下采用了IOCP实现异步I/O。

由于Windows平台和*nix平台的差异,Node提供了libuv作为抽象封装层,使得所有平台兼容性的判断都由这一层来完成,并保证上层的Node与下层的 自定义线程池 及 IOCP 之间各自独立。Node在编译期间会判断平台条件,选择性编译unix目录或是win目录下的源文件到目标程序中,其架构如下:

这里的I/O不仅仅只限于磁盘文件的读写。*nix将计算机抽象了一番,磁盘文件、硬件、套接字等几乎所有计算机资源都被抽象为了文件,因此这里描述的阻塞和非阻塞的情况同样能适合于套接字等。

需要强调的是我们时常提到Node是单线程的,这里的单线程仅仅只是JavaScript执行在单线程中罢了。在Node中,无论是*nix还是Windows平台,内部完成I/O任务的另有线程池。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值