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

【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第1张图片

前言

什么是写时拷贝

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

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

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

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

写时拷贝实现图
【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第2张图片
为什么要使用写时拷贝

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

一、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是直接终止
    并不会清理函数和写入缓冲区数据
    如图
    【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第3张图片

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

三、进程等待

3.1 进程等待必要性

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

3.2 制造一个僵尸进程

代码测试

#include 
#include 
#include 
#include 

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);
         }
     }
}

执行结果
【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第4张图片

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

循环完5次,子进程退出
而父进程还在执行
此时子进程变成Z僵尸状态
defunct表示他是无效的
【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第5张图片

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

【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第6张图片

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

代码测试

#include 
#include 
#include 
#include 
#include 
#include 

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);
         }
     }
}

执行结果
【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第7张图片
循环完5次,子进程死亡
父进程立马回收,最后只剩下
父进程一个状态
【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第8张图片
如果想看到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);
}

【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第9张图片

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

【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第10张图片
waitpid的参数如图
【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第11张图片

代码测试

#include 
#include 
#include 
#include 
#include 
#include 

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位
【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第12张图片
次低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
【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第13张图片
子进程收到的信号编号为0说明
我们的进程是正常跑完的
为其他数字说明进程异常结束
异常退出,退出码则没有意义

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

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

【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第14张图片
当子进程死循环,父进程
等不到子进程死亡则无法回收
我们可以使用9号信号
主动杀掉子进程

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

【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第15张图片
此时父进程立马收到子进程退出
是因为收到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 
#include 
#include 
#include 
#include 
#include 

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;
            }
        }
     }
}

执行结果
【Linux进程控制】进程终止和等待(waitpid回收僵尸进程)_第16张图片
这里的父进程只是简单的打印
表示非阻塞状态,我们也可以
让父进程调用其他函数等等

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

你可能感兴趣的:(Linux,linux,运维,服务器)