1.Linux 進程在內(nèi)存數(shù)據(jù)結(jié)構
可以看到一個可執(zhí)行程序在存儲(沒有調(diào)入內(nèi)存)時分為代碼段,數(shù)據(jù)段,未初始化數(shù)據(jù)段三部分:
1) 代碼段:存放CPU執(zhí)行的機器指令。通常代碼區(qū)是共享的,即其它執(zhí)行程序可調(diào)用它。假如機器中有數(shù)個進程運行相同的一個程序,那么它們就可以使用同一個代碼段。 2) 數(shù)據(jù)段:存放已初始化的全局變量,靜態(tài)變量(包括全局和局部的),常量。static全局變量和static函數(shù)只能在當前文件中被調(diào)用。 3) 未初始化數(shù)據(jù)區(qū)(uninitializeddata segment,BSS):存放全局未初始化的變量。BSS的數(shù)據(jù)在程序開始執(zhí)行之前被初始化為0或NULL。
代碼區(qū)所在的地址空間最低,往上依次是數(shù)據(jù)區(qū)和BSS區(qū),并且數(shù)據(jù)區(qū)和BSS區(qū)在內(nèi)存中是緊挨著的。。
可執(zhí)行程序在運行時又多出了兩個區(qū)域:棧段(Stack)和堆段(Heap)。 4) 棧區(qū):由編譯器自動釋放,存放函數(shù)的參數(shù)值,局部變量等。每當一個函數(shù)被調(diào)用時,該函數(shù)的返回類型和一些調(diào)用的信息被存儲到棧中。然后這個被調(diào)用的函數(shù)再為它的自動變量和臨時變量在棧上分配空間。每調(diào)用一個函數(shù)一個新的棧就會被使用。棧區(qū)是從高地址位向低地址位增長的,是一塊連續(xù)的內(nèi)在區(qū)域,最大容量是由系統(tǒng)預先定義好的,申請的??臻g超過這個界限時會提示溢出,用戶能從棧中獲取的空間較小。 5) 堆段:用于存放進程運行中被動態(tài)分配的內(nèi)存段,位于BSS和棧中間的地址位。由程序員申請分配(malloc)和釋放(free)。堆是從低地址位向高地址位增長,采用鏈式存儲結(jié)構。頻繁地malloc/free造成內(nèi)存空間的不連續(xù),產(chǎn)生碎片。當申請堆空間時庫函數(shù)按照一定的算法搜索可用的足夠大的空間。因此堆的效率比棧要低的多。
這個5中內(nèi)存區(qū)域中數(shù)據(jù)段、BSS和堆通常是被連續(xù)存儲的——內(nèi)存位置上是連續(xù)的,而代碼段和棧往往會被獨立存放。有趣的是堆和棧兩個區(qū)域關系很“曖昧”,他們一個向下“長”(i386體系結(jié)構中棧向下、堆向上),一個向上“長”,相對而生。但你不必擔心他們會碰頭,因為他們之間間隔很大(到底大到多少,你可以從下面的例子程序計算一下),絕少有機會能碰到一起。
下圖簡要描述了進程內(nèi)存區(qū)域的分布:

