Linux Kernel 核心中文手冊(3)--內存管理

Memory Management (內存管理)
 
    內存管理子系統是操作系統的重要部分。從計算機發展早期開始,就存在對於
大於系統中物理能力的內存需要。爲了克服這種限制,開發了許多種策略,其中最
成功的就是虛擬內存。虛擬內存通過在競爭進程之間共享內存的方式使系統顯得擁
有比實際更多的內存。
 
    虛擬內存不僅僅讓你的計算機內存顯得更多,內存管理子系統還提供:
 
    Large Address Spaces (巨大的地址空間)操作系統使系統顯得擁有比實際
更大量的內存。虛擬內存可以比系統中的物理內存大許多倍。
 
    < Protection (保護)系統中的每一個進程都有自己的虛擬地址空間。這些


虛擬的地址空間是相互完全分離的,所以運行一個應用程序的進程不會影響另外的
進程。另外,硬件的虛擬內存機制允許對內存區寫保護。這可以防止代碼和數據被
惡意的程序覆蓋。
 
    < Memory Mapping (內存映射)內存映射用來將映像和數據映射到進程的地
址空間。用內存映射,文件的內容被直接連結到進程的虛擬地址空間。
 
    < Fair Physics Memory Allocation (公平分配物理內存)內存管理子系統
允許系統中每一個運行中的進程公平地共享系統的物理內存
 
    Shared Virtual Memory (共享虛擬內存)雖然虛擬內存允許進程擁有分離(
虛擬)的地址空間,有時你也需要進程之間共享內存。例如,系統中可能有多個進
程運行命令解釋程序 < bash 。雖然可以在每一個進程的虛擬地址空間都擁有一份
 bash 的拷貝,更好的是在物理內存中只擁有一份拷貝,所有運行 bash 的進程共
享代碼。動態連接庫是多個進程共享執行代碼的另一個常見例子。共享內存也可以
用於進程間通訊 < (IPC) 機制,兩個或多個進程可以通過共同擁有的內存交換信
息。 Linux 系統支持系統 V 的共享內存 IPC 機制。
 
3.1 An Abstract Model of Virtual Memory (虛擬內存的抽象模型)
 
    在考慮 Linux 支持虛擬內存的方法之前,最好先考慮一個抽象的模型,以免
被太多的細節搞亂。


 
    在進程執行程序的時候,它從內存中讀取指令並進行解碼。解碼指令也許需要
讀取或者存儲內存特定位置的內容,然後進程執行指令並轉移到程序中的下一條指
令。進程不管是讀取指令還是存取數據都要訪問內存。
 
    在一個虛擬內存系統中,所有的地址都是虛擬地址而非物理地址。處理器通過
操作系統保存的一組信息將虛擬地址轉換爲物理地址。
 
    爲了讓這種轉換更簡單,將虛擬內存和物理內存分爲適當大小的塊,叫做頁(
 < page )。頁的大小一樣。(當然可以不一樣,但是這樣一來系統管理起來比較
困難)。 Linux 在 Alpha AXP 系統上使用 8K 字節的頁,而在 Intel x86 系統
上使用 4K 字節的頁。每一頁都賦予一個唯一編號: page frame number(PFN 頁
編號 ) 。在這種分頁模型下,虛擬地址由兩部分組成:虛擬頁號和頁內偏移量。
假如頁大小是 < 4K ,則虛擬地址的位 < 11 到 < 0 包括頁內偏移量,位 < 12
和以上的位是頁編號。每一次處理器遇到虛擬地址,它必須提取出偏移和虛擬頁編
號。處理器必須將虛擬頁編號轉換到物理的頁,並訪問物理頁的正確偏移處。爲此
,處理器使用了頁表( < page tables )。
 
    圖 3.1 顯示了兩個進程的虛擬地址空間,進程 X 和進程 Y ,每一個進程擁
有自己的頁表。這些頁表將每一個進程的虛擬頁映射到內存的物理頁上。圖中顯示
進程 < X 的虛擬頁號 < 0 映射到物理頁號 < 1 ,而進程 < Y 的虛擬頁編號 < 1
 映射到物理頁號 < 4 。理論上頁表每一個條目包括以下信息:


 
    有效標誌 表示頁表本條目是否有效
 
    本頁表條目描述的物理頁編號
 
    訪問控制信息 描述本頁如何使用:是否可以寫?是否包括執行代碼?
 
    頁表通過虛擬頁標號作爲偏移來訪問。虛擬頁編號 5 是表中的第 6 個元素(
 0 是第一個元素)
 
    要將虛擬地址轉換到物理地址,處理器首先找出虛擬地址的頁編號和頁內偏移
量。使用 < 2 的冪次的頁尺寸,可以用掩碼或移位簡單地處理。再一次看圖 3.
1 ,假設頁大小是 0x2000 (十進制 8192 ),進程 Y 的虛擬地址空間的地址是
 0x2194 ,處理器將會把地址轉換爲虛擬頁編號 1 內的偏移量 0x194 。
 
 
 
    處理器使用虛擬頁編號作爲索引在進程的頁表中找到它的頁表的條目。如果該
條目有效,處理器從該條目取出物理的頁編號。如果本條目無效,就是進程訪問了
它的虛擬內存中不存在的區域。在這種情況下,處理器無法解釋地址,必須將控制
權傳遞給操作系統來處理。
 


    處理器具體如何通知操作系統進程在訪問無法轉換的無效的虛擬地址,這個方
