per-CPU变量

为什么需要per-CPU变量

假设系统中有4个cpu, 同时有一个变量在各个CPU之间是共享的,每个cpu都有访问该变量的权限。
这里写图片描述

当cpu1在改变变量v的值的时候,cpu2也需要改变变量v的值。这时候就会导致变量v的值不正确。这时候机智的你就会说,在cpu1访问变量v的时候可以使用原子操作加锁,cpu2访问变量v的时候需要等待。可是机智的是否考虑过加锁对性能的影响,原子操作对cpu是极耗cpu的。

再考虑一种情况,现在高速的cpu都带有高速缓冲cache。它介于cpu和主存之间,主要作用是加快cpu的访问速度。因为主存的访问速度相比cpu读写比较慢,在之间引入cache之后,当CPU调用大量数据时,就可避开内存直接从缓存中调用,从而加快读取速度。
这里写图片描述

比如cpu1对变量v操作子后,变量v的值就发生了变化。而cpu2, cpu3, cpu4的cache中的值还是以前的值,所以这时候就需要将cpu2, cpu3, cpu4的cache中的值变为无效的,当cpu2读取变量v的时候就需要从内存中读取v。所以当某一个cpu对共享数据v做操作后,比较对其余的cache做无效操作,这也是对性能有所损耗的。

所以,就引入了per-cpu变量。

什么是per-CPU变量

per-CPU变量是linux系统一个非常有趣的特性,它为系统中的每个处理器都分配了该变量的副本。这样做的好处是,在多处理器系统中,当处理器操作属于它的变量副本时,不需要考虑与其他处理器的竞争的问题,同时该副本还可以充分利用处理器本地的硬件缓冲cache来提供访问速度。
这里写图片描述

per-CPU按照存储变量的空间来源分为静态per-CPU变量和动态per-CPU变量,前者的存储空间是在代码编译时静态分配的,而后者的存储空间则是在代码的执行期间动态分配的。

静态per-CPU变量声明和定义

声明DECLARE_PER_CPU宏:

<include/linux/percpu-defs.h>
----------------------------------------------------------------
#define DECLARE_PER_CPU(type, name)                 \
    DECLARE_PER_CPU_SECTION(type, name, "")

#define DECLARE_PER_CPU_SECTION(type, name, sec)            \
    extern __PCPU_ATTRS(sec) __typeof__(type) name

#define __PCPU_ATTRS(sec)                       \
    __percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
    PER_CPU_ATTRIBUTES
<include/asm-generic/percpu.h>
-----------------------------------------------------
#ifndef PER_CPU_BASE_SECTION
#ifdef CONFIG_SMP
#define PER_CPU_BASE_SECTION ".data..percpu"
#else
#define PER_CPU_BASE_SECTION ".data"
#endif
#endif

对上的宏定义DECLARE_PER_CPU使用例子: DECLARE_PER_CPU(int, val)来详细说明。

DECLARE_PER_CPUT(int, val)
 -> DECLARE_PER_CPU_SECTION(int, val, "")
    -> extern __PCPU_ATTRS("") __typeof__(int) val
       -> extern __percpu __attribute__((section(".data..percpu"))) int val

从上面的分析可以看出,该宏在源代码中声明了__percpu int val变量,该变量放在一个名为”.data..percpu”的section中。

定义DEFINE_PER_CPU宏:

<include/linux/percpu-defs.h>
----------------------------------------------------------------
#define DEFINE_PER_CPU(type, name)                  \
    DEFINE_PER_CPU_SECTION(type, name, "")

#define DEFINE_PER_CPU_SECTION(type, name, sec)             \
    __PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES            \
    __typeof__(type) name

#ifndef PER_CPU_DEF_ATTRIBUTES
#define PER_CPU_DEF_ATTRIBUTES
#endif

对以上DEFINE_PER_CPU依然使用DEFINE_PER_CPU(int, val)做举例说明:

DEFINE_PER_CPU(int, val)
-> DEFINE_PER_CPU_SECTION(int, val, "")
   ->__PCPU_ATTRS("") __typeof__(int) val
     -> __percpu __attribute__((section(".data..percpu"))) int val

可以看到,相比与DECLARE_PER_CPU,DEFINE_PER_CPU只是去掉了变量前面的extern, 所以使用DEFINE_PER_CPU(int,val)将会在源码中定义一个变量:

__percpu __attribute__((section(".data..percpu"))) int val

以下是per-CPU所有的形式的声明和定义:

声明和定义解释
DECALRE_PER_CPU(type, name)/DEFINE_PER_CPU(type, name)普通的per-CPU声明和定义
DECLARE_PER_CPU_FIRST(type, name)/DEFINE_PER_CPU_FIRST(type, name)该per-CPU变量会在整个serction的最前面,所谓的first
DECLARE_PER_CPU_SHARED_ALIGNED(type, name)/DEFINE_PER_CPU_SHARED_ALIGNED(type, name)该per-CPU在SMP系统下会对齐到cache line,在UP系统下不需要对齐
DECLARE_PER_CPU_ALIGNED(type, name)/DEFINE_PER_CPU_ALIGNED(type, name)在SMP和UP系统都对齐到cache line
DECLARE_PER_CPU_PAGE_ALIGNED(type, name)/DEFINE_PER_CPU_PAGE_ALIGNED(type, name)该per-CPU变量必须页对齐
DECLARE_PER_CPU_READ_MOSTLY(type, name)/DEFINE_PER_CPU_READ_MOSTLY(type, name)该per-CPU变量必须是read mostly

静态per-CPU变量的链接脚本

在上一节per-CPU变量的声明和定义中,可以看到最后的变量都是存在一个”.data..percpu”段中。

. = ALIGN((1 << 12)); 
.data..percpu : AT(ADDR(.data..percpu) - 0) 
{
    __per_cpu_load = .;
    __per_cpu_start = .;
     *(.data..percpu..first) . = ALIGN((1 << 12));
     *(.data..percpu..page_aligned) . = ALIGN(64);     
     *(.data..percpu..read_mostly) . = ALIGN(64); 
     *(.data..percpu) 
     *(.data..percpu..shared_aligned) 
    __per_cpu_end = .; 
}

可见,内核在编译链接的时候会把所有静态定义的per-CPU变量统一放到”.data..percpu”section中。链接器生成__per_cpu_start和__per_cpu_end两个变量表示该section的起始和结束地址。

动态分配per-CPU变量

  • 分配函数
#define alloc_percpu(type)                      \
    (typeof(type) __percpu *)__alloc_percpu(sizeof(type),       \
                        __alignof__(type))

根据类型type,分配per-CPU变量

  • 释放函数
void free_percpu(void __percpu *ptr)

释放ptr所指向的per-CPU变量。

使用静态per-CPU变量

因为per-CPU不能像一般的变量那样访问,必须使用内核提供的函数:

#define get_cpu_var(var)                        \
(*({                                    \
    preempt_disable();                      \
    this_cpu_ptr(&var);                     \
}))

#define put_cpu_var(var)                        \
do {                                    \
    (void)&(var);                           \
    preempt_enable();                       \
} while (0)

因为在get_cpu_var函数中关掉了抢占,所以在put_cpu_var中需要开启抢占。显然get函数和put函数需要匹配使用。
机智的你可能会问,为什么还需要关闭抢占,因为对于per-CPU来说已经是单处理器了。但是机智的你没有想到的是,在cpu访问per-CPU的时候,突然系统发生了一次紧急抢占,这时候cpu还在处理per-CPU变量,一旦被抢占了cpu资源,可能当前进程会换出处理器。所以关闭抢走还是必要的。

如果需要访问其他处理器的副本,可以使用函数per_cpu(var, cpu)

#define per_cpu(var, cpu)   (*per_cpu_ptr(&(var), cpu))

使用动态per-CPU变量

#define get_cpu_ptr(var)                        \
({                                  \
    preempt_disable();                      \
    this_cpu_ptr(var);                      \
})

#define put_cpu_ptr(var)                        \
do {                                    \
    (void)(var);                            \
    preempt_enable();                       \
} while (0)

#define per_cpu_ptr(ptr, cpu)   ({ (void)(cpu); VERIFY_PERCPU_PTR(ptr); })

以上get_cpu_ptr和put_cpu_ptr是在有抢占的情况下,需要关闭抢占使用。
而per_cpu_ptr(ptr, cpu)是根据per cpu变量的地址和cpu number,返回指定CPU number上该per cpu变量的地址。

<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`数组存储了每个CPUper-CPU区域相对于原始数据的偏移量。通过这个偏移量,我们可以计算出每个CPU变量的实际地址。###三、关键数据结构####1. `__per_cpu_offset`数组这是一个全局数组,存储了每个CPUper-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访问**:如果需要访问其他CPUper-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();//获取指定CPUper-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. **对齐缓存行**:为了避免不同CPUper-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、付费专栏及课程。

余额充值