当前位置: 首页 > news >正文

进程调度的艺术:从概念本质到 Linux 内核实现

前言

当我们在电脑上同时打开浏览器、编辑器和音乐播放器时,操作系统如何让这些程序 “和平共处”,既不卡顿又能响应及时?这背后的核心逻辑,正是进程调度 —— 一套决定 “谁先使用 CPU、用多久、如何切换” 的精密规则。

本文将沿着 “概念→实践→内核实现” 的脉络,拆解进程调度的核心知识:从最基础的 “孤儿进程如何被系统收养”,到 “进程优先级(PRI 与 NI)如何影响 CPU 分配”;从如何通过命令查看系统进程的 UID 和状态,到深入理解 “竞争、独立、并行、并发” 这些描述进程关系的关键概念。我们还会剖析进程切换的底层逻辑 —— 为何切换需要保存上下文?切换的成本在哪里?最终,将目光聚焦于 Linux 内核的真实调度算法,解析活动队列、过期队列以及 active/expired 指针如何协作,实现高效的进程调度。

无论你是想搞懂 “为什么调整 NI 值能让程序跑得更快”,还是想理解 “Linux 如何在千差万别的进程中分配资源”,这篇文章都将带你从表象到本质,看透进程调度的 “套路” 与 “智慧”。

目录

孤儿进程

进程优先级

概念理解

 查看系统进程

UID:user id

PRI and NI

调整优先级

补充:竞争,独立,并行,并发

进程切换

前提引入

切换理解

进程切换

 Linux真实调度算法

结构

活动队列

过期队列

active指针和expired指针


孤儿进程

父子进程关系中,如果父进程先退出,子进程要被1号init/systemd进程领养,这个被领养的进程(子进程),叫做孤儿进程。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{pid_t id = fork();if(id < 0){perror("fork");return 1;
}
else if(id == 0){//childprintf("I am child, pid : %d\n", getpid());sleep(10);
}
else{//parentprintf("I am parent, pid: %d\n", getpid());sleep(3);exit(0);
}return 0;
}

会发现子进程2140723变成孤儿进程了,被1号领养

 1号进程是谁呢?可以理解是我们的操作系统

在 Linux 系统中,1 号进程是初始化进程(init 进程),它是系统启动后创建的第一个用户态进程,是所有其他进程的 “祖先”,在进程树中处于根节点位置。

为什么要领养?

如果不被领养,就会进入僵尸状态,就会造成内存泄漏。 

孤儿进程因父进程退出而被 1 号进程收养,通常在后台运行,且 若其未自行终止,确实需要通过 kill 等命令手动终止。这一特性也体现了 1 号进程的 “管家” 职责 —— 仅负责回收资源,不主动干预孤儿进程的运行逻辑,因此终止孤儿进程的主动权仍在用户手中。

进程优先级

概念理解

是什么?为什么?

优先级是进程得到CPU资源的先后顺序。优先权高的进程有优先执行权利,配置进程优先权对多任务环境的linux很有用,可以改善系统性能。

目标资源稀缺,导致通过优先级确认谁先谁后的问题。

优先级VS权限
前者能得到资源,是先后的问题,后置是是否能得到资源。
优先级也是一种数字,int,在task_struct(PCB)里,值月底,优先级越高,反之优先级越低。
大部分系统是基于时间片的分时操作系统,考虑公平性。优先级可能变化,但是变化幅度不能太大。

 查看系统进程

在linux或者unix系统中,用ps ‒l命令则会类似输出以下几个内容:

UID:代表执行者的身份

 PID : 代表这个进程的代号 PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号

 PRI:代表进程的优先级,值越小越被执行,默认:80

NI:代表优先级的修正数据,nice值

进程真实的优先级=PRI(默认)+NI

UID:user id

 

系统怎么知道我访问文件的时候,是拥有者,所属组还是other?

进程拿着UID和文件的UID进行对比。

LInux系统中,访问任何资源,都是进程访问,进程就代表用户。 

PRI and NI

PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。
所以,调整进程优先级,在Linux下,就是调整进程nice值 .nice其取值范围是-20至19,一共40个级别。对应的Linux进程的优先级范围是60到99。

优先级设立不合理,会导致优先级低的进程,长时间得不到CPU资源,进而导致:进程饥饿。

调整优先级

用top命令更改已存在进程的nice:
1. top
2. 进入top后按“r”‒>输入进程PID‒>输入nice值
注意:
其他调整优先级的命令:nice,renice

 优先级不能随便修改,所以第二次修改我们用root,当我们将nice值设为-10时,是接着上次的90基础上进行变化吗?

显然从结果来看不是的,每次调整都是从默认值上调整的。

 补充:

nice 命令:启动进程时设置优先级

用于在启动新进程时指定其 nice 值,语法:

nice [选项] [nice值] 命令
# 以默认优先级(0)启动程序(等价于直接运行 ./test)
nice ./test# 以低优先级(nice 值 10)启动程序
nice -n 10 ./test# root 用户以高优先级(nice 值 -5)启动程序
sudo nice -n -5 ./test
  • 选项 -n 用于指定 nice 值(可省略,直接写 nice 10 ./test)。
  • 普通用户若尝试设置负的 nice 值,会被拒绝(权限不足)。

renice 命令:调整已运行进程的优先级

用于修改正在运行的进程的 nice 值,语法:

renice [nice值] -p [进程PID]
# 先查看进程 PID(假设 test 进程的 PID 是 12345)
ps -ef | grep test# 将 PID 为 12345 的进程 nice 值改为 5
renice 5 -p 12345# root 用户将进程 nice 值改为 -3(提高优先级)
sudo renice -3 -p 12345
  • 选项 -p 用于指定进程 PID(必须指定)。
  • 若要调整多个进程,可同时指定多个 PID(如 renice 10 -p 123 456)。

补充:竞争,独立,并行,并发

竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

进程切换

前提引入

死循环进程如何运行?

一旦一个进程占有CPU,会把自己的代码跑完吗?不会,除非很短。 系统会给进程分配一个时间片的东西。

对于死循环进程,不会打死系统,不会一直占用CPU。

CPU  and 寄存器

CPU里面有很多个寄存器,寄存器用于临时存放进程执行过程中的关键数据。

  • 程序计数器(PC):记录下一条要执行的指令地址(进程 “当前运行位置” 的标记)。
  • 通用寄存器:存放算术运算、逻辑操作的临时结果(如 a + b 的中间值)。
  • 状态寄存器(FLAGS):记录指令执行后的状态(如是否溢出、是否为零)。
  • 栈指针(SP):指向当前进程的栈顶位置,用于函数调用时的参数传递和返回地址存储。

这些数据是进程 “运行状态” 的直接体现 —— 离开它们,CPU 无法继续执行进程。

结论:1. 寄存器就是CPU内部的临时空间! 2. 寄存器!= 寄存器里面的数据。 

切换理解

进程切换小故事

在校大学生小胡想要去当兵,首先通知学校,得去跟辅导员宝报备,当兵一年回来继续学习,需要保留学籍,辅导员给了小胡一份档案,里面记录了小胡的信息,学校一份,给了小胡一份,当兵回来之后,需要恢复学籍,然后辅导员根据上次离开的学习信息,继续给小胡安排。

学校--CPU    导员--调度器   小胡--进程   学籍--进程运行的临时数据,CPU寄存器里面的内容(当前进程上下文数据)

保留学籍--保存进程上下文数据,CPU内寄存器里面的内容,保存起来。

恢复学籍--恢复进程上下文数据,保存起来,恢复到CPU内寄存器里

当兵--进程从CPU剥离出来

现实场景(小胡当兵)操作系统概念(进程调度)核心逻辑一致点
学校CPU是 “运行” 的核心载体(学校是学习的场所,CPU 是进程执行的硬件载体)。
辅导员调度器(Scheduler)负责 “安排谁在什么时候使用核心资源”(辅导员安排学生的学习 / 当兵时间,调度器安排进程使用 CPU)。
小胡进程(Process)是 “被管理的主体”(小胡是学校管理的学生,进程是操作系统管理的执行单元)。
学籍信息(当前课程、进度)进程上下文(CPU 寄存器数据、PC 指针、栈信息等)是 “主体当前状态的记录”(学籍记录学习进度,上下文记录进程执行到哪一步、临时数据是什么)。
保留学籍保存上下文(Context Save)暂时离开核心载体时,必须保存当前状态(小胡当兵前存档,进程被切换前保存寄存器数据到 PCB)。
恢复学籍恢复上下文(Context Restore)回到核心载体时,需加载之前的状态(小胡返校后接着上学,进程被调度时从 PCB 加载寄存器数据到 CPU)。
当兵期间进程被剥夺 CPU 使用权(进入阻塞 / 就绪态)暂时脱离核心载体,不占用资源但状态被保留(小胡不在学校,进程不在 CPU 上运行)。
返校继续学习进程被调度到 CPU 运行(进入运行态)重新获得核心载体的使用权,基于之前的状态继续推进(小胡接着上之前的课,进程接着执行之前的指令)。

相当于进程的一次切换 

进程切换

CPU上下文切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运行另外的任务时, 它保存正在运行任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务自己的堆栈中, 入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装入CPU寄存器, 并开始下一个任务的运行, 这一过程就是context switch。

