Linux 进程与进程状态・三层级深度解析

目录

一、进程概念(Process)

第一层・定义级

第二层・原理级

第三层・对比级

二、进程状态详解

[0. 状态总览](#0. 状态总览)

[1. 运行态(Running / TASK_RUNNING)](#1. 运行态(Running / TASK_RUNNING))

第一层・定义级

第二层・原理级

第三层・对比级

[2. 就绪态(Ready)](#2. 就绪态(Ready))

第一层・定义级

第二层・原理级

第三层・对比级

[3. 阻塞态(Blocked / Sleeping)](#3. 阻塞态(Blocked / Sleeping))

第一层・定义级

第二层・原理级

第三层・对比级

Demo:阻塞态观察

[4. 僵尸态(Zombie / EXIT_ZOMBIE)](#4. 僵尸态(Zombie / EXIT_ZOMBIE))

第一层・定义级

第二层・原理级

第三层・对比级

Demo:僵尸进程制造与观察

[5. 孤儿态(Orphan)](#5. 孤儿态(Orphan))

第一层・定义级

第二层・原理级

第三层・对比级


面试三层级方法论​

• 第一层(定义级):能说出是什么、有什么用 ------ 及格线​

• 第二层(原理级):能讲清楚底层怎么实现、为什么这么设计 ------ 加分项​

• 第三层(对比级):能横向对比同类方案、讲 trade-off、结合项目说

一、进程概念(Process)

第一层・定义级

是什么: 进程是 程序的一次运行实例 ,是操作系统进行 资源分配和调度的基本单位

  • 程序 = 存放在磁盘上的二进制文件(静态的、死的)
  • 进程 = 程序被加载到内存中运行后的实体(动态的、活的)

有什么用:

  1. 资源隔离:每个进程有独立的地址空间,一个进程崩了不影响其他进程
  2. 并发执行:OS 通过切换进程让多个程序 "同时" 运行
  3. 权限管理:每个进程有 uid/gid,系统据此控制资源访问权限

第二层・原理级

底层怎么实现: Linux 内核用一个叫 task_struct(进程描述符)的巨型结构体来描述一个进程,存在内核栈底部 / 单独的 slab 缓存中。

核心字段:

cpp 复制代码
struct task_struct {
    volatile long state;       // 进程状态
    void *stack;               // 进程内核栈指针
    pid_t pid;                 // 进程ID
    pid_t tgid;                // 线程组ID
    struct mm_struct *mm;      // 用户态地址空间
    struct files_struct *files;// 打开的文件表
    struct signal_struct *signal; // 信号处理
    struct sched_entity se;    // 调度实体(CFS调度用)
    struct task_struct *parent;// 父进程指针
    // ... 几百个字段
};

为什么这么设计:

  • 独立地址空间(mm_struct):通过页表实现虚拟内存隔离,安全性高,代价是进程切换要刷 TLB,开销大
  • task_struct 放内核态:用户态无法直接修改,保证内核数据结构安全
  • 父子关系树形结构:所有进程以 init 进程为根形成进程树,便于资源回收和信号传递

第三层・对比级

维度 进程 线程 协程
资源粒度 资源分配基本单位,独立地址空间 调度基本单位,共享进程地址空间 用户态调度,共享线程栈空间
切换开销 大(页表、TLB、文件表全换) 小(只换寄存器和栈) 极小(用户态完成,无内核陷入)
通信方式 IPC(管道、消息队列、共享内存) 共享变量 + 锁 直接函数调用式协作
崩溃影响 单个进程崩溃不影响其他 一个线程崩溃 → 整个进程挂 同线程内协程一起挂
适用场景 需要强隔离的独立程序 CPU 密集型并发、IO 多路复用 高并发 IO、轻量任务调度

二、进程状态详解

0. 状态总览

Linux 内核中 state 字段的核心宏定义:

cpp 复制代码
#define TASK_RUNNING         0x0000  // 运行/就绪态(Linux合并了)
#define TASK_INTERRUPTIBLE   0x0001  // 可中断阻塞
#define TASK_UNINTERRUPTIBLE 0x0002  // 不可中断阻塞
#define __TASK_STOPPED       0x0004  // 暂停态
#define EXIT_ZOMBIE          0x0020  // 僵尸态
#define EXIT_DEAD            0x0040  // 死亡态

1. 运行态(Running / TASK_RUNNING)

第一层・定义级

是什么: 进程正在 CPU 上执行 ,或者已经准备好随时可以被调度执行

⚠️ 注意:Linux 把 "运行中" 和 "就绪" 合并成了同一个状态 TASK_RUNNING,这是和教科书最大的区别!

有什么用: 标记这个进程是调度器的候选对象,会被放进运行队列(runqueue)里等待 CPU 时间片。

第二层・原理级

底层实现:

  • 每个 CPU 有一个 CFS(完全公平调度器)运行队列 cfs_rq
  • 所有 TASK_RUNNING 状态的进程按 vruntime(虚拟运行时间)挂在红黑树上
  • 调度器每次挑 vruntime 最小的进程上 CPU 运行

第三层・对比级

  • 运行中 vs 就绪:Linux 内核层面不分,但从 "是否占用 CPU" 的角度区分 ------ 占用 CPU 的叫 running,在队列里排队的叫 ready
  • 时间片耗尽:进程用完时间片 → 重新入队,状态不变(还是 TASK_RUNNING),只是从 CPU 上下来了

2. 就绪态(Ready)

第一层・定义级

是什么: 进程万事俱备,只等 CPU。所有资源都分配好了,就差被调度器选中上 CPU 执行。

有什么用: 区分 "能跑但没轮到" 和 "根本跑不了(阻塞)" 的进程,调度器只从就绪队列里挑进程。

第二层・原理级

底层实现: 在 Linux 里就是 TASK_RUNNING 状态但当前不在 CPU 上的那些进程,挂在每个 CPU 的 runqueue 红黑树上。

进入就绪态的时机:

  1. fork() 创建完新进程,初始化完成后
  2. 阻塞的进程等到了事件(IO 完成、信号到来)
  3. 被抢占的进程(高优先级进程来了)
  4. 时间片用完的进

第三层・对比级

状态 等待的资源 能否被调度
就绪 只缺 CPU ✅ 在 runqueue 里,随时可调度
阻塞 缺 IO / 锁 / 事件 ❌ 不在 runqueue,调度器看不到

3. 阻塞态(Blocked / Sleeping)

第一层・定义级

是什么: 进程因为等待某个事件发生而主动放弃 CPU,暂停执行。事件到来之前,给它 CPU 也跑不了。

两种阻塞:

  • 可中断阻塞(TASK_INTERRUPTIBLE) :等事件 + 收到信号也会唤醒(比如 sleep()wait()
  • 不可中断阻塞(TASK_UNINTERRUPTIBLE):只能等事件,信号都不响应(比如磁盘 IO 进行中)

有什么用: 避免忙等(busy waiting)浪费 CPU,让 CPU 去跑别的进程,事件来了再唤醒。

第二层・原理级

底层怎么实现:

  1. 进程把自己从 runqueue 中移除
  2. 把自己挂到某个**等待队列(wait_queue)**上
  3. 修改 state 为 TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE
  4. 调用 schedule() 主动触发调度,让出 CPU

唤醒时反向操作:事件触发后,等待队列上的进程被唤醒,state 改回 TASK_RUNNING,重新加入 runqueue。

为什么这么设计:

  • 等待队列是 "生产者 - 消费者" 模型:事件生产者(如中断处理函数)唤醒等待者
  • 不可中断阻塞存在的意义:防止 IO 过程中被信号打断导致数据不一致(比如写到一半的磁盘)

第三层・对比级

阻塞类型 唤醒条件 典型场景 ps 显示
可中断 事件发生 + 任意信号 sleep、wait、读终端 S
不可中断 只能是事件发生 磁盘 IO、锁 Semaphore D

Demo:阻塞态观察

cpp 复制代码
// demo_block.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    printf("PID: %d, 我马上进入 sleep(可中断阻塞)...\n", getpid());
    printf("另开终端执行: ps -o pid,stat,cmd -p %d\n", getpid());
    sleep(30);  // 睡眠30秒,状态为 S(可中断睡眠)
    printf("睡醒了,结束\n");
    return 0;
}
bash 复制代码
gcc demo_block.c -o demo_block && ./demo_block
# 另一个终端:ps -o pid,stat,cmd -p <pid>
# 会看到 STAT 是 S+(前台进程的可中断睡眠)

4. 僵尸态(Zombie / EXIT_ZOMBIE)

第一层・定义级

是什么: 进程已经执行结束退出了,但它的父进程还没有调用 wait()/waitpid() 来收尸 ,导致 task_struct 还没被释放。

有什么用: 保留退出状态码和资源使用统计,等父进程来 "收尸" 读取。僵尸进程本身几乎不占资源,只占一个 PID 和一个 task_struct 结构体。

第二层・原理级

底层实现:

  1. 进程调用 exit() → 释放用户态资源(地址空间、文件、内存)
  2. 状态设为 EXIT_ZOMBIE
  3. 给父进程发 SIGCHLD 信号
  4. 父进程调用 wait() 读取退出状态后,内核才释放 task_struct

为什么这么设计:

  • 父进程可能需要知道子进程是正常退出还是异常崩溃、退出码是多少、用了多少 CPU 时间
  • 这些信息必须在进程死后保留一段时间,所以设计了 "僵尸" 这个中间状态

第三层・对比级

维度 僵尸进程 孤儿进程
谁死了 子进程死了,父进程活着 父进程死了,子进程活着
资源占用 用户态资源已释放,只剩 task_struct 正常运行,占完整资源
危害 大量僵尸占满 PID 号,导致无法创建新进程 无危害,被 init 收养
怎么解决 父进程 wait,或 kill 父进程让 init 接管 不需要解决,正常现象

怎么杀僵尸进程? → 杀不了,它已经死了。只能 kill 它的父进程,让它变成孤儿,然后被 init 进程收养并回收。

Demo:僵尸进程制造与观察

bash 复制代码
// demo_zombie.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid = fork();
    
    if (pid < 0) {
        perror("fork失败");
        exit(1);
    } else if (pid == 0) {
        // 子进程:立刻退出,变成僵尸
        printf("子进程 PID=%d 即将退出,变成僵尸...\n", getpid());
        exit(0);
    } else {
        // 父进程:不 wait,休眠30秒让你观察
        printf("父进程 PID=%d,子进程 PID=%d\n", getpid(), pid);
        printf("另开终端执行: ps -o pid,stat,cmd -p %d\n", pid);
        sleep(30);
        printf("父进程结束\n");
    }
    return 0;
}
bash 复制代码
gcc demo_zombie.c -o demo_zombie && ./demo_zombie
# 另一个终端:ps aux | grep Z  或  ps -o pid,stat <子pid>
# 会看到 STAT 是 Z+(僵尸状态)

5. 孤儿态(Orphan)

第一层・定义级

是什么: 父进程先退出了,子进程还在运行,这个子进程就成了孤儿。

有什么用: 不是故意设计的状态,是一种现象。Linux 会自动把孤儿进程过继给 init 进程(PID=1),由 init 来负责回收它的尸体,保证不会永远僵尸。

第二层・原理级

底层实现:

  1. 父进程 exit 时,遍历自己的所有子进程
  2. 如果子进程还活着(非 EXIT_DEAD),就把它的父进程指针 p->parent 指向 child_reaper(通常是 init)
  3. 同时给新的父进程(init)发 SIGCHLD

为什么这么设计:

  • 必须有一个兜底机制,否则父进程意外死亡后,子进程退出时永远没人收尸,PID 会泄漏
  • init 进程天生自带 wait 循环,专门收尸

第三层・对比级

对比项 孤儿进程 僵尸进程
进程是否活着 ✅ 活着,正常运行 ❌ 已死,只剩壳
占用资源 完整的内存、文件、CPU 时间 几乎不占,只占 PID
回收方式 退出时由 init 自动回收 必须父进程 wait 或父死被 init 收
算不算问题 不算,系统自动处理 大量累积才是问题(PID 耗尽)
bash 复制代码
// demo_orphan.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid = fork();
    
    if (pid < 0) {
        perror("fork失败");
        exit(1);
    } else if (pid == 0) {
        // 子进程
        printf("子进程: PID=%d, 父进程PPID=%d\n", getpid(), getppid());
        sleep(3);  // 等父进程先死
        printf("子进程: 现在我的PPID=%d(应该是1或systemd)\n", getppid());
        printf("子进程: 我是孤儿,被init收养了\n");
        sleep(10);
        printf("子进程退出\n");
    } else {
        // 父进程:立刻退出
        printf("父进程: PID=%d,我先走一步\n", getpid());
        exit(0);
    }
    return 0;
}
bash 复制代码
gcc demo_orphan.c -o demo_orphan && ./demo_orphan
# 观察:子进程打印的 ppid 从父进程PID变成了1(或systemd的PID)
相关推荐
不会就选b1 小时前
linux之进程管理(二)--替换
linux·运维·服务器
倔强的石头1062 小时前
【Linux指南】动静态库系列(二):从源码复用到目标文件复用:为什么需要把 .o 打包成库
linux·运维·服务器
码农学院2 小时前
GEO与SEO协同:从传统搜索到生成式搜索的平滑迁移路径
服务器·前端·python
步步精BBJconn2 小时前
从GPU服务器到数据中心:AI服务器高压连接器的应用与发展趋势
大数据·运维·服务器·人工智能·科技·物联网
码农学院4 小时前
基于运维监控体系的网络品牌推广方案:从架构设计到技术实现
运维·网络
爱写代码的森10 小时前
鸿蒙三方库 | harmony-utils之ImageUtil图片保存到本地详解
服务器·华为·harmonyos·鸿蒙·huawei
大耳朵-小飞象13 小时前
电力安全运维的智能密码:BACS如何破解设备全生命周期管理难题,让电网安全“看得见、管得住”?
运维·安全·智慧城市·能耗系统·楼宇智控·未来生活
极客侃科技13 小时前
制造企业 MES/APS 选型:SAP PP/DS 集成、ERP-MES 边界划分与一体化架构要点
运维·架构·制造