文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

还在为慢速数据传输苦恼?Linux 零拷贝技术来帮你!

2024-11-28 15:16

关注

一、传统拷贝:数据搬运的“旧时代”

为了理解零拷贝,我们先看看传统数据传输的工作方式。想象一下,我们需要把一个大文件从硬盘读取后发送到网络上。这听起来很简单,但实际上,传统的数据传输涉及多个步骤并占用大量 CPU 资源。

1. 一个典型的文件传输过程(没有 DMA 技术):

假设我们要将一个大文件从硬盘读取后发送到网络。以下是传统拷贝方式的详细步骤:

2. 来看个图,更直观点:

3. 数据传输的“四次拷贝”

在这个过程中,数据在系统中经历了四次拷贝:

4. 性能瓶颈分析

这种传统拷贝方式的问题显而易见:

这些问题在处理大文件或高频率传输时尤为明显,CPU 被迫充当“搬运工”,性能因此受到严重限制。那么, 有没有一种方法能够减少 CPU 的“搬运”工作?此时,DMA(Direct Memory Access,直接内存访问)技术登场了。

二、DMA:零拷贝的前奏

DMA(Direct Memory Access,直接内存访问) 是一种让数据在硬盘和内存之间直接传输的技术,不需要 CPU 逐字节参与。简单来说,DMA 是 CPU 的“好帮手”,减少了它的工作量。

1. DMA 如何帮 CPU?

在传统的数据传输中,CPU 需要亲自把数据从硬盘搬到内存,再送到网络,这很耗费 CPU 资源。而 DMA 的出现让 CPU 可以少干活:

有了 DMA,CPU 只需要说一句:“嘿,DMA,把数据从硬盘搬到内存去!” 然后 DMA 控制器就会接过这活,自动把数据从硬盘传到内核缓冲区,CPU 只需要在旁边监督一下。

2. 有了 DMA , 再来看看数据传输的过程:

为了更好地理解 DMA 在整个数据搬运中的角色,我们用图来说明:

说明:

3. 哪些步骤仍需 CPU 参与?

虽然 DMA 能帮 CPU 分担一些任务,但它并不能全权代理所有数据拷贝工作。CPU 还是得负责以下两件事:

就像请了一个帮手,但有些细致活儿还得自己干。所以,在高并发或大文件传输时,CPU 依旧会因为这些拷贝任务感到压力。

4. 总结一下

总结来说,DMA 确实减轻了 CPU 在数据传输中的负担,让数据从硬盘传输到内核缓冲区和内核缓冲区到网卡时几乎无需 CPU 的参与。然而,DMA 无法彻底解决数据在内核和用户空间之间的拷贝问题。CPU 依然需要进行两次数据搬运,特别是在高并发和大文件传输场景下,这个限制变得尤为突出。

三、零拷贝:让数据“直达”

因此,为了进一步减少 CPU 的参与,提升传输效率,Linux 推出了 零拷贝 技术。这项技术的核心目标是:让数据在内核空间内直接流转,避免在用户空间的冗余拷贝,从而最大限度减少 CPU 的内存拷贝操作,提高系统性能。

接下来,我们来详细看看 Linux 中的几种主要零拷贝实现方式:

注意:Linux 中零拷贝技术的实现需要硬件支持 DMA。

1. sendfile:最早的零拷贝方式

sendfile 是最早在 Linux 中引入的零拷贝方式,专为文件传输设计。

2. sendfile 的工作流程

通过 sendfile,整个传输过程 CPU 只需要一次数据拷贝,减少了 CPU 的使用。

3. 简单图解:

sendfile 图解说明:

4. sendfile 接口说明

sendfile函数定义如下:

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

返回值是实际传输的字节数,出错时返回 -1,并设置 errno 来指示错误原因。

5. 简单代码示例

#include 

int main() {
    int input_fd = open("input.txt", O_RDONLY);
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    bind(server_fd, (struct sockaddr *)&address, sizeof(address));
    listen(server_fd, 3);
    int client_fd = accept(server_fd, NULL, NULL);

    sendfile(client_fd, input_fd, NULL, 1024);

    close(input_fd);
    close(client_fd);
    close(server_fd);

    return 0;
}

