linux环境下使用daemon函数实现守护进程

前言

daemon进程是后台守护进程,比如linux 下server都是daemon进程,像httpd、sshd等这些服务你肯定听说过,如果我们想写一个后台运行程序应该怎么做呢?其实非常容易,只要添加头文件 #include <unistd.h>,然后在调用 daemon(1, 0)函数即可。

daemon函数说明

/* Put the program in the background, and dissociate from the controlling
   terminal.  If NOCHDIR is zero, do `chdir ("/")'.  If NOCLOSE is zero,
   redirects stdin, stdout, and stderr to /dev/null.  */
extern int daemon (int __nochdir, int __noclose) __THROW __wur;
#endif /* Use misc.  */

头文件中已经说得很清楚了,当 nochdir为零时,将当前目录变为根目录,否则不变,当 noclose为零时,标准输入、标准输出和错误输出重导向为/dev/null不输出任何信息,否则照样输出。

daemon函数的实现

关于找daemon函数的实现还是费了一些功夫的,我发现我根本不会在github上查代码,一开始以为是linux源码所以去翻了Linus Torvalds的Linux源码结果没找到,最后还是在google收到了一些,整理如下:

安卓源码

https://android.googlesource.com/platform/bionic.git/+/donut-release/libc/unistd/daemon.c

#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int  daemon( int  nochdir, int  noclose )
{
   pid_t  pid;
   if ( !nochdir && chdir("/") != 0 )
       return -1;
   
   if ( !noclose )
   {
     int  fd = open("/dev/null", O_RDWR);
     if ( fd < 0 )
      return -1;
     if ( dup2( fd, 0 ) < 0 ||
      dup2( fd, 1 ) < 0 ||
          dup2( fd, 2 ) < 0 ) 
     {
       close(fd);
      return -1;
     }
     close(fd);
  }
  
   pid = fork();
   if (pid < 0)
    return -1;
   if (pid > 0)
    _exit(0);
   if ( setsid() < 0 )
     return -1;
   return 0;
}

苹果源码

https://opensource.apple.com/source/Libc/Libc-167/gen.subproj/daemon.c.auto.html

#include <fcntl.h>
#include <paths.h>
#include <unistd.h>

int
daemon(nochdir, noclose)
    int nochdir, noclose;
{
    int fd;

    switch (fork()) {
    case -1:
        return (-1);
    case 0:
        break;
    default:
        _exit(0);
    }

    if (setsid() == -1)
        return (-1);

    if (!nochdir)
        (void)chdir("/");

    if (!noclose && (fd = open(_PATH_DEVNULL, O_RDWR, 0)) != -1) {
        (void)dup2(fd, STDIN_FILENO);
        (void)dup2(fd, STDOUT_FILENO);
        (void)dup2(fd, STDERR_FILENO);
        if (fd > 2)
            (void)close (fd);
    }
    return (0);
}

glibc源码

https://elixir.bootlin.com/glibc/glibc-2.37/source/misc/daemon.c

#if defined(LIBC_SCCS) && !defined(lint)
static char sccsid[] = "@(#)daemon.c    8.1 (Berkeley) 6/4/93";
#endif /* LIBC_SCCS and not lint */

#include <errno.h>
#include <fcntl.h>
#include <paths.h>
#include <unistd.h>
#include <sys/stat.h>

#include <device-nrs.h>
#include <not-cancel.h>

int
daemon (int nochdir, int noclose)
{
    int fd;

    switch (__fork()) {
    case -1:
        return (-1);
    case 0:
        break;
    default:
        _exit(0);
    }

    if (__setsid() == -1)
        return (-1);

    if (!nochdir)
        (void)__chdir("/");

    if (!noclose) {
        struct __stat64_t64 st;

        if ((fd = __open_nocancel(_PATH_DEVNULL, O_RDWR, 0)) != -1
            && __glibc_likely (__fstat64_time64 (fd, &st) == 0)) {
            if (__builtin_expect (S_ISCHR (st.st_mode), 1) != 0
#if defined DEV_NULL_MAJOR && defined DEV_NULL_MINOR
                && (st.st_rdev
                == makedev (DEV_NULL_MAJOR, DEV_NULL_MINOR))
#endif
                ) {
                (void)__dup2(fd, STDIN_FILENO);
                (void)__dup2(fd, STDOUT_FILENO);
                (void)__dup2(fd, STDERR_FILENO);
                if (fd > 2)
                    (void)__close (fd);
            } else {
                /* We must set an errno value since no
                   function call actually failed.  */
                __close_nocancel_nostatus (fd);
                __set_errno (ENODEV);
                return -1;
            }
        } else {
            __close_nocancel_nostatus (fd);
            return -1;
        }
    }
    return (0);
}

