ECF는 프로그램의 내부 변수로는 알 수 없는 시스템 상태의 변화에 대응하기 위해 발생하는 제어 흐름의 급격한 변화를 의미합니다.
프로세서에 전원이 들어온 순간부터 꺼질 때까지, 프로그램 카운터(Program Counter)는 명령어 주소의 연속인 값을 가집니다.
시스템은 프로그램 내부 상태 변화뿐만 아니라, 프로그램 실행과 직접 관련 없는 시스템 상태(system state)의 변화에도 반응해야 합니다.
프로그래머가 ECF를 이해해야 하는 이유는 다음과 같습니다.
try-catch-throw와 같은 고수준 소프트웨어 예외는 C언어의 setjmp, longjmp 함수와 같은 저수준 비지역 점프(ECF의 한 형태)를 통해 그 구현 원리를 파악할 수 있습니다.예외는 하드웨어와 운영체제가 함께 구현하는 예외 제어 흐름(ECF)의 한 형태입니다. 세부 사항은 시스템마다 다르지만 기본 개념은 동일합니다.

I_curr를 실행하는 도중, 프로세서 내부의 비트나 신호로 인코딩된 상태에 중요한 변화가 발생하는 것을 이벤트(event)라고 합니다.
이벤트는 현재 실행 중인 명령어와의 관련성에 따라 나눌 수 있습니다.
예외 핸들러가 처리를 완료하면, 예외를 발생시킨 이벤트의 종류에 따라 다음 세 가지 중 하나의 동작을 수행합니다.
I_curr로 돌려줍니다. (이벤트가 발생했을 때 실행 중이던 명령어)I_next로 돌려줍니다. (예외가 없었다면 다음에 실행되었을 명령어)예외 처리는 하드웨어와 소프트웨어(OS)의 긴밀한 협력을 통해 이루어지므로, 각 구성 요소의 역할을 명확히 이해하는 것이 중요합니다.
k번째 항목에는 k번 예외를 처리하는 핸들러의 주소가 저장됩니다.

k를 결정합니다.k번째 항목을 통해 간접 프로시저 호출(indirect procedure call)을 수행하여 예외를 발생시킵니다.호출할 함수의 주소를 메모리나 레지스터에서 읽어와 실행하는 방식입니다.
우리가 일반적으로 사용하는 함수 호출은 직접(Direct) 호출입니다.
call function_Afunction_A의 주소가 결정되어, 기계어 코드에 그 주소가 그대로 박혀있습니다. CPU는 그냥 그 주소로 점프하면 됩니다.반면, 간접(Indirect) 호출은 한 단계를 더 거칩니다.
call [0x1000]예외 처리가 바로 이 방식입니다. CPU는 예외 핸들러의 주소를 직접 알지 못합니다. 대신 (예외 테이블 베이스 레지스터 값) + (예외 번호) 계산을 통해 예외 테이블의 특정 위치로 찾아가, 거기 저장된 핸들러의 주소를 읽어온 뒤 그 주소로 점프(호출)합니다.
예외 처리는 프로시저 호출과 유사하지만 다음과 같은 중요한 차이점이 있습니다.
I_curr)가 될 수도 있고, 다음에 실행될 다음 명령어(I_next)가 될 수도 있습니다.iret 명령어): ISR의 모든 작업이 끝나면, 일반 ret이 아닌 특수 명령어인 iret (Interrupt Return)을 실행합니다.iret 명령어는 하드웨어에게 커널 스택에 저장해 두었던 상태(사용자 스택 포인터, 복귀 주소 등)를 다시 CPU 레지스터로 복원하라고 지시합니다. 이 과정에서 CPU는 자동으로 커널 모드에서 사용자 모드로 다시 전환됩니다.예외는 그 특성에 따라 인터럽트(interrupts), 트랩(traps), 폴트(faults), 그리고 어보트(aborts)의 네 가지 클래스로 나눌 수 있습니다.
이어지는 내용은 각 클래스의 속성을 요약한 표(Figure 8.4)를 바탕으로 설명됩니다.

