linux進程地址空間分析及其應用_legend

 

(1)linux進程的虛擬地址傳統空間分佈

注:爲了使內核切換到傳統內存佈局,執行命令#sysctl -w vm.legacy_va_layout=1(因爲Linux 2.6.7及以後版本內核已經默認使用新的內存佈局方式了)。

(1.1)虛擬地址空間分類

在32位機器上linux操作系統中的進程的地址空間大小是4G

其中0-3G(0x00000000~0xbfffffff)是用戶空間,用戶態空間無論進程運行在用戶態還是內核態都可以尋址;

3G-4G(0xc0000000~0xffffffff)是內核空間,只有內核態的進程才能尋址;

即:用戶空間進程只能用0~3G的用戶空間的虛擬地址,不可以用3G-4G的內核空間的虛擬地址;

       內核空間進程可以用0~4G的虛擬地址空間;

(1.2)傳統佈局方式中虛擬地址空間之用戶空間的各個段組成

 

(1.2.1)棧(stack)

棧:存儲局部變量、函數參數、返回地址等;

自動變量(臨時變量)以及每次函數調用時所需保存的信息都存放在此段中。

每次函數調用時,其返回地址、以及調用者的環境信息(例如某些機器寄存器)都存放在棧中。然後,新被調用的函數在棧上爲其自動和臨時變量分配存儲空間。通過以這種方式使用棧,C函數可以遞歸調用。

虛擬地址空間傳統內存佈局中,進程的棧從地址0xc0000000向低地址發展;

(1.2.1.1)棧的原理

棧由編譯器自動分配釋放,行爲類似數據結構中的棧(先進後出)。

進程中的每個線程都有屬於自己的棧。向棧中不斷壓入數據時,若超出其容量就會耗盡棧對應的內存區域,從而觸發一個頁錯誤。此時若棧的大小低於堆棧最大值RLIMIT_STACK(通常是8M),則棧會動態增長,程序繼續運行。映射的棧區擴展到所需大小後,不再收縮。

(1.2.1.2)棧的大小

Linux中ulimit -s命令可查看和設置堆棧最大值,當程序使用的堆棧超過該值時, 發生棧溢出(Stack Overflow),程序收到一個段錯誤(Segmentation Fault)。

注意,調高堆棧容量可能會增加內存開銷和啓動時間。

進程的子線程們有各自的私有棧,可以共享父進程分配的堆內存空間,只有一個主線程的進程也就是有主線程對應的棧,所以棧這個說法通常只有線程棧,並沒有明確的進程棧之說,那就更沒有所謂的進程棧大小了。

通常使用ulimit -s可以看到”進程對應棧“的大小(現代linux系統下通常是8MB大小),不論在只有一個主線程的進程中,還是包含許多子線程的進程,都是指線程棧大小,默認8MB,

(1.2.1.3)函數調用時棧的行爲

棧在函數調用時,首先壓入主調函數中下條指令(函數調用語句的下條可執行語句)的地址,然後是函數實參,然後是被調函數的局部變量。本次調用結束後,局部變量先出棧,然後是參數,最後棧頂指針指向最開始存的指令地址,程序由該點繼續運行下條可執行語句

 

(1.2.2)內存映射區(memory mapping region)

內存映射區:映射可執行文件用到的動態鏈接庫,;

虛擬地址空間傳統內存佈局中(linux 2.4內核),內存映射區域從0×40000000向高地址發展,棧所用內存相對較小(通常小於100MB),因此內存映射區域約有2GB左右的映射空間;

在新的佈局(在Linux 2.6內核)中,共享庫的起始地址被往上移動至更靠近棧區的位置。

(1.2.2.1)內存映射區的背景

內核將硬盤文件的內容直接映射到內存, 任何應用程序都可通過Linux的mmap()系統調用請求這種映射。內存映射是一種方便高效的文件I/O方式, 因而被用於裝載動態共享庫。

