在Linux下创建守护进程

144
在Linux中,我想添加一个不能被停止的守护进程,用于监视文件系统的变化。如果检测到任何更改,它应该将路径写入启动它的控制台并换行。
我已经准备好了几乎所有的文件系统更改代码,但是我无法弄清如何创建一个守护进程。
我的代码来自这里:http://www.yolinux.com/TUTORIALS/ForkExecProcesses.html 在分叉之后该怎么做?
int main (int argc, char **argv) {

  pid_t pID = fork();
  if (pID == 0)  {              // child
          // Code only executed by child process    
      sIdentifier = "Child Process: ";
    }
    else if (pID < 0) {
        cerr << "Failed to fork" << endl;
        exit(1);
       // Throw exception
    }
    else                                   // parent
    {
      // Code only executed by parent process

      sIdentifier = "Parent Process:";
    }       

    return 0;
}

1
可能是重复问题:https://dev59.com/yG435IYBdhLWcg3wfgIf - Chimera
1
可能是重复的问题:https://dev59.com/yG435IYBdhLWcg3wfgIf(有关daemonize部分),https://dev59.com/XHNA5IYBdhLWcg3wgeOk(有关文件系统监视)。 - Ciro Santilli OurBigBook.com
如果您不需要 POSIX 兼容性,则可能对 inotify API 感兴趣。请参阅:inotify_initinotify_add_watchinotify_rm_watch - patryk.beza
9个回答

272
在Linux中,我想添加一个不能停止且监视文件系统更改的守护进程。如果检测到任何更改,它应该将路径写入启动它的控制台并换行。 守护进程在后台运行(通常…),不属于TTY,所以您无法以想要的方式使用stdout/stderr输出。通常使用系统日志守护进程(syslogd)将消息记录到文件(debug, error,…) 此外,有一些必要的步骤来使进程成为守护进程。
如果我没记错的话,这些步骤应该是这样的:
  • fork 父进程并让它在分离后终止。-> 因为父进程已经终止了,子进程现在在后台运行。
  • setsid - 创建一个新会话。调用进程成为新会话的领导者和新进程组的进程组长。该进程现在与其控制终端(CTTY)分离。
  • 捕获信号 - 忽略和/或处理信号。
  • 再次fork 并让父进程终止,以确保您摆脱了会话领导进程。(只有会话领导者才能再次获得一个TTY)
  • chdir - 更改守护进程的工作目录。
  • umask - 根据守护进程的需要更改文件模式掩码。
  • close - 关闭可能从父进程继承的所有打开的文件描述符。

为了给你一个起点:看看这个骨架代码,展示了基本步骤。现在这段代码也可以在GitHub上分叉:Linux守护进程的基本骨架

/*
 * daemonize.c
 * This example daemonizes a process, writes a few log messages,
 * sleeps 20 seconds and terminates afterwards.
 */

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

static void skeleton_daemon()
{
    pid_t pid;

    /* Fork off the parent process */
    pid = fork();

    /* An error occurred */
    if (pid < 0)
        exit(EXIT_FAILURE);

    /* Success: Let the parent terminate */
    if (pid > 0)
        exit(EXIT_SUCCESS);

    /* On success: The child process becomes session leader */
    if (setsid() < 0)
        exit(EXIT_FAILURE);

    /* Catch, ignore and handle signals */
    //TODO: Implement a working signal handler */
    signal(SIGCHLD, SIG_IGN);
    signal(SIGHUP, SIG_IGN);

    /* Fork off for the second time*/
    pid = fork();

    /* An error occurred */
    if (pid < 0)
        exit(EXIT_FAILURE);

    /* Success: Let the parent terminate */
    if (pid > 0)
        exit(EXIT_SUCCESS);

    /* Set new file permissions */
    umask(0);

    /* Change the working directory to the root directory */
    /* or another appropriated directory */
    chdir("/");

    /* Close all open file descriptors */
    int x;
    for (x = sysconf(_SC_OPEN_MAX); x>=0; x--)
    {
        close (x);
    }

    /* Open the log file */
    openlog ("firstdaemon", LOG_PID, LOG_DAEMON);
}

int main()
{
    skeleton_daemon();

    while (1)
    {
        //TODO: Insert daemon code here.
        syslog (LOG_NOTICE, "First daemon started.");
        sleep (20);
        break;
    }

    syslog (LOG_NOTICE, "First daemon terminated.");
    closelog();

    return EXIT_SUCCESS;
}


  • 编译代码:gcc -o firstdaemon daemonize.c
  • 启动守护进程:./firstdaemon
  • 检查所有工作是否正常:ps -xj | grep firstdaemon

  • 输出应该类似于:

+------+------+------+------+-----+-------+------+------+------+-----+
| PPID | PID  | PGID | SID  | TTY | TPGID | STAT | UID  | TIME | CMD |
+------+------+------+------+-----+-------+------+------+------+-----+
|    1 | 3387 | 3386 | 3386 | ?   |    -1 | S    | 1000 | 0:00 | ./  |
+------+------+------+------+-----+-------+------+------+------+-----+

您应该看到的是:

  • 该守护进程没有控制终端 (TTY = ?)
  • 父进程ID (PPID)为1(init进程)
  • PID != SID,这意味着我们的进程不是会话领导者(因为第二个fork())
  • 由于PID != SID,我们的进程无法再次控制TTY

阅读syslog:

  • 找到你的syslog文件。我的在这里:/var/log/syslog
  • 执行命令:grep firstdaemon /var/log/syslog

  • 输出应类似于以下内容:

  firstdaemon[3387]: First daemon started.
  firstdaemon[3387]: First daemon terminated.


说明: 实际上您还需要实现信号处理程序并正确设置日志记录(文件、日志级别等)。

更多阅读:


哇,谢谢!太棒了。所以我只需要把我的代码放进 while 循环里就可以了? - chrisMe
1
为什么不使用setsid()来代替第二个fork()呢? - Chimera
@Chimera 如果你第二次使用setsid而不是fork,你会得到与你想要的相反的结果... - Pascal Werkl
1
请注意,sigaction()函数提供了更全面和可靠的控制信号的机制;新应用程序应该使用sigaction()而不是signal() - patryk.beza
9
需要注意的是,这种方法是“旧”的方式。创建守护进程的新推荐方式是使用在此处找到的“新风格守护程序”:http://0pointer.de/public/systemd-man/daemon.html#New-Style%20Daemons - JoakimE
显示剩余8条评论

44

man 7 daemon详细描述了如何创建守护进程。我的答案只是从这个手册中摘录的。

至少有两种类型的守护进程:

  1. 传统的SysV守护进程(旧风格),
  2. Systemd守护进程(新风格)。

SysV守护进程