这个例子展示了如何使用 sendfile 将本地文件发送到一个通过网络连接的客户端。只需要调用 sendfile,数据就能从 input_fd 直接传输到 output_fd。

6. 适用场景

sendfile 主要用于将文件数据直接传输到网络,非常适合需要高效传输大文件的情况,例如文件服务器、流媒体传输、备份系统等。

在传统的数据传输方式中,数据需要经过多个步骤:

总结来说: sendfile 可以让数据传输更加高效,减少 CPU 的干预,特别适合简单的大文件传输场景。然而,如果遇到更复杂的传输需求,比如要在多个不同类型的文件描述符之间移动数据,splice 则提供了一种更加灵活的方法。接下来我们来看看 splice 是如何实现这一点的。

四、splice :管道式零拷贝

splice 是 Linux 中另一种实现零拷贝的数据传输系统调用,专为在不同类型的文件描述符之间高效地移动数据而设计,适用于在内核中直接传输数据,减少不必要的拷贝。

1. splice 的工作流程

整个过程在内核空间内完成,避免了数据从内核空间到用户空间的往返拷贝,大大减少了 CPU 的参与,提高了系统性能。

2. 简单图解:

和 sendfile 图解类似,只是接口不一样。

splice 图解说明:

数据通过 splice 从文件描述符传输到网络 socket。数据首先通过 DMA 进入内核缓冲区,然后直接传输到网络 socket,整个过程避免了用户空间的介入,显著减少了 CPU 的拷贝工作。

3. 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);

返回值是实际传输的字节数,出错时返回 -1,并设置 errno 来指示错误原因。

4. 简单代码示例

int main() {
    int input_fd = open("input.txt", O_RDONLY);
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    bind(server_fd, (struct sockaddr *)&address, sizeof(address));
    listen(server_fd, 3);
    int client_fd = accept(server_fd, NULL, NULL);

    splice(input_fd, NULL, client_fd, NULL, 1024, SPLICE_F_MORE);

    close(input_fd);
    close(client_fd);
    close(server_fd);

    return 0;
}

这个例子展示了如何使用 splice 将本地文件直接发送到网络 socket,以实现高效的数据传输。

5. 适用场景

splice 适用于在文件描述符之间进行高效、直接的数据传输,例如从文件到网络 socket 的传输,或在文件、管道和 socket 之间传递数据。在这种情况下,数据在内核空间内完成传输,无需进入用户空间,从而显著减少拷贝次数和 CPU 的参与。另外 splice 特别适合需要灵活数据流动和减少 CPU 负担的场景,例如日志处理、实时数据流处理等。

6. sendfile 与 splice 的区别

虽然 sendfile 和 splice 都是 Linux 提供的零拷贝技术,用于高效地在内核空间传输数据,但它们在应用场景和功能上存在一些显著区别:

数据流动方式:

适用场景:

灵活性:

五、mmap + write:映射式零拷贝

除了以上两种方式,mmap + write 也是一种常见的零拷贝实现方式。这种方式主要是通过内存映射来减少数据拷贝的步骤。

1. mmap + write 的工作流程

使用 mmap 系统调用将文件映射到进程的虚拟地址空间中,这样数据就可以直接在内核空间和用户空间共享,而不需要额外的拷贝操作。

使用 write 系统调用将映射的内存区域直接写入到目标文件描述符中(比如网络 socket),完成数据传输。

这种方式减少了数据拷贝,提高了效率,适合需要灵活操作数据后再发送的场景。通过这种方式,数据不需要显式地从内核空间拷贝到用户空间,而是通过映射的方式共享,从而减少了不必要的拷贝。

2. 简单图解:

mmap + write 图解说明:

3. mmap 接口说明

mmap 函数的定义如下:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

返回值为映射内存区域的指针,出错时返回 MAP_FAILED,并设置 errno。

4. 简单代码示例