注:用戶也可創建匿名內存映射,該映射沒有對應的文件, 可用於存放程序數據在 Linux中,若通過malloc()請求一大塊內存,C運行庫將創建一個匿名內存映射,而不使用堆內存。”大塊” 意味着比閾值 MMAP_THRESHOLD還大,缺省爲128KB,可通過mallopt()調整。

(1.2.3)堆(heap)

堆:存儲動態分配的內存;

通常在堆中進行動態存儲分配。由於歷史上形成的慣例,堆位於非初始化數據段頂和棧底之間。

進程堆的起始點大於BSS段的結束點,並向高地址發展,因爲0×40000000以上已用作內存映射用,因此堆的大小是接近1G,這有點太小了。

(1.2.3.1)堆的原理

堆用於存放進程運行時動態分配的內存段,可動態擴張或縮減。堆中內容是匿名的,不能按名字直接訪問,只能通過指針間接訪問。當進程調用malloc(C)/new(C++)等函數分配內存時,新分配的內存動態添加到堆上(擴張);當調用free(C)/delete(C++)等函數釋放內存時,被釋放的內存從堆中剔除(縮減) 。

 分配的堆內存是經過字節對齊的空間,以適合原子操作。堆管理器通過鏈表管理每個申請的內存,由於堆申請和釋放是無序的,最終會產生內存碎片。堆內存一般由應用程序分配釋放,回收的內存可供重新使用。若程序員不釋放,程序結束時操作系統可能會自動回收。

注:堆不同於數據結構中的”堆”,其行爲類似鏈表。

(1.2.3.2)使用堆的問題

使用堆時經常出現兩種問題:

1) 釋放或改寫仍在使用的內存(“內存破壞/踩內存”);

2)未釋放不再使用的內存(“內存泄漏”)。

當釋放次數少於申請次數時,可能已造成內存泄漏。泄漏的內存往往比忘記釋放的數據結構更大,因爲所分配的內存通常會圓整爲下個大於申請數量的2的冪次(如申請212B,會圓整爲256B)(即申請空間大小往下2的冪來取整)。

(1.2.3.3)堆的總大小

linux2.4中,在有共享庫的情況下,留給堆的可用空間還有兩處:

一處是從.bss段到0x40000000,約不到1GB的空間;

另一處是從共享庫到棧之間的空間,約不到2GB。

這兩塊空間大小取決於棧、共享庫的大小和數量。這樣來看,是否應用程序可申請的最大堆空間只有2GB?事實上,這與Linux內核版本有關。在上面給出的進程地址空間經典佈局圖中,共享庫的裝載地址爲0x40000000,這實際上是Linux kernel 2.6版本之前的情況了。

在2.6版本里,共享庫的裝載地址已經被挪到靠近棧的位置,即位於0xBFxxxxxx附近,因此,此時的堆範圍就不會被共享庫分割成2個“碎片”,故kernel 2.6的32位Linux系統中,malloc申請的最大內存理論值在2.9GB左右。

(1.2.3.4)堆和棧的區別

1)管理方式:

棧由編譯器自動管理;堆由程序員控制,使用方便,但易產生內存泄露。

2)生長方向:

棧向低地址擴展(即”向下生長”),是連續的內存區域;堆向高地址擴展(即”向上生長”),是不連續的內存區域。這是由於系統用鏈表來存儲空閒內存地址,自然不連續,而鏈表從低地址向高地址遍歷。

3)空間大小:

棧頂地址和棧的最大容量由系統預先規定(通常默認2M或10M);堆的大小則受限於計算機系統中有效的虛擬內存,32位Linux系統中堆內存可達2.9G空間。

4)存儲內容:

棧在函數調用時,首先壓入主調函數中下條指令(函數調用語句的下條可執行語句)的地址,然後是函數實參,然後是被調函數的局部變量。本次調用結束後,局部變量先出棧,然後是參數,最後棧頂指針指向最開始存的指令地址,程序由該點繼續運行下條可執行語句。堆通常在頭部用一個字節存放其大小,堆用於存儲生存期與函數調用無關的數據,具體內容由程序員安排。

