Linux操作系统~什么是虚拟地址?深度剖析进程地址空间

目录

1.所以进程的地址空间是什么呢?

2.mm_struct内部有什么?

3.虚拟地址空间与物理内存如何关联

页表

4.为什么设计这样一个进程地址空间,不让程序直接访问内存

Q:为什么子进程修改值以后,地址还是相同?

Q:常量字符串为什么地址相同


1.堆是堆,栈是栈,堆栈是栈

2.经过验证,C/C++的程序地址空间就是如图所示,栈区是往低地址方向增长的。

        子进程中对val进行修改,会发生写时拷贝。从而拷贝一份数据到另外的地方,变量的值是不一样的,但是他们两个变量对应的地址却是一样的,说明这个地址不是真正的物理地址,而是虚拟地址。

 1.所以进程的地址空间是什么呢?

A:实际上是一个结构体,里面存放了一个进程不同分区的虚拟地址空间。(不是真实的物理空间)

        每个进程都有一个地址空间,那么操作系统要管理,所以要先描述后管理,所以为每个进程定义一个mm_struct存放该进程的地址空间(里面有各个分区的起始和结束位置,总大小等)。

        因为这个进程地址空间是属于这个进程的,在进程的task_struct里面会存放指针指向这个mm_struct,通过task_struct可以找到mm_struct


2.mm_struct内部有什么?

        有代码段,常量区,堆区,栈区的起始位置和结束位置等信息。

        虽然这里只有start和end,但是每个进程都认为mm_struct代表整个内存,且所有的地址都是0x00 00 00 00 (1共1字节)到0xFF FF FF FF,这些地址也就是虚拟地址

        每个进程都认为地址空间的划分是按照4GB空间划分的(即每个进程都认为自己有4GB的空间)但是实际上物理内存可能只有1G

struct mm_struct
{
	unsigned int code_start;
	unsigned int code_end;

	unsigned int init_data_start;
	unsigned int init_data_end;

	unsigned int uninit_data_start;
	unsigned int uninit_data_end;

	unsigned int heap_start;
	unsigned int heap_end;
	//......
}

3.虚拟地址空间与物理内存如何关联

        mm_struct相当于描绘了一个4GB的虚拟地址空间,里面对应的地址叫做虚拟地址。(mm_struct内部并不是真的有这么大的地址空间,而是用起始位置和结束位置来表示的)

        那么物理地址和虚拟地址如何映射到一起的呢? ————使用页表+MMU(一种硬件,称为内存管理单元,用于查找页表,一般集成在CPU中)

页表

        页表是操作系统为每个进程维护的一张表,存放着虚拟地址和物理地址(就像hash表),用于根据虚拟地址去找到对应的物理地址。(将虚拟地址映射到对应的物理地址)


4.为什么设计这样一个进程地址空间,不让程序直接访问内存

原因一:操作系统可以对进程进行风险管理(权限管理),保证物理内存数据安全

原因一:如果进程直接访问物理内存的话,无法保证物理内存中的数据安全,通过添加一层软件层,操作系统可以完成有效的对进程操作内存进行风险管理(权限管理)(比如通过页表转地址,你有没有权限访问这个区域里面的内容,有没有越界访问等),本质目的是为了保护物理内存以及各个进程的数据安全!

        你虚拟地址转换成物理地址的时候,是交给操作系统来转的,它就可以决定帮不帮你转,转完以后你有没有权限可以访问。

const char* str = "zebra"

*str = "hello";

e.g:像这个地方,我们无法对常量区的内容进行修改,实际上是因为OS给你的权限只有r权限,这个和页表也有关系,你在使用str指针的时候,实际上也是使用了虚拟地址,在通过页表转换为物理地址的时候操作系统就会发现你没有权限对常量区的内容进行修改,所以就拒绝你的这次修改。

