리눅스 프로세스 시그널의 개념과 발생 메커니즘 심층 분석

시그널의 기본 개념과 비동기 특성

시그널(Signal)과 세마포어(Semaphore)는 이름이 유사하지만 완전히 다른 개념이다. 시그널은 운영체제에서 프로세스 간 통신 및 이벤트 알림을 위해 사용하는 가장 오래되고 기본적인 메커니즘 중 하나이다.

실생활에서 알람 소리는 수면을 중단하게 만들고, 교통신호는 보행이나 주행을 멈추게 한다. 이와 마찬가지로 리눅스 환경에서 시그널은 프로세스의 현재 실행 흐름을 중단시키고 특정 이벤트를 비동기적으로 통지하는 메커니즘으로 작동한다. 비동기적이라는 것은 시그널이 프로세스의 실행 상태와 무관하게 언제든지 발생할 수 있음을 의미한다.

시그널 처리와 관련된 주요 특성은 다음과 같다:

  • 프로세스는 시그널이 발생하기 전부터 해당 시그널을 어떻게 처리할지 미리 정의된 동작 방식을 내장하고 있다.
  • 시그널이 발생하더라도 프로세스는 즉시 처리하지 않고, CPU 스케줄링에 의해 실행 권한을 얻은 적절한 시점에 처리할 수 있다.
  • 시그널에 대한 인식과 처리 방식은 운영체제와 프로그래머에 의해 미리 설계되어 프로세스 내부에 탑재된다.

시그널 발생 경로 및 유형

1. 사용자 입력 및 명령어를 통한 발생

터미널에서 Ctrl+C를 누르면 포그라운드 프로세스가 종료된다. 이는 키보드 입력이 SIGINT(2번 시그널)로 변환되어 프로세스에 전달되기 때문이다. 리눅스에서는 kill -l 명령어를 통해 시스템에서 지원하는 모든 시그널 목록을 확인할 수 있다. 시그널은 본질적으로 정수 값이지만, 가독성을 위해 SIGINT, SIGKILL과 같은 매크로로 정의되어 사용된다.

시그널은 기본적으로 프로세스를 종료시키는 동작을 수행하지만, signal() 또는 sigaction() 시스템 콜을 활용하여 사용자 정의 핸들러를 등록하면 기본 동작을 재정의하거나 무시할 수 있다. 단, SIGKILL(9번)과 SIGSTOP(19번) 시그널은 시스템 안정성을 위해 캡처하거나 무시할 수 없도록 하드코딩되어 있다.

2. 전방/후방 프로세스와 시그널 전달

프로세스는 실행 방식에 따라 포그라운드(Foreground)와 백그라운드(Background)로 나뉜다. 쉘에서 명령어 뒤에 &를 붙이면 백그라운드로 실행된다. 키보드를 통한 시그널 입력은 오직 포그라운드 프로세스에만 전달된다. 백그라운드 프로세스는 표준 입력을 읽을 수 없으며, 이를 강제 종료하려면 kill -9 [PID] 명령어를 사용해야 한다.

프로세스 제어와 관련된 주요 쉘 명령어는 다음과 같다:

  • jobs: 현재 쉘 세션의 백그라운드 및 중지된 작업 목록 출력
  • fg [작업번호]: 백그라운드 작업을 포그라운드로 전환
  • bg [작업번호]: 중지된 백그라운드 작업을 다시 실행
  • Ctrl+Z: 포그라운드 프로세스를 일시 중지(Suspend)하고 백그라운드로 이동 (SIGTSTP 시그널 발생)

3. 시스템 콜을 통한 시그널 전송

프로세스는 내부적으로 시그널 발생 여부를 기록해야 한다. 이 정보는 프로세스 제어 블록(PCB, 리눅스의 task_struct) 내부의 비트맵(Bitmask)에 저장된다. 각 비트는 특정 시그널의 발생 여부를 나타내며, 1로 설정되면 해당 시그널이 대기 중임을 의미한다. 사용자가 kill 명령어나 kill() 시스템 콜을 호출하면, 커널이 대상 프로세스의 PCB 내 비트맵을 수정한다. 즉, 시그널 전송의 본질은 커널에 의한 프로세스 내부 데이터 구조 수정이다.

자체 프로세스에 시그널을 보내는 raise() 시스템 콜이나, 프로세스를 비정상적으로 종료시키고 코어 덤프를 생성하는 abort() 함수(SIGABRT 전송)도 이러한 시스템 콜 메커니즘을 기반으로 한다.

4. 하드웨어 예외 발생

프로그램 실행 중 하드웨어 레벨에서 치명적인 오류가 감지되면, CPU가 이를 운영체제에 알리고 OS는 해당 프로세스에 시그널을 전송한다.

  • 부동소수점 예외: CPU의 부동소수점 연산 장치(FPU)가 0으로 나누기 등의 오류를 감지하면 상태 레지스터에 플래그를 설정하고, OS는 이를 확인하여 SIGFPE 시그널을 보낸다.
  • 메모리 접근 위반: 메모리 관리 장치(MMU)가 매핑되지 않은 가상 메모리 주소에 접근하려 할 때 페이지 폴트(Page Fault)를 발생시킨다. OS는 이것이 유효한 접근이 아님을 판단하고 SIGSEGV(세그멘테이션 폴트) 시그널을 전송하여 프로세스를 종료시킨다.

5. 소프트웨어 조건 및 타이머

특정 소프트웨어적 조건이 충족될 때 커널이 시그널을 발생시키기도 한다. 대표적인 예로 파이프(Pipe) 통신에서 읽기 엔드포인트가 모두 닫혔을 때 쓰기 프로세스가 데이터를 기록하려고 하면 SIGPIPE 시그널이 발생한다.

또한 alarm() 시스템 콜을 사용하면 지정된 시간이 경과한 후 SIGALRM 시그널을 수신할 수 있다. 커널 내부에는 타이머를 관리하는 자료 구조(일반적으로 최소 힙)가 존재하며, 시스템 틱(Tick)마다 가장 짧은 시간의 타이머를 검사하여 만료된 타이머에 대해 시그널을 전달한다.

다음은 alarm()pause()를 활용하여 주기적인 작업을 수행하도록 재구성된 C++ 코드 예시이다.

#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <csignal>

using TaskCallback = std::function<void()>;
std::vector<TaskCallback> scheduled_tasks;

void dispatch_tasks(int signum) {
    for (const auto& task : scheduled_tasks) {
        task();
    }
    alarm(1); // 1초 후 다시 알람 설정
}

void monitor_resources() {
    std::cout << "[System] Monitoring CPU and Memory usage...\n";
}

void sync_data() {
    std::cout << "[System] Synchronizing in-memory data to disk...\n";
}

int main() {
    scheduled_tasks.emplace_back(monitor_resources);
    scheduled_tasks.emplace_back(sync_data);

    signal(SIGALRM, dispatch_tasks);
    alarm(1);

    // 시그널이 도착할 때까지 프로세스 대기
    while (true) {
        pause();
    }
    
    return 0;
}

이러한 소프트웨어 조건에 의한 시그널 발생은 운영체제가 백그라운드에서 주기적으로 상태를 점검하고 리소스를 관리하는 이벤트 루프 메커니즘으로 작동한다. 커널은 타이머 큐를 순회하며 조건이 충족된 프로세스의 PCB 비트맵을 업데이트하고, 이를 통해 비동기 이벤트를 주입한다.

태그: linux Process Signal SystemCall PCB

7월 4일 17:48에 게시됨