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

前言

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

daemon函数说明

1
2
3
4
5
/* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#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++混用,为了验证某个问题我已经各种尝试了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#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 可以输出详细的报错信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 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'"); 前提是可以正常打开用户界面的电脑,远程操作是不可以的
==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

除了死亡,都是擦伤~

2023-5-12 22:38:40

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客