在前面的文章《php多進程和多線程的比較》中已經介紹了一些多進程的基礎知識,這篇文章呢,主要是結合實例學習一下,php多進程的用途。文章分為三部分,第一部分介紹多進程用到的一些函數;第二部分介紹一個簡單的多進程示例,第三部分介紹一個利用php多進程的用途——守護進程。
多進程相關函數
1.$pid = pcntl_fork();//創建一個子進程
pcntl_fork 的返回值是一個int值 。如果$pid=-1 ,fork進程失敗 ;如果$pid=0, 當前的上下文環境為worker ; 如果$pid>0 當前的上下文環境為master,這個pid就是fork的worker的pid。
2.$pid = pcntl_wait(
$status
, WNOHANG)// 等待或返回fork的子進程狀態
status
參數上;WNOHANG表示如果沒有子進程退出立刻返回。函數的返回值$pid是一個Int值,如果是子進程退出了,表示子進程號;如果發生錯誤返回-1,如果提供了 WNOHANG
作為option,並且沒有可用子進程時返回0。主要作用是防止產生僵屍進程。子進程結束時,父進程沒有等待它(通過調用wait或者waitpid),那么子進程結束后不會釋放所有資源,這就是僵屍進程。
$pathname
, int $mode
)//創建一個管道(用於子進程之間的通訊,可以想象成文件,但是管道的讀寫都是自動上鎖的,原子性操作)
$filename
, string $mode)//打開管道
多進程示例
多進程的一個典型應用就是可以並行的去處理幾件事情,比如並行的去請求幾個url,並行的去發送郵件等。繼續衍生,可以多個進程去消息隊列里面去取任務來執行,也可以模擬Web服務器處理http請求的操作等。下面是一個簡單的示例:

1 <?php 2 define('NUM',5); 3 4 //先創建管道,1.pipe是管道名 5 if(!posix_mkfifo ( "pipefff" , 0666 )){ 6 die("create 1.pipe error"); 7 } 8 9 for($i=0;$i<NUM;$i++){ 10 $pid = pcntl_fork(); 11 if($pid == -1){ 12 die("fork error"); 13 }else if($pid == 0){//子進程空間 14 sleep(1); 15 echo "子進程ID:".posix_getpid().PHP_EOL; 16 exit(0); 17 }else{//主進程空間 18 echo "父進程ID:".posix_getpid().PHP_EOL; 19 pcntl_wait($status); 20 } 21 } 22 23 unlink("pipefff"); // 刪除管道,已經沒有作用了
示例,簡單演示了多進程的創建和管道的創建,以及何為子進程空間。
一個進一步的例子,用來多進程請求url,可以參考下面的代碼,相比單進程去請求,極大的減少了程序的運行時間(也是因為請求url是I/O密集型的任務,如果是cpu密集型的任務在單核下效果不明顯):

