시그널이란?
시그널은 프로세스에게 특정한 이벤트가 발생했음을 알려주는 비동기 알림(Asynchronous Notification)이다. 커널이나 다른 프로세스에서 특정 프로세스에게 신호를 보낼 수 있다. 이를 통해 프로세스의 정상적인 실행 흐름을 중단하고 잠시 시그널에 대한 처리를 하도록 할 수 있다.
ㅤ
이게 왜 필요하나면
프로세스가 뭘 하고있든 상관없이 실행 중간에 특정 코드를 처리하도록 만들 수 있다. 무한루프에 빠져있던 프로세스를 멈추기 위해서 시그널을 보내 잠시 무한루프를 빠져나와 프로세스를 종료하는 코드를 실행하도록 하는 등에 사용할 수 있다.
ㅤ
시그널의 전달과 처리 과정
시그널에서 유의할 점은 비동기 라는 것. 시그널의 발생과 함께 핸들러가 무조건 바로 즉시 호출되지는 않는다.
ㅤ
1. 시그널 발생
시그널은 여러 요인에 의해 만들어질 수 있다. 시그널이 발생하면 이게 커널에게 전달되어서 “타겟 프로세스에게 시그널을 전달해야겠다”라고 기록한다.
- HW 이벤트 : Ctrl+C 나 메모리에 잘못 접근하는 등
- SW 이벤트 : 명령어나 함수 호출 시
kill()시스템콜 호출 등 - 커널 이벤트 : 타이머 만료, 자식 프로세스 종료 등
ㅤ
2. 시그널 대기 (Pending)
생성된 시그널은 커널의 명령에 따라 타겟 프로세스의 Pending 시그널 목록에 기록된다. 코드 상에서는 PCB를 표현한 task_struct 구조체에서 확인할 수 있었다. 커널은 이 pending 에다가 현재 처리가 필요하다고 신호를 남긴다.