I_next)로 복귀합니다.open(), read(), fork() 같은 함수를 호출하면, 프로그램은 스스로 트랩을 발생시켜 커널 모드로 전환하고 운영체제의 서비스를 요청합니다. 디버깅을 위한 breakpoint도 트랩의 일종입니다.I_next)로 복귀합니다.I_curr)를 재실행하기 위해 복귀합니다. 해결할 수 없는 문제라면 프로그램을 종료시킵니다.인터럽트는 프로세서 외부의 I/O 장치로부터 오는 신호에 의해 비동기적으로(asynchronously) 발생하는 예외입니다.
I_next)로 넘어갑니다. (즉, 인터럽트가 없었다면 실행되었을 바로 그 다음 명령어)💡 동기적 예외 vs 비동기적 예외
- 인터럽트는 I/O 장치에 의해 비동기적으로 발생합니다.
- 나머지 예외들(트랩, 폴트, 어보트)은 현재 실행 중인 명령어, 즉 폴트 유발 명령어(faulting instruction)의 결과로 동기적으로(synchronously) 발생합니다.
I_next)로 돌려줍니다.트랩의 가장 중요한 용도는 사용자 프로그램과 커널 사이에 프로시저와 유사한 인터페이스를 제공하는 것인데, 이를 시스템 콜이라고 합니다.
read), 새 프로세스 생성(fork), 현재 프로세스 종료(exit) 등 커널의 서비스가 필요한 경우가 많습니다. 시스템 콜은 이러한 커널 서비스에 대한 통제된 접근을 허용하기 위한 메커니즘입니다.
n을 요청하기 위해 특별한 syscall n 명령어를 실행합니다.syscall 명령어 실행은 트랩을 발생시켜 예외 핸들러를 호출합니다.n)를 해석하여 해당하는 커널 루틴(kernel routine)을 호출하고 서비스를 수행합니다.프로그래머 관점에서는 시스템 콜이 일반 함수 호출과 동일하게 보이지만, 내부 구현 방식은 매우 다릅니다.
폴트는 핸들러가 복구할 수도 있는 오류(potentially recoverable error)로 인해 발생합니다.

I_curr)로 돌려주어 재실행하게 합니다.페이지 폴트는 폴트의 가장 대표적인 예시입니다.
어보트는 복구 불가능한 치명적인 오류(unrecoverable fatal error)로 인해 발생하며, 주로 하드웨어 오류가 원인입니다.

- 어보트 핸들러는 제어권을 **절대 애플리케이션으로 돌려주지 않습니다.**
- 핸들러는 애플리케이션을 종료시키는 **중단 루틴(abort routine)**으로 제어를 넘깁니다.

