【linux】进程地址空间


一、复习C/C++的地址空间

我们已经学习过了C/C++的地址空间的知识了:
在这里插入图片描述
那么,这些地址就是内存吗?我们以前或许就认为这上面的什么栈区就是内存,其实不是的!接下来我们就来深入研究一下什么是内存

1、提出问题

我们学习了前面的知识之后,知道了父进程与子进程之间的种种关系,但是pid_t id=fork();创建子进程之后,父子进程被执行的顺序是不确定的,主要是看调度器来决定的

那么,我们在编译器或者linux中看到的地址就是所谓的内存吗?

我们来测试一下
points.c:

  0 #include <sys/types.h>
  1 #include <stdio.h>  
  2 #include <unistd.h>  
  3 int global_val = 100;  
  4 int main()  
  5 {  
  6     pid_t id = fork();  
  7     if(id<0)  
  8     {  
  9         printf("fork error\n");  
 10         return 1;  
 11     }  
 12     else if(id == 0)  
 13     {  
 14         int cnt = 0;  
 15         while(1)  
 16         {  
 17             printf("我是子进程?pid:%d,ppi:%d|global_val:%d,&global_val:%pn",getpid(),getppid(),global_val,&global_val);  
 18             sleep(1);  
 19             ++cnt;  
 20             if(cnt == 10)  
 21             {  
 22                 global_val=300;  
 23                 printf("子进程已经更改全局变量val了!\n");  
 24             }  
 25         }  
 26     }  
 27     else  
 28     {  
 29         while(1)  
 30         {
 31             printf("我是父进程?pid:%d,ppi:%d|global_val:%d,&global_val:%pn",getpid(),getppid(),global_val,&global_val);
 32             sleep(2);                                                                                                                                                                                                                        
 33         }
 34     }                                                                                                                                            
 35     return 0;                                                                                                                                    
 36 } 

在这里插入图片描述
大家可以看到,上面的global_value的地址都是一样的。但是,子进程更改了value的值之后,父进程居然没有更改value的值,还是打印的100,而子进程打印的是300
由此可见,我们这里的value的地址不是物理地址,而是一个虚拟的地址,我们称之为虚拟地址。那么,我们以前学习的C/C++的指针,什么取地址(&),可见都是得到的虚拟地址。因为如果是物理地址,不存在一个地址对应两个值的情况

2、解答问题

根据上面的验证我们得出了结论,我们所看到的地址绝对不是物理地址,而是一种虚拟地址,也可以叫做线性地址/逻辑地址。我们把这种地址统称为虚拟地址空间

3、得出结论

1、变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
2、但地址值是一样的,说明,该地址绝对不是物理地址!
3、在Linux地址下,这种地址叫做 虚拟地址
4、我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
所以,==OS必须负责将 虚拟地址 转化成 物理地址 ==


二、虚拟地址空间

1、感性认识

我们先来感性认识虚拟地址空间

进程概念:进程会认为自己是独占系统资源的,但是实际上并不是这样

举个小例子:大学生每个人都有自己的床铺,你睡觉时都在自己的床上,但是,室友可能在你睡觉的时候,把你移动到其他地方,当你要醒的时候再移动回床上。这个时候你就认为你一直在使用自己的床铺,但其实不是这样,自己的床铺可能被其他人使用(比如:坐在我的床上,或者躺在我的床上)

操作系统会给进程画一个大饼,告诉每一个进程:“我这里有16G(一般电脑一共就16G的内存)的内存,可以供你使用”。但是,操作系统绝对不会一下子把16G的内存全部给出去,因为操作系统要兼顾多个进程。每一个进程都可以向操作系统要全部的内存,但是操作系统可以拒绝。不过对于进程来说,就算被操作系统拒绝,也认为自己有使用全部内存的权力。所以,操作系统给每一个进程画的大饼(大饼:每一个进程有着全部内存的使用权力)就叫做进程(虚拟)地址空间

2、“画大饼”

那么操作系统怎么“怎么画大饼”呢?
操作系统的记性一定要好,它要记得给每一个进程画的大饼是什么样的(多少内存,位置在哪里…),否则就算白画了
那么,画饼的本质就是构建一个蓝图——数据结构对象:

struct blueprint //struct 蓝图
{
	char* who;
	char* when;
	char* target;
	char* money;
	//岗位
	......
}

