Linuxの仮想 - 物理アドレスと、カーネル空間の概要

  • 30
    Like
  • 0
    Comment
More than 1 year has passed since last update.

あらまし

cgroupsを勉強しようと、折角なので資源管理の一つであるLinuxの仮想記憶周りを前回読んでみました。
その最後に

しかし、物理アドレスをLinuxではどうやって扱っているのでしょうか?
例えば、仮想アドレスから物理アドレスを得る、ページ構造体からそれが指している物理アドレスを得る方法が今ひとつ見えません。

と書きました。
そこで、今回、コードをうろつきつつ、この疑問にぶつかってみることにしました。
すでにcgroupsがどこかに行っていますが、最後にはわかると信じています(笑)。

なお、以下3点追記です。
(1)CPUアーキテクチャに依存したコードを見る場合、arch/x86の下の実装を参照します。
(2)流れがわかりにくい箇所もあります。が、ソースを読んで迷った過程をあえて記録に残したいと考えたからです。ご了解ください。
(3)ソースは手元にある3.15.6で、最新ではありませんが、きっと大きく違わないと思います。

マクロ発見か?

mmの下のソースを見ると、仮想アドレスからページ構造体のアドレスを得ると思われるマクロを見つけました。
実装元は以下の箇所です。

arch/x86/include/asm/page.h
/*
 * virt_to_page(kaddr) returns a valid pointer if and only if
 * virt_addr_valid(kaddr) returns true.
 */
#define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)

また、仮想アドレスから物理アドレスを得ると思われる関数の実装は以下の通りです。

include/asm-generic/io.h
#ifndef virt_to_phys
static inline unsigned long virt_to_phys(volatile void *address)
{
  return __pa((unsigned long)address);
}
/* 略 */
#endif

virt_to_page()とvirt_to_phys()の両方で使われている__pa()の実装を知ることで仮想アドレスと物理アドレスの関係が見えてきそうです。

__pa()の実装を追う

__pa()はx86の場合、以下の箇所にあります。

arch/x86/include/asm/page.h
#define __pa(x)              __phys_addr((unsigned long)(x))
arch/x86/include/asm/page_32.h
#ifndef __ASSEMBLY__

#define __phys_addr_nodebug(x)  ((x) - PAGE_OFFSET)
#ifdef CONFIG_DEBUG_VIRTUAL
extern unsigned long __phys_addr(unsigned long);
#else
#define __phys_addr(x)    __phys_addr_nodebug(x)
#endif

ここでは、CONFIG_DEBUG_VIRTUALが無効という前提としますが、その場合、なんともシンプルですね。単純に受け取った仮想アドレスからPAGE_OFFSETを引くだけです。
それでは、PAGE_OFFSETはどんな値でしょうか。

arch/x86/include/asm/page_types.h
#define PAGE_OFFSET          ((unsigned long)__PAGE_OFFSET)
arch/x86/include/asm/page_32_types.h
/*
 * This handles the memory map.
 *
 * A __PAGE_OFFSET of 0xC0000000 means that the kernel has
 * a virtual address space of one gigabyte, which limits the
 * amount of physical memory you can use to about 950MB.
 *
 * If you want more physical memory than this then see the CONFIG_HIGHMEM4G
 * and CONFIG_HIGHMEM64G options in the kernel configuration.
 */
#define __PAGE_OFFSET   _AC(CONFIG_PAGE_OFFSET, UL)

#define __START_KERNEL_map  __PAGE_OFFSET

でも、CONFIG_PAGE_OFFSETという定義はx86には見つけることができません...。
うーん、デフォルト値は0xc0000000ということなら...この値でgrepしてみましょう。

そうすると、arch/x86/KConfigというファイルが関係しているように思われます。

KConfigについて

Documentationの下を調べると以下のようなテキストが見つかります。

Documentation/kbuild/kconfig.txt
CONFIG_
--------------------------------------------------
If you set CONFIG_ in the environment, Kconfig will prefix all symbols
with its value when saving the configuration, instead of using the default,
"CONFIG_".

この規則に従い、PAGE_OFFSETを探すと...ありました。
正確な記法は知りませんが、以下からデフォルト値は0xc0000000と推定できます。
これまでの経験から、カーネルの仮想アドレス空間を表していると思われます。

arch/x86/Kconfig
config PAGE_OFFSET
  hex
  default 0xB0000000 if VMSPLIT_3G_OPT
  default 0x80000000 if VMSPLIT_2G
  default 0x78000000 if VMSPLIT_2G_OPT
  default 0x40000000 if VMSPLIT_1G
  default 0xC0000000
  depends on X86_32

virt_to_page()に戻る

さて、virt_to_page()のところに戻りましょう。

arch/x86/include/asm/page.h
/*
 * virt_to_page(kaddr) returns a valid pointer if and only if
 * virt_addr_valid(kaddr) returns true.
 */

