cpu_local 模块技术文档

1. 模块概述

cpu_local 是 Zero-OS 内核中用于对称多处理器 (SMP) 支持的核心基础模块,位于 kernel/cpu_local/lib.rs。该模块提供了一套最小化但完整的 Per-CPU 存储抽象,使内核中的调度器、中断处理程序、TLB 管理和 RCU 子系统能够安全高效地访问每个 CPU 独立的数据。

1.1 设计目标

  • 零开销抽象:通过 CPU ID 索引的静态数组实现 Per-CPU 数据访问,避免运行时动态分配开销。
  • 中断安全:所有 Per-CPU 字段均使用原子类型,确保中断处理程序可以安全地读写 Per-CPU 状态。
  • SMP 可扩展性:支持最多 64 个逻辑 CPU(MAX_CPUS = 64),通过 LAPIC ID 映射实现硬件 CPU 到逻辑索引的转换。
  • no_std 兼容:模块在 #![no_std] 环境下运行,仅依赖 alloc crate 进行堆分配。

1.2 模块在内核中的位置

cpu_local 被以下内核子系统广泛使用:

使用方用途
kernel/src/main.rs启动时调用 init_bsp() 初始化 BSP 的 Per-CPU 数据
kernel/arch/smp.rsAP 启动时调用 init_ap() 初始化各 AP 的 Per-CPU 数据
kernel/arch/interrupts.rs中断处理中调用 current_cpu().irq_enter() / irq_exit() 跟踪 IRQ 上下文
kernel/kernel_core/process.rs使用 CpuLocal<AtomicUsize> 存储每个 CPU 的当前 PID
TLB shootdown 子系统通过 PerCpuData.tlb_mailbox 进行跨 CPU TLB 失效通知

2. 核心数据结构

2.1 PerCpuData -- Per-CPU 元数据

#[repr(C)]
pub struct PerCpuData {
    pub cpu_id: AtomicUsize,           // 逻辑 CPU 索引 (0-based)
    pub lapic_id: AtomicU32,           // 硬件 Local APIC ID
    pub preempt_count: AtomicU32,      // 抢占禁用嵌套计数器
    pub irq_count: AtomicU32,          // 中断嵌套计数器
    pub fpu_owner: AtomicUsize,        // 当前 FPU 状态所有者 (PID)
    pub need_resched: AtomicBool,      // 重调度标志
    pub current_task: AtomicPtr<()>,   // 当前运行任务的原始指针
    pub kernel_stack_top: AtomicUsize, // 内核特权栈顶
    pub irq_stack_top: AtomicUsize,    // 中断栈顶 (IST1)
    pub syscall_stack_top: AtomicUsize,// 系统调用入口栈顶
    pub rcu_epoch: AtomicU64,          // RCU epoch 计数器
    pub tlb_mailbox: TlbShootdownMailbox, // TLB shootdown 邮箱
}

设计要点:

  • 全原子字段:所有字段均使用原子类型,无需额外加锁即可在中断上下文和跨 CPU 场景下安全访问。
  • #[repr(C)] 布局:确保内存布局可预测,字段按顺序排列以最小化填充并优化缓存行利用率。核心字段设计为可容纳在单个 64 字节缓存行内。
  • RawTaskPtr 类型别名current_task 使用 *mut () 原始指针而非具体的 Task 类型引用,以避免与调度器模块产生循环依赖。
  • 手动 Send + Sync 实现:由于所有字段均为原子类型,手动为 PerCpuData 实现了 unsafe impl Sendunsafe impl Sync

关键方法:

