☰
【Linux进程篇】Linux学习第十六弹——进程凭什么先跑?凭什么被换下?一文吃透优先级与进程切换!
2026/10/14 21:19:54 网站建设 项目流程


🎬 个人主页:MSTcheng · CSDN
🌱代码仓库:MSTcheng · Gitee

🔥 精选专栏:
《C语言》《数据结构》 《算法学习》
《C++由浅入深》
《Linux操作系统》
《Mysql数据库》 《白话大模型》
💬座右铭:路虽远行则将至,事虽难做则必成!

上一篇文章中我们给大家介绍了笼统的Linux操作系统的状态,和具体的Linux操作系统的状态。学完了Linux进程状态,本篇文章就来谈谈进程优先级。

文章目录

  • 一、进程优先级(Priority)
    • 1.1 优先级是什么? —— 对比"权限"
    • 2.2 为什么要有优先级?
    • 1.3 Linux 下优先级怎么设计(PRI 与 NI)
    • 1.4 Linux 优先级的本质
    • 1.5 如何修改 nice 值(改优先级)
    • 1.6 优先级的范围,以及为什么是有限的?
    • 1.7 为什么变化范围是有限的?(关键"为什么")
  • 二、分时系统 vs 实时系统(了解)
    • 2.1 分时系统(Time-Sharing)
    • 2.2 实时系统(Real-Time)
    • 2.3 Linux 同时支持两者
  • 三、四大概念:竞争、独立、并发、并行
    • 3.1 竞争性(Competition)
    • 3.2 独立性(Independence)
    • 3.3 并发(Concurrency)
    • 3.4 并行(Parallelism)
  • 四、进程切换(上下文切换)
    • 4.1 什么是上下文(Context)
    • 4.2 切换过程
    • 4.4 上下文保存到哪里?
    • 4.5 时间片与切换
  • 五、补充:进程切换的触发时机与机制(《深入理解 Linux 内核》)建议学完中断之后再回过头来看
  • 六、总结(1~4部分)

一、进程优先级(Priority)

1.1 优先级是什么? —— 对比"权限"

重点区分这两个概念:

  • 权限(Permission)解决"能不能"的问题。
    • 例:你去坐飞机,你想进入机长的驾驶舱去参观一下,肯定不被允许——因为你是普通乘客,所以不能进入。
  • 优先级(Priority)解决"先还是后"的问题。
    • 例:食堂排队打饭(安装常理食堂应该为每一个学生都准备饭菜),你的脚慢一点就排到后面,无非吃凉一点,但总有你的饭——因为已经能了,只差先后。

定义:进程优先级 = 在"已经能得到某种资源"的前提下,得到该资源的先后顺序。。前提很关键:优先级的前提是"已经能了"。

2.2 为什么要有优先级?

  • 想象一个场景,一个学校学校有1万名学生,不可能人手一个打饭窗口,食堂的窗口注定比人的数量要少,那么就注定要排队!
  • 根本原因:资源不足(狼多肉少、僧多粥少)→必须分配资源→ 必须确认先后→ 所以要有优先级。
  • 计算机中:进程可以创建几十上百个,但CPU 物理个数只有 1~2 颗(核再多,物理 CPU 也没几个),网卡/显卡等外设更少 →资源被共享,进程之间必然竞争。(所以为了避免竞争后面会引入锁的概念)。
  • 所以:优先级 = 决定进程获得某种资源的先后顺序。
  • 调度队列的本质,就是在确认优先级。

1.3 Linux 下优先级怎么设计(PRI 与 NI)

  • 查看:ps -l(显示更详细的进程信息)。

ps常见选项再认识:

