Transparent Hugepage コードウォーク

カーネル/VM Advent Calendar の第57日です。

本記事では 2.6.38-rc1 でメインライン入りした transparent hugepage のコードウォークを行います。RHEL6 とは実装が異なる可能性がある (詳細は未調査) 点は、注意して下さい。また、見切れなかった部分があるのを自覚していて、今後私の理解に応じて多少加筆・修正されることになると思います。間違いや不明点がありましたら、コメントをいただければ可能な範囲で追加調査します。

概略

Transparent Hugepage (THP) は、ユーザ空間から明示的に指示することなく透過的に hugepage を利用する機能である。Hugepage の恩恵は、一つの TLB エントリがカバーするメモリ領域を大きく取ることで、TLB の使用効率を高めることである。これにより page fault が頻繁に起こるようなワークロードにおいて性能が向上する。

THP パッチは、大きく以下の3種類のコードパスをカーネルに追加する。

  1. page fault を契機とした hugepage 割り当て処理
  2. khugepaged による regular size page の collapse 処理
  3. hugepage の split 処理

1. では buddy から直接 hugepage を構成する。メモリフラグメント等の理由により hugepage 割り当てに失敗する場合、キャッシュのライトバックなどを呼ぶなどして無理に空きメモリを確保しようと粘らずに、速やかに regular size page の割り当てパスに落ちるようになっている (但し defrag 未使用時)。このため通常の page fault 処理に追加されるオーバーヘッドはわずかである。Hugepage の割り当てに成功した場合、ユーザ空間にメモリ領域が引き渡される前に hugepage のゼロクリアが行われるため、regular size page と比べて最初の一回の page fault にかかる時間は増大する。しかし、このコストは後の TLB miss が低減できることによる性能メリットと比べると十分許容できる程度である。

2. ではカーネルスレッドが裏で regular size page を hugepage に変換する処理 (collapse) を行う。

3. はページングやページマイグレーションなど、現状では hugepage のままでは扱えない処理を行う必要が生じたときに、その場で hugepage を regular size page に分割する処理 (split) を行う。

1. で hugepage の割り当て失敗時に regular size page の割り当てパスに fall back するとか、3. で hugepage のまま扱えないケースで hugepage を分割して処理を行う、とかいった動作は graceful fallback と呼ばれている。graceful fallback の page fault の処理を重くしてしまうのを避けるとか、THP のことを知らない他のサブシステムに対する変更を最小限に抑えることができる、という特徴は、THP がメインラインにマージされるために非常に重要な役割を果たした。

sysfs インターフェース

コードウォークに入る前に、THP の動作を制御するための sysfs インターフェースを以下にリストする。max_ptes_none など、Documentaion/vm/transhuge.txt に書かれておらず、実装とドキュメントが多少食い違っているため、注意すること。

  • /sys/kernel/mm/transparent_hugepage/: THP 全体の制御 *1
    • enabled : THP を有効にするかどうか (always, madvise, never)
    • defrag : page fault と khugepaged でデフラグを有効にするかどうか (always, madvise, never)
  • /sys/kernel/mm/transparent_hugepage/khugepaged: khugepaged の制御
    • defrag : khugepaged におけるデフラグを有効にするかどうか (always, madvise, never)
    • pages_to_scan : 一回の khugepaged の処理でスキャンされるページ数
    • scan_sleep_millisecs : 各 khugepaged の処理間にスリープする秒数 (ms)
    • alloc_sleep_millisecs : hugepage allocation に失敗したとき、次のトライまでスリープする秒数 (ms)
    • max_ptes_none : 対象となる pmd が、ページに map されていない pte を含んでいる場合、その pmd を collapse すると、空きメモリが無駄になる可能性がある。map されていない pte の数が max_ptes_none を越えるとき、その pmd は collapse されない。デフォルトは 511 なので、一つでも map された pte があれば、collapse 対象となる。
    • pages_collapsed : (Read-only) これまで normal page -> hugepage 変換に成功した回数
    • full_scans : (Read-only) khugepaged が走った回数

実装

1. page fault を契機とした hugepage 割り当て

handle_mm_fault に下記のようなフックが挿入される。


int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
...
if (pmd_none(*pmd) && transparent_hugepage_enabled(vma)) { ... (1)
if (!vma->vm_ops)
return do_huge_pmd_anonymous_page(mm, vma, address,
pmd, flags);
} else {
pmd_t orig_pmd = *pmd;
barrier();
if (pmd_trans_huge(orig_pmd)) {
if (flags & FAULT_FLAG_WRITE &&
!pmd_write(orig_pmd) &&
!pmd_trans_splitting(orig_pmd))
return do_huge_pmd_wp_page(mm, vma, address,
pmd, orig_pmd);
return 0;
}
}
...
}

(1) の if 文は page fault が発生した仮想アドレス領域にまだ regular size page が割り当てられておらず場合 (pmd_none() == true)、かつ当該 vma に対する THP が有効 (/sys/kernel/mm/transparent_hugepage/enable を always に設定するか、madvise にした上で madvise(MADV_HUGEPAGE) を実行) になっている場合のみ hugepage 割り当てのパスに入る。else ブロックは THP から COW が発生する場合のパスで、とりあえず省略する。

続いて、hugepage 割り当ての入口で実行される以下は do_huge_pmd_anonymous_page() である。