1 <?php 2 class WebServer 3 { 4 private $list; 5 public function __construct() 6 { 7 $this->list = []; 8 } 9 public function worker($request){ 10 $pid = pcntl_fork(); 11 if($pid == -1){ 12 return false; 13 } 14 if($pid > 0){ 15 return $pid; 16 } 17 if($pid == 0){ 18 $time = $request[0]; 19 $method = $request[1]; 20 $start = microtime(true); 21 echo getmypid()."\t start ".$method."\tat".$start.PHP_EOL; 22 //sleep($time); 23 $c = file_get_contents($method); 24 // echo getmypid() ."\n"; 25 $end = microtime(true); 26 $cost = $end-$start; 27 echo getmypid()."\t stop \t".$method."\tat:".$end."\tcost:".$cost.PHP_EOL; 28 exit(0); 29 } 30 } 31 public function master($requests){ 32 $start = microtime(true); 33 echo "All request handle start at ".$start.PHP_EOL; 34 foreach ($requests as $request){ 35 $pid = $this->worker($request); 36 if(!$pid){ 37 echo 'handle fail!'.PHP_EOL; 38 return; 39 } 40 array_push($this->list,$pid); 41 } 42 while(count($this->list)>0){ 43 foreach ($this->list as $k=>$pid){ 44 $res = pcntl_waitpid($pid,$status,WNOHANG); 45 if($res == -1 || $res > 0){ 46 unset($this->list[$k]); 47 } 48 } 49 usleep(100); 50 } 51 $end = microtime(true); 52 $cost = $end - $start; 53 echo "All request handle stop at ".$end."\t cost:".$cost.PHP_EOL; 54 } 55 } 56 57 $requests = [ 58 [1,'http://www.sina.com'], 59 [2,'http://www.sina.com'], 60 [3,'http://www.sina.com'], 61 [4,'http://www.sina.com'], 62 [5,'http://www.sina.com'], 63 [6,'http://www.sina.com'] 64 ]; 65 66 echo "多進程測試:".PHP_EOL; 67 $server = new WebServer(); 68 $server->master($requests); 69 70 echo PHP_EOL."單進程測試:".PHP_EOL; 71 $start = microtime(true); 72 for($i=0;$i<6;$i++){ 73 $c = file_get_contents("http://www.sina.com"); 74 } 75 $end = microtime(true); 76 $cost = $end - $start; 77 echo "All request handle stop at ".$end."\t cost:".$cost.PHP_EOL;
多進程的用途
介紹一個多進程的用途,編寫守護進程。
守護進程是脫離於終端並且在后台運行的進程。守護進程脫離於終端是為了避免進程在執行過程中的信息在任何終端上顯示並且進程也不會被任何終端所產生的終端信息所打斷。

1 <?php 2 $pid = pcntl_fork();//創建子進程 3 if($pid < 0){ 4 die("fork fail"); 5 }else if($pid > 0){ 6 exit();//父進程退出 7 } 8 // 9 if(posix_setsid() === -1){ 10 die("Could not detach");//獲取會話控制權 11 } 12 13 $pid = pcntl_fork();//繼續創建子進程 14 if($pid < 0){ 15 die("fork fail"); 16 }else if($pid > 0){ 17 exit();//父進程退出 18 } 19 echo "可以看到輸出".PHP_EOL; 20 21 //關閉文件描述符 22 @fclose(STDOUT);//關閉標准輸出 23 @fclose(STDERR);//關閉標准出錯 24 $STDOUT = fopen('/dev/null', "a"); 25 $STDERR = fopen('/dev/null', "a"); 26 chdir('/');//改變當前工作目錄 27 umask(0);//清除進程權限 28 echo "不可以看到輸出"; 29 ?>
下面再說明一個例子,利用php守護進程記錄服務器的cpu使用率,內存使用率信息。采用面向對象的方式來編寫。

1 <?php 2 3 /* 4 * 利用php守護進程記錄服務器的cpu使用率,內存使用率信息 5 */ 6 7 class Daemon { 8 9 private $_pidFile; 10 private $_infoDir; 11 private $_logFile = null; 12 private $_jobs = array(); 13 14 /* 15 * 構造函數 16 * $dir 儲存log和pid的絕對路徑 17 */ 18 19 public function __construct($dir = "/tmp",$openLog = false) { 20 $this->_checkPcntl(); 21 $this->_setInfoDir($dir); 22 $this->_pidFile = rtrim($this->_infoDir, "/") . "/" . __CLASS__ . "_pid.log"; 23 if($openLog == true){ 24 $this->_logFile = rtrim($this->_infoDir, "/") . "/" . __CLASS__ . "_pid_log.log"; 25 file_put_contents($this->_logFile, "",FILE_APPEND); 26 } 27 } 28 29 private function _checkPcntl() {//檢查是否開啟pcntl模塊 30 !function_exists('pcntl_signal') && die('Error:Need PHP Pcntl extension!'); 31 } 32 33 private function _setInfoDir($dir = null) {//設置信息儲存的目錄 34 if (is_dir($dir)) { 35 $this->_infoDir = $dir; 36 } else { 37 $this->_infoDir = __DIR__; 38 } 39 } 40 41 private function _getPid() {//獲取pid 42 if (!file_exists($this->_pidFile)) { 43 return 0; 44 } 45 $pid = intval(file_get_contents($this->_pidFile)); 46 if (posix_kill($pid, SIG_DFL)) {//給進程發一個信號 47 return $pid; 48 } else { 49 unlink($this->_pidFile); 50 return 0; 51 } 52 } 53 54 private function _message($message) {//輸出相關信息 55 printf("%s %d %d %s" . PHP_EOL, date("Y-m-d H:i:s"), posix_getppid(), posix_getpid(), $message); 56 if ($this->_logFile != null) { 57 file_put_contents($this->_logFile, date("Y-m-d H:i:s") . " " . posix_getppid() . " " . posix_getpid() . " " . $message . PHP_EOL, FILE_APPEND); 58 } 59 } 60 61 private function daemonize() {//創建守護程序 62 if (!preg_match("/cli/i", php_sapi_name())) { 63 $this->_message("'Should run in CLI"); 64 die(); 65 } 66 $pid = pcntl_fork(); 67 if ($pid < 0) { 68 $this->_message("Fork fail"); 69 die(); 70 } elseif ($pid > 0) { 71 exit(0); 72 } 73 74 if (posix_setsid() === -1) { 75 $this->_message("Could not detach"); 76 die(); //獲取會話控制權 77 } 78 // $pid = pcntl_fork(); 79 // if ($pid < 0) { 80 // $this->_message("Fork fail"); 81 // die(); 82 // } elseif ($pid > 0) { 83 // exit(0); 84 // } 85 // fclose(STDIN); 86 // fclose(STDOUT); 87 // fclose(STDERR); 88 chdir("/"); 89 umask(0); 90 91 $fp = fopen($this->_pidFile, 'w') or die("Can't create pid file"); 92 fwrite($fp, posix_getpid()); 93 fclose($fp); 94 95 //進入守護進程,處理任務 96 if (!empty($this->_jobs)) { 97 foreach ($this->_jobs as $job) { 98 call_user_func($job["function"], $job["argv"]); 99 } 100 } 101 //file_put_contents("/root/wangli/test/daemon/Daemon_pid_log.log", "qqqqqqqqqqq"); 102 return; 103 } 104 105 private function start() {//程序啟動 106 //1.啟動前判斷是否已經存在一個進程 107 //2.沒有則創建一個守護進程 108 if ($this->_getPid() > 0) { 109 $this->_message("Is Running"); 110 exit(); 111 } 112 $this->daemonize(); 113 $this->_message('Start'); 114 } 115 116 private function stop() {//停止守護進程 117 $pid = $this->_getPid(); 118 if ($pid > 0) { 119 posix_kill($pid, SIGTERM); 120 unlink($this->_pidFile); 121 $this->_message("Stopped"); 122 } else { 123 $this->_message("Not Running"); 124 } 125 } 126 127 private function status() { 128 if ($this->_getPid() > 0) { 129 $this->_message('Is Running'); 130 } else { 131 $this->_message('Not Running'); 132 } 133 } 134 135 public function main($argv) {//程序控制台 136 $param = is_array($argv) && count($argv) == 2 ? $argv[1] : null; 137 switch ($param) { 138 case 'start' : 139 $this->start(); 140 break; 141 case 'stop': 142 $this->stop(); 143 break; 144 case 'status': 145 $this->status(); 146 break; 147 default : 148 echo "Argv start|stop|status" . PHP_EOL; 149 } 150 } 151 152 public function addJobs($jobs) { 153 if (!isset($jobs['function']) || empty($jobs['function'])) { 154 $this->_message('Need function param'); 155 } 156 157 if (!isset($jobs['argv']) || empty($jobs['argv'])) { 158 $jobs['argv'] = ""; 159 } 160 $this->_jobs[] = $jobs; 161 } 162 163 } 164 165 $daemon = new Daemon(__FILE__,true); 166 $daemon->addJobs(array( 167 'function' => 'recordServerData', 168 'argv' => 'GOGOGO' 169 )); 170 171 $daemon->main($argv); 172 173 174 function recordServerData($param) { 175 $i = 0; 176 while (true) { 177 $status=get_used_status(); //獲取服務器情況的函數 178 file_put_contents('/root/wangli/test/daemon/server.log', $status["detection_time"]." cpu_usage:".$status["cpu_usage"].PHP_EOL, FILE_APPEND); 179 sleep(5); 180 } 181 } 182 183 184 function get_used_status() { 185 $fp = popen('top -b -n 2 | grep -E "^(Cpu|Mem|Tasks)"', "r"); //獲取某一時刻系統cpu和內存使用情況 186 $rs = ""; 187 while (!feof($fp)) { 188 $rs .= fread($fp, 1024); 189 } 190 pclose($fp); 191 $sys_info = explode("\n", $rs); 192 193 $tast_info = explode(",", $sys_info[3]); //進程 數組 194 $cpu_info = explode(",", $sys_info[4]); //CPU占有量 數組 195 $mem_info = explode(",", $sys_info[5]); //內存占有量 數組 196 //正在運行的進程數 197 $tast_running = trim(trim($tast_info[1], 'running')); 198 199 200 //CPU占有量 201 $cpu_usage = trim(trim($cpu_info[0], 'Cpu(s): '), '%us'); //百分比 202 //內存占有量 203 $mem_total = trim(trim($mem_info[0], 'Mem: '), 'k total'); 204 $mem_used = trim($mem_info[1], 'k used'); 205 $mem_usage = round(100 * intval($mem_used) / intval($mem_total), 2); //百分比 206 207 208 209 $fp = popen('df -lh | grep -E "^(/)"', "r"); 210 $rs = fread($fp, 1024); 211 pclose($fp); 212 $rs = preg_replace("/\s{2,}/", ' ', $rs); //把多個空格換成 “_” 213 $hd = explode(" ", $rs); 214 $hd_avail = trim($hd[3], 'G'); //磁盤可用空間大小 單位G 215 $hd_usage = trim($hd[4], '%'); //掛載點 百分比 216 //print_r($hd); 217 //檢測時間 218 $fp = popen('date +"%Y-%m-%d %H:%M"', "r"); 219 $rs = fread($fp, 1024); 220 pclose($fp); 221 $detection_time = trim($rs); 222 223 224 return array('cpu_usage' => $cpu_usage, 'mem_usage' => $mem_usage, 'hd_avail' => $hd_avail, 'hd_usage' => $hd_usage, 'tast_running' => $tast_running, 'detection_time' => $detection_time); 225 } 226 ?>