Linux進程地址空間 && 進程內存佈局[轉]

一 進程空間分佈概述linux

 
    對於一個進程,其空間分佈以下圖所示:

                                     

 

 

 

程序段(Text):程序代碼在內存中的映射,存放函數體的二進制代碼。程序員

初始化過的數據(Data):在程序運行初已經對變量進行初始化的數據。算法

未初始化過的數據(BSS):在程序運行初未對變量進行初始化的數據。編程

棧 (Stack):存儲局部、臨時變量,函數調用時,存儲函數的返回指針,用於控制函數的調用和返回。在程序塊開始時自動分配內存,結束時自動釋放內存,其操做方式相似於數據結構中的棧。安全

堆 (Heap):存儲動態內存分配,須要程序員手工分配,手工釋放.注意它與數據結構中的堆是兩回事,分配方式相似於鏈表。數據結構

 

 

 
注:1.Text, BSS, Data段在編譯時已經決定了進程將佔用多少VM
        能夠經過size,知道這些信息:
 
          2. 正常狀況下,Linux進程不能對用來存放程序代碼的內存區域執行寫操做,即程序代碼是以只讀的方式加載到內存中,但它能夠被多個進程安全的共享。
 
二  內核空間和用戶空間
 
       Linux的虛擬地址空間範圍爲0~4G,Linux內核將這4G字節的空間分爲兩部分,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF)供內核使用,稱爲「內核空間」。而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF)供各個進程使用,稱爲「用戶空間。由於每一個進程能夠經過系統調用進入內核,所以,Linux內核由系統內的全部進程共享。因而,從具體進程的角度來看,每一個進程能夠擁有4G字節的虛擬空間。

    Linux使用兩級保護機制:0級供內核使用,3級供用戶程序使用,每一個進程有各自的私有用戶空間(0~3G),這個空間對系統中的其餘進程是不可見的,最高的1GB字節虛擬內核空間則爲全部進程以及內核所共享。
    內核空間中存放的是內核代碼和數據,而進程的用戶空間中存放的是用戶程序的代碼和數據。不論是內核空間仍是用戶空間,它們都處於虛擬空間中。 雖然內核空間佔據了每一個虛擬空間中的最高1GB字節,但映射到物理內存卻老是從最低地址(0x00000000),另外,使用虛擬地址能夠很好的保護內核空間被用戶空間破壞,虛擬地址到物理地址轉換過程有操做系統和CPU共同完成(操做系統爲CPU設置好頁表,CPU經過MMU單元進行地址轉換)。多線程

      
        注:多任務操做系統中的每個進程都運行在一個屬於它本身的內存沙盒中,這個沙盒就是虛擬地址空間(virtual address space),在32位模式下,它老是一個4GB的內存地址塊。這些虛擬地址經過頁表(page table)映射到物理內存,頁表由操做系統維護並被處理器引用。每一個進程都擁有一套屬於它本身的頁表。
 
  進程內存空間分佈以下圖所示:
 
                           

 

     一般32位Linux內核地址空間劃分0~3G爲用戶空間,3~4G爲內核空間
 
     注: 1.這裏是32位內核地址空間劃分,64位內核地址空間劃分是不一樣的
          2.現代的操做系統都處於32位保護模式下。每一個進程通常都能尋址4G的物理空間。可是咱們的物理內存通常都是幾百M,進程怎麼能得到4G 的物理空間呢?這就是使用了虛擬地址的好處,一般咱們使用一種叫作虛擬內存的技術來實現,由於可使用硬盤中的一部分來看成內存使用 。
                                                
 
   
        Linux系統對自身進行了劃分,一部分核心軟件獨立於普通應用程序,運行在較高的特權級別上,它們駐留在被保護的內存空間上,擁有訪問硬件設備的全部權限,Linux將此稱爲內核空間。
        相對地,應用程序則是在「用戶空間」中運行。運行在用戶空間的應用程序只能看到容許它們使用的部分系統資源,而且不能使用某些特定的系統功能,也不能直接訪問內核空間和硬件設備,以及其餘一些具體的使用限制。
        將用戶空間和內核空間置於這種非對稱訪問機制下有很好的安全性,能有效抵禦惡意用戶的窺探,也能防止質量低劣的用戶程序的侵害,從而使系統運行得更穩定可靠。
            內核空間在頁表中擁有較高的特權級(ring2或如下),所以只要用戶態的程序試圖訪問這些頁,就會致使一個頁錯誤(page fault)。在Linux中,內核空間是持續存在的,而且在全部進程中都映射到一樣的物理內存,內核代碼和數據老是可尋址的,隨時準備處理中斷和系統調用。與之相反,用戶模式地址空間的映射隨着進程切換的發生而不斷的變化,以下圖所示:

                                           

      上圖中藍色區域表示映射到物理內存的虛擬地址,而白色區域表示未映射的部分。能夠看出,Firefox使用了至關多的虛擬地址空間,由於它佔用內存較多。app

 
 三  進程內存佈局
 
       Linux進程標準的內存段佈局,以下圖所示,地址空間中的各個條帶對應於不一樣的內存段(memory segment),如:堆、棧之類的。
 
                                
 
                                                  q
 
      
          注:這些段只是簡單的虛擬內存地址空間範圍,與Intel處理器的段沒有任何關係。
 
      幾乎每一個進程的虛擬地址空間中各段的分佈都與上圖徹底一致,這就給遠程發掘程序漏洞的人打開了方便之門。一個發掘過程每每須要引用絕對內存地址:棧地址,庫函數地址等。遠程攻擊者必須依賴地址空間分佈的一致性,來探索出這些地址。若是讓他們猜個正着,那麼有人就會被整了。所以,地址空間的隨機排布方式便逐漸流行起來,Linux經過對棧、內存映射段、堆的起始地址加上隨機的偏移量來打亂佈局。但不幸的是,32位地址空間至關緊湊,這給隨機化所留下的空間不大,削弱了這種技巧的效果。
 

     進程地址空間中最頂部的段是棧,大多數編程語言將之用於存儲函數參數和局部變量。調用一個方法或函數會將一個新的棧幀(stack frame)壓入到棧中,這個棧幀會在函數返回時被清理掉。因爲棧中數據嚴格的遵照FIFO的順序,這個簡單的設計意味着沒必要使用複雜的數據結構來追蹤棧中的內容,只須要一個簡單的指針指向棧的頂端便可,所以壓棧(pushing)和退棧(popping)過程很是迅速、準確。進程中的每個線程都有屬於本身的棧。編程語言

      經過不斷向棧中壓入數據,超出其容量就會耗盡棧所對應的內存區域,這將觸發一個頁故障(page fault),而被Linux的expand_stack()處理,它會調用acct_stack_growth()來檢查是否還有合適的地方用於棧的增加。若是棧的大小低於RLIMIT_STACK(一般爲8MB),那麼通常狀況下棧會被加長,程序繼續執行,感受不到發生了什麼事情。這是一種將棧擴展到所需大小的常規機制。然而,若是達到了最大棧空間的大小,就會棧溢出(stack overflow),程序收到一個段錯誤(segmentation fault)。函數

 
     注:動態棧增加是惟一一種訪問未映射內存區域而被容許的情形,其餘任何對未映射內存區域的訪問都會觸發頁錯誤,從而致使段錯誤。一些被映射的區域是隻讀的,所以企圖寫這些區域也會致使段錯誤。
 
內存映射段     
     在棧的下方是內存映射段,內核將文件的內容直接映射到內存。任何應用程序均可以經過Linux的mmap()系統調用或者Windows的CreateFileMapping()/MapViewOfFile()請求這種映射。內存映射是一種方便高效的文件I/O方式,因此它被用來加載動態庫。建立一個不對應於任何文件的匿名內存映射也是可能的,此方法用於存放程序的數據。在Linux中,若是你經過malloc()請求一大塊內存,C運行庫將會建立這樣一個匿名映射而不是使用堆內存。「大塊」意味着比MMAP_THRESHOLD還大,缺省128KB,能夠經過mallocp()調整。
 
      與棧同樣,堆用於運行時內存分配;但不一樣的是,堆用於存儲那些生存期與函數調用無關的數據。大部分語言都提供了堆管理功能。在C語言中,堆分配的接口是malloc()函數。若是堆中有足夠的空間來知足內存請求,它就能夠被語言運行時庫處理而不須要內核參與,不然,堆會被擴大,經過brk()系統調用來分配請求所需的內存塊。堆管理是很複雜的,須要精細的算法來應付咱們程序中雜亂的分配模式,優化速度和內存使用效率。處理一個堆請求所需的時間會大幅度的變更。實時系統經過特殊目的分配器來解決這個問題。堆在分配過程當中可能會變得零零碎碎,以下圖所示:
                          

 

       通常由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式相似於鏈表。
         
 
