文章目录
- 🌈 Ⅰ 虚拟地址引入
- 🌈 Ⅱ 虚拟地址空间
- 🌈 Ⅲ 页表 (解释 fork() 的返回值既 > 0 又 == 0)
- 🌈 Ⅳ 什么是地址空间
- 🌈 Ⅴ 为什么要有地址空间
🌈 Ⅰ 虚拟地址引入
- 现在通过一段代码来观察一个现象,定义了一个全局变量 g_val = 100,然后使用 fork 创建一个子进程,让父子进程各自完成任务,在子进程中定义 count 来计数,当子进程的打印任务执行到第五次之后,让子进程讲 g_val 的值改成 300。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int g_val = 100;int main()
{printf("father is running, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork(); // 创建子进程if (0 == id) // 执行子进程部分代码{ int count = 0;while (1){printf("child process, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);sleep(1); // 每间隔一秒打印一次count++;if (5 == count){g_val = 300;printf("child process, change %d -> %d\n", 100, 300);}}}else if (id > 0) // 执行父进程部分代码{while (1){printf("father process, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);sleep(1); // 每间隔一秒打印一次}}return 0;
}
-
在子进程修改 g_val 的值时,父子进程的 g_val 的地址时一样的,然而在子进程讲 g_val 的值改成 300 时,能够发现父进程的 g_val 的值依然是 100。
- 变量内容不一样,所以父子进程输出的变量绝不是同一个变量。
- 但子进程和父进程的 g_val 的地址是却相同,说明输出的 g_val 地址绝不是一个物理地址。在 Linux 地址下,这种地址被称作虚拟地址。
- 用 C/C++ 所看到的地址,全都是虚拟地址,物理地址由操作系统 OS 统一管理。
-
OS 负责将 虚拟地址 转化成 物理地址。
🌈 Ⅱ 虚拟地址空间
- 操作系统 OS 会为每一个进程创建一个地址空间,这是一个虚拟的地址空间。
🌈 Ⅲ 页表 (解释 fork() 的返回值既 > 0 又 == 0)
不对公有变量进行修改时
- 操作系统 OS 需要将虚拟地址转化为物理地址,OS 可以通过页表来将虚拟地址和物理地址建立映射关系。
- 建立了映射关系之后,上层在使用虚拟地址访问时,OS 会自动拿着虚拟地址查页表,然后转化成物理地址从而访问到代码和数据。
- 在上述代码的父子进程都有属于自己的虚拟地址空间以及页表,只不过公用同一个虚拟地址罢了。
- 在没有进程对 g_val 的值进行修改时,父子进程的页表映射的都是同一个 g_val 的地址。
对公有变量进行修改时
- 操作系统 OS 会先找到一块地址将原先的变量内容拷贝一份下来,然后再对该内容进行修改。这个因为要发生修改而拷贝内容的操作称为写时拷贝。
- 假设值为 100 的 g_val 的物理地址为 0x112233,值为 300 的 g_val 的物理地址为 0x123456 对应的映射关系如下图所示:
- 如上图所示:同一个变量,值不同却地址相同,其实是虚拟地址相同,内容不同是被映射到的物理地址不同!
- 所以这也就能解释为什么 fork() 返回的 id 值既可以 > 0,又可以 = 0。
🌈 Ⅳ 什么是地址空间
- 地址空间本质是内核的一个 struct 结构体,内部的很多属性都是表示 start 和 end 的范围。
- 每个进程都有一个地址空间,系统中可能存在多个进程,每个进程通过地址空间来访问内存,因此系统中存在着多个地址空间。地址空间需要被 OS 管理起来。
- 一旦涉及到了管理,那么就要先描述,再组织
1. 先描述
- 地址空间本身是一个结构体对象,在 Linux 中被称为 struct mm_strunt。
- 先用结构体描述出地址空间,该结构体包含着区域信息,用于实现地址空间区域的划分。
struct mm_struct
{......
};
2. 再组织
- 进程 PCB 里有一个指向 mm_struct 的结构体指针,来将 PCB 和 mm_struct 联系起来。
🌈 Ⅴ 为什么要有地址空间
-
如果没有虚拟地址空间,那么进程访问的就都是物理地址,每个进程都需要记录物理地址。
-
进程直接访问物理地址,在发生异常情况时,就有可能破坏其他空间的代码和数据。
-
以下是页表 & 虚拟地址空间存在的价值:
1. 将无序变为有序
- 经过页表的映射,将无序的物理地址变成有序的虚拟地址,以统一的视角看待物理内存,以及自己运行的各个区域。
- 不需要再关心正文代码、初始化数据、非初始化数据等在物理内存的哪个位置,这些东西永远在虚拟地址空间的固定位置。
2. 提高内存使用率
- 如果一个进程早期在物理内存申请了一块空间,但长时间不去使用,就会造成浪费。
- 在页表中预先弄好虚拟地址,但不实际开辟这块空间,等到需要用到这块空间时,再去物理内存中开辟,然后将开辟好的物理空间地址与预先弄好的虚拟地址 映射即可。
3. 拦截非法请求 - 保护物理内存
- 没有地址空间:进程就能够直接访问物理内存,没人能管着你,增加了发生异常情况的概率。
- 有了地址空间:一个进程就只会映射自己的数据 ,如果去越界访问别人的空间,那么在页表中是找不到对应的映射关系的,OS 会直接进行终止。也就是说,只要进入到访问物理内存阶段,那么一定是安全的。