计算机常识——零拷贝

news/2025/2/24 7:33:24/文章来源:https://www.cnblogs.com/aoximin/p/18345061

前言

什么是零拷贝技术?

首先计算机不存在什么真的零拷贝技术,这点是确认的。

零拷贝值得是减少多余的拷贝的意思。

正文

首先如果我们要传输文件是怎么处理的呢?

当需要从磁盘读取数据到内存时,‌CPU会发出指令通知硬盘控制器进行读取操作。‌
此后,‌CPU可以执行其他任务,‌而不需要持续参与数据的读取过程。‌这个过程利用了直接内存访问(‌DMA)‌技术,‌允许硬件设备(‌如硬盘)‌直接访问系统内存,‌从而实现了数据的快速传输。‌具体来说,‌DMA控制器负责管理内存和硬盘之间的数据传输,‌当数据传输完成时,‌DMA控制器会向CPU发出中断信号,‌通知数据已经准备好。‌CPU收到中断信号后,‌会将数据从内核空间拷贝到用户空间,‌完成整个数据读取过程。‌在这个过程中,‌CPU的大部分时间用于处理其他任务,‌而不是直接参与数据的物理传输。‌
此外,‌这种数据传输方式提高了系统的整体效率,‌因为CPU可以在等待数据传输完成的时间段内执行其他任务,‌而不是被绑定在数据传输上。‌这种技术是现代计算机系统中提高性能的一种重要手段
操作系统中的内核空间和用户空间是指操作系统中划分出来的两个不同的内存区域。内核空间是操作系统核心的运行区域,具有较高的权限,可以直接访问硬件资源和执行关键操作;用户空间是给应用程序执行的区域,权限较低,不能直接访问硬件,必须通过系统调用访问内核功能。这种分隔提高了系统的安全性和稳定性。
内核空间和用户空间是通过硬件和操作系统的协作来实现的。操作系统通过使用特殊的机制(如分页机制)将整个内存地址空间划分为内核空间和用户空间。举个例子,假设整个内存地址空间是0到4GB,操作系统可以将0到2GB的地址空间分配给内核空间,而将2GB到4GB的地址空间分配给用户空间。这样,内核空间和用户空间在地址空间上是相互独立的。当应用程序在用户空间执行时,如果需要访问硬件资源或执行特权指令,就需要通过系统调用切换到内核空间,让操作系统代表应用程序执行必要的操作,然后再返回用户空间。这种划分和切换机制有助于保护系统的稳定性和安全性。
DMA技术是Direct Memory Access的缩写,允许外部设备直接访问计算机内存数据,减轻了CPU的负担。具体实现原理是CPU发出DMA请求,外部设备将数据传输到内存,减少了CPU在数据传输过程中的干预,提高了数据传输效率。

也就是是通过dma(direct memory access)那么cpu只是发送一个指令,就能让其他的硬件进行工作了,这时候它就可以去做其他事情了。

那么通过dma读取的数据是归哪个进程管理呢?那肯定是归内核进程管理,也就是内存在内核态。

正常操作如上:

  1. 磁盘到内存缓冲区
  2. 内存缓冲区拷贝到用户缓冲区
  3. 用户缓冲区到socket缓存区
  4. socket缓冲区到网卡

这里面就经过4步骤。

数据拷贝次数:2 次 DMA 拷贝,2 次 CPU 拷贝
CPU 切换次数:4 次用户态和内核态的切换

正常情况是这么做的。

那么为什么要这么做呢?

首先拷贝到内核态,这个肯定是要的,先到内存然后再发出去,这个肯定无法避免的。

那么为什么要内核态的缓冲区拷贝到内核态呢? 这是因为用户态无法直接读取到内核态的内存,权限受限了。

那如果让用户态进程直接读取内核态内存呢?这个就很不安全了,因为也就意味着,一个进程可以读取另外一个进程的操作数据了,很危险。

那就没有一点办法吗?

在操作系统中,我们知道进程的通讯之一就是共享内存,这样就可以实现。

共享内存的实现也就是内存做映射。

这样就可以了,这样就保全了安全问题了,用户进程也不会瞎访问了。

然后就变成了这样了。

看起来相当nice。

这时候就是2 次 DMA 拷贝,1 次 CPU 拷贝。

CPU 切换次数:4 次用户态和内核态的切换

这里说下为什么是4次用户态到内核态。

首先在用户态发起读取数据的指令,这个时候是第一次,切换到了内核态。

当读取完毕后,然后切换到用户态,这是第二次。

用户态发起将内存写入到socket缓存区,这时候就是第三次,切换到来了内核态。

然后当内核态发送完毕,这个时候切换到用户态,这是第四次。

在C#中,你可以使用MemoryMappedFile类来实现类似于Unix/Linux中mmap(内存映射)的功能。

这个类允许你直接在内存中映射文件,从而实现对文件内容的高效访问。

你可以使用MemoryMappedFile.CreateFromFile方法来创建一个内存映射文件。记得在使用完成后释放资源以避免内存泄漏。

