2025. 11. 26. 15:55ㆍ운영체제/QNX
시스템에서 프로세스가 비정상 종료되거나 작업을 마치고 나갈 때, 이를 감지하고 뒷정리를 하는 것은 매우 중요함. QNX는 이를 위해 POSIX 표준 방식과 QNX 특화 방식을 모두 제공함.
1. Parent/Child 관계 (POSIX 표준)
부모 프로세스가 자식 프로세스를 생성(fork, spawn)했을 때 사용하는 가장 일반적인 방법임.
1.1. 동작 메커니즘
- 자식 프로세스가 종료되면 커널은 부모에게 SIGCHLD 시그널을 보냄. (기본 동작은 무시지만, 핸들러 등록 가능)
- 부모는 waitpid()나 wait*() 계열 함수를 호출하여 자식의 종료 상태(Exit Status)를 확인함.
- 이 함수는 자식이 죽을 때까지 블로킹(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 정보가 들어있음.
💡 요약 및 인사이트
- 표준은 wait(): 부모-자식 관계라면 POSIX 표준인 wait()를 써서 종료 코드를 확인하고 좀비를 치워야 함.
- 관계없음 Death Pulse: 부모-자식 관계가 아닌, 시스템 전체의 주요 프로세스(예: 네비게이션, 오디오, 센서 처리)를 감시하는 System Health Monitor를 구현할 때는 procmgr_event_notify를 사용하는 것이 정석임.
- 좀비 관리: 개발 중에 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. 테스트 방법
- death_pulse 프로그램을 백그라운드나 다른 터미널에서 실행함.
- 또 다른 터미널에서 pidin 명령어를 입력함.
- 이유: pidin은 프로세스 리스트만 쫙 뿌리고 바로 종료(Die)하는 프로그램이기 때문임. 테스트용으로 아주 적절함.
- 결과: 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 부분은 임베디드 시스템 프로그래밍에서 매우 중요한 패턴임.
// 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
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
/* SIGCHLD 시그널이 올 때까지 여기서 잠듦 (Blocking) */
sigwaitinfo(&set, NULL);
- 부모 프로세스는 여기서 멈춰 섬.
- 자식이 30초 뒤에 sleep을 끝내고 죽으면, 커널이 SIGCHLD를 보냄.
- 아까 블록해뒀던 시그널이 도착하자마자 sigwaitinfo가 깨어나서 리턴함.
- 의미: "자식이 죽었다는 소식을 들었다"는 뜻임.
4. 좀비 구간 (Zombie State) 관찰
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
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 |