文章目录
- 五、fork 函数,创建进程
- 写时拷贝
- 六、进程终止
- 1. 退出码
- 2. 如何终止程序
- 七、进程等待
- 1. 概念
- 2. wait 函数
- waitpid 函数 🔺
- 3. 阻塞等待
五、fork 函数,创建进程
#include <unistd.h>
pid_t fork(void);
返回值:
- 子进程 返回 0
- 父进程 返回 子进程 id
- 出错 返回 -1
进程调用 fork,当控制转移到内核中的 fork 代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
fork 之前,父进程独立执行,
fork 之后,父子两个执行流分别执行 fork 后面的代码。
注意,fork之后,谁先执行完全由调度器决定。
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副
本。具体见下图:
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
六、进程终止
如何理解进程退出?
OS内少了一个进程,OS就要释放进程对应的内核数据结构 + 代码和数据(如果有独立的)
进程结束情况分类:
a. 执行完了,正常退出
- 结果正确
- 结果不正确
b. 崩溃了,进程异常 (通过 信号 的方式,后面更新)
- 崩溃的本质: 进程因为某些原因,导致进程收到了来自操作系统的信号
kill -9
1. 退出码
我们说,进程是有退出码的。
比如 main 函数中的 return x; 正常执行完了
- 结果正确,退出码为 0,
- 结果不正确, 1、2、3、4 则表示不同的原因(供用户进行进程退出健康状态的判定)
echo $?
可以 查看 最近执行的进程的 退出码
echo $?
2. 如何终止程序
正常终止有三种方式:
① main 函数 return。
- 其他函数return,仅仅代表该函数返回
- 进程执行,本质是main执行流执行!
②
void exit(int status)
函数退出
- 头文件
#include <stdlib.h>
- status 代表就是进程的退出码
- 等价于 main return XXX
- 会刷新缓冲区、关闭流等
- 其实 exit 函数 也是封装调用的 _exit 函数
③
void _exit(int status)
函数退出
- 头文件
#include <stdlib.h>
- status 代表就是进程的退出码
- 直接退出程序,不处理缓冲区等
由此我们可以推测出的就是:
缓冲区,一定不在 OS 内,不然肯定都能刷新了,事实上并没有
这是因为用户层和 OS 之间,还有一个 C库。(后面更新)
异常退出
ctrl + C
信号终止(信号!!后面更新)
七、进程等待
1. 概念
进程等待 就是 通过系统调用,获取 子进程 退出码 或者 退出信号 的方式,顺便释放内存问题。
进程等待的作用:
-
避免内存泄漏
-
获取子进程执行的结果(如果必要)
-
- 对于进程退出的结果查验,我们通过的还是上述提过的 信号 + 退出码 的方案。
等待,实际上就是 父进程 对 子进程 的等待
2. wait 函数
pid_t wait(int *status)
头文件:#include <sys/wait.h>
返回值:等待成功则返回 子进程 的 PID
waitpid 函数 🔺
pid_t waitpid(pid_t pid, int *status, int options);
头文件#include <sys/wait.h>
返回值:
- > 0,success(返回的是对应子进程的 pid)
- ==-1,failed(一般不会失败,pid 传错了会导致失败)
== 0,在选项设置了 WNOHANG 的情况下,发现没有已退出的子进程可收集参数 pid:
- >0,表示等待指定的子进程
- ==-1,表示等待任一个子进程,与 wait 等效
参数 status:是一个输出型参数
- 我们会想得到,子进程的退出状态,即 信号 + 返回码
- 如何用一个整数 返回两个整数…?实际上这里的 status 是类似位图作用的。信号和返回码都不超过一定值,用一个整数位存储足够啦!(见下文)
- WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status):若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)
参数 options:
- 0,默认父进程进行 阻塞等待
- WNOHANG,父进程进行 非阻塞轮询
🌰使用举例:
int status = 0;
pid_t ret_id = waitpid(id, &status, WNOHANG);
if(ret_id < 0)
{
printf("waitpid error!\n");
exit(1);
}
else if(ret_id == 0) // 子进程还没结束
{
RunTask();
sleep(1);
continue;
}
else // 子进程结束了
{
if(WIFEXITED(status)) // 正常退出
{
printf("wait success, child exit code: %d\n", WEXITSTATUS(status));
}
else // 异常退出
{
printf("wait success, child exit signal: %d\n", status & 0x7F);
}
break;
}
status 记录 信号 和 退出码 的方式:
00000000 00000000 00000000 00000000
- 高 16 位不需要
- 低 16 位中,次低的 8 位(黑体比特位),储存 退出状态
- 最低 7 位(斜体比特位),储存 终止信号
- 第8位,是 core dump 标志(后面更新,信号篇)
想要自己访问 信号 和 退出码,我们用如下方式:
// 退出码
(status>>8) & 0xFF;
// 终止信号
status & 0x7F;
所以,
父进程 如何拿到 子进程 的退出码和终止信号的呢?
实际上,进程的 PCB 中,就有这两个成员值。
struct task_struct
{
// ...
int exit_code;
int exit_signal;
task_struct* parent;
};
- 当子进程执行完毕时 main 函数的退出码,写入 PCB 的 exit_code 里
- 如果出异常,OS 会把遇到异常时的信号编号写到 PCB 的 exit_signal 里
- 进程退出之后,OS 会将进程 PCB 维护起来,通过系统调用接口 waitpid / wait,就可以让用户拿到这些数据。
3. 阻塞等待
父进程在 wait 的时候,如果子进程没有退出,父进程只能一直调用 waitpid 进行等待,这就叫 阻塞等待。
- 可以理解为父进程从 R 状态,开始等待,进入 S 状态。
- 这个等待不在运行队列,可以理解为在一个只有它一个节点的等待队列里等待。
- 等待的同时,子进程的 CPB 中找到并挂接到父进程,直到子进程结束,父进程再回到运行队列(R 状态)
如果我们不想 阻塞等待,即 不想在 waitpid 处卡住呢?
- 非阻塞轮询(WNOHANG,waitpid 的最后一个参数)
- 故,我们在日常这样情况的时候,说 这个程序 夯住了。