
大家好。在 Linux 系统中,进程就像是一个个独立的 "工作单元",系统能否高效运转,完全取决于对进程的管理能力。今天我们就聚焦三大核心内容:进程有哪些状态 、状态之间如何切换 ,进程优先级如何影响执行顺序,以及内核到底是怎样完成进程调度与切换的。

1.进程状态
进程状态,就是操作系统对当前进程所处运行阶段的标识,用来描述进程此刻在做什么、能否使用
CPU、是否等待资源。Linux 内核会根据进程行为、资源情况,自动切换状态,内核调度器也依靠
状态判断要不要给进程分配 CPU。

cpp
#define TASK_RUNNING 0 //R 运行/就绪态
#define TASK_INTERRUPTIBLE 1 //S 可中断睡眠
#define TASK_UNINTERRUPTIBLE 2 //D 不可中断睡眠
#define __TASK_STOPPED 4 //T 进程被暂停
#define __TASK_TRACED 8 //T 被调试器跟踪
/* in tsk->exit_state */
#define EXIT_ZOMBIE 16 //Z 僵尸进程
#define EXIT_DEAD 32 //进程彻底消亡
//运行,阻塞,挂起

1.运行状态 R
是进程唯一 能在 CPU 上执行的状态
ps -al 查看进程状态

CPU 与 R 状态的关系
单核 CPU :同一时刻,只能有1 个进程真正在 CPU 上执行。但就绪队列里可以有很多 R 状态的进程,它们在排队等待。
多核 / 多 CPU :同一时刻,每个核心都可以跑 1 个进程,所以可以有和核心数一样多的进程同时在 CPU 上执行,其他 R 状态的进程则在各自的队列里排队。
cpp
#include<stdio.h>
#include<unistd.h>
int main()
{
int ret=fork();
if(ret==0)
{
while(1)
{
printf("我是一个子进程:%d\n", getpid());
sleep(1);
}
}
else if(ret>0)
{
while(1)
{
}
}
return 0;
}

2.睡眠(阻塞)状态
进程睡眠状态:是指进程主动暂停执行,放弃 CPU 使用权,等待某个事件发生才能被唤醒继续运行的状态。
睡眠状态分为两种:
1. 可中断睡眠状态(S 状态)
标准定义:
进程处于等待状态,可以被信号唤醒,也可以被等待的事件唤醒,不占用 CPU,属于正常阻塞状态。2. 不可中断睡眠状态(D 状态)
标准定义:
进程处于深度等待状态,忽略所有信号,只能被等待的硬件 / IO 事件唤醒,用于保证原子操作。
1.可中断睡眠状态S
只要是 scanf、sleep、read、write 这类 "等事件" 的系统调用,默认都会让进程进入 S 状态 ------ 可中断睡眠,可以被信号(Ctrl+C、kill)唤醒或终止.

可以用kill -9+进程标识符杀掉可中断睡眠状态进程
2.不可中断睡眠状态D
进程在等待硬件 I/O(如磁盘读写)时,进入的一种不能被任何信号打断、不能被杀死的深度睡眠状态。
写磁盘 = 硬件操作
1.内核必须保证这次 I/O 原子性完成,不能中途被打断
2.所以进程进入 不可中断睡眠(D)
3.连 kill -9 都杀不死
3.暂停状态 T
暂停状态(T) :进程被外部信号强制挂起 ,完全停止执行、不占 CPU、保留现场,直到收到恢复信号才继续运行。
cpp
#include <stdio.h>
int main() {
printf("我的进程:%d\n", getpid());
while (1) {
int x;
scanf("%d", &x); // 平时是 S
printf("ok\n");
}
}

