Home 内核-perCPU(perCPU变量)
Post
Cancel

内核-perCPU(perCPU变量)

在Linux内核中,per-CPU变量是一种特殊类型的变量,用于在多处理器系统中以一种高效且线程安全的方式进行访问。 每个CPU核心都拥有自己的percpu变量副本,这意味着每个CPU核心都可以独立地读取和写入该变量的值,而无需进行同步操作。

主要特点

  1. 线程安全:由于每个CPU核心都有自己的变量副本,因此在不同核心的并发访问之间不存在数据竞争和冲突。每个CPU核心可以独立地读取和写入自己的percpu变量副本,而无需进行锁定或同步操作。

  2. 高效访问:percpu变量在多处理器系统中具有很高的效率。每个CPU核心可以直接访问其本地的变量副本,无需通过其他核心或总线进行通信,从而避免了访问共享变量的开销。

  3. 内存分配:percpu变量的内存分配是可以静态或动态。在内核初始化过程中,为每个CPU核心分配一块连续的内存区域,用于存储其本地的percpu变量副本。每个核心的内存区域大小相同,且在编译时确定。

  4. 访问方式:为了访问percpu变量,内核提供了一组宏函数,如get_cpu_var()put_cpu_var()this_cpu_ptr()。这些宏函数根据当前CPU核心的上下文选择正确的percpu变量副本,并返回对应的指针。

perCPU的特性,使得它常应用于计数器场景,即每个CPU上有独立的计数,然后总的计数只需要把所有副本相加即可,由于每个CPU有独立的副本,可以高效率访问,无需担心并发问题。

内存分配

在多处理器系统中,每个CPU核心都有自己的percpu变量副本,percpu变量的内存分配是通过Linux内核中的特殊内存分配器进行管理,称为percpu area allocator,由mm/percpu.c实现。 该分配器是针对percpu变量的特殊需求进行了优化的。它会为每个CPU核心分配一块连续的内存区域,用于存储该核心的percpu变量副本。 每个核心的内存区域大小相同,并在编译时确定,通常是根据预设的内存分配器页大小进行划分。内核中有相关配置,如CONFIG_SMPCONFIG_HAVE_SETUP_PER_CPU_AREA等选项。 以上这部分是静态分配的部分。还有动态分配的部分,支持在运行中动态管理percpu变量,有类似kmalloc,vmalloc这样的接口。但与常规的内存分配器(如kmalloc()vmalloc())有所不同。 因为它需要确保每个CPU核心都有自己的percpu变量副本,以提供高效的访问和线程安全性。

perCPU的实现,在不同的硬件架构上是不同的,(有差异的)。因为它们的体系结构和指令集不同。Linux 内核在不同架构上提供了相应的实现,以适应各自的硬件特性和架构要求。

静态分配方式

静态分配是一种在内核编译时为perCPU变量分配存储空间的方式。静态分配的原理是,为每个CPU核心分配一块固定大小的内存区域, 并将每个区域的地址保存在一个全局的perCPU变量数组中。每个CPU核心通过其对应的索引来访问自己的存储区域。

主要步骤

内核在全局范围内定义一个perCPU变量数组,数组的大小为最大CPU核心数。例如,DEFINE_PER_CPU(type, varname)宏可以用来定义一个perCPU变量。 perCPU变量在链接内核编译的链接阶段会被统一放到一个特别的段.data.percpu段中,每个CPU都有一个该段的副本。 在初始化阶段,内核将为每个CPU核心设置其对应的perCPU变量。这个过程通常发生在内核启动的早期阶段。找到自己CPU的.data.percpu段,之后访问某个perCPU变量只知道偏移即可。 当CPU核心需要访问perCPU变量时,可以使用this_cpu_ptr(varname)宏来获取对应的指针。这个宏根据当前CPU的编号来索引全局perCPU变量数组,然后返回对应的指针。

每个CPU核心通过访问自己的perCPU变量来读取和修改变量的值,而无需与其他核心进行同步。

还有一个特殊的场景,用户加载模块中的静态perCPU变量,和编译时静态链接的不同,模块静态percpu变量分配了reserved区域内存,在模块释放时,释放perCPU变量。

动态分配方式

运行时完全动态分配的,来源于vmalloc区域,管理也比较复杂。

相关API接口

总的头文件 <linux/percpu.h>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义和申明
DEFINE_PER_CPU(type, name)
DECLARE_PER_CPU(type, name)

//直接获取对应CPU的perCPU变量的指针或值
per_cpu_ptr(ptr, cpu)
this_cpu_ptr(ptr)

/*
  额外加入本地CPU的本地抢占保护,获取时,禁用本地CPU抢占,
  归还时,重新开启抢占。如果perCPU在本地CPU中有多个程序可能会并发访问的,
  就可以使用以下API,临时禁用本地CPU抢占。如果没有本地CPU并发访问的情况,
  直接使用上面的API获取其指针,直接操作即可。
*/ 
get_cpu_var(var)
put_cpu_var(var)
get_cpu_ptr(var)
put_cpu_ptr(var)

//动态分配额外需要使用的。
alloc_percpu(type)
void free_percpu(void __percpu *__pdata);

示例

静态分配方式

1
2
3
4
5
6
7
8
9
10
11
/* 定义一个静态分配的per-CPU变量 */
DEFINE_PER_CPU(int, my_percpu_variable);

 /* 可选,对预定义的所有perCPU进行初始化 */
for_each_possible_cpu(local_cpuid) {    //遍历内核配置中所有可能的CPU。这包括实际的物理CPU核心以及可能的虚拟CPU或逻辑CPU
    int *percpu_var = per_cpu_ptr(&my_percpu_variable, local_cpuid);
    *percpu_var = local_cpuid;  // 设置per-CPU变量的值
}

 // 之后正常使用
 // 也不需要用户释放

动态分配方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 定义一个动态分配的per-CPU变量 */
int __percpu *my_percpu_variable;

 /* 动态分配per-CPU变量 */
 my_percpu_variable = alloc_percpu(int);
 if (!my_percpu_variable) {
     pr_err("Failed to allocate per-CPU variable\n");
     return -ENOMEM;
 }

/* 可选,对分配的所有perCPU进行初始化 */
for_each_possible_cpu(cpu) {
    int *percpu_var = per_cpu_ptr(my_percpu_variable, cpu);
    *percpu_var = cpu;  // 设置per-CPU变量的值
}

// 之后正常使用

/* 释放动态分配的per-CPU变量 */
free_percpu(my_percpu_variable);

更详细的使用示例,还是直接参考内核中的现有代码,参考学习。

参考

linux内核中percpu变量的实现

PERCPU变量实现

This post is licensed under CC BY 4.0 by the author.

内核-atomic(原子变量)

内核-waitqueue(等待队列)