크래프톤 정글 (컴퓨터 시스템: CSAPP)/8장 예외적 제어 흐름

컴퓨터 시스템 : CSAPP 8장 정리 - 8.5 Signals Part.1 8.5.4 까지

고웅 2025. 4. 19. 10:29

8.5 Signals

이 절에서는 시그널(Signal)이라는 고수준 예외적 제어 흐름(high-level exceptional control flow) 메커니즘을 소개한다.
시그널은 커널 또는 다른 프로세스가 특정 프로세스에 이벤트 발생을 알려주는 작은 메시지다.

시그널의 개요

  • 시그널은 하드웨어 예외처럼 비동기적으로 발생할 수 있으며, 사용자 수준 프로세스에 도달할 수 있는 예외다.
  • 예: 사용자가 Ctrl+C를 누르면 커널이 SIGINT 시그널을 해당 프로세스에 보냄
  • 시그널은 총 30여 개 종류가 있으며, 각 시그널은 특정 이벤트에 대응한다:
    • SIGSEGV: 잘못된 메모리 참조
    • SIGCHLD: 자식 프로세스 종료
    • SIGKILL: 강제 종료
    • SIGALRM: 타이머 만료
시그널은 C에서 예외(exception)를 다룰 수 있는 비동기적인 방법으로서 매우 강력하고 유용하지만, 동시성 문제를 유발할 수 있어 주의가 필요하다​.

8.5.1 Signal Terminology (시그널 용어 설명)

시그녈 전달 과정은 두 단계로 이루어진다.

1. 시그널 전송 (Sending a signal)

  • 커널이 특정 이벤트를 감지하거나 프로세스가 명시적으로 kill 시스템 콜을 호출하면 시그널을 대상 프로세스의 컨텍스트에 등록한다.
  • 이 상태의 시그널은 "pending signal(보류 중인 시그널)"이라고 불린다.

2. 시그널 수신 (Receiving a signal)

  • 커널은 프로세스에게 시그널을 강제로 처리하도록 만든다.
  • 시그널 처리 방식:
    1. 무시 (ignore)
    2. 종료 (terminate)
    3. 사용자 정의 핸들러(signal handler) 실행

핵심 특징

  • 한 프로세스에는 같은 종류의 시그널은 최대 하나만 pending 상태가 될 수 있다.
  • 시그널이 block된 상태라면 pending으로 쌓이지만, unblock되기 전에는 수신되지 않는다.
  • 커널은 각 프로세스에 대해:
    • pending bit vector (보류 중인 시그널 집합)
    • blocked bit vector (차단된 시그널 집합)을 유지함​.

시그널 흐름 예시

  1. SIGINT 시그널 발생 (예: Ctrl+C)
  2. 프로세스가 수신 대기 중이면 → 핸들러 실행
  3. 핸들러 종료 후 → 중단된 명령어 위치에서 실행 재개

8.5.2 Sending Signals (시그널 보내기)

프로세스가 다른 프로세스에 시그널을 보내는 방법을 다룬다. 시그널은 프로세스 간 통신(IPC)의 일종으로 사용되며, 사용자 또는 커널이 특정 이벤트에 반응하기 위해 시그널을 전송한다.

시그널 전송 방법

1. kill 함수 (사용자 수준)

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

 

  • 기능: 프로세스 pid에 시그널 sig를 전송

사용 예시:

kill(1234, SIGINT);  // PID 1234에게 Ctrl+C 효과 전송

 

  • 주의: kill 함수는 프로세스를 "죽이는" 함수가 아니라 시그널을 보내는 함수다. 이름 때문에 혼동이 일어날 수 있으니 주의하라
  • pid 값의 의미:
pid 값 대상
> 0 해당 PID 하나의 프로세스
0 같은 그룹의 모든 프로세스
-1 사용자 권한 내 모든 프로세스
< -1 특정 프로세스 그룹

2. raise 함수 (자기 자신에게 시그널 보내기)

#include <signal.h>

int raise(int sig);
  • 현재 프로세스 자신에게 시그널 sig를 보냄
  • kill(getpid(), sig)와 동일

3. 명령줄에서 kill 명령어

  • 쉘 명령으로도 시그널 전송 가능:
  • ps, jobs 명령과 함께 사용하여 백그라운드 작업 제어 가능
$ kill -9 1234   # SIGKILL 전송
$ kill -INT 5678 # SIGINT 전송 (Ctrl+C 효과)

사용 예시

pid_t pid = Fork();
if (pid == 0) {
    // 자식 프로세스
    while (1);  // 무한 루프
} else {
    // 부모 프로세스
    sleep(2);
    kill(pid, SIGKILL);  // 자식 프로세스를 종료
}

