지난 시간까지는 Logical Space에 있는 Free Region을 어떻게 할당하고 관리할 것인가 대해 알아보았다. 이번엔 OS가 Process의 Memory Loading을 어떻게 하는지 알아보자. Process를 Memory에 Load할 때 어떠한 일들이 일어나는지를 알아볼 것이다.
Memory Virtualization
각 Process가 자신의 Memory Space를 각각 가지는 것과 같은 Illusion을 제공
이러한 Memory Virtualization을 통해 우리는 다음과 같은 이득을 얻는다.
~> 이러한 가상화의 제공을 위해, Process Loading 과정에선 다음과 같은 고민이 필요하다.
"어디다 Load할 것인가?"
"어떻게 Mapping할 것인가?"
=> 이제 이들에 대해 알아보자. (사실 앞전에 이미 Paging 기법을 소개한 바 있지만, 우선 그에 앞서 개발되었던 Old-Fashioned Approaches에 대해 알아볼 것임. Paging은 추후 자세히 다룰 것!)
우리는 본 포스팅의 학습 간에 다음과 같은 가정을 적용한다. (학습 편의성 목적)
Virtual Address Space가 Physical Memory보다 사이즈가 작다.
하나의 Virtual Address Space는 Physical Memory에 연속적인 형태로 Load된다.
각 Virtual Address Space의 크기는 동일하다.
위와 같은 가정을 고려한채, 아래의 간단한 예시 프로그램을 보자.
int main(int argc, char *argv[]) {
int a;
a = a + 4;
}
~> 이 Program을 Linux에서 'objdump'라는 Command를 통해 Assembly Code로 변환하면 다음과 같은 명령부가 포함된다. ex) "objdump -d simple(process_binary)"
0x20: movl 0x8(%rbp), %edi # rbp에 8을 더해서 edi로
0x23: addl $0x4, %edi # edi에 4를 더하고
0x29: movl %edi, 0x8(%rbp) # edi 값을 rbp에 8을 더한 위치에!
# 이때, 0x20, 0x23 이런건 모두 Commands의 Logical 주소임.
# 참고로, 0x8(%rbp)인 이유는, argc, argv가 a보다 먼저 할당
# 되었음을 의미함. ASM의 논리!
%rbp Register : 현재 Stack Frame의 Base를 가리키는 Base Pointer를 담는다.
%rip Register : Instruction Pointer Register이다.
%rip = 0x20, %rbp = 0x300이라고 가정하자. (Commands는 Code Segment이므로 낮은 주소 부근에 있고, Stack Frame은 Stack Segment이므로 높은 주소 부근에 있음)
1) 0x20 위치의 Instruction을 Fetch
2) 0x23 위치의 Instruction을 Fetch
3) 0x29 위치의 Instruction을 Fetch
~> Assembly Programming에 대해 배웠다면 매우 간단히 이해할 수 있을 것이다.
=> 여기서 우리의 의문은 다음과 같은 질문이다.
Assembly Level에서 Address는 Process Binary 내에서 Hardcoded되어 있다. 이게 무슨 말이냐면, 쉽게 말해 고정되어 있다는 것이다. 100개의 Process로 Simple Program을 각각 돌렸을 때, 매번 'objdump' 시 위와 같은 주소가 나온다는 것이다.
이때, Multiple Processes가 동시에 돌아가면, 모두 똑같은 주소를 참조하고 있는데 Collision이 어떻게 안일어날까?
도대체 Process Loading 시 어떠한 Memory Virtualization이 일어나는 것인가?
Time-Sharing, Static / Dynamic Relocation 등의 Mechanism을 통해 Collision을 회피하며 Process Loading을 수행한다. ★★★
~> 이제, 이들에 대해 하나 하나 알아보자.
OS가 CPU Virtualization을 제공하는 것과 관점이 다른 이야기다. 허나, 관점은 다르지만, 결국 원리는 같다.
Time-Sharing (Memory)
현재 돌고 있지 않은 Process의 Memory를 Disk에 저장하고, 다시 돌아갈 때 Main Memory로 Restore한다.
허나, Poor Performance가 자명하다.
개선책으로, Memory Space를 동시간대에 Process별로 분리해서 할당하는 방법도 존재한다.
Time-Sharing Method : Memory를 Time-Sharing마다 Swap하는 것 ★
Obviously, this method will have great overheads.
Static Relocation : Process가 올라갈 Physical Memory를 Static하게 지정하는 방식이다.
OS는 특정 Program을 Memory로 올려 Process를 만들 때, 해당 Program에 대해 Rewrite한다. (ELF를 VAS꼴로 Main Memory에 올리되, Address를 다 바꾼다는 것임)
Rewrite는 각각의 Process에 대해 서로 다른 Address와 Pointer를 부여한다. ★★★
당연히, 명확한 단점이 있다.
Process Virtual Address Space 간에 Protection이 존재하지 않는다.
또한, Process의 VAS가 Main Memory에 지정되고 나서는, 다른 위치로 이동할 수 없다.
Dynamic Relocation : Virtual Address를 항상 '특정 번지로부터 얼마만큼'으로 보자!
Dynamic Relocation
MMU는 BASE Register와 BOUNDS Register를 통해 Process 간의 Protection을 제공한다.
~> MMU는 Logical Address를 Physical Address로 변환하는데, MMU에 Logical Address가 들어오면, MMU는 그것을 BASE Register Value, BOUNDS Register Value를 이용해 Physical Address로 바꾸는 것이다. ★
이러한 MMU에는 Mode Bit도 존재한다. Mode Bit는 알다시피 Dual Mode Operations 개념에서의 두 Mode 지정을 가능케하는 Bit이다.
Privileged(Protected/Kernel) Mode : OS가 수행될 때의 Mode
User Mode : User Process가 수행될 때의 Mode
이제부터 Dynamic Relocation 상황에서 MMU가 어떻게 Logical Address를 Physical Address로 변환하는지 그 원리에 대해 알아보자. 이를 통칭 'Address Translation'이라 한다.
User Process의 '모든 Memory Access'에 대해 Address Translation이 수행된다.
Address Translation
1) MMU는 넘겨받은 Logical Address를 BOUNDS Register Value와 비교한다. ★
2) MMU는 1)에서 Validation한 Logical Address에 대해, BASE Register Value를 더해 Physical Address를 뽑아낸다. ★
Kernel Mode에선 사실상 MMU가 아무런 역할을 하지 않는다는 것을 알 수 있다. (오로지 Mode Bit Check만 수행한다. ★)
BASE와 BOUNDS Register는 모두 32Bits = 1Byte 크기이고, Mode Bit는 1Bit이다.
이러한 Dynamic Relocation을 실제로 구현하기 위해선 BASE Register Value와 BOUNDS Register Value를 각 Process마다 기억하는 루틴이 필요하다. ★
Process의 PCB에 넣어서 기억할 수 있다. ★★★
Process Context Switch가 일어나면,
이때, User Process는 자신의 BASE와 BOUNDS Register Value를 바꿀 수 없어야한다. ★
ex) 만약, ProcessP1이 있고, P1의 BASE와 BOUNDS 값이 1024라 하자. Sequence of Instructions가 아래와 같다면, 무슨 일이 일어날까?
P1: load 123, R1
P1: load 124, R1
P1: load 1023, R1
P1: load 512, R1
P1: load 2000, R1 -> Segmentation Fault!!!
~> 2000이라는 Address는 BOUNDS 값 이상이기 때문에 Error를 맞이하고 있음을 알 수 있다.
~~> 따라서, ProcessP1은 OS에 의해 Kill될 것이다.
Q) Dynamic Relocation에서 특정 Process가 다른 Process를 침범할 수 있을까요?
A) 없다. 왜냐? MMU 선에서 이를 처리해주기 때문!
Dynamic Relocation provides 'Protection among processes(includes OS)!'
자, 이제 우리가 포스팅 초반에 했던 가정을 하나 풀어보자. 각 Process의 Address Space Size가 서로 다를 수 있다고 하자.
Dynamic Relocation에선 'Hole'이라는 개념이 존재한다.
Hole : Available(Free) Memory의 Block
서로 다른 Size의 여러 프로세스가 할당되고 해제되고를 반복하면, Main Memory 전반에 걸쳐서 Hole이 여러군데 존재하게 된다.
~> 그리고, 당연히 특정 Process를 Hole에 넣지 못하는 상황도 발생할 수 있다. (External Fragmentation)
즉, OS에선 이러한 Free Hole들의 정보를 담고 있는 List를 운용해야한다.
Free Holes List에서 특정 Process Loading에 대해 적절한 Hole을 찾기 위한 방법은 무엇이 있을까?
Dynamic Memory Allocation 시 Free Space Search 논리와 유사하다.
First-Fit Approach : 처음으로 발견되는 'Cover할 수 있는 Hole'을 선택한다.
Best-Fit Approach : Cover할 수 있는 Hole 중 Size가 가장 작은 Hole을 선택한다.
Worst-Fit Approach : Largest Hole을 선택한다.
~> First-Fit과 Best-Fit이 성능, 공간 효율 측면에서 가장 우수하다고 알려진다.
※ 50% Rule : 일반적으로 N개의 Allocated Block이 있으면, 그 중 절반의 Block이 Fragmentation에 의해 Lost된다. (1/3은 Unusable)
Free Space Management에서도 언급했던 Fragmentation이다. 다시 한 번 짚고 넘어가보자. Dynamic Relocation에서도 중요하기 때문이다.
External Fragmentation : 전체 Memory Space는 Request를 충족할 수 있지만, 개별 Space들은 그렇지 못한 상황
Internal Fragmentation : Request에 대해 Allocate한 Memory Space의 Size가 실제 Requested Size보다 큰 문제이다. 즉, Memory Internal이 필요한 문제이다.
Dynamic Relocation에서 Hole의 존재는 곧 Great External Fragmentation을 의미한다.
Dynamic Relocation 시 External Fragmentation을 줄이는 방법
Compaction이 대표적이다.
Compaction을 위해선 Allocated Memory가 왔다갔다 할 수 있어야하고, 따라서 Dynamic Relocation 같은 방법론이 전제되어야 한다. (Process Loading에 적용한다고 했을 때)
허나, Overhead가 극심할 것은 자명하다.
또한, I/O Operation 수행 시 사용되는 Buffer도 Compaction할 때마다 매번 옮겨야한다는 문제가 존재한다. ★
Latch Process : 고정된 메모리 공간에서 일을 해야하는 프로세스 (주로 I/O 때문)
물론, '고정 Buffer(OS 내부에 두는 식의)'를 사용할 수도 있겠지만, 고정 Buffer를 쓰면 Concurrency와 같은 다른 문제를 신경써야한다.
이처럼 Dynamic Relocation의 여러 성질을 알아보았다. 마지막으로 Dynamic Relocation의 장단점을 확인하자.
장점
Process Virtual Address Space 간에 Protection을 제공한다. by MMU ★
Process를 여러 위치에 둘 수 있고, 동시에 VAS를 옮길수도 있다. (Dynamic의 의미)
Simple하다. 구현이 어렵지 않다. MMU와 약간의 알고리즘만 있으면 된다
(Compaction을 하지 않을 때) 성능이 좋다. 빠르다.
단점
Process가 Heap이나 Stack을 활용하지 않을 수도 있는데, Dynamic Relocation에선 이와 상관없이 반드시 다 할당해야한다. ★★★
Virtual Address Space의 일부분을 공유하는 행위도 불가능하다. ★★
External Fragmentation이 극심해질 수 있다. by Holes
"무언가 좀 더 우수한 Method가 필요하다...!"
지금까지의 학습에 우리는 다음과 같은 가정을 전제한 바 있다.
"Process Virtual Address Space가 Physical Memory에 Contiguous한 형태로 올라간다. 복수의 VAS끼리를 의미하는 것이 아니라, VAS 하나 그 자체가 말이다."
~> 이러한 가정을 토대로 설계한 상기한 Methods들엔 무슨 문제가 있었나?
=> 그것은 바로 External Fragmentation이 커진다는 것이었다. Dynamic Relocation의 경우 Hole의 존재가 바로 그 이유였다.
--> Segmentation은 이에 대한 해법을 제시한다. (Segmentation 자체가 Method 이름)
Segmentation : Process Virtual Address Space는 Code, Data, Heap, Stack 등의 Segment로 나뉘는데, 이때, VAS 단위로 Contiguous하게 올리지 말고, Logical Segment 단위로 Contiguous하게 올려보자! ★
즉, MMU에 Segment마다 BASE, BOUNDS를 두는 것이다! ★
(한 Process의) 각 Segment는,
이러한 Segmentation Method는 기존에 OS가 Process Virtual Address Space를 통째로 연속 형태로 올려서 생겨나던 Hole 문제, Unused Space 문제 등을 해결할 수 있다. ★
그렇다면, 이 Segmentation 방식에선 어떻게 Address Translation을 할까. 여기서 쓰이는 방식을 Segmented Addressing이라 부르는데, 아래와 같다.
Logical Address가 기존에는 말 그대로 단순 Address (with No Format)였던 것에서, 특정 Format을 가지도록 한다.
Logical Address (in Segmentation) : Segment Number + Offset (in Segment)
상위의 몇 비트는 Segment Number를 가리킨다.
하위의 몇 비트는 Offset in Segment를 가리킨다.
그리고, MMU에 각 Process마다 Segment Table을 둔다. ★
ex) 예를 들어, Logical Address가 표현할 수 있는 범위가 16K라 하자.
16K = 2^14
이때, 이 OS에선 VAS를 4 Segments로 나눈다. (Code-Data-Heap-Stack)
Segment Number = 4 = 2^2
그렇다면, 자연스럽게, 나머지 14-2 = 12Bits는 Offset을 나타낼 것이다.
위 그림을 보면 Segmentation Method의 원리를 잘 이해할 수 있을 것이다.
이때, Segment Table에서 각 Segment의 BOUNDS가 최대 0xfff임을 주목하자.
Protection Bits도 주목하자. 각 Segment마다 Protection이 다 다르다. 즉, 독립적이다. 이것 역시 Segmentation의 큰 특징이다.
한편, 이러한 Setting이 MMU에서 각 Process마다 되어야 한다.
ex) 만약, Segment가 8개이고, Segment 크기가 2K라면, Logical Address는 어떻게 표현될까?
~> ㅁㅁ ㅁ/ㅁㅁㅁ ㅁㅁㅁㅁ ㅁㅁㅁㅁ로, 위 예시처럼 14Bits로 구성될 것이다. 따라서, Process Virtual Address Space의 크기는 2^14 = 16K가 되겠다. ★
위와 같은 Segmented Addressing을 C Code로 표현하면 아래와 같다.
// SEG_MASK : 0011 0000 0000 0000 (b) = 0x3000
// SEG_SHIFT : 12
// OFFSET_MASK : 0000 1111 1111 1111 (b) = 0x0FFF
/* 14Bits Logical Address에서 상위 2Bits만을 AND로
살리고, Right Shift해서 Segment Number를 추출하자! */
seg_num = (vaddr & SEG_MASK) >> SEG_SHIFT;
/* 14Bits Logical Address에서 하위 12Bits를 추출! */
offset = vaddr & OFFSET_MASK; // Offset 추출
/* BOUNDS Error Checking */
if (offset >= BOUNDS[seg_num]) Exception(PROTECTION_FAULT);
else {
paddr = BASE[seg_num] + offset; // Physical Address 추출
register = AccessMemory(pddr); // Execution!
}
~> 이런 코드를 MMU에 설치하면 되겠다!
이때, (MMU 내의) 각 Process의 Segment Table에서 BOUNDS 값은 무엇을 의미할까?
BOUNDS Value in Segment Table : The Size of each Segment
ex) 예를 들어, 특정 Segment Table에서 2번 Index Slot이 Stack Segment를 나타내고, 해당 Slot의 BOUNDS가 0x7ff라 하자.
~> Offset이 0x7ff 이상이면 Fault Error가 날 것이다.
=> 그.리.고! 0x7ff는 곧 Stack Segment의 크기를 나타낸다.
--> 0x7ff = 0111 1111 1111 (b)로, 0x0 ~ 0x7ff는 총 11Bits로 표현할 수 있다.
--> 11Bits = 2^11 = 2K
===> 즉, 이 Process에서 Stack Segment는 2K 크기를 가지는 것이다. ★★★
장점
Process Virtual Address Space에 대한 Sparse Allocation이 가능하다. ★★
각 Segment마다 Independent Protection을 할 수 있다. ★★
Segment끼리의 Sharing이 가능하다. ★★
각 Segment의 Dynamic Relocation도 당연히 가능하다. ★★
단점
각 Segment는 반드시 Contiguous Form을 가져야 한다.
그리고, 또 하나 꼽자면, MMU가 복잡해질 것이다.
"무언가.. 좀 더 좋은 방법이 필요하다..."
"이제 진짜.. Paging을 공부할 때가 다가온다..."
금일 포스팅은 여기까지이다. 다음 포스팅부터 Paging을 제대로 알아보자.