式是和處理器相關的。處理器將這種信息( < page fault )進行傳遞,操作系統
得到通知,虛擬地址出錯,以及出錯的原因。
 
    假設這是一個有效的頁表條目,處理器取出物理頁號並乘以頁大小,得到了物
理內存中本頁的基礎地址。最後,處理器加上它需要的指令或數據的偏移量。
 
    再用上述例子,進程 Y 的虛擬頁編號 1 映射到了物理頁編號 4 (起始於
0x8000 , 4x 0x2000 ),加上偏移 0x194 ,得到了最終的物理地址 0x8194 。
 
 
    通過這種方式將虛擬地址映射到物理地址,虛擬內存可以用任意順序映射到系
統的物理內存中。例如,圖 < 3.1 中,虛擬內存 < X 的虛擬頁編號映射到了物理
頁編號 < 1 而虛擬頁編號 < 7 雖然在虛擬內存中比虛擬頁 < 0 要高,卻映射到
了物理頁編號 < 0 。這也演示了虛擬內存的一個有趣的副產品:虛擬內存頁不必
按指定順序映射到物理內存中。
 
3.1.1 Demand Paging
 
    因爲物理內存比虛擬內存少得多,操作系統必須避免無效率地使用物理內存。
節省物理內存的一種方法是隻加載執行程序正在使用的虛擬頁。例如:一個數據庫
程序可能正在數據庫上運行一個查詢。在這種情況下,並非所有的數據必須放到內


存中,而只需要正被檢查的數據記錄。如果這是個查找型的查詢,那麼加載程序中
增加記錄的代碼就沒什麼意義。這種進行訪問時才加載虛擬頁的技術叫做 <
demand paging 。
 
    當一個進程試圖訪問當前不在內存中的虛擬地址的時候處理器無法找到引用的
虛擬頁對應的頁表條目。例如:圖 < 3.1 中進程 < X 的頁表中沒有虛擬頁 2 的
條目,所以如果進程 < X 試圖從虛擬頁 < 2 中的地址讀取時,處理器無法將地址
轉換爲物理地址。這時處理器通知操作系統發生 page fault 。
 
    如果出錯的虛擬地址無效意味着進程試圖訪問它不應該訪問的虛擬地址。也許
是程序出錯,例如向內存中任意地址寫。這種情況下,操作系統會中斷它,從而保
護系統中其他的進程。
 
    如果出錯的虛擬地址有效但是它所在的頁當前不在內存中,操作系統必須從磁
盤映像中將相應的頁加載到內存中。相對來講磁盤存取需要較長時間,所以進程必
須等待直到該頁被取到內存中。如果當前有其他系統可以運行,操作系統將選擇其
中一個運行。取到的頁被寫到一個空閒的頁面,並將一個有效的虛擬頁條目加到進
程的頁表中。然後這個進程重新運行發生內存錯誤的地方的機器指令。這一次虛擬
內存存取進行時,處理器能夠將虛擬地址轉換到物理地址,所以進程得以繼續運行

 
    Linux 使用 < demand paging 技術將可執行映像加載到進程的虛擬內存中。


當一個命令執行時,包含它的文件被打開,它的內容被映射到進程的虛擬內存中。
這個過程是通過修改描述進程內存映射的數據結構來實現,也叫做內存映射( <
memory mapping )。但是,實際上只有映像的第一部分真正放在了物理內存中。
映像的其餘部分仍舊在磁盤上。當映像執行時,它產生 < page fault , < Linux
 使用進程的內存映像表來確定映像的那一部分需要加載到內存中執行。
 
3.1.2 Swapping (交換)
 
    如果進程需要將虛擬頁放到物理內存中而此時已經沒有空閒的物理頁,操作系
統必須廢棄物理空間中的另一頁,爲該頁讓出空間。
 
    如果物理內存中需要廢棄的頁來自磁盤上的映像或者數據文件,而且沒有被寫
過所以不需要存儲,則該頁被廢棄。如果進程又需要該頁,它可以從映像或數據文
件中再次加載到內存中。
 
    但是,如果該頁已經被改變,操作系統必須保留它的內容以便以後進行訪問。
這種也叫做 < dirty page ,當它從物理內存中廢棄時,被存到一種叫做交換文件
的特殊文件中。因爲訪問交換文件的速度和訪問處理器以及物理內存的速度相比很
慢,操作系統必須判斷是將數據頁寫到磁盤上還是將它們保留在內存中以便下次訪
問。
 
    如果決定哪些頁需要廢棄或者交換的算法效率不高,則會發生顛簸( <


thrashing )。這時,頁不斷地被寫到磁盤上,又被讀回,操作系統過於繁忙而無
法執行實際的工作。例如在圖 < 3.1 中,如果物理頁號 < 1 經常被訪問,那麼就
不要將它交換到硬盤上。進程正在使用的也叫做工作集 (working set) 。有效的
交換方案應該保證所有進程的工作集都在物理內存中。
 
    Linux 使用 < LRU ( Least Recently Used 最近最少使用)的頁面技術來公
平地選擇需要從系統中廢棄的頁面。這種方案將系統中的每一頁都賦予一個年齡,
這個年齡在頁面存取時改變。頁面訪問越多,年紀越輕,越少訪問,年紀越老越陳
舊。陳舊的頁面是交換的好候選。
 
3.1.3 Shared Vitual Memory (共享虛擬內存)
 
    虛擬內存使多個進程可以方便地共享內存。所有的內存訪問都是通過頁表,每