ㅤ
시그널은 “비동기” 알림이기 때문에, 이걸 하는 동안에 프로세스는 아직 자기 자신의 코드를 실행을 처리하고 있다.
ㅤ
3. 시그널 전달 시점
그렇다면 이 시그널을 처리하는 시점은 언제인가? 정확히는, 프로세스가 이 Pending 중인 시그널에 대한 정보를 확인하는 타이밍은 언제일까?
ㅤ
프로세스가 커널 모드에서 사용자 모드로 돌아가는 시점에 Pending 중인 시그널을 확인하고, 시그널이 있으면 원래 위치가 아니라 시그널의 핸들러 위치로 이동한다.
- 코드를 실행하다가 시스템콜 요청 → 커널에서 요청 처리 → 시스템콜 처리가 끝나고 돌아올 때 시그널 확인 후 처리
- 인터럽트 핸들러 종료 후 사용자 모드로 돌아갈 때 → 시그널 확인 후 처리
- 타이머 틱 발생으로 Context-Switch 가 발생 → 실행 프로세스 전환 후 사용자 모드로 들어갈 때 → 시그널 확인 후 처리
ㅤ
시그널 전달 → 감지하면 바로 실행 이 아니라, 시그널 전달 → 다음 커널모드 전환 후 복귀 시 처리 였다.
타이밍이 중요하지 않은 Task 처리를 해달라고 요청하는 상황이면 괜찮을 것 같은데, 즉시 신호 전달이 필요한 경우에는 확실히 시그널을 쓸 수 없을 것 같음. (ex 메시지 전달이나 공유자원 점유를 위한 신호)
아, 그래서 자식 프로세스의 자원 정리를 부모 프로세스에게 시그널로 전달하는데 이게 즉시 처리되는게 아님 + 그래서 자식은 Zombie 상태가 된다는거구나.
ㅤ
4. 시그널의 처리
- 프로세스가 커널 모드 → 사용자 모드로 복귀할 때 현재 Pending 중인 시그널을 검사
- 대기중인 시그널을 발견하면 시그널 핸들러의 실행을 준비
- 사용자 모드로 전환
- 시그널 핸들러 함수 실행
- 핸들러 종료 시 원래 위치로 복귀
- 원래 코드 실행
ㅤ
5. 스택 조작 (CPU 레벨에서의 Context Switching)
시그널 핸들러를 실행하기 위해서 어떤 준비를 해야할까. 커널모드의 처리를 완료하고 사용자 모드로 복귀하려고 하는 시점에 프로세스가 가지고 있는 정보는 원래 코드로 돌아가기 위한 정보들이다. 여기에서 추가로 시그널 핸들러를 실행하러 이동해야 하기 때문에, 원래 코드 실행에 대한 정보들은 안전하게 저장해주어야 한다.
ㅤ
사용자 모드로 돌아오는 중에 Pending 중인 시그널을 발견했다면
- 현재 CPU 레지스터를 저장
- 스택에 기존 컨텍스트를 저장
- 시그널 핸들러 주소를 PC에 로드
→ 핸들러를 실행하러 이동
ㅤ
6. 프로세스 복귀
시그널 핸들러가 끝나면 커널에서 sigreturn 을 호출한다. (약간 EXC_RETURN을 LR에 놓고 bx lr 하는 느낌인 것 같다) 그러면 스택에서 레지스터, PC, CPU Status 같은 정보들을 복구하고, 원래 코드 위치로 돌아와 실행을 재개한다.
ㅤ
혹시 시그널이 인터럽트인가?
시그널 처리에 대한 내용을 확실하게 찾아보고 싶어서 실습중인 보드인 라즈베리파이의 CPU 아키텍처인 Cortex-A72 문서를 한 번 확인해보려고 했다. 그런데 여기에서는 SIGINT 같은 키워드를 볼 수 없었다. 근데 그게 당연했음. 여기에서 다루는 시그널은 리눅스(POSIX)가 가지고 있는 OS 레벨의 신호이고, 이건 프로세서 아키텍처와는 전혀 무관한 내용이다.
ㅤ
즉, 시그널은 HW 계층 또는 아키텍처 계층에서 다루는 Interrupt나 Exception 과는 전혀 무관하고, 리눅스 내에서 프로세스끼리 주고받는 메시지(신호) 정도로만 생각하면 될 것 같다. 시그널은 Linux 위에서만 의미가 있다.
ㅤ
실행중이던 프로세스가 있을 때 터미널에 Ctrl+C 를 입력해서 kill 명령을 날린다고 해보자. 그럼 다음의 과정을 거친다.
- 키보드로 Ctrl + C 입력
- 키보드 HW가 신호를 만들어서 던진다
- 이때 GIC (Generic Interrupt Controller) 에게 인터럽트 신호를 전달 (NVIC 같은거인 듯)
- GIC는 CPU에게 이 신호를 전달한다.
- CPU의 인터럽트 처리 (아키텍처 레벨)
- CPU가 현재 실행을 잠시 중단하고
- 아키텍처 레벨에서 정의된 동작을 수행 (레지스터 저장, 모드 전환 등 처리) 이후에
- IRQ Handler로 점프한다
- 커널의 인터럽트 핸들러 (OS 레벨)
- Linux 커널에서 IRQ Handler를 실행
- 인터럽트 종류에 따른 입력 확인 및 발생시킬 Signal 을 판단 (SIGINT)
- 시그널을 받을 타겟 프로세스 찾기 (Foreground에 있는 프로세스 확인)
- 시그널 생성 (OS 레벨)
- 타겟 프로세스에게 발생한 시그널을 Pending Signal로 등록
- 시그널 전달 (OS → 사용자 모드)
- 프로세스가 커널 모드에서 사용자 모드로 돌아가려고 할 때 이 Pending Signal을 발견
- Signal Handler 코드로 점프
- 프로세스 시그널 핸들러 (App 레벨)
- 사용자가 정의한 Signal Handler 코드를 실행
ㅤ
ㅤ
시그널 종류
확인해보니 /usr/include/arm-linux-gnueabihf/bits/signum_generic.h 와 signum_arch.h 두개의 파일에 들어있다. 명령어로 바로 뽑아볼 수도 있다. 이게 더 확실할 듯.

