守护进程(Daemon)

来源:互联网 发布:天津办公软件培训 编辑:程序博客网 时间:2024/05/01 08:39

守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进 程。Linux的大多数服务器就是用守护进程实现的。比如,Web服务器httpd等。同时,守护进程完成许多系统任务。比如,作业规划进程crond, 打印进程lpd等。下面将我理解的Linux下守护进程做一些解释和说明。同时将网上一个常用的解说Linux下守护进程的程序作为实例介绍一下。

   Linux下的守护进程
   守护进程有三个最基本的特点:后台运行,独立于终端,完成一定的任务。
首先所谓的后台运行过程是一般是在图形界面或是终端不可见的;而独立于终端是说它不和终端联系,运行之后一般不接受终端的输入也不向终端输出;而完成一点的任务是每一个守护进程的运行都是为了完成一定的任务而运行的,这些任务一般都是系统相关的任务。
也就是控制台除开这些特殊性以外,守护进程与普通进程基本上没有什么区别。因此,守护进程可以由一个普通进程按照上述的守护进程的特性而改造成为守护进程。
   

   这里首先要说几个概念,进程组,会话和控制终端
   进程组:
每运行一个程序或是命令就会产生一个进程组,而每一个进程组有以个领头进程,一般进程组的第一个进程是领头进程,领头进程fork的进程也属于同一个进程组,但是子进程一旦执行exec等就会不属于该进程组。当然子进程也可以成为领头进程,下面的例子就是这样的。

会话:一次登录形成一个会话 一个会话可包含多个进程组, 但只能有一个前台进程组.   setsid 可建立一个新的会话。
控制终端:会话的领头进程打开一个终端之后, 该终端就成为该会话的控制终端 与控制终端建立连接的会话领头进程称为控制进程,一个会话只能有一个控制终端,产生在控制终端上的输入和信号将发送给会话的前台进程组中的所有进程 终端上的连接断开时 (比如网络断开或 Modem 断开), 挂起信号将发送到控制进程(session leader) 。

编程守护进程:
编程实现守护进程也就是要是想上面的三个特点:
  

1. 在后台运行。
为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。
if(pid=fork())
exit(0);//是父进程,结束父进程,子进程继续

2. 脱离控制终端,登录会话和进程组
先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。
控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:
setsid(); 以下是内核中的一段代码。
kernel/sys.c 中系统调用setsid所对应的函数。
asmlinkage long sys_setsid(void)
{
struct task_struct *group_leader = current->group_leader;
struct pid *sid = task_pid(group_leader);
pid_t session = pid_vnr(sid);
int err = -EPERM;

write_lock_irq(&tasklist_lock);
/* Fail if I am already a session leader */
if (group_leader->signal->leader)
goto out;
/* Fail if a process group id already exists that equals the
* proposed session id.
*/
if (pid_task(sid, PIDTYPE_PGID))
goto out;
group_leader->signal->leader = 1;
__set_special_pids(sid);
spin_lock(&group_leader->sighand->siglock);
group_leader->signal->tty = NULL; //这里将是脱离终端
spin_unlock(&group_leader->sighand->siglock);
err = session;
out:
write_unlock_irq(&tasklist_lock);
return err;
}
setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

3. 禁止进程重新打开控制终端
现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:这里也就数只有会话组长才能打开控制主终端。
if(pid=fork())
exit(0);//结束第一子进程,第二子进程继续(第二子进程不再是会话组长)

4. 关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:

for(i=0;i 关闭打开的文件描述符close(i);>

5. 改变当前工作目录

进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir(\”/\”)

6. 重设文件创建掩码

进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);
当 我们登录系统之后创建一个文件总是有一个默认权限的,那么这个权限是怎么来的呢?这就是umask干的事情。umask设置了用户创建文件的默认权限,它 与chmod的效果刚好相反,umask设置的是权限“补码”,而chmod设置的是文件权限码。也就是是说它可以决定当前进程打开或是创建文件的权限。 如:umask值为022,则默认目录权限为755.

