2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > 【Linux】进程概念 —— 虚拟内存地址空间

【Linux】进程概念 —— 虚拟内存地址空间

时间:2023-05-21 21:05:14

相关推荐

【Linux】进程概念 —— 虚拟内存地址空间

目录

一.进程地址空间

1.进程地址空间分布图

2.验证上述进程地址空间

3.Linux vs Windows

二.了解虚拟内存地址空间

0.通过代码引出虚拟内存地址空间概念

1.什么是虚拟内存地址空间

2.虚拟内存地址空间的设计方式

三.深入虚拟内存地址空间

1.虚拟内存地址空间与物理内存之间的映射

2.一个程序经过编译器到进程运行时的过程(帮助理解虚拟与物理之间映射关系)

3.虚拟内存地址空间存在的价值

四.重新理解fork函数

1.为什么会有两个返回值

2.为什么会有一个变量同时接收了两个返回值

3.写时拷贝

4.父子进程之间的关系

5.fork如何实现的调用fork之后父子进程代码共享

一.进程地址空间

我们先不谈虚拟内存与物理内存,当一个进程运行起来时,它的地址空间分布是什么样的呢?

1.进程地址空间分布图

2.验证上述进程地址空间

在Linux环境下验证!

代码:

#include<stdio.h>#include<stdlib.h>int g_val1 = 10;int g_val2 = 20;int g_val3;int g_val4;int main(int argc, char* argv[], char* env[]){//任务:验证进程地址空间//代码区printf("code address:%p\n", main);//只读常量区const char* p1 = "hello";printf("only read:%p\n", p1);const char* p2 = "hello world";printf("only read:%p\n", p2);//全局区printf("init global g_val1:%p\ninit global g_val2:%p\n", &g_val1, &g_val2);printf("uninit global g_val3:%p\nuninit global g_val4:%p\n", &g_val3, &g_val4);//堆区char* tmp1 = (char*)malloc(10);char* tmp2 = (char*)malloc(10);char* tmp3 = (char*)malloc(10);char* tmp4 = (char*)malloc(10);printf("heap:%p\n", tmp1);printf("heap:%p\n", tmp2);printf("heap:%p\n", tmp3);printf("heap:%p\n", tmp4);//栈区printf("stack:%p\n", &tmp1);printf("stack:%p\n", &tmp2);printf("stack:%p\n", &tmp3);printf("stack:%p\n", &tmp4);//命令行参数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;}

3.Linux vs Windows

1).以上对于进程地址空间的验证,是在linux环境下.

在windows下会跑出不一样的结果,因为windows基于安全考虑,做了特殊处理

二.了解虚拟内存地址空间

0.通过代码引出虚拟内存地址空间概念

代码:

代码描述:

1.先创建一个全局变量g_val,并且初始化为100

2.创建子进程后,在子进程内部将g_val改为200

3.观察父子进程的g_val值以及g_val地址

#include<stdio.h>#include<unistd.h>int g_val = 100;//全局变量int main(){pid_t id = fork();if(id == 0){//子进程g_val = 200;printf("I am child, pid:%d, ppid:%d, g_val:%d, &g_val:%p\n", getpid(), getppid(), g_val, &g_val);}else{//父进程printf("I am father, pid:%d, ppid:%d, g_val:%d, &g_val:%p\n", getpid(), getppid(), g_val, &g_val);}return 0;}

结果:

结论:

我们惊奇的发现,父子进程所打印的g_val的地址是一样的,但是!!里面却存了不同的值!!!

(注意一点:我们这里所打印的地址,是进程运行起来之后,由进程内部的printf系统调用所打印出来的,所以本质打印的这个地址是动态的)

为什么同一块空间,里面存放的不同的值呢?

经过进一步思考与猜测,那就是这个地址不是真实的物理地址! 而是虚拟地址!

在后面会给出对于这一现象的详细解释

1.什么是虚拟内存地址空间

现象:

每一个进程,都拥有自己的虚拟内存地址空间

虚拟内存地址空间的本质,就是我们上面验证的那个进程地址空间,这俩是一个东西

每一个进程都不知道外部其他进程的存在,都认为自己独享物理内存,并且拥有4G的空间

但实际上,并不是真正拥有4G内存,而是按需分配物理内存!

类比:

其实,这本质不就是老板给员工画的一张饼吗?

我们假设的认为:

操作系统 --- 老板

进程 --- 员工

虚拟内存地址空间 --- 老板给员工画的饼

物理内存 --- 真正的好处(例如:真的升职了,真的加薪了)

可以看出,虚拟内存地址空间只是操作系统给进程许诺,你拥有了全部的4G内存,但这仅仅只是是许诺而已,并没有执行!

而实际情况则是按需所取,如果进程过分索取内存的话,操作系统有权利拒绝!

这便可以很形象的表现出这四者之间的关系!

2.虚拟内存地址空间的设计方式

我们都知道,管理的本质是先描述,再组织

例如我们学过的进程,进程是如何被管理起来的呢?

