Linux——虚拟地址空间
1.虚拟地址空间
进程地址空间又叫虚拟地址空间
我们大家知道程序在运行时使用的空间被划分为多个不同的区域,每个区域都有不同的作用
- 正文代码:存放程序的可执行代码 通常都是只读的
- 初始化数据:
- 未初始化数据
- 堆区:用于动态分配内存,程序在运行可以通过new,malloc在堆上申请内存空间 地址由低向高
- 栈区:存储函数调用的上下文信息,函数的参数,返回值,当函数被调用,会在栈区开辟一块空间存储这些信息,当函数调用完毕就自动释放,地址由高向低,空间有限
- 命令行参数环境变量
我们来测试一下:
程序地址空间并不是物理内存,我们可以写代码来看一下:我们可以发现并不是真实的物理地址,而是虚拟地址
测试代码 :
1#include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 5 int val = 100;6 int main()7 {8 pid_t id = fork();9 if(id < 0)10 {11 return 1;12 }13 else if(id == 0)14 {15 //子进程16 while(1)17 {18 printf("子进程 val: %d &val: %p pid: %d ppid: %d\n ",val,&val,getpid(),getppid());19 val += 10;20 sleep(1);21 }22 }23 else 24 {25 while(1)26 {27 printf("父进程 val: %d &val: %p pid: %d ppid: %d\n ",val,&val,getpid(),getppid());28 sleep(1);29 } 30 }31 }
解释一下为什么虚拟地址一样val值不一样
一个进程一个虚拟地址空间 ,虚拟地址空间的宽是1字节,32位机器下的长度是2的32次方,所以32机器下的一个虚拟地址空间有2^32个地址,每个地址指向一个字节,我们看到的是起始虚拟地址,我们的val是int类型,那么一个字节肯定放不下,是起始地址+偏移量进行存放,每一个虚拟地址空间和物理内存之间都有一张页表,这张页表主要是进行映射,页表的主要作用是将虚拟地址映射到物理地址。当 CPU 访问虚拟地址时,会借助页表来查找对应的物理地址。在实验中我们有两个进程,父进程拷贝一份数据给子进程,父进程和子进程也就共享了,是浅拷贝,当子进程修改val值的时候,OS就会在物理内存将修改数据拷贝一份让子进程进行修改,这个时候就发现了写时拷贝,物理地址也就发生了变化,这也就是为什么父子进程的虚拟地址相同,但是值不同的原因
2.虚拟地址空间是一个结构体 mm_struct
进程的地址空间的所有的信息是一个结构体,叫 mm_struct,所有的区域都被进行了划分,划分了不同的区域,mm_stuct存放着这些区域的开始和结束
struct task_struct
{/*...*/struct mm_struct *mm; //对于普通的⽤⼾进程来说该字段指向他的虚拟地址空间的⽤⼾空间部struct mm_struct *active_mm;/*...*/}
这张图片是mm_struct内部的部分内容 其他内容我们现在可能看不懂,但是start_code,end_code,我们知道是进程代码段的开始和结束,start_data,end_data是进程数据段的开始和结束

在Linux内核中其实mm_struct内部有一个vm_area_struct结构体,里面存放着每个区域对应的起始和结束地址 ,mm_struct会维护一张链表,mm_struct用来描述进程整体的内存空间,而vm_area_struct是描述虚拟地址空间一个连续的区域
3.为什么要设置虚拟地址空间
1.磁盘的代码数据写入内存可能不是按照顺序存放的,可能是离散的,虚拟地址空间是连续的,那么就是不是将地址从"无序"变为了"有序"
2.页表其实有许多的功能,记录虚拟地址和物理地址只是一种功能,页表也具有权限,地址在转化的过程中,可以对地址和操作进行判断,从而对内存的数据进行保护,比如说我们知道常量是无法修改的,这是因为在页表设置了只读权限,当用户进行修改,硬件会触发一个异常,页表进行了拦截,所以无法修改
3.让进程管理和内存管理具有一定的解耦合 进程管理主要负责进程的创建、销毁、调度等操作。在有虚拟地址空间的情况下,进程可以专注于自身的任务执行,而无需关心物理内存的具体分配和管理细节。内存管理负责分配,回收,保护等工作,OS可以根据实际的物理内存使用情况,灵活的将虚拟地址映射到物理地址
举个例子 假设我们写了一个2GB的代码 虚拟地址空间在代码段开辟2GB的地址,写到页表里,但是此时内存空间只能给出500MB的空间,写入内存,在页表进行映射,当进程运行时,在页表如果发现有虚拟地址,但是没有内存地址,这个时候OS会自动让外设重新执行加载任务,这个过程就叫缺页中断
创建进程,我们是现有虚拟地址空间,申请对应的地址空间,然后再对数据代码进行加载
深刻理解挂起操作
4.写时拷贝
当我们学完虚拟地址空间,我们就可以深刻的理解写时拷贝了,子进程创建就是拷贝父进程的task_struct,父子进程代码数据共享,当有任何一方进行写入,这个时候OS就会拷贝一份被修改的数据让进程进行修改
具体操作:当有一方进行写入 OS会检测到写操作违反了页表的只读属性,权限不够,这时OS就会位修改的数据申请新的内存,将原来贡献的页面复制到新的页面上,让进程进行修改,然后让进行写入操作的进程的虚拟地址指向新页面的内存地址
写时拷贝的优点
- 可以加快子进程的速度 代码数据共享
- 避免不必要的拷贝 如果父子进程不修改数据段代码是共享的
- 节省内存空间 如果父进程的代码数据非常大 子进程不需要进行修改 这个时候就可能体现节省内存空间
- 提高系统并发性能