터미널 키 입력을 통한 신호 발생
사용자는 키보드 조합을 통해 프로세스에 다양한 신호를 전달할 수 있습니다:
- Ctrl+C: SIGINT 신호 전송
- Ctrl+\: SIGQUIT 신호로 프로세스 종료
- Ctrl+Z: SIGTSTP 신호로 포그라운드 프로세스 일시정지
이러한 신호들은 운영체제가 하드웨어 인터럽트를 소프트웨어적으로 시뮬레이션한 결과로, CPU 대상의 인터럽트와 달리 프로세스에 직접 전달됩니다.
시스템 명령어를 이용한 신호 전송
kill 명령은 특정 프로세스에게 신호를 보내는 데 사용됩니다:
# 신호 이름으로 전송
kill -SIGNAL_NAME PID
# 신호 번호로 전송 (권장)
kill -NUMBER PID
# 예시: PID 213784 프로세스에 SIGSEGV(11번) 신호 전송
kill -SIGSEGV 213784
kill -11 213784
백그라운드에서 무한 루프를 실행하는 프로세스를 만들어 테스트해 보겠습니다:
// infinite_loop.cpp
#include <iostream>
#include <unistd.h>
int main() {
while(true) {
sleep(1);
}
return 0;
}
g++ infinite_loop.cpp -o loop_process
./loop_process & # 백그라운드 실행
ps aux | grep loop_process # PID 확인 (예: 213784)
kill -SIGSEGV 213784 # 세그멘테이션 폴트 신호 전송
해당 프로세스는 SIGSEGV 신호를 받아 기본 동작인 종료 및 코어 덤프를 수행합니다.
함수를 통한 신호 생성
kill() 함수
kill() 시스템 호출은 지정된 프로세스에 임의의 신호를 전송할 수 있는 저수준 인터페이스입니다:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
성공 시 0을 반환하고 실패 시 -1을 반환합니다.
Makefile 예시:
.PHONY: all clean
all: signal_sender signal_receiver
signal_sender: sender.cpp
g++ -o $@ $^ -std=c++11
signal_receiver: receiver.cpp
g++ -o $@ $^ -std=c++11
clean:
rm -f signal_sender signal_receiver
sender.cpp 구현:
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <string>
// 사용법: ./signal_sender SIGNAL_NUMBER PID
int main(int argc, char *argv[]) {
if (argc != 3) {
std::cerr << "Usage: ./signal_sender SIGNAL_NUMBER PID" << std::endl;
return 1;
}
int signal_num = std::stoi(argv[1]);
pid_t process_id = std::stol(argv[2]);
int result = kill(process_id, signal_num);
if (result == 0) {
std::cout << "Signal " << signal_num << " sent to process " << process_id << std::endl;
} else {
std::cerr << "Failed to send signal" << std::endl;
return 1;
}
return 0;
}
receiver.cpp 구현:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void signal_handler(int sig) {
std::cout << "Received signal: " << sig << std::endl;
}
int main() {
signal(SIGINT, signal_handler);
int counter = 0;
while (true) {
std::cout << "Running... Count: " << counter++ << ", PID: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
raise() 함수
자기 자신에게 신호를 보내는 함수로, 내부적으로 kill(getpid(), sig)를 호출합니다:
#include <signal.h>
int raise(int sig);
성공 시 0을, 실패 시 0이 아닌 값을 반환합니다.
SIGKILL(9번)과 SIGSTOP(19번) 신호는 사용자 정의 핸들러로 캡처할 수 없습니다.
abort() 함수
강제로 프로세스를 비정상 종료시키며, SIGABRT 신호를 발생시킵니다:
#include <stdlib.h>
void abort(void);
이 함수는 항상 성공하며, 신호 핸들러가 등록되어 있어도 최종적으로 프로세스는 종료됩니다.
하드웨어 예외로 인한 신호 발생
0으로 나누기 오류 → SIGFPE
// zero_division.cpp
#include <stdio.h>
#include <signal.h>
void handle_signal(int sig) {
printf("Caught signal: %d\n", sig);
}
int main() {
// signal(SIGFPE, handle_signal); // 주석 해제 시 핸들링 가능
int value = 10;
value /= 0; // 0 나누기 오류
while(1); // 무한 루프
return 0;
}
기본 동작으로 SIGFPE(8번) 신호를 받고 프로세스가 종료되며, 핸들러가 등록되면 예외 상태가 초기화되지 않아 반복적으로 신호가 발생합니다.
무효 포인터 접근 → SIGSEGV
// null_pointer.cpp
#include <stdio.h>
#include <signal.h>
void handle_segfault(int sig) {
printf("Segmentation fault detected: signal %d\n", sig);
}
int main() {
// signal(SIGSEGV, handle_segfault); // 주석 해제 시 핸들링 가능
int *null_ptr = nullptr;
*null_ptr = 100; // NULL 포인터 역참조
while(1);
return 0;
}
기본적으로 SIGSEGV(11번) 신호를 받아 종료되며, MMU 상태가 초기화되지 않아 핸들러 등록 시 반복적인 신호 발생이 일어납니다.
하드웨어 예외와 신호 매핑
| 예외 유형 | 대표 코드 | 신호 | 번호 | 기본 동작 |
|---|---|---|---|---|
| 0 나누기 | a /= 0; |
SIGFPE | 8 | 종료 + 코어 덤프 |
| 세그멘테이션 폴트 | *p = 100; // p=nullptr |
SIGSEGV | 11 | 종료 + 코어 덤프 |
| 잘못된 명령어 | 손상된 바이너리 실행 | SIGILL | 4 | 종료 + 코어 덤프 |
| 버스 오류 | 메모리 정렬 오류 | SIGBUS | 7 | 종료 + 코어 덤프 |
| 부동소수점 예외 | 오버플로우/언더플로우 | SIGFPE | 8 | 종료 + 코어 덤프 |
| 디버깅 중단점 | int 3 명령 |
SIGTRAP | 5 | 종료 + 코어 덤프 |
운영체제의 예외 감지 메커니즘
신호는 모두 운영체제에 의해 생성되며, 하드웨어 레벨에서 다음과 같은 구성 요소들이 관여합니다:
| 구성 요소 | 역할 |
|---|---|
| EFLAGS 플래그 레지스터 | 산술 연산 결과 상태 저장 (오버플로우, 제로 플래그 등) |
| CR0 제어 레지스터 | CPU 작동 모드 제어 및 예외 활성화 비트 포함 |
| CR2 레지스터 | 페이지 폴트 발생 시 문제 주소 저장 |
| CR3 레지스터 | 페이지 디렉토리 테이블 물리 주소 저장 (MMU 활용) |
| IDT(인터럽트 디스크립터 테이블) | 예외 처리기 진입 주소 저장 |
소프트웨어 조건에 의한 신호 생성
운영체제 커널은 특정 소프트웨어 조건을 감지했을 때 자동으로 관련 프로세스에 신호를 전송합니다. 대표적인 사례로 파이프 깨짐(SIGPIPE)과 타이머 만료(SIGALRM)가 있습니다.
alarm() 함수 기반 검증
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
지정된 초 후 현재 프로세스에 SIGALRM 신호를 전송하며, 이전 설정이 있었다면 남은 시간을 반환합니다. seconds가 0이면 기존 알람을 취소합니다.
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
void signal_catcher(int sig) {
std::cout << "Signal received: " << sig << std::endl;
exit(13);
}
int main() {
for(int i = 1; i < 32; i++) {
signal(i, signal_catcher);
}
alarm(1); // 1초 후 SIGALRM 신호 발생
int count = 0;
while(true) {
std::cout << "Count: " << count++ << std::endl;
}
return 0;
}
pause() 시스템 호출
#include <unistd.h>
int pause(void);
| 특성 | 설명 |
|---|---|
| 기능 | 임의의 신호를 받을 때까지 프로세스를 인터럽트 가능한 대기 상태로 전환 |
| 반환값 | 항상 -1 반환, errno는 EINTR로 설정됨 |
| 특징 | CPU 리소스를 소비하지 않는 완전한 블로킹 |
| 사용처 | 신호 기반 이벤트 루프 대기 |
빈 루프 대신 pause()를 사용하는 이유는 CPU 점유율 차이 때문입니다:
// 잘못된 방식: CPU 낭비
while (true) {
// 빈 작업, CPU 100% 점유
}
// 올바른 방식: 효율적 대기
while (true) {
pause(); // 신호 수신 시까지 프로세스 일시정지
}
주기적 알람 설정
alarm()은 일회성 동작이므로 반복 실행을 위해서는 신호 핸들러 내에서 재설정해야 합니다:
#include <unistd.h>
#include <signal.h>
#include <iostream>
void periodic_handler(int sig) {
std::cout << "Periodic task executed: signal " << sig << std::endl;
alarm(1); // 다음 1초 알람 설정
}
int main() {
signal(SIGALRM, periodic_handler);
alarm(1); // 첫 알람 설정
while (true) {
pause(); // 신호 도착 시마다 다시 대기
}
return 0;
}
실제 운영체제 스타일의 주기적 작업 예시:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <functional>
#include <vector>
struct ProcessTask {
pid_t process_id;
int time_slice;
};
std::vector<ProcessTask> task_queue;
// 작업 함수들
void schedule_processes() {
std::cout << "[Scheduler] Process scheduling in progress" << std::endl;
}
void memory_management() {
std::cout << "[Memory Manager] Checking memory issues" << std::endl;
}
void flush_buffers() {
std::cout << "[Buffer Flush] Writing data to disk" << std::endl;
}
using TaskFunction = std::function<void()>;
std::vector<TaskFunction> task_functions;
void alarm_handler(int sig) {
std::cout << "=================================" << std::endl;
for(const auto& task : task_functions) {
task();
}
std::cout << "=================================" << std::endl;
alarm(1);
}
int main() {
task_functions.push_back(schedule_processes);
task_functions.push_back(memory_management);
task_functions.push_back(flush_buffers);
signal(SIGALRM, alarm_handler);
alarm(1);
while (true) {
pause();
}
return 0;
}
커널 내 알람 관리 메커니즘
타이머 데이터 구조
// 리눅스 커널 타이머 구조체 (간소화 버전)
struct kernel_timer {
struct list_head node; // 연결 리스트 노드
unsigned long expiration_time; // 만료 시간 (절대 시간, jiffies 단위)
void (*callback)(unsigned long); // 만료 시 호출될 함수
unsigned long callback_data; // 콜백 함수 파라미터
struct timer_base *timer_base_ptr; // 타이머 베이스 참조
};
| 필드 | 의미 |
|---|---|
expiration_time |
만료 시각 (jiffies 단위) |
callback |
실행될 함수 (SIGALRM 전송) |
callback_data |
콜백 함수에 전달되는 파라미터 (프로세스 정보 등) |
최소 힙 기반 조직
커널은 최소 힙(min-heap) 구조를 사용하여 알람들을 효율적으로 관리합니다:
- 부모 노드의 만료 시간 < 자식 노드의 만료 시간
- 루트 노드는 항상 가장 먼저 만료되는 알람
- 삽입, 삭제, 최솟값 조회: O(log n) 시간 복잡도
핵심 개념 요약
| 개념 | 설명 |
|---|---|
| alarm() 특성 | 일회성 동작이며 실행 후 자동 소멸 |
| pause() 효율성 | 블로킹 상태 유지로 CPU 리소스 절약 |
| 주기적 타이밍 | 신호 핸들러 내 재설정으로 반복 실행 |
| 커널 타이머 관리 | 최소 힙 구조로 효율적인 스케줄링 |
| 시스템 클럭 인터럽트 | 운영체제 전체 타이밍 드라이버 |
| 신호의 본질 | 비동기적 알림 메커니즘 |