關於linux記憶體管理

來源:互聯網
上載者:User

 Linux的記憶體管理主要分為兩部分:物理地址到虛擬位址的映射,核心記憶體配置管理(主要基於slab)。

物理地址到虛擬位址之間的映射

1、概念

  物理地址(physical address)

  用於記憶體晶片級的單元定址,與處理器和CPU串連的地址匯流排相對應。——這個概念應該是這幾個概念中最好理解的一個,但是值得一提的是,雖然可以直接把物理地址理解成插在機器上那根記憶體本身,把記憶體看成一個從0位元組一直到最大空量逐位元組的編號的大數組,然後把這個數組叫做物理地址,但是事實上,這隻是一個硬體提供給軟體的抽像,記憶體的定址方式並不是這樣。所以,說它是“與地址匯流排相對應”,是更貼切一些,不過拋開對實體記憶體定址方式的考慮,直接 把物理地址與物理的記憶體一一對應,也是可以接受的。也許錯誤的理解更利於形而上的抽像。

  虛擬記憶體(virtual memory)

  這是對整個記憶體(不要與機器上插那條對上號)的抽像描述。它是相對於實體記憶體來講的,可以直接理解成“不直實的”,“假的”記憶體,例如,一個0x08000000記憶體位址,它並不對就物理地址上那個大數組中0x08000000 - 1那個地址元素;

  之所以是這樣,是因為現代作業系統都提供了一種記憶體管理的抽像,即虛擬記憶體(virtual memory)。進程使用虛擬記憶體中的地址,由作業系統協助相關硬體,把它“轉換”成真正的物理地址。這個“轉換”,是所有問題討論的關鍵。有了這樣的抽像,一個程式,就可以使用比真實物理地址大得多的地址空間。(拆東牆,補西牆,銀行也是這樣子做的),甚至多個進程可以使用相同的地址。不奇怪,因為轉換 後的物理地址並非相同的。可以把串連後的程式反編譯看一下,發現連接器已經為程式分配了一個地址,例如,要調用某個函數A,代碼不是call
A,而是call0x0811111111 ,也就是說,函數A的地址已經被定下來了。沒有這樣的“轉換”,沒有虛擬位址的概念,這樣做是根本行不通的。

  打住了,這個問題再說下去,就收不住了。

  邏輯地址(logical address)

  Intel為了相容,將遠古時代的段式記憶體管理方式保留了下來。邏輯地址指的是機器語言指令中,用來指定一個運算元或者是一條指令的地址。以上例,我們說的連接器為A分配的0x08111111這個地址就是邏輯地址。——不過不好意思,這樣說,好像又違背了Intel中段式管理中,對邏輯地址要求,“一個邏輯地址,是由一個段標識符加上一個指定段內相對位址的位移量,表示為[段標識符:段內位移量],也就是說,上例中那個0x08111111,應該表示為[A的程式碼片段標識符: 0x08111111],這樣,才完整一些”

  線性地址(linear address)或也叫虛擬位址(virtual address)

  跟邏輯地址類似,它也是一個不真實的地址,如果邏輯地址是對應的硬體平台段式管理轉換前地址的話,那麼線性地址則對應了硬體頁式記憶體的轉換前地址。

  CPU將一個虛擬記憶體空間中的地址轉換為物理地址,需要進行兩步:首先將給定一個邏輯地址(其實是段內位移量,這個一定要理解!!!),CPU 要利用其段式記憶體管理單元,先將為個邏輯地址轉換成一個線程地址,再利用其頁式記憶體管理單元,轉換為最終物理地址。這樣做兩次轉換,的確是非常麻煩而且沒有必要的,因為直接可以把線性地址抽像給進程。之所以這樣冗餘,Intel完全是為了相容而已。

  2、CPU段式記憶體管理,邏輯地址如何轉換為線性地址

  一個邏輯地址由兩部份組成,段標識符: 段內位移量。段標識符是由一個16位長的欄位組成,稱為段選擇符。其中前13位是一個索引號。後面3位包含一些硬體細節,
 
  最後兩位涉及許可權檢查,本貼中不包含。

  索引號,或者直接理解成數組下標——那它總要對應一個數組吧,它又是什麼東東的索引呢?這個東東就是“段描述符(segment descriptor)”,呵呵,段描述符具體地址描述了一個段(對於“段”這個字眼的理解,我是把它想像成,拿了一把刀,把虛擬記憶體,砍成若干的截—— 段)。這樣,很多個段描述符,就組了一個數組,叫“段描述符表”,這樣,可以通過段標識符的前13位,直接在段描述符表中找到一個具體的段描述符,這個描 述符就描述了一個段,我剛才對段的抽像不太準確,因為看看描述符裡面究竟有什麼東東——也就是它究竟是如何描述的,就理解段究竟有什麼東東了,每一個段描
