콘텐츠로 이동

제 9장: 주 메모리 (Main Memory) 💾

문서 범위와 검증 기준

  • 범위: 연속 할당, 단순 paging과 swapping을 설명하는 교과서형 모델입니다. 실제 x86-64/ARM MMU, Linux page reclaim, NUMA, huge page와 보안 완화의 전체 구현을 대신하지 않습니다.
  • 전제: 주소·TLB 계산은 문제에 명시된 주소 폭, page/PTE 크기, page-walk 횟수와 cache 미적중 모델을 사용합니다. 의사 코드는 kernel code가 아니며 race, overflow와 오류 처리가 생략될 수 있습니다.
  • 근거 상태와 환경: page table 형식, canonical virtual-address 폭, TLB와 swap 정책은 CPU·OS·커널 버전별 공식 문서로 확인합니다. ns, MB/s, hit ratio와 임계값은 예제 입력이지 현대 시스템의 기본값이 아닙니다.
  • 실패/재시도: page fault, allocation 실패, swap I/O 오류와 OOM은 구분해 기록합니다. 재시도는 회수 가능한 page가 생길 조건과 상한을 두고, 무한 reclaim/thrashing은 실패 상태로 전환합니다.
  • 완료 증거: 계산에는 bit 분할과 단위를, 실험에는 CPU/커널, page size, NUMA·huge-page 설정, workload와 page-fault/TLB/swap trace를 남깁니다. 보호는 허용 접근과 거부 접근을 모두 관측해야 완료입니다.

📖 목차 (Table of Contents)

  1. 개요
  2. 배경
  3. 주소 바인딩
  4. 논리 주소와 물리 주소
  5. 동적 로딩과 연결
  6. 연속 메모리 할당
  7. 페이징
  8. 페이지 테이블 구조
  9. 스와핑
  10. 핵심 개념 정리
  11. 연습 문제

개요

**주 메모리 관리**는 운영체제의 핵심 기능 중 하나로, 한정된 메모리 자원을 효율적으로 관리하고 프로세스들 간의 메모리 보호를 제공합니다.

graph TD
    A[메모리 관리] --> B[메모리 할당<br/>Memory Allocation]
    A --> C[메모리 보호<br/>Memory Protection]
    A --> D[주소 변환<br/>Address Translation]
    A --> E[메모리 효율성<br/>Memory Efficiency]

    B --> B1[연속 할당]
    B --> B2[비연속 할당]

    C --> C1[베이스-리밋 레지스터]
    C --> C2[세그멘테이션]
    C --> C3[페이징]

    D --> D1[컴파일 시간]
    D --> D2[로드 시간]
    D --> D3[실행 시간]

    E --> E1[내부 단편화]
    E --> E2[외부 단편화]
    E --> E3[압축]

    style A fill:#e1f5fe
    style B fill:#fff3e0
    style C fill:#f3e5f5
    style D fill:#e8f5e8
    style E fill:#ffebee

🎯 학습 목표

이 장을 통해 다음을 이해할 수 있습니다: - 메모리 관리의 기본 개념과 필요성 - 주소 바인딩과 주소 변환 메커니즘 - 연속 할당과 페이징 기법 - 메모리 보호와 공유 메커니즘


배경

🏗️ 메모리 계층 구조

graph TD
    A[CPU 레지스터] --> B[캐시]
    B --> C[주 메모리<br/>Main Memory]
    C --> D[보조 저장소<br/>Secondary Storage]

    A --> A1["⚡ 속도: 매우 빠름<br/>💾 용량: 매우 작음<br/>💰 비용: 매우 높음"]
    B --> B1["⚡ 속도: 빠름<br/>💾 용량: 작음<br/>💰 비용: 높음"]
    C --> C1["⚡ 속도: 보통<br/>💾 용량: 중간<br/>💰 비용: 보통"]
    D --> D1["⚡ 속도: 느림<br/>💾 용량: 큼<br/>💰 비용: 낮음"]

    style A fill:#ffebee
    style B fill:#fff3e0
    style C fill:#e8f5e8
    style D fill:#e1f5fe

🔐 메모리 보호 (Memory Protection)

메모리 보호는 MMU 권한과 OS page-table 관리로 프로세스 접근을 제한합니다. 공유 mapping, kernel 권한, DMA/IOMMU, 하드웨어·커널 결함까지 포함한 완전한 격리를 이 한 메커니즘만으로 보장하지는 않습니다.

graph LR
    A[CPU] --> B[MMU<br/>Memory Management Unit]
    B --> C[주 메모리]

    subgraph "보호 메커니즘"
        D[베이스 레지스터<br/>Base Register]
        E[리밋 레지스터<br/>Limit Register]
    end

    B --> D
    B --> E

    style B fill:#e1f5fe
    style D fill:#e8f5e8
    style E fill:#e8f5e8

하드웨어 주소 보호

