こんにちは!今回はPHPの高度なシグナル制御機能であるpcntl_sigprocmask関数について詳しく解説します。クリティカルセクションでシグナルを一時的にブロックしたい、より精密なプロセス制御が必要な方は必見です!
pcntl_sigprocmask関数とは?
pcntl_sigprocmask()
は、プロセスのシグナルマスク(どのシグナルをブロックするか)を設定・取得する関数です。シグナルマスクを使うことで、特定のシグナルを一時的にブロック(遅延)したり、ブロックを解除したりできます。
基本構文
pcntl_sigprocmask(int $mode, array $signals, array &$old_signals = null): bool
- $mode: 操作モード(SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK)
- $signals: 対象となるシグナルの配列
- $old_signals: 以前のシグナルマスクを格納する配列(参照渡し)
- 戻り値: 成功時true、失敗時false
操作モードの種類
モード | 定数 | 説明 |
---|---|---|
ブロック | SIG_BLOCK | 指定したシグナルをマスクに追加(ブロック) |
解除 | SIG_UNBLOCK | 指定したシグナルをマスクから削除(許可) |
設定 | SIG_SETMASK | シグナルマスクを指定した内容で完全に置き換え |
なぜシグナルマスクが必要なのか?
使用シーン
- クリティカルセクションの保護
- データベース更新中にシグナルで中断されたくない
- ファイル書き込み中の割り込みを防ぎたい
- アトミックな操作
- 一連の処理を途中で中断させたくない
- シグナルのタイミング制御
- 特定の処理が完了するまでシグナルを遅延させる
- デッドロックの回避
- シグナルハンドラ内での競合状態を防ぐ
実践的なコード例
基本的な使い方:シグナルをブロック
<?php
$running = true;
pcntl_signal(SIGINT, function() use (&$running) {
echo "\nSIGINTを受信しました。終了します。\n";
$running = false;
});
echo "プログラム開始\n";
while ($running) {
echo "通常処理中... (Ctrl+Cで終了可能)\n";
sleep(1);
pcntl_signal_dispatch();
// クリティカルセクション開始
echo "\n=== クリティカルセクション開始 ===\n";
// SIGINTをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGINT], $old_mask);
echo "SIGINT をブロックしました (Ctrl+C は無効)\n";
// 重要な処理(中断されたくない)
for ($i = 1; $i <= 5; $i++) {
echo "重要な処理 {$i}/5...\n";
sleep(1);
// この間Ctrl+Cを押してもすぐには反応しない
}
// SIGINTのブロックを解除
pcntl_sigprocmask(SIG_UNBLOCK, [SIGINT]);
echo "SIGINT のブロックを解除しました\n";
echo "=== クリティカルセクション終了 ===\n\n";
// ブロック中に受信したシグナルがここで処理される
pcntl_signal_dispatch();
}
echo "プログラム終了\n";
?>
データベース更新の保護
<?php
class DatabaseTransaction {
private $running = true;
private $blocked_signals = [];
public function __construct() {
pcntl_signal(SIGTERM, [$this, 'handleShutdown']);
pcntl_signal(SIGINT, [$this, 'handleShutdown']);
}
public function handleShutdown($signal) {
echo "\n終了シグナルを受信。トランザクション完了後に終了します。\n";
$this->running = false;
}
public function blockSignals() {
$signals = [SIGTERM, SIGINT, SIGHUP];
if (pcntl_sigprocmask(SIG_BLOCK, $signals, $this->blocked_signals)) {
echo "[シグナル] ブロックしました: " . implode(', ', $signals) . "\n";
return true;
}
return false;
}
public function unblockSignals() {
if (pcntl_sigprocmask(SIG_SETMASK, $this->blocked_signals)) {
echo "[シグナル] ブロックを解除しました\n";
return true;
}
return false;
}
public function executeTransaction() {
echo "トランザクション開始\n";
// シグナルをブロック
$this->blockSignals();
try {
// BEGIN
echo " - データベース接続\n";
sleep(1);
echo " - データ更新中 (1/3)\n";
sleep(1);
echo " - データ更新中 (2/3)\n";
sleep(1);
echo " - データ更新中 (3/3)\n";
sleep(1);
// COMMIT
echo " - コミット実行\n";
sleep(1);
echo "トランザクション完了\n";
} finally {
// 必ずシグナルのブロックを解除
$this->unblockSignals();
pcntl_signal_dispatch();
}
}
public function run() {
echo "データベースワーカー起動: PID=" . getmypid() . "\n";
while ($this->running) {
$this->executeTransaction();
echo "\n待機中...\n";
sleep(2);
pcntl_signal_dispatch();
}
echo "データベースワーカー終了\n";
}
}
$db = new DatabaseTransaction();
$db->run();
?>
現在のシグナルマスクを取得
<?php
// 現在のシグナルマスクを取得
$current_mask = [];
pcntl_sigprocmask(SIG_SETMASK, [], $current_mask);
echo "現在ブロックされているシグナル:\n";
if (empty($current_mask)) {
echo " なし\n";
} else {
foreach ($current_mask as $signal) {
echo " - シグナル {$signal}\n";
}
}
// SIGINTとSIGTERMをブロック
echo "\nSIGINTとSIGTERMをブロックします\n";
pcntl_sigprocmask(SIG_BLOCK, [SIGINT, SIGTERM]);
// 再度確認
pcntl_sigprocmask(SIG_SETMASK, [], $current_mask);
echo "現在ブロックされているシグナル:\n";
foreach ($current_mask as $signal) {
echo " - シグナル {$signal}\n";
}
// 全てのブロックを解除
echo "\n全てのブロックを解除します\n";
pcntl_sigprocmask(SIG_SETMASK, []);
?>
SIG_BLOCKとSIG_UNBLOCKの違い
<?php
// 初期状態:何もブロックされていない
echo "=== SIG_BLOCK の例 ===\n";
// SIGINTをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGINT]);
echo "SIGINTをブロックしました\n";
// さらにSIGTERMをブロック(追加)
pcntl_sigprocmask(SIG_BLOCK, [SIGTERM]);
echo "SIGTERMもブロックしました\n";
// 現在のマスク:SIGINT + SIGTERM
pcntl_sigprocmask(SIG_SETMASK, [], $mask);
echo "ブロック中: " . count($mask) . "個のシグナル\n";
echo "\n=== SIG_UNBLOCK の例 ===\n";
// SIGINTのブロックを解除
pcntl_sigprocmask(SIG_UNBLOCK, [SIGINT]);
echo "SIGINTのブロックを解除しました\n";
// 現在のマスク:SIGTERMのみ
pcntl_sigprocmask(SIG_SETMASK, [], $mask);
echo "ブロック中: " . count($mask) . "個のシグナル\n";
echo "\n=== SIG_SETMASK の例 ===\n";
// マスクを完全に置き換え
pcntl_sigprocmask(SIG_SETMASK, [SIGHUP, SIGUSR1]);
echo "マスクをSIGHUPとSIGUSR1に設定しました\n";
pcntl_sigprocmask(SIG_SETMASK, [], $mask);
echo "ブロック中: " . count($mask) . "個のシグナル\n";
// 全てクリア
pcntl_sigprocmask(SIG_SETMASK, []);
echo "全てのブロックを解除しました\n";
?>
ファイル書き込みの保護
<?php
class SafeFileWriter {
private $blocked_signals;
public function writeFile($filename, $data) {
echo "ファイル書き込み開始: {$filename}\n";
// シグナルをブロック
$signals_to_block = [SIGTERM, SIGINT, SIGHUP];
pcntl_sigprocmask(SIG_BLOCK, $signals_to_block, $this->blocked_signals);
try {
// 一時ファイルに書き込み
$temp_file = $filename . '.tmp';
echo " - 一時ファイルに書き込み中...\n";
file_put_contents($temp_file, $data);
sleep(1); // 書き込み処理をシミュレート
// アトミックにリネーム
echo " - ファイルを移動中...\n";
rename($temp_file, $filename);
sleep(1);
echo "ファイル書き込み完了\n";
return true;
} catch (Exception $e) {
echo "エラー: " . $e->getMessage() . "\n";
// 一時ファイルをクリーンアップ
if (file_exists($temp_file)) {
unlink($temp_file);
}
return false;
} finally {
// 必ずシグナルのブロックを解除
pcntl_sigprocmask(SIG_SETMASK, $this->blocked_signals);
pcntl_signal_dispatch();
echo "シグナルブロック解除\n";
}
}
}
// シグナルハンドラ設定
$running = true;
pcntl_signal(SIGINT, function() use (&$running) {
echo "\n終了シグナルを受信しました\n";
$running = false;
});
$writer = new SafeFileWriter();
while ($running) {
$writer->writeFile('important_data.txt', date('Y-m-d H:i:s') . "\n");
echo "待機中... (Ctrl+Cで終了)\n";
sleep(2);
pcntl_signal_dispatch();
}
echo "プログラム終了\n";
?>
複数のクリティカルセクション
<?php
class CriticalSection {
private static $depth = 0;
private static $original_mask = [];
public static function enter() {
if (self::$depth === 0) {
// 初回のみシグナルをブロック
$signals = [SIGTERM, SIGINT, SIGHUP];
pcntl_sigprocmask(SIG_BLOCK, $signals, self::$original_mask);
echo str_repeat(' ', self::$depth) . "[ENTER] シグナルブロック開始\n";
} else {
echo str_repeat(' ', self::$depth) . "[ENTER] ネストされたセクション\n";
}
self::$depth++;
}
public static function leave() {
self::$depth--;
if (self::$depth === 0) {
// 最後のleaveでシグナルを解除
pcntl_sigprocmask(SIG_SETMASK, self::$original_mask);
echo str_repeat(' ', self::$depth) . "[LEAVE] シグナルブロック解除\n";
pcntl_signal_dispatch();
} else {
echo str_repeat(' ', self::$depth) . "[LEAVE] ネストから復帰\n";
}
}
}
function outer_function() {
CriticalSection::enter();
echo "外側の関数:処理1\n";
sleep(1);
inner_function();
echo "外側の関数:処理2\n";
sleep(1);
CriticalSection::leave();
}
function inner_function() {
CriticalSection::enter();
echo " 内側の関数:処理\n";
sleep(1);
CriticalSection::leave();
}
// テスト実行
pcntl_signal(SIGINT, function() {
echo "\nSIGINT受信!\n";
exit(0);
});
echo "プログラム開始 (Ctrl+Cを試してください)\n\n";
outer_function();
echo "\nプログラム完了\n";
?>
デバッグ用:シグナルマスクの可視化
<?php
function print_signal_mask($label = "") {
$current_mask = [];
pcntl_sigprocmask(SIG_SETMASK, [], $current_mask);
$signal_names = [
SIGHUP => 'SIGHUP',
SIGINT => 'SIGINT',
SIGQUIT => 'SIGQUIT',
SIGTERM => 'SIGTERM',
SIGUSR1 => 'SIGUSR1',
SIGUSR2 => 'SIGUSR2',
];
echo "--- シグナルマスク" . ($label ? " ({$label})" : "") . " ---\n";
if (empty($current_mask)) {
echo " ブロックなし\n";
} else {
foreach ($current_mask as $signal) {
$name = $signal_names[$signal] ?? "SIGNAL_{$signal}";
echo " 🚫 {$name} (ブロック中)\n";
}
}
echo "\n";
}
// 初期状態
print_signal_mask("初期状態");
// SIGINTをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGINT]);
print_signal_mask("SIGINT追加後");
// SIGTERMも追加
pcntl_sigprocmask(SIG_BLOCK, [SIGTERM]);
print_signal_mask("SIGTERM追加後");
// SIGINTだけ解除
pcntl_sigprocmask(SIG_UNBLOCK, [SIGINT]);
print_signal_mask("SIGINT解除後");
// 全て解除
pcntl_sigprocmask(SIG_SETMASK, []);
print_signal_mask("全て解除後");
?>
重要なポイントと注意事項
⚠️ 必ずブロックを解除する
// ❌ 悪い例:例外でブロック解除が実行されない
pcntl_sigprocmask(SIG_BLOCK, [SIGINT]);
risky_operation(); // 例外が発生する可能性
pcntl_sigprocmask(SIG_UNBLOCK, [SIGINT]); // 実行されないかも
// ✅ 良い例:finallyで確実に解除
pcntl_sigprocmask(SIG_BLOCK, [SIGINT], $old_mask);
try {
risky_operation();
} finally {
pcntl_sigprocmask(SIG_SETMASK, $old_mask);
}
💡 ブロックできないシグナル
**SIGKILL(9)とSIGSTOP(19)**はブロックできません:
// これらは無視される
pcntl_sigprocmask(SIG_BLOCK, [SIGKILL, SIGSTOP]); // 効果なし
🔍 ブロックとハンドラの関係
シグナルをブロックしても、ハンドラの設定は有効です:
// ハンドラを設定
pcntl_signal(SIGINT, function() {
echo "ハンドラ実行\n";
});
// SIGINTをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGINT]);
// Ctrl+Cを押す → 保留される
sleep(5);
// ブロック解除 → ハンドラが実行される
pcntl_sigprocmask(SIG_UNBLOCK, [SIGINT]);
pcntl_signal_dispatch(); // ここでハンドラが呼ばれる
ベストプラクティス
1. 必要最小限のブロック
// ✅ 良い例:必要な部分だけブロック
function critical_operation() {
pcntl_sigprocmask(SIG_BLOCK, [SIGINT, SIGTERM], $old);
try {
// クリティカルな処理
} finally {
pcntl_sigprocmask(SIG_SETMASK, $old);
}
}
// ❌ 悪い例:長時間ブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGINT, SIGTERM]);
long_running_process(); // ユーザーが終了できない!
2. 旧マスクの保存
// ✅ 常に旧マスクを保存して復元
pcntl_sigprocmask(SIG_BLOCK, [SIGINT], $old_mask);
// 処理
pcntl_sigprocmask(SIG_SETMASK, $old_mask); // 元に戻す
3. デバッグログの追加
function block_signals($signals) {
if (pcntl_sigprocmask(SIG_BLOCK, $signals, $old)) {
error_log("Blocked signals: " . implode(',', $signals));
return $old;
}
error_log("Failed to block signals");
return null;
}
まとめ
pcntl_sigprocmask()
は、PHPで高度なシグナル制御を実現するための重要な関数です。主なポイント:
- ✅ クリティカルセクションでシグナルを一時的にブロック
- ✅ データベーストランザクションやファイル操作を保護
- ✅ SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASKの使い分け
- ✅ 必ずfinallyブロックで解除する
- ✅ 旧マスクを保存して復元する
堅牢なプロセス制御が必要なシステムでは、この関数を活用して、中断されては困る処理を確実に保護しましょう!
関連記事
- pcntl_signal()でシグナルハンドラを設定
- pcntl_signal_dispatch()でシグナルを処理
- PHPでアトミックな操作を実装する方法