AI智能摘要
围绕 Linux 在 ARM64、SMP 场景下的并发挑战,本章聚焦内核 per‑CPU 变量机制及其底层内存布局。详细介绍了per-CPU的初始化流程,并以一个简单的案例来计算percpu memory的布局。
此摘要由AI分析文章内容生成,仅供参考。

本章源码遵循如下的约定:
1.芯片架构:ARM64
2.内核版本:linux-6.1.138
3.CONFIG_SMP=y

前言

随着对称多处理器(SMP)架构的普及,系统获得了前所未有的并行计算能力,但同时也面临着严峻的数据同步挑战。在单 CPU 时代,并发问题主要来源于进程调度带来的执行流交错;而在 SMP 系统中,程序真正地在多个处理器上同时执行,对共享数据的竞争变得无处不在。更复杂的是,为了缓解内存访问延迟,每个 CPU 都配备了私有缓存(如 L1 cache)以及共享的末级缓存(如 L2 cache),当某个 CPU 修改一个全局变量时,缓存一致性协议必须通知其他 CPU 使其对应的缓存行失效,这一过程会引入可观的开销,甚至成为性能瓶颈。

为了从根本上避免多 CPU 对同一数据的竞争,Linux 内核引入了 per‑CPU 变量机制。该机制为每个处理器在内存中保留一份独立的变量副本,每个 CPU 仅操作属于自己的那份副本,从而彻底消除了缓存同步和锁竞争的开销。当然,per‑CPU 变量并非万能,它只适用于那些在逻辑上天然是 CPU 私有的数据——在整个生命周期中,每个 CPU 的副本均由该 CPU 独占使用

除了当前处理器之外,没有其他处理器可以接触到这个per-CPU变量副本,因此不存在并发访问问题,所以当前处理器可以在不用锁的情况下访问per-CPU变量。但是内核抢占会影响到per-CPU变量,因此在操作per-CPU变量时都会禁止内核抢占

per-cpu变量,可以在编译时声明,也可以在系统运行时动态生成。

核心的数据结构

struct pcpu_alloc_info

struct pcpu_group_info {
	int			nr_units;	/* aligned # of units */
	unsigned long		base_offset;	/* base address offset */
	unsigned int		*cpu_map;	/* unit->cpu map, empty
						 * entries contain NR_CPUS */
};

struct pcpu_alloc_info {
	size_t			static_size;
	size_t			reserved_size;
	size_t			dyn_size;
	size_t			unit_size;
	size_t			atom_size;
	size_t			alloc_size;
	size_t			__ai_size;	/* internal, don't use */
	int			nr_groups;	/* 0 if grouping unnecessary */
	struct pcpu_group_info	groups[];
};

该结构体描述整个 per-CPU 内存区域的整体布局和分配参数,是内核在初始化 per-CPU 内存时使用的信息载体。

字段名

类型

含义

static_size

size_t

静态 per-CPU 变量所占用的总大小。这些变量在编译时定义(如 DEFINE_PER_CPU),在系统启动时即分配。

reserved_size

size_t

保留区域大小,通常用于模块加载等动态注册的 per-CPU 变量。

dyn_size

size_t

动态分配区域大小,用于运行时通过 alloc_percpu 等接口动态分配的 per-CPU 内存。

unit_size

size_t

每个 CPU 单元的大小。实际上每个 CPU 所拥有的内存块大小,可能大于 static_size+reserved_size+dyn_size,因为需要对齐和填充。

atom_size

size_t

原子分配粒度,通常为缓存行大小或最小分配单元。分配器保证分配的地址按此对齐。

alloc_size

size_t

实际从系统申请的总内存大小(所有 CPU 单元总和),通常等于 nr_groups 中各组的单元数乘以 unit_size 并加上对齐开销。

__ai_size

size_t

内部使用字段,存放 struct pcpu_alloc_info 结构体自身的总大小(包括变长数组 groups[] 占用的空间),供分配器复制信息时使用,不应对其直接操作

nr_groups

int

组的数量。如果为 0,表示所有 CPU 属于同一个组(即非 NUMA 或不需要分组的情况)。

groups[]

变长数组

柔性数组成员,包含 nr_groupsstruct pcpu_group_info 结构体,每个描述一个组的信息。

struct pcpu_chunk

struct pcpu_chunk {
#ifdef CONFIG_PERCPU_STATS
	int			nr_alloc;	/* # of allocations */
	size_t			max_alloc_size; /* largest allocation size */
#endif

	struct list_head	list;		/* linked to pcpu_slot lists */
	int			free_bytes;	/* free bytes in the chunk */
	struct pcpu_block_md	chunk_md;
	void			*base_addr;	/* base address of this chunk */

	unsigned long		*alloc_map;	/* allocation map */
	unsigned long		*bound_map;	/* boundary map */
	struct pcpu_block_md	*md_blocks;	/* metadata blocks */

	void			*data;		/* chunk data */
	bool			immutable;	/* no [de]population allowed */
	bool			isolated;	/* isolated from active chunk
						   slots */
	int			start_offset;	/* the overlap with the previous
						   region to have a page aligned
						   base_addr */
	int			end_offset;	/* additional area required to
						   have the region end page
						   aligned */
#ifdef CONFIG_MEMCG_KMEM
	struct obj_cgroup	**obj_cgroups;	/* vector of object cgroups */
#endif

	int			nr_pages;	/* # of pages served by this chunk */
	int			nr_populated;	/* # of populated pages */
	int                     nr_empty_pop_pages; /* # of empty populated pages */
	unsigned long		populated[];	/* populated bitmap */
};

struct pcpu_chunk 是 Linux 内核 per-CPU 内存分配器的核心数据结构,代表一个连续的内存块(chunk),用于管理分配给各 CPU 的内存单元。每个 chunk 内部划分为多个固定大小的单元(unit),每个单元对应一个 CPU 的私有副本。chunk 通过位图管理空闲空间、分配状态以及内存页的填充状态。下面按字段顺序逐一解析。

字段名

类型

描述

nr_alloc

int

(仅当 CONFIG_PERCPU_STATS=y 时存在)当前 chunk 中发生的分配次数统计。

max_alloc_size

size_t

(仅当 CONFIG_PERCPU_STATS=y 时存在)本 chunk 中曾分配过的最大内存块大小。

list

struct list_head

