QUESTION
큰 process에서 fork 직후 메모리가 두 배가 되지 않는 이유는 무엇인가?
fork는 새 task와 mm 관련 구조를 만들지만 anonymous page 내용을 전부 복사하지 않는다. parent와 child PTE를 write-protect하고 같은 physical page를 참조하게 한 뒤 어느 쪽이 쓰면 page fault에서 private copy를 만든다.
copy-on-write는 공짜가 아니다. page table 복제, TLB shootdown, 이후 write fault와 page copy 비용이 있으며 multithread process에서는 fork 후 child가 호출할 수 있는 함수도 제한된다.
STRUCTURE
구조 그림
Parent page table
- VA 0x4000
- PTE: read-only + COW
- mapcount reference
Before write
- Physical page A
- content=1
- parent + child 공유
Child page table
- VA 0x4000
- PTE: read-only + COW
- write fault 발생
After child write
- Parent → page A
- Child → new page B
- content B=2
fork 직후 parent와 child PTE는 같은 physical page를 read-only/COW로 가리킨다. child의 첫 쓰기에서만 새 page가 생긴다.
CALL PATH
호출 흐름
fork 시점의 복제와 child가 page를 쓸 때의 복제를 구분한다. RSS 합계만 보면 shared page를 중복 계산해 실제 물리 사용량을 오해할 수 있다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| kernel/fork.c | kernel_clone(), copy_process() | 새 task와 공유/복제 flag 결정 |
| kernel/fork.c | copy_mm(), dup_mm() | CLONE_VM 여부와 mm_struct 수명 |
| mm/memory.c | copy_page_range(), do_wp_page() | COW page table과 write-protect fault |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 fork_cow.c -o fork_cow01#define _DEFAULT_SOURCE
02#include <stdio.h>
03#include <stdlib.h>
04#include <sys/mman.h>
05#include <sys/wait.h>
06#include <unistd.h>
07
08int main(void)
09{
10 size_t length = 16 * 1024 * 1024;
11 unsigned char *area = mmap(NULL, length, PROT_READ | PROT_WRITE,
12 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
13 if (area == MAP_FAILED)
14 return 1;
15 for (size_t i = 0; i < length; i += 4096)
16 area[i] = 1;
17
18 pid_t pid = fork();
19 if (pid < 0)
20 return 1;
21 if (pid == 0) {
22 for (size_t i = 0; i < length; i += 4096)
23 area[i]++;
24 _exit(area[0] == 2 ? 0 : 2);
25 }
26
27 int status;
28 if (waitpid(pid, &status, 0) < 0)
29 return 1;
30 printf("parent=%u child_status=%d\n", area[0], WEXITSTATUS(status));
31 return munmap(area, length) != 0;
32}
CODE NOTES
코드 조각별 설명
MAP_PRIVATE | MAP_ANONYMOUS파일과 무관한 private mapping을 만든다. fork 뒤 처음에는 page를 공유하지만 한 process의 쓰기가 다른 process에 보이지 않는다.
i += 4096예제는 일반적인 4 KiB page마다 한 byte를 써 physical page를 미리 fault-in한다. 실제 page size는 sysconf(_SC_PAGESIZE)로 구해야 한다.
pid_t pid = forkparent에는 child PID, child에는 0이 돌아오며 두 실행 흐름 모두 같은 다음 instruction에서 시작한다.
area[i]++;child의 첫 쓰기마다 write-protect fault가 발생해 private physical page가 만들어지는 관찰 지점이다.
_exit(area[0]fork 후 child에서 stdio buffer를 다시 flush하지 않고 syscall 수준 종료를 사용한다.
DETAILS
세부 동작
복제되는 것은 page 내용보다 mapping 규칙이다
VMA는 address range, protection, file/anonymous backing을 설명한다. fork는 VMA tree와 page table을 복제하고 writable private mapping PTE를 양쪽에서 read-only처럼 다뤄 write fault를 유도한다.
MAP_SHARED mapping과 실제 shared memory는 COW 대상이 아니며 쓰기가 같은 backing page에 보인다.
multithread + fork에는 좁은 안전 구간이 있다
fork를 호출한 thread만 child에 남는다. 다른 thread가 잡고 있던 userspace mutex는 잠긴 상태로 복제될 수 있지만 풀어 줄 thread는 존재하지 않는다.
exec 전 child에서는 async-signal-safe 함수만 호출하고, 복잡한 준비는 parent에서 끝내거나 posix_spawn을 고려한다.
메모리 사용량은 PSS로 확인한다
parent와 child RSS를 단순 합산하면 공유 중인 COW page를 두 번 센다. /proc/PID/smaps_rollup의 Pss, Private_Dirty, Shared_Dirty를 fork 직후와 child write 뒤에 비교한다.
transparent huge page가 켜져 있으면 write fault 한 번의 분할·복사 단위가 관찰 결과에 영향을 준다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
task_struct | copy_process에서 생기고 release_task에서 최종 해제된다 | pid, state, files/mm pointer |
mm_struct | fork에서 복제되고 마지막 mm user가 빠질 때 해제된다 | VMA, page table, mm_users |
COW page | fork 전 page를 공유하다 write fault에서 private page로 나뉜다 | mapcount, PSS, dirty |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| fork가 ENOMEM/EAGAIN | memory commit, pid/cgroup/user process limit | ulimit -u, pids.current, overcommit 설정 |
| child에서 deadlock | 사라진 thread가 잡은 userspace lock | pthread_atfork와 child call list |
| fork 뒤 latency 급증 | page table 복제와 COW fault | perf stat page-faults, smaps_rollup |
LAB
직접 확인
- child write loop 앞에서 sleep을 넣고 fork 직후 parent/child smaps_rollup의 Pss를 비교한다.
- write loop를 제거한 실행과 유지한 실행의 minor-faults를 perf stat으로 비교한다.
- MAP_PRIVATE를 MAP_SHARED로 바꾸고 parent에서 area[0] 값이 어떻게 달라지는지 확인한다.
./fork_cowstrace -f -e trace=clone,wait4,mmap,munmap ./fork_cowPRIMARY REFERENCES