Linux Daemon Writing HOWTO
这里视频讲的很清楚
牛客网-c/C++Linux课程-守护进程
在这里插入图片描述

int main() {

    // 1.创建子进程,退出父进程
    pid_t pid = fork();

    if(pid > 0) {
        exit(0);
    }

    // 2.将子进程重新创建一个会话
    setsid();

    // 3.设置掩码
    umask(022);

    // 4.更改工作目录
    chdir("/home/nowcoder/");

    // 5. 关闭、重定向文件描述符
    int fd = open("/dev/null", O_RDWR);
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);

    // 6.业务逻辑

    // 捕捉定时信号
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    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(10);
    }

    return 0;
}

1
什么是守护进程?
守护进程是后台运行,自动运行,少交互或不交互。
守护进程(或服务)是一个后台进程,设计为自动运行,几乎不需要用户干预。ApacheWeb服务器http守护程序(httpd)就是这样一个守护程序示例。它在后台监听特定端口,并根据请求类型提供页面或处理脚本。

在Linux中创建守护进程会按照给定的顺序使用一组特定的规则。了解它们是如何工作的将有助于您了解守护进程在userland Linux中是如何运行的,但也可以通过对内核的调用来运行。事实上,一些守护进程与与与硬件设备(如外部控制器板、打印机和PDA)一起工作的内核模块接口。它们是Linux的基本构建块之一,为Linux提供了难以置信的灵活性和强大功能。

在本教程中,将用C语言构建一个非常简单的守护进程。随着我们的深入,将添加更多代码,显示启动和运行守护进程所需的正确执行顺序。
2.开始

首先,您需要在Linux机器上安装以下软件包来开发守护进程,具体来说:

GCC 3.2.2或更高版本

Linux开发头和库

如果您的系统尚未安装这些组件(不太可能,但还是要检查),则需要它们来开发本指南中的示例。要了解您安装的GCC版本,请使用:

gcc——版本

3.规划你的守护进程

3.1它将做什么?

守护进程应该做一件事,并且做得很好。这一点可能与管理多个域上的数百个邮箱一样复杂,也可能与编写报告并调用sendmail将其发送给管理员一样简单。

在任何情况下,你都应该有一个很好的计划来指导守护进程应该做什么。如果它将与你可能或可能不写的其他守护进程进行交互操作,那么这也是另一个需要考虑的问题。

3.2有多少互动?

守护进程永远不应该通过终端与用户直接通信。事实上,守护进程根本不应该与用户直接通信。所有通信都应该通过某种类型的接口(您可能需要也可能不需要编写),这种接口可以像GTK+GUI一样复杂,也可以像信号集一样简单。

4.基本守护程序结构

当守护进程启动时,它必须做一些低级的家务来为自己真正的工作做好准备。这包括几个步骤:

 Fork off the parent process
    Change file mode mask (umask)
    Open any logs for writing
    Create a unique Session ID (SID)
    Change the current working directory to a safe place
    Close standard file descriptors
    Enter actual daemon code
放弃父进程

更改文件模式掩码(umask)

打开所有日志进行写入

创建唯一的会话ID(SID)

将当前工作目录更改为安全位置

关闭标准文件描述符

输入实际的守护程序代码 

4.1分叉父进程

守护进程由系统本身或终端或脚本中的用户启动。当它启动时,这个过程就像系统上的任何其他可执行文件一样。为了使其真正自治,必须在执行实际代码的地方创建子进程。这就是所谓的forking,它使用fork()函数:

            pid_t pid;

            /* Fork off the parent process */       
            pid = fork();
            if (pid < 0) {
                    exit(EXIT_FAILURE);
            }
            /* If we got a good PID, then
               we can exit the parent process. */
            if (pid > 0) {
                    exit(EXIT_SUCCESS);//
                    //避免儿子成为僵尸进程 父进程先退出   子进程就是后台进程了
            }

注意调用fork()之后的错误检查。在编写守护程序时,必须尽可能地编写防御代码。事实上,守护进程中有很大一部分代码只包含错误检查。

函数的作用是:返回子进程的进程id(PID)(不等于零),或者在失败时返回-1。如果进程不能派生子进程,那么守护进程应该在这里终止。

