- Authors

- Name
- Youngju Kim
- @fjvbn20031
0. 이상한 현상 3가지 — 가상 메모리가 없으면 설명 불가
현상 1: malloc 거짓말
void* p = malloc(1ULL * 1024 * 1024 * 1024 * 100); // 100GB
printf("%p\n", p); // 성공!
물리 RAM 이 16GB 인데 100GB 할당이 성공한다. 왜?
현상 2: fork 의 속도
pid_t pid = fork();
10GB 를 쓰고 있는 프로세스에서 fork() 가 1ms 이하 에 끝난다. 10GB 를 복사하려면 몇 초는 걸려야 할 텐데.
현상 3: top 의 숫자가 안 맞음
PID USER VIRT RES SHR COMMAND
123 alice 2.3G 180M 45M chrome
VIRT (가상 메모리) 가 2.3GB 인데 RES (실제 사용) 가 180MB? 이 셋은 뭐가 다른가?
이 세 가지는 모두 가상 메모리 (Virtual Memory) 라는 같은 뿌리에서 나온다. 이 글은 가상 메모리의 작동 원리와 현대 OS 가 어떻게 이를 레버리지하는지 파헤친다.
1. 가상 메모리 — 왜 발명됐는가
1.1 물리 메모리 직접 접근의 재앙
1960년대 초 프로그램은 물리 주소에 직접 접근했다. 문제가 많았다:
- 프로세스 간 간섭: 프로세스 A 가 프로세스 B 의 메모리를 덮어쓸 수 있음.
- 주소 충돌: 두 프로그램이 같은 주소 0x1000 에 데이터를 두려고 함.
- 메모리 단편화: "사용 가능한 공간 2GB 는 있지만 연속된 1GB 는 없음".
- 확장성 없음: 프로그램이 RAM 보다 클 수 없음.
1.2 1962년 Manchester Atlas 와 Multics 의 혁명
Manchester 대학의 Atlas 컴퓨터 (1962) 가 최초의 가상 메모리 시스템을 구현. MIT Multics (1965) 가 이를 발전시켜 세그먼트 + 페이지 혼합 모델을 제안.
핵심 아이디어:
"각 프로세스에게 자기만의 거대한 가상 주소 공간 을 준다. 물리 RAM 은 OS 가 뒤에서 관리한다."
- 프로세스 A 의 주소 0x1000 과 프로세스 B 의 주소 0x1000 은 다른 물리 메모리 를 가리킨다.
- 가상 주소가 물리보다 클 수 있다 → 디스크로 스와핑 가능.
- OS 가 접근 권한을 페이지 단위로 제어.
1985년 386 CPU 이 x86에 페이징을 도입한 이후 모든 범용 OS 의 표준이 됐다.
2. 페이지 테이블 — 가상→물리 변환의 자료구조
2.1 페이지 단위의 매핑
가상 메모리의 기본 단위는 페이지 (보통 4KB). 물리 RAM 도 같은 크기의 프레임 으로 나뉜다. 페이지 테이블은 "가상 페이지 N → 물리 프레임 M" 매핑 테이블.
왜 4KB 인가?
- 너무 작으면: 페이지 테이블이 커지고 TLB 효율 저하.
- 너무 크면: 내부 단편화 (한 페이지에 5바이트만 쓰는데 4KB 할당).
- 4KB 는 1970년대 VAX 이후 사실상 표준. ARM 은 16KB 도 지원.
2.2 단일 레벨 페이지 테이블의 문제
x86-64 에서 가상 주소는 48비트 (실제 사용), 페이지는 4KB (12비트).
→ 가상 페이지 수 = 2^36 = 640억 개. → 각 엔트리가 8바이트면 페이지 테이블만 512GB.
프로세스마다 512GB 를 페이지 테이블에 쓰는 건 불가능. 해결: 다단계 페이지 테이블.
2.3 x86-64 의 4단계 페이지 테이블 (5단계도 있음)
가상 주소 (48-bit):
[ PML4 (9) | PDPT (9) | PD (9) | PT (9) | Offset (12) ]
PML4 (Page Map Level 4)
↓ [PML4 index 로 엔트리 선택] → PDPT 주소
PDPT (Page Directory Pointer Table)
↓ [PDPT index] → PD 주소
PD (Page Directory)
↓ [PD index] → PT 주소
PT (Page Table)
↓ [PT index] → 실제 물리 프레임 주소
- 각 테이블은 512개 엔트리 (9비트 인덱스).
- 엔트리 크기 8바이트 → 각 테이블 = 4KB (한 페이지에 딱 맞음).
- Sparse 영역 은 상위 엔트리가 NULL → 하위 테이블 아예 만들지 않음.
실제 프로세스의 페이지 테이블은 보통 수 MB 수준.
2.4 페이지 테이블 엔트리의 비트들
x86-64 PTE 구조 (64비트):
[63 NX | 62..52 Reserved | 51..12 Physical Frame Addr | 11..9 AVL | 8 G | 7 PAT | 6 D | 5 A | 4 PCD | 3 PWT | 2 U/S | 1 R/W | 0 P]
주요 비트:
- P (Present): 0 이면 "이 페이지는 현재 RAM 에 없음" → 접근 시 페이지 폴트.
- R/W: 쓰기 가능 여부.
- U/S: 사용자 모드 접근 허용.
- A (Accessed): CPU 가 접근 시 자동으로 1 세트 → LRU 힌트.
- D (Dirty): 쓰기 시 자동으로 1 → 디스크 writeback 필요.
- NX (No Execute): 이 페이지에서 코드 실행 금지 → 버퍼 오버플로우 공격 방어.
OS 는 이 비트들을 읽고 쓰면서 메모리 관리를 한다.
2.5 CR3 레지스터 — 프로세스 전환의 핵심
CPU 의 CR3 레지스터는 현재 프로세스의 PML4 주소를 가진다. fork() 나 문맥 전환 시:
ctx_switch():
mov new_process->pgd_phys, %rax
mov %rax, %cr3 # 페이지 테이블 교체!
이 한 줄이 "프로세스 A 의 주소 공간" → "프로세스 B 의 주소 공간" 전환.
부작용: CR3 을 바꾸면 TLB (Translation Lookaside Buffer) 가 대부분 플러시 → 문맥 전환의 숨은 비용.
3. TLB — 주소 변환의 캐시
3.1 4단계 페이지 워크의 비용
단순 계산: 가상 주소 → 물리 주소 변환마다 4번의 메모리 접근 필요 (PML4, PDPT, PD, PT). CPU 명령 하나 실행하려고 메모리 5번 접근 — 재앙.
3.2 TLB 의 구조
TLB 는 "최근에 변환한 가상 → 물리 매핑" 을 저장하는 작은 하드웨어 캐시:
- 크기: 보통 64~1024 엔트리.
- 접근 시간: 1 사이클 (메모리는 100+ 사이클).
- CPU 칩 안에 내장.
TLB hit: 1 cycle
TLB miss: 4-cycle page walk (실제로는 캐시 덕분에 덜 들지만)
3.3 TLB 친화적 코드 작성
TLB 미스를 줄이는 핵심은 공간 지역성:
// 나쁨: 열 우선 접근
for (int j = 0; j < N; j++)
for (int i = 0; i < N; i++)
sum += a[i][j]; // 매번 다른 페이지 접근
// 좋음: 행 우선 접근
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
sum += a[i][j]; // 같은 페이지 내 순차 접근
대규모 배열 (>TLB 용량 × 페이지 크기) 에서 이 차이가 10배 이상 성능 차이로 이어진다.
3.4 TLB 미스의 숨겨진 비용
행렬 곱셈 벤치마크에서 캐시 미스보다 TLB 미스가 병목인 경우가 많다. Intel 의 한 연구에서는 대형 데이터 처리 시 실행 시간의 20-30% 가 TLB 미스 관련.
해결책: Huge Pages (다음 장).
4. Huge Pages — TLB 커버리지 확장
4.1 표준 페이지의 한계
4KB 페이지 × 1024 TLB 엔트리 = 4MB 만 TLB 로 커버 가능. 10GB 짜리 데이터베이스는 TLB 미스 지옥.
4.2 Huge Page 의 해법
x86-64 는 페이지 테이블 계층을 "조기 종료" 로 큰 페이지 지원:
- PD 레벨에서 종료 → 2MB 페이지.
- PDPT 레벨에서 종료 → 1GB 페이지.
2MB 페이지 × 1024 TLB 엔트리 = 2GB 커버 → 데이터베이스, JVM 힙, HPC 배열에 매우 효과적.
4.3 Explicit Huge Pages vs Transparent
Explicit: /proc/sys/vm/nr_hugepages 로 예약. mmap 시 MAP_HUGETLB 명시. Postgres, Oracle, JVM 이 이 방식 활용.
void* p = mmap(NULL, 2*1024*1024, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0);
Transparent (THP): 커널이 자동으로 인접 4KB 페이지들을 2MB 로 병합. 투명 → 앱 수정 불필요.
4.4 THP 의 함정
THP 는 좋아 보이지만 프로덕션에서 악명 높다:
- khugepaged 백그라운드 스캔: CPU 사용.
- 메모리 단편화: 2MB 연속 공간 확보를 위해 compaction → latency spike.
- Redis / MongoDB 가 THP 끄기 권장: 특정 워크로드에서 레이턴시 폭발.
echo never > /sys/kernel/mm/transparent_hugepage/enabled
2015년 Redis 문서에 이 설정이 공식적으로 명시된 이유.
5. 페이지 폴트 — 지연된 할당의 마법
5.1 페이지 폴트의 3가지 유형
프로세스가 가상 주소에 접근했는데 PTE 의 P 비트가 0 → 페이지 폴트 발생. CPU 가 OS 에게 제어권을 넘김. OS 는 이유를 판별:
- Minor Fault (Soft Fault): 페이지가 메모리엔 있지만 이 프로세스 PTE 가 연결 안 됨. 즉시 연결하고 복귀.
- Major Fault (Hard Fault): 페이지가 디스크 (스왑 또는 파일) 에 있음. 디스크 I/O 필요 → 수 ms.
- Invalid Fault: 정말로 권한 없는 접근 → SIGSEGV (Segmentation Fault).
5.2 Demand Paging — malloc 의 거짓말 해설
void* p = malloc(100 * 1024 * 1024); // 100MB
이 순간 커널이 한 일 :
- 가상 주소 공간에 100MB 예약 (mmap 으로).
- 물리 페이지는 한 개도 할당 안 함.
- 페이지 테이블 엔트리 설정, P=0.
memset(p, 0, 100 * 1024 * 1024);
여기서 실제로 접근 → 페이지 폴트 연쇄 → 커널이 접근한 페이지만 물리 프레임 할당 + zero-filling.
이게 "malloc 100GB" 가 즉시 성공하는 이유. 하지만 실제로 쓰려고 하면 그때 OOM 이 발생.
5.3 Overcommit 정책
리눅스의 /proc/sys/vm/overcommit_memory:
- 0 (default): 휴리스틱. "명백히 말도 안 되는" 할당만 거절.
- 1: 항상 허용 (Redis 가 요구).
- 2: 엄격.
swap + RAM × ratio를 넘으면 거절.
모드 0 에서 malloc 은 낙관적으로 성공시키지만, 실제 접근 시 물리 메모리가 부족하면 OOM Killer 가 등장한다 (7장).
6. mmap — 모든 메모리 매핑의 통합 인터페이스
6.1 malloc 은 사실 mmap + brk
glibc malloc 의 내부:
- 작은 할당 (128KB 미만 기본):
brk시스템 콜로 힙 확장. - 큰 할당 (128KB 이상):
mmap(MAP_ANONYMOUS)로 독립 매핑.
brk 는 연속된 힙 영역을 관리하는 포인터를 움직이는 것. mmap 은 아무 가상 주소에나 새 매핑을 만든다.
6.2 파일 매핑 — mmap 의 진짜 위력
int fd = open("huge.dat", O_RDONLY);
char* data = mmap(NULL, file_size, PROT_READ, MAP_SHARED, fd, 0);
// 이제 data[i] 가 파일의 i번째 바이트!
무엇이 일어났나?
- 가상 주소 공간에 file_size 만큼 영역 예약.
- 접근 시 페이지 폴트 → 커널이 해당 페이지만 디스크에서 읽음.
- 페이지 캐시에 저장되고 공유됨 → 다른 프로세스가 같은 파일 mmap 시 같은 물리 메모리 공유.
6.3 mmap vs read 의 트레이드오프
mmap 장점:
- 단일 주소 공간에서 큰 파일 접근 (포인터 연산).
- 페이지 캐시 공유.
- 시스템 콜 오버헤드 없음 (접근은 일반 메모리 접근).
mmap 단점:
- 페이지 폴트가 예측 불가능한 지연 을 유발.
- 큰 파일에 무작위 접근 시 TLB 미스 증가.
- Signal handler 에서 SIGBUS (디스크 오류 시) 처리가 까다로움.
- 공유 매핑에 쓸 때 동기화가 미묘 (
msync필요).
2020 CMU 연구 ("Are You Sure You Want to Use MMAP in Your Database?") 는 DBMS 가 mmap 을 쓰지 말라고 경고. I/O 예측성과 에러 처리가 이유.
6.4 익명 매핑과 공유
MAP_PRIVATE | MAP_ANONYMOUS : 프로세스 전용, 파일 없음 (일반 malloc)
MAP_SHARED | MAP_ANONYMOUS : 프로세스 간 공유 (shm)
MAP_PRIVATE | fd : 파일 기반, 쓰기는 CoW
MAP_SHARED | fd : 파일 기반, 쓰기가 파일에 반영
MAP_SHARED | MAP_ANONYMOUS 는 부모-자식 (fork 후) 또는 POSIX shared memory 에서 활용.
7. Copy-on-Write — fork 가 빠른 이유
7.1 fork 의 전통적 모델 vs CoW
전통: 부모의 모든 메모리를 자식에게 복사. 10GB 프로세스 → 10GB 복사 → 수 초.
CoW (Copy-on-Write): 페이지 테이블만 복사. 물리 페이지는 공유. 쓰기 발생 시에만 그 페이지를 복사.
7.2 CoW 내부 동작
fork() 직후:
Parent PT: VA 0x1000 → PA 0x42000 (R--, Shared) # W 비트 꺼짐!
Child PT: VA 0x1000 → PA 0x42000 (R--, Shared)
Child 가 0x1000 에 쓰기:
페이지 폴트 (R-- 에 쓰기 시도)
OS: 이건 CoW 페이지다 → PA 0x42000 을 새 페이지 PA 0x80000 에 복사
Child PT: VA 0x1000 → PA 0x80000 (RW-)
쓰기 재시도 → 성공
7.3 exec 이 따라올 때 — fork 의 허세
일반적 패턴:
pid_t pid = fork();
if (pid == 0) {
execvp("ls", argv); // 자식 프로세스 이미지 완전 교체
}
exec 이 모든 CoW 페이지를 버리므로 사실상 fork 의 CoW 복사는 낭비. 해결: posix_spawn 또는 vfork — 페이지 테이블 복사도 생략.
7.4 Redis RDB 저장의 구현
Redis 는 백그라운드 저장 시 fork 를 쓴다:
부모: 계속 쿼리 처리 (쓰기 → CoW 발생 → 메모리 증가)
자식: 시점의 일관된 스냅샷을 디스크에 기록
문제: 부모가 계속 쓰면 쓰기 증폭 → 메모리 피크. 최악의 경우 메모리 2배 필요. 2018년 이전 Redis 운영자의 흔한 악몽.
7.5 Kernel Same-page Merging (KSM)
반대 방향: 여러 VM 이 같은 내용의 페이지를 가지고 있으면 하나로 병합. 페이지 A, B 가 같은 바이트 → 모두 PA 0x42000 을 가리키고 원본은 해제. 쓰기 시 CoW.
KVM, Xen 같은 하이퍼바이저에서 메모리 오버커밋에 활용. CPU 비용이 크므로 기본 비활성.
8. OOM Killer — 리눅스의 최후 수단
8.1 OOM 의 순간
물리 메모리 + 스왑 모두 고갈 → 커널이 어떻게든 메모리를 회수해야 함. oom_killer 가 등장.
8.2 OOM 점수 계산
/proc/<pid>/oom_score: 각 프로세스에 점수 매김.
- RSS 크기 비례 (큰 놈이 죽을 확률 ↑).
oom_score_adj로 사용자가 조정 (-1000 ~ 1000).- root 프로세스는 덜 죽음.
- 자식이 많은 프로세스는 죽어도 효과 작다고 판단.
8.3 잔혹한 현실
Out of memory: Killed process 1234 (postgres) total-vm:...kB, anon-rss:...kB
dmesg 에 이 메시지가 찍히는 순간 postgres 가 죽었다. 경고 없이, 즉시. 시그널 SIGKILL, 핸들러 실행 불가, 정상 종료 루틴 없음. 트랜잭션이 날아갈 수 있음.
8.4 회피 전략
oom_score_adj = -1000: "이 프로세스는 절대 죽이지 마". 모니터링 / DB 를 보호.
echo -1000 > /proc/<pid>/oom_score_adj
swap 확보: 스왑이 있으면 OOM 전에 페이지 아웃.
cgroup memory limit: 컨테이너 단위로 메모리 제한 → 한 놈이 전체를 잡아먹지 않게.
overcommit=2: 엄격 모드. malloc 이 실패하게 만들고 OOM 을 사전 방지. 단, 많은 앱이 overcommit 을 전제로 설계돼 있어서 부작용 큼.
8.5 Early OOM — Ubuntu 22.04 의 선제 대응
시스템이 OOM 에 가깝게 가면 일반 시스템이 매우 느려진다 (스와핑 지옥). earlyoom 은 이 현상을 감지해 OOM 보다 먼저 메모리 사용이 큰 프로세스를 죽임. Fedora, Ubuntu 22.04+ 에서 기본.
9. Swap — 디스크를 메모리처럼
9.1 스왑의 역할
물리 메모리가 부족할 때 덜 쓰이는 페이지를 디스크로 내보내고, 필요 시 다시 읽어들임.
- Swap partition vs Swap file: 현대 리눅스에서는 성능 차이 거의 없음.
- zswap, zram: 압축된 메모리를 스왑처럼 사용 (디스크 I/O 없음).
9.2 swappiness — 얼마나 적극적으로 스왑할지
/proc/sys/vm/swappiness (0-100):
- 0: 스왑을 최후 수단으로.
- 60 (기본): 균형.
- 100: 적극적 스왑.
DB 서버는 보통 10-20 으로 설정 → "캐시는 유지, 프로세스 메모리는 가능한 한 RAM 에".
9.3 Anonymous vs File-backed
커널은 두 종류를 구분:
- Anonymous memory (heap, stack): 스왑 아웃되면 swap 영역에 저장.
- File-backed memory (mmap'd files, code): 스왑 필요 없이 원본 파일에서 다시 읽으면 됨.
실무에서 "메모리 부족" 이 보이면 file-backed 가 먼저 제거됨 → 반복해서 디스크 I/O.
10. 프로세스 주소 공간의 레이아웃
10.1 64비트 리눅스 프로세스
0xffffffffffffffff ┌────────────────┐
│ 커널 공간 │
0xffff800000000000 ├────────────────┤
│ (사용 불가) │ ← 48-bit 가상 주소 한계
0x0000800000000000 ├────────────────┤
│ 스택 (↓ 성장) │
├────────────────┤
│ ... │
│ (빈 공간) │
│ ... │
├────────────────┤
│ mmap 영역 │ ← 공유 라이브러리 등
├────────────────┤
│ ... │
│ (빈 공간) │
│ ... │
├────────────────┤
│ 힙 (↑ 성장) │
├────────────────┤
│ BSS │
├────────────────┤
│ Data │
├────────────────┤
│ Text (코드) │
0x0000000000400000 ├────────────────┤
│ (매핑 안됨) │ ← NULL 포인터 보호
0x0000000000000000 └────────────────┘
10.2 ASLR — 주소 무작위화
보안을 위해 스택, 힙, mmap, 실행 코드 시작 주소를 매 실행마다 랜덤화:
cat /proc/self/maps # 주소가 매번 다름
버퍼 오버플로 공격자가 return 주소를 맞히기 어려워짐.
10.3 /proc/<pid>/maps 읽기
555555554000-55555555c000 r--p 00000000 08:01 ... /usr/bin/cat
55555555c000-555555568000 r-xp 00008000 08:01 ... /usr/bin/cat
...
7ffff7dc0000-7ffff7de8000 r--p 00000000 08:01 ... /usr/lib/libc.so.6
...
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
7ffffffff000-... r-xp 00000000 00:00 0 [vdso]
각 라인이 하나의 VMA (Virtual Memory Area). 권한, 파일, offset 을 보여준다. 디버깅의 보물창고.
10.4 vDSO — 시스템 콜의 빠른 경로
gettimeofday(), clock_gettime() 같은 자주 호출되는 시스템 콜은 vDSO 페이지로 매핑된 코드를 호출 → 커널 모드 전환 없이 실행.
100ns 이하의 시스템 콜. 시간 측정이 핫패스인 앱에서 결정적.
11. NUMA — 다중 소켓 서버의 현실
11.1 Uniform 이 아닌 Memory Access
멀티 소켓 서버에서 CPU 가 자기 로컬 메모리에 접근하면 빠르고, 다른 소켓 메모리에 접근하면 느림 (보통 2-3배).
$ numactl --hardware
node 0 size: 64GB
node 1 size: 64GB
node distances:
0 1
0: 10 21 # 다른 노드 접근은 2배 느림
1: 21 10
11.2 NUMA 친화 할당
기본 정책 (numactl --localalloc): "이 스레드가 실행 중인 CPU 의 로컬 노드에서 메모리 할당".
문제:
- 스레드가 다른 CPU 로 이동하면 메모리는 원래 노드에 남음 → 원격 접근.
- DB 가 수십 GB 메모리를 하나의 노드에 몰빵하는 실수.
11.3 실무 팁
- Redis, Postgres:
numactl --interleave=all로 시작. 메모리를 노드 간 라운드 로빈 → 원격 접근 평균화. - JVM:
-XX:+UseNUMA로 GC 가 NUMA 인식. - 메모리 피닝: 특정 스레드를 특정 CPU 에 고정 (
taskset).
12. 컨테이너와 가상 메모리
12.1 cgroup v2 메모리 제한
# 컨테이너의 메모리 상한
memory.max = 512M
memory.high = 400M # soft limit: 이걸 넘으면 reclaim 압력
12.2 memory.high 의 의미
hard limit 만 있으면 컨테이너가 상한에서 갑자기 OOM 당함. memory.high 는 soft limit — 도달하면 커널이 compaction, direct reclaim 을 적극 → throttle.
12.3 JVM / Node.js 의 흔한 함정
컨테이너에서 JVM 이 "호스트의 전체 메모리" 를 보고 힙을 설정 → cgroup 상한을 넘어 OOM.
해결: Java 10+ 의 -XX:+UseContainerSupport (기본 ON), Node.js 의 --max-old-space-size.
13. 실무 체크리스트
문제 진단:
top의 VIRT/RES/SHR: 가상/실제/공유 — 셋 다 봐야 정확./proc/<pid>/status의 VmRSS, VmData, VmSwap: 세밀한 통계./proc/meminfo: 시스템 전체 메모리 브레이크다운 (MemFree, Buffers, Cached, SwapUsed).vmstat 1: 1초마다si,so(스왑 인/아웃) 모니터링. 0 이 아니면 swap 중.sar -B 1: 페이지 폴트 통계 (minor/major).
성능 튜닝:
vm.swappiness=10: DB 서버.vm.overcommit_memory=1: Redis.- THP off: Redis, MongoDB.
- Huge pages 예약: Postgres의
huge_pages=on, JVM-XX:+UseLargePages.
OOM 방지:
- 모니터링 프로세스에
oom_score_adj=-1000. - cgroup memory limit 설정.
earlyoom활용.
컨테이너:
- JVM:
-XX:+UseContainerSupport확인. - Node:
--max-old-space-size를 컨테이너 한도의 80%. - Go:
GOMEMLIMIT(1.19+).
14. 마치며 — 투명한 추상화의 비용
가상 메모리는 프로그래머에게 투명 한 추상화처럼 보인다 — 그냥 malloc 하면 되는 것 같다. 하지만 실제로는:
malloc(100GB)의 성공에는 overcommit 이 있다.fork()의 속도에는 Copy-on-Write 가 있다.mmap의 편리함 뒤에는 예측 불가능한 페이지 폴트 가 있다.- 데이터베이스 서버의 안정성 뒤에는 Huge Pages, THP off, oom_score_adj 튜닝이 있다.
"memory is just memory" 라고 말하는 개발자와 "virtual memory is a beautiful lie" 라고 말하는 개발자의 차이가 여기서 나온다.
다음 글에서는 리눅스 I/O 의 진화 — 동기 read/write 에서 시작해서 epoll, io_uring 까지 — 를 파볼 예정이다. 왜 Node.js 가 싱글 스레드로 10만 연결을 처리할 수 있고, 왜 io_uring 이 수십 년 만의 I/O 혁명인지.
참고 자료
- "Understanding the Linux Kernel" — Bovet & Cesati, 3rd ed — Chapter 8-9 (Memory Management).
- Mel Gorman — "Understanding the Linux Virtual Memory Manager" (Prentice Hall, 2004).
- Ulrich Drepper — "What Every Programmer Should Know About Memory" (2007) — 필독.
- "Are You Sure You Want to Use MMAP in Your Database Management System?" — Crotty, Leis, Pavlo (CIDR 2022).
- Linux kernel documentation: Documentation/vm/ (hugetlbpage, transhuge, overcommit-accounting).
- Brendan Gregg — "Linux Performance" 블로그 시리즈.
- Operating Systems: Three Easy Pieces — Remzi Arpaci-Dusseau — Chapters 13-23 (Virtualization of Memory).