在 Linux 中创建守护进程

Posted

技术标签:

【中文标题】在 Linux 中创建守护进程【英文标题】:Creating a daemon in Linux 【发布时间】:2013-07-31 00:53:31 【问题描述】:

在 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;

【问题讨论】:

可能重复:***.com/q/5384168/1076451 可能重复:***.com/questions/5384168/… 用于守护程序部分,***.com/questions/931093/… 用于文件系统监视 如果您不需要 POSIX 合规性,您可能会对 inotify API 感兴趣。请参阅:inotify_initinotify_add_watchinotify_rm_watch 【参考方案1】:

在 Linux 中,我想添加一个无法停止并监视文件系统更改的守护程序。如果检测到任何更改,它应该将路径写入启动它的控制台 + 换行符。

守护进程在后台工作并且(通常...)不属于 TTY,这就是为什么您不能以您可能想要的方式使用 stdout/stderr。 通常使用 syslog 守护进程 (syslogd) 将消息记录到文件(调试、错误...)。

除此之外,还有一些必需的步骤来守护进程。


如果我没记错的话,这些步骤是:

fork 关闭父进程,如果 fork 成功则让它终止。 -> 因为父进程已经终止,子进程现在在后台运行。 setsid - 创建一个新会话。调用进程成为新会话的领导者和新进程组的进程组领导者。该进程现在已与其控制终端 (CTTY) 分离。 捕捉信号 - 忽略和/或处理信号。 再次分叉并让父进程终止以确保您摆脱会话领导进程。 (只有会议负责人可以再次获得 TTY。) chdir - 更改守护程序的工作目录。 umask - 根据守护进程的需要更改文件模式掩码。 close - 关闭所有可能从父进程继承的打开文件描述符。

为您提供一个起点:查看显示基本步骤的框架代码。此代码现在也可以在 GitHub 上分叉:Basic skeleton of a linux daemon

