CLI 시그널
Unix 시그널은 프로세스 통신과 제어의 기본 요소입니다. 실행 중인 프로세스를 중단, 제어, 통신하는 방법을 제공합니다. CodeIgniter의 SignalTrait는 CLI 명령에서 시그널을 쉽게 처리할 수 있게 해 주며, 우아한 종료, 일시정지/재개 기능, 사용자 정의 시그널 처리를 지원합니다.
시그널이란?
시그널은 운영체제가 프로세스에 전달하는 소프트웨어 인터럽트입니다. 사용자 입력 중단(Ctrl+C)부터 터미널 연결 끊김 같은 시스템 이벤트까지 다양한 이벤트를 프로세스에 알립니다.
SignalTrait는 시그널이 소비되기 전에 특정 동작을 수행할 수 있게 하고, 코드의 특정 부분을 시그널 중단으로부터 보호할 수 있게 합니다. 이 보호 메커니즘은 들어오는 시그널에 의해 중단되지 않고 중요한 명령 작업이 원자적으로 완료되도록 보장합니다.
일반적인 Unix 시그널
CLI 애플리케이션에서 가장 흔히 사용되는 시그널은 다음과 같습니다:
처리 가능한 시그널:
SIGTERM (15): 종료 시그널 - 정상 종료를 요청합니다
SIGINT (2): 인터럽트 시그널 - 보통 Ctrl+C로 보냅니다
SIGHUP (1): 행업 시그널 - 터미널이 연결 해제되거나 닫혔음을 의미합니다
SIGQUIT (3): 종료 시그널 - 보통 Ctrl+\로 보냅니다
SIGTSTP (20): 터미널 중지 - 보통 Ctrl+Z로 보냅니다(일시정지)
SIGCONT (18): 계속 시그널 - 일시정지된 프로세스를 재개합니다(fg 명령)
SIGUSR1 (10): 사용자 정의 시그널 1
SIGUSR2 (12): 사용자 정의 시그널 2
처리할 수 없는 시그널:
일부 시그널은 사용자 프로세스가 잡거나 차단하거나 처리할 수 없습니다:
SIGKILL (9): 강제 종료 - 잡거나 무시할 수 없습니다
SIGSTOP (19): 강제 일시정지 - 잡거나 무시할 수 없습니다
이 시그널들은 커널이 직접 처리하며, 사용자 정의 핸들러를 거치지 않고 프로세스를 즉시 종료하거나 일시정지시킵니다.
시스템 요구 사항
시그널 처리를 위해서는 다음이 필요합니다:
Unix 기반 시스템 (Linux, macOS, BSD) - Windows는 지원되지 않습니다
PCNTL 확장 - 시그널 등록 및 처리용
POSIX 확장 - 일시정지/재개 기능(SIGTSTP/SIGCONT)에 필요합니다
참고
이 확장들이 없는 시스템에서는 SignalTrait가 우아하게 기능을 축소하고 시그널 처리를 비활성화합니다.
SignalTrait 사용하기
SignalTrait는 CLI 명령을 위한 종합적인 시그널 처리 시스템을 제공합니다. 사용하려면 트레이트를 명령 클래스에 추가하고 명령의 run() 메서드에서 시그널을 등록하기만 하면 됩니다:
<?php
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
class SampleCommand extends BaseCommand
{
public function run(array $params): int
{
// Register basic termination signals
$this->registerSignals();
// Main processing loop
while ($this->isRunning()) {
// Do work here
$this->processItem();
sleep(3);
}
CLI::write('Command terminated gracefully', 'green');
return EXIT_SUCCESS;
}
}
이렇게 하면 수신 시 $running 상태를 false로 설정하는 3개의 종료 시그널이 등록됩니다.
사용자 정의 시그널 핸들러
특정 동작을 위해 시그널을 사용자 정의 메서드에 매핑할 수 있습니다:
<?php
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
class SampleCommand extends BaseCommand
{
public function run(array $params): int
{
// Register signals with custom handlers
$this->registerSignals(
[SIGTERM, SIGINT, SIGUSR1, SIGUSR2],
[
SIGTERM => 'onGracefulShutdown',
SIGINT => 'onInterrupt',
SIGUSR1 => 'onToggleDebug',
SIGUSR2 => 'onStatusReport',
],
);
while ($this->isRunning()) {
// Call custom method
$this->doWork();
sleep(1);
}
return EXIT_SUCCESS;
}
protected function onGracefulShutdown(int $signal): void
{
CLI::write('Received SIGTERM - shutting down gracefully...', 'yellow');
}
protected function onInterrupt(int $signal): void
{
CLI::write('Received SIGINT - stopping!', 'red');
}
protected function onToggleDebug(int $signal): void
{
// Custom debug mode
$this->debugMode = ! $this->debugMode;
CLI::write('Debug mode: ' . ($this->debugMode ? 'ON' : 'OFF'), 'blue');
}
protected function onStatusReport(int $signal): void
{
$state = $this->getProcessState();
CLI::write('Status: ' . json_encode($state, JSON_PRETTY_PRINT), 'cyan');
}
}
기본 시그널 핸들러
명시적 매핑이 없는 시그널에는 일반적인 onInterruption() 메서드를 구현할 수 있습니다:
<?php
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
class SampleCommand extends BaseCommand
{
public function run(array $params): int
{
// Register signals without explicit mappings
$this->registerSignals([SIGTERM, SIGINT, SIGHUP, SIGUSR1]);
while ($this->isRunning()) {
$this->doWork();
sleep(1);
}
return EXIT_SUCCESS;
}
/**
* Generic handler for all unmapped signals
*/
protected function onInterruption(int $signal): void
{
$signalName = $this->getSignalName($signal);
CLI::write("Received {$signalName} - handling generically", 'yellow');
switch ($signal) {
case SIGTERM:
CLI::write('Graceful shutdown requested', 'green');
break;
case SIGINT:
CLI::write('Immediate shutdown requested', 'red');
break;
case SIGHUP:
CLI::write('Configuration reload requested', 'blue');
break;
case SIGUSR1:
CLI::write('User signal 1 received', 'cyan');
break;
default:
CLI::write('Unknown signal received', 'light_gray');
break;
}
}
}
중요 구간
어떤 작업은 절대 중단되면 안 됩니다(데이터베이스 트랜잭션, 파일 작업). 원자적 작업을 만들려면 withSignalsBlocked()를 사용하세요:
<?php
use CodeIgniter\CLI\CLI;
class SampleCommand extends \BaseCommand
{
// ...
private function processOrder(array $orderData): void
{
// Critical section - no interruptions allowed
$result = $this->withSignalsBlocked(function () use ($orderData) {
CLI::write('Starting critical transaction - signals blocked', 'yellow');
// Start database transaction
$this->db->transStart();
try {
// Create order record
$orderId = $this->createOrder($orderData);
// Update inventory
$this->updateInventory($orderData['items']);
// Process payment
$this->processPayment($orderId, $orderData['payment']);
// Commit transaction
$this->db->transCommit();
CLI::write('Transaction completed successfully', 'green');
return $orderId;
} catch (\Exception $e) {
// Rollback on error
$this->db->transRollback();
throw $e;
}
});
CLI::write('Critical section complete - signals restored', 'cyan');
CLI::write("Order {$result} processed successfully", 'green');
}
}
중요 구간에서는 데이터 손상을 막기 위해 Ctrl+Z를 포함한 모든 시그널이 차단됩니다.
일시정지와 재개
SignalTrait는 사용자 정의 핸들러와 함께 올바른 Unix 작업 제어를 지원합니다:
<?php
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\I18n\Time;
class SampleCommand extends BaseCommand
{
public function run(array $params): int
{
// Register pause/resume signals with custom handlers
$this->registerSignals(
[SIGTERM, SIGINT, SIGTSTP, SIGCONT],
[
SIGTSTP => 'onPause',
SIGCONT => 'onResume',
],
);
while ($this->isRunning()) {
$this->processWork();
sleep(2);
}
return EXIT_SUCCESS;
}
protected function onPause(int $signal): void
{
CLI::write('Pausing - saving current date...', 'yellow');
// Save current timestamp
$state = [
'timestamp' => Time::now()->getTimestamp(),
];
file_put_contents(WRITEPATH . 'app_state.json', json_encode($state));
CLI::write('State saved. Process will now suspend.', 'green');
}
protected function onResume(int $signal): void
{
CLI::write('Resuming - restoring...', 'green');
$file = WRITEPATH . 'app_state.json';
// Restore saved state
if (file_exists($file)) {
$state = json_decode(file_get_contents($file), true);
$date = Time::createFromTimestamp($state['timestamp'])->format('Y-m-d H:i:s');
CLI::write('Restored from ' . $date, 'cyan');
}
CLI::write('Resuming normal operation...', 'green');
}
}
일시정지/재개 동작 방식
SIGTSTP 수신: 사용자 정의
onPause()핸들러가 실행됩니다프로세스 일시정지: 표준 Unix 작업 제어를 사용합니다
SIGCONT 수신: 프로세스가 재개되고 이어서
onResume()핸들러가 실행됩니다
이렇게 하면 적절한 셸 통합을 유지하면서 일시정지 전에 상태를 저장하고 재개 후 복원할 수 있습니다.
중요한 제한 사항
셸 작업 제어 vs 수동 시그널
셸 작업 제어를 사용하는 것과 시그널을 수동으로 보내는 것 사이에는 중요한 차이가 있습니다:
# RECOMMENDED: Use shell job control
php spark my:command
# Press Ctrl+Z to suspend
fg # Resume - maintains terminal control
# PROBLEMATIC: Manual signal sending
php spark my:command &
kill -TSTP $PID # Suspend
kill -CONT $PID # Resume - may lose terminal control
수동 SIGCONT의 문제
다른 터미널에서 kill -CONT를 수동으로 보내면:
- 예상 동작:
프로세스가 재개되고 사용자 정의 핸들러가 실행됩니다
- 부작용:
프로세스가 포그라운드 터미널 제어를 잃습니다
Ctrl+C와 Ctrl+Z가 작동하지 않을 수 있습니다
프로세스가 백그라운드 상태로 실행됩니다
이는 수동 kill -CONT가 프로세스를 터미널의 포그라운드 프로세스 그룹으로 복원하지 않기 때문에 발생합니다.
일시정지/재개 모범 사례
셸 작업 제어 사용 (Ctrl+Z, fg, bg)을 가능한 경우 사용하세요
애플리케이션이 수동 시그널 제어를 필요로 한다면 이 제한 사항을 문서화하세요
자동화 환경을 위해 대체 제어 방법을 제공하세요
배포 환경에서 충분히 테스트하세요
시그널 보내기
명령줄에서
kill 명령으로 실행 중인 프로세스에 시그널을 보낼 수 있습니다:
# Get the process ID
php spark long:running:command &
echo $! # Shows PID, e.g., 12345
# Send different signals
kill -TERM 12345 # Graceful shutdown
kill -INT 12345 # Interrupt (same as Ctrl+C)
kill -HUP 12345 # Hangup
kill -USR1 12345 # User-defined signal 1
kill -USR2 12345 # User-defined signal 2
# Pause and resume
kill -TSTP 12345 # Suspend (same as Ctrl+Z)
kill -CONT 12345 # Resume (same as fg)
키보드 단축키
다음 키보드 단축키는 포그라운드 프로세스에 시그널을 보냅니다:
Ctrl+C: SIGINT(인터럽트)를 보냅니다
Ctrl+Z: SIGTSTP(일시정지/일시중단)를 보냅니다
Ctrl+\: SIGQUIT(코어 덤프와 함께 종료)를 보냅니다
작업 제어
표준 Unix 작업 제어가 매끄럽게 동작합니다:
php spark long:command # Run in foreground
# Press Ctrl+Z to suspend
bg # Move to background
fg # Bring back to foreground
jobs # List suspended jobs
시그널 디버깅
프로세스 상태 정보
시그널 문제를 디버깅하려면 getProcessState()를 사용하세요:
<?php
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
class SampleCommand extends BaseCommand
{
// ...
protected function debugProcessState(): void
{
$state = $this->getProcessState();
CLI::write('=== PROCESS DEBUG INFO ===', 'yellow');
CLI::write('PID: ' . $state['pid'], 'cyan');
CLI::write('Running: ' . ($state['running'] ? 'YES' : 'NO'), 'cyan');
CLI::write('PCNTL Available: ' . ($state['pcntl_available'] ? 'YES' : 'NO'), 'cyan');
CLI::write('Signals Registered: ' . $state['registered_signals'], 'cyan');
CLI::write('Signal Names: ' . implode(', ', $state['registered_signals_names']), 'cyan');
CLI::write('Explicit Mappings: ' . $state['explicit_mappings'], 'cyan');
CLI::write('Signals Blocked: ' . ($state['signals_blocked'] ? 'YES' : 'NO'), 'cyan');
CLI::write('Memory Usage: ' . $state['memory_usage_mb'] . ' MB', 'cyan');
CLI::write('Peak Memory: ' . $state['memory_peak_mb'] . ' MB', 'cyan');
// POSIX info (if available)
if (isset($state['session_id'])) {
CLI::write('Session ID: ' . $state['session_id'], 'cyan');
CLI::write('Process Group: ' . $state['process_group'], 'cyan');
CLI::write('Has Terminal: ' . ($state['has_controlling_terminal'] ? 'YES' : 'NO'), 'cyan');
}
CLI::write('========================', 'yellow');
}
}
다음과 같은 종합 정보를 반환합니다:
프로세스 ID와 실행 상태
등록된 시그널과 매핑
메모리 사용 통계
터미널 제어 정보(세션, 프로세스 그룹)
시그널 차단 상태
클래스 참조
- trait CodeIgniter\CLI\SignalTrait
- registerSignals($signals = [SIGTERM, SIGINT, SIGHUP, SIGQUIT], $methodMap = [])
- 매개변수:
$signals (
array) – 처리할 시그널 목록$methodMap (
array) – 선택적 시그널-메서드 매핑
- 반환 형식:
void
선택적 사용자 정의 메서드 매핑으로 시그널 핸들러를 등록합니다.
<?php // Basic signal registration $this->registerSignals(); // Register specific signals $this->registerSignals([SIGTERM, SIGINT]); // Register signals with custom method mapping $this->registerSignals( [SIGTERM, SIGINT, SIGUSR1], [ SIGTERM => 'handleGracefulShutdown', SIGUSR1 => 'handleReload', ], );
참고
PCNTL 확장이 필요합니다. Windows에서는 시그널 처리가 자동으로 비활성화됩니다.
- isRunning()
- 반환:
프로세스를 계속 실행해야 하면 true, 아니면 false
- 반환 형식:
bool
프로세스가 계속 실행되어야 하는지(종료되지 않았는지) 확인합니다.
<?php use CodeIgniter\CLI\CLI; // Main application loop while ($this->isRunning()) { // Process work items $this->processNextItem(); // Small delay to prevent CPU spinning usleep(100000); // 0.1 seconds } CLI::write('Process terminated gracefully.');
- shouldTerminate()
- 반환:
종료 요청이 있으면 true, 아니면 false
- 반환 형식:
bool
종료 요청이 있었는지 확인합니다(
isRunning()의 반대).<?php use CodeIgniter\CLI\CLI; // Check for termination before expensive operations if ($this->shouldTerminate()) { CLI::write('Termination requested, skipping file processing.'); return; } // Process large file foreach ($largeDataSet as $item) { // Check periodically during long operations if ($this->shouldTerminate()) { CLI::write('Termination requested during processing.'); break; } $this->processItem($item); }
- requestTermination()
- 반환 형식:
void
프로세스 종료를 수동으로 요청합니다.
<?php use CodeIgniter\CLI\CLI; // Request termination based on conditions if ($errorCount > $this->maxErrors) { CLI::write("Too many errors ({$errorCount}), requesting termination.", 'red'); $this->requestTermination(); return; }
- resetState()
- 반환 형식:
void
모든 상태를 초기화합니다. 테스트나 재시작 시나리오에 유용합니다.
- withSignalsBlocked($operation)
- 매개변수:
$operation (
callable) – 중단 없이 실행할 중요 작업
- 반환:
작업 결과
- 반환 형식:
mixed
모든 시그널을 차단해 어떤 중단도 발생하지 않도록 중요 작업을 실행합니다.
참고
이것은 종료 시그널(SIGTERM, SIGINT), 일시정지/재개 시그널(SIGTSTP, SIGCONT), 사용자 정의 시그널(SIGUSR1, SIGUSR2)을 포함한 모든 중단 가능한 시그널을 차단합니다. 차단할 수 없는 SIGKILL만 여전히 프로세스를 종료할 수 있습니다.
- areSignalsBlocked()
- 반환:
현재 시그널이 차단되어 있으면 true, 아니면 false
- 반환 형식:
bool
현재 시그널이 차단되어 있는지 확인합니다.
- mapSignal($signal, $method)
- 매개변수:
$signal (
int) – 시그널 상수$method (
string) – 이 시그널에 대해 호출할 메서드 이름
- 반환 형식:
void
실행 중에 시그널-메서드 매핑을 추가하거나 업데이트합니다.
<?php use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use CodeIgniter\CLI\SignalTrait; class SampleCommand extends BaseCommand { use SignalTrait; public function run(array $params): int { // Register signals first $this->registerSignals([SIGTERM, SIGINT, SIGUSR1, SIGUSR2]); // Map signals to specific methods at runtime $this->mapSignal(SIGUSR1, 'handleReload'); $this->mapSignal(SIGUSR2, 'handleStatusDump'); } // Custom signal handlers public function handleReload(int $signal): void { CLI::write('Received reload signal, reloading configuration...'); $this->reloadConfig(); } public function handleStatusDump(int $signal): void { CLI::write('=== Process Status ==='); $this->printStatus($this->getProcessState()); } }
- getSignalName($signal)
- 매개변수:
$signal (
int) – 시그널 상수
- 반환:
사람이 읽을 수 있는 시그널 이름
- 반환 형식:
string
시그널 상수의 사람이 읽을 수 있는 이름을 가져옵니다.
<?php use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use CodeIgniter\CLI\SignalTrait; class SampleCommand extends BaseCommand { use SignalTrait; public function run(array $params): int { // ... } // Log signal information public function onInterruption(int $signal): void { $signalName = $this->getSignalName($signal); CLI::write("Received signal: {$signalName} ({$signal})", 'yellow'); } }
- hasSignals()
- 반환:
등록된 시그널이 있으면 true, 아니면 false
- 반환 형식:
bool
시그널이 하나라도 등록되어 있는지 확인합니다.
- getSignals()
- 반환:
등록된 시그널 상수 배열
- 반환 형식:
array
등록된 시그널 상수 배열을 가져옵니다.
- getProcessState()
- 반환:
종합적인 프로세스 상태 정보
- 반환 형식:
array
프로세스 ID, 메모리 사용량, 시그널 처리 상태, 터미널 제어 정보를 포함한 종합적인 프로세스 상태 정보를 가져옵니다.
<?php use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use CodeIgniter\CLI\SignalTrait; class SampleCommand extends BaseCommand { use SignalTrait; public function run(array $params): int { // ... } // Debug process state public function debugProcessState(): void { $state = $this->getProcessState(); CLI::write('=== Process Debug Information ==='); CLI::write("PID: {$state['pid']}"); CLI::write('Running: ' . ($state['running'] ? 'Yes' : 'No')); CLI::write("Memory Usage: {$state['memory_usage_mb']} MB"); CLI::write("Peak Memory: {$state['memory_peak_mb']} MB"); CLI::write('Registered Signals: ' . implode(', ', $state['registered_signals_names'])); CLI::write('Signals Blocked: ' . ($state['signals_blocked'] ? 'Yes' : 'No')); } }
- unregisterSignals()
- 반환 형식:
void
모든 시그널 등록을 해제하고 리소스를 정리합니다.
참고
이렇게 하면 이전에 등록된 모든 시그널에 대한 처리 동작이 제거됩니다.