进程状态与管理

进程状态与管理

1. 进程状态理论概述

在操作系统理论中,进程状态描述了进程在其生命周期中的不同阶段。在 Linux 环境下,进程状态主要有以下五种。需要注意的是,不同语境或理论体系下描述可能略有区别,本课程基于 Linux 内核视角。

1.1 五种基本状态

  1. 就绪态 (Ready)
    • 进程已创建,具备运行条件,但尚未获得 CPU 资源。
    • 原因:CPU 资源有限,进程数量远多于 CPU 核心数,需排队等待调度。
    • 转换:当调度器分配 CPU 时间片后,转为运行态。
  2. 运行态 (Running)
    • 进程正在 CPU 上执行指令。
    • 转换:时间片用完或被更高优先级进程抢占时,转回就绪态。
  3. 睡眠态/阻塞态 (Sleep/Blocked)
    • 进程因等待某些事件(如 I/O 资源、用户输入)而主动放弃 CPU。
    • 也称为 阻塞态等待态
    • 转换:当等待的资源到位(如键盘输入完成),转回就绪态。
    • 分类
      • 可中断睡眠 (Interruptible Sleep):浅度睡眠。可以被信号(Signal)打断唤醒。
      • 不可中断睡眠 (Uninterruptible Sleep):深度睡眠。通常涉及硬件交互,不能被信号打断,必须等待硬件响应。
  4. 停止态/暂停态 (Stop/Stopped)
    • 进程被强制暂停执行,并非永久终止。
    • 英文对应 Stopped,有时直译为“停止”,但实际含义是“暂停”。
    • 原因:通常由信号引起(如用户按下 Ctrl+Z),或调试器控制。
    • 区别:与睡眠态不同,睡眠是进程主动等待资源,停止态往往是被动被外部信号挂起。即使资源到位,若未收到继续信号,进程也不会运行。
    • 转换:收到继续信号(如 fg, bg)后,转回就绪态。
  5. 僵尸态 (Zombie)
    • 进程已终止,但其进程描述符(PCB)仍保留在内核进程表中。
    • 原因:子进程结束,但父进程尚未读取其退出状态。
    • 特点:无法再次运行,必须被父进程回收资源后才能彻底消失。

1.2 状态转换逻辑

  • 创建:使用 fork 系统调用创建进程,初始进入就绪态。
  • 调度:就绪态 ​\leftrightarrow 运行态(取决于 CPU 调度)。
  • 阻塞:运行态 ​\rightarrow 睡眠态(调用 sleep, scanf 等等待资源)。
  • 暂停:运行态 ​\rightarrow 停止态(收到 SIGSTOPCtrl+Z)。
  • 终止:运行态 ​\rightarrow 僵尸态(进程代码执行完毕或调用 exit,等待父进程 wait)。

2. Linux 进程状态实验环境与工具

实验主要在 Linux 终端下进行,涉及编译、运行及监控工具。

2.1 常用命令

  • gcc: 编译器。
    • 示例:gcc test.c -o test(编译并指定输出文件名)。
    • 示例:gcc -Wall test.c(显示所有警告)。
  • top: 实时监控系统进程状态。
    • 用法:top -p <PID>(监控指定进程)。
    • 状态列标识:
      • R: Running (运行态)
      • S: Sleep (睡眠态)
      • T: Stop (停止态)
      • Z: Zombie (僵尸态)
      • I: Idle (空闲)
  • ps: 进程快照。
    • 用法:ps -ef | grep <process_name>
  • kill: 发送信号。
    • 用法:kill -9 <PID>(发送 SIGKILL 信号强制终止)。

2.2 前台与后台进程控制

  • 前台运行: 进程占用终端,可接收键盘信号。
  • 后台运行: 进程在后台执行,通常使用 & 启动或 bg 命令。
  • 快捷键操作:
    • Ctrl+C: 发送 SIGINT 信号,终止前台进程。
    • Ctrl+Z: 发送 SIGTSTP 信号,暂停前台进程(进入停止态)。
    • fg: 将后台或暂停的作业调到前台继续运行。
    • bg: 将暂停的作业调到后台继续运行。
  • 注意: Ctrl+CCtrl+Z 仅对前台进程有效。若进程在后台运行,终端键盘信号无法直接作用于该进程。

3. 进程状态实验步骤详解

3.1 实验一:观察运行态 (Running)

  1. 代码准备 (test.c):

    • 逻辑:无限循环打印字符 1
    • 关键点:使用 fflush(stdout) 刷新缓冲区,确保字符立即显示。
    • 自定义延时函数:通过空循环占用 CPU 时间,模拟繁忙状态(区别于 sleep 让出 CPU)。
    while(1) {
        printf("1");
        fflush(stdout);
        delay(); // 自定义 CPU  busy 循环
    }
    
  2. 运行与监控:

    • 编译:gcc test.c -o test
    • 运行:./test & (后台运行,获取 PID)。
    • 监控:新开终端,top -p <PID>
    • 现象: 状态显示为 R,表示进程正在占用 CPU 运行。
    • 说明: 就绪态很难捕捉,因为 CPU 切换极快,通常看到的都是运行态。

3.2 实验二:观察停止态 (Stopped)

  1. 操作步骤:
    • 将进程调至前台:fg
    • 发送暂停信号:按下 Ctrl+Z
    • 现象: 终端提示 Stopped,进程不再输出字符。
    • 监控:top -p <PID> 显示状态为 T
  2. 恢复与终止:
    • 恢复运行:bg (后台) 或 fg (前台)。
    • 终止进程:前台状态下按 Ctrl+C
    • 现象: 进程消失,top 中不再显示。

