完全分析Linux mmap原理(看完悟了)
內(nèi)存映射是一個很有趣的思想,我們都知道操作系統(tǒng)分為用戶態(tài)和內(nèi)核態(tài),用戶態(tài)是不能直接和物理設(shè)備打交道,如果我們用戶空間想訪問硬盤的一塊區(qū)域數(shù)據(jù),則需要兩次拷貝(硬盤->內(nèi)核->用戶),但是內(nèi)存映射的設(shè)計只需要發(fā)生一次拷貝,大大提高了讀取數(shù)據(jù)的效率。那么內(nèi)存映射的原理和內(nèi)核是如何實現(xiàn)的呢?
1. 內(nèi)存映射概念
內(nèi)存映射,簡而言之就是將用戶空間的一段內(nèi)存區(qū)域映射到內(nèi)核空間,映射成功后,用戶對這段內(nèi)存區(qū)域的修改可以直接反映到內(nèi)核空間,同樣,內(nèi)核空間對這段區(qū)域的修改也直接反映給用戶空間,對于用戶空間和內(nèi)核空間兩者之間需要進(jìn)行大量數(shù)據(jù)傳輸?shù)炔僮鞯脑捫适欠浅8叩?。如下圖所示

實現(xiàn)這樣的映射后,進(jìn)程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會自動回寫臟頁到對應(yīng)的文件磁盤上,就可以完成對于文件的操作,而不需要再調(diào)用read/write等系統(tǒng)調(diào)用函數(shù)。同時,內(nèi)核空間對于這段區(qū)域的修改也可以直接反映到用戶空間,從而可以實現(xiàn)不同進(jìn)程間的文件共享。
mmap/munmap 接口是常用的內(nèi)存映射的系統(tǒng)調(diào)用接口,無論是在用戶空間分配內(nèi)存、讀寫大文件、連接動態(tài)庫文件,還是多進(jìn)程間共享內(nèi)存,都可以看到其身影,其聲明如下:
#include <sys/mman.h> void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length);
條件:
mmap()必須以PAGE_SIZE為單位進(jìn)行映射,而內(nèi)存也只能以頁為單位進(jìn)行映射,若要映射非PAGE_SIZE整數(shù)倍的地址范圍,要先進(jìn)行內(nèi)存對齊,強行以PAGE_SIZE的倍數(shù)大小進(jìn)行映射。
參數(shù)說明:
start:映射區(qū)的開始地址,設(shè)置為0時表示由系統(tǒng)決定映射區(qū)的起始地址。
length:映射區(qū)的長度。//長度單位是 以字節(jié)為單位,不足一內(nèi)存頁按一內(nèi)存頁處理
prot:期望的內(nèi)存保護(hù)標(biāo)志,不能與文件的打開模式?jīng)_突。是以下的某個值,可以通過or運算合理地組合在一起
PROT_EXEC: 表示映射的頁面是可以執(zhí)行的
PROT_READ:表示映射的頁面是可以讀取的
PROT_WRITE :表示映射的頁面是可以寫入的
PROT_NONE :表示映射的頁面是不可訪問的
4. flags:指定映射對象的類型,映射選項和映射頁是否可以共享。它的值可以是一個或者多個以下位的組合體
MAP_SHARED:創(chuàng)建一個共享映射的區(qū)域,多個進(jìn)程可以通過共享映射的方式來映射一個文件,這樣其他進(jìn)程也可以看到映射內(nèi)容的改變,修改后的內(nèi)容會同步到磁盤文件
MAP_PRIVATE:創(chuàng)建一個私有的寫時復(fù)制的映射,多個進(jìn)程可以通過私有映射方式來映射一個文件,其他的進(jìn)程不會看到映射文件內(nèi)容的改變,修改后也不會同步到磁盤中
MAP_ANONYMOUS:創(chuàng)建一個匿名映射,即沒有關(guān)聯(lián)到文件的映射
MAP_FIXED:
MAP_POPULATE:提前遇到文件內(nèi)容到映射區(qū)
5. fd:mmap映射釋放和文件相關(guān)聯(lián),可以分為匿名映射和文件映射
文件映射:將一個普通文件的全部或者一部分映射到進(jìn)程的虛擬內(nèi)存中。映射后,進(jìn)程就可以直接在對應(yīng)的內(nèi)存區(qū)域操作文件內(nèi)容!
匿名映射:匿名映射沒有對應(yīng)的文件或者對應(yīng)的文件時虛擬文件(如:/dev/zero),映射后會把內(nèi)存分頁全部初始化為0。
6. offset:被映射對象內(nèi)容的起點
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!??!前100名進(jìn)群領(lǐng)取,額外贈送一份價值699的內(nèi)核資料包(含視頻教程、電子書、實戰(zhàn)項目及代碼)?


