Linux 内存管理详细分析

引言

内存管理是 Linux 内核的基石,在确保系统稳定性、优化性能以及高效利用可用硬件资源方面发挥着至关重要的作用。像 Linux 这样的现代操作系统中的内存管理过程非常复杂,涉及从物理硬件到用户空间中运行的应用程序的多个抽象层和复杂的机制。不充分的内存管理可能导致一系列问题,包括显著的性能下降、表现为崩溃的系统不稳定,以及无法有效地并发运行多个应用程序。本报告旨在对 Linux 内存管理子系统进行全面且专业的分析。它将深入探讨构成内存管理基础的基本概念,探索 Linux 内核中负责跟踪和控制内存的关键数据结构,阐明内核空间和用户空间中内存分配和释放的机制,详细介绍内核在需要时回收内存所使用的策略,检查允许用户空间程序与内存管理子系统交互的系统调用,讨论可用于监控内存使用和性能的工具,概述各种优化内存性能的技术,深入了解 Linux 内核源代码的相关部分,并分析 BCC(伯克利包过滤器编译器集合)项目中与内存相关的工具的实用性。通过涵盖这些不同的方面,本报告旨在提供对 Linux 操作系统这一关键组件的透彻理解。

Linux 内存管理基础

Linux 中的内存管理的基础在于虚拟内存和物理内存之间的区别,以及地址空间的概念。这些抽象对于实现高效的多任务处理、提供安全性以及简化应用程序的内存管理至关重要。

虚拟内存与物理内存

物理内存,或称 RAM,是计算机系统中有限的资源,其容量直接限制了任何给定时间可以主动处理的数据和代码量。 此外,系统中安装的物理内存可能并不总是连续的块,并且其组织结构可能因不同的硬件架构而异。 即使在支持内存热插拔(允许动态添加或移除 RAM)的系统中,也存在最终的物理限制。 为了使应用程序软件免受直接管理物理内存的复杂性和限制的影响,开发了虚拟内存的概念。 虚拟内存充当中间层,从应用程序软件中抽象出物理内存的细节,并为应用程序提供一致的逻辑内存视图。 这种抽象简化了应用程序开发人员的内存管理,使他们能够在看似连续的地址空间中操作。

虚拟内存的一个关键优势是按需分页,这是一种技术,Linux 内核仅在进程实际需要时才将页面加载到物理内存中。 这种即时加载通过确保只有必要的数据和代码在任何给定时刻驻留在 RAM 中来优化资源利用率。此外,虚拟内存在不同的进程之间提供了至关重要的保护,防止一个应用程序无意或恶意地访问或破坏属于另一个进程的内存。 当明确需要时,它还有助于在进程之间进行受控的数据共享。 通过虚拟内存,进程发起的每个内存访问都使用虚拟地址。 当 CPU 遇到涉及从系统内存读取或写入的指令时,它会将该指令中编码的虚拟地址转换为内存控制器可以理解的物理地址。 此转换过程由内存管理单元 (MMU)(CPU 的硬件组件)处理,并且对应用程序完全透明。

虚拟内存提供的抽象也扩展到了应用程序可用的内存的表面大小。通过使用辅助存储(通常是硬盘或 SSD 上称为交换空间的专用部分),虚拟内存可以创建系统拥有比实际安装的更多的内存的错觉。 当物理 RAM 完全被使用时,内核可以将不经常访问的内存页从 RAM 移动到交换空间,从而为活动进程释放物理内存。 虽然这种机制允许系统处理更多并发运行的进程或内存密集型任务,但它引入了显著的性能折衷,因为访问辅助存储上的数据比访问 RAM 中的数据要慢得多。 高交换空间使用率通常是系统正在经历内存压力的明显迹象。 虚拟内存的核心优势在于它能够抽象底层物理硬件,为应用程序提供简化且一致的内存视图,这对于可移植性和易于开发至关重要。交换空间的使用虽然扩展了表面上的内存容量,但由于辅助存储的访问速度比 RAM 慢得多,因此会带来显著的性能损失。严重依赖交换空间表明可能存在内存限制。

地址空间

内核提供的虚拟地址空间通常分为两个不同的区域:内核空间和用户空间。 内核空间专供 Linux 内核及其设备驱动程序使用,包含内核代码、数据结构和其他内核相关信息。 另一方面,用户空间是用户级应用程序执行的虚拟地址空间部分。 这种分离不仅是逻辑上的,而且由 CPU 的特权级别强制执行,确保用户空间程序无法直接访问或操作内核内存。 这种严格的隔离是操作系统的一项基本安全特性,可防止未经授权访问关键系统组件。

在 Linux 系统上运行的每个独立进程都拥有自己的私有虚拟地址空间,与其他进程隔离。 这意味着,即使两个不同的进程恰好使用相同的虚拟内存地址,这些地址也会对应于物理内存中不同的位置,或者可能在交换空间中。 这种隔离对于系统稳定性和安全性至关重要,因为它防止了错误的或恶意的应用程序干扰其他进程的内存。 然而,在单个进程中,可以存在多个执行线程,这些线程共享相同的虚拟地址空间。 这种共享使得同一应用程序中的线程之间能够进行高效的通信和数据交换。 虚拟地址空间划分为内核空间和用户空间,并由硬件特权级别强制执行,是 Linux 中一项基本的安全机制,可防止用户级代码直接访问或破坏关键内核数据。进程内线程的共享地址空间允许高效的数据共享和通信,这是多线程的一个关键优势,但也需要仔细的同步以防止数据损坏。

虚拟内存详解

虚拟内存依赖于几种关键技术才能有效地工作,包括按需分页、交换和内存映射。这些机制协同工作,为每个进程提供一个大型、连续且私有的内存空间的假象。

按需分页和交换