一個進程都有自己的頁表。對於兩個共享一個物理內存頁的進程,這個物理頁編號
必須出現在兩個進程的頁表中。
 
    圖 3.1 顯示了兩個共享物理頁號 4 的進程。對於進程 X 虛擬頁號是 4 ,而
對於進程 Y 虛擬頁號是 6 。這也表明了共享頁的一個有趣的地方:共享的物理頁
不必存在共享它的進程的虛擬內存空間的同一個地方。
 
3.1.4 Physical and Vitual Addressing Modes (物理和虛擬尋址模式)
 


    對於操作系統本身而言,運行在虛擬內存中沒有什麼意義。如果操作系統必須
維護自身的頁表,這將會是一場噩夢。大多數多用途的處理器同時支持物理地址模
式和虛擬地址模式。物理尋址模式不需要頁表,處理器在這種模式下不需要進行任
何地址轉換。 < Linux 核心運行在物理地址模式。
 
    Alpha AXP 處理器沒有特殊的物理尋址模式。它將內存空間分爲幾個區,將其
中兩個指定爲物理映射地址區。核心的地址空間叫做 < KSEG 地址空間,包括從 <
 0xfffffc0000000000 向上的所有地址。爲了執行連接在 KSEG 的代碼(核心代碼
)或者訪問那裏的數據,代碼必須在覈心態執行。 Alpha 上的 Linux 核心連接到
從地址 0xfffffc0000310000 執行。
 
 
 
3.1.5 Access Control (訪問控制)  
 
    頁表條目也包括訪問控制信息。當處理器使用頁表條目將進程的虛擬地址映射
到物理地址的時候,它很容易利用訪問控制信息控制進程不要用不允許的方式進行
訪問。
 
    有很多原因你希望限制對於內存區域的訪問。一些內存,比如包含執行代碼,
本質上是隻讀的代碼,操作系統應該禁止進程寫它的執行代碼。反過來,包括數據
的頁可以寫,但是如果試圖執行這段內存應該失敗。大多數處理器有兩種執行狀態


:核心態和用戶態。你不希望用戶直接執行核心態的代碼或者存取核心數據結構,
除非處理器運行在覈心態。
 
    訪問控制信息放在 PTE ( page table entry )中,而且和具體處理器相關
。圖 3.2 顯示了 Alpha AXP 的 PTE 。各個位意義如下:
 
V 有效,這個 < PTE 是否有效
 
FOE “ < Fault on Execute ” < 試圖執行本頁代碼時,處理器是否要報告 <
page fault ,並將控制權傳遞給操作系統。
 
FOW “ Fault on Write” 如上,在試圖寫本頁時產生 page fault
 
FOR “ < fault on read ” 如上,在試圖讀本頁時產生 page fault
 
ASM 地址空間匹配。用於操作系統清除轉換緩衝區中的部分條目
 
KRE 核心態的代碼可以讀本頁
 
URE 用戶態的代碼可以讀本頁
 
GII 間隔因子,用於將一整塊映射到一個轉換緩衝條目而非多個。


 
KWE 核心態的代碼可以寫本頁
 
UWE 用戶態的代碼可以寫本頁
 
Page frame number 對於 V 位有效的 PTE ,包括了本 PTE 的物理頁編號;對於
無效的 PTE ,如果不是 0 ,包括了本頁是否在交換文件的信息。
 
以下兩位由 Linux 定義並使用
 
_PAGE_DIRTY 如果設置,本頁需要寫到交換文件中。
 
_PAGE_ACCESSED Linux 使用,標誌一頁已經訪問過
 
3.2 Caches (高速緩存)
 
    如果你用以上理論模型來實現一個系統,它可以工作,但是不會太高效率。操
作系統和處理器的設計師都盡力讓系統性能更高。除了使用更快的處理器、內存等
,最好的方法是維護有用信息和數據的高速緩存,這會使一些操作更快。 < Linux
 使用了一系列和高速緩存相關的內存管理技術:
 
    Buffer Cache : Buffer cache 包含了用於塊設備驅動程序的數據緩衝區。


這些緩衝區大小固定(例如 512 字節),包括從塊設備讀出的數據或者要寫到塊
設備的數據。塊設備是隻能通過讀寫固定大小的數據塊來訪問的設備。所有的硬盤
都是塊設備。塊設備用設備標識符和要訪問的數據塊編號作爲索引,用來快速定位
數據塊。塊設備只能通過 < buffer cache 存取。如果數據可以在 < buffer
cache 中找到,那就不需要從物理塊設備如硬盤上讀取,從而使訪問加快。
 
參見 fs/buffer.c
 
 
    Page Cache 用來加快對磁盤上映像和數據的訪問。它用於緩存文件的邏輯內
容,一次一頁,並通過文件和文件內的偏移來訪問。當數據頁從磁盤讀到內存中時
,被緩存到 < page cache 中。
 
參見 mm/filemap.c
 
    Swap Cache 只有改動過的(或髒 dirty )頁才存在交換文件中。只要它們寫
到交換文件之後沒有再次修改,下一次這些頁需要交換出來的時候,就不需要再寫
到交換文件中,因爲該頁已經在交換文件中了,直接廢棄該頁就可以了。在一個交
換比較厲害的系統,這會節省許多不必要和高代價的磁盤操作。
 
參見 mm/swap_state.c mm/swapfile.c
 


 
 
Hardware Cache: 硬件高速緩存的常見的實現方法是在處理器裏面: PTE 的高速
緩存。這種情況下,處理器不需要總是直接讀頁表,而在需要時把頁轉換表放在緩
存區裏。 < CPU 裏有轉換表緩衝區 < (TLB Translation Look-aside Buffers)
,放置了系統中一個或多個進程的頁表條目的緩存的拷貝。
 
    當引用虛擬地址時,處理區試圖在 TLB 中尋找。如果找到了,它就直接將虛
