Linux4.14.12(x86_64)のPage Global Directoryの設定を見てみる

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シリーズ)

動くメカニズムを図解&実験! Linux超入門 (My Linuxシリーズ)