目录
一.空间分布图解析
我们来通过编写代码来一步步验证这些区域的规律~
//makefile myenv:myenv.c gcc -o $@ $^ #-std=c99 .PHONY:clean clean: rm -f myenv
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int g_unval; int g_val = 100; int main(int argc, char *argv[], char *env[]) { printf("code addr: %p\n", main); printf("init data addr: %p\n", &g_val); printf("uninit data addr: %p\n", &g_unval); char *heap = (char*)malloc(20); char *heap1 = (char*)malloc(20); char *heap2 = (char*)malloc(20); char *heap3 = (char*)malloc(20); static int c; printf("heap addr: %p\n", heap); printf("heap1 addr: %p\n", heap1); printf("heap2 addr: %p\n", heap2); printf("heap3 addr: %p\n", heap3); printf("stack addr: %p\n", &heap); printf("stack addr: %p\n", &heap1); printf("stack addr: %p\n", &heap2); printf("stack addr: %p\n", &heap3); printf("c addr: %p, c: %d\n", &c, c); for(int i = 0; argv[i]; i++) { printf("&argv[%d]=%p\n", i, argv+i); } for(int i = 0; env[i]; i++) { printf("&env[%d]=%p\n", i, env+i); } return 0; }
- 首先正文代码部分应该是地址最小的:code 后面依次往上排就是
- 初始化数据:init 与 c
- 未初始化数据:uninit
- 堆:heap
- 栈:stack
- 命令行参数环境变量:argv env(这里还有待商榷)
这里我们取的只是两个表内的指针,它们指向字符串,并不是取到字符串的地址~而是指针的地址。
所以我们修改一下去了解字符串都是存在哪的~ 解引用里面的指针地址~
最后我们发现仍满足图中的分布条件~
那么问题来了~该表是否就是我们常说的内存表呢?我们用一个小测试来验证一下~
int g_val = 100; int main() { pid_t id = fork(); if(id == 0) { int cnt = 0; //子进程 while(1) { printf("child, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val); sleep(1); cnt++; if(cnt == 5) { g_val = 200; printf("child change g_val: 100->200\n"); } } } else { while(1) { printf("father, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val); sleep(1); } } return 0; }
一开始父进程与子进程都是共享代码,地址也是一样的。后面子进程修改数据,为了保证进程之间的独立性利用了写时拷贝,这样使得父进程保留原来的值,而子进程成功修改~
但最大的问题是为什么内容不一样,地址却还是一样的?
因为这个不是物理地址,并不会存放到内存中。而是虚拟地址(线性地址)。
而这张图也不是物理内存图,而是进程地址空间~
二.进程地址空间
我们先来解析一下流程~
首先每个进程都会有对于的地址空间,而地址空间存放在PCB中,那么既然地址空间里面全部都是虚拟地址,那物理地址又去哪里了呢?
一般都是去物理内存中开辟空间放入数据,然后再把物理地址交给页表,让页表建立虚拟地址与物理地址的映射~
子进程会继承父进程中的虚拟地址,但是会映射出与父进程不同的物理地址,然后在新的物理地址中拷贝g_val的内容并修改它~
之所以会出现不同的值但为同一个地址的原因就是因为该地址是虚拟地址,真正的物理地址二者是不同的!
2.1 什么是地址空间
接下来我们会通过类比来描述出地址空间~
一个身怀10亿的大富翁给四个孩子画饼,承诺只有他们四个好好干好自己的事业那么总有一天就能继承10个亿的家产~而这个过程只是大富豪给他们画的大饼,实际上什么都不会给他们~
这些大饼里面有0-10亿可以被他们在将来调度~
在我们生活中画大饼也是处处都有,那么问题来了~这些大饼需要管理吗?——当然需要,这样才能有各种各样的大饼~
在操作系统中是需要管理大饼的,那么怎么管理呢?——先描述,再组织~
所以本质是进程空间就是数据结构~
那么进程地址空间里面有什么属性呢?
下面我们用另一则类比为大家描述~
所以我们对描述地址空间的属性可以类似这样~
所以我们划分这些区域的本质是为了让区域内的这些地址都可以使用~
而关于转化工作是交给cpu中的寄存器cr3来做,它会指向页表(存储页表的物理地址),还有寻找对应物理地址等等操作大部分都是由寄存器去完成的~
最后我们把这整个过程分为两个模块,一个为内存管理模块,不需要去考虑进程管理,只需要页表能映射出物理空间即可,物理内存的存放不受进程所管理。另一个为进程管理,只需要管理好地址空间即可~
2.2 为什么要有地址空间+页表
- 将内存以无序变有序,让进程以统一的视角看内存
- 将进程管理和内存管理进行解耦合
二者各司其职,更好地保证内存的安全~
那么为什么不直接找物理内存?而是要通过页表转化呢?
这叫好比你想要拿压岁钱去小卖铺卖零食吃,但是你妈妈会跳出来要求管理你的压岁钱,免得你乱花~
而页表也是同样的情况,要去拦截非法操作~
所以我们申请内存并不是在申请物理地址,而是虚拟地址。那么申请后也不会去立马映射出物理地址,因为os需要为效率负责,它并不清楚你什么时候会往里面写入数据~所以就有了缺页中断~
只有当我们合理合法写入并且页表无映射时,os就会先暂停你的写入转而到物理内存开辟空间并让页表映射出物理地址~充分保证内存的使用率,不会空转~