欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 国际 > 六十天Linux从0到项目搭建(第十四天)(进程退出情况、理解进程退出、进程等待、wait/waitpid)

六十天Linux从0到项目搭建(第十四天)(进程退出情况、理解进程退出、进程等待、wait/waitpid)

2025/4/3 3:20:45 来源:https://blog.csdn.net/weixin_73266891/article/details/146798519  浏览:    关键词:六十天Linux从0到项目搭建(第十四天)(进程退出情况、理解进程退出、进程等待、wait/waitpid)

进程退出的情况分类

进程的退出状态可以分为两大类:

A. 正常执行完毕
  1. 结果正确

    • 退出码(Exit Code)为 0,表示程序按预期完成。

    • 示例

      int main() { return 0; }  // 退出码 0
      
      ./mytest
      echo $?  # 输出 0
  2. 结果不正确

    • 退出码为非 0,表示程序执行异常(如逻辑错误、文件未找到等)。

    • 示例

      int main() { return 1; }  // 退出码 1(表示某种错误)
      
      ./mytest
      echo $?  # 输出 1
    • 为什么需要不同的退出码?

      • 供脚本或调用者判断程序失败的具体原因(如 1 表示文件缺失,2 表示权限不足等)。

B. 崩溃(进程异常终止)
  • 崩溃的本质:进程因非法操作(如段错误、除零错误)收到操作系统的 信号(Signal) 而强制终止。

  • 常见信号

    信号含义触发场景
    SIGSEGV段错误(非法内存访问)解引用空指针、数组越界
    SIGFPE算术错误除零、整数溢出
    SIGKILL强制终止(kill -9无法被捕获或忽略
  • 示例

    int main() {int *p = NULL;*p = 10;  // 触发 SIGSEGV(段错误)return 0;
    }
    
    ./mytest  # 输出 "Segmentation fault (core dumped)"
    echo $?   # 输出 139(128 + 信号编号 11,即 SIGSEGV)

2. 退出码(Exit Code)与信号(Signal)的关系

退出状态获取方式取值范围含义
正常退出return 或 exit0(成功)程序主动退出,结果正确/错误
1-255(错误)程序主动退出,但逻辑异常
崩溃(信号终止)被信号杀死128 + 信号编号进程因非法操作被 OS 终止

示例

./mytest && echo "Success" || echo "Failed"  # 根据退出码判断

3. 如何查看进程退出状态?

  • echo $?:查看上一个命令的退出码(仅保留最近一次)。

  • 结合信号

    ./mytest
    if [ $? -eq 0 ]; thenecho "Success";
    elif [ $? -eq 1 ]; thenecho "File not found";
    elif [ $? -gt 128 ]; thenecho "Crashed with signal $(( $? - 128 ))";
    fi

4. 用户如何利用退出码?

  1. 脚本自动化:根据退出码决定后续操作(如重试或报警)。

    ./download_file.sh
    case $? in0) echo "Download succeeded";;1) echo "Network error";;2) echo "Disk full";;*) echo "Unknown error";;
    esac
  2. 调试程序:通过非 0 退出码定位错误类型。


5. 总结

  • 正常退出0 表示成功,1-255 表示错误(自定义语义)。

  • 崩溃退出:由信号触发,退出码 = 128 + 信号编号(如 139 = 128 + 11)。

  • $?:仅保留最近一次进程的退出状态,需及时检查。

关键点
退出码是进程与调用者的“约定”,用于传递执行结果。
信号是 OS 对非法操作的强制干预,无法被忽略(如 SIGKILL)。
合理设计退出码,方便调试和自动化管理

如何理解进程退出?——操作系统如何清理“死亡”的进程

进程退出(终止)时,操作系统需要彻底清理它占用的资源,包括 内核数据结构、内存、文件、信号等,否则会导致 内存泄漏、资源浪费,甚至系统不稳定


1. 进程退出的两种方式

(1)正常退出

  • 主动调用 exit() 或 return

    int main() {return 0;  // 或 exit(0);
    }
    • 退出码(Exit Code)0 表示成功,非 0 表示错误(如 1 表示文件未找到)。

    • 父进程可通过 wait() 获取退出状态

