引入原因

Per-CPU 变量是 Linux 内核中的一个重要概念,主要解决以下问题:

  1. 锁竞争问题

    • 在 SMP 系统中,如果多个 CPU 同时访问同一个变量,需要使用锁来保证数据一致性
    • 这种锁机制会导致性能下降,特别是在高并发场景下
    • Per-CPU 变量为每个 CPU 核心分配独立的变量副本,无需加锁即可安全访问
    • 需要注意的一点是,Per-CPU 变量是独立的变量,每个 CPU 分别有一个自己的变量,这些变量不是互相同步的。
  2. 缓存效率

    • 传统共享变量会导致频繁的缓存失效(cache invalidation)
    • Per-CPU 变量位于各自 CPU 的本地缓存中,提高缓存命中率
    • 减少了 CPU 间的缓存同步开销
  3. NUMA 友好

    • 在 NUMA 架构中,访问远端内存的开销很大
    • Per-CPU 变量确保数据位于本地节点,减少跨 NUMA 节点访问

这时候你可能会问,为什么不直接定义 16 个变量分别用于不同的 CPU?

  1. 代码冗余

    • 如果手动为每个 CPU 定义变量,代码会变得冗长且难以维护。

    • 例如,对于 128 个 CPU 的系统,可能需要定义 128 个变量:

      int var_cpu0, var_cpu1, var_cpu2, ..., var_cpu127;
      
  2. 不能动态支持多 CPU

    • 在支持 CPU 热插拔的系统中,CPU 的数量可能动态变化。
    • 手动定义变量无法适应这种动态变化。
  3. 访问效率低

    • 访问特定 CPU 的变量时,需要手动计算偏移量或使用条件语句,效率较低。

    • 例如:

      int *get_var(int cpu) {
          switch (cpu) {
              case 0: return &var_cpu0;
              case 1: return &var_cpu1;
              // ...
              default: return NULL;
          }
      }
      
  4. 缓存局部性问题

    • 手动定义的变量可能位于同一缓存行中,导致 缓存行共享(False Sharing),降低性能。

Per-CPU 变量的优势

  1. 代码简洁

    • Per-CPU 变量通过内核提供的机制自动为每个 CPU 分配变量,代码更简洁。

    • 例如:

      DEFINE_PER_CPU(int, my_var);
      
  2. 动态适应 CPU 数量

    • Per-CPU 变量支持动态 CPU 数量,能够自动适应 CPU 热插拔。

    • 例如,动态分配 Per-CPU 变量:

      int __percpu *my_var = alloc_percpu(int);
      
  3. 高效访问

    • Per-CPU 变量通过内核提供的宏(如 this_cpu_ptrper_cpu_ptr)高效访问当前或指定 CPU 的变量。

    • 例如:

      int *ptr = this_cpu_ptr(&my_var);  // 访问当前 CPU 的变量
      int *ptr = per_cpu_ptr(&my_var, cpu);  // 访问指定 CPU 的变量
      
  4. 缓存优化

    • 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_el1tpidr_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
}