int main() {
    int input_fd = open("input.txt", O_RDONLY);
    struct stat file_stat;
    fstat(input_fd, &file_stat);

    char *mapped = mmap(NULL, file_stat.st_size, PROT_READ, MAP_PRIVATE, input_fd, 0);

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    bind(server_fd, (struct sockaddr *)&address, sizeof(address));
    listen(server_fd, 3);
    int client_fd = accept(server_fd, NULL, NULL);

    write(client_fd, mapped, file_stat.st_size);

    munmap(mapped, file_stat.st_size);
    close(input_fd);
    close(client_fd);
    close(server_fd);

    return 0;
}

这个例子展示了如何使用 mmap 将文件映射到内存,然后通过 write 将数据发送到网络连接的客户端。

5. 适用场景

mmap + write 适用于需要对文件数据进行灵活操作的场景,例如需要在发送数据前进行修改或部分处理。与 sendfile 相比,mmap + write 提供了更大的灵活性,因为它允许在用户态访问数据内容,这对于需要对文件进行预处理的应用场景非常有用,例如压缩、加密或者数据转换等。

然而,这种方式也带来了更多的开销,因为数据需要在用户态和内核态之间进行交互,这会增加系统调用的成本。因此,mmap + write 更适合那些需要在数据传输前进行一些自定义处理的情况,而不太适合纯粹的大文件高效传输。

六、tee:数据复制的零拷贝方式

tee 是 Linux 中的一种零拷贝方式,它可以把一个管道中的数据复制到另一个管道,同时保留原管道中的数据。这意味着数据可以同时被发送到多个目标,而不影响原来的数据流,非常适合日志记录和实时数据分析等需要把同样的数据送往不同地方的场景。

1. tee 的工作流程

数据复制到另一个管道:tee 系统调用可以将一个管道中的数据复制到另一个管道,而不改变原有的数据。这意味着数据可以在内核空间中被同时用于不同的目的,而无需经过用户空间的拷贝。

2. tee 接口说明

tee 函数的定义如下:

ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

返回值是实际复制的字节数,出错时返回 -1,并设置 errno 来指示错误原因。

3. 简单代码示例

int main() {
    int pipe_fd[2];
    pipe(pipe_fd);

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    bind(server_fd, (struct sockaddr *)&address, sizeof(address));
    listen(server_fd, 3);
    int client_fd = accept(server_fd, NULL, NULL);

    // 使用 tee 复制数据
    tee(pipe_fd[0], pipe_fd[1], 1024, 0);
    splice(pipe_fd[0], NULL, client_fd, NULL, 1024, SPLICE_F_MORE);

    close(pipe_fd[0]);
    close(pipe_fd[1]);
    close(client_fd);
    close(server_fd);

    return 0;
}

这个例子展示了如何使用 tee 将管道中的数据复制,并通过 splice 将数据发送到网络 socket,从而实现高效的数据传输和复制。

4. 适用场景

tee 非常适合需要将数据同时发送到多个目标的场景,比如实时数据处理、日志记录等。通过 tee,可以在内核空间内实现多目标数据复制,提高系统性能,减少 CPU 负担。

总结对比:

下面我将 Linux 的几种零拷贝方式做了总结,方便大家对比学习:

方法

描述

零拷贝类型

CPU 参与度

适用场景

sendfile

直接将文件数据发送到套接字,无需拷贝到用户空间。

完全零拷贝

极少,数据直接传输。

文件服务器、视频流传输等大文件场景。

splice

在内核空间内高效地在文件描述符之间传输数据。

完全零拷贝

极少,完全在内核内。

文件、管道与 socket 之间的复杂传输场景。

mmap + write

将文件映射到内存并使用 write 发送数据,灵活处理数据

部分零拷贝

中等,需要映射和写入。

数据需要处理或修改的场景,如压缩加密。

tee

将管道中的数据复制到另一个管道,无需消耗原始数据。

完全零拷贝

极少,数据复制在内核。

日志处理、实时数据监控等多目标场景。

最后

希望这篇文章让你对 Linux 的零拷贝技术有了更全面、更清晰的了解!这些技术看起来可能有些复杂,但一旦掌握后,你会发现它们非常简单, 并且在实际项目中非常实用。

来源:跟着小康学编程内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