Advertisement

零拷贝网络传输技术研究与实践

阅读量:

作者:禅与计算机程序设计艺术

1.简介

改写说明

2.背景介绍

Linux下的零拷贝技术研究

在2.4内核版本之后,Linux引入的sendfile()系统调用属于零拷贝技术的一种。它在不复制数据到用户态的情况下,可以直接将文件内容发送到网络,而内核态只复制了一份副本。随着Linux社区的发展,Linux陆续推出了多个零拷贝功能:

  • 从2.6.28版本后的splice()函数实现了高效的数据传输,它可以在两个内存区域之间移动数据,而不需要进行实际的拷贝。此外,它还可用于在两个文件描述符之间移动数据。
  • 从2.6.39版本开始,基于userfaultfd机制的O_DIRECT标志被引入,通过在访问物理内存之前预先触发一个信号,这样就可以减少page fault发生的次数。
  • 从3.0版本开始,针对文件的系统调用比如open(),read(),write(),pread(),pwrite()实现了零拷CopyToUser()和CopyFromUser()系统调用,它们能够在用户态和内核态同时进行读写,大幅降低了数据复制带来的延迟。
  • 从3.17版本开始,VM_READAHEAD标志被引入,它使得当一个进程读取一个大文件时,将预读一些数据到页缓存中,而不是完全依赖page fault。这样,如果该进程再次访问相同的文件,则可以避免重复的I/O操作,从而提升了性能。
  • 在更高版本的内核中,AMD的zImage方案以及Gooch基础设施采用了Ring-Buffer的方式实现零拷贝技术。Ring Buffer是一种在数据路径上传输数据时,在内存中进行两次数据拷贝,但是只需要一次总线数据传送的机制。Ring Buffer通常用于网络数据传输以及磁盘操作等场景。

Linux下的零拷贝技术概览

在Linux系统中,零拷贝技术的实现方式多种多样,且在具体实现机制上存在显著差异。具体来说,Linux系统中几个重要的零拷贝技术的实现机制是:

splice()函数

该函数是Linux 2.6版本后引入的一种零拷贝技术,主要用于文件描述符或内存缓冲区之间的数据转移。在Linux系统中,它是一种较为通用的零拷贝方法。该函数能够将数据从一个文件描述符或内存缓冲区移动到另一个,而无需实际复制。由于避免了实际数据复制,该函数运行速度很快,同时消除了系统调用之间的上下文切换。该函数可解决以下问题:例如,数据在不同文件描述符之间的传输问题,以及减少系统调用切换带来的性能瓶颈。

  1. 文件描述符之间的数据传输。splice()函数可以用来在不同文件描述符之间传输数据块。
  2. 用户空间与内核空间的数据传输。splice()函数可以用来在用户空间与内核空间之间传输数据块。
  3. 数据块的分割和合并。如果数据源或目标处于阻塞情况下,可以使用splice()函数将数据块分割成小块,然后依次传输,并将这些小块重新组合起来。
  4. 单向数据传输机制。通过.splice()函数可以实现单向数据传输机制,将数据从一个方向传输到另一个方向。

splice()函数由三个系统调用组成:splice(), tee(), vmsplice()。

splice()函数用于在文件描述符或内存缓冲区之间转移数据。splice()函数的一般形式如下:

复制代码
    ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);

    
    代码解读

参数说明:

复制代码
 * `fd_in`:输入文件描述符。

 * `off_in`:指向loff_t类型的指针,表示输入文件偏移量。

 * `fd_out`:输出文件描述符。

 * `off_out`:指向loff_t类型的指针,表示输出文件偏移量。

 * `len`:要传输的字节数量。

flags标识符,其可选的取值包括SPLICE_F_MOVE、SPLICE_F_NONBLOCK、SPLICE_F_MORE和SPLICE_F_GIFT。

当flags参数的值设置为SPLICE_F_MOVE时,意味着在将源文件内容复制到目标文件后,源文件中对应位置的数据将被移除。

当flags参数的值设置为SPLICE_F_NONBLOCK时,表示在无法立即完成任务时,函数将返回-1并设置errno为EAGAIN。

