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

进程控制总结

文章目录

  • 1. 进程创建
  • 2. 进程终止
  • 3. 进程等待
  • 4. 进程替换
    • 4.1 exec 系列函数
    • 4.2 替换原理

1. 进程创建

在Linux系统中fork()函数是非常重要的函数,它用来在一个已经存在的进程中创建一个新的进程。新进程成为子进程,原进程称为父进程。

//
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1

在这里插入图片描述
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以执行它们各自己的代码。即fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

fork() 调用后,fork() 给父进程的返回值是子进程的 PID(>0),继续执行 after 之后的代码。fork()给子进程的返回值是 0,也从同一个 after 处开始执行。虽然执行路径相同,但各自拥有独立的用户空间(各自的堆栈、数据段等)。

为了提高效率,父子进程在 fork() 后并不立即复制所有物理页,而是共享这些页,只在进程尝试写入时才真正分配新页并复制内容。因此,fork() 本质上是浅拷贝,直到写时才深拷贝

总之,fork() 就是内核为子进程拷贝父进程的执行上下文和大部分资源,然后父子进程各自独立地从 fork 调用点(after)继续执行

fork常规用法

  1. 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  2. 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数来完成进程替换。

2. 进程终止

进程退出的场景:

  1. 代码运行完毕,结果正确。
  2. 代码运行完毕,结果不正确。
  3. 代码异常终止。

进程常见退出方法:

  1. main() 中 return表示进程终止(函数return表示函数结束)。
    return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
    在这里插入图片描述
  2. 在代码任意位置调用exit()函数表示进程终止。
    在这里插入图片描述
  3. 使用系统调用 _exit终止进程。
    在这里插入图片描述

exit和_exit的区别:
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:执行用户通过 atexit或on_exit定义的清理函数。关闭所有打开的流,所有的缓存数据均被写入。调用_exit。
在这里插入图片描述
通过下面代码也可以看出:
在这里插入图片描述

3. 进程等待

什么是进程等待?
进程等待(process waiting)通常指父进程为了同步子进程的结束并回收其资源,主动挂起自己,直到子进程状态发生变化(最常见的是结束)。父进程调用 wait() 或waitpid() 这样的系统调用,自己进入阻塞(waiting)状态,直到某个子进程退出或收到信号而改变状态。期间,父进程不会继续执行,直到子进程终止或满足指定条件。

为什么要等待?
子进程一旦退出,会先变成僵尸进程(zombie),它的 PCB 和退出状态还挂在系统里,直到父进程调用 wait*() 才释放这些资源。父进程通过 wait,解决子进程的僵尸问题,回收系统资源,避免造成内存泄漏。其次,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。

进程等待的方法

  1. wait方法
#include <sys/types.h>
#include <sys/wait.h>
int *status	输出参数,子进程的“状态”存放位置;如果只关心进程结束,不想获取状态,可传 NULL。
返回值	
成功:返回结束(或被信号终止)的子进程的 PID;
失败:返回 −1,并设置 errno。
pid_t wait(int *status);
  1. waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:当正常返回的时候waitpid返回收集到的子进程的进程ID;如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:pid:Pid=-1,等待任一个子进程。与wait等效。Pid>0.等待其进程ID与pid相等的子进程。status:WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)options:WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。

获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)。
在这里插入图片描述

使用wait等待子进程,并通过位操作解析status,获取子进程的退出信息:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if (0 == id){printf("I am a child process! pid: %d\n", getpid());sleep(10);_exit(123);}else if (id > 0){int status = 0;pid_t rid = wait(&status);if (rid > 0 && (status & 0x7F) == 0){printf("exit_code:%d\n", ((status >> 8) & 0xFF));}else if (rid > 0){printf("exit_single:%d\n", ((status) & 0x7F));}else{perror("waitpid");}}else{perror("fork");}return 0;
}

子进程睡眠10秒后正常结束,退出码和代码中一致是123:
在这里插入图片描述
在另一个进程kill掉子进程:
在这里插入图片描述
上面通过位操作解析 status 的值,也可以通过如下宏来解析status 的值:

1. WIFEXITED(status)
作用:判断进程是否正常退出。
原理:检查 status 的低 8 位是否非零,且未被信号终止。
实现:
#define WIFEXITED(status)  (((status) & 0x7F) == 0)2. WEXITSTATUS(status)
作用:提取进程的退出码。
原理:取 status 的 8-15 位(即右移 8 位后取低 8 位)。
实现:
#define WEXITSTATUS(status) (((status) >> 8) & 0xFF)

使用waitpid等待子进程,并通过宏解析status,获取子进程的退出信息:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t id = fork();if (0 == id){printf("I am a child process! pid: %d\n", getpid());sleep(10);_exit(123);}else if (id > 0){int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0 && WIFEXITED(status)){printf("exit_code:%d\n", WEXITSTATUS(status));}else {printf("wait child process failed, return\n");}}else{perror("fork");}return 0;
}