つまり、virt_to_page()は特定の仮想アドレス空間に属する仮想アドレスのみ捌くことができるということです。
では、コメントにあるvirt_addr_valid()を見てみましょう。

include/linux/page.h
#define virt_addr_valid(kaddr)  __virt_addr_valid((unsigned long) (kaddr))

続いて、__virt_addr_valid()の実装は以下の通りです。

mm/physaddr.c
bool __virt_addr_valid(unsigned long x)
{
  if (x < PAGE_OFFSET)
    return false;
  if (__vmalloc_start_set && is_vmalloc_addr((void *) x))
    return false;
  if (x >= FIXADDR_START)
    return false;
  return pfn_valid((x - PAGE_OFFSET) >> PAGE_SHIFT);
}
EXPORT_SYMBOL(__virt_addr_valid);

pfn_validの戻り値でtrueかどうかが決まりそうです。

pfn_valid()について

続けてpfn_valid()を確認します。

include/asm/page_32.h
#ifdef CONFIG_FLATMEM
#define pfn_valid(pfn)    ((pfn) < max_mapnr)
#endif /* CONFIG_FLATMEM */

受け取った値(物理ページ番号)がmax_mapnr未満か確認しています。max_mapnrは以下の通りです。

mm/init_32.c
max_mapnr = IS_ENABLED(CONFIG_HIGHMEM) ? highend_pfn : max_low_pfn;

highend_pfnは以下の通りです。

arch/x86/mm/init_32.c
#ifdef CONFIG_HIGHMEM
  highstart_pfn = highend_pfn = max_pfn;

max_pfnは文字通り、max Physical Frame Numberで最大物理ページ数を指します。
max_pfnについては、以下のとおりです。

arch/x86/kernel/setup.c
    max_pfn = e820_end_of_ram_pfn();

e820の説明だけ

e820とは、BIOS経由でメモリマップを取得する機能です。
INT 15hで、AXレジスタに0xe820を渡すことから、この名称となっています。

要するにpfn_valid()は

単に渡された物理ページ数が範囲内かどうかを調べるためのマクロでした。けれど、思わぬところで、深みにはまりそうになりました。
これが仮想記憶のコードを読む醍醐味(笑)です。

再度、__virt_addr_valid()へ戻る。

mm/physaddr.c
bool __virt_addr_valid(unsigned long x)
{
/* 略 */
  if (__vmalloc_start_set && is_vmalloc_addr((void *) x))
    return false;

is_vmalloc_addr()が気になります。
この関数は以下のとおりです。

include/linux/mm.h
static inline int is_vmalloc_addr(const void *x)
{
#ifdef CONFIG_MMU
  unsigned long addr = (unsigned long)x;

  return addr >= VMALLOC_START && addr < VMALLOC_END;
#else
  return 0;
#endif
}

カーネル仮想アドレス空間の概要

ところで、カーネル仮想アドレス空間はどうなっているのでしょうか。
x86の場合、以下のコメントが参考になると思います。
(おそらくは、コメントの下のほうに位置する空間のアドレスが若いアドレスになるので、直感的にわかりにくいですが...)

arch/x86/include/asm/highmem.h
/*
 * Ordering is:
 *
 * FIXADDR_TOP
 *          fixed_addresses
 * FIXADDR_START
 *          temp fixed addresses
 * FIXADDR_BOOT_START
 *          Persistent kmap area
 * PKMAP_BASE
 * VMALLOC_END
 *          Vmalloc area
 * VMALLOC_START
 * high_memory
 */

この中に、先のis_vmalloc_addr()に出てきた、VMALLOC_STARTなどが出てきます。
どうやらカーネル空間のうち、PAGE_OFFSETからVMALLOC_STARTの間のメモリだけは、仮想的にも物理的にも連続した値だということが言えそうです。(仮想アドレスを物理アドレスに変換するために、単に引き算だけだったことを思い出してください。)

色々調べると、「Linuxカーネル解読室」に以下のような記述があります。

ストレートマップ領域
カーネルはすべての実メモリをアクセスできる必要があります。(略)ただし、カーネル空間が1GBしかない関係上、ストレートマップできるのは、896MBまでとなっています。(略)ストレートマッピングされていない最初のアドレス(ストレートマップの終端)は、high_memoryという外部変数で表されています。(略)
(Linuxカーネル解読室 P187より一部抜粋)

これをまとめると、以下のようになりそうです。

PAGE_OFFSET    +-----------------+
               |   物理的に連続   |
               |   物理的に連続   |
high_memory    +-----------------+
VMALLOC_START  |   物理的に連続   |
               |   していないと   |
               |   思われる      |
max_mapnr      +-----------------+
    *
ページサイズ

仮想アドレスと物理アドレスの関係は、カーネル空間の一部のみはわかりました。
しかし、それ以外の仮想アドレス空間に対する物理アドレスの取り扱いが見えてきません。
次回はそこを追ってみる予定です。