[PHP]posix_setsid関数とは?セッション制御を徹底解説

PHP

こんにちは!今回はPHPのPOSIX拡張モジュールに含まれる「posix_setsid」関数について、詳しく解説していきます。この関数は、デーモンプロセスの作成や、プロセスを端末から独立させる際に必須となる重要な機能です。

posix_setsidとは何か?

posix_setsidは、現在のプロセスを新しいセッションのリーダーにする関数です。これにより、プロセスが端末から切り離され、バックグラウンドで独立して動作できるようになります。

基本的な構文

posix_setsid(): int

パラメータ:

  • なし

戻り値:

  • 成功時: 新しいセッションID(正の整数)
  • 失敗時: -1

セッションとは何か?

Unixシステムにおけるセッションは、1つ以上のプロセスグループの集合です。理解を深めるために、プロセスの階層構造を見てみましょう:

セッション
  └── プロセスグループ1 (フォアグラウンド)
      ├── プロセスA
      └── プロセスB
  └── プロセスグループ2 (バックグラウンド)
      └── プロセスC

セッションの重要な特徴

  1. セッションリーダー: セッション内の最初のプロセス
  2. 制御端末: セッションに関連付けられた端末(あれば)
  3. シグナル伝播: Ctrl+Cなどのシグナルがセッション内に伝わる

posix_setsidが行うこと

posix_setsidを実行すると、以下の3つのことが同時に起こります:

  1. 新しいセッションを作成: 現在のプロセスがセッションリーダーになる
  2. 新しいプロセスグループを作成: プロセスが新しいグループのリーダーになる
  3. 制御端末から切り離し: プロセスが端末の制御を受けなくなる
<?php
echo "現在のセッションID: " . posix_getsid(0) . "\n";
echo "現在のプロセスグループID: " . posix_getpgid(0) . "\n";

// 新しいセッションを作成
$new_sid = posix_setsid();

if ($new_sid == -1) {
    die("posix_setsidに失敗しました\n");
}

echo "新しいセッションID: " . posix_getsid(0) . "\n";
echo "新しいプロセスグループID: " . posix_getpgid(0) . "\n";
echo "プロセスID: " . posix_getpid() . "\n";

// 注目: セッションID = プロセスグループID = プロセスID
?>

デーモンプロセスの作成

posix_setsidの最も一般的な使用例は、デーモンプロセスの作成です。デーモンとは、バックグラウンドで動作し続けるプロセスのことです。

標準的なデーモン化の手順

<?php
function daemonize() {
    // ステップ1: 最初のfork
    $pid = pcntl_fork();
    
    if ($pid == -1) {
        die("フォークに失敗しました\n");
    } elseif ($pid) {
        // 親プロセスは終了
        exit(0);
    }
    
    // ステップ2: 新しいセッションリーダーになる
    $sid = posix_setsid();
    if ($sid == -1) {
        die("posix_setsidに失敗しました\n");
    }
    
    // ステップ3: 2回目のfork(オプションだが推奨)
    $pid = pcntl_fork();
    
    if ($pid == -1) {
        die("2回目のフォークに失敗しました\n");
    } elseif ($pid) {
        // 親プロセスは終了
        exit(0);
    }
    
    // ステップ4: 作業ディレクトリを変更
    chdir('/');
    
    // ステップ5: ファイルマスクをクリア
    umask(0);
    
    // ステップ6: 標準入出力を閉じる
    fclose(STDIN);
    fclose(STDOUT);
    fclose(STDERR);
    
    // ステップ7: 標準入出力を/dev/nullにリダイレクト
    $stdin = fopen('/dev/null', 'r');
    $stdout = fopen('/dev/null', 'w');
    $stderr = fopen('/dev/null', 'w');
    
    return true;
}

// デーモン化を実行
daemonize();

// ログファイルを開く
$log = fopen('/var/log/mydaemon.log', 'a');

// デーモンとしての処理
while (true) {
    $timestamp = date('Y-m-d H:i:s');
    fwrite($log, "[{$timestamp}] デーモンが実行中です\n");
    fflush($log);
    sleep(60); // 60秒ごとに実行
}
?>

