仮想記憶(かそうきおく、、バーチャルメモリ)とは、コンピュータ分野におけるメモリ管理の仮想化技法の一種であり、オペレーティングシステム (OS) などが物理的なメモリを、アプリケーション・ソフトウェア(プロセスなど)に対して、専用の連続した主記憶装置に見えるように提供する。

この技術により、物理的な主記憶装置に加えてハードディスク装置等の補助記憶装置を併用すれば、物理的な主記憶装置よりも大きな仮想メモリを提供する事ができる。またアプリケーション・プログラム側は、物理メモリ上のアドレスを意識しなくて良いため、マルチタスクの実現が容易である。このため現代のオペレーティングシステムの多くが仮想記憶をサポートしている。

仮想的に与えられたアドレスを仮想アドレス()または論理アドレス()、実記憶上で有効なアドレスを物理アドレス()または実アドレス()という。仮想アドレスの範囲を仮想アドレス空間、物理アドレスの範囲を物理アドレス空間という。

概要 仮想記憶の実装(仮想記憶方式)には、大きく分けてセグメント方式とページング方式の二種類がある。ちなみに68000システムでは、68451(セグメント方式)と、68851(ページング方式)のメモリ管理ユニット (、MMU) が準備されていた。

一般にページング方式の方がよく使われている。これにより、メモリスワッピング(あるいは匿名メモリページング)と仮想記憶が結びつけられる。メモリスワッピングとは、一次記憶装置内のメモリページを二次記憶装置(のスワップファイルあるいはスワップパーティションと呼ばれる場所)に書き出して、より高速な一次記憶装置を他のプロセスが使えるように解放することであるスワップとは - 意味をわかりやすく - IT用語辞典 e-Words。

Windows系OSのうち、Windows NT系ではページング方式の仮想記憶を採用しており、システムUI上では「仮想メモリ」という名前の設定項目が用意されているが、このシステム設定項目はページングファイル(ストレージへのメモリスワップ)に関するものである。アプリケーションおよび多くのシステムプロセスは常に仮想アドレスを使用してメモリを参照する。OSカーネルのコア部のみがアドレスの仮想化をバイパスすることができ、実アドレスを直接使用できる。たとえ稼働中の全プロセスによって要求されるメモリが、システムにインストールされているRAMの容量を超えていなくても、仮想記憶は常に使われているVirtual memory in 32-bit version of Windows - Windows Server | Microsoft Learn。

Unix系システムでもスワップファイルやスワップパーティションなしで仮想記憶を使用することが可能である。従って主記憶装置以上の大きな記憶領域を仮想的に使用できるようにすることは仮想記憶の主な目的ではあるが、本質ではないとも言える。本質は、不連続な物理メモリ領域を連続な仮想メモリ領域にマッピングすることであり、複数のプロセスがそれぞれ固有の連続な仮想メモリ領域を割り当てられる点である。これによって他のプロセスのことを気にせずに動作できる環境が提供されている。そういった意味で、仮想マシンがゲストOSに対して提供していることと、仮想記憶がユーザープロセスに提供していることは等価である。仮想記憶は物理メモリのフラグメンテーションを隠蔽することでアプリケーションのプログラミングを容易にする。すなわちカーネルに記憶階層の管理を委任することで、プログラムが明示的なオーバーレイの制御を行う必要性を排除している。

技術的には、仮想記憶を使うことにより、ソフトウェアが動作するメモリアドレス空間のサイズとアドレス範囲は当該コンピュータの物理メモリ領域には必ずしも縛られなくなる。仮想記憶を適切に実装するには、CPU(あるいはそれに付随するデバイス)がOSに対して仮想メモリを物理メモリにマップする(対応付ける)手段を提供し、主記憶(物理メモリ)に対応していない仮想アドレスにアクセスしたことを検出する手段を提供して必要なデータをスワップインできるようにしなければならない。CPUの支援なしで仮想記憶を提供することも可能だが、その場合は上述の機能を提供するCPUをエミュレートするだけであって、本質的には同じことである。アドビの一部のアプリケーションのように、アプリケーションプログラムが自前で仮想記憶機構を持つ例もあるPhotoshop で仮想記憶ディスクを設定するWindows および macOS 上での Illustrator のパフォーマンスの改善。

