【Linux进程】进程地址空间

目录

前言

1.程序地址空间初始理解

    1.1 各区域在内存中的划分

 1.2 Linux中父子进程的进程地址空间

 2. OS中的进程地址空间

 3. 虚拟地址空间设计的优点

 拓展

总结


前言

        早在学C语言阶段, 我们就时常听到栈区, 堆区, 常量区...等这些概念, 那他们到底是什么? 内存不是一大块空间吗? 为什么还要分区域, 我们在正常使用电脑时怎么没有注意过? 带着这些问题, 本文将为大家进行解惑;

在这里插入图片描述

1.程序地址空间初始理解

        大多数现代编程语言中都存在的内存管理概念。它们是用于描述程序在运行时如何分配和管理内存的区域,  在C/C++中尤其的明显; 

    1.1 各区域在内存中的划分

         根据前边C/C++的基础知识, 我们可以编写一个程序来验证一下:

int un_gval;
int init_gval = 100;
int main(int argc, char *argv[], char *env[])
{
    printf("code addr: %p\n", main);
    char *str = "hello Linux";
    printf("read only char addr: %p\n", str);
    printf("init global value addr: %p\n", &init_gval);
    printf("uninit global value addr: %p\n", &un_gval);
    char *heap1 = (char *)malloc(100);
    char *heap2 = (char *)malloc(100);
    char *heap3 = (char *)malloc(100);
    char *heap4 = (char *)malloc(100);

    static int a = 0;                    
    // static修饰前后对比: static修饰后本质变成了全局变量

    printf("heap1 addr : %p\n", heap1);
    printf("heap2 addr : %p\n", heap2);
    printf("heap3 addr : %p\n", heap3);
    printf("heap4 addr : %p\n", heap4);
    printf("stack addr : %p\n", &str); 
    printf("stack addr : %p\n", &heap1);
    printf("stack addr : %p\n", &heap2);
    printf("stack addr : %p\n", &heap3);
    printf("a addr : %p\n", &a);
    int i = 0;
    for (; argv[i]; i++)
    {
        printf("argv[%d]: %p\n", i, argv[i]); // 命令行参数
    }

    for (i = 0; env[i]; i++)
    {
        printf("env[%d]: %p\n", i, env[i]); // 环境变量
    }
    return 0;
}

 这里就不再演示执行结果, 感兴趣的可以去验证一下;

经过验证可以得出以下区域分布:

 数组在内存中如何存储?

 1.2 Linux中父子进程的进程地址空间

         使用fork创建进程, 子进程修改父进程中变量的数据, 根据前边的知识基础, 子进程修改数据时会进行写时拷贝, 我们可以编写一个小程序验证一下:

int g_val = 100;
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        //child
        int cnt = 5;
        while (1)
        {
            printf("child, Pid: %d, Ppid: %d, g_val: %d, &g_val=%p\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
            if (cnt == 0)
            {
                g_val = 200;
                printf("child change g_val: 100->200\n");
            }
            cnt--;
        }
    }
    else
    {
        //father
        while (1)
        {
            printf("father, Pid: %d, Ppid: %d, g_val: %d, &g_val=%p\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
        }
    }

    sleep(100);
    return 0;
}

 

         经过测试发现, 子进程修改了值以后父子进程值的地址依然相同, 相同的地址为什么会读取出不同的内容?

        至此可以说明: 我们平时在C/C++中看到的地址绝对不是物理地址! 而是虚拟地址;

 原理解释: 

         每个进程都会有自己的进程地址空间, 进程地址空间中的地址全部都是虚拟地址,  虚拟地址并不存储实际的数据, 数据实际存储在物理内存当中,  按照图中所示, 假设我们存储10这个数字,  在物理内存中有它自己的物理地址, 物理地址通过页表与虚拟地址建立连接, 形成映射结构;

        父进程在创建子进程时, 会将自己的数据拷贝一份给子进程(包含页表); 父进程与子进程虚拟地址相同, 页表相同, 最终就指向了相同的物理地址;

现在回到最初的问题: 为什么相同的地址会看到不同的值?

 当子进程对数据进行修改时:

         子进程对数据进行修改, 就会导致写时拷贝:  在物理内存中将数据拷贝一份, 然后将子进程页表中的物理地址修改为新的物理地址;

         在子进程修改数据时, 修改的就是新拷贝的数据,  这也就导致了为什么相同的虚拟地址看到的是不同的数据(页表中的虚拟地址与物理地址的映射发生了改变) ;

注意:

         这时的子进程与父进程依然共用同一份代码, 只有数据的映射发送变化;

 2. OS中的进程地址空间

         上述的结构是在单个进程中的结构, 那在OS中又是怎么表现的呢? 进程地址空间与OS的关系又是什么? OS如何管理这些进程的地址空间吗?

         可以这样理解, 假设OS就是一个大富翁, 而内存就算他的财富,  OS下运行的每个进程可以理解为是他的子女, 他们(进程相互独立)互相都不知道其他人的存在, 每个进程在执行任务时, OS会向进程分配空间; 

         大富翁(OS)的这些子女(进程)都认为自己将来可以得到所有财富(内存, 但实际上并不会) ,  这里涉及到页表映射机制, 本文不进行详细解释; 后边会进行介绍, 可以先这样理解;

        多个进程可以在OS下运行, 那么OS是如何管理这些进程地址空间的? 六字真言(先描述,再组织);

 所谓的地址空间在OS中最终一定是一个内核数据结构对象:

 结构复杂, 简化一下:

 

         而OS再将这些结构化对象组织成链表, 那么对这些进程地址空间的管理就变成了对链表的增删查改;

         在进程PCB中(进程控制块),  有着一个struct mm_struct* mm这样的指针, 这个指针会指向struct mm_struct对象, 这样进程就有了进程地址空间; 

 3. 虚拟地址空间设计的优点

  • 更加合理高效的使用内存

 任意一个进程, 都可以通过进程地址空间+页表, 将乱序的内存, 变得有序, 进行规划;

  •  访问内存的安全检查

         页表的结构其实还是比较复杂的, 它不仅可以映射内存, 还可以进行安全检查, 每个地址都有对应的标识字段, 标识该空间的访问权限, 以及这块空间是否分配(已使用) ;

  •  将进程管理和内存管理进行解耦

         把可执行程序从磁盘加载到内存当中, 可以是任意位置, 代码和数据也可以分开存储,  可以随意的加载, 有页表存在, 建立映射, 进程可以以统一的视角看待内存;

进程在运行时, CPU如何知道当前进程的页表呢?

         每个进程都有自己的页表,  进程在被CPU执行时,  CPU中有一个叫CR3的寄存器, 它记录着当前进程页表的地址;

注意:

        CPU中的CR3寄存器存储的是页表的物理内存;

         联系进程的切换: CPU在进行进程切换时, 会把CR3寄存器中进程的页表地址存储到进程的PCB当中(进程上下文);

  页表中有一个标记为, 记录内存空间是否分配, 为什么会有这个标志位?

        在现实使用中, 比如大一些大型游戏(可执行程序较大),  不可能一次性完全加载到内存当中,  它只会加载一部分, 在内存中先开一小块空间给进程, 然后通过页表映射; 当需要新的数据时, 就会把暂时不用的数据挂起, 加载到磁盘当中, 然后将需要的数据换入;

        如何判断数据已经被换出呢? 就需要这个标记位, 数据被换出, 就改为未分配状态; 如果页表中进程地址空间对应的标记位是未分配, 那么我们就可以认为当前进程被挂起;

 拓展

         进程在运行代码时, 只需要访问虚拟地址, 进程需要内存时, OS就会给进程申请, 然后将数据加载进来;

如果进程访问一块虚拟地址数据中, 该虚拟地址没有分配空间, 这时OS就会暂停进程:

  1. 在内存中开一块空间
  2. 把需要的代码和数据加载到内存当中
  3. 把对应的物理内存填充到进程的页表当中
  4. 将标志位进行修改(表示已分配)
  5. 继续进程执行

 总结:

        虚拟地址没有分配, OS暂停进程, 在内存申请空间, 将数据和代码进行加载, 修改页表, 这个过程就叫做缺页中断;

 在每个进程的mm_struct对象里还会有一个, vm_area_struct* mmap的结构体指针;

 该结构体对象的主要作用是对mm_struct分配的内存空间, 进行更细致的划分,  mm_struct对象中可以包含多个vm_area_struct对象,  每个vm_area_struct对象描述一块内存区域, 这些结构体对象按照地址的顺序连接起来, 形成一个链表结构, 虚拟内存区域链表;

 内存不是一大块空间吗? 为什么还要分区域, 我们在正常使用电脑时怎么没有注意过?


总结

        回到文章的开始,  在日常使用电脑时,用户通常与高层次的图形用户界面(GUI)交互,而不需要直接接触或管理内存的低层细节。这让许多人忽视了内存区域的划分和管理; 对于开发者和计算机科学学生来说,了解内存的划分和管理则是更为核心的知识,因为这直接影响到编写高效和安全的代码,  以上便是本文的全部内容, 希望对你有所帮助, 感谢阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值