内存管理锁与并发
内存子系统同时运行在单核启动、普通内核线程、多 CPU 释放、缺页异常和设备完成路径中。锁的选择由“能否睡眠、能否被中断、是否依赖当前 CPU、是否会调用外部代码”决定;本章给出当前源码中的锁、原子变量、顺序和禁止组合。
1. 并发模型
启动内存依赖引导处理器独占,运行时物理页依赖禁止中断自旋锁,虚拟地址空间由其所属操作系统提供外层锁。ax-memory-set、page-table-generic 和 axcpu::paging 本身不为一个实例增加隐式全局锁。
1.1 上下文矩阵
下表中的“允许”表示实现能够保持数据一致性,不代表满足硬实时延迟。Buddy 或 Slab 扩容即使使用禁止中断自旋锁,也不应出现在硬实时临界区。
| 执行上下文 | 可使用的同步 | 可进入的内存路径 | 禁止行为 |
|---|---|---|---|
| 单核 early boot | 无锁(约定引导核独占) | 固定容 量内存图、early bump、boot 页表 | 调度等待、回收、文件系统调用 |
| 普通内核线程 | 禁止中断/禁止抢占自旋锁,或操作系统 Mutex | Slab、Buddy、地址空间操作、缺页 | 持 allocator 锁调用文件系统或回收 |
| 硬中断 | 已预分配对象、固定 ring、极短禁止中断锁 | 驱动已有 descriptor 的状态转换 | 通用堆、Buddy、Slab 扩容、阻塞 Mutex |
| bottom half | 由具体运行时约束决定 | 已预留资源、无阻塞短路径 | 无界 reclaim、持自旋锁等待 I/O |
| 硬实时临界区 | 固定容量池或启动期预留资源 | 常数上界的本地状态转换 | Buddy、通用 Slab miss、动态虚拟映射 |
| 用户缺页异常 | 地址空间 Mutex、短时页表锁、外层有界回收 | 分配页、填充、映射、记账 | 中断上下文缺页、持页表锁执行文件 I/O |
禁止中断自旋锁解决的是重入死锁和内核可抢占问题,不会给分配搜索提供固定时间上界。实时路径必须通过预分配消除动态扩容。
1.2 同步对象
当前主要锁及其保护对象覆盖 early boot、运行时分配器、地址空间和虚拟机生命周期。测试专用的 std::sync::Mutex 不属于生产并发模型,不能用于推断禁止中断、CPU pinning 或内核可抢占语义。
| 锁或原子 | 源码 | 保护对象 | 关键约束 |
|---|---|---|---|
无锁 static mut(RAM_START/RAM_END/RAM_CURRENT) | someboot/src/mem/ram.rs | early bump 状态 | 仅引导处理器、单核 early boot 阶段调用;不承担运行时 IRQ 安全 |
SpinLock<BuddyAllocator>(ax_sync,经 lock_irqsave() 访问) | buddy-slab-allocator/src/global.rs | 全部 Buddy section、free lists 和 page metadata | 临界区内不调用上层策略或回收 |
SpinLock<SlabAllocator>(ax_sync) | buddy-slab-allocator/src/slab/mod.rs | 当前 CPU 的 Slab cache | 通过 ax_percpu::with_cpu_pin 获取 CPU-local 指针 |
| remote-free atomics | buddy-slab-allocator/src/slab/page.rs | 跨 CPU 归还的 object 链 | 释放者只发布节点,owner CPU drain |
SpinLock<Usages> | ax-alloc/src/buddy_slab.rs | UsageKind 字节计数 | 统计锁不发布资源,释放正确性由 allocator 锁和 owner 协议保证 |
SpinLock<AddrSpace>(ax_sync,lock_irqsave()) | axmm/src/lib.rs | ArceOS 内核地址空间 | 不在锁内执行可睡眠 I/O |
Mutex<AddrSpace> | Starry kernel/src/mm/aspace | 单个进程地址空间、虚拟内存区域与页表 | fault、map、clone 通过同一 owner 串行化 |
Mutex<Machine<...>>(IrqSafeMutex 别名) | axvm/src/vm/mod.rs | AxVM 生命周期资源、axaddrspace 与嵌套页表 | map、fault、客户机访问和 clear 均在同一虚拟机 owner 下执行 |
AtomicU64 汇总计数 | os/StarryOS/kernel/src/mm/aspace/accounting.rs | 单地址空间匿名页、文件页、共享内存页和峰值 | 映射操作由地址空间锁串行化;原子只提供无锁统计快照 |
IrqMutex<FrameTableRefCount> | Starry 写时复制 backend | 写时复制物理页索引;每个 frame 再用 IrqMutex<FrameRefCnt> 保存 u8 引用计数 | 不在外层表锁内执行外部 I/O;归还物理页前释放表锁 |
AtomicU64/AtomicI64 | Starry kernel mm stat/accounting | RSS/VSS 与峰值 | 原子顺序按统计语义选择;当前 /proc/meminfo 的 Committed_AS 固定展示 0 |
表中的同步对象保护不同层级状态,不能组成一个长期持有的全局锁链。尤其是 allocator、地址空间和文件 backend 之间需要在资源准备与状态提交阶段缩短临界区。
2. 启动期同步
early bump allocator 没有锁:ram.rs 用三个 static mut(RAM_START、RAM_END、RAM_CURRENT)保存状态,安全性完全依赖“仅引导处理器、单核 early boot 阶段调用”的约定。引导处理器在启动其他 CPU 前完成所有 early allocation、内存图发布和 per-CPU 区域构造。
2.1 单写者流程
启动内存的所有更新都发生在引导处理器单一调用链上。alloc() 完成对齐和边界检查后推进 RAM_CURRENT;失败不修改状态。memory_map_setup() 把尚未发布的已用区间以 Reserved 合入内存图,此后 early bump 不再使用(当前没有冻结机制,靠调用顺序保证)。
每 CPU 区域在引导核完成 typed 初始化和 cache maintenance 后才发布 CPU 数量。发布使用 Release,应用处理器观察使用 Acquire,保证 metadata、stack top 和页表地址先于 CPU online 可见。
2.2 启动页表发布
写入新页表根前,页表页必须已经清零并完成全部页表项写入。架构实现负责必要的页表根寄存器、地址转换后备缓冲区失效和指令/数据屏障;不能只依赖 Rust 锁释放替代硬件页表同步。
3. 运行时分配器
分配器把页级慢路径集中到单个 Buddy 锁,把小对象热路径分散到每 CPU Slab。该结构与主流实时操作系统的“简单全局页源 + 固定大小快速路径”一致,没有引入非统一内存访问、页迁移或复杂 reclaim 锁链。
3.1 Buddy 临界区
GlobalAllocator::buddy 是 ax_sync::SpinLock<BuddyAllocator>,通过 lock_irqsave() 访问(取锁同时关闭本地中断)。section 链、free list、PageMeta、拆分和合并都只在该锁内修改。region 初始化也持有同一锁,因此初始化期间不能并发分配。
alloc_pages
-> disable local IRQ + acquire Buddy lock
-> scan sections
-> find aligned block
-> split and mark PageMeta::Allocated
-> release lock + restore IRQ state
锁内不能调用虚拟文件系统、页缓存回收、缺页处理或任何可能再次分配的 callback。当前 ax-alloc 在 page allocation 失败后会释放 allocator 锁,再调用已注册的 page reclaim callback 并重试;因此新增 reclaim 代码必须维持“锁外执行、重试有界”的约束。
3.2 每 CPU Slab 与禁止抢占
字节分配在 ax-alloc/src/buddy_slab.rs 中通过 ax_percpu::with_cpu_pin 获取当前 CPU 的 Slab 指针,并由 per-CPU Slab 内部 SpinLock 串行化本 CPU cache。具体 allocation 代码只在运行时页与堆分配器