目次


Linuxメモリー・モデルの探究

Linux設計を理解するための第1ステップ

Comments

Linuxで使われているメモリー・モデルを理解することは、Linuxの設計と実装について把握するための第1ステップです。このため、ここではLinuxメモリー・モデルと管理について概説します。

Linuxでは統一型のアプローチを使って一連のプリミティブやシステム呼び出しを定義し、スーパーバイザー・モードで動作する複数のモジュールに、プロセス管理、並列処理、メモリー管理などのオペレーティング・システム・サービスを実装しています。Linuxは互換性の目的でセグメント制御ユニット・モデルをシンボリック表現として保守しますが、このモデルは最小レベルで使用されます。

メモリー管理に関する主な問題は次のとおりです。

  • 仮想メモリー管理(アプリケーション・メモリー要求と物理メモリー間の論理層)
  • 物理メモリー管理
  • カーネル仮想メモリー管理/カーネル・メモリー・アロケーター(メモリー要求に対応しようとするコンポーネント)。要求は、カーネル内またはユーザーから出すことができます。
  • 仮想アドレス・スペース管理
  • スワッピングおよびキャッシング

このドキュメントは、オペレーティング・システム内のメモリー管理の見地からLinuxの内部を理解するのに役立ちます。ここで説明する内容は次のとおりです。

  • セグメント制御ユニット・モデル(一般およびLinux専用)
  • ページング・モデル(一般およびLinux専用)
  • メモリー・ゾーンの物理的詳細

このドキュメントでは、Linuxカーネルによるメモリーの管理方法については詳述しませんが、詳しく学習するための枠組みとして、メモリー・モデル全体とそのアドレッシング方法についての情報を記載します。また、ここではx86アーキテクチャーについて扱いますが、このドキュメントの内容を他のハードウェア実装に応用することが可能です。

x86メモリー・アーキテクチャー

x86アーキテクチャーでは、メモリーが次の3種類のアドレスに分割されます。

  • 論理アドレスはストレージ・ロケーション・アドレスであり、物理ロケーションに直接関連する場合とそうでない場合があります。論理アドレスは一般に、コントローラーから情報を要求する場合に使われます。
  • リニア・アドレス(またはフラット・アドレス・スペース)は、0からアドレッシングされるメモリーです。後続の各バイトは、次のシーケンス番号(0、1、2、3など)でメモリーの最後まで参照されます。これは、Intel以外のほとんどのCPUで使われているメモリーのアドレッシング方法です。Intel®のアーキテクチャーではセグメント化されたアドレス・スペースを採用しており、このスペースではメモリーが64KBのセグメントに分割されます。また、セグメント・レジスターは常に、現在アドレッシングされているセグメントのベースを指しています。このアーキテクチャーの32ビット・モードはフラット・アドレス・スペースと考えられていますが、この場合もセグメントが使われます。
  • 物理アドレスは、物理アドレス・バス上のビットで表現されるアドレスです。物理アドレスが論理アドレスと異なる場合は、メモリー管理ユニットによって論理アドレスが物理アドレスに変換されます。

CPUは、セグメント・ユニットとページング・ユニットという2つのユニットを使って、論理アドレスを物理アドレスに変換します。

図1.2つのユニットによるアドレス・スペースの変換
Two units convert address spaces
Two units convert address spaces

一般的なセグメント制御ユニット・モデル

セグメンテーション・モデルの背景にある基本的な概念は、一連のセグメントを使ってメモリーを管理するということです。基本的に、各セグメントは専用のアドレス・スペースとなります。1つのセグメントは、次の2つの要素で構成されます。

  • 一部の物理メモリー・ロケーションのアドレスを含むベース・アドレス
  • セグメントの長さを指定する長さ値