BBS和數據段
      在C語言中,BSS和數據段保存的都是靜態(全局)變量的內容。區別在於BSS保存的是未被初始化的靜態變量內容,他們的值不是直接在程序的源碼中設定的。BSS內存區域是匿名的,它不映射到任何文件。若是你寫static intcntActiveUsers,則cntActiveUsers的內容就會保存到BSS中去。
      數據段保存在源代碼中已經初始化的靜態變量的內容。數據段不是匿名的,它映射了一部分的程序二進制鏡像,也就是源代碼中指定了初始值的靜態變量。因此,若是你寫static int cntActiveUsers=10,則cntActiveUsers的內容就保存在了數據段中,並且初始值是10。儘管數據段映射了一個文件,但它是一個私有內存映射,這意味着更改此處的內存不會影響被映射的文件。

      你能夠經過閱讀文件/proc/pid_of_process/maps來檢驗一個Linux進程中的內存區域。記住:一個段可能包含許多區域。好比,每一個內存映射文件在mmap段中都有屬於本身的區域,動態庫擁有相似BSS和數據段的額外區域。有時人們提到「數據段」,指的是所有的數據段+BSS+堆。

     你還能夠經過nm和objdump命令來察看二進制鏡像,打印其中的符號,它們的地址,段等信息。最後須要指出的是,前文描述的虛擬地址佈局在linux中是一種「靈活佈局」,並且做爲默認方式已經有些年頭了,它假設咱們有值RLIMT_STACK。可是,當沒有該值得限制時,Linux退回到「經典佈局」,以下圖所示:

                                   
 
 
 

進程內存分佈

  以前一直在分析棧,棧這個東西的做用也介紹得差很少了,可是棧在哪兒尚未搞清楚,以及堆、代碼、全局變量它們在哪兒,這都牽涉到進程的內存分佈。

linux 0.01 的進程內存分佈

  內存分佈隨着操做系統的更新換代,愈來愈科學合理,也愈來愈複雜,因此咱們仍是先了解一下早期操做系統的典型 linux 0.01 的進程的內存分佈:

  linux 0.01 的一個進程固定擁有64MB的線性內存空間(ACM競賽中單個程序的最大內存佔用限制爲64MB,這確定有貓膩O(∩_∩)O~),各個進程挨個放置在一張頁目錄表中,一個頁目錄表可管理4G的線性空間,所以 linux0.01 最多有 64個進程。每一個進程的內存分佈以下:

  • .text 裏存的是機器碼序列
  • .rodata 裏存的是源字符串等只讀內容
  • .data 裏存的是初始化的全局變量
  • .bss 上一篇介紹過了,存的是未初始化的全局變量
  • 堆、棧就不用介紹了吧!
  • 【注意】static 變量未初始化默認賦值爲0或者空格。未初始化變量和初始化爲0,都分配在.bss段。

  .text .rodata .data .bss 是常駐內存的,也就是說進程從開始運行到進程僵死它們一直蹲在那裏,因此訪問它們用的是常量地址;而棧是不斷的加幀(函數調用)、減幀(函數返回)的,幀內的局部變量只能用相對於當前 esp(指向棧頂)或 ebp(指向當前幀)的相對地址來訪問。

  棧被放置在高地址也是有緣由的: 調用函數(加幀)是減 esp 的,函數返回(減幀)是加 esp 的,調用在前,因此棧是向低地址擴展的,放在高地址再合適不過了。

現代操做系統的進程內存分佈

  認識了 linux 0.01 的內存分佈後,再看看現代操做系統的內存分佈發生了什麼變化:

  首先,linux 0.01 進程的64MB內存限制太過期了,如今的程序都有潛力使用到 2GB、3GB 的內存空間(每一個進程一張頁目錄表),固然,機器有硬傷的話也沒辦法,個人電腦就只有 2GB 的內存,想用 3GB 的內存是沒期望了。但也不是有4GB內存就能夠用4GB(32位),由於操做系統還要佔個坑呢!現代 linux 中 0xC0000000 以上的 1GB 空間是操做系統專用的,而 linux 0.01 中第1個 64MB 是操做系統的坑,因此別的進程徹底佔有它們的 64MB,也不用跟操做系統客氣。

  其次,linux 0.01只有進程沒有線程,可是現代 linux 有多線程了(linux 的線程實際上是個輕量級的進程),一個進程的多個線程之間共享全局變量、堆、打開的文件…… 但棧是不能共享的:棧中各層函數幀表明着一條執行線索,一個線程是一條執行線索,因此每一個線程獨佔一個棧,而這些棧又都必須在所屬進程的內存空間中。

  根據以上兩點,進程的內存分佈就變成了下面這個樣子:

  再者,若是把動態裝載的動態連接庫也考慮進去的話,上面的分佈圖將會更加"破碎"。

  若是咱們的程序沒有采用多線程的話,通常能夠簡單地認爲它的內存分佈模型是 linux 0.01 的那種。

相關文章
相關標籤/搜索