리눅스 시그널 완전 정리 - 쉘과 프로세스 제어
📖 목차
1. 시그널이란?
시그널은 프로세스 간 통신(IPC) 메커니즘 중 하나로 비동기적으로 프로세스에게 특정 이벤트를 알려주는 소프트웨어 인터럽트.
쉽게 말하면 프로세스에게 "이런 일이 일어났으니 적절히 반응해!"라고 알려주는 메시지 시스템.
2. 쉘과 프로세스 관계
🔹 쉘 안의 프로세스들
한 쉘에서 **앞에서 실행 중인 프로그램(포그라운드 프로세스)**는 보통 1개.
- 예: ./a.out 실행하면 → 쉘이 자식 프로세스를 만들어서 그 프로그램 실행
- 이때는 쉘이 잠시 대기 상태로 들어감 (포그라운드 프로세스가 끝날 때까지)
하지만 백그라운드로 돌리면 여러 개 동시에 실행 가능 (& 붙일 때).
$ ./program1 & # 백그라운드 실행
$ ./program2 & # 또 다른 백그라운드 실행
$ ./program3 # 포그라운드 실행 (대기)
3. Ctrl+C와 Ctrl+Z의 차이점
🔹 Ctrl+C (SIGINT)
- Interrupt (중단) 신호를 포그라운드 프로세스 그룹 전체에 보냄
- 기본 동작: 즉시 종료
- 의미: "그만 실행하고 끝내라"
$ ./a.out
^C ← 여기 누르면 프로세스가 죽음
[프로세스 종료]
$
🔹 Ctrl+Z (SIGTSTP)
- Stop (일시정지) 신호를 포그라운드 프로세스 그룹 전체에 보냄
- 프로세스가 메모리에 남아있지만 실행은 멈춤
- 쉘 프롬프트가 돌아오고, jobs 명령어로 확인 가능
- fg로 다시 포그라운드 실행, bg로 백그라운드 실행 가능
$ ./a.out
^Z ← 멈춤
[1]+ Stopped ./a.out
$ jobs
[1]+ Stopped ./a.out
$ fg ← 다시 실행
./a.out
🔹 SIGTSTP 동작 과정
- 쉘이 실행 중인 포그라운드 프로세스 그룹에 SIGTSTP 신호를 보냄
- 현재 실행 중이던 프로그램이 멈춤(Stop 상태)
- "CPU 스케줄링 안 받고 메모리에만 남아 있음"
- 해당 프로세스는 Stopped 상태로 전환
- ps나 jobs에서 보면 T (stopped) 상태로 표시됨
- 쉘은 다시 프롬프트로 돌아옴
- 일단 멈추고 쉘 포그라운드로 빠져나옴
🔹 이후 제어 명령어
- fg %번호 → 해당 작업을 포그라운드로 다시 실행
- bg %번호 → 백그라운드에서 실행 계속
- kill %번호 → 아예 종료
4. 운영체제 vs 개발자 역할
🟢 운영체제(커널)에서 기본 제공하는 것들
A. 시그널 자동 생성 메커니즘
// 커널이 자동으로 생성하는 시그널들
- Ctrl+C 입력 → SIGINT 자동 생성
- Ctrl+Z 입력 → SIGTSTP 자동 생성
- 세그먼테이션 폴트 → SIGSEGV 자동 생성
- 부동소수점 에러 → SIGFPE 자동 생성
- 파이프 broken → SIGPIPE 자동 생성
- 자식 프로세스 종료 → SIGCHLD 자동 생성
- alarm() 타이머 만료 → SIGALRM 자동 생성
B. 기본 시그널 처리 동작 (Default Actions)
// 핸들러를 등록하지 않으면 커널이 기본 동작 수행
SIGINT → 프로세스 종료
SIGTERM → 프로세스 종료
SIGTSTP → 프로세스 일시정지
SIGKILL → 프로세스 강제 종료 (핸들러 등록 불가)
SIGSTOP → 프로세스 일시정지 (핸들러 등록 불가)
SIGCHLD → 무시 (기본값)
SIGPIPE → 프로세스 종료
C. 시스템 콜 제공
// 커널에서 제공하는 시그널 관련 시스템 콜들
sigaction() // 시그널 핸들러 등록
kill() // 다른 프로세스에 시그널 전송
alarm() // 타이머 시그널 설정
pause() // 시그널 대기
sigsuspend() // 시그널 마스크 변경 후 대기
sigreturn() // 시그널 핸들러 복귀 (자동 호출)
🔵 개발자가 직접 구현해야 하는 것들
A. 시그널 핸들러 함수 (사용자 정의)
// 개발자가 직접 작성해야 하는 핸들러 함수
void sig_handler(int signum) {
switch(signum) {
case SIGINT: // Ctrl+C 처리
printf("인터럽트 신호 받음! 정리 작업 중...\n");
// 파일 저장, 리소스 정리 등 애플리케이션별 로직
cleanup_and_exit();
break;
case SIGTSTP: // Ctrl+Z 처리 (보통 기본 동작 사용)
printf("일시정지 요청\n");
break;
case SIGALRM: // 알람 시그널
printf("타이머 만료!\n");
// 주기적 작업, 타임아웃 처리 등
break;
case SIGUSR1: // 사용자 정의 시그널
printf("사용자 정의 신호!\n");
// 애플리케이션별 커스텀 동작
reload_config();
break;
default:
printf("알 수 없는 신호: %d\n", signum);
}
}
내가 헷갈리는 부분들,,ㅜㅜ 매번 정리중
5. 시그널 처리 코드 분석
시그널 핸들러를 구현하는 방법
기본적인 시그널 처리 프로그램
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
// 시그널 핸들러 함수 (개발자가 직접 구현)
void signal_handler(int signal_num) {
switch(signal_num) {
case SIGTERM:
printf("SIGTERM 받음 - 정상 종료합니다\n");
cleanup_resources();
exit(0);
break;
case SIGINT:
printf("SIGINT 받음 - 인터럽트 처리\n");
break;
case SIGUSR1:
printf("SIGUSR1 받음 - 사용자 정의 동작\n");
handle_user_command();
break;
default:
printf("알 수 없는 시그널: %d\n", signal_num);
}
}
int main() {
// sigaction 구조체 준비
struct sigaction sa;
// 시그널 마스크 초기화
sigemptyset(&sa.sa_mask);
// 핸들러 실행 중 블록할 시그널들 설정
sigaddset(&sa.sa_mask, SIGTERM);
sigaddset(&sa.sa_mask, SIGINT);
// 핸들러 함수 등록
sa.sa_handler = signal_handler;
sa.sa_flags = 0;
// 각 시그널에 핸들러 등록
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGUSR1, &sa, NULL);
printf("시그널 대기 중... (Ctrl+C, kill 명령 등으로 테스트)\n");
// 무한 대기 루프
while(1) {
pause(); // 시그널이 올 때까지 대기
}
return 0;
}
시그널 마스크의 이해
sa_mask는 시그널 핸들러가 실행되는 동안 일시적으로 블록될 시그널들을 지정.
이는 재진입 문제를 방지하고 핸들러의 원자성을 보장.
// 시그널 집합 초기화 (개발자 구현)
sigemptyset(&sa.sa_mask);
// 핸들러 실행 중 블록할 시그널들 추가 (개발자 구현)
sigaddset(&sa.sa_mask, SIGTERM);
sigaddset(&sa.sa_mask, SIGINT);
sigaddset(&sa.sa_mask, SIGUSR1);
pause() 함수의 역할
// 개발자가 작성하는 대기 루프
while(1) {
pause(); // 커널에서 제공하는 시스템 콜
}
pause() 함수의 역할 (커널 기능):
- 프로세스를 INTERRUPTIBLE_SLEEP 상태로 전환
- 시그널이 도착하면 깨어남
- 시그널 핸들러 실행 후 다시 pause()로 돌아감
6. 시그널 전달 과정
6.1 시그널 등록 단계 (유저 → 커널)
[개발자 코드] sigaction() 시스템 콜 호출
↓ (커널 처리)
[커널] sys_sigaction() 함수 실행
↓ (커널 처리)
[커널] task_struct의 sighand 구조체에 핸들러 정보 저장
↓ (커널 처리)
[커널] 프로세스의 시그널 테이블 업데이트
6.2 시그널 발생 단계
시그널 생성 요인 (모두 커널에서 자동 처리):
- Ctrl+C → SIGINT
- Ctrl+Z → SIGTSTP
- kill() 시스템 콜 → 지정된 시그널
- alarm() → SIGALRM
- 하드웨어 예외 → SIGSEGV, SIGFPE 등
6.3 시그널 전달 단계 (커널 → 유저)
[커널] 1. 커널이 시그널 생성
↓ (커널 처리)
[커널] 2. 대상 프로세스의 pending 시그널에 추가
↓ (커널 처리)
[커널] 3. 프로세스가 유저 모드로 복귀할 때 시그널 검사
↓ (커널 처리)
[커널] 4. 시그널 핸들러 호출을 위한 스택 조작
↓ (유저 공간으로 전환)
[개발자] 5. 유저 공간의 시그널 핸들러 실행 ← 개발자 코드!
↓ (커널로 복귀)
[커널] 6. sigreturn() 시스템 콜로 원래 컨텍스트 복구
커널 내부 구조 (모두 커널에서 제공)
struct task_struct {
struct sighand_struct *sighand; // 시그널 핸들러 테이블
sigset_t blocked; // 블록된 시그널 마스크
struct sigpending pending; // 대기 중인 시그널
// ...
};
7. 실제 동작 예시
Ctrl+C를 눌렀을 때의 전체 과정
[하드웨어] 1. 키보드 드라이버가 인터럽트 발생
↓ (커널 자동 처리)
[커널] 2. 터미널 드라이버가 SIGINT 시그널 생성
↓ (커널 자동 처리)
[커널] 3. 포그라운드 프로세스 그룹에 시그널 전달
↓ (커널 자동 처리)
[커널] 4. 프로세스의 시그널 pending 큐에 SIGINT 추가
↓ (커널 자동 처리)
[커널] 5. 프로세스가 시스템 콜에서 복귀할 때 시그널 검사
↓ (유저 공간 호출)
[개발자] 6. sig_handler(SIGINT) 실행 ← 개발자가 작성한 코드!
웹서버에서의 실제 시그널 처리 예시
void graceful_shutdown(int sig) {
switch(sig) {
case SIGTERM: // 정상 종료 요청
printf("서버 종료 요청 받음\n");
stop_accepting_connections(); // 새 연결 차단
wait_for_active_connections(); // 기존 연결 완료 대기
cleanup_resources(); // 리소스 정리
exit(0);
break;
case SIGINT: // Ctrl+C
printf("강제 종료 요청\n");
cleanup_resources();
exit(1);
break;
case SIGUSR1: // 설정 재로드
printf("설정 파일 재로드\n");
reload_configuration();
break;
case SIGCHLD: // 자식 프로세스 종료
wait_for_children(); // 좀비 프로세스 방지
break;
}
}
int main() {
// 시그널 핸들러 등록
struct sigaction sa;
sa.sa_handler = graceful_shutdown;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGUSR1, &sa, NULL);
sigaction(SIGCHLD, &sa, NULL);
// 서버 실행
run_server();
return 0;
}
8. 마무리
핵심 요약
Ctrl+C vs Ctrl+Z
- Ctrl+C: "종료해라" → SIGINT 보내서 프로세스 종료
- Ctrl+Z: "멈춰라" → SIGTSTP 보내서 일시정지, 나중에 이어서 실행 가능
역할
- 커널 제공: 시그널 생성, 전달, 기본 처리, 인프라
- 개발자 구현: 시그널을 받았을 때의 구체적인 반응과 로직
시그널의 장점
- 비동기적 이벤트 처리가 가능
- 안전한 프로그램 종료 구현
- 프로세스 간 간단한 통신
- 시스템 리소스 정리 및 상태 저장
정리
- SIGTERM 핸들러는 필수: 서버 프로그램에서는 정상 종료 로직 구현
- SIGCHLD 처리: 멀티프로세스 프로그램에서 좀비 프로세스 방지
- 시그널 마스킹 활용: 중요한 작업 중 인터럽트 방지
- SIGUSR1, SIGUSR2 활용: 애플리케이션별 커스텀 신호로 활용
🔗 관련 명령어 참고
# 프로세스 상태 확인
ps aux | grep [프로세스명]
# 백그라운드 작업 확인
jobs
# 시그널 전송
kill -SIGTERM [PID]
kill -SIGUSR1 [PID]
# 포그라운드로 가져오기
fg %1
# 백그라운드로 보내기
bg %1
'system' 카테고리의 다른 글
[리눅스 프로그래밍 · DevOps] GitHub Actions로 C Signal Handling Demo CI/CD 구축기 (0) | 2025.09.18 |
---|---|
🛡️SGI 서울보증 랜섬웨어 사건 정리 & 리눅스 백신 실습 (1) | 2025.09.16 |
[시스템 프로그래밍] myshell.c 코드 분석 - 2 (1) | 2025.09.12 |
[시스템 프로그래밍] myshell.c 코드 분석 - 1 (1) | 2025.09.11 |
[시스템 프로그래밍] Tiny Shell 프로젝트: 잡 컨트롤, 시그널, 레이스 컨디션 다루기 (0) | 2025.09.09 |