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