flags参数的值为SPLICE_F_MORE时,表示后续将会有更多数据块传输。

flags参数设置为SPLICE_F_GIFT时,表示源文件中的对应位置数据不会被删除。如果这些数据块共享同一片物理内存空间,那么它们可以被用来提高性能。在所有情况下,splice()函数都不会分配新的物理内存,它所有的操作都是基于现有物理内存地址进行的。因此,在采用零拷贝机制时,无需对原始数据进行修改。

该函数与shell脚本中的tee命令功能相似,其主要作用是将数据同时复制到标准输出并存储到文件中。其一般形式如下:

复制代码
    ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

    
    代码解读

参数说明:

复制代码
 * `fd_in`:输入文件描述符。

 * `fd_out`:输出文件描述符。

 * `len`:要传输的字节数量。

 * `flags`:标识符。

flags参数设置为SPLICE_F_NONBLOCK时,该操作返回-1并将其errno字段设置为EAGAIN,表示在无法立即完成任务时触发此错误状态。

vmsplice()函数可用于在用户空间与内核空间之间移动数据。vmsplice()函数的基本形式如下:

复制代码
    ssize_t vmsplice(int fd, const struct iovec *iov, unsigned long nr_segs, unsigned int flags);

    
    代码解读

参数说明:

复制代码
 * `fd`:输出文件描述符。

 * `iov`:指向iovec结构体数组的指针。

 * `nr_segs`:表示iov数组中的元素个数。

 * `flags`:标识符。

flags参数的值被指定为VM_DONTWAIT时,则该操作将在无法立即完成时返回-1,并将errno字段设置为EAGAIN。

O_DIRECT标志

在Linux 2.6.39版本中,O_DIRECT标志被引入,它是一种基于用户错误FD机制的零拷贝技术。O_DIRECT标志允许系统调用read()在访问文件时,直接从内存空间中读取数据,而无需触发页面错误。通过这种机制,系统调用的执行效率显著提升,比从磁盘读取快得多,特别适用于对高吞吐量数据传输要求较高的场景。

当O_DIRECT启动某个文件时,系统调用read()将直接访问文件对应的内存区域的数据,而不必经过buffer cache,即使该文件已经存在于page cache中。因为未发生page fault,因此运行速度很快。

当应用程序打开一个文件时,若使用O_DIRECT权限,系统调用read()不会将数据传递给程序,而是直接加载到内存中。此时,程序无法确认数据是否已写入磁盘。然而,应用程序可以通过执行fsync()系统调用,确保数据已写入磁盘。

CopyToUser()和CopyFromUser()系统调用

在Linux内核3.0版本中引入了CopyToUser()和CopyFromUser()两个系统函数。这两个函数均属于文件级系统调用,旨在支持应用程序在用户空间与内核空间之间直接进行物理内存的读写操作。通过这些系统函数,应用程序能够直接操作内核的数据结构,无需先复制数据至内核缓冲区,再从缓冲区复制回用户空间。

当应用程序调用CopyToUser()和CopyFromUser()系统调用以打开文件时,系统将为该文件创建一个映射,从而可以直接访问其对应的物理内存。通过减少数据复制开销,这些系统调用能够显著提升系统的性能水平。

VM_READAHEAD标志

该标志于Linux 3.17版本中首次引入,其作用在于优化大文件读取效率。具体而言,该机制允许在读取大数据时预存部分数据至页缓存,从而减少了对page fault的依赖。这样,当该进程再次访问同一文件时,可以避免重复的I/O操作,从而提升了性能。

该机制能够显著降低随机IO,从而有效提升文件系统的性能水平,尤其是在磁盘IO性能较为受限的场景下。

Ring buffer

Ring Buffer是一种在数据路径上传输数据时,在内存中完成两次数据拷贝,仅需一次总线数据传输的机制。Ring Buffer常用于网络数据传输和磁盘操作等场景。Ring Buffer在传统数据传输方式中具有天然的优势。

基于Ring Buffer机制,系统无需将数据逐一拷贝到内核缓冲区,从而显著提升了处理效率。