写入:

using System;
using System.IO.MemoryMappedFiles;class Program
{static void Main(){// 创建或打开一个内存映射文件using (var mmf = MemoryMappedFile.CreateOrOpen("TestMemoryMappedFile", 1024)){// 获取内存映射文件的一个视图var viewAccessor = mmf.CreateViewAccessor(0, 1024);// 使用 Marshal 写入字符串var str = "Hello, MemoryMappedFile!";var bytes = System.Text.Encoding.UTF8.GetBytes(str);viewAccessor.WriteArray(0, bytes, 0, bytes.Length);}}
}

读取:

using System;
using System.IO.MemoryMappedFiles;class Program
{static void Main(){// 打开一个已经存在的内存映射文件using (var mmf = MemoryMappedFile.OpenExisting("TestMemoryMappedFile")){// 获取内存映射文件的一个只读视图var viewAccessor = mmf.CreateViewAccessor(0, 1024);// 使用 Marshal 读取字符串byte[] bytes = new byte[1024];viewAccessor.ReadArray(0, bytes, 0, bytes.Length);var str = System.Text.Encoding.UTF8.GetString(bytes).Trim('\0');Console.WriteLine(str);}}
}

这个时候人们就会想啊,是不是不用cpu切换这么多次,直接由内存缓存区直接到socket缓冲区,不用在通知我内核态的进程了。

也就是说,用户态发出的指令是这样的,就硬盘的文件传给某个socket,而不是分为两个步骤了。

在 Linux 2.1 内核版本中,引入了一个系统调用方法:sendfile。

当调用 sendfile() 时,DMA 将磁盘数据复制到内核缓冲区 kernel buffer;然后将内核中的 kernel buffer 直接拷贝到 socket buffer(这里只拷贝数据的位置和长度);最后利用 DMA 将 socket buffer 通过网卡传输给客户端。

这个时候就是:

这时候就是2 次 DMA 拷贝,1 次 CPU 拷贝(极少),几乎可以不计。

CPU 切换次数: 2次用户态和内核态的切换.

图形还是这样,只是这次发送的指令比较长,需要内核进程直接做两步:

C# 中对 Linux 的 sendfile 进行支持可以使用 P/Invoke 来调用系统调用。下面是一个简单的示例代码,演示如何在 C# 中调用 Linux 的 sendfile 函数:
using System;
using System.IO;
using System.Runtime.InteropServices;public static class LinuxSendfile
{// 导入 Linux 的 sendfile 函数[DllImport("libc", SetLastError = true)]public static extern long sendfile(int out_fd, int in_fd, IntPtr offset, ulong count);public static void SendFile(int destination, int source, long offset, long count){IntPtr offPtr = IntPtr.Zero;if (offset > 0){offPtr = Marshal.AllocHGlobal(sizeof(long));Marshal.WriteInt64(offPtr, offset);}sendfile(destination, source, offPtr, (ulong) count);if (offPtr != IntPtr.Zero){Marshal.FreeHGlobal(offPtr);}}
}class Program
{static void Main(){int sourceFd = open("source.txt", O_RDONLY);int destFd = open("destination.txt", O_WRONLY);if (sourceFd == -1 || destFd == -1){return;}LinuxSendfile.SendFile(destFd, sourceFd, 0, 1024);close(sourceFd);close(destFd);}// Linux 系统调用需要用到的常量和函数const int O_RDONLY = 0;const int O_WRONLY = 1;[DllImport("libc", SetLastError = true)]public static extern int open(string fileName, int mode);[DllImport("libc", SetLastError = true)]public static extern int close(int fd);
}这个示例展示了如何在 C# 中使用 P/Invoke 调用 Linux 的 sendfile 函数,并实现文件的传输。请确保正确设置文件的读写权限和处理异常情况。

大概我们知道原理就行,到时候直接用库就行。

那么这个时候还是没有达到0拷贝这个效果啊,还是有一次cpu拷贝。

在 Linux 2.4 内核版本中,对 sendfile 系统方法做了优化升级,引入 SG-DMA 技术,需要 DMA 控制器支持。

其实就是对 DMA 拷贝加入了 scatter/gather 操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点来实现数据拷贝,可以多省去一次 CPU 拷贝。

整个拷贝过程,可以用如下流程图来描述!

SG-DMA 是 Scatter-Gather Direct Memory Access 的缩写,是一种用于数据传输的技术,允许将来自多个不连续内存区域的数据收集到一个连续内存区域中,或将一个连续内存区域的数据分散传送到多个不连续内存区域。这种技术常用于高性能计算和网络数据传输等场景中。

这样呢?就可以将磁盘中的数据放入到内存的不同位置,然后网卡dma可以直接读取成连续的,就是传输速度快了呗。

有人就问了,为啥不直接映射呢?没法映射啊,要知道映射其实是cpu的虚拟,这里映射完直接走dma,不走cpu,无法映射。

那还有没有进步空间呢?

在 Linux 2.6.17 内核版本中,引入了 splice 系统调用方法,和 sendfile 方法不同的是,splice 不需要硬件支持。

它将数据从磁盘读取到 OS 内核缓冲区后,内核缓冲区和 socket 缓冲区之间建立管道来传输数据,避免了两者之间的 CPU 拷贝操作。

一旦有个管道两者之间就可以通讯了。

Linux 系统 splice 拷贝流程,从上图可以得出如下结论:

数据拷贝次数:2 次 DMA 拷贝,0 次 CPU 拷贝
CPU 切换次数:2 次用户态和内核态的切换

建立管道的目的就是可以不停的读不停的写,比sendfile要快,因为sendfile是写完了后然后才拷贝给socket 存储区进行读,管道连续不断。

简单梳理一下概念。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/840643.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

编译 App 工程

Android Studio 跟 IDEA 一样,被改动的文件会自动保存,无须开发者手工保存。它还会自动编译最新的代码,如果代码有误,编辑界面会标红提示出错了。但是有时候可能因为异常关闭的缘故,造成 Android Studio 的编译文件发生损坏,此时需要开发者手动重新编译,手动编译有以下 …

华为交换机简单配置方法

ARP地址解析 二层交换机图示在二层交换机内主机第一次ping对方以后,即第一次发ARP广播,交换机记录双方的mac ip对应地址地址表,后续再交换数据,变成单播display mac-address 查看mac地址表 划分vlan(虚拟局域网)创建vlan 10 20 2个 先把pc1 pc2 pc3 连接的交换机上的端…

RHEL9.4上使用apache搭建http服务器提供repo源

时间:2024.11.24 计划:使用apache搭建HTTP(Hypertext Transfer Protocol)服务器,共享iso镜像为环境内其他主机提供repo(repository)源 参照:马哥教育王老师课程 基于Linux系统的本地Yum源搭建与配置(ISO方式、RPM方式) https://developer.aliyun.com/article/1356520 如何…

JVM常见面试题(四):垃圾回收

堆区域划分,对象什么时候可以被垃圾器回收,如何定位垃圾——引用计数法、可达性分析算法,JVM垃圾回收算法——标记清除算法、标记整理算法、复制算法、分代回收算法;JVM垃圾回收器——串行、并行、CMS垃圾回收器、G1垃圾回收器;强引用、软引用、弱引用、虚引用文章目录 前…

类和对象综合案例——模仿电影信息系统

1.需求2.实际操作 1.创建一个实体类 首先第一件事,就是写好一个实体类,为后面封装所有数据做准备,我们只需要私有化成员变量,然后再ptg即可2.创建处理业务类和测试类 写好了实体类就要开始处理业务了,所以我们要再创建一个专门处理业务的类,名为MovieService 写完了业务类…

CF2038A - Bonus Project 题解

题目传送门 https://codeforces.com/contest/2038/problem/A 先大致捋一下题目的含义 一共有n个工程师,每个工程师完成相应的工作都有一定的奖金a,但同时也会消耗成本b,目前一共有k个工作需要做 这些工程师对他们的同事很友好,他们能接受自己的总收益为0来增长经验,但不能…

BurpSuite安装captcha-killer插件

1.本机已经安装python3.8或者更高版本,使用命令进行更新pip和安装环境模块 python3 -m pip install --upgrade pip pip3 install ddddocr aiohttp -i https://pypi.tuna.tsinghua.edu.cn/simple/ (因为本机安装了python2和python3,所以输入pip3)2.bp导入插件包,低版本导…

Perf Linux性能事件(性能计数)器 与 Flame Graph

from ふぃーる 冬コミ2日目西ふ15Perf 性能采样和计数原理 首先要清楚perf是一个面向事件的可观察性工具from jyy perf在中断来临时,获取OS在中断之前所记录的关键性能指标Perf Stat (性能计数)stat (statistics) 有统计,计数,获取信息等含义perf stat <command&…

技术债正在悄悄拖垮你的团队!

0 前言 软件开发的核心在于应对变化。在软件的生命周期中,目标是能够在合理的时间内实施必要的更改。不管这些更改是技术性的,比如紧急安全升级,还是业务需求所驱动的,比如开发新功能以在目标市场中更具竞争力——能否快速应对变化是成败的关键。 是什么让我们慢下来?通常…

HCIA-06 IP路由基础

介绍了路由的基本概念,路由条目三种生成方式:直连路由、静态路由(缺省路由)、动态路由,路由器的基本工作原理、路由表的具体内容。路由器选择最佳路由的方法:先比较匹配的掩码长度、(如果掩码长度相同)再比较优先级、(如果优先级相同)再比较度量值。路由转发选择的路…

学习笔记(四十四):自定义组件@LocalBuilder装饰器

概述: 当开发者使用@Builder做引用数据传递时,会考虑组件的父子关系,使用了bind(this)之后,组件的父子关系和状态管理的父子关系并不一致。 为了解决组件的父子关系和状态管理的父子关系保持一致的问题,引入@LocalBuilder装饰器。 @LocalBuilder拥有和局部@Builder相同的功…