目录一、进程概念Process第一层・定义级第二层・原理级第三层・对比级二、进程状态详解0. 状态总览1. 运行态Running / TASK_RUNNING第一层・定义级第二层・原理级第三层・对比级2. 就绪态Ready第一层・定义级第二层・原理级第三层・对比级3. 阻塞态Blocked / Sleeping第一层・定义级第二层・原理级第三层・对比级Demo阻塞态观察4. 僵尸态Zombie / EXIT_ZOMBIE第一层・定义级第二层・原理级第三层・对比级Demo僵尸进程制造与观察5. 孤儿态Orphan第一层・定义级第二层・原理级第三层・对比级面试三层级方法论• 第一层定义级能说出是什么、有什么用 —— 及格线• 第二层原理级能讲清楚底层怎么实现、为什么这么设计 —— 加分项• 第三层对比级能横向对比同类方案、讲 trade-off、结合项目说一、进程概念Process第一层・定义级是什么进程是程序的一次运行实例是操作系统进行资源分配和调度的基本单位。程序 存放在磁盘上的二进制文件静态的、死的进程 程序被加载到内存中运行后的实体动态的、活的有什么用资源隔离每个进程有独立的地址空间一个进程崩了不影响其他进程并发执行OS 通过切换进程让多个程序 同时 运行权限管理每个进程有 uid/gid系统据此控制资源访问权限第二层・原理级底层怎么实现Linux 内核用一个叫 task_struct进程描述符的巨型结构体来描述一个进程存在内核栈底部 / 单独的 slab 缓存中。核心字段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字段的核心宏定义#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 红黑树上。进入就绪态的时机fork()创建完新进程初始化完成后阻塞的进程等到了事件IO 完成、信号到来被抢占的进程高优先级进程来了时间片用完的进第三层・对比级状态等待的资源能否被调度就绪只缺 CPU✅ 在 runqueue 里随时可调度阻塞缺 IO / 锁 / 事件❌ 不在 runqueue调度器看不到3. 阻塞态Blocked / Sleeping第一层・定义级是什么进程因为等待某个事件发生而主动放弃 CPU暂停执行。事件到来之前给它 CPU 也跑不了。两种阻塞可中断阻塞TASK_INTERRUPTIBLE等事件 收到信号也会唤醒比如sleep()、wait()不可中断阻塞TASK_UNINTERRUPTIBLE只能等事件信号都不响应比如磁盘 IO 进行中有什么用避免忙等busy waiting浪费 CPU让 CPU 去跑别的进程事件来了再唤醒。第二层・原理级底层怎么实现进程把自己从 runqueue 中移除把自己挂到某个等待队列wait_queue上修改 state 为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE调用schedule()主动触发调度让出 CPU唤醒时反向操作事件触发后等待队列上的进程被唤醒state 改回TASK_RUNNING重新加入 runqueue。为什么这么设计等待队列是 生产者 - 消费者 模型事件生产者如中断处理函数唤醒等待者不可中断阻塞存在的意义防止 IO 过程中被信号打断导致数据不一致比如写到一半的磁盘第三层・对比级阻塞类型唤醒条件典型场景ps 显示可中断事件发生 任意信号sleep、wait、读终端S不可中断只能是事件发生磁盘 IO、锁 SemaphoreDDemo阻塞态观察// 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; }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 结构体。第二层・原理级底层实现进程调用exit()→ 释放用户态资源地址空间、文件、内存状态设为EXIT_ZOMBIE给父进程发SIGCHLD信号父进程调用wait()读取退出状态后内核才释放 task_struct为什么这么设计父进程可能需要知道子进程是正常退出还是异常崩溃、退出码是多少、用了多少 CPU 时间这些信息必须在进程死后保留一段时间所以设计了 僵尸 这个中间状态第三层・对比级维度僵尸进程孤儿进程谁死了子进程死了父进程活着父进程死了子进程活着资源占用用户态资源已释放只剩 task_struct正常运行占完整资源危害大量僵尸占满 PID 号导致无法创建新进程无危害被 init 收养怎么解决父进程 wait或 kill 父进程让 init 接管不需要解决正常现象怎么杀僵尸进程→ 杀不了它已经死了。只能 kill 它的父进程让它变成孤儿然后被 init 进程收养并回收。Demo僵尸进程制造与观察// 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; }gcc demo_zombie.c -o demo_zombie ./demo_zombie # 另一个终端ps aux | grep Z 或 ps -o pid,stat 子pid # 会看到 STAT 是 Z僵尸状态5. 孤儿态Orphan第一层・定义级是什么父进程先退出了子进程还在运行这个子进程就成了孤儿。有什么用不是故意设计的状态是一种现象。Linux 会自动把孤儿进程过继给 init 进程PID1由 init 来负责回收它的尸体保证不会永远僵尸。第二层・原理级底层实现父进程 exit 时遍历自己的所有子进程如果子进程还活着非 EXIT_DEAD就把它的父进程指针p-parent指向child_reaper通常是 init同时给新的父进程init发 SIGCHLD为什么这么设计必须有一个兜底机制否则父进程意外死亡后子进程退出时永远没人收尸PID 会泄漏init 进程天生自带 wait 循环专门收尸第三层・对比级对比项孤儿进程僵尸进程进程是否活着✅ 活着正常运行❌ 已死只剩壳占用资源完整的内存、文件、CPU 时间几乎不占只占 PID回收方式退出时由 init 自动回收必须父进程 wait 或父死被 init 收算不算问题不算系统自动处理大量累积才是问题PID 耗尽// 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; }gcc demo_orphan.c -o demo_orphan ./demo_orphan # 观察子进程打印的 ppid 从父进程PID变成了1或systemd的PID