x86-64 시스템에는 최대 256개의 서로 다른 예외 유형이 정의되어 있습니다.
Linux는 파일 읽기/쓰기, 프로세스 생성 등 커널 서비스를 애플리케이션이 요청할 때 사용하는 수백 개의 시스템 콜을 제공합니다.
각 시스템 콜은 커널 내 점프 테이블의 오프셋에 해당하는 고유한 정수 번호를 가집니다.
printf, read)syscall 명령어)syscall이라는 트랩 명령어를 통해 시스템 콜을 직접 호출할 수 있습니다.syscall 명령어 호출 규약Linux 시스템 콜의 모든 인자는 스택이 아닌 범용 레지스터를 통해 전달됩니다.
%rax: 호출할 시스템 콜의 번호를 저장합니다.%rdi, %rsi, %rdx, %r10, %r8, %r9: 최대 6개의 인자를 순서대로 저장합니다. (첫 번째 인자는 %rdi, 두 번째는 %rsi...)%rax 레지스터에 결과값이 저장됩니다. -4095에서 -1 사이의 음수 값은 오류가 발생했음을 의미합니다.hello 프로그램
제공된 어셈블리 코드는 C 라이브러리 함수(printf) 없이 syscall 명령어로 write와 _exit 시스템 콜을 직접 호출하여 "hello, world"를 출력합니다.
write 함수 호출 (lines 9-13)movq $1, %rax: write 시스템 콜의 번호인 1을 %rax에 저장합니다.movq $1, %rdi: 첫 번째 인자인 stdout의 파일 디스크립터(1)를 %rdi에 저장합니다.movq $string, %rsi: 두 번째 인자인 출력할 문자열의 주소를 %rsi에 저장합니다.movq $len, %rdx: 세 번째 인자인 문자열의 길이를 %rdx에 저장합니다.syscall: 시스템 콜을 실행하여 커널에 트랩을 겁니다._exit 함수 호출 (lines 14-16)movq $60, %rax: _exit 시스템 콜의 번호인 60을 %rax에 저장합니다.movq $0, %rdi: 첫 번째 인자인 종료 상태(0)를 %rdi에 저장합니다.syscall: 시스템 콜을 실행하여 프로그램을 종료합니다.지금까지 하드웨어와 소프트웨어가 협력하는 저수준(low-level) 예외 메커니즘과, 이를 이용한 문맥 교환(context switch)을 살펴보았습니다. 이제 더 높은 수준(higher-level)의 소프트웨어 예외 제어 흐름인 Linux 시그널(signal)에 대해 알아보겠습니다. 시그널은 프로세스와 커널이 다른 프로세스를 중단시킬 수 있도록 허용합니다.
각 시그널 유형은 특정 시스템 이벤트에 해당합니다.
Ctrl+C를 입력하면, 커널이 해당 포그라운드 프로세스 그룹의 각 프로세스에 이 시그널을 보냅니다.
시그널이 목적지 프로세스로 전달되는 과정은 두 가지 명확한 단계로 이루어집니다.
시그널 전송이란 커널이 목적지 프로세스의 컨텍스트(context)에 있는 특정 상태를 업데이트하여 시그널을 전달(deliver)하는 것을 의미합니다. 시그널이 전달되는 이유는 두 가지입니다.
kill 함수를 호출하여, 커널에게 특정 프로세스로 시그널을 보내달라고 명시적으로 요청했을 때. (프로세스는 자기 자신에게 시그널을 보낼 수도 있습니다.)시그널 수신이란 목적지 프로세스가 커널에 의해 강제로 시그널 전달에 대한 반응을 보이는 것을 의미합니다. 프로세스는 다음 세 가지 방식으로 반응할 수 있습니다.
k 타입의 대기 시그널이 이미 있는데, 또 다른 k 타입 시그널이 전송되면 그 시그널은 큐에 쌓이지 않고 그냥 버려집니다(discarded).pending 비트 벡터: 대기 중인 시그널 집합을 관리합니다. k 타입 시그널이 전달되면 k번째 비트를 설정(set)하고, 수신되면 해제(clear)합니다.blocked 비트 벡터: 블록된 시그널 집합을 관리합니다.Unix 시스템은 프로세스에 시그널을 보내기 위한 여러 메커니즘을 제공합니다. 이 모든 메커니즘은 프로세스 그룹(process group)이라는 개념에 의존합니다.
모든 프로세스는 정확히 하나의 프로세스 그룹에 속하며, 이 그룹은 양의 정수인 프로세스 그룹 ID (process group ID)로 식별됩니다.
getpgrp 함수pid_t getpgrp(void);setpgid 함수를 사용하여 자신 또는 다른 프로세스의 프로세스 그룹을 변경할 수 있습니다.setpgid 함수int setpgid(pid_t pid, pid_t pgid);pid로 지정된 프로세스의 프로세스 그룹을 pgid로 변경합니다.pid가 0이면, 현재 프로세스의 PID가 사용됩니다.pgid가 0이면, pid로 지정된 프로세스의 PID가 프로세스 그룹 ID로 사용됩니다.setpgid(0, 0);을 호출하면, 이는 자신의 PID를 자신의 프로세스 그룹 ID로 사용하는 새로운 프로세스 그룹을 생성하는 것과 같습니다. 이 프로세스는 새로운 프로세스 그룹의 리더가 됩니다./bin/kill 프로그램을 이용한 시그널 전송/bin/kill 프로그램은 다른 프로세스에게 임의의 시그널을 보낼 수 있습니다.
linux> /bin/kill -9 15213SIGKILL)을 보냅니다.linux> /bin/kill -9 -15213SIGKILL 시그널을 보냅니다.참고: 쉘에 내장된 kill 명령어와 구분하기 위해 전체 경로(/bin/kill)를 사용하기도 합니다.
Unix 쉘은 하나의 명령 라인을 실행하며 생성된 프로세스들을 잡(job)이라는 단위로 관리합니다.

