【Linux】进程地址空间

一、什么是进程地址空间

我们在学习C/C++的动态内存管理的时候,通常会把地址空间划分为一下几个区域:

在这里插入图片描述

但是我们上面的地址空间是真正的物理空间吗,我们以下面的例子来验证:

#include <stdio.h>
#include <unistd.h>

int global_val = 100;

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        printf("fork error\n");
        return 1;
    }
    else if(id == 0)
    {
        int cnt = 0;
        while(1)
        {
            printf("我是子进程,pid:%d,ppid:%d | global_val:%d,&global:%p\n", getpid(),getppid(),global_val,&global_val);
            sleep(1);
            cnt++;
            if(cnt == 10)
            {
                printf("子进程已经更改了全局的变量啦.....\n");
                global_val = 300;
            }
        }
    }
    else
    {
        while(1)
        {
            printf("我是父进程,pid:%d,ppid:%d | global_val:%d,&global:%p\n", getpid(),getppid(),global_val,&global_val);
            sleep(2);
        }
    }
    
    return 0;
}

在这里插入图片描述

我们可以看到,当子进程修改了全局变量global_val的值之后,子进程和父进程的global_val的值不相同,这是正常的现象,因为我们知道进程具有独立性,不同的进程之间互不影响,也包括父子进程,但是我们发现,父进程和子进程的global_val的地址竟然是一样的。

这说明我们在上面程序中所得到的global_val的地址不是物理地址,因为在同一时间内一个物理地址中只能存储一个进程的数据,不同进程的不同数据不可能存在于同一个物理内存中,所以我们就可以进行大胆的猜测–上面我们所得到的地址不是物理地址。实际上操作系统会给每一个进程都创建一个独立的虚拟地址空间,然后通过页表将虚拟地址空间和物理内容进行一一的映射,我们只能得到虚拟地址空间中的虚拟地址,当我们修改地址中的数据时,操作系统会先通过也表找到对应的物理内存,然后修改物理内存中的数据

对于上面程序的现象,我们就可以进行解释了:子进程和父进程都拥有自己独立的进程地址空间,并且子进程的地址空间和页表都是从父进程哪里拷贝过来的,所以最开始的时候global_val是存放在同一块物理内存

现在子进程想要修改自己地址空间的global_val的值,当操作系统通过子进程的页表找到global_val的物理内存的时候,发现global_val是被两个进程所共享的,因为进程具有独立性,一个进程对共享的数据做修改,如果影响了其他进程,那么不能称之为独立性。所以为了保证进程的独立性,在任何一方尝试写入,操作系统先进程数据拷贝,更改页表映射,然后让进程进行修改global_val的值,这个过程称为写时拷贝

所以整个过程操作系统并没有影响/改变虚拟地址,只是改变了页表的映射关系,所以子进程和父进程global_val的虚拟地址相同,但是他们映射到各自的页表到的物理内存地址是不相同的,所以从物理地址取出的数据也是各不相同的

在这里插入图片描述

【注意事项】

1.在操作系统中,进程地址空间中的地址通常也被称为线性地址,因为它是按照比特位从全0到全1依次顺序编址的,磁盘程序的内部的地址通常被称为逻辑地址,在Linux中,虚拟地址,逻辑地址和线性地址是同一个地址

2.操作系统为每个进程都创建独立的地址空间就相当于给每个进程都画了一个“大饼”,即相当于告诉进程说“你享有计算机中的所有资源,,这些地址空间你都可以使用”,但是事实上,当我们的某一个进程一次性申请内存过大的时候,操作系统会拒绝进程的请求

进程地址空间的概念

进程地址空间是一个进程可用于存储其指令、数据和堆栈的虚拟地址空间,它由操作系统内核为每个进程分配并管理。该地址空间通常被划分为多个段,如代码段、数据段和堆栈段,每个段用于存储不同类型的信息。进程只能访问其分配的地址空间,而不会干扰其他进程的地址空间,这样可以保证各个进程之间的隔离性和安全性

【注意事项】

地址空间描述的基本空间大小是字节

32位下有2^32个地址

2^32* 1字节 = 4GB空间范围

每一个字节都要有唯一的地址

二、进程地址空间如何进行管理

操作系统会为每个进程都创建一个进程地址空间,但是操作系统内部存在着许许多多的进程,所以操作系统为了保证各个进程的正常运行,操作系统需要对每个进程地址空间进行管理。

