Linux-PHP 多进程编程

https://blog.csdn.net/weixin_42075590/article/details/80740968

前言

刚刚确认这个 Chat 主题的时候,周围就有同事和同学质疑,有的说 多进程没有前途,有的说多进程就是神经病。虽然这些说法过于武断,但是不可否认的,是 PHP 在多进程方面确实不擅长。

既然如此,我还为什么要用 PHP 去实现多进程呢?原因有二。

第一,在开发过程中,我们需要学习许多关于 Linux 进程的一些知识,比如什么是进程,什么是写时复制,什么是信号,如何守护进程化,什么叫僵尸进程等。这些知识是 Linux 进程的知识,与 PHP 语言本身没有什么关系。我们如果能够使用 PHP 进行多进程编程,那么换成 C++ 语言也一样能够搞的定,当然前提是你会 C++。而且 PHP 和 C++ 的多进程相关的 api 也十分类似,例如 PHP 创建一个子进程用的是pcntl_fork() ,而 C++ 用的是 fork() 。

既然如此,为什么我不直接用 C++ 呢?效率比 PHP 更高。理由很简单,因为我是 PHPer,对 C++ 不熟。哈哈。

第二,作为一个 PHP 开发人员,在工作中难免遇到一些需要使用多进程的时候,虽然有一些著名的框架(swoole,workerman)可以用,但是我们必须知其然,且知其所以然。知其所以然的最好方式莫过于自己动手实践一番。

本人也使用多进程开发了两个项目,一是公司使用的推送系统,二是定时任务管理系统(个人开源项目)

环境准备

项目环境:

  1. 代码运行环境是 Linux,如果你使用 Windows 编程,建议你使用 vagrant,或者使用虚拟机或云服务器(用 ftp 同步代码)。
  2. 安装好 PHP
  3. 安装好 pcntl 扩展和 posix 扩展。如果你使用 lnmp 一键安装,那么这两个扩展应该是默认安装好的。否则请自行安装扩展,这里不对此进行详述。

建议学习方式(仅供参考):

  1. 建议使用 PC 看本文,因为有不少代码需要你去实践
  2. 先跳到文章最后下载完整代码,然后一边看代码一边看文章内容。最后参照我的源码自己写一遍。

hello world

按照惯例,上来就是一个通俗易懂的 hello world 一定能取得大部分开发人员的好感。

pcntl 扩展让 PHP 拥有进程创建,信号处理等能力。我们使用 pcntl_fork() 创建子进程:

 
  1. <?php

  2.  
  3. //其他代码

  4.  
  5. $pid = pcntl_fork(); //fork进程

  6.  
  7. echo '父子进程都会执行的代码'.PHP_EOL;

  8.  
  9. if($pid > 0){

  10. echo "我是父进程,我创建的子进程id为 {$pid}".PHP_EOL;

  11. }else if($pid == 0){

  12. echo '我是子进程'.PHP_EOL;

  13. }else{

  14. echo 'fork进程失败'.PHP_EOL;

  15. }

  16.  

运行上面的代码,可以得到如下结果:

 
  1. [root@vagrant-centos65 default]# php test.php

  2. 父子进程都会执行的代码

  3. 我是父进程,我创建的子进程id为 8560

  4. 父子进程都会执行的代码

  5. 我是子进程

这段代码执行过程中,执行到 pcntl_fork() 时,将产生一个一模一样的脚本,即子进程,然后父进程和子进程分别继续执行之后的代码,互不干扰。

实际开发中我们需要父进程和子进程执行不一样的代码怎么办?

很简单,pcntl_fork() 为我们提供了一个返回值 pid,这个返回值有些特殊。在父进程脚本中,这个值是子进程的进程 id,而在子进程脚本中,这个值是 0,我们就可以用这个值区分父子进程。

运行过程示意图如下:enter image description here

看到这里应该对多进程编程有了个大概的概念。

信号

多进程编程不是上面几行代码就搞定了的,我们还有许多周边问题需要解决,例如如何在进程间进行通讯,如何让进程常驻,并且不会随随便便的挂掉。

玩过 Linux 的肯定都用过这个命令 kill -9 进程号 来杀死一个进程,许多新手就误以为 kill 命令是杀死进程的命令,其实并不是,kill 命令是向进程发送一个信号,我们可以简单的将信号理解为是一个指令,9 才是真正的杀死进程的罪魁祸首。

除了 9 以为,还有 1、2、3 等几十种信号,可以使用 kill -l 查看信号列表:

 
  1. [root@vagrant-centos65 default]# kill -l

  2. 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP

  3. 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1

  4. 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM

  5. 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP

  6. 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ

  7. 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR

  8. 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3

  9. 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8

  10. 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13

  11. 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12

  12. 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7

  13. 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2

  14. 63) SIGRTMAX-1 64) SIGRTMAX

有了信号,我们就能对进程发出指令,让进程执行退出,显示状态等动作。

