Zach的博客

守护进程

守护进程

守护进程(daemon)是生存期长的一种进程。它们常常在系统引导装入时启动,在系统关闭时才终止。它们没有控制终端,所以它们一直在后台运行。UNIX系统中有很多守护进程,它们执行日常事务活动。

编写守护进程

  1. 首先调用umask设置文件模式创建屏蔽字。如果守护进程要创建文件,那么它可能要设定特定的权限,继承而来的文件模式创建屏蔽字可能会屏蔽某些权限。一般调用为umask(0)
  2. 调用fork保证守护进程不是一个进程的组长进程,同时获得一个新的进程ID。
  3. 调用setsid创建一个会话
  4. 再次调用fork以保证当前进程不是会话的首进程,以防止日后打开一个终端时获得一个控制终端。
  5. 更改当前工作目录为根目录。这样可以防止某些文件系统不能被卸载。
  6. 关闭不需要的文件系统,通知把标准输入、标准输出和标准错误重定向到/def/null,这样可以防止某些利用这些描述符的库函数不会得不到描述符而出错。

一个将普通进程转换成守护进程的函数:

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
void daemonize(const char *cmd) {
int fd0, fd1, fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;

umask(0);

if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
perror(" getrlimit error");
exit(-1);
}

// become a session leader to lose controlling TTY
if ((pid = fork()) < 0) {
perror("fork error");
exit(-1);
} else if (pid > 0) {
/* parent */
exit(0);
}
setsid();

/* fork again, ensure future opens won;t allocate controlling TTY */
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0) {
fprintf(stderr, "%s: can't ignore SIGHUP", cmd);
perror("");
exit(-1);
}

if ((pid = fork()) < 0) {
perror("fork error");
exit(-1);
} else if (pid > 0) {
exit(0);
}

/* Change the current working directory to the root
so we won;t prevent file systems from being unmounted.
*/

if (chdir("/") < 0) {
perror("chdir error");
exit(-1);
}

/* close all open file descriptors */
if (rl.rlim_max == RLIM_INFINITY) {
rl.rlim_max = 1024;
}
for (int i=0; i<rl.rlim_max; i++) {
close(i);
}

/* attach stdin, stdout, stderr to /dev/null */
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);

/* Initialize log file */
openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
exit(1);
}
}

syslog

守护进程的一个问题是如何处理出错信息,因为它没有控制终端,所以不能简单的写到标准错误上。可以利用syslog来记录守护进程的错误信息。syslog的组织结构如下:

syslog.png

有以下三种产生日志消息的方法:

  1. 内核例程调用log函数。
  2. 大多数用户进程(守护进程)调用syslog函数产生日志消息
  3. 无论一个用户进程是否在此主机上,都可将日志消息发向UDP端口514.

syslog的接口为:

1
2
3
4
5
6
7
8
9
#include <syslog.h>

void openlog(const char *ident, int option, int facility);

void syslog(int priority, const char *fmt, ...);

viud closelog(void);

int setlogmask(int maskpri);

文件锁和单实例守护进程

fcntl函数可以用来给一整个文件或者是文件的部分区域加建议锁。文件锁分为shared lockexeclusive lock,一个文件可以加多个shared lock,就如同可以有多个读者一样,但是一个文件只能加一个exclusive loc,而且文件要么加shared lock要么加exclusive lock,不能同时加两个锁。

在设置文件锁时,需要传入flock结构的指针。

1
2
3
4
5
6
7
8
9
10
11
12
struct flock {
...
short l_type; /* Type of lock: F_RDLCK,
F_WRLCK, F_UNLCK */

short l_whence; /* How to interpret l_start:
SEEK_SET, SEEK_CUR, SEEK_END */

off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock
(F_GETLK only) */

...
};

需要说明的是,如果l_len被设置成0,那么从l_whencel_start指定的位置开始到文件的末尾都被加上了文件锁。

