目录
Ⅰ、什么是程序地址空间?
Ⅱ、虚拟地址空间是什么样的?
一、虚拟地址空间和页表
1、什么是页表?
2、什么是虚拟地址空间?
3、什么是vm_area_struct?
Ⅲ、为什么要用虚拟地址空间?
一、进程的独立性
二、保护数据安全
三、管理模块和内存模块的解耦合
Ⅰ、什么是程序地址空间?
在我们之前学习C语言/C++的时候可能我们看过下面图片,当时有人告诉我们不同的数据和代码存放在了内存的什么地方,当我们现在学习了操作系统了以后我们对这个图片产生了一些疑问,我们我们在之前的博客说过进程的产生如果我们的内存是这样排布的我们的进程应该怎么去存放?现在我们将提出虚拟地址空间也叫程序地址空间。
那什么是虚拟地址空间?
说的形象一点,假设操作系统是老板,进程是员工,那么虚拟地址空间就是老板给员工画的大饼,操作系统告诉进程,这个内存里边只有你和我在这里边,你只要好好进行这里边的内存全部是你的。这样进程以为他可以拥有全部的内存。这就是虚拟地址空间。
下面我们来看一段代码:
#include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 5 6 int num = 0;7 8 int main()9 {10 pid_t id = fork();11 if(id == 0)12 {13 //子进程14 num = 1;15 printf("我是一个子进程,num地址:%p, num = %d \n", &num , num); 16 }17 else18 {19 20 printf("我是一个父进程,num地址:%p, num = %d \n", &num , num);21 }22 return 0;23 }
~
~
看上面这段代码我们可以发现在同一个地址下不同的进程的值竟然是不一样的,我们在之前的学习我们知道一个地址只能有一个值,这是为什么?这其实就是虚拟地址空间的作用下面我们来详细了解虚拟地址空间。
Ⅱ、虚拟地址空间是什么样的?
一、虚拟地址空间和页表
我们先来解释一下上文的问题,为什么一个地址父子进程的值却不一样?这时以为在操作系统创建进程的时候不仅仅是创建PCB,去创建虚拟地址空间和页表,还要去加载代码和数据。这样一个进程才真正的被创建。
1、什么是页表?
下面我们先来解释页表的功能:页表可以把虚拟地址一一对应真实的物理地址。
我们上面说过虚拟程序地址就像操作系统给进程画的一张大饼,操作系统告诉进程你可以把你的数据按照一定的顺序放,进程把自己的代码和数据按照不同的特性放在了不同的区域,但实际上内存中不止一个进程当然一个进程也不可能拥有一整块内存,所以操作系统要靠一个东西把进程的代码和数据在虚拟地址空间和物理空间一一对应这个东西就是页表。
2、什么是虚拟地址空间?
虚拟地址空间被分为了很多部分,比如堆区,栈区,等等。那么如何去做区域划分的呢?我们知道整个虚拟地址空间(假设是32位机器)被统一编址从0X00000000~0Xffffffff。如果我们可以记录每个区域的起始和结束那么就可以实现了区域划分,那么在操作系统内核中是如何做的呢?在Linux中用了一个内核数据结构来实现这个就是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;
}
3、什么是vm_area_struct?
我们知道在我们申请空间的时候是在堆上开空间,那么我们每次申请的空间都是不连续的那么mm_struct里只有两个变量去区分堆的开始和结束,这里我们就有一个疑问:我们开的空间是不连续的但我们只有两个变量去确定区间,也不能把每个空间的开始和结束都表示出来啊?
这个时候我们用vm_area_struct去解决这个问题
struct vm_area_struct {
unsigned long vm_start; //虚存区起始
unsigned long vm_end; //虚存区结束
struct vm_area_struct *vm_next, *vm_prev; //前后指针
struct rb_node vm_rb; //红⿊树中的位置
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm; //所属的 mm_struct
pgprot_t vm_page_prot;
unsigned long vm_flags; //标志位
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops; //vma对应的实际操作
unsigned long vm_pgoff; //⽂件映射偏移量
struct file * vm_file; //映射的⽂件
void * vm_private_data; //私有数据
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;
我们看上面的代码我们可以看见start,end,next,prev,其实mm_struct中只记录每个区域总的开始和结束而具体的每个空间(比如我们在堆上开辟的空间)的 开始和结束是由vm_area_struct记录,然后将vm_area_struct再组织起来。
组织虚拟空间有两种方式:
1、当虚拟区比较少的时候用单链表,mmap指向这个链表
2、当虚拟区比较多的用红黑树进行组织,mm_rb指向这颗红黑树
Ⅲ、为什么要用虚拟地址空间?
一、进程的独立性
每个进程都有一个task_struct , mm_struct , vm_area_struct等相互独立,页表将虚拟地址和物理地址一一对应的,当创建子进程被创建的时候操作系统会先拷贝父进程的task_struct , mm_struct , vm_area_struct然后将一部分,比如pid,ppid等改变,最后交给子进程,当父进程或者子进程对数据进行改变的时候操作系统会进行写时拷贝,重新开辟一个新的物理空间然后把数据拷贝进去再去改动,然后操作系统改变子进程的页表中这个变量对应的物理地址而虚拟地址不改变,这也就解释了为什么一个地址会有两个值的问题。如果没有虚拟地址空间就没有页表也就不能实现独立性。
二、保护数据安全
页表是由操作系统去维护的如果想要去访问一个不合法的地址或者是数据的话当查询页表的时候操作系统就可以去拒绝进而对数据形成了保护。
三、管理模块和内存模块的解耦合
task_struct,mm_struct等这是属于管理模块,物理内存等都是属于内存模块,以为有虚拟地址空间和页表才会实现无论数据在物理内存是如何存储的在虚拟内存空间都是有序的,这样两个模块就可以解耦合。阻塞挂起也可以很好的展示管理模块和内存模块解耦合的好处,当进程是挂起状态的时候,操作系统会把进程的代码和数据换出到磁盘中,然后把页表的物理地址删除当进程调用数据和代码的时候操作系统再换回到内存中,这也很好展示了管理和内存模块是解耦合的。