前言:
还记得我们在讲解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 }
最后的运行结果如下图所示:
我们会惊讶的发现,同一个变量在两个不同的进程中竟然会有两个不同的值!更何况地址还是同一个地址,这就有点奇怪了?这就是我们接下来要讨论的地址空间!
尝试理解地址空间:
我们在之前学习C语言和C++的时候,我们或多或少也会见过各个区域在内存中的划分,比如栈区、静态区、堆区等等。
上图相信我们并不陌生,我们曾经在学习那些关于内存开辟的相关知识例如malloc和new的时候,都会介绍我们是从内存中开辟空间的,而在学习指针的时候好像潜移默化的认为一个地址就会对应一个值,那么我们又该如何去理解内存中的分布呢?
-
首先我们需要再重新认识一下内存的分布和页表映射关系。
我们对于进程的理解永远都是:进程 = 内核数据结构(task_struct) + 代码和数据
[!Tip]
首先我们打印出来的g_val的地址,本质上并不是真实的物理地址!我们上述所讲解的那些栈区、堆区之类的这些其实是操作系统内部特有的地址空间,里面对应的地址都为**虚拟地址,虚拟地址通过页表查找映射关系,在内存中找到自己真是的物理内存**。我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理 。
可以认为进程PCB中的内存指针指向的虚拟地址,这些虚拟地址用于在物理内存和进程之间建立映射,从而实现进程的内存管理和访问控制此处我们需要画图来进行详解:
地址空间的本质就是内核中的一个结构体对象。我们在学习C++理解各个变量存在于栈区还是堆区,这些区域其实就是操作系统的内部空间。咱们通过取地址(&)变量得到的地址,其实是虚拟地址。
-
尝试理解父子进程处理同一地址不同数据
现在我们理解了虚拟地址空间与真是物理空间存在页表映射关系,一个进程就存在一个特有的地址空间,而地址空间上面的地址属于==虚拟地址==,所以其实上述代码运行结果那两个相同的地址,其实就是虚拟地址相同而已,那对应的真实物理地址又该如何分布呢?如下所示:
[!TIP]
子进程是会继承父进程的代码和数据,因此父进程的地址空间和页表也会被子进程所继承,当然虚拟地址在页表中的映射关系也会被继承。(子进程会把父进程额外很多内核数据结果全拷贝一份)
但当子进程进行写入修改操作时,OS会自主发生==写实拷贝==,会给子进程修改的数据的物理地址再分配一个物理地址,并且将修改的值存放在新的物理地址中,并且页表的映射关系也会发生变化。
写实拷贝与在学习c++的深拷贝很相似。
细节问题
-
进程之间具有独立性,这就是为什么代码会出现同一块空间两个数据
-
可不可以把数据在创建子进程的时候,全部给子进程拷贝一份?
——不可以!父进程的地址空间中,对应图环境变量大概率是不会动的,因此全部拷贝的数据量 > 按需拷贝的数据量
-
地址空间本质是内核的一个struct结构体,内部很多属性都是表示start和end的范围
当一个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也随之被创建。操作系统可以通过进程的 task_struct 来找进程对应的 mm_struct 。
为什么要有地址空间?
- 将无序变成有序,让进程以统一的视角看待物理内存以及自己运行的各个区域
- 进程管理模块和内存模块进行解耦
- 拦截非法请求——>对物理内存进行保护
如何进一步了解页表和写实拷贝?
-
页表的管理(存在权限问题的处理)
-
写实拷贝的理解
-
地址空间和页表从哪里来的?
小问题:对于我们在编写代码时所创建的变量名和函数名,到最后形成可执行程序时这些名字还存在吗?
答案:不存在了,此时已经是对汇编语言处理成二进制语言了。具体可以输入指令:objdump -S test.exe > test.s 进行反汇编操作查看代码。此时会发现变量名和函数名均已被置换成一个一个的地址
所以,程序内部本身就有地址(虚拟地址)。所以地址空间和页表里的地址,都是由可执行程序里的地址提供的。(这里只是粗略的了解一下,以后还会再解释)。
-
Linux是如何真正进行调度的?(了解)
Linxu系统中每一个CPU都有一个运行队列(runqueue)
总结
现在我们已经了解关于地址空间的基础知识了,对于同一个地址但不同的值我们也清楚其本质是什么原因了,所以我们可以理解了为什么fork函数具有两个返回值了,对于同一个地址但不同的值我们也清楚其本质是什么原因了,所以我们可以理解了为什么fork函数具有两个返回值。