前言
紧跟前文 [linux内存管理] 第044篇 per-CPU基础知识以及per-CPU分配器的初始化,我们已经介绍了per-CPU的一些基础的概念,了解到了percpu的memory分布情况,介绍了percpu的初始化流程,reserved chunk和first chunk 的创建等流程,我们也通过实验来计算验证per-CPU的变量地址。本章开始介绍percpu的相关的几种类型,分别为静态per-CPU变量和动态per-CPU变量。
静态per-CPU变量
静态per-CPU变量的定义与声明
使用宏DECLARE_PER_CPU(type, name)声明per-CPU变量,使用宏DEFINE_PER_CPU(type, name)定义per-CPU变量,如下所示:
// include/linux/percpu-defs.h
/*
* Variant on the per-CPU variable declaration/definition theme used for
* ordinary per-CPU variables.
*/
#define DECLARE_PER_CPU(type, name) \
DECLARE_PER_CPU_SECTION(type, name, "")
#define DEFINE_PER_CPU(type, name) \
DEFINE_PER_CPU_SECTION(type, name, "")
DEFINE_PER_CPU(type, name)为系统中每个处理器定义了一个类型为type,名字为name的变量实例。如果要在其它地方声明该变量,要使用DECLARE_PER_CPU(type, name)。
我们将DEFINE_PER_CPU继续展开。。。
#define DEFINE_PER_CPU_SECTION(type, name, sec) \
__PCPU_ATTRS(sec) __typeof__(type) name
#define __PCPU_ATTRS(sec) \
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
PER_CPU_ATTRIBUTES
#define PER_CPU_BASE_SECTION ".data..percpu"
前面的 __percpu 只是代码可读性的标记,表示这是个 percpu 变量,没有实际意义;
通过 attribute((section(...))) 标记该变量处于某 section,参数 sec 为.data..percpu 的后缀。
最终展开就是:
__attribute__((section(".data..percpu"))) __typeof__(type) name我们这里以一个案例说明:
static DEFINE_PER_CPU(u64, nr_prod_sum);展开后,即为:
static __attribute__((section(".data..percpu"))) u64 nr_prod_sum就是在 .data..percpu 段定义一个u64类型的变量nr_prod_sum。
percpu 静态变量的定义根据不同的用途,会被定义在不同的数据段中
这些定义使用在不同的场合,主要的factor包括:
该变量在section中的位置
该变量的对齐方式
该变量对SMP和UP的处理不同
访问per cpu的形态
例如:
如果你准备定义的per cpu变量是要求按照page对齐的,那么在定义该per cpu变量的时候需要使用DECLARE_PER_CPU_PAGE_ALIGNED。
如果只要求在SMP的情况下对齐到cache line,那么使用DECLARE_PER_CPU_SHARED_ALIGNED来定义该per cpu变量。
静态per-CPU变量的API接口
静态定义的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)
#define get_cpu_ptr(var) \
({ \
preempt_disable(); \
this_cpu_ptr(var); \
})
#define put_cpu_ptr(var) \
do { \
(void)(var); \
preempt_enable(); \
} while (0)通常,get_cpu_var() 和 put_cpu_var() 都是成对出现,而且伴随着内核抢占的使能与关闭。
这里也提一个接口per_cpu,可以获取别的处理器的per-CPU数据,但是不会禁止内核抢占,也没有提供任何形式的加锁保护。
#define per_cpu_ptr(ptr, cpu) \
({ \
__verify_pcpu_ptr(ptr); \
SHIFT_PERCPU_PTR((ptr), per_cpu_offset((cpu))); \
})
#define per_cpu(var, cpu) (*per_cpu_ptr(&(var), cpu))
那么就不难看出 get_cpu_var的重点函数就是 this_cpu_ptr 。
this_cpu_ptr 的实现
#define this_cpu_ptr(ptr) raw_cpu_ptr(ptr)
#define raw_cpu_ptr(ptr) \
({ \
__verify_pcpu_ptr(ptr); \
arch_raw_cpu_ptr(ptr); \
})通过上面代码我们得知 this_cpu_ptr() 其实就是调用 raw_cpu_ptr() 。该函数首先对 ptr 做指针检查,确定该指针是 percpu 类型指针,然后调用 arch_raw_cpu_ptr() :
#ifndef __my_cpu_offset
#define __my_cpu_offset per_cpu_offset(raw_smp_processor_id())
#endif
/*
* Arch may define arch_raw_cpu_ptr() to provide more efficient address
* translations for raw_cpu_ptr().
*/
#ifndef arch_raw_cpu_ptr
#define arch_raw_cpu_ptr(ptr) SHIFT_PERCPU_PTR(ptr, __my_cpu_offset)
#endif该宏如上面的代码,调用的是 per_cpu_offset() ,也就是 __per_cpu_offset 数组中的值,其中数组的索引为 raw_smp_processor_id() 的值,该值为 percpu 变量 cpu_number 的值,也就是当前 cpu id。
per_cpu_offset 的定义如下:
// include/asm-generic/percpu.h
#ifdef CONFIG_SMP
/*
* per_cpu_offset() is the offset that has to be added to a
* percpu variable to get to the instance for a certain processor.
*
* Most arches use the __per_cpu_offset array for those offsets but
* some arches have their own ways of determining the offset (x86_64, s390).
*/
#ifndef __per_cpu_offset
extern unsigned long __per_cpu_offset[NR_CPUS];
#define per_cpu_offset(x) (__per_cpu_offset[x])
#endif而 SHIFT_PERCPU_PTR 的定义如下:
// include/linux/percpu-defs.h
#define SHIFT_PERCPU_PTR(__p, __offset) \
RELOC_HIDE((typeof(*(__p)) __kernel __force *)(__p), (__offset))
// include/linux/compiler.h
#ifndef RELOC_HIDE
# define RELOC_HIDE(ptr, off) \
({ unsigned long __ptr; \
__ptr = (unsigned long) (ptr); \
(typeof(ptr)) (__ptr + (off)); })
#endif
可以看到其实就是一个ptr地址+偏移量offset
静态per-CPU变量的链接
内核在编译链接时会把所有静态定义的per-CPU变量统一放到".data…percpu"section中,链接器生成__per_cpu_start和__per_cpu_end两个变量来表示该section的起始和结束地址。
// include/asm-generic/vmlinux.lds.h
/**
* PERCPU_INPUT - the percpu input sections
* @cacheline: cacheline size
*
* The core percpu section names and core symbols which do not rely
* directly upon load addresses.
*
* @cacheline is used to align subsections to avoid false cacheline
* sharing between subsections for different purposes.
*/
#define PERCPU_INPUT(cacheline) \
__per_cpu_start = .; \
*(.data..percpu..first) \
. = ALIGN(PAGE_SIZE); \
*(.data..percpu..page_aligned) \
. = ALIGN(cacheline); \
*(.data..percpu..read_mostly) \
. = ALIGN(cacheline); \
*(.data..percpu) \
*(.data..percpu..shared_aligned) \
PERCPU_DECRYPTED_SECTION \
__per_cpu_end = .;我们用readelf来看vmlinux,就可以看到这个段的位置
llvm-readelf.exe -l vmlinux
Elf file type is DYN (Shared object file)
Entry point 0xffffffc008000000
There are 27 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x010000 0xffffffc008000000 0xffffffc008000000 0x010000 0x010000 R E 0x10000
LOAD 0x020000 0xffffffc008010000 0xffffffc008010000 0x1019978 0x1019978 RWE 0x10000
LOAD 0x1040000 0xffffffc009030000 0xffffffc009030000 0x5be87e 0x5be87e RW 0x10000
LOAD 0x15fe880 0xffffffc0095ee880 0xffffffc0095ee880 0x002bf0 0x002bf0 R 0x10000
LOAD 0x1601470 0xffffffc0095f1470 0xffffffc0095f1470 0x0097d4 0x0097d4 R 0x10000
LOAD 0x160ac44 0xffffffc0095fac44 0xffffffc0095fac44 0x00e4cc 0x00e4cc R 0x10000
LOAD 0x1619110 0xffffffc009609110 0xffffffc009609110 0x00329c 0x00329c R 0x10000
LOAD 0x161c3ac 0xffffffc00960c3ac 0xffffffc00960c3ac 0x004c44 0x004c44 R 0x10000
LOAD 0x1620ff0 0xffffffc009610ff0 0xffffffc009610ff0 0x02e652 0x02e652 R 0x10000
LOAD 0x164f648 0xffffffc00963f648 0xffffffc00963f648 0x003b10 0x003b10 R 0x10000
LOAD 0x1653158 0xffffffc009643158 0xffffffc009643158 0x0003a8 0x0003a8 RW 0x10000
LOAD 0x1653500 0xffffffc009643500 0xffffffc009643500 0x003120 0x003120 R 0x10000
LOAD 0x1656620 0xffffffc009646620 0xffffffc009646620 0x000054 0x000054 R 0x10000
LOAD 0x1656674 0xffffffc009646674 0xffffffc009646674 0x560712 0x560712 R 0x10000
LOAD 0x1bb6d88 0xffffffc009ba6d88 0xffffffc009ba6d88 0x0002e0 0x0002e0 R 0x10000
LOAD 0x1bb8000 0xffffffc009ba8000 0xffffffc009ba8000 0x0060f0 0x0060f0 RW 0x10000
LOAD 0x1bbe800 0xffffffc009bae800 0xffffffc009bae800 0x004800 0x004800 R E 0x10000
LOAD 0x1bd0000 0xffffffc009bc0000 0xffffffc009bc0000 0x0638f0 0x0638f0 R E 0x10000
LOAD 0x1c338f0 0xffffffc009c238f0 0xffffffc009c238f0 0x36bb94 0x36bb94 R 0x10000
LOAD 0x1fa5000 0xffffffc009f95000 0xffffffc009f95000 0x016e48 0x016e48 RW 0x10000
LOAD 0x1fbc000 0xffffffc009fac000 0xffffffc009fac000 0x016f50 0x016f50 RW 0x10000
LOAD 0x1fd2f50 0xffffffc009fc2f50 0xffffffc009fc2f50 0x00a438 0x00a438 R 0x10000
LOAD 0x1fe0000 0xffffffc009fd0000 0xffffffc009fd0000 0x1d3cf0 0x1d3cf0 RW 0x10000
LOAD 0x21b3cf0 0xffffffc00a1a3cf0 0xffffffc00a1a3cf0 0x026d10 0x026d10 RW 0x10000
LOAD 0x21db000 0xffffffc00a1cb000 0xffffffc00a1cb000 0x000000 0x09e938 RW 0x10000
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x0
NOTE 0x1656620 0xffffffc009646620 0xffffffc009646620 0x000054 0x000054 R 0x4
Section to Segment mapping:
Segment Sections...
00 .head.text
01 .text
02 .rodata
03 .pci_fixup
04 .builtin_fw __ksymtab
05 __ksymtab_gpl
06 __kcrctab
07 __kcrctab_gpl
08 __ksymtab_strings
09 __param
10 __modver
11 __ex_table
12 .notes
13 .BTF
14 .BTF_ids
15 .hyp.rodata .rodata.hyp_events
16 .rodata.text
17 .init.text .exit.text
18 .altinstructions .eh_frame
19 .init.data
20 .data..percpu .hyp.data..percpu ///////////////////这里
21 .hyp.reloc .rela.dyn .relr.dyn
22 .data
23 __bug_table .hyp.data .mmuoff.data.write .mmuoff.data.read .pecoff_edata_padding
24 .bss
25
26 .notes
None .rodata1 __init_rodata .sbss .debug_aranges .debug_info .debug_abbrev .debug_line .debug_frame .debug_str .debug_loc .debug_ranges .comment .symtab .strtab .shstrtab.data..percpu 节位于 第 20 个程序头(Segment) 中。该段的详细信息如下:
程序头索引:20
类型:
LOAD虚拟地址:
0xffffffc009f95000物理地址:
0xffffffc009f95000文件大小:
0x16e48内存大小:
0x16e48标志:
RW(可读可写)对齐:
0x10000
模块静态per-CPU 变量
模块中定义的静态percpu 变量与内核静态percpu 变量相同,只不过模块静态 percpu 变量在编译后存在于 ko 的percpu section 中。
这些 percpu 静态变量只存在于模块ko 的 percpu section,因为在 insmod 将该模块动态加载到内核中运行时,不可能插入内核静态 percpu section,因为这些 section 在init 数据段,在内核初始化完成后会被 回收 。
因此,系统会为这些模块静态percpu 变量分配一个 reserved region 的内存,通过 percpu chunk 机制管理这个区域,系统中称其 pcpu_reserved_chunk 。在模块加载过程中,会动态从 pcpu_reserved_chunk 中分配对应大小的内存区域,然后将模块中的原始 percpu 变量拷贝到这一块分配好的内存区域中。
在模块释放的时候,也会将这部分从 pcpu_reserved_chunk 分配的内存回收,以备其他模块分配使用。
// include/linux/module.h
struct module {
//...
#ifdef CONFIG_SMP
/* Per-cpu data. */
void __percpu *percpu;
unsigned int percpu_size;
#endif
//...
}模块per-CPU变量相关API
#ifdef CONFIG_SMP
/**
* mod_percpu() - 获取模块的 per-CPU 基地址
* @mod: 目标模块
*
* 返回指向模块 per-CPU 数据区域的指针(__percpu 标记)。
*/
static inline void __percpu *mod_percpu(struct module *mod)
{
return mod->percpu;
}
/**
* percpu_modalloc() - 为模块分配 per-CPU 内存
* @mod: 模块结构体
* @info: 模块加载信息,包含节区头等
*
* 从系统预留的 per-CPU 区域分配内存,大小和对齐方式由 .data..percpu 节决定。
* 分配成功时设置 mod->percpu 和 mod->percpu_size,否则返回 -ENOMEM。
*/
static int percpu_modalloc(struct module *mod, struct load_info *info)
{
Elf_Shdr *pcpusec = &info->sechdrs[info->index.pcpu]; /* 指向 .data..percpu 节区头 */
unsigned long align = pcpusec->sh_addralign; /* 节区要求的对齐 */
if (!pcpusec->sh_size) /* 没有 percpu 数据,无需分配 */
return 0;
if (align > PAGE_SIZE) { /* 对齐要求不能超过页大小 */
pr_warn("%s: per-cpu alignment %li > %li\n",
mod->name, align, PAGE_SIZE);
align = PAGE_SIZE; /* 强制降低到页对齐 */
}
/* 从系统预留的 percpu 区域分配内存(静态区域) */
mod->percpu = __alloc_reserved_percpu(pcpusec->sh_size, align);
if (!mod->percpu) {
pr_warn("%s: Could not allocate %lu bytes percpu data\n",
mod->name, (unsigned long)pcpusec->sh_size);
return -ENOMEM;
}
mod->percpu_size = pcpusec->sh_size; /* 记录分配大小 */
return 0;
}
/**
* percpu_modfree() - 释放模块的 per-CPU 内存
* @mod: 模块结构体
*/
static void percpu_modfree(struct module *mod)
{
free_percpu(mod->percpu); /* 归还给 percpu 分配器 */
}
/**
* find_pcpusec() - 查找 .data..percpu 节区在节区表中的索引
* @info: 模块加载信息
*
* 返回节区索引,若未找到则返回 0。
*/
static unsigned int find_pcpusec(struct load_info *info)
{
return find_sec(info, ".data..percpu");
}
/**
* percpu_modcopy() - 将 per-CPU 模板数据复制到每个 CPU 的副本
* @mod: 模块结构体
* @from: 模板数据起始地址(位于模块映像中)
* @size: 要复制的字节数
*
* 遍历所有可能的 CPU,将模板数据复制到每个 CPU 对应的 per-CPU 区域。
*/
static void percpu_modcopy(struct module *mod,
const void *from, unsigned long size)
{
int cpu;
for_each_possible_cpu(cpu)
/* per_cpu_ptr(mod->percpu, cpu) 得到该 CPU 的副本起始地址 */
memcpy(per_cpu_ptr(mod->percpu, cpu), from, size);
}
/**
* __is_module_percpu_address() - 检查地址是否属于任何模块的 per-CPU 区域(内部实现)
* @addr: 要检查的地址
* @can_addr: 可选输出参数,返回调整后的规范地址(指向引导 CPU 的对应位置)
*
* 遍历所有已加载模块,对每个模块遍历所有可能的 CPU,判断 addr 是否落在该 CPU 的 percpu 区域内。
* 如果匹配且 can_addr 非空,则计算出该地址相对于本模块 percpu 基址的偏移,然后加上引导 CPU 的基址,
* 得到“规范”地址(即该变量在引导 CPU 上的地址)。
*
* 返回 true 表示地址属于某个模块的 percpu 区域。
*/
bool __is_module_percpu_address(unsigned long addr, unsigned long *can_addr)
{
struct module *mod;
unsigned int cpu;
preempt_disable(); /* 防止模块列表在遍历期间被修改 */
list_for_each_entry_rcu(mod, &modules, list) {
if (mod->state == MODULE_STATE_UNFORMED) /* 模块尚未完全形成,跳过 */
continue;
if (!mod->percpu_size) /* 模块没有 percpu 数据 */
continue;
for_each_possible_cpu(cpu) {
void *start = per_cpu_ptr(mod->percpu, cpu); /* 当前 CPU 的 percpu 区域起始 */
void *va = (void *)addr;
if (va >= start && va < start + mod->percpu_size) {
if (can_addr) {
/* 计算 addr 相对于当前 CPU 副本的偏移 */
*can_addr = (unsigned long) (va - start);
/* 将偏移加上引导 CPU 的基址,得到规范地址 */
*can_addr += (unsigned long)
per_cpu_ptr(mod->percpu,
get_boot_cpu_id());
}
preempt_enable();
return true;
}
}
}
preempt_enable();
return false;
}
/**
* is_module_percpu_address() - 检查地址是否来自模块静态 per-CPU 区域
* @addr: 要测试的地址
*
* 返回 true 表示地址属于某个模块的 percpu 区域。
*/
bool is_module_percpu_address(unsigned long addr)
{
return __is_module_percpu_address(addr, NULL);
}
#else /* !CONFIG_SMP */
/* 非 SMP 下的 stub 实现(略) */
#endif模块加载时的per-CPU内存分配
static int load_module(struct load_info *info, const char __user *uargs,
int flags)
{
//...
err = percpu_modalloc(mod, info);
//...
}
/**
* percpu_modalloc() - 为模块分配 per-CPU 内存
* @mod: 模块结构体
* @info: 模块加载信息,包含节区头等
*
* 从系统预留的 per-CPU 区域分配内存,大小和对齐方式由 .data..percpu 节决定。
* 分配成功时设置 mod->percpu 和 mod->percpu_size,否则返回 -ENOMEM。
*/
static int percpu_modalloc(struct module *mod, struct load_info *info)
{
Elf_Shdr *pcpusec = &info->sechdrs[info->index.pcpu]; /* 指向 .data..percpu 节区头 */
unsigned long align = pcpusec->sh_addralign; /* 节区要求的对齐 */
if (!pcpusec->sh_size) /* 没有 percpu 数据,无需分配 */
return 0;
if (align > PAGE_SIZE) { /* 对齐要求不能超过页大小 */
pr_warn("%s: per-cpu alignment %li > %li\n",
mod->name, align, PAGE_SIZE);
align = PAGE_SIZE; /* 强制降低到页对齐 */
}
/* 从系统预留的 percpu 区域分配内存(静态区域) */
mod->percpu = __alloc_reserved_percpu(pcpusec->sh_size, align);
if (!mod->percpu) {
pr_warn("%s: Could not allocate %lu bytes percpu data\n",
mod->name, (unsigned long)pcpusec->sh_size);
return -ENOMEM;
}
mod->percpu_size = pcpusec->sh_size; /* 记录分配大小 */
return 0;
}
/**
* __alloc_reserved_percpu - allocate reserved percpu area
* @size: size of area to allocate in bytes
* @align: alignment of area (max PAGE_SIZE)
*
* Allocate zero-filled percpu area of @size bytes aligned at @align
* from reserved percpu area if arch has set it up; otherwise,
* allocation is served from the same dynamic area. Might sleep.
* Might trigger writeouts.
*
* CONTEXT:
* Does GFP_KERNEL allocation.
*
* RETURNS:
* Percpu pointer to the allocated area on success, NULL on failure.
*/
void __percpu *__alloc_reserved_percpu(size_t size, size_t align)
{
return pcpu_alloc(size, align, true, GFP_KERNEL); // 第三个参数为true时,就再reserved区域申请
}加载流程如下:
计算大小:加载器解析模块的 ELF 文件,获取
.data..percpu段的大小,并赋值给mod->percpu_size。申请内存:关键一步来了。内核调用
__alloc_reserved_percpu函数,为这个模块的 per-cpu 变量申请内存。这个函数会优先从CPU unit的
reserved区域中分配内存。如果
reserved区域空间不足或未配置,它才会退回到普通的动态 per-cpu 内存池中分配。
初始化副本:分配成功后,
__alloc_reserved_percpu返回一个指向 CPU0 的这份 per-cpu 数据副本的指针。这个指针被保存在mod->percpu中。复制模板数据:加载器会将模块
.data..percpu段中的模板数据,复制到mod->percpu指向的 CPU0 副本中。对于其他 CPU,内核会在它们的 per-cpu 区域中,根据这个模板,在对应的偏移位置初始化好各自的数据副本。
小实验
// percpu_test.c
#include "linux/printk.h"
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/percpu.h>
#include <linux/smp.h>
#include <linux/cpu.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("percpu-test");
MODULE_DESCRIPTION("percpu memory layout test");
/* 定义一个 per-cpu 变量 */
DEFINE_PER_CPU(int, percpu_var);
DEFINE_PER_CPU(int, percpu_var2);
static int __init percpu_test_init(void)
{
int cpu;
pr_info("percpu_test: module loaded\n");
pr_info("__per_cpu_start: %px\n", __per_cpu_start);
pr_info("__per_cpu_end: %px\n", __per_cpu_end);
pr_info("pcpu_base_addr: %px\n", pcpu_base_addr);
pr_info("percpu_test: printing percpu addresses and values\n");
for_each_possible_cpu(cpu) {
int *addr;
int val;
addr = &per_cpu(percpu_var, cpu);
val = per_cpu(percpu_var, cpu);
if (is_module_percpu_address((unsigned long)addr))
{
pr_info("percpu_var is a module percpu var\n");
} else {
pr_info("percpu_var is not a module percpu var\n");
}
pr_info("percpu_var - CPU %d: addr=%px value=%d\n", cpu, addr, val);
int *addr2;
int val2;
addr2 = &per_cpu(percpu_var2, cpu);
val2 = per_cpu(percpu_var2, cpu);
pr_info("percpu_var2 - CPU %d: addr=%px value=%d\n", cpu, addr2, val2);
}
/* 给每个 CPU 写入不同的值 */
for_each_possible_cpu(cpu) {
int *addr;
int val;
addr = &per_cpu(percpu_var, cpu);
val = per_cpu(percpu_var, cpu);
per_cpu(percpu_var, cpu) = cpu * 100;
}
pr_info("percpu_test: printing percpu addresses and values\n");
for_each_possible_cpu(cpu) {
int *addr;
int val;
addr = &per_cpu(percpu_var, cpu);
val = per_cpu(percpu_var, cpu);
pr_info("percpu_var - CPU %d: addr=%px value=%d\n", cpu, addr, val);
}
return 0;
}
static void __exit percpu_test_exit(void)
{
pr_info("percpu_test: module unloaded\n");
}
module_init(percpu_test_init);
module_exit(percpu_test_exit);当我使用build-in的方式编译进内核时,输出结果:
[ 0.840431] percpu_test: module loaded
[ 0.840703] __per_cpu_start: ffffffc009af0000
[ 0.841056] __per_cpu_end: ffffffc009afbb28
[ 0.841342] pcpu_base_addr: ffffff803fd8b000
[ 0.841695] percpu_test: printing percpu addresses and values
[ 0.842342] percpu_var is not a module percpu var
[ 0.842600] percpu_var - CPU 0: addr=ffffff803fd94c24 value=0
[ 0.842971] percpu_var2 - CPU 0: addr=ffffff803fd94c28 value=0
[ 0.843298] percpu_var is not a module percpu var
[ 0.843895] percpu_var - CPU 1: addr=ffffff803fda9c24 value=0
[ 0.844397] percpu_var2 - CPU 1: addr=ffffff803fda9c28 value=0
[ 0.844653] percpu_var is not a module percpu var
[ 0.844856] percpu_var - CPU 2: addr=ffffff803fdbec24 value=0
[ 0.845145] percpu_var2 - CPU 2: addr=ffffff803fdbec28 value=0
[ 0.845443] percpu_var is not a module percpu var
[ 0.845686] percpu_var - CPU 3: addr=ffffff803fdd3c24 value=0
[ 0.845934] percpu_var2 - CPU 3: addr=ffffff803fdd3c28 value=0
[ 0.846299] percpu_test: printing percpu addresses and values
[ 0.846596] percpu_var - CPU 0: addr=ffffff803fd94c24 value=0
[ 0.846856] percpu_var - CPU 1: addr=ffffff803fda9c24 value=100
[ 0.847093] percpu_var - CPU 2: addr=ffffff803fdbec24 value=200
[ 0.847389] percpu_var - CPU 3: addr=ffffff803fdd3c24 value=300
当我编译成模块时,同时
bool __is_module_percpu_address(unsigned long addr, unsigned long *can_addr)
{
struct module *mod;
unsigned int cpu;
preempt_disable();
list_for_each_entry_rcu(mod, &modules, list) {
if (mod->state == MODULE_STATE_UNFORMED)
continue;
if (!mod->percpu_size)
continue;
for_each_possible_cpu(cpu) {
void *start = per_cpu_ptr(mod->percpu, cpu);
void *va = (void *)addr;
if (va >= start && va < start + mod->percpu_size) {
if (can_addr) {
*can_addr = (unsigned long) (va - start);
*can_addr += (unsigned long)
per_cpu_ptr(mod->percpu,
get_boot_cpu_id());
}
preempt_enable();
// 打印模块的name
printk("[ILIUQI] mod->name = %s\n", mod->name);
return true;
}
}
}
preempt_enable();
return false;
}
bool is_module_percpu_address(unsigned long addr)
{
return __is_module_percpu_address(addr, NULL);
}
// export符号表
EXPORT_SYMBOL_GPL(is_module_percpu_address);
输出结果:
[root@virt-machine ]#insmod /mnt/percpu_test.ko
[ 28.842348] percpu_test: module loaded
[ 28.842610] percpu_test: printing percpu addresses and values
[ 28.843763] [ILIUQI] mod->name = percpu_test
[ 28.843995] percpu_var is a module percpu var
[ 28.844202] percpu_var - CPU 0: addr=ffffff803fd96b28 value=0
[ 28.844630] percpu_var2 - CPU 0: addr=ffffff803fd96b2c value=0
[ 28.845093] [ILIUQI] mod->name = percpu_test
[ 28.845256] percpu_var is a module percpu var
[ 28.845476] percpu_var - CPU 1: addr=ffffff803fdabb28 value=0
[ 28.845794] percpu_var2 - CPU 1: addr=ffffff803fdabb2c value=0
[ 28.846166] [ILIUQI] mod->name = percpu_test
[ 28.846374] percpu_var is a module percpu var
[ 28.846555] percpu_var - CPU 2: addr=ffffff803fdc0b28 value=0
[ 28.846889] percpu_var2 - CPU 2: addr=ffffff803fdc0b2c value=0
[ 28.847135] [ILIUQI] mod->name = percpu_test
[ 28.847337] percpu_var is a module percpu var
[ 28.847539] percpu_var - CPU 3: addr=ffffff803fdd5b28 value=0
[ 28.847787] percpu_var2 - CPU 3: addr=ffffff803fdd5b2c value=0
[ 28.848304] percpu_test: printing percpu addresses and values
[ 28.848611] percpu_var - CPU 0: addr=ffffff803fd96b28 value=0
[ 28.849234] percpu_var - CPU 1: addr=ffffff803fdabb28 value=100
[ 28.850383] percpu_var - CPU 2: addr=ffffff803fdc0b28 value=200
[ 28.851176] percpu_var - CPU 3: addr=ffffff803fdd5b28 value=300
动态per-CPU变量
/**
* __alloc_percpu_gfp - allocate dynamic percpu area
* @size: size of area to allocate in bytes
* @align: alignment of area (max PAGE_SIZE)
* @gfp: allocation flags
*
* Allocate zero-filled percpu area of @size bytes aligned at @align. If
* @gfp doesn't contain %GFP_KERNEL, the allocation doesn't block and can
* be called from any context but is a lot more likely to fail. If @gfp
* has __GFP_NOWARN then no warning will be triggered on invalid or failed
* allocation requests.
*
* RETURNS:
* Percpu pointer to the allocated area on success, NULL on failure.
*/
void __percpu *__alloc_percpu_gfp(size_t size, size_t align, gfp_t gfp)
{
return pcpu_alloc(size, align, false, gfp);
}
EXPORT_SYMBOL_GPL(__alloc_percpu_gfp);
/**
* __alloc_percpu - allocate dynamic percpu area
* @size: size of area to allocate in bytes
* @align: alignment of area (max PAGE_SIZE)
*
* Equivalent to __alloc_percpu_gfp(size, align, %GFP_KERNEL).
*/
void __percpu *__alloc_percpu(size_t size, size_t align)
{
return pcpu_alloc(size, align, false, GFP_KERNEL);
}
EXPORT_SYMBOL_GPL(__alloc_percpu);最终调用的是 pcpu_alloc() 函数,区别在于和上面静态分配时的函数 __alloc_reserved_percpu() 调用的第三个参数为 true,即从 reserved chunk 中申请 percpu 内存。
pcpu_alloc
/**
* pcpu_alloc - the percpu allocator
* @size: size of area to allocate in bytes
* @align: alignment of area (max PAGE_SIZE)
* @reserved: allocate from the reserved chunk if available
* @gfp: allocation flags
*
* Allocate percpu area of @size bytes aligned at @align. If @gfp doesn't
* contain %GFP_KERNEL, the allocation is atomic. If @gfp has __GFP_NOWARN
* then no warning will be triggered on invalid or failed allocation
* requests.
*
* RETURNS:
* Percpu pointer to the allocated area on success, NULL on failure.
*/
static void __percpu *pcpu_alloc(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;
/* 根据当前进程的 cpuset 和 memalloc 上下文调整 gfp 标志 */
gfp = current_gfp_context(gfp);
/* 只允许部分标志传递给底层分配器(如伙伴系统) */
pcpu_gfp = gfp & (GFP_KERNEL | __GFP_NORETRY | __GFP_NOWARN);
/* 判断是否为原子分配(不允许睡眠) */
is_atomic = (gfp & GFP_KERNEL) != GFP_KERNEL;
/* 是否需要在失败时打印警告(除非调用者显式禁止) */
do_warn = !(gfp & __GFP_NOWARN);
/*
* 现在最小分配单元为 PCPU_MIN_ALLOC_SIZE,因此对齐必须至少为此值。
* 分配可能因向上取整而产生最多 PCPU_MIN_ALLOC_SIZE - 1 字节的内部碎片。
*/
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;
}
/* 内存 cgroup 预分配钩子,用于记帐,如果失败则返回 NULL */
if (unlikely(!pcpu_memcg_pre_alloc_hook(size, gfp, &objcg)))
return NULL;
/* 非原子分配需要获取 mutex,可能睡眠 */
if (!is_atomic) {
/*
* pcpu_balance_workfn() 在此 mutex 下分配内存,可能等待内存回收。
* 如果分配带 __GFP_NOFAIL,则允许当前任务成为 OOM 受害者(即允许被杀)。
*/
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;
}
}
/* 获取自旋锁保护 percpu 内部数据结构 */
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) {
/* 若在当前 slot 中未找到,但块可能因为碎片需要降级 */
if (slot < PCPU_SLOT_FAIL_THRESHOLD)
pcpu_chunk_move(chunk, 0);
continue;
}
off = pcpu_alloc_area(chunk, bits, bit_align, off);
if (off >= 0) {
/* 分配成功,重新整合块(可能移动其 slot) */
pcpu_reintegrate_chunk(chunk);
goto area_found;
}
}
}
/* 遍历完所有普通块后仍未找到合适空间 */
spin_unlock_irqrestore(&pcpu_lock, flags);
/*
* 没有剩余空间了,需要创建新块。我们不希望多个任务同时创建块,
* 因此加 mutex 序列化,并在加锁后再次检查是否确实没有空闲块。
*/
if (is_atomic) {
/* 原子分配不能阻塞创建新块,直接失败 */
err = "atomic alloc failed, no space left";
goto fail;
}
/* 检查是否真的没有空闲块(在 mutex 保护下) */
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);
/* 将新块插入到适当的 slot */
pcpu_chunk_relocate(chunk, -1);
} else {
/* 在释放锁期间可能有其他任务创建了块,重新获取锁后继续 */
spin_lock_irqsave(&pcpu_lock, flags);
}
/* 重新尝试分配 */
goto restart;
area_found:
/* 记录分配统计信息 */
pcpu_stats_area_alloc(chunk, size);
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);
/* 遍历所有尚未 populated 的页,依次填充 */
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;
}
/* 更新 populated 位图 */
pcpu_chunk_populated(chunk, rs, re);
spin_unlock_irqrestore(&pcpu_lock, flags);
}
/* 释放 mutex(至此已确保所有页已填充) */
mutex_unlock(&pcpu_alloc_mutex);
}
/* 如果全局 empty populated 页数量偏低,则调度后台平衡工作 */
if (pcpu_nr_empty_pop_pages < PCPU_EMPTY_POP_PAGES_LOW)
pcpu_schedule_balance_work();
/* 清零分配的区域(对每个 CPU 都执行) */
for_each_possible_cpu(cpu)
memset((void *)pcpu_chunk_addr(chunk, cpu, 0) + off, 0, size);
/* 将块内地址转换为 percpu 指针(__percpu 注释类型) */
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);
/* 内存 cgroup 后分配钩子,更新记账 */
pcpu_memcg_post_alloc_hook(objcg, chunk, off, size);
return ptr;
fail_unlock:
spin_unlock_irqrestore(&pcpu_lock, flags);
fail:
/* 记录分配失败 tracepoint */
trace_percpu_alloc_percpu_fail(reserved, is_atomic, size, align);
/* 非原子分配且允许警告时打印错误信息(限制次数) */
if (!is_atomic && do_warn && warn_limit) {
pr_warn("allocation failed, size=%zu align=%zu atomic=%d, %s\n",
size, align, is_atomic, err);
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 */
mutex_unlock(&pcpu_alloc_mutex);
}
/* 清理内存 cgroup 记账 */
pcpu_memcg_post_alloc_hook(objcg, NULL, 0, size);
return NULL;
}pcpu_alloc 主要完成以下任务:
参数检查与调整:确保 size 和 align 符合最小分配单元要求,并转换为以最小分配单元为单位的 bits 值。
保留区分配:若请求来自保留区(reserved)且存在保留 chunk,优先从其中分配。
普通区分配:遍历按空闲大小分组的 chunk 链表(
pcpu_chunk_lists),在每个 chunk 中查找合适的空闲块。创建新 chunk:若无足够空间且允许阻塞,则创建新 chunk 并重新尝试分配。
物理页填充:对于选中的 chunk,确保其虚拟地址范围内的物理页已分配并映射。
清零与返回:将所有 CPU 副本的对应区域清零,并返回可用于
this_cpu_ptr等宏的基地址。
并发控制通过 pcpu_lock(自旋锁)保护内部数据结构,而 pcpu_alloc_mutex(互斥锁)用于序列化可能引起内存回收的阻塞操作。
下面针对几个关键的点来剖析细节部分:
pcpu_find_block_fit
/**
* pcpu_find_block_fit - 查找适合分配的起始块索引
* @chunk: 要搜索的 chunk
* @alloc_bits: 请求大小(以分配单元为单位)
* @align: 对齐要求(以分配单元为单位)
* @pop_only: 是否仅考虑已 populated 的区域
*
* 给定一个 chunk 和分配规格,找到开始搜索空闲区域的偏移量。
* 该函数遍历位图元数据块,找到能保证满足要求的偏移量。
* 它并不完全是 first fit,如果分配不适合某个块或 chunk 的连续空闲 hint,
* 则跳过该块。这种做法偏向于谨慎,以防止过多的迭代。
* 不良的对齐可能导致分配器跳过实际上有空闲区域的块和 chunk。
*
* 返回值:
* 位图中开始搜索的偏移量。
* 如果未找到合适的偏移量,返回 -1。
*/
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;
/*
* 优化:首先检查 chunk 的全局 hint(最大连续空闲区域的大小和对齐)
* 是否能满足分配。如果不能,说明内存压力较大,后续扫描很可能失败,
* 且很快会创建新 chunk,因此直接返回 -1 跳过扫描。
*/
if (!pcpu_check_block_hint(chunk_md, alloc_bits, align))
return -1;
/*
* 从 hint 建议的起始偏移量开始搜索。
* pcpu_next_hint() 根据 chunk 的 hint 返回一个合适的起始点,
* 通常是上次分配结束的位置或 hint 指示的最大空闲区域的起点。
*/
bit_off = pcpu_next_hint(chunk_md, alloc_bits);
bits = 0;
/*
* 使用 pcpu_for_each_fit_region 宏遍历所有可能适合分配的空闲区域。
* 该宏内部会扫描位图,考虑对齐要求,并返回下一个候选区域的起始偏移
* 和该区域的长度(bits)。循环体中对每个候选区域进行检查。
*/
pcpu_for_each_fit_region(chunk, alloc_bits, align, bit_off, bits) {
/*
* 如果要求仅从已 populated 的区域分配(即原子分配等情况),
* 则需要确保当前候选区域的每个页都已经填充物理内存。
* pcpu_is_populated() 检查从 bit_off 开始、长度为 bits 的区域
* 是否全部 populated。如果不是,它会通过 next_off 返回下一个
* 可能适合的偏移量(即跳过未 populated 的部分),然后继续循环。
*/
if (!pop_only || pcpu_is_populated(chunk, bit_off, bits,
&next_off))
break; /* 找到合适的区域,退出循环 */
/* 当前区域未完全 populated,从 next_off 继续搜索 */
bit_off = next_off;
bits = 0; /* 重置 bits,让宏重新计算下一个区域的长度 */
}
/*
* 如果 bit_off 已经到达位图的末尾(即遍历完整个 chunk 的映射位图),
* 说明未找到合适的空闲区域,返回 -1。
*/
if (bit_off == pcpu_chunk_map_bits(chunk))
return -1;
/* 返回找到的起始偏移量,供上层 pcpu_alloc_area 使用 */
return bit_off;
}注释有,但是说实话挺抽象的,所以这里我以一个案例来说明如何找到一个合适的offset?
当前系统的 pcpu_reserved_chunk 这个chunk的信息如下:(这里以reserved chunk为例,dynamic chunk找offset也是一样的)

