引言

以下翻译自:Zero Copy I: User-Mode Perspective

零拷贝是什么?

为了更好地理解问题的解决方案,我们首先需要理解问题本身。让我们来看看什么是参与网络服务器的简单过程dæmon服务数据存储在一个文件通过网络客户端。下面是一些示例代码:

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

看起来很简单;您会认为只有这两个系统调用不会带来太多开销。事实上,这与事实相去甚远。在这两个调用之后,数据至少复制了四次,并且几乎执行了相同数量的用户/内核上下文切换。(实际上这个过程要复杂得多,但我想让它保持简单)。为了更好地了解所涉及的流程,请看图1。顶部显示上下文切换,底部显示复制操作。

图1。复制两个示例系统调用

第一步:read系统调用导致上下文从用户模式切换到内核模式。第一个副本由DMA引擎执行,它从磁盘读取文件内容并将其存储到内核地址空间缓冲区中。

第二步:将数据从内核缓冲区复制到用户缓冲区,read系统调用返回。调用的返回导致上下文从内核切换回用户模式。现在数据存储在用户地址空间缓冲区中,它可以再次开始向下移动。

第三步:write系统调用导致上下文从用户模式切换到内核模式。执行第三次复制,再次将数据放入内核地址空间缓冲区。不过,这一次,数据被放入一个不同的缓冲区,一个专门与套接字关联的缓冲区。

第四步:write系统调用返回,创建我们的第四个上下文切换。当DMA引擎将数据从内核缓冲区传递到协议引擎时,会独立地、异步地进行第四次复制。你可能会问自己,“独立和异步是什么意思?”在呼叫返回之前,数据没有传输吗?“呼叫返回,实际上并不保证传输;它甚至不能保证传输的开始。它只是意味着以太网驱动程序在它的队列中有空闲的描述符,并且已经接受我们的数据进行传输。可能有许多包在我们的前面排队。除非驱动程序/硬件实现优先级环或队列,否则数据是在先进先出的基础上传输的。(图1中分叉的DMA副本演示了最后一个副本可以延迟的事实)。

正如您所看到的,很多数据复制实际上并不是必要的。可以消除一些重复,以减少开销并提高性能。作为一名驱动程序开发人员,我使用的硬件具有一些非常高级的特性。一些硬件可以完全绕过主存,直接将数据传输到另一个设备。这个特性消除了系统内存中的副本,这是一个很好的特性,但是并不是所有的硬件都支持它。还有一个问题是来自磁盘的数据必须为网络重新打包,这带来了一些复杂性。为了消除开销,我们可以从消除内核和用户缓冲区之间的一些复制开始。

消除副本的一种方法是跳过调用read,而是调用mmap。例如:

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

为了更好地了解所涉及的流程,请看图2。上下文切换保持不变。

图2。调用mmap

第一步:mmap系统调用导致DMA引擎将文件内容复制到内核缓冲区。然后与用户进程共享缓冲区,而不需要在内核和用户内存空间之间执行任何复制。

第二步:write系统调用导致内核将原始内核缓冲区中的数据复制到与套接字相关的内核缓冲区中。

第三步:当DMA引擎将数据从内核套接字缓冲区传递到协议引擎时,发生第三次复制。

通过使用mmap而不是read,我们减少了内核必须复制的数据量的一半。当传输大量数据时,这将产生相当好的结果。然而,这种改善不是没有代价的;在使用mmap write方法时存在一些隐藏的陷阱。当您在内存中映射一个文件,然后调用write,而另一个进程截断相同的文件时,您将陷入其中之一。您的写系统调用将被总线错误信号SIGBUS中断,因为您执行了错误的内存访问。该信号的默认行为是终止进程并转储内核——这对于网络服务器来说不是最理想的操作。有两种方法可以解决这个问题。

第一种方法是为SIGBUS信号安装一个信号处理程序,然后在处理程序中简单地调用return。通过这样做,write系统调用将返回它在被中断之前写入的字节数,并将errno设置为成功。让我指出,这将是一个糟糕的解决方案,只解决症状,而不是问题的根源。因为SIGBUS信号表明进程出现了严重错误,所以我不建议使用它作为解决方案。

