Linux进程地址空间
零.前言
本文将介绍进程地址空间(虚拟地址)是什么,以及为什么要这样设计。
1.地址空间
我们在学习C语言C++程序的时候,一定见过这样的图:
在之前,我们通常将这段空间称为内存,我们也可以写代码来验证一下各个区的相对位置。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int g_unval;
int g_val=100;
int main(int argc,char* argv[],char* env[])
{
printf("code addr:%p\n",main);//代码段地址
char* s="hello world";
printf("string rdonly:%p\n",s);//字符常量区
printf("init addr:%p\n",&g_val);//全局数据区,已初始化
printf("uninit addr:%p\n",&g_unval);//全局数据区,未初始化
char* heap=(char*)malloc(10);
printf("heap addr:%p\n",heap);//堆区
printf("stack addr:%p\n",&s);//栈区
printf("argv[%d]:%p\n",0,argv[0]);//命令行参数
printf("env[%d]:%p\n",0,env[0]);//环境变量
}
我们发现内存的排布确实是这样的。
2.进程地址空间的引入
但是其实,我们打印的地址并不是存储器中的物理地址,而是进程中的虚拟地址,所谓的内存其实并不是真正意义上的内存。
我们可以通过一段代码来举例:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int g_unval;
int g_val=100;
int main(int argc,char* argv[],char* env[])
{
if(fork()==0)
{
int count=5;
while(count)
{
printf("I am child,g_val:%d,&g_val:%p\n",g_val,&g_val);
count--;
sleep(1);
if(count==3)
{
printf("child change the data!\n");
g_val=200;
printf("child had changed the data!\n");
}
}
}
else
{
while(1)
{
printf("I am parent,g_val:%d,&g_val:%p\n",g_val,&g_val);
sleep(1);
}
}
}
先定义一个全局变量g_val,在子进程执行一段时间之后更改g_val的值:
我们惊奇地发现,在没修改g_val的值之前,代码一切正常,但是修改之后g_val的地址是没有变化的,但是它的值在两个进程中却是不一样的。
所以这里我们使用的地址,绝对不是物理地址!
3.进程地址空间(mm_struct)
(1)概念
出现上述现象的原因就在于,所有正在执行的进程都认为自己独占着内存资源。在每一个进程创建出来的时候,除了会创建PCB还会创建一个进程地址空间。我们所谓的内存操作,其实都是在进程的地址空间进行操作,最后再映射在物理内存上的。
进程地址空间其实本质上就是一个结构体(mm_struct)。
(2)进程地址空间的工作方式
进程地址空间本质上是结构体,结构体的内容其实就是各种区域的指针:
当我们想存入数据或者删除数据的时候,或者通过指针或者下标来修改数据的时候,我们就可以直接通过++end或者–start等来进行实现空间的扩容。注意暂时还并没有对真正地对内存来进行操作。虽然这里只有各个区域的start和end,但每个进程认为mm_struct代表的就是整个内存,对其中的内容操作就是对内存操作。
而虚拟地址则表示的是,在地址空间上进行划分的时候所对应的线性位置。比如我们给start传入0x00000000值,给end传入0xaaaaaaaa的值,两个数据之间的内容都可以认为是虚拟地址,进程认为对这些地址进行传值就是在对内存进行操作。
(3)进程地址空间与物理地址间的桥梁–页表和MMU
MMU属于硬件,是CPU中的一个内存管理单元,它可以帮助硬件查询页表。
页表的本质是虚拟地址和物理地址之间的映射表。
除了映射之外,页表还可以进行权限管理,即帮助判定哪些区域是可读的,哪些区域是可写的。
当进程对虚拟内存进行操作的时候,虚拟内存的地址会直接通过页表来和物理内存相对应。从而再由操作系统对物理内存进行同样的操作。
4.进程地址空间存在的原因
有人会说了,直接让进程对物理内存进行操作不香吗?为什么还要搞出进程地址空间这一东西?其实核心原因就是方便操作系统来进行管理。让每个进程都认为自己是独立于系统资源的。
(1)非法指针越界访问
我们知道进程之间是各自独立的,一旦进程可以直接操作物理内存的话,如果该进程存在非法指针,就有可能越界修改别的进程的代码和数据。
我们知道在进程创建出来的时候,页表也会随之被创建,每一个虚拟地址对应的内存空间是限定死的,所以无论怎么对虚拟内存进行操作,它也不会更改其他进程的代码和数据。
(2)方便权限管理
上文中提到了,页表中除了会有虚拟内存和物理内存的对应关系,还会有权限管理,当我们写C语言程序的时候,定义的字符串常量是只读的,当我们要对其进行写操作的时候会发生报错。
这是因为当我们要对虚拟内存进行写操作的时候,虚拟内存对应到物理内存,但是页表中的权限显示为只读的,此时操作系统就会终止该操作,并进行报错。此时转换失败,所以虚拟内存与物理内存的对应操作是否执行,是由操作系统说了算的。
如果是只读的,比如常量字符串,通常内存中只会有一份,我们观察到的地址也是只有一份的,因为这样操作系统维护的成本最低。
(3)基于缺页中断的内存申请
当我们在内存中申请大量空间时,实际上只是对虚拟地址进行申请的,进程地址空间可能会将end增大,但操作系统并不会真的直接开辟这么多空间。
站在操作系统的角度,如果空间都给一个进程了,但是这个进程并没有着急使用这些空间,那么这些空间就会被闲置着,不如给其他的进程来使用。
当进程需要对这些空间进行读写的时候,操作系统会再花费时间去开空间,这一过程我们称为缺页中断。
可以将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间来屏蔽底层申请内存的过程,达到进程读写内存和OS进行内存管理操作,进行软件上面的分离。
(4)确定相对位置减少负担
CPU在查找程序入口(即主函数)当然是越方便越好,如果进程直接访问的物理内存,各个进程的主函数位置不同,CPU查找起来就会非常的麻烦。
进程地址空间可以将所有进程的主函数放在代码段中的一块接近的进程地址空间中。CPU每次查找只需要去寻找进程地址空间中特定的位置即可,就可以通过页表来直接对应主函数。
此时CPU就可以直接在不同的进程中寻找0x0026这一虚拟地址,直接对应着不同进程的主函数的存储位置了。不仅仅是主函数,所有进程的其他共有点也更加方便CPU寻找。本质上就是说:CPU知道某个区域大概存的是什么东西。
5.总结
操作系统在管理任何东西的时候,都是通过先描述后组织的方式来进行管理的,对于进程地址空间来说,先描述指的是mm_struct,再组织指的是将mm_struct通过页表与物理内存进行对应起来。其本质都是方便操作系统来进行管理。
- 点赞
- 收藏
- 关注作者
评论(0)