コピーオンライト
forkで親が子を作成すると、親の複製として子が作成される。通常execで新規のプロセスと差し替えられる(正確にはプロセスは同じだが中身が異なる。)わけで、一見無駄に思える処理の流れだが、親の複製を作成して動作させるシステムもあるわけで、この流れの延長線上にexecでの新規プロセス作成があるものと認識している。
そして、親の複製を低負荷にする手法がコピーオンライトまたはCOWと呼ばれるものである。(COWってカウって呼ぶのかな?それともC,O,Wかな?)
forkで子プロセスを作成すると、子は独自のページテーブルおよびmm_struct等の構造体を確保する。ただし中身は親の内容を複写する。これだけで子は親と同じメモリー空間を共有することになる。親の複製と言われる所以である。しかし通常、親/子はメモリに書き込んだ値は、互いに影響してはならない独自のプロセスである。
コピーオンライトは、親のmm_area_structにおいて書込みメモリー空間を、ページテーブルでは書込み不可とする。子もこのメモリー空間は親と同じように認識することになる。そしてどちらかのプロセスが書き込みを行うと、ページフォルトが発生し、そのページの参照カウンターが複数の場合(共有している。)、COWとして認識する。そして新規にページを確保し、元ページの内容を確保したページに複写し、そこに書き込み処理を行う。そして元ページの参照カウンタをデクリメントし、もう一つのプロセスがこのページを書き込みをおこなうと、参照カウントから自分しか参照していないことになると、そのページを書込み可とし、そのページに書き込み処理を行うという具合である。
ページフォルトが発生すると、 do_page_fault関数にハンドルされる。そこからユーザプロセスかカーネルプロセスのエラーか?とかカーネル空間でのエラーか、ユーザ区間でのエラーか?とかのチェックが行われ、かかる処理はhandle_pte_fault関数へと渡ってくる。ここではまずページテーブルエントリーのページが実メモリ上に存在するかチェックする。新規にallocされたメモリー空間とかスワップアウトされたケースがこれにあたる。そうでなければ、そのページは実メモリ上にあるということだ。write_accessで書き込みで、しかもページテーブルが書き込み不可故にページフォルトが発生したわけだ。そしてdo_wp_page関数がコールされる。
まず、問題のページを取得する。(この関数ではページ有るということでの処理であって、そのあとのページが取得できなければのif文のチェックの意味が今ひとつ分かりません。カーネルにはそのようなエラーチェックが沢山ありますが・・・、でたぶんページフォルトの処理では、スワップインとかメモリーマップとかで、ファイル読み込みが発生するわけで、その時にプロセスが切り替わるための処理だと勝手に解釈することにしています。)
PageAnon関数で無名ページかのチェックを行う。これはpage->mappingにPAGE_MAPPING_ANONビットがたっているかどうかで判断する。無名ページだとcan_share_swap_page関数をコールし、ここで上で述べたようなCOWを行うかどうかのチェックを行う。can_share_swap_page関数ではpage->_mapcountをチェックするが、can_share_swap_pageのshare_swapと名が示すようにswap処理のキャッシュを考慮する必要がある。スワップ処理ではスワップスラッシングを防ぐため、スワップアウト処理ではいきなりファイルに書き込まず、ページをいったんスワップキャッシュ状態に置くため、それを足しこんだものでチェックしているよう。でそれらが1つしか無い0をそれ以外なら-1をreuseにセットするようになっている。
reuseが0でないなら、そのページに書き込むことになる。そのページテーブルにアクセスフラグ、ダーティフラグ、書き込みフラグを設定している。
reuseが0の場合、COWとなる。新しいページを確保して、cow_user_page関数で元ページの内容を新しいページに複写し、そのページの書き込み可等のフラグを設定し、新しいページをvm_area_structのリストに繋いだり、また反対に古いページはそのリストから削除したりとしている。
そして、親の複製を低負荷にする手法がコピーオンライトまたはCOWと呼ばれるものである。(COWってカウって呼ぶのかな?それともC,O,Wかな?)
forkで子プロセスを作成すると、子は独自のページテーブルおよびmm_struct等の構造体を確保する。ただし中身は親の内容を複写する。これだけで子は親と同じメモリー空間を共有することになる。親の複製と言われる所以である。しかし通常、親/子はメモリに書き込んだ値は、互いに影響してはならない独自のプロセスである。
コピーオンライトは、親のmm_area_structにおいて書込みメモリー空間を、ページテーブルでは書込み不可とする。子もこのメモリー空間は親と同じように認識することになる。そしてどちらかのプロセスが書き込みを行うと、ページフォルトが発生し、そのページの参照カウンターが複数の場合(共有している。)、COWとして認識する。そして新規にページを確保し、元ページの内容を確保したページに複写し、そこに書き込み処理を行う。そして元ページの参照カウンタをデクリメントし、もう一つのプロセスがこのページを書き込みをおこなうと、参照カウントから自分しか参照していないことになると、そのページを書込み可とし、そのページに書き込み処理を行うという具合である。
ページフォルトが発生すると、 do_page_fault関数にハンドルされる。そこからユーザプロセスかカーネルプロセスのエラーか?とかカーネル空間でのエラーか、ユーザ区間でのエラーか?とかのチェックが行われ、かかる処理はhandle_pte_fault関数へと渡ってくる。ここではまずページテーブルエントリーのページが実メモリ上に存在するかチェックする。新規にallocされたメモリー空間とかスワップアウトされたケースがこれにあたる。そうでなければ、そのページは実メモリ上にあるということだ。write_accessで書き込みで、しかもページテーブルが書き込み不可故にページフォルトが発生したわけだ。そしてdo_wp_page関数がコールされる。
static inline int handle_pte_fault(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, pte_t *pte, pmd_t *pmd, int write_access) { pte_t entry; spinlock_t *ptl; entry = *pte; if (!pte_present(entry)) { : : } ptl = pte_lockptr(mm, pmd); spin_lock(ptl); if (unlikely(!pte_same(*pte, entry))) goto unlock; if (write_access) { if (!pte_write(entry)) return do_wp_page(mm, vma, address, pte, pmd, ptl, entry); entry = pte_mkdirty(entry); } : : return 0; }do_wp_page関数でCOWが行われる。ここでは無名ページとメモリーマップのページについてCOWの処理がなされていて、(メモリーマップかかる処理は今ひとつ・・・。です。)無名ページについて。
まず、問題のページを取得する。(この関数ではページ有るということでの処理であって、そのあとのページが取得できなければのif文のチェックの意味が今ひとつ分かりません。カーネルにはそのようなエラーチェックが沢山ありますが・・・、でたぶんページフォルトの処理では、スワップインとかメモリーマップとかで、ファイル読み込みが発生するわけで、その時にプロセスが切り替わるための処理だと勝手に解釈することにしています。)
PageAnon関数で無名ページかのチェックを行う。これはpage->mappingにPAGE_MAPPING_ANONビットがたっているかどうかで判断する。無名ページだとcan_share_swap_page関数をコールし、ここで上で述べたようなCOWを行うかどうかのチェックを行う。can_share_swap_page関数ではpage->_mapcountをチェックするが、can_share_swap_pageのshare_swapと名が示すようにswap処理のキャッシュを考慮する必要がある。スワップ処理ではスワップスラッシングを防ぐため、スワップアウト処理ではいきなりファイルに書き込まず、ページをいったんスワップキャッシュ状態に置くため、それを足しこんだものでチェックしているよう。でそれらが1つしか無い0をそれ以外なら-1をreuseにセットするようになっている。
reuseが0でないなら、そのページに書き込むことになる。そのページテーブルにアクセスフラグ、ダーティフラグ、書き込みフラグを設定している。
reuseが0の場合、COWとなる。新しいページを確保して、cow_user_page関数で元ページの内容を新しいページに複写し、そのページの書き込み可等のフラグを設定し、新しいページをvm_area_structのリストに繋いだり、また反対に古いページはそのリストから削除したりとしている。
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, pte_t *page_table, pmd_t *pmd, spinlock_t *ptl, pte_t orig_pte) { : : old_page = vm_normal_page(vma, address, orig_pte); if (!old_page) { if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) == (VM_WRITE|VM_SHARED)) goto reuse; goto gotten; } if (PageAnon(old_page)) { if (trylock_page(old_page)) { reuse = can_share_swap_page(old_page); unlock_page(old_page); } } else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) == (VM_WRITE|VM_SHARED))) { たぶんメモリーマップにかかる処理 : : } if (reuse) { reuse: flush_cache_page(vma, address, pte_pfn(orig_pte)); entry = pte_mkyoung(orig_pte); entry = maybe_mkwrite(pte_mkdirty(entry), vma); if (ptep_set_access_flags(vma, address, page_table, entry,1)) update_mmu_cache(vma, address, entry); ret |= VM_FAULT_WRITE; goto unlock; } page_cache_get(old_page); gotten: pte_unmap_unlock(page_table, ptl); if (unlikely(anon_vma_prepare(vma))) goto oom; VM_BUG_ON(old_page == ZERO_PAGE(0)); new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address); if (!new_page) goto oom; cow_user_page(new_page, old_page, address, vma); __SetPageUptodate(new_page); page_table = pte_offset_map_lock(mm, pmd, address, &ptl); if (likely(pte_same(*page_table, orig_pte))) { if (old_page) { if (!PageAnon(old_page)) { dec_mm_counter(mm, file_rss); inc_mm_counter(mm, anon_rss); } } else inc_mm_counter(mm, anon_rss); flush_cache_page(vma, address, pte_pfn(orig_pte)); entry = mk_pte(new_page, vma->vm_page_prot); entry = maybe_mkwrite(pte_mkdirty(entry), vma); ptep_clear_flush_notify(vma, address, page_table); set_pte_at(mm, address, page_table, entry); update_mmu_cache(vma, address, entry); lru_cache_add_active(new_page); page_add_new_anon_rmap(new_page, vma, address); if (old_page) { page_remove_rmap(old_page, vma); } : : return ret; }