Linux 守护进程原理和创建

Posted by KalosAner on February 5, 2025

一、引言

定义与特性

守护进程也就是通常说的 Daemon 进程,守护进程会不断地运行提供服务,类似于 Windows 上的系统服务。它有以下特性:

独立运行:启动后常驻内存,不受终端控制。

独立于用户:通常由 root 用户运行,不受普通用户影响。

提供服务:守护进程通常会持续提供服务,如监控端口。

通常由系统启动:系统启动时通常会启动所有的守护进程。

生命周期:常常在系统启动时就开始运行,直到系统关闭时才终止。

守护进程是一种特殊进程,它可以由普通进程按照上述特性改造成守护进程。在终端中输入 ps axj a 可以查看守护进程。

通过阅读创建守护进程的源码可能更能体会守护进程的特性。

常见的守护进程

sshd:SSH 服务器守护进程,监控 22 端口,提供远程登录服务。

cron:定时任务调度守护进程。

systemd-journald:管理日志的守护进程。

nginxhttpd:Web 服务器守护进程。

特殊的守护进程

有一个特殊守护进程称作超级守护进程,指在操作系统中,由一个统一的守护进程(如 xinetd)负责管理和调度其他网络服务的机制。这种设计旨在减少系统资源占用,提高管理效率。

它有以下特性:

统一监听:超级守护进程在系统启动时运行,统一监听多个服务端口。

按需启动:当某个端口收到请求时,超级守护进程根据配置,启动相应的服务进程来处理该请求。

资源优化:最初只有超级守护进程占有系统资源,未被请求的服务不会占用系统资源,只有在需要时才启动,降低了系统开销。

xinetd 是典型的超级守护进程,但在现代 Linux 系统中,systemd 已逐渐取代 xinetd,成为新的系统和服务管理器,但 xinetd 仍在某些场景下被广泛使用。

平时在安装 dockersshmysql等软件时都需要启动一个服务对相应的端口进行监测,使用的就是 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("/") 将工作目录设置为根目录,也可以修改为其他合适的目录。

如果不设置工作目录可能会造成以下影响:

  1. 如果父进程的工作目录位于某个可卸载的文件系统中(例如,网络挂载目录或可移动存储设备),当该文件系统被卸载时,守护进程可能会受到影响。
  2. 如果守护进程的当前工作目录位于某个需要删除的目录中,那么当守护进程运行的时候,该目录将无法被删除。
  3. 守护进程通常以高权限运行,如果其工作目录位于用户目录下,可能会带来安全隐患,例如意外修改或访问用户的敏感文件。将工作目录设置为根目录,可以减少对特定用户目录的依赖,降低安全风险。

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信号处理,达到进程的正常退出。

大概流程图为:

Snipaste_2025-02-21_16-28-49

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