セグメント化されたアドレスも、セグメント・セレクターとセグメントに対するオフセットの2つの要素で構成されます。セグメント・セレクターは使用するセグメント(つまりベース・アドレスと長さ値)を指定するのに対し、オフセット要素は実際のメモリー・アクセス用にベース・アドレスからのオフセットを指定します。実際のメモリー・ロケーションの物理アドレスは、オフセット値とベース・アドレス値の合計になります。オフセットがセグメントの長さを超える場合は、システムにより保護違反が出されます。

この表現をまとめると次のようになります。

Segmented Unit is represented as -> Segment: Offset model
can also be represented as -> Segment Identifier: Offset

各セグメントは、セグメントIDまたはセグメント・セレクターと呼ばれる16ビット・フィールドです。x86ハードウェアはセグメント・レジスターと呼ばれるいくつかのプログラマブル・レジスターで構成され、レジスターにこれらのセグメント・セレクターが保持されます。レジスターは、cs (コード・セグメント)、ds (データ・セグメント)、ss (スタック・セグメント)の3つです。各セグメントIDは、64ビット(8バイト)のセグメント・ディスクリプターで表されるセグメントを識別します。これらのセグメント・ディスクリプターはGDT (グローバル・ディスクリプター・テーブル)に格納されます。また、LDT (ローカル・ディスクリプター・テーブル)に格納することも可能です。

図2.セグメント・ディスクリプターとセグメント・レジスターの相互関係
Interplay of segment descriptors and segment registers
Interplay of segment descriptors and segment registers

セグメント・セレクターがセグメント・レジスターにロードされるたびに、対応するセグメント・ディスクリプターがメモリーから一致する非プログラマブルCPUレジスターにロードされます。各セグメント・ディスクリプターは8バイト長で、メモリー内の1つのセグメントを表します。これらはLDTまたはGDTに格納されます。セグメント・ディスクリプターのエントリーには、Baseフィールドで表される関連セグメント内の第1バイトを指すポインターと、メモリー内のセグメントのサイズを表す20ビット値(Limitフィールド)の両方が含まれています。

その他いくつかのフィールドには、権限レベルやセグメント・タイプ(csまたはds)などの特殊属性が含まれています。セグメント・タイプは4ビットのTypeフィールドで表します。

ここでは非プログラマブル・レジスターを使用しているため、論理アドレスからリニア・アドレスへの変換は実行されますが、GDTとLDTは参照されません。これにより、メモリーの変換速度が向上します。

セグメント・セレクターには次のものが含まれています。

  • GDTまたはLDTに含まれる、対応するセグメント・ディスクリプター・エントリーを識別する13ビットのインデックス。
  • TI (テーブル・インジケーター)フラグ。このフラグの値が0の場合は、セグメント・ディスクリプターがGDTに含まれていることが指定され、値が1の場合は、セグメント・ディスクリプターがLDTに含まれていることが指定されます。
  • RPL (要求権限レベル)は、対応するセグメント・セレクターがセグメント・レジスターにロードされるときの、CPUの現行の権限レベルを定義します。

セグメント・ディスクリプターは8バイト長なので、GDTまたはLDT内の相対アドレスは、セグメント・セレクターの最上位13ビット×8で求めることができます。たとえば、GDTがアドレス0x00020000に格納されていて、セグメント・セレクターで指定されたインデックスが2の場合、対応するセグメント・ディスクリプターのアドレスは(2*8) + 0x00020000となります。1つのGDTに格納できるセグメント・ディスクリプターの合計数は(2^13 - 1) = 8191となります。

図3は、論理アドレスからリニア・アドレスを取得する方法を図で示したものです。

図3.論理アドレスからリニア・アドレスを取得する方法
Obtaining a linear address from a logical address
Obtaining a linear address from a logical address

これとLinuxの違いは?

Linuxのセグメント制御ユニット

Linuxの場合、このモデルは多少異なります。すでに述べたように、Linuxでは、限られた方法で(ほとんどは互換性の目的で)セグメンテーション・モデルを使用します。

