4、进程控制:pcntl扩展介绍、创建子进程、进程间通信、信号处理、实战:多进程任务管理器
进程控制,说白了就是让 PHP 不再只是「单线程跑脚本」的工具。你想想看,一个耗时的任务,比如处理一万张图片,单进程跑得等到猴年马月。这时候,多进程就派上用场了。
我个人习惯把进程控制比作「开分店」。主进程是总店,子进程就是分店。各干各的活,最后汇总结果。嗯,这个比喻虽然糙,但道理不糙。
pcntl 扩展介绍
pcntl 是 PHP 的进程控制扩展。它提供了 fork、wait、signal 等底层操作。说白了,就是让你能在 PHP 里直接操控 Linux 的进程 API。
安装很简单,编译时加上 --enable-pcntl 就行。不过要注意,pcntl 只能在 CLI 模式下运行。Web 模式下用不了,因为 Apache 或 Nginx 的进程模型不允许你乱 fork。
我记得刚开始用 pcntl 时,犯过一个低级错误——在 Web 请求里调了 pcntl_fork()。结果整个 Apache 进程树都乱了,服务器直接挂掉。嗯,从那以后我记住了:pcntl 只属于 CLI。
创建子进程
创建子进程的核心函数是 pcntl_fork()。这个函数很特别,它调用一次,返回两次。父进程返回子进程的 PID,子进程返回 0。如果返回 -1,说明 fork 失败了。
<?php
$pid = pcntl_fork();
if ($pid == -1) {
die('fork 失败');
} elseif ($pid) {
// 父进程代码
echo "我是父进程,子进程 PID 是 $pid\n";
pcntl_wait($status); // 等待子进程结束
} else {
// 子进程代码
echo "我是子进程,PID 是 " . getmypid() . "\n";
exit(0); // 子进程执行完毕
}
这里有个坑:子进程会继承父进程的所有资源,包括文件描述符、数据库连接等。我曾经在项目中遇到过,子进程里操作了父进程的数据库连接,结果导致连接混乱。解决办法很简单——子进程里重新建立连接。
进程间通信
进程间通信,简称 IPC。多进程之间怎么传数据?常见方式有管道、消息队列、共享内存、信号等。
我个人最常用的是管道。PHP 提供了 proc_open() 和 stream_select() 来实现。不过更简单的方式是用 posix_mkfifo() 创建命名管道。
<?php
// 创建管道
$pipe = '/tmp/my_pipe';
if (!file_exists($pipe)) {
posix_mkfifo($pipe, 0666);
}
$pid = pcntl_fork();
if ($pid) {
// 父进程:写数据
$fp = fopen($pipe, 'w');
fwrite($fp, "任务数据\n");
fclose($fp);
pcntl_wait($status);
} else {
// 子进程:读数据
$fp = fopen($pipe, 'r');
$data = fgets($fp);
echo "子进程收到: $data";
fclose($fp);
exit(0);
}
还有一种方式是用共享内存。PHP 的 shmop 扩展可以操作共享内存段。不过说实话,共享内存容易出并发问题,我一般只在性能要求极高的场景下用。
| 通信方式 | 优点 | 缺点 |
|---|---|---|
| 管道 | 简单、可靠 | 只能单向通信 |
| 消息队列 | 支持多对多 | 需要额外扩展 |
| 共享内存 | 速度快 | 需要处理同步 |
| 信号 | 轻量级 | 只能传简单信号 |
信号处理
信号是进程间通信的一种特殊形式。它不传数据,只传一个数字编号。比如 SIGTERM(15)表示终止,SIGKILL(9)表示强制杀死。
PHP 里用 pcntl_signal() 来注册信号处理函数。注意,信号处理是异步的,可能会打断正在执行的代码。
<?php
// 注册信号处理
pcntl_signal(SIGTERM, function ($signo) {
echo "收到 SIGTERM 信号,准备退出\n";
exit(0);
});
// 分发信号
while (true) {
pcntl_signal_dispatch();
// 做其他工作
sleep(1);
}
我曾经在项目中遇到过一个问题:信号处理函数里调用了 exit(),但子进程没有正常退出,变成了僵尸进程。后来我加上了 pcntl_wait() 来回收子进程,问题就解决了。
实战:多进程任务管理器
好了,理论讲完了。我们来写一个真正的多进程任务管理器。这个工具可以同时处理多个任务,比如批量下载文件、处理日志等。
<?php
class TaskManager {
private $maxWorkers = 5;
private $tasks = [];
private $workers = [];
public function addTask($task) {
$this->tasks[] = $task;
}
public function run() {
while (!empty($this->tasks) || !empty($this->workers)) {
// 启动新子进程
while (count($this->workers) < $this->maxWorkers && !empty($this->tasks)) {
$task = array_shift($this->tasks);
$pid = pcntl_fork();
if ($pid == -1) {
die('fork 失败');
} elseif ($pid) {
// 父进程记录子进程
$this->workers[$pid] = $task;
} else {
// 子进程执行任务
$this->executeTask($task);
exit(0);
}
}
// 等待子进程结束
$pid = pcntl_wait($status, WNOHANG);
if ($pid > 0) {
unset($this->workers[$pid]);
echo "任务完成: " . $this->workers[$pid] . "\n";
}
}
}
private function executeTask($task) {
// 模拟任务处理
echo "处理任务: $task\n";
sleep(rand(1, 3));
}
}
// 使用示例
$manager = new TaskManager();
$manager->addTask('下载文件1');
$manager->addTask('下载文件2');
$manager->addTask('处理日志');
$manager->addTask('生成报告');
$manager->run();
这个管理器有几个关键点:
- 控制并发数:
$maxWorkers限制了同时运行的子进程数量。避免一下子 fork 太多,把系统资源吃光。 - 非阻塞等待:
pcntl_wait($status, WNOHANG)不会阻塞主进程。主进程可以继续启动新的子进程。 - 任务队列:用数组模拟队列,先进先出。你也可以用 Redis 或数据库来实现更复杂的队列。
pcntl_wait 没有正确回收子进程。后来我加上了 WNOHANG 标志,并在循环里持续检查,问题才解决。
嗯,到这里你应该对 PHP 多进程控制有了基本了解。记住几个要点:fork 后资源独立、信号处理要轻量、子进程一定要回收。下次遇到批量任务,别再用单线程傻等了,试试多进程吧。