当前位置: 首页 > news >正文

零拷贝原理面试回答

零拷贝原理

参考回答

传统文件传输的方式、需要涉及 2 次系统调用、write 和 read 函数、期间会发生 4 次用户态与内核态上下文切换和数据拷贝次数。

而零拷贝技术的文件传输方式只需要 1 次系统调用、就是 sendfile、这样相比传统文件传输的方式、减少了 2 次用户态与内核态上下文切换次数、再加上网卡支持 SG-DMA 技术的话、数据拷贝次数只需要 2 次数据拷贝次数、就可以完成文件的传输、而且 2 次的数据拷贝过程、都不需要通过 CPU、2 次都是由 DMA 来搬运。

所以零拷贝技术主要是为了提升文件传输的效率、Kafka 消息队列 I/O 的吞吐量高的原因,也是因为使用了零拷贝技术。

2.为什么要有 DMA 技术

在没有 DMA 技术前、I/O 的过程是这样的:

  • CPU 发出对应的指令给磁盘控制器、然后返回

  • 磁盘控制器收到指令后、于是就开始准备数据、会把数据放入到磁盘控制器的内部缓冲区中、然后产生一个中断;

  • CPU 收到中断信号后、停下手头的工作、接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器、然后再把寄存器里的数据写入到内存、而在数据传输的期间 CPU 是无法执行其他任务的。

  • CPU 亲自搬运数据是瓶颈: 在没有 DMA 的情况下、CPU 必须亲自参与数据的搬运过程、这严重限制了 CPU 的利用率。

  • 小数据量尚可: 对于少量数据的传输、CPU 搬运可能还能应付。

  • 大数据量不堪重负: 当使用千兆网卡或硬盘传输大量数据时、CPU 会被 I/O 操作完全占用、导致系统性能急剧下降、甚至崩溃。CPU 根本无法同时处理 I/O 和其他任务。

于是有了DMA技术

也就是进行I/O设备和内存的数据传输的时候、数据搬运的工作全部交给DMA控制器、而CPU不再参与任何与数据搬运相关的事情、这样CPU就可以去处理别的事务。

3.传统的文件传输上传流程

传统的服务端提供文件传输功能:

将磁盘上的文件读取出来、然后通过网络协议发送给客户端。

传统I/O的工作方式是、数据读取和写入是从用户空间到内核空间来回复制、而内核空间的数据是通过操作系统层面的I/O 接口从磁盘读取或写入。代码通常如下一般会需要两个系统调用:

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

应用程序(用户态)发起了文件上传的请求。

因为实现文件上传需要内核态 所以需要在用户态切成内核态

实现操作系统的核心功能、如管理进程、文件系统、内存管理等、这些功能需要在内核态下执行。

  • 用户态发起文件上传请求: 用户态的应用程序(例如浏览器、文件管理器)通过调用相关的API(例如 open()、read()write()、close() 等)来发起文件上传请求。这些API调用本质上是系统调用(system call)的请求。

  • 需要切换到内核态: 用户态的程序不能直接访问硬件资源(例如磁盘、网络接口)。文件上传涉及到读写磁盘(读取要上传的文件)和通过网络发送数据,这些操作都需要内核的参与。因此必须通过系统调用切换到内核态。

  • 系统调用: 系统调用是用户态程序请求内核服务的接口。当用户态程序发起系统调用时、会触发一个中断(通常是软件中断)、CPU会切换到内核态、执行相应的内核代码来处理请求。

传统文件上传流程(没有零拷贝):

1.用户态发起文件上传请求: 用户态应用程序(如浏览器)调用API

(如open(), read(),write()、close())发起上传请求。 这些API调用最终会转化为系统调用

1.系统调用和上下文切换: 文件上传需要读磁盘和通过网络发送数据,这些操作都需要内核参与。 因此需要通过系统调用切换到内核态。 典型的流程涉及至少两个系统调用:

read(): 从文件中读取数据。

write(): 将数据写入网络套接字。 每次系统调用都会导致用户态和内核态之间的上下文切换。

2.数据拷贝(4次): 这是性能瓶颈的关键所在。首先用户选择本地磁盘上的文件进行上传

1.第一次拷贝 (DMA): 磁盘 -> 内核读缓冲区。 内核使用DMA(Direct Memory Access、直接内存访问)将数据从磁盘直接拷贝到内核空间的读缓冲区。 CPU只需要发起DMA传输、然后就可以处理其他任务、DMA控制器负责完成数据拷贝。这里有一个 read()系统调用

