ページフォルト
1. 概要
ページフォルトが発生した時の処理のまとめ。
2. 処理の流れ
ページフォルトは正常動作中以外にもカーネルやプロセスのバグにより不正なアドレスにアクセスした場合などにも発生する。ページフォルトハンドラでは例外発生要因となったアドレスからアクセスの正常/異常を判断して、各々の処理を行う。
ページフォルト発生時の処理の流れを図1に示す。
ページフォルトが発生するとCPU例外のエントリルーチンとして登録されているpage_fault()が呼び出される。page_fault()はC言語のエントリルーチンdo_page_fault()を呼び出す。
do_page_fault()は例外発生の要因となったメモリアクセスがカーネル空間に対するものかユーザ空間に対するものかで、まず処理が大きくわかれる。
カーネル空間(0xc0000000以降)へのアクセスだった場合、それがvmalloc領域へのアクセスであれば、対応する物理ページへのマップを作成してアクセスできるようにする。vmalloc領域でなければ、カーネルのバグなのでPanicする。
ユーザ空間へのアクセスだった場合は、そのアドレスがプロセスの正規に割り当てられているアドレス空間に収まっているかチェックをする。もしアドレス空間外であればプロセスが不正アドレスへアクセスしたことになるので、SIGSEGVを発行してプロセスをセグメンテーションフォルトさせる。また、アドレス空間内であっても、Read Onlyの領域にWriteアクセスをしたようなProtection不正の場合もSIGSEGVを発行する(プロセスが誤ってコードセグメントにデータを書こうした場合などがこのケースに該当する)。アクセスがアドレス空間内でProtectionも問題なければ、handle_mm_fault()を呼び出して、ページフォルトが発生した領域に対するページの割り当て処理へ進む。

handle_mm_fault()ではページフォルトの発生要因となった仮想アドレス空間に対応するPageTableがまだ存在しなければ、ここで作成してhandle_pte_fault()を呼び出す。
handle_pte_fault()はページフォルトが発生した場所の状況に応じて、do_xxx()ルーチンを呼び出してページの割り当てやマップを行い、プロセスがデータにアクセスできるようにする(図2)。

