
계속 중복되는 내용이 있지만.. 수업 진도에 따라 겹치는 부분이 있어도 한번 씩 더 정리중입니다.
프로세스에게 보내는 긴급 문자 메시지(비동기 알림)으로, 받으면 하던 일을 멈추고 즉시 확인(처리)해야 합니다.
Ctrl+C를 누름).Ctrl+C, kill 명령어, 0으로 나누기 연산 등).개발자는 다음 3가지 중 하나를 선택하여 시그널에 대응합니다.
| 처리 방식 | 매크로 / 함수 | 설명 | 비고 |
|---|---|---|---|
| 무시 (Ignore) | SIG_IGN | "그냥 무시해." 아무 일도 일어나지 않음. | SIGKILL, SIGSTOP은 무시 불가. (관리자의 통제권 보장) |
| 기본 동작 (Default) | SIG_DFL | 커널이 정한 기본 행동 수행. | 대부분 종료(Terminate) 또는 코어 덤프(Core Dump). |
| 포착 (Catch) | handler_func | "내가 처리할게." 시그널 핸들러 함수 실행. | 실행 중인 코드를 멈추고 핸들러로 점프 → 수행 후 복귀. |
일반 함수 호출과 다르게, 커널이 강제로 실행 흐름을 바꿉니다.
i = i + 1; 수행 중.signal_handler() 함수로 강제 점프.return), 저장해둔 위치(i = i + 1 다음)로 복귀하여 계속 실행.| 이름 | 번호 | 기본 동작 | 발생 상황 |
|---|---|---|---|
| SIGINT | 2 | 종료 | 키보드 Ctrl + C 입력 시. |
| SIGQUIT | 3 | 코어 덤프 | 키보드 Ctrl + \ 입력 시. |
| SIGKILL | 9 | 강제 종료 | kill -9. (포착, 무시, 블록 불가) |
| SIGSEGV | 11 | 코어 덤프 | 잘못된 메모리 접근 (Segmentation Fault). |
| SIGTERM | 15 | 종료 | kill 명령의 기본값. (정상 종료 요청). |
| SIGSTOP | 19 | 정지 | 실행 일시 정지. (포착, 무시, 블록 불가) |
| SIGCHLD | 17 | 무시 | 자식 프로세스가 종료되거나 멈춤. |
+ 추가)

signal 함수 사용)#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
// 시그널 핸들러 함수 (User defined)
void my_handler(int signo) {
printf("\n[Signal] %d번 시그널을 받았습니다! 죽지 않아요.\n", signo);
// 보통 여기서 자원 정리나 플래그 설정을 함
}
int main() {
// 1. 핸들러 등록 (SIGINT 발생 시 my_handler 실행)
if (signal(SIGINT, my_handler) == SIG_ERR) {
perror("signal error");
exit(1);
}
// 2. SIGQUIT는 무시하도록 설정 (Ctrl + \)
signal(SIGQUIT, SIG_IGN);
printf("Running... (Ctrl+C를 눌러보세요. Ctrl+\\는 무시됩니다.)\n");
while (1) {
printf(".");
fflush(stdout);
sleep(1);
}
return 0;
}