用于将 chunk 链接到全局 pcpu_slot 链表中,按空闲字节数分档。

free_bytes

int

当前 chunk 中剩余的总空闲字节数(跨所有 CPU 单元)。

chunk_md

struct pcpu_block_md

整个 chunk 的元数据块,记录 first_free、contig_hint 等信息,辅助快速定位空闲空间。

base_addr

void *

chunk 的起始虚拟地址,各 CPU 单元在此基础上加上偏移量获得私有副本。

alloc_map

unsigned long *

分配位图,每个比特位表示一个最小分配单元是否被占用。

bound_map

unsigned long *

边界位图,标记分配块的边界,用于合并相邻空闲块。

md_blocks

struct pcpu_block_md *

指向元数据块数组,每个元素对应一个区块的元数据,用于加速大块空闲区域的扫描。

data

void *

通用指针,可存储与 chunk 关联的私有数据(如静态 chunk 的 group 信息)。

immutable

bool

若为真,禁止对该 chunk 进行页面填充或回收(通常用于静态初始化的 chunk)。

isolated

bool

若为真,表示该 chunk 已从活跃的 pcpu_slot 链表中移除,避免被分配器选中。

start_offset

int

chunk 起始地址相对于页对齐起点的偏移量,用于计算页索引和填充范围。

end_offset

int

chunk 末尾到页对齐终点所需的填充字节数,确保 chunk 覆盖区域为整数页。

obj_cgroups

struct obj_cgroup **

(仅当 CONFIG_MEMCG_KMEM=y 时存在)指向 obj_cgroup 指针数组,每个分配单元对应一个 cgroup,用于内存统计与控制。

nr_pages

int

chunk 所覆盖的总页数。

nr_populated

int

已实际填充(映射物理页)的页数量。

nr_empty_pop_pages

int

已填充但未分配任何内存块的页数量,可用于后续分配或回收。

populated[]

unsigned long[]

柔性数组,位图,每个比特位对应一页(索引 0 ~ nr_pages-1),标记该页是否已被填充。

struct pcpu_chunk 的设计体现了 per-CPU 内存分配器的几个关键思想:

  • 分块管理:将大块连续虚拟地址划分为多个 chunk,每个 chunk 独立管理。

  • 位图辅助:通过 alloc_mapbound_map 实现细粒度的空闲空间跟踪,populated 位图管理物理页映射。

  • 分层元数据chunk_mdmd_blocks 构成两级元数据,加速大块空闲区域的查找。

  • 对齐与填充start_offsetend_offset 确保 chunk 边界与页对齐,便于页表操作。

  • 扩展性:通过条件编译支持统计和内存 cgroup,同时提供 data 指针和状态标志应对特殊场景。

struct pcpu_block_md

struct pcpu_block_md {
	int			scan_hint;	/* scan hint for block */
	int			scan_hint_start; /* block relative starting
						    position of the scan hint */
	int                     contig_hint;    /* contig hint for block */
	int                     contig_hint_start; /* block relative starting
						      position of the contig hint */
	int                     left_free;      /* size of free space along
						   the left side of the block */
	int                     right_free;     /* size of free space along
						   the right side of the block */
	int                     first_free;     /* block position of first free */
	int			nr_bits;	/* total bits responsible for */
};

字段名

类型

描述

scan_hint

int

扫描提示,表示从 scan_hint_start 位置开始的连续空闲长度(以位为单位)。用于加速分配时的扫描。

scan_hint_start

int

扫描提示的起始位置(块内相对位置,即位图中的索引),与 scan_hint 共同指示一个可能适合分配的空闲区域。

contig_hint

int

整个块中最大的连续空闲长度(以位为单位),用于快速判断是否能满足较大分配请求。

contig_hint_start

int

最大连续空闲区域的起始位置(块内相对位置)。

left_free

int

块左侧(起始边界)连续空闲的位数。如果块起始就是空闲,该值表示从开头开始的连续空闲长度。

right_free

int

块右侧(结束边界)连续空闲的位数。如果块末尾是空闲,该值表示从末尾往前(或末尾连续)的空闲长度。

first_free

int

块中第一个空闲位的位置(块内相对位置)。若没有空闲位,可能设置为特殊值(如块长度)。

nr_bits

int

该元数据块负责管理的总位数(即位图长度),即该块包含的分配单元个数。

  • 该结构体是 per-CPU 内存分配器(percpu allocator)中“块元数据”的核心,用于描述一个固定大小的位图块(block)的空闲情况。

  • 所有位置(start 字段)均以位为单位,相对于该块起始的偏移。

  • 这些元数据帮助分配器在扫描位图时快速跳过已用区域,定位到合适的空闲空间,提高分配效率。

per-CPU内存分布

cpu unit memory

这张图是 cpu unit 的内存分配情况,其中:

  • static 的原始大小是 __per_cpu_end - __per_cpu_start;

  • reserved 固定大小是由宏 PERCPU_MODULE_RESERVE 指定,系统默认为 8K;

  • dynamic 原始大小是用宏 PERCPU_DYNAMIC_RESERVE 指定,系统默认为 28K;

  • static + reserved + dynamic 总大小经过 向上页对齐 之后就是 cpu unit 的大小;

  • 从上面三个区域原始尺寸可知,cpu unit 经过页对齐后是否有多出来内存,主要看 static 原始大小是否页对齐,如果 static 不是页对齐,那么系统会多申请一些内存, 多出来的这部分就是图中两个 黄色区域 其中大部分会归到 dynamic 区域,因此 dynamic 区域尺寸会比原始尺寸大;

  • static region 实际大小可能会偏大,因为原始尺寸需要按照 PCPU_MIN_ALLOC_SIZE (4字节) 对齐,这就是图中第一个黄色部分;

  • reserved region 起始位置是从 static 按 PCPU_MIN_ALLOC_SIZE 对齐后的位置开始计算,区域大小固定;

  • dynamic region 会在原始尺寸的基础上加上另一个 黄色区域

备注:本节摘自 Linux内存管理(十六):percpu 分配器——基本原理 第3节

per-CPU的初始化

在系统启动的早期,start_kernel 会调用 setup_per_cpu_areas() 来完成 per-cpu 内存的初始化。这一步为所有 per-cpu 变量(包括未来模块要用的)奠定了物理内存基础。

asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
    //...
	setup_nr_cpu_ids();
	setup_per_cpu_areas();
    //...
}

setup_per_cpu_areas() 之前会调用 setup_nr_cpu_ids() 函数,配置全局变量 nr_cpu_ids ,用以记录系统支持的 CPU core 数量,需要结合 boot_cpu_init() 进行确认。

对于 cpu 有 8 个核,则该 nr_cpu_ids 为 8。

void __init setup_nr_cpu_ids(void)
{
	set_nr_cpu_ids(find_last_bit(cpumask_bits(cpu_possible_mask), NR_CPUS) + 1);
}

setup_per_cpu_areas

通过setup_per_cpu_areas函数来实现为系统中的每个CPU产生一份变量的副本,而且会对per-CPU的动态分配机制进行初始化。

void __init setup_per_cpu_areas(void)
{
	unsigned long delta;
	unsigned int cpu;
	int rc;

	/*
	 * Always reserve area for module percpu variables.  That's
	 * what the legacy allocator did.
	 */
	rc = pcpu_embed_first_chunk(PERCPU_MODULE_RESERVE, PERCPU_DYNAMIC_RESERVE,
				    PAGE_SIZE, NULL, NULL);
	if (rc < 0)
		panic("Failed to initialize percpu areas.");

	delta = (unsigned long)pcpu_base_addr - (unsigned long)__per_cpu_start;
	for_each_possible_cpu(cpu)
		__per_cpu_offset[cpu] = delta + pcpu_unit_offsets[cpu];
}

它主要完成以下工作:

  1. 调用 pcpu_embed_first_chunk 分配物理内存
    pcpu_embed_first_chunk(PERCPU_MODULE_RESERVE, PERCPU_DYNAMIC_RESERVE, PAGE_SIZE, NULL, NULL);

    • 该函数会在内存中分配一块连续的物理地址空间,用于存放所有 CPU 的 per-cpu 数据副本。

    • 参数 PERCPU_MODULE_RESERVE 表示需要为内核模块中定义的静态 per-cpu 变量预留一段空间。这部分预留区域位于内核静态 per-cpu 区域之后,确保模块的 per-cpu 变量能够被放在一个固定的、可寻址的范围内。

    • PERCPU_DYNAMIC_RESERVE 则为动态分配的 per-cpu 变量预留空间。

    • 分配成功后,全局指针 pcpu_base_addr 指向整个 per-cpu 映射区域的起始地址,pcpu_unit_offsets[] 数组记录了每个 CPU 的数据块相对于该起始地址的偏移量。

  2. 计算运行时 CPU 偏移量

    delta = (unsigned long)pcpu_base_addr - (unsigned long)__per_cpu_start;
    for_each_possible_cpu(cpu)
        __per_cpu_offset[cpu] = delta + pcpu_unit_offsets[cpu];
    • __per_cpu_start 是链接脚本中定义的符号,表示内核静态 per-cpu 数据段(.data..percpu)的起始虚拟地址。

    • delta 表示实际分配的 per-cpu 区域起始地址与链接地址之间的差值。由于内核在链接时并不知道 per-cpu 区域最终会被映射到何处,因此需要这个偏移量来修正符号地址。

    • 对于每个可能的 CPU,其 per-cpu 数据块的起始地址相对于 pcpu_base_addr 的偏移由 pcpu_unit_offsets[cpu] 给出。将该偏移加上 delta 就得到了每个 CPU 的数据块相对于内核静态 per-cpu 链接地址的偏移,这个值最终被存入 __per_cpu_offset[cpu]

    • 运行时,访问 per-cpu 变量时,CPU 通过 __my_cpu_offset() 获取当前 CPU 对应的偏移值(通常保存在专用寄存器中),再与变量的链接地址相加,即可得到属于该 CPU 的变量副本的真实地址。

比如作者手上的这台设备的 pcpu_base_addr__per_cpu_start

2026/03/halo_zgiojtq.png

上用静态变量举例,来说明 pcpu_unit_offsets[]和__per_cpu_offset[] 两个数组。

在物理内存中存在两块内存: kernel image 和 percpu;

对于静态变量,存在于kernel image .data..percpu 段,在内存初始化初期会对这部分内存映射,当 CPU 访问某个 percpu 静态变量的时候,读取的地址归根结底是这 kernel image 中的 .data..percpu 段的某个变量的物理地址,如图中箭头 1 所示。该section 的起始物理地址为 __per_cpu_start

但实际上变量的实际内容在 percpu memory 段,这就是percpu 机制,当某 CPU(例如 cpu2) 想访问percpu 变量,实际需要到该 CPU (例如 cpu2) 所在的 percpu unit 中获取,如图中箭头 2 所示。

percpu memory 的起始地址为 pcpu_base_addr,为了方便管理系统创建了一个数组,即 pcpu_unit_offsets 数组,数组中存放的是每个 CPU 的 percpu unit 的起始地址相对于 pcpu_base_addr 的偏移量。

pcpu_base_addr - __per_cpu_start = delta ,这就是 .data.percpu 段的percpu 变量与实际 cpu unit 内存差值,如图中箭头 3 所示。

pcpu_embed_first_chunk

pcpu_embed_first_chunk 是 Linux 内核中用于初始化 per-cpu 数据区域的底层函数之一,它采用“嵌入”(embed)方式分配第一块 per-cpu 内存。该函数被 setup_per_cpu_areas 调用,为所有 CPU 分配连续的物理内存,并将内核静态定义的 per-cpu 变量数据复制到每个 CPU 的副本中,同时为模块预留空间和动态分配预留空间

