2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > 【Linux进程概念】 (4)进程地址空间

【Linux进程概念】 (4)进程地址空间

时间:2020-10-16 12:31:05

相关推荐

【Linux进程概念】 (4)进程地址空间

我们在编写C代码时往往出现一个错误就是变量地址的非法访问和使用,如果此时的地址对应内存中真实的物理地址,那么我们造成的多数错误可能会导致系统崩溃。所以关于地址空间的分布不是我们想象的那么简单,接下来我将从什么是地址空间地址空间是如何设计的,还有为什么要有地址空间这三个角度来讲述。

目录

一.什么是地址空间

1.验证地址空间分布

2.虚拟地址空间

3.虚拟地址和物理地址之间的关系

二.进程地址空间是如何设计的

1.深度理解虚拟地址

三.为什么要有地址空间

1.有效的保护了物理内存

2.内存管理和进程管理进行解耦合

3.地址空间可以将内存分布有序化

一.什么是地址空间

1.验证地址空间分布

我用下列的代码来验证地址空间的分布:

#include<stdio.h>#include<stdlib.h>int val;//未初始化int init_val = 100;//初始化int main(int argc,char *argv[],char *env[]){printf("env addr: %p\n",env[0]);//环境变量printf("args addr %p\n",argv[0]);//命令行参数char*p1 = "你好";char*p2 = "你们好";printf("stack addr:%p\n",p1);//栈区printf("stack addr:%p\n",p2);//栈区char*p3 = (char*)malloc(16);char*p4 = (char*)malloc(16);printf("heap addr: %p\n",p3);//堆区printf("heap addr: %p\n",p4);//堆区printf("global uninit val: %p\n",&val);//未初始化全局变量printf("global val: %p\n",&init_val);//已初始化全局变量printf("code addr: %p\n",main);//代码区起始地址 }

我们通过代码运行结果和下图直观感受地址空间的基本排布Linux32位系统下进程的地址空间范围是从0x00000000~0xFFFFFFFF,其中有3GB是用户内核,1GB是系统内核,今后我们在提及地址空间时要以整体来看。这里我们一定要明晰两个概念:上述的地址不是实实在在的内存中的地址,他是操作系统给每个进程分配的进程地址空间;进程地址空间会在进程的整个生命周期内一直存在,直到进程退出。

2.虚拟地址空间

我用下列代码做一组实验来验证进程地址空间不是内存的物理地址,用fork()函数创建子进程,让父子进程分别执行死循环打印数值和其地址,当子进程进行三次循环后修改其变量的数值,然后观察实验结果:

#include<stdio.h>#include<unistd.h>#include<sys/types.h>int val = 100;int main(){pid_t id = fork();//fork之后产生子进程if(id == 0){int cnt = 3;while(1){printf("我是子进程,pid:%d,ppid:%d,val:%d, &val:%p\n",getpid(), getppid(), val, &val);sleep(1);cnt--;if(cnt == 0){val = 200;printf("子进程将val改为200\n");}}}else{while(1){printf("我是父进程,pid:%d,ppid:%d,val:%d, &val:%p\n",getpid(), getppid(), val, &val); sleep(1);}}}

我们发现一个奇怪的现象,为什么在三次循环结束后子进程的val更改数值了却仍然和父进程的val地址相同,难道说一个内存地址上可以存在多个数值?答案是:当然不是。我们学过的任何语言里面的地址都不是内存中物理地址,而是操作系统给每个进程分配的虚拟地址。如何理解操作系统给各进程分配的地址,接下来我举一个例子方便理解:

你二爷是一个亿万富翁,他名下有三个孙子,有一天他分别给三个孙子偷偷的说:等我死后这十亿都是你的,现在好好想想怎么花吧。这一出把孙子们乐坏了,都在想自己的十亿怎么花。在这期间二爷的钱始终牢牢把握在他的手里,孙子们真正需要钱了,只要二爷能接受就给,不接受就回绝。最后每个孙子都沉浸在自己虚拟的十亿之中,手中的资源也默默分配在这十亿怎么花上,而且每个人怎么用都不一样。

