当前位置: 首页 > news >正文

[Linux]学习笔记系列 -- mm/swap.c 交换机制(Swap Mechanism) 物理内存的虚拟扩展

文章目录

  • mm/swap.c 交换机制(Swap Mechanism) 物理内存的虚拟扩展
      • 历史与背景
        • 这项技术是为了解决什么特定问题而诞生的?
        • 它的发展经历了哪些重要的里程碑或版本迭代?
        • 目前该技术的社区活跃度和主流应用情况如何?
      • 核心原理与设计
        • 它的核心工作原理是什么?
        • 它的主要优势体现在哪些方面?
        • 它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
      • 使用场景
        • 在哪些具体的业务或技术场景下,它是首选解决方案?
        • 是否有不推荐使用该技术的场景?为什么?
      • 对比分析
        • 请将其 与 其他相似技术 进行详细对比。
  • mm/swap.c
    • Folio Final Destructor: 记忆页面的最后仪式
      • 实现原理分析
      • 代码分析
    • Per-CPU LRU Batching: Amortizing Lock Overhead for High-Performance Memory Management
      • 实现原理分析
      • 特定场景分析:单核、无MMU的STM32H750平台
        • 硬件交互与MMU
        • 单核环境下的行为
        • 实际意义
      • 代码分析
    • Per-CPU LRU Drain: 批量内存管理的主力
      • 实现原理分析
      • 特定场景分析:单核、无MMU的STM32H750平台
        • 硬件交互与MMU
        • 单核环境下的行为
        • 实际意义
      • 代码分析
    • 页缓存辅助函数:降级与清理
      • 函数一:`deactivate_file_folio`
        • 实现原理分析
      • 函数二:`folio_batch_remove_exceptionals`
        • 实现原理分析
      • 代码分析

在这里插入图片描述

https://github.com/wdfk-prog/linux-study

mm/swap.c 交换机制(Swap Mechanism) 物理内存的虚拟扩展

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术以及其实现的交换(Swap)机制,是为了解决一个自计算机诞生以来就存在的根本性限制:物理内存(RAM)的大小是有限的

  • 运行大于物理内存的程序:在早期,如果一个程序需要比机器拥有的RAM更多的内存,它根本无法运行。交换机制通过将内存中不常用的部分临时存放到磁盘上,从而“欺骗”程序,让它以为自己拥有一个远大于实际物理内存的地址空间。这使得运行大型应用程序成为可能。
  • 同时运行更多的程序:即使每个程序都能装入内存,但同时运行多个程序可能会迅速耗尽RAM。交换机制允许内核将处于空闲或等待状态的进程的内存换出到磁盘,从而为当前活动的进程腾出宝贵的物理内存,提高系统的并发能力和整体吞吐量。
  • 系统休眠(Hibernation):为了实现休眠到磁盘(Suspend-to-Disk)功能,内核需要一个地方来存储整个系统内存的快照。交换空间(Swap Space)被自然地用作这个存储区域。
它的发展经历了哪些重要的里程碑或版本迭代?

Linux的交换机制从早期UNIX继承了基本思想,并随着内核的演进不断完善。

  • 从交换进程到交换页面:最早期的系统采用“交换”(Swapping),即换出整个进程的内存映像。现代Linux采用的是“分页”(Paging),它以固定大小的内存页(通常是4KB)为单位进行换出换入。分页比交换更精细、更高效,因为只需移动那些当前不活跃的页面,而不是整个进程。
  • 支持交换文件(Swap Files):最初,交换空间必须是一个独立的磁盘分区(Swap Partition)。后来内核增加了对使用普通文件作为交换空间的支持。这极大地提高了灵活性,因为管理员可以在不重新分区磁盘的情况下,动态地添加、删除或调整交换空间的大小。
  • 支持多交换区与优先级:内核允许多个交换分区和交换文件同时存在,并且可以为它们指定不同的优先级。这允许管理员将交换操作优先导向更快的磁盘。
  • 与内存控制组(cgroups)集成:在容器化时代,memcg(Memory Cgroup)的出现使得交换机制可以被精细地控制在每个容器的级别,实现了容器的交换空间隔离和限制。
  • 交换机制的优化(zswap/zram):虽然不是mm/swap.c本身的一部分,但像zswap这样的技术是交换机制的重要演进。zswap在将页面写入磁盘之前,先在内存中对其进行压缩。这相当于一个压缩了的、基于RAM的写回缓存,极大地降低了实际发生慢速磁盘I/O的频率,显著提升了交换性能。
目前该技术的社区活跃度和主流应用情况如何?

