以下の議論は 32 ビット ARM Linux に基づいており、カーネル ソース コードのバージョンは 3.9
です。最初のページ テーブルを設定し (関数によって後で上書きされますpaging_init
)、 MMUで。
カーネルがブートローダーによって最初に起動されるとき、アセンブリ関数stext
(arch\arm\kernel\head.s 内) が最初に実行される関数です。この時点では、MMU はまだオンになっていないことに注意してください。
特に、この関数によって実行される 2 つのインポート ジョブstext
は次のとおりです。
- 最初のページ テーブルを作成します (関数によって後で上書きされます
paging_init
) 。
- MMUをオンにする
- カーネル初期化コードの C 部分にジャンプして続行
質問を掘り下げる前に、次のことを知っておくと有益です。
- MMUがオンになる前は、CPUが発行するすべてのアドレスは物理アドレスです
- MMU がオンになった後、CPU によって発行されるすべてのアドレスは仮想アドレスです。
- MMU をオンにする前に、適切なページ テーブルを設定する必要があります。
- 慣例により、Linux カーネルは仮想アドレスの上位 1GB 部分を使用し、ユーザー ランドは下位 3GB 部分を使用します。
ここでトリッキーな部分:
最初のトリック: 位置に依存しないコードを使用します。アセンブリ関数 stext はPAGE_OFFSET + TEXT_OFFSET
、仮想アドレスであるアドレス " " (0xCxxxxxxx) にリンクされていますが、MMU はまだオンになっていないため、アセンブリ関数 stext が実行されている実際のアドレスは " PHYS_OFFSET + TEXT_OFFSET
" です (実際の値は、実際のアドレスによって異なります)。これは物理アドレスです。
関数のプログラムは、stext
0xCxxxxxxx のようなアドレスで実行されていると「考え」ていますが、実際にはアドレス (0x00000000 + some_offeset) で実行されています (ハードウェアが RAM の開始点として 0x00000000 を構成しているとします)。そのため、MMU を有効にする前に、アセンブリ コードを慎重に記述して、実行手順中に問題が発生しないようにする必要があります。実際には、位置独立コード (PIC) と呼ばれる手法が使用されています。
上記をさらに説明するために、いくつかのアセンブリ コード スニペットを抜粋します。
ldr r13, =__mmap_switched @ address to jump to after MMU has been enabled
b __enable_mmu @ jump to function "__enable_mmu" to turn on MMU
上記の「ldr」命令は、「関数__mmap_switchedの(仮想)アドレスを取得し、r13に入れる」ことを意味する疑似命令であることに注意してください。
そして、関数 __enable_mmu は関数 __turn_mmu_on を呼び出します: (関数 __turn_mmu_on からいくつかの命令を削除したことに注意してください。これらは関数にとって重要な命令ですが、私たちの関心事ではありません)
ENTRY(__turn_mmu_on)
mcr p15, 0, r0, c1, c0, 0 @ write control reg to enable MMU====> This is where MMU is turned on, after this instruction, every address issued by CPU is "virtual address" which will be translated by MMU
mov r3, r13 @ r13 stores the (virtual) address to jump to after MMU has been enabled, which is (0xC0000000 + some_offset)
mov pc, r3 @ a long jump
ENDPROC(__turn_mmu_on)
2 番目のトリック: MMU をオンにする前に初期ページ テーブルを設定するときの同一のマッピング。具体的には、カーネル コードが実行されている同じアドレス範囲が 2 回マップされます。
- 最初のマッピングは、予想どおり、アドレス範囲 0x00000000 (このアドレスはハードウェア構成によって異なります) から (0x00000000 + オフセット) までを 0xCxxxxxxx から (0xCxxxxxxx + オフセット) までマッピングします。
- 興味深いことに、2 番目のマッピングは、アドレス範囲 0x00000000 から (0x00000000 + オフセット) をそれ自体にマップします (つまり、0x00000000 --> (0x00000000 + オフセット))。
なぜそれをするのですか?MMU がオンになる前は、CPU によって発行されるすべてのアドレスは物理アドレス (0x00000000 から始まる) であり、MMU がオンになった後は、CPU によって発行されるすべてのアドレスは仮想アドレス (0xC0000000 から始まる) であることに注意してください。
ARM はパイプライン構造であるため、MMU がオンになった瞬間に、MMU がオンになる前に CPU によって生成された (物理) アドレスを使用している命令がまだ ARM のパイプラインに存在します。これらの指示が大げさになるのを避けるには、同じマッピングを設定して対応する必要があります。
質問に戻ります。
- この時点で、ページ テーブルをオンにした後、カーネル スペースはまだ 1GB (0xC0000000 から 0xFFFFFFFF まで) ですか?
A: MMU をオンにするという意味だと思います。答えはイエスです。カーネル空間は 1GB です (実際には 0xC0000000 より下の数メガバイトも占有しますが、それは私たちの関心事ではありません)
- また、カーネル プロセスのページ テーブルでは、0xC0000000 ~ 0xFFFFFFFF の範囲のページ テーブル エントリ (PTE) のみがマップされます。カーネル コードがそこにジャンプしないため、PTE がこの範囲外にある場合はマップされませんか?
A: この質問への回答は非常に複雑ですが、特定のカーネル構成に関する多くの詳細が含まれているためです。
この質問に完全に答えるには、カーネル ソース コードの初期ページ テーブルを設定する部分 (アセンブリ関数__create_page_tables
) と最終ページ テーブルを設定する関数 (C 関数 paging_init) を読む必要があります。
簡単に言うと、ARM には 2 つのレベルのページ テーブルがあり、最初のページ テーブルは PGD で、16KB を占有します。カーネルは、初期化プロセス中に最初にこの PGD をゼロにし、アセンブリ関数で初期マッピングを行います__create_page_tables
。function__create_page_tables
では、アドレス空間のごく一部のみがマップされます。
その後、最終的なページテーブルが関数に設定されますpaging_init
であり、この関数では、アドレス空間のかなりの部分がマップされます。512M RAM しかない場合、最も一般的な構成では、この 512M-RAM はカーネル コード セクションごとにマッピングされます (1 セクションは 1MB)。RAM が非常に大きい場合 (たとえば 2GB)、RAM の一部のみが直接マップされます。(設問2については詳細が多すぎるのでここで止めます)
- ページテーブルをオンにする前後のマッピングアドレスは同じですか?
A: この質問については、「第 2 のトリック: MMU を有効にする前に初期ページ テーブルを設定する際の同一マッピング」の説明で既に回答済みだと思います。
4 . カーネル空間のページ テーブルはグローバルであり、ユーザー プロセスを含むシステム内のすべてのプロセスで共有されますか?
A: はい、いいえ。はい、すべてのプロセスがカーネル ページ テーブル (上位 1GB 部分) の同じコピー (コンテンツ) を共有するためです。いいえ、各プロセスは独自の 16KB メモリを使用してカーネル ページ テーブルを格納するためです (ただし、上位 1GB 部分のページ テーブルの内容はすべてのプロセスで同じです)。
5. このメカニズムは、x86 32 ビットと ARM で同じですか?
異なるアーキテクチャは異なるメカニズムを使用します