> 技术文档 > 【Linux】进程地址空间

【Linux】进程地址空间


1.程序地址空间回顾

以32位操作系统处理器为例
【Linux】进程地址空间
1.栈向地址减小方向增长,栈底位于高地址,栈顶位于低地址,后定义的变量后入栈越靠近栈顶所以地址小;
2.堆由低地址向高地址生长与栈是相对生长的
3.代码区是程序存储可执行代码的区域,函数的地址就是其在代码区中的位置。
4.字符常量和字符串常量所在的区域就是代码区(文本段),代码区具有只读属性,用于存放程序的机器指令以及这些不可修改的常量数据。
5.初始化数据段和未初始化数据段(BSS)段主要针对的是全局变量和静态局部变量,区别在于变量有无被初始化
6.static修饰的局部变量,在编译时已经被编译到全局数据区

2.引出问题现象

【Linux】进程地址空间
从运行结果可以看出,不可能同一个变量,同一个地址读取到了不同的内容,若变量地址是物理地址不可能出现上述现象
给出结论:
该地址绝不是物理地址,而是线性地址或虚拟地址,我们平时写的C/C++里面用的指针里面也都不是物理地址

3.引入新概念-进程地址空间

【Linux】进程地址空间
1.
每一个进程都要有进程地址空间(也叫虚拟地址空间),当子进程被创建后会拷贝一份父进程的进程地址空间,未进行修改时父子进程共用相同的物理内存,但各自拥有独立的进程地址空间,它并不是真正的内存,可以让进程以统一的视角区看待内存
2.
程序编译后生成的地址是虚拟地址,当程序运行时,页表会将虚拟地址映射到实际的物理内存地址(物理地址是内存硬件的真实地址)。这种映射对程序透明,程序无需关心物理内存的实际位置。
3.
当子进程对父子进程共同拥有的一个变量进行修改时,会先经过写时拷贝,由操作系统自动完成,会在物理内存中重新开辟空间,但该过程中虚拟地址是没有感知的不关心它,也不会被影响。所以最终父子进程不再共用一块物理内存,进行修改的一方的页表映射关系发生变化,父子进程各自指向一块物理内存
4.现象解释:
经过上述的宏观分析,明白了问题现象的原理,我们通过程序运行得到的地址是虚拟地址,子进程的内核数据结构主要以父进程为模板拷贝再添加一点自身信息,所以父子进程对同一变量的虚拟地址相同,但打印结果不同,是因为相同虚拟地址经过父子进程的各自页表映射到不同的物理内存,对应不同的变量,所以我们能看到相同变量在打印地址相同情况下具有不同的值。
其实变量在进行编译后都会转化成地址,变量名只是在应用层上方便我们区分取地址,但进入汇编后其实都是对应的地址。
父子进程中 “相同变量” 的虚拟地址相同,但物理地址可能因写时复制(修改操作)而分离,导致变量内容独立 —— 这是物理地址的分离,而非虚拟地址的变化。
前提是进程在被调度运行

4.谈细节理解

1.地址空间是什么?

背景知识:
在32位计算机中,有32位的
地址总线(输 CPU 要访问的内存或外设的地址,告诉硬件 要操作哪个存储单元)
数据总线(传输 CPU 与内存 / 外设之间的实际数据如指令、变量值等,实现数据的读或写)
控制总线(传输控制信号,如 “读 / 写命令”)
每一根总线有0和1两个状态,这是软件层面上的理解,计算机识别和处理二进制信号,硬件层面上就是高电平和低电平,分别对应1和0两个状态,以导线为介质的电信号传输中,设备之间信息的传递就是二进制信号的传递也就是高低电平的充放电状态变化

地址空间与地址总线相关,32位系统为例,有32跟总线,每根状态只有0、1,有2^32种信号组合,对应4GB大小空间
总结:地址空间就是地址总线排列组合形成地址范围的[0,2^32)区域

2.如何理解地址空间上的区域划分

1.地址空间的本质是内核的一个数据结构对象(mm_struct),类似PCB一样,地址空间也是要被操作系统所管理的,既然要管理那么注定要先描述再组织,那么就要通过数据结构对象来描述
2.地址空间范围内,称为线性地址,每一个最小单位都可以有地址,这些地址都可以被我们所直接使用,为了方便管理这些地址,要对线性地址进行start和end即可,也就是通过start和end划分出一个个区域,所以mm_struct中一定包含各种内存区域start和end的信息(如栈、堆、代码区),实现间接管理
3.进程被创建时会创建一个管理描述该进程信息的结构体对象tast_struct,同时也会创建一个管理描述地址空间信息的结构体对象mm_struct,并且每个进程的 task_struct中包含一个 mm 字段,指向该进程的mm_struct,每个进程都有各自的进程地址空间,这样设计便于管理

3.为什么要有进程地址空间

1.让进程以统一的视角看待内存:
数据理论上可以存在物理内存上的任意地方,但进程地址空间上同类型数据都统一存在一块空间上,可以用统一视角来看待一种类型数据。若让进程直接访问物理内存,当信息发生变化时,需要频繁在PCB中改变对应信息,大大增加了维护成本,同时不利于我们去进行相关操作因为信息可能经常发生变化;若通过进程地址空间+页表来对虚拟地址进行映射间接访问物理内存,当物理内存发生变化时,那么虚拟地址可以不用变化改变映射关系即可,由操作系统本身去完成,降低了PCB信息的维护成本和我们的使用成本
2.
增加进程虚拟地址空间可以让我们访问内存的时候,增加一个转换的过程,在这个转换的过程中,可以对我们的寻址进行审查,所以一旦异常访问,直接拦截,该请求不会达到物理内存,从而保护物理内存
3.
因为有地址空间和页表的存在,将进程管理模块和内存管理模块进行解耦合
即从进程角度不需要内存管理是如何实现的,由操作系统完成,后续会深入学习内存管理部分
【Linux】进程地址空间