ls | sort 명령어는 ls와 sort 두 개의 프로세스로 구성된 하나의 포그라운드 잡을 생성하며, 이 두 프로세스는 같은 프로세스 그룹에 속하게 됩니다.Ctrl+C: 키보드로 Ctrl+C를 입력하면, 커널은 포그라운드 프로세스 그룹에 속한 모든 프로세스에게 SIGINT 시그널을 보냅니다. 기본 동작은 포그라운드 잡을 종료(terminate)시키는 것입니다.Ctrl+Z: 키보드로 Ctrl+Z를 입력하면, 커널은 포그라운드 프로세스 그룹에 속한 모든 프로세스에게 SIGTSTP 시그널을 보냅니다. 기본 동작은 포그라운드 잡을 정지(stop/suspend)시키는 것입니다.kill 함수를 이용한 시그널 전송프로세스는 kill 함수를 호출하여 다른 프로세스(자기 자신 포함)에게 시그널을 보낼 수 있습니다.

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
// 성공 시 0, 오류 시 -1 반환
pid 값에 따른 동작:pid > 0: pid로 지정된 특정 프로세스에게 sig 시그널을 보냅니다.pid == 0: 호출한 프로세스가 속한 프로세스 그룹 내의 모든 프로세스에게 sig 시그널을 보냅니다.pid < 0: 프로세스 그룹 ID가 |pid| (pid의 절댓값)인 그룹 내의 모든 프로세스에게 sig 시그널을 보냅니다.alarm 함수를 이용한 시그널 전송프로세스는 alarm 함수를 호출하여 자기 자신에게 SIGALRM 시그널을 보낼 수 있습니다.
#include <unistd.h>
unsigned int alarm(unsigned int secs);
// 이전 알람의 남은 시간을 반환, 이전 알람이 없었다면 0을 반환
alarm(secs)를 호출하면, 커널은 secs초 후에 호출한 프로세스에게 SIGALRM 시그널을 보내도록 예약합니다.secs가 0이면, 새로운 알람은 예약되지 않고 기존의 예약된 알람이 취소됩니다.alarm 함수를 호출하면 이전에 예약된 알람은 항상 취소됩니다.커널이 프로세스 p를 커널 모드에서 사용자 모드로 전환할 때마다(예: 시스템 콜에서 복귀하거나 문맥 교환이 완료될 때), 해당 프로세스의 블록되지 않은 대기 시그널 집합(pending & ~blocked)을 확인합니다.
p의 논리적 제어 흐름상 다음 명령어(I_next)로 넘깁니다.k(보통 가장 작은 번호의 시그널)를 선택하여 프로세스 p가 해당 시그널을 수신하도록 강제합니다. 시그널 수신은 프로세스의 특정 행동을 유발하며, 그 행동이 완료되면 제어권은 다시 다음 명령어(I_next)로 넘어갑니다.각 시그널 유형에는 미리 정의된 기본 처리 행동(default action)이 있으며, 다음 중 하나에 해당합니다.
SIGCONT 시그널을 받을 때까지 프로세스를 정지(stop/suspend)시킨다.예를 들어, SIGKILL의 기본 행동은 프로세스 종료이고, SIGCHLD의 기본 행동은 무시입니다. 프로세스는 signal 함수를 사용하여 이러한 기본 행동을 수정할 수 있습니다. 단, SIGSTOP과 SIGKILL의 기본 행동은 절대 변경할 수 없습니다.
코어 덤프(Core Dump)는 프로그램이 비정상적으로 종료될 때, 그 순간의 메모리 상태를 그대로 복사해서 저장해 놓은 파일입니다. 주로 디버깅 목적으로 사용됩니다.
코어 덤프의 유일한 목적은 사후 디버깅(post-mortem debugging)입니다. 프로그램이 왜 죽었는지 원인을 찾기 위해 만들어집니다. 이 파일 안에는 프로그램이 충돌한 순간의 거의 모든 정보가 담겨 있습니다.
코어 덤프는 프로그램의 메모리 전체를 복사하기 때문에 파일 크기가 매우 클 수 있습니다. 이 때문에 현대의 많은 운영체제에서는 기본적으로 코어 덤프 생성을 비활성화해두는 경우가 많습니다. ulimit -c unlimited 같은 명령어를 사용해 코어 덤프 파일의 최대 크기 제한을 풀어야 생성되기도 합니다.
signal 함수를 이용한 처리 행동 변경signal 함수는 특정 시그널(signum)에 대한 처리 행동을 변경합니다.

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// 성공 시 이전 핸들러의 포인터 반환, 오류 시 SIG_ERR 반환
handler 인자에 따른 세 가지 동작:SIG_IGN: signum 타입의 시그널을 무시합니다.SIG_DFL: signum 타입의 시그널에 대한 행동을 기본값으로 복원합니다.signum 타입의 시그널을 수신할 때마다 시그널 핸들러(signal handler)라고 불리는 사용자 정의 함수를 호출합니다.signal 함수에 핸들러의 주소를 전달하여 기본 행동을 변경하는 것을 의미합니다.핸들러가 실행을 마치고 반환되면, 제어권은 일반적으로 시그널에 의해 중단되었던 바로 그 지점의 명령어로 돌아갑니다. (단, 일부 시스템에서는 중단된 시스템 콜이 오류를 반환하며 즉시 복귀하기도 합니다.)
시그널 핸들러의 실행은 다른 시그널에 의해 중단될 수 있습니다.

