【Linux】Linux进程状态和僵尸进程:一篇看懂“进程在忙啥”

前言:欢迎 各位光临 本博客,这里小编带你直接手撕Make/Makefile (自动化构建) ,文章并不复杂,愿诸君耐其心性,忘却杂尘,道有所长!!!!

**🔥个人主页:IF'Maxue-CSDN博客

🎬作者简介:C++研发方向学习者

📖**个人专栏:
《C语言》
《C++深度学习》
《Linux》
《数据结构》
《数学建模》
**

⭐️人生格言:生活是默默的坚持,毅力是永久的享受。不破不立,远方请直行!

文章目录

一、先搞懂:进程状态藏在哪?

进程不是"黑盒子",它的所有信息------比如在干啥、代码存在哪、有啥权限------都记在一个叫task_struct的"档案本"里。而进程状态,就是这个档案本里的一个整数,像员工的"工作状态标签",直接告诉操作系统"这进程现在能干活不"。

二、课本里的核心逻辑:3句话说透

不管啥操作系统,进程状态的底层逻辑都逃不开这3点(对应你放的课本截图):

  1. 进程不只有"跑"和"不跑",有好几种状态;
  2. 状态能互相转------比如"等键盘输入"时会从"能跑"变"等着",输入完又变回去;
  3. 只有"运行状态"的进程,才真正拿着CPU干活。

三、基础三态:运行、阻塞、挂起(大白话版)

咱们用"工厂干活"的例子理解,比硬记定义简单:

1. 运行态:"在工位上,要么干活要么等活"

操作系统里,每个CPU都有一个"调度队列"(像工厂的"待岗工位"),里面放的全是task_struct(进程档案本)。
只要进程在这个队列里,状态就是运行态(Running) ------不管是正在被CPU"叫去干活",还是排队等CPU,都算运行态。

调度时,CPU会按规则(比如"先进先出"FIFO)从队列里挑一个档案本,找到对应的代码和数据,让它跑起来。

2. 阻塞态:"等材料,暂时离开工位"

比如你写了个带scanf的程序------运行后不敲键盘,进程就"卡住了"。这不是它偷懒,是它在等"键盘输入"这个"材料",这就是阻塞。

操作系统怎么管这种情况?它会给每个硬件(键盘、磁盘、网卡)建个"设备档案"(struct device),档案里专门留了个"等待队列"------所有等这个硬件的进程,都会被移到这个队列里。

简单说:阻塞 = 进程档案本从"调度队列"挪到"硬件等待队列" ,直到"材料"备好(比如你敲了键盘),再挪回调度队列。


3. 挂起态:"工位不够,去临时仓库待着"

如果内存实在不够(工厂工位满了),操作系统会把进程的"代码和数据"挪到磁盘(临时仓库),只留task_struct(档案本)在内存------这叫"阻塞挂起"(本来就在等材料,现在连工具都收起来了)。

要是内存还不够,连调度队列里的进程也会被挪去磁盘,叫"运行挂起"(本来在等活,现在先去仓库)。

不过不用记这么细------Linux里不细分挂起态 ,咱们重点看实际能用的状态就行。


四、内核小知识:用"链表"管进程

操作系统不是瞎管进程的,靠的是"链表"这种数据结构。Linux用的是list_head链表,特点很实用:

  • 每个task_struct里可以放多个list_head(像一个员工有多个"身份标签");
  • 这样一个进程能同时属于多个链表------比如既在"进程组链表",又在"调度队列链表";
  • 想通过list_head找到完整的task_struct?靠"地址偏移"------知道list_head在档案本里的位置,就能算出整个档案本的地址。

简单说:进程状态切换,本质就是list_head在不同链表间"挪位置" ,无非是增删查改的操作。





五、Linux实际进程状态:逐个拆,附代码

Linux的进程状态存在task_state_array里,咱们逐个讲,每个都给代码例子,跟着做就能看懂。

1. R状态:运行态(Running)

  • 特点:在调度队列里,要么正在跑,要么等CPU;不能被kill打断(想停它得等它自己出队列)。
  • 为啥有时查不到?比如printf这种操作太快,进程瞬间切到其他状态,得用"死循环不做IO"才能稳定抓到。

代码例子(抓R状态):

c 复制代码
#include <stdio.h>
int main() {
    while(1); // 死循环,不做任何IO,一直待在调度队列
    return 0;
}
  • 操作步骤:
    1. 编译:gcc test.c -o test
    2. 后台运行(不占终端):./test &
    3. 查看状态:ps aux | grep test,会看到状态是R




(注:状态后的+表示"前台进程",后台进程没有+

2. S状态:可中断休眠(浅睡眠)

  • 特点:等资源(键盘、文件),处于"浅睡";能被kill命令打断(比如等输入时,kill一下就退出)。
  • 最常见的阻塞态,比如scanf等输入、读文件时都算S状态。

代码例子(抓S状态):

c 复制代码
#include <stdio.h>
int main() {
    int a;
    scanf("%d", &a); // 等键盘输入,进程阻塞,状态变S
    printf("%d\n", a);
    return 0;
}
  • 操作步骤:
    1. 运行:./test,不输入任何内容;
    2. 另开终端查状态:ps aux | grep test,状态是S
    3. 测试kill:kill 进程号,进程会直接退出(S状态能被打断)。


3. T状态:暂停态(Stopped)

  • 特点:进程被"暂停",既不在调度队列也不在等待队列;得用命令恢复kill -18)。
  • 触发方式:按ctrl+z暂停前台进程,或用kill -19手动暂停。