方法功能
new()构造零初始化的 Per-CPU 记录(const fn,可用于静态初始化)
init(cpu_id, lapic_id, ...)用身份标识和栈地址初始化 CPU 槽位
preempt_disable() / preempt_enable()抢占禁用/启用(嵌套计数)
preemptible()检查当前 CPU 是否可抢占(preempt_count == 0 && irq_count == 0
irq_enter() / irq_exit()进入/退出 IRQ 处理上下文
set_need_resched() / clear_need_resched()设置/清除重调度标志
get_current_task() / set_current_task()获取/设置当前任务指针
get_fpu_owner() / set_fpu_owner() / clear_fpu_owner_if()惰性 FPU 所有权管理
tlb_mailbox()访问 TLB shootdown 邮箱

2.2 TlbShootdownMailbox -- TLB Shootdown 邮箱

#[repr(C)]
pub struct TlbShootdownMailbox {
    pub request_gen: AtomicU64,    // 最新请求的单调递增代号
    pub ack_gen: AtomicU64,        // 本 CPU 已处理的最新代号
    pub head: AtomicU64,           // 队列头(下一个待消费的条目)
    pub tail: AtomicU64,           // 队列尾(下一个待发布的槽位)
    pub entries: [TlbShootdownEntry; 4], // 固定大小环形缓冲区
}

设计要点:

  • 有界环形缓冲区:深度为 4 的 FIFO 队列(TLB_SHOOTDOWN_QUEUE_LEN = 4),允许批量排队多个 TLB shootdown 请求,避免在单槽位上串行化。
  • 代替单槽位邮箱:相比传统的单槽位设计,环形缓冲区减少了高频 shootdown 场景下的竞争和 IPI 开销。
  • head / tail 索引:通过取模运算实现环形访问,head 指向下一个待消费条目,tail 指向下一个可写入槽位。

2.3 TlbShootdownEntry -- 单条 TLB Shootdown 请求

#[repr(C)]
pub struct TlbShootdownEntry {
    pub generation: AtomicU64, // 请求代号(0 = 空/已处理)
    pub cr3: AtomicU64,        // 目标 CR3(0 表示无论 CR3 均刷新)
    pub start: AtomicU64,      // 页对齐的虚拟起始地址(0 表示全量刷新)
    pub len: AtomicU64,        // 页对齐的长度(0 表示全量刷新)
}

设计要点:

  • generation 字段兼作"槽位是否有效"的标记:值为 0 表示该槽位为空或已被处理。
  • cr3 为 0 时表示"无条件刷新",否则仅在当前 CR3 匹配时才执行刷新,这是一种针对 PCID 场景的优化。
  • startlen 均为 0 时表示全量 TLB 刷新;否则执行范围刷新。

2.4 CpuLocal -- 泛型 Per-CPU 存储包装器

pub struct CpuLocal<T> {
    init: fn() -> T,
    slots: Once<UnsafeCell<Box<[MaybeUninit<T>]>>>,
}

设计要点:

  • 惰性初始化:使用 spin::Once 确保槽位数组仅在首次访问时初始化一次。
  • 堆分配(R91-2 修复):槽位数组通过 Box::new_uninit_slice(MAX_CPUS) 在堆上分配,而非在栈上创建 [MaybeUninit<T>; MAX_CPUS]。对于大型 Per-CPU 类型(如 SampleRing 约 41KB),旧的栈分配方式会在栈上分配约 2.6MB(64 x 41KB),导致确定性的栈溢出。
  • const fn new():初始化函数可在编译期调用,支持 static 声明。

关键方法:

方法功能
new(init: fn() -> T)创建新的 Per-CPU 存储,init 为每个槽位的初始化函数
with(f: impl FnOnce(&T) -> R) -> R访问当前 CPU 的槽位(通过闭包)
with_cpu(cpu_id, f) -> Option<R>访问指定 CPU 的槽位(用于跨 CPU 操作)
get_cpu(cpu_id) -> Option<&'static T>获取指定 CPU 槽位的 'static 引用

使用示例:

use cpu_local::CpuLocal;
use core::sync::atomic::AtomicUsize;

static MY_DATA: CpuLocal<AtomicUsize> = CpuLocal::new(|| AtomicUsize::new(0));

// 访问当前 CPU 的数据
MY_DATA.with(|d| d.fetch_add(1, Ordering::SeqCst));

3. 核心函数

3.1 current_cpu_id() -- 获取当前 CPU 逻辑索引

pub fn current_cpu_id() -> usize

实现流程:

  1. 从硬件 LAPIC 寄存器(地址 0xFEE00020,位 31:24)读取当前 CPU 的 APIC ID。
  2. 使用 APIC ID 作为索引,在 LAPIC_ID_REVERSE_MAP 反向映射表中进行 O(1) 常量时间查找
  3. 若查找成功(返回值 < MAX_CPUS),直接返回逻辑 CPU 索引。
  4. 若查找失败,回退到 CPU 0(仅在早期启动阶段、LAPIC ID 尚未注册时安全)。

R67-8 优化:原始实现使用线性搜索 LAPIC_ID_MAP 数组(O(n)),在系统调用入口等性能关键路径上开销过大。引入 LAPIC_ID_REVERSE_MAP(256 项反向映射表)后,查找复杂度降为 O(1)。

3.2 register_cpu_id() -- 注册 LAPIC ID 映射

pub fn register_cpu_id(cpu_id: usize, lapic_id: u32)

在 CPU 启动过程中为每个 CPU 调用,建立双向映射:

  • 正向映射LAPIC_ID_MAP[cpu_id] = lapic_id(逻辑索引 -> 硬件 APIC ID)
  • 反向映射LAPIC_ID_REVERSE_MAP[lapic_id] = cpu_id(硬件 APIC ID -> 逻辑索引)

3.3 init_bsp() -- 初始化引导处理器

调用时机:内核早期启动阶段,中断使能之前。

执行步骤

  1. 调用 register_cpu_id(0, lapic_id) 注册 BSP 的 LAPIC ID 映射。
  2. 调用 current_cpu().init(...) 初始化 BSP(CPU 0)的 PerCpuData

3.4 init_ap() -- 初始化应用处理器

调用时机:AP 启动代码中,AP 开始执行后。

执行步骤

  1. 断言 cpu_id > 0(不能用于 BSP)且 cpu_id < MAX_CPUS
  2. 调用 register_cpu_id(cpu_id, lapic_id) 注册该 AP 的映射。
  3. 通过 PER_CPU_DATA.with() 初始化该 CPU 的 PerCpuData

3.5 current_cpu() -- 获取当前 CPU 的 PerCpuData 引用

pub fn current_cpu() -> &'static PerCpuData

这是内核中访问 Per-CPU 状态的 主要入口

3.6 其他辅助函数

函数功能
max_cpus() -> usize返回支持的最大 CPU 数量(64)
lapic_id_for_cpu(cpu_id) -> Option<u32>根据逻辑 CPU 索引查询硬件 LAPIC ID
num_online_cpus() -> usize返回当前在线 CPU 数量
mark_cpu_online()将在线 CPU 计数加 1(AP 初始化完成后调用)
clear_fpu_owner_all_cpus(pid)清除所有 CPU 上指定 PID 的 FPU 所有权

4. 并发模型

4.1 原子操作与内存序

场景内存序原因
PerCpuData 字段初始化Relaxed单线程阶段,无需同步
preempt_count / irq_countRelaxed仅本 CPU 访问
need_resched 设置Release确保写入对读取方可见
need_resched 清除AcqRelswap 需要双向语义
current_task 读/写Acquire/Release确保指针与对象同步
fpu_owner CASAcqRel/Relaxed成功路径需同步
ONLINE_CPU_COUNTAcquire/Release确保计数可见性
LAPIC 映射表Relaxed启动时写入,之后只读

4.2 TLB Shootdown 邮箱的内存序协议

请求方:Relaxed 写字段 -> Release 写 generation -> Release 写 request_gen
处理方:Acquire 读 generation -> Relaxed 读字段 -> Release 写 ack_gen
等待方:Acquire 读 ack_gen

4.3 Per-CPU 隔离模型

核心假设:每个 CPU 仅访问自己的槽位。跨 CPU 访问通过 with_cpu()/get_cpu() 显式进行。


5. 安全性考量

5.1 unsafe impl Send + Sync

PerCpuData 基于全原子字段。CpuLocal<T> 要求 T: Send + Sync

5.2 get_cpu() 中的生命周期转换

使用 transmute 将引用转为 'static,基于 static Once 堆分配永不释放。

5.3 current_cpu_id() 中的硬件访问

直接 MMIO 读取 LAPIC 寄存器,使用 read_volatile 防止编译器优化。

5.4 set_current_task() 的 unsafe 标记

调用者必须确保任务指针在设置期间有效。

5.5 边界检查

with() 包含 assert!(id < MAX_CPUS) 硬边界检查。

5.6 FPU 所有权原子清除

clear_fpu_owner_if() 使用 compare_exchange 避免竞态条件。


6. 历史缺陷修复

6.1 R67-8:O(1) LAPIC ID 查找

问题:线性搜索 O(n) 影响系统调用入口性能。
修复:引入 256 项反向映射表实现 O(1) 查找。

6.2 R91-2:堆分配 Per-CPU 槽位

问题:大型 Per-CPU 类型(~41KB x 64 = ~2.6MB)导致栈溢出。
修复:改用 Box::new_uninit_slice 堆分配。


7. 全局静态变量一览

变量类型用途
LAPIC_ID_MAP[AtomicU32; 64]正向映射:逻辑索引 -> LAPIC ID
LAPIC_ID_REVERSE_MAP[AtomicUsize; 256]反向映射:LAPIC ID -> 逻辑索引
PER_CPU_DATACpuLocal<PerCpuData>全局 Per-CPU 数据块
ONLINE_CPU_COUNTAtomicUsize在线 CPU 计数(初始值 1)

8. 常量定义

常量含义
MAX_CPUS64支持的最大 CPU 数量
INVALID_LAPIC_IDu32::MAX无效 LAPIC ID 标记
INVALID_CPU_IDusize::MAX无效 CPU 索引标记
LAPIC_ID_REVERSE_MAP_SIZE256反向映射表大小
NO_FPU_OWNERusize::MAX无 FPU 所有者标记
TLB_SHOOTDOWN_QUEUE_LEN4TLB shootdown 队列深度