Ring Buffer通常基于PCI Express、SATA、eSATA等固态存储设备。

3.基本概念术语说明

I/O模式

I/O模式指的是设备驱动程序处理I/O请求的方式,涵盖同步模式、异步模式、轮询模式和中断模式等多种类型。下面将简单介绍一下几种I/O模式的特点及其适用场景:

同步模式,其特点是必须在执行I/O操作的过程中,等待I/O操作完成,才能继续执行后续操作。常见的I/O模型包括软驱和打印机,它们都采用同步模式。

异步模式:异步模式是指在执行I/O操作时,让主线程在I/O操作完成后等待,以便继续执行后续操作。其中,典型的I/O模型包括网络数据传输等。

轮询模式:轮询模式是一种检测机制,定期对I/O事件进行检测,并采取相应的行动以确保其正常运行。串口通信协议是一种经典的I/O模型,广泛应用于工业控制领域。

中断模式:中断模式是指驱动程序将硬件中断通知主线程,主线程一般负责处理相关事件,通常采用回调函数来处理中断。典型的I/O模型,如USB,展示了这一模式的应用。

DMA

在计算机体系结构中,DMA(Direct Memory Access,直接内存存取)是一种允许CPU直接与主存进行数据传输的机制,无需中间人参与。通过采用DMA技术,CPU与主存之间可以交替进行数据传输,从而有效减少CPU资源的闲置。在Linux操作系统中,通过Scatter-Gather机制,可以实现DMA数据传输。

DMA引擎

DMA数据传输引擎是由硬件和驱动程序协同作用形成的模块,主要负责数据缓冲区的管理、DMA控制功能以及数据传输任务的执行。该模块通常由DMA控制器、通道引脚和DMA通道三部分构成,具体功能包括:数据缓冲区的管理、DMA控制功能以及数据传输任务的执行。

数据缓冲管理:DMA引擎负责管理内存缓存区列表,每个缓冲区负责传输一个待处理的数据块。

  1. DMA控制:基于配置的传输模式,DMA控制器生成相应的触发信号,以控制DMA通道进行数据传输操作。

数据传输:DMA引擎利用DMA通道将缓冲区中的数据输出至外部设备,同时也能从外部设备输入数据至缓冲区。

socket

在编程网络时,socket常被用来建立不同主机之间的连接。作为一个抽象层,socket有效地隐藏了底层网络协议的复杂性。socket不仅提供了应用程序与TCP/IP协议族之间的接口,还被设计为应用程序发送请求或接收响应的通道。作为应用层协议簇的一部分,socket在数据传输中扮演着关键角色。

sendfile()函数

sendfile()函数是Linux系统提供的核心系统调用,其显著特点在于无需将数据复制到用户空间,而仅在内核态生成并发送数据到网络。该函数具备灵活性,支持在文件描述符与套接字之间以及文件与套接字之间进行数据传输。

4.核心算法原理和具体操作步骤以及数学公式讲解

概念

零拷贝技术是一种旨在提高网络效率的技术。它基于用户态和内核态之间的数据传输机制,无需实际复制数据。其主要目标是降低数据复制过程中的额外资源消耗。在零拷贝技术中,系统调用可以直接将数据从物理内存传输到网络,而无需将数据复制到用户态的缓冲区。其优势主要体现在以下几个方面:首先,这种技术能够显著减少数据传输过程中的开销;其次,通过直接传输数据,可以提高网络处理效率;最后,避免数据复制操作可以降低系统的资源消耗。

该技术通过优化资源利用,实现了对内存和CPU资源的高效管理。零拷贝技术通过避免额外内存资源的占用,降低了数据复制过程中内存的消耗。

优化性能。无拷ipy技术有助于显著提升数据包传输速度,特别适用于网络数据包传输。

  1. 减少数据传输延迟。通过零拷贝技术,可以有效减少数据包传输延迟,特别适用于网络环境下的数据传输。

sendfile()函数

sendfile()函数是Linux系统提供的一个实现文件在内核态与用户态之间传输的系统调用。其函数原型定义为:

