news 2026/10/10 1:57:51

SMP/NUMA/PER_CPU

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SMP/NUMA/PER_CPU
  • why
  • what
  • how

PER_CPU

从上图中我们可以看到,各种源文件中

静态percpu变量

  • 通过DEFINE_PER_CPU的方式,定义了很多percpu变量,这些变量根据vmlinux.lds.S中的相关定义,会被linker聚合在一起,然后放到最终vmlinux文件的,一个名叫.data…percpu的section里。这些变量的地址也是被特殊处理过的,它们从零开始依次递增,这样一个变量的地址,就是该变量在整个vmlinux的.data…percpu区里的位置,有了这个位置,然后再知道某个cpu的percpu内存块的起始地址,就可以很方便的计算出该cpu对应的该变量的运行时内存地址。linux内核在启动时,会先把vmlinux文件加载到内存中,然后根据cpu的个数,为每个cpu都分配一块用于存放percpu变量的内存区域,之后把vmlinux中的.data…percpu section里的内容,拷贝到各个cpu的percpu内存块的static区域里,最后将各percpu内存块的起始地址放到对应cpu的gs寄存器里。到这里有关percpu变量的初始化工作就已经结束了。

  • 当我们在访问percpu变量时,只需要将gs寄存器里的地址,加上我们想要访问的percpu变量的地址,就能得到在该cpu上,该percpu变量真实的内存地址。 有了这个地址,我们就可以方便的操作这个percpu变量了。

  • 上图中重点描述的是那些,在内核编译期就已经确定的percpu变量,这些变量是静态的,是不会随着时间的推移而动态的增加或减少的,所以它们在内核初始化时,就直接被拷贝到了各个percpu内存块的static区。

  • 除了这种静态percpu变量,还有另外两种percpu变量, 其中一种是内核模块中的静态percpu变量,它虽然也是在编译期就能确定的,但由于内核模块动态加载的特性,它不是完全静态的,内核为这种percpu变量在percpu内存块中单独开辟了一个区域,叫reserved区,当内核模块被加载到内存时,其静态percpu变量就会在这个区域分配内存。

动态percpu变量

另外一种percpu变量就是纯动态的percpu变量,它是在运行时动态分配的,它使用的内存是上图中的dynamic区。


例子: raw_cpu_ptr(s->cpu_slab)

raw_cpu_ptr(s->cpu_slab)不是“查询当前CPU编号再查表”,per-cpu变量本身设计就是:给一个per-cpu变量的偏移,算出当前正在执行代码的CPU对应的那份副本地址。
s->cpu_slab是per-cpu 指针变量,每个CPU都有独立一份kmem_cache_cpu*;raw_cpu_ptr拿的就是此刻正在跑这条指令的CPU对应的副本。

重点区分三个API(SLUB fastpath用raw_cpu_ptr,这是关键)

  1. per_cpu_ptr(var, cpu):指定cpu号,拿那个远端CPU的per-cpu副本(你明确知道要访问CPU5的数据)
  2. this_cpu_ptr(var):拿当前CPU副本,隐含假设:不会发生CPU迁移;如果中间抢占+迁移,拿到的指针就失效,所以它要求关闭抢占/migrate
  3. raw_cpu_ptr(var):原始、裸的per-cpu地址计算,不做任何抢占保护,只做地址换算。它不保证拿到的指针在后续代码里仍然属于当前CPU。

.data..percpu只是静态per-CPU区域(DEFINE_PER_CPU,编译期就确定符号);
alloc_percpu用的是动态per-CPU内存池,在内核启动时预留一块大的per-CPU内存,运行时再从里面切小块分配。

内核percpu内存模型:

  1. 静态per-CPU区域:.data..percpu

    • 源码写DEFINE_PER_CPU(type, var)
    • 编译阶段:只生成**模板副本(0号CPU模板)**放在.data..percpu段
    • 内核启动setup_per_cpu_areas():
      把这个模板拷贝N份,给每个CPU创建独立副本,并且填充全局数组__per_cpu_offset[]
    • 特点:符号在编译时固定,大小不能变,不能运行时新增/销毁。适合全局固定的percpu变量。
  2. 动态per-CPU内存池:percpu chunk / pcpu内存管理器

    • 内核启动阶段,会预留一大片物理内存专门作为percpu动态池(主池,还有可选的备用池)
    • alloc_percpu()/__alloc_percpu()就是从这个预留的大percpu池里,切一块内存出来,给调用者使用
    • 没有编译期符号,没有.data..percpu段条目;运行时分配、也可以free_percpu释放
    • SLUB的s->cpu_slab就是典型例子:alloc_percpu(struct kmem_cache_cpu),每个kmem_cache实例创建时才分配,销毁时释放。