原因二:屏蔽内存申请内存的过程,将普通进程读写内存和OS对内存进行管理在软件层面上进行解耦(申请的时候只是划分虚拟地址空间,访问内存时才真正开辟物理内存,你申请了空间不使用的话,操作系统可以把空间给其他人用,等你真的要使用了才为你开辟内存)

        在OS角度,如果空间立马给你,意味着整个系统会有一部分空间,本来可以给别人立马用的,现在却可能被你闲置着,造成了空间浪费。

        一开始进程申请了空间的时候,操作系统并不会马上将物理空间分配给进程,而是给进程划分了一片虚拟内存空间(比如在mm_struct中将heap_end+100),当进程真正要读取这段空间的时候,操作系统说等等(缺页中断),然后为进程开辟一片物理空间给进程使用(基于缺页中断进行物理内存申请)。

原因二:将内存申请和内存使用的概念在时间上划分清楚(申请的时候申请,使用的时候使用,等你使用了我才真的为你开辟内存,类似写时拷贝),通过虚拟地址空间,来屏蔽内存申请内存的过程将普通进程进程读写内存和OS进行内存管理操作,进行软件层面上的分离(解耦)

e.g.:比如现在内存已经满了,你还要内存,我操作系统仍然可以给你(虚拟),然后执行对应的内存管理算法释放一些内存,等到你要用的时候,我把空间给你

原因三:不同进程中每个区域的相对位置比较确定,CPU可以以统一的视角看待内存;程序的代码数据可以加载到物理内存的任意位置

如果没有进程的地址空间,CPU没法以统一的方式去寻找每个进程的起始位置。

e.g:比如CPU执行不同进程,每个进程一个main函数的话,CPU每次开始执行进程的时候只需要从虚拟地址0x1234开始执行,根据不同进程的页表可以找到对应进程main函数的物理地址(每个进程的页表中,虚拟地址都是0x1234),这样CPU执行多个进程的时候就很方便。

原因三:有了地址空间以后,站在CPU和应用层的角度,进程统一可以看做统一使用4GB空间,(1).而且不同进程中每个空间区域的相对位置(比如堆栈大概在哪个位置),是比较确定的,CPU可以以统一的视角看待内存,执行进程/读取数据的时候会方便许多。(CPU执行一个进程的时候,可以知道堆,栈,常量区大概在地址空间的哪个位置,因为每个进程的地址空间都是一致的)

对操作系统来说,(2).程序的代码和数据可以被加载到物理内存的任意位置,只需要通过进程的页表将其位置和对应的地址空间映射起来就行,大大减少了内存管理的负担


Q:为什么子进程修改值以后,地址还是相同?

因为这个地址是虚拟地址,子进程和父进程中这个变量的物理地址是不相同的。

        子进程的创建是以父进程为模板的,所以子进程的进程地址空间和页表也是继承的父进程的,所以g_val的虚拟地址都是一样的。

        子进程修改了g_val的值以后,发生了写时拷贝,为子进程重新开辟了一块物理空间,将父进程g_val的值拷贝进入这个物理空间,然后修改子进程页表中原来g_val的虚拟地址和物理地址的映射关系,将物理地址改为新开辟内存的地址。但是此时虚拟地址没有发生变化,从而也就发生了这种情况。

        代码共享一份的实现也很简单,只要将父进程和子进程页表中存放代码的虚拟地址映射到同一块物理空间即可。


Q:常量字符串为什么地址相同

char* str = "hello world";

char* p = "hello world";

  • hello这个字符串常量字符串,是只读的,所以操作系统只会保存一份,存放在常量区
  • 字符串常量是一个表达式,表达式的值就是这个字符串常量第一个字母对应的地址,这是一个虚拟地址,然后把这个地址给p和str
  • 所以最后p和str的值打印出来都是相同的,表示的都是string这个字符串的虚拟地址
  • 虽然p和str两个指针变量的值是相同的,但是他们的虚拟地址是不相同的

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值