Linux系统-进程地址空间

2022-11-30 12:59:39 浏览数 (1)

Linux进程地址空间

  • 零、前言
  • 一、程序内存空间
  • 二、进程地址空间
    • 1、引入及概念
    • 2、进程地址空间
    • 3、相关问题

零、前言

本章主要讲解学习进程地址空间的知识

一、程序内存空间

在学习C/C 中我们知道了程序内存的空间开辟以及内存分区的基本概念

  • 示图:
  • 各分区作用:

  1. 内核空间:用户代码无法读写
  2. 命令行参数环境变量:储存命令行参数环境变量
  3. 栈区:存放运行函数而分配的局部变量、函数参数、返回数据、 返回地址等,栈区地址向下生长
  4. 共享区:储存文件映射,匿名映射,动态库
  5. 堆区:存放动态分配的变量,堆区地址向上生长
  6. 数据段(初始化数据/未初始化数据区):存放全局变量、静态数据
  7. 代码区:存放函数体(类成员函数和全局函数)的二进制代码
  • 代码验证示例:
代码语言:javascript复制
#include<stdio.h>
#include<stdlib.h>
int g_unval;
int g_val=1;
int main(int argc,char* argv[],char* env[])//命令行参数以及环境变量
{
    printf("code addr:%pn",main);//代码区
    char* str="hello world";
    printf("read only addr:%pn",str);//只读常量区:str是常量字符串的地址
    printf("init addr:%pn",&g_val);//初始化数据区
    printf("uninit addr:%pn",&g_unval);//未初始化数据区
    int* p1=(int*)malloc(10);
    int* p2=(int*)malloc(10);
    printf("heap addr:%pn",p1);//堆区
    printf("heap addr:%pn",p2);//堆区向上生长
    printf("stack addr:%pn",&str);//栈区
    printf("stack addr:%pn",&p1);//栈区向下生长
    for(int i=0;argv[i];i  )
        printf("args addr:%pn",argv[i]);//命令行参数区 
    for(int i=0;i<2;i  )
        printf("env addr:%pn",env[i]);//环境变量区
    return 0;
}
  • 结果:

二、进程地址空间

1、引入及概念

对于上述的程序地址空间,其实它的真实面貌为进程地址空间,对于进程地址空间本质上来说是一个虚拟地址空间,并非真实的物理空间

  • 示例:
代码语言:javascript复制
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
    int i=10;
    pid_t id=fork();//创建子进程
    if(id<0)
    {
        perror("fork failn");
        return 1;
    }
    else if(id==0)
    {
        //child
        int cnt=0;
        while(1)
        {
            printf("I am child:  i:= &i:%p pid:%dn",i,&i,getpid());
            sleep(1);
            if(cnt==3)//修改i的值
            {
                i=100;
            }
            cnt  ;
        }
    }
    else
    {
        //father
        while(1)
        {
            printf("I am father: i:= &i:%p pid:%dn",i,&i,getpid());
            sleep(1);
        }    
    }
    return 0;
}
  • 结果:
  • 分析:

  1. 我们知道,父进程创建子进程时,子进程以父进程为模板构建进程,代码父子共享,数据各有一份(谁进行写入谁发生拷贝)
  2. 而我们发现子进程数据发生修改时,子进程数据的地址和父进程的地址一样,没有发生改变
  3. 对于变量内容不一样,但地址值是一样的,说明该地址绝对不是物理地址,因为是物理地址根本不会有这种事发生

2、进程地址空间

  • 概念:

  1. 在Linux地址下,这种地址叫做 虚拟地址,我们在用C/C 语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理,OS必须负责将 虚拟地址 转化成 物理地址
  2. 进程地址空间本质是进程看待内存的方式,抽象出来的一个概念,对于每个进程来说,系统会给他们创建对应的PCB进程块结构体,同时也相应的分配了对应的mm_struct进程地址空间(PCB中储存了该进程对应的进程地址空间的地址),也就是每个进程都认为自己独占内存资源
  3. 对于进程来说,进程控制块以及进程地址空间以及相应的资源,随进程的创建而创建,随进程的退出而回收
  • 进程地址空间的内容:

  1. 进程地址空间是由0x00000000到0xffffffff的线性地址空间,按照刻度被划分为各个区域,例如代码区、堆区、栈区等
  2. 在结构体mm_struct当中,便记录了各个边界刻度(对于堆向上增长以及栈向下增长实际就是改变mm_struct当中堆和栈的边界刻度)
  • 示图:

注:在结构体mm_struct中各个刻度之间的每一个刻度都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系

  • 程序执行流程:

程序运行,进程被加载到CPU上,系统在内核为进程创建PCB记录进程属性,分配进程空间地址,由页表构建虚拟地址与物理地址的映射关系,程序查找或者修改数据会通过PCB找到对应的进程地址空间,再由进程地址空间上的虚拟地址由页表找到物理空间上分配的数据

  • 示图:
  • 对于父子进程变量地址相同数据不同:

父进程创建子进程时,子进程以父进程为模板构建进程,代码数据父子共享,当子进程进行修改数据时,由页表发现该数据是父子进程共享的,所以系统会找到另一个物理空间进行拷贝数据,拷贝数据后再修改数据,达到数据各有一份互不干扰的目的

注:这种在需要进行数据修改时再进行拷贝的技术称为写时拷贝

  • 示图:

3、相关问题

  1. 为什么数据要进行写时拷贝

进程需要保证独立性,多进程运行,需要独享各种资源,多进程运行期间互不干扰,数据写实拷贝让子进程的修改不影响到父进程

  1. 为什么不在创建子进程的时候就进行数据的拷贝

  1. 子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间
  2. 如果fork函数在子进程创建的同时即创建对应的数据结构还要拷贝数据的话,会降低fork的效率
  3. fork就是在向系统获取资源,如果再拷贝的话,即获取更多的资源,容易造成fork失败
  4. 代码会不会进行写时拷贝

90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝

  1. 为什么要有进程地址空间

  1. 保护物理内存,不让程序直接进行访问物理地址,方便进行合法性校验,控制以及管理了访问的权限

如果可以直接访问,那么看到的地址就是物理地址,对于野指针,越界访问等问题则不能进行很好的控制,不能保证程序的独立性;当通过物理地址暴露,恶意程序通过物理地址进行读取或者修改数据,无法保证信息和数据安全;控制以及管理了访问的权限,以常量区不能的常属性来说,当常量定义出来的时候不就是修改数据了么,但是再次修改时,通过页表访问时,页表发现是常量区数据则拒绝修改的访问,以此保护了数据的常属性

  1. 将内存管理与进程管理进行解耦

如果直接使用物理地址,那么进程一创建就需要立即将数据写到物理内存中,当进程退出就需要将数据立即释放,也就是说内存的管理需要特别关注进程的状态,这之间具有强相关性(耦合度高);具有进程地址空间后,进程管理只需管理PCB以及进程地址空间,而内存管理只需管理物理地址空间,也就是内存管理只需要通过智能指针知道内存区域那些是有效的哪些是无效的就能管理好内存,实现了进程管理与内存管理的解耦

  1. 让程序以同样的方式看待代码和数据

可执行程序实际上也被分为了各个区域,例如初始化区、未初始化区等(便于程序编译时进行查找和链接)。当该可执行程序运行起来时,操作系统则将对应的数据加载到对应内存当中即可,同时分区有利于执行的效率,大大提高了操作系统的工作效率。

0 人点赞