// 의사 코드: 하드웨어 주소 검사
void check_memory_access(int logical_address) {
    if (logical_address >= 0 && logical_address < limit_register) {
        int physical_address = base_register + logical_address;
        // 메모리 접근 허용
        access_memory(physical_address);
    } else {
        // 보호 위반 - 트랩 발생
        generate_protection_fault();
    }
}

예시: - 베이스 레지스터: 300040 - 리밋 레지스터: 120900 - 유효한 논리 주소 범위: 0 ~ 120899 - 물리 주소 범위: 300040 ~ 420939

graph TD
    A[논리 주소 346] --> B{346 < 120900?}
    B -->|Yes| C[물리 주소<br/>300040 + 346 = 300386]
    B -->|No| D[보호 위반<br/>트랩 발생]

    style C fill:#e8f5e8
    style D fill:#ffebee

주소 바인딩

🔄 주소 바인딩 단계

graph TD
    A[소스 코드] --> B[컴파일러/어셈블러]
    B --> C[목적 파일]
    C --> D[링커]
    D --> E[실행 파일]
    E --> F[로더]
    F --> G[메모리]

    A --> A1[기호적 주소<br/>변수명, 함수명]
    C --> C1[재배치 가능 주소<br/>상대 주소]
    E --> E1[절대 주소<br/>실제 메모리 주소]

    style A1 fill:#e1f5fe
    style C1 fill:#fff3e0
    style E1 fill:#e8f5e8

⏰ 바인딩 시점

1. 컴파일 시간 바인딩 (Compile-time Binding)

graph LR
    A[소스 코드] --> B[컴파일러]
    B --> C[절대 코드]

    A --> A1["int x;<br/>// 주소 미정"]
    C --> C1["LOAD 1000<br/>// 절대 주소"]

    style C fill:#e8f5e8

특징: - 메모리 위치가 컴파일 시점에 확정 - 시작 위치 변경 시 재컴파일 필요 - 임베디드 시스템에서 주로 사용

2. 로드 시간 바인딩 (Load-time Binding)

// 재배치 가능 코드 예시
LOAD R1, [BASE + 100]  // BASE는 로드 시점에 결정
STORE [BASE + 200], R1

특징: - 프로그램이 메모리에 로드될 때 주소 결정 - 실행 중 메모리 위치 이동 불가 - 대부분의 실행 파일 형태

3. 실행 시간 바인딩 (Execution-time Binding)

sequenceDiagram
    participant P as 프로세스
    participant MMU as MMU
    participant M as 메모리

    P->>MMU: 논리 주소 요청
    MMU->>MMU: 주소 변환 (동적)
    MMU->>M: 물리 주소로 접근
    M->>MMU: 데이터 반환
    MMU->>P: 데이터 전달

특징: - 실행 중 동적 주소 변환 - 프로세스 재배치 가능 - MMU 하드웨어 지원 필요


논리 주소와 물리 주소

🔍 주소 공간의 개념

graph TD
    A[주소 공간] --> B[논리 주소 공간<br/>Logical Address Space]
    A --> C[물리 주소 공간<br/>Physical Address Space]

    B --> B1["프로그램이 생성하는<br/>모든 논리 주소의 집합"]
    B --> B2["가상 주소라고도 함"]

    C --> C1["실제 메모리 하드웨어의<br/>주소 집합"]
    C --> C2["MMU에 의해 접근"]

    style B fill:#e1f5fe
    style C fill:#e8f5e8

🔧 메모리 관리 장치 (MMU)

graph LR
    A[CPU] -->|논리 주소| B[MMU]
    B -->|물리 주소| C[메모리]

    subgraph "MMU 내부"
        D[재배치 레지스터<br/>Relocation Register]
        E[주소 변환 로직]
    end

    B --> D
    B --> E

    style B fill:#e1f5fe
    style D fill:#fff3e0

단순 MMU 스키마

// MMU의 주소 변환 과정
int translate_address(int logical_address) {
    // 경계 검사
    if (logical_address >= limit_register) {
        generate_segmentation_fault();
        return -1;
    }

    // 물리 주소 계산
    int physical_address = relocation_register + logical_address;
    return physical_address;
}

예시: - 재배치 레지스터: 14000 - 논리 주소: 346 - 물리 주소: 14000 + 346 = 14346

graph LR
    A[논리 주소: 346] --> B[MMU]
    B --> C[물리 주소: 14346]

    B --> B1[재배치 레지스터: 14000]

동적 로딩과 연결

📥 동적 로딩 (Dynamic Loading)

graph TD
    A[프로그램 시작] --> B[주 모듈 로드]
    B --> C[실행 중]
    C --> D{루틴 호출?}
    D -->|Yes| E{루틴이 메모리에<br/>있는가?}
    D -->|No| C
    E -->|No| F[디스크에서 루틴 로드]
    E -->|Yes| G[루틴 실행]
    F --> G
    G --> C

    style F fill:#fff3e0
    style G fill:#e8f5e8

