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