LoginSignup
9
8

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-09-30

前回

どうもです。@akachochinです。久しぶりです。お元気ですか?
10月はカーネル系のイベントがたくさん開催されるようで、色々楽しみです。

前にも書きましたが、他にも調べているものがあったりで遅くなってます。
けれど、遅々でも続けて行きます。

今回やること

前回、handle_pte_fault()を見ました。
その中で以下4パターンがあったことを覚えているでしょうか。

PTE バッキングストア 呼び出す関数
未割り当て あり(かつfaultルーチンあり) do_linear_fault
未割り当て なし do_anonymous_page
何か割当済 あり do_nonlinear_fault
何か割当済 なし do_swap_page

今回は、最初に書いたdo_linear_fault()を見ます。

mm/memory.c
static int do_linear_fault(struct mm_struct *mm, struct vm_area_struct *vma,
    unsigned long address, pte_t *page_table, pmd_t *pmd,
    unsigned int flags, pte_t orig_pte)
{
  pgoff_t pgoff = (((address & PAGE_MASK)
      - vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;

  pte_unmap(page_table);
  /* 読み込みの場合は、常にdo_read_fault()で捌く */
  if (!(flags & FAULT_FLAG_WRITE))
    return do_read_fault(mm, vma, address, pmd, pgoff, flags,
        orig_pte);
  /* バッキングストアは通常書き込みが共有される。しかし、COWの場合は
     共有されないので、COWのハンドリングをする */
  if (!(vma->vm_flags & VM_SHARED))
    return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
        orig_pte);
  /* 共有しているメモリ空間への書き込みを捌く */
  return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
}

do_read_fault()、do_shared_fault()は措いて、まず、COWからみましょう。

COW(Copy On Write)

復習ですが、Copy On Writeとは「あるメモリ空間をあたかもコピーしたかのように見せかけ、実際の物理メモリ割り当て&コピーを行うのは実際に書き込みがあるときまで遅延させる技術」です。

代表的な使用例はfork()でしょう。

普通、プロセスは各プロセスごとに独立したメモリ空間を持つことができます。
親プロセスがfork()して子プロセスを作ったときに、親プロセスと子プロセスは同じメモリ内容を持ちます。
しかし、fork()したあとは大抵exec()するので、fork()時点でわざわざ物理メモリを本当に割り当てるのは領域的に無駄ですし、親プロセスのメモリ内容をコピーしなくてはいけないのでCPUリソース的にも無駄です。
よって、とりあえずは親と子の間で物理メモリを共有します。
これなら無駄なCPUリソースとメモリリソースを浪費しなくてすみます。

しかし、読むだけならともかく、どちらか一方が書き込みをした場合、それがもう片方に見えてはいけません。あくまでも「各プロセスごとに独立したメモリ空間を持つ」からです。
よって、書き込みがあったときに初めてメモリを割り当て、コピーします。
よって、COWには以下が必要になります。

(1)一時的に共有しているページ
(2)実際の書き込みがあったときに割り当てられるページ

これを踏まえてソースを見ましょう。

mm/memory.c
static int do_cow_fault(struct mm_struct *mm, struct vm_area_struct *vma,
    unsigned long address, pmd_t *pmd,
    pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
  /* 略 */
  new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
  if (!new_page)
    return VM_FAULT_OOM;

まずは、ページ構造体を割り当てます。第一引数はzoneを指定します。
ここで割り当てているページは、「(2)実際の書き込みがあったときに割り当てられるページ」です。

GPF_HIGHUSER_MOVABLEについて

zone(GPF_HIGHUSER_MOVABLE)は、以下のコメントのとおりです。movableはその名の通り、ページの割り当てを移動可能ということです。
想像するに、ある仮想アドレスに対する物理ページを中身をコピーしたうえで別の 物理メモリに割り当てられそうです。(page migration)
また、HIGHMEMなので、NORMALと違い仮想的かつ物理的に連続していません。よって、都度仮想アドレスに対して物理ページを割り当てなくてはいけません。

include/linux/gfp.h
 * __GFP_MOVABLE: Flag that this page will be movable by the page migration
 * mechanism or reclaimed
/* 略 */
#define GFP_HIGHUSER_MOVABLE  (__GFP_WAIT | __GFP_IO | __GFP_FS | \
         __GFP_HARDWALL | __GFP_HIGHMEM | \
         __GFP_MOVABLE)
/* 略 */
 * The zone fallback order is MOVABLE=>HIGHMEM=>NORMAL=>DMA32=>DMA.
 * But GFP_MOVABLE is not only a zone specifier but also an allocation
 * policy. Therefore __GFP_MOVABLE plus another zone selector is valid.
 * Only 1 bit of the lowest 3 bits (DMA,DMA32,HIGHMEM) can be set to "1".

次に、__do_fault()を呼び、先に説明した「(1)一時的に共有しているページ」を割り当てます。

mm/memory.c
  ret = __do_fault(vma, address, pgoff, flags, &fault_page);
  if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
    goto uncharge_out;

そして、「(1)一時的に共有しているページ」の内容を「(2)実際の書き込みがあったときに割り当てられるページ」にコピーします。

mm/memory.c
  copy_user_highpage(new_page, fault_page, address, vma);

そのあとは、あらためて仮想アドレスに対して、「(2)実際の書き込みがあったときに割り当てられるページ」を紐付けします。

mm/memory.c
  do_set_pte(vma, address, new_page, pte, true, true);
  pte_unmap_unlock(pte, ptl);
  unlock_page(fault_page);

そして、「(1)一時的に共有しているページ」ですが、page_cache_release()を使って返却します。もちろんリファレンスカウンタ(参照数)が0にならない(=他の人が使っている場合)は解放されません。

mm/memory.c
  page_cache_release(fault_page);
  return ret; 

念の為、確認します。
page_cache_release()は内部でput_page()を呼び出します。

mm/swap.c
void put_page(struct page *page)
{
  if (unlikely(PageCompound(page))) /* コメント見ると特殊ケースのため略 */
    put_compound_page(page);
  else if (put_page_testzero(page))
    __put_single_page(page);
}     
EXPORT_SYMBOL(put_page);

put_page_testzero()は以下のとおり、アトミック命令を使って減算と0チェックを同時に行っています。

include/linux/mm.h
static inline int put_page_testzero(struct page *page)
{   
  VM_BUG_ON_PAGE(atomic_read(&page->_count) == 0, page);
  return atomic_dec_and_test(&page->_count);
}

つまり、参照数デクリメントの結果、参照数0になったら、__put_single_page()で返却します。

少しだけ奇妙に思える点(きっと何かを見落としているのだと思う)

少し奇妙に思えます。
__do_fault()の時点で、「(1)一時的に共有しているページ」を取得した際、該当ページへの参照数が1の場合に、コピー&解放をせずにしれっと今回解決すべき仮想アドレスに「(1)一時的に共有しているページ」を紐付けてしまえば、メモリコピーも発生せずに良いかな・・・なんて安直に考えてしまいました。
・・・おそらく私が何か見落としているか誤っているかだと思います・・・。

次回

今回はここまでにします。
次回はdo_read_fault()、do_shared_fault()を見ることにします。

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