按需分页是包括 Linux 在内的现代操作系统的基石。它基于应用程序很少一次性使用所有代码和数据的原则。 内核不会在程序启动时将整个可执行文件加载到物理内存中,而是仅加载立即需要的特定页面。 这种“即时”加载显著减少了进程的初始内存占用,从而加快了启动速度并提高了 RAM 的利用效率。当进程尝试访问与当前不在物理内存中的页面对应的内存位置时,会发生页面错误。 MMU 检测到此无效访问并生成异常,然后由内核处理。 收到页面错误后,内核确定所需页面的位置,该位置可能在磁盘上(在原始可执行文件中或在交换空间中)。然后,内核启动一个过程,将此页面带入物理内存中的空闲帧。

交换空间充当 RAM 的扩展,位于系统辅助存储器的一部分上。 当物理内存已满,并且内核需要为新页面腾出空间时,它可以将较少使用的页面从 RAM 移动到交换空间。 此过程称为换出。 当进程稍后尝试访问已换出的页面时,会发生另一个页面错误。然后,内核从交换空间检索所需的页面并将其带回物理内存,此过程称为换入。 为了决定在内存不足时应换出哪些页面,内核采用各种页面替换算法。 常见的算法包括最近最少使用 (LRU),它会驱逐最长时间未被访问的页面,最近最少频率使用 (LFU) 和时钟算法。 Linux 维护活动和非活动 LRU 列表以帮助跟踪页面使用情况。 交换的积极程度可以通过 vm.swappiness 内核参数来控制。 此参数的较低值会鼓励内核尽可能长时间地将页面保留在 RAM 中,而较高的值会使内核更愿意交换以释放 RAM。 按需分页通过仅将应用程序的必要部分加载到 RAM 中来优化系统效率。交换允许系统处理比物理 RAM 所能容纳的更多的进程,但过度的交换会严重降低性能。

内存映射

内存映射,由 mmap 系统调用促成,为进程提供了一种强大而有效的方式,将文件或匿名内存区域直接映射到其虚拟地址空间中。 此技术消除了对文件 I/O 进行显式 readwrite 系统调用的需求,因为进程只需从映射的内存区域读取或写入即可访问文件的内容。 当进程映射文件时,内核会在进程虚拟地址空间的一部分和磁盘上的文件之间创建映射。 这可以针对常规文件和特殊文件完成。内存映射可以是文件支持的,也可以是匿名的。 文件支持的映射直接与文件系统上的文件关联,允许进程将文件的数据视为直接位于内存中。 内核在访问时处理将文件页加载到物理内存中,并且可能会根据需要将修改后的页面写回磁盘。 另一方面,匿名映射不与任何文件关联,用于分配进程私有内存。 这通常用于大型数据缓冲区或其他不需要持久化到磁盘的内存区域。

内存映射文件会增加进程的虚拟内存使用量,这反映在 top 等工具的 VIRT 列中。 即使只实际访问了大型文件的一小部分,整个映射的文件大小也会计入虚拟内存占用。 使用 fork 系统调用创建新进程时,内核会结合使用内存优化技术,称为写时复制 (COW) 和内存映射。最初,父进程和子进程共享映射区域的相同内存页。只有当其中一个进程尝试写入共享页时,才会为修改进程创建该页的新私有副本。这会将复制内存的开销延迟到绝对必要时,从而节省内存和时间,尤其是在子进程不修改共享内存区域的情况下。内存映射为处理文件 I/O 提供了一种有效的方式,特别是对于大型文件,它允许直接在内存中访问文件内容。与 mmap 关联的写时复制机制通过延迟内存页的复制直到发生写入操作来优化进程 fork 期间的资源使用。

物理内存组织

Linux 内核采用结构化的方法来组织和管理系统的物理 RAM。这种组织对于满足内核和用户空间进程的不同需求以及不同硬件组件的不同功能至关重要。

内存区域

为了有效地管理物理内存,Linux 内核将其划分为称为区域的不同块。 这些区域代表具有特定特征的连续物理内存范围,主要由访问内存方式的架构约束定义。 不同的系统架构和配置可能具有不同的活动内存区域集。Linux 中的一些关键内存区域包括 ZONE_DMAZONE_DMA32ZONE_NORMALZONE_HIGHMEMZONE_MOVABLEZONE_DEVICEZONE_DMAZONE_DMA32 历史上代表了适用于直接内存访问 (DMA) 的内存,外围设备对它们可以访问的物理地址范围有限制。ZONE_DMA 通常覆盖较低的 16MB 物理内存,而 ZONE_DMA32 在 64 位系统上覆盖 4GB 以下的内存。虽然现在存在更现代和健壮的接口来处理 DMA 需求,但这些区域仍然用于划分具有特定访问限制的内存。

ZONE_NORMAL 包含内核可以直接且一致地访问的“正常”内存。 如果 DMA 设备支持传输到整个可寻址内存空间,则可以在此区域中的页面上执行 DMA 操作。 此区域始终在内核中启用,并且由于其可访问性和性能特性,对于许多核心内核操作至关重要。ZONE_HIGHMEM 包含未永久映射到内核地址空间的物理内存部分。 内核只能通过创建临时映射来访问此区域中的内存。 此区域主要存在于某些 32 位架构上,并通过特定的内核配置选项启用。 它允许 32 位系统访问超过 4GB 的物理 RAM,尽管这种访问由于需要临时映射而带来一些性能开销。ZONE_MOVABLEZONE_NORMAL 类似,因为它包含通常可访问的内存。 然而,关键的区别在于,ZONE_MOVABLE 中大多数页面的内容可以重新定位到不同的物理内存位置,而无需更改其虚拟地址。 这对于内存热插拔等功能至关重要,通过这些功能可以动态地向系统添加或从中删除内存。 最后,ZONE_DEVICE 代表驻留在设备上的内存,例如持久内存 (PMEM) 和图形处理单元 (GPU)。 与常规 RAM 区域相比,这种类型的内存具有不同的特性,并且此区域为设备驱动程序管理此类内存提供了必要的框架。 将物理内存组织成区域使内核能够根据不同硬件组件的特定需求和限制来管理内存,从而确保兼容性和正常运行。ZONE_HIGHMEM 是解决 32 位系统寻址限制的方案,允许通过临时映射访问超过 4GB 的 RAM。