动态percpu底层原理

不管静态还是动态percpu,内核percpu内存组织方式是按CPU分块:

[CPU0区域][CPU1区域][CPU2区域]...[CPUn区域]

每一个CPU的区域是一块连续虚拟内存;同一个CPU的所有percpu对象(静态+动态)都放在这个CPU对应的区域内。

  • 静态变量:预先塞在每个CPU区域的靠前部分(.data..percpu拷贝过去的)
  • 动态池:每个CPU区域剩下的一大段空闲空间,交给pcpu分配器管理(类似kmalloc,但按percpu域管理)

当调用alloc_percpu(size):

  1. pcpu分配器在每个CPU对应的percpu区域里,各分配一段大小=size的内存;
  2. 返回一个__percpu *指针,这个指针是模板基地址(CPU0副本的起始地址);
  3. 后续访问:raw_cpu_ptr(base)= base + 当前CPU的__per_cpu_offset,算出当前CPU对应的副本地址。

所以:alloc_percpu不是普通kmalloc,它一次性给所有CPU各分配一块同大小内存,放到各自CPU的percpu虚拟地址区间。

静态 vs 动态percpu对比表

项目DEFINE_PER_CPU(静态percpu)alloc_percpu(动态percpu)
内存来源.data..percpu段,内核启动拷贝模板内核启动预先预留的percpu动态内存池
生命周期内核全程,不可释放运行时alloc / free_percpu可释放
符号编译期有全局符号无编译期符号,只有运行时指针
适用场景全局固定变量模块/结构体成员,按需创建(SLUB cpu_slab、驱动)
访问方式raw_cpu_ptr(&var)raw_cpu_ptr(base_ptr),base是alloc返回的__percpu *

为什么需要动态percpu?为什么不能全部用DEFINE_PER_CPU?

DEFINE_PER_CPU有硬伤:

  1. 编译期必须知道数量:kmem_cache可以动态创建,系统里slab缓存数量不确定,你不可能编译期写几千个DEFINE_PER_CPU(struct kmem_cache_cpu, xxx);
  2. 静态percpu不能释放:一旦定义,内核全程占用内存。slab缓存销毁时,kmem_cache_cpu对应的percpu内存要归还;
  3. 驱动模块:模块动态加载/卸载,模块里的percpu对象必须动态分配。

👉 所以内核设计两套机制:

  • 全局核心变量:DEFINE_PER_CPU静态percpu
  • 动态实例(slab、驱动):alloc_percpu动态percpu

结合SLUBstruct kmem_cache_cpu __percpu *cpu_slab串一遍

// 创建slab缓存时:s->cpu_slab=alloc_percpu(structkmem_cache_cpu);
  1. alloc_percpu在每个CPU的percpu区域,各分配一块sizeof(struct kmem_cache_cpu)的内存;
  2. 返回模板基地址,存入s->cpu_slab(类型struct kmem_cache_cpu __percpu *);
  3. fastpath访问:c = raw_cpu_ptr(s->cpu_slab);
    • raw_cpu_ptr= 模板基址 + 当前CPU的__per_cpu_offset(x86_64从%gs读取)
    • 得到当前CPU那份kmem_cache_cpu的虚拟地址;
  4. 销毁slab缓存:free_percpu(s->cpu_slab),归还内存到动态percpu池。

总结

  • alloc_percpu 内部分配的是percpu虚拟地址空间里的连续区域,由pcpu专门的内存管理器维护,不是普通kmalloc。__per_cpu_offset统一完成偏移映射。

  • 静态、动态percpu全部放在percpu虚拟地址区域,所以都能用raw_cpu_ptr+ GS偏移寻址。GS指向的是整个当前CPU的percpu区域基址,里面同时放静态变量 + 动态分配的对象。

  • 参考文章

    • 一张图看懂linux内核中percpu变量的实现
    • Linux内核同步机制之(二):Per-CPU变量
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 1:56:40

休闲食品定制加工厂避坑挑选指南:福建实力参考

休闲食品定制加工厂怎么挑选?很多经销商、餐饮茶饮品牌、酒店和贸易商在采购时都会遇到这个难题。下面围绕三个高频问题,逐一说明挑选思路,并结合福建龙海一家深耕30余年的休闲食品定制厂家旭源食品的情况,提供实际参考。 Q1:挑选…

作者头像 李华