int __init pcpu_embed_first_chunk(size_t reserved_size, size_t dyn_size,
				  size_t atom_size,
				  pcpu_fc_cpu_distance_fn_t cpu_distance_fn,
				  pcpu_fc_cpu_to_node_fn_t cpu_to_nd_fn)
{
	void *base = (void *)ULONG_MAX;
	void **areas = NULL;
	struct pcpu_alloc_info *ai;
	size_t size_sum, areas_size;
	unsigned long max_distance;
	int group, i, highest_group, rc = 0;

	ai = pcpu_build_alloc_info(reserved_size, dyn_size, atom_size,
				   cpu_distance_fn);
	if (IS_ERR(ai))
		return PTR_ERR(ai);

	size_sum = ai->static_size + ai->reserved_size + ai->dyn_size;
	areas_size = PFN_ALIGN(ai->nr_groups * sizeof(void *));

	areas = memblock_alloc(areas_size, SMP_CACHE_BYTES);
	if (!areas) {
		rc = -ENOMEM;
		goto out_free;
	}

	/* allocate, copy and determine base address & max_distance */
	highest_group = 0;
	for (group = 0; group < ai->nr_groups; group++) {
		struct pcpu_group_info *gi = &ai->groups[group];
		unsigned int cpu = NR_CPUS;
		void *ptr;

		for (i = 0; i < gi->nr_units && cpu == NR_CPUS; i++)
			cpu = gi->cpu_map[i];
		BUG_ON(cpu == NR_CPUS);

		/* allocate space for the whole group */
		ptr = pcpu_fc_alloc(cpu, gi->nr_units * ai->unit_size, atom_size, cpu_to_nd_fn);
		if (!ptr) {
			rc = -ENOMEM;
			goto out_free_areas;
		}
		/* kmemleak tracks the percpu allocations separately */
		kmemleak_ignore_phys(__pa(ptr));
		areas[group] = ptr;

		base = min(ptr, base);
		if (ptr > areas[highest_group])
			highest_group = group;
	}
	max_distance = areas[highest_group] - base;
	max_distance += ai->unit_size * ai->groups[highest_group].nr_units;

	/* warn if maximum distance is further than 75% of vmalloc space */
	if (max_distance > VMALLOC_TOTAL * 3 / 4) {
		pr_warn("max_distance=0x%lx too large for vmalloc space 0x%lx\n",
				max_distance, VMALLOC_TOTAL);
#ifdef CONFIG_NEED_PER_CPU_PAGE_FIRST_CHUNK
		/* and fail if we have fallback */
		rc = -EINVAL;
		goto out_free_areas;
#endif
	}

	/*
	 * Copy data and free unused parts.  This should happen after all
	 * allocations are complete; otherwise, we may end up with
	 * overlapping groups.
	 */
	for (group = 0; group < ai->nr_groups; group++) {
		struct pcpu_group_info *gi = &ai->groups[group];
		void *ptr = areas[group];

		for (i = 0; i < gi->nr_units; i++, ptr += ai->unit_size) {
			if (gi->cpu_map[i] == NR_CPUS) {
				/* unused unit, free whole */
				pcpu_fc_free(ptr, ai->unit_size);
				continue;
			}
			/* copy and return the unused part */
			memcpy(ptr, __per_cpu_load, ai->static_size);
			pcpu_fc_free(ptr + size_sum, ai->unit_size - size_sum);
		}
	}

	/* base address is now known, determine group base offsets */
	for (group = 0; group < ai->nr_groups; group++) {
		ai->groups[group].base_offset = areas[group] - base;
	}

	pr_info("Embedded %zu pages/cpu s%zu r%zu d%zu u%zu\n",
		PFN_DOWN(size_sum), ai->static_size, ai->reserved_size,
		ai->dyn_size, ai->unit_size);

	pcpu_setup_first_chunk(ai, base);
	goto out_free;

out_free_areas:
	for (group = 0; group < ai->nr_groups; group++)
		if (areas[group])
			pcpu_fc_free(areas[group],
				ai->groups[group].nr_units * ai->unit_size);
out_free:
	pcpu_free_alloc_info(ai);
	if (areas)
		memblock_free(areas, areas_size);
	return rc;
}

函数参数说明

  • reserved_size:为内核模块的静态 per-cpu 变量预留的空间大小(对应 PERCPU_MODULE_RESERVE)。

  • dyn_size:为动态分配的 per-cpu 变量预留的空间大小(对应 PERCPU_DYNAMIC_RESERVE)。

  • atom_size:分配时的原子粒度,通常为 PAGE_SIZE 或缓存行大小,用于对齐。

  • cpu_distance_fn:可选的回调函数,用于评估 CPU 之间的“距离”,以决定如何分组(NUMA 感知)。

  • cpu_to_nd_fn:可选的回调函数,将 CPU 编号转换为 NUMA 节点 ID。

主要流程分解

