• 守护进程的实现



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

    普通进程:

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

    守护进程:

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

    使用ps命令查看进程:

    在这里插入图片描述

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

    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 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    //创建守护进程
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

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

    在这里插入图片描述


    setsid函数的作用

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

    setsid()调用成功后,返回新的会话ID,调用setsid函数的进程成为新的会话的领头进程,并与其父进程的会话组和进程组脱离。
    在这里插入图片描述

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

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    我们用strcae来跟踪bash、父进程、子进程:
    在这里插入图片描述

    接着,我们手动关闭bash:
    在这里插入图片描述

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

    在这里插入图片描述

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

    在这里插入图片描述

    我们再用同样的方式跟踪一下,可以看到子进程没有退出,并且被1号进程收养:
    在这里插入图片描述

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

    我们再将代码改一下,这一次父进程不创建子进程了,直接让父进程执行setsid:
    在这里插入图片描述

    在这里插入图片描述

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

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

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

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

    nohup ./test > log.txt 2>&1 &
    
    • 1
  • 相关阅读:
    Spring MVC
    通过 Docker 灵活部署 Neo4j 图数据库
    Redis及其常用命令(二)
    Keepalived+Nginx高可用集群
    网络上哪里找到西北农林科技大学考研真题复习资料?
    小程序提交表单之后,清除表单form
    .NET 6 跨服务器联表查询, MySql、Oracle、SqlServer等相互联表
    SpringCloud相关技术总结
    【JAVASE】java开发人员最常犯的10中错误
    【C++】模板初阶
  • 原文地址:https://blog.csdn.net/qq_52670477/article/details/126438895