文章目錄
- 前言
- 一、簡介
- 1. mmap 是什么?
- 2. Linux 進程虛擬內存空間
- 二、mmap 內存映射
- 1. mmap 內存映射的實現過程
- 2. mmap 內存映射流程
- 2.1 mmap 系統調用函數
- 2.2 ksys_mmap_pgoff 函數
- 2.3 vm_mmap_pgoff 函數
- 2.4 do_mmap_pgoff 函數
- 2.5 do_mmap 函數
- 2.6 get_unmapped_area 函數
- 2.7 arch_get_unmapped_area 函數
- 2.8 find_vma_prev 函數
- 2.9 find_vma 函數
- 2.10 vm_unmapped_area 函數
- 2.11 unmapped_area 函數
- 2.12 mmap_region 函數
- 2.13 may_expand_vm 函數
- 2.14 find_vma_links 函數
- 2.15 vma_merge 函數
- 2.16 vma_link 函數
- 總結
前言
說到內存映射,很多人或多或少都了解過,筆者也看過很多文章,但總感覺晦澀難懂,甚至是越看越迷糊,不知道有沒有同學跟我有同感的。本著做筆記的原則,也為了方便以后的復習,把近幾天的學習成果及心得做一個記錄。由于 Linux 內核有關的知識點太多,虛擬內存、物理內存等,不可能一篇文章就能理清,本文僅對 mmap 內存映射的學習做個探索總結,如有不足還望一起討論學習。
一、簡介
1. mmap 是什么?
mmap 的全稱是 memory map,中文意思是內存映射或地址映射,是 Linux 操作系統中的一種系統調用,其作用是將一個文件或者其它對象映射到進程的虛擬地址空間,實現磁盤地址和進程虛擬地址空間一段虛擬地址的一一對應關系。通過 mmap 系統調用我們可以讓進程之間通過映射到同一個普通文件實現共享內存,普通文件被映射到進程虛擬地址空間當中后,進程可以像訪問普通內存一樣對文件進行一系列操作,而不需要通過 I/O 系統調用來讀取或寫入。
mmap ( ) 函數 聲明如下:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
函數各個參數的含義如下:
- addr:待映射的虛擬內存區域在進程虛擬內存空間中的起始地址(虛擬內存地址),通常設置成 NULL,意思就是完全交由內核來幫我們決定虛擬映射區的起始地址(要按照 PAGE_SIZE(4K) 對齊)。
- length:待申請映射的內存區域的大小,如果是匿名映射,則是要映射的匿名物理內存有多大,如果是文件映射,則是要映射的文件區域有多大(要按照 PAGE_SIZE(4K) 對齊)。
- prot:映射區域的保護模式。有 PROT_READ、PROT_WRITE、PROT_EXEC等。
- flags:標志位,可以控制映射區域的特性。常見的有 MAP_SHARED 和 MAP_PRIVATE 等。
- fd:文件描述符,用于指定映射的文件 (由 open( ) 函數返回)。
- offset:映射的起始位置,表示被映射對象 (即文件) 從那里開始對映,通常設置為 0,該值應該為大小為PAGE_SIZE(4K)的整數倍。
mmap ( ) 函數會將一個文件或其他對象映射到進程的地址空間中,并返回一個指向映射區域的指針,進程可以使用指針來訪問映射區域的數據,就像訪問內存一樣。系統會自動回寫臟頁面到對應的磁盤文件上,即完成了對文件的操作而不必再調用 read、write 等系統調用函數。相反,內核空間對這段映射區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。
port 取值的說明:
PORT_EXEC:映射的區域具有可執行權限
PROT_READ:映射的區域具有可讀權限
PROT_WRITE:映射區域具有可寫權限
PROT_NONE:映射區域不可被訪問;
flags 取值的說明:
MAP_SHARED:共享映射(用于多進程之間的通信),對映射區域的寫入操作直接反映到文件當中
MAP_FIXED:若在 start 上無法創建映射則失敗(如果沒有此標記會自動創建)
MAP_PRIVATE:私有映射,對映射區域的寫入操作只反映到緩沖區當中不會寫入到真正的文件
MAP_ANONYMOUS:匿名映射將虛擬地址映射到物理內存而不是文件(忽略fd、offset)
MAP_DENYWRITE:拒絕其它文件的寫入操作
MAP_LOCKED:鎖定映射區域保證其不被置換
MAP_POPULATE:內核在分配完虛擬內存之后,會立即分配物理內存,并在進程頁表中建立起虛擬內存與物理內存的映射關系
MAP_HUGETLB:用于大頁內存映射;
mmap 內存映射建立后的示意圖如下:
2. Linux 進程虛擬內存空間
由上小節的示意圖可以看出,進程的虛擬地址空間是由多個虛擬內存區域構成的。虛擬內存區域是進程的虛擬地址空間中的一個同質區間,即具有同樣特性的連續地址范圍。Linux 內核根據進程運行的過程中所需要不同種類的數據而為其開辟了對應的地址空間,分別為:
- 代碼段:存放進程程序二進制文件中的機器指令的存儲區域
- 數據段:也叫初始化數據段,代碼中被指定了初始值的全局變量和靜態變量在虛擬內存空間中的存儲區域
- bss段:代碼中沒有被指定初始值的全局變量和靜態變量在虛擬內存空間中的存儲區域
- 堆:程序運行過程中動態申請的內存在虛擬內存空間中的存儲區域
- 文件映射與匿名映射:存放動態鏈接庫中的代碼段,數據段,bss段,以及通過 mmap 系統調用映射的共享內存區的存儲區域
- 棧:存放函數調用過程中的局部變量和函數參數的存儲區域
Linux 內核中使用結構體 vm_area_struct (以下簡稱 vma)來描述這些虛擬內存區域,每個 vma 結構對應于虛擬內存空間中的唯一虛擬內存區域。vm_area_struct 結構體如下:
struct vm_area_struct {/* The first cache line has the info for VMA tree walking. */unsigned long vm_start; /* Our start address within vm_mm. */unsigned long vm_end; /* The first byte after our end address within vm_mm. *//* linked list of VM areas per task, sorted by address */struct vm_area_struct *vm_next, *vm_prev;struct rb_node vm_rb; // 紅黑樹struct mm_struct * vm_mm; /* The address space we belong to. */pgprot_t vm_page_prot; /* Access permissions of this VMA. */unsigned long vm_flags; /* Flags, see mm.h. */struct list_head anon_vma_chain; /* Serialized by mmap_sem & page_table_lock */struct anon_vma *anon_vma; /* Serialized by page_table_lock *//* Function pointers to deal with this struct. */const struct vm_operations_struct * vm_ops;unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE units,*not* PAGE_CACHE_SIZE */ struct file * vm_file; /* File we map to (can be NULL). */void * vm_private_data; /* was vm_pte (shared mem) */
}
vm_area_struct 結構體成員分析:
-
vm_start:指向虛擬內存區域的起始地址(最低地址),其本身包含在這塊虛擬內存區域內。
-
vm_end:指向虛擬內存區域的結束地址(最高地址),而其本身包含在這塊虛擬內存區域之外,所以 vm_area_struct 結構描述的是 [vm_start,vm_end) 這樣一段左閉右開的虛擬內存區域。
-
vm_next:后繼結點,指向下一個 vm_area_struct 的指針。
-
vm_prev:前驅節點,指向前一個 vm_area_struct 的指針,與 vm_next 共同構建進程的虛擬內存區域的鏈表(按地址排序)。
-
vm_rb:紅黑樹的一個葉節點,用來將多個 vma 連接成紅黑樹以便快速查詢。
-
vm_mm:反向指針,指向內存描述符 mm_struct 結構體,即虛擬內存區域所屬的進程的用戶虛擬地址空間。
vm_page_prot 和 vm_flags 都是用來標記 vm_area_struct 結構表示的這塊虛擬內存區域的訪問權限和行為規范。
- vm_page_prot:偏向于定義底層內存管理架構中頁這一級別的訪問控制權限,可以直接應用在底層頁表中,它是一個具體的概念。
- vm_flags:偏向于定義整個虛擬內存區域的訪問權限以及行為規范。描述的是虛擬內存區域中的整體信息,而不是虛擬內存區域中具體的某個獨立頁面,它是一個抽象的概念。
常用 vm_flags 訪問權限的取值說明:
VM_READ:可讀
VM_WRITE:可寫
VM_EXEC:可執行
VM_SHARD:可多進程之間共享
VM_IO:可映射至設備 IO 空間
VM_RESERVED:內存區域不可被換出
VM_SEQ_READ:內存區域可能被順序訪問
VM_RAND_READ:內存區域可能被隨機訪問
- anon_vma:如果該內存區域不與任何文件相關聯,也就是匿名映射,則用 struct anon_vma 結構體來指向關聯的匿名內存對象,用來組織匿名頁被映射到的所有的虛擬地址空間。
- vm_ops:指向針對虛擬內存區域的相關操作的函數指針,如:open、close、fault 函數等。
- vm_file:進行文件映射時,關聯被映射的文件,如果是匿名映射則為 null。
- vm_pgoff:表示映射進虛擬內存中的文件內容,在文件中的偏移,如果是匿名映射則無效。
- vm_private_data:存儲虛擬內存中的私有數據,具體的存儲內容和內存映射的類型有關。
虛擬內存空間中的 vma 是通過一個雙向鏈表(早期的內核實現是單向鏈表)串聯組織起來的,已有的 vma 按照低地址到高地址以遞增次序被歸入鏈表中,每個 vma 是這個鏈表里的一個節點。同時,為了快速在進程虛擬地址空間中查找 vma,vma 又通過紅黑樹(red black tree)組織起來,每個 vma 又是這個紅黑樹里的一個葉節點(使用紅黑樹查找的時間復雜度是O( l o g 2 N log_2N log2?N)),尤其是在 vma 數量很多的時候,可以顯著減少查找所需的時間(數量翻倍,查找次數也僅多一次)。
總的來說,vma 是 Linux 內核中非常重要的一個數據結構,承擔著描述進程虛擬內存區域的重要任務。當用戶進程調用 mmap 系統調用來映射文件時,系統會在當前進程的虛擬地址空間中,遍歷 vma 鏈表為其尋找一段連續的空閑地址。當找到合適的一段區間之后,會為其建立一個 vma 結構,完成這些后,該進程就有了一個專門用于 mmap 映射的虛擬內存區。此時進程頁表中,當前區域的線性地址還沒有對應的物理頁,接著系統會調用內核空間的系統調用函數 mmap,也就是需要我們在 file operations(f_op) 結構體中定義的這個 mmap,它將要完成對 vma 結構中的虛擬地址建立其相應的頁表項,完成其與文件的物理磁盤地址的映射關系。
二、mmap 內存映射
1. mmap 內存映射的實現過程
mmap 內存映射的實現過程,總的來說可以分為三個階段:
- 用戶進程啟動映射過程,并在虛擬地址空間中為映射創建虛擬映射區域
- 進程在用戶空間調用庫函數 mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
- 在當前進程的虛擬地址空間中,尋找一段空閑的滿足要求的連續的虛擬地址區域;
- 為此虛擬內存區域分配一個 vm_area_struct 結構,接著對這個結構的各個域進行初始化;
- 將新建的虛擬區結構 vm_area_struct 插入進程的虛擬地址區域鏈表或紅黑樹中;
- 調用內核空間的系統調用函數 mmap(不同于用戶空間函數),實現文件物理地址和進程虛擬地址的一一映射關系
- 為映射分配了新的虛擬地址區域后,通過待映射的文件指針,在文件描述符表中找到對應的文件描述符,通過文件描述符,鏈接到內核“已打開文件集”中該文件的文件結構體(struct file),每個文件結構體維護著和這個已打開文件的相關各項信息;
- 通過該文件的文件結構體,鏈接到 file_operations 模塊,調用內核函數 mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用戶空間庫函數;
- 內核 mmap 函數通過虛擬文件系統 inode 模塊定位到文件磁盤物理地址。
- 通過 remap_pfn_range 函數建立頁表,即實現了文件地址和虛擬地址區域的映射關系。此時,這片虛擬地址并沒有任何數據關聯到主存中;
- 進程發起對這片映射空間的訪問,引發缺頁異常,實現文件內容到物理內存(主存)的拷貝
- 進程的讀或寫操作訪問虛擬地址空間這一段映射地址,通過查詢頁表,發現這一段地址并不在物理頁面上。因為目前只建立了地址映射,真正的硬盤數據還沒有拷貝到內存中,因此引發缺頁異常;
- 缺頁異常進行一系列判斷,確定無非法操作后,內核發起請求調頁過程;
- 調頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內存頁,如果沒有則調用 nopage 函數把所缺的頁從磁盤裝入到主存中;
- 之后進程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間后系統會自動回寫臟頁面到對應磁盤地址,也即完成了寫入到文件的過程;
2. mmap 內存映射流程
首先看一下 mmap 內存映射的流程圖,結合流程圖再看其函數實現,會更加清晰明了:
2.1 mmap 系統調用函數
以 arm64 架構為例,系統調用函數 mmap 位于 /arch/arm64/kernel/sys.c 文件中,Linux 的系統調用對應的函數全部都是由 SYSCALL_DEFINE 相關的宏來定義的,有興趣的同學可自行學習了解,其源碼如下:
/arch/arm64/kernel/sys.c
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,unsigned long, prot, unsigned long, flags,unsigned long, fd, unsigned long, off)
{if (offset_in_page(off) != 0)return -EINVAL;return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}
方法中繼續調用了 ksys_mmap_pgoff() 方法,該方法位于 /mm/mmap.c 文件中
2.2 ksys_mmap_pgoff 函數
/mm/mmap.c
unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,unsigned long prot, unsigned long flags,unsigned long fd, unsigned long pgoff)
{struct file *file = NULL;unsigned long retval;if (!(flags & MAP_ANONYMOUS)) { // 預處理文件映射// 通過文件 fd 獲取映射文件的 struct file 結構audit_mmap_fd(fd, flags);// 通過文件 fd 獲取 file,從而獲取 inode 信息,關聯磁盤文件,后面關閉 fd,仍然可以用 mmap 操作file = fget(fd);......} else if (flags & MAP_HUGETLB) {// 從這里我們可以看出 MAP_HUGETLB 只能支持 MAP_ANONYMOUS 匿名映射的方式使用 HugePagestruct user_struct *user = NULL;struct hstate *hs; // 內核中的大頁池(預先創建)// 選取指定大頁尺寸的大頁池(內核中存在不同尺寸的大頁池)hs = hstate_sizelog((flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);if (!hs)return -EINVAL;// 映射長度 len 必須與大頁尺寸對齊len = ALIGN(len, huge_page_size(hs));// 在 hugetlbfs 中創建 anon_hugepage 文件,并預留大頁內存(禁止其他進程申請)file = hugetlb_file_setup(HUGETLB_ANON_FILE, len,VM_NORESERVE,&user, HUGETLB_ANONHUGE_INODE,(flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);if (IS_ERR(file))return PTR_ERR(file);}flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);// 開始內存映射retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
out_fput:if (file)fput(file);return retval;
}
ksys_mmap_pgoff 函數主要是針對 mmap 大頁映射的情況進行預處理,通過文件 fd 獲取對應的 struct file 結構,然后再將其轉發給位于 /mm/util.c 的 vm_mmap_pgoff 函數進行內存映射。
2.3 vm_mmap_pgoff 函數
/mm/util.c
unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,unsigned long len, unsigned long prot,unsigned long flag, unsigned long pgoff)
{unsigned long ret;struct mm_struct *mm = current->mm; // 獲取進程虛擬內存空間// 是否需要為映射的 vma,提前分配物理內存頁,避免后續的缺頁// 取決于 flag 是否設置了 MAP_POPULATE 或者 MAP_LOCKED,這里的 populate 表示需要分配物理內存的大小unsigned long populate;LIST_HEAD(uf); // 初始化 userfaultfd 鏈表// security_開頭的,都是security linux相關的,應該沒有人的服務器會開這個,返回值為 0ret = security_mmap_file(file, prot, flag);if (!ret) {// 對進程虛擬內存空間加寫鎖保護,防止多線程并發修改if (down_write_killable(&mm->mmap_sem))return -EINTR;// 開始 mmap 內存映射,在進程虛擬內存空間中分配一段 vma,并建立相關映射關系// 返回值 ret 為映射虛擬內存區域的起始地址ret = do_mmap_pgoff(file, addr, len, prot, flag, pgoff,&populate, &uf);up_write(&mm->mmap_sem); // 釋放寫鎖userfaultfd_unmap_complete(mm, &uf); // 等待 userfaultfd 處理完成if (populate)// 提前分配物理內存頁面,后續訪問不會缺頁,為 [ret , ret + populate] 這段虛擬內存立即分配物理內存mm_populate(ret, populate);}return ret;
}
vm_mmap_pgoff 函數的核心流程如下:
- 獲取進程虛擬內存空間 mm_struct,用于在開始 mmap 內存映射之前,對進程虛擬內存空間加寫鎖保護,防止多線程并發修改,映射完成后,再釋放寫鎖。
- 調用 do_mmap_pgoff 函數開始 mmap 內存映射,在進程虛擬內存空間中分配一段 vma,并建立相關映射關系。
- 如果設置了 MAP_POPULATE 或者 MAP_LOCKED 屬性,則調用 mm_populate 函數,提前為 [ret , ret + populate] 這段虛擬內存立即分配物理內存頁面,后續訪問不會發生缺頁中斷異常。
2.4 do_mmap_pgoff 函數
/include/linux/mm.h
static inline unsigned long
do_mmap_pgoff(struct file *file, unsigned long addr,unsigned long len, unsigned long prot, unsigned long flags,unsigned long pgoff, unsigned long *populate,struct list_head *uf)
{return do_mmap(file, addr, len, prot, flags, 0, pgoff, populate, uf);
}
do_mmap_pgoff 函數是一個內聯函數,其具體實現位于 /mm/mmap.c 的 do_mmap 函數中
2.5 do_mmap 函數
/mm/mmap.c
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 list_head *uf)
{struct mm_struct *mm = current->mm;int pkey = 0;*populate = 0;if (!len)return -EINVAL;// 如果進程帶有 READ_IMPLIES_EXEC 標記且文件系統是可執行的,則這段內存空間使用 READ 的屬性會附帶增加 EXEC 屬性if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))if (!(file && path_noexec(&file->f_path)))prot |= PROT_EXEC;/* force arch specific MAP_FIXED handling in get_unmapped_area */if (flags & MAP_FIXED_NOREPLACE)flags |= MAP_FIXED;if (!(flags & MAP_FIXED)) // 如果不是使用固定地址,則使用的 addr 會進行向下頁對齊addr = round_hint_to_min(addr);// 申請內存大小頁對齊,注意不要溢出len = PAGE_ALIGN(len);if (!len)return -ENOMEM;/* offset overflow? */if ((pgoff + (len >> PAGE_SHIFT)) < pgoff) // 判斷申請的內存是否溢出return -EOVERFLOW;// 一個進程虛擬內存空間內所能包含的虛擬內存區域 vma 是有數量限制的// sysctl_max_map_count 規定了進程虛擬內存空間所能包含 vma 的最大個數// 可以通過 /proc/sys/vm/max_map_count 內核參數調整 sysctl_max_map_count// mmap 需要在進程虛擬內存空間中創建映射的 vma,這里需要檢查已有 vma 的個數是否超過最大限制if (mm->map_count > sysctl_max_map_count)return -ENOMEM;// 在進程虛擬內存空間中尋找一塊未映射的虛擬內存區域,這段虛擬內存區域后續將會用于 mmap 內存映射addr = get_unmapped_area(file, addr, len, pgoff, flags);if (offset_in_page(addr)) // 如果返回的地址不是按照page對齊的,則直接返回return addr;if (flags & MAP_FIXED_NOREPLACE) {struct vm_area_struct *vma = find_vma(mm, addr);if (vma && vma->vm_start < addr + len)return -EEXIST;}if (prot == PROT_EXEC) {pkey = execute_only_pkey(mm);if (pkey < 0)pkey = 0;}// 簡單的檢查,通過 calc_vm_prot_bits 和 calc_vm_flag_bits 將 mmap 參數 prot , flag 中 // 設置的訪問權限以及映射方式等枚舉值轉換為統一的 vm_flags,后續一起映射進 VMA 的相應屬性中,相應前綴轉換為 VM_ vm_flags |= calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) |mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;// 設置了 MAP_LOCKED,表示用戶期望 mmap 背后映射的物理內存鎖定在內存中,不允許 swapif (flags & MAP_LOCKED)// 檢查是否可以將本次映射的物理內存鎖定if (!can_do_mlock())return -EPERM;// 進一步檢查鎖定的內存頁數是否超過了內核限制if (mlock_future_check(mm, vm_flags, len))return -EAGAIN;if (file) { // 文件映射struct inode *inode = file_inode(file);unsigned long flags_mask;if (!file_mmap_ok(file, inode, pgoff, len))return -EOVERFLOW;flags_mask = LEGACY_MAP_MASK | file->f_op->mmap_supported_flags;switch (flags & MAP_TYPE) {case MAP_SHARED: // 共享映射// 強制使用帶有 non-legacy 標志的 MAP_SHARED_VALIDATE。使用 MAP_SHARED 忽略不受支持的標志,以保持向后兼容性flags &= LEGACY_MAP_MASK;/* fall through */case MAP_SHARED_VALIDATE:if (flags & ~flags_mask)return -EOPNOTSUPP;if (prot & PROT_WRITE) {if (!(file->f_mode & FMODE_WRITE))return -EACCES;if (IS_SWAPFILE(file->f_mapping->host))return -ETXTBSY;}// 確保不向只追加的文件進行寫入if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))return -EACCES;// 確保文件上沒有強制鎖。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;pgoff = 0; // 忽略 pgoffvm_flags |= VM_SHARED | VM_MAYSHARE;break;case MAP_PRIVATE:pgoff = addr >> PAGE_SHIFT; // 根據匿名 vma 的 addr 設置 pgoffbreak;default:return -EINVAL;}}// 通常內核會為 mmap 申請虛擬內存的時候會綜合考慮 ram 以及 swap space 的總體大小。當映射的虛擬內存過大// 而沒有足夠的 swap space 的時候, mmap 就會失敗,設置 MAP_NORESERVE,內核將不會考慮上面的限制因素// 這樣當通過 mmap 申請大量的虛擬內存,并且當前系統沒有足夠的 swap space 的時候,mmap 系統調用依然能夠成功if (flags & MAP_NORESERVE) {// 設置 MAP_NORESERVE 的目的是為了應用可以申請過量的虛擬內存,如果內核本身是禁止 overcommit 的// 那么設置 MAP_NORESERVE 是無意義的,如果內核允許過量申請虛擬內存時(overcommit 為 0 或者 1)// 無論映射多大的虛擬內存,mmap 將會始終成功,但缺頁的時候會容易導致 oomif (sysctl_overcommit_memory != OVERCOMMIT_NEVER)vm_flags |= VM_NORESERVE; // 設置 VM_NORESERVE 表示無論申請多大的虛擬內存,內核總會答應// 大頁內存是提前預留出來的,并且本身就不會被 swap,所以不需要像普通內存頁那樣考慮 swap space 的限制因素if (file && is_file_hugepages(file))vm_flags |= VM_NORESERVE;}// 內存映射的核心,創建和初始化虛擬內存區域,并加入紅黑樹管理addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);// 假如沒有設置 MAP_POPULATE 標志位內核并不在調用 mmap 時就為進程分配物理內存空間,而是直到下次真正訪問// 地址空間時發現數據不存在于物理內存空間時才觸發 Page Fault 將缺失的 Page 換入內存空間 if (!IS_ERR_VALUE(addr) &&((vm_flags & VM_LOCKED) ||(flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))// 設置需要分配的物理內存大小*populate = len;return addr;
}
do_mmap 函數代碼很長,核心功能如下:
- 調用 get_unmapped_area 函數用于在進程地址空間中尋找出一段長度為 len,并且還未映射的虛擬內存區域 vma 出來,返回值 addr 表示這段虛擬內存區域的起始地址。之后根據不同的文件打開方式設置不同的 vm 標志位 flag;
- 調用 mmap_region 函數,首先會為剛才選取出來的映射虛擬內存區域分配 vma 結構,并根據映射信息進行初始化,以及建立 vma 與相關映射文件的關系,最后將這段 vma 插入到進程的虛擬內存空間中(鏈表或紅黑樹進行管理)。
接下來先跟蹤查看 get_unmapped_area 函數是如何尋找到合適長度的虛擬內存區域的?
2.6 get_unmapped_area 函數
/mm/mmap.c
unsigned long
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags)
{// 在進程虛擬空間中尋找還未被映射的 VMA 這段核心邏輯是被內核實現在特定于體系結構的函數中// 該函數指針用于指向真正的 get_unmapped_area 函數,在經典布局下,真正的實現函數為 arch_get_unmapped_areaunsigned long (*get_area)(struct file *, unsigned long,unsigned long, unsigned long, unsigned long);unsigned long error = arch_mmap_check(addr, len, flags);if (error)return error;/* Careful about overflows.. */// 映射的虛擬內存區域長度不能超過進程的地址空間if (len > TASK_SIZE) return -ENOMEM;// 如果是匿名映射,則采用 mm_struct 中保存的特定于體系結構的 arch_get_unmapped_area 函數get_area = current->mm->get_unmapped_area;if (file) {// 如果是文件映射,則需要使用 file->f_op 中的 get_unmapped_area 指向的函數來為文件映射申請虛擬內存// file->f_op 保存的是特定于文件系統中文件的相關操作,如 ext4 文件系統下的 thp_get_unmapped_area 函數if (file->f_op->get_unmapped_area)get_area = file->f_op->get_unmapped_area;} else if (flags & MAP_SHARED) {// 共享匿名映射是通過在 tmpfs 中創建的匿名文件實現的,所以這里也有其專有的 get_unmapped_area 函數pgoff = 0;// 共享匿名映射的情況下 get_unmapped_area 指向 shmem_get_unmapped_area 函數get_area = shmem_get_unmapped_area;}// 在進程虛擬內存空間中,根據指定的 addr,len 查找合適的 vmaaddr = get_area(file, addr, len, pgoff, flags);if (IS_ERR_VALUE(addr))return addr;// vma 區域不能超過進程地址空間if (addr > TASK_SIZE - len)return -ENOMEM;// addr 需要與 page size 對齊if (offset_in_page(addr))return -EINVAL;error = security_mmap_addr(addr);return error ? error : addr;
}
由代碼的注釋以及跟蹤查看各分支代碼可知:如果是文件映射,則需要使用 file->f_op 中的 get_unmapped_area 指向的函數來為文件映射申請虛擬內存,file->f_op 保存的是特定于文件系統中文件的相關操作,如 ext4 文件系統下的 thp_get_unmapped_area 函數;如果是共享匿名映射的情況下 get_unmapped_area 指向 shmem_get_unmapped_area 函數;上述兩種情況下,其最終會跟私有匿名映射一樣,都會調用到 mm->get_unmapped_area 函數指針指向的函數,在經典布局下,mm->get_unmapped_area 指向的是 arch_get_unmapped_area 函數。
2.7 arch_get_unmapped_area 函數
/mm/mmap.c
unsigned long
arch_get_unmapped_area(struct file *filp, unsigned long addr,unsigned long len, unsigned long pgoff, unsigned long flags)
{struct mm_struct *mm = current->mm;struct vm_area_struct *vma, *prev;struct vm_unmapped_area_info info;// 進程虛擬內存空間的末尾 TASK_SIZEconst unsigned long mmap_end = arch_get_mmap_end(addr);// 映射區域長度是否超過進程虛擬內存空間if (len > mmap_end - mmap_min_addr)return -ENOMEM;// 如果我們指定了 MAP_FIXED 表示必須要從我們指定的 addr 開始映射 len 長度的區域// 如果這塊區域已經存在映射關系,那么后續內核會把舊的映射關系覆蓋掉if (flags & MAP_FIXED)return addr;// 沒有指定 MAP_FIXED,但指定了 addr,內核從指定的 addr 地址開始映射,內核這里會檢查指定的這塊虛擬內存范圍是否有效if (addr) {addr = PAGE_ALIGN(addr); // addr 先保證與 page size 對齊// 內核這里需要確認一下我們指定的 [addr, addr+len] 這段虛擬內存區域是否存在已有的映射關系// [addr, addr+len] 地址范圍內已經存在映射關系,則不能按照我們指定的 addr 作為映射起始地址// 在進程地址空間中查找第一個符合 addr < vma->vm_end 條件的 vma// 如果不存在這樣一個 vma(!vma), 則表示 [addr, addr+len] 這段范圍的虛擬內存是可以使用的,內核將會從我們指定的 addr 開始映射// 如果存在這樣一個 vma ,則表示 [addr, addr+len] 這段范圍的虛擬內存區域目前已經存在映射關系了,不能采用 addr 作為映射起始地址// 這里還有一種情況是 addr 落在 prev 和 vma 之間的一塊未映射區域// 如果這塊未映射區域的長度滿足 len 大小,那么這段未映射區域可以被本次使用,內核也會從我們指定的 addr 開始映射vma = find_vma_prev(mm, addr, &prev);if (mmap_end - len >= addr && addr >= mmap_min_addr &&(!vma || addr + len <= vm_start_gap(vma)) &&(!prev || addr >= vm_end_gap(prev)))return addr;}// 如果明確指定 addr 但是指定的虛擬內存范圍是一段無效的區域或者已經存在映射關系// 那么內核會自動在地址空間中尋找一段合適的虛擬內存范圍出來,這段虛擬內存范圍的起始地址就不是指定的 addrinfo.flags = 0;// vma 區域長度info.length = len;// 定義從哪里開始查找 vma, mmap_base 表示從文件映射與匿名映射區開始查找info.low_limit = mm->mmap_base;// 查找結束位置為進程地址空間的末尾 TASK_SIZEinfo.high_limit = mmap_end;info.align_mask = 0;info.align_offset = 0;return vm_unmapped_area(&info);
}
arch_get_unmapped_area 函數的核心作用如下:
- 調用 find_vma_prev 函數,根據指定的映射起始地址 addr,在進程地址空間中查找出符合 addr < vma->vm_end 條件的第一個 vma,然后在進程地址空間 mm_struct 中 mmap 指向的 vma 鏈表中,找出它的前驅節點 pprev。
- 如果明確指定起始地址 addr ,但是指定的虛擬內存范圍有一段無效的區域或者已經存在映射關系,內核就不能按照我們指定的 addr 開始映射,此時調用 vm_unmapped_area 函數,內核會自動在文件映射與匿名映射區中按照地址的增長方向尋找一段 len 大小的虛擬內存范圍出來。注意:此時找到的虛擬內存范圍的起始地址就不是指定的 addr。
2.8 find_vma_prev 函數
/mm/mmap.c
struct vm_area_struct *
find_vma_prev(struct mm_struct *mm, unsigned long addr,struct vm_area_struct **pprev)
{struct vm_area_struct *vma;// 在進程地址空間 mm_struct 中查找第一個符合 addr < vma->vm_end 的 vmavma = find_vma(mm, addr);if (vma) { // 恰好包含 addr 的 vma 的前一個虛擬內存區域 *pprev = vma->vm_prev;} else {// 如果當前進程地址空間中,addr 不屬于任何一個 vma,那這里的 pprev 指向進程地址空間中最后一個 vmastruct rb_node *rb_node = rb_last(&mm->mm_rb);*pprev = rb_node ? rb_entry(rb_node, struct vm_area_struct, vm_rb) : NULL;}// 返回查找到的 vma,不存在則返回 null(內核后續會創建 vma)return vma;
}
繼續調用 find_vma 函數查找符合需求的 vma,找到則返回,不存在則返回 null(內核后續會創建 vma)。
2.9 find_vma 函數
/mm/mmap.c
/* Look up the first VMA which satisfies addr < vm_end, NULL if none. */
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{struct rb_node *rb_node;struct vm_area_struct *vma;// 進程地址空間中緩存了最近訪問過的 vma,首先從進程地址空間中 vma 緩存中開始查找,緩存命中率通常大約為 35%// 查找條件為:vma->vm_start <= addr && vma->vm_end > addrvma = vmacache_find(mm, addr);if (likely(vma))return vma;// 進程地址空間中的所有 vma 被組織在一顆紅黑樹中,為了方便內核在進程地址空間中快速查找特定的 vma// 這里首先需要獲取紅黑樹的根節點,內核會從根節點開始查找rb_node = mm->mm_rb.rb_node;while (rb_node) {struct vm_area_struct *tmp;// 獲取位于根節點的 vmatmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);if (tmp->vm_end > addr) {vma = tmp;// 判斷 addr 是否恰好落在根節點 vma 中: vm_start <= addr < vm_endif (tmp->vm_start <= addr)break;rb_node = rb_node->rb_left; // 如果不存在,則繼續到左子樹中查找} else// 如果根節點的 vm_end <= addr,說明 addr 在根節點 vma 的后邊,這種情況則到右子樹中繼續查找rb_node = rb_node->rb_right;}if (vma)// 更新 vma 緩存vmacache_update(addr, vma);// 返回查找到的 vma,如果沒有查找到,則返回 null,表示進程空間中目前還沒有這樣一個 vma,后續需要新建return vma;
}
由于進程地址空間中緩存了最近訪問過的 vma,因此 find_vma 函數首先從進程地址空間中 vma 緩存中開始查找,找到則直接返回。如果找不到則遍歷整個 vma 紅黑樹進行查找,找到則返回查找到的 vma,否則返回 null,表示進程地址空間中目前還沒有這樣一個 vma,后續需要新建。
回到 2.7 小節 arch_get_unmapped_area 函數,經過查找后,如果找到的這個 vma 與 [addr , addr +len] 這段虛擬地址范圍有重疊的部分,那么內核就不能按照指定的 addr 開始映射,此時則調用 vm_unmapped_area 函數,內核會自動在文件映射與匿名映射區中按照地址的增長方向尋找一段 len 大小的虛擬內存范圍出來。
2.10 vm_unmapped_area 函數
/include/linux/mm.h
static inline unsigned long
vm_unmapped_area(struct vm_unmapped_area_info *info)
{// 按照進程虛擬內存空間中文件映射與匿名映射區的地址增長方向分為兩個函數,用來在進程地址空間中查找未映射的 vmaif (info->flags & VM_UNMAPPED_AREA_TOPDOWN)// 當文件映射與匿名映射區的地址增長方向是從上到下逆向增長時(新式布局),采用 topdown 后綴的函數查找return unmapped_area_topdown(info);else// 地址增長方向為從下倒上正向增長(經典布局),采用該函數查找return unmapped_area(info);
}
vm_unmapped_area 函數是一個內聯函數,內部按照進程虛擬內存空間中文件映射與匿名映射區的地址增長方向分為兩個函數,這里選擇 unmapped_area 函數繼續跟蹤查看。
2.11 unmapped_area 函數
/mm/mmap.c
unsigned long unmapped_area(struct vm_unmapped_area_info *info)
{/** We implement the search by looking for an rbtree node that* immediately follows a suitable gap. That is,* - gap_start = vma->vm_prev->vm_end <= info->high_limit - length;* - gap_end = vma->vm_start >= info->low_limit + length;* - gap_end - gap_start >= length*/struct mm_struct *mm = current->mm;// 尋找未映射區域的參考 vma (該區域已存在映射關系)struct vm_area_struct *vma;// 未映射區域產生在 vma->vm_prev 與 vma 這兩個虛擬內存區域中的間隙 gap 中,length 表示本次映射區域的長度// low_limit ,high_limit 表示在進程地址空間中哪段地址范圍內查找,一個地址下限(mm->mmap_base),另一個標識地址上限(TASK_SIZE)// gap_start, gap_end 表示 vma->vm_prev 與 vma 之間的 gap 范圍,unmapped_area 將會在這里產生unsigned long length, low_limit, high_limit, gap_start, gap_end;// 調整搜索長度以考慮最壞情況下的對齊開銷length = info->length + info->align_mask;if (length < info->length)return -ENOMEM;// 根據需要的長度調整搜索限制if (info->high_limit < length)return -ENOMEM;// gap_start 需要滿足的條件:gap_start = vma->vm_prev->vm_end <= info->high_limit - length// 否則 unmapped_area 將會超出 high_limit 的限制high_limit = info->high_limit - length;if (info->low_limit > high_limit)return -ENOMEM;// gap_end 需要滿足的條件:gap_end = vma->vm_start >= info->low_limit + length// 否則 unmapped_area 將會超出 low_limit 的限制low_limit = info->low_limit + length;// 首先將 vma 紅黑樹的根節點作為 gap 的參考 vma,檢查根節點是否符合if (RB_EMPTY_ROOT(&mm->mm_rb))goto check_highest;// 獲取紅黑樹根節點的 vmavma = rb_entry(mm->mm_rb.rb_node, struct vm_area_struct, vm_rb);// rb_subtree_gap 為當前 vma 及其左右子樹中所有 vma 與其對應 vm_prev 之間最大的虛擬內存地址 gap// 最大的 gap 如果都不能滿足映射長度 length 則跳轉到 check_highest 處理if (vma->rb_subtree_gap < length)goto check_highest; // 從進程地址空間最后一個 vma->vm_end 地址處開始映射while (true) {// 左子樹,獲取當前 vma 的 vm_start 起始虛擬內存地址作為 gap_endgap_end = vm_start_gap(vma);// gap_end 需要滿足:gap_end >= low_limit,否則 unmapped_area 將會超出 low_limit 的限制// 如果存在左子樹,則需要繼續到左子樹中去查找,因為我們需要按照地址從低到高的優先級來查看合適的未映射區域if (gap_end >= low_limit && vma->vm_rb.rb_left) {struct vm_area_struct *left =rb_entry(vma->vm_rb.rb_left,struct vm_area_struct, vm_rb);// 如果左子樹中存在合適的 gap,則繼續左子樹的查找// 否則查找結束,gap 為當前 vma 與其 vm_prev 之間的間隙 if (left->rb_subtree_gap >= length) {vma = left;continue;}}// 獲取當前 vma->vm_prev 的 vm_end 作為 gap_startgap_start = vma->vm_prev ? vm_end_gap(vma->vm_prev) : 0;
check_current:/* Check if current node has a suitable gap */// gap_start 需要滿足:gap_start <= high_limit,否則 unmapped_area 將會超出 high_limit 的限制if (gap_start > high_limit)return -ENOMEM;if (gap_end >= low_limit &&gap_end > gap_start && gap_end - gap_start >= length)goto found; // 找到了合適的 unmapped_area 跳轉到 found 處理// 當前 vma 與其左子樹中的所有 vma 均不存在一個合理的 gap,那么從 vma 的右子樹中繼續查找if (vma->vm_rb.rb_right) {struct vm_area_struct *right =rb_entry(vma->vm_rb.rb_right,struct vm_area_struct, vm_rb);if (right->rb_subtree_gap >= length) {vma = right;continue;}}// 如果在當前 vma 以及它的左右子樹中均無法找到一個合適的 gap// 那么這里會從當前 vma 節點向上回溯整顆紅黑樹,在它的父節點中嘗試查找是否有合適的 gap// 因為這時候有可能會有新的 vma 插入到紅黑樹中,可能會產生新的 gapwhile (true) {struct rb_node *prev = &vma->vm_rb;if (!rb_parent(prev))goto check_highest;vma = rb_entry(rb_parent(prev),struct vm_area_struct, vm_rb);if (prev == vma->vm_rb.rb_left) {gap_start = vm_end_gap(vma->vm_prev);gap_end = vm_start_gap(vma);goto check_current;}}}check_highest:// 流程走到這里表示在當前進程虛擬內存空間的所有 vma 中都無法找到一個合適的 gap 來作為 unmapped_area// 那么就從進程地址空間中最后一個 vma->vm_end 開始映射// mm->highest_vm_end 表示當前進程虛擬內存空間中,地址最高的一個 vma 的結束地址位置gap_start = mm->highest_vm_end;gap_end = ULONG_MAX; /* Only for VM_BUG_ON below */if (gap_start > high_limit) // 這里最后需要檢查剩余虛擬內存空間是否滿足映射長度return -ENOMEM;found:/* We found a suitable gap. Clip it with the original low_limit. */// 流程走到這里表示已經找到了一個合適的 gap 來作為 unmapped_area,直接返回 gap_start(需要與 4K 對齊)作為映射的起始地址if (gap_start < info->low_limit)gap_start = info->low_limit;// 調整間隙地址到所需的對齊方式gap_start += (info->align_offset - gap_start) & info->align_mask;VM_BUG_ON(gap_start + info->length > info->high_limit);VM_BUG_ON(gap_start + info->length > gap_end);return gap_start; // 返回找到的地址間隙 gap
}
unmapped_area 函數的核心任務就是在管理進程地址空間這些 vma 的紅黑樹 mm_struct-> mm_rb 中查找出一個滿足條件的地址間隙 gap 用于內存映射。如果能夠找到符合條件的地址間隙 gap 則直接返回,否者就從進程地址空間中最后一個 vma->vm_end 開始映射。
回到 2.5 小節 do_mmap 函數,此時內核已經通過 get_unmapped_area 函數在進程地址空間中找出一段地址范圍為 [addr , addr + len] 的虛擬內存區域供 mmap 進行映射。接下來跟蹤查看 mmap_region 函數具體是如何初始化 vma 并建立映射關系的?首先看一下mmap_region 函數的流程圖,結合流程圖再看其函數實現,會更加清晰明了:
2.12 mmap_region 函數
/mm/mmap.c
unsigned long mmap_region(struct file *file, unsigned long addr,unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,struct list_head *uf)
{struct mm_struct *mm = current->mm;struct vm_area_struct *vma, *prev;int error;struct rb_node **rb_link, *rb_parent;unsigned long charged = 0;// 再次檢查本次映射是否超過了進程虛擬內存空間中的虛擬內存容量的限制,超過則返回 falseif (!may_expand_vm(mm, vm_flags, len >> PAGE_SHIFT)) {unsigned long nr_pages;// 如果 mmap 指定了 MAP_FIXED,表示內核必須要按照用戶指定的映射區來進行映射// 這種情況下就會導致,指定的映射區 [addr, addr + len] 有一部分可能與現有映射重疊// 內核將會覆蓋掉這段已有的映射,重新按照用戶指定的映射關系進行映射// 所以這里需要計算進程地址空間中與指定映射區[addr, addr + len]重疊的虛擬內存頁數 nr_pagesnr_pages = count_vma_pages_range(mm, addr, addr + len);// 由于這里的 nr_pages 表示重疊的虛擬內存部分,將會被覆蓋,所以這部分被覆蓋的虛擬內存不需要額外申請// 這里通過 len >> PAGE_SHIFT 減去這段可以被覆蓋的 nr_pages 在重新檢查是否超過虛擬內存相關區域的限額if (!may_expand_vm(mm, vm_flags,(len >> PAGE_SHIFT) - nr_pages))return -ENOMEM;}// 如果當前進程地址空間中存在指定映射區域 [addr, addr + len] 重疊的部分while (find_vma_links(mm, addr, addr + len, &prev, &rb_link,&rb_parent)) {if (do_munmap(mm, addr, len, uf)) // 調用 do_munmap 將這段重疊的映射部分解除掉,后續會重新映射這部分return -ENOMEM;}// 判斷將來是否會為這段虛擬內存 vma 申請新的物理內存,比如:私有、可寫(private writable)的映射方式,內核將來會通過 cow 重新為其分配新的物理內存。// 私有、只讀(private readonly)的映射方式,內核則會共享原來映射的物理內存,而不會申請新的物理內存。// 如果將來需要申請新的物理內存則會根據當前系統的 overcommit 策略以及當前物理內存的使用情況來 // 綜合判斷是否允許本次虛擬內存的申請。如果虛擬內存不足,則返回 ENOMEM,這樣的話可以防止缺頁的時候發生 OOMif (accountable_mapping(file, vm_flags)) {charged = len >> PAGE_SHIFT;// 根據內核 overcommit 策略以及當前物理內存的使用情況綜合判斷,是否能夠通過本次虛擬內存的申請// 虛擬內存的申請一旦這里通過之后,后續發生缺頁,內核將會有足夠的物理內存為其分配,不會發生 OOMif (security_vm_enough_memory_mm(mm, charged))return -ENOMEM;// 凡是設置了 VM_ACCOUNT 的 vma,表示這段虛擬內存均已經過 vm_enough_memory 的檢測// 當虛擬內存發生缺頁的時候,內核會有足夠的物理內存分配,而不會導致 OOM // 其虛擬內存的用量都會被統計在 /proc/meminfo 的 Committed_AS 字段中 vm_flags |= VM_ACCOUNT;}// 為了精細化的控制內存的開銷,內核這里首先需要嘗試看能不能和地址空間中已有的 vma 進行合并,嘗試將當前 vma 合并到已有的 vma 中vma = vma_merge(mm, prev, addr, addr + len, vm_flags,NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);if (vma)goto out; // 如果可以合并,則虛擬內存分配過程結束// 如果不可以合并,則只能從 slab 中取出一個新的 vma 結構來vma = vm_area_alloc(mm);if (!vma) {error = -ENOMEM;goto unacct_error;}// 根據要映射的虛擬內存區域屬性初始化 vma 結構中的相關字段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;if (file) { // 如果是文件映射if (vm_flags & VM_DENYWRITE) { // 映射的文件不允許寫入,調用 deny_write_accsess(file) 排斥常規的文件操作 error = deny_write_access(file);if (error)goto free_vma;}if (vm_flags & VM_SHARED) {error = mapping_map_writable(file->f_mapping); // 映射的文件允許其他進程可見, 標記文件為可寫if (error)goto allow_write_and_free_vma;}// 將文件與虛擬內存映射起來vma->vm_file = get_file(file); // 遞增 File 的引用次數,返回 File 賦給 vma// 將虛擬內存區域 vma 的操作函數 vm_ops 映射成文件的操作函數(和具體文件系統有關)// ext4 文件系統中的操作函數為 ext4_file_vm_ops,此刻開始,讀寫內存就和讀寫文件是一樣的了error = call_mmap(file, vma);if (error)goto unmap_and_free_vma;WARN_ON_ONCE(addr != vma->vm_start);// 文件系統提供的mmap函數可能會修改映射的一些參數。在這里需要在調用 vma_link 前回置addr = vma->vm_start;vm_flags = vma->vm_flags;} else if (vm_flags & VM_SHARED) { // 共享匿名映射// 共享匿名映射依賴于 tmpfs 文件系統中的匿名文件 dev/zero,父子進程通過這個匿名文件進行通訊// 該函數用于在 tmpfs 中創建匿名文件,并映射進當前共享匿名映射區 vma 中error = shmem_zero_setup(vma);if (error)goto free_vma;} else { // 私有匿名映射// 將 vma->vm_ops 設置為 null,只有文件映射才需要 vm_ops 這樣才能將內存與文件映射起來vma_set_anonymous(vma);}// 將當前 vma 按照地址的增長方向插入到進程虛擬內存空間的 mm_struct->mmap 鏈表// 以及 mm_struct->mm_rb 紅黑樹中,并建立文件與 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);// 進程內存狀態統計,在開啟了 proc 時才會有vm_stat_account(mm, vm_flags, len >> PAGE_SHIFT);if (vm_flags & VM_LOCKED) {if ((vm_flags & VM_SPECIAL) || vma_is_dax(vma) ||is_vm_hugetlb_page(vma) ||vma == get_gate_vma(current->mm))vma->vm_flags &= VM_LOCKED_CLEAR_MASK;elsemm->locked_vm += (len >> PAGE_SHIFT);}if (file)uprobe_mmap(vma);vma->vm_flags |= VM_SOFTDIRTY;// 更新地址空間 mm_struct 中的相關統計變量vma_set_page_prot(vma);return addr;
unmap_and_free_vma:vma->vm_file = NULL;fput(file);// 撤銷由設備驅動程序完成的映射unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);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:vm_area_free(vma);
unacct_error:if (charged)vm_unacct_memory(charged);return error;
}
mmap_region 函數主要負責創建虛擬內存區域,其核心流程如下:
- 調用 may_expand_vm 函數以檢查進程在本次 mmap 映射之后申請的虛擬內存是否超過限制,檢查(進程的虛擬內存總數+申請的頁數)是否超過地址空間限制,如果是私有的可寫映射,并且不是棧,則檢查(進程的虛擬內存總數+申請的頁數)是否超過最大數據長度;
- 調用 find_vma_links 函數查找當前進程地址空間中是否存在與指定映射區域 [addr, addr+len] 重疊的部分,如果有重疊則需調用 do_munmap 函數將這段重疊的映射部分解除掉,后續會重新映射這部分;
- 調用 vma_merge 函數,內核先嘗試看能不能將待映射的 vma 和地址空間中已有的 vma 進行合并,如果可以合并,則不用創建新的 vma 結構,節省內存的開銷。如果不能合并,則從 slab 中取出一個新的 vma 結構,并根據要映射的虛擬內存區域屬性初始化 vma 結構中的相關字段;
- 調用 vma_link 函數把虛擬內存區域 vma 插入到鏈表和紅黑樹中。如果 vma 關聯文件,那么把虛擬內存區域添加到文件的區間樹中,文件的區間樹用來跟蹤文件被映射到哪些虛擬內存區域;
- 調用 vma_set_page_prot 函數更新地址空間 mm_struct 中的相關統計變量,根據虛擬內存標志(vma->vm_flags)計算頁保護位(vma->vm_page_prot),如果共享的可寫映射想要把頁標記為只讀,其目的是跟蹤寫事件,那么從頁保護位刪除可寫位。
2.13 may_expand_vm 函數
/mm/mmap.c
// 檢查本次映射是否超過了進程虛擬內存空間中的虛擬內存總量的限制,超過則返回 false
bool may_expand_vm(struct mm_struct *mm, vm_flags_t flags, unsigned long npages)
{// mm->total_vm 表示當前進程地址空間中映射的虛擬內存頁總數// npages 表示此次要映射的虛擬內存頁個數// rlimit(RLIMIT_AS) 表示進程地址空間中允許映射的虛擬內存總量,單位為字節if (mm->total_vm + npages > rlimit(RLIMIT_AS) >> PAGE_SHIFT)return false; // 如果映射的虛擬內存頁總數超出了內核的限制,那么就返回 false 表示虛擬內存不足// 檢查本次映射是否屬于數據區域的映射,這里的數據區域指的是私有,可寫的虛擬內存區域(棧區除外)// 如果是則需要檢查數據區域里的虛擬內存頁是否超過了內核的限制// rlimit(RLIMIT_DATA) 表示進程地址空間中允許映射的私有,可寫的虛擬內存總量,單位為字節// 如果超過則返回 false,表示數據區虛擬內存不足if (is_data_mapping(flags) &&mm->data_vm + npages > rlimit(RLIMIT_DATA) >> PAGE_SHIFT) {/* Workaround for Valgrind */if (rlimit(RLIMIT_DATA) == 0 &&mm->data_vm + npages <= rlimit_max(RLIMIT_DATA) >> PAGE_SHIFT)return true;pr_warn_once("%s (%d): VmData %lu exceed data ulimit %lu. Update limits%s.\n",current->comm, current->pid,(mm->data_vm + npages) << PAGE_SHIFT,rlimit(RLIMIT_DATA),ignore_rlimit_data ? "" : " or use boot option ignore_rlimit_data");if (!ignore_rlimit_data)return false;}return true;
}
may_expand_vm 函數的核心邏輯就是判斷經過本次 mmap 映射之后,mm->total_vm + npages 是否超過了 rlimit(RLIMIT_AS) 中的限制,mm->data_vm + npages 是否超過了 rlimit(RLIMIT_DATA) 中的限制。如果超過,那么本次 mmap 內存映射流程在這里就會停止進行。注意:npages 是指 mmap 需要映射的虛擬內存頁數。
2.14 find_vma_links 函數
/mm/mmap.c
static int find_vma_links(struct mm_struct *mm, unsigned long addr,unsigned long end, struct vm_area_struct **pprev,struct rb_node ***rb_link, struct rb_node **rb_parent)
{struct rb_node **__rb_link, *__rb_parent, *rb_prev;// 獲取紅黑樹的根節點__rb_link = &mm->mm_rb.rb_node;rb_prev = __rb_parent = NULL;while (*__rb_link) { // 遍歷整棵紅黑樹,為[addr,addr+len]這段內存區域查找合適的插入位置struct vm_area_struct *vma_tmp;__rb_parent = *__rb_link;vma_tmp = rb_entry(__rb_parent, struct vm_area_struct, vm_rb);// 插入的 vma 起始地址小于當前紅黑樹節點 vma 結束地址,則遍歷紅黑樹左子樹if (vma_tmp->vm_end > addr) {// 如果紅黑樹中現有 vma 與該映射區域重疊,則返回失敗if (vma_tmp->vm_start < end)return -ENOMEM;__rb_link = &__rb_parent->rb_left; // 循環遍歷查找左子樹} else {// 插入的 vma 起始地址大于當前紅黑樹節點 vma 結束地址,則遍歷紅黑樹右子樹,說明紅黑樹左子節點到右子節點的VMA區域程遞增趨勢rb_prev = __rb_parent; // 更新待插入 vma 節點的前一個節點,即其父節點__rb_link = &__rb_parent->rb_right; // 循環遍歷查找右子樹}}// pprev 待插入 vma 節點的前一個節點的 vma,如果 rb_prev 為空,說明待插入節點是最左子節點,在鏈表mm->mmap中是頭節點*pprev = NULL;if (rb_prev) *pprev = rb_entry(rb_prev, struct vm_area_struct, vm_rb);*rb_link = __rb_link; // 查找到的待插入 vma 節點位置*rb_parent = __rb_parent; // 待插入位置節點的父節點return 0;
}
find_vma_links 函數的作用是在當前進程地址空間中查找是否存在與指定映射區域 [addr, addr+len] 重疊的部分,如果查找到現存的 vma 和該指定映射區域有重疊則返回錯誤,如果不存在重疊部分,則表示找到 vma 待插入的位置,包括其在鏈表中的位置 prev 和紅黑樹中的位置 rb_link 和 rb_parent,分別是待插入節點本身在紅黑樹中的位置和待插入節點的父節點。
2.15 vma_merge 函數
/mm/mmap.c
struct vm_area_struct *vma_merge(struct mm_struct *mm,struct vm_area_struct *prev, unsigned long addr,unsigned long end, unsigned long vm_flags,struct anon_vma *anon_vma, struct file *file,pgoff_t pgoff, struct mempolicy *policy,struct vm_userfaultfd_ctx vm_userfaultfd_ctx)
{pgoff_t pglen = (end - addr) >> PAGE_SHIFT; // 本次需要創建的 vma 區域大小// area 表示當前要創建的 vma,next 表示 area 的下一個 vma// 事實上 area 會在其 prev 前一個 vma 和 next 后一個 vma 之間的間隙 gap 中創建產生struct vm_area_struct *area, *next;int err;// 設置了 VM_SPECIAL 表示 area 區域是不可以被合并的,只能重新創建 vma,并直接退出合并流程if (vm_flags & VM_SPECIAL)return NULL;// 根據 prev vma 是否存在,設置 area 的 next vmaif (prev)next = prev->vm_next; // area 將在 prev vma 和 next vma 的間隙 gap 中產生elsenext = mm->mmap; // 如果 prev 不存在,那么 next 就設置為地址空間中的第一個 vmaarea = next;// 新 vma 的 end 與 next->vm_end 相等,表示新 vma 與 next vma 是重合的// 那么 next 指向下一個 vma,prev 和 next 這里的語義是始終指向 area 區域的前一個和后一個 vmaif (area && area->vm_end == end) /* cases 6, 7, 8 */next = next->vm_next;/* verify some invariant that must be enforced by the caller */VM_WARN_ON(prev && addr <= prev->vm_start);VM_WARN_ON(area && end > area->vm_end);VM_WARN_ON(addr >= end);// 判斷 area 是否能夠和 prev 進行合并if (prev && prev->vm_end == addr &&mpol_equal(vma_policy(prev), policy) &&can_vma_merge_after(prev, vm_flags,anon_vma, file, pgoff,vm_userfaultfd_ctx)) { // 如果 area 可以和 prev 進行合并,那么這里繼續判斷 area 能夠與 next 進行合并// 內核這里需要保證 vma 合并程度的最大化if (next && end == next->vm_start &&mpol_equal(policy, vma_policy(next)) &&can_vma_merge_before(next, vm_flags,anon_vma, file,pgoff+pglen,vm_userfaultfd_ctx) &&is_mergeable_anon_vma(prev->anon_vma,next->anon_vma, NULL)) { /* cases 1,6 */// 到此則表示 area 可以和它的 prev,next 區域進行合并 // __vma_adjust 是真正執行 vma 合并操作的函數,會重新調整已有 vma 的相關屬性,比如:vm_start,vm_end,vm_pgoff。// 以及涉及到相關數據結構的改變err = __vma_adjust(prev, prev->vm_start,next->vm_end, prev->vm_pgoff, NULL,prev);} else /* cases 2, 5, 7 */// 流程到此則表示 area 只能和 prev 進行合并err = __vma_adjust(prev, prev->vm_start,end, prev->vm_pgoff, NULL, prev);if (err)return NULL;khugepaged_enter_vma_merge(prev, vm_flags);return prev; // 返回最終合并好的 vma}// 下面這種情況屬于,area 的結束地址 end 與 next 的起始地址是重合的// 但是 area 的起始地址 start 和 prev 的結束地址不是重合的if (next && end == next->vm_start &&mpol_equal(policy, vma_policy(next)) &&can_vma_merge_before(next, vm_flags,anon_vma, file, pgoff+pglen,vm_userfaultfd_ctx)) {// area 區域前半部分和 prev 區域的后半部分重合// 那么就縮小 prev 區域,然后將 area 合并到 next 區域if (prev && addr < prev->vm_end) /* case 4 */err = __vma_adjust(prev, prev->vm_start,addr, prev->vm_pgoff, NULL, next);else { /* cases 3, 8 */// area 區域前半部分和 prev 區域是有間隙 gap 的// 那么這種情況下 prev 不變,area 合并到 next 中err = __vma_adjust(area, addr, next->vm_end,next->vm_pgoff - pglen, NULL, next);area = next; // 合并后的 area}if (err)return NULL;khugepaged_enter_vma_merge(area, vm_flags);return area; // 返回合并后的 vma}// prev 的結束地址不與 area 的起始地址重合,并且 area 的結束地址不與 next 的起始地址重合// 這種情況就不能執行合并,需要為 area 重新創建新的 vma 結構return NULL;
}
mmap_region 函數在創建新的 vma 結構之前,內核首先需要嘗試看能不能將當前 vma 和地址空間中已有的 vma 進行合并,以避免創建新的 vma 結構,節省內存的開銷。內核本著合并最大化的原則,檢查當前映射出來的 vma 能否與其前后兩個 vma 進行合并,能合并就合并,如果不能合并就從 slab 中申請新的 vma 結構。合并條件如下:
- 新映射 vma 的 vm_flags 不能設置 VM_SPECIAL 標志,該標志表示 vma 區域是不可以被合并的,只能重新創建 vma。
- 新映射 vma 的起始地址 addr 必須要與其前一個 vma 的結束地址重合,這樣 vma 才能和它的前一個 vma 進行合并,如果不重合,vma 則不能和前一個 vma 進行合并。
- 新映射 vma 的結束地址 end 必須要與其后一個 vma 的起始地址重合,這樣,vma 才能和它的后一個 vma 進行合并,如果不重合,vma 則不能和后一個 vma 進行合并。注意:如果前后都不能合并,則需新建 vma 結構。
- 新映射 vma 需要與其要合并 vma 區域的 vm_flags 相同,否則不能合并。
- 如果兩個合并區域都是文件映射區,那么它們映射的文件必須是同一個。并且他們的文件映射偏移 vm_pgoff 必須是連續的。
- 如果兩個合并區域都是匿名映射區,那么兩個 vma 映射的匿名頁 anon_vma 必須是相同的。
- 合并區域的 numa policy 必須是相同的。
- 要合并的 prev 和 next 虛擬內存區域中,不能包含 close 操作,也就是說 vma->vm_ops 不能設置有 close 函數,如果虛擬內存區域操作支持 close,則不能合并,否則會導致現有虛擬內存區域 prev 和 next 的資源無法釋放。
2.16 vma_link 函數
/mm/mmap.c
static void vma_link(struct mm_struct *mm, struct vm_area_struct *vma,struct vm_area_struct *prev, struct rb_node **rb_link,struct rb_node *rb_parent)
{struct address_space *mapping = NULL; // 文件 page cacheif (vma->vm_file) {mapping = vma->vm_file->f_mapping; // 獲取映射文件的 page cachei_mmap_lock_write(mapping);}// 將 vma 插入到地址空間中的 vma 鏈表 mm_struct->mmap 以及紅黑樹 mm_struct->mm_rb 中__vma_link(mm, vma, prev, rb_link, rb_parent);__vma_link_file(vma); // 建立文件與 vma 的反向映射if (mapping)i_mmap_unlock_write(mapping);mm->map_count++; // map_count 表示進程地址空間中 vma 的個數validate_mm(mm);
}
vma_link 函數的主要作用如下:
- 調用 __vma_link 函數將 vma 插入到鏈表和紅黑樹中,其內部調用 __vma_link_list 函數將 vma 插入到 mm->mmap 鏈表中,調用 __vma_link_rb 函數將 vma 插入到 mm->rb 紅黑樹中。
- 調用 __vma_link_file 函數將 vma 添加到文件樹中;
總結
在一般情況下,調用 mmap 進行內存映射時,內核只是會在進程的虛擬內存空間中為該次映射分配一段虛擬內存,然后建立好這段虛擬內存與相關文件之間的映射關系,至此流程就結束,完成 mmap 內存映射的實現過程的第一階段。此時內核并不會為映射分配物理內存,物理內存的分配工作需要延后到這段虛擬內存被 CPU 訪問的時候,通過缺頁中斷來進入內核,分配物理內存,并在頁表中建立好映射關系。