csapp 8장
그 중 .5 다
Linux 시그널 : 예외적 제어 흐름으로 프로세스1와 커널2이 다른 프로세스를 중단하게 해준다
시그널은 시스템에서 어떤 종류의 이벤트가 발생했음을 프로세스에 알리는 작은 메시지로 리눅스 시스템에서 30가지 유형이 있다
저수준에서 일어나는 하드웨어 예외는 우리가 볼 수 없지만 시그널은 이를 보이게 해준다
그 상황에 걸맞는 간단한 시그널을 보내 말이다
보내는 상황
시스템 이벤트 감지했을때 보낸다
kill
함수를 호출해 명시적 요청을 받았을 때
자기 자신한테도 보낼 수 있다
보냈지만 아직 수신되지 않은 시그널을 펜딩 시그널이라 한다
펜딩 시그널은 최대 하나만 존재 가능하다
이미 펜딩 시그널이 존재하면 그 이후의 시그널들은 대기 없이 바로 버려진다
이를 통해 특정 시그널들의 수신을 선택적으로 차단 가능하다
시그널을 보내기 위한 여러 메커니즘이 있는데 이 메커니즘들은 모두 프로세스 그룹(process group) 개념에 의존한다
모든 프로세스는 정확히 하나의 프로세스 그룹에 속하며, 이는 양의 정수인 프로세스 그룹 ID로 식별된다
기본적으로, 자식 프로세스는 부모와 같은 프로세스 그룹에 속한다
/bin/kill
프로그램으로 시그널 보내기/bin/kill
프로그램은 임의의 시그널을 다른 프로세스에 보낸다
음수를 사용하면 프로세스가 아닌 프로세스 그룹 ID로 해석해 그 그룹의 모든 프로세스에 시그널을 보낸다
잡(job): 셸4이 한 커맨드 라인으로 만든 프로세스 묶음이며, 각 잡은 하나의 프로세스 그룹(PGID) 으로 관리됨 (PGID는 보통 그룹 리더의 PID)
포그라운드/백그라운드: 한 번에 포그라운드 잡은 1개, 백그라운드 잡은 0개 이상 존재
파이프라인5 예시: ls | sort
→ 두 프로세스가 한 포그라운드 잡/그룹을 이룸
키보드 시그널은 TTY6의 포그라운드 프로세스 그룹 전체로 전달:
kill
함수로 시그널 보내기프로세스는 (자기 자신을 포함한) 다른 프로세스에 시그널을 보내기 위해 kill
함수를 호출한다
pid
값이 음수냐 양수냐 0이냐에 따라 시그널을 어디에 보낼지 정한다
양수면 시그널을 pid
프로세스에
0이면 호출한 프로세스가 속한 프로세스 그룹의 모든 프로세스에(자신 포함)에 시그널을
음수면 pid
프로세스 그룹 전부의 프로세스에게 시그널을 보낸다
예시 코드 ```c #include <sys/types.h> #include
int kill(pid_t pid, int sig); /* Returns: 0 if OK, −1 on error */ ```
alarm
함수로 시그널 보내기프로세스는 alarm
함수 호출 가능
alarm
호출은 보류중인 알람을 취소하고 취소된 알람의 남은 초를 반환
보류 중인 알람이 없었으면 0을 반환
커널1이 사용자 모드로 돌아가기 전, 차단되지 않은 보류 중 시그널 집합을 점검
비어있으면 다음 명령 실행
비어있지 않으면 한 신호 k를 강제로 수신
시그널 별 정의된 기본동작 존재
프로세스 종료
프로세스 종료 후 코어 덤프 남김
프로세스가 SIGCONT 시그널에 의해 재시작될 때까지 정지
프로세스가 시그널 무시
시그널 signum
에 대한 동작 변경
signum
유형 시그널 무시
signum
유형의 시그널 동작 기본 동작으로 되돌리기
그 외 사용자 핸들러 설치
SIGSTOP, SIGKILL은 포착/무시/변경 불가
반환값: 이전 핸들러 포인터 또는 SIG_ERR
(오류)
포착 시 핸들러(signum 인자 전달) 가 실행되어 시그널 유형을 구분 가능
보통 return
시 중단 지점으로 복귀하나, 일부 시스템에서는 중단된 시스템 콜이 즉시 오류로 반환될 수 있음
실행 중인 핸들러7 S가 또 다른 시그널 t로 T에 의해 중단될 수 있음
→ T 종료 후 S 재개
→ S 종료 후 메인 프로그램 재개
시그널 블록에 두 가지 메커니즘이 있다
암묵적 차단 메커니즘
명시적 차단 메커니즘
sigprocmask
함수와 그 보조 함수들로 선택한 시그널들을 명시적으로 차단/차단 해제 가능시그널 핸들러 작성하는 법 나온다
G0 최소화: 핸들러는 작게—전역 플래그만 set하고 곧바로 return; 실제 처리는 메인 루프에서
G1 안전한 함수만: 핸들러에서는 async-signal-safe 함수만 호출(예: write
, _exit
, sigaction
등)
printf/malloc/exit
금지
→ 필요하면 Sio(write 기반) 같은 안전 I/O 사용
G2 errno
보존: 핸들러 입구에서 int saved=errno;
저장 → 출구에서 복원(핸들러가 _exit
로 끝나면 불필요)
G3 공유 데이터 보호: 전역/공유 자료구조 접근은 일시적 시그널 차단(sigprocmask
)으로 감싸서 중단 불가 구역 보장
G4 volatile
: 핸들러와 메인이 공유하는 전역 변수는 volatile
로 선언 (레지스터 캐싱 방지)
G5 플래그는 sig_atomic_t
: 단일 읽기/쓰기의 원자성을 보장하는 타입 사용
volatile sig_atomic_t flag;
(단, flag++
같은 복합 갱신은 원자 아님)
신호는 큐잉되지 않음: 보류 집합은 타입당 1비트
같은 타입이 차단 중에 여러 번 오면 중복은 버려짐
→ “도착 횟수 세기” 용도로 쓰지 말 것
SIGCHLD 예시: 자식이 3번 종료해도 핸들러가 실행 중이면 두 번째만 보류, 세 번째는 소실
→ 좀비 남음
해법: 핸들러에서 while (waitpid(-1, NULL, …) > 0)
루프로 모든 자식을 한 번에 수거(보통 WNOHANG
옵션 권장)
signal()
의미론 차이: 일부 시스템은 핸들러 1회 실행 뒤 자동 기본값 복구
느린 시스템 콜: 일부 시스템에서 시그널로 중단 후 즉시 EINTR
로 실패(자동 재시작 없음)
sigaction
사용 권장: 의미론을 명시. 실전에서는 래퍼 Signal
을 써서
현재 처리 유형만 차단,
가능하면 SA_RESTART
로 중단된 시스템 콜 자동 재시작,
핸들러는 재설치 없이 지속되도록 설정
동시성은 상당히 중요하고 어렵기에 12장에서 제대로 다루고 지금은
예외적 제외흐름과 함께하는 겉핥기 정도다
예시로 셸과 같은 자식을 계속 만들고 잡 리스트를 관리하려다, 시그널 타이밍 때문에 deletejob
가 addjob
보다 먼저 실행될 수 있는 레이스를 보여준다
레이스 : 동기화 오류로 공유 자원에 여러 프로세스나 스레드가 접근하려 할때 발생한다
addjob
, deletejob
함수는 잡 리스트에 항목을 추가/삭제 한다
그리고 이러한 레이스 오류가 생기는 이유와 해결법 나온다
자식이 매우 빨리 종료하면 SIGCHLD
가 먼저 도착해 핸들러의 delete가 add보다 앞섬
→ 잡 리스트에 유령 항목 남음
fork
전 SIGCHLD
차단
→ fork
→ addjob 먼저
→ 차단 해제(보류된 신호는 해제 후 처리)
부모의 차단 상태 상속 → exec
전 SIGCHLD 해제
호출될 때마다 모든 종료 자식을 루프로 수거, 공유 리스트 수정 구간만 잠시 차단, errno
저장/복원, 본문은 짧고 단순
“add → delete” 순서를 차단/해제로 강제하고,
비큐잉 신호 특성을 감안해 루프 수거로 정확성 확보
명시적으로 시그널을 기다려야 할 때가 있다
바로
전면 작업을 생성할 때, 커널1은 이 작업이 종료되고 SIGHLD 핸들러에 의해 삭제될 때까지 기다려야만 한다
이에 대해서도 대안이 있다
스핀 루프 :
신호를 기다릴 때 잠들지 않고 계속 플래그 확인
자원을 많이 먹어 비효율적
pause
를 섞음 :
조건 검사 마지막 루프가 됨 while (pid == 0)
가 참
pause()
를 호출직전에 핸들러가 pid ≠ 0
으로 갱신하고 복귀
pause()
는 이미 호출되었지만 더받을 신호가 없음
무기한 대기
sleep
/nanosleep
섞기 :
얼마만큼 잘지 근거가 없음
짧으면 짧은대로 자원낭비
길면 긴대로 시간낭비
sigsuspend
SIGCHLD를 차단한 채로 자식 생성
전역 플래그(예: pid = 0
) 초기화 후
sigsuspend
로 잠깐 SIGCHLD만 허용하고 잠듦
자식이 끝나면 SIGCHLD 핸들러7가 플래그 갱신
→ sigsuspend가 깨고 돌아옴
원래 차단 상태가 자동 복원
→ 필요한 작업 하고 다음 라운드로
sigsuspend
는 항상 -1로 반환(보통 errno=EINTR) → 정상, 깨어났다는 뜻
핵심은 “SIGCHLD만 잠깐 허용하고 안전하게 잠들기” 로,
조건 확인 ↔ 잠들기 사이 레이스가 없다는 것
프로세스: 실행 중인 프로그램으로, 독립된 메모리와 상태를 가진 수행 단위 ↩
컨텍스트: 프로세스를 중단·재개하기 위해 커널이 보관하는 실행 상태의 묶음(레지스터, PC/SP, 시그널 마스크·대기 목록, 메모리/열린 파일 등) ↩
셸(Shell): 사용자 명령을 해석해 프로그램을 실행하고 입출력·잡 제어를 중개하는 명령줄 인터프리터 ↩
파이프라인(Pipeline): 프로세스들을 연결해 앞의 표준출력을 다음의 표준입력으로 넘기는 실행 구조 ↩
TTY: 터미널 장치(제어 터미널)로, 키보드/화면 I/O와 포그라운드 프로세스 그룹을 커널이 관리하는 인터페이스 ↩