让你的代码运行更快:掌握零拷贝技术

引言

想象一下,你是一个空调生产商,你通常通过各个省市县的代理商将产品卖给消费者。但是,如果你采用电商直销模式,你可以直接将产品卖给消费者,跳过了中间商,这将大大提高效率。在计算机世界中,我们也有类似的问题,那就是数据拷贝。CPU需要将数据从硬件设备拷贝到内核内存,再拷贝到用户内存,这个过程中涉及了多次拷贝操作。为了提高效率,我们引入了一种称为"零拷贝"的技术。

为什么需要零拷贝?

在我们探讨为什么需要零拷贝之前,首先需要理解计算机的操作系统是如何设计的。操作系统分为两个模式:用户模式和内核模式,有时也成为用户态和内核态。

在用户模式下,运行的软件(比如你的文档编辑器、浏览器等)没有直接访问硬件和内核数据结构的权限。它们只能通过系统调用间接地请求操作系统提供的服务,比如读写文件、打开网络连接等。这样做的好处是,如果用户程序出错,也不会影响到系统的正常运行。

与之相反,内核模式下的软件(主要是操作系统本身)具有对硬件和内核数据结构的直接访问权限。这使得操作系统可以有效地管理硬件资源,如内存、CPU等,同时也使得它可以控制和限制用户程序的行为,保证整个系统的稳定和安全。

操作系统采取这种用户模式和内核模式的设计,主要是出于以下两个原因:

第一,为了向用户程序提供良好的底层资源接口。你可以把操作系统想象成一个大管家,它负责管理计算机的所有硬件和软件资源,包括CPU、内存、硬盘、网络等。为了让用户程序更方便地使用这些资源,操作系统需要提供一套良好的接口。

第二,为了进行资源的统筹管理和安全管理。操作系统不仅需要管理资源,还需要确保资源的安全。例如,它需要防止一个程序占用过多的CPU时间,或者访问其他程序的内存空间。为了做到这一点,操作系统需要在用户模式和内核模式之间进行切换,这里称之为上下文切换。

然而,这种设计也带来了一些问题。其中最大的问题就是数据拷贝。在读写操作中,数据需要从硬件设备经过内核内存,再到用户内存,这个过程中涉及到多次的拷贝操作。首先,CPU需要把数据从硬件设备拷贝到内核内存;然后,CPU需要把数据从内核内存拷贝到用户内存。每次的拷贝操作都会消耗系统资源。

而且,每次的上下文切换也会消耗系统资源。当操作系统从用户模式切换到内核模式,或者从内核模式切换到用户模式时,都需要保存当前的状态,然后恢复新的状态。这就像是你在两个房间之间跑来跑去,每次都需要关门、开门,这显然是一种浪费。

综上所述,每次的读写操作都会进行两次拷贝和两次上下文切换,这就像是我们作为生产商,每次只能通过中间商将商品卖给消费者,效率低下。如果我们能找到一种方式,可以将商品从生产商直接卖给消费者,那将大大提高效率。在计算机世界中,这种方式就是零拷贝。通过零拷贝,我们可以减少数据拷贝和上下文切换,从而提高系统的性能。

注意这里说的零拷贝是一种思想,有时候并非完全消除了拷贝,可能只是去除了其中的某个拷贝过程。

零拷贝是如何实现的?

零拷贝的实现,就像是找到将商品从生产商直接卖给消费者的方法。在计算机世界中,这涉及到几种主要的技术:DMA、内核系统调用、写时复制、缓冲区共享等,它们都是减少了CPU直接参与拷贝数据的次数。

DMA

DMA的全称是直接内存访问,这其实是一种硬件技术,通过这种技术硬件可以直接访问内存,而不再需要通过CPU进行硬件和内存之间的数据拷贝。比如网卡收到数据后,直接将数据写入分配好的内核内存区域,写入达到一定量时再通知CPU来做下一步的处理。

很多硬件的系统都会使用DMA,包含硬盘控制器、绘图显卡、网卡和声卡等等。

内核系统调用

利用内核提供的接口实现的零拷贝技术。

  1. sendfile:这种方法的原理是用户进程调用内核程序发送文件数据,不将数据拷贝到用户态,从而减少两次上下文切换和一次CPU拷贝。但是,这种方法只适用于不需要用户态处理数据的程序。
  2. splice:这种方法的原理是用户进程调用内核程序发送文件数据,不将数据拷贝到用户态,从而减少两次上下文切换和一次CPU拷贝。并且,在写数据时,会在内核缓存和写缓存之间建立一个管道,从而减少一次CPU拷贝。但是,这种方法只适用于不需要用户态处理数据的程序,且用于传输数据的两者之间有一个必须是管道设备。

缓冲区共享

缓冲区共享是一种减少数据拷贝的技术,它的主要思想是让每个进程都有一个缓冲区,这个缓冲区能同时被映射到用户空间和内核空间。这样做的好处是,数据可以直接在用户空间和内核空间之间传递,无需通过CPU进行拷贝,从而提高了系统性能。

