Windows 앱에서 자식 프로세스를 안전하게 다루기 위한 체크리스트 - Job Object, 종료 전파, 표준 입출력, watchdog의 베스트 프랙티스
· 小村 豪 · Windows, Process, Job Object, IPC, C++, .NET, C#
변환 도구, 업데이터, 분석 워커, 외부 CLI, PowerShell, ffmpeg, 사내 유틸리티.
Windows 앱은 생각 이상으로 간단히 자식 프로세스에 의존합니다.
다만 사고가 나는 것은 「기동할 수 있었는가」가 아닙니다.
- 부모가 떨어졌는데 자식만 남는다
- 손자 프로세스만 살아남는다
stdout/stderr가 막혀WaitForExit가 돌아오지 않는다- watchdog이 감시 대상과 함께 죽는다
Kill(entireProcessTree: true)로 끝낸 셈인데 관측만 먼저 끝난다
Windows에서 자식 프로세스를 안전하게 다루는 요령은 기동 API를 고르는 것이 아니라 프로세스 트리의 소유자를 정하고 종료 절차와 I/O를 설계하는 것입니다.
이 글에서는 Job Object, 종료 전파, 표준 입출력, watchdog을 한 장의 설계로 정리합니다.
1. 먼저 결론
먼저 실무에서 가장 효과가 있는 곳만 나열합니다.
- 부모의 생사와 자식 프로세스 트리의 수명을 연결하고 싶다면, 기준점은 Job Object입니다
- console에의 종료 의뢰와 프로세스 트리의 회수는 별개입니다
- 전자는 process group과
GenerateConsoleCtrlEvent - 후자는 Job Object
- 전자는 process group과
- 기동 시점부터 Job에 넣고 싶다면
STARTUPINFOEX와PROC_THREAD_ATTRIBUTE_JOB_LIST를 쓰는 설계가 솔직합니다 - 표준 출력 / 표준 에러는 병렬로 빨아올리는 것이 기본입니다
stdin을 쓴다면, 다 쓰고 나서 close해서 EOF를 전하는 데까지 설계합니다- watchdog은 감시 대상의 Job 밖에 두는 편이 안전합니다
.NET의Kill(entireProcessTree: true)는 편리하지만 Windows의 tree lifecycle 관리 그 자체가 아닙니다
2. 무엇이 위험한가
자식 프로세스 기동의 구현은 처음에는 대체로 10줄 전후로 쓸 수 있습니다.
하지만 사고가 나는 것은 그 10줄의 바깥쪽입니다.
- 부모가 떨어진 뒤 자식이나 손자가 계속 남는다
- helper가 또 다른 helper를 기동해, 바로 아래 자식만 기다리고 만족한다
stdout/stderr의 한쪽이 막혀 부모도 자식도 서로 기다린다- UI thread에서 대기해, 화면도 COM도 굳는다
- watchdog이 감시 대상과 같은 운명 공동체가 되어, 이상 시에 함께 떨어진다
여기서 중요한 것은 「자식 프로세스 관리」는 하나의 API 이야기가 아니라는 점입니다.
적어도 다음 4가지를 나누어 생각하는 편이 정리하기 쉽습니다.
- 프로세스 트리를 누가 소유하는가
- 어떻게 협조 종료를 의뢰하는가
- 표준 입출력을 어떻게 흘리는가
- 이상 종료와 행을 어떻게 감시하는가
3. 구조의 역할을 섞지 않는다
process handle / process group / Job Object는 비슷해 보여도 역할이 다릅니다.
| 구조 | 주요 역할 | 맞는 장면 | 그것만으로는 부족한 것 |
|---|---|---|---|
| process handle | 1 프로세스의 종료 대기, exit code 취득 | 단발 도구의 완료 대기 | 손자 프로세스의 회수 |
| process group | console에의 Ctrl+Break 전파 | console child의 협조 종료 | 부모 크래시 시의 cleanup, GUI 자식 프로세스 |
| Job Object | 프로세스 트리의 묶음, 제한, 일괄 종료 | worker tree, updater, helper chain | 앱 고유의 「저장하고 나서 닫는다」 |
process group은 console signal을 어디로 보낼지를 정하는 구조이지, 부모가 죽으면 트리째 정리하기 위한 구조가 아닙니다.
한편 Job Object는 프로세스 군을 1단위로 관리하는 Windows 측의 구조입니다.
4. Job Object를 기준으로 한다
Job Object의 가장 강한 점은 「누구의 자식인가」가 아니라 「어느 Job에 속하는가」로 process tree를 묶을 수 있다는 것입니다. Job에 들어간 프로세스가 CreateProcess로 만드는 자식은 기본으로 그 Job에 들어갑니다.
나아가 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE를 붙이면 마지막 job handle이 닫혔을 때 Job에 관련지어진 전 프로세스가 종료됩니다.
4.1 먼저 잡아 두고 싶은 4가지
1. 부모 종료로 트리째 정리하고 싶다면 KILL_ON_JOB_CLOSE
이것은 Windows 앱에서 helper / worker를 다룰 때의 토대입니다. TerminateJobObject를 명시적으로 호출하는 설계도 좋지만, 부모의 이상 종료까지 포함해 cleanup을 부모의 수명으로 치우치고 싶다면 KILL_ON_JOB_CLOSE가 알기 쉽습니다.
2. BREAKAWAY를 가볍게 붙이지 않는다
JOB_OBJECT_LIMIT_BREAKAWAY_OK나 JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK는 편리해 보이지만 cleanup할 수 있는 셈이었던 트리에서 일부가 빠지는 원인이 됩니다. 의도가 없는 한 breakaway는 붙이지 않는 편이 사고율이 내려갑니다.
3. 기동 시점부터 Job에 넣고 싶다면 PROC_THREAD_ATTRIBUTE_JOB_LIST
AssignProcessToJobObject로 나중에 연결할 수도 있습니다.
다만 기동 직후부터 Job 소속을 전제로 하고 싶은 장면에서는 STARTUPINFOEX와 PROC_THREAD_ATTRIBUTE_JOB_LIST를 써서 작성 시에 Job을 지정하는 편이 줄거리가 좋습니다.
4. job handle의 소유자를 애매하게 두지 않는다
KILL_ON_JOB_CLOSE는 마지막 handle이 닫혔을 때 효과가 있습니다.
즉 반대로 말하면 job handle을 다른 프로세스로 복제하거나 의도치 않게 상속시키면 부모가 죽어도 상정대로 cleanup되지 않습니다. 누가 job handle의 최종 소유자인가는 먼저 정해 둬야 합니다.
4.2 Job Object는 observability에도 쓸 수 있지만, 통지는 만능이 아니다
Job Object에는 I/O completion port를 관련지어 통지를 받는 구조가 있습니다. 다만 completion port의 통지는 모든 케이스에서 완전 보장되는 통지라고 보지 않는 편이 안전합니다.
그래서 completion port는,
- 감시
- 집계
- 로그
- 메트릭
에는 편리하지만 그것만으로 correctness를 짜지 않는 편이 좋습니다.
5. 종료 전파를 protocol과 timeout으로 설계한다
자식 프로세스의 종료는 한 방의 kill API로 끝나는 이야기가 아닙니다.
가장 사고 나기 어려운 것은 대체로 다음 3단계입니다.
- 협조 종료를 의뢰한다
- 짧은 timeout으로 기다린다
- 마지막에 Job째 강제 종료한다
이 순서로 해 두면 정상적인 종료 경로는 유지하면서 행 시에는 회수할 수 있습니다.
5.1 GUI child
GUI를 가진 자식 프로세스라면 .NET에서는 CloseMainWindow가 close message 송신이 됩니다.
다만 이것은 종료 요청이지 강제 종료가 아닙니다. 그래서,
CloseMainWindow- 일정 시간 기다린다
- 안 되면 Job째 kill
이라는 흐름이 솔직합니다.
5.2 Console child
Console child에서는 GUI의 close message는 쓸 수 없습니다.
이때는 process group과 console signal을 씁니다.
CREATE_NEW_PROCESS_GROUP으로 기동하고 GenerateConsoleCtrlEvent로 CTRL_BREAK_EVENT를 보내는 흐름입니다.
여기서 중요한 것은,
CTRL_C_EVENT는 특정 group에의 한정에 맞지 않다- signal을 받을 수 있는 것은 console을 공유하고 있는 프로세스뿐
CREATE_NEW_PROCESS_GROUP을 쓰면CTRL+C의 의미도 바뀐다
는 점입니다.
5.3 Worker / headless child
Worker나 headless child는 GUI도 console도 아닌 경우가 많습니다.
이 경우는 자식 프로세스 전용의 종료 protocol을 가지는 편이 안전합니다.
stdin에quit를 보낸다- named pipe / socket / RPC로 shutdown command를 보낸다
- event object로 정지 요청을 전한다
Windows적으로는 Job Object가 tree cleanup을 담당하고, 앱적으로는 pipe나 stdin이 graceful shutdown을 담당한다는 분리가 사고 나기 어렵습니다.
6. 표준 입출력을 막히게 하지 않는다
6.1 stdout / stderr는 병렬 drain
첫 기본은 이것입니다.
stdout과 stderr는 병렬로 빨아올린다. 한쪽을 전부 읽고 나서 다른 쪽은 막히기 쉽습니다.
Windows의 pipe는 무한 버퍼가 아닙니다. 자식이 stderr를 대량 출력하고 부모가 stdout밖에 읽지 않으면, 자식은 write로 멈추고 부모는 종료 대기로 멈추는 형태가 평범하게 일어납니다.
6.2 stdin을 쓴다면 EOF까지 설계한다
stdin에 쓸 수 있다는 것과 자식이 끝날 수 있다는 것은 같지 않습니다.
- 입력을 쓴 뒤 close하지 않는다
- 부모는 「이미 건넸다」고 생각하고 있다
- 자식은 「아직 계속이 올 것이다」라고 생각해 계속 기다린다
라는 상태가 일어납니다. stdin을 쓴다면 다 쓰고 나서 close해서 EOF를 전하는 데까지 포함해 설계할 필요가 있습니다.
6.3 불필요한 pipe end를 반드시 닫는다
부모 측·자식 측의 미사용 end를 닫지 않으면 EOF가 전해지지 않고 종료 조건이 무너집니다.
이것은 단순하지만 실무에서는 꽤 많은 사고입니다.
6.4 UseShellExecute=false와 handle 상속의 취급을 애매하게 두지 않는다
표준 입출력 리다이렉트를 쓴다면 .NET에서는 UseShellExecute=false가 전제입니다.
Win32에서도 무엇을 상속시킬지를 가능한 한 좁히는 편이 안전합니다. bInheritHandles=TRUE인 채로 전부 상속시키면 생각지도 못한 handle leak의 원인이 됩니다.
7. watchdog은 「밖」에 둔다
watchdog을 넣을 때 가장 중요한 것은 감시 대상과 같은 Job에 넣지 않는 것입니다.
worker가 떨어지면 재기동하고 싶은데, 그 재기동 역할까지 같이 죽으면 의미가 없습니다.
7.1 exit 감시는 wait handle 기반으로 한다
프로세스는 종료하면 signaled 상태가 됩니다.
그래서 exit 감시는 본래 polling loop로 HasExited를 100ms마다 볼 필요가 없습니다.
Win32라면,
WaitForSingleObjectWaitForMultipleObjectsRegisterWaitForSingleObjectSetThreadpoolWait
가 정공법입니다. 여러 child를 다룬다면 timer polling보다 wait handle 기반이 자연스럽습니다.
7.2 UI thread에서 무한 대기하지 않는다
WaitForSingleObject(INFINITE)는 편리하지만, window를 가진 thread에서 쓰면 message pump를 멈추기 쉽습니다.
UI thread, COM apartment thread, message pump를 가진 thread에서는 대기의 놓을 장소를 먼저 생각하는 편이 안전합니다.
7.3 hang watchdog에는 heartbeat가 필요
exit watchdog은 process handle로 충분합니다.
하지만 hang watchdog은 다릅니다.
- CPU 100%로 굳어 있다
- deadlock하고 있다
- event loop는 살아 있지만 진척이 없다
- 입력 대기로 멈춰 있다
이런 상태는 「프로세스가 살아 있는가」만으로는 판정할 수 없습니다. 그래서 hang까지 보고 싶다면,
- heartbeat
- progress sequence
- last successful work timestamp
- health probe
같은 앱 층의 생존 확인이 필요합니다.
7.4 재기동 역할은 감시 대상의 밖에 둔다
실무에서 흔한 것은 다음 2패턴입니다.
- 부모 앱이 일시적으로 helper를 기동할 뿐
- 부모가 Job을 가지고, 부모 종료로 helper tree를 회수
- 장시간 worker를 상주시키고, 떨어지면 재기동하고 싶다
- 외부의 watchdog process / service가 worker generation마다 Job을 만든다
후자에서는 worker tree와 restart authority를 분리하는 편이 설계가 안정됩니다.
7.5 restart policy는 budget으로 가진다
watchdog을 넣으면 다음은 crash loop가 시작됩니다.
- 즉시 재기동
- 또 즉시 떨어진다
- 로그만 대량으로 나온다
이것을 피하려면,
- backoff
- 일정 시간 내의 restart 횟수 상한
- 연속 실패 시는 정지하고 통지
라는 restart budget을 가지는 편이 좋습니다.
8. 전형 패턴별의 추천 구성
| 장면 | 추천 구성 |
|---|---|
| 데스크톱 앱이 단발의 CLI helper를 기동한다 | 1 기동 = 1 Job. KILL_ON_JOB_CLOSE를 붙이고 stdout / stderr를 병렬 drain. 캔슬 시는 협조 종료 → timeout → Job kill |
| helper가 또 손자 프로세스를 기동한다 | Job Object를 전제로 하고, breakaway를 허락하지 않는다. 기동 시부터 고정하고 싶다면 PROC_THREAD_ATTRIBUTE_JOB_LIST |
| service / watchdog이 장시간 worker tree를 감시한다 | watchdog은 외부 process / service. worker generation마다 Job을 만들고, exit handle + heartbeat로 감시 |
| console tool을 정중하게 멈추고 싶다 | CREATE_NEW_PROCESS_GROUP으로 기동하고, CTRL_BREAK_EVENT로 협조 종료. 그 후 timeout으로 Job kill |
| GUI helper를 닫고 싶다 | CloseMainWindow / WM_CLOSE 상당 → timeout → Job kill |
| 다수의 자식 프로세스를 감시하고 싶다 | blocking thread를 늘리기보다 RegisterWaitForSingleObject / SetThreadpoolWait를 쓴다 |
여기서 가장 중요한 것은 graceful shutdown의 구조와 cleanup의 구조를 나누는 것입니다.
9. 해서는 안 되는 것
Kill(entireProcessTree: true)만으로 tree lifecycle이 풀렸다고 생각한다bInheritHandles=TRUE인 채로 전부 상속한다stdout을 전부 읽고 나서stderr를 읽는다- pipe의 미사용 end를 닫지 않는다
- UI thread에서
WaitForSingleObject(INFINITE)를 한다 - watchdog을 감시 대상과 같은 Job에 넣는다
- 259를 일반 exit code로 쓴다
- Job completion port의 통지를 유일한 진실로 한다
10. 정리
Windows 앱에서 자식 프로세스를 안전하게 다룰 때 가장 효과가 있는 것은 다음 정리입니다.
누가 process tree를 소유하는가
어떻게 종료 요청을 전하는가
표준 입출력을 어떻게 흘려 끝내는가
watchdog을 어디에 두는가
이 4가지를 먼저 정한다.
그 위에서 꽤 거칠게 말하면 다음입니다.
- tree cleanup의 기준점은 Job Object
- graceful shutdown은 GUI / console / worker로 나눈다
- stdio는 병렬 drain과 EOF까지 포함해 설계한다
- watchdog은 감시 대상의 밖에 두고, polling이 아니라 wait handle과 heartbeat로 본다
CreateProcess나 Process.Start 자체는 입구에 지나지 않습니다.
정말로 사고율에 효과가 있는 것은 종료 책임의 소재와 I/O의 흘려 끝냄입니다.
11. 참고 자료
- Microsoft Learn, Job Objects
- Microsoft Learn, JOBOBJECT_BASIC_LIMIT_INFORMATION
- Microsoft Learn, UpdateProcThreadAttribute
- Microsoft Learn, InitializeProcThreadAttributeList
- Microsoft Learn, Inheritance (Processes and Threads)
- Microsoft Learn, CreateProcessW
- Microsoft Learn, Creating a Child Process with Redirected Input and Output
- Microsoft Learn, Pipe Handle Inheritance
- Microsoft Learn, Process.Kill
- Microsoft Learn, Process.CloseMainWindow
- Microsoft Learn, GenerateConsoleCtrlEvent
- Microsoft Learn, WaitForSingleObject
- Microsoft Learn, RegisterWaitForSingleObject
- Microsoft Learn, GetExitCodeProcess
- Microsoft Learn, JOBOBJECT_ASSOCIATE_COMPLETION_PORT
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows에서 어디까지 싱글 바이너리로 만들 수 있는가 - 1 EXE로 가능한 범위, Windows 의존이 남는 곳, 배포 전 판단표
Windows 앱을 1 EXE로 만들고 싶을 때, 배포물을 합치는 것과 OS 의존을 없애는 것은 다릅니다. .NET single-file·Native AOT·정적 링크의 한계와 WebView2·WinUI·서비스·드라이버에 남는 의존을 판단표로 정...
공유 메모리를 사용할 때의 함정과 베스트 프랙티스 - 동기, 가시성, 수명, ABI, 보안을 먼저 정리
공유 메모리는 단순히 빠른 IPC가 아니라 동기, 가시성, 수명, ABI, 권한의 책임을 앱 측이 떠맡는 구조입니다. 본 글은 함정과 베스트 프랙티스를 정리하여 SPSC 링 버퍼나 더블 버퍼, 고정 헤더, 오프셋 참조 등 사고율을 내리는 설계 첫...
C#을 Native AOT로 네이티브 DLL로 만드는 방법 - UnmanagedCallersOnly로 C/C++에서 호출하기
.NET의 Native AOT와 UnmanagedCallersOnly로 C# 클래스 라이브러리를 네이티브 DLL로 발행해 C/C++에서 in-process로 호출하는 구성을, 핸들 기반 수명 관리와 에러 코드, C ABI 설계 요령으로 정리합니다.
ClickOnce란 무엇인가 - 구조, 업데이트, 어울리는 장면・어울리지 않는 장면을 실무 시점에서 정리
ClickOnce가 무엇이고 매니페스트, 캐시, 업데이트, 서명이 어떻게 맞물려 동작하는지를 Mermaid 그림과 함께 정리하고, 사내용 .NET 데스크톱 앱 배포에서 어울리는 안건과 어울리지 않는 안건을 실무 시점에서 판단할 수 있도록 도와드립니다.
시리얼 통신 앱의 함정 - 1 byte 단위, 타임아웃, 플로우 컨트롤, 재접속, USB 변환, UI 프리즈를 먼저 정리
시리얼 통신 앱이 가끔 멈추거나 응답이 어긋나는 진짜 원인은 byte stream의 메시지 경계, 타임아웃, 재접속, single writer 설계에 있습니다. 실무에서 무너지기 쉬운 함정과 먼저 정리할 체크리스트를 한 번에 정리했습니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크