Linux의 fork 함수 내부 동작을 설명해 주세요
- 예상 시간
- 6분
fork()는 현재 프로세스를 복제해 자식 프로세스를 만드는 시스템 콜입니다. 부모와 자식은 코드, 데이터, 열린 파일 디스크립터, 메모리를 공유하며 시작하지만 독립적인 프로세스입니다. 실제 메모리 복사는 즉시 일어나지 않고 Copy-on-Write로 수정이 생기는 순간에만 복사합니다.
부모 프로세스에서는 자식의 PID가 반환되고, 자식 프로세스에서는 0이 반환됩니다. 이 반환값으로 부모와 자식이 각자 다른 코드를 실행할 수 있습니다.
fork는 Unix 설계 철학의 핵심입니다. "새 프로세스를 만들 때 처음부터 새로 만들지 말고 현재 프로세스를 복제하라"는 아이디어입니다. 복제 후 자식 프로세스에서 exec()를 호출해 다른 프로그램을 실행하는 패턴이 표준입니다. 터미널에서 명령어를 실행하면 shell이 fork해 자식을 만들고, 자식이 exec로 해당 명령어 프로그램을 실행합니다.
꼬리질문
조금 더 깊게 물어본다면
답변 뒤에 이어질 수 있는 질문들을 하나씩 열어볼 수 있어요.
fork 호출 시 내부적으로 무슨 일이 일어나나요?
커널이 부모 프로세스의 PCB(Process Control Block)를 복사해 새 자식 프로세스를 만듭니다. 메모리 페이지는 즉시 복사하지 않고 Copy-on-Write로 표시합니다. 자식 프로세스는 부모와 같은 물리 페이지를 읽기 전용으로 공유합니다.
복제되는 것들:
- 코드, 데이터, 힙, 스택 (COW로 공유)
- 열린 파일 디스크립터 (같은 파일 테이블 엔트리 공유)
- 환경 변수
- 신호 핸들러
복제되지 않는 것들:
- PID (자식은 새 PID를 받음)
- 부모의 대기 중인 신호
- 파일 락
pid_t pid = fork();
if (pid == 0) {
// 자식 프로세스: pid == 0
printf("자식: %d\n", getpid());
} else if (pid > 0) {
// 부모 프로세스: pid == 자식 PID
printf("부모: 자식 pid = %d\n", pid);
} else {
// fork 실패
perror("fork");
}Copy-on-Write(COW)가 fork에서 어떻게 동작하나요?
fork 직후 부모와 자식이 같은 물리 페이지를 읽기 전용으로 공유합니다. 어느 쪽이든 페이지에 쓰기를 시도하면 MMU가 page fault를 발생시키고, OS가 그 페이지를 복사해 해당 프로세스에게 독립된 복사본을 줍니다.
COW가 없으면 fork 시 부모의 모든 메모리를 즉시 복사해야 합니다. 1GB 메모리를 쓰는 프로세스라면 fork마다 1GB를 복사해야 합니다. 하지만 대부분의 fork는 바로 exec()를 호출해 다른 프로그램을 실행하므로 복사한 메모리가 낭비됩니다.
COW는 실제로 수정이 일어나는 페이지만 복사합니다. exec() 전에 변경이 없으면 복사 비용이 거의 없습니다. Redis RDB 스냅샷도 이 원리를 씁니다. fork해 자식이 스냅샷을 디스크에 쓰는 동안 부모는 계속 요청을 처리합니다. 수정된 페이지만 COW로 분리됩니다.
fork와 exec는 왜 분리되어 있나요?
fork로 프로세스를 복제하고 exec로 다른 프로그램을 올리는 두 단계를 분리하면, 그 사이에 환경을 설정할 수 있습니다. 파일 디스크립터를 조작하거나, 신호 핸들러를 바꾸거나, 사용자 ID를 변경하는 작업이 가능합니다.
shell이 명령어를 실행하는 과정이 대표적입니다.
shell → fork() → 자식 프로세스
↓ (리다이렉션, 파이프 설정)
exec("ls")
↓
ls 프로그램이 자식 프로세스 공간에 올라감ls > output.txt처럼 출력을 파일로 리다이렉션할 때, fork 후 exec 전에 자식 프로세스가 stdout(fd 1)을 output.txt로 바꿉니다. exec 후 ls는 자기 stdout이 파일인 줄 모르고 그냥 출력합니다. 이런 유연성이 fork/exec를 분리한 이유입니다.
좀비 프로세스란 무엇인가요?
자식 프로세스가 종료됐지만 부모가 아직 wait()로 종료 상태를 수집하지 않은 상태입니다. 프로세스 테이블에 항목은 남아있지만 실행은 하지 않습니다.
자식이 종료되면 커널은 종료 코드를 부모가 가져갈 수 있도록 프로세스 테이블 항목을 남겨둡니다. 부모가 wait()나 waitpid()를 호출해 이 정보를 수집하면 항목이 제거됩니다.
부모가 wait()를 호출하지 않으면 자식은 좀비로 남습니다. 메모리는 이미 해제됐지만 프로세스 테이블 항목이 누적됩니다. PID가 고갈되면 새 프로세스를 만들 수 없습니다.
부모가 자식보다 먼저 죽으면 자식은 고아(orphan) 프로세스가 됩니다. 이때 init 프로세스(PID 1)가 고아를 입양해 wait()를 대신 처리합니다.
한 줄 정리
fork는 현재 프로세스를 COW 방식으로 복제해 자식을 만들고, 자식은 주로 exec로 다른 프로그램을 실행합니다.