1. 物理地址(physical address)
物理內(nèi)存,真實存在的插在主板內(nèi)存槽上的內(nèi)存條的容量的大小.
內(nèi)存是由若干個存儲單元組成的,每個存儲單元有一個編號,這種編號可唯一標識一個存儲單元,稱為內(nèi)存地址(或物理地址)。我們可以把內(nèi)存看成一個從0字節(jié)一直到內(nèi)存最大容量逐字節(jié)編號的存儲單元數(shù)組,即每個存儲單元與內(nèi)存地址的編號相對應。
2. 虛擬內(nèi)存(Virtual memory)(也叫虛擬存儲器)
虛擬內(nèi)存地址就是每個進程可以直接尋址的地址空間,不受其他進程干擾。每個指令或數(shù)據(jù)單元都在這個虛擬空間中擁有確定的地址。
虛擬內(nèi)存就是進程中的目標代碼,數(shù)據(jù)等虛擬地址組成的虛擬空間
虛擬內(nèi)存不考慮物理內(nèi)存的大小和信息存放的實際位置,只規(guī)定進程中相互關聯(lián)信息的相對位置。每個進程都擁有自己的虛擬內(nèi)存,且虛擬內(nèi)存的大小由處理機的地址結(jié)構和尋址方式?jīng)Q定。
如直接尋址,如果cpu的有效地址長度為16位,則其尋址范圍0 -64k。
再比如32位機器可以直接尋址4G空間,意思是每個應用程序都有4G內(nèi)存空間可用。但是顯然機器內(nèi)存罕有如此之大,可以支持每個程序使用4G內(nèi)存的。 虛擬內(nèi)存與物理內(nèi)存的區(qū)別:虛擬內(nèi)存就與物理內(nèi)存相反,是指根據(jù)系統(tǒng)需要從硬盤虛擬地勻出來的內(nèi)存空間,是一種計算機系統(tǒng)內(nèi)存管理技術,屬于計算機程序,而物理內(nèi)存為硬件。因為有時候當你處理大的程序時候系統(tǒng)內(nèi)存不夠用,此時就會把硬盤當內(nèi)存來使用,來交換數(shù)據(jù)做緩存區(qū),不過物理內(nèi)存的處理速度是虛擬內(nèi)存的30倍以上。
3. 邏輯地址(logical address)
源程序經(jīng)過匯編或編譯后,形成目標代碼,每個目標代碼都是以0為基址順序進行編址的,原來用符號名訪問的單元用具體的數(shù)據(jù)——單元號取代。這樣生成的目標程序占據(jù)一定的地址空間,稱為作業(yè)的邏輯地址空間,簡稱邏輯空間。
在邏輯空間中每條指令的地址和指令中要訪問的操作數(shù)地址統(tǒng)稱為邏輯地址。即應用程序中使用的地址。要經(jīng)過尋址方式的計算或變換才得到內(nèi)存中的物理地址。
很簡單,邏輯地址就是你源程序里使用的地址,或者源代碼經(jīng)過編譯以后編譯器將一些標號,變量轉(zhuǎn)換成的地址,或者相對于當前段的偏移地址。
邏輯地址是指由程序產(chǎn)生的與段相關的偏移地址部分。例如,你在進行C語言指針編程中,可以讀取指針變量本身值(&操作),實際上這個值就是邏輯地址,它是相對于你當前進程數(shù)據(jù)段的地址,不和絕對物理地址相干。只有在Intel實模式下,邏輯地址才和物理地址相等(因為實模式?jīng)]有分段或分頁機制,Cpu不進行自動地址轉(zhuǎn)換);邏輯也就是在Intel保護模式下程序執(zhí)行代碼段限長內(nèi)的偏移地址(假定代碼段、數(shù)據(jù)段如果完全一樣)。應用程序員僅需與邏輯地址打交道,而分段和分頁機制對您來說是完全透明的,僅由系統(tǒng)編程人員涉及。應用程序員雖然自己可以直接操作內(nèi)存,那也只能在操作系統(tǒng)給你分配的內(nèi)存段操作。
不過有些資料是直接把邏輯地址當成虛擬地址,兩者并沒有明確的界限。
在linux內(nèi)核,虛擬地址是3G-4G這段地址,它與物理地址通過頁表來映射,邏輯地址是指3G-3G+main_memory_size這段虛擬地址,它與物理地址的映射是線性的,當然也可以通過頁表映射。所以邏輯地址是虛擬地址的一部分。
邏輯地址的組成:是由一個段標識符加上一個指定段內(nèi)相對地址的偏移量,表示為 [段標識符:段內(nèi)偏移量]

