fork 는 부모를 복사해 자식을 만들고, exec 는 그 프로세스의 메모리를 새 프로그램으로 덮어쓰기 합니다. 그래서 fork 다음 exec 를 부르는 것이 기본 형태입니다.
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 BGSAVE | fork() + COW로 스냅샷 |