深入理解Linux記憶體管理機制(一)

2021-09-23 21:21:11 字數 4181 閱讀 2386

深入理解linux記憶體管理機制(一)通過本文,您即可以:

1. 儲存器硬體結構;

2.分段以及對應的組織方式;

3.分頁以及對應的組織方式。

注1:本文以linux核心2.6.32.59本版為例,其對應的**可以在

找到。

注2:本文所有的英文專有名詞都是我隨便翻譯的,請對照英文原文進行理解。

注3:推薦使用source insight進行原始碼分析。

記憶體組織

計算機記憶體屬於

隨機儲存器

(ram),目前pc機廣泛使用的是ddr

sdram,即「雙倍速率同步動態隨機儲存器」,其本質上仍然是由n bits*m kb個記憶體晶元組成的,比如如果我們需要8位64kb的記憶體,則我們就需要2*8=16塊4bits*8kb的記憶體塊。由於計算機通常是以

位元組(byte)進行資料交換的,所以對記憶體的位址編碼一般使用位元組,如上我們有64kb記憶體,則其位址編碼為0x0000~0xffff,稱為實體地址。對於32位機來說,由於其「

位址暫存器

(ar)」是32位,也就限制了其記憶體的最大定址範圍是2^32=4gb。

linux將實體地址按4kb的大小劃分成「幀(frame)」。為什麼是4kb?因為每乙個幀都需要用乙個c結構體來描述,稱之為「幀描述單元(frame discriptor)」,如果太小,幀描述單元顯然太多了,如果太大,那麼在記憶體分配時又會造成「

內碎片(inner

fragments)」。早些時候,計算機的記憶體址都是直接對映的,由於程式裡的位址是寫死的,這就意味著每段程式每次都只能對映對應的位址空間。這無論對程式設計者與系統都是相當大的負擔。linux使用「分段

」加「分頁

address)、線性位址(linear address)與實體地址(physics address)。

另外,linux支援眾多cpu架構,這裡只研究x86的,對應的源**為:.../x86/... 路徑。

linux中的分段

linux並不使用太多的分段,原因是某些risc機器對分段的支援不好。為此linux的分段都存在「全域性描述表(gdt)」中,gdt是乙個全域性desc_struct陣列(位於linux-2.6.32.59archx86includeasm),其結構如下:

#define gdt_entries 16

struct desc_struct gdt[gdt_entries];  

struct desc_struct ;  

struct ;  

};  

} __attribute__((packed));  

所以我們可以看出,段描述結構體占8個位元組,至於裡面的a,b,那是老的方式,後來使用c++ struts的bit fields後更方便了。type型別由以下幾種:

enum ;  

linux主要使用以下幾種段:

其它型別可以參見linux-2.6.32.59archx86includeasmsegment.h,裡面有非常詳細的說明。

它們都儲存在「全域性描述符表(gdt)」。linux本身並不使用「區域性描述符表(ldt)」,當乙個程序被建立時,其指向的是乙個預設的ldt,不過系統並不阻止程序建立它。也就是說乙個程序最多兩個段描述符:tss與ldt。由於segment selector為16位(為什麼只有16位,這個就是歷史原因了,由於x86在real mode下段位址只有20位,其中有效的就是16位,詳見:

x86

memory segmentation,但linux段內偏移位址高達32位,所以線性位址總共是48位),其中有效的索引位僅有13位,所以gdt的最大長度為2

13-1=8192,除去系統保留的12個,留給程序的只有8180個入口,那麼就意味linux程序的最大數為8180/2=4090。需要注意的是,程序在建立的時候並不會馬上建立自己的ldt,其指向的是gdt乙個預設的ldt,裡面的sd為null。只有在需要的時候程序才建立自己的ldt並把它放入gdt中。所以不管是ldt也好,tss也好,它們都存放在gdt裡面。而對於ucs與uds,所有的程序共享乙個。這樣位址空間不會重複嗎?不會,因為線性不是最終的實體地址,每個程序還有自己的頁表,所以最終對映到實體地址是不同的。

下面我們來看看段中位址是如何轉換的。假設我們需要訪問核心資料段的0x00124部分,由**知其gdt的入口為13,那麼其對應的記憶體位址=gdtr+13*8+0x00124,假設gptr為0x02000,則最終的結果為0x02228。gdtr是乙個暫存器,其為48位,用來儲存gdt的第乙個位元組線性位址與表限。

分頁

相對於分段來說,分頁更主流更流行一些。原因是其更靈活,其能把不同的線性位址對映到同乙個實體地址上,缺點是記憶體必須以頁大小的整數倍分配。按現在主流的4kb一頁來說,如果程式只申請100b的資料,那記憶體浪費還是相當的大。為此,linux使用了一種稱為slab的方法來解決這個問題,後面的文章會講到。