int do_huge_pmd_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd,
unsigned int flags)
{
struct page *page;
unsigned long haddr = address & HPAGE_PMD_MASK;
pte_t *pte;

if (haddr >= vma->vm_start && haddr + HPAGE_PMD_SIZE <= vma->vm_end) { ... (2)
if (unlikely(anon_vma_prepare(vma)))
return VM_FAULT_OOM;
if (unlikely(khugepaged_enter(vma))) ... (3)
return VM_FAULT_OOM;
page = alloc_hugepage_vma(transparent_hugepage_defrag(vma),
vma, haddr); ... (4)
if (unlikely(!page))
goto out;
if (unlikely(mem_cgroup_newpage_charge(page, mm, GFP_KERNEL))) {
put_page(page);
goto out;
}

return __do_huge_pmd_anonymous_page(mm, vma, haddr, pmd, page);
}
out:
/*
* Use __pte_alloc instead of pte_alloc_map, because we can't
* run pte_offset_map on the pmd, if an huge pmd could
* materialize from under us from a different thread.
*/
if (unlikely(__pte_alloc(mm, vma, pmd, address)))
return VM_FAULT_OOM;
/* if an huge pmd materialized from under us just retry later */
if (unlikely(pmd_trans_huge(*pmd)))
return 0;
/*
* A regular pmd is established and it can't morph into a huge pmd
* from under us anymore at this point because we hold the mmap_sem
* read mode and khugepaged takes it in write mode. So now it's
* safe to run pte_offset_map().
*/
pte = pte_offset_map(pmd, address);
return handle_pte_fault(mm, vma, address, pte, pmd, flags);
}

(2) において、対象となる 2MB 仮想アドレス領域が vma 内に収まらない場合、regular size page に fallback する。これは、少量のメモリで済む状況ではわざわざ hugepage を割り当てないということである。(3) の khugepage_enter() は page fault が発生した mm を khugepaged がスキャン対象とする mm_slot のリストに登録する。この mm_slot は後で khugepaged が collapse できるページがないか走査する際に利用される。(4) の alloc_hugepage_vma() において実際のメモリ割り当てを行うが、GFP フラグが GFP_TRANSHUGE にセットされていることに注意する。これは以下のように定義されている:


GFP_TRANSHUGE (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | \
__GFP_HIGHMEM | __GRP_MOVABLE | __GFP_COMP | \
__GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN | \
__GFP_NO_KSWAPD)
# defrag off の場合 __GFP_WAIT が落ちる

ここで重要なフラグは __GFP_NO_KSWAPD で、これは hugepage を割り当てるときに kswapd の起床を抑制することを指示する。概要で述べた graceful fallback はこのフラグにより実現されている。(alloc_hugepage_vma() は defrag を実行するかどうかのフラグで渡すことができるが、これをオンにした場合については後日フォローする予定。)

THP 割り当てに成功した場合、__do_huge_pmd_anonymous_page() でページテーブルを設定する。


static int __do_huge_pmd_anonymous_page(...)
{
...
pgtable_t pgtable;
...
pgtable = pte_alloc_one(mm, haddr); ... (5)
...
if (unlikely(!pmd_none(*pmd))) {
...
} else { // 割り当て成功時の分岐
pmd_t entry;
entry = mk_pmd(page, vma->vm_page_prot);
entry = maybe_pmd_mkwrite(pmd_mkdirty(entry), vma);
entry = pmd_mkhuge(entry);
/*
* The spinlocking to take the lru_lock inside
* page_add_new_anon_rmap() acts as a full memory
* barrier to be sure clear_huge_page writes become
* visible after the set_pmd_at() write.
*/
page_add_new_anon_rmap(page, vma, haddr);
set_pmd_at(mm, haddr, pmd, entry);
prepare_pmd_huge_pte(pgtable, mm); ... (6)
add_mm_counter(mm, MM_ANONPAGES, HPAGE_PMD_NR);
spin_unlock(&mm->page_table_lock);
}

return ret;
}

(5) の pte_alloc_one() の中で 1 page 余分に割り当てている点には注意が必要である。これは後に述べる hugepaeg の分割処理の後で、pmd が pte を指すエントリを格納するための領域である。Hugepage の分割処理が失敗しないために予め割り当てておく必要である。このページは (6) において mm->pmd_huge_pte のリンクリストに接続される。その他は、regular size page のページフォルト処理とそう大差はない。

2. khugepaged

khugepaged はバックグラウンドで regular size page -> hugepage 変換を行うカーネルスレッドである。CONFIG_TRANSPARENT_HUGEPAGE=y のとき、大体以下のようなコールチェーンを辿って初期化される。


hugepage_init()
sysfs 配下に hugepage, khugepaged のディレクトリを作成
khugepaged_slab_init() // slab に khugepaged_mm_slot 型を登録
mm_slots_hash_init() // mm_slot のハッシュリストの作成
start_khugepaged
kthread_run(&khugepaged) // カーネルスレッドを作成
if (&khugepaged_scan.mm_head に mm_slot が登録されていれば)
wake_up_interruptible(&khugepaged_wait); // スレッド起床・処理開始

khugepaged が起床すると、khugepaged() が実行される。ここから処理の中核部である khugepaged_scan_mm_slot() まで、いくつか関数を呼ばなければならないが、あまり重要ではない部分なので以下に大まかな流れを示して省略することにする。


