[TOC]
mm/percpu.c Per-CPU Variables Management Per-CPU数据管理的核心实现 历史与背景 这项技术是为了解决什么特定问题而诞生的? 这项技术是为了从根本上解决在多核(SMP)系统中并发访问共享数据所带来的性能瓶颈 而诞生的。
锁争用(Lock Contention) :在多核系统中,如果多个CPU核心频繁地更新同一个全局变量(例如,一个网络数据包统计计数器),它们必须使用锁(如自旋锁)来保护这个变量,以避免数据竞争。当核心数量增加时,对这个锁的争夺会变得非常激烈,导致CPU花费大量时间在等待锁上,而不是执行实际工作,从而严重限制了系统的扩展性。
缓存一致性开销(Cache Coherency Overhead) :即使不使用锁(例如使用原子操作),也会有性能问题。当一个CPU核心修改了共享变量,它所在的缓存行(Cache Line)会被标记为“脏”(Modified)。根据缓存一致性协议(如MESI),其他CPU核心上该缓存行的副本必须被置为“无效”(Invalidated)。当其他CPU也想访问这个变量时,就必须从修改过的那个核心的缓存或主存中重新加载数据。这种缓存行在多个CPU核心之间来回传递的现象被称为“缓存行弹跳”(Cache Bouncing)或“伪共享”(False Sharing),会消耗大量的总线带宽和时间。
percpu
机制通过为系统中的每个CPU核心提供一个变量的私有副本 来解决上述所有问题。每个CPU只访问自己的副本,因此完全不需要锁,也杜绝了缓存行弹跳,实现了真正的无锁并发。
它的发展经历了哪些重要的里程碑或版本迭代? Linux的Per-CPU实现经历了几个阶段的演进,变得越来越高效和灵活。
早期宏定义阶段 :最原始的方法是使用一个简单的数组,并通过一个宏来访问,如 DEFINE_PER_CPU(type, name)
实际上定义了 type name[NR_CPUS]
,访问时使用 per_cpu(name, smp_processor_id())
。这种方式的主要问题是无法解决伪共享 ,因为数组中相邻CPU的元素很可能位于同一个缓存行内。
对齐与填充 :为了解决伪共享,后续的实现为每个CPU的副本增加了填充(Padding),确保每个副本都独占一个或多个缓存行。
专有分配器(Chunk Allocator) :现代的 percpu
机制实现了一个高度优化的专有内存分配器,位于 mm/percpu.c
。它不再是为每个per-cpu变量单独分配内存,而是将许多per-cpu变量组织到大的**内存块(Chunks)**中。每个块被划分为多个单元(Units),每个CPU一个单元。这种方式:
内存效率更高 :减少了因对齐和填充造成的内存碎片。
寻址更快 :通过巧妙的地址计算,特别是利用段寄存器(如x86上的gs
或fs
),可以在一条指令内完成对当前CPU私有数据的访问,几乎没有运行时开销。
动态分配的支持 :除了编译时静态定义的per-cpu变量(DEFINE_PER_CPU
),该框架也加入了对动态分配的支持(alloc_percpu
, free_percpu
),使得驱动程序和模块可以在运行时根据需要创建和销毁per-cpu变量。
目前该技术的社区活跃度和主流应用情况如何? percpu
机制是Linux内核实现高性能和高扩展性的基石之一,是一项极其成熟、稳定且被广泛应用的核心技术。
社区活跃度 :作为内核的核心基础设施,其代码非常稳定。相关的改动通常是为了支持新的体系结构、进行性能微调或内存优化。
主流应用 :它被内核的各个子系统广泛使用,特别是性能敏感的领域:
调度器 :每个CPU的运行队列(struct rq
)就是per-cpu变量。
网络栈 :用于统计收发包数量、错误等。
文件系统 :用于缓存和计数器。
内存管理 :用于管理per-cpu的页面缓存。
中断和定时器 :每个CPU都有自己的本地定时器和中断处理数据。
核心原理与设计 它的核心工作原理是什么? mm/percpu.c
的核心是实现了一个特殊的内存分配器,它以“块(Chunk)”为单位进行管理。
静态变量布局 :对于静态定义的per-cpu变量(使用DEFINE_PER_CPU
),链接器会将它们收集到一个特殊的.data..percpu
段中。内核启动时,会为这个段分配第一个per-cpu内存块(first chunk),并将所有静态变量的副本按CPU依次拷贝到每个CPU对应的单元中。
动态变量分配 :当调用alloc_percpu()
时,分配器会尝试在现有的内存块中寻找足够的空间。如果找不到,它会从页分配器申请新的物理页来创建一个新的内存块。
快速寻址 :这是percpu
机制性能的关键。在x86-64等架构上,内核会将段寄存器(如%gs
)指向当前CPU的per-cpu区域的基地址。因此,访问一个静态per-cpu变量(例如 my_var
)的汇编指令可以是 movq %gs:my_var_offset, %rax
。这只是一条普通的内存访问指令,速度极快,完全避免了查找当前CPU ID再做数组索引的开销。
安全访问API :内核提供了get_cpu_var()
和put_cpu_var()
等宏。它们的作用是在访问per-cpu变量之前禁用抢占 ,在访问结束后再恢复抢占。这是至关重要的,因为如果一个任务在访问per-cpu变量的过程中被抢占,并被调度到另一个CPU上运行,那么它后续的访问就会访问到新CPU的错误数据。禁用抢占确保了在访问期间,任务不会被迁移到其他CPU。
它的主要优势体现在哪些方面?
极致的性能 :无锁、无缓存争用,是解决SMP扩展性问题的最佳方案。
高扩展性 :系统性能不会因为CPU核心数量的增加而在线性更新场景下出现下降。
API简洁 :对于使用者来说,其复杂性被很好地隐藏了,使用起来就像访问普通变量一样简单。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
内存开销 :为了避免伪共享,percpu
分配器会进行缓存行对齐,这会消耗比实际数据更多的内存。总内存消耗是 (数据大小 + 填充) * CPU核心数
,对于大型数据结构,这可能是一个巨大的开销。
访问模式限制 :它被设计为“一个CPU主要访问自己的数据”。虽然一个CPU也可以访问另一个CPU的数据副本(通过per_cpu(var, cpu_id)
),但这通常效率较低,并且需要额外的同步机制,违背了其设计的初衷。
数据聚合开销 :如果需要获取一个per-cpu变量的全局总和(例如,所有CPU上的数据包计数的总和),必须遍历所有可能的CPU核心,读取每个副本的值并相加。这个过程比读取一个单一的全局变量要慢。
抢占上下文要求 :访问per-cpu变量必须在禁用抢占的上下文中进行,否则可能导致数据不一致。
使用场景 在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。 当一个数据项被频繁地、独立地由每个CPU核心进行更新时,percpu
是唯一的、正确的、首选的 解决方案。
统计计数器 :网络设备驱动中,每个CPU都需要统计自己处理的收发包数量和字节数。使用per-cpu计数器,每个CPU可以无锁地递增自己的计数器,性能极高。当需要读取总数时(例如,用户通过ifconfig
查看),再遍历所有CPU的计数器求和。
Per-CPU数据结构 :Linux调度器的运行队列(runqueue)是每个CPU私有的。当一个CPU需要选择下一个要运行的任务时,它只在自己的运行队列中查找,完全避免了与其他CPU的竞争。
资源池/缓存 :为了避免对全局内存池的锁争用,可以为每个CPU创建一个per-cpu的对象缓存。当一个CPU需要一个对象时,它首先尝试从自己的本地缓存中获取,失败时才去访问全局池。
是否有不推荐使用该技术的场景?为什么?
真正的全局共享数据 :如果一个数据项代表的是一个全局状态,需要被所有CPU看到一个统一的、一致的视图(例如,一个全局的只读配置、一个系统状态标志),那么不应该使用per-cpu变量。
写操作稀疏的数据 :如果一个共享变量的写操作非常少,那么使用自旋锁或原子操作的开销可以忽略不计。在这种情况下,使用per-cpu变量所带来的内存开销可能得不偿失。
需要频繁跨CPU访问的数据 :如果一个CPU经常需要读取或修改另一个CPU的数据副本,那么percpu
的设计优势就不存在了,反而会因为复杂的访问逻辑和潜在的同步需求而使代码更复杂。
对比分析 请将其 与 其他相似技术 进行详细对比。 percpu
变量的主要对比对象是其他处理并发访问共享数据的技术。
特性
Per-CPU变量 (percpu
)
全局变量 + 自旋锁 (spinlock
)
全局原子变量 (atomic_t
)
简单Per-CPU数组 (无保护)
实现方式
为每个CPU创建独立的、缓存行对齐的数据副本。
单一的全局数据,通过锁机制保证互斥访问。
单一的全局数据,通过CPU的原子指令(如lock add
)进行修改。
var[smp_processor_id()]
锁机制
无锁 。
有锁 ,存在锁争用和阻塞。
无锁 ,但硬件层面仍有总线锁或缓存锁。
无锁 。
缓存性能
极佳 。无缓存行弹跳。
差 。持有锁的CPU会使其他CPU的缓存副本失效,导致缓存行弹跳。
较差 。原子操作同样会导致缓存行在多核间弹跳。
极差 。极易发生伪共享(False Sharing)。
扩展性
极佳 。性能与CPU核心数无关。
差 。性能随CPU核心数增加而急剧下降。
较差 。在高争用下,性能也会随核心数增加而下降。
差 。伪共享问题随核心数增加而恶化。
内存开销
较高 。size * NR_CPUS
+ 填充。
低 。单一实例。
低 。单一实例。
中等 。size * NR_CPUS
,但无填充。
聚合成本
高 。需要遍历所有CPU求和。
低 。获取锁后直接读取。
低 。直接读取。
高 。需要遍历所有CPU求和。
适用场景
各CPU频繁、独立地进行写操作。如计数器、本地队列。
写操作不频繁,或临界区非常短。
简单的整数计数或标志位,争用不极端。
不应使用 。是典型的反模式。
mm/percpu-internal.h pcpu_chunk_map_bits 计算 chunk 映射位数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static inline int pcpu_nr_pages_to_map_bits (int pages) { return pages * PAGE_SIZE / PCPU_MIN_ALLOC_SIZE; } static inline int pcpu_chunk_map_bits (struct pcpu_chunk *chunk) { return pcpu_nr_pages_to_map_bits(chunk->nr_pages); }
pcpu_chunk_nr_blocks 1 2 3 4 5 6 7 8 9 10 11 static inline int pcpu_chunk_nr_blocks (struct pcpu_chunk *chunk) { return chunk->nr_pages * PAGE_SIZE / PCPU_BITMAP_BLOCK_SIZE; }
include/asm-generic/percpu.h this_cpu_generic_to_op 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define per_cpu_ptr(ptr, cpu) \ ({ \ (void)(cpu); \ __verify_pcpu_ptr(ptr); \ PERCPU_PTR(ptr); \ }) #define raw_cpu_ptr(ptr) per_cpu_ptr(ptr, 0) #define raw_cpu_generic_to_op(pcp, val, op) \ do { \ *raw_cpu_ptr(&(pcp)) op val; \ } while (0) #define this_cpu_generic_to_op(pcp, val, op) \ do { \ unsigned long __flags; \ raw_local_irq_save(__flags); \ raw_cpu_generic_to_op(pcp, val, op); \ raw_local_irq_restore(__flags); \ } while (0)
this_cpu_write 1 #define this_cpu_write_1(pcp, val) this_cpu_generic_to_op(pcp, val, =)
include/linux/percpu.h alloc_percpu 分配每 CPU 的内存 1 2 3 4 5 6 #define __alloc_percpu(_size, _align) \ alloc_hooks(pcpu_alloc_noprof(_size, _align, false, GFP_KERNEL)) #define alloc_percpu(type) \ (typeof(type) __percpu *)__alloc_percpu(sizeof(type), \ __alignof__(type))
include/linux/percpu-defs.h preempt_count_add preempt_count_sub 增加和减少抢占计数 1 2 3 4 5 6 7 8 9 10 #if defined(CONFIG_DEBUG_PREEMPT) || defined(CONFIG_TRACE_PREEMPT_TOGGLE) extern void preempt_count_add (int val) ;extern void preempt_count_sub (int val) ;#define preempt_count_dec_and_test() \ ({ preempt_count_sub(1); should_resched(0); }) #else #define preempt_count_add(val) __preempt_count_add(val) #define preempt_count_sub(val) __preempt_count_sub(val) #define preempt_count_dec_and_test() __preempt_count_dec_and_test() #endif
__preempt_count_inc __preempt_count_dec 增加和减少抢占计数 1 2 #define __preempt_count_inc() __preempt_count_add(1) #define __preempt_count_dec() __preempt_count_sub(1)
get_cpu_ptr 获取当前CPU指针 1 2 3 4 5 #define get_cpu_ptr(var) \ ({ \ preempt_disable(); \ this_cpu_ptr(var); \ })
__PCPU_ATTRS 1 2 3 4 5 6 7 8 9 10 11 12 #define __PCPU_ATTRS(sec) \ __percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \ PER_CPU_ATTRIBUTES #define __PCPU_DUMMY_ATTRS \ __section(".discard" ) __attribute__((unused))
DEFINE_PER_CPU DECLARE_PER_CPU 定义每个 CPU 变量 声明每个 CPU 变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #if defined(ARCH_NEEDS_WEAK_PER_CPU) || defined(CONFIG_DEBUG_FORCE_WEAK_PER_CPU) #else #define DECLARE_PER_CPU_SECTION(type, name, sec) \ extern __PCPU_ATTRS(sec) __typeof__(type) name #define DEFINE_PER_CPU_SECTION(type, name, sec) \ __PCPU_ATTRS(sec) __typeof__(type) name #endif #define DECLARE_PER_CPU(type, name) \ DECLARE_PER_CPU_SECTION(type, name, "" ) #define DEFINE_PER_CPU(type, name) \ DEFINE_PER_CPU_SECTION(type, name, "" )
__verify_pcpu_ptr 验证@ptr是否为 percpu 指针 1 2 3 4 5 6 7 8 9 10 11 12 #define __verify_pcpu_ptr(ptr) \ do { \ const void __percpu *__vpp_verify = (typeof((ptr) + 0))NULL; \ (void)__vpp_verify; \ } while (0)
__pcpu_size_call 1 2 3 4 5 6 7 8 9 10 11 12 #define __pcpu_size_call(stem, variable, ...) \ do { \ __verify_pcpu_ptr(&(variable)); \ switch(sizeof(variable)) { \ case 1: stem##1(variable, __VA_ARGS__);break; \ case 2: stem##2(variable, __VA_ARGS__);break; \ case 4: stem##4(variable, __VA_ARGS__);break; \ case 8: stem##8(variable, __VA_ARGS__);break; \ default: \ __bad_size_call_parameter();break; \ } \ } while (0)
this_cpu_write 写入每个 CPU 变量 1 2 3 4 #define this_cpu_write(pcp, val) __pcpu_size_call(this_cpu_write_, pcp, val)
PERCPU_PTR 1 2 3 #define PERCPU_PTR(__p) \ (TYPEOF_UNQUAL(*(__p)) __force __kernel *)((__force unsigned long)(__p))
include/linux/percpu_counter.h percpu_counter_init 初始化每 CPU 计数器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static inline int percpu_counter_init_many (struct percpu_counter *fbc, s64 amount, gfp_t gfp, u32 nr_counters) { u32 i; for (i = 0 ; i < nr_counters; i++) fbc[i].count = amount; return 0 ; } static inline int percpu_counter_init (struct percpu_counter *fbc, s64 amount, gfp_t gfp) { return percpu_counter_init_many(fbc, amount, gfp, 1 ); }
mm/percpu-km.c percpu 的 kmalloc 风格分配器 一种用于大型或特殊对齐每 CPU 数据的后备分配器 历史与背景 这项技术是为了解决什么特定问题而诞生的? 这项技术以及 mm/percpu-km.c
文件的存在,是为了解决主 Per-CPU 分配器(在 mm/percpu.c
中实现)的内在局限性 。主分配器(chunk allocator)为了极致的性能和内存密度,做了一些设计上的取舍,这使得它不适合所有场景。percpu-km.c
提供了一种**备用或回退(fallback)**的分配策略,专门用于处理主分配器不擅长的情况:
大型Per-CPU分配 :主 Per-CPU 分配器基于“块(Chunk)”来管理内存,它将多个 Per-CPU 变量紧凑地排列在为每个CPU划分的“单元(Unit)”中。这种设计对于大量的小型、中型变量非常高效。但是,如果要为一个非常大的数据结构(例如,一个几KB甚至更大的缓冲区)分配Per-CPU实例,将其放入主分配器的块中会非常低效,甚至可能因为超过单元大小而无法分配。这会造成巨大的内部碎片,并破坏主分配器的紧凑布局。
特殊的内存对齐要求 :主分配器有其自己的内部对齐保证(通常是缓存行对齐),但它无法满足任意的、更严格的对齐要求。例如,某些DMA引擎可能要求其缓冲区必须是页对齐(PAGE_ALIGNED)的。主分配器无法提供这种保证。
percpu-km.c
实现的分配器,通过一种更简单、更直接的方式解决了这两个问题,它为每个CPU的副本进行独立的内存分配。
它的发展经历了哪些重要的里程碑或版本迭代? percpu-km.c
的发展与主 Per-CPU 框架的演进紧密相连,它不是一个独立演进的功能,而是作为整个Per-CPU基础设施的补充而存在的。
其概念的出现源于对主分配器局限性的认识。当内核开发者需要在驱动中创建大型的、需要特殊对齐的Per-CPU数据结构时,就需要一种不同于标准alloc_percpu()
的机制。
最初,这种备用分配可能是通过一些临时的宏或函数实现的。后来,为了统一和规范化,内核将这种“为每个CPU单独调用kmalloc”的模式抽象出来,形成了mm/percpu-km.c
中的实现。
最重要的里程碑是 pcpu_alloc()
API 的引入。这个API充当了一个前端或调度器:它会检查请求的分配大小。如果大小小于一个阈值(PCPU_MIN_UNIT_SIZE
),它就调用mm/percpu.c
中的高性能块分配器;如果大于该阈值,它就自动调用mm/percpu-km.c
中的备用分配器。这使得整个过程对大多数内核开发者来说是透明的。
目前该技术的社区活跃度和主流应用情况如何? percpu-km.c
是Per-CPU基础设施中一个稳定且必要的组成部分。
社区活跃度 :代码非常稳定,几乎没有大的变动。相关的修改通常是随着底层内存分配器(如slab, page allocator)的API变化而进行的适应性调整。
主流应用 :它虽然是“备用”方案,但在很多关键场景下被间接使用。任何时候内核代码需要一个尺寸较大的动态Per-CPU内存区域时,pcpu_alloc()
就会自动路由到percpu-km.c
的实现。例如,某些性能分析工具、复杂的网络驱动或存储驱动中需要的大型Per-CPU缓冲区,都可能使用此机制。
核心原理与设计 它的核心工作原理是什么? percpu-km.c
的工作原理与主分配器截然不同,它更加简单直接:
独立分配 :当percpu-km.c
中的分配函数被调用时,它不会去操作复杂的“块”和“单元”。相反,它会进入一个循环,为系统中的每一个 CPU核心,单独调用底层的内存分配器(如 kmalloc
或 __get_free_pages
)来分配一块独立的内存。
指针数组管理 :它会额外分配一个小的数组,这个数组的大小与CPU核心数相同。在循环中,每次为一个CPU成功分配了内存块后,就将这块内存的地址存入这个指针数组中对应的CPU索引位置。
地址翻译 :当需要访问特定CPU的副本时(例如,通过 per_cpu_ptr(ptr, cpu)
),访问逻辑会首先获取这个指针数组的基地址,然后根据传入的cpu
ID,从数组中取出对应CPU的独立内存块的地址并返回。
释放 :释放时,过程相反。它会遍历指针数组,对其中的每一个指针调用 kfree
或 free_pages
,最后再释放这个指针数组本身。
它的主要优势体现在哪些方面?
大小和对齐的灵活性 :可以分配任意大小的Per-CPU数据(只要底层kmalloc
能满足),并且可以轻易地支持任意的对齐要求(通过调用kmalloc
的对齐变体或直接从页分配器获取对齐的页)。
隔离性 :将大型分配与主Per-CPU分配器的精密数据布局分离开来,避免了对主分配器造成碎片化污染。
简单性 :其实现逻辑比主分配器的块管理算法要简单得多。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
访问性能较低 :这是其最主要的缺点。与主分配器可以通过单条指令(利用段寄存器)访问Per-CPU数据不同,通过percpu-km.c
分配的内存,其访问过程需要“获取CPU ID -> 访问指针数组 -> 间接寻址到最终内存”这几个步骤,速度明显慢于主分配器。
内存开销 :由于为每个CPU进行单独分配,可能会产生更多的内存碎片(取决于底层分配器的效率)。此外,还需要额外的空间来存储那个管理所有副本指针的数组。
缓存不友好 :虽然每个CPU的副本是独立的,但管理这些副本的指针数组本身是可能被所有CPU共享访问的,可能会带来一些微小的缓存一致性开销。
使用场景 在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。 percpu-km.c
中的机制是作为pcpu_alloc()
的后端,在以下场景中被自动选择 为首选解决方案:
大型Per-CPU数据结构 :一个驱动程序需要为每个CPU维护一个大的I/O环形缓冲区(ring buffer),大小可能是几个内存页。在这种情况下,调用pcpu_alloc(PAGE_SIZE * 4, ...)
时,内核会检测到请求的大小超过了阈值,并自动使用percpu-km.c
的逻辑,为每个CPU分配独立的4个页。
需要特定对齐的Per-CPU数据 :一个存储驱动需要为每个CPU准备一个用于DMA的元数据区域,并且硬件要求这个区域必须对齐到1024字节。通过调用pcpu_alloc()
并传入相应的对齐参数,最终会由percpu-km.c
的逻辑来满足这个特殊的对齐要求。
是否有不推荐使用该技术的场景?为什么? 绝对不应该 将此机制用于小型、性能极其敏感的Per-CPU变量,例如统计计数器或调度器中的小标志。
原因 :对于这类变量,访问速度是第一位的。主Per-CPU分配器(mm/percpu.c
)提供的单指令快速访问是专门为此优化的。如果强行使用percpu-km.c
的模式来分配一个4字节的计数器,不仅会浪费大量内存(每个CPU都会分配一个独立的、至少为SLAB
最小尺寸的内存块),而且每次访问计数器都会有显著的性能惩罚,这完全违背了使用Per-CPU变量的初衷。
对比分析 请将其 与 其他相似技术 进行详细对比。 percpu-km.c
所实现的分配器,其唯一的、也是最重要的对比对象,就是mm/percpu.c
中实现的主Per-CPU块分配器。
特性
主分配器 (mm/percpu.c
)
后备分配器 (mm/percpu-km.c
)
别名
Chunk Allocator, First-chunk Allocator
Page-based Allocator, kmalloc-based Allocator
核心机制
将多个变量打包进预先分配的大块内存(Chunks),每个Chunk为所有CPU划分了单元(Units)。
为每个CPU的副本单独调用 kmalloc
或页分配器进行分配,并用一个指针数组来管理。
访问性能
极高 。通常可通过一条指令(利用段寄存器如%gs
)直接访问,无额外开销。
较低 。访问需要通过指针数组进行间接寻址,涉及多次内存读取,速度较慢。
适用大小
优化用于小型到中型 的变量(小于 PCPU_MIN_UNIT_SIZE
)。
设计用于大型 变量(大于 PCPU_MIN_UNIT_SIZE
)。
对齐支持
提供基本的缓存行对齐,但不支持任意的、严格的对齐要求。
灵活 。可以支持由底层分配器提供的任意对齐要求(如页对齐)。
内存效率
对于大量小变量,内存密度非常高,碎片少。
对于大变量,避免了污染主分配器。但自身可能因多次独立分配而产生碎片。
主要API入口
alloc_percpu()
(动态), DEFINE_PER_CPU
(静态)
由 pcpu_alloc()
在检测到大尺寸请求时自动调用 。
pcpu_mem_zalloc pcpu_mem_free 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 static void *pcpu_mem_zalloc (size_t size, gfp_t gfp) { if (WARN_ON_ONCE(!slab_is_available())) return NULL ; if (size <= PAGE_SIZE) return kzalloc(size, gfp); else return __vmalloc(size, gfp | __GFP_ZERO); } static void pcpu_mem_free (void *ptr) { kvfree(ptr); }
pcpu_alloc_chunk 分配 chunk 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 static struct pcpu_chunk *pcpu_alloc_chunk (gfp_t gfp) { struct pcpu_chunk *chunk ; int region_bits; chunk = pcpu_mem_zalloc(pcpu_chunk_struct_size, gfp); if (!chunk) return NULL ; INIT_LIST_HEAD(&chunk->list ); chunk->nr_pages = pcpu_unit_pages; region_bits = pcpu_chunk_map_bits(chunk); chunk->alloc_map = pcpu_mem_zalloc(BITS_TO_LONGS(region_bits) * sizeof (chunk->alloc_map[0 ]), gfp); if (!chunk->alloc_map) goto alloc_map_fail; chunk->bound_map = pcpu_mem_zalloc(BITS_TO_LONGS(region_bits + 1 ) * sizeof (chunk->bound_map[0 ]), gfp); if (!chunk->bound_map) goto bound_map_fail; chunk->md_blocks = pcpu_mem_zalloc(pcpu_chunk_nr_blocks(chunk) * sizeof (chunk->md_blocks[0 ]), gfp); if (!chunk->md_blocks) goto md_blocks_fail; #ifdef NEED_PCPUOBJ_EXT if (need_pcpuobj_ext()) { chunk->obj_exts = pcpu_mem_zalloc(pcpu_chunk_map_bits(chunk) * sizeof (struct pcpuobj_ext), gfp); if (!chunk->obj_exts) goto objcg_fail; } #endif pcpu_init_md_blocks(chunk); chunk->free_bytes = chunk->nr_pages * PAGE_SIZE; return chunk; #ifdef NEED_PCPUOBJ_EXT objcg_fail: pcpu_mem_free(chunk->md_blocks); #endif md_blocks_fail: pcpu_mem_free(chunk->bound_map); bound_map_fail: pcpu_mem_free(chunk->alloc_map); alloc_map_fail: pcpu_mem_free(chunk); return NULL ; }
pcpu_create_chunk 创建 chunk 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 static struct pcpu_chunk *pcpu_create_chunk (gfp_t gfp) { const int nr_pages = pcpu_group_sizes[0 ] >> PAGE_SHIFT; struct pcpu_chunk *chunk ; struct page *pages ; unsigned long flags; int i; chunk = pcpu_alloc_chunk(gfp); if (!chunk) return NULL ; pages = alloc_pages(gfp, order_base_2(nr_pages)); if (!pages) { pcpu_free_chunk(chunk); return NULL ; } for (i = 0 ; i < nr_pages; i++) pcpu_set_page_chunk(nth_page(pages, i), chunk); chunk->data = pages; chunk->base_addr = page_address(pages); spin_lock_irqsave(&pcpu_lock, flags); pcpu_chunk_populated(chunk, 0 , nr_pages); spin_unlock_irqrestore(&pcpu_lock, flags); pcpu_stats_chunk_alloc(); trace_percpu_create_chunk(chunk->base_addr); return chunk; }
mm/percpu.c pcpu_alloc_alloc_info 分配 perCPU 分配信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 struct pcpu_alloc_info * __init pcpu_alloc_alloc_info (int nr_groups, int nr_units) { struct pcpu_alloc_info *ai ; size_t base_size, ai_size; void *ptr; int unit; base_size = ALIGN( struct_size(ai, groups, nr_groups), __alignof__(ai->groups[0 ].cpu_map[0 ])); ai_size = base_size + nr_units * sizeof (ai->groups[0 ].cpu_map[0 ]); ptr = memblock_alloc(PFN_ALIGN(ai_size), PAGE_SIZE); if (!ptr) return NULL ; ai = ptr; ptr += base_size; ai->groups[0 ].cpu_map = ptr; for (unit = 0 ; unit < nr_units; unit++) ai->groups[0 ].cpu_map[unit] = NR_CPUS; ai->nr_groups = nr_groups; ai->__ai_size = PFN_ALIGN(ai_size); return ai; }
pcpu_init_md_blocks 初始化元数据块结构体块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #define PCPU_BITMAP_BLOCK_SIZE PAGE_SIZE #define PCPU_BITMAP_BLOCK_BITS (PCPU_BITMAP_BLOCK_SIZE >> PCPU_MIN_ALLOC_SHIFT) static void pcpu_init_md_block (struct pcpu_block_md *block, int nr_bits) { block->scan_hint = 0 ; block->contig_hint = nr_bits; block->left_free = nr_bits; block->right_free = nr_bits; block->first_free = 0 ; block->nr_bits = nr_bits; } static void pcpu_init_md_blocks (struct pcpu_chunk *chunk) { struct pcpu_block_md *md_block ; pcpu_init_md_block(&chunk->chunk_md, pcpu_chunk_map_bits(chunk)); for (md_block = chunk->md_blocks; md_block != chunk->md_blocks + pcpu_chunk_nr_blocks(chunk); md_block++) pcpu_init_md_block(md_block, PCPU_BITMAP_BLOCK_BITS); }
pcpu_alloc_first_chunk 分配第一个 chunk 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 static struct pcpu_chunk * __init pcpu_alloc_first_chunk (unsigned long tmp_addr, int map_size) { struct pcpu_chunk *chunk ; unsigned long aligned_addr; int start_offset, offset_bits, region_size, region_bits; size_t alloc_size; aligned_addr = tmp_addr & PAGE_MASK; start_offset = tmp_addr - aligned_addr; region_size = ALIGN(start_offset + map_size, PAGE_SIZE); alloc_size = struct_size(chunk, populated, BITS_TO_LONGS(region_size >> PAGE_SHIFT)); chunk = memblock_alloc_or_panic(alloc_size, SMP_CACHE_BYTES); INIT_LIST_HEAD(&chunk->list ); chunk->base_addr = (void *)aligned_addr; chunk->start_offset = start_offset; chunk->end_offset = region_size - chunk->start_offset - map_size; chunk->nr_pages = region_size >> PAGE_SHIFT; region_bits = pcpu_chunk_map_bits(chunk); alloc_size = BITS_TO_LONGS(region_bits) * sizeof (chunk->alloc_map[0 ]); chunk->alloc_map = memblock_alloc_or_panic(alloc_size, SMP_CACHE_BYTES); alloc_size = BITS_TO_LONGS(region_bits + 1 ) * sizeof (chunk->bound_map[0 ]); chunk->bound_map = memblock_alloc_or_panic(alloc_size, SMP_CACHE_BYTES); alloc_size = pcpu_chunk_nr_blocks(chunk) * sizeof (chunk->md_blocks[0 ]); chunk->md_blocks = memblock_alloc_or_panic(alloc_size, SMP_CACHE_BYTES); #ifdef NEED_PCPUOBJ_EXT chunk->obj_exts = NULL ; #endif pcpu_init_md_blocks(chunk); chunk->immutable = true ; bitmap_fill(chunk->populated, chunk->nr_pages); chunk->nr_populated = chunk->nr_pages; chunk->nr_empty_pop_pages = chunk->nr_pages; chunk->free_bytes = map_size; if (chunk->start_offset) { offset_bits = chunk->start_offset / PCPU_MIN_ALLOC_SIZE; bitmap_set(chunk->alloc_map, 0 , offset_bits); set_bit(0 , chunk->bound_map); set_bit(offset_bits, chunk->bound_map); chunk->chunk_md.first_free = offset_bits; pcpu_block_update_hint_alloc(chunk, 0 , offset_bits); } if (chunk->end_offset) { offset_bits = chunk->end_offset / PCPU_MIN_ALLOC_SIZE; bitmap_set(chunk->alloc_map, pcpu_chunk_map_bits(chunk) - offset_bits, offset_bits); set_bit((start_offset + map_size) / PCPU_MIN_ALLOC_SIZE, chunk->bound_map); set_bit(region_bits, chunk->bound_map); pcpu_block_update_hint_alloc(chunk, pcpu_chunk_map_bits(chunk) - offset_bits, offset_bits); } return chunk; }
pcpu_chunk_move 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static void __pcpu_chunk_move(struct pcpu_chunk *chunk, int slot, bool move_front) { if (chunk != pcpu_reserved_chunk) { if (move_front) list_move(&chunk->list , &pcpu_chunk_lists[slot]); else list_move_tail(&chunk->list , &pcpu_chunk_lists[slot]); } } static void pcpu_chunk_move (struct pcpu_chunk *chunk, int slot) { __pcpu_chunk_move(chunk, slot, true ); }
pcpu_chunk_relocate 重新定位 chunk 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 static int pcpu_size_to_slot (int size) { if (size == pcpu_unit_size) return pcpu_free_slot; return __pcpu_size_to_slot(size); } static int pcpu_chunk_slot (const struct pcpu_chunk *chunk) { const struct pcpu_block_md *chunk_md = &chunk->chunk_md; if (chunk->free_bytes < PCPU_MIN_ALLOC_SIZE || chunk_md->contig_hint == 0 ) return 0 ; return pcpu_size_to_slot(chunk_md->contig_hint * PCPU_MIN_ALLOC_SIZE); } static void pcpu_chunk_relocate (struct pcpu_chunk *chunk, int oslot) { int nslot = pcpu_chunk_slot(chunk); if (chunk->isolated) return ; if (oslot != nslot) __pcpu_chunk_move(chunk, nslot, oslot < nslot); }
pcpu_setup_first_chunk 设置第一个 chunk 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 void __init pcpu_setup_first_chunk (const struct pcpu_alloc_info *ai, void *base_addr) { size_t size_sum = ai->static_size + ai->reserved_size + ai->dyn_size; size_t static_size, dyn_size; unsigned long *group_offsets; size_t *group_sizes; unsigned long *unit_off; unsigned int cpu; int *unit_map; int group, unit, i; unsigned long tmp_addr; size_t alloc_size; #define PCPU_SETUP_BUG_ON(cond) do { \ if (unlikely(cond)) { \ pr_emerg("failed to initialize, %s\n" , #cond); \ pr_emerg("cpu_possible_mask=%*pb\n" , \ cpumask_pr_args(cpu_possible_mask)); \ pcpu_dump_alloc_info(KERN_EMERG, ai); \ BUG(); \ } \ } while (0) PCPU_SETUP_BUG_ON(ai->nr_groups <= 0 ); #ifdef CONFIG_SMP PCPU_SETUP_BUG_ON(!ai->static_size); PCPU_SETUP_BUG_ON(offset_in_page(__per_cpu_start)); #endif PCPU_SETUP_BUG_ON(!base_addr); PCPU_SETUP_BUG_ON(offset_in_page(base_addr)); PCPU_SETUP_BUG_ON(ai->unit_size < size_sum); PCPU_SETUP_BUG_ON(offset_in_page(ai->unit_size)); PCPU_SETUP_BUG_ON(ai->unit_size < PCPU_MIN_UNIT_SIZE); PCPU_SETUP_BUG_ON(!IS_ALIGNED(ai->unit_size, PCPU_BITMAP_BLOCK_SIZE)); PCPU_SETUP_BUG_ON(ai->dyn_size < PERCPU_DYNAMIC_EARLY_SIZE); PCPU_SETUP_BUG_ON(!IS_ALIGNED(ai->reserved_size, PCPU_MIN_ALLOC_SIZE)); PCPU_SETUP_BUG_ON(!(IS_ALIGNED(PCPU_BITMAP_BLOCK_SIZE, PAGE_SIZE) || IS_ALIGNED(PAGE_SIZE, PCPU_BITMAP_BLOCK_SIZE))); PCPU_SETUP_BUG_ON(pcpu_verify_alloc_info(ai) < 0 ); alloc_size = ai->nr_groups * sizeof (group_offsets[0 ]); group_offsets = memblock_alloc_or_panic(alloc_size, SMP_CACHE_BYTES); alloc_size = ai->nr_groups * sizeof (group_sizes[0 ]); group_sizes = memblock_alloc_or_panic(alloc_size, SMP_CACHE_BYTES); alloc_size = nr_cpu_ids * sizeof (unit_map[0 ]); unit_map = memblock_alloc_or_panic(alloc_size, SMP_CACHE_BYTES); alloc_size = nr_cpu_ids * sizeof (unit_off[0 ]); unit_off = memblock_alloc_or_panic(alloc_size, SMP_CACHE_BYTES); for (cpu = 0 ; cpu < nr_cpu_ids; cpu++) unit_map[cpu] = UINT_MAX; pcpu_low_unit_cpu = NR_CPUS; pcpu_high_unit_cpu = NR_CPUS; for (group = 0 , unit = 0 ; group < ai->nr_groups; group++, unit += i) { const struct pcpu_group_info *gi = &ai->groups[group]; group_offsets[group] = gi->base_offset; group_sizes[group] = gi->nr_units * ai->unit_size; for (i = 0 ; i < gi->nr_units; i++) { cpu = gi->cpu_map[i]; if (cpu == NR_CPUS) continue ; PCPU_SETUP_BUG_ON(cpu >= nr_cpu_ids); PCPU_SETUP_BUG_ON(!cpu_possible(cpu)); PCPU_SETUP_BUG_ON(unit_map[cpu] != UINT_MAX); unit_map[cpu] = unit + i; unit_off[cpu] = gi->base_offset + i * ai->unit_size; if (pcpu_low_unit_cpu == NR_CPUS || unit_off[cpu] < unit_off[pcpu_low_unit_cpu]) pcpu_low_unit_cpu = cpu; if (pcpu_high_unit_cpu == NR_CPUS || unit_off[cpu] > unit_off[pcpu_high_unit_cpu]) pcpu_high_unit_cpu = cpu; } } pcpu_nr_units = unit; for_each_possible_cpu(cpu) PCPU_SETUP_BUG_ON(unit_map[cpu] == UINT_MAX); #undef PCPU_SETUP_BUG_ON pcpu_dump_alloc_info(KERN_DEBUG, ai); pcpu_nr_groups = ai->nr_groups; pcpu_group_offsets = group_offsets; pcpu_group_sizes = group_sizes; pcpu_unit_map = unit_map; pcpu_unit_offsets = unit_off; pcpu_unit_pages = ai->unit_size >> PAGE_SHIFT; pcpu_unit_size = pcpu_unit_pages << PAGE_SHIFT; pcpu_atom_size = ai->atom_size; pcpu_chunk_struct_size = struct_size((struct pcpu_chunk *)0 , populated, BITS_TO_LONGS(pcpu_unit_pages)); pcpu_stats_save_ai(ai); pcpu_sidelined_slot = __pcpu_size_to_slot(pcpu_unit_size) + 1 ; pcpu_free_slot = pcpu_sidelined_slot + 1 ; pcpu_to_depopulate_slot = pcpu_free_slot + 1 ; pcpu_nr_slots = pcpu_to_depopulate_slot + 1 ; pcpu_chunk_lists = memblock_alloc_or_panic(pcpu_nr_slots * sizeof (pcpu_chunk_lists[0 ]), SMP_CACHE_BYTES); for (i = 0 ; i < pcpu_nr_slots; i++) INIT_LIST_HEAD(&pcpu_chunk_lists[i]); static_size = ALIGN(ai->static_size, PCPU_MIN_ALLOC_SIZE); dyn_size = ai->dyn_size - (static_size - ai->static_size); tmp_addr = (unsigned long )base_addr + static_size; if (ai->reserved_size) pcpu_reserved_chunk = pcpu_alloc_first_chunk(tmp_addr, ai->reserved_size); tmp_addr = (unsigned long )base_addr + static_size + ai->reserved_size; pcpu_first_chunk = pcpu_alloc_first_chunk(tmp_addr, dyn_size); pcpu_nr_empty_pop_pages = pcpu_first_chunk->nr_empty_pop_pages; pcpu_chunk_relocate(pcpu_first_chunk, -1 ); pcpu_nr_populated += PFN_DOWN(size_sum); pcpu_stats_chunk_alloc(); trace_percpu_create_chunk(base_addr); pcpu_base_addr = base_addr; }
释放的 perCPU 分配信息 1 2 3 4 5 6 7 8 9 10 void __init pcpu_free_alloc_info (struct pcpu_alloc_info *ai) { memblock_free(ai, ai->__ai_size); }
setup_per_cpu_areas UP percpu 区域设置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void __init setup_per_cpu_areas (void ) { const size_t unit_size = roundup_pow_of_two(max_t (size_t , PCPU_MIN_UNIT_SIZE, PERCPU_DYNAMIC_RESERVE)); struct pcpu_alloc_info *ai ; void *fc; ai = pcpu_alloc_alloc_info(1 , 1 ); fc = memblock_alloc_from(unit_size, PAGE_SIZE, __pa(MAX_DMA_ADDRESS)); if (!ai || !fc) panic("Failed to allocate memory for percpu areas." ); kmemleak_ignore_phys(__pa(fc)); ai->dyn_size = unit_size; ai->unit_size = unit_size; ai->atom_size = unit_size; ai->alloc_size = unit_size; ai->groups[0 ].nr_units = 1 ; ai->groups[0 ].cpu_map[0 ] = 0 ; pcpu_setup_first_chunk(ai, fc); pcpu_free_alloc_info(ai); }
pcpu_size_to_slot 计算 chunk 的大小 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static int __pcpu_size_to_slot(int size){ int highbit = fls(size); return max(highbit - PCPU_SLOT_BASE_SHIFT + 2 , 1 ); } static int pcpu_size_to_slot (int size) { if (size == pcpu_unit_size) return pcpu_free_slot; return __pcpu_size_to_slot(size); }
pcpu_check_block_hint 检查块提示 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static bool pcpu_check_block_hint (struct pcpu_block_md *block, int bits, size_t align) { int bit_off = ALIGN(block->contig_hint_start, align) - block->contig_hint_start; return bit_off + bits <= block->contig_hint; }
pcpu_for_each_fit_region pcpu_for_each_md_free_region 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 static void pcpu_next_md_free_region (struct pcpu_chunk *chunk, int *bit_off, int *bits) { int i = pcpu_off_to_block_index(*bit_off); int block_off = pcpu_off_to_block_off(*bit_off); struct pcpu_block_md *block ; *bits = 0 ; for (block = chunk->md_blocks + i; i < pcpu_chunk_nr_blocks(chunk); block++, i++) { if (*bits) { *bits += block->left_free; if (block->left_free == PCPU_BITMAP_BLOCK_BITS) continue ; return ; } *bits = block->contig_hint; if (*bits && block->contig_hint_start >= block_off && *bits + block->contig_hint_start < PCPU_BITMAP_BLOCK_BITS) { *bit_off = pcpu_block_off_to_off(i, block->contig_hint_start); return ; } block_off = 0 ; *bits = block->right_free; *bit_off = (i + 1 ) * PCPU_BITMAP_BLOCK_BITS - block->right_free; } } static void pcpu_next_fit_region (struct pcpu_chunk *chunk, int alloc_bits, int align, int *bit_off, int *bits) { int i = pcpu_off_to_block_index(*bit_off); int block_off = pcpu_off_to_block_off(*bit_off); struct pcpu_block_md *block ; *bits = 0 ; for (block = chunk->md_blocks + i; i < pcpu_chunk_nr_blocks(chunk); block++, i++) { if (*bits) { *bits += block->left_free; if (*bits >= alloc_bits) return ; if (block->left_free == PCPU_BITMAP_BLOCK_BITS) continue ; } *bits = ALIGN(block->contig_hint_start, align) - block->contig_hint_start; if (block->contig_hint && block->contig_hint_start >= block_off && block->contig_hint >= *bits + alloc_bits) { int start = pcpu_next_hint(block, alloc_bits); *bits += alloc_bits + block->contig_hint_start - start; *bit_off = pcpu_block_off_to_off(i, start); return ; } block_off = 0 ; *bit_off = ALIGN(PCPU_BITMAP_BLOCK_BITS - block->right_free, align); *bits = PCPU_BITMAP_BLOCK_BITS - *bit_off; *bit_off = pcpu_block_off_to_off(i, *bit_off); if (*bits >= alloc_bits) return ; } *bit_off = pcpu_chunk_map_bits(chunk); } #define pcpu_for_each_md_free_region(chunk, bit_off, bits) \ for (pcpu_next_md_free_region((chunk), &(bit_off), &(bits)); \ (bit_off) < pcpu_chunk_map_bits((chunk)); \ (bit_off) += (bits) + 1 , \ pcpu_next_md_free_region((chunk), &(bit_off), &(bits))) #define pcpu_for_each_fit_region(chunk, alloc_bits, align, bit_off, bits) \ for (pcpu_next_fit_region((chunk), (alloc_bits), (align), &(bit_off), \ &(bits)); \ (bit_off) < pcpu_chunk_map_bits((chunk)); \ (bit_off) += (bits), \ pcpu_next_fit_region((chunk), (alloc_bits), (align), &(bit_off), \ &(bits)))
pcpu_next_hint 确定要使用的提示
决定扫描的起始位置,是从 scan_hint 开始还是从 first_free 开始。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static int pcpu_next_hint (struct pcpu_block_md *block, int alloc_bits) { if (block->scan_hint && block->contig_hint_start > block->scan_hint_start && alloc_bits > block->scan_hint) return block->scan_hint_start + block->scan_hint; return block->first_free; }
pcpu_is_populated 检查是否填充区域 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 static bool pcpu_is_populated (struct pcpu_chunk *chunk, int bit_off, int bits, int *next_off) { unsigned int start, end; start = PFN_DOWN(bit_off * PCPU_MIN_ALLOC_SIZE); end = PFN_UP((bit_off + bits) * PCPU_MIN_ALLOC_SIZE); start = find_next_zero_bit(chunk->populated, end, start); if (start >= end) return true ; end = find_next_bit(chunk->populated, end, start + 1 ); *next_off = end * PAGE_SIZE / PCPU_MIN_ALLOC_SIZE; return false ; }
pcpu_find_block_fit 查找块适合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 static int pcpu_find_block_fit (struct pcpu_chunk *chunk, int alloc_bits, size_t align, bool pop_only) { struct pcpu_block_md *chunk_md = &chunk->chunk_md; int bit_off, bits, next_off; if (!pcpu_check_block_hint(chunk_md, alloc_bits, align)) return -1 ; bit_off = pcpu_next_hint(chunk_md, alloc_bits); bits = 0 ; pcpu_for_each_fit_region(chunk, alloc_bits, align, bit_off, bits) { if (!pop_only || pcpu_is_populated(chunk, bit_off, bits, &next_off)) break ; bit_off = next_off; bits = 0 ; } if (bit_off == pcpu_chunk_map_bits(chunk)) return -1 ; return bit_off; }
pcpu_find_zero_area 在位图中查找一段连续的零位区域 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 static unsigned long pcpu_find_zero_area (unsigned long *map , unsigned long size, unsigned long start, unsigned long nr, unsigned long align_mask, unsigned long *largest_off, unsigned long *largest_bits) { unsigned long index, end, i, area_off, area_bits; again: index = find_next_zero_bit(map , size, start); index = __ALIGN_MASK(index, align_mask); area_off = index; end = index + nr; if (end > size) return end; i = find_next_bit(map , end, index); if (i < end) { area_bits = i - area_off; if (area_bits > *largest_bits || (area_bits == *largest_bits && *largest_off && (!area_off || __ffs(area_off) > __ffs(*largest_off)))) { *largest_off = area_off; *largest_bits = area_bits; } start = i + 1 ; goto again; } return index; }
pcpu_block_update 更新块的元数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 static void pcpu_block_update (struct pcpu_block_md *block, int start, int end) { int contig = end - start; block->first_free = min(block->first_free, start); if (start == 0 ) block->left_free = contig; if (end == block->nr_bits) block->right_free = contig; if (contig > block->contig_hint) { if (start > block->contig_hint_start) { if (block->contig_hint > block->scan_hint) { block->scan_hint_start = block->contig_hint_start; block->scan_hint = block->contig_hint; } else if (start < block->scan_hint_start) { block->scan_hint = 0 ; } } else { block->scan_hint = 0 ; } block->contig_hint_start = start; block->contig_hint = contig; } else if (contig == block->contig_hint) { if (block->contig_hint_start && (!start || __ffs(start) > __ffs(block->contig_hint_start))) { block->contig_hint_start = start; if (start < block->scan_hint_start && block->contig_hint > block->scan_hint) block->scan_hint = 0 ; } else if (start > block->scan_hint_start || block->contig_hint > block->scan_hint) { block->scan_hint_start = start; block->scan_hint = contig; } } else { if ((start < block->contig_hint_start && (contig > block->scan_hint || (contig == block->scan_hint && start > block->scan_hint_start)))) { block->scan_hint_start = start; block->scan_hint = contig; } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 static void pcpu_block_update_scan (struct pcpu_chunk *chunk, int bit_off, int bits) { int s_off = pcpu_off_to_block_off(bit_off); int e_off = s_off + bits; int s_index, l_bit; struct pcpu_block_md *block ; if (e_off > PCPU_BITMAP_BLOCK_BITS) return ; s_index = pcpu_off_to_block_index(bit_off); block = chunk->md_blocks + s_index; l_bit = find_last_bit(pcpu_index_alloc_map(chunk, s_index), s_off); s_off = (s_off == l_bit) ? 0 : l_bit + 1 ; pcpu_block_update(block, s_off, e_off); }
pcpu_block_update_hint_alloc 更新块的元数据(分配路径) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 static void pcpu_block_update_hint_alloc (struct pcpu_chunk *chunk, int bit_off, int bits) { struct pcpu_block_md *chunk_md = &chunk->chunk_md; int nr_empty_pages = 0 ; struct pcpu_block_md *s_block , *e_block , *block ; int s_index, e_index; int s_off, e_off; s_index = pcpu_off_to_block_index(bit_off); e_index = pcpu_off_to_block_index(bit_off + bits - 1 ); s_off = pcpu_off_to_block_off(bit_off); e_off = pcpu_off_to_block_off(bit_off + bits - 1 ) + 1 ; s_block = chunk->md_blocks + s_index; e_block = chunk->md_blocks + e_index; if (s_block->contig_hint == PCPU_BITMAP_BLOCK_BITS) nr_empty_pages++; if (s_off == s_block->first_free) s_block->first_free = find_next_zero_bit( pcpu_index_alloc_map(chunk, s_index), PCPU_BITMAP_BLOCK_BITS, s_off + bits); if (pcpu_region_overlap(s_block->scan_hint_start, s_block->scan_hint_start + s_block->scan_hint, s_off, s_off + bits)) s_block->scan_hint = 0 ; if (pcpu_region_overlap(s_block->contig_hint_start, s_block->contig_hint_start + s_block->contig_hint, s_off, s_off + bits)) { if (!s_off) s_block->left_free = 0 ; pcpu_block_refresh_hint(chunk, s_index); } else { s_block->left_free = min(s_block->left_free, s_off); if (s_index == e_index) s_block->right_free = min_t (int , s_block->right_free, PCPU_BITMAP_BLOCK_BITS - e_off); else s_block->right_free = 0 ; } if (s_index != e_index) { if (e_block->contig_hint == PCPU_BITMAP_BLOCK_BITS) nr_empty_pages++; e_block->first_free = find_next_zero_bit( pcpu_index_alloc_map(chunk, e_index), PCPU_BITMAP_BLOCK_BITS, e_off); if (e_off == PCPU_BITMAP_BLOCK_BITS) { e_block++; } else { if (e_off > e_block->scan_hint_start) e_block->scan_hint = 0 ; e_block->left_free = 0 ; if (e_off > e_block->contig_hint_start) { pcpu_block_refresh_hint(chunk, e_index); } else { e_block->right_free = min_t (int , e_block->right_free, PCPU_BITMAP_BLOCK_BITS - e_off); } } nr_empty_pages += (e_index - s_index - 1 ); for (block = s_block + 1 ; block < e_block; block++) { block->scan_hint = 0 ; block->contig_hint = 0 ; block->left_free = 0 ; block->right_free = 0 ; } } if (nr_empty_pages) pcpu_update_empty_pages(chunk, -nr_empty_pages); if (pcpu_region_overlap(chunk_md->scan_hint_start, chunk_md->scan_hint_start + chunk_md->scan_hint, bit_off, bit_off + bits)) chunk_md->scan_hint = 0 ; if (pcpu_region_overlap(chunk_md->contig_hint_start, chunk_md->contig_hint_start + chunk_md->contig_hint, bit_off, bit_off + bits)) pcpu_chunk_refresh_hint(chunk, false ); }
pcpu_alloc_area 从 pcpu_chunk 中分配一段内存区域 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 static int pcpu_alloc_area (struct pcpu_chunk *chunk, int alloc_bits, size_t align, int start) { struct pcpu_block_md *chunk_md = &chunk->chunk_md; size_t align_mask = (align) ? (align - 1 ) : 0 ; unsigned long area_off = 0 , area_bits = 0 ; int bit_off, end, oslot; lockdep_assert_held(&pcpu_lock); oslot = pcpu_chunk_slot(chunk); end = min_t (int , start + alloc_bits + PCPU_BITMAP_BLOCK_BITS, pcpu_chunk_map_bits(chunk)); bit_off = pcpu_find_zero_area(chunk->alloc_map, end, start, alloc_bits, align_mask, &area_off, &area_bits); if (bit_off >= end) return -1 ; if (area_bits) pcpu_block_update_scan(chunk, area_off, area_bits); bitmap_set(chunk->alloc_map, bit_off, alloc_bits); set_bit(bit_off, chunk->bound_map); bitmap_clear(chunk->bound_map, bit_off + 1 , alloc_bits - 1 ); set_bit(bit_off + alloc_bits, chunk->bound_map); chunk->free_bytes -= alloc_bits * PCPU_MIN_ALLOC_SIZE; if (bit_off == chunk_md->first_free) chunk_md->first_free = find_next_zero_bit( chunk->alloc_map, pcpu_chunk_map_bits(chunk), bit_off + alloc_bits); pcpu_block_update_hint_alloc(chunk, bit_off, alloc_bits); pcpu_chunk_relocate(chunk, oslot); return bit_off * PCPU_MIN_ALLOC_SIZE; }
pcpu_alloc_noprof 分配 perCPU 内存 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 void __percpu *pcpu_alloc_noprof (size_t size, size_t align, bool reserved, gfp_t gfp) { gfp_t pcpu_gfp; bool is_atomic; bool do_warn; struct obj_cgroup *objcg = NULL ; static int warn_limit = 10 ; struct pcpu_chunk *chunk , *next ; const char *err; int slot, off, cpu, ret; unsigned long flags; void __percpu *ptr; size_t bits, bit_align; gfp = current_gfp_context(gfp); pcpu_gfp = gfp & (GFP_KERNEL | __GFP_NORETRY | __GFP_NOWARN); is_atomic = !gfpflags_allow_blocking(gfp); do_warn = !(gfp & __GFP_NOWARN); if (unlikely(align < PCPU_MIN_ALLOC_SIZE)) align = PCPU_MIN_ALLOC_SIZE; size = ALIGN(size, PCPU_MIN_ALLOC_SIZE); bits = size >> PCPU_MIN_ALLOC_SHIFT; bit_align = align >> PCPU_MIN_ALLOC_SHIFT; if (unlikely(!size || size > PCPU_MIN_UNIT_SIZE || align > PAGE_SIZE || !is_power_of_2(align))) { WARN(do_warn, "illegal size (%zu) or align (%zu) for percpu allocation\n" , size, align); return NULL ; } if (unlikely(!pcpu_memcg_pre_alloc_hook(size, gfp, &objcg))) return NULL ; if (!is_atomic) { if (gfp & __GFP_NOFAIL) { mutex_lock(&pcpu_alloc_mutex); } else if (mutex_lock_killable(&pcpu_alloc_mutex)) { pcpu_memcg_post_alloc_hook(objcg, NULL , 0 , size); return NULL ; } } spin_lock_irqsave(&pcpu_lock, flags); if (reserved && pcpu_reserved_chunk) { chunk = pcpu_reserved_chunk; off = pcpu_find_block_fit(chunk, bits, bit_align, is_atomic); if (off < 0 ) { err = "alloc from reserved chunk failed" ; goto fail_unlock; } off = pcpu_alloc_area(chunk, bits, bit_align, off); if (off >= 0 ) goto area_found; err = "alloc from reserved chunk failed" ; goto fail_unlock; } restart: for (slot = pcpu_size_to_slot(size); slot <= pcpu_free_slot; slot++) { list_for_each_entry_safe(chunk, next, &pcpu_chunk_lists[slot], list ) { off = pcpu_find_block_fit(chunk, bits, bit_align, is_atomic); if (off < 0 ) { if (slot < PCPU_SLOT_FAIL_THRESHOLD) pcpu_chunk_move(chunk, 0 ); continue ; } off = pcpu_alloc_area(chunk, bits, bit_align, off); if (off >= 0 ) { pcpu_reintegrate_chunk(chunk); goto area_found; } } } spin_unlock_irqrestore(&pcpu_lock, flags); if (is_atomic) { err = "atomic alloc failed, no space left" ; goto fail; } if (list_empty(&pcpu_chunk_lists[pcpu_free_slot])) { chunk = pcpu_create_chunk(pcpu_gfp); if (!chunk) { err = "failed to allocate new chunk" ; goto fail; } spin_lock_irqsave(&pcpu_lock, flags); pcpu_chunk_relocate(chunk, -1 ); } else { spin_lock_irqsave(&pcpu_lock, flags); } goto restart; area_found: pcpu_stats_area_alloc(chunk, size); if (pcpu_nr_empty_pop_pages < PCPU_EMPTY_POP_PAGES_LOW) pcpu_schedule_balance_work(); spin_unlock_irqrestore(&pcpu_lock, flags); if (!is_atomic) { unsigned int page_end, rs, re; rs = PFN_DOWN(off); page_end = PFN_UP(off + size); for_each_clear_bitrange_from(rs, re, chunk->populated, page_end) { WARN_ON(chunk->immutable); ret = pcpu_populate_chunk(chunk, rs, re, pcpu_gfp); spin_lock_irqsave(&pcpu_lock, flags); if (ret) { pcpu_free_area(chunk, off); err = "failed to populate" ; goto fail_unlock; } pcpu_chunk_populated(chunk, rs, re); spin_unlock_irqrestore(&pcpu_lock, flags); } mutex_unlock(&pcpu_alloc_mutex); } for_each_possible_cpu(cpu) memset ((void *)pcpu_chunk_addr(chunk, cpu, 0 ) + off, 0 , size); ptr = __addr_to_pcpu_ptr(chunk->base_addr + off); kmemleak_alloc_percpu(ptr, size, gfp); trace_percpu_alloc_percpu(_RET_IP_, reserved, is_atomic, size, align, chunk->base_addr, off, ptr, pcpu_obj_full_size(size), gfp); pcpu_memcg_post_alloc_hook(objcg, chunk, off, size); pcpu_alloc_tag_alloc_hook(chunk, off, size); return ptr; fail_unlock: spin_unlock_irqrestore(&pcpu_lock, flags); fail: trace_percpu_alloc_percpu_fail(reserved, is_atomic, size, align); if (do_warn && warn_limit) { pr_warn("allocation failed, size=%zu align=%zu atomic=%d, %s\n" , size, align, is_atomic, err); if (!is_atomic) dump_stack(); if (!--warn_limit) pr_info("limit reached, disable warning\n" ); } if (is_atomic) { pcpu_atomic_alloc_failed = true ; pcpu_schedule_balance_work(); } else { mutex_unlock(&pcpu_alloc_mutex); } pcpu_memcg_post_alloc_hook(objcg, NULL , 0 , size); return NULL ; }
pcpu_chunk_addr_search 确定包含指定地址的块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 static bool pcpu_addr_in_chunk (struct pcpu_chunk *chunk, void *addr) { void *start_addr, *end_addr; if (!chunk) return false ; start_addr = chunk->base_addr + chunk->start_offset; end_addr = chunk->base_addr + chunk->nr_pages * PAGE_SIZE - chunk->end_offset; return addr >= start_addr && addr < end_addr; } static struct pcpu_chunk *pcpu_chunk_addr_search (void *addr) { if (pcpu_addr_in_chunk(pcpu_first_chunk, addr)) return pcpu_first_chunk; if (pcpu_addr_in_chunk(pcpu_reserved_chunk, addr)) return pcpu_reserved_chunk; addr += pcpu_unit_offsets[raw_smp_processor_id()]; return pcpu_get_page_chunk(pcpu_addr_to_page(addr)); }
pcpu_free_area 释放指定偏移量的内存区域 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 static int pcpu_free_area (struct pcpu_chunk *chunk, int off) { struct pcpu_block_md *chunk_md = &chunk->chunk_md; int bit_off, bits, end, oslot, freed; lockdep_assert_held(&pcpu_lock); pcpu_stats_area_dealloc(chunk); oslot = pcpu_chunk_slot(chunk); bit_off = off / PCPU_MIN_ALLOC_SIZE; end = find_next_bit(chunk->bound_map, pcpu_chunk_map_bits(chunk), bit_off + 1 ); bits = end - bit_off; bitmap_clear(chunk->alloc_map, bit_off, bits); freed = bits * PCPU_MIN_ALLOC_SIZE; chunk->free_bytes += freed; chunk_md->first_free = min(chunk_md->first_free, bit_off); pcpu_block_update_hint_free(chunk, bit_off, bits); pcpu_chunk_relocate(chunk, oslot); return freed; }
free_percpu 释放分配的内存 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 void free_percpu (void __percpu *ptr) { void *addr; struct pcpu_chunk *chunk ; unsigned long flags; int size, off; bool need_balance = false ; if (!ptr) return ; kmemleak_free_percpu(ptr); addr = __pcpu_ptr_to_addr(ptr); chunk = pcpu_chunk_addr_search(addr); off = addr - chunk->base_addr; spin_lock_irqsave(&pcpu_lock, flags); size = pcpu_free_area(chunk, off); pcpu_alloc_tag_free_hook(chunk, off, size); pcpu_memcg_free_hook(chunk, off, size); if (!chunk->isolated && chunk->free_bytes == pcpu_unit_size) { struct pcpu_chunk *pos ; list_for_each_entry(pos, &pcpu_chunk_lists[pcpu_free_slot], list ) if (pos != chunk) { need_balance = true ; break ; } } else if (pcpu_should_reclaim_chunk(chunk)) { pcpu_isolate_chunk(chunk); need_balance = true ; } trace_percpu_free_percpu(chunk->base_addr, off, ptr); spin_unlock_irqrestore(&pcpu_lock, flags); if (need_balance) pcpu_schedule_balance_work(); }
pcpu_balance_free: 异步回收空闲的Per-CPU内存块 此函数是内核Per-CPU分配器后台平衡工作(pcpu_balance_workfn
)的一个核心组成部分。它的根本作用是扮演一个垃圾回收器的角色, 负责识别并销毁系统中多余的、完全空闲的per-cpu内存块(chunks), 以便将它们占用的物理内存和虚拟地址空间返还给主系统 。
由于销毁一个内存块(chunk)是一个相对耗时且可能导致睡眠的操作(因为它需要归还多个物理页面给伙伴系统), 这个任务必须在可以安全睡眠的后台工作线程中异步执行, 而不能在对性能和延迟要求极高的per-cpu变量分配/释放的”快路径”上进行。
该函数最核心的实现原理是一个两阶段的”锁-释放-重锁”(lock-unlock-relock)模式 , 以兼顾安全性和性能:
阶段一: 快速选择 (在自旋锁保护下)
函数首先获取保护per-cpu核心数据结构的自旋锁pcpu_lock
。
它快速地遍历全局的”空闲块列表”(pcpu_free_slot
)。
它的策略是永远保留至少一个空闲块 作为备用, 以便快速响应未来的分配请求。因此, 它会跳过列表中的第一个块。
对于其余的空闲块, 它根据empty_only
标志来决定是否要回收它们, 并将选中的块从全局列表移动 到一个临时的、函数本地的to_free
链表中。这个移动操作非常快。
阶段二: 慢速销毁 (在释放自旋锁后)
在完成了快速的选择阶段后, 函数会释放pcpu_lock
自旋锁 。这是至关重要的一步, 因为接下来的操作很慢, 且可能需要睡眠。释放锁可以避免长时间持有锁而导致系统其他部分停顿。
现在, 它安全地遍历临时的to_free
链表。对于链表中的每一个块:
它会先”去填充”(depopulate)该块, 即将该块所持有的所有物理内存页面逐一返还给内核的伙伴系统(buddy allocator)。
然后, 它调用pcpu_destroy_chunk
来释放pcpu_chunk
结构体本身, 以及它所占用的虚拟地址空间(vmalloc area)。
在循环中, 它会调用cond_resched()
, 主动检查是否需要进行任务调度。这可以防止在销毁大量块时长时间独占CPU, 从而保持系统的响应性。
在完成了所有销毁操作后, 函数重新获取pcpu_lock
自旋锁 , 因为调用它的pcpu_balance_workfn
函数期望在它返回时锁仍然是被持有的。
empty_only
标志的意义 :pcpu_balance_workfn
会调用此函数两次, 每次使用不同的empty_only
值, 这是一种精细的平衡策略。
pcpu_balance_free(false)
(首次调用, “激进模式”): 此时, 它会回收所有多余的空闲块, 不管这些块是否还预留着已填充但未使用的页面 。
pcpu_balance_free(true)
(末次调用, “保守模式”): 在经过了中间的”回收”和”填充”步骤后, 可能会产生一些新的、不仅自身空闲、连预留页面也都没有的”纯空”块。这次调用会清理掉这些纯空块, 但会保留那些虽然空闲但仍持有预留页面的块, 因为这些页面可能马上就会被用到。
代码逐行解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 static void pcpu_balance_free (bool empty_only) { LIST_HEAD(to_free); struct list_head *free_head = &pcpu_chunk_lists[pcpu_free_slot]; struct pcpu_chunk *chunk , *next ; lockdep_assert_held(&pcpu_lock); list_for_each_entry_safe(chunk, next, free_head, list ) { WARN_ON(chunk->immutable); if (chunk == list_first_entry(free_head, struct pcpu_chunk, list )) continue ; if (!empty_only || chunk->nr_empty_pop_pages == 0 ) list_move(&chunk->list , &to_free); } if (list_empty(&to_free)) return ; spin_unlock_irq(&pcpu_lock); list_for_each_entry_safe(chunk, next, &to_free, list ) { unsigned int rs, re; for_each_set_bitrange(rs, re, chunk->populated, chunk->nr_pages) { pcpu_depopulate_chunk(chunk, rs, re); spin_lock_irq(&pcpu_lock); pcpu_chunk_depopulated(chunk, rs, re); spin_unlock_irq(&pcpu_lock); } pcpu_destroy_chunk(chunk); cond_resched(); } spin_lock_irq(&pcpu_lock); }
Per-CPU 内存池填充与回收 这两个函数, pcpu_balance_populated
和 pcpu_reclaim_populated
, 是Linux内核Per-CPU分配器后台平衡机制中相互协作、互为补充的两个核心组件。它们共同的目标是动态维护一个”水位线”——即预先分配并映射好、随时可以被原子分配器使用的”热”页面池, 以确保无锁、高速的原子分配请求总能快速得到满足, 同时在系统空闲时回收多余的页面以节约内存 。
pcpu_balance_populated
: 主动填充页面池此函数扮演着主动防御 的角色。它的核心原理是检查当前”热”页面的数量是否低于预设的最低水位线 (PCPU_EMPTY_POP_PAGES_LOW
), 如果是, 它就会主动从主内存分配器申请新的物理页面来”填充”(populate)per-cpu内存块, 直到页面数量达到最高水位线(PCPU_EMPTY_POP_PAGES_HIGH
) 。
工作流程与原理:
确定目标 : 函数首先计算需要填充多少页面(nr_to_pop
)才能达到最高水位线。它有一个特殊的”应急模式”: 如果之前有原子分配失败过(pcpu_atomic_alloc_failed
), 它会无条件地尝试填充到最高水位, 以尽快恢复服务能力。
选择填充位置 : 它会优先选择那些已经被部分使用的块(chunk
)来进行填充。这样做是为了减少内存碎片 , 尽量将分配集中在少数几个块中, 而不是让很多块都处于半满状态。
“锁-释放-重锁”执行 : 与pcpu_balance_free
类似, 它在循环中执行耗时的页面分配操作(pcpu_populate_chunk
)时, 会临时释放pcpu_lock
自旋锁, 以避免长时间阻塞快路径。分配完成后, 它会重新获取锁, 以原子方式更新块的populated
位图和全局计数器。
创建新块 : 如果遍历完所有现存的块都无法满足填充目标(例如, 所有块都已满), 函数会尝试创建一个全新的块(pcpu_create_chunk
), 然后通过goto retry_pop
重新开始整个填充过程。
无IO分配 : 它使用GFP_KERNEL | __GFP_NORETRY | __GFP_NOWARN
作为内存分配标志。__GFP_NORETRY
和__GFP_NOWARN
是很重要的优化: 它告诉主内存分配器, “请尽力而为, 但如果现在没有可用内存, 不要进入耗时的内存回收流程, 也不要打印OOM警告, 直接失败返回即可”。这是因为后台平衡工作是一种”尽力而为”的预取机制, 它不应该在系统内存紧张时通过触发内存回收来加剧系统压力。真正的内存压力应该由实际的分配请求来触发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 static void pcpu_balance_populated (void ) { const gfp_t gfp = GFP_KERNEL | __GFP_NORETRY | __GFP_NOWARN; struct pcpu_chunk *chunk ; int slot, nr_to_pop, ret; lockdep_assert_held(&pcpu_lock); retry_pop: if (pcpu_atomic_alloc_failed) { nr_to_pop = PCPU_EMPTY_POP_PAGES_HIGH; pcpu_atomic_alloc_failed = false ; } else { nr_to_pop = clamp(PCPU_EMPTY_POP_PAGES_HIGH - pcpu_nr_empty_pop_pages, 0 , PCPU_EMPTY_POP_PAGES_HIGH); } for (slot = pcpu_size_to_slot(PAGE_SIZE); slot <= pcpu_free_slot; slot++) { if (!nr_to_pop) break ; list_for_each_entry(chunk, &pcpu_chunk_lists[slot], list ) { nr_unpop = chunk->nr_pages - chunk->nr_populated; if (nr_unpop) break ; } if (!nr_unpop) continue ; for_each_clear_bitrange(rs, re, chunk->populated, chunk->nr_pages) { int nr = min_t (int , re - rs, nr_to_pop); spin_unlock_irq(&pcpu_lock); ret = pcpu_populate_chunk(chunk, rs, rs + nr, gfp); cond_resched(); spin_lock_irq(&pcpu_lock); if (!ret) { nr_to_pop -= nr; pcpu_chunk_populated(chunk, rs, rs + nr); } else { nr_to_pop = 0 ; } if (!nr_to_pop) break ; } } if (nr_to_pop) { spin_unlock_irq(&pcpu_lock); chunk = pcpu_create_chunk(gfp); cond_resched(); spin_lock_irq(&pcpu_lock); if (chunk) { pcpu_chunk_relocate(chunk, -1 ); goto retry_pop; } } }
pcpu_reclaim_populated
: 回收空闲的已填充页面此函数扮演着被动清理 的角色。当per-cpu的free
操作导致某个块中的某些页面变为空闲时, 这些页面仍然占据着物理内存。此函数的原理是扫描那些被标记为”待回收”(to_depopulate
)的块, 识别出其中完全空闲的页面, 并将这些页面返还给主内存系统(“去填充” - depopulate) 。
工作流程与原理:
处理”待回收”列表 : 函数只处理pcpu_to_depopulate_slot
列表中的块。一个块在何种条件下被放入这个列表是由free
路径的逻辑决定的(通常是当块的空闲空间超过某个阈值时)。
反向扫描 : 它以反向 顺序扫描块中的页面。这是一个重要的优化, 目的是将所有仍在使用的分配”压缩”到块的前端 , 从而让块的后端更容易形成连续的大片空闲页面, 使得整页回收的可能性最大化。
“锁-释放-重锁”执行 : 与填充操作类似, 它在执行pcpu_depopulate_chunk
这个慢速操作时也会释放pcpu_lock
。
批量TLB刷新 : “去填充”操作(本质上是unmap
)会导致TLB(翻译后备缓冲器)中的条目失效。为了摊销昂贵的TLB刷新操作的成本, 此函数会记录下在一个块中所有被释放的页面范围, 然后在处理完整个块后, 调用pcpu_post_unmap_tlb_flush
来执行一次批量的TLB刷新 。
块的最终归宿 : 在扫描完一个块后, 会根据其最终状态决定其去向:
重新整合(Reintegrate) : 如果块变为了完全空闲, 或者全局的”热”页面水位线过低需要紧急补充, 这个块会被pcpu_reintegrate_chunk
重新放回到活跃的分配列表中。
搁置(Sideline) : 如果块仍然包含部分分配, 但已经被清理过, 它会被移动到pcpu_sidelined_slot
搁置列表中, 暂时不参与新的分配, 除非系统内存非常紧张。
在STM32H750单核系统上, pcpu_lock
的保护作用、”锁-释放-重锁”模式避免阻塞快路径的原理、以及cond_resched
维持系统响应性的机制都与之前分析的函数完全相同。这两个函数共同构成了一个精巧的后台内存池管理器, 确保了per-cpu分配器在提供极高性能的同时, 也能高效地利用系统内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 static void pcpu_reclaim_populated (void ) { struct pcpu_chunk *chunk ; struct pcpu_block_md *block ; int freed_page_start, freed_page_end; int i, end; bool reintegrate; lockdep_assert_held(&pcpu_lock); while ((chunk = list_first_entry_or_null( &pcpu_chunk_lists[pcpu_to_depopulate_slot], struct pcpu_chunk, list ))) { WARN_ON(chunk->immutable); for (i = chunk->nr_pages - 1 , end = -1 ; i >= 0 ; i--) { if (pcpu_nr_empty_pop_pages < PCPU_EMPTY_POP_PAGES_HIGH) { reintegrate = true ; break ; } block = chunk->md_blocks + i; if (block->contig_hint == PCPU_BITMAP_BLOCK_BITS && test_bit(i, chunk->populated)) { if (end == -1 ) end = i; if (i > 0 ) continue ; i--; } if (end == -1 ) continue ; spin_unlock_irq(&pcpu_lock); pcpu_depopulate_chunk(chunk, i + 1 , end + 1 ); cond_resched(); spin_lock_irq(&pcpu_lock); pcpu_chunk_depopulated(chunk, i + 1 , end + 1 ); freed_page_start = min(freed_page_start, i + 1 ); freed_page_end = max(freed_page_end, end + 1 ); end = -1 ; } if (freed_page_start < freed_page_end) { spin_unlock_irq(&pcpu_lock); pcpu_post_unmap_tlb_flush(chunk, freed_page_start, freed_page_end); cond_resched(); spin_lock_irq(&pcpu_lock); } if (reintegrate || chunk->free_bytes == pcpu_unit_size) pcpu_reintegrate_chunk(chunk); else list_move_tail(&chunk->list , &pcpu_chunk_lists[pcpu_sidelined_slot]); } }
Per-CPU 异步平衡工作 此代码片段展示了Linux内核中per-cpu变量分配器 的后台维护机制。per-cpu分配器是一种高度优化的内存分配方案, 它为系统中的每个CPU核心都预留了独立的内存区域, 用于存储同一变量的不同副本。当内核代码需要访问一个per-cpu变量时, 它可以直接访问其当前CPU核心的私有副本, 从而完全避免了昂贵的锁操作和缓存行伪共享(cache line false sharing) , 极大地提升了性能。
然而, 这种高性能的分配/释放”快路径”(fast-path)背后, 需要一个”慢路径”(slow-path)的维护机制来管理其内部的内存池。这个维护机制就是由本代码片段中的**异步平衡工作(asynchronous balance work)**来实现的。
核心原理:
问题 : per-cpu分配器将其内存池划分为多个”块”(chunks), 每个块由一或多页内存组成。在使用过程中, 某些块可能会被完全释放变为空闲, 而当空闲内存不足时, 又需要从主内存分配器申请新的页面来”填充”(populate)块。这些分配/释放整页内存的操作相对耗时, 且可能需要睡眠(例如, 等待内存回收), 因此绝对不能在要求高速、原子性的”快路径”(即alloc_percpu
/free_percpu
调用)中直接执行。
解决方案 : 将这些耗时的维护工作(平衡内存池)异步化 。当快路径代码检测到内存池状态失衡时(例如, 可用空间低于某个阈值, 或空闲块过多), 它不会自己去处理, 而是仅仅调用pcpu_schedule_balance_work
来调度一个后台工作 。
延迟启动 : per-cpu分配器本身在内核启动的极早期阶段就需要被初始化, 此时工作队列(workqueue)子系统甚至还不可用。因此, 内核采用了一种延迟启动策略:
pcpu_async_enabled
全局标志在启动时默认为false
。
当内核初始化进行到subsys_initcall
阶段时, 工作队列已经可用, percpu_enable_async
函数会被调用, 将pcpu_async_enabled
设置为true
。从此, 异步平衡机制才正式启用。
后台执行 : 内核的工作队列线程会在稍后的某个时间点, 从队列中取出pcpu_balance_work
这个工作项, 并执行其处理函数pcpu_balance_workfn
。这个函数运行在标准的内核线程上下文中, 可以安全地执行加锁、内存分配等可能导致睡眠的操作。
代码逐行解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 static void pcpu_balance_workfn (struct work_struct *work) { unsigned int flags = memalloc_noio_save(); mutex_lock(&pcpu_alloc_mutex); spin_lock_irq(&pcpu_lock); pcpu_balance_free(false ); pcpu_reclaim_populated(); pcpu_balance_populated(); pcpu_balance_free(true ); spin_unlock_irq(&pcpu_lock); mutex_unlock(&pcpu_alloc_mutex); memalloc_noio_restore(flags); } static void pcpu_balance_workfn (struct work_struct *work) ;static DECLARE_WORK (pcpu_balance_work, pcpu_balance_workfn) ;static bool pcpu_async_enabled __read_mostly;static bool pcpu_atomic_alloc_failed;static void pcpu_schedule_balance_work (void ) { if (pcpu_async_enabled) schedule_work(&pcpu_balance_work); } static int __init percpu_enable_async (void ) { pcpu_async_enabled = true ; return 0 ; } subsys_initcall(percpu_enable_async);