摘要:
計算機操作系統內存管理是十分重要的,因為其中涉及到很多設計很多算法。《深入理解計算機系統》這本書曾提到過,現在操作系統存儲的設計就是“帶着鐐銬跳舞”,造成計算機一種一種容量多,速度快的假象。包括現在很多系統比如數據庫系統的設計和操作系統做法相似。所以在學習操作系統之余我來介紹並總結一些操作系統的內存管理。
首先我們看一下計算機的存儲層次結構
按照金字塔結構可以分為四種類型: 寄存器,快速緩存,主存和外存。而
寄存器和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++完成上述幾個算法和操作的具體實現,相信一定會大有脾益的。