因為頁表本身也需要儲存空間,按每頁32b來算,對於4gb記憶體,每頁4kb,共有1m頁,則頁表的大小為32mb,這顯然不可以接受,所以後來出現了

多級頁表

這個概念。2023年後linux版本使用的是四級頁表:第一級叫「全域性目錄(page

global directory)「、第二級叫「頁上級目錄(page

upper directory)」、第**叫」頁中間目錄(page

middle derectory)」、第四級叫」頁面表(page

table entry)」,最後頁內偏移量「offset」.

cr3是乙個暫存器,它儲存「global dir」的位址。當程序切換發生時,它將被儲存在tss中,前面說過了tss段表是每個程序乙個。分頁在linux內使用的地方很多,特別是程序內的位址轉換。分頁有硬體支援的,特別是

旁路轉換緩衝(translation

lookaside buffer)的出現,使用即使使用**頁表的linux在地轉轉換中的實際效果也是非常好的。與段表所有的程序都共用乙個的是,每個程序都擁有自己的分頁。其實也正是因為所有程序都共享乙個段表,每個程序才必須有自己的頁表,否則相同的linear位址如何對映到不同的實體地址去?下面我們著重來研究一下linux系統中是如何表示分頁中所用到的資料結構的。

每個「幀」在linux中都是以乙個名為page(位於linux-2.6.32.59includelinuxmm_types.h)的結構體來儲存的。所有的頁被放在乙個型別為page名為mem_map的陣列中(位於linux-2.6.32.59mmmemory.c)。**如下(為了顯示方便,僅列出部分:

struct page ;  

union ;  

...  

};  

union ;  

struct list_head lru;       /* 指向最近被使用的頁的雙向鍊錶,cache相關*/

};  

下面我們再來看看pgd頁表。每個程序的mm_struct->pgd(位於:linux-2.6.32.59includelinuxmm_types.h)指向自己的pgd:

struct mm_struct   

可以看出pdg實際上是乙個pgd_t結構陣列,pgd_t在x86系統中就是乙個usinged long,其指向的就是下一級頁表的位址。就這樣找下去,直到找到對應的頁為止,再加上頁內偏移,就可以進行記憶體訪問了。

例如線性位址為:0x91220b01,如果pgd、pud、pmd以及pte均5位。頁內偏移12位,即頁大小

那麼這段記憶體的解析步驟是:

pgd號為24,查pgd[24]得到pud入口;

pud號為4,再查pud[4];

pmd號為36,再查pmd[36];

pte號為2,再查pte[2];

需要補充的是,並不是所有的記憶體都是使用「分頁」,在核心初始化的時候,有100mb記憶體的樣子是使用直接對映的,這是因為總是要先裝入分頁的初始化**才能進行頁表初始化。

總結:不知不覺也寫了不少了。這次我們介紹了作業系統最基本的記憶體管理概念「分段」與「分頁」在linux中的實現,可以看出其與通過的概念還是很接近的。這正證明了基礎知識的重要性。下一次我們將介紹linux的記憶體初始化過程,如頁表的建立與初始化。

------------------一些資源與參考-------------------

linux slub 分配器詳解:

page frame management:

linux memory management:

linux記憶體管理**:

linux記憶體之頁表:

深入理解Linux記憶體管理機制(一)

一 記憶體組織 計算機記憶體屬於隨機儲存器 ram 目前pc機廣泛使用的是ddr sdram,即 雙倍速率同步動態隨機儲存器 其本質上仍然是由n bits m kb個記憶體晶元組成的,比如如果我們需要8位64kb的記憶體,則我們就需要2 8 16塊4bits 8kb的記憶體塊。由於計算機通常是以位元...

深入理解Linux記憶體管理機制(一)

深入理解linux記憶體管理機制 一 通過本文,您即可以 1.儲存器硬體結構 2.分段以及對應的組織方式 3.分頁以及對應的組織方式 注1 本文以linux核心2.6.32.59本版為例,其對應的 可以在 找到。注2 本文所有的英文專有名詞都是我隨便翻譯的,請對照英文原文進行理解。注3 推薦使用so...

深入FDO 記憶體管理機制

呼叫某些fdo的函式,如建立方法,需要申請記憶體,而這些記憶體需要在適當的時機釋放,以免記憶體洩漏。fdo使用了引用計數的方式來實現管理物件的生命週期,每個物件都維護著乙個引用計數,只有當這個物件的引用計數變為0時,才會去釋放這個物件。所以,fdo中每個類都從fdoidisposable類繼承而來的...