技术饭
PHP:PCNTL进程控制功能的基础使用
PHP:PCNTL进程控制功能的基础使用,多进程的作用主要用于日志分析、队列处理、批量处理等,如要处理10w级别的数据,一条数据需要执行1秒,那么一个进程不间断需要执行1天多的时间,但是如果分成20个进程(进程过多会导出cpu爆满),每个进程分5000条只需执行1.3小时左右就完成任务了。
php多进程需要pcntl、posix扩展支持,请先下载配置扩展,必须在命令行 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
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:多进程无限循环
// 最大的子进程数量
$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() 方法设置闹铃定时器
// 配合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()主动杀死子进程
/**
* 父进程通过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。
// 需求:数据量有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:子进程,数据处理,复制了主程序的所有信息,执行程序
// 子进程执行,复制了一份主程序的环境变量
// 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;
}
参考:
文明上网理性发言!