ㅤ
여기 중에서 활용도가 높은 (자주 쓰는) 시그널들을 위주로 뽑아봤다. ㅤ
| 신호 | 번호 | 발생 원인 | 기본 동작 | 핸들러 정의 | 활용도 |
|---|---|---|---|---|---|
| SIGINT | 2 | Ctrl+C 입력 | 종료 | 가능 | ⭐⭐⭐⭐⭐ |
| SIGTERM | 15 | kill 명령어 (기본값) | 종료 | 가능 | ⭐⭐⭐⭐⭐ |
| SIGKILL | 9 | kill -9 (강제 종료) | 즉시 종료 | 불가능 | ⭐⭐⭐⭐ |
| SIGCHLD | 17 | 자식 프로세스 상태 변화 | 무시 | 가능 | ⭐⭐⭐⭐ |
| SIGSEGV | 11 | 잘못된 메모리 접근 | 종료 + core dump | 가능 | ⭐⭐⭐ |
| SIGALRM | 14 | alarm() 타이머 만료 | 종료 | 가능 | ⭐⭐⭐ |
| SIGUSR1 | 10 | 사용자 정의 신호 1 | 종료 | 가능 | ⭐⭐⭐ |
| SIGUSR2 | 12 | 사용자 정의 신호 2 | 종료 | 가능 | ⭐⭐⭐ |
ㅤㅤ
시그널 핸들러
시그널 핸들러는 함수의 형태가 정해져있다. 내가 쓰고싶은 모양으로 아무렇게나 만들면 안되고, 규칙을 지켜서 민들어줘야 한다. 왜나면 이건 커널이 자동으로 호출하겠다고 정해두었기 때문!
void handler_function(int sig) {
// sig: 받은 신호의 번호
}
- 반환 타입은 void
- 파라미터로는 int 를 하나 받음
ㅤ
이 시그널 핸들러를 코드에 아무렇게나 적어둔다고 해서 시그널이 발생했을 때 이 함수를 호출해주지는 않는다. 특정 시그널이 들어왔을 때 이 함수를 호출해달라고 등록해야하며, 이것이 signal() 함수가 하는 역할이다.
// 프로세스에게 SIGINT 시그널이 발생하면 -> sigint_handler 함수를 실행하기
signal(SIGINT, sigint_handler);
ㅤ
만약 이렇게 시그널에 대해서 핸들러를 등록해주지 않았다면 어떻게 처리가 되나? 에 대해서는 리눅스 소스코드에서 이미 정의가 되어있다. 궁금하다면 이 곳을 살펴보자.

ㅤ
실습해보자!
SIGINT 받아보기
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 신호 핸들러 함수
void sigint_handler(int sig) {
printf("SIGINT received! (signal number: %d)\n", sig);
}
int main() {
// SIGINT에 대한 핸들러 등록
signal(SIGINT, sigint_handler);
printf("Press Ctrl+C to trigger SIGINT\n");
// 무한 루프 (신호를 받을 때까지 대기)
while(1) {
sleep(1);
}
return 0;
}
위 코드에서는 Ctrl+C로 입력되는 SIGINT 시그널에 대한 핸들러로 sigint_handler를 등록해 사용하고 있다.
ㅤ

덕분에, 프로세스 무한루프 동안 Ctrl + C 를 입력하면 SIGINT 가 프로세스에게 전달되고, 이때 프로세스는 sigint_handler를 호출하면서 메시지를 콘솔에 출력한다. handler를 등록하지 않았을 때의 동작이 terminate 였지만, 이 핸들러를 등록해주면서 Ctrl + C 를 입력하더라도 프로그램이 종료되지 않는 상태가 되었다. 그래서 핸들러 코드 수행 후 다시 main의 무한루프로 돌아가게된다.
ㅤ
SIGINT 받고 함수 종료시키기
실제 임베디드에서는 좀 더 유려하게 함수를 종료시켜줄 필요가 있다. 아래처럼 시그널을 받으면 반복을 종료하고 할당받았던 자원들을 정리하면서 프로그램을 종료하는 코드를 작성하면 더 좋다.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
// 전역 플래그
volatile int shutdown_flag = 0;
void sigint_handler(int sig) {
printf("\nSIGINT received! Shutting down...\n");
shutdown_flag = 1;
}
int main() {
signal(SIGINT, sigint_handler);
printf("Press Ctrl+C to shutdown\n");
while(!shutdown_flag) {
printf("Running...\n");
sleep(1);
}
printf("Cleanup and exit\n");
return 0;
}