NUMA 架构

在现代多核和多插槽系统中,通常采用一种称为非均匀内存访问 (NUMA) 的内存架构。 在 NUMA 系统中,内存被排列成库或节点,并且访问特定内存位置的成本(以延迟衡量)可能因发出请求的处理器之间的“距离”而异。 处理器可以更快地访问其本地节点上的内存,而不是位于远程节点上的内存。 为了利用这种局部性,Linux 内核默认情况下在分配内存时采用节点本地分配策略。 这意味着当进程请求内存时,内核会尝试从最靠近当前运行该进程的 CPU 的内存节点分配内存。 这样做的理由是,进程往往在同一 CPU 上运行很长时间,因此本地分配内存可能会导致更快的访问速度。 虽然此默认策略旨在优化性能,但用户也可以通过内核文档中描述的机制来控制 NUMA 系统上的内存分配策略。 在 NUMA 系统中,优化内存分配以最大限度地减少跨节点访问对于实现最佳性能至关重要,因为访问本地内存比访问远程节点上的内存要快得多。内核的 NUMA 感知内存管理默认旨在实现这种局部性。

页表:转换机制

页表是 Linux 内核用于实现虚拟内存的基本数据结构。 它们的主要作用是将 CPU 可见并由进程使用的虚拟地址转换为对应于 RAM 中实际位置的物理地址。 此转换过程是每个内存访问的关键步骤,并由 MMU 在页表的帮助下进行管理。

多级页表层次结构

由于现代计算系统(尤其是 64 位架构的系统)支持庞大的虚拟地址空间,因此使用单级页表会非常低效,并且会消耗过多的内存。 为了解决这个问题,Linux 与大多数现代操作系统一样,采用了分层的多级页表方法。 这种层次结构将页表分解为更小、更易于管理的部分,从而提高了效率并降低了整体内存消耗。 此层次结构中的级别数随着时间的推移而演变,并且可能因架构和内核配置而异,系统使用两级、三级、四级甚至五级页表。 例如,现代 64 位 Linux 系统通常使用四级页表。

四级系统的页表层次结构中的典型级别是:页全局目录 (PGD)、页上级目录 (PUD)、页中间目录 (PMD) 和页表项 (PTE)。 在五级系统中,在 PUD 之上引入了一个额外的级别,即页级别 4 目录 (P4D)。 页表层次结构中的每个级别本质上都是一个指针数组,指向层次结构中的下一级。 顶层页表 PGD 包含指向 P4D 表(在五级系统中)或 PUD 表(在四级系统中)的指针,依此类推,直到到达最低级别 PTE。 PTE 包含实际内存页的物理地址。 当 CPU 需要转换虚拟地址时,它会将地址分解为几个部分。虚拟地址的高位用作 PGD 的索引,以查找下一级表的条目。 然后,虚拟地址的后续位用作层次结构中每个级别的表的索引,直到到达 PTE。 虚拟地址的低位表示物理页本身内的偏移量。 对于内核自身的地址空间,主页表通常称为 swapper_pg_dir。 每个用户空间进程也有自己的一组页表,并且进程的 PGD 指针存储在进程的 struct mm_structpgd 字段中。 多级页表结构通过仅为实际使用的内存分配页表条目来有效地管理大型虚拟地址空间。页表层次结构中的级别数可能因架构和内核配置而异。

页表条目

在页表层次结构的底部是页表条目 (PTE)。 每个 PTE 包含关于单个虚拟页及其对应的物理页的关键信息。 此信息包括物理页帧号 (PFN),用于标识内存中的物理页。 PTE 还存储访问控制位,这些位定义了页面的权限,例如是否可以读取、写入或执行。 存在位指示虚拟页当前是否位于物理 RAM 中,或者是否已交换到磁盘。 当页面自加载到 RAM 后被修改时,会设置一个脏位,表明在驱逐该页面之前需要将其写回磁盘。 此外,页面替换算法通常使用一个引用位来跟踪页面最近被访问的频率。 PTE 是页表的基本单元,包含将虚拟页映射到物理页以及控制对该页的访问所需的所有必要信息。PTE 中的位对于内存保护和管理至关重要。

转换后备缓冲器 (TLB)

对于每次内存访问都遍历多级页表将是一项耗时的操作,可能会导致显著的性能开销。 为了缓解这个问题,现代 CPU 集成了一个称为转换后备缓冲器 (TLB) 的硬件缓存。 TLB 存储最近的虚拟到物理地址转换。 当 CPU 尝试访问内存位置时,它首先检查 TLB 中是否已存在相应虚拟地址的转换。 如果在 TLB 中找到转换(TLB 命中),则可以快速检索物理地址,并且内存访问可以继续进行,而无需遍历页表。 这显著加快了对常用转换的内存访问速度。 然而,如果在 TLB 中未找到转换(TLB 未命中),则 MMU 必须执行完整的页表遍历以确定物理地址。 找到物理地址后,该转换通常会存储在 TLB 中以供将来使用。 TLB 对于内存性能至关重要。通过缓存常用的转换,它显著减少了虚拟地址转换的开销,从而加快了内存访问和整体系统性能。

内核内存分配

Linux 内核自身也需要内存来执行其操作,包括管理进程、处理 I/O 和维护其内部数据结构。为了满足这些需求,内核采用了专门的内存分配机制,这些机制与用户空间中使用的机制不同。 内核使用的两种主要分配器是伙伴系统分配器和 slab 分配器。 此外,内核还提供了像 kmallocvmalloc 这样的函数,它们利用了这些底层分配器。

伙伴系统分配器

伙伴系统分配器是内核内存管理的基本组件,负责为内核分配连续的物理内存块,称为页帧。 它基于将内存划分为大小为 2 的幂的分区(称为阶)的原则运行。 可用于分配的整个内存空间最初被视为一个最高阶的单个块。 当内核请求特定大小的块时,伙伴系统首先检查是否存在该确切大小(阶)的空闲块。 如果没有,它会递归地将下一个更高阶的较大空闲块拆分为两个大小相等的“伙伴”块。 此过程一直持续到获得请求大小的块为止。 然后分配其中一个伙伴块以满足请求,而另一个伙伴块保持空闲。