交换机制是Linux内存管理中一个极其稳定和基础的部分。

  • 社区活跃度:其核心代码非常成熟。社区的开发活动主要集中在性能优化、与cgroup和NUMA等现代内核特性的集成,以及改进像zswap这样的外围支持技术。
  • 主流应用
    • 桌面和服务器:尽管现代系统拥有大量RAM,但交换空间仍然被普遍启用,作为防止系统因内存耗尽而崩溃的安全网,并支持休眠功能。
    • 低内存设备:在嵌入式系统或内存较小的云服务器上,交换机制对于系统的正常运行至关重要。
    • 内存超售(Overcommit)环境:在虚拟化和容器环境中,为了提高物理服务器的利用率,常常会进行内存超售(即分配给所有虚拟机的内存总和大于物理内存)。在这种场景下,高效的交换机制是必不可少的。

核心原理与设计

它的核心工作原理是什么?

交换机制的核心是在**物理内存(RAM)交换空间(磁盘)之间移动内存页,并通过页表(Page Table)**来对这个过程进行虚拟化。

换出(Swap-out)过程:

  1. 触发:当系统内存不足时,内核的页面回收逻辑(由kswapd后台线程或直接回收触发,主要实现在mm/vmscan.c)被激活。
  2. 选择牺牲页(Victim Selection):页面回收算法会选择一个合适的页面进行换出。它优先选择匿名页(Anonymous Page),即那些不与任何文件关联的内存,如进程的堆(heap)和栈(stack)。
  3. 分配交换槽(Swap Slot):内核在已配置的交换空间中找到一个空闲的槽位。
  4. 写入磁盘:内核将牺牲页的内容异步地写入到分配好的磁盘交换槽中。
  5. 修改页表项(PTE):一旦写入完成,内核会修改该内存页对应的进程页表项(PTE)。它会清除PTE中的物理页帧号,并将“存在位”(Present Bit)清零,然后将描述交换区位置的**交换条目(swp_entry_t)**存入PTE中。
  6. 释放物理页:最后,这个物理页面被释放,可以被系统用于其他目的。

换入(Swap-in)过程:

  1. 触发缺页异常(Page Fault):当进程试图访问一个已经被换出的内存地址时,CPU会发现其PTE的“存在位”为0,从而触发一个缺页异常。
  2. 异常处理:内核的缺页异常处理函数接管控制。它检查PTE的内容,发现里面存储的不是0,而是一个有效的交换条目。
  3. 从磁盘读取:内核根据交换条目中的信息,在交换空间中找到对应的页面数据。
  4. 分配物理页:内核分配一个新的空闲物理内存页。
  5. 读入数据:内核将数据从磁盘读入到这个新分配的物理页中。
  6. 更新页表:内核更新PTE,使其指向新的物理页,并设置“存在位”。
  7. 恢复执行:缺页异常处理完毕,进程从被中断的指令处恢复执行,此时它对内存的访问可以正常进行了。
它的主要优势体现在哪些方面?
  • 虚拟内存扩展:以较低的成本(磁盘空间远比RAM便宜)极大地扩展了系统的可用内存。
  • 提高系统稳定性:在内存压力下,通过换出不活跃页面,可以避免因内存不足而触发OOM Killer(Out-of-Memory Killer),从而保护重要进程不被杀死。
  • 支持休眠:为系统休眠到磁盘功能提供了基础。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
  • 性能悬崖(Performance Cliff):最大的劣势是性能。磁盘的访问速度比RAM慢几个数量级。一旦系统开始频繁地进行交换(称为“颠簸”或“Thrashing”),系统会把绝大部分时间花在等待I/O上,导致系统响应变得极度缓慢,几乎无法使用。
  • SSD寿命:在固态硬盘(SSD)上进行大量、持续的交换写入,理论上会消耗其写入寿命(Write Endurance)。不过,现代SSD的寿命已经大大提高,这在大多数场景下已不是主要问题。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

交换机制本身不是一个追求性能的“首选方案”,而是一个保障系统稳定运行的、必要的底层机制

  • 内存需求波动的桌面/服务器:一个Web服务器在白天负载很高,晚上负载很低。在低负载时,许多服务进程的内存可能会被换出,为文件缓存(Page Cache)腾出更多空间,从而在下一次负载高峰到来时,能更快地服务文件请求。
  • 内存受限的环境:在只有1GB内存的虚拟机或嵌入式设备上运行一个需要1.2GB内存峰值的应用,如果没有交换机制,这个应用根本无法启动。
  • 需要休眠功能的笔记本电脑:休眠功能依赖于将内存内容写入交换区。