5)分配方式:

棧可靜態分配或動態分配。靜態分配由編譯器完成,如局部變量的分配。動態分配由alloca函數在棧上申請空間,用完後自動釋放。堆只能動態分配且手工釋放。

6)分配效率:

棧由計算機底層提供支持:分配專門的寄存器存放棧地址,壓棧出棧由專門的指令執行,因此效率較高。

堆由函數庫提供,機制複雜,效率比棧低得多。Windows系統中VirtualAlloc可直接在進程地址空間中分配一塊內存,快速且靈活。

7)分配後系統響應:

只要棧剩餘空間大於所申請空間,系統將爲程序提供內存,否則報告異常提示棧溢出。
 操作系統爲堆維護一個記錄空閒內存地址的鏈表。

當系統收到程序的內存分配申請時,會遍歷該鏈表尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點鏈表中刪除,並將該結點空間分配給程序。

若無足夠大小的空間(可能由於內存碎片太多),有可能調用系統功能去增加程序數據段的內存空間,以便有機會分到足夠大小的內存,然後進行返回。大多數系統會在該內存空間首地址處記錄本次分配的內存大小,供後續的釋放函數(如free/delete)正確釋放本內存空間。

此外,由於找到的堆結點大小不一定正好等於申請的大小,系統會自動將多餘的部分重新放入空閒鏈表中。

8)碎片問題

棧不會存在碎片問題,因爲棧是先進後出的隊列,內存塊彈出棧之前,在其上面的後進的棧內容已彈出。

而頻繁申請釋放操作會造成堆內存空間的不連續,從而造成大量碎片,使程序效率降低。

堆容易造成內存碎片;由於沒有專門的系統支持,效率很低;由於可能引發用戶態和內核態切換,內存申請的代價更爲昂貴。

所以棧在程序中應用最廣泛,函數調用也利用棧來完成,調用過程中的參數、返回地址、棧基指針和局部變量等都採用棧的方式存放。所以,建議儘量使用棧,僅在分配大量或大塊內存空間時使用堆。

 

(1.2.4)BSS段 (bss segment)

BSS段:未初始化或初值爲0的全局變量和靜態局部變量;

bss 這一名稱來源於早期彙編程序的一個操作,意思是"block started by symbol",在程序開始執行之前,內核將此段初始化爲0。

特點是:可讀寫的,在程序執行之前BSS段會自動清0。所以,未初始的全局變量在程序執行之前已經成0了。

C語言中,未顯式初始化的靜態分配變量被初始化爲0(算術類型)或空指針(指針類型)。由於程序加載時,BSS會被操作系統清零,所以未賦初值或初值爲0的全局變量都在BSS中。

BSS段僅爲未初始化的靜態分配變量預留位置,在目標文件(可執行文件)中並不佔據空間,這樣可減少目標文件(可執行文件)體積。程序運行(程序即可執行文件被執行)需爲變量分配內存空間,故目標文件(可執行文件)必須記錄所有未初始化的靜態分配變量大小總和(通過start_bss和end_bss地址寫入機器代碼)。當加載器(loader)加載程序時,將爲BSS段分配的內存初始化爲0。

注:BSS段不包含數據,僅維護開始和結束地址,以便內存能在運行時被有效地清零。BSS所需的運行時空間由目標文件記錄,但BSS並不佔用目標文件(可執行文件)內的實際空間,即BSS節段應用程序的二進制映象文件中並不存在。

 

(1.2.5)數據段(data segment)

數據段:存儲已初始化且初值非0的全局變量和靜態局部變量;數據段屬於靜態內存分配(靜態存儲區),可讀可寫。

(1.2.5.1)數據段和BSS段的區別

1) BSS段不佔用目標文件(可執行文件)尺寸,但佔用程序內存空間;數據段佔用目標文件(可執行文件),也佔用程序內存空間。