(2)异常终止

  • 被信号(Signal)杀死

    • SIGSEGV(段错误)、SIGKILL(强制终止)、SIGTERM(优雅终止)。

    • 示例

      int main() {int *p = NULL;*p = 10;  // 触发 SIGSEGV,崩溃
      }
    • 退出码 = 128 + 信号编号(如 SIGSEGV 是 11,退出码 139)。


2. 进程退出时,操作系统做了什么?

(1)释放用户态资源

资源释放方式
代码和数据如果是独立地址空间(非共享),直接释放物理内存(通过页表清除映射)。
堆内存malloc 分配的内存由 OS 回收(除非泄漏,否则无需手动 free)。
文件描述符关闭所有打开的文件(如 socketopen() 的文件)。
工作目录无影响(每个进程的 cwd 是独立的)。

(2)清理内核数据结构

数据结构释放方式
task_struct删除进程控制块(PCB),移除进程列表。
页表(Page Table)清除虚拟→物理内存映射,释放物理页(如代码、数据、堆栈)。
信号处理表清除注册的信号处理器(如 signal(SIGINT, handler))。
定时器取消未触发的定时器(如 alarm())。

(3)通知父进程

  • 父进程调用 wait():获取子进程的退出状态(避免僵尸进程)。

  • 如果父进程先退出:子进程由 init 进程(PID=1)接管,最终被回收。


3. 关键问题

Q1:如果父进程不调用 wait() 会怎样?

  • 僵尸进程(Zombie)

    • 子进程退出后,task_struct 仍保留(直到父进程读取退出状态)。

    • 占用内核资源,但无法被 kill 杀死。

    • 解决方法

      • 父进程显式调用 wait()

      • 父进程忽略 SIGCHLD 信号(signal(SIGCHLD, SIG_IGN))。

Q2:exit() 和 _exit() 的区别?

函数行为
exit()1. 刷新 stdio 缓冲区(如 printf 的内容)。
2. 调用 atexit() 注册的函数。
_exit()直接终止进程,不清理缓冲区(适合子进程在 fork() 后立即退出)。

示例

int main() {printf("Hello");  // 无换行,缓冲区未刷新exit(0);          // 输出 "Hello"(缓冲区被刷新)// _exit(0);      // 无输出(缓冲区未刷新)
}

Q3:进程退出的完整流程?

  1. 用户态清理

    • 调用 exit() → 执行 atexit() 注册的函数 → 刷新 stdio 缓冲区。

  2. 内核态清理

    • 释放内存、文件、信号等资源 → 删除 task_struct → 通知父进程。

  3. 调度新进程

    • OS 从就绪队列选择下一个进程运行。