如果您对传统的SysV守护进程感兴趣,您应该实现以下步骤

  1. 关闭除标准输入、输出和错误(即前三个文件描述符0、1、2)以外的所有打开的文件描述符。这可以确保在守护进程中没有意外传递的文件描述符。在Linux上,最好通过迭代/proc/self/fd来实现,如果失败,则回退到从文件描述符3迭代到getrlimit()返回的RLIMIT_NOFILE值。
  2. 重置所有信号处理程序为它们的默认值。最好通过遍历可用信号,直到达到_NSIG的限制,并将它们重置为SIG_DFL来完成此操作。
  3. 使用sigprocmask()重置信号掩码。
  4. 清理环境块,删除或重置可能对守护进程运行产生负面影响的环境变量。
  5. 调用fork()创建一个后台进程。
  6. 在子进程中,调用setsid()以分离任何终端并创建独立的会话
  7. 在子进程中,再次调用fork(),以确保守护进程永远无法重新获取终端。
  8. 在第一个子进程中调用exit(),以便只有第二个子进程(实际的守护进程)保留。这确保了守护进程进程被重新父化为init/PID 1,因为所有守护进程都应该是这样的。
  9. 在守护进程中,将/dev/null连接到标准输入、输出和错误。
  10. 在守护进程中,将umask重置为0,以便传递给open()mkdir()等的文件模式直接控制所创建的文件和目录的访问模式。
  11. 在守护进程中,更改当前目录为根目录(/),以避免守护进程无意中阻止挂载点被卸载。
  12. 在守护进程中,将守护进程PID(由getpid()返回)写入PID文件,例如/run/foobar.pid(对于虚构的守护进程“foobar”),以确保守护进程不能多次启动。这必须以无竞争的方式实现,以便仅在同时验证PID文件中先前存储的PID不再存在或属于外部进程时更新PID文件。
  13. 在守护进程中,如果可能和适用,则放弃特权。
  14. 从守护进程中通知启动的原始进程初始化完成。这可以通过在第一个fork()之前创建并因此在原始进程和守护进程中都可用的未命名管道或类似的通信渠道来实现。
  15. 在原始进程中调用exit()。调用守护进程的进程必须能够依赖于此exit()在初始化完成并且所有外部通信渠道已建立和可访问之后发

    请注意此警告:

    不应使用BSD daemon()函数,因为它仅实现了这些步骤的子集

    需要提供与SysV系统兼容性的守护进程应该实现上述方案。然而,建议将此行为作为可选项和可通过命令行参数进行配置,以便于调试,并简化集成到使用systemd的系统中。

    请注意daemon()不符合POSIX标准。


    新式守护进程

    对于新式守护进程,建议采取以下步骤

    1. 如果收到 SIGTERM,则关闭守护进程并正常退出。
    2. 如果收到 SIGHUP,则重新加载配置文件(如果适用)。
    3. 为主要守护进程提供正确的退出代码,因为这将被 init 系统用于检测服务错误和问题。建议遵循 LSB 推荐的 SysV init 脚本的退出代码方案
    4. 如果可能且适用,则通过 D-Bus IPC 系统公开守护进程的控制接口,并在初始化的最后一步抓取总线名称。
    5. 为了集成到 systemd 中,请提供一个 .service unit 文件,其中包含有关启动、停止和维护守护程序的信息。详见 systemd.service(5)
    6. 尽可能地依赖 init 系统的功能来限制守护进程对文件、服务和其他资源的访问,即在 systemd 的情况下,依赖 systemd 的 资源限制控制 而不是在守护进程中实现自己的限制,依赖 systemd 的 特权降低 代码而不是在守护进程中实现它,等等。请参见 systemd.exec(5) 以获取可用的控件。
    7. 如果使用 D-Bus,请通过提供 D-Bus 服务激活 配置文件 使您的守护程序总线可激活。这有多个优点:您的守护程序可以按需惰性启动;它可以与其他需要它的守护程序并行启动,从而最大化并行和 启动速度;您的守护程序在失败时可以重新启动而不会失去任何总线请求,因为总线会为可激活服务排队请求。详见 下面
    8. 如果您的守护进程通过套接字向其他本地进程或远程客户端提供服务,则应按照下面 指出的方案 将其设置为 套接字可激活。与 D-Bus 激活一样,这也允许按需启动服务,并且它允许改进服务启动的并行性。此外,对于无状态协议(例如 syslog、DNS),实现基于套接字的激活的守护进程可以在不丢失任何请求的情况下重新启动。详见 下面
    9. 如果适用,守护进程应通过 sd_notify(3) 接口通知 init 系统有关启动完成或状态更新的信息。
    10. 新式守护进程可以选择通过 fprintf() 将日志记录到标准错误输出,而不是直接调用 syslog() 记录到系统 syslog 服务。如果需要日志级别,则可以通过像 "<4>" 这样的字符串前缀来编码各个日志行,该字符串表示 syslog 优先级方案中的日志级别 4 "WARNING",并遵循类似于 Linux 内核的 printk() 级别系统的风格。详见 sd-daemon(3)systemd.exec(5)

    要了解更多信息,请阅读整个 man 7 daemon


11

在Linux中,无法创建一个无法被杀死的进程。根用户(uid=0)可以向进程发送信号,有两个信号无法被捕获,分别是SIGKILL=9和SIGSTOP=19。当其他信号无法被捕获时也可能导致进程终止。

您可能需要一个更通用的daemonize函数,其中可以指定程序/守护程序的名称和运行程序的路径(例如“/”或“/tmp”)。您还可以提供stderr和stdout的文件(以及可能使用stdin的控件路径)。

以下是必要的包含内容:

#include <stdio.h>    //printf(3)
#include <stdlib.h>   //exit(3)
#include <unistd.h>   //fork(3), chdir(3), sysconf(3)
#include <signal.h>   //signal(3)
#include <sys/stat.h> //umask(3)
#include <syslog.h>   //syslog(3), openlog(3), closelog(3)

这里有一个更通用的函数:

