QNX RTOS: 4-3. Processes - Detecting Termination

2025. 11. 26. 15:55운영체제/QNX

시스템에서 프로세스가 비정상 종료되거나 작업을 마치고 나갈 때, 이를 감지하고 뒷정리를 하는 것은 매우 중요함. QNX는 이를 위해 POSIX 표준 방식과 QNX 특화 방식을 모두 제공함.

1. Parent/Child 관계 (POSIX 표준)

부모 프로세스가 자식 프로세스를 생성(fork, spawn)했을 때 사용하는 가장 일반적인 방법임.

1.1. 동작 메커니즘

  1. 자식 프로세스가 종료되면 커널은 부모에게 SIGCHLD 시그널을 보냄. (기본 동작은 무시지만, 핸들러 등록 가능)
  2. 부모는 waitpid()나 wait*() 계열 함수를 호출하여 자식의 종료 상태(Exit Status)를 확인함.
  3. 이 함수는 자식이 죽을 때까지 블로킹(Block) 대기하므로, 주로 Launcher 프로세스나 Watchdog 프로세스에서 루프를 돌며 사용함.

1.2. 좀비 프로세스 (Zombie Process)

  • 정의: 자식은 죽었지만(메모리 해제, 파일 닫힘), 부모가 아직 wait()를 호출해주지 않아 프로세스 테이블(Process Table)에 이름표(PID, 종료 코드)만 남아있는 상태.
  • 필요성: 부모가 나중에라도 "얘가 왜 죽었는지(시그널사인지, 정상종료인지)"를 확인해야 하므로, 커널이 정보를 잠시 보관하는 것임.
  • 해결: 부모가 wait()를 호출하는 순간 좀비는 완전히 소멸함.
  • 무시하기: 만약 부모가 자식의 죽음에 전혀 관심이 없다면, signal(SIGCHLD, SIG_IGN)을 설정함. 그러면 자식은 죽자마자 좀비가 되지 않고 즉시 소멸함.

2. Client/Server 관계 (QNX Message Passing)

QNX의 핵심인 메시지 패싱 구조를 활용한 방법임.

  • 개념: 프로세스 자체가 죽는 것을 직접 감지한다기보다, "연결(Connection)이 끊어짐"을 감지하는 방식임.
  • 서버 입장: 클라이언트가 죽으면 커널이 서버에게 "네 채널에 붙어있던 놈이 떨어져 나갔어"라고 알려줌 (Pulse 등을 통해).
  • 클라이언트 입장: 서버가 죽으면 클라이언트가 메시지를 보낼 때 에러가 발생하므로 감지 가능함.

3. Death Pulses (System-wide Notification)

가장 유연하고 강력한 방법으로, 부모-자식 관계가 아니더라도 시스템 내의 '아무(Any)' 프로세스가 죽는 것을 감시할 수 있음. 시스템 모니터링 데몬을 만들 때 주로 사용됨.

3.1. 동작 원리

  • 등록: procmgr_event_notify() 함수를 사용하여 커널에게 "누군가 죽으면 나에게 알려줘"라고 요청함.
  • 플래그: PROCMGR_EVENT_PROCESS_DEATH 사용.
  • 수신: 프로세스가 죽으면 커널은 감시자에게 Pulse(펄스)나 Signal을 보냄.
  • 정보: 수신된 Pulse 데이터 안에 죽은 프로세스의 PID 정보가 들어있음.

💡 요약 및 인사이트

  1. 표준은 wait(): 부모-자식 관계라면 POSIX 표준인 wait()를 써서 종료 코드를 확인하고 좀비를 치워야 함.
  2. 관계없음 Death Pulse: 부모-자식 관계가 아닌, 시스템 전체의 주요 프로세스(예: 네비게이션, 오디오, 센서 처리)를 감시하는 System Health Monitor를 구현할 때는 procmgr_event_notify를 사용하는 것이 정석임.
  3. 좀비 관리: 개발 중에 pidin을 쳤는데 Zombie 상태가 보인다면, 부모 프로세스가 wait 처리를 제대로 안 하고 있다는 증거임.

 

