운영체제론 중간고사 강의 내용 일부 정리한건데 나중에 볼 일이 있을 것 같아서
기록해둡니다 \_へ(´-`;)
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);
}
동작 과정
- open() 호출
- 파일이 존재하지 않음을 발견
- -1 반환
- 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 사용 규칙
- 함수 실패 시에만 errno 확인
- 즉시 저장하기 (다른 함수가 바꿀 수 있음)
- 핸들러에서는 항상 저장/복원
- 성공 시 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
→ 무한 루프
실행 결과
> ./forks 14
Handler reaped child 23240
Handler reaped child 23241
[프로그램이 멈춤]
분석:
- 5개 자식 생성
- 2개만 수거됨
- 나머지 3개는 좀비 프로세스
- ccount = 3
- 무한 루프 발생
좀비 프로세스
정의
죽었지만 부모가 수거하지 않은 프로세스
확인 방법
$ 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 사용 (버그):
if ((pid = wait(NULL)) > 0) { // 1개만 수거
ccount--;
}
// 5개 종료 → 1개만 수거
// 나머지 4개는 좀비
while 사용 (정상):
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으로 복원)
실행 결과
수정 후 출력
> ./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() 래퍼 함수
전체 코드
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);
최종 정리
문제점
- signal() 함수는 Unix마다 다름
- 핸들러 자동 해제될 수 있음
- 시스템 콜 재시작 안 될 수 있음
- 시그널 차단 안 될 수 있음
해결책
Signal() 래퍼 함수:
- sigaction 사용
- SA_RESTART 설정
- 일관된 동작 보장
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 보호 → 저장/복원 필수
'concept' 카테고리의 다른 글
| [운영체제론] 가상 메모리 (0) | 2025.10.31 |
|---|---|
| [운영체제론] Dynamic Memory Allocation (0) | 2025.10.29 |
| 컴퓨터 시스템의 메모리 계층 및 가상 메모리 구조 (0) | 2025.10.17 |
| 운영체제 프로세스 개념 정리 (0) | 2025.10.17 |
| x86-64 레지스터의 자동 Zero-Extension 규칙 (0) | 2025.10.05 |