聊聊操作系統(tǒng)的內(nèi)存管理
?內(nèi)存管理,是操作系統(tǒng)的主要功能。
操作系統(tǒng)從啟動一直到創(chuàng)建0號進程(idle進程),運行的大部分代碼都跟內(nèi)存有關(guān)。
操作系統(tǒng)的內(nèi)存管理,大概分這么幾個層次:
1.物理內(nèi)存管理
物理內(nèi)存是電腦上的真實內(nèi)存大小,這個數(shù)據(jù)可以通過BIOS獲取。
在分頁之后,物理內(nèi)存的管理結(jié)構(gòu)是個數(shù)組,每項表示1個物理內(nèi)存頁,每頁4096字節(jié)。
如下圖:
物理內(nèi)存的管理結(jié)構(gòu)
在一個簡單的內(nèi)核demo里,物理內(nèi)存頁的管理結(jié)構(gòu)可以只有一項:
即,物理內(nèi)存頁的引用計數(shù):計數(shù)為0表示空閑,> 0表示正在使用,具體數(shù)字表示共享這一頁的進程個數(shù)。
簡單的內(nèi)核demo一般是不支持SMP架構(gòu)的,所以自旋鎖(spinlock)也就省了。
在對稱多處理器(SMP)的CPU上,因為全局數(shù)據(jù)結(jié)構(gòu)會被多個CPU并發(fā)訪問,所以要加自旋鎖。
那么,物理內(nèi)存頁的管理結(jié)構(gòu)至少有2項:
自旋鎖的作用,與應(yīng)用程序里的鎖(mutex)差不多,只是它在獲取失敗之后會不斷地再次獲取,直到成功。
這就是給自旋鎖加鎖的函數(shù),while循環(huán)直到成功,不成功時就自旋在那里一直轉(zhuǎn)圈,所以叫自旋鎖。
它在(對稱多處理器)SMP環(huán)境里用于保護共享的數(shù)據(jù)結(jié)構(gòu):當一個CPU持有自旋鎖時,另一個CPU沒法訪問共享數(shù)據(jù)。
如果是單個CPU的環(huán)境,沒必要用自旋鎖,直接關(guān)閉中斷就行了。
單個CPU的情況下,關(guān)了中斷就可以阻止內(nèi)核的并發(fā),共享數(shù)據(jù)也就不會被踩踏了。
但多個CPU必須使用自旋鎖,因為關(guān)中斷只能關(guān)閉當前CPU的,沒法關(guān)其他CPU的:這時需要自旋鎖保護共享數(shù)據(jù)。
物理內(nèi)存的管理數(shù)組,是最重要的全局共享數(shù)據(jù)。
當需要給一個進程申請內(nèi)存的時候,哪個內(nèi)存頁是空閑的,哪個已經(jīng)被使用了,全靠查看這個數(shù)組。
加自旋鎖的時候一定要先關(guān)中斷,因為如果在加了鎖之后、關(guān)中斷之前、正好有個中斷來了,而在中斷處理函數(shù)里再次請求加同一個鎖,那就會遞歸死鎖了。
Linux內(nèi)核的關(guān)中斷加鎖的函數(shù)叫:spin_lock_irqsave().
Linux內(nèi)核的分配物理內(nèi)存頁的函數(shù)叫:get_free_pages(),它可以分配1頁或連續(xù)的多頁內(nèi)存。
如果分配多頁內(nèi)存的話,起始地址是要按頁數(shù)對齊的。
2.虛擬內(nèi)存管理
虛擬內(nèi)存都是通過進程的頁表管理的。
為了節(jié)省物理內(nèi)存,新創(chuàng)建的進程是與父進程共享同一套物理內(nèi)存頁的。
只有新進程要寫某個內(nèi)存頁時,才會給它復(fù)制一份新的物理內(nèi)存頁,然后取消該頁與父進程的共享,這就是寫時復(fù)制。
寫時復(fù)制的過程
寫時復(fù)制的過程:
1)申請一個新內(nèi)存頁,
2)把老內(nèi)存頁的內(nèi)容,復(fù)制到新內(nèi)存頁上,
3)把新內(nèi)存頁的地址填入子進程的頁表,
4)把老內(nèi)存頁的引用計數(shù)減1。
所以,新進程剛被創(chuàng)建出來時,它的用戶空間并沒有自己的物理內(nèi)存頁,只有當運行需要時才一點點地通過寫時復(fù)制添加,以讓物理內(nèi)存最大限度的空閑著。
另一個讓物理內(nèi)存最大限度空閑著的機制,就是需求加載:
1)當mmap一個文件時,操作系統(tǒng)并不會直接為這個文件分配內(nèi)存,并且把它的內(nèi)容加載到內(nèi)存里,
2)而是當進程真去讀這個文件的某一部分時,才給它申請物理內(nèi)存頁,并且把這一部分內(nèi)容從磁盤讀到內(nèi)存。
copy on write,load on read.
不到火燒眉毛的時候,Linux系統(tǒng)是不會把物理內(nèi)存給進程的?
3.用戶態(tài)的內(nèi)存函數(shù)
以上的這些機制都是OS內(nèi)核里的,應(yīng)用程序的代碼不需要管這些。
應(yīng)用程序分配內(nèi)存的最底層函數(shù),就是brk()系統(tǒng)調(diào)用。
brk()函數(shù)
brk()是一個系統(tǒng)調(diào)用,它的作用就是修改應(yīng)用程序的數(shù)據(jù)段的結(jié)尾,從而分配或回收應(yīng)用程序的堆空間。
brk系統(tǒng)調(diào)用的功能
C庫里的把它封裝成了sbrk()和brk()兩個函數(shù),讓它使用起來更符合人們的習(xí)慣:
sbrk()用于申請內(nèi)存:void* sbrk(int increment);
brk()用于回收內(nèi)存:int brk(void* addr);
實際上,Linux系統(tǒng)只有1個brk()系統(tǒng)調(diào)用,它既設(shè)置進程數(shù)據(jù)段的末尾,又會把這個值返回給應(yīng)用程序。
Linux內(nèi)核頭文件的sys_brk()函數(shù)
Linux內(nèi)核的頭文件里,brk()系統(tǒng)調(diào)用的處理函數(shù)sys_brk()是這么定義的,如上圖。
如果想直接使用系統(tǒng)調(diào)用,可以使用Linux的syscall()函數(shù),依次傳入調(diào)用號和參數(shù)列表,就可以看到哪些是真實的系統(tǒng)調(diào)用,哪些是C庫的封裝。
syscall()函數(shù)的聲明是:long syscall(long number, ...);
它的參數(shù)是可變的,系統(tǒng)調(diào)用的參數(shù)最多只有6個,因為寄存器的個數(shù)有限。
在sbrk() 和 brk()的基礎(chǔ)上再封裝,就是人們經(jīng)常使用的malloc() 和 free()了。
malloc() 申請的內(nèi)存是一塊塊的,可以不按次序釋放,而不影響使用。
brk() 和 sbrk() 申請的內(nèi)存必須按次序釋放,因為它會修改進程的數(shù)據(jù)段結(jié)尾:
數(shù)據(jù)段結(jié)尾(brk)之外的堆空間如果被使用,就屬于段錯誤。
所以,Linux man手冊里說明了,應(yīng)用程序不要用sbrk()和brk()申請和釋放內(nèi)存。
brk()的作用也只是通知Linux內(nèi)核哪個范圍的堆內(nèi)存是可用的,真正的物理內(nèi)存頁是在進程實際讀寫內(nèi)存的時候才會申請,而且是由內(nèi)核根據(jù)寫時復(fù)制/需求加載自動完成的,應(yīng)用程序感知不到這點。
Linux還會把不常用的物理內(nèi)存頁交換到磁盤上(即swap分區(qū)),以騰出更多的內(nèi)存。
所以,在內(nèi)存不足時,磁盤的讀寫頻次也會升高。