0%

C 迷你系列(七)创建守护进程

何为守护进程?

守护进程是一种在后台执行,而不由用户直接交互的电脑程序。此类程序会被以进程的形式初始化。守护进程程序的名字通常以字母 d 结尾,以指明这个进程实际是守护进程,如:syslogd,sshd 等。

编程原则?

创建守护进程分为以下几个步骤:

  1. 首先要做的就是调用 umask 函数,将文件模式创建屏蔽字设置为一个已知的值(通常为 0)。因为继承得来的文件模式创建屏蔽字可能会被设置为拒绝某些权限。如果守护进程要创建文件,那么它可能要设置特定的权限。例如:若守护进程要创建组可读、组可写的文件,继承的文件模式创建屏蔽字可能会屏蔽上述两种权限中的一种,而使其无法发挥作用。
    另一方面,如果守护进程调用库函数创建文件,那么将文件模式创建屏蔽字设置为一个限制性更强的值(如 007)可能会更明智,因为库函数可能不允许调用者通过一个显示的函数参数来设置权限。
  2. 调用 fork,然后使父进程 exit。这样做实现了以下几点:第一,如果该守护进程是作为一条简单的 shell 命令启动的,那么父进程终止会让 shell 认为这条命令已经执行完毕。第二,虽然子进程继承了父进程的进程组 ID,但获得了一个新的进程 ID,这就使得子进程不是一个进程组的组长进程。这是下面 setsid 调用的先决条件。
  3. 调用 setsid 创建一个新会话。这样使调用进程:a) 成为新会话的首进程,b) 成为一个新进程组的组长进程,c) 没有控制终端。

    在基于 System V 的系统中,有人建议在此时再次调用 fork,终止父进程,继续使用子进程中的守护进程。这就保证了该守护进程不是会话首进程,于是按照 System V 规则,可以防止它取得控制终端。为了避免取得控制终端的另一种方法是,打开一个终端设备时指定 O_NOCTTY。

  4. 将当前工作目录更改为根目录。从父进程继承来的当前目录可能在一个挂载的文件系统中。因为守护进程通常在系统再引导之前一直都存在,所以该文件系统将不能被卸载。
  5. 关闭不再需要的文件描述符。这使守护进程不再持有从父进程继承来的任何描述符(可以通过 open_max() 函数或 getrlimit() 函数获取拥有的最高文件描述符值,并关闭直到该值的所有描述符)。
  6. 某些守护进程打开 /dev/null 使其具有文件描述符 0、1 和 2。但是任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果。因为守护进程并不雨终端设备相关联,所以其输出无处显示,也无处从交互式用户那里接收输入。即使守护进程是从交互式会话启动的,但是守护进程是在后台运行的,所以登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们不希望在终端设备上见到守护进程的输出,用户也不希望他们在终端上的输入被守护进程读取。

Let’s begin

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
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/syslog.h>

void daemonize(const char *path)
{
int i, fd0, fd1, fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;

// 清除文件创建掩码
umask(0);
// 获取文件描述符最大编号
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
{
printf("%s: can't get file limit\n", path);
exit(EXIT_FAILURE);
}

// 创建子进程
if ((pid = fork()) < 0)
{
printf("%s: can't fork\n", path);
exit(EXIT_FAILURE);
} else if (pid != 0)
{
// 父进程退出
exit(EXIT_SUCCESS);
}
// 创建与一个新的会话
setsid();

sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;

if (sigaction(SIGHUP, &sa, NULL) < 0)
{
printf("%s: can't ignore SIGHUP\n", path);
exit(EXIT_FAILURE);
}
if ((pid = fork()) < 0)
{
printf("%s: can't fork again\n", path);
exit(EXIT_FAILURE);
} else if (pid != 0)
{
// 父进程退出
exit(EXIT_SUCCESS);
}

// 将当前工作目录改为 "/"
if (chdir("/") < 0)
{
printf("%s: can't change directory to /\n", path);
exit(EXIT_FAILURE);
}

// 关闭所有的文件描述符
if (rl.rlim_max == RLIM_INFINITY)
{
rl.rlim_max = 1024;
}
for (i = 0; i < rl.rlim_max; i++)
{
close(i);
}

// 附加文件描述符 0、1、2 到 /dev/null
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);

// 为了进行日志记录,需要初始化日志配置
openlog(path, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2)
{
syslog(LOG_ERR, "unexpected file descriptor %d %d %d", fd0, fd1, fd2);
exit(EXIT_FAILURE);
}
}


参考

  • 【UNIX 环境高级编程】