擬地址轉換到物理地址,進而對數據執行正確的操作。如果找不到,它就需要操作
系統的幫助。它用信號通知操作系統,發生了 < TLB missing 。一個和系統相關
的機制將這個異常轉到操作系統相應的代碼來處理。操作系統爲這個地址映射生成
新的 < TLB 條目。當異常清除之後,處理器再次嘗試轉換虛擬地址,這一次將會
成功因爲 TLB 中該地址有了一個有效的條目。
 
    高速緩存的副作用(不管是硬件或其他方式的)在於 Linux 必須花大量時間
和空間來維護這些高速緩存區,如果這些高速緩存區崩潰,系統也會崩潰。
 
3.3 Linux Page Tables ( Linux 頁表)  
 
    Linux 假定了三級頁表。訪問的每一個頁表包括了下一級頁表的頁編號。圖
3.3 顯示了一個虛擬地址如何分爲一系列字段:每一個字段提供了在一個頁表中的
偏移量。爲了將虛擬地址轉換爲物理地址,處理器必須取得每一級字段的內容,轉


換爲包括該頁表的物理頁內的偏移,然後讀取下一級頁表的頁編號。重複三次直到
包括虛擬地址的物理地址的頁編號找到爲止。然後用虛擬地址中的最後一個字段:
字節偏移量,在頁內查找數據。
 
    Linux 運行的每一個平臺都必須提供轉換宏,讓核心處理特定進程的頁表。這
樣,核心不需要知道頁表條目的具體結構或者如何組織。通過這種方式, < Linux
 成功地使用了相同的頁表處理程序用於 < Alpha 和 Intel x86 處理器,其中
< Alpha 使用三級頁表,而 < Intel 使用二級頁表。
 
參見 include/asm/pgtable.h
 
3.4 Page Allocation and Deallocation ( 頁的分配和回收 )
 
    系統中對於物理頁有大量的需求。例如,當程序映像加載到內存中的時候,操
作系統需要分配頁。當程序結束執行並卸載時需要釋放這些頁。另外爲了存放核心
相關的數據結構比如頁表自身,也需要物理頁。這種用於分配和回收頁的機制和數
據結構對於維護虛擬內存子系統的效率也許是最重要的。
 
    系統中的所有的物理頁都使用 mem_map 數據結構來描述。這是一個
mem_map_t 結構的鏈表,在啓動時進行初始化。每一個 mem_map_t (容易混淆的
是這個結構也被稱爲 page 結構)結構描述系統中的一個物理頁。重要的字段(至
少對於內存管理而言)是:


 
參見 include/linux/mm.h
 
count 本頁用戶數目。如果本頁由多個進程共享,計數器大於 1 。
 
Age 描述本頁的年齡。用於決定本頁是否可以廢棄或交換出去。
 
Map_nr mem_map_t 描述的物理頁編號。
 
    頁分配代碼使用 free_area 向量來查找空閒的頁。整個緩衝管理方案用這種
機制來支持。只要用了這種代碼,處理器使用的頁的大小和物理頁的機制就可以無
關。
 
    每一個 free_area 單元包括頁塊的信息。數組中的第一個單元描述了單頁,
下一個是 2 頁大小的塊,下一個是 4 頁大小的塊,以此類推,依次向上都是 2
的倍數。這個鏈表單元用作隊列的開頭,有指向 mem_map 數組中頁的數據結構的
指針。空閒的頁塊在這裏排隊。 Map 是一個跟蹤這麼大小的頁的分配組的位圖。
如果頁塊中的第 < N 塊空閒,則位圖中的第 < N 位置位。
 
    圖 3.4 顯示了 free_area 結構。單元 0 有一個空閒頁(頁編號 0 ),單元
 2 有 2 個 4 頁的空閒塊,第一個起始於頁編號 4 ,第二個起始於頁編號 56 。
 


 
3.4.1 Page Allocation ( 頁分配 )
 
 
參見 mm/page_alloc.c get_free_pages()
 
    Linux 使用 < Buddy 算法有效地分配和回收頁塊。頁分配代碼試圖分配一個
由一個或多個物理頁組成的塊。頁分配使用 < 2 的冪數大小的塊。這意味着可以
分配 < 1 頁大小, < 2 頁大小, < 4 頁大小的塊,依此類推。只要系統有滿足
需要的足夠的空閒頁( nr_free_pages > min_free_pages ),分配代碼就會在 <
 free_area 中查找滿足需要大小的一個頁塊。 Free_area 中的每一個單元都有描
述自身大小的頁塊的佔用和空閒情況的位圖。例如,數組中的第 < 2 個單元擁有
描述 < 4 頁大小的塊的空閒和佔用的分配圖。
 
    這個算法首先找它請求大小的內存頁塊。它跟蹤 free_area 數據結構中的
list 單元隊列中的空閒頁的鏈表。如果請求大小的頁塊沒有空閒,就找下一個尺
寸的塊( < 2 倍於請求的大小)。繼續這一過程一直到遍歷了所有的
free_area 或者找到了空閒頁塊。如果找到的頁塊大於請求的頁塊,則該塊將被分
開成爲合適大小的塊。因爲所有的塊都是 < 2 的冪次的頁數組成,所以這個分割
的過程比較簡單,你只需要將它平分就可以了。空閒的塊則放到適當的隊列,而分
配的頁塊則返回給調用者。
 


 
 
    例如在圖 3.4 中,如果請求 2 頁的數據塊,第一個 4 頁塊(起始於頁編號
 4 )將會被分爲兩個 2 頁塊。起始於頁號 4 的第一個 2 頁塊將會被返回給調用
