java zerocopy 原理

本文探讨了传统数据传输方式中的不足之处,详细介绍了零拷贝技术如何减少不必要的数据复制,从而极大提高应用程序性能。通过使用Java NIO Channel的transferTo()方法等技术,实现了高效的数据传输。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

许多web应用都会向用户提供大量的静态内容,这意味着有很多data从硬盘读出之后,会原封不动的通过socket传输给用户。这种操作看起来可能不会怎么消耗CPU,但是实际上它是低效的:kernal把数据从disk读出来,然后把它传输给user级的application,然后application再次把同样的内容再传回给处于kernal级的socket。这种场景下,application实际上只是作为一种低效的中间介质,用来把disk file的data传给socket。




data每次穿过user-kernel boundary,都会被copy,这会消耗cpu,并且占用RAM的带宽。幸运的是,你可以用一种叫做Zero-Copy的技术来去掉这些无谓的copy。应用程序用zero copy来请求kernel直接把disk的data传输给socket,而不是通过应用程序传输。Zero copy大大提高了应用程序的性能,并且减少了kernel和user模式的上下文切换。


Java的libaries在linux和unix中支持zero copy,一个关键的api是java.nio.channel.FileChannel的transferTo()方法。我们可以用transferTo()来把bytes直接从调用它的channel传输到另一个writable byte channel,中间不会使data经过应用程序。本文首先描述传统的copy是怎样坑爹的,然后再展示zero-copy技术在性能上是多么的给力以及为什么给力。


Date transfer: The traditional approach


考虑一下这个场景,通过网络把一个文件传输给另一个程序。这个操作的核心代码就是下面的两个函数:


Listing 1. Copying bytes from a file to a socket


File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
尽管看起来很简单,但是在OS的内部,这个copy操作要经历四次user mode和kernel mode之间的上下文切换,甚至连数据都被拷贝了四次!Figure 1描述了data是怎么移动的。






Figure 2 描述了上下文切换


Figure 2. Traditional context switches




其中的步骤如下:


read() 引入了一次从user mode到kernel mode的上下文切换。实际上调用了sys_read() 来从文件中读取data。第一次copy由DMA完成,将文件内容从disk读出,存储在kernel的buffer中。
然后data被copy到user buffer中,此时read()成功返回。这是触发了第二次context switch: 从kernel到user。至此,数据存储在user的buffer中。
send() socket call 带来了第三次context switch,这次是从user mode到kernel mode。同时,也发生了第三次copy:把data放到了kernel adress space中。当然,这次的kernel buffer和第一步的buffer是不同的两个buffer。
最终 send() system call 返回了,同时也造成了第四次context switch。同时第四次copy发生,DMA将data从kernel buffer拷贝到protocol engine中。第四次copy是独立而且异步的。
使用kernel buffer做中介(而不是直接把data传到user buffer中)看起来比较低效(多了一次copy)。然而实际上kernel buffer是用来提高性能的。在进行读操作的时候,kernel buffer起到了预读cache的作用。当写请求的data size比kernel buffer的size小的时候,这能够显著的提升性能。在进行写操作时,kernel buffer的存在可以使得写请求完全异步。


悲剧的是,当请求的data size远大于kernel buffer size的时候,这个方法本身变成了性能的瓶颈。因为data需要在disk,kernel buffer,user buffer之间拷贝很多次(每次写满整个buffer)。


而Zero copy正是通过消除这些多余的data copy来提升性能。


Data Transfer:The Zero Copy Approach


如果重新检查一遍traditional approach,你会注意到实际上第二次和第三次copy是毫无意义的。应用程序仅仅缓存了一下data就原封不动的把它发回给socket buffer。实际上,data应该直接在read buffer和socket buffer之间传输。transferTo()方法正是做了这样的操作。Listing 2是transferTo()的函数原型:


public void transferTo(long position, long count, WritableByteChannel target);
transferTo()方法把data从file channel传输到指定的writable byte channel。它需要底层的操作系统支持zero copy。在UNIX和各种Linux中,会执行List 3中的系统调用sendfile(),该命令把data从一个文件描述符传输到另一个文件描述符(Linux中万物皆文件):