3.3 实验三:观察睡眠态 (Sleep)

  1. 场景 A: 时间睡眠

    • 命令:sleep 100
    • 监控:top 显示状态为 S
    • 原理:进程主动让出 CPU,等待定时器中断。
  2. 场景 B: 资源等待睡眠

    • 代码:使用 scanf 等待用户输入。
    scanf("%d", &val); // 等待键盘输入
    
    • 监控:在输入前,top 显示状态为 S
    • 原理:进程阻塞在 I/O 操作上,属于可中断睡眠。

4. 僵尸进程 (Zombie Process)

4.1 产生原因

  • 场景: 父进程创建子进程 (fork),子进程先于父进程终止 (exit)。
  • 机制: 子进程终止后,内核需要保留其进程表项(PCB),以便父进程获取子进程的退出状态(返回值、终止信号等)。
  • 状态: 此时子进程进入 僵尸态 (Z)
  • 代码示例:
    pid_t cpid = fork();
    if (cpid == 0) {
        // 子进程
        exit(0); // 子进程立即退出
    } else {
        // 父进程
        while(1); // 父进程死循环,不调用 wait
    }
    
  • 结果: pstop 中可见子进程状态为 Z

4.2 危害

  • 资源占用: 僵尸进程不占用内存代码段,但占用内核进程表项。
  • 风险: 若大量进程变为僵尸态,会导致进程表满,系统无法创建新进程,甚至崩溃。
  • 避免方法: 父进程必须及时回收子进程资源。

4.3 解决方案

  • 父进程调用 waitwaitpid 系统调用。
  • 作用:阻塞父进程直到子进程结束,并读取子进程退出状态,随后内核彻底释放子进程资源。

5. wait 系统调用详解

5.1 函数原型

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
  • 参数: status 是一个整型指针,用于存储子进程的退出状态信息。若不需要可传 NULL
  • 返回值:
    • 成功:返回终止的子进程 PID。
    • 失败:返回 -1 (例如没有子进程)。
  • 行为: 阻塞调用。若子进程未结束,父进程会进入睡眠态等待。

5.2 退出状态分析宏

通过 status 变量分析子进程终止原因,需使用以下宏:

  1. 正常退出判断:
    • WIFEXITED(status): 若子进程正常终止(调用 exit 或返回),返回真。
    • WEXITSTATUS(status): 若 WIFEXITED 为真,获取子进程的返回值(exit code)。
  2. 信号终止判断:
    • WIFSIGNALED(status): 若子进程被信号终止,返回真。
    • WTERMSIG(status): 若 WIFSIGNALED 为真,获取终止该进程的信号编号。

5.3 实验示例

  • 正常退出: 子进程 exit(10)。父进程 wait 后,WIFEXITED 为真,WEXITSTATUS 为 10。
  • 信号终止: 子进程运行中,父进程或其他进程发送 kill -9 (SIGKILL)。父进程 wait 后,WIFSIGNALED 为真,WTERMSIG 为 9。
  • 错误处理: 若父进程循环调用 wait 但子进程已结束,wait 返回 -1,错误码为 ECHILD (No child processes)。

5.4 多子进程处理

  • 若父进程创建多个子进程,需循环调用 wait 直到返回 -1
  • 示例逻辑:
    while(1) {
        pid_t pid = wait(&status);
        if (pid < 0) break; // 无子进程可等待
        // 处理 status
    }
    

6. waitpid 系统调用详解

6.1 函数原型

pid_t waitpid(pid_t pid, int *status, int options);
  • 相比 waitwaitpid 提供了更灵活的控制。

6.2 参数详解

  1. pid (等待哪个进程):
    • -1: 等待任意子进程(等效于 wait)。
    • >0: 等待指定 PID 的子进程。
    • 0: 等待同一进程组的任意子进程。
    • <-1: 等待指定进程组的任意子进程。
  2. status: 同 wait,存储退出状态。
  3. options (选项):
    • 0: 阻塞等待(默认行为)。
    • WUNTRACED: 若子进程被停止(而非终止),也返回。用于调试器跟踪。
    • WNOHANG: 非阻塞模式。若子进程未结束,立即返回 0,而不阻塞父进程。

6.3 高级用法示例

  1. 等待指定子进程:
    • 设置 pid 为特定子进程 ID,仅回收该进程资源。
  2. 非阻塞轮询 (WNOHANG):
    • 场景:父进程需同时处理其他任务,不能一直阻塞在 wait 上。
    • 逻辑:
      while(1) {
          pid_t pid = waitpid(-1, &status, WNOHANG);
          if (pid > 0) {
              // 有子进程结束,处理回收
          } else if (pid == 0) {
              // 无子进程结束,继续做其他事
          } else {
              // 出错或无子进程
              break;
          }
          // 执行其他任务
      }
      
  3. 跟踪停止状态 (WUNTRACED):
    • 配合 WUNTRACED,当子进程被信号停止时,waitpid 也会返回,父进程可获取子进程停止的状态信息。

6.4 注意事项

  • 头文件: 需包含 <sys/types.h>, <sys/wait.h>, <unistd.h>, <stdio.h>, <stdlib.h>
  • 返回值检查: 始终检查 waitpid 返回值,区分成功 PID、0 (非阻塞无结果) 和 -1 (错误)。
  • 资源回收: 无论使用 wait 还是 waitpid,核心目的都是避免僵尸进程,确保内核资源释放。
进程:创建、终止 2026-03-05
system()、exec族 2026-03-09

评论区