int
daemonize(char* name, char* path, char* outfile, char* errfile, char* infile )
{
    if(!path) { path="/"; }
    if(!name) { name="medaemon"; }
    if(!infile) { infile="/dev/null"; }
    if(!outfile) { outfile="/dev/null"; }
    if(!errfile) { errfile="/dev/null"; }
    //printf("%s %s %s %s\n",name,path,outfile,infile);
    pid_t child;
    //fork, detach from process group leader
    if( (child=fork())<0 ) { //failed fork
        fprintf(stderr,"error: failed fork\n");
        exit(EXIT_FAILURE);
    }
    if (child>0) { //parent
        exit(EXIT_SUCCESS);
    }
    if( setsid()<0 ) { //failed to become session leader
        fprintf(stderr,"error: failed setsid\n");
        exit(EXIT_FAILURE);
    }

    //catch/ignore signals
    signal(SIGCHLD,SIG_IGN);
    signal(SIGHUP,SIG_IGN);

    //fork second time
    if ( (child=fork())<0) { //failed fork
        fprintf(stderr,"error: failed fork\n");
        exit(EXIT_FAILURE);
    }
    if( child>0 ) { //parent
        exit(EXIT_SUCCESS);
    }

    //new file permissions
    umask(0);
    //change to path directory
    chdir(path);

    //Close all open file descriptors
    int fd;
    for( fd=sysconf(_SC_OPEN_MAX); fd>0; --fd )
    {
        close(fd);
    }

    //reopen stdin, stdout, stderr
    stdin=fopen(infile,"r");   //fd=0
    stdout=fopen(outfile,"w+");  //fd=1
    stderr=fopen(errfile,"w+");  //fd=2

    //open syslog
    openlog(name,LOG_PID,LOG_DAEMON);
    return(0);
}

这是一个样例程序,它会变成一个守护进程(daemon),并持续运行一段时间后退出。
int
main()
{
    int res;
    int ttl=120;
    int delay=5;
    if( (res=daemonize("mydaemon","/tmp",NULL,NULL,NULL)) != 0 ) {
        fprintf(stderr,"error: daemonize failed\n");
        exit(EXIT_FAILURE);
    }
    while( ttl>0 ) {
        //daemon code here
        syslog(LOG_NOTICE,"daemon ttl %d",ttl);
        sleep(delay);
        ttl-=delay;
    }
    syslog(LOG_NOTICE,"daemon ttl expired");
    closelog();
    return(EXIT_SUCCESS);
}

请注意,SIG_IGN 表示捕获并忽略信号。您可以构建一个信号处理程序,记录信号接收情况并设置标志(例如,指示优雅关闭的标志)。

这与Pascal Werkl在此处上面两个答案中所接受的答案中的代码在功能上完全相同,唯一的例外是您的代码将字符传递给chdir和打开stdin等设备,然后打开stdin、stdout和stderr。但是,您还做了一件Pascal Werkl没有做的微妙之事——他关闭了_SC_OPEN_MAX到fd0(包括fd0)的所有文件描述符,而您关闭了(_SC_OPEN_MAX-1)到fd1,保留了_SC_OPEN_MAX(fd1024)和fd0。为什么? - astronomerdave

9
尝试使用daemon函数:
#include <unistd.h>

int daemon(int nochdir, int noclose);

man页中得知:

daemon()函数适用于希望将自己与控制终端分离并作为系统守护进程在后台运行的程序。

如果nochdir为零,daemon()函数会将调用进程的当前工作目录更改为根目录(“/”);否则,当前的工作目录不变。

如果noclose为零,daemon()函数将标准输入、标准输出和标准错误重定向到/dev/null;否则,这些文件描述符不做任何更改。


2
请注意,daemon(7)手册中提到了创建守护进程的步骤,并警告说:不应使用BSD daemon()函数,因为它只实现了这些步骤的子集。 daemon函数最初出现在4.4BSD不是 POSIX兼容 - patryk.beza
3
还要注意的是,在daemon(7)手册页的旧式SysV部分中提到了有关使用daemon()的警告。对于systemd,不建议避免使用daemon()函数。 - user2534096

6
如果您的应用程序属于以下之一:
{
  ".sh": "bash",
  ".py": "python",
  ".rb": "ruby",
  ".coffee" : "coffee",
  ".php": "php",
  ".pl" : "perl",
  ".js" : "node"
}

如果您不介意有NodeJS依赖项,那么请安装NodeJS并执行以下操作:

npm install -g pm2

pm2 start yourapp.yourext --name "fred" # where .yourext is one of the above

pm2 start yourapp.yourext -i 0 --name "fred" # run your app on all cores

pm2 list

为了在重启后保持所有应用程序运行(并让pm2成为守护进程):
pm2 startup

pm2 save

现在您可以:
service pm2 stop|restart|start|status

(还可以轻松地让您监视应用程序目录中的代码更改,并在代码更改发生时自动重新启动应用程序进程)

