第 3 章 記憶體管理


記憶體管理子系統是作業系統中最重要的組成部份之一。從早期電腦開始,系統的 實際記憶體總是不能滿足需求,為解決這一矛盾,人們想了許多辦法,其中虛擬記憶體是最 成功的一個。虛擬記憶體讓各程序共享系統記憶體空間, 這樣系統就似乎有了更多的記憶體。 虛擬記憶體不僅使電腦的記憶體看起來更多,記憶體管理子系統還提供以下功能: 擴大位址空間 作業系統擴大了系統的記憶體空間。虛擬記憶體能比系統的實際記憶體大許多倍。 記憶體保護 系統中每個程序都有它自己的虛擬位址空間。這些虛擬位址空間之間彼此分開,以 保証應用程式運行時互不影響。另外,虛擬記憶體機制可以對記憶體部份區域提供寫保護,以 防止程式碼和資料被其它惡意的應用程式所篡改。 記憶體映射 記憶體映射被用於將映像和資料檔案映射到一個程序的虛擬位址空間中, 也就是將 檔案內容連接到虛擬位址中。 公平分配記憶體 記憶體管理子系統公平地分配記憶體給正在運行的各程序。 虛擬記憶體共享 盡管虛擬記憶體允許各程序有各自的 ( 虛擬 ) 位址空間,但有時程序間需要共享記憶體。 例如,若干程序同時運行 Bash 命令。並非在每個程序的虛擬位址空間中,都有一個 Bash的拷貝。在記憶體中僅有一個運行的Bash拷貝供各程序共享。又如,若干程序可 以共享動態函數函式庫。 共享記憶體也能作為一種程序間的通信機制(IPC)。兩個或兩個以上程序可以通過共享 記憶體來交換資料。Linux 支援 Unix 系統 V 的共享記憶體 IPC標準。 3.1 一個抽像的虛擬記憶體模型
圖 3.1 :虛擬位址與實體位址之間的映射
在分析 Linux 實現虛擬記憶體的方法前,讓我們先來看一個沒有過多細節的抽像模型。 當處理器執行一段程式時,它先從記憶體中讀出一條指令並對它進行解碼。解碼時可 能需要在記憶體中的某一位址存取資料。然後處理器執行這條指令並移向下一條。可 見處理器總是不斷地在記憶體中存取資料或指令。 在虛擬記憶體系統中,所有位址都是虛擬位址而非實體位址。處理器根據作業系統中的一組 表格而把這些虛擬位址翻譯成相應的實體位址。 為使這翻譯的過程更容易,虛擬記憶體和實體記憶體被劃分成許多適當大小的塊,叫做“頁” (page)。為便於系統管理,這些頁都是一樣大小的。在 Alpha AXP 上的 Linux 系 統中,每頁有 8 Kbyte,但在 Intel x86 系統中,每頁有 4 Kbyte。每一頁又被分 配了一個各不相同的數字,叫頁號 ( PFN ) 。 在本模型中,一個虛擬位址由兩部份組成﹔偏移量和虛擬頁號。如果頁的大小是 4 Kbytes, 那麼虛擬位址的0至11位是偏移量,第12位以上是虛擬頁號。每次處理器遇到虛擬位址時, 它先取出偏移量和虛擬頁號。然後,處理器把虛擬頁號翻譯成實體頁號,再由偏移量得 到正確的實體位址,最後存取資料。處理器需要使用頁表來完成這整個過程。. 圖 3.1 顯示了兩個程序的虛擬記憶體位址空間。程序 X 和程序 Y 分別有各自的頁表。頁 表記錄各程序虛頁和實體頁之間的映射。如圖:X 虛擬記憶體的第0頁對應實體位址的第4頁。 理論上,頁表中每條記錄包含以下信息: -有效性標誌。 用以標識頁表記錄有效與否。 -頁號。用以記錄對應的實體記憶體頁號。 - 存取控制信息。描述這頁該怎樣被使用。可否可寫?可否包含可執行程式碼? 頁表中使用虛擬頁號作為偏移量。虛頁 5 將是表中的第 6 條記錄( 0 是第一條記錄 )。 把一個虛擬位址翻譯成實體位址時, 處理器必須先得出虛擬頁號和偏移量。頁的大小總 是 2 的冪,這便於進行mask和移位操作。圖 3.1中, 假定頁的大小是 0x2000 字 節 ( 它是十進制的 8192 ),在程序 Y的位址空間中有一虛擬位址 0x2194。那麼處理 器將把這個位址翻譯成偏移量為 0x194,虛擬頁號為1。 處理器使用虛擬頁號作為檢索程序頁表記錄的索引。如果對應那偏移量的頁表記錄是 有效的,處理器就從中拿出實體頁號。如果記錄是無效的,表明程序想存取一個不 在實體記憶體中的位址。在這種情況下,處理器不能翻譯這虛擬位址,必須把控制權傳 給作業系統,讓它處理。 當程序試圖存取一個無法翻譯的虛擬位址時,處理器將通知作業系統, 這被稱為一個 頁錯。各種處理器處理頁錯的方法是不同的,但都會通知作業系統產生頁錯的虛地 址和原因。 假設找到的是一有效的頁表記錄,處理器就取出實體頁號並且乘以頁的大小,得到 記憶體中頁的基位址。最後,處理器加上偏移量得到它需要資料的位址。 例如, 程序 Y 的虛擬記憶體第1頁被映射到記憶體第 4頁,它從 0x8000(4 x 0x2000 )開始。 加上偏移量 0x194 位元就得到最後的實體位址是 0x8194。 由虛擬位址映射到實體位址時, 虛擬記憶體各頁映射到系統記憶體中的順序是任意的。例如, 在圖 3.1 中, 程序 X 的虛擬記憶體第 0 頁被映射到記憶體第1頁,而虛擬記憶體第 7頁被映射到 記憶體第 0頁。這說明了虛擬記憶體的一個有趣現像,虛擬記憶體各頁在實體記憶體中不必有任何順 序。 3.1.1 按需裝載頁(Demanding Paging) 虛擬記憶體比實際記憶體大很多,所以作業系統一定要小心有效地使用記憶體。節省記憶體的一 個方法是只裝載被目前執行程式使用的虛頁。例如,有一個用來查詢資料函式庫的程式。 此時,並非所有資料函式庫中的資料都需被裝載進記憶體,只需要那些正在被存取的資料。 如果正運行一條資料函式庫搜索命令,那麼就不必載入添加新記錄的程式碼。當程式碼或數 據被存取時才裝載進記憶體,這叫作按需裝載頁(demand paging)。 當程序試圖存取一個不在記憶體中的虛擬位址時,處理器不可能在頁表中找到這一虛頁 的記錄。例如,在圖 3.1中, 程序 X 的虛擬記憶體第 2頁沒有對應的頁表記錄,如果嘗 試對這頁進行讀操作,那麼處理器不能把虛擬位址翻譯成實體位址。處理器就會通知 作業系統頁錯發生了。 如果頁錯(faulting) 對應的虛擬位址是無效的,這意味著程序試圖存取它不應該存取 的虛擬位址。這也許是因為應用程式出了某些錯誤, 例如試圖在記憶體中任意進行寫操 作。在這種情況下,作業系統將終止這個錯誤程序,以保護其它程序。 如果頁錯(faulting) 對應的虛擬位址是有效的,只是它所在頁目前不在記憶體中,操作 系統必須將對應的頁從磁碟載入記憶體。相對來說,磁碟存取會花很多時間,所以進 程必須等待相當一會兒直到頁被讀入。這時候,如果有其它程序能運行,作業系統 將選擇其中之一。被取的頁將被讀入記憶體一空頁中,並在程序頁表中加入一條記錄。 然後,程序從產生頁錯的機器指令重新啟動。這次處理器能將虛擬位址翻譯成實體地 址了,因此程序能繼續運行下去。 Linux 使用按需裝載頁來讀入可執行程序的映像。一個命令被執行時,包含它的文 件被打開,它的內容被印射入程序的虛擬記憶體。這操作需修改描述這程序記憶體映像的數 據結構 (memory mapping)。然而,只有映像的第一部份被實際載入實體記憶體,余下 部份被留在磁碟上。當映像執行時,它將不斷產生頁錯, Linux 使用程序的記憶體映 像表來決定哪塊映像該被載入記憶體。 3.1.2 頁交換 (Swapping) 當程序要裝載一虛頁進實體記憶體時,如果得不到空頁, 作業系統必須從記憶體中丟棄 別的頁,為這頁提供空間。 如果從記憶體中被丟棄的那頁是從映像或資料檔案中來的,並且映像和資料檔案沒被 修改過,那這頁不需再被保存,可以直接丟掉。如果程序再需要那頁,它可以重新 被從映像或資料檔案中讀入記憶體。 但如果該頁已被修改了,作業系統必須保存這頁的內容以便它以後能再被存取。這 類頁叫作臟 (dirty) 頁,當它們被從記憶體中移出時,它們被作為特殊的交換檔 (swap file) 保存。相對於處理器和記憶體的速度,交換檔的存取時間是很長的, 所以作業系統必須權衡是否需要把頁寫到磁碟上,還是保留在記憶體中以備後用。 如果交換演算法的效率不高,那麼thrashing現像就會發生。在這種情況下,頁常常一 會兒被寫到磁碟上,一會兒又被讀回來,作業系統忙於檔案存取而不能執行真正的 工作。例如,圖3.1 中,如果記憶體第 1頁不斷被存取,那它就不應該被交換到硬碟 上。程序目前正在使用的頁的集合被叫作工作集 (working set)。有效的交換演算法 將保証所有程序的工作集都在記憶體中。 Linux 使用最近最少使用演算法(Least Recently Used) 來公平選擇從記憶體中被丟棄 的頁。這個演算法中,當頁被存取時,它的年齡 (aging) 就變化了。頁越多被存取, 便越年輕﹔越少被存取就越舊。舊頁通常是被丟棄的好候選。 3.1.3 共享虛擬記憶體 虛擬記憶體使得若干程序更容易共享記憶體。程序所有的記憶體存取都要通過頁表,並且各進 程有各自獨立的頁表。當多程序共享記憶體中一頁時,實體頁號就會同時出現在每個 程序的頁表中。 圖3.1 中顯示兩程序共享實體第4頁。對程序 X 而言,那是虛擬記憶體的第 4頁,對程序 Y而言, 那是虛擬記憶體第 6頁。這說明一個有趣的現像:被共享的實體頁對應的虛擬記憶體頁號可以各 不相 同。 3.1.4 實體和虛擬位址模式 把作業系統運行在虛擬記憶體中是不明智之舉,如果作業系統還要為自己保存頁表,那將 是一場惡夢。因此,很多種處理器同時支援虛擬位址模式和實體位址模式。實體地 址模式不需要頁表,處理器不必做任何位址翻譯。 Linux 核心被直接連在實體位址 空間中運行。 Alpha AXP 處理器沒有實體位址模式。相反, 它把記憶體劃分成若干區域並且指定其 中兩 塊為實體位址區。這段核心位址空間叫作KSEG,包括所有0xfffffc0000000000以上的 位址。 在 KSEG執行的 (按定義,核程式碼 ) 或在那裡存取資料的程式碼肯定是在核心模式下執 行。在 Alpha 上的 Linux 核被連接從0xfffffc0000310000開始執行。 3.1.5 存取控制 頁表記錄中也包含了存取控制信息。處理器使用頁表記錄來把虛擬位址翻譯成實體地 址的同時,它也很容易地使用其中的存取控制信息來檢查程序是否在正確地存取記憶體。 在很多種情況下,你想要為記憶體的一段區域設置存取限制。一段記憶體, 例如包含可 執行的程式碼, 應為只讀記憶體﹔作業系統應該不允許程序在它的可執行的程式碼上寫資料。 相反的,包含資料的頁能被寫,但是當指令試圖執行那段記憶體時,應該失敗。 大多數處理器的執行程式碼有兩種模式:核 態和使用者模式。你將不想由一個用戶執行核 程式碼,或者讓核資料結構被不是核態執行的程式碼所存取。
圖 3.2 : Alpha AXP 的頁表記錄 (Page Table Entry)
存取控制信息被保存在 PTE中,並且不同的處理器,PTE的格式是不同的﹔圖3.2 顯 示的是 Alpha AXP 的PTE。各位包含以下信息: V 有效位。如果設置,表示這 PTE 是有效的。 FOE (Fault on Execute) 無論何時試圖在這頁執行指令時,處理器將報告頁錯, 並且把控制權傳給作業系統。 FOW (Fault on Write) 當在這頁上進行寫操作時報頁錯。 FOR (Fault on Read) 當在這頁上進行讀操作時報頁錯。 ASM(Address Space Match) 位址空間匹配。當作業系統僅僅希望清除翻譯緩衝區中 若干記錄時,這一位被使用。 KRE 在核心模式下運行的程式碼能讀這頁。 URE 在用戶模式下運行的程式碼能讀這頁。 GH 粒度性,指在映射一整塊虛擬記憶體時,是用一個翻譯緩衝記錄還是多個。 KWE 在核心模式下運行的程式碼能寫這頁。 UWE 在用戶模式下運行的程式碼能寫這頁。 頁號 在有效的PFE中, 這域包含對應的實體頁號 (page frame number )。對無效 的PTEs ,如果這域不是零,它包含了頁在交換檔中的信息。 以下兩位是 Linux 定義並使用的: _PAGE_DIRTY 如果設置,頁需要被寫到交換檔中。 _PAGE_ACCESSED 由 Linux 標記這頁是否曾被存取。 3.2 快取 如果你按照上面理論模型,可以實現一個工作的系統,但不會特別高效。作業系統 和處理器的設計者都在努力提高系統性能。除提高處理器和記憶體的速度外,最好的 途徑是把有用的信息和資料保存在快取中。 Linux 就使用了很多與記憶體管理有關的 快取: 緩衝區 緩衝區包含區塊設備驅動程式 (block device driver) 使用的資料緩衝區。 這些緩衝區有固定的大小 ( 例如 512 個位元 ) ,記錄從一台區塊設備讀或寫的信息。 一台區塊設備只能存取整塊資料。所有的硬碟都是區塊設備。 緩衝區通過設備辨識器和需要的塊號的索引來迅速發現所需資料。區塊設備只能通過 緩衝區進行存取操作。如果資料在緩衝區中,那麼它就不需要再從區塊設備中被讀(例 如硬碟),這樣存取得更快。 頁快取 它被用來加快磁碟上映像和資料的存取。 它被用來一次快取檔案的一頁,存取操作通過檔案名和偏移量來實現。當頁從磁碟 被讀進記憶體時,他們被快取在頁快取中。 交換快取 只有修改了的頁,即臟(dirty ) 頁,被保存在交換檔中。 只要一頁在被寫進交換檔以後,沒有再被修改,下次這頁被換出記憶體時,可以直 接被扔掉。對一個進行許多頁面交換的系統,這將節省許多不必要的並且昂貴的磁 盤操作。 硬體快取 處理器中有一經常用到的硬體快取:頁表記錄的快取。通常情況下,處理器並不總 是直接讀頁表,而是用頁表快取保留用到的記錄。這些被叫做 Translation Look-aside Buffer,保存了系統中多個程序頁表的拷貝。 當翻譯位址時,處理器先試圖找到一匹配的TLB 記錄。如果它發現了一個,它能直 接把虛擬位址翻譯成實體位址,並且對資料進行存取操作。如果處理器不能發現一匹 配的 TLB 記錄,那就必須借助作業系統。它發信號給作業系統,報告有一個 TLB 疏漏。特定的機制將把異常信號送給作業系統的程式碼。作業系統為印射的位址產生 一個新的 TLB 記錄。當異常被解決後,處理器將嘗試再翻譯那個虛擬位址。因為現在 那個位址在 TLB 中有一個有效的記錄,這次的位址翻譯一定成功。 使用緩衝區,硬體快取等的缺點是Linux 必須花費更多的時間和空間來維護這些緩 存, 如果快取發生錯誤,系統將崩潰。 3.3 Linux 頁表
圖 3.3 : 3級頁表
Linux頁表有3層。每一層負責保存下一層頁表所在的頁號。圖3.3 顯示一個虛擬位址 被分成了很多域﹔每個域記錄在某一層頁表中的偏移量。把一個虛擬位址翻譯成實體 位址時,處理器拿出每個域的內容把它變成頁表中的偏移量,進而讀出下層頁表的 所在頁號。這樣重復 3 次直到找到包含虛擬位址的實體頁號。虛擬位址的最後一個域, 叫做位元偏移量, 被用來在實體頁內找到所需資料。 每個運行 Linux 的平台必須提供翻譯巨集(Translation macros) 以便核心可以檢索 頁表,完成某種操作。這樣,核心不需要知道各平台上頁表記錄的具體格式和它們 是怎麼被安排的。 這就是為什麼 Linux 的 Alpha 處理器和Intel x 86 處理器使用一樣的頁表操作代 碼, 而Alpha有3層頁表,Intel x86處理器只有2層頁表。 3.4 頁的分配和回收 在系統中,對頁有許多操作。例如, 當一段映像被裝載進記憶體時,作業系統需要分 配頁。當映像執行完成並且被卸掉時,這些頁將被釋放。頁的另外的用途是保存內 核特定的資料結構,例如頁表。頁的分配和回收機制是維持虛擬記憶體分系統效率的關鍵。 系統中所有實體記憶體頁由 mem_map 資料結構描述,men_map由一列 mem_map_t 組成。 在初始化時,每個 mem_map_t 描述系統中的一頁。它重要的域如下(有關記憶體管理 ) : 計數器 描述使用這頁的用戶數。如果計數器比一大,則這頁被多程序共享。 年齡 描述頁的年齡,被用來決定頁是否是被丟棄或交換的好候選。 map_nr 描述這個 mem_map_t 對應的頁的實體頁號。 頁分配程式碼使用向量 free_area 來尋找並釋放頁。這機制支援整個緩衝區管理,對 於程式碼來說,頁的大小和處理器對頁的操作機制是與其無關的。 free_area 每個單元都包含一種頁塊的信息。在陣列的第一單元描述單個的頁, 下 一單元描述 2 頁塊,再下一單元描述 4 頁的塊,並以2的冪上升。表中每個單元作 為一個隊頭,有指標指向mem_map 陣列中的頁。空的頁塊在這裡排隊。map是指向bitmap的 一個指標,bitmap 記錄了這種大小頁塊的分配情況。位元映射表中,如果第 n 塊頁是空 的,那麼位 N 被置。 圖 free_area_figure 顯示的是 free_area 的結構,第0單元記錄有一個空頁,從 第0頁開始。第2單元記錄有兩個4頁的空塊,第一塊從第4頁開始,第二塊從第56頁 開始。 3.4.1 頁的分配 Linux 使用伙伴(Buddy) 演算法來有效地分配和回收頁塊。頁分配程式碼被用來分配一 頁或多頁的塊。頁的大小總是 2 的冪,即能分配1頁, 2 頁, 4 頁等等。只要系統 中有足夠滿足請求的空頁 ( nr_free_pages >min_free_pages ),分配程式碼就能在 free_area 裡找到所需大小的頁塊。free_area每個單元有一張分配圖 (bitmap)。 例如, 陣列的單元 2 有描述長度為4的頁塊的分配圖。 演算法尋找所需大小的頁塊時,它先搜索 free_area 資料結構中那種頁塊的隊列。如 果所需大小的頁塊沒有空,就在下一對列中尋找(頁塊的大小是所需的兩倍)。繼續 這一過程直到free_area 中所有單元都被找過了或發現了一空頁塊。如果找到的空 頁塊比所需的大,它必須被分割成正確的大小。
圖 3.4 : free_area 資料結構
例如, 在圖3.4 中,如果需要一 2 頁塊,那麼第一個空的 4 頁塊 (從第4頁起 ) 將被分成兩半。從第4頁開始的 2 頁塊被返回給請求者﹔從第6頁開始的 2 頁塊被 排在free_area的空的兩頁塊的隊中。 3.4.2 頁的回收 頁分配時容易將大塊連續的記憶體分成很多小塊。頁的回收程式碼須盡可能將小塊的空 記憶體重新組合成大塊的。事實上,頁塊的大小對記憶體的重新組合很重要。 當一頁塊被釋放時,系統會檢查它旁邊的和一樣的大小的頁塊,看它們是否是空的。 如果是,它們將被拼成一個大的整塊。每次當兩塊記憶體被拼成了更大的空塊時,頁 回收程式碼嘗試將它們與其它空塊繼續組合,以得到更大的空間。這樣得到的空頁塊 可以滿足任何對記憶體的需求。 例如,在圖 3.1中,如果第 1 頁被釋放,那它將與第0頁結合,並被放到 free_area 的兩頁空塊的隊中。 3.5 記憶體映射 當一映像被執行時,它的內容必須被讀入程序的虛擬記憶體。它調用的函式庫函數也必須被讀 入虛擬記憶體。這個可執行檔案並非被實際讀入記憶體, 相反它只是被連接入程序的虛擬記憶體。 然後,當程式的一部份被應用程式調用時,系統才將這部份映像讀入記憶體。將映像 連接到程序的虛擬位址空間叫做記憶體映射(memory mapping)。
圖 3.5 :虛擬記憶體
每個程序的虛擬記憶體空間由一個 mm_struct 資料結構表示。這包含目前正在執行的映像 的信息 (例如 Bash ),還有很多指向 vm_area_struct 的指標。每個 vm_area_struct 資料結構 描述一段虛擬記憶體區域的開始和結束,及程序對那段虛擬記憶體的存取權限和允許的操作。這 些操作是Linux 對這段虛擬記憶體必須使用的一套例程。例如, 當程序試圖存取虛擬記憶體中某 頁,但發現這頁並不在記憶體中時,應執行的正確操作是 nopage 操作(通過頁錯)。 Linux使用nopage 操作可以按需將一頁可執行映像載入記憶體。 當一段可執行映像被印射入程序的虛擬記憶體時,會產生一組 vm_area_struct 資料結構。 每個 vm_area_struct 資料結構代表可執行映像的一部份;可執行程式碼, 初始化資料 (變數),未初始化資料等等。 Linux 支援很多標準的虛擬記憶體操作,當 vm_area_struct 資料結構產生時,系統會把正確的虛擬記憶體操作集與他們相聯。 3.6 按需換頁 (Demanding Paging) 當一部份可執行映像被印射入程序虛擬記憶體後,它就可以開始執行了。可是這時只有映 像的開始部份被實際讀入記憶體,它將不斷存取不在記憶體中的部份。當程序存取一個 沒有有效頁表記錄的虛擬位址時,那處理器將報頁錯給 Linux 系統。 頁錯描述頁錯發生的虛擬位址和引起的存取操作。 Linux 必須先找到代表頁錯發生區域的 vm_area_struct。由於搜索 vm_area_struct 資料結構對高效處理頁錯非常關鍵,所以所有 vm_area_struct 被連接成AVL樹結構 (Adelson-Velskii and Landis)。 如果沒有 vm_area_struct 代表這頁錯發生的 虛擬位址, 表示這程序企圖存取一個非法的虛擬位址。Linux 將發送 SIGSEGV 信號給 程序,如果程序沒有對應這個信號的處理程式,它將被終止。 Linux 再檢查存取操作是否是被允許的。如果程序在用一個非法的方法存取記憶體, 例如,寫一個只讀區域,它也將引起一個記憶體錯誤信號。 如果 Linux 確定頁錯是合法的, 它就會處理它。 Linux 必須首先區別映像是在交換檔中還是在磁碟上。它是通過頁表記錄來區別 的。 如果那頁的頁表記錄是無效的,但非空,說明產生頁錯的那頁目前在交換檔中。 例如, Alpha AXP 頁表記錄中,這樣的記錄有效位未置,但是PFN 域不為零。在這 種情況下, PFN 域容納的信息表示這頁被保持在哪個交換檔中的哪裡。本章後半 部將講述怎樣處理在交換檔中的頁。 並非所有的 vm_area_struct 資料結構都有一組虛擬記憶體操作,即使有,也不一定有nopage 操作。缺損情況下,Linux 將分配一頁新記憶體,並為這頁增加一項頁表記錄。但如 果這段虛擬記憶體有 nopage 操作,Linux 將使用它。 通常 Linux 的 nopage 操作被用於把可執行映像通過頁快取讀入記憶體。 當頁被讀入記憶體後,程序的頁表將被更新。特別是如果處理器使用TLA 緩衝區的話, 它可能需要通過硬體操作來完成更新。頁錯被處理後,程序在產生頁錯的指令處重 新開始執行。 3.7 Linux 頁快取
圖 3.6 : Linux 頁快取
Linux 頁快取的作用是加快從磁碟上存取檔案的速度。每次系統讀取檔案的一頁並 將它放在頁快取中。圖 3.6 顯示頁快取包括 page_hash_table,它是一組指向 mem_map_t 的指標。 Linux 的每個檔案由一 VFS inode 資料結構表示 (請參看檔案系統章 ),並且每個 VFS inode 是唯一的並且描述一個且僅一個檔案。頁表中的索引包括了檔案的 VFS號 及其在檔案中的偏移量。 當從印像檔案中讀一頁時,例如,按需裝載一頁回記憶體時,讀操作將通過頁快取。 如果頁在快取中,一個指向它的 mem_map_t 指標將被返回給處理頁錯的程式碼。否則, 這頁必須被從檔案系統中讀入記憶體。 Linux 需分配一頁記憶體並從磁碟上讀檔案。 如果可能, Linux 將開始讀檔案的下一頁。向前多讀一頁意味著如果程序是連續地 存取檔案,那麼下一頁將等在記憶體中。 頁快取中的內容將隨著檔案的存取而越來越多。當他們不再被任何程序使用時,這 些頁將被從快取中移出。當 Linux 的空閒記憶體變得很少時,Linux 將減少頁快取的 大小。 3.8 頁的交換和釋放 當空記憶體變得很少時, Linux 記憶體管理系統必須釋放一些頁。這任務由核心交換程 序來完成( kswapd )。 核心交換程式是一種特殊的程序,是一個核執行緒。核執行緒是沒有虛擬記憶體的程序,他們 在實體位址空間以核心模式運行。核心交換程式不僅把頁交換到系統的交換檔中, 它的角色是保証系統有足夠的記憶體而使記憶體管理系統可以高效工作。 核心交換程式被核心 init 程序在初始時啟動,並等待核心交換定時器周期性地到 期時開始運行。 每次定時器到期,核心交換程式檢查系統中的空頁數是否變得太低。交換程式使用 兩個變數,free_pages_high 和 free_pages_low 來決定是否它應該釋放一些頁。 只要系統的空頁數大於 free_pages_high, 核心交換程式不做任何事情﹔它繼續休 息直到定時器再次到期。在做這項檢查時,交換程式計算了正在往交換檔中寫的 頁數。每次有一頁等待寫入交換檔時,計數器加1,當操作結束後,計數器減1。 free_pages_low 和free_pages_high 在系統開始時被設置,並且與系統記憶體的頁數 有關。如果系統的空頁數小於 free_pages_high 或甚至小於 free_pages_low , 核 交換駐留程式將嘗試 3 種方法以減少系統使用的頁數: 減少緩衝區和頁快取的大小 換出系統 V 的共享頁 換出並釋放一些頁 如果系統的空頁數小於 free_pages_low , 核交換程式在它下次運行以前,將嘗試 釋放 6 頁,否則它將嘗試釋放 3 頁。上面的方法將依次被使用直到有足夠的頁被 釋放。核交換程式將記住上一次它是用什麼方法釋放記憶體的,下一次將首先使用這 個成功的方法。 在系統有足夠的空頁後,交換程式將休息直到它的定時器到期。如果上次空頁數小 於free_pages_low, 它只休息一半時間。直到空頁數多於 free_pages_low,核交 換程式才恢復休息的時間。 3.8.1 減少頁快取和緩衝區的大小 在頁快取和緩衝區中保存的頁是被釋放的最佳候選。頁快取保存著記憶體映像檔案, 很可能包括了許多沒用的頁。同樣,緩衝區中,它保存讀寫實體設備的資料, 也很 可能包含許多不需要的資料。當系統的記憶體頁快用完時,從這些快取丟棄頁是相對 容易的 (不同於從記憶體交換頁),因為它們不需要寫實體設備。丟棄這些頁除了使訪 問設備和記憶體的速度減慢一些以外,沒有其它的副作用。並且如果對各程序公平對 待的話,對各程序的影響是相同的。 每次核心交換程式嘗試縮小這些快取時,它先檢查在 mem_map 中的頁塊,看是否有 頁可以被從記憶體中釋放。如果核心交換程式經常作交換操作,也就是系統空頁數已 經非常少了,它會先檢查大一些的塊。頁塊會被輪流檢查﹔每次減少快取時檢查一 組不同的頁塊。這被稱作時鐘演算法,像鐘的分針一樣輪流檢查 mem_map 中的頁。 檢查一頁是看它是否在頁快取或緩衝區中。應該注意共享頁在這時候不能被釋放, 並且一頁不能同時在兩個快取中。如果頁不在任何一個快取中,那麼就檢查 mem_map 中的下一頁。 頁被快取在緩衝區中 ( 或頁內的緩衝區被快取 )是為更有效地分配和回收快取。縮 小記憶體程式碼將嘗試釋放被檢查頁中的緩衝區。 如果所有的緩衝區都被釋放了,那麼對應它們的記憶體也就被釋放了。如果被檢查的 頁在 Linux 頁快取中,它將被從頁快取中移出並釋放。 當足夠的頁被釋放後,核心交換程式將等到下一個周期再運行。因為釋放的頁都是 程序的虛擬記憶體部份 ( 他們是被快取的頁 ), 所以沒有頁表記錄需要更新。如果沒有釋 放足夠的頁,那麼交換程式將試著釋放一些共享頁。 3.8.2 交換出系統 V 的 共享頁 系統 V 共享記憶體提供了程序間的通信機制。程序間如何共享記憶體,請參看IPC章。 系統 V 的共享區域被描述成一個 shmid_ds 資料結構。這包含一根指向一組 vm_area_struct 資料結構的指標,每個 vm_area_struct 對應共享這區域的一個程序。vm_area_struct 資料結構描述了每個程序在各自虛擬記憶體的哪裡共享系統 V 的這個區域。每個 vm_area_struct 由 vm_next_shared 和vm_prev_shared 指標相連。每個 shmid_ds 資料結構還包括 一組頁表記錄,描述這些共享頁是對應記憶體中的哪些頁。 核心交換程式也使用時鐘演算法來換出系統 V 的共享頁。每次它運行時,它記得上次 換出的是哪個共享頁。它將其記錄在兩個索引中,第一個是 shmid_ds 資料結構的 索引, 第二個是系統的這段共享記憶體的頁表記錄的索引。這保証它公平地對待系統 V 的所有共享頁。 由於共享頁的實體頁號在每一個共享程序中都有記錄,核心交換程式必須修改這些 頁表,顯示頁已不在記憶體中了,而被保存在交換檔中。對於每個換出的共享頁, 核心交換程式是順著 vm_area_struct 的指標找到這共享頁在各個程序中的頁表記 錄。如果這共享的系統 V 的頁對應的頁表記錄是有效的,交換程式將把它改成無效, 標為在交換檔中,再將對應這頁的計數器減1。被換出的系統 V 的共享頁仍包括 兩個索引,第一個是 shmid_ds 資料結構的索引, 第二個是系統中共享這段記憶體的 程序的頁表記錄的索引。 如果各程序的頁表修改過後,頁的計數器變成0,那麼這頁就可以被寫入交換檔了, shmid_ds 中各頁表記錄的值將變為交換檔中的位址,在交換檔中的頁的記錄 包括其對應交換檔的索引和偏移量。當這頁要被重新讀回記憶體時,這些信息將被 使用。 3.8.3 換出及釋放(程序的)頁 交換程式檢查系統中每一個程序,看它們是不是好的候選。好的候選是那些能被換 出的程序或那些能從記憶體中換出並釋放若干頁的程序。只有當這些頁不能從其它地 方得到時,它們才會被寫進交換檔。 許多映像的內容是可以從映像檔案中讀出的。例如, 一段映像的可執行指令決不會 被修改,所以不用被寫進交換檔。這些頁能被直接釋放﹔當他們再被程序調用時, 他們將被從可執行映像中重新讀入記憶體。 一旦確定了換出的程序,交換程式將檢查它所有的頁表記錄,找出不是共享或被鎖 的區域。 Linux 並不換出它所選擇程序的所有可交換頁﹔相反它僅移出其中的一小部份。 如果頁在記憶體中被鎖住了,它們就不能被換出或釋放。 Linux 交換演算法使用頁的年齡 (aging)。每頁有一個記數器 (保持在 mem_map_t 數 據結構中),告訴交換程式是否應將它移出。當它們閒置時,頁會變老﹔當被存取時, 頁變年輕。交換程式僅僅移出舊頁。預設狀態下,當一頁被分配時,起始年齡是3, 每次它被存取,它的年齡從 3 增加直到最大值 20。每次核心交換程式運行時,它 把所有頁的年齡數減1。這些預設操作都能被改變,它們被存儲在 swap_control 數 據結構中。 如果頁是舊的 ( 年齡 = 0 ),交換程式就進一步處理它(將它移出記憶體)。臟頁也可 以被移出。Linux 用PTE中的特定位來標示 (見 3.2圖)。然而, 並非所有的臟頁必 須被寫進交換檔。程序的每個虛擬記憶體區域都可以有它們自己的交換操作 (由 vm_area_struct 中的 vm_ops 指出),這個特定的操作將被調用。否則,交換程式將分配一頁交換文 件,並將那頁寫到磁碟上。 頁對應的頁表記錄將被改為無效,但包含了它在交換檔中的信息,它將指出是哪 個交換檔,並且偏移量是多少。無論採取什麼交換方法,原來的實體頁將被放回 free_area。.乾淨的 (not dirty) 頁可以直接被釋放並放回 free_area以備後用。 如果有足夠的頁被換出或釋放, 交換程式就又開始休息。下一次它運行時,它將檢 查系統中的下一個程序。這樣,交換程式對每個程序都移出幾頁,直到系統記憶體恢 復正常,這比移出一整個程序來的公平。 3.9 交換快取 當將頁移入交換檔中時,並非所有情況,Linux 都需進行寫操作。有時一頁既在 交換檔中,又在記憶體中。這種情況是由於這頁本來被移到了交換檔中,後又因 為被調用,重又被讀入記憶體。只要在記憶體中的頁沒被修改過, 在交換檔中的拷貝 仍然是有效。 Linux 使用交換快取來記錄這些頁。交換快取是一張頁表記錄的表,每條記錄對應 一頁。每條頁表記錄描述被換出的頁在哪個交換檔中及其在檔案中的位置。如果 一交換快取記錄非零,表示在交換檔中的那頁沒被修改過,如果頁被修改了(被寫 ),它的記錄將被從交換快取中移出。 當 Linux 需要移出一頁記憶體到交換檔中時,它先查詢交換快取, 如果這頁有一個 有效的記錄,它就不需要把頁寫到交換檔中了。因為自從它上次被從交換檔中 讀出後,在記憶體中沒被修改過。 交換快取中的記錄描述已被移到交換檔中的頁。它們被標為無效,但是告許Linux 頁在哪個交換檔以及在交換檔的哪一頁。 3.10 移入頁 保存在交換檔中的臟頁可能會被再次調用。例如,一個應用程式要將某些內容寫到一 已移出的頁中。當這頁被換到交換檔中時,描述這頁的頁表記錄已被標記為“無效”。 這樣,存取不在記憶體中的虛擬位址將引起頁錯。頁錯是由處理器發信號給作業系統,告訴 作業系統它不能把某個虛擬位址翻譯成實體位址,並告之引起頁錯的虛擬位址及原因 (不同的 處理器是用不同的格式傳遞這些信息的),同時,處理器把控制權交給作業系統。 作業系統用特定(與處理器有關)的程式碼來找到引起頁錯的虛擬位址對應的 vm_area_struct 數 據結構。在這個過程中,系統檢索程序所有的 vm_area_struct。這段程式碼對時間的要求很 高,所以vm_area_struct 應被合理組織起來,以縮短所需的時間。 系統執行了以上操作,証實了引起頁錯的虛擬位址是有效的後,處理頁錯的其它程式碼是與 處理器無關的。 下一步,(系統)處理程式碼尋找虛頁對應的頁表記錄。如果它發清b頁表記錄指示這頁在交 換檔案中, Linux 就把這頁讀回記憶體。頁表記錄的格式因處理器的不同而各不相同,但“有 效位”都應該是無效,並都保存著有關這頁在交換檔中的信息。Linux 需要利用這些信 息來把頁重新載入記憶體。 此時,Linux 知道了引起頁錯的虛擬位址及其對應的頁表記錄,記錄中保存著有關交換檔 的信息。而將頁從交換檔中讀回記憶體的函數通常由vm_area_struct 中的指標指向。這種 函數叫移入(swapin) 函數。如果能從vm_area_struct 中找到這函數,Linux 就會調用它。例 如,因為系統 V 的頁的格式與一般的頁不同,所以系統 V 中移出的頁需要特殊處理,這 時就需要調用它們的移入函數。然而,某頁可能沒有對應的移入函數,在這種情況下, Linux 將認為它是一普通的頁,而不需要做任何特別處理。 系統將分配記憶體中的一空頁並從交換檔中把這頁讀回來,交換檔中的位址信息是從 無效的頁表記錄中取回的。 如果引起頁錯的不是寫操作,那麼這頁將被留在交換快取中,它的頁表記錄不會被標為 “可寫”。如果後來這頁被寫了,那麼會產生另一個頁錯,這時,頁被標成“dirty”,並 被從交換緩衝中刪去。如果這頁沒被修改過,而它又需要被換出,Linux將不會再把這頁 寫到交換檔中,因為它已經在那兒了。 如果引起頁錯的是寫操作,頁將被從交換快取中刪除,它的頁表記錄將被標成“dirty”和“可寫( writable)”。