はじめに
PHPでマルチプロセスプログラミングを行う際、子プロセスが終了したのか、それとも一時停止しているだけなのかを区別する必要があります。今回は、子プロセスが停止状態にあるかを判定するpcntl_wifstopped関数について、実践的な例を交えて詳しく解説します。
pcntl_wifstopped関数とは?
pcntl_wifstopped()
は、子プロセスが現在停止(stopped)状態にあるかどうかを判定するPHP関数です。プロセスが終了したのではなく、シグナルによって一時停止されている状態を検出します。
基本構文
pcntl_wifstopped(int $status): bool
パラメータ:
$status
:pcntl_waitpid()
で取得したステータス値(WUNTRACEDオプション使用時)
戻り値:
true
: 子プロセスが停止状態にあるfalse
: 子プロセスが停止していない(実行中または終了済み)
プロセスの停止状態とは?
プロセスには以下の主な状態があります:
- 実行中(Running): 通常の実行状態
- 停止中(Stopped): SIGSTOPやSIGTSTPシグナルで一時停止
- 終了(Terminated): プロセスが完全に終了
停止状態のプロセスは終了していないため、SIGCONTシグナルで再開できます。デバッガやジョブ制御で頻繁に使用される機能です。
WUNTRACEDオプションの重要性
pcntl_wifstopped()
を使用するには、pcntl_waitpid()
の第3引数にWUNTRACEDオプションを指定する必要があります。このオプションがないと、停止中のプロセスの情報は取得できません。
// 正しい使用方法
pcntl_waitpid($pid, $status, WUNTRACED);
if (pcntl_wifstopped($status)) {
echo "プロセスは停止中です\n";
}
実践的なコード例
基本的な使用例
<?php
$pid = pcntl_fork();
if ($pid == -1) {
die("フォークに失敗しました\n");
} elseif ($pid == 0) {
// 子プロセス
echo "子プロセス開始(PID: " . getmypid() . ")\n";
// 長時間実行される処理
for ($i = 1; $i <= 20; $i++) {
echo "作業中... $i/20\n";
sleep(1);
}
exit(0);
} else {
// 親プロセス
echo "親プロセス: 子プロセス(PID: $pid)を生成しました\n";
// 2秒後に子プロセスを停止
sleep(2);
echo "子プロセスを停止します\n";
posix_kill($pid, SIGSTOP);
// 停止状態を確認
$result = pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
if ($result == $pid && pcntl_wifstopped($status)) {
$signal = pcntl_wstopsig($status);
echo "子プロセスが停止しました(シグナル: $signal)\n";
// 3秒待ってから再開
sleep(3);
echo "子プロセスを再開します\n";
posix_kill($pid, SIGCONT);
}
// 最終的な終了を待機
pcntl_waitpid($pid, $status);
echo "子プロセスが終了しました\n";
}
停止シグナルの種類を判定する例
<?php
function checkProcessStatus($pid)
{
$result = pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
if ($result == 0) {
return "実行中";
} elseif ($result == $pid) {
if (pcntl_wifstopped($status)) {
$signal = pcntl_wstopsig($status);
$signalNames = [
SIGSTOP => 'SIGSTOP(強制停止)',
SIGTSTP => 'SIGTSTP(端末停止)',
SIGTTIN => 'SIGTTIN(バックグラウンド読み込み)',
SIGTTOU => 'SIGTTOU(バックグラウンド書き込み)'
];
$signalName = $signalNames[$signal] ?? "シグナル $signal";
return "停止中: $signalName";
} elseif (pcntl_wifexited($status)) {
$exitcode = pcntl_wexitstatus($status);
return "終了済み(終了コード: $exitcode)";
} elseif (pcntl_wifsignaled($status)) {
$signal = pcntl_wtermsig($status);
return "シグナルで終了(シグナル: $signal)";
}
}
return "不明";
}
// 使用例
$pid = pcntl_fork();
if ($pid == 0) {
// 子プロセス
while (true) {
sleep(1);
}
} else {
// 親プロセス
sleep(1);
echo "初期状態: " . checkProcessStatus($pid) . "\n";
// 停止
posix_kill($pid, SIGSTOP);
sleep(1);
echo "停止後: " . checkProcessStatus($pid) . "\n";
// 再開
posix_kill($pid, SIGCONT);
sleep(1);
echo "再開後: " . checkProcessStatus($pid) . "\n";
// 終了
posix_kill($pid, SIGTERM);
pcntl_waitpid($pid, $status);
echo "終了後: 終了しました\n";
}
関連する重要な関数
1. pcntl_wstopsig()
停止中のプロセスが、どのシグナルで停止したかを取得します。
if (pcntl_wifstopped($status)) {
$signal = pcntl_wstopsig($status);
echo "停止シグナル: $signal\n";
}
2. posix_kill() – SIGCONTで再開
停止中のプロセスを再開するには、SIGCONTシグナルを送信します。
// プロセスを停止
posix_kill($pid, SIGSTOP);
// プロセスを再開
posix_kill($pid, SIGCONT);
3. WUNTRACEDオプション
停止状態の検出には必須のオプションです。
// WUNTRACEDを使用しないと停止状態を検出できない
pcntl_waitpid($pid, $status, WUNTRACED);
実用的な応用例: プロセス監視システム
<?php
class ProcessMonitor
{
private $processes = [];
public function spawn($name, callable $task)
{
$pid = pcntl_fork();
if ($pid == -1) {
throw new Exception("フォークに失敗しました");
} elseif ($pid == 0) {
// 子プロセス
$task();
exit(0);
} else {
// 親プロセス
$this->processes[$pid] = [
'name' => $name,
'started' => time(),
'status' => 'running'
];
echo "プロセス '$name' (PID: $pid) を起動しました\n";
return $pid;
}
}
public function pause($pid)
{
if (!isset($this->processes[$pid])) {
echo "エラー: プロセス $pid は存在しません\n";
return false;
}
posix_kill($pid, SIGSTOP);
$this->processes[$pid]['status'] = 'paused';
echo "プロセス {$this->processes[$pid]['name']} を一時停止しました\n";
return true;
}
public function resume($pid)
{
if (!isset($this->processes[$pid])) {
echo "エラー: プロセス $pid は存在しません\n";
return false;
}
posix_kill($pid, SIGCONT);
$this->processes[$pid]['status'] = 'running';
echo "プロセス {$this->processes[$pid]['name']} を再開しました\n";
return true;
}
public function checkStatus($pid)
{
$result = pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
if ($result == 0) {
// まだ実行中
return $this->processes[$pid]['status'];
} elseif ($result == $pid) {
if (pcntl_wifstopped($status)) {
$signal = pcntl_wstopsig($status);
$this->processes[$pid]['status'] = 'stopped';
return "停止中(シグナル: $signal)";
} elseif (pcntl_wifexited($status)) {
$exitcode = pcntl_wexitstatus($status);
unset($this->processes[$pid]);
return "終了(終了コード: $exitcode)";
} elseif (pcntl_wifsignaled($status)) {
$signal = pcntl_wtermsig($status);
unset($this->processes[$pid]);
return "シグナルで終了(シグナル: $signal)";
}
}
return "不明";
}
public function listProcesses()
{
echo "\n=== プロセス一覧 ===\n";
foreach ($this->processes as $pid => $info) {
$status = $this->checkStatus($pid);
$runtime = time() - $info['started'];
echo "PID: $pid | 名前: {$info['name']} | 状態: $status | 実行時間: {$runtime}秒\n";
}
echo "==================\n\n";
}
public function waitAll()
{
while (count($this->processes) > 0) {
foreach (array_keys($this->processes) as $pid) {
$result = pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
if ($result == $pid) {
if (pcntl_wifexited($status) || pcntl_wifsignaled($status)) {
echo "プロセス {$this->processes[$pid]['name']} が終了しました\n";
unset($this->processes[$pid]);
}
}
}
sleep(1);
}
}
}
// 使用例
$monitor = new ProcessMonitor();
// タスク1: カウンター
$pid1 = $monitor->spawn('counter', function() {
for ($i = 1; $i <= 30; $i++) {
echo "カウンター: $i\n";
sleep(1);
}
});
// タスク2: タイマー
$pid2 = $monitor->spawn('timer', function() {
$start = time();
while (time() - $start < 30) {
echo "経過時間: " . (time() - $start) . "秒\n";
sleep(2);
}
});
sleep(3);
$monitor->listProcesses();
// プロセス1を一時停止
$monitor->pause($pid1);
sleep(5);
$monitor->listProcesses();
// プロセス1を再開
$monitor->resume($pid1);
sleep(5);
$monitor->listProcesses();
// すべてのプロセスの終了を待機
$monitor->waitAll();
よくある使用シーン
1. デバッガの実装
デバッガは、対象プロセスを停止・再開しながらデバッグ情報を取得します。
function debugProcess($pid, $breakpoints)
{
// ブレークポイントに到達したらプロセスを停止
posix_kill($pid, SIGSTOP);
$result = pcntl_waitpid($pid, $status, WUNTRACED);
if (pcntl_wifstopped($status)) {
echo "ブレークポイントで停止しました\n";
// デバッグ情報を表示
// ...
// ユーザーの入力を待つ
readline("続行するにはEnterを押してください...");
// プロセスを再開
posix_kill($pid, SIGCONT);
}
}
2. ジョブコントロール
バックグラウンドジョブの一時停止・再開機能の実装。
class JobController
{
private $jobs = [];
public function addJob($pid, $name)
{
$this->jobs[$pid] = ['name' => $name, 'state' => 'running'];
}
public function stopJob($pid)
{
posix_kill($pid, SIGSTOP);
pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
if (pcntl_wifstopped($status)) {
$this->jobs[$pid]['state'] = 'stopped';
echo "ジョブ {$this->jobs[$pid]['name']} を停止しました\n";
}
}
public function resumeJob($pid)
{
posix_kill($pid, SIGCONT);
$this->jobs[$pid]['state'] = 'running';
echo "ジョブ {$this->jobs[$pid]['name']} を再開しました\n";
}
public function listJobs()
{
foreach ($this->jobs as $pid => $job) {
pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
if (pcntl_wifstopped($status)) {
$job['state'] = 'stopped';
}
echo "[$pid] {$job['name']} - {$job['state']}\n";
}
}
}
3. リソース制御
プロセスが多くのリソースを消費している場合、一時停止して他のプロセスに優先権を与える。
function monitorResourceUsage($pid)
{
while (true) {
// CPU使用率を確認(擬似コード)
$cpuUsage = getCpuUsage($pid);
if ($cpuUsage > 80) {
echo "CPU使用率が高いため、プロセスを一時停止します\n";
posix_kill($pid, SIGSTOP);
sleep(5); // 5秒待機
echo "プロセスを再開します\n";
posix_kill($pid, SIGCONT);
}
sleep(1);
}
}
SIGSTOPとSIGTSTPの違い
停止に関連する主なシグナルは2つあります:
SIGSTOP
- キャッチ不可: プロセスがハンドラを設定できない
- 必ず停止: 確実にプロセスを停止させる
- 用途: システムレベルの強制停止
posix_kill($pid, SIGSTOP); // 確実に停止
SIGTSTP
- キャッチ可能: プロセスがハンドラを設定できる
- Ctrl+Z: 端末から送信される
- 用途: ユーザーレベルの一時停止
posix_kill($pid, SIGTSTP); // Ctrl+Zと同等
注意点とベストプラクティス
1. WUNTRACEDオプションは必須
停止状態を検出するには、必ずWUNTRACEDオプションを使用します。
// ✗ 間違い: 停止状態を検出できない
pcntl_waitpid($pid, $status);
// ✓ 正しい: 停止状態を検出できる
pcntl_waitpid($pid, $status, WUNTRACED);
2. WNOHANGと組み合わせる
ノンブロッキングで状態をチェックする場合は、WNOHANGも指定します。
pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
3. 停止後は必ず再開を検討
停止したプロセスを放置すると、リソースリークの原因になります。
if (pcntl_wifstopped($status)) {
// 必要な処理を実行
doSomething();
// プロセスを再開または終了
posix_kill($pid, SIGCONT); // 再開
// または
posix_kill($pid, SIGKILL); // 終了
}
4. 状態判定の順序
プロセスの状態を正しく判定するには、適切な順序でチェックします。
if (pcntl_wifstopped($status)) {
// 停止中
} elseif (pcntl_wifexited($status)) {
// 正常終了
} elseif (pcntl_wifsignaled($status)) {
// シグナルで終了
}
トラブルシューティング
問題1: 停止状態が検出されない
原因: WUNTRACEDオプションを指定していない
解決策:
// WUNTRACEDを追加
pcntl_waitpid($pid, $status, WUNTRACED);
問題2: プロセスが再開しない
原因: SIGCONTシグナルを送信していない
解決策:
if (pcntl_wifstopped($status)) {
// SIGCONTで再開
posix_kill($pid, SIGCONT);
}
問題3: ゾンビプロセスが発生
原因: 停止後に適切にwaitしていない
解決策:
// 最終的には必ずwaitする
posix_kill($pid, SIGKILL);
pcntl_waitpid($pid, $status);
まとめ
pcntl_wifstopped()
は、プロセスの一時停止を検出するための重要な関数です。以下のポイントを押さえておきましょう:
- 目的: 子プロセスが停止状態にあるかを判定
- 必須: WUNTRACEDオプションを使用
- 組み合わせ:
pcntl_wstopsig()
で停止シグナルを取得 - 再開: SIGCONTシグナルでプロセスを再開
- 用途: デバッガ、ジョブコントロール、リソース管理
プロセスの停止・再開機能を理解することで、より柔軟なプロセス管理が可能になります。ぜひ実際のプロジェクトで活用してみてください!