시그널, 비동기 흐름의 시작
지금까지 본 프로세스의 흐름은 전부 순차적이고 예측 가능한 흐름이었는데요. 하지만 현실에서는 프로그램이 외부 이벤트나 내부 조건으로 인해 갑자기 흐름이 바뀌는 일이 자주 발생합니다. 바로 이런 상황에서 등장하는 것이 시그널(signal)입니다.
이번 포스트에서는 CSAPP 8.5절 내용을 바탕으로 시그널이란 무엇인지, 시그널을 어떻게 다룰 수 있는지, 안전하게 사용하는 방법은 무엇인지를 순서대로 정리해 봅니다.
시그널이란?
시그널(signal)은 커널이 프로세스에 보내는 비동기 이벤트 알림 메시지입니다. 각 시그널은 예기치 않은 시점에 발생한 특정 사건을 숫자로 표현한 기호라고 할 수 있죠.
시그널이 발생하는 경우
시그널 | 상황 | 설명 |
SIGINT | Ctrl+C | 사용자가 인터럽트 요청 |
SIGTERM | 종료 요청 | kill 명령 등 |
SIGSEGV | 잘못된 메모리 접근 | 세그멘테이션 오류 |
SIGCHLD | 자식 종료 | 자식 프로세스가 종료되었을 때 |
SIGALRM | 타이머 만료 | alarm() 호출 후 시간 초과 |
시그널의 기본 처리 방식
각 시그널에는 해당 시그널을 받은 프로세스가 기본적으로 취하는 기본 동작(default action)이 정해져 있습니다.
- SIGINT -> 프로그램 종료
- SIGCHLD -> 무시
- SIGSEGV -> 코어 덤프 + 종료
단, 대부분의 시그널은 사용자가 핸들러를 등록해 동작을 변경할 수 있습니다.
시그널 핸들링
사용자 정의 핸들러 등록 (signal)
SIGINT 시그널에 대한 핸들러를 등록하는 C 코드 예시는 다음과 같습니다.
#include <signal.h>
void handler(int sig) {
printf("Received signal %d\n", sig);
}
int main() {
signal(SIGINT, handler); // Ctrl+C 누르면 handler 실행
while (1) pause(); // 무한 대기
}
signal()
은 특정 시그널에 대해 사용자가 정의한 함수를 실행- 핸들러는 OS가 호출하는 콜백 함수처럼 작동
시그널 동작 방식
시그널이 동작하는 방식을 요약하면 다음과 같습니다.
- 커널이 시그널을 프로세스에 전달한다.
- 핸들러가 등록된 경우, 핸들러를 실행
- 핸들러가 등록되지 않은 경우, 기본 동작을 수행
- 핸들러가 끝나면 중단됐던 코드 위치로 복귀
시그널 사용 시 주의사항
시그널은 언제든지 발생할 수 있기 때문에, 코드가 예측 가능한 순서대로 실행되길 기대할 경우 버그의 원인이 될 수 있습니다. 따라서 시그널 핸들러 내부에서는 주의해서 코드를 짜야 합니다.
시그널의 안전한 사용
시그널 핸들러 내부에서는 반드시 “안전한 함수(Async-signal-safe functions)”만을 호출해야 하는데요. 시그널은 언제 어디서든 발생할 수 있기 때문에, 기존에 실행 중이던 코드가 도중에 끊기고 시그널 핸들러로 점프하게 됩니다.
이때, 파일을 쓰거나 메모리를 할당하던 도중과 같은 상황에서 동시에 같은 자원을 건드리는 함수를 호출하게 되면, 데이터가 꼬이거나 프로그램이 예기치 않게 동작할 수 있습니다. 예를 들어 malloc()
이 메모리를 정리하던 도중 시그널이 발생해 또 malloc()
을 호출하면 문제가 생길 수 있죠.
그래서 리눅스는 시그널 핸들러 안에서 호출해도 되는 "안전한 함수 목록"을 제공합니다. 이를 Async-signal-safe 함수라고 하며, 대표적으로 write()
, _exit()
, sigaction()
등이 있습니다. 결론적으로 시그널 핸들러는 비상 상황용 코드이기 때문에, 무겁고 복잡한 함수는 피하고 작고 빠르고 안전한 함수만을 써야 합니다.
Async-signal-safe 함수 예시
함수명 | 설명 |
_exit() | 즉시 종료. 버퍼 플러시 없이 프로세스를 종료함 |
write() | 저수준 출력. 표준 출력(stdout)도 직접 파일 디스크립터로 출력 |
signal() | 시그널 핸들러 등록/재설정. 다만 sigaction() 사용 권장 |
kill() | 프로세스에 시그널 보내기. (본인에게도 전송 가능) |
alarm() | 타이머 설정. n초 후 SIGALRM 시그널을 해당 프로세스에 전달 |
반면, printf()
, malloc()
, free()
, fork()
등의 함수는 비동기로 동작하며 안전하지 않기 때문에 사용이 금지됩니다.
이를 C 코드로 예를 들어보면 다음과 같습니다.
void handler(int sig) {
write(STDOUT_FILENO, "Signal received\n", 17); // 사용 가능
// printf("NOPE!\n"); ← 위험하므로 사용 금지
}
원칙적으로 시그널 핸들러 내부에서는 가급적 로직을 최소화하고, 주요 처리는 메인 루프에서 담당하도록 구조를 짜야 합니다.
핵심 요약
- 시그널은 커널이 프로세스에 보내는 비동기 이벤트 알림 메시지이다
- signal()은 사용자 핸들러 등록 함수이다
- 기본 동작은 시그널마다 정의된 기본 처리 방식이다
- 핸들러 흐름은 실행 중단 -> 핸들러 호출 -> 원래 코드 복귀 순으로 이루어진다
- async-signal-safe 개념은 시그널 핸들러에서 안전하게 호출할 수 있는 함수들만 사용해야 한다는 것이다
'크래프톤 정글 > 컴퓨터구조(CSAPP)' 카테고리의 다른 글
[CSAPP 8장 완전 정복] 8.7~8.8 프로세스 도구 실전 사용 + 8장 전체 요약 (0) | 2025.04.21 |
---|---|
[CSAPP 8장 완전 정복] 8.6 비지역 점프 - setjmp, longjmp로 흐름 전환하기 (0) | 2025.04.21 |
[CSAPP 8장 완전 정복] 8.4 프로세스를 만들고 제어하는 기술들 - fork, exec, wait 완전 정복 (0) | 2025.04.19 |
[CSAPP 8장 완전 정복] 8.1~8.3 예외와 프로세스, 시스템의 흐름을 바꾸는 힘 (0) | 2025.04.19 |
[CSAPP 7장 완전 정복] 7.14~7.15 오브젝트 파일 분석과 실행 흐름 마무리 (0) | 2025.04.18 |