크래프톤 정글 (컴퓨터 시스템: 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)
- 커널은 프로세스에게 시그널을 강제로 처리하도록 만든다.
- 시그널 처리 방식:
- 무시 (ignore)
- 종료 (terminate)
- 사용자 정의 핸들러(signal handler) 실행
핵심 특징
- 한 프로세스에는 같은 종류의 시그널은 최대 하나만 pending 상태가 될 수 있다.
- 시그널이 block된 상태라면 pending으로 쌓이지만, unblock되기 전에는 수신되지 않는다.
- 커널은 각 프로세스에 대해:
- pending bit vector (보류 중인 시그널 집합)
- blocked bit vector (차단된 시그널 집합)을 유지함.
시그널 흐름 예시
- SIGINT 시그널 발생 (예: Ctrl+C)
- 프로세스가 수신 대기 중이면 → 핸들러 실행
- 핸들러 종료 후 → 중단된 명령어 위치에서 실행 재개
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 (시그널 수신)
커널이 시그널을 수신하고 처리하는 방식과 프로세스가 시그널에 반응하는 방식을 설명한다.
시그널 수신은 커널 모드에서 유저 모드로 전환될 때 발생할 수 있는 비동기 제어 흐름이다.
커널의 시그널 수신 처리 흐름
- 프로세스가 시스템 콜에서 복귀하거나 컨텍스트 스위치 후 유저 모드로 돌아올 때,
- 커널은 프로세스에 대해 보류 중(pending) 시그널이 존재하고 해당 시그널이 블록되지 않았는지 확인한다.
- 조건이 만족되면, 커널은 하나의 시그널(보통 가장 낮은 번호)을 선택하여 프로세스가 해당 시그널을 받도록 강제한다.
- 시그널 처리 이후, 제어는 원래 실행 흐름의 다음 명령어 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로 프로그래머가 직접 차단/해제 |
용도 | 핸들러 내 일관성 유지, 공유 자원 보호, 레이스 조건 방지 |