进程地址空间

目录

回顾C/C++语言的程序地址空间

 感性认识虚拟地址空间

虚拟地址空间与物理空间如何建立映射关系

为什么要虚拟地址空间?


回顾C/C++语言的程序地址空间

在学习C/C++语言时我们知道了一个概念叫程序地址空间。通俗来说就是如下一张表,从中可以得知系统的几个区域:

 现在有个问题,这个表是内存吗?来看个例子就知道了:

 可以看到,一开始父进程和子进程对应打印的全局变量数值和地址都一样,奇怪的事情出现了:当全局变量的值在子进程中被改变后,父进程的变量地址和子进程的地址还是同一个,但是两个的值不一样!

这是不可想象的,之前讲进程的时候说了,进程具有独立性,多进程下进程间是互不干扰的,即使共用代码但也会根据条件判断语句分开执行。       一个物理地址对应的数据流只有一个,对应的值也只有一个,但是这里确实出现了一个地址对应两个不同值的情况。这只能说明一个事实:这里的地址不是物理地址!

这里的地址叫做虚拟地址,也叫线性地址或逻辑地址。这就回答了上面的问题,那不是物理意义的内存,而是虚拟内存,所以以前学语言的时候说的指针不是物理地址,而是虚拟地址。

 感性认识虚拟地址空间

进程在运行的时候会有一个错觉,它觉得它是独占CPU资源的,实际上在我们看来根本不是这样,CPU每时每刻都在调度不同进程,不断进行着切换。

根据这一点我举个例子帮助感性理解:

有个大帮派势力暗中统治着几座城市,帮主手下有两个心腹,但是他们俩互相不知道对方的存在。

为了让两个心腹好好干活别想着觊觎帮主之位,帮主对心腹A说,我死后这个集团就交给你打理了;同时,他也对心腹B说了一样的话,相当于分别给他们俩画了个大饼,而他们两人都不知道帮主和另一个人也许下了承诺。于是两人好好干,有一天A对帮主说想要更多实权,帮主说反正我快走了到时候一切都交给你,但你现在先别急,于是A愉快地干活去了,B也经历了同样的事。

这里帮主就好比操作系统,两个心腹A,B就是进程,两个进程以为自己独占着操作系统资源,操作系统为了更好地调度他们,给他们画了大饼,这张饼也就是”进程地址空间“,当进程想要向操作系统多要点空间资源时,操作系统不能一次全给它(本身内存吃紧要节省空间)就会分批一次一次给空间——对应我们malloc/new开空间。

那么操作系统是如何画饼的呢?————还是类比刚刚的例子,实际上画饼就是在人的脑中构建蓝图,可以看成一个数据结构:

struct 蓝图
{
    char* who;
    char* when;
    char* target;
};

那么操作系统也是一样,是对进程地址空间做管理,管理的本质是先描述再组织,描述也一样用struct结构体描述起来,地址空间的本质就是内核的数据结构mm_struct

地址空间上有heap,stack等区域,那么操作系统是怎么在地址空间上将它们描述起来的呢?————下面再举个例子:

帮主的帮派很大管理着几个城市,但是同等大小的势力也存在,并和该帮派对抗着。为了不引起大规模的乱斗,两个帮派划地分界,谁都不准越界半步。那么用数据结构进行区域划分是这样的:

struct area //区域
{
    unsigned int A_start; //帮派A区域起始位置
    unsigned int A_end;   //帮派A区域结束位置
    unsigned int B_start; //帮派B区域起始位置
    unsigned int B_end;   //帮派B区域结束位置
};

struct area partition = {1,50,51,100}; 
//帮派A区域 [1,50]
//帮派B区域 [51,100]

两个帮派定好地界后还是有问题,因为货物运输交易经常牵扯到边界附近地带,容易造成摩擦,所以经过商量将小镇T作为公共使用区域,两边都可涉足:

struct area partition = {1,45,55,100}; 

这是区域调整的方法。

类比以上,操作系统也是一个道理。

地址空间描述的基本空间大小是字节,32位下是2^32个地址,1个地址1字节,一共大概4GB空间。每一个字节都要有唯一的地址。

 如此一来,操作系统就可以根据划分好的地址描述heap等区域:

struct mm_struct
{
    unsigned int code_start;
    unsigned int code_end;
    unsigned int stack_start;
    unsigned int stack_end;
    .......
};

 假设mm_struct这个内核数据结构就是这样的,里面区域划分假定如上,总之默认占4GB大小。

code_start 到 code_end这段区域里面有很多地址,叫做虚拟地址。

想要调整区域,像例子中的设定公共区域,其实就是改变区域的起始地址和结束地址,比如要扩大stack区域,可以增加stack_end。我们写C/C++代码时,malloc/new空间实际上就是扩大堆区,free就是缩小堆区。

我们知道,进程在创建的时候操作系统会创建一个PCB内核数据结构task_struct,里面存了进程的相关信息如优先级...然后操作系统给进程 “画大饼” ,进程为了得到这块大饼,它的task_struct里面有个指针指向mm_struct。

虚拟地址空间与物理空间如何建立映射关系

这里牵扯到页表等相关概念,我们处理一下简单讲解。

