一、存儲器抽象:位址空間
現代計算機采用多道程式設計,系統中同時運作了多個程序。要保證多個程式同時在記憶體中并且互不影響,就需要解決兩個問題:保護和重定位。
位址空間:一個程序可以用來尋址記憶體的一套位址集合。
每個程序都有自己的一個位址空間,并且這個位址空間獨立于其他程序的位址空間。有了位址空間,一個程序就知道了自己可以尋址的範圍,進而解決了保護的問題。
解決了這個問題,又有一個問題出現了:記憶體不夠用。
現在的PC,一般也就是4G或者8G的記憶體,甚至可能隻有2G。而一個程序,幾百M都是很正常的,大型的遊戲可能數G了。
一台電腦要同時運作許多程序,以及一些作業系統自身占用的記憶體,如果全部程式都在記憶體中,那麼記憶體是肯定不夠用了。
交換:把一個程序完整調入記憶體,使該程序進入記憶體運作一段時間,然後把它存回磁盤。這樣,空閑的程序主要存儲在磁盤上,不運作就不占用記憶體。
交換技術的問題在于,磁盤與記憶體之間的I/O操作是很慢的,而且會在記憶體中産生很多空洞(hole)。是以要每隔一段時間進行記憶體緊縮,這樣會花費大量的時間。
程式設計語言通常有動态記憶體的功能,例如C中的malloc,C++中的new和allocator等,一般在堆上進行配置設定。作業系統需要對動态記憶體進行管理,方法主要有兩種
(1)位圖。位圖是一種資料結構,簡單來說,就是用一個比特位來表示可用和不可用兩種狀态。把記憶體分成大小為N的單元,用一個二進制位來表示可用或者不可用,進而可以用一個比較小的空間表示一大段記憶體的使用情況。
位圖的缺點是耗時大,查找空閑需要大量的時間。
(2)空閑連結清單。作業系統維護一個記錄着已配置設定記憶體段和空閑記憶體段的連結清單。當申請一塊新記憶體的時候,要從空閑連結清單中進行查找,比對方式有很多種:首次适配,下一次适配,最佳适配等等。可以對連結清單進行排序以提高查詢的速度。
此外,還可以對常用大小的記憶體塊分别設定連結清單,比如4KB的塊組成一個連結清單,8KB塊組成一個連結清單等。這種方式與Linux采用的夥伴算法有一定的相似之處。
二、虛拟記憶體
虛拟記憶體是大部分現代作業系統所采用的方式。交換方式雖然可以解決一部分記憶體不夠用的問題,但是I/O操作代價是比較大的,磁盤速度比較慢。
虛拟記憶體的基本思想,就是每個程序有自己的位址空間,并且把這個空間分成許多塊,每一塊叫做頁面(page)。每一頁都是一個連續的位址範圍,他們可以被映射到實體記憶體,頁并不需要在程序運作的每一時刻都在記憶體中,它可以隻在需要的時候才被調入,其他時間可以被調出記憶體。
程序對應的虛拟位址,構成了虛拟位址空間。虛拟記憶體可以通過MMU(記憶體管理單元)映射為實體位址。實體記憶體中的頁稱為頁框(page frame)。
實際記憶體中,用一個标志位表示一個頁在不在記憶體中。如果要讀的頁不在記憶體中,将會産生一個缺頁中斷,使得CPU陷入作業系統中,把頁面的内容讀到頁框中,再次請求該頁。
虛拟記憶體可以被分為虛拟頁号(高位)+偏移量(低位)。
頁表:頁号可以作為頁表的索引,來找到該虛拟頁面對應的頁表項。由頁表項可以找到頁框号,把頁框号送到偏移量的高位,形成送往記憶體的實體位址。實體位址=頁框号+偏移量。
一個頁表項需要包括:保護位(允許的通路類型),修改位(是否被修改),‘’在/不在‘’位(是否在記憶體中),高速緩存允許位,以及最為重要的頁框号。當然,還可能有更多位。
頁表的問題在于,如果記憶體很大,頁表項非常多,那麼頁表占用的記憶體也會很大。另外,每個程序都要有自己的頁表,因為頁表提供了虛拟記憶體到實體記憶體的轉換。
TLB(快表,轉換檢測緩沖區):相當于一種緩存,因為有些頁面的通路頻率很高,是以把這些頁面放在一個高速緩存中(例如内置在MMU中)。
需要通路某個頁時,先從TLB中查找,未命中才向頁表查找,并用它替換掉另外一個頁表項。
軟失效和硬失效:頁不在TLB中,但是在記憶體中,稱為軟失效;在硬碟中,稱為硬失效。很顯然,硬碟I/O是很慢的。
多級頁表:頁表項太多,會占用太多的記憶體。于是,多級頁表應運而生。本質上,多級頁表就是一級索引的項仍然是一個索引,而非頁表項。這樣可以節約大量的空間,避免把全部頁表一直儲存在記憶體中。
倒排頁表:之前的頁表是虛拟位址到實體位址的轉換。倒排頁表,就是實體位址到虛拟位址的映射。這樣做的目的,也是為了應對頁表太大的問題。這樣,每個實體頁框有一個表項,記錄(程序-虛拟頁面)映射到了該頁框。倒排頁表的缺陷,在于程序使用虛拟位址的時候不友善,必須搜尋這個倒排頁表才能得到實體頁框位址。為了提高速度,可以采用散列的方法。
三、Linux系統中的實作
1、基本概念
Linux作業系統中,每個程序都有三個段:代碼段、堆棧段、資料段。通常,代碼段不發生變化。
資料段有兩部分:初始化的資料和未初始化的資料(BSS段)。未初始化的資料在加載後被映射到一個專門的靜态零頁框,指派後進行寫時複制。
Linux允許資料段随着記憶體的配置設定和回收而增長和縮減,進而解決動态配置設定的問題。
Linux允許共享代碼段記憶體,但是每個程序的資料和堆棧不共享。
Linux允許記憶體映射檔案,即把一段記憶體映射到檔案上,檔案可以想數組一樣被讀寫。

