LInux系统篇(二):深入剖析 Linux 进程:状态变迁、优先级及调度切换逻辑

大家好。在 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 时,内核会执行以下步骤:

  1. 保存现场 :把进程 A 当前所有寄存器的值,全部保存到它的 struct task_struct(PBC)(进程控制块)中。
  2. 恢复现场 :从进程 B 的 struct task_struct 中,把它之前保存的寄存器值恢复到 CPU 中。
  3. 继续执行 :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];
};
相关推荐
daad7771 小时前
记录一个串口模块没有回包的问题
linux·运维·服务器
开发者联盟league1 小时前
在ubuntu上使用apt方式安装gitlab
linux·ubuntu·gitlab
青梅橘子皮1 小时前
Linux---虚拟地址空间
linux·运维·算法
晚风予卿云月1 小时前
【Linux】进程控制(一)—进程创建、进程终止与信号全流程详解
linux·运维·服务器·后端开发
roman_日积跬步-终至千里1 小时前
【架构实践(1)】架构师如何正确理解业务
运维·架构
skywalk81631 小时前
在Ubuntu安装明道名部署Playground web网页
linux·运维·ubuntu
Dontla2 小时前
WSL2 docker-desktop发行版介绍(用于运行Docker引擎(Docker Engine))(docker-desktop-data)
运维·docker·容器
爱和冰阔落2 小时前
Linux/Windows 双平台通关:YOLOv8 目标检测从模型选型到跨平台部署实战
linux·windows·yolo
小蜗牛的路2 小时前
Linux redhat 7在线安装docker、下载docker依赖、离线安装docker
linux·运维·docker