技术饭

PHP:PCNTL进程控制功能的基础使用

copylian    0 评论    2048 浏览    2024.03.14

PHP:PCNTL进程控制功能的基础使用,多进程的作用主要用于日志分析、队列处理、批量处理等,如要处理10w级别的数据,一条数据需要执行1秒,那么一个进程不间断需要执行1天多的时间,但是如果分成20个进程(进程过多会导出cpu爆满),每个进程分5000条只需执行1.3小时左右就完成任务了。

php多进程需要pcntlposix扩展支持,请先下载配置扩展,必须在命令行 cli 模式下才能执行,不支持windows。


php多进程核心函数:

pcntl_fork(创建子进程)、pcntl_wait(阻塞当前进程)

pcntl_fork一次调用两次返回,在父进程中返回子进程pid,在子进程中返回0,出错返回-1

pcntl_wait ( int &$status [, int $options ] ):

    阻塞当前进程,直到任意一个子进程退出或收到一个结束当前进程的信号,注意是结束当前进程的信号,子进程结束发送的SIGCHLD不算。使用$status返回子进程的状态码,并可以指定第二个参数来说明是否以阻塞状态调用

    阻塞方式调用的,函数返回值为子进程的pid,如果没有子进程返回值为-1

    非阻塞方式调用,函数还可以在有子进程在运行但没有结束的子进程时返回0

pcntl_waitpid ( int $pid , int &$status [, int $options ] )

    功能同pcntl_wait,区别为waitpid为等待指定pid的子进程。当pid为-1时pcntl_waitpid与pcntl_wait 一样。在pcntl_wait和pcntl_waitpid两个函数中的$status中存了子进程的状态信息。


系统信号:

信号是事件发生时对进程的通知机制,有时又称为软件中断。一个进程可以向另一个进程发送信号,比如子进程结束时都会向父进程发送一个SIGCHLD(17号信号)来通知父进程,所以有时信号也被当作一种进程间通信的机制。在linux系统下,通常我们使用 kill -9 XXPID 来结束一个进程,其实这个命令的实质就是向某进程发送SIGKILL(9号信号),对于在前台进程我们通常用Ctrl+c快捷键来结束运行,该快捷键的实质是向当前进程发送SIGINT(2号信号),而进程收到该信号的默认行为是结束运行

以下是常用的系统信号对应的编码、名称:kill -l

微信图片_20240314163710.png


demo-01.php:会先执行会先执行主进程,然后再执行子进程,如果主进程有 pcntl_wait($status) 则子进程结束之后再执行  pcntl_wait($status) 后的方法

<?php


// cli 模式
if (substr(php_sapi_name(), 0, 3) !== 'cli') {
   die("cli mode only");
}

function sig_handler($signal) {
 echo "接收到信号:{$signal}".PHP_EOL;
}

// 捕捉 Ctrl+C 信号
pcntl_signal(SIGCHLD, "sig_handler");

// 配合pcntl_signal使用,简单的说,是为了让系统产生时间云,让信号捕捉函数能够捕捉到信号量, ticks 性能不好,php高版本选用 pcntl_async_signals(true)pcntl_signal_dispatch()
//declare(ticks = 1);

//
开启异步监听信号
// pcntl_async_signals(true);

// pcntl_fork
函数创建一个新的子进程,并返回一个整数值,用于在父进程和子进程间进行区分
// pid == 0:在子进程中; pid > 0:在父进程中; pid == -1:进程启动失败,一次调用两次返回
$pid = pcntl_fork();

// var_export(['a'=> 1, 'b'=>2]); // var_export可以将一个数组转为一个字符串,以符合PHP的代码风格,输出者展示一个字符串的内容。

if ($pid == -1) {
 echo "启动进程失败".PHP_EOL;

 return;

} else if($pid) {

 pcntl_signal_dispatch(); // 调用信号

 
echo "主进程ID{$pid}".PHP_EOL;

 $wait = pcntl_wait($status); // 等待子进程结束,返回主进程ID

 
echo "pcntl_wait{$wait}, 等待子进程结束:{$status}".PHP_EOL;

} else {

 echo "子进程的PID{$pid}, 子进程ID".getmygid().PHP_EOL;

 sleep(2);

 pcntl_exit(1); // 终止子进程

 
// 子进程需要exit,防止子进程也进入for循环
 
exit();
}


demo-02-multiprocess.php:多进程无限循环

<?php
// 最大的子进程数量
$maxChildPro = 8;

// 当前的子进程数量
$curChildPro = 0;

// 当子进程退出时,会触发该函数,当前子进程数-1
function sig_handler($sig)
{
   global $curChildPro;
   switch ($sig) {
       case SIGCHLD:
           echo 'SIGCHLD', PHP_EOL;
           $curChildPro--;
           break;
   }
}

// 配合pcntl_signal使用,简单的说,是为了让系统产生时间云,让信号捕捉函数能够捕捉到信号量, ticks 性能不好,php高版本选用 pcntl_async_signals(true)pcntl_signal_dispatch()
//declare(ticks = 1);

//
开启异步监听信号
pcntl_async_signals(true);

// 注册子进程退出时调用的函数。SIGCHLD:在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程。
pcntl_signal(SIGCHLD, "sig_handler");

while (true) {
   $curChildPro++;
   $pid = pcntl_fork();
   if ($pid) {
//        pcntl_signal_dispatch(); // 调用信号
       
// 父进程运行代码,达到上限时父进程阻塞等待任一子进程退出后while循环继续
       
if ($curChildPro >= $maxChildPro) {
           pcntl_wait($status);
       }
   } else {
       // 子进程运行代码
       
$s = rand(2, 6);
       sleep($s);
       echo "child sleep $s second quit", PHP_EOL;

       // 子进程需要exit,防止子进程也进入for循环
       
exit();
   }
}