図2 ページフォルトの流れ2
3. 関連関数
3.1 page_fault()
ページフォルトのエントリルーチンC言語の関数do_page_fault()を呼び出す。
3.2 do_page_fault(regs, error_code)
ページフォルトが発生した要因を調べて、各処理に振り分ける。まず、例外発生要因となった仮想アドレスを取得して、カーネル空間/ユーザ空間のどちらで発生していたかを調べて処理を分ける。
カーネル空間だった場合はvmalloc領域への物理ページマッピングかカーネルのバグによるカーネルパニックにわかれる。
ユーザ空間へのアクセスでページフォルトが発生していた場合は、アドレスの妥当性(vma内か)をチェックして、アドレスが不正であればSIGSEGVを発行する。アクセスに問題なかったのであれば、このアドレス領域で本来アクセスできるデータにアクセスできるようにするため、handle_mm_fault()を呼び出す。
|
Bit |
意味 |
|---|---|
|
2^0 |
0 - ページが存在しなかった 1 - Protection Fault |
|
2^1 |
0 - Readアクセスだった 1 - Writeアクセスだった |
|
2^2 |
0 - カーネルモードだった 1 - ユーザーモードだった |
/* 例外発生仮想アドレス取得 */ address = read_cr2(); /* 例外発生アドレスがカーネル空間かチェック */ if (unlikely(address >= TASK_SIZE)) { /* カーネル空間(TASK_SIZE(0xC0000000)以降)で * 例外が発生していた */ /* カーネル空間でページが存在しない場合は、 * vmalloc()で確保した領域である可能性があるので * vmalloc_faultに飛ばす */ if (!(error_code & 5)) goto vmalloc_fault; goto bad_area_nosemaphore; } /* カレントプロセスのアドレス空間mmから * 仮想アドレスaddressに該当するvmaを取得 */ vma = find_vma(mm, address); if (!vma) goto bad_area; /* 仮想アドレス空間外だった */ if (vma->vm_start <= address) goto good_area; if (!(vma->vm_flags & VM_GROWSDOWN)) goto bad_area; if (error_code & 4) { if (address + 32 < regs->esp) goto bad_area; } if (expand_stack(vma, address)) goto bad_area; /* 以下、各ラベル毎に正常/異常処理がある */ good_area: <以下参照> survive: <以下参照> bad_area: <以下参照>
| (error_code & 3) |
意味 |
処理 |
|---|---|---|
|
0 |
Readしたらページがなかった |
アクセスしたアドレスのvmaがReadも実行もできない領域だったなら(*1)、bad_area:へ。 そうでなければ、survive:へ。 |
|
1 |
ReadしたらProtection Faultになった |
bad_area:へ |
|
2 |
Writeしたらページがなかった |
アクセスしたアドレスのvmaがWrite不可の領域だったら(*2)、bad_area:へ。 そうでなければ、survive:へ。 |
|
3 |
WriteしたらProtection Faultになった |
2と同じ。 |
(*2) !(vma->vm_flags & VM_WRITE) が真
survive: の処理内容
bad_area: の処理内容
vmalloc()をすると、カーネルのメモリマップであるinit_mmにのみPGD、PTEを設定する。このため、各プロセスのコンテキストでVMALLOC領域にアクセスした場合は、ページのマッピングがされていないため、ページフォルトが発生する。ページフォルト発生後、ここでマップを設定済みのinit_mmからPGDをコピーして物理ページへのマップを作成している。「カーネル空間のメモリマップ」参照。
/* 例外発生アドレスに該当するPage Directory内の * インデクスを求める */ int index = pgd_index(address); /* Page Directoryの物理アドレス */ pgd_paddr = read_cr3(); /* addressに該当するPage Directoryの * エントリへのポインタを取得 */ pgd = index + (pgd_t *)__va(pgd_paddr); /* マスターであるカーネルメモリマップの * Page Directoryのエントリへのポインタを取得 */ pgd_k = init_mm.pgd + index; /* マスターのPage Directoryにエントリが存在しなければ * addressはマップされていない(vmalloc()されていない)領域 * なのでno_contextへ */ if (!pgd_present(*pgd_k)) goto no_context; /* * 以下でマスターのPage Directoryの内容を * カレントプロセスのPage Directoryにコピーして * 物理ページをマッピングする */ /* i386系ではpudは存在しないので、pud_offset()は * pgdをそのまま返す */ pud = pud_offset(pgd, address); pud_k = pud_offset(pgd_k, address); if (!pud_present(*pud_k)) goto no_context; /* i386系ではpmdは存在しないので、pmd_offset()は * pudをそのまま返す */ pmd = pmd_offset(pud, address); pmd_k = pmd_offset(pud_k, address); if (!pmd_present(*pmd_k)) goto no_context; /* マスターのPage Directoryのエントリを * カレントプロセスのPage Directoryにコピーして * メモリマップを同期させる */ set_pmd(pmd, *pmd_k); /* addressに該当するPage Table Entryのアドレス */ pte_k = pte_offset_kernel(pmd_k, address); /* Page Table Entryがなかったら不正な領域への * アクセスなのでno_contextへ */ if (!pte_present(*pte_k)) goto no_context; return;
3.3 __handle_mm_fault(mm, vma, address, write_access)
handle_mm_fault()の処理の実体。
メモリマップ(mm)からaddressに該当するPage Table Entry(PTE)を求めてhandle_pte_fault()を呼び出す。
addressに該当するPage Tableがまだ存在しない場合は、ここで割り当てる (pte_alloc_map())。
3.4 handle_pte_fault()
ページフォルトの発生要因となったアドレスのPTEの内容を調べて、それに応じたハンドラルーチンを呼び出す。
old_entry = entry = *pte;
if (!pte_present(entry)) {
if (pte_none(entry)) {
/* ページが存在しない */
/* アドレス空間(vma)にNo Page時のハンドラが
* ないのならdo_anonymous_page()を呼び出して
* 匿名ページを割り当てる。
* ハンドラがあれば、do_no_page()へ。
* vma->vm_ops->nopageはdo_no_page()から
* 呼び出される */
if (!vma->vm_ops || !vma->vm_ops->nopage)
return do_anonymous_page(mm, vma, address,
pte, pmd, write_access);
return do_no_page(mm, vma, address,
pte, pmd, write_access);
}
/* pte_present()が偽でpte_none()も偽である状態は
* PTEのPresent bitは落ちているが、Present bit以外の
* 領域に値が書き込まれている状態。
* これには以下のケースがある。
*
* (1) ページアウトされている
* ページアウトされているアドレス空間のPTEには
* ページアウト先の情報が書き込まれている。
* (2) 非線型ファイルマッピングをしている領域
* この領域のPTEには以下のBitが立っている
* #define _PAGE_FILE 0x040
*/
if (pte_file(entry))
return do_file_page(mm, vma, address,
pte, pmd, write_access, entry);
return do_swap_page(mm, vma, address,
pte, pmd, write_access, entry);
}
/* ページが存在する */
if (write_access) {
/* Write不可のページにWriteした場合は.
* do_wp_page()を呼び出す */
if (!pte_write(entry))
return do_wp_page(mm, vma, address,
pte, pmd, ptl, entry);
entry = pte_mkdirty(entry);
}
:
:
