Per-cpu 變量【轉】

轉自: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_apicidirq_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。

就這麼多了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章