进程虚拟地址
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main(){
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
输出结果:
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8
可发现, 父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做虚拟地址
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由 OS(操作系统) 统一管理
进程虚拟地址控制
进程的虚拟地址是程序员虚拟出来的逻辑地址 不是物理地址, 是为了更好滴去利用我们的物理内存,
虚拟地址通过页表映射到物理内存中找到对应的值
创建子进程时(拷贝时)也会拷贝页表(映射关系), 所以父子进程计算的结果是一样的, (两个虚拟地址映射的是物理内存中的同一个值)
写时拷贝: 当子进程拷贝父进程的PCB以及页表结构之后, 子进程当中的虚拟地址空间在拷贝完成的时候, 和父进程是一模一样的, 同样, 页表当中的映射关系也是一模一样的,
当创建子进程完成之后, 父子进程当中有一个进程更改了内存当中保存的变量的值, 这时操作系统就会在内存当中重新开辟一段空间, 保存新的值, 而我们看到的现象就是, 父子进程通过页表指向了物理内存当中不同的区域;
如果说父子进程都不改变原来的内存当中的值, 那么页表的映射关系还是映射到同一块物理内存
若改变进程中的变量计算, 则会在物理内存中重新申请一块空间来存放新的计算结构
- 当创建一个子进程时, 子进程会拷贝父进程的页表结构, 也就是说会将父进程虚拟地址空间和物理内存之间的映射关系也拷贝了
- 如果创建完成一个子进程, 子进程也会通过页表映射到物理内存的同一块区域
- 如果父子进程都不进行修改, 则映射关系也不会修改,(写时拷贝技术), 和平共处,访问同一块物理内存
- 如果有一个进行了修改, 则就不能同时访问同一块物理内存, 需要重新去开辟空间, 同时修改改动进程的映射关系
同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
分页式:
进程虚拟地址 = 页号 + 页内偏移
映射:
- 进程虚拟地址空间分成了一页一页的小块, 将物理内存分成了一块一块的小块, 一页大小 = 一块大小 = 4096k
- 页表当中维护的就是页和块的映射关系
页号: 进程会将虚拟地址空间分成一页一页的结构
块号: 物理内存分为不同的块,每一个块都有一个块号,
页的大小 = 块的大小
通过页号找到块号 通过块号找到其在物理内存中的具体的块, 通过页内偏移找到在这个块中的具体位置
具体计算如下:
例如:
地址为5888(十进制)
块的大小为4096
1. 虚拟地址 = 页号 + 页内偏移
页号 = 地址大小 / 块的大小 ===> 5888/4096 ==> 1
页内偏移 = 地址大小 % 块的大小 ===> 5888 % 4096 ==> 1792
2. 物理地址
页号 -->找到 块号 ===> 给定3
块的起始地址 = 块号 * 块的大小 ===> 3 * 2096 = 12288
物理地址 = 块的起始地址 + 页内偏移 ===> 12288 + 1792 = 14080
分段式:
虚拟地址 = 段号 + 段内偏移
物理地址 = 段的起始位置 + 段内偏移
段页式:
虚拟地址 = 段号 + 页号 + 页内偏移
1. 通过段号找到页的起始地址
2. 通过页的起始地址 , 找到对应的页表结构
3. 通过页号, 在页表当中找到对应的块号
4. 通过块号乘以块大小找到块的起始地址
5. 块的起始地址加上页内偏移, 找到具体的物理地址