Linux per-cpu_linux percpu


前言

`

一、per-cpu简介

SMP系统多个核心与内存交互的时候,因为L1 cache的存在,会出现一致性的问题。所以,最好的方式就是每个核自己维护一份变量。
在这里插入图片描述

在SMP的Linux系统上,为系统中的每个处理器都分配了per-CPU变量的一个副本。在多处理器系统中,当处理器操作属于它的per-CPU变量副本时,不需要考虑与其他处理器竞争的问题,同时该副本还可以充分利用处理器本地的硬件缓存以提高访问速度。per-CPU是基于空间换时间思想。

除了当前处理器之外,没有其他处理器可以接触到这个per-CPU变量副本,因此不存在并发访问问题,所以当前处理器可以在不用锁的情况下访问per-CPU变量。

但是内核抢占会影响到per-CPU变量,因此在操作per-CPU变量时都会禁止内核抢占。

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

二、静态per-CPU变量

2.1 普通静态per-CPU变量

使用宏DECLARE_PER_CPU(type, name)声明per-CPU变量,使用宏DEFINE_PER_CPU(type, name)定义per-CPU变量,如下所示:

// /linux-3.10.1/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(type, name)一步步展开:

// /linux-3.10.1/include/linux/percpu-defs.h

#define DEFINE\_PER\_CPU(type, name) \
 DEFINE\_PER\_CPU\_SECTION(type, name, "")

#define DEFINE\_PER\_CPU\_SECTION(type, name, sec) \
 \_\_PCPU\_ATTRS(sec) PER\_CPU\_DEF\_ATTRIBUTES \
 \_\_typeof\_\_(type) name

/\*
 \* Base implementations of per-CPU variable declarations and definitions, where
 \* the section in which the variable is to be placed is provided by the
 \* 'sec' argument. This may be used to affect the parameters governing the
 \* variable's storage.
 \*
 \* NOTE! The sections for the DECLARE and for the DEFINE must match, lest
 \* linkage errors occur due the compiler generating the wrong code to access
 \* that section.
 \*/
#define \_\_PCPU\_ATTRS(sec) \
 \_\_percpu \_\_attribute\_\_((section(PER\_CPU\_BASE\_SECTION sec))) \
 PER\_CPU\_ATTRIBUTES

上述每个 CPU 变量声明和定义的基本实现,其中放置变量的部分由“sec”参数提供。

// /linux-3.10.1/include/asm-generic/percpu.h

#ifdef CONFIG\_SMP
#define PER\_CPU\_BASE\_SECTION ".data..percpu"

最终展开就是:

 \_\_attribute\_\_((section(".data..percpu"))) \_\_typeof\_\_(type) name

静态的per-CPU变量存放在 “.data…percpu” section中。
注意这里只是将 per-CPU变量 放在".data…percpu" section中,每个处理器中还没有 per-CPU变量 副本。

2.2 静态per-CPU变量的使用

2.2.1 API

(1)

// /linux-3.10.1/include/linux/percpu.h

/\*
 \* Must be an lvalue. Since @var must be a simple identifier,
 \* we force a syntax error here if it isn't.
 \*/
#define get\_cpu\_var(var) (\*({ \
 preempt\_disable(); \
 &\_\_get\_cpu\_var(var); }))

/\*
 \* The weird & is necessary because sparse considers (void)(var) to be
 \* a direct dereference of percpu variable (var).
 \*/
#define put\_cpu\_var(var) do { \
 (void)&(var); \
 preempt\_enable(); \
} while (0)

get_cpu_var会禁止内核抢占,put_cpu_var会使能内核抢占。

(2)

#define per\_cpu(var, cpu) (\*((void)(cpu), VERIFY\_PERCPU\_PTR(&(var))))

per_cpu()可以获取别的处理器的per-CPU数据,但是不会禁止内核抢占,也没有提供任何形式的加锁保护,小心使用。

2.2.1 API使用

(1)

 DEFINE\_PER\_CPU\_FIRST(int, per_cpu_name);   //声明一个Int变量
 int var = get\_cpu\_var(per_cpu_name);
 var = var + 1;
 put\_cpu\_var(per_cpu_name);

get_cpu_var和 put_cpu_var成对使用。

(2)
应该要考虑加锁保护使用:

int var = per\_cpu(per_cpu_name, cpu);
var = var + 1;

2.3 其他的per-CPU变量

/\*
 \* Declaration/definition used for per-CPU variables that must come first in
 \* the set of variables.
 \*/
#define DECLARE\_PER\_CPU\_FIRST(type, name) \
 DECLARE\_PER\_CPU\_SECTION(type, name, PER\_CPU\_FIRST\_SECTION)

#define DEFINE\_PER\_CPU\_FIRST(type, name) \
 DEFINE\_PER\_CPU\_SECTION(type, name, PER\_CPU\_FIRST\_SECTION)

/\*
 \* Declaration/definition used for per-CPU variables that must be cacheline
 \* aligned under SMP conditions so that, whilst a particular instance of the
 \* data corresponds to a particular CPU, inefficiencies due to direct access by
 \* other CPUs are reduced by preventing the data from unnecessarily spanning
 \* cachelines.
 \*
 \* An example of this would be statistical data, where each CPU's set of data
 \* is updated by that CPU alone, but the data from across all CPUs is collated
 \* by a CPU processing a read from a proc file.
 \*/
#define DECLARE\_PER\_CPU\_SHARED\_ALIGNED(type, name) \
 DECLARE\_PER\_CPU\_SECTION(type, name, PER\_CPU\_SHARED\_ALIGNED\_SECTION) \
 \_\_\_\_cacheline\_aligned\_in\_smp

#define DEFINE\_PER\_CPU\_SHARED\_ALIGNED(type, name) \
 DEFINE\_PER\_CPU\_SECTION(type, name, PER\_CPU\_SHARED\_ALIGNED\_SECTION) \
 \_\_\_\_cacheline\_aligned\_in\_smp

#define DECLARE\_PER\_CPU\_ALIGNED(type, name) \
 DECLARE\_PER\_CPU\_SECTION(type, name, PER\_CPU\_ALIGNED\_SECTION) \
 \_\_\_\_cacheline\_aligned

#define DEFINE\_PER\_CPU\_ALIGNED(type, name) \
 DEFINE\_PER\_CPU\_SECTION(type, name, PER\_CPU\_ALIGNED\_SECTION) \
 \_\_\_\_cacheline\_aligned

/\*
 \* Declaration/definition used for per-CPU variables that must be page aligned.
 \*/
#define DECLARE\_PER\_CPU\_PAGE\_ALIGNED(type, name) \
 DECLARE\_PER\_CPU\_SECTION(type, name, "..page\_aligned") \
 \_\_aligned(PAGE\_SIZE)

#define DEFINE\_PER\_CPU\_PAGE\_ALIGNED(type, name) \
 DEFINE\_PER\_CPU\_SECTION(type, name, "..page\_aligned") \
 \_\_aligned(PAGE\_SIZE)

/\*
 \* Declaration/definition used for per-CPU variables that must be read mostly.
 \*/
#define DECLARE\_PER\_CPU\_READ\_MOSTLY(type, name) \
 DECLARE\_PER\_CPU\_SECTION(type, name, "..readmostly")

#define DEFINE\_PER\_CPU\_READ\_MOSTLY(type, name) \
 DEFINE\_PER\_CPU\_SECTION(type, name, "..readmostly")


(1)使用宏“DEFINE_PER_CPU_FIRST(type, name)”定义必须在每处理器变量集合中最先出现的每处理器变量。
(2)使用宏“DEFINE_PER_CPU_SHARED_ALIGNED(type, name)”定义和处理器缓存行对齐的每处理器变量,仅仅在SMP系统中需要和处理器缓存行对齐。
(3)使用宏“DEFINE_PER_CPU_ALIGNED(type, name)”定义和处理器缓存行对齐的每处理器变量,不管是不是SMP系统,都需要和处理器缓存行对齐。
(4)使用宏“DEFINE_PER_CPU_PAGE_ALIGNED(type, name)”定义和页长度对齐的每处理器变量。
(5)使用宏“DEFINE_PER_CPU_READ_MOSTLY(type, name)”定义以读为主的每处理器变量。

2.4 静态per-CPU变量的链接

// /linux-3.10.1/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) \
 VMLINUX\_SYMBOL(\_\_per\_cpu\_start) = .; \
 \*(.data..percpu..first) \
 . = ALIGN(PAGE\_SIZE); \
 \*(.data..percpu..page\_aligned) \
 . = ALIGN(cacheline); \
 \*(.data..percpu..readmostly) \
 . = ALIGN(cacheline); \
 \*(.data..percpu) \
 \*(.data..percpu..shared\_aligned) \
 VMLINUX\_SYMBOL(\_\_per\_cpu\_end) = .;


内核在编译链接时会把所有静态定义的per-CPU变量统一放到".data…percpu"section中,链接器生成__per_cpu_start和__per_cpu_end两个变量来表示该section的起始和结束地址。紧接着为了配合链接器的行为,Linux内核源码中针对以上的链接脚本声明了如下的外部变量:

// /linux-3.10.1/include/asm-generic/sections.h

extern char __per_cpu_load[], __per_cpu_start[], __per_cpu_end[];

在这里插入图片描述
在这里插入图片描述
__per_cpu_start从地址0开始,__per_cpu_end后面就是内核代码段的其实地址_text了。

__per_cpu_load的地址值是正常的内核编译地址,它用来指定,当vmlinux被加载到内存后,vmlinux里的.data…percpu section所处内存的位置:
在这里插入图片描述

其它的外部变量:

// /linux-3.10.1/include/asm-generic/sections.h

/\* References to section boundaries \*/

extern char _text[], _stext[], _etext[];
extern char _data[], _sdata[], _edata[];
extern char __bss_start[], __bss_stop[];
extern char __init_begin[], __init_end[];
extern char _sinittext[], _einittext[];
extern char _end[];
extern char __per_cpu_load[], __per_cpu_start[], __per_cpu_end[];
extern char __kprobes_text_start[], __kprobes_text_end[];
extern char __entry_text_start[], __entry_text_end[];
extern char __initdata_begin[], __initdata_end[];
extern char __start_rodata[], __end_rodata[];

/\* Start and end of .ctors section - used for constructor calls. \*/
extern char __ctors_start[], __ctors_end[];

2.5 setup_per_cpu_areas

前面用DEFINE_PER_CPU定义的变量,让系统中的每个CPU都拥有该变量的一个副本。目前只在".data…percpu" section中才有一份,接下来让系统中每个CPU都拥有该变量的一个副本。通过setup_per_cpu_areas函数来实现为系统中的每个CPU产生一份变量的副本。

(1)

// /linux-3.10.1/init/main.c
asmlinkage void __init start\_kernel(void)
{
	......
	setup\_per\_cpu\_areas();
	......
}

于系统初始化期间,也就是start_kernel函数中,调用的setup_per_cpu_areas函数,这个函数不但会完成变量副本的生成,而且会对per-CPU变量的动态分配机制进行初始化。

注意setup_per_cpu_areas函数初始化的调用在内存初始化的前面,此时Linux系统的物理内存管理系统还没有建立起来,所以使用的是Linux引导期内存分配器。

percpu memory allocator与slab memory allocator是一个层面的东西,都建立在page memory allocator基础之上。

(2)

// /linux-3.10.1/arch/x86/kernel/setup\_percpu.c

void __init setup\_per\_cpu\_areas(void)
{
	unsigned int cpu;
	unsigned long delta;
	int rc;

	pr\_info("NR\_CPUS:%d nr\_cpumask\_bits:%d nr\_cpu\_ids:%d nr\_node\_ids:%d\n",
		NR_CPUS, nr_cpumask_bits, nr_cpu_ids, nr_node_ids);
		
	......
	
	rc = -EINVAL;
	if (pcpu_chosen_fc != PCPU_FC_PAGE) {
		const size\_t dyn_size = PERCPU_MODULE_RESERVE +
			PERCPU_DYNAMIC_RESERVE - PERCPU_FIRST_CHUNK_RESERVE;
		size\_t atom_size;

		/\*
 \* On 64bit, use PMD\_SIZE for atom\_size so that embedded
 \* percpu areas are aligned to PMD. This, in the future,
 \* can also allow using PMD mappings in vmalloc area. Use
 \* PAGE\_SIZE on 32bit as vmalloc space is highly contended
 \* and large vmalloc area allocs can easily fail.
 \*/
#ifdef CONFIG\_X86\_64
		atom_size = PMD_SIZE;
#else
		atom_size = PAGE_SIZE;
#endif
		rc = pcpu\_embed\_first\_chunk(PERCPU_FIRST_CHUNK_RESERVE,
					    dyn_size, atom_size,
					    pcpu_cpu_distance,
					    pcpu_fc_alloc, pcpu_fc_free);
		if (rc < 0)
			pr\_warning("%s allocator failed (%d), falling back to page size\n",
				   pcpu_fc_names[pcpu_chosen_fc], rc);
	}
	if (rc < 0)
		rc = pcpu\_page\_first\_chunk(PERCPU_FIRST_CHUNK_RESERVE,
					   pcpu_fc_alloc, pcpu_fc_free,
					   pcpup_populate_pte);
	if (rc < 0)
		panic("cannot initialize percpu area (err=%d)", rc);

	/\* alrighty, percpu areas up and running \*/
	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(this_cpu_off, cpu) = per\_cpu\_offset(cpu);
		per\_cpu(cpu_number, cpu) = cpu;
		setup\_percpu\_segment(cpu);
		setup\_stack\_canary\_segment(cpu);
		......
		/\*
 \* Up to this point, the boot CPU has been using .init.data
 \* area. Reload any changed state for the boot CPU.
 \*/
		if (!cpu)
			switch\_to\_new\_gdt(cpu);
		}
}


调用pcpu_embed_first_chunk为percpu建立第一个chunk,调用成功便不会再调用pcpu_page_first_chunk函数了。因此我们重点分析pcpu_embed_first_chunk函数。

setup\_per\_cpu\_areas()
	(1) // #define BUILD\_EMBED\_FIRST\_CHUNK
	-->pcpu\_embed\_first\_chunk()
		-->ai = pcpu\_build\_alloc\_info(reserved_size, dyn_size, atom_size,
				   cpu_distance_fn);{
			const size\_t static_size = __per_cpu_end - __per_cpu_start;
		}

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

		--> areas = alloc\_bootmem\_nopanic(areas_size);
		
		-->/\* allocate, copy and determine base address \*/
			/\* allocate space for the whole group \*/
		-->ptr = alloc\_fn(cpu, gi->nr_units \* ai->unit_size, atom_size);

		-->/\*
 \* Copy data and free unused parts. This should happen after all
 \* allocations are complete; otherwise, we may end up with
 \* overlapping groups.
 \*/
			 /\* copy and return the unused part \*/
			memcpy(ptr, __per_cpu_load, ai->static_size);

		-->/\* base address is now known, determine group base offsets \*/

		-->pcpu\_setup\_first\_chunk();
		
	(2) //#define BUILD\_PAGE\_FIRST\_CHUNK
	-->pcpu\_page\_first\_chunk()
		--> ai = pcpu\_build\_alloc\_info(reserved_size, 0, PAGE_SIZE, NULL);
		
		--> pages_size = PFN\_ALIGN(unit_pages \* num\_possible\_cpus() \* sizeof(pages[0]));
		--> pages = alloc\_bootmem(pages_size);

		--> /\* allocate pages \*/

		/\* allocate vm area, map the pages and copy static data \*/
		-->\_\_pcpu\_map\_pages(unit_addr, &pages[unit \* unit_pages],unit_pages);
		/\* copy static data \*/
		-->memcpy((void \*)unit_addr, __per_cpu_load, ai->static_size);
		
		-->pcpu\_setup\_first\_chunk(ai, vm.addr);

(3)

pcpu\_embed\_first\_chunk();

/\* alrighty, percpu areas up and running \*/
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(this_cpu_off, cpu) = per\_cpu\_offset(cpu);
	per\_cpu(cpu_number, cpu) = cpu;
	setup\_percpu\_segment(cpu);
	setup\_stack\_canary\_segment(cpu);

	......
	
	switch\_to\_new\_gdt(cpu);
	
	......
	}

pcpu_embed_first_chunk为percpu建立第一个chunk,内核为percpu分配了一大段空间,在整个percpu空间中根据cpu个数将percpu的空间分为不同的unit,pcpu_base_addr表示整个系统中percpu的起始内存地址,__per_cpu_start表示静态分配的percpu起始地址。即 section ".data…percpu"的起始地址。

首先算出副本空间首地址(pcpu_base_addr)与".data…percpu"section首地址(__per_cpu_start)之间的偏移量delta:

delta = (unsigned long)pcpu_base_addr - (unsigned long)__per_cpu_start;

遍历系统中的cpu,设置每个cpu的__per_cpu_offset指针
pcpu_unit_offsets[cpu]保存对应cpu所在副本空间相对于pcpu_base_addr的偏移量
加上delta,这样就可以得到每个cpu的per-cpu变量副本的偏移量, 放在__per_cpu_offset数组中.

在这里插入图片描述
在这里插入图片描述
x86_64下,设置各cpu的percpu内存块的起始地址值到各自cpu的gs寄存器里,这样gs寄存器中存放的是当前cpu的percpu内存块的起始地址。当我们在访问percpu变量时,只需要将gs寄存器里的地址,加上我们想要访问的percpu变量的地址,就能得到在该cpu上,该percpu变量真实的内存地址。有了这个地址,我们就可以操作这个percpu变量了。

void load\_percpu\_segment(int cpu)
{
#ifdef CONFIG\_X86\_32
	loadsegment(fs, __KERNEL_PERCPU);
#else
	loadsegment(gs, 0);
	wrmsrl(MSR_GS_BASE, (unsigned long)per\_cpu(irq_stack_union.gs_base, cpu));
#endif
	load\_stack\_canary\_segment();
}

/\*
 \* Current gdt points %fs at the "master" per-cpu area: after this,
 \* it's on the real one.
 \*/
void switch\_to\_new\_gdt(int cpu)
{
	struct desc\_ptr gdt_descr;

	gdt_descr.address = (long)get\_cpu\_gdt\_table(cpu);
	gdt_descr.size = GDT_SIZE - 1;
	load\_gdt(&gdt_descr);
	/\* Reload the per-cpu base \*/

	load\_percpu\_segment(cpu);
}

(4)pcpu_embed_first_chunk
pcpu_embed_first_chunk() 由通用 percpu 设置使用。如果 arch config 需要或将使用通用设置,请构建它。
这是一个帮助程序,可以方便地设置嵌入的第一个percpu块,为percpu建立第一个chunk,可以在需要pcpu_setup_first_chunk()的地方调用它。
如果此函数用于设置第一个块,则通过调用alloc_fn来分配它,并按原样使用它,而不映射到vmalloc区域。分配总是与 atom_size 对齐的 atom_size 的整数倍。
这使第一个块能够返回到通常使用较大页面大小的线性物理映射。请注意,这可能导致NUMA机器上的cpu->unit 映射非常sparse,因此需要很大的vmalloc地址空间。如果 vmalloc 空间不是比node内存地址之间的距离大几个数量级(即 32 位 NUMA 机器),则不要使用此分配器。
dyn_size 指定最小动态区域大小。
如果所需的大小小于最小或指定的unit size,则使用free_fn返回剩余的大小。

/\*
 \* pcpu\_embed\_first\_chunk - 将第一个 percpu 块嵌入 bootmem
 \* @reserved\_size: percpu 保留区域的大小(以字节为单位)
 \* @dyn\_size:动态分配的最小可用大小(以字节为单位)
 \* @atom\_size: 分配原子大小
 \* @cpu\_distance\_fn: 确定cpu之间距离的回调函数,可选
 \* @alloc\_fn: 分配 percpu 页面的函数
 \* @free\_fn: 释放 percpu 页面的函数
 \* RETURNS:
 \* 0 on success, -errno on failure.
\*/
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, max_distance;
	int group, i, rc;

	//收集整理该架构下的percpu信息,结果放在struct pcpu\_alloc\_info结构中
	ai = pcpu\_build\_alloc\_info(reserved_size, dyn_size, atom_size,
				   cpu_distance_fn);
	if (IS\_ERR(ai))
		return PTR\_ERR(ai);

	//计算每个cpu占用的percpu内存空间大小,包括static\_size + reserved\_size + dyn\_size
	size_sum = ai->static_size + ai->reserved_size + ai->dyn_size;
	areas_size = PFN\_ALIGN(ai->nr_groups \* sizeof(void \*));

	//areas用来保存每个group的percpu内存起始地址,为其分配空间,做临时存储使用,用完释放掉
	areas = alloc\_bootmem\_nopanic(areas_size);
	if (!areas) {
		rc = -ENOMEM;
		goto out_free;
	}

	/\* allocate, copy and determine base address \*/
	//对该系统下的每个group操作,为每个group分配percpu内存区域
	//为该group分配percpu内存区域。长度为该group里的cpu数目 X 每颗处理器的percpu递进单位
	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 \*/
		//返回物理地址(是从bootmem里取得内存,得到的是物理内存)的内存虚拟地址ptr
		ptr = alloc\_fn(cpu, gi->nr_units \* ai->unit_size, atom_size);
		if (!ptr) {
			rc = -ENOMEM;
			goto out_free_areas;
		}
		/\* kmemleak tracks the percpu allocations separately \*/
		kmemleak\_free(ptr);
		//将分配到的该组percpu内存虚拟起始地址保存在areas数组中
		areas[group] = ptr;

		//比较每个group的percpu内存地址,保存最小的内存地址,即percpu内存的起始地址
		//为后边计算group的percpu内存地址的偏移量
		base = min(ptr, base);
	}

	/\*
 \* 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++) {
		//取出该group下的组信息
		struct pcpu\_group\_info \*gi = &ai->groups[group];
		//得到该group的percpu内存起始地址
		void \*ptr = areas[group];
		
		//遍历该组中的cpu,并得到每个cpu对应的percpu内存地址
		for (i = 0; i < gi->nr_units; i++, ptr += ai->unit_size) {
			if (gi->cpu_map[i] == NR_CPUS) {
				/\* unused unit, free whole \*/
				//释放掉未使用的unit
				free\_fn(ptr, ai->unit_size);
				continue;
			}
			/\* copy and return the unused part \*/
			//将静态定义的percpu变量拷贝到每个cpu的percpu内存起始地址
			memcpy(ptr, __per_cpu_load, ai->static_size);
			//为每个cpu释放掉多余的空间,多余的空间是指 static\_size + reserved\_size + dyn\_size
			free\_fn(ptr + size_sum, ai->unit_size - size_sum);
		}
	}

	/\* base address is now known, determine group base offsets \*/
	//计算group的percpu内存地址的偏移量
	max_distance = 0;
	for (group = 0; group < ai->nr_groups; group++) {
		ai->groups[group].base_offset = areas[group] - base;
		max_distance = max\_t(size\_t, max_distance,
				     ai->groups[group].base_offset);
	}
	max_distance += ai->unit_size;

	/\* warn if maximum distance is further than 75% of vmalloc space \*/
	//检查最大偏移量是否超过vmalloc空间的75%
	if (max_distance > (VMALLOC_END - VMALLOC_START) \* 3 / 4) {
		pr\_warning("PERCPU: max\_distance=0x%zx too large for vmalloc "
			   "space 0x%lx\n", max_distance,
			   (unsigned long)(VMALLOC_END - VMALLOC_START));
#ifdef CONFIG\_NEED\_PER\_CPU\_PAGE\_FIRST\_CHUNK
		/\* and fail if we have fallback \*/
		rc = -EINVAL;
		goto out_free;
#endif
	}

	pr\_info("PERCPU: Embedded %zu pages/cpu @%p s%zu r%zu d%zu u%zu\n",
		PFN\_DOWN(size_sum), base, ai->static_size, ai->reserved_size,
		ai->dyn_size, ai->unit_size);

	//为percpu建立第一个chunk
	rc = pcpu\_setup\_first\_chunk(ai, base);
	goto out_free;

out_free_areas:
	for (group = 0; group < ai->nr_groups; group++)
		free\_fn(areas[group],
			ai->groups[group].nr_units \* ai->unit_size);
out_free:
	pcpu\_free\_alloc\_info(ai);
	if (areas)
		free\_bootmem(\_\_pa(areas), areas_size);
	return rc;
}
#endif /\* BUILD\_EMBED\_FIRST\_CHUNK \*/

小结:
setup_per_cpu_areas函数首先计算出".data…percpu"section的空间大小(static_size =__per_cpu_end - __per_cpu_start),static_size是内核源码中所有用DEFINE_PER_CPU及其变体所定义出的静态per-CPU变量所占空间的大小。此外内核还为模块使用的per-CPU变量以及动态分配的per-CPU变量预留了空间,大小分别记为reserved_size和dyn_size。

然后setup_per_cpu_areas函数调用alloc_bootmem_nopanic来分配一段内存,用来保存per-CPU变量副本。此时因为系统的内存管理系统还没有建立起来,所以使用的是Linux引导期内存分配器。这块内存的大小要依赖于系统中CPU的数量,因为要为每个CPU创建变量的副本。内核代码称每个CPU变量副本所在内存空间为一个unit,所以代码中的nr_units变量实际上表示了系统中CPU的数量,每个unit的大小记为unit_size,unit_size =PFN_ALIGN(static_size + reserved_size +dyn_size)。如此,变量副本所在空间的大小就是nr_units * unit_size。指针变量pcpu_base_addr指向副本空间的起始地址。

(5)pcpu_build_alloc_info
此函数确定单元的分组、它们到 cpu 的映射以及考虑所需的 percpu 大小、分配原子大小和 CPU 之间的距离的其他参数。用来收集整理该架构下的percpu信息,结果放在struct pcpu_alloc_info结构中。

Groups是atom size的倍数,两种都是LOCAL_DISTANCE的cpu会被分组在一起,共享同一组的单元的空间。返回的配置保证在不同组的不同节点上有 CPU,并且分配的虚拟地址空间的使用率 >=75%。

/\* pcpu\_build\_alloc\_info() is used by both embed and page first chunk \*/
#if defined(BUILD\_EMBED\_FIRST\_CHUNK) || defined(BUILD\_PAGE\_FIRST\_CHUNK)

/\*
 \* pcpu\_build\_alloc\_info - build alloc\_info considering distances between CPUs
 \* @reserved\_size: percpu 保留区域的大小(以字节为单位)
 \* @dyn\_size:动态分配的最小可用大小(以字节为单位)
 \* @atom\_size: 分配原子大小
 \* @cpu\_distance\_fn: 确定cpu之间距离的回调函数,可选
 \* 
 \* RETURNS:
 \* 成功时,返回指向新的 allocation\_info 的指针。
 \* 失败时,返回 ERR\_PTR 值。
\*/
static struct pcpu\_alloc\_info \* __init 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;
	
	//计算出".data..percpu"section的空间大小(static\_size =\_\_per\_cpu\_end - \_\_per\_cpu\_start)
	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, uninitialized\_var(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));

	/\* calculate size\_sum and ensure dyn\_size is enough for early alloc \*/
	//计算每个cpu所占有的percpu空间大小,包括static\_size + reserved\_size + dyn\_size
	size_sum = PFN\_ALIGN(static_size + reserved_size +
			    max\_t(size\_t, dyn_size, PERCPU_DYNAMIC_EARLY_SIZE));
	//重新计算动态分配的percpu空间大小
	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.
 \*/
	 //计算每个unit的大小,即每个group中的每个cpu占用的percpu内存大小为一个unit
	min_unit_size = max\_t(size\_t, size_sum, PCPU_MIN_UNIT_SIZE);

	alloc_size = roundup(min_unit_size, atom_size);
	upa = alloc_size / min_unit_size;
	while (alloc_size % upa || ((alloc_size / upa) & ~PAGE_MASK))
		upa--;
	max_upa = upa;

	/\* group cpus according to their proximity \*/
	//为cpu分组,将接近的cpu分到一组中
	//group\_cnt[group]即是该组中的cpu个数
	for\_each\_possible\_cpu(cpu) {
		group = 0;
	next_group:
		for\_each\_possible\_cpu(tcpu) {
			if (cpu == tcpu)
				break;
			if (group_map[tcpu] == group && cpu_distance_fn &&
			    (cpu\_distance\_fn(cpu, tcpu) > LOCAL_DISTANCE ||
			     cpu\_distance\_fn(tcpu, cpu) > LOCAL_DISTANCE)) {
				group++;
				nr_groups = max(nr_groups, group + 1);
				goto next_group;
			}
		}
		group_map[cpu] = group;
		group_cnt[group]++;
	}

	/\*
 \* Expand unit size until address space usage goes over 75%
 \* and then as much as possible without using more address
 \* space.
 \*/
	last_allocs = INT_MAX;
	for (upa = max_upa; upa; upa--) {
		int allocs = 0, wasted = 0;

		if (alloc_size % upa || ((alloc_size / upa) & ~PAGE_MASK))
			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;
	}
	upa = best_upa;

	/\* allocate and fill alloc\_info \*/
	//计算每个group中的cpu个数
	for (group = 0; group < nr_groups; group++)
		nr_units += roundup(group_cnt[group], upa);
	
	//分配pcpu\_alloc\_info结构空间,并初始化
	ai = pcpu\_alloc\_alloc\_info(nr_groups, nr_units);
	if (!ai)
		return ERR\_PTR(-ENOMEM);
	//为每个group的cpu\_map指针赋值为group[0],group[0]中的cpu\_map中的值初始化为NR\_CPUS
	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);
	}

	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_cnt[group]; 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.
 \*/
		 //设置组内的相对于0地址偏移量,后续会设置真正的对于percpu起始地址的偏移量
		gi->base_offset = unit \* ai->unit_size;

		//设置cpu\_map数组,数组保存该组中的cpu id号。以及设置组中的cpu个数gi->nr\_units
		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;
}
#endif /\* BUILD\_EMBED\_FIRST\_CHUNK || BUILD\_PAGE\_FIRST\_CHUNK \*/

// linux-3.10.1/mm/percpu.c

/\* the address of the first chunk which starts with the kernel static area \*/
void \*pcpu_base_addr __read_mostly;
EXPORT\_SYMBOL\_GPL(pcpu_base_addr);

// linux-3.10.1/include/linux/percpu.h

extern void \*pcpu_base_addr;
extern const unsigned long \*pcpu_unit_offsets;

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

struct pcpu_alloc_info:
static_size:静态定义的percpu变量占用内存区域长度。在内核初始化时,就直接被拷贝到了各个percpu内存块的static区。
reserved_size:预留区域,在percpu内存分配指定为预留区域分配时,将使用该区域。内核模块中的静态percpu变量,当内核模块被加载到内存时,其静态percpu变量就会在这个区域分配内存。
dyn_size:动态分配的percpu变量占用内存区域长度。
unit_size:每个cpu的percpu空间所占得内存空间为一个unit, 每个unit的大小记为unit_size。
atom_size:PMD_SIZE(CONFIG_X86_64)/ PAGE_SIZE。
alloc_size:要分配的percpu内存空间。
__ai_size:整个pcpu_alloc_info结构体的大小。
nr_groups:该架构下的处理器分组数目。
struct pcpu_group_info groups[]:该架构下的处理器分组信息。

struct pcpu_group_info:
nr_units:该组的处理器数目
base_offset:组的percpu内存地址起始地址,即组内处理器数目×处理器percpu虚拟内存递进基本单位
unsigned int *cpu_map:组内cpu对应数组,保存cpu id号

(6)pcpu_setup_first_chunk
初始化包含内核静态 perpcu 区域的第一个 percpu 块。 此函数将从 arch percpu 区域设置路径中调用。
ai 包含初始化第一个块和启动动态 percpu 分配器所需的所有信息。

ai->static_size 是静态 percpu 区域的大小。
ai->reserved_size,如果非零,指定在第一个块中的静态区域之后要保留的字节数。 这会保留第一个块,使其仅可通过保留的 percpu 分配获得。 这主要用于在寻址模型对符号重定位的偏移范围有限的架构上为模块 percpu 静态区域提供服务,以确保模块 percpu 符号落在可重定位范围内。
ai->dyn_size 确定第一个块中可用于动态分配的字节数。 ai->static_size + ai->reserved_size + ai->dyn_size 和 ai->unit_size 之间的区域未使用。
ai->unit_size 指定单元大小,必须与 PAGE_SIZE 对齐并且等于或大于 ai->static_size + ai->reserved_size + ai->dyn_size。
ai->atom_size 是分配原子大小,用作 vm 区域的对齐。
ai->alloc_size 是分配大小,总是 ai->atom_size 的倍数。 如果 ai->unit_size 大于 ai->atom_size,则它大于 ai->atom_size。
ai->nr_groups 和 ai->groups 描述 percpu 区域的虚拟内存布局。 应该合并的单元被放在同一个组中。 动态 VM 区域将根据这些分组进行分配。如果ai->nr_groups为零,则假设一个包含所有单位的组。

调用者应该已经将第一个块映射到 base_addr 并将静态数据复制到每个单元。

如果第一个块最终同时拥有保留区域和动态区域,那么它将由两个块提供服务——一个用于核心静态区域和保留区域,另一个用于动态区域。它们共享相同的vm和页面映射,但使用不同的区域分配映射来隔离彼此。后一个块在块槽中循环,可以像任何其他块一样进行动态分配。

最后的话

最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!

资料预览

给大家整理的视频资料:

给大家整理的电子书资料:

如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
于 ai->static_size + ai->reserved_size + ai->dyn_size。
ai->atom_size 是分配原子大小,用作 vm 区域的对齐。
ai->alloc_size 是分配大小,总是 ai->atom_size 的倍数。 如果 ai->unit_size 大于 ai->atom_size,则它大于 ai->atom_size。
ai->nr_groups 和 ai->groups 描述 percpu 区域的虚拟内存布局。 应该合并的单元被放在同一个组中。 动态 VM 区域将根据这些分组进行分配。如果ai->nr_groups为零,则假设一个包含所有单位的组。

调用者应该已经将第一个块映射到 base_addr 并将静态数据复制到每个单元。

如果第一个块最终同时拥有保留区域和动态区域,那么它将由两个块提供服务——一个用于核心静态区域和保留区域,另一个用于动态区域。它们共享相同的vm和页面映射,但使用不同的区域分配映射来隔离彼此。后一个块在块槽中循环,可以像任何其他块一样进行动态分配。

最后的话

最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!

资料预览

给大家整理的视频资料:

[外链图片转存中…(img-OrXkWb2m-1714132642199)]

给大家整理的电子书资料:

[外链图片转存中…(img-7NRDTAtO-1714132642199)]

如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 13
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值