linux创建守护进程

一、概述

        linux开启进程都是与终端绑定的,终端一关,进程也关,如果想独立不受干扰,必须将此进程变为守护进程(在后台运行,不以终端方式与用户交互)。

        守护进程能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才退出。如果想让某个进程不因为用户或终端或其他地变化而受到影响,那么就必须把这个进程变成一个守护进程。

 

二、守护进程特性

        守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程序TSR与之相似。其次,守护进程必须与其运行前的环境隔离开来:这些环境包括未关闭的文件描述符控制终端会话和进程组工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。

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

 

三、创建守护进程步骤

创建一个简单的守护进程:

step1:创建子进程,父进程退出: (假象--父进程已完成,可退出终端)

step2: 在子进程中创建新会话:使用系统函数setid()--进程组、会话期

step3: 改变当前目录为根目录,用fork创建的子进程继承了父进程的当前工作目录

step4: 重设文件权限掩码: umask(0)

step5: 关闭文件描述符

 

  1、创建子进程,父进程退出: (假象--父进程已完成,可退出终端)

  这是编写守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在Shell终端里造成一程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离。

  在Linux中父进程先于子进程退出会造成子进程成为孤儿进程,而每当系统发现一个孤儿进程是,就会自动由1号进程(init)收养它,这样,原先的子进程就会变成init进程的子进程。

  2、在子进程中创建新会话: 使用系统函数setid()--进程组、会话期

  这个步骤是创建守护进程中最重要的一步,虽然它的实现非常简单,但它的意义却非常重大。在这里使用的是系统函数setsid,在具体介绍setsid之前,首先要了解两个概念:进程组和会话期

  进程组:是一个或多个进程的集合。进程组有进程组ID来唯一标识。除了进程号(PID)之外,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID。且该进程组ID不会因组长进程的退出而受到影响。

  会话周期:会话期是一个或多个进程组的集合。通常,一个会话开始与用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。

  接下来就可以具体介绍setsid的相关内容:

  (1)setsid函数作用:

  setsid函数用于创建一个新的会话,并担任该会话组的组长。调用setsid有下面的3个作用:

  让进程摆脱原会话的控制

  让进程摆脱原进程组的控制

  让进程摆脱原控制终端的控制

  那么,在创建守护进程时为什么要调用setsid函数呢?由于创建守护进程的第一步调用了fork函数来创建子进程,再将父进程退出。由于在调用了fork函数时,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,还还不是真正意义上的独立开来,而setsid函数能够使进程完全独立出来,从而摆脱其他进程的控制。

  3、改变当前目录为根目录

  这一步也是必要的步骤。使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入但用户模式)。因此,通常的做法是让"/"作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数式chdir。

  4、重设文件权限掩码: umask(0)

  文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常的使用方法为umask(0)。

  5、关闭文件描述符

  同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。

        在上面的第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)已经失去了存在的价值,也应被关闭。通常按如下方式关闭文件描述符:

for(i=0;i<MAXFILE;i++)  
    close(i);

这样,一个简单的守护进程就建立起来了。

实现守护进程的完整实例(每隔10s在/tmp/dameon.log中写入一句话):

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#define MAXFILE 65535
int main()
{
    pid_t pc;
    int i, fd, len;
    char *buf="this is a Dameon\n";
    len = strlen(buf);
    pc = fork(); //第一步
    if(pc<0){
        printf("error fork\n");
        exit(1);
    }
    else if(PC>0)
        exit(0);
        setsid(); //第二步
        chdir("/"); //第三步
      umask(0); //第四步
      for(i=0;i<MAXFILE;i++) //第五步
          close(i);
          if((fd=open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0){
              perror("open");
              exit(1);
      }   
      while(1){
         write(fd,buf,len+1);
          sleep(10);
      }
}

四、守护进程编程要点

        不同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下必须显式等待子进程结束才能释放僵尸进程。

-------------------------------------------------------------------------------------------------------------------

守护进程顾名思义就是默默运行在后台的进程。它具有以下特征:

  1. 父进程是init
  2. 没有和任何控制终端关联,所以也不会收到诸如SIGINT、SIGTSTP等信号
创建守护进程步骤
  1. fork()创建子进程,父进程退出这样做的2个目的:

  • 使调用程序的控制台结束挂起,可以继续执行
  • 使得新创建的进程不会成为进程组的leader,保证了接下来的setsid()顺利执行

  1. 调用setsid(),创建新的session,使得调用者进程失去和控制终端的关联
  2. 避免进程后续打开终端设备不会成为控制终端,有2种办法:

  •  每次open()的时候指定参数O_NOTTY
  •  再执行一次fork()并且让主进程退出,使得进程的子进程不会成为会话的leader

  1. 清除从祖先继承下来umask值,以便后续的创建的文件文件夹能有指定的权限
  2. 改变工作目录(可选)

  •  一般是换到/目录下。如果不这样做,有可能导致占用文件系统,使之无法卸载。

  1. 关闭从父进程继承的文件描述符(可选)

  • 既然已经失去了控制终端,还保留0 1 2文件描述符就显得多余。
  • 当还有文件描述符在某个磁盘上时,会妨碍文件系统的卸载。

  1. 重定向描述符(可选)

  • 将0 1 2指向/dev/null
  • 避免后续向这3个描述符的io操作不会发生异常

另外一个例子:
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int daemonize() {

    // 1. 第一次fork()
    switch (fork()) {
    case -1: exit(-1); // fork错误
    case 0: break;     // 子进程继续
    default: exit(0);  // 父进程退出
    }

    // 2. 创建新的session
    setsid();

    // 3. 第二次fork()
    switch (fork()) {
    case -1: exit(-1); // fork错误
    case 0: break;     // 子进程继续
    default: exit(0);  // 父进程退出
    }

    // 4. 清空umask值
    umask(0);

    // 5. 改变工作目录(可选)
    chdir("/");

    // 6. 关闭从父进程继承来的文件描述符
    int maxfd = sysconf(_SC_OPEN_MAX); // 获取系统的最大打开文件数
    if (-1 == maxfd) {
        maxfd = 8192; // 获取失败,使用猜测的最大数
    }
    // 关闭所有文件描述符
    for (int fd = 0; fd < maxfd; fd++) {
        close(fd);
    }

    // 7. 重定向0 1 2
    close(STDIN_FILENO);
    int fd = open("/dev/null", O_RDWR);
    if (fd != STDIN_FILENO) {
        return -1;
    }
    if (dup2(STDIN_FILENO, STDOUT_FILENO) != STDOUT_FILENO) {
        return -1;
    }
    if (dup2(STDIN_FILENO, STDERR_FILENO) != STDERR_FILENO) {
        return -1;
    }

    return 0;
}

int main(int argc, char *argv[]) {
    daemonize();
    sleep(100);
    return 0;
}

注意:

        1.进程组的id需要和leader进程的id保持一致。由于组id是父进程的id,故子进程id必然不会和组的id一致。

        2.进程组leader调用setsid(),会发生EPERM错误。进程调用setsid()时,会同时创建新的进程组,并使用调用进程的id作为新进程组的组id。又因为进程组的组id总是组leader进程的id,若leader进程使用setsid()创建了新的session,一并创建的新的进程组id就会和原进程组id冲突。

        3.会话的id是setsid()的调用者的进程id。这里再fork()一次,会话的id就是父进程的id。则子进程的id必然不会是会话的id,进而失去了获得控制终端的能力。


posted @ 2014-10-12 12:27  DianaCody  阅读(2171)  评论(0编辑  收藏  举报