文章目录

  • 普通进程和守护进程的区别
  • 守护进程的编写
  • setsid函数的作用



普通进程和守护进程的区别

普通进程:

  • 进程有对应的终端,如果终端退出,那么对应的进程也就消失了:它的父进程是一个bash。
  • 普通进程在前台时,终端被占住了,输入各种命令这个终端都没有反应。
  • 当终端退出时,会向同一个会话里的所有进程发送SIGHUP信号,如果普通进程忽略这个信号,就不会退出,而是在终端退出后挂载到1号进程上。

守护进程:

  • 守护进程的生存周期长,一般是操作系统启动时它就启动,操作系统关闭它才关闭。
  • 大多数守护进程都是以root身份运行。
  • 守护进程跟终端无关联,也就是说它们没有控制终端,因此终端退出不会导致守护进程退出。内核守护进程以无控制终端的方式启动,普通守护进程可能是守护进程调用了setsid的结果。
  • 守护进程在后台运行,不会占用终端。

使用ps命令查看进程:

进程守护 supervisor 监控python吗 进程守护怎么用_linux

根据内容,我们可以把这些守护进程分为以下几种:

  1. PPID=0:内核进程,跟随系统启动而启动,其生命周期贯穿整个操作系统。
  2. CMD列名字中带有[ ],是内核守护进程。
  3. 1号进程:也是系统守护进程,它负责启动运行层次特定的系统服务,所以很多进程的PPID都是1。1号进程也负责收养孤儿进程。
  4. CMD列中不带[ ]的进程是普通的守护进程(用户级守护进程)。

守护进程的编写

具体的实现流程为:

  1. 调用fork或vfork生成一个进程,然后父进程退出。 这一步的作用是让子进程调用setsid能够脱离原来的进程组,让其不受终端控制。
  2. 子进程调用setsid,使子进程成为一个新的会话组长和进程组组长。相当于与原来的会话脱离,这样当终端退出时,就不会给子进程发SIGHUP信号。这一步很重要
  3. 禁止进程重新打开控制终端(这一步不是必须的)。通过使进程不再是会话组长来实现,再一次通过fork创建新的子进程,使调用fork的进程退出 。
  4. 将当前进程工作目录更改为根目录(因为根目录下的文件不易被删除)。
  5. 关闭不再需要的文件描述符。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>

//创建守护进程
int create_daemon()
{
    int fd = 0;
    switch (fork())
    {
    case -1:
        //创建子进程失败,这里可以写入错误日志或者打印错误信息
        return -1;
    case 0:
        //子进程,break,执行下面的动作
        break;
    default:
        //父进程,直接退出
        exit(0);
    }
    //只有子进程才会走到这里
    //调用setsid脱离当前终端
    if (setsid() == -1)
    {
        // setsid调用错误写入错误日志
        return -1;
    }
    //chdir("/");

    //关闭所有文件描述符(除了标准输出), getdtablesize函数获取进程可打开最大文件数
    //也可以将标准输出进行重定向到文件中,这样就不会显示到屏幕上了
    close(0);
    for (int i = 2; i < getdtablesize(); i++)
    {
        close(i);
    }
    return 1;
}

int main()
{
    if (create_daemon() != 1)
    {
        //创建守护进程失败
        return 1;
    }
    else
    {
        //到这里,就是守护进程需要执行的动作,可以使用execl进行程序替换
        while (1)
        {
            sleep(1);
            //由于重定向了标准输入和输出,所以这一句不会打印到显示器
            printf("休息1秒,进程id=%d\n", getpid());
        }
    }
    return 0;
}

可以看到守护进程和它的bash不在同一个进程组中,因此bash退出并不会影响守护进程。

进程守护 supervisor 监控python吗 进程守护怎么用_bash_02


setsid函数的作用

当bash退出的时候,它会向会话中所有的进程发送SIGHUP信号,让它们都退出。

setsid()调用成功后,返回新的会话ID,调用setsid函数的进程成为新的会话的领头进程,并与其父进程的会话组和进程组脱离。

进程守护 supervisor 监控python吗 进程守护怎么用_linux_03

以下面的代码为例进行测试:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>

void sig_usr(int signo)
{
    printf("收到了SIGUSR1信号,进程id=%d\n", getpid());
}

int main()
{
    pid_t pid;
    printf("进程开始执行\n");
    if (signal(SIGUSR1, sig_usr) == SIG_ERR)
    {
        printf("无法捕捉SIGUSR1信号\n");
        exit(1);
    }

    //创建子进程
    pid = fork();
    if (pid < 0)
    {
        printf("子进程创建失败,程序退出\n");
        exit(1);
    }
    while (1)
    {
        sleep(1);
        printf("休眠一秒,进程id=%d\n", getpid());
    }
    printf("程序结束\n");
    return 0;
}

我们用strcae来跟踪bash、父进程、子进程:

进程守护 supervisor 监控python吗 进程守护怎么用_网络_04

接着,我们手动关闭bash:

进程守护 supervisor 监控python吗 进程守护怎么用_子进程_05

接下来,我们对子进程进行一些修改,让它调用setsid:

进程守护 supervisor 监控python吗 进程守护怎么用_bash_06

启动程序,再次查看,发现子进程的SID字段和PGRP字段都和父进程不相同:

进程守护 supervisor 监控python吗 进程守护怎么用_linux_07

我们再用同样的方式跟踪一下,可以看到子进程没有退出,并且被1号进程收养:

进程守护 supervisor 监控python吗 进程守护怎么用_bash_08

那你肯定会有这样的疑问,为什么需要子进程来调用setsid呢?父进程直接调用不行吗?

我们再将代码改一下,这一次父进程不创建子进程了,直接让父进程执行setsid:

进程守护 supervisor 监控python吗 进程守护怎么用_网络_09

进程守护 supervisor 监控python吗 进程守护怎么用_linux_10

可以看到执行失败,这主要的原因是:

  • 调用setsid()的进程将成为一个新的进程组的组长,脱离原来的进程组。这从上面的实验也能看到。
  • 父进程是其所在进程组的组长,如果允许一个进程组长调用setsid()的话,那父进程就会成为两个组的组长。很明显这是不合理的,因为 进程组组长的进程ID=进程组ID,那这样的话两个进程组的ID就要相同,或者进程组长有两个不同的进程ID。

另外再补充一点,既然bash退出的时候会向所有会话组的进程发送SIGHUP信号,那只需要捕捉这个信号,让其不执行原来的动作,就不会退出了。

nohup这个命令就能够让程序在运行时忽略SIGHUP信号,比如想让test程序在后台执行,将输出信息输出到log文件里,并且bash退出时test程序不退出,就可以这样写:

nohup ./test > log.txt 2>&1 &