[Linux] 什么是进程地址空间?父子进程的代码时如何继承的?程序是怎么加载成进程的?为什么要有进程地址空间?
Linux下的进程地址空间
如果不了解C++内存管理, 可以去看一下博主介绍C++内存管理的博客:
[C++] 超详细分析 C++内存分布、管理(new - delete) ~ C 和 C++ 内存管理关系 ~ 内存泄漏 ~_c++ 嵌套delete
- 这块空间表示实际的物理地址空间吗?
- CPU是如何从这块空间中获取数据并处理的呢?
- ……
验证各种类数据存储区域
#include <stdio.h>
#include <stdlib.h>
int global_Var;
int init_global_Var = 1;
int main() {
static int static_Var = 1;
char* stack_data1 = (char*)malloc(100);
char* stack_data2 = (char*)malloc(100);
char* stack_data3 = (char*)malloc(100);
char* stack_data4 = (char*)malloc(100);
printf("main addr:: %p\n", main);
printf("global_Var addr:: %p\n", &global_Var);
printf("init_global_Var addr:: %p\n", &init_global_Var);
printf("static_Var addr:: %p\n", &static_Var);
printf("stack_data1 addr:: %p\n", &stack_data1);
printf("stack_data2 addr:: %p\n", &stack_data2);
printf("stack_data3 addr:: %p\n", &stack_data3);
printf("stack_data4 addr:: %p\n", &stack_data4);
printf("heap_data1 addr:: %p\n", stack_data1);
printf("heap_data2 addr:: %p\n", stack_data2);
printf("heap_data3 addr:: %p\n", stack_data3);
printf("heap_data4 addr:: %p\n", stack_data4);
return 0;
}
-
首先输出 main函数地址:
是输出的所有地址中最小的, 也就是最低的
但也不是整个进程的首地址, 这可以大致说明
进程的代码地址应该是在其他所有数据之前的
-
其次是 未初始化的全局变量、初始化的全局变量 和 初始化的函数内部定义的静态变量:
首先是未初始化、初始化的全局变量:可以看到, 未初始化的全局变量的地址是在已经初始化的全局变量上面的, 也就对应了图中细分的静态区区域:
全局变量相对来讲:未初始化数据在高地址, 初始化数据在低地址
而且可以看到, 经过初始化的在main函数体内部定义的static变量的地址 位于两个全局变量之间, 其实这就说明,
被static修饰的变量 其实实际上就是一个全局变量, 只有在进程结束后才会被释放的
-
定义在栈上的数据:
按照定义的顺序, 最先定义的数据的地址空间最大最高, 之后定义的
按照定义顺序逐渐减小
, 这表明在栈上定义数据 是由高到低占用空间的, 即在栈上定义数据占用空间是向下增长的
-
定义在堆上的数据:
按照定义的顺序,
其占用空间的方向 与栈刚好相反
.在堆区定义数据 是由低到高占用空间的, 即在堆区定义数据占用空间是向上增长的
-
栈 和 堆区数据的地址, 存在非常大的断层:
这也说明 堆和栈之间是存在着非常大的一块地址空间的
如何感知到进程确实存在进程地址空间
#include <stdio.h>
#include <unistd.h>
int global_Var = 100;
int main() {
pid_t id = fork();
if(id == 0) {
int cnt = 5;
while(1) {
if(cnt > 0) {
prinf("我是子进程, global_Var= %d, addr=%p, 还有 %ds 修改global_Var\n", global_Var, &global_Var, cnt);
cnt--;
if(cnt == 0) {
global_Var = 200;
printf("我是子进程, 我已修改global_Var\n");
}
}
else
printf("我是子进程, global_Var= %d, addr=%p\n", global_Var, &global_Var);
sleep(1);
}
}
else {
while(1) {
printf("我是父进程, global_Var= %d, addr=%p\n\n", global_Var, &global_Var);
sleep(2);
}
}
return 0;
}
父子进程的global_Val地址相同, 但是值却不同了
**这两个进程使用的地址, 一定不是内存的物理地址, 即 C/C++中的地址一定不是内存的物理地址.
因为, 如果使用的物理地址根本不可能存在同一个地址却拥有两个不同的值的这种情况C/C++使用的地址空间其实是虚拟地址空间, 而并不是实际的内存物理地址空间
虚拟(进程)地址空间
虚拟地址空间(也叫进程地址空间)
, 这个虚拟地址空间并不是内存的物理空间但是存在一定的映射关系, 且虚拟地址是被task_struct描述的
. 也就是说, 进程的task_struct、虚拟地址与物理地址存在类似此图示一样的关系:task_struct描述着进程的所有属性, 也描述着进程的虚拟地址空间, 而虚拟地址空间与内存实际的物理地址只存在相互映射的关系
struct mm_struct{}
:它描述着进程地址空间中各个区域之间的范围:栈区、堆区、静态区、代码段……各区域的地址范围
, 实际上 struct mm_struct{}
就是用来维护进程地址空间的父子进程代码继承关系
博主有关此问题的博客
代码的物理地址共享
即 父进程地址空间中的代码、数据地址通过页表映射到物理地址中其相对应的指定地址, 而子进程地址空间中的代码、数据地址
通过页表映射到与父进程相同的物理地址
:父子进程都有属于自己的进程地址空间, 但内存中实际只加载了一份代码与数据
0x60104C地址
所存储的值时, 操作系统就会在在物理空间中申请一个新的地址供子进程存储数据使用
, 同时修改子进程页表内容:这种在数据做修改时, 才实际操作物理地址拷贝一份空间的方法叫
写时拷贝
程序是如何加载为进程的
运行这个可执行程序, 然后操作系统生成PCB 与 程序的代码、数据一起加载到内存中, 创建了一个进程地址空间, 此时就说一个进程被创建了.
操作系统是根据什么将程序的数据加载到内存中的呢?
-
程序在没有运行、没有被加载到内存中的时候, 程序内部是否存在地址?
一定是存在的. 程序是源文件被编译器编译链接而生成的, 编译的过程暂且不讲. 而链接, 其实就是将程序内各种数据、函数的地址与库中的地址链接起来, 才能成为可执行程序的. 所以 程序中原本就是存在地址的.
-
程序在没有运行、没有被加载到内存中的时候, 程序内部是否存在类似进程地址空间里设置的区域?
程序内, 其实也是存在区域的. 在Linux系统中, 可以很简单的观察到:
readelf 指令可以用来查看文件的某些信息
排版整理之后可以得到程序本身的一个地址区域表, 就像一个地址空间一样, 包含程序数据分布的各种信息
0000~FFFF(全0~全F)
编址的当程序运行时, 操作系统会根据程序本身的这个地址区域表 将程序的数据加载到内存中.
操作系统根据程序中的数据地址和实际内存地址计算出了相应的虚拟地址
(只是可能, 具体算法要看操作系统), 直到程序中所有数据全部加载到内存中, 操作系统创建进程地址空间, 同时根据虚拟地址和实际地址创建页表:CPU向内存访问数据的时候遇到的地址就是经过计算的虚拟地址
, 如果修改了数据, 进程地址空间就会通过页表找到实际的内存物理地址将内存物理地址中的数据进行修改, 完成一系列的数据访问.程序本身拥有的其数据即代码的相对分布地址, 在程序加载到内存中变为进程的过程中起到了非常至关重要的作用
所以,
虚拟地址空间不仅仅操作空间会考虑, 其实编译器也是需要考虑的, 因为编译器需要创建的是程序自身的程序地址空间
为什么要存在进程地址空间
操作系统在限制进程直接访问物理地址
硬件本身是不会限制软件访问的!硬件的地址只能被动的被读取和写入
进程很有可能访问到其他进程已经占用的地址, 或者访问到硬件本身数据的地址
, 这是非常可怕的!一个不小心硬件就出问题了.进程之间无法互相访问, 互相影响
。 当进程之间不主动互相影响, 也不会互相影响的时候, 程序设计时就不需要考虑更多的东西, 可以使各种程序以一种统一的视角认识内存, 可以方便编译运行加载
。也保证了进程间的安全
就像上面父子进程的例子, 如果子进程修改global_Var时其实修改的是物理地址的数据, 那么势必会影响到父进程的数据, 显然这对两个进程来说是不合理的, 也是很危险的(即操作系统针对每个进程维护的task_struct(PCB) 和 mm_struct(进程地址空间))
. 每个程序的结构都相同, 这对操作系统来说是非常方便管理的.- 保护硬件, 可以为系统及硬件提供更安全的进程服务
- 保护进程, 以及方便编译器编译及操作系统加载
- 方便操作系统管理、维护每个进程
作者: 哈米d1ch 发表日期:2023 年 3 月 6 日