菜单

kaoqi
kaoqi
发布于 2026-09-22 / 16 阅读
0
0

再论kernel的PER-CPU

之前的文章讨论过,DEFINE_PER_CPU这个宏展开实际上是下面这个东西:/include/linux/percpu-defs.h

#define __PCPU_ATTRS(sec)						                \
	__percpu __attribute__((section(PER_CPU_BASE_SECTION sec)))    \
	PER_CPU_ATTRIBUTES

#define __PCPU_DUMMY_ATTRS						                \
	__section(".discard") __attribute__((unused))

#define DEFINE_PER_CPU_SECTION(type, name, sec)			        \
	__PCPU_DUMMY_ATTRS char __pcpu_scope_##name;			        \
	extern __PCPU_DUMMY_ATTRS char __pcpu_unique_##name;	        \
	__PCPU_DUMMY_ATTRS char __pcpu_unique_##name;			        \
	extern __PCPU_ATTRS(sec) __typeof__(type) name;		        \
	__PCPU_ATTRS(sec) __weak __typeof__(type) name
#else

#define DEFINE_PER_CPU(type, name)					            \
	DEFINE_PER_CPU_SECTION(type, name, "")

为什么要重提呢,因为往往只有根据这个宏展开搞出来的。

具体解析一下这里的展开,PCPU ATTRS这个东东,它主要包含三个参数。其中第三个参数的定义就是空的:

// 来自/include/asm-generic/percpu.h
#ifndef PER_CPU_ATTRIBUTES
#define PER_CPU_ATTRIBUTES
#endif

第一个参数__percpu也是一个宏展开,也是空的:

// 来自/include/linux/compiler_types.h
#define __percpu

目前来看,只有第二个参数__attribute__((section(PER_CPU_BASE_SECTION sec)))是有具体含义的。

它的展开是:

#define PER_CPU_BASE_SECTION ".data..percpu"

所以,DEFINE_PER_CPU这个宏的展开,除了一开始的参数校验外,有效的变量定义其实就是这个添加到.data..percpu的段的动作。

相应的,在链接脚本中,也会针对这个特殊的变量定义做展开:

#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 = .;

__per_cpu_start是内存中描述cpu的开始部分,__per_cpu_end是cpu的结束部分。

继续的来看,在链接器中收集到的这些片段,最终会在setup_per_cpu_areas展开。

这个函数在/arch/arm64/mm/numa.c文件:

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,
				    pcpu_cpu_distance,
				    pcpu_fc_alloc, pcpu_fc_free);
	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];
}

这里的主要功能包括:

  • 分配并初始化Per-CPU内存块,为静态Per-CPU变量分配内存

  • 计算地址偏移量,也就是Per-CPU的长度

  • 填充__per_cpu_offset数组,存储了每个 CPU 的 Per-CPU 区域相对于模板起始地址的总偏移量

当内核代码访问 Per-CPU 变量时,底层宏(如 this_cpu_read)或汇编代码会利用这个偏移量来快速定位当前 CPU 的变量地址。

对kernel来说,它在一开始的时候不知道当前的CPU核数有多少,因此我们在kernel中看到的变量看起来都只定义了一次。正真的逐CPU的变量展开正是在setup_per_cpu_areas中进行的。

进一步来说,pcpu_embed_first_chunk的展开是:

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_alloc_fn_t alloc_fn,
				  pcpu_fc_free_fn_t free_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;
	}
	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);
		ptr = alloc_fn(cpu, gi->nr_units * ai->unit_size, atom_size);
		if (!ptr) {
			rc = -ENOMEM;
			goto out_free_areas;
		}
		kmemleak_free(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;
	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
		rc = -EINVAL;
		goto out_free_areas;
#endif
	}
	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) {
				free_fn(ptr, ai->unit_size);
				continue;
			}
			memcpy(ptr, __per_cpu_load, ai->static_size);
			free_fn(ptr + size_sum, ai->unit_size - size_sum);
		}
	}
	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])
			free_fn(areas[group],
				ai->groups[group].nr_units * ai->unit_size);
out_free:
	pcpu_free_alloc_info(ai);
	if (areas)
		memblock_free_early(__pa(areas), areas_size);
	return rc;
}

这里,各个输入参数的含义为:

  • reserved_size: 保留的 percpu 区域大小(字节)

  • dyn_size: 用于动态分配的最小空闲空间大小(字节)

  • atom_size: 分配原子大小(allocation atom size)

  • cpu_distance_fn: 用于确定 CPU 之间距离的回调函数,可选

  • alloc_fn: 用于分配 percpu 页面的函数

  • free_fn: 用于释放 percpu 页面的函数

这个函数的主要内容为:

  • pcpu_build_alloc_info函数,根据系统拓扑结构和配置参数,计算并构建pcpu_alloc_info‌结构体

  • size_sum和areas_size用以结合输入变量来计算需要的空间大小

  • memblock_alloc用以分配内存

  • group循环与NR_CPUS的循环用以处理所有的CPU

  • alloc_fn用于具体的内存分配器指针ptr,并使用kmemleak_free进行指针指示

这里,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[];
};

有关这个简易内存分配器的更多细节,还需要再来一篇文章讲。

一般,认为进行过PERCPU声明的变量,最终会被展开为:

CPU 0 的 per-CPU area:
┌──────────────────────────────────────────────┐
│ var_a │ var_b │ var_c │ ... │ (padding)      │
└──────────────────────────────────────────────┘

CPU 1 的 per-CPU area:
┌──────────────────────────────────────────────┐
│ var_a │ var_b │ var_c │ ... │ (padding)      │
└──────────────────────────────────────────────┘

CPU N 的 per-CPU area:
┌──────────────────────────────────────────────┐
│ var_a │ var_b │ var_c │ ... │ (padding)      │
└──────────────────────────────────────────────┘

对于PERCPU的变量来说,存在很多常见的场景:

  • 中断计数器,每个CPU独立统计自己的中断计数

  • 调度器,每个CPU维护自己的就绪队列

  • slab分配器,CPU有本地对象缓存

  • 网络协议栈,每个CPU独立的收发包队列

  • 时间戳,每个CPU的时钟源

上述的这些场景,都是常见的需要CPU单独变量的情况。

在kernel的各个任务中,尤其是rest init任务前,搭建各种基础环境的时候,经常会需要用上这种PERCPU的变量。


评论