メモリ管理

Win32は「仮想メモリ」をサポートしている。Windows上で実行されているプロセスは、実際の物理メモリの容量に関わらず、プロセスごとに4Gバイトの仮想メモリ空間が割り当てられる。

仮想メモリ空間は、ページと呼ばれる単位で管理される。x86系ならば1メモリページは4Kバイトである。システムは、プロセスの仮想メモリ空間中で、プロセスが使用しているメモリページだけを物理メモリに割り当て、未使用のメモリページは物理メモリに割り当てない。この仕組みにより、物理メモリの容量を超える仮想メモリ空間を実現することができる。

物理メモリには、RAMとページングファイルがある。システムは、RAMの容量以上の物理メモリが必要になると、RAM上のメモリページをページングファイルに退避し、RAM上に空きメモリページを作り、このページを使用する。ページングファイルに退避されたメモリページに割り当てられていた仮想メモリ空間上のメモリページにアクセスすると、システムは再びページングファイルからRAM上へメモリページを移し、読み書きを可能とする。

RAM上のメモリをページングファイルへ退避することをスワップアウトと呼ぶ。また、仮想メモリ空間上のあるメモリページをアクセスした際、そのメモリページがページングファイルにスワップアウトされていた場合、「ページフォールト」というイベントが発生する。システムは、ページフォールトが発生したら、ページフォールトを起こしたメモリページをページングファイルからRAMに移動し、アクセスを再び可能とする。

各々のプロセスは、ワーキングセットと呼ばれるメモリページの集合を保持している。ワーキングセットは、常にRAM上に割り当てられるメモリページ群である。ワーキングセットに含まれるメモリページの数は動的に変化するが、上限と下限が定められている。この上限と下限はSetProcessWorkingSetSize関数によって指定可能である。プロセスがメモリ割り当てを行うと、システムはこのワーキングセットから未使用のメモリページを探し、これをメモリ割り当てに使用する。ワーキングセット中に未使用のメモリページが存在せず、メモリページの個数が上限に達している場合、ワーキングセットからメモリページを取り除き、取り除いたメモリページをページングファイルにスワップアウトし、新たにRAM上のメモリページを割り当て、ワーキングセットに追加する。

プロセスがメモリ開放を行った場合、ワーキングセット中のメモリページがすぐに破棄されるわけではない。この場合、メモリページはワーキングセットからは取り除かれるが、スタンバイリストと呼ばれるリストに追加される。プロセスが再度メモリ割り当てを行った場合、可能ならばスタンバイリストからメモリページを再度ワーキングセット中に移動し、これをメモリ割り当てに再利用する。長時間プロセスがメモリ割り当てを行わない場合、スタンバイリスト中のメモリページは自動的に破棄される。

プロセスの持つ仮想メモリ空間4Gバイトの内、ユーザが使用可能なメモリ領域は下位2Gバイトである。上位2Gバイトはシステムによって使用される。このシステム領域は、更にページドプールとノンページドプールに分けられる。ページドプール中のメモリページはスワップアウト可能である。ノンページドプール中のメモリページはスワップアウト不可能で、システムの重要な情報を保持するために使用される。

メモリページには、アクセス権を設定することができる。設定可能なアクセス権は、読み込み、読み書き、書き込み時コピー、実行可能、実行および読み込み可能、実行及び読み書き可能、実行及び書き込み時コピー、保護、アクセス禁止、キャッシュなし、などの組み合わせである。

書き込み時コピーは、複数のプロセスが初期状態で同一の内容を持つメモリを操作する場合に使用する際に有用である。これは、POSIXサブシステムのfork関数などで使用される。Win32サブシステムのCreateProcess関数は、POSIXサブシステムではfork関数とexec関数の組み合わせに相当する。CreateProcessとは異なり、forkコマンドでは、まず子プロセスが親プロセスの複製として作成されるので、子プロセスが持つメモリの内容は親プロセスのメモリ内容と同一のものとなる。従って、親プロセスの規模が大きい場合、メモリを大量に消費することになる。このような場合、書き込み時コピーを使用する。書き込み時コピーが付与されているメモリページは、親プロセスも子プロセスも初期状態で同一の物理メモリを参照している。子プロセスがそのメモリへの書き込みを行った時点で初めて子プロセス用の物理メモリが割り当てられ、そのメモリページに対して書き込みが行われる。このような方法を用いることにより、メモリの消費量を最小限に押さえることができる。

実行可能であるメモリページ上にマシンコードを置くことにより、このマシンコードを実行することができる。

