进程地址空间
这是一块虚拟地址空间的展示图

地址从下往上增长
下面时对应存放的数据
代码段: 存放可执行代码和只读常量
数据段:存放全局变量和静态变量
堆区:动态地址空间,地址一般是增长的
栈区:存放局部变量,地址一般是降低的
内核:命令行参数argv和环境变量env等
我们来用一段代码来验证图片信息是否准确
#include<stdio.h>
#include<stdlib.h>
int val=10;
int un_val;
int main(int argc,char* argv[],char*env[])
{
printf("main函数地址 :%p\n",&main);
printf("未初始化地址空间:%p\n",&un_val);
printf("已初始化地址空间:%p\n",&val);
int *heap1=(int*)malloc(sizeof(int)*1);
int *heap2=(int*)malloc(sizeof(int)*1);
int *heap3=(int*)malloc(sizeof(int)*1);
printf("heap1堆地址空间 :%p\n",heap1);
printf("heap2堆地址空间 :%p\n",heap2);
printf("heap3堆地址空间 :%p\n",heap3);
printf("heap1栈地址空间 :%p\n",&heap1);
printf("heap2栈地址空间 :%p\n",&heap1);
printf("heap3栈地址空间 :%p\n",&heap1);
printf("argv[0]地址空间 :%p\n",argv[0]);
printf("env[0]地址空间 :%p\n",env[0]);
}
我们先是输出了main函数的地址,函数也是有自己的地址的
然后我们输出了两个变量的地址,分别是已初始化变量和未初始化变量
我们malloc了三个堆的空间分别用指针heap1和heap2和heap3去管理
然后我们输出这三段堆空间的地址
我们在输出三个heap1和heap2和heap3的地址,虽然他们是指针,但他们也是变量
然后我们输出argv和env这种内核地址空间
运行结果

我们可以看到,已初始化变量地址是低于未初始化变量地址的
堆的空间增长空间地址是增大的
栈的空间地址增长是减小的
argv的地址和env的第一个数据的地址是最大的
由此我们可以得出结论
OS内核>栈区>堆区>数据段>代码区
虚拟地址空间
先看以下代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int val=10;
int un_val;
int main()
{
int val=5;
pid_t id=fork();
if(id==0)
{
printf("child :val:%d,&val:%p\n",val,&val);
val+=5;
sleep(1);
printf("child :val:%d,&val:%p\n",val,&val);
}
else
{
printf("parent:val:%d,&val:%p\n",val,&val);
sleep(2);
printf("parent:val:%d,&val:%p\n",val,&val);
}
sleep(1);
}
运行结果

这里的子进程和父进程的val输出结果不同,但是父进程和子进程的输出地址居然都相同
按道理来讲,在物理地址层面相同的地址存放的数据应该不可能不相同,那么只有一个可能的结果,那么就是这个地址是一个虚拟的不存在的地址
在语言层面接触到的地址都是虚拟地址,都不是物理地址
不管时那个语言,接触到的都是虚拟地址,不止C++/C语言
进程地址空间管理
让我们来解释上面的情况,为什么地址相同而数据却不相同
实际上在每一块PCB,也就是进程里面都有自己的一块虚拟内存,并且进程创建时,会产生一个页表,它是一个位图表,将PCB虚拟内存地址和物理内存地址对应
大致就是如此

我们将图标简化成上面问题更好理解的图

当我们需要调用一个进程中的地址对应的数据,那么操作系统会按照这个进程的页表来去找这个数据在物理地址中的位置
那么为什么上面父进程的数据和子进程不一样,但是地址却是一样的呢
当我们创建一个子进程时,子进程会拷贝父进程的数据,但和父进程共用同一套数据

子进程和父进程的页表和虚拟地址位置是一摸一样的,但是当子进程需要修改它的某个变量值时,子进程会重新创建一份空间

虚拟地址对应的物理空间发生了变化,但虚拟空间还是和父进程保持一致,
所以我们会观测到父进程和子进程的地址时一致的但是对应的变量的数据是不一样的
就是因为子进程的物理地址发生了改变,但虚拟地址没有发生改变,操作系统同过这样的方式可以极大的节省内存空间
系统层面管理

我们之前 就看过这张图片,PCB的虚拟内存是通过一个结构体mm_struct来管理的,PCB中存储着mm_struct的指针,mm_struct通过存储数据来划分虚拟空间不同的区域
以下是mm_struct在linux中的源代码
unsigned long total_vm, locked_vm, shared_vm, exec_vm;
unsigned long stackvm, reserved_vm, def_flags, nr_ptes;
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;
其中start_code表示代码段的开始,end_code表示代码段的结束
start_data表示数据段的开始,end_data表示数据段的结束
mm_struct的结构大致如下
其中task_struct就是进程PCB,mm_struct就是进程空间地址,page table就是页表,physical memony就是物理空间地址
硬件层面管理
在CPU中存在一个MMU单元memony management unit内存管理单元,它可以将虚拟地址转化成物理地址

在MMU内存管理单元中,存在一个CR3寄存器,里面存放了一个页表,当MMU拿到虚拟地址后可以通过,CR3里面的页表,查询对应的映射关系从而得到物理地址
进程地址空间的意义
将无序的地址变为有序的地址
物理地址在真正意义上存放数据是非常杂乱的,当我们使用页表映射来管理物理内存,就有了栈区,堆区,等,由此内存变得有序,可以更好地管理内存的空间
将进程管理和内存管理解耦
由于页表的存在,进程几乎不用关心是内存是通过上面操作管理的,而内存也不用关心进程是如何读取数据的,进程只要把自己的虚拟内存地址加载到页表中,内存只要把物理地址加载到页表中就可以时间进程和物理内存数据的对应
保护了内存安全
当我们想访问一个虚拟内存时,这个虚拟内存可能已经过界了,他并没有对应的物理内存地址那么页表就会终止用户的访问,通过这种方式来保护物理内存
确保了进程的独立性
进程都有自己独立的页表,当进程要运行时,只要操作自己进程对应的物理空间地址就可以了,可以通过自己独立的页表操作自己独立的数据
动态内存的管理机制
当我们想系统申请一块动态内存时,我们会使用malloc或new
但是当我们申请空间的时候操作系统并不会直接给我们分配一块空间,因为操作系统不知道这块内存到底会不会得到使用,所以操作系统只会返回一个虚拟内存
那么页表当中就会有一个虚拟内存,这个虚拟内存还没有映射的物理内存只是返回了一个地址
当用户访问这个虚拟内存时,操作系统发现这个访问是合法访问,但是页表中没有对应的物理地址
那么就会发生缺页中断,操作系统会开辟一块空间与这个虚拟内存在页表中对应。此时操作系统才算真正开辟空间
好处
提高了内存的使用效率,因为只有用户要进行数据的写入时,才真正开辟一块空间
提高了new和malloc申请空间的速度,因为实际上只是返回了一个地址,并没有开辟空间