예를 들어, 메인 프로그램이 s 시그널을 받아 핸들러 S를 실행하던 중, 다른 시그널 t를 받게 되면 S의 실행이 중단되고 핸들러 T가 실행됩니다. T가 반환되면 S가 중단되었던 지점부터 실행을 재개하고
Linux는 시그널을 블록하기 위한 암묵적 메커니즘과 명시적 메커니즘을 제공합니다.
s 시그널을 받아 핸들러 S를 실행하는 중에 또 다른 s 시그널이 도착하면, 그 시그널은 pending 상태가 되지만 핸들러 S가 반환될 때까지 수신되지 않습니다. 이는 핸들러가 자기 자신에 의해 중단되는 것을 방지합니다.sigprocmask 함수와 관련 함수들을 사용하여 특정 시그널을 명시적으로 블록하거나 언블록할 수 있습니다.sigprocmask 함수와 시그널 집합sigprocmask 함수는 현재 블록된 시그널의 집합(blocked 비트 벡터)을 변경합니다.
#include <signal.h>
// 핵심 함수: 시그널 마스크를 변경
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// 시그널 집합(sigset_t)을 조작하는 헬퍼 함수들
int sigemptyset(sigset_t *set); // 집합을 비움
int sigfillset(sigset_t *set); // 모든 시그널을 집합에 추가
int sigaddset(sigset_t *set, int signum); // 특정 시그널을 추가
int sigdelset(sigset_t *set, int signum); // 특정 시그널을 제거
int sigismember(const sigset_t *set, int signum); // 멤버인지 확인
how 인자에 따른 동작:SIG_BLOCK: set에 포함된 시그널들을 현재 blocked 집합에 추가합니다 (blocked = blocked | set).SIG_UNBLOCK: set에 포함된 시그널들을 현재 blocked 집합에서 제거합니다 (blocked = blocked & ~set).SIG_SETMASK: 현재 blocked 집합을 set으로 완전히 교체합니다 (blocked = set).oldset 인자:oldset이 NULL이 아니면, 변경하기 이전의 blocked 비트 벡터 값이 oldset에 저장됩니다. 이는 나중에 원래 상태로 복원할 때 유용합니다.제공된 코드는 sigprocmask를 사용하여 SIGINT 시그널의 수신을 일시적으로 차단하는 방법을 보여줍니다.

sigemptyset(&mask);: mask라는 시그널 집합을 비웁니다.sigaddset(&mask, SIGINT);: mask 집합에 SIGINT 시그널을 추가합니다. 이제 mask는 SIGINT만 포함한 집합이 됩니다.sigprocmask(SIG_BLOCK, &mask, &prev_mask);:SIG_BLOCK을 사용하여 mask에 있는 SIGINT를 현재 프로세스의 블록 목록에 추가합니다.prev_mask에 저장됩니다.SIGINT에 의해 중단되지 않는 임계 구역(critical section)이 됩니다.sigprocmask(SIG_SETMASK, &prev_mask, NULL);:SIG_SETMASK를 사용하여 이전에 저장해 둔 prev_mask로 블록 목록을 완전히 복원합니다.SIGINT의 블록이 해제되고, 프로세스는 원래의 시그널 수신 상태로 돌아갑니다.시그널 핸들링은 Linux 시스템 수준 프로그래밍에서 가장 까다로운 부분 중 하나입니다.
핸들러는 다음과 같은 속성 때문에 논리적으로 추론하기 어렵습니다.
이 섹션에서는 이러한 문제들을 해결하고, 안전하고 정확하며 이식성 있는 시그널 핸들러를 작성하기 위한 기본 지침을 제공합니다.
시그널 핸들러가 까다로운 주된 이유는 메인 프로그램 및 다른 핸들러와 동시적으로 실행될 수 있기 때문입니다. 만약 핸들러와 메인 프로그램이 동일한 전역 자료구조에 동시에 접근하면, 그 결과는 예측 불가능하며 종종 치명적인 오류로 이어질 수 있습니다.
이러한 지침을 무시하면, 대부분의 경우에는 올바르게 작동하지만 아주 가끔 예측 불가능하고 재현할 수 없는 방식으로 실패하는 미묘한 동시성 오류가 발생할 위험이 있습니다. 이런 오류는 디버깅하기가 극도로 어렵습니다.
핸들러는 가능한 한 작고 단순하게 만드는 것이 가장 안전합니다. 예를 들어, 핸들러는 전역 플래그(global flag)만 설정하고 즉시 반환하고, 실제 시그널 처리는 주기적으로 플래그를 확인하는 메인 프로그램이 담당하도록 하는 것이 좋습니다.


