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) .
三、总结
- 进程依靠优先级确定调度权重,用户只能修改 nice 值间接改变内核 PRI;进程具备资源隔离的独立性,同时会竞争 CPU 等资源;
- 并发是时间片交替执行,并行是多核同时执行,二者概念不能混淆;
- O (1) 调度器为每个 CPU 分配独立 runqueue,依靠 bitmap 快速定位最高优先级就绪进程;
- active 存放有剩余时间片的就绪进程,expired 存放时间片耗尽的就绪进程;active 耗尽时直接交换 active、expired 指针,开启新一轮调度;
- 调度器选出进程之后,触发上下文切换,保存旧进程上下文、加载新进程上下文,完成 CPU 使用权转移。
补充:O (1) 调度器虽然经典,但它对 IO 密集型交互进程的公平性存在短板,后续被 CFS 完全公平调度器取代。但是 per-CPU 队列、双数组轮转、位图快速查找的思想,是 Linux 调度学习的经典内容。