成功執(zhí)行時,mmap()返回被映射區(qū)的指針,munmap()返回0。失敗時,mmap()返回MAP_FAILED[其值為(void *)-1],munmap返回-1。
根據(jù)文件關(guān)聯(lián)性和映射區(qū)域示范共享等屬性,其分為

2. 源碼分析
查看 mmap 的系統(tǒng)調(diào)用的代碼實現(xiàn),其流程為 sys_mmp_pg_off(),最終會調(diào)用達(dá)到 do_mmap_pgoff,該函數(shù)使一個體系結(jié)構(gòu)無關(guān)的代碼,定義在 mm/mmap.c 中,
首先我們來看看 do_mmap(),是整個 mmap() 的具體操作函數(shù)
unsigned long do_mmap(struct file *file, unsigned long addr,
? unsigned long len, unsigned long prot,
? unsigned long flags, vm_flags_t vm_flags,
? unsigned long pgoff, unsigned long *populate)
{
struct mm_struct *mm = current->mm; ? ? ? ?//獲取該進(jìn)程的memory descriptor
int pkey = 0;
*populate = 0;
//函數(shù)對傳入的參數(shù)進(jìn)行一系列檢查, 假如任一參數(shù)出錯,都會返回一個errno
if (!len)
?return -EINVAL;
/*
?* Does the application expect PROT_READ to imply PROT_EXEC?
?*
?* (the exception is when the underlying filesystem is noexec
?* ?mounted, in which case we dont add PROT_EXEC.)
?*/
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
?if (!(file && path_noexec(&file->f_path)))
? prot |= PROT_EXEC;
//假如沒有設(shè)置MAP_FIXED標(biāo)志,且addr小于mmap_min_addr, 因為可以修改addr, 所以就需要將addr設(shè)為mmap_min_addr的頁對齊后的地址
if (!(flags & MAP_FIXED))
?addr = round_hint_to_min(addr);
/* Careful about overflows.. */
len = PAGE_ALIGN(len); ? ? ? ? //進(jìn)行Page大小的對齊
if (!len)
?return -ENOMEM;
/* offset overflow? */
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
?return -EOVERFLOW;
/* Too many mappings? */
if (mm->map_count > sysctl_max_map_count) ? ?//判斷該進(jìn)程的地址空間的虛擬區(qū)間數(shù)量是否超過了限制
?return -ENOMEM;
//get_unmapped_area從當(dāng)前進(jìn)程的用戶空間獲取一個未被映射區(qū)間的起始地址
addr = get_unmapped_area(file, addr, len, pgoff, flags);
if (offset_in_page(addr)) ? ? ? ?//檢查addr是否有效
?return addr;
if (prot == PROT_EXEC) {
?pkey = execute_only_pkey(mm);
?if (pkey < 0)
? pkey = 0;
}
/* Do simple checking here so the lower-level routines won't have
?* to. we assume access permissions have been handled by the open
?* of the memory object, so we don't do any here.
?*/
vm_flags |= calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) |
? mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
//假如flags設(shè)置MAP_LOCKED,即類似于mlock()將申請的地址空間鎖定在內(nèi)存中, 檢查是否可以進(jìn)行l(wèi)ock
if (flags & MAP_LOCKED)
?if (!can_do_mlock())
? return -EPERM;
if (mlock_future_check(mm, vm_flags, len))
?return -EAGAIN;
if (file) { ? ? ? ? ?// file指針不為nullptr, 即從文件到虛擬空間的映射
?struct inode *inode = file_inode(file); ? //獲取文件的inode
?switch (flags & MAP_TYPE) { ? ? ? //根據(jù)標(biāo)志指定的map種類,把為文件設(shè)置的訪問權(quán)考慮進(jìn)去
?case MAP_SHARED:
? if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))
? ?return -EACCES;
? /*
? ?* Make sure we don't allow writing to an append-only
? ?* file..
? ?*/
? if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
? ?return -EACCES;
? /*
? ?* Make sure there are no mandatory locks on the file.
? ?*/
? if (locks_verify_locked(file))
? ?return -EAGAIN;
? vm_flags |= VM_SHARED | VM_MAYSHARE;
? if (!(file->f_mode & FMODE_WRITE))
? ?vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
? /* fall through */
?case MAP_PRIVATE:
? if (!(file->f_mode & FMODE_READ))
? ?return -EACCES;
? if (path_noexec(&file->f_path)) {
? ?if (vm_flags & VM_EXEC)
? ? return -EPERM;
? ?vm_flags &= ~VM_MAYEXEC;
? }
? if (!file->f_op->mmap)
? ?return -ENODEV;
? if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
? ?return -EINVAL;
? break;
?default:
? return -EINVAL;
?}
} else {
?switch (flags & MAP_TYPE) {
?case MAP_SHARED:
? if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
? ?return -EINVAL;
? /*
? ?* Ignore pgoff.
? ?*/
? pgoff = 0;
? vm_flags |= VM_SHARED | VM_MAYSHARE;
? break;
?case MAP_PRIVATE:
? /*
? ?* Set pgoff according to addr for anon_vma.
? ?*/
? pgoff = addr >> PAGE_SHIFT;
? break;
?default:
? return -EINVAL;
?}
}
/*
?* Set 'VM_NORESERVE' if we should not account for the
?* memory use of this mapping.
?*/
if (flags & MAP_NORESERVE) {
?/* We honor MAP_NORESERVE if allowed to overcommit */
?if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
? vm_flags |= VM_NORESERVE;
?/* hugetlb applies strict overcommit unless MAP_NORESERVE */
?if (file && is_file_hugepages(file))
? vm_flags |= VM_NORESERVE;
}
//一頓檢查和配置,調(diào)用核心的代碼mmap_region
addr = mmap_region(file, addr, len, vm_flags, pgoff);
if (!IS_ERR_VALUE(addr) &&
? ? ((vm_flags & VM_LOCKED) ||
? ? ?(flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
?*populate = len;
return addr;
}
do_mmap() 根據(jù)用戶傳入的參數(shù)做了一系列的檢查,然后根據(jù)參數(shù)初始化 vm_area_struct 的標(biāo)志 vm_flags,vma->vm_file = get_file(file) 建立文件與vma的映射, mmap_region() 負(fù)責(zé)創(chuàng)建虛擬內(nèi)存區(qū)域:
unsigned long mmap_region(struct file *file, unsigned long addr,
?unsigned long len, vm_flags_t vm_flags, unsigned long pgoff)
{
struct mm_struct *mm = current->mm; ? ? //獲取該進(jìn)程的memory descriptor
struct vm_area_struct *vma, *prev;
int error;
struct rb_node **rb_link, *rb_parent;
unsigned long charged = 0;
/* 檢查申請的虛擬內(nèi)存空間是否超過了限制 */
if (!may_expand_vm(mm, vm_flags, len >> PAGE_SHIFT)) {
?unsigned long nr_pages;
?/*
? * MAP_FIXED may remove pages of mappings that intersects with
? * requested mapping. Account for the pages it would unmap.
? */
?nr_pages = count_vma_pages_range(mm, addr, addr + len);
?if (!may_expand_vm(mm, vm_flags,
? ? (len >> PAGE_SHIFT) - nr_pages))
? return -ENOMEM;
}
/* 檢查[addr, addr+len)的區(qū)間是否存在映射空間,假如存在重合的映射空間需要munmap ?*/
while (find_vma_links(mm, addr, addr + len, &prev, &rb_link,
? ? ? ? &rb_parent)) {
?if (do_munmap(mm, addr, len))
? return -ENOMEM;
}
/*
?* Private writable mapping: check memory availability
?*/
if (accountable_mapping(file, vm_flags)) {
?charged = len >> PAGE_SHIFT;
?if (security_vm_enough_memory_mm(mm, charged))
? return -ENOMEM;
?vm_flags |= VM_ACCOUNT;
}
//檢查是否可以合并[addr, addr+len)區(qū)間內(nèi)的虛擬地址空間vma
vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
? NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
if (vma) ? ? ? //假如合并成功,即使用合并后的vma, 并跳轉(zhuǎn)至out
?goto out;
//如果不能和已有的虛擬內(nèi)存區(qū)域合并,通過Memory Descriptor來申請一個vma
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
?error = -ENOMEM;
?goto unacct_error;
}
//初始化vma
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
INIT_LIST_HEAD(&vma->anon_vma_chain);
if (file) { //假如指定了文件映射
?if (vm_flags & VM_DENYWRITE) { ? //映射的文件不允許寫入,調(diào)用deny_write_accsess(file)排斥常規(guī)的文件操作
? error = deny_write_access(file);
? if (error)
? ?goto free_vma;
?}
?if (vm_flags & VM_SHARED) { ? //映射的文件允許其他進(jìn)程可見, 標(biāo)記文件為可寫
? error = mapping_map_writable(file->f_mapping);
? if (error)
? ?goto allow_write_and_free_vma;
?}
?//遞增File的引用次數(shù),返回File賦給vma
?vma->vm_file = get_file(file);
?error = file->f_op->mmap(file, vma); ?//調(diào)用文件系統(tǒng)指定的mmap函數(shù)
?if (error)
? goto unmap_and_free_vma;
?/* Can addr have changed??
? *
? * Answer: Yes, several device drivers can do it in their
? * ? ? ? ? f_op->mmap method. -DaveM
? * Bug: If addr is changed, prev, rb_link, rb_parent should
? * ? ? ?be updated for vma_link()
? */
?WARN_ON_ONCE(addr != vma->vm_start);
?addr = vma->vm_start;
?vm_flags = vma->vm_flags;
} else if (vm_flags & VM_SHARED) {
?error = shmem_zero_setup(vma); ?//假如標(biāo)志為VM_SHARED,但沒有指定映射文件,需要調(diào)用shmem_zero_setup(),實際映射的文件是dev/zero
?if (error)
? goto free_vma;
}
//將申請的新vma加入mm中的vma鏈表
vma_link(mm, vma, prev, rb_link, rb_parent);
/* Once vma denies write, undo our temporary denial count */
if (file) {
?if (vm_flags & VM_SHARED)
? mapping_unmap_writable(file->f_mapping);
?if (vm_flags & VM_DENYWRITE)
? allow_write_access(file);
}
file = vma->vm_file;
out:
perf_event_mmap(vma);
//更新進(jìn)程的虛擬地址空間mm
vm_stat_account(mm, vm_flags, len >> PAGE_SHIFT);
if (vm_flags & VM_LOCKED) {
?if (!((vm_flags & VM_SPECIAL) || is_vm_hugetlb_page(vma) ||
? ? vma == get_gate_vma(current->mm)))
? mm->locked_vm += (len >> PAGE_SHIFT);
?else
? vma->vm_flags &= VM_LOCKED_CLEAR_MASK;
}
if (file)
?uprobe_mmap(vma);
/*
?* New (or expanded) vma always get soft dirty status.
?* Otherwise user-space soft-dirty page tracker won't
?* be able to distinguish situation when vma area unmapped,
?* then new mapped in-place (which must be aimed as
?* a completely new data area).
?*/
vma->vm_flags |= VM_SOFTDIRTY;
vma_set_page_prot(vma);
return addr;
unmap_and_free_vma:
vma->vm_file = NULL;
fput(file);
/* Undo any partial mapping done by a device driver. */
unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
charged = 0;
if (vm_flags & VM_SHARED)
?mapping_unmap_writable(file->f_mapping);
allow_write_and_free_vma:
if (vm_flags & VM_DENYWRITE)
?allow_write_access(file);
free_vma:
kmem_cache_free(vm_area_cachep, vma);
unacct_error:
if (charged)
?vm_unacct_memory(charged);
return error;
}
mmap_region() 調(diào)用了 call_mmap(file, vma),call_mmap 根據(jù)文件系統(tǒng)的類型選擇適配的 mmap() 函數(shù),我們選擇目前常用的ext4,ext4_file_mmap() 是ext4對應(yīng)的mmap, 功能非常簡單,更新了file的修改時間(file_accessed(flie)),將對應(yīng)的operation賦給 vma->vm_flags,后面的文件系統(tǒng)章節(jié)在學(xué)習(xí)這塊。
通過分析mmap的源碼我們發(fā)現(xiàn)在調(diào)用 mmap() 的時候僅僅申請一個 vm_area_struct 來建立文件與虛擬內(nèi)存的映射,并沒有建立虛擬內(nèi)存與物理內(nèi)存的映射。假如沒有設(shè)置 MAP_POPULATE 標(biāo)志位,Linux并不在調(diào)用 mmap() 時就為進(jìn)程分配物理內(nèi)存空間,直到下次真正訪問地址空間時發(fā)現(xiàn)數(shù)據(jù)不存在于物理內(nèi)存空間時,觸發(fā) Page Fault 即缺頁中斷,Linux才會將缺失的Page換入內(nèi)存空間。其代碼流程圖如下所示

3. 應(yīng)用場景
對于傳統(tǒng)的linux系統(tǒng)文件操作是如何的呢?首選我們來看看工作流是如何的,其流程如下圖所示

其特點為
使用頁緩存機制,提高讀寫效率和保護(hù)磁盤
讀文件時,先將文件從磁盤拷貝到緩存,由于頁緩存區(qū)是在內(nèi)核空間,不能被用戶空間直接訪問,所以需要將頁緩存區(qū)數(shù)據(jù)再次拷貝到用戶空間,有2次文件拷貝工作
下面來看看使用內(nèi)存映射文件讀/寫的流程,其流程圖如下圖所示

其特點為:
用戶空間與內(nèi)核空間的交互式通過映射的區(qū)域直接交互,用內(nèi)存的讀取代替I/O讀寫,文件讀寫效率高
數(shù)據(jù)拷貝次數(shù)少,對文件的讀取操作跨過頁緩存,減少了數(shù)據(jù)拷貝一次,效率提高
可實現(xiàn)高效的大規(guī)模數(shù)據(jù)傳輸
在Linux系統(tǒng)中,根據(jù)內(nèi)存映射的本質(zhì)和特點,其應(yīng)用場景在于
實現(xiàn)內(nèi)存共享,如跨進(jìn)程通信
提高數(shù)據(jù)讀/寫效率:如讀寫操作
對于進(jìn)程間的通信,其工作流程如下圖所示

創(chuàng)建一塊共享的接收區(qū),實現(xiàn)地址映射關(guān)系
發(fā)送進(jìn)程數(shù)據(jù)到自身的虛擬內(nèi)存區(qū)域,數(shù)據(jù)拷貝1次
由于發(fā)送進(jìn)程的虛擬地址空間與接收進(jìn)程的虛擬內(nèi)存地址存在映射關(guān)系,所以發(fā)送到的數(shù)據(jù)也存放到接收進(jìn)程的虛擬內(nèi)存中,即實現(xiàn)了跨進(jìn)程間通信
4. 總結(jié)
內(nèi)存映射的讀寫操作主要的過程如下:
創(chuàng)建虛擬映射區(qū)域,其在當(dāng)前進(jìn)程的虛擬地址空間中,尋找一段滿足大小要求的虛擬地址,并且為此虛擬地址分配一個虛擬內(nèi)存區(qū)域(vm_area_struct結(jié)構(gòu)),初始化該虛擬內(nèi)存區(qū)域,插入到進(jìn)程虛擬地址區(qū)域的鏈表和紅黑樹中
實現(xiàn)地址映射關(guān)系,建立頁表,該過程在mmap函數(shù)中并未實現(xiàn),此時只是創(chuàng)建了映射關(guān)系,并不將任何文件數(shù)據(jù)拷貝至主存中,真正的數(shù)據(jù)拷貝是通過進(jìn)程發(fā)起讀寫操作時
進(jìn)程訪問該映射空間,實現(xiàn)文件內(nèi)容到物理內(nèi)存的數(shù)據(jù)拷貝,當(dāng)進(jìn)程讀寫訪問該映射地址時,如果進(jìn)程寫操作改變了內(nèi)容,并不會立即更新,而是一定時間后系統(tǒng)會自動會寫臟數(shù)據(jù)到對應(yīng)硬盤的地址空間
使用mmap來創(chuàng)建文件映射,由于只建立了進(jìn)程地址空間VMA,并沒有馬上分配page cache和建立映射關(guān)系。那么就會導(dǎo)致一個問題,當(dāng)創(chuàng)建一個很大的VMA,會頻繁發(fā)生缺頁中斷。
內(nèi)存映射機制mmap是POSIX標(biāo)準(zhǔn)的系統(tǒng)調(diào)用,有匿名映射和文件映射兩種。
匿名映射使用進(jìn)程的虛擬內(nèi)存空間,它和malloc(3)類似,實際上有些malloc實現(xiàn)會使用mmap匿名映射分配內(nèi)存,不過匿名映射不是POSIX標(biāo)準(zhǔn)中規(guī)定的。
文件映射有MAP_PRIVATE和MAP_SHARED兩種。前者使用COW的方式,把文件映射到當(dāng)前的進(jìn)程空間,修改操作不會改動源文件。后者直接把文件映射到當(dāng)前的進(jìn)程空間,所有的修改會直接反應(yīng)到文件的page cache,然后由內(nèi)核自動同步到映射文件上。
相比于IO函數(shù)調(diào)用,基于文件的mmap的一大優(yōu)點是把文件映射到進(jìn)程的地址空間,避免了數(shù)據(jù)從用戶緩沖區(qū)到內(nèi)核page cache緩沖區(qū)的復(fù)制過程;當(dāng)然還有一個優(yōu)點就是不需要頻繁的read/write系統(tǒng)調(diào)用。
