【Linux 系统篇(十六)】进程(四) : 僵尸进程、孤儿进程、进程优先级


大家好,欢迎来到 huangjin007_ 的博客
个人主页:huangjin007_
🔥 文章收录专栏:Linux 内功修炼手册(系统篇)
总会有一些坚持
能从冰封的土地里
培育出十万朵怒放的蔷薇


Linux 系统篇(十六) ------ 僵尸进程、孤儿进程、进程优先级

文章目录

  • [Linux 系统篇(十六) ------ 僵尸进程、孤儿进程、进程优先级](#Linux 系统篇(十六) —— 僵尸进程、孤儿进程、进程优先级)
    • [一、僵尸进程(Zombie Process)](#一、僵尸进程(Zombie Process))
      • [1.1 什么是僵尸进程?为什么它叫"僵尸"?](#1.1 什么是僵尸进程?为什么它叫“僵尸”?)
      • [1.2 创建一个持续 30 秒的僵尸进程](#1.2 创建一个持续 30 秒的僵尸进程)
      • [1.3 僵尸进程的危害](#1.3 僵尸进程的危害)
        • [1.3.1 为什么说僵尸进程会导致内存泄漏?](#1.3.1 为什么说僵尸进程会导致内存泄漏?)
        • [1.3.2 内核对象申请与 Slab 分配器](#1.3.2 内核对象申请与 Slab 分配器)
      • [1.4 如何避免僵尸进程?](#1.4 如何避免僵尸进程?)
    • [二、孤儿进程(Orphan Process)](#二、孤儿进程(Orphan Process))
      • [2.1 什么是孤儿进程?](#2.1 什么是孤儿进程?)
      • [2.2 观察孤儿进程的收养过程](#2.2 观察孤儿进程的收养过程)
      • [2.3 孤儿进程 vs 僵尸进程](#2.3 孤儿进程 vs 僵尸进程)
    • 三、进程优先级(Priority)
      • [3.1 为什么需要进程优先级?](#3.1 为什么需要进程优先级?)
      • [3.2 查看系统进程的优先级:`ps -l` 命令](#3.2 查看系统进程的优先级:ps -l 命令)
      • [3.3 深入理解 `PRI` 和 `NI`](#3.3 深入理解 PRINI)
        • [3.3.1 什么是 PRI?](#3.3.1 什么是 PRI?)
        • [3.3.2 什么是 NI(nice)?](#3.3.2 什么是 NI(nice)?)
        • [3.3.3 PRI 和 NI 的关系](#3.3.3 PRI 和 NI 的关系)
      • [3.4 如何修改进程的优先级(调整 nice 值)](#3.4 如何修改进程的优先级(调整 nice 值))
      • [3.5 补充重要概念:竞争性、独立性、并行、并发](#3.5 补充重要概念:竞争性、独立性、并行、并发)
      • [3.6 深入理解 nice 值的"饥饿问题"](#3.6 深入理解 nice 值的“饥饿问题”)
      • [3.7 进程与权限:一个小知识](#3.7 进程与权限:一个小知识)
    • 四、面试问题
    • 结语:

一、僵尸进程(Zombie Process)

1.1 什么是僵尸进程?为什么它叫"僵尸"?

  想象你正走在路上,一个人突然从你身边跑过,却脚下一滑,摔倒在地,猝死了。虽然他已经没有了生命体征,但因为死因不明,他的遗体和随身物品(对应子进程的 task_struct、退出码和信号)必须原封不动地保留在原地,任何人不得擅自收尸。这个"等待验尸、尸体尚未处理"的阶段,就是 Linux 中的 僵尸状态

  你马上报警,警察赶到现场:拍照、登记身份信息、查明死因(对应父进程调用 waitpid 获取子进程的退出信息)。一切登记完毕,警察才挥手让救护车把遗体拉走,此时才真正进入死亡状态(X 状态),所有相关资源(task_struct)被释放。

  如果警察一直不来(父进程始终不调用 wait),遗体就会一直留在原地,占用公共空间------这正是僵尸进程的危害所在。

  在 Linux 中,僵尸进程 就是指:一个子进程已经退出(结束运行),但它的父进程还没有调用 wait()waitpid() 来读取这个子进程的退出状态 。此时,子进程虽然已经不再执行任何代码,但它仍然保留在内核的进程表中,占据着一个 task_struct 结构体,处于 Z(Zombie)状态。

  用更准确的话来说:

  • 进程退出后,它的退出状态(例如退出码、是否被信号杀死等)需要被保存下来,以便父进程查询。
  • 这些信息保存在内核的 task_struct(进程控制块,PCB)中。
  • 只要父进程没有读取这些信息,这个 task_struct 就不会被释放,进程就一直处于僵尸状态。

  "僵尸"这个名字很形象:它已经"死"了(退出执行),但它的"尸体"(进程表项)还留在系统里,没有被"收尸"(回收)。

1.2 创建一个持续 30 秒的僵尸进程

  我们写一段简单的 C 代码来亲自制造一个僵尸进程。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    pid_t id = fork();  // 创建子进程

    if (id < 0) 
    {
        perror("fork");
        return 1;
    }
    else if (id > 0) 
    { 
        // 父进程分支
        printf("parent[%d] is sleeping...\n", getpid());
        sleep(30);  // 父进程睡 30 秒,期间不回收子进程
    }
    else 
    {
        // 子进程分支
        printf("child[%d] is begin Z...\n", getpid());
        sleep(5);   // 子进程睡 5 秒后退出
        exit(EXIT_SUCCESS);
    }
    return 0;
}

代码解析

  • fork() 会创建一个新进程。在父进程中返回子进程的 PID(>0),在子进程中返回 0,出错返回 -1。
  • 父进程进入 id > 0 分支,打印自己的 PID 后,调用 sleep(30) 休眠 30 秒。
  • 子进程进入 else 分支,打印自己的 PID 后,调用 sleep(5) 休眠 5 秒,然后调用 exit(EXIT_SUCCESS) 正常退出。

  预期结果 :子进程在 5 秒后退出,但父进程还在睡觉,没有读取子进程的退出状态,因此子进程会变成僵尸进程,持续 25 秒(直到父进程醒来退出或调用 wait)。

步骤

  1. 将上述代码保存为 zombie.c
  2. 编译:gcc zombie.c -o zombie
  3. 在一个终端运行 ./zombie
  4. 在另一个终端中,快速执行 ps -lps -al | grep zombie 来观察进程状态。

观察输出

  运行程序后,立即在另一个终端执行:

bash 复制代码
while true; do ps -o pid,ppid,state,comm -C zombie; sleep 1; done

  或者简单使用:

bash 复制代码
ps -al | grep zombie

  你会看到类似下面的输出(具体 PID 可能不同):

  注意第三行:状态列显示为 Z,并且命令名后有 <defunct>(已失效)。这就证明我们成功制造了一个僵尸进程。

1.3 僵尸进程的危害

  你可能觉得,反正子进程已经退出了,只是留个记录而已,能有多大危害?但实际上,僵尸进程的危害是实实在在的,主要体现在内存泄漏上。

1.3.1 为什么说僵尸进程会导致内存泄漏?
  • 每个进程在内核中都有一个对应的 task_struct 结构体。这个结构体非常大,包含了进程的所有信息:PID、状态、优先级、内存管理信息、文件描述符表、信号处理函数等等。
  • 当进程退出时,大部分资源(如用户空间内存、打开的文件等)会被立即释放。但是,task_struct 本身不会立即释放,因为内核需要保留其中的退出状态(如退出码)供父进程查询。
  • 如果父进程一直不调用 wait(),那么这个 task_struct 就会一直占据内核内存。
  • 如果一个父进程创建了大量子进程,但从不回收它们,就会产生大量僵尸进程,导致内核内存被耗尽,系统性能下降,甚至无法创建新进程。
1.3.2 内核对象申请与 Slab 分配器

  这里涉及一个更深层的知识点:内核是如何管理 task_struct 的?

  • 创建进程时,内核需要从内存中申请一块空间来存放 task_struct
  • 进程退出且未被回收时,这个 task_struct 处于"僵尸态",占用内存但无法使用。
  • 当父进程调用 wait() 回收子进程后,task_struct 被释放。
  • 为了提高效率,内核并不会把释放的 task_struct 直接归还给操作系统,而是放入一个叫做 Slab 分配器 的缓存中。下次创建新进程时,可以直接从缓存中取出一个 task_struct 结构体复用,而不需要重新分配内存。这种机制叫做"数据结构对象的缓存"。

  所以,虽然僵尸进程的 task_struct 最终会被回收,但在回收之前,它一直占据着内存,是一种内存泄漏。尤其是那些常驻内存的进程(如服务器进程),如果它们不断产生僵尸子进程而不回收,内存泄漏会越来越严重。

  面试常问 :僵尸进程会导致内存泄漏吗?为什么?回答要点:是的,因为退出状态保存在 task_struct 中,父进程不读取就一直占用内核内存。

1.4 如何避免僵尸进程?

  避免僵尸进程的关键在于父进程要及时回收子进程。方法主要有:

  • 父进程调用 wait()waitpid() 等待子进程结束并获取其状态。
  • 父进程显式忽略 SIGCHLD 信号(signal(SIGCHLD, SIG_IGN)),这样子进程退出后内核会自动回收,不会产生僵尸进程。
  • 使用双 fork() 技巧:父进程先 fork 一个子进程,子进程再 fork 一个孙进程,然后子进程立即退出,孙进程被 1 号进程收养,由 1 号进程负责回收。这样原父进程就不需要关心孙进程的回收了。

  这些方法会在后续讲解时详细展开,目前只需要知道"父进程必须负责回收"即可。


二、孤儿进程(Orphan Process)

2.1 什么是孤儿进程?

  与僵尸进程相反,孤儿进程 是指:父进程先于子进程退出,导致子进程失去了父进程,变成了"孤儿"

  那么问题来了:父进程死了,子进程的退出状态谁来读取?如果没有进程来回收,子进程退出后不也会变成僵尸吗?

  Linux 的设计非常巧妙:当一个进程的父进程退出时,它的子进程会被自动"领养"给 1 号进程(init 或 systemd) 。1 号进程是系统启动的第一个进程,负责管理系统中的所有进程,它会定期调用 wait() 来回收这些被领养的孤儿进程,因此孤儿进程不会变成僵尸。

所以,孤儿进程虽然暂时失去了生父,但会有一个"养父"(1 号进程)负责它的善后工作。

2.2 观察孤儿进程的收养过程

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t id = fork();

    if (id < 0) {
        perror("fork");
        return 1;
    }
    else if (id == 0) {
        // 子进程
        printf("I am child, pid : %d, ppid : %d\n", getpid(), getppid());
        sleep(10);  // 子进程睡 10 秒,确保父进程已经退出
        printf("child after sleep, ppid : %d\n", getppid()); // 打印新的父进程PID
        exit(0);
    }
    else {
        // 父进程
        printf("I am parent, pid: %d\n", getpid());
        sleep(3);   // 父进程只睡 3 秒,然后退出
        exit(0);
    }
    return 0;
}

运行并观察

  1. 编译运行程序。
  2. 父进程在 3 秒后退出。
  3. 子进程在 10 秒后退出。在父进程退出后、子进程退出前,子进程的父进程 ID 会变成 1(或 systemd 的 PID,通常是 1)。

预期输出(可能略有不同):

  看到最后一行,子进程的 PPID 变成了 1,说明它已经被 1 号进程收养。

2.3 孤儿进程 vs 僵尸进程

特征 僵尸进程 孤儿进程
产生条件 子进程退出,父进程还在运行但不调用 wait() 父进程先退出,子进程还在运行
状态 子进程处于 Z 状态,占用进程表 子进程正常运行,只是父进程变成 1 号进程
危害 导致内存泄漏(task_struct 不被释放) 一般无危害,因为 1 号进程会负责回收
处理者 需要父进程主动回收 由 1 号进程(init/systemd)自动收养并回收
本质 父进程"不作为" 父进程"提前离场"

三、进程优先级(Priority)

3.1 为什么需要进程优先级?

  在多任务操作系统中,CPU 的数量是有限的(通常只有几个核心),而同时运行的进程可能成百上千。CPU 必须决定"先执行谁,后执行谁"。进程优先级就是决定进程获取 CPU 资源先后顺序的规则

  • 优先级高的进程:优先获得 CPU 时间,运行得更快、更及时。
  • 优先级低的进程:需要等待,可能延迟较久。

3.2 查看系统进程的优先级:ps -l 命令

  在 Linux 或 Unix 系统中,使用 ps -l 命令可以查看当前终端相关进程的详细信息。例如:

bash 复制代码
ps -l

输出类似:

重点关注以下几列:

  • UID:进程执行者的用户 ID,表示这个进程属于哪个用户。
  • PID:进程 ID,进程的唯一标识。
  • PPID:父进程 ID,表示这个进程是由哪个进程创建的。
  • PRI :进程的优先级(Priority),数值越小,优先级越高,越早被执行
  • NI:进程的 nice 值,是优先级的修正数值。

3.3 深入理解 PRINI

3.3.1 什么是 PRI?

  PRI 就是进程的实际优先级。在 Linux 中,普通进程的 PRI 默认值是 80 , PRI 值越小,进程越优先被调度。

3.3.2 什么是 NI(nice)?

  NI 是 nice 值,表示进程的"谦让程度"。nice 的英文原意是"友好的、和善的"。在 Linux 中,nice 值越大,表示进程越"谦让",愿意把 CPU 让给别人,所以它的优先级就越低。反之,nice 值越小(甚至为负数),表示进程越"霸道",优先级越高。

nice 值的取值范围是 -20 到 19,共 40 个级别。

3.3.3 PRI 和 NI 的关系

  很多人会混淆 PRI 和 NI,以为它们是同一个东西。其实不是。NI 只是 PRI 的一个修正量,它们的关系如下:

复制代码
PRI(new) = PRI(old) + NI

  但更准确的表述是,在 Linux 的普通进程调度中:

复制代码
实际优先级 PRI = 默认优先级(80) + nice 值

  也就是说,普通进程的 PRI 默认是 80,然后加上 NI 值得到最终的 PRI

  • 如果 NI = 0,则 PRI = 80
  • 如果 NI = -20(最小 nice 值,最高优先级),则 PRI = 60(80 - 20 = 60,数值更小,优先级更高)。
  • 如果 NI = 19(最大 nice 值,最低优先级),则 PRI = 99(80 + 19 = 99,数值更大,优先级更低)。

注意:为什么说"普通进程默认 80"?因为 Linux 将优先级分为两个范围:

  • 实时优先级 :范围 0~99,数值越小优先级越高。实时进程用于对时间要求非常严格的任务(如音频处理、工业控制)。实时进程的 PRI 值通常在 0~99 之间,并且它们的调度策略不同。
  • 普通优先级 :范围 100~139,对应 PRI 显示为 80~119(实际上内核内部值映射后,ps 显示的 PRI 默认是 80)。普通进程是我们通常创建的那些进程。

  当我们用 ps -l 看到 PRI 为 80 时,就是普通进程的默认优先级。我们只需要记住:普通进程的 PRI 范围大约是 80~99,通过调整 nice 值(-20~19)来改变

  面试常问PRINI 的区别?答:PRI 是实际优先级,NI 是 nice 值,是对优先级的修正。公式:PRI = 80 + NINI 越小,优先级越高。

3.4 如何修改进程的优先级(调整 nice 值)

  调整进程优先级本质上就是修改它的 nice 值。有三种常用方式:

方法一:使用 top 命令动态修改

  top 是 Linux 下常用的性能监控工具。进入 top 界面后,你可以修改某个运行中进程的 nice 值。

步骤:

  1. 在终端输入 top 并回车。
  2. 按下 r 键(renice 的缩写)。
  3. 提示 PID to renice:,输入你想要修改的进程 PID,回车。
  4. 提示 Renice PID to value:,输入新的 nice 值(范围 -20 到 19),回车。

  注意:普通用户只能调高 nice 值(降低优先级),只有 root 用户可以调低 nice 值(提高优先级)。

方法二:使用 nicerenice 命令

  • nice 命令:在启动进程时指定 nice 值。

    bash 复制代码
    nice -n -5 ./myprogram   # 以 nice 值 -5 启动 myprogram

    如果不指定,默认 nice 值为 0。

  • renice 命令:修改已经运行进程的 nice 值。

    bash 复制代码
    renice -10 -p 1234    # 将 PID 为 1234 的进程 nice 值改为 -10

方法三:使用系统调用(编程方式)

  在 C 程序中,可以使用以下系统调用来获取或设置进程的优先级:

c 复制代码
#include <sys/time.h>
#include <sys/resource.h>

int getpriority(int which, int who);
int setpriority(int which, int who, int prio);
  • which:指定操作对象类型,例如 PRIO_PROCESS(进程)、PRIO_PGRP(进程组)、PRIO_USER(用户)。
  • who:对应 which 的 ID,如果为 0 表示当前进程/进程组/用户。
  • prio:新的 nice 值(范围 -20 到 19)。
  • 返回值:getpriority 返回当前 nice 值(可能为负数,所以要用 errno 判断错误),setpriority 成功返回 0,失败返回 -1。

示例:获取并设置当前进程的 nice 值

c 复制代码
#include <stdio.h>
#include <sys/resource.h>
#include <sys/time.h>
#include <errno.h>

int main() 
{
    int prio = getpriority(PRIO_PROCESS, 0);
    printf("Current nice value: %d\n", prio);

    // 尝试将 nice 值设置为 5(降低优先级)
    if (setpriority(PRIO_PROCESS, 0, 5) == -1) 
    {
        perror("setpriority");
        return 1;
    }
    prio = getpriority(PRIO_PROCESS, 0);
    printf("New nice value: %d\n", prio);
    return 0;
}

3.5 补充重要概念:竞争性、独立性、并行、并发

  • 竞争性 :系统中有大量进程,但 CPU 资源非常有限(甚至只有 1 个),所以进程之间必然存在竞争关系。为了更高效地完成任务,合理分配资源,操作系统引入了优先级机制,让重要的进程优先运行。
  • 独立性:多个进程同时运行,每个进程都认为自己独占所有资源(内存、CPU 等),互不干扰。这是通过虚拟内存、时间片轮转等机制实现的。独立性保证了进程之间不会因为对方的错误而崩溃。
  • 并行(Parallelism) :当系统有多个 CPU 核心时,多个进程可以在同一时刻真正地同时运行,每个核心运行一个进程。这叫做并行。
  • 并发(Concurrency):当系统只有一个 CPU 核心(或核心数少于进程数)时,操作系统通过快速切换进程,让每个进程都执行一小段时间(时间片),从宏观上看好像多个进程在同时运行,但微观上任意时刻只有一个进程在执行。这叫做并发。

类比

  • 并行:多个窗口同时服务多个顾客(每个窗口一个顾客)。
  • 并发:一个窗口,营业员快速轮换服务多个顾客,每个顾客服务几秒钟就切换,看起来像同时服务。

  它们与优先级的关系:在并发环境下,调度器根据优先级决定哪个进程先获得时间片;在并行环境下,优先级也决定了哪个进程优先被分配到空闲的 CPU 核心。

3.6 深入理解 nice 值的"饥饿问题"

  前面提到,nice 值越小,优先级越高。如果某个进程的 nice 值设置得非常低(例如 -20),它就会获得大量 CPU 时间。如果系统中同时有很多低优先级进程(nice 值很高,如 19),那么这些低优先级进程可能很长时间得不到 CPU 资源,导致进程饥饿(Starvation)

  进程饥饿是指:一个进程长期无法获得所需的资源(如 CPU 时间),导致无法推进。在极端情况下,低优先级进程可能永远无法运行,就像一个排队的人总是被插队,永远轮不到他。

  因此,Linux 的调度器(如 CFS,完全公平调度器)并不是简单的"优先级高就完全独占",而是会保证每个进程都能获得一定的 CPU 时间,只是高优先级进程获得的时间片更多。这样既保证了高优先级任务的响应速度,又避免了低优先级任务被饿死。

  注意:普通用户只能调高自己进程的 nice 值(降低优先级),而不能调低 nice 值(提高优先级),这是为了防止普通用户抢占系统资源。只有 root 用户才能设置负的 nice 值。

3.7 进程与权限:一个小知识

  你可能知道 Linux 文件有权限设置,比如读、写、执行,并且文件有所有者(UID)。但是,系统是怎么知道"谁"在访问文件的呢?

  答案是:进程 。进程是资源访问的主体。当你运行一个程序时,这个程序对应的进程会携带你的用户身份(UID)。当进程去访问文件时,内核根据进程的 UID 来判断是否有权限。所以,实际上是进程在代表用户访问资源

  理解这一点很重要:进程的优先级、权限等属性都是记录在 task_struct 中的,包括它的 UID、GID、优先级等。当进程尝试打开文件时,内核会检查进程的凭证来决定是否允许访问。


四、面试问题

Q1:什么是僵尸进程?如何产生?如何避免?

A:子进程退出后,父进程没有调用 wait() 回收,导致子进程的 task_struct 保留在进程表中,处于 Z 状态。避免方法:父进程调用 wait/waitpid,或忽略 SIGCHLD 信号,或使用双 fork

Q2:僵尸进程有什么危害?

A:导致内核内存泄漏,因为 task_struct 一直占用内存。大量僵尸进程会耗尽系统资源。

Q3:什么是孤儿进程?它会被谁收养?

A:父进程先退出,子进程还在运行,子进程成为孤儿进程。它会被 1 号进程(init/systemd)收养并回收。

Q4:PRINI 的区别?

A:PRI 是进程的实际优先级,数值越小优先级越高。NI 是 nice 值,是对优先级的修正量。普通进程的 PRI = 80 + NINI 范围 -20~19。

Q5:如何调整进程优先级?

A:可以使用 top 命令(按 r 输入 PID 和 nice 值)、nice/renice 命令,或在代码中使用 setpriority 系统调用。

Q6:并行和并发有什么区别?

A:并行是多个进程在多个 CPU 核心上同时运行;并发是多个进程在一个 CPU 上通过时间片轮转快速切换,宏观上同时运行。

Q7:nice 值设置不当会导致什么问题?

A:如果某个进程 nice 值过低(优先级高),可能导致其他低优先级进程得不到 CPU 时间,产生饥饿。现代调度器会有公平机制来缓解。


结语:

  今天的内容到这里就结束了,希望你能有所收获~

干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

相关推荐
cui_hao_nan20 分钟前
Git常用命令1
运维·git
IvorySQL22 分钟前
PostgreSQL 日报|备用服务器 FSM 数据不一致(9 月 2 日)
服务器·数据库·postgresql
瞬间&永恒~28 分钟前
【Kubernetes】(十五)维护与升级、ETCD 备份和恢复
linux·运维·docker·云原生·kubernetes
susplus37 分钟前
【linux应用软件编程】进程间的通信方式2【网络通信从入门到UDP编程】
linux·udp·ip
MSTcheng.38 分钟前
【Linux】Linux学习第三弹——Linux基本指令2
linux·windows·学习·ubuntu·操作系统
姚永强43 分钟前
gitlab安装
运维·gitlab
希望奇迹很安静44 分钟前
Linux基本使用命令
linux·运维·服务器
邪修king1 小时前
Re:Linux 系统篇(十七):进程篇(六):环境变量深度解析|命令行参数、环境变量表、4 种获取方式与继承机制万字详解
linux·服务器·c++
姚永强1 小时前
安装Jenkins
运维·jenkins