【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)

在这里插入图片描述

前言

什么是写时拷贝

创建子进程,没必要将不会被访问
或只读的数据重新拷贝一份

只有将来会被父或子进程写入的数据才
值得拷贝,提前拷贝也并不会立马使用
一般而言即使是os,也无法提前知道
哪些空间可能被写入

所以os选择写时拷贝技术对父子进程
数据进行分离

写时拷贝本质是一种延时申请
当你想去修改数据的时候os再帮你去拷贝

写时拷贝实现图
在这里插入图片描述
为什么要使用写时拷贝

因为有写时拷贝技术的存在
父子进程得以彻底分离
完成进程独立性技术保证
写时拷贝是延时申请技术
可以提高整机内存使用率

一、EIP(pc指针)

进程 = 内核数据结构 + 进程代码和数据
内核数据结构是操作系统维护的
进程代码和数据一般从磁盘中来
也就是C/C++程序加载后的结果

父子进程代码共享是所有代码共享
代码汇编之后,会有多行代码
且每行代码加载到内存都有对应的地址
因进程可能随时被中断(可能没执行完)
下次回来还在之前的位置继续运行
就要求cpu随时记录当前进程执行的
位置,所以cpu内有个EIP
在教程上也叫pc指针(程序计数器)
pc指针永远记录当前正在执行代码
的下一行代码的地址

寄存器在cpu内只有一份
而寄存器内的数据则可以有多份
这些数据就叫做进程的上下文数据
在创建子进程时这些数据也要给子进程一份
虽然父子进程各自调度,各自修改EIP
但却已经不重要了,因为子进程已经认为
自己的EIP起始值就是fork之后的代码
但并不代表子进程看不到fork之前的代码

二、进程终止

当进程终止时,要释放进程申请的
内核数据结构和对应的数据和代码
本质就是释放系统资源

2.1 进程终止的常见方式?

  1. 代码跑完,结果正确
  2. 代码跑完,结果不正确
  3. 代码没跑完,程序崩溃

main函数是什么,返回值的意义?

main函数是进程退出码
main函数返回值并不总是0
也可以是其他的
返回值返回给上一级进程
用来评判给进程执行结果用的
返回0表示运行结果正确
非0标识的是运行结果的不正确
非零有无数个,不同非零值
标识不同错误原因

main函数返回值使用

int sum(int top) // 求1 + 到 99 的和
{
	int s = 0;
	for (int i = 1; i < top; i++)
	{
		s += i;
	}
	return s;
}

int main()
{
	int ret = 0;
	int res = sum(100);
	if (res != 4950)
	{
		// 代码将来的运行结果不正确
		ret = 1;
	}
	return ret;
}

退出码为0,代码运行结果正确
在这里插入图片描述

strerror() 函数
把数字转化成对应的错误码信息

echo $?
获取最近一个行程执行完毕的退出码

程序崩溃的时候,退出码是没有意义的
一般而言,退出码对应的return语句
没有被执行

2.2 如何用代码终止一个程序?

进程常见退出方法

  1. 从main返回
    main函数内return语句就是终止进程的
  2. 调用exit
    exit() 函数
    在代码的任何地方调用
    都表示直接终止进程

终止之前exit需要

  1. 执行用户通过 atexit或on_exit定义的清理函数
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit
  1. _exit
    跟exit一样
    不同的是_exit是直接终止
    并不会清理函数和写入缓冲区数据
    如图
    在这里插入图片描述

而缓冲区是C标准库维护的
在平时推荐用exit()函数

三、进程等待

3.1 进程等待必要性

  • 子进程退出父进程如果不管不顾就可能
    造成‘僵尸进程’问题,进而造成内存泄漏
  • 进程一旦变成僵尸状态,那就刀枪不入
    “杀人不眨眼”的kill -9 也无能为力,因为
    谁也没有办法杀死一个已经死去的进程
  • 父进程派给子进程的任务完成的如何
    我们需要知道。如,子进程运行完成
    结果对还是不对,或者是否正常退出
  • 父进程通过进程等待的方式,回收
    子进程资源,获取子进程退出信息

3.2 制造一个僵尸进程

代码测试

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.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(0); // 子进程执行完后退出
     }
     else
     {                                                                                                    
         // 父进程
         while (1)
         {
             printf("我是父进程:pid: %d, ppid: %d\n", getpid(), getppid());
             sleep(1);
         }
     }
}

执行结果
在这里插入图片描述

开启另一个窗口检测命令
while :; do ps ajx | head -1 && ps ajx | grep myproc | grep -v grep; sleep 1; echo “-------------------------------”; done

循环完5次,子进程退出
而父进程还在执行
此时子进程变成Z僵尸状态
defunct表示他是无效的
在这里插入图片描述

3.3 wait回收僵尸状态的子进程

在这里插入图片描述

wait:等一个进程,直到这个进程状态发生变化
简而言之就是父进程等子进程死亡
如果不死就一直等,直到你的状态
由R或S状态变成Z状态然后回收他
成功返回子进程ID,不成功返回-1

代码测试

#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(0); // 子进程执行完后退出
     }
     else
     {                                                                                                    
         // 父进程
         printf("我是父进程:pid: %d, ppid: %d\n", getpid(), getppid());
         // 子进程要循环5次死亡,而父进程在阻塞式等待
         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);
         }
     }
}

