ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SMP/NUMA/PER_CPU

SMP/NUMA/PER_CPU whywhathowPER_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对应的副本。重点区分三个APISLUB fastpath用raw_cpu_ptr这是关键per_cpu_ptr(var, cpu)指定cpu号拿那个远端CPU的per-cpu副本你明确知道要访问CPU5的数据this_cpu_ptr(var)拿当前CPU副本隐含假设不会发生CPU迁移如果中间抢占迁移拿到的指针就失效所以它要求关闭抢占/migrateraw_cpu_ptr(var)原始、裸的per-cpu地址计算不做任何抢占保护只做地址换算。它不保证拿到的指针在后续代码里仍然属于当前CPU。.data..percpu只是静态per-CPU区域DEFINE_PER_CPU编译期就确定符号alloc_percpu用的是动态per-CPU内存池在内核启动时预留一块大的per-CPU内存运行时再从里面切小块分配。内核percpu内存模型静态per-CPU区域.data..percpu源码写DEFINE_PER_CPU(type, var)编译阶段只生成**模板副本0号CPU模板**放在.data..percpu段内核启动setup_per_cpu_areas()把这个模板拷贝N份给每个CPU创建独立副本并且填充全局数组__per_cpu_offset[]特点符号在编译时固定大小不能变不能运行时新增/销毁。适合全局固定的percpu变量。动态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)pcpu分配器在每个CPU对应的percpu区域里各分配一段大小size的内存返回一个__percpu *指针这个指针是模板基地址CPU0副本的起始地址后续访问raw_cpu_ptr(base) base 当前CPU的__per_cpu_offset算出当前CPU对应的副本地址。所以alloc_percpu不是普通kmalloc它一次性给所有CPU各分配一块同大小内存放到各自CPU的percpu虚拟地址区间。静态 vs 动态percpu对比表项目DEFINE_PER_CPU静态percpualloc_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_CPUDEFINE_PER_CPU有硬伤编译期必须知道数量kmem_cache可以动态创建系统里slab缓存数量不确定你不可能编译期写几千个DEFINE_PER_CPU(struct kmem_cache_cpu, xxx)静态percpu不能释放一旦定义内核全程占用内存。slab缓存销毁时kmem_cache_cpu对应的percpu内存要归还驱动模块模块动态加载/卸载模块里的percpu对象必须动态分配。 所以内核设计两套机制全局核心变量DEFINE_PER_CPU静态percpu动态实例slab、驱动alloc_percpu动态percpu结合SLUBstruct kmem_cache_cpu __percpu *cpu_slab串一遍// 创建slab缓存时s-cpu_slaballoc_percpu(structkmem_cache_cpu);alloc_percpu在每个CPU的percpu区域各分配一块sizeof(struct kmem_cache_cpu)的内存返回模板基地址存入s-cpu_slab类型struct kmem_cache_cpu __percpu *fastpath访问c raw_cpu_ptr(s-cpu_slab);raw_cpu_ptr 模板基址 当前CPU的__per_cpu_offsetx86_64从%gs读取得到当前CPU那份kmem_cache_cpu的虚拟地址销毁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变量
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进