乌海品牌网站建设,做跨国婚恋网站赚钱吗,大连软件公司排名,卖东西的网站有哪些#x1f923; 爆笑教程 #x1f449; 《看表情包学Linux》#x1f448; 猛戳订阅 #x1f525; #x1f4ad; 写在前面#xff1a;本章核心主题为 进程地址空间#xff0c;会通过验证 Linux 进程的地址空间来开头#xff0c;抛出 同一个值能有不同内… 爆笑教程 《看表情包学Linux》 猛戳订阅 写在前面本章核心主题为 进程地址空间会通过验证 Linux 进程的地址空间来开头抛出 同一个值能有不同内容 的现象通过该现象去推导出 虚拟地址 的概念。然后带着大家理解为什么虚拟地址不能是物理内存、讲解进程地址空间的概念以及如何设计。讲解什么是区域对区域的理解再引出内核中的数据结构是如何维护的如何加载的问题。最后我们会揭秘文章开头的验证抛出的问题从而引出 写时拷贝 的概念。讲解完写时拷贝后我们就能理解为什么 同一个值能有不同内容的现象并且也能解释本专栏进程开篇时抛出的 fork为什么会有两个返回值 的问题了。文章的最后我们再探讨一下虚拟地址空间存在的意义会印证 进程本身是有独立性的 概念。 本篇博客全站热榜排名14 0x00 引入地址空间是内存吗非也 程序地址空间是内存吗不是程序地址空间不是内存
其实我们称之为程序地址空间都不准确应该叫 进程地址空间这是一个系统级的概念 0x01 验证Linux 进程地址空间
我们来写个代码验证一下 Linux 进程地址空间 代码Linux 进程地址空间
#include stdio.h
#include unistd.h
#include stdlib.hint un_g_val;
int g_val 100;int main(int argc, char* argv[], char* env[])
{printf(code addr : %p\n, main);printf(init global addr : %p\n, g_val);printf(uninit global addr : %p\n, un_g_val);char* m1 (char*)malloc(100);printf(heap addr : %p\n, m1);printf(stack addr : %p\n, m1);int i 0;for (i 0; i argc; i) {printf(argv addr : %p\n, argv[i]); }for (i 0; env[i]; i) {printf(env addr : %p\n, env[i]);}
}运行结果如下 我们发现整体的地址是依次增大的。 请注意堆和栈之间能观察到有非常大的地址镂空。
下面我们来验证一下堆和栈的 挤压式 增长方向的问题在刚才的代码中我们加上如下代码
/* 堆上申请四块空间 */
char* m1 (char*)malloc(100);
char* m2 (char*)malloc(100);
char* m3 (char*)malloc(100);
char* m4 (char*)malloc(100);printf(heap addr : %p\n, m1);
printf(heap addr : %p\n, m2);
printf(heap addr : %p\n, m3);
printf(heap addr : %p\n, m4);
现在我们再验证一下栈区 依次入栈我们取地址将其分别打印出来
printf(stack addr : %p\n, m1);
printf(stack addr : %p\n, m2);
printf(stack addr : %p\n, m3);
printf(stack addr : %p\n, m4); 我们发现堆区向地址增大方向增长栈区向地址减少方向增长。
堆和栈相对而生
我们一般在 C 函数中定义的变量通常在栈上保存那么先定义的一定是地址比较高的
后定义的地址一定是比较低的。因为先定义的先入栈后定义的后入栈。
我们再来理解一下 static 变量如何理解 static 变量 我们知道一个变量在函数内被定义如果声明其为 static那么它的作用域不变但它的生命周期会随着程序存在一直存在。 凭什么在函数内定义 static 变量该变量就能寿与天齐了 我们可以加入一个 static 变量进刚才的代码中我们来观察观察
static int s 100; 我们的 s 是被初始化的所以就被当成了全局变量它只是一个写在函数内的全局变量。 这也就是为什么它能够寿与天齐因为它本来就是全局变量。 结论函数内定义的变量用 static 修饰本质是编译器会把该变量编译进全局数据区。 0x02 感知地址空间的存在 我们还是写代码去观察分析
#include stdio.h
#include unistd.h
#include stdlib.hint g_val 100;
int main(void)
{pid_t id fork();if (id 0) {// childwhile (1) {printf(我是子进程: %d, ppid: %d, g_val: %d, g_val: %p\n\n, getpid(), getppid(), g_val, g_val);sleep(1);}}else {// fatherwhile (1) {printf(我是父进程: %d, ppid: %d, g_val: %d, g_val: %p\n\n, getpid(), getppid(), g_val, g_val);sleep(2);}}
} 运行结果如下 结论当父子进程没有人修改全局数据的时候父子是共享该数据的。 如果此时尝试写入比如我们让子进程有一个修改的操作。
我们在子进程那定义一个 flag sleep(1) 执行五次即五秒之后给它改值
#include stdio.h
#include unistd.h
#include stdlib.hint g_val 100;
int main(void)
{pid_t id fork();if (id 0) {// childint flag 0;while (1) {printf(我是子进程: %d, ppid: %d, g_val: %d, g_val: %p\n\n, getpid(), getppid(), g_val, g_val);sleep(1);flag;// 五秒之后开始更改if (flag 5) {g_val 200;printf(我是子进程全局数据我已做修改注意查看\n);}}}else {// fatherwhile (1) {printf(我是父进程: %d, ppid: %d, g_val: %d, g_val: %p\n\n, getpid(), getppid(), g_val, g_val);sleep(2);}}
} 运行结果如下 发现父子进程读取同一个变量因为地址一样但是后续没有人修改的情况下父子进程读取到的内容却不一样。 父子进程打出来的地址是一样的值却不一样 妈妈生的即答
既然如此那我就告诉你真相 —— 我们在 C/C 中使用的地址绝对不是物理地址 (梅开二度) 震惊居然不是物理地址…… 听到这就像是《三体》中所说的 物理学从来就没有存在过 一样。 如果是物理地址上面出现的那种现象是不可能产生的
不是物理地址那是什么呢本章我们还不能证明需要后续章节的铺垫才能够讲解。 我们先抛出概念我们在 C/C 中使用的地址是 虚拟地址。
虚拟地址在我们 Linux 下也称为 线性地址有些教材中也称之为 逻辑地址。这三个概念实际上是不一样的但是在 Linux 下它是一样的这和其本身的空间布局有关系。 我们再抛出一个问题为什么我的操作系统不让我直接看到物理内存呢
如果能让你直接看到物理内存或者让你访问物理内存岂不是会出乱子。
内存就是一个硬件不能阻拦你访问只能被动地进行读取和写入 0x03 讲解进程地址空间
每一个进程在启动的时侯都会让操作系统给它创建一个地址空间该地址空间就是 进程地址空间
操作系统为了管理一个进程给该进程维护一个 task_struct 叫做进程控制块。 首先每一个进程都会有一个自己的进程地址空间。
操作系统要不要管理这些进程地址空间呢当然是要管理了我们还是引出前几章提出的 先描述再组织。 所谓的进程地址空间其实是内核的一个数据结构叫做 mm_struct 。 下面我们就来讲解究竟什么是地址空间
在上一章我们谈论过进程的概念竞争和独立、并行和并发我们要需要谈论其中的 独立性。
进程具备独立性简单来说就是一个进程挂掉或崩溃是不会波及其他进程的。
进程相关的数据结构是独立的进程的代码和数据是独立的。说得好但是独立性又和地址空间有什么关系呢我们来讲个故事。 小故事环节 《重生之我是财阀老板私生子》 韩国某个财阀老板非常滴有钱他有 3 个私生子每个私生子都并不知道对方的存在他们都以为自己是独生子。因为他们彼此不知道对方的存在所以他们在生活和工作上也没有交集不会有任何互相的影响这就是独立性的体现。财阀老板为了维护自己的独立性 他就对大儿子说儿子你好好学习以后老爹钱都是你的。大儿子一听卧槽真好高枕无忧就好好学习一想到自己以后有钱就更想学习了。 然后又对二儿子说儿子好好工作等以后我就把公司给你。二儿子一听热泪盈眶于是就好好工作等着将来有一天可以继承公司。 后来又对三儿子说儿子你好好干活等你长大老爹的家产交给你三儿子知道自己以后会继承老爹的所有财产开心坏了就努力的干活。 只要在财阀爹的可承受范围内孩子要多少钱他都给多少钱所以三个儿子自然都认为自己有很多钱。财阀老板给他的三个儿子画了一张虚拟的、不存在的大饼让他们都能努力学习工作干活这个步骤就是给他们分别建立了进程地址空间。 上面的故事中财阀老板就是操作系统三个私生子就是进程
财阀老板给他的三个儿子画的大饼我们就称之为 进程地址空间。
所以进程地址空间并不是物理上存在的概念而是在逻辑上抽象的一个虚拟的空间。
财阀老板给三个私生子画饼就是为了维护这三个私生子互相之间的独立性
如果让私生子知道自己并不是唯一那以后分割财产必然会造成矛盾
对他来说自然就不是一件好事。
所以进程地址空间就是就是给进程画的大饼。
进程地址空间 → 逻辑上抽象的概念 → 让每个进程都认为自己独占系统的所有资源 概念操作系统通过软件的方式给进程提供一个软件视角认为自己是独占系统的所有资源内存。 0x04 理解区域和页表
什么叫做区域我们来拿一张桌子来理解初中的时候我和我的同桌分过 38线 。 我们把一张桌子分为两个区域对桌子进行区域划分 比如既然要标出区域定义一个桌面区域其实用两个变量就可以表示了
struct destop_area {int start; // 区域起始位置int end; // 区域结束位置
};struct destop_area A {1,50};
struct destop_area B {50, 100}; 抢地盘对桌面区域进行划分调整区域的大小只需要让 end 加上 调整值 就行。
这就是区域的概念我们只需要定义 start 和 end 就可以表示了。
每个区域范围都是可以有对应的编号的比如以厘米为单位我的修正带就放在了 50cm。
我们的 mm_struct 里面不就是区域范围吗所以 mm_struct 就可以靠 start 和 end 定义
struct mm_struct {long code_start;long code_end;long init_start;long init_end;long uninit_start;long uninit_end;long heap_start;long heap_end;long stack_start;long stack_end;...
}
程序加载到内存由程序变成进程后由操作系统给每个进程构建的一个页表结构就是 页表。
我们来看看内核代码就是用一个 start 一个 end 来呈现区域空间。 每个区域都有一个 start 和 end它们之间就有了地址地址我们称之为虚拟地址 然后这些虚拟地址经过页表就能映射到内存中了。 0x05 揭秘原来是写时拷贝
❓ 思考程序是如何变成进程的
程序被编译出来没有被加载的时候程序内部有地址吗有 有没有区域也有 区分我们程序内部的地址和内存的地址是没有关系的。
编译程序的时候我们就认为程序是按照 ~ 进行编址的。
虚拟地址空间不仅仅是操作系统会考虑编译器也会考虑。
每个进程都会创建一个 task_struct每一个进程都会维护一个 mm_struct自己有对应的区域当我们的程序加载到内存时程序有自己的加载到物理内存的物理地址虚拟地址和物理地址建立映射关系进程访问某个区域当中的地址时经过页表找到对应的代码和数据。当找到代码和数据后代码加入到对应的 CPU 中代码中的地址在加载中就已经转化成了线性地址/虚拟地址所以 CPU 可以继续照着这个逻辑向后运行。
所以刚才我们代码测试打印看到的虚拟地址值是一样的并且内容也是一样的。在没有人写入的时候虚拟地址到物理地址之间映射的页表是一样的所以指向的代码和数据都是一样的。 因为进程具有独立性比如如果此时子进程把变量改了写入就会导致父进程识别的问题就出现了父进程和子进程不一的情况因为进程是具有独立性的所以我们就要做到互不影响。我们的子进程要进行修改了影响到父进程怎么办没关系操作系统会出手当我们识别到子进程要修改时操作系统会重新给子进程开辟一段空间并且把 100 拷贝下来重新给进程建立映射关系所以子进程的页表就不再指向父进程所对应的 100 了而直接指向新的 100。你在做修改时又把它的值从 100 改成 200 时我们就出现了 改的时候永远改的是页表的右侧左侧不变 的情况所以最后你看到了父子进程的虚拟地址一样但是经过页表映射到了不同的物理内存所以了你看到了一个是 100 一个是 200父子进程的数据不同的结果。
我们的操作系统当我们的父子对数据进行修改时操作系统会给修改的一方重新开辟一块空间并且把原始数据拷贝到新空间当中这种行为就是 写时拷贝
当父子有任何一个进程尝试修改对应变量时有一个人想修改就会触发写时拷贝让他去拷贝新的物理内存这只需要重新构建也表的映射关系虚拟地址是不发生任何变化的所以最终你看的结果是虚拟地址不变而内容不同。 现在再看一点都不神奇了。
通过页表将父子进程的数据就可以通过写时拷贝的方式进行了分离。
这就做到父子进程具有独立性父子进程不互相影响。 0x06 回顾fork 有两个返回值的问题
我们在讲解进程的第一个章节就提出过一个问题关于 fork 为什么有两个返回值的问题。
当时我们还提出了两个问题局限于当时还没有讲到进程地址空间所以没有办法深入讲解。 我们当时说过要在 进程地址空间 讲完后再讲现在就可以讲了
我们先回顾一下上下文 代码验证 fork 返回值的问题我们把 id 给打印出来
#include stdio.h
#include unistd.h
#include sys/types.hint main(void) {pid_t id fork();printf(Hello, World! id: %d\n, id);sleep(1);
} fork 有两个返回值pid_t id同一个变量为什么会有两个返回值
本章我们就可以理解了因为当它 return 的时候pid_t id 是属于父进程的栈空间中定义的。
fork 内部 return 会被执行两次return 的本质就是通过寄存器将返回值写入到接收返回值的变量中。当我们的 id fork() 时谁先返回谁就要发生 写时拷贝。所以同一个变量会有不同的返回值本质是因为大家的虚拟地址是一样的但大家的物理地址是不一样的。 0x07 探讨为什么要有虚拟地址空间
如果我们没有虚拟地址空间直接让进程访问物理内存是不安全的。
有了虚拟地址空间就是给访问内存添加了一层软硬关键层可以对转化过程进行审核非法的访问就可以被直接拦截了可以 保护内存。
还能够将 进程管理 和 Linux 内存管理通过地址空间进行功能模块的解耦。
让进程或者程序可以以一种统一的视角看待内存
有了虚拟地址空间还可以让进程或者程序可以 以统一的视角看待内存。方便以统一的方式来编译和加载所有的可执行程序。如此一来就可以简化进程本身的设计和实现。 [ 笔者 ] 王亦优[ 更新 ] 2023.2.14
❌ [ 勘误 ] /* 暂无 */[ 声明 ] 由于作者水平有限本文有错误和不准确之处在所难免本人也很想知道这些错误恳望读者批评指正 参考资料 Creference[EB/OL]. []. http://www.cplusplus.com/reference/. Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. . 百度百科[EB/OL]. []. https://baike.baidu.com/. 比特科技. Linux[EB/OL]. 2021[2021.8.31 xiw