시그널 핸들러 내에서는 비동기-시그널-안전 함수만 호출해야 합니다. 안전한 함수는 재진입 가능(reentrant)하거나 시그널 핸들러에 의해 중단되지 않는 속성을 가집니다.
printf, sprintf, malloc, exit와 같이 널리 쓰이는 많은 함수들은 안전하지 않습니다.write 시스템 콜을 직접 사용해야 합니다. CSAPP 라이브러리는 이를 위해 Sio (Safe I/O) 패키지를 제공합니다. (sio_puts, sio_putl)exit 대신 안전한 버전인 _exit를 사용해야 합니다.errno를 저장하고 복원하라많은 비동기-시그널-안전 함수들은 오류 발생 시 errno 전역 변수를 설정합니다. 핸들러 내에서 이런 함수를 호출하면 메인 프로그램의 errno 값에 영향을 줄 수 있습니다.
errno 값을 지역 변수에 저장하고, 핸들러가 반환하기 직전에 원래 값으로 복원해야 합니다.핸들러가 메인 프로그램이나 다른 핸들러와 전역 자료구조를 공유한다면, 해당 자료구조에 접근하는 동안에는 모든 시그널을 일시적으로 블록해야 합니다. 이는 여러 명령어에 걸쳐 자료구조를 수정하는 도중에 핸들러가 끼어들어 데이터가 깨지는 상태(inconsistent state)를 방지합니다.
volatile로 선언하라최적화 컴파일러는 메인 프로그램에서 값이 변하지 않는 것처럼 보이는 전역 변수를 레지스터에 캐싱할 수 있습니다. 이렇게 되면 메인 프로그램은 핸들러가 수정한 최신 값을 메모리에서 읽지 못하게 됩니다.
volatile 키워드와 함께 선언하면(volatile int g;), 컴파일러에게 해당 변수를 캐싱하지 말고 매번 메모리에서 직접 읽도록 강제할 수 있습니다.sig_atomic_t로 선언하라핸들러가 플래그를 설정하고 메인 프로그램이 이를 읽는 일반적인 설계에서, C는 sig_atomic_t라는 특별한 정수 타입을 제공합니다.
flag++와 같이 여러 명령어가 필요한 연산에는 적용되지 않습니다.volatile sig_atomic_t flag; (volatile과 함께 사용하는 것이 일반적입니다.)위에 제시된 지침들은 보수적이며 항상 엄격하게 필요한 것은 아닐 수 있습니다. 하지만 반례를 증명하기는 매우 어렵기 때문에, 핸들러를 최대한 단순하게 유지하고, 안전 함수를 호출하며, errno를 저장/복원하고, 공유 데이터 접근을 보호하는 보수적인 접근법을 따르는 것이 권장됩니다.
시그널의 비직관적인 측면 중 하나는 대기 중인(pending) 시그널이 큐에 저장되지 않는다는 것입니다.
pending 비트 벡터는 각 시그널 종류마다 단 하나의 비트만 가지고 있습니다.SIGCHLD 핸들러가 실행 중이어서 SIGCHLD 시그널이 블록된 상태일 때, 두 번째 SIGCHLD가 도착하면 pending 비트가 1이 됩니다. 그러나 세 번째 SIGCHLD가 도착하면, 이미 pending 비트가 1이므로 이 시그널은 그냥 버려집니다.signal1)문제 상황: 부모 프로세스가 여러 자식 프로세스를 생성하고, 자식이 종료될 때마다 발생하는 SIGCHLD 시그널을 핸들러로 처리하여 자식을 수확(reap)하려고 합니다.
SIGCHLD 시그널 하나가 자식 하나의 종료에 해당한다고 가정하고, 핸들러가 호출될 때마다 자식을 한 명만 수확(wait)합니다.
SIGCHLD 시그널이 부모에게 전달되어 핸들러가 실행됩니다.SIGCHLD 시그널은 암묵적으로 블록됩니다.SIGCHLD 시그널이 전달되지만 블록되어 pending 상태가 됩니다.SIGCHLD가 전달되지만, 이미 같은 종류의 시그널이 pending 상태이므로 이 시그널은 버려집니다.pending 상태의 시그널을 처리하기 위해 핸들러를 두 번째로 실행합니다.pending 상태인 SIGCHLD는 없습니다. 세 번째 자식의 종료에 대한 정보는 영원히 사라집니다.signal2)이 문제를 해결하려면, SIGCHLD 핸들러가 호출될 때마다 수확 가능한 모든 좀비 자식을 수확하도록 수정해야 합니다.

