内存页


一、什么是“内存页”?

1. 内存页的定义

  • 内存页(Page)是操作系统分配内存的最小单位,典型大小为 4KB(即 4096 字节)。
  • 虚拟内存(程序看到的地址空间)和物理内存之间的映射就是以页为单位建立的。

你可以把物理内存理解成一张纸被裁成很多“页”,操作系统通过“页表”来记录这些页怎么分配。

2. 为什么要用“内存页”?

  • 性能优化:如果数据能按页对齐分布,内存访问更高效。
  • 内存映射:可以将文件直接映射到内存页上,进行快速读写。
  • 跨进程共享内存:两个程序可以共享同一块内存页进行通信。

二、在 C# 中如何使用“内存页”?

我们重点掌握两种方式:


✅ 方式一:使用 MemoryMappedFile 映射一块内存页

这个方法最实用,适合用于大文件访问多进程共享内存等场景。

👇 示例:创建一块 4KB 的共享内存

using System;
using System.IO.MemoryMappedFiles;

class Program
{
    static void Main()
    {
        // 创建或打开一个内存映射文件,大小为一页(4096 字节)
        using var mmf = MemoryMappedFile.CreateOrOpen("MySharedMemory", 4096);

        // 创建视图(可读写)
        using var accessor = mmf.CreateViewAccessor();

        // 向共享内存写入整数
        accessor.Write(0, 123456);

        // 读取数据
        int value = accessor.ReadInt32(0);
        Console.WriteLine("读取的值: " + value);  // 输出:123456
    }
}

🎯 应用场景

  • 大文件映射进内存,避免整文件加载
  • 与其他进程共享内存(通过相同的 "MySharedMemory" 名字)
  • 替代传统 TCP/管道 方式通信

✅ 方式二:使用 Marshal.AllocHGlobal 申请页大小的非托管内存

这适合用于与 C/C++ 库交互、需要精确控制内存时。

👇 示例:分配并访问一页大小的内存

using System;
using System.Runtime.InteropServices;

class Program
{
    static void Main()
    {
        const int pageSize = 4096;
        IntPtr ptr = Marshal.AllocHGlobal(pageSize);

        try
        {
            // 写入一个整数
            Marshal.WriteInt32(ptr, 0, 1000);

            // 读取
            int val = Marshal.ReadInt32(ptr, 0);
            Console.WriteLine("读取的值:" + val);
        }
        finally
        {
            // 释放内存,防止内存泄漏
            Marshal.FreeHGlobal(ptr);
        }
    }
}

🎯 应用场景

  • 与底层 C++ 库共享内存
  • 构建高性能缓存池
  • 页对齐内存控制、SIMD 加速、数据包解析等

三、使用时需要注意什么?

注意点 原因
分配的大小应为页大小(4096 的整数倍) 避免内存浪费,提高效率
使用完必须释放 MemoryMappedFileAllocHGlobal 都需要手动释放
不要让托管代码直接持有非托管指针 否则容易出现悬挂指针或内存泄漏
非托管内存操作需加 unsafeMarshal 操作 以避免运行时错误
多进程访问要用相同名字映射文件 否则不能共享同一页

四、实际项目怎么使用?

假设你现在有个项目,需要多个组件共享数据,甚至多个进程共享:

场景 A:主程序把数据写到共享内存,子程序读取处理

// 写入方(主程序)
var mmf = MemoryMappedFile.CreateOrOpen("MyData", 4096);
var accessor = mmf.CreateViewAccessor();
accessor.Write(0, 9999);

// 读取方(另一个进程)
var mmf = MemoryMappedFile.OpenExisting("MyData");
var accessor = mmf.CreateViewAccessor();
int val = accessor.ReadInt32(0);

五、我该怎么学会它?

学习路线(建议你按顺序完成):

  1. 理解页的大小和作用:每页 4096 字节,不可拆分
  2. 掌握 MemoryMappedFile 的用法(练习跨方法读写)
  3. 掌握 Marshal.AllocHGlobal 的分配与释放
  4. 了解进阶内容(页对齐、虚拟内存布局、跨平台差异)
  5. ⛳ 在你当前项目中:找到适合分页内存的模块(如大数据缓存、实时共享等)

