計算機操做系統內存管理是十分重要的,由於其中涉及到不少設計不少算法。《深刻理解計算機系統》這本書曾提到過,如今操做系統存儲的設計就是「帶着鐐銬跳舞」,形成計算機一種一種容量多,速度快的假象。包括如今不少系統好比數據庫系統的設計和操做系統作法類似。因此在學習操做系統之餘我來介紹並總結一些操做系統的內存管理。程序員
首先咱們看一下計算機的存儲層次結構算法
按照金字塔結構能夠分爲四種類型: 寄存器,快速緩存,主存和外存。而數據庫
寄存器和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;
未分配分區表在整個系統空間上的結構以下:
這裏咱們介紹四種基於順序搜索的尋找空閒存儲空間的算法:
系統中空閒分區表以下按照地址遞增次序排列,現有三個做業分配申請內存空間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++完成上述幾個算法和操做的具體實現,相信必定會大有脾益的。