demo-03-alarm.php:pcntl_alarm() 方法设置闹铃定时器

<?php
// 配合pcntl_signal使用,简单的说,是为了让系统产生时间云,让信号捕捉函数能够捕捉到信号量, ticks 性能不好,php高版本选用 pcntl_async_signals(true)pcntl_signal_dispatch()
//declare(ticks = 1);

//
开启异步监听信号
pcntl_async_signals(true);

function signal_handler($signal) {
 print "Caught SIGALRM\n";
 pcntl_alarm(5);
}

pcntl_signal(SIGALRM, "signal_handler", true);
pcntl_alarm(5); // 通过pcntl_alarm实现隔5s发一个信号

for(;;) {
 sleep(1);
}


demo-04-kill.php:通过posix_kill()或system()主动杀死子进程

<?php
/**
*
父进程通过pcntl_wait等待子进程退出
* 子进程通过信号kill自己,也可以在父进程中发送kil信号结束子进程
*/

// 生成子进程
$pid = pcntl_fork();

if ($pid == -1) {
 die('could not fork');
} else {
 if ($pid) {
   $status = 0;
   // 阻塞父进程,直到子进程结束,不适合需要长时间运行的脚本.
   //
可使用pcntl_wait($status, WNOHANG)实现非阻塞式
   
pcntl_wait($status);
   echo $status, PHP_EOL;
   exit;
 } else {
   // 结束当前子进程,以防止生成僵尸进程
   
if(function_exists("posix_kill")){
     echo "posix_kill", PHP_EOL;
     posix_kill(getmypid(), SIGTERM);
   } else {
     system('kill -9'. getmypid());
   }
   exit;
 }
}


下面是一个测试实例:

test-pcntl.php:主进程,同时处理5w多条数据,一个进程需要131秒的时间(cup占用20%),同时开启5个进程只需要40秒(cpu占用30%~50%),子程序需要 exit() 退出否则会不断循环,主程序需要开启 pcntl_wait() 否则会出现子程序未执行完就断开的情况,导致出现大量的僵尸进程,这时候手动批量kill进程即可。

ubuntu根据关键词批量杀进程:https://www.cnblogs.com/McGeeForest/p/15303494.html

ps -ef | grep /www/pcntl.php | grep -v grep | cut -c 12-16 | xargs kill -s 9

grep -v grep 是在列出的进程中去除含有关键字“grep”的进程。

cut -c 10-16 是截取输入行的第9个字符到第15个字符,而这正好是进程号PID。

<?php
// 需求:数据量有5w多条,需要分配5个进程处理,每个进程处理1w条数据

// 进程数量
$processNum = 5;

// 数据
$data = range(1,53256);

// 单线程执行
//$time = time();
//foreach ($data as $v) {
//  file_put_contents(__DIR__."/test-pcntl.txt", $v.PHP_EOL, FILE_APPEND | LOCK_EX);
//  echo $v, PHP_EOL;
//}
//echo time()-$time, PHP_EOL; //
执行单个程序大概花费 131 秒,电脑cpu大概是20%
//exit();

// time
$time = time();

// 统计每个进程需要处理的数据量
$count = ceil(count($data)/$processNum);

// 分配数据
$forks = 0;

while ($forks < $processNum) {

 // 开启子进程
 
$pid = pcntl_fork();
 if ($pid == -1) {

   die("create pcntl error{$pid}".PHP_EOL);

 } else if($pid) {

   // 主进程
   
$forks++;

 } else {

   // 子进程

   
// 处理方式1
   //
数据切份
//    $processData = array_slice($data, $forks * $count, $count);

   //
处理数据
//    foreach ($processData as $v) {
//      file_put_contents(__DIR__."/a.txt", $v.PHP_EOL, FILE_APPEND | LOCK_EX);
//    }

   //
处理方式2
   
$file = '/tmp/test_pcntl_'.$forks.'.json';

   file_put_contents($file, json_encode(array_slice($data, $forks * $count, $count), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));

   pcntl_exec('/usr/local/bin/php', [__DIR__.'/test-pcntl-worker.php', $file], $_ENV);

   // 退出子进程,防止不断循环
   
exit();
 }
}

for (;;) {
 // 等待子进程结束
 
if (pcntl_wait($status, WNOHANG) == -1) {
   break;
 }
}

echo time()-$time, PHP_EOL; // 执行5个进程大概花费 40 秒,电脑cpu大概是30%~50%


test-pcntl-worker.php:子进程,数据处理,复制了主程序的所有信息,执行程序

<?php
// 子进程执行,复制了一份主程序的环境变量

// getenv():获取pcntl_exec 传递的 env_vars 环境变量参数
//var_export(getenv());

// $argv
参数:获取pcntl_exec 传递的 args 参数
//var_export($argv[1]);

$data = json_decode(file_get_contents($argv[1]), true);

foreach ($data as $v) {
 file_put_contents(__DIR__."/test-pcntl.txt", $v.PHP_EOL, FILE_APPEND | LOCK_EX);
 echo $v, PHP_EOL;
}


参考:

PHP: PCNTL - Manual

php如何使用PHP的进程控制功能?

php多进程总结

PHP pcntl_exec 调用多个子进程并行执行,主进程等待全部子进程完成示例

一篇文章吃透PHP进程信号处理(建议收藏)

php中如何实现多进程

php中进程、线程、协程详细讲解

php写守护进程(Daemon)

PHP多进程处理并行处理任务实例

PHP的pcntl进程控制教程一(pcntl_fork)

只袄早~~~
感谢你的支持,我会继续努力!
扫码打赏,感谢您的支持!
PHP PCNTL 进程 子进程 

文明上网理性发言!

  • 还没有评论,沙发等你来抢