はじめに
前回のLinuxカーネル4.1のSLUBアローケータ(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモと同じくドラフト版公開です。
カーネルのバージョンは4.1系です。
文書自体も完成版ではないし、markdownから手作業ではてなblogにコピペして修正してるので章立てとか変になってるところとかあるかもしれませんが気にしないでください。 一部は文書修正してます。
ユーザプロセス空間とカーネル空間
Linux x86_64では48bit(256TiB)のアドレス空間を使用できます。この256TiBの範囲のうち、128TiBをユーザープロセスが使用し、残りの128TiBをカーネルが使用します。32bitのカーネルではユーザプロセスに3GiB、カーネルに1GiBの割当でしたので、64bit環境では使用可能なメモリを使用量が大幅に増えました。 Linuxではこの256TiBのアドレス空間をプロセスとカーネルが使用しますが、0〜0xffff800000000000までのユーザプロセス空間はプロセスごとに割り当てられ、後半のカーネル空間に関しては常に1つとなり、「図_ユーザプロセス空間とカーネル空間」のようになります。
図_ユーザプロセス空間とカーネル空間
この256TiBのメモリはアドレスの範囲によって使用目的が決まっています。その内訳を「表_メモリマップ」に示します。
x86_64のメモリレイアウト
開始アドレス | 終了アドレス | サイズ | 内容 |
---|---|---|---|
0x0000000000000000 | 0x00007fffffffffff | 128TiB | ユーザプロセス空間 |
0xffff800000000000 | 0xffff87ffffffffff | 8TiB | ハイパーバイザー向けに予約 |
0xffff880000000000 | 0xffffc7ffffffffff | 64TiB | ダイレクトマップ領域 |
0xffffc80000000000 | 0xffffc8ffffffffff | 1TiB | 未使用 |
0xffffc90000000000 | 0xffffe8ffffffffff | 32TiB | vmalloc/ioremapが使用 |
0xffffe90000000000 | 0xffffe9ffffffffff | 1TiB | 未使用 |
0xffffea0000000000 | 0xffffeaffffffffff | 1TiB | 仮想メモリマップ |
0xffffeb0000000000 | 0xffffebffffffffff | 1023GiB | 未使用 |
0xffffec0000000000 | 0xfffffc0000000000 | 16TiB | kasan shadow memory |
0xfffffc0000000000 | 0xfffffeffffffffff | 3072GiB | 未使用 |
0xffffff0000000000 | 0xffffff7fffffffff | 512GiB | スタック領域 |
0xffffff8000000000 | 0xffffffff7fffffff | 509GiB | 未使用 |
0xffffffff80000000 | 0xffffffffa0000000 | 512MiB | カーネルテキストマッピング領域。物理メモリ0~512MiB |
0xffffffffa0000000 | 0xffffffffff5fffff | 1525MiB | カーネルモジュールマッピング領域 |
0xffffffffff600000 | 0xffffffffffdfffff | 8MiB | vsyscallsで使用 |
0xffffffffffe00000 | 0xffffffffffffffff | 2MiB | 未使用 |
表_メモリマップ
この内訳はDocumentation/x86/x86_64/mm.txtにて確認することができます。 ユーザープロセス空間は128TiBをフルに使用しますが、カーネル空間の場合、ある特定の範囲を使用目的毎に分けています。128TiB全てが使用されているわけではなく、現在は未使用の領域として空いているところもあります。
また、EFIを使用するシステムでは図_メモリマップのように0xffffffef00000000から0xffffffff00000000の範囲をEFIのランタイムサービスにマッピングします。
図_メモリマップ
ダイレクトマップ(ストレートマップ)領域
カーネルは全ての物理メモリをこの領域にマッピングします。x86_32アーキテクチャではアーキテクチャの制限として、物理アドレスを直接マッピングできるのは896MiBまでで、それ以上のアドレスを使用するためにHIGHMEM領域を使用していましたが、x86_64アーキテクチャではHIGHMEM領域が不要になり、ダイレクトマップ領域だけを使用します。
ダイレクトマップ領域の開始アドレスはPAGE_OFFSETとして定義されていています。
arch/x86/include/asm/page_types.h #define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET) arch/x86/include/asm/page_64_types.h #define __PAGE_OFFSET _AC(0xffff880000000000, UL)
図_PAGE_OFFSETの定義
物理アドレスからリニアアドレスへの変換は__vaマクロを使用します。この場合は物理アドレスに対してPAGE_OFFSETを足すことでリニアアドレスを得ることができます。
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
リニアアドレスから物理アドレスへの変換は__pa関数マクロです。このマクロの実体は__phys_addr_nodebug関数です。x86_32ではアドレスからPAGE_OFFSETを引くだけでアドレスを計算できたのですが、x86_64ではカーネルテキストマッピング領域の開始アドレス 0xffffffff80000000(__START_KERNEL_map)を基準としてアドレスの計算を行います。この関数で使用するphys_baseは通常0となっています。アドレスの変換は、「図_リニアアドレスから物理アドレスへの変換」のように行います
図_リニアアドレスから物理アドレスへの変換
vmalloc/ioremap領域
vmalloc関数やioremap関数などでメモリを確保する場合、アドレスをこの領域から割り当てます。vmalloc関数については別記事
Linuxカーネル4.1のvmalloc()(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ
にて説明します。この領域で使用するページフレームは物理的に連続していませんが、論理的に連続します。
仮想メモリマップ
この領域は主にページフレーム番号とpage構造体の相互変換に使用します。ページフレーム番号からページ構造体を取得するには__pfn_to_pageマクロ、page構造体からページフレーム番号の取得には__page_to_pfnマクロを使用します。
/* memmap is virtually contiguous. */ #define __pfn_to_page(pfn) (vmemmap + (pfn)) #define __page_to_pfn(page) (unsigned long)((page) - vmemmap)
図_ページフレーム番号とpage構造体の変換インターフェース
これらの変換操作にはグローバル変数のvmemmapに設定されている仮想メモリマップ領域の開始アドレスを基準として、このアドレスに対してページフレーム番号を足すことで、該当のpage構造体を取得、また、page構造体のアドレスからvmemmapを減算することで、ページフレーム番号の取得を行います。
%esp fixup stack
このスタックはPAGE_SIZEが4096の場合、各CPUに対して64個のスタックを読み込み専用として作成します。このスタックはministacksと呼ばれます。 ministacksはStack-Segment Fault(SS)からiret命令により、カーネルからユーザランドへ制御が戻る時に、iretで使用していたスタックフレームをLDTにコピーしてからユーザランドへ戻ります。 この後、General Faultなどが発生した場合はInterrupt Stack Tableから専用のスタックを使用します。 このような読み込み用のスタックを使用するのはセキュリティ上の理由です(注1)。iret命令は16bitのセグメントを元に戻しますが、%espの16〜31bitはカーネルのスタックを指しているため、カーネルの情報が漏れてしまいます。この状況に対応するため、専用のスタックを用意してiret命令から戻るようになっています。
カーネルテキスト領域
物理メモリのアドレス0〜512MiBをこの範囲にマッピングします。この領域の開始アドレスは__START_KERNEL_mapとして定義されています。この領域にはカーネルをマッピングするため、このアドレス範囲に対して操作を行うことはありませんが、先に見たように__paマクロによる物理アドレスの取得など、アドレスを扱う場合に__START_KERNEL_mapを利用することがあります。
モジュールマッピング領域
カーネルモジュールをロードする領域です。この領域はカーネルテキストマッピング領域の直後に位置します。
#define MODULES_VADDR (__START_KERNEL_map + KERNEL_IMAGE_SIZE) #define MODULES_END _AC(0xffffffffff000000, UL) #define MODULES_LEN (MODULES_END - MODULES_VADDR)
図_モジュールマッピング領域の定義
カーネルモジュールをロードする時にmodule_alloc関数が__vmalloc_node_range関数を用いて、モジュールのために確保するメモリ領域をMODULES_VADDRからのMODULES_ENDまでの範囲を指定します。
vsyscall
vsyscallのために割り当てられた領域で、開始アドレスはVSYSCALL_ADDRとして以下のように定義されています。値は0xffffffffff600000となります。
10 #define VSYSCALL_ADDR (-10UL << 20)
vsyscall領域は/proc/pid/mapsファイルにて、1ページ分がマッピングされていることを確認できます。
$ cat /proc/self/maps ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
固定マップ領域
機能によって、特定のアドレスにメモリを割り当てて使用したいという要求があり、これを実現するのが固定マップ領域です。固定マップ領域は0xffffffffff7ff000からの領域でアドレスの低位に向かって使用します。 固定マップ領域で使用する機能目的ごとのアドレスの定義はarch/x86/include/asm/fixmap.hで、enumのfixed_addressesで各領域ごとにインデックスが決められています。
固定マップ領域のインデックス(fixed_addresses)からリニアアドレスへの変換は__fix_to_virtマクロ、リニアアドレスからインデックス番号への変換は__virt_to_fixマクロで行います。
20 #define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT)) 21 #define __virt_to_fix(x) ((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT)
FIXDADDR_TOPマクロは以下のように定義されています。VSYSCALL_ADDRはvsyscall領域の開始アドレスで、x86_64では0xffffffffff600000となります。
#define FIXADDR_TOP (round_up(VSYSCALL_ADDR + PAGE_SIZE, 1<<PMD_SHIFT) - \ PAGE_SIZE)
固定マップ領域の確保はset_fixmap関数にて行います。これはマクロで__set_fixmap関数を呼び出します。この関数の実装はCPUアーキテクチャに依存します。x86_64アーキテクチャでは__native_set_fixmap関数が使われます。固定マップ領域の設定は先に説明した__fix_to_virtマクロによるリニアアドレスへの変換を行い、変換結果のアドレスをPTEテーブルへの登録を行います。
例えば、固定マップ領域のインデックス「VSYSCALL_PAGE」は0x1ffで、このインデックスに対するアドレスは0xffffffffff600000となります。この値は先に見たようにvsyscall領域のアドレスです。よって、vsyscall領域は固定マップ領域の仕組みを利用して、アドレスを固定して確保していることがわかります。 固定マップ領域のインデックス範囲は0x1ff-0x600です。アドレスに変換すると0xffffffffff600000〜0xffffffffff1ff000となります。
- 作者: Andreas Zeller,中田秀基,今田昌宏,大岩尚宏,竹田香苗,宮原久美子,宗形紗織
- 出版社/メーカー: オライリージャパン
- 発売日: 2012/12/22
- メディア: 大型本
- 購入: 4人 クリック: 184回
- この商品を含むブログ (8件) を見る