引言
所谓守护进程(daemon),是不具备控制终端的,常常是在系统引导装入时启动,仅在系统关闭时才终止,输出的报告信息一般在日志文件。
守护进程没有控制终端通常源于它们由系统初始化脚本(centos下如用chkconfig命令设置开机启动);然后守护进程也可以从某个终端由用户在shell提示符下键入命令启动,这种情况下,守护进程必须亲自脱离与控制终端的关联。
相关知识
因为守护进程是不具备控制终端的, 那么如何创建没有控制终端的进程呢?在开始编写守护进程(daemon)前,先了解一些相关知识。
注意,这里只讨论具备作业控制的linux系统、shell,使用GUN Bourne-again shell均支持作业控制(ubuntu \ centos等均支持作业控制);相关概念:进程、进程组、shell、控制终端、前台进程组、后台进程组、会话首进程、控制进程。
图.1 作业控制示意图
当我们在xwindow打开一个控制终端时,用ps命令查看:
$ ps -o pid,ppid,pgid,sid,tpgid,comm当我们打开一个控制终端时,shell就是作业控制的会话首进程(控制进程),在shell环境执行ps命令时,进程关系如下图:
PID PPID PGID SID TPGID COMMAND
4098 4092 4098 4098 4113 bash
4113 4098 4113 4098 4113 ps
图.2 进程关系
可以看到shell会fork一个子进程来执行ps,由于ps不是以后台方式运行(ps &),所以此时的TPGID是等于ps进程(进程组组长ID),也就是控制终端的会话首进程就等于ps进程id了;另外,你会发现这个会话id(SID)是不变的,因为这个会话有控制终端,所以shell的子进程不用额外open 设备(/dev/tty)即可获得终端;此时shell不拥有控制终端,直到ps进程结束才回收控制终端的所有权。
如果我们以后台形式执行ps:
$ ps -o pid,ppid,pgid,sid,tpgid,comm &此时,ps进程不拥有控制终端,而是shell拥有(TPGID此时等于shell进程组id)。 那为什么ps还是能够输出到终端呢?那是因为作业控制的实现允许后台进程组的进程输出到该会话所拥有的控制终端,但是后台进程试图从控制终端输入就会发生错误,这是控制终端驱动程序的默认行为;其实,可以修改作业控制,这里不展开 ; 会话首进程(前台进程组组长ID)是控制终端的一个属性。
[1] 4285
$ PID PPID PGID SID TPGID COMMAND
4098 4092 4098 4098 4098 bash
4285 4098 4285 4098 4098 ps
[1]+ 已完成 ps -o pid,ppid,pgid,sid,tpgid,comm
好了,了解了这些相关知识后,开始学习如何创建守护进程。
创建守护进程
① 文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。
② fork,然后父进程exit。保证了子进程不是进程组组长。(因为进程组组长还是可以通过open /dev/tty来获取一个控制终端)
③ 调用setuid创建一个新会话。此时,调用进程成为新会话的首进程,成为一个新进程组的组长进程;这个会话没有控制终端。(注意,为了使会话没有控制终端而创建了新会话,但又违反了第2点约定!!另外,为什么需要setuid呢?因为子进程全盘拷贝了父进程的会话期、进程组ID、控制终端,所以通过setuid将这些值重置为空的意思)
④ 再调用fork,父进程exit。(其实这步不要也可以,不过就像第2点说的,有此进程有获取终端的权限;所以,通过再次fork,这个子进程就是一个没有控制终端的会话的首进程的子进程,所以永远不可能再能取得控制终端)
⑤ 将当前目录更改为根目录。
⑥ 关闭不必要的文件描述符。
⑦ 某些守护进程打开/dev/null垃圾箱,使其具有文件描述符0\1\2(stdin\stdout\stderr)。
创建守护进程的函数mydaemon如下:
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <sys/stat.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
void mydaemon(const char *cmd)
{
int i,fd0,fd1,fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;
umask(0);
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
{
write(STDERR_FILENO, "getrlimit error\n", 16);
exit(-1);
}
if ((pid = fork()) < 0)
{
write(STDERR_FILENO, "fork error\n", 11);
exit(-1);
}else if (pid != 0)
exit(0);
setsid();
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0)
{
write(STDERR_FILENO, "sigaction error\n", 16);
exit(-1);
}
if ((pid = fork()) < 0)
{
write(STDERR_FILENO, "fork error\n", 11);
exit(-1);
}else if (pid != 0)
exit(0);
if (chdir("/") < 0)
{
write(STDERR_FILENO, "chdir error\n", 11);
exit(-1);
}
if (rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024;
for (i = 0; i < rl.rlim_max; ++i)
close(i);
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2)
{
syslog(LOG_ERR, "unexpected file %d %d %d\n", fd0, fd1, fd2);
exit(-1);
}
}
already_running可用于鉴定是否已经运行了守护进程,通过文件锁达到目标。
#define LOCKFILE "/var/run/mydaemon.pid"
#define LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)
extern int lockfile(int);
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\n", 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\n", LOCKFILE, strerror(errno));
exit(1);
}
ftruncate(fd, 0);
sprintf(buf, "%ld", (long)getpid());
write(fd, buf, strlen(buf) + 1);
return 0;
}
RereadConf 为更新配置文件所要求的配置,测试。
void RereadConf(void)
{
}
void SigProcSigterm(int sig)
{
syslog(LOG_INFO, "got SIGTERM; exiting");
exit(0);
}
void SigProcSighup(int sig)
{
syslog(LOG_INFO, "Re-reading configuration file");
RereadConf();
}
int main(int argc, char *argv[])
{
char *cmd;
struct sigaction sa;
if ((cmd = strchr(argv[0], '/')) == NULL)
cmd = argv[0];
else
cmd++;
mydaemon(cmd);
if (already_running() < 0)
{
syslog(LOG_ERR, "mydaemon already running");
exit(-1);
}
sa.sa_handler = SigProcSigterm;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGHUP);
sa.sa_flags = 0;
if (sigaction(SIGTERM, &sa, NULL) < 0)
{
syslog(LOG_ERR, "can't catch SIGTERM:%s", strerror(errno));
exit(-1);
}
sa.sa_handler = SigProcSighup;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGTERM);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0)
{
syslog(LOG_ERR, "can't catch SIGHUP:%s", strerror(errno));
exit(-1);
}
/*
* 开始进程守护进程需要的工作 ......
*/
sleep(100);
return 0;
}
注意,上面有两个信号处理函数并注册了,下面我们通过发送信号到进程,让守护进程产生日志信息。
守护进程的信息维护
通过syslog函数,我们的日志信息会通过unix域套接字发送到rsyslogd守护进程,此进程再帮我们记录下来。
运行mydaemon:
$ ./mydaemon运行daemon,并查看它的进程。咦,奇怪了,怎么没有这个进程,怎么回事?怎么办?
$ ps aux | grep mydaemon
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
查看rsyslogd守护进程的日志信息:
$tail -n -20 /var/log/syslog // 查看最后的20条日志信息
...
Apr 6 11:59:40 William mydaemon: can't open /var/run/mydaemon.pid : Permission denied
...
哦,原来是权限的问题。好,以root方式运行:
# ps aux | grep mydaemon
root 9130 0.0 0.0 4224 88 ? S 12:04 0:00 ./mydaemon
root 9317 0.0 0.0 11192 2172 pts/0 S+ 12:06 0:00 grep --color=auto mydaemon
哪个问号代表没有控制终端的意思 。
发送SIGHUP信号给守护进程:
# kill -SIGHUP 9130查看日志信息:
# tail -n -5 /var/log/syslog看,更新配置文件成功的日志信息输出来了。
......
Apr 6 12:07:49 William mydaemon: Re-reading configuration file
......
这就是守护进程。
守护进程的使用方向
守护进程一般用在服务器,应该是服务器一般都是守护进程,你不可能24小时对着终端看信息吧,而输出到日志文件随时查看,万事无忧;守护进程也会设置开机启动啦。