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

Linux操作系统之进程(三):进程优先级与进程切换调度

目录

前言

什么是进程优先级

进程优先级的查看

优先级的修改

 进程的切换

​​1. 为什么需要进程切换?​​

2、进程切换的过程

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

补充知识

总结:


前言

在Linux操作系统的进程管理体系中,进程优先级是调度机制的核心要素。本篇文章,将会把进程切换调度过程与进程优先级结合起来给大家讲解。理解本篇文章内容需要有一定的Linux基础,欢迎大家订阅我的Linux专栏,阅读以往文章。(Linux专栏)、


什么是进程优先级

进程优先级本质就是:获得某种资源的先后顺序。比如,排队的本质是在确认优先级。

cpu资源分配的先后顺序,就是指进程的优先级(priority)。
优先级⾼的进程有优先执⾏权利。配置进程优先权对多任务环境的linux很有⽤,可以改善系统性
能。
还可以把进程运⾏到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以⼤ 改善
系统整体性能。
为什么会有进程优先级这个东西呢?
大家想一下,我们进程的运行离不开CPU等硬件设施,但是硬件就这么多,但我们的进程可以是无穷无尽,说明了目标资源是永久稀缺的,需求永远多样。

目标资源比较少,就会有竞争,进而有优先级。

大家都知道,Linux系统下,一切皆文件。而对于每个文件,都有对应的权限来限制。权限来决定用户有没有资格使用,而优先级是你已经有了资格,但执行的先后顺序不同。


进程优先级的查看

和进程的属性,进程状态一样,在Task_struct-结构体中,操作系统用了特定的几个int类型的变量表示优先级。

那我们我们应该如何查看一个进程的优先级呢?

请试着在终端输入以下指令:
 

ps -al

你会看到以下格式的信息矩阵:
 

UID表示当前进程所属的用户ID,我们可以通过这个知道是哪个用户启动的对应的进程:

  1. 文件会记录下拥有者,所属组与对应权限
  2. 在linux下一切皆文件
  3. 我们的所有操作,都是进程操作,进程自己会记录是谁启动的我

以上三点,实现了权限的控制。 

我们可以看到,有两栏的属性代表是PRI与NI:

这两个属性代表着什么呢?

PRI的全称是priority,就是优先级的意思,NI的全称是NICE,是优先级的修正值。这个修正值是由用户来决定的。一个进程的最终优先级为:PRI(老优先级)+NI(修正值),比如老优先级为80,我们更改NI为9后,PRI会呈现89=80+9(老优先级+NI)。

一个进程的PRI越低,代表着这个进程的优先级越高,一般来说,我们自己写的代码,运行的程序都是普通进程,优先级都是80左右。

来验证一下以上的说法:

我们有以下文件:
 

Makefile:

# 定义编译器和编译选项
CXX = g++
CXXFLAGS = -Wall -std=c++11# 定义目标文件和可执行文件名
TARGET = process
SRC = process.cpp# 默认目标
all: $(TARGET)# 直接生成可执行文件(不生成.o文件)
$(TARGET): $(SRC)$(CXX) $(CXXFLAGS) -o $@ $<# 清理生成的文件
clean:rm -f $(TARGET)# 运行程序
run: $(TARGET)./$(TARGET).PHONY: all clean run

一个简单的循环代码:

process.cpp:

#include<stdio.h>
#include<unistd.h>int main()
{int cnt=0;while(1){printf("hello cnt : %d , my pid : %d \n",cnt,getpid());}return 0;
}

使用make指令生成可执行文件process,运行;

随后在另外一个终端中输入ps -al指令,有以下结果:

 

可以看到,我们创建的这个进程的PRI为80


 

优先级的修改

想要修改优先级,我们有以下两种的方法:
1、通过指令:在终端输入nice renice top等指令可以通过对NI的数值进行调整来修改优先级(top->r->按照提醒对应输入值(有时候会出现OS不允许频繁修改或者没有权限修改的情况)),或者通过使用 chrt 命令来设置实时优先级。当然,chrt命令一般只有root超级管理员用户才能使用,普通用户是没有权限使用的。