进程的阻塞等待和非阻塞等待
1.阻塞等待
在 Linux 系统编程中,如果需要让 waitpid 阻塞等待子进程终止(即父进程暂停执行,直到目标子进程退出或被信号终止),应将 options 参数设置为 0(即不启用任何特殊选项)。此时 waitpid 的行为与 wait 类似。

调用 waitpid 后,父进程暂停执行,直到子进程状态变化(如终止、被信号杀死、暂停等)。父进程必须等待子进程完成后才能继续。

阻塞等待流程:

父进程代码│▼
调用 wait()waitpid(..., 0)│▼
父进程暂停执行,进入阻塞状态(等待子进程终止)│          ↙ 子进程运行中...▼        ↙
子进程终止,内核通知父进程│▼
父进程恢复执行,处理子进程状态│▼
继续后续代码

示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main() 
{pid_t id = fork();if (id == 0) {// 子进程执行任务sleep(2);exit(42); // 退出码 42} else {// 父进程阻塞等待指定子进程int status;pid_t rid = waitpid(id, &status, 0); // options=0,阻塞等待if (rid == id && WIFEXITED(status)) {printf("child  %d 退出,状态码: %d\n", id, WEXITSTATUS(status)); // 输出 42} else {perror("waitpid 失败");}}return 0;
}

2.非阻塞等待
如果需要让 waitpid 非阻塞等待子进程终止,应将 options 参数设置为WNOHANG
调用 waitpid 后,父进程立即返回,可继续执行其他任务。需循环调用 waitpid 检查子进程状态(轮询机制)。

非阻塞等待流程:

父进程代码│▼
调用 waitpid(..., WNOHANG)│▼
内核检查子进程状态:├─ 子进程已终止 → 立即返回 PID,父进程处理状态└─ 子进程未终止 → 返回 0,父进程继续执行其他任务│▼
父进程循环调用 waitpid 轮询子进程状态│▼
直到子进程终止,父进程处理状态│▼
继续后续代码

示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main() 
{pid_t id = fork();if (id == 0) {sleep(2);       // 子进程运行 2 秒exit(42);} else {int status;while (1) {// 非阻塞等待子进程pid_t rid = waitpid(id, &status, WNOHANG);if (rid == id && WIFEXITED(status)) {printf("子进程退出,状态码: %d\n", WEXITSTATUS(status));break;} else if (rid == 0) {printf("子进程未退出,父进程继续工作...\n");sleep(1);  // 模拟父进程执行其他任务} else {perror("waitpid 错误");break;}}}return 0;
}

3.形象比喻

阻塞等待: 你在一家咖啡店排队,必须等到咖啡做好后才能离开(父进程挂起,直到子进程完成)。
非阻塞等待:你点单后拿到一个取餐号,可以继续逛街,每隔一段时间回来问咖啡是否做好(父进程轮询检查子进程状态)。

4. 进程替换

4.1 exec 系列函数

在 Linux 系统编程中,进程替换通过 exec 系列函数实现,它的核心作用是将当前进程的代码段、数据段等替换为新的程序,使当前进程转而执行另一个程序
以下是 exec 系列函数的详细解析:

exec 系列函数的的核心作用
用新程序的代码、数据、堆栈等替换当前进程的原有内容。PID 保持不变(不创建新进程),原进程的打开文件描述符、信号处理等属性默认保留。exec 成功后,原进程的代码不再执行(被新程序完全替代)。

exec 系列函数的的命名规则和原型
所有 exec 函数均定义在 <unistd.h> 中,命名遵循以下规则:
后缀 l(list):参数以 可变参数列表 形式传递(NULL 结尾)。
后缀 v(vector):参数以 字符串数组 形式传递。
后缀 e(environment):允许自定义环境变量(需额外传递 envp 数组)。
后缀 p(PATH):自动在 PATH 环境变量指定的目录中搜索可执行文件。

函数原型特点
int execl(const char *path, const char *arg0, …, NULL)参数列表形式,需指定完整路径。
int execlp(const char *file, const char *arg0, …, NULL)参数列表形式,自动搜索 PATH。
int execle(const char *path, const char *arg0, …, NULL, char *const envp[])参数列表形式,可自定义环境变量。
int execv(const char *path, char *const argv[])参数数组形式,需指定完整路径。
int execvp(const char *file, char *const argv[])参数数组形式,自动搜索 PATH。
int execvpe(const char *file, char *const argv[], char *const envp[])参数数组形式,自动搜索 PATH 并自定义环境变量。

exec 系列函数使用示例

  1. execl 使用示例:
#include <stdio.h>
#include <unistd.h>int main()
{printf("进程启动, 即将被替换为ls命令!!!\n");//path:可执行文件的 完整路径(如 /usr/bin/ls)。//arg0, ..., NULL:参数列表,以 NULL 结尾。//第一个参数 arg0 通常是程序名称(即 argv[0])。execl("/usr/bin/ls", "ls", "-a", "-l", NULL);perror("execl 失败");return 0;
}
  1. execlp 使用示例:
#include <stdio.h>
#include <unistd.h>int main()
{printf("进程启动, 即将被替换为ls命令!!!\n");//file:可执行文件的 名称(如 ls),系统会在 PATH 中搜索//arg0, ..., NULL:参数列表,以 NULL 结尾。//第一个参数 arg0 通常是程序名称(即 argv[0])。execlp("ls", "ls", "-a", "-l", NULL);perror("execp 失败");return 0;
}
  1. execle 原使用示例:
#include <stdio.h>
#include <unistd.h>int main()
{printf("进程启动, 即将被替换为ls命令!!!\n");//path:可执行文件的 完整路径(如 /usr/bin/ls)。//arg0, ..., NULL:参数列表,以 NULL 结尾。//第一个参数 arg0 通常是程序名称(即 argv[0])。//env:自定义环境变量数组,以 NULL 结尾。char* env[] = {"USER=ZhuZebo", NULL};execle("/usr/bin/ls", "ls", "-a", "-l", NULL, env);perror("execle 失败");return 0;
}
  1. execv 原使用示例:
#include <stdio.h>
#include <unistd.h>int main()
{printf("进程启动, 即将被替换为ls命令!!!\n");//path:可执行文件的 完整路径(如 /usr/bin/ls)。//argv:参数数组,以 NULL 结尾。//argv[0] 通常是程序名称。char* argv[] = {"ls", "-a", "-l", NULL};execv("/usr/bin/ls", argv);perror("execv 失败\n");return 0;
}
  1. execvp 原使用示例:
#include <stdio.h>
#include <unistd.h>int main()
{printf("进程启动, 即将被替换为ls命令!!!\n");//file:可执行文件的 名称(如 ls),系统会在 PATH 中搜索//argv:参数数组,以 NULL 结尾。//argv[0] 通常是程序名称。char* argv[] = {"ls", "-a", "-l", NULL};execvp("ls", argv);perror("execvp 失败\n");return 0;
}
  1. execvpe 原使用示例:
#include <stdio.h>
#include <unistd.h>int main()
{printf("进程启动, 即将被替换为ls命令!!!\n");//file:可执行文件的 名称(如 ls),系统会在 PATH 中搜索//argv:参数数组,以 NULL 结尾。//argv[0] 通常是程序名称。//env:自定义环境变量数组,以 NULL 结尾。char* argv[] = {"ls", "-a", "-l", NULL};char* env[] = { "USER=ZhuZebo", NULL};execvpe("ls", argv, env);perror("execvpe 失败\n");return 0;
}

4.2 替换原理

程序替换通过 exec 系列函数实现,其本质是 将当前进程的代码和数据替换为新程序的代码和数据,但保持以下属性不变:
进程标识符(PID):进程的 ID 不变。
内核态信息:文件描述符表、进程优先级等。
在这里插入图片描述
替换步骤:
1.内核验证与权限检查:检查文件路径、权限、格式。
2.解析可执行文件(ELF):读取代码段、数据段、入口地址。
3 释放原进程的内存资源:释放原进程内存,加载新程序到虚拟地址空间。
4. 继承与重置属性:保留文件描述符,重置堆栈,恢复默认信号处理。
5.跳转到新程序入口:CPU 从新入口地址开始执行。
程序替换通过替换进程的代码和数据段实现进程重生

至此,本片文章就结束了,若本篇内容对您有所帮助,请三连点赞,关注,收藏支持下。
创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!
如果本篇博客有任何错误,请批评指教,不胜感激 !!!
在这里插入图片描述

http://www.xdnf.cn/news/520903.html

相关文章:

  • Spyglass:参数(parameter)及其设置方式
  • 考研数学积分学
  • supervisorctl守护进程
  • PCB设计实践(十九)PCB设计中NPN/PNP选型策略
  • C++(23):容器类<vector>
  • C++控制结构详解:if-else、switch、循环(for/while/do-while)
  • 嵌入式学习笔记 - U(S)ART 模块HAL 库函数总结
  • 开启健康生活的多元养生之道
  • Prism使用消息总线打开窗体的案例(中介者模式)
  • GBS 8.0服装裁剪计划软件在线试用
  • SAPROv5.7
  • Muduo网络库大总结
  • 大语言模型 vs NLTK/SpaCy:NLP工具的代际跃迁与互补之道
  • LORA 微调 - LoRA 介绍与 LoRA 微调指南
  • 最长公共子序列(LCS)
  • 网络编程套接字(二)
  • 17 C 语言数据类型转换与数据溢出回绕详解:隐式转换、显式转换、VS Code 警告配置、溢出回绕机制
  • 并发编程(4)
  • 中山市东区信息学竞赛2025 题目解析
  • CMake调试与详细输出选项解析
  • 基于区块链技术的智能汽车诊断与性能分析
  • 运行vscode编辑器源码
  • 课外活动:再次理解页面实例化PO对象的魔法方法__getattr__
  • 【免杀】C2免杀技术(五)动态API
  • C2S-Scale方法解读
  • [Android] 青木扫描全能文档3.0,支持自动扫描功能
  • 机器学习入门之朴素叶贝斯和决策树分类(四)
  • 【VMware】开启「共享文件夹」
  • 计算机系统的工作原理
  • 2.2.5