引入原因
Per-CPU 变量是 Linux 内核中的一个重要概念,主要解决以下问题:
锁竞争问题
- 在 SMP 系统中,如果多个 CPU 同时访问同一个变量,需要使用锁来保证数据一致性
- 这种锁机制会导致性能下降,特别是在高并发场景下
- Per-CPU 变量为每个 CPU 核心分配独立的变量副本,无需加锁即可安全访问
- 需要注意的一点是,Per-CPU 变量是独立的变量,每个 CPU 分别有一个自己的变量,这些变量不是互相同步的。
缓存效率
- 传统共享变量会导致频繁的缓存失效(cache invalidation)
- Per-CPU 变量位于各自 CPU 的本地缓存中,提高缓存命中率
- 减少了 CPU 间的缓存同步开销
NUMA 友好
- 在 NUMA 架构中,访问远端内存的开销很大
- Per-CPU 变量确保数据位于本地节点,减少跨 NUMA 节点访问
这时候你可能会问,为什么不直接定义 16 个变量分别用于不同的 CPU?
代码冗余
如果手动为每个 CPU 定义变量,代码会变得冗长且难以维护。
例如,对于 128 个 CPU 的系统,可能需要定义 128 个变量:
int var_cpu0, var_cpu1, var_cpu2, ..., var_cpu127;
不能动态支持多 CPU
- 在支持 CPU 热插拔的系统中,CPU 的数量可能动态变化。
- 手动定义变量无法适应这种动态变化。
访问效率低
访问特定 CPU 的变量时,需要手动计算偏移量或使用条件语句,效率较低。
例如:
int *get_var(int cpu) { switch (cpu) { case 0: return &var_cpu0; case 1: return &var_cpu1; // ... default: return NULL; } }
缓存局部性问题
- 手动定义的变量可能位于同一缓存行中,导致 缓存行共享(False Sharing),降低性能。
Per-CPU 变量的优势
代码简洁
Per-CPU 变量通过内核提供的机制自动为每个 CPU 分配变量,代码更简洁。
例如:
DEFINE_PER_CPU(int, my_var);
动态适应 CPU 数量
Per-CPU 变量支持动态 CPU 数量,能够自动适应 CPU 热插拔。
例如,动态分配 Per-CPU 变量:
int __percpu *my_var = alloc_percpu(int);
高效访问
Per-CPU 变量通过内核提供的宏(如
this_cpu_ptr
和per_cpu_ptr
)高效访问当前或指定 CPU 的变量。例如:
int *ptr = this_cpu_ptr(&my_var); // 访问当前 CPU 的变量 int *ptr = per_cpu_ptr(&my_var, cpu); // 访问指定 CPU 的变量
缓存优化
- Per-CPU 变量会自动对齐到缓存行,避免缓存行共享问题。
- 例如,内核会确保每个 CPU 的变量位于不同的缓存行中。
静态声明和定义
// 1. 定义 Per-CPU 变量
DEFINE_PER_CPU(type, name); // 定义并初始化为 0
DEFINE_PER_CPU(type, name) = value; // 定义并指定初值
// 2. 声明外部 Per-CPU 变量
DECLARE_PER_CPU(type, name); // 声明在其他地方定义的变量
// 3. 定义 Per-CPU 数组
DEFINE_PER_CPU(type, name[SIZE]); // 定义数组类型
// 4. 定义只读 Per-CPU 变量
DEFINE_PER_CPU_READ_MOSTLY(type, name); // 主要用于只读数据,优化缓存
内部实现机制
#define DEFINE_PER_CPU(type, name) \
DEFINE_PER_CPU_SECTION(type, name, "")
__attribute__((section(".data..percpu"))) int per_cpu_n
#define DEFINE_PER_CPU_SECTION(type, name, sec) \
__PCPU_ATTRS(sec) __typeof__(type) name
#define __PCPU_ATTRS(sec) \
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec)))
#define PER_CPU_BASE_SECTION ".data..percpu"
比如:
// 在编译时定义 Per-CPU 变量
DEFINE_PER_CPU(int, my_counter) = 0;
// 展开后实际上是
__attribute__((section(".data..percpu"))) int my_counter = 0;
这里为什么要引入 section 机制?
对于 kernel 中的普通变量,经过了编译和链接后,会被放置到.data 或者.bss 段,系统在初始化的时候会准备好一切(例如 clear bss),由于 per CPU 变量的特殊性,内核将这些变量放置到了其他的 section,位于 kernel address space 中
__per_cpu_start
和__per_cpu_end
之间,我们称之 Per-CPU 变量的原始变量。 摘自:Linux 内核同步机制之(二):Per-CPU 变量
- Section 机制允许编译器将特定的数据放在目标文件的特定区域
- 对于 Per-CPU 变量,Linux 使用 .data..percpu section 将所有 Per-CPU 变量集中存放
- 这种集中存放便于运行时进行整体复制和管理
Per-CPU 变量如何建立副本的?
在内核初始化阶段会调用setup_per_cpu_areas
函数,为每个 CPU 设置 Per-CPU 变量的偏移地址。
void __init setup_per_cpu_areas(void)
{
unsigned long delta;
unsigned int cpu;
...
delta = (unsigned long)pcpu_base_addr - (unsigned long)__per_cpu_start;
for_each_possible_cpu(cpu)
__per_cpu_offset[cpu] = delta + pcpu_unit_offsets[cpu];
}
__per_cpu_offset
是一个全局数组,用于存储每个 CPU 的 Per-CPU 变量的偏移地址。- 在初始化时,内核会为每个 CPU 计算其 Per-CPU 变量的基地址,并将其存储在
__per_cpu_offset[cpu]
中。 - 这个偏移地址是相对于 Per-CPU 变量的起始地址(
__per_cpu_start
)的。
在 CPU 初始化时,内核会使用调用set_my_cpu_offset
函数设置每个 CPU 静态 Per-CPU 变量的偏移地址。
#define per_cpu_offset(x) (__per_cpu_offset(x))
void notrace cpu_init(void)
{
#ifndef CONFIG_CPU_V7M
unsigned int cpu = smp_processor_id();
struct stack *stk = &stacks[cpu];
if (cpu >= NR_CPUS) {
pr_crit("CPU%u: bad primary CPU number\n", cpu);
BUG();
}
/*
* This only works on resume and secondary cores. For booting on the
* boot cpu, smp_prepare_boot_cpu is called after percpu area setup.
*/
set_my_cpu_offset(per_cpu_offset(cpu));
cpu_proc_init();
...
}
在set_my_cpu_offset
函数中,根据ARM64_HAS_VIRT_HOST_EXTN
的值,选择将偏移地址写入tpidr_el1
或者tpidr_el2
寄存器。
/* arch/arm64/include/asm/percpu.h */
static inline void set_my_cpu_offset(unsigned long off)
{
asm volatile(ALTERNATIVE("msr tpidr_el1, %0",
"msr tpidr_el2, %0",
ARM64_HAS_VIRT_HOST_EXTN)
:: "r" (off) : "memory");
}
在访问时使用this_cpu_ptr
宏,展开后可以发现相当于 percpu 变量指针 ptr 加上__my_cpu_offset
。__my_cpu_offset
宏即是从当前cpu的tpidr_el1
、tpidr_el2
寄存器中取出此前设置的__per_cpu_offset[cpu]
值。
#define this_cpu_ptr(ptr) raw_cpu_ptr(ptr)
#define raw_cpu_ptr(ptr) \
({ \
__verify_pcpu_ptr(ptr); \
arch_raw_cpu_ptr(ptr); \
})
#define arch_raw_cpu_ptr(ptr) SHIFT_PERCPU_PTR(ptr, __my_cpu_offset)
static inline unsigned long __my_cpu_offset(void)
{
unsigned long off;
/*
* We want to allow caching the value, so avoid using volatile and
* instead use a fake stack read to hazard against barrier().
*/
asm(ALTERNATIVE("mrs %0, tpidr_el1",
"mrs %0, tpidr_el2",
ARM64_HAS_VIRT_HOST_EXTN)
: "=r" (off) :
"Q" (*(const unsigned long *)current_stack_pointer));
return off;
}
使用方式
Per-CPU 变量的常用操作方式:
静态定义
// 定义 Per-CPU 整型变量
DEFINE_PER_CPU(int, my_percpu_var);
// 定义 Per-CPU 结构体
DEFINE_PER_CPU(struct my_struct, my_percpu_struct);
访问变量
// 获取本地 CPU 的变量
int cpu_var = get_cpu_var(my_percpu_var);
put_cpu_var(my_percpu_var); // 必须配对使用
// 访问指定 CPU 的变量
int var_cpu0 = per_cpu(my_percpu_var, 0);
// 遍历所有 CPU 的变量
int cpu;
for_each_possible_cpu(cpu) {
int value = per_cpu(my_percpu_var, cpu);
// 处理 value
}