장점: - 메모리 공간 효율적 활용 - 사용되지 않는 루틴은 로드되지 않음 - 큰 프로그램에 적합

구현 예시:

// 동적 로딩 의사 코드
void call_routine(char* routine_name) {
    if (!is_loaded(routine_name)) {
        void* routine = load_from_disk(routine_name);
        register_routine(routine_name, routine);
    }
    execute_routine(routine_name);
}

🔗 동적 연결 (Dynamic Linking)

graph TD
    A[정적 연결] --> A1["컴파일 시점에 라이브러리<br/>코드가 실행 파일에 포함"]
    A --> A2["실행 파일 크기 증가"]
    A --> A3["라이브러리 업데이트 시<br/>재컴파일 필요"]

    B[동적 연결] --> B1["실행 시점에<br/>라이브러리와 연결"]
    B --> B2["실행 파일 크기 감소"]
    B --> B3["공유 라이브러리 사용"]

    style A fill:#ffebee
    style B fill:#e8f5e8

스텁 (Stub) 메커니즘

// 스텁 의사 코드
void printf_stub() {
    static bool loaded = false;
    static void (*real_printf)() = NULL;

    if (!loaded) {
        // 실제 printf 함수 위치 찾기
        real_printf = locate_library_routine("printf");
        loaded = true;

        // 스텁을 실제 함수 주소로 교체
        replace_stub_with_address(real_printf);
    }

    // 실제 함수 호출
    real_printf();
}

공유 라이브러리 장점: - 메모리 절약 (여러 프로세스가 공유) - 라이브러리 업데이트 용이 - 시스템 패치 적용 효율성


연속 메모리 할당

🏠 메모리 분할

graph TD
    A[주 메모리] --> B[운영체제 영역]
    A --> C[사용자 프로세스 영역]

    B --> B1["낮은 주소<br/>(Low Memory)"]
    B --> B2["인터럽트 벡터"]
    B --> B3["커널 코드/데이터"]

    C --> C1["높은 주소<br/>(High Memory)"]
    C --> C2["사용자 프로세스들"]

    style B fill:#ffebee
    style C fill:#e8f5e8

🔄 가변 분할 (Variable Partition)

graph LR
    subgraph "메모리 상태"
        A[OS<br/>0-100K]
        B[프로세스 A<br/>100K-200K]
        C[홀<br/>200K-300K]
        D[프로세스 B<br/>300K-450K]
        E[홀<br/>450K-500K]
        F[프로세스 C<br/>500K-600K]
    end

    style A fill:#ffebee
    style B fill:#e8f5e8
    style C fill:#fff3e0
    style D fill:#e8f5e8
    style E fill:#fff3e0
    style F fill:#e8f5e8

동적 저장소 할당 알고리즘

1. First Fit (최초 적합)
void* first_fit(size_t size) {
    for (hole_t* hole = hole_list; hole != NULL; hole = hole->next) {
        if (hole->size >= size) {
            return allocate_from_hole(hole, size);
        }
    }
    return NULL; // 할당 실패
}
2. Best Fit (최적 적합)
void* best_fit(size_t size) {
    hole_t* best_hole = NULL;
    size_t min_waste = SIZE_MAX;

    for (hole_t* hole = hole_list; hole != NULL; hole = hole->next) {
        if (hole->size >= size) {
            size_t waste = hole->size - size;
            if (waste < min_waste) {
                min_waste = waste;
                best_hole = hole;
            }
        }
    }

    return best_hole ? allocate_from_hole(best_hole, size) : NULL;
}
3. Worst Fit (최악 적합)
void* worst_fit(size_t size) {
    hole_t* worst_hole = NULL;
    size_t max_size = 0;

    for (hole_t* hole = hole_list; hole != NULL; hole = hole->next) {
        if (hole->size >= size && hole->size > max_size) {
            max_size = hole->size;
            worst_hole = hole;
        }
    }

    return worst_hole ? allocate_from_hole(worst_hole, size) : NULL;
}

📊 알고리즘 비교

알고리즘 속도 메모리 활용도 외부 단편화
First Fit 🟢 빠름 🟡 보통 🟡 보통
Best Fit 🟡 느림 🟢 좋음 🟢 적음
Worst Fit 🟡 느림 🔴 나쁨 🔴 많음

🧩 단편화 (Fragmentation)

외부 단편화 (External Fragmentation)

graph LR
    subgraph "메모리 상태"
        A[프로세스 A<br/>100K]
        B[홀<br/>50K]
        C[프로세스 B<br/>80K]
        D[홀<br/>30K]
        E[프로세스 C<br/>70K]
        F[홀<br/>40K]
    end

    G[새 프로세스<br/>요청: 100K]

    B --> B1[사용 불가<br/>너무 작음]
    D --> D1[사용 불가<br/>너무 작음]
    F --> F1[사용 불가<br/>너무 작음]

    style B1 fill:#ffebee
    style D1 fill:#ffebee
    style F1 fill:#ffebee