2.第二次拷贝 (CPU): 内核读缓冲区 -> 用户缓冲区。 内核将数据从内核读缓冲区拷贝到用户应用程序提供的用户缓冲区。 这个拷贝通常由CPU完成、因为内核需要控制数据的访问和权限。

3.第三次拷贝 (CPU): 用户缓冲区 -> 内核 socket 缓冲区。 用户应用程序调用write()系统调用、将用户缓冲区的数据拷贝到内核空间的socket缓冲区。 这个拷贝也由CPU完成。

4.第四次拷贝 (DMA): 内核 socket 缓冲区 -> 网卡缓冲区。 内核将socket缓冲区的数据拷贝到网卡(网络接口卡)的缓冲区、以便通过网络发送。 这个拷贝通常由DMA完成、网卡DMA控制器直接从内核socket缓冲区读取数据。

这里有2次系统调用 4次上下文切换 4次数据拷贝

  • 系统调用: 2次、分别是 read()write()

  • 上下文切换: 4次。 每次系统调用都会导致用户态和内核态之间的切换。 read()write() 各需要两次切换:

    • 用户态 -> 内核态 (进入 read()write() 系统调用)

    • 内核态 -> 用户态 (从 read()write() 系统调用返回)

  • 数据拷贝: 4次、你的描述完全正确。

    • DMA: 磁盘 -> 内核读缓冲区

    • CPU: 内核读缓冲区 -> 用户缓冲区

    • CPU: 用户缓冲区 -> 内核 socket 缓冲区

    • DMA: 内核 socket 缓冲区 -> 网卡缓冲区

我们回过头看这个文件传输的过程、我们只是搬运一份数据、结果却搬运了4次、过多的数据拷贝无疑会消耗CPU资源、大大降低了系统性能。

这种简单又传统的文件传输方式、存在冗余的上下文切换和数据拷贝、在高并发系统里是非常糟糕的、多了很多不必要的开销、会严重影响系统性能。

所以要想提高文件传输的性能、就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。

4.如何优化文件传输的性能

如何减少「用户态与内核态的上下文切换」的次数呢

读取磁盘数据的时候、之所以要发生上下文切换、这是因为用户空间没有权限操作磁盘或网卡、内核的权限最高

这些操作设备的过程都需要交由操作系统内核来完成、所以一般要通过内核去完成某些任务的时候、就需要使用操作系统提供的系统调用函数。

而一次系统调用必然会发生2次上下文切换:首先从用户态切换到内核态、当内核执行完任务后、再切换回用户态交由进程代码执行。

所以要想减少上下文切换到次数、就要减少系统调用的次数。

那如何减少「数据拷贝」的次数

在前面我们知道了、传统的文件传输方式会历经4次数据拷贝、而且这里面

「从内核的读缓冲区拷贝到用户的缓冲区里、再从用户的缓冲区里拷贝到socket的缓冲区里」

这个过程是没有必要的。

因为文件传输的应用场景中、在用户空间我们并不会对数据「再加工」、所以数据实际上可以不用搬运到用户空间、因此用户的缓冲区是没有必要存在的。

如何实现零拷贝

零拷贝技术实现的方式通常有2种:

mmap + write sendfile
  • sendfile() (Linux 2.1+): sendfile() 系统调用允许内核直接将数据从文件描述符(例如磁盘文件)传输到另一个文件描述符(例如socket)。 它避免了将数据复制到用户空间。

  • mmap() + write(): mmap() 将文件映射到内存

  • 允许内核和用户空间共享同一块内存区域。 然后write() 系统调用可以将数据从映射的内存区域传输到 socket

1.mmap + write

在前面我们知道 read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里

于是为了减少这一步开销、我们可以用 mmap() 替换 read() 系统调用函数。

buf = mmap(file, len) write(sockfd, buf, len)

mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间

这样操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。

  • 第一次拷贝 (DMA): 磁盘 -> 内核读缓冲区。

  • 第二次拷贝 (CPU): 内核读缓冲区 -> 用户缓冲区。

  • 第三次拷贝 (CPU): 用户缓冲区 -> 内核 socket 缓冲区。

  • 第四次拷贝 (DMA): 内核 socket 缓冲区 -> 网卡缓冲区。

我们知道 read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里

于是为了减少这一步开销、我们可以用 mmap() 替换 read() 系统调用函数。

具体过程如下:

  • 应用进程调用了 mmap() 后、DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着应用进程跟操作系统内核「共享」这个缓冲区

  • 应用进程再调用 write() 操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中、这一切都发生在内核态、由 CPU 来搬运数据;

  • 最后把内核的 socket 缓冲区里的数据、拷贝到网卡的缓冲区里、这个过程是由 DMA 搬运的。