上述例子中二爷就是操作系统孙子就是进程二爷自己的十亿就是内存中的物理地址空间二爷赋予给孙子们想象中的十亿就是操作系统给每个进程分配的虚拟地址空间,在这虚拟地址空间下,每一个进程都认为自己有4GB的空间,每个进程都可以向内存申请空间,只要能接受都会给你,不能接受操作系统会直接拒绝,在进程看来他依旧认为自己有4GB的空间。

3.虚拟地址和物理地址之间的关系

接着上边二爷的例子,无论孙子怎么幻想,最终都是拿到了二爷的钱。换言之,虚拟地址无论怎么分配都真实的在物理内存中分配到了空间,学过哈希的同学应该知道数据是可以通过映射得到另一组数据,这里虚拟地址也是通过某些映射机制转变为物理地址,这之间的转化就是靠操作系统完成的。

这里的映射机制也起到监督的作用,当虚拟地址合法申请内存时,操作系统对应着映射到内存中的物理地址;当虚拟地址非法申请内存时,操作系统就禁止映射,禁止这次的非法申请。

二.进程地址空间是如何设计的

首先我们先要理解进程地址空间内是如何划分区域的,如何区分堆区和正文代码区?这里我举例方便理解:

你是小帅,你现在正在上小学,你的同桌叫小美。有一天你们谁也看不惯谁,就在桌子上画三八线划分区域。那么如何描述你们分的区域呢?假设桌子长1米,左边从0开始画到30厘米后结束,计算出你的区域划分为[0,30],那么小美就接着从你结束的地方继续划分,计算出区域为[31,100]。

上述例子中桌子的总长相当于进程地址空间的大小默认范围是从0x00000000~0xFFFFFFFF,而属于小美小帅的那部分桌子就相当于空间中不同的区域。这里我们要理解一个概念:每一个进程都必须要有地址空间,换一种说法讲进程地址空间也是进程的一种属性,操作系统要管理每一个进程的地址空间时必然要做到先描述,再组织。这里的先描述就像例子中对一块区域描述为start(开始),end(结束)这两个数据一样,所有的区域都会别描述为具体的start,end数据,最终我们的进程空间被描述成一个存放着不同区域上下限的struct结构体,所以我们得到的结论是:地址空间的本质是在内核中的一种数据结构。

在Linux内核中,地址空间的结构被定义为struct_mm_struct结构体,每个进程和他对应的进程地址空间必然有联系,所以在进程的task_struct中存放着指向struct_mm_struct结构体的指针,通过他可以找到对应的地址空间,用下图可以形象的描绘操作空间管理地址空间:

上图中的页表是操作系统给每一个进程配发的,他主要负责维护虚拟地址和物理地址的映射关系,所以只要保证每一个进程的页表是独立的,就能保证每一个进程的虚拟地址映射到的物理地址是不同的。

1.深度理解虚拟地址

上面我一直在强调的概念就是,进程地址空间是操作系统给每一个进程分配的,但向更深一层学习,地址空间不仅仅是操作系统内部要遵守,我们的编译器也要遵守,虚拟地址空间需要编译器配合。什么意思呢,直观的理解就是当我们的程序在编译形成可执行程序,在没有被加载到内存的时候,编辑器就已经给我们形成了各个区域,比如代码区、数据区等,而且采用和Linux内核中一样的编址方式,给每一个变量,每一条代码都进行了编址,所以我们的程序在编译的时候,每一个字段早已经有了一个虚拟地址。紧跟我所叙述的概念,程序被加载到内存时编译器形成的虚拟地址也会被加载到内存中(我们将这个整体看作是可执行程序),此时可执行程序变为进程,我们的每一代码在内存中对应生成物理地址,操作系统对其管理对应生成task_struct结构体、进程地址空间、页表。接着进程地址空间通过使用可执行程序中编译器编译好的各部分代码地址段来初始化自己的mm_struct结构体中的每一个区域,例如将mian函数的起始地址和结束地址放入正文代码区,将已初始化全局变量的地址范围填充到初始化数据区……,页表的初始化无非是把可执行程序中的虚拟地址放在左侧,将可执行程序在内存中分配的物理地址对应放入右侧形成映射关系。至此我用下图来形象的解释这一过程。