對於大型數組如int ar0[10000] = {1, 2, 3, ...}和int ar1[10000],ar1放在BSS段,只記錄共有10000*4個字節需要初始化爲0,而不是像ar0那樣記錄每個數據1、2、3...,此時BSS爲目標文件所節省的磁盤空間相當可觀。

 2) 當程序讀取數據段的數據時,系統會出發缺頁故障,從而分配相應的物理內存;

當程序讀取BSS段的數據時,內核會將其轉到一個全零頁面,不會發生缺頁故障,也不會爲其分配相應的物理內存。

運行時數據段和BSS段的整個區段通常稱爲數據區。某些資料中“數據段”指代數據段 + BSS段 + 堆。

 

(1.2.6)文本段/代碼段/正文段(text segment)

代碼段:可執行代碼(函數)、字符串字面值(字符串常量)、只讀變量

代碼段存放 程序執行代碼(即CPU可執行的機器指令),一般C語言執行語句都編譯成機器代碼保存在代碼段。另外,文本段常常是隻讀的,以防止程序由於意外事故而修改器自身的指令(對該段的寫操作將導致段錯誤)。

另外:通常代碼段是可共享的,因此頻繁執行的程序只需要在內存中擁有一份拷貝即可。

可執行文件的正文段從0×8048000開始,然後依次是數據段,BSS段;

 

代碼段指令根據程序設計流程依次執行:對於順序指令,只會執行一次(每個進程);若有反覆,則需使用跳轉指令;若進行遞歸,則需要藉助棧來實現。

代碼段指令中包括操作碼和操作對象(或對象地址引用)。若操作對象是立即數(具體數值),將直接包含在代碼中;若是局部數據,將在棧區分配空間,然後引用該數據地址;若位於BSS段和數據段,同樣引用該數據地址。

(1.2.7)保留區(reserved)

保留區位於虛擬地址空間的最低部分,未賦予物理地址。任何對它的引用都是非法的,用於捕捉使用空指針和小整型值指針引用內存的異常情況。

它並不是一個單一的內存區域,而是對地址空間中受到操作系統保護而禁止用戶進程訪問的地址區域的總稱。大多數操作系統中,極小的地址通常都是不允許訪問的,如NULL。C語言將無效指針賦值爲0也是出於這種考慮,因爲0地址上正常情況下不會存放有效的可訪問數據。

(1.2.8)分段的好處

進程運行過程中,代碼指令根據流程依次執行,只需訪問一次(當然跳轉和遞歸可能使代碼執行多次);而數據(數據段和BSS段)通常需要訪問多次;

因此單獨開闢空間以方便訪問和節約空間。具體解釋如下:

1》當程序被裝載後,數據和指令分別映射到兩個虛存區域。數據區對於進程而言可讀寫,而指令區對於進程只讀。兩區的權限可分別設置爲可讀寫和只讀。以防止程序指令被有意或無意地改寫。

2》現代CPU具有極爲強大的緩存(Cache)體系,程序必須儘量提高緩存命中率。指令區和數據區的分離有利於提高程序的局部性。現代CPU一般數據緩存和指令緩存分離,故程序的指令和數據分開存放有利於提高CPU緩存命中率。

3》當系統中運行多個該程序的副本時,其指令相同,故內存中只須保存一份該程序的指令部分。若系統中運行數百進程,通過共享指令將節省大量空間(尤其對於有動態鏈接的系統)。其他只讀數據如程序裏的圖標、圖片、文本等資源也可共享。而每個副本進程的數據區域不同,它們是進程私有的。

 

(1.3)字符串常量 && const常量 && 棧溢出

(1.3.1)字符串常量

變量、常量分爲:局部變量、靜態局部變量、全局變量、全局靜態變量、字符串常量以及動態申請的內存區;

1)字符串常量存儲在代碼段

字符串常量,只需要讀,不需要修改,代碼段不能修改,只能讀取,放在代碼段可以節約數據段空間,這是編譯器優化的;

注:有的文章中說字符串常量則是存儲在常量存儲區;常量存儲區是在代碼區和數據區之間的應該;

 

2)字符串常量可共享

