当前位置: 首页 > news >正文

【Linux】从0到1掌握进程控制:终止、等待与替换的核心逻辑

上面一篇文章我们学习了进程的相关概念,为进程控制以及理解起到很大的作用。这篇文章就让我们一起学习Linux系统下进程控制的相关知识,在下一篇我们融会贯通根据所学完成一个自定义shell程序。

我们在上一篇中详细讲解了进程创建fork()的使用以及理解,这篇我们就从进程终止入手了解进程控制的相关概念。

1.1进程终止

进程的终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构以及对应的数据和代码。进程退出主要分为正常退出和异常退出,二者的触发原因和系统处理方式不同。

1)正常退出:从主函数main返回;调用exit ();调用_exit ()等场景。exit()和_exit ()的参数就是返回的错误码,return后面的数字也是返回码。

2)异常退出:ctrl+c,信号终止,程序结束;外部kill命令或系统资源耗尽;依赖的库文件丢失、网络连接中断且无容错逻辑等,这一类进程因错误或外部干预被迫中止,退出码非0(用来标识错误类型)。

使用命令"echo $?"可以查询到上一个进程的退出码,常见的退出码如下图:

可以使用strerror函数获取退出码的对应描述。

exit函数和_exit函数:exit(status)是C标准库提供的函数退出函数,参数status就是我们人为传进去的返回码参数,就是返回码。return(n)和exit(n)n就是退出码。 exit和_exit的区别就是是否刷新文件缓存区

特性exit()_exit()类Unix特有 / _Exit()
头文件<stdlib.h><unistd.h>_exit)或 <stdlib.h>_Exit
清理操作会执行标准 I/O 缓冲区刷新、调用全局 / 静态变量的析构函数、执行 atexit() 注册的回调函数等清理工作。直接终止进程,不执行任何清理操作,不刷新缓冲区,不调用析构函数或 atexit() 回调。
适用场景正常终止用户进程,确保资源正确释放。多进程编程中(如子进程),避免清理操作干扰父进程;或需要立即终止且无需清理的场景。

1.2进程等待

在进程概念篇中我们讲过僵尸概念,我们提到僵尸进程问题,僵尸进程会导致内存泄漏的问题,我们为了防止进程变成僵尸进程,就是必须要求父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息,从而判断子进程的执行结果。

1)wait()函数

wait()函数阻塞等待任意一个子进程退出,并获取其退出状态。

#include <sys/wait.h>
pid_t wait(int *status);  // status 用于存储子进程退出状态,传 NULL 表示不关心

返回值:成功返回退出子进程的PID;失败返回-1(如无可用子进程等待)。

2)waitpid()函数

比wait函数更加灵活,可指定等待的子进程PID,设置非阻塞模式等功能。

pid_t waitpid(pid_t pid, int *status, int options);

参数说明
status:存储子进程的退出状态,是一个输出性参数。如果在传入status时传入的是NULL,那么就是表示不关心子进程的退出状态信息
options:WNOHANG表示非阻塞等待,若子进程未推出则立即返回0;
WUNTRACED表示还要等待被暂停的子进程;适用于需监控子进程的暂停状态的场景。
WCONTINUED表示被暂停后又恢复运行的子进程;适用于需跟踪子进程从暂停状态恢复运行的情况。这三个选项可以组合使用(使用" | "组合)。

1.3阻塞等待和非阻塞等待

1)阻塞等待

父进程调用等待函数后,会暂停执行(进入阻塞状态),直到有子进程退出或发生错误才会被唤醒并继续执行。

示例代码:

#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>int main() {pid_t pid = fork();if (pid == 0) {// 子进程:执行 2 秒后退出sleep(2);printf("子进程退出\n");exit(0);} else {// 父进程:阻塞等待,期间不执行任何操作printf("父进程开始阻塞等待...\n");wait(NULL);  // 阻塞在这里,2 秒后才会继续//waitpid(pid, &status, 0);printf("父进程等待结束\n");}return 0;
}

2) 非阻塞等待

父进程调用等待函数后,不会暂停执行。如果子进程未退出,函数会立即返回(0或者错误码),子进程可以继续处理其他任务;若子进程已退出,则正常获取其状态。

示例代码:

#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>int main() {pid_t pid = fork();if (pid == 0) {// 子进程:执行 2 秒后退出sleep(2);printf("子进程退出\n");exit(0);} else {// 父进程:非阻塞等待,期间可做其他事int status;while (1) {// 非阻塞查询子进程状态pid_t ret = waitpid(pid, &status, WNOHANG);if (ret == 0) {// 子进程未退出,父进程可处理其他任务printf("子进程未退出,父进程继续工作...\n");sleep(1);  // 模拟其他工作} else if (ret == pid) {// 子进程已退出,退出循环printf("父进程等待结束\n");break;}}}return 0;
}

3)status输出型参数

wait()和waitpid()通过status指针返回子进程的退出状态,想要知道需要用宏解析

WIFEXITED():判断子进程是否正常退出,返回非0表示正常退出。
WEXITSTATUS():若是WIFEXITED为真,获取子进程的退出码(如exit(n)中的n)。
WIFSIFGNALED():判断子进程是否因为信号(如kill命令)异常终止,返回非0为信号终止。
WTERMSIG():若WIFSIGNALED为真,获取终止子进程的信号编号(如SIGNKILL对应9)。

