【Linux】揭秘Linux进程优先级与调度机制
8.进程优先级以及进程调度切换
文章目录
- 8.进程优先级以及进程调度切换
- 一、进程优先级
- 介绍
- 查看进程的优先级
- PRI and NI
- 查看进程优先级的命令
- 二、进程切换
- 1. CPU 执行程序的过程
- 2. 什么是上下文、为什么要保存、保存到哪
- 3. 上下文保存和恢复的完整流程
- 4. 多进程如何交替运行(通过上下文切换)
- 5. Linux 中进程上下文保存到 PCB/TSS 的设计
- 6. 调度和切换之间的关系
- 7. O(1) 调度算法结构
- 结构
- 调度时调度运行
- 进程饥饿问题与 expired 队列的解决方案
- Linux 中如何使用双链表维护进程队列
- ==通过结构体成员访问整个进程数据(container_of 思想)==
- 三、总结
一、进程优先级
介绍
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权⾼的进程有优先执⾏权利。配置进程优先权对多任务环境的linux很有⽤,可以改善系统性能。
- 还可以把进程运⾏到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以⼤⼤改善系统整体性能。
查看进程的优先级
使用命令ps -al
可以查看当前所有进程的详细信息
[lisihan@hcss-ecs-b735 ~]$ ps -al
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 9926 8709 0 80 0 - 1054 hrtime pts/1 00:00:00 code
1 S 1000 9927 9926 0 80 0 - 1054 hrtime pts/1 00:00:00 code
0 R 1000 9929 8735 0 80 0 - 38332 - pts/2 00:00:00 ps
我们很容易注意到其中的⼏个重要信息,有下:
- UID : 代表执⾏者的⾝份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍⽣⽽来的,亦即⽗进程的代号
- PRI :代表这个进程可被执⾏的优先级,其值越⼩越早被执⾏
- NI :代表这个进程的nice值
注意:
-
UID标识的就是执行这个进程的用户,之前我们用ls命令查看文件资源的时候,显示的就是用户的名称,但其实在linux中系统还为每一位用户分配了一个数字ID号,在查看文件的时候可以用
ls -ln
来查看[lisihan@hcss-ecs-b735 lession12]$ ls -l total 20 -rwxrwxr-x 1 lisihan lisihan 9744 May 28 16:01 code -rw-rw-r-- 1 lisihan lisihan 489 May 28 16:01 code.c -rw-rw-r-- 1 lisihan lisihan 61 May 28 14:31 Makefile [lisihan@hcss-ecs-b735 lession12]$ ls -ln total 20 -rwxrwxr-x 1 1000 1000 9744 May 28 16:01 code -rw-rw-r-- 1 1000 1000 489 May 28 16:01 code.c -rw-rw-r-- 1 1000 1000 61 May 28 14:31 Makefile
-
目前运行的所有的指令编译,包括编译代码都是进程,所以进程在进行操作。既然所有进程都是进程操作,进程自己会记录谁启动的我。所以当用进程去控制与文件相关的概念时,那么就可以通过进程自己对应的UID来和文件的拥有者所属组ID做对比,就可以进行权限控制。
PRI and NI
- PRI也还是⽐较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执⾏的先后顺序,此值越⼩进程的优先级别越⾼
- 那NI呢?就是我们所要说的nice值了,其表⽰进程可被执⾏的优先级的修正数值
- PRI值越⼩越快被执⾏,那么加⼊nice值后,将会使得PRI变为:PRI(new)=PRI(default)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变⼩,即其优先级会变⾼,则其越快被执⾏
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20⾄19,⼀共40个级别。
- 需要强调⼀点的是,进程的nice值不是进程的优先级,他们不是⼀个概念,但是进程nice值会影响到进程的优先级变化。
- 可以理解nice值是进程优先级的修正修正数据
查看进程优先级的命令
⽤top命令更改已存在进程的nice:进⼊top后按“r”‒>输⼊进程PID‒>输⼊nice值
其他调整优先级的命令:nice,renice
二、进程切换
CPU上下⽂切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运⾏另外的任务时, 它保存正在运⾏任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务⾃⼰的堆栈中, ⼊栈⼯作完成后就把下⼀个将要运⾏的任务的当前状况从该任务的栈中重新装⼊CPU寄存器,并开始下⼀个任务的运⾏, 这⼀过程就是context switch。
时间⽚:当代计算机都是分时操作系统,没有进程都有它合适的时间⽚(其实就是⼀个计数器)。时间⽚到达,进程就被操作系统从CPU中剥离下来。
- 时间片,时间片到了,进程就要被切换
- Linux是基于时间片,进行调度轮转的
- 一个进程在时间片到了的时候,并不一定跑完了,可以在任何地方被重新调度切换
1. CPU 执行程序的过程
要想理解进程之间的相互切换,就先要明白CPU是如何执行程序的。
CPU 执行程序是一个循环过程:首先,程序计数器(PC,x86架构下称为 EIP)指向下一条要执行的指令。CPU 通过 PC 从内存中取指(取出当前指令到指令寄存器 IR),然后译码并执行这条指令。执行完一条指令后,CPU 将 PC 加上指令长度(或由 CPU 自动更新 PC)使其指向下一条指令地址,然后继续下一轮循环。简单来说,CPU 不停地按照如下步骤运行:
- CPU 根据寄存器中保存的 PC(指令指针)从内存中读取指令到 IR。
- 解析并执行 IR 中的指令。
- 执行结束后,PC 自动更新到下一条指令的地址(PC新=PC旧+指令长度)。
- 重复以上过程,直至程序结束或发生中断/抢占。
这种“取指–执行–更新 PC”循环确保了程序指令能够被按序执行
2. 什么是上下文、为什么要保存、保存到哪
所谓进程的“**上下文**”,就是指该进程执行时 CPU 内部寄存器的状态。因为CPU中寄存器的内容记录了当前正在执行任务的全部执行状态(包括数据寄存器、指令指针等)。例如,一个进程运行到一半时,CPU 的通用寄存器(如 eax、ebx 等)、标志寄存器 EFLAGS 以及程序计数器 EIP 等都会保存该进程的执行进度、数据等信息。这些寄存器内容即是进程的**上下文**(可理解为进程的“现场”)。
由于 CPU 只有一套硬件寄存器,但系统中可能有多个进程并发执行,所以在多进程环境下,必须为每个进程保存各自的上下文,以便切换回来时继续执行。如果不保存上下文,当一个进程被切走后再调回,CPU 只能从头开始执行,原来的执行进度将丢失。例如,如果两个进程交替运行但不保存寄存器状态,那么被切出的进程再次运行时就会“从头开始”,无法正确工作。
上下文的保存位置: Linux 将进程的上下文保存在其 PCB(进程控制块)中。在 Linux 内核中,每个进程都有一个 task_struct
结构作为其 PCB,其中包含了一个任务状态段 (struct tss_struct tss
) 成员,用来存储 CPU 寄存器的值。这个 TSS 结构里有一组变量(如 eax、ecx 等),直接对应 CPU 的寄存器,用来保存该进程运行时各寄存器的值。因此,进程的“历史记录”(上下文)就存放在对应的 task_struct
中的 TSS 字段里。
- 上下文(Context):进程运行时 CPU 寄存器的状态[
- 为什么保存:单一 CPU 多进程切换,需要保存每个进程的寄存器状态以备恢复,否则切换后进程无法继续执行
- 保存位置:Linux 将上下文保存到进程的 PCB(
task_struct
)中的tss_struct
成员
3. 上下文保存和恢复的完整流程
当发生进程切换(例如时间片耗尽或进程主动阻塞/让出 CPU)时,操作系统需要执行上下文切换。其过程大致如下:
- 调用调度器:内核调度函数(如
schedule()
)决定下一个要运行的进程,找到目标进程的 PCB。 - 保存当前上下文:接着内核通过架构相关的
switch_to
(或__switch_to
)宏来完成寄存器状态的切换。CPU 会把当前进程的所有寄存器值写入当前task_struct
的 TSS 中。也就是说,原进程的通用寄存器、EIP、标志位等都被保存下来。 - 恢复新进程上下文:然后,CPU 再加载下一个进程在其 TSS 中保存的寄存器值,更新 TR 寄存器指向新任务的 TSS 段,并将相应值写入 CPU 各寄存器。此时新进程的执行状态(寄存器、栈指针等)被恢复。
- 继续执行:完成保存与恢复后,新进程开始运行,继续从上次停止的地方执行。
简而言之,switch_to
完成了上下文切换:它保存旧进程的所有寄存器信息,恢复新进程的寄存器信息,然后开始执行新进程。例如,Linux 中的 switch_to
宏通过一条长跳转指令触发 CPU 做硬件上下文切换:CPU 会自动将当前寄存器内容写入当前 TSS,然后加载目标任务的 TSS 内容到寄存器。完成这些步骤后,当前指令流就跳转到下一个任务,切换过程结束。
4. 多进程如何交替运行(通过上下文切换)
操作系统通过时间片轮转和中断等方式让多个进程在 CPU 上交替运行。每个可运行的进程都会被放入就绪队列中排队等待CPU。当一个进程的时间片用完,或者发生高优先级进程抢占时,操作系统就会执行一次上下文切换:保存当前进程的上下文,并恢复另一个进程的上下文,从而切换到下一个进程上运行
例如,在 Linux 的 O(1) 调度器中,调度器会从活跃(active)就绪队列中挑选一个优先级最高的进程,将其作为下一个执行任务。然后,内核调用 switch_to
将当前进程切出并切入新进程,完成切换。随着时间片的流逝或者进程状态变化,内核会多次执行这样的切换,使得多个进程看起来“轮流”在 CPU 上运行。通过不断地保存和恢复各进程的上下文,操作系统保证每个进程都能被公平调度和执行这三行就是算法的核心,首先去从runqueue的active队列中的bitmap找到一个下标,这个下标就是对应的优先级,然后获取到对应优先级的链表,然后从中获取 一个next进程。后面的操作就是执行进程切换,调度了。)。
5. Linux 中进程上下文保存到 PCB/TSS 的设计
在 x86 架构上,硬件本身支持任务状态段(TSS)来保存任务寄存器状态。Linux 内核在 task_struct
中定义了一个 struct tss_struct tss
成员,用来保存进程切换时的寄存器镜像。这个 tss_struct
结构体中包含了所有需要保存的寄存器字段(如 eax、ecx 等),与 CPU 硬件寄存器一一对应。
值得注意的是,Linux 并不是为每个进程维护独立的硬件 TSS。实际上,每个 CPU 核心只使用一个 TSS,TR 寄存器指向当前 CPU 的 TSS 段。在发生任务切换时,内核更新当前任务的 task_struct.tss
中的内核栈指针(esp0)等信息,然后通过一个对新的 TSS 描述符的长跳转让 CPU 自动完成寄存器保存与恢复。在这个过程中,原进程的寄存器值被写入当前 TSS,CPU 再加载下一个进程的 TSS,从而实现了任务切换。总之,Linux 利用 task_struct
中的 TSS 存储上下文,并借助硬件机制(longjmp到TSS)在进程切换时自动完成寄存器的保存与恢复。
6. 调度和切换之间的关系
调度(scheduling)和切换(context switch)是进程管理中的两个不同层面:
- 调度 是决策过程,负责选择下一个要运行的进程。调度器根据进程优先级、时间片等策略,从就绪队列中找到最合适的进程。例如,在 O(1) 调度器中,会先查找 bitmap 找出最高优先级,再从对应优先级链表中取出一个进程作为下一个运行目标
- 切换 是执行过程,负责实际保存当前进程状态并加载新进程状态。调度器决定了要运行哪个进程后,会调用
context_switch
或switch_to
来完成上下文切换:先保存旧进程的寄存器、栈等状态,然后恢复新进程的上下文。
可以将调度和切换比作“谁上场”和“上场动作”两个步骤:调度决定下一个上场的进程(谁上场),切换负责把场上原进程的现场保存好并把新进程带上场(上场动作)。调度函数(如 schedule()
)会在适当时机(如时间片耗尽、进程主动让出)调用切换函数,完成实际的进程切换。
7. O(1) 调度算法结构
Linux 2.6 系列中引入了 O(1) 调度器,使调度时间与就绪进程数无关。其主要结构如下:
结构
-
每个 CPU 核心维护独立的 runqueue:这样在多核系统中不同 CPU 可以并行调度,避免单个就绪队列竞争。每个 runqueue 包含两个多级优先级数组:一个是 active(活跃进程) 队列组,一个是 expired(过期进程) 队列组。
-
优先级数组 (prio_array_t array):每个 array 包含 140 个优先级链表(索引 0–139,每个索引对应一个优先级)和一个对应的位图(bitmap)。位图记录了哪些优先级队列当前有可运行的进程,只需扫位图就能快速找到下一个非空优先级,确保调度操作为 O(1) 时间。
具体来说:queue中储存的是不同优先级的task_struct,同一优先级的task_struct用指针链接在一起,可以说queue中储存的是一个一个的双链表(这个后面会细说);位图中的bitmap的bit位数为32 * 5 = 160,恰好等于queue中的160个优先级。每一个bit位表示对应优先级
-
active(活跃进程) 队列:保存当前轮中可执行的进程。
-
expired(过期进程) 队列:保存已经耗尽时间片需等待下一轮的进程。当进程在 active 中运行完它的时间片后,会重新计算优先级并被放入 expired 队列,过期队列中的进程暂时不参与当前调度。
调度时调度运行
- 系统首先在 active 队列组中查找可运行进程:通过检索 active 的位图找到优先级最低(数值最小)的可用位,然后在对应链表中取出一个进程运行。
- 先通过
bitmap
找到最高优先级索引idx
,然后从对应的链表取出下一个可运行进程作为调度目标。 - 当一个进程没有运行完退出而是时间片耗尽,这个进程会从active队列转移到expired队列,如果有新的进程创建,不会直接链接active队列,而是链接在expired队列中,以保证时间分配的平衡(也就是为了解决进程饥饿的问题,后面会谈到)
- 当 active 队列中的所有进程都用完时间片或阻塞(即 active 变空)时,就会交换 active 和 expired 队列,开始新一轮调度。即原来的 expired 队列变为新的 active,重新为其中进程分配时间片,原 active 队列置为新的 expired。这样做避免了对所有进程重新遍历赋时片的开销。
- 总之,O(1) 调度器通过双数组结构、位图快速定位和 active/expired 队列轮换,实现了常数时间复杂度的调度。
进程饥饿问题与 expired 队列的解决方案
进程饥饿指的是低优先级进程长时间得不到调度的情况。
O(1) 调度器通过 active/expired 机制有效解决了这个问题:每个进程在用完自己的时间片后,不管优先级多高,都会被移动到 expired 队列。只有当 active 队列中的所有进程都耗尽时间片时,才交换 active 和 expired 队列,重新开始下一轮调度。这样一来,即使某些高优先级进程不断运行,它们终归会因为时间片用完被暂时放入 expired,给低优先级进程腾出运行机会。一旦一轮结束,active 队列与 expired 队列互换,所有进程又重新获得时间片,这就保证了低优先级进程也能周期性地运行,避免了无限饥饿。简而言之,expired 队列机制让调度器形成“多轮次”的公平轮转,即使优先级不同,所有进程最终都能得到执行。
Linux 中如何使用双链表维护进程队列
Linux 内核大量使用双向链表来管理进程队列和其他列表。
在调度器中,runqueue 结构以及每个 prio_array 的 queue[MAX_PRIO]
都是链表头数组。具体来说,每个优先级对应一个 list_head
链表,用于挂载所有具有该优先级的任务中定义了 struct list_head queue[MAX_PRIO];
每个 queue[i]
就是优先级 i
的双链表。调度时,内核在这些链表中插入或删除 task_struct
对象,将进程添加到相应优先级的链表。除此之外,runqueue 中还有诸如 migration_queue
等 list_head
链表,用于负载均衡时迁移进程等。利用双向链表,内核可以在 O(1) 时间内将进程从一个队列移动到另一个队列,并且可以灵活遍历同一优先级下的所有进程。这种链表结构使得进程管理既高效又灵活.
通过结构体成员访问整个进程数据(container_of 思想)
在 Linux 内核中,经常需要通过结构体某个成员指针来获取包含该成员的整个结构体指针。为此,内核提供了 container_of
宏,其作用就是“根据结构体成员指针计算出对应的结构体首地址”。其原理是利用成员在结构体中的偏移量:假设有 ptr
指向结构体 type
的某个成员 member
,则通过 container_of(ptr, type, member)
宏可以得到指向结构体 type
的指针。该宏定义(简化版)如下:
#define container_of(ptr, type, member) ({ \const typeof(((type *)0)->member) *__mptr = (ptr); \(type *)((char *)__mptr - offsetof(type, member)); })
- 作用:根据
member
成员的地址ptr
,计算并返回包含该成员的type
结构体地址。
例如,在调度器中 task_struct
有一个 run_list
成员(类型为 struct list_head
),它用来链接调度队列。如果有一个指向某个 run_list
节点的指针 plist
,可以通过 container_of(plist, struct task_struct, run_list)
得到包含该 run_list
的 task_struct
对象指针。也就是说:
struct task_struct *task = container_of(plist, struct task_struct, run_list);
这使得内核在遍历链表时,只需要操作链表节点(list_head
),就能够快速定位到真正的 task_struct
对象,从而访问进程的完整信息。container_of
宏是 Linux 内核操作链表和其他内嵌结构时常用的技巧,通过已知成员地址反推整个结构体的地址,便于在内核代码中简洁安全地访问包含该成员的上下文。
三、总结
本文介绍了Linux系统中的进程优先级和进程调度切换机制。主要内容包括:1) 进程优先级由PRI和NI值共同决定,可通过调整nice值(-20至19范围)来改变进程执行顺序;2) 进程切换涉及CPU上下文保存与恢复,包括寄存器状态等信息会存储在PCB的TSS结构体中;3) Linux采用时间片轮转调度,通过调度器实现多进程交替执行,每个进程被分配时间片后可能在任何位置被切换;4) 上下文切换过程包括保存当前进程状态、加载新进程状态,保证切换后能继续原执行点。这些机制共同实现了Linux的多任务并发执行能力。