👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
一、初识进程地址空间
进程地址空间是操作系统为每个进程分配的内存空间。进程地址空间通常被划分成以下几个区域
可他们真的是这样分布的吗?我们可以分别打印地址来验证
【程序结果】
我们发现:从代码区到栈区,其地址确实是增长的。并且栈是向下增长,堆则是向上增长。
- 这里再来验证一个问题:为什么
static
修饰的局部变量不会像普通的局部变量一样在函数执行结束时被销毁?
我们可以打印出static
修饰的局部变量的地址:
【程序结果】
我们发现,打印出来的地址和全局数据区的地址非常接近,因此可以得出一个结论:static
修饰的局部变量在编译的时候就已经被编译到全局数据区了。
二、验证是否发生写时拷贝
在往期的博客中,我们提到:fork()
创建的子进程共享父进程的代码和数据,而进程之间具有独立性,如果子进程修改了父进程的数据,那么势必会影响父进程,因此,如果子进程要修改父进程的数据,操作系统会为子进程分配一块新的内存空间,复制需要修改的数据,并且在新的内存空间上进行修改,而不会影响到父进程的数据。我们称这种过程为写时拷贝。
接下来,我们用代码来验证:当子进程要修改父进程的数据时,操作系统是否会真的会为子进程分配一块新的内存空间?
【程序结果】
可以发现:子进程确实成功修改了父进程的数据,而不影响父进程代码和数据,但是这有一个问题:为什么这里打印出来的地址是一模一样的?子进程要修改父进程的数据,不是会发生写时拷贝吗?操作系统不应该要为子进程分配一块新的内存空间吗?
首先可以得出第一个结论:以上的地址绝不可能是物理地址(计算机内存中的实际地址)。
三、分页机制
事实上,这种地址叫做虚拟地址 ,也称线性地址 ,并且我们平时所使用的地址全部都是虚拟地址 ,因此进程地址空间通常也被称为虚拟地址空间。而物理地址对于我们用户是看不到的,它统一由操作系统管理。
因此,操作系统必须负责将虚拟地址转化成物理地址。那么操作系统是如何通过虚拟地址找到物理地址的呢?
-
Linux
操作系统除了要为进程创建结构体对象task_struct
(表示进程的数据结构,包含了进程的所有属性,如进程标识符PID
);除此之外,操作系统还会为每个进程创建进程地址空间结构体对象mm_struct
(存储了进程的地址空间信息,包括堆、栈等)。可以通过task_struct
对象找到对应的进程地址空间。 -
虚拟地址和物理地址该如何构成联系呢?操作系统还需为每个进程创建页表 ,
Linux
操作系统系统会通过分页机制 来管理虚拟地址和物理地址之间的映射关系,用于将虚拟地址映射到物理地址。当程序访问虚拟地址时,操作系统会根据页表将虚拟地址转换为物理地址。 -
总结:
进程 = PCB + mm_struct + 页表 + 代码和数据
bash
struct mm_struct //默认划分的区域是4GB.
{
/* ... 其他字段 ... */
unsigned long start_code, end_code; /* 代码段的开始和结束地址 */
unsigned long start_data, end_data; /* 数据段的开始和结束地址 */
unsigned long start_brk, brk; /* 堆的当前和最大结束地址 */
unsigned long start_stack;/* 栈的开始地址*/
unsigned long arg_start, arg_end; /* 命令行参数的开始和结束地址 */
unsigned long env_start, env_end; /* 环境变量的开始和结束地址 */
/* ... 其他字段 ... */
};
因此,我们就可以解释为什么以上打印出来的地址是一样的了:
当父进程创建子进程时,操作系统会为新创建的子进程创建一个内核数据结构task_struct
,子进程会以父进程为模板初始化其内部的结构体对象。每个进程都需要一个地址空间,所以子进程也会复制父进程的地址空间。子进程同样需要一个独立的页表结构。
开始时,父子进程的页表指向相同的物理地址,这也就是为什么父子进程能够共享代码和数据的原因。但当子进程尝试修改数据时,比如将data
从0
改为985
,操作系统会识别到这个修改操作。那么操作系统就会在页表通过虚拟地址找到物理地址,在物理内存中进行写时拷贝,即为这个变量开辟新的空间进行修改,那么对应的物理地址也要更新为新开辟空间的地址,而虚拟地址不发生变化。
这样,父子进程在物理内存上的值就被分开了。通过这种方式,子进程能够独立地运行,而不会受到父进程的影响。
所以子进程在打印时,显示的地址与父进程相同,这是因为子进程继承了父进程的地址空间(虚拟地址)。但在实际访问时,读取到的内容是不同的,这是因为父进程通过页表找到的是物理内存中的某个地址,值为0
;而子进程通过自己的页表找到的是物理内存中的另一个地址,值为985
。因此,虽然应用层面上看到的虚拟地址相同,但实际的值却是不一样的。
四、为什么要有进程地址空间/虚拟地址空间
- 让进程以统一的视角看待内存
进程地址空间的存在可以让进程以统一的视角看待内存,因为进程地址空间将内存抽象为一组连续的虚拟地址,使得程序员可以将内存视为一个统一的地址空间,而不需要关心内存的实际物理位置。
举个例子来帮助理解:假设一个程序需要同时访问内存中的两个变量,这两个变量存储在内存的不同位置。如果没有进程地址空间,程序员需要知道这两个变量的实际物理地址,并使用不同的方式来访问它们。但是有了进程地址空间,程序员可以将这两个变量都视为位于内存的连续地址空间中的一部分,使用统一的方式来访问它们,使得程序的编写更加简单和直观。
- 保护物理内存
进程虚拟地址空间可以让我们访问物理内存的时候,增加一个转换的过程,在这个转化的过程中,可以对我们的寻址请求进行审查,一旦异常访问,直接拦截,该请求不会到达物理内存,起到保护物理内存的作用
举个例子来帮助理解:假设有两个进程 A 和 B,它们分别运行在操作系统中,并且拥有独立的地址空间。如果进程 A 尝试访问进程 B 的内存空间,例如读取或写入进程 B 的数据,操作系统会检测到这样的访问并拒绝执行,因为进程 A 只能访问自己的地址空间,无法越界访问其他进程的内存。这样,进程地址空间的存在保护了物理内存,防止了进程之间的内存相互干扰,提高了系统的安全性和稳定性。
- 能将内存管理和进程管理进行解耦
因为进程地址空间为每个进程提供了独立的内存空间,使得内存管理和进程管理可以分别进行,彼此之间相互独立。这样做的一个例子是,当一个进程被终止时,操作系统可以简单地释放该进程所占用的内存空间,而不需要考虑其他进程的内存状态,从而实现了内存管理和进程管理的解耦。
举个例子来帮助理解:假设有两个进程 A 和 B,它们分别运行在操作系统中。每个进程都有自己独立的地址空间,其中包含了代码、数据和堆栈等内存区域。当进程 A 被终止时,操作系统可以简单地释放进程 A 所占用的内存空间,而不需要考虑进程 B 的内存状态。这样,内存管理和进程管理之间的关系就被解耦了,操作系统可以更灵活地管理内存,而不受进程的影响。