课程推荐
本篇文章会围绕 **三个问题(什么是地址空间?地址空间是如何设计的?为什么要有地址空间?)**进行展开讲述。其中主要是了解虚拟地址和物理地址的区别。希望本篇文章会对你有所帮助。
一、什么是地址空间?
什么是地址空间呢?我们在学C语言时,经常说到程序的变量存储在栈区、静态区
堆区等。这些综合起来就是地址空间。通俗来讲,地址空间就是表示计算机系统中内存的总体范围。它是可用于存储和访问数据的内存地址的集合。
1、1 验证地址空间
我们了解地址空间后,不妨来验证一下我们之前所学的是否正确。我们之前学的地址空间如下图:
我们通过下段代码来验证我们之前所学的是否正确:
#include
#include
#include
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
// int a = 10;
//字面常量
const char *str = "helloworld";
// 10;
// 'a';
printf("code addr: %pn", main);
printf("init global addr: %pn", &g_val);
printf("uninit global addr: %pn", &g_unval);
char *heap_mem = (char*)malloc(10);
char *heap_mem1 = (char*)malloc(10);
printf("heap addr: %pn", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %pn", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %pn", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %pn", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("read only string addr: %pn", str);
int i;
for(i = 0 ;i < argc; i++)
{
printf("argv[%d]: %pn", i, argv[i]);
}
for(i = 0; env[i]; i++)
{
printf("env[%d]: %pn", i, env[i]);
}
return 0;
}
上述代码就有我们所熟知的不同存储区,我们再来看运行结果:
上图正是在Linux下运行的结果。在windows下运行的结果所得出的结论也是相同的。我们看到上图的运行结果后是符合我们所学的地址空间的规律。
栈和堆之间有大量空间是空着的。 其次堆和栈是相向而生的。细心的小伙伴可能发现,总共的内存空间是4G,而用户空间只占用3G,那剩下的1G呢?其实完整的地址空间如下:
1、2 地址空间是指的物理内存吗?
我们之前在学C语言时,经常会提到 ''地址'等词汇。例如,我们随查看的临时变量所存储的地址。那么我们经常所说的这些地址是指的物理内存( 物理内存是指由于安装内存条而获得的临时储存空间。主要作用是在计算机运行时为操作系统和各种程序提供临时储存。)中的地址吗?
答案不确定时,我们看看如下代码:
#include
#include
#include
int g_val = 10;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
while(1)
{
printf("child[%d]: %d : %pn", getpid(), g_val, &g_val);
sleep(1);
}
}else{ //parent
while(1)
{
printf("parent[%d]: %d : %pn", getpid(), g_val, &g_val);
sleep(1);
g_val=100;
}
}
sleep(1);
return 0;
}
上述代码就是区分父子进程,打印同一个变量的值。结果如下图:
我们惊奇的发现,同一个变量(地址是相同的),他们的值竟然不一样!难道是一个变量可以存储两个不同的值的原因吗?答案是不是的。
我们知道物理内存中的地址表示唯一一块空间,那上述的运行结果证明了,我们所说的 地址空间并不是物理地址的!而是存储在 **虚拟地址(虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存 ,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换)**中。
1、3 地址空间解释
地址空间 本质就是一种内核数据结构,在Linux当中,叫做struct mm_struct(linux内核当中的地址空间结构体)包含了一些区域信息( 先描述),能够实现 区域划分(本质就是在一定的范围内定义start和end)。
struct mm_struct
{
unsigned long code_start;
unsigned long code_end;
unsigned long init_start;
unsigned long init_end;
unsigned long uninit_start;
unsigned long uninit_end;
unsigned long heap_start;
unsigned long heap_end;
unsigned long stack_start;
unsigned long stack_end;
//...等不同的区域划分
}
每个进程都会有自己的地址空间,同时进程控制块 *(PCB)中也包含了 mm_struct 指针,可使我们直接找到自己所对应的进程地址空间( 后组织)。
上述讲述的这么多,我们可以理解为进程地址空间就是 操作系统给进程花了一个大饼。
这个大饼就是指的 每个进程都会有4GB的连续的空间(0x00000000~0xFFFFFFFF)。实际上呢,这4GB的的空间是虚拟内存,虚拟内存对应的实际物理内存,可能只对应的分配了一点点的物理内存,实际使用了多少内存,就会对应多少物理内存。
这4G虚拟内存是一个连续的地址空间(这也只是进程认为),而实际上,它的数据是存储在多个物理内存碎片的,还有一部分存储在外部磁盘存储器上,在需要时将数据交换进物理内存。
二、进程访问地址
2、1 历史的程序寻址
在虚拟地址出现之前,程序的寻址都是直接寻找的物理地址。但是这样会有很多的不足:
由于上述的三个直接原因,后来就产生了虚拟地址。
2、2 进程地址空间映射到物理内存
当有了虚拟内存的概念后,上述的问题就得到了很好的解决。当我们访问物理内存中的数据时,需要先访问进程地址空间上的地址。然后把 虚拟地址空间上的地址 通过页表映射到对应的物理内存上。具体如下图:
地址空间和页表是每个进程都独有的一份,只要保证每一个进程的页表,能够映射到不同区域的物理内存,就能够做到进程之间互不干扰。这就是我们所说的进程所具有独立性。
映射是由谁来完成的呢?答案是操作系统!操作系统通过地址转换机制将虚拟地址映射到物理地址,以实现对内存的访问。这种映射通常在页表或段表等数据结构上实现,其中存储了虚拟地址与物理地址之间的映射关系。
2、3 解释相同地址打印出不同数据
我们在上述的 1、2 中看到了相同的地址打印出不同的数据。注意, 我们所访问到的地址都是虚拟地址。并不是物理地址。在上述的 1、2 中我们创建了一个子进程, 子进程本身是继承了父进程的数据和代码。在没有对数据进行 修改之前,子进程和父进程共享了一份数据。一但对子进程或者父进程的 数据进行修改,就会发生写时拷贝。对 修改的数据进行深拷贝,从而达到对彼此不产生干扰,实现进程独立性。
那就对相同地址打印出不同数据的现象不难理解了。当我们对父进程的数据进行修改时,父进程发生了写时拷贝,在内存中开辟了空间。但他们 都有自己的地址空间(虚拟地址),所以 地址相同也是正常现象(子进程继承父进程的代码和数据)。即使虚拟地址一样,但是可 通过页表映射到不同的物理内存中。具体如下图:
三、为什么要有地址空间
3、1 保护物理内存
可能还有一些疑惑:即使有了虚拟地址,那我要是 对野指针进行了访问修改,页表对野指针映射后,还不是对物理内存进行了非法的访问修改吗?地址空间的设置不就多此一举了吗?
事实并非上述一样。 凡是非法的访问或者映射,操作系统都会识别到的。一但你进行了非法的访问或者映射,操作系统就会终止掉你的程序。举个例子,当我们对野指针进行访问修改时,你的程序就会崩溃,这不就是程序终止退出吗!!!
地址空间有效的保护了物理内存。因为地址空间和页表是操作系统创建并且维护的。这也就意味着地址空间和页表进行映射时需要操作系统进行监管!
3、2 内存管理和进程管理完成解耦合
因为有了地址空间和页表,所以我们的数据可以在物理内存中的任何合法位置加载。因为他们之间有映射。物理内存的分配和进程的管理可以做到没有关系!
所以进程模块和内存模块只需要各自完成各自的事情,最后通过页表的映射将他们连接起来,产生关系。降低了他们之间互相的影响度。
3、3 将无序的物理内存有序化
由于数据可以在物理内存中理论上可以加载任何位置,那么是不是物理内存中几乎所有的数据和代码在内存中都是乱序的。
但是,也为页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,那么在进程的视角所有内存分布就是有序的!
四、总结
我们平常所访问到的地址均为虚拟地址。地址空间并不是物理地址,而是虚拟地址。通过页表映射访问物理地址。 每个进程都有自己的地址空间和页表。
页表是一种数据结构,它存储了虚拟地址与物理地址之间的映射关系。在进行地址转换时,操作系统根据进程的页表查找对应的物理地址,然后将虚拟地址转换为物理地址,以便进行实际的内存访问。
通过使用虚拟地址,操作系统可以为每个进程提供独立的地址空间,使得多个进程可以并发运行,彼此之间相互隔离,互不干扰。虚拟地址还提供了更高的灵活性和保护性,使得操作系统可以有效地管理和分配内存资源,提高系统的性能和安全性。