なぜ2回forkするのか?

<?php
/*
 * 1回目のfork: 親プロセス(シェル)から分離
 * posix_setsid: 新しいセッションリーダーになる
 * 2回目のfork: セッションリーダーではなくなる
 * 
 * 2回目のforkの目的:
 * - セッションリーダーは制御端末を再取得できる
 * - 2回目のforkでセッションリーダーでなくなることで、
 *   制御端末を持つことが完全に不可能になる
 */

function createDaemon() {
    // 1回目のfork
    $pid = pcntl_fork();
    if ($pid == -1) return false;
    if ($pid) exit(0);
    
    // 新しいセッション
    if (posix_setsid() == -1) return false;
    
    // 2回目のfork(セッションリーダーでなくなる)
    $pid = pcntl_fork();
    if ($pid == -1) return false;
    if ($pid) exit(0);
    
    echo "デーモンプロセスになりました\n";
    echo "PID: " . posix_getpid() . "\n";
    echo "SID: " . posix_getsid(0) . "\n";
    echo "PGID: " . posix_getpgid(0) . "\n";
    echo "セッションリーダー: いいえ\n";
    
    return true;
}

createDaemon();
?>

実践的な使用例

例1: シンプルなバックグラウンドワーカー

<?php
class BackgroundWorker {
    private $logFile;
    private $pidFile;
    
    public function __construct($logFile, $pidFile) {
        $this->logFile = $logFile;
        $this->pidFile = $pidFile;
    }
    
    public function start() {
        // デーモン化
        if (!$this->daemonize()) {
            die("デーモン化に失敗しました\n");
        }
        
        // PIDファイルを作成
        file_put_contents($this->pidFile, posix_getpid());
        
        // ログ開始
        $this->log("ワーカーを開始しました");
        
        // シグナルハンドラを設定
        pcntl_signal(SIGTERM, [$this, 'handleShutdown']);
        pcntl_signal(SIGINT, [$this, 'handleShutdown']);
        
        // メインループ
        $this->run();
    }
    
    private function daemonize() {
        $pid = pcntl_fork();
        if ($pid == -1) return false;
        if ($pid) exit(0);
        
        if (posix_setsid() == -1) return false;
        
        $pid = pcntl_fork();
        if ($pid == -1) return false;
        if ($pid) exit(0);
        
        chdir('/');
        umask(0);
        
        // 標準入出力を閉じる
        fclose(STDIN);
        fclose(STDOUT);
        fclose(STDERR);
        
        return true;
    }
    
    private function run() {
        while (true) {
            // シグナルをチェック
            pcntl_signal_dispatch();
            
            // 実際の処理
            $this->doWork();
            
            sleep(5);
        }
    }
    
    private function doWork() {
        // ここに実際の処理を書く
        $this->log("処理を実行中...");
        
        // 例: キューから取得して処理
        // $job = $this->getNextJob();
        // $this->processJob($job);
    }
    
    public function handleShutdown($signo) {
        $this->log("シャットダウンシグナルを受信しました: {$signo}");
        
        // クリーンアップ
        if (file_exists($this->pidFile)) {
            unlink($this->pidFile);
        }
        
        $this->log("ワーカーを終了します");
        exit(0);
    }
    
    private function log($message) {
        $timestamp = date('Y-m-d H:i:s');
        $pid = posix_getpid();
        $line = "[{$timestamp}] [PID:{$pid}] {$message}\n";
        file_put_contents($this->logFile, $line, FILE_APPEND);
    }
}

// 使用例
$worker = new BackgroundWorker(
    '/var/log/worker.log',
    '/var/run/worker.pid'
);
$worker->start();
?>

例2: キューワーカーシステム

<?php
class QueueWorker {
    private $running = true;
    private $jobQueue;
    
    public function __construct($queueFile) {
        $this->queueFile = $queueFile;
    }
    
