【Linux】进程地址空间

目录

  • 前言
  • 1、程序地址空间
    • 1.1、验证程序地址空间的存在
    • 1.2、验证栈和堆的增长方向
  • 2、感知地址空间的存在
  • 3、进程地址空间概念
    • 3.1、进程地址空间与页表
  • 4、fork相关问题
  • 5、虚拟地址的作用

前言

本篇文章进行操作系统中进程地址空间的学习!!!


1、程序地址空间

我们曾经在C/C++所学的程序地址空间的分布图是真正的内存吗?

  • 程序地址空间不是真正的内存,它只是一个“虚拟内存”

  • 程序地址空间准确的说是:进程地址空间,这是OS的概念,真正的内存指的是“物理内存”

【Linux】进程地址空间_第1张图片

1.1、验证程序地址空间的存在

  • Linux环境下验证,其他平台可能做了安全转换,导致地址变化
[lyh_sky@localhost lesson13]$ cat process.c 
#include 
#include 

int un_g_val;
int g_val = 0;

int main(int argc, char* argv[], char* env[])
{
    // 代码/只读区
    printf("Code area adress:              %p\n", main);
    const char* p = "abcdef";
    printf("Code area adress:              %p\n", p);

    // 初始化/未初始化数据区
    printf("init global area adress:       %p\n", &g_val);
    printf("uninit global area adress:     %p\n", &un_g_val);

    // 堆区
    char* m1 = (char*)malloc(sizeof(char) * 100 );
    printf("heap area adress:               %p\n", m1);

    // 栈区
    printf("stack area adress:              %p\n", &m1);

    // 命令行环境变量区
    for (int i = 0; i < argc; ++i)
    {
        printf("command line adress:            %p\n", argv[i]);
    }
    for (int i = 0; env[i]; ++i)
    {
        printf("environ val adress:             %p\n", env[i]);
    }
    return 0;
}

通过验证可以看出程序地址空间是存在的!!!

【Linux】进程地址空间_第2张图片


1.2、验证栈和堆的增长方向

栈是向下增长的,而堆是向上增长的!!!

  • 堆栈是相对而生的!!!

  • 在定义函数时,会将参数和函数体的变量压入栈中,先定义的变量地址比较高

[lyh_sky@localhost lesson13]$ cat process.c 
#include 
#include 

int un_g_val;
int g_val = 0;

int main(int argc, char* argv[], char* env[])
{
    // 代码区/只读区
    printf("Code area adress:              %p\n", main);
    const char* p = "abcdef";
    printf("Code area adress:              %p\n", p);

    // 初始化/未初始化数据区 
    printf("init global area adress:       %p\n", &g_val);
    printf("uninit global area adress:     %p\n", &un_g_val);

    // 堆区
    char* m1 = (char*)malloc(100);
    char* m2 = (char*)malloc(100);
    char* m3 = (char*)malloc(100);
    char* m4 = (char*)malloc(100);
    printf("heap area adress:               %p\n", m1);
    printf("heap area adress:               %p\n", m2);
    printf("heap area adress:               %p\n", m3);
    printf("heap area adress:               %p\n", m4);

    // 栈区
    printf("stack area adress:              %p\n", &m1);
    printf("stack area adress:              %p\n", &m2);
    printf("stack area adress:              %p\n", &m3);
    printf("stack area adress:              %p\n", &m4);

    // 命令行环境变量区
    for (int i = 0; i < argc; ++i)
    {
        printf("command line adress:            %p\n", argv[i]);
    }
    for (int i = 0; env[i]; ++i)
    {
        printf("environ val adress:             %p\n", env[i]);
    }
    return 0;
}

【Linux】进程地址空间_第3张图片

如何理解static变量

  • 函数内定义的变量用static修饰,本质是编译器会把该变量编译进全局数据区中

2、感知地址空间的存在

从以下测试可以得出:

  • 从全局数据g_val的值和地址都相同,可以看出来

  • 当父子进程中没有人修改数据的时候,父子进程是"共享"该数据的

[lyh_sky@localhost lesson13]$ cat process.c 
#include 
#include 
#include 
#include 

int g_val = 100;

int main()
{
    pid_t id = fork();

    if (id == 0)
    {
        while (1)
        {
            printf("我是子进程: %d, ppid: %d, g_val: %d, g_val adress: %p\n\n", getpid(), getppid(), g_val, &g_val);
            sleep(3);   
        }
    }
    else
    {
        while (1)
        {
            printf("我是父进程: %d, ppid: %d, g_val: %d, g_val adress: %p\n\n", getpid(), getppid(), g_val, &g_val);
            sleep(3);
        }
    }
    return 0;
}

【Linux】进程地址空间_第4张图片


如果让父子进程中的一个进行写入,那么g_val的值和地址会发生什么变化呢?

  • 父子进程读取同一个变量(地址相同),但是后续地址没有改变的情况下,父子进程读取到的内容却是不同的!!!

  • 由此可以得出:在C/C++程序中使用的地址,绝对不是物理地址

  • 在C/C++程序中使用的地址是"虚拟/线性/逻辑地址"(后面证明)

  • 我们在用C/C++中所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理

  • OS必须负责将 “虚拟地址” 转化成 “物理地址”

注意:虚拟地址、线性地址和逻辑是完全不同的概念,在Linux下是一样的!

