🍎作者:阿润菜菜
📖专栏:Linux系统编程
我们在学习C语言的时候,都学过内存区域的划分如栈、堆、代码区、数据区这些。但我们其实并不真正理解内存 — 我们之前一直说的内存是物理上的内存吗?
前言
我们先看一段测试代码:
#include <stdio.h>
#include <assert.h>
#include <unistd.h>
int g_value = 100; //全局变量
int main()
{
// fork在返回的时候,父子都有了,return两次,id是不是pid_t类型定义的变量呢?返回的本质,就是写入!
// 谁先返回,谁就让OS发生写时拷贝
pid_t id = fork();
assert(id >= 0);
if(id == 0)
{
//child
while(1)
{
printf("我是子进程, 我的id是: %d, 我的父进程是: %d, g_value: %d, &g_value : %p\n",\
getpid(), getppid(), g_value, &g_value);
sleep(1);
g_value=200; // 只有子进程会进行修改
}
}
else
{
//father
while(1)
{
printf("我是父进程, 我的id是: %d, 我的父进程是: %d, g_value: %d, &g_value : %p\n",\
getpid(), getppid(), g_value, &g_value);
sleep(1);
}
}
}
运行结果:
我们可以注意到子进程的变量值内容发生了改变,而父进程的变量值内容一直没有发生改变,并且两个进程的全局变量打印出来的地址值是一样的。
那这地址到底是不是真的物理地址?
== 当然不能是,如果是同一个物理地址,不可能读取同一个变量会读取到不同的数值 。==
从上面结果我们可以得出: 子进程对全局变量数据修改,不影响父进程 - — 进程具有独立性 也就是说,我们在语言层面用的地址,不是物理地址。
所以我们之前说‘程序的地址空间’是不准确的,那准确的说这是什么地址呢?
我们一般叫:虚拟地址或者线性地址
我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。OS必须负责将 虚拟地址 转化成 物理地址 。
理解进程地址空间
通过故事引入
我们知道操作系统会帮助我们用于管理计算机硬件和软件资源.假设操作系统是个大富翁,手底下有10个亿的内存,大富翁同时有四个私生子,四个分别从事不同的活动,彼此不知道互相的存在(对应进程的独立性),在大富翁老去时,会将自己的财产继承给私生子,那么由于每个私生子彼此不知道存在,大富翁让每个私生子都会以为自己可以继承10个亿的财产(画的大饼),是孩子就总会有给老爹要钱的时候,那么为了管理自己的10个亿财产,大富翁很有必要将其描述组织起来,以供自己四个彼此独立的私生子使用。
那么大富翁(操作系统)将画的大饼先描述,在组织,其实就是管理进程地址空间的过程 ---- 本质就是:一个内核数据结构,struct mm_struct{ }
现在我们知道了地址空间就是内核数据结构 ---- 那它是怎么对应物理内存的?
先来看一下地址空间是怎么划分的:
代码区、数据区、堆区等这些区域如何理解?
在我们小学的时候可能会遇到课桌上有一道“线”的情况,是什么线?三八线。那当时画三八线的本质就是:区域划分 — 地址空间就是线性区域
同样在Linux内核数据结构中也存在区域划分,类似下面的这种代码,用来管理内存空间:
struct area
{
int start;
int end;
}
同时,我们对线性区域进行指定start和end即可完成区域划分 ---- 类似于这样
struct area owner_1 (1,50};
struct area owner_2 (50,100};
如果限定了区域,那区域之间的数据是什么?以一个4GB的内存为例,大概是这样的:
struct mm_struct //4GB
{
long code_start;
long code_end;
long init_start;
long init_end;
//.....
long stack_start;
long stack_end;
}
通过上述,我们可以知道地址空间区域是可以进行动态调整大小的 ---- 即更改 start或者end
同时虚拟地址是经过页表(+MMU(集成在cpu中))映射到物理地址 ---- 像是我们大学生会被学号编号,进行确认
同时根据上述知识可以知道,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址
找到地址不是目的,而是一种手段(页表),目的是该地址对应的内容!
到这里还能回答原始问题:子进程的mm_struct继承父进程 ---- 即虚拟地址一样 进程独立,映射到不同的物理地址
深入扩展
地址空间为什么要存在?
---- 比如野指针越界问题,破坏了进程独立性,进程的数据会遭到破坏,影响到进程运行
- 防止地址随意访问,保护物理内存与其他进程运行
- 同时页表具有读写权限控制属性,解释了为什么代码段只是可读的,为什么有些变量不能赋值
- 将进程管理和内存管理进行解耦合 | 通过malloc本质讲解
- 可以让进程以统一视角看待自己的代码和数据
malloc 本质
作为一款优秀的操作系统,不能允许任何的浪费或者不高效的存在。
所以操作系统使用缺页中断方式来管理内存 ---- 即先在虚拟地址空间申请虚拟内存。
然后通过页表映射 ,在实际物理内存上开辟空间 同时不需要关心数据放在物理内存哪个位置 因为通过页表映射都能找到
重新理解地址空间
-
我们的程序在被编译的时候,没有被加载到内存,那么我们的程序内部有没有地址呢?
答案是:有的。源代码被编译的时候,就已经按照虚拟地址的方式进行了代码和数据的编址(使用ELF格式:划分数据区域)
所以虚拟地址这样的策略不只是影响OS,我们的编译器同样遵守这样的规则! -
进程的代码和数据必须一直在内存中吗?
答:不是。OS会将暂时不用的进程代码、数据和部分进程控制块通过我们的页表技术交换至磁盘中存储。