是否有不推荐使用该技术的场景?为什么?
  • 硬实时系统:交换过程引入的延迟是不可预测且巨大的,会彻底破坏实时系统的时间确定性要求。
  • 高性能计算(HPC)/内存数据库:对于这类应用,所有数据都应常驻内存。任何交换操作都会导致灾难性的性能下降。这些应用通常会使用mlock()系统调用将自己的内存锁定在RAM中,防止被换出。

对比分析

请将其 与 其他相似技术 进行详细对比。

对比一:交换(Swapping Anonymous Pages) vs. 回写(Writing Back Dirty File Pages)

这两者都是在内存压力下将内存页写入磁盘,但目的和目标完全不同。

特性交换 (Swap)页面缓存回写 (Page Cache Write-back)
处理对象匿名内存页(Anonymous Pages),如进程的堆、栈。与文件关联的内存页(File-backed Pages),即Page Cache。
写入目标交换空间(Swap Space),一个专用的分区或文件。文件本身,在原始的文件系统中。
目的为了临时腾出物理内存,这些数据没有其他“家”。为了持久化数据,确保对文件的修改被保存。
回收逻辑当一个匿名页被换出后,其物理页可以被立即释放。当一个“脏”的文件页被写回磁盘后,它就变成了“干净”的页。如果内存紧张,这个干净的页可以被直接丢弃(因为可以从文件重新读回),而不需要写入任何地方。

对比二:传统交换 vs. zswap

特性传统交换 (Traditional Swap)zswap
工作模式直接将内存页写入到磁盘上的交换设备。充当一个写回缓存(Write-back Cache)
工作流程内存 -> 磁盘内存 -> 压缩 -> zswap内存池 -> (如果池满或页面变冷)-> 解压 -> 磁盘
性能。受限于磁盘I/O速度。快得多。大部分交换操作在RAM中完成(压缩/解压),避免了磁盘I/O。
资源消耗消耗磁盘I/O和磁盘空间。消耗CPU周期(用于压缩/解压)和部分RAM(用于存储压缩后的页面)。
关系zswap不是交换的替代品,而是其增强。它仍然需要一个底层的传统交换设备作为最终的存储。

mm/swap.c

Folio Final Destructor: 记忆页面的最后仪式

本代码片段定义了 __folio_put 函数,它是Linux内核内存管理中一个 folio(代表一个或多个连续物理页)生命周期的终点。这个函数不是简单地递减引用计数,而是在 folio 的引用计数已经降为零时,由 folio_put 宏调用的最终清理和释放函数。其核心功能是撤销 folio 分配和使用过程中附加的所有状态(如页缓存、cgroup记账等),并最终将 folio所占用的物理内存返还给伙伴系统(buddy system)分配器,使其能够被重新使用。

实现原理分析

__folio_put 的执行流程是一个精确的、逆向的资源回收过程。它必须清理掉 folio 可能关联的所有子系统,然后才能释放物理内存。

  1. 处理特殊内存类型 (Early Exits):

    • if (unlikely(folio_is_zone_device(folio))): 首先检查 folio 是否属于特殊的 “device” 内存区域,例如持久化内存(PMEM)或GPU显存。这类内存有自己独立的分配器和释放逻辑,因此会调用 free_zone_device_folio 并提前返回。
    • if (folio_test_hugetlb(folio)): 接着检查 folio 是否属于 HugeTLB 页。HugeTLB 页由一个独立的、预分配的池进行管理,有自己的释放函数 free_huge_folio
    • 对于普通内存,这两个条件都为假。
  2. 从页缓存解绑 (page_cache_release):

    • 这是一个关键步骤。当一个 folio 被用作页缓存时,它会被关联到一个 inodeaddress_space 上。page_cache_release 会执行必要的清理,例如,递减 address_space 的引用计数。这是确保当一个文件的所有页缓存都被释放后,其 address_space 也能被正确回收的关键。
  3. 清理延迟拆分 (folio_unqueue_deferred_split):

    • 为了性能,当内核需要将一个大的folio(例如16KB)拆分成小的基页(4KB)时,这个拆分操作可能会被延迟执行。这个 folio 会被放进一个“待拆分”队列。__folio_put 在这里检查该 folio 是否还在这个队列中,如果是,就将其移除,因为既然整个 folio 都要被释放了,再执行拆分就毫无意义了。
  4. Cgroup 内存记账返还 (mem_cgroup_uncharge):

    • 如果内核启用了内存控制组(cgroups),那么当一个 folio 被分配时,它的内存使用量会被“记账”(charge)到分配它的那个cgroup上。mem_cgroup_uncharge 执行相反的操作:它将这个 folio 的大小从其所属 cgroup 的内存使用量中减去。这是实现容器内存限制和隔离的基础。
  5. 返还物理内存 (free_frozen_pages):

    • 这是整个流程的最后一步,也是最根本的一步。free_frozen_pages 是伙伴系统分配器的核心API之一。
    • 它接收 folio 对应的 struct page 和 folio 的阶数(folio_order,即 log2(页面数量))。
    • 它会将这个(可能由多个连续页面组成的)物理内存块,返还到伙伴系统中对应大小的空闲链表上,使其可供系统下一次内存分配使用。