每个信号都有一个默认响应动作,我们可以使用 pcntl_signal 来修改响应动作。我们来做个实验,先执行以下代码 php test.php

 
  1. <?php

  2.  
  3. //注册SIGINT和SIGUSR2信号的响应

  4. pcntl_signal(SIGINT, 'signalHandler', false); //SIGINT : 2

  5. pcntl_signal(SIGUSR2, 'signalHandler', false); //SIGINT : 12

  6.  
  7. function signalHandler($signal){

  8.  
  9. echo "收到了信号:".$signal;

  10.  
  11. if($signal == SIGUSR2){

  12. echo "进行业务操作1" . PHP_EOL;

  13. }elseif($signal == SIGINT){

  14. echo "进行业务操作2" . PHP_EOL;

  15. }

  16. // else if

  17. }

  18.  
  19. while(true){

  20.  
  21. sleep(1);

  22. //调用该方法,信号处理函数才会被执行

  23. pcntl_signal_dispatch();

  24.  
  25. }

  26.  

while 循环将使进程一直运行,不退出,然后我们另外开一个终端,先用 ps aux 命令找到它的进程 id,然后发信号:

 
  1. [root@vagrant-centos65 default]# kill -2 8600

  2. [root@vagrant-centos65 default]# kill -12 8600

回到第一个终端,就可以看到当前进程收到了信号,并执行了信号处理函数,结果如下:

 
  1. [root@vagrant-centos65 default]# php test.php

  2. 收到了信号:2进行业务操作2

  3. 收到了信号:12进行业务操作1

PS:上面脚本无法使用ctrl+c退出,因为 ctrl+c 就是信号 2,响应动作被我们修改掉了。请使用 kill -9 进程号 退出。

理论上来说,我们可以修改任何一个信号的响应动作,但是不建议随便修改,例如 9(SIGKILL),这个信号一般都是用来强制杀死进程,如果被改掉了,将对运维人员造成一定的困扰。

SIGUSR1 和 SIGUSR2 是留给用户使用的。

在 PHP 中,使用 posix_kill 来向一个进程发出信号,后面代码会使用到。

守护进程化

守护进程是不受终端控制,在后台运行的进程。我们必须使用守护进程,因为你无法保证你的终端永远不被关闭。

守护进程化的步骤如下:

  1. 创建子进程,父进程自杀
  2. 子进程创建新会话,以此摆脱终端控制

代码如下:

 
  1. <?php

  2.  
  3. //设置文件掩码

  4. umask(0);

  5.  
  6. $pid = pcntl_fork();

  7.  
  8. if($pid > 0){

  9. //父进程自杀

  10. exit(0);

  11. }elseif($pid == 0){

  12. //子进程创建新会话,摆脱终端控制

  13. if( -1 === posix_setsid() ){

  14. throw new Exception("setsid fail");

  15. }

  16. //此处可以结合上面的信号代码

  17. while(1){

  18. sleep(1);

  19. //pcntl_signal_dispatch();

  20. }

  21. }else{

  22. throw new Exception("fork fail");

  23. }

  24.  

执行上面脚本后我们可以使用 ps aux 命令查看到该脚本在后台默默运行

master-worker 进程模型

master-worker 进程模型是一个比较经典且常用的进程模式,nginx 也是用这个模型:

 
  1. [root@vagrant-centos65 default]# ps -ef | grep nginx

  2. root 1096 1 0 May04 ? 00:00:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

  3. www 1097 1096 0 May04 ? 00:02:20 nginx: worker process

  4. www 1098 1096 0 May04 ? 00:00:00 nginx: worker process

模型示意图:

enter image description here

worker 进程是 master 进程的子进程,worker进程负责具体的工作,而 master 进程仅负责监控 worker 进程的状态,一旦发现 worker 进程挂掉了,就立刻 fork 一个新的 worker 进程,以此来保证程序的高可用。

到这里你可能会有疑问,如果 master 也挂掉了怎么办?其实不用担心,worker 进程会挂掉基本上都是因为业务上抛异常导致的,而 master 进程仅负责监控,不做任何业务处理,只要你不主动杀死它,它挂掉的概率几乎为零。

worker 进程的数量建议等于 CPU 核心数,或整数倍,这样可以最有效合理地利用 CPU 资源。

master 和 worker 之间则可以使用信号进行通讯。假设我们现在要 stop 这个程序,我们只需要向 master 发出一个信号,然后 master 分别向各个 worker 发出信号让 worker 停止,最后 master 自己也停止。

有了上面的基础,我们就可以正式开始开发。

开发步骤一:环境检查

首先我们分别创建 Worker.php 和 test.php :

worker.php

 
  1. <?php

  2.  
  3. class Worker{

  4.  
  5.  
  6. public static function runAll(){

  7. static::checkEnv();

  8. }

  9.  
  10. public static function checkEnv(){

  11.  
  12. if (php_sapi_name() != 'cli') {

  13. exit('请使用命令行模式运行!');

  14. }

  15.  
  16. if(!function_exists('posix_kill')){

  17. exit('请先安装posix扩展'."\n");

  18. }

  19.  
  20. if(!function_exists('pcntl_fork')){

  21. exit('请先安装pcntl扩展'."\n");

  22. }

  23.  
  24. }

  25.  
  26. }

  27.  

test.php

 
  1. <?php

  2.  
  3. require 'Worker.php';

  4.  
  5. Worker::runAll();

  6.  

php test.php 运行

下面所有代码都写在Worker类中,重复代码不再贴出

开发步骤二:初始化

