Linux进程(三)概念:进程优先级与进程切换篇

发布时间:2026/11/2 6:32:42
Linux进程(三)概念:进程优先级与进程切换篇

目录

一、进程优先级

1.概念

2.PRI和NI

PRI

NI(Nice 值)

3.查看进程优先级

4.竞争和独立

4.1进程独立

4.2进程竞争

5.并行和并发

并发(Concurrency)

并行(Parallelism)

例如:单核下多进程交替调度

对比

二、进程切换

1.进程切换

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

2.1一个 CPU 拥有一个 runqueue

2.2优先级

2.3活动队列 active

2.4过期队列 expired

2.5active 指针和 expired 指针

三、总结


一、进程优先级

1.概念

进程优先级,就是操作系统调度器选择哪个进程先上 CPU 运行的权重规则。 CPU 是稀缺资源,同一时刻 CPU 核心只能跑一个进程;调度器依靠优先级决定进程的调度顺序:

优先级越高,越容易抢到 CPU 时间片,被优先调度

优先级低的进程,只有高优先级进程不运行、让出 CPU 时才有机会执行

Linux 下:数字越小 → 优先级越高

核心目的:调度器倾斜资源,保证交互进程(shell、桌面程序)响应更快;后台批量任务可以降低优先级,不抢占前台。

2.PRI和NI

PRI

PRI(Priority):内核维护的进程优先级,内核真正用来调度的数值Linux 下普通进程 PRI 范围:60 ~ 99

PRI 越小,优先级越高 PRI = 80 + NI (默认 NI=0,默认 PRI=80)

NI(Nice 值)

NI,nice 值,是用户可以手动修改的偏移量,用来调整进程的 PRI NI 取值范围:-20 ~ 19

NI = -20:最高 nice,PRI=60,优先级最高

NI = 0:默认值,PRI=80

NI = 19:最低 nice,PRI=99,优先级最低

公式:PRI(new) = PRI(old) + NI的变化

注意:用户不能直接修改 PRI!只能修改 NI,由内核根据 NI 重新计算 PRI只有 root 用户可以设置负数 NI(提高优先级);普通用户只能设置正数 NI(降低自己进程优先级)

3.查看进程优先级

4.竞争和独立

4.1进程独立

进程是操作系统资源分配的基本单位。 每个进程有独立:地址空间、页表、文件描述符表、信号掩码、资源限额。 进程之间默认内存隔离:一个进程直接读写不了另一个进程的变量,互不干扰。

独立性是进程最重要特征。

代码验证进程独立:

#include <stdio.h> #include <unistd.h> int g_val = 100; int main() { pid_t pid = fork(); if(pid == 0) { // 子进程 g_val = 200; printf("子进程:g_val = %d, &g_val=%p\n", g_val, &g_val); } else if(pid > 0) { sleep(1); // 等子进程修改 printf("父进程:g_val = %d, &g_val=%p\n", g_val, &g_val); } return 0; }

编译运行:

gcc fork_test.c -o fork_test ./fork_test

输出示例:

子进程:g_val = 200, &g_val=0x55f87216a010 父进程:g_val = 100, &g_val=0x55f87216a010

现象:地址看起来一样,但实际是虚拟地址,物理内存已经发生写时拷贝,父子进程变量互不影响。证明进程内存独立。

4.2进程竞争

多个进程,争抢同一个系统资源(CPU、磁盘 IO、带宽、锁),就是竞争关系。 CPU 竞争就是优先级发挥作用的场景:多个就绪进程排队抢 CPU 时间片。 竞争带来问题:饥饿(低优先级进程长期抢不到 CPU 一直无法运行)。 Linux CFS 调度器通过调度策略缓解饥饿,不会让低优先级进程完全得不到调度。

竞争示例:两个死循环进程抢占 CPU

#include <stdio.h> #include <unistd.h> int main() { pid_t p1 = fork(); if(p1 == 0) { while(1){} } pid_t p2 = fork(); if(p2 == 0) { while(1){} } while(1){} return 0; }

运行后 top 看,三个进程均分 CPU;如果你修改其中一个进程 NI=19,它分到的 CPU 占比会明显变少。

5.并行和并发

并发(Concurrency)

一个 CPU 核心,快速切换多个进程,宏观上看起来同时跑微观上任意时刻,单核 CPU 只能执行一个进程;依靠时间片轮转快速切换。

关键词:交替执行、宏观同时、单核、时间片切换

例子:单核电脑一边听歌一边打字。操作系统快速在播放器进程、输入法进程之间来回切换。

并行(Parallelism)

多个 CPU 核心,同一时刻,多个进程真的同时运行每个核心跑一个进程,同一瞬间多条指令同时执行。

即多核、同一时刻真正同时跑

区分:

并发:宏观一起跑,微观交替跑(单核)

并行:宏观微观都一起跑(多核)

关系:并行一定属于并发;并发不一定是并行。

例如:单核下多进程交替调度
#include <stdio.h> #include <unistd.h> int main() { pid_t pid = fork(); if(pid == 0) { for(int i=0;i<5;i++) { printf("【子进程】计数 %d\n", i); sleep(1); } } else { for(int i=0;i<5;i++) { printf("【父进程】计数 %d\n", i); sleep(1); } } return 0; }

运行输出是交替打印,这就是并发效果。在单核机器上,它们是轮流上 CPU;多核机器上有可能并行。