选项含义
a显示一个终端的所有进程(all)
x显示没有控制终端的进程(如后台进程)
ax前后台都显示
j显示任务相关:PID、PPID、进程组、会话等
u显示 CPU、内存等占用情况
-l显示更详细的进程信息(类似ls -l)
mstcheng@ubuntu-dev:~/117/code/Process$ps-alF SUIDPIDPPIDC PRI NI ADDR SZ WCHAN TTY TIME CMD0S1000639927601060800-692hrtime pts/2 00:00:00 processTest4R100063995563993699800-3213- pts/0 00:00:00ps
  • ps -al输出里两个关键字段:
    • UID:这个进程是谁启动的(在系统里人也是一串编号,如 1000)。这是"归责依据"——进程乱删数据,一查 UID 就找到你。
    • PRI:priority,进程优先级(可以看到它是一个数字!!!!)。
    • NI:nice,nice 值(优先级修正数,下面会详细介绍)。

1.4 Linux 优先级的本质

  • 所以优先级的本质其实就是task_struct里的整型变量(一个数字!),核心两个:PRI(priority)和NI(nice)。
  • 规则:数字越小,优先级越高(像考试名次,名次越小越好),数字越小,越先被 CPU 调度。
  • PRI 默认值是 80。(非常重要!!!)


通过上图可以得出 PRI 与 NI 的计算公式:

新优先级(PRI)=80(固定起点)+ nice值(NI)
  • nice 值是一个"修正数字",Linux 特有。
  • nice 为正数→PRI 变大→ 优先级变低;
  • nice 为负数→PRI 变小→ 优先级变高。

注意事项(非常重要):每次改优先级时,起点old永远是 80,不是"上次的 PRI"!

  • 误以为:本次 PRI=90,nice 改成 -10 → 以为新 PRI = 90-10 = 80。
  • 实际结果:PRI = 70(因为每次该nice值都从固定的 80 开始:80 + (-10) = 70)。
  • 所以"优先级 80 起算,且与上一次无关"——每次改 nice,都从 80 重新算。
  • 为什么从 80 起算:因为简单。你只用改一个数(nice)就能达到调优先级的目的,不用查旧 PRI、不用手算。

1.5 如何修改 nice 值(改优先级)

  • 修改方式有 5 种:nice命令、renice命令、top里改、系统调用(setpriority/getpriority)。除了上面那种直接renice命令修改,还有一种使用top命令。
  • top 操作流程:
    1. 运行top;
    2. 按r(renice),输入目标PID;
    3. 输入新的nice 值,回车即改。
mstcheng@ubuntu-dev:~/117/code/linux-code/process/processTest/Process$ps-alF SUIDPIDPPIDC PRI NI ADDR SZ WCHAN TTY TIME CMD0S100063992760106070-10-692hrtime pts/2 00:00:00 processTest 👆我给processTest这个进程nice值-10,可以看到该进程的pri已经变成70了!
  • 操作重要性结论:一般来说优先级几乎不自己改,由系统决定。

继续回到上图中引出的那三个问题:

1.6 优先级的范围,以及为什么是有限的?

先看范围

  • 通过实验验证:
    • 把 nice 设成100→ 实际只到19(被截断)。
    • 把 nice 设成-100→ 实际只到-20(被截断)。
  • 结论:
    • nice 值范围:-20 ~ 19,共40 个取值。
    • PRI 范围:60 ~ 99(= 80 + nice),共40 个梯度。
    • 超过范围的输入会被"夹"到边界(19 或 -20)。

为什么改优先级要 sudo / 普通用户改两次就被拒?

  • 普通用户第一次改还行,第二、三次就不让改了(Permission denied)。
  • 原因:系统禁止高频改优先级,会怀疑你是恶意程序,影响整体调度。
  • 改用root(超级用户)就能改。
  • 结论:优先级一般不许改,尤其禁止高频改。

