Linux - 僵尸进程、孤儿进程、守护进程

1. 僵尸进程

在一个启动的进程中创建子进程,这时候就有了父子两个进程,父进程正常运行,子进程先与父进程结束,子进程无法释放自己的 PCB 资源,需要父进程来做这个件事儿,但是如果父进程也不管,这时候子进程就变成了僵尸进程。

僵尸进程不能将它看成是一个正常的进程,这个进程已经死亡了,用户区资源已经被释放了,只是还占用着一些内核资源(PCB)。 僵尸进程就相当于是一副已经腐烂只剩下骨头的尸体。

僵尸进程的出现是由于这个已死亡的进程的父进程不作为造成的。

运行下面的代码就可以得到一个僵尸进程了:

 
int main()
{
    pid_t pid;
    // 创建子进程
    for(int i=0; i<5; ++i)
    {
        pid = fork();
        if(pid == 0)
        {
            break;
        }
    }

    // 父进程
    if(pid > 0)
    {
        // 需要保证父进程一直在运行
        // 一直运行不退出, 并且也做回收, 就会出现僵尸进程
        while(1)
        {
            printf("我是父进程, pid=%d\n", getpid());
            sleep(1);
        }
    }
    else if(pid == 0)
    {
        // 子进程, 执行这句代码之后, 子进程退出了
        printf("我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());
    }
    return 0;
}
 
# ps aux 查看进程信息
# Z+ --> 这个进程是僵尸进程, defunct, 表示进程已经死亡
robin     22598  0.0  0.0   4352   624 pts/2    S+   10:11   0:00 ./app
robin     22599  0.0  0.0      0     0 pts/2    Z+   10:11   0:00 [app] <defunct> # 子进程
robin     22600  0.0  0.0      0     0 pts/2    Z+   10:11   0:00 [app] <defunct> # 子进程
robin     22601  0.0  0.0      0     0 pts/2    Z+   10:11   0:00 [app] <defunct> # 子进程
robin     22602  0.0  0.0      0     0 pts/2    Z+   10:11   0:00 [app] <defunct> # 子进程
robin     22603  0.0  0.0      0     0 pts/2    Z+   10:11   0:00 [app] <defunct> # 子进程


消灭僵尸进程的方法是,杀死这个僵尸进程的父进程,这样僵尸进程的资源就被系统回收了。

通过 kill -9 僵尸进程PID 的方式是不能消灭僵尸进程的,这个命令只对活着的进程有效,僵尸进程已经死了,鞭尸是不能解决问题的。

 

 

2. 孤儿进程

在一个启动的进程中创建子进程,这时候父子进程同时运行,但是父进程由于某种原因先退出了,子进程还在运行,这时候这个子进程就可以被称之为孤儿进程(跟现实是一样的)。

操作系统是非常关爱运行的每一个进程的,当检测到某一个进程变成了孤儿进程,这时候系统中就会有一个固定的进程领养这个孤儿进程(有干爹了)。如果使用 Linux 没有桌面终端,这个领养孤儿进程的进程就是 init 进程(PID=1),如果有桌面终端,这个领养孤儿进程就是桌面进程。

那么问题来了,系统为什么要领养这个孤儿进程呢?在子进程退出的时候, 进程中的用户区可以自己释放, 但是进程内核区的pcb资源自己无法释放,必须要由父进程来释放子进程的pcb资源,孤儿进程被领养之后,这件事儿干爹就可以代劳了,这样可以避免系统资源的浪费。

下面这段代码就可以得到一个孤儿进程:

 
int main()
{
    // 创建子进程
    pid_t pid = fork();

    // 父进程
    if(pid > 0)
    {
        printf("我是父进程, pid=%d\n", getpid());
    }
    else if(pid == 0)
    {
        sleep(1);	// 强迫子进程睡眠1s, 这个期间, 父进程退出, 当前进程变成了孤儿进程
        // 子进程
        printf("我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());
    }
    return 0;
}
 
# 程序输出的结果
$ ./a.out 
我是父进程, pid=22459
我是子进程, pid=22460, 父进程ID: 1        # 父进程向退出, 子进程变成孤儿进程, 子进程被1号进程回收

 

 

3. 守护进程

守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。
 

3.1 进程组

多个进程的集合就是进程组,这个组中必须有一个组长,组长就是进程组中的第一个进程,组长以外的都是普通的成员,每个进程组都有一个唯一的组 ID,进程组的 ID 和组长的 PID 是一样的。
 
进程组中的成员是可以转移的,如果当前进程组中的成员被转移到了其他的组,或者进制中的所有进程都退出了,那么这个进程组也就不存在了。如果进程组中组长死了,但是当前进程组中有其他进程,这个进程组还是继续存在的。下面介绍几个常用的进程组函数:
 
得到当前进程所在的进程组的组 ID
pid_t getpgrp(void);
 
获取指定的进程所在的进程组的组 ID,参数 pid 就是指定的进程
pid_t getpgid(pid_t pid);
 
将某个进程移动到其他进程组中或者创建新的进程组
int setpgid(pid_t pid, pid_t pgid);
 
 
参数
  • pid: 某个进程的进程 ID
  • pgid: 某个进程组的组 ID
    • 如果 pgid 对应的进程组存在,pid 对应的进程会移动到这个组中,pid != pgid
    • 如果 pgid 对应的进程组不存在,会创建一个新的进程组,因此要求 pid == pgid, 当前进程就是组长了
返回值:
  • 函数调用成功返回 0,失败返回 - 1
 

3.2 会话

会话 (session) 是由一个或多个进程组组成的,一个会话可以对应一个控制终端,也可以没有。一个普通的进程可以调用 setsid() 函数使自己成为新 session 的领头进程(会长),并且这个 session 领头进程还会被放入到一个新的进程组中。先来看一下 setsid() 函数的原型:
#include <unistd.h>


// 获取某个进程所属的会话ID
pid_t getsid(pid_t pid);


// 将某个进程变成会话 =>> 得到一个守护进程
// 使用哪个进程调用这个函数, 这个进程就会变成一个会话
pid_t setsid(void);
 
 
使用这个函数的注意事项:
 
调用这个函数的进程不能是组长进程,如果是该函数调用失败,如果保证这个函数能调用成功呢?
先 fork () 创建子进程,终止父进程,让子进程调用这个函数
如果调用这个函数的进程不是进程组长,会话创建成功
这个进程会变成当前会话中的第一个进程,同时也会变成新的进程组的组长
该函数调用成功之后,当前进程就脱离了控制终端,因此不会阻塞终端
 

3.3 创建守护进程

如果要创建一个守护进程,标准步骤如下,部分操作可以根据实际需求进行取舍:
 
1. 创建子进程,让父进程退出
  • 因为父进程有可能是组长进程,不符合条件,也没有什么利用价值,退出即可
  • 子进程没有任何职务,目的是让子进程最终变成一个会话,最终就会得到守护进程
2. 通过子进程创建新的会话,调用函数 setsid (),脱离控制终端,变成守护进程
 
3. 改变当前进程的工作目录 (可选项,不是必须要做的)
  • 某些文件系统可以被卸载,比如: U 盘,移动硬盘,进程如果在这些目录中运行,运行期间这些设备被卸载了,运行的进程也就不能正常工作了。
  • 修改当前进程的工作目录需要调用函数 chdir()
int chdir(const char *path);
4. 重新设置文件的掩码 (可选项,不是必须要做的)
  • 掩码: umask, 在创建新文件的时候需要和这个掩码进行运算,去掉文件的某些权限
  • 设置掩码需要使用函数 umask()
mode_t umask(mode_t mask);
5. 关闭 / 重定向文件描述符 (不做也可以,但是建议做一下)
  • 启动一个进程,文件描述符表中默认有三个被打开了,对应的都是当前的终端文件
  • 因为进程通过调用 setsid () 已经脱离了当前终端,因此关联的文件描述符也就没用了,可以关闭
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
  • 重定向文件描述符 (和关闭二选一): 改变文件描述符关联的默认文件,让他们指向一个特殊的文件 /dev/null,只要把数据扔到这个特殊的设备文件中,数据被被销毁了
int fd = open("/dev/null", O_RDWR);
// 重定向之后, 这三个文件描述符就和当前终端没有任何关系了
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
6. 根据实际需求在守护进程中执行某些特定的操作
 
 
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

int main()
{
	//1. 重设文件权限      umask
	umask(0);
    //2. 创建子进程        fork
    int ret = fork();
    if(ret<0) printf("创建进程失败:%m\n"),exit(-1);
    //3. 结束父进程    
    if(ret>0) printf("父进程结束 守护进程id:%d\n",ret),exit(0);
    if(ret==0)
	{
		//4. 创建新会话        setsid
		setsid();
    	//5. 忽略 SIGCHLD SIGHUP信号  
    	signal(SIGCHLD,SIG_IGN);
    	signal(SIGHUP,SIG_IGN);
    	//6. 改变当前工作目录   chdir
    	chdir("/");
    	//7. 重定向文件描述符号  dup2
    	int fd = open("/dev/NULL",O_RDWR);
    	dup2(fd,0);
    	dup2(fd,1);

    	while(1)
		{
    		sleep(1);//模拟守护进程工作
    	}
    }
    
	return 0;
}
运行结果:
 

3.4 守护进程的应用

写一个守护进程,每隔 2s 获取一次系统时间,并将得到的时间写入到磁盘文件中。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <time.h>

// 信号的处理动作
void writeFile(int num)
{
    // 得到系统时间
    time_t seconds = time(NULL);
    // 时间转换, 总秒数 -> 可以识别的时间字符串
    struct tm* loc = localtime(&seconds);
    // sprintf();
    char* curtime = asctime(loc); // 自带换行
    // 打开一个文件, 如果文件不存在, 就创建, 文件需要有追加属性
    // ./对应的是哪个目录? /home/robin
    // 0664 & ~022
    int fd = open("./time+++++++.log", O_WRONLY|O_CREAT|O_APPEND, 0664);
    write(fd, curtime, strlen(curtime));
    close(fd);
}

int main()
{
    // 1. 创建子进程, 杀死父进程
    pid_t pid = fork();
    if(pid > 0)
    {
        // 父进程
        exit(0); // kill(getpid(), 9); raise(9); abort();
    }

    // 2. 子进程, 将其变成会话, 脱离当前终端
    setsid();

    // 3. 修改进程的工作目录, 修改到一个不能被修改和删除的目录中 /home/robin
    chdir("/home/robin");

    // 4. 设置掩码, 在进程中创建文件的时候这个掩码就起作用了
    umask(022);

    // 5. 重定向和终端关联的文件描述符 -> /dev/null
    int fd = open("/dev/null", O_RDWR);
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);

    // 5. 委托内核捕捉并处理将来发生的信号-SIGALRM(14)
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = writeFile;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    // 6. 设置定时器
    struct itimerval val;
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;
    setitimer(ITIMER_REAL, &val, NULL);

    while(1)
    {
        sleep(100);
    }

    return 0;
}
 
 
 
posted @ 2023-01-08 10:32  [BORUTO]  阅读(42)  评论(0编辑  收藏  举报