50% 규칙: 정상상태의 특정 무작위 할당/해제 모델에서 First Fit은 N개의 할당 블록 사이에 약 0.5N개의 hole이 생긴다는 경험적 결과입니다. “블록의 절반이 항상 손실”이라는 보장이 아니며 크기 분포와 allocator 정책에 따라 달라집니다.

내부 단편화 (Internal Fragmentation)

graph LR
    A[프로세스 요청<br/>18.1KB] --> B[할당된 블록<br/>20KB]

    B --> C[사용됨<br/>18.1KB]
    B --> D[낭비됨<br/>1.9KB]

    style C fill:#e8f5e8
    style D fill:#ffebee

압축 (Compaction)

graph TD
    subgraph "압축 전"
        A1[프로세스 A]
        B1[홀]
        C1[프로세스 B]
        D1[홀]
        E1[프로세스 C]
        F1[홀]
    end

    subgraph "압축 후"
        A2[프로세스 A]
        C2[프로세스 B]
        E2[프로세스 C]
        G2[큰 홀]
    end

    A1 --> A2
    C1 --> C2
    E1 --> E2

    style G2 fill:#e8f5e8

압축의 문제점: - 높은 CPU 오버헤드 - I/O 작업 중인 프로세스 처리 복잡 - 실행 시간 바인딩에서만 가능


페이징

📄 페이징 개념

**페이징**은 외부 단편화 문제를 해결하기 위해 물리 메모리를 고정 크기 블록으로 나누는 기법입니다.

graph TD
    A[페이징 시스템] --> B[물리 메모리]
    A --> C[논리 메모리]
    A --> D[페이지 테이블]

    B --> B1[프레임<br/>Frame]
    B --> B2[고정 크기<br/>예: 4KB]

    C --> C1[페이지<br/>Page]
    C --> C2[프레임과 동일 크기]

    D --> D1[페이지 → 프레임<br/>매핑 정보]

    style A fill:#e1f5fe
    style B fill:#e8f5e8
    style C fill:#fff3e0
    style D fill:#f3e5f5

🔢 주소 변환 스키마

논리 주소 구조

graph LR
    A[논리 주소] --> B[페이지 번호<br/>p]
    A --> C[페이지 오프셋<br/>d]

    B --> B1[상위 m-n 비트]
    C --> C1[하위 n 비트]

    style B fill:#e1f5fe
    style C fill:#fff3e0

주소 크기 관계: - 논리 주소 공간: 2^m - 페이지 크기: 2^n - 페이지 번호: m-n 비트 - 페이지 오프셋: n 비트

주소 변환 과정

// 페이징 주소 변환 알고리즘
struct page_table_entry {
    int frame_number;
    bool valid;
    bool dirty;
    bool referenced;
};

int translate_address(int logical_address, int page_size) {
    // 페이지 번호와 오프셋 추출
    int page_number = logical_address / page_size;
    int page_offset = logical_address % page_size;

    // 페이지 테이블 접근
    if (!page_table[page_number].valid) {
        generate_page_fault();
        return -1;
    }

    // 물리 주소 계산
    int frame_number = page_table[page_number].frame_number;
    int physical_address = frame_number * page_size + page_offset;

    return physical_address;
}

📊 페이징 예시

시스템 설정: - 물리 메모리: 32 바이트 (8 프레임) - 페이지 크기: 4 바이트 - 논리 주소: 4 비트 (16 가능한 주소)

graph TD
    subgraph "논리 메모리"
        P0[페이지 0<br/>주소 0-3]
        P1[페이지 1<br/>주소 4-7]
        P2[페이지 2<br/>주소 8-11]
        P3[페이지 3<br/>주소 12-15]
    end

    subgraph "물리 메모리"
        F0[프레임 0<br/>주소 0-3]
        F1[프레임 1<br/>주소 4-7]
        F2[프레임 2<br/>주소 8-11]
        F3[프레임 3<br/>주소 12-15]
        F4[프레임 4<br/>주소 16-19]
        F5[프레임 5<br/>주소 20-23]
        F6[프레임 6<br/>주소 24-27]
        F7[프레임 7<br/>주소 28-31]
    end

    P0 --> F1
    P1 --> F4
    P2 --> F3
    P3 --> F7

    style P0 fill:#e1f5fe
    style P1 fill:#fff3e0
    style P2 fill:#e8f5e8
    style P3 fill:#f3e5f5

페이지 테이블: | 페이지 | 프레임 | |--------|---------| | 0 | 1 | | 1 | 4 | | 2 | 3 | | 3 | 7 |

주소 변환 예시: - 논리 주소 5 → 페이지 1, 오프셋 1 → 프레임 4 → 물리 주소 17

🧮 내부 단편화 계산

