內(nèi)存管理的主要工作就是對物理內(nèi)存進行組織,然后對物理內(nèi)存的分配和回收。但是Linux引入了虛擬地址的概念。
【資料圖】
虛擬地址的作用
如果用戶進程直接操作物理地址會有以下的壞處:
1、 用戶進程可以直接操作內(nèi)核對應的內(nèi)存,破壞內(nèi)核運行。
2、 用戶進程也會破壞其他進程的運行
CPU中寄存器中存儲的是邏輯地址,需要進行映射才能轉(zhuǎn)化為對應的物理地址,然后獲取對應的內(nèi)存。
通過引入邏輯地址,每個進程都擁有單獨的邏輯地址范圍。
當進程申請內(nèi)存的時候,會為其分配邏輯地址和物理地址,并將邏輯地址和物理地址做一個映射。
所以,Linux內(nèi)存管理涉及到了以下三個部分:
1、物理內(nèi)存
物理內(nèi)存的組織
Linux 中內(nèi)存分為 3 個級別,從下到上依次為:
1、Page:一個 page 的大小為4k, Page 是內(nèi)存的一個最基本的單位。
2、Zone:Zone 中提供了多個隊列來管理 page。
Zone分為 3 種
2.1、 ZONE_DMA:用來存放 DMA 讀取 IO 設備的數(shù)據(jù),內(nèi)核專用
2.2、 ZONE_NORMAL:用來存放內(nèi)核的相關數(shù)據(jù),內(nèi)核專用
2.3、 ZONE_HIGHMEM:高端內(nèi)存,用來存放用戶進程數(shù)據(jù)
3、Node 節(jié)點,一個 CPU 對應著一個 Node,一個 Node 包括一個 Zone_DMA、 ZONE_NORMAL、ZONE_HIGHMEM。
同時當一個 CPU 對應的內(nèi)存用光后,可以申請其他 CPU 對應的內(nèi)存。
物理內(nèi)存的分配
Linux將內(nèi)存分配分為兩種:
1、大內(nèi)存
大內(nèi)存利用伙伴系統(tǒng)分配。
伙伴系統(tǒng)的做法是將 ZONE 中的 Page 分組,然后組裝為多個鏈表。鏈表中存放的是頁塊的集合。頁塊對應著有不同的大小,分別為 1、2、4、8 … 1024個頁。
當請求 (2i-1 ,2i] 大小的 page 的時候,會直接請求 2i 個頁, 如果對應的鏈表中有對應的頁塊,就直接分配。如果對應的鏈表沒有,就往上找 2i+1,如果 2i+1 存在,就將其分為 2 個 2i 頁塊,將其中 1 個 2i 加入到對應的鏈表中,將另外一個分配出去。
例如,要請求一個 128 個頁的頁塊時,先檢查 128 個頁的頁塊鏈表是否有空閑塊。如果沒有,則查 256 個
的頁塊鏈表;如果有空閑塊的話,則將 256 個頁的頁塊分成兩份,一份使用,一份插入 128 個頁的頁塊鏈表中。如果還是沒有,就查 512 個頁的頁塊鏈表;如果有的話,就分裂為 128、128、256 三個頁塊,一個 128 的使用,剩余兩個插入對應頁塊鏈表。
2、小內(nèi)存分配
小內(nèi)存分配利用slub分配,比如對象等數(shù)據(jù) slub 就是 將幾個頁單獨拎出來作為緩存,里面維護了鏈表。每次直接從鏈表中獲取對應的內(nèi)存,用完之后也不用清空,就直接掛到鏈表上,然后等待下次利用。
2、如何組織虛擬地址
虛擬地址對應的是虛擬空間,虛擬空間只不過是一個虛擬地址的集合,用來映射物理內(nèi)存。
虛擬空間分為用戶態(tài)和內(nèi)核態(tài)。
32位系統(tǒng)中將虛擬空間按照 1:3 的比例分配給內(nèi)核態(tài)和用戶態(tài)。
64位系統(tǒng)中分別給 內(nèi)核態(tài) 和 用戶態(tài) 分配了 128T。
用戶態(tài)結(jié)構
每個進程 都會 對應一個 用戶態(tài)虛擬空間, 里面存放了 Text(代碼)的內(nèi)存虛擬地址范圍、 Data(數(shù)據(jù))的內(nèi)存虛擬地址范圍、BSS(全局變量)的內(nèi)存虛擬地址范圍、堆的虛擬地址范圍、棧的虛擬地址范圍,以及mmap 內(nèi)存映射區(qū)。
其中 mmap 用于申請動態(tài)內(nèi)存的時候的映射,堆和棧都是動態(tài)變化的。
一個進程對應的用戶態(tài)中的 各個方面的虛擬地址信息都通過一個 struct 來存儲在內(nèi)存中,當創(chuàng)建進程的時候會為其分配內(nèi)存存儲對應的虛擬地址信息。
內(nèi)核態(tài)結(jié)構
Linux 的內(nèi)核程序共用一個內(nèi)核態(tài)虛擬空間。其中分為了以下幾部分:
1、直接映射區(qū)
896M,內(nèi)核空間直接映射到對應的ZONE_DMA和ZONE_NORMAL中。為什么叫做直接映射呢?邏輯地址 直接 減去對應的差值就可以得到對應的物理地址。固定死了。
2、動態(tài)映射
為什么要引入動態(tài)映射呢?因為所有物理內(nèi)存的分配都需要內(nèi)核程序進行申請,用戶進程沒有這個權限。所以內(nèi)核空間一定要能映射到所有的物理內(nèi)存地址。
那么如果都采用直接映射的話,1G大小邏輯地址的內(nèi)核空間只能映射1G大小的物理內(nèi)存。
所以引入了動態(tài)映射,動態(tài)映射就是 內(nèi)核空間的邏輯地址可以映射到 物理內(nèi)存中的ZONE_HIGHMEM(高端內(nèi)存)中的任何一個地址,并且在對應的物理內(nèi)存使用完之后,可以再映射其他物理內(nèi)存地址。
動態(tài)映射分為三種:
1、動態(tài)內(nèi)存映射:使用完對應的物理內(nèi)存后,就可以映射其他物理內(nèi)存了。
2、永久內(nèi)存映射:一個虛擬地址只能映射一個物理地址。如果需要映射其他物理地址,需要解綁。
3、固定內(nèi)存映射:只能被某些特定的函數(shù)來調(diào)用引用物理地址。
動態(tài)內(nèi)存映射和直接映射的區(qū)別
動態(tài)映射和直接映射的區(qū)別就是邏輯地址到物理地址的轉(zhuǎn)化規(guī)則。
直接映射
直接映射的規(guī)則是死的,一個邏輯地址對應的物理地址是固定的。通過邏輯地址加或者減去一個數(shù),就可以得到對應的物理地址。
動態(tài)映射
動態(tài)映射是動態(tài)的綁定,每個邏輯地址對應的物理地址是動態(tài)的,通過頁表進行查詢。
用戶空間映射:
用戶空間采用動態(tài)映射,每個虛擬地址可以被映射到一個物理地址,映射到ZONE_HIGHMEM。
為什么用戶空間不采用直接映射呢?
因為物理內(nèi)存是多個進程所有的,每個進程都有一個用戶空間。如果采用直接映射的話,對應的物理地址是會沖突的。其用戶空間的邏輯地址大小都為 3G,所以存在邏輯地址相同,但是對應的物理地址不同。需要通過頁表來轉(zhuǎn)化,一個進程會對應一個頁表。
3、如何將虛擬地址映射到物理內(nèi)存
虛擬地址通過頁表將虛擬地址轉(zhuǎn)化為物理地址,每個進程都對應著一個頁表,內(nèi)核只有一個頁表。
虛擬空間 和 物理內(nèi)存 都按照 4k 來分頁,一個虛擬空間中的頁 和 物理內(nèi)存中頁 是 一一對應的。
頁表映射
如上圖所示,將虛擬地址中的頁號 通過頁表轉(zhuǎn)化為 對應的物理頁號,然后通過頁內(nèi)偏移量 就可以得到對應的 物理地址了。
但是 1 個進程就需要一個頁表,一個 4G的內(nèi)存條,就需要 1M 個頁表記錄來描述,假如 1 個 頁表記錄需要 4個字節(jié),那么就需要 4MB。而且頁表記錄是通過下標來對應的,通過虛擬頁號來乘以對應的頁表項大小來計算得到對應的地址的。
所以 Linux 將 4M 分為 1K 個 4K, 一個 4K 對應著一個 page,用來存儲對應的真正的頁表記錄。將 1K 個 page 分開存放,就不要求連續(xù)的 4M 了。
如果將 4M 分成 1K 個離散的 page 的話,怎么虛擬地址對應的頁表號呢?
利用指針,存儲 1K 個地址,分別指向這 1K 個 page, 地址的大小為 4 個字節(jié),也就是32位,完全可以表示整個內(nèi)存的地址范圍。
1K * 4個字節(jié),正好是一個 page 4k,所以 也就是利用 1 個 page來存儲對應的頁表記錄索引。
所以 我們的虛擬地址尋找過程如下:
1、找到對應的頁表記錄索引位置,因為有 1K 個索引,所以用 10 位就可以表示了
2、通過索引可以找到對應的真正的頁表地址,對應的有 1K 個頁表記錄,所以用 10 位就可以表示了
3、1個頁有 4K,通過 12 位就可以表示其頁內(nèi)偏移量了。
所以虛擬地址被分為了三部分:
1、10位 表示索引偏移
2、10位 表示頁表記錄偏移
3、 12位 表示頁內(nèi)偏移
雖然這種方式增加了索引項,進一步增加了內(nèi)存,但是減少了連續(xù)內(nèi)存的使用,通過離散的內(nèi)存就可以存儲頁表。
這是對于32位系統(tǒng),而 64 位系統(tǒng)采用了5級頁表。
映射流程圖
用戶態(tài)申請內(nèi)存時,只會申請對應的虛擬地址,不會直接為其分配物理內(nèi)存,而是等到真正訪問內(nèi)存的時候,產(chǎn)生缺頁中斷,然后內(nèi)核才會為其分配,然后為其建立映射,也就是建立對應的頁表項。
TLB
TLB 就是一個緩存,放在 CPU 中。用來將虛擬地址和對應的物理地址進行緩存。當查詢對應的物理地址的時候,首先查詢 TLB,如果TLB中存在對應的記錄,就直接返回。如果不存在,就再去查詢頁表。
虛擬內(nèi)存
虛擬內(nèi)存指的是 將硬盤中劃出一段 swap 分區(qū) 當作 虛擬的內(nèi)存,用來存放內(nèi)存中暫時用不到的內(nèi)存頁,等到需要的時候再從 swap 分區(qū)中 將對應的內(nèi)存頁調(diào)入到 內(nèi)存中。硬盤此時相當于一個虛擬的內(nèi)存。
從邏輯上能夠運行更大內(nèi)存的程序,因為程序運行的時候并不需要把所有數(shù)據(jù)都加載到內(nèi)存中,只需要將當前運行必要的相關程序和數(shù)據(jù)加載到內(nèi)存中就可以了,當需要其他數(shù)據(jù)和程序的時候,再將其調(diào)入。
相較于真正的內(nèi)存加載,虛擬內(nèi)存需要將數(shù)據(jù)在內(nèi)存和磁盤中不斷切換,這是一個耗時的操作,所以速度比不上真正的內(nèi)存加載。
總結(jié)
虛擬空間 和 物理內(nèi)存 都分為 內(nèi)核空間 和 用戶空間。
虛擬地址需要通過頁表轉(zhuǎn)化為物理地址,然后才能訪問。
用戶虛擬空間 只能映射 物理內(nèi)存中的用戶內(nèi)存,無法映射到物理內(nèi)存中的內(nèi)核內(nèi)存,也就是說,用戶進程只能操作用戶內(nèi)存。
內(nèi)核空間 只能被 內(nèi)核 申請使用,用戶進程只能操作用戶空間的物理內(nèi)存和虛擬空間。
當用戶進程 調(diào)用系統(tǒng)調(diào)用的時候,會將其對應的代碼和數(shù)據(jù)運行在內(nèi)核空間中。
所以當調(diào)用 內(nèi)核空間 讀取文件或者網(wǎng)絡數(shù)據(jù)的時候,首先會將數(shù)據(jù)拷貝到內(nèi)存空間,然后在將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間。因為 用戶進程不能訪問內(nèi)核空間。
審核編輯:湯梓紅
標簽: