——进程(二):深入剖析 Linux 进程:状态变迁、优先级及调度切换逻辑)
大家好。在 Linux 系统中进程就像是一个个独立的 “工作单元”系统能否高效运转完全取决于对进程的管理能力。今天我们就聚焦三大核心内容进程有哪些状态、状态之间如何切换进程优先级如何影响执行顺序以及内核到底是怎样完成进程调度与切换的。1.进程状态进程状态就是操作系统对当前进程所处运行阶段的标识用来描述进程此刻在做什么、能否使用CPU、是否等待资源。Linux 内核会根据进程行为、资源情况自动切换状态内核调度器也依靠状态判断要不要给进程分配 CPU。 #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 状态的进程则在各自的队列里排队。#includestdio.h #includeunistd.h int main() { int retfork(); if(ret0) { while(1) { printf(我是一个子进程%d\n, getpid()); sleep(1); } } else if(ret0) { while(1) { } } return 0; }2.睡眠阻塞状态进程睡眠状态是指进程主动暂停执行放弃 CPU 使用权等待某个事件发生才能被唤醒继续运行的状态。睡眠状态分为两种1. 可中断睡眠状态S 状态标准定义进程处于等待状态可以被信号唤醒也可以被等待的事件唤醒不占用 CPU属于正常阻塞状态。2. 不可中断睡眠状态D 状态标准定义进程处于深度等待状态忽略所有信号只能被等待的硬件 / IO 事件唤醒用于保证原子操作。1.可中断睡眠状态S只要是 scanf、sleep、read、write 这类 “等事件” 的系统调用默认都会让进程进入 S 状态 —— 可中断睡眠可以被信号CtrlC、kill唤醒或终止.可以用kill -9进程标识符杀掉可中断睡眠状态进程2.不可中断睡眠状态D进程在等待硬件 I/O如磁盘读写时进入的一种不能被任何信号打断、不能被杀死的深度睡眠状态。写磁盘 硬件操作1.内核必须保证这次 I/O 原子性完成不能中途被打断2.所以进程进入 不可中断睡眠D3.连kill -9 都杀不死3.暂停状态 T暂停状态T进程被外部信号强制挂起完全停止执行、不占 CPU、保留现场直到收到恢复信号才继续运行。#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. 进程状态ZZombie 僵尸态不占用 CPU、内存仅占用进程表项。4. 危害单个僵尸进程无影响大量僵尸进程会占满系统进程表导致系统无法创建新进程。#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 系统中所有孤儿进程会被 PID1 的 init/systemd 进程自动收养由 init 进程充当新父进程并负责后续资源回收。3. 进程状态正常运行态R/S和普通进程无区别。4. 危害无危害系统会自动管理不会占用额外资源。#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 / SZ管理方被 PID1 进程收养父进程持有 PCB无人主动回收资源占用正常占用系统资源仅占用进程表项无 CPU / 内存占用危害性无害大量堆积会导致无法创建新进程2.进程优先级进程优先级是操作系统用来决定哪个进程优先获得 CPU 时间片的数值 / 权重。数值越小→ 优先级越高→ 越先被 CPU 执行。1.优先级切换1.top2.r(renice)3.输入要修改的进程标识符4.输入要修改的值5.回车2.优先级有极限优先级有极限核心原因有三点1.防止用户进程无限抢占 CPU保护系统稳定如果普通进程的优先级可以无限调高会挤掉内核、关键系统进程的执行时间导致系统卡顿甚至崩溃。设限可以避免这种 “恶性抢占”。2.保障进程调度的公平性避免 “饿死” 低优先级进程如果优先级没有下限进程可能被压得极低永远得不到 CPU 时间。设限保证再低优先级的进程也有机会被调度执行。3.内核调度模型本身存在固定的优先级区间无论是普通进程的 nice 值-20~19还是实时进程的调度优先级1~99内核都设计了固定的范围不允许超出这是调度算法的固有约束。3.优先级的最终权限值最终权限值进程优先级默认80nice值对于上面的进程优先级默认值801999默认值80-20603.进程的切换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.调度原理O1调度算法1. nr_active作用记录当前队列里就绪进程的总数量。用途调度器需要快速知道有多少进程在等待 CPU用来做负载统计、调度决策比如判断队列是否为空、是否需要负载均衡。简单说它就是个 “计数器”告诉你现在有多少进程在排队。2. bitmap[5]位图数组作用快速定位最高优先级的就绪进程。原理早期 Linux 把进程优先级分成 0~139共140 个等级每个优先级对应队列数组 queue[140] 里的一个队列。bitmap[5] 一共 5×32160 位足够覆盖 140 个优先级是典型的 “用空间换时间” 的设计bitmap 里的每一位代表一个优先级如果该位为 1说明这个优先级的队列里有就绪进程为 0 则说明该队列为空。调度器直接找 bitmap 里第一个为 1 的位就能立刻找到最高优先级的进程时间复杂度是 O (1)。3.queue[140]队列数组作用按优先级分组存放不同优先级的就绪进程链表。原理数组下标 0~139 对应进程的 0~139 优先级数值越小优先级越高。每个queue[i] 都是一个链表存放所有优先级为 i的就绪进程。调度器根据 bitmap 找到最高优先级 k 后直接取 queue[k] 里的第一个进程运行即可。简单说它就是个 “按优先级分好的等待队列”每个队列里都是同优先级的进程。活动队列已经调度的进程会链到过期队列中两者此消彼长活动队列空时会和过期队列进行交换再次执行活动队列里面的代码3.Linux 2.6 内核中 O (1) 调度器的核心数据结构定义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]; };