/proc/pid/pagemapの話

前回 /proc/pid/pagemap について簡単に触れたので,実際にpagemapを読み取るときどういうことが起きるか少し追ってみようと思います.

/proc/pid/pagemapに関する操作はカーネルfs/proc/task_mmu.c内でproc_pagemap_operationsに定義してあります.

https://github.com/torvalds/linux/blob/v4.11/fs/proc/task_mmu.c#L1463

const struct file_operations proc_pagemap_operations = {
    .llseek     = mem_lseek, /* borrow this */
    .read       = pagemap_read,
    .open       = pagemap_open,
    .release    = pagemap_release,
};

要するに,/proc/pid/pagemapをreadする場合はpagemap_read()が呼ばれるということです. pagemap_read()の処理を抜粋すると以下のようになります.

https://github.com/torvalds/linux/blob/v4.11/fs/proc/task_mmu.c#L1351

static ssize_t pagemap_read(struct file *file, char __user *buf,
               size_t count, loff_t *ppos)
{
   struct mm_struct *mm = file->private_data;
   struct pagemapread pm;
   struct mm_walk pagemap_walk = {};
   unsigned long src;
   unsigned long svpfn;
   unsigned long start_vaddr;
   unsigned long end_vaddr;
...
   pagemap_walk.pmd_entry = pagemap_pmd_range;
   pagemap_walk.pte_hole = pagemap_pte_hole;
#ifdef CONFIG_HUGETLB_PAGE
   pagemap_walk.hugetlb_entry = pagemap_hugetlb_range;
#endif
   pagemap_walk.mm = mm;
   pagemap_walk.private = ±

   src = *ppos;
   svpfn = src / PM_ENTRY_BYTES;
   start_vaddr = svpfn << PAGE_SHIFT;
   end_vaddr = mm->task_size;
...
   while (count && (start_vaddr < end_vaddr)) {
        ...
       down_read(&mm->mmap_sem);
       ret = walk_page_range(start_vaddr, end, &pagemap_walk);
       up_read(&mm->mmap_sem);
       start_vaddr = end;

       len = min(count, PM_ENTRY_BYTES * pm.pos);
       if (copy_to_user(buf, pm.buffer, len)) {
           ret = -EFAULT;
           goto out_free;
       }
       copied += len;
       buf += len;
       count -= len;
   }

この関数ではread()のオフセットから求めたい仮想アドレスを計算し(start_vaddr)その後 walk_page_range() 関数を呼びます.このwalk_page_range()関数は https://github.com/torvalds/linux/blob/master/mm/pagewalk.c#L283 に定義されており,指定したページ範囲を探索し,必要に応じて設定したコールバック関数を呼ぶ関数です.

今回の場合,基本的には walk_page_range() => __walk_page_range() => walk_pgd_range() => walk_pud_range() => walk_pmd_range() と探索していったのち,walk->pmd_entry() が呼ばれます.このwalk->pmd_entry()pagemap_read()の前半部分で設定したコールバック関数で,実体はpagemap_pmd_range()です.

pagemap_pmd_range()の主要部を抜き出すと以下のようになっています. https://github.com/torvalds/linux/blob/v4.11/fs/proc/task_mmu.c#L1205

static int pagemap_pmd_range(pmd_t *pmdp, unsigned long addr, unsigned long end,
                 struct mm_walk *walk)
{
    ...
    orig_pte = pte = pte_offset_map_lock(walk->mm, pmdp, addr, &ptl);
    for (; addr < end; pte++, addr += PAGE_SIZE) {
        pagemap_entry_t pme;

        pme = pte_to_pagemap_entry(pm, vma, addr, *pte);
        err = add_to_pagemap(addr, &pme, pm);
        if (err)
            break;
    }
    ....
}

また,add_to_pagemap()は以下のようになっています. https://github.com/torvalds/linux/blob/v4.11/fs/proc/task_mmu.c#L1121

static int add_to_pagemap(unsigned long addr, pagemap_entry_t *pme,
              struct pagemapread *pm)
{
    pm->buffer[pm->pos++] = *pme;
    if (pm->pos >= pm->len)
        return PM_END_OF_BUFFER;
    return 0;
}

やっていることはpmdからpteを求め,そのptepte_to_pagemap_entryでpagemapの64bitのエントリに変換し,add_to_pagemapでそれをバッファに保存します.こうして得られたpagemapのエントリを最終的にpagemap_read()のい中でcopy_to_user()でコピーしています.

ということで,pagemapをreadするとページテーブルを探索してそれの結果を返すという,そりゃそうだろうという話ですがこうなっているという話でした.