suminworld

concept

[운영체제론] errno와 시그널 핸들러

숨usm 2025. 10. 23. 04:17

운영체제론 중간고사 강의 내용 일부 정리한건데 나중에 볼 일이 있을 것 같아서
기록해둡니다 \_へ(´-`;)

errno란 무엇인가

정의

errno = error number (에러 번호)

시스템 함수가 실패했을 때 실패 이유를 알려주는 전역 변수입니다.

선언

#include <errno.h>

extern int errno;  // 전역 변수

extern은 다른 파일에 선언된 변수를 의미하며, errno는 C 라이브러리가 관리합니다.


errno의 동작 원리

int result = open("file.txt", O_RDONLY);

if (result == -1) {
    // 실패 시 errno에 이유가 저장됨
    printf("errno = %d\n", errno);
}

동작 과정

  1. open() 호출
  2. 파일이 존재하지 않음을 발견
  3. -1 반환
  4. errno = 2 (ENOENT) 설정

주요 errno 값

번호이름의미발생 상황
1 EPERM 권한 없음 권한 부족
2 ENOENT 파일 없음 파일이 존재하지 않음
3 ESRCH 프로세스 없음 해당 PID 없음
4 EINTR 중단됨 시그널로 중단
9 EBADF 잘못된 파일 유효하지 않은 파일 디스크립터
10 ECHILD 자식 없음 wait할 자식이 없음
11 EAGAIN 다시 시도 리소스 일시 부족
12 ENOMEM 메모리 부족 malloc 실패
13 EACCES 접근 거부 파일 접근 권한 없음

실제 사용 예제

예제 1: 파일 열기

#include <stdio.h>
#include <errno.h>
#include <string.h>

int main() {
    FILE *fp = fopen("없는파일.txt", "r");
    
    if (fp == NULL) {
        printf("errno = %d\n", errno);
        printf("의미: %s\n", strerror(errno));
    }
}
```

출력:
```
errno = 2
의미: No such file or directory

예제 2: 프로세스 찾기

#include <signal.h>
#include <errno.h>

int main() {
    int result = kill(99999, SIGTERM);  // 없는 PID
    
    if (result == -1) {
        if (errno == ESRCH) {
            printf("프로세스가 존재하지 않습니다\n");
        }
    }
}

예제 3: wait 사용

#include <sys/wait.h>
#include <errno.h>

int main() {
    pid_t pid = wait(NULL);
    
    if (pid == -1) {
        if (errno == ECHILD) {
            printf("기다릴 자식이 없습니다\n");
        }
    }
}

errno 사용 시 주의사항

주의사항 1: 성공 시 errno는 변경되지 않음

잘못된 사용:

errno = 0;
result = some_function();
if (errno != 0) {  // 잘못된 체크
    // ...
}

올바른 사용:

result = some_function();
if (result == -1) {  // 먼저 반환값 확인
    if (errno == ENOENT) {
        // ...
    }
}

이유: 성공한 함수는 errno를 0으로 초기화하지 않습니다. 이전 에러값이 남아있을 수 있습니다.

주의사항 2: errno는 덮어써질 수 있음

잘못된 코드:

int fd = open("file.txt", O_RDONLY);  // 실패, errno = 2

printf("Error!\n");  // printf가 errno를 바꿀 수 있음

printf("errno = %d\n", errno);  // 잘못된 값 출력 가능

올바른 코드:

int fd = open("file.txt", O_RDONLY);

if (fd == -1) {
    int saved = errno;  // 즉시 저장
    printf("Error!\n");
    printf("errno = %d\n", saved);
}

주의사항 3: 시그널 핸들러에서의 errno

void handler(int sig) {
    int saved = errno;  // 1. 저장
    
    // 핸들러 작업
    wait(NULL);  // errno를 바꿀 수 있음
    
    errno = saved;  // 2. 복원
}
```

필요한 이유:
```
Main 함수:
    read() 호출 → errno = 0 (성공)
    
[시그널 발생]

Handler:
    wait() 호출 → errno = ECHILD (실패)
    
Main 함수 복귀:
    errno 확인 → ECHILD
    → read가 실패한 것처럼 보임
```

저장/복원 후:
```
Main 함수:
    read() 호출 → errno = 0 (성공)
    
[시그널 발생]

Handler:
    saved = 0 (저장)
    wait() 호출 → errno = ECHILD
    errno = 0 (복원)
    
Main 함수 복귀:
    errno = 0 (올바른 값)

errno 관련 유틸리티 함수

strerror

char *strerror(int errnum);

 

사용 예:

printf("Error: %s\n", strerror(errno));

 

예제:

errno = 2;
printf("%s\n", strerror(errno));
// 출력: No such file or directory

errno = 10;
printf("%s\n", strerror(errno));
// 출력: No child processes

perror

void perror(const char *s);

 

사용 예:

if (open("file.txt", O_RDONLY) == -1) {
    perror("open failed");
}
```

출력:
```
open failed: No such file or directory

 

내부 동작:

// perror는 내부적으로 다음과 같이 동작
fprintf(stderr, "%s: %s\n", s, strerror(errno));

직접 errno 확인

#include <errno.h>

if (errno == ENOENT) {
    printf("파일이 없습니다\n");
} else if (errno == EACCES) {
    printf("권한이 없습니다\n");
} else {
    printf("알 수 없는 에러: %d\n", errno);
}

실전 예제: 완전한 에러 처리

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

int main() {
    int fd = open("test.txt", O_RDONLY);
    
    if (fd == -1) {
        // 방법 1: switch 문으로 직접 확인
        switch (errno) {
            case ENOENT:
                fprintf(stderr, "파일이 존재하지 않습니다\n");
                break;
            case EACCES:
                fprintf(stderr, "접근 권한이 없습니다\n");
                break;
            default:
                fprintf(stderr, "알 수 없는 에러: %s\n", 
                        strerror(errno));
        }
        
        // 방법 2: perror 사용
        perror("open");
        
        exit(1);
    }
    
    printf("파일 열기 성공\n");
    close(fd);
    return 0;
}

시그널 핸들러에서의 errno 처리

저장하지 않은 경우 (버그)

void handler(int sig) {
    wait(NULL);  // errno를 바꿈
}

int main() {
    Signal(SIGCHLD, handler);
    
    int fd = open("file.txt", O_RDONLY);
    if (fd == -1) {
        // 이 시점에 시그널이 발생하면?
        perror("open");  // 잘못된 에러 메시지 출력
    }
}
```

문제 발생:
```
1. open 실패 → errno = ENOENT
2. SIGCHLD 발생 → handler 실행
3. handler의 wait → errno = ECHILD
4. perror 출력 → "No child processes" (잘못됨)

저장한 경우 (올바름)

void handler(int sig) {
    int saved = errno;  // 저장
    
    wait(NULL);  // errno를 바꿔도 됨
    
    errno = saved;  // 복원
}

int main() {
    Signal(SIGCHLD, handler);
    
    int fd = open("file.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");  // 정확한 에러 출력
    }
}
```

해결:
```
1. open 실패 → errno = ENOENT
2. SIGCHLD 발생 → handler 실행
   2-1. saved = ENOENT (저장)
   2-2. wait 실행 → errno = ECHILD
   2-3. errno = ENOENT (복원)
3. perror 출력 → "No such file" (정확함)

errno 사용 패턴

패턴 1: 기본 체크

if (함수() == -1) {
    perror("함수 이름");
    exit(1);
}

패턴 2: 특정 에러 처리

if (함수() == -1) {
    if (errno == EINTR) {
        // 시그널로 중단됨, 재시도
    } else {
        perror("함수");
        exit(1);
    }
}

패턴 3: 핸들러에서 안전하게 사용

void handler(int sig) {
    int saved = errno;
    
    // 작업 수행
    
    errno = saved;
}

핵심 정리

errno 사용 규칙

  1. 함수 실패 시에만 errno 확인
  2. 즉시 저장하기 (다른 함수가 바꿀 수 있음)
  3. 핸들러에서는 항상 저장/복원
  4. 성공 시 errno는 변경되지 않음

올바른 사용 방법

// 1. 반환값으로 실패 확인
if (result == -1) {
    // 2. errno 저장
    int saved = errno;
    
    // 3. 에러 처리
    if (saved == ENOENT) {
        // 처리
    }
}
```

---

# Correct Signal Handling

## 문제 상황: 시그널은 큐가 아니다

### 핵심 문제

영어: "Pending signals are not queued"

한국어: 대기 중인 시그널은 큐에 저장되지 않습니다.

설명: 프로세스에 같은 타입의 시그널이 대기 중이면, 추가로 오는 같은 타입의 시그널은 버려집니다.

### 일반 큐와 시그널 비교

일반 큐 (메시지):
```
메시지 큐
├─ SIGCHLD 1
├─ SIGCHLD 2
├─ SIGCHLD 3
└─ SIGCHLD 4

→ 4개 모두 전달됨
```

시그널 (비트):
```
시그널 상태
SIGCHLD: [1]  ← 비트 1개로 표시

SIGCHLD 1 도착 → [1]
SIGCHLD 2 도착 → [1] (이미 표시됨)
SIGCHLD 3 도착 → [1] (이미 표시됨)
SIGCHLD 4 도착 → [1] (이미 표시됨)

→ "SIGCHLD 있음" 1번만 알림

버그가 있는 코드 분석

전체 코드

int ccount = 0;

void child_handler(int sig) {
    int olderrno = errno;
    pid_t pid;
    
    if ((pid = wait(NULL)) < 0)
        Sio_error("wait error");
    
    ccount--;
    Sio_puts("Handler reaped child ");
    Sio_putl((long)pid);
    Sio_puts(" \n");
    sleep(1);
    
    errno = olderrno;
}

변수 선언

int ccount = 0;

역할: 아직 종료되지 않은 자식 프로세스 개수를 추적하는 전역 변수

핸들러 분석

errno 저장

int olderrno = errno;

main 함수의 errno 값을 보호하기 위해 저장합니다.

자식 프로세스 수거

if ((pid = wait(NULL)) < 0)
    Sio_error("wait error");

wait()의 역할: 종료된 자식 프로세스 1개를 수거하고 그 PID를 반환합니다.

 

문제점:

// 자식 3개가 동시에 종료된 경우
Child1 종료 → SIGCHLD 발생
Child2 종료 → SIGCHLD 발생  
Child3 종료 → SIGCHLD 발생

// Pending 상태
SIGCHLD: [1] ← 1개만 표시됨

// 핸들러 실행
wait() 호출 → Child1만 수거
// Child2, Child3는 수거되지 않음

카운터 감소

ccount--;
```

의도: 자식 1개를 수거했으므로 카운터를  1 감소

실제 문제:
```
ccount = 5 (자식 5개 생성)

자식 5개 모두 종료
→ 핸들러 1번만 실행
→ ccount-- 1번만 실행
→ ccount = 4

실제로는 자식이 모두 종료되었지만
ccount는 4로 남아있음

출력 및 대기

Sio_puts("Handler reaped child ");
Sio_putl((long)pid);
Sio_puts(" \n");
sleep(1);

sleep(1)은 디버깅을 위한 지연입니다.

main 함수

void fork14() {
    pid_t pid[N];
    int i;
    ccount = N;
    
    Signal(SIGCHLD, child_handler);
    
    for (i = 0; i < N; i++) {
        if ((pid[i] = Fork()) == 0) {
            Sleep(1);
            exit(0);
        }
    }
    
    while (ccount > 0)
        ;
}

초기화

ccount = N;

N개의 자식을 생성할 예정이므로 ccount를 N으로 설정합니다.

핸들러 등록

Signal(SIGCHLD, child_handler);

SIGCHLD는 자식 프로세스가 종료될 때 부모에게 전달되는 시그널입니다.

자식 생성

for (i = 0; i < N; i++) {
    if ((pid[i] = Fork()) == 0) {
        Sleep(1);
        exit(0);
    }
}

 

자식의 동작:

Sleep(1);    // 1초 대기
exit(0);     // 종료 → 부모에게 SIGCHLD 전송
```

Sleep(1)을 사용하는 이유:

Sleep 없는 경우:
```
자식들이 순차적으로 종료
→ 핸들러가 N번 실행됨
→ 문제가 드러나지 않음
```

Sleep 있는 경우:
```
자식들이 거의 동시에 종료
→ 핸들러가 1번만 실행됨
→ 버그 발생

대기 루프

while (ccount > 0)
    ;
```

spin lock: ccount가 0이 될 때까지 계속 확인합니다.

의도한 동작:
```
ccount = 5
자식1 종료 → ccount = 4
자식2 종료 → ccount = 3
자식3 종료 → ccount = 2
자식4 종료 → ccount = 1
자식5 종료 → ccount = 0
→ 루프 종료
```

실제 동작:
```
ccount = 5
자식들이 거의 동시에 종료
→ SIGCHLD 1개만 pending
→ 핸들러 1번만 실행
→ ccount = 4
→ 무한 루프
```

---

## 버그 시나리오

### 타임라인
```
T=0초:
부모: N=5개 자식 생성
      ccount = 5

T=0~1초:
자식1~5: Sleep(1) 실행 중

T=1초: (거의 동시에)
자식1: exit(0) → SIGCHLD 전송
자식2: exit(0) → SIGCHLD 전송
자식3: exit(0) → SIGCHLD 전송
자식4: exit(0) → SIGCHLD 전송
자식5: exit(0) → SIGCHLD 전송

Pending 비트:
SIGCHLD: [1] ← 1개만 표시됨

T=1.1초:
부모: child_handler 실행
      wait() → 자식1 수거
      ccount-- → ccount = 4
      출력: "Handler reaped child 23240"
      sleep(1)

T=2.1초:
부모: handler 종료
      while (ccount > 0) 실행
      ccount = 4 ≠ 0
      → 무한 루프

실행 결과

bash
> ./forks 14
Handler reaped child 23240
Handler reaped child 23241
[프로그램이 멈춤]

분석:

  • 5개 자식 생성
  • 2개만 수거됨
  • 나머지 3개는 좀비 프로세스
  • ccount = 3
  • 무한 루프 발생

좀비 프로세스

정의

죽었지만 부모가 수거하지 않은 프로세스

확인 방법

 
bash
$ ps aux | grep defunct
USER   PID  STAT
user 23242   Z    <defunct>
user 23243   Z    <defunct>
user 23244   Z    <defunct>
```

STAT = Z는 Zombie 상태를 의미합니다.

특징:
- 메모리는 해제됨
- 프로세스 테이블에는 남아있음
- 부모가 wait()로 수거해야 완전히 제거됨

---

## 핵심 문제 정리

### 문제 1: 시그널은 큐가 아님
```
예상: SIGCHLD 5개 → 핸들러 5번 실행
현실: SIGCHLD 5개 → pending 1개 → 핸들러 1번 실행

문제 2: wait()는 1개만 수거

void handler(int sig) {
    wait(NULL);  // 1개만 수거
    ccount--;    // 1만 감소
}

 

필요한 것:

while (종료된 자식이 있으면) {
    wait(NULL);  // 모두 수거
    ccount--;
}

비교: 큐 vs 비트

항목메시지 큐시그널 (비트)
저장 방식 모두 저장 1개만 표시
순서 보장됨 없음
개수 세기 가능 불가능
손실 없음 있음

올바른 해결책

수정된 코드

void child_handler2(int sig) {
    int olderrno = errno;
    pid_t pid;
    
    while ((pid = wait(NULL)) > 0) {
        ccount--;
        Sio_puts("Handler reaped child ");
        Sio_putl((long)pid);
        Sio_puts(" \n");
    }
    
    if (errno != ECHILD)
        Sio_error("wait error");
    
    errno = olderrno;
}

코드 상세 분석

errno 저장

int olderrno = errno;
```

이유:
```
Main에서:
    read() 성공 → errno = 0

Handler 실행:
    wait() 여러 번 → errno 변경

Main 복귀:
    errno 확인 → 잘못된 값 참조 가능

 

해결:

int olderrno = errno;  // 처음에 저장
// ... 작업 ...
errno = olderrno;      // 마지막에 복원

while 루프 (핵심 해결책)

while ((pid = wait(NULL)) > 0) {
    ccount--;
    Sio_puts("Handler reaped child ");
    Sio_putl((long)pid);
    Sio_puts(" \n");
}

wait() 반환값

pid = wait(NULL);
```

반환값:
- 양수: 수거한 자식의 PID
- -1: 에러 (수거할 자식 없음)

#### 루프 동작 과정

5개 자식이 모두 종료된 상태:
```
1번째 반복:
    pid = wait(NULL) → 23240 (자식1)
    23240 > 0? Yes
    ccount-- (5 → 4)
    출력: "Handler reaped child 23240"

2번째 반복:
    pid = wait(NULL) → 23241 (자식2)
    23241 > 0? Yes
    ccount-- (4 → 3)
    출력: "Handler reaped child 23241"

3번째 반복:
    pid = wait(NULL) → 23242 (자식3)
    23242 > 0? Yes
    ccount-- (3 → 2)
    출력: "Handler reaped child 23242"

4번째 반복:
    pid = wait(NULL) → 23243 (자식4)
    23243 > 0? Yes
    ccount-- (2 → 1)
    출력: "Handler reaped child 23243"

5번째 반복:
    pid = wait(NULL) → 23244 (자식5)
    23244 > 0? Yes
    ccount-- (1 → 0)
    출력: "Handler reaped child 23244"

6번째 반복:
    pid = wait(NULL) → -1 (더 이상 없음)
    -1 > 0? No
    루프 종료

if vs while 비교

if 사용 (버그):

 
 
c
if ((pid = wait(NULL)) > 0) {  // 1개만 수거
    ccount--;
}

// 5개 종료 → 1개만 수거
// 나머지 4개는 좀비

while 사용 (정상):

 
 
c
while ((pid = wait(NULL)) > 0) {  // 모두 수거
    ccount--;
}

// 5개 종료 → 5개 모두 수거

에러 체크

if (errno != ECHILD)
    Sio_error("wait error");

ECHILD

  • Error: CHILD not found
  • "기다릴 자식이 없음"
  • errno = 10

ECHILD 발생 시점

while ((pid = wait(NULL)) > 0) {
    // 자식들 모두 수거
}

// 루프 종료 시점:
// - 더 이상 자식 없음
// - wait()가 -1 반환
// - errno = ECHILD 설정
```

정상 상황:
```
wait() 성공 → pid > 0
wait() 성공 → pid > 0
wait() 성공 → pid > 0
wait() 실패 → pid = -1, errno = ECHILD

→ ECHILD는 정상
```

비정상 상황:
```
wait() 실패 → pid = -1, errno = EINTR

→ EINTR은 비정상 (시그널로 중단)

에러 체크 로직

// 루프 종료 후
if (errno != ECHILD) {
    // ECHILD가 아닌 다른 에러
    Sio_error("wait error");
}

가능한 errno:

  • ECHILD: 정상 (자식 모두 수거함)
  • EINTR: 비정상 (시그널로 중단)
  • EINVAL: 비정상 (잘못된 인자)

errno 복원

errno = olderrno;
```

복원 과정:
```
Handler 시작:
    olderrno = 0 (Main의 errno)

Handler 작업:
    wait() 호출들 → errno = ECHILD

Handler 종료:
    errno = olderrno (0으로 복원)

실행 결과

수정 후 출력

bash
> ./forks 15
Handler reaped child 23246
Handler reaped child 23247
Handler reaped child 23248
Handler reaped child 23249
Handler reaped child 23250
>

분석:

  • 5개 자식 모두 수거됨
  • ccount = 0
  • 프로그램 정상 종료
  • 좀비 프로세스 없음

버그 vs 수정 비교

코드 비교

버그 버전

void child_handler(int sig) {
    int olderrno = errno;
    pid_t pid;
    
    if ((pid = wait(NULL)) < 0)  // 1개만 수거
        Sio_error("wait error");
    
    ccount--;
    errno = olderrno;
}

수정 버전

void child_handler2(int sig) {
    int olderrno = errno;
    pid_t pid;
    
    while ((pid = wait(NULL)) > 0) {  // 모두 수거
        ccount--;
    }
    
    if (errno != ECHILD)
        Sio_error("wait error");
    
    errno = olderrno;
}

실행 비교

항목버그 버전수정 버전
자식 생성 5개 5개
자식 종료 5개 5개
수거된 자식 1-2개 5개
좀비 프로세스 3-4개 0개
ccount 3-4 0
프로그램 상태 무한루프 정상종료

타이밍에 따른 시나리오

경우 1: 순차 종료 (버그가 드러나지 않음)

T=1.0초: 자식1 종료 → SIGCHLD
         Handler 실행 → 자식1 수거
         
T=1.1초: 자식2 종료 → SIGCHLD  
         Handler 실행 → 자식2 수거
         
T=1.2초: 자식3 종료 → SIGCHLD
         Handler 실행 → 자식3 수거

→ 버그 버전도 작동함 (문제 발견 어려움)
```

### 경우 2: 동시 종료 (버그 발생)
```
T=1.0초: 자식1 종료 → SIGCHLD [1]
         자식2 종료 → (무시됨, 이미 pending)
         자식3 종료 → (무시됨)
         자식4 종료 → (무시됨)
         자식5 종료 → (무시됨)
         
T=1.1초: Handler 실행
         while문 → 모두 수거

→ 수정 버전만 작동

Sleep 제거 실험

Sleep 제거 시

// 자식에서 Sleep 제거
for (i = 0; i < N; i++) {
    if (Fork() == 0) {
        // Sleep(1);  // 주석 처리
        exit(0);
    }
}
```

**결과:**
```
자식들이 순차적으로 종료
→ Handler도 순차적으로 실행
→ 버그 버전도 작동하는 것처럼 보임
→ 버그 발견 못함

Sleep(1)은 버그를 드러내기 위한 코드입니다. 실전에서는 타이밍이 예측 불가능하므로 항상 while을 사용해야 안전합니다.


다양한 구현 방법

방법 1: 기본

while ((pid = wait(NULL)) > 0) {
    ccount--;
}

if (errno != ECHILD)
    Sio_error("wait error");

방법 2: do-while

do {
    pid = wait(NULL);
    if (pid > 0)
        ccount--;
} while (pid > 0);

방법 3: 무한 루프 + break

while (1) {
    pid = wait(NULL);
    
    if (pid == -1) {
        if (errno == ECHILD)
            break;  // 정상 종료
        else
            Sio_error("wait error");
    }
    
    ccount--;
}

장점: 에러 처리가 명확하고 디버깅이 쉽습니다.


흔한 실수 3가지

실수 1: if 사용

// 잘못된 코드
if ((pid = wait(NULL)) > 0) {
    ccount--;
}

// 1개만 수거됨

실수 2: 고정 횟수 루프

// 잘못된 코드
for (int i = 0; i < N; i++) {
    wait(NULL);
    ccount--;
}

// N번만 시도
// 중간에 실패하면 문제 발생

실수 3: errno 체크 누락

// 위험한 코드
while ((pid = wait(NULL)) > 0) {
    ccount--;
}
// errno 체크 안 함

// wait의 실제 에러를 구분 못함
```

---

## 핵심 정리

### while 루프를 사용하는 이유
```
문제: 시그널은 큐가 아님
해결: while로 모든 자식 수거

SIGCHLD 5개 → pending 1개
→ Handler 1번 실행
→ while이 5번 반복
→ 모두 수거

완벽한 패턴

void handler(int sig) {
    int saved_errno = errno;  // 1. 저장
    pid_t pid;
    
    while ((pid = wait(NULL)) > 0) {  // 2. 모두 수거
        // 처리
    }
    
    if (errno != ECHILD)  // 3. 에러 체크
        Sio_error("wait error");
    
    errno = saved_errno;  // 4. 복원
}

Portable Signal Handling

Unix 호환성 문제

주요 문제

Unix의 다른 버전들은 서로 다른 시그널 처리 방식을 가질 수 있습니다.


문제 1: 핸들러 자동 해제

오래된 시스템의 동작

Signal(SIGINT, handler);

// 첫 번째 Ctrl+C
// → handler 실행
// → 핸들러 자동 해제

// 두 번째 Ctrl+C  
// → 기본 동작 (종료)
```

**예상:**
```
Ctrl+C → handler 실행
Ctrl+C → handler 실행
Ctrl+C → handler 실행
```

**현실 (옛날 Unix):**
```
Ctrl+C → handler 실행
         (자동으로 기본 동작으로 복귀)
Ctrl+C → 프로그램 종료

문제 2: 시스템 콜 중단

EINTR (Error INTerRupted)

의미:

  • 시그널 때문에 중단되었음
  • 다시 시도하면 성공할 수 있음
char buf[100];
int n = read(fd, buf, 100);  // 읽기 시작

[SIGINT 발생]

// read()가 중단됨
// n = -1 반환
// errno = EINTR

Unix 버전별 차이

Unix A (자동 재시작):

n = read(fd, buf, 100);

[시그널 발생]
→ 핸들러 실행
→ read 자동으로 다시 시작
→ n = 실제 읽은 바이트 수

 

Unix B (재시작 안 함):

n = read(fd, buf, 100);

[시그널 발생]
→ 핸들러 실행
→ read 반환: n = -1, errno = EINTR
→ 프로그래머가 직접 재시도해야 함

대응 코드

단순 버전 (Unix B에서 실패):

int n = read(fd, buf, 100);
if (n == -1) {
    perror("read failed");
    exit(1);
}

 

호환 버전:

int n;
while ((n = read(fd, buf, 100)) == -1) {
    if (errno == EINTR) {
        continue;  // 재시도
    } else {
        perror("read failed");
        exit(1);
    }
}
```

**실행 흐름:**
```
1차 시도: read() → 시그널 → -1, EINTR
          → continue

2차 시도: read() → 성공 → n = 10
          → 루프 종료

문제 3: 시그널 차단 안 함

일반적인 동작

void handler(int sig) {
    printf("Handler start\n");
    sleep(3);
    printf("Handler end\n");
}

// Ctrl+C 누름
Handler start
[3초 대기] (이 동안 Ctrl+C 차단됨)
Handler end

// Ctrl+C 다시 누름
Handler start
[3초 대기]
Handler end

문제 있는 시스템

void handler(int sig) {
    printf("Handler start\n");
    sleep(3);  // 여기서 Ctrl+C 또 누르면?
    printf("Handler end\n");
}

// Ctrl+C 누름
Handler start
[1초 후 Ctrl+C 또 누름]
  Handler start  (중첩)
  [3초]
  Handler end
[남은 2초]
Handler end
```

**문제:**
```
핸들러 안에서 또 핸들러 실행
→ 재귀적 호출
→ 예상 못한 동작

해결책: sigaction

signal() vs sigaction()

함수호환성제어권장
signal() 불안정 제한적 X
sigaction() 안정적 완전한 제어 O

Signal() 래퍼 함수

전체 코드

c
handler_t *Signal(int signum, handler_t *handler) {
    struct sigaction action, old_action;
    
    action.sa_handler = handler;
    sigemptyset(&action.sa_mask);
    action.sa_flags = SA_RESTART;
    
    if (sigaction(signum, &action, &old_action) < 0)
        unix_error("Signal error");
    
    return (old_action.sa_handler);
}

코드 상세 분석

1. 함수 시그니처

handler_t *Signal(int signum, handler_t *handler)

매개변수:

  • signum: 시그널 번호 (예: SIGINT)
  • handler: 핸들러 함수 포인터

반환값: 이전 핸들러 (나중에 복원 가능)

handler_t 정의:

typedef void (*handler_t)(int);

2. 구조체 선언

struct sigaction action, old_action;

 

sigaction 구조체:

struct sigaction {
    void (*sa_handler)(int);     // 핸들러 함수
    sigset_t sa_mask;            // 차단할 시그널들
    int sa_flags;                // 플래그 옵션
};

3. 핸들러 설정

action.sa_handler = handler;

의미: "이 시그널이 오면 handler 함수를 실행해줘"

4. 시그널 마스크 초기화

sigemptyset(&action.sa_mask);

sa_mask: 핸들러 실행 중 추가로 차단할 시그널들

 

동작:

// 기본 차단 (자동)
SIGINT 처리 중 → SIGINT 차단

// 추가 차단 (sa_mask 설정)
sigaddset(&action.sa_mask, SIGTERM);
// SIGINT 처리 중 → SIGINT + SIGTERM 차단

// 우리 코드
sigemptyset(&action.sa_mask);
// → 추가 차단 없음
// → 기본 차단만 (현재 시그널)

5. 플래그 설정

action.sa_flags = SA_RESTART;

SA_RESTART: "시그널로 중단된 시스템 콜을 자동으로 재시작해줘"

효과:

SA_RESTART 없으면:

n = read(fd, buf, 100);
[시그널 발생]
→ read 중단
→ n = -1, errno = EINTR
→ 프로그래머가 재시도 필요

 

SA_RESTART 있으면:

n = read(fd, buf, 100);
[시그널 발생]
→ 핸들러 실행
→ read 자동 재시작
→ n = 실제 바이트 수

 

재시작 지원 함수:

// 자동 재시작 지원
read(), write(), wait(), waitpid(), ioctl()

// 재시작 안 됨
select(), poll()

6. sigaction 호출

if (sigaction(signum, &action, &old_action) < 0)
    unix_error("Signal error");
```

**동작 과정:**
```
시그널 테이블 Before:
SIGINT → 기본 동작 (종료)

sigaction(SIGINT, &action, &old_action) 호출

1. old_action에 현재 설정 저장
   old_action.sa_handler = 기본_동작
   
2. action으로 새 설정 적용
   SIGINT → my_handler

시그널 테이블 After:
SIGINT → my_handler

7. 이전 핸들러 반환

return (old_action.sa_handler);

용도: 나중에 복원 가능

handler_t old = Signal(SIGINT, new_handler);
// 작업 수행
Signal(SIGINT, old);  // 복원

사용 예제

예제 1: 기본 사용

void my_handler(int sig) {
    Sio_puts("Caught signal!\n");
}

int main() {
    Signal(SIGINT, my_handler);
    pause();  // 시그널 대기
}

특징:

  • 여러 번 Ctrl+C 가능 (핸들러 해제 안 됨)
  • 시스템 콜 자동 재시작

예제 2: 핸들러 복원

void temp_handler(int sig) {
    Sio_puts("Temporary!\n");
}

void main_handler(int sig) {
    Sio_puts("Main!\n");
}

int main() {
    handler_t old;
    
    Signal(SIGINT, main_handler);
    
    // 임시로 변경
    old = Signal(SIGINT, temp_handler);
    sleep(5);  // 5초 동안만 temp_handler
    
    // 복원
    Signal(SIGINT, old);
    
    pause();
}

예제 3: 시스템 콜 재시작

void handler(int sig) {
    Sio_puts("Signal received\n");
}

int main() {
    Signal(SIGINT, handler);  // SA_RESTART 적용
    
    char buf[100];
    int n = read(STDIN_FILENO, buf, 100);
    
    // Ctrl+C 눌러도 read 자동 재시작
    
    printf("Read %d bytes\n", n);
}

sigaction 직접 사용

Signal() 래퍼 vs sigaction()

Signal() 래퍼:

Signal(SIGINT, handler);

장점: 간단하고 한 줄로 끝

 

sigaction() 직접:

struct sigaction action;

action.sa_handler = handler;
sigemptyset(&action.sa_mask);
action.sa_flags = SA_RESTART;

sigaction(SIGINT, &action, NULL);

장점: 세밀한 제어와 더 많은 옵션


호환성 비교

signal() (옛날 방식)

동작Unix AUnix BUnix C
핸들러 유지 O X O
시스템 콜 재시작 O X ?
시그널 차단 O ? X

문제점: 동작이 불안정하고 예측 불가능

Signal() 래퍼

동작모든 Unix
핸들러 유지 O
시스템 콜 재시작 O
시그널 차단 O

장점: 동작이 안정적이고 예측 가능


sigaction 고급 사용

추가 시그널 차단

struct sigaction action;

action.sa_handler = handler;
sigemptyset(&action.sa_mask);

// SIGTERM도 추가로 차단
sigaddset(&action.sa_mask, SIGTERM);

// SIGQUIT도 차단
sigaddset(&action.sa_mask, SIGQUIT);

action.sa_flags = SA_RESTART;

sigaction(SIGINT, &action, NULL);
```

**효과:**
```
SIGINT 처리 중:
- SIGINT 자동 차단 (항상)
- SIGTERM 차단 (추가)
- SIGQUIT 차단 (추가)

모든 시그널 차단

struct sigaction action;

action.sa_handler = handler;

// 모든 시그널 차단
sigfillset(&action.sa_mask);

action.sa_flags = SA_RESTART;

sigaction(SIGINT, &action, NULL);
```

**효과:**
```
SIGINT 처리 중:
→ 모든 시그널 차단 (SIGKILL, SIGSTOP 제외)

주의사항

1. SIGKILL과 SIGSTOP은 변경 불가

// 불가능
Signal(SIGKILL, my_handler);  // 실패
Signal(SIGSTOP, my_handler);  // 실패
```

**이유:**
```
SIGKILL = 강제 종료 신호 (막을 수 없음)
SIGSTOP = 강제 정지 신호 (막을 수 없음)

2. SA_RESTART 제한

// SA_RESTART가 항상 동작하는 건 아님
select()  // 재시작 안 됨
poll()    // 재시작 안 됨

대응:

while ((n = select(...)) == -1 && errno == EINTR) {
    // 수동으로 재시도
}

3. 이식성

// 이식성 좋음 (권장)
Signal(SIGINT, handler);

// 이식성 나쁨 (비권장)
signal(SIGINT, handler);

최종 정리

문제점

  1. signal() 함수는 Unix마다 다름
  2. 핸들러 자동 해제될 수 있음
  3. 시스템 콜 재시작 안 될 수 있음
  4. 시그널 차단 안 될 수 있음

해결책

Signal() 래퍼 함수:

  1. sigaction 사용
  2. SA_RESTART 설정
  3. 일관된 동작 보장

Signal() 구조

1. action.sa_handler = handler
2. sigemptyset(&action.sa_mask)
3. action.sa_flags = SA_RESTART
4. sigaction(signum, &action, ...)

마치며

시그널 처리는 타이밍에 민감한 작업입니다. 버그가 있어도 순차 실행 시에는 문제가 드러나지 않을 수 있습니다. 따라서 처음부터 올바른 패턴(while 루프, sigaction)을 사용하는 것이 중요합니다.

핵심 원칙:

  • 시그널은 큐가 아니다 → while로 모두 수거
  • Unix 호환성 문제 → sigaction 사용
  • errno 보호 → 저장/복원 필수