컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
os · 06

메모리 관리

OS메모리단편화

메모리 관리는 프로세스에 메모리를 효율적으로 할당하고 해제하는 OS의 핵심 기능입니다. 연속 할당은 구현이 단순하지만 단편화 문제가 생기고, 페이징은 이를 해결해 현대 OS의 기반이 됩니다.

핵심 개념

연속 할당 (Contiguous Allocation)

프로세스 전체를 연속된 하나의 메모리 영역에 배치합니다. 구현이 단순하고 주소 변환이 빠르지만, 메모리 해제 후 단편화가 누적되는 문제가 있습니다.

핵심 개념

외부 단편화 (External Fragmentation)

총 여유 메모리는 충분하지만 연속된 공간이 없어 프로세스를 적재할 수 없는 현상입니다. 해결책은 압축(Compaction) 또는 페이징입니다.

핵심 개념

페이징 (Paging)

물리 메모리를 고정 크기 프레임으로, 논리 메모리를 같은 크기 페이지로 나눕니다. 페이지를 빈 프레임 어디에나 배치할 수 있어 외부 단편화가 발생하지 않습니다.

핵심 개념

페이지 테이블

각 프로세스의 논리 페이지 번호와 물리 프레임 번호의 매핑 정보를 저장합니다. MMU(Memory Management Unit)가 주소 변환 시 참조합니다. TLB로 변환 속도를 높입니다.

핵심 개념

내부 단편화 (Internal Fragmentation)

페이지 크기로 나누어떨어지지 않는 프로세스가 마지막 페이지의 남는 공간을 낭비합니다. 페이지 크기를 작게 하면 줄어들지만 페이지 테이블 크기가 커집니다.

핵심 개념

세그멘테이션 (Segmentation)

프로그램을 코드·스택·힙 등 논리적 단위(세그먼트)로 나눠 각각을 독립적으로 관리합니다. 페이징과 달리 각 세그먼트 크기가 다릅니다. 현대 운영체제는 대부분 페이징을 중심으로 메모리를 관리합니다.

First Fit / Best Fit / Worst Fit — 연속 할당에서 빈 공간 선택 전략. First Fit은 첫 번째 맞는 공간, Best Fit은 가장 작게 남는 공간, Worst Fit은 가장 크게 남는 공간을 선택합니다. First Fit은 탐색이 짧아 빠릅니다. 전략마다 장단이 있고 요청 순서에 따라 결과가 달라집니다(아래 숫자로 확인하기 참고).
TLB (Translation Lookaside Buffer) — 페이지 테이블 접근 비용을 줄이는 하드웨어 캐시. 최근 변환 결과를 저장해 TLB Hit 시 메모리 접근 없이 주소를 변환합니다.
다단계 페이지 테이블 — 페이지 테이블 자체가 너무 커지는 문제를 해결합니다. x86-64 리눅스는 보통 4단계 페이지 테이블을 사용하며(5단계도 지원) 실제 사용하는 항목만 메모리에 올립니다(Sparse).
압축 (Compaction) — 외부 단편화 해결을 위해 프로세스를 한쪽으로 몰아 큰 연속 공간을 만드는 방법입니다. 모든 실행 중인 프로세스의 메모리를 복사해야 하므로 비용이 매우 크고 실시간 시스템에서는 사용하기 어렵습니다.

메모리 관리가 풀어야 하는 문제

여러 프로세스가 한정된 물리 메모리를 나눠 쓰면서 서로의 영역을 침범하지 않게 하고, 프로그램이 메모리의 어디에 놓이든 같은 주소로 동작하게 해야 합니다. 이를 위해 하드웨어의 MMU가 논리 주소를 물리 주소로 바꿔 주고 운영체제가 그 매핑을 관리합니다. 현대 시스템은 대부분 페이징을 기본으로 씁니다.

숫자로 확인하기

