IO多路复用

本文从操作系统的角度来解释BIO,NIO,AIO的概念,含义和背后的那些事 。本文主要分为3篇 。
第一篇:讲解BIO和NIO以及IO多路复用
第二篇:讲解磁盘IO和AIO
第三篇:讲解在这些机制上的一些应用的实现方式,比如nginx,,Java NIO等
到底什么是“IO Block”
很多人说BIO不好,会“block”,但到底什么是IO的Block呢?考虑下面两种情况:
如果你的直觉告诉你,这两种都算“Block”,那么很遗憾,你的理解与Linux不同 。Linux认为:
是的,对于磁盘文件IO,Linux总是不视作Block 。
你可能会说,这不科学啊,磁盘读写偶尔也会因为硬件而卡壳啊,怎么能不算Block呢?但实际就是不算 。
一个解释是,所谓“Block”是指操作系统可以预见这个Block会发生才会主动Block 。例如当读取TCP连接的数据时,如果发现 里没有数据就可以确定定对方还没有发过来,于是Block;而对于普通磁盘文件的读写,也许磁盘运作期间会抖动,会短暂暂停,但是操作系统无法预见这种情况,只能视作不会Block,照样执行 。
基于这个基本的设定,在讨论IO时,一定要严格区分网络IO和磁盘文件IO 。NIO和后文讲到的IO多路复用只对网络IO有意义 。
严格的说,和IO多路复用,对标准输入输出描述符、管道和FIFO也都是有效的 。但本文侧重于讨论高性能网络服务器下各种IO的含义和关系,所以本文做了简化,只提及网络IO和磁盘文件IO两种情况 。
本文先着重讲一下网络IO 。
BIO
有了Block的定义,就可以讨论BIO和NIO了 。BIO是 IO的意思 。在类似于网络中进行read,write,一类的系统调用时会被卡住 。
举个例子,当用read去读取网络的数据时,是无法预知对方是否已经发送数据的 。因此在收到数据之前,能做的只有等待,直到对方把数据发过来,或者等到网络超时 。
对于单线程的网络服务,这样做就会有卡死的问题 。因为当等待时,整个线程会被挂起,无法执行,也无法做其他的工作 。
顺便说一句,这种Block是不会影响同时运行的其他程序(进程)的,因为现代操作系统都是多任务的,任务之间的切换是抢占式的 。这里Block只是指Block当前的进程 。
于是,网络服务为了同时响应多个并发的网络请求,必须实现为多线程的 。每个线程处理一个网络请求 。线程数随着并发连接数线性增长 。这的确能奏效 。实际上2000年之前很多网络服务器就是这么实现的 。但这带来两个问题:
也许现在看来1GB内存不算什么,现在服务器上百G内存的配置现在司空见惯了 。但是倒退20年,1G内存是很金贵的 。并且,尽管现在通过使用大内存,可以轻易实现并发1万甚至10万的连接 。但是水涨船高,如果是要单机撑1千万的连接呢?
问题的关键在于,当调用read接受网络请求时,有数据到了就用,没数据到时,实际上是可以干别的 。使用大量线程,仅仅是因为Block发生,没有其他办法 。
当然你可能会说,是不是可以弄个线程池呢?这样既能并发的处理请求,又不会产生大量线程 。但这样会限制最大并发的连接数 。比如你弄4个线程,那么最大4个线程都Block了就没法响应更多请求了 。
要是操作IO接口时,操作系统能够总是直接告诉有没有数据,而不是Block去等就好了 。于是,NIO登场 。
NIO
NIO是指将IO模式设为“Non-”模式 。在Linux下,一般是这样:
void setnonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);