执行结果
在这里插入图片描述
循环完5次,子进程死亡
父进程立马回收,最后只剩下
父进程一个状态
在这里插入图片描述
如果想看到Z状态可以让父进程
等7秒针,而子进程是循环5次
每秒一次。这样就有两秒空挡
可以看到两次Z状态

// 父进程
printf("我是父进程:pid: %d, ppid: %d\n", getpid(), getppid());
sleep(7);
// 子进程要循环5次死亡,而父进程在阻塞式等待
pid_t ret = wait(NULL); 
if (ret > 0)
{
	printf("等待子进程成功,ret: %d\n", ret);
}

在这里插入图片描述

3.4 waitpid回收僵尸状态的子进程

在这里插入图片描述
waitpid的参数如图
在这里插入图片描述

代码测试

#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(0); // 子进程执行完后退出
     }
     else
     {                                                                                                    
         // 父进程
         printf("我是父进程:pid: %d, ppid: %d\n", getpid(), getppid());
         sleep(7);
         // 子进程要循环5次死亡,而父进程在阻塞式等待
         pid_t ret = waitpid(id, NULL, 0); 
         if (ret > 0)
         {
         	printf("等待子进程成功,ret: %d\n", ret);
         }
         while (1)
         {
             printf("我是父进程:pid: %d, ppid: %d\n", getpid(), getppid());
             sleep(1);
         }
     }
}

3.5 waitpid的参数status

status不关心子进程的退出状态信息
可以设为NULL
如果关心,操作系统会根据该参数
将子进程的退出信息反馈给父进程

status并不是按照整数来整体使用的
而是按照比特位的方式,将32个
比特位进行划分,我们只学低16位
在这里插入图片描述
次低8位表示子进程退出的退出码
最低7个比特位表示进程收到的信号
还有一个比特位代表core dump标志
这个学到后面在讲

(status >> 8) & 0xFF // 拿到次低8位退出码
status & 0x7F // 拿到最低7位收到的信号

代码测试

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(99); // 子进程执行完后退出
     }
     else
     {                                                                                                    
         // 父进程
         printf("我是父进程:pid: %d, ppid: %d\n", getpid(), getppid());
         // 子进程要循环5次死亡,而父进程在阻塞式等待
         int status = 0;
         pid_t ret = waitpid(id, &status, 0); 
         if (ret > 0)
         {
         	printf("等待子进程成功,ret: %d, 子进程收到的信号编号: %d, 子进程退出码: %d\n", ret, status & 0x7F, (status >> 8) & 0xFF);
         }
     }
}

父进程可以通过waitpid的参数status
接收子进程退出码判断子进程代码
是否正确执行并退出

执行结果
子进程退出码为我们设置的99
在这里插入图片描述
子进程收到的信号编号为0说明
我们的进程是正常跑完的
为其他数字说明进程异常结束
异常退出,退出码则没有意义

进程异常退出或崩溃本质是操作系统
杀掉了你的进程
而操作系统则是通过发送信号的方式杀掉进程的

kill -l 查看所有信号
我们应用程序员只要学1~31个普通信号

在这里插入图片描述
当子进程死循环,父进程
等不到子进程死亡则无法回收
我们可以使用9号信号
主动杀掉子进程

kill -9 [子进程PID]
杀掉子进程命令

在这里插入图片描述
此时父进程立马收到子进程退出
是因为收到9号信号
程序异常不光是内部代码问题
也可能是外力直接杀掉

子进程宏处理
WIFEXITED(status): 若为正常终止子进程返回
的状态,则为真用于查看进程是否是正常退出
WEXITSTATUS(status): 若WIFEXITED非零
提取子进程退出码

// 还是上面的代码
else
{                                                                                                    
	// 父进程
    printf("我是父进程:pid: %d, ppid: %d\n", getpid(), getppid());
    // 子进程要循环5次死亡,而父进程在阻塞式等待
    int status = 0;
    pid_t ret = waitpid(id, &status, 0); 
    if (ret > 0)
    {
    	// 可以不这么检测
        // printf("等待子进程成功,ret: %d, 子进程收到的信号编号: %d, 子进程退出码: %d\n", ret, status & 0x7F, (status >> 8) & 0xFF);
        if (WIFEXITED(status))
        {
        	// 子进程正常退出
        	printf("子进程执行完毕,子进程退出码:%d\n", WEXITSTATUS(status));
        }
        else
        {
        	printf("子进程异常退出:%d\n", WIFEXITED(status));
        }
    }
}

3.6 waitpid第三个参数option

前面说过option默认为0
表示阻塞等待,即父进程啥也不干
等待子进程死亡,然后回收子进程
option:WNOHANG选项
表示父进程非阻塞等待

WNOGANG是一个宏定义
全名wait no hang(夯住了)
也就是进度没有被cpu调度
hang表示悬挂也就是非阻塞

代码测试

#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(0); // 子进程执行完后退出
     }
     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");
                sleep(1);
            }
            else
            {
                // 等待失败
                printf("wait失败\n");
                quit = 1;
            }
        }
     }
}

执行结果
在这里插入图片描述
这里的父进程只是简单的打印
表示非阻塞状态,我们也可以
让父进程调用其他函数等等

✨✨✨✨✨✨✨✨
本篇博客完,感谢阅读🌹
如有错误之处可评论指出
博主会耐心听取每条意见
✨✨✨✨✨✨✨✨

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 18
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

新梦空间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值