ㅤ
Pending 된다며!!!
그 다음으로는 Pending 에 대해 궁금해서 간단히 실험해봤다. Task가 Sleep에 들어가있는 동안은 다시 Task가 올라오지 않을테니 Context Switch가 발생하지 않을거고, 그러면 시그널을 받더라도 Pending 상태로 가지고 있다가 Sleep 에서 깨어나는 시점에 이 시그널을 처리할 것이라 예상했다. 이 가정을 확인하기 위해서 코드를 작성해줬다.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
// 전역 플래그
volatile int shutdown_flag = 0;
void sigint_handler(int sig) {
printf("\nSIGINT received! Shutting down...\n");
shutdown_flag = 1;
}
int main() {
signal(SIGINT, sigint_handler);
printf("Press Ctrl+C to shutdown\n");
while(!shutdown_flag) {
printf("Before sleep: %ld\n", time(NULL));
sleep(5);
printf("After sleep: %ld\n", time(NULL));
}
printf("Cleanup and exit\n");
return 0;
}
ㅤ
근데 실제로 코드를 실행해봤을 때에는 Sleep 중인데에도 신호를 받으니 바로 반복문이 종료되어버렸다.

이건 버그가 아니라 기능이였다. sleep(5) 함수는 1) 5초가 지나거나 2) 신호가 들어오면 깨어나는 함수이기 때문. 실제로 sleep이 타이머로 깨어났는지 아니면 신호로 깨어났는지 반환값으로 확인할 수 있다.
ㅤ
// 테스트를 위해 sleep 쪽 함수를 약간 수정해주기
printf("Sleep Well : %d\n", sleep(5));
인터럽트로 깨어난 타이밍에는 sleep()의 반환값이 1로 나오는 것을 볼 수 있다.

ㅤ
그리고 여기에서 잘 보면 sigint_handler 의 출력이 sleep의 출력보다 더 먼저 나온 것을 볼 수 있다. 이것은 Pending Signal의 처리 순서의 영향이다.
sleep()으로 잠들어서 타이머 만료 또는 신호를 기다림 (Blocked 상태)- 이때 HW interrupt → 커널이 Pending Signal에
SIGINT기록 - 신호가 들어와서 프로세스는 Ready 상태가 되어 실행을 기다림
- 스케줄러에게 선택받아 실행을 위해 사용자모드로 전환하려는 타이밍에 Pending Signal 확인
- 여기에서
SIGINT가 있으니sigint_handler실행 (출력) - 실행 후 원래 위치 (sleep 중)으로 돌아와서 신호에 의해 깨어났음의 의미로 1 반환 (출력)
- 반복 조건 만족하지 않아 탈출 후 프로세스 종료
ㅤ
나는 한 30분 동안
sleep()이 왜 5초가 지나지도 않았는데 커널 → 사용자로 전환하면서 Pending Signal을 확인하는거지??? 라고 생각하고 있었다. 그런데sleep()이 시간에만 반응하는게 아니라 시그널에도 반응하는 녀석이라 그런거였다. sleep → Blocked 상태에 들어가는데, 여기에서 Ready로 올라가는 조건이 5초가 지나거나 시그널을 받거나 이기 때문.
ㅤ
사실 sigaction이 더 실무적이다
앞서 시그널 핸들러를 등록해줄 때 signal() 이라는 함수를 사용했었다. 그런데 요 녀석을 구형 API 이기도 하고, 플랫폼마다 동작이 달라서 이식성도 낮다. 그래서 실무적으로는 잘 사용되지 않는다고 한다. 여기 스택오버플로우 글에서도 둘 중에 뭘 써야하는지 논쟁을 하는게 아니라 sigaction을 쓰는게 맞다! 라고 강조하고있다.

