io_module

io_module

来源: [原创链接: http://www.smithfox.com/?e=191%EF%BB%BF, 转载请保留此声明, 谢谢! ]

异步和同步

同步(synchronous?), 异步?(asynchronous?)?, 阻塞(blocking) 和 非阻塞(non-blocking).

 同步阻塞
 同步非阻塞
异步阻塞
异步非阻塞

异步就是异步! 只有同步时才有阻塞和非阻塞之分.

阻塞和非阻塞?

我们说 阻塞和 非阻塞 时, 要区分场合范围, 比如 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 所做的, 用内存映射.

时间: 2024-08-14 04:51:09

io_module的相关文章