当释放一个内存块时,伙伴系统会检查其“伙伴”块是否也空闲。 如果两个块大小相同、在内存中相邻并且最初是通过拆分一个更大的块形成的,则它们被认为是伙伴块。 如果两个伙伴块都空闲,则它们会合并回一个更大的空闲块,其阶数比原来的高一级。 只要相邻的伙伴块空闲,此合并过程就会在阶层结构中递归地向上继续。 伙伴分配器维护着各种阶的内存块表,从而简化了分配和释放过程。 关于空闲物理内存区域的信息存储在 free_area 结构中。/proc/buddyinfo 文件提供了伙伴分配器监管下的内存状态快照,显示了不同大小(阶)的块中可用页面的数量。

伙伴系统的一个潜在缺点是外部碎片,即空闲内存的总量可能足以满足请求,但它分散在许多小的、不连续的块中。 虽然伙伴系统通过其 2 的幂分配和合并机制尝试最大限度地减少这种情况,但它并不能完全消除它。 为了进一步缓解碎片问题,Linux 伙伴分配器引入了内存迁移类型的概念,例如不可移动、可回收和可移动页面。 这允许内核根据页面是否可以重新定位到内存中的其他位置来对页面进行分组,这有助于在需要时创建更大的连续空闲块。 伙伴系统的 2 的幂分配策略简化了内存管理,并通过高效的伙伴地址计算实现了快速的内存释放。外部碎片仍然是一个潜在问题,Linux 尝试通过内存迁移类型来解决这个问题。

阶 (n)

大小 (基于 PAGE_SIZE)

0

PAGE_SIZE

1

2 * PAGE_SIZE

2

4 * PAGE_SIZE

...

...

MAX_ORDER-1

2<sup>MAX_ORDER-1</sup> * PAGE_SIZE

导出到 Google 表格

Slab 分配器

Slab 分配器构建在伙伴系统之上,旨在高效地分配小型、常用的内核对象。 许多内核操作涉及重复分配和释放相同类型的小数据结构,例如进程描述符、文件对象和 inode 结构。 Slab 分配器旨在通过维护这些常用对象的缓存(处于初始化状态,随时可用)来优化此过程。 这避免了重复从伙伴系统分配内存然后从头开始初始化对象的开销。

Slab 分配器将内存组织成 slab 缓存,每个缓存都专用于存储特定类型或大小的对象。 每个缓存由一个或多个 slab 组成,这些 slab 是从伙伴分配器获得的连续物理内存块(通常是一个或多个页面)。 每个 slab 进一步划分为许多固定大小的对象,这些对象是缓存管理的类型。 Slab 可以处于三种状态之一:空闲(所有对象都空闲)、部分使用(一些对象已使用,一些对象空闲)或已满(所有对象都在使用中)。 当内核需要分配特定类型的对象时,slab 分配器首先检查相应缓存的部分使用 slab 中是否有可用的空闲对象。 如果找到空闲对象,则立即分配。如果在部分使用 slab 中没有可用的空闲对象,则分配器可能会从伙伴系统分配一个新的 slab,用对象填充它,然后分配一个,或者它可能会在可能的情况下从已满的 slab 中回收对象。 当释放一个对象时,它会被返回到分配它的 slab 并标记为空闲,通常保持初始化状态以供将来使用。

Linux 中有几种 slab 分配器的实现,包括 SLOB(简单块列表)、SLAB 和 SLUB(无队列位图)。 由于其更好的性能和更低的开销,SLUB 是大多数现代 Linux 发行版中的默认 slab 分配器。 Slab 分配器通过减少碎片、缓存已初始化的对象以及通过 slab 着色等技术提高硬件缓存利用率来提高性能。 Slab 着色涉及将 slab 内的对象对齐到不同的偏移量,以增加它们驻留在不同 CPU 缓存行的可能性,从而减少缓存争用。 Slab 分配器通过减少碎片并加快通过缓存进行的分配和释放来优化常用内核对象的内存管理。从 SLOB 到 SLAB 再到 SLUB 的演变反映了不断努力提高内核内存分配效率的过程。

kmallocvmalloc

Linux 内核提供了两个主要函数用于在其自身的地址空间中分配内存:kmallocvmallockmalloc 通常用于分配物理上连续的小到中等大小的内存块。kmalloc 分配的内存驻留在内核的地址空间中,通常位于 ZONE_NORMALZONE_DMA 中。 由于内存是物理上连续的,因此适用于像 DMA 这样的操作,硬件设备需要直接访问连续的物理内存块。kmalloc 通常使用 slab 分配器来分配小于页面大小的内存。

另一方面,vmalloc 用于分配较大的内存区域,这些区域在内核的地址空间中是虚拟连续的,但在物理上可能分散在不同的非连续物理内存页中。vmalloc 分配的内存不驻留在 lowmem 区域中,而是驻留在内核地址空间的专用区域中,有时在内核虚拟地址的上下文中称为 highmem。 当内核需要一个大的、连续的地址范围用于其数据结构时,即使底层的物理内存是碎片化的,vmalloc 也特别有用。 然而,访问 vmalloc 分配的内存可能比访问 kmalloc 分配的内存慢,因为它可能涉及额外的开销,这是由于底层物理页面的非连续性以及潜在的 TLB 失效。 一般来说,kmalloc 是大多数内核分配的首选,尤其是小于页面的分配,或者当需要物理连续性时,例如对于 DMA 缓冲区。vmalloc 通常保留用于较大的分配,在这种情况下,虚拟连续性就足够了,而物理连续性不是必需的。kmallocvmalloc 之间的选择取决于内核子系统或驱动程序的具体要求,需要在物理连续性和性能考虑之间进行权衡。

用户空间内存分配

