虚拟内存 & I/O & 零拷贝( 六 )


epoll 对文件描述符的操作有两种模式:LT(level trigger)和 ET(edge trigger) 。

  1. LT 模式 LT(level triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作 。如果你不作任何操作,内核还是会继续通知你 。
  2. ET 模式 ET(edge-triggered)是高速工作方式,只支持 no-block socket 。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 告诉你 。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个 EWOULDBLOCK 错误) 。注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高 。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死 。
3.4 网络 IO 模型实际的网络模型常结合 I/O 复用和线程池实现,如 Reactor 模式:
虚拟内存 & I/O & 零拷贝

文章插图
3.4.1 单 reactor 单线程模型
此种模型通常只有一个 epoll 对象,所有的接收客户端连接、客户端读取、客户端写入操作都包含在一个线程内 。
虚拟内存 & I/O & 零拷贝

文章插图
优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成 缺点:单线程无法完全发挥多核 CPU 的性能;I/O 操作和非 I/O 的业务操作在一个 Reactor 线程完成,这可能会大大延迟 I/O 请求的响应;线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障;使用场景:客户端的数量有限,业务处理非常快速,比如 redis 在业务处理的时间复杂度 O(1) 的情况
3.4.2 单 reactor 多线程模型该模型将读写的业务逻辑交给具体的线程池来处理
虚拟内存 & I/O & 零拷贝

文章插图
优点:充分利用多核 cpu 的处理能力,提升 I/O 响应速度;缺点:在该模式中,虽然非 I/O 操作交给了线程池来处理,但是所有的 I/O 操作依然由 Reactor 单线程执行,在高负载、高并发或大数据量的应用场景,依然容易成为瓶颈 。
3.4.3 multi-reactor 多线程模型在这种模型中,主要分为两个部分:mainReactor、subReactors 。mainReactor 主要负责接收客户端的连接,然后将建立的客户端连接通过负载均衡的方式分发给 subReactors,subReactors 来负责具体的每个连接的读写 对于非 IO 的操作,依然交给工作线程池去做 。
虚拟内存 & I/O & 零拷贝

文章插图
优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理 。Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据 。缺点:编程复杂度较高 。
3.4.4 主流的中间件所采用的网络模型 3.5 异步 IO
虚拟内存 & I/O & 零拷贝

文章插图
前面介绍的所有网络 IO 都是同步 IO,因为当数据在内核态就绪时,在内核态拷贝用用户态的过程中,仍然会有短暂时间的阻塞等待 。而异步 IO 指:内核态拷贝数据到用户态这种方式也是交给系统线程来实现,不由用户线程完成,如 windows 的 IOCP,Linux 的 AIO 。
四、 零拷贝 4.1 传统 IO 流程
传统 IO 流程会经过如下两个过程:
  • 数据准备阶段:数据从硬件到内核空间
  • 数据拷贝阶段:数据从内核空间到用户空间

虚拟内存 & I/O & 零拷贝

文章插图
零拷贝:指数据无需从硬件到内核空间或从内核空间到用户空间 。下面介绍常见的零拷贝实现
4.2 mmap + write
mmap 将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射,从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程,整个拷贝过程会发生 4 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝 。


推荐阅读