用戶虛擬地址空間的管理比內核地址空間的管理複雜:node
(如下默認系統有一個內存管理單元MMU,支持使用虛擬內存)程序員
各個進程的虛擬地址空間起始於地址0,延伸到TASK_SIZE - 1,其上是內核地址空間,用戶程序只能訪問整個地址空間的下半部分,不能訪問內核部分。不管當前哪一個用戶進程處於活動狀態,虛擬地址空間內核部分的內容老是一樣的,虛擬地址空間由許多不一樣長度的段組成,用於不一樣的目的,必須分別處理。緩存
虛擬地址空間包含了若干區域,其分佈方式特定於體系結構,但它們有如下共同成分:安全
系統中,各個進程都具備一個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 }
(各個體系結構能夠經過幾個配置選項影響虛擬地址空間的佈局,好比在不一樣mmap區域佈局之間選擇,建立新內存映射時指定具體地址,尋找新的內存映射低端內存位置的方式等等。)app
進程有一個標誌PF_RANDOMIZE,設置標誌後,內核不會爲棧和內存映射的起點選擇固定位置,而是每次新進程啓動時隨機改變這些值的設置。(引入複雜性防止攻擊)框架
圖1爲前述各部分在大多數體系結構裏虛擬地址空間中的分佈狀況。dom
圖1 進程的線性地址空間的組成編輯器
圖1所示的這種經典佈局意味着堆最高只能到mmap的起始位置(IA-32中一般大小爲1G),所以出現了圖2所示的新的佈局。新的佈局中,使用固定值限制棧的最大長度,內存映射區域能夠在棧末端下方開始,自頂向下擴展,堆依然處於虛擬地址空間中較低位置向上增加,所以mmap區域和堆能夠相對擴展,直至耗盡虛擬地址空間中的剩餘區域。(爲確保棧和mmap區域不衝突,二者之間設置了一個安全隙)函數
圖2 mmap區域自頂向下擴展時IA-32系統虛擬地址空間的佈局
在使用load_elf_binary裝載一個ELF二進制文件時,將建立進程的地址空間(exec系統調用中實現)。圖3爲load_elf_binary的代碼流程圖。
圖3 load_elf_binary代碼流程圖
因爲全部用戶進程總的虛擬地址空間比可用的物理內存大得多,因此只有最經常使用的部分才與物理頁幀關聯。以文本編輯器爲例,一般用戶只關注文件結尾處,所以儘管整個文件都映射到內存中,實際上只用了幾頁來存儲文件末尾的數據,至於文件開始處的數據,內核須要在地址空間保存相關信息(如數據在磁盤上的位置,以及須要數據時如何讀取)。
內核提供一種數據結構創建虛擬地址空間的區域和相關數據所在位置之間的關聯。
按需分配和填充頁稱爲按需調頁法(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 }
每一個區域都經過一個vm_area_struct實例描述,進程各區域按兩種方法排序:
用戶虛擬地址空間中的每一個區域由開始和結束地址描述。現存的區域按起始地址以遞增次序被納入鏈表中。掃描鏈表找到與特定地址關聯的區域,在有大量區域時是很是低效的操做(數據密集型的應用程序就是這樣)。所以vm_area_struct的各個實例還經過紅黑樹管理,能夠顯著加快掃描速度。
增長新區域時,內核首先搜索紅黑樹,找到恰好在新區域以前的區域。所以,內核能夠向樹和線性鏈表添加新的區域,而無需掃描鏈表。最後,內存中的狀況如圖5所示(樹的表示只是象徵性的,沒有反映真實佈局的複雜性)。
圖5 vm_area_struct實例與進程的虛擬地址空間關聯
每一個區域都是一個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_DONTCOPY,則相關的區域在fork系統調用執行時不復制。
VM_DONTEXPAND禁止區域經過mremap系統調用擴展。
若是區域是基於某些體系結構支持的巨型頁,則設置VM_HUGETLB標誌。
VM_ACCOUNT指定區域是否被納入overcommit特性的計算中。這些特性以多種方式限制內存分配。
優先查找樹(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所示:
經過虛擬地址,find_vma能夠查找用戶地址空間中結束地址在給定地址以後的第一個區域,即知足addr小於vm_area_struct->vm_end條件的第一個區域。該函數的參數不只包括虛擬地址(addr),還包括一個指向mm_struct實例的指針,後者指定了掃描哪一個進程的地址空間。
如圖8所示,在新區域被加到進程的地址空間時,內核會檢查它是否能夠與一個或多個現存域合併,經過函數vm_merge實現,該函數的參數包括相關進程的地址空間實例,緊接着新區域以前的區域,該區域在紅黑查找樹中的父結點,新區域的開始地址、結束地址、標誌。若是該區域屬於一個文件映射,有一個指向表示該文件的file實例的指針,和指定了映射在文件數據內的偏移量。
insert_vm_struct是內核用於插入新區域的標準函數。實際工做委託給兩個輔助函數,如圖9所示。
圖9 insert_vm_struct代碼流程圖
首先調用find_vma_prepare,經過新區域的起始地址和涉及的地址空間(mm_struct),獲取相關信息;而後使用vma_link將新區域合併到該進程現存的數據結構中。
在向數據結構插入新的內存區域以前,內核必須確認虛擬地址空間中有足夠的空閒空間,可用於給定長度的區域,該工做分配給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系統調用用於刪除映射(此時不須要文件偏移量)。
mmap和mmap2可設置的標誌集以下:
sys_map在大多數體系結構上行爲相似,最終都會進入do_mmap_pgoff函數,mmap2系統調用入口是sys_mmap2,它會當即將工做委託給do_map2,內核在此找到所處理文件的全部特徵數據,隨後工做委託給do_mmap_pgoff。
do_mmap_pgoff與體系結構無關,圖10爲它的代碼流程圖。
圖10 do_mmap_pgoff代碼流程圖
內核維護了進程用於映射的頁數目統計。因爲能夠限制進程的資源用量,內核必須始終確保資源使用不超出容許值。對於每一個進程能夠建立的映射,還有一個最大數目的限制。
內核必須進行普遍的安全性和合理性檢查,以防應用程序設置無效參數或可能影響系統穩定性的參數。例如,映射不能比虛擬地址空間更大,也不能擴展到超出虛擬地址空間的邊界。
從虛擬地址空間刪除現存映射,必須使用munmap系統調用,它須要兩個參數:解除映射區域的起始地址和長度,sys_munmap是該系統調用的入口,sys_munmap系統調用將工做委託給do_munmap函數,其代碼流程圖如圖11所示。
圖11 do_munmap代碼流程圖
普通的映射將文件中一個連續的部分映射到虛擬內存中一個一樣連續的部分。若是須要將文件的不一樣部分以不一樣順序映射到虛擬內存的連續區域中,則使用非線性映射。sys_remap_file_pages系統調用專門用於該目的,它能夠將現存映射移動到虛擬內存中的一個新的位置。其代碼流程圖如圖12所示。
圖12 sys_remap_file_pages代碼流程圖
內核經過頁表創建了虛擬和物理地址之間的關係,內核還完成了進程的一個內存區域與其虛擬內存頁地址之間的切換。除此之外,內核還採用了一種逆向映射方法(一些附加的數據結構和函數),創建頁和全部映射了該頁的位置之間的關聯。
內核使用了簡潔的數據結構,以最小化逆向映射的管理開銷。page結構包含了一個用於實現逆向映射的成員。
1 struct page { 2 .... 3 atomic_t _mapcount; // 內存管理子系統中映射的頁表項計數,用於表示頁是否已經映射,還用於限制逆向映射搜索。 4 ... 5 };
_mapcount代表共享該頁的位置的數目。計數器的初始值爲1。在頁插入到逆向映射數據結構時,計數器賦值爲0。頁每次增長一個使用者時,計數器加1。這使得內核可以快速檢查在全部者以外該頁有多少使用者。此外,經過在優先查找樹中嵌入屬於非匿名映射的每一個區域和指向內存中同一頁的匿名區域的鏈表即可在給定的page實例中找到全部映射了該物理內存頁的位置。
內核在實現逆向映射時採用的技巧是,不直接保存頁和相關的使用者之間的關聯,而只保存頁和頁所在區域之間的關聯。包含該頁的全部其餘區域(進而全部的使用者)均可以找到。該方法又名基於對象的逆向映射(object-based reverse mapping),由於沒有存儲頁和使用者之間的直接關聯。相反,在二者之間插入了另外一個對象(該頁所在的區域)。
在建立逆向映射時,有必要區分兩個備選項:匿名頁和基於文件映射的頁。
(1)匿名頁
將匿名頁插入到逆向映射數據結構中有兩種方法。對新的匿名頁必須調用page_add_new_anon_rmap;對已經有引用計數的頁,則使用page_add_anon_rmap。這兩個函數之間惟一的差異是,前者將映射計數器page->_mapcount設置爲0(新初始化的頁_mapcount的初始值爲-1),後者將計數器加1。
(2)基於文件映射的頁
基於文件映射的頁的逆向映射的創建比較簡單,基本上,所須要作的只是對_mapcount變量加1(原子操做)並更新各內存域的統計量。
函數page_referenced使用了逆向映射方案所涉及的數據結構,統計了最近活躍地使用(即訪問)了某個共享頁的進程的數目,這不一樣於該頁映射到的區域數目。
該函數至關於一個多路複用器,對匿名頁調用page_referenced_anon,而對基於文件映射的頁調用page_referenced_file。分別調用的兩個函數,其目的都是肯定有多少地方在使用一個頁,但因爲底層數據結構的不一樣,兩者採用了不一樣的方法。
堆是進程中用於動態分配變量和數據的內存區域,堆的管理對應用程序員不是直接可見的。
堆是一個連續的內存區域,在擴展時自下至上增加。mm_struct結構包含了堆在虛擬地址空間中的起始和當前結束地址(start_brk和brk)。
brk系統調用只須要一個參數,用於指定堆在虛擬地址空間中新的結束地址,其入口是sys_brk函數,代碼流程圖如圖13所示。
圖13 sys_brk代碼流程圖
若是進程訪問的虛擬地址空間部分還沒有與頁幀關聯,處理器自動地引起一個缺頁異常,由內核處理此異常。圖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種狀況:
按需分配頁的工做委託給函數do_linear_fault,在轉換一些參數以後,其他的工做委託給函數__do_fault,函數__do_fault的代碼流程圖如圖16所示。
圖16 __do_fault代碼流程圖
對給定涉及區域的vm_area_struct的讀取操做,內核進行如下三步操做:
若是須要寫訪問,內核必須區分共享和私有映射。對私有映射,必須準備頁的一份副本。
對於沒有關聯到文件做爲後備存儲器的頁,須要調用do_anonymous_page進行映射。除了無需向頁讀入數據以外,該過程幾乎與映射基於文件的數據沒什麼不一樣。在highmem內存域創建一個新頁,並清空其內容。接下來將頁加入到進程的頁表,並更新高速緩存或者MMU。
寫時複製在do_wp_page中處理,主要步驟爲:
因爲異常地址與映射文件的內容並不是線性相關,所以必須從先前用pgoff_to_pte編碼的頁表項中,獲取所需位置的信息(pte_to_pgoff分析頁表項並獲取所需的文件中的偏移量(以頁爲單位))。在得到文件內部的地址以後,讀取所需數據相似於普通的缺頁異常。所以內核將工做移交先前討論的函數__do_page_fault,處理到此爲止。
在訪問內核地址空間時,缺頁異常可能被如下條件觸發:
前兩種狀況是真正的錯誤,內核必須對此進行額外的檢查。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 處理用戶空間數據中的字符串的標準函數