AI智能摘要
延续前一篇对 per-CPU 基础与初始化的分析,这一部分聚焦于内核中的静态 per-CPU 变量及其使用方式。之后将问题的重点放在了动态per-CPU变量的分配逻辑上,并通过一个案例来分析分配逻辑的内部细节。
此摘要由AI分析文章内容生成,仅供参考。

前言

紧跟前文 [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 静态变量的定义根据不同的用途,会被定义在不同的数据段中

声明和定义Per-CPU变量的API

描述

所在字段

DECLARE_PER_CPU(type, name)
DEFINE_PER_CPU(type, name)

普通的、没有特殊要求的per cpu变量定义接口函数。没有对齐的要求

.data..percpu

DECLARE_PER_CPU_FIRST(type, name)
DEFINE_PER_CPU_FIRST(type, name)

通过该API定义的per cpu变量位于整个per cpu相关section的最前面。

.data..percpu..first

DECLARE_PER_CPU_SHARED_ALIGNED(type, name)
DEFINE_PER_CPU_SHARED_ALIGNED(type, name)

通过该API定义的per cpu变量在SMP的情况下会对齐到L1 cache line

.data..percpu..shared_aligned

DECLARE_PER_CPU_ALIGNED(type, name)
DEFINE_PER_CPU_ALIGNED(type, name)

需要对齐到L1 cache line

.data..percpu..shared_aligned

DECLARE_PER_CPU_PAGE_ALIGNED(type, name)
DEFINE_PER_CPU_PAGE_ALIGNED(type, name)

为定义page aligned per cpu变量而设定的API接口

.data..percpu..page_aligned

DECLARE_PER_CPU_READ_MOSTLY(type, name)
DEFINE_PER_CPU_READ_MOSTLY(type, name)

通过该API定义的per cpu变量是read mostly的

.data..percpu..read_mostly

这些定义使用在不同的场合,主要的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区域申请
}

加载流程如下:

  1. 计算大小:加载器解析模块的 ELF 文件,获取 .data..percpu 段的大小,并赋值给 mod->percpu_size

  2. 申请内存:关键一步来了。内核调用 __alloc_reserved_percpu 函数,为这个模块的 per-cpu 变量申请内存。

    • 这个函数会优先从CPU unit的 reserved 区域中分配内存。

    • 如果 reserved 区域空间不足或未配置,它才会退回到普通的动态 per-cpu 内存池中分配。

  3. 初始化副本:分配成功后,__alloc_reserved_percpu 返回一个指向 CPU0 的这份 per-cpu 数据副本的指针。这个指针被保存在 mod->percpu 中。

  4. 复制模板数据:加载器会将模块 .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 = 3nr_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_hint

  • contig_hint_start

  • right_free

  • left_free

  • first_free

去找一个“从元数据上保证有希望 fit 的区域”。

  • PCPU_BITMAP_BLOCK_BITS = PAGE_SIZE / 4 = 4096 / 4 = 1024 = 0x400

  • chunk 一共 nr_pages = 3,所以总 bitmap 大小 0xC00 bits,正好 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,即 0x0C

  • contig_hint_start > scan_hint_start,即 0x2C6 > 0x1D4

  • alloc_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 00

0x3F = 00111111b,意味着这个 word 的 前 6 个 bit 是 1,后面全是 0。
所以这个 word 里连续空闲的起点就是:

0x6C0 + 6 = 0x6C6

这正好和之前 chunk_md.contig_hint_start = 0x6C6md_blocks[1].right_free = 0x13Amd_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 里,它的实现很直白,大致就是四步:

  1. pcpu_get_pages():拿一块临时的 struct page * 数组,用来暂存这次要分配/映射的页。

  2. pcpu_alloc_pages(chunk, pages, page_start, page_end, gfp):真正从页分配器拿页。

  3. pcpu_map_pages(chunk, pages, page_start, page_end):把这些页映射到这个 chunk 对应的 vmalloc 地址范围里。若映射失败,就回滚并释放刚拿到的页。

  4. 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->populatednr_populated 等统计信息。全部搞定后,才会 memset() 并返回。

所以从职责划分上看:

  • pcpu_alloc_area():位图层面“占坑”

  • pcpu_populate_chunk():物理页和页表层面“铺地”

  • pcpu_chunk_populated():更新 populated bitmap / 统计

  • memset():把刚分出来的per CPU 副本清零