花莲县网站建设_网站建设公司_CMS_seo优化
2026/1/16 14:30:02 网站建设 项目流程

在Linux系统中,进程是程序执行的基本单位。理解进程如何结束、父进程如何回收子进程资源,以及进程如何执行新的程序,是掌握系统编程的关键。本篇博客将深入探讨进程的终止、等待和程序替换。

一、进程终止

当一个进程完成其任务或遇到异常时,它需要终止。进程终止的本质是操作系统回收其占用的资源(如内存、文件描述符等)。

1. 进程退出的场景

进程退出主要有三种场景:

  • 代码运行完毕,结果正确:这是最理想的情况。

  • 代码运行完毕,结果不正确:程序逻辑执行完成,但可能由于输入或逻辑错误,得到了错误的结果。

  • 代码异常终止:进程在运行过程中被信号(如SIGSEGV段错误)终止。

2. 进程退出的方法

正常终止

  1. main函数返回return 0等同于调用exit(0)

  2. 调用exit函数:这是标准的库函数,在终止进程前会执行清理工作。

  3. 调用_exit_Exit函数:这是系统调用,直接终止进程,不做任何清理。

异常退出

  • 通过Ctrl+C产生SIGINT信号终止进程。

  • 其他信号,如kill -9发送的SIGKILL

3. 退出码

进程退出时,会有一个退出码,用于向启动它的进程(通常是父进程或Shell)报告自己的终止状态。可以通过echo $?命令查看上一个命令的退出码。

常见的退出码及其含义如下:

退出码

解释

0

命令成功执行

1

通用错误代码

2

命令或参数使用不当

126

权限被拒绝或无法执行

127

未找到命令(PATH错误)

128+n

命令被信号终止(n为信号编号)

130 (128+2)

通过Ctrl+C(SIGINT) 终止

143 (128+15)

通过 SIGTERM(默认终止信号)终止

注意_exit(int status)函数中,虽然statusint类型,但只有低8位会被父进程使用。所以_exit(-1)在 Shell 中查看到的退出码是255

4.exit_exit的区别

这是理解进程终止的一个关键点。

  • _exit系统调用。直接使进程终止,立即关闭所有文件描述符,不会刷新(flush)标准I/O缓冲区。

  • exit库函数。它在调用_exit之前,会先执行以下清理工作:

    1. 执行用户通过atexit()on_exit()注册的清理函数。

    2. 关闭所有打开的流(标准I/O流,如stdout),并将缓冲区中的数据写入文件。

示例对比

// 示例1:使用 exit int main() { printf("hello"); // 注意:字符串后没有换行符 \n exit(0); } // 运行结果:输出 hello // 因为 exit 会刷新缓冲区,将 "hello" 写入标准输出。 // 示例2:使用 _exit int main() { printf("hello"); _exit(0); } // 运行结果:可能没有任何输出 // 因为 _exit 直接终止进程,缓冲区中的 "hello" 未被刷新。

二、进程等待

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

当一个子进程先于父进程终止时,如果父进程不采取任何措施,子进程就会进入“僵尸进程”​ 状态。

  • 僵尸进程的危害:僵尸进程保留了其在内核中的进程描述符等少量资源,如果父进程一直不回收,会导致资源泄漏(内存泄漏)。更严重的是,僵尸进程“刀枪不入”,连kill -9也无法杀死。

  • 获取子进程信息:父进程需要通过等待来获取子进程的退出状态,判断其是正常结束还是异常退出,以及正常的退出码是多少。

因此,进程等待​ 是父进程的责任,其主要目的有两个:

  1. 回收子进程资源,防止僵尸进程的产生。

  2. 获取子进程的退出信息

2. 进程等待的方法

主要有两个函数:waitwaitpid

wait函数

#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
  • 作用:阻塞等待任意一个子进程退出。

  • 参数status是一个输出型参数,用于获取子进程的退出状态。如果不关心状态,可设置为NULL

  • 返回值:成功则返回被等待子进程的PID,失败返回-1。

waitpid函数

