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]环境下运行,仅依赖alloccrate 进行堆分配。
1.2 模块在内核中的位置
cpu_local 被以下内核子系统广泛使用:
| 使用方 | 用途 |
|---|---|
kernel/src/main.rs | 启动时调用 init_bsp() 初始化 BSP 的 Per-CPU 数据 |
kernel/arch/smp.rs | AP 启动时调用 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 Send和unsafe 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 场景的优化。start和len均为 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
实现流程:
- 从硬件 LAPIC 寄存器(地址
0xFEE00020,位 31:24)读取当前 CPU 的 APIC ID。 - 使用 APIC ID 作为索引,在
LAPIC_ID_REVERSE_MAP反向映射表中进行 O(1) 常量时间查找。 - 若查找成功(返回值 <
MAX_CPUS),直接返回逻辑 CPU 索引。 - 若查找失败,回退到 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() -- 初始化引导处理器
调用时机:内核早期启动阶段,中断使能之前。
执行步骤:
- 调用
register_cpu_id(0, lapic_id)注册 BSP 的 LAPIC ID 映射。 - 调用
current_cpu().init(...)初始化 BSP(CPU 0)的PerCpuData。
3.4 init_ap() -- 初始化应用处理器
调用时机:AP 启动代码中,AP 开始执行后。
执行步骤:
- 断言
cpu_id > 0(不能用于 BSP)且cpu_id < MAX_CPUS。 - 调用
register_cpu_id(cpu_id, lapic_id)注册该 AP 的映射。 - 通过
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_count | Relaxed | 仅本 CPU 访问 |
need_resched 设置 | Release | 确保写入对读取方可见 |
need_resched 清除 | AcqRel | swap 需要双向语义 |
current_task 读/写 | Acquire/Release | 确保指针与对象同步 |
fpu_owner CAS | AcqRel/Relaxed | 成功路径需同步 |
ONLINE_CPU_COUNT | Acquire/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_DATA | CpuLocal<PerCpuData> | 全局 Per-CPU 数据块 |
ONLINE_CPU_COUNT | AtomicUsize | 在线 CPU 计数(初始值 1) |
8. 常量定义
| 常量 | 值 | 含义 |
|---|---|---|
MAX_CPUS | 64 | 支持的最大 CPU 数量 |
INVALID_LAPIC_ID | u32::MAX | 无效 LAPIC ID 标记 |
INVALID_CPU_ID | usize::MAX | 无效 CPU 索引标记 |
LAPIC_ID_REVERSE_MAP_SIZE | 256 | 反向映射表大小 |
NO_FPU_OWNER | usize::MAX | 无 FPU 所有者标记 |
TLB_SHOOTDOWN_QUEUE_LEN | 4 | TLB shootdown 队列深度 |