构建分配信息(pcpu_build_alloc_info
ai = pcpu_build_alloc_info(reserved_size, dyn_size, atom_size, cpu_distance_fn);

static struct pcpu_alloc_info * __init __flatten pcpu_build_alloc_info(
				size_t reserved_size, size_t dyn_size,
				size_t atom_size,
				pcpu_fc_cpu_distance_fn_t cpu_distance_fn)
{
	static int group_map[NR_CPUS] __initdata;
	static int group_cnt[NR_CPUS] __initdata;
	static struct cpumask mask __initdata;
    // 静态变量总大小,从 __per_cpu_start ~ __per_cpu_end
	const size_t static_size = __per_cpu_end - __per_cpu_start;
	int nr_groups = 1, nr_units = 0;
	size_t size_sum, min_unit_size, alloc_size;
	int upa, max_upa, best_upa;	/* units_per_alloc */
	int last_allocs, group, unit;
	unsigned int cpu, tcpu;
	struct pcpu_alloc_info *ai;
	unsigned int *cpu_map;

	/* this function may be called multiple times */
	memset(group_map, 0, sizeof(group_map));
	memset(group_cnt, 0, sizeof(group_cnt));
	cpumask_clear(&mask);
    // //计算每个cpu所占有的percpu内存空间,
    //  包括: 静态空间 + 预留空间 + 动态空间
	/* calculate size_sum and ensure dyn_size is enough for early alloc */
	size_sum = PFN_ALIGN(static_size + reserved_size +
			    max_t(size_t, dyn_size, PERCPU_DYNAMIC_EARLY_SIZE));
    // 实际的动态空间,相当于上面size_sum是按照页对齐的,三者和多出来的部分最终都算到了dyn_size中
	dyn_size = size_sum - static_size - reserved_size;

	/*
	 * Determine min_unit_size, alloc_size and max_upa such that
	 * alloc_size is multiple of atom_size and is the smallest
	 * which can accommodate 4k aligned segments which are equal to
	 * or larger than min_unit_size.
	 */
    //理论上每个cpu unit的大小是按照size_sum,
    //但系统设计要求每个cpu unit的大小,不能小于PCPU_MIN_UNIT_SIZE (32KB)
	min_unit_size = max_t(size_t, size_sum, PCPU_MIN_UNIT_SIZE);

	/* determine the maximum # of units that can fit in an allocation */
    // 每次实际从内存分配器获取的连续内存块大小,按 atom_size 向上取整
	alloc_size = roundup(min_unit_size, atom_size);
	upa = alloc_size / min_unit_size;
	while (alloc_size % upa || (offset_in_page(alloc_size / upa)))
		upa--;
	max_upa = upa;

	cpumask_copy(&mask, cpu_possible_mask);

	/* group cpus according to their proximity */
    // 该循环将所有可能的 CPU 按距离函数分组:同组的 CPU 之间距离为 LOCAL_DISTANCE
	for (group = 0; !cpumask_empty(&mask); group++) {
		/* pop the group's first cpu */
		cpu = cpumask_first(&mask);
		group_map[cpu] = group;
		group_cnt[group]++;
		cpumask_clear_cpu(cpu, &mask);

		for_each_cpu(tcpu, &mask) {
			if (!cpu_distance_fn ||
			    (cpu_distance_fn(cpu, tcpu) == LOCAL_DISTANCE &&
			     cpu_distance_fn(tcpu, cpu) == LOCAL_DISTANCE)) {
				group_map[tcpu] = group;
				group_cnt[group]++;
				cpumask_clear_cpu(tcpu, &mask);
			}
		}
	}
	nr_groups = group;

	/*
	 * Wasted space is caused by a ratio imbalance of upa to group_cnt.
	 * Expand the unit_size until we use >= 75% of the units allocated.
	 * Related to atom_size, which could be much larger than the unit_size.
	 */
	last_allocs = INT_MAX;
	best_upa = 0;
	for (upa = max_upa; upa; upa--) {
		int allocs = 0, wasted = 0;

		if (alloc_size % upa || (offset_in_page(alloc_size / upa)))
			continue;

		for (group = 0; group < nr_groups; group++) {
			int this_allocs = DIV_ROUND_UP(group_cnt[group], upa);
			allocs += this_allocs;
			wasted += this_allocs * upa - group_cnt[group];
		}

		/*
		 * Don't accept if wastage is over 1/3.  The
		 * greater-than comparison ensures upa==1 always
		 * passes the following check.
		 */
		if (wasted > num_possible_cpus() / 3)
			continue;

		/* and then don't consume more memory */
		if (allocs > last_allocs)
			break;
		last_allocs = allocs;
		best_upa = upa;
	}
	BUG_ON(!best_upa);
	upa = best_upa;

	/* allocate and fill alloc_info */
    // 计算总单元数并分配 alloc_info 结构
	for (group = 0; group < nr_groups; group++)
		nr_units += roundup(group_cnt[group], upa);

	ai = pcpu_alloc_alloc_info(nr_groups, nr_units);
	if (!ai)
		return ERR_PTR(-ENOMEM);
	cpu_map = ai->groups[0].cpu_map;

	for (group = 0; group < nr_groups; group++) {
		ai->groups[group].cpu_map = cpu_map;
		cpu_map += roundup(group_cnt[group], upa);
	}
    // 填充 alloc_info 的基本字段
	ai->static_size = static_size;
	ai->reserved_size = reserved_size;
	ai->dyn_size = dyn_size;
	ai->unit_size = alloc_size / upa;
	ai->atom_size = atom_size;
	ai->alloc_size = alloc_size;
    // 填充每个组的详细信息
	for (group = 0, unit = 0; group < nr_groups; group++) {
		struct pcpu_group_info *gi = &ai->groups[group];

		/*
		 * Initialize base_offset as if all groups are located
		 * back-to-back.  The caller should update this to
		 * reflect actual allocation.
		 */
		gi->base_offset = unit * ai->unit_size;

		for_each_possible_cpu(cpu)
			if (group_map[cpu] == group)
				gi->cpu_map[gi->nr_units++] = cpu;
		gi->nr_units = roundup(gi->nr_units, upa);
		unit += gi->nr_units;
	}
	BUG_ON(unit != nr_units);

	return ai;
}

pcpu_build_alloc_info 通过分析 CPU 拓扑、对齐要求和内存大小,设计出一套最优的 percpu 内存分配方案:

  • 将 CPU 按距离分组,使同组 CPU 共享一个内存块,减少跨节点访问。

  • 调整单元大小和分配块大小,平衡内存利用率和分配次数。

  • 输出一个详细的分配信息结构,指导后续的 percpu 内存区域建立。

计算总大小与分配 areas 数组
size_sum = ai->static_size + ai->reserved_size + ai->dyn_size;
areas_size = PFN_ALIGN(ai->nr_groups * sizeof(void *));
areas = memblock_alloc(areas_size, SMP_CACHE_BYTES);
  • size_sum 是每个 CPU 单元中实际有效数据的大小(不含填充对齐部分)。

  • areas 是一个指针数组,用于保存每个组分配到的内存基地址,数组长度按页对齐分配,使用 memblock_alloc(启动初期内存分配器)。

为每个组分配物理内存
for (group = 0; group < ai->nr_groups; group++) {
    ...
    ptr = pcpu_fc_alloc(cpu, gi->nr_units * ai->unit_size, atom_size, cpu_to_nd_fn);
    areas[group] = ptr;
    base = min(ptr, base);
    ...
}
  • 遍历每个组,调用 pcpu_fc_alloc 为整个组分配一段连续物理内存(大小为 组内单元数 * unit_size)。

  • 分配时可能考虑 NUMA 节点(通过 cpu_to_nd_fn 获取 CPU 所在节点)。

  • 记录所有分配地址中的最小地址 base(即所有组内存的起始基址),以及地址最高的组(用于计算最大距离)。

复制静态数据
for (group = 0; group < ai->nr_groups; group++) {
    for (i = 0; i < gi->nr_units; i++, ptr += ai->unit_size) {
        if (gi->cpu_map[i] == NR_CPUS) {
            pcpu_fc_free(ptr, ai->unit_size);  // 未使用的单元(空洞),整块释放
            continue;
        }
        memcpy(ptr, __per_cpu_load, ai->static_size);          // 复制内核静态数据
        pcpu_fc_free(ptr + size_sum, ai->unit_size - size_sum); // 释放单元尾部未使用的填充部分
    }
}

对于每个组内的每个单元(每个单元对应一个 CPU 或一个空洞):

  • 如果是空洞(cpu_map[i] == NR_CPUS),表示该单元没有对应的 CPU,因此整块释放。

  • 否则,将内核静态 per-cpu 数据(从符号 __per_cpu_load 开始,大小为 static_size)复制到该单元的开头。

  • 然后释放该单元中超出 size_sum 的尾部填充部分(因为 unit_size 可能大于 size_sum,多余部分用于对齐,现在可以归还)。

调用 pcpu_setup_first_chunk 完成最终设置

这是核心设置函数,将其作为单独一节剖析说明

pcpu_setup_first_chunk

pcpu_setup_first_chunk 函数是 Linux 内核 percpu 内存初始化流程中的第二步,它在 pcpu_build_alloc_info 生成分配信息后调用,根据该信息和分配的物理/虚拟地址,建立实际的 percpu 第一块内存区域(first chunk)。该函数负责:

  • 校验输入参数的有效性;

  • 分配并填充内部映射表(如 unit_mapunit_off);

  • 根据 ai 中的分组信息计算每个 CPU 对应的单元偏移;

  • 初始化 percpu 块(chunk)管理结构;

  • 设置全局 percpu 状态变量(如 pcpu_base_addrpcpu_first_chunk 等)。

函数原型与输入

void __init pcpu_setup_first_chunk(const struct pcpu_alloc_info *ai,
                                   void *base_addr)
  • ai:由 pcpu_build_alloc_info 构建的分配信息结构,包含静态大小、保留大小、动态大小、单元大小、分组信息等。

  • base_addr:percpu 第一块区域的起始虚拟地址(线性映射地址),由调用者提供(例如 __per_cpu_load 或从 memblock 分配的物理内存的虚拟地址)。

  • 返回值:无,但会设置一系列全局 percpu 变量,并创建第一个 percpu 块。

局部变量定义与辅助宏

size_t size_sum = ai->static_size + ai->reserved_size + ai->dyn_size;
// ... 其他变量
#define PCPU_SETUP_BUG_ON(cond) ...
  • size_sum 是一个 CPU 单元的总需求大小(静态+保留+动态)。

  • PCPU_SETUP_BUG_ON 是一个条件检查宏,如果条件成立,则打印错误信息、dump ai 内容并触发 BUG,用于在初始化阶段捕获致命错误。

输入校验(sanity checks)

一系列 PCPU_SETUP_BUG_ON 检查,确保 aibase_addr 满足基本要求:

  • nr_groups > 0:至少有一个组。

  • static_size > 0(SMP 下):必须有静态 percpu 变量。

  • __per_cpu_start 页对齐。

  • base_addr 非空且页对齐。

  • unit_size >= size_sum 且页对齐,且不小于 PCPU_MIN_UNIT_SIZE

  • unit_sizePCPU_BITMAP_BLOCK_SIZE 的倍数(用于位图管理)。

  • dyn_size >= PERCPU_DYNAMIC_EARLY_SIZE 且非零。

  • reserved_size 按最小分配粒度对齐。

  • 位图块大小与页大小的对齐关系合理(两者之一必须是另一个的倍数)。

  • 调用 pcpu_verify_alloc_info(ai) 验证分组信息的一致性(如 CPU 映射不重复、不越界等)。

这些检查确保后续操作的安全性。

分配内部映射表

alloc_size = ai->nr_groups * sizeof(group_offsets[0]);
group_offsets = memblock_alloc(alloc_size, SMP_CACHE_BYTES);
...
group_sizes = memblock_alloc(...);
unit_map = memblock_alloc(nr_cpu_ids * sizeof(unit_map[0]), ...);
unit_off = memblock_alloc(nr_cpu_ids * sizeof(unit_off[0]), ...);
  • 使用 memblock_alloc 在早期内存分配器(boot阶段)中分配四个数组:

    • group_offsets:每个组在虚拟地址空间中的基偏移(相对于 base_addr)。

    • group_sizes:每个组占用的总大小(nr_units * unit_size)。

    • unit_map:将 CPU 编号映射到全局单元编号(unit index)。

    • unit_off:每个 CPU 对应的单元在虚拟地址空间中的偏移(相对于 base_addr)。

  • 这些数组是全局 percpu 系统的核心数据结构,后续用于快速定位每个 CPU 的 percpu 数据区。

初始化 unit_map 并遍历分组填充映射

for (cpu = 0; cpu < nr_cpu_ids; cpu++)
    unit_map[cpu] = UINT_MAX;
pcpu_low_unit_cpu = pcpu_high_unit_cpu = NR_CPUS;
  • unit_map 初始化为无效值 UINT_MAX

  • pcpu_low_unit_cpupcpu_high_unit_cpu 分别记录偏移最小和最大的 CPU,用于后续可能的范围优化。

接下来遍历所有组:

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;   // 空洞单元(对齐预留),不映射到真实 CPU

        // 检查 CPU 合法性
        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; // 单元偏移

        // 记录最低/最高偏移的 CPU
        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;
    }
}
  • 这里 unit 是全局单元计数器,i 是该组内的单元索引。每个组中可能有空洞(cpu_map[i] = NR_CPUS),这些单元不分配给任何 CPU,但占据空间。

  • 对于每个有效的 CPU,记录其全局单元号和虚拟偏移。

  • 最终 pcpu_nr_units = unit 记录总单元数。

