守护进程

孤儿进程

在操作系统领域中,孤儿进程指的是在其父进程执行完成或被终止后仍继续运行的一类进程。<维基百科>

为避免孤儿进程退出时无法释放所占用的资源而僵死,孤儿进程一旦产生,将会立即由系统进程init收养。init的进程ID为1,因为被收养的孤儿进程的父进程ID更新为1.

孤儿进程组

当一个终端控制进程(即会话首进程)终止后,那么这个终端可以用来建立一个新的会话。这可能会产生一个问题,原来旧的会话(一个或者多个进程组的集合)中的任一进程可再次访问这个的终端。为了防止这类问题的产生,于是就有了孤儿进程组的概念。当一个进程组成为孤儿进程组时,posix.1要求向孤儿进程组中处于停止状态的进程发送SIGHUP(挂起)信号,系统对于这种信号的默认处理是终止进程,然而如果无视这个信号或者另行处理的话那么这个挂起进程仍可以继续执行。

孤儿进程的应用:

刻意使进程成为孤儿进程,使之与用户会话脱钩,并转至后台运行。这就是守护进程。

Daemon守护进程:

daemon在英文中是“精灵”的意思,因此也有人称守护进程为精灵进程。daemon进程是后台服务进程,寿命很长,从开始到系统关闭,一直在运行。几乎所有的服务器程序,包括我们熟知的Apache和wu-FTP,都用daemon进程的形式实现。很多Linux下常见的命令如inetd和ftpd,末尾的字母d就是指daemon。

为什么一定要使用daemon进程呢?Linux中每一个系统与用户进行交流的界面称为终端(terminal),每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端(Controlling terminal),当控制终端被关闭时,相应的进程都会被自动关闭。但是daemon进程却能够突破这种限制,即使对应的终端关闭,它也能在系统中长久地存在下去,如果我们想让某个进程长命百岁,不因为用户或终端或其他的变化而受到影响,就必须把这个进程变成一个daemon进程。

Daemon进程的编程规则

如果想把自己的进程变成daemon进程,我们必须严格按照以下步骤进行:

1. 调用fork产生一个子进程,同时父进程退出。我们所有后续工作都在子进程中完成。这样做我们可以:
  • 如果我们是从命令行执行的该程序,这可以造成程序执行完毕的假象,shell会回去等待下一条命令;       刚刚通过fork产生的新进程一定不会是一个进程组的组长,这为第2步的执行提供了前提保障。
  • 这样做还会出现一种很有趣的现象:由于父进程已经先于子进程退出,会造成子进程没有父进程,变成一个孤儿进程(orphan)。每当系统发现一个孤儿进程,就会自动由1号进程(init)收养它,这样,原先的子进程就会变成1号进程的子进程。

2. 调用setsid系统调用。这是整个过程中最重要的一步。它的作用是创建一个新的会话(session),并自任该会话的组长(session leader)。如果调用进程是一个进程组的组长,调用就会失败,但这已经在第1步得到了保证。调用setsid有3个作用:

  • 让进程摆脱原会话的控制;
  • 让进程摆脱原进程组的控制;
  • 让进程摆脱原控制终端的控制;

总之,就是让调用进程完全独立出来,脱离所有其他进程的控制。

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

现在,第一子进程已经成为五中断的会话组长,但他可以重新申请打开一个控制终端。为了避免第一子进程重新申请控制终端,使进程不再是会话组长:fork一个第二子进程,结束第一子进程,此时的第二子进程不是会话组长。

4. 把当前工作目录切换到根目录。

如果我们是在一个临时加载的文件系统上执行这个进程的,比如:/mnt/floppy/,该进程的当前工作目录就会是/mnt/floppy/。在整个进程运行期间该文件系统都无法被卸下(umount),而无论我们是否在使用这个文件系统,这会给我们带来很多不便。解决的方法是使用chdir系统调用把当前工作目录变为根目录,应该不会有人想把根目录卸下吧。

当然,在这一步里,如果有特殊的需要,我们也可以把当前工作目录换成其他的路径,比如/tmp。

5. 将文件权限掩码设为0。

这需要调用系统调用umask。每个进程都会从父进程那里继承一个文件权限掩码,当创建新文件时,这个掩码被用于设定文件的默认访问权限,屏蔽掉某些权限,如一般用户的写权限。当另一个进程用exec调用我们编写的daemon程序时,由于我们不知道那个进程的文件权限掩码是什么,这样在我们创建新文件时,就会带来一些麻烦。所以,我们应该重新设置文件权限掩码,我们可以设成任何我们想要的值,但一般情况下,大家都把它设为0,这样,它就不会屏蔽用户的任何操作。

如果你的应用程序根本就不涉及创建新文件或是文件访问权限的设定,你也完全可以把文件权限掩码一脚踢开,跳过这一步。

6. 关闭所有不需要的文件。