7. 处理SIGCHLD信号

处 理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵 尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将 SIGCHLD信号的操作设为SIG_IGN。

signal(SIGCHLD,SIG_IGN);

这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。

这里我的理解是:主要应用在进程分离当中,最典型的当属ftp,http等网络服务程序的守护进程。这里使用 SIGCHLD信号后,子进程将会不属于父进程,而从属于init进程,父进程则不须等待子进程的结束或着负责子进程结束后的回收,子进程的回收完全有init进程来完成。

http://apps.hi.baidu.com/share/detail/19530514


守护进程实例包括两部分:主程序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,"Im here at %sn",asctime(localtime(&t)) ); 
fclose(fp); 



以上程序在RedHat Linux6.0下编译通过。步骤如下: 
编译:gcc -g -o test init.c test.c 
执行:./test 
查看进程:ps -ef 
从输出可以发现test守护进程的各种特性满足上面的要求


http://blogold.chinaunix.net/u2/69674/showart_1081162.html

守护进程的编程规则:

1、屏蔽一些有关控制终端操作的信号

防止在守护进程没有正常运转起来时,控制终端受到干扰退出或挂起。

2、脱离控制终端,登录会话和进程组

登录会话可以包含多个进程组,这些进程组共享一个控制终端,这个控制终端通常是创建进程的登录终端。控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。

其方法是在fork()的基础上,调用setsid()使进程成为会话组长。调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离,由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

setsid()实现了以下效果:

(a) 成为新对话期的首进程

(b) 成为一个新进程组的首进程

(c) 没有控制终端。


3、禁止进程重新打开控制终端

现在,进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端,再fork()一次。

4、关闭打开的文件描述符

进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在地文件系统无法卸下以及无法预料的错误。一般来说,必要的是关闭0、1、2三个文件描述符,即标准输入、标准输出、标准错误。因为我们一般希望守护进程自己有一套信息输出、输入的体系,而不是把所有的东西都发送到终端屏幕上。

5、改变当前工作目录

将当前工作目录更改为根目录。从父进程继承过来的当前工作目录可能在一个装配的文件系统中。因为守护进程通常在系统重启之前是一直存在的,所以如果守护进程的当前工作目录在一个装配文件系统中,那么该文件系统就不能被拆卸。

另外,某些守护进程可能会把当前工作目录更改到某个指定位置,在此位置做它们的工作。例如,行式打印机假脱机守护进程常常将其工作目录更改到它们的spool目录上。

6、重设文件创建掩码

将文件方式创建屏蔽字设置为0:umask(0)。 由继承得来的文件方式创建的屏蔽字可能会拒绝设置某些许可权。例如,若守护进程要创建一个组可读、写的文件,而继承的文件方式创建屏蔽字,屏蔽了这两种许可权,则所要求的组可读、写就不能起作用。

7、处理SIGCHLD信号

处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时fork子进程出来处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)而仍占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在系统V下可以简单地将SIGCHLD信号的操作设为SIG_IGN,即忽略掉。这样,内核在子进程结束时不会产生僵尸进程,这一点与BSD4不同,在BSD4下必须显示等待子进程结束才能释放僵尸进程。

8、记录信息

在Linux/Unix下有个syslogd的守护进程,向用户提供了syslog()系统调用。任何程序都可以通过syslog记录事件。

源码实现及分析:

#include<stdio.h>

#include<signal.h>

#include<syslog.h>

#include<unistd.h>

#include<fcntl.h>

#include<time.h>

#include<sys/file.h>

#include<sys/ioctl.h>

#include<stdlib.h>

int main(int argc,char *argv[])