保護は、動的配列などの実装に便利である。保護ページにアクセスした場合、一度のみ例外が発生する。これを利用して、例えば動的配列の末尾の次のページを保護ページとしておく。例外ハンドラでは、保護例外が発生したページのメモリ割り当てを行い、次のページを保護ページとして予約して置く。これにより、動的配列へのデータの追加などを、動的配列のサイズのチェックを意識することなく行うことができる。

仮想メモリの割り当ては、予約とコミットという二つの動作で行われる。ある仮想メモリを予約した場合、その仮想メモリ空間は他の操作によって使用されない。しかし、その仮想メモリ空間にはまだ物理メモリは割り当てられない。コミットを行うと、予約した仮想メモリ空間に物理メモリが割り当てられる。この二段階の操作は、大量のデータを扱う場合に便利である。アプリケーションは、データの格納に必要となる連続した仮想メモリ領域を予約しておき、実際のデータの書き込みを行う時点で、コミットを行うことにより、物理メモリの使用量を必要最小限に押さえることができる。

VirtualLock関数を使用すると、コミットされている仮想メモリ領域のスワップアウトを禁止することができる。これにより、データを常にRAM上で操作することが可能となるが、RAMは貴重なリソースであり、注意して使用しなければならない。また、VirtualLockを使用した場合にでも、プロセス全体がスワップアウトした場合ロックした仮想メモリ領域も同様にスワップアウトされる。

ユーザは、個々のメモリページを操作する代わりに、ヒープを使用することができる。ヒープは連続したメモリ領域で、メモリ割り当てが必要な際に、このヒープから必要な分だけメモリ領域を切り出して使用することができる。Win32では、ヒープの作成、ヒープからメモリ領域の割り当て、開放などを簡易に行うインターフェースを提供している。

プロセスを起動した場合、自動的にデフォルトのヒープが作成される。これをプロセスヒープと呼ぶ。

通常のアプリケーション開発では、メモリの割り当てなどにはGlobalAllocなどが使用される。GlobalAllocには、「固定メモリ」と「移動可能メモリ」の二種類のメモリを割り当てる機能がある。固定メモリを割り当てた場合、割り当てたメモリ領域のアドレスが返される。移動可能メモリを割り当てた場合、メモリ領域を識別するハンドルが返され、メモリ領域自体は任意に移動される。移動可能メモリは仮想メモリ機構の存在しなかったWin16などで威力を発揮したため、今でも移動可能メモリがメモリの効率的な使用に貢献すると考える人もいるが、仮想メモリ機構の備わったWin32環境ではほとんど意味はない。ただし、DDEやクリップボードなどへのデータの送受信など、特定の目的で使用されることはある。

GlobalAllocなどの関数は、内部的にはヒープへのアクセスを行う関数である。このため、オーバーヘッドがあるという理由でHeapAllocなどを使用するように推奨する人もいる。しかし、この程度のオーバーヘッドが気になる程頻繁にメモリ割り当て・開放を行うような状況ならば、その前にアプリケーションの設計を疑った方が良い。

なお、メモリページのサイズと(VirtualAllocなどの)メモリ割り当ての単位のサイズは異なるので注意が必要。それぞれの値は、GetSystemInfo関数により取得したSYSTEM_INFO構造体のdwPageSizeメンバとdwAllocationGranularityメンバによって取得できる。

プロセスの持つ仮想メモリ空間はプロセス固有のもので、初期状態では他のプロセスの仮想メモリ空間と共有されたメモリ領域を持つことはできない。共有メモリを実現するには、ファイルマッピングオブジェクトを使用する。ファイルマッピングオブジェクトは、ファイルの内容をメモリ領域上にマップする。また、マップされたメモリ領域を複数のプロセス間で共有可能とする。マップされたメモリ領域はビューと呼ばれる。ビュー中のメモリ領域は、ページングファイルの代わりに、マップ元のファイルにスワップアウトされる。ビューをアンマップすると、ビュー上の全てのデータがファイルにスワップアウトされる。

通常のファイルをマップする代わりに、ページングファイルの領域をメモリ上にビューとしてマップすることも可能である。この場合、通常のメモリページと同様にページングファイルへスワップアウトされる共有メモリとなる。

通常他のプロセスの仮想メモリ空間へのアクセスを行うことはないが、これを行う特別な関数も存在する。ReadProcessMemoryは他のプロセスの仮想メモリ空間を読み込み、WriteProcessMemoryは他のプロセスの仮想メモリ空間へデータを書き込む。これらの関数は、通常デバッガなどで使用される。

(99/04/05 更新)

本ドキュメントの内容は保証しません。本ドキュメントによって生じた結果について、一切の責任を負いません。