以前我们在看《unix环境高级编程》的时候,有专门的整章详细介绍如何编写一个后台daemon程序(精灵程序),主要涉及到创建会话组,切换工 作目录,设置文件屏蔽字,关闭不必要的描述符等多个操作。这些操作对于每一个后台程序来说都是类似的。
在Linux中专门提供了一个函数来完成这个daemon化的过程,这个函数的原型如下
int daemon (int __nochdir, int __noclose);
如果__nochdir的值为0,则将切换工作目录为根目录;如果__noclose为0,则将标准输入,输出和标准错误都重定向到/dev /null。
经过这个函数调用后的程序将运行在后台,成为一个daemon程序,而linux下大多的服务都是以此方式运行的。
我们来看一个简单的例子。例如编写例子程序test.c
#include <unistd.h> #include <stdio.h> int do_sth() { //Add what u want return 0; } int main() { daemon(0,0); while ( 1 ) { do_sth(); sleep(1); } }
编译并运行
[leconte@localhost daemon]$ gcc -o test test.c [leconte@localhost daemon]$ ./test
程序进入了后台,通过ps查看进程情况,可以看到进程的父进程id为1,即init进程
用lsof查看test进程所打开的文件,可以看到文件描述符0,1,2都被重定向到/dev/null
并且能够看到,进程的当前工作目录(cwd)为根目录/,daemon函数已经帮我们完成了daemon化的过程,接下来我们只需要关注于程序功能 的实现了。
摘要:针对Linux环境下的守护进程daemon,分析了一般性守护进程的编写方法,并提出若干见解,通过总结归纳进而为设计和开发守护进程提供了有意的参考,给出了基于Linux守护进程实现的主要思想。
关键词: 守护进程;信号量;控制终端
1 引言
Linux在启动时需要启动很多系统服务,它们向本地和网络用户提供了Linux的系统功能接口,直接面向应用程序和用户。提供这些服务的程序是由运行在后台的守护进程(Daemons)来执行的。
编写守护进程实际上是把一个普通进程按照守护进程的特性进行改造。比如,网络通信服务中的守护进程需要能同时接受多个请求,它不断地在侦听端等待远程的连接请求,收到请求后,创建一个子进程,让其负责与远端的通信,而自己则继续返回侦听。子进程和父进程间的通信采用消息机制,因此守护进程的开发涉及到子进程、进程组、会晤期、信号量、文件权限、目录和控制终端等多个概念。本文主要分析守护进程的概念、实现原理以及编写守护进程的方法。
2 Linux进程结构
2.1 进程的创建
Linux使用fork()函数来创建一个子进程,当fork()调用失败时系统返回-1。一旦子进程被创建,父子进程一起从fork()处继续执行,相互竞争系统的资源。如果希望子进程继续执行,而父进程阻塞直到子进程完成任务,这时候可以调用wait()或者waitpid()系统调用。
pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid,int *stat_loc,int options);
wait()系统调用会使父进程阻塞直到一个子进程结束或者是父进程接受到了一个信号。如果没有父进程或者没有子进程或者它的子进程已经结束了,wai()会立即返回。成功时(因一个子进程结束)wait()将返回子进程的ID,否则返回-1。
2.2 守护进程的创建
守护进程最重要的特性是后台运行,因此守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符、控制终端、会话和进程组、工作目录以及文件创建掩码等。这些环境是守护进程从执行它的父进程(特别是Shell)中继承下来的。以下程序使用一个INIT_DAEMON宏来实现守护进程的初始化工作。
#define INIT_DAEMON
{
if (fork()>0)
exit(0);// 是父进程,结束父进程
else if(fork()<0)exit(1);// fork失败,退出
setsid(); // 第一子进程成为新的进程组长,并与控制终端分离
if(fork()>0) exit(0);//是第一子进程,结束第一子进程
else if(fork<0) exit(1);// fork失败,退出。
} //是第二子进程,继续,第二子进程不再是会话组长
第一次调用fork函数,为避免挂起,控制终端将守护进程放入后台执行,然后调用setsid()函数脱离控制终端和进程组,使该进程成为会话组长,并与原来的登录会话和进程组脱离。此时进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端。为了避免这种情况,可以通过使进程不再成为会话组长来禁止进程重新打开控制终端,这就需要第二次调用fork()函数。父进程(会话组长)退出,子进程继续执行,并不再拥有打开控制终端的能力。在正在执行的进程中调用INIT_DAEMON后,进程将成为守护进程,脱离控制终端进入后台执行。
2.3 信号量机制
为了防止在守护进程没有正常运转起来时,控制终端受到干扰退出或挂起,需要将以下有关控制终端操作的信号屏蔽。
signal(SIGTTOU, SIG_IGN);// 后台进程写控制终端
signal(SIGTTIN, SIG_IGN);// 后台进程读控制终端
signal(SIGTSTP, SIG_IGN);// 终端挂起
signal(SIGHUP, SIG_IGN);// 进程组长退出时向所有会议成员发出
当信号出现时,开发人员可以要求系统进行以下三种操作:
(1)忽略信号。大多数信号都是采取这种方式进行处理,但是对SIGKILL和SIGSTOP信号不能做忽略处理。
(2)捕捉信号。最常见的情况是,如果捕捉到SIGCHID信号,则表示子进程已经终止。可在此信号的捕捉函数中调用waitpid()函数取得该子进程的进程ID和它的终止状态。
(3)执行系统的默认动作。对绝大多数信号而言,系统的默认动作都是终止该进程。
3 守护进程开发准则
3.1 控制终端
首先使用ps命令打印系统中各个进程的状态。
所有守护进程都以超级用户(用户ID为0)的优先权运行。没有一个守护进程具有控制终端,终端名称设置为问号(?)、终端前台进程组ID设置为-1。缺少控制终端是守护进程调用了setsid()的结果。
3.2 进程组
进程组是一个或多个进程的集合。进程组ID类似于进程ID,可存放在 pid_t 数据类型中。每个进程组有一个组长进程,组长进程的标识是其进程组ID等于其进程ID。进程组组长可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程组中有一个进程存在,则该进程就存在,这与其组长进程是否终止无关。
3.3 会话期
会话期(Session)是一个或多个进程组的集合。在一个会话期中有3个进程组,通常是有Shell的管道线将几个进程编成一组。一个会话期可以有一个单独的控制终端,即在其上登录的终端设备(终端登录)或伪终端设备(网络登录),但这个控制终端并不是必需的。如果一个会话期有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组。
3.4 脱离控制终端,登录会话和进程组
登录会话可以包含多个进程组,这些进程组共享一个控制终端,这个控制终端通常是创建进程的控制终端。登录会话和进程组通常是从父进程继承下来的。需要说明的是,当进程是会话组长时,setsid()调用将失败,2.2已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离,由于会话过程对控制终端的独占性,进程同时与控制终端脱离。具体是操作就是:
(1)成为新对话期的首进程;
(2)成为一个新进程组的首进程;
(3)没有控制终端。
守护进程要摆脱从父进程继承下来的控制终端它们影响,可以使用setsid()函数设置新会话的领头进程,并与原来的登录会话和进程组脱离。这只是其中的一种方法,还可以使用如下处理方法:
if ((fd = open("/dev/tty",O_RDWR)) >= 0)
{ioctl(fd,TIOCNOTTY,NULL);
// 其中/dev/tty是一个流设备,也是终端映射
close(fd); } // 调用close()函数将终端关闭
3.5 关闭文件描述符
进程从创建它的父进程那里继承了打开的文件描述符,如果守护进程留下一个处于打开状态的普通文件,将阻止该文件被任何其他进程从文件系统中删除。一般来说,必要的是关闭0、1、2三个文件描述符,即标准输入(STDINT)、标准输出(STDOU)、标准错误(STDERR)。关闭不必要的连接甚至更为重要,因为在该终端上的用户退出系统后,将执行vhangup()系统调用,守护进程访问该终端的权利于是被撤消。这表示守护进程虽有它以为处于打开状态的文件描述符,事实上已不再能通过这些文件描述符访问该终端。关闭三者的代码如下:
for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++)
// fdtablesize是一个进程一次可以打开进程的最大数
close(fd); // 关闭打开的文件描述符,包括标准输入、标准输出和标准错误输出
如果程序想保留0、1、2三个文件描述符,那么循环应绕过这三者。实现代码如下:
for (fd =3, fdtablesize = getdtablesize();fd < fdtablesize; fd++)
close(fd);
4 结束语
守护进程广泛应用于Linux/Unix环境下的系统管理、网络通信以及嵌入式应用等领域。本文分析了Linux守护进程的结构与实现原理,所欠缺的是构建程序的通用化,原因是存在不同环境之间切换并执行不同的任务,同时还必须考虑其他系统之间的所有差异,今后的工作主要集中在标准化以及简化异构环境中的管理任务的方法上。
beyes2010-05-16 02:09
Linux 守护进程的编程方法
守护进程(Daemon)是运行在后台的一种特别进程。他单独于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。比如,作业规划进程crond,打印进程 lpd等。
守护进程的编程本身并不复杂,复杂的是各种版本的Unix的实现机制不尽相同,造成不同Unix环境下守护进程的编程规则并不一致。这需要读者注意,照搬某些书上的规则(特别是BSD4.3和低版本的System V)到Linux会出现错误的。下面将全面介绍Linux下守护进程的编程要点并给出周详实例。
一. 守护进程及其特性
守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程式TSR和之相似。其次,守护进程必须和其运行前的环境隔离开来。这些环境包括未关闭的文档描述符,控制终端,会话和进程组,工作目录连同文档创建掩模等。这些环境通常是守护进程从执行他的父进程(特别是shell)中继承下来的。最后,守护进程的启动方式有其特别之处。他能够在Linux系统启动时从启动脚本/etc/rc.d中启动,能够由作业规划进程crond启动,还能够由用户终端(通常是shell)执行。
总之,除开这些特别性以外,守护进程和普通进程基本上没有什么区别。因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。假如读者对进程有比较深入的认识就更容易理解和编程了。
二. 守护进程的编程要点
前面讲过,不同Unix环境下守护进程的编程规则并不一致。所幸的是守护进程的编程原则其实都相同,区别在于具体的实现细节不同。这个原则就是要满足守护进程的特性。同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来和BSD4相比更方便。编程要点如下;
1. 在后台运行。
为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。
if(pid=fork())
exit(0);//是父进程,结束父进程,子进程继续
2. 脱离控制终端,登录会话和进程组
有必要先介绍一下Linux中的进程和控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话能够包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。
控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱他们,使之不受他们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:
setsid();
说明:当进程是会话组长时setsid()调用失败。但第一点已确保进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并和原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时和控制终端脱离。
3. 禁止进程重新打开控制终端
现在,进程已成为无终端的会话组长。但他能够重新申请打开一个控制终端。能够通过使进程不再成为会话组长来禁止进程重新打开控制终端:
if(pid=fork())
exit(0);//结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
4. 关闭打开的文档描述符
进程从创建他的父进程那里继承了打开的文档描述符。如不关闭,将会浪费系统资源,造成进程所在的文档系统无法卸下连同引起无法预料的错误。按如下方法关闭他们:
for(i=0;i 关闭打开的文档描述符close(i);>
5. 改变当前工作目录
进程活动时,其工作目录所在的文档系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir("/")
6. 重设文档创建掩模
进程从创建他的父进程那里继承了文档创建掩模。他可能修改守护进程所创建的文档的存取位。为防止这一点,将文档创建掩模清除:umask(0);
7. 处理SIGCHLD信号
处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。假如父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。假如父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下能够简单地将 SIGCHLD信号的操作设为SIG_IGN。
signal(SIGCHLD,SIG_IGN);
这样,内核在子进程结束时不会产生僵尸进程。这一点和BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。
三. 守护进程实例
守护进程实例包括两部分:主程式test.c和初始化程式init.c。主程式每隔一分钟向/tmp目录中的日志test.log报告运行状态。初始化程式中的init_daemon函数负责生成守护进程。读者能够利用init_daemon函数生成自己的守护进程。
1. init.c清单
#include < unistd.h >
#include < signal.h >
#include < sys/param.h >
#include < sys/types.h >
#include < sys/stat.h >
void init_daemon(void)
{
int pid;
int i;
if(pid=fork())
exit(0);//是父进程,结束父进程
else if(pid< 0)
exit(1);//fork失败,退出
//是第一子进程,后台继续执行
setsid();//第一子进程成为新的会话组长和进程组长
//并和控制终端分离
if(pid=fork())
exit(0);//是第一子进程,结束第一子进程
else if(pid< 0)
exit(1);//fork失败,退出
//是第二子进程,继续
//第二子进程不再是会话组长
for(i=0;i< NOFILE; i)//关闭打开的文档描述符
close(i);
chdir("/tmp");//改变工作目录到/tmp
umask(0);//重设文档创建掩模
return;
}
2. test.c清单
#include < stdio.h >
#include < time.h >
void init_daemon(void);//守护进程初始化函数
main()
{
FILE *fp;
time_t t;
init_daemon();//初始化为Daemon
while(1)//每隔一分钟向test.log报告运行状态
{
sleep(60);//睡眠一分钟
if((fp=fopen("test.log","a")) >=0)
{
t=time(0);
fprintf(fp,"I'm here at %s/n",asctime(localtime(&t)) );
fclose(fp);
}
}
}