LoginSignup
9
8

More than 5 years have passed since last update.

Linux(x86-32bit)のページフォルトハンドラを読んでみる(その4)

Posted at

前回

どうもです。@akachochinです。飲んだりして緩くやっています。お元気ですか?
文書書きながらということと、他にも追っているものがあり、最近少し時間がとれていなかったので遅くなりました。けれど、遅々でも続けて行きます。

(カーネルのソース読むのは、結構おもしろいですよ。カーネル初心者は、まず気になるシステムコール、簡単そうかな、と思ったシステムコールを読んでみると少しずつ読めるようになってきます。あと、Linuxだと書籍も結構出ているのでそれを参考にして読み進めてもよいと思います。)

前回は、handle_mm_fault()のところまで読みました。
かなり間が開いてしまいましたが、とりあえずざっくりと読んでみたいと思います。

対象となるバージョン

少し古いですが、3.15.6を対象とします。
理由は手元にあったのがこのバージョンなだけです。骨格は恐らく変わらないかと思いますが、cgroupsに関してはどうなるかわかりません。ひと通りソースを読んだら、新しいcgroupがどうなっているのか見るのも良いかもしれません。

handle_mm_fault()を読む

mm/memory.c
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
        unsigned long address, unsigned int flags)
{
  int ret;

  __set_current_state(TASK_RUNNING);

これは、現在のタスク(プロセス)の状態をTASK_RUNNINGにする処理です。

include/linux/sched.h
#define __set_current_state(state_value)      \
  do { current->state = (state_value); } while (0)

これはちょっと謎です。何故って、ユーザ空間でコード実行中でないとページフォルトは起き得ません。つまり、このときはTASK_RUNNINGなわけで・・・。
これまで見てきた処理では、カーネル空間でコードを実行中に発生したpage faultではここにたどり着かないと思われます。よって、特殊なケースは考えづらいものがあります。
Linuxでは何かあるのかもしれません。とりあえず措いておきます。

統計情報

さて、次は各種統計情報の更新。

mm/memory.c
  count_vm_event(PGFAULT);

まずは、count_vm_event()。

include/linux/vmstat.h
/*
 * Light weight per cpu counter implementation.
 *
 * Counters should only be incremented and no critical kernel component
 * should rely on the counter values.
 *
 * Counters are handled completely inline. On many platforms the code
 * generated will simply be the increment of a global address.
 */

struct vm_event_state {
  unsigned long event[NR_VM_EVENT_ITEMS];
};

DECLARE_PER_CPU(struct vm_event_state, vm_event_states);

/* 略 */
static inline void count_vm_event(enum vm_event_item item)
{
  this_cpu_inc(vm_event_states.event[item]);
}

カーネル内で使われる簡易なイベントカウンタのようです。次に進みましょう。

mm/memory.c
  mem_cgroup_count_vm_event(mm, PGFAULT);

cgroup関係のようです。

またも寄り道。cgroupをちょっとだけ

cgroupとは、

今回出てきたmem_cgroup_count_vm_event()は以下の実装です。

include/linux/memcontrol.h
static inline void mem_cgroup_count_vm_event(struct mm_struct *mm,
               enum vm_event_item idx)
{
  if (mem_cgroup_disabled())
    return;
  __mem_cgroup_count_vm_event(mm, idx);
}
mm/memcontrol.c
void __mem_cgroup_count_vm_event(struct mm_struct *mm, enum vm_event_item idx)
{   
  struct mem_cgroup *memcg;

  rcu_read_lock();
  memcg = mem_cgroup_from_task(rcu_dereference(mm->owner));
  if (unlikely(!memcg))
    goto out;

  switch (idx) {
  case PGFAULT:
    this_cpu_inc(memcg->stat->events[MEM_CGROUP_EVENTS_PGFAULT]);
    break;
  case PGMAJFAULT:
    this_cpu_inc(memcg->stat->events[MEM_CGROUP_EVENTS_PGMAJFAULT]);
    break;
  default:
    BUG();
  }
out:
  rcu_read_unlock();
}

やっていることは、mem_cgroup構造体を取得して、中のカウンタをインクリメントすることだけ。極めて単純です。なので、今回は先に進みます。

mm/memory.c
  /*
   * Enable the memcg OOM handling for faults triggered in user
   * space.  Kernel faults are handled more gracefully.
   */
  if (flags & FAULT_FLAG_USER)
    mem_cgroup_oom_enable();

  ret = __handle_mm_fault(mm, vma, address, flags);

  if (flags & FAULT_FLAG_USER) {
    mem_cgroup_oom_disable();
                /*
                 * The task may have entered a memcg OOM situation but
                 * if the allocation error was handled gracefully (no
                 * VM_FAULT_OOM), there is no need to kill anything.
                 * Just clean up the OOM state peacefully.
                 */
                if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM))
                        mem_cgroup_oom_synchronize(false);
  }

  return ret;
}