述符由8個位元組組成,如:
 
  這些東東很複雜,雖然可以利用一個資料結構來定義它,不過,我這裡只關心一樣,就是Base欄位,它描述了一個段的開始位置的線性地址。

  Intel設計的本意是,一些全域的段描述符,就放在“全域段描述符表(GDT)”中,一些局部的,例如每個進程自己的,就放在所謂的“局部段 描述符表(LDT)”中。那究竟什麼時候該用GDT,什麼時候該用LDT呢?這是由段選擇符中的T1欄位表示的,=0,表示用GDT,=1表示用LDT。

  GDT在記憶體中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT則在ldtr寄存器中。好多概念,像繞口令一樣。這張圖看起來要直觀些:
 
  首先,給定一個完整的邏輯地址[段選擇符:段內位移地址],

  1、看段選擇符的T1=0還是1,知道當前要轉換是GDT中的段,還是LDT中的段,再根據相應寄存器,得到其地址和大小。我們就有了一個數組了。

  2、拿出段選擇符中前13位,可以在這個數組中,尋找到對應的段描述符,這樣,它了Base,即基地址就知道了。

  3、把Base + offset,就是要轉換的線性地址了。

  還是挺簡單的,對於軟體來講,原則上就需要把硬體轉換所需的資訊準備好,就可以讓硬體來完成這個轉換了。OK,來看看Linux怎麼做的。

  3、Linux的段式管理

  Intel要求兩次轉換,這樣雖說是相容了,但是卻是很冗餘,呵呵,沒辦法,硬體要求這樣做了,軟體就只能照辦,怎麼著也得形式主義一樣。

  另一方面,其它某些硬體平台,沒有二次轉換的概念,Linux也需要提供一個高層抽像,來提供一個統一的介面。所以,Linux的段式管理,事實上只是“哄騙”了一下硬體而已。按照Intel的本意,全域的用GDT,每個進程自己的用LDT——不過Linux則對所有的進程都使用了相同的段來對 指令和資料定址。即使用者資料區段,使用者程式碼片段,對應的,核心中的是核心資料區段和核心程式碼片段。這樣做沒有什麼奇怪的,本來就是走形式嘛,像我們寫年終總結一樣。
include/asm-i386/segment.h

 

#define GDT_ENTRY_DEFAULT_USER_CS        14
#define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)
#define GDT_ENTRY_DEFAULT_USER_DS        15
#define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3)
#define GDT_ENTRY_KERNEL_BASE        12
#define GDT_ENTRY_KERNEL_CS                (GDT_ENTRY_KERNEL_BASE + 0)
#define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8)
#define GDT_ENTRY_KERNEL_DS                (GDT_ENTRY_KERNEL_BASE + 1)
#define __KERNEL_DS (GDT_ENTRY_KERNEL_DS * 8)

 

把其中的宏替換成數值,則為:

 

 

#define __USER_CS 115       [00000000 1110  0  11]
#define __USER_DS 123       [00000000 1111  0  11]
#define __KERNEL_CS 96     [00000000 1100  0  00]
#define __KERNEL_DS 104   [00000000 1101  0  00]

 

方括弧後是這四個段選擇符的16位二製表示,它們的索引號和T1欄位值也可以算出來了

 

 

__USER_CS              index= 14   T1=0
__USER_DS               index= 15   T1=0
__KERNEL_CS           index=  12  T1=0
__KERNEL_DS           index= 13   T1=0

 

