Linux内核机制—percpu-每CPU变量-2-实现
一、Per-CPU 变量的地址差值
每个 CPU 上同一 per-cpu 变量的地址差值是 __per_cpu_offset[cpu],它不是固定值,各 CPU 之间也不一定等距。
1. 地址计算公式:
某变量在 CPU N 上的实际地址 = 变量的"静态基址" + __per_cpu_offset[N]
2. 内核中的实现:
#define per_cpu(var, cpu) (*SHIFT_PERCPU_PTR(&(var), per_cpu_offset(cpu))) #define per_cpu_offset(cpu) (__per_cpu_offset[cpu]) /* HIFT_PERCPU_PTR 就是做指针加偏移 */ #define SHIFT_PERCPU_PTR(ptr, offset) (typeof(ptr))((unsigned long)(ptr) + (offset))
3. 内存布局
编译时,所有 per-cpu 变量被放在一个特殊的 section(.data..percpu)中,这段区域作为"模板",启动时为每个 CPU 复制一份。
Per-CPU 区域分配(不一定连续!) __per_cpu_offset[0] ──→ ┌────────────────────┐ CPU 0 的 per-cpu 区域 │ my_count │ ← &per_cpu(my_count, 0) │ other_var │ │ ... │ │ (整个 section 副本) │ └────────────────────┘ __per_cpu_offset[1] ──→ ┌────────────────────┐ CPU 1 的 per-cpu 区域 │ my_count │ ← &per_cpu(my_count, 1) │ other_var │ │ ... │ └────────────────────┘ __per_cpu_offset[2] ──→ ┌────────────────────┐ CPU 2 的 per-cpu 区域 │ my_count │ ← &per_cpu(my_count, 2) │ other_var │ │ ... │ └────────────────────┘
4. 差值是多少
不是一个编译时常量,取决于系统配置。 可以在运行时查看:
cat /proc/kallsyms | grep __per_cpu_offset
cat /sys/kernel/debug/percpu/chunks # 如果有 debugfs
典型值示例(某 ARM64 8 核设备):
__per_cpu_offset[0] = 0xFFFFFF8079C00000 __per_cpu_offset[1] = 0xFFFFFF8079C80000 差值 = 0x80000 (512KB) __per_cpu_offset[2] = 0xFFFFFF8079D00000 差值 = 0x80000 __per_cpu_offset[3] = 0xFFFFFF8079D80000 差值 = 0x80000 ...
在这个例子中每个 CPU 的 per-cpu 区域大小约 512KB,等距排列。但这不是保证的——NUMA 系统上各 CPU 的 per-cpu 区域可能分配在不同的 NUMA node 内存上,offset 之间的差值可能完全不规则。
5. 决定差值的因素
--------------------------------------------------------------------------------------------------------------- 因素 影响 --------------------------------------------------------------------------------------------------------------- per-cpu section 总大小 所有 per-cpu 变量 + 动态分配的 per-cpu 内存的总和,决定每个 CPU 区域的大小 NUMA 拓扑 每个 CPU 的 per-cpu 区域优先分配在本地 NUMA node 上,各 node 的地址空间不连续 内存分配器 启动时通过 pcpu_setup_first_chunk() 分配,具体地址取决于 memblock 分配结果 CONFIG_SMP UP 系统没有 offset,只有一份 per-cpu 数据 ---------------------------------------------------------------------------------------------------------------
6. 内核中如何确定这些 offset
启动流程的早期调用:
/* start_kernel() --> setup_per_cpu_areas() */ void __init setup_per_cpu_areas(void) { // 1. 计算 per-cpu section 大小(静态 + 预留动态空间) // 2. 为每个 CPU 分配一块内存(尽量在本地 NUMA node) // 3. 将"模板" section 内容复制到各 CPU 的区域 // 4. 填写 __per_cpu_offset[] 数组 for_each_possible_cpu(cpu) { __per_cpu_offset[cpu] = areas[cpu] - __per_cpu_start; // areas[cpu] 是为该 CPU 分配的内存起始 // __per_cpu_start 是模板 section 的起始 } }
7. ARM64 上的快速访问优化
ARM64 Linux 用 TPIDR_EL1 寄存器存储当前 CPU 的 per-cpu offset,避免每次都从 __per_cpu_offset[] 数组读取:
//arch/arm64/include/asm/percpu.h static inline unsigned long __my_cpu_offset(void) { unsigned long off; asm ("mrs %0, tpidr_el1" : "=r" (off)); // 一条指令读取当前 CPU 的 offset return off; } #define this_cpu_ptr(ptr) SHIFT_PERCPU_PTR(ptr, __my_cpu_offset())
这样 this_cpu_read(var) 的开销就是:1 条 mrs + 1 次加法 + 1 次内存访问。
8. 实际验证方法
在内核模块中打印:
#include <linux/percpu.h> static DEFINE_PER_CPU(unsigned long, my_var); static int __init test_init(void) { int cpu; for_each_possible_cpu(cpu) { pr_info("CPU%d: my_var addr = %px, offset = 0x%lx\n", cpu, &per_cpu(my_var, cpu), __per_cpu_offset[cpu]); } return 0; }
输出类似:
CPU0: my_var addr = ffffff8079c12340, offset = 0xffffff8079c00000 CPU1: my_var addr = ffffff8079c92340, offset = 0xffffff8079c80000 CPU2: my_var addr = ffffff8079d12340, offset = 0xffffff8079d00000 ...
可以看到同一变量在不同 CPU 上的地址差 = 对应 CPU offset 之差(这里是 0x80000 = 512KB)。
9. 总结
(1) 差值不是固定常量,是 __per_cpu_offset[cpuB] - __per_cpu_offset[cpuA];
(2) 典型值等于 per-cpu 区域的大小(几百 KB),但 NUMA 系统可能不等距;
(3) ARM64 通过 TPIDR_EL1 寄存器加速当前 CPU 的 offset 访问;
posted on 2026-08-17 15:59 Hello-World3 阅读(2) 评论(0) 收藏 举报
浙公网安备 33010602011771号