圖4.1 作業(yè)的名空間、邏輯地址空間和裝入后的物理空間
4. 線性地址或Linux下也叫虛擬地址(virtual address)
這個地址很重要,也很不容易理解。分段機制下CPU尋址是二維的地址即,段地址:偏移地址,CPU不可能認識二維地址,因此需要轉(zhuǎn)化成一維地址即,段地址*16+偏移地址,這樣得到的地址便是線性地址(在未開啟分頁機制的情況下也是物理地址)。這樣有什么意義呢?或者說這個一維地址的計算方法隨便一個學計算機的人都知道,但是你真的理解它的意思嗎?要想理解它的意思,必須要知道什么是地址空間,下文詳述。
線性地址是邏輯地址到物理地址變換之間的中間層。程序代碼會產(chǎn)生邏輯地址,或者說是段中的偏移地址,加上相應段的基地址就生成了一個線性地址。如果啟用了分頁機制,那么線性地址可以再經(jīng)變換以產(chǎn)生一個物理地址。若沒有啟用分頁機制,那么線性地址直接就是物理地址。Intel 80386的線性地址空間容量為4G(2的32次方即32根地址總線尋址)。
跟邏輯地址類似,它也是一個不真實的地址,如果邏輯地址是對應的硬件平臺段式管理轉(zhuǎn)換前地址的話,那么線性地址則對應了硬件頁式內(nèi)存的轉(zhuǎn)換前地址。
CPU將一個虛擬內(nèi)存空間中的地址轉(zhuǎn)換為物理地址,需要進行兩步:首先將給定一個邏輯地址(其實是段內(nèi)偏移量=),CPU要利用其段式內(nèi)存管理單元,先將為個邏輯地址轉(zhuǎn)換成一個線程地址,再利用其頁式內(nèi)存管理單元,轉(zhuǎn)換為最終物理地址。
由 I NTEL公 司 推 出 的 32 位 80386 芯 片 的 工 作 模 式 包 括 實地址模式和虛地址模式 。 實地址模式與8086 完全兼容 . 它的 尋址范圍是1 MB的地址空間. 分段功能受到限制 .不能區(qū)分特權級 . 當然分頁機制也不能啟用 。 在虛地址模式下. 分段機 制得到加強 . 段最大可達4GB. 并且提供段 內(nèi)分頁管理機制 . 為 Linux虛擬內(nèi)存管理機制提供了支持 。
80386 的虛擬地址模式使用了如下分段和分頁兩級地址 轉(zhuǎn)換機制來實現(xiàn)虛擬地址向物理地址的轉(zhuǎn)換 。
2 .1 虛擬地址向線性地址的轉(zhuǎn)換
用戶進程要訪問的虛擬地址包括一個1 6 位的段選擇器和 一個32 位的段 內(nèi)偏移 . 80386 的分段機制將段寄存器中所裝的 段選擇器和32 位段 內(nèi)偏移量相加. 得到32位的線性地址 , 如圖1所示 , 16 位的段選擇器最低兩位表示請求者的特權級 , 那么 最多可以有16k個段 , 每段的最大尺寸為4GB。 但是它們都必 須被映射到4GB的線性地址空間。

圖1
2 .2 線性地址向物理地址的轉(zhuǎn)換
Linux的每個用 戶進 程都可 以訪 問4 GB的線 性地址空間, 而實際的物理 內(nèi)存可能遠 遠少于4GB. 采用分頁機制 。 Linux僅把可執(zhí)行映像的一小部分 裝入物理 內(nèi)存. 當需要訪問未裝入的頁面時 . 系統(tǒng)產(chǎn)生一個缺頁中斷 , 把需要的頁讀入 物理內(nèi)存。

