본문 바로가기
지식/시스템프로그래밍

Exceptional Control Flow: Signals and Nonlocal Jumps

by 칙칙폭폭 땡땡 2025. 4. 10.
반응형

 

Shells

쉘은 운영체게에서 사용자의 명령을 해석하고 실행하는 명령어 인터프리터

  • init [1] : 시스템 부팅 시 가장 먼저 실행되는 최초의 프로세스
  • Login shell : 사용자가 로그인하면 init 이 로그인 쉘을 실행함.
  • Deamon : 시스템의 백그라운드에서 실행되는 특별한 프로세스

기본적인 쉘 구조

  • 커맨드 라인은 사용자의 명령을 저장할 저장고
  • 프롬프트 표시는 “> “ 로 함.
  • Fgets 를 통해서 사용자 입력 받음
  • 입력 종료 감지하면 쉘 종료
  • 그게 아니면 명령어 실행

eval Function

Copy
void eval(char *cmdline) 
{
    char *argv[MAXARGS]; /* execve()에 전달할 명령어 리스트 */
    char buf[MAXLINE];   /* 명령어를 저장할 버퍼 */
    int bg;              /* 백그라운드 실행 여부 (1이면 백그라운드) */
    pid_t pid;           /* 프로세스 ID */
  • cmdline: 사용자가 입력한 명령어를 저장하는 문자열
  • argv: 명령어를 파싱해서 저장하는 배열
  • bg: 명령어가 백그라운드에서 실행되어야 하는지 나타내는 변수
  • pid: 자식 프로세스의 pid 저장
Copy
    strcpy(buf, cmdline);
    bg = parseline(buf, argv);
    if (argv[0] == NULL)
        return;   /* 빈 명령어는 무시 */
  • strcpy(buf, cmdline);: 명령어를 buf에 복사 (수정 가능하게 만들기 위해)
  • parseline(buf, argv); : 명령어를 argv 배열로 변환 (파싱)
  • if (argv[0] == NULL) return; : 사용자가 Enter을 입력하면 아무 동작도 하지 않고 종료
Copy
    if (!builtin_command(argv)) {
  • builtin_command(argv) : 만약 cd, exit 같은 **내장 명령어(built-in command)**라면 따로 처리
  • 여기서는 내장 명령어가 아니라면, 새로운 프로세스를 생성해서 실행하는 과정으로 넘어감
Copy
        if ((pid = Fork()) == 0) {   /* 자식 프로세스 생성 */
  • Fork()를 호출하여 새로운 자식 프로세스를 생성
  • pid == 0이면 현재 실행 중인 프로세스가 자식 프로세스임을 의미
Copy
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
  • execve(argv[0], argv, environ);
  • if (execve(...) < 0) → 명령어가 존재하지 않으면 오류 메시지 출력 후 종료

execve()는 현재 프로세스를 새로운 프로그램으로 덮어씌우는 역할을 함.

(만약 execve()가 성공하면 이후 코드는 실행되지 않음!)

Copy
        /* 부모 프로세스가 포그라운드(일반 실행) 작업을 기다림 */
        if (!bg) {
            int status;
            if (waitpid(pid, &status, 0) < 0)
                unix_error("waitfg: waitpid error");
        }
  • if (!bg) → 만약 백그라운드 실행(&)이 아니라면 자식 프로세스가 끝날 때까지 기다림
  • waitpid(pid, &status, 0);
포그라운드, 백그라운드
Copy
        else
            printf("%d %s", pid, cmdline);

만약에 커맨드가 백그라운드 실행이엇으면 부모가 기다리지 않고 실행 중인 프로세스의 ID만 출력함.

Problem with Simple Shell Example

포그라운드 작업은 waitpid() 가 사용되어서 자식 프로세스가 종료되면 알아서 reaping 해줌.

하지만 백그라운드 작업은 waitpid() 가 없기 때문에 자식 프로세스가 종료도고 나서 좀비 프로세스가 되어버림.

→ 이게 문제야 문제

이 문제를 Exceptional Control Flow(ECF)로 해결 할 수 있다.

ECF

일반적으로 코드가 위에서 아래로 실행되는데, 예상치 못한 이벤트가 발생하면 일반적인 실행 흐름을 중단하고 특정 처리를 수행함.

대표적으로 운영체제의 Signal

ECF의 동작 방식은

  1. 백그라운드 프로세스가 종료됨
  2. 커널이 부모 프로세스에게 SIGCHILD 시그널을 보냄
  3. 쉘은 시그널을 받아서 waitpid() 를 호출하여 좀비 프로세스를 제거함.
내가 약간 햇갈리는건

결국 백그라운드, 포그라운드 둘다 waitpid() 호출이 필수적이다.

하지만 백그라운드는 자식이 종료될 때(exit(0))을 통헤서 커널이 SIGCHILD 를 부모에게 보내줌.

부모는 시그널 핸들러를 호출해서 좀비 프로세스를 수거함. 여기서 waitpid() 의 pid가 -1을 주로 쓰는데 -1의 의미는 모든 자식 프로세스라는 뜻임.

Signal

시그널은 이벤트가 발생했다고 프로세스에 알리는 메시지다.

예외나 인터럽트랑 비슷함

커널이나 다른 프로세스가 특정 프로세스에 시그널을 보낼 수 있음

시그널에는 ID만 포함되고 다른 데이터는 없음

주요 시그널

Signal Concept

Sending a Signal

커널이 특정 프로세스에 시그널을 보낼 때, 그 프로세스의 시그널 펜딩 목록에 시그널을 추가한다.

이후에 해당 프로세스가 실행되면(컨텍스트 스위칭되어서 CPU를 얻으) 시그널을 처리하도록 한다.

커널은 시스템이 0으로 나누기(SIGFPE)나 자식 프로세스가 종료(SIGCHILD)되는 시스템 이벤트가 발생하면 시그널을 보냄

(SIGFPE 는 0으로 나누기를 시도한 프로세스에 시그널을 보내고, SIGCHILD 는 부모에게 시그널을 전달함.)

또는 다른 프로세스가 kill(pid, signam) 을 시도하면 시그널을 보냄.

(이건 프로세스 아이디가 pid 인 프로세스에 시그널을 보내서 줌. kill() 이라고 다른 프로세스를 항상 종료시키는거 아님!!!!! 시그널을 보낸다는 의미임!!!!)

Receiving a Signal

커널이 강제로 프로세스에게 반응을 요구함.

시그널을 받은 프로세스의 반응으로는 3가지가 가능함.

반응 방식
설명
1. Ignore (무시)
아무 동작도 하지 않음 (SIGCHLD의 기본 동작)
2. Terminate (종료)
프로세스를 종료 (SIGKILL, SIGTERM 등)
3. Catch (시그널 핸들러 실행)
사용자 정의 핸들러 실행 (SIGUSR1, SIGINT 등)

Pending and Blocked Signal

Pending

시그널이 프로세스에 도착했지만, 아직 처리되지 않은 상태

커널이 해당 프로세스에 시그널을 보냈지만 프로세스가 아직 받지 않은 상태를 말한다.

컨텍스트 스위칭으로 해당 프로세스가 CPU를 점유하게 되면 이 시그널 펜딩 목록에 있는 시그널들부터 처리하고 자신의 작업을 수행함.

시그널 펜딩 목록에 이미 똑같은 시그널이 존재하면 새롭게 도착한 시그널은 무시됨.

같은 논리로 한번에 똑같은 시그널이 여러번 발생해도 한 번만 기록됨

Blocked

프로세스가 특정 시그널을 받을 수 없도록 차단하는 것을 말한다.

하지만 시그널이 블록되어 있어도 펜딩 상태로 남아 있을 수 있음.

블록된 시그널은 시그널이 도착하더라도 즉시 실행되지 않고, 펜딩 상태로 유지되었다가 블록이 해제되면 실행됨.

마찬가지로 펜딩 상태에 있기 때문에 시그널 펜딩 목록에 저장됨

cf) 시그널 펜딩 목록에 있는 시그널들은 시그널 번호의 오름차순으로 처리됨.

Pending/Blocked Bits

커널은 두 가지 비트 벡터를 관리함.

Pending 비트 벡터

여기는 도착했지만 아직 처리되지 않은 시그널들이 있음.

커널이 특정 시그널을 프로세스에게 전달하면 해당 시그널의 비트를 1로 설정하고 프로세스가 시그널을 처리하면 해당 비트를 0으로 리셋

시그널
SIGINT (2)
SIGUSR1 (10)
SIGUSR2 (12)
SIGTERM (15)
펜딩 상태
1
0
1
0

위 표에서는 SIGINT 랑 SIGUR2 만 도착하고 나머지는 도착 안했음.

Blocked 비트 벡터

프로세스가 일시적으로 차단한 시그널 목록이 있음

특정 시그널을 블록하면, 해당 시그널이 도착해도 즉시 실행되지 않고 pending 목록에 추가된다. 블록이 해제되면, 펜딩된 시그널이 처리됨.

프로세스가 직접 sigprocmask() 를 사용해서 시그널을 블록함.

커널은 그냥 시그널 보내기만 하고, 이 시그널을 블록할지 말지는 프로세스가 결정함.

Process Groups

모든 프로세스는 반드시 하나의 프로세스 그룹에 속한다.

프로세스 그룹은 하나 이상의 관련된 프로세스들을 묶어서 관리함

쉘은 프로세스가 실행될 때 포그라운드나 백그라운드 그룹으로 구분함.

 

  • getpgrp() : 현재 프로세스의 그룹 ID를 확인함.
  • setpgid(pid, pgid) : 프로세스의 그룹을 변경함.
여담s

with /bin/kill Program

./forks 16 : 포크가 실행되어서 두 개의 자식 프로세스를 만든다.

두 프로세스는 동일한 프로세스 그룹에 속함 (pgid = 24817)

ps: 현재 실행 중인 프로세스 목록을 확인함.

kill -9 24818 : 특정 프로세스만 종료함. pid=24818 인 프로세스를 종료시킴

kill -9 -24817: -pgid 를 사용해서 해당 프로세스 그룹의 모든 프로세스에 시그널이 전달됨.

ps: 목록 보면 pgid가 24817 인 녀석들은 다 종료된걸 확인 가능함.

from the Keyboard

Ctrl + C (SIGINT)

포그라운드 프로세스 그룹 전체에 SIGINT 시그널을 보냄.

시그널을 받은 프로세스들은 종료됨

걍 현재 실행 중인 포그라운드 작업들 싹 다 강제 종료

reaping 되진 않음!!! 부모가 waitpid() 해주어야 함!!!!!

Ctrl + Z (SIGSTP)

포그라운드 프로세스 그룹 전체에 SIGSTP 시그널을 보냄

시그널을 받은 프로세스들 일시 정지

fg 명령어를 사용하면 다시 실행 가능함.

./forks 17 : 포크로 자식 프로세스 생성함

자식과 부모가 같은 그룹에 있는 것을 확인 가능함

ctrl + z : 포그라운드에서 돌아가는 작업들 일시 정지

ps w : 돌아가고 있는 프로세스들의 상태 보여줌

fg : 일시 정지한 포그라운드 작업을 재개

ctrl + c : 포그라운드에서 돌아가고 있는 프로세스들 강제 종료

with kill Function

Copy
void fork12() {

fork12() 함수는 N 개의 자식 프로세스를 생성하고 시그널을 보내 관리하는 함수.

Copy
pid_t pid[N];  
int i;
int child_status;

for (i = 0; i < N; i++)
    if ((pid[i] = fork()) == 0) {
        /* Child: Infinite Loop */
        while(1)
            ;
    }

N 개의 자식 프로세스를 생성함.

자식 프로세스라면 while(1) 로 무한루프를 돈다.

부모는 자식의 pid 를 pid[i] 로 저장하고 다음 자식 생성함.

Copy
for (i = 0; i < N; i++) {
    printf("Killing process %d\n", pid[i]);
    kill(pid[i], SIGINT);
}

SIGINT 를 통해서 각 자식들에게 SIGINT 시그널을 보냄.

자식 프로세스는 SIGINT 를 받고 종료됨.

Copy
for (i = 0; i < N; i++) {
    pid_t wpid = wait(&child_status);
    if (WIFEXITED(child_status))
        printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status));
    else
        printf("Child %d terminated abnormally\n", wpid);
}

wait(&child_status); 로 자식 프로세스가 종료될 때까지 대기 & 종료 상태 확인함.

WIFEXITED(child_status) 는 정상 종료되었는지 확인

WEXITSTATUS(child_status) 는 자식 프로세스의 종료 코드 확인

만약 SIGINT로 종료되었다면, 정상 종료(WIFEXITED)가 아닐 수도 있음.

Receiving Signals

커널이 예외 처리 핸들러를 마치고 나서 어떤 프로세스에 제어를 넘길이 결정함.

돌아가기로 마음먹은 프로세스가 결정되었다고 해당 프로세스의 유저 코드가 바로 실행되는 것은 아님.

되돌아가려는 프로세스의 Pending 과 Blocked 를 커널이 확인함.

걍 단순하게 커널이 “나 예외 처리 다함^^ 유저코드(프로세스)로 돌아감 ^^” 이러지 않아요.

왜냐하면 되돌아가려는 프로세스에 Pending이나 Blocked된 시그널이 있으면 이걸 처리해야되거든요.

시그널 처리를 먼저 확인하는데 그 과정은 아래와 같음.

먼저 pnb(Pending & blocked)연산을 수행함.

Copy
pnb = pending & ~blocked;

pending : 프로세스에 도착했지만 아직 처리되지 않은 시그널

blocked : 프로세스가 자발적으로 처리하지 않고 막아 놓은 시그널

pnb : 이 둘의 연산 결과 = 지금 처리 가능한 시그널들

pnd == 0 : 처리할 게 없으니 유저 코드로 그냥 돌아감

pnb ≠ 0 : 시그널 핸들러 실행

Default Actions

시그널의 기본 동작의 종류

  • 프로세스 종료 (Terminate)
  • 프로세스 일시 정지 (Stop)
  • 무시 (Ignore)

Custom Signal Handlers

내가 작성한 함수를 시그널이 발생했을 때 자동으로 실행되도록 할 수 있음.

Copy
handler_t *signal(int signum, handler_t *handler);

signum : 설정하려는 시그널 번호 (예: SIGINT , SIGTERM )

handler : 시그널을 받았을 때 실행할 동작 지정

  • SIG_IGN: 무시
  • SIG_DFL: 기본 동작
  • 사용자 정의 핸들러 함수의 주소: 시그널을 받을 때 해당 함수 실행

SIGINT가 발생했을 때 실행할 핸들러 함수를 정의함

그 핸들러를 signal()함수를 통해 등록한 다음에 시그널이 오기를 기다리는 구조

pause()는 시그널이 도착할 때까지 프로그램을 멈춤

아무 시그널이 오지 않으면 이 줄에서 계속 프로그램이 멈춰있을 거

signal(SIGINT, sigint_handler) : 이 함수는 앞으로 SIGINT 시그널이 오면 sigint_handler()를 실행하겠다고 커널에 등록하는 작업.

signal() 함수는 등록에 성공하면 핸들러의 주소를 리턴하고, 실패하면 SIG_ERR를 리턴함.

시그널 도착 → 커널모드 → 커널이 시그널이 발생한 프로세스의 다음에 실행될 명령어를 핸들러 함수의 주소로 세팅 → 유저모드 → 자동으로 핸들러 실행

Signals Handlers as Concurrent Flows

프로세스 A 가 while(1) 같은 무한 루프를 돌다가 외부에서 SIGINT 같은 시그널이 도착하면 핸들러가 개입해서 실행됨.

메인 루프는 중단되고, 핸들러 실행 후 다시 메인 루프로 돌아가는 구조

⇒ 핸들러는 별도의 스레드나 프로세스가 아님

⇒ 단지 실행 흐름이 중간에 인터럽트가 되고 분기 되는 것

Blocking and Unblocking Signals

Inplicit Blocking Mechanism

핸들러가 실행 중일 때, 동일한 종류의 시그널은 자동으로 블럭됨

만약 SIGINT 의 핸들러가 실행 중이면 새로운 SIGINT가 도착해도 바로 처리되지 않고 pending으로 대기

Explicit Blocking Mechanism

사용자가 직접 특정 시그널을 블럭/언블럭

⇒ sigprocmask

Copy
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

 

인자
설명
how
SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK
set
차단하거나 해제할 시그널 집합
oldset
이전 시그널 마스크를 저장할 곳 (필요 없으면 NULL)

시그널 집합을 다룰 수 있게 도와주는 유틸들

함수
설명
sigemptyset(&set)
빈 시그널 집합 생성
sigfillset(&set)
모든 시그널 번호 추가
sigaddset(&set, SIGINT)
특정 시그널 추가
sigdelset(&set, SIGINT)
특정 시그널 제거
sigprocmask

Temporarily Blocking Signals

sigemptyset(&mask) : mask라는 빈 시그널 집합 생성

sigaddset(&mask, SIGINT) : mask라는 시그널 집합에 SIGINT 추가

sigprocmask(SIG_BLOCK, &mask, &prev_mask) : mask라는 시그널 집합을 블락, 기존 마스크 상태를 prev_mask에 저장

마스크 상태를 저장한다는 말은 현재의 시그널 차단 목록을 다른 변수에 복사해 두는 것

커널 내부에서 시그널 차단 목록이 직접 수정될 수 있기 때문.

sigprocmask(SIG_SETMASK, &prev_mask, NULL) : 기존의 시그널 집합으로 복구

Safe Signal Handling

lock()의 개념

핸들러는 메인 프로그램과 동시에 수행되기 때문에 똑같은 전역 변수 혹은 공유 변수를 동시에 수정할 수도 있음. 이게 진짜 위험한 것.

여기서 동시라는 개념은 메인 프로그램이 읽기, 쓰기, 저장하기와 같은 일련의 ‘작업을 수행 중’에 시그널이 도착해서 시그널 핸들러가 작동하는 것을 말함.

위의 문제 상황을 data race 라고 함.

시그널 핸들러는 언제 도착할지 예측할 수 없는 것이기 때문에 메인 프로그램에서 수행하는 {읽기, 쓰기, 저장하기}가 하나의 작업으로 보장되어야 함 ← 원자성(atomic)

이를 해결하기 위해서 lock()이라는 개념을 도입함

Copy
// T1
lock();
a = count;
a = a + 1;
count = a;
unlock();
Copy
// T2
lock();
a = count;
a = a + 1;
count = a;
unlock();

lock은 mutex라는 공유 변수를 플래그 삼아서 mutex가 0일 때 lock을 통해서 1로 바뀌고 아래 작업들을 수행할 수 있음.

lock 에 들어갔는데 이미 mutex가 1이면 계속 대기하도록 함. (다른 원자 작업에서 일하는 중…)

그래서 T1이 안풀리면 T2는 lock 에서 계속 대기함

근데 이 마저도 위험함

공유 변수를 동시에 가지고 노는 것이 문제였는데, mutex도 공유 변수임.

만약 T1이 mutex 를 1로 바꾸고 있는 과정에서 T2가 lock 에 진입 하면 T2가 읽는 mutex도 0이라 얘도 lock을 잡음

그러면 다시 원점으로 돌아온 것임.

Dead Lock

printf 는 내부적으로 lock()을 잡고 자신의 작업을 수행하고 unlock()하는 구조임.

근데 printf 가 lock() 을 잡고 자기 일 하는 도중에 시그널이 도착해서 시그널 핸들러가 수행되는 상황을 임.

근데 이 시그널 핸들러에서도 printf 가 있다고 하자.

시그널 핸들러의 printf 도 lock() 을 잡아야 되는데 다른 놈이 이미 lock() 을 잡고 있어서 여기서 무한 대기 해버림

프로그램이 죽어버렸다고 해서 Dead Lock

Guidelines for Writing Safe Handlers

G0: Keep your handlers as simple as possible

오바하지 마라

딱 간결하게 너 할 일만 하고 빠져. 더 하려고 들지마

G1: Call only async-signal-safe functions in your handlers

핸들러 안에서는 시그널 안전 함수들만 호출하자

안전하지 않은 함수: printf, malloc, free, sprintf, exit, …

G2: Save and restore errno

errno 는 전역 변수인데 핸들러가 이걸 건드리면 메인 코드가 의미 없는 에러 코드를 볼 수 도 있음.

그래서 따로 저장해놨다가 핸들러 끝나면 혹시 모르니 다시 세팅해주도록 하자.

G3: Protect accesses to shared data structures by temporarily blocking all signals.

시그널 핸들러 중에서 공유 자원을 건드릴지도 모르는 시그널들은 sigprocmask 로 블락하자.

G4: Declare global variables as volatile

변수가 스레드들의 공유 변수가 될 가능성이 있다면(플래그) volatile 로 global 하게 선언하자 내부적으로 하지마.

일반 변수들은 컴파일러가 처음에 변수의 값을 읽고 이후에 변수가 변하지 않는다고 판단하면 레지스터에만 이 변수의 값을 캐싱해놓음

하지만 시그널 핸들러나, 인터럽트나 IO와 같은 외부의 동작에 의해 값이 바뀌는 걸 컴파일러는 고려를 안했기 때문에 이 녀석들에 의해서 플래그가 바뀌어도 메인 프로그램은 이를 인지하지 못함.

그래서 값을 아무리 바꿔도 메모리의 값은 바뀌는데 컴파일러가 읽는 레지스터에 캐싱된 변수의 값은 바뀌질 않는 것.

이럴때 volatile global 로 변수를 선언하라는 것.

그러면 레지스터에 캐싱하지 않고 항상 메모리에서 값을 읽어옴.

G5: Declare flags as volatile sig_atomic_t

플래그 변수는 volatile sig_atomic_t 로 선언하자

이 타입은 OS가 보장하는 원자적 데이터 타입임

이 타입으로 선언하면 읽기, 쓰기 얀산은 중단되지 않고 안전하게 수행할 수 있다

단!! flag = 1같은 단순한 read, write만!!! flag++ 이딴거 하지마 제발

Correct Signal Handling

현재 상황은 부모가 자식 프로세스를 N개 생성함 (여기서는 5개라고 가정하자)

자식이 생성되고 슬립으로 1초 뒤에 문제없이 엑싯을 하고 있음

그러면 SIGCHLD가 부모에게 전달되고, child_handler()가 호출 됨.

child_handler()는 wait()으로 자식을 처리하고 ccount를 1 감소 시킴

부모는 ccount 가 0이 될 때까지 살아 있는 상태

여기서 문제가 되는 상황은 시그널(SIGCHLD)을 처리하는 과정임

⇒ child_handler()

만약 자식 3명이 거의 동시에 종료되었다고 가정하면 SIGCHLD는 3번 발생함.

커널은 각 시그널을 큐에 쌓지 않음.

딱 한 비트만 사용해서 SIGCHLD가 pending 상태라고 표시함.

pending 상태로 들어와 있는 시그널을 처리함.

child_handler() 가 한번 실행되면 pending 비트가 클리어됨.

동시에 들어왔던 나머지 시그널들은 버려짐.

그러면 wait()으로 자식들을 수거한다는 시나리오가 틀려먹음

버려진 시그널을 발생시킨 자식 프로세스들은 좀비 프로세스로 메모리에 잔류함.

ccount는 절대 줄어들 수 없고, 무한 루프에 빠지게 됨.

SIGCHLD는 자식이 죽으면 발생함

하지만 시그널은 큐잉되지 않는게 문제임

그래서 시그널 핸들러가 한번만 실행될 수 있는게 문제임

그래서 걍 핸들러 내부에서 wait() 를 반복 호출해서 한번에 종료된 자식들을 수거해야됨.

그래서 핸들러가 while문으로 종료된 자식이 있으면 계속 수거하는 식으로 해야됨.

그러면 while문으로 모든 자식을 처리할 수 있게된다~~

만약 5명의 자식 중에서 자식1이 자식2가 생성되기 전에 종료되고 시그널 핸들러 수행도 완료 했다면???

그러면 시그널 핸들러는 처음에 실행되서 자식1을 리핑할거임. 그리고 종료.

그 다음에 자식2가 생성되고 종료되면 다시 SIGCHLD에 의해서 시그널 핸들러가 수행되고 자식2를 리핑해줌

아주 좋구만

Synchronizing Flows to Avoid Races

지금 그럴듯해 보이지만 data race가 있음!!!

메인 함수 내부에서 포크로 자식 프로세스를 만들고 나서 모든 시그널을 블락하고 자식 프로세스의 아이디를 에드잡 하고 있는데, 만약 시그널 블락 전에 자식 프로세스가 먼저 종료되서 SIGCHLD가 블락 전에 도착하게 되면?????????

에드한 적도 없는 잡을 딜리트 해야되는 어처구니 없는 상황이 일어난다.

그래서 자식을 생성하기 전에 적어도 시그 차일드라도 블락을 해주어야 함.

그리고 나서 포크를 통해 자식 프로세스를 만드는 것.

근데 포크를 하면 부모의 시그널 마스크가 자식에게도 복사됨. (이후에는 각자의 시그널 마스크로 사용함, 독립적으로)

그러면 자식 프로세스도 시그 차일드가 블락된 상태가 되는데, 이거는 예상치 못한 나비효과를 부를 수 있기 때문에 자식 프로세스에서 시그 차일드를 언블락 해주어여함.

여기까지 상황에서는 부모는 아직 시그 차일드를 받지 않는 상태임.

그러고 나서 자식 프로세스의 아이디로 에드 잡을 할 때 모든 시그널을 블락함.

그러고 나서 안전하게 에드 잡하고 에드 잡 잘 했으면 모든 시그널 블락한걸 다시 언블락함.

Explicitly Waiting for Signals

메인 함수 내부 보면 pid를 0으로 해놓는데, 이게 어디서 바뀌냐 하면, 시그널 핸들러 안에서 바뀜.

pid는 volatile로 선언되어있는데, 이건 지금 공유 변수임.

(시그널 핸들러랑 메인 코드가 공유해서 사용하는 변수)

자식이 죽으면 핸들러가 실행되고, 자식의 프로세스 아이디가 pid에 저장됨.

메인 함수의 루프에서 조건 만족으로 탈출

근데 과연 이 방식이 맞을지???????????

이 방식은 CPU를 무지막지하게 낭비함.

계속 루프 돌면서 “아~~ pid는 언제 바뀌나~하~~”

이게 CPU 입장에서는 되게 소모적인 행동임.

그래서 해결책으로 pause() 를 사용함.

근데 이렇게 하면 data race가 발생할 수 있음!!!!!!!!

어떤 data race 인지 이해하려면 먼저 pause() 부터 이해해야됨.

pause는 현재 프로세스를 중단 시켜서, 어떤 시그널이 도착할 때까지 잠들게 하는 것임.

무한하게 멈춰있다가 아무 시그널이 도착해서 핸들러가 실행되면 그 다음에 pause() 가 리턴함.

근데 while 문 들어온 다음, pause 들어가기 전에 시그널이 도착하면 pause는 시그널이 도착한 것을 인지하지 못하고 무한정 대기하게 됨.

그래서 또 다른 해결책으로 pause 대신에 sleep을 사용함

반응성이 너무 떨어짐.

시그 차일드가 도착 했어도 최대 1초까지 딜레이가 걸릴 수 있음.

너무 싫어~~

그래서 진짜 해결책은 뭐야??????????????

⇒ sissuspend() 사용

시그 서스펜드는 아래 코드를 원자성을 가지고 실행하는 함수

Copy
sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);