fcntl有三种和文件锁有关的操作:

  1. F_GETLK:flock参数表示调用者这时候想要加上的锁,如果锁能被加上,那么fcntl并没有实际上锁,它将l_type置为F_UNLCK。如果不能上锁,那么就把文件锁的相关信息填入flock参数中。
  2. F_SETLK:给文件设置锁,如果另外一个进程已经持有锁而使得当前进程无法获得锁,返回-1。
  3. F_SETLKW:和F_SETLCK一样,但是当锁无法获得时,调用进程被阻塞,直至锁可用。

需要注意一点是,当对应的文件被关闭时,其上的锁被自动释放,子进程也无法继承父进程的锁。当拥有锁的进程终止时,锁也被自动释放。

flock接口也可以获得文件锁(另外一种文件锁,不一定和上文的文件锁兼容),不同的是,子进程可以继承父进程的文件锁,但是子进程得到的只是文件锁的引用,即如果子进程释放文件锁,那么父进程的文件锁也被释放。具体可以参看flock(2)的manual。

我们利用fcntl函数设置文件锁,只有当前运行的守护进程拥有文件锁,之后尝试启动的守护进程都将无法得到该文件锁,从而无法启动,于是就可以达到单实例守护进程的目的。

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#include <syslog.h>
#include <signal.h>
#include <unistd.h>
#include <sys/resource.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>

#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

sigset_t mask;

void reread() {
// re-read configuration

}

int lockfile(int fd) {
struct flock fl;

fl.l_start = 0;
fl.l_len = 0;
fl.l_whence = SEEK_SET;
fl.l_type = F_WRLCK;

return fcntl(fd, F_SETLK, &fl);
}

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

umask(0);

if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
perror(" getrlimit error");
exit(-1);
}

// become a session leader to lose controlling TTY
if ((pid = fork()) < 0) {
perror("fork error");
exit(-1);
} else if (pid > 0) {
/* parent */
exit(0);
}
setsid();

/* fork again, ensure future opens won;t allocate controlling TTY */
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0) {
fprintf(stderr, "%s: can't ignore SIGHUP", cmd);
perror("");
exit(-1);
}

if ((pid = fork()) < 0) {
perror("fork error");
exit(-1);
} else if (pid > 0) {
exit(0);
}

/* Change the current working directory to the root
so we won;t prevent file systems from being unmounted.
*/

if (chdir("/") < 0) {
perror("chdir error");
exit(-1);
}

/* close all open file descriptors */
if (rl.rlim_max == RLIM_INFINITY) {
rl.rlim_max = 1024;
}
for (int i=0; i<rl.rlim_max; i++) {
close(i);
}

/* attach stdin, stdout, stderr to /dev/null */
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);

/* Initialize log file */
openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
exit(1);
}

}

int already_running(void) {
int fd;
char buf[16];

fd = open(LOCKFILE, O_RDWR | O_CREAT, LOCKMODE);
if (fd < 0) {
syslog(LOG_ERR, "can't open %s: %s", LOCKFILE, strerror(errno));
exit(1);
}

if (lockfile(fd) < 0) {
if (errno == EACCES || errno == EAGAIN) {
/* already has one daemon running */
close(fd);
return 1;
}
syslog(LOG_ERR, "can't lock %s: %s", LOCKFILE, strerror(errno));
exit(1);
}
ftruncate(fd, 0);
sprintf(buf, "%ld", (long)getpid());
write(fd, buf, strlen(buf) + 1);
return 0;
}

int main(int argc, char **argv) {

int err;
pthread_t tid;
char *cmd;
struct sigaction sa;

if ((cmd = strrchr(argv[0], '/')) == NULL)
cmd = argv[0];
else
cmd++;

daemonize(cmd);

if (already_running()) {
syslog(LOG_ERR, "daemon already running");
exit(1);
}

/* do daemon's work */
while (1);

exit(0);
}

在运行一次之后,我们尝试在此运行,查看/var/log/syslog得到:

1
Oct 23 17:00:35 localhost a.out: daemon already running

使用sigwait以及多线程处理信号

守护进程一般在接受到SIGHUP信号后重新读区其配置文件。这是因为守护进程没有控制终端,它永远不会收到来自终端的SIGHUP信号,那么就可以将SIGHUP信号重复使用,收到该信号就重新读取配置文件。