// 내부 단편화 계산
void calculate_internal_fragmentation() {
    int page_size = 2048;      // 2KB
    int process_size = 72766;  // 바이트

    int pages_needed = (process_size + page_size - 1) / page_size;  // 올림
    int allocated_memory = pages_needed * page_size;
    int internal_fragmentation = allocated_memory - process_size;

    printf("필요한 페이지 수: %d\n", pages_needed);           // 36
    printf("할당된 메모리: %d 바이트\n", allocated_memory);    // 73728
    printf("내부 단편화: %d 바이트\n", internal_fragmentation); // 962
}

평균 내부 단편화: 페이지 크기의 ½

🚀 페이지 테이블 구현

TLB (Translation Lookaside Buffer)

graph TD
    A[CPU] --> B[TLB 검색]
    B --> C{TLB 히트?}
    C -->|Yes| D[물리 주소 바로 계산]
    C -->|No| E[페이지 테이블 접근]
    E --> F[TLB 업데이트]
    F --> D
    D --> G[메모리 접근]

    style C fill:#e1f5fe
    style D fill:#e8f5e8
    style E fill:#fff3e0

유효 접근 시간 (EAT) 계산

// 유효 접근 시간 계산
float calculate_effective_access_time(float hit_ratio, 
                                    float tlb_access_time,
                                    float memory_access_time) {
    float hit_time = tlb_access_time + memory_access_time;
    float miss_time = tlb_access_time + 2 * memory_access_time; // 페이지 테이블 + 데이터

    float eat = hit_ratio * hit_time + (1 - hit_ratio) * miss_time;
    return eat;
}

// 산술 확인용 단순 모델: cache/page-walk overlap 없음
// 히트율 80%, TLB 접근을 0ns로 둔 가상 입력, 메모리 접근 10ns
float eat_80 = calculate_effective_access_time(0.8, 0, 10);  // 12ns

// 히트율 99%, TLB 접근 0ns, 메모리 접근 10ns  
float eat_99 = calculate_effective_access_time(0.99, 0, 10); // 10.1ns

🛡️ 메모리 보호

graph TD
    A[페이지 테이블 항목] --> B[프레임 번호]
    A --> C[유효-무효 비트]
    A --> D[보호 비트]

    C --> C1[Valid: 유효한 페이지]
    C --> C2[Invalid: 무효한 페이지]

    D --> D1[Read-only]
    D --> D2[Read-write]
    D --> D3[Execute-only]

    style C1 fill:#e8f5e8
    style C2 fill:#ffebee
    style D fill:#fff3e0

페이지 테이블 항목 구조

typedef struct {
    unsigned int frame_number : 20;  // 프레임 번호 (20비트)
    unsigned int valid : 1;          // 유효 비트
    unsigned int readable : 1;       // 읽기 권한
    unsigned int writable : 1;       // 쓰기 권한
    unsigned int executable : 1;     // 실행 권한
    unsigned int user_access : 1;    // 사용자 접근 권한
    unsigned int dirty : 1;          // 수정 비트
    unsigned int accessed : 1;       // 참조 비트
    unsigned int reserved : 5;       // 예약 비트
} page_table_entry_t;

📚 공유 페이지

graph TD
    subgraph "프로세스 1"
        A1[페이지 테이블 1]
        B1[텍스트 에디터 코드]
        C1[프로세스 1 데이터]
    end

    subgraph "프로세스 2"
        A2[페이지 테이블 2]
        B2[텍스트 에디터 코드]
        C2[프로세스 2 데이터]
    end

    subgraph "물리 메모리"
        D[공유 텍스트 에디터<br/>프레임]
        E[프로세스 1<br/>데이터 프레임]
        F[프로세스 2<br/>데이터 프레임]
    end

    B1 --> D
    B2 --> D
    C1 --> E
    C2 --> F

    style D fill:#e8f5e8
    style E fill:#fff3e0
    style F fill:#f3e5f5

공유 가능한 코드의 조건: - 재진입 가능 (Reentrant): 실행 중 수정되지 않음 - 읽기 전용: 여러 프로세스가 동시 접근 가능 - 위치 독립적 코드가 유리: 같은 물리 page를 프로세스마다 서로 다른 가상 주소에 매핑할 수도 있음


페이지 테이블 구조

📈 페이지 테이블 크기 문제

32비트 시스템에서의 문제: - 논리 주소: 32비트 (4GB) - 페이지 크기: 4KB (2^12) - 페이지 수: 2^20 = 1,048,576개 - 페이지 테이블 크기: 1M × 4바이트 = 4MB per 프로세스

graph TD
    A[페이지 테이블 크기 문제] --> B[계층적 페이징<br/>Hierarchical Paging]
    A --> C[해시 페이지 테이블<br/>Hashed Page Tables]
    A --> D[역 페이지 테이블<br/>Inverted Page Tables]

    style A fill:#ffebee
    style B fill:#e8f5e8
    style C fill:#fff3e0
    style D fill:#f3e5f5

🏗️ 계층적 페이지 테이블

두 수준 페이지 테이블