ソフトウェアによって仮想記憶を実現することは、本来ハードウェアで実現できる機能をあえてソフトウェアで行おうとしているので一見非効率に思える場合がある。しかし、この見方は場合によっては誤りである。なぜならページ方式やセグメント方式では仮想アドレス空間に対する広がりを持たせているので、例えば画像などを扱う場合には単にアドレス空間方向への広がり=すなわち水平方向への広がりしか持たない。だが画像に対する操作は、特定の2次元空間=面的な広がりを持っている。ここにソフトウェアで仮想記憶を実施するに当たって、面的な広がりをメモリ参照の局在性としてみなすと、単なる仮想記憶よりも性能の向上が期待できる。

背景 まず、以下の議論の前提とする、簡略化した典型的な記憶装置の階層構造のモデルを述べる。 CPU内のレジスタ。CPUのクロック速度で動作するため、最も高速である。レジスタの使用は一般に呼出規約の制限下にあり、コンパイラによる管理に任せられる。大きな容量はないため(32ビットあるいは64ビット、現代的なマイクロプロセッサでは16本〜普通は100本前後)次の計算が使えるように明け渡す必要があり、データを長期間保持させることは通常はできない。内部的にはレジスタ・リネーミングを行っていることもある。 CPU内かCPUに隣接したキャッシュメモリ。通常はある種のSRAMを使用。レジスタに次いで高速であるが、容量は128kバイト〜数メガバイト程度に留まる。では1次キャッシュから3次キャッシュまでもがオンダイという場合もあり、ここで示した容量などに特に拘る意味は無い。 主記憶装置(通常はDRAMを使用)は、プログラミング上CPUが直接リード/ライトできる。キャッシュメモリに比べると速度は落ちるものの、より大きな容量(数百メガバイト〜数ギガバイト)を装着できる。 補助記憶装置としての磁気ディスク装置は、機械的な動作を伴うため、主記憶装置と比べても桁違いに低速だが、数十ギガバイト~数十テラバイト以上と容量が大きい。

主記憶装置とキャッシュメモリのどちらを使用するかは、一般にハードウェアに任せられているため、プログラマからはどちらも同じ物理メモリとしてしか見えない(しかし性能への影響は大きいので、高性能計算など、キャッシュヒット率を考慮してコードを書くこともある)。ハードウェアの動作としては、まずキャッシュメモリをアクセスし、さらに必要ならキャッシュメモリと主記憶とのやり取りを行う。

多くのアプリケーションは、情報(実行ファイルの命令列やデータ)がなるべく物理メモリ上に格納された状態でアクセスできることを要求する。これは特に見かけ上並列に複数のプロセス/アプリケーションを同時実行するオペレーティングシステムでは重要となる。全実行中プログラムが必要とする物理メモリ量が実装されている物理メモリ量より大きい場合、当然の結果として情報の一部をディスクに退避し、必要に応じてその内容を物理メモリに戻して使用することになる。しかし、これを実現する手法は様々である。

ひとつの方法として、アプリケーション自身が物理メモリ上に置くべき情報の範囲を決定し、補助記憶装置との情報のやりとりも制御することが考えられる。プログラマはプログラム(およびデータ)のどの部分が現時点で必要か不必要かを判断し、それらの領域の物理メモリへのロード(あるいはアンロード)を必要に応じて行わなければならないだろう。この手法の欠点は、各アプリケーションのプログラマがそのような設計/実装/デバッグに時間を費やす必要がある点であり、アプリケーションそのものに集中できなくなってプログラミング効率が低下する。また、あるプログラマがある時点で物理メモリ上に置くべきデータなどを決定しても、それが例えば、どうしても全物理メモリが必要となった場合など、他のプログラマの決定と衝突してしまう危険がある。仮想記憶が普及する以前のオーバーレイ方式がほぼこれに相当する。

別の方法として、データの参照をポインタではなく何らかのハンドルで行う方式である。OSはそのようなハンドルと対応するデータを物理メモリにロードしたり、逆に補助記憶装置に移したりする。この方式の欠点は、アプリケーションのコードが非常に複雑になる点、アプリケーションがうまく振舞わなければならない点(必要なデータを物理メモリにロックする機能が必要になるだろう)、標準ライブラリが大きなメモリを確保しておいてアプリケーションのメモリ確保要求に応えるという機能を使えなくなる点などが上げられる。この方式の既存の例としてはMicrosoft Windows 3.1が有名である。