在项目中,我们还需要一些必备的初始化操作,例如配置日志文件路径,目录权限检查等:

 
  1. public static $log_file = '';

  2.  
  3. //将master进程id保存到这个文件中

  4. public static $pid_file = '';

  5.  
  6. //保存worker进程的状态

  7. public static $status_file = '';

  8.  
  9.  
  10. public static function runAll(){

  11. static::checkEnv();

  12. static::init();

  13. }

  14.  
  15. public static function init(){

  16.  
  17. //$temp_dir = sys_get_temp_dir() . '/myworker/';

  18. $temp_dir = __DIR__.'/tmp/';

  19.  
  20. if (!is_dir($temp_dir) && !mkdir($temp_dir)) {

  21. exit('mkdir runtime fail');

  22. }

  23. $test_file = $temp_dir . 'test';

  24. //尝试创建文件

  25. if(touch($test_file)){

  26. @unlink($test_file);

  27. }else{

  28. exit('permission denied: dir('.$temp_dir.')');

  29. }

  30.  
  31. if (empty(static::$status_file)) {

  32. static::$status_file = $temp_dir . 'status_file.status';

  33. }

  34.  
  35. if (empty(self::$pid_file)) {

  36. static::$pid_file = $temp_dir . 'master.pid';

  37. }

  38.  
  39. if (empty(self::$log_file)) {

  40. static::$log_file = $temp_dir . 'worker.log';

  41. }

  42.  
  43. static::log('初始化完成');

  44. }

  45.  
  46. public static function log($message)

  47. {

  48. $message = '['.date('Y-m-d H:i:s') .']['. $message . "]\n";

  49. file_put_contents((string)self::$log_file, $message, FILE_APPEND | LOCK_EX);

  50. }

开发步骤三:解析命令

启动和停止项目都是使用命令行操作,在 PHP 中,使用 global $argv 可以获取我们输入的命令,如:

 
  1. <?php

  2.  
  3. global $argv;

  4.  
  5. var_dump($argv);

  6.  

测试:

 
  1. [root@vagrant-centos65 myworker]# php test_arvg.php a b c

  2. array(4) {

  3. [0]=>

  4. string(13) "test_arvg.php"

  5. [1]=>

  6. string(1) "a"

  7. [2]=>

  8. string(1) "b"

  9. [3]=>

  10. string(1) "c"

  11. }

我们启动多进程项目的命令一般格式是:php test.php start -d ,-d 是可选选项,表示使用守护进程模式启动,适合项目正式运行时使用。没有 -d 则是调试模式,所有信息都会输出到终端,适合在开发阶段使用。

代码如下:

 
  1. //是否使用守护进程模式启动

  2. public static $deamonize = false;

  3.  
  4. public static function runAll(){

  5. static::checkEnv();

  6. static::init();

  7. static::parseCommand();

  8. }

  9.  
  10. public static function parseCommand(){

  11. global $argv;

  12.  
  13. if(!isset($argv[1]) || !in_array($argv[1],['start','stop','status'])){

  14. exit('usage: php your.php start | stop | status !' . PHP_EOL);

  15. }

  16.  
  17. $command1 = $argv[1]; //start , stop , status

  18. $command2 = $argv[2]; // -d

  19.  
  20. //检测master是否正在运行

  21. $master_id = @file_get_contents(static::$pid_file);

  22. //向master进程发送0信号,0信号比较特殊,进程不会响应,但是可以用来检测进程是否存活

  23. $master_alive = $master_id && posix_kill($master_id,0);

  24.  
  25. if($master_alive){

  26. //不能重复启动

  27. if($command1 == 'start' && posix_getpid() != $master_id){

  28. exit('worker is already running !'.PHP_EOL);

  29. }

  30. }else{

  31. //项目未启动的情况下,只有start命令有效

  32. if ($command1 != 'start') {

  33. exit('worker not run!' . PHP_EOL);

  34. }

  35. }

  36.  
  37. switch($command1){

  38. case 'start':

  39. if($command2 == '-d'){

  40. static::$deamonize = true;

  41. }

  42. break;

  43. case 'stop':

  44. //停止进程

  45. //必须exit退出

  46. exit(0);

  47. break;

  48. case 'status':

  49. //查看状态

  50. //必须exit退出

  51. exit(0);

  52. break;

  53. default:

  54. exit('usage: php your.php start | stop | status !' . PHP_EOL);

  55. break;

  56. }

  57.  
  58. }

stop和status 功能后面再说,我们先实现 start 功能,在 parseComman() 方法中,start 不需要进行任何实质性的操作。

开发步骤四:守护进程化

解析完命令,如果是 start 操作,立即进行守护进程化操作:

 
  1. public static function runAll(){

  2. static::checkEnv();

  3. static::init();

  4. static::parseCommand();

  5. static::deamonize();

  6. }

  7. public static function deamonize(){

  8.  
  9. if(static::$deamonize == false){

  10. return;

  11. }

  12.  
  13. umask(0);

  14.  
  15. $pid = pcntl_fork();

  16.  
  17. if($pid > 0){

  18. exit(0);

  19. }elseif($pid == 0){

  20. if(-1 === posix_setsid()){

  21. throw new Exception("setsid fail");

  22. }

  23. static::setProcessTitle('myworker: master');

  24. }else{

  25. throw new Exception("fork fail");

  26. }

  27. }

  28.  
  29. public static function setProcessTitle($title){

  30. //设置进程名

  31. if (function_exists('cli_set_process_title')) {

  32. @cli_set_process_title($title);

  33. }

  34. }

