目录
前言:
1. 程序地址空间回顾
2. 进程空间是什么
3. 进程地址空间与内存
4. 进程地址空间和内存的关联
5. 为什么要有进程地址空间
前言:
我们在平时学习的过程当中总是听到栈、堆、代码段等等储存空间,但是这些东西到底是什么,我们又怎么理解呢?本篇就为大家解惑。
1. 程序地址空间回顾
相信大伙们对于这张图片还是很熟悉吧,特别是对于堆和栈这一片区域,堆向上延伸,栈向下扩展,基本上是共同使用这一片空间。这片空间这样使用主要是服务于我们的动态库。
如果大伙想要验证这一张图片的正确性,可以自己验证一下,低地址代码地址长度更小,高地址更大。代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int global_uval; //全局变量未初始化
int global_val = 1; //全局变量初始化
int main(int argc, char* argv[],char* envp[])
{
const char* point = "hello world"; //栈内指针和常量区字符串
char* heap = (char*)malloc(100); //栈内指针和堆区空间
printf("代码区起始地址: %p\n", main);
printf("常量区字符串: %p\n", point);
printf("全局变量初始化: %p\n", &global_val);
printf("全局变量未初始化: %p\n", &global_uval);
printf("堆区地址: %p\n", heap);
printf("栈区地址: %p\n", &point);
printf("命令行参数: %p\n", argv[0]);
printf("环境变量: %p\n", envp[0]);
return 0;
}
Linux下可以看到地址空间的变化逐渐增加,也正好对应了我们的地址空间的图。
但是同样的代码在vs下却运行出了不同的结果,如下:
仔细看vs下运行堆区地址和栈区地址大小与Linux下运行相反,具体原因我也不是很清楚,博主在这里也疑惑了半天,估计是vs悄悄的改了一些实现方法。
2. 进程空间是什么
看了上面的解释也没有理解,啥是进程地址空间?博主知道你很急,但是你先别急,什么?非要看?好吧,请看下方结论:
进程地址空间不是真实物理地址而是虚拟地址
知道了吗?没看懂是不?这就对了,还是等我徐徐道来。
请先看下方代码:
利用fork创建了一个子进程,count用于计数,当循环走过两次时,a被修改为100
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7 int a = 100;
8 int count = 0;
9 pid_t res = fork(); //创建一个子进程
10
11 while(1)
12 {
13 if(res == 0)
14 {
15 printf("我是一个子进程,a = %d, &a = %p\n",a,&a);
16 sleep(1);
17 }
18 else{
19 if(count == 2)
20 a = 200;
21 printf("我是一个父进程,a = %d, &a = %p\n",a,&a);
22 count++;
23 sleep(1);
24
25 }
26 }
27
28
29 return 0;
30 }
运行结果如下:
上图中大家有发现什么不对的地方吗?首先,变量a定义在两个进程分流之前,那么父子进程应该拿到的是同一块数据,通过查看地址,再对应值,确实如此,但继续往下看,当计数两次之后,我们将父进程的a修改了,父进程的a也确实被更改了值,但是,父子进程拿到了同一块数据,为什么子进程的a没有被改变呢?更离谱的是它们两个的地址竟然一模一样,这合理吗?这很不合理。
根据我们学习语言所知,同一块空间只能只能存一个数据,这里竟然存了两个数据,那么根据理论而言,我们能够分析出,我们获取到的地址不可能是真实的物理地址,而是虚拟地址。
但是根据我们学习的计算机体系结构可以知道,任何一个磁盘上的内容想要跑到CPU中被运行,必须经过被载入内存这一过程,但是上面又显示我们拿到的不是真实的物理地址,就不可能是再内存当中的,难道是我们的冯诺依曼体系在这里出错了?很明显不可能,所以只有一种可能,那就是我们在程序中获取到的地址与我们的内存之间有联系。
3. 进程地址空间与内存
这一部分有些抽象,所以我先为大家讲一个故事吧。(这个故事是博主去别人哪里copy的,博主认为十分生动形象)。
咱们在学校总是经历过划分三八线这种事情吧。就是给桌子划分区域,假设有小明和小妞两个人,他们有自己的区域,不能越过区域,到别人的领域去。如下:
我们可以想一下,他们是怎么区分他们自己的区域的呢?不就是记录自己区域的起始结束位置嘛,也就是说小妞想要表示自己的位置,那就是如下:
struct xiaoniu_area{
size_t begin = 0;
size_t end = 50;
};
小明也是同理,也就是说他们自己知道这一片空间的所属就行,不过呢,小明和小妞关系很好,可以亲亲抱抱的那种,所以这一条三八线也就是当作一个提醒罢了。所以呢,小妞有一天要画画,她需要更大的位置操作,那么她就向小明说,我想要多一点点位置,之后还你,小明说:“反正现在我也不用,你用吧”。这样小妞的位置就变为了如下:
struct xiaoniu_area{
size_t begin = 0;
size_t end = 70;
};
那么,相对的小明的位置就被缩减了,图如下:
看到上面的图,大伙有没有将我们的地址分配图联系起来呢?我们的进程地址也就是通过这个方式表示的,就像是我们的栈、堆等等,它的位置都是能够被调整的。
在Linux当中有一个struct mm_struct这样的结构体,用于表示各个区所对应的位置。如下:
struct mm_struct
{//代码区
unsigned long code_start;
unsigned long code_end;//堆区
unsigned long heap_start;
unsigned long heap_end;
//栈区
unsigned long stack_start;
unsigned long stack_end;
}
也就相当于整个进程地址空间的使用被这样一个结构体划分的明明白白。
但是这又跟我们的内存有什么联系呢?别着急,接着看。
我先给出结论:
每个进程都可以独占整个内存空间
怎么理解上面这句话呢?咋一看是不是感觉很奇怪?仔细一看更奇怪了是不?
我还是举一个例子。
有一个10亿家产的富豪,他有2个私生子,这两个私生子不知道彼此的存在,此时呢,富豪对两个孩子分别说了,只要我寄了,这10亿就是你的了,所以呢,这两个还是都认为这10亿自己稳得了。这时,大儿子说,老爸,我想要20万买个车上班,富豪说,行,好好加油,之后小儿子又说,老爸,给我5个亿,我要单开一家公司,做大做强,富豪转身就抽出了七匹狼,你要皮带不要?小儿子就跑了。但是对于大儿子和小儿子来说,他们认为这10还是自己得吗?肯定是啊。
这里的富豪就是操作系统,10亿就是内存,而两个儿子就是进程,他们可以向操作系统申请空间,操作系统也可以拒绝他们的请求,但是他们能得到的空间是内存大小的空间。这也就表明了,每一个进程都认为自己是可以独占内存的,但是操作系统是能够自己判断是否给你这么多。
4. 进程地址空间和内存的关联
进程地址空间通过页表和内存关联起来。
什么是页表?
页表就是:进程将自己的代码和数据首先放在虚拟地址空间的对应的区域,在这其中会有一种表结构,叫做页表,页表的核心工作就是完成虚拟地址到物理地址之间的映射,最终我们的可执行程序的代码和数据可以加载到物理内存的任意位置,因为最终只需要建立代码和数据与物理内存之间的映射关系,就可以通过虚拟地址找到物理内存的对应地址。
咱们可以想到每一个进程都是独立的,父子进程也是不例外的,那么每一个进程也都有属于知道的页表去对应真实的内存。看到这里,想必大家也能回到之前父子进程不同值,地址相同得问题了吧。那就是父子进程得虚拟地址相同,但是这个虚拟地址存在于两张页表当中,这两个页表通过相同的虚拟地址却映射了两个内存空间。
那么,这两个映射不会映射到一个空间吗?
不会,还记得我之前讲的mm_struct结构体,内存将这些空间用结构体划分了起来,并且划分之后还会标识这片区域已经被某个进程使用了,那么页表就知道了这篇空间不能使用了,它就会换一片区域去映射。如下:
看到上面的图,我们可以认为父子进程是完全独立的吗?
其实不能,因为我们的操作系统是十分会节省空间的,也就是当父进程或子进程的数据没有被改变的时候,两个页表指向的真实空间也是相同的,只有在数据发生改变的时候操作系统会为这一个空间开辟一个新的空间,然后对应给页表,但是页表的虚拟内存没有改变。这一过程被称为写时拷贝。
5. 为什么要有进程地址空间
我们使用malloc时,操作系统在我们申请内存的时候并没有直接的给我们那一片地址,但是这一片空间并不能被其他的进程使用,该片地址会处于一个闲置状态。这篇地址不能使用是这一个进程不能使用,而是其它进程可以使用,但是操作系统会保证数据不会冲突。
也就是说,我们能够获取到虚拟地址,但是我们没有在意到底有没有实际的物理地址在哪里,反正我们要使用的时候,操作系统能给我们就是了。所以这样做就让我们的进程管理和内存管理之间解耦了。
还有就是当操作系统要加载一个很大的程序,比如32个G,这样一个程序一定是不可能全部运行起来的,所以操作系统会慢慢的加载,没有被使用的部分就被先睡眠起来了。我们唯一的感受也就是程序变慢了。
并且,通过进程地址空间,我们操作系统能够清楚地知道你写的进程是否正确,有没有越界?如果越界了,那么操作系统就会让你的进程崩溃,因为你的指针或者其它数据指向了其它进程的区域,保证了进程之间的独立性和安全性。
以上就是我对进程地址空间的全部理解咯,有问题请帮忙指出啦,博主也是努力进步当中,哈哈。