首先我们先了解不同数据存储的位置
堆区:动态分配的内存,例如使用malloc或者new出来的对象
栈区:存储局部变量
静态区:存储全局变量或者静态变量
只读常量区(数据段):存储只读数据例如字符串常量
代码段:存储执行代码
可以通过以下代码验证
1 #include <stdio.h>
2 #include <stdlib.h>
3 int val_1;
4 int val_2 = 10;
5 int main()
6 {
7 printf("code=%p\n", main);
8 printf("uninited=%p\n",&val_1);
9 printf("inited=%p\n",&val_2);
10 static int sta_val = 10;
11 printf("static=%p\n", &sta_val);
12 const char* str_1 = "abc";
13 printf("const string=%p\n",str_1);
14 int a;
15 int b = 10;
16 int c = 20;
17 int* d = (int*)malloc(sizeof(int));
18 int* e = (int*)malloc(sizeof(int));
19 printf("stack=%p\n", &a);
20 printf("stack=%p\n", &b);
21 printf("stack=%p\n", &c);
22 printf("heap=%p\n", d);
23 printf("heap=%p\n", e);
24 return 0;
25 }
进程的地址是虚拟地址
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int val = 10;
6 int main()
7 {
8 pid_t id = fork();
9 if(id==0)
10 {
11 //子进程
12 int cnt = 5;
13 while(cnt--)
14 {
15 if(cnt == 2)
16 {
17 printf("val changed");
18 val = 20;
19 }
20 printf("child:%p\n",&val);
21 }
22 }
23 else{
24 //父进程
25 int cnt = 5;
26 while(cnt--)
27 {
28 printf("parent:%p\n",&val);
29 }
30 }
31 return 0;
32 }
可以看到,父子进程都使用了一个全局变量,但是当子进程修改变量值的时候,父子进程的变量地址都不变,可是一个地址不可能存放两个不同的变量,那么原因只有一个,进程的地址空间是虚拟出来的。
什么是进程(虚拟)地址空间
上面的画的简单的数据存储结构就是实际进程的地址空间结构,在Linux操作系统中定义了一个mm_struct结构体用来存储各个数据段的地址,例如:代码段地址可以用两个成员表示,code_start记录代码段的首地址,code_end记录代码段的末尾地址。这样就可以为存储的每个数据段都划分出一个空间。
进程的地址是虚拟的和物理地址有一种映射关系,这种关系被放到了页表这个结构中
为什么要有进程地址空间
1.让系统以统一的视角看待内存,例如,如果一个进程处于挂起状态,那么它的数据会先放到硬盘中。当再次加载到内存时,为它分配的物理地址肯定会发生变化,但是虚拟地址不会变。有了页表这个媒介,无需在意物理地址的变化。
2.保护物理内存,当我们要对数据修改时,会先在页表中查找,如果页表对应的r-w是只读,就会拦截防止对物理地址的异常操作。
以创建子进程为例,讲一下这个过程:
父子进程的数据和代码在一开始是共享的。子进程在创建时,页表也会创建,子进程页表的虚拟地址映射关系和父进程的一样。当我们对其中一个的数据进行修改时,数据对应的页表的r-w是r状态,系统检测到写时拷贝,会为该数据再建立一个映射关系,使得父子进程不相同的数据都能分别存储。
3.页表将进程管理块和内存管理块解耦合(每次程序运行,页表地址都会被加载到cr3寄存器中)。再次回顾一下进程的概念,程序对应的内核数据结构(task_struct,mm_struct,页表...)和代码数据;内存管理模块是由物理地址空间和页表等部分构成。为什么说解耦合呢? 我们都玩过50+G的游戏,目前一般电脑的内存都是16G/32G,内存怎么装的下的?
进程数据的加载是一种惰性加载,即不会一次性把整个程序的数据都加载到内存,只会先加载一部分,当进程需要数据时,通过页表查找,页表中还有一个记录数据是否被加载的标识,如果未加载就会触发系统的一个机制——缺页中断,将数据加载到内存。这样就做到的进程不需要过多关心内存的问题,内存也不需要关心进程。