🌈 个人主页:谁在夜里看海.****
🔥 个人专栏:《C++系列》** 《Linux系列》《算法系列》**
⛰️ 丢掉幻想,准备斗争
目录
引言
在上一篇关于Linux的章节中,我们介绍了进程的各种状态:R运行状态、S睡眠状态、D磁盘休眠状态、T停止状态、X死亡状态。其实还有一种特殊的运行状态,叫做Z僵死状态,为什么有僵死和死亡两种不同的状态呢?僵死并不等同于死亡:
*有人说,一个人肉体的死亡并不算真正的死亡,因为他还被人记着,直到世界上没有人记得他的时候,这个人才是完全意义上死亡了。*僵死状态就是这个道理,一个进程已经退出了,但是它的父进程并没有读取到子进程退出的代码,这时就会产生僵死进程(子进程死亡了但是父进程还记得它)
那么为什么父进程还会记得已经"死亡"的子进程呢?这就要从父子进程的工作原理讲起了:
一、僵尸进程
1.子进程的创建与退出
父进程使用fork()系统调用来创建一个新的子进程。fork()函数会返回两次:在父进程中返回子进程的PID,在子进程中返回0,父进程和子进程是各自独立执行代码的:
cpp
#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
pid_t pid = fork(); // 创建子进程
if (pid < 0) {
cerr << "子进程创建失败!" << endl;
return 1;
}
if (pid == 0) { // 子进程执行的代码
cout << "子进程正在运行,PID为:" << getpid() << endl;
return 0; // 子进程退出
} else { // 父进程执行的代码
cout << "父进程正在运行,PID为:" << getpid() << endl;
return 0; // 父进程退出
}
}
当执行到fork()时,会生成一个新进程。如果fork()成功,父进程和子进程都会继续执行。子进程的 PID 将返回给父进程,而在子进程中,fork()返回 0。
2.进程表
子进程在完成自身任务并退出时,操作系统并没有立即将其从进程表中移除。子进程变为"已终止"状态,但它会保留在进程表中,直到父进程收集它的退出状态。
进程表是操作系统用来管理所有进程的核心数据结构。每个进程都有一个进程控制块(PCB),其中包含了进程的各种信息,包括其父进程的 ID(PPID)和进程的状态。
++为什么需要存储运行的状态?++
父进程通过查看进程表,获取子进程的状态,根据不同的状态执行不同操作。
提问:父进程获取子进程状态是为了帮助子进程进行内存资源释放吗?
回答:并不是!子进程在执行结束之后,会自行释放内存资源,即使子进程错误中断了,没有自行释放资源,操作系统也会帮其释放资源。
那么父进程收集子进程状态信息的作用是什么呢?下面是几种常见的原因:
处理子进程执行失败时的错误恢复
父进程可以根据子进程的退出状态判断子进程是否正常完成任务。如果子进程由于某些错误退出,父进程可以选择执行错误恢复操作。例如:
假设父进程是一个文件处理程序,它会启动多个子进程来处理不同的文件。如果一个子进程处理某个文件时失败了(退出码非零),父进程就可以通过获取退出状态来判断这一点,然后采取以下行动:
①:重新启动该子进程来处理失败的文件。
②:记录错误日志,并通知用户某个文件处理失败。
③:跳过失败的文件,继续处理其他文件。
决定是否继续或终止任务
父进程需要知道子进程的执行结果,以决定是否继续执行其他任务或者终止整个操作。例如:
父进程启动多个子进程来执行并行任务,每个子进程执行不同的计算任务。父进程可能需要决定是否继续执行其他任务,或者如果某个子进程失败,是否停止其他所有任务:
如果某个子进程退出码表明它执行失败,父进程可以选择终止所有其他子进程,避免浪费计算资源。
如果所有子进程都成功,父进程可以汇总结果并继续进行后续步骤。
资源清理和收尾操作
有些程序需要父进程在子进程结束后做一些额外的资源清理工作。例如,父进程可能会在子进程执行任务时分配某些资源(内存、文件句柄等),需要在子进程退出后根据退出状态来执行适当的清理工作。例如:
假设父进程管理一个服务器,启动子进程来处理客户端请求。子进程在处理请求时可能会打开临时文件或者分配内存,父进程需要知道子进程是否正常退出,以便执行以下操作:
如果子进程正常退出,父进程可以清理相关资源,比如关闭临时文件、释放内存。
如果子进程异常退出,父进程可能需要保留错误日志或者重新处理请求。
上述这些情况都在说明父子进程的协作关系 :父进程和子进程并非完全独立,而是有相互依赖的关系,父进程需要回收子进程的资源,操作系统会协调这一过程。
3.僵尸状态产生
父进程具体是如何查看到子进程的运行条目等信息的呢?
父进程通过调用 wait()
或 waitpid()
来回收子进程的退出状态。当子进程退出并且父进程调用 wait()
时,父进程执行后续操作,然后操作系统会清除子进程在进程表中的条目。
*那么问题又来了:*进程中断后,操作系统会为其释放内存资源(理解为擦屁股),那么父进程没有调用wait()
时,操作系统还会清除条目等信息吗?答案是不会:
操作系统保留进程表条目,是为了确保父进程能够获取子进程的退出状态,执行相应的操作。操作系统不自动删除这些条目,因为父进程需要在适当的时候通过系统调用(如 wait()
)来获取子进程的退出信息。所以,如果父进程没有调用wait()获取信息,操作系统就不会释放进程条目这些资源,这就会导致内存泄漏,这是操作系统故意这么设计的,目的就是为了保证父进程可以收集到子进程的条目信息!
子进程执行完毕之后,父进程没有通过wait()收集进程条目信息,就会导致条目信息不会被释放,造成内存泄漏,此时这个存在内存泄漏的进程也叫做僵尸进程。
4.直观感受一下:
下面这段代码就是演示僵尸进程的产生过程:
cpp
1 #include <iostream>
2 #include <unistd.h>
3 #include <sys/wait.h>
4 #include <ctime>
5 using namespace std;
6
7 int main()
8 {
9 pid_t pid = fork();
10 if(pid<0)
11 cout<<"子进程创建失败!"<<endl;
12 else if(pid == 0)
13 {
14 cout<<"子进程正在运行,pid为:"<<getpid()<<e ndl;
15 sleep(5);
16 cout<<"子进程结束!"<<endl;
17 }
18 else
19 {
20 cout<<"父进程正在运行,pid为:"<<getpid()<<e ndl;
21 sleep(30); // 父进程没有结束就不会调用wait()
22 cout<<"父进程调用wait,回收子进程..."<<endl;
23 wait(NULL);
24 cout<<"完成回收!"<<endl;
25 }
26 return 0;
27 }
可以看到,此时子进程执行结束,父进程还未调用wait(),僵尸进程(Z状态)就产生了
父进程调用玩wait()对子进程条目信息收集完毕之后,操作系统为其释放资源,进程解除僵尸状态
二、孤儿进程
1.产生原因
上面提到僵尸进程产生是由于父进程没有调用wait()导致的,处理办法就是让父进程必须调用wait(),但是有这么一种特殊情况:
子进程还在执行,可是父进程提前执行完毕了,之后子进程执行完毕时,已经没有父进程来为它调用wait()了,进程条目资源就不会被系统释放,导致了内存泄漏 。父进程提前结束的进程称为孤儿进程。
面对孤儿进程这种情况(内存泄漏),操作系统采用了一种巧妙的处理办法:
2.处理办法
init进程
操作系统对孤儿进程的处理办法就是将孤儿进程托管给init进程,让init进程调用wait(),实现进场条目资源的释放,避免内存泄漏,那么init进程是何方神圣,作用这么大呢:
init
是 Unix 和类 Unix 操作系统中的第一个进程,它在操作系统启动时由内核启动,并且是所有其他进程的祖先。每个系统中至少有一个 init
进程,它是系统中最为重要的进程之一,负责系统初始化和其他进程的管理。
init
进程的生命周期非常长,可以说是操作系统中唯一一个始终存在的进程 ,直到系统关机或重启。因此将孤儿进程托管给它,一定可以保证资源的正常释放
3.直观感受一下:
下面这段代码可以清除展示孤儿进程的产生与init进程的托管过程:
cpp
1 #include<iostream>
2 #include<unistd.h>
3 #include<sys/wait.h>
4 #include<ctime>
5 #include<cstdlib>
6 using namespace std;
7
8 int main()
9 {
10 pid_t pid = fork();
11 if(pid<0)
12 cout<<"子进程创建失败!"<<endl;
13 else if(pid == 0)
14 {
15 cout<<"子进程正在运行,pid为:"<<getpid()<<endl;
16 cout<<"父进程ppid为:"<<getppid()<<endl;
17 sleep(10);
18 cout<<"子进程正在运行,pid为:"<<getpid()<<endl;
19 cout<<"父进程ppid为:"<<getppid()<<endl;
20 sleep(5);
21 cout<<"子进程结束!"<<endl;
22 return 0;
23 }
24 else
25 {
26 sleep(5);
27 cout<<"父进程结束!pid为:"<<getpid()<<endl;
28 exit(0);
29 }
30 return 0;
31 }
init进程是操作系统的第一个进程,所以它的PID为1,我们可以观察到,子进程的父进程结束后,其的确被托管给init进程了(父进程变成了init进程)
总结
本篇篇博客详细讲解了 僵尸进程 和 孤儿进程 的产生过程与处理办法。僵尸进程产生于子进程退出后,父进程未调用 wait()
收集其退出状态,导致进程表中的信息未被清理,从而造成资源泄漏。孤儿进程则是父进程提前结束,子进程在没有父进程的情况下继续执行,操作系统将其交给 init
进程处理,以确保资源的正常回收。
以上就是【详解僵尸进程与孤儿进程】的全部内容,欢迎指正~
码文不易,还请多多关注支持,这是我持续创作的最大动力!