进程虚拟地址空间打破了我一直以来对于程序地址空间的认识,它真的好神奇。
我们首先来看一下下面这段代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 /*
4 *定义一个全局变量
5 *创建子进程
6 *修改变量值,再取地址,观察地址是否一样
7 * */
8 int glo_val=10;
9 int main()
10 {
11 pid_t ret = fork();
12 if(ret < 0)
13 return 0;
14 else if(ret == 0)
15 {
16 //child
17 glo_val = 20;
18 printf("I am child,glo_val = %d,address = %p\n",glo_val,&glo_val);
19 }else if(ret > 0)
20 {
21 //father
22 printf("I am father,glo_val = %d,address = %p\n",glo_val,&glo_val);
23 }
24 return 0;
25 }
我们来看一下它的运行结果:
是不是很神奇!!!
打印出来的两个地址相同,但是!值不一样!!
就非常奇怪啊,为什么会这个样子呢?难道我拿的地址是假地址?我取地址的方式有误?但是我检查过了,没有毛病,所以这个可能是某个我不知道的知识引起的,于是,我去查了查,下面就给大家解密。
通过上面的程序和它的运行结果,我们可以发现:变量内容不一样,所以父子进程输出的变量绝对不是同一个变量,但是地址值是一样的,说明这个地址肯定不是物理地址!!!打印的这种地址叫做虚拟地址。
操作系统为什么要弄一个虚拟地址出来呢?
提高内存的使用率
虚拟地址:我们用户是看不到物理地址的,那大家可能就有疑问了,我取地址取的不是地址吗?是地址,只不过是虚拟地址,真正的物理地址是由OS统一管理的。OS需要将程序中的虚拟地址转化为物理地址。
进程虚拟地址空间
在32为操作系统下,OS为每一个进程无差别的虚拟出4G的虚拟地址空间,程序在访问内存的时候,访问的是虚拟地址空间,既然是虚拟地址空间,那就不能直接存储数据,存储数据还是得存储在真正的物理空间里,于是OS就需要将虚拟地址空间转化为物理地址进行访问。那怎么转化呢?就使用我们接下来将要提到的页表。
为什么OS要给每一个进程都虚拟出来一个进程虚拟地址空间呢?为什么不直接允许进程访问物理内存呢?
因为各个进程访问同一个内存地址空间,就会不可控,可能会导致越界访问。在有限的内存空间里,进程它是不知道哪块内存是被其他进程使用的,也不知道哪块内存是空闲的。所以,在这种场景下,让进程直接访问物理内存,一定会导致多个进程在访问物理内存时出现混乱。所以,内存由OS统一管理起来,但是又不能采用预先分配内存的方式给进程,因为OS也不清楚进程真正要保存多少数据,不知道分配多少内存给进程合适,于是操作系统就虚拟的给每一个进程分配了4G(32位操作系统下)的地址,这个地址是虚拟的地址。
进程需要保存数据,或者申请内存的时候,操作的都是虚拟地址,让操作系统再给进程进行分配,同时也节省空间,用多少分配多少,这样是不是就很合理。
每一个进程无差别的使用拿到的虚拟地址,OS会将虚拟地址通过页表映射转换为真正的物理地址。
页表映射
进程虚拟地址空间和物理内存之间存在着映射关系,就是页表映射,将虚拟地址分成一页一页的,将物理内存分为一块一块的,通过页表来记录虚拟地址和物理地址的映射关系,页表由页号和页内偏移构成。
我们可以通过虚拟地址+页表的方式来找到物理地址。
- 虚拟地址 = 页号+页内偏移
- 页号 = 虚拟地址 / 页的大小
- 页内偏移 = 虚拟地址 % 页的大小
这样我们就能根据页的起始地址找到具体的物理地址。
同时需要注意的是,在进行内存分配的时候,我们不采取连续分配的方式,而是采用一页一页离散分配的方式,主要是为了防止产生内存碎片。
最后总结一下:
- 每个进程都有独立的虚拟地址空间,进程访问的并不是真正的物理地址。
- 虚拟地址可以通过每个进程上面的页表与物理地址进程映射,获得真正的物理地址。。
- 页表在每个进程的内核虚拟地址空间里
- 如果虚拟地址对应的物理地址不在内存当中,就会产生缺页中断,真正分配物理地址,同时更新进程的页表,如果此时物理内存已经耗尽,则根据内存替换算法淘汰部分页面至物理磁盘中。