深入理解Linux內(nèi)核之臟頁跟蹤
本文轉(zhuǎn)載自微信公眾號(hào)「Linux內(nèi)核遠(yuǎn)航者」,作者Linux內(nèi)核遠(yuǎn)航者 。轉(zhuǎn)載本文請(qǐng)聯(lián)系Linux內(nèi)核遠(yuǎn)航者公眾號(hào)。
1.開場(chǎng)白
環(huán)境:
處理器架構(gòu):arm64
內(nèi)核源碼:linux-5.10.50
ubuntu版本:20.04.1
代碼閱讀工具:vim+ctags+cscope
Linux內(nèi)核由于存在page cache, 一般修改的文件數(shù)據(jù)并不會(huì)馬上同步到磁盤,會(huì)緩存在內(nèi)存的page cache中,我們把這種和磁盤數(shù)據(jù)不一致的頁稱為臟頁,臟頁會(huì)在合適的時(shí)機(jī)同步到磁盤。為了回寫page cache中的臟頁,需要標(biāo)記頁為臟。
臟頁跟蹤是指內(nèi)核如何在合適的時(shí)機(jī)記錄文件頁為臟,以便內(nèi)核在進(jìn)行臟頁回寫時(shí),知道將哪些頁面回寫到磁盤。匿名頁不需要跟蹤臟頁,因?yàn)椴恍枰降酱疟P;私有文件頁也不需要跟蹤臟頁,因?yàn)橛成涞臅r(shí)候,可寫頁會(huì)映射為只讀,寫訪問會(huì)發(fā)生寫時(shí)復(fù)制,轉(zhuǎn)變?yōu)槟涿?所以只有共享的文件頁需要跟蹤臟頁。跟蹤有兩個(gè)層面:一個(gè)是頁表項(xiàng)記錄,一個(gè)是頁描述符記錄。
訪問文件頁有兩種方式:一種是通過mmap映射文件,一種是通過文件系統(tǒng)的write接口操作文件,本文將對(duì)這兩種方式進(jìn)行講解。在Linux內(nèi)核中,因?yàn)楦櫯K頁會(huì)涉及到文件回寫、缺頁異常、反向映射等技術(shù),所以本文也重點(diǎn)講解在Linux內(nèi)核中如何跟蹤臟頁。
2.mmap映射的文件頁
基本過程如下:
1)通過mmap映射共享文件。
2)第一次訪問文件頁時(shí),發(fā)生缺頁后讀文件頁到page cache, 如果是寫訪問則設(shè)置相應(yīng)進(jìn)程的頁表項(xiàng)為臟、可寫。
3)臟頁回寫時(shí),會(huì)通過反向映射機(jī)制,查找映射這個(gè)頁的每一個(gè)vma, 設(shè)置相應(yīng)進(jìn)程的頁表項(xiàng)為只讀,清臟標(biāo)記。
4)假如第二次寫訪問這個(gè)文件頁時(shí),臟頁的處理有兩種情況:
- page cache中的文件頁還未回寫到磁盤(3步驟之前), 此刻,這個(gè)文件頁依然是臟頁。因?yàn)橄鄳?yīng)進(jìn)程的頁表項(xiàng)為臟、可寫,所以可以直接寫這個(gè)頁。
- page cache中的文件頁已經(jīng)回寫到磁盤(3步驟之后), 此刻,這個(gè)文件頁不再是臟頁。因?yàn)轫摫眄?xiàng)為只讀,所以寫訪問會(huì)發(fā)生寫時(shí)復(fù)制缺頁異常,異常處理中將處理共享文件頁映射,重新將相應(yīng)進(jìn)程的頁表項(xiàng)為設(shè)置為臟、可寫。
分析如下:
2.1 第一次寫訪問文件頁時(shí)
如果是mmap映射文件頁,在沒有填充頁表情況下,寫訪問會(huì)發(fā)生轉(zhuǎn)換表錯(cuò)誤類型的缺頁異常。
- //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, //對(duì)于ext2
- ->set_page_dirty(page)
- ->__set_page_dirty_buffers
- ->__set_page_dirty//page cache中標(biāo)記頁為臟
- ->TestSetPageDirty(page) //設(shè)置頁描述符臟標(biāo)記
- ->finish_fault //設(shè)置頁表項(xiàng)
- ->alloc_set_pte
- ->if (write)
- entry = maybe_mkwrite(pte_mkdirty(entry), vma) //設(shè)置頁表項(xiàng)臟、可寫
2.2 臟頁回寫時(shí)
- //mm/page-writeback.c
- write_cache_pages
- ->clear_page_dirty_for_io(page) //對(duì)于回寫的每一個(gè)頁
- ->page_mkclean(page) //清臟標(biāo)記 mm/rmap.c
- ->page_mkclean_one //反向映射查找這個(gè)頁的每個(gè)vma,調(diào)用清臟標(biāo)記和寫保護(hù)處理
- ->entry = pte_wrprotect(entry); //寫保護(hù)處理,設(shè)置只讀
- entry = pte_mkclean(entry); //清臟標(biāo)記 set_pte_at(vma->vm_mm, address, pte, entry) //設(shè)置到頁表項(xiàng)中
- ->TestClearPageDirty(page) //清頁描述符臟標(biāo)記
2.3 第二次寫訪問文件頁時(shí)
1)臟頁還沒有回寫時(shí)(確切的說是調(diào)用clear_page_dirty_for_io之前),頁描述符已經(jīng)設(shè)置了臟標(biāo)記,頁表項(xiàng)已經(jīng)設(shè)置了臟標(biāo)記、可寫。
這時(shí)可以直接寫訪問文件頁,不會(huì)發(fā)生缺頁。
2)臟頁已經(jīng)回寫時(shí)(確切的說是調(diào)用clear_page_dirty_for_io之后),頁描述符已經(jīng)清除了臟標(biāo)記,頁表項(xiàng)已經(jīng)清除了臟標(biāo)記,且只讀。
這時(shí)寫訪問文件頁會(huì)發(fā)生寫時(shí)復(fù)制缺頁異常(訪問權(quán)限錯(cuò)誤缺頁)。
調(diào)用鏈如下:
- //mm/memory.c
- handle_pte_fault
- ->if (vmf->flags & FAULT_FLAG_WRITE) { //vma可寫
- if (!pte_write(entry)) //頁表項(xiàng)沒有可寫屬性 return do_wp_page(vmf) //寫時(shí)復(fù)制缺頁異常處理
- 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, //對(duì)于ext2 ->set_page_dirty(page)
- ->__set_page_dirty_buffers //page cache中標(biāo)記頁為臟
- ->TestSetPageDirty(page) //設(shè)置頁描述符臟標(biāo)記
- ->finish_mkwrite_fault
- ->wp_page_reuse
- ->entry = maybe_mkwrite(pte_mkdirty(entry), vma) //重新設(shè)置頁表項(xiàng)臟、可寫
2.4 再次寫訪問
重復(fù)上面步驟。
3.write接口操作的文件頁
由于通過write接口訪問文件頁時(shí),會(huì)讀取文件頁到page cache,不會(huì)映射到任何進(jìn)程地址空間,所有這種方式跟蹤臟頁是通過設(shè)置/清除頁描述符臟標(biāo)記來實(shí)現(xiàn)。
3.1 第一次寫訪問文件頁時(shí)
會(huì)首先讀文件頁到page cache,然后將用戶空間寫緩沖區(qū)數(shù)據(jù)寫到page cache,調(diào)用鏈如下:
- 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 //戶空間寫緩沖區(qū)數(shù)據(jù)寫到page cache頁 -> a_ops->write_end() //寫之后處理
- ->block_write_end
- ->__block_commit_write
- ->mark_buffer_dirty
- if (!TestSetPageDirty(page)) { //設(shè)置頁描述符臟標(biāo)記 ->__set_page_dirty //設(shè)置頁為臟(設(shè)置頁描述符臟標(biāo)記)
3.2 臟頁回寫時(shí)
- write_cache_pages //mm/page-writeback.c
- ->clear_page_dirty_for_io
- ->TestClearPageDirty(page) //清除頁描述符臟標(biāo)記
3.3 第二次寫訪問文件頁時(shí)
臟頁回寫之前,頁描述符臟標(biāo)志位依然被置位,等待回寫, 不需要設(shè)置頁描述符臟標(biāo)志位。
臟頁回寫之后,頁描述符臟標(biāo)志位是清零的,文件寫頁調(diào)用鏈會(huì)設(shè)置頁描述符臟標(biāo)志位。
4.總結(jié)
1)對(duì)于mmap映射的共享文件頁,因?yàn)檫@個(gè)文件頁可能會(huì)被多個(gè)進(jìn)程共享到多個(gè)vma中,所以通過頁表項(xiàng)的臟標(biāo)志位來跟蹤臟頁:第一次寫訪問發(fā)生缺頁異常會(huì)讀文件頁到page cache中并設(shè)置進(jìn)程的頁表項(xiàng)的臟標(biāo)志,回寫之前(clear_page_dirty_for_io完成之前),頁表項(xiàng)的臟標(biāo)志是置位的,回寫的時(shí)候(clear_page_dirty_for_io的調(diào)用)會(huì)通過反向映射機(jī)制將所有映射這個(gè)頁的頁表項(xiàng)的臟標(biāo)志位清零并設(shè)置只讀權(quán)限,回寫之后(clear_page_dirty_for_io完成之后),再次的寫訪問會(huì)發(fā)生寫時(shí)復(fù)制缺頁異常,再次設(shè)置頁表項(xiàng)的臟標(biāo)志位,如此重復(fù),從而跟蹤了臟頁。
2)對(duì)于直接通過write接口訪問的文件頁,因?yàn)檫@個(gè)文件頁只會(huì)被讀取到page cache中,并沒有映射到任何進(jìn)程地址空間,進(jìn)程寫訪問是通過copy_from_user的方式,所以通過頁描述符記錄臟頁?;貙懼?clear_page_dirty_for_io完成之前),寫文件的時(shí)候通過文件系統(tǒng)的寫文件的調(diào)用鏈會(huì)設(shè)置頁描述符臟標(biāo)志位,回寫的時(shí)候(clear_page_dirty_for_io的調(diào)用)會(huì)清除頁描述符臟標(biāo)志位,回寫之后(clear_page_dirty_for_io完成之后),再次通過write接口寫訪問時(shí),再次通過文件系統(tǒng)的寫文件的調(diào)用鏈會(huì)再次設(shè)置頁描述符臟標(biāo)志位,如此重復(fù),從而跟蹤了臟頁。