每CPU变量

最好的同步技术是把设计不需要同步的临界资源放在首位,这是一种思维方法,因为每一种显式的同步原语都有不容忽视的性能开销。

最简单也是最重要的同步技术包括把内核变量或数据结构声明为每CPU变量(per-cpu variable)。每CPU变量主要是数据结构的数组,系统的每个CPU对应数组的一个元素。

一个CPU不应该访问与其他CPU对应的数组元素,另外,它可以随意读或修改它自己的元素而不用担心出现竞争条件,因为它是唯一有资格这么做的CPU。但是,这也意味着每CPU变量基本上只能在特殊情况下使用,也就是当它确定在系统的CPU上的数据在逻辑上是独立的时候。

每CPU的数组元素在主存中被排列以使每个数据结构存放在硬件高速缓存的不同行,因此,对每CPU数组的并发访问不会导致高速缓存行的窃用和失效(这种操作会带来昂贵的系统开销)。

虽然每CPU变量为来自不同CPU的并发访问提供保护,但对来自异步函数(中断处理程序和可延迟函数)的访问不提供保护,在这种情况下需要另外的同步技术。

此外,在单处理器和多处理器系统中,内核抢占都可能使每CPU变量产生竞争条件。总的原则是内核控制路径应该在禁用抢占的情况下访问每CPU变量。因为当一个内核控制路径获得了它的每CPU变量本地副本的地址,然后它因被抢占而转移到另外一个CPU上,但仍然引用原来CPU元素的地址,这是非常危险的。

本博,我们介绍一些每CPU方面有用的宏。

1 DEFINE_PER_CPU(type, name)


静态分配一个每CPU数组,数组名为name,结构类型为type:
#define DECLARE_PER_CPU(type, name) extern __typeof__(type) per_cpu__##name

2 per_cpu(name, cpu)


获得一个为用DEFINE_PER_CPU宏为CPU选择的一个每CPU数组元素,CPU由cpu指定,数组名称为name:
#define per_cpu(var, cpu)     (*((void)(cpu), &per_cpu__##var))

3 __get_cpu_var(name)


选择每CPU数组name的本地CPU元素:
#define __get_cpu_var(var)            per_cpu__##var

4 get_cpu_var(name)


先禁用内核抢占,然后在每CPU数组name中,为本地CPU选择元素:
#define get_cpu_var(var) (*({ preempt_disable(); &__get_cpu_var(var); }))

5 put_cpu_var(name)


启用内核抢占(不使用name):
#define put_cpu_var(var) preempt_enable()

6 alloc_percpu(type)


动态分配type类型数据结构的每CPU数组,并返回它的地址:
#define alloc_percpu(type)    ((type *)(__alloc_percpu(sizeof(type))))
static inline void *__alloc_percpu(size_t size)
{
    void *ret = kmalloc(size, GFP_KERNEL);
    if (ret)
        memset(ret, 0, size);
    return ret;
}

7 free_percpu(pointer)


释放动态分配的每CPU数组,pointer指示其地址:
static inline void free_percpu(const void *ptr)
{   
    kfree(ptr);
}

8 per_cpu_ptr(pointer, cpu)


返回每CPU数组中与cpu对应CPU元素地址,pointer给出数组地址:
#define per_cpu_ptr(ptr, cpu) ({ (void)(cpu); (ptr); })