复制代码
    ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
    
    
    代码解读

其中,out_fd和in_fd分别表示写入数据的目的端和读取数据的源端的文件描述符。count参数决定了要传输的字节数量,而offset参数则指示偏移量的位置。

sendfile()函数主要包含以下步骤:

将数据复制至内核的页缓存中。复制数据至内核页缓存时,系统仅分配物理内存空间,不再分配虚拟内存,从而避免页面错误的出现。

首先,将数据复制到用户端存储区域中。随后,当数据被复制到用户端存储区域后,系统将通知应用程序有可用的数据。

通过DMA引擎实现数据传输。DMA引擎负责将数据从内核空间的页缓存拷贝到网卡缓冲区中,或者将数据从网卡缓冲区拷贝到内核空间的页缓存中。

在数据被复制至网卡缓冲区的过程中,系统会腾出用户空间和内核空间的缓冲区。当数据完成复制操作后,系统会释放用户空间的缓冲区。

以上四个步骤构成了sendfile()函数的核心操作流程。

splice()函数

该函数自Linux 2.6内核版本引入,属于零拷贝技术的一种。该接口允许跨越两个文件描述符、两个内存缓冲区或两个管道之间的数据传输。splice()函数的原型为:

复制代码
    ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
    
    
    代码解读

其中,参数fd_in和fd_out各自表示输入文件描述符和输出文件描述符,off_in和off_out各自表示输入文件偏移量和输出文件偏移量。len表示要传输的字节数。flags为splice()函数的标识符,可选多个值,具体含义如下:

  • SPLICE_F_MOVE:默认情况下,源文件中的数据将被删除。
  • SPLICE_F_NONBLOCK:当无法立即完成操作时,该函数将返回-1并设置errno为EAGAIN。
  • SPLICE_F_MORE:表示传输过程中还有数据需要继续传输。
  • SPLICE_F_GIFT:该参数默认情况下将保留源文件中的数据。

splice()函数主要包含以下步骤:

在生成新的页缓存和DMA缓冲区时,系统不会触发任何页面错误。负责分配新的页缓存和DMA缓冲区。

验证数据长度。该函数用于验证是否拥有足够的存储空间来容纳所需的数据。如果不够,该函数会产生-ENOMEM错误,并警告调用者为其预留更大的存储空间。

数据复制任务由函数完成。数据起始偏移量和传输的字节数由调用者指定,该函数将数据复制到新的页缓存中,并分配一个新的dma缓冲区。

  1. 将数据发送至DMA引擎。在数据完成准备后,函数将发送数据给DMA引擎。DMA引擎将数据从页缓存传输至DMA缓冲区,或者从DMA缓冲区传输至页缓存中。

  2. 更新文件偏移量。函数更新文件偏移量,返回成功。

以上五个步骤构成了splice()函数的核心操作流程。

tee()函数

tee()函数的原型为:

复制代码
    ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
    
    
    代码解读

该函数通过复制数据将数据从一个文件描述符传递到另一个文件描述符,并将数据输出到标准输出。具体操作步骤如下:

在处理新的页缓存和DMA缓冲区分配任务时,系统不会触发任何页面错误。系统在创建新的页缓存和DMA缓冲区时,不会触发任何页面错误。

用户设置数据起始偏移量和传输的字节数后,该函数完成数据复制任务,并为复制操作分配新的dma缓冲区。

在数据准备完成之后,程序将向DMA引擎发送数据块。DMA引擎将执行数据从页缓存到DMA缓冲区的拷贝,或从DMA缓冲区到页缓存的转移。

  1. 更新文件偏移量。函数更新文件偏移量,返回成功。

以上四个步骤构成了tee()函数的核心操作流程。

vmsplice()函数

vmsplice()函数的原型为:

复制代码
    ssize_t vmsplice(int fd, const struct iovec *iov, unsigned long nr_segs, unsigned int flags);
    
    
    代码解读

