本节内容只讨论32位地址空间
虚拟地址空间
讨论虚拟地址空间前,我们先来观看一个现象:
//演示代码
#include <stdio.h>
#include <unistd.h>
int g_val = 10;
int main()
{
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
return 0;
}
else if(pid == 0)
{
//child
printf("i am child, g_val=[%d], &g_val=[%p], pid=%d, ppid=%d\n", g_val, &g_val, getpid(), getppid());
}
else
{
//father
//g_val += 10;
printf("i am father, g_val=[%d], &g_val=[%p], pid=%d, ppid=%d\n", g_val, &g_val, getpid(), getppid());
}
return 0;
}
父进程未将g_val += 10时:
父进程将 g_val += 10;
看到结果我们非常困惑,怎么我父进程和子进程的变量地址一样,怎么值不一样???莫不是操作系统出问题了,给我反回了一个错误的答案。其实是由于我们存在进程虚拟地址空间导致的,我们拿到的地址是虚拟地址,并不是直接存变量的物理地址,物理地址是唯一的,保存的内容肯定不一样,所以本质上是由于父进程和子进程中g_val的物理地址映射不同,所以才会得到不同的结果。所以我们使用的 & 取得地址都是虚拟地址。
上图就形象的说明了这个映射过程,当 fork 完成之后,父进程之前的数据,会被父子进程通过页表结构映射到同一块物理内存上,当有一方修改变量内容时会发生写时拷贝。
写时拷贝:
1.当 fork 的时候,如果父子进程不修改数据,则页表的映射关系不会改变 2.当有一方修改数据时,为了防止导致另外一方读取数据时错误的,所以需要在物理内存当中重新开辟一块空间,保存修改后的值,并且将修改的进程的页表结构当中的映射关系重新指向新的物理内存。
页表结构:
页表结构有分页式,分段式和段页式
分页式:
1.进程虚拟地址空间分成了一页一页的小块,物理内存分成了一块一块的小块,一页的大小等于一块的大小 = 4096K 2.页表维护了页和块的关系
计算方法:
- 虚拟地址 = 页号 + 页内偏移
- 页号 = 虚拟地址 (转换为10进制) / 块的大小(4096)
- 页内偏移 = 虚拟地址 % 块的大小
- 块的起始地址 = 块号 * 块大小(4096)
- 物理地址 = 块的起始地址 + 页内偏移
分段式:
虚拟地址 = 段号 + 段内偏移
段页式:
虚拟地址 = 段号 + 页号 + 业内偏移
计算方法:
- 通过段号找到页的起始地址
- 通过页的起始地址,找到对应的页表结构
- 通过页号找到对应的块号,通过块号,计算出块的起始地址
- 块的起始地址加上页内偏移计算出物理地址