ReadOnlySpan memeory span、内存页、堆栈是什么关系

  • ReadOnlySpan<T> / Span<T> / Memory<T>
  • 内存页(Memory Page)
  • 堆(Heap)、栈(Stack)

它们都和内存有关,但属于不同层次的抽象或用途。我们分层次梳理,先建立概念,再讲它们之间的关系。


🧱 一、三个核心概念介绍


Span<T> / ReadOnlySpan<T> / Memory<T>

这些是 .NET 提供的高性能内存访问结构

类型 所在内存 特点
Span<T> 栈上或托管内存 用于短期、快速的数据操作,不能被 async 方法捕获
ReadOnlySpan<T> 栈上或托管内存 只读版本,适合只读视图
Memory<T> 堆上 可以跨 async 方法使用,可以转成 Span<T>

它们本质上是:

“对一块已存在内存的轻量引用视图,而不是实际拥有这块内存”。


✅ 内存页(Memory Page)

这是操作系统层面的内存管理单位,比如 Windows 和 Linux 都以 4KB 为默认一页。

内存页定义了:

  • 程序的虚拟地址空间如何分布
  • 哪部分在物理内存、哪部分在磁盘(例如交换区)
  • 页对齐(对性能有帮助)

内存页和 Span<T>/Memory<T> 之间没有直接联系,但当你访问大量数据时,它们的访问会跨越多个内存页,会影响性能(如 TLB 缓存失效)。


✅ 堆 vs 栈(托管代码视角)

特性 栈(Stack) 堆(Heap)
分配 快速、由编译器自动管理 较慢、由 GC 管理
生命周期 方法调用期间 手动/GC释放
示例 局部变量、Span<T> 引用类型、Memory<T>、数组等

🧠 二、它们之间的关系(图 + 解说)

我们可以把它们看作是不同的“层”:

【硬件层】
└── 物理内存
    └── 被划分为内存页(Page,通常为 4KB)

【操作系统层】
└── 虚拟内存(由内存页映射到物理内存)
    ├── 栈内存区域(函数局部变量、Span)
    └── 堆内存区域(引用类型、Memory)

【.NET 应用层】
└── 变量引用内存页上的数据:
    ├── Span<T> / ReadOnlySpan<T>(轻量视图,通常位于栈)
    └── Memory<T>(跨方法,通常引用堆上对象)

关系总结:

  • Span<T> / ReadOnlySpan<T> 是对已有内存(栈上或托管堆上)的一种零分配视图
  • Memory<T> 是对堆上内存的引用,可以长时间持有和异步操作
  • 这些结构最终访问的实际数据,都可能跨越多个内存页
  • 是否跨页影响性能(缓存失效、TLB miss)

📌 举个真实例子说明它们关系

byte[] buffer = new byte[8192]; // 在堆上分配了 2 个页大小(2x4096)

Span<byte> span = buffer.AsSpan(0, 100); // 创建 span 引用 buffer 的前 100 字节

这段代码:

  • 堆上分配 8192 字节 => 操作系统分配了 2 页
  • Span<byte> 只是对前 100 字节的视图(没有新分配内存)
  • 这 100 字节可能全在同一页内,也可能跨页

如果你处理大量数据、跨页读取较多,可能会频繁触发内存页加载、TLB miss,性能下降。


🎯 使用建议和注意事项

场景 推荐结构 原因
仅在方法内部处理小块数据 Span<T> / ReadOnlySpan<T> 无GC、速度快
跨方法/异步处理 Memory<T> 生命周期长、可用作缓存池
与非托管内存交互 Span<byte> + Marshal / UnmanagedMemoryAccessor 精细控制
需要页对齐(性能优化) 非托管内存 + 页大小对齐 避免跨页,提高效率

✅ 总结一句话:

Span<T> / Memory<T> 是对内存中一块数据的“高效访问视图”,而内存页决定了这些数据底层是如何被操作系统组织和访问的,最终落在堆或栈。


如果你项目中涉及:

  • 使用大内存块处理高频数据
  • 想避免不必要的内存分配
  • 与非托管代码(如设备驱动、共享内存)打交道

那你应该合理结合使用:Memory/Page/Span/Stack/Heap 这些概念。


posted @ 2025-06-03 14:04  青云Zeo  阅读(143)  评论(0)    收藏  举报