该函数通过vm_area_struct结构体来管理用户空间缓冲区。其主要功能是将数据从用户空间缓冲区复制到内核缓冲区。具体操作步骤如下:首先,它会获取用户空间缓冲区的可用空间;然后,它会将数据从用户空间缓冲区复制到内核缓冲区;最后,它会更新内核缓冲区的可用空间。

生成新的页缓存和DMA缓冲区。当生成新的页缓存和DMA缓冲区时,系统不会触发任何页面错误。

  1. 获取用户空间缓冲区。调用者传递用户空间缓冲区地址和长度信息。

复制数据。遍历用户空间缓存区,同时将数据转移至新的页缓存区域,并分配新的DMA缓冲区。

  1. 被提交至DMA引擎。在数据完成准备后,该函数将被提交至DMA引擎。DMA引擎将被用来将数据从页缓存被复制至DMA缓冲区,或者从DMA缓冲区被复制至页缓存中。

  2. 返回成功。

以上五个步骤构成了vmsplice()函数的核心操作流程。

5.具体代码实例和解释说明

sendfile()函数示例代码

首先,建议查看sendfile()函数的示例代码,以主要演示如何利用该函数获取本地文件的具体内容,并通过网络传输给其他主机。

客户端代码(sender.cpp):

复制代码
    #include <iostream>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    using namespace std;
    
    int main() {
      // 服务器地址和端口号
      char server[] = "localhost"; 
      int port = 9999;
    
      // 创建套接字
      int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
      // 设置服务器地址和端口号
      sockaddr_in addr;
      memset(&addr, 0, sizeof(sockaddr_in));
      addr.sin_family = AF_INET;
      addr.sin_port = htons(port);
      inet_aton(server, &addr.sin_addr);
    
      // 连接服务器
      connect(sockfd, (sockaddr*)&addr, sizeof(sockaddr_in));
    
      // 打开本地文件
      int filefd = open("localfile", O_RDONLY);
    
      // 准备发送文件头部信息
      char header[10] = {'\0'};
      sprintf(header, "%d:%ld", getpid(), lseek(filefd, 0L, SEEK_END));
    
      // 发送文件头部信息
      write(sockfd, header, strlen(header)+1);
    
      // 发送文件
      sendfile(sockfd, filefd, NULL, 0x7fffffff);
    
      close(sockfd);
      close(filefd);
    
      return 0;
    }
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

服务端代码(receiver.cpp):

