【Linux】进程概念(四):Linux进程优先级与进程调度的核心逻辑
文章目录
- 一、进程优先级
- 查看进程优先级
- PRI and NI
- 修改进程优先级
- nice值为什么有范围
- 二、竞争、独⽴、并⾏、并发
- 三、进程切换
- 三、linux进程调度策略
一、进程优先级
进程优先级是进程获得某种资源的先后顺序,优先级和权限不一样,权限是能和不能的问题,优先级是大家都能,只是有先后顺序。进程优先级出现的原因是进程多而资源少,让每个进程都能有序的获取资源。
查看进程优先级
在linux系统中,⽤ps ‒l命令会类似输出以下⼏个内容:
- UID : 代表执⾏者的⾝份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍⽣⽽来的,亦即⽗进程的代号
- PRI :代表这个进程可被执⾏的优先级,其值越⼩越早被执⾏
- NI :代表这个进程的nice值
PRI and NI
- 我们首先要清楚PRI和NI是整数变量存储在进程的task_struct里的。 PRI也还是⽐较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执⾏的先后顺序,此值越⼩进程的优先级别越⾼。
- NI就是我们所要说的nice值了,其表⽰进程可被执⾏的优先级的修正数值,RI值越⼩越快被执⾏,那么加⼊nice值后,将会使得PRI变为:PRI(new) = PRI(old) + nice,上面通过ps -l 指令显示的PRI值是PRI(new)的值。
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变⼩,即其优先级会变⾼,则其越快被执⾏,所调整进程优先级,在Linux下,就是调整进程nice值。
- 这时有读者可能会疑惑为什么不直接调优先级,而是用nice值来调,这是因为linux优先级和进程调度息息相关,需要我们学习了linux的进程调度才能理解,这里小编就不展开讲了。
- nice其取值范围是-20⾄19,⼀共40个级别。
修改进程优先级
我们首先介绍⽤top命令更改已存在进程的nice,步骤如下:
进⼊top后按“r”‒>输⼊进程PID‒>输⼊nice值
下面我们实际操作一下,注意普通用户只能将进程的nice值调大(降低优先级),只有超级用户(root)才能将nice值调低(提高优先级)
1、运行进程一开始的优先级为80,nice值为0。
2、top指令后按r,然后输入要修改优先级的进程pid:
3、输入nice值:
我们可以看到8952进程的NI确实覆盖为了10,PRI也变成了90:
当我们再把nice值改为-10后优先级是变成80吗?nonono,是变成70,因为在linux中PRI(old)固定为80,这样在改变进程优先级时再也不关注进程曾经的优先级了,这是一种简化做法。
除了top指令之外,还可以用nice,renice指令调整优先级,示例如下:
nice -n 10 command_to_run
- nice 命令用于在启动一个新进程时设置其优先级。
- -n 选项后面跟的数字表示优先级,数值范围通常是 - 20 到 19,数值越小优先级越高(-20 是最高优先级,19 是最低优先级)。
- command_to_run 是要启动的命令,例如 nice -n 10 sleep 60 ,表示以优先级 10 启动 sleep 命令,让其睡眠 60 秒。
renice -n 5 -p 1234
- -n 5 指定新的nice值为 5(降低优先级。
- -p 1234 指定要修改的进程 ID 为1234。若要提高优先级(设负值),需root权限,例如 sudo renice -n -10 -p 1234(将 PID 为1234的进程nice值设为-10,提高优先级)。
nice值为什么有范围
关于为什么优先级要设置nice值范围小编想聊两句,这主要是未来防止极端资源抢占:如果nice值没有范围限制,用户可能会将某个进程的优先级设置得极高,导致该进程几乎独占CPU等系统资源,使得其他进程长时间得不到执行,进而影响整个系统的稳定性和可用性(进程饥饿问题),在实际开发中,我们很少调整进程优先级,会破环调度器的调度平衡,就算要改,也要在可控的范围内改。
但是有关为什么是nice值具体范围是[-20,19]要学了后面的进程调度算法才能明白。
二、竞争、独⽴、并⾏、并发
- 竞争性: 系统进程数⽬众多,⽽CPU资源只有少量,甚⾄1个,所以进程之间是具有竞争属性的。为了⾼效完成任务,更合理竞争相关资源,便具有了优先级。
- 独⽴性: 多进程运⾏,需要独享各种资源,多进程运⾏期间互不⼲扰。虚拟机、解释器技术就是基于此实现的,因为虚拟机、解释器本质也是跑在一个进程上的程序。
- 并⾏: 多个进程在多个CPU下分别,同时进⾏运⾏,这称之为并⾏。
- 并发: 多个进程在⼀个CPU下采⽤进程切换的⽅式,在⼀段时间之内,让多个进程都得以推进(同时得到调度),称之为并发。CPU进程切换,在物理上认为CPU只有一个,在逻辑上,可以认为有几个进程就有几个CPU,只不过每个进程的效率低一点。
三、进程切换
在介绍进程切换小编补充一个概念,时间⽚:当代计算机都是分时操作系统,每一个进程都有它合适的时间⽚(其实就是⼀个计数器)。时间⽚到达,进程就被操作系统从CPU中剥离下来。基于此,操作系统可以做到以较为公平公正的方式进行进程调度。
CPU上下⽂切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运⾏另外的任务时, 它保存正在运⾏任务的当前状态,也就是CPU寄存器中的全部内容。这些内容被保存在任务⾃⼰的堆栈中,⼊栈⼯作完成后就把下⼀个将要运⾏的任务的当前状况从该任务的栈中重新装⼊CPU寄存器,并开始下⼀个任务的运⾏, 这⼀过程就是context switch。
进程被换走后它的上下文数据会存放在它的task_struct的任务状态段(tss)中。 参考⼀下Linux内核0.11代码:
三、linux进程调度策略
关于进程调度我们不谈操作系统学科的进程调度,只关注linux系统的进程调度。linux系统CPU的调度队列并不是一个单纯的先进先出队列,我们先看下面linux调度队列的示意图:
runqueue本质是一个结构体,它内部不仅有内置类型,也嵌套了结构体变量,我们重点关注*active、*expired,和红蓝方框两部分,我们先看一眼linux源码对它们的定义:(这里小编只截取了一部分)
struct prio_array* active, * expired, arrays[2];
int *pa, *pb, arr[2]; //示例
这句代码类似下面一行代码的定义方式,定义了两个结构体指针,和一个结构体数组,数组里有两个结构体元素,分别对应红、蓝两个方框结构体。
结构体prio_array内部定义了一个整型变量,一个位图,一个 struct list_head 类型的数组。
清楚了定义后再来看runqueue是如何实现进程优先级的,我们先看queue数组,它的类型是struct list_head,我们可以大胆猜测这是调度队列中用来链接进程task_struct的,它一共有140个元素,其中0-99部分是实时优先级(这里暂时不关心),100-139是普通优先级,我们回想一下前面介绍的nice值范围是[-20,19],进程优先级有40个级别,范围是[60,99],正好和这里对应,进程优先级加40就可以对应queue中普通优先级的40个下标。所以queue结构类似一个链地址法定义的哈希表,每个数组元素就是一个进程队列(物理结构是双向链表)并对应一个优先级,优先级相同的进程遵循先入先出的原则将进程task_struct的list_head链入同一数组元素的进程队列。
接下来我们看prio_array结构体的另外两个成员,nr_active表示当前queue中有多少活跃的进程,DECLARE_BITMAP是定义的宏,本质就是位图,我们首先明确CPU调度当前队列的策略是选取当前队列中优先级最高的进程队列,然后.拿到选中队列的第⼀个进程开始运⾏,如果没有位图就需要遍历整个queue[140],效率很低,而位图可以通过它快速定位最高优先级的非空队列,特别是当queue有很多空队列,例如queue的前32个队列为空,那么位图能一次操作排查32个队列。位图大小为5,因为queue有140个元素,5*32=160能覆盖所有下标且不浪费。
然后来看runqueue是如何实现进程调度的,我们知道linux是分时操作系统,每一个进程都要对应的时间片,时间片耗尽进程就会从CPU上剥离下来,剥离下来的进程去哪了呢?这里就要引出两个概念:活跃队列和过期队列,runqueue里的结构体指针变量*active指向活跃队列,*expired指向过期队列,对应图中红蓝两个方框,活跃队列和过期队列的结构一摸一样。CPU会优先按优先级运行活跃队列的进程,因为有时间片的存在,进程不会一直运行,所以在活跃队列中时间片耗尽的进程会按优先级链入过期队列中,当CPU把活跃队列里的进程全部运行完后(操作系统检查活跃队列的nr_active等于0),系统会自动交换*active和*expired两个指针的内容,之前的过期队列就变成了活跃队列,然后CPU继续拿活跃队列中优先级最高的进程开始运行,周而复始。
这种设计策略可以反向解释为什么要有nice值,为什么调整进程优先级不是直接调整,而是修改nice值,如果当前进程正在活跃,直接调整进程优先级就意味着需要将进程从活跃队列的链表中拿下来,重新插入到对应优先级的链表,而调整nice值的话该次活跃队列的调度还是按原优先级调度,进程在该次调度运行完插入过期队列时会按照新的nice值重新计算优先级并插入,这样可以节省一次插入操作。
并且这种设计也间接解决进程饥饿的问题,因为活跃队列和过期队列的存在CPU会把局部进程全部调度完才会切换下一个,即便一些优先级较低的进程在本批次调度中也有享有CPU资源的机会。这样可以保证局部上有优先级,整体不会造成进程饥饿。
总结:在系统当中查找⼀个最合适调度的进程的时间复杂度是⼀个常数,不随着进程增多⽽导致时间成本增加,我们称之为进程调度O(1)算法!
解释先前遗留问题:
这时我们再回头看为什么queue数组有140个元素,首先我们要知道一个概念,如今操作系统按功能划分为两种:分时操作系统(windows\linux)和实时操作系统(rtos),分时操作系统是基于时间片轮转调度,它强调调度任务要公平公正,实时操作系统更注重实时性,来一个任务立即确定它的优先级,只有把一个任务执行完才会执行下一个任务。linux虽然是分时操作系统,但是它也支持实时操作系统的功能,这就是我们经常听到的linux内核裁剪,通过裁剪可以使linux只支持实时操作系统的功能。linux中的实时系统优先级就是[0-99]。
这里小编再谈谈为什么linux操作系统弱化了就绪状态和运行状态两者的区别,操作系统学科认为此时此刻被CPU运行的进程是运行状态,具备运行条件但因 CPU 被其他进程占用,暂时并等待 CPU 调度的状态是就绪状态,而linux操作系统将这两种状态合并了,这种合并设计的核心原因是简化调度器实现并提高效率:
1、调度器的工作逻辑:Linux 调度器的核心任务是从 “可运行” 的进程中选择一个执行。对调度器而言,“正在运行” 和 “就绪等待” 的进程本质上属于同一类 —— 它们都具备运行条件,唯一的区别是是否正在使用 CPU。合并为TASK_RUNNING状态可以减少状态判断的复杂度。
2、减少状态切换开销:如果区分 “就绪” 和 “运行” 状态,进程每次被调度器选中或暂停时都需要切换状态(如从就绪→运行、运行→就绪),这会增加内核的操作开销。合并后,仅需通过 “是否在运行队列中” 即可区分,无需修改state字段。
3、与调度队列的配合:Linux 的运行队列(runqueue)本身就承担了 “就绪队列” 的功能 —— 所有TASK_RUNNING状态的进程都在队列中等待,调度器只需从队列中选取下一个进程即可,无需额外维护 “就绪” 状态的标识。
以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~