检查所有可能的 CPU 都有映射

for_each_possible_cpu(cpu)
    PCPU_SETUP_BUG_ON(unit_map[cpu] == UINT_MAX);

确保每个可能的 CPU 都被分配了一个有效的单元。

保存全局配置变量

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(chunk, populated,
                                     BITS_TO_LONGS(pcpu_unit_pages));
  • 将局部数组指针赋给全局变量,供后续使用。

  • 计算并保存单元大小(页数、字节数)、原子分配粒度、chunk 结构大小(包含位图数组)。

设置 chunk 管理槽位

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(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]);

percpu 分配器使用多个 slot 来管理不同状态的 chunk(例如空闲、待清理等)。这里计算出特殊 slot 的索引并分配链表数组。


对齐静态区域并调整动态大小

static_size = ALIGN(ai->static_size, PCPU_MIN_ALLOC_SIZE);
dyn_size = ai->dyn_size - (static_size - ai->static_size);

静态区域需要按最小分配粒度对齐,以便后续预留区域对齐。对齐后可能会增加一些大小,这些增加的部分从动态区域中扣除,保持总大小不变。

创建第一个 chunk

tmp_addr = (unsigned long)base_addr + static_size;
map_size = ai->reserved_size ?: dyn_size;
chunk = pcpu_alloc_first_chunk(tmp_addr, map_size);
  • pcpu_alloc_first_chunk 是一个辅助函数,用于创建一个 chunk,起始地址为 tmp_addr,大小为 map_size,并初始化其内部位图等。

  • 如果 reserved_size 非零,则第一个 chunk 用于保留区域;否则第一个 chunk 直接用于动态区域。