<think>我们正在讨论per-CPU变量的实现机制。根据引用[4]中的示例代码,我们可以看到per-CPU变量的基本使用方法:使用`alloc_percpu`分配,使用`per_cpu_ptr`获取特定CPU的指针,并使用`get_cpu`和`put_cpu`来防止抢占。现在,我们需要深入其实现原理。 ###一、per-CPU变量的设计目标per-CPU变量主要用于解决多核环境下对共享数据的访问竞争问题。每个CPU都有自己独立的变量副本,这样在访问时就不需要加锁,提高了性能[^2]。 ###二、实现原理####1.数据存储布局在编译时,per-CPU变量被放在一个特殊的段(section)中。在Linux内核中,这个段通常命名为`.data..percpu`(在x86架构中)[^1]。当系统启动时,为每个CPU复制这个段的内容,使得每个CPU都有自己独立的数据副本。 ####2.动态分配与静态声明-**静态声明**:使用`DEFINE_PER_CPU(type,name)`宏,例如:```cDEFINE_PER_CPU(int, my_percpu_var);```这会在`.data..percpu`段中定义一个名为`my_percpu_var`的变量,每个CPU都有一个独立的副本。- **动态分配**:使用`alloc_percpu`函数,如引用[4]中的示例:```cptr= alloc_percpu(int);```####3.访问机制-**获取当前CPU变量**:使用`get_cpu_var`和`put_cpu_var`宏。这两个宏会禁用内核抢占,确保在当前CPU上操作的原子性。```cint*val= get_cpu_var(my_percpu_var);*val =10;put_cpu_var(my_percpu_var);``` -**访问指定CPU变量**:使用`per_cpu_ptr`宏,如引用[4]所示:```cthe_ptr= per_cpu_ptr(ptr,cpu);```####4.初始化过程在系统启动时,会调用`setup_per_cpu_areas`函数(在x86架构中位于`arch/x86/kernel/setup_percpu.c`)。该函数会为每个CPU分配内存,并将初始的per-CPU数据(即`.data..percpu`段的内容)复制到每个CPU的私有区域中。引用[3]中的代码片段展示了这一过程: ```cfor (i =0; i< NR_CPUS;i++,ptr +=size){__per_cpu_offset[i] =ptr -__per_cpu_start;memcpy(ptr,__per_cpu_start,__per_cpu_end -__per_cpu_start);} ```这里,`__per_cpu_offset`数组存储了每个CPU的per-CPU区域相对于原始数据的偏移量。通过这个偏移量,我们可以计算出每个CPU变量的实际地址。###三、关键数据结构####1. `__per_cpu_offset`数组这是一个全局数组,存储了每个CPU的per-CPU区域的基地址相对于原始`.data..percpu`段起始地址的偏移量。通过这个偏移量,我们可以得到某个CPU上某个per-CPU变量的地址: ```c#define per_cpu_ptr(ptr,cpu)({ \(typeof(ptr)) ((char*)(ptr)+ __per_cpu_offset[cpu]); \}) ```####2.per-CPU变量的内存布局```mermaidgraphLRA[原始.data..percpu段] -->|包含初始数据| B(CPU0的per-CPU区域)A-->|复制| C(CPU1的per-CPU区域)A -->|复制| D(CPU2的per-CPU区域)A-->|...|E(其他CPU) ```###四、使用场景与性能优势1.**计数器**:如网络子系统中的统计计数器,每个CPU独立更新,减少缓存行竞争。2.**缓存**:如slab分配器的每CPU缓存,提高内存分配效率。 3. **避免锁竞争**:当数据主要被一个CPU访问时,使用per-CPU变量可以避免锁的开销。 ###五、注意事项1. **抢占控制**:在访问per-CPU变量时,必须使用`get_cpu`和`put_cpu`来防止进程被抢占到其他CPU上,否则可能导致数据不一致。2.**跨CPU访问**:如果需要访问其他CPU的per-CPU变量,必须确保该CPU已经停止运行(例如在热插拔时),否则需要额外的同步机制。 3. **内存占用**:per-CPU变量会为每个CPU都分配一份副本,因此对于大型数据结构,需要权衡内存开销。 ###六、示例代码解析(基于引用[4]) ```c#include <linux/module.h> #include<linux/kernel.h>#include <linux/init.h> #include<asm/io.h>#include <asm/uaccess.h> #include<linux/percpu.h> staticint *ptr;static int__inithello_init(void){int cpu;int *the_ptr;//动态分配一个int类型的per-CPU变量ptr= alloc_percpu(int);//遍历所有可能的CPUfor_each_possible_cpu(cpu){//禁止抢占,并返回当前CPU的ID(同时确保不会被迁移到其他CPU)get_cpu();//获取指定CPU的per-CPU变量指针the_ptr =per_cpu_ptr(ptr, cpu);//给该CPU的副本赋值*the_ptr= cpu;printk("val= %d\n", *the_ptr);//允许抢占put_cpu();}return0;} staticvoid __exit hello_exit(void){//释放per-CPU变量free_percpu(ptr); }module_init(hello_init);module_exit(hello_exit);``` 在这个示例中:-`alloc_percpu`为每个CPU分配了一个`int`大小的内存。 -`for_each_possible_cpu`遍历所有可能的CPU。-在循环中,通过`get_cpu`禁止抢占,然后使用`per_cpu_ptr`获取当前CPU变量指针,并赋值。-最后使用`free_percpu`释放内存。###七、实现细节####1.静态per-CPU变量的实现以`DEFINE_PER_CPU(int, my_var)`为例,其展开后大致如下:```c__attribute__((section(".data..percpu"))) int__percpu__my_var; ```这样,`__percpu__my_var`就被放置在`.data..percpu`段中。 ####2.动态分配的实现`alloc_percpu`函数最终会调用`__alloc_percpu`,该函数会为每个CPU分配指定大小的内存,并返回一个指针。这个指针实际上指向原始数据(即第一个CPU的副本),但通过`per_cpu_ptr`访问时,会加上对应CPU的偏移量。 ###八、跨架构支持不同架构的per-CPU实现可能有所不同,主要体现在:1.**偏移量数组**:`__per_cpu_offset`数组的定义和初始化位置不同。2.**段名称**:如x86使用`.data..percpu`,而ARM可能使用不同的段名。3.**访问指令**:某些架构可能提供专门的指令来访问per-CPU变量(如x86的`%gs`段寄存器)。 ###九、性能优化技巧1. **对齐缓存行**:为了避免不同CPU的per-CPU变量共享同一缓存行(导致伪共享),可以使用`____cacheline_aligned_in_smp`属性进行对齐。```cDEFINE_PER_CPU_ALIGNED(int,my_var);``` 2. **使用局部变量**:在长时间操作per-CPU变量时,可以先将其复制到局部变量,操作完再写回,减少禁用抢占的时间。 ###十、相关函数总结|函数/宏|作用| |---------|------|| `DEFINE_PER_CPU(type, name)` |静态定义一个per-CPU变量| |`alloc_percpu(type)`|动态分配一个per-CPU变量 || `free_percpu(ptr)`|释放动态分配的per-CPU变量 || `get_cpu_var(var)`|获取当前CPU变量指针(并禁用抢占) || `put_cpu_var(var)`|释放变量(并启用抢占) || `per_cpu_ptr(ptr,cpu)`|获取指定CPU变量指针| ###相关问题1. per-CPU变量在SMP和NUMA架构中有什么不同的表现?2.如何避免per-CPU变量的伪共享(false sharing)问题? 3. per-CPU变量在中断上下文中如何使用?4.动态分配的per-CPU变量在内存管理上有何特点? 5.在多核系统中,per-CPU变量如何与RCU机制配合使用?[^2]</think>### Linux内核中per-CPU变量的实现机制 per-CPU变量是Linux内核中用于**高效处理多核共享数据**的关键技术,其核心思想是为每个CPU创建变量的独立副本,从而**避免多核访问时的锁竞争**。以下是其实现原理和方式: --- #### 一、设计目标与核心优势 1. **零锁并发** - 每个CPU独立访问自己的变量副本,无需锁机制[^2] - 解决多核缓存一致性导致的性能瓶颈 2. **缓存局部性优化** - 变量副本位于CPU本地缓存,减少缓存行冲突 3. **动态扩展性** - 支持CPU热插拔,动态调整副本数量 --- #### 二、实现原理图解 ```mermaid graph TD A[原始数据段 .data..percpu] --> B(CPU0副本) A --> C(CPU1副本) A --> D(CPU2副本) B --> E[通过__per_cpu_offset[0]定位] C --> F[通过__per_cpu_offset[1]定位] D --> G[通过__per_cpu_offset[2]定位] ``` --- #### 三、关键技术实现 ##### 1. 内存布局管理 - **专用数据段** 所有per-CPU变量编译时放入`.data..percpu`段(x86架构)[^1] ```c #define PER_CPU_BASE_SECTION ".data..percpu" ``` - **启动时复制** 系统初始化时为每个CPU创建独立副本: ```c for (i = 0; i < NR_CPUS; i++, ptr += size) { __per_cpu_offset[i] = ptr - __per_cpu_start; memcpy(ptr, __per_cpu_start, __per_cpu_end - __per_cpu_start); } // 引用[3] ``` ##### 2. 访问机制 - **偏移量数组** 全局数组`__per_cpu_offset[]`存储每个CPU副本的基地址偏移 ```c extern unsigned long __per_cpu_offset[NR_CPUS]; ``` - **地址计算宏** 获取指定CPU变量地址: ```c #define per_cpu_ptr(ptr, cpu) \ ((typeof(ptr))((char*)(ptr) + __per_cpu_offset[cpu])) ``` ##### 3. 安全访问控制 - **抢占保护** 使用`get_cpu()`/`put_cpu()`确保操作原子性: ```c get_cpu(); // 禁用抢占 *per_cpu_ptr(var, smp_processor_id()) = value; put_cpu(); // 启用抢占 ``` --- #### 四、使用方式示例 ##### 1. 动态分配 ```c // 分配per-CPU整型数组 int __percpu *arr = alloc_percpu(int); // 获取CPU1的副本指针 int *cpu1_ptr = per_cpu_ptr(arr, 1); *cpu1_ptr = 100; // 安全写入 ``` ##### 2. 静态声明 ```c // 声明每CPU变量 DEFINE_PER_CPU(int, counter); // 安全访问当前CPU副本 get_cpu_var(counter)++; put_cpu_var(counter); ``` ##### 3. 遍历所有CPU ```c for_each_possible_cpu(cpu) { int *ptr = per_cpu_ptr(arr, cpu); printk("CPU%d: %d\n", cpu, *ptr); } ``` --- #### 五、性能优化技术 1. **缓存对齐** 使用`____cacheline_aligned`避免伪共享 ```c DEFINE_PER_CPU_ALIGNED(int, cacheline_var); ``` 2. **热插拔支持** 通过`cpuhp_setup_state()`注册回调函数 ```c cpuhp_setup_state(CPUHP_AP_ONLINE, "mymodule:online", my_online_cb, my_offline_cb); ``` --- #### 六、典型应用场景 1. **网络收发包统计** ```c struct net_device_stats __percpu *dev_stats; ``` 2. **内存分配器缓存** SLAB/SLUB分配器的每CPU缓存 3. **任务调度统计** 内核调度器的运行队列统计 --- ### 相关问题 1. per-CPU变量如何保证跨CPU访问的安全性? 2. 在NUMA架构中per-CPU变量实现有何不同? 3. 如何动态调整per-CPU变量的大小? 4. per-CPU变量与RCU机制如何协同工作?[^2] 5. 用户态程序能否直接访问内核的per-CPU变量
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值