零拷贝和多路复用模型
1.零拷贝
1.1 零拷贝简介
零拷贝指的是从一个存储区域到另一个存储区域的 copy 任务没有 CPU 参与.
通常用于网络文件传输, 以减少 CPU 消耗 和 内存带宽占用, 减少 用户空间 与 CPU 内核空间 的拷贝过程, 减少 用户上下文 与 CPU 内核上下文间的切换, 提高系统效率.
零拷贝需要 DMA 控制器 的协助. Direct Memory Access, 直接内存存取, 是 CPU 的组成部分, 其可以在 CPU 内核 (算术逻辑运算器 ALU 等) 不参与运算的情况下将数据从一个地址空间拷贝到另一个地址空间.
1.2 传统拷贝
场景 将一个硬盘中的文件通过网络发送出去

4 次用户空间与内核空间的上下文切换,以及 4 次数据拷贝
存在问题
应用程序的作用仅仅就是一个数据传输的中介, 最后将 kernel buffer
中的数据传递到了 socket buffer
1.3 零拷贝实现
通过 sendfile
系统调用实现的

kernel buffer
与 socket buffer
有什么区别呢?DMA 控制器所控制的拷贝过程有一个要求,数据在源头的存放地址空间必须连续的. kernel buffer
中的数据无法保证其连续性, 所以需要将数据再拷贝到 socket buffer
,socket buffer
可以保证了数据的连续性
1.4 Gather Copy DMA
只是将 kernel buffer 中的 数据描述信息 写到了socket buffer
中. 数据描述信息包含了两方面的信息: kernel buffer
中数据的地址 及 偏移量

传统拷贝中 user buffer
中存有数据, 因此应用程序能够对数据进行修改等操作; 零拷贝中的 user buffer
中没有了数据, 所以应用程序无法对数据进行操作了. Linux 的 mmap
零拷贝解决了这个问题
1.5 mmap 零拷贝
应用程序与内核共享了 Kernel buffer
.由于是共享, 所以应用程序也就可以操作该buffer

应用程序可以对数据进行修改
1.6 零拷贝的运用
在主流的开源框架中, 源码层次都会有涉及零拷贝的运用, 消息中间件Kafka在处理文件 I/O以及Netty,
2. 多路复用 select | poll | epoll
复用方式 | select | poll | epoll |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 哈希表 |
IO效率 | 每次调用线性遍历, 时间复杂度O(n) | 每次调用线性遍历, 时间复杂度O(n) | 时间通知方式, 当fd就绪, 系统注册的回调函数就会调用, 将就绪fd放到readList 里, 时间复杂度O(1) |
最大连接数 | 1024 (x86) 2048 (x64) | 无上限 | 无上限 |
fd拷贝形式 | 每调select, 需要将fd集合 从用户态拷贝到内核态 | 每调poll, 需要将fd集合 从用户态拷贝到内核态 | 调用 epoll_ctl时拷贝进内核并保存, 之后每次epoll_wait不拷贝 |
2.1 介绍
多进程/多线程连接处理模型

一个用户连接请求会由一个 内核进程 处理, 而一个内核进程会创建一个 应用程序进程
BIO方式: 应用程序进程在未获取到 IO 响应 之前是处于 阻塞态 的
内核进程不存在对app进程的竞争,一个内核进程对应一个app进程
多路复用连接处理模型

只有一个 app 进程来处理内核进程事务, 且 app 进程一次只能处理一个内核进程事务, 存在竞争
需要通过 “多路复用器” 来获取各个内核进程的状态信息, app 进程在进行 IO 时, 其采用的是 NIO 通讯方式, 即该 app 进程不会阻塞
2.2 实现原理
select
采用 轮询 的方式, 一直在轮询所有的相关内核进程状态. 若已经就绪, 则马上将该内核进程放入到就绪队列. 处理内核事务前, 会从内核空间中将用户连接请求相关数据复制到用户空间
存在的问题
对所有内核进程采用轮询方式效率会很低
由于就绪队列底层由数组实现, 其所能处理的内核进程数量是有限制的, 能够处理的最大并发连接数量是有限制的
从内核空间到用户空间的复制, 系统开销大
poll
由于其就绪队列由链表实现,对于要处理的内核进程数量理论上是没有限制的 ulimit -n
epoll
采用回调方式实现对内核进程状态的获取: 一旦内核进程就绪, 其就会 回调 epoll
多路复用器, 进入到多路复用器的 就绪链表队列
应用程序所使用的数据, 使用 mmap
零拷贝机制
内核进程就绪信息通知了 epoll 多路复用器后, 多路复用器就会马上对其进行处理
LT 模式
LT, 水平触发模式. 内核进程的就绪通知由于某种原因暂时没有被 epoll 处理, 则该内核进程就会 定时 将其就绪信息通知 epoll
. 直到 epoll
将其写入到就绪队列, 或由于某种原因该内核进程又不再就绪而不再通知. 支持两种通讯方式:BIO 与 NIO
ET 模式
ET, 边缘触发模式. 仅支持 NIO 的通讯方式. 当内核进程的就绪信息 仅会通知一次 epoll
,无论 epoll 是否处理该通知. 效率要高于 LT 模式, 但其有可能会出现就绪通知被忽视的情况, 即连接请求丢失的情况.
什么时候文件描述符活跃,活跃后内核如何处理
epoll工作在ET模式的时候,必须使用非阻塞套文件读写,以避免由于一个文件句柄的阻塞读/阻塞写操作容易阻塞在read函数时,因为没有读取到需要的字节数,而服务器又不能脱离read的阻塞状态去调用epoll函数接收客户端的数据造成死锁。
包从硬件网卡(NIC) 上进来之后,会触发一个中断,告诉 cpu 网卡上有包过来了,需要处理,同时通过 DMA(direct memory access) 的方式把包存放到内存的某个地方,这块内存通常称为 ring buffer,是网卡驱动程序初始化时候分配的
当 cpu 收到这个中断后,会调用中断处理程序,这里的中断处理程序就是网卡驱动程序,因为网络硬件设备网卡需要驱动才能工作。网卡驱动会先关闭网卡上的中断请求,表示已经知晓网卡上有包进来的事情,同时也避免在处理过程中网卡再次触发中断,干扰或者降低处理性能。驱动程序启动软中断,继续处理数据包.
Reference
- Zero Copy
- epoll
- …