Linuxでは、すべてのセグメント・レジスターが同じ範囲のセグメント・アドレスを指します。言い換えれば、それぞれが同一セットのリニア・アドレスを使用することになります。これにより、Linuxは限られた数のセグメント・ディスクリプターを使用できるようになり、したがってすべてのディスクリプターをGDT内に保持できるようになります。このモデルには、次の2つの利点があります。

  • すべてのプロセスが同じセグメント・レジスター値を使用する場合(同一セットのリニア・アドレスを共有する場合)に、メモリー管理が簡素化されます。
  • ほとんどのアーキテクチャーとの移植性が実現されます。この限定方法でのセグメンテーションをサポートするRISCプロセッサーもあります。

図4に、この違いを示します。

図4.Linuxではセグメント・レジスターが同一セットのアドレスを指す
In Linux segment registers point to the same set of addresses
In Linux segment registers point to the same set of addresses

セグメント・ディスクリプター

Linuxでは、次のセグメント・ディスクリプターを使用します。

  • カーネル・コード・セグメント
  • カーネル・データ・セグメント
  • ユーザー・コード・セグメント
  • ユーザー・データ・セグメント
  • TSSセグメント
  • デフォルトのLDTセグメント

このそれぞれについて、詳しく見てみましょう。

GDT内のカーネル・コード・セグメント・ディスクリプターは、次の値を持ちます。

  • Base = 0x00000000
  • Limit = 0xffffffff (2^32 -1) = 4GB
  • G (細分性フラグ) = セグメント・サイズをページ単位で表す場合は1
  • S = 通常のコードまたはデータ・セグメントの場合は1
  • Type = 読み取りおよび実行可能なコード・セグメントの場合は0xa
  • DPL値 = カーネル・モードの場合は0

このセグメントに関連付けられたリニア・アドレスは4 GBです。S =1およびType = 0xaの場合はコード・セグメントを指します。セレクターはcsレジスターに格納されます。対応するセグメント・セレクターにアクセスするときに使われるLinuxのマクロは_KERNEL_CSです。

これに対するカーネル・データ・セグメント・ディスクリプターの値はカーネル・コード・セグメントと類似していますが、ファイルのType値が2に設定される点が異なります。これは、セグメントがデータ・セグメントであり、セレクターがdsレジスターに格納されることを意味します。対応するセグメント・セレクターにアクセスするときに使われるLinuxのマクロは_KERNEL_DSです。

ユーザー・コード・セグメントは、ユーザー・モードになっているすべてのプロセスで共有されます。GDTに格納される、対応するセグメント・ディスクリプターの値は次のとおりです。

  • Base = 0x00000000
  • Limit = 0xffffffff
  • G = 1
  • S = 1
  • Type = 読み取りおよび実行可能なコード・セグメントの場合は0xa
  • DPL = ユーザー・モードの場合は3

このセグメント・セレクターにアクセスする場合に使われるLinuxのマクロは_USER_CSです。

ユーザー・データのセグメント・ディスクリプターでは、Typeフィールドのみが異なります。このフィールドは2に設定され、読み取りおよび書き込み可能なデータ・セグメントを定義します。このセグメント・セレクターにアクセスする場合に使われるLinuxのマクロは_USER_DSです。

GDTには、これらのセグメント・ディスクリプターに加え、作成されるプロセスごとにTSSセグメントとLDTセグメントの2つのセグメント・ディスクリプターが含まれています。

各TSSセグメント・ディスクリプターは異なるプロセスを指します。TSSには、各CPUのハードウェア・コンテキスト情報が保持されます。この情報は、コンテキスト切り替えを有効にする際に役立ちます。たとえば、UモードからKモードへ切り替える際に、x86 CPUはTSSからカーネル・モード・スタックのアドレスを取得します。