glibc github地址

https://github.com/lattera/glibc/blob/master/misc/daemon.c

#if defined(LIBC_SCCS) && !defined(lint)
static char sccsid[] = "@(#)daemon.c    8.1 (Berkeley) 6/4/93";
#endif /* LIBC_SCCS and not lint */

#include <errno.h>
#include <fcntl.h>
#include <paths.h>
#include <unistd.h>
#include <sys/stat.h>

#include <device-nrs.h>
#include <not-cancel.h>

int
daemon (int nochdir, int noclose)
{
    int fd;

    switch (__fork()) {
    case -1:
        return (-1);
    case 0:
        break;
    default:
        _exit(0);
    }

    if (__setsid() == -1)
        return (-1);

    if (!nochdir)
        (void)__chdir("/");

    if (!noclose) {
        struct stat64 st;

        if ((fd = __open_nocancel(_PATH_DEVNULL, O_RDWR, 0)) != -1
            && (__builtin_expect (__fxstat64 (_STAT_VER, fd, &st), 0)
            == 0)) {
            if (__builtin_expect (S_ISCHR (st.st_mode), 1) != 0
#if defined DEV_NULL_MAJOR && defined DEV_NULL_MINOR
                && (st.st_rdev
                == makedev (DEV_NULL_MAJOR, DEV_NULL_MINOR))
#endif
                ) {
                (void)__dup2(fd, STDIN_FILENO);
                (void)__dup2(fd, STDOUT_FILENO);
                (void)__dup2(fd, STDERR_FILENO);
                if (fd > 2)
                    (void)__close (fd);
            } else {
                /* We must set an errno value since no
                   function call actually failed.  */
                __close_nocancel_nostatus (fd);
                __set_errno (ENODEV);
                return -1;
            }
        } else {
            __close_nocancel_nostatus (fd);
            return -1;
        }
    }
    return (0);
}

挺有意思的写法

https://github.com/bmc/daemonize/blob/master/daemon.c

int daemon(int nochdir, int noclose)
{
    int status = 0;

    openlog("daemonize", LOG_PID, LOG_DAEMON);

    /* Fork once to go into the background. */
    if((status = do_fork()) < 0 )
        ;

    /* Create new session */
    else if(setsid() < 0)               /* shouldn't fail */
        status = -1;

    /* Fork again to ensure that daemon never reacquires a control terminal. */
    else if((status = do_fork()) < 0 )
        ;

    else
    {
        /* clear any inherited umask(2) value */

        umask(0);

        /* We're there. */

        if(! nochdir)
        {
            /* Go to a neutral corner. */
            chdir("/");
        }

        if(! noclose)
            redirect_fds();
    }

    return status;
}

nginx中实现

https://github.com/nginx/nginx/blob/master/src/os/unix/ngx_daemon.c

ngx_int_t
ngx_daemon(ngx_log_t *log)
{
    int  fd;

    switch (fork()) {
    case -1:
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed");
        return NGX_ERROR;

    case 0:
        break;

    default:
        exit(0);
    }

    ngx_parent = ngx_pid;
    ngx_pid = ngx_getpid();

    if (setsid() == -1) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "setsid() failed");
        return NGX_ERROR;
    }

    umask(0);

    fd = open("/dev/null", O_RDWR);
    if (fd == -1) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
                      "open(\"/dev/null\") failed");
        return NGX_ERROR;
    }

    if (dup2(fd, STDIN_FILENO) == -1) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDIN) failed");
        return NGX_ERROR;
    }

    if (dup2(fd, STDOUT_FILENO) == -1) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDOUT) failed");
        return NGX_ERROR;
    }

#if 0
    if (dup2(fd, STDERR_FILENO) == -1) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDERR) failed");
        return NGX_ERROR;
    }
#endif

    if (fd > STDERR_FILENO) {
        if (close(fd) == -1) {
            ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() failed");
            return NGX_ERROR;
        }
    }

    return NGX_OK;
}

chatgpt生成

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

