进程的地址空间

前言:

​ 还记得我们在讲解fork函数的返回值时,发现了一个函数竟然返回了两个不同的变量,当时我们难以理解,本章学习完后就会有更深刻的理解。

地址空间的引入:

来看以下代码:

	 1	#include <stdio.h>
     2	#include <unistd.h>
     3	#include <sys/types.h>
     4	
     5	
     6	int g_val = 100; // 定义全局变量
     7	
     8	int main()
     9	{
    10	    printf("father is running, pid: %d, ppid: %d\n", getpid(), getppid());
    11	    pid_t id = fork();
    12	    if(id == 0)
    13	    {
    14	        // child
    15	        int cnt = 0;
    16	        while(1)
    17	        {
    18	            printf("This is child process, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
    19	            sleep(1); 
    20	            cnt++;
    21	            if(cnt == 5)
    22	            {
    23	                g_val = 300;
    24	                printf("child process modify g_val from 100 -> 300\n");
    25	            }
    26	        }
    27	    }
    28	    else
    29	    {
    30	        // father
    31	        while(1)
    32	        {
    33	            printf("This is father process, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
    34	            sleep(1);
    35	        }
    36	    }
    37	    return 0;
    38	}

最后的运行结果如下图所示: image-20240813221105769

​ 我们会惊讶的发现,同一个变量在两个不同的进程中竟然会有两个不同的值!更何况地址还是同一个地址,这就有点奇怪了?这就是我们接下来要讨论的地址空间!

尝试理解地址空间:

​ 我们在之前学习C语言和C++的时候,我们或多或少也会见过各个区域在内存中的划分,比如栈区、静态区、堆区等等。
image-20240813222443261

​ 上图相信我们并不陌生,我们曾经在学习那些关于内存开辟的相关知识例如malloc和new的时候,都会介绍我们是从内存中开辟空间的,而在学习指针的时候好像潜移默化的认为一个地址就会对应一个值,那么我们又该如何去理解内存中的分布呢?

  • 首先我们需要再重新认识一下内存的分布和页表映射关系。

    我们对于进程的理解永远都是:进程 = 内核数据结构(task_struct) + 代码和数据

    [!Tip]

    首先我们打印出来的g_val的地址,本质上并不是真实的物理地址!我们上述所讲解的那些栈区、堆区之类的这些其实是操作系统内部特有的地址空间,里面对应的地址都为**虚拟地址,虚拟地址通过页表查找映射关系,在内存中找到自己真是的物理内存**。我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理 。

    可以认为进程PCB中的内存指针指向的虚拟地址,这些虚拟地址用于在物理内存和进程之间建立映射,从而实现进程的内存管理和访问控制此处我们需要画图来进行详解:
    image-20240813235001888

    地址空间的本质就是内核中的一个结构体对象。我们在学习C++理解各个变量存在于栈区还是堆区,这些区域其实就是操作系统的内部空间。咱们通过取地址(&)变量得到的地址,其实是虚拟地址

  • 尝试理解父子进程处理同一地址不同数据

    现在我们理解了虚拟地址空间与真是物理空间存在页表映射关系,一个进程就存在一个特有的地址空间,而地址空间上面的地址属于==虚拟地址==,所以其实上述代码运行结果那两个相同的地址,其实就是虚拟地址相同而已,那对应的真实物理地址又该如何分布呢?如下所示:

    [!TIP]

    子进程是会继承父进程的代码和数据,因此父进程的地址空间和页表也会被子进程所继承,当然虚拟地址在页表中的映射关系也会被继承。(子进程会把父进程额外很多内核数据结果全拷贝一份)
    但当子进程进行写入修改操作时,OS会自主发生==写实拷贝==,会给子进程修改的数据的物理地址再分配一个物理地址,并且将修改的值存放在新的物理地址中,并且页表的映射关系也会发生变化。
    写实拷贝与在学习c++的深拷贝很相似

    image-20240814120136813

细节问题

  1. 进程之间具有独立性,这就是为什么代码会出现同一块空间两个数据

  2. 可不可以把数据在创建子进程的时候,全部给子进程拷贝一份?

    ——不可以!父进程的地址空间中,对应图环境变量大概率是不会动的,因此全部拷贝的数据量 > 按需拷贝的数据量

  3. 地址空间本质是内核的一个struct结构体,内部很多属性都是表示start和end的范围

    当一个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也随之被创建。操作系统可以通过进程的 task_struct 来找进程对应的 mm_struct 。

为什么要有地址空间?

  1. 将无序变成有序,让进程以统一的视角看待物理内存以及自己运行的各个区域
  2. 进程管理模块和内存模块进行解耦
  3. 拦截非法请求——>对物理内存进行保护
    image-20240814124451633

如何进一步了解页表和写实拷贝?

  • 页表的管理(存在权限问题的处理)

    image-20240814124840892

  • 写实拷贝的理解
    image-20240814125422590
    在这里插入图片描述

    • 地址空间和页表从哪里来的?

      小问题:对于我们在编写代码时所创建的变量名和函数名,到最后形成可执行程序时这些名字还存在吗?
      答案:不存在了,此时已经是对汇编语言处理成二进制语言了。具体可以输入指令:objdump -S test.exe > test.s 进行反汇编操作查看代码。此时会发现变量名和函数名均已被置换成一个一个的地址
      image-20240814132126463

      所以,程序内部本身就有地址(虚拟地址)。所以地址空间和页表里的地址,都是由可执行程序里的地址提供的。(这里只是粗略的了解一下,以后还会再解释)。

Linux是如何真正进行调度的?(了解)

​ Linxu系统中每一个CPU都有一个运行队列(runqueue)

image-20240814132336048

image-20240814140723099

image-20240814140737810

image-20240814140806292

总结

​ 现在我们已经了解关于地址空间的基础知识了,对于同一个地址但不同的值我们也清楚其本质是什么原因了,所以我们可以理解了为什么fork函数具有两个返回值了,对于同一个地址但不同的值我们也清楚其本质是什么原因了,所以我们可以理解了为什么fork函数具有两个返回值。
image-20240814141156310在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无双@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值