僵尸进程孤儿进程以及守护进程

一.僵尸进程

  • 描述

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

  • 如何产生
    一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用 exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。
  • 危害
    在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置(PCB),记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装 SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了, 那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是 为什么系统中有时会有很多的僵尸进程。
	R:运行
	S:后台运行
	Z:僵尸进程
  • 强行凑一个僵尸进程
#include <iostream>
using namespace std;
#include <unistd.h>
#include <sys/types.h>
int main()
{
	pid_t pid;
	pid = fork();

	if(pid == 0)
	{
		cout<<"child"<<endl;
		exit(0);
	}
	else if( pid > 0)
	{
		for(;;)
		{
			sleep(1);
			cout<<"parent"<<endl;
		}
	}
	return 0;
}

子进程执行完后退出,父进程死循环不退出.

g++ -o zom zombie.c
./zom

使用ps命令查看:ps -ef   找到defunct
UID        PID  PPID  C STIME TTY          TIME CMD
//........
txp      14301 11835  0 10:54 pts/2    00:00:00 ./zombie
txp      14302 14301  0 10:54 pts/2    00:00:00 [zombie] <defunct>
  • 避免/解决僵尸进程
  1. 改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用 wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略,如果想响应这个消息,可以设置一个处理函数;
  2. 把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失;
  3. 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCLD, SIG_IGN)或signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号;
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
 
 //当只有一个子进程的时候
void deal_child(int sig_no)
{
    wait(NULL);
}
//存在多个子进程时
void deal_child(int sig_no)
{
    for (;;) {
        if (waitpid(-1, NULL, WNOHANG) == 0)
            break;
    }  
}
 
int main(int argc, char **argv)
{
    signal(SIGCHLD, deal_child);
    //省略
    return 0;
}
父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起。
执行wait()或waitpid()系统调用,则子进程在终止后会立即把它在进程表中的数据返回给父进程,
此时系统会立即删除该进入点。在这种情形下就不会产生defunct进程。
实际上父进程执行wait()与多线程中主线程执行pthread_join()是类似的,也会将主线程阻塞挂起。

学校学习时整理的关于wait()笔记:

方案一:

pid_t  wait (int * status)  的三个作用:

1.父进程阻塞,然后等待子进程退出(注意这里是阻塞,如果子进程没死,父进程阻塞)
2.回收子进程残留资源 (残留的资源是子进程的PCB)
3.获取子进程结束原因(正常的退出、异常终止)
失败返回-1。

wait() 调用一次,只回收一个子进程,对于多个子进程,需要调用多次wait();

pid_t wpid,pid;
if(pid > 0)
{
	wpid = wait( NULL );
}

循环的等待多个子进程的结束:
if ( pid == 0)
{
	while(wait(NULL));
}
进一步获取子进程结束原因:利用下面的宏函数
1.
WIFEXITED(status) 为非0,表示进程正常的结束
WEXITSTATUS(status)如果上面的宏为真
用这个宏获取进程的退出码(实际上,正常退出的退出码是exit(n)中的n)

2.
SIFSIGNALED(status)为非0,表示进程异常终止
WTERMSIG(status)如上面的宏为真,使用该宏函数,取得使进程终止的那个信号的编码

linux中的进程异常退出都是由于 信号  产生的,进程收到了一个信号,所以异常终止回收信号编号

pid_t wpid,pid;
int status;

if(pid == 0)
{
	return 100;
}
if( pid > 0 )
{
	wpid = wait(&status);
}
if( WIFEXITED(status) )
{
	pfint("退出码是%d",WEXITSTATUS(status));
}
//子进程trturn 100,所以这里打印出来也是100
//异常退出类似,只不过打印出来的退出码是收到异常信号的编号



方案二:
pid_t  waitpid(pid_t pid , int *status , int options);  可以不阻塞父进程
成功,返回清理掉的所有子进程的ID;失败返回-1(无子进程)

参数pid:
>0 回收直到ID的子进程
-1 回收任意一个子进程(此时相当于wait)
0 回收和当前调用waitpid一个组的所有子进程
<-1 回收指定进程组内的任意子进程

参数:status
与wait()的作用的和用法一样,结合宏函数实现

参数: options 
以轮询的方式,查看子进程是否死了,指定为宏 WNOHANG,就为非阻塞的

二.孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。

PID = 1 的进程,所有进程的起源/sbin/init

孤儿进程对于系统的危害微乎其微,一般情况下,大可不必担心。

三.守护进程

不与任何终端关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(apache和postfix)运行,并能处理一些系统级的任务。守护进程脱离于终端,是为了避免进程在执行过程中的信息在任何终端上显示,并且进程也不会被任何终端所产生的终端信息所打断(比如关闭终端等)

  • 不与终端关联:比如我们经常使用的bash就是一个终端,不会输出任何信息;
  • 随系统一起启动:守护进程可以理解为一个服务,比如windows上的服务,这些服务都是随系统启动时一起启动的;
  • 它只会在后台默默的工作,为系统或者用户提供各种各样的服务,不受终端等的控制;
  • 守护进程一般会一直运行不会被中断,也就是说是一个死循环。

如何创建一个守护进程

  1. 创建子进程后,将父进程退出;
简单来说,给子进程成为进程组组长和会话组组长做铺垫,因为它要独立出去
成为独立的一个组,自然要脱离父进程和终端
  1. 在子进程中创建会话;
子进程会继承父进程的一些信息,就比如会话信息,所以,子进程要创建属于自己的会话
setsid()调用成功后,进程成为新的会话组长和新的进程组长,
并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
  1. 屏蔽掉SIGCHLD信号;
防止僵尸进程的出现
  1. 改变子进程的工作目录;
子进程继承了父进程的工作目录,既然要独立出去,那么肯定得有自己的工作目录
  1. 重定向文件描述符;
同样的子进程会继承父进程的文件描述符
守护进程是非交互式程序,没有控制终端,所以任何输出,
无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
一般我们将这些文件描述符重定向到/dev/null/ 者相当于是一个黑洞或者是一个垃圾桶
  1. 重建文件创建掩码umask
创建文件是,文件创建掩码会被屏蔽一些权限相对应的位,所以需要重建文件创建掩码

示例:

#include <fcntl.h>
#include <unistd.h>
#include "daemon.h"

int daemon(int nochdir, int noclose)
{
    int fd;

    switch (fork()) {
    case -1:
        return (-1);
    case 0:
        break;
    default:
        _exit(0);
    }

    if (setsid() == -1)
        return (-1);

    if (!nochdir)
        (void)chdir("/");

    if (!noclose && (fd = open("/dev/null", O_RDWR, 0)) != -1) {
        (void)dup2(fd, STDIN_FILENO);
        (void)dup2(fd, STDOUT_FILENO);
        (void)dup2(fd, STDERR_FILENO);
        if (fd > 2)
            (void)close (fd);
    }
    return (0);
}     

对于守护进程中出现的几个名词,做一个解释:

  1. 进程组长:进程组中的第一个进程;
  2. 会话:可以认为是建立的通讯,比如我们在linux种打开一个终端,可以认为我们和内核建立了会话,也可以认为由一堆的进程组组成;
  3. 会话组长:是一个进程,多个进程组中的第一个进程组的组长。
发布了237 篇原创文章 · 获赞 98 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/KingOfMyHeart/article/details/98181471