Linux "Zero-Copy”技术 sendfile

本文详细探讨了在操作系统层面如何实现零拷贝技术,通过对比传统数据传输方式,介绍了mmap和sendfile系统调用的使用及优劣,并进一步解释了如何通过硬件支持的聚合操作特性完全消除数据冗余。

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

What ‘s “Zero-copy” ?

      首先以一个网络服务守护进程为例,考虑它在将存储在文件中的信息通过网络传送给客户这样的简单过程中,所涉及的操作,下面是其中的部分简单代码:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

      在执行这两个系统调用的过程中,目标数据至少被复制了4次,同时发生了同样多次数的用户/内核空间的切换。

这里写图片描述

- 步骤一:系统调用read导致了从用户空间到内核空间的上下文切换。DMA模块从磁盘中读取文件内容,并将其存储在内核空间的缓冲区内,完成了第1次复制。
- 步骤二:数据从内核空间缓冲区复制到用户空间缓冲区,之后系统调用read返回,这导致了从内核空间向用户空间的上下文切换。此时,需要的数据已存放在指定的用户空间缓冲区内(参数tmp_buf),程序可以继续下面的操作。
- 步骤三:系统调用write导致从用户空间到内核空间的上下文切换。数据从用户空间缓冲区被再次复制到内核空间缓冲区,完成了第3次复制。不过,这次数据存放在内核空间中与使用的socket相关的特定缓冲区中,而不是步骤一中的缓冲区。

步骤四:系统调用返回,导致了第4次上下文切换。第4次复制在DMA模块将数据从内核空间缓冲区传递至协议引擎的时候发生,这与我们的代码的执行是独立且异步发生的。

      你可能会疑惑:“为何要说是独立、异步?难道不是在write系统调用返回前数据已经被传送了?write系统调用的返回,并不意味着传输成功——它甚至无法保证传输的开始。调用的返回,只是表明以太网驱动程序在其传输队列中有空位,并已经接受我们的数据用于传输。可能有众多的数据排在我们的数据之前。除非驱动程序或硬件采用优先级队列的方法,各组数据是依照FIFO的次序被传输的(图1中叉状的DMA copy表明这最后一次复制可以被延后)。

      正如你所看到的,上面的过程中存在很多的数据冗余。某些冗余可以被消除,以减少开销、提高性能。消除复制的一种方法是将read系统调用,改为mmap系统调用,例如:

tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

      为了更好的理解这其中设计的操作,请看图2。上下文切换部分与图1保持一致。

这里写图片描述

- 步骤一:mmap系统调用导致文件的内容通过DMA模块被复制到内核缓冲区中,该缓冲区之后与用户进程共享,这样就内核缓冲区与用户缓冲区之间的复制就不会发生。
- 步骤二:write系统调用导致内核将数据从内核缓冲区复制到与socket相关联的内核缓冲区中。
- 步骤三:DMA模块将数据由socket的缓冲区传递给协议引擎时,第3次复制发生。

      通过调用mmap而不是read,我们已经将内核需要执行的复制操作减半。当有大量数据要进行传输是,这将有相当良好的效果。然而,性能的改进需要付出代价的;是用mmap与write这种组合方法,存在着一些隐藏的陷阱。例如,考虑一下在内存中对文件进行映射后调用write,与此同时另外一个进程将同一文件截断的情形。此时write系统调用会被进程接收到的SIGBUS信号中断,因为当前进程访问了非法内存地址。对SIGBUS信号的默认处理是杀死当前进程并生成dump core文件——而这对于网络服务器程序而言不是最期望的操作。

      有两种方式可用于解决该问题:

      第一种方式是为SIGBUS信号设置信号处理程序,并在处理程序中简单的执行return语句。在这样处理方式下,write系统调用返回被信号中断前已写的字节数,并将errno全局变量设置为成功。必须指出,这并不是个好的解决方式——治标不治本。由于收到SIGBUS信号意味着进程发生了严重错误,我不鼓励采取这种解决方式。

      第二种方式应用了文件租借(在Microsoft Windows系统中被称为“机会锁”)。这才是解决前面问题的正确方式。通过对文件描述符执行租借,可以同内核就某个特定文件达成租约。从内核可以获得读/写租约。当另外一个进程试图将你正在传输的文件截断时,内核会向你的进程发送实时信号——RT_SIGNAL_LEASE。该信号通知你的进程,内核即将终止在该文件上你曾获得的租约。这样,在write调用访问非法内存地址、并被随后接收到的SIGBUS信号杀死之前,write系统调用就被RT_SIGNAL_LEASE信号中断了。write的返回值是在被中断前已写的字节数,全局变量errno设置为成功。下面是一段展示如何从内核获得租约的示例代码。