同文件权限掩码一样,我们的新进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不被我们的daemon进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。需要指出的是,文件描述符为0、1和2的三个文件(文件描述符的概念将在下一章介绍),也就是我们常说的输入、输出和报错这三个文件也需要被关闭。很可能不少读者会对此感到奇怪,难道我们不需要输入输出吗?但事实是,在上面的第2步后,我们的daemon进程已经与所属的控制终端失去了联系,我们从终端输入的字符不可能达到daemon进程,daemon进程用常规的方法(如printf)输出的字符也不可能在我们的终端上显示出来。所以这三个文件已经失去了存在的价值,也应该被关闭。

7. 处理SIGCHLD信号

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

代码如下:

class DaemonBase{

    private $pid_file="";
    private $pid_dir="/home/daisyfan/";
    private $work_count=8;

    public function __construct($is_sington=false,$user='nobody',$output="/home/daisyfan/")
    {

        $this->is_sington = $is_sington; //是否单例运行,单例运行会在tmp目录下建立一个唯一的PID
        $this->user = $user;//设置运行的用户 默认情况下nobody
        $this->output = $output; //设置输出的地方
        $this->checkPcntl();
    }

        /**
     * 启动守护进程
     */
    public function daemonize(){

        set_time_limit(0);
        //调用fork产生一个子进程,同时父进程退出
        $pid=pcntl_fork();
        if($pid==-1){
            exit("fork子进程出错");
        }else if($pid>0){
            exit(0); //父进程退出
        }
        posix_setsid();//将第一子进程设为会话首进程
        $pid=pcntl_fork();
        if($pid==-1){
            exit("fork第二子进程出错");
        }elseif($pid>0){
            exit(0);//第一子进程退出
        }
        chdir("/home/daisyfan/");//改变工作目录
        umask(0);//把文件掩码清零

        $this->setUser($this->user) or die("cannot change owner");

        //关闭打开的文件描述符
        fclose(STDIN);
        fclose(STDOUT);
        fclose(STDERR);

        $stdin  = fopen($this->output, 'r');
        $stdout = fopen($this->output, 'a');
        $stderr = fopen($this->output, 'a');

        if ($this->is_sington==true){
            $this->createPidfile();
        }
    }

    /**
     * 处理信号
     */
    public function checkPcntl(){
        // PHP < 5.3 uses ticks to handle signals instead of pcntl_signal_dispatch
        // call sighandler only every 10 ticks
        if(!function_exists('pcntl_signal_dispatch')){
            declare(ticks=10);
        }
        if(!function_exists('pcntl_signal')){
            $message = 'PHP does not appear to be compiled with the PCNTL extension.  This is neccesary for daemonization';
            $this->_log($message);
            throw new Exception($message);
        }
        pcntl_signal(SIGTEM,array($this,'signalHandler'),false);//安装信号处理器
        pcntl_signal(SIGINT,array($this,'signalHandler'),false);
        pcntl_signal(SIGQUIT,array($this,'signalHandler'),false);

    }

    //信号处理函数
    public function signalHandler($signal){
        switch($signal){
            //用户自定义信号
            case SIGUSR1: //busy
                if ($this->workers_count < $this->workers_max){
                    $pid = pcntl_fork();
                    if ($pid > 0){
                        $this->workers_count ++;
                    }
                }
                break;
            //子进程结束信号
            case SIGCHLD:
                while(($pid=pcntl_waitpid(-1, $status, WNOHANG)) > 0){
                    $this->workers_count --;
                }
                break;
            //中断进程
            case SIGTERM:
            case SIGHUP:
            case SIGQUIT:
                break;
            default:
                return false;
        }

    }

    //检测pid file是否存在
    public function checkPID(){
        if(!file_exists($this->pid_file)){
            return true;
        }

        $pid=file_get_contents($this->pid_file);
        $pid=intval($pid);
        if($pid>0 && posix_kill($pid,0) ){//posix_kill — Send a signal to a process
            $this->_log("the daemon process is already started");
        }else{
            $this->_log("the daemon process end abnormally, please check the pid file".$this->pid_file);
        }

        exit(1);
    }

    //创建pid file
    public function createPidfile(){
       if(! is_dir($this->pid_dir)){
           mkdir($this->pid_dir);
       }
        $fp=fopen($this->pid_file,'w') or exit('can not create pid file.');
        fwrite($fp,posix_getpid());
        fclose($fp);
        $this->_log("create pid file:".$this->pid_file);
    }

    public function setUser($name){
        $result=false;
        if(empty($name)){
            return true;
        }
        $user=posix_getpwnam($name);// Return info about a user by username
        if($user){
            $uid=$user['uid'];
            $gid=$user['gid'];
            $result=posix_setuid($uid);
            posix_setgid($gid);
        }
        return $result;
    }

    public function start($count=1){
        $this->_log("Daemon process is running now");

        while(true){
            if(function_exists('pcntl_signal_dispatch')){
                pcntl_signal_dispatch();
            }
            $pid=-1;
            if($this->$work_count<$count){
            $pid=pcntl_fork();
            }
            if($pid>0){
                $this->work_count++;
            }elseif($pid==0){
                // 这个符号表示恢复系统对信号的默认处理
                pcntl_signal(SIGTERM, SIG_DFL);
                pcntl_signal(SIGCHLD, SIG_DFL);
            }
        }
    }


}