Linux 中的用户空间程序采用不同的机制来为其数据和代码分配内存。主要方法包括使用 malloc 等函数在堆上管理内存,以及使用 mmap 系统调用直接映射内存区域。

堆管理:mallocbrksbrk

大多数用户空间应用程序依赖于标准 C 库函数 malloc(及其相关函数,如 callocreallocfree)来在堆上动态分配内存。 堆是进程虚拟地址空间中用于程序运行时动态内存分配的区域。malloc 库代表应用程序管理此堆区域。 在内部,malloc 通常使用内核提供的较低级别的系统调用来根据需要扩展或缩小堆的大小。 用于此目的的两个主要系统调用是 brksbrkbrk 系统调用将进程数据段(包括堆)的末尾设置为虚拟地址空间中的指定地址。sbrk 系统调用按给定的增量增加(或减少)数据段的大小。 当应用程序调用 malloc 请求一定量的内存时,malloc 库首先尝试从其自身在当前分配的堆区域中的空闲块池中满足请求。 如果没有足够大的空闲块可用,malloc 可能会使用 brksbrk 系统调用从内核请求更多内存,从而扩展堆。 然后,malloc 库管理这个新获得的内存,根据需要将其划分为更小的块以满足后续的分配请求。当不再需要内存时,应用程序调用 free 将其返回给 malloc 库的空闲块池,使其可用于将来的分配。用户空间内存分配使用 malloc 通常是在 brksbrk 等较低级别系统调用的基础上实现的。malloc 库管理内核分配给进程的堆区域内的内存。

内存映射:mmap 系统调用的详细解释,匿名映射与文件支持映射

如前所述,mmap 系统调用是一个多功能的工具,允许用户空间进程将文件或匿名内存区域映射到其虚拟地址空间中。 对于用户空间应用程序,mmap 可用于创建匿名映射,以便直接从内核分配大型连续内存区域,而无需管理堆的开销;也可用于创建文件支持的映射,以便高效地访问文件数据。 使用 mmap 创建匿名映射时,进程本质上是请求内核分配一个由物理内存(以及可能的交换空间)支持但未与磁盘上的任何文件关联的虚拟内存区域。 这通常被需要大型缓冲区用于数据处理或其他目的的应用程序使用。内核在进程访问时使用按需分页来处理底层物理页面的分配。

文件支持的 mmap 允许进程将文件(或文件的一部分)映射到其虚拟地址空间中。 一旦文件被映射,进程就可以通过简单地读取或写入相应的内存地址来访问其内容。 内核负责在访问时将必要页面从文件加载到物理内存中(按需分页),并且在取消映射或系统需要回收内存时,可能会将任何修改过的页面写回磁盘。 这种方法通常比使用传统的 readwrite 系统调用效率更高,特别是对于大型文件,因为它减少了系统调用的次数,并允许内核更有效地管理文件数据的缓存。 此外,mmap 可用于创建多个进程可以访问的共享内存区域,从而促进进程间通信。 通过在调用 mmap 时指定适当的标志,进程可以创建共享或私有的映射,并具有特定的访问权限(只读、读写、执行)。mmap 系统调用为用户空间内存管理提供了一种灵活而强大的机制,能够实现高效的内存分配、文件访问和进程间通信。

关键内核数据结构

Linux 内核依赖于几个关键的数据结构来有效地管理内存。这些结构存储了关于系统物理内存、进程的虚拟地址空间以及它们之间的映射的关键信息。 其中最重要的包括 struct pagestruct vm_area_structstruct mm_struct

struct page

struct page 是 Linux 内核中的一个基本数据结构,系统的每个物理页帧都对应一个实例。 此结构包含关于相应物理页面的大量信息,包括其当前状态、使用情况以及任何到虚拟地址的映射。struct page 中的一些关键字段包括 flags,它存储页面的当前状态,例如是否被锁定、脏(已修改)、为内核保留或当前已分配给 slab 分配器。_count 字段(也可以通过 page_count 函数访问)维护页面的引用计数,跟踪对其存在的引用数量。 当此计数达到零时,该页面被认为是空闲的,可以重新分配。_mapcount 字段指示当前为此物理页面存在的虚拟映射数量。 如果该页面是页缓存(用于缓存内存中的文件数据)的一部分,则 mapping 字段指向与该文件关联的 address_space 对象。index 字段存储页面在映射对象(例如,文件)中的偏移量。 对于作为 LRU(最近最少使用)列表的一部分进行管理的页面以进行页面替换,lru 字段是一个链表头,将页面链接到这些列表中。virtual 字段(如果不是 NULL)保存页面的内核虚拟地址,但这仅对驻留在内核地址空间的 lowmem 区域中的页面有效。 对于用于表示更大的连续内存块(如巨页)的复合页面,使用 compound_headcompound_dtorcompound_ordercompound_mapcountcompound_nr 等字段。 当页面由 slab 分配器分配时,struct page 中的 freelistinuseobjects 等字段用于管理 slab 中的对象。 物理页帧号 (PFN) 与其对应的 struct page 之间存在一对一的映射,内核提供了 pfn_to_pagepage_to_pfn 等函数来在这两种表示形式之间进行转换。 内核采用不同的内存模型,例如 FLATMEM 和 SPARSEMEM,来根据系统的内存架构组织 struct page 实例。struct page 是内核跟踪物理内存状态和使用情况的基本方式。

字段

描述

flags

页面的状态(锁定、脏、保留等)

_count

页面的引用计数

mapping

如果页面在页缓存中,则指向 address_space

index

在映射对象中的偏移量

lru

用于管理 LRU 列表中的页面

virtual

页面的虚拟地址(对于低端内存)

freelist

指向 slab 中第一个空闲对象的指针(如果由 slab 分配器分配)

inuse

slab 中当前正在使用的对象数量

objects

slab 中的对象总数

导出到 Google 表格

struct vm_area_struct