如果从fork()返回的PID成功,则父进程必须正常退出。PID >0 返回子进程的id 此时需要exit退出父进程,也避免儿进程成为僵尸进程。子进程就是后台进程了。对于没有见过它的人来说,这可能看起来很奇怪,但通过分叉,子进程将继续从代码中执行。

4.2更改文件模式掩码(Umask)

为了写入守护进程创建的任何文件(包括日志),必须更改文件模式掩码(umask),以确保它们可以正确写入或读取。这类似于从命令行运行umask,但我们在这里是通过编程实现的。我们可以使用umask()函数来实现这一点:

        pid_t pid, sid;
        
        /* Fork off the parent process */
        pid = fork();
        if (pid < 0) {
                /* Log failure (use syslog if possible) */
                exit(EXIT_FAILURE);
        }
        /* If we got a good PID, then
           we can exit the parent process. */
        if (pid > 0) {
                exit(EXIT_SUCCESS);
        }

        /* Change the file mode mask */
        umask(0);

通过将umask设置为0,我们可以完全访问守护进程生成的文件。即使您不打算使用任何文件,也最好在这里设置umask,以防您访问文件系统上的文件。

4.3打开日志进行书写

这部分是可选的,但建议您在系统中的某个位置打开日志文件进行写入。这可能是您可以查找有关守护进程的调试信息的唯一地方。

4.4创建唯一会话ID(SID)

从这里开始,子进程必须从内核获得唯一的SID才能运行。否则,子进程将成为系统中的孤立进程。上一节中声明的pid_t类型也用于为子进程创建新的SID:

        pid_t pid, sid;
        
        /* Fork off the parent process */
        pid = fork();
        if (pid < 0) {
                exit(EXIT_FAILURE);
        }
        /* If we got a good PID, then
           we can exit the parent process. */
        if (pid > 0) {
                exit(EXIT_SUCCESS);
        }
        
        /* Change the file mode mask */
        umask(0);
        
        /* Open any logs here */
        
        /* Create a new SID for the child process */
        sid = setsid();
        if (sid < 0) {
                /* Log any failure */
                exit(EXIT_FAILURE);
        }

同样,setsid()函数的返回类型与fork()相同。我们可以在这里应用相同的错误检查例程,以查看函数是否为子进程创建了SID。

4.5更改工作目录

当前的工作目录应该更改为保证始终存在的位置。由于许多Linux发行版并不完全遵循Linux文件系统层次结构标准,因此唯一保证存在的目录是根目录(/)。我们可以使用chdir()函数来实现这一点:

        pid_t pid, sid;
        
        /* Fork off the parent process */
        pid = fork();
        if (pid < 0) {
                exit(EXIT_FAILURE);
        }
        /* If we got a good PID, then
           we can exit the parent process. */
        if (pid > 0) {
                exit(EXIT_SUCCESS);
        }

        /* Change the file mode mask */
        umask(0);       
        
        /* Open any logs here */        
                
        /* Create a new SID for the child process */
        sid = setsid();
        if (sid < 0) {
                /* Log any failure here */
                exit(EXIT_FAILURE);
        }
        
        /* Change the current working directory */
        if ((chdir("/")) < 0) {
                /* Log any failure here */
                exit(EXIT_FAILURE);
        }
        

再一次,你可以看到防御性编码正在发生。chdir()函数在失败时返回-1,因此在更改到守护进程中的根目录后,一定要检查是否返回了-1。

4.6关闭标准文件描述符

设置守护进程的最后一步是关闭标准文件描述符(STDIN、STDOUT、STDERR)。由于守护进程无法使用终端,这些文件描述符是冗余的,存在潜在的安全隐患。

close()函数可以为我们处理这个问题:

        pid_t pid, sid;
        
        /* Fork off the parent process */
        pid = fork();
        if (pid < 0) {
                exit(EXIT_FAILURE);
        }
        /* If we got a good PID, then
           we can exit the parent process. */
        if (pid > 0) {
                exit(EXIT_SUCCESS);
        }
        
        /* Change the file mode mask */
        umask(0);       
        
        /* Open any logs here */
        
        /* Create a new SID for the child process */
        sid = setsid();
        if (sid < 0) {
                /* Log any failure here */
                exit(EXIT_FAILURE);
        }
        
        /* Change the current working directory */
        if ((chdir("/")) < 0) {
                /* Log any failure here */
                exit(EXIT_FAILURE);
        }
        
        
        /* Close out the standard file descriptors */
        close(STDIN_FILENO);
        close(STDOUT_FILENO);
        close(STDERR_FILENO);