signal()은 시그널 처리하는동안 다른 시그널의 발생과 Handler 호출을 막지 않는다.signal()시에 핸들러가 호출되고나면 핸들러 등록이 초기화되어 다시 핸들러를 등록하는 과정을 내부적으로 거친다. 만약 초기화되고 다시 등록하는 틈새에 시그널이 들어온다면 → 의도하지 않은 방식으로 동작할 수 있다.signal()은 System V / BSD(POSIX) 에서 동작이 다를 수 있기 때문에, 이식성이 좋지 않다.
이런 단점들 때문에 sigaction을 사용하는게 맞다고 한다. 위 글에 따르면 공식문서에도 제발 sigaction 쓰라고 나와있다고 한다.
ㅤ
signal은 그러면 하위호환성 때문에 못없애고 있는 레거시 코드인가보오
ㅤ
실습을 해보고자 클로드에게 요청해서 다음의 코드를 받았다. 이게 실무적인 방법을 담은 코드라고 하는데, 나였다면 signal handler 코드를 3개의 함수로 따로 만들었을 것 같긴 한데, 이 방식으로 해보는게 더 나은 방법인지는 아직 판단이 서지 않는다. 그러나, signal 보다 sigaction이 더 좋다는건 느껴지는게, 여러 확장 옵션들을 설정할 수 있는 것만 봐도 조금 더 유용해보이긴 한다.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
volatile int shutdown_flag = 0;
volatile int reload_flag = 0;
void signal_handler(int sig) {
// 공통적으로 실행할 코드
if (sig == SIGINT) {
printf("\n[SIGINT] User interrupt detected\n");
shutdown_flag = 1;
}
else if (sig == SIGTERM) {
printf("\n[SIGTERM] Termination signal received\n");
shutdown_flag = 1;
}
else if (sig == SIGUSR1) {
printf("\n[SIGUSR1] Reload signal received\n");
reload_flag = 1;
}
}
int setup_signal_handlers(void) {
struct sigaction sa;
// sigaction 구조체 초기화
memset(&sa, 0, sizeof(sa));
// 핸들러 함수 지정
sa.sa_handler = signal_handler;
// 핸들러 실행 중 다른 신호 차단하지 않음 설정
sigemptyset(&sa.sa_mask);
// SA_RESTART: 신호로 중단된 시스템 콜 자동 재개 설정
sa.sa_flags = SA_RESTART;
// SIGINT 등록
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction SIGINT");
return -1;
}
// SIGTERM 등록
if (sigaction(SIGTERM, &sa, NULL) == -1) {
perror("sigaction SIGTERM");
return -1;
}
// SIGUSR1 등록
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction SIGUSR1");
return -1;
}
return 0;
}
void reload_config(void) {
printf("[CONFIG] Reloading configuration...\n");
reload_flag = 0;
}
int main(void) {
if (setup_signal_handlers() == -1) {
fprintf(stderr, "Failed to setup signal handlers\n");
return 1;
}
printf("PID: %d\n", getpid());
printf("Press Ctrl+C to shutdown\n");
printf("Or use: kill -TERM %d\n", getpid());
printf("Or use: kill -USR1 %d to reload\n\n", getpid());
while (!shutdown_flag) {
if (reload_flag) {
reload_config();
}
printf("[RUNNING] Working... (PID: %d)\n", getpid());
sleep(2);
}
printf("\n[CLEANUP] Cleaning up resources...\n");
printf("[EXIT] Process terminated gracefully\n");
return 0;
}
ㅤ
시그널 마스킹
시그널을 등록해줄 때, 다음의 설정들이 포함되어 있는 것을 볼 수 있다.
// 핸들러 실행 중 다른 신호 차단하지 않음 설정
sigemptyset(&sa.sa_mask);
// SA_RESTART: 신호로 중단된 시스템 콜 자동 재개 설정
sa.sa_flags = SA_RESTART;
ㅤ
이 설정값들을 변경하여서 먼저 들어온 시그널을 처리하는 동안 다음 시그널이 차단되고 허용됨을 설정할 수 있다.
| 함수 | 기능 |
|---|---|
sigemptyset(&set) |
신호 집합을 비움 (아무 신호도 차단 안 함) |
sigfillset(&set) |
신호 집합을 모두 채움 (모든 신호 차단) |
sigaddset(&set, sig) |
특정 신호를 집합에 추가 (차단 대상에 추가) |
sigdelset(&set, sig) |
특정 신호를 집합에서 제거 (차단 해제) |
sigismember(&set, sig) |
특정 신호가 집합에 포함되어 있는지 확인 |
ㅤ
| 플래그 | 기능 |
|---|---|
SA_RESTART |
신호로 중단된 시스템 콜 자동 재개 |
SA_NOCLDSTOP |
자식 프로세스가 멈춰도 SIGCHLD 발생 안 함 |
SA_NOCLDWAIT |
자식 프로세스 자동 회수 (zombie 방지) |
SA_NODEFER |
같은 신호 중첩 허용 (자동 차단 해제) |
SA_RESETHAND |
핸들러 실행 후 기본 동작으로 리셋 |
SA_SIGINFO |
핸들러가 더 많은 정보를 받음 (sa_sigaction 사용) |
0 (없음) |
기본 동작 |
ㅤ
예시 중 하나로, 시그널 핸들러에 동일한 인터럽트가 중첩으로 들어오는 상황을 한 번 만들어보자. 사실 이 케이스는 굉장히 위험하다. 자원을 해제하는 시그널 핸들러가 호출되었는데, 동일한 시그널 핸들러가 한 번 더 실행되어서 2개의 핸들러 실행흐름이 생겼다고 해보자. 이때 동일한 자원에 두 핸들러 접근하기 때문에 race condition으로 문제가 발생할 수 있다. 그치만 지금은 그냥 예시니깐!
ㅤ
기본적으로 같은 시그널에 대해서는 처리 도중에 핸들러가 중첩해서 호출되지 않는다. 즉 SIGINT 처리중에는 SIGINT를 그냥 Pending 상태로 걸어둔다.
void signal_handler(int sig) {
if (sig == SIGINT) {
count++;
printf("\n[SIGINT] count: %d\n", count);
sleep(3);
printf("\n[SIGINT] count: %d\n", count);
shutdown_flag = 1;
}

ㅤ
만약 이렇게 플래그를 SA_NODEFER 를 전달해주면 Ctrl+C로 신호를 줄 때마다 핸들러가 중첩해서 호출되는 것을 볼 수 있다.
// 이 플래그를 설정하면 핸들러가 실행되는 동안 동일한 시그널(SIGINT)을 블록하지 않는다.
act.sa_flags = SA_NODEFER;

ㅤ
아니면, 이렇게 시그널 실행 중 차단 대상(무시 대상)에 시그널을 넣어버릴수도 있다.
// SIGINT 처리중에는 SIGINT 를 무시하기
sigaddset(&sa.sa_mask, SIGINT);
ㅤ
시그널 보내 시그널 보내
이번에는 프로세스끼리 시그널을 보내는 실습을 해보았다. 시그널을 보내기 위해서는 kill() 이라는 함수를 사용한다. 이게 완전 옛날 1970년대의 UNIX 에서는 프로세스에게 보낼 신호가 kill 밖에 없었기 때문에 kill 이라는 이름을 사용했었는데, 여러 기능들이 붙으면서 이 명령어(함수)가 하는 역할은 프로세스에게 시그널을 날리는 것이지만 명령어의 이름은 그냥 kill로 남아있게 되었다.
ㅤ
// receiver.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig) {
printf("\n[Receive] SIGINT(Ctrl+C) 받음! 프로세스 종료.\n");
exit(0); // 프로그램 종료
}
int main() {
signal(SIGINT, handler); // 핸들러 연결
printf("대기 중... (내 PID: %d)\n", getpid());
while(1) {
pause(); // 시그널이 올 때까지 CPU 소모 없이 대기
}
return 0;
}
// sender.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("사용법: %s <PID>\n", argv[0]);
return 1;
}
int target_pid = atoi(argv[1]); // 문자열 PID를 정수로 변환
// 해당 PID로 SIGINT 전송
kill(target_pid, SIGINT);
printf("[Send] PID %d에게 SIGINT 전송 완료.\n", target_pid);
return 0;
}
ㅤ
receiver 에서 신호를 기다리다가 sender가 kill 명령어 (함수)로 보낸 SIGINT를 받고 프로세스가 종료되는 것을 확인할 수 있다.

