안녕하세요! CPU 스케줄링과 프로세스 동기화에 이어, 운영체제의 또 다른 핵심 기능인 메모리 관리(Memory Management)에 대해 알아보겠습니다. 여러 프로세스가 동시에 실행되려면, 한정된 물리 메모리를 어떻게 효율적으로 나누어 사용하고 서로 보호할 수 있을까요?
이번 글에서는 메모리 관리의 가장 기본적인 개념인 논리 주소와 물리 주소의 차이점, 그리고 이 둘을 연결하는 주소 바인딩(Address Binding)과 MMU(Memory Management Unit)의 역할, 초기 메모리 할당 방식인 연속 할당(Contiguous Allocation)까지 살펴보겠습니다.
참고 강의 링크: https://core.ewha.ac.kr/publicview/C0101020140425151219100144?vmode=f
반효경 [운영체제] 18. Memory Management 1
설명이 없습니다.
core.ewha.ac.kr
1. 논리 주소 (Logical Address) vs 물리 주소 (Physical Address)
메모리 주소를 이야기할 때는 두 가지 종류를 구분해야 합니다.
- 논리 주소 (Logical Address) 또는 가상 주소 (Virtual Address):
- 프로세스마다 독립적으로 가지는 주소 공간입니다. 각 프로세스는 자신만의 0번지부터 시작하는 주소 공간을 가지고 있다고 생각합니다.
- CPU가 생성하고 참조하는 주소는 바로 이 논리 주소입니다. CPU는 현재 실행 중인 프로세스의 논리적인 주소 공간 내에서 명령어(Instruction)를 가져오고 데이터를 읽고 씁니다.
- 왜 CPU는 논리 주소를 볼까? 프로그래머가 코드를 작성할 때(예: int x; x = 10;), 특정 물리 메모리 주소를 직접 지정하는 것이 아니라 변수 이름(x)과 같은 심볼릭 주소(Symbolic Address)를 사용합니다. 컴파일러와 링커는 이를 해당 프로세스만의 논리적인 주소(예: 100번지)로 변환하여 실행 파일에 기록합니다. CPU는 이 실행 파일을 실행할 때 해당 논리 주소를 참조하게 됩니다. CPU가 실제 물리 주소를 직접 다루면, 여러 프로세스를 동시에 실행하거나 메모리 내에서 프로세스 위치를 옮기는 것이 매우 복잡해집니다.
- 물리 주소 (Physical Address):
- 메모리(RAM) 하드웨어 상의 실제 주소입니다. 메모리 컨트롤러가 이해하는 주소죠.
- 주소 변환 (Address Translation) 또는 주소 바인딩 (Address Binding):
- 프로세스의 논리 주소를 실제 물리 메모리 주소로 매핑(Mapping)하는 과정입니다.
- 언제 변환될까? 이 변환이 언제 이루어지느냐가 메모리 관리 기법을 구분하는 중요한 기준이 됩니다. (아래 주소 바인딩 시점 참조)
- 누가 변환할까? MMU(Memory Management Unit)라는 하드웨어 장치가 CPU와 메모리 사이에서 이 변환을 매우 빠르게 수행합니다.
(질문) 실제 물리 메모리 공간은 한정적인데, 어떻게 수많은 프로그램을 동시에 실행하고 각 프로그램이 자신만의 0번지부터 시작하는 큰 주소 공간을 가질 수 있을까요? 이것이 바로 가상 메모리 시스템의 핵심이며, 논리 주소와 물리 주소를 분리함으로써 가능해집니다. 모든 논리 주소 공간이 항상 물리 메모리에 올라와 있을 필요는 없습니다! (페이징 시스템에서 자세히 다룸)
[Q&A]
- Q: "CPU가 보는 주소가 논리 주소라면, 최종적으로 메모리에서 데이터를 가져올 때는 어떻게 물리 주소를 알게 되나요?"
- A: CPU가 논리 주소를 통해 메모리에 접근하려고 하면, 그 요청은 MMU(Memory Management Unit)라는 하드웨어 장치를 거치게 됩니다. MMU는 운영체제가 관리하는 매핑 정보(예: 페이지 테이블)를 참조하여 해당 논리 주소에 대응하는 물리 주소를 찾아내고, 이 물리 주소를 이용해 실제 메모리 버스로 요청을 보냅니다. 이 모든 과정은 하드웨어 수준에서 매우 빠르게 이루어지므로 CPU는 마치 논리 주소로 직접 메모리에 접근하는 것처럼 느끼게 됩니다.
2. 주소 바인딩 (Address Binding) 시점
논리 주소를 물리 주소로 변환하는 시점은 크게 세 가지로 나눌 수 있습니다.
- 컴파일 타임 바인딩 (Compile time binding):
- 컴파일 시점에 해당 프로세스가 적재될 물리 메모리 주소를 미리 결정하고, 그 주소에 맞게 코드를 생성(절대 코드, Absolute Code)합니다. 논리 주소가 곧 물리 주소가 됩니다.
- 장점: 실행 시 주소 변환 과정이 없어 빠릅니다.
- 단점: 프로세스의 시작 위치가 변경되면 반드시 재컴파일해야 합니다. 현대적인 다중 프로그래밍 환경에서는 거의 사용되지 않습니다. (예: 아주 단순한 임베디드 시스템이나 MS-DOS 시절의 .com 파일)
- 로드 타임 바인딩 (Load time binding):
- 컴파일 시에는 물리 주소를 확정하지 않고, 재배치 가능 코드(Relocatable Code)를 생성합니다. 논리 주소는 0번지부터 시작하는 것으로 간주됩니다.
- 프로그램이 메모리에 적재(Load)되는 시점에, 로더(Loader)가 비어있는 메모리 위치를 찾아 그 시작 주소(Base Address)를 기준으로 논리 주소를 물리 주소로 변환하여 적재합니다.
- 장점: 컴파일 타임 바인딩보다 유연합니다. 시작 위치 변경 시 재컴파일 불필요.
- 단점: 일단 메모리에 적재된 후에는 실행 중에 위치를 옮기기 어렵습니다. (모든 주소를 다시 계산해야 함)
- 실행 시간 바인딩 (Execution time binding 또는 Runtime binding):
- 현대 운영체제가 사용하는 방식. 프로그램이 실행을 시작한 이후에도 물리 메모리 상의 위치가 변경될 수 있습니다.
- CPU가 논리 주소를 참조할 때마다, MMU라는 하드웨어가 실시간으로 해당 논리 주소를 물리 주소로 변환해 줍니다.
- 이를 위해 하드웨어 지원(MMU, Base/Limit 레지스터 또는 페이지 테이블)이 반드시 필요합니다.
- 장점: 매우 유연합니다. 프로세스를 메모리 내에서 자유롭게 이동시킬 수 있어 메모리 압축(Compaction)이나 페이징/스와핑(Swapping) 같은 고급 메모리 관리 기법 사용이 가능합니다.
- 단점: 주소 변환을 위한 하드웨어 비용과 약간의 시간 오버헤드가 발생합니다.
- 과정 요약:
- 프로그래머 (소스코드) → 심볼릭 주소 (변수명 등)
- 컴파일러/링커 (실행파일) → 논리 주소 (0번지부터 시작)
- 로더/실행 (메모리 적재) → 물리 주소 (실제 RAM 주소) (바인딩 시점에 따라 변환)
[Q&A]
- Q: "컴파일 타임 바인딩은 언제 사용될 수 있나요? 정말 지금은 안 쓰나요?"
- A: 네, 현대적인 데스크톱이나 서버 운영체제에서는 거의 사용하지 않습니다. 하지만 아주 단순한 시스템, 예를 들어 메모리 구조가 고정되어 있고 단일 프로그램만 실행되는 일부 임베디드 시스템이나 초기 부트로더 등에서는 여전히 사용될 수 있습니다. 운영체제 없이 하드웨어를 직접 제어하는 경우에도 유사한 방식이 쓰일 수 있습니다.
- Q: "실행 시간 바인딩에서 프로세스 위치를 옮기는 게 왜 필요한가요?"
- A: 여러 프로세스가 메모리에 적재되고 종료되기를 반복하면, 사용 가능한 메모리 공간(Hole)들이 작은 조각으로 나뉘어 흩어지는 외부 단편화(External Fragmentation) 문제가 발생합니다. 실행 시간 바인딩은 이렇게 흩어진 조각들을 모아 큰 가용 공간을 만드는 메모리 압축(Compaction)을 가능하게 합니다. 또한, 메모리가 부족할 때 프로세스 전체 또는 일부를 디스크로 내렸다가(Swap-out) 나중에 다시 다른 위치에 올리는(Swap-in) 스와핑(Swapping)이나 페이징(Paging) 기법을 사용할 수 있게 해줍니다. 즉, 메모리 활용률을 높이는 데 필수적입니다.
3. MMU (Memory-Management Unit): 논리 주소를 물리 주소로!
- MMU란? CPU가 요청하는 논리 주소를 물리 주소로 변환해주는 하드웨어 장치입니다. CPU 칩 내부에 포함되거나 별도의 칩으로 존재할 수 있습니다.
- 기본적인 MMU 동작 방식 (Relocation Register 사용):
- 가장 단순한 형태의 MMU는 재배치 레지스터(Relocation Register, 또는 Base Register)를 사용합니다.
- 이 레지스터에는 해당 프로세스가 적재된 물리 메모리의 시작 주소가 저장되어 있습니다.
- CPU가 논리 주소 L을 생성하면, MMU는 이 논리 주소에 재배치 레지스터 값 R을 더하여 물리 주소 P = L + R을 계산합니다.
- 사용자 프로그램은 논리 주소만 사용하며, 실제 물리 주소는 MMU를 통해 자동으로 변환되므로 알 필요도 없고 알 수도 없습니다 (메모리 보호).
- 동적 재배치 (Dynamic Relocation)와 메모리 보호:
- 실행 시간 바인딩을 지원하는 MMU는 보통 재배치 레지스터와 한계 레지스터(Limit Register)를 함께 사용합니다.
- 재배치 레지스터 (Base Register): 프로세스가 접근할 수 있는 가장 작은 물리 주소 (시작 주소).
- 한계 레지스터 (Limit Register): 프로세스가 사용할 수 있는 논리 주소 공간의 크기 (범위).
- 주소 변환 및 보호 과정:
- CPU가 논리 주소 L을 생성합니다.
- MMU는 먼저 논리 주소 L이 한계 레지스터 값보다 작은지 확인합니다 (L < Limit Register).
- 만약 L이 더 크거나 같으면, 이는 프로세스가 자신의 할당된 주소 공간을 벗어나려는 시도이므로 **메모리 접근 오류(Segmentation Fault 등)**를 발생시키고 운영체제에게 트랩(Trap)을 겁니다. (메모리 보호)
- 만약 L이 더 작으면 유효한 접근입니다.
- MMU는 유효한 논리 주소 L에 재배치 레지스터 값 R을 더하여 최종 물리 주소 P = L + R을 계산합니다.
- 이 물리 주소 P를 이용해 메모리에 접근합니다.
+---------+ 논리 주소 (L) +-----------------+ 물리 주소 (P) | CPU | ---- (L) ------------> | MMU | ---- (P=L+R) ----> [메모리] +---------+ +-----------------+ | ^ | | Check L < Limit V | +-------+ +-------+ | Limit | | Reloc | | Reg. | | Reg(R)| +-------+ +-------+
- 실행 시간 바인딩을 지원하는 MMU는 보통 재배치 레지스터와 한계 레지스터(Limit Register)를 함께 사용합니다.
[Q&A]
- Q: "단순히 Base/Limit 레지스터만 사용하는 MMU 방식은 현대 시스템에서도 쓰이나요?"
- ✅ A: 아닙니다. Base/Limit 레지스터 방식은 연속 메모리 할당(Contiguous Allocation)을 가정하는 가장 기본적인 MMU 기법입니다. 이 방식은 앞서 언급한 외부 단편화 문제를 해결하기 어렵습니다. 현대 운영체제는 대부분 페이징(Paging)이나 세그멘테이션(Segmentation) 같은 불연속 메모리 할당(Noncontiguous Allocation) 기법을 사용하며, 이를 지원하기 위해 페이지 테이블(Page Table)이나 세그먼트 테이블(Segment Table)을 사용하는 더 복잡한 MMU 구조를 가집니다. Base/Limit 레지스터 방식은 이러한 고급 기법의 기본 원리를 이해하는 데 도움이 됩니다.
4. 메모리 활용률을 높이기 위한 기법들 (초기 개념)
한정된 메모리를 효율적으로 사용하기 위해 다양한 기법들이 고안되었습니다.
- 동적 로딩 (Dynamic Loading):
- 프로세스 실행 시작 시 모든 코드를 메모리에 올리는 대신, 특정 루틴(함수)이 실제로 호출될 때 해당 코드 부분을 메모리에 적재(Load)하는 방식입니다.
- 장점: 메모리 사용률 향상 (특히 잘 사용되지 않는 코드 부분), 빠른 프로그램 시작 시간.
- 구현: 운영체제의 특별한 지원 없이도 프로그래머가 라이브러리 등을 이용해 구현할 수 있습니다. (예: 필요할 때 DLL/Shared Object 로딩)
- 동적 연결 (Dynamic Linking):
- 정적 연결(Static Linking)은 컴파일 시점에 라이브러리 코드를 실행 파일에 모두 포함시키는 방식입니다. 실행 파일 크기가 커지고, 동일 라이브러리를 여러 프로세스가 중복해서 메모리에 올리는 낭비가 발생합니다.
- 동적 연결은 라이브러리 코드를 실행 파일에 포함하지 않고, 프로그램 실행 시점에 메모리에 이미 있는 라이브러리 루틴과 연결(Link)하여 사용하는 방식입니다.
- 동작: 실행 파일에는 라이브러리 함수 호출 위치에 스텁(Stub)이라는 작은 코드 조각만 포함됩니다. 스텁은 해당 라이브러리가 메모리에 있는지 확인하고, 없으면 디스크에서 로드한 후, 실제 라이브러리 함수의 주소로 점프하여 실행합니다.
- 장점: 실행 파일 크기 감소, 메모리 절약 (라이브러리 공유), 라이브러리 업데이트 용이.
- 구현: 운영체제의 도움이 필요합니다 (공유 라이브러리 관리 및 로딩).
- 오버레이 (Overlays):
- 매우 초창기 메모리 관리 기법. 물리 메모리 크기가 프로세스 크기보다 작을 때 사용되었습니다.
- 프로세스를 의미 있는 조각(Overlay)들로 나누고, 실행에 당장 필요한 조각만 메모리에 올려놓고, 필요에 따라 다른 조각으로 교체하는 방식입니다.
- 구현: 프로그래머가 수동으로 어떤 조각을 언제 올리고 내릴지 관리해야 했습니다. 프로그래밍이 매우 복잡했습니다.
- 현황: 현대 운영체제의 가상 메모리 시스템(페이징)으로 완전히 대체되어 더 이상 사용되지 않습니다. 역사적인 의미만 있습니다.
- 스와핑 (Swapping):
- 메모리가 부족할 때, 우선순위가 낮거나 당장 실행되지 않는 프로세스 전체를 일시적으로 메모리에서 보조 저장장치(Backing Store, 보통 디스크의 Swap Area)로 내쫓는(Swap-out) 기법입니다.
- 나중에 해당 프로세스를 다시 실행해야 할 때, 보조 저장장치에서 메모리의 빈 공간으로 다시 불러옵니다(Swap-in).
- 중기 스케줄러(Swapper)가 어떤 프로세스를 Swap-out 할지 결정합니다.
- 주소 바인딩과의 관계:
- 컴파일 타임 또는 로드 타임 바인딩 환경에서는 Swap-in 시 반드시 원래의 물리 주소 위치로 돌아와야 합니다.
- 실행 시간 바인딩 환경에서는 메모리의 어떤 가용 공간으로든 Swap-in 될 수 있어 훨씬 유연합니다. (MMU가 주소 변환)
- 오버헤드: 디스크 I/O는 매우 느리므로, Swap-in/Swap-out에 걸리는 시간(Transfer Time)이 상당히 큽니다. 프로세스 크기가 클수록 오래 걸립니다.
- 현황: 현대 시스템에서는 프로세스 전체를 스와핑하기보다는, 페이징 시스템과 결합하여 페이지 단위로 Swap-in/out 하는 방식(Demand Paging)을 주로 사용합니다. 하지만 스와핑의 기본 개념은 여전히 중요합니다.
[Q&A]
- Q: "동적 로딩과 동적 연결은 어떻게 다른가요?"
- A: 동적 로딩은 프로그램의 일부 코드(루틴)를 실행 시점에 필요할 때 메모리에 올리는 것이고, 동적 연결은 프로그램이 사용하는 라이브러리 코드를 실행 시점에 메모리에 있는 공유 라이브러리와 연결하는 것입니다. 목적은 다르지만 둘 다 실행 시점에 필요한 코드만 로드/연결하여 메모리 효율성을 높인다는 공통점이 있습니다. 동적 연결된 라이브러리 자체도 동적 로딩 방식으로 메모리에 올라올 수 있습니다.
- Q: "스와핑은 너무 느릴 것 같은데, 왜 사용했나요?"
- A: 과거에는 물리 메모리가 매우 작고 비쌌습니다. 스와핑은 비록 느리더라도, 실제 물리 메모리 크기보다 더 많은 수의 프로세스를 동시에 실행할 수 있는 다중 프로그래밍의 정도(Degree of Multiprogramming)를 높이는 유일한 방법이었기 때문에 사용되었습니다. 느린 디스크 I/O를 감수하고서라도 더 많은 프로그램을 동시에 돌리는 것이 중요했던 시대의 산물입니다.
5. 물리 메모리 할당 방식
운영체제는 사용자 프로세스에게 물리 메모리를 어떻게 할당할까요? 크게 연속 할당과 불연속 할당 방식으로 나뉩니다.
- 메모리 사용 영역:
- 일반적으로 메모리는 운영체제 상주 영역(주로 낮은 주소 영역, 인터럽트 벡터 포함)과 사용자 프로세스 영역(주로 높은 주소 영역)으로 나뉘어 사용됩니다.
- 연속 할당 (Contiguous Allocation):
- 각 프로세스가 메모리의 연속적인(Contiguous) 하나의 공간 덩어리에 적재되는 방식입니다.
- 고정 분할 (Fixed Partition) 방식:
- 물리 메모리를 미리 정해진 크기의 여러 고정된 파티션(분할)으로 나눕니다. (파티션 크기는 같을 수도, 다를 수도 있음)
- 각 파티션에는 하나의 프로세스만 적재됩니다.
- 단점: 융통성 부족 (동시 실행 프로세스 수 고정, 최대 프로그램 크기 제한). 내부 단편화(Internal Fragmentation) 발생 (할당된 파티션 크기 > 프로세스 실제 크기일 때 남는 공간 낭비).
- 가변 분할 (Variable Partition) 방식:
- 미리 파티션을 나누지 않고, 프로세스가 요청하는 크기에 맞춰 동적으로 메모리 공간을 할당합니다. 파티션의 크기와 개수가 실행 중에 변합니다.
- 기술적 관리 필요: 운영체제는 현재 할당된 공간과 사용 가능한 공간(Hole) 목록을 관리해야 합니다.
- 외부 단편화(External Fragmentation) 발생: 프로세스들이 메모리에 할당되고 해제되기를 반복하면, 총 가용 메모리는 충분하지만 작은 Hole들이 여러 곳에 흩어져 있어 실제 필요한 연속 공간을 할당하지 못하는 문제 발생.
- 불연속 할당 (Noncontiguous Allocation):
- 하나의 프로세스가 물리 메모리의 여러 분산된 영역에 나뉘어 적재될 수 있는 방식입니다.
- 현대 운영체제가 사용하는 방식이며, 연속 할당의 단편화 문제를 해결하기 위해 등장했습니다.
- 주요 기법:
- 페이징 (Paging): 프로세스의 논리 주소 공간과 물리 메모리를 **동일한 고정 크기(페이지/프레임)**로 나누어 관리. 외부 단편화 해결. (가장 널리 사용됨)
- 세그멘테이션 (Segmentation): 프로세스의 논리 주소 공간을 **의미 단위(코드, 데이터, 스택 등)**의 가변 크기 세그먼트로 나누어 관리.
- 페이지드 세그멘테이션 (Paged Segmentation): 페이징과 세그멘테이션을 결합한 방식.
- 가변 분할 방식에서의 Hole 할당 문제 (Dynamic Storage-Allocation Problem):
- 크기 n의 메모리를 요청하는 프로세스에게 어떤 Hole을 할당해 줄 것인가?
- 최초 적합 (First-fit): 충분한 크기의 Hole 중 가장 먼저 찾은 Hole에 할당. (속도 빠름)
- 최적 적합 (Best-fit): 충분한 크기의 Hole 중 가장 작은 Hole(낭비 최소화)에 할당. (작은 Hole 많이 생성 가능, 탐색 시간 필요)
- 최악 적합 (Worst-fit): 충분한 크기의 Hole 중 가장 큰 Hole에 할당. (큰 Hole 남기려는 시도, 탐색 시간 필요)
- 일반적으로 First-fit과 Best-fit이 Worst-fit보다 속도 및 공간 이용률 면에서 더 나은 것으로 알려져 있습니다.
- 크기 n의 메모리를 요청하는 프로세스에게 어떤 Hole을 할당해 줄 것인가?
- 압축 (Compaction):
- 외부 단편화 문제를 해결하는 방법 중 하나. 흩어져 있는 사용 중인 메모리 영역들을 한쪽으로 모으고, Hole들을 다른 한쪽으로 모아 큰 가용 공간을 만듭니다.
- 단점: 매우 비용이 많이 듭니다. (대량의 메모리 복사 필요) 프로세스 실행 중단 필요. 실행 시간 주소 바인딩이 지원되어야만 가능합니다. (프로세스 위치 변경 가능해야 함)
[Q&A]
- Q: "내부 단편화와 외부 단편화는 어떻게 다른가요?"
- A:
- 내부 단편화 (Internal Fragmentation): 프로세스에게 할당된 메모리 공간 내에서, 프로세스가 실제 사용하는 크기보다 할당된 공간이 더 커서 사용되지 않고 낭비되는 부분을 의미합니다. 주로 고정 분할 방식이나 페이징 시스템에서 발생합니다 (페이지 마지막 부분 낭비).
- 외부 단편화 (External Fragmentation): 전체 가용 메모리 공간은 충분하지만, 그것이 연속적이지 않고 작은 조각(Hole)들로 나뉘어 있어 실제로 요청된 크기의 연속 공간을 할당할 수 없는 상태를 의미합니다. 주로 가변 분할 방식이나 세그멘테이션 시스템에서 발생합니다.
- Q: "압축(Compaction)은 왜 비용이 많이 드나요? 실제로 자주 사용되나요?"
- A: 압축은 메모리 내의 많은 데이터를 물리적으로 다른 위치로 복사하는 작업을 수반합니다. 이는 상당한 CPU 시간과 메모리 버스 대역폭을 소모합니다. 또한, 압축 중에는 관련된 프로세스들의 실행이 중단되어야 할 수도 있습니다. 이런 높은 비용 때문에 실제 범용 운영체제에서는 압축을 자주 사용하지 않습니다. 대신 페이징과 같은 불연속 할당 기법을 통해 외부 단편화 문제를 근본적으로 해결하려고 합니다.
[마무리하며]
이번 글에서는 메모리 관리의 가장 기본적인 개념들을 살펴보았습니다. CPU가 사용하는 논리 주소와 실제 메모리의 물리 주소는 다르며, MMU가 이 둘을 주소 바인딩 시점(주로 실행 시간)에 맞춰 변환해줍니다. 초기 메모리 할당 방식인 연속 할당은 구현이 비교적 간단하지만 단편화라는 심각한 문제를 야기했습니다. 특히 외부 단편화는 메모리 낭비의 주범이었죠.
이러한 연속 할당의 한계를 극복하기 위해 등장한 것이 불연속 할당 기법, 그중에서도 페이징(Paging)입니다. 다음 글에서는 현대 운영체제 메모리 관리의 핵심인 페이징 시스템에 대해 자세히 알아보겠습니다!
'운영체제 > 반효경 교수' 카테고리의 다른 글
운영체제: 메모리 관리 ③ - 페이징 심화 및 세그멘테이션 (0) | 2025.04.14 |
---|---|
운영체제: 메모리 관리 ② - 페이징 (Paging) 기초 (0) | 2025.04.14 |
운영체제: 교착 상태 (Deadlock) - 원인과 해결 전략 (0) | 2025.04.13 |
운영체제: 프로세스 동기화 - 모니터 (Monitor)를 이용한 병행 제어 (0) | 2025.04.13 |
운영체제: 프로세스 동기화 - 고전적인 동기화 문제들 (세마포어 활용) (0) | 2025.04.12 |