零拷贝前置知识
拷贝就是指计算机中的I/O操作,简单来说就是数据文件从文件系统一个位置到另一个位置的过程,零拷贝区别于传统的计算机I/O操作
传统的计算机I/O操作流程类似下图
这里数据经过以下节点
切到内核态 - > 读数据到内核缓存区 -> 切到用户态 - > 拷贝数据到用户缓存区
发现这里主要是两个操作很消耗性能开销
- 用户态和内核态的切换
- 数据的两次拷贝迁移
发现一个问题没有?为什么要进行用户态到内核态的转换呢?真的是,就不能直接读到数据后就给到用户程序吗?
答案:当然是不可以的!下面就来细讲一下为什么
为什么需要进行用户态到内核态的转换?
操作系统设计的初衷就是为了确保安全性、稳定性和资源管理。这里有几个主要原因:
- 硬件访问控制: 用户程序不能直接访问硬件资源(如磁盘、内存、CPU等)。这是为了防止用户程序直接操作硬件可能导致系统崩溃或者对其他程序的影响。如果程序直接访问硬件,它可能会破坏硬件数据结构或者篡改内存中的其他进程数据。
- 保护用户程序: 用户程序处于一个“受限环境”下,在用户态下无法直接执行系统级的指令或操作,避免用户程序错误或恶意行为对整个操作系统造成影响。只有当需要执行涉及硬件或系统管理的操作时,操作系统才会将控制权交给内核态。
- 资源管理: 操作系统管理着多个进程以及它们对硬件资源(CPU、内存、磁盘等)的访问。通过内核态的介入,操作系统能够有效地管理这些资源,保证不同进程之间不会互相干扰并合理分配资源。
- 系统调用和I/O操作: 大多数情况下用户程序通过操作系统提供的接口(即系统调用)来执行诸如文件操作、网络通信等任务。在执行这些系统调用时,用户态会转变为内核态,因为这些操作通常涉及到硬件资源的访问。系统调用实际上是一个用户态到内核态的转换机制,让操作系统来完成复杂的底层操作,而程序本身无需关心具体细节。
这里说了那么多专业化的表述,实际上就是在说两句话
- 为了你好,权限给的太多等下你瞎操作到硬件的一些东西,系统直接坏了,电脑都得给你搞烂
- 给你提供便利,如果你能操作到内核底层的东西,那么你可能就要接触到一些复杂的底层操作,但是实际上你不需要去关心这些
这里从操作系统的角度也给出了解释,就不过多赘述了。
那么在经过上面的分析后,我们就会发现,为了安全性需要多出几次的拷贝和形态切换,导致性能的开销。那么如果我们在了解操作安全性的情况下,能不能通过减少用户态到内核态切换或者减少拷贝操作提高性能呢?以下有四种方案实现
零拷贝的应用场景
Kafka
Kafka的文件传输底层代码中,内部调用了Java NIO 库中的tansferTo()方法,这个transferTo()方法底层采用的就是sendfile()系统调用函数
Nginx
开启零拷贝技术配置如下
http { ... sendfile on ... }
- on 即为打开配置,那么就只需要进行2次上下文切换+2次数据拷贝
- off 即为采用传统方案read+write 需要4次上下文切换+4次拷贝
实现零拷贝方案
1. 直接内存访问(DMA)
DMA是一种硬件特性,能让磁盘/网络适配器直接访问到系统内存,就不需要CPU介入,数据传输的时候直接把数据从内存-> 外设,或者反过来。避免了数据的多次拷贝动作
2. sendfile(Linux)
Linux系统提供的特殊系统调用操作,通过这个sendfile,应用程序可以直接将文件数据从文件系统传到目标文件,跳过了用户缓冲区和内核缓存区的拷贝
原方案
A文件数据 -> 内核缓冲区,内核缓冲区 -> 用户缓冲区;
内核再将用户缓冲区数据 -> 内核缓冲区,之后才能写入到B文件;
使用sendfile
省了用户缓冲区的拷贝+内核缓冲区的拷贝
3. 共享内存
用户直接把数据放到共享内存,然后内核从共享内存拿数据,就不需要进行用户太和内核态之间的数据拷贝动作,这个思想实际上就是开发中很常见的一句话,没有什么是不能通过加一层中间件解决的,如果有那就再加一层
4. 内存映射文件(mmap机制)
这个的思想和3实际上是一样的,但是原理是不同的,3是共享一个内存地址引用,4是通过映射文件内容
Java中实现零拷贝的方案
Java的NIO中有一套新的I/O类,利用ByteBuffer 与Channel 交互可以实现零拷贝
ByteBuffer:可以直接操作字节数据,避免了数据在用户态和内核态之间的复制。Channel:支持直接将数据从文件通道或网络通道传输到另一个通道,实现文件和网络的零拷贝传输。
Channel 示例代码如下(并发环境下是线程安全)
public static void nioTransferTo() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}
参考文章如下
cloud.tencent.com/developer/a… 腾讯云 对优化的分析比较详细
blog.csdn.net/huhigher/ar… CSDN 主要讲解了零拷贝的来龙去脉以及方案
www.cnblogs.com/xiaolincodi… 小林Coding 针对场景分析技术选型 优质分析