目录
[1.1 fork](#1.1 fork)
[1.2 fork的常规用法](#1.2 fork的常规用法)
[1.3 fork失败的原因](#1.3 fork失败的原因)
[2.1 基本概念](#2.1 基本概念)
[2.2 进程退出场景](#2.2 进程退出场景)
[2.3 退出码](#2.3 退出码)
[2.4 进程常见退出方式](#2.4 进程常见退出方式)
[3.1 进程等待的必要性](#3.1 进程等待的必要性)
[3.2 进程等待的方式](#3.2 进程等待的方式)
[3.2.1 wait](#3.2.1 wait)
[3.2.2 waitpid(常用)](#3.2.2 waitpid(常用))
[4.1 替换原理](#4.1 替换原理)
[4.2 替换函数](#4.2 替换函数)
1、进程创建
1.1 fork
通过fork(系统调用),创建子进程。
bash
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
printf("hello proc : %d!, ret: %d\n", getpid(), ret);
sleep(1);
return 0;
}
- 两个返回值,对父进程返回子进程的PID ,对子进程返回0 。因为父:子 = 1:N,父进程需要区分子进程,而子进程能通过PPID找到父进程。所以可以if ,让父子进程执行不同的语句。
- fork() 创建子进程后,父子进程从 fork() 返回处继续执行 。注意:子进程不会执行fork()之前的代码。
- 当父子进程尝试修改数据 ,会发生写时拷贝 (减少创建子进程的时间,减少内存浪费),重新拷贝一份数据。所以父子进程独立运行。
1.2 fork的常规用法
- 父进程创建子进程后,父子进程各自执行不同的逻辑。
- 子进程通过 exec 系列函数完全替换为另一个程序。
1.3 fork失败的原因
-
进程总数超过内核限制。
-
用户进程数超过配额。
2、进程退出
2.1 基本概念
进程退出,释放代码和数据 ,没有释放PCB对象。
2.2 进程退出场景
- 代码运行完毕,结果正确。
- 代码运行完毕,结果不正确。
- 代码异常终止(一般是收到了信号)。
2.3 退出码
- 如果是异常终止 ,退出码无意义(代码都没执行完)。
- 不是异常终止 ,0为结果正确 ,非0为结果不正确(不同的值,表示不同的原因)。
注意:
- $?,显示最近一个进程退出时的退出码。
- errno,当系统调用或库函数发生错误时 ,errno会被设置为对应的错误码。需包含<errno.h>。
- strerror(),根据错误码 ,显示错误信息。
2.4 进程常见退出方式
- main函数的return 退出码 ,(其他函数的return,只表示函数调用完成),表示进程退出。
- _exit(退出码) ,系统调用 ,进程退出。
- exit(退出码),C标准库函数 (封装了exit()),进程退出 ,还会刷新I/O缓冲区等。
3、进程等待
3.1 进程等待的必要性
子进程退出 ,父进程需要获取子进程退出前的信息 (即子进程PCB对象里面的信息,其指向的代码和数据已被释放,可选),并释放子进程的PCB对象 (必要),如果父进程没有"回收"子进程 ,那么子进程 被称为"僵尸进程 ",其PCB对象将会一直存在 ,造成内存泄漏。
父进程 通过进程等待 的方式"回收"子进程。
3.2 进程等待的方式
3.2.1 wait
bash
// stat_loc 输出型参数,记录子进程的退出状态
pid_t wait(int *stat_loc);
- 父进程阻塞等待 任意一个退出的子进程 ,若子进程退出 ,返回子进程的pid ,若调用失败 ,返回**-1**。
3.2.2 waitpid(常用)
bash
// pid,指定等待子进程,stat_loc,子进程的退出信息,options,功能
pid_t waitpid(pid_t pid, int *stat_loc, int options);
- pid,等待指定pid的子进程 。若为**-1**,等待任意一个退出的子进程。
- stat_loc,输出型参数,32位,高16位不用。

正常退出 ,次第八位 为进程退出码 ,低八位 为0。
异常终止 (一般是收到了信号),次第八位 ,无意义 (因为代码都没执行完),低八位 ,core dump(一位)+信号编号(七位)。
宏WEXITSATTUS(stat_loc),获取退出码。
宏WIFEXITED(stat_loc),子进程正常退出 ,为真,否则,为假。
- options,为0,父进程阻塞等待 (一直等,直到子进程退出),若子进程退出 ,返回子进程pid ,若调用失败 (如pid不存在),返回 -1。为WNOHANG,父进程非阻塞等待 (询问一次,知道子进程的状态,父进程可以做自己的事,一般需要多次询问),若子进程退出 ,返回子进程pid ,若子进程没有退出 ,返回0 ,若调用失败 (如pid不存在),返回-1。
4、进程程序替换
4.1 替换原理
用 fork创建子进程后,子进程执行的是和父进程相同的程序 (但可能执行不同的代码分支)。子进程 通常会调用 一种 exec函数以执行另一个程序 。当进程调用 exec函数时,该进程的用户空间代码和数据会被新程序完全替换(替换就是进行修改,触发写时拷贝,然后覆盖),并从新程序的启动例程开始执行。调用 exec 不会创建新进程 ,因此调用前后该进程的 PID 保持不变。
4.2 替换函数
path/file,是要执行谁 ,arg/argv,是怎么执行 (命令行怎么写,就怎么写),envp,设置新的环境变量 (会覆盖原有的环境变量)
bash
int execl(const char *path, const char *arg0, ..., NULL);
int execlp(const char *file, const char *arg0, ..., NULL);
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[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
函数名 | 参数传递方式 | 是否按照 PATH(环境变量)搜索 |
是否指定环境变量 | 后缀含义 |
---|---|---|---|---|
execl |
字符串列表 | ❌ 否 | ❌ 默认环境 | l =list |
execv |
字符串数组 | ❌ 否 | ❌ 默认环境 | v =vector |
execlp |
字符串列表 | ✅ 是 | ❌ 默认环境 | p =PATH |
execvp |
字符串数组 | ✅ 是 | ❌ 默认环境 | p =PATH |
execle |
字符串列表 | ❌ 否 | ✅ 自定义环境 | e =environment |
execvpe |
字符串数组 | ✅ 是 | ✅ 自定义环境 | pe =PATH+environment |
注意:
- exec系列函数,调用失败返回-1 ,调用成功 就直接替换成新的程序 了,无需返回值。所以不用进行返回值判断 ,因为执行exec后面的代码 ,一定是失败了。
- 无论是字符串列表 还是字符串数组 ,都要显示以NULL结尾。
- 带了p,就默认在PATH的环境变量下搜索命令 。不带p,要提供绝对路径 或相对路径。
- 带了e,就设置新的环境变量(会覆盖原有的环境变量)。
- 如果新增环境变量 ,不想覆盖原有的环境变量 ,子进程 直接putenv(),使用非 e 后缀函数 ,替换的程序默认使用子进程的环境变量 。如果使用带 e 后缀函数 ,就传environ(指向当前进程的环境变量表的指针,需声明extern char ** environ;),替换的程序继承子进程的环境变量。
- 还有execve,是系统调用 。上面的函数 ,是对execve的封装 ,以满足不同的场景。
bash
int execve(const char *path, char *const argv[], char *const envp[]);