者,而第二個 2 頁塊(起始於頁號 6 )將會排在 free_area 數組中的單元 1 中
 2 頁空閒塊的隊列中。
 
3.4.2 Page Deallocation (頁回收)
 
    分配頁塊的過程中將大的頁塊分爲小的頁塊,將會使內存更爲零散。頁回收的
代碼只要可能就把頁聯成大的頁塊。其實頁塊的大小很重要( < 2 的冪數),因
爲這樣才能很容易將頁塊組成大的頁塊。
 
    只要一個頁塊回收,就檢查它的相鄰或一起的同樣大小的頁塊是否空閒。如果
是這樣,就把它和新釋放的頁塊一起組成以一個新的下一個大小的空閒頁塊。每一
次兩個內存頁塊組合成爲更大的頁塊時,頁回收代碼都要試圖將頁塊合併成爲更大
的塊。這樣,空閒的頁塊就會儘可能的大。
 
    例如,在圖 3.4 ,如果頁號 1 釋放,那麼它會和已經空閒的頁號 0 一起組
合併放在 free_area 的單元 1 中空閒的 2 頁塊隊列中。
 
3.5 Memory Mapping (內存映射)


 
    當一個映像執行時,執行映像的內容必須放在進程的虛擬地址空間中。對於執
行映像連接到的任意共享庫,情況也是一樣。執行文件實際並沒有放到物理內存,
而只是被連接到進程的虛擬內存。這樣,只要運行程序引用了映像的部分,這部分
映像就從執行文件中加載到內存中。這種映像和進程虛擬地址空間的連接叫做內存
映射。
 
    每一個進程的虛擬內存用一個 mm_struct 數據結構表示。這包括當前執行的
映像的信息(例如 bash )和指向一組 vm_area_struct 結構的指針。每一個 <
vm_area_struct 的數據結構都描述了內存區域的起始、進程對於內存區域的訪問
權限和對於這段內存的操作。這些操作是一組例程, < Linux 用於管理這段虛擬
內存。例如其中一種虛擬內存操作就是當進程試圖訪問這段虛擬內存時發現(通過
 page fault )內存不在物理內存中所必須執行的正確操作,這個操作叫做 
nopage 操作。 Linux 請求把執行映像的頁加載到內存中的時候用到 nopage 操作

 
    當一個執行映像映射到進程的虛擬地址空間時,產生一組 <
vm_area_struct 數據結構。每一個 < vm_area_struct 結構表示執行映像的一部
分:執行代碼、初始化數據(變量)、未初始化數據等等。 < Linux 支持一系列
標準的虛擬內存操作,當 < vm_area_struct 數據結構創建時,一組正確的虛擬內
存操作就和它們關聯在一起。
 


3.6 Demand Paging
 
    只要執行映像映射到進程的虛擬內存中,它就可以開始運行。因爲只有映像的
最開始的部分是放在物理內存中,很快就會訪問到還沒有放在物理內存的虛擬空間
區。當進程訪問沒有有效頁表條目的虛擬地址的時候,處理器向 < Linux 報告
page fault 。 Page fault 描述了發生 page fault 的虛擬地址和內存訪問類型

 
    Linux 必須找到 < page fault 發生的空間區所對應的 < vm_area_struct 數
據結構(用 < Adelson-Velskii and Landis AVL 樹型結構連接在一起)。如果找
不到這個虛擬地址對應的 vm_area_struct 結構,說明進程訪問了非法的虛擬地址
。 < Linux 將向該進程發信號,發送一個 < SIGSEGV 信號,如果進程沒有處理這
個信號,它就會退出。
 
參見 handle_mm_fault() in mm/memory.c
 
    Linux 然後檢查 < page faul 的類型和該虛擬內存區所允許的訪問類型。如
果進程用非法的方式訪問內存,比如寫一個它只可以讀的區域,也會發出內存錯的
信號。
 
    現在 Linux 確定 page fault 是合法的,它必須進行處理。 Linux 必須區分
在交換文件和磁盤映像中的頁,它用發生 page fault 的虛擬地址的頁表條目來確


定。
 
參見 do_no_page() in mm/memory.c
 
    如果該頁的頁表條目是無效的但非空,此頁是在交換文件中。對於 Alpha AXP
 頁表條目來講,有效位置位但是 < PFN 域非空。這種情況下 < PFN 域存放了此
頁在交換文件(以及那一個交換文件)中的位置。頁在交換文件中如何處理在本章
後面討論。
 
    並非所有的 vm_area_struct 數據結構都有一整套虛擬內存操作,而且那些有
特殊的內存操作的也可能沒有 < nopang 操作。因爲缺省情況下,對於 <
nopage 操作, < Linux 會分配一個新的物理頁並創建有效的頁表條目。如果這一
段虛擬內存有特殊的 nopage 操作, Linux 會調用這個特殊的代碼。
 
    通常的 Linux nopage 操作用於對執行映像的內存映射,並使用 page
cache 將請求的映像頁加載到物理內存中。雖然在請求的頁調入的物理內存中以後
,進程的頁表得到更新,但是也許需要必要的硬件動作來更新這些條目,特別是如
果處理器使用了 < TLB 。既然 page fault 得到了處理,就可以扔在一邊,進程
在引起虛擬內存訪問錯誤的指令那裏重新運行。
 