进程切换,最核心的,就是保存和恢复当前进程的硬件上下文的数据,即CPU内寄存器的内容!!!

故引出新的问题,1. 所以当前进程要把自己的进程硬件上下文数据保存起来,保存到哪里?

保存到进程的task_struct里面,TSS --任务状态段

参考⼀下Linux内核0.11代码

2.  全新的进程VS已经调度过的进程

在task_struct增加一个标记位来表示进程是否调度,全新设置为0.

注意:
时间片:当代计算机都是分时操作系统,没有进程都有它合适的时间片(其实就是一个计数
器)。时间片到达,进程就被操作系统从CPU中剥离下来。

 Linux真实调度算法

调度和切换构成了调度器

结构

Linux2.6内核进程O(1)调度队列

一个CPU拥有一个runqueue
如果有多个CPU就要考虑进程个数的负载均衡问题
普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
实时优先级:0~99(不关心)

活动队列

时间片还没有结束的所有进程都按照优先级放在该队列
nr_active: 总共有多少个运行状态的进程
queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
从该结构中,选择一个最合适的进程,过程是怎么的呢?
1. 从0下表开始遍历queue[140]
2. 找到第一个非空队列,该队列必定为优先级最高的队列
3. 拿到选中队列的第一个进程,开始运行,调度完成!
4. 遍历queue[140]时间复杂度是常数!但还是太低效了!
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用
5*32个比特位表示队列是否为空,这样,便可以大提高查找效率!

过期队列

过期队列和活动队列结构一模一样
过期队列上放置的进程,都是时间片耗尽的进程
当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算

active指针和expired指针

active指针永远指向活动队列
expired指针永远指向过期队列
可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!

结束语

进程调度看似是 “分配 CPU 时间” 的简单问题,实则是操作系统对 “效率与公平” 的永恒权衡。从孤儿进程被 1 号进程收养的设计,到 PRI 与 NI 共同决定的优先级体系;从进程切换时上下文的精准保存与恢复,到 Linux 调度算法中活动队列与过期队列的巧妙轮转,每一个细节都体现了 “让系统更流畅、资源利用更高效” 的目标。

理解这些知识,不仅能帮我们在实际工作中更好地调试程序(比如通过调整优先级优化资源占用),更能让我们透过现象看本质:操作系统的每一个机制,都是对 “如何管理复杂系统” 的抽象与落地。

进程调度的故事远未结束 —— 随着多核 CPU、实时系统等场景的发展,调度算法仍在不断进化。但无论技术如何迭代,“理解进程的行为与调度规则” 始终是程序员与操作系统 “对话” 的基础。希望本文能成为你探索更深层系统知识的起点,也欢迎在评论区分享你对进程调度的思考或实践经验~

http://www.dtcms.com/a/295696.html

相关文章:

  • 从“各玩各的”到“无缝贴贴”:Modbus转Profinet让机器人告别“信息孤岛”
  • 【自动化运维神器Ansible】Ansible常用模块之shell模块详解
  • 数据版本控制系统(Oxen)
  • Terraform与Ansible的关系
  • Mysql-UDF提权
  • 家政小程序系统开发:开启智慧家政新时代
  • 详解力扣高频 SQL 50 题-1757.可回收且低脂的产品【入门】
  • 使用phpstudy极简快速安装mysql
  • LLM层归一化:γβ与均值方差的协同奥秘
  • 用 Function Call 让 AI 主动调用函数(超入门级示例)|保姆级大模型应用开发实战
  • day 34 打卡
  • LLM中典型的Transformer层中:MLP Residual; LN Agg: μ, σ; SM Agg 是什么意思
  • [202103][Docker 实战][第2版][耿苏宁][译]
  • [Linux入门] Linux 网络设置入门:从查看、测试到配置全攻略
  • 进阶系统策略
  • 二分查找----4.搜索旋转排序数组
  • 为什么Java的String不可变?
  • 洛谷P1512 伊甸园日历游戏
  • Qt(资源库和按钮组)
  • Django基础(八)———数据库外键及表关系
  • DRF - 博客列表API
  • GaussDB 数据库架构师(八) 等待事件概述-1
  • Spring Boot项目的模块继承父项目的全部依赖
  • 中国5G RedCap基站开通情况及2025年全年计划
  • 【ComfyUI学习笔记03】案例学习:图片放大的3个基本工作流
  • 基于规则架构风格对业务的重构
  • 与deepseek的问答:dot net与Borland VCL的关系
  • 抖音小游戏好做吗?
  • MySQL的底层原理--InnoDB记录存储结构
  • 【Unity开发】飞机大战项目实现总结