轉自:https://xinqiu.gitbooks.io/linux-insides-cn/content/Concepts/linux-cpu-1.html
Per-cpu 變量
Per-cpu 變量是一項內核特性。從它的名字你就可以理解這項特性的意義了。我們可以創建一個變量,然後每個 CPU 上都會有一個此變量的拷貝。本節我們來看下這個特性,並試着去理解它是如何實現以及工作的。
內核提供了一個創建 per-cpu 變量的 API - DEFINE_PER_CPU
宏:
#define DEFINE_PER_CPU(type, name) \
DEFINE_PER_CPU_SECTION(type, name, "")
正如其它許多處理 per-cpu 變量的宏一樣,這個宏定義在 include/linux/percpu-defs.h 中。現在我們來看下這個特性是如何實現的。
看下 DECLARE_PER_CPU
的定義,可以看到它使用了 2 個參數:type
和 name
,因此我們可以這樣創建 per-cpu 變量:
DEFINE_PER_CPU(int, per_cpu_n)
我們傳入要創建變量的類型和名字,DEFINE_PER_CPU
調用 DEFINE_PER_CPU_SECTION
,將兩個參數和空字符串傳遞給後者。讓我們來看下 DEFINE_PER_CPU_SECTION
的定義:
#define DEFINE_PER_CPU_SECTION(type, name, sec) \
__PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES \
__typeof__(type) name
#define __PCPU_ATTRS(sec) \
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
PER_CPU_ATTRIBUTES
其中 section
是:
#define PER_CPU_BASE_SECTION ".data..percpu"
當所有的宏展開之後,我們得到一個全局的 per-cpu 變量:
__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
段中的 per-cpu 變量就被創建了。內核初始化時,調用 setup_per_cpu_areas
函數多次加載 .data..percpu
段,每個 CPU 一次。
讓我們來看下 per-cpu 區域初始化流程。它從 init/main.c 中調用 setup_per_cpu_areas
函數開始,這個函數定義在 arch/x86/kernel/setup_percpu.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
開始輸出在內核配置中以 CONFIG_NR_CPUS
配置項設置的最大 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
然後我們檢查 per-cpu
第一個塊分配器。所有的 per-cpu 區域都是以塊進行分配的。第一個塊用於靜態 per-cpu 變量。Linux 內核提供了決定第一個塊分配器類型的命令行:percpu_alloc
。我們可以在內核文檔中讀到它的說明。
percpu_alloc= 選擇要使用哪個 per-cpu 第一個塊分配器。
當前支持的類型是 "embed" 和 "page"。
不同架構支持這些類型的子集或不支持。
更多分配器的細節參考 mm/percpu.c 中的註釋。
這個參數主要是爲了調試和性能比較的。
mm/percpu.c 包含了這個命令行選項的處理函數:
early_param("percpu_alloc", percpu_alloc_setup);
其中 percpu_alloc_setup
函數根據 percpu_alloc
參數值設置 pcpu_chosen_fc
變量。默認第一個塊分配器是 auto
:
enum pcpu_fc pcpu_chosen_fc __initdata = PCPU_FC_AUTO;
如果內核命令行中沒有設置 percpu_alloc
參數,就會使用 embed
分配器,將第一個 per-cpu 塊嵌入進帶 memblock 的 bootmem。最後一個分配器和第一個塊 page
分配器一樣,只是將第一個塊使用 PAGE_SIZE
頁進行了映射。
如我上面所寫,首先我們在 setup_per_cpu_areas
中對第一個塊分配器檢查,檢查到第一個塊分配器不是 page 分配器:
if (pcpu_chosen_fc != PCPU_FC_PAGE) {
...
...
...
}
如果不是 PCPU_FC_PAGE
,我們就使用 embed
分配器並使用 pcpu_embed_first_chunk
函數分配第一塊空間。
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
將第一個 per-cpu 塊嵌入 bootmen,因此我們傳遞一些參數給 pcpu_embed_first_chunk
。參數如下:
PERCPU_FIRST_CHUNK_RESERVE
- 爲靜態變量per-cpu
保留空間的大小;dyn_size
- 動態分配的最少空閒字節;atom_size
- 所有的分配都是這個的整數倍,並以此對齊;pcpu_cpu_distance
- 決定 cpus 距離的回調函數;pcpu_fc_alloc
- 分配percpu
頁的函數;pcpu_fc_free
- 釋放percpu
頁的函數。
在調用 pcpu_embed_first_chunk
前我們計算好所有的參數:
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
。 per-cpu
區域準備好以後,我們用 setup_percpu_segment
函數設置 per-cpu
的偏移和段(只針對 x86
系統),並將前面的數據從數組移到 per-cpu
變量(x86_cpu_to_apicid
, irq_stack_ptr
等等)。當內核完成初始化進程後,我們就有了N個 .data..percpu
段,其中 N 是 CPU 個數,bootstrap 進程使用的段將會包含用 DEFINE_PER_CPU
宏創建的未初始化的變量。
內核提供了操作 per-cpu 變量的API:
- get_cpu_var(var)
- put_cpu_var(var)
讓我們來看看 get_cpu_var
的實現:
#define get_cpu_var(var) \
(*({ \
preempt_disable(); \
this_cpu_ptr(&var); \
}))
Linux 內核是搶佔式的,獲取 per-cpu 變量需要我們知道內核運行在哪個處理器上。因此訪問 per-cpu 變量時,當前代碼不能被搶佔,不能移到其它的 CPU。如我們所見,這就是爲什麼首先調用 preempt_disable
函數然後調用 this_cpu_ptr
宏,像這樣:
#define this_cpu_ptr(ptr) raw_cpu_ptr(ptr)
以及
#define raw_cpu_ptr(ptr) per_cpu_ptr(ptr, 0)
per_cpu_ptr
返回一個指向給定 CPU(第 2 個參數) per-cpu 變量的指針。當我們創建了一個 per-cpu 變量並對其進行了修改時,我們必須調用 put_cpu_var
宏通過函數 preempt_enable
使能搶佔。因此典型的 per-cpu 變量的使用如下:
get_cpu_var(var);
...
//用這個 'var' 做些啥
...
put_cpu_var(var);
讓我們來看下這個 per_cpu_ptr
宏:
#define per_cpu_ptr(ptr, cpu) \
({ \
__verify_pcpu_ptr(ptr); \
SHIFT_PERCPU_PTR((ptr), per_cpu_offset((cpu))); \
})
就像我們上面寫的,這個宏返回了一個給定 cpu 的 per-cpu 變量。首先它調用了 __verify_pcpu_ptr
:
#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數:
#define per_cpu_offset(x) (__per_cpu_offset[x])
該宏將 x
擴展爲 __per_cpu_offset
數組:
extern unsigned long __per_cpu_offset[NR_CPUS];
其中 NR_CPUS
是 CPU 的數目。__per_cpu_offset
數組以 CPU 變量拷貝之間的距離填充。例如,所有 per-cpu 變量是 X
字節大小,所以我們通過 __per_cpu_offset[Y]
就可以訪問 X*Y
。讓我們來看下 SHIFT_PERCPU_PTR
的實現:
#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 的奧祕。
讓我們再看下獲得 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。
就這麼多了。