赞
踩
前言: 本节内容就将进入linux进程里面的又一个大板块, 博主认为这个板块和PCB的板块是平级——两者独立;之前友友们可能认为进程分为PCB和代码与数据。 但是本节过后, 我们可以对进程重新定义——进程 = (PCB(linux下task_struct) + 虚拟地址空间 + 页表) + 代码与数据。
ps:本节内容和前面PCB小节是各自独立的。但是如果有前面知识的铺垫, 本节会更加好理解。 而本节最低适合已经了解进程概念的友友们进行观看。
目录
首先我们来看一下我们在学习语言的时候, 学习的内存划分结构:
我们在学习语言的时候, 认为我们调试窗口, 看到的地址就是真实内存的地址。 但是这个地址真的是真实内存的地址吗?——答案是不是, 那么为什么不是,就是本节要讨论的问题。
首先, 我们要抛出一个概念——地址空间——要理解这个概念, 我们这里写一个程序:
上面程序运行后, 就是打印下图这一串串的地址:
然后我们观察这些地址:
然后我们仔细观察它们的地址大小顺序, 就会发现, 我们在程序中定义的这些变量——不管是全局变量, 栈区变量、常量、代码段等等, 实际上都是遵循上面的左图的。
那么, 我们就可以信服一件事情——为什么对于堆区变量,全局变量能够一直有效, 就是因为它们在堆区或者全局变量区开辟空间。 而栈区变量在栈区开辟空间。 常量之所以不能够修改是因为常量保存在常量区。
现在我们来看一下栈区和堆区的增长趋势——注意, 有些知识点博主讲到只是为了辅助理解, 和本节内容有关系, 但是不是直接相关。
如何验证栈区和堆区的地址增长趋势,我们只需要写两个下面这种程序:
针对栈区:
针对堆区:
两个程序的运行图分别是:
由上面两张运行图我们就能发现, 栈区是向低地址方向增长;堆区是向高地址方向增长。 并且由图中可以发现一个很奇怪的现象。那就是栈区和堆区的地址相差极大!——这个是运行时堆栈的内容,博主没学过, 但是和本节内容关系不大, 有兴趣的友友自行学习即可。
、我们知道, 对于一个栈区变量来说, 如果加上static就能从栈区变量变成一个全局变量。 我们使用代码来看一下:
如下图:
从上面我们就能发现,static修饰的局部对象, 编译的时候已经编译到了全局数据区。
上面是我们运行实验时的代码, 我们编译运行这串代码后, 就可以看到如下:
会隔一秒打印一次, 现在没有问题, 那么我们再来修改一下我们的代码:
运行后, 就能看到我们一个非常奇怪非常棘手的现象, 也是我们本节主要要解决的问题——进程父子进程变量相同, 结果不同:
看上图的蓝框框, 蓝框框内的内容, 明明变量名相同, 变量的地址也相同, 但是它们的结果却不相同。 ——我们学过fork, 可以解释为子进程原本和父进程用同一块数据, 但是当想要修改父进程数据的时候, 却发生了写实拷贝。 这个时候子进程的g_val和父进程的g_val不是同一份变量。 但是这里又有一个问题, 那就是我们从上图可以看到, 子进程发生写实拷贝后, 子进程的g_val和父进程的g_val的地址是一样的。 既然地址一样, 为什么变量却不是同一份呢?请问这是为什么?怎么可能同一份地址, 读取到了不同的值??
那么, 这里的地址真的是真实的物理地址吗? 如果是真实的物理地址, 就绝对不会出现上面的现象!!!所以, 这里的地址, 就绝对不是真实的物理地址!!
而这里的地址我们一般把它叫做线性地址&&虚拟地址, 而我们平时用的C/C++指针, 指着里面的地址全部不是物理地址, 而是虚拟的。
地址空间其实就是一个虚拟内存, 逻辑结构就如上图那样。 其实里面是一个个整形的数据范围。 而真正的内存是物理内存。 物理内存就是真正的内存空间。 它的物理结构就是如上图那样。
而物理内存和虚拟内存之间的练习是一个页表。 这是一个kv数据结构。 k是虚拟内存,v是物理内存。 整个结构图如下:
对于32位机器, 有32位的地址和数据总线, CPU和内存相连。 ——》我们从c语言中就学过, 每一根总线, 只有0、1概念。 一共有32根, 那么就有2 ^ 32种组合。 一共是4GB空间。
所以, 什么是地址空间? 我们的地址总线排列组合形成的地址个数(例如下图(0, 2^32))就叫做地址空间。
而现在我们从实际工作的角度上理解地址空间——这里有一个小故事
假如这里有两个小朋友——一个小胖, 一个小花。 这两个人是同桌。 下面是他们的桌子:
他们的桌子是100cm。 也就是说, 他们的地址空间是0~100. 但是呢, 小花很讨厌小胖, 所以呢, 小花就在桌子上面画了一根三八线。 结果, 小胖这个人不长记性, 总是越过三八线, 就导致小花总是胖揍小胖一顿。 ——现在, 请问,小花画三八线的本质是什么? ——本质就是区域划分!!!
我们可以使用计算机语言, 那么就是说, 小花和小胖画好三八线之后, 就要管理好自己的地址空间。 而我们知道, 管理的本质就是——先描述, 再组织。 所以据需要一个数据类型来描述这个地址空间, 供给两人进行管理。 而今天, 这个数据类型博主就告诉友友们大体如何定义:
- struct area
- {
- int start;
- int end;
- }
-
- struct dest_ares
- {
- struct area xiaopang;
- struct area xiaohua;
- }
也可以:
- struct dest_area
- {
- int start_xiaopang;
- int end_xiaopang;
- int start_xiaopang;
- int end_xiaohua;
- }
那么, 现在小花和小胖可以管理自己的空间了。 但是呢, 有一天, 小胖又越界了, 小花呢,这一天心情不太好。 所以小花看到小胖越界就很愤怒, 她不仅将小胖打了一顿, 而且将三八线向小胖那边挪动了一下:
而对应他们的结构体, 是不是就是小胖管理的end_xiaopang -= 10, 小花管理的start_xiaohua += 10;
所以, 所谓的区域的调整变大或者变小, 本质上就是对这些struct结构体管理的end 还有start变大或者变小。
小胖是一个有强迫症的人, 这个桌子对于小胖来说,假如这个桌子的范围是0 ~ 40。又因为是他自己管理, 所以他就将它的铅笔放到了3厘米处, 将他的本子放到了10厘米处。 那么等到别人要借用他的本子或者笔的时候, 他就可以直接使用坐标3, 或者10.告诉别人本子或者笔在哪。 那么, 这个过程中, 我们不仅仅要看到给小胖划分了多少空间, 而且要看到在范围内连续的空间中, 每一个单位都有一个地址, 这个地址可以被小胖随意的使用!!!——即可以访问地址空间内的任意地址处。
所以, 什么是地址空间呢? 所谓的地址空间, 本质上就是一个描述进程可视范围的大小!我们地址空间内一定要存在各种区域划分, 对线性地址进行start和end即可。
由上面我们已经知道, 操作系统在创建进程的时候, 不仅要创建进程的PCB结构体, 而且要创建进程的地址空间。 而这个地址空间, 本质上就是一个数据结构对象。 类似于PCB一样, 地址空间也要被操作系统管理——先描述, 再组织。
而如何描述呢? 其实和小胖小花管理自己桌子是一样的, 就是使用一个有着start, end的结构体, 如下图:
理解地址空间这点是不够的, 但是我们先理解一下页表。 再回来更深一步理解地址空间。
、我们知道, 我们的程序, 只要创建了结构体, 并且开辟了mm_struct——这个mm_struct里面是一个个int类型的范围, 对应着物理内存的内存区域。——那么这个cpu中此时该进程一定处于被调度的状态。 而且我们知道, 为了让我们的进程的地址实现虚拟地址到物理地址的转化, 进程同时也维护了一个页表结构。
而我们如何找到这个页表? ——原因就是在cpu内部维护了一个寄存器, 这个寄存器叫做cr3寄存器。如下图:
这个寄存器会保存当前进程页表的起始地址。 ——既然这个地址不存在于内存中, 那么进程的切换, 下次cpu还会记得这个页表吗? 答案是会的, 为什么? 因为这个cpu中页表的起始地址对于进程来说, 本质上他也是这个进程的硬件上下文。 我们知道, 对于一个进程的上下文来说, 当进程切换的时候, 它会被进程带走!当下次再次运行, 这些上下文数据又会被放到cpu中进行执行。 所以,我们进程从始至终都会在自己进行执行的时候, 将自己需要的数据放到cpu中, 不会影响自己的执行。
假入页表中有一组kv。 如下图:
那么我们就会发现, 最后一个格子是干什么的呢? 那么我们思考一下, 对于一个变量来说, 我们不同的身份会有不同的权限, 那么我们怎么知道, 我们对于某一个变量是可读还是可写的? 答案就在这个页表之中。
上面的0x1234处的地址如果是变量区, 那么这个变量的权限就是可读可写。 但是如果此时又来一串代码。这串代码在虚拟内存的0x123处。 他映射到了物理内存的0x112233处, 并且我们知道, 代码是可读但是不可写的。 那么对于这串代码来说, 他的操作权限就是r。
此时的页表就是如下:
当我们要访问这个代码, 并且对于这个代码进行修改的时候, 页表就会发现代码只有读, 没有写。 那么它就会拦截这个进程的操作。 并且操作系统会直接干掉这个进程。 ——这, 就是页表的权限的管理。
现在,我们就可以解释下面这个现象了:
看下面这串代码:
这串代码运行后显示之后会直接挂掉:
在没有学习这一节之前, 我们对于这个的理解可能就是——因为代码段只读, 修改代码程序直接挂掉。
但是那是语言层面的理解, 我们只要深究就会发现, 既然代码只读, 那么它是如何加载到内存中的呢? 现在我们利用新的知识, 对于这个东西就有了新的立即——代码段本身并没有什么可读可写的概念, 只是对于物理内存来说, 只要是数据, 它就会来者不拒, 将这些数据放到相应的内存区。 而之所以数据会有所谓的只读, 只写或者可读可写。 本质上是因为页表。 页表会根据数据的类型规定相应的权限, 只要越权,那么就会拦截, 操作系统进而会干掉进程。
这里思考一个问题, 就是我们平时玩一个很大型的游戏, 这个游戏可能一百G, 那么这么大的游戏, 显然不可能全部加载到内存。 这就说明了什么呢? ——说明操作系统对于大文件, 可以实现分批加载。
在我们的操作系统中, 我们的操作系统当中可以实现分批加载。比如说我们加载了500M的空间, 但是代码是一行一行跑的, 短期内代码跑不完。而加载的这部分数据是不会被释放的。 这就造成了空间上的浪费。 所以实际上, 操作系统对于内存的加载是采用惰性加载的方式。 ——也就是说虽然承诺加载了500M, 但是实际上可能只分配了几十kb。
当我们查地址的时候, 就要先看一下标志位, 如果标志位显示加载到内存中。 那么就是可以找到物理地址, 并且进行访问。 但是如果没有显示加载到内存, 那么就是缺页中断。
如果触发缺页中断, 就说明我们需要访问该处内存, 却没有数据加载到物理内存, 这个时候操作系统就会重新加载物理内存, 将数据重新加载到物理内存中, 然后将这个内存映射到页表之中。 这个时候进程就可以访问物理内存中的数据。
那么, 这里我们就可以重新理解下写时拷贝, 写时拷贝其实也是一个缺页中断的过程。
那么, 如果创建进程的时候, 只创建PCB, 进程地址空间, 页表, 但是不加载物理空间, 可以吗?
进程创建的时候, 实现创建内核数据结构呢? 还是先加载对应的可以执行程序呢?
现在我们来思考一个问题, 对于进程来说, 当进程加载到内存或者进程切换的时候, 就要将数据加载到物理内存, 并且物理内存还要映射到当前页表当中。 那么请问这个过程中, 进程需要管理吗? 答案是不需要, 对于进程来说, 进程不需要关心物理内存如何缓存数据, 页表如何被映射物理内存, 事实上, 她也不会去关心。 进程只需要管理好自己的PCB, 管理好自己的进程地址空间和页表的映射即可, 那么这个就形成了两个板块。
上面这两个板块, 一个是内存管理, 一个是进程管理。 对于进程来说, 进程只需要管理自己的地址空间, 地址空间去访问页表。 如果页表中没有映射value, 那么页表发生缺页中断, 就会自动区物理内存中缓存数据并且形成映射。 那么如何申请等等, 就是内存管理事情。 进程不需要关心。 所以虚拟地址空间的存在, 他把进程管理和内存管理实现了软件层面的解耦。
学完页表, 我们就可以知道了一个问题的部分答案——为什么要有虚拟内存?因为有地址空间和页表的存在,将进程管理模块和内存管理模块进行解耦合。
现在我们通过一个故事, 来重新理解一下进程地址空间:
就是说假如有一个大富翁, 这个大富翁有三个私生子, 这个大富翁有10亿家产。如下图:
这些私生子, 很显然并不知道大富翁有其他的孩子存在, 以为大富翁只有他们自己一个孩子。
有一天, 大富翁找到私生子1, 说以后自己的10亿家产是他的。
又有一天大富翁找到私生子2, 说以后自己的10亿家产是他的。
同样的一天, 大富翁找到私生子3, 说以后自己的10亿家产是他的。
现在, 这三个私生子都坚信着, 大富翁去世后,他的家产就是自己的了。 一天,私生子1找大富翁借1亿, 大富翁骂了他一顿, 没有借。 还有那么一天,私生子2, 或者3都借了1亿。 大富翁没有接。 请问这个时候, 这些私生子是不是相信大富翁的10亿家产是自己的? 肯定是相信的, 因为大富翁有10亿, 而他们是大富翁的孩子, 所以他们相信大富翁的家产未来是自己的。 而实际上呢? 可能吗——不可能, 因为大富翁本质上有3个孩子, 这些人顶多分3.3亿。
那么操作系统对于一个进程呢?大富翁其实就是这里的操作系统, 进程呢? 就是私生子。 每一个进程都被操作系统画了一张大饼, 这个大饼就是——进程地址空间。
这个进程地址空间就是用来标识操作系统所有内存的空间范围。 每一个进程都有一个虚拟地址空间, 但是要给进程可以全部占用物理内存吗? 不可能。
那么每一个进程都有一个虚拟地址空间后, 就能使用一样的视角——虚拟地址空间——来看待物理内存了。 进程1可以使用虚拟内存中的0x111, 进程1的子进程同样可以使用虚拟内存中的0x111——虚拟内存一样, 但是映射到物理内存就是不一样的内存。
所以, 我们就知道了为什么要有虚拟内存的第二点——有统一的视角。
同样的, 对于进程来说, 如果没有虚拟地址空间, 进程如果发生了非法访问, 那么自己的进程被操作系统干掉是小事。 而如果非法访问影响到了其他程序呢? 这个时候问题就大了,但是如果加上虚拟地址空间呢? 就能在物理内存和进程之间加一个缓冲, 如果进程发生了非法访问。 那么虚拟地址空间内有这个地址, 也就没有映射到页表当中, 没有映射到页表当中,页表就会对进程进行拦截, 然后操作系统就会直接干掉这个进程, 防止发生危险。 ——这就是为什么要有虚拟地址空间的第三个原因:因为增加虚拟地址空间, 可以在访问内存的时候,增加一个转化的过程。 在这个转化的过程中可以进行系统级别的检查, 所以一旦异常访问, 直接拦截, 该请求就不会直接到达物理内存, 保护物理内存。
我们重新理解后的进程可以是: 进程 = 内核数据结构(task_strruct + mm_struct + 页表) + 进程的代码和数据。
而在进程切换的时候, 只要我们的进程一切换, 那么进程的PCB就切换了, PCB一切换, 因为PCB指向进程的地址空暗金, 所以PCB一切换, 地址空间就切换了。 而我们的cr3寄存器存在于进程的上下文中, 所以进程一切换, cr3也切换, 也就是页表发生切换。 所以进程整个就切换了。
-----------------------------以上就是本节全部内容, 下面是博主整理的本节笔记:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。