守护进程化在前面就说过了,不再重复。

cli_set_process_title 是设置进程的名称,方便以后查看进程,就像用 ps 命令查看 nginx,下图红框中就是进程名:

enter image description here

开发步骤五:保存 master 进程号

代码运行到这里,当前进程就是 master 进程了,我们要将 master 进程 id 保存到文件中:

 
  1. public static $master_pid = 0;

  2.  
  3. public static function runAll(){

  4. static::checkEnv();

  5. static::init();

  6. static::parseCommand();

  7. static::deamonize();

  8. static::saveMasterPid();

  9. }

  10. public static function saveMasterPid(){

  11. static::$master_pid = posix_getpid();

  12. if(false === @file_put_contents(static::$pid_file, static::$master_pid)){

  13. throw new Exception('fail to save master pid ');

  14. }

  15. }

开发步骤六:注册信号处理

上面已经解释过信号,现在我们要注册几个信号处理函数,分别对应 stop,status 操作

 
  1. public static function runAll(){

  2. static::checkEnv();

  3. static::init();

  4. static::parseCommand();

  5. static::deamonize();

  6. static::saveMasterPid();

  7. static::installSignal();

  8. }

  9.  
  10. public static function installSignal()

  11. {

  12. pcntl_signal(SIGINT, array(__CLASS__, 'signalHandler'), false);

  13. pcntl_signal(SIGUSR2, array(__CLASS__, 'signalHandler'), false);

  14. //SIG_IGN表示忽略该信号,不做任何处理。SIGPIPE默认会使进程退出

  15. pcntl_signal(SIGPIPE, SIG_IGN, false);

  16. }

  17.  
  18. public static function signalHandler($signal){

  19. switch ($signal) {

  20. case SIGINT: // Stop.

  21. //static::stopAll();

  22. break;

  23. case SIGUSR2: // Show status.

  24. //static::writeStatus();

  25. break;

  26. }

  27. }

stop 和 status 的具体实现后面再说

开发步骤七:重定向输入输出

如果是使用守护进程的模式启动项目,我们要求系统代码中的 echo、var_dump 等方法输出的内容不能显示到终端,因此需要将标准输出给重定向到 /dev/null 中。

如果是调试模式,则不需要该步骤中提供了 global $STDOUT, $STDERR; 让我们能够获取到标准输出。

 
  1. public static $stdoutFile = '/dev/null';

  2.  
  3. public static function runAll(){

  4. static::checkEnv();

  5. static::init();

  6. static::parseCommand();

  7. static::deamonize();

  8. static::saveMasterPid();

  9. static::installSignal();

  10. static::resetStd();

  11. }

  12. public static function resetStd(){

  13. if(static::$deamonize == false){

  14. return;

  15. }

  16. global $STDOUT, $STDERR;

  17. $handle = fopen(self::$stdoutFile, "a");

  18. if ($handle) {

  19. unset($handle);

  20. @fclose(STDOUT);

  21. @fclose(STDERR);

  22. $STDOUT = fopen(self::$stdoutFile, "a");

  23. $STDERR = fopen(self::$stdoutFile, "a");

  24. } else {

  25. throw new Exception('can not open stdoutFile ' . self::$stdoutFile);

  26. }

  27. }

Linux 中的 /dev/null 是一个黑洞,丢进去的东西都将消失不见,相当于垃圾桶。

开发步骤八:fork 子进程

重头戏来了,这一步是核心步骤,首先我们需要确定我们要几个 worker 进程,然后循环 fork 出来,值得注意的是,fork 完成后,master 进程和 worker 进程都要使用 while 死循环保持运行状态,否则代码执行完毕就会自动退出了。

 
  1. public static $workers = [];

  2.  
  3. //worker实例

  4. public static $instance = null;

  5.  
  6. //worker数量

  7. public $count = 2;

  8.  
  9. //worker启动时的回调方法

  10. public $onWorkerStart = null;

  11.  
  12. public function __construct(){

  13. static::$instance = $this;

  14. }

  15. public static function runAll(){

  16. static::checkEnv();

  17. static::init();

  18. static::parseCommand();

  19. static::deamonize();

  20. static::saveMasterPid();

  21. static::installSignal();

  22. static::resetStd();

  23. static::forkWorkers();

  24. }

  25. public static function forkWorkers(){

  26.  
  27. $worker_count = static::$instance->count;

  28.  
  29. while(count(static::$workers) < $worker_count ){

  30. static::forkOneWorker(static::$instance);

  31. }

  32. }

  33.  
  34. public static function forkOneWorker($instance){

  35. $pid = pcntl_fork();

  36. if($pid > 0){

  37. static::$workers[$pid] = $pid;

  38. }elseif($pid == 0){

  39. static::log('创建了一个worker');

  40. static::setProcessTitle('myworker process');

  41. //运行

  42. $instance->run();

  43. }else{

  44. throw new Exception('fork one worker fail');

  45. }

  46. }

  47.  
  48. public function run(){

  49. if($this->onWorkerStart){

  50. try {

  51. //worker启动,调用onWorkerStart回调

  52. call_user_func($this->onWorkerStart, $this);

  53. } catch (\Exception $e) {

  54. static::log($e);

  55. sleep(1);

  56. exit(250);

  57. } catch (\Error $e) {

  58. static::log($e);

  59. sleep(1);

  60. exit(250);

  61. }

  62. }

  63. //死循环,保持worker运行,并且一有信号来了就调用信号处理函数

  64. while (1) {

  65. pcntl_signal_dispatch();

  66. sleep(1);

  67. }

  68. }