2、第二种就是在代码里调用系统接口来进行优先级的设置了

系统调用​​:

  • nice():调整当前进程的NI值。
  • setpriority():修改指定进程的优先级。
  • sched_setscheduler():设置实时调度策略。
#include <unistd.h>
#include <sys/resource.h>// 方法1:调整NI值
nice(-10);  // 提高优先级(需root权限)// 方法2:通过setpriority
setpriority(PRIO_PROCESS, 0, -10);  // 当前进程,NI=-10// 方法3:设为实时进程(需root)
struct sched_param param;
param.sched_priority = 90;  // 实时优先级(1~99)
sched_setscheduler(0, SCHED_FIFO, &param);

 我们这里主要用top命令来给大家展示一下修改优先级:

重新执行./process:
 

随后我们新建一个终端,输入ps -al指令:

 我们可以看到,进程process的PRI为80,NI为0.

随后我们继续输入top指令,终端界面变为:

 

我们在键盘上点击r,界面会在中间新增一行: 

PID to renice:我们需要重新设置修正值进程的PID,那么我们输入对应进程PID,并按下回车

  随后又变为:

 需要设定的NICE值,由于我们不知道具体的NICE范围,所以我们随便输入一个值:100.回车,于是回到刚top的界面,按下ctrl c退出top

重新输入ps -al指令查看PRI与NI: 

我们可以发现我们的process进程NI已经变为了19,但我们输入的是100,这说明NI的最大值就为19,大家也可以看见,我们NI为19的时候,PRI值也变成了99,但是原来的PRI的值是80,这是因为我们之前说过: 一个进程的最终优先级为:PRI(老优先级)+NI(修正值)

process一开始的PRI为80,这是他的老优先级,我们NI输入为100,但最大就是19,所以默认就把NI改为了19,最后得到的新PRI为=80+19=99.所以我们一般更喜欢把这个优先级的修改叫做重置

同理我们输入一个比较小的负数-100,可以得到NI最小为-20。

也就是说,NI修正值的取值范围在[-20,19]间,一共有四十个取值。

为什么对NI限定了范围呢?

这是我们通常使用的是分时操作系统,在进程调度时追求的是尽量公平,如果可以随意调整NI,倘若我们修改的NI值太极端,这样会导致进程调度不公平,违背了我们的追求。


 进程的切换

进程切换(也称为 ​​上下文切换,Context Switching​​)是操作系统在多任务环境下,将CPU从一个正在运行的进程转移到另一个进程的过程。它类似于​​舞台上的演员换场​​——当前演员(进程)暂停表演,舞台(CPU)交给下一个演员,同时记录当前演员的状态,以便后续恢复。

​1. 为什么需要进程切换?​

  • ​CPU资源有限​​:单核CPU同一时间只能运行一个进程。
  • ​多任务需求​​:用户希望同时运行多个程序(如边听音乐边写代码)。
  • ​公平性​​:防止某个进程独占CPU,导致其他进程“饿死”。
  • ​响应性​​:高优先级进程(如用户交互)需要及时抢占CPU。

2、进程切换的过程

先给定以下概念:

  1. 时间片,一个进程的时间片到了,进程就要被切换
  2. Linux是基于时间片进行调度轮转的
  3. 一个进程在时间片到了的时候,并不一定代表这个进程跑完了,它之后可以在任何地方被重新调度切换
  4. 如大一入伍参军一年,若入伍前没通知学校,学校未为你保留学籍,你就去入伍了,是会被退学的(学校不知道你去干什么了)。进程也是如此,在被切换前需要保存历史运行痕迹,为了重新被切换时恢复学籍
  5. 时间片到了,需要保留上下文数据,重新调度时,恢复上下文数据

这里需要重要理解一下上下文数据。 

再来理解一下进程切换的过程:


a、你的进程在运行的过程中,会有很多的临时数据,都在CPU的寄存器中保存:比如ebx,ecx,ecs,eds,cr0,ebp等寄存器。

b、CPU内部的寄存器的数据,是进程执行时的顺时状态信息数据(我们把这种数据,叫做CPU的上下文数据)

