逆向映射(reverse mapping)技術有助於從虛擬記憶體頁跟蹤到對應的物理記憶體頁; 缺頁處理(page fault handling)允許從塊設備按需讀取數據填充虛擬地址空間。 一、簡介 用戶虛擬地址空間的管理比內核地址空間的管理複雜: 每個應用程式都有自身的地址空間,與所有其他應用程式分隔開 ...
- 逆向映射(reverse mapping)技術有助於從虛擬記憶體頁跟蹤到對應的物理記憶體頁;
- 缺頁處理(page fault handling)允許從塊設備按需讀取數據填充虛擬地址空間。
一、簡介
用戶虛擬地址空間的管理比內核地址空間的管理複雜:
- 每個應用程式都有自身的地址空間,與所有其他應用程式分隔開;
- 通常在巨大的線性地址空間中,只有很少的段可用於各個用戶空間進程,這些段彼此有一定的距離,內核需要一些數據結構,來有效地管理這些(隨機)分佈的段;
- 地址空間只有極小的一部分與物理記憶體頁直接關聯,不經常使用的部分,則僅當必要時與頁幀關聯;
- 內核信任自身,但無法信任用戶進程,因此,各個操作用戶地址空間的操作都伴隨有各種檢查,以確保程式的許可權不會超出應有的限制,進而危及系統的穩定性和安全性;
- fork-exec模型在UNIX操作系統下用於產生新進程,如果實現得較為粗劣,模型功能不強大,內核則必須藉助於一些技巧,來儘可能高效地管理用戶地址空間。
(以下預設系統有一個記憶體管理單元MMU,支持使用虛擬記憶體)
二、進程虛擬地址空間
各個進程的虛擬地址空間起始於地址0,延伸到TASK_SIZE - 1,其上是內核地址空間,用戶程式只能訪問整個地址空間的下半部分,不能訪問內核部分。無論當前哪個用戶進程處於活動狀態,虛擬地址空間內核部分的內容總是同樣的,虛擬地址空間由許多不同長度的段組成,用於不同的目的,必須分別處理。
1、進程地址空間的佈局
虛擬地址空間包含了若幹區域,其分佈方式特定於體繫結構,但它們有以下共同成分:
- 當前運行代碼的二進位代碼(其代碼通常稱為text,所處的虛擬記憶體區域稱為text段);
- 程式使用的動態庫的代碼;
- 存儲全局變數和動態產生的數據的堆;
- 用於保存局部變數和實現函數/過程調用的棧;
- 環境變數和命令行參數的段;
- 將文件內容映射到虛擬地址空間中的記憶體映射。
系統中,各個進程都具有一個struct mm_struct實例,實例中保存了進程的記憶體管理信息,可以通過task_struct訪問。
1 struct mm_struct { 2 ... 3 unsigned long (*get_unmapped_area) (struct file *filp, 4 unsigned long addr, unsigned long len, 5 unsigned long pgoff, unsigned long flags); 6 ... 7 unsigned long mmap_base; /* mmap區域的基地址 */ 8 unsigned long task_size; /* 進程虛擬記憶體空間的長度 */ 9 ... 10 unsigned long start_code, end_code, start_data, end_data; 11 unsigned long start_brk, brk, start_stack; 12 unsigned long arg_start, arg_end, env_start, env_end; 13 ... 14 }
- 可執行代碼占用的虛擬地址空間區域,開始和結束部分分別通過start_code和end_code標記;start_data和end_data標記了包含已初始化數據的區域。(ELF二進位文件映射到地址空間後,這些區域長度不再改變)
- 堆的起始地址保存在start_brk,brk表示堆區域當前結束的地址(堆的起始地址在進程生命周期中不變,但其長度會變化,意味著brk的值會變化)。
- 參數列表和環境參數分別由arg_start和arg_end、env_start和env_end描述,兩個區域都位於棧中最高的區域。
- mmap_base表示虛擬地址空間中用於記憶體映射的其實地址。
- task_size存儲了對應進程的地址空間長度(通常為TASK_SIZE)。
(各個體繫結構可以通過幾個配置選項影響虛擬地址空間的佈局,比如在不同mmap區域佈局之間選擇,創建新記憶體映射時指定具體地址,尋找新的記憶體映射低端記憶體位置的方式等等。)
進程有一個標誌PF_RANDOMIZE,設置標誌後,內核不會為棧和記憶體映射的起點選擇固定位置,而是每次新進程啟動時隨機改變這些值的設置。(引入複雜性防止攻擊)
圖1為前述各部分在大多數體繫結構里虛擬地址空間中的分佈情況。
圖1 進程的線性地址空間的組成
- text段映射到虛擬地址空間中的方式由ELF標準確定,每個體繫結構都指定了一個特定的起始地址,IA-32系統起始於0x8048000,在text段的起始地址與最低可用地址之間有大約128MB間距,用於捕獲NULL指針。堆緊接著text段,向上增長。
- 棧起始於STACK_TOP(大多數體繫結構為TASK_SIZE),如果設置了PF_RANDOMIZE則會減少一個隨機量,進程的參數列表和環境變數都是棧的初始數據。
- 用於記憶體映射的區域起始於mm_struct->mmap_base,通常設置為TASK_UNMAPPED_BASE(幾乎所有情況下其值為TASK_SIZE/3)。
圖1所示的這種經典佈局意味著堆最高只能到mmap的起始位置(IA-32中通常大小為1G),因此出現了圖2所示的新的佈局。新的佈局中,使用固定值限制棧的最大長度,記憶體映射區域可以在棧末端下方開始,自頂向下擴展,堆依然處於虛擬地址空間中較低位置向上增長,因此mmap區域和堆可以相對擴展,直至耗盡虛擬地址空間中的剩餘區域。(為確保棧和mmap區域不衝突,兩者之間設置了一個安全隙)
圖2 mmap區域自頂向下擴展時IA-32系統虛擬地址空間的佈局
2、建立佈局
在使用load_elf_binary裝載一個ELF二進位文件時,將創建進程的地址空間(exec系統調用中實現)。圖3為load_elf_binary的代碼流程圖。
圖3 load_elf_binary代碼流程圖
- 如果全局變數randomize_va_space設置為1,則啟用地址空間隨機化機制,通常情況下是啟用的;
- 然後由arch_pick_mmap_layout完成選擇佈局的工作,如果對應體繫結構沒有提供一個具體的函數,則使用內核的預設常式;
- 最後setup_arg_pages函數負責在適當的位置創建棧,該函數需要棧頂位置作為參數,棧頂由特定於體繫結構的常數STACK_TOP給出,而後調用randomize_stack_top,確保在啟用地址空間隨機化的情況下,對該地址進行隨機偏移。
三、記憶體映射的原理
由於所有用戶進程總的虛擬地址空間比可用的物理記憶體大得多,所以只有最常用的部分才與物理頁幀關聯。以文本編輯器為例,通常用戶只關註文件結尾處,因此儘管整個文件都映射到記憶體中,實際上只用了幾頁來存儲文件末尾的數據,至於文件開始處的數據,內核需要在地址空間保存相關信息(如數據在磁碟上的位置,以及需要數據時如何讀取)。
內核提供一種數據結構建立虛擬地址空間的區域和相關數據所在位置之間的關聯。
按需分配和填充頁稱為按需調頁法(demand paging),它基於處理器和內核之間的交互,使用的數據結構如圖4所示。
圖4 按需調頁期間各數據結構的交互
- 進程試圖訪問用戶地址空間中的一個記憶體地址,但使用頁表無法確定物理地址(物理記憶體中沒有關聯頁);
- 處理器接下來觸發一個缺頁異常,發送到內核;
- 內核會檢查負責缺頁區域的進程地址空間數據結構,找到適當的後備存儲器,或者確認該訪問實際上是不正確的;
- 分配物理記憶體頁,並從後備存儲器讀取所需數據填充;
- 藉助於頁表將物理記憶體頁併入到用戶進程的地址空間,應用程式恢復執行。
四、數據結構
與記憶體佈局相關的信息在struct mm_struct中為:
1 struct mm_struct { 2 struct vm_area_struct * mmap; /* 虛擬記憶體區域列表 */ 3 struct rb_root mm_rb; 4 struct vm_area_struct * mmap_cache; /* 上一次find_vma的結果 */ 5 ... 6 }
1、樹和鏈表
每個區域都通過一個vm_area_struct實例描述,進程各區域按兩種方法排序:
- 在一個單鏈表上(開始於mm_struct->mmap);
- 在一個紅黑樹中,根結點位於mm_rb。
用戶虛擬地址空間中的每個區域由開始和結束地址描述。現存的區域按起始地址以遞增次序被歸入鏈表中。掃描鏈表找到與特定地址關聯的區域,在有大量區域時是非常低效的操作(數據密集型的應用程式就是這樣)。因此vm_area_struct的各個實例還通過紅黑樹管理,可以顯著加快掃描速度。
增加新區域時,內核首先搜索紅黑樹,找到剛好在新區域之前的區域。因此,內核可以向樹和線性鏈表添加新的區域,而無需掃描鏈表。最後,記憶體中的情況如圖5所示(樹的表示只是象徵性的,沒有反映真實佈局的複雜性)。
圖5 vm_area_struct實例與進程的虛擬地址空間關聯
2、虛擬記憶體區域的表示
每個區域都是一個vm_area_struct實例。其結構體如下所示:
1 vm_area_struct { 2 struct mm_struct * vm_mm; //反向指針,指向該區域所屬的mm_struct實例 3 unsigned long vm_start; //該區域在用戶空間中的起始地址 4 unsigned long vm_end; //該區域在用戶空間中的結束地址 5 /* 各進程的虛擬記憶體區域鏈表,按地址排序 */ 6 struct vm_area_struct *vm_next; //進程所有vm_area_struct實例的鏈表指針 7 pgprot_t vm_page_prot; //存儲該區域的訪問許可權 8 unsigned long vm_flags; //描述該區域的一組標誌,如下列出 9 struct rb_node vm_rb; //進程所有vm_area_struct實例的紅黑樹集成 10 /* 11 對於有地址空間和後備存儲器的區域來說, 12 shared連接到address_space->i_mmap優先樹, 13 或連接到懸掛在優先樹結點之外、類似的一組虛擬記憶體區域的鏈表, 14 或連接到address_space->i_mmap_nonlinear鏈表中的虛擬記憶體區域。 */ 15 union { 16 struct { 17 struct list_head list; 18 void *parent; /* 與prio_tree_node的parent成員在記憶體中位於同一位置 */ 19 struct vm_area_struct *head; 20 } vm_set; 21 struct raw_prio_tree_node prio_tree_node; 22 } shared; 23 /* 24 *在文件的某一頁經過寫時複製之後,文件的MAP_PRIVATE虛擬記憶體區域可能同時在i_mmap樹和 25 *anon_vma鏈表中。MAP_SHARED虛擬記憶體區域只能在i_mmap樹中。 26 *匿名的MAP_PRIVATE、棧或brk虛擬記憶體區域(file指針為NULL)只能處於anon_vma鏈表中。 27 */ 28 struct list_head anon_vma_node; //鏈表元素,用於管理源自匿名映射(anonymous mapping)的共用頁 29 struct anon_vma *anon_vma; //用於管理源自匿名映射(anonymous mapping)的共用頁 30 /* 用於處理該結構的各個函數指針。 */ 31 struct vm_operations_struct * vm_ops; //指向多個方法的集合,用於在區域上執行各種操作 32 /* 後備存儲器的有關信息: */ 33 unsigned long vm_pgoff; //用於只映射文件部分內容時指定文件映射偏移量,單位是PAGE_SIZE,不是PAGE_CACHE_SIZE 34 struct file * vm_file; //映射到的文件(可能是NULL),指向file實例 35 void * vm_private_data; //vm_pte(即共用記憶體),用於存儲私有數據,取決於映射類型 36 };
- VM_READ、VM_WRITE、VM_EXEC、VM_SHARED分別指定了頁的內容是否可以讀、寫、執行,或者由幾個進程共用;
- VM_MAYREAD、VM_MAYWRITE、VM_MAYEXEC、VM_MAYSHARE用於確定是否可以設置對應的VM_*標誌,這是mprotect系統調用所需要的;
- VM_GROWSDOWN和VM_GROWSUP表示一個區域是否可以向下或向上擴展(到更低或更高的虛擬地址),由於堆自下而上增長,其區域需要設置VM_GROWSUP,VM_GROWSDOWN對棧設置,該區域自頂向下增長;
- 如果區域很可能從頭到尾順序讀取,則設置VM_SEQ_READ,VM_RAND_READ指定了讀取可能是隨機的,這兩個標誌用於“提示”記憶體管理子系統和塊設備層,以優化其性能。
如果設置了VM_DONTCOPY,則相關的區域在fork系統調用執行時不複製。
VM_DONTEXPAND禁止區域通過mremap系統調用擴展。
如果區域是基於某些體繫結構支持的巨型頁,則設置VM_HUGETLB標誌。
VM_ACCOUNT指定區域是否被歸入overcommit特性的計算中。這些特性以多種方式限制記憶體分配。
3、優先查找樹
優先查找樹(priority search tree)用於建立文件中的一個區域與該區域映射到的所有虛擬地址空間之間的關聯。
(1)附加的數據結構
每個打開文件(和每個塊設備,因為這些也可以通過設備文件進行記憶體映射)都表示為struct file的一個實例,該結構包含了一個指向地址空間對象struct address_space的指針,該對象是優先查找樹(prio tree)的基礎,而文件區間與其映射到的地址空間之間的關聯即通過優先樹建立。
此外,每個文件和塊設備都表示為struct inode的一個實例,struct file是通過open系統調用打開的文件的抽象,與此相反,inode表示文件系統自身中的對象。inode是一個特定於文件的數據結構,file是特定於給定進程的,記憶體中各結構之間的關聯如圖6所示。
圖6 藉助優先樹跟蹤文件給定區間所映射到的虛擬地址空間
地址空間是優先樹的基本要素,優先樹包含了所有相關的vm_area_struct實例,描述了與inode關聯的文件區間到一些虛擬地址空間的映射。每個struct vm_area的實例都包含了一個指向所屬進程的mm_struct的指針,因此建立關聯。此外,vm_area_struct還可以通過以i_mmap_nonlinear為表頭的雙鏈表與一個地址空間關聯,這是非線性映射(nonlinear mapping)所需。
(2)優先樹的表示
優先樹用來管理表示給定文件中特定區間的所有vm_area_struct實例,它不僅能夠處理重疊區間,還處理相同的文件區間。對於重疊區間,區間的邊界提供了一個唯一的索引,將各個區間存儲在一個唯一的樹結點中,如果一個區間完全包含在另一個區間只會中,那麼前者是後者的子結點;對於相同區間,可以將一個vm_set的鏈表與一個優先樹結點關聯起來,如圖7所示。
圖7 管理共用的相同映射所涉及各個數據結構的關聯
五、對區域的操作
內核提供了各種函數操作進程的虛擬記憶體區域,還負責在管理這些數據結構時進行優化。
圖8 對區域的操作
如圖8所示:
- 如果一個新區域緊接著現存區域前後直接添加,內核將涉及的數據結構合併為一個(前提是涉及的所有區域的訪問許可權相同,而且是從同一後備存儲器映射的連續數據);
- 如果在區域的開始或結束處進行刪除,則必須據此截斷現存的數據結構;
- 如果刪除兩個區域之間的一個區域,那麼一方面需要減小現存數據結構的長度,另一方面需要為形成的新區域創建一個新的數據結構。
1、將虛擬地址關聯到區域
通過虛擬地址,find_vma可以查找用戶地址空間中結束地址在給定地址之後的第一個區域,即滿足addr小於vm_area_struct->vm_end條件的第一個區域。該函數的參數不僅包括虛擬地址(addr),還包括一個指向mm_struct實例的指針,後者指定了掃描哪個進程的地址空間。
2、區域合併
如圖8所示,在新區域被加到進程的地址空間時,內核會檢查它是否可以與一個或多個現存域合併,通過函數vm_merge實現,該函數的參數包括相關進程的地址空間實例,緊接著新區域之前的區域,該區域在紅黑查找樹中的父結點,新區域的開始地址、結束地址、標誌。如果該區域屬於一個文件映射,有一個指向表示該文件的file實例的指針,和指定了映射在文件數據內的偏移量。
3、插入區域
insert_vm_struct是內核用於插入新區域的標準函數。實際工作委托給兩個輔助函數,如圖9所示。
圖9 insert_vm_struct代碼流程圖
首先調用find_vma_prepare,通過新區域的起始地址和涉及的地址空間(mm_struct),獲取相關信息;然後使用vma_link將新區域合併到該進程現存的數據結構中。
4、創建區域
在向數據結構插入新的記憶體區域之前,內核必須確認虛擬地址空間中有足夠的空閑空間,可用於給定長度的區域,該工作分配給get_unmapped_area輔助函數完成。
首先檢查是否設置了MAP_FIXED標誌,該標誌表示映射將在固定地址創建。倘若如此,內核只會確保該地址滿足對齊要求(按頁),而且所要求的區間完全在可用地址空間內。
如果沒有指定區域位置,內核將調用arch_get_unmapped_area在進程的虛似記憶體區中查找適當的可用區域。如果指定了一個特定的優先選用(與固定地址不同)地址,內核會檢查該區域是否與現存區域重疊。如果不重疊,則將該地址作為目標返回。否則,內核必須遍歷進程中可用的區域,設法找到一個大小適當的空閑區域。這樣做時,內核會檢查是否可使用前一次掃描時緩存的區域。如果搜索持續到用戶地址空間的末端(TASK_SIZE),仍然沒有找到適當的區域,則內核返回一個-ENOMEM錯誤。(如果mmap區域自頂向下擴展,那麼分配新區域的函數是arch_get_unmapped_area_topdown,其處理邏輯與上文所述類似)
六、地址空間
文件的記憶體映射可以認為是兩個不同的地址空間之間的映射,一個地址空間是用戶進程的虛擬地址空間,另一個是文件系統所在的地址空間。
在內核創建一個映射時,必須建立兩個地址空間之間的關聯,以支持二者以讀寫請求的形式通信。vm_operations_struct結構即用於完成該工作,它提供了一個操作,來讀取已經映射到虛擬地址空間、但其內容尚未進入物理記憶體的頁。該操作不瞭解映射類型或其性質的相關信息,但address_space結構中包含了有關映射的附加有信息。
vm_operations_struct和address_space之間的聯繫不是靜態的,它們使用內核為vm_operations_struct提供的標準連接,幾乎所有文件系統都採用這種方式。
1 struct vm_operations_struct generic_file_vm_ops = { 2 .fault = filemap_fault, 3 };
filemap_fault的實現使用了相關映射的readpage方法,也採用了address_space的概念。
七、記憶體映射
C標準庫中通過mmap函數建立文件到記憶體的映射,在內核一端,提供mmap和mmap2兩個系統調用在用戶虛擬地址空間中的pos位置,建立一個長度為len的映射,其訪問許可權通過prot定義。flags是一組標誌集,fd是文件描述符標識。mmap和mmap2之間的差別在於偏移量的語義(off),前者單位是位元組,後者單位是頁。
asmlinkage unsigned long sys_mmap{2}(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, unsigned long off)
munmap系統調用用於刪除映射(此時不需要文件偏移量)。
1、創建映射
mmap和mmap2可設置的標誌集如下:
- MAP_FIXED指定除了給定地址之外,不能將其他地址用於映射。如果沒有設置該標誌,內核可以在受阻時隨意改變目標地址;
- 如果一個對象(通常是文件)在幾個進程之間共用時,則必須使用MAP_SHARED;
- MAP_PRIVATE創建一個與數據源分離的私有映射,對映射區域的寫入操作不影響文件中的數據;
- MAP_ANONYMOUS創建與任何數據源都不相關的匿名映射,fd和off參數被忽略。此類映射可用於為應用程式分配類似malloc所用的記憶體。
- prot可指定PROT_EXEC、PROT_READ、PROT_WRITE、PROT_NONE值的組合,來定義訪問許可權。
sys_map在大多數體繫結構上行為類似,最終都會進入do_mmap_pgoff函數,mmap2系統調用入口是sys_mmap2,它會立即將工作委托給do_map2,內核在此找到所處理文件的所有特征數據,隨後工作委托給do_mmap_pgoff。
do_mmap_pgoff與體繫結構無關,圖10為它的代碼流程圖。
圖10 do_mmap_pgoff代碼流程圖
- do_mmap_pgoff函數分為兩個部分,一個部分徹底檢查用戶應用程式傳遞的參數,第二個考慮大量特殊情況。
- 它首先調用get_unmapped_area函數,在虛擬地址空間中找到一個適當的區域用於映射;
- 然後檢查參數,設置所需要的標誌;
- 最後將工作委托給mmap_region,找到映射的起始地址,在這過程中,會對代碼執行路徑中的不同位置進行幾次檢查,如果某一次失敗則結束操作並返回一個操作代碼。
內核維護了進程用於映射的頁數目統計。由於可以限制進程的資源用量,內核必須始終確保資源使用不超出允許值。對於每個進程可以創建的映射,還有一個最大數目的限制。
內核必須進行廣泛的安全性和合理性檢查,以防應用程式設置無效參數或可能影響系統穩定性的參數。例如,映射不能比虛擬地址空間更大,也不能擴展到超出虛擬地址空間的邊界。
2、刪除映射
從虛擬地址空間刪除現存映射,必須使用munmap系統調用,它需要兩個參數:解除映射區域的起始地址和長度,sys_munmap是該系統調用的入口,sys_munmap系統調用將工作委托給do_munmap函數,其代碼流程圖如圖11所示。
圖11 do_munmap代碼流程圖
- 內核首先調用find_vma_prev,以找到解除映射區域的vm_area_struct實例,返回指向前一個區域的指針;
- 如果解除映射區域的起始地址與find_vma_prev找到的區域起始地址不同,則只解除部分映射,而不是整個映射區域(此時需要通過區域分裂將映射劃分為幾個部分);
- 如果解除映射的部分區域的末端與原區域末端並不重合,那麼原區域後部仍然有一部分未解除映射,因此需要對這部分也進行分裂;
- 接下來調用detach_vmas_to_be_unmapped,列出所有需要解除映射的區域;
- 最後調用unmap_region從頁表刪除與映射相關的所有項,完成後將相關項從TLB中移除,用用remove_vma_list釋放vm_area_struct實例占用的空間,完成從內核中刪除映射的工作。
3、非線性映射
普通的映射將文件中一個連續的部分映射到虛擬記憶體中一個同樣連續的部分。如果需要將文件的不同部分以不同順序映射到虛擬記憶體的連續區域中,則使用非線性映射。sys_remap_file_pages系統調用專門用於該目的,它可以將現存映射移動到虛擬記憶體中的一個新的位置。其代碼流程圖如圖12所示。
圖12 sys_remap_file_pages代碼流程圖
- 內核首先檢查所有標誌,並確保重新映射的範圍有效後,通過find_vma選中目標區域的vm_area_struct實例,如果目標區域此前沒有進行過非線性映射,則vm_area_struct->vm_flags不會設置VM_NONLINEAR標誌,此時需要從優先樹移除該線性映射,並將其插入到非線性列表中;
- 然後由populate_range設置修改過的頁幀項;
- 最後一步是讀入映射的頁(在需要的情況下才會讀入,通過設置MAP_NONBLOCK標誌可阻止讀入)。
八、反向映射
- 在映射一頁時,它關聯到一個進程,但不一定處於使用中;
- 對頁的引用次數表明頁使用的活躍程度,為確定該數目,內核首先必須建立頁和所有使用者之間的關聯,接下來必須藉助於一些技巧來計算出頁使用的活躍程度。
內核通過頁表建立了虛擬和物理地址之間的關係,內核還完成了進程的一個記憶體區域與其虛擬記憶體頁地址之間的切換。除此以外,內核還採用了一種逆向映射方法(一些附加的數據結構和函數),建立頁和所有映射了該頁的位置之間的關聯。
1、數據結構
內核使用了簡潔的數據結構,以最小化逆向映射的管理開銷。page結構包含了一個用於實現逆向映射的成員。
1 struct page { 2 .... 3 atomic_t _mapcount; // 記憶體管理子系統中映射的頁表項計數,用於表示頁是否已經映射,還用於限制逆向映射搜索。 4 ... 5 };
_mapcount表明共用該頁的位置的數目。計數器的初始值為1。在頁插入到逆向映射數據結構時,計數器賦值為0。頁每次增加一個使用者時,計數器加1。這使得內核能夠快速檢查在所有者之外該頁有多少使用者。此外,通過在優先查找樹中嵌入屬於非匿名映射的每個區域和指向記憶體中同一頁的匿名區域的鏈表便可在給定的page實例中找到所有映射了該物理記憶體頁的位置。
內核在實現逆向映射時採用的技巧是,不直接保存頁和相關的使用者之間的關聯,而只保存頁和頁所在區域之間的關聯。包含該頁的所有其他區域(進而所有的使用者)都可以找到。該方法又名基於對象的逆向映射(object-based reverse mapping),因為沒有存儲頁和使用者之間的直接關聯。相反,在兩者之間插入了另一個對象(該頁所在的區域)。
2、建立逆向映射
在創建逆向映射時,有必要區分兩個備選項:匿名頁和基於文件映射的頁。
(1)匿名頁
將匿名頁插入到逆向映射數據結構中有兩種方法。對新的匿名頁必須調用page_add_new_anon_rmap;對已經有引用計數的頁,則使用page_add_anon_rmap。這兩個函數之間唯一的差別是,前者將映射計數器page->_mapcount設置為0(新初始化的頁_mapcount的初始值為-1),後者將計數器加1。
(2)基於文件映射的頁
基於文件映射的頁的逆向映射的建立比較簡單,基本上,所需要做的只是對_mapcount變數加1(原子操作)並更新各記憶體域的統計量。
3、使用逆向映射
函數page_referenced使用了逆向映射方案所涉及的數據結構,統計了最近活躍地使用(即訪問)了某個共用頁的進程的數目,這不同於該頁映射到的區域數目。
該函數相當於一個多路復用器,對匿名頁調用page_referenced_anon,而對基於文件映射的頁調用page_referenced_file。分別調用的兩個函數,其目的都是確定有多少地方在使用一個頁,但由於底層數據結構的不同,二者採用了不同的方法。
九、堆的管理
堆是進程中用於動態分配變數和數據的記憶體區域,堆的管理對應用程式員不是直接可見的。
堆是一個連續的記憶體區域,在擴展時自下至上增長。mm_struct結構包含了堆在虛擬地址空間中的起始和當前結束地址(start_brk和brk)。
brk系統調用只需要一個參數,用於指定堆在虛擬地址空間中新的結束地址,其入口是sys_brk函數,代碼流程圖如圖13所示。
圖13 sys_brk代碼流程圖
- brk機制不是獨立的內核概念,是基於匿名映射實現的,以減少內部開銷。
- 內核首先檢查用作brk值的新地址是否超出堆的限制;
- 然後sys_brk將請求地址按頁長度對其;
- 接著如果需要收縮堆時將調用do_munmap,如果堆將要擴大,內核首先必須檢查新的長度是否超出進程的最大堆長度限制,若超出限制,則什麼也不做,否則,將擴大堆的工作交給do_brk並返回新的brk的值(實質上do_brk是do_mmap_pgoff的簡化版本,它在用戶地址空間中創建了一個匿名映射,省去了一些數處理)。
十、缺頁異常的處理
如果進程訪問的虛擬地址空間部分尚未與頁幀關聯,處理器自動地引發一個缺頁異常,由內核處理此異常。圖14給出了內核在處理缺頁異常時,可能使用的各種代碼路徑的概述。
圖14 處理缺頁異常的各種可能選項
缺頁異常主要通過函數do_page_fault處理,其代碼流程圖如圖15所示。
圖15 IA-32處理器上do_page_fault的代碼流程圖
do_page_fault需要傳遞兩個參數:發生異常時使用中的寄存器集合(pt_regs *regs),提供異常原因信息的錯誤代碼(long error_code),具體檢測關聯流程如圖15所示。如果頁成功建立,則常式返回VM_FAULT_MINOR(數據已經在記憶體中)或VM_FAULT_MAJOR(數據需要從塊設備讀取)。內核接下來更新進程的統計量。但在創建頁時,也可能發生異常。如果用於載入頁的物理記憶體不足,內核會強制終止該進程,在最低限度上維持系統的運行。如果對數據的訪問已經允許,但由於其他的原因失敗(例如,訪問的映射已經在訪問的同時被另一個進程收縮,不再存在於給定的地址),則將SIGBUS信號發送給進程。
十一、用戶空間缺頁異常的校正
確認缺頁異常是從允許的地址觸發後,內核必須確定將所需數據讀取到物理記憶體的適當方法。該任務委托給函數handle_mm_fault,它不依賴於底層體繫結構,而是在記憶體管理的框架下、獨立於系統而實現。該函數確認在各級頁目錄中,通向對應於異常地址的頁表項的各個頁目錄項都存在。函數handle_pte_fault分析缺頁異常的原因。
如果頁不在物理記憶體中,則必須區分下麵3種情況:
- 如果沒有對應的頁表項(page_none),則內核必須從頭開始載入該頁,對匿名映射稱之為按需分配(demand allocation),對基於文件的映射,則稱之為按需調頁(demand paging)。如果vm_ops中沒有註冊vm_operations_struct,則不適用上述做法。在這種情況下,內核必須使用do_anonymous_page返回一個匿名頁;
- 如果該頁標記為不存在,而頁表中保存了相關的信息,則意味著該頁已經換出,因而必須從系統的某個交換區換入(換入或按需調頁);
- 非線性映射已經換出的部分不能像普通頁那樣換入,因為必須正確地恢復非線性關聯,pte_file函數可以檢查頁表項是否屬於非線性映射,do_nonlinear_fault在這種情況下可用於處理異常。
1、按需分配/調頁
按需分配頁的工作委托給函數do_linear_fault,在轉換一些參數之後,其餘的工作委托給函數__do_fault,函數__do_fault的代碼流程圖如圖16所示。
圖16 __do_fault代碼流程圖
對給定涉及區域的vm_area_struct的讀取操作,內核進行以下三步操作:
- 使用vm_area_struct->vm_file找到映射的file對象;
- 在file->f_mapping中找到指向映射自身的指針;
- 每個地址空間都有特定的地址空間操作,從中選擇readpage方法,使用mapping->a_ops->readpage(file, page)從文件中將數據傳輸到物理記憶體。
如果需要寫訪問,內核必須區分共用和私有映射。對私有映射,必須準備頁的一份副本。
2、匿名頁
對於沒有關聯到文件作為後備存儲器的頁,需要調用do_anonymous_page進行映射。除了無需向頁讀入數據之外,該過程幾乎與映射基於文件的數據沒什麼不同。在highmem記憶體域建立一個新頁,並清空其內容。接下來將頁加入到進程的頁表,並更新高速緩存或者MMU。
3、寫時複製
寫時複製在do_wp_page中處理,主要步驟為:
- 內核首先調用vm_normal_page,通過頁表項找到頁的struct page實例,本質上這個函數基於pte_pfn和pfn_to_page,這兩者是所有體繫結構都必須定義的。前者查找與頁表項相關的頁號,而後者確定與頁號相關的page實例;
- 在用page_cache_get獲取頁之後,接下來anon_vma_prepare準備好逆向映射機制的數據結構,以接受一個新的匿名區域,由於異常的來源是需要將一個充滿有用數據的頁複製到新頁,因此內核調用alloc_page_vma分配一個新頁,cow_user_page接下來將異常頁的數據複製到新頁,進程隨後可以對新頁進行寫操作;
- 然後使用page_remove_rmap,刪除到原來的只讀頁的逆向映射,新頁添加到頁表,此時也必須更新CPU的高速緩存;
- 最後,使用lru_cache_add_active將新分配的頁放置到LRU緩存的活動列表上,並通過page_add_anon_rmap將其插入到逆向映射數據結構。此後,用戶空間進程可以向頁寫入數據。
4、獲取非線性映射
由於異常地址與映射文件的內容並非線性相關,因此必須從先前用pgoff_to_pte編碼的頁表項中,獲取所需位置的信息(pte_to_pgoff分析頁表項並獲取所需的文件中的偏移量(以頁為單位))。在獲得文件內部的地址之後,讀取所需數據類似於普通的缺頁異常。因此內核將工作移交先前討論的函數__do_page_fault,處理到此為止。
十二、內核缺頁異常
在訪問內核地址空間時,缺頁異常可能被以下條件觸發:
- 內核中的程式設計錯誤導致訪問不正確的地址,這是真正的程式錯誤(這在穩定版本中應該永遠都不會發生,但在開發版本中會偶爾發生);
- 內核通過用戶空間傳遞的系統調用參數,訪問了無效地址;
- 訪問使用vmalloc分配的區域,觸發缺頁異常。
前兩種情況是真正的錯誤,內核必須對此進行額外的檢查。vmalloc的情況是導致缺頁異常的合理原因,需要加以校正。直至對應的缺頁異常發生之前,vmalloc區域中的修改都不會傳輸到進程的頁表,必須從主頁表複製適當的訪問許可權信息。
在處理不是由於訪問vmalloc區域導致的缺頁異常時,異常修正(exception fixup)機制是一個最後手段。在某些時候,內核有很好的理由準備截取不正確的訪問。例如,從用戶空間地址複製作為系統調用參數的地址數據。
在向或從用戶空間複製數據時,如果訪問的地址在虛擬地址空間中不與物理記憶體頁關聯,則會發生缺頁異常。當處於內核態時,該異常訂單處理方式與用戶狀態稍有不同。
每次發生缺頁異常時,將輸出異常的原因和當前執行代碼的地址。這使得內核可以編譯一個列表,列出所有可能執行未授權記憶體訪問操作的危險代碼塊。這個“異常表”在鏈接內核映像時創建,在二進位文件中位於__start_exception_table和__end_exception_table之間。每個表項都對應於一個struct exception_table實例,該結構是體繫結構相關的。
在異常處理過程中,藉助於函數fixup_exception搜索異常表,查找適當的匹配項;在找到修正常式時,將指令指針設置到對應的記憶體位置。在fixup_exception通過return返回後,內核將執行找到的常式。如果沒有修正常式,就表示出現了一個真正的內核異常,在對search_exception_table(不成功的)調用之後,將調用do_page_fault來處理該異常,最終導致內核進入oops狀態(出現了致命問題,給出各錯誤狀態)。
十三、在內核和用戶空間之間複製數據
內核經常需要從用戶空間向內核空間複製數據(比如系統調用中採用指針間接傳遞冗長的數據結構),從內核空間向用戶空間也有寫數據需求。
由於用戶空間程式不能訪問內核地址,也無法保證用戶空間中指針指向的虛擬記憶體頁確實與物理記憶體頁關聯,所以不能只是傳遞並反引用指針。內核提供了幾個標準函數,以處理內核空間和用戶空間之間的數據交換。
圖17是用戶空間和內核空間之間交換數據的標準函數示例。圖18是處理用戶空間數據中的字元串標準函數的定義。
圖17 用戶空間和內核空間之間的交換數據的標準函數
圖18 處理用戶空間數據中的字元串的標準函數