khugepaged()
while (THP が on) {
khugepaged_loop()
while (THP が enabled) {
khugepaged_do_scan()
while (scan したページ数 < pages_to_scan) {
khugepaged_scan_mm_slot() // ここから本処理
for (vma) {
khugepaged_scan_pmd()
collapse_huge_page()
__collapse_huge_page_isolate()
__collapse_huge_page_copy()
}
}
}
}

khugepaged_scan_mm_slot() は、グローバル変数の khugepaged_scan 構造体 (mm_slot のリスト構造を管理する) からスキャン対象リストに繋がれた mm_slot を走査し、collapse できるメモリがないかどうかをチェックし、collapse を試みる。以下、関数が長いので、インラインで説明していく (カーネルのコメントの流儀に反するが、以下のインライン解説は解説挿入部の直前のコードについての言及とする)。


static unsigned int khugepaged_scan_mm_slot(unsigned int pages,
struct page **hpage)
{
struct mm_slot *mm_slot;
struct mm_struct *mm;
struct vm_area_struct *vma;
int progress = 0;

VM_BUG_ON(!pages);
VM_BUG_ON(!spin_is_locked(&khugepaged_mm_lock));

if (khugepaged_scan.mm_slot)
mm_slot = khugepaged_scan.mm_slot;

前回の情報が残っている場合、その続きから再開する。

else {
mm_slot = list_entry(khugepaged_scan.mm_head.next,
struct mm_slot, mm_node);
khugepaged_scan.address = 0;
khugepaged_scan.mm_slot = mm_slot;
}
spin_unlock(&khugepaged_mm_lock);

mm = mm_slot->mm;
down_read(&mm->mmap_sem);

ページテーブルを walk するので mmap_sem を取る。

if (unlikely(khugepaged_test_exit(mm)))
vma = NULL;
else
vma = find_vma(mm, khugepaged_scan.address);

collapse 対象の vma を決定する。

progress++;
for (; vma; vma = vma->vm_next) {
unsigned long hstart, hend;

cond_resched();
if (unlikely(khugepaged_test_exit(mm))) {
progress++;
break;
}

当該 mm のユーザが 0 だったら collapse できても効果がないので break;

if ((!(vma->vm_flags & VM_HUGEPAGE) &&
!khugepaged_always()) ||
(vma->vm_flags & VM_NOHUGEPAGE)) {
progress++;
continue;
}

当該 vma が THP を有効にしていない場合 break;

/* VM_PFNMAP vmas may have vm_ops null but vm_file set */
if (!vma->anon_vma || vma->vm_ops || vma->vm_file) {
khugepaged_scan.address = vma->vm_end;
progress++;
continue;
}

現状では anonymous page の vma にしか対応していないため、file backed な場合は break;

VM_BUG_ON(is_linear_pfn_mapping(vma) || is_pfn_mapping(vma));

hstart = (vma->vm_start + ~HPAGE_PMD_MASK) & HPAGE_PMD_MASK;
hend = vma->vm_end & HPAGE_PMD_MASK;
if (hstart >= hend) {
progress++;
continue;
}
if (khugepaged_scan.address < hstart)
khugepaged_scan.address = hstart;
if (khugepaged_scan.address > hend) {
khugepaged_scan.address = hend + HPAGE_PMD_SIZE;
progress++;
continue;
}
BUG_ON(khugepaged_scan.address & ~HPAGE_PMD_MASK);

while (khugepaged_scan.address < hend) {

ここに入ったら、当該 vma は collapse できる条件を満たしている。

int ret;
cond_resched();
if (unlikely(khugepaged_test_exit(mm)))
goto breakouterloop;

VM_BUG_ON(khugepaged_scan.address < hstart ||
khugepaged_scan.address + HPAGE_PMD_SIZE >
hend);
ret = khugepaged_scan_pmd(mm, vma,
khugepaged_scan.address,
hpage);

khugepaged_scan.address (hugepage align された仮想アドレス) から始まる
pmd に対して、さらに collapse できるかどうか詳しいチェックを行う。

/* move to next address */
khugepaged_scan.address += HPAGE_PMD_SIZE;
progress += HPAGE_PMD_NR;
if (ret)
/* we released mmap_sem so break loop */
goto breakouterloop_mmap_sem;
if (progress >= pages)
goto breakouterloop;
}
}
breakouterloop:
up_read(&mm->mmap_sem); /* exit_mmap will destroy ptes after this */
breakouterloop_mmap_sem:

以下、後始末。

spin_lock(&khugepaged_mm_lock);
BUG_ON(khugepaged_scan.mm_slot != mm_slot);
/*
* Release the current mm_slot if this mm is about to die, or
* if we scanned all vmas of this mm.
*/
if (khugepaged_test_exit(mm) || !vma) {
/*
* Make sure that if mm_users is reaching zero while
* khugepaged runs here, khugepaged_exit will find
* mm_slot not pointing to the exiting mm.
*/
if (mm_slot->mm_node.next != &khugepaged_scan.mm_head) {
khugepaged_scan.mm_slot = list_entry(
mm_slot->mm_node.next,
struct mm_slot, mm_node);
khugepaged_scan.address = 0;
} else {
khugepaged_scan.mm_slot = NULL;
khugepaged_full_scans++;
}

collect_mm_slot(mm_slot);
}

return progress;
}