いよいよ__handle_mm_fault()

mm/memory.c
/*
 * By the time we get here, we already hold the mm semaphore
 */
static int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
           unsigned long address, unsigned int flags)
{
  pgd_t *pgd;
  pud_t *pud;
  pmd_t *pmd;
  pte_t *pte;

  /* 今回、HUGE TLBは飛ばします。いつかやります。 */
  if (unlikely(is_vm_hugetlb_page(vma)))
    return hugetlb_fault(mm, vma, address, flags);
mm/memory.c
  pgd = pgd_offset(mm, address);
  pud = pud_alloc(mm, pgd, address);
  if (!pud)
    return VM_FAULT_OOM;
  pmd = pmd_alloc(mm, pud, address);
  if (!pmd)
    return VM_FAULT_OOM;

「Linuxの仮想 - 物理アドレスと、カーネル空間の概要(その2)」でPUDなどページテーブルのデータ構造の概要を説明しているので、繰り返し説明しません。

p[u|m]d_alloc()は以下のいずれにも該当しない場合、NULLを返すマクロです。
(1)仮想アドレスに対応するページテーブルが既に存在する
(2)(1)で「存在しない」場合、新たなページテーブルを割り当てるが、これに失敗した

mm/memory.c

  if (pmd_none(*pmd) && transparent_hugepage_enabled(vma)) {
    int ret = VM_FAULT_FALLBACK;
    if (!vma->vm_ops)
      ret = do_huge_pmd_anonymous_page(mm, vma, address,
          pmd, flags);
    if (!(ret & VM_FAULT_FALLBACK))
      return ret;
  } else {

pmd_none()は、pmdのエントリが0、つまりPresent Bit(PDEの bit0)が立っておらず(このPMDにあたる仮想アドレスに対して物理メモリの割り当てがない)、他のエントリも空な未使用なpmdであればtrueを返します。

次のマクロは、Huge Pageを割り当てて良いケースかどうかを判断します。
参考までにマクロ実装を示します。

include/linux/huge_mm.h
#define transparent_hugepage_enabled(__vma)       \   
  ((transparent_hugepage_flags &          \   
    (1<<TRANSPARENT_HUGEPAGE_FLAG) ||       \   
    (transparent_hugepage_flags &         \\
     (1<<TRANSPARENT_HUGEPAGE_REQ_MADV_FLAG) &&     \   
     ((__vma)->vm_flags & VM_HUGEPAGE))) &&     \   
   !((__vma)->vm_flags & VM_NOHUGEPAGE) &&      \   
   !is_vma_temporary_stack(__vma))

Huge Pageとは

Huge Pageはその名の通り、「大きなページ」です。
多くのCPUアーキテクチャでは、標準的なメモリページのサイズは4KByteです。

しかし、それだと大きなサイズのメモリをマップするときにTLBを無駄遣いしたり、ページテーブルへのマップ書き込みが非常に面倒でパフォーマンスに効いてきます。
4KByteのページしか許さない場合、TLBの1エントリが4kbyteのメモリ空間の変換にしか使えません。数MByteの連続したメモリ空間にアクセスする場合、4KbyteごとにTLBのエントリを消費してしまいます。

そこで、もう少し大きな特例的なページをサポートすることにしました。
こういうページをHuge Pageとか言ったりします。(もちろんCPUアーキテクチャごとに異なります。

Intelの場合、Intel SDMのVol3. Chapter4にあるとおり、以下のとおりとなります。これはページテーブル(ディレクトリ)のエントリの設定で決まります。

Paging Mode サポートするページサイズ
32-bit 4KB or 4MB
PAE 4KB or 2MB
IA-32e 4KB or 2MB or 1GB

今回、Huge Pageがらみのif節の説明はここまでとします。
おそらく多くの場合、else側が実行されるのでそちらを説明します。

mm/memory.c
    pmd_t orig_pmd = *pmd;
    int ret;

    barrier();
    if (pmd_trans_huge(orig_pmd)) {
    /* 先の理由により、Huge Page系は今回省きます。*/
    }
  }

  /*
   * 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(pmd_none(*pmd)) &&
      unlikely(__pte_alloc(mm, vma, pmd, address)))
    return VM_FAULT_OOM;

ここは、4KByteのノーマルなメモリページを割り当てるので、pmdだけでなくpteも必要です。

mm/memory.c
  /* Huge Page系の処理は略 */

  /*
   * 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);

pte_offset_map()は「pmdの記述内容と今回のマップ対象の仮想アドレスから、今回使うPTEエントリの仮想アドレスを取得する」です。
実装を確認されたい場合、arch/x86/include/asm/pgtable_32.h
を参考にしてください。

mm/memory.c
  return handle_pte_fault(mm, vma, address, pte, pmd, flags);
}
mm/memory.c
static int handle_pte_fault(struct mm_struct *mm,
         struct vm_area_struct *vma, unsigned long address,
         pte_t *pte, pmd_t *pmd, unsigned int flags)
{
  pte_t entry;
  spinlock_t *ptl;

  entry = *pte;
  if (!pte_present(entry)) {

この関数は大きく分けると、以下2つのケースを捌きます。
(1)物理ページの割り当てのない箇所に物理ページを割り当てる
(2)既に割り当てられた物理ページの属性を変える特殊ケース

pte_present()はPTEが未使用かどうかを判定しますので、上記if文が真であれば(1)のケースに該当します。
以下は(1)のケースです。

mm/memory.c
    if (pte_none(entry)) {

pte_none()は、そもそもPTEのエントリが0(Present Bitだけでなくその他のパラメータも0)か判定します。つまり、今物理メモリの割り当てがないだけでなく、そもそも何も割り当てられていないことを判定するのです。

このような判定をする理由の主なところは、swap(物理RAMの割り当てはないが、該当仮想アドレスは使われている)でない「一般的な」ページフォルトハンドリングをすべきか判断するだと思われます。

mm/memory.c
      if (vma->vm_ops) {
        if (likely(vma->vm_ops->fault))
          return do_linear_fault(mm, vma, address,
            pte, pmd, flags, entry);
      }
      return do_anonymous_page(mm, vma, address,
             pte, pmd, flags);

Annoymous Memoryについては「cgroupsとメモリ資源と関係を勉強する前に、Linuxの仮想記憶周りを読む...」を、バッキングストアのある仮想アドレス空間の割り当てやページャについては、「Linuxのmmap()を通して、アドレス空間の扱いを垣間見る」の説明を参照お願いします。

mm/memory.c
    }
    if (pte_file(entry))
      return do_nonlinear_fault(mm, vma, address,
          pte, pmd, flags, entry);
    return do_swap_page(mm, vma, address,
          pte, pmd, flags, entry);
  }

以降は、(2)のケースで既に存在する物理ページに対する特殊なハンドリングを行っていきます。

とりあえず、今回はここで締めて、次回は(2)のケースを読んでみることにします。
そして、その後は各関数を個別に読んでいきます。

Appendix

cgoupsの関数を少しだけ覗く

いつもの悪い癖です。また寄り道します。

include/linux/memcontrol.h
static inline void mem_cgroup_oom_enable(void)
{
  WARN_ON(current->memcg_oom.may_oom);
  current->memcg_oom.may_oom = 1;
}

static inline void mem_cgroup_oom_disable(void)
{
  WARN_ON(!current->memcg_oom.may_oom);
  current->memcg_oom.may_oom = 0;
}
mm/memcontrol.c
static void mem_cgroup_oom(struct mem_cgroup *memcg, gfp_t mask, int order)
{
  if (!current->memcg_oom.may_oom)
    return;

要するにフラグを立てるだけです。ざっとは読んでみましたが、OOMがからみそうなので、OOMのところを別途読むときにでもみたいと思います。

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8