1.7 为什么变化范围是有限的?(关键"为什么")

  • 因为我们用的是分时操作系统(Time-Sharing OS)。
  • 分时系统的核心特点:给每个 进程分配时间片,用相对公平、均衡的策略,让 所有进程 在一段时间内 都能得到 CPU;不是"谁先来谁把任务全做完"。
  • 为什么需要公平?因为用户/互联网需求:
    • 打游戏时不能等杀毒软件"全部跑完"才轮到你;
    • 你要同时直播、画图、听歌、写代码,一段时间内都要推进;
    • 买火车票、刷抖音,大家地位对等,都要被公平对待。
  • 如果允许把优先级改到极端(如 -1 万):该进程会疯狂抢占 CPU,其他进程长期得不到 CPU→ 这就是调度中的"饥饿问题(starvation)"。
  • 所以为了让优先级在可控范围内变化,Linux 用 nice 值把变化限制在40 个梯度内。

逻辑闭环:既然是分时系统 → 那么必须保证公平调度 → 优先级变化必须有限 → Linux 选择 40 个优先级(nice -20~19)。
静态优先级 vs 动态优先级:nice 值决定静态优先级,内核还会根据调度历史动态调整(现代 CFS 调度器思路)。

二、分时系统 vs 实时系统(了解)

2.1 分时系统(Time-Sharing)

  • 主流 OS(Windows、Linux、手机、笔记本、服务器)全是分时系统。
  • 特点:同时可跑多个任务,大家都没跑完但都在跑;“我可以慢一点,但每个人都要跑”。

2.2 实时系统(Real-Time)

  • 与分时相对。“实时系统比分时系统更简单,是死脑筋”:任务来了必须立刻执行、且必须执行完才能换下一个(可插队、有优先级,但插进来的必须做完)。
  • 典型场景:工业控制、单片机(如 FreeRTOS、RT-Thread/µC/OS 这类几千行的内核)。
  • 例子:机械臂给汽车喷漆——不能喷一半去装别的东西,必须把这道工序做完。
  • 例子(自动驾驶汽车):你踩一脚刹车,不是直接到刹车片,而是先到车载系统(行车电脑),由 OS 判断后制动。
    • 刹车这个动作:① 优先级必须拉满;② 必须做完,不能做一半。
    • 所以车载系统需要实时功能(否则"听歌优先于刹车"会出大事)。

2.3 Linux 同时支持两者

  • Linux 内核既有分时调度,又有实时调度。
  • 通过条件编译 / 裁剪内核,可以选择保留或裁掉实时/分时功能。
  • 我们平时用的 Linux 被编译成分时系统(用于互联网应用)

三、四大概念:竞争、独立、并发、并行

这四个概念承上启下:既总结前面的状态/优先级,又给"切换与调度"做铺垫。

3.1 竞争性(Competition)

  • 本质原因:资源不足(CPU、内存、外设永远不足),进程数量永远比资源数量多 → 进程之间必然竞争。
  • 正因为存在竞争,才需要优先级来确认谁先谁后。

3.2 独立性(Independence)

  • 日常生活中的现象:有时候抖音闪退不会让微信跟着退;杀毒软件闪退不会让浏览器闪退。
  • 原因:每个进程有自己独立的task_struct(内核数据结构)+ 独立的代码和数据。
    • 内核数据结构独立:各有各的 PCB,PID 不同。
    • 代码和数据独立:你占你内存,我占我内存,互不影响。
  • 父子进程也具有独立性(父子是两个进程):
    • 各有独立的task_struct、独立 PID。
    • 代码共享但只读(大家都不改,所以不影响)。
    • 数据一旦被改,发生“写时拷贝”(Copy-On-Write)——这正是为了维护父子间的独立性,防止一个进程改数据影响到另一个。(写时拷贝在后面讲进程控制 的篇目会详细说明)
    • 这也是fork返回值在父子里 “数值相同但内容不同” 的原因。

