目录
1.什么是零拷贝?
零拷贝(英语: Zero-copy) 技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
1)零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率
2)零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上下文切换而带来的开销。
最早的零拷贝定义,来源于 Linux 系统的 sendfile 方法逻辑!
在 Linux 2.4 内核中,sendfile 系统调用方法,可以将磁盘数据通过 DMA 拷贝到内核态 Buffer 后,再通过 DMA 拷贝到 NIC Buffer(socket buffer),无需 CPU 拷贝,这个过程被称之为零拷贝。
1.1DMA
DMA,英文全称是 Direct Memory Access,即直接内存访问。DMA 本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行 IO 数据传输,其过程不需要 CPU 的参与。
1.2内核空间和用户空间
操作系统的核心是内核,与普通的应用程序不同,它可以访问受保护的内存空间,也有访问底层硬件设备的权限。
为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部分,一部分是内核空间(Kernel-space),一部分是用户空间(User-space)。 在 Linux 系统中,内核模块运行在内核空间,对应的进程处于内核态;而用户程序运行在用户空间,对应的进程处于用户态。
内核空间总是驻留在内存中,它是为操作系统的内核保留的。应用程序是不允许直接在该区域进行读写或直接调用内核代码定义的函数。
当启动某个应用程序时,操作系统会给应用程序分配一个单独的用户空间,其实就是一个用户独享的虚拟内存,每个普通的用户进程之间的用户空间是完全隔离的、不共享的,当用户进程结束的时候,用户空间的虚拟内存也会随之释放。
同时处于用户态的进程不能访问内核空间中的数据,也不能直接调用内核函数的,如果要调用系统资源,就要将进程切换到内核态,由内核程序来进行操作。
2.传统的数据传输机制
以客户端从服务器下载文件为例,熟悉服务端开发的同学可能知道,服务端需要做两件事:
- 第一步:从磁盘中读取文件内容
- 第二步:将文件内容通过网络传输给客户端
事实上看似简单的操作,里面的流程却没那么简单,例如应用程序从磁盘中读取文件内容的操作,大体会经过以下几个流程:
- 第一步:用户应用程序调用 read 方法,向操作系统发起 IO 请求,CPU 上下文从用户态转为内核态,完成第一次 CPU 切换
- 第二步:操作系统通过 DMA 控制器从磁盘中读数据,并把数据存储到内核缓冲区
- 第三步:CPU 把内核缓冲区的数据,拷贝到用户缓冲区,同时上下文从内核态转为用户态,完成第二次 CPU 切换
整个读取数据的过程,完成了 1 次 DMA 拷贝,1 次 CPU 拷贝,2 次 CPU 切换;反之写入数据的过程,也是一样的。整个拷贝过程,可以用如下流程图来描述!
上图,我们可以得出如下结论,4 次拷贝次数、4 次上下文切换次数。
- 数据拷贝次数:2 次 DMA 拷贝,2 次 CPU 拷贝
- CPU 切换次数:4 次用户态和内核态的切换
而实际 IO 读写,有时候需要进行 IO 中断,同时也需要 CPU 响应中断,拷贝次数和切换次数比预期的还要多,以至于当客户端进行资源文件下载的时候,传输速度总是不尽人意。
3.mmap 内存映射拷贝流程
mmap 内存映射的拷贝,指的是将用户应用程序的缓冲区和操作系统的内核缓冲区进行映射处理,数据在内核缓冲区和用户缓冲区之间的 CPU 拷贝将其省略,进而加快资源拷贝效率。
整个拷贝过程,可以用如下流程图来描述!
mmap 内存映射拷贝流程,从上图可以得出如下结论:
- 数据拷贝次数:2 次 DMA 拷贝,1 次 CPU 拷贝
- CPU 切换次数:4 次用户态和内核态的切换
整个过程省掉了数据在内核缓冲区和用户缓冲区之间的 CPU 拷贝环节,在实际的应用中,对资源的拷贝能提升不少。
4.sendfile 拷贝流程
4.1 Linux 2.1版本的sendfile
在 Linux 2.1 内核版本中,引入了一个系统调用方法:sendfile
。
当调用 sendfile() 时,DMA 将磁盘数据复制到内核缓冲区 kernel buffer;然后将内核中的 kernel buffer 直接拷贝到 socket buffer;最后利用 DMA 将 socket buffer 通过网卡传输给客户端。
整个拷贝过程,可以用如下流程图来描述!
Linux 系统 sendfile 拷贝流程,从上图可以得出如下结论:
- 数据拷贝次数:2 次 DMA 拷贝,1 次 CPU 拷贝
- CPU 切换次数:2 次用户态和内核态的切换
相比 mmap 内存映射方式,Linux 2.1 内核版本中 sendfile 拷贝流程省掉了 2 次用户态和内核态的切换,同时内核缓冲区和用户缓冲区也无需建立内存映射,对资源的拷贝能提升不少。
4.2Linux 2.4的sendfile
在 Linux 2.4 内核版本中,对 sendfile 系统方法做了优化升级,引入 SG-DMA 技术,需要 DMA 控制器支持。
其实就是对 DMA 拷贝加入了 scatter/gather 操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点来实现数据拷贝,可以多省去一次 CPU 拷贝。
整个拷贝过程,可以用如下流程图来描述!
Linux 系统 sendfile With DMA scatter/gather 拷贝流程,从上图可以得出如下结论:
- 数据拷贝次数:2 次 DMA 拷贝,0 次 CPU 拷贝
- CPU 切换次数:2 次用户态和内核态的切换
可以发现,sendfile With DMA scatter/gather 实现的拷贝,其中 2 次数据拷贝都是 DMA 拷贝,全程都没有通过 CPU 来拷贝数据,所有的数据都是通过 DMA 来进行传输的,这就是操作系统真正意义上的零拷贝(Zero-copy) 技术,相比其他拷贝方式,传输效率最佳。
5.splice 零拷贝流程
在 Linux 2.6.17 内核版本中,引入了 splice 系统调用方法,和 sendfile 方法不同的是,splice 不需要硬件支持。
它将数据从磁盘读取到 OS 内核缓冲区后,内核缓冲区和 socket 缓冲区之间建立管道来传输数据,避免了两者之间的 CPU 拷贝操作。
整个拷贝过程,可以用如下流程图来描述!
Linux 系统 splice 拷贝流程,从上图可以得出如下结论:
- 数据拷贝次数:2 次 DMA 拷贝,0 次 CPU 拷贝
- CPU 切换次数:2 次用户态和内核态的切换
Linux 系统 splice 方法逻辑拷贝,也是操作系统真正意义上的零拷贝。
6.IO拷贝机制的对比
拷贝机制 | 系统调用 | CPU拷贝次数 | DMA拷贝次数 | 上下文切换次数 | 特点 |
传统拷贝机制 | read/write | 2 | 2 | 4 | 消耗系统资源比较多,拷贝数据效率慢 |
mmap | mmap/write | 1 | 2 | 5 | 相比传统方法,少了用户缓冲区与内核缓冲区的数据拷贝,效率更高 |
sendfile | sendfile | 1 | 2 | 2 | 相比mmap方式,少了内存文件映射步骤,效率更高 |
sendfile With DMA scatter/gather | sendfile | 0 | 2 | 2 | 需要DMA控制器支持,没有cpu拷贝数据缓解,真正的零拷贝 |
splice | splice | 0 | 2 | 2 | 没有cpu拷贝数据缓解,真正的零拷贝,编程逻辑复杂 |
7.NIO提供的sendfile
Java NIO 中提供的 FileChannel 拥有 transferTo 和 transferFrom 两个方法,可直接把FileChannel 中的数据拷贝到另外一个 Channel,或者直接把另外一个 Channel 中的数据拷贝到FileChannel。该接口常被用于高效的网络 / 文提供的件的数据传输和大文件拷贝。在操作系统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,所以其性能一般高于 Java IO 中提供的方法。
8.kafka中的零拷贝
Kafka两个重要过程都使用了零拷贝技术,且都是操作系统层面的狭义零拷贝,一是Producer生产的数据存到broker,二是 Consumer从broker读取数据。
Producer生产的数据持久化到broker,broker里采用mmap文件映射,实现顺序的快速写入;
Customer从broker读取数据,broker里采用sendfile,将磁盘文件读到OS内核缓冲区后,直接转到socket buffer进行网络发送。
参考博客: