深刻Linux內核架構——進程虛擬內存

  • 逆向映射(reverse mapping)技術有助於從虛擬內存頁跟蹤到對應的物理內存頁;
  • 缺頁處理(page fault handling)容許從塊設備按需讀取數據填充虛擬地址空間。

1、簡介

用戶虛擬地址空間的管理比內核地址空間的管理複雜:node

  • 每一個應用程序都有自身的地址空間,與全部其餘應用程序分隔開;
  • 一般在巨大的線性地址空間中,只有不多的段可用於各個用戶空間進程,這些段彼此有必定的距離,內核須要一些數據結構,來有效地管理這些(隨機)分佈的段;
  • 地址空間只有極小的一部分與物理內存頁直接關聯,不常用的部分,則僅當必要時與頁幀關聯;
  • 內核信任自身,但沒法信任用戶進程,所以,各個操做用戶地址空間的操做都伴隨有各類檢查,以確保程序的權限不會超出應有的限制,進而危及系統的穩定性和安全性;
  • fork-exec模型在UNIX操做系統下用於產生新進程,若是實現得較爲粗劣,模型功能不強大,內核則必須藉助於一些技巧,來儘量高效地管理用戶地址空間。

(如下默認系統有一個內存管理單元MMU,支持使用虛擬內存)程序員

2、進程虛擬地址空間

各個進程的虛擬地址空間起始於地址0,延伸到TASK_SIZE - 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區域佈局之間選擇,建立新內存映射時指定具體地址,尋找新的內存映射低端內存位置的方式等等。)app

進程有一個標誌PF_RANDOMIZE,設置標誌後,內核不會爲棧和內存映射的起點選擇固定位置,而是每次新進程啓動時隨機改變這些值的設置。(引入複雜性防止攻擊)框架

1爲前述各部分在大多數體系結構裏虛擬地址空間中的分佈狀況。dom

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系統虛擬地址空間的佈局

二、創建佈局

在使用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,確保在啓用地址空間隨機化的狀況下,對該地址進行隨機偏移。

3、內存映射的原理

因爲全部用戶進程總的虛擬地址空間比可用的物理內存大得多,因此只有最經常使用的部分才與物理頁幀關聯。以文本編輯器爲例,一般用戶只關注文件結尾處,所以儘管整個文件都映射到內存中,實際上只用了幾頁來存儲文件末尾的數據,至於文件開始處的數據,內核須要在地址空間保存相關信息(如數據在磁盤上的位置,以及須要數據時如何讀取)。

內核提供一種數據結構創建虛擬地址空間的區域和相關數據所在位置之間的關聯。

按需分配和填充頁稱爲按需調頁法(demand paging),它基於處理器和內核之間的交互,使用的數據結構如圖4所示。

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實例描述,進程各區域按兩種方法排序:

  • 在一個單鏈表上(開始於mm_struct->mmap);
  • 在一個紅黑樹中,根結點位於mm_rb。

用戶虛擬地址空間中的每一個區域由開始和結束地址描述。現存的區域按起始地址以遞增次序被納入鏈表中。掃描鏈表找到與特定地址關聯的區域,在有大量區域時是很是低效的操做(數據密集型的應用程序就是這樣)。所以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_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特性的計算中。這些特性以多種方式限制內存分配。

三、優先查找樹

優先查找樹(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 管理共享的相同映射所涉及各個數據結構的關聯

5、對區域的操做

內核提供了各類函數操做進程的虛擬內存區域,還負責在管理這些數據結構時進行優化。

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,其處理邏輯與上文所述相似)

6、地址空間

文件的內存映射能夠認爲是兩個不一樣的地址空間之間的映射,一個地址空間是用戶進程的虛擬地址空間,另外一個是文件系統所在的地址空間。

在內核建立一個映射時,必須創建兩個地址空間之間的關聯,以支持兩者以讀寫請求的形式通訊。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的概念。

7、內存映射

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可設置的標誌集以下:

  • 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,找到映射的起始地址,在這過程當中,會對代碼執行路徑中的不一樣位置進行幾回檢查,若是某一次失敗則結束操做並返回一個操做代碼。

內核維護了進程用於映射的頁數目統計。因爲能夠限制進程的資源用量,內核必須始終確保資源使用不超出容許值。對於每一個進程能夠建立的映射,還有一個最大數目的限制。

內核必須進行普遍的安全性和合理性檢查,以防應用程序設置無效參數或可能影響系統穩定性的參數。例如,映射不能比虛擬地址空間更大,也不能擴展到超出虛擬地址空間的邊界。