if (WIFEXITED(status)) {printf("正常退出,退出码:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {printf("被信号 %d 终止\n", WTERMSIG(status));
}

还有判断子进程其他退出信息的宏解析这里就不一一列出来了。

status是一个int类型的指针,底层是一个位掩码结构。32位结构如下:

31 ~ 171615 ~ 876 ~ 0
未使用coreexit_code0

signal

signal:存储导致子进程终止或暂停的信号编号(如SIGKILL是9,SIGSSTOP是19)。

第8位:模式区分位,用于区分信号和退出码模式;0表示子进程状态与信号相关;1表示子进程正常退出(在15~8位存储退出码)。

exit_code:退出码,仅当第7位为1时才有效;存储exit(n)或main函数return (n)的n值(取值0-255超过则会截断)。

core:核心转储标志;当子进程因收到致命信号(如内存访问错误、非法指令等)而异常终止时,操作系统会将该进程终止瞬间的内存状态、寄存器信息、堆栈数据等关键信息写入一个特殊文件(通常名为 core 或 core.<pid>),这个文件就称为 “核心转储文件”(用户可通过gdb分析该文件,定位程序崩溃的原因)。当core为1时产生了核心转储文件;为0时则表示未产生。

高15位:为扩展位,未使用,预留扩展。

好了知道status的底层结构,我们就可以通过左移等位计算操作得到我们想要得到的退出码或终止信号,WXEXITSTATUS宏的本质就是对status进行位运算。

1.4进程程序替换

进程程序替换允许一个正在运行的程序变声为另一个程序,新程序从头执行,替换原进程的代码、数据、堆、和栈等内存区域,但是进程的PID保持不变。

1)进程替换原理

用fork创建一个子进程后执行的是和父进程相同的程序,这是让子进程进行程序替换,使用exec系列函数让子进程执行另一个程序。调用exec并不创建新进程。

2)进程替换函数

#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[]);

exec系列函数一共有六个主要成员,它们之间也可以叠加使用:
1)l(list):命令行参数以列表形式传递。
2)v(vector):命令行参数以数组形式传递。
3)p(path):自动在PATH环境变量指定的路径中查找可执行文件。
4)e(environment):允许指定新程序的环境变量。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main() {pid_t pid = fork();if (pid == 0) { // 子进程printf("Child process (PID: %d) will execute exec\n", getpid());// 示例1: 使用execlp执行ls命令// execlp("ls", "ls", "-l", "-a", NULL);// 示例2: 使用execvp执行ls命令// char *argv[] = {"ls", "-l", "-a", NULL};// execvp("ls", argv);// 示例3: 使用execle执行程序并指定环境变量// char *envp[] = {"PATH=/bin:/usr/bin", "USER=test", NULL};// execle("/bin/echo", "echo", "Hello", "World", NULL, envp);// 如果执行到这里,说明exec失败perror("exec failed");exit(EXIT_FAILURE);}else if (pid > 0) { // 父进程wait(NULL); // 等待子进程结束printf("Parent process (PID: %d) exiting\n", getpid());}else { // fork失败perror("fork failed");return 1;}return 0;
}

返回值成功执行后,exec系列函数都不会返回(因为当前进程已被替换)。只有发生错误的时候才会返回-1,并设置errno。常见错误:文件不存在(ENOENT)、权限不足(EACCES)、不是可执行文件(ENOEXEC),可以通过查看全局变量errno的值来确定具体的错误类型。

注意:在exec系列函数中,其实只有execve是唯一真正的系统调用,其他的函数都是通过间接或直接调用execve的库函数。它们之间的关系如下图所示:

http://www.dtcms.com/a/352842.html

相关文章:

  • 音频中的噪音门
  • 视频加水印_带gif 加动态水印 gif水印 视频浮动水印
  • 2025年03月 Python(三级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • 《MongoDB 常用命令详解:从数据库操作到高级查询》
  • mongodb influxdb
  • Vue JS安装部署与使用方法(保姆级教程)
  • Java 实现 MongoDB ObjectId 算法
  • Python常见设计模式3: 行为型模式
  • 数据分析与数据挖掘
  • 【技术教程】如何为ONLYOFFICE协作空间开发文件过滤UI插件
  • string类的学习及模拟
  • vue拖动排序,vue使用 HTML5 的draggable拖放 API实现内容拖并排序,并更新数组数据
  • 【无标题】淘宝直播间详情数据
  • 云原生安全架构设计与零信任实践
  • 三格电子——高频一体式工业级RFID读写器的应用
  • 核心内涵解析:销采一体化 CRM 是什么?
  • 贴片式TE卡 +北京君正+Rk瑞芯微的应用
  • 亚马逊ASIN定投广告的智能化突破:从人工苦力到数据驱动的华丽转身
  • Part 1️⃣:相机几何与单视图几何-第六章:相机模型
  • Android中点击链接跳转到对应App页面的底层原理
  • Linux 云服务器日志清理自动化方法
  • 第二阶段Winfrom-8:特性和反射,加密和解密,单例模式
  • 点评项目(Redis中间件)第一部分Redis基础
  • golang 12 package 和 module
  • SegEarth-R1: Geospatial Pixel Reasoning via Large Language Model
  • week5-[字符数组]长度和
  • GraphRAG数据可视化
  • Java中JUnit知识点
  • Qt表格组件封装与远程数据库连接:从数据展示到交互体验
  • 阿里云——应用交付与负载均衡