深入Linux核心架構——程序虛擬記憶體
- 逆向對映(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) (structfile *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 處理使用者空間資料中的字串的標準函式