| 항목 | 내용 |
|---|---|
| 프로젝트 | Linux Kernel |
| 관련 릴리스 | 7.1 (2026-06-14 tag) — 기능 자체는 7.1 머지 윈도우에서 vfs-7.1-rc1.pidfs 태그로 2026-04-13에 mainline에 들어감 |
| 관련 커밋 | 12ae2c81 — clone: add CLONE_AUTOREAP, 24baca56 — clone: add CLONE_NNP, c8134b5f — pidfd: add CLONE_PIDFD_AUTOKILL, 병합 커밋 a2900f5a |
| 작성자/리뷰어 | Christian Brauner(VFS/pidfd 메인테이너, 제안 및 구현), Oleg Nesterov(리뷰) |
| 계기가 된 이슈 | uapi-group/kernel-features#45 — Child PID auto-reaping (or equivalent) (2026-02-02, 이식 가능한 C++ I/O 라이브러리 개발자가 제기) |
| 발견 출처 | kernelnewbies.org Linux_7.1 "Process management" 항목, LWN.net: As ye clone(), so shall ye AUTOREAP |
| 검증 근거 | torvalds/linux 실제 커밋 diff(kernel/fork.c, kernel/signal.c, kernel/ptrace.c, fs/pidfs.c, include/uapi/linux/sched.h, include/uapi/linux/pidfd.h), 병합 커밋 메시지, uapi-group 이슈 원문 |
유닉스 계열 프로세스 모델에서 자식 프로세스가 종료되면 부모가 wait()로 회수(reap)하기 전까지 "좀비" 상태로 남는다. 부모가 이를 잊으면 좀비가 쌓이는데, 지금까지 커널이 제공한 유일한 자동 회수 수단은 SIGCHLD를 SIG_IGN으로 설정하는 것뿐이었고 이는 그 부모의 모든 자식에게 일괄 적용됐다. 서브프로세스를 만드는 라이브러리 입장에서는 자신이 만든 자식만 선택적으로 자동 회수하고 싶어도 이 전역 스위치로는 불가능했다. Linux 7.1은 clone3()에 CLONE_AUTOREAP 플래그를 추가해 이 문제를 자식 하나 단위로 풀고, 여기에 CLONE_NNP·CLONE_PIDFD_AUTOKILL을 더해 "pidfd를 닫으면 자식이 즉시 죽는다"는 컨테이너/서비스 매니저용 생명주기 관리 메커니즘까지 함께 도입했다.
프로세스가 exit()하면 커널은 그 자리에서 태스크 구조체를 곧바로 없애지 않는다. 부모가 나중에 wait()/waitpid()로 종료 상태(exit code, 시그널 등)를 읽어갈 수 있어야 하기 때문에, 자식은 커널 태스크 리스트에 "좀비"(EXIT_ZOMBIE) 상태로 남아 최소한의 정보만 유지한 채 대기한다. 부모가 wait()를 호출하면 그 정보를 넘겨받고 나서야 커널이 태스크를 완전히 해제(EXIT_DEAD → 회수)한다. 부모가 wait()를 영영 호출하지 않으면 좀비는 그 부모가 죽을 때까지(또는 프로세스 테이블이 찰 때까지) 남는다.
이 부담을 피하려는 부모는 전통적으로 두 가지 중 하나를 쓴다: SIGCHLD를 SIG_IGN으로 설정하거나, sigaction()에 SA_NOCLDWAIT를 지정한다. 둘 다 커널이 자식 종료 시 좀비 상태를 건너뛰고 즉시 회수하게 만들지만, 이 설정은 그 프로세스가 앞으로 낳는 모든 자식에 적용되는 프로세스 전역 속성이다 — 특정 자식만 선택적으로 자동 회수하고 다른 자식은 wait()로 직접 관리하는 것이 불가능하다.
pidfd(PID file descriptor)는 PID 대신 프로세스를 가리키는 파일 디스크립터다. CLONE_PIDFD 플래그로 clone3()을 호출하면 커널이 새 프로세스의 pidfd를 돌려주는데, 이 fd는 poll()로 자식의 종료를 감시할 수 있고(PID 재사용 경쟁 없이 그 프로세스만 정확히 가리킴), pidfd_send_signal()로 시그널을 보낼 수 있고, PIDFD_GET_INFO ioctl로 종료 상태를 읽을 수 있다. clone3()은 구버전 clone() 시스템 콜과 달리 struct clone_args를 통해 플래그를 64비트 폭으로 확장 가능하게 설계돼 있어, 새 동작을 추가할 때 새 플래그 비트 하나만 정의하면 된다 — 이번 변경의 CLONE_AUTOREAP(1ULL << 34), CLONE_NNP(1ULL << 35), CLONE_PIDFD_AUTOKILL(1ULL << 36)도 이 방식으로 추가됐다.
[변경 전] SIGCHLD 기반 auto-reap: 프로세스 전역, 자식 단위 선택 불가
부모 프로세스
├─ signal->action[SIGCHLD] = SIG_IGN (또는 SA_NOCLDWAIT)
│ │
│ ▼ 이 설정은 "이 부모가 낳는 모든 자식"에 적용됨
│
├─ 자식 A (라이브러리가 관리하고 싶은 프로세스) ──exit()──▶ 즉시 회수(회피 불가)
├─ 자식 B (애플리케이션이 wait()로 직접 관리하려던 프로세스) ──exit()──▶ 마찬가지로 즉시 회수됨
│ (SIGCHLD도 안 옴, wait()도 -ECHILD)
└─ 자식 C ...
문제: 라이브러리가 "내가 만든 프로세스만" 자동 회수하려 해도
SIG_IGN/SA_NOCLDWAIT은 그 부모의 다른 컴포넌트(다른 라이브러리, 앱 코드)가
만든 자식에도 그대로 적용된다. 반대로 앱이 SIGCHLD 핸들러를 쓰면
라이브러리의 자동 회수 의도가 깨진다 — 전역 스위치라 공존이 불가능하다.
uapi-group/kernel-features#45(2026-02-02)에서 이식 가능한 C++ I/O 라이브러리 개발자가 지적한 것이 정확히 이 문제였다: "라이브러리는 전역 시그널 핸들러 같은 걸 쓸 수 없다 — 그러면 자신이 만들지 않은 자식에도 영향을 준다. 그렇다고 회수 책임을 사용자에게 떠넘길 수도 없다." 이 요구를 만족하려면 지금까지는 별도 스레드를 만들어 epoll로 자식들의 pidfd를 감시하며 회수하는 우회책을 써야 했다.
Christian Brauner가 작성한 12ae2c81("clone: add CLONE_AUTOREAP")은 struct signal_struct(include/linux/sched/signal.h)에 1비트 플래그를 추가한다.
/* include/linux/sched/signal.h */
unsigned int is_child_subreaper:1;
unsigned int has_child_subreaper:1;
unsigned int autoreap:1; /* 새로 추가 */clone3() 호출 시 CLONE_AUTOREAP이 지정되면 kernel/fork.c의 copy_process()가 이 플래그를 그 자식 하나의 signal_struct에만 기록한다 — 부모의 시그널 핸들러 설정은 전혀 건드리지 않는다. 자식이 종료할 때 kernel/signal.c의 do_notify_parent()가 이 플래그를 확인한다:
/* kernel/signal.c, do_notify_parent() */
if (!tsk->ptrace && tsk->signal->autoreap) {
autoreap = true;
sig = 0; /* SIGCHLD를 보내지 않음 */
}autoreap == true가 되면 exit_notify() 경로가 좀비 상태를 거치지 않고 태스크를 곧바로 EXIT_DEAD로 전이시킨다. SIGCHLD도 보내지 않으므로(exit_signal이 0이어야만 이 플래그를 쓸 수 있도록 강제됨) 부모는 wait()를 호출할 필요도, 호출할 수도 없다(-ECHILD가 반환됨) — 대신 CLONE_PIDFD를 함께 지정했다면 그 pidfd를 poll()하고 PIDFD_GET_INFO로 종료 상태를 읽으면 된다.
이 플래그는 자식의 signal_struct에 저장되므로 원래 부모가 먼저 죽어서 자식이 subreaper나 init으로 재부모화(reparent)되더라도 그대로 유지된다 — 재부모화된 곳이 다시 SIGCHLD를 받고 회수해야 하는 번거로움 없이 자식이 스스로 자동 회수된다. copy_process()는 CLONE_THREAD(자동회수는 프로세스 단위 속성이므로 스레드에는 의미 없음)와 CLONE_PARENT(만약 autoreap 자식이 CLONE_PARENT로 손자를 낳으면 그 손자가 exit_signal == 0은 물려받으면서 autoreap 플래그는 못 물려받아 "시그널도 안 보내고 회수도 안 되는" 깨진 좀비가 될 수 있음) 조합을 -EINVAL로 거부한다. kernel/ptrace.c의 __ptrace_detach()에도 동일한 autoreap 검사가 추가돼, ptrace로 추적 중인 자식에 대해서도 일관되게 동작한다.
왜 CLONE_PIDFD_AUTOKILL은 CLONE_PIDFD와 CLONE_AUTOREAP을 둘 다 요구하는가: c8134b5f("pidfd: add CLONE_PIDFD_AUTOKILL")는 clone3()이 돌려준 pidfd의 마지막 참조가 닫히는 순간(fs/pidfs.c의 .release 핸들러) 자식에게 SIGKILL을 보낸다. CLONE_PIDFD가 없으면 애초에 생명주기를 묶을 fd 자체가 없고, CLONE_AUTOREAP이 없으면 SIGKILL로 죽은 자식이 회수해줄 사람 없이 좀비로 남는다 — autokill의 주 사용 사례(부모 프로세스가 크래시하거나 pidfd를 방치)는 애초에 회수해줄 부모가 없는 상황이므로, autoreap이 빠지면 이 기능은 좀비를 양산하는 기능이 돼버린다.
/* fs/pidfs.c, pidfs_file_release() */
static int pidfs_file_release(struct inode *inode, struct file *file)
{
...
if (!(file->f_flags & PIDFD_AUTOKILL))
return 0;
...
do_send_sig_info(SIGKILL, SEND_SIG_PRIV, task, PIDTYPE_TGID);
return 0;
}주목할 점은 이 동작이 clone3()이 만든 그 pidfd 파일 하나에만 묶인다는 것이다. 같은 프로세스를 가리키는 다른 pidfd(예: pidfd_open()으로 나중에 딴 것)는 이 파일과 별개의 struct file이라 PIDFD_AUTOKILL 플래그를 갖지 않으므로 그 fd를 닫아도 자식은 죽지 않는다 — dup()이나 fork()로 같은 fd를 공유한 경우에만(같은 struct file을 참조하므로) 마지막 참조가 사라질 때까지 생명이 연장된다. PIDFD_AUTOKILL은 실제로는 pidfd 파일에는 쓰이지 않는 open() 플래그인 O_TRUNC 비트를 내부용으로 재사용하며, PIDFD_STALE/PIDFD_THREAD/PIDFD_NONBLOCK과 비트가 겹치지 않는지 BUILD_BUG_ON(hweight32(...) != 4)로 컴파일 타임에 강제한다.
권한 모델을 CLONE_NNP 유무로 가른 이유: CLONE_PIDFD_AUTOKILL을 CLONE_NNP(child에 no_new_privs를 강제) 없이 쓰려면 호출자가 자신의 유저 네임스페이스에서 CAP_SYS_ADMIN을 가져야 한다. CLONE_NNP와 함께 쓰면 비특권 사용자도 허용된다 — no_new_privs가 설정된 자식은 setuid/setgid 실행으로도 권한을 절대 상승시킬 수 없으므로, 예기치 않게 SIGKILL당하는 상황이 권한 상승 경로로 악용될 여지가 없기 때문이다. 커밋 메시지는 이를 기존의 PR_SET_PDEATHSIG(부모 사망 시그널) 메커니즘과 명시적으로 대비한다 — pdeath_signal은 secureexec/commit_creds() 중에 리셋돼버려서, 스스로 권한을 낮추는(deprivilege) 컨테이너 런타임에는 애초에 쓸모가 없었다. CLONE_PIDFD_AUTOKILL은 이 구멍을 pidfd 소유권 기반 모델로 대체한 것이다.
[변경 후] pidfd에 생명주기를 묶어 "부모가 사라지면 자식도 죽는다"
부모(컨테이너 런타임 / 서비스 매니저)
│
│ clone3(CLONE_PIDFD | CLONE_AUTOREAP | CLONE_PIDFD_AUTOKILL [| CLONE_NNP])
▼
┌───────────────┐ signal->autoreap = 1
│ 자식 프로세스 │◀────── exit_signal = 0 (SIGCHLD 없음)
│ (워크로드) │
└───────┬───────┘
│ 정상 종료
▼
do_notify_parent(): autoreap=1 → 좀비 상태 건너뛰고 EXIT_DEAD
(부모는 pidfd를 poll()하고 PIDFD_GET_INFO로 종료 상태 확인)
부모가 pidfd를 닫음(크래시/방치/의도적 종료)
│
▼
pidfs_file_release(): PIDFD_AUTOKILL 플래그 확인
│
▼
do_send_sig_info(SIGKILL) → 자식 즉시 종료
│
▼
autoreap=1이므로 회수해줄 부모 없이도 좀비로 남지 않고 바로 정리됨
초기 구현이 커버하지 않는 것: CLONE_AUTOREAP은 명시적으로 opt-in이며 기존 wait()/SIGCHLD 기반 코드와는 아무 상호작용이 없다 — 즉 기존 프로그램의 동작은 전혀 바뀌지 않는다(하위 호환에 대한 트레이드오프가 없는 순수 추가 기능). 대신 그만큼 "이미 짜여진 자식 관리 로직을 자동으로 최적화해주는" 효과도 없다 — 라이브러리/애플리케이션이 코드를 고쳐서 이 플래그를 명시적으로 요청해야만 혜택을 받는다.
- glibc의
posix_spawn()이나 libuv 같은 널리 쓰이는 프로세스 스폰 라이브러리들이CLONE_AUTOREAP을 실제로 채택하기까지 얼마나 걸릴까? CLONE_PIDFD_AUTOKILL이 systemd 같은 서비스 매니저의 기존 cgroup 기반 정리 메커니즘과 함께 쓰일 때 두 메커니즘이 경쟁하거나 중복 종료를 시도하는 경우는 없을까?- 재부모화(reparenting)된 autoreap 자식이 subreaper 쪽에서 종료를 알아야 하는 경우(예: 상태 로깅)를 위한
prctl()확장이, 커밋 메시지가 언급한 대로 실제로 뒤따라 나올까?
clone: add CLONE_AUTOREAP(12ae2c81)clone: add CLONE_NNP(24baca56)pidfd: add CLONE_PIDFD_AUTOKILL(c8134b5f)- 병합 커밋: "pidfd: add CLONE_AUTOREAP, CLONE_NNP, and CLONE_PIDFD_AUTOKILL" (a2900f5a)
- uapi-group/kernel-features#45 — Child PID auto-reaping (or equivalent)
- Linux_7.1 — Linux Kernel Newbies
- As ye clone(), so shall ye AUTOREAP — LWN.net
본문 참고 자료로 충분했다.