c、CPU中有很多个寄存器,整体我们称为一套寄存器。而寄存器,不等于寄存器里的数据(寄存器是被多个进程共享使用的)

进程切换的核心,就在于进程上下文数据的保存与恢复:时间片到了,需要进行切走,等待应该重新运行进程了,倘若你没有恢复上下文数据,你就只能重新运行该进程,而不是接着上次运行的代码运行,所以需要进行切回操作。


切走:将相关寄存器的内容,保存起来

切回:将历史保存的寄存器的数据,恢复到寄存器中

我们知道,临时数据是存储在寄存器,那么我们执行切走操作,那么相关寄存器的数据应该存放到哪里呢??

答案就是在内存,进程的PCB中。


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

讲完进程切换,接下来就是调度的理解。

在Linux系统中,​​进程优先级​​、​​进程切换​​和​​调度​​三者紧密关联,共同决定了CPU资源的分配方式。它们的关系可以用一个简单的比喻来理解:

  • ​进程优先级​​ → ​​VIP等级​​(决定谁更重要)
  • ​进程切换​​ → ​​换人上台表演​​(保存当前状态,切换到下一个)
  • ​调度​​ → ​​导演安排演出顺序​​(决定谁先上台)

调度是一个过程,这过程里随时会发生进程切换,进程切换的之后的下一个进程的决定规矩是由进程优先级所确定的。

在Linux2.6中有一个调度队列的代码结构:

我们主要只需要看这几个关键的代码就行:

 