2
这与C语言无关。 - melpomene
4
我明白有一个C标签。然而,原帖并未在问题中提及与C相关的要求。标题是“在Linux中创建恶魔”,该回答满足了该要求。 - danday74
1
哦,你说得对。它标记为C,但实际要求是C++(根据OP的代码和链接的文章可见)。 - melpomene

6
我可以先看第一个要求:“不能被停止的守护进程…”,很遗憾这是不可能的。但是,你可以使用更好的工具,即内核模块来实现同样的功能。
这里有一篇文章介绍了一个名为inotify的Linux文件系统事件监控工具:http://www.infoq.com/articles/inotify-linux-file-system-event-monitoring
所有的守护进程都可以被停止,某些守护进程比其他的更容易停止。甚至对于伴随着可以重启丢失伴侣的守护进程对,也可以停止它们,只是需要花费一些额外的功夫。

8
作者所说的“无法停止的守护进程”实际上是指,当会话结束时,该守护进程仍在后台持续运行。 - FaceBro

4

守护进程模板

我编写了一个遵循新式守护进程的守护进程模板: 链接

你可以在GitHub上找到整个模板代码: 这里

Main.cpp

// This function will be called when the daemon receive a SIGHUP signal.
void reload() {
    LOG_INFO("Reload function called.");
}

int main(int argc, char **argv) {
    // The Daemon class is a singleton to avoid be instantiate more than once
    Daemon& daemon = Daemon::instance();
    // Set the reload function to be called in case of receiving a SIGHUP signal
    daemon.setReloadFunction(reload);
    // Daemon main loop
    int count = 0;
    while(daemon.IsRunning()) {
        LOG_DEBUG("Count: ", count++);
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    LOG_INFO("The daemon process ended gracefully.");
}

Daemon.hpp

class Daemon {
    public:

    static Daemon& instance() {
        static Daemon instance;
        return instance;
    }

    void setReloadFunction(std::function<void()> func);

    bool IsRunning();

    private:

    std::function<void()> m_reloadFunc;
    bool m_isRunning;
    bool m_reload;

    Daemon();
    Daemon(Daemon const&) = delete;
    void operator=(Daemon const&) = delete;

    void Reload();

    static void signalHandler(int signal);
};

Daemon.cpp

Daemon::Daemon() {
    m_isRunning = true;
    m_reload = false;
    signal(SIGINT, Daemon::signalHandler);
    signal(SIGTERM, Daemon::signalHandler);
    signal(SIGHUP, Daemon::signalHandler);
}

void Daemon::setReloadFunction(std::function<void()> func) {
    m_reloadFunc = func;
}

bool Daemon::IsRunning() {
    if (m_reload) {
        m_reload = false;
        m_reloadFunc();
    }
    return m_isRunning;
}

void Daemon::signalHandler(int signal) {
    LOG_INFO("Interrup signal number [", signal,"] recived.");
    switch(signal) {
        case SIGINT:
        case SIGTERM: {
            Daemon::instance().m_isRunning = false;
            break;
        }
        case SIGHUP: {
            Daemon::instance().m_reload = true;
            break;
        }
    }
}

daemon-template.service

[Unit]
Description=Simple daemon template
After=network.taget

[Service]
Type=simple
ExecStart=/usr/bin/daemon-template --conf_file /etc/daemon-template/daemon-tenplate.conf
ExecReload=/bin/kill -HUP $MAINPID
User=root
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=daemon-template

[Install]
WantedBy=multi-user.target

3

通过调用fork()函数,您创建了一个子进程。如果fork成功(fork返回非零PID),则执行将从子进程中的此点继续。在这种情况下,我们希望优雅地退出父进程,然后在子进程中继续工作。

也许这可以帮助您: http://www.netzmafia.de/skripten/unix/linux-daemon-howto.html


2

守护进程就是在后台运行的进程。如果你想让你的程序在操作系统启动时启动,在Linux上,你可以将启动命令添加到/etc/rc.d/rc.local(在所有其他脚本之后运行)或/etc/startup.sh。

在Windows上,你需要创建一个服务,注册该服务,然后在管理 -> 服务面板中设置其在启动时自动启动。


1
谢谢。那么“守护进程”和普通程序之间没有区别吗?我不希望它容易被关闭。 - chrisMe
2
不,守护进程只是一个后台进程。更具体地说,您从父进程分叉,运行子进程并终止父进程(以便无法访问程序的终端)。但这并不是成为“守护进程”所必需的:http://en.wikipedia.org/wiki/Daemon_(computing) - KrisSodroski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接