4.僵尸进程(Z)与孤儿进程
1.僵尸进程
1. 定义
子进程正常退出 ,父进程存活,但未调用系统函数回收子进程退出状态 ,子进程残留的进程控制块(PCB)称为僵尸进程。
2. 运行机制
进程退出后,代码段、栈、堆等资源会立即释放,但PCB 会短暂保留,用于向父进程传递退出码。若父进程一直不回收,PCB 会长期驻留进程表(内存泄漏)。
3. 进程状态
Z(Zombie 僵尸态),不占用 CPU、内存,仅占用进程表项。
4. 危害
单个僵尸进程无影响;大量僵尸进程会占满系统进程表,导致系统无法创建新进程。
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
return 1;
}
else if (pid == 0)
{
// 子进程:运行2秒后直接退出
printf("子进程 PID: %d 即将退出\n", getpid());
sleep(2);
exit(0);
}
else
{
// 父进程:持续运行,不回收子进程
while (1)
{
printf("父进程 PID: %d 运行中\n", getpid());
sleep(2);
}
}
return 0;
}
2.孤儿进程
1. 定义
父进程先终止退出,子进程仍在运行,该子进程称为孤儿进程。
2. 运行机制
Linux 系统中,所有孤儿进程会被 PID=1 的 init/systemd 进程自动收养,由 init 进程充当新父进程,并负责后续资源回收。
3. 进程状态
正常运行态(R/S),和普通进程无区别。
4. 危害
无危害,系统会自动管理,不会占用额外资源。
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
return 1;
}
else if (pid == 0)
{
// 子进程:持续运行
while (1)
{
printf("子进程 PID: %d, 父进程 PID: %d\n", getpid(), getppid());
sleep(2);
}
}
else
{
// 父进程:休眠1秒后直接退出
sleep(1);
printf("父进程即将退出\n");
exit(0);
}
return 0;
}
3.孤儿进程 vs 僵尸进程 对比表
|----------|--------------|----------------------|
| 对比项 | 孤儿进程 | 僵尸进程 |
| 形成原因 | 父进程先退出,子进程存活 | 子进程先退出,父进程存活且不回收 |
| 进程状态 | R / S | Z |
| 管理方 | 被 PID=1 进程收养 | 父进程持有 PCB,无人主动回收 |
| 资源占用 | 正常占用系统资源 | 仅占用进程表项,无 CPU / 内存占用 |
| 危害性 | 无害 | 大量堆积会导致无法创建新进程 |
2.进程优先级
进程优先级:是操作系统用来决定哪个进程优先获得 CPU 时间片的数值 / 权重。
数值越小 → 优先级越高 → 越先被 CPU 执行。

1.优先级切换
1.top
2.r(renice)
3.输入要修改的进程标识符
4.输入要修改的值
5.回车

2.优先级有极限
优先级有极限,核心原因有三点:
1.防止用户进程无限抢占 CPU,保护系统稳定
如果普通进程的优先级可以无限调高,会挤掉内核、关键系统进程的执行时间,导致系统卡顿甚至崩溃。设限可以避免这种 "恶性抢占"。
2.保障进程调度的公平性,避免 "饿死" 低优先级进程
如果优先级没有下限,进程可能被压得极低,永远得不到 CPU 时间。设限保证再低优先级的进程也有机会被调度执行。
3.内核调度模型本身存在固定的优先级区间
无论是普通进程的 nice 值(-20~19),还是实时进程的调度优先级(1~99),内核都设计了固定的范围,不允许超出,这是调度算法的固有约束。

3.优先级的最终权限值
最终权限值=进程优先级(默认(80))+nice值
对于上面的进程优先级
默认值(80)+19=99
默认值(80)+(-20)=60
3.进程的切换
1.为什么需要进程切换?
先想一个问题:如果一个进程中有一个死循环程序,其他进程还会不会执行?
答案是:会。现代操作系统都实现了抢占式多任务,CPU 不会被单个进程一直霸占。它会在合适的时候,强制或主动地把 CPU 使用权交给其他进程。
在时间片轮转调度(RR)中,每个就绪的进程会分到一个固定长度的时间片 (比如 10ms-100ms)
每一个时间片内会切换一个进程
这个 "交权" 的过程,就是进程切换。它解决了两个核心问题:
1.并发执行 :让多个进程看起来像是在同时运行,提升用户体验和系统利用率。
2.资源隔离:每个进程都感觉自己独占 CPU,互不干扰。
2.进程切换的核心:上下文
进程切换,本质上是保存和恢复进程的上下文。你可以把它理解成进程的 "运行快照"。
1. 什么是上下文?
1.当进程运行时,CPU 的寄存器中保存着它的所有状态信息 ,包括:
2.通用寄存器: eax, ebx, ecx, edx 等,存储运算数据。
3.程序计数器(PC/EIP) :记录下一条要执行的指令地址。
4.栈指针(ESP/EBP) :记录当前栈的位置。
5.标志寄存器(eflags):记录运算结果的状态(如是否进位、是否为零)。
这些信息共同构成了进程的硬件上下文。
3. 切换的完整流程
当进程 A 要让出 CPU 给进程 B 时,内核会执行以下步骤:
- 保存现场 :把进程 A 当前所有寄存器的值,全部保存到它的
struct task_struct(PBC)(进程控制块)中。 - 恢复现场 :从进程 B 的
struct task_struct中,把它之前保存的寄存器值恢复到 CPU 中。 - 继续执行 :CPU 从进程 B 的
EIP指向的指令开始,继续执行。
一句话总结:进程切换,就是 "把进程 A 的状态打包存起来,再把进程 B 的状态加载进 CPU"。


