こんにちは!今回はPHPのシグナル制御における高度な機能、pcntl_sigtimedwait関数について詳しく解説します。シグナルを同期的に待ちたい、タイムアウト処理を実装したい方は必見です!
pcntl_sigtimedwait関数とは?
pcntl_sigtimedwait()
は、指定したシグナルが到着するまで待機し、タイムアウト時間を設定できる関数です。通常のシグナルハンドラとは異なり、シグナルを同期的に受信できるのが大きな特徴です。
基本構文
pcntl_sigtimedwait(array $signals, array &$info = [], int $seconds = 0, int $nanoseconds = 0): int|false
- $signals: 待機するシグナルの配列
- $info: シグナル情報を格納する配列(参照渡し)
- $seconds: タイムアウト秒数
- $nanoseconds: タイムアウトナノ秒数(0-999,999,999)
- 戻り値: 受信したシグナル番号、タイムアウト時false、エラー時-1
pcntl_sigwaitinfoとの違い
関数 | タイムアウト | 用途 |
---|---|---|
pcntl_sigwaitinfo() | なし(無限待機) | シグナルを確実に受信したい |
pcntl_sigtimedwait() | あり | タイムアウト処理が必要 |
実践的なコード例
基本的な使い方
<?php
echo "プロセスID: " . getmypid() . "\n";
echo "SIGUSR1を送信してください: kill -USR1 " . getmypid() . "\n";
// SIGUSR1をブロック(ハンドラではなく同期的に受信するため)
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]);
echo "SIGUSR1を5秒間待機します...\n";
$info = [];
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);
if ($signal === false) {
echo "タイムアウトしました(5秒以内にシグナルが来なかった)\n";
} elseif ($signal === -1) {
echo "エラーが発生しました\n";
} else {
echo "シグナルを受信しました: {$signal}\n";
echo "シグナル情報:\n";
print_r($info);
}
?>
タイムアウト処理を含むワーカー
<?php
class SignalWorker {
private $running = true;
public function run() {
echo "ワーカー起動: PID=" . getmypid() . "\n";
echo "コマンド例:\n";
echo " - 処理実行: kill -USR1 " . getmypid() . "\n";
echo " - 停止: kill -TERM " . getmypid() . "\n";
// シグナルをブロック(同期的に受信するため)
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1, SIGTERM, SIGINT]);
while ($this->running) {
echo "\n待機中... (タイムアウト:10秒)\n";
$info = [];
$signal = pcntl_sigtimedwait(
[SIGUSR1, SIGTERM, SIGINT],
$info,
10, // 10秒
0
);
if ($signal === false) {
// タイムアウト
echo "[タイムアウト] シグナルなし。ヘルスチェック実行\n";
$this->healthCheck();
} elseif ($signal === SIGUSR1) {
// 処理実行シグナル
echo "[SIGUSR1] 処理を実行します\n";
$this->processTask();
} elseif ($signal === SIGTERM || $signal === SIGINT) {
// 終了シグナル
echo "[終了シグナル] シャットダウンします\n";
$this->running = false;
} else {
echo "[不明なシグナル] {$signal}\n";
}
}
echo "ワーカー終了\n";
}
private function processTask() {
echo " タスク実行中...\n";
sleep(2);
echo " タスク完了\n";
}
private function healthCheck() {
$memory = memory_get_usage(true);
echo " メモリ使用量: " . round($memory / 1024 / 1024, 2) . " MB\n";
}
}
$worker = new SignalWorker();
$worker->run();
?>
高精度タイムアウト(ナノ秒指定)
<?php
echo "PID: " . getmypid() . "\n";
// SIGUSRをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]);
// ナノ秒単位の高精度タイムアウト
echo "待機開始(タイムアウト:1.5秒)\n";
$start = microtime(true);
$signal = pcntl_sigtimedwait(
[SIGUSR1],
$info,
1, // 1秒
500000000 // 500ミリ秒(0.5秒)
);
$elapsed = microtime(true) - $start;
if ($signal === false) {
echo "タイムアウト! 経過時間: " . round($elapsed, 3) . "秒\n";
} else {
echo "シグナル受信: {$signal}, 経過時間: " . round($elapsed, 3) . "秒\n";
}
?>
複数シグナルの優先度制御
<?php
class PrioritySignalHandler {
private $running = true;
public function run() {
echo "PID: " . getmypid() . "\n";
echo "シグナル優先度:\n";
echo " 1. SIGTERM (最優先) - 即座に終了\n";
echo " 2. SIGUSR1 - 通常処理\n";
echo " 3. SIGUSR2 - 低優先度処理\n";
// 全てのシグナルをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGTERM, SIGINT, SIGUSR1, SIGUSR2]);
while ($this->running) {
// まず高優先度シグナルをチェック(タイムアウト0)
$signal = pcntl_sigtimedwait([SIGTERM, SIGINT], $info, 0, 0);
if ($signal !== false) {
echo "\n[緊急] 終了シグナル受信\n";
$this->running = false;
break;
}
// 次に通常優先度シグナルをチェック
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 2, 0);
if ($signal === SIGUSR1) {
echo "\n[通常] SIGUSR1を処理\n";
$this->handleNormalTask();
continue;
}
// 最後に低優先度シグナルをチェック
$signal = pcntl_sigtimedwait([SIGUSR2], $info, 1, 0);
if ($signal === SIGUSR2) {
echo "\n[低優先度] SIGUSR2を処理\n";
$this->handleLowPriorityTask();
continue;
}
// 全てタイムアウトの場合
echo "アイドル状態...\n";
sleep(1);
}
echo "終了\n";
}
private function handleNormalTask() {
echo " 通常タスク実行中\n";
sleep(1);
}
private function handleLowPriorityTask() {
echo " 低優先度タスク実行中\n";
sleep(1);
}
}
$handler = new PrioritySignalHandler();
$handler->run();
?>
シグナル情報の詳細取得
<?php
echo "PID: " . getmypid() . "\n";
echo "SIGUSR1を送信してください: kill -USR1 " . getmypid() . "\n";
// SIGUSRをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]);
echo "待機中...\n";
$info = [];
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 30, 0);
if ($signal !== false) {
echo "\nシグナルを受信しました!\n";
echo "━━━━━━━━━━━━━━━━━━━━━━\n";
echo "シグナル番号: {$signal}\n";
if (isset($info['signo'])) {
echo "signo: {$info['signo']}\n";
}
if (isset($info['errno'])) {
echo "errno: {$info['errno']}\n";
}
if (isset($info['code'])) {
echo "code: {$info['code']}\n";
}
if (isset($info['pid'])) {
echo "送信元PID: {$info['pid']}\n";
}
if (isset($info['uid'])) {
echo "送信元UID: {$info['uid']}\n";
}
if (isset($info['status'])) {
echo "status: {$info['status']}\n";
}
if (isset($info['utime'])) {
echo "utime: {$info['utime']}\n";
}
if (isset($info['stime'])) {
echo "stime: {$info['stime']}\n";
}
echo "━━━━━━━━━━━━━━━━━━━━━━\n";
} else {
echo "タイムアウトまたはエラー\n";
}
?>
ポーリングパターン(タイムアウト0)
<?php
class NonBlockingSignalPoller {
private $running = true;
private $task_queue = [];
public function run() {
echo "ノンブロッキングポーラー起動: PID=" . getmypid() . "\n";
// シグナルをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1, SIGTERM]);
while ($this->running) {
// シグナルをポーリング(即座に戻る)
$signal = pcntl_sigtimedwait([SIGUSR1, SIGTERM], $info, 0, 0);
if ($signal === SIGUSR1) {
echo "[シグナル受信] タスクをキューに追加\n";
$this->task_queue[] = time();
} elseif ($signal === SIGTERM) {
echo "[シグナル受信] 終了要求\n";
$this->running = false;
continue;
}
// メイン処理(シグナルとは独立)
if (!empty($this->task_queue)) {
$task = array_shift($this->task_queue);
echo "タスク処理: {$task}\n";
usleep(500000); // 0.5秒
} else {
echo "アイドル...\n";
usleep(100000); // 0.1秒
}
}
echo "残タスク: " . count($this->task_queue) . "個\n";
echo "終了\n";
}
}
$poller = new NonBlockingSignalPoller();
$poller->run();
?>
子プロセスとの通信
<?php
$pid = pcntl_fork();
if ($pid === -1) {
die("フォーク失敗\n");
} elseif ($pid > 0) {
// 親プロセス
echo "親プロセス: PID=" . getmypid() . "\n";
echo "子プロセス: PID={$pid}\n";
// SIGCHLDをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGCHLD]);
echo "子プロセスの完了を待機中...\n";
$info = [];
$signal = pcntl_sigtimedwait([SIGCHLD], $info, 30, 0);
if ($signal === SIGCHLD) {
echo "子プロセスが終了しました\n";
echo "子プロセスPID: {$info['pid']}\n";
echo "ステータス: {$info['status']}\n";
// ゾンビプロセスを回収
pcntl_waitpid($pid, $status, WNOHANG);
} else {
echo "タイムアウト:子プロセスが応答しません\n";
}
} else {
// 子プロセス
echo "子プロセス起動: PID=" . getmypid() . "\n";
// 何か処理
echo "子:処理実行中...\n";
sleep(3);
echo "子:処理完了\n";
exit(0);
}
?>
リトライ付き待機
<?php
class RetryableSignalWaiter {
private $max_retries;
private $timeout_seconds;
public function __construct($max_retries = 3, $timeout_seconds = 5) {
$this->max_retries = $max_retries;
$this->timeout_seconds = $timeout_seconds;
}
public function waitForSignal($signals) {
echo "シグナル待機開始\n";
// シグナルをブロック
pcntl_sigprocmask(SIG_BLOCK, $signals);
for ($retry = 1; $retry <= $this->max_retries; $retry++) {
echo "\n試行 {$retry}/{$this->max_retries}\n";
echo "待機中 (タイムアウト:{$this->timeout_seconds}秒)...\n";
$info = [];
$signal = pcntl_sigtimedwait(
$signals,
$info,
$this->timeout_seconds,
0
);
if ($signal !== false) {
echo "✓ シグナル受信: {$signal}\n";
return ['success' => true, 'signal' => $signal, 'info' => $info];
}
echo "✗ タイムアウト\n";
if ($retry < $this->max_retries) {
echo "リトライします...\n";
}
}
echo "\n最大リトライ回数に達しました\n";
return ['success' => false];
}
}
echo "PID: " . getmypid() . "\n";
echo "SIGUSR1を送信: kill -USR1 " . getmypid() . "\n\n";
$waiter = new RetryableSignalWaiter(3, 5);
$result = $waiter->waitForSignal([SIGUSR1]);
if ($result['success']) {
echo "\n処理を続行します\n";
} else {
echo "\nシグナルを受信できませんでした\n";
}
?>
エラーハンドリング
<?php
function safe_sigtimedwait($signals, $timeout_sec = 5) {
// シグナルをブロック
$old_mask = [];
if (!pcntl_sigprocmask(SIG_BLOCK, $signals, $old_mask)) {
error_log("シグナルマスクの設定に失敗");
return null;
}
try {
$info = [];
$signal = pcntl_sigtimedwait($signals, $info, $timeout_sec, 0);
if ($signal === false) {
// タイムアウト
return ['status' => 'timeout'];
} elseif ($signal === -1) {
// エラー
$errno = pcntl_get_last_error();
$errmsg = pcntl_strerror($errno);
error_log("pcntl_sigtimedwait エラー: {$errmsg}");
return ['status' => 'error', 'errno' => $errno, 'errmsg' => $errmsg];
} else {
// 成功
return ['status' => 'success', 'signal' => $signal, 'info' => $info];
}
} finally {
// シグナルマスクを復元
pcntl_sigprocmask(SIG_SETMASK, $old_mask);
}
}
// 使用例
echo "PID: " . getmypid() . "\n";
$result = safe_sigtimedwait([SIGUSR1], 3);
switch ($result['status']) {
case 'success':
echo "シグナル受信: {$result['signal']}\n";
break;
case 'timeout':
echo "タイムアウト\n";
break;
case 'error':
echo "エラー: {$result['errmsg']}\n";
break;
}
?>
重要なポイントと注意事項
⚠️ シグナルを必ずブロックする
pcntl_sigtimedwait()を使う前に、対象シグナルを必ずブロックする必要があります:
// ❌ 間違い:ブロックせずに待機
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);
// シグナルハンドラが実行されてしまう可能性
// ✅ 正しい:ブロックしてから待機
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]);
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);
💡 タイムアウト0の活用
タイムアウトを0にすると、ノンブロッキングで動作します:
// シグナルが来ていればすぐ返る、なければfalse
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 0, 0);
🔍 シグナルハンドラとの違い
方式 | 処理タイミング | 用途 |
---|---|---|
ハンドラ | 非同期(任意のタイミング) | イベント駆動型 |
sigtimedwait | 同期(明示的に待機) | 順次処理型 |
// ハンドラ方式:非同期
pcntl_signal(SIGUSR1, function() {
echo "いつ呼ばれるか分からない\n";
});
// sigtimedwait方式:同期
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);
echo "この行に到達する前に確実に待機する\n";
ベストプラクティス
1. タイムアウトは適切に設定
// ✅ 用途に応じた適切なタイムアウト
pcntl_sigtimedwait([SIGUSR1], $info, 30, 0); // 長い処理
pcntl_sigtimedwait([SIGUSR1], $info, 1, 0); // ポーリング
pcntl_sigtimedwait([SIGUSR1], $info, 0, 100000000); // 0.1秒
2. 複数シグナルの優先度管理
// 高優先度シグナルを先にチェック
$signal = pcntl_sigtimedwait([SIGTERM], $info, 0, 0);
if ($signal !== false) {
// 即座に終了
}
// 通常シグナルをチェック
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);
3. エラーチェックを忘れずに
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);
if ($signal === -1) {
// エラー処理
$errno = pcntl_get_last_error();
error_log("Error: " . pcntl_strerror($errno));
} elseif ($signal === false) {
// タイムアウト処理
} else {
// 正常処理
}
トラブルシューティング
シグナルが受信できない
- シグナルをブロックしていますか?
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]); // これが必要!
- 正しいシグナル番号を指定していますか?
// ✓ 正しい pcntl_sigtimedwait([SIGUSR1], $info, 5, 0); // ✗ 間違い pcntl_sigtimedwait([30], $info, 5, 0); // 定数を使う
- シグナルハンドラと競合していませんか?
- ハンドラとsigtimedwaitは併用できません
まとめ
pcntl_sigtimedwait()
は、シグナルを同期的に受信し、タイムアウト制御ができる強力な関数です。重要なポイント:
- ✅ シグナルを同期的に受信(順次処理)
- ✅ タイムアウトを設定して無限待機を防ぐ
- ✅ 使用前に必ずシグナルをブロック
- ✅ ナノ秒単位の高精度タイムアウト
- ✅ シグナル情報の詳細取得が可能
イベント駆動型ではなく、明示的にシグナルを待ち受けたい場合に最適な関数です。プロセス間通信やワーカープロセスの実装に活用しましょう!
関連記事
- pcntl_sigwaitinfo()でシグナルを無限待機
- pcntl_sigprocmask()でシグナルをブロック
- PHPでプロセス間通信を実装する方法