一个例子:

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#include <syslog.h>
#include <signal.h>
#include <unistd.h>
#include <sys/resource.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>

#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

sigset_t mask;

void reread() {
// re-read configuration

}

int lockfile(int fd) {
struct flock fl;

fl.l_start = 0;
fl.l_len = 0;
fl.l_whence = SEEK_SET;
fl.l_type = F_WRLCK;

return fcntl(fd, F_SETLK, &fl);
}

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

umask(0);

if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
perror(" getrlimit error");
exit(-1);
}

// become a session leader to lose controlling TTY
if ((pid = fork()) < 0) {
perror("fork error");
exit(-1);
} else if (pid > 0) {
/* parent */
exit(0);
}
setsid();

/* fork again, ensure future opens won;t allocate controlling TTY */
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0) {
fprintf(stderr, "%s: can't ignore SIGHUP", cmd);
perror("");
exit(-1);
}

if ((pid = fork()) < 0) {
perror("fork error");
exit(-1);
} else if (pid > 0) {
exit(0);
}

/* Change the current working directory to the root
so we won;t prevent file systems from being unmounted.
*/

if (chdir("/") < 0) {
perror("chdir error");
exit(-1);
}

/* close all open file descriptors */
if (rl.rlim_max == RLIM_INFINITY) {
rl.rlim_max = 1024;
}
for (int i=0; i<rl.rlim_max; i++) {
close(i);
}

/* attach stdin, stdout, stderr to /dev/null */
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);

/* Initialize log file */
openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
exit(1);
}

}

int already_running(void) {
int fd;
char buf[16];

fd = open(LOCKFILE, O_RDWR | O_CREAT, LOCKMODE);
if (fd < 0) {
syslog(LOG_ERR, "can't open %s: %s", LOCKFILE, strerror(errno));
exit(1);
}

if (lockfile(fd) < 0) {
if (errno == EACCES || errno == EAGAIN) {
/* already has one daemon running */
close(fd);
return 1;
}
syslog(LOG_ERR, "can't lock %s: %s", LOCKFILE, strerror(errno));
exit(1);
}
ftruncate(fd, 0);
sprintf(buf, "%ld", (long)getpid());
write(fd, buf, strlen(buf) + 1);
return 0;
}

void *thr_fn(void *arg) {
int err, signo;
for(;;) {
err = sigwait(&mask, &signo);
if (err != 0) {
syslog(LOG_ERR, "sigwait failed");
exit(1);
}

switch (signo) {
case SIGHUP: {
syslog(LOG_INFO, "Re-reading configuration file");
reread();
break;
}
case SIGTERM: {
syslog(LOG_INFO, "got SIGTERM; exiting");
exit(0);
}
default:
syslog(LOG_INFO, "unexpected signal %d\n", signo);
}
}

return 0;
}

int main(int argc, char **argv) {

int err;
pthread_t tid;
char *cmd;
struct sigaction sa;

if ((cmd = strrchr(argv[0], '/')) == NULL)
cmd = argv[0];
else
cmd++;

daemonize(cmd);

if (already_running()) {
syslog(LOG_ERR, "daemon already running");
exit(1);
}

/*
Restore SIGHUP default and block all signals
*/

sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0) {
syslog(LOG_ERR, "%s: can't restore SIGHUP default", cmd);
exit(1);
}
sigfillset(&mask);
if ((err = pthread_sigmask(SIG_BLOCK, &mask, NULL)) != 0) {
syslog(LOG_ERR, "SIG_BLOCK error");
exit(1);
}

err = pthread_create(&tid, NULL, thr_fn, 0);
if (err != 0) {
syslog(LOG_ERR, "can't create thread");
}

/* do daemon's work */
while (1);

exit(0);
}

我们将SIGHUP的处理函数恢复的到默认之后(否则进程忽略这个信号,sigwait永远不会见到它),阻塞所有信号,然后创建一个线程处理信号。该线程的唯一工作是等待SIGHUP和SIGTERM。当收到SIGHUP是就重新读取配置文件。