wait (또는 waitpid) 함수를 계속 호출해야 합니다.SIGCHLD 시그널 일부가 버려지더라도, 한 번의 핸들러 실행으로 모든 좀비 자식을 깨끗하게 정리할 수 있습니다.Unix 시그널 핸들링의 또 다른 까다로운 점은 시스템마다 시그널을 처리하는 방식(semantics)이 다르다는 것입니다. 이로 인해 한 시스템에서 잘 동작하는 코드가 다른 시스템에서는 오작동할 수 있습니다.
주요 차이점은 다음과 같습니다.
signal 함수의 동작 방식 차이:k에 대한 핸들러가 실행되고 나면 해당 시그널에 대한 처리 방식이 기본값으로 다시 복원됩니다.signal() 함수를 다시 호출하여 핸들러를 재설치해야 합니다.read, wait, accept와 같이 오랫동안 프로세스를 블록시킬 수 있는 시스템 콜을 느린 시스템 콜(slow system calls)이라고 합니다.errno를 EINTR로 설정합니다.sigaction 함수와 Signal 래퍼이러한 이식성 문제를 해결하기 위해, POSIX 표준은 sigaction 함수를 정의합니다. 이 함수를 사용하면 핸들러를 설치할 때 원하는 시그널 처리 방식을 명확하게 지정할 수 있습니다.
int sigaction(int signum, struct sigaction *act, struct sigaction *oldact);하지만 sigaction 함수는 복잡한 구조체를 직접 설정해야 해서 다루기 어렵습니다. 더 나은 접근법은 sigaction을 대신 호출해주는 Signal이라는 래퍼 함수(wrapper function)를 사용하는 것입니다. (CSAPP 라이브러리에 포함)

Signal 래퍼 함수의 동작 방식Signal 래퍼 함수는 다음과 같이 예측 가능하고 이식성 있는 방식으로 핸들러를 설치해 줍니다.
EINTR 문제를 해결)Signal 함수가 SIG_IGN이나 SIG_DFL 인자와 함께 다시 호출되기 전까지는 계속 설치된 상태를 유지합니다. (핸들러를 재설치할 필요 없음)메인 프로그램과 시그널 핸들러 같은 동시성 흐름(concurrent flows)이 동일한 전역 변수를 공유할 때 발생하는 경쟁 상태(Race Condition)라는 심각한 버그와 그 해결책을 설명합니다.
Unix 쉘과 유사한 프로그램을 예로 들어, 부모 프로세스가 전역 작업 목록(job list)을 사용하여 자식 프로세스를 관리하는 상황을 가정합니다.
addjob: 부모는 fork로 자식을 생성한 후, 이 함수를 호출하여 작업 목록에 자식을 추가합니다.deletejob: 자식이 종료되어 SIGCHLD 시그널을 받으면, 핸들러 내에서 이 함수를 호출하여 작업 목록에서 자식을 제거합니다.이 코드는 겉보기에 문제가 없어 보이지만, 실행 순서에 따라 치명적인 버그가 발생할 수 있습니다.
다음과 같은 최악의 실행 순서(interleaving)가 가능합니다.

