1、进程创建
1.1 fork
通过 fork 系统调用,创建子进程。
#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,让父子进程执行不同的语句。创建失败,返回 -1。
- 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
// stat_loc 输出型参数,记录子进程的退出状态
pid_t wait(int *stat_loc);
- 父进程阻塞等待任意一个退出的子进程,若子进程退出,返回子进程的 pid,若调用失败,返回 -1。
3.2.2 waitpid(常用)
// 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(一位)+信号编号 (七位)。
- 宏 WEXITSTATUS(stat_loc),获取退出码。
- 宏 WIFEXITED(stat_loc),子进程正常退出,为真,否则,为假。
- options:
- 为 0,父进程阻塞等待 (一直等,直到子进程退出),若子进程退出,返回子进程 pid,若调用失败 (如 pid 不存在),返回 -1。
- 为 WNOHANG,父进程非阻塞等待 (询问一次,知道子进程的状态,父进程可以做自己的事,一般需要多次询问),若子进程退出,返回子进程 pid,若子进程没有退出,返回 0,若调用失败 (如 pid 不存在),返回 -1。
4、进程程序替换
4.1 替换原理
- 用 fork 创建子进程后,子进程可以调用一种 exec 系列函数以执行另一个程序。
- exec 系列函数会替换当前进程的代码段、数据段和堆栈等,但文件描述符表会被保留(引用计数不变)(除非文件描述符设置了 FD_CLOEXEC 标志)。
- 调用 exec 不会创建新进程,因此调用前后该进程的 PID 保持不变。
4.2 替换函数
- path/file,是要执行谁,arg/argv,是怎么执行 (命令行怎么写,就怎么写),envp,设置新的环境变量 (会覆盖原有的环境变量)
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 的封装,以满足不同的场景。
int execve(const char *path, char *const argv[], char *const envp[]);