修改 test.php:

 
  1. <?php

  2.  
  3. require 'Worker.php';

  4.  
  5. $worker = new Worker();

  6.  
  7. $worker->count = 2;

  8.  
  9. $worker->onWorkerStart = function($worker){

  10. echo 'onWorkerStart' . PHP_EOL;

  11. };

  12.  
  13. Worker::runAll();

  14.  

业务代码一般写在onWorkerStart ,worker启动时会调用该方法。比如可以开一个socket,阻塞等待用户连接。

调试模式运行:

 
  1. [root@vagrant-centos65 myworker]# php test.php start

  2. onWorkerStart

  3. onWorkerStart

  4. [root@vagrant-centos65 myworker]# ps aux |grep myworker

  5. root 8813 0.0 0.1 154664 5532 pts/3 S 14:43 0:00 myworker process

  6. root 8814 0.0 0.1 154664 5532 pts/3 S 14:43 0:00 myworker process

运行起来后,发现 onWorkerStart 都正常输出了,但是 master 进程却不见了,这是咋回事?因为上面代码中我们没有让 master 死循环,它执行完代码就退出了。下面我们就来做这件事。

开发步骤九:监控 worker 进程

为什么master 死循环的操作不在上面代码中直接实现,而要单独出来讲,因为master 进程不仅要保持运行,它还肩负着监控 worker 进程的重任,一旦发现 worker 进程意外退出了,就立刻重新 fork 一个 worker 进程,做到高可用。

 
  1. //记录当前进程的状态

  2. public static $status = 0;

  3.  
  4. //运行中

  5. const STATUS_RUNNING = 1;

  6. //停止

  7. const STATUS_SHUTDOWN = 2;

  8.  
  9. public static function runAll(){

  10. static::checkEnv();

  11. static::init();

  12. static::parseCommand();

  13. static::deamonize();

  14. static::saveMasterPid();

  15. static::installSignal();

  16. static::resetStd();

  17. static::forkWorkers();

  18. static::monitorWorkers();

  19. }

  20. public static function monitorWorkers(){

  21. //设置当前状态为运行中

  22. static::$status = static::STATUS_RUNNING;

  23. while (1) {

  24. pcntl_signal_dispatch();

  25. $status = 0;

  26. //阻塞,等待子进程退出

  27. $pid = pcntl_wait($status, WUNTRACED);

  28.  
  29. self::log("worker[ $pid ] exit with signal:".pcntl_wstopsig($status));

  30.  
  31. pcntl_signal_dispatch();

  32. //child exit

  33. if ($pid > 0) {

  34. //意外退出时才重新fork,如果是我们想让worker退出,status = STATUS_SHUTDOWN

  35. if (static::$status != static::STATUS_SHUTDOWN) {

  36. unset(static::$workers[$pid]);

  37. static::forkOneWorker(static::$instance);

  38. }

  39. }

  40. }

  41. }

上面代码中最关键的就是 pcntl_wait ,该方法的作用是等待子进程退出,一旦有子进程退出,就会返回退出的进程的 id,然后就重新 fork 一个 worker 进程。

注意:到了这一步,要先删除前面测试产生的 master.pid 文件并且 kill -9 前面运行起来的 worker 进程。否则会影响下面的实验

我们来实验一下,先使用守护进程模式启动:

 
  1. [root@vagrant-centos65 myworker]# php test.php start -d

  2. [root@vagrant-centos65 myworker]# ps aux | grep myworker

  3. root 8844 0.0 0.1 154660 5528 ? Ss 15:20 0:00 myworker: master

  4. root 8845 0.0 0.1 154660 5508 ? S 15:20 0:00 myworker process

  5. root 8846 0.0 0.1 154660 5508 ? S 15:20 0:00 myworker process

可以看到三个进程都正常运行,进程 id 分别为 8844、8845、8846。

然后我们用kill命令杀死 8845,再次查看进程状态:

 
  1. [root@vagrant-centos65 myworker]# kill -9 8845

  2. [root@vagrant-centos65 myworker]# ps aux | grep myworker

  3. root 8844 0.0 0.1 154660 5636 ? Ss 15:20 0:00 myworker: master

  4. root 8846 0.0 0.1 154660 5508 ? S 15:20 0:00 myworker process

  5. root 8849 0.0 0.1 154660 5508 ? S 15:22 0:00 myworker process

根据上面结果,8845 确实被我们杀掉了,并且重新启动了一个新的 worker,进程 id 为 8849。

到此,多进程的启动已经全部实现,并且整个项目已经完成了四分之三。

PS:由于 stop 功能还没实现,所以需要手动 kill master 和 worker 进程,注意要先 kill master,再 kill worker,然后手动删除 master.pid 文件

