进程(2)

引言:

上次我们学习了进程的一些基本概念以及进程的状态,今天我们就来接着学习进程的优先级以及进程的调度.

一 : 进程优先级

1. 是什么?

在给出优先级的概念之前,先来拿前面学习过的权限来做对比.

权限解决的是能不能的问题,而优先级的前提是已经能了,它解决的是先后的问题.

概念:

cpu资源分配的先后顺序,就是指进程的优先权(priority

现实例子:

比如日常生活中的排队现象,这个排队的动作本质就是在确定优先级,再比如找工作时的简历的优劣,也会影响你找工作的优先级.

2. 为什么?

为什么要存在优先级呢?从现实生活中的现象入手,大家通过高考进入大学,本质也是通过优先级来决定的,分数越高,选择的范围也就越大,如果没有高考直接进入大学可以吗?大家都直接读好大学,显然这是不现实的,因为资源是有限的,因此大家都才特别努力地卷学习,为了就是提升自己的优先级以此来获得更多的资源.

3. Linux下是怎么设计的呢?(最重要的部分)

既然要认识优先级,那我们就先来见见Linux下的优先级:

通过命令 ps - l 即可查看:

这里的PRI就是优先级(priority)的缩写,NI是nice的缩写.

优先值越小说明其优先级越高,越先被执行.

因为进程是由内核数据结构 + 代码和数据构成的,而这个内核数据结构就是task_struct因此当前阶段就可以理解为PRINI就是保存在task_struct里面的两个整形变量.

(1) PRI 与 NI
  1. PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小说明进程的优先级别越高,
  2. NI就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值.
  3. PRIPRI(new)=PRI(old)+nice(优先级的计算公式)
  4. 所以,调整进程优先级,在Linux下,就是调整进程nice值.
  5. nice其取值范围是-20 - 19,⼀共40个级别.
(2) PRI vs NI
  1. 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化.
  2. 可以理解nice值是进程优先级的修正数据.

4. 修改进程的优先级:

在了解完进程的PRI和NI之后我们可以理解了修改进程的优先级,是通过修改其nice值实现的,那么如何进行修改呢?

这里我们先介绍一种方法: top ->进入top后按 r‒> 输⼊进程的 PID ‒> 输入nice



可以看到该进程的优先级就被修改成了70

5. 思考:

(1) 思考1: nice的取值范围为什么是-20 - 19

既然有优先级,那么我为什么不能搞个无穷大呢?如果我想让一个进程最先被执行,就把它的优先级值搞成很小,想让它最后执行,就把它的优先级的值搞成特别大,为什么不能这样呢?

因为更改优先级不会是一个高频的动作,如果频繁修改优先级会影响进程调度

而且为什么会有这个范围限制呢?这就要提到操作系统,除了工业控制领域,其他情况下我们的操作系统都是分时操作系统,而分时操作系统是针对时间片的轮转来实现的,

它会给每个进程分配一个时间片,当轮到它的时候就让CPU调度,等时间片到了之后,就将其从CPU上剥离,这样做的目的是为了让进程都能尽可能的平均地来占用CPU资源.

这样做也是比较合理的, 举个例子:假设你今天在你的电脑上想要启动游戏,但是你的电脑的一款杀毒软件正在工作,操作系统说先杀完毒才能启动游戏,那你肯定不爽啊,因此分时操作系统更能满足互联网与用户的需求.

既然提到了分时操作系统,那么就存在实时操作系统,实时操作系统主要是应用在工业控制领域,什么是实时操作系统呢?对比分时操作系统,实时操作系统就是一根筋,来一个进程它就要把当前进程执行完之后才能执行后面的进程.其实这也合理吧,比如汽车生产时,当前这个机械臂正在对车身进行喷漆操作,你总不能喷漆到一半去组装轮胎吧.

但大多数情况下分时操作系统都是带有实时操作系统的子功能的,就拿自动驾驶汽车来说吧.你一边开车一边放着车载音乐,当遇到障碍物需要紧急制动刹车时,此时如果是实时操作系统的话,你的操作系统会说:此时正在放着音乐,等放完之后才能进行刹车.那此时不就出事了嘛,所以其也会有分时操作系统.

(2) 思考2: PRI (old)是什么?

上面我们将进程的优先级改成了70,那么我们此时想将其优先级增加10,那么此时你可能会认为 70 + 10 = 80,那我们来看看是这样吗?

事与愿违啊,此时进程的优先级是90,也就是说其实这个进程的PRI(old)为80,因此其实这个PRI(old)是一个固定值.

6. 补充概念-竞争、独立、并行、并发

(1) 竞争:

竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级.

(2)独立

独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰.

拿父子进程来举例子,我们知道父进程在创建子进程之后,子进程会共享父进程的代码和数据,但是它们还是有自己的独立性的,因为它们都有自己的task_struct.

(3) 并行:

并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行.

单看概念可能有点模糊,举个具体例子吧,就比如拿学生给老师背诵课文,可以一个个来,但为了节省时间,老师就会让一些学生一起来背诵,在这个过程中通过观察学生背诵程度也能判断其有没有背下来,这个过程中这些学生是同时进行地背诵动作,因此称为并行.

(4) 并发:

并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发.

还是拿刚才学生给老师背诵课文为例,只不过这次是一个个地给老师背诵,但是每个人都有一个限定时间,比如说30s,当时间到了,如果没背完也就先停止换下一个同学,当下一次再轮到的时候再接着背诵.

二: 进程切换

之前我们说操作系统管理进程的方式是:先描述,再组织.但前面我们都只学习了怎么描述,接下来我们就要学习其是如何组织的了.

首先要清楚一个概念:寄存器是共享的,但是寄存器里面的数据是进程私有的,这里面的数据叫做进程的上下文. (联想之前我们讲的休学和返学的例子).

1. 前置知识:

在正式讲解进程是如何组织之前,先来讲一点看似不相关的东西.

2. 双链表来组织task_struct

很简单.这不就是我们前面提到的问题嘛,已知结构体中某个元素的地址,那么不就可以获取这个结构体的地址么,之后也就能获取其他属性了.

(2) 思考二: 为什么它要这样设计呢?

比起传统的链表式指针域和数据域放在一块的形式,这样设计肯定是有好处的.

如果是之前的那种传统的结构,假如这次我要维护的是一个银行信息的进程,而下次我要维护的是学校信息的进程,那么此时数据不一样,就需要维护两份代码,但如果这个指针域和数据域分开了的话,我是不是只需要维护这个指针域的代码,直接移植过去就行.
这就提高了链式管理的拓展性.

(3) 思考三: 一个进程的PCB怎么能放在不同的数据结构中???

我们知道Linux 会将所有进程的task_struct 都放在一张双链表中,但是进程不是还有运行队列以及阻塞队列...吗?它怎么能既存在于这张双链表中,又存在于运行队列和阻塞队列中呢???

3. 调度队列( 重中之重 )

下面我们就来介绍一下Linux中O(1) 级别的调度队列,感受一下大佬的绝妙思路:

上图是Linux2.6内核中进程队列的数据结构,之间关系也已经给大家画出来,方便大家理解.
注: 每一个CPU有一个调度队列.

(1) 活动队列queue 介绍:
  1. 普通优先级:100〜139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
  2. 实时优先级:0〜99(不关心)

下面的分析过程中我们只考虑普通优先级范围:

a. nr_activebitmap

显然,当CPU在选择合适的进程进行调度时,会从0一直找到139,但万一活动队列中就没有进程呢?那此时遍历一遍队列不就很浪费时间吗???

放心,普通人能想到的,大佬都考虑到了,于是有下面这个概念:

nr_active:总共有多少个运行状态的进程.

如果CPU发现该变量为0时,自然也就不会去遍历活动队列了.

即使遍历一遍,这个时间复杂度是有上限的,因此是O(1)级别的,但是还可以再优化吗?

可以的,于是就有了下面这一概念:
bitmap[5] : ⼀共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!

b. 过期队列与active / expired指针

是有可能的,假设这样一个场景: 队列中都是优先级为61的进程,但此时不断加入优先级为60的进程,那么此时这些优先级为61的进程会被一直"插队",于是一直等不到调度,

此时就产生了"调度饥饿"问题.

面对这个问题,大佬是怎么解决的呢?

还是看这张图:

其实,这里设计了两个队列:活动队列和过期队列,活动队列为array[0],过期队列为array[1].

当有新的进程来到时,CPU会将其先放到优先级映射的过期队列,CPU只会执行活动队列中的进程,每次执行完一个进程之后,CPU就会将其放到优先级映射的过期队列中,因此,活动队列中的进程数是会慢慢减少的,等活动队列里的进程执行完之后,这时候就用到了两个指针:active指针 和expired指针,前者是指向活动队列的,后者是指向过期队列的,此时将两个指针的内容swap一下,是不是又进入到新一轮的调度? 这就很好的避免了调度饥饿的问题,大佬的设计思路啊,真的优雅!

三: 命令行参数

1. 问题引入:

首先来问大家一个问题:main函数可以有参数吗?

答案是可以有,大家之前在学习过程中可能看到过这种写法:

接着编译运行一下看看:

可以看到没有问题,到这有同学就有疑问了,这里的main函数怎么能有参数呢? 这里的参数是什么呢?

2. 命令行参数:

我们先来观察这两个参数,第一个是int类型的变量,第二个是char* 的指针数组,

其实这里的argc指的是命令行参数的个数,第二个参数是命令行参数列表.

接下俩我们来将其打印出来看看:

编译运行:

可以发现这里的argc为1,说明只有一个命令行参数,接着打印这个命令行参数可以发现:

这个命令行参数就是 ./ process.c 这不就是输入的程序名吗?

如果我们在运行的时候在后面加上其他的字符串会发生什么呢???

我们发现在运行的时候,我们在后面输入其他的字符串也会被当做命令行参数,并且这些命令行参数是以空格作为分隔符的.

3.思考

(1) argc的取值范围是多少呢?

其实这里的argc最小为1,因为argv[0] 一定是指向程序名的.

(2) 为什么要有命令行参数呢?

我先来写一段代码:

然后编译运行:

现在是不是恍然大悟了,这是啥啊? 这不就是同一个命令的不同选项吗???
ls - a ls - l 是不是? 原来命令行参数可以用来实现指令的不同选项,以此来增加其子功能啊!

现在知道为什么要有命令行参数了吧!

(3) argv列表的最后一个元素是啥?

其实argv列表中的最后一个元素一定是NULL,可以来验证一下:

如果最后一个元素为NULL的话,最后for循环会停下来

验证成功!

(4) cat 是如何 打印出文件内容的呢?

我们知道cat + 文件名 就可以打印出文件的内容,那么它是怎么做到的呢?

其实也是通过命令行参数做到的,cat是一个函数,它也会有命令行参数,因此它可以获取到文件名, 通过文件名就能获取到文件的内容(因为argv[0]指向的就是程序名),之后再进行只读,然后进行拷贝,再将内容打印出来,这不就完成了嘛.

四: 环境变量

1. 概念引入:

先来看一个现象:

这是写的一段简单代码,接着编译运行

这里在运行我们自己创建的程序时,前面要先加上./ 这是为什么呢?这个问题前面我们提起过,这是因为OS不知道我们的这个程序在哪里,因此./的本质其实就是在告诉OS用户要执行的程序在当前路径下.

那为什么其他的指令不用加./呢?

比如这里的这三条指令都是直接执行的,都没有加./,这是为什么呢? 在Linux下,一切皆文件,那我们来看一下这些指令的文件位置吧

可以看到这些指令的文件位置清一色的都是在/usr/bin路径下,

这里给出结论:其实不加./的话,OS默认是去/usr/bin路径下去查找,到这有同学可能就有想法了,那我把自己写的程序地址改到这个路径下不就不用带./就可以执行了吗?

那来试试吧:

确实,这个办法是可行的.

这里有同学可能会有这样的疑问: OS怎么会知道去/usr/bin 路径下去找呢?它还会不会去其他路径下寻找呢? 这里就需要引入环境变量的概念了:

2. PATH环境变量:

先来查看一下PATH环境变量的内容:

为什么OS会去usr/bin路径下查找,就是由这个PATH环境变量的内容决定的,可以看到其中包括了很多由:作为分隔的路径,其中就包括了usr/bin路径, 所以说,如果我们将我们自己程序的路径也加入到这个PATH里面,那不也能解决问题吗?

于是就有同学这样来写:

这里直接将PATH的值改为了我们自己程序所在的位置,这样对吗?

显然有问题,我们是要添加,而不是覆盖,这里将PATH的内容都覆盖掉了,导致有的其他指令都失效了,不用担心,重新启动xshell 之后 PATH就会恢复初始值.

那么正确的添加方式是什么呢?

在原有内容的基础上添加,这样也就不用加./也能直接运行这个路径下的程序了.

思考:PATH变量的本质?

其实PATH变量的本质就是辅助OS进行查找的一个环境变量.

3. Windows下的环境变量



这里拿浏览器为例:

将其路径添加到这个PATH环境变量下,之后就可以在命令行模式下打开浏览器

感兴趣的同学可以自己来试试

4. 环境变量与c/c++代码以及进程之间的关系?

(1) 用main函数参数获取环境变量

这里的环境变量和前面学的pid是不是一样能通过代码获取呢?

对的,环境变量也是作为main函数的一个参数,可以被获取,下面看一段代码:

这里我们通过main函数的第三个参数获取了环境变量.

(2) 借助第三方变量environ来获取环境变量:

那就需要先来认识下 environ

接着我们就用它来获取环境变量:

(3) 借助函数来获取环境变量:

先来认识一下这个函数 getenv :


(3) 再探方式1获取环境变量:

这里通过main函数的参数来获取环境变量,本质是把环境变量表交给进程,那么进程是从谁的手中拿到的的?其实很容易猜到,就是它的父进程啊,因为前面学习父子进程的时候我们知道,子进程是可以继承父进程的代码和数据的,因此子进程就可以拿到这个环境变量表,那最上面的父进程是谁呢? 无疑是bath进程,但bath是一号进程,那它又从谁手中拿呢?其实这里bath是通过Linux的配置文件中拿到的,在启动xshell的时候,bath进程会被创建,此时它会从配置文件中拿到环境变量表,之后不断让下面的进程继承,这样进程就都可以拿到这张表了,当前层面我们只能认为是子进程从父进程那继承得到的,具体细节后面会学到.

其实这里也可以说明一个问题:这个环境变量是具有全局属性的,因此子进程才能从父进程那里拿到,并且这里的环境变量是内存级的,这也是为什么一开始我们修改了PATH内容之后,重新启动xshell之后PATH值会恢复了,bath会重新从配置文件中获取.

(4) 这个环境变量有什么用呢?

回过头来看之前获取的环境变量,

仔细看这些信息:有 pwd:当前路径 LOGNAME:登陆人HOME:家目录...

这是不是跟我们现实生活中的描述一个人的信息很像,比如我们的学生证... 正是有了这些信息,我们在执行pwd的时候就知道当前路径...

总而言之,不同的环境变量都有不同的应用场景.

(5) 修改配置文件:

既然bath是从配置文件中获取信息的,那么我们来修改下配置文件验证一下:

打开配置文件:

添加一条语句:

重新启动xshell:

可以看到我们添加的语句就自动被执行了.

5. 本地变量 && 环境变量 && 内建命令

(1) 本地变量

既然环境变量是个变量,那么我是不是也能自己创建呢? 是的,我们可以自己创建,

接下来就来试一下:

这里我们自己创建了一个变量,并且还可以对该变量进行查看.

那在程序中能不能对其进行获取呢?

此时并没有找到这个变量,因此说明我们自己创建的变量并不是环境变量,其实我们自己创建的变量是本地变量,但是可以将其变成环境变量:
export + 变量名:将本地变量导入到环境变量中

那怎么去掉这个环境变量呢?
unset + 变量名: 清除这个环境变量

此时可以看到这个环境变量就被清除了

(2) 内建命令

先来看这样一个场景:

可以看到PATH变量被清空之后很多指令都不能使用了,但是pwd指令还能运行,这是为什么呢?这是因为pwd属于内建命令,也就是说同是命令,但命令和命令之间亦有差别,

大多数指令都是存在的二进制级别的文件,而内建命令是shell内部自己定义的,是自己内部的一次调用.并不依赖于第三方路径.

(3) 本地变量与环境变量的区别:

环境变量具有全局属性,可以被子进程获取.

本地变量不具有全局属性,只能在bash内部自己使用,不能被子进程获取.

五: 程序地址空间

1. 旧知回顾:

之前在C语言学习阶段,大家可能见过这张图(在正文代码和初始化数据之间其实还有一个常量区):

但当时我们只是见过而已,其实并不理解这些区域,

那这个图代表的是内存吗? 有些同学可能会认为是内存,因为这里的代码数据一定是存在内存中的,因此其肯定是内存,那我问你,进程的PCB在哪? 这时就说不通了,

那此时又有同学说这个不是内存,那我问你,代码和数据不是存放在内存中的吗?

是不是又说不通了,存在争议了,先放一放,后面解答.

我们先来验证一下这张图中的区域分布:

(1) 总体区域划分验证:

我们先来对总体区域划分进行验证:

  1. 打印main函数的地址即为正文代码区域.
  2. 这里的heap_mem 指向的就是堆上开辟的地址,因此不需要取地址
  3. 这里的&heap_mem 打印的就是栈上开辟的heap_mem的地址,需要取地址
  4. 这里的test 是定义的静态整形变量.
  5. 这里的addr是定义的只读字符串常量.

接着打印查看结果:

可以看到地址大小分布和上图一致.

补充问题: 这里的char* 前面不加 const 可以吗?

这个细节可能会有同学注意到: 其实这里不加const也是可以的,编译器顶多会给你一个警告,但不加const 不意味着这个字符串就可以被修改了,因为这是个常量字符串,是由操作系统层面决定的,这里加const只是程序员自身的一个预警,告诉自己这个字符串不能修改,也是为了在编译的时候就能发现错误,而不是在运行的时候才发现错误.

(2) 堆\ 栈 相对而生:

注意上图中堆和栈区域分别有一个向上和向下的箭头,这是什么意思呢?

其实是在栈区域中开辟空间是往下开辟的,在堆区域是向上开辟的,

但是在使用的时候都是从低地址开始的,

下面我们来验证下栈:

编译运行:

可以看到依次在栈区开辟的地址是依次递减的.

接着再来验证堆:

编译运行:

可以看到在堆上开辟的地址是依次增大的.

2. 虚拟地址:

(1) 现象引入:

先看下面这样一段代码:

编译运行:

我们来分析一下:

父进程通过fork 创建子进程,子进程可以共享父进程的数据,因此在子进程中也可以打印g_val 的值,这没问题,我知道,但这里子进程对g_val 进行了自增操作,之后打印出的值也变化了,但是父进程那里打印出的值还是0,怎么会这样呢? 一个g_val 怎么可能同时为0,又同时等于其他值呢? 这不合理吧!

于是就要引出一个新概念- 虚拟地址

(2) 技术层面来解释这一现象:

先来想一个问题: 这里打印出的地址可能是物理地址吗? 显然不可能,因为一个物理地址只可能对应着一个唯一值,其实这个地址是虚拟地址. 下面就来介绍虚拟地址:

其实父进程中 存在着一个虚拟地址空间,还有链接虚拟地址空间和物理内存的页表,

虚拟地址空间的变量通过页表,将虚拟地址映射为物理地址,这样就能访问真实的物理内存, 那怎么解释上面的现象呢?

结论一:

想一件事,既然父进程有虚拟地址空间和页表,那么子进程是不是也应该有,因为子进程会继承父进程的那一套东西啊,

结论二:

fork之后,父子会共享代码和数据,为什么呢?因为子进程会拷贝一份父进程的页表,类似发生了浅拷贝.

新旧结论结合:

之前我们学习进程的时候知道进程之间是具有独立性的,但是这里发生浅拷贝合理吗?

这样不就会导致多个指针指向同一块地址空间吗? 其实大多数时候代码都是只读的,所以就不会破坏这种独立性,但是如果子进程对代码进行修改的话就会破坏这种独立性,

其实OS已有规定来维护这种独立性:

父子进程之前如果有其中一方对数据进行修改的话并不是直接进行修改的,是要通过发生"写时拷贝"实现的.

这里的"写时拷贝"类似于是发生一次深拷贝,也就是说子进程在进行修改数据的时候,并不是直接进行修改,而是会再重新开辟一块新的空间,之后再进行修改,

这样其实就能解释上面的现象了.

而且此时还可以解决之前遗留的一个问题:

"fork的返回值 id 怎么可能等于0 又大于0"

3. 一些概念的理解:

(1) 什么叫做虚拟地址空间:

到这大家其实只是看到了这个东西,但其实并不知道它具体是个什么东西,下面通过一个例子来理解:

(2) 如何理解虚拟地址空间中的区域划分呢?

要想理解这个例子先回忆下小时候你跟同桌在吵架时是不是可能会划一道分界线,这个分界线就是在划分区域,那么该怎么用计算机语言来描述呢?

大概就是类似于这种,记录每个区域的起止和结束,其实这样划分的结果是什么呢?

会让某个区域获得一块连续的地址空间.

Linux内核里面差不多也是这么来写的.而并不是通过指针来指向,因为我只需要知道这个区域的范围,之后再通过指针...来访问就行了.

(3) 为什么全局变量\静态变量的生命周期是全局的?

其实,在虚拟地址空间中,由于 全局数据区和常量区会一直存在,当进程结束的时候才会被释放,也就是说这些区域的数据生命周期是随进程的,因此它们的生命周期为全局的.

(4) 常量字符串和代码是怎么做到只读的?

要想解决这个问题就要重新认识下页表.

通过页表内的权限就可以实现这个功能,而且页表还有其他标志位来实现不同的功能,后面再学.

4. 补充问题: 为什么要有虚拟地址空间?

(1) 为了控制进程的行为

先来举个例子: 在我们小时候,过年时会收到长辈们给我们的压岁钱,大多数人应该都会被妈妈哄骗说:你还小,妈妈先来帮你保管,等你要花的时候找我要就行. 如果不让妈妈保管的话,我们拿着压岁钱就可以随便去商店消费,但是如果让妈妈保管的话,你买东西之前就需要先通过妈妈这一关.

类比到计算机这里, 这里的进程就是我们, 虚拟地址空间和页表 就是帮你保管压岁钱的妈妈,你要买的东西就是物理内存, 只有当你的请求合理的时候,妈妈才会给你钱,也就是说只有进程的请求合理的时候,虚拟地址空间才会进程分配物理内存.

也就是说 这里的虚拟地址空间存在的原因是为了控制进程的行为(因),从而达到保护物理内存的目的(果).

(2) 让进程的内存分布空间由无序变有序:

这句话是什么意思呢?下面来解释:

首先想一个问题: 在物理内存空间里面,代码和数据是加载在固定位置还是随便加载的呢?

其实是随便加载的,为什么可以这样呢? 正是由于虚拟地址空间和页表的存在. 由于虚拟地址空间的分布是固定的,也就是说在虚拟地址空间里是有序的,虽然在物理内存空间里面是无序的,但是通过页表映射在虚拟地址空间里就是有序的,这也是虚拟地址空间存在的意义. 试想一下如果没有页表,那么你在虚拟地址空间里面要考虑放到哪,在物理内存地址空间里还要考虑放到哪,是不是太麻烦了呢?

(3) 让进程管理和内存管理解耦合

将我们之前所画的图分下区域:

想一个问题: 一个进程在的时候,是先创建它的内核数据结构还是先加载代码和数据呢?

其实是先创建它的内核数据结构,举个例子,你malloc一块空间之后,你可能并不会立马就使用这块空间,其实这个时候只是在虚拟地址空间给你申请了一块空间,当你使用的时候才会给你在物理内存申请, 也就是说一个进程在被你创建之后,你可能并不着急立马执行这个进程,那么此时的代码和数据就不着急加载到内存中,可以等到执行这个进程的时候再加载,甚至一边执行一边加载,这个就叫做懒加载. 这么做的好处什么呢? 如果你不着急运行这个进程的话,晚点把代码和数据加载到内存中的话就能省下这些内存给其他进程使用,因为目前你又不着急使用,这样就提升了空间的利用效率.

但话又说回来,你为什么能这么做呢? 这得益于 进程管理块和内存管理块 的互相独立,因为双方并不会相互影响,因此就可以实现这种懒加载的场景. 这也叫做进程控制和内存控制的解耦合.

(4) 综合考虑:
  1. 安全风险
    每个进程都可以访问任意的内存空间,这也就意味着任意一个进程都能够去读写系统相关内存区域,如果是一个木马病毒,那么他就能随意的修改内存空间,让设备直接瘫痪。
  2. 地址不确定
    众所周知,编译完成后的程序是存放在硬盘上的,当运行的时候,需要将程序搬到内存当中去运行,如果直接使用物理地址的话,我们无法确定内存现在使用到哪里了,也就说拷贝的实际内存地址每⼀次运行都是不确定的,比如:第一次执行a.out时候,内存当中一个进程都没有运行,所以搬移到内存地址是0x00000000,但是第二次的时候,内存已经10个进程在运行了,那执行a.out的时候,内存地址就不一定了.
  3. 效率低下
    如果直接使用物理内存的话,一个进程就是作为一个整体(内存块)操作的,如出现物理内存不够用的时候,我们一般的办法是将不常用的进程拷贝到磁盘的交换分区中,腾出内存,但是如果是物理地址的话,就需要将整个进程一起拷走,这样,在内存和磁盘间拷贝时间太长,效率较低。

但有了虚拟地址空间和页表就可以解决上述问题,

5. 虚拟地址空间的分布是连续的,但是堆不是,那么是怎么管理的呢?

因为在堆区里面可能会出现这种情况:

其实在Linux内核中是这样进行管理的:

存在一个结构体mm_struct 来管理虚拟地址空间,在这个结构体中存在一个叫做mmap的指针来管理 开辟的这些空间,这里在堆区开辟的一个个空间就是一个个vm_area_struct, 这样结构体像链表一样串起来,即使不连续也能被管理起来,因此不仅仅堆区是这样,其实整个空间都是这样进行管理的:

相关推荐
FoldWinCard3 小时前
D5 Linux 网络及端口命令
linux·运维·服务器
零涂毕业设计4 小时前
毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享
linux·stm32·单片机·嵌入式·esp32·fpga·oled
tedcloud1234 小时前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
霸道流氓气质4 小时前
KMS 密钥管理服务(Key Management Service)原理与实践
linux·服务器·数据库
SakitamaX4 小时前
LVS(Linux Virtual Server)概念详解
linux·运维·lvs
小张同学a.5 小时前
Linux系统管理
linux·运维
阿龙的工作记录5 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
代码村新手5 小时前
Linux的基本指令
linux·运维·服务器
hkj88086 小时前
Ubuntu 切换java版本
java·linux·ubuntu
撩得Android一次心动7 小时前
Linux编程笔记3【个人用】
linux·笔记