[PHP]pcntl_sigprocmask関数とは?シグナルマスクの設定方法を徹底解説

PHP

こんにちは!今回は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シグナルマスクを指定した内容で完全に置き換え

なぜシグナルマスクが必要なのか?

使用シーン

  1. クリティカルセクションの保護
    • データベース更新中にシグナルで中断されたくない
    • ファイル書き込み中の割り込みを防ぎたい
  2. アトミックな操作
    • 一連の処理を途中で中断させたくない
  3. シグナルのタイミング制御
    • 特定の処理が完了するまでシグナルを遅延させる
  4. デッドロックの回避
    • シグナルハンドラ内での競合状態を防ぐ

実践的なコード例

基本的な使い方:シグナルをブロック

<?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でアトミックな操作を実装する方法
タイトルとURLをコピーしました