代码分析

// __folio_put: 当folio引用计数降为0时,执行的最终清理函数。
void __folio_put(struct folio *folio)
{// 检查是否为特殊的设备内存。在STM32H750上,此条件为false。if (unlikely(folio_is_zone_device(folio))) {free_zone_device_folio(folio);return;}// 检查是否为HugeTLB页。在STM32H750上,此条件为false。if (folio_test_hugetlb(folio)) {free_huge_folio(folio);return;}// 步骤1: 从页缓存子系统解绑,处理address_space等资源的引用计数。page_cache_release(folio);// 步骤2: 如果folio在延迟拆分队列中,将其移除。folio_unqueue_deferred_split(folio);// 步骤3: 从内存控制组返还内存记账。// (如果CONFIG_MEMCG未定义,此函数为空操作)。mem_cgroup_uncharge(folio);// 步骤4: 将folio代表的物理内存返还给伙伴系统分配器。// folio_order() 告诉分配器要释放的内存块的大小。free_frozen_pages(&folio->page, folio_order(folio));
}
// 导出符号,供内核其他部分(如slab分配器)调用。
EXPORT_SYMBOL(__folio_put);

Per-CPU LRU Batching: Amortizing Lock Overhead for High-Performance Memory Management

本代码片段揭示了Linux内存管理子系统中一个核心的性能优化机制:per-CPU页/folio批处理(batching)。其主要功能是避免在每次需要将一个页面(folio)添加到LRU(Least Recently Used)链表时都去竞争一个全局的、高争用的锁。取而代之的是,内核将这些LRU操作请求暂存在一个当前CPU私有的、无锁的批处理队列中。lru_add_drain_all函数的作用就是**强制处理(drain)**这些per-CPU队列,将所有暂存的页面一次性地提交到全局LRU链表中。

实现原理分析

这个机制是典型的用空间换时间、分摊锁开销的优化策略。

  1. Per-CPU数据结构 (cpu_fbatches):

    • 内核为每个CPU核心都定义了一个cpu_fbatches结构体。这意味着每个CPU都有自己独立的批处理队列(lru_add, lru_deactivate等)。
    • 优点: 当一个CPU上的代码需要将一个页面加入LRU时,它只需访问自己私有的cpu_fbatches结构。因为没有其他CPU会访问这个结构,所以几乎没有缓存争用,性能极高。
    • folio_batch: 这是一个简单的数据结构,本质上是一个固定大小的folio指针数组,用于暂存待处理的folio。
  2. 分级锁 (local_lock vs local_lock_irq):

    • local_lock_t: 这是一个轻量级锁。在多核系统上,它通过禁止抢占来保护数据。这足以防止在同一个CPU上的不同任务之间产生竞争。
    • local_lock_irq: 这是一个更强的锁,它会同时禁止抢占和本地中断。用于保护那些可能在中断上下文中也被访问的数据(如lru_move_tail)。
  3. 批处理与清空 (Batch and Drain):

    • 批处理: 当内核代码(例如,在缺页异常处理中)需要将一个新页面加入LRU时,它实际上是调用一个内部函数(如folio_add_lru,未在此处显示),该函数将folio指针添加到当前CPU的cpu_fbatches.lru_add批处理队列中,然后就快速返回了。它不直接操作全局LRU链表。
    • 清空 (Drain): lru_add_drain()函数就是“清空”操作。它会锁定当前CPU的批处理队列,然后调用一个工作函数(lru_add_drain_cpu)来处理队列中的所有folio。这个工作函数会获取一次全局的LRU链表锁,然后在一个循环中将批处理队列里的所有folio都移动到全局LRU链表中,最后释放一次全局锁。
  4. lru_add_drain_all 的角色:

    • 此函数在vm/drop_caches.c中被调用。它的作用是在执行drop_caches操作之前,确保所有CPU上所有暂存的、待加入LRU的页面都已经被处理完毕。
    • 这保证了全局LRU链表处于一个完全一致和最新的状态,drop_caches才能看到并清理掉所有符合条件的缓存页,而不会遗漏那些还“卡在”per-CPU批处理队列里的页面。

