linux环境下使用daemon函数实现守护进程
文章目录
- 前言
- daemon函数说明
- daemon函数的实现
- 安卓源码
- 苹果源码
- glibc源码
- glibc github地址
- 挺有意思的写法
- nginx中实现
- chatgpt生成
- daemon函数fork两次的原因
- 自己做实验的代码
- 总结
前言
daemon进程是后台守护进程,比如linux 下server都是daemon进程,像httpd、sshd等这些服务你肯定听说过,如果我们想写一个后台运行程序应该怎么做呢?其实非常容易,只要添加头文件 #include ,然后在调用 daemon(1, 0)函数即可。
daemon函数说明
/* Put the program in the background, and dissociate from the controllingterminal. 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
#include
#include
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
#include
#include 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
#include
#include
#include
#include #include
#include 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 nofunction 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
#include
#include
#include
#include #include
#include 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 nofunction 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 0if (dup2(fd, STDERR_FILENO) == -1) {ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDERR) failed");return NGX_ERROR;}
#endifif (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
#include
#include
#include
#include
#include 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()函数的工作原理:
调用第一次
fork()函数:创建子进程1。父进程退出,子进程1成为孤儿进程,也就是没有父进程的进程。此时,子进程1仍然与原来的控制终端相关联。子进程1调用
setsid()函数创建一个新的会话,并成为新会话的领头进程和新进程组的组长进程。由于孤儿进程已经成为新会话的领头进程,因此它无法再重新关联到终端。此时,子进程1仍然是当前进程组的组长进程,并且与新的控制终端相关联。调用第二次
fork()函数:创建子进程2。由于子进程1是新会话的领头进程,因此子进程2不再与任何控制终端相关联。此时,子进程2成为孤儿进程,并与新会话和新进程组分离。子进程2关闭不需要的文件描述符,改变工作目录和文件掩码,最后运行守护进程。
需要注意的是,在现代的Linux系统中,使用
daemon()函数只需要调用一次fork()函数和一些必要的操作(如setsid()、关闭文件描述符等),就可以将进程变成守护进程,而不需要调用两次fork()函数。
自己做实验的代码
自己在测试 daemon 函数的时候写了一些代码,记录一下方便再次拿来用,请原谅我c/c++混用,为了验证某个问题我已经各种尝试了。
#include
#include
#include
#include
#include //open O_RDWR
#include //umask
#include 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/15cur path [/]
试验结果不论是fork一次还是两次,都可以打开新的控制台,这与网络上很多文章是相悖的,真相还需继续检验。运行结果中的报错"Unable to init server: Could not connect: Connection refused" 是因为我远程连接无法打开GUI。
总结
- 想实现后台守护进程,只需添加头文件
#include,然后在调用daemon(1, 0)函数即可 daemon的开源实现有很多种,早期很多说是fork两次,但是查了很多最新的版本都是fork一次- c/c++代码中打开控制台可以使用
system("gnome-terminal -- /bin/bash -c 'date; exec bash'");前提是可以正常打开用户界面的电脑,远程操作是不可以的
除了死亡,都是擦伤~
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