T1均為0,則表示都使用了GDT,再來看初始化GDT的內容中相應的12-15項(arch/i386/head.S):

 

        .quad0x00cf9a000000ffff        /* 0x60 kernel 4GBcode at 0x00000000 */
        .quad0x00cf92000000ffff        /* 0x68 kernel 4GBdata at 0x00000000 */
        .quad0x00cffa000000ffff        /* 0x73 user 4GBcode at 0x00000000 */
        .quad0x00cff2000000ffff        /* 0x7b user 4GBdata at 0x00000000 */

  按照前面段描述符表中的描述,可以把它們展開,發現其16-31位全為0,即四個段的基地址全為0。

  這樣,給定一個段內位移地址,按照前面轉換公式,0 +段內位移,轉換為線性地址,可以得出重要的結論,“在Linux下,邏輯地址與線性地址總是一致(是一致,不是有些人說的相同)的,即邏輯地址的位移量欄位的值與線性地址的值總是相同的。!!!”

  忽略了太多的細節,例如段的許可權檢查。呵呵。Linux中,絕大部份進程並不例用LDT,除非使用Wine ,模擬Windows程式的時候。

  4.CPU的頁式記憶體管理

  CPU的頁式記憶體管理單元,負責把一個線性地址,最終翻譯為一個物理地址。從管理和效率的角度出發,線性地址被分為以固定長度為單位的組,稱為頁(page),例如一個32位的機器,線性地址最大可為4G,可以用4KB為一個頁來劃分,這頁,整個線性地址就被劃分為一個 tatol_page[2^20]的大數組,共有2的20個次方個頁。這個大數組我們稱之為頁目錄。目錄中的每一個目錄項,就是一個地址——對應的頁的地址。

  另一類“頁”,我們稱之為物理頁,或者是頁框、頁楨的。是分頁單元把所有的實體記憶體也劃分為固定長度的管理單位,它的長度一般與記憶體頁是一一對應的。這裡注意到,這個total_page數組有2^20個成員,每個成員是一個地址(32位機,一個地址也就是4位元組),那麼要單單要表示這麼一個數 組,就要佔去4MB的記憶體空間。為了節省空間的,引入了一個二級管理員模式的機器來組織分頁單元。文字描述太累,看圖直觀一些:
 
  如,

  1、分頁單元中,頁目錄是唯一的,它的地址放在CPU的cr3寄存器中,是進行地址轉換的開始點。萬裡長征就從此長始了。

  2、每一個活動的進程,因為都有其獨立的對應的虛似記憶體(頁目錄也是唯一的),那麼它也對應了一個獨立的頁目錄位址。——運行一個進程,需要將它的頁目錄位址放到cr3寄存器中,將別個的儲存下來。

  3、每一個32位的線性地址被劃分為三部份,面目錄索引(10位):頁表索引(10位):位移(12位)

  依據以下步驟進行轉換:

  1、從cr3中取出進程的頁目錄位址(作業系統負責在調度進程的時候,把這個地址裝入對應寄存器);

  2、根據線性地址前十位,在數組中,找到對應的索引項目,因為引入了二級管理員模式,頁目錄中的項,不再是頁的地址,而是一個頁表的地址。(又引入了一個數組),頁的地址被放到頁表中去了。

  3、根據線性地址的中間十位,在頁表(也是數組)中找到頁的起始地址;

  4、將頁的起始地址與線性地址中最後12位相加,得到最終我們想要的葫蘆;

  這個轉換過程,應該說還是非常簡單地。全部由硬體完成,雖然多了一道手續,但是節約了大量的記憶體,還是值得的。那麼再簡單地驗證一下:

  1、這樣的二級模式是否仍能夠表示4G的地址;頁目錄共有:2^10項,也就是說有這麼多個頁表.每個目表對應了:2^10頁;
每個頁中可定址:2^12個位元組。還是2^32 = 4GB

  2、這樣的二級模式是否真的節約了空間;也就是算一下頁目錄項和頁表項共占空間(2^10 *4 + 2 ^10 *4) = 8KB。哎,……怎麼說呢!!!
