个人主页:敲上瘾-CSDN博客
个人专栏:Linux学习、游戏、数据结构、c语言基础、c++学习、算法
目录
问题引入
一、什么是虚拟内存
二、虚拟内存的描述与组织
三、页表的优势
四、虚拟内存区域划分
问题引入
为引入今天的话题,我们先来看下面一段程序:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
int k=10;
pid_t id=fork();//创建子进程
if(id==0)
{
int n=3;
while(n--)
{
k++;
printf("子进程:&k=%p,k=%d\n",&k,k);
sleep(1);//休眠1秒
}
}
else
{
int n=3;
while(n--)
{
printf("父进程:&k=%p,k=%d\n",&k,k);
sleep(1);
}
}
return 0;
}
执行结果如下:
我们发现父进程和子进程的k值是不同的,这个很好理解,因为父子进程在共用资源时如果其中一方需要对资源进行修改就会发生写时拷贝,从而使得父子进程保持独立性。不过在这里最难让人理解的是父进程的k和子进程的k的地址是一样的但k值却是不一样,这实在难以让人接受😱,难道是同一块内存地址还储存两个不同的值?这是根本不可能的。
其实这里的地址并不是物理地址,而是虚拟地址,虚拟地址与物理内存地址之间还存在一个媒介。接下来给大家详细讲解。
一、什么是虚拟内存
如果一个物理内存总大小为4G,那么每个进程都会创建一个自己的虚拟内存,大小和物理内存大小一样是4G,用来接收物理内存给它分配的内存。
当然了这个虚拟内存比物理内存复杂得多,需要做栈区、堆区等等的区域划分。
在虚拟内存与物理内存之间存在着一个媒介,它就是页表,起到一个交通枢纽的作用,它实际上是一个映射关系,把虚拟内存上的值通过页表映射得到对应的物理内存。当然页表的作用不止于此,它还起到权限管理的作用,即每个地址都用自己的rwx权限,对野指针、空指针等进行访问,就是在页表这里被拦截的。
有了虚拟内存的认识我们就可以解释问题引入了。
问题的解决:
一个进程(pcb)是有自己的属性和代码和数据的,父进程创建子进程后,子进程的代码和数据是和父进程共用的,所以子进程创建了和父进程一模一样的虚拟内存和页表,指向相同的物理内存。所以才会有问题引入那里父子进程的k的地址是一样的,实际上是虚拟地址。
而当子进程发生写时拷贝的时候,就会开辟新的物理内存,把原来的虚拟地址映射到新开辟的内存地址。也就是父子进程各有自己的虚拟内存和页表,而它们的虚拟内存是一样的,当发生写时拷贝时,页表的映射关系发生了改变。所以才会有父子进程虚拟内存地址相同,物理内存不同的情况。
二、虚拟内存的描述与组织
进程有自己的虚拟内存,那么就需要对它进行管理,既然要管理就需要对它进行描述与组织,所以在Linux中就有了mm_struct结构体对虚拟内存进行描述。它所在的文件为mm_types.h。可以翻阅文档进行查看。
可以说,mm_struct结构是对整个⽤⼾空间的描述。每⼀个进程都会有⾃⼰独⽴的mm_struct,这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。
三、页表的优势
- 使地址变得有序。在把数据代码加载到内存的时候,并不是有序储存的而是混乱的,也没有什么栈区,堆区等等这些区分。而使用页表做一个映射关系则可以把这些空间在虚拟地址上变得有序,方便管理。
- 保护物理内存。页表除了有一个映射的作用,它还控制了每个地址空间的rwx这些权限,从而达到保护物理内存的目的。
- 解耦合。不直接从虚拟内存去与物理内存匹配,而是在此之间加一个页表,这样可以使得虚拟内存和物理内存保持自己的独立性,从而起到一个解耦合的作用。
这个解耦合的意义是非常的大的。它使得程序执行变得更加灵活。比如在创建进程的时候,只需要把虚拟地址空间和页表搭建好,可以先不把内存加载入内存,等执行到该程序在查询页表时发现代码数据并没有加载到物理内存,这个时候再数据加载入内存。这就是缺页中断。
那么同理,我们就可以理解挂起操作了,操作系统在执行过程中发现内存不足,能够实现把阻塞状态或优先级低的进程的代码数据从内存中释放,留出更多的内存给其它进程。就是因为只要保留了虚拟内存地址和页表,那么对应的代码数据就能再次加载入内存。
四、虚拟内存区域划分
在上面我们提到虚拟地址空间是使用mm_struct来描述的,而mm_struct把虚拟内存划分为各个分区,它整体记录了各个分区的开始和结束的地址,每个分区的大小不是固定的,每个程序都有各自不同的区域大小需求。如下为mm_struct的部分源码。
struct mm_struct
{
/*...*/
struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb; /* red_black树 */
unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
/*...*/
// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
/*...*/
}
那既然每⼀个进程都会有⾃⼰独⽴的mm_struct,操作系统肯定是要将这么多进程的mm_struct组织起来的!虚拟空间的组织⽅式有两种:
- 当虚拟区较少时采取单链表,由mmap指针指向这个链表。
- 当虚拟区间多时采取红⿊树进⾏管理,由mm_rb指向这棵树。
linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA),由于每个不同质的虚拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型的虚拟内存区域。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA,以便进程快速访问。
非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!