(2009-10-22)
守护进程(daemon)是运行在后台,并且一直在运行的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程,Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。比如,作业规划进程crond、打印进程lpd等。
1 守护进程及其特性
守护进程最重要的特性是后台运行。其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩码等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,也可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。
总之,除了这些特殊性以外,守护进程与普通进程基本上没有什么区别。因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。如果读者对前面进程控制编程有比较深入的认识,就更容易理解和编写守护进程了。
请读者了解一下,在Linux环境下有哪些内核守护进程。通过ps-aux命令就可查看Linux环境下的守护进程了。
1)Init系统守护进程:它是进程1,负责启动各运行层次特定的系统服务。这些服务通常是在它们自己拥有的守护进程的帮助下实现的。
2)Keventd守护进程:为在内核中运行计划执行的函数提供进程上下文。
3)Kswapd守护进程:也称为页面调出守护进程。它将脏页面低速写到磁盘上,从而使这些页面在需要时仍可回收使用,这种方式支持虚存子系统。
4)bdflush和kupdated守护进程:Linux内核使用两个守护进程,即bdflush和kupdated,将调整缓存中的数据冲洗到磁盘上。当可用内存达到下限时,bdflush守护进程将脏缓冲区从缓冲池中冲洗到磁盘上,每隔一定时间间隔,kupdated守护进程将脏页面冲洗到磁盘上,以便在系统失效时减少丢失的数据。
5)portmap端口映射守护进程:提供将RPC(远程过程调用)程序号映射为网络端口号的服务。
6)syslogd守护进程:可由帮助操作人员把系统消息记入日志的任何程序使用。
7)inetd守护进程(xinetd):它侦听系统网络接口,以便取得来自网络的对各种网络服务进程的请求。
8)nfsd、lockd、rpciod守护进程:提供对网络文件系统(NFS) 的支持。
9)cron守护进程:在指定的日期和时间执行指定的命令。许多系统管理任务是由cron定期地执行相关程序而实现的。
10)cupsd守护进程:是打印假脱机进程,它处理对系统提出的所有打印请求。
Tips:大多数守护进程都以超级用户(用户ID为0)特权运行,并且在后台运行,其所对应的终端号显示为问号(?)。
2 编写守护进程的要点
(1)创建子进程,终止父进程
由于守护进程是脱离控制终端的,因此首先复制子进程,终止父进程,使得程序在shell终端里造成一个已经运行完毕的假象。之后所有的工作都在子进程中完成,而用户在shell终端里则可以执行其他的命令,从而使得程序以僵尸进程形式运行,在形式上做到了与控制终端的脱离。
创建子进程,终止父进程的代码如下:
pid=fork();
if(pid>0)
{exit(0);} /*终止父进程*/
(2)在子进程中创建新会话
这个步骤是创建守护进程中最重要的一步,虽然它的实现非常简单,在这里使用的是系统函数setsid。
setsid函数用于创建了一个新的会话,并担任该会话组的组长。调用setsid有下面三个作用:让进程摆脱原会话的控制、让进程摆脱原进程组的控制和让进程摆脱原控制终端的控制。
在调用fork函数时,子进程全盘拷贝父进程的会话期、进程组、控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,还不是真正意义上独立开来,而setsid函数能够使进程完全独立出来,从而脱离所有其他进程的控制。
(3)改变工作目录
使用fork创建的子进程也继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统不能卸载。因此,把当前工作目录换成其他的路径,如“/”或“/tmp”等。改变工作目录的常见函数是chdir。
(4)重设文件创建掩码
文件创建掩码是指屏蔽掉文件创建时的对应位。由于使用fork函数新建的子进程继承了父进程的文件创建掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件创建掩码设置为0,可以大大增强该守护进程的灵活性。设置文件创建掩码的函数是umask。在这里,通常的使用方法为umask(0)。
(5)关闭文件描述符
用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,而可能导致所在文件系统无法卸载。
通常按如下方式关闭文件描述符:
for(i=0;i<NOFILE;i++)
close(i);
或者也可以用如下方式:
for(i=0;i<MAXFILE;i++)
close(i);
Tips:父进程创建了子进程,而父进程退出之后,此时该子进程就变成了“孤儿进程”。在Linux中,每当系统发现一个孤儿进程,就会自动由1号进程(也就是init进程)收养它,原先的子进程就会变成init进程的子进程了。
3 守护进程的编写
根据第2节的编写要点就可以编制具体的守护进程程序了,具体例子可参考文后的实验二则。
Tips:在编写守护进程的调试过程中会发现,程序运行成为守护进程后,完全脱离控制终端,调试的时候没法像普通程序调试一样,在终端中看到错误信息,甚至用gdb也无法正常调试。在Linux系统中,编写调试守护进程,一般是利用系统的日志服务,通过系统守护进程syslogd控制自己编写的守护进程的告警信息。LinuxC语言中,只要调用syslog函数,将守护进程的出错信息写入“/var/log/messages”系统日志文件,就可以做到这一点。
--------------------------------------------------------------------------------------------------------------------------------------------------
【实验1】
编写一程序,要求运行后成为守护进程,复制守护进程的子进程,子进程往某个文件里写入字符串“测试守护进程”,守护进程的错误信息输出到系统日志文件“/var/log/messages”。请把产生守护进程的部分分割成独立的程序文件。
参考代码:
(1)init.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<syslog.h>
#include<signal.h>
#include<sys/param.h>
#include<sys/stat.h>
void init_daemon(void)
{
pid_t child0;
int i;
openlog("1-1程序信息",LOG_PID,LOG_DAEMON);
child0=fork();
if(child0>0)
exit(0);
else if(child0<0)
{
syslog(LOG_INFO,"创建守护进程失败");
exit(1);
}
setsid();
chdir("/tmp");
umask(0);
for(i=0;i< NOFILE;++i)
close(i);
return;
}
(2)1-1.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<syslog.h>
#include<signal.h>
#include<sys/param.h>
#include<sys/stat.h>
void init_daemon(void);
int main()
{
FILE *fp;
pid_t child;
init_daemon();
openlog("",LOG_PID,LOG_DAEMON);
child=fork();
if(child<0)
{
syslog(LOG_INFO,"创建子进程失败");
exit(1);
}
else if(child==0)
{
while(1)
{
sleep(5);
if((fp=fopen("test.txt","a+"))>0)
{
fprintf(fp,"测试守护进程\n");
fclose(fp);
}
else
syslog(LOG_INFO, "打开文件失败");
}
}
else
{
waitpid(child,NULL,0);
while(1)
{
sleep(10);
}
}
return 0;
}
(3)编译成可执行目标1-1:
gcc init.c 1-1.c -o 1-1
【实验2】
编写一程序,要求运行后成为守护进程,每隔5分钟修改一次本机的IP地址。所有的IP地址分行放在一个文本文件中,每隔5分钟随机读取一个,Linux C中可以用“ioctl”和“sysctl”函数实现,也可以调用系统命令“ifconfig”,例如“ifconfig eth0 192.168.0.20 netmask 255.255.255.0”。
参考代码:
1-2.c:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<time.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<syslog.h>
#include<signal.h>
#include<sys/param.h>
#include<sys/stat.h>
int main()
{
FILE *fp;
pid_t child;
int i,lines,line,move;
char str[20],command[100];
child=fork();
if(child>0)
exit(0);
else if(child<0)
{
perror("创建子进程失败");
exit(1);
}
setsid();
chdir("/tmp");
umask(0);
for(i=0;i< NOFILE;++i)
close(i);
fp=fopen("ip.txt","r");
if(fp<=0)
{
perror("打开文件失败");
exit(1);
}
lines=0;
while(fscanf(fp,"%s",&str)!=EOF)
{
lines++;
}
while(1)
{
fseek(fp,0,SEEK_SET);
srand((int)time(0));
line=rand()%lines+1;
move=0;
while(move<line)
{
fscanf(fp,"%s",&str);
move++;
}
strcpy(command,"ifconfig eth0 ");
strcat(command,str);
strcat(command," netmask 255.255.255.0");
system(command);
sleep(5*60);
}
fclose(fp);
return 0;
}