紅色錯誤,標註一下,後文貼中有此討論。。。。。。按<深入理解電腦系統>中的解釋,二級模式空間的節約是從兩個方面實現的:

  A、如果一級頁表中的一個頁表條目為空白,那麼那所指的二級頁表就根本不會存在。這表現出一種巨大的潛在節約,因為對於一個典型的程式,4GB虛擬位址空間的大部份都會是未分配的;

  B、只有一級頁表才需要總是在主存中。虛擬儲存空間系統可以在需要時建立,並頁面調入或調出二級頁表,這就減少了主存的壓力。只有最經常使用的二級頁表才需要緩衝在主存中。——不過Linux並沒有完全享受這種福利,它的頁表目錄和已指派頁面相關的頁表都是常駐記憶體的。值得一提的是,雖然頁目錄和頁表中的項,都是4個位元組,32位,但是它們都只用高20位,低12位屏蔽為0——把頁表的低12屏蔽為0,是很好理解的,因為這樣,它剛好和一個頁面大 小對應起來,大家都成整數增加。計算起來就方便多了。但是,為什麼同時也要把頁目錄低12位屏蔽掉呢?因為按同樣的道理,只要屏蔽其低10位就可以了,不
過我想,因為12>10,這樣,可以讓頁目錄和頁表使用相同的資料結構,方便。

  本貼只介紹一般性轉換的原理,擴充分頁、頁的保護機制、PAE模式的分頁這些麻煩點的東東就不囉嗦了……可以參考其它專業書籍。

  5.Linux的頁式記憶體管理

  原理上來講,Linux只需要為每個進程分配好所需資料結構,放到記憶體中,然後在調度進程的時候,切換寄存器cr3,剩下的就交給硬體來完成了 (呵呵,事實上要複雜得多,不過偶只分析最基本的流程)。前面說了i386的二級頁管理架構,不過有些CPU,還有三級,甚至四級架構,Linux為了在 更高層次提供抽像,為每個CPU提供統一的介面。提供了一個四層頁管理架構,來相容這些二級、三級、四級管理架構的CPU。這四級分別為:

  頁全域目錄PGD(對應剛才的頁目錄)

  頁上級目錄PUD(新引進的)

  頁中間目錄PMD(也就新引進的)

  頁表PT(對應剛才的頁表)。

  整個轉換依據硬體轉換原理,只是多了二次數組的索引罷了,如:
 
  那麼,對於使用二級管理架構32位的硬體,現在又是四級轉換了,它們怎麼能夠協調地工作起來呢?嗯,來看這種情況下,怎麼來劃分線性地址吧!從硬體的角度,32位地址被分成了三部份——也就是說,不管理軟體怎麼做,最終落實到硬體,也只認識這三位老大。

  從軟體的角度,由於多引入了兩部份,,也就是說,共有五部份。——要讓二層架構的硬體認識五部份也很容易,在地址劃分的時候,將頁上級目錄和頁 中間目錄的長度設定為0就可以了。這樣,作業系統見到的是五部份,硬體還是按它死板的三部份劃分,也不會出錯,也就是說大家共建了和諧電腦系統。

  這樣,雖說是多此一舉,但是考慮到64位地址,使用四層轉換架構的CPU,我們就不再把中間兩個設為0了,這樣,軟體與硬體再次和諧——抽像就是強大呀!!!

  例如,一個邏輯地址已經被轉換成了線性地址,0x08147258,換成二制進,也就是:

  0000100000 0101000111 001001011000

  核心對這個地址進行劃分,

  PGD = 0000100000
  PUD = 0
  PMD = 0
  PT = 0101000111
  offset = 001001011000

  現在來理解Linux針對硬體的花招,因為硬體根本看不到所謂PUD,PMD,所以,本質上要求PGD索引,直接就對應了PT的地址。而不是再 到PUD和PMD中去查數組(雖然它們兩個線上性地址中,長度為0,2^0 =1,也就是說,它們都是有一個數組元素的數組),那麼,核心如何合理安排地址呢?

  從軟體的角度上來講,因為它的項只有一個,32位,剛好可以存放與PGD中長度一樣的地址指標。那麼所謂先到PUD,到到PMD中做映射轉換, 就變成了保持原值不變,一一轉手就可以了。這樣,就實現了“邏輯上指向一個PUD,再指向一個PDM,但在物理上是直接指向相應的PT的這個抽像,因為硬 件根本不知道有PUD、PMD這個東西”。然後交給硬體,硬體對這個地址進行劃分,看到的是:

  頁目錄 = 0000100000

  PT = 0101000111

  offset = 001001011000

  嗯,先根據0000100000(32),在頁目錄數組中索引,找到其元素中的地址,取其高20位,找到頁表的地址,頁表的地址是由核心動態分配的,接著,再加一個offset,就是最終的物理地址了。