參見 mm/filemap.c 中 filemap_nopage()
 


 
 
 
 
3.7 The Linux Page Cache
 
    Linux 的 < page cache 的作用是加速對於磁盤文件的訪問。內存映射文件每
一次讀入一頁,這些頁被存放在 page cache 中。圖 < 3.6 顯示了 page cache
,包括一個指向 < mem_map_t 數據結構的指針向量: < page_hash_table 。 <
Linux 中的每一個文件都用一個 < VFS inode 的數據結構標示(在第 < 9 章描述
),每一個 VFS I 節點都是唯一的並可以完全確定唯一的一個文件。頁表的索引
取自 VFS 的 I 節點號和文件中的偏移。
 
參見 linux/pagemap.h
 
    當一頁的數據從內存映射文件中讀出,例如當 demand paging 時需要放到內
存中的時候,此頁通過 < page cache 中讀出。如果此頁在緩存中,就返回一個指
向 mem_map_t 數據結構的指針給 page fault 的處理代碼。否則,此頁必須從存
放此文件的文件系統中加載到內存中。 < Linux 分配物理內存並從磁盤文件中讀
出該頁。如果可能, Linux 會啓動對文件下一頁的讀。這種單頁的超前讀意味着
如果進程從文件中順序讀數據的話,下一頁數據將會在內存中等待。
 


    當程序映像讀取和執行的時候 page cache 不斷增長。如果頁不在需要,將從
緩存中刪除。比如不再被任何進程使用的映像。當 < Linux 使用內存的時候,物
理頁可能不斷減少,這時 Linux 可以減小 page cache 。
 
3.8 Swapping out and Discarding Pages (交換出去和廢棄頁)
 
    當物理內存缺乏的時候, Linux 內存管理子系統必須試圖釋放物理頁。這個
任務落在覈心交換進程上( < kswapd )。核心交換守護進程是一種特殊類型的進
程,一個核心線程。核心線程是沒有虛擬內存的進程,以核心態運行在物理地址空
間。核心交換守護進程名字有一點不恰當,因爲它不僅僅是將頁交換到系統交換文
件上。它的任務是保證系統有足夠的空閒頁,使內存管理系統有效地運行。
 
    核心交換守護進程( kswapd )在啓動時由核心的 init 進程啓動,並等待覈
心的交換計時器到期。每一次計時器到期,交換進程檢查系統中的空閒頁數是否太
少。它使用兩個變量: < free_pages_high 和 < free_pages_low 來決定是否釋
放一些頁。只要系統中的空閒頁數保持在 free_pages_high 之上,交換進程什麼
都不做。它重新睡眠直到它的計時器下一次到期。爲了做這種檢查,交換進程要考
慮正在向交換文件中寫的頁數,用 < nr_async_pages 來計數:每一次一頁排到隊
列中等待寫到交換文件中的時候增加,寫完的時候減少。 < Free_page_low 和
< free_page_high 是系統啓動時間設置的,和系統中的物理頁數相關。如果系統
中的空閒頁數小於 < free_pages_high 或者比 < free_page_low 還低,核心交換
進程會嘗試三種方法來減少系統使用的物理頁數:


 
參見 mm/vmscan.c 中的 kswapd()
 
減少 buffer cache 和 page cache 的大小
 
將系統 V 的共享內存頁交換出去
 
交換和廢棄頁
 
    如果系統中的空閒頁數低於 free_pages_low ,核心交換進程將試圖在下一次
運行前釋放 6 頁。否則試圖釋放 3 頁。以上的每一種方法都要被嘗試直到釋放了
足夠的頁。核心交換進程記錄了它上一次使用的釋放物理頁的方法。每一次運行時
它都會首先嚐試上一次成功的方法來釋放頁。
 
    釋放了足夠的頁之後,交換進程又一次睡眠,直到它的計時器又一次過期。如
果核心交換進程釋放頁的原因是系統空閒頁的數量少於 < free_pages_low ,它只
睡眠平時的一半時間。只要空閒頁數大於 free_pages_low ,交換進程就恢復原來
的時間間隔進行檢查。
 
3.8.1 Reducing the size of the Page and Buffer Caches
 
    page 和 < buffer cache 中的頁是釋放到 < free_area 向量中的好選擇。 <


 Page Cache ,包含了內存映射文件的頁,可能有不必要的數據,佔去了系統的內
存。同樣, Buffer Cache ,包括了從物理設備讀或向物理設備寫的數據,也可能
包含了無用的緩衝。當系統中的物理頁將要耗盡的時候,廢棄這些緩存區中的頁相
對比較容易,因爲它不需要向物理設備寫(不象將頁從內存中交換出去)。廢棄這
些頁不會產生多少有害的副作用,只不過使訪問物理設備和內存映射文件時慢一點
。雖然如此,如果公平地廢棄這些緩存區中的頁,所有的進程受到的影響就是平等
的。
 
    每一次當核心交換進程要縮小這些緩存區時,它要檢查 mem_map 頁矢量中的
頁塊,看是否可以從物理內存中廢棄。如果系統空閒頁太低(比較危險時)而核心
交換進程交換比較厲害,這個檢查的頁塊大小就會更大一些。頁塊的大小進行循環
檢查:每一次試圖減少內存映射時都用一個不同的頁塊大小。這叫做 < clock 算
法,就象鐘的時針。整個 < mem_map 頁向量都被檢查,每次一些頁。
 
參見 mm/filemap.c shrink_map()
 
    檢查的每一頁都要判斷緩存在 page cache 或者 buffer cache 中。注意共享