各プロセスには、GDTに格納された対応するプロセスについての専用のTSSディスクリプターがあります。ディスクリプターの値は次のとおりです。

  • Base = &tss (対応するプロセス・ディスクリプター(&tss_structなど)のTSSフィールドの値)。これはLinuxカーネルのschedule.hファイルで定義されます。
  • Limit = 0xeb (TSSセグメントは236バイト長)
  • Type = 9または11
  • DPL = 0。ユーザー・モードではTSSにアクセスしません。Gフラグがクリアされます。

すべてのプロセスはデフォルトのLDTセグメントを共有します。LDTセグメントには、デフォルトによりNULLのセグメント・ディスクリプターが含まれています。このデフォルトのLDT セグメント・ディスクリプターはGDTに格納されます。Linuxによって生成されるLDTのサイズは24バイトです。デフォルトにより、次の3つのエントリーが常駐します。

LDT[0] = null
LDT[1] = user code segment
LDT[2] = user data/stack segment descriptor

TASKSの計算

GDTの最大許容エントリー数を計算するには、NR_TASKSを理解することが必要です(NR_TASKSは、Linuxで同時にサポートされるプロセス数を判断する変数です。カーネル・ソース内のデフォルト値は512で、1つのインスタンスに対し最大256の同時接続が可能です)。

GDTで許容される最大エントリー数は、次の式で求めることができます。

Number of entries in GDT = 12 + 2 * NR_TASKS.
As mentioned earlier GDT can have entries = 2^13 -1 = 8192.

8192のセグメント・ディスクリプターのうち、6つのセグメント・ディスクリプターがLinuxで使われ、さらに4つのセグメント・ディスクリプターがAPM機能(拡張電源管理機能)用に使われ、GDT内の4つのエントリーは未使用のまま残ります。したがって、実質的にGDTで許容されるエントリー数は8192 - 14または8180となります。

どの時点でも、GDT内のエントリー数が8180を超えることはできません。したがって、次の式が成り立ちます。

2 * NR_TASKS = 8180
および NR_TASKS = 8180/2 = 4090

(2 * NR_TASKSとなるのは、作成されるプロセスごとにTSSディスクリプター(コンテキスト切り替えコンテキストの保守に使われる)だけでなく、LDTディスクリプターもロードされるためです。)

x86アーキテクチャーにおけるこのプロセス数の制限はLinux 2.2の要素でしたが、カーネル2.4以降は、(TSSの使用が必須になる)ハードウェア・コンテキスト切り替えを部分的に廃止し、プロセス切り替えにすることでこの問題は解消されました。

次は、ページング・モデルについて説明します。

一般的なページング・モデル

ページング・ユニットは、リニア・アドレスを物理アドレスに変換します(図1)。一連のリニア・アドレスはグループ化されてページを形成します。これらのリニア・アドレスは本質的に連続しています。ページング・ユニットはこれらの連続メモリー・セットを、対応する連続物理アドレス・セットとマッピングします。この物理アドレス・セットをページ・フレームと呼びます。ページング・ユニットは、固定サイズのページ・フレームにパーティション分割されるRAMを視覚化する点に注意してください。

このため、ページングには次のような利点があります。

  • 1つのページに対して定義されるアクセス権限は、ページを形成するこれらのリニア・アドレス・グループに対しても有効です。
  • ページの長さとページ・フレームの長さは同じです。

これらのページとページ・フレームをマッピングするデータ構造をページ・テーブルと呼びます。ページ・テーブルはメイン・メモリーに格納され、ページング・ユニットを有効にする前にカーネルによって正しく初期化されます。図5は、ページ・テーブルを示しています。

図5.ページとページ・フレームを対応付けるページ・テーブル
A page table matches pages to page frames
A page table matches pages to page frames

Page1に含まれるアドレス・セットは、Page Frame1に含まれるアドレス・セットと対応することに注意してください。

