一、引言
定义与特性
守护进程也就是通常说的 Daemon 进程,守护进程会不断地运行提供服务,类似于 Windows 上的系统服务。它有以下特性:
独立运行:启动后常驻内存,不受终端控制。
独立于用户:通常由 root
用户运行,不受普通用户影响。
提供服务:守护进程通常会持续提供服务,如监控端口。
通常由系统启动:系统启动时通常会启动所有的守护进程。
生命周期:常常在系统启动时就开始运行,直到系统关闭时才终止。
守护进程是一种特殊进程,它可以由普通进程按照上述特性改造成守护进程。在终端中输入 ps axj a
可以查看守护进程。
通过阅读创建守护进程的源码可能更能体会守护进程的特性。
常见的守护进程
sshd
:SSH 服务器守护进程,监控 22 端口,提供远程登录服务。
cron
:定时任务调度守护进程。
systemd-journald
:管理日志的守护进程。
nginx
、httpd
:Web 服务器守护进程。
特殊的守护进程
有一个特殊守护进程称作超级守护进程,指在操作系统中,由一个统一的守护进程(如 xinetd
)负责管理和调度其他网络服务的机制。这种设计旨在减少系统资源占用,提高管理效率。
它有以下特性:
统一监听:超级守护进程在系统启动时运行,统一监听多个服务端口。
按需启动:当某个端口收到请求时,超级守护进程根据配置,启动相应的服务进程来处理该请求。
资源优化:最初只有超级守护进程占有系统资源,未被请求的服务不会占用系统资源,只有在需要时才启动,降低了系统开销。
xinetd 是典型的超级守护进程,但在现代 Linux 系统中,systemd
已逐渐取代 xinetd
,成为新的系统和服务管理器,但 xinetd
仍在某些场景下被广泛使用。
平时在安装 docker
、ssh
、mysql
等软件时都需要启动一个服务对相应的端口进行监测,使用的就是 systemd
对这些服务进行管理,例如:systemctl start docker
。
普通的守护进程也可以使用超级守护进程来启动,如:systemctl start mydaemon
。
二、创建守护进程
在制作守护进程之前需要了解会话、进程组和控制终端的概念。
2.1 创建子进程,父进程退出
这是编写守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在 Shell 终端里造成一程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在 Shell 终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离
在Linux中父进程先于子进程退出会造成子进程成为孤儿进程,而每当系统发现一个孤儿进程是,就会自动由1号进程(init
)收养它,这样,原先的子进程就会变成 init
进程的子进程。
2.2 在子进程中创建新会话
这个步骤是创建守护进程中最重要的一步,虽然它的实现非常简单,但它的意义却非常重大。在这里使用的是系统函数 setsid
。因为这个进程是由父进程创造的,所以它的 pid
不会是进程组 id
,所以可以直接调用 setsid
。
那么,在创建守护进程时为什么要调用 setsid
函数呢?由于创建守护进程的第一步调用了 fork
函数来创建子进程,再将父进程退出。由于在调用了 fork
函数时,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变。因此,还不是真正意义上的独立开来,而 setsid
函数能够使进程完全独立出来,从而摆脱其他进程的控制。
2.3 再次创建子进程
现在,进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端。守护进程的特性就是不受控制终端影响,所以为了避免守护进程重新获取终端需要再次 fork
一个子进程。该子进程不是会话首进程,该进程将不能重新打开控制终端。
2.4 设置工作目录
使用 fork
创建的子进程会继承父进程的工作目录。创建守护进程时通常会调用 chdir("/")
将工作目录设置为根目录,也可以修改为其他合适的目录。
如果不设置工作目录可能会造成以下影响:
- 如果父进程的工作目录位于某个可卸载的文件系统中(例如,网络挂载目录或可移动存储设备),当该文件系统被卸载时,守护进程可能会受到影响。
- 如果守护进程的当前工作目录位于某个需要删除的目录中,那么当守护进程运行的时候,该目录将无法被删除。
- 守护进程通常以高权限运行,如果其工作目录位于用户目录下,可能会带来安全隐患,例如意外修改或访问用户的敏感文件。将工作目录设置为根目录,可以减少对特定用户目录的依赖,降低安全风险。
2.5 提升文件权限
在创建守护进程时,通常会在子进程中调用 umask(0)
将文件权限掩码设置为 0。这样可以确保守护进程可以创建出具有足够高权限的文件和目录,但是当创建出文件和目录时要根据需要对权限再进行修改。
原因如下:
避免继承父进程的文件权限掩码:子进程会继承父进程的 umask
值。如果父进程的 umask
设置较为严格(例如,屏蔽了某些权限),守护进程可能会在创建文件或目录时受到限制,导致权限不足。通过将 umask
设置为 0,可以确保新创建的文件和目录不受父进程 umask
的影响。
确保文件和目录的最大权限:umask(0)
将文件权限掩码设置为 0,表示不屏蔽任何权限。这样,守护进程在创建文件或目录时,可以赋予它们最大的权限(如 0777),然后根据需要使用 chmod
等函数调整为合适的权限。
2.6 关闭不用的文件描述符
用 fork
函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。包括文件描述符为 0、1 和 2 的3个文件(常说的输入、输出和异常)已经失去了存在的价值,也应被关闭。
2.7 守护进程退出处理
当用户需要外部停止守护进程运行时,往往会使用 kill 命令停止该守护进程。所以,守护进程中需要编码来实现 kill 发出的signal信号处理,达到进程的正常退出。
大概流程图为:
2.8 deamon 函数
函数原型
1
2
#include <unistd.h>
int daemon(int nochdir, int noclose);
nochdir:切换工作目录到根目录,0表示切换,1表示不切换
noclose:重定向所有标准流到 /dev/null
,0表示重定向,1表示重定向。
用法就是直接放在程序的开头。
三、代码示例
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
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
static bool flag = true;
void create_daemon();
void handler(int);
int main()
{
time_t t;
int fd;
create_daemon();
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (sigaction(SIGQUIT, &act, NULL)) {
printf("sigaction error.\n");
exit(0);
}
while(flag)
{
fd = open("/home/kalosaner/daemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd == -1) {
printf("open error\n");
}
t = time(0);
char *buf = asctime(localtime(&t));
write(fd, buf, strlen(buf));
close(fd);
sleep(60);
}
return 0;
}
void handler(int sig)
{
printf("I got a signal %d\nI'm quitting.\n", sig);
flag = false;
}
void create_daemon()
{
pid_t pid;
/*(1)-----创建一个进程来用作守护进程-----*/
pid = fork();
/*(1.1)-----------原父进程退出-------------*/
if (pid == -1) {
printf("fork error\n");
exit(1);
} else if (pid != 0) {
exit(0);
}
/*(2)---setsid使子进程独立。摆脱会话控制、摆脱原进程组控制、摆脱终端控制----*/
if (-1 == setsid()) {
printf("setsid error\n");
exit(1);
}
/*(3)---通过再次创建子进程结束当前进程,使进程不再是会话首进程来禁止进程重新打开控制终端----*/
pid = fork();
if (pid == -1) {
printf("fork error\n");
exit(1);
} else if (pid != 0) {
exit(0);
}
/*(4)---子进程中调用chdir()让根目录成为子进程工作目录----*/
chdir("/");
int i;
/*(6)---关闭文件描述符(常说的输入,输出,报错3个文件)----*/
for (i = 0; i < 3; ++i) {
close(i);
}
/*(5)---重设文件掩码为0(将权限全部开放)----*/
umask(0);
return;
}
退出守护进程只需要向守护进程发送 SIGQUIT
信号即可
1
2
ps -ef | grep 'daemon'
sudo kill -3 xxx