目录
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,让父子进程执行不同的语句。
- 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(一位)+信号编号(七位)。
宏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,设置新的环境变量(会覆盖原有的环境变量)
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[]);