종료를 위해선 다른 터미널을 열어 아래 명령어 입력
ps aux | grep signal_1
kill -9 [Process ID]
시그널 핸들러는 비동기적으로 호출되므로, 핸들러 내부에서 printf, malloc 같은 일반 라이브러리 함수를 쓰는 것은 위험합니다. (이 함수들이 실행되는 도중에 시그널이 와서 또 호출하면 꼬일 수 있음).
원칙적으로는, 핸들러 내부에서는 전역 변수(플래그)만 1로 세팅하고 최대한 빨리 빠져나오는 것이 정석입니다.
핸들러는
깃발(Flag)만 올리고, 뒤처리는메인 루프가 담당한다.
시그널 핸들러 내부에서 printf 같은 무거운 함수를 제거하고, volatile sig_atomic_t 타입을 사용하여 비동기 신호 안전(Async-Signal-Safe) 원칙을 지키는 코드로 재작성된 코드입니다.
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
/* * [핵심 1] volatile sig_atomic_t
* - volatile: 컴파일러가 이 변수를 최적화(캐싱)하지 못하게 막음 (언제든 값이 변할 수 있음을 알림).
* - sig_atomic_t: CPU가 한 번의 명령으로 읽고 쓸 수 있음을 보장하는 정수 타입 (쪼개지지 않음).
*/
volatile sig_atomic_t g_exit_flag = 0;
// 시그널 핸들러: 최대한 짧고 단순하게!
void my_handler(int signo) {
// 여기서는 복잡한 작업(printf, malloc 등) 금지!
// 오직 플래그 값만 변경하고 즉시 리턴함.
g_exit_flag = 1;
}
int main() {
// 핸들러 등록
if (signal(SIGINT, my_handler) == SIG_ERR) {
perror("signal error");
exit(1);
}
printf("프로그램 실행 중... (Ctrl+C를 누르면 안전하게 종료합니다)\n");
while (1) {
// [핵심 2] 메인 루프에서 플래그 검사
if (g_exit_flag == 1) {
printf("\n[Main] 종료 플래그 감지! 자원을 정리하고 종료합니다.\n");
break; // 루프 탈출
}
// 평소 작업 수행
printf(".");
fflush(stdout);
// sleep 중에 시그널이 오면 sleep은 즉시 깨어나고(잔여 시간 반환),
// 핸들러 실행 후 다음 라인으로 넘어감.
sleep(1);
}
printf("Bye Bye!\n");
return 0;
}
printf 제거 이유 (Reentrancy)printf를 호출해서 내부적으로 락(Lock)을 걸고 있는데, 시그널이 발생해서 핸들러가 또 printf를 호출하면?volatile 키워드while(1) 내부에서 g_exit_flag를 건드리는 코드가 없으면, 성능을 위해 이 값을 레지스터에 캐싱(저장)해버립니다.1로 바꿔도, CPU는 레지스터의 0만 계속 보고 있어서 루프가 안 끝나는 버그가 발생합니다. volatile은 "캐싱하지 말고 무조건 메모리에서 다시 읽어!"라고 지시합니다.sig_atomic_t 타입int가 32비트인데 8비트 CPU에서 돌린다면? 값을 쓰는 도중(상위 16비트 쓰고 하위 16비트 쓰려는 찰나)에 읽어가면 엉뚱한 값이 될 수 있습니다."시그널 핸들러에서 printf 쓰면 위험하다"는 것을 증명하는 교착 상태(Deadlock) 타이밍이 맞으면 프로그램이 그대로 멈춰버립니다.
이 코드는 printf가 내부적으로 사용하는 락(Lock) 때문에 죽습니다.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h> // 뮤텍스 사용
// 1. 자원을 보호하는 자물쇠 (Mutex)
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void handler(int signo) {
printf("[Handler] 시그널 수신! 락 획득 시도 중...\n");
// 3. 여기서 멈춤 (DEADLOCK)
// Main이 이미 락을 가지고 있는데, Handler가 끝나야 Main이 락을 품.
// 하지만 Handler는 락을 얻어야 끝남. -> 무한 대기
pthread_mutex_lock(&lock);
printf("[Handler] 락 획득 성공! (이 메시지는 절대 안 보임)\n");
pthread_mutex_unlock(&lock);
}
int main() {
signal(SIGINT, handler);
printf("[Main] 시작: 락을 겁니다.\n");
// 1. 메인이 락을 잠금
pthread_mutex_lock(&lock);
printf("[Main] 락 획득함. 이제 시그널을 스스로 보냄 (자살골).\n");
// 2. 락을 쥔 상태에서 시그널 발생 (Ctrl+C를 코드로 누름)
// 이 순간 하던 일을 멈추고 handler로 점프함
raise(SIGINT);
// 4. 핸들러가 끝나야 여기로 돌아오는데, 핸들러가 멈춰서 못 돌아옴.
printf("[Main] 락 해제 (이 메시지도 절대 안 보임)\n");
pthread_mutex_unlock(&lock);
return 0;
}

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// 핸들러에서 메모리 할당 시도 (절대 금지 행위)
void handler(int signo) {
// 여기서 malloc이 메인이 잡고 있는 '힙 락'을 기다리다 멈춤
void *ptr = malloc(1024);
free(ptr);
}
int main() {
signal(SIGINT, handler);
printf("Running... (이 코드는 곧 멈춥니다)\n");
// 자식 프로세스가 시그널 난사
pid_t pid = getpid();
if (fork() == 0) {
while(1) {
kill(pid, SIGINT);
usleep(100); // 0.1ms 간격 폭격
}
}
// 메인 루프: 쉴 새 없이 메모리 할당/해제 반복 (락을 계속 잡았다 풀었다 함)
while(1) {
void *p = malloc(1024);
// [데드락 포인트]
// malloc이 내부 락을 잡고, 아직 리턴하기 전에 시그널이 오면 -> 사망
free(p);
}
return 0;
}
위코드는 데드락에 안들어 갑니다. 확률적인 이유로 시그널 핸들러 안에서는 절대 뮤텍스(Lock)를 쓰거나, 락을 쓰는 함수(printf, malloc 등)를 호출하면 안 됩니다.
시그널 핸들러 안에서는 락을 쓰지 않는 Async-Signal-Safe 함수만 써야 합니다. printf 대신 write를 써야합니다.
// 안전한 버전
void handler(int signo) {
char *msg = "Handler: Safe Write!\n";
write(STDOUT_FILENO, msg, strlen(msg)); // 락을 안 씀
}
"여러 시그널을 비트 마스크(sigset_t)로 묶어, 특정 구간에서 수신을 잠시 막거나(Block) 푸는(Unblock) 기술.
sigset_t)리눅스는 64개의 시그널을 다루기 위해 unsigned long 배열 비트 마스크인 sigset_t 타입을 사용합니다. 직접 비트 연산을 하지 않고 전용 매크로 함수를 사용합니다.
sigemptyset(sigset_t *set): 집합 비우기 (모든 비트 0).sigfillset(sigset_t *set): 모든 시그널 포함 (모든 비트 1).sigaddset(sigset_t *set, int signum): 특정 시그널 추가.sigdelset(sigset_t *set, int signum): 특정 시그널 제거.sigismember(sigset_t *set, int signum): 포함 여부 확인 (True/False).시그널 집합을 커널에 등록하여 "이 구간에서는 이 시그널들을 잠시 보류해줘"라고 요청하는 것입니다.
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
| how 옵션 | 동작 설명 | 비고 |
|---|---|---|
SIG_BLOCK | 현재 마스크 + set (합집합) | 기존 차단 목록에 추가 |
SIG_UNBLOCK | 현재 마스크 - set (차집합) | 차단 목록에서 해제 |
SIG_SETMASK | 현재 마스크 = set (대입) | 아예 새 목록으로 덮어쓰기 |
중요한 데이터를 쓰고 있을 때 SIGINT(Ctrl+C)가 들어와도 즉시 종료되지 않고, 작업이 끝난 후에 처리되도록 합니다.
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main() {
sigset_t new_set, old_set;
// 1. 집합 초기화 및 SIGINT(2번) 추가
sigemptyset(&new_set);
sigaddset(&new_set, SIGINT);
printf("[Start] 중요 작업 시작 전 (Ctrl+C 가능)\n");
sleep(3);
// 2. 블록 설정 (이제부터 SIGINT는 Pending 큐에 쌓이고 전달 안 됨)
// old_set에 이전 상태를 백업해둠
sigprocmask(SIG_BLOCK, &new_set, &old_set);
printf("\n[Critical Section] 중요 데이터 기록 중... (Ctrl+C 눌러도 안 죽음)\n");
for(int i=0; i<5; i++) {
printf("Writing data... %d/5\n", i+1);
sleep(1);
}
printf("[Critical Section] 완료.\n");
// 3. 블록 해제 (백업해둔 상태로 복구)
// 이 시점에 아까 눌렀던 Ctrl+C가 있다면 즉시 배달되어 프로세스 종료됨
sigprocmask(SIG_SETMASK, &old_set, NULL);
printf("\n[End] 작업 끝. (여기까지 출력되면 Ctrl+C 안 누른 것)\n");
sleep(3);
return 0;
}

