守护进程

          守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待 处理某些发生的事件。守护进程是一种很有用的进程。 Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。 同时,守护进程完成许多系统任务。比如,作业规划进程crond,打印进程lpd等。  

           守护进程的编程本身并不复杂,复杂的是各种版本的Unix的实现机制不尽相同, 造成不同 Unix环境下守护进程的编程规则并不一致。 需要注意,照搬某些书上的规则(特别是BSD4.3和低版本的System V)到Linux会出现错误的。

            下面结合一些前辈的文档和自己的例子说说守护进程的编程。.基本概念.进程 .每个进程都有一个父进程 .当子进程终止时,父进程会得到通知并能取得子进程的退出状态。.进程组 .每个进程也属于一个进程组 .每个进程主都有一个进程组号,该号等于该进程组组长的PID号 .一个进程只能为它自己或子进程设置进程组ID号.会话期 .对话期(session)是一个或多个进程组的集合。 .setsid()函数可以建立一个对话期: 如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期。

              (1)此进程变成该对话期的首进程

              (2)此进程变成一个新进程组的组长进程。

              (3)此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。 如果该进程是一个进程组的组长,此函数返回错误。

              (4)为了保证这一点,我们先调用fork()然后exit(),此时只有子进程在运行, 子进程继承了父进程的进程组ID,但是进程PID却是新分配的,所以不可能是新会话的进程组的PID。 从而保证了这一点。

               if((pid=fork())>0) //parent exit(0);

               else if(pid==0){ //th1 child 

                        setsid(); //th1是成为会话期组长

                         if(fork() ==0){

                                  //th2不会是会话期组长(变成孤儿进程组) ... }

                 }

                  一. 守护进程及其特性

                 (1)守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程序TSR与之相似。

                 (2)其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端, 会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell) 中继承下来的。

                 (3)最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动, 可以由作业规划进程crond启动,还可以由用户终端(通常是 shell)执行。  

                  总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别。 因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。

               二. 守护进程的编程要点 (来自UEAP)  

              前面讲过,不同Unix环境下守护进程的编程规则并不一致。所幸的是守护进程的编程原则其实都一样, 区别在于具体的实现细节不同。这个原则就是要满足守护进程的特性。 同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来与BSD4相比更方便。编程要点如下;

              1. 在后台运行。  

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

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

              进程属于一个进程组,进程组号(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,"Im here at %sn",asctime(localtime(&t)) );

                           fclose(fp);

                     } }}

          以上程序在RedHat Linux6.0下编译通过。步骤如下:编译:gcc -g -o test init.c test.c执行:./test查看进程:ps -ef说明:在系统调用库中有一个库函数可以直接使一个进程变成守护进程,

           #include <unistd.h>

           int daemon(int nochdir, int noclose);

          创建守护进程的时,往往要做以下两件事情

1.将进程的工作目录修改为"/"根目录
   daemon的参数nochdir为0时,即可将工作目录修改为根目录; 
2.将标准输入,输出和错误输出重定向到/dev/null 
   daemon的参数noclose为0时,输入,输出以及错误输出重定向到/dev/null 。

返回值:成功返回0
            错误返回-1

daemon()函数透析

#include <unistd.h>
int main(int argc, char *argv[])
{
    ...
    if (daemon(0, 0)) {//调用glibc库函数daemon,创建daemon守护进程
        perror("daemon");
        return -1;
    }
    子进程内容
}

 


int daemon( int nochdir, int noclose )
{
   pid_t pid;

   if ( !nochdir && chdir("/") != 0 )   //如果nochdir=0,那么改变到"/"根目录
       return -1;
   
   if ( !noclose
)                      //如果没有noclose=0,那么开始输入,输出以及错误输出重定向到/dev/null 
   {
     int fd = open("/dev/null", O_RDWR);//打开神奇的黑洞文件.

     if ( fd < 0 )
      return -1;

//每个进程都拥有自己fds文件描述符表,
//fdt->fd[0],fdt->fd[1],fdt->fd[2],
//表中fd[0],fd[1],和fd[2]文件句柄位置对应的fops文件操作函数集,分别与标准输入,标准输出,标准错误输出相关联
//所以用户应用程序调用open函数打开文件时,默认都是以3索引为开始句柄,也就是fd[3],故当前open返回的文件句柄最小值为3

 


//dup2(unsigned int oldfd, unsigned int newfd)系统调用就是用oldfd的fops操作文件集file,复制到newfd所在处
//即:fdt->fd[newfd] = fdt->fd[oldfd];

     if ( dup2( fd, 0 ) < 0 || //使用字符设备/dev/null的fops函数操作集,替换0句柄对应的文件操作集.
     dup2( fd, 1 ) < 0 ||     
//使用字符设备/dev/null的fops函数操作集,替换1句柄对应的文件操作集.
          dup2( fd, 2 ) < 0 ) 
//使用字符设备/dev/null的fops函数操作集,替换2句柄对应的文件操作集.
     {
       close(fd);
      return -1;
     }

 //如果替换成功,那么键盘的任何操作将不会对该进程产生任何影响,因为0,1,2句柄所在处的fops文件操作集已经都变成了,
 

//被重定向为"/dev/null"空洞设备的fops.所以对0,1,2句柄的读写操作,也就是在对/dev/null设备作读写操作.
     close(fd); //关闭打开的/dev/null
  }
   pid = fork();//
创建子进程.
   if (pid < 0)
    return -1;

   if (pid > 0)
    _exit(0);  //
返回执行的是父进程,那么父进程退出,让子进程变成真正的孤儿进程.

               //ok,我们期望的daemon子进程执行到这里了.
   if ( setsid() < 0 )
//设置session id.
     return -1;

   return 0;   //成功创建daemon子进程[luther.gliethttp].
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值