以下、当該 vma 内の指定した 2MB の仮想アドレス領域に対して、pte と page の状態をチェックして collapse 可能かどうかを調べる。


static int khugepaged_scan_pmd(struct mm_struct *mm,
struct vm_area_struct *vma,
unsigned long address,
struct page **hpage)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *pte, *_pte;
int ret = 0, referenced = 0, none = 0;
struct page *page;
unsigned long _address;
spinlock_t *ptl;

VM_BUG_ON(address & ~HPAGE_PMD_MASK);

pgd = pgd_offset(mm, address);
if (!pgd_present(*pgd))
goto out;

pud = pud_offset(pgd, address);
if (!pud_present(*pud))
goto out;

pmd = pmd_offset(pud, address);
if (!pmd_present(*pmd) || pmd_trans_huge(*pmd))
goto out;

pte = pte_offset_map_lock(mm, pmd, address, &ptl);
for (_address = address, _pte = pte; _pte < pte+HPAGE_PMD_NR;
_pte++, _address += PAGE_SIZE) {

当該 2MB 領域内の各ページ pte に対して collapse 可能な条件を満たしているか
どうかをチェックする。満たしていない場合、goto out_unmap; で抜けることから、
512 個ある pte の一つでも collapse に相応しくない状態だったら諦める実装となる。

pte_t pteval = *_pte;
if (pte_none(pteval)) {
if (++none <= khugepaged_max_ptes_none)

/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none で制御される部分
当該 2MB 領域内の空 pte の数が max_ptes_none を越えたら collapse を諦める。

continue;
else
goto out_unmap;
}
if (!pte_present(pteval) || !pte_write(pteval))
goto out_unmap;

スワップエントリや writeprotected なエントリは collapse しない。

page = vm_normal_page(vma, _address, pteval);
if (unlikely(!page))
goto out_unmap;

pte の指す先が取得できなかった場合も collapse しない。

VM_BUG_ON(PageCompound(page));
if (!PageLRU(page) || PageLocked(page) || !PageAnon(page))
goto out_unmap;

LRU になかったり、Lock されていたり (他のカーネルコードが使用している)、
anonymous page でなかった場合、collapse しない。

/* cannot use mapcount: can't collapse if there's a gup pin */
if (page_count(page) != 1)
goto out_unmap;

gup (get_user_pages() or get_user_pages_fast()) で pin されている場合、
collapse しない。

if (pte_young(pteval) || PageReferenced(page) ||
mmu_notifier_test_young(vma->vm_mm, address))
referenced = 1;

reference = 1; なら collapse を行う。この if 条件の意図はよく理解できないが、
pte (あるいは kvm プロセスなら spte) が全て young でない場合、collapse しない。
http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=commitdiff;h=8ee53820edfd1f3b6554c593f337148dd3d7fc91

}
if (referenced)
ret = 1;

out_unmap:
pte_unmap_unlock(pte, ptl);
if (ret)
/* collapse_huge_page will return with the mmap_sem released */
collapse_huge_page(mm, address, hpage, vma);
out:
return ret;
}

続いて collapse_huge_page() である。この関数はかなり長いが、大きく3つのことをやっていて、(1) collapse 後の hugepage 領域を割り当てる、(2) 当該アドレス領域の各 pte が指すページを LRU リストから外し (__collapse_huge_page_isolate()) 、(3) regular size page 内のデータを hugepage にコピーする (__collapse_huge_page_map())。


static void collapse_huge_page(struct mm_struct *mm,
unsigned long address,
struct page **hpage,
struct vm_area_struct *vma)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd, _pmd;
pte_t *pte;
pgtable_t pgtable;
struct page *new_page;
spinlock_t *ptl;
int isolated;
unsigned long hstart, hend;

VM_BUG_ON(address & ~HPAGE_PMD_MASK);
#ifndef CONFIG_NUMA
VM_BUG_ON(!*hpage);
new_page = *hpage;
#else
VM_BUG_ON(*hpage);
/*
* Allocate the page while the vma is still valid and under
* the mmap_sem read mode so there is no memory allocation
* later when we take the mmap_sem in write mode. This is more
* friendly behavior (OTOH it may actually hide bugs) to
* filesystems in userland with daemons allocating memory in
* the userland I/O paths. Allocating memory with the
* mmap_sem in read mode is good idea also to allow greater
* scalability.
*/
new_page = alloc_hugepage_vma(khugepaged_defrag(), vma, address);

collapse 後の hugepage 領域の割り当て。

if (unlikely(!new_page)) {
up_read(&mm->mmap_sem);
*hpage = ERR_PTR(-ENOMEM);
return;
}
#endif
if (unlikely(mem_cgroup_newpage_charge(new_page, mm, GFP_KERNEL))) {
up_read(&mm->mmap_sem);
put_page(new_page);
return;
}

/* after allocating the hugepage upgrade to mmap_sem write mode */
up_read(&mm->mmap_sem);

/*
* Prevent all access to pagetables with the exception of
* gup_fast later hanlded by the ptep_clear_flush and the VM
* handled by the anon_vma lock + PG_lock.
*/
down_write(&mm->mmap_sem);
if (unlikely(khugepaged_test_exit(mm)))
goto out;

