首页 > 代码库 > 如何编写一个守护进程daemon

如何编写一个守护进程daemon

大体步骤如下

1.   调用fork()函数创建子进程后,让父进程立即exit(),这样产生的子进程变成孤儿进程,由init进程接管。

2.   调用setsid()函数,使得新创建的进程脱离控制终端,同时创建新的进程组,并成为该进程组的首进程。在linux系统中,所有的进程都属于各自的进程组,进程组是一个或多个进程的集合,一个进程组中至少有一个进程成员,否则就消亡了。每个进程组都有一个进程组ID,是由领头进程的进程号决定的,会话则是一个或多个进程组的集合,每个会话都有一个领头进程,会话和进程组是linux内核用于管理多用户情况下用户进程的方法,每个进程都属于一个进程组,而进程组又属于某个会话,当用户从终端登录系统时,系统会创建一个新的会话,在该终端上启动的进程都会被系统划归到会话的进程组中。会话中的进程通过该会话中的领头进程与一个终端相连,该终端是会话的控制终端,一个会话只能有一个控制终端,如果会话存在一个控制终端时,则它必然拥有一个前台进程组,属于该组的进程可以从从控制终端获得输入。由于守护进程没有控制终端,而使用fork()函数创建的子进程会继承父进程的控制终端,会话和进程组,因此,必须用setsid()创建新的会话,以脱离父进程的影响。Setsid函数将创建新的会话,并使得调用setsid函数的进程成为新会话的领头进程。调用setsid函数的进程是新创建会话中的唯一的进程组,进程组ID为调用进程的进程号。Setsid函数产生这一结果还有个条件,即调用进程不为一个进程的领头进程。由于在第一步调用fork的父进程退出,使得子进程不可能是进程组的领头进程。该会话的领头进程没有控制终端与其相连,至此,满足了守护进程没有控制终端的要求

3.   更改当前工作目录

使用fork()函数创建的子进程会继承父进程的当前工作目录,当进程工作没有结束时,其工作目录是不能被卸载的.为了防止此问题,守护进程一般要chdir()了数将其工作目录更改到别的目录下(一般为/根目录,因为根目录是永远不会被卸载的,除非关机).

4.   关闭文件描述符,并重定向标准输入,输出和错误

子进程会继承父进程某些打开了的文件描述符,如果不使用这些文件描述符,则需要关闭它们.守护进程是运行在系统后台的,不应该在终端有任何的输出信息,可以使用dup()函数将其重定向到/dev/null空设备上.

5.   设置守护进程的文件权限创建掩码

守护进程会创建一些临时文件,出于性的考虑,往往不希望这些文件被别的用户查看,这时可以用umask()函数修改文件权限,创建掩码的取值.

上面说的是创建守护进程的大体步骤,解释其中的两点:

1、setsid的作用

一、让进程摆脱原会话的控制

二. 让进程摆脱原进程组的控制
三. 让进程摆脱原控制终端的控制
setsid()就是将进程和它当前的对话过程和进程组分离开,并且把它设置成一个新的对话过程的领头进程。也就是说让调用setsid()函数的这个进程脱离原有的进程组,自立门户。同时需要注意一点就是,调用这个函数的进程不能是一个进程组的进程组长,不然进程组长自立门户一户,这个进程组的其他进程也就会消亡(这个和平时自己创建父子进程,然后父子进程各自做各自的事情还不太一样)。这也就解释了上面红色字体的那句话。在子进程中调用setsid,不管这个时候父进程退出与否,子进程都不会是它当前所在进程组的进程组长。所以调用这个函数的是子进程,下面这个例子就是在父进程中调用setsid的副作用,会使父进程所在的进程组的其他子进程销毁
pid_t pid = fork();
if (pid == 0) {
        ...
        int result = execl(path, "adb", "fork-server", "server", NULL);
} else {
        // run a program in a new session
        setsid();//之前parent和child运行在同一个session里,而且parent是session头,
        //所以作为session头的parent如果exit结束执行的话,那么会话session组中的所有进程将都被杀死;
        //所以执行setsid()之后,parent将重新获得一个新的会话session组id,child将仍持有原有的会话session组,
        //这时parent退出之后,将不会影响到child了。
}
      会话session是一个或多个进程组的集合。进程调用setsid函数建立一个新会话。如果调用此函数的进程不是一个进程组的组长,则此函数就会创建一个新会话,该进程变成会话的首进程,然后该进程成为一个新进程组的组长进程,该进程没有控制终端。因为会话首进程是具有唯一进程ID的单个进程,所以可以将会话首进程的进程ID视为会话Id。