struct vm_area_struct (VMA) 表示进程地址空间内一个连续的虚拟内存区域。 每个 VMA 都有一个定义的起始地址 (vm_start) 和结束地址 (vm_end),指定了它覆盖的虚拟内存范围。 一个关键字段是 vm_mm,它指向此 VMA 所属的 struct mm_struct,将内存区域链接到进程的整体内存描述符。 进程内的 VMA 组织成一个链表,vm_nextvm_prev 指针用于将它们链接在一起,按其起始虚拟地址排序。 它们也存储在红黑树中,vm_rb 字段充当此树中的节点,允许基于地址高效地搜索 VMA。vm_page_prot 字段定义了内存区域的访问权限,例如读取、写入和执行。vm_flags 字段包含一组标志,描述了 VMA 的属性和行为,包括它是共享的还是私有的、可执行的、可增长的还是与文件相关的。 如果 VMA 由文件支持(例如,通过 mmap),则 vm_file 字段指向相应的文件结构,而 vm_pgoff 字段存储文件中的偏移量。vm_ops 字段指向与此 VMA 关联的一组操作(方法),例如处理此区域内页面错误的函数。 VMA 由 mmap 等系统调用创建。 可以通过检查 /proc/<pid>/maps 文件来检查进程的内存区域(由 VMA 表示)。struct vm_area_struct 提供了一种抽象,用于管理进程内具有特定属性的连续虚拟内存区域。

字段

描述

vm_start

内存区域的起始虚拟地址

vm_end

内存区域的结束虚拟地址

vm_mm

指向此区域所属的 mm_struct

vm_next

链表中的下一个 VMA

vm_prev

链表中的上一个 VMA

vm_page_prot

内存区域的访问权限

vm_flags

描述内存区域属性的标志(例如,读、写、共享)

vm_file

指向文件的指针(如果该区域映射到文件)

vm_pgoff

文件中的偏移量

vm_ops

与此内存区域关联的操作

导出到 Google 表格

struct mm_struct

struct mm_struct,也称为内存描述符,表示进程(或共享地址空间的一组线程)的整个虚拟地址空间。 它充当从内核角度管理进程内存的中心数据结构。mmap 字段指向属于此地址空间的 vm_area_struct 实例链表的头部,按其起始虚拟地址排序。 类似地,mm_rb 字段是包含此进程所有 VMA 的红黑树的根,允许高效搜索。 为了加快 VMA 查找速度,mmap_cache 字段指向最近访问的 VMA。pgd 字段是指向页全局目录的关键指针,页全局目录是进程的顶级页表,能够将此地址空间中的虚拟地址转换为物理地址。mm_users 字段跟踪当前正在使用此地址空间的任务(进程或线程)的数量。mm_count 字段是 mm_struct 本身的主要引用计数。 其他重要字段包括 total_vm,它存储任务映射的总页数,以及 map_count,它指示任务使用的 VMA 数量。start_stack 字段记录进程堆栈段的起始地址。 每个进程通常都有自己唯一的 mm_struct,但同一进程中的线程除外,它们共享一个 mm_struct。 没有用户空间上下文的内核线程,其 task_struct 中的 mm 字段设置为 NULL。mm_struct 通过进程的 task_struct 中的 mm 字段与进程关联。struct mm_struct 是进程内存管理的核心控制块,保存着指向所有 VMA、页表和记账信息的指针。

字段

描述

mmap

VMA 链表的头部

mm_rb

VMA 红黑树的根

pgd

指向页全局目录的指针

mm_users

使用此地址空间的进程/线程数

mm_count

mm_struct

的引用计数

total_vm

任务映射的总页数

map_count

虚拟内存区域 (VMA) 的数量

start_stack

进程堆栈的起始地址

导出到 Google 表格

struct pagestruct vm_area_structstruct mm_struct 之间的关系

这三个数据结构,struct pagestruct vm_area_structstruct mm_struct,形成了一个分层关系,这是 Linux 内存管理的基础。struct mm_struct 提供了进程整个虚拟地址空间的总体描述。 在此虚拟地址空间内,struct vm_area_struct 定义了连续的虚拟内存区域,每个区域都有自己的一组属性,例如权限和标志。 这些虚拟内存区域 (VMA) 中的每一个最终都由物理内存页支持,而系统中的每个物理页都由一个 struct page 表示。 VMA 中的虚拟地址与实际物理 struct page 实例之间的关键链接由页表提供。struct mm_struct 中的 pgd 字段指向进程的顶级页表,而这些页表中的条目将虚拟地址映射到相应的物理页帧号,而物理页帧号又与 struct page 实例关联。 因此,mm_struct 提供上下文,vm_area_struct 定义该上下文中的逻辑区域,而 struct page 跟踪支持这些区域的底层物理资源,页表则充当转换机制。

Linux 内存回收

当对内存的需求超过可用的物理 RAM 时,Linux 内核会采用各种机制来回收内存。这些机制旨在释放当前未被主动使用的内存,或者可以轻松地从其他来源(例如磁盘)检索的内存。 内存回收的主要技术包括页面替换算法、交换和分页以及内存压缩。

页面替换算法

如前所述,当发生页面错误且没有可用的空闲物理内存时,内核需要从 RAM 中驱逐一个页面,以便为新页面腾出空间。 驱逐哪个页面的选择由页面替换算法决定。 常用的算法有几种,包括最近最少使用 (LRU),它驱逐最近最长时间未被访问的页面 ;最近最少频率使用 (LFU),它驱逐被访问次数最少的页面;时钟算法,它是 LRU 的一个更简单的近似;以及最近未使用 (NRU),它根据页面的访问位和修改位对页面进行分类。 Linux 主要使用 LRU 算法的变体,为匿名内存和页面缓存维护活动和非活动列表。 活动列表包含最近被访问的页面,而非活动列表包含适合驱逐的页面。 内核根据页面的使用模式定期在这些列表之间移动页面。 有效的页面替换算法对于最大限度地减少交换的性能影响至关重要,它试图驱逐近期最不可能需要的页面。

交换和分页