一個程序中多次出現相同的字符串常量,其實只保存了一份;

 

(1.3.2)const變量

和const無關,根據變量的性質,決定其存儲在哪裏;

 

(1.3.3)棧溢出

(1.3.3.1)分類

因爲棧通常是從高地址向低地址增長的,因此"棧溢出"分爲兩種:

超出低地址範圍的overrun(上溢)和超出高地址範圍的underrun(下溢);

"上溢"主要是由過深的函數調用引起(比如遞歸調用):

而"下溢"則會出現在數組/字符串越界的時候,即高地址越界,比如超過數組的大小;(數組的內存分佈是從低地址到高地址的)。

 

(1.3.3.2)棧溢出的檢測

(1)上溢檢測之填充魔數

對於那些不使用虛擬內存機制的RTOS,通常採用的做法是在stack創建之初就填充滿固定的字符(比如0x5a5a5a5a),如果發生了"上溢",那麼stack末端(最低地址處)的填充字符則有可能會被更改,這樣操作系統就可以在發生線程切換的時候,通過檢測線程棧的末端字符(比如最後16個字節)是否被更改來判斷是否有"上溢"發生,當然這會增加一些線程切換的開銷。之所以說是“有可能”,是因爲末端的那段字節可能正好被跳過,所以這種檢測方法並不是100%有效的。

分析:
在線程運行過程中,棧空間的使用率有起有落,但沒有被覆蓋過的"0x5a5a5a5a"一定是棧未曾達到過的區域,由此我們可以計算出棧的最大使用率。如果這個最大使用率已經逼近棧的極限(最低地址),那麼我們就應該適當增加該線程的棧空間大小,避免在更極端的情況下出現"棧溢出"。

(2)下溢檢測之數組後隨機數是否被修改;

至於"下溢",則可以在將函數的返回地址壓棧的時候,加上一個隨機產生的整數,如果出現了數組越界,那麼這個整數將被修改,這樣在函數返回的時候,就可以通過檢測這個整數是否被修改,來判斷是否有"下溢"發生。這個隨機的整數被稱爲"canary",它的原意是金絲雀,這種鳥對危險氣體的敏感度超過人類,所以過去煤礦工人往往會帶着金絲雀下井,如果金絲雀死了,礦工便知道井下有危險氣體,需要撤離。

只需要在gcc編譯的時候,加入"-fstack-protector"選項即可。一個函數對應一個stack frame,每個stack frame都需要一個canary,這會消耗掉一部分的棧空間。此外,由於每次函數返回時都需要檢測canary,代碼的整執行時間也勢必會增加。

 

 

(1.4)查看進程的內存地址空間分佈情況

1)cat /proc/Pid/maps 可以查看當前進程的虛擬地址的情況;

2)size 可執行文件

#size a.out
   text       data        bss        dec        hex    filename
5415541     130120    6697952    12243613     bad29d   a.out

 

(1.5)32位機器上查看進程地址空間的範例

範例如下:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
    int first = 0;
    int *p0 = malloc(1024);
    int *p1 = malloc(1024 * 1024);
    int *p2 = malloc(512 * 1024 * 1024 );
    int *p3 = malloc(1024 * 1024 * 1024 );
    printf("main=%p print=%p\n", main, printf);
    printf("first=%p\n", &first);
    printf("p0=%p p1=%p p2=%p p3=%p\n", p0, p1, p2, p3);
    sleep(10);
    return 0;
}

結果如下:
#root@slack:~#./a.out &
[6] 9528
main=0x80483e4 print=0x8048300
first=0xbfd00b9c
p0=0x804a008 p1=0x4018d008 p2=0x4028e008 p3=0x6028f008
 