特定场景分析:单核、无MMU的STM32H750平台

硬件交互与MMU

此机制是纯粹的内核内存管理算法,与硬件或MMU无关。

单核环境下的行为

虽然per-CPU是为多核设计的,但它在单核系统上会优雅地退化:

  • DEFINE_PER_CPU: 在单核系统上,这只会定义一个普通的、非数组的cpu_fbatches变量。所有per_cpu_ptr()之类的操作都会被编译器优化为对这个单一变量的直接访问。
  • local_lock_t: 在单核可抢占内核中,local_lock()被编译为preempt_disable()local_unlock()被编译为preempt_enable()。这仍然是绝对必要的。它可以防止一个任务在修改批处理队列的过程中被另一个任务抢占,从而避免了数据损坏。
  • smp_processor_id(): 在单核系统上,此宏总是返回0。
实际意义

你可能会问,既然只有一个CPU,没有CPU间的竞争,为什么还需要这么复杂的批处理机制?
答案是,这个机制仍然能带来显著的性能优势

  • 减少锁的持有时间: 全局LRU链表通常由一个自旋锁保护。在单核系统上,获取这个自旋锁意味着禁止本地中断。如果每次添加一个页面都要“禁止中断 -> 操作链表 -> 开启中断”,当中断频繁或链表操作耗时时,会增加系统的中断延迟,影响实时性。
  • 分摊开销: 通过批处理,内核可以“攒一堆”页面,然后一次性地“禁止中断 -> 循环处理15个页面 -> 开启中断”。这极大地**分摊(amortize)**了开关中断和获取/释放锁的开销,使得平均到每个页面的同步成本大大降低。

结论: 在STM32H750上,lru_add_drain_all和它背后的per-CPU批处理机制,是一个通过分摊同步开销来提高内存管理效率、降低中断延迟的关键优化。它使得即使在单核系统上,高频率的内存页面操作也不会对系统响应性造成过大的冲击。

代码分析

// cpu_fbatches: 定义一个per-CPU结构体,用于暂存待处理的folio批次。
struct cpu_fbatches {/** 以下folio批次被组织在一起,因为它们通过禁用抢占来保护。*/local_lock_t lock;struct folio_batch lru_add;             // 待加入LRU的foliostruct folio_batch lru_deactivate_file; // 待取消激活的文件foliostruct folio_batch lru_deactivate;      // 待取消激活的匿名foliostruct folio_batch lru_lazyfree;        // 待惰性释放的folio
#ifdef CONFIG_SMPstruct folio_batch lru_activate;        // 待激活的folio(仅多核)
#endif/* 保护以下批次,它们需要禁用中断。 */local_lock_t lock_irq;struct folio_batch lru_move_tail;       // 待移动到LRU尾部的folio
};// 使用DEFINE_PER_CPU为每个CPU定义并初始化一个cpu_fbatches实例。
// 在单核STM32H750上,这只会创建一个实例。
static DEFINE_PER_CPU(struct cpu_fbatches, cpu_fbatches) = {.lock = INIT_LOCAL_LOCK(lock),.lock_irq = INIT_LOCAL_LOCK(lock_irq),
};// lru_add_drain: 清空当前CPU的LRU批处理队列。
void lru_add_drain(void)
{// 获取本地锁(在单核上,这会禁用抢占)。local_lock(&cpu_fbatches.lock);// 调用工作函数,处理当前CPU(ID由smp_processor_id()获取)的所有批处理队列。lru_add_drain_cpu(smp_processor_id());// 释放本地锁(在单核上,这会恢复抢占)。local_unlock(&cpu_fbatches.lock);// 清空本地的mlock批处理队列。mlock_drain_local();
}// lru_add_drain_all: 清空所有CPU的LRU批处理队列。
// 在本代码片段中,它被简化为只清空当前CPU的,
// 因为drop_caches通常是通过向所有CPU发送IPI(处理器间中断)来确保所有CPU都执行这个操作。
// 但对于此文件的上下文,可以理解为“清空所有需要清空的”。
void lru_add_drain_all(void)
{lru_add_drain();
}

Per-CPU LRU Drain: 批量内存管理的主力

本代码片段定义了 lru_add_drain_cpu 函数,它是 per-CPU 页/folio 批处理机制的核心执行者。在 lru_add_drain 获取了本地锁之后,它就调用这个函数来完成实际的工作。lru_add_drain_cpu 的功能是遍历指定CPU的 cpu_fbatches 结构中所有类型的批处理队列,并将其中所有暂存的folio提交到全局的、共享的LRU链表中。这是将为性能而“延迟”的操作最终“同步”回系统全局状态的关键一步。