graph TD
    A[논리 주소] --> B[p1<br/>외부 페이지 번호]
    A --> C[p2<br/>내부 페이지 번호]
    A --> D[d<br/>페이지 오프셋]

    B --> E[외부 페이지 테이블]
    E --> F[내부 페이지 테이블]
    C --> F
    F --> G[프레임 번호]
    G --> H[물리 주소]
    D --> H

    style E fill:#e1f5fe
    style F fill:#fff3e0
    style G fill:#e8f5e8

주소 변환 과정:

int two_level_translation(int logical_address) {
    int page_size = 1024;  // 1KB 페이지

    // 주소 분할
    int p1 = (logical_address >> 20) & 0x3FF;  // 상위 10비트
    int p2 = (logical_address >> 10) & 0x3FF;  // 중간 10비트
    int d = logical_address & 0x3FF;           // 하위 10비트

    // 외부 페이지 테이블 접근
    int inner_table_base = outer_page_table[p1];

    // 내부 페이지 테이블 접근
    int frame_number = inner_page_table[inner_table_base + p2];

    // 물리 주소 계산
    return frame_number * page_size + d;
}

64비트 시스템의 도전

이론적 flat table 문제: - 64비트 가상 주소와 4KiB page를 그대로 가정하면 가상 page 수는 2^52개입니다. - PTE가 8바이트라면 완전히 채운 flat table은 2^55바이트가 필요합니다. - 실제 CPU는 더 좁은 canonical address 폭과 다단계·희소 page table을 사용하므로 이 이론값을 실제 프로세스 사용량으로 해석하지 않습니다.

해결책: - 희소 주소 공간 활용 - 3단계 이상의 페이징 - 해시 페이지 테이블

🔍 해시 페이지 테이블

graph TD
    A[가상 페이지 번호] --> B[해시 함수]
    B --> C[해시 테이블 인덱스]
    C --> D[체인 탐색]
    D --> E[일치하는 항목 찾기]
    E --> F[물리 프레임 번호]

    subgraph "해시 테이블 항목"
        G[가상 페이지 번호]
        H[물리 프레임 번호]
        I[다음 항목 포인터]
    end

    D --> G
    D --> H
    D --> I

    style B fill:#e1f5fe
    style D fill:#fff3e0
    style F fill:#e8f5e8

클러스터 페이지 테이블: - 각 항목이 여러 페이지 (예: 16개) 참조 - 희소 주소 공간에 효과적 - 연속된 페이지 접근 패턴 최적화

🔄 역 페이지 테이블

graph TD
    A[역 페이지 테이블] --> B[물리 메모리 기준]
    A --> C[프로세스별 테이블 불필요]
    A --> D[메모리 절약]

    subgraph "전통적 페이지 테이블"
        E[프로세스마다 테이블]
        F[논리 페이지 → 물리 프레임]
        G[크기: 논리 주소 공간에 비례]
    end

    subgraph "역 페이지 테이블"
        H[시스템에 하나]
        I[물리 프레임 → 프로세스/페이지]
        J[크기: 물리 메모리에 비례]
    end

    style H fill:#e8f5e8
    style I fill:#e8f5e8
    style J fill:#e8f5e8

역 페이지 테이블 항목:

typedef struct {
    int process_id;        // 프로세스 식별자
    int virtual_page;      // 가상 페이지 번호
    bool valid;            // 유효 비트
} inverted_page_entry_t;

inverted_page_entry_t inverted_page_table[PHYSICAL_FRAMES];

주소 변환 과정:

int inverted_table_lookup(int pid, int virtual_page) {
    // 해시 테이블 사용하여 검색 최적화
    int hash_index = hash(pid, virtual_page);

    for (int i = hash_index; i < PHYSICAL_FRAMES; i++) {
        if (inverted_page_table[i].process_id == pid &&
            inverted_page_table[i].virtual_page == virtual_page &&
            inverted_page_table[i].valid) {
            return i; // 물리 프레임 번호
        }
    }

    return -1; // 페이지 폴트
}


스와핑

💾 스와핑 개념

**스와핑**은 메모리 공간 부족 시 프로세스를 일시적으로 보조 저장소로 이동시키는 기법입니다.

이 절의 전체 프로세스 swap-in/out은 고전적 모델입니다. 현대 범용 OS는 익명 page 회수·압축·swap과 file-backed page drop을 혼합하며, 정책과 단위는 커널·설정에 따라 달라집니다.

sequenceDiagram
    participant P as 프로세스
    participant M as 주 메모리
    participant S as 스왑 공간
    participant OS as 운영체제

    OS->>M: 메모리 부족 감지
    OS->>P: 프로세스 선택 (스왑 아웃)
    P->>S: 프로세스 이미지 저장
    M->>M: 메모리 공간 확보

    Note over OS: 나중에 필요할 때

    OS->>S: 프로세스 복구 요청 (스왑 인)
    S->>M: 프로세스 이미지 로드
    M->>P: 실행 재개

