메모리 가상화란?
- os가 물리적 메모리를 가상화하는 것.
- os가 각 프로세스마다 가상 메모리 공간을 부여함
- 각 프로세스는 전체 메모리를 쓰는 것처럼 보이게 된다!
(메모리는 byte의 배열!)

메모리 가상화의 목표:
- 투명성: 프로그램이 메모리 공유를 신경쓰지 않게함
- 효율성: 파편화를 최소화하고 하드웨어의 도움을 받아 속도를 높임
- 보호: 다른 프로세스의 오류로부터 OS과 프로세스들을 보호(서로를 고립시킴)

초창기 시스템은 한 번에 하나의 프로세스만 실행해서 효율성이 떨어졌다.
-> 이 후 여러 프로세스를 동시에 올리는 '멀티프로그래밍'이 도입되었으나 한 프로세스가 다른 프로세스의 메모리에 잘못 접근하는 치명적인 보호(Protection) 문제가 발생하게 된다.

모든 프로세스는 가상 메모리(주소 공간)을 제공받게 된다. (주소 공간에서 0KB에서 시작한다고 실제 메모리에서 그런게 아님)
주소 공간은 Code(명령어 존재), Heap(동적 메모리 할당되는 곳), Stack(반환 주소나 값, 지역 변수, 매개변수 등이 존재) 등으로 이루어져있다.
OS가 뒤에서 가상 주소를 진짜 물리 주소로 몰래 변환하고 있다(Address Translation Mechanism)


각 주소를 출력해서 나오는 것들은 주소 공간(가상 메모리 주소)이다.
[Address Translation]
(Memory Virtualizing with Efficiency and Control): 메모리 가상화는 효율성과 제어를 위해 '제한적 직접 실행(LDE, Limited Direct Execution)' 전략을 취합니다. 이를 달성하기 위해 레지스터, TLB, 페이지 테이블과 같은 하드웨어의 지원을 필수적으로 받습니다.
하드웨어가 가상 주소를 실제 정보가 담긴 물리 주소로 변환한다. 운영체제(OS)는 이 하드웨어를 설정하고 빈 메모리 공간을 추적 및 관리 하기 위해 개입해야 합니다. 이 때 모든 주소 공간이 연속적으로 배치되고, 물리 메모리보다 주소 공간이 작으며, 각 주소 공간은 같은 크기라는 가정을 둔다.
c언어에서 x = x + 3; 이라는 단순한 코드가 어셈블리어로 다음과 같이 실행된다.

메모리에서 값을 불러오고 (Load), 더하고 (Add), 다시 메모리에 저장(Store)하는 과정을 거친다.


Relocation Address Space: 프로그램의 가상 주소 공간은 항상 0번지부터 시작하지만, OS는 실제 물리 메모리의 0번지가 아닌 다른 곳(예: 32KB 위치)에 프로세스를 배치(Relocation)하려고 합니다.
static vs dynamic

Static Relocation : 초기의 소프트웨어 기반 접근 방식. OS가 프로그램을 메모리에 적재하기 전에 모든 메모리 주소를 일일이 수정합니다. 하드웨어 지원이 필요 없다는 장점이 있으나, 보호 기능이나 프라이버시가 없고 한 번 배치된 메모리를 옮길 수 없다는 치명적인 단점이 있습니다.
Dynamic Relocation: 하드웨어 기반의 재배치 방식이다. MMU(Memory Management Unit)라는 하드웨어가 매번 메모리를 참조할 때마다 동적으로 주소 변환을 수행하며, 유효하지 않은 주소 접근 시 예외(Exception)를 발생시켜 보호 기능을 강제합니다.
OS는 MMU에게 현재 프로세스의 유효한 주소 공간에 대한 정보를 넘긴다.

Base and Bounds Register: MMU는 'Base'와 'Bounds'라는 두 개의 레지스터를 사용합니다. 실제 물리 주소는 가상 주소 + Base로 계산되며, 가상 주소는 0보다 크거나 같고 Bounds보다는 작아야 한다는 규칙 (0 <= virtual address < bounds)으로 보호를 수행합니다.