fork를 호출하고, 커널은 부모 대신 새로 생성된 자식을 먼저 실행시킵니다.SIGCHLD 시그널을 전달합니다.SIGCHLD 시그널을 먼저 처리하도록 시그널 핸들러를 실행시킵니다.deletejob을 호출하지만, 아직 부모가 자식을 목록에 추가하지 않았기 때문에 아무런 일도 일어나지 않습니다.fork 함수에서 반환되어 addjob을 호출합니다. 이로 인해 이미 존재하지 않는 자식이 작업 목록에 잘못 추가됩니다.이것이 바로 addjob과 deletejob 사이의 경쟁 상태입니다. addjob이 먼저 실행되면(경쟁에서 이기면) 결과는 올바르지만, deletejob이 먼저 호출되면(경쟁에서 지면) 결과는 틀리게 됩니다.
이 경쟁 상태는 시그널 블로킹을 통해 해결할 수 있습니다.

fork를 호출하기 전에, SIGCHLD 시그널을 블록(block)합니다.fork를 호출하고 addjob까지 실행을 마친 후에, SIGCHLD 시그널을 언블록(unblock)합니다.이렇게 하면 addjob이 항상 SIGCHLD 핸들러의 deletejob보다 먼저 실행되는 것이 보장됩니다. 시그널이 블록된 동안 도착한 SIGCHLD는 addjob이 끝난 뒤 언블록하는 순간 처리되기 때문입니다.
주의: 자식 프로세스는 부모의 블록된 시그널 집합을 상속받으므로, 자식은 execve를 호출하기 전에 반드시 블록된 SIGCHLD 시그널을 언블록 해주어야 합니다.
sigsuspend)메인 프로그램이 특정 시그널 핸들러가 실행되기를 효율적이고 안전하게 기다려야 할 때가 있습니다. 단순한 방법들은 리소스를 낭비하거나 심각한 버그를 유발할 수 있으므로, sigsuspend 함수를 사용해야 합니다.
쉘이 자식 프로세스의 종료를 기다리는 상황을 가정할 때, 다음과 같은 잘못된 방법들을 사용할 수 있습니다.
전역 변수 pid를 핸들러가 설정할 때까지 메인 루프가 계속 확인하는 방식입니다.
while (!pid)
; /* Spin loop */
pause 함수 사용 - 경쟁 상태 발생루프 안에 pause 함수를 넣어 리소스 낭비를 줄이려는 시도입니다.
while (!pid) /* Race! */
pause();
while 조건문 확인과 pause() 호출 사이의 틈 때문에 심각한 경쟁 상태(Race Condition)가 발생합니다. 조건 확인 직후 시그널이 도착하면, pause는 영원히 잠들게 될 수 있습니다.sleep 함수 사용 - 비효율적sleep 함수를 사용하여 일정 시간 대기하는 방법입니다.
while (!pid) /* Too slow! */
sleep(1);
sleep 호출 직후 시그널이 오면 불필요하게 오래 기다려야 합니다.sigsuspend 함수이 문제를 해결하기 위한 올바른 방법은 sigsuspend를 사용하는 것입니다.
#include <signal.h>
int sigsuspend(const sigset_t *mask);sigsuspend는 다음 두 가지 작업을 원자적(atomic)으로, 즉 중간에 절대 중단되지 않는 단일 연산으로 수행합니다.mask로 일시적으로 교체합니다.핸들러가 실행되고 반환되면, sigsuspend도 반환되며 블록된 시그널 집합은 sigsuspend 호출 이전의 원래 상태로 자동 복원됩니다. 이 원자성은 pause의 경쟁 상태 문제를 원천적으로 제거합니다.
sigsuspend 사용법올바른 대기 방식은 다음과 같습니다.
SIGCHLD)을 블록합니다.sigsuspend를 호출하면서, 인자로 기다릴 시그널의 블록을 잠시 푸는 임시 마스크를 전달합니다.sigsuspend는 시그널이 올 때까지 프로세스를 효율적으로 잠재웁니다.sigsuspend가 반환되면 시그널은 다시 자동으로 블록됩니다.이 방식은 리소스를 낭비하지 않고, 경쟁 상태를 피하며, 효율적으로 시그널을 기다릴 수 있는 가장 올바른 방법입니다.