/*
 * 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 |西德 |文字打字机 | TPGID |统计 |识别码 |时间 |命令行 | +-----+-----+------+------+-----+--------+------+ ------+------+------+ | 1 | 3387 | 3386 | 3386 | ? | -1 |小号 | 1000 | 0:00 | ./ | +-----+-----+------+------+-----+--------+------+ ------+------+------+

您应该在这里看到的是:

守护进程没有控制终端 (TTY = ?) 父进程 ID (PPID) 为 1(初始化进程) PID != SID 表示我们的进程不是会话领导者 (因为第二个 fork()) 因为 PID != SID 我们的进程无法再次控制 TTY

阅读系统日志:

找到您的系统日志文件。我的在这里:/var/log/syslog

做一个:grep firstdaemon /var/log/syslog

输出应该和这个类似:

firstdaemon[3387]:第一个守护进程启动。 firstdaemon[3387]:第一个守护进程终止。

备注: 实际上,您还希望实现一个信号处理程序并正确设置日志记录(文件、日志级别...)。

进一步阅读:

Linux-UNIX-Programmierung - German Unix Daemon Server Programming

【讨论】:

哇,谢谢!那太棒了。所以我必须把我的代码放到while循环中就可以了? 基本上,是的。但这段代码只是一个例子。这完全取决于使用守护进程您想要实现什么。请务必阅读此答案:@Edwin 而不是第二个fork(),为什么不直接使用setsid() Note表示sigaction()函数提供了更全面、更可靠的信号控制机制;新应用程序应使用sigaction() 而不是signal() 应该提醒观众,这种方法是“老”的方式。创建守护程序的新推荐方法是使用此处的“新样式守护程序”:0pointer.de/public/systemd-man/daemon.html#New-Style%20Daemons 或【参考方案2】:

man 7 daemon 详细描述了如何创建守护进程。我的回答只是摘自本手册。

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

    传统的 SysV 守护进程 (old-style), systemd 守护进程 (new-style)。

SysV 守护进程

如果你对传统的SysV 守护进程感兴趣,你应该实现following steps:

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

注意这个警告:

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

需要为 SysV 系统提供兼容性的守护进程应该实现上面指出的方案。但是,建议通过命令行参数使此行为成为可选和可配置的,以简化调试并简化使用 systemd 与系统的集成。

请注意,daemon() 不符合 POSIX。


新型守护进程

对于新型守护进程,推荐使用following steps:

    如果收到SIGTERM,请关闭守护程序并彻底退出。 如果收到SIGHUP,则重新加载配置文件(如果适用)。 从主守护进程提供正确的退出代码,因为它被 init 系统用来检测服务错误和问题。建议遵循LSB recommendations for SysV init scripts 中定义的退出代码方案。 如果可能且适用,通过D-Bus IPC 系统公开守护程序的控制接口,并获取总线名称作为初始化的最后一步。 要在 systemd 中集成,请提供一个 .service unit 文件,其中包含有关启动、停止和以其他方式维护守护程序的信息。详情请见systemd.service(5)。 尽可能依赖init系统的功能来限制守护进程对文件、服务和其他资源的访问,即在systemd的情况下,依赖systemd的resource limit control而不是自己实现,依赖systemd 的privilege dropping 代码而不是在守护进程中实现它,等等。有关可用控件,请参阅 systemd.exec(5)。 如果使用D-Bus,通过提供D-Bus 服务激活configuration file 使您的守护程序总线可激活。这有很多优点:你的守护进程可以按需延迟启动;它可以与其他需要它的守护进程并行启动——最大化并行化和boot-up speed;您的守护程序可以在失败时重新启动,而不会丢失任何总线请求,因为总线将可激活服务的请求排队。详情请见below。 如果您的守护进程通过套接字向其他本地进程或远程客户端提供服务,则应按照below 指出的方案将其设置为socket-activatable。与 D-Bus 激活一样,这支持按需启动服务,并允许改进服务启动的并行化。此外,对于无状态协议(例如 syslog、DNS),实现基于套接字的激活的守护进程可以在不丢失单个请求的情况下重新启动。详情请见below。 如果适用,守护程序应通过sd_notify(3) 接口通知初始化系统启动完成或状态更新。 不使用syslog() 调用直接记录到系统syslog 服务,新型守护程序可以选择通过fprintf() 简单地记录到标准错误,然后由init 系统转发到syslog。如果需要日志级别,可以通过在单个日志行前加上“”之类的字符串(对于 syslog 优先级方案中的日志级别 4“WARNING”)进行编码,遵循与 Linux 内核的printk() 级别类似的样式系统。详情请见sd-daemon(3)systemd.exec(5)

要了解更多信息,请阅读全文man 7 daemon

【讨论】:

【参考方案3】:

你不能在 linux 中创建一个不能被杀死的进程。 root 用户(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);

这里是一个示例程序,它变成一个守护进程,挂起,然后离开。

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 和设备传递字符以打开标准输入等,然后你打开标准输入、标准输出、标准错误。然而,你还做了一件 Pascal Werkl 没有做的微妙的事情——他关闭了从 _SC_OPEN_MAX 到 fd0 的所有文件描述符,同时你关闭 (_SC_OPEN_MAX-1) 到 fd1,留下打开的 _SC_OPEN_MAX (fd1024) 和 fd0 .这是为什么呢?【参考方案4】:

尝试使用daemon函数:

#include <unistd.h>

int daemon(int nochdir, int noclose);

来自man page:

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

如果 nochdir 为零,daemon() 改变调用进程的当前 工作目录到根目录(“/”);否则,当前 工作目录保持不变。

如果 noclose 为零,daemon() 重定向标准输入,标准 输出和标准错误到 /dev/null;否则,没有变化 对这些文件描述符进行了处理。

【讨论】:

请注意,daemon(7) 手册中提到了创建守护进程的步骤并警告说:不应使用 BSD daemon() 函数,因为它只实现了这些步骤的子集。 daemon 函数最早出现在 4.4BSD 并且不是 POSIX-compliant。 还要注意关于使用 daemon() 的警告位于 daemon(7) man page 的旧式 SysV 部分。 systemd 不建议使用 daemon()。【参考方案5】:

我可以停止在第一个要求“一个守护进程无法停止 ...”

不可能,我的朋友;但是,您可以使用更好的工具,即内核模块来实现同样的目的。

http://www.infoq.com/articles/inotify-linux-file-system-event-monitoring

可以停止所有守护程序。有些人比其他人更容易停止。即使是一个守护进程对,其伙伴处于按住状态,如果失去伙伴,则重新生成伙伴,也可以被阻止。你只需要再努力一点。

【讨论】:

我认为作者所说的“无法停止的守护进程”实际上是指会话终止时守护进程始终在后台运行。【参考方案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

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

【讨论】:

这与C无关。 我很欣赏有一个 C 标签。但是,OP 在问题中没有提到关于 C 的要求。标题是在linux中创造一个恶魔。这个答案满足了。 哦,你是对的。它被标记为 C,但实际要求是 C++(正如 OP 的代码和链接文章所证明的那样)。【参考方案7】:

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

也许这会有所帮助: http://www.netzmafia.de/skripten/unix/linux-daemon-howto.html

【讨论】:

【参考方案8】:

守护程序模板

我在新式守护进程之后写了一个守护进程模板:link

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

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

【讨论】:

【参考方案9】:

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

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

【讨论】:

谢谢。那么“守护程序”和普通程序之间没有区别吗?我不希望它被轻易关闭。 不,守护进程只是一个后台进程。更具体地说,您从父进程派生,运行子进程并终止父进程(这样就没有终端访问程序)。尽管要成为“守护进程”,但这并不是必需的:en.wikipedia.org/wiki/Daemon_(computing)

以上是关于在 Linux 中创建守护进程的主要内容,如果未能解决你的问题,请参考以下文章

如何开始在类 Unix 操作系统(如 Linux)中编写守护进程?

守护进程模型创建思路及详细实现代码

linux中的守护进程

Android C++系列:Linux守护进程

celery: 守护进程不允许有子进程

如何阻止 mkdir() 文件将“守护进程”设置为管理员