Linux进程地址空间初谈
文章目录
- 一、C语言地址与进程地址空间的关联
- 二、初谈页表
- 三、剖析地址空间
- 3.1地址空间究竟是什么
- 3.1.1什么叫作地址空间
- 3.1.2如何理解地址空间上的区域划分
- 3.1.3结论
- 3.2为什么要有进程,以及进程地址空间
- 四、二谈页表
- 4.1CR3寄存器
- 4.2读写权限
- 4.3缺页中断
- 4.4小结
- 五、结语
一、C语言地址与进程地址空间的关联
说到进程地址空间就不得不谈在C语言学习过程中学习到有关地址的知识,就不得不联想出来一系列的问题
C语言所学的地址与内存有关系吗
C语言所得到的地址是真实的地址吗
1 #include <stdio.h>
2 #include <stdlib.h>
W> 3 using namespace std;
4 int addr1;
5 int addr2=100;
6 int main()
7 {
8 printf("代码区:%p\n",main);
9 const char* addr3="hello world";10 printf("字符常量区:%p\n",addr3);11 printf("已初始化全局变量:%p\n",&addr2);12 printf("未初始化全局变量:%p\n",&addr1); 13 char* addr4=(char*)malloc(sizeof(int));14 printf("堆区:%p\n",addr4);15 printf("栈区:%p\n",&addr3);16 return 0;17 }
C语言当中地址空间是由低地址向高地址进行排列,栈区与堆区中间间隔着一段距离
1 #include <stdio.h>
2 #include <stdlib.h>3 using namespace std;
4 int main()
5 {
6 int a=1;
7 int b=2;
8 int c=3;
9 10 printf("栈区:%p\n",&a);11 printf("栈区:%p\n",&b);12 printf("栈区:%p\n",&c);13 char* a1=(char*)malloc(sizeof(int));14 char* a2=(char*)malloc(sizeof(int));15 char* a3=(char*)malloc(sizeof(int));16 printf("堆区:%p\n",a1);17 printf("堆区:%p\n",a2);18 printf("堆区:%p\n",a3);19 return 0;20 }
可以得到栈区的地址是向下排列,而堆区的地址是向上分布
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>4 using namespace std;
5 int val=100;
6 int main()
7 {
8 pid_t id=fork();
9 if(id==0)10 {11 while(1)12 {13 printf("i am child,pid :%d,ppid : %d,val:%d,&val:%p\n",getpid(),getppid(),val,&val);14 sleep(1);15 }16 }17 else if(id>0)18 {19 while(1)20 {21 printf("i am father,pid :%d,ppid : %d,val:%d,&val:%p\n",getpid(),getppid(),val,&val);22 sleep(1); 23 }24 return 0;25 }26 }
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>4 using namespace std;
5 int val=100;
6 int main()
7 {
8 pid_t id=fork();
9 if(id==0)10 {11 int cnt=2;12 while(1)13 {14 if(cnt) cnt--;15 else val=200;16 printf("i am child,pid :%d,ppid : %d,val:%d,&val:%p\n",getpid(),getppid(),val,&val);17 18 sleep(1);19 }20 }21 else if(id>0)22 {23 while(1)24 {25 printf("i am father,pid :%d,ppid : %d,val:%d,&val:%p\n",getpid(),getppid(),val,&val);26 sleep(1);27 }28 return 0;29 }30 }
同样的地址,同样的变量,却出现不同的值,由进程的独立性我们可以得知,子进程要修改值的时候会发生写时拷贝,所以必不可能在内存上是同一份地址
结论:如果变量的地址,是物理地址,不可能存在上面的现象,绝对不是物理地址,变量的地址是线性地址/虚拟地址
如果直接进行在pcb里面存入物理内存的地址,那么就要对物理地址做管理,倘若我们代码和数据进入挂起状态,物理内存的地址又要变了
因为有地址空间的存在
虚拟地址可以填,物理地址不一定填
二、初谈页表
页表是一种kv式的映射关系
页表是在虚拟地址和物理地址建立映射关系。
重点:虚拟地址填到页表的左侧,而操作系统会在物理内存开辟一段空间,将所谓的物理地址填写到页表的右侧。当进程访问这段地址的时候,操作系统会将虚拟地址转化为物理地址。
创建子进程的时候,子进程要创建自己的PCB内核数据结构、进程地址空间和页表。子进程的地址空间是从父进程那里继承下来的,可以理解为子进程将父进程页表拷贝了一份,所以子进程页表的虚拟地址也指向和父进程一样的物理地址。
上面所述的val变量,由于子进程拷贝了一份父进程的地址空间,所以val的虚拟地址子进程也能和父进程一样指向相同的物理地址,重点:所以父子进程此时完成数据共享、代码也共享的情况。
当修改子进程的val值时,操作系统识别到要对val的虚拟地址进行写入,写入时去找对应的物理地址,操作系统识别到这个数据父子进程是共享的,重点:所以操作系统在物理内存上重新开辟一段空间,将新空间的物理地址写入到子进程的页表当中重新建立映射关系,所以这个数据就在物理内存当中分离了(先经过写时拷贝,这是由操作系统自动完成的)。
重新开辟空间,但是在这个过程中,左侧的虚拟地址是0感知的,不关心,不会影响它。
重点:所以数据不同的根本原因就是,父子进程都查自己的页表,访问相同的虚拟地址,但是页表建立与不同物理地址的映射关系。
三、剖析地址空间
3.1地址空间究竟是什么
3.1.1什么叫作地址空间
地址空间是进程对内存进行划分的宏观视角
当进程访问地址空间的前提条件是,进程正在被cpu调度**,cpu根据地址总线访问我们的物理地址**
在32位计算机中,有32位的地址和数据总线
每一根地址总线通过对内存充放电来表示01,高电频表示1低电频表示0,所以构成了32位的地址
一个硬盘向内存中写入的过程其实就是充放电的过程
32根地址总线每一根都有01两种态,所以32根就是2^32种组合
内存的大小就是2^32*1bite=4GB
结论:你的地址总线排列组合形成地址空间范围[0,2^32],地址空间就是大概的一段数据范围
3.1.2如何理解地址空间上的区域划分
有100亩的土地,老王和老张各划分了50亩地,所谓的区域划分就是在一段地址空间上,划分好各个所属地址的起始和结束位置
所谓的空间区域调整变大或者变小如何理解
有一天老王屡次把地种到属于老张的田地里面去,老张就跟老王说你这10亩地归我了,这就是你的报应,
本质就是对start和end的变大和变小
在范围内,连续的空间中,每一个最小单位都可以有地址,这个地址可以被直接使用
比如老王很有规划,第一亩地种花生,第二亩地种南瓜,在属于老王的50亩地里面地址可以被直接使用
3.1.3结论
所谓的进程地址空间,本质是一个描述进程可视范围的大小,地址空间内一定要存在各种区域划分,对线性地址进行start和end即可
因为每个进程都有所谓的进程地址空间,所以操作系统势必要像对创建pcb一样对地址空间做管理
操作系统为了管理地址空间创建了mm_struct结构体,在里面像老王和老张等多人划定了自己的起始和结束,比如代码区,字符常量区等
3.2为什么要有进程,以及进程地址空间
操作系统就相当于一个大富翁,他在外面有很多的孩子,每一个孩子(进程)都是独立的,不知道其他的存在,每一个孩子都认为自己会继承父亲的4GB大遗产,当进程向操作系统申请空间的时候,小空间操作系统就直接给你了,太大甚至全部空间操作系统不给你但是进程仍然认为自己有4GB大遗产可以拿到
因为每个进程相互独立,都认为自己有4GB的内存,比如说父进程从0x112233开始规划地址空间,子进程也可以从0x112233开始规划自己的地址空间,因为互不干扰想怎么规划就怎么规划
结论:
1.让进程以统一的视角看待内存
2.增加进程给虚拟地址空间可以让我们访问内存的时候,增加一个转换的过程,在这个转化的过程中,可以对我们的寻址请求进行审查,所以一旦异常访问,直接拦截,该请求不会到达物理内存,保护物理内存
(比如进程直接访问物理内存就可能越界或者说修改其他文件的数据,有了进程地址空间加页表这一关就可以对这条比如说对字符常量做修改的请求直接拦截,不给你达到物理内存)
页表可以很好的提供权限管理
四、二谈页表
4.1CR3寄存器
页表的起始地址(物理地址)存放在CR3寄存器当中,页表地址本质上属于进程硬件的上下文,所以在进程切换的时候页表地址不会随着进程切换而改变
4.2读写权限
字符常量区等数据不能被修改势必是操作系统为变量加上了一层权限
页表当中存放着变量的读写权限,当只有只读权限的时候,操作系统通过CR3寄存器找到页表的地址,尝试向该位置写入就会直接拦截你,进行非法操作就会直接干掉你这个进程
所以代码是只读的,字符常量区是只读的,都是页表加了一层只读权限
4.3缺页中断
大文件从磁盘加载到内存是分批加载的,是惰性加载的(比如说我们加载了500M的空间可是短期之你使用的只有5M空间,CPU也一下子跑不完那么多),惰性加载就是用多少给你多少,内存的使用率就变高了
所以页表中有一个标记位,表示对应的代码和数据已经被加载到内存,当我们正在访问对应的虚拟地址,先看标记位如果是1那就是已经被加载,直接读取物理地址
如果标记位是0,就会触发缺页中断,在物理空间上开辟一段空间,将磁盘上的数据加载到物理空间,填写物理地址,恢复你要的访问,就可以得到你想要的数据了
进程是可以被挂起的,通过这个标记位就可以知道进程的代码和数据是否已经被加载到内存了
4.4小结
正是因为有了页表的存在,进程不需要关心内存的加载和释放,比如数据设置为只读权限,进程要访问这一块数据的虚拟地址的时候,发现数据本来就是可写的为什么设置成只读的,这个时候就会触发缺页中断,缺页中断是操作系统自动完成的,操作系统就会再内存开辟空间填写物理位置,达到了进程管理和内存管理的解耦
进程=内核数据结构(task_struct&&mm_struct&&页表+程序的代码和数据)
每个进程的代码和数据的虚拟地址可以完全一样,但只要指向不同的物理地址,每个进程的代码和数据就会相互解耦了,所以进程出现异常了,释放掉自己的代码和数据也不会影响到其他进程
进程把代码和数据逻辑上放到进程地址空间,转化到物理内存的任意地方(比如你选了一个学校随机抽了几十位同学,但是你只要按自己的方法给他们按上1234,就方便管理起来了),这就叫作无序变有序
五、结语
更深层次的认识进程地址空间在动态库和线程的时候会有进一步的讲解