제 9장: 주 메모리 (Main Memory) 💾¶
📖 목차 (Table of Contents)¶
개요¶
**주 메모리 관리**는 운영체제의 핵심 기능 중 하나로, 한정된 메모리 자원을 효율적으로 관리하고 프로세스들 간의 메모리 보호를 제공합니다.
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)¶
메모리 보호는 프로세스가 자신의 주소 공간 내에서만 접근할 수 있도록 보장합니다.
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)¶
특징: - 프로그램이 메모리에 로드될 때 주소 결정 - 실행 중 메모리 위치 이동 불가 - 대부분의 실행 파일 형태
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개의 블록이 단편화로 손실
내부 단편화 (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;
}
// 예시 계산
// 히트율 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): 실행 중 수정되지 않음 - 읽기 전용: 여러 프로세스가 동시 접근 가능 - 위치 독립적: 모든 프로세스에서 동일한 논리 주소
페이지 테이블 구조¶
📈 페이지 테이블 크기 문제¶
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비트 시스템의 도전¶
문제점: - 4KB 페이지 크기 가정 - 페이지 테이블 항목: 2^52개 - 외부 페이지 테이블만으로도 2^44 바이트 필요
해결책: - 희소 주소 공간 활용 - 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; // 페이지 폴트
}
스와핑¶
💾 스와핑 개념¶
**스와핑**은 메모리 공간 부족 시 프로세스를 일시적으로 보조 저장소로 이동시키는 기법입니다.
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 스타일 스와핑 제어
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;
// 메모리 사용률이 90% 초과 시 스와핑 시작
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
🎯 주소 변환 방법 비교¶
| 특성 | 베이스-리밋 | 페이징 | 세그멘테이션 |
|---|---|---|---|
| 주소 공간 | 연속 | 비연속 | 논리적 단위 |
| 단편화 | 외부 | 내부 | 외부 |
| 하드웨어 지원 | 간단 | 페이지 테이블 | 세그먼트 테이블 |
| 메모리 보호 | 제한적 | 우수 | 매우 우수 |
| 공유 지원 | 어려움 | 가능 | 용이 |
💡 설계 고려사항¶
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%
결론: TLB 히트율이 높을수록 성능이 크게 향상되며, 99% 히트율에서 약 48.5%의 성능 향상을 얻을 수 있습니다.
📚 참고 자료¶
- 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. 모든 권리 보유.