물리 주소 = 가상 주소 + base
킬로바이트(KB) -> 바이트 단위 변환: 1KB = 1024Byte로 32*1024=32768Byte이다. 따라서 위 그림 같은 식이 나옵니다.
두 레지스터 모두 물리 메모리나 가상 주소 공간에 있는 것이 아니라, CPU 내부의 하드웨어(MMU, Memory Management Unit) 안에 위치해 있습니다.
- Base Register (베이스 레지스터): 이 프로세스가 물리 메모리의 어느 위치(예: 32KB)부터 적재되어 있는가(시작 주소)를 저장합니다. 그래서 물리 메모리의 시작점을 가리키는 화살표가 있는 것입니다.
- Bounds Register (바운즈 레지스터): 이 프로세스의 논리적인 가상 주소 공간의 최대 크기(예: 16KB)가 얼마인가를 저장합니다. 그래서 가상 주소 공간의 크기 제한선(끝)을 가리키고 있는 것입니다.
CPU 내부(MMU)에는 Base 레지스터와 Bounds 레지스터가 딱 한 쌍만 존재합니다. (또는 아주 소수만 존재합니다.) 따라서 모든 프로세스의 주소를 동시에 하드웨어 레지스터에 저장해 둘 수는 없습니다.
CPU가 메모리를 읽거나 쓰려고 할 때, CPU 내부에 있는 MMU는 이 두 레지스터를 다음과 같이 활용합니다.
- 보호(Protection): 프로그램이 요청한 가상 주소가 Bounds(예: 16KB)보다 작은지 확인합니다 (0 <= virtual_address < bounds). 주소 공간을 벗어난 요청이라면 에러(Exception)를 냅니다.
- 변환(Translation): 안전하다고 판단되면, 가상 주소에 Base(예: 32KB) 값을 더해서 진짜 물리 주소를 계산(physical address = virtual address + base)하여 물리 메모리로 찾아갑니다.
Page 25. Two ways of Bounds Register: Bounds 레지스터를 구현하는 두 가지 방식을 설명합니다. 주소 공간의 '크기' 자체(예: 16KB)를 저장하거나, 주소 공간이 끝나는 실제 '물리 주소'(예: 48KB)를 저장할 수 있습니다.

Base and Bounds 방식을 위해 OS가 개입해야 하는 3가지 핵심 시점을 설명합니다.

프로세스 시작 시: Free list를 조회하여 물리 메모리에서 새로운 주소 공간이 들어갈 빈자리를 찾습니다.\
buddy 개념으로 동일 간격? 같은 메모리 단위만큼 간격임

프로세스 종료 시: 사용하던 메모리를 다시 Free list로 반환하여 다른 프로세스가 쓸 수 있게 합니다.

