## 理解內存明天就要回家過年了,回家之前翻譯了一篇比較基礎的文章,英文太爛,各位看官見笑了。[原文地址](http://www.ualberta.ca/CNS/RESEARCH/linuxClusters/mem.html)---這篇文章討論基于的環境是AICT Linux集群,運行64位的GNU / Linux的AMD Opteron處理器。###內容* 簡介* 程序和進程* 存儲類別和作用域* 程序大小* 內存映射* 調用堆棧* 頁表* 庫* 內存限制* 內存分配* 實現細節* 參考文獻-----###簡介在Linux下,所有的程序都在虛擬內存環境中運行。如果有某個C程序員將指針的值打印出來(在實踐中從來沒有必要),其結果將是一個虛擬內存地址。在Fortran里,雖然指針不是標準的特性,但虛擬內存尋址隱含在每個變量和每次子程序調用中。當然,一個程序的代碼和數據實際是駐留在物理內存當中的。因此,每一個虛擬內存地址都會被操作系統通過一個叫做```頁表```(參看圖1)的數據結構映射成物理內存地址。因此,比如取回或更新一個變量的值時,程序必須首先通過查找頁表得到該變量的那個和虛擬內存地址關聯的實際內存地址。幸運的是,對于用戶來說,這個過程通過Linux的處理是透明的。高度優化的Linux內核和CPU中的專用電路使得這個過程合理而且高效。然而,你可能很像知道為什么這么做。![圖1 通過頁表將虛擬內存映射成物理內存][1]在一個通用的多用戶計算環境中,例如Linux,所有的程序必定是共享有限的可用物理內存的。在沒有虛擬內存的環境中,每個程序勢必要了解其他程序的活動狀況。例如,考慮在直接內存訪問的情況下,兩個相互獨立的程序同時申請同一塊可用的物理內存。為了避免沖突,兩個程序之間必須進行同步協作,這會導致非常復雜的編碼。相反,在虛擬內存的機制下,所有底層的內存管理都委托給了操作系統。從而,在Linux內核中**為每個程序維護了一個頁表**,給人一種計算機上每個程序都是獨立的假象。當兩個并發的程序引用相同的虛擬內存地址的時候,內核保證了它們實際上解析的是不同的物理內存地址,如圖2所示。![圖2 并發程序][2]這個圖也可以幫助我們理解虛擬內存作為一個抽象層,操作系統是如何用它來隔離作為硬件的存儲器。所有程序都是簡單的運行在這個抽象的頂層而無需知道底層內存管理實現的細節。我們將會在下面的章節漸漸熟悉虛擬內存相關的概念和術語。雖然我們關注的是AICT集群,但是所描述的原則在絕大多數其他的計算環境中都是適用的。###程序和進程一個程序的頁表是其執行上下文的一個組成部分。其他部分包括當前的工作目錄,打開的句柄列表,環境變量等等。所有部分在一起組成了我們所知道的```進程(PRocess)```或```任務(Task)```。通常“進程”和“程序”可以互換著使用。但是它們之間有很大的區別。“程序”這個術語通常結合著特定語言寫的源碼來用的,比如我們經常說的Fortran或C語言。也可以用來指存儲在磁盤上的編譯過后的源碼或可執行文件。一個進程,從操作系統的角度是特指正在運行的程序。內核給每個進程都賦予一個唯一的標識數字,```進程ID```,用來索引存儲了進程相關信息的數據結構。程序可以通過編程接口獲得它的進程ID以及其他相關的數據。這個接口在C語言里是標準,在Fortran是作為擴展來支持的。例如,```getuid()```會返回調用進程的用戶ID(uid)[3]。在shell命令行和程序進行交互是創建一個新進程的常用方法。新的進程是在字面上或通過forked(通過```fork()```這個系統調用)從shell進程催生的。通過這種方式,一個進程的繼承層次便出現了,shell進程作為父進程,新產生的作為子進程。自然的,子進程從父進程那里繼承了很多屬性,像當前的工作目錄,環境變量等。格外重要的是,內存資源上面的限制也會從父進程那里繼承過來,后面會詳細的說到這點。###存儲類別和作用域程序的組成包括可執行的語句和數據申明。每一個數據單元都有其```存儲類別(strage class)```這一屬性用來反映它在程序執行期間的生命周期。與之相關聯的另外一個屬性叫做```作用域(scope)```,用來表征數據的可見性程度。存儲類別和作用域是通過數據申明所在的位置來確定的,同時確定了它在虛擬內存中的位置。在C語言中,任何函數體外申明的變量的作用域都是全局的,擁有靜態(永久)的有效期。雖然初始化的時候可以賦值,但全局變量通常是未初始化的。同時,在方法體內包括```main()```,申明的變量擁有局部作用域和自動(臨時)的有效期。一個局部的變量可以通過static修飾符申明為永久的,因此它可以保存方法調用過程中的值。在Fortran中,所有的變量都只有局部作用域,除非申明在模塊或命名的公共塊中。###程序的大小編譯器將程序的可執行語句翻譯成CPU指令,將靜態的數據翻譯成特定機器的數據規格。為了產生一個可執行文件,系統鏈接器將指令和數據聚合在不同的段中。所有的CPU指令被放在一個叫做```text```段中,不幸的是,這個名字給人的感覺它包含的是程序的源代碼,但其實不是。同時,數據被分成了兩個段,一個叫做```data```,包括初始化的靜態變量和字面常量,另外一個叫做```bss```,包括未初始化的靜態變量。Bss曾經也用來表示“***block started from symbol***”,這是一種大型機匯編語言指令,這個術語在今天已經沒有任何意義??紤]下面這段C語言程序,與之相當的是用Fortran90/95版本寫的,它主要的組成部分是一個200W的靜態數組。``` c/** * simple.c */#include #include #define NSIZE 200000000char x[NSIZE];intmain (void){ for (int i=0; i<NSIZE; i++) x[i] = 'x'; printf ("done/n"); exit (EXIT_SUCCESS);}``````shell$ pgcc -c9x -o simple simple.c$ size simple text data bss dec hex filename 1226 560 200000032 200001818 bebc91a simple $ ls -l simple-rwxr-xr-x 1 esumbar uofa 7114 Nov 15 14:12 simple``````Fortran!! simple.f90!module globals implicit none integer, parameter :: NMAX = 200000000 character(1), save :: x(NMAX)end module globalsprogram simple use globals implicit none integer :: i do i = 1, NMAX x(i) = 'x' end do print*, "done" stopend program simple``````shell$ pgf95 -o simple simple.f90$ size simple text data bss dec hex filename 77727 1088772 200003752 201170251 bfd9d4b simple $ ls -l simple-rwxr-xr-x 1 esumbar uofa 1201694 Nov 15 14:12 simple$ file simplesimple: ELF 64-bit LSB executable, AMD x86-64, ...```編譯(隱含鏈接的過程)產生如上圖所示的```ELF```(Executable and Linking Format)可執行程序文件。運行```size```命令來查看ELF文件中```text```,```data```,```bss```段的大小。在兩種情況下,```bss```段的大小都的確有200萬字節(加上一些額外的管理開銷)。從源程序中看實際貢獻給```data```段的只有兩個字符字面常量和一個數值型常量,這跟上面打印出的結果都相去甚遠,顯然,這是編譯器做的手腳。此外,因為ELF包含了所有程序的指令和所有初始化的數據,所以```text```段和```data```段大小總和會很接近但絕不會超過它在磁盤上的文件的大小。而為```bss```段預留的空間在它存儲實際數據之前是沒有必要浪費磁盤空間的,這是可以通過實例來證實的。>>>請注意,```data```和```bss```段經常統稱為```data```,偶爾會導致混淆。###內存映射當ELF文件被執行后,```text```段和兩個```data```段會被加載到虛擬內存中一片獨立的區域內。按照慣例,```text```段占據著最低地址空間,```data```段在它上面。每個部分被分配了適當的權限。通常,```text```段是只讀的,```data```段是可讀寫的。一個典型的進程```內存映射(memory map)```如圖3所示。![圖3 進程中text,data,bsss段的內存映射情況][3]如圖虛擬內存的地址空間是從底部的0到頂部的512GB。512GB以上的地址空間是Linux內核保留的。這是特定于AMD64硬件的,其他的體系架構可能有不同的限制。雖然一個進程的大?。╰ext+data+bsss)在編譯時候就已經確定了并且在執行期間可以保持不變。但是程序仍然可以在運行時從虛擬內存中未被占用的地方拓展自己的空間,在C中用```malloc()```函數,在Fortran 90/95中用```ALLOCATABLE arrays```。在Fortran 77可以通過擴展獲得相似的特性。這些動態分配的內存位于```data```段之上的```heap```段。如圖4所示。![圖4 包括heap段在內的內存映射,data和bss統稱為data][4]所有的三個段,```text```,```data```(data+bss),以及```heap```,都通過頁表映射到物理內存。由圖可知,隨著內存的分配和釋放,```heap```段會不斷膨脹和收縮,所以意味著,頁表項必須不斷的新增和刪除。###調用堆棧面向過程(相對面向對象來說)式的程序會被組織成子程序調用的邏輯層次結構。一般情況下,每次調用子程序涉及從調用者傳遞參數給被調用者。此外,被調用者可以聲明臨時的局部變量。子程序參數和局部變量(```automatic ```)位于從虛擬內存頂部開始向下生長的```stack```段或簡稱```stack```。如圖5所示。![圖5 顯示棧段的內存映射][5]子程序的調用層次是開始于操作系統調用程序的主函數,在C中是```main()```,在Fortran中是```MAIN```,在正常的情況下,結束于主函數返回給操作系統。整個序列可以表示為如圖6所示的調用圖。![圖6 典型的子程序調用圖][6] >1. 操作系統調用主函數main >2. main調用func1 >3. func1調用func2 >4. func2返回給func1 >5. func1調用func3 >6. func3返回給func1 >7. func1返回給main >8. main調用func4 >9. func4返回給main> 10. main返回給操作系統在調用主函數之前,操作系統會將用于調用程序的命令行參數推到初始空棧的"頂部"。在C中```main()```函數是通過```argc```和```argv```來訪問這些參數的。隨著程序的執行,主函數將自己的局部變量推到棧的頂部。這會導致棧朝著低地址空間生長。然后,先順序的執行函數func1,主函數將參數傳遞給func1。作為整體,主函數的局部變量和傳遞給函數func1的參數組成了一個```棧幀(statck frame)```。棧幀隨著調用圖不斷往下而積累,往上而拆除。該過程如圖7所示。![圖7 圖6所示的調用層次中棧的不斷演變的情形][7]通常,當前活動的子程序只能引用到傳遞給自己的參數,自己的局部變量以及靜態變量(包括任何全局可訪問的數據)。舉例來說,當函數func2執行的時候,它不能訪問函數func1的局部變量,除非函數func1將自己變量的引用通過傳參傳給func2。###頁表如圖8所示,展示了頁表,內存映射,物理內存之間的關系:頁表的膨脹或收縮,映射或多或少的物理內存,堆棧和堆兩個部分大小的改變。![圖8 內存映射、頁表和物理內存][8]假設每個頁表項都用一個64位的數字表示一個虛擬內存地址,用另外一個64位數字表示物理地址,那么每個頁表項的大小為16字節。為了映射一個200MB的進程空間,將需要3200MB的頁表。顯然這是不切實際的。為了取代映射單個字節,頁表使用了一個叫做```頁(page)```(頁表因此而得名)的更大的虛擬內存塊。實際內存中相應的一個增量空間稱為```頁幀(page frame)```。頁的大小是特定于具體的體系架構的,通常也是可配置的。這兩方面都會有很多作用。 AICT Linux集群中的AMD處理器使用的4KB。從而,現在映射一個200MB的空間只需要800KB的頁表。超過1.28億的頁面涵蓋了512GB的虛擬內存地址空間。它們以一個```虛擬頁號(VPN)```進行順序編號。每個頁表項將進程的虛擬頁映射到物理內存的頁幀,即從一個```VPN```映射到一個```頁幀號(PFN)```。```VPN```是通過將虛擬內存的地址除以頁的大小得到的(將地址右移)。一個具體的字節是通過它在頁中偏移量來定位的。###庫鏈接一個靜態庫到程序中,將其```text```和```data```段整合到ELF文件中。結果,兩個鏈接同一個靜態庫的程序都會將庫數據映射到物理內存中。如圖9所示。![圖9 兩個程序鏈接相同的靜態庫][9]靜態庫的ELF格式的文件以```.a(archive)```作為擴展名。ELF格式還支持動態共享庫,以```.so(shared object)```作為擴展名。從兩個方面來看動態共享庫和靜態庫的區別。第一,只有庫的名稱會被記錄在ELF文件中,沒有```text```和```data```,結果是更小的可執行文件。第二,始終只會有一份拷貝存在于物理內存中,這樣更節省內存而且加速了其他需要鏈接相同動態庫的程序的加載過程。這樣的情形,如圖10所示。![圖10 兩個程序鏈接相同的動態共享庫][10]當第一個程序被執行,系統會找到庫并更新進程的頁表將庫的```text```和```data```段映射到物理內存中。然后當第二個程序執行時,其頁表項中對于庫的```text```段的映射會被指向已經存在的物理內存。庫的```text```段之所以可以被這樣共享,是因為它有這樣的權限,像所有其他程序的```text```段一樣,它是可讀的。初始時,庫的```data```段只有讀的時候也是被共享的,可是,當另一個程序視圖去更新這個部分的時候,一個私有備份操作將會發生,這個程序的頁表將會映射這份副本。使用了一個廣為認知的策略```COW(copy on write)```。上圖展示的是每個程序擁有自己的庫的```data```段副本。###內存限制在**程序和進程**那一節,我們提到一個進程會從它的父進程那里繼承一些內存上的限制,父進程通常是shell進程。在AICT中這種情形,```bash```和```tcsh```會有一個限制棧段的大小不能超過10MB的```軟限制(soft limit)```。其他一些內存參數,```data```段,總的虛擬內存,總的物理內存則沒有限制。雖然```text```,```data```,```heap```以及```stack```段已經被描述成有一定的大小。但是在一些典型的高性能應用場景中無論是```data```段還是```bss```或```heap```段都有可能有一個或多個的大數組。如果一個大數組是基于棧的,則很有可能會超過限制的大小。這種情況,進程會發生```"段違規(Segmentation violation)"```而終止。為了避免這種結果。軟限制可以使用內建的shell命令增加。例如,在```bash```下用``` ulimit -s 20480```,在```tcsh ```下```limit stacksize 20480```將棧的大小調整為20MB。軟限制可以被提升到相應配置的```硬限制(hard limit)```。在AICT 集群中,棧段大小的硬限制是"```unlimited```",意思是沒有限制。值得注意的是,新的限制只對從調整限制的```shell```衍生的進程有效。>要知道,對```shell```資源的軟限制和硬限制在不同的系統中差別很大。很多的AICT集群安裝有10GB的內存。其中有接近1000MB是保留給操作系統用來維護進程信息的,包括頁表在內。另外在較為緩慢的作為二級存儲的磁盤上有一個1GB的空間(可配置)作為```交換空間(swap space)```,或者,更為準確的說叫做分頁空間(```paging space```)。所有的可用的內存加上交換空間大約有9800MB(經驗值),這個值代表總的虛擬的或實際的內存,一個節點上所有可以用來作為進程映射的空間,這個數量有一個術語叫做**```SWAP```**(也可以被稱作為邏輯交換空間```logical swap space```)。為了說明以交互方式運行的進程的棧空間軟限制是如何運作的(AICT集群的頭結點),參看圖11。![圖11 一個交互進程上??臻g的軟限制][11]如圖所示,軟限制就像一個圍在頁表中棧段映射那部分周圍一個邊框。當軟限制為"unlimited"的時候就好比移除這個邊框。同時,堆段的空間大小只限制于可用的```SWAP```。由于批處理是一個shell腳本,它可能包括一些適當的限制命令,在調用一些計算程序之前增加棧的大小限制。而且,批處理作業被分配有足夠的```SWAP```用來滿足請求進程的虛擬內存(```pvmem```)。為了保證作業不會超過它的```pvmem```規范,批處理系統會減少它關聯的```shell```進程的虛擬內存的軟限制以匹配```pvmem```的值,通常初始值為"unlimited"。每當一個軟限制被修改,相應的硬限制也會被重新設置為相同的值。此外,一個作業的大?。╰ext+data+bsss)超過虛擬內存的限制是不允許被啟動的。當一個作業試圖在運行時將其虛擬內存擴展到限制的大小以外,它會因為"```Segmentation violation```信號異常中斷"。要正常退出,在C語言中可以檢查```malloc()```函數的返回值是否為空指針。在AICT集群上,一個進程以批處理方式運行,它的內存限制如圖12所示。![圖12 批處理進程上的??臻g和虛擬內存限制][12]因為頁表代表了一個進程虛擬內存的占用情況,所以虛擬內存的軟限制被描述成頁表周圍的邊框。注意在某些情況下,虛擬內存的限制可能會被基于棧的數據所侵犯,即使棧的大小并不會超過它的限制。###內存分配到目前為止,我們圖形化的頁表展示似乎暗示我們虛擬內存的頁總是能夠映射到實際的物理內存頁幀。但是,實際上,因為有些頁從來都沒被用到,對于操作系統來說更有效的策略是推遲映射到確實需要的時候,這項技術被叫做```按需分頁(demand paging)```。例如,在Fortran 77中一個普遍做法是,用一個預估的最大的靜態數組去編譯一個程序,并使用相同的可執行文件配合各種數組的擴展來進行演算。在這種情況下,頁表中會保留足夠多的虛擬頁來容納那些大數組。但是,在實際內存中,只包含由程序實際引用數組元素的頁被分配的頁幀。那些從來沒被引用的頁不會分配頁幀。所有被分配頁幀的總和叫做```駐留集大?。╮esident set size,rss)```。如圖13所示,RSS小于或等于進程占用的虛擬內存。![圖13 對圖12更為真實的一個描述,包括了駐留集大小][13]當進程第一次引用一個虛擬頁的時候,會發生一個```頁錯誤(page fault)```。Linux負責從一個剩余頁池中獲取一個頁幀,并將其分配給進程頁表中對應的虛擬頁。另外,頁表中最近的512個頁表項會被緩存在CPU的一個叫做"```translation lookaside buffer,TLB```"的緩存中。引用``TLB```中的頁表項要比訪問位于內核內存中的頁表快。對于高性能的應用,理想的情況是每個進程都有充足的可用內存(```RAM```)。如果非常多的內存被使用導致內存耗盡,這時一個頁幀可以被另外一個進程“偷走”。偷走的頁幀上的內容會被轉移到磁盤上的交換空間中,同時被偷進程的頁表會被更新。換句話說,頁被換出,或者更準確,術語叫做```paged out```。位于交換空間的頁幀是不能直接使用的。因此,如果一個進程引用了該頁,被稱作```主頁錯誤(major page fault)```,它首先必須被換進,造成一個頁從其它進程被換出,導致結果是需要更多的內存,更多交換空間的使用,從而又導致更多的```swapping```。因為磁盤的訪問是相對緩慢的,結果是所有進程的性能受到嚴重的影響。最終,當交換空間都承受不住的時候,進程會被殺掉以回收內存。在這種情況,節點會快速的變成不可用。為了防止上面這種情況,批處理系統必須遵循```pvmem```規范以保證節點上總的SWAP不會被耗盡。###實現細節略。###參考文獻 1. Understanding the Linux Kernel, by Daniel P. Bovet and Marco Cesati, ©2001 O'Reilly 2. Linkers and Loaders, by John R. Levine, ©2000 Morgan Kaufmann 3. System V application Binary Interface: AMD64 Architecture Processor Supplement, Draft Version 0.96, by Michael Matz, Jan Hubi?ka, Andreas Jaeger, and Mark Mitchell [1]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure1.png [2]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure2.png [3]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure3.png [4]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure4.png [5]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure5.png [6]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure6.png [7]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure7.png [8]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure8.png [9]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure9.png [10]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure10.png [11]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure11.png [12]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure12.png [13]: http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/images/mem/figure13.png
新聞熱點
疑難解答