聊聊操作系統的內存管理
?內存管理,是操作系統的主要功能。
操作系統從啟動一直到創建0號進程(idle進程),運行的大部分代碼都跟內存有關。
操作系統的內存管理,大概分這么幾個層次:
1.物理內存管理
物理內存是電腦上的真實內存大小,這個數據可以通過BIOS獲取。
在分頁之后,物理內存的管理結構是個數組,每項表示1個物理內存頁,每頁4096字節。
如下圖:
物理內存的管理結構
在一個簡單的內核demo里,物理內存頁的管理結構可以只有一項:
即,物理內存頁的引用計數:計數為0表示空閑,> 0表示正在使用,具體數字表示共享這一頁的進程個數。
簡單的內核demo一般是不支持SMP架構的,所以自旋鎖(spinlock)也就省了。
在對稱多處理器(SMP)的CPU上,因為全局數據結構會被多個CPU并發訪問,所以要加自旋鎖。
那么,物理內存頁的管理結構至少有2項:
自旋鎖的作用,與應用程序里的鎖(mutex)差不多,只是它在獲取失敗之后會不斷地再次獲取,直到成功。
這就是給自旋鎖加鎖的函數,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.
不到火燒眉毛的時候,Linux系統是不會把物理內存給進程的?
3.用戶態的內存函數
以上的這些機制都是OS內核里的,應用程序的代碼不需要管這些。
應用程序分配內存的最底層函數,就是brk()系統調用。
brk()函數
brk()是一個系統調用,它的作用就是修改應用程序的數據段的結尾,從而分配或回收應用程序的堆空間。
brk系統調用的功能
C庫里的把它封裝成了sbrk()和brk()兩個函數,讓它使用起來更符合人們的習慣:
sbrk()用于申請內存:void* sbrk(int increment);
brk()用于回收內存:int brk(void* addr);
實際上,Linux系統只有1個brk()系統調用,它既設置進程數據段的末尾,又會把這個值返回給應用程序。
Linux內核頭文件的sys_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分區),以騰出更多的內存。
所以,在內存不足時,磁盤的讀寫頻次也會升高。