fork 后,父子进程的在内存中是怎样的?测试环境centos7 x64

写一个测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <unistd.h>

char g_ch = 'G';

int main() {
pid_t ret = fork();
if (ret == 0) {
// child process
printf("child pid=%d, g_ch=%c, &g_ch=%p\n", getpid(), g_ch, &g_ch);
} else if (ret > 0) {
// father process
printf("father pid=%d, g_ch=%c, &g_ch=%p\n", getpid(), g_ch, &g_ch);
} else {
// error
perror("fork");
}
sleep(1);

return 0;
}

输出:

1
2
3
4
[parallels@centos-7 vimExercise]$ ./forkAddressTest 
father pid=6431, g_ch=G, &g_ch=0x601054
child pid=6432, g_ch=G, &g_ch=0x601054
[parallels@centos-7 vimExercise]$

一毛一样,子进程把父进程fork了一份,就像从别人的GitHub仓库fork 下来一份。再试试修改下这个子进程中的变量……

一毛一样

修改后的代码:

1
2
3
4
5
if (ret == 0) {
// child process
g_ch = 'A';
printf("child pid=%d, g_ch=%c, &g_ch=%p\n", getpid(), g_ch, &g_ch);
}

程序输出:

1
2
3
[parallels@centos-7 vimExercise]$ ./forkAddressTest 
father pid=9357, g_ch=G, &g_ch=0x601054
child pid=9358, g_ch=A, &g_ch=0x601054

地址是一样的,但是变量内容不一样……诶?这个盒子里可以放下这两个东西吗?我觉得不行,这一个东西就把盒子占满了,另一个再放进去要么覆盖,要么就放不进去(除非可以压缩,手动滑稽)。

查阅资料:这个是映射出去的虚拟地址不是物理地址。两个不是同一个盒子,盒子里装的也是不同的东西。在C/C++ 程序中输出的地址全部都是虚拟地址,物理地址由OS统一管理。OS负责把虚拟地址转换成虚拟地址。

每次都需要物理地址和虚拟地址之间的转换,靠软件来实现转换逻辑效率是比较低的,在《计算机组成原理》和《微型计算机原理》这两门课中老师反复提到了地址之间的转换,是通过一种叫MMU的硬件设备负责地址之间的转换,硬件实现效率高。

内存管理模块负责控制进程如何访问物理内存资源。通过硬件内存管理系统(MMU)管理进程虚拟内存和机器物理内存之间的映射。每一个进程都有自己独立的虚拟内存空间,所以两个进程可能有相同的虚拟地址,但是它们实际上在不同的物理内存区域运行。MMU 提供内存保护,让两个进程的物理内存空间不互相干扰。内存管理模块还支持交换——将暂时不用的内存页换出到磁盘上的交换分区,这种技术让进程的虚拟地址空间大于物理内存的大小。虚拟地址空间的大小由机器字长决定。——Linux 概念架构的理解

在一个多任务操作系统中的每个进程都运行在它自己的内存“沙箱”中。这个沙箱是一个虚拟地址空间virtual address space,在 32 位的模式中它总共有 4GB 的内存地址块。这些虚拟地址是通过内核页表page table映射到物理地址的,并且这些虚拟地址是由操作系统内核来维护,进而被进程所消费的。每个进程都有它自己的一组页表,但是这里有点玄机。一旦虚拟地址被启用,这些虚拟地址将被应用到这台电脑上的所有软件包括内核本身。因此,一部分虚拟地址空间必须保留给内核使用。——Anatomy of a Program in Memory
Kernel/User Memory Split

画个图我就理解了,自己也画个滑稽。

自己画的滑稽


几个小时后的补充:这里再附上另外一张图,上图中稍微有一些错误,我搞清楚再总结一下。