현재의 시그널 마스크를 mask로 교체하고 시그널이 도착할 때까지 대기하다가 시그널 핸들러를 실행하고 나면 이전 마스크로 자동 복원함.

mask는 우리가 블락하고 싶은 시그널 집합을 담는 변수

prev는 현재 시그널 마스크를 저장해 둘 변수 (복구용)

Copy
Sigemptyset(&mask);
Sigaddset(&mask, SIGCHLD)

위 코드를 실행하고 나면

mask는 {SIGCHLD}로 되어있을 거임

prev는 아무 짓도 안했으니까 아무것도 없음

Copy
Sigprocmask(SIG_BLOCK, &mask, &prev);  // Block SIGCHLD

현재 시그널 마스크에 SIGCHLD를 추가해서 block 하기

그 전에 사용중이던 시그널 마스크는 prev에 저장

여기서 변수의 상태는 아래와 같다.

mask는 {SIGCHLD} 를 가지고 있을 것이고.

prev는 SIGCHLD가 블락되지 않던 원래 마스크 상태를 가지고 있을 거임

즉, mask는 SIGCHLD가 블락된 상태를 가질 것이고, prev는 SIGCHLD가 언블락된 상태를 가질 것임.

Copy
while (!pid)
    Sigsuspend(&prev);   // ← 여기서 prev는 SIGCHLD가 unblock된 마스크

이렇게 하면 플로우는 아래와 같다

  1. sigprocmask(SIG_SETMASK, &prev, &prev); prev는 원래 언블락 상태임. 이걸로 현재 마스크를 바꾸고, 기존에 시그 차일드만 블락한 상태를 prev에 저장함.
  2. pause();
  3. sigprocmask(SIG_SETMASK, &prev, NULL);

그리고 sigsuspend() 는 끝난 순간에, 시그널 마스크는 자동으로 sigsuspend() 들어가기 전 상태로 복원됨

Copy
Sigprocmask(SIG_SETMASK, &prev, NULL);

지금은 prev가 들어갔으니까 prev가 들어가기 전 상태로 복원 됨.

들어가기 전에 prev는 언블락 상태임.

결국 위 코드는 시그 차일드를 언블락하는 것!

반응형

'지식 > 시스템프로그래밍' 카테고리의 다른 글

System-Level I/O  (1) 2025.04.21
Exceptional Control Flow: Exceptions and Processes  (0) 2025.03.29