頁的廢棄這時不考慮,一頁不會同時在兩個緩存中。如果該頁不在這兩個緩衝區中
,則 < mem_map 頁向量表的下一頁被檢查。
 
    緩存在 buffer cache ch 中的頁(或者說頁中的緩衝區被緩存)使緩衝區的
分配和釋放更有效。縮小內存映射的代碼試圖釋放包含檢查過的頁的緩衝區。如果


緩衝區釋放了,則包含緩衝區的頁也被釋放了。如果檢查的頁是在 < Linux 的
page cache 中,它將從 page cache 中刪除並釋放。
 
參見 fs/buffer.c free_buffer()
 
    如果這次嘗試釋放了足夠的頁,核心交換進程就會繼續等待直到下一次被週期
性地喚醒。因爲釋放的頁不屬於任何進程的虛擬內存(只是緩存的頁),因此不需
要更新進程的頁表。如果廢棄的緩存頁仍然不夠,交換進程會試圖交換出一些共享
頁。
 
3.8.2 Swapping Out System V Shared Memory Pages (交換出系統 V 的共享內
存頁)
 
    系統 V 的共享內存是一種進程間通訊的機制,通過兩個或多個進程共享虛擬
內存交換信息。進程間如何共享內存在第 < 5 章詳細討論。現在只要講講每一塊
系統 < V 共享內存都用一個 < shmid_ds 的數據結構描述就足夠了。它包括一個
指向 vm_area_struct 鏈表數據結構的指針,用於共享此內存的每一個進程。 <
Vm_area_struct 數據結構描述了此共享內存在每一個進程中的位置。這個系統
V 的內存中的每一個 vm_area_struct 結構都用 vm_next_shared 和
vm_prev_shared 指針連接在一起。每一個 shmid_ds 數據結構都有一個頁表條目
的鏈表,每一個條目都描述一個共享的虛擬頁和物理頁的對應關係。
 


    核心交換進程將系統 V 的共享內存頁交換出去時也用 clock 算法。它每一次
運行都記錄了上一次交換出去了那一塊共享內存的那一頁。它用兩個索引來記錄:
第一個是 < shmid_ds 數據結構數組中的索引,第二個是這塊共享內存區的頁錶鏈
中的索引。這樣可以共享內存區的犧牲比較公平。
 
參見 ipc/shm.c shm_swap()
 
    因爲一個指定的系統 V 共享內存的虛擬頁對應的物理頁號包含在每一個共享
這塊虛擬內存的進程的頁表中,所以核心交換進程必須修改所有的進程的頁表來體
現此頁已經不在內存而在交換文件中。對於每一個交換出去的共享頁,交換進程必
須找到在每一個共享進程的頁表中對應的此頁的條目(通過查找每一個 <
vm_area_struct 指針)如果在一個進程頁表中此共享內存頁的條目有效,交換進
程要把它變爲無效,並且標記是交換頁,同時將此共享頁的在用數減 < 1 。交換
出去的系統 < V 共享頁表的格式包括一個在 < shmid_ds 數據結構組中的索引和
在此共享內存區中頁表條目的索引。
 
    如果所有共享的內存都修改過,頁的在用數變爲 0 ,這個共享頁就可以寫到
交換文件中。這個系統 V 共享內存區的 shmid_ds 數據結構指向的頁表中此頁的
條目將會換成交換出的頁表條目。交換出的頁表條目無效但是包含一個指向打開的
交換文件的索引和此頁在此文件內的偏移量。這個信息用於將此頁再取回物理內存
中。
 


3.3 Swapping Out and Discarding Pages
 
    交換進程輪流檢查系統中的每一個進程是否可以用於交換。好的候選是可以交
換的進程(有一些不行)並且有可以從內存中交換出去或廢棄的一個或多個頁。只
有其他方法都不行的時候纔會把頁從物理內存交換到系統交換文件中。
 
參見 mm/vmscan.c swap_out()
 
    來自於映像文件的執行映像的大部分內容可以從文件中重新讀出來。例如:一
個映像的執行指令不會被自身改變,所以不需要寫到交換文件中。這些頁只是被簡
單地廢棄。如果再次被進程引用,可以從執行映像再次加載到內存中。
 
    一旦要交換的進程確定下來,交換進程就查看它的所有虛擬內存區域,尋找沒
有共享或鎖定的區域。 < Linux 不會把選定進程的所有可以交換出去的頁都交換
出去,而只是去掉少量的頁。如果頁在內存中鎖定,則不能被交換或廢棄。
 
    參見 mm/vmscan.c swap_out_vme() 跟蹤進程 mm_struct 中排列的
vm_area_struct 結構中的 vm_next vm_nex 指針。
 
    Linux 的交換算法使用了頁的年齡。每一個頁都有一個計數器(放在
mem_map_t 數據結構中),告訴核心交換進程此頁是否值得交換出去。頁不用時變
老,訪問時更新。交換進程只交換老的頁。缺省地,頁第一次分配時年齡賦值爲 <


 3 。每一次訪問,它的年齡就增加 < 3 ,直到 < 20 。每一次系統交換進程運行
時它將頁的年齡減 < 1 使頁變老。這個缺省的行爲可以更改,所以這些信息(和
其他相關信息)都存放在 < swap_control 數據結構中。
 
    如果頁太老 ( 年齡 age = 0) ,交換進程會進一步處理。髒頁可以交換出去