根据前文对 struct pcpu_chunk 结构体的介绍,我们可以得到以下的信息:
chunk_md成员
nr_bits = 3072:位图共有 3072 个单元,每个单元 4 字节,因此 chunk 总容量为 3072 × 4 = 12288 字节。contig_hint = 1140:当前 chunk 中最大连续空闲块的长度为 1140 个单元,即 1140 × 4 = 4560 字节。contig_hint_start = 1734:该最大连续空闲块的起始单元索引(即从第 1734 个单元开始,对应字节偏移为 1734 × 4 = 6936 字节)。scan_hint_start = 917:建议开始扫描的单元索引(上次扫描或分配留下的提示,对应字节偏移 917 × 4 = 3668 字节)。nr_pages = 3,nr_populated = 3:该 chunk 占用 3 个物理页
假设上层调用 pcpu_alloc 要求从保留块分配 800 字节,对齐为 8 字节,且是原子分配(pop_only = true)。
步骤 1:参数转换
单元大小 = 4 字节,因此 alloc_bits = 800 / 4 = 200 个单元。
对齐要求 4 字节,即起始单元索引必须是 (4/4) = 1 的倍数(因为每个单元 4 字节,1 个单元就是 4 字节对齐)。所以 bit_align = 1。
此时:
size = 800 bytes
alloc_bits= 800 / 4 = 200 bits = 0xC8
align = 4 bytes (若未特别指定)
bit_align = 4 / 4 = 1步骤 2:全局 hint 检查
判断公式
bit_off = ALIGN(contig_hint_start, align) - contig_hint_start;
return bit_off + bits <= contig_hint;也就是:chunk 级别已知的最大连续空闲段,考虑对齐损耗后,能不能装下这次申请
代入数据:
contig_hint = 0x474
contig_hint_start = 0x6C6
alloc_bits = 0x0C8
bit_align = 0x1
因为 align = 1 bit,所以:
ALIGN(0x6C6, 0x1) - 0x6C6 = 0于是:
0 + 0x0C8 <= 0x0474 => 成立说明:
这个 reserved chunk 从全局摘要上看,肯定放得下这 800B。
步骤 3:确定起始搜索点
然后 pcpu_find_block_fit() 会调用:
bit_off = pcpu_next_hint(chunk_md, alloc_bits);pcpu_next_hint() 的规则很简单:
如果
scan_hint存在,而且它明显装不下这次申请,就跳过它否则直接从
first_free开始
chunk_md 是:
scan_hint = 0x0
first_free = 0x54C所以这里没有悬念:
pcpu_next_hint(chunk_md, 0xC8) = 0x54C也就是说,第一次候选搜索起点是 bit 0x54C。
接下来 pcpu_find_block_fit() 不会直接把 0x54C 当成答案,而是进入:
pcpu_for_each_fit_region(chunk, alloc_bits, align, bit_off, bits)这个迭代器实际依赖的是 md_blocks[],不是只看 chunk_md。它会从 bit_off=0x54C 所在的 metadata block 开始,结合每个 block 的:
contig_hintcontig_hint_startright_freeleft_freefirst_free
去找一个“从元数据上保证有希望 fit 的区域”。
PCPU_BITMAP_BLOCK_BITS = PAGE_SIZE / 4 = 4096 / 4 = 1024 = 0x400chunk 一共
nr_pages = 3,所以总 bitmap 大小0xC00bits,正好 3 个 md block。

