コンテンツにスキップ

OS の役割:抽象化・スケジューリング・仮想記憶はなぜ必要か

前提:コンピュータアーキテクチャとCPUの構造:トランジスタから RISC-V まで

生 Markdown
  • OS の仕事は突き詰めると 2 つです。ハードウェアの面倒な差異を隠す抽象化と、有限の資源を複数のプログラムに配る資源管理。この 2 つが「複数のプログラムが同時に、しかも互いを壊さずに動く」という日常を支えています。
  • 抽象化と管理を強制するには、ハードウェア側の協力が要ります。特権モード割り込みがその協力の実体で、プログラムから OS への入口がシステムコールです。
  • CPU の配分(スケジューリング)は数理的に扱えます。全ジョブが同時に到着する 1 CPU の非割り込みモデルでは、短いジョブ優先(SJF)が平均待ち時間を最小にします定理 4.1)。ただし実際の OS が SJF を使わないのには理由があります。
  • メモリの配分はページングで行います。仮想アドレスを固定長のページ単位で物理フレームに写す仕組みで、多段ページテーブルと TLB がこれを実用的な速度と容量に収めます(命題 5.3命題 5.5)。
  • 置き換えアルゴリズムの良し悪しも定理になります。FIFO はメモリを増やすと性能が落ちることがあり(例 5.7)、LRU にはこの異常が起きません(定理 5.8)。
  • 同時実行の安全性はデッドロックの回避に帰着します。デッドロックが起きるには 4 つの条件がすべて必要で(定理 6.4)、どれか 1 つを崩せば防げます。

1. 動機:むき出しのハードウェアで何が困るのか

Section titled “1. 動機:むき出しのハードウェアで何が困るのか”

コンピュータアーキテクチャと CPU の構造 で見たように、CPU は「メモリから命令を取り、デコードし、実行する」という単純なループを回しているだけの機械です(定義 6.1[コンピュータアーキテクチャとCPUの構造]例 7.1[コンピュータアーキテクチャとCPUの構造])。では、OS が無い状態、つまり電源を入れると自分のプログラムだけが CPU を独占する状態を考えてみてください。1970 年代以前の多くの計算機、あるいは今日のごく小さな組み込み機器はまさにそうなっています。

何が困るでしょうか。素朴に数え上げると、少なくとも 4 つの困りごとがあります。

第一に、ハードウェアの差異がプログラムに漏れます。 ディスクに 1 バイト書くには、そのディスクコントローラのレジスタ配置と手順を知らなければなりません。コントローラが変われば、プログラムを書き直すことになります。同じ「ファイルに保存する」という意図が、機種ごとに別のコードになるのは明らかに不合理です。

第二に、資源が奪い合いになります。 2 つのプログラムを同時に動かしたいとして、CPU は 1 つ(あるいは数個)しかありません。物理メモリも有限です。誰がいつ CPU を使うのか、どのプログラムがどの番地を使ってよいのかを、誰かが決めなければなりません。

第三に、隔離がありません。 プログラム A のバグが番地を踏み外してプログラム B のデータを壊したとき、あるいは悪意あるプログラムが他人のパスワードを読みに行ったとき、それを止めるものが何もありません。

第四に、暴走を止められません。 無限ループに入ったプログラムがあると、CPU は永遠にそれを実行し続けます。他のプログラムに実行の機会が回ってきません。

OS(オペレーティングシステム)は、これら 4 つに一度に答えるソフトウェアです。第一の困りごとには抽象化(ファイル、ソケット、プロセスといった、ハードウェアに依存しない概念を提供する)で、第二には資源管理(スケジューリングとメモリ管理)で、第三と第四には保護(特権モードと割り込みを使った強制)で答えます。

2. 準備:特権モードと割り込み

Section titled “2. 準備:特権モードと割り込み”

OS が「境界」を強制できるのは、OS が偉いからではありません。CPU が 2 つの実行モードを持っているからです。

定義 2.1特権モードとユーザモード

CPU は状態レジスタの 1 ビット(以上)で表される実行モードを持つ。

  • カーネルモード(特権モード、supervisor mode)では、すべての命令を実行でき、すべての物理メモリと入出力デバイスにアクセスできる。
  • ユーザモードでは、特権命令(メモリ管理ユニットの設定変更、割り込みの禁止、入出力ポートへの直接アクセス、モードビット自体の書き換えなど)の実行が禁止され、実行しようとするとハードウェアが例外を発生させる。またアクセスできるメモリは、後述のアドレス変換機構が許可した範囲に限られる。

ユーザモードからカーネルモードへの遷移は、ハードウェアが定めたごく限られた入口(トラップ・割り込み・例外)を通じてのみ起こり、遷移先の番地は事前にカーネルが設定したベクタで決まる。

最後の一文が保護の要です。ユーザプログラムは「カーネルモードになる」ことはできても、「カーネルモードで自分の好きなコードを走らせる」ことはできません。遷移すると必ず OS が用意した番地に飛ばされるからです。

もう 1 つの道具が割り込みです。タイマ割り込みは一定周期(多くの OS で 1〜10 ミリ秒程度)で CPU の実行を強制的に中断し、カーネルに制御を移します。これがあるおかげで、無限ループするプログラムからも OS は CPU を取り返せます。第四の困りごとへの答えがこれです。

定義 2.2システムコール

ユーザモードのプログラムが OS のサービスを要求するための、規定された手続き。ISA(定義 8.1[コンピュータアーキテクチャとCPUの構造])が定める専用命令(x86-64 の syscall、RISC-V の ecall、ARM の svc)を実行すると、CPU はカーネルモードに切り替わり、あらかじめ設定されたトラップハンドラの番地から実行を再開する。引数はレジスタまたはメモリを介して渡され、要求の種類はシステムコール番号で指定される。処理を終えたカーネルは復帰命令(RISC-V の sret など)でユーザモードに戻す。

flowchart LR
U["ユーザモード:アプリケーション"] -->|"ecall / syscall 命令"| T["トラップ処理(カーネルが設定した番地)"]
T --> K["カーネルモード:OS のサービス実行"]
K -->|"sret / sysret 命令"| U
H["タイマ・デバイス"] -.->|"割り込み"| T
E["不正な番地アクセス・特権命令"] -.->|"例外"| T
ユーザモードとカーネルモードの往復。プログラムから OS への入口は限られている

C 言語で read(fd, buf, n) と書いたとき、実際に起きているのはこの往復です。ライブラリ関数 read は引数をレジスタに詰めて syscall 命令を実行し、カーネルがファイル記述子 fd を解決し、ディスクドライバに読み出しを依頼し、結果を buf にコピーして戻ってきます。プログラム側はディスクコントローラのことを何も知りません。これが第一の困りごとへの答えです。

