daemon进程是后台守护进程,比如linux 下server都是daemon进程,像httpd、sshd等这些服务你肯定听说过,如果我们想写一个后台运行程序应该怎么做呢?其实非常容易,只要添加头文件 #include
,然后在调用 daemon(1, 0)
函数即可。
/* 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函数的实现还是费了一些功夫的,我发现我根本不会在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);
}
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 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/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 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;
}
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;
}
#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;
}
以上这些开源实现中,有的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/15
cur path [/]
试验结果不论是fork一次还是两次,都可以打开新的控制台,这与网络上很多文章是相悖的,真相还需继续检验。运行结果中的报错"Unable to init server: Could not connect: Connection refused" 是因为我远程连接无法打开GUI。
#include
,然后在调用 daemon(1, 0)
函数即可daemon
的开源实现有很多种,早期很多说是fork两次,但是查了很多最新的版本都是fork一次system("gnome-terminal -- /bin/bash -c 'date; exec bash'");
前提是可以正常打开用户界面的电脑,远程操作是不可以的除了死亡,都是擦伤~