各位观众老爷们,今天咱们来聊聊PHP里那些“不安分”的小家伙——进程!别害怕,不是要你真的去学操作系统原理,而是用 pcntl
扩展,让你的 PHP 代码也能玩转多进程、进程间通信,甚至还能优雅地处理信号。准备好了吗?咱们开始!
一、PHP 的“人格分裂”:多进程编程
PHP 默认是单线程执行的,就像一个勤勤恳恳的工蜂,一次只能处理一个任务。但如果你的任务非常耗时,比如要处理大量的图片、视频,或者要调用外部服务,单线程就显得力不从心了。这时候,就需要让 PHP 来一次“人格分裂”,变成多个进程,每个进程负责处理一部分任务,从而提高整体的效率。
pcntl
扩展就是 PHP 提供的“分裂工具”。它允许你创建、控制和管理进程。
1. 创建子进程:pcntl_fork()
pcntl_fork()
函数是创建子进程的核心。它会复制当前进程的所有内容(代码、数据、文件描述符等等),然后产生一个完全相同的子进程。
<?php
$pid = pcntl_fork();
if ($pid == -1) {
die('啊哦,fork失败了!');
} else if ($pid) {
// 父进程
echo "我是父进程,我的PID是:". getmypid() . ", 子进程的PID是:". $pid . "n";
} else {
// 子进程
echo "我是子进程,我的PID是:". getmypid() . ", 我爹的PID是:". getmypid() . "n";
exit(0); // 子进程执行完毕,一定要退出
}
?>
这段代码简单地创建了一个子进程。注意,pcntl_fork()
会返回不同的值:
-1
: 表示 fork 失败了,可能是系统资源不足。0
: 表示当前进程是子进程。> 0
: 表示当前进程是父进程,返回值是子进程的 PID (进程ID)。
重要提示: 子进程创建后,会从 pcntl_fork()
的下一行代码开始执行。所以,你需要使用 if...else
结构来区分父进程和子进程,并让它们执行不同的任务。子进程执行完自己的任务后,一定要调用 exit()
函数退出,否则可能会出现意想不到的问题。
2. 等待子进程:pcntl_wait()
和 pcntl_waitpid()
父进程创建了子进程后,通常需要等待子进程执行完毕。pcntl_wait()
和 pcntl_waitpid()
函数就是用来做这件事的。
pcntl_wait(&$status)
: 等待任意一个子进程结束。$status
参数用来接收子进程的退出状态。pcntl_waitpid($pid, &$status, $options)
: 等待指定 PID 的子进程结束。$pid
参数指定要等待的子进程的 PID。$options
参数可以控制等待的行为,例如是否阻塞等待。
<?php
$pid = pcntl_fork();
if ($pid == -1) {
die('啊哦,fork失败了!');
} else if ($pid) {
// 父进程
echo "我是父进程,我的PID是:". getmypid() . ", 子进程的PID是:". $pid . "n";
pcntl_waitpid($pid, $status); // 等待指定的子进程
echo "子进程 ". $pid . " 已经结束,退出状态是:". $status . "n";
} else {
// 子进程
echo "我是子进程,我的PID是:". getmypid() . ", 我爹的PID是:". getmypid() . "n";
sleep(2); // 模拟耗时操作
exit(0); // 子进程执行完毕,一定要退出
}
?>
3. 执行外部程序:pcntl_exec()
除了执行 PHP 代码,子进程还可以执行外部程序。pcntl_exec()
函数就是用来做这件事的。
<?php
$pid = pcntl_fork();
if ($pid == -1) {
die('啊哦,fork失败了!');
} else if ($pid) {
// 父进程
echo "我是父进程,我的PID是:". getmypid() . ", 子进程的PID是:". $pid . "n";
pcntl_waitpid($pid, $status);
echo "子进程 ". $pid . " 已经结束,退出状态是:". $status . "n";
} else {
// 子进程
pcntl_exec('/usr/bin/ls', ['-l']); // 执行 ls -l 命令
exit(0); // 理论上这里不会执行,因为exec已经替换了当前进程
}
?>
pcntl_exec()
函数的第一个参数是要执行的程序路径,第二个参数是传递给程序的参数数组。需要注意的是,pcntl_exec()
函数会替换当前进程,所以执行完 pcntl_exec()
后,子进程的代码就不会再执行了。
二、兄弟齐心,其利断金:进程间通信 (IPC)
多进程之间需要交换数据、共享资源,这就是进程间通信 (IPC)。 pcntl
扩展并没有提供非常完善的 IPC 机制,但我们可以借助一些其他的方法来实现。
1. 管道 (Pipes):proc_open()
管道是一种简单的 IPC 方式,它允许进程之间单向地传递数据。 proc_open()
函数可以创建管道,并启动一个新的进程。
<?php
$descriptorspec = array(
0 => array("pipe", "r"), // 标准输入,子进程从这里读取
1 => array("pipe", "w"), // 标准输出,子进程向这里写入
2 => array("pipe", "w") // 标准错误输出,子进程向这里写入
);
$process = proc_open('wc -w', $descriptorspec, $pipes);
if (is_resource($process)) {
// 向子进程的标准输入写入数据
fwrite($pipes[0], "Hello World!n");
fclose($pipes[0]);
// 从子进程的标准输出读取数据
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
// 从子进程的标准错误输出读取数据
$error = stream_get_contents($pipes[2]);
fclose($pipes[2]);
// 关闭进程
$return_value = proc_close($process);
echo "Word count: " . trim($output) . "n";
echo "Error: " . $error . "n";
echo "Return value: " . $return_value . "n";
}
?>
这段代码使用 proc_open()
函数启动了 wc -w
命令,并将标准输入、标准输出和标准错误输出都连接到管道。父进程可以通过管道向子进程发送数据,并从子进程接收数据。
2. 共享内存 (Shared Memory):shmop
扩展
共享内存是一种更高级的 IPC 方式,它允许进程之间直接访问同一块内存区域。PHP 提供了 shmop
扩展来支持共享内存。
注意: shmop
扩展默认没有启用,需要在 php.ini
中启用。
<?php
// 创建共享内存段
$shm_key = ftok(__FILE__, 't'); // 生成一个唯一的键
$shm_id = shmop_open($shm_key, "c", 0644, 1024); // 创建一个 1024 字节的共享内存段
if (!$shm_id) {
die("无法创建共享内存段n");
}
// 父进程
$pid = pcntl_fork();
if ($pid == -1) {
die('啊哦,fork失败了!');
} else if ($pid) {
// 父进程
echo "我是父进程,我的PID是:". getmypid() . "n";
shmop_write($shm_id, "Hello from parent!", 0); // 向共享内存写入数据
pcntl_waitpid($pid, $status);
echo "子进程已经结束n";
// 读取共享内存中的数据
$data = shmop_read($shm_id, 0, 1024);
echo "父进程读取到共享内存中的数据:". $data . "n";
shmop_close($shm_id); // 关闭共享内存段
shmop_delete($shm_id); // 删除共享内存段
} else {
// 子进程
echo "我是子进程,我的PID是:". getmypid() . "n";
sleep(1); // 等待父进程写入数据
// 读取共享内存中的数据
$data = shmop_read($shm_id, 0, 1024);
echo "子进程读取到共享内存中的数据:". $data . "n";
shmop_write($shm_id, "Hello from child!", 0); // 向共享内存写入数据
shmop_close($shm_id); // 关闭共享内存段
exit(0);
}
?>
这段代码创建了一个共享内存段,父进程和子进程都可以访问这块内存。父进程向共享内存写入数据,子进程可以读取到这些数据,反之亦然。
3. 消息队列 (Message Queues):msg_queue
扩展
消息队列是一种异步的 IPC 方式,它允许进程之间通过发送和接收消息来交换数据。PHP 提供了 msg_queue
扩展来支持消息队列。
注意: msg_queue
扩展默认没有启用,需要在 php.ini
中启用。
<?php
// 创建消息队列
$queue_key = ftok(__FILE__, 'm'); // 生成一个唯一的键
$queue_id = msg_get_queue($queue_key, 0666 | MSG_IPC_CREAT);
if (!$queue_id) {
die("无法创建消息队列n");
}
// 父进程
$pid = pcntl_fork();
if ($pid == -1) {
die('啊哦,fork失败了!');
} else if ($pid) {
// 父进程
echo "我是父进程,我的PID是:". getmypid() . "n";
$message = ["type" => 1, "text" => "Hello from parent!"];
msg_send($queue_id, 1, $message); // 向消息队列发送消息
pcntl_waitpid($pid, $status);
echo "子进程已经结束n";
} else {
// 子进程
echo "我是子进程,我的PID是:". getmypid() . "n";
msg_receive($queue_id, 1, $message_type, 1024, $message); // 从消息队列接收消息
echo "子进程接收到消息:". $message["text"] . "n";
exit(0);
}
?>
这段代码创建了一个消息队列,父进程向消息队列发送一条消息,子进程可以从消息队列接收到这条消息。
4. Socket 通信
Socket 是一种通用的网络通信方式,也可以用于进程间通信。你可以使用 socket
扩展创建 Unix Domain Socket,实现本地进程间的通信。 因为比较复杂,这里只给出概念,不提供示例代码。
IPC 方式选择建议:
IPC 方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
管道 | 简单易用 | 单向通信,效率较低 | 简单的进程间数据传递 |
共享内存 | 效率高,速度快 | 需要考虑同步问题,容易出错 | 大量数据共享,对性能要求高的场景 |
消息队列 | 异步通信,可靠性高 | 效率相对较低,需要序列化和反序列化数据 | 异步任务处理,进程间解耦 |
Socket通信 | 通用性强,可以跨机器通信 | 复杂度高,需要编写较多的代码 | 需要跨机器通信,或者需要更灵活的通信方式 |
三、优雅地谢幕:信号处理
在多进程编程中,信号是一种重要的进程间通信方式。它可以用来通知进程发生了某个事件,例如收到一个请求、发生了错误等等。
pcntl
扩展提供了信号处理的功能。
1. 注册信号处理函数:pcntl_signal()
pcntl_signal()
函数用来注册信号处理函数。当进程收到指定的信号时,系统会自动调用该函数。
<?php
// 信号处理函数
function signal_handler($signal) {
switch($signal) {
case SIGTERM:
echo "收到 SIGTERM 信号,准备退出...n";
exit(0);
break;
case SIGUSR1:
echo "收到 SIGUSR1 信号,执行自定义操作...n";
// 执行一些自定义操作
break;
default:
echo "收到未知信号:". $signal . "n";
}
}
// 注册信号处理函数
pcntl_signal(SIGTERM, "signal_handler");
pcntl_signal(SIGUSR1, "signal_handler");
// 主循环
while (true) {
echo "程序正在运行...n";
sleep(1);
pcntl_signal_dispatch(); // 处理未决信号
}
?>
这段代码注册了 SIGTERM
和 SIGUSR1
信号的处理函数。当进程收到 SIGTERM
信号时,会执行退出操作。当进程收到 SIGUSR1
信号时,会执行一些自定义操作。
重要提示: 在 PHP 中,信号处理函数必须是全局函数,不能是类的成员函数。另外,为了确保信号处理函数能够被及时调用,需要在主循环中调用 pcntl_signal_dispatch()
函数,它会处理所有未决的信号。
2. 发送信号:posix_kill()
posix_kill()
函数用来向指定的进程发送信号。
<?php
$pid = pcntl_fork();
if ($pid == -1) {
die('啊哦,fork失败了!');
} else if ($pid) {
// 父进程
echo "我是父进程,我的PID是:". getmypid() . ", 子进程的PID是:". $pid . "n";
sleep(2); // 等待子进程启动
posix_kill($pid, SIGUSR1); // 向子进程发送 SIGUSR1 信号
pcntl_waitpid($pid, $status);
echo "子进程已经结束n";
} else {
// 子进程
echo "我是子进程,我的PID是:". getmypid() . "n";
pcntl_signal(SIGUSR1, function($signal) {
echo "子进程收到 SIGUSR1 信号n";
});
while (true) {
pcntl_signal_dispatch();
sleep(1);
}
exit(0);
}
?>
这段代码中,父进程向子进程发送 SIGUSR1
信号,子进程收到信号后会执行相应的处理函数。
常用的信号:
信号名称 | 含义 |
---|---|
SIGTERM | 终止信号 (通常由 kill 命令发送) |
SIGKILL | 强制终止信号 (无法被捕获) |
SIGINT | 中断信号 (通常由 Ctrl+C 发送) |
SIGUSR1 | 用户自定义信号 1 |
SIGUSR2 | 用户自定义信号 2 |
SIGHUP | 挂起信号 (通常由终端断开连接发送) |
SIGCHLD | 子进程状态改变信号 (子进程结束、停止或继续) |
四、多进程编程的注意事项
- 资源竞争: 多个进程同时访问共享资源时,可能会发生资源竞争。需要使用锁机制 (例如
flock()
函数) 来保护共享资源。 - 僵尸进程: 子进程结束后,如果没有被父进程回收,就会变成僵尸进程,占用系统资源。父进程必须调用
pcntl_wait()
或pcntl_waitpid()
函数来回收子进程。 - 信号处理: 信号处理函数必须是全局函数,并且需要在主循环中调用
pcntl_signal_dispatch()
函数。 - 错误处理: 多进程编程更容易出错,需要仔细考虑各种错误情况,并做好错误处理。
- 调试: 多进程程序的调试比较困难,可以使用
var_dump()
、error_log()
等函数来输出调试信息。 - 进程数量: 不要创建过多的进程,否则会占用大量的系统资源,反而降低效率。
五、总结
pcntl
扩展为 PHP 带来了强大的多进程编程能力。通过 pcntl_fork()
、pcntl_wait()
、pcntl_exec()
等函数,你可以创建、控制和管理进程。通过管道、共享内存、消息队列等方式,你可以实现进程间通信。通过信号处理,你可以优雅地处理各种事件。
当然,多进程编程也需要注意一些问题,例如资源竞争、僵尸进程、信号处理等等。只有掌握了这些知识,才能编写出高效、稳定、可靠的多进程 PHP 程序。
好了,今天的讲座就到这里。希望大家能够掌握 pcntl
扩展的使用方法,让你的 PHP 代码也能玩转多进程。下次再见!