Files
linux-insides-zh/Concepts/per-cpu.md
2017-04-04 23:46:38 +08:00

228 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
每CPU变量
================================================================================
每CPU变量是一项内核特性。从它的名字你就可以理解这项特性的意义了。我们可以创建一个变量然后每个CPU上都会有一个此变量的拷贝。本节我们来看下这个特性并试着去理解它是如何实现以及工作的。
内核提供了一个创建每CPU变量的API - `DEFINE_PER_CPU` 宏:
```C
#define DEFINE_PER_CPU(type, name) \
DEFINE_PER_CPU_SECTION(type, name, "")
```
像其它许多每CPU变量一样这个宏定义在 [include/linux/percpu-defs.h](https://github.com/torvalds/linux/blob/master/include/linux/percpu-defs.h) 中。现在我们来看下这个特性是如何实现的。
看下 `DECLARE_PER_CPU` 的定义,可以看到它使用了 2 个参数:`type``name`因此我们可以这样创建每CPU变量
```C
DEFINE_PER_CPU(int, per_cpu_n)
```
我们传入要创建变量的类型和名字,`DEFINE_PER_CPU` 调用 `DEFINE_PER_CPU_SECTION`,将两个参数和空字符串传递给后者。让我们来看下 `DEFINE_PER_CPU_SECTION` 的定义:
```C
#define DEFINE_PER_CPU_SECTION(type, name, sec) \
__PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES \
__typeof__(type) name
```
```C
#define __PCPU_ATTRS(sec) \
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
PER_CPU_ATTRIBUTES
```
其中 `section` 是:
```C
#define PER_CPU_BASE_SECTION ".data..percpu"
```
展开所有的宏我们得到一个全局的每CPU变量
```C
__attribute__((section(".data..percpu"))) int per_cpu_n
```
这意味着我们在 `.data..percpu` 段有了一个 `per_cpu_n` 变量,可以在 `vmlinux` 中找到它:
```
.data..percpu 00013a58 0000000000000000 0000000001a5c000 00e00000 2**12
CONTENTS, ALLOC, LOAD, DATA
```
好,现在我们知道了,当我们使用 `DEFINE_PER_CPU` 宏时,一个在 `.data..percpu` 段中的每CPU变量就被创建了。当内核初始化时调用 `setup_per_cpu_areas` 函数加载几次 `.data..percpu`每个CPU上对每个段都加载一次。
让我们来看下每CPU区域初始化流程。它从 [init/main.c](https://github.com/torvalds/linux/blob/master/init/main.c) 中调用 `setup_per_cpu_areas` 函数开始,这个函数定义在 [arch/x86/kernel/setup_percpu.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/setup_percpu.c) 中。
```C
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);
```
`setup_per_cpu_areas` 开始输出CPUs集合的最大个数这个在内核配置中以 `CONFIG_NR_CPUS` 配置项设置实际的CPU个数`nr_cpumask_bits`(对于新的 `cpumask` 操作来说和 `NR_CPUS` 是一样的),还有 `NUMA` 节点个数。
我们可以在dmesg中看到这些输出
```
$ dmesg | grep percpu
[ 0.000000] setup_percpu: NR_CPUS:8 nr_cpumask_bits:8 nr_cpu_ids:8 nr_node_ids:1
```
然后我们检查 `percpu` 第一个块分配器。所有的每CPU区域都是以块进行分配的。第一个块用于静态每CPU变量。Linux内核提供了决定第一个块分配器类型的命令行`percpu_alloc` 。我们可以在内核文档中读到它的说明。
```
percpu_alloc= 选择要使用哪个每CPU第一个块分配器。
当前支持的类型是 "embed" 和 "page"。
不同架构支持这些类型的子集或不支持。
更多分配器的细节参考 mm/percpu.c 中的注释。
这个参数主要是为了调试和性能比较的。
```
[mm/percpu.c](https://github.com/torvalds/linux/blob/master/mm/percpu.c) 包含了这个命令行选项的处理函数:
```C
early_param("percpu_alloc", percpu_alloc_setup);
```
其中 `percpu_alloc_setup` 函数根据 `percpu_alloc` 参数值设置 `pcpu_chosen_fc` 变量。默认第一个块分配器是 `auto`
```C
enum pcpu_fc pcpu_chosen_fc __initdata = PCPU_FC_AUTO;
```
如果内核命令行中没有设置 `percpu_alloc` 参数,就会使用 `embed` 分配器将第一个每CPU块嵌入进带 [memblock](http://0xax.gitbooks.io/linux-insides/content/mm/linux-mm-1.html) 的bootmem。最后一个分配器和第一个块 `page` 分配器一样,只是将第一个块使用 `PAGE_SIZE` 页进行了映射。
如我上面所写,首先我们在 `setup_per_cpu_areas` 中对第一个块分配器检查检查到第一个块分配器不是page分配器
```C
if (pcpu_chosen_fc != PCPU_FC_PAGE) {
...
...
...
}
```
如果不是 `PCPU_FC_PAGE`,我们就使用 `embed` 分配器并使用 `pcpu_embed_first_chunk` 函数分配第一块空间。
```C
rc = pcpu_embed_first_chunk(PERCPU_FIRST_CHUNK_RESERVE,
dyn_size, atom_size,
pcpu_cpu_distance,
pcpu_fc_alloc, pcpu_fc_free);
```
如前所述,函数 `pcpu_embed_first_chunk` 将第一个percpu块嵌入bootmen因此我们传递一些参数给 `pcpu_embed_first_chunk`。参数如下:
* `PERCPU_FIRST_CHUNK_RESERVE` - 为静态变量 `percpu` 的保留空间大小;
* `dyn_size` - 动态分配的最少空闲字节;
* `atom_size` - 所有的分配都是这个的整数倍,并以此对齐;
* `pcpu_cpu_distance` - 决定cpus距离的回调函数
* `pcpu_fc_alloc` - 分配 `percpu` 页的函数;
* `pcpu_fc_free` - 释放 `percpu` 页的函数。
在调用 `pcpu_embed_first_chunk` 前我们计算好所有的参数:
```C
const size_t dyn_size = PERCPU_MODULE_RESERVE + PERCPU_DYNAMIC_RESERVE - PERCPU_FIRST_CHUNK_RESERVE;
size_t atom_size;
#ifdef CONFIG_X86_64
atom_size = PMD_SIZE;
#else
atom_size = PAGE_SIZE;
#endif
```
如果第一个块分配器是 `PCPU_FC_PAGE`,我们用 `pcpu_page_first_chunk` 来代替 `pcpu_embed_first_chunk``percpu` 区域准备好以后,我们用 `setup_percpu_segment` 函数设置 `percpu` 的偏移和段(只针对 `x86` 系统),并将前面的数据从数组移到 `percpu` 变量(`x86_cpu_to_apicid`, `irq_stack_ptr` 等等。当内核完成初始化进程后我们就有了N个 `.data..percpu` 段,其中 N 是 CPU 个数bootstrap 进程使用的段将会包含用 `DEFINE_PER_CPU` 宏创建的未初始化的变量。
内核提供了操作每 CPU 变量的API
* get_cpu_var(var)
* put_cpu_var(var)
让我们来看看 `get_cpu_var` 的实现:
```C
#define get_cpu_var(var) \
(*({ \
preempt_disable(); \
this_cpu_ptr(&var); \
}))
```
Linux 内核是抢占式的,获取每 CPU 变量需要我们知道内核运行在哪个处理器上。因此访问每 CPU 变量时,当前代码不能被抢占,不能移到其它的 CPU。如我们所见这就是为什么首先调用 `preempt_disable` 函数然后调用 `this_cpu_ptr` 宏,像这样:
```C
#define this_cpu_ptr(ptr) raw_cpu_ptr(ptr)
```
以及
```C
#define raw_cpu_ptr(ptr) per_cpu_ptr(ptr, 0)
```
`per_cpu_ptr` 返回一个指向给定CPU第2个参数每CPU变量的指针。当我们创建了一个每CPU变量并对其进行了修改时我们必须调用 `put_cpu_var` 宏通过函数 `preempt_enable` 使能抢占。因此典型的每CPU变量的使用如下
```C
get_cpu_var(var);
...
//用这个 'var' 做些啥
...
put_cpu_var(var);
```
让我们来看下这个 `per_cpu_ptr` 宏:
```C
#define per_cpu_ptr(ptr, cpu) \
({ \
__verify_pcpu_ptr(ptr); \
SHIFT_PERCPU_PTR((ptr), per_cpu_offset((cpu))); \
})
```
就像我们上面写的这个宏返回了一个给定cpu的每CPU变量。首先它调用了 `__verify_pcpu_ptr`
```C
#define __verify_pcpu_ptr(ptr)
do {
const void __percpu *__vpp_verify = (typeof((ptr) + 0))NULL;
(void)__vpp_verify;
} while (0)
```
声明了 `ptr` 类型的 `const void __percpu *`
然后我们看到调用了 `SHIFT_PERCPU_PTR` 宏,带了两个参数。第一个参数是我们的指针,第二个参数是传给 `per_cpu_offset` 宏的CPU数
```C
#define per_cpu_offset(x) (__per_cpu_offset[x])
```
该宏将 `x` 扩展为 `__per_cpu_offset` 数组:
```C
extern unsigned long __per_cpu_offset[NR_CPUS];
```
其中 `NR_CPUS` 是CPU的数目。`__per_cpu_offset` 数组以CPU变量拷贝之间的距离填充。例如所有每CPU变量是 `X` 字节大小,所以我们通过 `__per_cpu_offset[Y]` 就可以访问 `X*Y`。让我们来看下 `SHIFT_PERCPU_PTR` 的实现:
```C
#define SHIFT_PERCPU_PTR(__p, __offset) \
RELOC_HIDE((typeof(*(__p)) __kernel __force *)(__p), (__offset))
```
`RELOC_HIDE` 仅是取 `(typeof(ptr)) (__ptr + (off))` 的偏移量,并返回该变量的指针。
就这些了当然这不是全部的API只是一个大概。开头是比较艰难但是理解per-cpu变量你只需理解 [include/linux/percpu-defs.h](https://github.com/torvalds/linux/blob/master/include/linux/percpu-defs.h) 的奥秘。
让我们再看下获得 per-cpu 变量指针的算法:
* 内核在初始化流程中创建多个 `.data..percpu` 区域(每 per-cpu 变量);
* 所有 `DEFINE_PER_CPU` 宏创建的变量都将重分配到首个扇区或者 CPU0
* `__per_cpu_offset` 数组以 (`BOOT_PERCPU_OFFSET`) 和 `.data..percpu` 扇区之间的距离填充;
* 当调用 `per_cpu_ptr` 时,例如取一个 per-cpu 变量的第三个 CPU 的指针,将访问 `__per_cpu_offset` 数组,该数组的索引指向了所需 CPU。
就这么多了。