对于优先级的存储,相同优先级的进程PCB会被连接到一起(链表的形式,这里类似于哈希表的开散列的形式,也就是链地址法,我在哈希表时有讲解:(链接))。这里面有两个queue[140数组。queue[140]前100个位置不用考虑,存储的是实时进程,而后40个正好对应我们用户所能修改到的真实的进程优先级——60-99: 

 

对于这两个queue数组,以及上面的*active与*espired指针,真实的结构是类似这样的:

我们有一个存储struct queue类型的数组array来存储两个queue数组,而这个array数组又是属于runqueue这个运行队列结构体。

array数组大小为2,正好对应了两个指针active与espired,所以每个指针都会分别指向一个array的元素,也就是一个queue数组:

而nractive代表该queue数组内的活跃的进程的数量,我们这里有个大小为5个int的数组bit_map。

这个充当是是一个位图的角色。

我们每个queue数组有140个位置,每个位置代表一个优先级,都可以有多个进程 。我们active指针所指向的queue数组里的进程,就是应该即将被执行的进程,当该进程的时间片耗尽,如果这个进程还未执行完毕,就会把这个进程连入到espired指针所指向的queue数组的相应位置。如果此时新增进程,也会被先连入espired所指的queue数组。

当我们的active所指向的queue中不在有进程了,我们只需要执行:

swap(&active,&espired);

就能实现这个过程的循环往复。 

这里面会涉及到我们对于该执行进程的查找。那我们如何去寻找一个进程呢?

总不能一个一个的遍历吧?

此时就应该发挥bit_map的作用了,我们知道,一个int类型是4字节,32个比特位。那么5个int的数组就是160个比特位正好大于了queue的总大小140.

//充当位图

0000 0000 0000 0000 0000 ............. 0000 0000

对应比特位为1时,代表有进程。所以我们只需要进行for循环:

for(int i=0 ;i<5;++i)
{if(bit_map[i]==0){continue;}else    {//再从32比特位中寻找哪里有进程}
}

 这样就大大简化了查找进程的时间复杂度。


总结一下,

1、CPU调度只会从active所指向队列中的进程选择

2、调度有三种情况:
        a、运行退出了

        b、不退出,但是时间片到了:放入到expired队列,而不能被放到active

        c、有新的进程产生了:放入到expired队列,而不能被放到active

所以active队列中的进程一定会越来越少,当active为空,只需要swap(&active,&expired)。以上的结构被称为O(1)调度算法。

补充知识


1、所有的进程都要用链表的形式链接

2、进程可以在调度队列中,也可以在阻塞队列中

3、Linux的链式结构一定是双链表结构

我们之前一会说进程在这里,一会又说进程在那里。进程怎么可能同时存在在这么多地方呢?这就是靠了一个结构,进程的链式结构双链表也是基于这个形成的:

我们定义了一个专门的node结构体(实际上叫做list_head) ,来实现各种连接,而不是直接把next与prev指针定义在task_struct结构体里。(因为这样会增加维护的成本)

用这个node的意义是什么呢?

在task_struct中,我们可以有多个xxxnode的链表结构,通过这些不同的结构,我们可以将各种规则下的PCB链接起来形成不同的链表,如:

struct task_struct 
{// ... 其他字段(pid, state, priority等)struct list_head tasks;    // 链接到全局进程链表struct list_head children; // 链接到子进程链表struct list_head sibling; // 链接到兄弟进程链表// ... 可能还有其他链表
};

我们只需要知道一个链表,就可以找到该PCB的所有属性。

有人会感到疑问,你就知道一个tasks,你怎么找到其他属性,比如pid,children呢?

这是C语言的知识。

比如我们有一个结构体A:

struct A
{int a;char b;double c;float d;
};

他在内存的存储是由低到高的,比如我们构建一个A对象,打印各属性的地址,就可以发现a的地址等于整个对象的地址,小于b地址,小于c地址,小于d地址。

倘若我们只知道这个对象的c属性的地址,然后把这个地址减去c到a地址,也就是A对象的地址的大小,就可以得到a的地址,也就是整个对象地址,随后强制类型转化为一个A结构体,就可以访问任意成员元素了。

那么偏移量怎么获得呢?

我们把0地址转换为A类型,访问c成员的地址,就是偏移量:

 

如果觉得以上操作麻烦,并且经常用,可以定义一个宏。

C语言中专门有个关键字offset就是这样的功能:

 

总结:

本文紧跟上文进程状态的内容,对进程的优先级,进程的切换,以及操作系统对进程的调度过程进行了讲解。随后在补充部分,为大家解释了一下PCB在操作系统内核中的双链表实现以及对C语言offset功能的回顾。

文章写的可能比较乱,但都是我的第一时间的思路。如果大家有任何不懂得地方,或者我说错的地方。欢迎在评论区指正讨论!

谢谢!

http://www.xdnf.cn/news/635977.html

相关文章:

  • npm幻影依赖问题
  • npm修改镜像的教程,将npm镜像修改为国内地址增加下载速度
  • SpringBoot-11-基于注解和XML方式的SpringBoot应用场景对比
  • 【微服务】SpringBoot 对接飞书审批流程使用详解
  • [Excel VBA]如何製作買三送一優惠條件的POS結帳介面?
  • 论文阅读笔记——Janus,Janus Pro
  • java高级 -Junit单元测试
  • JVM八股速查
  • MySQL的索引
  • leetcode2081. k 镜像数字的和-hard
  • 1.altium designer16 软件安装
  • 【短距离通信】【WiFi】WiFi7关键技术之4096-QAM、MRU
  • 深度图数据增强-形态学腐蚀操作
  • 历年哈尔滨工业大学(深圳)保研上机真题
  • MySQL-----表的操作
  • 【b站计算机拓荒者】【2025】微信小程序开发教程 - chapter3 项目实践 -1 项目功能描述
  • 代码随想录第43天:图论4(最小生成树、拓扑排序)
  • python学习打卡day36
  • 【node.js】node.js 安装详细步骤教程【安装在D盘】
  • Vite 构建原理 的深度解析
  • Vue3 + TypeScript + el-input 实现人民币金额的输入和显示
  • react 脚手架
  • mysql数据库之备份
  • 前端的core-js是什么?有什么作用?
  • 基于javaweb的SpringBoot体检管理系统设计与实现(源码+文档+部署讲解)
  • #RabbitMQ# 消息队列入门
  • 嵌入式预处理链接脚本lds和map文件
  • ​​IIS文件上传漏洞绕过:深入解析与高效防御​
  • MySQL索引失效的12种场景及解决方案
  • 深入理解 Linux 的 set、env 和 printenv 命令