文章目录
- ❓什么是虚拟地址空间?
- 😊我们先来看这样的一个程序:
- ⚠️感性的认识虚拟地址空间
- 😊Linux具体是怎么实现进程地址空间的
- ❓为什么会存在虚拟地址空间?
❓什么是虚拟地址空间?
虚拟地址空间是操作系统为了实现进程管理所设定的一种虚拟化解决方案,通过虚拟地址空间可以让每个进程都认为自己可以独占系统资源。
学过C语言的肯定听说C/C++地址空间、里面有代码区、已初始化区、未初始化区、堆区、栈区等空间。堆向下生长、栈向上生长等等。但是其实我们对于他们的了解其实还并不全面。
😊我们先来看这样的一个程序:
#include <stdio.h>
#include <unistd.h>
int global_value = 100;
int main()
{
pid_t id = fork();
if(id < 0)
{
printf("fork error\n");
return 1;
}
else if(id == 0)
{
int cnt = 0;
while(1)
{
printf("我是子进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value);
sleep(1);
cnt++;
if(cnt == 10)
{
global_value = 300;
printf("子进程已经更改了全局的变量啦..........\n");
}
}
}
else
{
while(1)
{
printf("我是父进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value);
sleep(2);
}
}
sleep(1);
}
在子进程没有改变全局变量global_value的时候,可以看到父子进程之前的global_value以及global_value的地址都是一样的;
在子进程改变全局变量global_value之后,可以看到父子进程的值有了区别,子进程的值是300,父进程是100,因为我们知道进程具有独立性,再往后看奇怪的事情发生了,为什么父子进程的这个全局变量global_value的地址是一样的呢?
既然地址一样那为什么值也不是一样的呢?所以说我们曾经学习过的语言级别的地址(指针)绝对不是在硬件层次上对应的物理地址。
⚠️对于这段代码奇怪的现象的解释在 为什么会存在虚拟地址空间?中的第二条有介绍。
那上边用&取出的这个地址是什么?其实这个地址叫做虚拟地址。
⚠️感性的认识虚拟地址空间
首先,每个进程都会认为自己是独占系统资源的,事实上并不是,操作系统会给每个进程“画饼”,就好比提前跟进程说,我这些系统的资源都是你的,但事实上每个进程一般也用不了这么多的资源,操作系统也不可能把所有的资源给一个进程,那么虚拟地址空间其实也就是操作系统给每个进程画的大饼。
那么操作系统是怎么给每个进程“画饼”的呢?
首先给每个进程画饼,每个进程需要被管理,同时给每个进程画的大饼也就是进程地址空间(进程的虚拟地址空间)它也要被管理好,乱画饼万一不够了怎么办?怎么管理呢?管理的本质就是对数据的管理,一说到管理那就要提到先描述再组织了,是一个建模的过程,进程地址空间的本质其实就是Linux内核的一种数据结构(mm_struct)。
进程地址空间描述的基本空间大小是字节。在32位操作系统下,就有 2 32 2^{32} 232个地址,一个地址占一个字节,那么总共就是4GB的空间范围。每一个地址都要有唯一的标识,那么其实用32位二进制序列就可以唯一标识这些地址。这些序列就是所谓的虚拟地址。
😊Linux具体是怎么实现进程地址空间的
我们知道操作系统管理每个进程时要用PCB结构体(在Linux中是task_struct)来管理,而task_struct中就包含操作系统给每个进程画的大饼mm_struct(也就是对每个进程的进程地址空间做管理的结构体),在32位系统下这个mm_struct假装把4GB的虚拟空间全部都给了每个进程。此时每个进程都会认为自己是独占系统资源的。
下面是Linux内核源代码PCB中的mm_struct结构体的指针。
我们转到mm_struct的定义来看,mm_struct内部有很多无符号整形变量start和end来标识每个区域的大小,假如栈区的start和end分别是1和4,那么栈区域起始地址就是1,栈区域的结束地址就是4,这之间的区域就归该进程的栈使用,其他区域也是这样的。就是下面红圈圈住的一部分代码。
就像其他区域当进程被创建之后就不可以被修改了,但是栈区和堆区是可以被修改的,那么其实这两个区域的区域调整本质上也就是修改各个区域的start和end的值即可。
❓为什么会存在虚拟地址空间?
1️⃣如果让进程直接访问物理内存,万一进程越界直接非法访问我们在磁盘的重要信息呢?所以说让进程直接访问物理内存是非常不安全。页表让虚拟地址和物理地址做映射的同时还会监视进程非法的访问,一旦有非法的访问那么就会被拦截。
2️⃣地址空间的存在可以更方便地进行进程和进程的数据代码的解耦,保证了进程独立性这样的特征。
解释:
就像文章一开始写的那个父子进程的程序,为什么会出现那样的现象呢?
因为在创建子进程的时候,子进程把父进程的进程地址空间全部都拷贝了一份,在子进程没有改变global_value的值之前,两个进程&global_value确实都一样,都位于虚拟地址的同一个地址上,当然此时通过页表转换成的物理地址也是一样的,然而当子进程改变了global_value的值之后,父进程就受到影响了,但是我们知道 进程具有独立性 ,一个进程对被共享的数据做了修改,如果影响了其他进程,那么进程就不具有独立性了。操作系统不允许这样的事情发生,在子进程把global_value的值修改了之后,操作系统会更改子进程页表原来的global_value与物理地址之间的映射关系,重新在物理地址(物理内存)的其他位置寻找一块空间来存放子进程改变之后的global_value的值。
但整个过程与虚拟地址没有关系,所以我们才会看到父子进程的这个&global_value的值一样,而global_value的值不一样的现象。
我们将任何一个进程尝试写入,操作系统先进行数据拷贝,然后更改页表映射,再让进程进行修改的现象叫做写时拷贝。
所以说操作系统为了保证进程的独立性,做了很多工作,也是煞费苦心。不同的PCB、不同的进程地址空间、通过地址空间、通过页表、让不同的进程映射到不同的物理内存处。
通过前面对于进程的讲解我们知道:进程=内核数据结构+进程对应的代码和数据
内核数据结构像PCB、以及PCB内的进程地址空间是独立的。进程对应的数据在需要独立的时候通过写时拷贝也能进行独立,所以操作系统就让进程理所当然的具有了独立性。
3️⃣通过虚拟地址空间,让进程以统一的视角来看待进程对应的代码和数据等各个区域。也就是说每个进程看待各个区域像是栈区、堆区的视角都是一样的,但他们通过页表映射就可以映射到物理内存的不同区域。编译器也以虚拟地址空间这样的视角统一地进行编译代码。规则是一样的,所以编译完即可直接使用。
解释:
虚拟地址空间的这套规则不能认为只有操作系统才会遵守对应的规则,编译器也要遵守对应的规则。编译器编译代码形成.exe
可执行程序的时候,也是按照虚拟地址空间的方式进行对应的代码和数据进行编址的。那么其实在磁盘上的可执行程序内部早就有地址了,程序在链接的时候就已经把地址填入到你自己的可执行程序里,让你的可执行程序在运行的时候可以找到对应的内容。(磁盘中可执行程序里的地址就是逻辑地址。)
补充:
- 虚拟地址空间是操作系统给进程画的大饼,进程要被管理,PCB内的进程地址空间也要被管理,所以都是先描述再组织。然后每一个进程都有它对应的进程地址空间,经过进程对应的页表映射,来完成CPU在进程的上下文当中进行寻址的工作。
- 程序加载到内存中,自然就具有了一个外部的物理地址。
- 物理内存其实现在有两套地址
1.标识物理内存中代码和数据的地址
2.在程序内部互相跳转的时候用的虚拟地址
进程地址空间上的虚拟地址和物理地址之间是通过页表来一一映射,建立映射关系的,每个进程是直接看不到内存上的物理地址的。