目录
一、验证地址空间排布
在32位下,一个进程的地址空间取值范围0x00000000~0xFFFFFFFF,其中[0,3GB]属于用户空间,[3GB,4GB]属于内核空间
以下验证代码只在Linux下有效,在Window下会跑出不同的结果
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4 int g_unval;
5 int g_val=100;
6
7 int main(int argc,char* argv[],char* env[])
8 {
9 //代码区 已初始化全局区 未初始化全局区
10 printf("code addr: %p\n",main);
11 printf("init global addr: %p\n",&g_val);
12 printf("init unglobal addr: %p\n",&g_unval);
13
14 char* str="hello world";
15 printf("read only string addr: %p\n",str);
16 //堆栈地址
17 static int test=10;
18 //static的意义 把局部变量转变成全局变量
19 //在函数内定义一个局部变量 被static修饰
20 //在编译器看来已经把static变量编译进了全局数据区
21 //该变量在函数的约束上只在本函数内可用
22 char* heap_mem=(char*)malloc(16);
23 char* heap_mem1=(char*)malloc(16);
24 char* heap_mem2=(char*)malloc(16);
25 char* heap_mem3=(char*)malloc(16);
26
27 printf("heap addr: %p\n",heap_mem);
28 printf("heap addr: %p\n",heap_mem1);
29 printf("heap addr: %p\n",heap_mem2);
30 printf("heap addr: %p\n",heap_mem3);
31
32 printf("static test stack addr: %p\n",&test);
33 printf("stack addr: %p\n",&heap_mem);
34 printf("stack addr: %p\n",&heap_mem1);
35 printf("stack addr: %p\n",&heap_mem2);
36 printf("stack addr: %p\n",&heap_mem3);
37
38 //命令行参数地址 环境变量地址
39 int i=0;
40 for(i=0;i<argc;i++)
41 {
42 printf("argv[%d]: %p\n",i,argv[i]);
43 }
44
45 for(i=0;env[i];i++)
46 {
47 printf("env[%d]: %p\n",i,env[i]);
48 }
49 }
//运行结果
code addr: 0x40057d
init global addr: 0x60103c
init unglobal addr: 0x601048
read only string addr: 0x40085d
heap addr: 0x12ca010
heap addr: 0x12ca030
heap addr: 0x12ca050
heap addr: 0x12ca070
static test stack addr: 0x601040
stack addr: 0x7fff481e1a48
stack addr: 0x7fff481e1a40
stack addr: 0x7fff481e1a38
stack addr: 0x7fff481e1a30
argv[0]: 0x7fff481e277e
env[0]: 0x7fff481e2787
env[1]: 0x7fff481e279d
env[2]: 0x7fff481e27b5
env[3]: 0x7fff481e27c0
env[4]: 0x7fff481e27d0
env[5]: 0x7fff481e27de
env[6]: 0x7fff481e2800
env[7]: 0x7fff481e2813
env[8]: 0x7fff481e281b
env[9]: 0x7fff481e285d
env[10]: 0x7fff481e2df9
env[11]: 0x7fff481e2e11
env[12]: 0x7fff481e2e69
env[13]: 0x7fff481e2e9d
env[14]: 0x7fff481e2eae
env[15]: 0x7fff481e2eb6
env[16]: 0x7fff481e2ec4
env[17]: 0x7fff481e2ecf
env[18]: 0x7fff481e2eff
env[19]: 0x7fff481e2f22
env[20]: 0x7fff481e2f94
env[21]: 0x7fff481e2fb3
env[22]: 0x7fff481e2fc9
env[23]: 0x7fff481e2fd4
二、什么是地址空间?
地址空间本质上是OS为进程虚拟抽象出来的一种看待内存和外设的一种方案,让进程PCB指向自己的地址空间数据结构,结合页表来完成到物理内存的映射。虚拟地址是Linux配合硬件,软硬件结合的方案,创造出的地址空间的概念,以统一的视角来看到不同的设备,地址空间是进程地址空间,地址空间是一种内核数据结构struct mm_struct{},它里面有各个区域的划分,它来描述进程所看到的各个区域,进程里面会有结构体指针指向mm_struct{}
struct addr_room
{
int code_start;
int code_end;
int init_start;
int init_end;
int uninit_start;
int uninit_end;
int heap_start;
int heap_end;
int stack_start;
int stack_end;
int arg_start;
int arg_end;
int env_start;
int env_end;
//...
//其它属性
}
三、为什么要有地址空间?
1、防止非法映射
凡是非法的访问或者映射,OS都会识别到,并终止你这个进程,保护了物理内存
因为内存本身是随时可以被读写的。如果内存中有几个进程在运行,进程1如果出现野指针,可能把进程3的代码和数据进行改掉了,导致运行结果不正确或者进程崩溃,所以直接使用物理内存级的访问会特别不安全。
因此不能直接使用物理地址,现代计算机,提出了以下方式,每个进程有自己的PCB(进程控制块),操作系统会给每个进程创建一个虚拟地址空间,编址是全0到全F的,上面的地址为虚拟地址,并且要构建一个映射机制(页表),凡是要访问物理内存要进行映射(虚拟地址->物理地址),如果虚拟地址是非法地址,可以不把非法地址映射到物理内存,从而的保护了物理内存。因为有虚拟地址空间和页表的存在,可以对用户的非法访问进行拦截。
因为地址空间和页表是OS创建并维护的,也就意味着凡是想使用地址空间和页表进行映射,也一定要在OS的监管之下来进行访问,也便保护了物理内存中的所有合法数据,包括各个进程以及内核相关的数据结构。
2、完成模块之间的解耦
地址空间的存在可以完成内存管理模块和进程管理模块的解耦合以及实现延迟分配策略
因为有地址空间的存在,因为有页表的映射的存在,我们物理内存中,是不是可以对未来的数据进行任意位置的加载?
可以的,物理内存的分配就可以和进程管理做到没有关联关系,完成了内存管理模块和进程管理模块的解耦合!(解耦:减少模块和模块之间的关联性)
因为有地址空间的存在,所以上层申请空间,其实是在地址空间上申请的,物理内存甚至可以一个字节都不分配,而当真正对物理地址空间访问的时候,才会指向内存相关的管理算法,申请相关内存和构建映射关系,然后,再让你进行内存的访问,以上是OS自动完成,用户和进程零感知。
在物理内存分配的时候可以采用延时分配的策略,来提高整机的策略,提高了内存有效使用。
3、实现进程的独立性
进程虚拟地址空间+页表的方式可以实现进程的独立性
因为在物理内存中理论上可以任意位置加载,那么是不是物理内存中的几乎所有的数据和内存在内存中是乱序的?
因为有页表的存在,它可以将地址空间上的虚拟地址空间和物理地址进行映射,那么在进程视角所有的内存分布,都是可以有序的
地址空间+页表的存在可以将内存分布整体有序化,可以让进程有序化的看待物理地址空间
结合进程要访问的物理内存中的数据和代码,可能目前并没有在物理内存中,同样的,也可以让不同的进程映射到不同的物理内存,便很容易做到进程独立性的实现。
因为有地址空间的存在,每个进程都认为自己拥有4GB空间,并且各个区域是有序的,进而可以通过页表映射到不同区域,来实现进程的独立性。
四、解释写时拷贝
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4 #include<unistd.h>
5
6
7 //验证写时拷贝
8 //子进程运行五秒后更改g_val的值
9 //查看父子进程g_val的值以及g_val对应的地址
10 int g_val=100;
11
12 int main()
13 {
14 pid_t id=fork();
15 if(id==0)
16 {
17 int cnt=0;
18 while(1)
19 {
20 printf("I am child,pid: %d, ppid:%d, g_val: %d, &g_val: %d\n",\
21 getpid(),getppid(),g_val,&g_val);
22 sleep(1);
23 cnt++;
24 if(cnt==5)
25 {
26 g_val=200;
27 printf("child change g_val 100 -> 200 sucess\n");
28 }
29 }
30 }
31 else
32 {
33 //Father
34
35 while(1)
36 {
37 printf("I am Father,pid: %d, ppid: %d, g_val: %d, &g_val: %d\n",\
38 getpid(),getppid(),g_val,&g_val);
39 sleep(1);
40 }
41 }
42 }
原因:父子进程被创建,子进程会继承父进程的很多属性,大部分东西和父进程,其中就包括地址空间和页表,以及映射关系也一样,子进程对私有化的数据更改,大部分属性和父进程是一样的,父子进程g_val的虚拟地址指向的是同一个变量,当子进程尝试进行修改,为了要保证进程的独立性,当OS系统识别到当前子进程想通过页表找到g_val,并且修改g_val的时候,OS会重新开辟一个空间,把g_val的拷贝进新的内存空间,并且通过页表重新构建映射关系。因此就出现了父子进程的虚拟地址是一样的,但是经过页表映射到了物理内存的不同区域,父子进程看到的值便不一样了。
这种策略为写时拷贝,如果创建出的子进程,大部分内容和父进程一样,当子进程尝试对两者指向的同一块变量修改时,会重新给该变量开辟空间,重新构建映射关系。