0


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

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

个人主页@水墨不写bug

(图片来源于AI)


一、什么是进程地址空间

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

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

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

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

    我们在Linux下,可以使用系统调用**fork()创建一个子进程**:![](https://i-blog.csdnimg.cn/direct/7b972199743a45da97baa516b34ecf3a.png)![](https://i-blog.csdnimg.cn/direct/e25950d99eb04b84bc643f3667fcaf0e.png)

    子进程的会"继承"父进程的代码和数据,所以我们可以通过一个if条件判断把父进程和子进程将要执行的代码分开:
#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变量**)
#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的运行队列当中的**:![](https://i-blog.csdnimg.cn/direct/9752c46f6cff475ea1284afb00f0c3f5.png)         这样,我们就对进程地址空间有了全新的直观的认识了。CPU会根据《Linux的O(1)调度算法来调度进程》(可转跳的这篇文章),这是之前的故事了,这里不再赘述。

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

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

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

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

    于是,解决这一问题的方法:**写时拷贝**就会发生。
   **当子进程想要该表父进程的g_val时,会发生写时拷贝,在物理地址空间上重新开辟一块空间,复制拷贝一份g_val专门给子进程使用,这在保证进程的独立性的同时也在最大程度上节约了空间资源。**![](https://i-blog.csdnimg.cn/direct/0c9904843f194d62bdf56d1890bb1299.png)         到这里,你也许就会明白为什么相同的地址的值不同了:**因为父子进程页表的内容是相同的。不同的是页表的映射关系不同了。**

完~

未经作者同意禁止转载

标签: linux 服务器 进程

本文转载自: https://blog.csdn.net/2301_79465388/article/details/142725787
版权归原作者 水墨不写bug 所有, 如有侵权,请联系我们删除。

“[Linux] Linux 初识进程地址空间 (进程地址空间第一弹)”的评论:

还没有评论