Linux 内核 | Per CPU 变量


# 简介

Per CPU 变量,顾名思义指每个CPU核都有对某一个变量的一份拷贝。Per CPU 变量的应用之一是保存每个 CPU 的 ID,这是因为内核有时需要判断自己当前是运行在哪个CPU核上,因此需要获取当前执行代码的 CPU ID。

实现

为每个CPU定义一个变量的拷贝的宏定义在文件include/linux/percpu-defs.h中,如下:

#define DEFINE_PER_CPU(type, name) \
        DEFINE_PER_CPU_SECTION(type, name, "")

若我们使用DEFINE_PER_CPU(int, per_cpu_n)为每个CPU定义变量,其展开宏如下。

__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"

在链接过程中,所有通过DEFINE_PER_CPU宏定义的变量都将链接到一起。在操作系统启动时,Linux 将为该段分配一段内存。 查看编译出的内核镜像可找到.data..percpu段(Linux镜像分析方法请先参考此链接)。

# readelf -S vmlinux
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [21] .data..percpu     PROGBITS         0000000000000000  01000000
       000000000001d000  0000000000000000  WA       0     0     4096

per CPU 变量的访问通过宏get_cpu_var完成 。Linux内核是可抢占的,并且在访问访问 per cpu变量时我们需要知道当前代码运行在哪个CPU核上。 因此,在访问每个cpu变量时,应当不允许抢占当前代码并将其移至另一个CPU。例如,若在获取到 CPU id 为 1 后,该任务被抢占而移动到了 CPU 2上继续运行,这时访问的将仍然是 CPU 1的per cpu 变量。因此,在 get_cpu_var 宏中,首先要调用preempt_disable()函数禁止任务抢占。

// in  include/linux/percpu-defs.h
#define get_cpu_var(var)                        \
(*({                                    \
    preempt_disable();                        \
    this_cpu_ptr(&var);                        \
}))

我比较好奇的是 this_cpu_ptr是如何实现的。内核如何将该变量对应到属于该CPU的 per CPU 变量内存呢?

在初始化时,内核会使用一个数组__per_cpu_offset[cpu]记录每个CPU静态per cpu 变量的偏移地址。在ARM64架构下, OS 启动时将 per cpu 偏移地址写入到 TPDIR_EL1 和 TPDIR_EL2 寄存器中。

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];
}

/* 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。

#define arch_raw_cpu_ptr(ptr) SHIFT_PERCPU_PTR(ptr, __my_cpu_offset)
#define raw_cpu_ptr(ptr)                        \
({                                    \
    __verify_pcpu_ptr(ptr);                        \
    arch_raw_cpu_ptr(ptr);                        \
})

#define this_cpu_ptr(ptr)    raw_cpu_ptr(ptr)

__my_cpu_offset宏即是从当前cpu的tpidr_el1tpidr_el2寄存器中取出此前设置的__per_cpu_offset[cpu]值,实现如下:

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;
}

有时会需要指定某个 CPU 获取其某个 per cpu 变量的地址,通过宏per_cpu_ptr实现,源码如下:

#define SHIFT_PERCPU_PTR(__p, __offset)                    \
    RELOC_HIDE((typeof(*(__p)) __kernel __force *)(__p), (__offset))

#define per_cpu_ptr(ptr, cpu)                        \
({                                    \
    __verify_pcpu_ptr(ptr);                        \
    SHIFT_PERCPU_PTR((ptr), per_cpu_offset((cpu)));            \
})
__user,__kernel,__safe,__force等定义在compiler_type.h头文件中。看到两个很奇怪的现象,一个是只有在__CHECKER__宏打开的情况下,他们的定义才会被实现,否则他们的定义是空的。第二个是它们的attribute的定义,并不是gcc支持的属性。那到底是哪里使用到了呢?原来linux的作者们自己开发了一套编译期代码检查的工具Sparse,可以用于在编译阶段快速发现代码中隐含的问题。[1]
  • address_space 定义了指针能指向的内存的类型,0代表kernel space,1代表user space,2代表设备地址空间,3代表cpu局部的内存空间
  • safe 表示变量可以为空
  • force 表示变量可以强制类型转换

Per CPU 变量的应用

记录每个CPU 的 id 是 per CPU 变量的应用之一。

那么有了 Per CPU变量之后,如何获得当前执行代码的CPU 编号? 内核函数smp_processor_id()用来获取当前 CPU 的 id 。

CPU id 的存储依赖于 per CPU 变量(DEFINE_PER_CPU宏用来定义 cpu_number 变量)。

// 每个CPU的cpuid是放置在cpu_number这个percpu变量中
DEFINE_PER_CPU(int, cpu_number);

在内核初始化时,smp_prepare_cpus()函数执行per_cpu(cpu_number, cpu) = cpu;设定每个核的编号。

// in /arch/arm64/kernel/smp.c
void __init smp_prepare_cpus(unsigned int max_cpus)
{
    const struct cpu_operations *ops;
    int err;
    unsigned int cpu;
    unsigned int this_cpu;

    init_cpu_topology();

    this_cpu = smp_processor_id();

    for_each_possible_cpu(cpu) {
        // 设置 CPU id
        per_cpu(cpu_number, cpu) = cpu;
        // 确定在哪个核上执行的,若是本身则跳过。
        if (cpu == smp_processor_id())
            continue;

        ops = get_cpu_ops(cpu);
        if (!ops)
            continue;

        err = ops->cpu_prepare(cpu);
        if (err)
            continue;

        set_cpu_present(cpu, true);
        numa_store_cpu_info(cpu);
    }
}

smp_processer_id ()函数(定义在 include/linux/smp.h)展开如下。

# define smp_processor_id() __smp_processor_id()
#define __smp_processor_id(x) raw_smp_processor_id(x)

raw_smp_processor_id与处理器架构相关(下例为ARM64)的实现如下,raw_cpu_ptr 获取到 cpu_number 的地址,在解引用得到 cpu id。

#define raw_smp_processor_id() (*raw_cpu_ptr(&cpu_number))

per CPU 变量在多文件下的用法

声明一个 per cpu 变量并在另一个文件中引用,以获取当前 task_struct 为例(x86下 current 宏的实现)。

定义方式如下:

DEFINE_PER_CPU(struct task_struct *, current_task) ____cacheline_aligned =
    &init_task;
EXPORT_PER_CPU_SYMBOL(current_task);

在另一个文件中引用方式如下:

DECLARE_PER_CPU(struct task_struct *, current_task);
static __always_inline struct task_struct *get_current(void)
{
    return this_cpu_read_stable(current_task);
}

#define current get_current()

参考资料

[1]. 聊一聊linux内核中的基础工具库(2) Sparse

声明:一丁点儿|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Linux 内核 | Per CPU 变量


勿在浮沙筑高台,每天进步一丁点儿!