보안 및 권한

  • root가 아닌 사용자 프로세스는 일반적으로 자신이 소유한 프로세스에게만 시그널을 보낼 수 있음
  • 권한이 없으면 kill은 -1을 반환하고 errno를 설정함

8.5.3 Receiving Signals (시그널 수신)

커널이 시그널을 수신하고 처리하는 방식과 프로세스가 시그널에 반응하는 방식을 설명한다.
시그널 수신은 커널 모드에서 유저 모드로 전환될 때 발생할 수 있는 비동기 제어 흐름이다.

커널의 시그널 수신 처리 흐름

  1. 프로세스가 시스템 콜에서 복귀하거나 컨텍스트 스위치 후 유저 모드로 돌아올 때,
  2. 커널은 프로세스에 대해 보류 중(pending) 시그널이 존재하고 해당 시그널이 블록되지 않았는지 확인한다.
  3. 조건이 만족되면, 커널은 하나의 시그널(보통 가장 낮은 번호)을 선택하여 프로세스가 해당 시그널을 받도록 강제한다.
  4. 시그널 처리 이후, 제어는 원래 실행 흐름의 다음 명령어 I_next로 이어진다​.

시그널의 기본 동작 (Default Actions)

각 시그널은 기본 동작(default action)을 가진다:

  • 프로세스 종료
  • 코어 덤프 후 종료
  • 프로세스 정지(SIGSTOP)
  • 무시(SIGCHLD 등)

→ 예: SIGKILL의 기본 동작은 즉시 종료, SIGCHLD는 무시

사용자 정의 시그널 핸들러 등록

프로세스는 signal 함수를 통해 특정 시그널에 대한 기본 동작을 수정할 수 있다.

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);
  • SIG_IGN: 해당 시그널을 무시
  • SIG_DFL: 기본 동작으로 설정
  • 사용자 함수 주소: 해당 시그널이 오면 호출할 시그널 핸들러 등록

핸들러 작동 방식

  • 시그널 핸들러는 시그널 번호를 인자로 받는 함수이며, 이를 등록하는 과정을 installing the handler, 핸들러가 호출되는 것을 catching the signal, 그 실행을 handling the signal이라 부른다.
  • 하나의 핸들러 함수로 여러 종류의 시그널을 처리할 수 있도록, 시그널 번호가 인자로 전달된다.

실행 흐름 예시

void handler(int sig) {
    printf("Caught signal %d\n", sig);
}

int main() {
    signal(SIGINT, handler);  // Ctrl+C 처리
    pause();  // 시그널 기다리기
    return 0;
}

→ Ctrl+C를 누르면 handler(2)가 호출됨 (SIGINT는 2번)


8.5.4 Blocking and Unblocking Signals (시그널 차단과 해제)

시그널 처리 중 또는 특정 코드 영역에서 시그널의 수신을 일시적으로 차단(blocking)하고 다시 해제(unblocking)하는 방법을 설명한다.

암시적 차단 (Implicit Blocking)

  • 커널은 현재 처리 중인 시그널과 동일한 종류의 시그널은 자동으로 차단한다.
  • 예: SIGINT 핸들러가 실행 중일 때 또 다른 SIGINT가 발생하면, 해당 시그널은 보류(pending) 상태로 남고, 핸들러가 종료될 때까지 수신되지 않음.

명시적 차단 (Explicit Blocking)

  • 프로그래머가 직접 시그널을 차단하거나 해제할 수 있음.
  • 주요 함수:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
  • how 인자는 세 가지 방식 지정:
    • SIG_BLOCK: 주어진 시그널을 차단 목록에 추가
    • SIG_UNBLOCK: 차단 목록에서 제거
    • SIG_SETMASK: 차단 목록을 주어진 세트로 완전히 교체

예제: SIGINT 일시 차단

sigset_t mask, prev_mask;

Sigemptyset(&mask);
Sigaddset(&mask, SIGINT);

/* SIGINT 차단하고 이전 차단 집합 저장 */
Sigprocmask(SIG_BLOCK, &mask, &prev_mask);

/* SIGINT로부터 보호할 코드 */
...
/* 이전 상태로 복원 (SIGINT 차단 해제) */
Sigprocmask(SIG_SETMASK, &prev_mask, NULL);

위 코드는 SIGINT 시그널이 수신되지 않도록 보호하며, 이후 원래 설정으로 복원​.

요약

개념 설명
암시적 차단 현재 핸들러가 처리 중인 시그널은 자동 차단
명시적 차단 sigprocmask로 프로그래머가 직접 차단/해제
용도 핸들러 내 일관성 유지, 공유 자원 보호, 레이스 조건 방지