【Linux系统编程】--虚拟地址空间

一、程序地址空间

我们回顾一下,前面学习C语言的时候,我们对指针这个内容肯定是非常熟悉的,那么就有一个疑问了,我们取地址,得到的地址是物理内存地址么?

不过验证这个问题之前,我们先回顾在C语言学习的时候,看到的一个关于内存的图:

那么我们在内存中,其对于我们的数据的存储方式是如上的方式,下面我们可以通过代码验证:

这里是定义两个全局变量,然后一个是初始化的,一个是未初始化的。

总体验证的代码如上,下面我们运行看看结果:

第一个打印的是程序的入口地址,也就是文本段,也就是正文代码,可以看到其地址是最小的。然后我们看第二个,就是初始化数据的地址了,可以看到其在正文代码的上面,然后未初始化数据,其就还要再大一点,然后再往上就是我们的堆区,然后就是栈区,最上面就是命令行参数和环境变量的地址。

然后我们看到上面的图片,堆区的是向共享区增大的,然后栈区是向下增大的。

如图所示,在我们的栈空间中,其地址是从高地址开始使用的,然后对于数组的话其使用方式,图中也很客观的表现出来了。

堆和栈是相向而生的。

二、进程虚拟地址空间

前面我们验证了,在我们的内存中,其对应的数据的存储方式,那么我们的问题还是解决,我们在C语言中使用的地址是我们的物理内存么?

其实我们在C语言中使用的内存是虚拟地址空间,下面我们来看一段代码的运行情况:

如上,我们创建一个全局变量,然后我们在父进程的执行的代码中,对这个全局变量进程++操作。

下面我们看其运行结果:

可以看到,上面我们对g_val取地址,两个进程之间的地址都是一样的,但是呢g_val的值是不一样的,那么我们就有疑惑了,这个咋可能一个地址,两个不一样的值的。

所以我们在C语言中使用的地址,并不是实际上的物理内存的地址。而且虚拟地址。

实际上,物理地址我们用户是看不到的。

那么上面的情况我们如何解释呢?

前面我们提到,我们的子进程,其会和父进程共享数据和代码,那么我们发现,这个时候,又不共享了。

实际上,我们的进程和物理内存地址之间,还存在一个虚拟内存地址,虚拟内存地址和物理内存地址之间,存在一个页表将虚拟地址和物理地址之间进程映射。而且这个虚拟地址是每个进程都会有的。当我们的父进程创建子进程,那么我们的子进程就会将父进程的数据和代码共享,创建一个属于自己的task_struct,但是此时,其数据和代码和父进程使用的是同一份,其虚拟地址中映射的物理地址和父进程的是一份。前面我们也讲过,父进程和子进程之间有一个写时拷贝,只有当两个进程之间谁对数据或者代码进行修改了,那么就会在物理内存中为其申请一个空间,然后就是存放修改后的数据和代码。

上面我们的代码中虽然看到两个进程的地址是一样的,但是实际上在物理内存中,映射的已经不是同一块空间了。

如上图所示。

在我们进程的task_struct中,还存在一个struct mm_struct结构体成员,其就是我们的虚拟地址表。

虚拟地址理解:

物理内存就是主板上那几根内存条,容量有限,所有程序抢着用。 操作系统 + CPU 的 MMU 硬件联手,给每一个进程单独画一张超大独立地址表 ,这张表里的编号就是虚拟地址。 进程眼里:我独占一整片完整内存,地址从 0 到很大,随便读写; 硬件底层:偷偷把虚拟地址翻译成真实内存条上的物理地址,不同进程相同虚拟地址对应完全不同物理内存,互不干扰。

举个生活化类比: 一栋大楼(物理内存)里有很多办公室(进程)。 每个办公室员工(程序代码)手里拿到一张专属楼层图纸(虚拟地址空间),图纸上楼层编号全都是从 1 层开始排。 大楼管理员(OS+MMU)手里有对照表: A 办公室图纸的 5 层 → 大楼真实 105 房间 B 办公室图纸的 5 层 → 大楼真实 205 房间 员工只看自己图纸,永远不知道大楼真实房间号,也看不到别的办公室内容。

那么我们的虚拟地址是如何划分呢?

其就是通过一个结构体,将各个区域,通过一些变量,然后规定这些范围是那个区域的:

页表是如何工作?

首先我们的页表,实际上不止只有虚拟地址和物理地址之间的映射,其还会存在有权限还有是否存在的两个标志位。

那么为啥我们的全局变量是生命周期是全局的呢?

这是因为其本质上,生命周期是随着进程的,其在整个进程生命周期中,对应的是否存在的标识位一直是1。

还有一个标识位,就是权限位,其是标识我们这个映射的物理地址是否可以被访问,是否可以对其进行修改等,也就是我们的可读可写。

那么为啥我们的常量字符串和代码我们只能读,其本质就是在这个权限位的时候做了限制。

如上是我们对于虚拟地址空间的初步理解,后续我们会对其进行更深入的学习。

相关推荐
不会C语言的男孩1 小时前
Linux 系统编程 · 第 3 章:文件 I/O 基础
linux·服务器
硬件工程师宝典1 小时前
I2C从入门到精通之一:I2C的历史起源和综合简介
服务器·嵌入式硬件·硬件架构·i2c
吠品1 小时前
.NET 8 单文件发布:把 exe 和一堆 dll 打进一个文件里
服务器·数据库·windows
dadaobusi2 小时前
Linux内核完成大量内存/调度/时间子系统初始化的关键阶段
java·linux·前端
唐墨1232 小时前
关于linux kernel错误码为负数编码这件事情,我个人的一些看法
linux·运维·服务器
Full Stack Developme2 小时前
Linux Shell 教程概览
linux·前端·chrome
IT WorryFree2 小时前
基于Fortinet MIB实现设备资产管理完整方案
运维·服务器·网络
网络系统管理2 小时前
第八届江苏技能状元大赛选拔赛信息通信网络运行管理项目模块D网络服务与系统运维-Linux样题
linux·运维
凡人叶枫2 小时前
Effective C++ 条款24:若所有参数皆须要类型转换,请为此采用 non-member 函数
linux·前端·c++·算法·嵌入式开发