交换和分页是在物理 RAM 和辅助存储(交换空间)之间移动页面的过程。 如前所述,当内核将不活动的页面从 RAM 移动到交换空间以释放物理内存时,会发生换出。 这通常发生在系统内存压力过大时,即可用 RAM 量较低时。 当进程尝试访问已换出的页面时,会发生换入,内核必须将其从交换空间带回 RAM。 虽然交换允许系统在物理内存耗尽时继续运行,但由于磁盘 I/O 的速度远低于 RAM,因此会带来显著的性能损失。 过度的交换,通常称为抖动,会导致系统花费更多的时间在 RAM 和磁盘之间移动页面,而不是实际执行应用程序,从而导致非常差的性能。 如果系统达到内存严重不足的状态,并且交换不足以缓解压力,内核可能会调用内存不足 (OOM) 杀手。 OOM 杀手会选择并终止一个或多个进程以释放内存并防止系统崩溃。 交换由低可用内存触发,并可能由于磁盘 I/O 导致性能瓶颈。OOM 杀手是内存严重不足时终止进程的最后手段。

内存压缩

内存压缩是内核用于减少内存碎片的过程。 随着时间的推移,当内存以各种大小进行分配和释放时,空闲物理内存可能会碎片化为许多小的、不连续的块。 这使得内核难以分配大的连续内存块,即使空闲内存的总量足够。 内存压缩试图通过移动物理内存中的可移动页面来解决这个问题,以便将空闲内存整合为更大的连续块。 这对于依赖于分配连续内存块的伙伴系统分配器尤其重要。 内核识别可以移动的页面(例如,属于 ZONE_MOVABLE 的页面),并将它们重新定位到不同的物理地址,更新相应的页表条目以反映新的位置。 此过程可能耗时,并且在运行时可能会影响性能,但对于确保内核能够长期满足大型内存分配请求至关重要。 内存压缩有助于缓解外部碎片,确保内核可以在需要时分配大的连续内存块。

内存相关的系统调用

Linux 内核提供了一组系统调用,允许用户空间程序与内存管理子系统进行交互。这些系统调用使进程能够分配内存、将文件映射到其地址空间、控制访问权限以及向内核提供关于其内存使用模式的提示。

mmap

mmap 系统调用是一个用于创建内存映射的多功能接口。 它可用于将文件或设备映射到进程的虚拟地址空间中,允许像直接在内存中一样访问文件内容。mmap 也可用于创建匿名映射,这些映射提供不被任何文件支持的大内存区域。 进程可以指定映射的起始地址、长度、所需的访问权限(读、写、执行)以及映射应该是与其他进程共享还是私有的。

brksbrk

brksbrk 系统调用用于调整进程堆的大小,堆是用于通过 malloc 进行动态分配的内存区域。brk 将数据段末尾的地址设置为指定值,而 sbrk 按给定的增量增加(或减少)数据段的大小。 这些调用通常由 malloc 等内存分配库在内部使用。

其他相关系统调用

其他几个系统调用提供了对内存管理的额外控制。munmap 用于取消映射先前映射的内存区域,释放相应的虚拟地址空间。mprotect 允许进程更改其虚拟地址空间区域的访问权限,例如,使只读区域可写,反之亦然。madvise 使进程能够向内核提供关于其打算如何使用某些内存区域的建议,例如,它将按顺序还是随机访问,这可以帮助内核做出更明智的关于缓存和预取的决策。mlockmunlock 允许进程将内存页锁定或解锁在 RAM 中,防止它们被交换到磁盘。 这对于性能关键型应用程序非常有用,因为在这些应用程序中,交换的延迟是不可接受的。这些系统调用为用户空间应用程序提供了管理其内存使用并与内核的内存管理工具交互所需的工具。

监控 Linux 内存使用情况

Linux 提供了各种工具和命令,允许用户和管理员监控系统的内存使用情况并识别潜在问题。 这些工具提供了不同级别的详细信息和关于内核和用户空间进程如何利用内存资源的视角。

free

free 命令提供系统内存使用情况的快速概览,显示物理内存和交换内存的总量,以及当前已使用和空闲的量。 它还显示了用于缓冲区和缓存的内存量。free 输出中的关键指标包括 MemTotal(可用物理内存总量)、MemFree(完全空闲、未使用的 RAM 量)和 MemAvailable(估计可用于启动新应用程序的 RAM 量,考虑到可以释放的缓存)。SwapTotalSwapFree 值显示交换空间的总大小及其当前空闲量。free 命令对于获取内存使用情况的高级快照很有用。

vmstat

vmstat 命令报告关于虚拟内存的统计信息,包括关于进程、内存、分页、块 I/O、陷阱、磁盘和 CPU 活动的信息。 与内存监控相关的是 si(换入)和 so(换出)列,它们分别显示数据从磁盘换入和换出到磁盘的速率。 这些列中的高值可能表明系统正处于内存压力之下。page inpage out 列也提供了关于分页活动的见解,显示每秒分页进出页面的数量。vmstat 对于观察内存和交换使用情况随时间变化的趋势很有用。

tophtop

tophtop 命令提供系统正在运行的进程的实时交互式视图,包括关于其资源使用情况的详细信息。 几个与内存相关的列特别有用。VIRT(虚拟内存大小)显示进程使用的虚拟内存总量,包括所有代码、数据和共享库,以及可能已交换出去的内存。RES(常驻内存大小)指示进程当前正在使用的且驻留在 RAM 中的物理内存量。SHR(共享内存大小)显示进程使用的共享内存量。 这些工具允许用户识别哪些进程消耗的内存最多。

/proc/meminfo

/proc/meminfo 文件直接从内核提供了关于系统内存使用情况的大量详细的底层信息。 它包括 free 显示的所有指标,以及许多其他内核内部统计信息。 一些关键字段包括 Active(anon)Inactive(anon),它们分别显示活动和非活动匿名内存量;Active(file)Inactive(file),它们显示活动和非活动文件支持内存量;Slab,它是 slab 分配器使用的总内存;SReclaimable,它是可能被回收的 slab 内存部分;以及 SUnreclaim,它是无法回收的 slab 内存部分。 分析 /proc/meminfo 的内容可以深入了解内核如何管理内存。这些工具提供了不同级别的详细信息以及内存使用情况的实时与快照视图。