if (ai->reserved_size) {
    pcpu_reserved_chunk = chunk;   // 将第一个 chunk 设为保留 chunk
    tmp_addr = (unsigned long)base_addr + static_size + ai->reserved_size;
    map_size = dyn_size;
    chunk = pcpu_alloc_first_chunk(tmp_addr, map_size); // 再创建一个动态 chunk
}

如果有保留区域,则需要两个 chunk:一个保留,一个动态。保留 chunk 保存后,再为动态区域创建一个新 chunk。

pcpu_first_chunk = chunk;   // 指向动态区域 chunk(如果没有保留区域,则指向第一个 chunk)
pcpu_nr_empty_pop_pages = pcpu_first_chunk->nr_empty_pop_pages;
pcpu_chunk_relocate(pcpu_first_chunk, -1);  // 将 chunk 放入合适的 slot

pcpu_nr_populated += PFN_DOWN(size_sum);   // 统计已 populate 的页数
pcpu_stats_chunk_alloc();
trace_percpu_create_chunk(base_addr);

pcpu_base_addr = base_addr;   // 保存基地址

设置全局变量 pcpu_base_addr,至此 chunk 配置完成。

分析到这里,其实我们可以看到,虽然pcpu_setup_first_chunk函数名是first chunk,但是实际上会创建两个chunk。

  1. 第一个 chunk(由 pcpu_alloc_first_chunk 创建):

    • 起始地址 = base_addr + static_size(静态区之后)。

    • 大小 = reserved_size(如果 reserved_size > 0)或 dyn_size(如果没有保留区)。

    • 如果 reserved_size > 0,这个 chunk 被赋值给 pcpu_reserved_chunk,作为保留 chunk。

  2. 第二个 chunk(仅在 reserved_size > 0 时创建):

    • 起始地址 = 保留区之后:base_addr + static_size + reserved_size

    • 大小 = dyn_size

    • 这个 chunk 被赋值给 pcpu_first_chunk,作为动态 chunk。

函数名中的“first chunk”指的是整个第一块 percpu 内存区域(即静态区 + 保留区 + 动态区),该区域可能被拆分成多个 chunk 来管理,但逻辑上仍属于初始化阶段的第一块内存。最终 pcpu_first_chunk 总是指向服务动态分配的那个 chunk,而 pcpu_reserved_chunk 指向保留 chunk。

字段

pcpu_first_chunk

pcpu_reserved_chunk

含义

base_addr

0xFFFFFF8179326000

0xFFFFFF8179324000

chunk 的起始虚拟地址,两者相差 0x2000 (8KB)

nr_pages

8

3

chunk 占用的物理页帧数(页大小通常 4KB)

nr_populated

8

3

实际已映射(populate)的页数,此处全部已映射

nr_empty_pop_pages

0

0

已映射且空闲的页数(无空闲页)

free_bytes

0

4624

chunk 内剩余可分配字节数

start_offset

3304

3304

chunk 起始地址到实际可用区域开头的偏移(对齐开销)

end_offset

0

792

chunk 末尾未使用的字节数(对齐填充)

immutable

true

true

不可变,表示这两个 chunk 属于第一块静态分配区域,不会被释放或合并

isolated

false

false

不在隔离列表中,正常参与分配


总结

setup_per_cpu_areas 可以把它理解成:把 percpu 虚拟空间搭起来 + 把静态模板复制到per CPU + 把动态分配器(chunk)初始化好

阶段 A:算大小与对齐(layout 参数)

它会得到并统一这些关键量(概念上):

  • static_size = __per_cpu_end - __per_cpu_start
    → 静态 per-cpu 变量模板区大小

  • reserved_size
    → early boot 预留给动态 percpu 分配的空间(避免早期碎片)

  • unit_size
    → 每个 CPU 的 unit 总大小(static + reserved + dynamic + 对齐/填充)

  • atom_size / alloc_align
    → 内部分配粒度与对齐约束(决定 bitmap 的粒度、block 的拆分方式)

这一步在代码里看到的很多“round_up/align/pow2”都属于这个阶段。


阶段 B:构造 pcpu_alloc_info(布局蓝图)

pcpu_alloc_info 解决两件事:

  1. CPU 怎么分组(group)(通常跟 NUMA/拓扑有关)

  2. 每个 CPU 的 unit 在 percpu 虚拟空间里放在哪(offset)

最终产出最关键的东西之一就是:

  • pcpu_unit_offsets[cpu]

它会保证:

  • 每个 CPU 的 unit 布局一致(同样的 static/reserved/dynamic 切分)

  • 访问公式成立:addr = pcpu_base_addr + pcpu_unit_offsets[cpu] + obj_offset


阶段 C:分配 percpu 虚拟空间并建立 first chunk

这一步会:

  • 得到 pcpu_base_addr(percpu 虚拟空间的 base)

  • 创建并初始化 pcpu_first_chunk

pcpu_first_chunk 是 percpu 动态分配器的第一个 arena:它管理一段“offset 空间”,这段 offset 同时对应所有 CPU unit 的动态区。


阶段 D:建立 reserved chunk(早期预留区)

然后会从 first chunk 里“切”出早期预留部分,建立:

  • pcpu_reserved_chunk

它的目标是:

  • early boot 的 percpu 动态分配尽量落在 reserved 区里

  • 避免把后续常规 dynamic 区弄碎(也避免早期阶段依赖复杂回收)


阶段 E:把静态模板复制到每个 CPU 的 static 区

也就是把:

  • [__per_cpu_start, __per_cpu_end) 的模板内容

复制到每个 CPU unit 对应位置:

  • unit_base(cpu) + static_offset

