메모리 관리
메모리 관리는 프로세스에 메모리를 효율적으로 할당하고 해제하는 OS의 핵심 기능입니다. 연속 할당은 구현이 단순하지만 단편화 문제가 생기고, 페이징은 이를 해결해 현대 OS의 기반이 됩니다.
핵심 개념
연속 할당 (Contiguous Allocation)
프로세스 전체를 연속된 하나의 메모리 영역에 배치합니다. 구현이 단순하고 주소 변환이 빠르지만, 메모리 해제 후 단편화가 누적되는 문제가 있습니다.
핵심 개념
외부 단편화 (External Fragmentation)
총 여유 메모리는 충분하지만 연속된 공간이 없어 프로세스를 적재할 수 없는 현상입니다. 해결책은 압축(Compaction) 또는 페이징입니다.
핵심 개념
페이징 (Paging)
물리 메모리를 고정 크기 프레임으로, 논리 메모리를 같은 크기 페이지로 나눕니다. 페이지를 빈 프레임 어디에나 배치할 수 있어 외부 단편화가 발생하지 않습니다.
핵심 개념
페이지 테이블
각 프로세스의 논리 페이지 번호와 물리 프레임 번호의 매핑 정보를 저장합니다. MMU(Memory Management Unit)가 주소 변환 시 참조합니다. TLB로 변환 속도를 높입니다.
핵심 개념
내부 단편화 (Internal Fragmentation)
페이지 크기로 나누어떨어지지 않는 프로세스가 마지막 페이지의 남는 공간을 낭비합니다. 페이지 크기를 작게 하면 줄어들지만 페이지 테이블 크기가 커집니다.
핵심 개념
세그멘테이션 (Segmentation)
프로그램을 코드·스택·힙 등 논리적 단위(세그먼트)로 나눠 각각을 독립적으로 관리합니다. 페이징과 달리 각 세그먼트 크기가 다릅니다. 현대 운영체제는 대부분 페이징을 중심으로 메모리를 관리합니다.
메모리 관리가 풀어야 하는 문제
여러 프로세스가 한정된 물리 메모리를 나눠 쓰면서 서로의 영역을 침범하지 않게 하고, 프로그램이 메모리의 어디에 놓이든 같은 주소로 동작하게 해야 합니다. 이를 위해 하드웨어의 MMU가 논리 주소를 물리 주소로 바꿔 주고 운영체제가 그 매핑을 관리합니다. 현대 시스템은 대부분 페이징을 기본으로 씁니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
내부 단편화와 외부 단편화의 차이는 무엇인가요?
내부 단편화는 할당 단위 안에서 남는 공간이고, 외부 단편화는 전체 여유는 충분한데 연속된 공간이 없어 할당하지 못하는 것입니다. 페이징은 후자를 없애고 전자를 남깁니다.
면접 질문
페이징과 세그멘테이션의 차이는 무엇인가요?
페이징은 고정 크기로 나누고 사용자에게는 보이지 않으며, 세그멘테이션은 코드, 스택, 힙처럼 논리적 단위의 가변 크기로 나눕니다. 현대 시스템은 대부분 페이징을 중심으로 씁니다.
면접 질문
페이지 테이블이 너무 커지는 문제는 어떻게 해결하나요?
페이지 테이블을 여러 단계로 나누어 실제로 쓰는 영역의 하위 테이블만 만듭니다. 주소 공간이 넓은 64비트 시스템에서 필수입니다.
면접 질문
TLB는 무엇이고 왜 필요한가요?
최근의 주소 변환 결과를 저장하는 하드웨어 캐시입니다. 변환마다 메모리의 페이지 테이블을 읽으면 접근이 몇 배로 느려지므로, TLB 히트일 때 그 접근을 줄입니다.
실제 시스템에서는
sysconf(_SC_PAGESIZE)로 시스템의 페이지 크기를 확인할 수 있고, x86에서는 보통 4KB입니다. 커널 hugetlbpage 문서는 2MB 같은 큰 페이지도 지원한다고 설명합니다.
/proc/PID/status의 VmRSS는 실제 메모리에 올라온 크기를(매뉴얼은 이 값이 부정확할 수 있다고 설명합니다), /proc/PID/smaps는 영역별 사용량을 보여 줍니다(proc_pid_status(5), proc_pid_smaps(5) 매뉴얼).
함께 보면 좋은 토픽
참고 자료
C — mmap / malloc 으로 메모리 할당 원리 확인
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> /* ── malloc: Heap 영역 동적 할당 (내부적으로 sbrk/mmap 사용) ── */ void demo_malloc() { size_t size = 16 * 1024; // 16KB (작은 할당 — 모두 128KB 미만이라 힙에 연속 배치) void *p1 = malloc(size); // 연속 16KB 요청 void *p2 = malloc(size * 2); // 연속 32KB 요청 void *p3 = malloc(size / 2); // 연속 8KB 요청 printf("p1: %p\np2: %p\np3: %p\n", p1, p2, p3); /* 주소를 보면 p1→p2→p3 순으로 연속 배치됨 */ free(p2); // p2 해제 → p1과 p3 사이에 '구멍' 발생 (외부 단편화) void *p4 = malloc(640 * 1024); // 640KB(128KB 초과): glibc는 내부적으로 mmap 사용 // p4의 주소가 p1·p3와 전혀 다른 영역 — 페이지 단위로 분리 배치 printf("p4: %p\n", p4); free(p1); free(p3); free(p4); } /* ── mmap: OS에 직접 페이지 단위 메모리 요청 ────────────────── */ void demo_mmap() { size_t page = 4096; // 일반적인 페이지 크기 4KB // MAP_ANONYMOUS: 파일 없이 순수 메모리 할당 (페이징 직접 사용) void *mem = mmap(NULL, page * 5, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (mem == MAP_FAILED) { perror("mmap"); return; } memset(mem, 0, page * 5); printf("mmap 주소: %p (페이지 정렬됨)\n", mem); // 주소가 항상 페이지 크기(4096)의 배수 → 페이징 단위로 관리 munmap(mem, page * 5); // 해제: 프레임 반환 } int main() { demo_malloc(); demo_mmap(); return 0; }
malloc은 내부적으로 glibc가 Heap 영역을 관리합니다. 작은 할당은 sbrk()로 Heap을 늘리고, 큰 할당(기본 128KB 이상)은 mmap()으로 독립된 페이지를 직접 요청합니다.
free(p2) 후 p1과 p3 사이에 구멍이 생기지만, OS는 이를 모릅니다. glibc의 메모리 할당자가 free list로 관리하며 다음 malloc 시 재사용합니다. 이것이 사용자 공간의 단편화입니다.
mmap으로 얻은 주소는 항상 페이지 크기(일반적으로 4096B)의 배수입니다.
OS가 페이지 단위로만 메모리를 관리하기 때문입니다.
Java — 페이지 테이블 개념을 코드로 시뮬레이션
import java.util.*; /** * 페이지 테이블 시뮬레이션 * 실제 OS의 페이지 테이블 동작을 단순화해 구현합니다. */ public class PagingSimulator { static final int PAGE_SIZE = 64; // 64KB (단위 생략) static final int N_FRAMES = 8; // 물리 메모리: 8 프레임 // 물리 프레임 테이블: -1 = 빈 프레임, 그 외 = 점유한 PID static int[] frames = new int[N_FRAMES]; // 프로세스별 페이지 테이블: pid → (논리 페이지 → 물리 프레임) static Map<Integer, Map<Integer, Integer>> pageTables = new HashMap<>(); static { Arrays.fill(frames, -1); // 초기화: 모든 프레임 비어 있음 } /** 프로세스에 필요한 페이지 수만큼 프레임을 비연속 할당 */ static boolean allocate(int pid, int sizeKB) { int pages = (int) Math.ceil((double) sizeKB / PAGE_SIZE); Map<Integer, Integer> table = new LinkedHashMap<>(); int page = 0; for (int f = 0; f < N_FRAMES && page < pages; f++) { if (frames[f] == -1) { // 빈 프레임 발견 frames[f] = pid; // 프레임 점유 table.put(page++, f); // 논리 페이지 → 물리 프레임 매핑 } } if (page < pages) { // 프레임 부족 table.keySet().forEach(p -> frames[table.get(p)] = -1); return false; } pageTables.put(pid, table); System.out.printf("P%d 할당 (%dKB, %d 페이지): %s%n", pid, sizeKB, pages, table); return true; } /** 논리 주소 → 물리 주소 변환 (MMU 역할) */ static int translate(int pid, int logicalAddr) { int pageNum = logicalAddr / PAGE_SIZE; // 논리 페이지 번호 int offset = logicalAddr % PAGE_SIZE; // 페이지 내 오프셋 int frameNum = pageTables.get(pid).get(pageNum); // 페이지 테이블 조회 return frameNum * PAGE_SIZE + offset; // 물리 주소 } public static void main(String[] args) { allocate(1, 128); // P1: 128KB = 2 페이지 → F0, F1 allocate(2, 256); // P2: 256KB = 4 페이지 → F2~F5 allocate(3, 64); // P3: 64KB = 1 페이지 → F6 // P2 종료 → 프레임 해제 pageTables.get(2).values().forEach(f -> frames[f] = -1); pageTables.remove(2); System.out.println("P2 해제 후 프레임: " + Arrays.toString(frames)); // P4: 320KB = 5 페이지 → 비연속으로 F2,F3,F4,F5,F7 할당 성공! allocate(4, 320); // 논리 주소 변환 예시 System.out.printf("P4 논리주소 200 → 물리주소 %d%n", translate(4, 200)); } }
페이지 테이블은 논리 주소(프로세스 관점)를 물리 주소(실제 RAM)로 변환하는 자료구조입니다. 각 프로세스는 자신만의 독립적인 페이지 테이블을 가집니다.
논리 주소 변환: 페이지 번호 = 논리 주소 / 페이지 크기, 오프셋 = 논리 주소 % 페이지 크기, 물리 주소 = 프레임 번호 × 페이지 크기 + 오프셋.
실제 OS에서는 TLB(Translation Lookaside Buffer)가 최근 변환 결과를 캐시합니다.
TLB Hit 시 메모리 접근 없이 즉시 변환(~1 사이클), Miss 시 페이지 테이블 조회(~수십~수백 사이클)가 발생합니다.
Q1
외부 단편화(External Fragmentation)란?
Q2
페이징(Paging)이 외부 단편화 문제를 해결하는 방식은?
Q3
내부 단편화(Internal Fragmentation)가 발생하는 경우는?