3. プロセス:実行中のプログラムという抽象

Section titled “3. プロセス:実行中のプログラムという抽象”

定義 3.1プロセス

プロセスとは、実行中のプログラムを表す OS の抽象であり、次の情報の組として実現される。

  1. アドレス空間:そのプロセスが見る仮想アドレスの全体と、その各領域(コード、データ、ヒープ、スタック)の内容。
  2. CPU の実行文脈:プログラムカウンタ、汎用レジスタ、スタックポインタ、状態レジスタの値。
  3. カーネルが管理する資源:開いているファイル記述子の表、シグナルの設定、親子関係、資源利用の統計など。
  4. 状態:実行中(running)、実行可能(ready)、待ち(blocked)のいずれか。

これらをまとめて保持するカーネル内のデータ構造をプロセス制御ブロック(PCB)と呼ぶ。

プロセスという抽象が与えるものは 2 つあります。1 つはCPU の仮想化で、実際には 1 つの CPU を時分割しているのに、各プロセスからは自分専用の CPU があるように見えます。もう 1 つはメモリの仮想化で、これは 定義 5.1 以降で扱います。

定義 3.2コンテキストスイッチ

実行中のプロセス PP から別のプロセス QQ へ CPU を移す操作。カーネルは (a) PP のレジスタ群を PP の PCB に退避し、(b) メモリ管理ユニットのページテーブル基底レジスタを QQ のものに切り替え、(c) QQ の PCB からレジスタ群を復元し、(d) QQ のユーザモードへ復帰する。

コンテキストスイッチには直接費用(レジスタの退避と復元、数マイクロ秒未満)と間接費用(キャッシュと TLB の内容が新しいプロセスのものに入れ替わるまでの性能低下、しばしば直接費用より大きい)があります。この費用があるために、「タイムスライスをいくらでも細かくすればよい」とはなりません。

実行可能なプロセスが複数あるとき、どれを次に走らせるかを決めるのがスケジューラです。まず評価指標を定めます。プロセス ii が時刻 aia_i に到着し、CPU を合計 tit_i だけ必要とし(この tit_iバースト時間という)、時刻 cic_i に完了したとします。

  • 待ち時間 wi=(ciai)tiw_i = (c_i - a_i) - t_i:実行可能なのに走れなかった時間の合計。
  • 応答時間 rir_i:到着してから初めて CPU を得るまでの時間。
  • ターンアラウンド時間 ciaic_i - a_i

対話的な用途では応答時間が、バッチ処理では平均待ち時間が重視されます。この 2 つは同時には最適化できません。まず平均待ち時間について、はっきりした答えがあります。

定理 4.1最短ジョブ優先の最適性

nn 個のプロセスがすべて時刻 00 に到着し、バースト時間 t1,,tn>0t_1, \ldots, t_n > 0 が既知であるとする。CPU は 1 個、いったん実行を始めたプロセスは完了するまで中断しない(非プリエンプティブ)ものとし、コンテキストスイッチの費用は 00 とする。このとき、バースト時間の昇順に実行する順序(最短ジョブ優先、SJF)は平均待ち時間を最小にする。

証明(定理 4.1)

実行順序を全単射 π:{1,,n}{1,,n}\pi : \{1,\ldots,n\} \to \{1,\ldots,n\} で表し、π(k)\pi(k)kk 番目に実行するプロセスとします。すべて時刻 00 に到着し中断が無いので、kk 番目に実行されるプロセスの待ち時間は、それより前に実行されたプロセスのバースト時間の総和です。すなわち

wπ(k)=j=1k1tπ(j).w_{\pi(k)} = \sum_{j=1}^{k-1} t_{\pi(j)} .

総待ち時間 W(π)=k=1nwπ(k)W(\pi) = \sum_{k=1}^{n} w_{\pi(k)} を、和の順序を入れ替えて jj について整理します。tπ(j)t_{\pi(j)} が現れるのは k=j+1,j+2,,nk = j+1, j+2, \ldots, nnjn-j 個の項なので

W(π)=k=1nj=1k1tπ(j)=j=1n(nj)tπ(j).W(\pi) = \sum_{k=1}^{n}\sum_{j=1}^{k-1} t_{\pi(j)} = \sum_{j=1}^{n} (n-j)\, t_{\pi(j)} .

ここで、π\pi が昇順でないと仮定します。すると隣り合う位置 j,j+1j, j+1a:=tπ(j)>tπ(j+1)=:ba := t_{\pi(j)} > t_{\pi(j+1)} =: b となるものが存在します(もし隣接するすべての組で tπ(j)tπ(j+1)t_{\pi(j)} \le t_{\pi(j+1)} なら列全体が昇順だからです)。この 2 つを入れ替えた順序を π\pi' とすると、j,j+1j, j+1 以外の項は変わらないので

