【Linux】-- 程序地址空间

目录

程序地址空间

进程地址空间 - 虚拟地址空间

概念引入(浅)

初步理解结构

深入理解虚拟地址

为什么要有地址空间?

程序地址空间的角度理解挂起


程序地址空间

C/C++在Linux下的程序地址空间分布:

  • 栈向低地址增长,堆向高地址增长(堆、栈相对而生
  • 正文代码区包括其中的字符常量区,只能读不能写
#include<stdio.h>
#include<stdlib.h>

int un_g_val;
int g_val = 10;

int main(int argc, char* argv[], char* env[])
{
    printf("代码区:%p\n", main);      //代码区

    char* p = "hello world";         //字面常量
    printf("字面常量:%p\n", p);

    printf("已初始化全局数据区:%p\n", &g_val);    //已初始化全局数据区

    static int num = 10;
    printf("static修饰的局部变量:%p\n", &num);    //static
    printf("未初始化全局数据区:%p\n", &un_g_val); //未初始化全局数据区
    
    char* p1 = (char*)malloc(4);
    char* p2 = (char*)malloc(4);
    char* p3 = (char*)malloc(4);
    
    printf("堆1:%p\n", p1);         //堆
    printf("堆2:%p\n", p2);         //堆
    printf("堆3:%p\n", p3);         //堆
    printf("栈1:%p\n", &p1);        //栈
    printf("栈2:%p\n", &p2);        //栈
    printf("栈3:%p\n", &p3);        //栈

    for(size_t i = 0; i < argc; ++i)
    {
        printf("argv[%d]:%p\n", i, argv[i]);
    }
    for(size_t i = 0; env[i]; ++i)
    {
        printf("env[%d]:%p\n", i, env[i]);
    }
    return 0;
}

        在我们看来,父子进程是代码共享的,所以&g_val地址相同是可以理解的,父进程具有独立性,所以子进程的g_val改变父进程的g_val不变是可以理解点,但是结合起来看,具有相同地址的变量却是不同值,就很奇怪。

        其实,这里地址并不是物理上的地址,而是:虚拟地址(线性地址)。操作系统上的地址与编程语言上的地址是不一样的,不是一个理解。几乎所有有 “地址” 的概念的语言,其所谓的地址一定不是物理地址,而是虚拟地址。在操作系统上,接触不到物理地址,都是虚拟地址,这是操作系统自我的一种保护,防止空间被随意的更改。

        虚拟地址:计算机通过软硬结合的方案,创造出的地址空间的概念。

进程地址空间 - 虚拟地址空间

概念引入(浅)

初步理解进程地址空间是虚拟地址空间的意义。(计算机的演变)

        最初,其实并没有虚拟地址空间的概念,是CPU直接对物理地址空间(内存)操作。

由于物理地址空间就是自由的空间,并未对读写设置权限,也没有违规操作的处理:

  • 物理地址空间没有隔离,每一个进程都可以修改其他进程的数据,修改内核空间中的数据。
  • 由于是直接存储于物理空间内,所以进程数据必须统一规范性放置,会出现碎片化内存浪费。
  • 内存地址随机分配,程序运行的地址不确定,CPU需要查询消耗。

初步总结:我们需要一个处理方式,使得CPU运行进程的操作是规范的,安全的;对于物理地址空间中的进程管理是合理的。


这样就有了虚拟地址空间的概念。虚拟地址空间的存在就如同一幅图,一把尺

一幅图:

        直接给进程一个 0x0000……0000 到 0xffff……ffff 的空间,并把每个区域进行划分,将代码与数据 “画” 在图(虚拟地址空间)中。CPU也无需关注物理内存的存在,只需要根据 “图” 中的位置进行访问即可,剩下的转换交给操作系统,操作系统转换 “图” 中的虚拟地址变为物理地址,然后将数据输出给CPU。也就是说:每一个进程都有一幅针对它的 “图”,这副 “图” 是每个进程私有的,而在转换的过程中就是检测CPU操作是否规范的关键所在。

        CPU根据虚拟地址空间的位置向操作系统索要数据。

一把尺:

        对于虚拟的 0x0000……0000 到 0xffff……ffff 区域的划分,区域的划分就是begin与end:

初步理解结构

         操作系统将虚拟地址空间中的地址转换为物理地址是通过所谓的页表。

        CUP通过运行进程,以进程的task_struct结构题提取到虚拟地址空间中所储存的虚拟地址,该虚拟地址再以页表的映射关系找到物理地址,再以此到物理空间中读取数据。可以说整个过程CPU都是不知道物理内存的存在的,而一直认为虚拟地址空间就是数据真正处于的位置。

地址空间和页表是每一个进程都私有的一份,以每个进程的页表的映射关系就能做到:

  • 通过映射,虚拟地址与物理地址对应,就能做到,进程之间不会互相干扰,保证进程的独立性。
  • 只有页表中的虚拟地址,或者虚拟地址相对应的物理空间才能访问。就能提高安全性,防止出现野指针越界访问其他进程的问题。

解决前面的父进程与子进程具有相同的地址,却g_val值不同的问题: 

        fork之后,代码是父子共享的,所以子进程的地址空间与页表绝大多数数据都是复制的父进程的地址空间与页表数据,二者是一样的,前提是并未在子或父进程中更改数据。

        当我们更改子进程的g_val的数据时:

        操作系统,会以实时拷贝,开辟一个空间将g_val复制拷贝,改变子进程页表的映射关系改变成新开辟的空间,然后进行子进程的g_val的数据更改。我们表面上所看的地址只是虚拟地址,其实在物理内存中,有属于自己的变量空间,只不过在用户层使用同一个变量(虚拟地址)来标识。

深入理解虚拟地址

        当我们的程序在编译的时候(形成可执行程序的时候),或者说没被加载到内存当中的时候,程序内部其实就已经有地址了。

        地址空间不仅仅是操作系统需要遵守的,其实编译器也要遵守。即编译器编译代码的时候,就已经形成了各个区域、代码区、数据区……,并且采用个Linux内核中的一样的编址方式,给每一个变量,每一行代码都进行了编址。

        所以:程序在编译的时候,每一个字段其实早就已经具有了一个虚拟地址。

        其实这正是地址空间与页表,最开始的时候的数据来源。

为什么要有地址空间?

1、凡是非凡的访问或者是映射,操作系统都会识别到,并终止此进程。

        因为地址空间和页表是操作系统创建并维护的。这也就意味着凡是想用地址空间和页表进行映射,就一定要在操作系统的监管之下来进行访问。也便保护了物理内存中所有的合法数据,包括各个进程以及内核的相关有效数据。

更直接的说就是:有效的保护了物理地址。

  • 所访问的虚拟地址是页表未有的,即访问不合法。
  • 虚拟地址所访问的物理地址是页表未映射的,即访问不合法。
  • 页表中不仅有映射关系,更有读写权限。此也是自由的物理内存,却有只能读的代码区的关键。

2、因为有地址空间和页表的存在,所以在物理内存中,可以对未来的数据进行任意的位置加载。

        由于进程与内存的联系以页表分隔。对于虚拟地址在内存中的物理地址存储,是由页表的映射而联系的,所以内存管理模块与进程管理模块就完成了解耦合。

        所以如C/C++语言上的new、malloc申请空间的时候,本质就是在虚拟空间申请。因为对于物理空间申请了,但是无法立马使用,就会造成空间的浪费,而由于两个模块的解耦合,那么两个模块的联系也就是一个页表,对于物理空间并没有立马给与的必要。只需延迟分配的策略,就可以提高整机的效率。

        之所以可以延迟分配,本质上:因为有地址空间的存在。所以地址 “许诺” 给进程就可以了,上层申请空间就是在地址空间上申请,物理内存可以一个bit为都不给。进程既不是立马运行,那就延迟实现 “许诺” ,提高内存的使用。而当你真正的对物理地址空间访问的时候,才执行内存的相关管理算法,申请空间,构建页表映射关系)

        内存管理操作系统自动完成。用户、CPU、进程完全0感知。

