- Authors

- Name
- Youngju Kim
- @fjvbn20031
배경
기본 개념
CPU가 직접 접근할 수 있는 저장소는 레지스터와 메인 메모리뿐이다. 디스크의 데이터는 반드시 메모리에 올라와야 CPU가 처리할 수 있다.
메모리 접근에는 시간이 걸리므로, CPU와 메모리 사이에 캐시를 두어 속도 차이를 완화한다.
CPU <-> 레지스터 (~1ns) <-> 캐시 (~10ns) <-> 메인 메모리 (~100ns)
주소 바인딩 (Address Binding)
프로그램에서 사용하는 주소를 실제 메모리 주소로 변환하는 과정이다. 바인딩 시점에 따라 세 가지로 나뉜다.
[주소 바인딩 시점]
1. 컴파일 시간 바인딩
- 프로세스가 메모리의 어디에 올라갈지 컴파일 시 결정
- 위치가 바뀌면 재컴파일 필요
- 예: MS-DOS의 .COM 프로그램
2. 로드 시간 바인딩
- 프로그램을 메모리에 올릴 때 주소 결정
- 재배치 가능 코드(relocatable code) 사용
- 로드 후 주소 변경 불가
3. 실행 시간 바인딩 (현대 OS)
- 실행 중에 주소를 동적으로 변환
- MMU(Memory Management Unit) 하드웨어 필요
- 프로세스를 메모리 내에서 이동 가능
논리적 주소와 물리적 주소
- 논리적 주소(Logical Address): CPU가 생성하는 주소. 가상 주소라고도 함
- 물리적 주소(Physical Address): 실제 메모리 하드웨어의 주소
[MMU를 통한 주소 변환]
CPU --[논리적 주소: 346]--> MMU --[물리적 주소: 14346]--> 메모리
|
재배치 레지스터
(기준값: 14000)
물리적 주소 = 논리적 주소 + 재배치 레지스터 값
346 + 14000 = 14346
사용자 프로그램은 논리적 주소만 다루며, 물리적 주소를 직접 볼 수 없다.
동적 로딩과 동적 링킹
동적 로딩: 루틴이 호출될 때만 메모리에 적재한다. 사용하지 않는 루틴은 메모리를 차지하지 않으므로 메모리 이용률이 향상된다.
동적 링킹: 라이브러리를 실행 시간에 링크한다.
[정적 링킹 vs 동적 링킹]
정적 링킹:
프로그램 A: [코드 + libc 복사본] -- 50MB
프로그램 B: [코드 + libc 복사본] -- 50MB
총 메모리: 100MB
동적 링킹 (공유 라이브러리):
프로그램 A: [코드 + libc 참조] -- 10MB
프로그램 B: [코드 + libc 참조] -- 10MB
libc.so: [공유 라이브러리] -- 40MB
총 메모리: 60MB
Linux: .so 파일 (Shared Object)
Windows: .dll 파일 (Dynamic-Link Library)
연속 메모리 할당
가장 단순한 메모리 관리 방식으로, 각 프로세스를 연속된 메모리 영역에 배치한다.
메모리 보호
[기준 레지스터와 한계 레지스터]
기준(base) 한계(limit)
| |
v v
+-------+=======================+-------+
| OS | 프로세스 P의 영역 | 기타 |
+-------+=======================+-------+
0 300040 420940
CPU가 생성한 주소 addr에 대해:
if (addr >= base && addr < base + limit)
접근 허용 -> 물리 메모리 접근
else
트랩 발생 -> OS가 오류 처리
메모리 할당 전략
빈 공간(hole) 목록에서 프로세스에게 메모리를 할당하는 방법이다.
[메모리 할당 예시]
초기 상태:
|---OS---|--P1--|-------빈공간-------|--P3--|---빈공간---|
할당 전략:
1. 최초 적합(First Fit): 첫 번째로 충분한 빈 공간에 할당
장점: 빠름
2. 최적 적합(Best Fit): 가장 작은 충분한 빈 공간에 할당
장점: 작은 남은 공간 생성
단점: 전체 탐색 필요, 매우 작은 조각 생성
3. 최악 적합(Worst Fit): 가장 큰 빈 공간에 할당
장점: 남은 공간이 커서 재활용 가능
단점: 전체 탐색 필요
단편화 (Fragmentation)
[외부 단편화 vs 내부 단편화]
외부 단편화:
|P1| 빈 |P2| 빈 |P3| 빈 |P4|
100KB 50KB 80KB
총 빈 공간: 230KB인데, 200KB 프로세스를 올릴 수 없음!
(연속 공간이 아니므로)
해결: 압축(Compaction) - 프로세스를 한쪽으로 이동
|P1|P2|P3|P4|-----230KB 빈 공간-----|
내부 단편화:
메모리를 고정 크기 블록으로 할당할 때
프로세스가 블록보다 작으면 내부에 남는 공간 발생
프로세스 크기: 18,462 바이트
할당 블록 크기: 20,000 바이트
내부 단편화: 1,538 바이트 낭비
페이징 (Paging)
페이징은 논리적 주소 공간을 비연속적으로 할당하여 외부 단편화를 완전히 제거하는 기법이다.
기본 개념
- 페이지(Page): 논리적 메모리를 고정 크기 블록으로 나눈 것
- 프레임(Frame): 물리적 메모리를 같은 크기의 블록으로 나눈 것
- 페이지 테이블(Page Table): 페이지를 프레임에 매핑하는 테이블
[페이징 주소 변환]
논리적 주소 = 페이지 번호(p) + 페이지 오프셋(d)
예: 페이지 크기 4KB (2^12), 논리 주소 32비트
상위 20비트 = 페이지 번호 (최대 2^20 = 1M 페이지)
하위 12비트 = 오프셋 (0 ~ 4095)
+--------+--------+
| p (20) | d (12) | 논리적 주소
+--------+--------+
|
v
[페이지 테이블]
p -> f (프레임 번호)
|
v
+--------+--------+
| f (20) | d (12) | 물리적 주소
+--------+--------+
[페이징 예시]
논리 메모리 (4페이지): 물리 메모리 (8프레임):
+------+ +------+
|페이지0| ----+ | | 프레임0
+------+ | +------+
|페이지1| --+ | |페이지2| 프레임1
+------+ | | +------+
|페이지2| -+| | | | 프레임2
+------+ || | +------+
|페이지3| || +------------>|페이지0| 프레임3
+------+ || +------+
|+--------------->|페이지1| 프레임4
| +------+
+---------------->|페이지2| 프레임5 (X)
+------+
|페이지3| 프레임6
+------+
| | 프레임7
+------+
페이지 테이블:
페이지0 -> 프레임3
페이지1 -> 프레임4
페이지2 -> 프레임1
페이지3 -> 프레임6
TLB (Translation Lookaside Buffer)
매번 페이지 테이블을 메모리에서 조회하면 메모리 접근이 두 배로 늘어난다. TLB는 페이지 테이블의 캐시 역할을 한다.
[TLB를 통한 주소 변환]
CPU --논리 주소(p, d)--> TLB 검색
|
+-----------+-----------+
| |
TLB 히트 TLB 미스
(빠름, ~1ns) (느림)
| |
프레임 f 획득 페이지 테이블 조회
| 프레임 f 획득
| TLB에 등록
| |
+----------+------------+
|
물리 주소 (f, d)로
메모리 접근
유효 접근 시간(EAT) 계산:
TLB 히트율 = 99% (일반적)
메모리 접근 시간 = 100ns
TLB 접근 시간 = 10ns
EAT = 0.99 * (10 + 100) + 0.01 * (10 + 100 + 100)
= 0.99 * 110 + 0.01 * 210
= 108.9 + 2.1
= 111ns
TLB 없이: 200ns (메모리 2번 접근)
TLB 있으면: 111ns (44.5% 개선)
페이지 테이블 구조
프로세스의 주소 공간이 크면(예: 64비트) 페이지 테이블 자체가 매우 커진다.
계층적 페이지 테이블 (Multi-level Page Table)
[2단계 페이지 테이블]
32비트 주소, 4KB 페이지:
+--------+--------+--------+
| p1(10) | p2(10) | d(12) |
+--------+--------+--------+
외부 페이지 테이블 (1단계)
+---+
| 0 |---> 2단계 테이블 A
+---+ +---+
| 1 |--+ | 0 |--> 프레임 번호
+---+ | +---+
| 2 | | | 1 |--> 프레임 번호
+---+ | +---+
| | ...|
| +---+
|
+-> 2단계 테이블 B
+---+
| 0 |--> 프레임 번호
+---+
| ...|
+---+
장점: 사용하지 않는 영역의 2단계 테이블은 생성하지 않음
-> 메모리 절약
해시 페이지 테이블
[해시 페이지 테이블]
논리 페이지 번호 p를 해시 함수에 입력
-> 해시 테이블의 슬롯으로 매핑
-> 체인(연결 리스트)에서 p를 검색
-> 대응하는 프레임 번호 반환
64비트 주소 공간처럼 매우 큰 경우에 유용
역 페이지 테이블 (Inverted Page Table)
[역 페이지 테이블]
일반 페이지 테이블: 프로세스당 하나 (논리 -> 물리)
역 페이지 테이블: 시스템에 하나 (물리 프레임 -> 프로세스, 페이지)
물리 메모리의 각 프레임에 대해 하나의 항목:
프레임 0: (PID=5, 페이지=3)
프레임 1: (PID=2, 페이지=7)
프레임 2: (비어 있음)
프레임 3: (PID=5, 페이지=0)
...
장점: 물리 메모리 크기에 비례하는 테이블 크기
단점: 주소 변환 시 전체 테이블 검색 필요 (해시로 해결)
스와핑 (Swapping)
프로세스 전체 또는 일부를 디스크로 내보내 메모리를 확보하는 기법이다.
[표준 스와핑]
메인 메모리 백킹 스토어(디스크)
+---------+ +---------+
| OS | | |
+---------+ | P2의 |
| P1 | <-- swap in --- | 이미지 |
+---------+ | |
| P3 | --- swap out --> | |
+---------+ +---------+
| 빈공간 |
+---------+
현대 시스템에서는 프로세스 전체가 아닌
페이지 단위로 스와핑 (페이지 스와핑)
[페이지 단위 스와핑]
프로세스의 특정 페이지만 디스크로 이동:
- 오랫동안 사용하지 않은 페이지를 swap out
- 필요할 때 다시 swap in
- 가상 메모리의 기반 기술
메모리 보호
페이징 환경에서의 메모리 보호는 페이지 테이블에 보호 비트를 추가하여 구현한다.
[페이지 테이블 항목의 구조]
+-------+-----+-----+-----+-------+
| 프레임 | 유효 | 읽기 | 쓰기 | 실행 |
| 번호 | 비트 | 가능 | 가능 | 가능 |
+-------+-----+-----+-----+-------+
유효 비트(Valid bit):
1 = 이 페이지가 프로세스의 논리 주소 공간에 속함
0 = 유효하지 않은 페이지 (접근 시 트랩 발생)
보호 비트:
읽기 전용 페이지에 쓰기 시도 -> 하드웨어 트랩
코드 페이지에 대해 실행만 허용, 쓰기 불허
// 메모리 보호 예시: mprotect 시스템 콜
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void segfault_handler(int sig) {
printf("세그먼트 폴트 발생! 보호된 메모리 접근 시도\n");
exit(1);
}
int main() {
signal(SIGSEGV, segfault_handler);
// 페이지 크기 단위로 메모리 할당
size_t page_size = getpagesize(); // 보통 4096
void *ptr = aligned_alloc(page_size, page_size);
// 데이터 쓰기
*(int *)ptr = 42;
printf("값: %d\n", *(int *)ptr);
// 메모리를 읽기 전용으로 보호
mprotect(ptr, page_size, PROT_READ);
// 쓰기 시도 -> 세그먼트 폴트!
*(int *)ptr = 100;
free(ptr);
return 0;
}
정리
메인 메모리 관리는 프로세스에게 효율적이고 안전한 메모리 공간을 제공하기 위한 핵심 기능이다. MMU가 논리적 주소를 물리적 주소로 변환하고, 페이징은 외부 단편화를 제거하며, TLB가 주소 변환 성능을 보장한다. 계층적 페이지 테이블과 역 페이지 테이블은 대용량 주소 공간을 효율적으로 관리하고, 보호 비트를 통해 프로세스 간 메모리 격리를 보장한다.