W(π)W(π)=[(nj)b+(nj1)a][(nj)a+(nj1)b]=ba<0.W(\pi') - W(\pi) = \bigl[(n-j)b + (n-j-1)a\bigr] - \bigl[(n-j)a + (n-j-1)b\bigr] = b - a < 0 .

つまり入れ替えると総待ち時間が真に減ります。順序は有限個(n!n! 通り)しかないので最小値を取る順序が存在し、いま示したことからそれは昇順でなければなりません。逆に昇順の順序は(バースト時間が等しいものの並べ方を除いて)一意で、値はすべて等しくなります。平均待ち時間は W(π)/nW(\pi)/n なので、同じ順序が平均も最小にします。

注意 4.2

定理 4.1 は美しいのですが、実際の OS はそのままでは使えません。理由は 2 つあります。第一に、バースト時間 tit_i は事前には分かりません(分かるならプログラムの停止性が分かってしまいます。プログラムを走らせずにその実行時のふるまいを言い当てることの原理的な限界については 定理 5.6[プログラミング言語論] を参照)。第二に、長いジョブが飢餓(starvation)に陥ります。短いジョブが到着し続ける限り、長いジョブは永遠に走れません。実用のスケジューラは、過去のバースト長の指数移動平均で tit_i を推定したり、待たされた時間に応じて優先度を上げるエイジングを組み合わせたりします。

対話的な応答時間を保証するには、時間で強制的に切り上げる方式を使います。

命題 4.3

ラウンドロビン方式(実行可能プロセスを循環キューに並べ、各プロセスに一定のタイムクォンタム q>0q > 0 ずつ CPU を与え、使い切ったらキューの末尾に戻す)を考える。実行可能なプロセスが常に高々 nn 個であり、1 回のコンテキストスイッチに時間 s0s \ge 0 かかるとする。このとき、実行可能になったどのプロセスも、(n1)(q+s)(n-1)(q+s) 以内に必ず最初の CPU 時間を得る。また、どのプロセスもクォンタムを使い切る(実行の途中で終了したり待ちに入ったりしない)ならば、CPU の実効利用率は q/(q+s)q/(q+s) である。

証明(命題 4.3)

あるプロセス PP がキューの末尾に入った瞬間を考えます。PP の前には高々 n1n-1 個のプロセスがいます。ラウンドロビンでは各プロセスは 1 回の順番につき最大 qq しか CPU を占有せず、その後に 1 回のコンテキストスイッチ(時間 ss)が入るので、PP の前の 1 個あたりに費やされる時間は高々 q+sq + s です。前のプロセスがクォンタムを使い切らずに終了または待ちに入る場合はそれより短くなります。したがって PP が CPU を得るまでの時間は高々 (n1)(q+s)(n-1)(q+s) です。

利用率については、キューが空でない限り、時間軸は「実行 qq」と「切り替え ss」の繰り返しで埋まります。よって CPU が有用な仕事に使われる割合は q/(q+s)q/(q+s) です。なお、クォンタムを使い切らずに終了または待ちに入るプロセスがあると、切り替えの回数はそのままで実行部分だけが短くなるので、この比率は下がりえます。

この 2 つの主張が、クォンタム qq の設計指針を与えます。qq を小さくすると応答時間の上界 (n1)(q+s)(n-1)(q+s) は改善しますが、利用率 q/(q+s)q/(q+s) が落ちます。s=5 μss = 5\ \mu\mathrm{s}q=1 msq = 1\ \mathrm{ms} なら利用率は 1000/100599.5%1000/1005 \approx 99.5\%q=50 μsq = 50\ \mu\mathrm{s} なら 50/5591%50/55 \approx 91\% です。実際の Linux の CFS は固定クォンタムではなく「各プロセスの実行済み時間(仮想実行時間)が最小のものを選ぶ」方式ですが、思想は同じで、最小粒度を設けて切り替えすぎを防いでいます。

例 4.43 方式の平均待ち時間と応答時間

時刻 00 に 4 つのプロセスが到着し、バースト時間が t1=24, t2=3, t3=3, t4=6t_1 = 24,\ t_2 = 3,\ t_3 = 3,\ t_4 = 6(単位はミリ秒)だとします。切り替え費用は 00 とします。

FCFS(到着順、P1P2P3P4P_1 \to P_2 \to P_3 \to P_4:待ち時間は 0, 24, 27, 300,\ 24,\ 27,\ 30 なので

wˉ=0+24+27+304=814=20.25.\bar{w} = \frac{0+24+27+30}{4} = \frac{81}{4} = 20.25 .

応答時間も同じ 0,24,27,300, 24, 27, 30 で平均 20.2520.25 です。

SJF(P2P3P4P1P_2 \to P_3 \to P_4 \to P_1:待ち時間は 0, 3, 6, 120,\ 3,\ 6,\ 12 なので

wˉ=0+3+6+124=214=5.25.\bar{w} = \frac{0+3+6+12}{4} = \frac{21}{4} = 5.25 .

定理 4.1 の通り、これが最小です。

ラウンドロビン(q=4q = 4:実行の様子を追うと、[0,4)[0,4)P1P_1(残 20)、[4,7)[4,7)P2P_2 完了、[7,10)[7,10)P3P_3 完了、[10,14)[10,14)P4P_4(残 2)、[14,18)[14,18)P1P_1(残 16)、[18,20)[18,20)P4P_4 完了、以降 [20,36)[20,36)P1P_1 が完了します。完了時刻は c=(36,7,10,20)c = (36, 7, 10, 20) なので待ち時間は citic_i - t_i より 12, 4, 7, 1412,\ 4,\ 7,\ 14、平均は

wˉ=12+4+7+144=374=9.25.\bar{w} = \frac{12+4+7+14}{4} = \frac{37}{4} = 9.25 .

応答時間は 0, 4, 7, 100,\ 4,\ 7,\ 10 で平均 5.255.25。平均待ち時間では SJF に負けますが、応答時間は FCFS の 20.2520.25 から 5.255.25 へ約 4 分の 1 に縮んでいます。対話的なシステムがラウンドロビン系を選ぶ理由がここにあります。

CPU の次はメモリです。複数のプロセスを同時にメモリに置くとき、素朴に「プロセス A は物理番地 0x1000 から、B は 0x9000 から」と割り当てると、3 つの問題が起きます。(i) A が番地を踏み外すと B を壊す。(ii) A をコンパイルする時点で配置先を知っていなければならない。(iii) 物理メモリより大きなプログラムが動かせない。

これらを一度に解くのがアドレス変換です。

定義 5.1ページングによるアドレス変換

仮想アドレス空間と物理アドレス空間を、同じ大きさ 2p2^{p} バイトのページフレームに分割する(典型的には 212=42^{12} = 4 KiB)。bb ビットの仮想アドレス vv

v=v/2p仮想ページ番号 VPN2p+(vmod2p)オフセットv = \underbrace{\lfloor v / 2^{p} \rfloor}_{\text{仮想ページ番号 } \mathrm{VPN}} \cdot 2^{p} + \underbrace{(v \bmod 2^{p})}_{\text{オフセット}}

と分解し、プロセスごとに用意されたページテーブル TT(VPN から物理フレーム番号 PFN への部分写像)を使って物理アドレスを

phys(v)=T(VPN)2p+(vmod2p)\mathrm{phys}(v) = T(\mathrm{VPN}) \cdot 2^{p} + (v \bmod 2^{p})

で定める。各ページテーブル項目(PTE)は PFN に加えて、有効ビット、読み書き実行の許可ビット、ユーザモードからのアクセス可否、参照済みビット、変更済みビットを持つ。TT が VPN に対して未定義(有効ビットが 00)のアドレスに触れると、CPU はページフォルト例外を起こしてカーネルに制御を移す。この変換はメモリ管理ユニット(MMU)がハードウェアで行い、ページテーブルの先頭物理アドレスは特権レジスタ(x86-64 の CR3、RISC-V の satp)が保持する。

オフセットが変換されないのが要点です。ページ内の連続性は保たれ、変換の単位はページ単位で済みます。

仮想ページ番号 VPN(上位 20 bit)オフセット(下位 12 bit)ページテーブル T:VPN → PFN+許可ビット・有効ビット物理フレーム番号 PFNオフセット(そのまま)仮想アドレス(32 bit)物理アドレス変換されない
1 段ページテーブルによるアドレス変換。オフセットはそのまま通り抜ける

例 5.2アドレス変換を最後まで計算する

32 ビット仮想アドレス、ページサイズ 4 KiB(p=12p = 12)とします。仮想アドレス v=0x00403ABCv = \mathtt{0x00403ABC} を変換します。

オフセットは下位 12 ビット、つまり 16 進で下 3 桁なので 0xABC=2748\mathtt{0xABC} = 2748 です。仮想ページ番号は

VPN=0x00403ABC/212=0x00403=1027.\mathrm{VPN} = \lfloor \mathtt{0x00403ABC} / 2^{12} \rfloor = \mathtt{0x00403} = 1027 .

ページテーブルの第 10271027 項を引いたところ、有効ビットが 11、PFN が 0x1F2=498\mathtt{0x1F2} = 498、書き込み許可ありだったとします。すると物理アドレスは

phys(v)=4984096+2748=2039808+2748=2042556=0x001F2ABC.\mathrm{phys}(v) = 498 \cdot 4096 + 2748 = 2039808 + 2748 = 2042556 = \mathtt{0x001F2ABC} .

16 進で見ると、上位 5 桁が 00403001F2\mathtt{00403} \to \mathtt{001F2} に置き換わり、下 3 桁 ABC\mathtt{ABC} はそのまま残っていることが確認できます。

この仕組みが冒頭の 3 つの問題を解きます。(i) ページテーブルに載っていない物理フレームには物理的に触れられないので、プロセスは互いに隔離されます。(ii) すべてのプロセスが同じ仮想アドレスから始めてよいので、配置先を知らずにコンパイルできます。(iii) 有効ビットを 00 にしてページの内容をディスクに退避しておき、アクセス時のページフォルトで読み戻せば、物理メモリより大きな仮想アドレス空間が使えます。これが仮想記憶です。

5.1. ページテーブルが大きすぎる問題

Section titled “5.1. ページテーブルが大きすぎる問題”

定義 5.1 をそのまま実装すると容量が破綻します。32 ビット・4 KiB ページなら VPN は 20 ビットなので 2202^{20} 項、1 項 4 バイトとして 44 MiB。これをプロセスごとに常駐させる必要があり、100 プロセスで 400 MiB です。64 ビットではまったく不可能です。

解決は、ページテーブル自身をページングすることです。VPN をさらに分割し、木構造にします。

命題 5.3

32 ビット仮想アドレス、ページサイズ 4 KiB、PTE 4 バイトの 2 段ページテーブルを考える。仮想アドレスを上位 10 ビット(1 段目の添字)、次の 10 ビット(2 段目の添字)、下位 12 ビット(オフセット)に分割する。あるプロセスが実際に使っている仮想ページの集合を SS とし、SS に属するページの 1 段目添字の集合を I={VPN/210:VPNS}I = \{\lfloor \mathrm{VPN}/2^{10}\rfloor : \mathrm{VPN} \in S\} とする。このとき、そのプロセスのページテーブルが占める容量はちょうど (1+I)×4 KiB(1 + |I|) \times 4\ \mathrm{KiB} である。

証明(命題 5.3)

1 段目の表は 210=10242^{10} = 1024 項、各 4 バイトなので 1024×4=40961024 \times 4 = 4096 バイト =4= 4 KiB。ちょうど 1 ページに収まり、これは常に 1 つ必要です。

2 段目の表も同様に 1024 項 ×\times 4 バイト == 4 KiB です。2 段目の表は 1 段目の添字ごとに 1 つ用意されますが、その添字を持つ仮想ページが 1 つも使われていなければ、1 段目の該当項の有効ビットを 00 にしておけばよく、表そのものを確保する必要がありません。逆に、その添字を持つ仮想ページが 1 つでも使われていれば表を 1 つ確保します。したがって確保される 2 段目の表の個数はちょうど I|I| 個です。合計 (1+I)×4(1 + |I|) \times 4 KiB。

例 5.4実際のプロセスで容量を見積もる

典型的な小さなプロセスが、コード・データ領域を 0x00400000\mathtt{0x00400000} 付近に、スタックを 0xBFFFF000\mathtt{0xBFFFF000} 付近に持つとします。命題 5.3 の 1 段目添字は仮想アドレスを 222=42^{22} = 4 MiB で割った商です。

0x004000004194304=41943044194304=1,0xBFFFF0004194304=32212213764194304=767.\left\lfloor \frac{\mathtt{0x00400000}}{4194304} \right\rfloor = \frac{4194304}{4194304} = 1, \qquad \left\lfloor \frac{\mathtt{0xBFFFF000}}{4194304} \right\rfloor = \left\lfloor \frac{3221221376}{4194304} \right\rfloor = 767 .

(後者は 767×4194304=3217031168767 \times 4194304 = 3217031168768×4194304=3221225472>3221221376768 \times 4194304 = 3221225472 > 3221221376 から確かめられます。)よって I={1,767}I = \{1, 767\}I=2|I| = 2 で、ページテーブルの容量は

(1+2)×4 KiB=12 KiB.(1 + 2) \times 4\ \mathrm{KiB} = 12\ \mathrm{KiB} .

1 段構成の 44 MiB に対して約 1/3411/341 です。x86-64 は同じ発想を 4 段(9+9+9+9+12=489+9+9+9+12 = 48 ビット)に拡張し、RISC-V の Sv39 は 3 段(9+9+9+12=399+9+9+12 = 39 ビット)を使います。

段数を増やすと容量は減りますが、1 回のメモリアクセスのために表を段数回たどることになります。4 段なら、データ 1 回のアクセスに合計 5 回のメモリアクセスです。これでは 5 倍遅くなります。

対策は変換結果のキャッシュで、これを TLB(Translation Lookaside Buffer)と呼びます。TLB は VPN から PFN への最近の対応を数十〜数千項保持する連想メモリで、MMU の中にあります。

命題 5.5

TLB の参照に時間 ε\varepsilon、主記憶の 1 回のアクセスに時間 mm かかるとする。ページテーブルは 1 段とし、TLB のヒット率を hh0h10 \le h \le 1)、ページフォルトは起きないものとする。このとき、データ 1 語を読むまでの平均時間(実効アクセス時間)は

EAT=ε+m+(1h)m\mathrm{EAT} = \varepsilon + m + (1-h)\, m

である。

証明(命題 5.5)

どちらの場合も、まず TLB を引くので ε\varepsilon がかかります。

TLB ヒットのとき(確率 hh)、PFN が即座に得られるので、あとは目的のデータを主記憶から読む 1 回だけで、合計 ε+m\varepsilon + m です。

TLB ミスのとき(確率 1h1-h)、ページテーブルを主記憶から読む 1 回(mm)と、目的のデータを読む 1 回(mm)が必要で、合計 ε+2m\varepsilon + 2m です。

期待値を取ると

EAT=h(ε+m)+(1h)(ε+2m)=ε+m[h+2(1h)]=ε+m(2h)=ε+m+(1h)m.\mathrm{EAT} = h(\varepsilon + m) + (1-h)(\varepsilon + 2m) = \varepsilon + m\bigl[h + 2(1-h)\bigr] = \varepsilon + m(2 - h) = \varepsilon + m + (1-h)m .

例 5.6TLB が効く理由を数値で見る

ε=1\varepsilon = 1 ns、m=100m = 100 ns とします。TLB が無ければ 1 段でも 2m=2002m = 200 ns、つまりアクセスのたびに 2 倍の時間がかかります。

ヒット率 h=0.98h = 0.98 なら 命題 5.5 より

EAT=1+100+0.02×100=103 ns,\mathrm{EAT} = 1 + 100 + 0.02 \times 100 = 103\ \mathrm{ns},

つまり変換の追加費用はわずか 3%3\% です。h=0.90h = 0.90 に落ちると EAT=1+100+10=111\mathrm{EAT} = 1 + 100 + 10 = 111 ns(11%11\% 増)。hh が数 % 変わるだけで体感が変わるので、TLB を溢れさせないことは性能上きわめて重要です。大きなページ(2 MiB の huge page)が使われるのは、同じ TLB 項数でカバーできるメモリ量を 512 倍にするためです。

なお、コンテキストスイッチ(定義 3.2)はページテーブルを切り替えるので、素朴には TLB を全部無効化する必要があります。現代の CPU が TLB 項にアドレス空間識別子(ASID/PCID)を持たせているのは、この一括無効化を避けるためです。

物理メモリが足りなくなると、どのページをディスクに追い出すかを決めなければなりません。理論上の最適解は「次に使われるのが最も遠い先のページを追い出す」(Belady の最適アルゴリズム)ですが、未来が必要なので実装できません。実用の候補は FIFO(最も古く読み込んだものを追い出す)と LRU(最後に参照されたのが最も古いものを追い出す)です。

直感的には「フレーム数を増やせばページフォルトは減る」と思えます。しかし FIFO ではそうなりません。

例 5.7Belady の異常:FIFO はメモリを増やすと遅くなることがある

参照列 σ=1,2,3,4,1,2,5,1,2,3,4,5\sigma = 1,2,3,4,1,2,5,1,2,3,4,5 を、最初は空のメモリに対して FIFO で処理します。

フレーム 3 個(角括弧は読み込んだ順、左が最古):

参照123412512345
判定FFFFFFFHHFFH

44 で最古の 11 を追い出して [2,3,4][2,3,4]、次の 1122 を追い出して [3,4,1][3,4,1]、次の 2233 を追い出して [4,1,2][4,1,2]5544 を追い出して [1,2,5][1,2,5]。ここで 1122 が連続ヒットします。その後 3311 を追い出して [2,5,3][2,5,3]4422 を追い出して [5,3,4][5,3,4]、最後の 55 はヒット。フォルトは 9 回です。

フレーム 4 個

参照123412512345
判定FFFFHHFFFFFF

最初の 4 つで [1,2,3,4][1,2,3,4] が埋まり、1,21, 2 はヒットします。しかし 55 で最古の 11 を追い出した瞬間から歯車が狂い、以降は追い出したばかりのものを次に要求する、という状態が続いて 77 回目以降すべてフォルトします。フォルトは 10 回

フレームを 1 個増やしたのにフォルトが 9 回から 10 回に増えました。これが Belady の異常です。原因は、FIFO の「メモリ内容」がフレーム数について包含関係を持たないことにあります。

定理 5.8LRU はスタックアルゴリズムであり Belady の異常を起こさない

デマンドページング(参照されたページだけを読み込み、最初メモリは空とする)のもとで LRU を用いる。参照列 σ=σ(1)σ(2)\sigma = \sigma(1)\sigma(2)\cdots に対し、フレーム数 mmtt 回目の参照を処理した直後にメモリにあるページの集合を Sm(t)S_m(t)tt 回目までのページフォルト回数を Fm(t)F_m(t) と書く。このとき、すべての m1m \ge 1 とすべての t0t \ge 0 に対し

Sm(t)Sm+1(t)S_m(t) \subseteq S_{m+1}(t)

が成り立ち、その帰結としてすべての ttFm+1(t)Fm(t)F_{m+1}(t) \le F_m(t) が成り立つ。

証明(定理 5.8)

第 1 段:Sm(t)S_m(t) の特徴づけ。 σ(1),,σ(t)\sigma(1),\ldots,\sigma(t) に現れる相異なるページを、最後に参照された時刻の遅い順(新しい順)に並べたものを q1(t),q2(t),,qdt(t)q_1(t), q_2(t), \ldots, q_{d_t}(t) とします(dtd_t は相異なるページ数)。主張は

Sm(t)={q1(t),,qmin(m,dt)(t)}S_m(t) = \{ q_1(t), \ldots, q_{\min(m, d_t)}(t) \}

です。これを tt についての帰納法で示します。

t=0t = 0 のときメモリは空で d0=0d_0 = 0、両辺とも空集合です。

tt で成り立つとし、p:=σ(t+1)p := \sigma(t+1) を参照します。参照後の新しい順序 q(t+1)q_\bullet(t+1) は、pp が先頭に来て、それ以外のページの相対順序は変わりません。3 つの場合に分けます。

(a) pSm(t)p \in S_m(t) のとき。ヒットなのでメモリの中身は変わらず Sm(t+1)=Sm(t)S_m(t+1) = S_m(t)。一方、帰納法の仮定より pp は新しい順で min(m,dt)\min(m,d_t) 位以内にいたので、pp を先頭に移しても上位 min(m,dt)\min(m,d_t) 個の集合は変わりません。また dt+1=dtd_{t+1} = d_t。よって主張は保たれます。

(b) pSm(t)p \notin S_m(t) かつ Sm(t)<m|S_m(t)| < m のとき。帰納法の仮定より Sm(t)=dt<m|S_m(t)| = d_t < m、つまりメモリには過去に現れた相異なるページがすべて入っているので、pp は初出です。フォルトして pp を空きフレームに入れるので Sm(t+1)=Sm(t){p}S_m(t+1) = S_m(t) \cup \{p\}。他方 dt+1=dt+1md_{t+1} = d_t + 1 \le m で、上位 min(m,dt+1)=dt+1\min(m, d_{t+1}) = d_{t+1} 個は過去に現れた全ページに pp を加えたものです。一致します。

(c) pSm(t)p \notin S_m(t) かつ Sm(t)=m|S_m(t)| = m のとき。フォルトし、LRU は最後の参照が最も古いページ、すなわち帰納法の仮定より qm(t)q_m(t) を追い出して pp を入れます。よって

Sm(t+1)=({q1(t),,qm(t)}{qm(t)}){p}={p,q1(t),,qm1(t)}.S_m(t+1) = \bigl(\{q_1(t),\ldots,q_m(t)\} \setminus \{q_m(t)\}\bigr) \cup \{p\} = \{p, q_1(t), \ldots, q_{m-1}(t)\}.

一方、新しい順序では pp が 1 位、q1(t),,qm1(t)q_1(t),\ldots,q_{m-1}(t) が順に 2 位から mm 位に繰り下がるので、上位 mm 個はまさにこの集合です。一致します。

以上で第 1 段が示せました。

第 2 段:包含関係。 第 1 段より Sm(t)S_m(t) は「新しい順で上位 min(m,dt)\min(m,d_t) 個」、Sm+1(t)S_{m+1}(t) は「上位 min(m+1,dt)\min(m+1,d_t) 個」です。min(m,dt)min(m+1,dt)\min(m,d_t) \le \min(m+1,d_t) で、どちらも同じ順序 q(t)q_\bullet(t) の先頭からの切り出しなので Sm(t)Sm+1(t)S_m(t) \subseteq S_{m+1}(t)

第 3 段:フォルト回数の単調性。 t+1t+1 回目の参照 p=σ(t+1)p = \sigma(t+1)mm フレームでヒットする、すなわち pSm(t)p \in S_m(t) ならば、第 2 段より pSm+1(t)p \in S_{m+1}(t) なので m+1m+1 フレームでもヒットします。対偶を取れば「m+1m+1 フレームでフォルトするなら mm フレームでもフォルトする」。したがって各時刻でのフォルトの指示関数について 1[m+1 でフォルト]1[m でフォルト]\mathbf{1}[m+1 \text{ でフォルト}] \le \mathbf{1}[m \text{ でフォルト}] が成り立ち、tt まで足し合わせて Fm+1(t)Fm(t)F_{m+1}(t) \le F_m(t) を得ます。

第 1 段の (c) が、FIFO では成り立たない部分です。FIFO は追い出す対象を「読み込んだ順」で選ぶため、メモリの中身が「参照の新しさ順の上位 mm 個」という mm について入れ子になる形にならず、包含関係が壊れます。例 5.7 はその具体的な現れです。

6. 同時実行の安全性:相互排除とデッドロック

Section titled “6. 同時実行の安全性:相互排除とデッドロック”

プロセスやスレッドが同じデータを触り始めると、新しい種類の誤りが生まれます。

定義 6.1クリティカルセクションと相互排除

複数の実行主体が共有する状態を読み書きするコード領域をクリティカルセクションという。任意の時刻に高々 1 つの実行主体しかクリティカルセクションを実行していないことを保証する性質を相互排除(mutual exclusion)という。相互排除に加えて、(i) クリティカルセクションに入りたい主体が存在し、中に誰もいなければ有限時間内に誰かが入れる(進行性)、(ii) 入りたい主体が無限に待たされない(有限待ち)、が満たされるとき、その排他機構は正しいという。

たとえば counter = counter + 1 は、翻訳器(定義 3.1[プログラミング言語論])が生成する機械語では「読む・足す・書く」の 3 命令であり、2 つのスレッドが交互に実行すると更新が 1 回失われます。この 3 命令をクリティカルセクションとして守る必要があります。OS はそのためにミューテックスやセマフォを提供し、その実装には CPU のアトミック命令(compare-and-swap など)と、待ち状態のプロセスをスケジューラから外す仕組みを使います。

排他機構を導入すると、今度は互いに待ち合って進まなくなる状態が生じます。

定義 6.2デッドロック

プロセスの集合 DD \ne \emptysetデッドロックしているとは、DD のどのプロセスも、DD の別のプロセスが保持している資源の解放を待っており、その解放が起こり得ない状態をいう。待ちグラフ(wait-for graph)を、頂点をプロセス、辺 PQP \to Q を「PP が要求している資源を QQ が保持している」と定めた有向グラフとする。

補題 6.3

有限有向グラフ GG のすべての頂点の出次数が 11 以上ならば、GG は有向閉路を含む。

証明(補題 6.3)

頂点を 1 つ選んで v0v_0 とします。すべての頂点の出次数が 11 以上なので、v0v_0 から出る辺を 1 本選んで行き先を v1v_1、同様に v1v_1 から v2v_2、と無限に歩き続けられます。GG の頂点数は有限の NN 個なので、鳩の巣原理より v0,v1,,vNv_0, v_1, \ldots, v_NN+1N+1 個の中に同じ頂点が 2 度現れます。すなわち i<ji < jvi=vjv_i = v_j となる組があります。このとき vivi+1vj=viv_i \to v_{i+1} \to \cdots \to v_j = v_i は有向閉路です。

定理 6.4デッドロックの 4 必要条件(Coffman 条件)

デッドロック(定義 6.2)が発生しているならば、次の 4 条件がすべて成り立っている。

  1. 相互排除:少なくとも 1 種類の資源は、同時に 1 つのプロセスしか保持できない。
  2. 保持と待ち:あるプロセスが少なくとも 1 つの資源を保持したまま、別の資源の解放を待っている。
  3. 横取り不可:資源は保持しているプロセスが自発的に解放するまで、外部から取り上げられない。
  4. 循環待ち:プロセスの列 P1,P2,,PkP_1, P_2, \ldots, P_kk2k \ge 2)が存在し、各 ii について PiP_iPi+1P_{i+1} が保持する資源を待っている(添字は mod k\bmod\ k、すなわち PkP_kP1P_1 を待つ)。
証明(定理 6.4)

デッドロックしているプロセスの集合を DD とします。

1 について。 もしすべての資源が任意個のプロセスに同時に保持できるなら、資源の要求は常に即座に満たされるので、どのプロセスも待ち状態になりません。これは DD の各プロセスが待っているという 定義 6.2 に反します。よって少なくとも 1 種類の資源は排他的です。

2 について。 定義 6.2 より DD の各プロセス PP は他のプロセスの資源解放を待っています。もし PP が何も保持していないなら、PP が待っている相手 QDQ \in D を考えます。QQ もまた誰かを待っており、その連鎖をたどると(第 4 項で示すように)閉路が生じますが、閉路上のプロセスはすべて「他から待たれている」ので資源を保持しています。よって DD の中に、資源を保持しながら待っているプロセスが必ず存在します。

3 について。 資源を横取りできるなら、待っているプロセスに資源を割り当てることで待ちを解消できます。すると「解放が起こり得ない」という 定義 6.2 の条件が成り立たなくなります。よって横取り不可です。

4 について。 DD の要素を頂点とする待ちグラフの誘導部分グラフ GG を考えます。定義 6.2 より、DD の各プロセスは DD の別のプロセスが保持する資源を待っています。すなわち GG のすべての頂点の出次数は 11 以上です。DD は有限集合なので、補題 6.3 より GG は有向閉路 P1P2PkP1P_1 \to P_2 \to \cdots \to P_k \to P_1 を含みます。辺 PiPi+1P_i \to P_{i+1} の定義がまさに「PiP_iPi+1P_{i+1} が保持する資源を待つ」なので、これが循環待ちです。なお閉路の長さは 22 以上です(自分が保持している資源を自分で待つことは、同一資源の再帰的取得を許さない通常のミューテックスでは循環待ちの一種として同様に扱えます)。

定理 6.4 は「4 条件が必要」と言っているので、対偶としてどれか 1 つを成り立たなくすればデッドロックは起きません。実務での対策はこの 4 つのどれを崩すかで分類できます。

崩す条件手法代償
相互排除読み取り専用資源、ロックフリーデータ構造適用できる資源が限られる
保持と待ち必要な資源を最初に一括取得する資源の利用効率が落ちる、事前に全要求を知る必要がある
横取り不可取得に失敗したら保持中の資源を手放してやり直すライブロックの危険、処理のやり直し費用
循環待ち資源に全順序を定め、常に昇順に取得する設計時に全順序を定める規律が要る

最後の行が実装上もっとも使われる方法で、Linux カーネルのロック順序の規約はまさにこれです。正しさは 演習 7.4 で証明してもらいます。

演習 7.1

32 ビットの仮想アドレス空間、ページサイズ 8 KiB、1 つのページテーブル項目が 4 バイトの計算機を考えます。1 段のページテーブルを使うとき、1 プロセスあたりのページテーブルの容量を求めてください。また、ページサイズを 4 KiB にしたときと比べて何倍になるかを述べてください。

解答

8 KiB=2138\ \mathrm{KiB} = 2^{13} なのでオフセットは 13 ビット、仮想ページ番号は 3213=1932 - 13 = 19 ビットです。項数は 219=5242882^{19} = 524288、1 項 4 バイトなので

219×4=221=2097152 バイト=2 MiB.2^{19} \times 4 = 2^{21} = 2097152\ \text{バイト} = 2\ \mathrm{MiB}.

ページサイズ 4 KiB のときは VPN が 20 ビットで 220×4=42^{20} \times 4 = 4 MiB でした。したがって 2 MiB/4 MiB=1/22\ \mathrm{MiB} / 4\ \mathrm{MiB} = 1/2 倍、つまり半分になります。

ページを大きくするとページテーブルは小さくなりますが、代わりに 1 ページ内の使われない部分(内部断片化)が平均してページサイズの半分だけ増えます。この綱引きが、ページサイズが 4 KiB 前後に落ち着いている理由です。

演習 7.2標準

時刻 00 に 4 つのプロセスが到着し、バースト時間が t1=8, t2=4, t3=9, t4=5t_1 = 8,\ t_2 = 4,\ t_3 = 9,\ t_4 = 5 であるとします。切り替え費用は 00 とします。(a) FCFS(到着順)、(b) SJF、(c) ラウンドロビン(q=4q = 4、キューの初期順序は P1,P2,P3,P4P_1, P_2, P_3, P_4、クォンタムを使い切ったプロセスはキューの末尾に戻る)のそれぞれについて平均待ち時間を求め、(c) が (a) より悪くなり得ることを確認してください。

解答

(a) FCFS:実行順は P1,P2,P3,P4P_1, P_2, P_3, P_4。待ち時間は 0, 8, 12, 210,\ 8,\ 12,\ 21 なので

wˉ=0+8+12+214=414=10.25.\bar{w} = \frac{0+8+12+21}{4} = \frac{41}{4} = 10.25 .

(b) SJF:バースト昇順は P2(4),P4(5),P1(8),P3(9)P_2(4), P_4(5), P_1(8), P_3(9)。待ち時間は 0, 4, 9, 170,\ 4,\ 9,\ 17 なので

wˉ=0+4+9+174=304=7.5.\bar{w} = \frac{0+4+9+17}{4} = \frac{30}{4} = 7.5 .

定理 4.1 よりこれが最小です。

(c) ラウンドロビン q=4q=4:実行を順に追います。

区間実行残り実行後のキュー
[0,4)[0,4)P1P_14P2,P3,P4,P1P_2, P_3, P_4, P_1
[4,8)[4,8)P2P_20(完了)P3,P4,P1P_3, P_4, P_1
[8,12)[8,12)P3P_35P4,P1,P3P_4, P_1, P_3
[12,16)[12,16)P4P_41P1,P3,P4P_1, P_3, P_4
[16,20)[16,20)P1P_10(完了)P3,P4P_3, P_4
[20,24)[20,24)P3P_31P4,P3P_4, P_3
[24,25)[24,25)P4P_40(完了)P3P_3
[25,26)[25,26)P3P_30(完了)

完了時刻は c=(20,8,26,25)c = (20, 8, 26, 25)。到着が 00 なので待ち時間は citic_i - t_i より 12, 4, 17, 2012,\ 4,\ 17,\ 20

wˉ=12+4+17+204=534=13.25.\bar{w} = \frac{12+4+17+20}{4} = \frac{53}{4} = 13.25 .

FCFS の 10.2510.25 より悪くなりました。バースト時間が近い値ばかりの場合、ラウンドロビンは全員の完了を一様に後ろへずらすだけで、平均待ち時間には不利に働きます。一方で応答時間は FCFS の (0+8+12+21)/4=10.25(0+8+12+21)/4 = 10.25 に対し (0+4+8+12)/4=6(0+4+8+12)/4 = 6 に改善しており、命題 4.3 の趣旨どおりです。

演習 7.3標準

例 5.7 と同じ参照列 σ=1,2,3,4,1,2,5,1,2,3,4,5\sigma = 1,2,3,4,1,2,5,1,2,3,4,5 を、今度は LRU で処理してください。フレーム 3 個の場合とフレーム 4 個の場合のページフォルト回数をそれぞれ求め、定理 5.8 と整合することを確かめてください。

解答

フレーム 3 個(集合の中身を新しい順で書きます)。

11:F、[1][1]22:F、[2,1][2,1]33:F、[3,2,1][3,2,1]44:F、最も古い 11 を追い出して [4,3,2][4,3,2]11:F、22 を追い出して [1,4,3][1,4,3]22:F、33 を追い出して [2,1,4][2,1,4]55:F、44 を追い出して [5,2,1][5,2,1]11:H、[1,5,2][1,5,2]22:H、[2,1,5][2,1,5]33:F、55 を追い出して [3,2,1][3,2,1]44:F、11 を追い出して [4,3,2][4,3,2]55:F、22 を追い出して [5,4,3][5,4,3]

フォルトは 10 回です。

フレーム 4 個

1,2,3,41,2,3,4:4 回とも F、[4,3,2,1][4,3,2,1]11:H、[1,4,3,2][1,4,3,2]22:H、[2,1,4,3][2,1,4,3]55:F、最も古い 33 を追い出して [5,2,1,4][5,2,1,4]11:H、[1,5,2,4][1,5,2,4]22:H、[2,1,5,4][2,1,5,4]33:F、44 を追い出して [3,2,1,5][3,2,1,5]44:F、55 を追い出して [4,3,2,1][4,3,2,1]55:F、11 を追い出して [5,4,3,2][5,4,3,2]

フォルトは 8 回です。

8108 \le 10 なのでフレームを増やすとフォルトは減っており、定理 5.8Fm+1FmF_{m+1} \le F_m と整合します。さらに各時点でのメモリ内容を比べると、たとえば 7 回目(55 の参照後)は 3 フレームで {5,2,1}\{5,2,1\}、4 フレームで {5,2,1,4}\{5,2,1,4\} となっており、包含関係 S3S4S_3 \subseteq S_4 も確かめられます。

演習 7.4

資源の全体集合 RR に全順序 \prec を定め、すべてのプロセスが次の規律に従うとします:あるプロセスが資源 rr を要求するとき、そのプロセスが現在保持しているどの資源 rr' についても rrr' \prec r である(つまり資源は必ず \prec の昇順にしか取得しない)。このとき、デッドロックは決して発生しないことを証明してください。

解答

背理法で示します。デッドロックが発生したとすると、定理 6.4 の条件 4 より、プロセスの列 P1,P2,,PkP_1, P_2, \ldots, P_kk2k \ge 2)が存在して、各 ii について PiP_iPi+1P_{i+1} が保持している資源を待っています(添字は mod k\bmod\ k)。

PiP_i が待っている資源を ri+1r_{i+1} と書きます(これは Pi+1P_{i+1} が保持しています)。添字を合わせると、各 ii について次の 2 つが同時に成り立ちます。

  • PiP_i は資源 ri+1r_{i+1} を要求している。
  • PiP_i は資源 rir_i を保持している(rir_iPi1P_{i-1} が待っている資源であり、PiP_i が保持しているものです)。

規律より、要求する資源は保持している資源より \prec で真に大きいので

riri+1(i=1,2,,k, 添字はmodk).r_i \prec r_{i+1} \qquad (i = 1, 2, \ldots, k,\ \text{添字は} \bmod k).

これを i=1i = 1 から順に連ねると

r1r2rkr1r_1 \prec r_2 \prec \cdots \prec r_k \prec r_1

となり、\prec の推移律から r1r1r_1 \prec r_1 を得ます。\prec は全順序なので反射的でなく(rrr \prec r は成り立たない)、矛盾です。

よってデッドロックは発生しません。

補足:この論法は 補題 6.3 が保証する閉路の存在を、\prec という「一方向にしか進めない量」の存在と衝突させています。同じ構造は、有限の半順序集合に無限下降列が存在しないことを使う議論一般に現れます。実務ではこの \prec を「ロックの取得順序の規約」として文書化し、Linux カーネルの lockdep のような検証機構で違反を実行時に検出します。

  • R. H. Arpaci-Dusseau, A. C. Arpaci-Dusseau, Operating Systems: Three Easy Pieces, Arpaci-Dusseau Books, 2018 — 仮想化(プロセス・スケジューリング・ページング)と並行性の章。全文が 公式サイト で公開されています。
  • A. Silberschatz, P. B. Galvin, G. Gagne, Operating System Concepts, 10th ed., Wiley, 2018 — プロセスとスレッド、CPU スケジューリング、メインメモリと仮想記憶、デッドロックの各章。
  • A. S. Tanenbaum, H. Bos, Modern Operating Systems, 4th ed., Pearson, 2015 — プロセスとスレッド、メモリ管理の章。
  • L. A. Belady, R. A. Nelson, G. S. Shedler, “An anomaly in space-time characteristics of certain programs running in a paging machine”, Communications of the ACM 12 (1969), 349–353. doi:10.1145/363011.363155
  • E. G. Coffman, M. J. Elphick, A. Shoshani, “System Deadlocks”, ACM Computing Surveys 3 (1971), 67–78. doi:10.1145/356586.356588
  • A. Waterman, K. Asanović (eds.), The RISC-V Instruction Set Manual, Volume II: Privileged Architecture — 特権モード(M/S/U)、ecallsretsatp レジスタと Sv39 のページテーブル形式。RISC-V 仕様ページ

x86-64 の場合。 歴史的経緯から 4 つの特権レベル(リング 0〜3)を持ちますが、実際に使われるのはリング 0(カーネル)とリング 3(ユーザ)の 2 つだけです。システムコールは syscall 命令で、復帰は sysret。ページテーブルの基底は CR3 レジスタが保持し、4 段(PML4 → PDPT → PD → PT)をたどって 48 ビットの仮想アドレスを変換します。仮想化支援(VT-x)が導入されてからは、リング 0 のさらに下に「ルートモード」が加わり、ハイパーバイザがゲスト OS のカーネルを管理できるようになりました。

RISC-V の場合。 設計が新しいぶん整理されており、マシンモード(M)、スーパーバイザモード(S)、ユーザモード(U)の 3 段階です。M モードはブートとファームウェアが使い、OS のカーネルは S モードで動きます。システムコールは ecall 命令で、U モードから実行すると S モードのトラップハンドラ(stvec レジスタが指す番地)へ飛びます。復帰は sret。ページテーブルの基底とアドレス変換方式は satp レジスタが一体で持ち、Sv39(3 段、39 ビット仮想アドレス)、Sv48(4 段)、Sv57(5 段)から選べます。仕様書が短く読み通せるので、この記事で扱った仕組みが実際のハードウェアでどう規定されているかを確かめるには、RISC-V の特権仕様が最良の教材だと思います。

なぜ 2〜3 段階で足りるのか。 特権レベルを細かく分けても、結局は「境界が破れないこと」だけが本質だからです。境界を 1 枚引ければ、その内側でさらに細かい保護をしたいときは、同じ仕組みを再帰的に適用する(仮想機械を作る、あるいはユーザ空間でサンドボックスを作る)ほうが単純で、検証もしやすくなります。次章の プログラミング言語論 では、この「境界を引く」という発想が言語の型システム(型健全性(系 5.5)[プログラミング言語論])という別の形でも現れることを見ます。

この記事の誤りを報告する ・運営: 夢現技研合同会社料金プラン利用条件特定商取引法に基づく表記

© 2026 夢現技研合同会社 ・本文の LLM への入力は自由です。コード例は MIT ライセンスです。