[QNX Deep Dive] 실습: Death Pulse와 Zombie Process 눈으로 확인하기

이번 실습의 목표는 이론으로만 배웠던 "프로세스가 죽는 순간"을 시스템이 어떻게 감지하는지, 그리고 부모-자식 관계가 실제 프로세스 트리에서 어떻게 보이는지 확인하는 것임.

1. 전역 감시 실습: death_pulse.c

이 예제는 Death Pulse (시스템 내 아무 프로세스가 죽으면 알림 받기)를 시연함.

1.1. 동작 원리

  • procmgr_event_notify()를 이용해 시스템 전역의 PROCMGR_EVENT_PROCESS_DEATH 이벤트를 구독함.
  • 프로그램은 Pulse를 기다리며 블로킹(Blocking) 됨.

1.2. 테스트 방법

  1. death_pulse 프로그램을 백그라운드나 다른 터미널에서 실행함.
  2. 또 다른 터미널에서 pidin 명령어를 입력함.
    • 이유: pidin은 프로세스 리스트만 쫙 뿌리고 바로 종료(Die)하는 프로그램이기 때문임. 테스트용으로 아주 적절함.
  3. 결과: pidin이 종료되는 순간, death_pulse 프로그램이 "PID [숫자]가 죽었음"이라고 로그를 찍는 것을 확인함.

2. 부모-자식 관계 실습: spawn_example.c

이 예제는 posix_spawn()으로 자식을 낳고, 좀비(Zombie) 상태를 관찰하는 시나리오임. 30초 간격으로 상태가 변하도록 짜여 있으니 타이밍에 맞춰 확인해야 함.

2.1. 프로세스 가계도 (Family Tree) 확인

자식이 부모에게 잘 종속되었는지 확인하는 두 가지 방법이 있음.

방법 A: 커맨드 라인 (pidin family)

  • 터미널에 pidin family를 입력함.
  • PPID (Parent Process ID) 컬럼을 확인.
  • spawn_example이 생성한 자식(sleep 프로세스 등)의 PPID가 spawn_example의 PID와 일치하는지 확인함.

 

방법 B: IDE (Target Navigator)

  • IDE의 System Information Perspective -> Target Navigator 뷰로 이동.
  • View 메뉴(역삼각형 아이콘) -> Group by PID Family 선택.
  • 트리 구조가 펼쳐짐:
    • procnto (Root)
    • qconn (IDE 에이전트)
    • spawn_example (우리가 실행한 예제)
    • sleep (예제가 낳은 자식)

2.2. 왜 qconn이 부모인가?

  • IDE에서 Run 버튼을 누르면, 호스트 PC의 IDE는 타겟 보드의 qconn 에이전트에게 "이거 실행해줘"라고 요청함.
  • 그래서 qconn이 spawn_example을 낳게 되고, spawn_example이 다시 sleep을 낳는 3대 가족 관계가 형성됨.

3. 좀비 프로세스의 소멸 조건

이론에서 부모가 wait()를 호출하면 좀비가 사라진다고 했음. 하지만 이번 실습에서 중요한 포인트가 하나 더 있음.

  • Scenario: 자식이 죽어서 좀비가 됨. 그런데 부모가 wait()를 안 하고 버티다가 부모가 먼저 죽어버리면?
  • Result: 좀비도 같이 사라짐.
    • 부모가 죽으면 고아(Orphan)가 된 좀비 자식들은 시스템의 루트(procnto 등)로 입양되었다가 즉시 정리(Reap)당하기 때문임.
    • 즉, 좀비가 영원히 남을까 봐 걱정할 필요는 없음. 부모만 확실히 죽여주면 됨.

 

[Code Deep Dive] spawn_example.c 분석

이 프로그램의 목적은 자식 프로세스(sleep)를 낳고, 자식이 죽는 것을 감지한 뒤, 의도적으로 좀비 상태를 유지하여 관찰할 시간을 주고, 마지막에 정리(Reap)하는 것.

1. 시그널 설정: Race Condition 방지 (핵심!)

초반부의 sigaddset, pthread_sigmask, signal 부분은 임베디드 시스템 프로그래밍에서 매우 중요한 패턴임.