2、其实创建daemon进程是调用fork()两次的。
一、如果开启daemon进程的父进程本身可能不单单是为了创建daemon,假如父进程在某个地方一直阻塞,这时守护进程一直在运行,父进程却没有正常退出。如果守护进程因为正常或非正常原因退出了,就会变成ZOMBIE进程。

如果fork两次呢?父进程先fork出一个儿子进程,儿子进程再fork出孙子进程做为守护进程,然后儿子进程立刻退出,守护进程被init进程接管,这样无论父进程做什么事,无论怎么被阻塞,都与守护进程无关了。所以,fork两次的守护进程很安全,避免了僵尸进程出现的可能性。

二、第一个子进程调用setsid()函数,调用以后第一个子进程才成为了新的进程组的进程组长(注意,调用setsid()函数的第一个子进程在调用这个函数之前不是进程组的进程组长)第一子进程成为新的会话组长和进程组长进程组长有权利申请打开一个控制终端,第二次fork的意义就在此,关闭掉第一子进程,使第二子进程成为守护进程,并且因为没有进程组长,  所以守护进程不会被关闭。
下面是个代码示例:
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <time.h>
    #include <syslog.h>
    #include <signal.h>
    #include <sys/param.h>
    #include <fcntl.h>
    int init_daemon(void)
    {
        int pid;
        int i;
        int fd;
        /*忽略终端I/O信号,STOP信号*/
        signal (SIGTTOU, SIG_IGN);
        signal (SIGTTIN, SIG_IGN);
        signal (SIGTSTP, SIG_IGN);
        signal (SIGHUP, SIG_IGN);
        printf(“ppid = %d\n”,getppid());
        pid = fork();
        fd = open(“/dev/tty”,O_RDONLY);
        printf(“!fd = %d\n”,fd);
        close(fd);
        if (pid > 0)
        {
            printf(“Parent process pid = %d\n”,getpid());
            exit(0);  //结束父进程,使得子进程成为后台进程
        }
        else if (pid < 0)
            return -1;
        printf(“First Child process pid = %d\n”,getpid());
        //当前进程为第一子进程
        //建立一个新的进程组,在这个新的进程组中,子进程成为这个进程组的首进程,以使该进程脱离所有终端
        printf(“pgid = %d\n”,getpgid(getpid()));
        //printf(“pid = %d\n”,getpid());
        setsid();
        printf(“pgid = %d\n”,getpgid(getpid()));
        //printf(“pid = %d\n”,getpid());
        fd = open(“/dev/tty”,O_RDONLY);
        printf(“fd = %d\n”,fd);
        close(fd);
        //再次新建一个子进程,退出父进程(第一子进程),保证该进程不是进程组长,同时让该进程无法再打开一个新的终端
        pid = fork();
        fd = open(“/dev/tty”,O_RDONLY);
        printf(“#fd = %d\n”,fd);
        close(fd);
        if (pid > 0)
        {
            printf(“First Child process pid = %d\n”,getpid());
            exit(0);
        }
        else if (pid < 0)
          return -1;
        //关闭所有从父进程继承的不再需要的文件描述符
        for (i=0; i < NOFILE; close(i++));
        //改变工作目录,使得进程不与任何文件系统联系
        chdir(“/”);
        //将文件屏蔽字设置为0
        umask(0);
        //忽略SIGCHLD信号
        signal(SIGCHLD,SIG_IGN);
        return 0;
    }
    int main(int argc, char *argv[])
    {
        time_t now;
        init_daemon();
        syslog(LOG_USER|LOG_INFO,”测试守护进程!\n”);
        while(1)
        {
            sleep(8);
            time(&now);
            syslog(LOG_USER|LOG_INFO,”系统时间:\t%s\t\t\n”,ctime(&now));
        }
    }



如何编写一个守护进程daemon