    public function start() {
        echo "ワーカーを起動中...\n";
        
        // デーモン化
        $this->becomeDaemon();
        
        // シグナルハンドラ
        pcntl_signal(SIGTERM, [$this, 'shutdown']);
        pcntl_signal(SIGINT, [$this, 'shutdown']);
        pcntl_signal(SIGHUP, [$this, 'reload']);
        
        $this->writeLog("ワーカーが起動しました");
        
        // メインループ
        while ($this->running) {
            pcntl_signal_dispatch();
            $this->processQueue();
            sleep(1);
        }
    }
    
    private function becomeDaemon() {
        // 1回目のfork
        $pid = pcntl_fork();
        if ($pid == -1) {
            die("フォークに失敗しました\n");
        }
        if ($pid > 0) {
            // 親プロセスは終了
            echo "デーモンを起動しました (PID: {$pid})\n";
            exit(0);
        }
        
        // 新しいセッションリーダーになる
        $sid = posix_setsid();
        if ($sid == -1) {
            die("posix_setsidに失敗しました\n");
        }
        
        // 2回目のfork
        $pid = pcntl_fork();
        if ($pid == -1) {
            die("2回目のフォークに失敗しました\n");
        }
        if ($pid > 0) {
            exit(0);
        }
        
        // 環境を設定
        chdir('/');
        umask(0);
        
        // 標準入出力を閉じる
        $this->closeStandardStreams();
    }
    
    private function closeStandardStreams() {
        fclose(STDIN);
        fclose(STDOUT);
        fclose(STDERR);
        
        $stdin = fopen('/dev/null', 'r');
        $stdout = fopen('/dev/null', 'w');
        $stderr = fopen('/dev/null', 'w');
    }
    
    private function processQueue() {
        if (!file_exists($this->queueFile)) {
            return;
        }
        
        $jobs = json_decode(file_get_contents($this->queueFile), true);
        
        if (empty($jobs)) {
            return;
        }
        
        foreach ($jobs as $key => $job) {
            $this->writeLog("ジョブを処理中: " . json_encode($job));
            
            // ジョブを処理
            $this->executeJob($job);
            
            // 処理済みジョブを削除
            unset($jobs[$key]);
            file_put_contents($this->queueFile, json_encode(array_values($jobs)));
        }
    }
    
    private function executeJob($job) {
        // 実際のジョブ処理
        sleep(2); // 処理をシミュレート
        $this->writeLog("ジョブ完了: {$job['id']}");
    }
    
    public function shutdown($signo) {
        $this->writeLog("シャットダウンシグナルを受信: {$signo}");
        $this->running = false;
    }
    
    public function reload($signo) {
        $this->writeLog("リロードシグナルを受信: {$signo}");
        // 設定を再読み込み
    }
    
    private function writeLog($message) {
        $timestamp = date('Y-m-d H:i:s');
        $log = "[{$timestamp}] {$message}\n";
        file_put_contents('/tmp/queue_worker.log', $log, FILE_APPEND);
    }
}

// 使用例
$worker = new QueueWorker('/tmp/job_queue.json');
$worker->start();
?>

例3: 監視デーモン

<?php
class MonitorDaemon {
    private $config;
    private $running = true;
    
    public function __construct($configFile) {
        $this->config = json_decode(file_get_contents($configFile), true);
    }
    
    public function start() {
        // デーモン化の前に情報を出力
        $this->printStartupInfo();
        
        // デーモン化
        if (!$this->daemonize()) {
            die("デーモン化に失敗しました\n");
        }
        
        // PIDファイルを作成
        $this->writePidFile();
        
        // シグナルハンドラ
        $this->setupSignalHandlers();
        
        // ログ
        $this->log("監視デーモンを開始しました");
        
        // 監視ループ
        $this->monitorLoop();
    }
    
    private function printStartupInfo() {
        echo "=== 監視デーモン起動 ===\n";
        echo "現在のPID: " . posix_getpid() . "\n";
        echo "現在のSID: " . posix_getsid(0) . "\n";
        echo "デーモン化を開始します...\n";
    }
    