3、根据2,因为在物理内存中理论可以任意位置加载,那么物理内存中的几乎所有数据和代码在内存中都是乱序的。

        因为页表的存在,它可以将地址空间上的虚拟地址与物理地址进行映射,所以可以做到在进程的视角中所有的内存分布,是有序的。

        重点:地址空间+页表 = 内存乱序分布变的有序化。

        不同进程的需要访问的物理内存中的代码和数据,是由页表规范化,映射到不同的物理内存中,进而可以做到进程的独立性。

        重点:地址空间+页表 = 内存乱序分布变的独立化。

        实现的有序化、独立化,更是由于地址空间的存在。因为每一个进程都认为自己有一个4GB空间(32位),进而有了虚拟地址分布的地址空间,并通过页表映射到不同的区域,实现有序化、独立化。

程序地址空间的角度理解挂起

        加载的本质就是创建进程,按照前面第3点,并不是必须把所有程序的代码和数据加载到内存中,并创建内核数据结构建立映射关系。在极端的情况下,甚至可能只有内核结构被创建出来,并未分配物理空间,这就是未了提高内存利用率进行的分批加载,同样的也有分批换出。

        当已被执行完的代码与数据,或者由于处于对于某种资源的等待导致的阻塞,也就是此进程短时间不会再执行了。进程的数据和代码就换出,也就叫做挂起。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

川入

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

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

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

打赏作者

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

抵扣说明:

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

余额充值