第二种解决方案涉及从内核租用文件(在Microsoft Windows中称为“机会锁定”)。这是解决这个问题的正确方法。通过在文件描述符上使用租借,您可以对特定文件的内核进行租借。然后可以从内核请求读/写租约。当另一个进程试图截断您正在传输的文件时,内核会向您发送实时信号,即RT_SIGNAL_LEASE信号。它告诉您内核正在破坏您对该文件的读或写租约。在程序访问无效地址并被SIGBUS信号终止之前,写调用被中断。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;
}

您应该在映射文件之前获得您的租约,并在完成之后取消您的租约。这是通过使用F_UNLCK的租赁类型调用fcntl F_SETLEASE实现的。

Sendfile

在内核版本2.1中,引入了sendfile系统调用,以简化通过网络和两个本地文件之间的数据传输。sendfile的引入不仅减少了数据复制,还减少了上下文切换。像这样使用它:

sendfile(socket, file, len);

为了更好地了解所涉及的流程,请看图3。

图3。用Sendfile替换读和写

第一步:sendfile系统调用导致DMA引擎将文件内容复制到内核缓冲区。然后,内核将数据复制到与套接字关联的内核缓冲区中。

步骤2:当DMA引擎将数据从内核套接字缓冲区传递到协议引擎时,发生第三次复制。

您可能想知道如果另一个进程截断了我们使用sendfile系统调用传输的文件,会发生什么情况。如果我们不注册任何信号处理程序,sendfile调用只返回它在中断之前传输的字节数,errno将被设置为成功。

但是,如果我们在调用sendfile之前从内核获得文件的租约,则行为和返回状态是完全相同的。我们还将在sendfile调用返回之前获得RT_SIGNAL_LEASE信号。

到目前为止,我们已经能够避免让内核复制几个副本,但是仍然只剩下一个副本。这也能避免吗?当然,在硬件的帮助下。为了消除内核所做的所有数据重复,我们需要一个支持收集操作的网络接口。这仅仅意味着等待传输的数据不需要在连续的内存中;它可以分散在不同的内存位置。在内核版本2.4中,修改了套接字缓冲区描述符以适应那些需求——在Linux下称为零拷贝。这种方法不仅减少了多个上下文切换,还消除了处理器造成的数据重复。对于用户级应用程序,一切都没有改变,所以代码仍然是这样的:

sendfile(socket, file, len);

为了更好地了解所涉及的流程,请看图4。



图4。支持收集的硬件可以从多个内存位置收集数据,从而消除了另一个副本。

第一步:sendfile系统调用导致DMA引擎将文件内容复制到内核缓冲区。

第二步:没有数据被复制到套接字缓冲区。相反,只有包含关于数据位置和长度信息的描述符才会被附加到套接字缓冲区中。DMA引擎直接将数据从内核缓冲区传递到协议引擎,从而消除了剩余的最终副本。

因为数据实际上仍然是从磁盘复制到内存,从内存复制到连接,所以有些人可能会认为这不是真正的零拷贝。但是,从操作系统的角度来看,这是零拷贝,因为数据不是在内核缓冲区之间复制的。在使用零拷贝时,除了避免拷贝之外,还可以获得其他性能优势,比如更少的上下文切换、更少的CPU数据缓存污染和更少的CPU校验和计算。

下面是两篇非常好的文章,收藏:

Linux 中的零拷贝技术,第 1 部分

Linux 中的零拷贝技术,第 2 部分

