← OSと低レイヤのしくみ コース
8. ページフォルト — 「メモリが無い」ときにカーネルがすること
ページフォルトが起きてから解決されるまでの流れを図で追っていきましょう。
① 発端 — アドレス変換がPTEで止まる
実行中の命令ld a0, 0(t0)
t0 は仮想アドレスsatp が指すページテーブルを歩く
PTE を読むa + va.vpn[i] × PTESIZE
PTE の中身を判定
V = 0無効なPTE
R/W/X 不足権限が足りない
OK変換成立
左の2つはここへ
ページフォルト例外変換は完了しない
- ・PTEのビット0がV(valid)。V=0なら残りのビットはdon't-careで、ソフトウェアが自由に使えると規定されています
- ・ビット1がR、ビット2がW、ビット3がX。R/W/Xが3つとも0のPTEは葉ではなく次段テーブルへのポインタです
- ・書き込み可能なページは読み込み可能でもある必要があり、W=1かつR=0は将来のために予約された組み合わせです
CPUは仮想アドレスをページテーブルで変換します。途中のPTEでV=0だったり、要求した操作に対する権限ビットが無かったりすると、変換はそこで打ち切られ、ページフォルト例外になります。
② 例外の正体 — どの操作で失敗したかが原因コードになる
何をしようとしたか
操作の種類ごとに別コード
命令フェッチcause = 12
Instruction page faultロードcause = 13
Load page faultストア/AMOcause = 15
Store/AMO page fault同時に記録される
stval / mtvalフォルトした仮想アドレス
sepc / mepc止まった命令のアドレス
- ・原因コード12/13/15はページフォルト。物理メモリに届かないアクセスフォルト(1/5/7)とは別物です
- ・ページフォルト例外でstvalに非ゼロ値が書かれる場合、そこにはフォルトした仮想アドレスが入ると規定されています
- ・Linuxのarch/riscv/mm/fault.cはEXC_STORE_PAGE_FAULTならFAULT_FLAG_WRITEを立てて先へ渡します
ページフォルトは1種類ではなく、命令フェッチ・ロード・ストアの3つに分かれています。カーネルはこの番号だけで「読もうとしたのか書こうとしたのか」を判別できます。
③ なぜやり直せるのか — epcは「次」ではなく「停止地点」
0x1000add
0x1004ld a0, 0(t0)
ここでフォルト0x1008addi
トラップ時に書かれる値
sepc = 0x1004フォルトした命令自身
0x1008 ではないカーネルが原因を解消してから sret
0x1004 を再実行今度は変換が成功する
- ・仕様では、S-modeへのトラップ時にsepcへ「中断された、または例外に遭遇した命令の仮想アドレス」が書かれると規定されています
- ・システムコール(ecall)のように命令を進めたい場合は、ハンドラ側がepcに4を足してから復帰します
- ・ページフォルトでは加算しません。加算しないことが「やり直し」そのものです
ここがページフォルトの肝です。epcには次の命令ではなく、例外を起こしたその命令のアドレスが入ります。だからカーネルが状況を直してそのまま戻れば、同じ命令が最初からやり直され、今度は成功します。
④ 分類 — カーネルはまずVMAを引く
handle_page_fault()arch/riscv/mm/fault.c
アドレスを含むVMAを探す
VMA が無い未マップ領域
VMA が有る登録済みの領域
VMAがある場合、権限を照合
ストアなのに VM_WRITE 無し許可されていない
権限は足りている正当なフォルト
正当なものだけが先へ
handle_mm_fault()実際にページを用意する
- ・access_error()はcauseごとにVM_EXEC / VM_READ|VM_WRITE / VM_WRITEを確認します
- ・権限が足りない場合はSEGV_ACCERR、VMAが無い場合はSEGV_MAPERRとして扱われます
- ・mmap(2)で作った領域はこのVMAとして登録されます
カーネルはフォルトアドレスがそのプロセスに登録された領域(VMA)に入っているかを調べます。VMAが無ければ即座に不正、あればVMAの許可と実際の操作を突き合わせます。
⑤ 正当なフォルトの代表的な3パターン
handle_pte_fault()mm/memory.c
PTEの状態と操作の種類で分岐
デマンドページングdo_anonymous_page()
初回アクセスCopy-on-Writedo_wp_page()
共有ページへの書き込みスワップインdo_swap_page()
退避済みを読み戻すどの経路でも最後は同じ
物理ページを確保し PTE を更新V=1 と適切な権限を書き込む
- ・MAP_ANONYMOUSの領域は内容がゼロで初期化されると規定されています
- ・MAP_PRIVATEはcopy-on-writeのマッピングで、変更は元ファイルに反映されません
- ・スワップインはディスクI/Oを伴うため、他の経路より大きく待たされます
正当なフォルトはさらに原因別に分かれます。いずれも「まだ物理ページが結び付いていない」状態を、必要になった今このタイミングで解消します。
⑥ 不正なら SIGSEGV
修復できないフォルトVMA無し / 権限違反
ユーザーモードからのアクセスなら
do_trap(SIGSEGV)SEGV_MAPERR または SEGV_ACCERR
シグナル配送
ハンドラ未登録既定動作で終了
sigaction(2) で登録済みハンドラが呼ばれる
- ・SIGSEGVはsi_addrにフォルトしたアドレスを埋めるため、ハンドラ側でどこを触ったか分かります
- ・si_codeはアドレス未マップならSEGV_MAPERR、権限違反ならSEGV_ACCERRになります
- ・カーネルモードで同じことが起きた場合はSIGSEGVではなくカーネル側の処理になります
VMAが無い、あるいは権限が足りないアクセスは修復のしようがありません。カーネルはページを用意せず、プロセスにSIGSEGVを送ります。ハンドラを登録していなければ既定動作でプロセスが終了します。
⑦ なぜこの設計なのか
もし即座に全部確保するならmmapした瞬間に物理ページが必要
実際には触られない領域が多い
使わないページの分まで浪費物理メモリが足りなくなる
フォルトで通知される設計にすると
触れた瞬間だけ確保デマンドページング
書くまで共有を維持Copy-on-Write
これが成立する前提
epc が停止地点を指すやり直しても意味が変わらない
- ・命令を再実行しても副作用が重複しないことが、遅延確保を安全にしています
- ・fork直後の親子が同じ物理ページを共有できるのも、書き込み時にフォルトで気付けるからです
- ・ページフォルトの回数はPERF_COUNT_SW_PAGE_FAULTSという名前のソフトウェアイベントとして計測できます
ページフォルトは「失敗の通知」ではなく「必要になった瞬間を知るための仕掛け」です。epcがやり直しを可能にしているからこそ、カーネルは遅延させる判断を安全に選べます。