Linux进程4:孤儿进程,僵尸进程(及解决方法),守护进程讲解

孤儿进程,僵尸进程(及解决方法),守护进程讲解

孤儿进程:

父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”此时的子进程叫做孤儿进程。====爹没了
Linux避免系统存在过多的孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程。====init养父

僵尸进程:

创建子进程后,子进程退出状态不被收集,变成僵尸进程。爹不要它了
除非
爹死后
变孤儿init养父接收。如果父进程是死循环,那么该僵尸进程就变成游魂野鬼消耗空间。

守护进程:

守护进程(Daemon)是在一类脱离终端在后台执行的程序, 通常以 d 结尾, 随系统启动, 其父进程 (ppid) 通常是init 进程。====后台小天使

1.孤儿进程:

案例演示:

实例:以下是一个孤儿进程的示例程序,在此程序中,让父进程先退出,然后子进程再次打印自己的父进程号:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<unistd.h>
int main()
{
    
    
pid_t pid;
pid = fork();//创建一个进程
if (pid < 0)
{
    
    
perror("fork error:");//创建失败
exit(1);
}
//子进程
if (pid == 0)
{
    
    
printf("I am the childprocess.\n");
//输出进程ID和父进程ID
printf("pid:%d\tppid:%d\n",getpid(),getppid());
printf("I will sleep fiveseconds.\n");
//睡眠5s,保证父进程先退出
sleep(5);
printf("pid:%d\tppid:%d\n",getpid(),getppid());
printf("child process isexited.\n");
}
//父进程
else
{
    
    
printf("I am fatherprocess.\n");
//父进程睡眠1s,保证子进程输出进程id
sleep(1);
printf("father process is  exited.\n");
}
return 0;
}

注意
getpid函数可以获得当前进程的pid,getppid函数可以获得当前进程的父进程号。

运行结果:

在这里插入图片描述
说明
首先打印子进程和父进程的ID,后来父进程提前终结,子进程成为孤儿进程,打印子进程和init父进程ID。

2.僵尸进程:

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

注意
僵尸进程还会消耗一定的系统资源,并且还保留一些概要信息供父进程查询子进程的状态可以提供父进程想要的信息。一旦父进程得到想要的信息,僵尸进程就会结束。

僵尸进程怎样产生的:

一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用 exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。

在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装 SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了, 那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是 为什么系统中有时会有很多的僵尸进程。

怎么查看僵尸进程:

利用命令:ps,可以看到有标记为Z的进程就是僵尸进程。

怎样来清除僵尸进程:

方法一:

改写父进程,在子进程死后要为它收尸。
具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用 wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。

方法二:

把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失。
注:僵尸进程将会导致资源浪费,而孤儿则不会

案例演示1:

实例1:以下是一个僵尸进程的示例程序,在此程序中,子进程先退出,父进程不调用wait()或waitpid()清理子进程信息。

#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<stdlib.h>
int main()
{
    
    
pid_t pid;
pid = fork();
if (pid < 0)
{
    
    
perror("fork error:");
exit(1);
}
else if (pid == 0)
{
    
    
printf("I am child process.I amexiting.\n");
exit(0);
}
printf("I am father process.I willsleep two seconds\n");
//等待子进程先退出
sleep(2);
//输出进程信息
system("ps -opid,ppid,state,tty,command");
printf("father process isexiting.\n");
return 0;
}

运行结果:
在这里插入图片描述
说明
子进程变成了僵尸进程

案例演示2:

实例2:父进程循环创建子进程,子进程退出,造成多个僵尸进程。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
int main()
{
    
    
pid_t pid;
//循环创建子进程
while(1)
{
    
    
pid = fork();
if (pid < 0)
{
    
    
perror("fork error:");
exit(1);
}
else if (pid == 0)
{
    
    
printf("I am a childprocess.\nI am exiting.\n");
//子进程退出,成为僵尸进程
exit(0);
}
else
{
    
    
//父进程休眠20s继续创建子进程
sleep(4);
continue;
}
}
return 0;
}

运行结果:

在这里插入图片描述
3.僵尸进程解决办法:

通过信号机制:

子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。测试程序如下所示:

#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<stdlib.h>
#include<signal.h>
static voidsig_child(int signo);
int main()
{
    
    
pid_t pid;
//创建捕捉子进程退出信号
signal(SIGCHLD,sig_child);
pid = fork();
if (pid < 0)
{
    
    
perror("fork error:");
exit(1);
}
else if (pid == 0)
{
    
    
printf("I am child process,pid id%d.I am exiting.\n",getpid());
exit(0);
}
printf("I am father process.I willsleep two seconds\n");
//等待子进程先退出
sleep(2);
//输出进程信息
system("ps -opid,ppid,state,tty,command");
printf("father process isexiting.\n");
return 0;
}
static voidsig_child(int signo)
{
    
    
pid_t pid;
int stat;
//处理僵尸进程
while ((pid = waitpid(-1, &stat, WNOHANG))>0)
printf("child %dterminated.\n", pid);
}

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

两次fork():

《Unix 环境高级编程》8.6节说的非常详细。原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。测试程序如下所示:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    
    
