IO之同步、异步、阻塞、非阻塞 (2)
[原创链接: http://www.smithfox.com/?e=191, 转载请保留此声明, 谢谢! ]
I/O Model 是一个很大的话题, 也是一个实践性很强的事情, 网上有各种说法和资料, 我们必须用辩证的态度去看待(包括本Blog :) ), 因为有的信息是过时的, 有些则可能是未经实践的片面的理解.
为避免走题(走到 高并发问题 上去了), 本次讨论作了以下限制 (从另一方面讲, 也是一些思路)
1. 单服务器的情况, (不考虑分布式)
2. 主流硬件, (不考虑基于hardware的I/O提升, 比如SSD硬盘, 光纤)
3. 多进程或是多线程不是讨论的重点, (这又是一个很大的话题)
4. 基于主流服务器软件架构: linux2.6 + java
5. 不考虑其它优化方法, 比如application层的cache机制
6. 只考虑最常见的socket一应一答模式, 不考虑改进通讯模式的改进, 比如cometd机制
异步就是异步
网上有许多I/O模型的相关文章, 主要涉及四个概念: 同步(synchronous), 异步(asynchronous), 阻塞(blocking) 和 非阻塞(non-blocking). 有些文章将这四个作了两两组合, 于是有了: 异步阻塞 和 异步非阻塞 , 可以很明确地说, 这完全是牵强之理解. 无论是 <Unix网络编程>一书中所列的I/O模式, 还是POSIX标准, 都没有提这两个概念. 异步就是异步! 只有同步时才有阻塞和非阻塞之分.
阻塞和非阻塞
我们说 阻塞和 非阻塞 时, 要区分场合范围, 比如 Linux中说的 非阻塞I/O 和 Java的NIO1.0中的 非阻塞I/O 不是相同的概念. 从最根本来说, 阻塞就是进程 "被" 休息, CPU处理其它进程去了. 非阻塞可以理解成: 将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会 "被" CPU光顾, 理论上可以做点其它事. 看上去 Linux非阻塞I/O 要比阻塞好, 但CPU会很大机率因socket没数据而空转. 虽然这个进程是爽了, 但是从整个机器的效率来说, 浪费更大了! Java NIO1.0中的非阻塞I/O中的 Selector.select()函数还是阻塞的, 所以不会有无谓的CPU浪费.
Java NIO1.0, 与其说是非阻塞I/O, 还不如说是, 多路复用I/O, 更好让人理解!
异步
异步可以说是I/O最理想的模型: CPU的原则是, 有必要的时候才会参与, 既不浪费, 也不怠慢.
理想中的异步I/O: Application无需等待socket数据(也正是因此进程而被 "休息"), 也无需 copy socket data, 将由其它的同学(理想状态, 不是CPU) 负责将socket data copy到Appliation事先指定的内存后, 通知一声Appliation(一般是回调函数).
copy socket data, Application是不用做了, 但事情总是需要做, 不管是谁做, CPU是否还是要花费精力去参与呢?
可以用 "内存映射" 以及 DMA等方式来达到 "不用CPU去参与繁重的工作" 的目的. "内存映射" 是不用copy, 而DMA是有其它芯片来代替CPU处理.
传统的阻塞socket有什么问题?
最传统的阻塞socket, 为了不致使处理一个client的请求时, 让其它的client一直等, 一般会一个client连接, 就会起一个Thread. 实际情况是, 有的业务, 只是client连接多, 但每个client连接上的通讯并不是非常频繁, 就算是很频繁, 也会因网络延迟, 而使得大部分时间内,Thread们都在被"休息"(因为等待scoket上数据), 因为相对cpu的运算速度, 网络延迟产生的间歇时间相当多. 这就造成了: 虽然Thread多, 并不能处理太多的socket请求, 要知道在JVM中每个Thread都会单独分配栈的(JVM默认好象是1M, 可以通过 -Xss来调整), 而且需CPU要不断地在很多线程之间switch, 保存/恢复 Thread Context 代价非常大!
多路复用
为了解决阻塞I/O的问题, 就有了 I/O多路复用 模型, 多路复用就是用单独的线程(是内核级的, 可以认为是高效的优化的) 来统一等待所有的socket上的数据, 一当某个socket上有数据后, 就启用用户线程(可能是从线程池中取出, 而不是重新生成), copy socket data, 并且处理message. 因为网络延迟的原因, 同时在处理socket data的用户线程往往比实际的socket数量要少很多. 所以实际应用中, 大部分是用线程池, 池中thread数量可随socket的高峰和低谷 而动态调整.
上面说的 多路复用I/O, 很多文章称之为 同步非阻塞. 个人认为, 不要老揪着那四个词不放! 多累呀!
多路复用, 既可以理解成 "非阻塞", 也可以理解成 "阻塞"
多路复用I/O 中内核中统一的 wait socket data那部分可以理解成 是 "非阻塞", 也可以理解成"阻塞". 可以理解成"非阻塞" 是因为它不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户线程来处理, 理解成"阻塞", 是因为它和用户空间(Appliction)层的"非阻塞"socket的不同是: socket中没有数据时, 内核还是wait(阻塞)的, 而用户空间的非阻塞socket没有数据也会返回, 会造成CPU的浪费(上面已经解释过了).
select 和 poll
Linux下的 select和poll 就是 多路复用模式, poll 相对 select, 没有了句柄数的限制, 但他们都是在内核层通过轮询socket句柄的方式来实现的, 没有利用更底层的 notify 机制. 但就算是这样,相对阻塞socket 也已经进步了很多很多了! 毕竟用一个内核线程就解决了, 阻塞socket中N多线程都在无谓地wait的局面.
多路复用I/O 还是让用户层来copy socket data. 这个过程是将内核中的socket buffer copy 到用户空间的 buffer. 这有两个问题: 一是多了一次内核空间switch到用户空间的过程, 二是用户空间层不便暴露很低层但很高效的copy 方式(比如DMA), 所以如果由内核层来做这个动作, 可以更好地提高效率!
epoll, Linux的AIO
于是, 在Linux2.6 epoll出现了, epoll是Linux下 AIO(异步IO)的实现方式, 实际上在epoll成为最终方案之前, 也有其它的方案, 而且在其它的操作系统中都有着不同的AIO实现.
epoll 的出现是革命性的, 因为它相对 poll 又有了很 cool 的改进, 完全可以基于它实现 AIO了.
epoll 已经采用了更为底层的 notify 机制, 而不是肓目地轮询来实现, 这样既减少了内核层的CPU消耗, 也使得上层的Application能更集中地关注应该关注的socket, 而不必每次都要将所有的 socket 通过 FD_ISSET来判断一下.
更为重要的是, epoll 因为采用 mmap的机制, 使得 内核socket buffer和 用户空间的 buffer共享, 从面省去了 socket data copy, 这也意味着, 当epoll 回调上层的 callback函数来处理 socket 数据时, 数据已经从内核层 "自动" 到了用户空间, 虽然和 用poll 一样, 用户层的代码还必须要调用 read/write, 但这个函数内部实现所触发的深度不同了.
用 poll 时, poll 通知用户空间的Appliation时, 数据还在内核空间, 所以Appliation调用 read API 时, 内部会做 copy socket data from kenel space to user space.
而用 epoll 时, epoll 通知用户空间的Appliation时, 数据已经在用户空间, 所以 Appliation调用 read API 时, 只是读取用户空间的 buffer, 没有 kernal space和 user space的switch了.
Java NIO和epoll
Java NIO也就是 NIO1.0 在Linux JDK6时已经改用 epoll 来作为 default selectorProvider了.
所以, 我有一个最大的疑问: 是否可以说, Java7中的 NIO2.0中的 AIO 改进已经无法压榨出 Linux2.6下epoll所带来的好处了?! 毕竟NIO1.0 在JDK6时已经用过 epoll 了.
还没有来得及研究Java7中的NIO2.0, 但无论如何, NIO2.0从 framework层面所带来的好处肯定是非常深远的.
Zero Copy
上面多次提到 内核空间 和 用户空间 的switch, 在socket read/write这么小的粒度频繁调用, 代价肯定是很大的.
所以可以在网上看到 Zero Copy的技术, 说到底 Zero Copy的思路就是: 分析你的业务, 看看是否能避免不必要的 跨空间copy, 比如可以用 sendfile() 函数充分利用 内核可以调用DMA 的优势, 直接在内核空间将文件的内容通过socket发送出去, 而不必经过用户空间. 显然, sendfile是有很多的前提条件的, 如果你想让文件内容作一些变换再发出去, 就必须要经过 用户空间的 Appliation logic, 也是无法使用sendfile了. 还有一种方式就是象 epoll 所做的, 用内存映射.
[原创链接: http://www.smithfox.com/?e=191, 转载请保留此声明, 谢谢! ]
最后列些链接:
http://unknownerror.net/2011-05/23919-linuxs-epoll-model.html
<<Unix网络编程>> 这本书
CK100问题, 是一个非常全面的 服务端socket并发的讨论, 一直更新, 上面有很多非常有价值的链接: http://www.kegel.com/c10k.html
有关 Zero Copy, 在IBM developer上的文章:
http://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy1/index.html
http://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy2/index.html
http://www.ibm.com/developerworks/cn/java/j-zerocopy/
下面是我看过的其他blog的文章:
http://blog.csdn.net/shallwake/article/details/5265287
http://www.sogou.com/labs/report/1-1.pdf