カーネルのページフォルト
Linuxのページング機構は移植性を考慮して、制御レジスタ(cr3)->ページグルーバルディレクトリ(pgd)->ページアッパディレクトリ(pud)->ページミドルディレクトリ(pmd)->ページテーブル(pte)の4段構えでアドレッシングを行っている。X86-32では2段構成のアーキテクチャで、与えられたテーブルディレクトリをそのまま返すことで実装している。
カーネルはvm_area_structを管理しながら、プロセスのメモリー管理をしている。そしてvm_area_structで管理される実メモリ(ページ)をページテーブルで管理されている。
プロセスが動作するには、mm_structが必要となる。mm_structにはcr3のアドレスも有している。しかしカーネルはこのメモリ管理構造体を有していない。ユーザプロセスからカーネルに切り替わるとそのユーザのmm_structを使っている。ユーザプロセスが起動するとき、3G以降のページテーブルはinitのmm_struct(マスターカーネルテーブル)が複写されている。もしカーネルも独自のmm_struct下のページテーブルで動作するような実装だと、ユーザ空間、カーネル空間とも4Gのアドレッシングが可能となる。しかしこのような実装となっているのは、もしカーネルも独自のページテーブルを有していると、ユーザプロセスからカーネルに切り替わる毎に、cr3レジスタの変更を必要とする。cr3レジスターを変更するとTLBを無効にする。TLBをアドレス変換を行うためのキャッシュである。その負荷が大きいからだということだ。
そのような分けで、カーネルはユーザプロセスのページテーブルを使って動作する。
そこで、カーネルもvmallocで動的にメモリを確保する。ユーザプロセスならメモリー獲得において自分のページテーブルのみを更新すればいい。しかしカーネルでは全てのユーザプロセスのカーネル部分のページテーブルを変更する必要があるわけだ。
ページフォルトが発生すると、do_page_fault関数がコールされる。
#define pud_offset(pgd, start) (pgd) static inline pmd_t *pmd_offset(pgd_t * dir, unsigned long address) { return (pmd_t *) dir; }ユーザプロセスのメモリはそれぞれに上記のページテーブルを有していて、実際使っている空間をtask_struct->mm_struct->vm_area_structで管理している。その内ユーザが使えるメモリー空間は32ビットでは3G、それ以降はカーネルが使うようになっている。
カーネルはvm_area_structを管理しながら、プロセスのメモリー管理をしている。そしてvm_area_structで管理される実メモリ(ページ)をページテーブルで管理されている。
プロセスが動作するには、mm_structが必要となる。mm_structにはcr3のアドレスも有している。しかしカーネルはこのメモリ管理構造体を有していない。ユーザプロセスからカーネルに切り替わるとそのユーザのmm_structを使っている。ユーザプロセスが起動するとき、3G以降のページテーブルはinitのmm_struct(マスターカーネルテーブル)が複写されている。もしカーネルも独自のmm_struct下のページテーブルで動作するような実装だと、ユーザ空間、カーネル空間とも4Gのアドレッシングが可能となる。しかしこのような実装となっているのは、もしカーネルも独自のページテーブルを有していると、ユーザプロセスからカーネルに切り替わる毎に、cr3レジスタの変更を必要とする。cr3レジスターを変更するとTLBを無効にする。TLBをアドレス変換を行うためのキャッシュである。その負荷が大きいからだということだ。
そのような分けで、カーネルはユーザプロセスのページテーブルを使って動作する。
そこで、カーネルもvmallocで動的にメモリを確保する。ユーザプロセスならメモリー獲得において自分のページテーブルのみを更新すればいい。しかしカーネルでは全てのユーザプロセスのカーネル部分のページテーブルを変更する必要があるわけだ。
ページフォルトが発生すると、do_page_fault関数がコールされる。
void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code) { : address = read_cr2(); : #ifdef CONFIG_X86_32 if (unlikely(address >= TASK_SIZE)) { #else if (unlikely(address >= TASK_SIZE64)) { #endif if (!(error_code & (PF_RSVD|PF_USER|PF_PROT)) && vmalloc_fault(address) >= 0) return; : : } : :addressにはエラーが発生したアドレスが設定される。そしてそのアドレスがTASK_SIZEより大きいとき、これはカーネル空間で発生したことになる。TASK_SIZEは32ビットでは0xC0000000となっている。そしてこれがカーネルが実行したとか、もろもろのチェックとともに、vmalloc_fault関数が実行され、実際の処理はvmalloc_sync_one関数で行われる。ここがカーネルのページデマンド処理となる。要はマスターグローバルテーブルを該当するプロセスのページテーブルに複写する。
static inline pmd_t *vmalloc_sync_one(pgd_t *pgd, unsigned long address) { unsigned index = pgd_index(address); pgd_t *pgd_k; pud_t *pud, *pud_k; pmd_t *pmd, *pmd_k; pgd += index; pgd_k = init_mm.pgd + index; if (!pgd_present(*pgd_k)) return NULL; pud = pud_offset(pgd, address); pud_k = pud_offset(pgd_k, address); if (!pud_present(*pud_k)) return NULL; pmd = pmd_offset(pud, address); pmd_k = pmd_offset(pud_k, address); if (!pmd_present(*pmd_k)) return NULL; if (!pmd_present(*pmd)) { set_pmd(pmd, *pmd_k); arch_flush_lazy_mmu_mode(); } else BUG_ON(pmd_page(*pmd) != pmd_page(*pmd_k)); return pmd_k; }引数はエラーが発生した時のpgdとアドレスである。まずアドレスのpgdのインデックスからinit_mm.pgdのpud、pmd(変数pud_k,pmd_kに設定)を取得し(pud_offset,pmd_offset関数は該当するページディレクトリがなければ確保している。たぶん)、そして、エラーが発生したpmdのエントリーがなければ、set_pmd関数でinit_mmのpmdをエラーのpmdにセットしている。