2、Linux系統中的記憶體管理
Linux區分三種記憶體區域:
(1)ZONE_DMA:可以DMA操作的頁。
(2)ZONE_NORMAL:正正常則映射的頁。
(3)ZONE_HIGHMEM:高記憶體位址的頁,不永久性映射。
Linux的記憶體由三部分組成:常駐記憶體的核心和記憶體映射,以及被劃分為頁框的其他部分。核心維護一個記憶體映射,包含所有實體記憶體使用情況的資訊:
首先,Linux維護一個頁描述符數組mem_map,頁描述符為page類型,系統中的每個實體頁框都有一個頁描述符,每個頁描述符都有一個指針,指向它所屬的位址空間(倒排頁表),另外有一對指針可以與其他描述符形成雙向連結清單,記錄空閑頁框和一些其他域。
因為實體記憶體被分成了三個區,是以為每個區維護一個區域描述符。區域描述符記錄了每個區域記憶體使用情況,以及一個空閑區數組,第i個元素标記2^i個空閑頁的第一個塊的第一個頁描述符(指針數組)。
Linux采用了一個四級頁表。
3、Linux的記憶體配置設定機制
Linux支援多種記憶體配置設定機制,頁面配置設定器使用了夥伴算法。
當一塊記憶體過大時,會被分為兩個大小相同的部分,組成一對夥伴。劃分會一直持續,直到再次劃分不足以容納。釋放記憶體時,如果一對夥伴都為空閑,則會進行合并。
同時,LInux維護一個指針數組,數組的元素是大小為1 2 4……個機關的記憶體塊連結清單的頭部。
另外,還有一種slab配置設定器,它也使用夥伴算法獲得記憶體塊,但是從其中切出給小的單元。例如,一個65頁面的塊,需要一個128頁面的塊來使用。
PS:圖檔來自《現代作業系統》
四、一些零碎的東西
共享庫:我們知道,一部分記憶體如果被多個程序共享,通常都是隻讀狀态。如果一個程序對它進行了寫操作,那麼這些頁面就“髒”了,系統會為另外的程序複制該頁面,這就是寫時複制。
當一個程式與共享庫連結時,連結器沒有加載被調用的函數,而是加載了一小段能夠在運作時綁定被調用函數的存根例程。關鍵在于,一旦一部分共享庫被裝載,就不需要再次裝載它了。共享庫會按照頁面裝載到記憶體,不需要的部分暫時不會被裝載。
另外,當共享庫中的函數被修改時,并不需要重新編譯調用了這個函數的程式。共享庫相當于一個子產品,隻需要進行調用。
另外一個問題是重定位。連結的兩個主要步驟,就是符号解析和重定位。函數的重定位也就是擷取實際的位址。因為共享庫在記憶體中隻有一份,而兩個程序中,可能在自己程序位址空間的不同位置需要共享頁面。這樣,在編譯共享庫時,需要用一個編譯選項告訴編譯器,不産生使用絕對位址的指令,而是隻産生使用相對位址的指令。這樣,無論共享庫被放置在虛拟位址空間的什麼位置,指令都可以正常工作。隻使用相對偏移量的代碼被稱作位置無關代碼。
與分頁有關的實作工作:
建立新程序時:作業系統要确定程式和資料初始有多大,建立一個頁表,并在記憶體空間中為頁表配置設定空間并進行初始化。程序被換出時,頁表不必駐留記憶體,但是當程序在記憶體中時,頁表也必須在記憶體中。為了解決記憶體不足的問題,可以把一部分内容放在磁盤交換區。Linux就有交換區(swap),可以通過一系列指令設定交換區大小以及檢視交換區的使用。當然,硬碟I/O速度很慢,swap使用過多可能會造成速度變慢許多。好像我就有這種體驗...實驗室電腦比較渣,虛拟機記憶體設定的比較少,然後用top檢視的時候就發現記憶體和swap區占用都很多,操作簡直慢如蝸牛0 0通常是因為我開了Pycharm...