.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分のセクションが個別にロードされたかの様に見える。

ざっくりすぎるが、何となくイメージできるところまでソースを見られたように思う。