ㅤ
이 시그널 누가 보낸기야!
그럼 혹시 이 신호를 누가 보냈는지도 파악할 수 있을까? 기본적으로 간단하게 쓰던 signal 로는 이 정보를 가져올 수 없지만, 역시나 우리의 듬직한 sigaction을 써주면 핸들러에 이 정보를 받아올 수 있다. 플래그에서 SA_SIGINFO 를 Set 해주면 핸들러에게 siginfo 인자를 통해 정보를 받아올 수 있게 된다.
// receiver.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
// 인자가 3개인 핸들러 (SA_SIGINFO 사용 시)
void handler(int sig, siginfo_t *info, void *ucontext) {
pid_t sender_pid = info->si_pid; // 발신자의 PID 추출
printf("\n[Receive] PID %d 로부터 시그널 받음!\n", sender_pid);
printf("[Receive] %d 에게 무지개반사!\n", sender_pid);
// 발신자에게 역으로 SIGINT 전송
kill(sender_pid, SIGINT);
}
int main() {
struct sigaction act;
// sa_sigaction 필드에 핸들러 지정 (sa_handler 아님)
act.sa_sigaction = handler;
sigemptyset(&act.sa_mask);
// ★ 중요: 발신자 정보를 얻기 위해 SA_SIGINFO 플래그 설정
act.sa_flags = SA_SIGINFO;
sigaction(SIGINT, &act, NULL);
printf("대기 중... (내 PID: %d)\n", getpid());
while(1) {
pause();
}
return 0;
}
// sender.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
// 답장을 받으면 실행될 핸들러
void handler(int sig) {
printf("\n[Send] 상대방에게서 답장(SIGINT)이 왔습니다. 종료합니다.\n");
exit(0);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("사용법: %s <Target_PID>\n", argv[0]);
return 1;
}
// 답장을 받기 위한 핸들러 등록
signal(SIGINT, handler);
int target_pid = atoi(argv[1]);
printf("[Send] %d 에게 시그널 발사하고 대기 중...\n", target_pid);
kill(target_pid, SIGINT); // 1. 시그널 전송
// 2. 답장이 올 때까지 무한 대기
while(1) {
pause();
}
return 0;
}
ㅤ
실제로 위 코드를 실행해보면, 오른쪽 sender에서 SIGINT 신호를 보냈고, 이걸 좌측 receiver 가 받았지만 누가 보냈는지 찾아내어 해당 pid를 가진 프로세스에게 다시 SIGINT롤 보내고있는 것을 볼 수 있다.