int daemon(int nochdir, int noclose)
{
    pid_t pid;

    // 创建子进程并退出父进程
    if ((pid = fork()) < 0) {
        return -1;
    } else if (pid != 0) {
        exit(0);
    }

    // 创建新的会话
    if (setsid() < 0) {
        return -1;
    }

    // 忽略 SIGHUP 信号
    signal(SIGHUP, SIG_IGN);

    // 再次创建子进程并退出父进程
    if ((pid = fork()) < 0) {
        return -1;
    } else if (pid != 0) {
        exit(0);
    }

    // 更改当前工作目录
    if (!nochdir) {
        if (chdir("/") < 0) {
            return -1;
        }
    }

    // 关闭文件描述符
    if (!noclose) {
        int fd;

        if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
            dup2(fd, STDIN_FILENO);
            dup2(fd, STDOUT_FILENO);
            dup2(fd, STDERR_FILENO);

            if (fd > STDERR_FILENO) {
                close(fd);
            }
        }
    }

    return 0;
}

daemon函数fork两次的原因

以上这些开源实现中,有的fork一次,有的fork两次,不过fork一次的多一些,对于这个问题我还查了不少资料

http://www.cppblog.com/justin-shi/archive/2014/10/10/208530.html
https://blog.csdn.net/shaoye_csdn1/article/details/94599271
https://m.xp.cn/b.php/75000.html

关于为什么要fork两次,我找到了上面几篇相关的解释,总结如下:

第一次fork使父进程终止,让子进程中进入后台执行,同时保证了会话id与当前子进程id不同,可以调用setsid函数创建新的会话,保证了子进程是会话组长(sid==pid),也是进程组组长(pgid == pid)。

第二次fork目的是防止进程再次打开一个控制终端,因为打开一个控制终端的前提条件是该进程必须是会话组长,再fork一次可以保证子进程pid != sid(sid是进程父进程的sid),所以也无法打开新的控制终端。

关于第一次fork我是理解的,第二次fork我也做过实验,无论fork一次还是两次,都可以再次打开新的终端,也就是说即使保证了pid != sid,一样可以打开新的终端,而有很多daemon函数的开源的实现中也只是调用了一次fork,包括我使用的Ubuntu20.04实验结果也是调用了一次fork,所以调用两次fork的原因成谜。

https://www.cntofu.com/book/46/linux_system/153.md

而这篇文章总结了两次fork技巧,不过和我们本文中所说的daemon两次fork前提不一样,在父进程不退出时会使用两次fork技巧避免出现僵尸进程,但时在daemon函数中fork只之后的父进程都是直接退出的,所以应该不是这个原因。

关于这个疑问我还专门去ChatGPT上问了一下,它给的答案如下:

在早期的Unix系统中,使用 daemon() 函数通常需要调用两次 fork() 函数,以确保子进程成为孤儿进程(orphan process),即与任何终端无关联,并且不会收到终端信号。孤儿进程通常是由父进程创建的子进程,但父进程已经退出或被杀死。

以下是调用两次 fork() 函数的工作原理:

  1. 调用第一次 fork() 函数:创建子进程1。父进程退出,子进程1成为孤儿进程,也就是没有父进程的进程。此时,子进程1仍然与原来的控制终端相关联。

  2. 子进程1调用 setsid() 函数创建一个新的会话,并成为新会话的领头进程和新进程组的组长进程。由于孤儿进程已经成为新会话的领头进程,因此它无法再重新关联到终端。此时,子进程1仍然是当前进程组的组长进程,并且与新的控制终端相关联。

  3. 调用第二次 fork() 函数:创建子进程2。由于子进程1是新会话的领头进程,因此子进程2不再与任何控制终端相关联。此时,子进程2成为孤儿进程,并与新会话和新进程组分离。

  4. 子进程2关闭不需要的文件描述符,改变工作目录和文件掩码,最后运行守护进程。

需要注意的是,在现代的Linux系统中,使用 daemon() 函数只需要调用一次 fork() 函数和一些必要的操作(如 setsid()、关闭文件描述符等),就可以将进程变成守护进程,而不需要调用两次 fork() 函数。

自己做实验的代码

自己在测试 daemon 函数的时候写了一些代码,记录一下方便再次拿来用,请原谅我c/c++混用,为了验证某个问题我已经各种尝试了。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <limits.h>
#include <fcntl.h> //open O_RDWR
#include <sys/stat.h> //umask
#include <iostream>

