프로세스 생명주기
fork는 복제, exec는 교체, wait는 정리 지점이다
정상 실행은 부모가 자식을 만들고, 자식이 프로그램 이미지를 바꾸며, 부모가 종료 상태를 회수하는 순서로 읽는다. 좀비와 고아는 이 회수 책임이 어긋난 경우다.
fork
새 PID를 만들지만 주소 공간은 COW로 늦게 복사된다.
exec
PID는 유지하고 코드와 데이터 이미지만 교체한다.
wait
종료 상태를 읽으면서 커널의 남은 기록을 제거한다.
정상 경로를 레인별로 읽기
parent child kernel status
01 prepare
외부 명령 실행 준비
부모인자, 환경 변수, 파이프, 리다이렉션을 정리한다.
자식아직 없음. 분기는 fork 이후에 생긴다.
커널호출 가능 여부와 자원 한도를 확인한다.
상태실행 전이므로 회수할 종료 기록은 없다.
02 fork
하나의 호출이 두 흐름으로 반환
부모자식 PID를 받아 계속 실행한다.
자식반환값 0으로 자식 분기를 탄다.
커널COW 주소 공간과 FD 테이블 참조를 만든다.
상태실패하면 -1만 반환되고 자식은 없다.
03 exec
자식 이미지를 새 프로그램으로 교체
부모foreground 명령이면 waitpid 준비 상태가 된다.
자식성공한 exec는 돌아오지 않는다.
커널코드, 데이터, 스택을 새 이미지로 바꾼다.
상태exec 다음 줄 실행은 실패 경로라는 뜻이다.
04 exit
자식이 결과를 남기고 종료
부모아직 wait 전이면 자식 결과를 읽지 못했다.
자식코드와 스택은 사라지고 실행은 끝난다.
커널PID 항목과 종료 status를 잠시 보관한다.
상태이 틈이 길어지면 좀비로 보인다.
05 wait
부모가 종료 상태를 회수
부모WEXITSTATUS, WTERMSIG 등으로 결과를 해석한다.
자식실행 실체는 이미 없고 결과만 남아 있었다.
커널wait가 읽은 뒤 프로세스 테이블 기록을 지운다.
상태회수 완료. 다음 프롬프트나 부모 로직으로 돌아간다.
정상 경로가 어긋나는 세 지점
zombie
자식은 끝났지만 부모가 아직 wait하지 않음
child exit
Z 상태
wait 필요
메모리는 거의 사라졌지만 PID와 종료 status가 남는다. 장기 서버는 SIGCHLD에서 waitpid를 반복해 누적을 막는다.
orphan
부모가 먼저 사라져 자식의 회수 책임이 이동
parent exit
reparent
init
현재 PID namespace의 init 또는 subreaper가 입양한다. 컨테이너 PID 1은 reaping 책임을 분명히 가져야 한다.
exec fail
이미지 교체 실패 후 자식이 부모 로직으로 새면 안 됨
exec -1
errno
_exit(127)
exec가 성공하면 반환하지 않는다. 다음 줄이 실행되면 실패 처리 후 즉시 종료해 중복 부모 동작을 막는다.
운영에서 보는 확인 순서
COW 비용
fork 직후 전체 메모리를 복사하지 않고 쓰는 페이지부터
복사한다.
FD 상속
exec 전에 close-on-exec와 리다이렉션을 정리해야 누수가
줄어든다.
회수 책임
부모, SIGCHLD 핸들러, PID 1이 누가 wait할지 정해야 한다.