答案是:先描述,再组织. 先通过进程控制块(PCB)(在linux中的具体表现形式:task_struct)将进程描述起来,然后将这些PCB以某种数据结构的形式组织起来,这样操作系统便将所有的进程管理了起来!

那么,虚拟内存地址空间是如何设计的呢?

以及它在linux中的具体表现形式是什么?

它是如何被管理的?

1.虚拟内存地址空间是如何设计的

实际上,虚拟内存地址空间的本质就是一种数据结构,并且将来要和特定的进程关联起来.

这种数据结构在linux中表现为mm_struct

与特定的进程关联起来是某个进程PCB中有一个指针指向mm_struct

2.mm_struct的本质是什么(大概了解)

其内部会有区域划分(这只是一部分)

3.对于虚拟内存地址空间的管理

先描述,再组织

先描述:通过mm_struct描述起来,mm_struct就是一个结构体,这个结构体内部包含各种数据结构,各种边界判定

再组织:进程PCB有一个指针指向mm_struct,便将进程与其虚拟地址空间一一对应起来,同时mm_struct内部有很多数据结构也能够自行组织

三.深入虚拟内存地址空间

1.虚拟内存地址空间与物理内存之间的映射

虚拟内存与物理内存映射图

2.一个程序经过编译器到进程运行时的过程(帮助理解虚拟与物理之间映射关系)

1).我们写的一个程序,在编译器编译成功之后,形成了一个.exe可执行文件(这是一个二进制文件)并且存储在磁盘上

提问:虚拟地址空间中的地址从何而来?磁盘中的.exe文件有没有地址呢?

答:虚拟地址空间中的地址,是编译器给的! 编译器在编译的时候,就已经为每一行代码都编好了地址(这个编好的地址是每一行代码的地址,是代码区的,并不是堆栈,堆栈在进程运行时才产生),并且在形成.exe文件的时候,代码/地址,此时已经全部存在于.exe文件中.

2).当我们准备执行这个.exe文件时,点击执行的那一刹那,程序就正式成为了一个进程,此时操作系统会为这个进程创建虚拟内存地址空间与页表,并且加载部分数据到物理内存,然后通过页表将虚拟内存与物理内存映射起来

2).0).创建虚拟内存地址空间时,会先将.exe文件中编译器给编好的地址放到代码区和全局区,然后在执行过程中,在生成对应的堆区和栈区

2).1).cpu读取/分析/执行指令时,会先去找虚拟地址,然后通过页表映射关系,找到物理地址

2).2).一些运行时的报错:比如说对只读变量做了修改,或者对野指针进行访问的非法操作

这些报错是谁报出来的?硬件,比如内存:其实是随时都仅仅只是简单的读写操作,并没有监管这么一说,那实际的报错,其实是

操作系统的介入,在页表映射的时候会记录有映射到这个物理内存空间中的权限,从而操作系统就可以根据权限来做出判断

3).当实际访问的时候,发现对应的物理地址没有数据或者代码,就直接发生缺页中断,此时操作系统执行页面调入与内存管理算法,完成之后,进行填充页表,完成映射

重点提取:

0.程序编译好生成.exe文件存在磁盘上,此时.exe内已经有地址了

1.编译器给程序生成了虚拟地址(代码区/全局区)

2.cpu执行的是虚拟地址

3.物理内存只需要按需载入

4.物理内存很傻,只是随时随地可以读取,而是在页表映射中给相应的物理内存空间赋予权限

3.虚拟内存地址空间存在的价值

从安全的角度来看:有效保护物理内存

如果直接使用物理内存,必然会发生进程A可以访问或修改进程B的数据,甚至可以影响内核程序

使用虚拟内存然后映射到物理内存这一机制,使得每个进程彼此互不干扰甚至不知道对方的存在

同时由于中间层(虚拟内存与页表)的介入,使得可以直接拒绝(通过OS)一些危险操作

从而OS可以杀掉一些危险进程,起到保护其他进程与OS内核的作用

从管理模式的角度来看:完成了对于进程管理模块与内存管理模块的解耦合

什么是解耦合:

如果我将所有代码都写到main函数内,这就是高耦合

如果我将代码分功能分模块的写到不同函数内,然后在main函数中只是调用这些函数,这就是低耦合

所谓的耦合,就是关联性的意思

虚拟内存地址空间 --- 只负责进程管理

物理内存 --- 只负责内存管理

两者互不干扰

也就是说,代码和数据被加载到物理内存上时,并不用考虑区域的问题,而是随意加载物理内存的任意位置

无论加载到哪里,都可以让虚拟地址通过映射关系,找到所对应的所在物理内存上的位置

虚拟内存地址空间+页表 可以将在物理内存中乱序的指令有序化(因为虚拟地址空间负责进程管理,它是有序的)

从效率的角度来看:避免了不必要的空间浪费

对于空间浪费的认识:

如果我说申请了多余的空间,毫无疑问这肯定是浪费了空间

而如果说,我申请了一块空间,但这块空间没有很及时的被使用,这依旧也造成了空间浪费

