[Linux] Linux 初识进程地址空间 (进程地址空间第一弹)

水墨不写bug 2024-10-09 16:07:03 阅读 63

标题:[Linux] Linux初识进程地址空间

个人主页@水墨不写bug

(图片来源于AI) 


目录

一、什么是进程地址空间

二、为什么父子进程相同地址的变量的值不同

三、初识虚拟地址、页表


一、什么是进程地址空间

         其实,在很久之前,也就是去年的时候,我在《C语言:动态内存规划》(可转跳的这篇文章)一文中,就提到了内存分布:不同的变量类型会存储在不同的内存区域。

        但是这个内存真的是真正的内存吗?一个进程都有一个这样的内存区域吗?这样的内存区域是如何组织的?这是本文想要回答的问题。

        我们在动态内存规划中讲解过,C/C++语言的不同变量存在于不同的区域:

         但是,实际上我们对这个图片并没有非常只管的认识,我们只知道不同数据类型存储在不同的区域,我们甚至不知道这个整体的结构到底存储在哪里!接下来,一切都要从一个现象说起。


        我们在Linux下,可以使用系统调用fork()创建一个子进程

        子进程的会"继承"父进程的代码和数据,所以我们可以通过一个if条件判断把父进程和子进程将要执行的代码分开:

<code>#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

int g_val = 0;

int main()

{

pid_t id = fork();

if(id < 0)

//返回值小于0,表示创建子进程失败,打印错误信息后返回

{

perror("fork");

return 1;

}

else if(id == 0)

//child子进程只能执行这里的代码

{

printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);

}

else

//parent父进程只能执行这里的代码

{

printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);

}

sleep(100);

return 0;

}

        上述的代码就通过if判断,把子进程和父进程将要执行的代码分离开,这样就能单独编写父子进程需要执行的任务了。

        我们运行上面的代码,得到结果:

         我们发现,父子进程的全局变量g_val的值是相同的,地址也是相同的!这符合我们的预期,因为子进程会按照父进程为模板,父子都没有对这个变量做出任何修改。

        但是如果我们把这个代码稍微修改一下:(在子进程中故意修改全局的g_val变量

<code>#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

int g_val = 0;

int main()

{

pid_t id = fork();

if(id < 0)

{

perror("fork");

return 0;

}

else if(id == 0)

//child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取

{

g_val=100;

printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);

}

else

//parent

{

sleep(3);

printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);

}

sleep(100);

return 0;

}

        这个时候再运行程序,得到结果:

        这个结果发生了在语言层面上根本无法回答的问题:

同一个地址处的变量的值不相同!! 

二、为什么父子进程相同地址的变量的值不同

         其实,我们以前在C/C++中打印出来并看到的地址全都是虚拟地址,并不是真正的地址。

        我们可以推测:

        变量内容不一样,所以父子进程输出的变量绝对不是同一个变量。但地址值是一样的,说明,该地址绝对不是物理地址!

        我们其实访问的都是虚拟地址,对于物理地址,用户一律是看不到的,是由操作系统统一管理的。于是操作系统负责讲把虚拟地址转化为物理地址。

        具体如何操作呢?这就需要结合进程地址空间,同时,需要引入一个新的概念:页表。

三、初识虚拟地址、页表

         页表,浅层次的理解,就是一个能把虚拟地址映射到物理地址的表。

        操作系统通过PCB(也就是Linux的task_struct)来组织管理进程,进程地址空间本质是PCB内部的一个结构体,简单一点理解就是PCB内部含有维护本进程虚拟地址空间的信息

        在进程正常运行的情况下,PCB是链接在CPU的运行队列当中的

         这样,我们就对进程地址空间有了全新的直观的认识了。CPU会根据《Linux的O(1)调度算法来调度进程》(可转跳的这篇文章),这是之前的故事了,这里不再赘述。

         言归正传,页表是一个把虚拟地址映射到物理地址的结构:

         我们创建的全局变量g_val就存储在数据段,g_val自然会通过页表映射到一段物理地址:

        当我们fork创建子进程,子进程会有和父进程一样的代码和数据:

 

        但是,由于进程间具有独立性,所以注定子进程改变全局变量g_val不能影响到父进程:这就好比你写的代码遇到空指针挂了,你的VS不会跟着挂一样。

        于是,解决这一问题的方法:写时拷贝就会发生。

        当子进程想要该表父进程的g_val时,会发生写时拷贝,在物理地址空间上重新开辟一块空间,复制拷贝一份g_val专门给子进程使用,这在保证进程的独立性的同时也在最大程度上节约了空间资源。

         到这里,你也许就会明白为什么相同的地址的值不同了: 因为父子进程页表的内容是相同的。不同的是页表的映射关系不同了。


完~

未经作者同意禁止转载



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。