.data.percpuまわりをざっくりと読んでみた。
前回の記事「15分カーネルソースリーディング」の続き。
まず、.data.percpuが何か特殊なセクションか調べてみることにする。
私は今Ubuntuを使っているので、手っ取り早く手元にあるLinuxのセクションを見ようとした。
/boot/vmlinuzうんたらにreadelfしたが
readelf: Error: Not an ELF file - it has the wrong magic bytes at the start
readelf: Error: /boot/vmlinuz-2.6.31-22-generic: Failed to read file header
と怒られた。
結論から言うと、このファイルはelfの実行形式ファイルではない。(参考資料:http://wiki.bit-hive.com/linuxkernelmemo/pg/bzImage)
ただ、リンク先の説明によれば、どうやらelf実行形式ファイルを材料にはしているようだ。
なので、Linuxをコンパイルし、生成されたelf実行形式ファイルにreadelf -Sを実行した。
[29] .data PROGBITS c076e000 66f000 04fda8 00 WA 0 0 4096
[45] .data.percpu PROGBITS c0844000 745000 008b94 00 WA 0 0 4096
普通の.dataセクションと属性は変わらないことがわかる。
ということはブートローダもしくはカーネルのブートシーケンスの極めて早い時点でCPUの数分だけこのセクションをロードするのだろうか?
早速.data.percpuでgrepをかけた。include/asm-generic/vmlinux.lds.hinclude/asm-generic/vmlinux.lds.hで.data.percpuを扱っている箇所があるようだ。
早速このファイルを見ると、以下の箇所が見つかる。
#define PERCPU_VADDR(vaddr, phdr) \ VMLINUX_SYMBOL(__per_cpu_load) = .; \ .data.percpu vaddr : AT(VMLINUX_SYMBOL(__per_cpu_load) \ - LOAD_OFFSET) { \ VMLINUX_SYMBOL(__per_cpu_start) = .; \ *(.data.percpu.first) \ *(.data.percpu.page_aligned) \ *(.data.percpu) \ *(.data.percpu.shared_aligned) \ VMLINUX_SYMBOL(__per_cpu_end) = .; \ } phdr \ . = VMLINUX_SYMBOL(__per_cpu_load) + SIZEOF(.data.percpu);
どう見ても、単なるリンカスクリプトである。
そして、.data.percpuの先頭を指すシンボルとして「__per_cpu_load」と「__per_cpu_start」の二つが定義されていることが分かる。
VMLINUX_SYMBOL(__per_cpu_load) = .; \ VMLINUX_SYMBOL(__per_cpu_start) = .; \
このシンボルをそれぞれgrepしてみる。すると、mm/percpu.cというソースコードにこれらのシンボルを使った処理が存在することが分かる。
__per_cpu_startについて結論を書くと、セクションのサイズを求めるためにmm/percpu.cで使われている。(詳細は割愛。)
今回は残る__per_cpu_loadをメインに追う。mm/percpu.c内で使われている箇所を探す。
#ifdef CONFIG_NEED_PER_CPU_PAGE_FIRST_CHUNK (中略) int __init pcpu_page_first_chunk(size_t reserved_size, pcpu_fc_alloc_fn_t alloc_fn, pcpu_fc_free_fn_t free_fn, pcpu_fc_populate_pte_fn_t populate_pte_fn) { (中略) for (unit = 0; unit < num_possible_cpus(); unit++) { (中略) /* copy static data */ memcpy((void *)unit_addr, __per_cpu_load, ai->static_size);
どうやら、セクションの中身をCPUの個数分だけコピーしているようだ。
確かにこうすることで、「CPUの数分.data.percpuセクションをロードしている」ように見える。
また、mm/percpu.c内のもう一ヶ所別のところにも同じような処理がある事が分かる。
int __init pcpu_embed_first_chunk(size_t reserved_size, ssize_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) { (中略) for (i = 0; i < gi->nr_units; i++, ptr += ai->unit_size) { (中略) if (gi->cpu_map[i] == NR_CPUS) { /* unused unit, free whole */ free_fn(ptr, ai->unit_size); continue; } /* copy and return the unused part */ memcpy(ptr, __per_cpu_load, ai->static_size); free_fn(ptr + size_sum, ai->unit_size - size_sum); }
まず、pcpu_page_first_chunk()から見る。
このifdefで使われているCONFIG_NEED_PER_CPU_PAGE_FIRST_CHUNKでgrepしてみると、
include/config/auto.conf:CONFIG_NEED_PER_CPU_PAGE_FIRST_CHUNK=y
include/generated/autoconf.h:#define CONFIG_NEED_PER_CPU_PAGE_FIRST_CHUNK 1
というのが見つかる。
ここから考えると、ifdef内のコードは生きている。
呼び出し元を調べてみると、
arch/x86/kernel/setup_percpu.c: rc=pcpu_page_first_chunk(PERCPU_FIRST_CHUNK_RESERVE,
arch/sparc/kernel/smp_64.c:
rc=pcpu_page_first_chunk(PERCPU_MODULE_RESERVE,
の2点だけ。ここではx86側のみ追うことにする。
arch/x86/kernel/setup_percpu.c内での呼び出し箇所を調べるとsetup_per_cpu_areas()という関数からコールされていることが分かる。
void __init setup_per_cpu_areas(void) { (中略) 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) rc = pcpu_page_first_chunk(PERCPU_FIRST_CHUNK_RESERVE, pcpu_fc_alloc, pcpu_fc_free, pcpup_populate_pte);
ここで。もう一つの関数pcpu_embed_first_chunk()についても見てみよう。
詳細は割愛するが、pcpu_embed_first_chunk()はmm/percpu.c内のsetup_per_cpu_areas()から呼ばれている。
先に見つけたarch/x86/kernel/setup_percpu.cと同じパターンで、両者ともsetup_per_cpu_areas()という関数から呼ばれているようだ。
呼び出し元を調べると、setup_per_cpu_areas()はinit/main.c内のstart_kernel()からコールされていることがわかる。もろにブートシーケンスの一部だ。
このことから、おおよそではあるが、以下の事柄が言えるように思われる。
CPUごとの変数を格納するために.data.percpuセクションがコンパイル時に用意される。このセクションはCPUの数だけメモリ上にコピーされる。これにより、あたかも各CPU分のセクションが個別にロードされたかの様に見える。
ざっくりすぎるが、何となくイメージできるところまでソースを見られたように思う。