龙岗专业做网站,网络营销的功能是什么,嘉兴类网站系统总部,游戏网站建设的目的❤️前言 大家好#xff01;这里是好久没有营业的大懒虫lion#xff0c;今天要和大家聊的内容是我最近新学习的关于进程地址空间的相关知识。
正文 当我们使用C/C语言进行内存管理时#xff0c;经常会接触到这样的一张图片#xff1a; 它常常被我们称作程序地址空间#…❤️前言 大家好这里是好久没有营业的大懒虫lion今天要和大家聊的内容是我最近新学习的关于进程地址空间的相关知识。
正文 当我们使用C/C语言进行内存管理时经常会接触到这样的一张图片 它常常被我们称作程序地址空间在我们编写自己的代码时都是在这样的内存布局的基础上进行思考我们访问内存中定义的变量访问内存中存储的代码数据……在我们的眼中这样的空间布局似乎是真实存在于物理空间中但是事实真的如此吗 首先让我们来看看下面的这一段代码
#include stdio.h
#include unistd.h
#include stdlib.hint g_val 0;
int main()
{pid_t id fork();if (id 0) {perror(fork);return 0;}else if (id 0) { //childprintf(child[%d]: %d : %p\n, getpid(), g_val, g_val);}else { //parentprintf(parent[%d]: %d : %p\n, getpid(), g_val, g_val);}sleep(1);return 0;
}//输出结果:
//与环境相关观察现象即可
//parent[2995]: 0 : 0x80497d8
//child[2996]: 0 : 0x80497d8 我们很容易就可以推断出这段代码的结果当我们调用fork()函数产生一个子进程这个子进程以父进程为模版并且子进程并没有对全局变量进行修改。 那么让我们将这段代码稍加改动
#include stdio.h
#include unistd.h
#include stdlib.hint g_val 0;
int main()
{pid_t id fork();if (id 0) {perror(fork);return 0;}else if (id 0) { //child,子进程肯定先跑完也就是子进程先修改完成之后父进程再读取g_val 100;printf(child[%d]: %d : %p\n, getpid(), g_val, g_val);}else { //parentsleep(3);printf(parent[%d]: %d : %p\n, getpid(), g_val, g_val);}sleep(1);return 0;
}//输出结果
//与环境相关观察现象即可
//child[3046]: 100 : 0x80497e8
//parent[3045]: 0 : 0x80497e8 我们发现在子进程和父进程中全局变量g_val的值不相同但是地址却是一样的这就和我们当初在语言层面上看待内存空间的视角有较大出入了(既然这个变量用的是同一个地址值进行存储那么为什么在子进程和父进程中这个变量的值却会变得不同呢)。 我们以之前的观念无法解释这样的问题那么我们不妨做出这样的推论这里的地址并不是真的物理地址而是操作系统用某种方式将物理地址转化成的虚拟地址事实也确实是这样并且我们平时在使用C/C编写代码时使用的地址也全部不是物理地址而是操作系统所修饰出的虚拟/线性地址。
引入进程地址空间 当我们发现了这样的一种现象单凭现有的知识概念已经无法对虚拟/线性地址有更详细的了解也不能对这种现象进行更加深入的解释那么我们便可以引入新的概念——进程地址空间通过研究进程地址空间从而初步理解这种现象。 我们所说的进程地址空间其实就是上面所说的“程序地址空间”但是相较来说前者的描述更加准确。前者则是当我们学习了进程相关知识在操作系统的层面上看待地址空间的描述方式而后者是当我们在语言层面上编写代码、看待地址空间的描述方式。 我们都知道当我们创建出一个进程他们都会带有自己的内核数据结构那么今天我们研究的进程地址空间也便是进程自带的内核数据结构中的一种。当我们要对它进行管理就需要对应的指针对应的指针存于哪里呢自然是存在于描述Linux进程信息的进程控制块——task_struct中。 当我们使用语言编写程序时是以统一的视角来看待内存空间但是每个进程的task_struct不相同进程地址空间也便是相互独立的那么独立的每一个进程是如何做到以统一的视角看待内存的呢
引入页表 这里我们又必须要提出一个新的内核数据结构——页表通过页表我们便可以完成上述的设计那么上述的问题具体是如何解决的呢 页表的主要作用是与对应进程的地址空间相配合从而建立起线性地址与物理地址之间的映射关系同时这种映射是由系统内核自己完成的。上述问题的解决方式简单来说就是我们通过页表在线性地址与物理地址之间加上了一层映射关系而且这种映射是由操作系统自己分配的那么独立的每个进程在物理地址中存储的数据在正常情况下便不会发生冲突这时每个进程便只需要看好自己的地址空间即可。 于是我们便了解到了线性地址与物理地址之间的相互关系每个进程需要管理自己进程地址空间中的线性地址而操作系统通过页表进行从线性地址到物理地址的映射对应操作。 现在我们可以反到上面去研究和解释父子进程中的全局变量问题。我们上面得出推论全局变量g_val使用的并不是直接的物理地址而是虚拟地址线性地址那么其中发生了哪些变化最后的结果又是如何产生的呢
我们可以画出如下的图像 我们从代码进行到fork()后开始分析父进程创建了一个新的子进程这个子进程继承了父进程在创建它之前的所有代码操作也就是地址空间和页表中存储的数据是一致的也就是这两者目前的代码、变量在物理空间中其实是占用着相同的空间的。这里又新引入一个页表的行为当子进程对先前的线性地址中的数据进行修改那么此时就会发生写时拷贝将另一块物理地址映射给当前的线性地址但是不会将线性地址的值进行改变(也没有必要)。这样我们便可以理解这段代码的运行结果这两个进程中的g_val其实不是同一个变量它们所占的相同线性地址以页表对应映射出的物理地址是不同的不同的物理地址中存储着不同的数据于是最后打印出的结果便是相同的线性地址中存储着不同的值。
深入进程地址空间 进程地址空间究竟是什么这可以通过我们早期对C语言指针的研究进行解释以32位机器举例简单来说就是将内存地址以32根数据总线的01信号进行二进制的编址32根总线可以表示出2^32个不同的数字对应着这么多单位的地址(一个单位即为一个字节)他们分别管理着对应的内存[0,2^32)也就是地址空间的地址范围。 那么如何理解进程地址空间中的区域划分呢 地址空间中有许多不同的分区这些区域的划分其实就是在一大块线性地址中划出一些分界线以此就可以分出小块的具有不同数据和功能的区域。在进程地址空间对应的内核数据结构中其实就存储了许多不同的用于分界的数据其中包括这个区域的起始地址和结尾地址等。 当我们需要调整修改地址空间中的区域划分也就是只要对涉及的各个结构对象中的数据进行修改。
// 各个分区的结构体对象就类似如下结构
struct area
{int start;int end;// ...
};
深入页表 进程地址空间中的不同区域往往含有不同的属性存储着不同性质的数据(只读、读写)这样的情况显然不是简单的区域划分就可以做到的那么我们现在来探讨操作系统如何做到这点。 进程地址空间需要页表与物理地址相勾连那么操作系统便可以在这中间的操作中加以限制从而进行不同分区数据的管理。 事实上页表中除了线性地址和物理地址的映射关系以外还存有对应线性地址的数据读写权限这样当进程想要修改某个内存中的数据时便可以进行先一步的识别如果这一块内存不能修改那就会进行报错。也就是说物理内存本身其实是没有所谓的分区、读写权限的概念的所有的这些都是在上层进行设计的都是由操作系统内核限定和操作的。
使用地址空间和页表的好处
让进程以统一的视角看待内存。使用进程地址空间和页表可以让我们在访问内存的时候增加一个过程在这个过程中我们可以对访问内存的行为做检查错误的访问将不会到达物理内存这样就保护了物理内存。有了进程地址空间和页表我们可以做到将进程管理模块和内存管理模块进行解耦合。
结语 以上就是最近接触到的关于进程地址空间和页表的一些知识希望能帮助到阅读此篇博客的读者。