小知识:pcntlwait() 方法不仅可以监控子进程退出,而且有回收资源的作用。fork 出来的每一个子进程最终都要使用 pcntlwait 进行资源回收,否则就会产生僵尸进程。僵尸进程就是代码执行完了却没有被回收的进程,会白白占用 CPU 等资源,造成系统资源浪费。

开发步骤十:查看进程状态

查看进程状态这一步并不是必须的,你可以直接用 ps aux | grep myworker 查看即可。

但如果你并不满足,想要查看更多的状态,例如查看一些业务状态,那么就必须要开发这个功能。

原理其实很简单,我们给 master 发一个信号,然后 master 分别给各个 worker 发一个信号。worker 收到这个信号,就往一个文件里写入一些信息(状态信息),然后我们再读取这个文件的内容并显示出来就可以了。

这一步,我们需要修改前面的 parseCommand() 和 signalHandler() 方法的代码,如果不明白,请先回头看一看。

 
  1. pubilc static function parseCommand(){

  2. //其他代码,重复的不贴出来了

  3. switch($command1){

  4. // case start 和 case stop的代码就不贴出来了

  5. case 'status':

  6. //查看状态

  7. if(is_file(static::$status_file)){

  8. //先删除就得status文件

  9. @unlink(static::$status_file);

  10. }

  11. //给master发送信号

  12. posix_kill($master_id,SIGUSR2);

  13. //等待worker进程往status文件里写入状态

  14. usleep(300000);

  15. @readfile(static::$status_file);

  16. exit(0);

  17. break;

  18. default:

  19. exit('usage: php your.php start | stop | status !' . PHP_EOL);

  20. break;

  21. }

  22. }

  23.  
  24. public static function signalHandler($signal){

  25. switch ($signal) {

  26. case SIGINT: // Stop.

  27. //static::stopAll();

  28. break;

  29. case SIGUSR2: // Show status.

  30. //master和worker都执行

  31. static::writeStatus();

  32. break;

  33. }

  34. }

  35.  
  36. public static function writeStatus(){

  37. $pid = posix_getpid();

  38.  
  39. if($pid == static::$master_pid){ //master进程

  40.  
  41. $master_alive = static::$master_pid&& posix_kill(static::$master_pid,0);

  42. $master_alive = $master_alive ? 'is running' : 'die';

  43. $result = file_put_contents(static::$status_file, 'master[' . static::$master_pid . '] ' . $master_alive . PHP_EOL, FILE_APPEND | LOCK_EX);

  44. //给worker进程发信号

  45. foreach(static::$workers as $pid){

  46. posix_kill($pid,SIGUSR2);

  47. }

  48. }else{ //worker进程

  49.  
  50. $name = 'worker[' . $pid . ']';

  51. $alive = $pid && posix_kill($pid, 0);

  52. $alive = $alive ? 'is running' : 'die';

  53. file_put_contents(static::$status_file, $name . ' ' . $alive . PHP_EOL, FILE_APPEND | LOCK_EX);

  54. }

  55. }

测试:

 
  1. [root@vagrant-centos65 myworker]# php test.php start -d

  2. [root@vagrant-centos65 myworker]# php test.php status

  3. master[8905] is running

  4. worker[8906] is running

  5. worker[8907] is running

PS:Worker 往文件写状态信息时,可以根据具体的业务写入更多的数据,这点可以自由发挥。

开发步骤十一:停止进程

停止进程的和查看进程状态的原理是一样的,向 master 进程发一个信号叫他go die,然后 master 向 worker 进程发信号叫他们 go die,然后大家一起 go die。

同样的修改 parseCommand() 和 signalHandler() 方法:

 
  1. public static function signalHandler($signal){

  2. switch ($signal) {

  3. case SIGINT: // Stop.

  4. static::stopAll();

  5. break;

  6. case SIGUSR2: // Show status.

  7. //master和worker都执行

  8. static::writeStatus();

  9. break;

  10. }

  11. }

  12. pubilc static function parseCommand(){

  13. //其他代码,重复的不贴出来了

  14. switch($command1){

  15. // case start 和 case status的代码就不贴出来了

  16. case 'stop':

  17. //停止进程

  18. $master_id && posix_kill($master_id, SIGINT);

  19. //只要还没杀死master,就一直杀

  20. while ($master_id && posix_kill($master_id, 0)) {

  21. usleep(300000);

  22. }

  23. exit(0);

  24. break;

  25. default:

  26. exit('usage: php your.php start | stop | status !' . PHP_EOL);

  27. break;

  28. }

  29. }

  30. public static function stopAll(){

  31.  
  32. $pid = posix_getpid();

  33.  
  34. if($pid == static::$master_pid){ //master进程

  35. //将当前状态设为停止,否则子进程一退出master重新fork

  36. static::$status = static::STATUS_SHUTDOWN;

  37. //通知子进程退出

  38. foreach(static::$workers as $pid){

  39. posix_kill($pid,SIGINT);

  40. }

  41. //删除pid文件

  42. @unlink(static::$pid_file);

  43. exit(0);

  44. }else{ //worker进程

  45. static::log('worker[' . $pid .'] stop');

  46. exit(0);

  47. }

  48. }