坚持使用为文件描述符定义的常量是一个好主意,以实现系统版本之间的最大可移植性。

5.编写守护程序代码

5.1初始化

现在,您基本上已经告诉Linux您是一个守护程序,所以现在是时候编写实际的守护程序代码了。初始化是这里的第一步。因为这里可以调用许多不同的函数来设置守护进程的任务,所以我不会在这里深入讨论。

这里最重要的一点是,在初始化守护进程中的任何内容时,这里也适用相同的防御性编码准则。在写入系统日志或您自己的日志时,请尽可能详细。如果没有足够的关于守护进程状态的信息,调试守护进程可能会非常困难。

5.2大循环

守护进程的主代码通常位于无限循环中。从技术上讲,它不是一个无限循环,但它的结构是一个循环:

        pid_t pid, sid;
        
        /* Fork off the parent process */
        pid = fork();
        if (pid < 0) {
                exit(EXIT_FAILURE);
        }
        /* If we got a good PID, then
           we can exit the parent process. */
        if (pid > 0) {
                exit(EXIT_SUCCESS);
        }

        /* Change the file mode mask */
        umask(0);       
        
        /* Open any logs here */
        
        /* Create a new SID for the child process */
        sid = setsid();
        if (sid < 0) {
                /* Log any failures here */
                exit(EXIT_FAILURE);
        }
        
        
        /* Change the current working directory */
        if ((chdir("/")) < 0) {
                /* Log any failures here */
                exit(EXIT_FAILURE);
        }
        
        /* Close out the standard file descriptors */
        close(STDIN_FILENO);
        close(STDOUT_FILENO);
        close(STDERR_FILENO);
        
        /* Daemon-specific initialization goes here */
        
        /* The Big Loop */
        while (1) {
           /* Do some task here ... */
           sleep(30); /* wait 30 seconds */
        }

这种典型的循环通常是一个while循环,它有一个无限终止条件,在那里调用sleep使其以指定的间隔运行。

把它想象成心跳:当你的心跳时,它会执行一些任务,然后等待下一次心跳的发生。许多守护进程都遵循相同的方法。

6.把所有这些放在一起

6.1完整样品

下面列出的是一个完整的示例守护程序,它显示了设置和执行所需的所有步骤。要运行它,只需使用gcc编译,并从命令行开始执行。要终止,请在找到PID后使用kill命令。

我还加入了正确的include语句,用于与syslog进行接口,除了使用自己的日志进行fopen()/fwrite()/fclose()函数调用之外,建议至少发送start/stop/pause/die log语句。

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <syslog.h>
#include <string.h>

int main(void) {
        
        /* Our process ID and Session ID */
        pid_t pid, sid;
        
        /* Fork off the parent process */
        pid = fork();
        if (pid < 0) {
                exit(EXIT_FAILURE);
        }
        /* If we got a good PID, then
           we can exit the parent process. */
        if (pid > 0) {
                exit(EXIT_SUCCESS);
        }

        /* Change the file mode mask */
        umask(0);
                
        /* Open any logs here */        
                
        /* Create a new SID for the child process */
        sid = setsid();
        if (sid < 0) {
                /* Log the failure */
                exit(EXIT_FAILURE);
        }
        

        
        /* Change the current working directory */
        if ((chdir("/")) < 0) {
                /* Log the failure */
                exit(EXIT_FAILURE);
        }
        
        /* Close out the standard file descriptors */
        close(STDIN_FILENO);
        close(STDOUT_FILENO);
        close(STDERR_FILENO);
        
        /* Daemon-specific initialization goes here */
        
        /* The Big Loop */
        while (1) {
           /* Do some task here ... */
           
           sleep(30); /* wait 30 seconds */
        }
   exit(EXIT_SUCCESS);
}

从这里,您可以使用这个框架来编写自己的守护进程。确保添加自己的日志(或使用syslog工具),并进行防御性编码、防御性编码、防御性编码!

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