操作例子

  1. 运行上面的scanf程序:./test
  2. ctrl+z,终端提示"已暂停";
  3. 查状态:ps aux | grep test,状态是T
  4. 恢复:kill -18 进程号,进程继续等输入;
  5. 再暂停:kill -19 进程号,变回T。

4. t状态:断点态(Trace Stopped)

  • 特点:只有调试时会出现!进程在断点处停下,比如gdb设断点后运行。

操作例子

  1. 用gdb调试:gdb ./test
  2. 设断点:b main(在main函数开头停);
  3. 运行:r
  4. 另开终端查状态:ps aux | grep test,状态是t


5. D状态:不可中断休眠(深睡眠)

  • 特点:等"关键资源"(比如磁盘IO),处于"深睡";kill -9都杀不掉(怕打断磁盘操作导致数据损坏)。
  • 常见场景:用dd命令拷贝大文件时。

操作例子(抓D状态):

  1. 执行磁盘读写命令:dd if=/dev/zero of=/tmp/test bs=1G count=10(往/tmp写10G文件);
  2. 另开终端查状态:ps aux | grep dd,状态是D
  3. 试杀:kill -9 进程号,进程纹丝不动,直到磁盘操作完成才退出。


6. Z状态:僵尸态(Zombie)

  • 特点:子进程退出了,但父进程没"要它的退出信息"(比如退出码),只剩task_struct(档案本)在内存;不能被调度,也杀不掉(因为进程已经死了,只剩空壳)。
  • 风险:僵尸进程占内存,父进程一直不管就会"内存泄露"(尤其是开机就跑的常驻进程)。

代码例子(造僵尸进程):

c 复制代码
#include <stdio.h>
#include <unistd.h>
int main() {
    pid_t pid = fork(); // 创建子进程
    if (pid == 0) {
        // 子进程:直接退出,没被回收
        printf("子进程PID:%d\n", getpid());
        return 0;
    } else if (pid > 0) {
        // 父进程:死循环,不回收子进程
        while(1) sleep(1); 
    }
    return 0;
}
  • 操作步骤:
    1. 运行:./test
    2. 查状态:ps aux | grep test,会看到子进程状态是Z
    3. 解决办法:让父进程调用wait()回收,或杀掉父进程(子进程会被领养)。

(注:slab是Linux内核的内存分配机制,专门管理像task_struct这样的小对象,僵尸进程的task_struct就存在这里,不回收会占 slab 内存)

7. X状态:死亡态(Dead)

  • 特点:进程彻底退出,所有资源(代码、数据、task_struct)被OS回收;看不到这个状态(因为瞬间就没了),只是理论上的状态。

六、孤儿进程:爹跑了,谁管孩子?

如果父进程先退出,子进程没人管,就成了"孤儿进程"------这时候Linux会让1号进程领养它:

  • 新内核(比如Ubuntu、CentOS 7+):1号进程是systemd
  • 老内核:1号进程是init

为啥要领养?

怕孤儿进程退出后没人回收,变成僵尸进程,导致内存泄露。1号进程会负责"要它的退出信息",相当于"福利院"。

代码例子(造孤儿进程):

c 复制代码
#include <stdio.h>
#include <unistd.h>
int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程:睡10秒,等父进程先退出
        printf("子进程PID:%d,当前父进程PID:%d\n", getpid(), getppid());
        sleep(10); 
        printf("子进程现在父进程PID:%d\n", getppid()); // 会变成1
        return 0;
    } else if (pid > 0) {
        // 父进程:马上退出,不管子进程
        printf("父进程退出,PID:%d\n", getpid());
        return 0;
    }
    return 0;
}
  • 操作步骤:
    1. 运行:./test
    2. 父进程瞬间退出,子进程一开始的父进程是终端(比如bash);
    3. 10秒后,子进程的父进程变成1(被1号进程领养);
    4. 孤儿进程会变成后台进程,终端看不到它的输出(除非重定向)。






七、总结:3个核心记牢

  1. 进程状态本质:task_struct在不同链表间挪位置(调度队列、等待队列);
  2. Linux重点状态:R(跑)、S(浅睡等资源)、D(深睡杀不掉)、Z(僵尸要回收)、T(暂停);
  3. 孤儿/僵尸处理:孤儿找1号进程领养,僵尸靠父进程wait()回收,避免内存泄露。
相关推荐
jzwalliser3 小时前
关于Linux生态的补充
linux·语言暴力
半桔3 小时前
【Linux手册】动静态库:从原理到制作
linux·运维·服务器·动态库
z202305083 小时前
Linux之块设备的多队列的实现机制
linux·运维·服务器
liulilittle4 小时前
Unix/Linux 平台通过 IP 地址获取接口名的 C++ 实现
linux·开发语言·c++·tcp/ip·unix·编程语言
阿贤Linux4 小时前
设置网卡名称为传统命名方式
linux·ubuntu
云飞云共享云桌面4 小时前
SolidWorks对电脑的硬件配置要求具体有哪些
java·服务器·前端·网络·数据库
塔子终结者4 小时前
网络安全A模块专项练习任务十解析
java·服务器·网络安全
码界奇点4 小时前
从零构建Linux Shell解释器深入理解Bash进程创建机制
linux·运维·解释器模式·bash·ux·源代码管理
闻道且行之4 小时前
嵌入式|Linux中打开视频流的两种方式V4l2和opencv
linux·笔记·opencv·嵌入式