文章目录
- 3.进程等待
- 3.1什么是进程等待
- 3.2为什么要进程等待
- 3.3如何进行进程等待?
- 1.wait
- 2.waitpid
- 2.1函数的讲解
- 2.2status的理解
- 2.3代码理解
- 3.4学后而思
- 1.直接用全局变量获取子进程退出码可以吗?如下
- 2.进程具有独立性 退出码是子进程的数据 父进程是如何拿到退出码的
- 3.对内存泄露的认识
- 4.对于上文中提到的系统设置的宏
- 5.阻塞等待和非阻塞等待
- 6.status相关宏加入后的简便写法
- 7.非阻塞式等待的实现
3.进程等待
3.1什么是进程等待
进程等待: 进程的一种状态 父进程等待子进程退出时的一个过程
3.2为什么要进程等待
进程退出时 会关闭所有的文件描述符 释放在内存中的代码和数据 内核数据结构task_struct会暂时保留 里面存放着进程的退出状态以及统计信息等 父进程创建子进程 让子进程来处理事务 父进程需要得知子进程对任务的完成情况即上述的三种情况且需要获取子进程的退出状态 如果子进程先于父进程退出 父进程则无法获取子进程的退出状态 子进程此时就会处于僵尸状态
所以进程等待的两大原因:
- 获取子进程的退出状态 避免出现僵尸进程 减少内存泄漏的概率[回收子进程资源]
- 获取子进程对任务的完成情况[获取子进程退出信息]
3.3如何进行进程等待?
1.wait
- 返回值:
函数调用成功+目标进程成功改变状态 返回目标进程的pid
失败返回-1 - 参数:
输出型参数 获取子进程退出状态 不关心则可以设置成为NULL 表示不获取
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int code = 0;
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
exit(1); //进程终止 结果不正确
}
else if (id == 0)
{
//子进程
int cnt = 5;
while (cnt)
{
printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
sleep(1);
cnt--;
}
exit(0); //子进程运行5s后正常退出
}
else
{
//父进程
printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(6);
pid_t ret = wait(NULL); //阻塞式的等待
if (ret > 0)
{
printf("成功等待子进程改变状态, ret = %d\n", ret);
}
while (1)
{
printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
}
当子进程运行5s完退出 父进程还在运行 此时子进程处于僵尸状态 当父进程运行了7s 父进程执行了wait()
函数 我们可以理解为 父进程被调度到子进程的后面或阻塞队列 直到子进程从僵尸状态改变状态为终止态 父进程才继续运行 从man手册我们可以查到wait()函数成功等待返回值为被终止掉的子进程的PID 没有成功等待则返回-1
2.waitpid
2.1函数的讲解
waitpid(pid, NULL, 0) == wait(NULL);
返回值
- 正常返回 waitpid返回收集到的子进程的PID
- 没有已退出的子进程可收集即所有的子进程都还在运行 返回0 当
options = WNOHANG
时 - 函数调用出错 返回-1 errno被设置成相应的值以指示错误原因(错误原因如目标子进程不存在)
pid_t pid:
传参数pid = -1 表示父进程要等待任意一个子进程 ==
传参数pid = Pid > 0 等待PID是Pid的进程
int* status:
- 输出型参数,由操作系统填充
int status = 0; waitpid(pid, &status, 0);
操作系统会根据子进程PCB中的退出码和退出信号,将子进程的退出信息通过status反馈给父进程 - 如果传递NULL,表示不关心子进程的退出状态信息。
- 程序运行结果有三种 status并不是按照整数整体来使用的 它是按照比特位的方式 将32个比特位进行划分 此文只讲解低16位
- 实际上为了更方便使用status 即不再是用位运算 OS提供了宏定义 调用即可
WIFEXITED(status)
:wait if exited
进程是否正常退出 若为正常终止子进程 返回真
WEXITSTATUS(status)
:wait exit status
获取进程退出码 若WIFEXITED
非零==>即子进程正常退出(代码跑完了 不是被信号杀死的) 提取子进程退出码
int options:
默认为0 表示阻塞式等待
options = 1: 非阻塞式等待
- 为了不再程序中写一些数字 通常用宏来指示特定含义
如1就可用WNOHANG
来指示wait no hang
表示父进程进行非阻塞式等待
- 子进程都在运行 函数返回0 不进行持续等待
- 子进程正常退出 函数返回子进程PID
2.2status的理解
- 已知可以通过查看status来获取子进程的退出码进而得知子进程的运行结果/退出状态信息
- 简单来说 进程终止有两种方式 代码跑完和没跑完 代码跑完无论结果正确都叫正常终止 而如果是没跑完就终止即为异常终止 即程序崩溃或异常退出 (我们之前没学过进程 所以一直说程序崩溃或程序退出 其实正确的说法应为进程异常退出或进程崩溃)
- 进程崩溃/异常退出 本质是OS通过发信号的方式杀掉了进程 可以通过查看进程收到的信号编号来得知子进程收到了几号信号 如果进程崩溃/异常退出 退出码就没有意义 所以我们不仅要获取退出码 还要通过查看收到的信号编号是0(正常跑完) 1~31(异常/崩溃) 来判断退出码是否有意义
OS都能发哪些信号呢? 1~31重点了解 32/33没有 34-64了解即可
- 父进程调用waitpid()函数之前定义一个int变量 调用waitpid()函数时 把这个变量地址传给waitpid()函数
- 这个函数不仅会等待子进程改变状态 还会把子进程的退出码和进程收到的信号编号以上图形式填充给你传过来的变量 即status 通过对status的位操作可以查看退出码和信号编号
- 程序异常,可能是内部代码有问题,也可能是外力杀掉 (子进程代码是否跑完是不确定的)
2.3代码理解
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
exit(1);
}
else if (id == 0)
{
//子进程
int cnt = 5;
while (cnt)
{
printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
sleep(1);
cnt--;
}
exit(15);
}
else
{
//父进程
printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
int status = 0;
pid_t ret = waitpid(id, &status, 0); //阻塞式的等待
if (ret > 0)
{
int signal_number = status & 0x7F;
int exit_code = (status >> 8) & 0xFF;
printf("等待子进程改变状态, ret: %d, 子进程收到的信号编号: %d, 子进程退出码: %d\n", ret, signal_number, exit_code);
}
}
}
信号编号取最低7位: 按位与0000 0000 0000 0000 0000 0000 0111 1111
退出码取次低8位: 按位与0000 0000 0000 0000 0000 0000 1111 1111
跟1按位与: 值不变 是0的是0 是1的是1
跟0按位与: 值全变0
通过可以修改代码 可以看到对应的信号
子进程死循环 外部 kill -9 子进程pid : 杀死子进程
signal_number = 9(SIGKILL:signal kill)
exit_code = 0 sn!=0 退出码无意义
cnt--;
signal_number = 11(SIGSEGV:signal segmentation violation)
exit_code = 0 sn!=0 退出码无意义
int * p = NULL;
*p = 100;
signal_number = 8(SIGFPE:signal float point error)
exit_code = 0 sn!=0 退出码无意义
int a = 10;
a /= 0;
3.4学后而思
1.直接用全局变量获取子进程退出码可以吗?如下
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int code = 0;
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
exit(1);
}
else if (id == 0)
{
//子进程
int cnt = 5;
while (cnt)
{
printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
sleep(1);
cnt--;
}
code = 15;
exit(15);
}
else
{
//父进程
printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
int status = 0;
pid_t ret = waitpid(id, &status, 0); //阻塞式的等待
if (ret > 0)
{
int signal_number = status & 0x7F;
int exit_code = (status >> 8) & 0xFF;
printf("等待子进程改变状态, ret: %d, 子进程收到的信号编号: %d, 子进程退出码: %d\n", ret, signal_number, exit_code);
printf("code: %d\n", code);
}
}
}
- 答案是不行. 已知父进程通过wait/waitpid可以拿到子进程的退出结果(退出码+收到的信号)
- 1. 父进程 子进程均有全局变量code 初始值均为0 当子进程对code修改发生写时拷贝 父进程去访问code时是他自己的code仍为0
- 2. 即便你显示传一个值把他作为退出码 你怎么直到他的退出码是几? 就是说 退出码是显示子进程的运行结果的 你不知道它结果正确或是发生错误 进而也就不知道他的退出码是几
- 3. 即便你可以拿到退出码 也拿不到子进程收到的信号
2.进程具有独立性 退出码是子进程的数据 父进程是如何拿到退出码的
僵尸进程: 子进程已死亡等待父进程读取状态
- 僵尸进程的代码和数据已经释放但是PCB还存在 且
task_struct
里的int exit_code, exit_signal:
字段保留了进程退出时的退出码和退出信号(不仅是僵尸进程的PCB会保留 任何进程退出时都会把退出码和退出信号保留在)int exit_code, exit_signal:
- 那么系统调用接口
wait/waitpid
可以读取子进程的PCB里的int exit_code, exit_signal:
把退出码和退出信号以位图形式存在status
中 然后父进程就可以再反向获取 - 父进程没办法直接获取子进程的信息 但是可以通过调用系统接口来获取
3.对内存泄露的认识
- 用户写的C/C++程序中malloc/new的空间当进程终止 即便这些空间有泄露 OS也已经回收
- 子进程死亡父进程如果一直不读取子进程的退出状态 那么子进程将一直处于Z状态 子进程的PCB属于内核数据结构 需要OS来释放 如果父进程不管 将影响其他进程
4.对于上文中提到的系统设置的宏
- linux是用C语言写的 linux将自己一些系统调用接口二次封装成函数提供使用 除了一些系统调用接口/函数外 还有一些宏定义 比如WNOHANG
- 一些运维的程序员会说如这个进程hang住了之类的话 其实就是我们通常说的卡了 可能是在等待某种资源如网络/磁盘等 可能是进程太多了 CPU忙不过来了 这个进程hang住了 说这个进程hang住 就是说它要么在阻塞队列中 要么等待CPU调度
grep -ER 'WNOHANG' /usr/include/
grep -ER是一个Linux命令,用于在文件中递归搜索指定的字符串模式。其中,E表示使用扩展正则表达式,R表示递归搜索子目录。下面是一个例子:
假设我们有一个名为test.txt的文件,内容如下:
hello world
hello grep
我们可以使用grep -ER命令来搜索包含“hello”的行:
grep -ER "hello" test.txt
输出结果为:
test.txt:hello world
test.txt:hello grep
如果我们只想搜索以“h”开头的行,可以使用正则表达式“^h”:
grep -ER "^h" test.txt
输出结果为:
test.txt:hello world
test.txt:hello grep
如果我们想要搜索以“h”开头并且包含“o”的行,可以使用正则表达式“^h.*o”:
grep -ER "^h.*o" test.txt
输出结果为:
test.txt:hello world
5.阻塞等待和非阻塞等待
1先来回顾一下阻塞状态和挂起状态的知识
- 阻塞等待一般是在内核中阻塞 等待资源就绪去完成自己的工作 而一个进程一直在阻塞即资源一直无法就绪 这个进程的代码和数据就被移到磁盘SWAP分区了 即此进程被切换到挂起状态了
- 非阻塞等待: 父进程调用waitpid来等待子进程 如果子进程没有退出 他不像阻塞等待那样一直等着直至父进程被挂起还在等 他会直接返回 这样父进程在子进程运行的这一段时间还可以做其他事情 只不过中途需要再去看看子进程运行状况(下面讲怎么中途查看)
- 关于网络部分的代码 大部分时IO类别 阻塞和非阻塞接口非常多
对进程等待的认识
- 在阻塞式等待下 只有子进程退出的时候,waitpid函数才会进行返回父进程只是挂起 仍然存活
- waitpid/wait 让进程退出具有一定的顺序性 将来可以让父进程进行更多的收尾工作.
6.status相关宏加入后的简便写法
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id < 0)
{
printf("创建子进程失败!\n");
exit(1);
}
else if (id == 0)
{
printf("我是子进程: pid: %d,我将异常退出!\n", getpid());
int* p = NULL;
*p = 1; // 引发异常
}
else
{
printf("我是父进程: pid: %d,我将耐心地等待子进程!\n", getpid());
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0)
{
// 等待成功
//printf("父进程等待成功, 退出码: %d, 退出信号: %d\n", (status>>8)&0xFF, status & 0x7F);
if (WIFEXITED(status))
{
printf("子进程正常退出,退出码: %d\n", WEXITSTATUS(status));
}
else if (WIFSIGNALED(status))
{
printf("子进程异常退出,信号编号: %d\n", WTERMSIG(status));
}
}
}
return 0;
}
7.非阻塞式等待的实现
当子进程未退出 父进程可以处理其他事务 父进程要处理的事务不是一次就确定好的 可能在日后会有新的事务添加进来 为了封装/降低耦合 可以设置指针数组/回调函数 只需要编写Load函数 就可以增加父进程的任务单
typedef void (*pfunc)(); //函数指针类型
std::vector<pfunc> vfunc; //函数指针数组
void fun_one()
{
printf("临时任务1\n");
}
void fun_two()
{
printf("临时任务2\n");
}
void Load()
{
vfunc.push_back(fun_one);
vfunc.push_back(fun_two);
}
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int cnt = 5;
while(cnt)
{
printf("我是子进程: %d\n", cnt--);
sleep(1);
}
exit(123); // 123: 测试 无意义
}
else
{
int quit = 0;
while(!quit)
{
int status = 0;
pid_t res = waitpid(-1, &status, WNOHANG); //非阻塞方式等待
if(res > 0)
{
//函数调用成功 && 子进程已退出
printf("等待子进程退出成功, 退出码: %d\n", WEXITSTATUS(status));
quit = 1;
}
else if( res == 0 )
{
//函数调用成功 && 子进程未退出
printf("子进程未退出,父进程可以处理其他事务\n");
if(vfunc.empty())
Load();
for(auto func : vfunc)
{
func();
}
}
else
{
//等待失败
printf("函数调用失败!\n");
quit = 1;
}
sleep(1);
}
}
}