Linux 4.14でプロセスをforkした時のPage Global Directoryの設定を見てみます。読むカーネルはv4.14.12です。 前にLinux x86_64のPaging:Page Global Directory辺りの扱いを見てみる - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ書いてたけど、最新のカーネルで調べてみたので。
プロセス生成時のPage Global Directoryの設定の流れ
Page Global Directory(pgd)の設定はpgd_alloc()で行います。fork()からの流れはこのような形。
_do_fork() -> copy_process() -> copy_mm() -> dup_mm() -> mm_init() -> mm_alloc_pgd() -> pgd_alloc()
pgd_alloc()
pgd_alloc()はこのような関数です。
pgd_t *pgd_alloc(struct mm_struct *mm) { pgd_t *pgd; pmd_t *pmds[PREALLOCATED_PMDS]; pgd = _pgd_alloc(); if (pgd == NULL) goto out; mm->pgd = pgd; if (preallocate_pmds(mm, pmds) != 0) goto out_free_pgd; if (paravirt_pgd_alloc(mm) != 0) goto out_free_pmds; /* * Make sure that pre-populating the pmds is atomic with * respect to anything walking the pgd_list, so that they * never see a partially populated pgd. */ spin_lock(&pgd_lock); pgd_ctor(mm, pgd); pgd_prepopulate_pmd(mm, pgd, pmds); spin_unlock(&pgd_lock); return pgd; out_free_pmds: free_pmds(mm, pmds); out_free_pgd: _pgd_free(pgd); out: return NULL; }
pgd_tとpmd_tの変数が宣言されてますね。pgdのほうは良いとして、pmdsですが配列となっています。サイズはPREALLOCATED_PMDSですが、これはPAEの設定で変わります。x86_64環境だとPAEは無いのでPREALLOCATED_PMDSは0と定義されています。
/* No need to prepopulate any pagetable entries in non-PAE modes. */ #define PREALLOCATED_PMDS 0
では、処理を見ていきます。
pgd = _pgd_alloc();
最初に_pgd_alloc()を呼んでメモリを確保してます。_pgd_alloc()もPAEの有無で処理が変わるのですが、x86_64の場合は__get_free_pages()でページを確保します。 ここで最近話題のKPTIに関する分岐がありました。KPTIが有効なら2ページ確保してます。
#ifdef CONFIG_PAGE_TABLE_ISOLATION /* * Instead of one PGD, we acquire two PGDs. Being order-1, it is * both 8k in size and 8k-aligned. That lets us just flip bit 12 * in a pointer to swap between the two 4k halves. */ #define PGD_ALLOCATION_ORDER 1 #else #define PGD_ALLOCATION_ORDER 0 #endif
ページが確保できたら mm->pgd
にpgdをセットします。
次にpreallocate_pmds()でpmd用のメモリを確保します。
if (preallocate_pmds(mm, pmds) != 0) goto out_free_pgd;
preallocate_pmds()はx86_64環境だと特にやることはありません。ここもPAEが有効な場合に主要な処理があるだけです。x86_64だと引数で渡されたmm構造体がinit_mmだったらアカウンティングをしないという設定をする程度です。
if (mm == &init_mm)
gfp &= ~__GFP_ACCOUNT;
ここは特になにもありません。
if (paravirt_pgd_alloc(mm) != 0) goto out_free_pmds;
関数はarch/x86/include/asm/paravirt.hで定義されていて、
static inline int paravirt_pgd_alloc(struct mm_struct *mm) { return PVOP_CALL1(int, pv_mmu_ops.pgd_alloc, mm); }
pgd_allocは__paravirt_pgd_alloc()がセットされてます。
.pgd_alloc = __paravirt_pgd_alloc, .pgd_free = paravirt_nop,
__paravirt_pgd_alloc()は単に0を返してます。
static inline int __paravirt_pgd_alloc(struct mm_struct *mm) { return 0; }
こちらはコンストラクタ的な処理です。pgd_ctor()は後ほど。
pgd_ctor(mm, pgd);
最後はpmdの設定です。
pgd_prepopulate_pmd(mm, pgd, pmds);
x86_64の場合、pmd用のメモリ確保もしていないのでpgd_prepopulate_pmd()も特に処理はありません。 こんな感じになってます。
if (PREALLOCATED_PMDS == 0) /* Work around gcc-3.4.x bug */ return;
ここまででエラーが無ければ終了です。
pgd_ctor()
pgd_ctor()はコンストラクタ的な処理ですね。
static void pgd_ctor(struct mm_struct *mm, pgd_t *pgd) { /* If the pgd points to a shared pagetable level (either the ptes in non-PAE, or shared PMD in PAE), then just copy the references from swapper_pg_dir. */ if (CONFIG_PGTABLE_LEVELS == 2 || (CONFIG_PGTABLE_LEVELS == 3 && SHARED_KERNEL_PMD) || CONFIG_PGTABLE_LEVELS >= 4) { clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY, swapper_pg_dir + KERNEL_PGD_BOUNDARY, KERNEL_PGD_PTRS); } /* list required to sync kernel mapping updates */ if (!SHARED_KERNEL_PMD) { pgd_set_mm(pgd, mm); pgd_list_add(pgd); } }
最初のif文は、通常のx86_64環境ならCONFIG_PGTABLE_LEVELSは4だと思います。5段階ページングとか有効にしてたら5でしょうけども。それはともかく、clone_pgd_range()の実行があります。 clone_pgd_range()はこのような関数です。
static inline void clone_pgd_range(pgd_t *dst, pgd_t *src, int count) { memcpy(dst, src, count * sizeof(pgd_t)); #ifdef CONFIG_PAGE_TABLE_ISOLATION if (!static_cpu_has(X86_FEATURE_PTI)) return; /* Clone the user space pgd as well */ memcpy(kernel_to_user_pgdp(dst), kernel_to_user_pgdp(src), count * sizeof(pgd_t)); #endif }
最初にアドレスswapper_pg_dir + KERNEL_PGD_BOUNDARY
からKERNEL_PGD_PTRS*sizeof(pgd_t)
バイトをアドレスpgd + KERNEL_PGD_BOUNDARY
にコピーします。KERNEL_PGD_BOUNDARYについてはKASLRの有効無効で値が変わります。KERNEL_PGD_PTRSは以下の用になっていて、PTRS_PER_PGDは512です。
#define KERNEL_PGD_PTRS (PTRS_PER_PGD - KERNEL_PGD_BOUNDARY)
static_cpu_has()のところはCPUがPTIをサポートしてないならここで終了です。このフラグについては/arch/x86/include/asm/cpufeatures.hに定義があります。 そうでなければユーザー空間のマッピングもコピーしてます。
2018/01/10追記 このフラグはpti_check_boottime_disable()の処理でPTIが無効にセットされていなければ、setup_force_cpu_cap()を使ってセットしてます。
setup_force_cpu_cap(X86_FEATURE_PTI);
pgd_ctor()に戻って、SHARED_KERNEL_PMDの値をチェックして0ならpgd_set_mm()とpgd_list_add()の実行があります。4段階のページングを使ってるならSHARED_KERNEL_PMDの値は0です。
pgd_set_mm()はpgd変数のアドレスに該当するpage構造体のindexメンバ変数にmm構造体をセットしてます。
static void pgd_set_mm(pgd_t *pgd, struct mm_struct *mm) { BUILD_BUG_ON(sizeof(virt_to_page(pgd)->index) < sizeof(mm)); virt_to_page(pgd)->index = (pgoff_t)mm; }
pgd_list_add()のほうはarch/x86/mm/fault.cにあるpgd_listにpgdのpage構造体を追加してます。
static inline void pgd_list_add(pgd_t *pgd) { struct page *page = virt_to_page(pgd); list_add(&page->lru, &pgd_list); }
ここまででpgdの設定が終わりです。
最後に
pgd_alloc()はx86_64の場合はpmdの設定はなくて、pgdの設定のみ行います。また、KPTIが入ったことでこの機能関連の処理が追加されていました。
この後、kernel/fork.cのmm_init()まで戻ってinit_new_context()を実行します。init_new_context()はまたの機会に。
( ´ー`)フゥー...
動くメカニズムを図解&実験! Linux超入門 (My Linuxシリーズ)
- 作者: 宗像尚郎/海老原祐太郎
- 出版社/メーカー: CQ出版
- 発売日: 2016/04/15
- メディア: 単行本
- この商品を含むブログ (3件) を見る