vma = find_vma(mm, address);
hstart = (vma->vm_start + ~HPAGE_PMD_MASK) & HPAGE_PMD_MASK;
hend = vma->vm_end & HPAGE_PMD_MASK;
if (address < hstart || address + HPAGE_PMD_SIZE > hend)
goto out;

if ((!(vma->vm_flags & VM_HUGEPAGE) && !khugepaged_always() ) ||
(vma->vm_flags & VM_NOHUGEPAGE))
goto out;

/* VM_PFNMAP vmas may have vm_ops null but vm_file set */
if (!vma->anon_vma || vma->vm_ops || vma->vm_file)
goto out;
VM_BUG_ON(is_linear_pfn_mapping(vma) || is_pfn_mapping(vma));

down_write(&mm->mmap_sem) からここまで、vma が collapse 可能かどうかを
再度チェックしている。

pgd = pgd_offset(mm, address);
if (!pgd_present(*pgd))
goto out;

pud = pud_offset(pgd, address);
if (!pud_present(*pud))
goto out;

pmd = pmd_offset(pud, address);
/* pmd can't go away or become huge under us */
if (!pmd_present(*pmd) || pmd_trans_huge(*pmd))
goto out;

anon_vma_lock(vma->anon_vma);

pte = pte_offset_map(pmd, address);
ptl = pte_lockptr(mm, pmd);

spin_lock(&mm->page_table_lock); /* probably unnecessary */
/*
* After this gup_fast can't run anymore. This also removes
* any huge TLB entry from the CPU so we won't allow
* huge and small TLB entries for the same virtual address
* to avoid the risk of CPU bugs in that area.
*/
_pmd = pmdp_clear_flush_notify(vma, address, pmd);

page collapse 処理中に OS から見える pmd/pte と CPU 上の TLB の情報が
食い違うことがあるため、get_user_pages_fast() などと race しないよう
TLB を flush しておく。

spin_unlock(&mm->page_table_lock);

spin_lock(ptl);
isolated = __collapse_huge_page_isolate(vma, address, pte);

collapse 対象のページを LRU リストから外す処理を行う。
この関数の中は説明しないが、当該 pmd 配下の全ての pte が指す page が
LRU リストから isolate できれば、isolated = 1 が返る。

isolated = __collapse_huge_page_isolate(vma, address, pte);

spin_unlock(ptl);

if (unlikely(!isolated)) {

isolate に失敗したら collapse は諦める。

pte_unmap(pte);
spin_lock(&mm->page_table_lock);
BUG_ON(!pmd_none(*pmd));
set_pmd_at(mm, address, pmd, _pmd);
spin_unlock(&mm->page_table_lock);
anon_vma_unlock(vma->anon_vma);
mem_cgroup_uncharge_page(new_page);
goto out;
}

/*
* All pages are isolated and locked so anon_vma rmap
* can't run anymore.
*/
anon_vma_unlock(vma->anon_vma);

__collapse_huge_page_copy(pte, new_page, vma, address, ptl);

データのコピーを行う。

pte_unmap(pte);
__SetPageUptodate(new_page);
pgtable = pmd_pgtable(_pmd);
VM_BUG_ON(page_count(pgtable) != 1);
VM_BUG_ON(page_mapcount(pgtable) != 0);

_pmd = mk_pmd(new_page, vma->vm_page_prot);
_pmd = maybe_pmd_mkwrite(pmd_mkdirty(_pmd), vma);
_pmd = pmd_mkhuge(_pmd);

hugepage 用の pmd の中身をこしらえる。

/*
* spin_lock() below is not the equivalent of smp_wmb(), so
* this is needed to avoid the copy_huge_page writes to become
* visible after the set_pmd_at() write.
*/
smp_wmb();

spin_lock(&mm->page_table_lock);
BUG_ON(!pmd_none(*pmd));
page_add_new_anon_rmap(new_page, vma, address);
set_pmd_at(mm, address, pmd, _pmd);
update_mmu_cache(vma, address, entry);
prepare_pmd_huge_pte(pgtable, mm);
mm->nr_ptes--;
spin_unlock(&mm->page_table_lock);

#ifndef CONFIG_NUMA
*hpage = NULL;
#endif
khugepaged_pages_collapsed++;

統計情報 /sys/kernel/mm/transparent_hugepage/khugepaged/pages_collapsed に計上

out_up_write:
up_write(&mm->mmap_sem);
return;

out:
#ifdef CONFIG_NUMA
put_page(new_page);
#endif
goto out_up_write;
}


3. hugepage の分割

Hugepage の分割処理について説明する。Hugepage の分割処理は split_huge_page() (あるいはそのラッパー) である。この関数の呼び出し元は以下のようなものがある。これらの処理は現状では hugepage に対応していないため、処理を実行する前に hugepage を regular size page に分割する必要がある。このため、split_huge_page() は必ず成功するように書かれている。

  • unmap_vma()
  • follow_page()
  • add_to_swap(): page out から
  • unmap_and_move(): page migration から
  • page_trans_compound_anon_split(): ksm から
  • check_pmd_range(): mbind() から
  • change_pmd_range(): mprotect() から
  • get_old_pmd(): remap() から
  • walk_pmd_range(): pagewalk から
  • collect_procs_anon(): HWPOISON から