二、刪除映射

從虛擬地址空間刪除現存映射,必須使用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實例佔用的空間,完成從內核中刪除映射的工做。

三、非線性映射

普通的映射將文件中一個連續的部分映射到虛擬內存中一個一樣連續的部分。若是須要將文件的不一樣部分以不一樣順序映射到虛擬內存的連續區域中,則使用非線性映射。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標誌可阻止讀入)。

8、反向映射

  • 在映射一頁時,它關聯到一個進程,但不必定處於使用中;
  • 對頁的引用次數代表頁使用的活躍程度,爲肯定該數目,內核首先必須創建頁和全部使用者之間的關聯,接下來必須藉助於一些技巧來計算出頁使用的活躍程度。

內核經過頁表創建了虛擬和物理地址之間的關係,內核還完成了進程的一個內存區域與其虛擬內存頁地址之間的切換。除此之外,內核還採用了一種逆向映射方法(一些附加的數據結構和函數),創建頁和全部映射了該頁的位置之間的關聯。

一、數據結構

內核使用了簡潔的數據結構,以最小化逆向映射的管理開銷。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。分別調用的兩個函數,其目的都是肯定有多少地方在使用一個頁,但因爲底層數據結構的不一樣,兩者採用了不一樣的方法。

9、堆的管理

堆是進程中用於動態分配變量和數據的內存區域,堆的管理對應用程序員不是直接可見的。

堆是一個連續的內存區域,在擴展時自下至上增加。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的簡化版本,它在用戶地址空間中建立了一個匿名映射,省去了一些數處理)。

10、缺頁異常的處理

若是進程訪問的虛擬地址空間部分還沒有與頁幀關聯,處理器自動地引起一個缺頁異常,由內核處理此異常。圖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信號發送給進程。

11、用戶空間缺頁異常的校訂

確認缺頁異常是從容許的地址觸發後,內核必須肯定將所需數據讀取到物理內存的適當方法。該任務委託給函數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在這種狀況下可用於處理異常。

 一、按需分配/調頁

按需分配頁的工做委託給函數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)從文件中將數據傳輸到物理內存。

若是須要寫訪問,內核必須區分共享和私有映射。對私有映射,必須準備頁的一份副本。

二、匿名頁

對於沒有關聯到文件做爲後備存儲器的頁,須要調用do_anonymous_page進行映射。除了無需向頁讀入數據以外,該過程幾乎與映射基於文件的數據沒什麼不一樣。在highmem內存域創建一個新頁,並清空其內容。接下來將頁加入到進程的頁表,並更新高速緩存或者MMU。

三、寫時複製

寫時複製在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將其插入到逆向映射數據結構。此後,用戶空間進程能夠向頁寫入數據。

四、獲取非線性映射

因爲異常地址與映射文件的內容並不是線性相關,所以必須從先前用pgoff_to_pte編碼的頁表項中,獲取所需位置的信息(pte_to_pgoff分析頁表項並獲取所需的文件中的偏移量(以頁爲單位))。在得到文件內部的地址以後,讀取所需數據相似於普通的缺頁異常。所以內核將工做移交先前討論的函數__do_page_fault,處理到此爲止。

12、內核缺頁異常

在訪問內核地址空間時,缺頁異常可能被如下條件觸發:

  • 內核中的程序設計錯誤致使訪問不正確的地址,這是真正的程序錯誤(這在穩定版本中應該永遠都不會發生,但在開發版本中會偶爾發生);
  • 內核經過用戶空間傳遞的系統調用參數,訪問了無效地址;
  • 訪問使用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狀態(出現了致命問題,給出各錯誤狀態)。

十3、在內核和用戶空間之間複製數據

內核常常須要從用戶空間向內核空間複製數據(好比系統調用中採用指針間接傳遞冗長的數據結構),從內核空間向用戶空間也有寫數據需求。

因爲用戶空間程序不能訪問內核地址,也沒法保證用戶空間中指針指向的虛擬內存頁確實與物理內存頁關聯,因此不能只是傳遞並反引用指針。內核提供了幾個標準函數,以處理內核空間和用戶空間之間的數據交換。

17是用戶空間和內核空間之間交換數據的標準函數示例。圖18是處理用戶空間數據中的字符串標準函數的定義。

 圖17 用戶空間和內核空間之間的交換數據的標準函數

 

18 處理用戶空間數據中的字符串的標準函數

相關文章
相關標籤/搜索