#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
在List 1中的file.read()和socket.send()可以用一句transferTo()替代,如List 4:


transferTo(position, count, writableChannel);
Figure 3 展示了在使用transferTo()之后的数据流向






Figure 4 展示了在使用transferTo()之后的上下文切换




在像Listing 4那样使用transferTo()之后,整个过程如下:


transferTo()方法使得文件内容被DMA engine直接copy到一个read buffer中。然后数据被kernel再次拷贝到和output socket相关联的那个kernel buffer中去。
第三次拷贝由DMA engine完成,它把kernel buffer中的data拷贝到protocol engine中。
这是一个很明显的进步:我们把context switch的次数从4次减少到了2次,同时也把data copy的次数从4次降低到了3次(而且其中只有一次占用了CPU,另外两次由DMA完成)。但是,要做到zero copy,这还差得远。如果网卡支持 gather operation,我们可以通过kernel进一步减少数据的拷贝操作。在2.4及以上版本的linux内核中,开发者修改了socket buffer descriptor来适应这一需求。这个方法不仅减少了context switch,还消除了和CPU有关的数据拷贝。user层面的使用方法没有变,但是内部原理却发生了变化:


transferTo()方法使得文件内容被copy到了kernel buffer,这一动作由DMA engine完成。
没有data被copy到socket buffer。取而代之的是socket buffer被追加了一些descriptor的信息,包括data的位置和长度。然后DMA engine直接把data从kernel buffer传输到protocol engine,这样就消除了唯一的一次需要占用CPU的拷贝操作。
Figure 5描述了新的transferTo()方法中的data copy:





<think>好的,我现在需要回答用户关于如何实现零拷贝(zero-copy)访问接口的示例和代码解释。首先,我得回忆一下零拷贝的相关知识,确保自己理解正确。零拷贝技术主要是减少数据在内核空间和用户空间之间的拷贝次数,从而提升性能,特别是在处理大量数据传输时,比如文件传输或网络通信。 用户提到了几个引用,比如Linux的sendfile、splice和mmap等方法。我需要结合这些技术给出具体的实现示例。首先,可能要从这些系统调用或接口入手,分别举出代码示例,并解释每个步骤的作用。 首先,mmap是一种常用的零拷贝方法,它通过内存映射将文件直接映射到用户空间,这样应用程序可以直接读写内存而无需使用read或write系统调用进行数据拷贝。例如,在驱动程序中实现mmap接口,用户程序就可以通过映射的内存区域直接访问设备内存或文件内容,避免了数据从内核到用户空间的拷贝。引用[1]中的例子提到应用程序写入字符串到mmap映射的内存,驱动不需要额外的拷贝操作,这应该是一个mmap的典型应用。 接下来是sendfile系统调用,引用[4]中提到了sendfile可以直接将数据从文件描述符传输到套接字,无需用户空间参与。这在网络传输文件时特别有用,比如Web服务器发送静态文件。用户可能想看一个使用sendfile的示例代码,比如在C语言中调用sendfile函数,将文件内容发送到网络套接字的过程。 然后是splice系统调用,引用[3]说明splice利用管道在内核中移动数据,而不需要实际的数据拷贝。这可能用于两个文件描述符之间的数据传输,比如从一个文件管道到另一个,或者从文件到网络套接字。需要展示如何使用splice,并解释其参数,比如输入输出文件描述符、管道等。 此外,可能还需要提到其他零拷贝技术,比如使用DMA(直接内存访问)或结合特定框架的API,比如Java中的FileChannel.transferTo,这也是基于系统的零拷贝机制实现的。但用户可能更关注系统级实现,比如C语言的例子,所以重点放在mmap、sendfile和splice上。 现在需要检查每个示例的正确性,确保代码语法正确,并符合用户的要求。例如,在mmap的例子中,正确打开文件,映射内存,然后进行读写操作。对于sendfile,正确调用系统函数,处理错误情况。对于splice,正确设置管道并使用splice在两个描述符之间转移数据。 同时,用户要求代码示例和解释,所以每个部分都要有对应的代码块和分步骤的解释。需要确保使用正确的LaTeX格式,行内公式用$...$,独立公式用$$...$$,但可能这个问题中数学公式较少,但如果有相关的变量或方程需要展示,要注意格式。 最后,生成相关问题,帮助用户进一步了解零拷贝的其他方面,比如性能分析、不同技术的对比、应用场景等。需要确保问题相关且有价值,引导用户深入理解。 可能遇到的问题:不确定某些系统调用的具体参数和用法,需要查阅资料确认,比如splice的参数是否包括管道,或者sendfile在不同系统上的兼容性。但根据提供的引用,用户已经给出了一些信息,比如引用[3]中splice使用管道,而引用[4]中sendfile的参数是out_fd、in_fd等。所以可以基于这些信息编写示例代码。 总结步骤: 1. 介绍mmap方法,给出C语言示例代码,解释其工作原理。 2. 介绍sendfile,给出C代码示例,说明如何传输文件到套接字。 3. 介绍splice,给出使用管道和splice的代码示例。 4. 每个部分结合引用中的内容,添加引用标识。 5. 生成相关问题。</think>### 如何实现 Zero-Copy 访问接口的示例与代码解释 实现零拷贝接口的核心在于**绕过用户态与内核态之间的冗余数据拷贝**。以下是三种典型方法的示例与分步解释: --- #### 1. 使用 `mmap` 实现内存映射(文件操作) **原理**:将文件直接映射到进程内存空间,避免 `read/write` 的拷贝操作[^1]。 ```c #include <sys/mman.h> #include <fcntl.h> int main() { int fd = open("data.txt", O_RDWR); char *mapped_mem = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); // 直接操作映射内存(无需read/write) mapped_mem[0] = 'A'; // 修改文件内容 printf("First byte: %c\n", mapped_mem[0]); // 读取文件内容 munmap(mapped_mem, 4096); close(fd); return 0; } ``` **步骤说明**: - `mmap` 将文件 `data.txt` 映射到进程的虚拟内存地址。 - 直接通过指针 `mapped_mem` 读写文件,无需调用 `read` 或 `write`。 - `munmap` 解除映射,确保修改同步到文件。 --- #### 2. 使用 `sendfile` 传输文件到网络(网络传输) **原理**:在内核态直接将文件数据发送到网络套接字[^4]。 ```c #include <sys/sendfile.h> #include <fcntl.h> #include <sys/socket.h> int main() { int file_fd = open("large_file.zip", O_RDONLY); int sock_fd = socket(AF_INET, SOCK_STREAM, 0); struct stat file_stat; fstat(file_fd, &file_stat); // 将文件内容直接发送到网络套接字 sendfile(sock_fd, file_fd, NULL, file_stat.st_size); close(file_fd); close(sock_fd); return 0; } ``` **步骤说明**: - `sendfile` 接受输入文件描述符 `file_fd` 和输出套接字 `sock_fd`。 - 内核自动完成文件读取和网络发送,**无用户态数据拷贝**。 - 适用于大文件传输(如HTTP服务器发送静态资源)。 --- #### 3. 使用 `splice` 通过管道转移数据 **原理**:利用管道在内核中“移动”数据,仅拷贝内存指针而非数据本身[^3]。 ```c #include <fcntl.h> #include <unistd.h> #include <sys/splice.h> int main() { int pipefd[2]; pipe(pipefd); // 创建管道 int in_fd = open("input.txt", O_RDONLY); int out_fd = open("output.txt", O_WRONLY); // 将数据从输入文件通过管道转移到输出文件 splice(in_fd, NULL, pipefd[1], NULL, 4096, 0); splice(pipefd[0], NULL, out_fd, NULL, 4096, 0); close(in_fd); close(out_fd); return 0; } ``` **步骤说明**: - `splice` 将 `in_fd` 的数据写入管道,再从管道读取到 `out_fd`。 - 数据全程在内核态流转,用户态无拷贝操作。 - 适用于文件复制或网络代理等场景。 --- ### 关键对比 | 方法 | 适用场景 | 用户态参与 | |----------|---------------------|------------| | `mmap` | 文件随机访问 | 直接操作内存 | | `sendfile` | 文件到网络传输 | 无需参与 | | `splice` | 内核管道数据转移 | 无需参与 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值