linux_mem2
本文《linux_mem2》由小宇宙从原站整理搬运,详细讲解相关技术要点。
4.7 深入理解 Linux 物理内存管理 作者:公众号@bin的技术小屋 来源:一步一图带你深入理解 Linux 物理内存管理 (opens new window) 1. 前文回顾 在上篇文章 《深入理解 Linux 虚拟内存管理》 (opens new window) 中,我分别从进程用户态和内核态的角度详细深入地为大家介绍了 Linux 内核如何对进程虚拟内存空间进行布局以及管理的相关实现。在我们深入理解了虚拟内存之后,那么何不顺带着也探秘一下物理内存的管理呢? 所以本文的目的是在深入理解虚拟内存管理的基础之上继续带大家向前奋进,一举击破物理内存管理的知识盲区,使大家能够俯瞰整个 Linux 内存管理子系统的整体全貌。 而在正式开始物理内存管理的主题之前,我觉得有必须在带大家回顾下上篇文章中介绍的虚拟内存管理的相关知识,方便大家来回对比虚拟内存和物理内存,从而可以全面整体地掌握 Linux 内存管理子系统。 在上篇文章的一开始,我首先为大家展现了我们应用程序频繁接触到的虚拟内存地址,清晰地为大家介绍了到底什么是虚拟内存地址,以及虚拟内存地址分别在 32 位系统和 64 位系统中的具体表现形式: 在我们清楚了虚拟内存地址这个基本概念之后,随后我又抛出了一个问题:为什么我们要通过虚拟内存地址访问内存而不是直接通过物理地址访问? 原来是在多进程系统中直接操作物理内存地址的话,我们需要精确地知道每一个变量的位置都被安排在了哪里,而且还要注意当前进程在和多个进程同时运行的时候,不能共用同一个地址,否则就会造成地址冲突。 而虚拟内存空间的引入正是为了解决多进程地址冲突的问题,使得进程与进程之间的虚拟内存地址空间相互隔离,互不干扰。每个进程都认为自己独占所有内存空间,将多进程之间的协同相关细节统统交给内核中的内存管理模块来处理,极大地解放了程序员的心智负担。这一切都是因为虚拟内存能够为进程提供内存地址空间隔离的功劳。 在我们清楚了虚拟内存空间引入的意义之后,我紧接着为大家介绍了 进程用户态 虚拟内存空间分别在 32 位机器和 64 位机器上的布局情况: 在了解了用户态虚拟内存空间的布局之后,紧接着我们又介绍了 Linux 内核如何对用户态虚拟内存空间进行管理以及相应的管理数据结构: 在介绍完用户态虚拟内存空间的布局以及管理之后,我们随后又介绍了 内核态 虚拟内存空间的布局情况,并结合之前介绍的 用户态 虚拟内存空间,得到了 Linux 虚拟内存空间分别在 32 位和 64 位系统中的整体布局情况: 在虚拟内存全部介绍完毕之后,为了能够承上启下,于是我继续在上篇文章的最后一个小节从计算机组成原理的角度介绍了物理内存的物理组织结构,方便让大家理解到底什么是真正的物理内存 ?物理内存地址到底是什么 ?由此为本文的主题 —— 物理内存的管理 ,埋下伏笔 最后我介绍了 CPU 如何通过物理内存地址向物理内存读写数据的完整过程: 在我们回顾完上篇文章介绍的用户态和内核态虚拟内存空间的管理,以及物理内存在计算机中的真实组成结构之后,下面我就来正式地为大家介绍本文的主题 —— Linux 内核如何对物理内存进行管理 2. 从 CPU 角度看物理内存模型 在前边的文章中,我曾多次提到内核是以页为基本单位对物理内存进行管理的,通过将物理内存划分为一页一页的内存块,每页大小为 4K。一页大小的内存块在内核中用 struct page 结构体来进行管理,struct page 中封装了每页内存块的状态信息,比如:组织结构,使用信息,统计信息,以及与其他结构的关联映射信息等。 而为了快速索引到具体的物理内存页,内核为每个物理页 struct page 结构体定义了一个索引编号:PFN(Page Frame Number)。PFN 与 struct page 是一一对应的关系。 内核提供了两个宏来完成 PFN 与 物理页结构体 struct page 之间的相互转换。它们分别是 page\ to\ pfn 与 pfn\ to\ page。 内核中如何组织管理这些物理内存页 struct page 的方式我们称之为做物理内存模型,不同的物理内存模型,应对的场景以及 page\ to\ pfn 与 pfn\ to\ page 的计算逻辑都是不一样的。 2.1 FLATMEM 平坦内存模型 我们先把物理内存想象成一片地址连续的存储空间,在这一大片地址连续的内存空间中,内核将这块内存空间分为一页一页的内存块 struct page 。 由于这块物理内存是连续的,物理地址也是连续的,划分出来的这一页一页的物理页必然也是连续的,并且每页的大小都是固定的,所以我们很容易想到用一个数组来组织这些连续的物理内存页 struct page 结构,其在数组中对应的下标即为 PFN 。这种内存模型就叫做平坦内存模型 FLATMEM 。 内核中使用了一个 mem\ map 的全局数组用来组织所有划分出来的物理内存页。mem\ map 全局数组的下标就是相应物理页对应的 PFN 。 在平坦内存模型下 ,page\ to\ pfn 与 pfn\ to\ page 的计算逻辑就非常简单,本质就是基于 mem\ map 数组进行偏移操作。 ARCH\ PFN\ OFFSET 是 PFN 的起始偏移量。 Linux 早期使用的就是这种内存模型,因为在 Linux 发展的早期所需要管理的物理内存通常不大(比如几十 MB),那时的 Linux 使用平坦内存模型 FLATMEM 来管理物理内存就足够高效了。 内核中的默认配置是使用 FLATMEM 平坦内存模型。 2.2 DISCONTIGMEM 非连续内存模型 FLATMEM 平坦内存模型只适合管理一整块连续的物理内存,而对于多块非连续的物理内存来说使用 FLATMEM 平坦内存模型进行管理则会造成很大的内存空间浪费。 因为 FLATMEM 平坦内存模型是利用 mem\ map 这样一个全局数组来组织这些被划分出来的物理页 page 的,而对于物理内存存在大量不连续的内存地址区间这种情况时,这些不连续的内存地址区间就形成了内存空洞。 由于用于组织物理页的底层数据结构是 mem\ map 数组,数组的特性又要求这些物理页是连续的,所以只能为这些内存地址空洞也分配 struct page 结构用来填充数组使其连续。 而每个 struct page 结构大部分情况下需要占用 40 字节(struct page 结构在不同场景下内存占用会有所不同,这一点我们后面再说),如果物理内存中存在的大块的地址空洞,那么为这些空洞而分配的 struct page 将会占用大量的内存空间,导致巨大的浪费。 为了组织和管理这些不连续的物理内存,内核于是引入了 DISCONTIGMEM 非连续内存模型,用来消除这些不连续的内存地址空洞对 mem\ map 的空间浪费。 在 DISCONTIGMEM 非连续内存模型中,内核将物理内存从宏观上划分成了一个一个的节点 node (微观上还是一页一页的物理页),每个 node 节点管理一块连续的物理内存。这样一来这些连续的物理内存页均被划归到了对应的 node 节点中管理,就避免了内存空洞造成的空间浪费。 内核中使用 struct pglist\ data 表示用于管理连续物理内存的 node 节点(内核假设 node 中的物理内存是连续的),既然每个 node 节点中的物理内存是连续的,于是在每个 node 节点中还是采用 FLATMEM 平坦内存模型的方式来组织管理物理内存页。每个 node 节点中包含一个 数组,用来组织管理 node 中的连续物理内存页。 我们可以看出 DISCONTIGMEM 非连续内存模型其实就是 FLATMEM 平坦内存模型的一种扩展,在面对大块不连续的物理内存管理时,通过将每段连续的物理内存区间划归到 node 节点中进行管理,避免了为内存地址空洞分配 struct page 结构,从而节省了内存资源的开销。 由于引入了 node 节点这个概念,所以在 DISCONTIGMEM 非连续内存模型下 page\ to\ pfn 与 pfn\ to\ page 的计算逻辑就比 FLATMEM 内存模型下的计算逻辑多了一步定位 page 所在 node 的操作。 通过 arch\ pfn\ to\ nid 可以根据物理页的 PFN 定位到物理页所在 node。 通过 page\ to\ nid 可以根据物理页结构 struct page 定义到 page 所在 node。 当定位到物理页 struct page 所在 node 之后,剩下的逻辑就和 FLATMEM 内存模型一模一样了。 2.3 SPARSEMEM 稀疏内存模型 随着内存技术的发展,内核可以支持物理内存的热插拔了(后面我会介绍),这样一来物理内存的不连续就变为常态了,在上小节介绍的 DISCONTIGMEM 内存模型中,其实每个 node 中的物理内存也不一定都是连续的。 而且每个 node 中都有一套完整的内存管理系统,如果 node 数目多的话,那这个开销就大了,于是就有了对连续物理内存更细粒度的管理需求,为了能够更灵活地管理粒度更小的连续物理内存,SPARSEMEM 稀疏内存模型就此登场了。 SPARSEMEM 稀疏内存模型的核心思想就是对粒度更小的连续内存块进行精细的管理,用于管理连续内存块的单元被称作 section 。物理页大小为 4k 的情况下, section 的大小为 128M ,物理页大小为 16k 的情况下, section 的大小为 512M。 在内核中用 struct mem\ section 结构体表示 SPARSEMEM 模型中的 section。 由于 section 被用作管理小粒度的连续内存块,这些小的连续物理内存在 section 中也是通过数组的方式被组织管理,每个 struct mem\ section 结构体中有一个 section\ mem\ map 指针用于指向 section 中管理连续内存的 page 数组。 SPARSEMEM 内存模型中的这些所有的 mem\ section 会被存放在一个全局的数组中,并且每个 mem\ section 都可以在系统运行时改变 offline / online (下线 / 上线)状态,以便支持内存的热插拔(hotplug)功能。 在 SPARSEMEM 稀疏内存模型下 page\ to\ pfn 与 pfn\ to\ page 的计算逻辑又发生了变化。 在 page\ to\ pfn 的转换中,首先需要通过 page\ to\ section 根据 struct page 结构定位到 mem\ section 数组中具体的 section 结构。然后在通过 section\ mem\ map 定位到具体的 PFN。 在 struct page 结构中有一个 属性,在 flag 的高位 bit 中存储着 page 所在 mem\ section 数组中的索引,从而可以定位到所属 section。 在 pfn\ to\ page 的转换中,首先需要通过 \ \ pfn\ to\ section 根据 PFN 定位到 mem\ section 数组中具体的 section 结构。然后在通过 PFN 在 section\ mem\ map 数组中定位到具体的物理页 Page 。 PFN 的高位 bit 存储的是全局数组 mem\ section 中的 section 索引,PFN 的低位 bit 存储的是 section\ mem\ map 数组中具体物理页 page 的索引。 从以上的内容介绍中,我们可以看出 SPARSEMEM 稀疏内存模型已经完全覆盖了前两个内存模型的所有功能,因此稀疏内存模型可被用于所有内存布局的情况。 2.3.1 物理内存热插拔 前面提到随着内存技术的发展,物理内存的热插拔 hotplug 在内核中得到了支持,由于物理内存可以动态的从主板中插入以及拔出,所以导致了物理内存的不连续已经成为常态,因此内核引入了 SPARSEMEM 稀疏内存模型以便应对这种情况,提供对更小粒度的连续物理内存的灵活管理能力。 本小节我就为大家介绍一下物理内存热插拔 hotplug 功能在内核中的实现原理,作为 SPARSEMEM 稀疏内存模型的扩展内容补充。 在大规模的集群中,尤其是现在我们处于云原生的时代,为了实现集群资源的动态均衡,可以通过物理内存热插拔的功能实现集群机器物理内存容量的动态增减。 集群的规模一大,那么物理内存出故障的几率也会大大增加,物理内存的热插拔对提供集群高可用性也是至关重要的。 从总体上来讲,内存的热插拔分为两个阶段: 物理热插拔阶段:这个阶段主要是从物理上将内存硬件插入(hot add),拔出(hot remove)主板的过程,其中涉及到硬件和内核的支持。 逻辑热插拔阶段:这一阶段主要是由内核中的内存管理子系统来负责,涉及到的主要工作为:如何动态的上线启用(online)刚刚 hot add 的内存,如何动态下线(offline)刚刚 hot remove 的内存。 物理内存拔出的过程需要关注的事情比插入的过程要多的多,实现起来也更加的困难, 这就好比在《Java 技术栈中间件优雅停机方案设计与实现全景图》 (opens new window) 一文中我们讨论服务优雅启动,停机时提到的:优雅停机永远比优雅启动要考虑的场景要复杂的多,因为停机的时候,线上的服务正在承载着生产的流量需要确保做到业务无损。 同样的道理,物理内存插入比较好说,困难的是物理内存的动态拔出,因为此时即将要被拔出的物理内存中可能已经为进程分配了物理页,如何妥善安置这些已经被分配的物理页是一个棘手的问题。 前边我们介绍 SPARSEMEM 内存模型的时候提到,每个 mem\ section 都可以在系统运行时改变 offline ,online 状态,以便支持内存的热插拔(hotplug)功能。 当 mem\ section offline 时, 内核会把这部分内存隔离开, 使得该部分内存不可再被使用, 然后再把 mem\ section 中已经分配的内存页迁移到其他 mem\ section 的内存上. 。 但是这里会有一个问题,就是并非所有的物理页都可以迁移,因为迁移意味着物理内存地址的变化,而内存的热插拔应该对进程来说是透明的,所以这些迁移后的物理页映射的虚拟内存地址是不能变化的。 这一点在进程的用户空间是没有问题的,因为进程在用户空间访问内存都是根据虚拟内存地址通过页表找到对应的物理内存地址,这些迁移之后的物理页,虽然物理内存地址发生变化,但是内核通过修改相应页表中虚拟内存地址与物理内存地址之间的映射关系,可以保证虚拟内存地址不会改变。 但是在内核态的虚拟地址空间中,有一段直接映射区,在这段虚拟内存区域中虚拟地址与物理地址是直接映射的关系,虚拟内存地址直接减去一个固定的偏移量(0xC000 0000 ) 就得到了物理内存地址。 直接映射区中的物理页的虚拟地址会随着物理内存地址变动而变动, 因此这部分物理页是无法轻易迁移的,然而不可迁移的页会导致内存无法被拔除,因为无法妥善安置被拔出内存中已经为进程分配的物理页。那么内核是如何解决这个头疼的问题呢? 既然是这些不可迁移的物理页导致内存无法拔出,那么我们可以把内存分一下类,将内存按照物理页是否可迁移,划分为不可迁移页,可回收页,可迁移页。 大家这里需要记住一点,内核会将物理内存按照页面是否可迁移的特性进行分类,我后面在介绍内核如何避免内存碎片的时候还会在提到 然后在这些可能会被拔出的内存中只分配那些可迁移的内存页,这些信息会在内存初始化的时候被设置,这样一来那些不可迁移的页就不会包含在可能会拔出的内存中,当我们需要将这块内存热拔出时, 因为里边的内存页全部是可迁移的, 从而使内存可以被拔除。 3. 从 CPU 角度看物理内存架构 在上小节中我为大家介绍了三种物理内存模型,这三种物理内存模型是从 CPU 的视角来看待物理内存内部是如何布局,组织以及管理的,主角是物理内存。 在本小节中我为大家提供一个新的视角,这一次我们把物理内存看成一个整体,从 CPU 访问物理内存的角度来看一下物理内存的架构,并从 CPU 与物理内存的相对位置变化来看一下不同物理内存架构下对性能的影响。 3.1 一致性内存访问 UMA 架构 我们在上篇文章 《深入理解 Linux 虚拟内存管理》 (opens new window)的 “ 8.2 CPU 如何读写主存” 小节中提到 CPU 与内存之间的交互是通过总线完成的。 首先 CPU 将物理内存地址作为地址信号放到系统总线上传输。随后 IO bridge 将系统总线上的地址信号转换为存储总线上的电子信号。 主存感受到存储总线上的地址信号并通过存储控制器将存储总线上的物理内存地址 A 读取出来。 存储控制器通过物理内存地址定位到具体的存储器模块,从 DRAM 芯片中取出物理内存地址对应的数据。 存储控制器将读取到的数据放到存储总线上,随后 IO bridge 将存储总线上的数据信号转换为系统总线上的数据信号,然后继续沿着系统总线传递。 CPU 芯片感受到系统总线上的数据信号,将数据从系统总线上读取出来并拷贝到寄存器中。 上图展示的是单核 CPU 访问内存的架构图,那么在多核服务器中多个 CPU 与内存之间的架构关系又是什么样子的呢? 在 UMA 架构下,多核服务器中的多个 CPU 位于总线的一侧,所有的内存条组成一大片内存位于总线的另一侧,所有的 CPU 访问内存都要过总线,而且距离都是一样的,由于所有 CPU 对内存的访问距离都是一样的,所以在 UMA 架构下所有 CPU 访问内存的速度都是一样的。这种访问模式称为 SMP(Symmetric multiprocessing),即对称多处理器。 这里的一致性是指同一个 CPU 对所有内存的访问的速度是一样的。即一致性内存访问 UMA(Uniform Memory Access)。 但是随着多核技术的发展,服务器上的 CPU 个数会越来越多,而 UMA 架构下所有 CPU 都是需要通过总线来访问内存的,这样总线很快就会成为性能瓶颈,主要体现在以下两个方面: 1. 总线的带宽压力会越来越大,随着 CPU 个数的增多导致每个 CPU 可用带宽会减少 2. 总线的长度也会因此而增加,进而增加访问延迟 UMA 架构的优点很明显就是结构简单,所有的 CPU 访问内存速度都是一致的,都必须经过总线。然而它的缺点我刚刚也提到了,就是随着处理器核数的增多,总线的带宽压力会越来越大。解决办法就只能扩宽总线,然而成本十分高昂,未来可能仍然面临带宽压力。 为了解决以上问题,提高 CPU 访问内存的性能和扩展性,于是引入了一种新的架构:非一致性内存访问 NUMA(Non uniform memory access)。 3.2 非一致性内存访问 NUMA 架构 在 NUMA 架构下,内存就不是一整片的了,而是被划分成了一个一个的内存节点 (NUMA 节点),每个 CPU 都有属于自己的本地内存节点,CPU 访问自己的本地内存不需要经过总线,因此访问速度是最快的。当 CPU 自己的本地内存不足时,CPU 就需要跨节点去访问其他内存节点,这种情况下 CPU 访问内存就会慢很多。 在 NUMA 架构下,任意一个 CPU 都可以访问全部的内存节点,访问自己的本地内存节点是最快的,但访问其他内存节点就会慢很多,这就导致了 CPU 访问内存的速度不一致,所以叫做非一致性内存访问架构。 如上图所示,CPU 和它的本地内存组成了 NUMA 节点,CPU 与 CPU 之间通过 QPI(Intel QuickPath Interconnect)点对点完成互联,在 CPU 的本地内存不足的情况下,CPU 需要通过 QPI 访问远程 NUMA 节点上的内存控制器从而在远程内存节点上分配内存,这就导致了远程访问比本地访问多了额外的延迟开销(需要通过 QPI 遍历远程 NUMA 节点)。 在 NUMA 架构下,只有 DISCONTIGMEM 非连续内存模型和 SPARSEMEM 稀疏内存模型是可用的。而 UMA 架构下,前面介绍的三种内存模型都可以配置使用。 3.2.1 NUMA 的内存分配策略 NUMA 的内存分配策略是指在 NUMA 架构下 CPU 如何请求内存分配的相关策略,比如:是优先请求本地内存节点分配内存呢 ?还是优先请求指定的 NUMA 节点分配内存 ?是只能在本地内存节点分配呢 ?还是允许当本地内存不足的情况下可以请求远程 NUMA 节点分配内存 ? | 内存分配策略 | 策略描述 | | | | | MPOL\ BIND | 必须在绑定的节点进行内存分配,如果内存不足,则进行 swap | | MPOL\ INTERLEAVE | 本地节点和远程节点均可允许分配内存 | | MPOL\ PREFERRED | 优先在指定节点分配内存,当指定节点内存不足时,选择离指定节点最近的节点分配内存 | | MPOL\ LOCAL (默认) | 优先在本地节点分配,当本地节点内存不足时,可以在远程节点分配内存 | 我们可以在应用程序中通过 libnuma 共享库中的 API 调用 set\ mempolicy 接口设置进程的内存分配策略。 mode : 指定 NUMA 内存分配策略。 nodemask:指定 NUMA 节点 Id。 maxnode:指定最大 NUMA 节点 Id,用于遍历远程节点,实现跨 NUMA 节点分配内存。 libnuma 共享库 API 文档:https://man7.org/linux/man pages/man3/numa.3.html top\ of\ page set\ mempolicy 接口文档:https://man7.org/linux/man pages/man2/set\ mempolicy.2.html 3.2.2 NUMA 的使用简介 在我们理解了物理内存的 NUMA 架构,以及在 NUMA 架构下的内存分配策略之后,本小节我来为大家介绍下如何正确的利用 NUMA 提升我们应用程序的性能。 前边我们介绍了这么多的理论知识,但是理论的东西总是很虚,正所谓眼见为实,大家一定想亲眼看一下 NUMA 架构在计算机中的具体表现形式,比如:在支持 NUMA 架构的机器上到底有多少个 NUMA 节点?每个 NUMA 节点包含哪些 CPU 核,具体是怎样的一个分布情况? 前面也提到 CPU 在访问本地 NUMA 节点中的内存时,速度是最快的。但是当访问远程 NUMA 节点,速度就会相对很慢,那么到底有多慢?本地节点与远程节点之间的访问速度差异具体是多少 ? 3.2.2.1 查看 NUMA 相关信息 numactl 文档:https://man7.org/linux/man pages/man8/numactl.8.html 针对以上具体问题, 命令可以给出我们想要的答案: 命令可以查看服务器的 NUMA 配置,上图中的服务器配置共包含 4 个 NUMA 节点(0 3),每个 NUMA 节点中包含 16个 CPU 核心,本地内存大小约为 64G。 大家可以关注下最后 这一栏,node distances 给出了不同 NUMA 节点之间的访问距离,对角线上的值均为本地节点的访问距离 10 。比如 [0,0] 表示 NUMA 节点 0 的本地内存访问距离。 我们可以很明显的看到当出现跨 NUMA 节点访问的时候,访问距离就会明显增加,比如节点 0 访问节点 1 的距离 [0,1] 是16,节点 0 访问节点 3 的距离 [0,3] 是 33。距离越远,跨 NUMA 节点内存访问的延时越大。应用程序运行时应减少跨 NUMA 节点访问内存。 此外我们还可以通过 来查看 NUMA 的内存分配策略设置: 通过 还可以查看各个 NUMA 节点的内存访问命中率: numa\ hit :内存分配在该节点中成功的次数。 numa\ miss : 内存分配在该节点中失败的次数。 numa\ foreign:表示其他 NUMA 节点本地内存分配失败,跨节点(numa\ miss)来到本节点分配内存的次数。 interleave\ hit : 在 MPOL\ INTERLEAVE 策略下,在本地节点分配内存的次数。 local\ node:进程在本地节点分配内存成功的次数。 other\ node:运行在本节点的进程跨节点在其他节点上分配内存的次数。 numastat 文档:https://man7.org/linux/man pages/man8/numastat.8.html 3.2.2.2 绑定 NUMA 节点 numactl 工具可以让我们应用程序指定运行在哪些 CPU 核心上,同时也可以指定我们的应用程序可以在哪些 NUMA 节点上分配内存。通过将应用程序与具体的 CPU 核心和 NUMA 节点绑定,从而可以提升程序的性能。 通过 可以指定我们的应用程序只能在哪些具体的 NUMA 节点上分配内存,如果这些节点内存不足,则分配失败。 通过 可以指定我们的应用程序只能运行在哪些 NUMA 节点上。 另外我们还可以通过 将我们的应用程序绑定到具体的物理 CPU 上。这个选项后边指定的参数我们可以通过 输出信息中的 processor 这一栏查看。例如:通过 命令将进程 numatest 绑定到 0 15 CPU 上执行。 我们可以通过 numactl 命令将 numatest 进程分别绑定在相同的 NUMA 节点上和不同的 NUMA 节点上,运行观察。 大家肯定一眼就能看出绑定在相同 NUMA 节点的进程运行会更快,因为通过前边对 NUMA 架构的介绍,我们知道 CPU 访问本地 NUMA 节点的内存是最快的。 除了 numactl 这个工具外,我们还可以通过共享库 libnuma 在程序中进行 NUMA 相关的操作。这里我就不演示了,感兴趣可以查看下 libnuma 的 API 文档:https://man7.org/linux/man pages/man3/numa.3.html top\ of\ page 4. 内核如何管理 NUMA 节点 在前边我们介绍物理内存模型和物理内存架构的时候提到过:在 NUMA 架构下,只有 DISCONTIGMEM 非连续内存模型和 SPARSEMEM 稀疏内存模型是可用的。而 UMA 架构下,前面介绍的三种内存模型均可以配置使用。 无论是 NUMA 架构还是 UMA 架构在内核中都是使用相同的数据结构来组织管理的,在内核的内存管理模块中会把 UMA 架构当做只有一个 NUMA 节点的伪 NUMA 架构。这样一来这两种架构模式就在内核中被统一管理起来。 下面我先从最顶层的设计开始为大家介绍一下内核是如何管理这些 NUMA 节点的 NUMA 节点中可能会包含多个 CPU,这些 CPU 均是物理 CPU,这点大家需要注意一下。 4.1 内核如何统一组织 NUMA 节点 首先我们来看第一个问题,在内核中是如何将这些 NUMA 节点统一管理起来的? 内核中使用了 struct pglist\ data 这样的一个数据结构来描述 NUMA 节点, 在内核 2.4 版本之前 ,内核是使用一个 pgdat\ list 单链表将这些 NUMA 节点串联起来的,单链表定义在 文件中: 每个 NUMA 节点的数据结构 struct pglist\ data 中有一个 next 指针,用于将这些 NUMA 节点串联起来形成 pgdat\ list 单链表,链表的末尾节点 next 指针指向 NULL。 在内核 2.4 之后的版本中 ,内核移除了 struct pglist\ data 结构中的 pgdat\ next 之指针, 同时也删除了 pgdat\ list 单链表。取而代之的是,内核使用了一个大小为 MAX\ NUMNODES ,类型为 struct pglist\ data 的全局数组 node\ data[] 来管理所有的 NUMA 节点。 全局数组 node\ data[] 定义在文件 中: NODE\ DATA(nid) 宏可以通过 NUMA 节点的 nodeId,找到对应的 struct pglist\ data 结构。 node\ data[] 数组大小 MAX\ NUMNODES 定义在 文件中: UMA 架构下 NODES\ SHIFT 为 0 ,所以内核中只用一个 NUMA 节点来管理所有物理内存。 4.2 NUMA 节点描述符 pglist\ data 结构 node\ id 表示 NUMA 节点的 id,我们可以通过 numactl H 命令的输出结果查看节点 id。从 0 开始依次对 NUMA 节点进行编号。 struct page 类型的数组 node\ mem\ map 中包含了 NUMA节点内的所有的物理内存页。 node\ start\ pfn 指向 NUMA 节点内第一个物理页的 PFN,系统中所有 NUMA 节点中的物理页都是依次编号的,每个物理页的 PFN 都是 全局唯一的 (不只是其所在 NUMA 节点内唯一) node\ present\ pages 用于统计 NUMA 节点内所有真正可用的物理页面数量(不包含内存空洞)。 由于 NUMA 节点内包含的物理内存并不总是连续的,可能会包含一些内存空洞,node\ spanned\ pages 则是用于统计 NUMA 节点内所有的内存页,包含不连续的物理内存地址(内存空洞)的页面数。 以上内容是我从整体上为大家介绍的 NUMA 节点如何管理节点内部的本地内存。事实上内核还会将 NUMA 节点中的本地内存做近一步的划分。那么为什么要近一步划分呢? 4.3 NUMA 节点物理内存区域的划分 我们都知道内核对物理内存的管理都是以页为最小单位来管理的,每页默认 4K 大小,理想状况下任何种类的数据都可以存放在任何页框中,没有什么限制。比如:存放内核数据,用户数据,磁盘缓冲数据等。 但是实际的计算机体系结构受到硬件方面的制约,间接导致限制了页框的使用方式。 比如在 X86 体系结构下,ISA 总线的 DMA (直接内存存取)控制器,只能对内存的前16M 进行寻址,这就导致了 ISA 设备不能在整个 32 位地址空间中执行 DMA,只能使用物理内存的前 16M 进行 DMA 操作。 因此直接映射区的前 16M 专门让内核用来为 DMA 分配内存,这块 16M 大小的内存区域我们称之为 ZONE\ DMA。 用于 DMA 的内存必须从 ZONE\ DMA 区域中分配。 而直接映射区中剩下的部分也就是从 16M 到 896M(不包含 896M)这段区域,我们称之为 ZONE\ NORMAL。从字面意义上我们可以了解到,这块区域包含的就是正常的页框(没有任何使用限制)。 ZONE\ NORMAL 由于也是属于直接映射区的一部分,对应的物理内存 16M 到 896M 这段区域也是被直接映射至内核态虚拟内存空间中的 3G + 16M 到 3G + 896M 这段虚拟内存上。 而物理内存 896M 以上的区域被内核划分为 ZONE\ HIGHMEM 区域,我们称之为高端内存。 由于内核虚拟内存空间中的前 896M 虚拟内存已经被直接映射区所占用,而在 32 体系结构下内核虚拟内存空间总共也就 1G 的大小,这样一来内核剩余可用的虚拟内存空间就变为了 1G 896M = 128M。 显然物理内存中剩下的这 3200M 大小的 ZONE\ HIGHMEM 区域无法继续通过直接映射的方式映射到这 128M 大小的虚拟内存空间中。 这样一来物理内存中的 ZONE\ HIGHMEM 区域就只能采用动态映射的方式映射到 128M 大小的内核虚拟内存空间中,也就是说只能动态的一部分一部分的分批映射,先映射正在使用的这部分,使用完毕解除映射,接着映射其他部分。 所以内核会根据各个物理内存区域的功能不同,将 NUMA 节点内的物理内存主要划分为以下四个物理内存区域: 1. ZONE\ DMA:用于那些无法对全部物理内存进行寻址的硬件设备,进行 DMA 时的内存分配。例如前边介绍的 ISA 设备只能对物理内存的前 16M 进行寻址。该区域的长度依赖于具体的处理器类型。 2. ZONE\ DMA32:与 ZONE\ DMA 区域类似,该区域内的物理页面可用于执行 DMA 操作,不同之处在于该区域是提供给 32 位设备(只能寻址 4G 物理内存)执行 DMA 操作时使用的。该区域只在 64 位系统中起作用,因为只有在 64 位系统中才会专门为 32 位设备提供专门的 DMA 区域。 3. ZONE\ NORMAL:这个区域的物理页都可以直接映射到内核中的虚拟内存,由于是线性映射,内核可以直接进行访问。 4. ZONE\ HIGHMEM:这个区域包含的物理页就是我们说的高端内存,内核不能直接访问这些物理页,这些物理页需要动态映射进内核虚拟内存空间中(非线性映射)。该区域只在 32 位系统中才会存在,因为 64 位系统中的内核虚拟内存空间太大了(128T),都可以进行直接映射。 以上这些物理内存区域的划分定义在 文件中: 大家可能注意到内核中定义的 zone\ type 除了上边为大家介绍的四个物理内存区域,又多出了两个区域:ZONE\ MOVABLE 和 ZONE\ DEVICE。 ZONE\ DEVICE 是为支持热插拔设备而分配的非易失性内存( Non Volatile Memory ),也可用于内核崩溃时保存相关的调试信息。 ZONE\ MOVABLE 是内核定义的一个 虚拟内存区域 ,该区域中的物理页可以来自于上边介绍的几种真实的物理区域。该区域中的页全部都是可以迁移的,主要是为了防止内存碎片和支持内存的热插拔。 既然有了这些实际的物理内存区域,那么内核为什么又要划分出一个 ZONE\ MOVABLE 这样的虚拟内存区域呢 ? 因为随着系统的运行会伴随着不同大小的物理内存页的分配和释放,这种内存不规则的分配释放随着系统的长时间运行就会导致内存碎片,内存碎片会使得系统在明明有足够内存的情况下,依然无法为进程分配合适的内存。 如上图所示,假如现在系统一共有 16 个物理内存页,当前系统只是分配了 3 个物理页,那么在当前系统中还剩余 13 个物理内存页的情况下,如果内核想要分配 8 个连续的物理页的话,就会由于内存碎片的存在导致分配失败。(只能分配最多 4 个连续的物理页) 内核中请求分配的物理页面数只能是 2 的次幂!! 如果这些物理页处于 ZONE\ MOVABLE 区域,它们就可以被迁移,内核可以通过迁移页面来避免内存碎片的问题: 内核通过迁移页面来规整内存,这样就可以避免内存碎片,从而得到一大片连续的物理内存,以满足内核对大块连续内存分配的请求。 所以这就是内核需要根据物理页面是否能够迁移的特性,而划分出 ZONE\ MOVABLE 区域的目的 。 到这里,我们已经清楚了 NUMA 节点中物理内存区域的划分,下面我们继续回到 struct pglist\ data 结构中看下内核如何在 NUMA 节点中组织这些划分出来的内存区域: nr\ zones 用于统计 NUMA 节点内包含的物理内存区域个数, 不是每个 NUMA 节点都会包含以上介绍的所有物理内存区域,NUMA 节点之间所包含的物理内存区域个数是不一样的 。 事实上只有第一个 NUMA 节点可以包含所有的物理内存区域,其它的节点并不能包含所有的区域类型,因为有些内存区域比如:ZONE\ DMA,ZONE\ DMA32 必须从物理内存的起点开始。这些在物理内存开始的区域可能已经被划分到第一个 NUMA 节点了,后面的物理内存才会被依次划分给接下来的 NUMA 节点。因此后面的 NUMA 节点并不会包含 ZONE\ DMA,ZONE\ DMA32 区域