종료·통지·수거는 다른 시점
do_exit()는 현재 task의 종료 작업을 진행하고 exit_notify()를 호출한다. exit_notify() 안에서 잠시 EXIT_ZOMBIE를 설정하지만, 조건에 따라 바로 EXIT_DEAD로 바꿔 release_task()에 넘기거나, zombie를 남겨 기다리는 쪽에서 수거하게 한다. 따라서 “종료 뒤 항상 zombie → wait → release” 한 줄은 맞지 않는다.
원문 범위와 확인 한계
첫 블록은 do_exit() 전체가 아닌 kernel/exit.c L946–984의 연속 부분 발췌다. 함수 진입과 PF_EXITING, group_dead 설정은 L898–924, exit_notify() 뒤의 마무리와 do_task_dead()는 L985–1016에 있다. 둘째 블록은 exit_notify() 함수 전체 L738–781다.
여기서 EXIT_ZOMBIE는 특정 분기에서 남는 상태이며 자원이 모두 유지된다는 뜻이 아니다. release_task()의 내부와 마지막 task 참조 해제 시점, 특정 부모의 wait 동작은 별도 함수에서 확인해야 한다.
도식 참고와 버전 경계: 문C 블로그 Scheduler -1- (Basic)의 태스크 상태 절과 해당 상태 그림에서 종료 흐름과 부모 wait를 다른 구간으로 읽는 표현을 참고했다. 이 절에는 고정 커널 버전 표기가 없고 글은 2017년에 게시됐으므로 v6.18.37 동작의 근거로 쓰지 않는다. 아래 그림은 원도를 복제하지 않고 do_exit()·exit_notify()와 이후 wait_task_zombie()를 별도 단계로 그렸다. 스케줄러의 task->__state와 exit_state를 하나의 상태축으로 합치지 않는다.
같은 태그의 원본 파일: kernel/exit.c · kernel/signal.c · kernel/fork.c. 이 페이지의 코드 두 블록과 주요 도해 근거는 kernel/exit.c이고, 뒤의 두 파일은 추가 읽기 위치다.
두 연속 코드 발췌
1. do_exit 후반: trace에서 exit_notify까지
kernel/exit.c:946–984 · 함수 일부만 표시하며, 종료의 시작과 최종 task 해제까지 전부 보여 주지는 않는다.
tsk->exit_code = code;
taskstats_exit(tsk, group_dead);
unwind_deferred_task_exit(tsk);
trace_sched_process_exit(tsk, group_dead);
/*
* Since sampling can touch ->mm, make sure to stop everything before we
* tear it down.
*
* Also flushes inherited counters to the parent - before the parent
* gets woken up by child-exit notifications.
*/
perf_event_exit_task(tsk);
exit_mm();
if (group_dead)
acct_process();
exit_sem(tsk);
exit_shm(tsk);
exit_files(tsk);
exit_fs(tsk);
if (group_dead)
disassociate_ctty(1);
exit_task_namespaces(tsk);
exit_task_work(tsk);
exit_thread(tsk);
sched_autogroup_exit_task(tsk);
cgroup_exit(tsk);
/*
* FIXME: do that only when needed, using sched_exit tracepoint
*/
flush_ptrace_hw_breakpoint(tsk);
exit_tasks_rcu_start();
exit_notify(tsk, group_dead);trace_sched_process_exit()는 exit_mm()보다 앞이다. 이 trace를 보았다는 사실만으로 mm/files/fs 정리 완료나 부모 통지가 끝났다고 판단하지 않는다.
2. exit_notify 전체: zombie와 autoreap
kernel/exit.c:738–781 · reparent, 조건별 통지와 release_task() 호출까지다.
static void exit_notify(struct task_struct *tsk, int group_dead)
{
bool autoreap;
struct task_struct *p, *n;
LIST_HEAD(dead);
write_lock_irq(&tasklist_lock);
forget_original_parent(tsk, &dead);
if (group_dead)
kill_orphaned_pgrp(tsk->group_leader, NULL);
tsk->exit_state = EXIT_ZOMBIE;
if (unlikely(tsk->ptrace)) {
int sig = thread_group_leader(tsk) &&
thread_group_empty(tsk) &&
!ptrace_reparented(tsk) ?
tsk->exit_signal : SIGCHLD;
autoreap = do_notify_parent(tsk, sig);
} else if (thread_group_leader(tsk)) {
autoreap = thread_group_empty(tsk) &&
do_notify_parent(tsk, tsk->exit_signal);
} else {
autoreap = true;
/* untraced sub-thread */
do_notify_pidfd(tsk);
}
if (autoreap) {
tsk->exit_state = EXIT_DEAD;
list_add(&tsk->ptrace_entry, &dead);
}
/* mt-exec, de_thread() is waiting for group leader */
if (unlikely(tsk->signal->notify_count < 0))
wake_up_process(tsk->signal->group_exec_task);
write_unlock_irq(&tasklist_lock);
list_for_each_entry_safe(p, n, &dead, ptrace_entry) {
list_del_init(&p->ptrace_entry);
release_task(p);
}
}EXIT_ZOMBIE를 설정한 뒤 ptrace·thread group leader·non-leader 경로에 따라 autoreap이 달라진다. autoreap일 때만 이 함수가 EXIT_DEAD로 바꾸고 dead 목록에서 release_task()를 호출한다.
exit_notify의 세 조건을 펼쳐 보기
함수 시작에서 forget_original_parent()로 자식의 부모 관계를 처리한 뒤(L744–745), EXIT_ZOMBIE를 설정한다. 이후 부모 알림과 자동 수거 판단은 ptrace 및 thread group 상태에 따라 달라진다.
exit_signal 또는 SIGCHLD로 do_notify_parent()를 호출해 autoreap을 받는다.L752–757do_notify_parent()를 호출할 수 있다.L758–760autoreap = true; do_notify_pidfd()를 호출한다.L761–765EXIT_DEAD로 바꾸고 dead 목록을 나중에 순회하며 release_task()를 호출한다.L767–779release_task() 호출은 제거·정리 단계지만 “이 시점에 마지막 task 참조가 즉시 free된다”는 뜻은 아니다. 구현은 L244–313을 따로 본다.남은 zombie를 wait가 읽거나 수거할 때
wait_task_zombie()는 WEXITED가 없으면 건너뛰고, WNOWAIT이면 상태를 수거하지 않고 정보만 읽는 경로를 택한다(L1170–1190). 일반 경로는 EXIT_ZOMBIE에서 EXIT_DEAD 또는 ptrace용 EXIT_TRACE로 원자 전이하며(L1191–1201), 뒤쪽에서 state == EXIT_DEAD일 때만 release_task()를 호출한다(L1264–1277).
cmpxchg로 zombie를 DEAD 또는 TRACE로 바꿀 수 있는 쪽이 소유한다.L1191–1201release_task(p) 호출. 이 함수 내부도 별도 단계다.L1276–1277 · L244–313| 조건 | 표시된 상태 처리 | release_task | 근거 |
|---|---|---|---|
WNOWAIT | 상태를 전이하지 않고 정보 반환 | 이 경로에는 없음 | L1180–1190 |
| 일반 wait | zombie→DEAD 또는 TRACE 원자 전이 시도 | DEAD면 뒤에서 호출 | L1191–1201, L1264–1277 |
| ptrace TRACE | 재통지 후 zombie 또는 DEAD로 다시 설정 | DEAD로 끝날 때만 호출 | L1264–1277 |
관찰 위치가 뜻하는 것
trace_sched_process_exit()는 L946–960에서 exit_mm() 전에 호출된다. 종료 trace가 발생해도 자원 정리나 exit_notify()가 끝났다는 뜻은 아니다. 사후 /proc/<pid>/status의 상태 표시 역시 특정 내부 호출이 언제 완료됐는지 직접 말해 주지 않는다.
실행 환경에서 종료 원인을 분리하려면 do_exit(), exit_notify(), wait_task_zombie() 중 어느 경계를 보고 있는지 먼저 정한다. 이 글은 검증되지 않은 ftrace event 이름이나 일괄 필터 명령을 제시하지 않는다.
이어서 읽을 위치
release_task() 안에서는 process/thread/PID 목록에서 제거하고 put_task_struct_rcu_user()를 호출한다(L244–313). 실제 마지막 참조와 최종 메모리 해제까지 단정하려면 RCU와 task 참조 수명 경로를 더 읽어야 한다. 개념 설명·함수 원문·그림·GIF은 종료 경로의 넓은 맥락을 보는 별도 자료다.