我们已经知道磁盘上的程序会加载到内存,并且一字节对应内存上的一个物理地址空间。

 假设mytest.exe程序大小是8KB,内存起始地址是0x1111 1111,那么虚拟进程地址空间是如何与物理内存建立映射关系的呢?————系统中有一个东西叫页表,可以建立映射。

 关于页表,页等概念这里先不说,主要看进程地址空间。

虚拟地址空间的一个字节0xFF01 EEEC存到页表左边,与右边存物理地址空间的0x2100 1110对应。 假设我们写代码int a = 10; 这里&a就是虚拟地址,会对应虚拟进程地址空间的一个字节,再对应页表的数据,再与物理空间对应。当修改a的值,物理内存存储的值也被改变。

补充:

1、在Linux下,我们认为虚拟地址和线性地址是一个概念(在其它某些OS下不一样)。因为2^32个虚拟地址都是紧挨一起线性排布下去的。

2、再次画图理解整个过程

 每个进程都有自己的task_struct和mm_struct以及页表,当然物理内存只有一个,通过上述方式建立联系。操作系统OS操控管理着一切,给进程 “画大饼”,让进程看似好像独享操作系统资源,坐拥2^32字节大小空间,但其实进程无法直接查看物理内存大小和占用情况,只能通过   mm_struct ---> 页表---->物理内存的方式,而进程胃口也比较小,一次开个10MB.100MB很多了,再多OS也不会再给进程空间了。

为什么要虚拟地址空间?

1、虚拟地址空间可以保护物理空间不被错误修改,提高了系统的安全性。

试想一下要是没有虚拟地址空间,用户直接访问物理空间会发生什么。

假设你想修改一个数据,但是你访问错位置了,将一块重要的数据给修改了;或者你写的程序出现野指针等错误,容易造成系统的崩溃。

 刚刚提到的页表除了建立虚拟空间地址和物理内存的映射关系外,还有其他作用,其中之一是保护与拦截。

当进程做出违反操作系统的行为访问物理内存时,页表会进行拦截。因为进程不能直接访问物理内存,需要通过mm_struct--->页表,此时页表就可以拦截越界行为,保护物理内存。

 2、虚拟进程地址的存在,可以更方便的进行进程与进程数据代码的解耦,保证进程的独立性。

这段话很抽象,我们用例子来理解。还记得上面父子进程的那个程序吗,在子进程内部改变了全局变量globol_val,子进程和父进程打印的变量数值不同但虚拟地址相同。

 现在我们结合刚刚获取的知识再来深入理解一下为什么虚拟地址相同。

 系统生成父进程后,再生成子进程,子进程是以父进程为模板构建出来的,tast_struct , mm_struct,页表都一样。它们里面的全局变量globol_val的虚拟地址都依据映射指向物理内存的同一地址,所以一开始执行的时候打印的变量数值一样地址也一样。

当子进程内想要改变全局变量globol_val, 要经过虚拟地址、页表访问物理内存,此时操作系统会做一个工作——拷贝物理内存对应位置的数据给它另一块地址,然后更改页表映射,将子进程虚拟地址映射指向新的物理内存地址,最后更改新的globol_val的值。

为什么操作系统要做这一步?————因为进程具有独立性,当一个进程对被共享的数据进行修改时会破坏独立性,影响其他共享数据的进程,所以为了让进程改变数据的同时不破坏独立性,操作系统将该进程的页表映射更改,所以产生了虚拟地址相同,但对应值不同的现象。

这里的拷贝方式叫做写时拷贝,它可以让不同进程的数据分离。

3、 虚拟地址空间可以让进程以统一的视角来看待进程对应的代码和数据等各个区域,方便使用。

       编译器也是以同样的视角来编译代码(地址)。

问题:可执行程序内部代码有地址吗?————有。进入汇编可以查看代码对应的地址,并且在链接的过程中也需要代码地址与库链接。

既然可执行程序代码有地址,那这个地址是什么地址呢?是物理地址还是虚拟地址?————自然是虚拟地址,或者这里更准确的说应该叫逻辑地址。

磁盘上的可执行程序里面有一个main函数,其内部调用了fun函数,并且fun函数的地址是0x1122,main函数的地址是0xFEE0, 程序也有全局数据区,代码区等,32位平台下和上面讲的虚拟地址空间编址方式一样是以32位编址的。

可执行程序加载到内存,天然地就有了一个外部的物理地址,和程序内部编址方式一样的。在程序内部main函数寻址调用fun函数,外部物理内存也是一样通过物理寻址main函数调用fun函数。

所以我们现在有两套地址,标识物理内存中代码和数据的地址,还有在程序内部进行跳转的虚拟地址。

 内存将进程数据加载到CPU,CPU收到数据包括main函数和fun函数的地址(注意:此时CPU接收到的地址是虚拟地址!因为CPU读取的是指令,指令内部就有地址),PC指针记录下fun函数的地址,CPU先拿着main函数的虚拟地址,到页表查表映射物理内存,找到对应的main函数的物理地址然后执行main函数。

这里有个问题:CPU执行出来的地址是物理地址还是虚拟地址?————是虚拟地址,虚拟地址通过页表映射到物理内存。再调用main函数内部的fun函数,根据PC指针保存的fun函数地址,CPU再通过页表映射物理内存,此时CPU出来的还是虚拟地址。这样就形成了一个循环。

 CPU在这整个过程中没有见到过物理地址,全都是虚拟地址。

        

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值