Linux 内存性能优化

优化 Linux 中的内存性能涉及多种技术,从调整内核参数到编写高效的应用程序和利用内核特性。 目标是最大化 RAM 的利用率,最小化对较慢的交换空间的依赖,并确保应用程序能够高效地访问它们所需的内存。

调整内核参数

可以调整几个内核参数来影响内存管理的行为。vm.swappiness 参数控制内核使用交换空间的积极程度。 较低的值(例如,10)使内核不太可能进行交换,而更倾向于将页面保留在 RAM 中,而较高的值(例如,60)使其更愿意进行交换。 最佳值取决于系统的工作负载和可用的 RAM 量。vm.dirty_ratiovm.dirty_background_ratio 参数控制内核何时开始将脏页(已修改但尚未写入磁盘的页面)写入磁盘。 调整这些值可能会影响 I/O 性能。

识别和解决内存泄漏

内存泄漏(即分配了内存但从未释放)可能导致过度的内存消耗,并最终降低系统性能。 识别和解决用户空间应用程序和内核模块中的内存泄漏至关重要。 Valgrind 等工具可用于检测用户空间程序中的内存泄漏。内核还具有内置的内存泄漏检测机制,可以启用以进行调试。

高效使用缓存

Linux 将所有未被运行程序使用的可用物理内存用作文件缓存。 此页面缓存和缓冲区缓存可以通过减少重复从磁盘读取数据的需求来显著提高 I/O 性能。 了解这些缓存的工作方式及其对内存使用情况的影响对于优化非常重要。例如,drop_caches 命令可用于在需要时释放缓存的内存,但应谨慎使用,因为它可能会暂时影响性能。

利用巨页

对于分配和访问大型连续内存块的应用程序,使用巨页(大于标准 4KB 或 8KB 页面的页面)可以提高性能。 巨页可以减少 TLB 未命中,并减少所需的页表条目数量,从而提高内存访问效率。 配置和使用巨页可能需要特定的应用程序级调整。

避免过度交换

由于交换会显著影响性能,因此通常希望尽量减少交换。 这可以通过确保系统具有足够的 RAM 来满足其工作负载、优化应用程序内存使用以及适当调整 vm.swappiness 参数来实现。 监控交换使用情况对于识别潜在的内存瓶颈至关重要。

内存控制组

内存控制组 (cgroups) 提供了一种限制进程组内存消耗的机制。 这对于管理容器化环境中的资源或防止单个进程组消耗所有可用内存非常有用。调整内核参数、识别和解决内存泄漏、高效使用缓存、利用巨页、避免过度交换以及使用内存控制组都是优化内存性能的重要策略。

探索 Linux 内核内存管理源代码

为了更深入地理解 Linux 如何管理内存,检查内核源代码是无价的。 内核源代码树中与内存管理相关的主要目录是 mm/。 在此目录中,有几个关键文件特别值得关注。mm/vmscan.c 包含页面替换算法的实现以及换入和换出页面的逻辑。mm/page_alloc.c 包含伙伴系统分配器的代码。 不同 slab 分配器(SLAB、SLUB、SLOB)的实现可以在 mm/slab.cmm/slub.cmm/slob.c 等文件中找到。mmap 系统调用和相关的内存映射操作的处理位于 mm/mmap.c 中。 核心内存管理功能可以在 mm/memory.c 中找到。此外,关键数据结构(如 struct pagestruct vm_area_structstruct mm_struct)的定义,以及各种内存管理相关的常量和宏,通常位于 include/linux/ 下的头文件中,例如 include/linux/mm.hinclude/linux/mm_types.hinclude/linux/page.h。 像 elixir.bootlin.com 上的内核源代码浏览器或 kernel.org 上的搜索功能可以帮助导航和查找庞大内核代码库中的特定函数和数据结构。检查内核源代码可以最详细地了解 Linux 内存管理的实现。

分析 BCC 中的内存相关工具

BCC(伯克利包过滤器编译器集合)是一个用于 Linux 系统的动态跟踪和性能分析的强大工具包。 它包含几个可用于更深入了解内存管理行为的工具。memleak 是一个 BCC 工具,它使用动态跟踪 (eBPF) 来识别用户空间应用程序和内核中的内存泄漏。 它通过跟踪内存分配和释放函数(如 malloc/freekmalloc/kfree)并识别最终未释放的分配来工作。cachestat 是另一个 BCC 工具,它提供关于 Linux 内核磁盘缓存(页面缓存和缓冲区缓存)的实时统计信息。 它报告诸如缓存命中、未命中、读取和写入等指标,允许用户了解内核如何有效地使用缓存来提高 I/O 性能。其他可能与内存分析相关的 BCC 工具包括 oomkill(跟踪与 OOM 杀手相关的事件)和 slabinfo(提供关于 slab 分配器的详细统计信息)。此外,BCC 的 eBPF 功能允许用户编写自定义脚本来跟踪特定的内存相关事件或内核函数,以进行更有针对性的调查。BCC 工具为深入的内存分析提供了强大的动态跟踪功能。

结论

Linux 内存管理是一个复杂且多方面的子系统,对于操作系统的有效和稳定运行至关重要。它涉及虚拟内存和物理内存、地址空间、页表、伙伴系统和 slab 分配器等分配机制、包括页面替换和交换在内的内存回收策略以及一组允许用户空间应用程序与这些底层机制交互的系统调用之间的复杂交互。用于监控内存使用情况的工具提供了关于系统性能和资源利用率的宝贵见解,而优化技术可以用于针对特定工作负载微调内存管理行为。对于那些寻求更深入理解的人来说,Linux 内核源代码提供了权威的参考,而像 BCC 项目中的高级跟踪工具则为动态分析提供了强大的功能。Linux 中内存的有效管理是一个持续的过程,需要全面理解这些不同的组件及其相互作用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值