    private function daemonize() {
        // 既にセッションリーダーの場合は失敗する
        if (posix_getsid(0) == posix_getpid()) {
            $this->log("警告: 既にセッションリーダーです");
        }
        
        // 1回目のfork
        $pid = pcntl_fork();
        if ($pid == -1) {
            return false;
        }
        if ($pid > 0) {
            // 親プロセスに新しいPIDを通知
            echo "デーモンPID: {$pid}\n";
            exit(0);
        }
        
        // 新しいセッションを作成
        $sid = posix_setsid();
        if ($sid == -1) {
            error_log("posix_setsidに失敗しました");
            return false;
        }
        
        // 2回目のfork
        $pid = pcntl_fork();
        if ($pid == -1) {
            return false;
        }
        if ($pid > 0) {
            exit(0);
        }
        
        // 環境設定
        chdir('/');
        umask(0);
        
        // 標準ストリームをリダイレクト
        $this->redirectStreams();
        
        return true;
    }
    
    private function redirectStreams() {
        global $STDIN, $STDOUT, $STDERR;
        
        fclose(STDIN);
        fclose(STDOUT);
        fclose(STDERR);
        
        $STDIN = fopen('/dev/null', 'r');
        $STDOUT = fopen($this->config['log_file'], 'a');
        $STDERR = fopen($this->config['error_log'], 'a');
    }
    
    private function setupSignalHandlers() {
        pcntl_signal(SIGTERM, [$this, 'handleTerminate']);
        pcntl_signal(SIGINT, [$this, 'handleTerminate']);
        pcntl_signal(SIGHUP, [$this, 'handleReload']);
    }
    
    private function monitorLoop() {
        $checkInterval = $this->config['check_interval'] ?? 60;
        
        while ($this->running) {
            pcntl_signal_dispatch();
            
            // 監視対象をチェック
            foreach ($this->config['monitors'] as $target) {
                $this->checkTarget($target);
            }
            
            sleep($checkInterval);
        }
        
        $this->cleanup();
    }
    
    private function checkTarget($target) {
        $status = $this->getTargetStatus($target);
        
        if (!$status['healthy']) {
            $this->log("警告: {$target['name']} が異常です");
            $this->sendAlert($target, $status);
        } else {
            $this->log("正常: {$target['name']}");
        }
    }
    
    private function getTargetStatus($target) {
        // 実際の監視ロジック
        // 例: ファイルの存在確認、プロセスの確認、HTTP応答など
        
        if ($target['type'] == 'file') {
            return [
                'healthy' => file_exists($target['path'])
            ];
        }
        
        return ['healthy' => true];
    }
    
    private function sendAlert($target, $status) {
        // アラート送信ロジック
        $this->log("アラート送信: {$target['name']}");
    }
    
    public function handleTerminate($signo) {
        $this->log("終了シグナルを受信: {$signo}");
        $this->running = false;
    }
    
    public function handleReload($signo) {
        $this->log("リロードシグナルを受信");
        // 設定を再読み込み
    }
    
    private function writePidFile() {
        $pidFile = $this->config['pid_file'] ?? '/var/run/monitor.pid';
        file_put_contents($pidFile, posix_getpid());
    }
    
    private function cleanup() {
        $this->log("クリーンアップを実行中");
        
        $pidFile = $this->config['pid_file'] ?? '/var/run/monitor.pid';
        if (file_exists($pidFile)) {
            unlink($pidFile);
        }
        
        $this->log("監視デーモンを終了しました");
    }
    
    private function log($message) {
        $timestamp = date('Y-m-d H:i:s');
        $pid = posix_getpid();
        $log = "[{$timestamp}] [PID:{$pid}] {$message}\n";
        
        $logFile = $this->config['log_file'] ?? '/tmp/monitor.log';
        file_put_contents($logFile, $log, FILE_APPEND);
    }
}

