虚拟地址空间 && 页表
我们先来看一段代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int a = 0;
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
return -1;
}
else if (id == 0)
{ //child
a = 100;
printf("child[%d]: %d : %p\n", getpid(), a, &a);
}
else
{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), a, &a);
}
sleep(1);
return 0;
}
运行结果为 :
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8
我们可以看见, 子进程与父进程打印出来的 a 的值不一样, 但是它们打印出来的地址确是一样的, 这是怎么回事呢, 其实这是因为在Linux下我们所能看到的程序打印出来的地址都是一个虚拟地址, 并非真正的物理地址,即Linux下的程序地址空间是一个虚拟地址空间, 用户是看不到物理地址的, 但是为什么虚拟地址就可以一样呢? 我们先留下一个这样的疑问, 一起来往下看。
什么是虚拟地址空间?
在Linux下,虚拟地址空间其实就是进程(pcb)的内存描述符,它是一个mm_struct结构体,存放在task_struct中, 操作系统用这个mm_struct描述一段物理内存, 在这个结构体里面有很多的字段, 都是用来描述当前进程的,比如当前进程中各个段的起始地址和结束地址,数据段,栈区, 堆区等等。如图 :
我们每创建一个进程, 就会产生一个虚拟地址空间, 在Linux32位操作系统上, 这个虚拟地址空间的大小是4G, 当然这是虚拟的空间, 如上图, 包括1G的内核空间和3G的用户空间,内核空间所有进程共享。
为什么要使用虚拟地址空间?
传统的直接使用物理内存会造成资源不够, 盲目操作等问题, 内存使用率非常低,物理地址是连续的, 如果中间有一块被占用但是两边的内存不够另外的程序去使用, 这个时候就造成了资源浪费,所以我们使用虚拟地址去存储当前程序中各个变量, 然后通过页表进行映射, 再将这些变量分别找地方存下, 相当于见缝插针, 这样的话会大大提高内存的利用率。比如 :
我们假设内存是16M, 现在程序1和程序2一共占了12M, 但是因为物理内存是连续的, 当前剩余的内存虽然是5M,但是无论是上面的内存还是下面的内存都放不下这个5M的程序,这个时候操作系统会将一个别的进程放到硬盘中,然后下次再用的时候再以同样的方法找寻足够运行的空间, 这样的方式效率是非常非常低的,所以我们要使用虚拟地址空间搭配页表来对内存进行访问控制。
如何通过虚拟地址映射物理地址?
虚拟地址 + 页表
虚拟地址空间和页表共同使用 :提高内存利用率, 增加内存访问控制保证进程之间的独立性
页表又是什么呢?
我们将物理内存分成很多个内存页,一般为4K, 然后把虚拟地址和物理地址的映射关系放在一张表中, 就叫做页表。
那么我们的映射关系是怎样的呢?如下 :
具体的计算方式呢?
通过虚拟地址中的前20位获取到页号, 然后在页表当中通过页号找到对应的页面信息(权限信息等),获取到物理页面页号, 然后将虚拟地址的业内偏移拿出来和物理页号相结合最终得到一个物理地址.
内存管理的几种方式 :
分页式内存管理 :
首先, 地址是由 : 页号(20) + 页内偏移(12) 组成 , 通过虚拟地址中的前20位获取到页号, 然后在页表当中通过页号找到对应的页面信息(权限信息等),获取到物理页面页号, 然后将虚拟地址的业内偏移拿出来和物理页号相结合最终得到一个物理地址.1级表占用内存太多,一个程序其实只用到其中很少一部分的内存地址,没必要全部都弄一个完整的表,内存占用比较大,所以我们一般使用分级页表
段页式内存管理 :
地址组成 : 段号 + 段内页号 + 业内偏移 , 通过段号找段表 ,找到之后可以找到段内存区域对应的页表, 然后通过段内页号获取到页表信息,然后得到物理页号,加上业内偏移, 组成物理地址分段式内存管理 :
将内存分成多个段, 地址是由 : 段起始地址 + 段内偏移 构成, 寻址方式与页式的基本一样。
写时拷贝技术
开头我们留下了一个问题 : 为什么子进程和父进程的a值不同,但是打印出来的地址却相同呢, 是因为我们用fork()创建出阿里的子进程与父进程 代码共享, 数据独有,子进程拷贝父进程, 这个时候注意, 子进程将父进程的虚拟地址空间和页表全部都拷贝过来了
我们可以看见下图,当我们创建一个子进程的时候, 子进程会将父进程的虚拟地址空间和页表全部拷贝,所以父进程中所定义的变量的虚拟地址子进程也拷贝走了,但是当子进程需要对这个变量的值进行修改的时候,操作系统会在内存中重新开辟一片空间去存储子进程修改后的变量的值, 之后将子进程页表中原来a变量的虚拟地址和物理地址的映射关系断开,重新建立虚拟地址和系统新分配的物理内存地址的关系,但是虚拟地址没有变, 物理地址发生了变化, 这个就是写时拷贝技术, 其实也好理解 , 就是子进程对数据进行修改时重新开辟空间,并且建立新的页表映射关系。
但是对于用户来说, 我们打印出来的地址是虚拟地址, 但是虚拟地址并没有变化, 变的是物理地址, 所以出现了a的值不同但是地址一样的结果。
以上就是虚拟地址空间和页表,写时拷贝的一些个人理解。