所以我们可以得知 通过使用 mmap() 来代替 read()可以减少一次数据拷贝的过程。

mmap() 减少一次数据拷贝、它减少的是 内核读缓冲区 -> 用户缓冲区 的拷贝。

但这还不是最理想的零拷贝、因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里

mmap()write() 会导致 4 次上下文切换:

  1. mmap() 系统调用:

    1. 用户态 -> 内核态: 用户程序调用 mmap() 函数、触发一个中断、CPU从用户态切换到内核态、开始执行内核中处理 mmap() 调用的代码。

    2. 内核态 -> 用户态: 内核完成 mmap() 的处理(例如建立映射关系)、然后通过一个特殊的指令(例如 iret)将CPU从内核态切换回用户态、并将控制权返回给用户程序。

  2. write() 系统调用:

    1. 用户态 -> 内核态: 用户程序调用 write() 函数、触发一个中断、CPU从用户态切换到内核态、开始执行内核中处理 write() 调用的代码。

    2. 内核态 -> 用户态: 内核完成 write() 的处理(例如将数据写入 socket 缓冲区)、然后通过一个特殊的指令将CPU从内核态切换回用户态,并将控制权返回给用户程序。

  • mmap() 并没有消除所有的数据拷贝 而是通过共享内存和延迟加载 减少了传统方式下的数据拷贝次数。

  • 使用 mmap() 仍然需要两次系统调用 (mmap() 和 write()) 因此仍然需要 4 次上下文切换。

仍然系统调用还是 2 次。 需要 4 次上下文切换

所以就有了以下的技术:

2.sendfile

在 Linux 内核版本 2.1 中、提供了一个专门发送文件的系统调用函数 sendfile()、函数形式如下:

它的前两个参数分别是目的端和源端的文件描述符、后面两个参数是源端的偏移量和复制数据的长度、返回值是实际复制数据的长度。

首先它可以替代前面的 read() 和 write() 这两个系统调用、这样就可以减少一次系统调用、也就减少了 2 次上下文切换的开销。

其次该系统调用、可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里、不再拷贝到用户态、这样就只有 2 次上下文切换、和 3 次数据拷贝。如下图:

但是这还不是真正的零拷贝技术、如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术

(和普通的 DMA 有所不同)、我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。

你可以在你的 Linux 系统通过下面这个命令、查看网卡是否支持 scatter-gather 特性:

于是从 Linux 内核 2.4 版本开始起、对于支持网卡支持 SG-DMA 技术的情况、系统调用的过程发生了点变化,具体过程如下:

  • 第一步、通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;

  • 第二步、缓冲区描述符和数据长度传到 socket 缓冲区、这样网卡的 SG-DMA 控制器就可以直接将内核缓冲区中的数据拷贝到网卡的缓冲区里

  • 此过程不需要将数据从操作系统内核缓冲区拷贝socket 缓冲区中、这样就减少了一次数据拷贝;

所以这个过程之中、只进行了 2 次数据拷贝:如下图:

这就是所谓的零拷贝(Zero-copy)技术

因为我们没有在内存层面去拷贝数据、也就是说全程没有通过 CPU 来搬运数据、所有的数据都是通过 DMA 来进行传输的。

零拷贝技术的文件传输方式相比传统文件传输的方式、减少了 2 次上下文切换和数据拷贝次数

只需要 2 次上下文切换和数据拷贝次数、就可以完成文件的传输、而且 2 次的数据拷贝过程、都不需要通过 CP、2 次都是由 DMA 来搬运。

所以总体来看、零拷贝技术可以把文件传输的性能提高至少一倍以上。

使用 sendfile() 和 SG-DMA 的零拷贝技术:

  • 系统调用次数: 1 (sendfile())

  • 上下文切换次数: 2 (进入和退出 sendfile() 系统调用)

  • 数据拷贝次数: 2 (DMA 拷贝:磁盘 -> 内核缓冲区 内核缓冲区 -> 网卡缓冲区)

1次系统调用 2次上下文切换 2次数据拷贝

总结

通过 sendfile() 和 SG-DMA、零拷贝技术实现了以下优化:

  • 减少系统调用: 从 2 次 (read()、rite()) 减少到 1 次 (sendfile())

  • 减少上下文切换: 从 4 次减少到 2 次。

  • 减少数据拷贝: 从 4 次减少到 2 次、并且这两次拷贝都由 DMA 完成、无需 CPU 参与。

使用零拷贝技术的项目 Kafka