圖2
Linux采用兩級頁表結(jié)構—— 頁目錄表和頁表實現(xiàn)地址 映射. 當前正在運行進程的頁 目錄表的地址被保存在控制寄 存器 CR3 中。 由上面轉(zhuǎn)換機制所得到的線性地址可以分為3 部分 , 高 10位是 DI R域—— 頁 目錄表的索引值 . 它與 CR3 中的地址一起 計算得到頁表的物理地址 . 中間1O位保 存相對于頁表的索引 值 . 通過它得到所需的物理頁號。 物理頁號與低1 2 位頁內(nèi)偏移 組合得到物理地址 。 其結(jié)構如圖2 所示 。
每個用戶進程都可以有4 GB的虛存空 間. 為了更好地管 理這部分虛存空間.Linux主要定義了如下三個數(shù)據(jù)結(jié)構 : struct vm_area_struct ,
struct vm_operations_struct struct vmm_struct 虛存段( vm_area_struct ) . 簡稱 vma是某個進程的一段 連續(xù)的虛存空間. 一個進程通常占用幾個vma段 . 例如代碼段 、 數(shù)據(jù)段、 堆棧段等 。 vma不僅可以代表一段內(nèi)存區(qū)間, 也可 以對應于一個文件、 共享內(nèi)存或者對換設備。
每一個進程的所有vma由一個雙向鏈表管理。 為了提高對vma的查詢、 插入、 刪除等操作的效率 .Linux把系統(tǒng)中所 有進程的 vma組成了一棵 AVL樹。 這是一棵平衡二叉樹 . 當 vma數(shù)量特別 大時。 利用這棵 AVL樹查找 v ma的效率得到 明顯提 高 。
不同的 vma可能需要不同的操作處理方式 . 但同時考慮到統(tǒng)接口的統(tǒng) 一 性 . Linux采 用vm_operations_struct結(jié) 構和面向?qū)ο蟮乃枷雭矶x操作方式 . 一個vm_operations_struct結(jié)構體是一組 函數(shù)指針 , 對于不同的 vma . 它可能指向 不同的處理 函數(shù).例如當發(fā)生缺頁錯誤時 . 共 享內(nèi)存和代碼 段 的 readpage所 指 向的頁面讀入函數(shù)可能就不同 。
內(nèi)存管理中另外 一個 非 常重要的數(shù) 據(jù) 結(jié) 構是vmm_struct 結(jié)構體 .進程 的 task_struct中的mm成員指向 它. 當前運行進程的整個虛擬空間都 由它來管理和描述 . 它不僅包含該進程的映像信 息. 而且它的 mma p成員項指向該進 程所有vma組成的鏈 表 。 它的 mmap_avl 成 員 項 指 向整個系統(tǒng) 的 AVL樹 。
這三個數(shù)據(jù)結(jié)構之間相互關聯(lián). 共同管理虛擬內(nèi)存 . 它們之 間的 關 系如圖 3所 示 。