4. 现实类比

  • 进程退出 ≈ 退房手续

    • 正常退房(exit():结清费用(关闭文件)、归还钥匙(释放内存)、注销登记(删除 task_struct)。

    • 强制退房(kill -9:酒店经理(OS)直接清空房间,不关心未保存的数据(缓冲区丢失)。

  • 僵尸进程 ≈ 已退房但未结账

    • 房间无人住,但酒店系统仍保留记录(需前台手动清理)。


5. 总结

阶段操作
用户态exit() 刷新缓冲区,return 返回退出码。
内核态释放内存、文件、信号,删除 task_struct,通知父进程。
父进程通过 wait() 回收子进程,避免僵尸进程。

核心原则
OS 必须彻底清理退出的进程,否则资源泄漏会拖慢系统。
僵尸进程是“半死不活”的,需父进程或 init 回收。
exit() 比 _exit() 更安全,确保数据不丢失

深入理解进程等待(wait/waitpid

——为什么需要等待?如何获取子进程状态?父进程在等什么?


1. 为什么需要进程等待?

(1)避免内存泄漏(必须做!)

  • 子进程退出后,内核会保留部分信息(如 task_struct、退出状态),直到父进程调用 wait/waitpid 读取。

  • 如果不等待

    • 子进程变成 僵尸进程(Zombie),占用内核资源(如 PID、进程表项)。

    • 僵尸进程过多会导致 系统无法创建新进程

(2)获取子进程执行结果(可选)

父进程可能需要知道子进程的终止状态,分为三种情况:

  1. 代码跑完,结果正确 → 退出码 0

  2. 代码跑完,结果错误 → 非 0 退出码(如 1 表示文件未找到)。

  3. 代码运行异常 → 被信号终止(如 SIGSEGV)。

总结

  • 等待 = 通过系统调用获取子进程退出状态 + 释放内核资源


2. 如何等待?——wait 和 waitpid

(1)wait:等待任意一个子进程

#include <sys/wait.h>
pid_t wait(int *status);
  • 参数

    • status:输出型参数,存储子进程的退出状态(需用宏解析)。

  • 返回值

    • 成功:返回被回收的子进程 PID。

    • 失败:返回 -1(如没有子进程)。

(2)waitpid:等待指定子进程

pid_t waitpid(pid_t pid, int *status, int options);
  • 参数

    • pid

      • > 0:等待指定的子进程(PID)。

      • -1:等待任意子进程(等效于 wait)。

    • status:同 wait

    • options

      • 0:阻塞等待(子进程不退出,父进程一直等)。

      • WNOHANG:非阻塞(子进程未退出时,父进程继续执行)。


3. 父进程在 wait 时在干什么?

  • 如果子进程已退出

    • 父进程立即读取其退出状态,释放内核资源。

  • 如果子进程未退出

    • 默认(options=0:父进程 阻塞(挂起),直到子进程退出。

    • WNOHANG:父进程 不阻塞,直接返回 0(子进程仍在运行)。

示例

int status;
pid_t pid = fork();
if (pid == 0) {// 子进程sleep(2);exit(123);  // 退出码 123
} else {// 父进程waitpid(pid, &status, 0);  // 阻塞等待if (WIFEXITED(status)) {printf("Child exit code: %d\n", WEXITSTATUS(status));  // 输出 123}
}

4. 如何解析 status

status 是一个 位图,结构如下(假设 32 位):

复制

00000000 00000000 00000000 00000000
|------| |------| |------| |------|保留    退出码    信号编号   核心转储标志
  • 关键宏

    • WIFEXITED(status):子进程是否正常退出(true 表示调用 exit 或 return)。

    • WEXITSTATUS(status):获取退出码(仅当 WIFEXITED 为真时有效)。

    • WIFSIGNALED(status):子进程是否被信号终止。

    • WTERMSIG(status):获取终止信号的编号(如 SIGSEGV 是 11)。

示例

if (WIFEXITED(status)) {printf("Exit code: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {printf("Killed by signal: %d\n", WTERMSIG(status));
}

5. 关键问题

Q1:wait 和 waitpid 的区别?

函数等待目标阻塞行为灵活性
wait任意子进程必须阻塞低(无法指定 PID)
waitpid可指定 PID可阻塞或非阻塞高(支持 WNOHANG

Q2:如果父进程不等待,子进程会怎样?

  • 僵尸进程:子进程退出后,内核保留其 task_struct 直到父进程调用 wait

  • 最终由 init 进程回收:如果父进程先退出,子进程由 init(PID=1)接管并回收。

Q3:如何避免僵尸进程?

  1. 显式调用 wait/waitpid(推荐)。

  2. 忽略 SIGCHLD 信号

    signal(SIGCHLD, SIG_IGN);  // 子进程退出时自动回收
  3. fork 两次(“孙子进程”由 init 直接回收)。


6. 总结

操作目的
wait阻塞等待任意子进程退出,避免僵尸进程。
waitpid更灵活(可指定 PID、非阻塞)。
解析 status使用 WIFEXITEDWEXITSTATUS 等宏判断退出状态。

核心结论
父进程必须等待子进程,否则会导致僵尸进程泄漏。
status 是位图,需用宏解析退出码或信号。
waitpid 更强大,支持非阻塞和指定 PID。

4 (status>>8)&0xFF,(status & 0x7F);

深入解析 status 的位操作:(status>>8)&0xFF 和 (status & 0x7F)

在 Linux 进程等待中,status 是一个 32 位整数,但它并不是一个普通的数值,而是由多个字段组成的 位图(bitmask),存储了子进程的退出状态信息。以下是它的结构:

复制

31                             16 15              8 7               0
+--------------------------------+----------------+----------------+
|          Unused (High 16 bits) |   Exit Code    |  Signal Number |
+--------------------------------+----------------+----------------+
关键字段
  1. 低 8 位(status & 0xFF

    • 存储 导致进程终止的信号编号(如 SIGSEGV=11)。

    • 如果进程是正常退出(exit() 或 return),这里为 0

  2. 次低 8 位((status>>8) & 0xFF

    • 存储 进程的退出码(Exit Code)(如 exit(123) 会在这里存 123)。

    • 只有进程正常退出时,这个字段才有意义。

  3. 最高 16 位

    • 通常未使用(某些系统可能存储 core dump 标志位)。


1. (status >> 8) & 0xFF —— 提取退出码

作用

  • 获取子进程的 退出码(Exit Code),即 exit(code) 或 return code 传入的值。

  • 仅当进程正常退出时有效(未被信号杀死)。

计算过程

  1. status >> 8

    • 将 status 右移 8 位,使 退出码字段 移动到最低 8 位。

    • 示例:

      status = 0x00007F00;  // 假设退出码是 127(0x7F)
      (status >> 8) = 0x0000007F;
  2. & 0xFF

    • 取最低 8 位(防止高位干扰)。

    • 示例:

      0x0000007F & 0xFF = 0x7F (即 127)

使用场景

if (WIFEXITED(status)) {  // 判断是否正常退出int exit_code = (status >> 8) & 0xFF;printf("Child exited with code: %d\n", exit_code);
}

2. status & 0x7F —— 提取信号编号

作用

  • 获取 导致进程终止的信号编号(如 SIGSEGV=11)。

  • 仅当进程被信号杀死时有效

计算过程

  1. status & 0x7F

    • 直接取 status 的最低 7 位(因为信号编号最大是 127)。

    • 示例:

      status = 0x0000000B;  // SIGSEGV=11
      status & 0x7F = 0x0B (即 11)

使用场景

if (WIFSIGNALED(status)) {  // 判断是否被信号杀死int signal_num = status & 0x7F;printf("Child killed by signal: %d\n", signal_num);
}

3. 为什么不用 WEXITSTATUS 和 WTERMSIG

Linux 提供了更安全的宏来解析 status

  • WEXITSTATUS(status):等价于 (status >> 8) & 0xFF(提取退出码)。

  • WTERMSIG(status):等价于 status & 0x7F(提取信号编号)。

推荐使用宏,因为:

  1. 可读性更好,直接表明意图。

  2. 兼容性更强,不同系统的 status 布局可能略有差异。


4. 完整示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>int main() {pid_t pid = fork();if (pid == 0) {// 子进程:故意除以 0 触发 SIGFPEint a = 10 / 0;exit(0);  // 不会执行到这里} else {// 父进程int status;waitpid(pid, &status, 0);if (WIFEXITED(status)) {printf("Exit code: %d\n", WEXITSTATUS(status));} else if (WIFSIGNALED(status)) {printf("Killed by signal: %d\n", WTERMSIG(status));  // 输出 8 (SIGFPE)}}return 0;
}

5. 总结

操作等效宏作用
(status >> 8) & 0xFFWEXITSTATUS获取退出码(正常退出时有效)
status & 0x7FWTERMSIG获取信号编号(被杀死时有效)

核心结论
status 是位图,需用位操作或宏解析。
推荐使用 WEXITSTATUS 和 WTERMSIG,代码更清晰。
父进程必须正确处理子进程状态,避免僵尸进程。

父进程如何获取子进程的退出信息?

父进程通过 wait 或 waitpid 系统调用获取子进程的退出状态,具体流程如下:


1. 父进程获取子进程退出信息的机制

(1)子进程退出时,内核会保留其退出状态

  • 子进程退出(正常 exit 或异常终止)后,内核不会立即销毁它,而是:

    • 保留 task_struct(进程描述符)的部分信息(如 PID、退出码、终止信号)。

    • 将子进程标记为 ZOMBIE(僵尸状态),等待父进程读取状态。

(2)父进程调用 wait/waitpid 读取状态

  • wait(&status)

    • 阻塞等待 任意一个子进程 退出。

    • 读取子进程的退出状态到 status

  • waitpid(pid, &status, options)

    • 可以指定等待某个子进程(pid > 0)或任意子进程(pid = -1)。

    • 支持 阻塞(options=0 或 非阻塞(options=WNOHANG 模式。

(3)内核清理僵尸进程

  • 父进程调用 wait/waitpid 后:

    • 内核返回子进程的 退出码 或 终止信号

    • 彻底释放子进程的 task_struct 和剩余资源(如 PID)。

  • 如果父进程不调用 wait

    • 子进程会一直保持 ZOMBIE 状态,直到父进程退出后由 init 进程(PID=1)回收。


2. 父进程在 wait 时的行为

(1)如果子进程已经退出(ZOMBIE)

  • 父进程的 wait 会 立即返回,并获取子进程的退出状态。

  • 示例:

    pid_t pid = fork();
    if (pid == 0) {exit(123);  // 子进程直接退出
    } else {int status;wait(&status);  // 立即返回,status 包含退出码 123
    }

(2)如果子进程未退出(RUNNING)

  • 默认(阻塞模式,options=0

    • 父进程进入 TASK_INTERRUPTIBLE(可中断睡眠) 状态:

      • 从 运行队列 移出,加入 等待队列

      • 不再占用 CPU,直到子进程退出后由内核唤醒。

    • 示例:

      pid_t pid = fork();
      if (pid == 0) {sleep(10);  // 子进程 10 秒后退出exit(0);
      } else {int status;wait(&status);  // 父进程阻塞 10 秒
      }
  • 非阻塞模式(options=WNOHANG

    • 如果子进程未退出,waitpid 立即返回 0,父进程继续执行。

    • 示例:

      pid_t pid = fork();
      if (pid == 0) {sleep(10);exit(0);
      } else {int status;while (1) {if (waitpid(pid, &status, WNOHANG) == pid) {break;  // 子进程退出}printf("Waiting...\n");sleep(1);}
      }

3. 关键问题解析

Q1:父进程在 wait 时是否占用 CPU?

  • 阻塞模式下(options=0

    • 父进程被移出 运行队列,加入 等待队列不占用 CPU

    • 子进程退出后,内核通过 信号或调度机制 唤醒父进程。

  • 非阻塞模式下(WNOHANG

    • 父进程继续运行,需主动轮询子进程状态。

Q2:status 如何存储退出码和信号?

  • status 是位图,结构如下:

    复制

    31                             16 15              8 7               0
    +--------------------------------+----------------+----------------+
    |          Unused (High 16 bits) |   Exit Code    |  Signal Number |
    +--------------------------------+----------------+----------------+
  • 解析方法

    • 退出码(status >> 8) & 0xFF 或 WEXITSTATUS(status)

    • 信号编号status & 0x7F 或 WTERMSIG(status)

Q3:如果父进程不 wait,子进程会怎样?

  • 子进程成为 僵尸进程(Zombie)

    • 占用内核资源(如 PID、进程表项)。

    • 无法被 kill 杀死,必须由父进程 wait 或父进程退出后由 init 回收。


4. 总结

场景父进程行为
子进程已退出(ZOMBIE)wait 立即返回,读取退出状态并释放子进程资源。
子进程未退出(RUNNING)默认阻塞:父进程睡眠,不占 CPU;非阻塞(WNOHANG):父进程继续执行并轮询。
父进程不 wait子进程保持僵尸状态,直到父进程退出后由 init 回收。

核心结论
wait/waitpid 是父进程回收子进程资源的唯一方式,避免僵尸进程。
阻塞模式下,父进程在 wait 时不占用 CPU,由内核管理唤醒。
非阻塞模式下,父进程需主动轮询子进程状态

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词