# 简介
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_el1
、tpidr_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()
Comments | NOTHING