一、什么是守护进程
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进 程。 Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。 比如,作业规划进程crond,打印进程lpd等。
二、守护进程的特征
(1)守护进程是后台运行的。
(2)守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。
(3)守护进程的启动方式有其特殊之处。它可以在 Linux 系统启动时从启动脚本 /etc/rc.d 中启动,可以由作业规划进程 crond 启动,还可以由用户终端执行。
三、创建步骤
(1)调用 umask 将文件模式创建屏蔽字设置为已知值(一般是0)
由于使用fork函数新建的子进程继承了父进程的文件创建掩码,这就给该子进程使用文件带来了诸多的麻烦,因此把文件创建掩码设置为0,可以大大增强该守护进程的灵活性,设置文件创建掩码的函数是 umask,通常的使用方法为 umask(0)
(2)调用 fork,然后使父进程 exit
由于守护进程是脱离控制终端的,因此首先创建子进程,终止父进程,使得程序在shell终端里造成一个已经运行完毕的假象,之后所有的工作都在子进程中完成,而用户在 shell 终端里则可以执行其他的命令,从而使得程序以僵尸进程形式运行,在形式上做到了与控制终端的脱离
(3)调用 setsid 创建一个新的会话
setsid函数用于创建一个新的会话,并担任该会话组的组长。调用 setsid 有三个步骤:成为新会话的首进程;成为一个新会话的进程组长;没有控制终端
(4)将当前工作目标更改为根目录
使用fork创建的子进程也继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统不能卸载,因此,把当前工作目录换成其他的路径,如 “/” 或 “/tmp” 等,改变工作目录的常见函数是 chdir
(5)关闭不再需要的文件描述符
用fork新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,可能导致所在的文件系统无法卸载
(6)某些守护进程打开 dev/null,使其文件描述符具有 0,1,2
守护进程不与终端设备相关联,所以其输出无法显示,也无法从交互用户接收输入
四、代码实现
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/resource.h>
int CreateDaemon()
{
// 调用 umask 将文件模式创建屏蔽字设置为 0
umask(0);
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
perror("getrlimit failed!");
// 调用 fork,然后使父进程 exit
int pid;
if((pid = fork()) < 0 )
{
perror("fork");
return -1;
}
else if(pid > 0)
{
exit(0);
}
//设置新会话
if (setsid() == -1)
{
perror("setsid");
return -1;
}
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0)
{
perror("sigaction");
return -1;
}
if((pid = fork()) > 0)
{
exit(0);
}
else if(pid < 0)
{
perror("fork");
return -1;
}
//改变当前的工作目录
if ((chdir("/")) < 0)
{
perror("chdir");
return -1;
}
struct rlimit r1;
// 关闭所有打开的文件描述符
if (r1.rlim_max == RLIM_INFINITY)
{
r1.rlim_max = 1024;
}
setrlimit(RLIMIT_NOFILE, &rl);
for(size_t i = 0; i < r1.rlim_max; i++)
close(i);
// 重定向标准的3个文件描述符
int fd0 = open("/dev/null", O_RDWR);
int fd1 = dup(0);
int fd2 = dup(0);
if (fd0 != 0 || fd1 != 1 || fd2 != 2)
{
printf("fd0:%d, fd1:%d, fd2:%d\n", fd0, fd1, fd2);
return 1;
}
return 0;
}
int main()
{
CreateDaemon();
while(1);
return 0;
}
将程序编译为 a.out,运行后使用 ps -efj 查看(已过滤掉其他的),可得到
ch 1205 1 1204 1204 19 11:54 ? 00:00:14 ./a.out
五、日志
1. 产生日志的方法
(1)内核例程调用 log 函数
(2)大多数用户进程调用 syslog 函数来产生日志消息
(3)在本机和通过 TCP/IP 网络连接到此主机,可将日志消息发向 UDP 端口 514,syslog 不产生 UDP 数据报
2. 接口
#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 mask);
// 返回前日志记录优先级屏蔽字
(1)openlog --可选择
警告消息
ident:程序的名称,如cron,inetd
option:LOG_CONS,LOG_NDELAY,LOG_NOWAIT,LOG_ODELAY,LOG_PERROR,LOG_PID
facility:使配置文件使用不同的方式处理
(2)syslog --没有调用 openlog,第一次调用 syslog 调用 openlog
产生日志消息
priority:facility 与 level 的结合
format:替换为 errno 对应的 strerror
(3)setlogmask
设置进程记录的优先级屏蔽字
#include <stdarg.h>
void vsyslog(int priority, const char *format, va_list ap);
处理可变参数列表
六、单实例守护进程
如果守护进程需要访问一设备,而该设备驱动程序将阻止多次打开在/dev目录下的相应设备节点,那么这就达到了任何时刻只运行守护进程一个副本的要求。但是如果没有这种设备可供使用,那么我们就需要自行处理。文件锁和记录锁机制是一种方法的基础,该方法用来保证一个守护进程只有一个副本在运行。如果每一个守护进程创建一个文件,并且在整个文件上加上一把写锁,那就只允许创建一把这样的写锁,所以在此之后如试图再创建一把这样的写锁就将失败,以此向后续守护进程副本指明已有一个副本正在运行,在该守护进程终止时,这把写锁将被自动删除。这就简化了复原所需的处理,去除了对以前的守护进程实例需要进行清理的有关操作。
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <sys/stat.h>
#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)
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", 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。我们需要将文件长度截短为0,其原因是以前守护进程实例的进程 ID 字符串可能长于调用此函数的当前进程的进程 ID 字符串。例如,若以前的守护进程的进程 ID 是 12345,而新实例的进程 ID 是9999,那么将此进程 ID 写入文件后,在文件中留下的是 99995,将文件长度截短为0就解决了此问题。
七、守护进程的惯例
若守护进程使用锁文件,那么该文件通常存放在/var/run目录中。注意,守护进程可能需要具有超级用户权限才能在此目录下创建文件。锁文件的名字通常是 name.pid,其中,name是该守护进程或服务的名字。例如cron守护进程锁文件的名字是 /var/run/crond.pid。
4若守护进程支持配置选项,5那么配置文件通常存放在/etc目录中。配置文件的名字通常是name.conf,其中,name是该守护进程或服务的名字。例如,syslogd守护进程的配置文件是/etc/syslog.conf。
守护进程可用命令行启动,但通常它们是由系统初始化脚本之一(/etc/rc*或/etc/init.d/*)启动的。如果在守护进程终止时,应当自动地重新启动它,则我们可在/etc/inittab中为该守护进程包括_respawn记录项,这样,init就将重启动该守护进程。
若一守护进程有一配置文件,那么当该守护进程启动时,它读该文件,但在此之后一般就不会再查看它。若一管理员更改了配置文件,那么该守护进程可能需要被停止,然后再启动,以使配置文件的更改生效。为避免此种麻烦,某些守护进程将捕捉SIGHUP信号,当它们接收到该信号时,重读配置文件。因为守护进程并不与终端相结合,它们或者是无控制终端的会话首进程,或者是孤儿进程组的成员,所以守护进程并不期望接收SIGHUP。于是,它们可以安全地重复使用它。