聊一聊Linux內存管理
本章首先以應用程序開發者的角度審視Linux的進程內存管理,在此基礎上逐步深入到內核中討論系統物理內存管理和內核內存的使用方法。力求從外到內、水到渠成地引導網友分析Linux的內存管理與使用。在本章最后,我們給出一個內存映射的實例,幫助網友們理解內核內存管理與用戶內存管理之間的關系,希望大家最終能駕馭Linux內存管理。
前言
內存管理一向是所有操作系統書籍不惜筆墨重點討論的內容,無論市面上或是網上都充斥著大量涉及內存管理的教材和資料。因此,我們這里所要寫的Linux內存管理采取避重就輕的策略,從理論層面就不去班門弄斧,貽笑大方了。我們最想做的和可能做到的是從開發者的角度談談對內存管理的理解,最終目的是把我們在內核開發中使用內存的經驗和對Linux內存管理的認識與大家共享。
當然,這其中我們也會涉及到一些諸如段頁等內存管理的基本理論,但我們的目的不是為了強調理論,而是為了指導理解開發中的實踐,所以僅僅點到為止,不做深究。
遵循“理論來源于實踐”的“教條”,我們先不必一下子就鉆入內核里去看系統內存到底是如何管理,那樣往往會讓你陷入似懂非懂的窘境(我當年就犯了這個錯誤!)。所以最好的方式是先從外部(用戶編程范疇)來觀察進程如何使用內存,等到大家對內存的使用有了較直觀的認識后,再深入到內核中去學習內存如何被管理等理論知識。最后再通過一個實例編程將所講內容融會貫通。
進程與內存
進程如何使用內存?
毫無疑問,所有進程(執行的程序)都必須占用一定數量的內存,它或是用來存放從磁盤載入的程序代碼,或是存放取自用戶輸入的數據等等。不過進程對這些內存的管理方式因內存用途不一而不盡相同,有些內存是事先靜態分配和統一回收的,而有些卻是按需要動態分配和回收的。
對任何一個普通進程來講,它都會涉及到5種不同的數據段。稍有編程知識的朋友都能想到這幾個數據段中包含有“程序代碼段”、“程序數據段”、“程序堆棧段”等。不錯,這幾種數據段都在其中,但除了以上幾種數據段之外,進程還另外包含兩種數據段。下面我們來簡單歸納一下進程對應的內存空間中所包含的5種不同的數據區。
代碼段:代碼段是用來存放可執行文件的操作指令,也就是說是它是可執行程序在內存中的鏡像。代碼段需要防止在運行時被非法修改,所以只準許讀取操作,而不允許寫入(修改)操作——它是不可寫的。
數據段:數據段用來存放可執行文件中已初始化全局變量,換句話說就是存放程序靜態分配[1]的變量和全局變量。
BSS段[2]:BSS段包含了程序中未初始化的全局變量,在內存中 bss段全部置零。
堆(heap):堆是用于存放進程運行中被動態分配的內存段,它的大小并不固定,可動態擴張或縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)
棧:棧是用戶存放程序臨時創建的局部變量,也就是說我們函數括弧“{}”中定義的變量(但不包括static聲明的變量,static意味著在數據段中存放變量)。除此以外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,并且待到調用結束后,函數的返回值也會被存放回棧中。由于棧的先進先出特點,所以棧特別方便用來保存/恢復調用現場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時數據的內存區。
進程如何組織這些區域?
上述幾種內存區域中數據段、BSS和堆通常是被連續存儲的——內存位置上是連續的,而代碼段和棧往往會被獨立存放。有趣的是,堆和棧兩個區域關系很“曖昧”,他們一個向下“長”(i386體系結構中棧向下、堆向上),一個向上“長”,相對而生。但你不必擔心他們會碰頭,因為他們之間間隔很大(到底大到多少,你可以從下面的例子程序計算一下),絕少有機會能碰到一起。
實存、虛存
實存:進程分配的、加載到主存中的內存。包含來自共享庫的內存,只要這些庫占用的頁框還在主存中,也包含所有正在使用的堆棧和堆內存。可以通過 ps -o rss 查看進程的實存大小。
虛存:包含進程可以訪問的所有內存,包含被換出、已經分配但還未使用的內存,以及來自共享庫的內存。可以通過 ps -o vsz 查看進程的虛存大小。
舉個例子,如果進程A具有500K二進制文件并且鏈接到2500K共享庫,則具有200K的堆棧/堆分配,其中100K實際上在內存中(其余是交換或未使用),并且它實際上只加載了1000K的共享庫然后是400K自己的二進制文件:
- RSS: 400K + 1000K + 100K = 1500K
- VSZ: 500K + 2500K + 200K = 3200K
實存和虛存是怎么轉換的呢?當程序嘗試訪問的地址未處于實存中時,就發生頁面錯誤,操作系統必須以某種方式處理這種錯誤,從而使應用程序正常運行。這些操作可以是:
- 找到頁面駐留在磁盤上的位置,并加載到主存中。
- 重新配置MMU,更新線性地址和物理地址的映射關系。
- 等。
隨著進程頁面錯誤的增長,主存中可用頁面越來越少,為了防止內存完全耗盡,操作系統必須盡快釋放主存中暫時不用的頁面,以釋放空間供以后使用,方式如下:
- 將修改后的頁面寫入到磁盤的專用區域上(調頁空間或者交換區)。
- 將未修改的頁面標記為空閑(沒必要寫入磁盤,因為沒有被修改)。
調頁或者交換是操作系統的正常部分,需要注意的是過度交換,這表示當前主存空間不足,頁面換出抖動對系統極為不利,會導致CPU和I/O負載升高,極端情況下,會造成操作系統所有的資源花費在調頁層面。