3.3 并发(Concurrency)

  • 定义:多个进程在单 CPU 下,通过"快速切换"的方式,在一段时间内都得以推进。
  • 这就好像以前老师叫我们背书:每人只给有限时间背书,背不完就下去,轮一圈再继续——这样每个进程都被"推进"了一点。
  • 时间片计算示例:
    • 10 个进程,每个时间片 10ms →轮一圈 = 100ms→1 秒能轮 10 圈→ 每个进程 1 秒被调度10 次。
  • 我们在日常使用app时感受到很"丝滑",但其实底层是一段一段的"卡顿":因为人眼约 24 帧/秒就看不出断续;时间片切换时,系统里没有一个进程在跑(因为正在切换/选下一个),只是这个过程非常的快人感受不到。(这也有点像静态数码管的显示,硬件层面其实一直在闪,但是人眼看到却是静止了,因为频率太快了人眼感受不到!)

注意:

  • 并发 ≠ 并行。并发是"交替执行"(宏观同时、微观交替),并行是"真正同时"。
  • 单 CPU 只能并发;多 CPU 才可能并行。

3.4 并行(Parallelism)

  • 定义:真正的同时运行。多 CPU(多核/多物理 CPU)下,各 CPU 各有调度队列,任一"时刻"都有多个进程同时在跑。
  • 并发看的是"一段时间"内都推进;并行看的是"某一时刻"都在跑。
  • 现实:笔记本/手机大多是单 CPU 多核,多物理 CPU 功耗高(手机续航受限),所以并行场景较少。

并发 vs 并行一句话:

  • 并发:一段时间内,大家交替着推进(宏观同时)。
  • 并行:某一时刻,大家一起真同时跑。

四、进程切换(上下文切换)

4.1 什么是上下文(Context)

  • 进程在 CPU 上运行时,各种寄存器里都保存着当前运行的临时数据(函数返回值、PC 指针、通用寄存器、状态寄存器、各种标志位……)。
  • 这些"运行期间的寄存器临时数据"就叫当前进程的上下文数据(硬件上下文)。

4.2 切换过程

  • 单 CPU 最典型(也最复杂,切换概率最高):
    1. 进程A 时间片到了→把 A 的所有寄存器数据保存起来;
    2. 从运行队列重新选进程 B,恢复 B 的寄存器数据;
    3. B 从上次停下的地方继续运行(不会从头再来)。
  • 例:拷贝 1G 数据,拷了 500M 被切走,下次回来应接着拷后 500M,不是从头开始。

核心结论:

寄存器这个 “硬件” 是共享的,但寄存器里面的 “数据” 是进程私有的

  • 寄存器(硬件)只有一套/一份 →共享;
  • 寄存器里的数据→ 每个进程一套 →私有(走的时候带走,回来的时候恢复)。
  • 类比变量的左值/右值:
    • 寄存器 = 变量空间(左值,共享);
    • 寄存器内容 = 变量内容(右值,私有)。
      -"把寄存器保存"这句话是错的——应该是把寄存器里的数据保存(寄存器是硬件,你拿袋子装不走它)。

4.4 上下文保存到哪里?

  • 新式内核(2.6 等):严格说,新式内核并不是“再拿一个指针指向别处”(老式内核),而是把进程切换要用的硬件上下文,从 TSS 挪到了task_struct的thread成员里。这个成员的类型叫struct thread_struct(线程结构体),ip、sp 等寄存器上下文都保存在它里面。
  • 简化理解:进程把自己的上下文保存在自己的task_struct(PCB)里(老内核如 Linux 0.11 就是task_struct里含一个tss_struct,里面存eip/esp/ebp及各种段寄存器等)。
  • 从硬件角度:从 CPU(寄存器)保存到内存(task_struct);下次调度时再从内存恢复到 CPU。

本篇文章中对于上下文的保存以及上下文恢复如何找到恢复前的位置有详细的说明,想了解的可以去看这篇文章👉【Linux进程篇】Linux学习第十三弹——程序放进内存就是进程吗?先把“进程”理解透

4.5 时间片与切换

  • 分时系统每个进程都有合适的时间片,时间片到了进程就从 CPU 上被剥离。
  • “凭什么剥离?怎么剥离?” → 跟中断(interrupt)有关,后面讲信号时再说。