pid_t pid;
//创建第一个子进程
pid = fork();
if (pid < 0)
{
    
    
perror("fork error:");
exit(1);
}
//第一个子进程
else if (pid == 0)
{
    
    
//子进程再创建子进程
printf("I am the first childprocess.pid:%d\tppid:%d\n",getpid(),getppid());
pid = fork();
if (pid < 0)
{
    
    
perror("fork error:");
exit(1);
}
//第一个子进程退出
else if (pid >0)
{
    
    
printf("first procee isexited.\n");
exit(0);
}
//第二个子进程
//睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程里
sleep(3);
printf("I am the second childprocess.pid: %d\tppid:%d\n",getpid(),getppid());
exit(0);
}
//父进程处理第一个子进程退出
if (waitpid(pid, NULL, 0) != pid)
{
    
    
perror("waitepid error:");
exit(1);
}
exit(0);
return 0;
}

运行结果:

在这里插入图片描述
说明
父进程变成了init进程。

4.守护进程:

同样我们需要了解一下什么是守护进程,守护进程就是在后台运行,不与任何终端关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(apache和postfix)运行,并能处理一些系统级的任务。习惯上守护进程的名字通常以d结尾(sshd),但这些不是必须的。

守护进程是脱离于终端并且在后台运行的进程。守护进程脱离于终端,是为了避免进程在执行过程中的信息在任何终端上显示,并且进程也不会被任何终端所产生的终端信息所打断。

守护进程,也就是通常说的Daemon进程,是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统引导装入时启动,在系统关闭时终止Linux系统有很多守护进程,大多数服务都是通过守护进程实现的,同时,守护进程还能完成许多系统任务,例如,作业规划进程crond、打印进程lqd等(这里的结尾字母d就是Daemon的意思)。

由于在Linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才退出。如果想让某个进程不因为用户或终端或其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。

下面介绍一下创建守护进程的步骤:

· 调用fork(),创建新进程,它会是将来的守护进程.
· 在父进程中调用exit,保证子进程不是进程组长
· 调用setsid()创建新的会话区
· 将当前目录改成跟目录(如果把当前目录作为守护进程的目录,当前目录不能被卸载他作为守护进程的工作目录)
· 将标准输入,标注输出,标准错误重定向到/dev/null

代码演示:

#include<sys/types.h>
#incldue<sys/stat.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<signal.h>
#include<errno.h>
#include<signal.h>
#include<fcntl.h>
#incldue<unistd.h>
#include<linux/fs.h>
int main(void)
{
    
    
pid_t pid;
int i;
pid = fork();    //创建一个新进程,将来会是守护进程
if(pid == -1)
{
    
    
return -1;
}
else if(pid != 0){
    
     //父进程调用exit,保证子进程不是进程组长
exit(EXIT_SUCCESS);
}  
if(setsid() == -1) //创建新的会话区
{
    
    
return -1;        
}   
if(chdir("/") == -1)  //将当前目录改成根目录
{
    
    
return -1;
}  
for(i = 0;i < NR_OPEN;i++)
{
    
    
close(i);
}
open("/dev/null",O_RDWR); 重定向
dup(0);
dup(0);   
return 0;
}

运行结果:

disda    26217     1 0 
06:59 ?        00:00:00 ./dm01_demon 则出现了守护进程!

5.常见问答:

孤儿进程有危害吗?

孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

僵尸进程有危害吗?

僵尸进程危害场景:“擒贼先擒王”。

例如有个进程,它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。 严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程(父进程)之后,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。这就是守护进程的作用,如果发生大量的僵尸进程,守护进程就会查找其父进程,然后无情的kill掉!

什么要尽量避免僵尸进程?

首先要明白,僵尸进程不是活着的进程,可以说就是一个数据结构,它是已经完成的任务的进程,但是不是它完成任务后就会烟消云散的,他会留下一点东西,这个东西就是他的进程Id,他的结束状态等,为什么了留下这个东西呢?因为这个是用来向他的父进程报告自己的完成状况用的,想想父进程为什么会创建一个进程,是用来完成任务的,父进程需要知道子进程的完成情况,所有出现这样的机制,对于僵尸进程只有父进程自己可以清理掉,调用wait等命令。就可以了。但是父进程不清理咋办,那么就说明僵尸进程存在,浪费了进程Id,进程的id是一种有限资源,用一个少一个啊,所以如果大量的僵尸进程存在的话,解决方法可以是杀掉无良的爹,孩子就可以被收养了。所以说,系统中的进程数量是有限的,虽然僵尸进程占用的资源和内存都比较少,但是它却占领着数字,可能会导致系统无法再创建新的进程,因此及时清除僵尸进程很重要!

补充

Linux中的常见命令:
用于文件操作的常见命令:
cp(拷贝)、rm(删除)、mkdir(创建)、cd(切换目录)、mv(改名)、ls(罗列文件/文件夹)、tar(解压缩)、chmod(更改权限)、chown(更改所有者)
用于系统进程操作的常见命令:
top/htop(查看系统中所有进程实时运行情况)、ps(列出系统中的进程)、lsof(查看某个端口是否被占用)、kill(杀死某个进程)、iotop(监控磁盘I/O情况)、ifconfig(查看本机IP)

猜你喜欢

转载自blog.csdn.net/weixin_40734514/article/details/108990454