测试:

 
  1. [root@vagrant-centos65 myworker]# php test.php start -d

  2. [root@vagrant-centos65 myworker]# php test.php status

  3. master[8920] is running

  4. worker[8921] is running

  5. worker[8922] is running

  6. [root@vagrant-centos65 myworker]# php test.php stop

到此,多进程模型全部开发了。根据 status 和 stop 的原理,大家可以自行开发 reload,restart 等功能。

除此之外,我们在 worker 启动时调用了 onWorkerStart 回调,同理的,我们也可以开发 onWorkerStop 等回调。

扩展:定时器 timer

在多进程模型中,我们经常使用到定时器。其实定时器就是用了信号来实现。

pcntl 扩展为我们提供了pcntl_alarm() 方法,调用 pcntl_alarm(1) ,系统就会在 1 秒之后给当前进程发一个SIGALRM信号,收到信号时,我们再次调用 pcntl_alarm(1),如此不断循环,就会每秒执行一次信号处理函数,就这样形成了定时器。

了解了原理,代码其实很简单,仔细阅读一下就明白了。

Timer.php

 
  1. <?php

  2.  
  3. class Timer

  4. {

  5. public static $tasks = array();

  6.  
  7. public static function init()

  8. {

  9. pcntl_signal(SIGALRM, array( __CLASS__, 'signalHandle'), false);

  10. }

  11.  
  12. public static function signalHandle()

  13. {

  14. pcntl_alarm(1);

  15.  
  16. if (empty(self::$tasks)) {

  17. return;

  18. }

  19. //执行任务

  20. foreach (self::$tasks as $run_time => $task) {

  21. $time_now = time();

  22. if ($time_now >= $run_time) {

  23. $func = $task[0];

  24. $args = $task[1];

  25. $interval = $task[2];

  26. $persistent = $task[3];

  27. call_user_func_array($func, $args);

  28. unset(self::$tasks[$run_time]);

  29. if($persistent){

  30. Timer::add($interval, $func, $args,$persistent);

  31. }

  32. }

  33. }

  34. }

  35.  
  36. /**

  37. * @param $interval 几秒后执行

  38. * @param $func 要执行的回调方法

  39. * @param array $args 参数

  40. * @param bool $persistent 是否持续执行

  41. * @return bool

  42. */

  43. public static function add($interval, $func, $args = array(),$persistent = true)

  44. {

  45. if ($interval <= 0) {

  46. echo new Exception('wrong interval');

  47. return false;

  48. }

  49. if (!is_callable($func)) {

  50. echo new Exception('not callable');

  51. return false;

  52. } else {

  53. $runtime = time() + $interval;

  54. self::$tasks[$runtime] = array($func, $args, $interval,$persistent);

  55. return true;

  56. }

  57. }

  58.  
  59. public static function tick()

  60. {

  61. pcntl_alarm(1);

  62. }

  63. }

  64.  

测试:

 
  1. <?php

  2.  
  3. require 'Worker.php';

  4. require 'Timer.php';

  5.  
  6. $worker = new Worker();

  7.  
  8. $worker->count = 2;

  9.  
  10. $worker->onWorkerStart = function($worker){

  11. Timer::init();

  12. //2秒执行一次

  13. Timer::add(2,function(){

  14. $pid = posix_getpid();

  15. echo date('Y-m-d H:i:s') .' pid : ' . $pid.PHP_EOL;

  16. },[],true);

  17.  
  18. //注意,一定要调用tick方法定时器才会运行

  19. Timer::tick();

  20. };

  21.  
  22. Worker::runAll();

  23.  

为了直观的看到效果,使用调试模式运行:

 
  1. [root@vagrant-centos65 myworker]# php test.php start

  2. 2018-05-14 16:38:53 pid : 9181

  3. 2018-05-14 16:38:53 pid : 9182

  4. 2018-05-14 16:38:55 pid : 9181

  5. 2018-05-14 16:38:55 pid : 9182

  6. 2018-05-14 16:38:57 pid : 9182

  7. 2018-05-14 16:38:57 pid : 9181

  8. 2018-05-14 16:38:59 pid : 9182

  9. 2018-05-14 16:38:59 pid : 9181

总结

主要知识点:

  • 使用 pcntl_fork 创建子进程
  • 守护进程化:fork 一个进程并且杀死自己
  • 使用 pcntl_signal 注册信号处理方法
  • 使用 posix_kill 向指定进程发送信号
  • worker 进程 while 循环等待信号
  • master 进程 while 循环中持续监控 worker 进程状态
  • 在 onWorkerStart 回调中实现业务代码

完整代码下载:

  • CSDN 下载(需要 1 积分):https://download.csdn.net/download/u010837612/10413528
  • 网盘下载:链接: https://pan.baidu.com/s/1MI4E1Y521I5Q5UEyvCQvcg 密码: x8wn

建议学习方式:先下载完整代码,然后一边看代码一边看文章内容。最后参照我的源码自己写一遍。

其他学习资料:本人最开始学习这一块内容是通过研究 workerman 的源码,workerman 是非常好的学习资料,有兴趣的朋友可以看看。

