零拷贝
零拷贝是服务器网络编程的关键,指的是计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源,其通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间而直接在内核空间中传输到网络的方式,常用的零拷贝有mmap和sendFile
内核空间和用户空间
在说零拷贝之前,先了解一下计算机的内存空间
当前操作系统都是采用的虚拟存储器,对32位操作系统而言,寻址地址为$2^{32}$也就是4G,操作系统将虚拟空间划分为两部分,一部分是内核空间,一部分是用户空间,将最高的1G字节分配给内核使用,称为内核空间;将较低的3G字节供各个进程使用,称为用户空间
内核空间
内核空间主要是指操作系统运行时所使用的用于程序调度、虚拟内存的使用或者连接硬件资源等的程序逻辑
用户空间
由于操作系统中除了操作系统之外,还有运行在操作系统中的用户程序,而每个进程都独立的使用属于自己的内存,为了操作系统的稳定性,运行在操作系统中的用户程序不能访问操作系统所使用的内存空间。
如果用户程序需要访问硬件资源,可以调用操作系统提供的接口来实现,这个调用接口的过程就是系统调用,每一次系统调用都会存在两个内存空间的切换
传统数据读写的过程
- 第一次数据拷贝,read调用导致用户态到内核态的一次变化,同时,第一次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用CPU拷贝数据到内存,而是DMA引擎传输数据到内存,用于解放CPU),从磁盘读取文件,将数据放入到内核缓冲区
- 第二次数据拷贝,即:将内核缓冲区的数据拷贝到用户缓冲区,同时,发生了一次内核态到用户态的上下文切换
- 第三次数据拷贝,调用write方法,系统将用户缓冲区的数据拷贝到Socket缓冲区,此时,又发生了一次用户态到内核态的上下文切换
- 第四次数据拷贝,数据异步的从Socket缓冲区,使用DMA引擎拷贝到网络协议引擎,此次不需要进行上下文切换
- write方法返回,再次从内核态切换到用户态
需要 4 次数据拷贝、4 次上下文切换
mmap读写过程
mmap用到了虚拟内存,用虚拟的地址取代物理地址,使得内核空间和用户空间的虚拟地址映射到同一个物理地址,这样DMA就可以填充对内核和用户空间同时可见的缓冲区了
mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据,可以减少内核空间到用户空间的拷贝次数
user buffer和kernel buffer共享,不需要拷贝到用户空间,再从用户空间拷贝到Socket缓冲区,只需从内核缓冲区拷贝到Socket缓冲区即可,将减少一次内存拷贝(3次拷贝),但是不减少上下文切换次数
需要3次数据拷贝,4次上下文切换
适合小数据量读写
sendFile读写过程
sendFile操作数据不经过用户态,直接从内核缓冲区进入到Socket缓冲区,由于不经过用户态,所以减少了一次上下文切换
- 数据被DMA引擎从文件复制到内核缓冲区
- 调用write方法从内核缓冲区进入到Socket
- 数据从Socket缓冲区进入到协议栈
数据进行了3次拷贝,3次上下文切换
Linux2.4版本优化
在Linux2.4版本中,对于sendFile进行了一次调整,避免了从内核缓冲区拷贝到Socket buffer中,而是直接拷贝到协议栈,从而再次减少了一次数据拷贝
- DMA引擎从文件拷贝到内核缓冲区
- 从内核缓冲区将数据拷贝到网络协议栈
需要2次数据拷贝,3次上下文切换
适合大文件传输
Netty零拷贝
Netty的接收和发送ByteBuffer采用直接缓冲区(Direct Buffers),使用堆外内存直接进行Socket读写,避免了堆内存和堆外内存来回复制,不需要进行字节缓冲区的二次拷贝