int my_daemon (int nochdir, int noclose)
{
  pid_t pid;

  /* Fork and exit parent process.  */
  if ((pid = fork ()) < 0)
    return -1;
  else if (pid != 0)
    _exit (0);

  /* Detach ourselves from the parent process group.  */
  if (setsid () < 0)
    return -1;

  /* Fork again to ensure we are not session leader.  */
  if ((pid = fork ()) < 0)
    return -1;
  else if (pid != 0)
    _exit (0);

  /* Change the file mode mask.  */
  umask (022);

  /* If nochdir is zero, change to the root directory.  */
  if (!nochdir)
    chdir ("/");

  /* If noclose is zero, redirect standard input, standard output,
     and standard error to /dev/null.  */
  if (!noclose)
    {
      int fd;

      fd = open ("/dev/null", O_RDWR, 0);
      if (fd != -1)
        {
          dup2 (fd, STDIN_FILENO);
          dup2 (fd, STDOUT_FILENO);
          dup2 (fd, STDERR_FILENO);
          if (fd > 2)
            close (fd);
        }
    }

  return 0;
}

void showpidinfo(const char* info)
{
    std::cout << info << "\n"
        << "getpid=" << getpid() << "\n"
        << "getppid=" << getppid() << "\n"
        << "getpgrp=" << getpgrp() << "\n"
        << "getpgid(pid)=" << getpgid(getpid()) << "\n"
        << "getsid(pid)=" << getsid(getpid()) << "\n"
        << "getuid=" << getuid() << "\n"
        << "geteuid=" << geteuid() << "\n"
        << "getgid=" << getgid() << "\n"
        << "getegid=" << getegid() << "\n"
        << "ttyname(0)=" << ttyname(0) << "\n"
        << std::endl;
}

void open_new_terminal()
{
    system("gnome-terminal -- /bin/bash -c 'date; exec bash'");
    //system("konsole  -e './v'");
    //system("xterm   -e './v'");
};

int main(int argc, char *argv[])
{
    char strCurPath[PATH_MAX];

    showpidinfo("===before daemon:");

    //while(1);

    int n = daemon(0, 1);
    //int n = my_daemon(0, 1);

    open_new_terminal();

    printf("daemon result %d\n", n);
    showpidinfo("===after daemon:");

    if(n < 0)
    {
        perror("error daemon.../n");
        exit(1);
    }

    sleep(2);

    if(getcwd(strCurPath, PATH_MAX) == NULL)
    {
        perror("error getcwd");
        exit(1);
    }
    printf("cur path [%s]\n", strCurPath);  //打印当前目录
    return 0;
}
  • showpidinfo 函数用于输出进程id相关信息
  • open_new_terminal 函数用于打开一个新的控制台
  • my_daemon 函数是自定义的daemon实现,采用fork两次的形式
  • perror 可以输出详细的报错信息
# demo @ ThinkPad-X390 in ~/cpp/daemontest [23:30:53]
$ g++ a.cpp
$ ./a.out
===before daemon:
getpid=352638
getppid=62351
getpgrp=352638
getpgid(pid)=352638
getsid(pid)=62351
getuid=1000
geteuid=1000
getgid=1000
getegid=1000
ttyname(0)=/dev/pts/15
# demo @ ThinkPad-X390 in ~/cpp/daemontest [23:30:57]
$ Unable to init server: Could not connect: Connection refused
# Failed to parse arguments: Cannot open display:
daemon result 0
===after daemon:
getpid=352639
getppid=1
getpgrp=352639
getpgid(pid)=352639
getsid(pid)=352639
getuid=1000
geteuid=1000
getgid=1000
getegid=1000
ttyname(0)=/dev/pts/15

cur path [/]

试验结果不论是fork一次还是两次,都可以打开新的控制台,这与网络上很多文章是相悖的,真相还需继续检验。运行结果中的报错"Unable to init server: Could not connect: Connection refused" 是因为我远程连接无法打开GUI。

总结

  • 想实现后台守护进程,只需添加头文件 #include <unistd.h>,然后在调用 daemon(1, 0)函数即可
  • daemon 的开源实现有很多种,早期很多说是fork两次,但是查了很多最新的版本都是fork一次
  • c/c++代码中打开控制台可以使用 system("gnome-terminal -- /bin/bash -c 'date; exec bash'"); 前提是可以正常打开用户界面的电脑,远程操作是不可以的
==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

除了死亡,都是擦伤~

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AlbertS

常来“玩”啊~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值