作者簡(jiǎn)介:
余華兵,2005年畢業(yè)于華中科技大學(xué)計(jì)算機(jī)學(xué)院,取得碩士學(xué)位。畢業(yè)后的十余年一直在網(wǎng)絡(luò)通信行業(yè)從事軟件設(shè)計(jì)和開發(fā)工作,研究方向包括IPv4協(xié)議棧、IPv6協(xié)議棧和Linux內(nèi)核。
3.4 內(nèi)存映射
內(nèi)存映射是在進(jìn)程的虛擬地址空間中創(chuàng)建一個(gè)映射,分為以下兩種。
(1)文件映射:文件支持的內(nèi)存映射,把文件的一個(gè)區(qū)間映射到進(jìn)程的虛擬地址空間,數(shù)據(jù)源是存儲(chǔ)設(shè)備上的文件。
(2)匿名映射:沒有文件支持的內(nèi)存映射,把物理內(nèi)存映射到進(jìn)程的虛擬地址空間,沒有數(shù)據(jù)源。
通常把文件映射的物理頁稱為文件頁,把匿名映射的物理頁稱為匿名頁。
根據(jù)修改是否對(duì)其他進(jìn)程可見和是否傳遞到底層文件,內(nèi)存映射分為共享映射和私有映射。
(1)共享映射:修改數(shù)據(jù)時(shí)映射相同區(qū)域的其他進(jìn)程可以看見,如果是文件支持的映射,修改會(huì)傳遞到底層文件。
(2)私有映射:第一次修改數(shù)據(jù)時(shí)會(huì)從數(shù)據(jù)源復(fù)制一個(gè)副本,然后修改副本,其他進(jìn)程看不見,不影響數(shù)據(jù)源。
兩個(gè)進(jìn)程可以使用共享的文件映射實(shí)現(xiàn)共享內(nèi)存。匿名映射通常是私有映射,共享的匿名映射只可能出現(xiàn)在父進(jìn)程和子進(jìn)程之間。
在進(jìn)程的虛擬地址空間中,代碼段和數(shù)據(jù)段是私有的文件映射,未初始化數(shù)據(jù)段、堆和棧是私有的匿名映射。
內(nèi)存映射的原理如下
(1)創(chuàng)建內(nèi)存映射的時(shí)候,在進(jìn)程的用戶虛擬地址空間中分配一個(gè)虛擬內(nèi)存區(qū)域。
(2)Linux 內(nèi)核采用延遲分配物理內(nèi)存的策略,在進(jìn)程第一次訪問虛擬頁的時(shí)候,產(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)存映射,可能同時(shí)移動(dòng)。
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)核空間中可以使用以下兩個(gè)函數(shù)。
(1)remap_pfn_range 把內(nèi)存的物理頁映射到進(jìn)程的虛擬地址空間,這個(gè)函數(shù)的用處是實(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)庫(kù)提供的函數(shù) malloc()申請(qǐng)內(nèi)存。glibc 庫(kù)的內(nèi)存分配器 ptmalloc使用 brk 或 mmap 向內(nèi)核以頁為單位申請(qǐng)?zhí)摂M內(nèi)存,然后把頁劃分成小內(nèi)存塊分配給應(yīng)用程序。默認(rèn)的閾值是 128KB,如果應(yīng)用程序申請(qǐng)的內(nèi)存長(zhǎng)度小于閾值,ptmalloc 分配器使用 brk 向內(nèi)核申請(qǐng)?zhí)摂M內(nèi)存,否則 ptmalloc 分配器使用 mmap 向內(nèi)核申請(qǐng)?zhí)摂M內(nèi)存。
應(yīng)用程序可以直接使用 mmap 向內(nèi)核申請(qǐng)?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)兩個(gè)進(jìn)程針對(duì)同一個(gè)文件創(chuàng)建共享的內(nèi)存映射,實(shí)現(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)核把這個(gè)參數(shù)作為提示,在附近選擇虛擬地址。
(2)length:映射的長(zhǎng)度,單位是字節(jié)。
(3)prot:保護(hù)位。
(4)flags:標(biāo)志。常用的標(biāo)志如下。
(5)fd:文件描述符。僅當(dāng)創(chuàng)建文件映射的時(shí)候,這個(gè)參數(shù)才有意義。如果是匿名映射,有些實(shí)現(xiàn)要求參數(shù) fd 是?1,可移植的應(yīng)用程序應(yīng)該保證參數(shù) fd 是?1。
(6)offset:偏移,單位是字節(jié),必須是頁長(zhǎng)度的整數(shù)倍。僅當(dāng)創(chuàng)建文件映射的時(shí)候,這個(gè)參數(shù)才有意義。
返回值:
如果成功,返回起始虛擬地址,否則返回負(fù)的錯(cuò)誤號(hào)。
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:起始虛擬地址,必須是頁長(zhǎng)度的整數(shù)倍。
(2)len:虛擬內(nèi)存區(qū)域的長(zhǎng)度,單位是字節(jié)。
(3)prot:保護(hù)位。
返回值:
如果成功,返回 0,否則返回負(fù)的錯(cuò)誤號(hào)。
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:起始虛擬地址,必須是頁長(zhǎng)度的整數(shù)倍。
(2)length:虛擬內(nèi)存區(qū)域的長(zhǎng)度,單位是字節(jié)。
(3)advice:建議。
POSIX 標(biāo)準(zhǔn)定義的建議值如下。
Linux 私有的建議值如下。
返回值:
如果成功,返回 0,否則返回負(fù)的錯(cuò)誤號(hào)。
3.4.2 數(shù)據(jù)結(jié)構(gòu)
1.虛擬內(nèi)存區(qū)域
虛擬內(nèi)存區(qū)域是分配給進(jìn)程的一個(gè)虛擬地址范圍,內(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 指向文件的一個(gè)打開實(shí)例(file)。索引節(jié)點(diǎn)代表一個(gè)文件,描述文件的屬性。
(2)成員 vm_pgoff 存放文件的以頁為單位的偏移。
(3)成員 vm_ops 指向虛擬內(nèi)存操作集合,創(chuàng)建文件映射的時(shí)候調(diào)用文件操作集合中的 mmap 方法(file->f_op->mmap)以注冊(cè)虛擬內(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 所示,共享匿名映射的實(shí)現(xiàn)原理和文件映射相同,區(qū)別是共享匿名映射關(guān)聯(lián)的文件是內(nèi)核創(chuàng)建的內(nèi)部文件。在內(nèi)存文件系統(tǒng) tmpfs 中創(chuàng)建一個(gè)名為“/dev/zero”的文件,名字沒有意義,創(chuàng)建兩個(gè)共享匿名映射就會(huì)創(chuàng)建兩個(gè)名為“/dev/zero”的文件,兩個(gè)文件是獨(dú)立的,毫無關(guān)系。
圖3.10 共享匿名映射的虛擬內(nèi)存區(qū)域
(1)成員 vm_file 指向文件的一個(gè)打開實(shí)例(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)核定義了一個(gè)保護(hù)位映射數(shù)組,把 VM_READ、VM_WRITE、VM_EXEC 和VM_SHARED 這 4 個(gè)標(biāo)志轉(zhuǎn)換成保護(hù)位組合。
每種處理器架構(gòu)需要定義__P000 到__S111 的宏,P 代表私有(Private),S 代表共享(Shared),后面的 3 個(gè)數(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)的實(shí)現(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í)行和可以被多個(gè)進(jìn)程共享。
2)VM_MAYREAD 表示允許設(shè)置 VM_READ,VM_MAYWRITE 表示允許設(shè)置VM_WRITE,VM_MAYEXEC 表示允許設(shè)置 VM_EXEC,VM_MAYSHARE 表示允許設(shè)置VM_SHARED。這 4 個(gè)標(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 等價(jià)于 VM_GROWSDOWN;少數(shù)處理器(例如 PA-RISC 處理器)的棧是向上擴(kuò)展,VM_STACK 等價(jià)于 VM_GROWSUP。
4)VM_PFNMAP 表示頁幀號(hào)(Page Frame Number,PFN)映射,特殊映射不希望關(guān)聯(lián)頁描述符,直接使用頁幀號(hào),可能是因?yàn)轫撁枋龇淮嬖?,也可能是因?yàn)椴幌胧褂庙撁枋龇?/p>
5)VM_MIXEDMAP 表示映射混合使用頁幀號(hào)和頁描述符。
6)VM_LOCKED 表示頁被鎖定在內(nèi)存中,不允許換出到交換區(qū)。
7)VM_SEQ_READ 表示進(jìn)程從頭到尾按順序讀一個(gè)文件,VM_RAND_READ 表示進(jìn)程隨機(jī)讀一個(gè)文件。這兩個(gè)標(biāo)志用來提示文件系統(tǒng),如果進(jìn)程按順序讀一個(gè)文件,文件系統(tǒng)可以預(yù)讀文件,提高性能。
8)VM_DONTCOPY 表示調(diào)用 fork 以創(chuàng)建子進(jìn)程時(shí)不把虛擬內(nèi)存區(qū)域復(fù)制給子進(jìn)程。
9)VM_DONTEXPAND 表示不允許使用 mremap()擴(kuò)大虛擬內(nèi)存區(qū)域。
10)VM_ACCOUNT 表示虛擬內(nèi)存區(qū)域需要記賬,判斷所有進(jìn)程申請(qǐng)的虛擬內(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); /* 通知以前的只讀頁即將變成可寫,* 如果返回一個(gè)錯(cuò)誤,將會(huì)發(fā)送信號(hào)SIGBUS給進(jìn)程*/ int (*page_mkwrite)(struct vm_fault *vmf); /* 使用VM_PFNMAP或者VM_MIXEDMAP時(shí)調(diào)用,功能和page_mkwrite相同*/ int (*pfn_mkwrite)(struct vm_fault *vmf); … }
1)open 方法:在創(chuàng)建虛擬內(nèi)存區(qū)域時(shí)調(diào)用 open 方法,通常不使用,設(shè)置為空指針。
2)close 方法:在刪除虛擬內(nèi)存區(qū)域時(shí)調(diào)用 close 方法,通常不使用,設(shè)置為空指針。
3)mremap 方法:使用系統(tǒng)調(diào)用 mremap 移動(dòng)虛擬內(nèi)存區(qū)域時(shí)調(diào)用 mremap 方法。
4)fault 方法:訪問文件映射的虛擬頁時(shí),如果沒有映射到物理頁,生成缺頁異常,異常處理程序調(diào)用 fault 方法來把文件的數(shù)據(jù)讀到文件的頁緩存中。
5)huge_fault 方法:和 fault 方法類似,區(qū)別是 huge_fault 方法針對(duì)使用透明巨型頁的文件映射。
6)map_pages 方法:讀文件映射的虛擬頁時(shí),如果沒有映射到物理頁,生成缺頁異常,異常處理程序除了讀入正在訪問的文件頁,還會(huì)預(yù)讀后續(xù)的文件頁,調(diào)用 map_pages 方法在文件的頁緩存中分配物理頁。
7)page_mkwrite 方法:第一次寫私有的文件映射時(shí),生成頁錯(cuò)誤異常,異常處理程序執(zhí)行寫時(shí)復(fù)制,調(diào)用 page_mkwrite 方法以通知文件系統(tǒng)頁即將變成可寫,以便文件系統(tǒng)檢查是否允許寫,或者等待頁進(jìn)入合適的狀態(tài)。
8)pfn_mkwrite 方法:和 page_mkwrite 方法類似,區(qū)別是 pfn_mkwrite 方法針對(duì)頁幀號(hào)映射和混合映射。
2.鏈表和樹
如圖 3.12 所示,進(jìn)程的虛擬內(nèi)存區(qū)域按兩種方法排序。
圖3.12 虛擬內(nèi)存區(qū)域的鏈表和樹
(1)雙向鏈表,mm_struct.mmap 指向第一個(gè) vm_area_struct 實(shí)例。
(2)紅黑樹,mm_struct.mm_rb 指向紅黑樹的根。
虛擬內(nèi)存區(qū)域使用起始地址和結(jié)束地址描述,鏈表按起始地址遞增排序。紅黑樹是平衡的二叉查找樹,按起始地址排序,使用紅黑樹有以下好處。
1)在紅黑樹中查找一個(gè)虛擬內(nèi)存區(qū)域的速度快。
2)增加一個(gè)新的區(qū)域時(shí),先在紅黑樹中找到剛好在新區(qū)域前面的區(qū)域,然后向鏈表和樹中插入新區(qū)域,可以避免掃描鏈表。
3.4.3 創(chuàng)建內(nèi)存映射
C 標(biāo)準(zhǔn)庫(kù)封裝了函數(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);
兩個(gè)系統(tǒng)調(diào)用的區(qū)別是:mmap 指定的偏移的單位是字節(jié),而 mmap2 指定的偏移的單位是頁。有的處理器架構(gòu)實(shí)現(xiàn)了這兩個(gè)系統(tǒng)調(diào)用,有的處理器架構(gòu)只實(shí)現(xiàn)了其中一個(gè)系統(tǒng)調(diào)用,例如 ARM64 架構(gòu)只實(shí)現(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 實(shí)例。
(2)如果是創(chuàng)建匿名巨型頁映射,在 hugetlbfs 文件系統(tǒng)中創(chuàng)建文件“anon_hugepage”,并且創(chuàng)建該文件的一個(gè)打開實(shí)例 file。
注意:文件名沒有實(shí)際意義,創(chuàng)建匿名巨型頁映射兩次,就會(huì)在 hugetlbfs 文件系統(tǒng)中創(chuàng)建兩個(gè)名為“anon_hugepage”的文件,這兩個(gè)文件沒有關(guān)聯(lián)。
(3)調(diào)用函數(shù) vm_mmap_pgoff 進(jìn)行處理。
函數(shù) vm_mmap_pgoff 的執(zhí)行流程如下。
(1)以寫者身份申請(qǐng)讀寫信號(hào)量 mm->mmap_sem。
(2)把創(chuàng)建內(nèi)存映射的主要工作委托給函數(shù) do_mmap。
(3)釋放讀寫信號(hào)量 mm->mmap_sem。
(4)如果調(diào)用者要求把頁鎖定在內(nèi)存中,或者要求填充頁表并且允許阻塞,那么調(diào)用函數(shù) mm_populate,分配物理頁,并且在頁表中把虛擬頁映射到物理頁。
常見的情況是:創(chuàng)建內(nèi)存映射的時(shí)候不分配物理頁,等到進(jìn)程第一次訪問虛擬頁的時(shí)候,生成頁錯(cuò)誤異常,頁錯(cuò)誤異常處理程序分配物理頁,在頁表中把虛擬頁映射到物理頁。
函數(shù) do_mmap 實(shí)現(xiàn)創(chuàng)建內(nèi)存映射的主要工作,執(zhí)行流程如圖 3.14 所示。
(1)調(diào)用函數(shù) get_unmapped_area,從進(jìn)程的虛擬地址空間分配一個(gè)虛擬地址范圍。函數(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)核在裝載程序時(shí),如果選擇傳統(tǒng)布局,函數(shù) arch_pick_mmap_layout把 mm->get_unmapped_area 設(shè)置為函數(shù) arch_get_unmapped_area。
圖3.14 函數(shù)do_mmap的執(zhí)行流程
(2)計(jì)算虛擬內(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)志合并到一個(gè)標(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 個(gè)標(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)程申請(qǐng)的虛擬內(nèi)存是否超過限制。
首先檢查(進(jìn)程的虛擬內(nèi)存總數(shù) + 申請(qǐng)的頁數(shù))是否超過地址空間限制:mm->total_vm +npages > rlimit(RLIMIT_AS) >> PAGE_SHIFT。
如果是私有的可寫映射,并且不是棧,那么檢查(進(jìn)程數(shù)據(jù)的虛擬內(nèi)存總數(shù) + 申請(qǐng)的頁數(shù))是否超過最大數(shù)據(jù)長(zhǎng)度:mm->data_vm + npages > rlimit(RLIMIT_DATA) >> PAGE_SHIFT。
(2)如果是固定映射,調(diào)用者強(qiáng)制指定虛擬地址范圍,可能和舊的虛擬內(nèi)存區(qū)域重疊,那么需要從舊的虛擬內(nèi)存區(qū)域刪除重疊的部分。
(3)如果是私有的可寫映射,檢查所有進(jìn)程申請(qǐng)的虛擬內(nèi)存的總和是否超過物理內(nèi)存的容量。
/** 如果是需要記賬的映射,那么檢查所有進(jìn)程申請(qǐng)的虛擬內(nèi)存的總和是否超過物理內(nèi)存的容量。* 需要記賬的映射具備以下3個(gè)條件。* (1)私有的可寫映射。* (2)不是標(biāo)準(zhǔn)巨型頁(因?yàn)闃?biāo)準(zhǔn)巨型頁單獨(dú)記賬)。 * (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 方法很重要:第一次訪問虛擬頁的時(shí)候,觸發(fā)頁錯(cuò)誤異常,異常處理程序?qū)⒄{(diào)用虛擬內(nèi)存操作集合中的 fault 方法以把文件的數(shù)據(jù)讀到內(nèi)存。
文件的文件操作集合是在打開文件的時(shí)候設(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)建一個(gè)名為“/dev/zero”的文件,并且創(chuàng)建文件的一個(gè)打開實(shí)例 file,虛擬內(nèi)存區(qū)域的成員 vm_file 指向這個(gè)打開實(shí)例,把虛擬內(nèi)存操作集合設(shè)置為 shmem_vm_ops。如果沒有開啟共享內(nèi)存的配置宏 CONFIG_SHMEM,shmem_vm_ops 等價(jià)于 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)計(jì)算頁保護(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):猜測(cè),估算可用內(nèi)存的數(shù)量,因?yàn)闆]法準(zhǔn)確計(jì)算可用內(nèi)存的數(shù)量,所以說是猜測(cè)。
(2)OVERCOMMIT_ALWAYS(1):總是允許過量提交。
(3)OVERCOMMIT_NEVER(2):不允許過量提交。
默認(rèn)策略是猜測(cè),用戶可以通過文件“/proc/sys/vm/overcommit_memory”修改策略。
在創(chuàng)建新的內(nèi)存映射時(shí),調(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 行代碼,如果使用猜測(cè)的過量提交策略,那么估算可用內(nèi)存的數(shù)量,處理如下。
1)第 9 行和第 10 行代碼,空閑頁加上文件頁,文件頁有后備存儲(chǔ)設(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ù)大于申請(qǐng)的頁數(shù),那么允許創(chuàng)建新的內(nèi)存映射。
如果使用不允許過量提交的策略,那么處理如下。
1)第 32 行代碼,計(jì)算提交內(nèi)存的上限。有兩個(gè)控制參數(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 行代碼,為了防止一個(gè)用戶啟動(dòng)一個(gè)消耗內(nèi)存大的進(jìn)程,保留一部分內(nèi)存:取“進(jìn)程虛擬內(nèi)存長(zhǎng)度的 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)存映射,它有兩個(gè)參數(shù):起始地址和長(zhǎng)度。
系統(tǒng)調(diào)用 munmap 的執(zhí)行流程如圖 3.15 所示,它把主要工作委托給源文件“mm/mmap.c”中的函數(shù) do_munmap。
圖3.15 系統(tǒng)調(diào)用munmap的執(zhí)行流程
(1)根據(jù)起始地址找到要?jiǎng)h除的第一個(gè)虛擬內(nèi)存區(qū)域 vma。
(2)如果只刪除虛擬內(nèi)存區(qū)域 vma 的一部分,那么分裂虛擬內(nèi)存區(qū)域 vma。
(3)根據(jù)結(jié)束地址找到要?jiǎng)h除的最后一個(gè)虛擬內(nèi)存區(qū)域 last。
(4)如果只刪除虛擬內(nèi)存區(qū)域 last 的一部分,那么分裂虛擬內(nèi)存區(qū)域 last。
(5)針對(duì)所有刪除目標(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ū)域鏈表和樹中刪除,單獨(dú)組成一條臨時(shí)的鏈表。
(7)調(diào)用函數(shù) unmap_region,針對(duì)所有刪除目標(biāo),在進(jìn)程的頁表中刪除映射,并且從處理器的頁表緩存中刪除映射。
(8)調(diào)用函數(shù) arch_unmap 執(zhí)行處理器架構(gòu)特定的處理。各種處理器架構(gòu)自定義函數(shù)arch_unmap,它默認(rèn)是一個(gè)空函數(shù)。
(9)調(diào)用函數(shù) remove_vma_list 刪除所有目標(biāo)。
編輯:黃飛
?
評(píng)論
查看更多