一、I/O是什么?
Linux里对数据流的操作叫I/O操作(Input and Output)。而Linux世界里一切都抽象成文件,文件实际就是一串二进制流。不管是管道、Socket、标准输入、标准输出等,在Linux看来都是文件,都是流,流的对象通过文件描述符标识,对文件描述符的读写操作,就等于对流的读写操作,就是我们说的I/O操作。随着互联网的发展,对I/O的要求越来越高,每个时期都有新的技术对I/O进行优化,这就是今天我们要聊的I/O的演进,看看以前的工程师是如何思考并处理问题,以后的I/O之路又会走向何方。
总的说I/O分为:磁盘I/O,网络I/O,内存I/O三种。因为内存很快,不会有问题,所以通常说的I/O指的是前两者。
二、著名的“C10K问题”
以前服务器并发的吞吐量很低,只能处理几十、几百并发。在2000年左右,互联网发展迅速,各类应用层出不穷,即时通信、实时互动等对网络通信需求越来越高,迫使技术更新,由此提出C10K问题,即单服务器处理1万个并发连接问题。在解决C10K问题的道路上,Linux内核I/O操作出现5次更新,对应5种I/O通信模型。
三、Linux内核的5种I/O通信模型
在此之前需要先了解阻塞、非阻塞,异步、同步的概念。在大部份场景下,同步意味着阻塞,异步意味着非阻塞,所以很多人搞混了。实际情况它们是有区别的,同步、异步关注的是消息通信机制,阻塞、非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。两两组合,实际上有同步阻塞、同步非阻塞、异步阻塞、异步非阻塞四种情况。
I/O的操作抽像的说分两步:等待数据,拷贝数据。主要问题是出在等待数据,要提高I/O的效率,就需要尽可能降低等待的时间。接下来我们看看每种模型是如何降低等待时间的。
1.阻塞I/O模型
比喻 | 一个人在钓鱼,当没鱼上钩时,就坐在岸边一直等 |
特点 | 在I/O执行的两个阶段(等待数据和拷贝数据)都被阻塞 |
典型应用 | 阻塞Socket,JavaBIO |
优点 | 1.进程阻塞挂起不消耗CPU资源,及时响应每个操作 2.实现难度低,开发应用容易 3.适合并发量小的网络应用开发 |
缺点 | 1.不适合并发量大的应用,因为每个请求I/O会阻塞进程 2.需要为每个请求分配一个处理进程以及时响应(或线程),系统开销大 |
2.非阻塞I/O模型
比喻 | 边钓鱼边玩手机,隔会再看看有没有鱼上钩,有的话就迅速拉杆 |
特点 | 用户进程需要不断地主动询问内核,数据准备好了没有 |
典型应用 | Socket设置为NON_BLOCK |
优点 | 实现难度低,开发应用相对阻塞I/O模型较难 |
缺点 | 1.进程轮询(重复)调用,消耗CPU的资源 2.适合并发量较小且不需要及时响应的网络应用开发 |
3.多路复用I/O模型
比喻 | 放了一堆鱼竿,在岸边一直守着这堆鱼竿,没鱼上钩就玩手机 |
特点 | 对于每一个Socket,一般都要设置成非阻塞,但是整个用户的进程其实是一直被阻塞的,只不过进程是被select函数阻塞,而不是被Socket I/O阻塞 |
典型应用 | Java NIO,Nginx(epoll,poll,select) |
优点 | 1.专一进程解决多个进程I/O的阻塞问题,性能好,Reactor模式 2.适合高并发服务应用开发,一个进程(或线程)响应多个请求 |
缺点 | 实现和开发应用难度较大 |
I/O多路复用中的 “多路” 是指同时监听多个打开的文件描述符,“复用” 是指复用一个进程(或线程)去监听这些打开的文件描述符。
最早期是select,存在监听的文件描述符有数量限制,内核态到用户态的描述符拷贝等问题,后面poll是对select优化,突破了描述符有数量的限制,但它还是存在内核到态用户态的拷贝等问题。后面出现的epoll的实现机制与select/poll机制完全不同,所以他们的缺点在epoll上不复存在。
特点 | select | poll | epoll |
---|---|---|---|
监听描述符个数 | 有上限 | 无上限 | 无上限 |
描述符从内核态到用户态的拷贝及从用户态到内核态的拷贝 | 需要 | 需要 | 不需要 |
调用后轮询检测事件是否发生 | 需要 | 需要 | 不需要 |
调用前是否需要重置监听的描述符状态(函数参数) | 需要 | 需要 | 不需要 |
4.信号驱动I/O模型
比喻 | 鱼竿上系了个铃铛,当铃铛响,就知道鱼上钩,然后可以专心玩手机 |
特点 | 并不符合异步I/O要求,只能算是伪异步,并且实际中并不常用 |
典型应用 | 应用场景较少 |
优点 | 应用较少,不做详细总结 |
缺点 | 实现和开发应用难度大 |
5.异步I/O模型
比喻 | 有个机器人帮你钓鱼,钓到鱼就告诉你,你只要在旁边专心玩手机 |
特点 | 真正实现了异步I/O,是五种I/O模型中唯一的异步模型 |
典型应用 | Java7 AIO,高性能服务器应用 |
优点 | 1.不阻塞,数据一步到位,采用Proactor模式 2.非常适合高性能、高并发应用 |
缺点 | 1.需要操作系统底层支持,Linux2.5内核首现,Linux2.6产品的内核标准特性 2.实现和开发应用难度大 |
6.各I/O模型总结
从上图可以看出,阻塞程度:阻塞I/O>非阻塞I/O>多路复用I/O>信号驱动I/O>异步I/O,越往后,阻塞越少,效率是由低到高的。
四、java的BIO,NIO,AIO
在操作系统提供5种I/O模型的前提下,java的网络编程也对应用实现了BIO、NIO、AIO,它们的出现的顺序跟操作系统的IO模型演进是一致的,因为就是依赖操作系统实现的。
1.BIO
同步阻塞式IO,简单理解:一个线程处理一个连接,发起和处理IO请求都是同步的。对应系统的I/O模型为:阻塞I/O模型
2.NIO
同步非阻塞IO,简单理解:一个线程处理多个连接,发起IO请求是非阻塞的但处理IO请求是同步的。对应用系统的I/O模型为:多路复用I/O模型
3.AIO
异步非阻塞IO,简单理解:一个有效请求一个线程,发起和处理IO请求都是异步的。对应用系统的I/O模型为:异步I/O模型
五、I/O对应的设计模式
设计模式代表了最佳的实践,在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
1.Reactor模式
2.Proactor模式
六、未来IO的可能方向
在现有Linux架构下收发报文必须采用硬中断来做通讯,每次硬中断大约消耗100微秒;数据在内核态用户态之间拷贝带来大量CPU消耗;多次系统调用的开销;到网卡经过的路径太长。随着并发要求不段提高,可以看到系统内核一直在改为,在原来的分层与封装的设计思想之上,又增加了直接访问内核的手段。I/O优化的未来,可能是去内核态的方式。跳过内核直接驱动网卡,自己实现数据的拆包与封包,充分理用硬件资源,从通用型服务器变成专用型服务器。现在DPDK(www.dpdk.org)已经基本实现这一想法。
参考:《Netty 4核心原理与手写RPC框架实战》谭勇德 著