而起始搜索点为0x54c,也就落入了md_blocks[1],因为一个md_blocks为0x400,所以
0x54C 在 block 1(第二个 block)
block 内偏移 = 0x54C - 0x400 = 0x14C接下来关键一步是 start = pcpu_next_hint(block1, 0xC8)。
这里 pcpu_next_hint() 的三个条件都满足:
scan_hint != 0,即0x0Ccontig_hint_start > scan_hint_start,即0x2C6 > 0x1D4alloc_bits > scan_hint,即0xC8 > 0x0C
因此它不会返回 first_free=0x14C,而是跳过那个只有 0x0C bits 的小 hole
start = scan_hint_start + scan_hint
= 0x1D4 + 0x0C
= 0x1E0这正是 pcpu_next_hint() 的设计目的:请求比 scan_hint 大时,直接越过这个太小的洞
然后 pcpu_next_fit_region() 会设置:
*bit_off = pcpu_block_off_to_off(1, 0x1E0)
= 0x400 + 0x1E0
= 0x5E0所以,最终 pcpu_find_block_fit(...) 返回 0x5E0
pcpu_alloc_area
/**
* pcpu_alloc_area - 在 chunk 中分配一块区域
* @chunk: 要分配的目标 chunk
* @alloc_bits: 需要分配的单元数(每个单元大小为 PCPU_MIN_ALLOC_SIZE)
* @align: 起始单元索引的对齐倍数(例如 align=2 表示起始索引为偶数)
* @start: 开始搜索的位索引(由 pcpu_find_block_fit 返回的候选偏移)
*
* 该函数在 chunk 的分配位图中,从 start 开始寻找满足对齐要求的连续空闲区域,
* 将其标记为已占用,并更新相关的元数据(如 hint、边界位图、空闲字节数等)。
*
* 返回值:分配区域的起始字节偏移(相对于 chunk 基址),失败时返回 -1。
*/
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;
/* 将对齐倍数转换为位图扫描用的掩码:例如 align=2 时,align_mask=1(检查低位是否为0) */
size_t align_mask = (align) ? (align - 1) : 0;
unsigned long area_off = 0, area_bits = 0;
int bit_off, end, oslot;
/* 确保调用时已持有 pcpu_lock 自旋锁 */
lockdep_assert_held(&pcpu_lock);
/* 记录当前 chunk 所在的 slot,供后续 relocation 使用 */
oslot = pcpu_chunk_slot(chunk);
/*
* 设置搜索终点:从 start 开始,到 start + alloc_bits + 块大小 为止,
* 但不超过位图总位数。这是为了控制搜索范围,避免过远的无效扫描。
*/
end = min_t(int, start + alloc_bits + PCPU_BITMAP_BLOCK_BITS,
pcpu_chunk_map_bits(chunk));
/*
* 在位图 chunk->alloc_map 中,从 start 到 end 范围内寻找第一个
* 满足长度 alloc_bits 且起始索引对齐 align_mask 的零位区域。
* area_off 和 area_bits 用于返回找到的区域的起始和长度(用于更新 scan hint)。
*/
bit_off = pcpu_find_zero_area(chunk->alloc_map, end, start, alloc_bits,
align_mask, &area_off, &area_bits);
/* 如果没找到合适区域(bit_off >= end),返回 -1 */
if (bit_off >= end)
return -1;
/*
* 如果 area_bits 非零,说明 pcpu_find_zero_area 在搜索过程中遇到了
* 一个较大的空闲区域(可能是 contig hint 的一部分),用该区域信息
* 更新 chunk 的 scan hint,以优化后续分配。
*/
if (area_bits)
pcpu_block_update_scan(chunk, area_off, area_bits);
/* 在分配位图中将 [bit_off, bit_off+alloc_bits) 区域设置为 1(已占用) */
bitmap_set(chunk->alloc_map, bit_off, alloc_bits);
/* 更新边界位图:在区域的起始和末尾(+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 的空闲字节数(减少分配的字节数) */
chunk->free_bytes -= alloc_bits * PCPU_MIN_ALLOC_SIZE;
/* 如果分配的起始位恰好是 chunk_md->first_free(第一个空闲位),
* 则需要更新 first_free 为下一个空闲位的位置。 */
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);
/* 更新块元数据的 hint(contig hint、scan hint 等),因为分配后空闲区域变化 */
pcpu_block_update_hint_alloc(chunk, bit_off, alloc_bits);
/* 根据更新后的空闲信息,将 chunk 重新移动到合适的 slot 中 */
pcpu_chunk_relocate(chunk, oslot);
/* 返回分配区域的起始字节偏移(单元索引 × 单元大小) */
return bit_off * PCPU_MIN_ALLOC_SIZE;
}紧接上面的案例步骤,通过 pcpu_find_block_fit 我们得到了 0x5e0,这个是我们在位图中(alloc_map)开始搜索的偏移量的起始处。
步骤4:找到满足size的bit_off
我们前面算过:
bit_off = 0x5E0
word_idx = 0x5E0 / 64 = 23 = 0x17
bit_in_word = 0x5E0 % 64 = 32所以对应的 word 地址是:
0xFFFFFF817F25E780 + 23 * 8
= 0xFFFFFF817F25E838
alloc_bits = 800 / 4 = 200 = 0xC8 bits,所以 allocator 需要找的是:
从某个 bit 开始,连续 0xC8 个 0 bit。
而图里很关键的几行是:
0x...E858 : 3F 00 00 00 00 00 00 00
0x...E860 : 00 00 00 00 00 00 00 00
0x...E868 : 00 00 00 00 00 00 00 000x3F = 00111111b,意味着这个 word 的 前 6 个 bit 是 1,后面全是 0。
所以这个 word 里连续空闲的起点就是:
0x6C0 + 6 = 0x6C6这正好和之前 chunk_md.contig_hint_start = 0x6C6、md_blocks[1].right_free = 0x13A、md_blocks[2].left_free = 0x33A 完全一致。也就是:
0x13A + 0x33A = 0x474形成整个 chunk 的最大连续空闲段 [0x6C6, 0x6C6+0x474)
所以 pcpu_find_zero_area() 会在这里命中:
final bit_off = 0x6C6然后 pcpu_alloc_area() 返回的字节 offset 是:
off = 0x6C6 * 4
= 0x1B18 bytes这就是最终落在 chunk 内的偏移
步骤5:转换为percpu地址
pcpu_reserved_chunk->base_addr = 0xFFFFFF8179324000
那么这个 800B 申请返回的 percpu 逻辑位置对应 chunk 内地址就是:
0xFFFFFF8179324000 + 0x1B18
= 0xFFFFFF8179325B18这还是“chunk 基址 + off”的位置;真正访问某个 CPU 的实例时,还要再加上 pcpu_unit_offsets[cpu]。也就是:
addr(cpu_n) = chunk->base_addr + pcpu_unit_offsets[n] + 0x1B18保留区分配逻辑
备注:动态分配不会走到这里
/* 如果要求从保留块分配且保留块存在,则尝试从保留块分配 */
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;
}动态区分配逻辑
这部分看pcpu_alloc函数的注释吧,没什么太大的区别,核心还是在于 pcpu_find_block_fit 以及 pcpu_alloc_area
pcpu_populate_chunk
把某个 chunk 里 [page_start, page_end) 这一段页,对每个 CPU 的 unit 都补上真实页,并映射进该 chunk 的 vmalloc 虚拟地址。
在 percpu-vm.c 里,它的实现很直白,大致就是四步:
pcpu_get_pages():拿一块临时的struct page *数组,用来暂存这次要分配/映射的页。pcpu_alloc_pages(chunk, pages, page_start, page_end, gfp):真正从页分配器拿页。pcpu_map_pages(chunk, pages, page_start, page_end):把这些页映射到这个 chunk 对应的 vmalloc 地址范围里。若映射失败,就回滚并释放刚拿到的页。pcpu_post_map_flush(chunk, page_start, page_end):映射完成后做 cache flush。
源码片段本身就是:
pages = pcpu_get_pages();
if (!pages)
return -ENOMEM;
if (pcpu_alloc_pages(chunk, pages, page_start, page_end, gfp))
return -ENOMEM;
if (pcpu_map_pages(chunk, pages, page_start, page_end)) {
pcpu_free_pages(chunk, pages, page_start, page_end);
return -ENOMEM;
}
pcpu_post_map_flush(chunk, page_start, page_end);
return 0;小总结
pcpu_alloc() 在找到 off 之后,会先把 off,size 换成页范围:
page_start = PFN_DOWN(off)page_end = PFN_UP(off + size)
然后遍历 chunk->populated 里这一段范围内尚未 populated 的 page region,对每个空洞调用 pcpu_populate_chunk()。成功后,再调用 pcpu_chunk_populated() 更新 chunk->populated、nr_populated 等统计信息。全部搞定后,才会 memset() 并返回。
所以从职责划分上看:
pcpu_alloc_area():位图层面“占坑”pcpu_populate_chunk():物理页和页表层面“铺地”pcpu_chunk_populated():更新 populated bitmap / 统计memset():把刚分出来的per CPU 副本清零