// 設定ファイルの例 (config.json)
/*
{
    "log_file": "/var/log/monitor.log",
    "error_log": "/var/log/monitor_error.log",
    "pid_file": "/var/run/monitor.pid",
    "check_interval": 60,
    "monitors": [
        {
            "name": "重要なファイル",
            "type": "file",
            "path": "/path/to/important/file"
        }
    ]
}
*/

// 使用例
$daemon = new MonitorDaemon('config.json');
$daemon->start();
?>

よくあるエラーと対処法

エラー1: EPERMエラー

<?php
// プロセスグループリーダーの場合、posix_setsidは失敗する
$pid = posix_getpid();
$pgid = posix_getpgid(0);

if ($pid == $pgid) {
    echo "エラー: プロセスグループリーダーです\n";
    echo "解決策: 先にforkしてください\n";
    
    // 正しい方法
    $child_pid = pcntl_fork();
    if ($child_pid == 0) {
        // 子プロセスでposix_setsidを実行
        $sid = posix_setsid();
        echo "成功: 新しいセッションID = {$sid}\n";
    }
}
?>

エラー2: 標準出力が使えない

<?php
function safeDaemonize() {
    // デーモン化の前にメッセージを出力
    echo "デーモン化を開始します\n";
    flush();
    
    $pid = pcntl_fork();
    if ($pid > 0) {
        echo "子プロセスPID: {$pid}\n";
        exit(0);
    }
    
    posix_setsid();
    
    // この時点ではまだ標準出力が使える
    echo "posix_setsid成功\n";
    
    // ログファイルを開く
    $log = fopen('/var/log/daemon.log', 'a');
    
    // 標準出力を閉じる前に切り替え
    fclose(STDOUT);
    $GLOBALS['STDOUT'] = $log;
    
    // 以降はログファイルに出力される
    fwrite($log, "デーモンが起動しました\n");
}
?>

制限事項と注意点

1. プロセスグループリーダーでは使えない

<?php
// NG例: プロセスグループリーダーでposix_setsidを呼ぶ
// これは失敗する

// OK例: forkしてから子プロセスで呼ぶ
$pid = pcntl_fork();
if ($pid == 0) {
    posix_setsid(); // 成功
}
?>

2. 制御端末の扱い

<?php
// posix_setsid後は制御端末がなくなる
posix_setsid();

// 端末関連の操作は失敗する
// 例: tcgetpgrp(), tcsetpgrp() など
?>

3. デーモン化のチェックリスト

<?php
function properDaemonize() {
    // ✓ forkする
    $pid = pcntl_fork();
    if ($pid > 0) exit(0);
    
    // ✓ posix_setsidを呼ぶ
    posix_setsid();
    
    // ✓ もう一度fork(推奨)
    $pid = pcntl_fork();
    if ($pid > 0) exit(0);
    
    // ✓ 作業ディレクトリを変更
    chdir('/');
    
    // ✓ ファイルマスクをクリア
    umask(0);
    
    // ✓ 標準入出力を閉じる
    fclose(STDIN);
    fclose(STDOUT);
    fclose(STDERR);
    
    // ✓ /dev/nullにリダイレクト
    fopen('/dev/null', 'r');
    fopen('/dev/null', 'w');
    fopen('/dev/null', 'w');
    
    return true;
}
?>

まとめ

posix_setsidは、PHPでデーモンプロセスを作成する際の必須関数です。

重要なポイント:

  • プロセスを新しいセッションのリーダーにする
  • 制御端末から切り離す
  • デーモン化には必ず先にforkが必要
  • 2回forkすることで完全な独立を実現

主な用途:

  • バックグラウンドワーカーの作成
  • 長時間実行されるサービスの実装
  • キュー処理システム
  • 監視・監査デーモン

デーモンプロセスは、サーバー管理やバッチ処理において非常に重要な技術です。posix_setsidを正しく理解して使うことで、安定したバックグラウンドサービスを構築できます!


関連記事:

  • pcntl_fork(): 子プロセスを作成
  • posix_getsid(): セッションIDを取得
  • posix_getpgid(): プロセスグループIDを取得
  • posix_setpgid(): プロセスグループを設定
タイトルとURLをコピーしました