对比
特性并发 Concurrency并行 Parallelism
CPU 核心单核 / 多核都可以必须多核
执行时间片交替切换同一时刻同时执行
核心调度切换,提高资源利用率硬件多核,提升运行速度

二、进程切换

在 Linux 2.6 内核之前,老版本调度器是 O (n) 算法,每次选下一个进程都要遍历就绪队列,进程一多,调度延迟就明显上涨。2.6 内核推出的O (1) 调度器一举解决了这个痛点,调度选择、入队出队操作都是常数时间,也是 Linux 调度演进里非常经典的设计。本文从 runqueue、优先级、active/expired 双队列,一步步拆解它的原理,同时关联进程切换。

1.进程切换

进程切换,就是 CPU 从正在运行的进程 A,切换到另外一个就绪进程 B 执行的过程,也叫上下文切换。 一次完整的切换分为两大块:调度选择+上下文切换。

调度器:从就绪队列里挑选下一个要运行的进程(这就是 O (1) 调度器负责的工作);

上下文切换:保存当前进程 A 的寄存器、栈等上下文,加载进程 B 的上下文,CPU 跳转到 B 的指令继续执行。

调度队列就是给调度器准备好所有就绪进程的数据结构,没有调度队列,调度器就不知道该选谁运行。Linux 2.6 O (1) 调度器的核心舞台,就是 runqueue 运行队列。

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

O (1) 名字的含义:不管就绪队列里有多少个进程,挑选下一个进程的耗时固定,不会随着进程数量增加而变慢。这个目标,完全依靠 runqueue + active/expired 双优先级数组实现。

2.1一个 CPU 拥有一个 runqueue

O (1) 调度器采用per-CPU runqueue(每个 CPU 私有运行队列)设计:

多核环境下,每一个 CPU 核心,都单独维护属于自己的 runqueue;

就绪进程默认挂载到某一个 CPU 的 runqueue 上,减少跨 CPU 的锁竞争,提升多核性能;

runqueue 存放这个 CPU 上所有TASK_RUNNING(就绪)的进程;

不同 CPU 的 runqueue 互相独立,只有负载均衡的时候,才会把进程从一个 CPU 的 rq 迁移到另一个 CPU 的 rq。

即谁的 CPU,谁管自己的就绪进程。

2.2优先级

O (1) 调度器一共划分140 个优先级档位,数值越小,优先级越高:

0 ~ 99:实时进程优先级,实时进程优先级高于普通进程,抢占能力更强;

100 ~ 139:普通进程的静态优先级,用户可以通过nice调整(nice 范围 -20 ~ +19,对应 100~139)。

每个优先级,都对应一条独立的进程链表。同时配套一个bitmap位图,位图里每一个 bit 标记对应优先级链表上是否还有就绪进程。 调度器找最高优先级进程的时候,直接扫描 bitmap,找到第一个置 1 的 bit,就能定位到最高优先级链表,不需要遍历全部进程,这是 O (1) 的关键。

举例:bitmap 第 10 位为 1,说明优先级 10 的链表里面还有就绪任务,优先它。

2.3活动队列 active

活动队列(active 数组,prio_array):存放还有剩余时间片的就绪进程。

新唤醒、新创建的就绪进程,会加入 active 队列;

CPU 调度的时候,只从 active 队列挑选进程运行;

进程被选中后,消耗自己的时间片;只要时间片还没耗尽,就留在 active 队列。

active 数组就是当前这一轮调度周期里,允许被调度执行的进程集合。

2.4过期队列 expired

过期队列(expired 数组):存放时间片已经用完的就绪进程。

进程时间片耗尽后,调度器会重新计算这个进程下一轮的时间片,然后把它移出 active,插入 expired 队列;

expired 队列里的进程,在当前轮次不会被调度,要等 active 全部耗尽之后才有机会;

expired 里面的进程依然是就绪态,不是阻塞态,只是这一轮时间片用完 “排队等下一轮”。

2.5active 指针和 expired 指针

runqueue 里面并不是拷贝两份完整队列,而是维护两个指针:active指针、expired指针,分别指向两个prio_array优先级数组。

核心操作:指针交换当 active 指向的数组里面,所有进程的时间片全部耗尽、active 为空之后:

// 伪代码,交换指针 swap(rq->active, rq->expired);

交换之后:

原来的 expired 数组,变成新的 active,里面的进程获得新一轮时间片,可以开始被调度;

原来空掉的 active 数组,变成新的 expired,用来接纳后续时间片耗尽的进程。

亮点:交换只是交换指针的值,没有拷贝任何链表节点,开销极低,是 O (1) .


三、总结

  1. 进程依靠优先级确定调度权重,用户只能修改 nice 值间接改变内核 PRI;进程具备资源隔离的独立性,同时会竞争 CPU 等资源;
  2. 并发是时间片交替执行,并行是多核同时执行,二者概念不能混淆;
  3. O (1) 调度器为每个 CPU 分配独立 runqueue,依靠 bitmap 快速定位最高优先级就绪进程;
  4. active 存放有剩余时间片的就绪进程,expired 存放时间片耗尽的就绪进程;active 耗尽时直接交换 active、expired 指针,开启新一轮调度;
  5. 调度器选出进程之后,触发上下文切换,保存旧进程上下文、加载新进程上下文,完成 CPU 使用权转移。

补充:O (1) 调度器虽然经典,但它对 IO 密集型交互进程的公平性存在短板,后续被 CFS 完全公平调度器取代。但是 per-CPU 队列、双数组轮转、位图快速查找的思想,是 Linux 调度学习的经典内容。