实现原理分析

lru_add_drain_cpu 是一个高度特化的工作函数,其设计前提是调用者已经确保了必要的同步(通常是禁用了抢占)。

  1. 目标明确: 函数接收一个 cpu 编号,并通过 per_cpu 宏直接定位到该CPU的 cpu_fbatches 实例。这使得它可以被用于清空当前CPU的队列,也可以在CPU热拔插等场景下,由其他CPU来清空一个即将离线的CPU的队列。

  2. 分批处理: 函数依次处理 cpu_fbatches 结构中的每一个 folio_batchlru_add, lru_move_tail, lru_deactivate_file 等。

  3. 通用提交逻辑 (folio_batch_move_lru): 对于每个非空的批处理队列(通过 folio_batch_count 检查),它都会调用一个外部函数 folio_batch_move_lru。这个函数(未在此代码中显示)是真正实现“提交”的地方。我们可以推断其内部逻辑为:
    a. 获取保护相应全局LRU链表的重量级锁(通常是一个需要禁用中断的自旋锁)。
    b. 在一个循环中,将批处理队列中的所有folio指针逐一添加到全局LRU链表中。
    c. 释放全局LRU链表的锁。
    d. 清空本地的 folio_batch 队列。

  4. 特殊的中断安全处理 (lru_move_tail):

    • lru_move_tail 批处理队列的处理方式与其他不同。它使用了 local_lock_irqsave
    • 原因: 从 cpu_fbatches 结构体的注释可知,lru_move_tail 是一个可以从中断上下文中被修改的队列。
    • 问题: 仅仅禁用抢占不足以保护它。一个任务在执行 lru_add_drain_cpu 的过程中,可能会被一个中断打断,而这个中断服务程序(ISR)可能会尝试向 lru_move_tail 队列中添加一个新的folio,导致数据竞争。
    • 解决方案: 因此,在处理 lru_move_tail 队列之前,必须使用 local_lock_irqsave,它不仅会获取一个锁,还会禁用当前CPU的本地中断。这确保了在清空该队列期间,不会有任何中断来干扰它,从而保证了操作的原子性和安全性。data_race() 宏可能用于在编译时或运行时检查这种潜在的竞争。

特定场景分析:单核、无MMU的STM32H750平台

硬件交互与MMU

此函数是纯粹的内核内存管理算法,与硬件或MMU无关。

单核环境下的行为

在单核的STM32H750上,此函数的行为逻辑完全正确,并且其设计中的保护措施仍然是必要的。

  • 调用上下文: lru_add_drain 在调用此函数前会禁用抢占 (local_lock)。
  • per_cpu(..., cpu): cpu 参数将始终为0,函数将操作全局唯一的 cpu_fbatches 实例。
  • local_lock_irqsave: 在单核CPU上,这个函数的核心作用就是 local_irq_save,即禁用本地中断。这仍然是与中断服务程序进行同步的唯一正确方式。因此,对 lru_move_tail 的特殊保护逻辑在单核系统上依然是必需和有效的。
  • folio_activate_drain(cpu): 同样,这个函数会处理 lru_activate 批处理队列,确保所有待激活的页面都被处理。
实际意义

lru_add_drain_cpu 是 per-CPU 批处理策略兑现其性能承诺的地方。在STM32H750上,它的意义在于:

  • 实现开销分摊: 正是这个函数,通过folio_batch_move_lru,实现了“一次锁定,多次操作”的模式。这显著降低了平均到每个页面的同步开销。
  • 降低中断延迟: 对于lru_move_tail这种可能在中断中使用的队列,批处理机制允许中断服务程序(ISR)只做一个极快的、无锁的“添加到本地队列”操作就立即返回,而将耗时的、需要加全局锁的链表操作延迟lru_add_drain_cpu 在非中断上下文中执行。这极大地缩短了ISR的执行时间,降低了系统的最大中断延迟,对于需要实时响应的嵌入式系统来说,这是一个非常重要的特性。

结论: lru_add_drain_cpu 是 per-CPU 批处理优化的执行引擎。在STM32H750上,它不仅通过分摊锁开销提高了整体效率,更重要的是,它通过延迟中断上下文中的重度工作,帮助降低了系统中断延迟,提升了系统的实时性和响应性。

代码分析