現代の解決策は仮想記憶方式の使用である。特別なハードウェアとOSの組合せにより、主記憶容量が大きくなったように見せ、各プログラムが自由気ままに空間を広げて使用することを可能にする。動作中の他のソフトウェアからは、仮想記憶機構の内部の動きは見えない。一般に仮想的な主記憶容量はほとんどどんな大きさにもできる。ただし、アドレスそのもののサイズによる制限がある。32ビットシステムでは、仮想アドレス空間サイズは全体で 232バイト、つまり4ギガバイトである。64ビットの場合、アドレスは64ビットや48ビットといったさらに大きなものになっている。多くのオペレーティングシステムは仮想アドレス空間全体をアプリケーションに使わせることはなく、一部をカーネル空間にすることでカーネルからユーザ空間に容易にアクセスできるようにしている。ただし、これは絶対必要な機能ではなく、OSによっては仮想空間全体をユーザー空間としている。

仮想記憶によってアプリケーションプログラマの仕事はずっと単純になる。アプリケーションが必要とするメモリ容量を気にする必要はなく、必要なサイズの主記憶が使えるように見え、仮想アドレス空間全体の好きな場所にデータを配置することができる。プログラマは主記憶と補助記憶の間でデータをやりとりするのを気にしなくてもよい。もちろん、プログラマが大量のデータを扱う際の性能を考慮しなければならない場合、アクセスするデータがなるべく近いアドレスに配置されるよう注意して、ある時点で必要なメモリ量を減らし不要なスワッピングを回避しなければならない。

仮想記憶はコンピュータ・アーキテクチャの重要な部分であり、その実装にはハードウェア的サポートがほぼ必須である。メモリ管理ユニット (MMU) と呼ばれる部分であり、性能の都合もありCPUに組込まれることも多い。1960年代までのメインフレームの大部分は、基本的には仮想記憶をサポートしていなかった。1960年代のメインフレームで例外といえるものを以下に挙げる。 Atlas Supervisor(Atlas) MCP(バロース B5000) MTS、TSS/360、CP/CMS(IBM System/360 Model 67) Multics(GE 645) TSOS(RCA Spectra 70/46)

1980年代のパーソナルコンピュータで仮想記憶をサポートした例として Apple Lisa がある。

歴史 仮想記憶技術が開発される以前、1940年代から1950年代のプログラマは2レベルの記憶装置(主記憶あるいはRAMと、磁気ディスク装置あるいは磁気ドラムメモリといった二次記憶)を直接管理する必要があり、大規模プログラムではオーバーレイなどの技法が使われていた。従って仮想記憶は、主記憶を拡張するためだけでなく、そのような拡張をプログラマが可能な限り容易に扱えるように導入された。マルチプログラミングやマルチタスクを実装した初期のシステムは、メモリを複数のプログラムに分割するのに仮想記憶を使っていない。例えば初期のPDP-10はレジスタを使ってマルチタスクを実現していた。

ページング方式はマンチェスター大学のAtlas上で開発された。1万6千ワードの磁気コアメモリの一次記憶と9万6千ワードの磁気ドラムメモリによる二次記憶を制御するものである。最初のAtlasは1962年に稼働開始したが、ページングのプロトタイプは1959年に開発されているR. J. Creasy, "The origin of the VM/370 time-sharing system", IBM Journal of Research & Development, Vol. 25, No. 5 (September 1981), p. 486Atlas design includes virtual memory 。なお、ドイツの初期の情報工学者 Fritz-Rudolf Güntsch (後に Telefunken TR 440 というメインフレームを開発)は 1957年の博士論文 Logischer Entwurf eines digitalen Rechengerätes mit mehreren asynchron laufenden Trommeln und automatischem Schnellspeicherbetrieb(複数非同期ドラム装置と自動高速メモリモードを持つデジタル計算機の論理概念)で仮想記憶のコンセプトを発明していたと言われている。

1961年、バロースはセグメント方式で仮想記憶をサポートした世界初の商用コンピュータ B5000 をリリースした。

1965年にMITが開発したMultics以降、仮想記憶は本格的に採用され始めた。