4.进程的调度
如果说进程切换是 "换人上 CPU" 的动作,那么进程调度器就是决定 "下一个该谁上" 的决策者。它的目标是:
公平性 :每个进程都能分到 CPU 时间。
效率 :CPU 尽量不空闲。
低延迟:交互式进程(如编辑器、终端)响应要快。
1. 调度队列与优先级
Linux 调度器基于优先级来决定谁先运行。我们在用户态看到的 nice 值(-20 ~ 19),会映射成内核的动态优先级。
调度器维护着多个队列,比如:
活动队列(active queue) :存放还没耗尽时间片的进程。
过期队列(expired queue):存放已经耗尽时间片的进程,等待下一轮调度
调度器会优先从高优先级的队列中挑选进程运行。
2.调度原理(O(1)调度算法)

1. nr_active
作用 :记录当前队列里就绪进程的总数量。
用途 :调度器需要快速知道有多少进程在等待 CPU,用来做负载统计、调度决策,比如判断队列是否为空、是否需要负载均衡。
简单说:它就是个 "计数器",告诉你现在有多少进程在排队。
2. bitmap5(位图数组)
作用 :快速定位最高优先级的就绪进程。
原理:
早期 Linux 把进程优先级分成 0~139,共 140 个等级,每个优先级对应队列数组 queue140 里的一个队列。
bitmap5 一共 5×32=160 位,足够覆盖 140 个优先级,是典型的 "用空间换时间" 的设计
bitmap 里的每一位代表一个优先级:如果该位为 1,说明这个优先级的队列里有就绪进程;为 0 则说明该队列为空。
调度器直接找 bitmap 里第一个为 1 的位,就能立刻找到最高优先级的进程,时间复杂度是 O (1)。
3.queue140(队列数组)
作用 :按优先级分组,存放不同优先级的就绪进程链表。
原理 :
数组下标 0~139 对应进程的 0~139 优先级(数值越小优先级越高)
。
每个queuei 都是一个链表,存放所有优先级为 i 的就绪进程。
调度器根据 bitmap 找到最高优先级 k 后,直接取 queuek 里的第一个进程运行即可。
简单说:它就是个 "按优先级分好的等待队列",每个队列里都是同优先级的进程。

活动队列已经调度的进程会链到过期队列中(两者此消彼长)活动队列空时会和过期队列进行交换,再次执行活动队列里面的代码
3.Linux 2.6 内核中 O (1) 调度器的核心数据结构定义
cpp
struct rq {
spinlock_t lock;
/*
* nr_running and cpu_load should be in the same cacheline because
* remote CPUs use both these fields when doing load calculation.
*/
unsigned long nr_running;
unsigned long raw_weighted_load;
#ifdef CONFIG_SMP
unsigned long cpu_load[3];
#endif
unsigned long long nr_switches;
/*
* This is part of a global counter where only the total sum
* over all CPUs matters. A task can increase this counter on
* one CPU and if it got migrated afterwards it may decrease
* it on another CPU. Always updated under the runqueue lock:
*/
unsigned long nr_uninterruptible;
unsigned long expired_timestamp;
unsigned long long timestamp_last_tick;
struct task_struct* curr, * idle;
struct mm_struct* prev_mm;
struct prio_array* active, * expired, arrays[2];
int best_expired_prio;
atomic_t nr_iowait;
#ifdef CONFIG_SMP
struct sched_domain* sd;
/* For active balancing */
int active_balance;
int push_cpu;
struct task_struct* migration_thread;
struct list_head migration_queue;
#endif
#ifdef CONFIG_SCHEDSTATS
/* latency stats */
struct sched_info rq_sched_info;
/* sys_sched_yield() stats */
unsigned long yld_exp_empty;
unsigned long yld_act_empty;
unsigned long yld_both_empty;
unsigned long yld_cnt;
/* schedule() stats */
unsigned long sched_switch;
unsigned long sched_cnt;
unsigned long sched_goidle;
/* try_to_wake_up() stats */
unsigned long ttwu_cnt;
unsigned long ttwu_local;
#endif
struct lock_class_key rq_lock_key;
};
/*
* These are the runqueue data structures:
*/
struct prio_array {
unsigned int nr_active;
DECLARE_BITMAP(bitmap, MAX_PRIO + 1); /* include 1 bit for delimiter */
struct list_head queue[MAX_PRIO];
};