まず、hugepage の分割処理の概要を以下に示しておく。


split_huge_page()
__split_huge_page()
for 分割対象の hugepage を map する vma に対して {
__split_huge_page_splitting() // 当該 hugepage が現在分割処理中
でないことをチェック
}
__split_huge_page_refcount() // tail page に対して参照カウント、
ページフラグをセット
for 分割対象の hugepage を map する vma に対して {
__split_huge_page_map() // pmd -> pte の分割処理
}

これを踏まえて、__split_huge_page() から見ていく。


/* must be called with anon_vma->root->lock hold */
static void __split_huge_page(struct page *page,
struct anon_vma *anon_vma)
{
int mapcount, mapcount2;
struct anon_vma_chain *avc;

BUG_ON(!PageHead(page));
BUG_ON(PageTail(page));

mapcount = 0;
list_for_each_entry(avc, &anon_vma->head, same_anon_vma) {

同じ anon_vma に属する vma に対する for を実行する。
anon_vma_chain については以下が詳しい。
http://lwn.net/Articles/383162/
http://www.atmarkit.co.jp/flinux/rensai/watch2010/watch04a.html

struct vm_area_struct *vma = avc->vma;
unsigned long addr = vma_address(page, vma);
BUG_ON(is_vma_temporary_stack(vma));
if (addr == -EFAULT)
continue;
mapcount += __split_huge_page_splitting(page, vma, addr);

__split_huge_page_splitting() は、当該 hugepage を指す pmd の _PAGE_SPLITTING
フラグをセットしている。

}
/*
* It is critical that new vmas are added to the tail of the
* anon_vma list. This guarantes that if copy_huge_pmd() runs
* and establishes a child pmd before
* __split_huge_page_splitting() freezes the parent pmd (so if
* we fail to prevent copy_huge_pmd() from running until the
* whole __split_huge_page() is complete), we will still see
* the newly established pmd of the child later during the
* walk, to be able to set it as pmd_trans_splitting too.
*/
if (mapcount != page_mapcount(page))
printk(KERN_ERR "mapcount %d page_mapcount %d\n",
mapcount, page_mapcount(page));
BUG_ON(mapcount != page_mapcount(page));

__split_huge_page_refcount(page);

Hugepage の分割後に、regular size page の参照カウントやページフラグをセットする (後述)。

mapcount2 = 0;
list_for_each_entry(avc, &anon_vma->head, same_anon_vma) {
struct vm_area_struct *vma = avc->vma;
unsigned long addr = vma_address(page, vma);
BUG_ON(is_vma_temporary_stack(vma));
if (addr == -EFAULT)
continue;
mapcount2 += __split_huge_page_map(page, vma, addr);

当該 2MB アドレス領域に対応する pmd/pte を regular size page 向けの状態に
戻している (後述)。

}
if (mapcount != mapcount2)
printk(KERN_ERR "mapcount %d mapcount2 %d page_mapcount %d\n",
mapcount, mapcount2, page_mapcount(page));
BUG_ON(mapcount != mapcount2);
}

以下、__split_huge_page_refcount()。


static void __split_huge_page_refcount(struct page *page)
{
int i;
unsigned long head_index = page->index;
struct zone *zone = page_zone(page);
int zonestat;

/* prevent PageLRU to go away from under us, and freeze lru stats */
spin_lock_irq(&zone->lru_lock);
compound_lock(page);

新しくページフラグ PG_compound_lock を用いて hugepage の分割処理と
tail page に対する put_page が競合しないようにしている (後述)。

for (i = 1; i < HPAGE_PMD_NR; i++) {
struct page *page_tail = page + i;

/* tail_page->_count cannot change */
atomic_sub(atomic_read(&page_tail->_count), &page->_count);
BUG_ON(page_count(page) <= 0);
atomic_add(page_mapcount(page) + 1, &page_tail->_count);
BUG_ON(atomic_read(&page_tail->_count) <= 0);

/* after clearing PageTail the gup refcount can be released */
smp_mb();

page_tail->flags &= ~PAGE_FLAGS_CHECK_AT_PREP;
page_tail->flags |= (page->flags &
((1L << PG_referenced) |
(1L << PG_swapbacked) |
(1L << PG_mlocked) |
(1L << PG_uptodate)));
page_tail->flags |= (1L << PG_dirty);

/*
* 1) clear PageTail before overwriting first_page
* 2) clear PageTail before clearing PageHead for VM_BUG_ON
*/
smp_wmb();

/*
* __split_huge_page_splitting() already set the
* splitting bit in all pmd that could map this
* hugepage, that will ensure no CPU can alter the
* mapcount on the head page. The mapcount is only
* accounted in the head page and it has to be
* transferred to all tail pages in the below code. So
* for this code to be safe, the split the mapcount
* can't change. But that doesn't mean userland can't
* keep changing and reading the page contents while
* we transfer the mapcount, so the pmd splitting
* status is achieved setting a reserved bit in the
* pmd, not by clearing the present bit.
*/
BUG_ON(page_mapcount(page_tail));
page_tail->_mapcount = page->_mapcount;

BUG_ON(page_tail->mapping);
page_tail->mapping = page->mapping;

page_tail->index = ++head_index;

BUG_ON(!PageAnon(page_tail));
BUG_ON(!PageUptodate(page_tail));
BUG_ON(!PageDirty(page_tail));
BUG_ON(!PageSwapBacked(page_tail));

mem_cgroup_split_huge_fixup(page, page_tail);

lru_add_page_tail(zone, page, page_tail);

分割した subpage を LRU リストに戻す。通常の anonymous ページと完全に
同じ状態に戻る。

}

__dec_zone_page_state(page, NR_ANON_TRANSPARENT_HUGEPAGES);
__mod_zone_page_state(zone, NR_ANON_PAGES, HPAGE_PMD_NR);

/*
* A hugepage counts for HPAGE_PMD_NR pages on the LRU statistics,
* so adjust those appropriately if this page is on the LRU.
*/
if (PageLRU(page)) {
zonestat = NR_LRU_BASE + page_lru(page);
__mod_zone_page_state(zone, zonestat, -(HPAGE_PMD_NR-1));
}

ClearPageCompound(page);
compound_unlock(page);
spin_unlock_irq(&zone->lru_lock);

for (i = 1; i < HPAGE_PMD_NR; i++) {
struct page *page_tail = page + i;
BUG_ON(page_count(page_tail) <= 0);
/*
* Tail pages may be freed if there wasn't any mapping
* like if add_to_swap() is running on a lru page that
* had its mapping zapped. And freeing these pages
* requires taking the lru_lock so we do the put_page
* of the tail pages after the split is complete.
*/
put_page(page_tail);
}

/*
* Only the head page (now become a regular page) is required
* to be pinned by the caller.
*/
BUG_ON(page_count(page) <= 0);
}