这里编译器最初形成的虚拟地址对内使用,可以让内部代码通过CPU的执行找到并执行接下来的功能,保证代码的正常运行,这里CPU接受代码中函数返回的地址是虚拟地址;代码在内存中的物理地址,可以保证每一条代码都能被加载到内存中受外部操作系统和CPU的执行调配。这种机制也叫做虚拟内存

三.为什么要有地址空间

1.有效的保护了物理内存

首先我们要清楚一个概念,属于硬件的物理内存是可以被任意的读写内容的,这里是从软件层面的方式来限制内存的读写权限的。我们回想一个知识点:在C语言中定义char* c1 = "cigarette",然后改变c1指向字符串的首字母内容*c1 = 'A',运行程序后程序崩溃报错。从系统的角度看待这个场景,进程地址空间给出代码区的虚拟地址段,在放入页表准备映射物理内存时,页表对应每个区域所开放的读写权限的设置均不同,此时对于处在代码常量区的字符串"cigarette"页表对于此区域是不开放写权限的,当操作系统识别到进程非法访问物理内存时,就即刻强制杀掉了进程使进程退出,从用户的角度看就是系统崩溃了

我们在思考一下指针越界问题,难道是所有的指针越界都会导致进程崩溃吗?答案是:不是的。越界可能是在他自己的合法区域。比如他本来指向的是栈区,越界后它依然指向栈区,部分编译器的检查机制认为这是合法的,但当你指针本来指向栈区,后来指针却指向了字符常量区,编译器根据mm_struct里面的start,end区间来判断你越界了就会报错。

综上所述在使用地址空间和页表进行映射时,要在操作系统的监管下进行,这样既保护了物理内存中所有的合法数据,也保护了各进程甚至内核的相关数据

2.内存管理和进程管理进行解耦合

我们先明晰一个概念:操作系统有四种核心管理分别是进程管理内存管理驱动管理文件管理。这里我们只谈和地址空间相关的进程管理和内存管理,实质上因为进程地址空间的存在物,理内存的分配和进程的管理可以做到没有关系。如果没有进程地址空间,进程直接访问物理内存,当进程退出时,内存管理需要尽快将该进程回收,在这个过程当中必须得保证内存管理得知道某个进程退出了,并且内存管理也得知道某个进程开始了,这样才能给他们及时的分配资源和回收资源,当系统中存在大量的进程时这样强关联的管理会让整个系统效率低下,有了进程地址空间,内存管理负责为可执行程序开辟内存空间,当一个进程需要读写物理内存的时候,通过页表映射过去即可,内存管理就只需要知道哪些内存区域(配置)是无效的,哪些是有效的即可,当一个进程退出时,它的映射关系也就没了,此时没有了映射关系,物理内存这里就将该进程的数据设置为无效。

综上所述:因为地址空间的存在将内存管理和进程管理进行解耦合,用户层申请的空间其实是在地址空间申请的,物理内存可以在短时间内不给你分配空间,而当你真正对物理空间进行访问时,操作系统才实行相关算法帮助用户建立映射关系。这是一种内存的延迟分配策略,可以提高整机效率。

3.地址空间可以将内存分布有序化

我们有没有想过一个问题,CPU是如何获取我们可执行程序中第一条代码的地址?其实地址空间内可以为每一个进程的初始代码段均划分统一的地址分区,在每个进程各自的mm_struct中有统一的start开始,这样CPU就可以快速的获取每个进程初始代码的地址。结合上述问题,我们从进程的视角出发,进程内部所有的数据分布,在地址空间里均可以是有序的,尽管他映射出的物理内存可能是无序的

综上:因为有地址空间的存在,每一个进程都认为自己有4GB的全部空间,而且各个区域都是有序的,进而可以通过页表映射到不同的区域,即可证明进程的独立性。

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