메모리 관리
메모리 관리의 기초 : 주소와 바인딩
모든 메모리 관리 기법의 근본적인 목표는, 한정된 물리 메모리(RAM) 를 여러 프로세스가 안전하고 효율적으로 나누어 쓰도록 하는 것입니다.
논리 주소 vs 물리 주소
- 논리 주소 (Logical Address) : 프로세스(프로그램)의 관점에서 바라보는 주소입니다. 각 프로세스는 자신만의 0번지부터 시작하는 거대한 ‘개인’ 주소 공간을 갖는 것처럼 행동합니다. CPU가 생성하는 주소가 바로 이것입니다.
- 물리 주소 (Physical Address) : 실제 메모리(RAM) 하드웨어 칩의 관점에서 본 주소입니다. 0번지부터 RAM의 실제 크기까지의 물리적인 위치를 의미합니다.
주소 바인딩(Address Binding)
프로그램이 사용하는 ‘논리 주소’를 실제 ‘물리 주소’로 매핑(Mapping) 하는 작업입니다. 이 바인딩이 언제 일어나느냐에 따라 시스템의 유연성이 결정됩니다.
컴파일 타임 (Compile Time) 바인딩
컴파일 시점에 물리 주소가 결정됩니다. 주소 변환이 필요 없어 가장 빠르지만, 프로그램이 메모리의 특정 위치에만 적재되어야 하므로 유연성이 전혀 없습니다. (현대 OS는 사용 안 함)
적재 타임 (Load Time) 바인딩
프로그램이 메모리에 적재되는 시점에 물리 주소를 결정합니다. 프로그램이 일단 메모리에 올라가면, 실행 중에는 위치를 옮길 수 없습니다.
실행 타임 (Execution Time) 바인딩
프로그램이 실행되는 도중에 주소 바인딩이 일어납니다. (명령어가 실행되는 바로 그 순간) MMU(Memory Management Unit) 라는 하드웨어의 도움을 받아, 모든 논리 주소를 실시간으로 물리 주소로 변환합니다.
- 프로세스를 실행 중에 메모리 내의 다른 위치로 자유롭게 이동(Swap-in/out)시킬 수 있습니다.
- 현대 운영체제는 모두 이 방식을 사용하며, 이는 가상 메모리의 전제 조건입니다.
프로그램의 적재 : 로딩과 연결
프로세스가 실행되려면, 디스크에 있던 프로그램이 메모리로 올라와야 합니다. 이 과정을 ‘로딩’이라고 하며, 효율성을 높이기 위한 두 가지 기법이 있습니다.
동적 로딩(Dynamic Loading)
프로그램 전체를 실행 시점에 모두 메모리에 올리는 것이 아니라, 특정 기능(루틴, 함수)이 실제로 호출되는 바로 그 순간에 해당 부분만 메모리에 적재(Load)하는 기법입니다.
- 메모리 효율 : 사용되지 않는 기능(특히 예외 처리 등)이 메모리를 낭비하지 않습니다.
- 빠른 시작 : 프로그램의 초기 로딩 속도가 매우 빨라집니다.
동작
- 프로그램의 본체(Main)만 먼저 메모리에 적재되어 실행을 시작합니다.
- 특정 함수 A(오류 처리 루틴 등)를 호출하는 코드를 만납니다.
- OS는 이 함수 A가 아직 메모리에 없는 것을 확인하고, 디스크에서 A의 코드를 찾아 빈 메모리 공간에 적재합니다.
- 함수 A의 주소를 페이지 테이블(혹은 주소록)에 갱신하고, 함수 A를 실행합니다.
동적 연결(Dynamic Linking)
printf()처럼 많은 프로그램이 공통으로 사용하는 라이브러리 함수를 처리하는 방식입니다.
- 메모리 절약 : 라이브러리 코드가 메모리에서 공유되므로 RAM 사용량이 획기적으로 줄어듭니다.
- 유지보수 :
printf함수의 버그를 고칠 때,A.exe,B.exe를 재컴파일할 필요 없이, 공유 라이브러리 파일 하나만 교체하면 됩니다.
정적 연결 (Static Linking)
(구 방식) 컴파일 시점에 printf의 기계어 코드를 복사하여 실행 파일(my_program.exe)에 포함시킵니다.
A.exe,B.exe가 모두printf를 쓰면, 메모리에printf코드가 2개 중복으로 올라가며, 파일 크기도 커집니다.
동적 연결 (Dynamic Linking)
(현대 방식) 컴파일 시점에 printf의 코드를 복사하는 대신, “이 부분은 printf를 호출하는 자리”라는 ‘스텁(Stub)’ 코드만 남겨둡니다. 프로그램이 실행될 때, OS는 이 ‘스텁’을 보고, 메모리에 이미 로드되어 있는 공유 라이브러리(DLL, .so) 의 printf 실제 주소로 연결(Link)해 줍니다.
- 메모리에
printf라이브러리는 단 하나만 올라와 있고, 모든 프로세스가 이 코드를 공유합니다.
메모리 할당 방식 : 연속 할당(Contiguous Allocation)
가장 초기의 단순한 메모리 할당 방식입니다. 하나의 프로세스를 실행하기 위해, 해당 프로세스 크기와 똑같거나 더 큰, ‘연속된’ 단일 메모리 블록에 프로세스 전체를 할당합니다.
- 고정 분할(Fixed Partition)
- 메모리를 미리 고정된 크기의 파티션으로 나눠둡니다.
- 내부 단편화(Internal Fragmentation) 가 심각합니다. 100KB짜리 프로세스가 1MB 파티션에 들어가면 900KB가 낭비됩니다.
- 가변 분할(Variable Partition)
- 프로세스가 요청하는 크기만큼의 ‘빈 공간(Hole)’을 찾아 할당합니다.
- 외부 단편화(External Fragmentation) 가 심각합니다.
가변 분할에서 프로그램을 적재할 메모리 공간을 선택하는 알고리즘
- first-fit : 가장 처음으로 발견된 가용 공간에 메모리를 할당
- best-fit : 프로세스가 들어갈 수 있는 빈 공간 중, 크기가 가장 작은 공간에 메모리를 할당
- worst-fit : 프로세스가 들어갈 수 있는 빈 공간 중, 크기가 가장 큰 공간에 메모리를 할당
단편화(Fragmentation)
연속 할당 방식의 치명적인 문제입니다.
- 내부 단편화 (Internal Fragmentation)
- 할당된 메모리 공간(파티션, 페이지)이 프로세스가 실제 필요한 크기보다 커서, 그 ‘내부’ 에 사용되지 않고 낭비되는 공간이 생기는 현상입니다.
- 외부 단편화 (External Fragmentation)
- 프로세스들이 할당되고 해제되기를 반복하면서, 메모리 사이사이에 작은 빈 공간(Hole)들이 생깁니다.
- 이 빈 공간들의 총합은 500MB로 충분하지만, 이들이 모두 1MB짜리 조각으로 흩어져 있다면, 100MB짜리 프로세스 하나가 들어갈 ‘연속된’ 공간이 없어 메모리를 할당하지 못하는 현상입니다.
- 해결책 (압축, Compaction) : OS가 흩어져 있는 프로세스들을 메모리 한쪽으로 모두 ‘밀어서’ 작은 빈 공간들을 하나의 큰 공간으로 합칩니다. (매우 비싼 오버헤드 발생)
불연속 할당(Non-Contiguous Allocation) : Paging
불연속 할당은 외부 단편화 문제를 근본적으로 해결하기 위해 등장한 현대적인 방식입니다.
프로세스를 하나의 큰 덩어리로 올릴 필요 없이, 여러 조각으로 쪼개서 메모리 곳곳의 빈 공간에 분산 배치하도록 고안되었습니다.
페이징은 가장 보편적이고 중요한 불연속 할당 기법입니다. 이는 프로세스를 메모리에 연속적으로 배치할 때 발생하는 외부 단편화(External Fragmentation) 문제를 근본적으로 해결합니다.
핵심 원리는 프로세스의 주소 공간과 실제 물리 메모리를 모두 고정된 크기의 블록으로 나누어 관리하는 것입니다.
- 페이지(Page) : 프로세스(논리 주소 공간)를 나눈 조각입니다.
- 프레임(Frame) : 물리 메모리(RAM)를 나눈 조각입니다. 페이지와 크기가 정확히 같습니다.
이 방식을 사용하면, 300MB 크기의 프로세스라도 4KB 페이지 조각들로 나뉘어, 물리 메모리 곳곳에 흩어져 있는 빈 프레임(10번, 25번, 1004번 프레임…)에 효율적으로 배치될 수 있습니다.
페이지 테이블(Page Table)
페이지 테이블은 “프로세스의 논리적 페이지가 물리 메모리의 어떤 프레임에 있는가?”라는 ‘매핑 정보(지도)’ 를 저장하는 핵심 자료구조입니다.
운영체제(OS)는 각 프로세스마다 독립적인 페이지 테이블을 하나씩 관리합니다. 예를 들어, 이 테이블에는 “프로세스의 0번 페이지는 10번 프레임에, 1번 페이지는 25번 프레임에 있다”는 정보가 저장됩니다.
페이지 테이블의 구현(in Physical Memory)
페이지 테이블 자체의 크기도 상당히 클 수 있습니다 (32비트 시스템에서 4KB 페이지를 사용하면 최대 100만 개 이상의 항목 필요). 따라서 페이지 테이블은 CPU 내부의 고속 레지스터에 보관할 수 없고, 물리 메모리(RAM)에 위치하게 됩니다.
OS는 두 개의 특수 목적 CPU 레지스터를 사용하여 현재 실행 중인 프로세스의 페이지 테이블을 가리킵니다.
- PTBR (Page Table Base Register) : 페이지 테이블의 시작 주소를 저장합니다. CPU가 페이지 테이블에 접근할 수 있도록 기준 주소를 제공합니다.
- PTLR (Page Table Length Register) : 페이지 테이블의 크기(엔트리 개수) 를 저장합니다. 이는 주소 변환 시, 프로세스가 할당받지도 않은 페이지 번호에 접근하는 것을 막는 보호(Protection) 역할을 합니다.
주소 변환 (Address Translation) 과정
페이징 시스템에서 CPU가 메모리에 접근하는 과정은 다음과 같습니다.
- CPU가 논리 주소를 생성합니다. 이 주소는
<페이지 번호(p), 오프셋(d)>두 부분으로 나뉩니다.- p (페이지 번호) : 페이지 테이블의 인덱스(index)로 사용됩니다.
- d (오프셋) : 하나의 페이지 내에서의 상대적인 위치(변위)입니다.
- MMU(메모리 관리 장치)는 먼저
p가 PTLR 값보다 작은지 검사합니다. (만약p가 PTLR보다 크면, 이는 잘못된 접근이므로 트랩(Trap)을 발생시킵니다.) - MMU는 PTBR에 저장된 ‘페이지 테이블 시작 주소’와
p를 이용해, 실제 페이지 테이블 항목(PTE)이 있는 물리 메모리 주소를 계산합니다. - 해당 주소의 메모리에 접근하여 프레임 번호(
f)를 읽어옵니다. ← (1차 메모리 접근) - MMU는 획득한 프레임 번호(
f)와 원래의 오프셋(d)을 조합하여 최종 물리 주소(f, d)를 만듭니다. - 이 물리 주소를 통해 실제 데이터에 접근합니다. ← (2차 메모리 접근)
페이징의 성능 문제와 TLB
위 과정에서 보듯이, 페이징은 모든 메모리 접근마다 두 번의 RAM 접근(페이지 테이블 접근 1번, 실제 데이터 접근 1번)을 필요로 합니다. 이는 메모리 접근 속도를 절반으로 떨어뜨리는 심각한 오버헤드입니다.
이 성능 문제를 해결하기 위해 TLB(Translation Lookaside Buffer) 라는 특수한 하드웨어 캐시를 사용합니다.
- TLB는 MMU 내부에 존재하는 매우 빠른 연관 캐시이며, 페이지 테이블의 일부, 즉 최근에 사용된 (페이지, 프레임) 매핑 정보를 저장합니다.
- 프로그램은 참조의 지역성(Locality of Reference)(최근 접근한 메모리는 곧 다시 접근할 확률이 높음)을 갖기 때문에, TLB Hit 비율은 매우 높습니다.
TLB를 포함한 주소 변환 과정
- CPU가 논리 주소
<p, d>를 생성합니다. - MMU는
p를 TLB에서 먼저 고속으로 검색합니다. - TLB Hit (성공)
p에 대한 프레임 번호f가 TLB에 있습니다.- MMU는 즉시 물리 주소
(f, d)를 만들어 실제 데이터에 접근합니다. (페이지 테이블 접근 생략, 메모리 접근 1회)
- TLB Miss (실패)
p가 TLB에 없습니다.- MMU는 (느린 방식대로) PTBR을 참조하여 RAM의 페이지 테이블에 접근합니다. (1차 접근)
- 페이지 테이블에서 프레임 번호
f를 찾아냅니다. - 이
(p, f)매핑 정보를 TLB에 새로 저장합니다. (오래된 항목은 교체) - 물리 주소
(f, d)를 만들어 실제 데이터에 접근합니다. (2차 접근)
불연속 할당(Non-Contiguous Allocation) : Segmentation
세그먼테이션은 페이징과 함께 가상 메모리를 구현하는 또 다른 불연속 할당 방식입니다. 하지만 페이징이 메모리를 ‘물리적인 고정 크기(4KB)’로 나누는 것과 달리, 세그먼테이션은 메모리를 ‘논리적인 의미 단위’ 로 나눕니다.
즉, 프로세스를 Code Segment, Data Segment, Stack Segment처럼 의미가 다른 가변 크기의 덩어리(세그먼트)로 분할하여 관리합니다.
- 세그먼트 (Segment) : 프로그램의 논리적인 구성 요소 (코드, 데이터, 스택 등). 각 세그먼트는 서로 다른 크기를 가질 수 있습니다.
- 세그먼트 번호 (s) : 각 세그먼트를 식별하는 고유 번호입니다.
- 오프셋 (d) : 해당 세그먼트의 시작점으로부터 떨어진 상대적인 위치(변위)입니다.
세그먼트 테이블 (Segment Table)
OS는 각 프로세스마다 세그먼트 테이블을 관리합니다. 이 테이블은 각 세그먼트(논리 단위)가 물리 메모리의 어디에 위치하는지, 그리고 그 크기가 얼마인지를 저장합니다.
각 테이블 항목(엔트리)은 주로 두 가지 핵심 정보로 구성됩니다.
- base (기준 주소): 해당 세그먼트가 물리 메모리에서 시작하는 주소입니다.
- limit (한계/크기): 해당 세그먼트의 크기(허용되는 최대 오프셋)입니다.
주소 변환 (Address Translation) 과정
- CPU가 논리 주소
<세그먼트 번호(s), 오프셋(d)>를 생성합니다. - MMU(메모리 관리 장치)는 세그먼트 테이블에서
s번 항목을 조회하여base와limit값을 찾습니다. - (보호 단계) MMU는
d가limit값보다 작은지 검사합니다.- 만약
d >= limit이면, 이는 세그먼트의 경계를 벗어난 잘못된 접근(메모리 침범)이므로, CPU에 ‘세그먼테이션 폴트(Segmentation Fault)’ 라는 트랩(Trap)을 발생시킵니다.
- 만약
- (변환 단계) 접근이 유효하다면, 물리 주소 =
base[s] + d를 계산하여 실제 RAM에 접근합니다.
페이징(Paging) vs 세그먼테이션(Segmentation)
| 특징 | 페이징 (Paging) | 세그먼테이션 (Segmentation) |
|---|---|---|
| 분할 기준 | 물리적 (Physical) | 논리적 (Logical) |
| 분할 단위 | 페이지 (Page) (고정된 크기, 4KB) | 세그먼트 (Segment) (의미 단위의 가변 크기, Code, Data) |
| 주요 장점 | 외부 단편화 해결 (메모리 할당 효율 극대화) | 논리적 보호 및 공유 (Code는 Read-Only, Data는 R/W) |
| 주요 단점 | 내부 단편화 발생 (마지막 페이지에서 낭비 발생) | 외부 단편화 발생 (가변 크기 할당으로 인한 빈 공간 조각화) |
| 주소 변환 | (페이지 번호, 오프셋) → 페이지 테이블 조회 | (세그먼트 번호, 오프셋) → 세그먼트 테이블 조회 (크기 검사 포함) |
| 관리 복잡성 | OS가 관리하기 비교적 단순함 (모든 조각의 크기가 같음) | OS가 관리하기 복잡함 (가변 크기 할당 문제, 외부 단편화 관리) |
| 프로그래머 관점 | 프로그래머는 페이징을 거의 인식하지 못함 (OS가 투명하게 처리) | 프로그래머가 논리적 단위를 인식함 (Code, Stack 세그먼트) |
- 페이징은 OS 입장에서 메모리 할당 효율성을 극대화하는 데 초점을 맞춘 기법입니다. (외부 단편화 해결)
- 세그먼테이션은 프로그래머/프로세스 입장에서 보호 및 공유를 직관적으로 관리하는 데 초점을 맞춘 기법입니다.
이 때문에 현대의 많은 시스템(특히 x86)은 두 기법을 혼용합니다. 즉, 세그먼테이션으로 Code, Data 등 논리적 보호를 먼저 적용한 뒤, 각 세그먼트 내부를 다시 페이징(Paging)하여 물리 메모리에 효율적으로 배치합니다.