コンピュータ史上の多くの技術と同様、仮想記憶にも様々な曲折があった。安定した技術と見なされるまで、仮想記憶の様々な問題点を解決しようとするモデルや理論が開発され実験がなされた。仮想アドレスを物理アドレスに変換するハードウェア機構の開発も必然的だったが、初期の実装ではそれによってメモリアクセスが若干遅くなった。システム全体を対象とするアルゴリズム(仮想記憶)は従来のアプリケーション単位のアルゴリズム(オーバーレイ)よりも非効率ではないかという懸念もあった。1969年、商用コンピュータでの仮想記憶に関する論争は事実上終結した。David Sayre 率いるIBMの研究チームが仮想記憶システムが手動制御システムよりも優位にあることを示したのである。

1970年、IBMはSystem/370シリーズのOSであるDOS/VS、OS/VS1、OS/VS2(後のMVS)で仮想記憶をサポートした。OS/VS1はシングルタスクの仮想記憶で、マルチタスクには従来通りユーザーによるマルチプログラミングが必要であったが、OS/VS2はマルチタスクの仮想記憶(複数の仮想アドレススペース)をオペレーティングシステムの機能としてサポートした。以後の各社メインフレームでは仮想記憶が一般的となる。

ミニコンピュータで初めて仮想記憶を導入したのは、ノルウェーのNORD-1である(1969年)。1976年、DECのミニコンピュータ VAXシリーズのOSであるVMSで仮想記憶をサポートした。

しかし、1980年代の初期のパーソナルコンピュータでは仮想記憶は採用されていない。これは当時のマイクロプロセッサの性能や機能の問題もあるし、個人用のコンピュータに仮想記憶が必要になると見なされていなかったという面もある。当時の主流はバンク切り換えによるメモリ増設だった。x86アーキテクチャで仮想記憶が導入されたのは、Intel 80286 によるプロテクトモードが最初だが、セグメント単位のスワッピングはセグメントが大きくなると性能が悪くなるという問題があった。Intel 80386 では既存のセグメント方式の下層にページング方式を実装し、ページフォールトによるページングが可能となった。しかしセグメントディスクリプタのロードは時間のかかる処理だったため、OS設計者はセグメントを使わずページングだけを使うようになっていった。仮想記憶が導入されたのは、OS/2(1987年)、Microsoft Windows 3.0 (1990年)、MacintoshのSystem 7(1991年)、Linuxカーネル 0.11+VM(1991年)などが最初である。

ページング方式 仮想記憶は、必須ではないものの通常ページング方式を使って実装される。ページングでは仮想アドレスを表すビット列の下位ビット列部分はそのまま物理アドレスの下位ビット列部分として使われる。上位ビット列部分はアドレス変換テーブル(群)のキーとして使用され、それによって実際の物理アドレスの上位ビット部分を得る。

このため、サイズが2の冪乗の仮想アドレス空間の連続したアドレス範囲が、対応する連続な物理アドレス範囲に変換される。そのような範囲で参照されるメモリをページと呼ぶ。ページサイズは512バイトから8192バイトが一般的であり、特に4096バイトが最もよく使われるが、特殊用途として4Mバイトやそれ以上のサイズのページも使われることがある。

オペレーティングシステムは、ページテーブルと呼ばれるデータ構造にアドレス変換テーブル、つまり仮想ページ番号と物理ページ番号のマッピング情報を格納する。

あるページが使用不可とされている場合(物理メモリに対応しておらず、スワップ領域に内容がある場合など)、CPUがそのページ内のメモリ位置を参照しようとしたとき、ハードウェアの機構がオペレーティングシステムに、一般にページフォールトと呼ばれる例外を通知する。これにより実行コンテキストはオペレーティングシステム内の例外処理ルーチンにジャンプする。そのページがスワップ領域にあるなら、そのルーチンは「ページスワップ」と呼ばれる処理を実行して必要なページの内容を物理メモリに読み込む。