缓冲区共享的两个例子:

  1. 用户态直接IO:这种方式是让用户态程序直接对硬件设备进行IO操作,避免了数据在用户模式和内核模式之间的拷贝,可以看作是一种缓冲区共享的方式。这种方法的优点是可以减少一次数据拷贝,提高效率。然而,它也有一些限制。比如,它只适应于不需要内核处理数据的程序,如数据库系统,在大多数场景下,数据库系统对数据有非常精确的控制和需求,它们知道数据的布局,可以直接对硬件进行IO操作。此外,因为CPU和硬盘IO之间的速度相差很大,需要引入异步IO,以避免浪费CPU资源。
  2. mmap + write:这种方法的基本思路是这样的:首先,我们使用mmap系统调用将一个文件映射到内存中。这样,文件的内容就可以直接在内存中访问了,而不需要通过read系统调用来读取。然后,当我们需要将数据写入文件时,我们可以直接在映射的内存区域中写入数据,然后使用write系统调用将数据写入文件。这种方式的好处是,我们只需要一次CPU拷贝,就可以将数据从用户空间写入文件。然而,这种方式也有一些限制。首先,因为mmap需要对齐内存页,所以不适合小数据。如果我们需要写入的数据小于一个内存页的大小,就会造成内存浪费。其次,mmap可能会造成虚拟内存空间的碎片化,如果频繁地使用mmap和munmap,可能会导致性能下降。

写时复制

写时复制(Copy-On-Write,COW)是一种优化策略,它的主要思想是在多个进程共享内存的情况下,只有在进程需要对数据进行修改时,才会拷贝数据到自己的用户态内存。这样做的好处是,如果数据没有被修改,就不需要进行数据拷贝,从而节省了系统资源,提高了系统性能。

举个例子,在Redis进行持久化时,会用到写时复制技术。具体来说,它先会创建一个子进程来进行磁盘写入操作。这个子进程会共享父进程(也就是主Redis进程)的内存空间,而不会立即复制内存数据。

这时,如果主进程需要修改某个数据,它就会先将这个数据复制一份,然后在新的数据上进行修改,原有的数据仍然保留给子进程使用。这就是写时复制技术,它可以避免在创建子进程时复制整个内存空间,从而大大节省了内存资源。

使用写时复制技术后,也能保证数据的一致性。因为在持久化过程中,即使主进程对数据进行了修改,也不会影响到子进程正在写入磁盘的数据。这样,在高并发的环境下,就能保证数据的一致性和系统的稳定性。

这里之所以把写时复制也列入零拷贝,是考虑到使用这种技术时,我们确实避免了大量非必要的数据拷贝操作。

以上就是零拷贝的几种主要实现方式。虽然每种方式都有其特点和限制,但它们都有一个共同的目标,那就是减少数据拷贝和上下文切换,提高系统的性能。这就是零拷贝技术的魅力所在。

高级语言中的零拷贝实现

零拷贝技术在高级语言中有非常优雅的实现方式。我们可以通过这些语言提供的API,将零拷贝技术应用到我们的程序中,从而提高程序的性能。接下来,我将具体介绍如何在Java和DotNet中使用零拷贝技术。

Java NIO:Java NIO提供了FileChannel类,其中的transferTo和map方法可以用于实现零拷贝。

  • FileChannel.transferTo:这个方法实现了Linux中的sendfile功能,可以将数据直接从一个通道传输到另一个通道,避免了数据在用户空间和内核空间之间的多次复制。
  • FileChannel.map:这个方法实现了Linux中的mmap功能,可以将文件映射到内存中,使我们可以直接在内存中操作文件,而不需要进行额外的数据拷贝。

此外,Netty框架提供了FileRegion接口,它使用了FileChannel的transferTo方法。Netty还提供了一种机制,可以将多段数据虚拟为整段数据,或者将整段数据虚拟拆分为多段数据,从而避免数据的整合拷贝,我认为这也是一种零拷贝技术,只是没有上下文的切换。

以下是Java NIO使用FileChannel.transferTo的代码示例:

ini 复制代码
FileChannel srcChannel = new FileInputStream("src.txt").getChannel();
FileChannel destChannel = new FileOutputStream("dest.txt").getChannel();
srcChannel.transferTo(0, srcChannel.size(), destChannel);

DotNET:DotNet提供了Socket和Stream类,其中的SendFile和CopyToAsync方法可以用于实现零拷贝,类似于 Java NIO 中提供的相关能力。

  • Socket.SendFile:这个方法可以将数据直接从文件发送到Socket,避免了数据在用户空间和内核空间之间的多次复制。
  • Stream.CopyToAsync:这个方法可以异步地将数据从一个流复制到另一个流,避免了数据在用户空间和内核空间之间的多次复制。

以下是DotNet使用Socket.SendFile的代码示例:

arduino 复制代码
Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
s.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 11000));
s.SendFile("test.txt");

以上就是Java和DotNet中零拷贝实现的简单介绍。通过这些技术,我们可以在编程时有效地利用零拷贝技术,从而提高程序的性能。


零拷贝是一种高效的数据处理技术,它通过减少数据拷贝和上下文切换,提高了系统的性能。虽然零拷贝有一些限制,但它的优点仍然被广大的开发者所认可和使用,我们想提高应用程序的处理速度和吞吐量时,不妨考虑下零拷贝的技术思路。

关注微/信/公/众\号萤火架构,提升技术不迷路!

相关推荐
NiNg_1_23414 分钟前
SpringSecurity入门
后端·spring·springboot·springsecurity
Lucifer三思而后行1 小时前
YashanDB YAC 入门指南与技术详解
数据库·后端
王二端茶倒水2 小时前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
夜色呦3 小时前
现代电商解决方案:Spring Boot框架实践
数据库·spring boot·后端
爱敲代码的小冰3 小时前
spring boot 请求
java·spring boot·后端
java小吕布4 小时前
Java中的排序算法:探索与比较
java·后端·算法·排序算法
Goboy5 小时前
工欲善其事,必先利其器;小白入门Hadoop必备过程
后端·程序员
李少兄5 小时前
解决 Spring Boot 中 `Ambiguous mapping. Cannot map ‘xxxController‘ method` 错误
java·spring boot·后端
代码小鑫5 小时前
A031-基于SpringBoot的健身房管理系统设计与实现
java·开发语言·数据库·spring boot·后端
Json____5 小时前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库