五、补充:进程切换的触发时机与机制(《深入理解 Linux 内核》)建议学完中断之后再回过头来看

① 什么情况会触发进程切换?(三类时机)

类别场景内核动作
主动让出进程阻塞(等 IO、等锁、sleep)、exit()、sched_yield()主动调用schedule()
时间片耗尽(被动)每个时钟节拍到来 →scheduler_tick()递减当前进程的time_slice,减到 0置TIF_NEED_RESCHED标志,表示"该换人了"
被唤醒进程抢占(被动)try_to_wake_up()把睡眠/停止的进程置为TASK_RUNNING,并插入本地 CPU 的运行队列被唤醒进程可能抢占本地 CPU 上正在运行的进程;唤醒接口带sync标志,可禁止这种抢占

② 关键机制:“想换"≠"马上换”——先打标志,等"调度点"才切换

  • 内核发现该换进程时(时间片耗尽、被抢占),不会当场切换,而是先给当前进程打上TIF_NEED_RESCHED标志。
  • ⭐⭐⭐书里原话:“就像第四章’从中断和异常返回’一节中所描述的,该标志强制调用schedule()函数”。也就是说:真正切换发生在从中断或异常返回的路上——内核在那里检查到标志,才强制调用schedule(),选出新进程并完成上下文切换。
  • 这正好填上 9.5 的坑:时钟中断里"打标记",中断返回时"换人"——这就"跟中断(interrupt)有关"。

💡高频面试点:“时间片用完,进程会立刻被切换吗?”
不会。只是置了TIF_NEED_RESCHED标志,等内核走到调度点(中断/异常返回、系统调用返回等)才真正切换。

⚠️注意版本:书基于 2.6 的 O(1) 调度器。注意时间片耗尽后并不是一律进过期队列——交互式进程会被放回活动队列,只有非交互式(或过期队列饿太久的)才进过期队列;但无论最后去哪,TIF_NEED_RESCHED标志都会先打上——"打标志"和"进哪个队列"是两件事(完整流程和时序见 9.7③)。现代内核用 CFS,但"耗尽 → 打标志 → 调度点完成切换"这条主轴不变。

③ 实时进程什么时候被换下?FIFO vs RR (对比)

  • SCHED_FIFO:时钟节拍里scheduler_tick()什么都不做——FIFO 进程不会被优先级更低或相等的进程抢占,所以只有两种情况会被换下:主动让出、或被更高优先级的进程抢占。
  • SCHED_RR:时间片用完 → 重填时间片 + 被放到同优先级活动队列的尾部+ 置TIF_NEED_RESCHED→ 随后被同优先级的下一个实时进程取代(如果有的话),这就是"时间片轮转"。
  • 一句话:"什么时候被换下"正是 FIFO 与 RR 的本质区别——FIFO 不轮转,RR 在同优先级内按时间片轮转。

六、总结(1~4部分)

  • 优先级:资源不足 → 进程竞争 → 用优先级决定先后;本质是task_struct里的数字(PRI = 80 + NI),数字越小越先被调度,范围有限(40 个梯度)。
  • 分时 vs 实时:分时公平分配时间片、大家交替推进;实时必须立刻做完才换下一个;Linux 两者都支持。
  • 四大概念:竞争(资源不足必然竞争)、独立(各进程有独立 PCB 与代码数据,父子靠写时拷贝维护独立性)、并发(单 CPU 交替推进)、并行(多 CPU 真正同时跑)。
  • 进程切换:时间片到 → 保存 A 的寄存器数据 → 恢复 B 的寄存器数据 → B 从上次停下的地方继续;上下文保存在各自的task_struct里。

进程切换,是在你脑子里形成一个"抢占座位"的印象——先记住"时间片到了就换人、换人就是保存/恢复寄存器数据"这个画面;等到后面学了调度,脑子里的进程才能真正跑起来!本篇文章的内容就分享到这里,下一篇文章再见!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询