🔥🔥 欢迎来到小林的博客!!
🛰️博客主页:✈️小林爱敲代码
🛰️博客专栏:✈️Linux之路
🛰️社区 :✈️ 进步学堂
🛰️欢迎关注:👍点赞🙌收藏✍️留言
文章目录
- 💖进程地址空间
- 💖进程地址空间是什么?
- 进程地址空间的划分
- 虚拟内存转换成物理内存
- 💖为什么要有进程地址空间?
💖进程地址空间
我们在学习C语言的时候,应该都知道这个内存空间图。
但其实我们对它并不了解,为什么呢?我们用一段代码来感受一下!
#include<stdio.h>
#include<unistd.h>
int g_val = 100;
int main()
{
int pid = fork(); //创建子进程
if(pid == 0)
{
//child
int count = 5;
while(count)
{
printf("i am child , g_val = %d, &g_val = %p\n",g_val,&g_val);
if(count == 3)
{
//修改数据
printf("******开始修改数据*******\n");
printf("i am child , g_val = %d, &g_val = %p\n",g_val,&g_val);
g_val = 200;
printf("******修改数据done*******\n");
}
count--;
sleep(1);
}
}
else if(pid > 0)
{
//parent
while(1)
{
printf("i am father , g_val = %d, &g_val = %p\n",g_val,&g_val);
sleep(1);
}
}
else
{
//erro
perror("fork:");
}
return 0;
}
我们这个代码的主体逻辑是,创建一个全局变量。然后再创建一个子进程,随后打印全局变量的值和地址,在子进程特定的时候修改这个全局变量。
那么我们来看看运行结果吧!
我们可以发现,g_val的值被修改了! 但是它们的地址还是一样的。这是怎么回事!???
我们都知道,父进程和子进程如果没有发生数据修改,那么会共用同一份数据。如果有一方的数据发生了修改,那么就会写实拷贝一份。所以此时的父进程和子进程各有一份属于自己的数据,既然有2份数据那么就说明有2个g_val。2个独立进程的g_val变量用了同一块内存空间,这合理吗?完全不合理!!!2个进程用同一块空间,这不就起冲突了。这是为什么呢??
再探索这个问题之前,先给大家讲个小故事,方便大家理解。
在美国有一个大富翁,他有三个私生子,这三个私生子互相不认识。而这个大富翁有100亿美金。
然后这个时候,大富翁对私生子小A说:等我老了,就由你来继承我100亿财产吧。这时小A就以为这100亿是他的了。然后大富翁又对私生子小B说:等我老了,你来继承我的100亿吧。 小B听了高兴坏了,也以为这100亿是他的了。然后大富翁又对私生子小C说了同样的话。 所以,大富翁给他的所有私生子都花了一张大饼。告诉他们,他们未来都会继承这100亿。所以大富翁的私生子,都认为自己有100亿可以花,就可以按照这100亿来为自己分配生活。
而这里面的大富翁,就是操作系统,私生子就是进程,大饼就是进程地址空间,那么这100亿美金,就是我们的物理内存。而我在之前的篇章里说过,进程的本质其实就是 描述进程的结构体(PCB)+代码数据。那么进程地址空间是否在PCB里呢?答案当然是的。也就是说每个进程都会有一个进程地址空间,每个私生子都认为自己独占了大富翁的100亿美金,所以每个进程都认为自己独占了物理内存。 所以,我们的**进程地址空间,也被我们称之为虚拟内存。**那么为什么会打印相同地址?我们先了解一些东西,再最侯为大家总结结论。
💖进程地址空间是什么?
那么进程地址空间是什么呢?地址空间本质是内核中的一种数据类型,在Linux内核中,它是一个struct mm_struct
的结构体。
也就说,我们程序的内存划分,本质上是一个区域!!
进程地址空间的划分
那么进程地址空间是怎么划分的?
打个比方:
假如你现在是一名小学生,你的同桌是一名爱干净的小女孩。而你一名爱流鼻涕不讲卫生的小男孩,这时侯,你的同桌嫌弃你。假设你俩的桌子长100cm,此时你的同桌在50cm的地方画了一根三八线,跟你划清界限。那么此时你还能不能把东西放到你同桌所在的区域?当时是不能了!假设你的区域是 0 - 50cm的地方,那么你的东西只能放在0-50区间。假设这时候有一把尺子,你想把你的橡皮擦放在第38cm的地方,于是你就拿尺子量出了38cm,把橡皮放在这个位置上。这里面呢,你和你同桌,充当的是一块区域,而这把尺子,是进程地址空间,橡皮擦,则是你的数据。你要把数据放在指定的地方,那么就需要进程地址空间充当尺子。为什么你知道桌子是100cm?因为有尺子,所以你才知道桌子是100cm。所以你要划分区域,也需要进程地址空间充当尺子来划分区域。
struct mm_struct
{
unsigned int code_start; //代码段起始地址
unsigned int code_end; //代码段结束地址
unsigned int init_data_start;//初始化变量区起始地址.
unsigned int init_data_end;//初始化变量区结束地址
unsigned int uninit_data_start;//未初始化变量区起始地址.
unsigned int uninit_data_end;//未初始化变量区结束地址
unsigned int heap_start;//堆区起始地址.
unsigned int heap_end;//堆区结束地址
.....
unsigned int stack_start;//堆区起始地址.
unsigned int stack_end;//堆区结束地址
}
每个进程都认为地址空间的划分是按照4GB的空间划分的,而地址空间上进行区域划分的位置,是虚拟地址!虽然这里只要start和end,但是每个进程都可以认为mm_struct 代表整个内存,且所有的地址为0x00000000 -> 0xFFFFFFFF。
虚拟内存转换成物理内存
既然每个进程都有一块地址空间,而程序里面的数据和代码都是根据进程地址空间存放。那么我们的系统调用它时是如何为它分配地址的呢?如何把对应的数据放到物理内存的呢?
那是因为物理内存和虚拟内存之间,有一张页表。
而页表的本质就是哈希表。通过虚拟内存来映射物理内存。也就说,每一个进程地址空间,都会有一张对应的页表。意思就是每一个进程都会有一张页表,通过页表的虚拟地址,就可以找到对应的物理内存。从而操作系统对物理内存进行操作。
💖为什么要有进程地址空间?
1. 通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质的目的是为了,保护物理内存以及各个进程的数据安全。
比如:
先给大家放一段代码。
int main()
{
const char* str = "hello world";
str = "HW";
return 0;
}
这段代码会报错,为什么呢?因为 str它所处的内存空间是常量区。通过虚拟内存映射到真实的物理地址之后,它的权限是只读权限。当你修改它时,因为你不具备写权限,所以操作系统会直接把你干掉。这也是为什么要有进程地址空间的原因。如果没有进程地址空间,那么就无法进行权限管理,那么即使是常量也可以被修改!这是非常严重的!而有了进程地址空间之后,你能不能修改,全部取决于操作系统让不让你修改!
2. 将内存申请和内存使用的概念在时间上划分清除,通过虚拟地址空间,来屏蔽底层申请的过程,达到进程读写内存和OS进行内存管理操作,进行软件上面的分离!
先抛出一个问题:假如我们申请5000个字节,我们立马能使用这5000字节吗??
答案是:不一定,可能会存在暂时不会全部使用,甚至暂时不使用的情况。
因为,在OS(操作系统)的角度上,如果空间立马就给你的话,是不是就意味着,整个系统会有一部分空间,本来可以先给其他进程立马使用,现在却被你闲置着?说简单点就是这个进程现在正茅坑,但丝毫没有要拉屎的意思。这种做法是人人恨之的,所以操作系统不一定会立马给你使用。
打个比方:比如你要开学了,你和你老爹要8000块钱的学费。但是你还有一星期才开学呢,于是你老爹说:好,我知道了,开学前一天给你。 你像你老爸要了8000块钱,你老爸对应给你了。这就就相当于你申请了8000字节的空间。但是你还有一星期才开学,也就是你这8000块钱暂时用不上,你这8000字节也暂时用不上。所以你爸说等你开学前一天的时候给你,而操作系统也在进程要使用的时候,给进程真实的物理内存。
而这和我们的写实拷贝非常的像,数据不改变就共用同一份数据,改变就拷贝一份。
3.站在CPU和应用层的角度,进程统一可以看做统一使用4GB空间,而且每个空间区域的相对位置,是比较确定的!OS最终这样设计的目的,达到了一个目标:每个进程都认为自己是独占系统资源的!进程具有独立性的!
这种情况也就是我们开头演示的那样,为什么2个进程的g_val的地址是相同,而值是不同的。这是因为**子进程创建是以父进程为模板创建的,所以子进程也会继承父进程的页表。**在子进程没有对g_val的值进行修改时,父子进程共享一份数据。而一旦子进程对g_val的值进行修改,那么在OS会对g_val的数据进行一份拷贝(写实拷贝)。且让子进程页表映射到g_val的值映射至新拷贝后的物理地址。这样子,即使它们的g_val的地址是相同的,但是在它们在页表 g_val的数据 是映射到不同的物理地址。
最后,为什么下面str1和str2的地址是相等的?
#include<stdio.h>
int main()
{
const char* str1 = "hello world";
const char* str2 = "hello world";
printf("str1 的地址是: %p",str1);
printf("str2 的地址是: %p",str2);
}
如上代码,我们会发现str1和str2的地址是一样的,可是它们不是同一个变量啊,为什么?因为str1和str2都在常量区。也就是说操作系统只给了这个区域可读权限,所以操作系统认为,对于只有可读的数据,操作系统只需要维护一份即可。