前言:在講完環境變量后,相信大家對Linux有更進一步的認識,而Linux進程概念到這也快接近尾聲了,現在我們了解Linux進程中的地址空間!
本篇主要內容:
了解程序地址空間
理解進程地址空間
探究頁表和虛擬地址空間
進程地址空間
- 1. 程序地址空間
- 2. 進程地址空間
- 3. 什么是地址空間
- 3. 地址空間的管理
- 4. 頁表
- 5. 為什么要存在地址空間
- 6. 總結拓展
1. 程序地址空間
我們在學習C語言的時候,大家都了解過這樣的空間布局圖
那么到底是不是這樣排布的呢,我們來驗證一下
1 #include<stdio.h>2 #include<stdlib.h>3 4 int un_gval;5 int init_gval = 100; 6 7 int main(int argc, char *argv[], char *env[])8 { 9 printf("code addr: %p\n", main); 10 const char *str = "Hello, Linux!"; 11 printf("read only char addr: %p\n", str); 12 printf("init global value addr: %p\n", &init_gval);13 printf("uninit global value addr: %p\n", &un_gval);14 15 char *heap1=(char*)malloc(100);16 char *heap2=(char*)malloc(100);17 char *heap3=(char*)malloc(100);18 char *heap4=(char*)malloc(100);19 20 int a = 100; 21 22 printf("heap1 addr: %p\n", heap1);23 printf("heap2 addr: %p\n", heap2);24 printf("heap3 addr: %p\n", heap3);25 printf("heap4 addr: %p\n", heap4); 26 27 printf("stack addr: %p\n", &str); 28 printf("stack addr: %p\n", &heap1); 29 printf("stack addr: %p\n", &heap2); 30 printf("stack addr: %p\n", &heap3);31 printf("stack addr: %p\n", &heap4); 32 printf("a addr: %p\n",&a); 33 return 0;34 }
棧區中的數組和結構體
int num[10] ......&a[0] &a[9]struct s
{int a; ......&s.aint b; ......&s.bint c; ......&s.c
}
注意:棧區是整體向下增長,局部想上使用的,就是地址最低處,依次往上放后面的元素
但是如果我們將代碼更改還能運行過去嘛?
char *str = "Hello, Linux!";
*str = 'S';
顯然我們是不能更改的,一更改就就運行不了了
注意:其實是因為字符常量區與代碼區很接近,而編譯器在編譯時,字符常量區就是被編譯到代碼區的,代碼又不可被寫入,所以字符常量區也不可被修改
綜上:
- 棧區是整體向下增長,局部想上使用的,就是地址最低處,依次往上放后面的元素
- 常量區的字符串不允許修改
但是這都是我們之前了解的知識,現在我們來重新了解地址,我們先來看這段代碼
1 #include<stdio.h>2 #include<stdlib.h>3 #include<sys/types.h>4 #include<unistd.h>5 6 int g_val = 200;7 8 int main()9 {10 pid_t id = fork();11 if(id == 0)12 {13 // 子進程14 int cnt = 5;15 while(1)16 {17 printf("child, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);18 sleep(1);19 if(cnt == 0)20 {21 g_val = 100;22 printf("child change g_val: 200 -> 100\n");23 }24 cnt--;25 }26 27 }28 else{29 // 父進程30 while(1)31 {32 printf("father, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);33 sleep(1); 34 }35 }36 return 0;37 }
我們發現在開始時,輸出出來的變量值和地址是一模一樣的!
因為我們之前講過子進程按照父進程為模版,父子并沒有對變量進行進行任何修改
但是在達到一定條件之后,父子進程,輸出地址是一致的,但是變量內容不一樣!
但是相同的地址為什么會有不同的值?
- 所以我們能得出結論,我們之前看到的地址,絕對不是物理地址,我們平時用到的地址,其實都是虛擬地址/線性地址!
- 而虛擬地址就是進程地址空間的內容
2. 進程地址空間
我們現在來深入的了解一下為什么相同的的地址為什么會有不同的值?
首先引入一個概念:每一個進程運行之后,都會有一個進程地址空間的存在,在系統層面都要有自己的頁表映射結構!
因此:當一個進程先修改后,它就不再指向原來那塊物理空間,而是擁有一個新的物理空間!而頁表左邊的虛擬空間沒有發生改變,所以相同的的地址為什么會有不同的值,是因為映射的物理空間不同!
3. 什么是地址空間
在講什么是地址空間之前,我們先來講一個故事,來方便理解!
一個擁有10億美元身家的富豪,他有4個私生子,每個人都不知道彼此的存在,但是富豪對每個孩子都說,認真做好現在的事,在未來可以繼承自己的10個億家產。
但是在得到10個億之前,他的幾個孩子,在經濟上遇到了問題,前三個都要找富豪要10w美金來解決麻煩,富豪覺得合情合理也就給了,但是它的第四個孩子直接找他要10個億,富豪當然不能給他,然后講明原因后給了他20w美金。因此他的所有孩子都可以得到10億之內的經濟資助,但是絕對拿不到10個億。
在這個故事中:
- 操作系統:富豪
- 內存:10億美金
- 進程:私生子
- 虛擬地址空間:繼承10億的大餅
虛擬地址空間并不是真實的地址
3. 地址空間的管理
富豪給每一個私生子都畫了餅,他要把每個私生子都管理起來,也就是要把所有大餅管理起來。
因此:地址空間也要被OS管理起來!!每一個進程都要有地址空間,系統中,一定要對地址空間做管理!!
而操作系統管理地址空間,一定是“先描述,在組織”!地址空間最終一定是一個內核的數據結構對象,
就是一個內核結構體!
而我們觀察進程地址空間,發現里面是一堆的地址劃分。
在Linux中,這個描述虛擬地址空間的東西叫做:
struct mm _struct
{long code_start;long code_end;long data_start;long data_end;long heap_start;long heap end; //brklong stack _start;long stack _end;......
}
而該結構體的大小會被初始化成4gb,線性編程范圍從全0到全F,然后把線性范圍拆分成細小的范圍,這就是地址空間
4. 頁表
在上面我們了解到了頁表,頁表的映射關系中左側表示虛擬地址,右側表示物理地址,但是除了這兩個其實在頁表的映射關系中還存在一個標記字段——訪問權限字段
講到這里我們再回到字符常量區那里。
char *str = "Hello, Linux!";
*str = 'S';
此時我們就可以解釋通字符常量區為什么不能修改:
- 字符常量區在經過頁表映射時,訪問權限字段只設置成只讀的,所以在寫入時,頁表直接將我們攔住,不讓我們訪問,所以字符常量區不能修改,代碼區也是如此!
所以頁表可以進行安全評估,有效的進行進程訪問內存的安全檢查
在除去上面提到的東西以外,頁表還可以通過二進制衡量能存中有沒有內容,是否分配地址
當我們有個虛擬地址要被訪問了,但是它并沒有被分配空間,更不會有內容,那該則么辦呢?
其實在這個時候操作系統會將你的這個訪問暫停,然后進行一下操作:
- 操作系統會將你的可執行程序重新開辟空間
- 把對應可執行程序需要執行的這個虛擬地址對應的代碼加載到內存里
- 把對應的虛擬地址填充到頁表
- 把標志位改為1,代表已經分配地址,且內容已經填充
- 將暫停的代碼繼續訪問
操作過程也稱為缺頁中斷
而我們操作系統在進行這些工作時,是在進行內存管理, 而進程管理和內存管理因為有了地址空間的存在 ,實現了在操作系統層面上的模塊的解耦!
5. 為什么要存在地址空間
到了這里我想大家也都了解得差不多了,為什么要存在地址空間,原因有很多
一、 讓無序便有序
- 讓進程以統一的視角看待內存
- 在頁表層映射時會將不同的數據類型進行劃分使得映射到物理內存后是比較有序的一種狀態!
- 所以任意一個進程,可以通過地址空間+頁表可以將亂序的內存數據,變成有序,分門別類的規劃好!
二、存在虛擬地址空間,可以有效的進行進程訪問內存的安全檢查
三、將進程管理和內存管理進行解耦
四、保證進程的獨立性
通過頁表讓進程雖然虛擬地址一樣但是映射到不同的物理內存處,從而實現進程的獨立性
6. 總結拓展
拓展:
在mm_struct中還會存在一個struct vm_area_struct的結構 ,它能劃分出一個start,一個end。如果我們還想繼續劃分就會有多個struct vm_area_struct的結構,然后他們會構成一個線性劃分的鏈表結構。
struct vm_area_struct
{struct mm_struct * vm_mm;unsigned long vm_start;unsigned long vm_end;......
}
到這里我們的進程地址空間也接近尾聲了,地址空間讓進程管理和內存管理互不干涉,起到了很大作用。結束進程地址空間,我們的Linux進程概念到這里也結束了,后面我將帶大家走進進程控制。
謝謝大家支持本篇到這里就結束了