[lyh_sky@localhost lesson13]$ cat process.c 
#include 
#include 
#include 
#include 

int g_val = 100;

int main()
{
    pid_t id = fork();
    int count = 1;
    if (id == 0)
    {
        while (1)
        {
            printf("我是子进程: %d, ppid: %d, g_val: %d, g_val adress: %p\n\n", getpid(), getppid(), g_val, &g_val);
            sleep(3); 

            ++count;
            if (count == 3)
            {
                g_val = 200;
                printf("我是子进程,全局数据g_val已经被更改!!!\n");
            }
        }
    }
    else
    {
        while (1)
        {
            printf("我是父进程: %d, ppid: %d, g_val: %d, g_val adress: %p\n\n", getpid(), getppid(), g_val, &g_val);
            sleep(3);
        }
    }
    return 0;
}

【Linux】进程地址空间_第5张图片

为什么OS不让用户直接访问物理内存呢?

  • 内存是一个硬件,不能阻拦用户的访问,只能被动的进行读取和写入

  • 如果用户可以直接访问到物理内存,发生越界访问时,可能这个进程会影响到其他的进程

  • 如果越界访问到OS的内存时,对OS内存进行非法修改,会导致OS直接挂掉


3、进程地址空间概念

概念:

  • 每一个进程在启动的时候,OS都会给这些进程创建一个地址空间,该地址空间就是“进程地址空间”

  • OS是通过描述再组织来管理进程地址空间的,通过struct描述它,然后用数据结构管理起来

  • 进程地址空间其实是内核中一个数据结构(mm_struct)

进程地址空间需要通过页表将虚拟地址映射到物理地址,后面讲

【Linux】进程地址空间_第6张图片


进程的独立性:多进程运行,需要独享各种资源(包括内存),多进程之间互不干扰

  • 进程相关的数据结构之间是独立的,进程代码数据之间也是独立的

  • 所谓进程地址空间:其实是OS通过软件的方式,给进程提供一个软件视角,认为自己是独占系统的全部资源(内存)!!!

【Linux】进程地址空间_第7张图片


3.1、进程地址空间与页表

进程地址空间与页表的作用

  • 可执行程序在磁盘中,运行时会被加载到物理内存

  • 进程里面维护一个进程地址空间,进程地址空间会被加载到页表的左边(虚拟地址),然后通过映射转换成物理地址加载到物理内存中

【Linux】进程地址空间_第8张图片

内存分布图中有这么多区域,那么这些空间区域是什么呢???

【Linux】进程地址空间_第9张图片

Linux内核中mm_struct的源代码

【Linux】进程地址空间_第10张图片

程序是如何变成进程的???

  1. 程序还没被加载到物理内存时,程序内部就已经存在虚拟地址了(除了堆栈需要通过页表映射)
  • 程序在链接阶段时,程序需要通过地址找到库当中的代码或其他头文件产生关联
  1. 程序还没被加载到物理内存时,程序内部就已经存在空间区域了
  • 使用readelf 【-S】指令进行查看可执行文件的区域和地址

【Linux】进程地址空间_第11张图片

回答如何被加载到内存的过程:

  • 在对程序进行编译时,就认为程序是按0x0000 ~ 0xFFFF进行编址的

  • 加载到内存时,会将内存初始地址与程序的地址相加得到虚拟地址

  • 进程读内存拿到虚拟地址后,通过页表映射转化成物理地址,就能找到在内存对应的地址,继续执行

【Linux】进程地址空间_第12张图片

举个例子:

【Linux】进程地址空间_第13张图片


4、fork相关问题

首先说明:父子进程之间具有独立性,二个进程之间互不影响

为什么fork一个子进程,对子进程数据进行修改时,二个进程数据地址一样,值却不一样?

  • 一个进程加载到内存时,需要通过页表将虚拟地址映射转化成物理地址

  • 父子进程的代码是共享的,说明:父子进程在加载到内存时,映射的物理地址是一样的

  • 当子进程数据进行修改时,会影响父子进程有独立性的这一特征

  • 此时,子进程会在物理内存中创建一个新的地址,将数据和代码拷贝到里面(写时拷贝),然后让虚拟地址映射到新的物理内存中,因为只改了物理内存的地址,虚拟地址是不变的!!!

【Linux】进程地址空间_第14张图片

  • 上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!

fork有二个返回值,为什么他们的值都不一样呢?

  • fork内存,return会被执行两次,就是通过寄存器将保存的返回值写到接受返回值的变量中

  • 当pid_t id = fork()的时候,父子进程谁先返回一个值,谁就会发生写时拷贝,所以同一个变量会有不同的值,本质是因为父子进程中的虚拟地址一样,只是物理地址发生了改变!!!


5、虚拟地址的作用

为什么要有虚拟地址呢?

  • 让用户访问内存添加了一层软硬件层,可以对转化过程进行审核,非法的访问,可以直接拦截它

  • 保护内存:用户进行非法访问,可能导致OS直接挂掉或影响其他的进程

  • 进程管理:通过地址空间,进行功能模块的解耦

  • 让进程或者程序可以以一种统一的视角看待内存(独占全部内存资源)

  • 方便以统一的方式来编译和加载所有的可执行程序,简化进程的设计与实现

你可能感兴趣的:(Linux,linux,java,c++)