Linuxでは、セグメンテーション・ユニットよりもページング・ユニットの方を多用します。Linuxとセグメンテーションの説明で前述したように、各セグメント・ディスクリプターは同一のアドレス・セットを使ってリニア・アドレスを指定することで、セグメンテーション・ユニットを使って論理アドレスをリニア・アドレスに変換する必要性を最小限に抑えています。セグメンテーション・ユニットよりもページング・ユニットの方を多用することにより、Linuxでのメモリー管理と異種ハードウェア・プラットフォーム間の移植性は大幅に向上します。

ページングで使用するフィールド

次に、x86アーキテクチャーでのページング指定に使われるフィールドについて説明します。これはLinuxでページングを実現する際に役立ちます。ページング・ユニットは、セグメンテーション・ユニットの出力としてリニア・アドレスを取得し、さらに次のフィールドに分割します。

  • ディレクトリーは10 MSBで表します(最上位ビット(MSB)とは、1つの2進数の中で最も大きな位の値を持つビット桁を指します。MSBは左端ビットとも呼ばれます)。
  • テーブルは中間の10ビットで表します。
  • オフセットは12 LSBで表します(最下位ビット(LSB)とは、1つの2進整数の中で最も小さな位の値を持つ(つまり数値が偶数か奇数かを決める)ビット桁を指します。LSBは右端ビットとも呼ばれます。これは10進整数の最下位桁(1の位の桁または右端の桁)と似ています)。

リニア・アドレスから対応する物理ロケーションへの変換は、2ステップのプロセスです。最初のステップではページ・ディレクトリーと呼ばれる変換テーブルを使用し(ページ・ディレクトリーからページ・テーブルへ移動します)、2番目のステップではページ・テーブルと呼ばれる変換テーブルを使用します(これはページ・テーブルと、必要なページ・フレームに対するオフセットで構成されます)。図6にこれを示します。

図6.ページング・フィールド
Paging fields
Paging fields

始めに、ページ・ディレクトリーの物理アドレスがcr3レジスターにロードされます。リニア・アドレス内のディレクトリー・フィールドは、ページ・ディレクトリー内で正しいページ・テーブルを指すエントリーを判断します。テーブル・フィールド内のアドレスは、ページ・テーブル内で、そのページを含むページ・フレームの物理アドレスが入っているエントリーを判断します。オフセット・フィールドは、ページ・フレーム内の相対位置を判断します。このオフセットの長さは12ビットなので、各ページには4 KBのデータが入ります。

物理アドレスの計算方法を次にまとめます。

  1. cr3 + ページ・ディレクトリー(10 MSB) = table_baseを指す
  2. table_base + ページ・テーブル(10中間ビット) = page_baseを指す
  3. page_base + オフセット = 物理アドレス(ページ・フレームを取得)

ページ・ディレクトリーとページ・テーブルの長さは10ビットなので、これらに対してアドレッシング可能な上限は1024*1024 KBであり、オフセットに対してアドレッシング可能な上限は2^12 (4096バイト)です。したがって、ページ・ディレクトリーによるアドレッシング可能な上限の合計は1024*1024*4096 (= 2^32 メモリー・セル数 = 4 GB)となります。このため、x86アーキテクチャーでもアドレッシング可能な上限の合計は4 GBとなります。

拡張ページング

拡張ページングは、ページ・テーブルの変換テーブルを削除することで得られます。この場合、リニア・アドレスの分割はページ・ディレクトリー(10 MSB)とオフセット(22 LSB)の間で行われます。

22 LSBは、ページ・フレーム用の4 MBの境界を形成します(2^22)。拡張ページングは通常のページングと共存し、大きな連続リニア・アドレスと対応する物理アドレスとのマッピングに使用できます。オペレーティング・システムはページ・テーブルを削除するため、拡張ページングが提供されます。これを有効にするには、PSE (ページ・サイズ拡張)フラグを設定します。

36ビットのPSEは、36ビットの物理アドレスのサポートを4 MBのページにまで拡張すると同時に、4バイトのページ・ディレクトリー・エントリーを保守します。これにより、オペレーティング・システムに大きな設計変更を加えることなく、4 GB以上の物理メモリーをアドレッシングできる単純なメカニズムが実現します。この手法には、デマンド・ページングに関する実質的な制限があります。

