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

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

고웅 2025. 4. 19. 10:46

8.5.5 Writing Signal Handlers (시그널 핸들러 작성)

시그널 핸들러를 올바르게 작성하는 방법에 대해 설명한다. 시그널 핸들러는 예외적으로 실행되며, 메인 프로그램과 동시에 실행될 수 있기 때문에 작성이 까다롭다.

시그널 핸들러 작성의 어려움

  • 시그널 핸들러는 메인 프로그램과 동시에 실행되며, 전역 변수를 공유함.
  • 언제 시그널이 수신되는지는 예측이 어려움 → 비순차적 실행
  • 시스템마다 동작 방식이 다를 수 있어 이식성 문제도 있음​.

안전한 시그널 핸들러 작성 지침

G0. 핸들러를 최대한 단순하게 유지

  • 가능한 한 간단한 동작만 수행해야 함
  • 예: 전역 변수에 플래그를 설정하고 바로 반환 → 메인 루프가 주기적으로 플래그를 검사하여 처리

G1. async-signal-safe 함수만 호출

  • 시그널 핸들러 내에서는 printf, malloc, exit 등은 사용 금지
  • 안전한 함수 예시: write, _exit 등
  • 이를 위해 책에서는 Sio (Safe I/O) 패키지를 제공하여 출력 지원
void sigint_handler(int sig) {
    Sio_puts("Caught SIGINT!\n");
    _exit(0);
}

G2. errno 저장 및 복원

  • 핸들러 내부에서 errno를 덮어쓰면, 다른 코드에 영향 줄 수 있음
  • errno를 로컬 변수에 저장하고, 핸들러 종료 전 복원할 것

G3. 전역 데이터 접근 시 모든 시그널 차단

  • 전역 변수는 시그널 핸들러와 메인 코드에서 동시 접근 시 충돌 위험
  • 구조체나 배열을 다룰 때는 sigprocmask로 시그널 차단 후 접근

G4. volatile로 전역 변수 선언

  • 컴파일러가 변수 캐싱을 하지 않도록 함
  • 예: volatile int g;

G5. sig_atomic_t로 플래그 선언

  • sig_atomic_t는 읽기/쓰기 연산이 원자적(atomic)이므로 시그널 간 충돌 방지
  • 예: volatile sig_atomic_t flag;

예제 요약

volatile sig_atomic_t flag = 0;

void handler(int sig) {
    flag = 1;
}

→ 메인 프로그램은 주기적으로 flag를 체크하여 시그널 수신 여부 확인


8.5.6 Synchronizing Flows to Avoid Nasty Concurrency Bugs(흉측한 동시성 버그를 피하기 위한 흐름 동기화)

동시에 실행되는 여러 흐름(logical flows)같은 메모리 공간을 읽고 쓸 때 발생할 수 있는 경쟁 조건(race condition)을 방지하는 방법을 설명한다.

동시성 프로그래밍의 어려움

  • 여러 흐름이 같은 데이터에 접근할 수 있을 때, 그 실행 순서(=interleaving)에 따라 프로그램의 동작이 달라질 수 있음.
  • 가능한 실행 순서 조합은 지수적으로 많으며, 일부 조합만이 올바른 결과를 만든다.
  • 모든 흐름을 올바르게 동기화하지 않으면 비결정적이고 디버깅이 어려운 버그가 생긴다​.

문제 예시: 셸 프로그램의 동기화 오류

  • 부모가 fork()로 자식을 만든 후 자식 정보를 job 리스트에 추가(addjob)해야 함.
  • 자식이 너무 빨리 종료되어 SIGCHLD 핸들러가 먼저 실행되어 deletejob을 호출할 수 있음.
  • 이 경우 아직 addjob이 호출되지 않았기 때문에, deletejob은 아무 작업도 하지 않음 → 이후 addjob이 호출되며 리스트에 유령(job이 이미 종료된 자식)의 정보가 남게 됨.

해결 방법: 시그널 블로킹을 통한 동기화

  • 해결책은 fork() 이전에 SIGCHLD를 차단(block)하고,
  • addjob 이후에 다시 SIGCHLD를 해제(unblock)하는 것이다.

이 방식은 다음을 보장한다:

  • 자식 프로세스가 종료하더라도 SIGCHLD는 블로킹되어 처리되지 않음
  • 부모가 addjob을 완료한 뒤에만 SIGCHLD 핸들러가 실행되며, deletejob이 정상 작동함

핵심 아이디어 정리

문제 해결
시그널이 예기치 않게 먼저 실행됨 시그널 블로킹하여 흐름 제어
전역 데이터(job 리스트) 수정 타이밍 오류 동기화를 통해 올바른 순서 보장

8.5.7 Explicitly Waiting for Signals (명시적으로 시그널을 기다리기)

시그널 핸들러가 실행될 때까지 명시적으로 기다리는 방법에 대해 설명한다.
이는 전통적인 블로킹(waiting) 방식 대신 비동기 시그널 처리를 사용하는 구조에서 매우 중요하다.

동기화된 대기 시나리오

예: 셸(shell)이 포그라운드 작업을 실행할 때, 그 작업이 종료될 때까지 다음 사용자 명령을 받지 않도록 기다려야 한다. 이 경우 SIGCHLD 시그널이 자식의 종료를 알려주며, 셸은 이 시그널을 기다린다.

잘못된 방법들

Spin loop (회전 루프)

while (!pid);  // 바쁜 대기, CPU 낭비

pause() 사용

while (!pid)
    pause();  // SIGCHLD를 놓치면 영원히 멈춤

sleep() 사용

while (!pid)
    sleep(1);  // 너무 느리거나 비효율적

→ 이 방식들은 레이스 조건(race condition)이나 CPU 자원 낭비를 초래한다​.

이상적인 해결: sigsuspend 사용

  • sigsuspend는 일시적으로 시그널 마스크를 교체하고, 시그널이 도착할 때까지 안전하게 기다림
  • 시그널이 수신되면, 원래 마스크로 복원된 후 복귀