最后,感谢大家的支持。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
众所周知,人工智能是当前最热门的话题之一, 计算机技术与互联网技术的快速发展更是将对人工智能的研究推向一个新的高潮。 人工智能是研究模拟和扩展人类智能的理论与方法及其应用的一门新兴技术科学。 作为人工智能核心研究领域之一的机器学习, 其研究动机是为了使计算机系统具有人的学习能力以实现人工智能。 那么, 什么是机器学习呢? 机器学习 (Machine Learning) 是对研究问题进行模型假设,利用计算机从训练数据中学习得到模型参数,并最终对数据进行预测和分析的一门学科。 机器学习的用途 机器学习是一种通用的数据处理技术,其包含了大量的学习算法。不同的学习算法在不同的行业及应用中能够表现出不同的性能和优势。目前,机器学习已成功地应用于下列领域: 互联网领域----语音识别、搜索引擎、语言翻译、垃圾邮件过滤、自然语言处理等 生物领域----基因序列分析、DNA 序列预测、蛋白质结构预测等 自动化领域----人脸识别、无人驾驶技术、图像处理、信号处理等 金融领域----证券市场分析、信用卡欺诈检测等 医学领域----疾病鉴别/诊断、流行病爆发预测等 刑侦领域----潜在犯罪识别与预测、模拟人工智能侦探等 新闻领域----新闻推荐系统等 游戏领域----游戏战略规划等 从上述所列举的应用可知,机器学习正在成为各行各业都会经常使用到的分析工具,尤其是在各领域数据量爆炸的今天,各行业都希望通过数据处理与分析手段,得到数据中有价值的信息,以便明确客户的需求和指引企业的发展。
### 回答1: Linux多进程编程是指在Linux操作系统下,使用多个进程同时执行任务的编程方式。 在Linux中,每个进程都是一个独立的执行环境,有自己独立的地址空间和资源。多进程编程可以通过创建多个进程来同时执行不同的任务,从而提高程序的并发性和效率。 在Linux中,可以使用fork()系统调用创建一个新的进程。原有进程称为父进程,新创建的进程称为子进程,父进程和子进程具有相同的代码段和数据段,但拥有不同的进程ID。 通过fork()创建的进程在执行时,会复制父进程的所有资源,包括打开的文件、文件描述符等。子进程独立于父进程运行,并且可以通过exec()系列函数来加载新的程序,替代原有的代码段和数据段,实现不同的任务。 多进程编程可以通过父子进程间的通信来实现数据交换。常用的通信方式包括管道、共享内存、信号和套接字等。父进程可以通过管道或共享内存将数据传递给子进程,子进程可以通过套接字与其他进程进行通信,实现进程间的数据共享和同步。 多进程编程也需要注意避免进程之间的竞争条件和死锁。可以使用进程同步机制,如互斥锁和信号量,来保证多个进程对共享资源的互斥访问和同步执行。 总而言之,Linux多进程编程是一种有效利用多核处理器和提高程序并发性的编程方式,可以通过创建多个进程来同时执行不同的任务,并通过不同的进程间通信方式来实现数据交换和同步执行。 ### 回答2: Linux多进程编程是指在Linux操作系统上使用多个进程同时执行任务的编程方式。 Linux作为一个多用户、多任务的操作系统,支持多进程的并行执行。多进程编程可以充分利用操作系统提供的资源管理和调度机制,实现并发性和并行性。 在Linux中,一个程序可以通过创建新的进程来执行不同的任务。多进程编程可以通过调用系统调用fork()创建新的进程,从而将任务分配给不同的进程执行。同时,通过调用系统调用exec()可以在子进程中加载新的程序代码,实现任务的切换和执行。 多进程编程可以实现任务的分割和并行化处理,提高了程序的执行效率和响应速度。不同的进程之间可以通过进程间通信(IPC)机制来进行数据的交换和协调,如管道、信号、共享内存等。 在多进程编程中,需要注意进程的创建和销毁、进程间通信的机制、进程间的同步与互斥等问题。合理使用多进程编程可以更好地利用多核处理器的计算能力和资源,提高程序的并发性和性能。 总之,Linux多进程编程是一种高效利用操作系统资源的编程方式,可以实现任务的并行处理,提高程序的执行效率和响应速度。 ### 回答3: Linux多进程编程是指在Linux操作系统上使用多个进程进行编程的一种方法。在Linux下,可以创建多个进程来同时执行不同的任务,扩展系统的处理能力,提高整体的效率和响应速度。 多进程编程的主要特点是可以实现并发执行,每个进程独立运行,相互之间不会相互干扰。在Linux中,可以使用fork()系统调用来创建新的进程,子进程的运行和父进程是并行的。子进程可以继承父进程的资源,如打开的文件描述符、信号处理等,也可以通过exec()系列系统调用来加载新的程序,实现进程的替换。 在多进程编程中,进程之间可以通过进程间通信(IPC)方式进行数据交换和同步。常用的IPC机制有管道、共享内存、消息队列和信号量等。这些机制可以让多个进程之间传递数据、共享资源,实现各个进程之间的协作和通信。 多进程编程还可以通过进程管理和调度来控制各个进程的执行顺序和优先级,提高系统的整体性能。Linux提供了丰富的进程管理工具和调度算法,可以根据实际需求进行调整和配置,以达到最佳的性能和资源利用率。 总之,Linux多进程编程是一种高效的编程模式,可以充分利用多核处理器的优势,实现并发执行和资源共享。它不仅可以提高系统的处理能力和响应速度,还可以提高系统的稳定性和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值