C
 
// 1. 시그널 집합(set)을 비우고 SIGCHLD를 추가
sigemptyset(&set);
sigaddset(&set, SIGCHLD);

// 2. SIGCHLD 시그널을 '블록(Block)' 함 (핵심)
pthread_sigmask(SIG_BLOCK, &set, NULL);

// 3. 더미 핸들러 등록
signal(SIGCHLD, sigfunc);
  • 왜 블록(Block)을 먼저 할까?
    • 만약 자식 프로세스가 엄청나게 빨리 실행되고 죽어서, 부모가 sigwaitinfo()를 호출하기도 전에 SIGCHLD 시그널이 도착해버린다면?
    • 기본 동작(Default Action)에 의해 시그널이 무시되거나 사라질 수 있음.
    • 이를 방지하기 위해 "일단 시그널이 오면 처리하지 말고 큐에 담아둬(Pending)"라고 설정하는 것이 SIG_BLOCK임.
  • 왜 빈 핸들러(sigfunc)를 등록할까?
    • SIGCHLD의 기본 동작은 '무시(Ignore)'. 혹시라도 블록이 풀렸을 때 프로세스가 죽거나 이상하게 동작하지 않도록, 안전장치로 빈 핸들러를 달아둠. 하지만 실제로는 블록되어 있으므로 이 함수는 호출되지 않음.

2. 프로세스 생성: posix_spawn

C
 
char *child_argv[3] = {"sleep", "30", NULL };
// "sleep 30" 명령을 실행하는 자식 프로세스 생성
ret = posix_spawn(&pid, "/system/bin/sleep", NULL, NULL, child_argv, envp);
  • 자식의 역할: 30초 동안 잔다. 즉, 30초 동안은 Running 또는 Reply-blocked 상태로 살아있음.
  • 실습 포인트: 이 시점에 pidin family를 입력하면 spawn_example 밑에 sleep이 자식으로 붙어있는 것을 볼 수 있음.

3. 종료 감지 대기: sigwaitinfo

C
 
/* SIGCHLD 시그널이 올 때까지 여기서 잠듦 (Blocking) */
sigwaitinfo(&set, NULL);
  • 부모 프로세스는 여기서 멈춰 섬.
  • 자식이 30초 뒤에 sleep을 끝내고 죽으면, 커널이 SIGCHLD를 보냄.
  • 아까 블록해뒀던 시그널이 도착하자마자 sigwaitinfo가 깨어나서 리턴함.
  • 의미: "자식이 죽었다는 소식을 들었다"는 뜻임.

4. 좀비 구간 (Zombie State) 관찰

C
 
printf("Child has died, pidin should now show it as a zombie\n");
sleep(30); // 부모가 30초 동안 딴짓을 함
  • 여기가 이 코드의 하이라이트.
  • 자식은 이미 죽었음(SIGCHLD 발생).
  • 하지만 부모는 아직 wait()를 호출하여 뒤처리를 안 했음.
  • 실습 포인트: 이 30초 동안 터미널에서 pidin을 입력해봄. 자식 프로세스(sleep)의 상태가 Zombie로써 살아있음. 메모리는 다 반납했지만, 부모에게 종료 코드를 전달하기 위해 껍데기만 남은 상태임.

5. 좀비 청소 (Reaping): wait

C
 
pid = wait(&child_status);
printf("Zombie is now gone...\n");
  • 부모가 드디어 wait()를 호출함.
  • 커널은 보관하고 있던 자식의 종료 상태(child_status)를 부모에게 넘겨주고, 프로세스 테이블에서 자식을 완전히 지워버림.
  • 이제 좀비가 사라졌음.

'운영체제 > QNX' 카테고리의 다른 글

QNX RTOS: 4-5. Threads - Creation  (0) 2025.11.26
QNX RTOS: 4-4. Thread  (0) 2025.11.26
QNX RTOS: 4-2. Processes - Creation  (0) 2025.11.26
QNX RTOS: 4-1. Processes and Threads  (0) 2025.11.26
QNX RTOS: 3. Security Policies  (0) 2025.11.26