圖 3
這部分相關的系統(tǒng)調(diào)用主要有如下兩個 :
- do_mmap(
- struct file *file,
- unsigned long addr
- unsigned long len ,
- unsigned long prot ,
- unsigned long flags ,
- unsigned long off
- );
- find_vma (
- struct mm_struct mm ,
- unsigned long addr
- );
do _mmap函數(shù)實現(xiàn)了 內(nèi)存映射 。 find_vma函數(shù)的功能 是找到包含參數(shù) addr指定的虛擬地址所屬的 vma 。 當要運行一個可執(zhí)行映像時 . 調(diào)用 do _mmap將其裝入 到該進程 的虛擬地址空間 . 并且產(chǎn)生一組 vma結(jié)構 . 如前所 述該進程的整個虛擬空間由 vmm_struct 結(jié)構管理 . 但是此時 可執(zhí)行文件僅僅被連接到進程的虛擬空間中. 只有一小部分 頁面被裝入到物理 內(nèi)存 . 其余大部分并沒有被真正裝入到物 理內(nèi)存 . 在進程的運行過程 中. 產(chǎn)生缺頁錯誤 . 操作 系統(tǒng)首先調(diào)用 find_vma. 找到該虛擬地 址所在的 vma . 然后根據(jù)該 vma的成員變量 vm_ops指向的 vm_operations_struct結(jié)構 中的缺頁操作 函數(shù)。 把頁裝入物理內(nèi)存。 ·
32位Linux系統(tǒng)的每個進程可以有4 GB的虛擬 內(nèi)存空間 . 而且系統(tǒng)中還要同時存在多個進程 ,但是 ,事實上大多數(shù)計算機都沒有這么多物理內(nèi)存空間 , 當系統(tǒng)中的物理內(nèi)存緊缺時 . 就需要利用對換空間把一部分未來可能不用的頁面從物理內(nèi)存中移 到對換設備或?qū)Q文件中。 Linux采用兩種方式保存換出的頁面 :
一種是利用整個塊設備 , 如硬盤的一個分區(qū) . 即對換設備,
另一種是利用文件系統(tǒng)中固定長度的文件 . 即對換文件。 它們統(tǒng)稱為對換空間。
這兩種方式的相同之處是它們的內(nèi)部格式一致. 但是在執(zhí)行效率方面 . 對換設備要好一些. 這是因為對換設備上同一頁面 的數(shù)據(jù)塊是連續(xù)存放的 . 故而可以順序存取 , 而在對換文件中 。 同一頁面的數(shù)據(jù)塊實際的物理位置可能是不連續(xù)的 . 需要通過對換文件的 inode檢索. 這就降低了存取效率 。
每個對換文件或?qū)Q設備 由 struct swap — info — struct 結(jié)構來描述。 有關對換設備的 函數(shù)主要是 get — swap — page ( …) . 當內(nèi)存中的頁面需要被換出時 . 調(diào)用 get — swap —page函數(shù) 申請得到一個對換空間中的物理頁面 。 如果成功, 就返 回一個非零代 碼 . 否則返 回0。
Linux使用分頁管理機制來更加有效地利用物理內(nèi)存.當創(chuàng)建一個進程時.僅僅把當前進程的一小部分真正裝入內(nèi) 存.其余部分需要訪問時.處理器產(chǎn)生一個頁故障.由缺頁中斷服務程序根據(jù)缺頁虛擬地址和出錯碼調(diào)用寫拷貝函數(shù)do—wp—page、此地址所屬的vma的vm—ops指向的nopage、do—swap—page.swap—in等函數(shù)將需要的頁換入物理內(nèi)存。隨著可執(zhí)行映像的運行和頁面的換入.系統(tǒng)中的內(nèi)存有可能變得不足.這時Linux核心就必須調(diào)用kswapd守護進程釋放部分物理內(nèi)存。kswapd在系統(tǒng)啟動時由init進程建立。在系統(tǒng)的運行過程中。它被定期喚醒。檢查系統(tǒng)中的空閑物理內(nèi)存是否很少。如果是.則釋放一部分內(nèi)存.或者將一些頁面換出到對換空間。然后繼續(xù)睡眠。
- kswapd 守護進程負責確保內(nèi)存保持可用空閑空間。它監(jiān)測內(nèi)核中的 pages_high 和pages_low 標記,如果空閑內(nèi)存空間值小于 pages_low 值, kswwapd 進程開始掃描并嘗試每次回收 32 個頁面,如此重復直至空閑內(nèi)存空間大于 pages_high 值。
- kswapd 進程履行以下操作:
- * 假如頁面未改變,它將該頁面放入 free list。
- * 假如頁面發(fā)生改變且被文件系統(tǒng)回寫,它將頁面內(nèi)容寫入磁盤。
- * 假如頁面發(fā)生改變且未被文件系統(tǒng)回寫(無名頁) ,它將頁面內(nèi)容寫入 swap 設備。
缺頁中斷和頁面換入 頁面換入主要由缺頁中斷服務入口函數(shù)do—page—fault來實現(xiàn)。當系統(tǒng)中產(chǎn)生頁面故障時.如果虛擬內(nèi)存地址有效.則產(chǎn)生錯誤的原因有如下兩種: 虛擬內(nèi)存地址對應的物理頁不在內(nèi)存中。那么它必然在磁盤或?qū)Q空間中.如果在磁盤上.那么我們調(diào)用do—nO— page函數(shù).而do—no—page調(diào)用vma一>vm—ops一>nopage()函數(shù)建立頁面映射.從對換空間或磁盤中調(diào)入頁面.或者通過do—swap—page()函數(shù)調(diào)用swap—in()來換入頁面。 · 該虛擬地址對應的物理頁在內(nèi)存。但是被寫保護.如果這種情況發(fā)生在一個共享頁面上.則需要“寫拷貝“函數(shù)do— wp—page來換入頁面.do—wp—page函數(shù)首先調(diào)用一get—free—page獲得一新頁面.然后調(diào)用copy—COW—page拷貝頁面的內(nèi)容.當然還要調(diào)用相應的刷新函數(shù)刷新TLB和緩存等。
頁交換進程和頁面換出 正如我們上面所描述的.系統(tǒng)使用kswapd守護進程來定期地換出頁面。使系統(tǒng)中有足夠的空閑物理內(nèi)存頁。 kswapd進程定期地檢查系統(tǒng)中的空閑頁面數(shù).如果少于一定值.則按照以下三中途徑獲得空閑頁面:
①減少緩沖區(qū)和頁面高速緩存的大小;
②把共享內(nèi)存占用的頁面置換到對換空間; ③換出或丟棄物理內(nèi)存頁。
|