摘要:
計算機作業系統記憶體管理是十分重要的,因為其中涉及到很多設計很多演算法。《深入理解計算機系統》這本書曾提到過,現在作業系統儲存的設計就是“帶著鐐銬跳舞”,造成計算機一種一種容量多,速度快的假象。包括現在很多系統比如資料庫系統的設計和作業系統做法相似。所以在學習作業系統之餘我來介紹並總結一些作業系統的記憶體管理。
首先我們看一下計算機的儲存層次結構
按照金字塔結構可以分為四種型別: 暫存器,快速快取,主存和外存。而
暫存器和L1快取
都在Processor內部。在金字塔中,越往下價格越低速度越慢但容量越大。
還有兩種儲存空間需要分清:
- 地址空間:又稱邏輯地址空間,源程式經過編譯後得到的目標程式,存在於它所限定的地址範圍內,這個範圍稱為地址空間。地址空間是邏輯地址的集合。
- 記憶體空間: 又稱儲存空間或實體地址空間。是指主存中一系列儲存資訊的物理單元(劃重點)的集合,這些單元的編號稱為實體地址或絕對地址。
簡言之就這兩個空間分別是程式設計師能夠觀測到的儲存空間和真實的物理空間。
需求與管理的目標
需求:
- 每個程式設計師希望沒有第三方因素干擾程式執行
- 計算機希望將有限的資源儘可能為多個使用者提供服務
為了滿足需求的目標:
- 計算機至少同時存在一個使用者程式和一個伺服器程式(作業系統核心管理)
- 每個程式互不干擾,所以其地址空間應該相互獨立。
- 每個程式使用的空間應該被保護,最怕執行的時候程式中斷。就和看電影的時候無法播放一樣難受。
程式的記憶體管理
作業系統在記憶體中的位置有以下三種可能
只有一個程式的環境下的記憶體管理
此時整個記憶體只有兩個程式,即使用者程式和作業系統。
作業系統所佔的空間是固定的,則使用者程式空間也是固定的,因此可以將使用者程式永遠載入到同一個地址,即使用者程式永遠從同一個地方開始執行。這種情況下,使用者程式地址可以在執行之前就可以計算出來。
我們通過載入器計算程式執行之前的實體地址靜態翻譯。此時既不需要額外實現地址獨立和地址保護。因為使用者不需要知道實體記憶體的相關知識,而且也沒有其它使用者程式。
多個程式的環境下的記憶體管理
此時使用者的程式空間需要通過分割槽來分給多個不同的程式了。每個應用程式佔用一個或幾個分割槽,這種分配支援多個程式併發執行,但難以進行記憶體分割槽的共享。
其中分割槽有兩種方法:
一種方法: 固定(靜態)式 分割槽分配, 讓程式適應分割槽
顧名思義就是把記憶體劃分為若干個固定大小的連續分割槽,這幾個分割槽或者大小相等以適合多個相同程式併發,或者大小不等的分割槽以適合不同大小的程式。
這種分配方法優點很明顯,在於非常容易實現,開銷小。
缺點就是會產生很多內部碎片(也就是未被利用的儲存空間),固定的分割槽總數也限制了併發執行的程式數目。我們簡單介紹下靜態分配的幾種方法。
單一佇列的分配方式
多佇列分配方式
固定分割槽管理
先使用表進行大小初始化,固定分割槽大小
另一種方法:可變(動態)式 分割槽分配, 讓分割槽適應程式
此時分割槽的邊界可以移動,但也產生了分割槽與分割槽之間狹小的外部碎片。
在可變分割槽中,知道記憶體的空閒空間大小就十分重要了。OS通過跟蹤記憶體使用計算出記憶體有多少空閒。跟蹤的方法有兩種:
點陣圖表示法
也就是所謂的bitmap
,用每一位來存放某種狀態。將記憶體每一個分配單元賦予一個判斷的用於判斷狀態的字位,字位取值位0
表示單元閒置;字位為1
表示單元被佔用
特點
- 空間成本固定:不受記憶體程式數量影響
- 時間成本低:操作的時候只需要將狀態值改變
- 缺少容錯能力:由於記憶體單元發生錯誤的時會將狀態值改變,對作業系統來講,這個狀態值是因為發生錯誤發生的改變還是原來的狀態很難判斷。
連結串列表示法
將分配單元按照是否閒置連結,P代表這個空間被佔用,H代表這個這是一片閒置空間。為了方便遍歷查詢,每個程式空間的結點接著一個空閒空間的結點每個連結串列結點還有一個起始地址,與分配單元的大小,用程式碼表示為
enum Status{P=0,H=1};
struct LinkNode{
enum Status status;//P表示程式,H表示空閒
struct LinkNode *begin_address;//起始地址
size_t size;//閒置空間大小
struct LinkNode *next;
};
特點
1. 空間成本:取決於程式數量
2. 時間成本:連結串列不停的遍歷速度很慢,同時還要進行連結串列的插入和刪除修改。
3. 有一定的容錯能力,可以通過程式空間結點和空閒空間結點相互驗證。
可變分割槽的記憶體分配
OS通過上面兩種跟蹤方法知道記憶體空閒容量,而現在作業系統一般都以連結串列的形式進行記憶體空閒容量跟蹤。如果有新的程式需要讀入記憶體,可變分割槽就要對空閒的分割槽進行記憶體分配。
記憶體分配使用兩張表:已分配分割槽表和未分配分割槽表。用C++描述如下:
//未分配分割槽表
struct FreeBlock {
int id; // 記憶體分割槽號
int address; // 該分割槽的首地址
unsigned length; // 分割槽長度
};
//已分配分割槽表
struct AllocatedBlock {
int id; // 記憶體分割槽號
int address; // 該分割槽的首地址
int pid; // 程式 ID
unsigned length; // 分割槽長度
};
然後OS用雙向連結串列將所有未分配分割槽表進行串聯
struct{
FreeBlock data;
Node* prior;
Node* next;
}Node;
未分配分割槽表在整個系統空間上的結構如下:
基於 順序搜尋 的分配演算法:
這裡我們介紹四種基於順序搜尋的尋找空閒儲存空間的演算法:
- 首次適應演算法( First Fit ) :每個空白區按其地址順序連在一起,從這個空白區域鏈的始端開始查詢,選擇第一個足以滿足請求的空白塊。
- 下次適應演算法( Next Fit ) :將儲存空間中空白區構成一個迴圈鏈,每次為儲存請求查詢合適的分割槽時,總是從上次查詢結束的下一個空閒塊開始,只要找到一個足夠大的空白區,就將它劃分後分配出去。
- 最佳適應演算法( Best Fit ) : 為一個作業選擇分割槽時,總是尋找其大小最接近(小於等於)於作業所要求的儲存區域。
- 最壞適應演算法( Worst Fit ) :為作業選擇儲存區域時,總是尋找最大的空白區。
演算法舉例!!
系統中空閒分割槽表如下按照地址遞增次序排列,現有三個作業分配申請記憶體空間100K
,30K
,7K
。
區號 | 大小 | 地址 | 狀態 |
---|---|---|---|
1 | 32K | 20K | 未分配 |
2 | 8K | 52K | 未分配 |
3 | 120K | 60K | 未分配 |
4 | 331K | 180K | 未分配 |
首次適應:
從上到下尋找合適的大小
- 申請作業
100K
,從低地址到高地址找到3號分割槽,分配完後3號分割槽起始地址變為100K+60K=160K
,剩餘空間為120K-100K=20K
- 申請作業
30K
,從低地址到高地址找到1號分割槽,分配完後1號分割槽起始地址變為20K+30K=50K
,剩餘空間為32K-30K=2K
- 申請作業
7K
,從低地址到高地址找到2號分割槽,分配完後2號分割槽起始地址變為52K+7K=59K
,剩餘空間為8K-7K=1K
- 結論:優先利用記憶體低地址部分的空閒分割槽。但由於低地址部分不斷被劃分,留下許多難以利用的很小的空閒分割槽(碎片或零頭) ,而每次查詢又都是從低地址部分開始,增加了查詢可用空閒分割槽的開銷。
- 申請作業
下次適應
- 申請作業
100K
,找到3號分割槽,分配完後3號分割槽起始地址變為100K+60K=160K
,剩餘空間為120K-100K=20K
- 申請作業
30K
,從3號分割槽後繼續出發,找到4號分割槽,分配完後4號分割槽起始地址變為180K+30K=210K
,剩餘空間為331K-30K=301K
- 申請作業
7K
,從4號分割槽後繼續出發,找到1號分割槽,分配完後1號分割槽起始地址變為20K+7K=27K
,剩餘空間為32K-7K=25K
- 結論:使儲存空間的利用更加均衡,不致使小的空閒區集中在儲存區的一端,但這會導致缺乏大的空閒分割槽。
- 申請作業
最佳適應演算法
- 申請作業
100K
,找到最適合的3號分割槽,分配完後3號分割槽起始地址變為100K+60K=160K
,剩餘空間為120K-100K=20K
- 申請作業
30K
,找到最適合的1號分割槽,分配完後1號分割槽起始地址變為20K+30K=50K
,剩餘空間為32K-30K=2K
- 申請作業
7K
,找到最適合的2號分割槽,分配完後1號分割槽起始地址變為52K+7K=59K
,剩餘空間為8K-7K=1K
- 結論:若存在與作業大小一致的空閒分割槽,則它必然被選中,若不存在與作業大小一致的空閒分割槽,則只劃分比作業稍大的空閒分割槽,從而保留了大的空閒分割槽。最佳適應演算法往往使剩下的空閒區非常小,從而在儲存器中留下許多難以利用的小空閒區(碎片) 。
- 申請作業
最壞適應演算法
- 申請作業
100K
,找到4號分割槽,分配完後3號分割槽起始地址變為180K+60K=240K
,剩餘空間為331K-100K=231K
- 申請作業
30K
,此時被分配過的4號分割槽依然容量最大,於是還是找到4號分割槽,分配完後4號分割槽起始地址變為240+30K=250K
,剩餘空間為231K-30K=201K
- 申請作業
7K
,此時被分配過的4號分割槽依然容量最大,找到4號分割槽,分配完後4號分割槽起始地址變為250+7K=257K
,剩餘空間為201K-7K=194K
- 結論:總是挑選滿足作業要求的最大的分割槽分配給作業。這樣使分給作業後剩下的空閒分割槽也較
大,可裝下其它作業。由於最大的空閒分割槽總是因首先分配而劃分,當有大作業到來時,其儲存空間的申請往往會得不到滿足。
- 申請作業
基於順序搜尋的分配演算法實際上只適合小型的作業系統,大中型系統使用了是比較複雜的索引搜尋的動態分配演算法。
如何回收記憶體
- 回收分割槽上鄰接一個空閒分割槽,合併後首地址為空閒分割槽的首地址,大小為二者之和。
- 回收分割槽下鄰接一個空閒分割槽,合併後首地址為回收分割槽的首地址,大小為二者之和。
- 回收分割槽上下鄰接空閒分割槽,合併後首地址為上空閒分割槽的首地址,大小為三者之和。
- 回收分割槽不鄰接空閒分割槽,這時在空閒分割槽表中新建一表項,並填寫分割槽大小等資訊。
用iPad
畫了一個簡單的示意圖如下:
最後
記憶體分配實際上是作業系統非常重要的一環,如果僅限於理論而不寫程式碼實踐則容易迷惘,很多具體的實現與都比較困難。如上面的基於順序搜尋的最佳適應演算法,比如幾個分割槽的表示方法,都用到了資料結構和演算法的知識。如果能用C或者C++完成上述幾個演算法和操作的具體實現,相信一定會大有脾益的。