문맥 교환 시: CPU를 내어줄 때 현재 프로세스의 Base와 Bounds 값을 PCB(Process Control Block)에 저장하고, 새로 실행될 프로세스의 값을 레지스터에 복원합니다. (궁금증에 대한 답임)
궁금증 : 그럼 Base와 Bound 레지스터에 프로세스마다의 base, bound 주소가 저장된게 아니라면 어떻게하는거지?
답>>>>>>>>>
핵심은 PCB(Process Control Block)와 문맥 교환(Context Switch)입니다.
- 평소에는 어디에 있나요? (저장소) 프로세스가 실행 중이 아닐 때, 해당 프로세스만의 고유한 Base와 Bounds 값은 메모리에 있는 **PCB(Process Control Block)**라는 구조체(소프트웨어적인 데이터 구조) 안에 안전하게 저장되어 있습니다.
- 실행될 때 어떻게 되나요? (문맥 교환 시나리오) 만약 프로세스 A에서 프로세스 B로 넘어가는 상황이라면 다음과 같이 동작합니다.
- 현재 A 실행 중: CPU의 Base/Bounds 레지스터에는 현재 실행 중인 프로세스 A의 값(예: Base 32KB)이 들어있습니다.
- A의 정보 저장(Save): 운영체제가 개입하여 프로세스 A를 멈춥니다. 그리고 현재 CPU 레지스터에 있는 A의 Base와 Bounds 값을 뽑아서 프로세스 A의 PCB에 안전하게 적어둡니다(Save).
- B의 정보 복원(Restore): 운영체제는 메모리에 잠들어 있던 프로세스 B의 PCB를 찾아가서, 예전에 저장해 두었던 B만의 Base와 Bounds 값을 읽어옵니다.
- 레지스터 세팅: 읽어온 B의 값들을 텅 빈 CPU의 Base/Bounds 레지스터에 덮어씌웁니다(Restore).
- B 실행 시작: 이제 CPU(MMU)는 새롭게 세팅된 레지스터 값을 바탕으로 프로세스 B의 주소 공간을 완벽하게 주소 변환하고 보호하며 실행합니다.
[Segmentation]
세그먼테이션(Segmentation)의 등장 (Pages 30 - 37)
Pages 30-32 (Inefficiency of Base and Bound / Segmentation): 기존 Base/Bounds 방식은 힙과 스택 사이에 막대한 '사용하지 않는 빈 공간(free space)'까지 물리 메모리에 통째로 올려야 하므로 공간 낭비가 심했습니다. 이를 해결하기 위해 주소 공간을 Code, Heap, Stack과 같은 논리적 단위인 '세그먼트(Segment)'로 분할합니다. (사용하든 안하든 16KB만큼 차지..)

Pages 33 (Placing Segment In Physical Memory): 각 세그먼트는 물리 메모리의 각기 다른 위치에 독립적으로 배치될 수 있으며, 각각 고유한 Base와 Size(Bounds) 레지스터 쌍을 가집니다.(레지스터 더 필요하다..)
물리 메모리에 올라갈 때 전과 다르게 필요한 애들, 잘라 애들만 할당해서 올림. -> 메모리를 아낀다.
물리메모리에 올라갈 때 순서 상관없이 올라감.
오프셋(Offset)은 가상 주소에서 해당 세그먼트의 시작 주소를 빼서 구합니다.
1. 오프셋(Offset)의 개념
세그먼테이션에서 오프셋은 **'해당 세그먼트의 시작점으로부터 얼마나 떨어져 있는가(거리)'**를 의미합니다. 주소 공간이 Code, Heap, Stack으로 쪼개졌기 때문에, 전체 가상 주소의 0번지부터 맹목적으로 계산하는 것이 아니라 각 조각(세그먼트)의 시작점을 기준으로 상대적인 위치를 다시 재는 것입니다.
예시 시나리오: 힙(Heap) 영역 내부의 특정 데이터에 접근하려고 할 때
- CPU가 프로그램 실행 중 가상 주소 5KB 위치에 있는 데이터를 읽으려고 합니다.
- 이 주소(5KB)는 4KB와 6KB 사이에 있으므로 Heap 세그먼트에 속한다는 것을 파악합니다.
- 오프셋 공식에 대입합니다: offset = 5KB (가상 주소) - 4KB (Heap의 가상 시작 주소)
- 계산 결과, 오프셋은 1KB가 됩니다. 즉, 이 데이터는 힙 세그먼트의 맨 앞 대문에서부터 딱 1KB만큼 떨어져 있다는 뜻입니다. (가상 메모리에서 시작점과 차이나는 간격만큼 실제 물리 메모리에서도 그 간격만큼 시작점과 떨어져있음)
물리 주소(Physical Address)로의 최종 변환]
오프셋을 구하는 궁극적인 목적은 진짜 물리 메모리의 위치를 찾기 위함입니다. 33페이지의 첫 번째 공식이 이를 보여줍니다.
- physical address = offset + base
- 방금 구한 오프셋(1KB)에 33페이지 우측 중간 표에 명시된 힙의 Base 주소(34KB)를 더해줍니다.
- 최종 물리 주소 계산: 1KB (오프셋) + 34KB (Base) = 35KB
결과적으로 오프셋을 구하는 과정은 거대한 가상 주소 덩어리를 뚝 떼어낸 뒤, **"이 세그먼트 덩어리 안에서 정확히 몇 걸음이나 들어가야 데이터가 나오는가?"**를 계산하는 핵심 단계입니다.