ページスワップ操作には一連の段階がある。まず、メモリ上のページを選択する。例えば、最近アクセスされておらず、なるべくならスワップ領域を含む何らかのディスクから読み込まれたままで変更されていないページを選択する(詳細はページ置換アルゴリズムを参照)。そのページが変更されている場合、その内容をスワップ領域に書き出す。次に必要とされている、例外発生時に参照しようとしていた仮想アドレスに対応するページの情報を読み込む。ページの読み込みが完了したら、その物理メモリの内容更新に応じて仮想アドレスから物理アドレスへの変換テーブルを更新する。ページスワップが完了すると、例外処理を完了し、元のプログラムの実行が例外発生箇所から再開されて何事もなかったかのように処理が続行される。

仮想ページに何も割り当てられていないために使用不可となっている可能性もある。そのような場合、未使用、あるいはスワップアウトして未使用にした物理ページを割り当て、OSによってはその内容をゼロクリアする。ページテーブルはそれに対応して更新され、上述の場合と同様にプログラムが再開される。

セグメント方式 バロース B5000などのシステムは、ページング方式ではなくセグメント方式を使い、仮想アドレス空間を可変長のセグメントに分割する。その場合、仮想アドレスはセグメント番号とセグメント内オフセットから成る。Intel 80286の保護モードにもそのようなセグメント方式があったが、ほとんど使われなかった。セグメントとページは、セグメントをページに分割するという形で同時に使用できる。そのようなメモリ構成のシステムとして、MulticsやSystem/38がある。その場合の基本はページングであり、セグメントはメモリ保護に使われる。

Intel 80386 とその後のIA-32プロセッサでは、セグメントをページ化された32ビットのリニアなアドレス空間に置く。セグメントによる管理と、ページ単位による管理の、2段階のシステムとなっている。しかし、複数のセグメントを活用しているOSは少なく(ただし、研究レベルまで含めればそんなに少なくない)、単純にベースアドレスを全てゼロとして、範囲のみ指定した最小限必要なだけのセグメントと、ページングのみを使っていることが多い。

単一レベル記憶

これは、mmapやWin32のMapViewOfFileのような機構とは異なる。mmap等ではファイルは任意の位置にマッピングされる可能性があるため、ファイル間のポインタは使えないためである。Multicsでは、ファイル(複セグメントファイルの場合はセグメント)はセグメント機構を通してアドレス空間にマッピングされる。そのためファイルは常にセグメント境界にマッピングされる。ファイルのリンク部分にはポインタが並んでおり、そのポインタをレジスタにロードしたり間接参照したりするとトラップが発生する。未解決のポインタにはセグメント名を示す値とセグメント内オフセットがある。トラップハンドラは対応するセグメントをアドレス空間にマッピングし、ポインタのセグメント識別子部分をセグメント番号に書き換えるので、2度目以降はそのポインタにアクセスしてもトラップが発生しなくなる。この方式ではリンケージエディタが不要であり、同じファイルを複数のプロセスが異なる位置にマッピングしても問題なく機能する。

詳細 thumb|right|350px|アドレス変換機構の概念図 仮想アドレスから物理アドレスへの変換はメモリ管理ユニット (MMU) というハードウェア装置によって実装されている。これはCPUに内蔵されたモジュールの場合もあるし、外付けでCPUに密結合された別のチップの場合もある。これを動的アドレス変換機構 (DAT : Dynamic Address Translation) と呼ぶ。

OSは、プログラムの仮想アドレス空間のどの部分を物理メモリに保持するかを決定する。OSは、MMUが使用する仮想アドレスから物理アドレスへの変換テーブルも管理する。さらに仮想メモリ例外が発生したら、OSはそれを解決するために物理メモリ領域を確保し、必要なら元の内容をディスクに追い出した上で新たに必要とされている情報をディスクから持ってきて、変換テーブルを更新し、例外発生したソフトウェアの実行を再開する。

多くのコンピュータでは、この変換テーブルは物理メモリに格納されている。従って仮想メモリを参照すると、本来の参照以外に変換テーブルの参照が(変換テーブルの構成によっては複数回)発生する。このアドレス変換による性能低下を低減するため、ほとんどのMMUはよく使われる仮想ページに高速にアクセスできるよう、最近使われた仮想アドレスとそれに対応する物理アドレスを保持しておくテーブルを持っている。これをトランスレーション・ルックアサイド・バッファ(TLB)と呼ぶ。参照アドレスがTLB内に格納された変換テーブルでカバーされていれば、余分な変換テーブルの参照をせずに、高速に変換を行うことができる。ただし、TLBは高価な装置のためテーブルの大きさが限られており、目標の仮想アドレスが見当たらない場合は物理メモリ上の変換テーブルを参照してアドレス変換が行われる。