/** 清空指定cpu的folio_batch队列中的页面。* 前提条件:要么“cpu”是当前CPU,并且抢占已被禁用;* 要么“cpu”正在被热拔插,并且已经死亡。*/
void lru_add_drain_cpu(int cpu)
{// 获取指定CPU的fbatches结构体指针。struct cpu_fbatches *fbatches = &per_cpu(cpu_fbatches, cpu);struct folio_batch *fbatch;// --- 处理 lru_add 批处理 ---fbatch = &fbatches->lru_add;// 如果lru_add队列不为空...if (folio_batch_count(fbatch))// ...则调用工作函数将其中的所有folio移动到全局lru_add链表。folio_batch_move_lru(fbatch, lru_add);// --- 处理 lru_move_tail 批处理 (中断安全) ---fbatch = &fbatches->lru_move_tail;// 检查lru_move_tail队列是否可能存在数据竞争(来自中断)。if (data_race(folio_batch_count(fbatch))) {unsigned long flags;// 获取一个同时禁用本地中断的锁,以安全地与中断服务程序同步。local_lock_irqsave(&cpu_fbatches.lock_irq, flags);// 移动所有folio到全局lru_move_tail链表。folio_batch_move_lru(fbatch, lru_move_tail);// 释放锁并恢复中断状态。local_unlock_irqrestore(&cpu_fbatches.lock_irq, flags);}// --- 处理 lru_deactivate_file 批处理 ---fbatch = &fbatches->lru_deactivate_file;if (folio_batch_count(fbatch))folio_batch_move_lru(fbatch, lru_deactivate_file);// --- 处理 lru_deactivate 批处理 ---fbatch = &fbatches->lru_deactivate;if (folio_batch_count(fbatch))folio_batch_move_lru(fbatch, lru_deactivate);// --- 处理 lru_lazyfree 批处理 ---fbatch = &fbatches->lru_lazyfree;if (folio_batch_count(fbatch))folio_batch_move_lru(fbatch, lru_lazyfree);// 清空与页面激活相关的批处理队列。folio_activate_drain(cpu);
}

页缓存辅助函数:降级与清理

本代码片段定义了两个重要的页缓存管理辅助函数:

  • deactivate_file_folio: 当一个folio无法被立即驱逐(例如,因为它是脏的)时,此函数提供了一种“降级”机制。它将folio标记为内存回收的优先候选者,增加了它在未来被回收的可能性。
  • folio_batch_remove_exceptionals: 这是一个内务(housekeeping)函数,用于清理一个folio_batch,从中移除所有非folio的“特殊条目”(如影子条目),以确保后续操作只处理真实的内存页。

函数一:deactivate_file_folio

实现原理分析

deactivate_file_folio 的核心思想是,当不能直接删除一个缓存页时,就将其移动到LRU(Least Recently Used)链表的“冷”端,从而“暗示”内存管理系统这个页面已不再活跃。

  1. 安全检查 (folio_test_unevictable):

    • 函数首先检查folio是否被标记为“不可驱逐”(unevictable)。这种情况发生在页面被mlock()系统调用锁定在内存中,或者用于某些特殊的内核数据结构。
    • 对于不可驱逐的folio,尝试将其降级是毫无意义的,因为无论如何它都不会被回收。因此函数直接返回。
  2. 新一代LRU处理 (lru_gen_enabled):

    • 现代Linux内核引入了更先进的多代LRU(Multi-Generational LRU)算法,由lru_gen_enabled()检查是否启用。
    • 如果启用了新算法,它会调用lru_gen_clear_refs。这个函数会尝试清除folio的“被访问”标志位。在新算法中,一个没有被访问过的folio自然就成了回收的候选者。如果这个操作成功了,就达到了“降级”的目的,函数直接返回。
  3. 传统LRU处理 (folio_batch_add_and_move):

    • 如果新一代LRU未启用或未处理该folio,则回退到传统的双链表(Active/Inactive)LRU机制。
    • folio_batch_add_and_move 是一个高效的批处理接口。它会将folio添加到当前CPU的一个私有批处理队列lru_deactivate_file)中。
    • 这个操作不会立即获取全局LRU锁并移动folio。相反,它只是快速地将folio暂存起来。真正的移动操作会被延迟,直到批处理队列满了或者被lru_add_drain等函数强制清空时,才会一次性地处理整批folio。这极大地分摊了锁竞争的开销。

函数二:folio_batch_remove_exceptionals

实现原理分析

