前言
daemon进程是后台守护进程,比如linux 下server都是daemon进程,像httpd、sshd等这些服务你肯定听说过,如果我们想写一个后台运行程序应该怎么做呢?其实非常容易,只要添加头文件 #include <unistd.h>
,然后在调用 daemon(1, 0)
函数即可。
daemon函数说明
1 | /* Put the program in the background, and dissociate from the controlling |
头文件中已经说得很清楚了,当 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 |
|
苹果源码
https://opensource.apple.com/source/Libc/Libc-167/gen.subproj/daemon.c.auto.html
1 |
|
glibc源码
https://elixir.bootlin.com/glibc/glibc-2.37/source/misc/daemon.c
1 |
|
glibc github地址
1 |
|
挺有意思的写法
1 | int daemon(int nochdir, int noclose) |
nginx中实现
https://github.com/nginx/nginx/blob/master/src/os/unix/ngx_daemon.c
1 | ngx_int_t |
chatgpt生成
1 |
|
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的原因成谜。
而这篇文章总结了两次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++混用,为了验证某个问题我已经各种尝试了。
1 |
|
showpidinfo
函数用于输出进程id相关信息open_new_terminal
函数用于打开一个新的控制台my_daemon
函数是自定义的daemon实现,采用fork两次的形式perror
可以输出详细的报错信息
1 | # demo @ ThinkPad-X390 in ~/cpp/daemontest [23:30:53] |
试验结果不论是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