🌈 Ⅰ 进程创建
01. fork 函数介绍
fork 函数介绍
- 在 Linux 中可以使用 fork 函数从已经存在的进程中创建新进程。
- 新的进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
fork 的返回值
- fork 函数有三种返回值。
返回值状态 | 说明 |
---|---|
返回值 < 0 | 表示子进程创建失败 |
返回值 = 0 | 表示当前进程为子进程 |
返回值 > 0 | 该返回值是子进程的 pid,当前进程为父进程,其持有子进程 pid |
02. 写时拷贝
1. 什么是写时拷贝
- 通常情况下,父子进程的代码共享,父子进程在不进行写入操作时,其数据也是共享的,当任意一方试图写入时,便会以写时拷贝的方式各自留存一份副本。
2. 为什么写时拷贝
- 创建子进程时,子进程不一定要用到父进程的全部数据,因此不需要直接将父进程的所有数据全部拷贝一份给子进程,而是在要进行修改时再从父进程那拷贝这部分数据即可即可。
- 子进程正在尝试对这部分数据进行修改,但是父进程不打算修改这部分共享的数据,因此子进程就必须将旧数据拷贝一份进行修改。
03. fork 常规用法
父子进程执行不同的代码段
- 判断 fork 的返回值,从而让不同的进程去执行不同的代码段。
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
using std::cout;
using std::endl;
int main()
{
pid_t id = fork(); // 创建进程
if (0 == id) // 执行子进程部分代码
cout << "I am child process" << endl;
else if(id < 0) // 子进程的创建失败了
cout << "process creation failure" << endl;
else // 执行父进程部分代码
cout << "I am parent process" << endl;
return 0;
}
04. fork 调用失败的原因
- 系统中进程太多,再 fork 时就会内存不足从而导致进程创建失败。
- 实际用户的进程数超过了限制,系统对每个用户能创建的进程数量是有上限的。
🌈 Ⅱ 进程终止
01. 进程退出场景
- 代码运行完毕,结果正确。
- 代码运行完毕,结果错误。
- 代码没执行完,异常终止。
02. 常见退出方法
1. 使用 _exit 函数退出进程
- 函数原型
- 该函数是个系统调用。
- _exit 函数 不支持 刷新缓冲区。
#include <unistd.h>
// 用于终止进程,status 表示进程退出时的退出码
void _exit(int status);
- 函数用例
int main()
{
while (true)
{
cout << "I am a process, pid: " << getpid() << endl;
_exit(3);
}
return 0;
}
2. 使用 exit 函数退出进程
- 函数原型
- 该函数本质上是执行了系统调用的 _exit 函数。
- exit 函数 支持 刷新缓冲区。
#include <stdlib.h>
// 用于终止进程,status 表示进程退出时的退出码
void exit(int status);
- 函数用例
- 在代码的任何地方调用 exit 函数都表示退出进程。
int main()
{
while (true)
{
cout << "I am a process, pid: " << getpid() << endl;
exit(2);
}
return 0;
}
3. 使用 return 退出进程
- 执行 return n 等同于执行 exit(n),因为调用 main 的运行时函数会将 main 函数的返回值当做 exit 函数的参数。
- return 返回的值为 0 则表示进程执行成功,反之则表示进程执行失败。且 0 之外的不同数字能够表示进程执行失败的不同原因。
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
int main()
{
return 3;
}
- 使用 echo $? 能够查看最近一次执行的进程的退出码。
🌈 Ⅲ 进程等待
01. 进程等待必要性
为什么要进程等待
- 子进程在退出时,父进程如果对其不管不问,就会造成僵尸问题,从而造成内存泄漏。
- 进程如果进入了僵尸状态,就没人能够将其干掉,无法杀死一个已经死去的进程。
- 父进程需要知道派发给子进程的任务完成得如何,结果是否正确,是否正常退出等。
- 因此父进程需要通过进程等待得方式,去回收子进程所占用得资源,并且获取子进程退出的相关信息。
进程等待能做什么
- 父进程能够通过 wait 方法,回收子进程的资源 (必然)。
- 父进程能够通过 wait 方法,获取子进程的退出信息 (退出码、退出信号) (可选)。
02. 进程等待的方法
2.1 wait 方法
wait 函数原型
#include <sys/wait.h>
#include <sys/types.h>
pid_t wait (
int *status); // 输出型参数,能通过该参数获取子进程退出结果,默认置为空即可
wait 函数功能
- 父进程阻塞等待任意一个子进程,子进程不退则父进程不退。
- 该函数能够回收子进程资源,以及获取子进程的 pid。
wait 函数返回值
- 返回值 > 0:返回值是所等待的子进程的 pid。
- 返回值 < 0:等待失败。
函数用例
- 演示父进程回收子进程资源。
#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>
using std::cout;
using std::endl;
int main()
{
// 创建子进程
pid_t id = fork();
// 子进程执行自己的代码
if (0 == id)
{
for (size_t i = 0; i < 5; i++)
{
cout << "child process is running, pid: "
<< getpid() << ", ppid: " << getppid() << endl;
sleep(1);
}
cout << "子进程准备退出,马上变僵尸" << endl;
exit(0);
}
cout << "父进程休眠" << endl;
sleep(8);
cout << "父进程开始回收僵尸进程" << endl;
// 父进程阻塞等待任意子进程
pid_t rid = wait(nullptr);
// 等待成功,rid 是子进程的 pid
if (rid > 0)
cout << "wait success, rid: " << rid << endl;
cout << "父进程回收僵尸进程成功" << endl;
sleep(1);
return 0;
}
2.2 waitpid 方法
waitpid 函数原型
#include <sys/wait.h>
#include <sys/types.h>
pid_t waitpid (pid_t pid, int *status, int options);
waitpid 函数参数
- pid_t pid:
- pid 为 -1 时,等待任意一个子进程,功能等同 wait。
- pid > 0 时,指定具体想要等待的那个进程。
- int *status:输出型参数,能通过该参数获取子进程退出结果,默认置为空即可
- int options:指定父进程的等待方式,为 0 则让父进程进行 阻塞 等待,非 0 则进行 非阻塞 等待。
waitpid 函数功能
- 回收子进程资源,解决僵尸问题的同时,还能够获取子进程退出信息。
waitpid 函数返回值
- 返回值 > 0:返回值是所等待的子进程的 pid。
- 返回值 < 0:等待失败。
函数用例
#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>
using std::cout;
using std::endl;
int main()
{
// 创建子进程
pid_t id = fork();
// 子进程
if (0 == id)
{
for (size_t i = 0; i < 5; i++)
{
cout << "child process is running, pid: "
<< getpid() << ", ppid: " << getppid() << endl;
sleep(1);
}
cout << "子进程准备退出,马上变僵尸" << endl;
exit(1);
}
cout << "父进程休眠" << endl;
sleep(8);
cout << "父进程开始回收僵尸进程" << endl;
// 用来获取子进程的退出信息 (退出码 + 退出信号)
int status;
// 父进程阻塞等待由 id 指定的子进程
pid_t rid = waitpid(id, &status, 0);
// 等待成功,rid 是子进程的 pid
if (rid > 0)
{
cout << "等待成功, 子进程 pid: " << rid
<< " 子进程退出码: " << status << endl;
}
sleep(1);
return 0;
}
- 这里子进程的退出码之所以不是 1 而是 256 的原因是变量 status 装着退出码和退出信号两部分信息。
- 在 status 中,只有低 16 位用于存储 退出码 + 退出信号。
- 正常退出时,这 16 位的高 8 位存储退出码,低 8 位默认全部存 0。
- 异常退出时,这 16 位的高 8 位不使用,低 7 位 存储终止信号,还有 1 位是 core dump 标志位。
- 当前子进程没有出异常,因此为 1 的退出码在内存中是这样存储的 0000 0001 0000 0000,因此按照整形的方式打印出的结果就是 256 了。
03. 获取子进程状态
3.1 使用位运算获取退出信息
- 获取子进程退出码:将获取的状态码右移 8 位再和 0xFF 相与即可,(status >> 8) & 0xFF
- 获取子进程退出信号:将获取的状态码和 0x7F 相与即可,status & 0x7F
// 1. 使用位运算获取退出信息
#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>
using std::cout;
using std::endl;
int main()
{
// 创建子进程
pid_t id = fork();
// 子进程
if (0 == id)
{
for (size_t i = 0; i < 5; i++)
{
cout << "child process is running, pid: "
<< getpid() << ", ppid: " << getppid() << endl;
sleep(1);
}
cout << "子进程准备退出,马上变僵尸" << endl;
exit(1);
}
cout << "父进程休眠" << endl;
sleep(8);
cout << "父进程开始回收僵尸进程" << endl;
// 用来获取子进程的退出信息 (退出码 + 退出信号)
int status;
// 父进程阻塞等待由 id 指定的子进程
pid_t rid = waitpid(id, &status, 0);
// 等待成功,rid 是子进程的 pid
if (rid > 0)
{
cout << "等待成功, 子进程 pid: " << rid
<< " 子进程退出信号: " << (status & 0x7F)
<< " 子进程退出码: " << ((status >> 8) & 0xFF) << endl;
}
sleep(1);
return 0;
}
3.2 使用宏获取退出信息
- WIFEXITED(status):如果子进程是正常退出,则该宏的值位真。(用以查看进程是否是正常退出)
- WEXITSTATUS(status):如果 WIFEXITED 的值为真,则提取子进程退出码。(用以查看进程的退出码)
// 2. 使用宏获取退出信息
#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>
using std::cout;
using std::endl;
int main()
{
// 创建子进程
pid_t id = fork();
// 子进程
if (0 == id)
{
for (size_t i = 0; i < 5; i++)
{
cout << "child process is running, pid: "
<< getpid() << ", ppid: " << getppid() << endl;
sleep(1);
}
cout << "子进程准备退出,马上变僵尸" << endl;
exit(1);
}
cout << "父进程休眠" << endl;
sleep(8);
cout << "父进程开始回收僵尸进程" << endl;
// 用来获取子进程的退出信息 (退出码 + 退出信号)
int status;
// 父进程阻塞等待由 id 指定的子进程
pid_t rid = waitpid(id, &status, 0);
// 等待成功,rid 是子进程的 pid
if (rid > 0)
{
// 子进程是正常退出的, 用户只需要关心退出码即可
if (WIFEXITED(status))
{
cout << "等待成功, 子进程 pid: " << rid
<< " 子进程退出码: " << WEXITSTATUS(status) << endl;
}
}
sleep(1);
return 0;
}
04. 非阻塞轮询访问
- 父进程在阻塞等待子进程时,父进程这时候什么事的做不了,只能等待子进程退出才能去做自己的事,效率太低,非阻塞轮询访问就因此出现。
1. 非阻塞轮询访问
- 父进程每隔一段时间就执行一次系统调用,判断子进程是否退出。
- 如果子进程没有退出,则父进程子继续执行自己的任务。
- 如果子进程已经退出,则父进程回收子进程的资源及退出信息。
2. 如何使用非阻塞等待
- 将 waitpid 函数的第三个参数改成非 0 值即可,一般是用 WNOHANG 宏作为参数。
pid_t rid = waitpid(id, &status, WNOHANG);
3. 非阻塞轮询访问实例
// 非阻塞轮询访问
#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>
using std::cout;
using std::endl;
int main()
{
// 创建子进程
pid_t id = fork();
// 子进程
if (0 == id)
{
for (size_t i = 0; i < 5; i++)
{
cout << "子进程正在运行, pid: "
<< getpid() << ", ppid: " << getppid() << endl;
sleep(1);
}
exit(1);
}
// 用来获取子进程的退出信息 (退出码 + 退出信号)
int status = 0;
while (true)
{
// 父进程以非阻塞状态等待子进程退出
pid_t rid = waitpid(id, &status, WNOHANG);
if (rid > 0) // 等待成功, 子进程已经退出
{
cout << "等待成功, 子进程 pid: " << rid
<< " 子进程退出码: " << WEXITSTATUS(status) << endl;
break; // 不需要再执行循环等待了
}
else if (0 == rid) // 等待成功,子进程还没退出, 父进程可以执行其他任务
{
cout << "子进程还未退出, 父进程执行其他任务" << endl;
// ... 父进程在等待子进程退出期间要执行的任务
}
else // 等待失败
{
perror("waitpid");
break;
}
sleep(1); // 父进程每隔 1 秒查询一次子进程是否退出
}
sleep(1);
return 0;
}
🌈 Ⅳ 进程程序替换
01. 替换原理
1. 什么是程序替换
- 在用 fork 创建子进程之后,子进程执行的是和父进程相同的程序 (但是有可能执行的是不同的代码分支),这样创建子进程就没多大意义了。
- 如果创建的子进程想执行其他程序的代码,需要调用 exec 系列函数去执行其他程序,这种操作被称之为程序替换。
- 当进程调用 exec 系列函数时,该进程的用户空间代码和数据会完成被新的程序所替换,从一个新的程序启动例程看i是执行。
- 调用 exec 系列函数不会创建新进程,因此在调用 exec 系列函数的前后该进程的 pid 不变。
02. 替换函数
- exec 系列函数总共有 6 种,都是以 exec 开头的,这些函数统称为 exec 函数。
#include <unistd.h>
int execl (const char *path, const char *arg, ...);
int execlp (const char *file, const char *arg, ...);
int execle (const char *path, const char *arg, ..., 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[]);
03. 函数解释
1. execl 函数
int execl ( // 替换失败时返回 -1,成功时没有返回值
const char *path, // 进程要执行的程序的所在路径
const char *arg, ...); // 参数列表,表示如何执行该程序 (命令行怎么写参数就怎么传), 以 NULL 结尾
2. execlp 函数
int execlp (
const char *file, // 要执行的程序名,无需提供程序路径,会自动去 PATH 环境变量中查找
const char *arg, ...); // 参数列表,表示如何执行该程序 (命令行怎么写参数就怎么传)
3. execle 函数
int execle (
const char *path, // 进程要执行的程序的所在路径
const char *arg, ..., // 参数列表,表示如何执行该程序 (命令行怎么写参数就怎么传)
char *const envp[]); // 自己提供环境变量给子进程
4. execv 函数
int execv (
const char *path, // 进程要执行的程序的所在路径
char *const argv[]); // 该参数是个指针数组,用以存储参数,表示如何执行该程序
5. execvp 函数
int execvp (
const char *file, // 要执行的程序的程序名,无需提供程序路径
char *const argv[]); // 参数数组,表示如何执行该程序
6. execvpe 函数
int execvpe(
const char *file, // 要执行的程序的程序名,无需提供程序路径
char *const argv[], // 参数数组,表示如何执行该程序
char *const envp[]); // 自己提供环境变量给子进程
04. 命名理解
解释 exec 之外的每个字母所表示的含义
- l (list) : 表示参数采用列表。
- v (vector) : 表示参数用数组。
- p (path) : 有 p 表示会自动搜索环境变量 PATH。
- e (env) : 表示需要用户自己维护环境变量。
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 否,需要自己提供程序路径 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 否,需要自己提供程序路径 | 否,需要自己配置环境变量 |
execv | 数组 | 否,需要自己提供程序路径 | 是 |
execvp | 数组 | 是 | 是 |
execve | 数组 | 否,需要自己提供程序路径 | 否,需要自己配置环境变量 |
05. 函数用例
5.1 execl 函数使用示例
- 让当前进程以 ls -a -l 的方式执行 /usr/bin/ls 路径所指定的程序。
int main()
{
cout << "程序替换开始" << endl;
// 当前进程以 ls -a -l 的方式跑去执行 /usr/bin 目录下的 ls 程序
execl("/usr/bin/ls", "ls", "-a", "-l", nullptr);
cout << "程序替换结束" << endl;
return 0;
}
5.2 execlp 函数使用示例
- 用第一个参数作为要执行的程序的程序名,无需自己补全程序路径。
int main()
{
pid_t id = fork();
// 由子进程去进程程序替换
if (0 == id)
{
cout << "程序替换开始" << endl;
// 以 ls -a -l 的方式执行 ls 程序,无需自己补全程序路径
execlp("ls", "ls", "-a", "-l", nullptr);
cout << "程序替换结束" << endl;
exit(1);
}
// 父进程阻塞等待子进程
pid_t rid = waitpid(id, nullptr, 0);
if (rid > 0)
cout << "等待成功" << endl;
return 0;
}
5.3 execle 函数使用示例
// 演示 execle 函数
int main()
{
// 定义环境变量
char *const env[] = { (char*)"hello=world", (char*)"PAHT=/" };
pid_t id = fork();
// 由子进程去进程程序替换
if (0 == id)
{
cout << "程序替换开始" << endl;
// 以 mytest 的方式执行 当前目录下的 mytest.exe 文件
execle("./mytest.exe", "mytest", NULL, env);
}
pid_t rid = waitpid(id, nullptr, 0);
if (rid > 0)
cout << "等待成功" << endl;
return 0;
}
5.4 execv 函数使用示例
// 演示 execv 函数
int main()
{
pid_t id = fork();
// 由子进程去进程程序替换
if (0 == id)
{
// 存储参数的指针数组
char *argv[] = { (char*)"ls", (char*)"-a", (char*)"-l" };
cout << "程序替换开始" << endl;
// 以 ls -a -l 的方式执行指定路径下的程序,
execv("/usr/bin/ls", argv);
}
pid_t rid = waitpid(id, nullptr, 0);
if (rid > 0)
cout << "等待成功" << endl;
return 0;
}
5.5 execvp 函数使用示例
// 演示 execvp 函数
int main()
{
cout << "演示 execvp 函数" << endl;
pid_t id = fork();
// 由子进程去进程程序替换
if (0 == id)
{
// 存储参数的指针数组
char *argv[] = { (char*)"ls", (char*)"-a", (char*)"-l" };
cout << "程序替换开始" << endl;
// 以 ls -a -l 的方式执行 ls 程序,无需指定程序所在路径
execvp("ls", argv);
}
pid_t rid = waitpid(id, nullptr, 0);
if (rid > 0)
cout << "等待成功" << endl;
return 0;
}
5.6 execvpe 函数使用示例
// 演示 execve 函数
int main()
{
// 定义环境变量
char *const env[] = { (char*)"hello=world", (char*)"PAHT=/" };
pid_t id = fork();
// 由子进程去进程程序替换
if (0 == id)
{
// 存储参数的指针数组
char *argv[] = { "mytest" };
cout << "程序替换开始" << endl;
// 以 mytest 的方式执行 当前目录下的 mytest.exe 文件
execve("./mytest.exe", argv, env);
}
pid_t rid = waitpid(id, nullptr, 0);
if (rid > 0)
cout << "等待成功" << endl;
return 0;
}