目录
V.用 虚拟内存(虚拟地址空间) 来 解释 刚才 fork()产生的 一个变量两个值 的问题
一、什么是虚拟地址空间 / 虚拟地址空间是如何被设计的
1.先看一下linux空间分布
I.示意图:
II.验证:
myhello:myhello.c
gcc -o myhello myhello.c
//linux小技能:
//myhello:hello1.c hello2.c hello3.c hello4.c
//gcc -o $@ $^
//可以把 hello1.c hello2.c hello3.c hello4.c一次性全部gcc 成可执行程序
.PHNOY: clean
clean:
rm hello1.c hello2.c hello3.c hello4.c
//myhello.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char* argv[], char* env[])
//int argc是命令行选项的个数,
//char *argv[]是用来存 该可执行程序的文件名和其对应的操作选项的,如ls -a -l
//char *env[]是用来接收环境变量的指针数组,因为环境变量基本是字符串,而且有多个,所以用指针数组存
{
// int a = 10;
//字面常量
const char* str = "helloworld";
// 10;
// 'a';
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
static int test = 10;
char* heap_mem = (char*)malloc(10);
char* heap_mem1 = (char*)malloc(10);
char* heap_mem2 = (char*)malloc(10);
char* heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
printf("test stack addr: %p\n", &test); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
printf("read only string addr: %p\n", str);
for (int i = 0; i < argc; 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;
}
ps:
1.堆是向上增长的
2.栈是向下增长的
2. 在已知Linux内存分布之后,我们来看一个奇怪的现象
I.代码 :
//makefile
mytest:test.c
gcc test.c -o mytest
.PHONY: clean
clean:
rm -f mytest
//test.c
1 #include <stdio.h>
2 #include <unistd.h>
3 int a=100;
4 int main()
5 {
6 pid_t pid =fork();
7 if(pid<0)
8 {
9 sleep(1);
10 while(1)
11 {
12 printf("error\n");
13
14 }
15 }
16 if(pid==0)
17 {
18
19 while(1)
20 {
21 sleep(1);
22 a=200;
E> 23 pintf("i am child,pid:%d,ppid:%d,&a:%p,a:%d\n",getpid(),getppid(),&a,a);
24 }
25 }
26 if(pid>0)
27 {
28 while(1)
29 {
30 sleep(1);
31 a=500;
32 printf("i am father,pid:%d,ppid:%d,&a:%p,a:%d\n",getpid(),getppid(),&a,a);
33 }
34 }
35 return 0;
36 }
II. 输出:
III.思考,引出--虚拟内存(虚拟地址空间):
疑问:为什么一个变量会有两个值呢?我们理解的物理内存上的同一个变量绝对不可能一个变量有两个值的。那么只有一个可能,这个变量并不是在我们所理解的物理内存上存储的。
答案 :是的,不仅是Linux上这个变量a,而且以往,我们所有在C/C++中所学的变量,基本都是不是存储在物理内存上的,而是存储在 虚拟内存 上。----所以刚才我们展示的Linux内存分布图,其实也是其 虚拟内存 的分布图
Linux 的虚拟内存的分布图
IV.虚拟内存(虚拟地址空间)
接下来浅浅看下图,了解 虚拟内存 大致的位置
图文解释:
1.TCB--进程结构体,负责存储进程的属性信息等;
2.struct address_room--虚拟地址空间内核结构体;
3.页表--用于映射的结构体
(以上三种是操作系统中常见的三种结构体,其中struct address_room 和 页表 共同形成一个 具有访问条件判断 的 映射)
过程:
TCB结构体在进入CPU的运行队列之前(在源程序编译阶段就已经存在了虚拟地址,因为编译阶段需要调用各种函数)就已经将对应的 虚拟地址 和 虚拟地址的的映射关系 存在了虚拟地址空间 、页表 、和 物理内存 中,形成了对应的映射,并且这种映射 是有访问的条件 判断的。每当要合法的访问某个变量时,系统都会去虚拟内存里找对应的映射关系去访问物理内存。
V.用 虚拟内存(虚拟地址空间) 来 解释 刚才 fork()产生的 一个变量两个值 的问题
需知
我们要知道的是,在操作系统中, 页表 和 虚拟地址空间 是不止一对的,因为进程很多,每个进程都可以有 页表 和虚拟地址空间。
初步解释
所以刚才的 由于fork();之后所产生的一个变量两个值的问题,其实是由于父进程和子进程都有自己的 页表 和 虚拟地址 空间的问题
在深入理解之前,我们先简单了解一下 写时拷贝
写时拷贝的作用/优点:
1.写时拷贝使得两个指向同一块空间的指针或迭代器或寄存器 得以彻底分离,保证了进程的独立性
2.是一周延时申请空间的技术,因为只有在真正需要用的时候才会去开辟空间,减少了开辟空间不使用的情况,所以--可以提高整机的使用率
深入解释
图文解释:
pid:1.因为fork() 结束之后,return 回的值给pid_t pid前,子进程的所有属性和信息都是仿照了父进程复制过去的,所以父进程中 a的虚拟地址 也和 子进程中 a的虚拟地址 是一样的,所以通过页表在 物理内存中的映射的a也是同一个;2.在fork()结束后,return回值给pid_t pid时,pid_t pid会因为写时拷贝,父进程中的pid和子进程中的pid会指向不同的物理内存,使得其对应的pid是独立的,也使得其对应的pid的值可以是不同的。
a: 同理,a在未被修改值之前,父进程和子进程的a的虚拟地址是相同的,也是指向同一块物理内存的;a在被修改值之后,父进程和子进程之后的a的虚拟地址虽然相同,但是子进程中的a已经因为写时拷贝的原因,重新开辟了一个空间了,使得父进程中和父进程中的a是独立的不同的两个a,所以其两个a的值也可以是不同的。
写时拷贝之后
OK,通过以上的 图解 和 例子,我们已经对虚拟地址空间有了较深的理解,
那么虚拟地址空间到底是什么呢?总结:是一种看待内存的方法,是一种数据结构。
那么虚拟地址空间是如何设计的呢? 总结: 将物理地址 在 虚拟地址空间中 描述起来,再用虚拟地址空间和页表来管理。
二、为什么要有地址空间
a. 当进程 非法的 访问或者映射, OS都会识别到并且终止该进程。
详解:比如所有的进程崩溃,就是进程的退出 ———》实质上是操作系统管理进程,将进程杀掉,让进程退出——》对用户的非法访问进行了有效的拦截,有效的保护了物理内存——》保护了物理内存中所有合法的数据-包括 各个进程,以及内核的相关有效数据(因为地址空间和页表是 OS 创建的,所以想使用虚拟地址空间和页表进行映射,也一定要在OS的监督之下来进行访问)
b.任意位置处,都可以用虚拟地址+页表解耦用来提高其他进程之间的独立性
详解:
1.任意位置处加载:我们的物理内存中 ,可以对未来的数据进行任意位置加载。只要映射能找到,物理内存的 就可以 进程管理 做到没有关系。(额外一个小知识:malloc在开辟空间时会预留“饼干空间”来储存开辟的空间的属性,映射就是通过“饼干空间”中的开辟内存的属性来进行对应的映射的)
2.解耦:因为虚拟地址+页表可以做到任意位置处加载,使得 内存管理模块 和进 程管理模块 无关联 就完成了解耦合。(解耦合-位置关联性降低,在映射的时候不需要对应的映射区域,减少模块和模块之间的关联性。耦合度越低,维护成本越低。和模块化函数之后方便维护是一个道理)
c.地址空间中有写时拷贝,可以延迟分配,用来提高整机效率
详解:
写时拷贝,延迟分配,可以做到,以足够内存的视角来执行进程,并且在使用时再开辟,减少了开辟了不及时使用,而增加系统内耗,浪费系统资源的可能。
d.可以让进程以统一的独占整个内存(实际上不是,只是以这种视角)的视角,映射到任意不同的物理内存的位置,来完成进程独立性的实现