컴퓨터의 주기억장치(DRAM)는 1비트마다 트랜지스터 1개와 커패시터 1개로 구성된 미세 물리 셀들의 집합체입니다.
운영체제(OS)는 한정된 물리 RAM을 여러 프로세스가 번갈아 할당받고 해제(free)하도록 중계합니다.
0으로 초기화된 대형 메모리를 확보하기 위해 malloc 직후 memset을 수행하는 방식은 하드웨어 자원을 극심하게 낭비합니다.
void *ptr = malloc(1024 * 1024 * 100); // 100MB 가상 메모리 할당
memset(ptr, 0, 1024 * 1024 * 100); // 100MB 전체에 물리적 0 쓰기 루프 실행
calloc은 메모리 요청 시점의 실제 물리 메모리 기록을 생략하고, MMU(Memory Management Unit)와 페이징 하드웨어를 제어하여 할당 속도를 상수 시간 O(1)로 단축합니다.
가상 주소를 물리 주소로 변환하기 위해 프로세스마다 존재하는 다단계 페이지 테이블의 최하위 행(Entry)을 PTE(Page Table Entry)라고 부르며, 8바이트(64비트) 크기를 갖습니다.
| 비트 필드 | 명칭 | 기능 및 상태 |
|---|---|---|
| Bit 0 | Present (P) | 1: 물리 RAM에 프레임 존재 / 0: 미할당 또는 스왑 디스크 상태 (접근 시 Page Fault 유발) |
| Bit 1 | Read/Write (R/W) | 1: 읽기 및 쓰기 모두 허용 / 0: 읽기 전용 (쓰기 시도 시 Page Fault 유발) |
| Bit 2 | User/Supervisor (U/S) | 1: 사용자 모드 접근 허용 / 0: 커널 모드 전용 접근 |
| Bit 5 | Accessed (A) | 해당 페이지의 읽기/쓰기 참조 발생 시 하드웨어가 1로 설정 (페이지 교체 시 참조) |
| Bit 6 | Dirty (D) | 해당 페이지에 쓰기 연산 발생 시 하드웨어가 1로 설정 (스왑 동기화 판단) |
| Bit 12~51 | PFN (Page Frame Number) | 매핑된 실제 물리 RAM 프레임의 4KB 단위 물리 기준 주소 비트열 |
운영체제 커널은 부팅 시 모든 바이트가 0으로 채워진 단 하나의 물리 4KB 페이지 프레임인 ZERO_PAGE를 물리 RAM에 상주시켜 둡니다.
[프로세스 가상 메모리 공간] [물리 RAM]
가상 페이지 0 (0x1000) ──┐
가상 페이지 1 (0x2000) ──┼──(PTE: Present=1, R/W=0)──▶ [물리 ZERO_PAGE (4KB 단일 프레임)]
가상 페이지 2 (0x3000) ──┘
프로세스가 할당받은 주소 공간을 읽기(Read)만 할 때는 수만 개의 가상 페이지가 단 하나의 물리 ZERO_PAGE를 공유하며 항상 0을 읽어옵니다.
실제 쓰기(Write) 연산이 발생하는 순간의 하드웨어 및 커널 제어 흐름은 다음과 같습니다.
1. 프로그램: 특정 가상 주소에 데이터 쓰기(Write) 명령어 실행
│
2. CPU MMU: 페이지 테이블 조회 ──▶ PTE의 R/W 비트가 0(읽기 전용)임을 감지
│
3. 하드웨어 인터럽트: CPU가 #PF (Page Fault 인터럽트 벡터 14) 발생
│
4. 제어권 이관: 커널의 Page Fault 핸들러로 전환
│
5. 예외 원인 판별: 핸들러가 VMA를 검사하여 정당한 쓰기 영역(CoW 대상)인지 불법 주소(Segmentation Fault)인지 확인
│
6. 물리 프레임 분리: 커널이 빈 물리 메모리에서 새 4KB 프레임 1장을 즉각 할당
│
7. 데이터 복제: 기존 ZERO_PAGE의 내용(4KB의 0)을 새로 할당된 물리 프레임으로 1회 복사
│
8. PTE 재설정: 해당 가상 페이지의 PFN을 새 물리 프레임 주소로 교체하고, R/W 비트를 1(쓰기 허용)로 갱신
│
9. 복귀 및 재실행: Page Fault 핸들러 종료 후, CPU가 중단되었던 쓰기 명령어를 정상 재실행
'0으로 초기화된 메모리가 디스크 또는 RAM 공간을 차지하지 않는다'는 개념은 컴파일/링크 타임의 정적 바이너리 구조와 런타임 운영체제 가상 메모리 관리의 차이에서 비롯됩니다.
+--------------------------------------------------+
| ELF 실행 파일 |
| +--------------------------------------------+ |
| | .text 섹션: 컴파일된 기계어 코드 | |
| +--------------------------------------------+ |
| | .data 섹션: 초기화된 전역/정적 변수 | | ◀── 실제 디스크 용량 차지 (초기화 데이터 기록)
| +--------------------------------------------+ |
| | .bss 섹션: 0 또는 미초기화 전역 변수 | | ◀── 메타데이터(크기 정보)만 기록 (용량 차지 0)
| +--------------------------------------------+ |
+--------------------------------------------------+
int arr[1000] = {1, 2, ...};처럼 0이 아닌 유의미한 값으로 초기화된 전역 변수입니다. 초기화된 실제 바이트 데이터가 실행 파일(바이너리) 내부에 물리적으로 기록되므로 파일의 디스크 용량을 직접 차지합니다.int arr[1000] = {0};처럼 0으로 초기화되었거나 초기화되지 않은 전역 변수입니다. 컴파일러는 4000바이트의 0을 파일에 기록하지 않고, "프로그램 적재 시 4000바이트 크기의 0 공간이 필요함"이라는 메타데이터(크기 정보)만 기록합니다. 따라서 실행 파일의 디스크 크기가 늘어나지 않습니다.calloc의 Zero Page CoW 최적화는 모든 크기의 할당에 일괄 적용되지 않으며, 할당기가 메모리를 가져오는 공급 경로에 따라 물리적 동작이 분기됩니다.
| 비교 항목 | 대형 할당 (Large Allocation) | 소형 할당 (Small Allocation) |
|---|---|---|
| 기준 크기 | glibc 기준 대략 128KB 이상 (MMAP_THRESHOLD) | 수 바이트 ~ 수십 KB 단위 |
| 기저 메커니즘 | 커널 mmap 시스템 콜 직접 호출 | 사용자 공간 힙 내부 가용 리스트(Free List) 탐색 및 분할 |
| 메모리 상태 | OS가 새로 매핑한 Clean(0 보장) 페이지 | 이전에 프로세스가 쓰다 해제한 Dirty(쓰레기 값 잔존) 블록 |
| calloc 내부 동작 | 커널의 0 보장을 신뢰하여 유저 레벨 0 쓰기 전면 생략 | 기존 잔존 데이터를 지우기 위해 내부에서 직접 memset 루프 실행 |
| 속도 차이 | Zero Page 공유로 calloc이 수십~수백 배 압도적으로 빠름 | 내부에서 memset을 돌리므로 calloc과 malloc + memset의 속도가 기계적으로 동일 |
말록랩에서 구현하는 동적 메모리 할당기의 힙 블록은 다음과 같은 구조로 동작합니다.
[할당 블록 메모리 구조] [가용 블록(Free Block) 메모리 구조]
┌───────────────────────────┐ ┌───────────────────────────┐
│ Header (크기 + 할당 비트 1) │ │ Header (크기 + 할당 비트 0) │
├───────────────────────────┤ ├───────────────────────────┤
│ │ │ NEXT 가용 포인터 (8바이트) │ ──▶ 다음 가용 블록 주소
│ User Payload 데이터 영역 │ ├───────────────────────────┤
│ │ │ PREV 가용 포인터 (8바이트) │ ◀── 이전 가용 블록 주소
│ │ ├───────────────────────────┤
│ │ │ 미사용 여백 공간 │
├───────────────────────────┤ ├───────────────────────────┤
│ Footer (크기 + 할당 비트 1) │ │ Footer (크기 + 할당 비트 0) │
└───────────────────────────┘ └───────────────────────────┘
물리 메모리의 전압이나 OS 가상 메모리 조작과 무관하게, 배열이나 테이블 자료구조의 논리적 초기화 비용을 항상 상수 시간 O(1)로 유지하는 소프트웨어 알고리즘 기법이 존재합니다.
데이터를 저장하는 각 슬롯에 실제 값과 함께 해당 데이터가 기록된 세대 번호(generation)를 구조체로 묶어 관리합니다. 시스템 전역에는 단 하나의 정수 카운터인 global_epoch를 유지합니다.
#include <stdio.h>
#include <stdint.h>
#define MAX_SIZE 1000000
typedef struct {
int value;
uint32_t generation;
} Slot;
Slot table[MAX_SIZE];
uint32_t global_epoch = 1;
// O(1) 논리적 초기화 연산
void clear_table(void) {
global_epoch++; // 수백만 개 요소를 0으로 미는 루프 없이 카운터 1 증가로 전체 초기화
}
// 읽기 연산: 세대 번호 일치 여부 판별
int read_slot(int index) {
if (table[index].generation != global_epoch) {
return 0; // 현재 유효 세대의 값이 아니므로 초기화된 상태(0)로 취급
}
return table[index].value;
}
// 쓰기 연산: 현재 세대 번호를 각인
void write_slot(int index, int val) {
table[index].value = val;
table[index].generation = global_epoch; // 최신 세대 기록
}
컴퓨터 시스템의 메모리 관리 아키텍처는 유저 레벨과 커널 레벨로 명확히 분리되며, 말록랩과 핀토스는 이 거대한 구조의 상부와 하부를 각각 직접 구현하는 과정입니다.
[사용자 공간 (User Space)]
│ 애플리케이션 프로그램 코드 (User Application)
│ C 표준 라이브러리 (malloc, calloc, realloc, free)
│
└─▶ [6주차 말록랩 (Malloc Lab)]: 사용자 레벨 힙 할당기 직접 구현
- mem_sbrk로 연속된 가상 힙 메모리 공간 확보
- 헤더/푸터 경계 태그(Boundary Tag) 비트 연산
- Explicit Free List 관리 (페이로드 포인터 오버레이)
- 가용 블록 탐색(First-fit / Next-fit / Best-fit) 및 분할(place)
- 상수 시간 인접 가용 블록 병합(Coalescing) 및 단편화 제어
──────────────── 시스템 콜 경계 (Syscall Interface: brk, mmap) ────────────────
[커널 공간 (Kernel Space)]
│
└─▶ [7주차 핀토스 (Pintos Project 3: Virtual Memory)]: OS 커널 메모리 서브시스템 직접 구현
- 하드웨어 다단계 페이지 테이블(Page Table) 및 64비트 PTE 비트 직접 조작
- CPU 하드웨어 인터럽트 #PF를 수신하는 Page Fault 핸들러 구현
- 익명 페이지(Anonymous Page) 지연 할당(Lazy Allocation)
- Frame Table을 통한 물리 RAM 프레임 점유 관리
- 물리 RAM 부족 시 페이지 교체(Clock 알고리즘, Eviction) 수행
- 스왑 디스크(Swap Table) 읽기/쓰기 및 파일 매핑(Memory-Mapped Files) 구현
void *mm_calloc(size_t nmemb, size_t size) {
size_t total_size = nmemb * size;
void *ptr = mm_malloc(total_size);
if (ptr != NULL) {
memset(ptr, 0, total_size); // 힙 가용 블록 재사용이므로 사용자 레벨 memset 필수
}
return ptr;
}