守護進程(Daemon)是運行在後台的一種特殊進程。它獨立於控制終端並且周期性地執行某種任務或等待處理某些發生的事件。守護進程是一種很有用的進程。php也可以實現守護進程的功能。
1、基本概念
進程
每個進程都有一個父進程,子進程退出,父進程能得到子進程退出的狀態。
進程組
每個進程都屬於一個進程組,每個進程組都有一個進程組號,該號等於該進程組組長的PID
2、守護編程要點
守護進程(Daemon)是運行在後台的一種特殊進程。它獨立於控制終端並且周期性地執行某種任務或等待處理某些發生的事件。守護進程是一種很有用的進程。php也可以實現守護進程的功能。
1、基本概念
進程
每個進程都有一個父進程,子進程退出,父進程能得到子進程退出的狀態。
進程組
每個進程都屬於一個進程組,每個進程組都有一個進程組號,該號等於該進程組組長的PID
2、守護編程要點
1. 在後台運行。
為避免掛起控制終端將Daemon放入後台執行。方法是在進程中調用fork使父進程終止,讓Daemon在子進程中後台執行。 if($pid=pcntl_fork()) exit(0);//是父進程,結束父進程,子進程繼續
2. 脫離控制終端,登錄會話和進程組
有必要先介紹一下Linux中的進程與控制終端,登錄會話和進程組之間的關系:進程屬於一個進程組,進程組號(GID)就是進程組長的進程號(PID)。登錄會話可以包含多個進程組。這些進程組共享一個控制終端。這個控制終端通常是創建進程的登錄終 端。 控制終端,登錄會話和進程組通常是從父進程繼承下來的。我們的目的就是要擺脫它們,使之不受它們的影響。方法是在第1點的基礎上,調用setsid()使進程成為會話組長: posix_setsid();
說明:當進程是會話組長時setsid()調用失敗。但第一點已經保證進程不是會話組長。setsid()調用成功後,進程成為新的會話組長和新的進程組長,並與原來的登錄會話和進程組脫離。由於會話過程對控制終端的獨占性,進程同時與控制終端脫離。
3. 禁止進程重新打開控制終端
現在,進程已經成為無終端的會話組長。但它可以重新申請打開一個控制終端。可以通過使進程不再成為會話組長來禁止進程重新打開控制終端: if($pid=pcntl_fork()) exit(0);//結束第一子進程,第二子進程繼續(第二子進程不再是會話組長)
4. 關閉打開的文件描述符
進程從創建它的父進程那裡繼承了打開的文件描述符。如不關閉,將會浪費系統資源,造成進程所在的文件系統無法卸下以及引起無法預料的錯誤。按如下方法關閉它們:
fclose(STDIN),fclose(STDOUT),fclose(STDERR)關閉標准輸入輸出與錯誤顯示。
5. 改變當前工作目錄
進程活動時,其工作目錄所在的文件系統不能卸下。一般需要將工作目錄改變到根目錄。對於需要轉儲核心,寫運行日志的進程將工作目錄改變到特定目錄如chdir("/")
6. 重設文件創建掩模
進程從創建它的父進程那裡繼承了文件創建掩模。它可能修改守護進程所創建的文件的存取位。為防止這一點,將文件創建掩模清除:umask(0);
7. 處理SIGCHLD信號
處理SIGCHLD信號並不是必須的。但對於某些進程,特別是服務器進程往往在請求到來時生成子進程處理請求。如果父進程不等待子進程結束,子進程將成為僵屍進程(zombie)從而占用系統資源。如果父進程等待子進程結束,將增加父進程的負擔,影 響服務器進程的並發性能。在Linux下可以簡單地將SIGCHLD信號的操作設為SIG_IGN。 signal(SIGCHLD,SIG_IGN);
這樣,內核在子進程結束時不會產生僵屍進程。這一點與BSD4不同,BSD4下必須顯式等待子進程結束才能釋放僵屍進程。關於信號的問題請參考Linux 信號說明列表
3、實例
<?php /** *@author [email protected] *@date 2013-07-25 * 後台腳本控制類 */ class DaemonCommand{ private $info_dir="/tmp"; private $pid_file=""; private $terminate=false; //是否中斷 private $workers_count=0; private $gc_enabled=null; private $workers_max=8; //最多運行8個進程 public function __construct($is_sington=false,$user='nobody',$output="/dev/null"){ $this->is_sington=$is_sington; //是否單例運行,單例運行會在tmp目錄下建立一個唯一的PID $this->user=$user;//設置運行的用戶 默認情況下nobody $this->output=$output; //設置輸出的地方 $this->checkPcntl(); } //檢查環境是否支持pcntl支持 public function checkPcntl(){ if ( ! function_exists('pcntl_signal_dispatch')) { // PHP < 5.3 uses ticks to handle signals instead of pcntl_signal_dispatch // call sighandler only every 10 ticks declare(ticks = 10); } // Make sure PHP has support for pcntl 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(SIGTERM, array(__CLASS__, "signalHandler"),false); pcntl_signal(SIGINT, array(__CLASS__, "signalHandler"),false); pcntl_signal(SIGQUIT, array(__CLASS__, "signalHandler"),false); // Enable PHP 5.3 garbage collection if (function_exists('gc_enable')) { gc_enable(); $this->gc_enabled = gc_enabled(); } } // daemon化程序 public function daemonize(){ global $stdin, $stdout, $stderr; global $argv; set_time_limit(0); // 只允許在cli下面運行 if (php_sapi_name() != "cli"){ die("only run in command line mode\n"); } // 只能單例運行 if ($this->is_sington==true){ $this->pid_file = $this->info_dir . "/" .__CLASS__ . "_" . substr(basename($argv[0]), 0, -4) . ".pid"; $this->checkPidfile(); } umask(0); //把文件掩碼清0 if (pcntl_fork() != 0){ //是父進程,父進程退出 exit(); } posix_setsid();//設置新會話組長,脫離終端 if (pcntl_fork() != 0){ //是第一子進程,結束第一子進程 exit(); } chdir("/"); //改變工作目錄 $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(); } } //--檢測pid是否已經存在 public function checkPidfile(){ 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)){ $this->_log("the daemon process is already started"); } else { $this->_log("the daemon proces end abnormally, please check pidfile " . $this->pid_file); } exit(1); } //----創建pid public function createPidfile(){ if (!is_dir($this->info_dir)){ mkdir($this->info_dir); } $fp = fopen($this->pid_file, 'w') or die("cannot 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); if ($user) { $uid = $user['uid']; $gid = $user['gid']; $result = posix_setuid($uid); posix_setgid($gid); } return $result; } //信號處理函數 public function signalHandler($signo){ switch($signo){ //用戶自定義信號 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: $this->terminate = true; break; default: return false; } } /** *開始開啟進程 *$count 准備開啟的進程數 */ public function start($count=1){ $this->_log("daemon process is running now"); pcntl_signal(SIGCHLD, array(__CLASS__, "signalHandler"),false); // if worker die, minus children num while (true) { if (function_exists('pcntl_signal_dispatch')){ pcntl_signal_dispatch(); } if ($this->terminate){ break; } $pid=-1; if($this->workers_count<$count){ $pid=pcntl_fork(); } if($pid>0){ $this->workers_count++; }elseif($pid==0){ // 這個符號表示恢復系統對信號的默認處理 pcntl_signal(SIGTERM, SIG_DFL); pcntl_signal(SIGCHLD, SIG_DFL); if(!empty($this->jobs)){ while($this->jobs['runtime']){ if(empty($this->jobs['argv'])){ call_user_func($this->jobs['function'],$this->jobs['argv']); }else{ call_user_func($this->jobs['function']); } $this->jobs['runtime']--; sleep(2); } exit(); } return; }else{ sleep(2); } } $this->mainQuit(); exit(0); } //整個進程退出 public function mainQuit(){ if (file_exists($this->pid_file)){ unlink($this->pid_file); $this->_log("delete pid file " . $this->pid_file); } $this->_log("daemon process exit now"); posix_kill(0, SIGKILL); exit(0); } // 添加工作實例,目前只支持單個job工作 public function setJobs($jobs=array()){ if(!isset($jobs['argv'])||empty($jobs['argv'])){ $jobs['argv']=""; } if(!isset($jobs['runtime'])||empty($jobs['runtime'])){ $jobs['runtime']=1; } if(!isset($jobs['function'])||empty($jobs['function'])){ $this->log("你必須添加運行的函數!"); } $this->jobs=$jobs; } //日志處理 private function _log($message){ printf("%s\t%d\t%d\t%s\n", date("c"), posix_getpid(), posix_getppid(), $message); } } //調用方法1 $daemon=new DaemonCommand(true); $daemon->daemonize(); $daemon->start(2);//開啟2個子進程工作 work(); //調用方法2 $daemon=new DaemonCommand(true); $daemon->daemonize(); $daemon->addJobs(array('function'=>'work','argv'=>'','runtime'=>1000));//function 要運行的函數,argv運行函數的參數,runtime運行的次數 $daemon->start(2);//開啟2個子進程工作 //具體功能的實現 function work(){ echo "測試1"; } ?> <?php /** *@author [email protected] *@date 2013-07-25 * 後台腳本控制類 */ class DaemonCommand{ private $info_dir="/tmp"; private $pid_file=""; private $terminate=false; //是否中斷 private $workers_count=0; private $gc_enabled=null; private $workers_max=8; //最多運行8個進程 public function __construct($is_sington=false,$user='nobody',$output="/dev/null"){ $this->is_sington=$is_sington; //是否單例運行,單例運行會在tmp目錄下建立一個唯一的PID $this->user=$user;//設置運行的用戶 默認情況下nobody $this->output=$output; //設置輸出的地方 $this->checkPcntl(); } //檢查環境是否支持pcntl支持 public function checkPcntl(){ if ( ! function_exists('pcntl_signal_dispatch')) { // PHP < 5.3 uses ticks to handle signals instead of pcntl_signal_dispatch // call sighandler only every 10 ticks declare(ticks = 10); } // Make sure PHP has support for pcntl 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(SIGTERM, array(__CLASS__, "signalHandler"),false); pcntl_signal(SIGINT, array(__CLASS__, "signalHandler"),false); pcntl_signal(SIGQUIT, array(__CLASS__, "signalHandler"),false); // Enable PHP 5.3 garbage collection if (function_exists('gc_enable')) { gc_enable(); $this->gc_enabled = gc_enabled(); } } // daemon化程序 public function daemonize(){ global $stdin, $stdout, $stderr; global $argv; set_time_limit(0); // 只允許在cli下面運行 if (php_sapi_name() != "cli"){ die("only run in command line mode\n"); } // 只能單例運行 if ($this->is_sington==true){ $this->pid_file = $this->info_dir . "/" .__CLASS__ . "_" . substr(basename($argv[0]), 0, -4) . ".pid"; $this->checkPidfile(); } umask(0); //把文件掩碼清0 if (pcntl_fork() != 0){ //是父進程,父進程退出 exit(); } posix_setsid();//設置新會話組長,脫離終端 if (pcntl_fork() != 0){ //是第一子進程,結束第一子進程 exit(); } chdir("/"); //改變工作目錄 $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(); } } //--檢測pid是否已經存在 public function checkPidfile(){ 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)){ $this->_log("the daemon process is already started"); } else { $this->_log("the daemon proces end abnormally, please check pidfile " . $this->pid_file); } exit(1); } //----創建pid public function createPidfile(){ if (!is_dir($this->info_dir)){ mkdir($this->info_dir); } $fp = fopen($this->pid_file, 'w') or die("cannot 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); if ($user) { $uid = $user['uid']; $gid = $user['gid']; $result = posix_setuid($uid); posix_setgid($gid); } return $result; } //信號處理函數 public function signalHandler($signo){ switch($signo){ //用戶自定義信號 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: $this->terminate = true; break; default: return false; } } /** *開始開啟進程 *$count 准備開啟的進程數 */ public function start($count=1){ $this->_log("daemon process is running now"); pcntl_signal(SIGCHLD, array(__CLASS__, "signalHandler"),false); // if worker die, minus children num while (true) { if (function_exists('pcntl_signal_dispatch')){ pcntl_signal_dispatch(); } if ($this->terminate){ break; } $pid=-1; if($this->workers_count<$count){ $pid=pcntl_fork(); } if($pid>0){ $this->workers_count++; }elseif($pid==0){ // 這個符號表示恢復系統對信號的默認處理 pcntl_signal(SIGTERM, SIG_DFL); pcntl_signal(SIGCHLD, SIG_DFL); if(!empty($this->jobs)){ while($this->jobs['runtime']){ if(empty($this->jobs['argv'])){ call_user_func($this->jobs['function'],$this->jobs['argv']); }else{ call_user_func($this->jobs['function']); } $this->jobs['runtime']--; sleep(2); } exit(); } return; }else{ sleep(2); } } $this->mainQuit(); exit(0); } //整個進程退出 public function mainQuit(){ if (file_exists($this->pid_file)){ unlink($this->pid_file); $this->_log("delete pid file " . $this->pid_file); } $this->_log("daemon process exit now"); posix_kill(0, SIGKILL); exit(0); } // 添加工作實例,目前只支持單個job工作 public function setJobs($jobs=array()){ if(!isset($jobs['argv'])||empty($jobs['argv'])){ $jobs['argv']=""; } if(!isset($jobs['runtime'])||empty($jobs['runtime'])){ $jobs['runtime']=1; } if(!isset($jobs['function'])||empty($jobs['function'])){ $this->log("你必須添加運行的函數!"); } $this->jobs=$jobs; } //日志處理 private function _log($message){ printf("%s\t%d\t%d\t%s\n", date("c"), posix_getpid(), posix_getppid(), $message); } } //調用方法1 $daemon=new DaemonCommand(true); $daemon->daemonize(); $daemon->start(2);//開啟2個子進程工作 work(); //調用方法2 $daemon=new DaemonCommand(true); $daemon->daemonize(); $daemon->addJobs(array('function'=>'work','argv'=>'','runtime'=>1000));//function 要運行的函數,argv運行函數的參數,runtime運行的次數 $daemon->start(2);//開啟2個子進程工作 //具體功能的實現 function work(){ echo "測試1"; } ?>