核心記憶體配置管理

記憶體管理方法應該實現以下兩個功能:

  • 最小化管理記憶體所需的時間
  • 最大化用於一般應用的可用記憶體(最小化管理開銷)

1.直接堆分配

每個記憶體管理器都使用了一種基於堆的分配策略。在這種方法中,大塊記憶體(稱為 )用來為使用者定義的目的提供記憶體。當使用者需要一塊記憶體時,就請求給自己分配一定大小的記憶體。堆管理器會查看可用記憶體的情況(使用特定演算法)並返回一塊記憶體。搜尋過程中使用的一些演算法有first-fit(在堆中搜尋到的第一個滿足請求的記憶體塊)和best-fit(使用堆中滿足請求的最合適的記憶體塊)。當使用者使用完記憶體後,就將記憶體返回給堆。

這種基於堆的分配策略的根本問題是片段(fragmentation)。當記憶體塊被分配後,它們會以不同的順序在不同的時間返回。這樣會在堆中留下一些洞,需要花一些時間才能有效地管理空閑記憶體。這種演算法通常具有較高的記憶體使用量效率(分配需要的記憶體),但是卻需要花費更多時間來對堆進行管理。

2.夥伴分配演算法

另外一種方法稱為 buddy memory allocation,是一種更快的記憶體配置技術,它將內 存劃分為 2 的冪次方個分區,並使用 best-fit 方法來分配記憶體請求。當使用者釋放記憶體時,就會檢查 buddy 塊,查看其相鄰的記憶體塊是否也已經被釋放。如果是的話,將合并記憶體塊以最小化記憶體片段。這個演算法的時間效率更高,但是由於使用 best-fit 方法的緣故,會產生記憶體浪費。

3.slab

關於slab 分配器有很多文檔介紹。簡單的說就是核心經常申請固定大小的

一些記憶體空間,這些空間一般都是結構體。而這些結構體往往都會有一個共同的初始化行為比如:初始化裡面的訊號量、鏈表指標、成員。通過Sun 的大牛JeffBonwick 的研究發現,核心對這些結構體的初始化所消耗的時間比分配它們的時間還要長。所以他設計了一種演算法,當這些結構體的空間被釋放的時候,只是讓他回到剛剛分配好的狀態而不真正釋放,下次再申請的時候就可以節約初始化的時間。整個過程可以理解為借用白板的過程。申請空間就是從別人那裡借多塊白板。由於每塊白板的用處不同,每次用的時候都要先在不同的白板上畫上不同的表格,然後往裡面填內容。如果一般的演算法則是用完白板後,直接還給人家,下次要用的時候再借回來然後畫好表格。最佳化一點的演算法就是用完後暫時不還人家,人家要用的時候再還,第二次再要用白板的時候隨便取一塊白板重新畫表格。而使用slab
演算法就是不用白板的時候擦除表格的內容留下表格,白板也暫時不還人家。下次要用的時候根據用途取出正確的白板,由於表格是現成的直接往裡面填內容就可以了。省去了借白板和畫表格這兩個操作。

一、slab分配器的基本觀點

*  slab分配器把記憶體區看作對象(object),把包含快取的主記憶體區劃分為多個slab;

*  slab分配器把對象按照類型分組放進快取,每個快取都是同種類型對象的一種“儲備”;

*  每個slab由一個或多個連續的頁框組成,這些頁框中包含已指派的對象,也包含閒置對象;

*  slab分配器通過夥伴系統分配頁框。

二、slab 緩衝分配器的優點

1)、核心通常依賴於對小對象的分配,它們會在系統生命週期內進行無數次分配。slab緩衝分配器通過對類似大小的對象進行緩衝而提供這種功能,從而避免了常見的片段問題。2)、slab 分配器還支援通用對象的初始化,從而避免了為同一目的而對一個對象重複進行初始化。3)、slab 分配器還可以支援硬體緩衝對齊和著色,這允許不同緩衝中的對象佔用相同的緩衝行,從而提高緩衝的利用率並獲得更好的效能。

uClibc-0.9.28中的malloc可以調用mmap,從而與物理地址聯絡起來,也可以通過sbrk,從而與核心之間的記憶體管理聯絡起來,猜測可能也會經過slab。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.