作業系統的記憶體管理你知道嗎

大雄45發表於2023-03-30
導讀 brk()的作用也只是通知 核心哪個範圍的堆記憶體是可用的,真正的實體記憶體頁是在程式實際讀寫記憶體的時候才會申請,而且是由核心根據寫時複製/需求載入自動完成的,應用程式感知不到這點。

記憶體管理,是作業系統的主要功能。

作業系統從啟動一直到建立0號程式(idle程式),執行的大部分程式碼都跟記憶體有關。

作業系統的記憶體管理,大概分這麼幾個層次:

1.實體記憶體管理

實體記憶體是電腦上的真實記憶體大小,這個資料可以透過BIOS獲取。

在分頁之後,實體記憶體的管理結構是個陣列,每項表示1個實體記憶體頁,每頁4096位元組。

如下圖:

作業系統的記憶體管理你知道嗎作業系統的記憶體管理你知道嗎

實體記憶體的管理結構

在一個簡單的核心demo裡,實體記憶體頁的管理結構可以只有一項:

atomic_t refs;

即,實體記憶體頁的引用計數:計數為0表示空閒,> 0表示正在使用,具體數字表示共享這一頁的程式個數。

簡單的核心demo一般是不支援SMP架構的,所以自旋鎖(spinlock)也就省了。

在對稱多處理器(SMP)的CPU上,因為全域性資料結構會被多個CPU併發訪問,所以要加自旋鎖。

那麼,實體記憶體頁的管理結構至少有2項:

atomic_t spinlock;
atomic_t refs;

自旋鎖的作用,與應用程式裡的鎖(mutex)差不多,只是它在獲取失敗之後會不斷地再次獲取,直到成功。

void spin_lock(atomic_t* lock)
{
while (spin_trylock(lock) == 0);
}

這就是給自旋鎖加鎖的函式,while迴圈直到成功,不成功時就自旋在那裡一直轉圈,所以叫自旋鎖。

它在(對稱多處理器)SMP環境裡用於保護共享的資料結構:當一個CPU持有自旋鎖時,另一個CPU沒法訪問共享資料。

如果是單個CPU的環境,沒必要用自旋鎖,直接關閉中斷就行了。

單個CPU的情況下,關了中斷就可以阻止核心的併發,共享資料也就不會被踩踏了。

但多個CPU必須使用自旋鎖,因為關中斷只能關閉當前CPU的,沒法關其他CPU的:這時需要自旋鎖保護共享資料。

實體記憶體的管理陣列,是最重要的全域性共享資料。

當需要給一個程式申請記憶體的時候,哪個記憶體頁是空閒的,哪個已經被使用了,全靠檢視這個陣列。

加自旋鎖的時候一定要先關中斷,因為如果在加了鎖之後、關中斷之前、正好有個中斷來了,而在中斷處理函式里再次請求加同一個鎖,那就會遞迴死鎖了。

Linux核心的關中斷加鎖的函式叫:spin_lock_irqsave().

Linux核心的分配實體記憶體頁的函式叫:get_free_pages(),它可以分配1頁或連續的多頁記憶體。

如果分配多頁記憶體的話,起始地址是要按頁數對齊的。

2.虛擬記憶體管理

虛擬記憶體都是透過程式的頁表管理的。

為了節省實體記憶體,新建立的程式是與父程式共享同一套實體記憶體頁的。

只有新程式要寫某個記憶體頁時,才會給它複製一份新的實體記憶體頁,然後取消該頁與父程式的共享,這就是寫時複製。

作業系統的記憶體管理你知道嗎作業系統的記憶體管理你知道嗎

寫時複製的過程

寫時複製的過程:

1)申請一個新記憶體頁,

2)把老記憶體頁的內容,複製到新記憶體頁上,

3)把新記憶體頁的地址填入子程式的頁表,

4)把老記憶體頁的引用計數減1。

所以,新程式剛被建立出來時,它的使用者空間並沒有自己的實體記憶體頁,只有當執行需要時才一點點地透過寫時複製新增,以讓實體記憶體最大限度的空閒著。

另一個讓實體記憶體最大限度空閒著的機制,就是需求載入:

1)當mmap一個檔案時,作業系統並不會直接為這個檔案分配記憶體,並且把它的內容載入到記憶體裡,

2)而是當程式真去讀這個檔案的某一部分時,才給它申請實體記憶體頁,並且把這一部分內容從磁碟讀到記憶體。

copy on write,load on read.

不到火燒眉毛的時候, 是不會把實體記憶體給程式的

3.使用者態的記憶體函式

以上的這些機制都是OS核心裡的,應用程式的程式碼不需要管這些。

應用程式分配記憶體的最 底層函式,就是brk()系統呼叫。

作業系統的記憶體管理你知道嗎作業系統的記憶體管理你知道嗎

brk()是一個系統呼叫,它的作用就是修改應用程式的資料段的結尾,從而分配或回收應用程式的堆空間。

作業系統的記憶體管理你知道嗎作業系統的記憶體管理你知道嗎

C庫裡的把它封裝成了sbrk()和brk()兩個函式,讓它使用起來更符合人們的習慣:

sbrk()用於申請記憶體:void* sbrk(int increment);

brk()用於回收記憶體:int brk(void* addr);

實際上,Linux系統只有1個brk()系統呼叫,它既設定程式資料段的末尾,又會把這個值返回給應用程式。

作業系統的記憶體管理你知道嗎作業系統的記憶體管理你知道嗎

Linux核心的標頭檔案裡,brk()系統呼叫的處理函式sys_brk()是這麼定義的,如上圖。

如果想直接使用系統呼叫,可以使用Linux的syscall()函式,依次傳入呼叫號和引數列表,就可以看到哪些是真實的系統呼叫,哪些是C庫的封裝。

syscall()函式的宣告是:long syscall(long number, ...);

它的引數是可變的,系統呼叫的引數最多隻有6個,因為暫存器的個數有限。

在sbrk() 和 brk()的基礎上再封裝,就是人們經常使用的malloc() 和 free()了。

malloc() 申請的記憶體是一塊塊的,可以不按次序釋放,而不影響使用。

brk() 和 sbrk() 申請的記憶體必須按次序釋放,因為它會修改程式的資料段結尾:

資料段結尾(brk)之外的堆空間如果被使用,就屬於段錯誤。

所以,Linux man手冊裡說明了,應用程式不要用sbrk()和brk()申請和釋放記憶體。

brk()的作用也只是通知Linux核心哪個範圍的堆記憶體是可用的,真正的實體記憶體頁是在程式實際讀寫記憶體的時候才會申請,而且是由核心根據寫時複製/需求載入自動完成的,應用程式感知不到這點。

Linux還會把不常用的實體記憶體頁交換到磁碟上(即swap分割槽),以騰出更多的記憶體。

所以,在記憶體不足時,磁碟的讀寫頻次也會升高。

原文來自:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2929380/,如需轉載,請註明出處,否則將追究法律責任。

相關文章