, Linux 在描述此頁的 PTE 中用一個和體系結構相關的位來描述這種頁(見圖
3.2 )。但是,並非所有的髒頁都需要寫到交換文件。每一個進程的虛擬內存區域
都可以擁有自己的交換操作(由 < vm_area_struct 中的 < vm_ops 指針指示),
如果這樣,交換進程會用它的這種方式。否則,交換進程會從交換文件中分配一頁
,並把此頁寫到該文件中。
 
    此頁的頁表條目會用一個無效的條目替換,但是包括了此頁在交換文件的信息
:此頁所在文件內的偏移和所用的交換文件。不管什麼方式交換,原來的物理頁被
放回到 < free_area 重釋放。乾淨(或不髒)的頁可以被廢棄,放回到
free_area 中重用。
 
    如果交換或廢棄了足夠的可交換進程的頁,交換進程重新睡眠。下一次喚醒時
它會考慮系統中的下一個進程。這樣,交換進程輕咬去每一個進程的物理頁,直到
系統重新達到平衡。這種做法比交換出整個進程更公平。
 
3.9 The Swap Cache (交換緩存)
 


    當把頁交換到交換文件時, Linux 會避免寫不必要寫的頁。有時可能一個頁
同時存在於交換文件和物理內存中。這發生於一頁被交換出內存然後在進程要訪問
時又被調入內存的情況下。只要內存中的頁沒有被寫過,交換文件中的拷貝就繼續
有效。
 
    Linux 用 < swap cache 來記錄這些頁。交換緩存是一個頁表條目或者系統物
理頁的鏈表。一個交換頁有一個頁表條目,描述使用的交換文件和它在交換文件中
的位置。如果交換緩存條目非 < 0 ,表示在交換文件中的一頁沒有被改動。如果
此頁後來被改動了(被寫),它的條目就從交換緩存中刪除)
 
    當 Linux 需要交換一個物理頁到交換文件的時候,它查看交換緩存,如果有
此頁的有效條目,它不需要把此頁寫到交換文件。因爲內存中的此頁從上次讀到交
換文件之後沒有被修改過。
 
    交換緩存中的條目是曾經交換出去的頁表條目。它們被標記爲無效,但是包含
了允許 < Linux 找到正確交換文件和交換文件中正確頁的信息。
 
3.10 Swapping Page In (交換進)
 
    保存在交換文件中的髒頁可能又需要訪問。例如:當應用程序要向虛擬內存中
寫數據,而此頁對應的物理頁交換到了交換文件時。訪問不在物理內存的虛擬內存
頁會引發 < page fault 。 < Page fault 是處理器通知操作系統它不能將虛擬內


存轉換到物理內存的信號。因爲交換出去後虛擬內存中描述此頁的頁表條目被標記
爲無效。處理器無法處理虛擬地址到物理地址的轉換,將控制轉回到操作系統,告
訴它發生錯誤的虛擬地址和錯誤的原因。這個信息的格式和處理器如何把控制轉回
到操作系統是和處理器類型相關的。處理器相關的 < page faule 處理代碼必須定
位描述包括出錯虛擬地址的虛擬內存區的 vm_area_struct 的數據結構。它通過查
找該進程的 < vm_area_struct 數據結構,直到找到包含了出錯的虛擬地址的那一
個。這是對時間要求非常嚴格的代碼,所以一個進程的 < vm_area_struct 數據結
構按照特定的方式排列,使這種查找花費時間儘量少。
 
參見 arch/i386/mm/fault.c do_page_fault()
 
    執行了合適的和處理器相關的動作並找到了包括錯誤(發生)的虛擬地址的有
效的虛擬內存, < page fault 的處理過程又成爲通用的,並可用於 Linux 能運
行的所有處理器。通用的 page fault 處理代碼查找錯誤虛擬地址的頁表條目。如
果它找到的頁表條目是交換出去的頁, < Linux 必須把此頁交換回物理內存。交
換出去的頁的頁表條目的格式和處理器相關,但是所有的處理器都將這些頁標爲無
效並在頁表條目中放進了在交換文件中定位頁的必要信息。 < Linux 使用這種信
息把此頁調回到物理內存中。
 
參見 mm/memory.c do_no_page()
 
    這時, Linux 知道了錯誤(發生)的虛擬地址和關於此頁交換到哪裏去的頁


內存中的頁交換回到物理內存中。這是 < swapin 操作。如果這塊內存中有 <
swapin 操作, < Linux 會使用它。其實,交換出去的系統 < V 的共享內存之所
以需要特殊的處理因爲交換的系統 V 的共享內存頁的格式和普通交換頁的不同。
如果沒有 swapin 操作, Linux 假定這是一個普通頁,不需要特殊的處理。它分
配一塊空閒的物理頁並將交換出去的頁從交換文件中讀進來。關於從交換文件哪裏
(和哪一個交換文件)的信息取自無效的頁表條目。
 
參見 mm/page_alloc.c swap_in()
 
    如果引起 page fault 的訪問不是寫訪問,頁就留在交換緩存中,它的頁表條
目標記爲不可寫。如果後來此頁又被寫,會產生另一個 < page fault ,這時,此
頁被標誌爲髒頁,而它的條目也從交換緩存中刪除。如果此頁沒有被修改而又需要
交換出來, < Linux 就可以避免將此頁寫到交換文件,因爲此頁已經在交換文件
中了。
 
    如果將此頁從交換文件調回的訪問是寫訪問,這個頁就從交換緩存中刪除,此
頁的頁表條目頁標記爲髒頁和可寫。


發佈了0 篇原創文章 · 獲贊 0 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章