复制代码
    #include <iostream>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    using namespace std;
    
    int main() {
      // 监听端口号
      int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    
      sockaddr_in addr;
      memset(&addr, 0, sizeof(sockaddr_in));
      addr.sin_family = AF_INET;
      addr.sin_port = htons(9999);
      addr.sin_addr.s_addr = INADDR_ANY;
    
      bind(listenfd, (sockaddr*)&addr, sizeof(sockaddr_in));
    
      listen(listenfd, SOMAXCONN);
    
      while (true) {
    // 等待连接
    int connfd = accept(listenfd, NULL, NULL);
    
    // 接收文件头部信息
    char header[10];
    read(connfd, header, 10);
    
    // 解析文件头部信息
    pid_t pid;
    off_t offset;
    sscanf(header, "%d:%ld", &pid, &offset);
    
    // 创建文件
    char filename[30];
    sprintf(filename, "/tmp/%d.%lld", pid, static_cast<long long>(offset));
    int filefd = creat(filename, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
    
    // 接收文件
    recvfile(filefd, connfd, NULL, 0x7fffffff);
    
    cout << "Received: " << filename << endl;
    
    close(filefd);
    close(connfd);
      }
    
      close(listenfd);
    
      return 0;
    }
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

以上示例代码的详细操作流程如下:

  1. 客户端打开本地文件,获取文件的大小和偏移量,构造文件头部信息。

  2. 客户端通过套接字向服务端发送文件头部信息。

  3. 服务端接受客户端发送的消息,解析文件头部信息,创建对应的文件。

服务端通过recvfile()函数接收客户端发送的文件内容,并将其写入新创建的文件中。

  1. 客户端关闭文件句柄和套接字,退出程序。

splice()函数示例代码

了解splice()函数的示例代码,主要演示如何使用该函数从文件描述符读取数据并将其写入另一个文件描述符。

客户端代码(client.cpp):

复制代码
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/uio.h>
    #include <linux/aio_abi.h>
    #include <sys/ioctl.h>
    
    const int fds[] = {0, 1};
    char buf1[4096], buf2[4096];
    size_t nbytes1, nbytes2;
    
    void printmsg(const char *str,...) {
    va_list args;
    va_start(args, str);
    vprintf(str, args);
    va_end(args);
    printf("\n");
    }
    
    int do_splice() {
    /* prepare data */
    for (int i=0; i<sizeof(buf1)/2; ++i)
        *(short *)(buf1+i) = i+1;
    strcpy((char*)buf2, "hello world!");
    
    /* copy from one FD to another using the splice system call */
    nbytes1 = sizeof(buf1)/2;
    nbytes2 = strlen(buf2);
    
    if (splice(fds[0], &nbytes1, fds[1], &nbytes2, sizeof(buf2)-strlen(buf2), 0)) {
        perror("splice failed");
        exit(-1);
    }
    
    /* check results */
    bool ok = true;
    for (int i=0; i<(int)(nbytes1 + nbytes2)/2 && ok; ++i) {
        short x = *(short *)(((char *)buf2)+(i*(nbytes2+(int)nbytes2%2)));
        if ((x!= i+1) || (*((char *)buf2+nbytes2+i)!='!'))
            ok = false;
    }
    
    if (!ok)
        printmsg("*** error: data corruption detected ***");
    
    else if (!(nbytes1 == sizeof(buf1)/2 &&!strcmp((char*)buf2, "hello")))
        printmsg("received unexpected data:");
    else
        printmsg("%lu bytes received correctly", nbytes1 + nbytes2);
    
    return 0;
    }
    
    int main() {
    aio_context_t ctx;
    io_event event;
    
    if (io_setup(1, &ctx)) {
        perror("io_setup failed");
        return -1;
    }
    
    int rc = do_splice();
    
    if (rc) goto out;
    
    while (true) {
        if (io_getevents(ctx, 1, 1, &event, NULL)) {
            perror("io_getevents failed");
            break;
        }
    
        switch (event.data) {
            case IOCB_CMD_PREAD:
                if (event.obj->result == nbytes1)
                    printmsg("%lu bytes read successfully", nbytes1);
                else
                    printmsg("*** error: pread returned %zd instead of %lu ***",
                             event.obj->result, nbytes1);
                break;
    
            case IOCB_CMD_PWRITE:
                if (event.obj->result == nbytes2)
                    printmsg("%lu bytes written successfully", nbytes2);
                else
                    printmsg("*** error: pwrite returned %zd instead of %lu ***",
                             event.obj->result, nbytes2);
                break;
    
            default:
                break;
        }
    }
    
    out:
    io_destroy(ctx);
    
    return rc? -1 : 0;
    }
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

服务端代码(server.cpp):

复制代码
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/uio.h>
    #include <linux/aio_abi.h>
    #include <sys/ioctl.h>
    
    const int fds[] = {1, 0};
    char buf1[4096], buf2[4096];
    size_t nbytes1, nbytes2;
    
    void printmsg(const char *str,...) {
    va_list args;
    va_start(args, str);
    vprintf(str, args);
    va_end(args);
    printf("\n");
    }
    
    int do_splice() {
    /* create buffers and fill them with test data */
    for (int i=0; i<sizeof(buf1)/2; ++i)
        *(short *)(buf1+i) = i+1;
    strcpy((char*)buf2, "hello world!");
    
    /* copy from one FD to another using the splice system call */
    nbytes1 = sizeof(buf1)/2;
    nbytes2 = strlen(buf2);
    
    if (splice(fds[0], NULL, fds[1], NULL, sizeof(buf1)*2, SPLIECE_F_NONBLOCK|SPLIECE_F_MORE)) {
        perror("splice failed");
        exit(-1);
    }
    
    /* verify that all data has been copied */
    if (!(nbytes1 == sizeof(buf1)/2 &&
         !(memchr(buf1, '\0', nbytes1)))) {
        printmsg("*** error: not enough input data available ***");
        return -1;
    }
    
    /* write output data back to client */
    if (splice(fds[0], NULL, fds[1], NULL, nbytes1, 0)) {
        perror("splice failed");
        return -1;
    }
    
    /* wait for remaining output data to be written */
    while (true) {
        struct pollfd pfd = {.fd = fds[1],.events = POLLOUT};
        int rc = poll(&pfd, 1, -1);
        if (rc == -1) {
            perror("poll failed");
            exit(-1);
        }
        if (rc == 1 &&!(pfd.revents&POLLOUT))
            break;
    }
    
    /* verify output data was completely sent by waiting until client is ready
       to receive more data */
    while (true) {
        struct pollfd pfd = {.fd = fds[1],.events = POLLIN};
        int rc = poll(&pfd, 1, -1);
        if (rc == -1) {
            perror("poll failed");
            exit(-1);
        }
        if (rc == 1 &&!(pfd.revents&POLLIN))
            break;
    }
    
    return 0;
    }
    
    int main() {
    aio_context_t ctx;
    io_event event;
    
    if (io_setup(1, &ctx)) {
        perror("io_setup failed");
        return -1;
    }
    
    int rc = do_splice();
    
    if (rc) goto out;
    
    while (true) {
        if (io_getevents(ctx, 1, 1, &event, NULL)) {
            perror("io_getevents failed");
            break;
        }
    
        switch (event.data) {
            case IOCB_CMD_PWRITE:
                if (event.obj->result == nbytes1)
                    printmsg("%lu bytes written successfully", nbytes1);
                else
                    printmsg("*** error: pwrite returned %zd instead of %lu ***",
                             event.obj->result, nbytes1);
                break;
    
            case IOCB_CMD_PREAD:
                if (event.obj->result == nbytes2)
                    printmsg("%lu bytes read successfully", nbytes2);
                else
                    printmsg("*** error: pread returned %zd instead of %lu ***",
                             event.obj->result, nbytes2);
                break;
    
            default:
                break;
        }
    }
    
    out:
    io_destroy(ctx);
    
    return rc? -1 : 0;
    }
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

以上示例代码的详细操作流程如下:

  1. 客户端准备输入数据,创建输出数据缓冲区。

应用程序通过调用splice()函数,将输入数据从文件描述符fd[0]读取自外部文件传输至输出数据缓冲区。

  1. 客户端验证输出数据是否正确。

应用程序通过调用splice()函数,将输入数据从文件描述符fd[0]读取自外部文件传输至输出数据缓冲区。

  1. 客户端等待输出数据完全传输完毕,并等待客户端接收数据。

  2. 服务端创建套接字,接收客户端数据。

  3. 服务端验证输入数据是否正确。

  4. 服务端调用splice()函数,将输出数据从文件描述符fd[1]拷贝到套接字。

  5. 服务端等待输出数据完全传输完毕。

6.未来发展趋势与挑战

大文件传输

在传输过程中,网络数据包传输必须进行数据复制,尤其在传输超大文件时,零拷贝技术能够在一定程度上缓解内存占用过高的问题。然而,随着存储技术的迅速推广,零拷贝技术在大文件传输中的作用逐渐提升。因此,零拷贝技术的应用范围不断扩大,并在提高网络传输效率的同时,能够适应存储设备的发展趋势,把握新机遇,抓住存储设备发展的新领域。

协议优化

相较于其他技术,零拷贝技术在提升网络效率方面表现突出,但其优势可能远超预期。具体而言,SSH协议在网络传输中对数据拷贝的优化效果显著超过Zero-copy技术。基于网络协议的特殊性,未来可能会有更多的场景可以应用零拷贝技术。

NIC性能优化

当前,普遍采用的网络接口芯片(NIC)都实现了显著的性能提升。鉴于此,零拷贝技术在提升网络性能水平方面依然发挥着关键作用。

7.参考资料

全部评论 (0)

还没有任何评论哟~