有了虚拟内存地址空间与页表的介入之后,如果我们申请了一块空间,这样的申请只能说是一种约定,相当于告诉物理内存

我之后要使用这块空间,物理内存知道了,但没有立刻开辟出来,而是当我们在使用这块空间时,发现实际并没有这块空间,此时

就会发生缺页中断,然后分配物理内存,建立映射关系,再继续执行

这种延时分配的策略,提高了整机的效率,使内存的利用率几乎是100%

以上这些逻辑是由操作系统自动完成,用户不会感知到,并且对于用户来说也无需担心!

从进程独立性的角度来看:真正意义上的实现了进程之间互不干扰,互相独立

由于每个进程都拥有自己的虚拟内存地址空间与页表,并且当cpu在执行时,总是执行虚拟地址再找到物理地址,所以对于这个进程来说,它是不知道外部世界的,因为cpu根本不会访问外部地址(物理地址),总是通过进程内的虚拟地址找到对应的物理地

从进程挂起的角度来看:实现了对进程分批加载分批换入换出,进而提高硬件的上限

既然可以实现延时分配内存的策略,自然也就同样可以支持分批换入换出操作

(本质都是当实际映射到的物理内存空间不存在时,通过缺页中断,来执行页面调入和内存管理算法,填充页表,完成映射)

现实生活中的例子: 当前时代的游戏,随便就有几十G大小,而我们的内存只有4G/8G/16/32G...(内存的成本非常高)

如何让高达几十甚至几百G的游戏运行起来?

很显然将几十/几百G全部加载到内存,成本消耗巨大,普通人是承受不起的,这时发现我们8G的电脑都是可以运行并且

很流畅的,这就应用到了OS对进程分批加载,并且将需要的部分换入,不需要的部分换出.让8G的内存达到了几十G的效果

如果这个进程短时间内不会被执行,就叫做阻塞

进程的数据和代码被换出,就叫做挂起

所以进程阻塞之后,进程的代码和数据就会被挂起!!!!!!

四.重新理解fork函数

1.为什么会有两个返回值

fork是系统调用,fork函数内部会创建子进程,当fork函数准备return时,子进程便已经被创建出来了,子进程被创建出来之后的代码父子共享(原理在第5点会介绍),此时父进程需要return,而刚刚被创建出来的子进程也要执行return,给子进程返回0,给父进程返回子进程的pid

2.为什么会有一个变量同时接收了两个返回值

我们知道了为什么表面上同一个函数为什么会有两个返回值之后

那么同一个变量,是如何接收两个不同的返回值,并且还能同时作出判断的呢???

这在我们不知道虚拟内存地址空间之前,是无法理解的

现在我们便可以给出合理并且正确的解释:

1).当我们创建出子进程,子进程会拷贝(或继承)父进程的虚拟地址空间

(为什么会继承父进程的虚拟地址空间呢? 因为在编译器编译的时候,只编译了父进程,给父进程生成了.exe文件,并且给每一行代码都进行了编址,那么在父进程运行时,子进程凭空出现,自己成的虚拟地址又怎么样让它生成呢?直接简单粗暴,拷贝父进程的!)

2).假设接收返回值的变量命名为id,这时子进程和父进程对于id这个变量拥有相同的虚拟地址(因为子继承的父),并且通过映射指向的是同一块物理内存空间

3).当第二次进行对id的写入时,会通过虚拟地址然后通过映射机制找到物理内存,并且知道这一次写入的(第二次写入)是父还是子,发现第一次已经写入过一次了,所以第二次就会重新为其开辟新的空间并且建立新的映射关系,此时父子表面上对于id的虚拟地址是相同的,但是实际指向的物理内存却是两块不同的空间

画图解释:

3.写时拷贝

创建子进程时只需要将未来会写入的数据再重新拷贝一份

但是操作系统并不知道哪些数据将来会被写入,即便是知道了提前就开辟好一块新的空间并且拷贝,这样长时间占用内存却不使用的情况也会造成空间浪费

所以操作系统所采用的方法是写时拷贝,只有当数据被写入时,才会发生拷贝

这一理念在C++的深浅拷贝中也有所体现!

4.父子进程之间的关系

1).子进程被创建出来时,会拷贝父进程的虚拟地址空间

2).共享子进程被创建完之后的代码(fork之后),也就是父子都从那一刻同时执行之后的代码

3).父与子是两个独立的进程,具有独立性,不相互影响不相互依赖

4).代码是只读的,所以代码层面上的共享,对于他俩的独立性而言没有影响

5).数据是可以被修改的,所以必须分离

5.fork如何实现的调用fork之后父子进程代码共享

由于进程具有并发性,所以随时有可能中断,所以必须要有多个硬件(寄存器)来存放当前进程执行到的数据与状态(例如进程执行到的位置),以便将来用于继续执行还未执行完的进程,这种寄存器内,存储的数据称为:进程的上下文数据

其中EIP寄存器,就是用来存放cpu将要读取的下一条指令的地址

创建好子进程之后,子进程认为从EIP开始执行,所以就会从父进程的EIP记录处为起始,开始继续向下执行指令

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。