那么,不仅仅是进程要被管理,操作系统给进程画的大饼,也就是进程/虚拟地址空间也是要被管理的
在这里插入图片描述
所以,地址空间的本质就是:是内核的一种数据结构:mm_struct。这就可以通过管理mm_struct结构体从而管理地址空间

3、区域划分

在这里插入图片描述

知识点:

1、地址空间的基本空间大小是字节
2、x86也就是32位平台下,有2的32次方个地址
3、2的32次方 * 1字节 = 4GB空间大小
4、每一个字节都对应唯一的一个地址,这些地址都是虚拟地址(unsinged int (32bit))

这里可能有人回疑惑,那为什么上面的例子中,value的值不一样,但是地址一样呢?
我们下面回解释的

同理,对于区域的划分,我们也是通过一个结构体来表示的:

struct Destop
{
	//进程A的区域
	unsigned int processA_start;
	unsigned int processA_end;
	//进程B的区域
	unsigned int processB_start;
	unsigned int processB_end;
}

我们想要更改一个进程的区域直接更改start和end的范围就行了

struct Destop AB = {1,50,51,100};//从1开始
struct Destop fix1 = {1,45,55,100};//第一次改变
struct Destop fix2 = {1,30,31,100};//第二次改变

所以,我们实际上的地址空间的划分以及调整就是通过改变边界的大小,来达到地址空间的动态分配的目的
上面的A和B就算mm_struct结构体对象

由此可得,我们的虚拟地址区域划分也是这样的:
在这里插入图片描述
我们的栈区和堆区可以扩大缩小,本质上就算对heap/stack的start和end进行改变,达到扩大/缩小的目的

我们根据前面的知识知道,每一个进程创建都会生成对于的task_struct(进程控制块PCB),这里面都会有2的32次方空间,里面包含该进程的Pid,ppid,属性,数据,优先级,进程状态等等,每个tast_struct都会对应一个mm_struct,也就是对应画一个大饼,而task_struct里面的指针指向对应的mm_struct

在这里插入图片描述
所以,我们这里就证明了C/C++的地址空间这种叫法是错误的,正确的叫法应该是进程/虚拟地址空间
在这里插入图片描述


三、进程地址空间与内存之间的关系

1、虚拟地址

通过上面的认识,我们知道了我们写的代码,监视窗口查看的地址等等内容都是虚拟地址。因为虚拟地址是连续的地址,所以又可以称为线性地址

举例:二维数组的地址是连续的,我们可以用两个for循环打印,也可以用一个for循环打印

在这里插入图片描述

2、物理地址

我们说的物理地址是经常与磁盘/内存产生联系的。数据从磁盘加载到内存以及从内存输出到磁盘的过程,我们称为IO,IO的单位是4KB,我们就把内存中4KB大小的空间看成一个page页。所以,内存4GB的空间可以分为4GB/4KB个page页。
这个时候我们可以把内存当成一个结构体数组,struct page mem[4GB/4KB],通过偏移量就能访问内存的所有page页,从而达到访问内存的所有数据

在这里插入图片描述

3、物理地址与虚拟地址的关系

虚拟地址实际上是需要存放在物理地址上面的某个位置的,从而产生某种关系。
而两者之间需要一种媒介来进行关联,这种媒介就叫做页表
页表是非常大的,一个页表要存放两个地址,要使得虚拟地址和物理地址对应起来,所以页表一共要2^32*8左右的空间

在这里插入图片描述
在这里插入图片描述
所以,虚拟地址会被页表映射到物理地址,从而完成操作。而我们只需要将数据/代码编译好保存到虚拟地址上面,至于页表映射等其他操作,都是操作系统来帮助我们完成的

4、多级映射

假设我们有两个进程,来看看逻辑图是怎么样的:
在这里插入图片描述
进程1和进程2同时执行时,是看不到物理内存的,只能够看到mm_struct虚拟地址空间,每一个mm_struct内部都有2^32个地址,但是操作系统是不允许任何一个进程占用完所有地址的

四、如何理解进程地址空间

1、为什么存在进程地址空间

为什么要有进程地址空间呢?我们之间访问物理内存不行吗?

1、保证安全性
如果我们直接访问物理内存,万一我们的进程出现了越界访问等违法操作是非常不安全的。而虚拟地址的越界访问,页表可以通过缺页中断来阻止我们的非法操作,相当于保护了物理内存

2、保证独立性

我们上面的例子中,value在父进程打印为100,但是子进程打印为300,这是为什么呢?