주소 변환 계산 — 페이지 크기가 4KB(4,096바이트)일 때 논리 주소 20,000은 20,000 ÷ 4,096 = 4 나머지 3,616이라서 페이지 번호 4, 오프셋 3,616입니다. 페이지 테이블에서 페이지 4가 프레임 7이라면 물리 주소는 7 × 4,096 + 3,616 = 32,288입니다.
내부 단편화 — 프로세스가 10,000바이트이고 페이지가 4,096바이트이면 3페이지(12,288바이트)가 필요해 마지막 페이지에서 2,288바이트가 낭비됩니다. 프로세스 크기가 페이지 크기의 배수가 아니면 마지막 페이지에서 항상 일부가 낭비됩니다.
페이지 테이블의 크기 — 32비트 주소와 4KB 페이지에서는 페이지 번호가 20비트라 항목이 2²⁰ = 1,048,576개이고, 항목이 4바이트이면 프로세스마다 4MB가 됩니다. 이렇게 큰 테이블을 줄이려고 다단계 페이지 테이블을 씁니다.
x86-64의 4단계 구조 — 리눅스 커널 문서는 x86-64가 4단계 페이지 테이블로 48비트 가상 주소를 쓴다고 설명합니다. 4KB 페이지의 오프셋 12비트를 빼면 36비트이고 4단계로 나누면 한 단계가 9비트(항목 512개)입니다.
TLB가 접근 시간에 미치는 영향(가정) — TLB 접근 10ns, 메모리 접근 100ns로 가정합니다. 히트율 80%이면 평균 0.8 × 110 + 0.2 × 210 = 130ns이고, 히트율 99%이면 0.99 × 110 + 0.01 × 210 = 111ns입니다.
First, Best, Worst Fit 비교 — 빈 공간이 100K, 500K, 200K, 300K, 600K이고 212K, 417K, 112K, 426K를 차례로 요청하면 First Fit과 Worst Fit은 426K를 배치하지 못하고, Best Fit은 네 요청을 모두 배치합니다.

자주 하는 오해

페이징은 단편화가 전혀 없다 — 외부 단편화는 없애지만 마지막 페이지가 남아 내부 단편화는 생깁니다.
논리 주소와 물리 주소는 같다 — 프로세스가 보는 것은 논리 주소이고 MMU가 페이지 테이블로 물리 주소로 바꿉니다.
TLB 미스는 페이지 폴트다 — TLB 미스는 변환 결과가 TLB에 없어서 페이지 테이블을 찾아보는 것이고, 페이지 폴트는 해당 페이지가 메모리에 없을 때 운영체제가 개입하는 것입니다.
free()를 호출하면 메모리가 바로 운영체제에 반환된다 — 할당자는 해제된 메모리를 재사용하려고 들고 있다가 조건에 따라 반환합니다. glibc의 동작은 mallopt(3) 매뉴얼의 trim 관련 설명에서 볼 수 있습니다.

면접에서 이렇게 물어요

면접 질문

내부 단편화와 외부 단편화의 차이는 무엇인가요?

내부 단편화는 할당 단위 안에서 남는 공간이고, 외부 단편화는 전체 여유는 충분한데 연속된 공간이 없어 할당하지 못하는 것입니다. 페이징은 후자를 없애고 전자를 남깁니다.

면접 질문

페이징과 세그멘테이션의 차이는 무엇인가요?

페이징은 고정 크기로 나누고 사용자에게는 보이지 않으며, 세그멘테이션은 코드, 스택, 힙처럼 논리적 단위의 가변 크기로 나눕니다. 현대 시스템은 대부분 페이징을 중심으로 씁니다.

면접 질문

페이지 테이블이 너무 커지는 문제는 어떻게 해결하나요?

페이지 테이블을 여러 단계로 나누어 실제로 쓰는 영역의 하위 테이블만 만듭니다. 주소 공간이 넓은 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) 매뉴얼).
Linux의 주소 공간 배치 — x86-64 리눅스의 가상 주소 공간 배치는 커널 문서 x86_64 mm가 정리합니다.

함께 보면 좋은 토픽

가상 메모리 — 메모리보다 큰 주소 공간을 쓰는 방법을 봅니다.
프로세스 — 프로세스의 메모리 구조를 봅니다.
스레드 — 주소 공간을 공유하는 실행 단위를 봅니다.
시스템 콜 — 메모리 할당이 커널로 들어가는 방식을 봅니다.

참고 자료

교재 — Silberschatz, Galvin, Gagne, "Operating System Concepts", 10th Edition, "Main Memory" 장 / Intel, "Intel 64 and IA-32 Architectures Software Developer's Manual", Volume 3, "Paging" 장

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)가 발생하는 경우는?

정해진 크기 단위로 할당하다 보니 딱 맞지 않아 생기는 낭비입니다.
내부 단편화는 고정 크기 페이지/블록 안에서 실제 사용하는 양보다 할당된 양이 많아 그 차이만큼 낭비되는 것을 말합니다.