カーネルのページフォルト
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にセットしている。