#root@slack:~#cat /proc/9528/maps
08048000-08049000 r-xp 00000000 08:01 140878     /root/a.out
08049000-0804a000 rw-p 00000000 08:01 140878     /root/a.out
0804a000-0806b000 rw-p 00000000 00:00 0          [heap]
40000000-4001d000 r-xp 00000000 08:01 931801     /lib/ld-2.13.so
4001d000-4001e000 r--p 0001c000 08:01 931801     /lib/ld-2.13.so
4001e000-4001f000 rw-p 0001d000 08:01 931801     /lib/ld-2.13.so
4001f000-40022000 rw-p 00000000 00:00 0
40029000-40185000 r-xp 00000000 08:01 931779     /lib/libc-2.13.so
40185000-40186000 ---p 0015c000 08:01 931779     /lib/libc-2.13.so
40186000-40188000 r--p 0015c000 08:01 931779     /lib/libc-2.13.so
40188000-40189000 rw-p 0015e000 08:01 931779     /lib/libc-2.13.so
40189000-a0290000 rw-p 00000000 00:00 0
bf841000-bf862000 rw-p 00000000 00:00 0          [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]

說明:

1、因爲main()函數和printf()函數位於文本段中,而文本段是從0×08048000開始的,所以符合表中所述;

2、first是第一個臨時變量,臨時變量存在於棧中;由於在first之前還有一些環境變量,它的值並非0xbfffffff,而是0xbfcd1264,這是正常的。

3、p0是在堆中分配的,其地址小於0×4000 0000,這也是正常的。

4、但p1和p2也是在堆中分配的,而其地址竟大於0×4000 0000,與表一描述不符。

原因在於:運行時堆的位置與內存管理算法相關,也就是與malloc的實現相關。在glibc實現的內存管理算法中,malloc小塊內存是在小於0×4000 0000的內存中分配的,通過brk/sbrk不斷向上擴展;而分配大塊內存,malloc直接通過系統調用mmap實現,分配得到的地址在文件映射區,所以其地址大於0×40000000。

所以,p0是在堆上分配的,而p1~p3則是通過mmap實現的,這表現在maps文件的倒數第三行(40189000-a0290000)。

5. maps內容分析:

08048000-08049000 代碼段
08049000-0804a000 數據段
(注意本程序無BSS段)
0804a000-0806b000 堆
40000000-40189000 內存映射區,本程序映射了ld和libc動態鏈接庫
40189000-a0290000 內存映射區,malloc用其爲p1~p3分配內存
bf841000-bf862000 棧
ffffe000-fffff000 內核爲我們映射的系統調用入口代碼;

---------

(2)linux進程的虛擬地址新空間分佈

鑑於以上傳統內存佈局的限制,Linux 2.6.7及以後版本已經默認使用另一種新的內存佈局方式,如下圖所示:

比如,傳統佈局方式中,32位機器上,堆的大小最大接近1G的限制;

爲了使用此新的內存佈局,執行命令#sysctl -w vm.legacy_va_layout=0;新編譯運行程序並查看其輸出及maps文件內容:

(2.1)新佈局方式中虛擬地址空間之用戶空間組成

mm_struct 內存段

用戶空間地址還是由代碼段(文本段)、數據段,bss段,堆,內存映射區,棧組成;

(2.2)區別

棧頂和棧之間、棧和內存映射區之間,堆和BSS段之間都有一個隨機的offset,每次運行程序時的值都不一樣;Linux通過對棧、內存映射段、堆的起始地址加上隨機偏移量來打亂佈局,以免惡意程序通過計算訪問棧、庫函數等地址。

注:如果需要,當然也可以讓程序的棧和映射區域從一個固定位置開始,只需要設置全局變量randomize_va_space值爲0即可(默認值爲 1)。

 

(2.3)範例:

進程範例如上;

(2.3.1)進程的maps說明:

#root@slack:~#./a.out &
[6] 9529
main=0x80483e4 print=0x8048300
first=0xbff18e6c
p0=0x804a008 p1=0xb7554008 p2=0x97553008 p3=0x57552008
 
