はじめに
PHPでプロセス制御を行う際、シグナル処理は重要な要素です。しかし、PHP 7.1より前は、シグナルを受け取るために pcntl_signal_dispatch()
を明示的に呼び出す必要がありました。
PHP 7.1で導入された pcntl_async_signals
関数は、この面倒な作業を不要にし、シグナルを自動的に非同期で処理できるようにします。この記事では、モダンなPHPにおけるシグナル処理の新しい方法について詳しく解説します。
pcntl_async_signals関数とは
pcntl_async_signals
は、シグナルを非同期で処理するかどうかを設定する関数です。有効にすると、pcntl_signal_dispatch()
を明示的に呼び出さなくても、シグナルハンドラが自動的に実行されます。
基本構文
bool pcntl_async_signals([bool $enable = null])
パラメータ
- $enable (bool, optional):
- true: 非同期シグナル処理を有効化
- false: 非同期シグナル処理を無効化
- null (省略時): 現在の設定を取得
戻り値
- bool: パラメータを指定した場合は常にtrue、省略した場合は現在の設定状態
対応バージョン
- PHP 7.1.0以降で利用可能
前提条件
<?php
// 環境チェック
if (php_sapi_name() !== 'cli') {
die("この機能はCLI環境でのみ利用可能です。\n");
}
if (!function_exists('pcntl_async_signals')) {
die("pcntl_async_signals関数が利用できません(PHP 7.1.0以降が必要)。\n");
}
if (version_compare(PHP_VERSION, '7.1.0', '<')) {
die("PHP 7.1.0以降が必要です(現在: " . PHP_VERSION . ")。\n");
}
echo "✓ 環境チェック完了\n";
?>
従来の方法との比較
従来の方法(PHP 7.0以前)
<?php
// 従来の方法: pcntl_signal_dispatch()を明示的に呼び出す必要がある
$received_signal = false;
// シグナルハンドラ登録
pcntl_signal(SIGTERM, function($signo) use (&$received_signal) {
echo "SIGTERMを受信しました\n";
$received_signal = true;
});
echo "プロセス起動(PID: " . getmypid() . ")\n";
echo "別ターミナルから 'kill -TERM " . getmypid() . "' を実行してください\n\n";
// メインループ
while (!$received_signal) {
echo "処理中...\n";
// 重要: シグナルを処理するために明示的な呼び出しが必要
pcntl_signal_dispatch();
sleep(1);
}
echo "終了します\n";
?>
新しい方法(PHP 7.1以降)
<?php
// 新しい方法: 非同期シグナル処理を有効化
$received_signal = false;
// 非同期シグナル処理を有効化
pcntl_async_signals(true);
// シグナルハンドラ登録
pcntl_signal(SIGTERM, function($signo) use (&$received_signal) {
echo "SIGTERMを受信しました\n";
$received_signal = true;
});
echo "プロセス起動(PID: " . getmypid() . ")\n";
echo "別ターミナルから 'kill -TERM " . getmypid() . "' を実行してください\n\n";
// メインループ
while (!$received_signal) {
echo "処理中...\n";
// pcntl_signal_dispatch()の呼び出しが不要!
sleep(1);
}
echo "終了します\n";
?>
基本的な使用例
シンプルな非同期シグナル処理
<?php
// 非同期シグナル処理を有効化
pcntl_async_signals(true);
$signals_received = [];
// 複数のシグナルハンドラを登録
pcntl_signal(SIGTERM, function($signo) use (&$signals_received) {
$signals_received[] = 'SIGTERM';
echo "[" . date('H:i:s') . "] SIGTERM受信\n";
});
pcntl_signal(SIGINT, function($signo) use (&$signals_received) {
$signals_received[] = 'SIGINT';
echo "[" . date('H:i:s') . "] SIGINT受信(Ctrl+C)\n";
exit(0);
});
pcntl_signal(SIGUSR1, function($signo) use (&$signals_received) {
$signals_received[] = 'SIGUSR1';
echo "[" . date('H:i:s') . "] SIGUSR1受信(カスタムシグナル)\n";
});
echo "PID: " . getmypid() . "\n";
echo "Ctrl+Cで終了、または以下のコマンドでシグナル送信:\n";
echo " kill -USR1 " . getmypid() . "\n";
echo " kill -TERM " . getmypid() . "\n\n";
// シンプルなメインループ
$counter = 0;
while (true) {
echo "実行中... {$counter}\n";
$counter++;
sleep(2);
}
?>
設定状態の確認
<?php
// 現在の設定を取得
$current_state = pcntl_async_signals();
echo "現在の非同期シグナル設定: " . ($current_state ? '有効' : '無効') . "\n";
// 有効化
pcntl_async_signals(true);
echo "非同期シグナルを有効化しました\n";
echo "設定: " . (pcntl_async_signals() ? '有効' : '無効') . "\n\n";
// 無効化
pcntl_async_signals(false);
echo "非同期シグナルを無効化しました\n";
echo "設定: " . (pcntl_async_signals() ? '有効' : '無効') . "\n";
?>
実践的な活用例
1. グレースフルシャットダウンの実装
<?php
class GracefulShutdownHandler {
private $is_shutting_down = false;
private $tasks_running = 0;
private $start_time;
public function __construct() {
$this->start_time = time();
// 非同期シグナル処理を有効化
pcntl_async_signals(true);
// シャットダウンシグナルを登録
pcntl_signal(SIGTERM, [$this, 'handleShutdown']);
pcntl_signal(SIGINT, [$this, 'handleShutdown']);
echo "グレースフルシャットダウンハンドラを初期化しました\n";
echo "PID: " . getmypid() . "\n\n";
}
/**
* シャットダウンシグナルハンドラ
*/
public function handleShutdown($signo) {
$signal_name = $signo === SIGTERM ? 'SIGTERM' : 'SIGINT';
echo "\n[" . date('H:i:s') . "] {$signal_name}を受信しました\n";
if ($this->is_shutting_down) {
echo "既にシャットダウン処理中です\n";
return;
}
$this->is_shutting_down = true;
echo "グレースフルシャットダウンを開始します...\n";
// 実行中のタスクを待機
$this->waitForTasks();
// クリーンアップ処理
$this->cleanup();
$uptime = time() - $this->start_time;
echo "稼働時間: {$uptime}秒\n";
echo "正常終了しました\n";
exit(0);
}
/**
* タスク実行(シミュレーション)
*/
public function runTask($task_id, $duration) {
if ($this->is_shutting_down) {
echo "シャットダウン中のため、新規タスクは受け付けません\n";
return false;
}
$this->tasks_running++;
echo "[タスク{$task_id}] 開始(予定時間: {$duration}秒)\n";
sleep($duration);
echo "[タスク{$task_id}] 完了\n";
$this->tasks_running--;
return true;
}
/**
* 実行中タスクの待機
*/
private function waitForTasks() {
if ($this->tasks_running > 0) {
echo "実行中のタスク({$this->tasks_running}個)の完了を待機中...\n";
$timeout = 30; // 最大30秒待機
$elapsed = 0;
while ($this->tasks_running > 0 && $elapsed < $timeout) {
sleep(1);
$elapsed++;
echo " 待機中... 残りタスク: {$this->tasks_running}\n";
}
if ($this->tasks_running > 0) {
echo "⚠️ タイムアウト: {$this->tasks_running}個のタスクが未完了\n";
} else {
echo "✓ すべてのタスクが完了しました\n";
}
}
}
/**
* クリーンアップ処理
*/
private function cleanup() {
echo "クリーンアップ処理を実行中...\n";
// データベース接続のクローズ
echo " - データベース接続をクローズ\n";
// 一時ファイルの削除
echo " - 一時ファイルを削除\n";
// ログファイルのフラッシュ
echo " - ログファイルをフラッシュ\n";
echo "✓ クリーンアップ完了\n";
}
/**
* メインループ
*/
public function run() {
$task_counter = 1;
while (!$this->is_shutting_down) {
echo "\n[" . date('H:i:s') . "] タスクを実行します\n";
$this->runTask($task_counter, rand(1, 3));
$task_counter++;
sleep(2);
}
}
}
// 使用例
$handler = new GracefulShutdownHandler();
$handler->run();
?>
2. ワーカープロセスの管理
<?php
class WorkerManager {
private $workers = [];
private $max_workers = 5;
private $should_stop = false;
public function __construct($max_workers = 5) {
$this->max_workers = $max_workers;
// 非同期シグナル処理を有効化
pcntl_async_signals(true);
// シグナルハンドラ登録
pcntl_signal(SIGTERM, [$this, 'handleTerminate']);
pcntl_signal(SIGINT, [$this, 'handleTerminate']);
pcntl_signal(SIGCHLD, [$this, 'handleChildExit']);
pcntl_signal(SIGUSR1, [$this, 'handleStatus']);
echo "ワーカーマネージャーを初期化しました\n";
echo "PID: " . getmypid() . "\n";
echo "最大ワーカー数: {$this->max_workers}\n";
echo "ステータス確認: kill -USR1 " . getmypid() . "\n\n";
}
/**
* 終了シグナルハンドラ
*/
public function handleTerminate($signo) {
echo "\n終了シグナルを受信しました\n";
$this->should_stop = true;
$this->stopAllWorkers();
}
/**
* 子プロセス終了ハンドラ
*/
public function handleChildExit($signo) {
// ゾンビプロセスを回収
while (($pid = pcntl_waitpid(-1, $status, WNOHANG)) > 0) {
if (isset($this->workers[$pid])) {
$worker = $this->workers[$pid];
$exit_code = pcntl_wexitstatus($status);
echo "[ワーカー{$worker['id']}] 終了(PID: {$pid}, 終了コード: {$exit_code})\n";
unset($this->workers[$pid]);
}
}
}
/**
* ステータス表示ハンドラ
*/
public function handleStatus($signo) {
echo "\n=== ワーカーステータス ===\n";
echo "稼働中のワーカー数: " . count($this->workers) . " / {$this->max_workers}\n";
foreach ($this->workers as $pid => $worker) {
$uptime = time() - $worker['start_time'];
echo " ワーカー{$worker['id']} (PID: {$pid}) - 稼働時間: {$uptime}秒\n";
}
echo "======================\n\n";
}
/**
* ワーカーを起動
*/
public function spawnWorker($worker_id) {
$pid = pcntl_fork();
if ($pid === -1) {
echo "ワーカーの起動に失敗しました\n";
return false;
} elseif ($pid === 0) {
// 子プロセス
$this->workerProcess($worker_id);
exit(0);
} else {
// 親プロセス
$this->workers[$pid] = [
'id' => $worker_id,
'pid' => $pid,
'start_time' => time()
];
echo "[ワーカー{$worker_id}] 起動(PID: {$pid})\n";
return true;
}
}
/**
* ワーカープロセスの処理
*/
private function workerProcess($worker_id) {
// 子プロセスでも非同期シグナルを有効化
pcntl_async_signals(true);
$stop = false;
pcntl_signal(SIGTERM, function() use (&$stop) {
$stop = true;
});
echo "[ワーカー{$worker_id}] 処理開始\n";
$job_count = 0;
while (!$stop && $job_count < 10) {
echo "[ワーカー{$worker_id}] ジョブ{$job_count}を処理中\n";
sleep(rand(1, 3));
$job_count++;
}
echo "[ワーカー{$worker_id}] 処理完了({$job_count}ジョブ)\n";
}
/**
* すべてのワーカーを停止
*/
private function stopAllWorkers() {
echo "すべてのワーカーに停止シグナルを送信します\n";
foreach ($this->workers as $pid => $worker) {
echo " ワーカー{$worker['id']} (PID: {$pid}) に停止シグナル送信\n";
posix_kill($pid, SIGTERM);
}
// すべてのワーカーが終了するまで待機
$timeout = 10;
$elapsed = 0;
while (count($this->workers) > 0 && $elapsed < $timeout) {
sleep(1);
$elapsed++;
}
if (count($this->workers) > 0) {
echo "⚠️ 一部のワーカーが応答しません。強制終了します\n";
foreach ($this->workers as $pid => $worker) {
posix_kill($pid, SIGKILL);
}
} else {
echo "✓ すべてのワーカーが正常に終了しました\n";
}
}
/**
* マネージャーを実行
*/
public function run() {
// 初期ワーカーを起動
for ($i = 1; $i <= $this->max_workers; $i++) {
$this->spawnWorker($i);
usleep(100000); // 0.1秒待機
}
// メインループ
while (!$this->should_stop) {
sleep(1);
}
echo "ワーカーマネージャーを終了します\n";
}
}
// 使用例
$manager = new WorkerManager(3);
$manager->run();
?>
3. 動的な設定リロード
<?php
class ConfigReloadHandler {
private $config = [];
private $config_file;
private $last_reload;
public function __construct($config_file) {
$this->config_file = $config_file;
// 非同期シグナル処理を有効化
pcntl_async_signals(true);
// SIGHUP(設定リロード用の慣習的なシグナル)を登録
pcntl_signal(SIGHUP, [$this, 'handleReload']);
// 初期設定を読み込み
$this->loadConfig();
echo "設定リロードハンドラーを初期化しました\n";
echo "PID: " . getmypid() . "\n";
echo "設定リロード: kill -HUP " . getmypid() . "\n\n";
}
/**
* 設定リロードハンドラ
*/
public function handleReload($signo) {
echo "\n[" . date('H:i:s') . "] SIGHUPを受信 - 設定をリロードします\n";
$old_config = $this->config;
$this->loadConfig();
// 変更された設定を表示
$this->displayConfigChanges($old_config, $this->config);
}
/**
* 設定ファイルを読み込み
*/
private function loadConfig() {
if (!file_exists($this->config_file)) {
echo "⚠️ 設定ファイルが見つかりません: {$this->config_file}\n";
$this->createDefaultConfig();
}
$json = file_get_contents($this->config_file);
$this->config = json_decode($json, true) ?? [];
$this->last_reload = time();
echo "✓ 設定を読み込みました: " . count($this->config) . "項目\n";
}
/**
* デフォルト設定を作成
*/
private function createDefaultConfig() {
$default_config = [
'app_name' => 'MyApp',
'debug' => false,
'max_connections' => 100,
'timeout' => 30
];
file_put_contents(
$this->config_file,
json_encode($default_config, JSON_PRETTY_PRINT)
);
echo "デフォルト設定ファイルを作成しました\n";
}
/**
* 設定変更を表示
*/
private function displayConfigChanges($old_config, $new_config) {
echo "\n=== 設定変更 ===\n";
$all_keys = array_unique(array_merge(
array_keys($old_config),
array_keys($new_config)
));
$changes_found = false;
foreach ($all_keys as $key) {
$old_value = $old_config[$key] ?? null;
$new_value = $new_config[$key] ?? null;
if ($old_value !== $new_value) {
$changes_found = true;
if ($old_value === null) {
echo "[追加] {$key}: " . json_encode($new_value) . "\n";
} elseif ($new_value === null) {
echo "[削除] {$key}\n";
} else {
echo "[変更] {$key}: " . json_encode($old_value) .
" → " . json_encode($new_value) . "\n";
}
}
}
if (!$changes_found) {
echo "変更はありませんでした\n";
}
echo "===============\n\n";
}
/**
* 現在の設定を表示
*/
public function displayCurrentConfig() {
echo "\n=== 現在の設定 ===\n";
foreach ($this->config as $key => $value) {
echo "{$key}: " . json_encode($value) . "\n";
}
echo "最終リロード: " . date('Y-m-d H:i:s', $this->last_reload) . "\n";
echo "================\n\n";
}
/**
* 設定値を取得
*/
public function get($key, $default = null) {
return $this->config[$key] ?? $default;
}
/**
* メインループ
*/
public function run() {
$counter = 0;
while (true) {
if ($counter % 10 === 0) {
$this->displayCurrentConfig();
}
echo "実行中... {$counter} (debug=" .
($this->get('debug') ? 'true' : 'false') . ")\n";
$counter++;
sleep(2);
}
}
}
// 使用例
$config_file = '/tmp/app_config.json';
$handler = new ConfigReloadHandler($config_file);
echo "設定ファイルを編集してから以下を実行:\n";
echo " kill -HUP " . getmypid() . "\n\n";
$handler->run();
?>
4. パフォーマンス統計の収集
<?php
class PerformanceMonitor {
private $stats = [
'requests_processed' => 0,
'total_time' => 0,
'min_time' => PHP_FLOAT_MAX,
'max_time' => 0,
'errors' => 0
];
private $start_time;
public function __construct() {
$this->start_time = microtime(true);
// 非同期シグナル処理を有効化
pcntl_async_signals(true);
// 統計表示用のシグナルを登録
pcntl_signal(SIGUSR1, [$this, 'displayStats']);
pcntl_signal(SIGUSR2, [$this, 'resetStats']);
echo "パフォーマンスモニターを初期化しました\n";
echo "PID: " . getmypid() . "\n";
echo "統計表示: kill -USR1 " . getmypid() . "\n";
echo "統計リセット: kill -USR2 " . getmypid() . "\n\n";
}
/**
* 統計表示ハンドラ
*/
public function displayStats($signo) {
echo "\n" . str_repeat("=", 60) . "\n";
echo "パフォーマンス統計\n";
echo str_repeat("=", 60) . "\n";
$uptime = microtime(true) - $this->start_time;
$avg_time = $this->stats['requests_processed'] > 0
? $this->stats['total_time'] / $this->stats['requests_processed']
: 0;
$rps = $this->stats['requests_processed'] / $uptime;
echo sprintf("稼働時間: %.2f秒\n", $uptime);
echo sprintf("処理リクエスト数: %d\n", $this->stats['requests_processed']);
echo sprintf("リクエスト/秒: %.2f\n", $rps);
echo sprintf("平均処理時間: %.4f秒\n", $avg_time);
echo sprintf("最小処理時間: %.4f秒\n",
$this->stats['min_time'] === PHP_FLOAT_MAX ? 0 : $this->stats['min_time']);
echo sprintf("最大処理時間: %.4f秒\n", $this->stats['max_time']);
echo sprintf("エラー数: %d\n", $this->stats['errors']);
if ($this->stats['requests_processed'] > 0) {
$error_rate = ($this->stats['errors'] / $this->stats['requests_processed']) * 100;
echo sprintf("エラー率: %.2f%%\n", $error_rate);
}
echo str_repeat("=", 60) . "\n\n";
}
/**
* 統計リセットハンドラ
*/
public function resetStats($signo) {
echo "\n統計をリセットします\n";
$this->stats = [
'requests_processed' => 0,
'total_time' => 0,
'min_time' => PHP_FLOAT_MAX,
'max_time' => 0,
'errors' => 0
];
$this->start_time = microtime(true);
echo "✓ 統計をリセットしました\n\n";
}
/**
* リクエスト処理(シミュレーション)
*/
public function processRequest() {
$start = microtime(true);
// ランダムな処理時間
$processing_time = rand(10, 500) / 1000; // 0.01〜0.5秒
usleep($processing_time * 1000000);
// ランダムにエラーを発生させる(5%の確率)
$has_error = rand(1, 100) <= 5;
$elapsed = microtime(true) - $start;
// 統計を更新
$this->stats['requests_processed']++;
$this->stats['total_time'] += $elapsed;
$this->stats['min_time'] = min($this->stats['min_time'], $elapsed);
$this->stats['max_time'] = max($this->stats['max_time'], $elapsed);
if ($has_error) {
$this->stats['errors']++;
echo "✗ ";
} else {
echo "✓ ";
}
if ($this->stats['requests_processed'] % 50 === 0) {
echo "\n";
}
}
/**
* メインループ
*/
public function run() {
echo "リクエスト処理を開始します...\n\n";
while (true) {
$this->processRequest();
usleep(100000); // 0.1秒
}
}
}
// 使用例
$monitor = new PerformanceMonitor();
$monitor->run();
?>
注意点とベストプラクティス
リエントラント安全性への配慮
<?php
/**
* シグナルハンドラはいつでも実行される可能性があるため、
* リエントラント安全性を考慮する必要がある
*/
pcntl_async_signals(true);
$processing = false;
$signal_count = 0;
pcntl_signal(SIGUSR1, function($signo) use (&$processing, &$signal_count) {
$signal_count++;
// 処理中にシグナルが来た場合の対応
if ($processing) {
echo "[⚠️ 警告] 処理中にシグナルを受信しました({$signal_count}回目)\n";
// 複雑な処理は避け、フラグのみ設定
return;
}
echo "[✓] シグナル処理: {$signal_count}回目\n";
});
echo "PID: " . getmypid() . "\n";
echo "シグナル送信: kill -USR1 " . getmypid() . "\n\n";
$counter = 0;
while (true) {
$processing = true;
echo "処理中... {$counter}\n";
sleep(2);
$processing = false;
$counter++;
}
?>
パフォーマンスへの影響
<?php
/**
* 非同期シグナル処理のパフォーマンステスト
*/
function benchmarkSignalHandling($async_enabled, $iterations = 100000) {
pcntl_async_signals($async_enabled);
$signal_received = false;
pcntl_signal(SIGUSR1, function($signo) use (&$signal_received) {
$signal_received = true;
});
$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
if (!$async_enabled) {
pcntl_signal_dispatch(); // 従来の方法
}
// 何か処理
$dummy = $i * 2;
}
$elapsed = microtime(true) - $start;
return [
'mode' => $async_enabled ? '非同期' : '同期',
'iterations' => $iterations,
'time' => $elapsed,
'per_iteration' => $elapsed / $iterations * 1000000 // マイクロ秒
];
}
echo "=== シグナル処理のパフォーマンス比較 ===\n\n";
// 同期モードでベンチマーク
$sync_result = benchmarkSignalHandling(false, 100000);
echo "同期モード(pcntl_signal_dispatch使用):\n";
echo sprintf(" 合計時間: %.4f秒\n", $sync_result['time']);
echo sprintf(" 1回あたり: %.4fμs\n\n", $sync_result['per_iteration']);
// 非同期モードでベンチマーク
$async_result = benchmarkSignalHandling(true, 100000);
echo "非同期モード(pcntl_async_signals有効):\n";
echo sprintf(" 合計時間: %.4f秒\n", $async_result['time']);
echo sprintf(" 1回あたり: %.4fμs\n\n", $async_result['per_iteration']);
// 比較
$diff = (($sync_result['time'] - $async_result['time']) / $sync_result['time']) * 100;
echo "パフォーマンス差: " . number_format(abs($diff), 2) . "%\n";
echo ($diff > 0 ? "非同期モードの方が高速" : "同期モードの方が高速") . "\n";
?>
エラーハンドリング
<?php
class SafeSignalHandler {
private $handlers = [];
public function __construct() {
// 非同期シグナル処理を有効化
if (!pcntl_async_signals(true)) {
throw new RuntimeException("非同期シグナル処理を有効化できませんでした");
}
}
/**
* 安全なシグナルハンドラ登録
*/
public function register($signal, $callback, $error_handler = null) {
if (!is_callable($callback)) {
throw new InvalidArgumentException("コールバックが無効です");
}
$safe_callback = function($signo) use ($callback, $error_handler) {
try {
call_user_func($callback, $signo);
} catch (Exception $e) {
// エラーハンドラが指定されていれば実行
if (is_callable($error_handler)) {
call_user_func($error_handler, $e);
} else {
// デフォルトのエラー処理
error_log("シグナルハンドラでエラー: " . $e->getMessage());
}
}
};
if (!pcntl_signal($signal, $safe_callback)) {
throw new RuntimeException("シグナルハンドラの登録に失敗しました");
}
$this->handlers[$signal] = [
'callback' => $callback,
'error_handler' => $error_handler,
'registered_at' => time()
];
return true;
}
/**
* シグナルハンドラを解除
*/
public function unregister($signal) {
if (pcntl_signal($signal, SIG_DFL)) {
unset($this->handlers[$signal]);
return true;
}
return false;
}
/**
* 登録済みハンドラの一覧
*/
public function listHandlers() {
echo "=== 登録済みシグナルハンドラ ===\n";
foreach ($this->handlers as $signal => $info) {
$signal_name = $this->getSignalName($signal);
$uptime = time() - $info['registered_at'];
echo " {$signal_name} (登録後 {$uptime}秒)\n";
}
echo "==============================\n";
}
/**
* シグナル名を取得
*/
private function getSignalName($signal) {
$signals = [
SIGTERM => 'SIGTERM',
SIGINT => 'SIGINT',
SIGHUP => 'SIGHUP',
SIGUSR1 => 'SIGUSR1',
SIGUSR2 => 'SIGUSR2',
SIGCHLD => 'SIGCHLD',
SIGALRM => 'SIGALRM'
];
return $signals[$signal] ?? "SIGNAL_{$signal}";
}
}
// 使用例
try {
$handler = new SafeSignalHandler();
// 正常なハンドラ
$handler->register(SIGUSR1, function($signo) {
echo "SIGUSR1を受信しました\n";
});
// エラーが発生するハンドラ
$handler->register(SIGUSR2, function($signo) {
throw new RuntimeException("意図的なエラー");
}, function($exception) {
echo "エラーをキャッチしました: " . $exception->getMessage() . "\n";
});
$handler->listHandlers();
echo "\nPID: " . getmypid() . "\n";
echo "テスト用コマンド:\n";
echo " kill -USR1 " . getmypid() . " (正常)\n";
echo " kill -USR2 " . getmypid() . " (エラー発生)\n\n";
while (true) {
sleep(1);
}
} catch (Exception $e) {
echo "エラー: " . $e->getMessage() . "\n";
}
?>
実装時のチェックリスト
<?php
class SignalHandlerChecklist {
/**
* 環境チェック
*/
public static function checkEnvironment() {
$checks = [];
// PHP バージョン
$checks['php_version'] = [
'check' => version_compare(PHP_VERSION, '7.1.0', '>='),
'message' => 'PHP 7.1.0以降',
'current' => PHP_VERSION
];
// CLI環境
$checks['cli_mode'] = [
'check' => php_sapi_name() === 'cli',
'message' => 'CLI環境での実行',
'current' => php_sapi_name()
];
// PCNTL拡張
$checks['pcntl_extension'] = [
'check' => extension_loaded('pcntl'),
'message' => 'PCNTL拡張が有効',
'current' => extension_loaded('pcntl') ? '有効' : '無効'
];
// pcntl_async_signals関数
$checks['async_signals'] = [
'check' => function_exists('pcntl_async_signals'),
'message' => 'pcntl_async_signals関数が利用可能',
'current' => function_exists('pcntl_async_signals') ? '可能' : '不可'
];
// POSIX拡張(オプション)
$checks['posix_extension'] = [
'check' => extension_loaded('posix'),
'message' => 'POSIX拡張が有効(推奨)',
'current' => extension_loaded('posix') ? '有効' : '無効'
];
return $checks;
}
/**
* チェック結果を表示
*/
public static function displayResults() {
echo "=== 環境チェック結果 ===\n\n";
$checks = self::checkEnvironment();
$all_passed = true;
foreach ($checks as $name => $check) {
$status = $check['check'] ? '✓' : '✗';
$all_passed = $all_passed && $check['check'];
echo "[{$status}] {$check['message']}\n";
echo " 現在: {$check['current']}\n\n";
}
echo str_repeat("=", 40) . "\n";
if ($all_passed) {
echo "✓ すべてのチェックに合格しました\n";
return true;
} else {
echo "✗ 一部のチェックに失敗しました\n";
return false;
}
}
}
// チェック実行
if (SignalHandlerChecklist::displayResults()) {
echo "\npcntl_async_signalsを安全に使用できます。\n";
} else {
echo "\n環境を確認してください。\n";
}
?>
まとめ
pcntl_async_signals
関数は、PHP 7.1で導入された革新的な機能で、シグナル処理を大幅に簡素化します。
主な利点:
- コードの簡潔化:
pcntl_signal_dispatch()
の明示的な呼び出しが不要 - リアルタイム性の向上: シグナルが即座に処理される
- 可読性の向上: メインロジックとシグナル処理の分離が容易
主な用途:
- グレースフルシャットダウンの実装
- プロセス間通信の制御
- 動的な設定リロード
- パフォーマンス統計のリアルタイム表示
- ワーカープロセスの管理
重要な注意点:
- PHP 7.1.0以降が必須
- CLI環境専用(Web環境では使用不可)
- リエントラント安全性への配慮が必要
- シグナルハンドラ内では複雑な処理を避ける
- エラーハンドリングを適切に実装する
従来の方法との使い分け:
- 新規プロジェクト:
pcntl_async_signals(true)
を使用 - レガシーコード: 互換性のため従来の方法を維持
- PHP 7.1未満:
pcntl_signal_dispatch()
を使用
この関数により、PHPでのプロセス制御がより直感的で保守しやすくなりました。特に長時間稼働するCLIアプリケーションやデーモンプロセスの実装において、その真価を発揮します。
PHP 7.1以降の機能を活用して、モダンで堅牢なCLIアプリケーションを構築しましょう。非同期シグナル処理により、より洗練されたプロセス制御が可能になります。