Ctrl+C를 누름.Writing data...를 끝까지 출력함.sigprocmask 복구 직후), 아까 참았던 SIGINT가 도착하여 즉시 종료됨. (마지막 [End] 메시지는 출력 안 됨).시그널이 64 기존 sig 8bit 구분해서 프린트하게, 왼쪽이 낮은 비트
예) 00000000 00100000 00000000 00000000 00000000 00000000 00000000 00000000
#define _POSIX_C_SOURCE 200809L //IntelliSense (코드 분석기)의 오탐으로 인해 추가
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void printf_SIG(sigset_t sig){
int count=1;
for (int i = 1; i <= 64; i++)
{
if(sigismember(&sig, i)==1){
printf("1");
}else printf("0");
if((count%8)==0)printf(" ");
count++;
}
printf("\n");
}
int main() {
sigset_t new_set, old_set;
// 1. 집합 초기화 및 SIGINT(2번) 추가
printf_SIG(old_set);
sigemptyset(&new_set);
sigaddset(&new_set, SIGINT);
printf_SIG(new_set);
printf("[Start] 중요 작업 시작 전 (Ctrl+C 가능)\n");
sleep(3);
printf("[Start] 마스킹처리됨 (Ctrl+C 안됨)\n");
sigprocmask(0, NULL,&old_set);
printf_SIG(old_set);
// 2. 블록 설정 (이제부터 SIGINT는 Pending 큐에 쌓이고 전달 안 됨)
// old_set에 이전 상태를 백업해둠
sigprocmask(SIG_BLOCK, &new_set, &old_set); //old_set => 0
printf_SIG(old_set);
sigprocmask(0, NULL,&old_set); //=> 1
printf_SIG(old_set);
printf("\n[Critical Section] 중요 데이터 기록 중... (Ctrl+C 눌러도 안 죽음)\n");
for(int i=0; i<5; i++) {
printf("Writing data... %d/5\n", i+1);
sleep(1);
}
printf("[Critical Section] 완료.\n");
// 3. 블록 해제 (백업해둔 상태로 복구)
// 이 시점에 아까 눌렀던 Ctrl+C가 있다면 즉시 배달되어 프로세스 종료됨
sigprocmask(SIG_SETMASK, &old_set, NULL);
printf_SIG(old_set);
printf("\n[End] 작업 끝. (여기까지 출력되면 Ctrl+C 안 누른 것)\n");
sleep(3);
return 0;
}
위 코드 한번 더 정리할 것
#define _POSIX_C_SOURCE 200809L //IntelliSense (코드 분석기)의 오탐으로 인해 추가
#include <stdio.h> // printf()
#include <stdlib.h> // exit()
#include <unistd.h> // sleep(), getpid()
#include <signal.h> // sigaction, siginfo_t, SI_USER, SI_KERNEL
#include <string.h>
void detailed_handler(int sig, siginfo_t *info, void *ucontext) {
printf("\n[Signal Received] 번호: %d\n", sig);
// 1. 누가 보냈는가?
printf(" - 보낸 프로세스 PID: %d\n", info->si_pid);
printf(" - 보낸 사용자 UID: %u\n", info->si_uid);
// 2. 왜 보냈는가? (si_code 분석)
printf(" - 발생 원인 코드: %d ", info->si_code);
if (info->si_code == SI_USER) {
printf("(사용자가 kill이나 raise로 직접 전송)\n");
} else if (info->si_code == SI_KERNEL) {
printf("(커널에서 전송)\n");
} else {
printf("(기타 사유)\n");
}
}
int main() {
struct sigaction sa;
// 구조체 초기화
sa.sa_sigaction = detailed_handler; // 상세 정보를 받는 핸들러 연결
sigemptyset(&sa.sa_mask); // 핸들러 실행 중 블록할 시그널 없음
sa.sa_flags = SA_SIGINFO; // 상세 정보 사용 플래그 설정
// SIGINT(Ctrl+C)에 대해 sigaction 설정
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}
printf("현재 프로세스 PID: %d\n", getpid());
printf("Ctrl+C를 누르거나, 다른 터미널에서 'kill -2 %d'를 입력하세요.\n", getpid());
// 시그널 대기
while (1) {
sleep(1);
}
return 0;
}

썸넬 reference : https://blockdmask.tistory.com/23
블로그 다른 글들을 훑어보았는데 굉장히 열정적이시네요!
열정적인 모습 배워가겠습니다.
또한 소개글의 목표도 저와 비슷하여 공감이 많이 가네요~
로우 레벨에 대해 함께 고민하기 위해 종종 찾아오겠습니다 :)