if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
 perror("kernel lease set signal");
 return -1;
 }

 /* l_type can be F_RDLCK F_WRLCK */
 if(fcntl(fd, F_SETLEASE, l_type)){
 perror("kernel lease set type");
 return -1;
 }

Sendfile

       sendfile系统调用在内核版本2.1中被引入,目的是简化通过网络在两个本地文件之间进行的数据传输过程。sendfile系统调用的引入,不仅减少了数据复制,还减少了上下文切换的次数。使用方法如下:

sendfile(socket, file, len);

为了更好的理解所涉及的操作,请看图3

这里写图片描述

  • 步骤一:sendfile系统调用导致文件内容通过DMA模块被复制到某个内核缓冲区,之后再被复制到与socket相关联的缓冲区内。
    • 步骤二:当DMA模块将位于socket相关联缓冲区中的数据传递给协议引擎时,执行第3次复制。

       你可能会在想,我们在调用sendfile发送数据的期间,如果另外一个进程将文件截断的话,会发生什么事情?如果进程没有为SIGBUS注册任何信号处理函数的话,sendfile系统调用返回被信号中断前已发送的字节数,并将全局变量errno置为成功。

      然而,如果在调用sendfile之前,从内核获得了文件租约,那么类似的,在sendfile调用返回前会收到RT_SIGNAL_LEASE。

       到此为止,我们已经能够避免内核进行多次复制,然而我们还存在一分多余的副本。这份副本也可以消除吗?当然,在硬件提供的一些帮助下是可以的。为了消除内核产生的素有数据冗余,需要网络适配器支持聚合操作特性。该特性意味着待发送的数据不要求存放在地址连续的内存空间中;相反,可以是分散在各个内存位置。

      在内核版本2.4中,socket缓冲区描述符结构发生了改动,以适应聚合操作的要求——这就是Linux中所谓的”零拷贝“。这种方式不仅减少了多个上下文切换,而且消除了数据冗余。从用户层应用程序的角度来开,没有发生任何改动,所有代码仍然是类似下面的形式:

sendfile(socket, file, len);

为了更好的理解所涉及的操作,请看图4

这里写图片描述

Figure 4. Hardware that supports gather can assemble data from multiple memory locations, eliminating another copy.

  • 步骤一:sendfile系统调用导致文件内容通过DMA模块被复制到内核缓冲区中。
  • 步骤二:数据并未被复制到socket关联的缓冲区内。取而代之的是,只有记录数据位置和长度的描述符被加入到socket缓冲区中。DMA模块将数据直接从内核缓冲区传递给协议引擎,从而消除了遗留的最后一次复制。

      由于数据实际上仍然由磁盘复制到内存,再由内存复制到发送设备,有人可能会声称这并不是真正的”零拷贝”。然而,从操作系统的角度来看,这就是”零拷贝”,因为内核空间内不存在冗余数据。应用”零拷贝”特性,出了避免复制之外,还能获得其他性能优势,例如更少的上下文切换,更少的CPU cache污染以及没有CPU必要计算校验和。

<think>好的,我现在需要回答用户关于如何实现零拷贝(zero-copy)访问接口的示例和代码解释。首先,我得回忆一下零拷贝的相关知识,确保自己理解正确。零拷贝技术主要是减少数据在内核空间和用户空间之间的拷贝次数,从而提升性能,特别是在处理大量数据传输时,比如文件传输或网络通信。 用户提到了几个引用,比如Linuxsendfile、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、付费专栏及课程。

余额充值