{

     time_t now;

     int childpid,fd,fdtablesize;

//     int error,in,out;

     int fp;    

 //屏蔽一些有关控制终端操作的信号。防止在守护进程没有正常运转起来时,控制终端受到

//干扰 退出或挂起,此处忽略了终端I/O信号、STOP信号

     signal(SIGTTOU,SIG_IGN);

     signal(SIGTTIN,SIG_IGN);

     signal(SIGTSTP,SIG_IGN);

     signal(SIGHUP,SIG_IGN);

//由于子进程会继承父进程的某些特性,如控制终端、登录会话、进程组等,而守护进程

//最终要脱离控制终端到后台去运行,所以必须把父进程杀掉,以确保子进程不是进程组长,

//这也是成功调用setsid()所要求的。

     if (fork() != 0)

     {

         exit(1);

     }

//脱离了控制终端还要脱离登录会话和进程组,这里可以调用setsid()函数,调用成功后

//进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离,由于会话过程

//对控制终端的独占性,进程同时与控制终端脱离。

/*

     if (setsid() < 0)

     {

         exit(1);

     }

*/

     //要达到setsid()函数的功能也可以用如下处理方法。"/dev/tty"是一个流设备,也是终端映射,调用close()函数将终端关闭。

     if ((fp=open("/dev/tty",O_RDWR)) >= 0)

     {

         ioctl(fp,TIOCNOTTY,NULL);

         close(fp);

     }

     //进程已经成为无终端的会话组长,但它可以重新申请打开一个新的控制终端。可以通过不再让进程成为会话组长的方式来禁止进程重新打开控制终端,需要再次调用fork函数。

     if (fork() != 0)

     {

         exit(1);

     }

    

     //从父进程继承过来的当前工作目录可能在一个装配的文件系统中。因为守护进程通常在系统重启之前是一直存在的,所以如果守护进程的当前工作目录在一个装配文件系统中,那么该文件系统就不能被卸载。比如说从父进程继承的当前目录是/mnt下面的一个被挂载的目录。

     if (chdir("/tmp") == -1)

     {

         exit(1);

     }

     //关闭打开的文件描述符,或重定向标准输入、标准输出和标准错误输出的文件描述符。进程从创建它的父进程那里继承了打开的文件描述符。如果不关闭,将会浪费系统资源,引起无法预料的错误。getdtablesize()返回某个进程所能打开的最大的文件数。

     for (fd=0,fdtablesize=getdtablesize();fd<fdtablesize;fd++)

     {

         close(fd);

     }

     //有的程序有些特殊的需求,还需要将这三者重新定向。

/*

     error=open("/tmp/error",O_WRONLY|O_CREAT,0600);

     dup2(error,2);

     close(error);

     in=open("/tmp/in",O_RDONLY|O_CREAT,0600);

     if(dup2(in,0)==-1) perror("in");

     close(in);

     out=open("/tmp/out",O_WRONLY|O_CREAT,0600);

     if(dup2(out,1)==-1) perror("out");

     close(out);

*/

     //由继承得来的文件方式创建的屏蔽字可能会拒绝设置某些权限,所以要重新赋于所有权限。

     umask(0);

     //如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源,如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。因此需要对SIGCHLD信号做出处理,回收僵尸进程的资源,避免造成不必要的资源浪费。

     signal(SIGCHLD,SIG_IGN);

     //守护进程不属于任何终端,所以当需要输出某些信息时,它无法像一般程序那样将信息直接输出到终端,可以使用linux中自带的syslogd守护进程,它向用户提供了syslog()系统调用函数。信息都保存在/var/log/syslog文件中。

     syslog(LOG_USER|LOG_INFO,"守护进程测试!\n");

     //每隔60秒钟就向/var/log/syslog日志文件里写入一次syslog()发出的信息。

     while (1)

     {

         //time(time_t *t)返回从1970年1月1日0时0分0秒到现在的所有秒数。

         time(&now);

            

         //获得父进程ID和子进程ID。

         syslog(LOG_USER|LOG_INFO,"PPID: %d PID: %d\n",getppid(),getpid());

         //ctime(const time_t *)返回当前时间的一个char型指针。

         syslog(LOG_USER|LOG_INFO,"当前时间:%s\n",ctime(&now));

         //睡眠60秒

         sleep(60);

     }

     return 0;

}



原创粉丝点击