作者簡介:
余華兵,2005年畢業(yè)于華中科技大學(xué)計算機(jī)學(xué)院,取得碩士學(xué)位。畢業(yè)后的十余年一直在網(wǎng)絡(luò)通信行業(yè)從事軟件設(shè)計和開發(fā)工作,研究方向包括IPv4協(xié)議棧、IPv6協(xié)議棧和Linux內(nèi)核。
3.4 內(nèi)存映射
內(nèi)存映射是在進(jìn)程的虛擬地址空間中創(chuàng)建一個映射,分為以下兩種。
(1)文件映射:文件支持的內(nèi)存映射,把文件的一個區(qū)間映射到進(jìn)程的虛擬地址空間,數(shù)據(jù)源是存儲設(shè)備上的文件。
(2)匿名映射:沒有文件支持的內(nèi)存映射,把物理內(nèi)存映射到進(jìn)程的虛擬地址空間,沒有數(shù)據(jù)源。
通常把文件映射的物理頁稱為文件頁,把匿名映射的物理頁稱為匿名頁。
根據(jù)修改是否對其他進(jìn)程可見和是否傳遞到底層文件,內(nèi)存映射分為共享映射和私有映射。
(1)共享映射:修改數(shù)據(jù)時映射相同區(qū)域的其他進(jìn)程可以看見,如果是文件支持的映射,修改會傳遞到底層文件。
(2)私有映射:第一次修改數(shù)據(jù)時會從數(shù)據(jù)源復(fù)制一個副本,然后修改副本,其他進(jìn)程看不見,不影響數(shù)據(jù)源。
兩個進(jìn)程可以使用共享的文件映射實現(xiàn)共享內(nèi)存。匿名映射通常是私有映射,共享的匿名映射只可能出現(xiàn)在父進(jìn)程和子進(jìn)程之間。
在進(jìn)程的虛擬地址空間中,代碼段和數(shù)據(jù)段是私有的文件映射,未初始化數(shù)據(jù)段、堆和棧是私有的匿名映射。
內(nèi)存映射的原理如下
(1)創(chuàng)建內(nèi)存映射的時候,在進(jìn)程的用戶虛擬地址空間中分配一個虛擬內(nèi)存區(qū)域。
(2)Linux 內(nèi)核采用延遲分配物理內(nèi)存的策略,在進(jìn)程第一次訪問虛擬頁的時候,產(chǎn)生缺頁異常。如果是文件映射,那么分配物理頁,把文件指定區(qū)間的數(shù)據(jù)讀到物理頁中,然后在頁表中把虛擬頁映射到物理頁;如果是匿名映射,那么分配物理頁,然后在頁表中把虛擬頁映射到物理頁。
內(nèi)存管理子系統(tǒng)提供了以下常用的系統(tǒng)調(diào)用。
(1)mmap()用來創(chuàng)建內(nèi)存映射。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
(2)mremap()用來擴(kuò)大或縮小已經(jīng)存在的內(nèi)存映射,可能同時移動。
void *mremap(void *old_address, size_t old_size, size_t new_size, int flags, ... /* void *new_address */);
(3)munmap()用來刪除內(nèi)存映射。
int munmap(void *addr, size_t length);
(4)brk()用來設(shè)置堆的上界。
int brk(void *addr);
(5)remap_file_pages()用來創(chuàng)建非線性的文件映射,即文件區(qū)間和虛擬地址空間之間的映射不是線性關(guān)系,現(xiàn)在被廢棄了。
(6)mprotect()用來設(shè)置虛擬內(nèi)存區(qū)域的訪問權(quán)限。
int mprotect(void *addr, size_t len, int prot);
(7)madvise()用來向內(nèi)核提出內(nèi)存使用的建議,應(yīng)用程序告訴內(nèi)核期望怎樣使用指定的虛擬內(nèi)存區(qū)域,以便內(nèi)核可以選擇合適的預(yù)讀和緩存技術(shù)。
int madvise(void *addr, size_t length, int advice);
在內(nèi)核空間中可以使用以下兩個函數(shù)。
(1)remap_pfn_range 把內(nèi)存的物理頁映射到進(jìn)程的虛擬地址空間,這個函數(shù)的用處是實現(xiàn)進(jìn)程和內(nèi)核共享內(nèi)存。
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned long pfn,unsigned long size, pgprot_t prot);
(2)io_remap_pfn_range 把外設(shè)寄存器的物理地址映射到進(jìn)程的虛擬地址空間,進(jìn)程可以直接訪問外設(shè)寄存器。
int io_remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot);
應(yīng)用程序通常使用 C 標(biāo)準(zhǔn)庫提供的函數(shù) malloc()申請內(nèi)存。glibc 庫的內(nèi)存分配器 ptmalloc使用 brk 或 mmap 向內(nèi)核以頁為單位申請?zhí)摂M內(nèi)存,然后把頁劃分成小內(nèi)存塊分配給應(yīng)用程序。默認(rèn)的閾值是 128KB,如果應(yīng)用程序申請的內(nèi)存長度小于閾值,ptmalloc 分配器使用 brk 向內(nèi)核申請?zhí)摂M內(nèi)存,否則 ptmalloc 分配器使用 mmap 向內(nèi)核申請?zhí)摂M內(nèi)存。
應(yīng)用程序可以直接使用 mmap 向內(nèi)核申請?zhí)摂M內(nèi)存。
1.系統(tǒng)調(diào)用 mmap()
系統(tǒng)調(diào)用 mmap()有以下用處。
(1)進(jìn)程創(chuàng)建匿名的內(nèi)存映射,把內(nèi)存的物理頁映射到進(jìn)程的虛擬地址空間。
(2)進(jìn)程把文件映射到進(jìn)程的虛擬地址空間,可以像訪問內(nèi)存一樣訪問文件,不需要調(diào)用系統(tǒng)調(diào)用read()和write()訪問文件,從而避免用戶模式和內(nèi)核模式之間的切換,提高讀寫文件的速度。
(3)兩個進(jìn)程針對同一個文件創(chuàng)建共享的內(nèi)存映射,實現(xiàn)共享內(nèi)存。
函數(shù)原型:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
參數(shù)如下。
(1)addr:起始虛擬地址。如果 addr 是 0,內(nèi)核選擇虛擬地址。如果 addr 不是 0,內(nèi)核把這個參數(shù)作為提示,在附近選擇虛擬地址。
(2)length:映射的長度,單位是字節(jié)。
(3)prot:保護(hù)位。
(4)flags:標(biāo)志。常用的標(biāo)志如下。
(5)fd:文件描述符。僅當(dāng)創(chuàng)建文件映射的時候,這個參數(shù)才有意義。如果是匿名映射,有些實現(xiàn)要求參數(shù) fd 是?1,可移植的應(yīng)用程序應(yīng)該保證參數(shù) fd 是?1。
(6)offset:偏移,單位是字節(jié),必須是頁長度的整數(shù)倍。僅當(dāng)創(chuàng)建文件映射的時候,這個參數(shù)才有意義。
返回值:
如果成功,返回起始虛擬地址,否則返回負(fù)的錯誤號。
2.系統(tǒng)調(diào)用 mprotect()
mprotect()用來設(shè)置虛擬內(nèi)存區(qū)域的訪問權(quán)限。
函數(shù)原型:
int mprotect(void *addr, size_t len, int prot);
參數(shù)如下。
(1)addr:起始虛擬地址,必須是頁長度的整數(shù)倍。
(2)len:虛擬內(nèi)存區(qū)域的長度,單位是字節(jié)。
(3)prot:保護(hù)位。
返回值:
如果成功,返回 0,否則返回負(fù)的錯誤號。
3.系統(tǒng)調(diào)用 madvise()
madvise()用來向內(nèi)核提出內(nèi)存使用的建議,應(yīng)用程序告訴內(nèi)核期望怎樣使用指定的虛擬內(nèi)存區(qū)域,以便內(nèi)核可以選擇合適的預(yù)讀和緩存技術(shù)。
函數(shù)原型:
int madvise(void *addr, size_t length, int advice);
參數(shù)如下。
(1)addr:起始虛擬地址,必須是頁長度的整數(shù)倍。
(2)length:虛擬內(nèi)存區(qū)域的長度,單位是字節(jié)。
(3)advice:建議。
POSIX 標(biāo)準(zhǔn)定義的建議值如下。
Linux 私有的建議值如下。
返回值:
如果成功,返回 0,否則返回負(fù)的錯誤號。
3.4.2 數(shù)據(jù)結(jié)構(gòu)
1.虛擬內(nèi)存區(qū)域
虛擬內(nèi)存區(qū)域是分配給進(jìn)程的一個虛擬地址范圍,內(nèi)核使用結(jié)構(gòu)體 vm_area_struct 描述虛擬內(nèi)存區(qū)域,主要成員如表 3.4 所示。
表 3.4 虛擬內(nèi)存區(qū)域的主要成員
文件映射的虛擬內(nèi)存區(qū)域如圖 3.9 所示。
圖3.9 文件映射的虛擬內(nèi)存區(qū)域?
(1)成員 vm_file 指向文件的一個打開實例(file)。索引節(jié)點代表一個文件,描述文件的屬性。
(2)成員 vm_pgoff 存放文件的以頁為單位的偏移。
(3)成員 vm_ops 指向虛擬內(nèi)存操作集合,創(chuàng)建文件映射的時候調(diào)用文件操作集合中的 mmap 方法(file->f_op->mmap)以注冊虛擬內(nèi)存操作集合。例如:假設(shè)文件屬于 EXT4文件系統(tǒng),文件操作集合中的 mmap 方法是函數(shù) ext4_file_mmap,該函數(shù)把虛擬內(nèi)存區(qū)域的成員 vm_ops 設(shè)置為 ext4_file_vm_ops。
共享匿名映射的虛擬內(nèi)存區(qū)域如圖 3.10 所示,共享匿名映射的實現(xiàn)原理和文件映射相同,區(qū)別是共享匿名映射關(guān)聯(lián)的文件是內(nèi)核創(chuàng)建的內(nèi)部文件。在內(nèi)存文件系統(tǒng) tmpfs 中創(chuàng)建一個名為“/dev/zero”的文件,名字沒有意義,創(chuàng)建兩個共享匿名映射就會創(chuàng)建兩個名為“/dev/zero”的文件,兩個文件是獨立的,毫無關(guān)系。
圖3.10 共享匿名映射的虛擬內(nèi)存區(qū)域
(1)成員 vm_file 指向文件的一個打開實例(file)。
(2)成員 vm_pgoff 存放文件的以頁為單位的偏移。
(3)成員 vm_ops 指向共享內(nèi)存的虛擬內(nèi)存操作集合 shmem_vm_ops。
私有匿名映射的虛擬內(nèi)存區(qū)域如圖 3.11 所示。
圖3.10 私有匿名映射的虛擬內(nèi)存區(qū)域
成員 vm_file 沒有意義,是空指針。
成員 vm_pgoff 沒有意義。
成員 vm_ops 是空指針。
(1)頁保護(hù)位(vm_area_struct.vm_page_prot):描述虛擬內(nèi)存區(qū)域的訪問權(quán)限。內(nèi)核定義了一個保護(hù)位映射數(shù)組,把 VM_READ、VM_WRITE、VM_EXEC 和VM_SHARED 這 4 個標(biāo)志轉(zhuǎn)換成保護(hù)位組合。
每種處理器架構(gòu)需要定義__P000 到__S111 的宏,P 代表私有(Private),S 代表共享(Shared),后面的 3 個數(shù)字分別表示可讀、可寫和可執(zhí)行,例如__P000 表示私有、不可讀、不可寫和不可執(zhí)行,__S111 表示共享、可讀、可寫和可執(zhí)行。
mm/mmap.c pgprot_t protection_map[16] = { __P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111, __S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111 }; pgprot_t vm_get_page_prot(unsigned long vm_flags) { return __pgprot(pgprot_val(protection_map[vm_flags & (VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)]) | pgprot_val(arch_vm_get_page_prot(vm_flags))); }
函數(shù) arch_vm_get_page_prot 由每種處理器架構(gòu)自定義,默認(rèn)的實現(xiàn)如下:
include/linux/mman.h
include/linux/mman.h #ifndef arch_vm_get_page_prot #define arch_vm_get_page_prot(vm_flags) __pgprot(0) #endif
(2)虛擬內(nèi)存區(qū)域標(biāo)志:結(jié)構(gòu)體 vm_area_struct 的成員 vm_flags 存放虛擬內(nèi)存區(qū)域的標(biāo)志,頭文件“include/linux/mm.h”定義了各種標(biāo)志,常用的標(biāo)志如下。
1)VM_READ、VM_WRITE、VM_EXEC 和 VM_SHARED 分別表示可讀、可寫、可執(zhí)行和可以被多個進(jìn)程共享。
2)VM_MAYREAD 表示允許設(shè)置 VM_READ,VM_MAYWRITE 表示允許設(shè)置VM_WRITE,VM_MAYEXEC 表示允許設(shè)置 VM_EXEC,VM_MAYSHARE 表示允許設(shè)置VM_SHARED。這 4 個標(biāo)志用來限制系統(tǒng)調(diào)用 mprotect 可以設(shè)置的訪問權(quán)限。
3)VM_GROWSDOWN 表示虛擬內(nèi)存區(qū)域可以向下(低的虛擬地址)擴(kuò)展,VM_GROWSUP 表示虛擬內(nèi)存區(qū)域可以向上(高的虛擬地址)擴(kuò)展。VM_STACK 表示虛擬內(nèi)存區(qū)域是棧,絕大多數(shù)處理器的棧是向下擴(kuò)展,VM_STACK 等價于 VM_GROWSDOWN;少數(shù)處理器(例如 PA-RISC 處理器)的棧是向上擴(kuò)展,VM_STACK 等價于 VM_GROWSUP。
4)VM_PFNMAP 表示頁幀號(Page Frame Number,PFN)映射,特殊映射不希望關(guān)聯(lián)頁描述符,直接使用頁幀號,可能是因為頁描述符不存在,也可能是因為不想使用頁描述符。
5)VM_MIXEDMAP 表示映射混合使用頁幀號和頁描述符。
6)VM_LOCKED 表示頁被鎖定在內(nèi)存中,不允許換出到交換區(qū)。
7)VM_SEQ_READ 表示進(jìn)程從頭到尾按順序讀一個文件,VM_RAND_READ 表示進(jìn)程隨機(jī)讀一個文件。這兩個標(biāo)志用來提示文件系統(tǒng),如果進(jìn)程按順序讀一個文件,文件系統(tǒng)可以預(yù)讀文件,提高性能。
8)VM_DONTCOPY 表示調(diào)用 fork 以創(chuàng)建子進(jìn)程時不把虛擬內(nèi)存區(qū)域復(fù)制給子進(jìn)程。
9)VM_DONTEXPAND 表示不允許使用 mremap()擴(kuò)大虛擬內(nèi)存區(qū)域。
10)VM_ACCOUNT 表示虛擬內(nèi)存區(qū)域需要記賬,判斷所有進(jìn)程申請的虛擬內(nèi)存的總和是否超過物理內(nèi)存容量。
11)VM_NORESERVE 表示不需要預(yù)留物理內(nèi)存。
12)VM_HUGETLB 表示虛擬內(nèi)存區(qū)域使用標(biāo)準(zhǔn)巨型頁。
13)VM_ARCH_1 和 VM_ARCH_2 由各種處理器架構(gòu)自定義。
14)VM_HUGEPAGE 表示虛擬內(nèi)存區(qū)域允許使用透明巨型頁,VM_NOHUGEPAGE表示虛擬內(nèi)存區(qū)域不允許使用透明巨型頁。
15)VM_MERGEABLE 表示 KSM(內(nèi)核相同頁合并,Kernel Samepage Merging)可以合并數(shù)據(jù)相同的頁。
(3)虛擬內(nèi)存操作集合(vm_operations_struct):定義了虛擬內(nèi)存區(qū)域的各種操作方法,其代碼如下。
include/linux/mm.h struct vm_operations_struct { void (*open)(struct vm_area_struct * area); void (*close)(struct vm_area_struct * area); int (*mremap)(struct vm_area_struct * area); int (*fault)(struct vm_fault *vmf); int (*huge_fault)(struct vm_fault *vmf, enum page_entry_size pe_size); void (*map_pages)(struct vm_fault *vmf, pgoff_t start_pgoff, pgoff_t end_pgoff); /* 通知以前的只讀頁即將變成可寫,* 如果返回一個錯誤,將會發(fā)送信號SIGBUS給進(jìn)程*/ int (*page_mkwrite)(struct vm_fault *vmf); /* 使用VM_PFNMAP或者VM_MIXEDMAP時調(diào)用,功能和page_mkwrite相同*/ int (*pfn_mkwrite)(struct vm_fault *vmf); … }
1)open 方法:在創(chuàng)建虛擬內(nèi)存區(qū)域時調(diào)用 open 方法,通常不使用,設(shè)置為空指針。
2)close 方法:在刪除虛擬內(nèi)存區(qū)域時調(diào)用 close 方法,通常不使用,設(shè)置為空指針。
3)mremap 方法:使用系統(tǒng)調(diào)用 mremap 移動虛擬內(nèi)存區(qū)域時調(diào)用 mremap 方法。
4)fault 方法:訪問文件映射的虛擬頁時,如果沒有映射到物理頁,生成缺頁異常,異常處理程序調(diào)用 fault 方法來把文件的數(shù)據(jù)讀到文件的頁緩存中。
5)huge_fault 方法:和 fault 方法類似,區(qū)別是 huge_fault 方法針對使用透明巨型頁的文件映射。
6)map_pages 方法:讀文件映射的虛擬頁時,如果沒有映射到物理頁,生成缺頁異常,異常處理程序除了讀入正在訪問的文件頁,還會預(yù)讀后續(xù)的文件頁,調(diào)用 map_pages 方法在文件的頁緩存中分配物理頁。
7)page_mkwrite 方法:第一次寫私有的文件映射時,生成頁錯誤異常,異常處理程序執(zhí)行寫時復(fù)制,調(diào)用 page_mkwrite 方法以通知文件系統(tǒng)頁即將變成可寫,以便文件系統(tǒng)檢查是否允許寫,或者等待頁進(jìn)入合適的狀態(tài)。
8)pfn_mkwrite 方法:和 page_mkwrite 方法類似,區(qū)別是 pfn_mkwrite 方法針對頁幀號映射和混合映射。
2.鏈表和樹
如圖 3.12 所示,進(jìn)程的虛擬內(nèi)存區(qū)域按兩種方法排序。
圖3.12 虛擬內(nèi)存區(qū)域的鏈表和樹
(1)雙向鏈表,mm_struct.mmap 指向第一個 vm_area_struct 實例。
(2)紅黑樹,mm_struct.mm_rb 指向紅黑樹的根。
虛擬內(nèi)存區(qū)域使用起始地址和結(jié)束地址描述,鏈表按起始地址遞增排序。紅黑樹是平衡的二叉查找樹,按起始地址排序,使用紅黑樹有以下好處。
1)在紅黑樹中查找一個虛擬內(nèi)存區(qū)域的速度快。
2)增加一個新的區(qū)域時,先在紅黑樹中找到剛好在新區(qū)域前面的區(qū)域,然后向鏈表和樹中插入新區(qū)域,可以避免掃描鏈表。
3.4.3 創(chuàng)建內(nèi)存映射
C 標(biāo)準(zhǔn)庫封裝了函數(shù) mmap 用來創(chuàng)建內(nèi)存映射,內(nèi)核提供了 POSIX 標(biāo)準(zhǔn)定義的系統(tǒng)調(diào)用 mmap:
asmlinkage long sys_mmap(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, off_t off);
Linux 內(nèi)核從 2.3.31 版本開始提供私有的系統(tǒng)調(diào)用 mmap2:
asmlinkage long sys_mmap2(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, off_t off);
兩個系統(tǒng)調(diào)用的區(qū)別是:mmap 指定的偏移的單位是字節(jié),而 mmap2 指定的偏移的單位是頁。有的處理器架構(gòu)實現(xiàn)了這兩個系統(tǒng)調(diào)用,有的處理器架構(gòu)只實現(xiàn)了其中一個系統(tǒng)調(diào)用,例如 ARM64 架構(gòu)只實現(xiàn)了系統(tǒng)調(diào)用 mmap。
系統(tǒng)調(diào)用 sys_mmap 的執(zhí)行流程如圖 3.13 所示。
(1)檢查偏移是不是頁的整數(shù)倍,如果偏移不是頁的整數(shù)倍,返回“-EINVAL”。
(2)如果偏移是頁的整數(shù)倍,那么把偏移轉(zhuǎn)換成以頁為單位的偏移,然后調(diào)用函數(shù)sys_mmap_pgoff。
圖3.13 系統(tǒng)調(diào)用sys_mmap的執(zhí)行流程
函數(shù) sys_mmap_pgoff 的執(zhí)行流程如下。
(1)如果是創(chuàng)建文件映射,根據(jù)文件描述符在進(jìn)程的打開文件表中找到 file 實例。
(2)如果是創(chuàng)建匿名巨型頁映射,在 hugetlbfs 文件系統(tǒng)中創(chuàng)建文件“anon_hugepage”,并且創(chuàng)建該文件的一個打開實例 file。
注意:文件名沒有實際意義,創(chuàng)建匿名巨型頁映射兩次,就會在 hugetlbfs 文件系統(tǒng)中創(chuàng)建兩個名為“anon_hugepage”的文件,這兩個文件沒有關(guān)聯(lián)。
(3)調(diào)用函數(shù) vm_mmap_pgoff 進(jìn)行處理。
函數(shù) vm_mmap_pgoff 的執(zhí)行流程如下。
(1)以寫者身份申請讀寫信號量 mm->mmap_sem。
(2)把創(chuàng)建內(nèi)存映射的主要工作委托給函數(shù) do_mmap。
(3)釋放讀寫信號量 mm->mmap_sem。
(4)如果調(diào)用者要求把頁鎖定在內(nèi)存中,或者要求填充頁表并且允許阻塞,那么調(diào)用函數(shù) mm_populate,分配物理頁,并且在頁表中把虛擬頁映射到物理頁。
常見的情況是:創(chuàng)建內(nèi)存映射的時候不分配物理頁,等到進(jìn)程第一次訪問虛擬頁的時候,生成頁錯誤異常,頁錯誤異常處理程序分配物理頁,在頁表中把虛擬頁映射到物理頁。
函數(shù) do_mmap 實現(xiàn)創(chuàng)建內(nèi)存映射的主要工作,執(zhí)行流程如圖 3.14 所示。
(1)調(diào)用函數(shù) get_unmapped_area,從進(jìn)程的虛擬地址空間分配一個虛擬地址范圍。函數(shù) get_unmapped_area 根據(jù)情況調(diào)用特定函數(shù)以分配虛擬地址范圍。
1)如果是創(chuàng)建文件映射或匿名巨型頁映射,那么調(diào)用 file->f_op->get_unmapped_area以分配虛擬地址范圍。
2)如果是創(chuàng)建共享的匿名映射,那么調(diào)用 shmem_get_unmapped_area 以分配虛擬地址范圍。
3)如果是創(chuàng)建私有的匿名映射,那么調(diào)用 mm->get_unmapped_area 以分配虛擬地址范圍。ARM64 架構(gòu)的內(nèi)核在裝載程序時,如果選擇傳統(tǒng)布局,函數(shù) arch_pick_mmap_layout把 mm->get_unmapped_area 設(shè)置為函數(shù) arch_get_unmapped_area。
圖3.14 函數(shù)do_mmap的執(zhí)行流程
(2)計算虛擬內(nèi)存標(biāo)志。
vm_flags |= calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) | mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
把系統(tǒng)調(diào)用中指定的保護(hù)位和標(biāo)志合并到一個標(biāo)志集合中,函數(shù) calc_vm_prot_bits把以“PROT_”開頭的保護(hù)位轉(zhuǎn)換成以“VM_”開頭的標(biāo)志,函數(shù) calc_vm_flag_bits 把以“MAP_”開頭的標(biāo)志轉(zhuǎn)換成以“VM_”開頭的標(biāo)志。
mm->def_flags 是默認(rèn)的虛擬內(nèi)存標(biāo)志:進(jìn)程默認(rèn)的虛擬內(nèi)存標(biāo)志是 VM_NOHUGEPAGE,即不使用透明巨型頁;內(nèi)核線程默認(rèn)的虛擬內(nèi)存標(biāo)志是 0。
VM_MAYREAD 表示允許設(shè)置標(biāo)志 VM_READ,VM_MAYWRITE 表示允許設(shè)置標(biāo)志VM_WRITE,VM_MAYEXEC 表示允許設(shè)置標(biāo)志 VM_EXEC。這 3 個標(biāo)志是系統(tǒng)調(diào)用 mprotect所需要的。
(3)調(diào)用函數(shù) mmap_region 以創(chuàng)建虛擬內(nèi)存區(qū)域。
函數(shù) mmap_region 負(fù)責(zé)創(chuàng)建虛擬內(nèi)存區(qū)域,執(zhí)行流程如下。
(1)調(diào)用函數(shù) may_expand_vm 以檢查進(jìn)程申請的虛擬內(nèi)存是否超過限制。
首先檢查(進(jìn)程的虛擬內(nèi)存總數(shù) + 申請的頁數(shù))是否超過地址空間限制:mm->total_vm +npages > rlimit(RLIMIT_AS) >> PAGE_SHIFT。
如果是私有的可寫映射,并且不是棧,那么檢查(進(jìn)程數(shù)據(jù)的虛擬內(nèi)存總數(shù) + 申請的頁數(shù))是否超過最大數(shù)據(jù)長度:mm->data_vm + npages > rlimit(RLIMIT_DATA) >> PAGE_SHIFT。
(2)如果是固定映射,調(diào)用者強(qiáng)制指定虛擬地址范圍,可能和舊的虛擬內(nèi)存區(qū)域重疊,那么需要從舊的虛擬內(nèi)存區(qū)域刪除重疊的部分。
(3)如果是私有的可寫映射,檢查所有進(jìn)程申請的虛擬內(nèi)存的總和是否超過物理內(nèi)存的容量。
/** 如果是需要記賬的映射,那么檢查所有進(jìn)程申請的虛擬內(nèi)存的總和是否超過物理內(nèi)存的容量。* 需要記賬的映射具備以下3個條件。* (1)私有的可寫映射。* (2)不是標(biāo)準(zhǔn)巨型頁(因為標(biāo)準(zhǔn)巨型頁單獨記賬)。 * (3)需要預(yù)留物理內(nèi)存(即未設(shè)置VM_NORESERVE)。*/ if (accountable_mapping(file, vm_flags)) { charged = len >> PAGE_SHIFT; /* 根據(jù)虛擬內(nèi)存過量提交的策略,判斷物理內(nèi)存是否足夠。*/ if (security_vm_enough_memory_mm(mm, charged)) return -ENOMEM; vm_flags |= VM_ACCOUNT; }
(4)如果可以和已有的虛擬內(nèi)存區(qū)域合并,那么調(diào)用函數(shù) vma_merge,和已有的虛擬內(nèi)存區(qū)域合并。
(5)如果不能和已有的虛擬內(nèi)存區(qū)域合并,處理如下。
1)創(chuàng)建新的虛擬內(nèi)存區(qū)域。
2)如果是文件映射,那么調(diào)用文件的文件操作集合中的 mmap 方法(file->f_op->mmap),mmap 方法的主要功能是設(shè)置虛擬內(nèi)存區(qū)域的虛擬內(nèi)存操作集合(vm_area_struct.vm_ops),其中的 fault 方法很重要:第一次訪問虛擬頁的時候,觸發(fā)頁錯誤異常,異常處理程序?qū)⒄{(diào)用虛擬內(nèi)存操作集合中的 fault 方法以把文件的數(shù)據(jù)讀到內(nèi)存。
文件的文件操作集合是在打開文件的時候設(shè)置的,和文件所屬的文件系統(tǒng)相關(guān)。
很多文件系統(tǒng)把文件操作集合中的 mmap 方法設(shè)置為公共函數(shù) generic_file_mmap,函數(shù) generic_file_mmap 的主要功能是把虛擬內(nèi)存區(qū)域的虛擬內(nèi)存操作集合設(shè)置為 generic_file_vm_ops,其中 fault 方法是函數(shù) filemap_fault。
EXT4 文件系統(tǒng)把文件操作集合中的 mmap 方法設(shè)置為函數(shù) ext4_file_mmap,函數(shù) ext4_file_mmap 的主要功能是把虛擬內(nèi)存區(qū)域的虛擬內(nèi)存操作集合設(shè)置為 ext4_file_vm_ops,其中 fault 方法是函數(shù) ext4_filemap_fault。
3)如果是共享的匿名映射,那么在內(nèi)存文件系統(tǒng) tmpfs 中創(chuàng)建一個名為“/dev/zero”的文件,并且創(chuàng)建文件的一個打開實例 file,虛擬內(nèi)存區(qū)域的成員 vm_file 指向這個打開實例,把虛擬內(nèi)存操作集合設(shè)置為 shmem_vm_ops。如果沒有開啟共享內(nèi)存的配置宏 CONFIG_SHMEM,shmem_vm_ops 等價于 generic_file_vm_ops。
4)調(diào)用函數(shù) vma_link,把虛擬內(nèi)存區(qū)域添加到鏈表和紅黑樹中。如果虛擬內(nèi)存區(qū)域關(guān)聯(lián)文件,那么把虛擬內(nèi)存區(qū)域添加到文件的區(qū)間樹中,文件的區(qū)間樹用來跟蹤文件被映射到哪些虛擬內(nèi)存區(qū)域。
5)調(diào)用函數(shù) vma_set_page_prot,根據(jù)虛擬內(nèi)存標(biāo)志(vma->vm_flags)計算頁保護(hù)位(vma-> vm_page_prot),如果共享的可寫映射想要把頁標(biāo)記為只讀,目的是跟蹤寫事件,那么從頁保護(hù)位刪除可寫位。
3.4.4 虛擬內(nèi)存過量提交策略
虛擬內(nèi)存過量提交,是指所有進(jìn)程提交的虛擬內(nèi)存的總和超過物理內(nèi)存的容量,內(nèi)存管理子系統(tǒng)支持 3 種虛擬內(nèi)存過量提交策略。
(1)OVERCOMMIT_GUESS(0):猜測,估算可用內(nèi)存的數(shù)量,因為沒法準(zhǔn)確計算可用內(nèi)存的數(shù)量,所以說是猜測。
(2)OVERCOMMIT_ALWAYS(1):總是允許過量提交。
(3)OVERCOMMIT_NEVER(2):不允許過量提交。
默認(rèn)策略是猜測,用戶可以通過文件“/proc/sys/vm/overcommit_memory”修改策略。
在創(chuàng)建新的內(nèi)存映射時,調(diào)用函數(shù)__vm_enough_memory 根據(jù)虛擬內(nèi)存過量提交策略判斷內(nèi)存是否足夠,主要代碼如下:
mm/util.c1 int __vm_enough_memory(struct mm_struct *mm, long pages, int cap_sys_admin) 2 { 3 long free, allowed, reserve; 4 … 5 if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS) 6 return 0; 7 8 if (sysctl_overcommit_memory == OVERCOMMIT_GUESS) { 9 free = global_page_state(NR_FREE_PAGES); 10 free += global_node_page_state(NR_FILE_PAGES); 11 12 free -= global_node_page_state(NR_SHMEM); 13 14 free += get_nr_swap_pages(); 15 16 free += global_page_state(NR_SLAB_RECLAIMABLE); 137 第 3 章 內(nèi)存管理 if (free <= totalreserve_pages) 19 goto error; 20 else 21 free -= totalreserve_pages; 22 23 if (!cap_sys_admin) 24 free -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10); 25 26 if (free > pages) 27 return 0; 28 29 goto error; 30 } 31 32 allowed = vm_commit_limit(); 33 34 if (!cap_sys_admin) 35 allowed -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10); 36 37 if (mm) { 38 reserve = sysctl_user_reserve_kbytes >> (PAGE_SHIFT - 10); 39 allowed -= min_t(long, mm->total_vm / 32, reserve); 40 } 41 42 if (percpu_counter_read_positive(&vm_committed_as) < allowed) 43 return 0; 44 error: 45 vm_unacct_memory(pages); 46 47 return -ENOMEM; 48 }
第 5 行代碼,如果使用總是允許過量提交的策略,那么允許創(chuàng)建新的內(nèi)存映射。
第 8 行代碼,如果使用猜測的過量提交策略,那么估算可用內(nèi)存的數(shù)量,處理如下。
1)第 9 行和第 10 行代碼,空閑頁加上文件頁,文件頁有后備存儲設(shè)備支持,可以回收。
2)第 12 行代碼,共享內(nèi)存頁不應(yīng)該算作空閑頁,它們不能被釋放,只能換出到交換區(qū)。
3)第 14 行代碼,加上交換區(qū)的空閑頁數(shù)。
4)第 16 行代碼,加上可回收的內(nèi)存緩存頁。使用 SLAB_RECLAIM_ACCOUNT 標(biāo)志創(chuàng)建的內(nèi)存緩存,宣稱可回收,dentry 和 inode 緩存應(yīng)該屬于這種情況。
5)第 21 行代碼,減去保留的頁數(shù)。
6)第 23 行和第 24 行代碼,如果進(jìn)程沒有系統(tǒng)管理權(quán)限,那么減去為根用戶保留的頁數(shù)。
7)第 26 行和第 27 行代碼,如果可用內(nèi)存的頁數(shù)大于申請的頁數(shù),那么允許創(chuàng)建新的內(nèi)存映射。
如果使用不允許過量提交的策略,那么處理如下。
1)第 32 行代碼,計算提交內(nèi)存的上限。有兩個控制參數(shù):sysctl_overcommit_kbytes是字節(jié)數(shù),sysctl_overcommit_ratio 是比例值,sysctl_overcommit_kbytes 的默認(rèn)值是 0,sysctl_overcommit_ratio 的默認(rèn)值是 50。如果 sysctl_overcommit_kbytes 不是 0,那么上限等于“sysctl_overcommit_kbytes + 交換區(qū)的空閑頁數(shù)”,否則上限等于“(物理內(nèi)存容量 ? 巨型頁總數(shù))* sysctl_overcommit_ratio/100 + 交換區(qū)的空閑頁數(shù)”。
2)第34 行和第35 行代碼,如果進(jìn)程沒有系統(tǒng)管理權(quán)限,那么需要為根用戶保留一部分內(nèi)存。
3)第 37~40 行代碼,為了防止一個用戶啟動一個消耗內(nèi)存大的進(jìn)程,保留一部分內(nèi)存:取“進(jìn)程虛擬內(nèi)存長度的 1/32”和“用戶保留的頁數(shù)”的較小值。
4)第 42 行和第 43 行代碼,vm_committed_as 是所有進(jìn)程提交的虛擬內(nèi)存的總和,如果它小于 allowed,那么允許創(chuàng)建新的內(nèi)存映射。
3.4.5 刪除內(nèi)存映射
系統(tǒng)調(diào)用 munmap 用來刪除內(nèi)存映射,它有兩個參數(shù):起始地址和長度。
系統(tǒng)調(diào)用 munmap 的執(zhí)行流程如圖 3.15 所示,它把主要工作委托給源文件“mm/mmap.c”中的函數(shù) do_munmap。
圖3.15 系統(tǒng)調(diào)用munmap的執(zhí)行流程
(1)根據(jù)起始地址找到要刪除的第一個虛擬內(nèi)存區(qū)域 vma。
(2)如果只刪除虛擬內(nèi)存區(qū)域 vma 的一部分,那么分裂虛擬內(nèi)存區(qū)域 vma。
(3)根據(jù)結(jié)束地址找到要刪除的最后一個虛擬內(nèi)存區(qū)域 last。
(4)如果只刪除虛擬內(nèi)存區(qū)域 last 的一部分,那么分裂虛擬內(nèi)存區(qū)域 last。
(5)針對所有刪除目標(biāo),如果虛擬內(nèi)存區(qū)域被鎖定在內(nèi)存中(不允許換出到交換區(qū)),那么調(diào)用函數(shù) munlock_vma_pages_all 以解除鎖定。
(6)調(diào)用函數(shù) detach_vmas_to_be_unmapped,把所有刪除目標(biāo)從進(jìn)程的虛擬內(nèi)存區(qū)域鏈表和樹中刪除,單獨組成一條臨時的鏈表。
(7)調(diào)用函數(shù) unmap_region,針對所有刪除目標(biāo),在進(jìn)程的頁表中刪除映射,并且從處理器的頁表緩存中刪除映射。
(8)調(diào)用函數(shù) arch_unmap 執(zhí)行處理器架構(gòu)特定的處理。各種處理器架構(gòu)自定義函數(shù)arch_unmap,它默認(rèn)是一個空函數(shù)。
(9)調(diào)用函數(shù) remove_vma_list 刪除所有目標(biāo)。
編輯:黃飛
?
評論