전체 흐름
Linux v6.6의 종료는 호출 중인 태스크가 do_exit()에서 자원을 정리하고, exit_notify()가 부모·추적자와의 관계를 정리한 뒤, 별도의 wait 또는 자동 수거 경로가 수거 절차를 진행하는 과정이다. exit() 호출과 release_task()를 같은 시점으로 읽으면 안 된다.
점선 화살표는 반드시 wait가 실행된다는 뜻이 아니다. 자동 수거 시 release_task()는 do_task_dead()보다 먼저 실행될 수도 있다.
소스 코드 위치
먼저 확인할 Linux v6.6 소스 파일
Linux v6.6: kernel/exit.c
Linux v6.6: kernel/signal.c
Linux v6.6: kernel/fork.c
설명: 종료와 wait의 상태 전이는 kernel/exit.c에 있고, 부모의 자동 수거 판단은 kernel/signal.c의 do_notify_parent()에도 걸린다. kernel/fork.c는 생성 경로와 대조할 때 참고한다.
GitHub 원본:
대표 코드
아래 첫 발췌는 do_exit()의 앞부분만, 둘째 발췌는 exit_notify()의 잠금 해제까지 보여 준다. 뒤따르는 자원 정리와 dead 목록의 release_task()는 v6.6 do_exit 뒷부분과 exit_notify 뒷부분에서 따로 확인해야 한다.
void __noreturn do_exit
signal->group_stop_count = 0;
}
spin_unlock_irq(&sighand->siglock);
}
void __noreturn do_exit(long code)
{
struct task_struct *tsk = current;
int group_dead;
WARN_ON(irqs_disabled());
synchronize_group_exit(tsk, code);
WARN_ON(tsk->plug);
kcov_task_exit(tsk);
kmsan_task_exit(tsk);
coredump_task_exit(tsk);
ptrace_event(PTRACE_EVENT_EXIT, code);
user_events_exit(tsk);
validate_creds_for_do_exit(tsk);
io_uring_files_cancel();
exit_signals(tsk); /* sets PF_EXITING */
/* sync mm's RSS info before statistics gathering */
if (tsk->mm)
sync_mm_rss(tsk->mm);
acct_update_integrals(tsk);
group_dead = atomic_dec_and_test(&tsk->signal->live);
if (group_dead) {
/*
* If the last thread of global init has exited, panic
* immediately to get a useable coredump.
*/
if (unlikely(is_global_init(tsk)))
panic("Attempted to kill init! exitcode=0x%08x\n",
확인 사항: 이 발췌는 exit_signals()의 PF_EXITING 설정과 thread group의 live 감소까지 보인다. exit_mm()·tracepoint·파일/파일시스템 정리·exit_notify()는 발췌 뒤에 있다.
exit_notify
/*
* Send signals to all our closest relatives so that they know
* to properly mourn us..
*/
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;
}
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);
확인 사항: 잠금 안에서 우선 EXIT_ZOMBIE가 되고, ptrace·leader·non-leader 분기에 따라 autoreap가 달라진다. 발췌 뒤의 dead 목록 순회에서만 release_task()가 보인다.
함수별 분석
do_exit()는 오류를 반환하며 복구하는 경로가 아니라 __noreturn 경로다. exit_notify()가 부모에게 종료를 알리는 시점과 release_task()가 참조를 정리하는 시점은 분리된다.
do_notify_parent()의 true는 ptrace가 아닌 SIGCHLD 통지에서 부모가 SIGCHLD를 명시적으로 무시하거나 SA_NOCLDWAIT를 사용한 경우다. 그룹 리더는 살아 있는 형제 스레드가 있으면 즉시 부모 통지·자동 수거되지 않는다.
| 함수·행 | 하는 일 | 구분할 시점 |
|---|---|---|
do_exit()exit.c 809–926 | exit_mm() 후 tracepoint를 찍고, 파일·fs·thread 등 정리 뒤 exit_notify()와 do_task_dead()로 간다. | tracepoint가 최종 release를 뜻하지 않는다. |
exit_notify()exit.c 727–767 | 재부모화 후 EXIT_ZOMBIE, 분기별 자동 수거 여부를 결정한다. | 조건부 EXIT_DEAD와 dead 목록. |
do_notify_parent()signal.c 2058–2154 | 부모/추적자 통지, SIGCHLD 처리 설정에 따른 self-reaping 반환. | 통지와 장기 zombie 여부는 다르다. |
wait_task_zombie()exit.c 1099–1210 | WNOWAIT는 관찰만, 일반 수거는 상태 소유권을 가져와 조건부 release. | 부모의 별도 실행 문맥. |
구조체와 필드
종료가 모두 한 필드에 기록되는 것은 아니다. task_struct.__state는 스케줄러의 실행 가능 상태이고 exit_state는 wait/reap 상태다. exit_code와 signal의 group 종료 정보는 부모가 보고할 상태와 자원 통계에 쓰인다.
종료하는 task_struct
__state — 스케줄러 상태; 끝에서 do_task_dead().
exit_state — EXIT_ZOMBIE / EXIT_DEAD / ptrace EXIT_TRACE.
exit_code, signal — wait가 읽을 종료 코드·thread group 통계.
부모·추적자와 남은 자식
parent / real_parent — 알림·wait 주체.
children — forget_original_parent()에서 새 reaper로 이동.
wait_task_zombie() — exit_state와 종료 정보를 확인.
exit_mm(), exit_files(), exit_fs()가 수행되어도 부모가 읽어야 할 종료 상태와 통계 때문에 task_struct는 조건에 따라 더 남는다.| 필드·관계 | v6.6에서 확인할 의미 |
|---|---|
__state / exit_state | 각각 runnability와 종료 수거 상태. 동일한 상태 기계가 아니다. |
exit_code / signal->group_exit_code | wait는 SIGNAL_GROUP_EXIT 조건에 따라 보고할 코드를 고른다. |
parent / real_parent | ptrace 관계와 자연 부모가 다를 수 있어 통지·수거 분기가 달라진다. |
children / signal->live | 자식 재부모화와 마지막 thread 판정은 서로 다른 관계다. |
실행 단계와 상태 변화
좀비는 exit_notify()에서 일단 설정되지만, 항상 오래 유지되는 상태는 아니다. 자동 수거면 같은 함수 안에서 EXIT_DEAD로 바뀐다. 부모가 상태를 수거하는 경로는 종료 태스크의 do_exit() 호출 스택 밖에 있다.
| 단계 | 조건 | 남는 것 / 다음 경로 |
|---|---|---|
EXIT_ZOMBIE | exit_notify가 먼저 설정, autoreap이 false | 부모·추적자의 wait 대상. |
즉시 EXIT_DEAD | exit_notify의 autoreap이 true | dead 목록에서 release_task 호출. |
WNOWAIT | wait_task_zombie에서 WEXITED와 WNOWAIT | 정보만 반환, 수거하지 않음. |
| wait 수거 | cmpxchg로 EXIT_ZOMBIE 점유 | ptrace 조건에 따라 EXIT_TRACE를 경유하거나 EXIT_DEAD 뒤 release. |
불변 조건과 실패 사례
exit_mm()와 exit_files() 뒤에도 exit_state·exit_code가 부모 wait를 위해 남을 수 있다. 좀비 존재만으로 mm 또는 열린 파일이 그대로 남는다는 뜻은 아니다.
exit_notify()는 tasklist_lock 아래 재부모화와 상태 전이를 수행하고, 자동 수거 대상의 release_task()는 잠금을 푼 다음 호출한다.
wait_task_zombie()의 cmpxchg가 EXIT_ZOMBIE 전환을 한 waiter에게만 허용한다. WNOWAIT는 이 전환을 하지 않는다.
leader가 먼저 끝났어도 다른 스레드가 남으면 부모 통지가 지연될 수 있다. 마지막 non-leader의 release_task()가 zombie leader를 다시 살펴본다.
tracepoint를 관찰하고 곧바로 파일·fs 정리와 task_struct release까지 끝났다고 해석하지 않는다.
모든 EXIT_ZOMBIE가 장기간 남는다고 단정하지 않는다. SIGCHLD 처리 설정, ptrace, thread group 조건을 함께 본다.
계측과 검증
sched_process_exit 이벤트는 v6.6 do_exit()에서 exit_mm() 뒤, exit_files()·exit_fs() 앞에 발생한다. 따라서 이 이벤트만으로 파일 정리·부모 통지·wait 수거 완료를 주장할 수 없다. sched_process_wait는 do_wait() 진입의 관찰점이지 반드시 수거 성공을 뜻하지 않는다.
| 관찰점 | 말할 수 있는 것 | 말할 수 없는 것 |
|---|---|---|
sched_process_exit | exit_mm() 후 exit 경로가 이 지점에 도달. | 파일·fs 정리, exit_notify, release 완료. |
sched_process_wait | wait 호출 경로에 진입. | WNOWAIT 여부나 실제 수거 성공. |
/proc/<pid>/status | 관찰 순간의 State·PPid 등. | 짧은 EXIT_ZOMBIE→EXIT_DEAD 전이의 완전한 기록. |
# tracefs가 이미 제공되는 테스트 환경에서 이벤트 형식만 확인
cat /sys/kernel/tracing/events/sched/sched_process_exit/format
cat /sys/kernel/tracing/events/sched/sched_process_wait/format
# 살아 있는 대상으로만; PID는 실험 대상 값으로 교체
cat /proc/<pid>/status실제 기록이 필요하면 격리된 실험 환경에서 해당 이벤트만 선택하고, 이벤트의 타임스탬프와 별도로 wait 결과를 비교한다. 전역 tracer·filter를 바꾸는 명령은 이 글의 읽기 절차에 포함하지 않는다.
추가 확인 사항
- 종료 대상이 thread group leader인가, 다른 thread가 남아 있는가, ptrace 관계가 있는가?
- 부모의 SIGCHLD 설정이 명시적 SIG_IGN 또는 SA_NOCLDWAIT인가? 그렇다면
do_notify_parent()는 언제 true를 반환하는가? - 부모가 WNOWAIT로 관찰했는가, 아니면
wait_task_zombie()가 cmpxchg로 수거권을 가져왔는가? - tracepoint 시점이
exit_mm()뒤이면서 파일·fs 정리 앞이라는 점을 실험 결과 해석에 반영했는가?