PHP多進程編程

使用PHP真正的多進程運行模式,適用於數據採集、郵件羣發、數據源更新、tcp服務器等環節。php

PHP有一組進程控制函數(編譯時須要 –enable-pcntl與posix擴展),使得php能在*nix系統中實現跟c同樣的建立子進程、使用exec函數執行程序、處理信號等功能。 PCNTL使用ticks來做爲信號處理機制(signal handle callback mechanism),能夠最小程度地下降處理異步事件時的負載。何謂ticks?Tick 是一個在代碼段中解釋器每執行 N 條低級語句就會發生的事件,這個代碼段須要經過declare來指定。linux

經常使用的PCNTL函數
1. pcntl_alarm ( int $seconds )
設置一個$seconds秒後發送SIGALRM信號的計數器數據庫

2. pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls ] )
爲$signo設置一個處理該信號的回調函數。下面是一個隔5秒發送一個SIGALRM信號,並由signal_handler函數獲取,而後打印一個「Caught SIGALRM」的例子:編程

1數組

2服務器

3數據結構

4多線程

5併發

6異步

7

8

9

10

11

12

13

14

15

<?php 

declare(ticks = 1); 

 

function signal_handler($signal) { 

    print "Caught SIGALRM\n"

    pcntl_alarm(5); 

 

pcntl_signal(SIGALRM, "signal_handler", true); 

pcntl_alarm(5); 

 

for(;;) { 

 

?>

3. pcntl_exec ( string $path [, array $args [, array $envs ]] )
在當前的進程空間中執行指定程序,相似於c中的exec族函數。所謂當前空間,即載入指定程序的代碼覆蓋掉當前進程的空間,執行完該程序進程即結束。

1

2

3

4

5

6

7

8

9

10

11

<?php 

$dir = '/home/shankka/'

$cmd = 'ls'

$option = '-l'

$pathtobin = '/bin/ls'

 

$arg = array($cmd, $option, $dir); 

 

pcntl_exec($pathtobin, $arg); 

echo '123';    //不會執行到該行 

?>

4. pcntl_fork ( void )
爲當前進程建立一個子進程,而且先運行父進程,返回的是子進程的PID,確定大於零。在父進程的代碼中能夠用 pcntl_wait(&$status)暫停父進程知道他的子進程有返回值。注意:父進程的阻塞同時會阻塞子進程。可是父進程的結束不影響子進程的運行。
父進程運行完了會接着運行子進程,這時子進程會從執行pcntl_fork()的那條語句開始執行(包括此函數),可是此時它返回的是零(表明這是一個子進程)。在子進程的代碼塊中最好有exit語句,即執行完子進程後當即就結束。不然它會又重頭開始執行這個腳本的某些部分。

注意兩點:
1. 子進程最好有一個exit;語句,防止沒必要要的出錯;
2. pcntl_fork間最好不要有其它語句,例如:

1

2

3

4

5

6

7

8

9

10

11

12

<?php

$pid = pcntl_fork();

//這裏最好不要有其餘的語句

if ($pid == -1) {

    die('could not fork');

} else if ($pid) {

    // we are the parent

pcntl_wait($status); //Protect against Zombie children

} else {

    // we are the child

}

?>

5. pcntl_wait ( int &$status [, int $options ] )
阻塞當前進程,只到當前進程的一個子進程退出或者收到一個結束當前進程的信號。使用$status返回子進程的狀態碼,並能夠指定第二個參數來講明是否以阻塞狀態調用:
1. 阻塞方式調用的,函數返回值爲子進程的pid,若是沒有子進程返回值爲-1;
2. 非阻塞方式調用,函數還能夠在有子進程在運行但沒有結束的子進程時返回0。

6. pcntl_waitpid ( int $pid , int &$status [, int $options ] )
功能同pcntl_wait,區別爲waitpid爲等待指定pid的子進程。當pid爲-1時pcntl_waitpid與pcntl_wait 同樣。在pcntl_wait和pcntl_waitpid兩個函數中的$status中存了子進程的狀態信息,這個參數能夠用於 pcntl_wifexited、pcntl_wifstopped、pcntl_wifsignaled、pcntl_wexitstatus、 pcntl_wtermsig、pcntl_wstopsig、pcntl_waitpid這些函數。
例如:

1

2

3

4

5

6

7

8

9

10

11

12

<?php 

$pid = pcntl_fork(); 

if($pid) { 

    pcntl_wait($status); 

    $id = getmypid(); 

    echo "parent process,pid {$id}, child pid {$pid}\n"

}else

    $id = getmypid(); 

    echo "child process,pid {$id}\n"

    sleep(2); 

?>

子進程在輸出child process等字樣以後sleep了2秒才結束,而父進程阻塞着直到子進程退出以後才繼續運行。

7. pcntl_getpriority ([ int $pid [, int $process_identifier ]] )
取得進程的優先級,即nice值,默認爲0,在個人測試環境的linux中(CentOS release 5.2 (Final)),優先級爲-20到19,-20爲優先級最高,19爲最低。(手冊中爲-20到20)。

8. pcntl_setpriority ( int $priority [, int $pid [, int $process_identifier ]] )
設置進程的優先級。

9. posix_kill
能夠給進程發送信號

10. pcntl_singal
用來設置信號的回調函數

當父進程退出時,子進程如何得知父進程的退出
當父進程退出時,子進程通常能夠經過下面這兩個比較簡單的方法得知父進程已經退出這個消息:

1. 當父進程退出時,會有一個INIT進程來領養這個子進程。這個INIT進程的進程號爲1,因此子進程能夠經過使用getppid()來取得當前父進程的pid。若是返回的是1,代表父進程已經變爲INIT進程,則原進程已經推出。
2. 使用kill函數,向原有的父進程發送空信號(kill(pid, 0))。使用這個方法對某個進程的存在性進行檢查,而不會真的發送信號。因此,若是這個函數返回-1表示父進程已經退出。

除了上面的這兩個方法外,還有一些實現上比較複雜的方法,好比創建管道或socket來進行時時的監控等等。

PHP多進程採集數據的例子

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

<?php

/**

* Project: Signfork: php多線程庫

* File:    Signfork.class.php

*/

 

class Signfork{

  /**

   * 設置子進程通訊文件所在目錄

   * @var string

   */

  private $tmp_path='/tmp/';

 

/**

  * Signfork引擎主啓動方法

  * 一、判斷$arg類型,類型爲數組時將值傳遞給每一個子進程;類型爲數值型時,表明要建立的進程數.

  * @param object $obj 執行對象

  * @param string&#124;array $arg 用於對象中的__fork方法所執行的參數

  * 如:$arg,自動分解爲:$obj->__fork($arg[0])、$obj->__fork($arg[1])...

  * @return array  返回   array(子進程序列=>子進程執行結果);

  */

  public function run($obj,$arg=1){

    if(!method_exists($obj,'__fork')){

      exit("Method '__fork' not found!");

    }

 

    if(is_array($arg)){

     $i=0;

     foreach($arg as $key=>$val){

       $spawns[$i]=$key;

       $i++;

       $this->spawn($obj,$key,$val);

     }

     $spawns['total']=$i;

    }elseif($spawns=intval($arg)){

      for($i = 0; $i < $spawns; $i++){

        $this->spawn($obj,$i);

      }

    }else{

      exit('Bad argument!');

    }

 

   if($i>1000) exit('Too many spawns!');

      return $this->request($spawns);

   }

 

  /**

   * Signfork主進程控制方法

   * 一、$tmpfile 判斷子進程文件是否存在,存在則子進程執行完畢,並讀取內容

   * 二、$data收集子進程運行結果及數據,並用於最終返回

   * 三、刪除子進程文件

   * 四、輪詢一次0.03秒,直到全部子進程執行完畢,清理子進程資源

   * @param  string&#124;array $arg 用於對應每一個子進程的ID

   * @return array  返回   array([子進程序列]=>[子進程執行結果]);

   */

   private function request($spawns){

     $data=array();

     $i=is_array($spawns)?$spawns['total']:$spawns;

     for($ids = 0; $ids<$i; $ids++){

       while(!($cid=pcntl_waitpid(-1, $status, WNOHANG)))usleep(30000);

       $tmpfile=$this->tmp_path.'sfpid_'.$cid;

       $data[$spawns['total']?$spawns[$ids]:$ids]=file_get_contents($tmpfile);

       unlink($tmpfile);

     }

     return $data;

   }

 

/**

  * Signfork子進程執行方法

  * 一、pcntl_fork 生成子進程

  * 二、file_put_contents 將'$obj->__fork($val)'的執行結果存入特定序列命名的文本

  * 三、posix_kill殺死當前進程

  * @param object $obj        待執行的對象

  * @param object $i                子進程的序列ID,以便於返回對應每一個子進程數據

  * @param object $param 用於輸入對象$obj方法'__fork'執行參數

  */

  private function spawn($obj,$i,$param=null){

    if(pcntl_fork()===0){

      $cid=getmypid();

      file_put_contents($this->tmp_path.'sfpid_'.$cid,$obj->__fork($param));

      posix_kill($cid, SIGTERM);

      exit;

    }

  }

}

?>

php在pcntl_fork()後生成的子進程(一般爲殭屍進程)必須由pcntl_waitpid()函數進行資源釋放。但在 pcntl_waitpid()不必定釋放的就是當前運行的進程,也多是過去生成的殭屍進程(沒有釋放);也多是併發時其它訪問者的殭屍進程。但可使用posix_kill($cid, SIGTERM)在子進程結束時殺掉它。

子進程會自動複製父進程空間裏的變量。

PHP多進程編程示例2

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

<?php

//.....

//須要安裝pcntl的php擴展,並加載它

if(function_exists("pcntl_fork")){

   //生成子進程

  $pid = pcntl_fork();

  if($pid == -1){

    die('could not fork');

  }else{

    if($pid){

      $status = 0;

      //阻塞父進程,直到子進程結束,不適合須要長時間運行的腳本,可以使用pcntl_wait($status, 0)實現非阻塞式

      pcntl_wait($status);

      // parent proc code

      exit;

    }else{

      // child proc code

      //結束當前子進程,以防止生成殭屍進程

      if(function_exists("posix_kill")){

        posix_kill(getmypid(), SIGTERM);

      }else{

        system('kill -9'. getmypid());

      }

      exit;

    }

  }

}else{

   // 不支持多進程處理時的代碼在這裏

}

//.....

?>

若是不須要阻塞進程,而又想獲得子進程的退出狀態,則能夠註釋掉pcntl_wait($status)語句,或寫成:

1

2

3

4

5

<?php

pcntl_wait($status, 1);

//或

pcntl_wait($status, WNOHANG);

?>

在上面的代碼中,若是父進程退出(使用exit函數退出或redirect),則會致使子進程成爲殭屍進程(會交給init進程控制),子進程再也不執行。

殭屍進程是指的父進程已經退出,而該進程dead以後沒有進程接受,就成爲殭屍進程.(zombie)進程。任何進程在退出前(使用exit退出) 都會變成殭屍進程(用於保存進程的狀態等信息),而後由init進程接管。若是不及時回收殭屍進程,那麼它在系統中就會佔用一個進程表項,若是這種殭屍進程過多,最後系統就沒有能夠用的進程表項,因而也沒法再運行其它的程序。

預防殭屍進程有如下幾種方法:

1. 父進程經過wait和waitpid等函數使其等待子進程結束,而後再執行父進程中的代碼,這會致使父進程掛起。上面的代碼就是使用這種方式實現的,但在WEB環境下,它不適合子進程須要長時間運行的狀況(會致使超時)。
使用wait和waitpid方法使父進程自動回收其殭屍子進程(根據子進程的返回狀態),waitpid用於臨控指定子進程,wait是對於全部子進程而言。
2. 若是父進程很忙,那麼能夠用signal函數爲SIGCHLD安裝handler,由於子進程結束後,父進程會收到該信號,能夠在handler中調用wait回收
3. 若是父進程不關心子進程何時結束,那麼能夠用signal(SIGCHLD, SIG_IGN)通知內核,本身對子進程的結束不感興趣,那麼子進程結束後,內核會回收,並再也不給父進程發送信號,例如:

1

2

3

4

5

<?php

pcntl_signal(SIGCHLD, SIG_IGN);

$pid = pcntl_fork();

//....code

?>

4. 還有一個技巧,就是fork兩次,父進程fork一個子進程,而後繼續工做,子進程再fork一個孫進程後退出,那麼孫進程被init接管,孫進程結束後,init會回收。不過子進程的回收還要本身作。下面是一個例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

#include "apue.h"

#include <sys/wait.h>

 

int main(void){

pid_t    pid;

 

if ((pid = fork()) < 0){

   err_sys("fork error");

} else if (pid == 0){     /**//* first child */

  if ((pid = fork()) < 0){

     err_sys("fork error");

  }elseif(pid > 0){

     exit(0);    /**//* parent from second fork == first child */

  }

 

  /**

   * We're the second child; our parent becomes init as soon

   * as our real parent calls exit() in the statement above.

   * Here's where we'd continue executing, knowing that when

   * we're done, init will reap our status.

   */

   sleep(2);

   printf("second child, parent pid = %d ", getppid());

   exit(0);

}

 

if (waitpid(pid, NULL, 0) != pid)  /**//* wait for first child */

  err_sys("waitpid error");

 

/**

 * We're the parent (the original process); we continue executing,

 * knowing that we're not the parent of the second child.

 */

 exit(0);

}

在fork()/execve()過程當中,假設子進程結束時父進程仍存在,而父進程fork()以前既沒安裝SIGCHLD信號處理函數調用 waitpid()等待子進程結束,又沒有顯式忽略該信號,則子進程成爲殭屍進程,沒法正常結束,此時即便是root身份kill-9也不能殺死殭屍進程。補救辦法是殺死殭屍進程的父進程(殭屍進程的父進程必然存在),殭屍進程成爲」孤兒進程」,過繼給1號進程init,init會按期調用wait回收清理這些父進程已退出的殭屍子進程。

因此,上面的示例能夠改爲:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

<?php

//.....

//須要安裝pcntl的php擴展,並加載它

if(function_exists("pcntl_fork")){

 //生成第一個子進程

$pid = pcntl_fork();  //$pid即所產生的子進程id

if($pid == -1){

  //子進程fork失敗

  die('could not fork');

}else{

  if($pid){

    //父進程code

    sleep(5);  //等待5秒

    exit(0); //或$this->_redirect('/');

  }else{

    //第一個子進程code

    //產生孫進程

    if(($gpid = pcntl_fork()) < 0){ ////$gpid即所產生的孫進程id

      //孫進程產生失敗

      die('could not fork');

    }elseif($gpid > 0){

      //第一個子進程code,即孫進程的父進程

      $status = 0;

      $status = pcntl_wait($status); //阻塞子進程,並返回孫進程的退出狀態,用於檢查是否正常退出

      if($status ! = 0) file_put_content('filename', '孫進程異常退出');

      //獲得父進程id

      //$ppid =  posix_getppid(); //若是$ppid爲1則表示其父進程已變爲init進程,原父進程已退出

      //獲得子進程id:posix_getpid()或getmypid()或是fork返回的變量$pid

      //kill掉子進程

      //posix_kill(getmypid(), SIGTERM);

      exit(0);

    }else{ //即$gpid == 0

      //孫進程code

      //....

      //結束孫進程(即當前進程),以防止生成殭屍進程

      if(function_exists('posix_kill')){

         posix_kill(getmypid(), SIGTERM);

      }else{

         system('kill -9'. getmypid());

      }

      exit(0);

    }

  }

}

}else{

 // 不支持多進程處理時的代碼在這裏

}

//.....

?>

怎樣產生殭屍進程的
一個進程在調用exit命令結束本身的生命的時候,其實它並無真正的被銷燬,而是留下一個稱爲殭屍進程(Zombie)的數據結構(系統調用exit,它的做用是使進程退出,但也僅僅限於將一個正常的進程變成一個殭屍進程,並不能將其徹底銷燬)。在Linux進程的狀態中,殭屍進程是很是特殊的一種,它已經放棄了幾乎全部內存空間,沒有任何可執行代碼,也不能被調度,僅僅在進程列表中保留一個位置,記載該進程的退出狀態等信息供其餘進程收集,除此以外,殭屍進程再也不佔有任何內存空間。它須要它的父進程來爲它收屍,若是他的父進程沒安裝SIGCHLD信號處理函數調用wait或waitpid()等待子進程結束,又沒有顯式忽略該信號,那麼它就一直保持殭屍狀態,若是這時父進程結束了,那麼init進程自動會接手這個子進程,爲它收屍,它仍是能被清除的。可是若是若是父進程是一個循環,不會結束,那麼子進程就會一直保持殭屍狀態,這就是爲何系統中有時會有不少的殭屍進程。

任何一個子進程(init除外)在exit()以後,並不是立刻就消失掉,而是留下一個稱爲殭屍進程(Zombie)的數據結構,等待父進程處理。這是每一個子進程在結束時都要通過的階段。若是子進程在exit()以後,父進程沒有來得及處理,這時用ps命令就能看到子進程的狀態是」Z」。若是父進程能及時 處理,可能用ps命令就來不及看到子進程的殭屍狀態,但這並不等於子進程不通過殭屍狀態。

若是父進程在子進程結束以前退出,則子進程將由init接管。init將會以父進程的身份對殭屍狀態的子進程進行處理。

另外,還能夠寫一個php文件,而後在之後臺形式來運行它,例如:

1

2

3

4

5

6

7

8

9

<?php

//Action代碼

public function createAction(){

    //....

    //將args替換成要傳給insertLargeData.php的參數,參數間用空格間隔

    system('php -f insertLargeData.php ' . ' args ' . '&');

    $this->redirect('/');

}

?>

而後在insertLargeData.php文件中作數據庫操做。也能夠用cronjob + php的方式實現大數據量的處理。

若是是在終端運行php命令,當終端關閉後,剛剛執行的命令也會被強制關閉,若是你想讓其不受終端關閉的影響,可使用nohup命令實現:

1

2

3

4

5

6

7

8

9

<?php

//Action代碼

public function createAction(){

    //....

    //將args替換成要傳給insertLargeData.php的參數,參數間用空格間隔

    system('nohup php -f insertLargeData.php ' . ' args ' . '&');

    $this->redirect('/');

}

?>

你還可使用screen命令代替nohup命令。

相關文章
相關標籤/搜索