以下、__split_huge_page_refcount()。


static int __split_huge_page_map(struct page *page,
struct vm_area_struct *vma,
unsigned long address)
{
struct mm_struct *mm = vma->vm_mm;
pmd_t *pmd, _pmd;
int ret = 0, i;
pgtable_t pgtable;
unsigned long haddr;

spin_lock(&mm->page_table_lock);
pmd = page_check_address_pmd(page, mm, address,
PAGE_CHECK_ADDRESS_PMD_SPLITTING_FLAG);
if (pmd) {
pgtable = get_pmd_huge_pte(mm);
pmd_populate(mm, &_pmd, pgtable);

Hugepage に余分に割り当てていた 1 page をここで取り出し、regular size page に
戻した後の pmd エントリを格納するページとして使う。

for (i = 0, haddr = address; i < HPAGE_PMD_NR;
i++, haddr += PAGE_SIZE) {
pte_t *pte, entry;
BUG_ON(PageCompound(page+i));
entry = mk_pte(page + i, vma->vm_page_prot);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
if (!pmd_write(*pmd))
entry = pte_wrprotect(entry);
else
BUG_ON(page_mapcount(page) != 1);
if (!pmd_young(*pmd))
entry = pte_mkold(entry);
pte = pte_offset_map(&_pmd, haddr);
BUG_ON(!pte_none(*pte));
set_pte_at(mm, haddr, pte, entry);
pte_unmap(pte);
}

mm->nr_ptes++;
smp_wmb(); /* make pte visible before pmd */
/*
* Up to this point the pmd is present and huge and
* userland has the whole access to the hugepage
* during the split (which happens in place). If we
* overwrite the pmd with the not-huge version
* pointing to the pte here (which of course we could
* if all CPUs were bug free), userland could trigger
* a small page size TLB miss on the small sized TLB
* while the hugepage TLB entry is still established
* in the huge TLB. Some CPU doesn't like that. See
* http://support.amd.com/us/Processor_TechDocs/41322.pdf,
* Erratum 383 on page 93. Intel should be safe but is
* also warns that it's only safe if the permission
* and cache attributes of the two entries loaded in
* the two TLB is identical (which should be the case
* here). But it is generally safer to never allow
* small and huge TLB entries for the same virtual
* address to be loaded simultaneously. So instead of
* doing "pmd_populate(); flush_tlb_range();" we first
* mark the current pmd notpresent (atomically because
* here the pmd_trans_huge and pmd_trans_splitting
* must remain set at all times on the pmd until the
* split is complete for this pmd), then we flush the
* SMP TLB and finally we write the non-huge version
* of the pmd entry with pmd_populate.
*/
set_pmd_at(mm, address, pmd, pmd_mknotpresent(*pmd));
flush_tlb_range(vma, address, address + HPAGE_PMD_SIZE);
pmd_populate(mm, pmd, pgtable);
ret = 1;
}
spin_unlock(&mm->page_table_lock);

return ret;
}

4. 排他処理

THP の主要な処理は以上だが、排他処理に関して注目すべき変更があるので、以下排他処理について説明する。THP パッチは排他制御のために以下の変更を加えている:

  • Hugepage の tail ページの参照カウントを head ページにも計上するよう変更。THP は hugetlbfs と違い、分割可能で、get_user_pages() (あるいはそのバリアント) によって subpage ごとに pin される可能性があるため、subpage 単位で参照カウントを計上する必要がある。これに対してマップカウントは、pmd 単位でマッピングが行われるため、分割後は全ての subpage を指す pte が同じマップカウントを持つものとして扱えるため、このような心配はない。
  • ページフラグに PG_compound_lock を追加。PG_compound_lock は、上記 tail 参照カウントの実装に伴って、page_compound_page() の実装が複雑になり、その結果生じる hugepage 分割 (__split_huge_page_refcount()) と put_compound_page() の競合の可能性を防ぐために必要である。現在具体的に解決した問題は futex における __get_user_pages_fast() の競合がある。

