Foundry
운영체제
중급
핵심

fork()와 exec()

Unix 프로세스 생성과 프로그램 실행

fork()와 exec()

유닉스에서 프로세스를 생성하고 새 프로그램을 실행하는 핵심 시스템 콜

fork(): 프로세스 복제

부모 프로세스(PID 100)가 fork 를 부르면 자식(PID 101)이 복제됩니다.

호출한 쪽fork 반환값
부모자식의 PID (101)
자식0
실패-1
pid_t pid = fork();
if (pid == 0) {
    // 자식 프로세스
    printf("I am child\n");
} else {
    // 부모 프로세스
    printf("Child PID: %d\n", pid);
}

exec(): 프로그램 교체

[프로세스 PID=101]
  현재: shell 코드
    ↓ exec("ls")
[프로세스 PID=101]
  이제: ls 코드
  (PID 유지, 내용만 교체!)

fork() + exec() 패턴

주체하는 일
Shell (부모)fork 로 자신을 복제하고 wait 로 자식이 끝나기를 기다린다
복제된 자식exec 로 자기 자신을 ls 프로그램으로 바꿔 실행한다
자식 종료 후exit 가 부모에게 알려지고 Shell 이 다시 입력을 받는다

이것이 셸에서 명령어를 실행하는 방식!

Copy-on-Write (COW)

fork() 직후:
부모와 자식이 같은 메모리 페이지 공유
(복사 안 함! 읽기 전용으로 공유)

자식이 메모리 수정 시:
→ 해당 페이지만 복사 후 수정
→ 필요한 만큼만 복사 = 효율적!
[fork 직후]
부모 Page 1 ─┐
             ├→ 물리 메모리 (공유)
자식 Page 1 ─┘

[자식이 Page 1 수정]
부모 Page 1 → 물리 메모리 A (원본)
자식 Page 1 → 물리 메모리 B (복사본)

wait()와 좀비 방지

pid_t pid = fork();
if (pid == 0) {
    exec("ls");  // 자식: 프로그램 실행
} else {
    wait(NULL);  // 부모: 자식 종료 대기
    // 자식 종료 후 계속 실행
}

wait() 안 하면 → 좀비 프로세스 발생!

실무 활용

상황패턴
웹 서버 (Apache)fork()로 워커 생성
셸 명령 실행fork() + exec()
데몬 프로세스fork() 후 부모 종료
Redis BGSAVEfork() + COW로 스냅샷
면접에서 이렇게 나옵니다

Q.fork()와 exec()의 역할과 차이를 설명해주세요.

호출하는 일
fork현재 프로세스를 복제해 자식을 만든다
exec현재 프로세스의 내용을 다른 프로그램으로 바꾼다

fork 는 프로세스를 하나 늘리고, exec 는 늘리지 않고 내용만 교체합니다.

fork 의 반환값으로 부모와 자식을 구분합니다.

호출한 쪽반환값
부모자식의 PID
자식0
실패음수

둘을 나눠 둔 이유는 그 사이에 환경을 설정할 수 있기 때문입니다. 자식에서 표준 출력을 파일로 바꾸거나 권한을 낮춘 뒤 exec 를 부르면, 새 프로그램이 그 설정을 물려받습니다. 셸의 리다이렉션이 이 구조로 동작합니다.

흔한 실수: exec 가 새 프로세스를 만든다고 답하는 것. PID 는 그대로이고 실행 중인 프로그램만 바뀝니다. 성공하면 원래 코드로 돌아오지 않습니다.

Q.Copy-on-Write(COW)란 무엇이고 왜 사용하나요?

fork 할 때 메모리를 실제로 복사하지 않고 공유해 두었다가, 쓰려는 순간에만 복사하는 기법입니다.

시점하는 일
fork 직후부모와 자식이 같은 물리 페이지를 가리킨다. 모두 읽기 전용으로 표시한다
쓰기 시도페이지 폴트가 나고, 커널이 그때 복사해 각자에게 준다

이 기법이 없으면 fork 마다 프로세스 전체 메모리를 복사해야 합니다. 1GB 를 쓰는 프로세스라면 1GB 복사입니다.

특히 잘 맞는 경우가 fork 직후 exec 입니다. 어차피 메모리를 통째로 갈아엎을 것이라 복사가 전부 낭비인데, 쓰기 시 복사면 거의 아무것도 복사하지 않습니다.

흔한 실수: fork 가 메모리를 안 쓴다고 답하는 것. 페이지 테이블은 복사해야 하고, 이후 쓰는 만큼 실제 복사가 일어납니다. 부모가 큰 메모리를 계속 수정하면 결국 상당 부분이 복사됩니다.

Q.셸에서 명령어 실행 과정을 fork/exec로 설명해주세요.

순서주체하는 일
1fork 로 자기를 복제한다
2자식필요한 설정을 한다. 리다이렉션, 파이프 연결
3자식exec 로 자신을 해당 프로그램으로 바꾼다
4부모(셸)wait 로 자식이 끝나기를 기다린다
5자식exit 로 종료하고 종료 코드를 남긴다
6종료 코드를 거두고 다시 입력을 받는다

2번이 fork 와 exec 를 나눠 둔 이유입니다. ls > out.txt 는 자식에서 표준 출력을 파일로 바꾼 뒤 exec 를 부르는 것으로 구현됩니다. ls 자체는 리다이렉션을 모릅니다.

백그라운드 실행(&)은 4번을 건너뛰는 것입니다. 셸이 기다리지 않고 바로 다음 입력을 받습니다.

흔한 실수: 셸이 프로그램을 직접 실행한다고 답하는 것. 자기를 복제한 뒤 그 복제본을 프로그램으로 바꾸는 두 단계입니다.

Q.fork() 후 wait()를 안 하면 어떤 문제가 생기나요?

자식이 끝나도 종료 상태가 거둬지지 않아 좀비 프로세스로 남습니다.

자식이 exit 한다
커널은 종료 코드를 남겨둔다. 부모가 물어볼 수 있어야 하므로
부모가 wait 하지 않으면 그 항목이 계속 남는다

좀비는 메모리를 쓰지 않지만 프로세스 표의 항목과 PID 를 점유합니다. 서버가 자식을 계속 만드는 구조라면 쌓여서 새 프로세스를 만들 수 없게 됩니다.

해결방법
명시적으로 거둔다wait 나 waitpid 호출
신호 처리기자식 종료 신호를 받아 그때 거둔다
자동 정리자식 종료 신호를 무시로 설정하면 커널이 대신 거둔다

부모가 먼저 죽으면 자식은 고아가 되고 init 이 입양해 대신 거둡니다. 그래서 좀비는 부모가 살아 있으면서 거두지 않을 때 문제가 됩니다.

흔한 실수: 좀비가 메모리를 점유한다고 답하는 것. 메모리는 이미 해제됐고 프로세스 표 항목만 남습니다.

먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.

더 깊이 공부하기

읽었으면 문제로 확인해보세요

운영체제 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.