Linux进程——程序地址空间详解

程序地址空间

我们之前学习内存的时候,有说内存的分布大概是这样的

111b3a1ff461945fc4c793a9c6ee813e.png

其中堆由下而上,栈由上而下

除此之外,实际上C/C++的常量字符串处于正文代码区,因此常量字符串是不允许修改的

其次虽然栈是由上而下的,但是数组和结构体的地址是从下而上开辟的,简单说就是a[0]的地址比a[1]的地址低

地址空间与物理内存

我们考虑下面这样的代码

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <stdlib.h>    
int main()    
{    
    int id=fork();    
    int tmp=10;    
    if(id==0)//子进程执行的代码    
    {    
        tmp=20;    
        while(1)    
        {    
            printf("子进程,tmp: %d,&tmp: %p\n",tmp,&tmp);    
            sleep(1);    
        }    
    }    
    if(id>1)//父进程执行的代码    
    {    
        while(1)    
        {    
            printf("父进程,tmp: %d,&tmp: %p\n",tmp,&tmp);    
            sleep(1);    
        }    
    }                                                                                                                                                                   
    return 0;    
}    

简单说就是先开一个进程,分别声明一个tmp,再父子进程分别打印tmp的值和地址

image.png

这里我们发现很奇怪,父子进程的值不同,因为我们改了,但是为什么地址却相同呢,同一个地址还能存两个不同的值吗

如果按照我们之前所学理解这样肯定是不行的,非要说的话,那也只是这两个值相同,实际存储的地方一定不同

出现这样的现象主要是因为这样的地址信息并非真正的物理地址,而是我们所说的程序地址空间的的地址

什么是程序地址空间

我们刚刚已经知道了,程序显示给我们的地址并非真正的物理地址,其实是虚拟地址,也称为线性地址

直接扯概念就太难受了,我们形象的来解释

操作系统是内存资源的管理者,而每一个进程都想获取操作系统手上的内存资源

但是操作系统又不好提前设定每个进程的内存大小,就只好告诉他们,我目前有32G(全部并有限)的内存,你可以来申请

这里的32G就是全部的物理内存,但是有那么多进程,不可能每个都有32G,那明显不够分的,但是每个进程又以为自己有32G

这就像老板给你画的饼,你好好努力就可以年薪百万,事实上并没有

所以每一个进程都有自己的程序地址空间,都认为他拥有全部的物理地址

管理程序地址空间

我们说管理一定是先描述后组织的

操作系统想要管理这个程序地址空间也一定是基于这个逻辑的

那么本质上来说,程序地址空间也就是一个struct结构体

111b3a1ff461945fc4c793a9c6ee813e.png

也就是用一个结构体管理这么一个空间

也就是开辟一整个大空间,确定每一个分区的起止位置,然后分别用指针管理就行

那么大概就是长这个样子的

struct addr_room
{
	int code_start; // 代码区起始
	int code_end; // 代码区结束
	int init_start; // 初始化区起始
	int init_end; // 初始化区结束
	int heap_start; // 堆区起始
	int heap_end; // 堆区结束
	// ......
	// 其他属性
};

那么想要对应的分区大一点或者小一点,其实也就是让他们的起止位置进行变化即可

虚拟地址与物理地址的映射

那么我们了解了虚拟地址之后,知道了两个相同的地址变量是有可能的,但似乎还是没有理解到,所有虚拟地址的大小的和明明是大于物理地址的,这是怎么办到的呢

实际上在虚拟地址和物理地址之间并非一一对应的映射关系,而是通过页表的哈希映射关系,而且每一个进程都有一个对应的页表来进行映射,如果你访问非法地址,那么页表层就会阻止你进行访问了

那么问题就容易解释多了

当我们创建子进程之后,原来的子进程和父进程的数据是共享的,不光是数据共享,页表指向的内容也是相同的,当我们进行修改变量值的操作时,操作系统会重新为其开辟一份物理内存,并修改子进程页表映射的后半段内容

也就是说整个访问数据过程分为两步,第一步获取虚拟地址,第二步获取页表中对应虚拟地址的物理地址,这样就能取到对应的值了

而对于这两个进程来说,他们各自有两个页表,两个变量,只是这两个变量名称相同,通过查页表的地址才能获取这两个变量的值,他们的虚拟地址相同,物理地址不同

这也是操作系统写时拷贝的深层次逻辑

页表的结构及其作用

页表还有一个作用,他不仅仅是带了地址的映射关系,还带了每个地址的访问权限,具体起来就是读写权限

这也就能解释,为什么代码区的内存无法修改,常量字符串处于代码区,也无法修改,但是编译通过运行不通过,就是这个原因,因为操作系统不同意,在访问页表的时候,页表就给他拒绝了

这就给系统的安全性加上了一道锁

其次页表还有一个作用就是代替我们程序员进行数据的分类调序,在页表映射的时候,会将不同的数据类型进行划分,让其映射到物理内存中是一种相对有序的状态,这样做也可以节省内存资源嘛

程序地址空间的作用

  1. 保护物理内存
    这一点我们刚刚有进行说明,因为页表可以控制权限,让操作系统识别到非法操作
  2. 降低操作系统耦合度
    引入页表之后,物理内存所需要做的事情就只有提供地方存数据了,具体存什么,怎么存,都不关心,物理内存的分配和进程的管理几乎完全分开
  3. 保持进程的独立性
    这样做每一个进程都认为自己拥有一整块大内存,所有复杂的操作都由操作系统以及页表完成,进程只负责用,物理内存只需要提供地方
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

栖林_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值