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)    收藏  举报

导航