我们前面学到了,子进程被创建之后会继承父进程的数据和代码。那么也就是说,子进程的mm_struct和父进程的mm_struct所指向的页表的内容是一样的,都会通过页表与物理内存形成映射关系,这也就是为什么子进程父进程开始打印的时候value值都是一样的
答案就算子进程继承了父进程 的代码和数据,所有子进程的mm_struct也指向了父进程指向的页表内容

在这里插入图片描述

但是,子进程要改变global_value的值,由于进程具有独立性,一个进程改变变量不能影响其他进程的变量,所以一旦子进程要改变global_value
操作系统会将子进程的页表与物理内存直接的联系给断开,并且在物理内存的其他地方将原来的物理内存数据拷贝过来,在与子进程的页表进行映射联系,这种方法叫做写时拷贝
这样子就不用担心子进程改变变量影响到父进程了
在这里插入图片描述
所以,我们看到的父进程与子进程的value地址一样,但是值不一样。
这也就是进程的独立性

**进程=内核数据结构+进程对应的数据和代码**

内核数据结构是独立的,进程对应的数据和代码也是独立的,所以进程就算独立的

所以得出结论:
地址空间的存在,更加方便的进行进程和进程数据代码之间的解耦,保证了进程的独立性特征

3、保证统一性

在linux中,虚拟地址和逻辑地址是一样的

提问1:当我们写了一个程序在磁盘中,在未被加载到内存的时候,内部的函数和变量有地址吗?

肯定有的。当程序在编译链接的时候,磁盘中的程序就有了地址,这个地址被我们叫做逻辑地址

提问2:虚拟地址空间的规则只有操作系统遵守吗?

肯定不是的。不仅仅操作系统OS要遵守,编译器也要遵守。所以说,磁盘中的程序在编译器编译的时候,就是按照虚拟地址空间的规则来进行编址的!

图片分析:
在这里插入图片描述
图中的fun()函数和main()函数都要被保存,那么就要通过虚拟地址空间规则来进行编址。

在32位平台下进行编址。在编译的时候main()函数中的fun()函数会通过逻辑地址跳转到定义的fun()函数(可通过反汇编查看)。当程序的代码加载到内存时,这个逻辑地址依然存在;也就是程序被加载到内存时,程序里面的变量和函数所使用的逻辑地址还存在。但是当我们程序加载到内存时,代码是数据,那么就一定要在物理内存中某个位置进行保存,这个时候,代码既有外部的物理地址,也有代码内部执行跳转等操作的逻辑地址,相当于代码有了两个地址!

我们的程序先被加载到了物理内存中,然后cpu通过程序内部函数的虚拟地址,先取到了main函数,然后把main函数的虚拟(逻辑)地址,通过页表映射找到物理内存中的main函数进行调度;然后main函数执行过程中跳转到fun()函数,这个时候cpu就拿到了fun()函数的虚拟(逻辑)地址,然后cpu再通过页表映射找到物理内存中的fun()函数进行调度
在这里插入图片描述
所以cpu接触到的都是虚拟地址,不是物理地址

得出结论:
让进程以统一的视角,来看待进程对应的代码和数据等各个区域,方便编译器也是以统一的视角来进行编译代码。也就是指:编译器也是使用的虚拟地址空间规则,因为规定都是一样的,所以我们的代码编完就可以使用

4、简化内存管理

程序可以申请一块连续的1GB虚拟内存,而实际物理内存可能由多个不连续的4KB页组成
进程看到的是连续的虚拟地址空间,无需关心物理内存的碎片化分配

2、小结

1、保证安全性
如果我们直接访问物理内存,万一我们的进程出现了越界访问等违法操作是非常不安全的。而虚拟地址的越界访问,页表可以通过缺页中断来阻止我们的非法操作,相当于保护了物理内存

2、保证独立性
进程与进程之间共用共享内存,或者父子进程对同一份资源做修改等操作,发生写实拷贝等操作,使各个进程修改数据不会干扰其他进程
更加方便的进行进程和进程数据代码之间的解耦,保证了进程的独立性特征

3、保证统一性
让进程以统一的视角,来看待进程对应的代码和数据等各个区域,方便编译器也是以统一的视角来进行编译代码。也就是指:编译器也是使用的虚拟地址空间规则,因为规定都是一样的,所以我们的代码编完就可以使用

4、简化内存管理
程序可以申请一块连续的1GB虚拟内存,而实际物理内存可能由多个不连续的4KB页组成
进程看到的是连续的虚拟地址空间,无需关心物理内存的碎片化分配

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值