事实上Kafka 这个开源项目、就利用了「零拷贝」技术、从而大幅提升了 I/O 的吞吐率

这也是 Kafka 在处理海量数据为什么这么快的原因之一。

追溯 Kafka 文件传输的代码、最终它调用了 Java NIO 库里的 transferTo 方法:

Kafka的零拷贝技术具体是怎么实现的

面试回答零拷贝原理:

在实际应用中、如果我们需要把磁盘中的某个文件内容发送到远程服务器上

那么它必须要经过几个拷贝的过程:

  1. 从磁盘中读取目标文件内容拷贝到内核缓冲区

  2. CPU控制器再把内核缓冲区的数据赋值到用户空间的缓冲区中

  3. 接着在应用程序中、调用write()方法、把用户空间缓冲区中的数据拷贝到内核下的Socket Buffer中。

  4. 最后、把在内核模式下的SocketBuffer中的数据赋值到网卡缓冲区(NIC Buffer)

  5. 网卡缓冲区再把数据传输到目标服务器上。

在这个过程中我们可以发现、数据从磁盘到最终发送出去、要经历4次拷贝、而在这四次拷贝过程中、有两次拷贝是浪费的、分别是:

  1. 从内核空间赋值到用户空间

  2. 从用户空间再次复制到内核空间

除此之外由于用户空间和内核空间的切换会带来CPU的上线文切换、对于CPU性能也会造成性能影响。

而零拷贝就是把这两次多于的拷贝省略掉、应用程序可以直接把磁盘中的数据从内核中直接传输给Socket、而不需要再经过应用程序所在的用户空间

零拷贝通过DMA(Direct Memory Access)技术把文件内容复制到内核空间中的Read Buffer。

接着把包含数据位置和长度信息的文件描述符加载到Socket Buffer中、DMA引擎直接可以把数据从内核空间中传递给网卡设备。

在这个流程中、数据只经历了两次拷贝就发送到了网卡中、并且减少了2次cpu的上下文切换、对于效率有非常大的提高。

所以所谓零拷贝、并不是完全没有数据赋值、只是相对于用户空间来说、不再需要进行数据拷贝。

对于前面说的整个流程来说、零拷贝只是减少了不必要的拷贝次数而已。

在程序中如何实现零拷贝呢

  • 在Linux中、零拷贝技术依赖于底层的sendfile()方法实现

  • 在Java中、FileChannal.transferTo() 方法的底层实现就是 sendfile() 方法。

除此之外还有一个 mmap 的文件映射机制

它的原理是:将磁盘文件映射到内存,、用户通过修改内存就能修改磁盘文件。使用这种方式可以获取很大的I/O提升、省去了用户空间到内核空间复制的开销。

总而言之零拷贝技术通过减少数据拷贝和上下文切换、显著提高了 I/O 性能

以上就是我对于零拷贝原理的理解

相关文章:

  • 【字符设备驱动开发–IMX6ULL】(二)Linux 设备号
  • 多模态交互下的车载机械臂体感控制系统设计与实现研究
  • 计算机网络基础:软件定义网络(SDN)深度解析
  • 使用 fetch 实现流式传输:核心原理与实践
  • 启幕数据结构算法雅航新章,穿梭C++梦幻领域的探索之旅——堆的应用之堆排、Top-K问题
  • 3.26 代码随想录第二十七天打卡
  • 参考文献格式对齐1-100
  • idea 快捷键
  • Harbor自建证书实现Https访问
  • LLVM学习-DragonEgg工具
  • 强化学习和智能决策:Q-Learning和Deep Q-Learning算法
  • 漫画|基于SprinBoot+vue的漫画网站(源码+数据库+文档)
  • 【0基础跟AI学软考高项】质量管理
  • PVE 安装黑苹果 MacOS
  • mac m3 pro 部署 stable diffusion webui
  • cJSON- API 深度解析:设计理念与实现原理(二)
  • 本地靶场的“作弊模式”?从单用户模式解锁网络与权限的秘密
  • 用Deepseek写扫雷uniapp小游戏
  • 【GPUStack】【dify】【RAGflow】:本地部署GPUStack并集成到dify和RAGflow
  • (基本常识)左值引用、右值引用、万能引用、移动语义和完美转发——原理和代码示例
  • 做一个网站以及app多少钱/百度指数是搜索量吗
  • 网页设计企业网站设计的功能/怎样做电商 入手
  • 用上海注册的公司建的网站/广告公司推广方案
  • 湘潭网站优化公司/合肥网站推广电话
  • vps搭建vpn无法访问国内网站/十大广告公司
  • 建站小二/谷歌seo博客