进程控制总结
文章目录
- 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常规用法:
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数来完成进程替换。
2. 进程终止
进程退出的场景:
- 代码运行完毕,结果正确。
- 代码运行完毕,结果不正确。
- 代码异常终止。
进程常见退出方法:
- main() 中 return表示进程终止(函数return表示函数结束)。
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
- 在代码任意位置调用exit()函数表示进程终止。
- 使用系统调用 _exit终止进程。
exit和_exit的区别:
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:执行用户通过 atexit或on_exit定义的清理函数。关闭所有打开的流,所有的缓存数据均被写入。调用_exit。
通过下面代码也可以看出:
3. 进程等待
什么是进程等待?
进程等待(process waiting)通常指父进程为了同步子进程的结束并回收其资源,主动挂起自己,直到子进程状态发生变化(最常见的是结束)。父进程调用 wait() 或waitpid() 这样的系统调用,自己进入阻塞(waiting)状态,直到某个子进程退出或收到信号而改变状态。期间,父进程不会继续执行,直到子进程终止或满足指定条件。
为什么要等待?
子进程一旦退出,会先变成僵尸进程(zombie),它的 PCB 和退出状态还挂在系统里,直到父进程调用 wait*() 才释放这些资源。父进程通过 wait,解决子进程的僵尸问题,回收系统资源,避免造成内存泄漏。其次,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
进程等待的方法
- wait方法
#include <sys/types.h>
#include <sys/wait.h>
int *status 输出参数,子进程的“状态”存放位置;如果只关心进程结束,不想获取状态,可传 NULL。
返回值
成功:返回结束(或被信号终止)的子进程的 PID;
失败:返回 −1,并设置 errno。
pid_t wait(int *status);
- 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 系列函数使用示例
- 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;
}
- 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;
}
- 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;
}
- 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;
}
- 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;
}
- 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 从新入口地址开始执行。
程序替换通过替换进程的代码和数据段实现进程重生。
至此,本片文章就结束了,若本篇内容对您有所帮助,请三连点赞,关注,收藏支持下。
创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!
如果本篇博客有任何错误,请批评指教,不胜感激 !!!