在学习C语言时,我们经常会用到“&”符号,而下表,那么检索到的地址是否与真实的物理地址相对应呢?让我们写一个分步验证。
从上图不难看出,从正文**,到命令行参数环境变量,地址是从低到高,我们来写一段**来验证一下。
从这里不难发现,地址确实是按从高到低的顺序出现的。
那么命令行参数和环境变量呢,我们再写几组**。
从上面的结果不难发现,堆栈和堆的地址是相对的,命令行参数的地址确实在地址空间的顶部。
父进程和子进程同时创建,然后由子进程修改全局变量的结果。
我们发现 g val 的值在 5 秒之前没有变化,并且 g val 地址在父进程和子进程中都是相同的,这没什么好混淆的。
五秒钟后,我们更改了 g val 的值,但这次 g val 的值不同,但地址相同。 那么我们错了,还是计算机错了?显然,计算机肯定不会出错。 此地址是实际存在的物理地址吗?绝对不是,这是计算机给我们的线性地址的虚拟地址。
进程地址空间:那么我们平时说的程序的地址空间是不对的,应该叫进程地址空间,那么怎么理解呢?什么是地址空间:每个进程都有一个大小为 [0,4GB] 的进程地址空间。 那么为什么会这样呢?
父进程在创建子进程时的行为类似于浅拷贝,因此子进程继承了大量父属性,包括页表,页表是映射虚拟地址和物理地址的关系表。 每个进程都有自己的页表。
当子进程想要修改数据时,它会触发写入时复制。 操作系统会介入,为子进程准备一个特殊的空间来存储修改后的数据,保护进程的独立性。 但是,子流程页表对应的虚拟地址未被修改,但子流程页表上的虚拟地址对应的物理地址已被修改。
不仅有虚拟地址和物理地址的映射,还有权限位。 当子进程尝试修改数据时(**默认不修改),会触发写入时复制,造成丢页中断,操作系统会介入判断写入是否合法,当行为合法时,操作系统会为子进程打开物理空间。 然后,子进程写入并修改自己的数据。
无论 C C++ 语言如何,“&&”打印进程的虚拟地址,因此我们上面观察到的地址没有改变。 每个进程都有一个进程地址空间,操作系统将组织这些进程地址空间的管理,如上所述。 简单来说,进程地址空间就是一个特定的数据结构对象。
那么进程地址空间中的属性是什么呢?
根据 Linux 发布的源 **,在任务结构中存在 mm struct 这样的结构,它也在进程控制块中,我们可以看到上面有一些字符,比如 “strart” 和 “end”,不难猜测这是进程地址空间的区域划分, 并且其自身区域内的内存资源可以被进程使用,避免越界的问题。
我们的地址空间没有能力保存我们的**和数据,无论是**还是数据存储在物理内存中。 该过程为我提供了一个表格,其中显示了虚拟地址和物理地址之间的关系。 然后,这会将进程地址空间(虚拟线性)地址转换为物理内存!
2023 让我们翻开这一页:为什么我们需要进程地址空间和页表?
a.将物理内存从表上的无序状态映射到有序状态。
b.使用页表时,进程管理和内存管理是分开的,操作系统决定何时释放内存,然后将物理地址写入页表。 这将进程管理和内存管理分离。
c.地址空间加上页表是保护内存安全的重要手段,进程不会被允许随便访问内存(非法访问可以被页表截获)。
注意:CPU 上有一个 CR3 寄存器,用于存储页表的物理地址。
注意:当我们请求内存时,我们会在进程的虚拟空间中这样做,并且操作系统不会在物理内存中为我们打开物理空间(如果用户尚未尝试写入)。 只有当用户实际尝试在空间上写入时,操作系统才会打开物理空间并在页表上建立映射。 这种将虚拟地址的创建与物理地址的创建分开的行为大大提高了操作系统的效率,因为用户不一定立即使用空间,避免了内存空闲和资源浪费。