以下、put_compound_page() のソースコードを示し、tail ページへの put が発生した場合の流れを示す。


static void put_compound_page(struct page *page)
{
if (unlikely(PageTail(page))) {
/* __split_huge_page_refcount can run under us */
struct page *page_head = page->first_page;
smp_rmb();
/*
* If PageTail is still set after smp_rmb() we can be sure
* that the page->first_page we read wasn't a dangling pointer.
* See __split_huge_page_refcount() smp_wmb().
*/

__split_huge_page_refcount() の smp_wmb() により、tail のページフラグがクリア
されてから head の page->mapping, _mapcount, lru が切り替えられるので
PageTail がセットされていればまだ PageHead は hugepage の状態を保持しているはず。

if (likely(PageTail(page) && get_page_unless_zero(page_head))) {
unsigned long flags;
/*
* Verify that our page_head wasn't converted
* to a a regular page before we got a
* reference on it.
*/
if (unlikely(!PageHead(page_head))) {

Hugepage がこの時点までに分割されていないかどうかチェック。

/* PageHead is cleared after PageTail */
smp_rmb();
VM_BUG_ON(PageTail(page));
goto out_put_head;
}

/*
* Only run compound_lock on a valid PageHead,
* after having it pinned with
* get_page_unless_zero() above.
*/
smp_mb();
/* page_head wasn't a dangling pointer */
flags = compound_lock_irqsave(page_head);

ここで、__split_huge_page_refcount() と排他。

if (unlikely(!PageTail(page))) {

compound_lock を取ってみたが、split が一足早かったようで、もはや当該ページ
が tail ページではなくなってしまっていた場合、個別のページとして put する。

/* __split_huge_page_refcount run before us */
compound_unlock_irqrestore(page_head, flags);
VM_BUG_ON(PageHead(page_head));
out_put_head:
if (put_page_testzero(page_head))
__put_single_page(page_head);
out_put_single:
if (put_page_testzero(page))
__put_single_page(page);
return;
}
VM_BUG_ON(page_head != page->first_page);
/*
* We can release the refcount taken by
* get_page_unless_zero now that
* split_huge_page_refcount is blocked on the
* compound_lock.
*/
if (put_page_testzero(page_head))
VM_BUG_ON(1);
/* __split_huge_page_refcount will wait now */
VM_BUG_ON(atomic_read(&page->_count) <= 0);
atomic_dec(&page->_count);
VM_BUG_ON(atomic_read(&page_head->_count) <= 0);
compound_unlock_irqrestore(page_head, flags);
if (put_page_testzero(page_head)) {
if (PageHead(page_head))
__put_compound_page(page_head);

今 put しようとしてた tail ページへの参照が、当該 hugepage の最後の参照
カウントなので、hugepage のまま解放する。

else
__put_single_page(page_head);
}
} else {
/* page_head is a dangling pointer */
VM_BUG_ON(PageTail(page));
goto out_put_single;
}
} else if (put_page_testzero(page)) {
if (PageHead(page))
__put_compound_page(page);
else
__put_single_page(page);
}
}

compound_lock がケアする競合は具体的には以下のようなもの。


ケース1

__split_huge_page_refcount put_compound_page
------------------------------------------------------------------
compound_lock
for each subpage check PageTail && get_page_unless_zero(head)
set refcount on tails check !PageHead(head)
set pageflags on tails // このあたり lock 外なのでタイミング依存に
ClearPageCompound // なるが、あとで再チェックするので OK
compound_unlock
compound_lock_irqsave
check tail or regular sized ?
atomic_dec(&page->_count)
compound_unlock_irqrestore
if (put_page_test_zero(head))
if (PageHead(head))
__put_compound_page()
else
__put_single_page()


ケース2

__split_huge_page_refcount put_compound_page
------------------------------------------------------------------
check PageTail && get_page_unless_zero(head)
check !PageHead(head)
compound_lock_irqsave
check tail or regular sized ?
atomic_dec(&page->_count)
compound_unlock_irqrestore
// もう decrement 済んでいるので無問題
compound_lock
for each subpage
set refcount on tails
set pageflags on tails
ClearPageCompound // 以下 split と同時実行されるけど
compound_unlock // 直前に PageHead をチェックするから OK ?
if (put_page_test_zero(head))
if (PageHead(head))
__put_compound_page()
else
__put_single_page()

最後に

現状では、anonymous page の THP しか対応していないが、pagecache/swapcache/tmpfs への拡張するパッチはマージに向けて準備中のようだ。また KSM ページをスキャン対象に含めることも考えているようだ。現状では THP に対応している他のサブシステムは futex だけで、多くは THP を分割してから扱っているが、次は mremap() を THP 対応させるつもりでいるようだ。(http://lwn.net/Articles/419933/ の FAQ 参照)

というわけで、まだまだネタになりそうだということで、本記事を締める。

*1:システムの稼働中に THP の設定 (enable や defrag) を変更した場合、変更した設定内容は設定後に起動したプロセスにのみ反映される。既存のプロセスの THP の設定を変更するにはプロセスを再起動する必要がある。