浅聊一下 C#程序的 内存映射文件 玩法

一下,c#,程序,内存,映射,文件,玩法 · 浏览次数 : 2804

小编点评

**1. 理解虚拟地址** 虚拟地址是计算机内存地址的偏移,它是指实际地址的偏移量。 **2. 自定义 Image** Image 是内存地址的映射,可以自定义内存地址以指向不同的物理地址。 **3. MemoryMappedFile 类** MemoryMappedFile 类可以封装 Windows API 的内存映射功能,提供更便捷的内存操作。 **4. 创建 MemoryMappedFile 对象** 使用 MemoryMappedFile.CreateFromFile() 创建一个内存映射文件对象,可以从文件路径创建内存映射文件。 **5. 使用 ViewAccessor 创建内存访问器** ViewAccessor 对象可以访问内存文件,并提供一些内存访问方法。 **6. 写入和读取内存** 使用 ViewAccessor.WriteArray() 将内存数据写入内存文件,使用 ViewAccessor.ReadArray() 读取内存文件中的数据。 **7. 示例代码** ```csharp using System.Memory; using System.MemoryMapped; internal class Program { static void Main(string[] args) { int capaticy = 1024; using (var mmf = MemoryMappedFile.CreateFromFile(@\"C:\\1.txt\", FileMode.OpenOrCreate, @"testmapfile", capaticy, MemoryMappedFileAccess.ReadWrite)) { var viewAccessor = mmf.CreateViewAccessor(0, capaticy); while (true) { Console.WriteLine("请输入你要写入的内容: "); string input = Console.ReadLine(); viewAccessor.WriteArray(0, input.ToArray(), 0, input.Length); } } } } ``` **8. 总结** *虚拟地址是计算机内存地址的偏移量。 *自定义 Image可以映射内存地址以指向不同的物理地址。 *MemoryMappedFile 类可以提供更便捷的内存操作。 *创建 MemoryMappedFile 对象从文件路径创建内存映射文件。 *使用 ViewAccessor 创建内存访问器。 *写入和读取内存可以使用 ViewAccessor 进行操作。

正文

一:背景

1. 讲故事

前段时间训练营里有朋友问 内存映射文件 是怎么玩的?说实话这东西理论我相信很多朋友都知道,就是将文件映射到进程的虚拟地址,说起来很容易,那如何让大家眼见为实呢?可能会难倒很多人,所以这篇我以自己的认知尝试让大家眼见为实。

二:如何眼见为实

1. 我想象的文件映射

在任何讨论之前,内存文件映射大概像下面这样,多个进程可以完全View一个文件,也可以 View 文件的一部分到进程的虚拟地址中,画个图大概像下面这样。

但仔细一想,这里还有很多的小细节,比如:

疑问1:到底是映射文件还是映射磁盘的物理地址 ?

疑问2:既然是后备存储,那是不是每次修改虚拟地址都要刷硬盘 ?

疑问3:内存页是4k为一个单位,文件大小不是 4k 整数倍怎么办 ?

这三个疑问我相信很多朋友或多或少都会遇到,这里我简单解答一下,后面再用 windbg 验证。

  1. 严格来说是 硬盘物理地址

  2. 文件所处的硬盘地址为后备存储这个不假,但这里有个小细节,对虚拟地址的读写涉及到 内存页 概念,如果访问的虚拟地址所在的物理地址不在 物理内存 中,就会引发缺页中断,操作系统会将 磁盘上的 4k 页粒度灌入到 物理内存 中,同样的道理,如果修改了虚拟地址,那么物理内存页就是脏数据,会在后续的某个时刻刷新到 硬盘 上,产生磁盘 IO。

总的来说:从磁盘到物理内存(内存条) 之间的内存页的换入换出都是一种按需的 懒加载懒写入行为,稍后我们用 windbg 验证下。

  1. 内存的管理采用的是内存页的方式,如果 View 大于 文件Size,那么文件会扩容到 4k 对齐,这样方便对文件追加写入。

综合上面的三点信息,图就可以画的再详细一点了,比如下面这样:

熟悉内存管理的朋友应该知道,我们程序的 exe 和 dll 就是用 内存映射文件 的方式加载到虚拟地址中的,所以就拿它开刀吧。

2. 一段测试代码

为了方便演示,上一段简单的的测试代码,观察 ConsoleApp1.exe 的映射方式。


        static void Main(string[] args)
        {
            Console.WriteLine($"当前时间:{DateTime.Now}, 程序启动!");
            Console.ReadLine();
        }

接下来用 windbg 启动 ConsoleApp1.exe 两次,结合详细分解图,我们观察下这两个进程的虚拟地址所映射的内存条物理地址是否一致?

  1. 实例1

ModLoad: 00007ff6`bfe00000 00007ff6`bfe2a000   apphost.exe
ModLoad: 00007ff9`b1450000 00007ff9`b1648000   ntdll.dll
...