folio_batch_remove_exceptionals 的功能非常专一:过滤一个folio_batch,只留下真正的folio。

  1. 问题背景: find_lock_entries 为了效率,会把XArray中的所有条目——无论是真正的folio指针还是代表文件空洞的“影子条目”(value entries)——都抓取到folio_batch中。
  2. 过滤算法:
    • 函数使用了一个经典的双指针、原地(in-place)过滤算法
    • i 是“读指针”,它遍历批处理中的每一个原始条目。
    • j 是“写指针”,它指向下一个有效folio应该被放置的位置。
    • 在循环中,if (!xa_is_value(folio)) 检查当前条目是否不是一个影子条目。
    • 如果是真正的folio,就执行 fbatch->folios[j++] = folio;,将其“拷贝”到写指针的位置,然后写指针前进。
    • 如果不是真正的folio(是影子条目),写指针j不会前进,这个条目在下一次有效的拷贝中就会被覆盖掉。
  3. 更新计数: 循环结束后,j的值就是批处理中所有真实folio的数量。fbatch->nr = j; 更新批处理的计数值,逻辑上“截断”了数组,丢弃了末尾的所有无效条目。

代码分析

/*** deactivate_file_folio() - 降级一个文件folio。* @folio: 要降级的folio。** 此函数向VM暗示@folio是一个很好的回收候选者,例如,* 如果因为folio是脏的或正在回写而导致其失效操作失败。** 上下文: 调用者持有对folio的引用。*/
void deactivate_file_folio(struct folio *folio)
{// 检查1: 如果folio是不可驱逐的(例如被mlock),则降级无意义。if (folio_test_unevictable(folio))return;// 检查2: 如果启用了新一代LRU,则使用其机制来“老化”这个folio。if (lru_gen_enabled() && lru_gen_clear_refs(folio))return;// 检查3: 回退到传统LRU,将folio添加到per-CPU的“待降级”批处理队列中。folio_batch_add_and_move(folio, lru_deactivate_file, true);
}/*** folio_batch_remove_exceptionals() - 从批处理中修剪掉非folio的条目。* @fbatch: 要修剪的批处理。** find_get_entries()会用folio和影子/交换/DAX条目填充批处理。* 此函数从@fbatch中修剪掉所有非folio的条目,且不留下空洞,* 以便将其传递给只处理folio的批处理操作。*/
void folio_batch_remove_exceptionals(struct folio_batch *fbatch)
{unsigned int i, j; // i是读指针,j是写指针// 使用双指针算法进行原地过滤。for (i = 0, j = 0; i < folio_batch_count(fbatch); i++) {struct folio *folio = fbatch->folios[i];// 检查当前条目是否不是一个“值条目”(即,它是一个真正的folio)。if (!xa_is_value(folio))// 如果是,则将其移动到写指针的位置,并移动写指针。fbatch->folios[j++] = folio;}// 更新批处理的有效数量为真实folio的数量。fbatch->nr = j;
}
http://www.dtcms.com/a/363059.html

相关文章:

  • Linux92 shell:倒计时,用户分类
  • 【JavaEE】多线程案例
  • 删除⽂件之git
  • 前端20个高效开发的JS工具函数
  • 《水浒智慧》第二部“英雄是怎么炼成的”(下篇)读书笔记
  • 宋红康 JVM 笔记 Day11|直接内存
  • 怎么用redis lua脚本实现各分布式锁?Redisson各分布式锁怎么实现的?
  • Higress云原生API网关详解 与 Linux版本安装指南
  • lua脚本在redis中如何单步调试?
  • docker 安装 redis 并设置 volumes 并修改 修改密码(二)
  • MATLAB矩阵及其运算(四)矩阵的运算及操作
  • 互联网大厂求职面试记:谢飞机的搞笑答辩
  • Linux为什么不是RTOS
  • 对矩阵行化简操作几何含义的理解
  • 集群无法启动CRS-4124: Oracle High Availability Services startup failed
  • TSMC-1987《Convergence Theory for Fuzzy c-Means: Counterexamples and Repairs》
  • uni-app 实现做练习题(每一题从后端接口请求切换动画记录错题)
  • Nginx的反向代理与正向代理及其location的配置说明
  • 久等啦!Tigshop O2O多门店JAVA/PHP版本即将上线!
  • SpringBoot3 + Netty + Vue3 实现消息推送(最新)
  • B树和B+树,聚簇索引和非聚簇索引
  • 云计算学习100天-第44天-部署邮件服务器
  • vscode炒股插件-韭菜盒子AI版
  • 小白H5制作教程!一分钟学会制作企业招聘H5页面
  • Linux 环境配置 muduo 网络库详细步骤
  • WPF 开发必备技巧:TreeView 自动展开全攻略
  • gbase8s之导出mysql导入gbase8s
  • WebSocket STOMP协议服务端给客户端发送ERROR帧
  • 串口服务器技术详解:2025年行业标准与应用指南
  • 大文件稳定上传:Spring Boot + MinIO 断点续传实践