diff --git a/NET-Memory-Management-For-BCPS/Low-Level-Memory-Management.md b/NET-Memory-Management-For-BCPS/Low-Level-Memory-Management.md index 388d697..3021142 100644 --- a/NET-Memory-Management-For-BCPS/Low-Level-Memory-Management.md +++ b/NET-Memory-Management-For-BCPS/Low-Level-Memory-Management.md @@ -713,8 +713,59 @@ Windows上初始线程栈大小(包括保留和初始提交)来源于可执 #### Linux内存布局 +Linux 进程的内存布局与 Windows 非常相似。对于 **32 位版本**,用户空间占 **3 GB**,内核空间占 **1 GB**。这个分割点可以通过 **CONFIG_PAGE_OFFSET** 参数在**内核构建时进行配置**。对于 **64 位版本**,分割点位于与 Windows 相似的地址(参见 **图 2-20**)。 + +![](asserts/2-20.png) + +图 2-20:Linux 上 x86/ARM(32 位)和 x64(64 位)进程的虚拟内存布局 + +与 Windows 类似,Linux 系统提供了一套用于操作内存页的 API,其中包括: + +- **mmap**:用于**直接操作内存页**,包括**文件映射、共享内存、普通内存页**,以及**匿名映射**(不与任何文件关联,但用于存储程序数据)。 +- **brk/sbrk**:与 `VirtualAlloc` 方法最为接近,它允许设置/增加所谓的 “程序断点”,即**增加堆的大小**。 + +常见的C/C++ 内存分配器会根据分配大小的不同选择 `mmap` 或 `brk`。该阈值可以使用 `mallopt` 及其 `M_MMAP_THRESHOLD` 设置进行配置。如后续内容所述,CLR(.NET 公共语言运行时)在分配匿名私有页时使用的是 `mmap`。 + +在线程栈的处理方式上,Linux 与 Windows 存在一个显著区别: + +- Windows 采用**两阶段内存预留**,即先保留一块连续的虚拟内存区域,然后根据需要提交实际的物理内存页。 +- Linux 不会预先保留线程栈的内存页,而是根据需要动态扩展栈。因此,Linux 线程栈的内存并不是一个连续的区域,而是按需创建新的内存页。 + +#### 操作系统的影响 + +CLR 中包含的跨平台版垃圾回收器是否考虑了内存管理方面的任何差异?一般来说,垃圾回收器代码是非常独立于平台的,但由于显而易见的原因,在某些时候必须进行系统调用。两种操作系统的内存管理器工作方式类似,都是基于虚拟内存、分页和类似的内存分配方式。当然,虽然调用的系统应用程序接口不同,但从概念上讲,除了我们现在要描述的两种情况外,代码中没有具体的区别。 + +第一个区别已经提到过。Linux 没有预留内存和提交内存这两个步骤。在 Windows 上,可以使用系统调用先预留一个大内存块。这将创建适当的系统结构,而不会实际占用物理内存。只有在必要时,才会执行第二阶段的内存提交操作。由于 Linux 没有这种机制,因此只能在没有 “保留 ”的情况下分配内存。不过,我们需要一个系统应用程序接口来模仿保留/提交两步法。为此,我们使用了一种流行的技巧。在 Linux 中,“保留 ”是通过分配访问模式为 PROT_NONE 的内存来实现的,这实际上意味着不允许访问该内存。不过,在这样的保留区中,你可以再次分配具有正常权限的特定子区域,从而模拟 “提交 ”内存。 + +第二个区别是所谓的**内存写监视机制(memory write watch mechanism)**。在后面的章节中我们将看到,垃圾回收器需要跟踪哪些内存区域(页)被修改过。为此,Windows 提供了一个方便的 API。在分配页面时,可以设置 `MEM_WRITE_WATCH` 标志。然后,使用 `GetWriteWatch` API 就可以检索已修改页面的列表。在实现 .NET Core 的过程中,我们发现 Linux 上没有系统调用来建立这种监视机制。因此,必须将这一逻辑转移到写屏障(第 5 章将详细解释这一机制)中,运行时支持这一机制,而无需操作系统支持。 + ## NUMA 和 CPU 组 +内存管理大拼图中还有一块重要的拼图。**对称多处理(SMP)**是指一台计算机有多个相同的 CPU,它们连接到一个共享的主内存。它们由一个操作系统控制,该操作系统可能会也可能不会对所有处理器一视同仁。众所周知,每个 CPU 都有自己的 L1 和 L2 高速缓存。换句话说,每个 CPU 都有一些专用的本地内存,其访问速度比其他内存区域快得多。在不同 CPU 上运行的线程和程序可能会共享一些数据,但这并不理想,因为通过 CPU 互联共享数据会导致严重的延迟。这就是**非统一内存架构(NUMA)**发挥作用的地方。这意味着内存区域具有不同的性能特征,这取决于访问它们的 CPU。软件(主要是操作系统,也可以是程序本身)应具有 NUMA 感知,以便优先使用本地内存,而不是较远的内存。这种配置如图 2-21 所示。 + +![](asserts/2-21.png) + +图 2-21. 简单的 NUMA 配置,八个处理器分成两个 NUMA 节点 + +这种访问非本地内存的额外开销称为 **NUMA 因子**。由于直接连接所有 CPU 的成本非常高昂,因此每个 CPU 通常只能连接两到三个其他 CPU。要访问远处的内存,必须在处理器之间进行几次跳转,从而增加了延迟。如果使用非本地内存,CPU 越多,NUMA 因子就越重要。也有采用混合方法的系统,即处理器组之间有一些共享内存,而这些处理器组之间的内存是不均匀的,它们之间的 NUMA 因子很大。事实上,这是 NUMA 感知系统中最常见的方法。CPU 被分组为更小的系统,称为 NUMA 节点。每个 NUMA 节点都有自己的处理器和内存,由于硬件组织的原因,NUMA 因子较小。NUMA 节点当然可以互连,但它们之间的传输意味着更大的开销。 + +操作系统和程序代码对 NUMA 感知的主要要求是,坚持使用包含执行 CPU 的 NUMA 节点的本地 DRAM。但如果某些进程比其他进程消耗更多内存,这可能会导致不平衡状态。这就引出了一个问题:.NET 是否具有 NUMA 感知?答案很简单,是的!.NET的GC会在适当的NUMA节点上分配内存,因此内存会 “靠近 ”执行托管代码的线程。在堆平衡期间,GC 会优先平衡位于同一 NUMA 节点上的堆的分配。理论上,NUMA 感知可以通过运行时部分配置中的 `GCNumaAware` 设置来禁用,但很难想象为什么会有人想这么做。 + +不过,清单 2-7 中还显示了另外两个与所谓处理器组相关的重要应用程序设置。在拥有超过 64 个逻辑处理器的 Windows 系统中,这些处理器被分为上述 CPU 组。默认情况下,进程被限制在一个 CPU 组中,这意味着它们不会使用系统中所有可用的 CPU。 + +清单 2-7 .NET 运行时中的处理器组配置 + +``` + + + + + + +``` + +`GCCpuGroup` 设置用于指定垃圾收集器是否应通过在所有可用组中创建内部 GC 线程来支持 CPU 组,以及在创建和管理堆时是否将所有可用内核考虑在内。该设置应与 `gcServer` 设置同时启用。 +## 总结 -## 总结 \ No newline at end of file +通过本章的学习,我们已经有了长足的进步。我们简要介绍了最重要的硬件和系统内存管理机制。这些知识与上一章的理论介绍一起,将为您更好地理解.NET中的内存管理提供更广阔的背景。在以后的每一章中,我们将进一步远离一般硬件和理论陈述,深入研究.NET运行时。 \ No newline at end of file diff --git a/NET-Memory-Management-For-BCPS/asserts/2-20.png b/NET-Memory-Management-For-BCPS/asserts/2-20.png new file mode 100644 index 0000000..2dfe250 Binary files /dev/null and b/NET-Memory-Management-For-BCPS/asserts/2-20.png differ diff --git a/NET-Memory-Management-For-BCPS/asserts/2-21.png b/NET-Memory-Management-For-BCPS/asserts/2-21.png new file mode 100644 index 0000000..485fb3b Binary files /dev/null and b/NET-Memory-Management-For-BCPS/asserts/2-21.png differ