0:008> lmvm apphost
Browse full module list
start             end                 module name
00007ff6`bfe00000 00007ff6`bfe2a000   apphost  C (private pdb symbols)  c:\mysymbols\apphost.pdb\1643A9EB126F4FE184548E9CC1B740B71\apphost.pdb
    Loaded symbol image file: D:\net7\ConsoleApplication1\ConsoleApp1\bin\Debug\net6.0\ConsoleApp1.exe
    Image path: apphost.exe
    Image name: apphost.exe
    ...

0:008> ~
   0  Id: 232c.4abc Suspend: 1 Teb: 0000000e`7b1a5000 Unfrozen

  1. 实例2

ModLoad: 00007ff6`bfe00000 00007ff6`bfe2a000   apphost.exe
ModLoad: 00007ff9`b1450000 00007ff9`b1648000   ntdll.dll
...

0:008> ~
   0  Id: 60e8.3e3c Suspend: 1 Teb: 000000da`ab498000 Unfrozen
   1  Id: 60e8.53b0 Suspend: 1 Teb: 000000da`ab49a000 Unfrozen

这里要提醒一下的是在 Windows 平台上 ConsoleApp1.exe 已经成了一个引导程序,通过 lmvm 可以看到它其实是 apphost.exe

两个实例都开起来后,可以看到 apphost.exe 在各自进程的虚拟地址都一样,那他们的物理地址是否也一样呢? 要寻找答案,接下来我们到 Windows 内核态去挖一挖。


lkd> !process 0 0 ConsoleApp1.exe

PROCESS ffff838bd84c9080
    SessionId: 8  Cid: 232c    Peb: e7b1a4000  ParentCid: 0b14
FreezeCount 2
    DirBase: 3468cf000  ObjectTable: ffff938feae02900  HandleCount: 172.
    Image: ConsoleApp1.exe

PROCESS ffff838bef157080
    SessionId: 8  Cid: 60e8    Peb: daab497000  ParentCid: 4804
FreezeCount 2
    DirBase: 3552f3000  ObjectTable: ffff938fe8f7ec40  HandleCount: 166.
    Image: ConsoleApp1.exe

从卦中看,Cid: 232c 是我们的实例1, Cid: 60e8 是我们的实例2,接下来用 windbg 提供的 !vtop 命令观察 apphost.exe 的首地址对应的物理地址。


// ----  实例1 -----
lkd> !vtop 3468cf000 00007ff6bfe00000
Amd64VtoP: Virt 00007ff6bfe00000, pagedir 00000003468cf000
Amd64VtoP: PML4E 00000003468cf7f8
Amd64VtoP: PDPE 00000001138dbed0
Amd64VtoP: PDE 00000002153dcff8
Amd64VtoP: PTE 000000024dadd000
Amd64VtoP: Mapped phys 00000002271c2000
Virtual address 7ff6bfe00000 translates to physical address 2271c2000.

//----  实例2 -----

lkd> !vtop 3552f3000 00007ff6bfe00000
Amd64VtoP: Virt 00007ff6bfe00000, pagedir 00000003552f3000
Amd64VtoP: PML4E 00000003552f37f8
Amd64VtoP: PDPE 00000002db7ffed0
Amd64VtoP: PDE 0000000208100ff8
Amd64VtoP: PTE 000000033de01000
Amd64VtoP: Mapped phys 00000002271c2000
Virtual address 7ff6bfe00000 translates to physical address 2271c2000.

从卦中看,实例1 和 实例2 的 虚拟地址 映射的 物理地址 是相同的 2271c2000。这也很好的解释了那张图。

有朋友可能会有疑问,能否看下 2271c2000 这个 物理地址 的内容? 这当然是可以的,用 windbg 的 !da 就好了。


lkd> !db 2271c2000
#2271c2000 4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00 MZ..............
#2271c2010 b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00 ........@.......
#2271c2020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#2271c2030 00 00 00 00 00 00 00 00-00 00 00 00 e8 00 00 00 ................
#2271c2040 0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68 ........!..L.!Th
#2271c2050 69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f is program canno
#2271c2060 74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20 t be run in DOS 
#2271c2070 6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00 mode....$.......

从卦中看,物理地址上有一段 This program cannot be run in DOS mode,这不就是经典的 PE 文件哈,如果不相信可以用 WinHex 打开 ConsoleApp1.exe 即可,截图如下:

最后就是内核中的 内存管理器 会将 物理地址 与 磁盘地址 进行打通,实现懒加载和懒写入。

3. 如何自定义实现

Image 虽然是一个快捷的观察内存文件映射方式,那如果自己能实现一个就更有意思了,比如下面对 1.txt 进行文件映射,在 C# 中有一个快捷类 MemoryMappedFile 实现了 win32api 的封装,参考代码如下:


    internal class Program
    {
        static void Main(string[] args)
        {
            int capaticy = 1024; //1k

            using (var mmf = MemoryMappedFile.CreateFromFile(@"C:\1.txt", FileMode.OpenOrCreate,
                                                            "testmapfile",
                                                             capaticy,
                                                             MemoryMappedFileAccess.ReadWrite))
            {
                var viewAccessor = mmf.CreateViewAccessor(0, capaticy);

                while (true)
                {
                    Console.WriteLine("请输入你要写入的内容: ");

                    string input = Console.ReadLine();

                    viewAccessor.WriteArray(0, input.ToArray(), 0, input.Length);
                }
            }
        }
    }

接下来用 windbg 附加一下,观察 1.txt 是不是被 MappedFile 上了,同时做的修改有没有更新到物理磁盘上。


0:006> !address

  BaseAddr EndAddr+1 RgnSize     Type       State                 Protect             Usage
-----------------------------------------------------------------------------------------------
...
+  31a0000  31a1000     1000 MEM_MAPPED  MEM_COMMIT  PAGE_READWRITE                     MappedFile "\Device\HarddiskVolume3\1.txt"
...

0:006> du 31a0000
031a0000  "helloworld!"

从卦中可以看到,虽然 1.txt 最大的 View 区间是 1k,但提交的内存页还是按照最小粒度 4k 给的。

三:总结

这篇我们就简单的浅聊一下,如果这块是知识盲区的朋友应该会有一点帮助,希望没有带偏大家,更多的细节期待大家挖掘!

图片名称

与浅聊一下 C#程序的 内存映射文件 玩法相似的内容:

浅聊一下 C#程序的 内存映射文件 玩法

## 一:背景 ### 1. 讲故事 前段时间训练营里有朋友问 `内存映射文件` 是怎么玩的?说实话这东西理论我相信很多朋友都知道,就是将文件映射到进程的虚拟地址,说起来很容易,那如何让大家眼见为实呢?可能会难倒很多人,所以这篇我以自己的认知尝试让大家眼见为实。 ## 二:如何眼见为实 ### 1.

【JavaScript】聊一聊js中的浅拷贝与深拷贝与手写实现

什么是深拷贝与浅拷贝?深拷贝与浅拷贝是js中处理对象或数据复制操作的两种方式。‌在聊深浅拷贝之前咱得了解一下js中的两种数据类型:

痞子衡嵌入式:浅聊恩智浦i.MXRT官方SDK里关于串行Flash相关的驱动与例程资源(上篇)

大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家介绍的是恩智浦i.MXRT官方SDK里关于串行Flash相关的驱动与例程资源。 经常有同事以及 i.MXRT 客户咨询痞子衡,咱们恩智浦官方 SDK 里有哪些串行 Flash 相关的示例,每一次痞子衡都是按照询问需求将 SDK 里相应资源路径发

浅拷贝、深拷贝与序列化【初级Java必需理解的概念】

浅拷贝 首先创建两个类,方便理解浅拷贝 @Data class Student implements Cloneable{ //年龄和名字是基本属性 private int age; private String name; //书包是引用属性 private Bag bag; public Stu

浅入深出的微前端MicroApp

本文是由最近做的一个项目有感而发,因为之前做了一些技术栈的统一,为了用ant Design的pro-table,PC统一使用react,但是我们有一些老的项目是vue的,本次新页面较多,老页面的改动较少,除此之外老项目想换菜单,因此我们想借助本次机会用react开发,经过了几番思考,发现本次很适合用...

【知识点】浅入线段树与区间最值问题

线段树的数据结构、基本原理、构建方法、区间查询和更新操作,以及其在解决区间最值问题和进行优化(如懒标记)中的应用和代码实现。

Java的深浅拷贝认识

目录浅拷贝深拷贝分辨代码里的深浅拷贝 在Java中,深拷贝和浅拷贝是对象复制的两种方式,主要区别在于对对象内部的引用类型的处理上。 浅拷贝 定义: 浅拷贝是指创建一个新的对象,但这个新对象的属性(包括引用类型的属性)仍然指向原来对象的属性。换言之,如果原对象中的属性是一个引用类型,那么浅拷贝只会复制

什么是浅拷贝和深拷贝,如何用 js 代码实现?

〇、简介和对比 简介 浅拷贝:只复制原始对象的第一层属性值。 如果属性值是值类型,将直接复制值,本值和副本变更互不影响; 如果是引用数据类型,则复制内存地址,因此原始对象和新对象的属性指向相同的内存地址,改变任一值,另一变量值也会同步变更。 深拷贝:递归地复制原始对象的所有层级。 每一个属性值都会在

限流器设计思路(浅入门)

目录令牌桶算法(Token Bucket)漏桶算法(Leaky Bucket)滑动窗口(Sliding Window)总结 限流器(Rate Limiter)是一种用于控制系统资源利用率和质量的重要机制。它通过限制单位时间内可以执行的操作数量,从而防止系统过载和保护服务的可靠性。在Java中,可以使

C#中的浅拷贝与深拷贝

## 前言 众所周知,C#中有两种类型变量:那就是**值类型**和**引用类型**。对于值类型而言,copy就相当于是全盘复制了,真正的实现了复制,属于**深拷贝**;而对于引用类型而言,一般的copy只是**浅拷贝**,只是copy到了引用对象的地址,相当于值传递了一个引用指针,==新的对象通过地