深入了解Netty【二】零拷贝的更多相关文章

  1. NIO学习笔记,从Linux IO演化模型到Netty—— Linux零拷贝

    这里只是感性地认识Linux零拷贝,不涉及具体细节. 1.Linux传统的数据拷贝 用户进程是不能直接访问文件系统的,要先切换到内核态,发起系统调用,DMA把磁盘中的数据写入内核空间,内核再把数据拷贝 ...

  2. Netty 零拷贝(三)Netty 对零拷贝的改进

    Netty 零拷贝(三)Netty 对零拷贝的改进 Netty 系列目录 (https://www.cnblogs.com/binarylei/p/10117436.html) Netty 的&quo ...

  3. 感悟优化——Netty对JDK缓冲区的内存池零拷贝改造

    NIO中缓冲区是数据传输的基础,JDK通过ByteBuffer实现,Netty框架中并未采用JDK原生的ByteBuffer,而是构造了ByteBuf. ByteBuf对ByteBuffer做了大量的 ...

  4. Netty基础系列(4) --堆外内存与零拷贝详解

    前言 到目前为止,我们知道Nio当中有三个最最核心的组件,分别是:Selelctor,Channel,Buffer.在Netty基础系列(3) --彻底理解NIO 这一篇文章中只是进行了大致的介绍. ...

  5. Netty源码解析 -- 零拷贝机制与ByteBuf

    本文来分享Netty中的零拷贝机制以及内存缓冲区ByteBuf的实现. 源码分析基于Netty 4.1.52 Netty中的零拷贝 Netty中零拷贝机制主要有以下几种 1.文件传输类DefaultF ...

  6. 【Netty技术专题】「原理分析系列」Netty强大特性之ByteBuf零拷贝技术原理分析

    零拷贝Zero-Copy 我们先来看下它的定义: "Zero-copy" describes computer operations in which the CPU does n ...

  7. Java基础-零拷贝技术应用案例

    Java基础-零拷贝技术应用案例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 零拷贝技术在Hadoop生态圈中很多组件得到应用,典型的比如kafka组件,它就很成功的应用了零拷贝 ...

  8. netty如何实现零拷贝

    根据 Wiki 对 Zero-copy 的定义: "Zero-copy" describes computer operations in which the CPU does n ...

  9. Netty 零拷贝(一)NIO 对零拷贝的支持

    Netty 零拷贝(二)NIO 对零拷贝的支持 Netty 系列目录 (https://www.cnblogs.com/binarylei/p/10117436.html) 非直接缓冲区(HeapBy ...

随机推荐

  1. Elasticsearch权威指南(中文版)

    Elasticsearch权威指南(中文版) 下载地址: https://pan.baidu.com/s/1bUGJmwS2Gp0B32xUyXxCIw 扫码下面二维码关注公众号回复100010 获取 ...

  2. TCP学习指北

    限于博主水平有限不敢说指南,但应该能够避免刚学TCP的同学出现找不着北的情况. TCP与UDP的区别 区别: UDP是无连接的,而TCP是面向连接的,传数据前要先建立连接. UDP可以一对多,多对多通 ...

  3. Spring——IOC(控制反转)与DI(依赖注入)

    IOC与DI的理解及使用 控制反转IOC(Inversion of Control)是一种设计思想,DI(依赖注入)是实现IOC的一种方法.在没有IOC的程序中,我们使用面向对象编程,对象的创建于对象 ...

  4. 源码剖析Springboot自定义异常

    博主看到新服务是封装的自定义异常,准备入手剖析一下,自定义的异常是如何进行抓住我们请求的方法的异常,并进行封装返回到.废话不多说,先看看如何才能实现封装异常,先来一个示例: @ControllerAd ...

  5. centos go 安装 使用

    #goland 确保能ping通百度[root@z my_project]# vi /etc/resolv.conf# Generated by NetworkManagersearch locald ...

  6. 聊聊MySQL主从复制的几种复制方式

    目录 异步复制 多线程复制 增强半同步复制 异步复制 MySQL的复制默认是异步的,主从复制至少需要两个MYSQL服务,这些MySQL服务可以分布在不同的服务器上,也可以在同一台服务器上. MySQL ...

  7. Python钉钉报警及Zabbix集成钉钉报警

    钉钉报警设置 创建群机器人 11111 接口地址 发送短消息 发送普通消息 import requests import json url = 'https://oapi.dingtalk.com/r ...

  8. Linux内核之 内存管理

    前面几篇介绍了进程的一些知识,从这篇开始介绍内存.文件.IO等知识,发现更不好写哈哈.但还是有必要记录下自己的所学所思.供后续翻阅,同时写作也是一个巩固的过程. 这些知识以前有文档涉及过,但是角度不同 ...

  9. mycli工具mysql命令自动补全

    简介 MyCli 是一个 MySQL 的命令行客户端,可以实现自动补全和语法高亮.MyCli 也可用于 MariaDB 和Percona. 项目地址:http://mycli.net/ 安装 pip安 ...

  10. SNN对抗攻击笔记

    SNN对抗攻击笔记: 1. 解决SNN对抗攻击中脉冲与梯度数据格式不兼容性以及梯度消失问题: G2S Converter.Gradient Trigger[1] 2. 基于梯度的对抗攻击方式: FGS ...