Linuxでのページング・モデル

Linuxでのページングは一般的なページングに似ていますが、x86アーキテクチャーでは、次のもので構成される3レベルのページ・テーブル・メカニズムを採用しています。

  • ページ・グローバル・ディレクトリー(pgd)は、マルチレベル・ページ・テーブルのうち抽象化された最上位レベルです。ページ・テーブルの各レベルは異なるサイズのメモリーを処理します。このグローバル・ディレクトリーが処理できる領域のサイズは4 MBです。各エントリーは、より小さいサイズのディレクトリーの下位テーブルを指すポインターになるため、pgdはページ・テーブルのディレクトリーとなります。コードがこの構造体をトラバースする(一部のドライバーがこれを行います)ことを、ページ・テーブルを「歩く」と言います。
  • ページ・ミドル・ディレクトリー(pmd)は、ページ・テーブルの中間レベルです。x86アーキテクチャーの場合、pmdはハードウェア内に存在するのではなく、カーネル・コード内のpgdに組み込まれています。
  • ページ・テーブル・エントリー(pte)は、ページを直接処理する(PAGE_SIZEを調べる)最下位レベルです。この値には、ページの物理アドレスと関連ビットが含まれています。これらのビットは、エントリーが有効かどうかや、関連ページが実際のメモリー内にあるかどうかなどを示します。

Linuxには、大きなメモリー領域をサポートするためにこの3レベルのページング・スキームも組み込まれています。大きなメモリー領域のサポートが不要な場合は、pmdを「1」で定義して、2レベルのページングにすることができます。

これらのレベルはコンパイル時に最適化され、ミドル・ディレクトリーの有効/無効を切り替えるだけで、(同じコード・セットを使って)第2レベルと第3レベルの両方が有効化されます。32ビット・プロセッサーはpmdページング、64ビット・プロセッサーはpgdページングを採用しています。

図7.ページングの3つのレベル
Three levels of paging
Three levels of paging

64ビット・プロセッサーの場合、次のようになります。

  • 21 MSBは使用しません。
  • 13 LSBはページ・オフセットで表されます。
  • 残りの30ビットは次のように分割されます。
    • ページ・テーブル用に10ビット
    • ページ・グローバル・ディレクトリー用に10ビット
    • ページ・ミドル・ディレクトリー用に10ビット

このアーキテクチャーからわかるように、実際にアドレッシングに使われるのは43ビットです。したがって、64ビット・プロセッサーでは事実上、使用可能な仮想メモリーは2の43乗となります。

各プロセスには、ページ・ディレクトリーとページ・テーブルからなる専用セットがあります。実際のユーザー・データが含まれているページ・フレームを参照するため、オペレーティング・システムはまず(x86アーキテクチャーの場合) pgdをcr3レジスターにロードします。Linuxでは、CPUで新しいプロセスが実行されるたびに、TSSセグメントにcr3レジスターの内容を保存し、TSSセグメントにある別の値をcr3レジスターにロードします。この結果、ページング・ユニットによって正しいページ・テーブル・セットが参照されます。

pgdテーブルの各エントリーは、pmdエントリーの配列が含まれたページ・フレームを指します。次にpmdエントリーはpteが含まれたページ・フレームを指し、最後にpteはユーザー・データが含まれたページ・フレームを指します。調べる対象のページがスワップアウトされている場合は、スワップ・エントリーがpteテーブルに格納され、(ページ・エラーが発生した場合に)メモリーに再ロードするページ・フレームを検出する目的で使われます。

図8は、各ページ・テーブル・レベルにオフセットを追加し、対応するページ・フレーム・エントリーとマッピングしたものです。これらのオフセットを得るには、セグメンテーション・ユニットからの出力として受け取ったリニア・アドレスを分割します。各ページ・テーブル要素に対応するリニア・アドレスを分割するため、カーネルでは各種のマクロが使われます。これらのマクロについては詳しく触れずに、リニア・アドレスの分割を図で見てみましょう。