这样 DEFINE_PER_CPU() 的变量从此对所有 CPU 都“就位”。

阶段 F:让 arch 能“快速访问 percpu”(arm64 的 TPIDR_EL1)

最后会把每 CPU 的 percpu base(unit_base)用于 arch 侧设置:

  • arm64:把当前 CPU 的 unit_base 写到 TPIDR_EL1
    之后 this_cpu_ptr() 就变成:

  • mrs tpidr_el1 + add offset


我们要申明一点的是,chunk 管“offset 空间”,不是管某个 CPU

alloc_percpu() 时:

  • 分配器返回一个 obj_offset

  • 这个 offset 在 所有 CPU unit 上都有效

  • 所以每 CPU 都在 unit_base(cpu) + obj_offset 有一份对象

小实验

实验步骤:

  • 定义一个 percpu 变量

  • 给每个 CPU 写入不同的值

  • 打印每个 CPU 上这个变量的地址

  • 打印每个 CPU 上这个变量的值

这样可以直接观察:

  • 不同 CPU 的地址差异

  • unit offset 的规律

  • percpu 变量的布局

// 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);

        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);

在 Linux 内核中,每个 per-CPU 变量(如本案例中的 percpu_var )在特定 CPU 上的地址是通过 编译时基地址 加上 该 CPU 的 per-CPU 数据区偏移量 计算得到的

每个 CPU 的区域相对于 CPU0 区域的偏移量被记录在一个全局数组 __per_cpu_offset[] 中。 pcpu_unit_offsets 数组就是这个偏移量数组

&pcpu_unit_offsets[0]=0xffffff803fdf6100
	*(&pcpu_unit_offsets[0])=0
&pcpu_unit_offsets[1]=0xffffff803fdf6108
	*(&pcpu_unit_offsets[1])=0x15000
&pcpu_unit_offsets[2]=0xffffff803fdf6110
	*(&pcpu_unit_offsets[2])=0x2a000=0x15000*2
&pcpu_unit_offsets[3]=0xffffff803fdf6118
	*(&pcpu_unit_offsets[3])=0x3f000=0x15000*3

per_cpu(percpu_var, cpu) 展开后的地址计算逻辑为

addr = (unsigned long)(&percpu_var) + __per_cpu_offset[cpu];

其中 &percpu_var 就是 CPU0 上的地址(即编译时基地址,编译时已确定)。因此:

  • CPU1 percpu_var 地址 = 0xffffff803fd94c24 + 0x15000 = 0xffffff803fda9c24

  • CPU2 percpu_var 地址 = 0xffffff803fd94c24 + 0x2a000 = 0xffffff803fdbec24

  • CPU3 percpu_var 地址 = 0xffffff803fd94c24 + 0x3f000 = 0xffffff803fdd3c24

这个计算结果也和输出的日志保持一致!而且当我们定义了一个percpu_var变量后,直接读取可以看到这个变量在per-CPU中都被初始化为了0。

从上图的pcpu_base_addr = 0xffffff803fd8b000,又怎么理解呢?

percpu变量地址计算过程
[    0.878314] __per_cpu_start: ffffffc009af0000
[    0.878565] __per_cpu_end: ffffffc009afbb28
[    0.878771] pcpu_base_addr: ffffff803fd8b000
[    0.878993] percpu_test: printing percpu addresses and values
[    0.879350] percpu_var - CPU 0: addr=ffffff803fd94c24 value=0
[    0.879659] percpu_var2 - CPU 0: addr=ffffff803fd94c28 value=0

对于变量percpu_var:

变量对于per cpu基地址的偏移为:0xffffff803fd94c24 - 0xffffff803fd8b000(pcpu_base_addr) = 0x9c24

变量percpu_var在各cpu上的地址

计算公式:

addr = pcpu_base_addr + pcpu_unit_offsets[cpu] + 偏移(0x9c24

  • CPU0: pcpu_base_addr + 0 + 0x9c24 = 0xffffff803fd94c24

  • CPU1: pcpu_base_addr + 0x15000 + 0x9c24 = 0xffffff803fda9c24

  • CPU2: pcpu_base_addr + 0x2a000 + 0x9c24 = 0xffffff803fdbec24

  • CPU3: pcpu_base_addr + 0x3f000 + 0x9c24 = 0xffffff803fdd3c24

对于变量percpu_var2:

变量对于per cpu基地址的偏移为:0xffffff803fd94c28 - 0xffffff803fd8b000(pcpu_base_addr) = 0x9c28

变量percpu_var在各cpu上的地址

计算公式:

addr = pcpu_base_addr + pcpu_unit_offsets[cpu] + 偏移(0x9c28

  • CPU0: pcpu_base_addr + 0 + 0x9c28 = 0xffffff803fd94c28

  • CPU1: pcpu_base_addr + 0x15000 + 0x9c28 = 0xffffff803fda9c28

  • CPU2: pcpu_base_addr + 0x2a000 + 0x9c28 = 0xffffff803fdbec28

  • CPU3: pcpu_base_addr + 0x3f000 + 0x9c28 = 0xffffff803fdd3c28

percpu_var2 在 CPU0 上的地址为 ffffff803fd94c28,比 percpu_var 高 4 字节,符合 int 类型大小,说明两个变量在模块的 percpu 段内是连续存放的。

然后每个per-CPU percpu_var变量分别赋值的输出结果

[    0.844582] percpu_test: printing percpu addresses and values
[    0.845437] CPU 0: addr=ffffff803fd94c24 value=0
[    0.845693] CPU 1: addr=ffffff803fda9c24 value=0
[    0.845919] CPU 2: addr=ffffff803fdbec24 value=0
[    0.846138] CPU 3: addr=ffffff803fdd3c24 value=0
[    0.846458] percpu_test: printing percpu addresses and values
[    0.902621] CPU 0: addr=ffffff803fd94c24 value=0
[    0.919498] CPU 1: addr=ffffff803fda9c24 value=100
[    0.931415] CPU 2: addr=ffffff803fdbec24 value=200
[    0.940844] CPU 3: addr=ffffff803fdd3c24 value=300

在下一篇文章中,我们将聊聊我们提到的静态per-CPU,模块per-CPU以及动态per-CPU变量的概念!

敬请期待!

参考文档

https://justinwei.blog.csdn.net/article/details/134292241