故事的開始是這樣的,某天在脈脈上看到有人發(fā)了下面的帖子:
mmap 原理
在之前的文章中,我們也介紹過 mmap 的原理,比如這篇:《原來 mmap 這么簡單》。當(dāng)然這篇文章只是簡單介紹了 mmap 的原理,但是 mmap 的實(shí)現(xiàn)遠(yuǎn)不止那么簡單,這是因?yàn)?mmap 涉及多個子系統(tǒng),如:內(nèi)存管理、文件系統(tǒng)、中斷處理等。
好消息是,這幾個子系統(tǒng)我們都有對應(yīng)的文章介紹過:
在閱讀本文前,最好復(fù)習(xí)一下上面的文章。
雖然在《原來 mmap 這么簡單》一文中,我們簡單介紹過 mmap 的原理。但為了方便分析源碼,下面還是簡單回顧一下 mmap 的原理吧。
mmap 的全稱是 memory map,中文意思是 內(nèi)存映射。其用途是將文件映射到內(nèi)存中,然后可以通過對映射區(qū)的內(nèi)存進(jìn)行讀寫操作,其效果等同于對文件進(jìn)行讀寫操作。
下面我們通過一幅圖來對 mmap 的原理進(jìn)行闡述:
從上圖可以看出,mmap 的原理就是將虛擬內(nèi)存空間映射到文件的頁緩存,在《什么是頁緩存》一文中可知,對文件進(jìn)行讀寫時需要經(jīng)過頁緩存進(jìn)行中轉(zhuǎn)的。所以當(dāng)虛擬內(nèi)存地址映射到文件的頁緩存后,就可以直接通過讀寫映射區(qū)內(nèi)存來對文件進(jìn)行讀寫操作。
mmap 實(shí)現(xiàn)
在分析 mmap 的實(shí)現(xiàn)前,最好先了解其使用方式,mmap 的使用可以參考《原來 mmap 這么簡單》這篇文章。
1. 文件映射
當(dāng)我們使用 mmap() 系統(tǒng)調(diào)用對文件進(jìn)行映射時,將會觸發(fā)調(diào)用 do_mmap_pgoff() 內(nèi)核函數(shù)來完成工作,我們來看看 do_mmap_pgoff() 函數(shù)的實(shí)現(xiàn)(經(jīng)過精簡后):
unsigned?long do_mmap_pgoff(struct?file?*file,?unsigned?long?addr,? ??????????????unsigned?long?len,?unsigned?long?prot,? ??????????????unsigned?long?flags,?unsigned?long?pgoff) { ????... ????//?1.?獲取一個未被使用的虛擬內(nèi)存區(qū) ????addr?=?get_unmapped_area(file,?addr,?len,?pgoff,?flags); ????if?(addr?&?~PAGE_MASK) ????????return?addr; ????... ????//?2.?調(diào)用?mmap_region()?函數(shù)繼續(xù)進(jìn)行映射操作 ????return?mmap_region(file,?addr,?len,?flags,?vm_flags,?pgoff,?accountable); }
經(jīng)過精簡后的 do_mmap_pgoff() 函數(shù)主要完成 2 個工作:
- 首先,調(diào)用 get_unmapped_area() 函數(shù)來獲取進(jìn)程沒被使用的虛擬內(nèi)存區(qū),并且返回此內(nèi)存區(qū)的首地址。
- 然后,調(diào)用 mmap_region() 函數(shù)繼續(xù)進(jìn)行映射操作。
?
在 32 位的操作系統(tǒng)中,每個進(jìn)程都有 4GB 的虛擬內(nèi)存空間,應(yīng)用程序在使用內(nèi)存前,需要先向操作系統(tǒng)發(fā)起申請內(nèi)存的操作。操作系統(tǒng)會從進(jìn)程的虛擬內(nèi)存空間中查找未被使用的內(nèi)存地址,并且返回給應(yīng)用程序。
操作系統(tǒng)會記錄進(jìn)程正在使用中的虛擬內(nèi)存地址,如果內(nèi)存地址沒被登記,說明此內(nèi)存地址是空閑的(未被使用)。
我們繼續(xù)來看看 mmap_region() 函數(shù)的實(shí)現(xiàn),代碼如下(經(jīng)過精簡后):
unsigned?long mmap_region(struct?file?*file,?unsigned?long?addr, ????????????unsigned?long?len,?unsigned?long?flags, ????????????unsigned?int?vm_flags,?unsigned?long?pgoff, ????????????int?accountable) { ????struct?mm_struct?*mm?=?current->mm; ????struct?vm_area_struct?*vma,?*prev; ????int?correct_wcount?=?0; ????int?error; ????... ????//?1.?申請一個虛擬內(nèi)存區(qū)管理結(jié)構(gòu)(vma) ????vma?=?kmem_cache_zalloc(vm_area_cachep,?GFP_KERNEL); ????... ????//?2.?設(shè)置vma結(jié)構(gòu)各個字段的值 ????vma->vm_mm?=?mm; ????vma->vm_start?=?addr; ????vma->vm_end?=?addr?+?len; ????vma->vm_flags?=?vm_flags; ????vma->vm_page_prot?=?protection_map[vm_flags?&?(VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)]; ????vma->vm_pgoff?=?pgoff; ????if?(file)?{ ????????... ????????vma->vm_file?=?file; ????????/*?3.?此處是內(nèi)存映射的關(guān)鍵點(diǎn),調(diào)用文件對象的?mmap()?回調(diào)函數(shù)來設(shè)置vma結(jié)構(gòu)的?fault()?回調(diào)函數(shù)。 ?????????*????vma對象的?fault()?回調(diào)函數(shù)的作用是: ?????????*????????-?當(dāng)訪問的虛擬內(nèi)存沒有映射到物理內(nèi)存時, ?????????*????????-?將會調(diào)用?fault()?回調(diào)函數(shù)對虛擬內(nèi)存地址映射到物理內(nèi)存地址。 ?????????*/ ????????error?=?file->f_op->mmap(file,?vma); ????????... ????} ????... ????//?4.?把?vma?結(jié)構(gòu)連接到進(jìn)程虛擬內(nèi)存區(qū)的鏈表和紅黑樹中。 ????vma_link(mm,?vma,?prev,?rb_link,?rb_parent); ????... ????return?addr; }
mmap_region() 函數(shù)主要完成以下 4 件事情:
- 申請一個 vm_area_struct 結(jié)構(gòu)(vma),內(nèi)核使用 vma 來管理進(jìn)程的虛擬內(nèi)存地址,關(guān)于 vma 的詳細(xì)介紹可以參考:《Linux虛擬內(nèi)存空間管理》。
- 設(shè)置 vma 結(jié)構(gòu)各個字段的值。
- 通過調(diào)用文件對象的 mmap() 回調(diào)函數(shù)來設(shè)置vma結(jié)構(gòu)的 fault() 回調(diào)函數(shù),一般文件對象的 mmap() 回調(diào)函數(shù)為:generic_file_mmap()。
- 把新創(chuàng)建的 vma 結(jié)構(gòu)連接到進(jìn)程的虛擬內(nèi)存區(qū)鏈表和紅黑樹中。
內(nèi)核使用 vm_area_struct 結(jié)構(gòu)來管理進(jìn)程的虛擬內(nèi)存地址。當(dāng)進(jìn)程需要使用內(nèi)存時,首先要向操作系統(tǒng)進(jìn)行申請,操作系統(tǒng)會使用 vm_area_struct 結(jié)構(gòu)來記錄被分配出去的內(nèi)存區(qū)的大小、起始地址和權(quán)限等。
我們來看看 vm_area_struct 結(jié)構(gòu)的定義:
struct?vm_area_struct?{ ????struct?mm_struct?*vm_mm; ????unsigned?long?vm_start;??????????????//?內(nèi)存區(qū)的開始地址 ????unsigned?long?vm_end;????????????????//?內(nèi)存區(qū)的結(jié)束地址 ????struct?vm_area_struct?*vm_next;??????//?把進(jìn)程所有已分配的內(nèi)存區(qū)鏈接起來 ????pgprot_t?vm_page_prot;???????????????//?內(nèi)存區(qū)的權(quán)限 ????... ????struct?rb_node?vm_rb;????????????????//?為了加快查找內(nèi)存區(qū)而建立的紅黑樹 ????... ????struct?vm_operations_struct?*vm_ops;?//?內(nèi)存區(qū)的操作回調(diào)函數(shù)集 ????unsigned?long?vm_pgoff; ????struct?file?*vm_file;????????????????//?如果映射到文件,將指向映射的文件對象 ????... }; struct?vm_operations_struct?{ ????//?當(dāng)虛擬內(nèi)存區(qū)沒有映射到物理內(nèi)存地址時,將會觸發(fā)缺頁異常, ????//?而在缺頁異常處理函數(shù)中,將會調(diào)用此回調(diào)函數(shù)來對虛擬內(nèi)存映射到物理內(nèi)存。 ????int?(*fault)(struct?vm_area_struct?*vma,?struct?vm_fault?*vmf); ????... };
當(dāng)把文件映射到虛擬內(nèi)存空間時,需要把 vma 結(jié)構(gòu)的 vm_file 字段設(shè)置為要映射的文件對象,然后調(diào)用文件對象的 mmap() 回調(diào)函數(shù)來設(shè)置 vma 結(jié)構(gòu)的 fault() 回調(diào)函數(shù)。
?
vma 結(jié)構(gòu)的 fault() 回調(diào)函數(shù)的作用是:當(dāng)虛擬內(nèi)存區(qū)沒有映射到物理內(nèi)存地址時,將會觸發(fā)缺頁異常。而在缺頁異常處理中,將會調(diào)用此回調(diào)函數(shù)來對虛擬內(nèi)存映射到物理內(nèi)存。
我們來看看 generic_file_mmap() 函數(shù)是怎么設(shè)置 vma 結(jié)構(gòu)的 fault() 回調(diào)函數(shù)的:
struct?vm_operations_struct?generic_file_vm_ops?=?{ ????.fault?=?filemap_fault,?//?將?fault()?回調(diào)函數(shù)設(shè)置為:filemap_fault() }; int?generic_file_mmap(struct?file?*file,?struct?vm_area_struct?*vma) { ????... ????vma->vm_ops?=?&generic_file_vm_ops; ????... ????return?0; }
至此,文件映射的過程已經(jīng)分析完畢。我們來看看其調(diào)用鏈:
sys_mmap() └→?do_mmap_pgoff() ???└→?mmap_region() ??????└→?generic_file_mmap()
2. 缺頁異常
前面介紹了 mmap() 系統(tǒng)調(diào)用的處理過程,可以發(fā)現(xiàn) mmap() 只是將 vma 的 vm_file 字段設(shè)置為被映射的文件對象,并且將 vma 的 fault() 回調(diào)函數(shù)設(shè)置為 filemap_fault()。也就是說,mmap() 系統(tǒng)調(diào)用并沒有對虛擬內(nèi)存進(jìn)行任何的映射操作。
我們在《漫畫解說 “內(nèi)存映射”》一文中介紹過,虛擬內(nèi)存必須映射到物理內(nèi)存才能使用。如果訪問沒有映射到物理內(nèi)存的虛擬內(nèi)存地址,CPU 將會觸發(fā)缺頁異常。也就是說,虛擬內(nèi)存并不能直接映射到磁盤中的文件。
那么 mmap() 是怎么將文件映射到虛擬內(nèi)存中呢?我們在《 什么是頁緩存》一文中介紹過,讀寫文件時并不是直接對磁盤上的文件進(jìn)行操作的,而是通過 頁緩存 作為中轉(zhuǎn)的,而頁緩存就是物理內(nèi)存中的內(nèi)存頁。所以,mmap() 可以通過將文件的頁緩存映射到虛擬內(nèi)存空間來實(shí)現(xiàn)對文件的映射。
但我們在 mmap() 系統(tǒng)調(diào)用的實(shí)現(xiàn)中,也沒看到將文件頁緩存映射到虛擬內(nèi)存空間。那么映射過程是在什么時候發(fā)生的呢?
?
答案就是:缺頁異常。
由于 mmap() 系統(tǒng)調(diào)用并沒有直接將文件的頁緩存映射到虛擬內(nèi)存中,所以當(dāng)訪問到?jīng)]有映射的虛擬內(nèi)存地址時,將會觸發(fā) 缺頁異常。當(dāng) CPU 觸發(fā)缺頁異常時,將會調(diào)用 do_page_fault() 函數(shù)來修復(fù)觸發(fā)異常的虛擬內(nèi)存地址。
我們主要來看看 do_page_fault() 函數(shù)對文件映射的實(shí)現(xiàn)部分,其調(diào)用鏈如下:
do_page_fault() └→?handle_mm_fault() ???└→?handle_pte_fault() ??????└→?do_linear_fault() ?????????└→?__do_fault()
所以我們直接來看看 __do_fault() 函數(shù)的實(shí)現(xiàn):
static?int __do_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) { ????... ????vmf.virtual_address?=?address?&?PAGE_MASK;?//?要映射的虛擬內(nèi)存地址 ????vmf.pgoff?=?pgoff;?????????????????????????//?映射到文件的偏移量 ????vmf.flags?=?flags;?????????????????????????//?標(biāo)志位 ????vmf.page?=?NULL;???????????????????????????//?映射到虛擬內(nèi)存中的物理內(nèi)存頁 ????//?1.?如果虛擬內(nèi)存管理區(qū)提供了?falut()?回調(diào)函數(shù),那么將調(diào)用此函數(shù)來獲取要映射的物理內(nèi)存頁, ????//????我們在?mmap()?系統(tǒng)調(diào)用的實(shí)現(xiàn)中看到,已經(jīng)將其設(shè)置為?filemap_fault()?函數(shù)了。 ????if?(likely(vma->vm_ops->fault))?{ ????????ret?=?vma->vm_ops->fault(vma,?&vmf); ????????... ????} ????... ????if?(likely(pte_same(*page_table,?orig_pte)))?{ ????????... ????????//?2.?通過物理內(nèi)存頁生成一個頁表項(xiàng)值(可以參考內(nèi)存映射一文) ????????entry?=?mk_pte(page,?vma->vm_page_prot); ????????if?(flags?&?FAULT_FLAG_WRITE) ????????????entry?=?maybe_mkwrite(pte_mkdirty(entry),?vma); ????????//?3.?將虛擬內(nèi)存地址映射到物理內(nèi)存(也就是將進(jìn)程的頁表項(xiàng)設(shè)置為剛生成的頁表項(xiàng)的值) ????????set_pte_at(mm,?address,?page_table,?entry); ????????... ????} ????... ????return?ret; }
__do_fault() 函數(shù)對處理文件映射部分主要分為 3 個步驟:
- 調(diào)用虛擬內(nèi)存管理區(qū)結(jié)構(gòu)(vma)的 fault() 回調(diào)函數(shù)(也就是 filemap_fault() 函數(shù))來獲取到文件的頁緩存。
- 通過頁緩存的物理內(nèi)存頁來生成一個頁表項(xiàng)值,可以參考《漫畫解說 “內(nèi)存映射”》一文。
- 將虛擬內(nèi)存地址映射到頁緩存的物理內(nèi)存頁(也就是將進(jìn)程的頁表項(xiàng)設(shè)置為上面生成的頁表項(xiàng)的值)。
對于 filemap_fault() 函數(shù)是怎樣讀取文件頁緩存的,本文不作解釋,有興趣的可以自行閱讀源碼。
最后,我們以一幅圖來描述一下虛擬內(nèi)存是如何與文件進(jìn)行映射的:
從上圖可以看出,mmap() 是通過將虛擬內(nèi)存地址映射到文件的頁緩存來實(shí)現(xiàn)的。當(dāng)對映射后的虛擬內(nèi)存進(jìn)行讀寫操作時,其效果等價于直接對文件的頁緩存進(jìn)行讀寫操作。對文件的頁緩存進(jìn)行讀寫操作,也等價于對文件進(jìn)行讀寫操作。