Pages 34-37 (Address Translation & Segmentation Fault): 세그먼테이션 환경에서의 주소 변환 예시입니다. 가상 주소에 해당하는 세그먼트의 오프셋을 구한 뒤 Base 값을 더해 물리 주소를 구합니다. 만약 오프셋이 세그먼트의 Size(Bounds)를 넘어서는 잘못된 접근(예: 7KB 참조)을 시도하면 하드웨어가 'Segmentation Fault'를 발생시켜 차단합니다.


Pages 38-39 (Referring to Segment): 가상 주소가 주어졌을 때 이것이 Code인지, Heap인지, Stack인지 하드웨어는 어떻게 알 수 있을까요? 가상 주소의 최상위 몇 비트(Top few bits)를 '세그먼트 식별자'로 사용하여 명시적으로 구분합니다 (예: 00=Code, 01=Heap, 11=Stack).


offset은 맨 앞 두 비트 이후인 비트들이 의미한다. 2^10=1KB이므로 위 사진은 offset이 3KB임.
1. 하드웨어의 방향성 체크 (Grows Positive?)
교안 40페이지를 보면 세그먼트 레지스터에 **'Grows Positive?'**라는 비트가 추가된 것을 볼 수 있습니다.
- Code와 Heap은 위로 자라므로 1 (Positive)
- Stack은 아래로 자라므로 0 (Negative)
하드웨어(MMU)는 이 비트가 0인 것을 보고, 해당 세그먼트는 오프셋을 음수로 변환하여 접근해야 한다는 것을 파악합니다.
2. 음수 오프셋(Negative Offset) 계산법 (교안 41페이지 기준)
단순히 가상 주소에서 구한 오프셋에 마이너스 기호만 붙이는 것이 아니라, 최대 세그먼트 크기를 활용한 계산 공식을 거칩니다.
[상황] 가상 주소 15KB에 접근하려고 할 때
- 가상 오프셋 추출: 14비트 가상 주소 체계에서 상위 2비트는 세그먼트 구분용(Stack=11), 하위 12비트가 오프셋입니다. 15KB의 가상 주소에서 오프셋 부분을 추출하면 3KB가 나옵니다.
- 최대 세그먼트 크기 확인: 하위 12비트로 표현할 수 있는 주소 공간의 최대 크기는 $2^{12}$ 바이트, 즉 4KB입니다.
- 음수 오프셋 계산: 가상 오프셋 - 최대 세그먼트 크기 공식을 사용합니다.
- 3KB - 4KB = -1KB
Pages 40-41 (Referring to Stack Segment): 스택은 다른 세그먼트와 달리 주소가 줄어드는 방향(아래로, Backward)으로 성장합니다. 이를 처리하기 위해 하드웨어는 해당 세그먼트가 양수 방향으로 자라는지 여부(Grows Positive? 1 or 0)를 체크하는 추가적인 비트를 지원해야 합니다.