#root@slack:~#cat /proc/9529/maps
08048000-08049000 r-xp 00000000 08:01 140882     /root/a.out
08049000-0804a000 rw-p 00000000 08:01 140882     /root/a.out
0804a000-0806b000 rw-p 00000000 00:00 0          [heap]
575c8000-b76cc000 rw-p 00000000 00:00 0
b76cc000-b7828000 r-xp 00000000 08:01 931779     /lib/libc-2.13.so
b7828000-b7829000 ---p 0015c000 08:01 931779     /lib/libc-2.13.so
b7829000-b782b000 r--p 0015c000 08:01 931779     /lib/libc-2.13.so
b782b000-b782c000 rw-p 0015e000 08:01 931779     /lib/libc-2.13.so
b782c000-b782f000 rw-p 00000000 00:00 0
b7837000-b7839000 rw-p 00000000 00:00 0
b7839000-b7856000 r-xp 00000000 08:01 931801     /lib/ld-2.13.so
b7856000-b7857000 r--p 0001c000 08:01 931801     /lib/ld-2.13.so
b7857000-b7858000 rw-p 0001d000 08:01 931801     /lib/ld-2.13.so
bff4e000-bff6f000 rw-p 00000000 00:00 0          [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]

說明:

對比上一次maps文件內容,發現現在的內存映射區域已經從b7857000開始了,並且向下發展。p1~p3現在位於575c8000-b76cc000區域內。

(2.3.2)offset影響說明

說明:由於offset每次執行程序都是不一樣的;如下,再執行一次;

#root@slack:~#./a.out &
[6] 9564
main=0x80483e4 print=0x8048300
first=0xbfec32cc
p0=0x804a008 p1=0xb7654008 p2=0x97653008 p3=0x57652008
 
#root@slack:~#cat /proc/9564/maps
08048000-08049000 r-xp 00000000 08:01 140882     /root/a.out
08049000-0804a000 rw-p 00000000 08:01 140882     /root/a.out
0804a000-0806b000 rw-p 00000000 00:00 0          [heap]
574ea000-b75ee000 rw-p 00000000 00:00 0
b75ee000-b774a000 r-xp 00000000 08:01 931779     /lib/libc-2.13.so
b774a000-b774b000 ---p 0015c000 08:01 931779     /lib/libc-2.13.so
b774b000-b774d000 r--p 0015c000 08:01 931779     /lib/libc-2.13.so
b774d000-b774e000 rw-p 0015e000 08:01 931779     /lib/libc-2.13.so
b774e000-b7751000 rw-p 00000000 00:00 0
b7759000-b775b000 rw-p 00000000 00:00 0
b775b000-b7778000 r-xp 00000000 08:01 931801     /lib/ld-2.13.so
b7778000-b7779000 r--p 0001c000 08:01 931801     /lib/ld-2.13.so
b7779000-b777a000 rw-p 0001d000 08:01 931801     /lib/ld-2.13.so
bfa0c000-bfa2d000 rw-p 00000000 00:00 0          [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]

和之前一次執行的輸出相比較,發現程序輸出中first變量的地址已經變了,而且maps文件中棧和內存映射區域的地址也變了。

------

(3)多線程進程的虛擬地址空間分佈

(3.1)多線程範例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void* thread_proc(void* param)
{
    int  first = 0;
    int* p0 = malloc(1024);
    int* p1 = malloc(1024 * 1024);
    printf("(0x%x): first=%p/n",    pthread_self(), &first);
    printf("(0x%x): p0=%p p1=%p /n", pthread_self(), p0, p1);
    return 0;
}
 
#define N 5
int main(int argc, char* argv[])
{
    int first = 0;
    int i= 0;
    void* ret = NULL;
    pthread_t tid[N] = {0};
    printf("first=%p/n", &first);
    for(i = 0; i < N; i++)
    {
        pthread_create(tid+i, NULL, thread_proc, NULL);
    }
    for(i = 0; i < N; i++)
    {
        pthread_join(tid[i], &ret);
    }
    return 0;
}

說明:

first=0xbfbaf648
(0xb671db70): first=0xb671d384
(0xb671db70): p0=0x804a248 p1=0xb4e1d008
(0xb6f1db70): first=0xb6f1d384
(0xb6f1db70): p0=0x804a650 p1=0xb49fd008
(0xb771db70): first=0xb771d384
(0xb771db70): p0=0xb4d00468 p1=0xb4bff008
(0xb5f1db70): first=0xb5f1d384
(0xb5f1db70): p0=0xb4d00900 p1=0xb4afe008
(0xb571db70): first=0xb571d384
(0xb571db70): p0=0x804aa58 p1=0xb48fc008

主線程與第一個線程的棧之間的距離:0xbfbaf648 – 0xb771d384 = 132M

第一個線程與第二個線程的棧之間的距離:0xb771d384 – 0xb6f1d384= 8M

其它幾個線程的棧之間距離均爲8M。

也就是說,主線程的棧空間最大爲132M,而普通線程的棧空間僅爲8M,超這個範圍就會造成棧溢出(後果很嚴重)。

----------------------

(4)進程的虛擬地址和真實物理地址

linux操作系統每個進程的地址空間都是獨立的,其實這裏的獨立說得是物理空間上的獨立。虛擬地址通過頁表(Page Table)映射到物理內存,頁表由操作系統維護並被處理器引用。

execve(2)負責爲進程代碼段和數據段建立映射,真正將代碼段和數據段的內容讀入內存是由系統的缺頁異常處理程序按需完成的。另外,execve(2)還會將BSS段清零。

 

那相同的虛擬地址,不同的物理地址,他們之間是怎樣聯繫起來的呢?

在linux操作系統中,每個進程都通過一個task_struct的結構體描敘,每個進程的地址空間都通過一個mm_struct描敘,c語言中的每個段空間都通過vm_area_struct表示,他們關係如下 :

當一個程序被執行時,該程序的內容必須被放到進程的虛擬地址空間,對於可執行程序的共享庫也是如此。可執行程序並非真正讀到物理內存中,而只是鏈接到進程的虛擬內存中。

當一個可執行程序映射到進程虛擬地址空間時,一組vm_area_struct數據結構將被產生。每個vm_area_struct數據結構是或是初始化的數據,以及未初始化的數據等。

(4.1)進程加載數據的流程

在進程創建的過程中,程序內容被映射到進程的虛擬內存空間。爲了讓一個很大的程序在有限的物理內存空間運行,我們可以把這個程序的開始部分先加載到物理內存空間運行,因爲操作系統處理的是進程的虛擬地址,如果在進行虛擬到物理地址的轉換工程中,發現物理地址不存在時,這個時候就會發生缺頁異常(nopage),接着操作系統就會把磁盤上還沒有加載到內存中的數據加載到物理內存中,對應的進程頁表進行更新。

(4.1.1)物理內存不足時

如果一個進程想將一個虛擬頁裝入物理內存,而又沒有可使用的空閒物理頁,操作系統就必須淘汰物理內存中的其他頁來爲此頁騰出空間。

1)  如果從物理內存中被淘汰的頁來自於一個映像或數據文件,並且還沒有被寫過,則該頁不必保存,它可以丟掉。如果有進程在需要該頁時就可以把它從映像或數據文件中取回內存。

2)如果該頁被修改過,操作系統必須保留該頁的內容以便晚些時候在被訪問。這種頁稱爲"髒(dirty)頁",當它被從內存中刪除時,將被保存在一個稱爲交換文件的特殊文件中(磁盤上)。

(4.1.2) 內存淘汰算法

相對於處理器和物理內存的速度,訪問交換文件要很長時間,操作系統必須在將頁寫到磁盤以及再次使用時取回到內存的問題上花費心機。linux使用"最近最少使用(Least Recently Used ,LRU)"頁面調度技巧來公平地選擇哪個頁可以從系統中刪除。這種設計系統中每個頁都有一個"年齡",年齡隨頁面被訪問而改變。頁面被訪問越多它越年輕;被訪問越少越老。年老的頁是用於交換的最佳候選頁。

 

參見:https://blog.csdn.net/freeelinux/article/details/53782986

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章