図8.異なるアドレス長を持つリニア・アドレス
Linear addresses have different address lengths
Linear addresses have different address lengths

予約済みページ・フレーム

Linuxでは、カーネル・コードとデータ構造専用にいくつかのページ・フレームが予約されています。これらのページがディスクにスワップされることはありません。0x0から0xc0000000 (PAGE_OFFSET)までのリニア・アドレスは、ユーザー・コードとカーネル・コードの両方で参照されます。PAGE_OFFSETから0xffffffffまではカーネル・コードによってアドレッシングされます。

したがって、4 GBのうちユーザー・アプリケーションに使用できるのは3 GBのみです。

ページングの有効化方法

Linuxプロセスで使われるページング・メカニズムは、次の2段階で構成されます。

  • ブートストラップ時に、システムによって8 MBの物理メモリーのページ・テーブルがセットアップされます。
  • 次に第2段階で、残りの物理メモリーのマッピングが完了します。

ブートストラップ段階では、startup_32()呼び出しによってページングが開始されます。これはarch/i386/kernel/head.Sファイル内で実装されています。この8 MBのマッピングは、PAGE_OFFSET以上のアドレスで行われます。初期化ではまず、swapper_pg_dirと呼ばれるコンパイル時配列が静的に定義されます。これは、コンパイル時に特定のアドレス(0x00101000)に置かれます。

このアクションにより、コード内で静的に定義された2つのページ(pg0とpg1)に対してページ・テーブル・エントリーが確立されます。ページ・サイズ拡張ビットが設定されていない限り、これらのページ・フレームのサイズはデフォルトで4 KBになります(PSEの詳細については「拡張ページング」を参照してください)。サイズはそれぞれ4 MBです。グローバル配列によって指定されるデータ・アドレスは、cr3レジスターに格納されます(これがLinuxプロセス用のページング・ユニット設定の第1段階です)。残りのページ・エントリーは、第2段階で設定されます。

第2段階は、メソッド呼び出しpaging_init()によって処理されます。

x86の32ビット・アーキテクチャーでは、PAGE_OFFSETと、4番目のGB制限(0xFFFFFFFF)で表されるアドレスとの間でRAMのマッピングが行われます。したがって、Linuxの起動時とデフォルトによるマッピングの発生時には、約1 GBのRAMをマッピングできます。ただし、HIGHMEM_CONFIGが設定された場合は、1 GBを超える物理メモリーをカーネルにマッピングすることも可能です。これは一時的な調整であり、kmap()呼び出しによって実行されることに注意してください。

物理メモリー・ゾーン

前述のとおり、Linuxカーネル(32ビット・アーキテクチャーの場合)は仮想メモリーを3:1の比率(3 GBはユーザー・スペース用、1 GBはカーネル・スペース用)で分割します。カーネル・コードとそのデータ構造は、この1 GBのアドレス・スペース内に常駐する必要がありますが、このアドレス・スペースをさらに多く消費するのが物理メモリーの仮想マッピングです。

これが行われるのは、カーネルがアドレス・スペースとマッピングされない場合はメモリーを操作できないためです。したがって、カーネルが処理できる物理メモリーの最大量は、カーネルの仮想アドレス・スペースとマッピングできる量から、カーネル・コード自体のマッピングに必要なスペースを差し引いた値になります。その結果、x86ベースのLinuxシステムが処理できる物理メモリーの最大量は1 GB弱になります。

