Linux内存模型
了解linux的內存模型,或許不能讓你大幅度提高編程能力,但是作為一個基本知識點應該熟悉。坐火車外出旅行時,即時你對沿途的地方一無所知,仍然可以到達目標地。但是你對整個路途都很比較清楚的話,每到一個站都知道自己在哪里,知道當地的風土人情,對比一下所見所想,旅程可能更有趣一些。?
類似的,了解linux的內存模型,你知道每塊內存,每個變量,在系統中處于什么樣的位置。這同樣會讓你心情愉快,知道這些,有時還會讓你的生活輕更松些。看看變量的地址,你可以大致斷定這是否是一個有效的地址。一個變量被破壞了,你可以大致推斷誰是犯罪嫌疑人。?
Linux的內存模型,一般為:
| 地址 | 作用 | 說明 |
| >=0xc000 0000 | 內核虛擬存儲器 | 用戶代碼不可見區域 |
| <0xc000 0000 | Stack(用戶棧) | ESP指向棧頂 |
| ? | ↓ ? ↑ | ? 空閑內存 |
| >=0x4000 0000 | 文件映射區 | ? |
| <0x4000 0000 | ? ? ↑ | ? 空閑內存 ? |
| ? | Heap(運行時堆) | 通過brk/sbrk系統調用擴大堆,向上增長。 |
| ? | .data、.bss(讀寫段) | 從可執行文件中加載 |
| >=0x0804 8000 | .init、.text、.rodata(只讀段) | 從可執行文件中加載 |
| <0x0804 8000 | 保留區域 | ? |
?
很多書上都有類似的描述,本圖取自于《深入理解計算機系統》p603,略做修改。本圖比較清析,很容易理解,但仍然有兩點不足。下面補充說明一下:
?
1.???????? 第一點是關于運行時堆的。
為說明這個問題,我們先運行一個測試程序,并觀察其結果:
#include <stdio.h> intmain(intargc, 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); getchar(); return 0; }
運行后,輸出結果為:
main=0x8048404 print=0x8048324
first=0xbfcd1264
p0=0x9253008 p1=0xb7ec0008 p2=0x97ebf008 p3=0x57ebe008
? main和print兩個函數是代碼段(.text)的,其地址符合表一的描述。
l???????? first是第一個臨時變量,由于在first之前還有一些環境變量,它的值并非0xbfffffff,而是0xbfcd1264,這是正常的。
l???????? p0是在堆中分配的,其地址小于0x4000 0000,這也是正常的。
l???????? 但p1和p2也是在堆中分配的,而其地址竟大于0x4000 0000,與表一描述不符。?
原因在于:運行時堆的位置與內存管理算法相關,也就是與malloc的實現相關。關于內存管理算法的問題,我們在后繼文章中有詳細描述,這里只作簡要說明。在glibc實現的內存管理算法中,Malloc小塊內存是在小于0x4000 0000的內存中分配的,通過brk/sbrk不斷向上擴展,而分配大塊內存,malloc直接通過系統調用mmap實現,分配得到的地址在文件映射區,所以其地址大于0x4000 0000。?
從maps文件中可以清楚的看到一點:
| 00514000-00515000 r-xp 00514000 00:00 0 00624000-0063e000 r-xp 00000000 03:01 718192???? /lib/ld-2.3.5.so 0063e000-0063f000 r-xp 00019000 03:01 718192???? /lib/ld-2.3.5.so 0063f000-00640000 rwxp 0001a000 03:01 718192???? /lib/ld-2.3.5.so 00642000-00766000 r-xp 00000000 03:01 718193???? /lib/libc-2.3.5.so 00766000-00768000 r-xp 00124000 03:01 718193???? /lib/libc-2.3.5.so 00768000-0076a000 rwxp 00126000 03:01 718193???? /lib/libc-2.3.5.so 0076a000-0076c000 rwxp 0076a000 00:00 0 08048000-08049000 r-xp 00000000 03:01 1307138??? /root/test/mem/t.exe 08049000-0804a000 rw-p 00000000 03:01 1307138??? /root/test/mem/t.exe 09f5d000-09f7e000 rw-p 09f5d000 00:00 0????????? [heap] 57e2f000-b7f35000 rw-p 57e2f000 00:00 0 b7f44000-b7f45000 rw-p b7f44000 00:00 0 bfb2f000-bfb45000 rw-p bfb2f000 00:00 0????????? [stack] |
?
2.???????? 第二是關于多線程的。
現在的應用程序,多線程的居多。表一所描述的模型無法適用于多線程環境。按表一所述,程序最多擁有上G的棧空間,事實上,在多線程情況下,能用的棧空間是非常有限的。為了說明這個問題,我們再看另外一個測試:
#include <stdio.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 intmain(intargc, char* argv[]) { intfirst = 0; inti= 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=0xbfd3d35c
(0xb7f2cbb0): first=0xb7f2c454
(0xb7f2cbb0): p0=0x84d52d8 p1=0xb4c27008
(0xb752bbb0): first=0xb752b454
(0xb752bbb0): p0=0x84d56e0 p1=0xb4b26008
(0xb6b2abb0): first=0xb6b2a454
(0xb6b2abb0): p0=0x84d5ae8 p1=0xb4a25008
(0xb6129bb0): first=0xb6129454
(0xb6129bb0): p0=0x84d5ef0 p1=0xb4924008
(0xb5728bb0): first=0xb5728454
(0xb5728bb0): p0=0x84d62f8 p1=0xb7e2c008
?
我們看一下:
主線程與第一個線程的棧之間的距離:0xbfd3d35c - 0xb7f2c454=0x7e10f08=126M
第一個線程與第二個線程的棧之間的距離:0xb7f2c454 - 0xb752b454=0xa01000=10M
其它幾個線程的棧之間距離均為10M。
也就是說,主線程的棧空間最大為126M,而普通線程的棧空間僅為10M,超這個范圍就會造成棧溢出。
系統為進程分配數據空間有三種形式。
靜態分配
整塊靜態分配空間,包括其中的所有數據實體,都是在進程創建時由系統一次性分配的(同時為UNIX稱為Text的代碼分配空間)。這塊空間在進程運行期間保持不變。
初始化的和未初始化的實體分別放在初始化數據段和未初始化數據段(BSS)。后者和前者不同,在.o文件a.out文件里都不存在(只有構架信息),在進程的虛擬空間里才展開。
extern變量和static變量采用靜態分配。
在進程創建時做靜態分配,分配正文(text)段、數據段和棧空間。
正文和初始化數據是按a.out照樣復制過來;未初始化數據按構架信息展開,填以0或空;棧空間的大小由鏈接器開關(具體哪個開關忘了)決定。
棧分配
整個棧空間已在進程創建時分配好。棧指針SP的初值的設定,確定了棧空間的大小。鏈接器的某個開關可以設定棧空間的大小。在進程運行期間,棧空間的大小不變。但是,在進程剛啟動時,棧空間是空的,里面沒有實體。在進程運行期間,對具體實體的棧分配是進程自行生成(壓棧)和釋放(彈出)實體,系統并不參與。
auto變量和函數參數采用棧分配。
只要壓入的實體的總長度不超過棧空間尺寸,棧分配就與系統無關。如果超過了,就會引發棧溢出錯誤。
堆分配
當進程需要生成實體時,向系統申請分配空間;不再需要該實體時,可以向系統申請回收這塊空間。
堆分配使用特定的函數(如malloc()等)或操作符(new)。所生成的實體都是匿名的,只能通過指針去訪問。
對實體來說,棧分配和堆分配都是動態分配:實體都是在進程運行中生成和消失。而靜態分配的所有實體都是在進程創建時全部分配好的,在運行中一直存在。
同為動態分配,棧分配與堆分配是很不相同的。前者是在進程創建時由系統分配整塊棧空間,以后實體通過壓棧的方式產生,并通過彈出的方式取消。不管是否產生實體,產生多少實體,棧空間總是保持原來的大小。后者并沒有預設的空間,當需要產生實體時,才向系統申請正好能容納這個實體的空間。當不再需要該實體時,可以向系統申請回收這塊空間。因此,堆分配是真正的動態分配。
顯然,堆分配的空間利用率最高。
棧分配和靜態分配也有共性:整塊空間是在進程創建時由系統分配的。但是,后者同時分配了所有實體的空間,而前者在進程啟動時是空的。另外,棧上的實體和數據段里的實體都是有名實體,可以通過標識符來訪問。
| ? | 靜態分配 | 棧分配 | 堆分配 |
| 整塊空間生成 | 進程創建時 | 進程創建時 | 用一點分配一點 |
| 實體生成時間 | 進程創建時 | 進程運行時 | 進程運行時 |
| 實體生成者 | 操作系統 | 進程 | 進程申請/系統實施 |
| 生命期 | 永久 | 臨時 | 完全可控 |
| 有名/匿名 | 有名 | 有名 | 匿名 |
| 訪問方式 | 能以標識訪問 | 能以標識訪問 | 只能通過指針訪問 |
| 空間可否回收 | 不可 | 不可 | 可以 |
?
棧溢出的后果是比較嚴重的,或者出現Segmentation fault錯誤,或者出現莫名其妙的錯誤。
總結
- 上一篇: 在showModalDialog和sho
- 下一篇: 物联网的兴起与二维码的前景