一、前言
Hello,大家好。本文要给大家带来的是有关Linux中的进程地址空间的讲解
- 首先我们来看着一张图,相信有学习过 C/C++内存管理 的同学一定可以清楚下面的这张图。知道内存中划分了很多的区域,包括 栈区、堆区、静态区、只读常量区、代码段、共享区等等。
- 但是呢却不知道为什么要存在这样一个分布?以及为什么要这样来分布?
💬 在本文中我将会带大家去理解一下这个进程地址空间
二、细说进程地址空间
1、一段测试的代码
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <assert.h> 4 5 int g_val = 100; // 全局变量 6 7 int main(void) 8 { 9 pid_t id = fork(); 10 assert(id >= 0); 11 while(1) 12 { 13 if(id == 0) 14 { 15 // child 16 printf("我是子进程,我的id是:%d, 我的父进程是:%d, g_val = %d, &g_val = %p\n", getpid(), getppid(), g_val, &g_val); 17 sleep(1); 18 } 19 else 20 { 21 // father 22 printf("我是父进程,我的id是:%d, 我的父进程是:%d, g_val = %d, &g_val = %p\n", getpid(), getppid(), g_val, &g_val); 23 sleep(2); 24 } 25 } 26 return 0; 27 }
- 然后我们来看执行的结果,可以发现因为
fork()
的原因,在返回结果的时候进行了分流,继而父子进程所在的分支就都被执行了,因为父子进程所访问的都是同一个全局变量,所以我们所看到打印出来的结果的值都是一样的,而且地址也都是一样的
那现在我对上面的代码做一个小小的修改~
- 在子进程内部我们去修改一下这个
g_val
的值
if(id == 0) { // child printf("我是子进程,我的id是:%d, 我的父进程是:%d, g_val = %d, &g_val = %p\n", getpid(), getppid(), g_val, &g_val); sleep(1); g_val++; }
- 然后再去执行一下可以发现 子进程 每次打印出来的
g_val
一直在递增,但是呢 父进程 所打印出来的g_val
却始终没有发生变化,而是保持【100】不变 - 但是呢很奇怪的一个现象是,父子进程所访问这个全局变量的地址却是同一个,这是为什么?相信有很多同学对此产生了很大的疑惑
所以由上面的这个现象可以联想到我们在讲解 进程的基本概念 的所提到的相关概念
- 也就是对于进程而言是具备独立性的,所以子进程对于全局数据的修改,不会影响父进程。这也联想到了一个知识点叫做【写时拷贝】,子进程若是需要修改数据的时候,会将父进程的数据拷贝一份进行修改,而不是直接去进行修改,导致数据出现了问题
那我们便可以发出这样的疑问了:同样去读取一个地址,竟然读到了不同的值,它是一个
普通的地址
吗?
- 很明显这不是一个物理地址,还记得我们在讲解 C语言指针 的时候所提到的【地址】吗?那是我么说到指针其实就是地址,现在我们又要深入地去谈一谈了,对于我们之前一直所聊到的
地址
,其实并不指的是 物理地址,却是一个 虚拟地址 - 也就是说我所打印出来看到的,其实并不是一个内存中真实的地址,只是我们看到的是这个地址罢了,在内存中所对应的可能又是另一个地址
好,有了虚拟地址的概念,我们来小结一下:
💬 父子进程在访问同一变量的时候,这个变量的地址绝对不是【物理地址】,因为它们读到了不同的值,我们在语言层面所用的地址,叫 虚拟地址 / 线性地址
2、引入地址空间
① 富豪与他的私生子👨
接下去我会先通过一个故事来引入一下虚拟地址空间
:book:在十九世纪美国纽约呢,有一个大富翁,坐拥千万美金,他呢有4个私生子,留下了一笔遗产给到他们💴
- 其中【A】是 做生意 的,自己本身就是一个商人,也不是很缺钱。
- 其中【B】是 卖化妆品 的,也靠着这个赚了很多钱
- 其中【C】正在美国的一所重点大学 -- 哈佛大学 读书,靠着努力学习获得了很多的奖学金
- 其中【D】是最小的,在高二的那一年就 辍学 了,现在在混社会
A B C D 呢彼此并不知道彼此的存在那此时大富翁分别对这几个孩子说:
- 小A啊,你做生意呢就好好做,到时候等我临走的时候就把这10亿美金一并给你
- 小B啊,你这个化妆品行业最近挺火的,好好干争取上市,到时候再给你一笔钱就当是投资了
- 小C啊,书要好好读,一定要出人头地,到时候拿着我给你的这一笔钱自己开家公司当老板
- 小D啊,你要混的话就好好混,争取有一年可以混成想黑帮教父那样,再给你一笔钱就更好过了
于是富翁就给这四个孩子分别都画了张大饼⚪,虽然不知是否会兑现承诺,但是先给每个人都说到这个
那对于我们上面所讲的故事我们可以对应计算机去做一个抽象💻
- 这个大富翁呢则是对应我们所熟知的【操作系统】,拥有最大的掌管权;
- 这些孩子们呢就是操作系统中的各个【进程】,由OS来进行管理;
- 对于富豪给各个孩子所画的饼我们称之为【进程地址空间】,每个饼都对应一个具体的区域;
- 对于这10亿美金来说呢我们称之为【内存】,用于分配给各个进程来使用;
那富豪既然给孩子们画了这些饼的话,按需不需要将这些饼给统一收好然后一一分发给各个孩子呢?即操作系统是否需要将各个进程地址空间给组织管理起来?
答案是:当然要。因为管理的本质就是 —— ==先描述,再组织==
- 所以画的这一个个的饼即【进程地址空间】,其实就是一个个的结构体对象,我们将其称作为是
mm_struct
② 38线竟是这么来的!
首先我们来看到这个进程地址空间,刚才我们讲到画的这一个个大饼就对应着每个进程的【进程地址空间】
- 我们看到在这个进程地址空间中有着很多的区域:栈、堆、共享区等等,这些我们在上面就有讲说到过,在这里面的【正文代码段】中呢,可能就有我们在前面所讲到过的虚拟地址,它呢可能并不是内存中的一个实际地址,而是需要通过
一定的手段
才能找到内存中的那一个物理地址,继而找到正确的内容 - 那不管是画的饼还是这一个个的分块的区域结构,都是抽象的,我们若是想看到一些实在的东西,就还需要将其转换为 物理层面的内容。就像富豪要给私生子们拿这笔钱就需要去银行里实际地取出来才行(那时没有网银)
不过对于mm_struct这个地址空间中的【代码区】、【数据区】、【堆区】、【栈区】到底该如何理解?
📕这里我还是通过一个故事来进行引入
- 小花和小胖两个人呢在一所学校读中学,有一天小胖惹小花生气了,不小心打翻了她的水杯,于是呢小花就在桌子的中间画了一条 “38线”:表示小胖不可以越过这条线,一旦越过的话就算是违规了
那我现在想问:小花画这条38线的本质是什么?
- 相信反应快的同学一下子就能说个大概:没错,就是了【区域划分】。那如果使用计算机的术语去表述的话该如何去表述呢?
那就是使用我们在C语言中所学习到的struct
结构体,内部呢有【start】和【end】这两个成员 ⇒ 那么对线性区域进行 指定start和end的划分即可完成区域的划分!
struct area { int start; int end; }
- 对这两块区域去做一个初始化的话就可以像下面这样
struct area xiaohua_area = {1, 50}; struct area xiaopang_area = {50 100};
但是呢在某一天呢,小胖又惹小花生气了,于是呢小花把这条线从55对半划到了给小胖只剩 3 的区域,那也就变成了真正的 “38线”
- 那对应到代码层面我们就可以去做这样一个修整。将小花的末节区域
end
修改为【80】,并且将小胖的初始区域start
设置为【80】
xiaohua_area.end = 80 xiaopang_area.start = 80
③ 地址空间的深层理解
清楚了什么叫做区域划分之后,我们再通过进一步的理解加深对地址空间认识
- 但是在上面讲了这么半天【线性划分】和【线性区域】,这我们本节所要讲的 地址空间 有什么关系呢?
那在这里我可以先给出结论:
地址空间本质就是一个线性区域
首先呢我们可以先来理解一下什么叫做【线性空间】👈
- 我在 C语言指针章节 里也有说到过在32位系统中有32根总线,可以有2^32^个排列组合,即有2^32^个地址,从最低的地址
0x00000000
到最高的地址0xFFFFFFFF
,这里的每一个地址都是连续的,换算成十进制就是 0 ~ 42亿多。所以我们把这个地址空间称之为【线性空间】 - 因为这些数字是线性的,所以地址空间整体是线性的。每一个数字表示一个地址,一个地址表示一个字节
知道了什么是线性空间后,我们还要去进一步理解
类型
的相关概念
- 在 C语言数据类型章节 以及上面的指针章节我有将其过对于一个数据类型于我们而言可以去区分各种不同的数据;于计算机而言呢例如对于 指针来说决定了它走一步可以跨过多大的范围
💬 所以我们要明白类型存在的意义
- 在计算机里可以帮我们确认当前变量申请的起始地址,然后会再根据类型来看它到底能取几个字节
在一开始我们就有提到过有关进程地址空间中的各种区域,那现在在熟知了【线性空间】这个概念之后呢,知道了一个区域有
起
和终
,那此时我们如何使用代码去维护这一段段的空间呢?
- 下面是我在Linux的源代码中节选出来的:
- 例如【代码段】,它的区域就是使用
code_start
和code_end
来进行维护的 - 例如【栈区】,它的区域就是使用
stack_start
和stack_end
来进行维护的
struct mm_struct { long code_start; long code_end; long init_start; long init_end; ... long brk_start; long brk_end; long stack_start; long stack_end; }
那既然可以使用结构体来表示这些区域的话,那我想请问:那么区域之间的数据叫做什么呢?
- 例如
[1000, 2000]
之间的这段数据,1200
、1500
or1700
这些,上面说到过,对于一个区域内的数据都是连续的,而且每一个数字代表一个地址,因此我们可以称它们为【虚拟地址 / 线性地址】
:book: 最后我们来总结一下:
- 地址空间就是一段线性范围,从全0到全F,换算成十进制就是 0 ~ 42亿多。每一个数组不叫做整数,而叫做地址。因为数字是线性的,每个数字表示一个地址,每个地址对应1字节,因为 CPU寻址的最小单位就是字节,如果需要多个地址的话就连续申请多个字节,但一般是把 首地址 返回,在应用层再根据这个首地址去确定其在内存当中的位置,然后再加上类型,确定所申请的内存空间有多大(类型的本质就叫做偏移量)
- 那上面说了这么多,其实本质我们还是想要讲一点:==地址空间本身就是线性结构==