⚡ 스와핑 성능

스와핑 시간 계산

// 스와핑 시간 계산
typedef struct {
    int transfer_rate;    // MB/s
    int latency;         // ms
    int process_size;    // MB
} swap_info_t;

int calculate_swap_time(swap_info_t info) {
    int transfer_time = (info.process_size * 1000) / info.transfer_rate; // ms
    int total_time = info.latency + transfer_time;
    return total_time;
}

// 예시: 100MB 프로세스, 50MB/s 전송률, 8ms 지연
swap_info_t example = {50, 8, 100};
int swap_out_time = calculate_swap_time(example);  // 2008ms
int swap_in_time = calculate_swap_time(example);   // 2008ms
int total_time = swap_out_time + swap_in_time;     // 4016ms

스와핑 최적화

graph TD
    A[스와핑 최적화] --> B[압축된 스와핑<br/>Compressed Swapping]
    A --> C[부분 스와핑<br/>Partial Swapping]
    A --> D[지능형 선택<br/>Intelligent Selection]

    B --> B1[CPU 사용하여 압축]
    B --> B2[I/O 시간 단축]

    C --> C1[더티 페이지만 스왑]
    C --> C2[읽기 전용 페이지 제외]

    D --> D1[최근 사용 빈도 고려]
    D --> D2[프로세스 우선순위 고려]

    style B fill:#e8f5e8
    style C fill:#fff3e0
    style D fill:#f3e5f5

🔄 페이징과 스와핑

graph TD
    A[메모리 관리 기법] --> B[전체 프로세스 스와핑]
    A --> C[페이지 단위 스와핑]

    B --> B1[컨텍스트 스위치 시간 증가]
    B --> B2[큰 I/O 오버헤드]
    B --> B3[간단한 구현]

    C --> C1[세밀한 메모리 관리]
    C --> C2[요구 페이징 지원]
    C --> C3[복잡한 구현]

    style B fill:#ffebee
    style C fill:#e8f5e8

현대 시스템의 스와핑

// 설명용 정책 의사 코드이며 Linux reclaim 구현이 아님
typedef struct {
    int vm_swappiness;      // 스와핑 적극성 (0-100)
    long free_memory;       // 여유 메모리
    long total_memory;      // 전체 메모리
    int memory_pressure;    // 메모리 압박 수준
} memory_state_t;

bool should_start_swapping(memory_state_t state) {
    float memory_usage = (float)(state.total_memory - state.free_memory) 
                        / state.total_memory;

    // 예시 정책 임계값: 제품 요구사항과 측정으로 정해야 함
    if (memory_usage > 0.9) {
        return true;
    }

    // 메모리 압박이 심할 때
    if (state.memory_pressure > PRESSURE_THRESHOLD) {
        return true;
    }

    return false;
}

핵심 개념 정리

📊 메모리 관리 기법 비교

graph TD
    A[메모리 관리 기법] --> B[연속 할당]
    A --> C[페이징]
    A --> D[세그멘테이션]
    A --> E[세그멘트 페이징]

    B --> B1["✅ 구현 단순<br/>❌ 외부 단편화<br/>❌ 메모리 활용률 낮음"]

    C --> C1["✅ 외부 단편화 없음<br/>✅ 메모리 보호<br/>❌ 내부 단편화"]

    D --> D1["✅ 논리적 단위<br/>✅ 공유/보호 용이<br/>❌ 외부 단편화"]

    E --> E1["✅ 두 방법의 장점<br/>❌ 복잡한 구현<br/>❌ 높은 오버헤드"]

    style B1 fill:#ffebee
    style C1 fill:#e8f5e8
    style D1 fill:#fff3e0
    style E1 fill:#f3e5f5

🎯 주소 변환 방법 비교

특성 베이스-리밋 페이징 세그멘테이션
주소 공간 연속 비연속 논리적 단위
단편화 외부 내부 외부
하드웨어 지원 간단 페이지 테이블 세그먼트 테이블
메모리 보호 범위 검사 중심 page별 권한과 OS 정책에 의존 segment 권한과 구현에 의존
공유 지원 어려움 가능 용이

💡 설계 고려사항

graph TD
    A[메모리 관리 설계] --> B[성능<br/>Performance]
    A --> C[보안<br/>Security]
    A --> D[효율성<br/>Efficiency]
    A --> E[확장성<br/>Scalability]

    B --> B1[TLB 히트율 최적화]
    B --> B2[페이지 크기 선택]
    B --> B3[캐시 친화적 설계]

    C --> C1[메모리 보호]
    C --> C2[접근 권한 제어]
    C --> C3[프로세스 격리]

    D --> D1[단편화 최소화]
    D --> D2[메모리 오버헤드 감소]
    D --> D3[스와핑 최적화]

    E --> E1[대용량 메모리 지원]
    E --> E2[다중 프로세서 확장]
    E --> E3[가상화 지원]

    style A fill:#e1f5fe

