Linux操作系统之进程(三):进程优先级与进程切换调度
目录
前言
什么是进程优先级
进程优先级的查看
优先级的修改
进程的切换
1. 为什么需要进程切换?
2、进程切换的过程
内核进程O(1)调度队列(Linux2.6)
补充知识
总结:
前言
在Linux操作系统的进程管理体系中,进程优先级是调度机制的核心要素。本篇文章,将会把进程切换调度过程与进程优先级结合起来给大家讲解。理解本篇文章内容需要有一定的Linux基础,欢迎大家订阅我的Linux专栏,阅读以往文章。(Linux专栏)、
什么是进程优先级
进程优先级本质就是:获得某种资源的先后顺序。比如,排队的本质是在确认优先级。
目标资源比较少,就会有竞争,进而有优先级。
大家都知道,Linux系统下,一切皆文件。而对于每个文件,都有对应的权限来限制。权限来决定用户有没有资格使用,而优先级是你已经有了资格,但执行的先后顺序不同。
进程优先级的查看
和进程的属性,进程状态一样,在Task_struct-结构体中,操作系统用了特定的几个int类型的变量表示优先级。
那我们我们应该如何查看一个进程的优先级呢?
请试着在终端输入以下指令:
ps -al
你会看到以下格式的信息矩阵:
UID表示当前进程所属的用户ID,我们可以通过这个知道是哪个用户启动的对应的进程:
- 文件会记录下拥有者,所属组与对应权限
- 在linux下一切皆文件
- 我们的所有操作,都是进程操作,进程自己会记录是谁启动的我
以上三点,实现了权限的控制。
我们可以看到,有两栏的属性代表是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, ¶m);
我们这里主要用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、进程切换的过程
先给定以下概念:
- 时间片,一个进程的时间片到了,进程就要被切换
- Linux是基于时间片进行调度轮转的
- 一个进程在时间片到了的时候,并不一定代表这个进程跑完了,它之后可以在任何地方被重新调度切换
- 如大一入伍参军一年,若入伍前没通知学校,学校未为你保留学籍,你就去入伍了,是会被退学的(学校不知道你去干什么了)。进程也是如此,在被切换前需要保存历史运行痕迹,为了重新被切换时恢复学籍
- 时间片到了,需要保留上下文数据,重新调度时,恢复上下文数据
这里需要重要理解一下上下文数据。
再来理解一下进程切换的过程:
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功能的回顾。
文章写的可能比较乱,但都是我的第一时间的思路。如果大家有任何不懂得地方,或者我说错的地方。欢迎在评论区指正讨论!
谢谢!