大勢のユーザーに対応するため、より多くのメモリーをサポートするため、パフォーマンスを向上させるため、そしてアーキテクチャーに依存しないメモリーの記述方法を確立するため、Linuxのメモリー・モデルは進化しなければなりません。これらの目的を達成するために、新しいモデルではメモリーがバンク単位で配列され、各CPUに割り当てられています。各バンクはノードと呼ばれ、各ノードはゾーンに分割されます。ゾーン(メモリー内の範囲を表す)は、さらに次のタイプに分類されます。

  • ZONE_DMA (0から16 MB): 一部のISA/PCIデバイスが必要とする下位の物理メモリー領域に常駐するメモリー範囲。
  • ZONE_NORMAL (16から896 MB): カーネルによって物理メモリーの上位領域と直接マッピングされるメモリー範囲。すべてのカーネル処理にはこのメモリー・ゾーンを使う必要があるため、これは最もパフォーマンスへの影響が大きいゾーンとなります。
  • ZONE_HIGHMEM (896 MB以上): カーネルによるマッピングが行われない、システム内の残りの使用可能メモリー。

カーネルにノードの概念を実装するには、struct pglist_data構造を使用します。ゾーンを記述するにはstruct zone_struct構造を使用します。物理ページ・フレームはstruct Page構造で表され、これらのstructはすべてグローバル構造配列struct mem_map内に保持されます。この配列はNORMAL_ZONEの先頭に格納されます。ノード、ゾーン、ページ・フレームの基本的な関係を図9に示します。

図9.ノード、ゾーン、ページ・フレームの関係
Relationships among the node, zone, and page frame
Relationships among the node, zone, and page frame

Pentium IIの仮想メモリー拡張のサポート(32ビット・システムでは物理アドレス拡張(PAE)により最大64 GBまでアクセス可能)と4 GBの物理メモリーのサポート(同じく32ビット・システムの場合)の両方が実装されると、高メモリー・ゾーンもカーネル・メモリー管理で扱われるようになります。これはx86プラットフォームとSPARCプラットフォームに適用される概念です。一般に、この4 GBのメモリーにアクセスできるようにするには、kmap()を使ってZONE_HIGHMEMとZONE_NORMALをマッピングします。なお、PAEが有効な場合でも、32ビット・アーキテクチャーで16 GBを超えるRAMを使用することはお勧めできません。

(PAEはIntelが提供するメモリー・アドレス拡張です。PAEでは、Address Windowing Extensions APIを使ってアプリケーションのホスト・オペレーティング・システムでサポートすることにより、プロセッサーが物理メモリーのアドレッシングに使用可能なビット数を32ビットから36ビットに拡張できるようにします。)

この物理メモリー・ゾーンの管理は、ゾーン・アロケーターによって行われます。ゾーン・アロケーターはメモリーを多数のゾーンに分割し、各ゾーンを割り振り単位として扱います。特定の割り振り要求では、ゾーンのリストを利用して優先度の高い順に割り振りを試行できます。

次に例を示します。

  • ユーザー・ページの要求は、まず「通常」ゾーン(ZONE_NORMAL)から埋められます。
  • これに失敗した場合は、ZONE_HIGHMEMから埋められます。
  • さらにこれに失敗した場合は、ZONE_DMAから埋められます。

このような割り振り用のゾーン・リストは、ZONE_NORMALゾーン、ZONE_HIGHMEMゾーン、ZONE_DMAゾーンの順に構成されます。一方、DMAページの要求はDMAゾーンから埋める必要があるため、このような要求用のゾーン・リストはDMAゾーンのみで構成されます。

まとめ

メモリー管理は、大掛かりで複雑かつ時間のかかるタスクの集まりです。実際のマルチプログラム環境におけるシステムの動作方法をモデル化することは大変なため、目標を達成するのは難しいと言えます。スケジューリング、ページング動作、マルチプロセス・インタラクションなどの要素はかなり厄介です。皆さんが作業を開始するにあたり、このドキュメントを参考にして、Linuxのメモリー管理の問題に取り組むのに必要な基礎知識を掴んでいただければ幸いです。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=226832
ArticleTitle=Linuxメモリー・モデルの探究
publish-date=01242006