こんにちは!今回はPHPのPOSIX拡張モジュールに含まれる「posix_setsid」関数について、詳しく解説していきます。この関数は、デーモンプロセスの作成や、プロセスを端末から独立させる際に必須となる重要な機能です。
posix_setsidとは何か?
posix_setsidは、現在のプロセスを新しいセッションのリーダーにする関数です。これにより、プロセスが端末から切り離され、バックグラウンドで独立して動作できるようになります。
基本的な構文
posix_setsid(): int
パラメータ:
- なし
戻り値:
- 成功時: 新しいセッションID(正の整数)
- 失敗時:
-1
セッションとは何か?
Unixシステムにおけるセッションは、1つ以上のプロセスグループの集合です。理解を深めるために、プロセスの階層構造を見てみましょう:
セッション
└── プロセスグループ1 (フォアグラウンド)
├── プロセスA
└── プロセスB
└── プロセスグループ2 (バックグラウンド)
└── プロセスC
セッションの重要な特徴
- セッションリーダー: セッション内の最初のプロセス
- 制御端末: セッションに関連付けられた端末(あれば)
- シグナル伝播: Ctrl+Cなどのシグナルがセッション内に伝わる
posix_setsidが行うこと
posix_setsidを実行すると、以下の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(): プロセスグループを設定
