进程地址空间
进程地址空间是操作系统为每个进程分配的一块内存空间,用于存储进程的代码、数据和堆栈等信息。进程地址空间是逻辑上独立而相互隔离的,每个进程拥有自己独立的地址空间,进程之间不能直接访问彼此的地址空间。
-
代码段:存放可执行程序的机器指令,也称为文本段,也有可能包含一些只读的常数变量,例如字符串常量等。这些指令在程序运行时是只读的,保存程序的执行逻辑。
-
数据段:存放程序的全局变量、静态变量和常量,这些数据在程序运行时可以被修改。
-
堆:存放动态分配的内存,如使用malloc或new申请的内存,在程序运行时可以动态地进行内存分配和释放。
-
栈:用于存放函数调用所需的局部变量、函数参数和函数返回地址等。栈是一种先进后出的数据结构,每个函数调用会在栈上分配一块内存空间,函数返回后会释放该内存空间。
-
环境变量区域:存放程序的运行环境变量,如PATH、PWD、HOME等。
虚拟地址 (线性地址)
其实我们每个进程具有的地址空间并不是计算机内存,而是存放在计算机内存中。而且每个进程PCB内部有一个指针会指向这块进程地址空间。
#include<iostream>
#include<unistd.h>
using namespace std;
int g_val=10;
int main()
{
pid_t id=fork();
if(id==0)//子进程
{
int k=3;
while(1)
{
cout<<"子进程:&g_val="<<&g_val<<",g_val="<<g_val<<endl;
k--;
if(k==0)
{
g_val=20;
cout<<"子进程修改数据后-----"<<endl;
}
sleep(1);
}
}
else//父进程
{
while(1)
{
cout<<"父进程:&g_val="<<&g_val<<",g_val="<<g_val<<endl;
sleep(1);
}
}
return 0;
}
就拿以上代码而言,g_val是一个已初始化的全局变量也就是进程地址空间的数据段区域,但是经过运行以后,我们发现该数据会有两个不同的值,这个是因为fork()函数会创建子进程,而子进程与父进程的代码是共享的,而数据是分别独立存放的,并且满足写时拷贝。但是为什么地址还是不变呢,同一个物理地址下的值不可能会有不同的值啊。所以此时可以断定这一定不是物理地址,那就是虚拟地址 。
进程地址空间是不具有存储数据的能力的,所以我们的数据实际上还是会存放在计算机的内存当中的。 因为根据冯诺依曼体系结构可以知道CPU处理数据是通过与内存进行交互的。而我们的数据存在内存中,也有其数据对应的物理地址,所以此时我们进程地址空间上的虚拟地址想要访问对应物理地址的话,肯定是需要中间“桥梁”的。
页表(映射表)
这就是虚拟地址与物理地址交互的“桥梁”。操作系统会为每个进程构建一个对应的映射表。
所以我们访问数据的过程其实是通过进程地址空间的虚拟地址去页表中寻找对应的物理地址,再访问物理地址所对应的数据。
所以接续上面的问题,为什么同一地址下的值不一样?
这其实因为这是虚拟地址,父进程在创建子进程时会将自己的的进程地址空间和页表都拷贝给子进程,而接下来无论是更改父进程的值还是子进程的值,并不会直接修改物理地址下的数据,因为此时的这个物理地址下的数据不止被一个进程所指,所以改变某个进程的值时会再单独在物理内存中开辟一个空间存放原来的值,然后再修改新空间的值(写时拷贝)。最后只需要改变页表上虚拟地址所对应的物理地址即可。从而达到了父子进程互不干扰的效果。所以两个页表中尽管虚拟地址相同,各自对应的物理地址也是不一样的,从而访问的数据也各不相同。
管理进程地址空间(到底什么是进程地址空间)
我们的每一个进程地址空间都是经过区域分区的,每个区域的数据种类不同,而为了对进程地址空间做管理则必然是有一个类似于PCB(进程控制块)的的结构体,而这个结构体的名称就是mm_struct,而这个结构体里用long long类型的变量存放了有关进程地址空间的所有区域地址划分(也就是每个区域(例如栈区)的起始位置),而区域之间的地址可以被我们直接用来使用,也正是虚拟地址。而进程PCB中也有一个指针指向这个结构体。
所以此时对进程地址空间就有了一个全新的理解,进程地址空间其实就是mm_struct这样的内核数据结构
为什么要有进程地址空间和虚表
我们的虚表存在的目的就是将虚拟地址和内存的实际地址映射起来,并且提供数据的权限等作用,而我们为什么需要进程地址空间呢,直接将可执行程序的代码和数据存在内存,并直接访问内存不更好嘛?非也非也!
- 我们知道可执行程序在运行时加载的的所有代码和数据并不是存在进程地址空间的,而是存在计算机的内存当中的。而内存中存放的数据并不一定是像进程地址空间一样将不同数据进行划分,而是可以将代码数据存在内存任意的空闲位置处。所以数据如果存在内存中的话是十分随意地,因此进程地址空间存在的第一个目的是:让所有进程都以同一种方式看待内存空间数据分布,并且能够将内存中无序的数据代码映射到地址空间时划分的井然有序。所以接下来即使进程挂起或代码数据被切换的话都会影响进程代码数据的物理地址,但是并不会影响其虚拟地址,所以进程PCB所记录的进程地址空间也不受影响。
- 我们知道进程地址空间的代码段是只读的,而数据段是可读可写的,所以每个区域的数据访问权限并不是相同的,所以页表的每一个映射行关系里其实还有一个权限字段,存放的就是该地址下数据的访问权限字段。所以这样就可以防止程序意外访问或修改其进程的内存数据。
- 将进程管理和内存管理解耦
进程切换的理解
我们知道每个进程的地址空间都有自己所对应的页表,可内存中肯定不止一个进程的,那么当进程运行,CPU调度该进程时是如何将该进程地址空间与自己的页表对应起来的呢??? 其实CPU中有一个寄存器CR3里存放着进程地址空间所对应的页表地址(物理地址),所以当进程在切换运行时,寄存器CR3所存的页表地址都是不一样的,所以该寄存器里的内容肯定就在该进程的硬件上下文 中。
(进程的硬件上下文不仅仅是一个寄存器。硬件上下文是一个包含多个部分的数据结构,它记录了进程在当前执行位置和状态的各种硬件信息。例如CPU寄存器(记录了进程在执行时各个寄存器的状态,如通用寄存器、程序计数器(用于存储当前指令执行位置的寄存器)等)、内存页表等)进程PCB中包含了记录进程硬件上下文的信息,并且在进程切换时用于保存和恢复进程的硬件上下文(简单来说就是当某个进程被CPU调度时,该进程PCB就会将有关该进程的上下文数据都导入进程硬件上下文当中,并且继续执行该进程的后续代码数据,而当进程切换出CPU时再将该进程硬件上下文的数据都拷贝到进程PCB中保存起来便于下一次的调度)。
缺页中断的部分理解
其实我们的页表映射行中还有一个字段信息:标识着该物理地址对应的空间是否在内存中分配给该数据以及该空间是否有内容。
首先我们要知道在程序执行之前,操作系统会为进程分配一段虚拟地址空间,然后在程序执行过程中将其映射到物理地址上。当我们的虚拟地址要被操作系统访问了,那么就会判断页表中字段信息的数据状况,如果页表字段信息中标识的内存和内容都是无的话,此时的访问请求就会暂停,但是还没完,操作系统会先在物理内存中开辟空间,在可执行程序中找到该虚拟地址所要访问的数据代码段落,并将该代码段和数据加载到开辟的内存当中,此时将物理地址填充到页表中,然后把标志字段信息的内容修改成内存已分配,内容已填充。最后访问请求就不再是暂停标志,所以可以继续访问数据信息。(这属于内存管理,进程并不知道该过程)
所以我们执行一个程序的时候并不是将程序的所有数据全部一下子加载进内存当中(全部加载内存不一定放得下)而是分部分的加载进内存,也就是取决于操作系统想要访问的数据。