5.初识页表

CPU中有cr3,是x86下寄存器,里面存的是当前进程的页表地址,是物理地址,本质属于进程的硬件上下文(是指当进程运行时,CPU 中一系列寄存器(以及部分硬件状态)存储的与该进程相关的数据集合)
进程切换的核心:
当操作系统需要暂停当前运行的进程(如时间片用完、发生中断或进程阻塞),并调度另一个进程运行时,必须先将当前进程的硬件上下文保存到内存中(通常存储在进程的内核栈或 task_struct 结构体中),再从新进程的内存中恢复其硬件上下文到 CPU 寄存器。
这个 “保存 - 恢复” 的过程确保了进程切换后,新进程能像从未被中断过一样继续执行。

页表有多个字段,目前已知第一个字段代表虚拟地址,第二个字段代表物理地址,第三个字段是读写权限的标记位,第四个字段表示对应的代码和数据是否已经被加载到内存(可以判断进程是否处于挂起状态)

当通过页表映射后的物理地址找到对应的物理内存位置发现代码和数据并没有被加载进来,操作系统会触发缺页中断。于是会吃重新开辟一块物理内存,找到可执行程序将其中代码和数据加载到其中,再把对应的物理地址重新填到页表当中重新构建映射关系,该过程自动完成,再重新执行该过程就可以找到目标数据

物理内存是没有权限管理的,物理内存 “不设防”,但 CPU 和操作系统为它 “加了锁”。
代码是只读的,加载到物理内存中也是写入,怎么解释?

原因是页表中映射关系后还有对应的读写标识符,要符合标识符才允许操作,页表通过权限管理不同内存区是否可读写
【Linux】进程地址空间
【Linux】进程地址空间
这里是访问只读内存,这是进程因非法内存操作被操作系统强制终止的错误

操作系统对大文件可以实现分批加载,也叫做惰性加载的方式,会把当前需要的数据加载到内存中去,按需申请分配,坚决不浪费内存空间,运行完了再加载另外一个,结果就是在页表中可以把可执行程序所对应的虚拟地址都填上去但物理地址不填,这样就不会把所有数据一次性加载到物理内存中去
这也是为什么下载大型游戏要几十个g的内存但照样可以流畅运行
共识:现代操作系统几乎不做任何浪费空间和时间的事情

a:从技术角度理论上,可以实现操作系统将进程的PCB、进程地址空间、页表都创建好,但不加载可执行程序进去,当需要调度执行是操作系统再惰性加载,就可以实现边使用边加载了。
b:实际上操作系统会将可执行程序中的一部分代码加载进来,还要预读以下可执行程序的格式,因为进程地址空间和页表的一些字段需要可执行文件信息(与编译原理有关,了解即可)

6.重新理解概念

1.进程具有独立性,为什么?怎么做到的?

1.因为每一个进程都有对应的PCB和mm_struct,同时cr3寄存器中存储对应页表的地址,进程切换时会将信息保存到对应PCB中,所以进程之间具有独立性
2.独立性是通过进程地址空间和页表做到的。我们运行程序所得到的都是虚拟地址,可以统一的角度去看虚拟地址,不用关心实际物理地址之间的变化,因为页表会自动帮我们映射虚拟地址到物理地址,而每一个进程都有自己的进程地址空间与页表,所以自己物理内存的变化不会影响其他进程,实现进程独立性

2.什么是进程

进程=内核数据结构(tast_struct&&mm_struct&&页表)+程序的代码和数据。
以前认为的内核数据结构只有task_struct

3.进程在被创建时,是先创建的内核数据结构呢?还是先加载对应的可执行程序呢?

经过上述分析,我们经常说把程序加载到内存,实际上不需要全部加载而且我们如何知道这个进程被创建?所以一定要先创建内核数据结构,将进程信息维护好后再慢慢加载可执行程序,可能程序一下就运行起来了,但并没有被完全加载完成

4.环境变量会被子进程所继承,具有全局属性

1.子进程的内核数据结构会以父进程为模板进行创建再添加一些自身信息,子进程初始时与父进程共享物理内存,仅当两者中任意一方修改内存数据时,内核才会为子进程复制修改的内存页。这使得子进程的内核数据结构在 “模板复制” 的基础上,通过延迟分配实现了高效的资源复用。一般不会主动修改环境变量,所以父子进程共用一块环境变量的物理内存,所以就相当于环境变量被子进程所继承
2.环境变量的 “全局属性” 核心体现在:
传递全局
通过进程树的继承机制,在整个进程层级中传递,形成跨进程的可见性;从逻辑上看,复制父进程的环境变量及环境变量表和命令行参数表后,子进程已经 “拥有” 了自己的环境变量副本(独立的虚拟地址空间),再共享物理内存看是否发生写时拷贝
影响全局
对单个进程的所有内部行为(代码、库、子模块)产生统一影响;
配置全局
系统级 / 用户级的统一配置,使其在特定范围(系统、用户)内普遍生效。