#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);
  • 作用:功能更强大,可以等待指定的子进程,并支持非阻塞模式。

  • 参数

    • pid

      • pid = -1:等待任意一个子进程,与wait等效。

      • pid > 0:等待进程ID等于pid的子进程。

    • status:同wait

    • options

      • 0:默认选项,表示阻塞等待

      • WNOHANG:表示非阻塞等待。如果指定的子进程没有结束,则waitpid立即返回0,不等待。

  • 返回值

    • 成功时返回收集到的子进程的PID。

    • 如果设置了WNOHANG且子进程未退出,则返回0

    • 调用失败返回-1

3. 如何解析 status 参数?

status参数不能简单地当作一个整数来看待,而应该将其视为一个位图。它的低16位包含了退出信息(在32位系统上)。

我们通常使用宏来安全地解析这些信息:

  • WIFEXITED(status):如果这个宏为真(非零),表示子进程是正常终止​ 的。

  • WEXITSTATUS(status):如果WIFEXITED为真,此宏用于提取子进程的退出码(即exitreturn的参数)。

  • WIFSIGNALED(status):如果这个宏为真,表示子进程是被信号终止​ 的(异常退出)。

  • WTERMSIG(status):如果WIFSIGNALED为真,此宏用于获取导致子进程终止的信号编号

示例代码

int main(void) { pid_t pid; if ((pid = fork()) == -1) { perror("fork"); exit(1); } if (pid == 0) { // 子进程 sleep(20); exit(10); // 子进程正常退出,退出码为10 } else { // 父进程 int st; int ret = wait(&st); if (ret > 0) { if (WIFEXITED(st)) { // 正常退出 printf("Child exit code: %d\n", WEXITSTATUS(st)); // 输出 10 } else if (WIFSIGNALED(st)) { // 被信号杀死 printf("Child killed by signal: %d\n", WTERMSIG(st)); } } } return 0; }

三、进程程序替换

fork创建的子进程默认执行的是父进程相同的代码。如果我们希望子进程去执行一个全新的、不同的程序(例如,在Shell中输入ls命令),就需要用到进程程序替换

1. 替换原理

进程程序替换的核心函数是exec系列函数。当进程调用exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的main函数开始执行。

  • 关键点exec函数并不创建新的进程。调用前后,进程的PID保持不变。它只是用磁盘上的一个新程序,替换了当前进程的代码段、数据段等。

2. exec 函数族

有6个以exec开头的函数,它们功能相同,但参数传递方式不同。

#include <unistd.h> int execl (const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv (const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);

命名规律

  • l (list):参数采用列表​ 形式,逐个传入,以NULL结尾。例如:execl("/bin/ls", "ls", "-l", NULL)

  • v (vector):参数放入一个字符串数组​ 中传入,数组最后一个元素必须是NULL。例如:

    char *const argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv);
  • p (path):带有p的函数会自动在环境变量 PATH​ 指定的目录中搜索可执行文件,无需写全路径。例如:execlp("ls", "ls", "-l", NULL)

  • e (environment):带有e的函数需要用户自己组装并传入环境变量​ 数组envp[],不使用当前进程的环境变量。

重要特性exec函数只有在调用失败时才有返回值(-1)。如果调用成功,则执行新程序,原程序的后续代码不会再执行。

3. 函数关系

实际上,只有execve是真正的系统调用,其他五个函数都是库函数,它们最终都会封装execve来实现功能。其关系如下图所示:

+----------+ +----------+ +----------+ | execl | | execv | | execlp | ... (Library Functions) +----------+ +----------+ +----------+ | | | v v v +-------------------------------------------------+ | execve | (System Call) +-------------------------------------------------+

总结与实践:微型Shell

将进程创建(fork)、进程等待(waitpid)和进程替换(exec)结合起来,就能理解Shell的工作原理。一个简单的Shell流程如下:

  1. 获取命令行:显示提示符,读取用户输入的命令。

  2. 解析命令行:将命令字符串解析为命令名和参数列表。

  3. 创建子进程:使用fork

  4. 子进程程序替换:在子进程中使用execvp执行命令。

  5. 父进程等待子进程退出:使用waitpid等待子进程结束,防止其变成僵尸进程。

这个过程完美体现了“调用/返回”​ 的对称性:

  • 在函数中,call调用函数,函数return返回。

  • 在进程间,fork创建子进程,子进程exec执行新程序,新程序exit退出,父进程wait回收。

通过编写一个微型的Shell(代码已在你提供的文档中),可以极大地加深对进程控制的理解。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询