系统核心解析:深入操作系统内部机制——进程管理与控制指南(三)【进程优先级/切换/调度】
♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥
♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥
♥♥♥我们一起努力成为更好的自己~♥♥♥
♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥
♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥
✨✨✨✨✨✨个人主页✨✨✨✨✨✨
前面我们已经学习了进程的前置知识,今天这一篇博客我们继续来学习进程,准备好了吗~我们发车去探索进程的奥秘啦~🚗🚗🚗🚗🚗🚗
目录
进程优先级😜
基本概念😘
Linux 中的进程优先级😍
PRI 和 NI😜
优先级计算公式😄
查看进程优先级😀
调整进程优先级的方法😋
使用 nice 和 renice 命令🙃
使用 top 命令动态调整😁
使用系统调用 setpriority 和 getpriority👍
总结与对比🐷
进程的竞争性、独立性、并行与并发😊
进程切换(Context Switching)😀
什么是进程切换?😊
为什么会进程切换?❤
进程切换步骤😭
Linux O(1) 调度器😍
理论核心架构😜
调度过程 😜
队列交换 🙃
为什么叫O(1)算法?👌
总结🐷
进程优先级😜
基本概念😘
1. 什么是进程优先级?
进程优先级(Priority)是操作系统调度器用来决定哪个进程先获得 CPU 资源的一种机制,优先级高的进程会优先被调度执行。注意优先级与“权限”不同:优先级决定的是“谁先谁后”,而权限决定的是“能否访问”。
2. 为什么需要进程优先级?
资源有限性:CPU 资源有限,而进程数量众多,需要通过优先级合理分配资源。
系统性能优化:通过调整进程优先级,可以确保关键任务优先执行,提升系统整体性能。
Linux 中的进程优先级😍
PRI 和 NI😜
PRI(Priority):进程的实际优先级,值越小优先级越高。
NI(Nice):用于调整 PRI 的修正值,用户可通过修改 NI 来间接影响 PRI。
优先级计算公式😄
PRInew=PRIold+nice
nice 的取值范围为 -20 到 19,共 40 个级别【Linux 是一个分时操作系统,这一个范围也保证了一定的公平公正】,过度调高某个进程的优先级可能导致其他进程“饥饿”【其他进程无法得到调度和CPU资源】。
PRI 的范围一般为 60 到 99(Linux 默认范围),Nice 值的 40 个级别(-20 到 19)正好一对一地映射到优先级(PRI)的 40 个级别(60 到 99)上。
NI
是用户可见的、可调整的“输入”,而PRI
是内核内部实际使用的“输出”值。
查看进程优先级😀
使用 ps -l 或 ps -al 命令
我们可以发现【ps -l】和【ps -al】查询到的进程是两种不同的结果~这也就是它们的区别所在,我们来看看下面的对比表格~
特性 | ps -l | ps -al |
---|---|---|
显示范围 | 仅当前终端 | 所有终端 |
用户范围 | 仅当前用户 | 所有用户 |
进程数量 | 较少 | 较多 |
终端显示 | 仅当前终端 (pts/0) | 多个终端 (pts/0, pts/1) |
除此之外,我们还可以发现进程的 PRI
都为 80,NI
都为 0——这正是 Linux 进程调度默认行为的体现~
在大多数 Linux 系统中,80 是用户进程启动时的默认基准优先级。这个值(具体数值可能因内核版本和发行版略有不同,但 80 非常常见)被设计为调度队列中的一个中间值。这意味着新创建的进程不会天然地拥有很高或很低的优先级,它们在竞争 CPU 时间时处于一个相对公平的起跑线上。
0 是默认的 Nice 值。Nice 值是用户或系统用来对默认优先级进行微调的修正值。一个为 0 的 NI 值表示“不做任何调整”,所以进程的最终优先级 PRI
就等于默认的基准优先级 80 + 0 = 80
。
调整进程优先级的方法😋
使用 nice
和 renice
命令🙃
nice 用于启动一个进程并指定其 nice 值,renice 用于修改已运行进程的 nice 值~
非特权用户只能调低优先级(提高 nice 值),不能调高(降低 nice 值)~
# 启动一个进程(code)并设置 nice 值为 10
nice -n 10 ./code# 修改已运行进程(PID=4340)的 nice 值为 5
renice -n 15 -p 4340
注意事项:普通用户只能降低优先级(增加 NI 值),只有 root 用户才能提高优先级(设置负的 NI 值)。
nice
用于启动新进程时设置,renice
用于修改已运行进程的优先级。
使用 top
命令动态调整😁
命令行输入top;
按
r
;输入进程 PID;
输入新的 nice 值。
按q退出
注意事项:这是交互式实时调整,同样受权限限制(普通用户不能设置负值)【与
renice
命令完全相同。在top
界面中,如果你是普通用户,尝试输入负值会得到“Operation not permitted”
的错误】修改仅对进程的当前运行实例有效,进程重启后恢复默认优先级。
使用系统调用 setpriority
和 getpriority👍
注意事项:需要在程序代码中调用,主要权限限制与命令行相同。必须包含完善的错误处理(如检查返回值),适合在应用程序中实现自适应的优先级管理。
总结与对比🐷
特性 | nice / renice 命令 | top 命令 | 系统调用 |
---|---|---|---|
使用场景 | 命令行、脚本 | 交互式实时监控与调整 | 程序内部集成 |
控制对象 | 新进程 / 已存在进程 | 已存在进程 | 自身或其他进程(编程控制) |
灵活性 | 高 | 交互式,中 | 最高,可编程逻辑控制 |
权限要求 | 普通用户只能降级 | 普通用户只能降级 | 普通用户只能降级 |
持久性 | 进程运行期间有效 | 进程运行期间有效 | 进程运行期间有效 |
主要优势 | 简单直接,易于脚本化 | 实时直观,结合系统监控 | 灵活自动化,嵌入程序逻辑 |
通用重要注意事项:
权限是核心:记住
-20
到-1
【负值】的区间是 root 的“特权区”。公平性:不要滥用高优先级,否则可能导致系统资源饥饿(Starvation),影响其他重要进程和系统整体稳定性。
非永久性:所有修改都只在进程生命周期内有效,进程重启后设置失效。
进程的竞争性、独立性、并行与并发😊
竞争性:多个进程竞争有限的 CPU 资源,优先级用于解决竞争问题。
独立性:进程之间相互隔离,一个进程的崩溃不会直接影响其他进程。
并行:多个进程在多个 CPU 核心上同时执行。
并发:多个进程在一个 CPU 核心上通过时间片轮转交替执行,宏观上间隔时间很短看似同时运行。
进程切换(Context Switching)😀
什么是进程切换?😊
进程切换是指操作系统将当前正在运行的进程挂起,并恢复另一个进程执行的过程。这个过程也叫做 CPU 上下文切换(CPU Context Switching)。
那么什么是上下文呢?前面对于进程学习,我们简单介绍了一下上下文数据~ 接下来我们来看看什么是CPU上下文?
理论:CPU上下文指的是在任务(进程/线程)执行时,CPU寄存器和程序计数器在任意时刻的状态。CPU内的寄存器只有一份,但是上下文可以有多份,分别对应不同的进程。寄存器存储着进程的临时数据、地址、状态信息,程序计数器(PC)存储着下一条要执行的指令地址。
比喻:这就像 一个厨师的工作台当前状态——哪些食材正在处理、刀放在哪、火开到多大、菜谱翻到了哪一页。这一切定义了“工作做到哪一步了”。
为什么会进程切换?❤
理论:操作系统为了实现并发(Concurrency),让多个进程在一段时间内“看起来”同时运行。当一个进程的时间片用完、或需要等待I/O操作、或有更高优先级进程就绪时,就需要切换。
比喻:餐厅经理(操作系统)为了保证公平性和效率,规定每个厨师一次只能在厨房工作一段时间(时间片)。时间到了就必须换人,让其他厨师也能用厨房,防止一个厨师霸占厨房。
进程切换步骤😭
-
保存上下文 (Save the Context):
-
理论:将当前进程的CPU寄存器状态全部保存到它的内核栈(Kernel Stack) 中。
-
比喻:厨师A被叫出厨房前,他必须迅速而准确地在自己的记事本(内核栈) 上记下所有工作状态:“洋葱切了一半,还剩3个,汤用中火炖了5分钟,盐放了2勺...”。
-
-
选择下一个进程 (Pick Next Process):
-
理论:由操作系统的调度器(Scheduler) 从就绪队列中选择一个最合适的进程来运行。
-
比喻:餐厅经理查看任务板(运行队列),根据优先级规则,决定接下来让哪位厨师(进程)进入厨房。
-
-
恢复上下文 (Restore the Context):
-
理论:将下一个要运行进程的上下文信息,从其内核栈中加载到CPU的各个寄存器中。
-
比喻:厨师B进入厨房。他并不关心上一位厨师做了什么。他拿出自己的记事本,按照上面的记录恢复工作现场:“哦,我上次鱼煎到一半,油温是七成热,下一步该翻面了...”。
-
-
跳转执行 (Jump and Execute):
-
理论:将程序计数器(PC)设置为新进程的下一条指令地址,CPU开始执行新进程的代码。
-
比喻:厨师B看了一眼记事本上记录的下一步骤,然后开始继续操作。
-
Linux O(1) 调度器😍
理论核心架构😜
运行队列 (Runqueue):
理论:Linux为每个CPU核心都维护一个
struct runqueue
(运行队列)结构。这是调度的核心数据结构。比喻:这家餐厅的每个厨房(CPU核心) 都有自己独立的任务管理区(运行队列),这样多个厨房可以同时工作,互不干扰,避免了厨师们挤在一个任务板前争吵(避免了锁竞争)。
优先级数组 (Priority Arrays) - 活动队列与过期队列:
理论:每个运行队列包含两个
prio_array_t
结构的数组:活动队列(active) 和 过期队列(expired)。
活动队列 (active):存放所有时间片尚未耗尽的就绪进程。
过期队列 (expired):存放所有时间片已经耗尽的就绪进程【新增进程/时间片到了的进程】
比喻:每个厨房的任务管理区都有两块巨大的白板:
“正在叫号”板 (Active Board):上面挂着所有还有工作时间的厨师的订单。
“等待换班”板 (Expired Board):上面挂着所有工作时间已用完的厨师的订单。
队列结构 (Queue Structure):
理论:每个
prio_array_t
包含:
queue[140]
: 一个包含140个链表的数组。每个链表(queue[i]
)都是一个FIFO队列,存放着所有优先级为i
的进程。下标即优先级,目前我们主要了解O(1)调度算法如何管理调度下标为【100-139】的普通进程,【0-99】的实时进程的管理调度后面再继续了解~
bitmap[5]
: 一个5*32=160位的位图(bitmap),每一位代表一个优先级队列是否为空(1为非空,0为空)。用于快速查找最高优先级的非空队列。比喻:
“正在叫号”白板上有140个挂钩(
queue[140]
),编号从0到139。编号越小,代表优先级越高(VIP挂钩)。每个挂钩上可以挂一串订单(进程链表),同一挂钩上的订单优先级相同,按先来后到的顺序排队。
经理手边还有一个电子指示屏(
bitmap
),上面有140个小灯,每个灯对应一个挂钩。如果某个挂钩上有订单,对应的灯就会亮起。经理一眼扫过屏幕,就能立刻知道哪个编号最小的亮灯挂钩(即最高优先级的非空队列)。
更加形象可以看看下面这张图片:
调度过程 😜
理论步骤:
调度器查看
active->bitmap
,找到第一个被设置的位(即优先级最高的非空队列)。从
active->queue[i]
中取出第一个进程。将该进程投入运行。
当该进程的时间片用完,它会被从CPU上剥离。
调度器重新计算它的时间片和新优先级(可能根据其行为交互式还是计算型进行动态调整),然后将其放入
expired->queue[k]
中(k是计算出的新优先级)。重复步骤1-5,直到
active
队列为空。比喻步骤:
经理查看电子指示屏,找到编号最小的、灯还亮着的挂钩(比如101号)。
他走到101号挂钩前,取下最前面的那个订单,叫对应的厨师(比如厨师A)进厨房工作。
厨师A在厨房工作,直到经理喊“时间到!”。
厨师A出来,经理根据他刚才的表现(是一直在切菜(CPU密集型)还是经常等送食材(I/O密集型)),重新评估他的效率,并给他分配新的下次工作时间(重置时间片)。
然后经理把厨师A的订单挂到 “等待换班”板 的对应优先级的挂钩上(比如新优先级变成了105)。
经理继续从“正在叫号”板叫下一个厨师。
队列交换 🙃
理论:当
active
队列完全为空时,调度器只需执行一个简单的指针交换操作:swap(active, expired)
。之后,原来的过期队列变成新的活动队列,而空的活动队列则成为新的过期队列。比喻:当 “正在叫号”板 上的所有订单都被处理完,变得空空如也时。经理并不慌张,他做了一件非常聪明的事:直接把“正在叫号”和“等待换班”两块白板的标签互换!瞬间,满是订单的“等待换班”板变成了新的“正在叫号”板,而空的板子则成为了新的“等待换班”板。所有等待的厨师又都有了新的工作时间。
为什么叫O(1)算法?👌
理论:无论系统中有多少进程,查找下一个要运行的进程的时间都是一个常数。因为它不遍历所有进程,而是通过查询固定大小的
bitmap
(常数时间)和操作固定数量的队列(140个)来完成。比喻:无论餐厅有多少等待的厨师,经理决定下一个叫谁的时间都是一样的。他只需要看一眼固定大小的电子指示屏,而不是从头到尾数一遍所有厨师。
总结🐷
了解了这么多,我们可以看到Linux调度器的设计之美:
-
效率:通过
bitmap
和双队列结构,将调度算法复杂度降为O(1)。 -
公平:通过时间片轮转和优先级结合,保证所有进程都能得到执行,不会出现“饥饿”现象。
-
智能:调度器会根据进程的过去行为(I/O密集还是CPU密集)动态调整其优先级,让交互式程序(如桌面点击)响应更快,体验更好。
♥♥♥本篇博客内容结束,期待与各位优秀程序员交流,有什么问题请私信♥♥♥
♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥
✨✨✨✨✨✨个人主页✨✨✨✨✨✨