ㅤ
혹시나! 걱정했는데, bash 에는 SIGINT에 대한 핸들러가 따로 있지는 않은가보다. 그냥 무시해버린다.

ㅤ
궁금해서
kill -9 1380으로 Bash에게 프로세스 종료를 날려봤는데, 그대로 꺼졌다. 아무리 Bash 라고 하더라도 기본 설정을 막을 수는 없나보다.
ㅤ
라고 생각할 때 그렇다면 루트 프로세스들을 죽여버리면 어떻게 될까? 얘네도 이거 못하나? 싶어서 바로 살을 날려봤는데 sudo 명령어가 먹긴 하지만 회피를 해버린 모습. 이건 안전하게 보호해주나보다.


'Embedded System > Embedded Linux' 카테고리의 다른 글
| [Embedded Linux] 프로세스간 통신 IPC - System V 기반의 IPC (0) | 2025.11.29 |
|---|---|
| [Embedded Linux] 프로세스간 통신 IPC - 파일 기반의 IPC (0) | 2025.11.29 |
| [Embedded Linux] 리눅스의 프로세스 타파 (0) | 2025.11.27 |
| [Embedded Linux] 라즈베리파이의 부팅 (1) | 2025.11.25 |
| [Embedded Linux] Linux 커널 아키텍처 (1) | 2025.11.25 |