APUE - 守护进程
编程规则
在编写守护进程程序时需遵循一些基本规则,以便防止产生并不需要的交互作用。
- 首先要做的是调用 umask 将文件模式创建屏蔽字设置为 0。由继承得来的文件模式创建屏蔽字可能会拒绝某些权限。
- 调用 fork,然后使父进程退出。这样做实现了下面几点:第一,如果该守护进程是作为一条简单 shell 命令启动的,那么父进程终止使得 shell 认为这条命令已经执行完毕;第二,子进程继承了父进程的进程组 ID,但具有一个新的进程 ID,这就保证了子进程不是一个进程组的组长进程。这对于下面就要做的 setsid 调用是必要的前提条件。
- 调用 setsid 以创建一个新会话。使调用进程:(a)成为新会话的首进程,(b)成为一个新进程组的组长进程,(c)没有控制终端。
- 将当前工作目录更改为根目录。从父进程处继承过来的当前工作目录可能在一个装配文件系统中。因为守护进程通常在系统再引导之前一直存在的,所以如果守护进程的当前工作目录在一个装配文件系统中,那么该文件系统就不能被拆卸。
- 关闭不再需要的文件描述符。这使守护进程不再持有从其父进程继承来的某些文件描述符。
- 某些守护进程打开 /dev/null 使其具有文件描述符 0、1 和 2,这样,任何一个试图读标准输入、写标准输出或标准出错的库例程都不会产生任何效果。
初始化一个守护进程
void daemonize(const char *cmd)
{
int i, fd0, fd1, fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;
/*
* Clear file creation mask.
*/
umask(0);
/*
* Get maximum number of file descriptors.
*/
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
err_quit("%s: can't get file limit", cmd);
/*
* Become a session leader to lose controlling TTY.
*/
if ( (pid = fork() ) < 0)
err_quit("%s: can't fork", cmd);
else if (pid != 0) /* parent */
exit(0);
setsid();
/*
* Ensure future opens won't allocate controlling TTYs.
*/
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0)
err_quit("%s: can't ignore SIGHUP", cmd);
if ( (pid = fork() ) < 0)
err_quit("%s: can't fork", cmd);
else if (pid != 0) /* parent */
exit(0);
/*
* Change the current working directory to the root so
* we won't prevent file systems from being unmounted.
*/
if (chdir("/") < 0)
err_quit("%s: can't change directory to /", cmd);
/*
* Close all open file descriptors.
*/
if (rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024;
for (i = 0; i < rl.rlim_max; i++)
close(i);
/*
* Attach file descriptors 0, 1, and 2 to /dev/null.
*/
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
/*
* Initialize the 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);
}
}
出错记录
#include <syslog.h>
void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ... );
void closelog(void);
int setlogmask(int maskpri);
#include <syslog.h>
#include <stdarg.h>
void vsyslog(int priority, const char *format, va_list arg);
单实例守护进程
#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)
int lockfile(int fd)
{
struct flock fl;
fl.l_type = F_WRLCK;
fl.l_start = 0;
fl.l_whence = SEEK_SET;
fl.l_len = 0;
return fcntl(fd, F_SETLK, &fl);
}
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) {
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;
}
守护进程的每个副本都将试图创建一个文件,并将其进程 ID 写到该文件中。这使管理人员易于标识该进程。如果该文件已经加了锁,那么 lockfile 函数将失败,errno 设置为 EACCES 或 EAGAIN,函数返回 1,这表明该守护进程已经运行。否则将文件长度截短为 0,将进程 ID 写入该文件,函数返回 0。
守护进程的惯例
在 UNIX 系统中,守护进程遵循下列公共惯例:
- 若守护进程使用锁文件,那么该文件通常存放在 /var/run 目录中。锁文件的名字通常是 name.pid,其中,name 是该守护进程或服务的名字。
- 若守护进程支持配置选项,那么配置文件通常存放在 /etc 目录中。配置文件的名字通常是 name.conf,其中,name 是该守护进程或服务的名字。
- 守护进程可用命令行启动,但通常它们是由系统初始化脚本之一启动的。如果在守护进程终止时,应当自动地重新启动它,则我们可在 /etc/inittab 中为该守护进程包括 _respawn 记录项,这样,init 就将重启动该守护进程。
- 若一守护进程有一配置文件,那么当该守护进程启动时,它读该文件,但在此之后一般就不会再查看它。若一管理员更改了配置文件,那么该守护进程可能需要被停止,然后再启动,以使配置文件的更改生效。为避免此种麻烦,某些守护进程将捕捉 SIGHUP 信号,当它们接收到该信号时,重读配置文件。因为守护进程并不与终端相结合,它们或者是无控制终端的会话首进程,或者是孤儿进程组的成员,所以守护进程并不期望接收 SIGHUP。于是,它们可以安全地重复使用它。