연습 문제

🧩 문제 1: 주소 변환 계산

32비트 시스템에서 페이지 크기가 4KB일 때, 논리 주소 0x12345678의 페이지 번호와 오프셋을 구하고, 페이지 테이블에서 해당 페이지가 프레임 0x9AB에 매핑되어 있다면 물리 주소를 계산하세요.

답안:

// 주어진 값
int logical_address = 0x12345678;
int page_size = 4096;  // 4KB = 2^12
int frame_number = 0x9AB;

// 페이지 번호와 오프셋 계산
int page_number = logical_address >> 12;        // 상위 20비트
int page_offset = logical_address & 0xFFF;      // 하위 12비트

printf("논리 주소: 0x%08X\n", logical_address);     // 0x12345678
printf("페이지 번호: 0x%05X\n", page_number);       // 0x12345
printf("페이지 오프셋: 0x%03X\n", page_offset);     // 0x678

// 물리 주소 계산
int physical_address = (frame_number << 12) | page_offset;
printf("물리 주소: 0x%08X\n", physical_address);    // 0x9AB678

🧩 문제 2: 내부 단편화 계산

프로세스가 다음과 같은 메모리를 요청할 때, 페이지 크기가 4KB인 시스템에서 발생하는 내부 단편화를 계산하세요: - 프로세스 A: 10,000 바이트 - 프로세스 B: 8,192 바이트
- 프로세스 C: 15,500 바이트

답안:

int page_size = 4096;  // 4KB

struct process {
    char name;
    int size;
};

struct process processes[] = {
    {'A', 10000},
    {'B', 8192},
    {'C', 15500}
};

int total_internal_fragmentation = 0;

for (int i = 0; i < 3; i++) {
    int pages_needed = (processes[i].size + page_size - 1) / page_size;
    int allocated_memory = pages_needed * page_size;
    int internal_fragmentation = allocated_memory - processes[i].size;

    printf("프로세스 %c:\n", processes[i].name);
    printf("  요청 크기: %d 바이트\n", processes[i].size);
    printf("  필요한 페이지: %d개\n", pages_needed);
    printf("  할당된 메모리: %d 바이트\n", allocated_memory);
    printf("  내부 단편화: %d 바이트\n\n", internal_fragmentation);

    total_internal_fragmentation += internal_fragmentation;
}

printf("총 내부 단편화: %d 바이트\n", total_internal_fragmentation);

결과: - 프로세스 A: 3페이지 필요, 2,288 바이트 단편화 - 프로세스 B: 2페이지 필요, 0 바이트 단편화 - 프로세스 C: 4페이지 필요, 884 바이트 단편화 - 총 내부 단편화: 3,172 바이트

🧩 문제 3: TLB 성능 분석

TLB 접근 시간이 2ns, 메모리 접근 시간이 100ns인 시스템에서 TLB 히트율에 따른 유효 접근 시간을 계산하고 그래프로 나타내세요.

답안:

#include <stdio.h>

float calculate_eat(float hit_ratio) {
    float tlb_time = 2.0;      // ns
    float memory_time = 100.0; // ns

    float hit_time = tlb_time + memory_time;              // 102ns
    float miss_time = tlb_time + 2 * memory_time;         // 202ns

    return hit_ratio * hit_time + (1 - hit_ratio) * miss_time;
}

int main() {
    printf("TLB 히트율\t유효 접근 시간\t성능 향상\n");
    printf("----------------------------------------\n");

    float base_time = 200.0; // TLB 없을 때

    for (int hit_percent = 50; hit_percent <= 99; hit_percent += 10) {
        float hit_ratio = hit_percent / 100.0;
        float eat = calculate_eat(hit_ratio);
        float improvement = ((base_time - eat) / base_time) * 100;

        printf("%d%%\t\t%.1f ns\t\t%.1f%%\n", 
               hit_percent, eat, improvement);
    }

    return 0;
}

결과:

TLB 히트율    유효 접근 시간    성능 향상
----------------------------------------
50%          152.0 ns         24.0%
60%          142.0 ns         29.0%
70%          132.0 ns         34.0%
80%          122.0 ns         39.0%
90%          112.0 ns         44.0%
99%          103.0 ns         48.5%

결론: 이 단순한 2회 메모리 접근 모델과 주어진 2ns/100ns 입력에서는 99% hit ratio가 103ns, 기준 200ns 대비 48.5%로 계산됩니다. 실제 page walk cache, 다단계 table, out-of-order 실행과 측정 기준이 달라지면 이 비율도 달라집니다.


📚 참고 자료

  • Operating System Concepts - Silberschatz, Galvin, Gagne
  • Computer Systems: A Programmer's Perspective - Bryant, O'Hallaron
  • Modern Operating Systems - Andrew S. Tanenbaum

🔗 관련 링크


© 2024 Operating Systems Study Guide. 모든 권리 보유.