3. ARM64架构基础(下)
在调度器移植适配过程中,ARM64 的内存管理单元(MMU)、缓存一致性协议以及原子操作指令是决定内核能否正确、高效运行的关键硬件基础。本章深入剖析这些核心机制,为后续调度器上下文切换、进程地址空间管理及同步原语的实现提供坚实的硬件视角支撑。
3.1 ARM64 内存管理单元(MMU)与地址转换
ARM64 的 MMU 负责将处理器发出的虚拟地址(VA)转换为物理地址(PA),并实施内存属性(如可缓存性、可共享性)控制。调度器在创建进程、切换进程时,必须正确配置 MMU 相关的页表。
3.1.1 虚拟地址空间布局与转换粒度
ARM64 支持 4KB、16KB 和 64KB 三种页粒度,以及 4 级(L0~L3)或 3 级页表结构。以最常见的 4KB 页、48 位虚拟地址空间为例,地址转换过程如下:
- 输入地址:48 位虚拟地址(VA[47:0])。
- 页表基址寄存器:TTBR0_EL1(用户空间)和 TTBR1_EL1(内核空间)。
- 转换级别:L0(9位索引)→ L1(9位)→ L2(9位)→ L3(9位)→ 页内偏移(12位)。
context_switch 中切换进程时,会通过 switch_mm 更新 TTBR0_EL1 指向新进程的页表基址,并执行 isb 指令保证页表切换生效。
ASID(Address Space ID)机制允许 TLB 中同时缓存多个进程的页表项,避免每次进程切换都刷新 TLB,显著降低调度开销。
3.2 页表描述符与内存属性
ARM64 页表描述符不仅包含物理地址,还编码了内存类型、访问权限、可共享性等属性。调度器在管理内核栈、进程描述符时,需要确保这些内存区域被正确映射。
| 描述符字段 | 位域 | 说明 | 调度器相关场景 |
|---|---|---|---|
| 输出地址 | [47:12] | 物理页帧号(PFN) | 进程页表映射 |
| AttrIndx | [10:8] | 指向 MAIR 寄存器中的属性索引 | 内核栈可缓存性配置 |
| NS | [5] | 非安全空间标识 | 虚拟化场景隔离 |
| AP | [7:6] | 访问权限(EL0/EL1 读写) | 用户态/内核态隔离 |
| SH | [9:8] | 可共享性(非共享/内部共享/外部共享) | 多核调度数据一致性 |
| AF | [10] | 访问标志(Access Flag) | 页面回收与缺页统计 |
| nG | [11] | 非全局位(区分进程特有页) | ASID 匹配与 TLB 管理 |
3.3 缓存架构与一致性协议
ARM64 处理器通常包含多级缓存(L1/L2/L3),并使用 MESI 或其变体(如 MOESI)协议维护缓存一致性。调度器在进程迁移、内核栈切换时必须考虑缓存状态。
3.3.1 缓存层级与行大小
- L1 缓存:分为指令缓存(I-cache)和数据缓存(D-cache),典型大小为 32KB~64KB,行大小 64 字节。
- L2 缓存:统一缓存,典型大小 256KB~1MB,行大小 64 字节。
- L3 缓存:可选,多核共享,典型大小 2MB~8MB。
3.3.2 缓存一致性域与调度器影响
当调度器将一个进程从 CPU0 迁移到 CPU1 时,该进程的数据在 CPU0 的 L1/L2 缓存中可能处于 Modified 或 Exclusive 状态。硬件一致性协议会自动嗅探(Snoop)或通过目录(Directory)方式将数据迁移到 CPU1 的缓存中。但以下场景需要软件显式维护:
- 非一致内存访问(DMA):设备访问内存前,需要执行缓存清理(Clean)或失效(Invalidate)操作。
- 跨集群迁移:在大小核架构(如 big.LITTLE)中,不同集群可能使用不同的一致性协议,调度器迁移时需要额外的内存屏障。
3.4 原子操作与内存屏障指令
调度器是内核中并发度最高的模块之一,大量使用原子操作和内存屏障来保护临界区、实现无锁数据结构。ARM64 提供了丰富的原子指令和屏障指令。
3.4.1 独占访问指令(LDXR/STXR)
ARM64 使用 LDXR(Load Exclusive Register)和 STXR(Store Exclusive Register)实现原子读-改-写操作。调度器中的 atomic_add、atomic_cmpxchg 等操作均基于此实现。
// ARM64 原子加操作示例(内核实现)
static inline void atomic_add(int i, atomic_t *v)
{
unsigned long tmp;
int result;
asm volatile(
"1: ldxr %w0, [%2]\n"
" add %w0, %w0, %w3\n"
" stxr %w1, %w0, [%2]\n"
" cbnz %w1, 1b\n"
: "=&r" (result), "=&r" (tmp)
: "r" (&v->counter), "Ir" (i)
: "memory");
// 说明:
// 1. ldxr 独占加载 counter 值到 result
// 2. add 执行加法
// 3. stxr 尝试独占存储,成功时 tmp=0,失败时 tmp=1
// 4. cbnz 判断 tmp,非零则跳回 1b 重试
}
3.4.2 内存屏障指令
ARM64 定义了三种主要的内存屏障指令,调度器在不同场景下使用它们来保证内存访问顺序:
| 指令 | 全称 | 屏障类型 | 调度器典型使用场景 |
|---|---|---|---|
DMB |
Data Memory Barrier | 数据访问顺序 | 自旋锁、读写锁临界区 |
DSB |
Data Synchronization Barrier | 数据访问完成 + 等待 | TLB 刷新后、缓存维护指令后 |
ISB |
Instruction Synchronization Barrier | 指令流水线刷新 | 页表切换后、修改上下文 ID 后 |
context_switch 中,切换 TTBR0_EL1 后必须执行 isb;在 switch_mm 中刷新 TLB 后必须执行 dsb ish + isb。
3.4.3 获取/释放语义(Acquire/Release)
ARM64 在 ARMv8.1 及之后版本引入了 LDAPR 和 STLR 指令,提供硬件级的获取(Acquire)和释放(Release)语义,用于实现更高效的 smp_load_acquire 和 smp_store_release。
// ARM64 获取语义加载(用于无锁队列头指针读取)
static inline int read_head_acquire(struct lockless_queue *q)
{
int head;
asm volatile(
" ldapr %w0, [%1]\n"
: "=&r; (head)
: "r" (&q->head)
: "memory");
return head;
}
// ARM64 释放语义存储(用于无锁队列尾指针更新)
static inline void write_tail_release(struct lockless_queue *q, int val)
{
asm volatile(
" stlr %w0, [%1]\n"
:
: "r" (val), "r" (&q->tail)
: "memory");
}