プロセッサによっては、この一連の処理がハードウェア内で行われる。MMUは物理メモリ上の変換テーブルから必要な変換内容を持ってくるので、ソフトウェア側は余分な処理を必要としない。別の種類のプロセッサでは、オペレーティングシステムの介在が必須である。TLBに必要な変換内容がない場合、例外が発生し、オペレーティングシステムがTLB内の1つのエントリを必要な変換テーブルの内容と置き換え、当初のメモリ参照を行った命令から実行が再開され、再度TLBを参照して変換を行う。

仮想記憶をサポートするハードウェアの多くはメモリ保護もサポートしている。MMUはメモリ参照の種類(リード、ライト、実行)やメモリ参照時のCPUモードによって扱いを変える機能を持っていることもある。これによってオペレーティングシステムは自身のコードとデータ(例えばアドレス変換テーブルなど)を問題のあるアプリケーションプログラムの不正なメモリアクセスから保護したり、アプリケーションを相互に保護したり、アプリケーション自身の不正動作(例えば自身のコード部分に書き込もうとするなどの動作)から保護したりする。

仮想アドレス空間管理 thumb|right|350px|多重仮想記憶の概念図 各プロセスの仮想アドレス空間には、そのプロセスが使用するコードやデータが配置される。ページング方式であれ、セグメント方式であれ、仮想アドレス空間内で使用している範囲の管理と制御が仮想記憶機構として必須である。例えば、実行ファイルの内容を仮想メモリ上に配置する領域、スタックを配置する領域などがある。このような領域をセグメントと呼ぶ。セグメント方式のセグメントと似ているが、純粋に仮想的なオブジェクトである。実行ファイルを配置する領域は必ずしも連続ではない。プログラムのコード部分とデータ部分を分離して配置するのが一般的で、前者をテキストセグメントもしくはコードセグメント、後者をデータセグメントと呼ぶ。Unix系システムや Windows では、一般的にデータセグメントの一部としてBSSセクションとヒープ領域を含む。BSSセクションにはプロセス起動時に0に初期化される静的変数を配置する。初期値が0の静的変数を別扱いしているのは、読み書きが発生するまで0で初期化するのを後回しに出来るようにするための高速化のテクニックである。Unix系システムではヒープ領域はデータセグメントの末尾に配置され、brk() 関数などでデータセグメントのサイズを変えることでヒープ領域のサイズを変えられるようにする。各セグメントはマッピングしているオブジェクトが何であるか、その領域へのアクセス権などを属性情報として保持する。

テキストセグメントはファイルシステム上の実行ファイルの一部と完全に対応しており、書き換えられることもない。従って、マッピングしているオブジェクトは実行ファイルであり、アクセス属性は「リードオンリー」となる。データセグメントやスタックは一時的な存在であるため何かをマッピングしているわけではない。そこでこれらは匿名ファイル(Anonymous File)をマッピングしているものとして管理される。匿名ファイルをマッピングしているセグメントに対応するページを匿名ページと呼び、これがスワッピングの際にスワップ領域に書き出される。データセグメントは当初は実行ファイルの一部と対応しているが、書き込み可能な属性が設定されている。ページング方式の場合、データセグメント内の内容が更新されたページはページ単位で匿名ページへと属性変更される。

exec() システムコールなどで新たにプロセスの仮想アドレス空間を設定した当初は、基本的にこのような仮想アドレス空間を管理するデータ構造がカーネル内に作成されるだけで、実際の実行ファイルの内容はロードされない。Unix系システムでは、exec() システムコールからユーザ空間に制御が戻された瞬間にページフォールトが発生し、そこで初めてページ単位に実行ファイルの内容がロードされる。ただし、性能向上目的で事前にマッピングを作成する場合もある。

各プロセスの仮想アドレス空間のアドレス範囲は同じでありオーバーラップしているのが一般的である。これを多重仮想記憶と呼ぶ。MMUは現に実行中のプロセスの仮想空間のみを認識する。コンテキストスイッチでプロセスを切り替える際、MMUに対して仮想アドレス空間の切り替えも指示する必要があるが、その方式はアーキテクチャによって様々である。