故事的開始是這樣的,某天在脈脈上看到有人發(fā)了下面的帖子:
想不到 mmap 都成了黑科技了,,為了讓大家都能了解這個(gè)黑科技,,所以還是寫篇文章來(lái)詳細(xì)介紹一下 mmap 的實(shí)現(xiàn)吧,。
其實(shí),,源碼分析是比較難寫的,,主要有兩個(gè)原因:
一方面是源碼實(shí)現(xiàn)一般會(huì)涉及多個(gè)知識(shí)點(diǎn),,所以在分析源碼時(shí)需要穿插多個(gè)知識(shí)點(diǎn),,從而增加分析的難度,。另一方面是源碼實(shí)現(xiàn)會(huì)處理很多細(xì)節(jié)問(wèn)題,,這些細(xì)節(jié)問(wèn)題雖然不是設(shè)計(jì)的主要框架,但忽略了有時(shí)會(huì)讓人摸不著頭腦,。
所以,,為了降低分析的難度和讓讀者能夠更容易看懂,在分析源碼時(shí)更注重知識(shí)點(diǎn)的實(shí)現(xiàn),,而在不影響理解的情況下,,我會(huì)忽略一些細(xì)節(jié)問(wèn)題。而對(duì)于穿插其他知識(shí)點(diǎn)的時(shí)候,,會(huì)先跳過(guò)其實(shí)現(xiàn),,并且在后續(xù)的文章對(duì)其進(jìn)行分析。
mmap 原理
在之前的文章中,,我們也介紹過(guò) mmap 的原理,,比如這篇:《原來(lái) mmap 這么簡(jiǎn)單》。當(dāng)然這篇文章只是簡(jiǎn)單介紹了 mmap 的原理,,但是 mmap 的實(shí)現(xiàn)遠(yuǎn)不止那么簡(jiǎn)單,,這是因?yàn)?mmap 涉及多個(gè)子系統(tǒng),如:內(nèi)存管理,、文件系統(tǒng),、中斷處理等。
好消息是,這幾個(gè)子系統(tǒng)我們都有對(duì)應(yīng)的文章介紹過(guò):
內(nèi)存管理:《Linux虛擬內(nèi)存空間管理》
文件系統(tǒng):《 什么是頁(yè)緩存》
中斷處理:《Linux中斷處理》
在閱讀本文前,,最好復(fù)習(xí)一下上面的文章,。
雖然在《原來(lái) mmap 這么簡(jiǎn)單》一文中,我們簡(jiǎn)單介紹過(guò) mmap 的原理,。但為了方便分析源碼,,下面還是簡(jiǎn)單回顧一下 mmap 的原理吧。
mmap 的全稱是 memory map,,中文意思是 內(nèi)存映射,。其用途是將文件映射到內(nèi)存中,然后可以通過(guò)對(duì)映射區(qū)的內(nèi)存進(jìn)行讀寫操作,,其效果等同于對(duì)文件進(jìn)行讀寫操作,。
下面我們通過(guò)一幅圖來(lái)對(duì) mmap 的原理進(jìn)行闡述:
從上圖可以看出,mmap 的原理就是將虛擬內(nèi)存空間映射到文件的頁(yè)緩存,,在《什么是頁(yè)緩存》一文中可知,,對(duì)文件進(jìn)行讀寫時(shí)需要經(jīng)過(guò)頁(yè)緩存進(jìn)行中轉(zhuǎn)的。所以當(dāng)虛擬內(nèi)存地址映射到文件的頁(yè)緩存后,,就可以直接通過(guò)讀寫映射區(qū)內(nèi)存來(lái)對(duì)文件進(jìn)行讀寫操作,。
mmap 實(shí)現(xiàn)
在分析 mmap 的實(shí)現(xiàn)前,最好先了解其使用方式,,mmap 的使用可以參考《原來(lái) mmap 這么簡(jiǎn)單》這篇文章,。
1. 文件映射
當(dāng)我們使用 mmap() 系統(tǒng)調(diào)用對(duì)文件進(jìn)行映射時(shí),將會(huì)觸發(fā)調(diào)用 do_mmap_pgoff() 內(nèi)核函數(shù)來(lái)完成工作,,我們來(lái)看看 do_mmap_pgoff() 函數(shù)的實(shí)現(xiàn)(經(jīng)過(guò)精簡(jiǎn)后):
unsigned long
do_mmap_pgoff(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, unsigned long pgoff)
{
...
// 1. 獲取一個(gè)未被使用的虛擬內(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)過(guò)精簡(jiǎn)后的 do_mmap_pgoff() 函數(shù)主要完成 2 個(gè)工作:
首先,,調(diào)用 get_unmapped_area() 函數(shù)來(lái)獲取進(jìn)程沒(méi)被使用的虛擬內(nèi)存區(qū),并且返回此內(nèi)存區(qū)的首地址,。然后,,調(diào)用 mmap_region() 函數(shù)繼續(xù)進(jìn)行映射操作。
在 32 位的操作系統(tǒng)中,,每個(gè)進(jìn)程都有 4GB 的虛擬內(nèi)存空間,,應(yīng)用程序在使用內(nèi)存前,需要先向操作系統(tǒng)發(fā)起申請(qǐng)內(nèi)存的操作,。操作系統(tǒng)會(huì)從進(jìn)程的虛擬內(nèi)存空間中查找未被使用的內(nèi)存地址,,并且返回給應(yīng)用程序。
操作系統(tǒng)會(huì)記錄進(jìn)程正在使用中的虛擬內(nèi)存地址,,如果內(nèi)存地址沒(méi)被登記,,說(shuō)明此內(nèi)存地址是空閑的(未被使用)。
我們繼續(xù)來(lái)看看 mmap_region() 函數(shù)的實(shí)現(xiàn),,代碼如下(經(jīng)過(guò)精簡(jiǎn)后):
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. 申請(qǐng)一個(gè)虛擬內(nèi)存區(qū)管理結(jié)構(gòu)(vma)
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
...
// 2. 設(shè)置vma結(jié)構(gòu)各個(gè)字段的值
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)用文件對(duì)象的 mmap() 回調(diào)函數(shù)來(lái)設(shè)置vma結(jié)構(gòu)的 fault() 回調(diào)函數(shù),。
* vma對(duì)象的 fault() 回調(diào)函數(shù)的作用是:
* - 當(dāng)訪問(wèn)的虛擬內(nèi)存沒(méi)有映射到物理內(nèi)存時(shí),
* - 將會(huì)調(diào)用 fault() 回調(diào)函數(shù)對(duì)虛擬內(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 件事情:
申請(qǐng)一個(gè) vm_area_struct 結(jié)構(gòu)(vma),內(nèi)核使用 vma 來(lái)管理進(jìn)程的虛擬內(nèi)存地址,,關(guān)于 vma 的詳細(xì)介紹可以參考:《Linux虛擬內(nèi)存空間管理》,。設(shè)置 vma 結(jié)構(gòu)各個(gè)字段的值。通過(guò)調(diào)用文件對(duì)象的 mmap() 回調(diào)函數(shù)來(lái)設(shè)置vma結(jié)構(gòu)的 fault() 回調(diào)函數(shù),,一般文件對(duì)象的 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)來(lái)管理進(jìn)程的虛擬內(nèi)存地址,。當(dāng)進(jìn)程需要使用內(nèi)存時(shí),,首先要向操作系統(tǒng)進(jìn)行申請(qǐng),操作系統(tǒng)會(huì)使用 vm_area_struct 結(jié)構(gòu)來(lái)記錄被分配出去的內(nèi)存區(qū)的大小,、起始地址和權(quán)限等,。
我們來(lái)看看 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ū)鏈接起來(lái)
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; // 如果映射到文件,將指向映射的文件對(duì)象
...
};
struct vm_operations_struct {
// 當(dāng)虛擬內(nèi)存區(qū)沒(méi)有映射到物理內(nèi)存地址時(shí),,將會(huì)觸發(fā)缺頁(yè)異常,
// 而在缺頁(yè)異常處理函數(shù)中,,將會(huì)調(diào)用此回調(diào)函數(shù)來(lái)對(duì)虛擬內(nèi)存映射到物理內(nèi)存,。
int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
...
};
當(dāng)把文件映射到虛擬內(nèi)存空間時(shí),需要把 vma 結(jié)構(gòu)的 vm_file 字段設(shè)置為要映射的文件對(duì)象,,然后調(diào)用文件對(duì)象的 mmap() 回調(diào)函數(shù)來(lái)設(shè)置 vma 結(jié)構(gòu)的 fault() 回調(diào)函數(shù),。
vma 結(jié)構(gòu)的 fault() 回調(diào)函數(shù)的作用是:當(dāng)虛擬內(nèi)存區(qū)沒(méi)有映射到物理內(nèi)存地址時(shí),將會(huì)觸發(fā)缺頁(yè)異常,。而在缺頁(yè)異常處理中,,將會(huì)調(diào)用此回調(diào)函數(shù)來(lái)對(duì)虛擬內(nèi)存映射到物理內(nèi)存。
我們來(lá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;
}
至此,,文件映射的過(guò)程已經(jīng)分析完畢,。我們來(lái)看看其調(diào)用鏈:
sys_mmap()
└→ do_mmap_pgoff()
└→ mmap_region()
└→ generic_file_mmap()
2. 缺頁(yè)異常
前面介紹了 mmap() 系統(tǒng)調(diào)用的處理過(guò)程,可以發(fā)現(xiàn) mmap() 只是將 vma 的 vm_file 字段設(shè)置為被映射的文件對(duì)象,,并且將 vma 的 fault() 回調(diào)函數(shù)設(shè)置為 filemap_fault(),。也就是說(shuō),mmap() 系統(tǒng)調(diào)用并沒(méi)有對(duì)虛擬內(nèi)存進(jìn)行任何的映射操作,。
我們?cè)凇堵嫿庹f(shuō) “內(nèi)存映射”》一文中介紹過(guò),,虛擬內(nèi)存必須映射到物理內(nèi)存才能使用。如果訪問(wèn)沒(méi)有映射到物理內(nèi)存的虛擬內(nèi)存地址,,CPU 將會(huì)觸發(fā)缺頁(yè)異常,。也就是說(shuō),,虛擬內(nèi)存并不能直接映射到磁盤中的文件。
那么 mmap() 是怎么將文件映射到虛擬內(nèi)存中呢,?我們?cè)凇?什么是頁(yè)緩存》一文中介紹過(guò),,讀寫文件時(shí)并不是直接對(duì)磁盤上的文件進(jìn)行操作的,而是通過(guò) 頁(yè)緩存 作為中轉(zhuǎn)的,,而頁(yè)緩存就是物理內(nèi)存中的內(nèi)存頁(yè),。所以,mmap() 可以通過(guò)將文件的頁(yè)緩存映射到虛擬內(nèi)存空間來(lái)實(shí)現(xiàn)對(duì)文件的映射,。
但我們?cè)?mmap() 系統(tǒng)調(diào)用的實(shí)現(xiàn)中,,也沒(méi)看到將文件頁(yè)緩存映射到虛擬內(nèi)存空間。那么映射過(guò)程是在什么時(shí)候發(fā)生的呢,?
答案就是:缺頁(yè)異常,。
由于 mmap() 系統(tǒng)調(diào)用并沒(méi)有直接將文件的頁(yè)緩存映射到虛擬內(nèi)存中,所以當(dāng)訪問(wèn)到?jīng)]有映射的虛擬內(nèi)存地址時(shí),,將會(huì)觸發(fā) 缺頁(yè)異常,。當(dāng) CPU 觸發(fā)缺頁(yè)異常時(shí),將會(huì)調(diào)用 do_page_fault() 函數(shù)來(lái)修復(fù)觸發(fā)異常的虛擬內(nèi)存地址,。
我們主要來(lái)看看 do_page_fault() 函數(shù)對(duì)文件映射的實(shí)現(xiàn)部分,,其調(diào)用鏈如下:
do_page_fault()
└→ handle_mm_fault()
└→ handle_pte_fault()
└→ do_linear_fault()
└→ __do_fault()
所以我們直接來(lái)看看 __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)存頁(yè)
// 1. 如果虛擬內(nèi)存管理區(qū)提供了 falut() 回調(diào)函數(shù),那么將調(diào)用此函數(shù)來(lái)獲取要映射的物理內(nèi)存頁(yè),,
// 我們?cè)?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. 通過(guò)物理內(nèi)存頁(yè)生成一個(gè)頁(yè)表項(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)程的頁(yè)表項(xiàng)設(shè)置為剛生成的頁(yè)表項(xiàng)的值)
set_pte_at(mm, address, page_table, entry);
...
}
...
return ret;
}
__do_fault() 函數(shù)對(duì)處理文件映射部分主要分為 3 個(gè)步驟:
調(diào)用虛擬內(nèi)存管理區(qū)結(jié)構(gòu)(vma)的 fault() 回調(diào)函數(shù)(也就是 filemap_fault() 函數(shù))來(lái)獲取到文件的頁(yè)緩存,。通過(guò)頁(yè)緩存的物理內(nèi)存頁(yè)來(lái)生成一個(gè)頁(yè)表項(xiàng)值,,可以參考《漫畫解說(shuō) “內(nèi)存映射”》一文。將虛擬內(nèi)存地址映射到頁(yè)緩存的物理內(nèi)存頁(yè)(也就是將進(jìn)程的頁(yè)表項(xiàng)設(shè)置為上面生成的頁(yè)表項(xiàng)的值),。
對(duì)于 filemap_fault() 函數(shù)是怎樣讀取文件頁(yè)緩存的,,本文不作解釋,有興趣的可以自行閱讀源碼,。
最后,,我們以一幅圖來(lái)描述一下虛擬內(nèi)存是如何與文件進(jìn)行映射的:
從上圖可以看出,mmap() 是通過(guò)將虛擬內(nèi)存地址映射到文件的頁(yè)緩存來(lái)實(shí)現(xiàn)的,。當(dāng)對(duì)映射后的虛擬內(nèi)存進(jìn)行讀寫操作時(shí),,其效果等價(jià)于直接對(duì)文件的頁(yè)緩存進(jìn)行讀寫操作。對(duì)文件的頁(yè)緩存進(jìn)行讀寫操作,,也等價(jià)于對(duì)文件進(jìn)行讀寫操作,。
更多信息可以來(lái)這里獲取==>>電子技術(shù)應(yīng)用-AET<<