Pages 46-48 (Pros & Cons): 세그먼테이션은 내부 단편화가 없고 메모리를 효율적으로 쓰며 공유가 쉽다는 장점이 있지만 , 외부 단편화가 발생하며 거대하고 듬성듬성 사용되는 힙을 처리하기에는 여전히 유연성이 부족하다는 한계를 명시하며 챕터가 마무리됩니다.
추가 정리
1. 내부 파편화 (Internal Fragmentation)
내부 파편화란, 운영체제가 어떤 프로세스에게 메모리를 할당해주었을 때 '할당된 영역 안(내부)에' 남아서 버려지는 잉여 공간을 뜻합니다.
- Base and Bound 방식의 한계: 교안 31페이지를 보면 단일 Base/Bound 레지스터를 쓸 때 주소 공간(Code, Heap, Stack) 전체를 하나의 거대한 덩어리로 물리 메모리에 올립니다. 이때 힙(Heap)과 스택(Stack) 사이에는 아직 쓰지 않는 거대한 '빈 공간(free space)'이 존재하게 됩니다. OS가 이 거대한 덩어리 전체를 한 프로세스에게 줘버렸기 때문에, 이 빈 공간은 다른 프로세스가 쓸 수 없는 낭비되는 공간이 됩니다. 이것이 바로 내부 파편화입니다.
- 세그먼테이션의 해결: 세그먼테이션은 딱 필요한 조각(Code, Heap, Stack)만 가위로 오려내듯 물리 메모리에 올립니다. 따라서 세그먼트 내부에는 버려지는 공간이 없으므로 **"내부 파편화가 발생하지 않는다(No internal fragmentation)"**는 엄청난 장점을 갖게 됩니다.
2.Coarse-Grained vs. Fine-Grained (교안 43페이지)
세그먼테이션을 적용할 때 주소 공간을 **'얼마나 잘게 쪼갤 것인가'**에 대한 두 가지 접근 방식입니다.
① Coarse-Grained (큰 단위 분할 / 굵은 입자)
- 개념: 주소 공간을 **아주 적은 개수(a small number)**의 큼직한 세그먼트로 큼직큼직하게 나누는 방식입니다.
- 예시: 우리가 교안 내내 살펴보았던 Code, Heap, Stack 딱 3가지로만 나누는 것이 전형적인 Coarse-Grained 방식입니다.
- 특징: 세그먼트 개수가 적기 때문에 CPU 안에 들어있는 레지스터 몇 쌍(Base/Bounds 레지스터 3쌍 정도)만으로도 충분히 관리가 가능합니다.
② Fine-Grained (작은 단위 분할 / 미세한 입자)
- 개념: 주소 공간을 매우 많은 수의 자잘한 세그먼트로 잘게 쪼개는 방식입니다.
- 특징: 메모리를 아주 잘게 나누기 때문에 물리 메모리의 자잘한 틈새에도 프로세스를 끼워 넣을 수 있어 **훨씬 더 뛰어난 유연성(more flexibility)**을 제공합니다.
- 하드웨어적 한계와 극복: 하지만 세그먼트가 수십~수백 개로 늘어나면 CPU 안에 그만큼 많은 레지스터를 만들어 넣을 수가 없습니다. 따라서 이를 지원하기 위해서는 메모리 상에 세그먼트들의 위치를 쭉 적어놓은 장부, 즉 세그먼트 테이블(Segment Table)을 지원하는 추가적인 하드웨어 구조가 필수적으로 요구됩니다.
결과적으로 세그먼테이션은 내부 파편화를 없애주었지만, 메모리를 이리저리 쪼개어 쓰다 보니 물리 메모리 바깥쪽(세그먼트들 사이)에 자잘한 틈이 생기는 **외부 파편화(External Fragmentation)**라는 새로운 골칫거리를 낳게 된 것입니다.
'CS > OS' 카테고리의 다른 글
| 운영체제(OS)-WEEK6 (2) Disadvantage of Linear Tables, Hybrid Approach, Multi-Level Page tables (0) | 2026.04.09 |
|---|---|
| 운영체제(OS)-WEEK5 (2) [페이징(Paging)]+WEEK6 (1) (0) | 2026.04.03 |
| 운영체제(OS)-WEEK3(2) MLFQ 시뮬레이터 구현하기 (with C) (0) | 2026.03.26 |
| 운영체제(OS)-WEEK3(1) 스케줄링(Scheduling) (0) | 2026.03.17 |
| 운영체제(OS)-week2 (0) | 2026.03.17 |