之前的文章讨论过,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的变量。