1.開場白
環境:
處理器架構:arm64
內核源碼:linux-5.10.50
ubuntu版本:20.04.1
代碼閱讀工具:vim+ctags+cscope
Linux內核由於存在page cache, 一般修改的文件數據並不會馬上同步到磁盤,會緩存在內存的page cache中,我們把這種和磁盤數據不一致的頁稱為臟頁,臟頁會在合適的時機同步到磁盤。為了回寫page cache中的臟頁,需要標記頁為臟。
臟頁跟蹤是指內核如何在合適的時機記錄文件頁為臟,以便內核在進行臟頁回寫時,知道將哪些頁面回寫到磁盤。匿名頁不需要跟蹤臟頁,因為不需要同步到磁盤;私有文件頁也不需要跟蹤臟頁,因為映射的時候,可寫頁會映射為只讀,寫訪問會發生寫時復制,轉變為匿名頁;所以只有共享的文件頁需要跟蹤臟頁。跟蹤有兩個層面:一個是頁表項記錄,一個是頁描述符記錄。
訪問文件頁有兩種方式:一種是通過mmap映射文件,一種是通過文件系統的write接口操作文件,本文將對這兩種方式進行講解。在Linux內核中,因為跟蹤臟頁會涉及到文件回寫、缺頁異常、反向映射等技術,所以本文也重點講解在Linux內核中如何跟蹤臟頁。
2.mmap映射的文件頁
基本過程如下:
1)通過mmap映射共享文件。
2)第一次訪問文件頁時,發生缺頁后讀文件頁到page cache, 如果是寫訪問則設置相應進程的頁表項為臟、可寫。
3)臟頁回寫時,會通過反向映射機制,查找映射這個頁的每一個vma, 設置相應進程的頁表項為只讀,清臟標記。
4)假如第二次寫訪問這個文件頁時,臟頁的處理有兩種情況:
page cache中的文件頁還未回寫到磁盤(3步驟之前), 此刻,這個文件頁依然是臟頁。因為相應進程的頁表項為臟、可寫,所以可以直接寫這個頁。
page cache中的文件頁已經回寫到磁盤(3步驟之后), 此刻,這個文件頁不再是臟頁。因為頁表項為只讀,所以寫訪問會發生寫時復制缺頁異常,異常處理中將處理共享文件頁映射,重新將相應進程的頁表項為設置為臟、可寫。
分析如下:
2.1 第一次寫訪問文件頁時
//mm/memory.c
handle_pte_fault
->do_fault
->do_shared_fault
->__do_fault //讀文件頁到page cache
->do_page_mkwrite
->vmf->vma->vm_ops->page_mkwrite()
->filemap_page_mkwrite, //對於ext2
->set_page_dirty(page)
->__set_page_dirty_buffers
->__set_page_dirty//page cache中標記頁為臟
->TestSetPageDirty(page) //設置頁描述符臟標記
->finish_fault //設置頁表項
->alloc_set_pte
->if (write)
entry = maybe_mkwrite(pte_mkdirty(entry), vma) //設置頁表項臟、可寫
2.2 臟頁回寫時
//mm/page-writeback.c
write_cache_pages
->clear_page_dirty_for_io(page) //對於回寫的每一個頁
->page_mkclean(page) //清臟標記 mm/rmap.c
->page_mkclean_one //反向映射查找這個頁的每個vma,調用清臟標記和寫保護處理
->entry = pte_wrprotect(entry); //寫保護處理,設置只讀
entry = pte_mkclean(entry); //清臟標記 set_pte_at(vma->vm_mm, address, pte, entry) //設置到頁表項中
->TestClearPageDirty(page) //清頁描述符臟標記
2.3 第二次寫訪問文件頁時
1)臟頁還沒有回寫時(確切的說是調用clear_page_dirty_for_io之前),頁描述符已經設置了臟標記,頁表項已經設置了臟標記、可寫。
這時可以直接寫訪問文件頁,不會發生缺頁。
2)臟頁已經回寫時(確切的說是調用clear_page_dirty_for_io之后),頁描述符已經清除了臟標記,頁表項已經清除了臟標記,且只讀。
這時寫訪問文件頁會發生寫時復制缺頁異常(訪問權限錯誤缺頁)。
調用鏈如下:
//mm/memory.c
handle_pte_fault
->if (vmf->flags & FAULT_FLAG_WRITE) { //vma可寫
if (!pte_write(entry)) //頁表項沒有可寫屬性 return do_wp_page(vmf) //寫時復制缺頁異常處理
do_wp_page
->} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) == (VM_WRITE|VM_SHARED))) { //是共享可寫的文件映射vma
return wp_page_shared(vmf);
->do_page_mkwrite
->vmf->vma->vm_ops->page_mkwrite()
->filemap_page_mkwrite, //對於ext2 ->set_page_dirty(page)
->__set_page_dirty_buffers //page cache中標記頁為臟
->TestSetPageDirty(page) //設置頁描述符臟標記
->finish_mkwrite_fault
->wp_page_reuse
->entry = maybe_mkwrite(pte_mkdirty(entry), vma) //重新設置頁表項臟、可寫
2.4 再次寫訪問
重復上面步驟。
3.write接口操作的文件頁
由於通過write接口訪問文件頁時,會讀取文件頁到page cache,不會映射到任何進程地址空間,所有這種方式跟蹤臟頁是通過設置/清除頁描述符臟標記來實現。
3.1 第一次寫訪問文件頁時
會首先讀文件頁到page cache,然后將用戶空間寫緩沖區數據寫到page cache,調用鏈如下:
ext2_file_write_iter //fs/ext2/file.c
->generic_file_write_iter //mm/filemap.c
->__generic_file_write_iter
->generic_perform_write
->a_ops->write_begin() //寫之前處理 分配page cache頁 ->iov_iter_copy_from_user_atomic //戶空間寫緩沖區數據寫到page cache頁 -> a_ops->write_end() //寫之后處理
->block_write_end
->__block_commit_write
->mark_buffer_dirty
if (!TestSetPageDirty(page)) { //設置頁描述符臟標記 ->__set_page_dirty //設置頁為臟(設置頁描述符臟標記)
3.2 臟頁回寫時
write_cache_pages //mm/page-writeback.c
->clear_page_dirty_for_io
->TestClearPageDirty(page) //清除頁描述符臟標記
3.3 第二次寫訪問文件頁時
臟頁回寫之前,頁描述符臟標志位依然被置位,等待回寫, 不需要設置頁描述符臟標志位。
臟頁回寫之后,頁描述符臟標志位是清零的,文件寫頁調用鏈會設置頁描述符臟標志位。
4.總結
1)對於mmap映射的共享文件頁,因為這個文件頁可能會被多個進程共享到多個vma中,所以通過頁表項的臟標志位來跟蹤臟頁:第一次寫訪問發生缺頁異常會讀文件頁到page cache中並設置進程的頁表項的臟標志,回寫之前(clear_page_dirty_for_io完成之前),頁表項的臟標志是置位的,回寫的時候(clear_page_dirty_for_io的調用)會通過反向映射機制將所有映射這個頁的頁表項的臟標志位清零並設置只讀權限,回寫之后(clear_page_dirty_for_io完成之后),再次的寫訪問會發生寫時復制缺頁異常,再次設置頁表項的臟標志位,如此重復,從而跟蹤了臟頁。
2)對於直接通過write接口訪問的文件頁,因為這個文件頁只會被讀取到page cache中,並沒有映射到任何進程地址空間,進程寫訪問是通過copy_from_user的方式,所以通過頁描述符記錄臟頁。回寫之前(clear_page_dirty_for_io完成之前),寫文件的時候通過文件系統的寫文件的調用鏈會設置頁描述符臟標志位,回寫的時候(clear_page_dirty_for_io的調用)會清除頁描述符臟標志位,回寫之后(clear_page_dirty_for_io完成之后),再次通過write接口寫訪問時,再次通過文件系統的寫文件的調用鏈會再次設置頁描述符臟標志位,如此重復,從而跟蹤了臟頁。