我们知道操作系统对进程地址空间的管理,对于管理的本质是对数据进行管理,管理的方法是先描述再组织。对进程空间的管理也是如此,操作系统会使用一种内核数据结构对来地址空间进行管理,Linux中使用名为mm_struct的内核数据结构对其进行管理,操作系统会为每个进程创建一个mm_struct对象,然后通过管理结构体对象来简介管理进程地址空间:

Linux中 mm_struct的部分源码如下:

在这里插入图片描述

在这里插入图片描述

我们可以看到,进程地址空间其实也是进程属性的一种,我们可以使用进程PCB task_struct来找到管理进程对应的地址空间

进程地址空间如何进行区域划分和区域调整

我们知道进程地址空间被划分为许多区域,我们所熟悉的从低地址到高地址一次为常量区(代码段),未初始化全局数据区,已初始化全局数据区,堆区,栈区。但是操作系统是如何对这些区域进行划分的呢?操作系统是通过两个表示区域边界的变量start,end来维护一块区域的,比如:

struct mm_struct
{
    //uint32:32位系统下的无符号整形
    uint32_t code_start,code_end;
    uint32_t data_start,data_end;
    uint32_t heap_strat,heap_end;
    uint32_t stack_start,stack_end;
};

在Linux中,mm_struct中关于区域划分的部分源码如下:

在这里插入图片描述

当我们在C/C++程序运行的过程中有时候需要动态的申请或者释放开辟的空间,这个时候就要区域进行调整,此时操作系统调整mm_struct中维护该区域的start和end即可。

【总结】

heap和stack所谓的区域调整,本质就是修改各个区域的start or end

定义局部变量,malloc new堆空间–扩大栈区或者堆区

函数调用完毕,free delete–缩小栈区或者堆区

三、为什么会存在进程地址空间

现在我能已经知道了什么是地址进程空间,以及进程地址空间是如何进行管理的,但是为什么会存在进程地址空间呢?为什么我们访问数据的时候需要通过页表的映射对应的物理地址,才能对数据进行访问,这不是浪费时间吗,我们直接将数据存入物理内存,直接访问不就可以了吗。其实操作系统这样做有以下三点原因:

1.进程地址空间保证了数据的安全性

我们为每一个进程都创建一个进程地址空间,然后通过页表来映射对应的物理地址,这个就可以避免我们对某一进程的虚拟内存进行越界访问或者非法访问的时候就可以将其进行拦截,从而保证了内存中数据的安全性

2.进程地址空间可以方便的进行不同进程间代码和数据的解耦,保证了进程的独立性这样的特征

对于两个互不相关的进程来说,他们都拥有自己独立的进程地址空间和页表,页表会映射到不同的物理内存上,磁盘代码和数据加载到内存中的位置也不同,一个进程数据的改变不会影响另一个进程

对于父子进程来说,由于子进程mm_struct也页表是通过拷贝父进程得到的,所以二者指向同一块物理内存,共用内存中的同一块代码和数据,但是,当父进程或者子进程需要对共享的数据做修改的时候,会发生写时拷贝,一个进程对数据进行修改不会改变对其他进程,这样就保证了进程的独立性

3.进程地址空间让进程以统一的视角来看待磁盘代码和各个内存区域,使得编译器也能够以相同的视角来进行代码的编译工作,即规则是一样的,编译完毕之后直接使用

我们编写的程序,变成可执行的程序之后,在程序的内部就已经有了地址,这是因为对于磁盘中的程序以及编译器来说,编译器也是以进程地址空间的规则来进行编译的,所以磁盘中的可执行程序的内部也是有地址的,并且该地址为虚拟地址,所以,当我们的程序被加载到内存变成进程之后,不仅 程序中的各个数据会被分配物理地址,程序的内部也存在虚拟地址,使得CPU在取指令进行运算的时候,拿到的吓一跳指令的地址也是虚拟地址,这样CPU也可以以虚拟地址->页表->物理地址 的方式来统一执行工作

【总结】

编译器在编译我们代码的时候,就是按照虚拟地址空间的方式进行对我们的代码和数据进程编址的

程序加载到内存中,天然就具有了一个外部的物理地址

程序运行时有两套地址:1.标识物理内存中代码和数据的地址 2.在程序内部互相跳转的时候 --虚拟地址

四、进程地址空间区域的划分

进程地址空间严格的区域划分如下:

在这里插入图片描述

其中,我们代码段,以初始化数据区,未初始化数据区,堆区,栈区和命令行参数环境变量去统称为用户空间,在32未操作系统下,这部分空间占总空间的3/4,即3GB,剩下的1GB空间属于内核空间

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

椿融雪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值