CPU Virtualization을 구현하기 위해, 프로세스가 System call을 호출하거나 Timer interrupt로 인해 강제로 OS에 의해 CPU 사용권을 빼앗았을 때 다른 프로세스가 CPU를 사용할 수 있도록 하는 LDE(Limited Direct Execution) 기법을 사용했었다.
CPU를 프로세스가 쓰고 싶은대로 쓰도록 내버려두지 않고 OS가 관리하도록 했던 것처럼, Memory에 대해서도 OS가 이를 효율적이고(efficiency), 통제 가능한 선에서(control), 안전하게(protection) 관리할 수 있도록 여러 가상화 기법을 적용해야 한다.
우리가 물리적인 메모리를 떠올릴 때 흔히 생각하는 어ㅓㅓㅓㅁ청 긴 배열로 메모리를 다룬다고 생각해보자. 생각만 해도 골치 아프지 않은가? 앞서 CPU Virtualization에서도 다룬 Efficiency, Control, Protection 세 가지 이외에도 OS는 메모리를 쉽게 사용할 수 있도록 지원하기 위한 추상화, 그러니까 flexibility 역시 지원해 주어야 한다.
이 네 가지 요소를 잘 지원하기 위해, Hardware-based address translation(=Address translation), 그러니까 주소 변환 기법을 사용할 것이다.
변환이라면 '변환 전'과 '변환 후'가 당연히 있어야 하겠지?
주소 변환에서 변환 전의 주소는 가상 주소(Virtual address), 변환 후의 주소는 실재하는 물리 주소(Physical address)이다. 또, 뒤에서 자세히 배우겠지만 여기서 이 가상 주소를 물리 주소로 변환해 주는 CPU의 구성 요소를 MMU(Memory Management Unit)라 한다. 여기서 요점은 논리적으로(SW) 이 변환 정보가 저장되어 있는게 아니라, HW에 저장해놓은 정보를 바탕으로 변환을 수행하기 때문에 빠르게 수행할 수 있다는 것이라 할 수 있다.
설명을 위해 아래 세 가정을 먼저 해놓고 시작하겠다.
첫 번째 가정이 좀 설명이 부족한 것 같은데, 세 번째 가정을 같이 생각해보면 좋다. 물리 메모리가 빈 틈 없이 같은 크기로 나누어져(?) 있어서, 각각의 프로세스가 그냥 이 덩어리들을 동일한 크기로 할당 받는다고 이해하면 된다!
주소 변환이 왜 필요한지, 변환 과정에 무엇이 필요한지 알아보기 위해 간단한 예제를 먼저 살펴보자.
쉽다! x에 정수값 3000을 넣어주고, 아래서 x에 3을 더해주는 코드이다.
이 코드를 어셈블리어로 변환해주면 아래와 같은 코드가 나온다.
어 음,, 책에는 이렇게 나와 있다.
후! 괜히 쫄았다!
이때 프로세스의 메모리 상태는 위와 같다. 위에서 보았듯 위치 128, 132, 135의 명령어가 이 코드 영역에 포함되어 있고, Heap은,, 위 코드에선 안보이지만 일단 있고, 지역 변수로 선언한 x 값이 Stack 영역에 포함되어 있는 것을 확인할 수 있다.
그런데 이걸 그대-로 물리 메모리의 0KB ~ 16KB에 올려놓는다면 어떤 일이 생길까?
두 프로그램이 동시에 0KB ~ 16KB 영역의 메모리를 점유한다고 작성해두었다면,,? 그냥 메모리를 덮어 쓰던가, 아니면 하나의 프로세스가 점유하고 있던 메모리를 뱉고 나서야 해당 공간을 필요로 하는 프로세스가 실행될 수 있을 것이다. 뭔가 이상하지 않은가?
이런 문제점 때문에 OS는 프로그램이 사용하고 싶어 하는(어셈블리어에 나와 있는) 메모리 주소 그대로 물리 메모리에 올리는게 아니라, 다른 위치에 재배치(Relocation) 해주어야 한다.
위와 같이 말이다! 총 16KB의 주소 공간을 사용하는 것은 같은데, 물리 메모리의 0KB ~ 16KB 영역은 OS가 이미 사용하고 있고, 적당한 위치(16KB ~ 48KB)에 프로세스가 사용할 메모리 공간을 재배치 해주었다. not in use라고 되어 있는 부분은 말 그대로 아직 아무 프로세스도 사용하고 있지 않은 빈 메모리 공간이고.
아까 CPU에 포함된 장치인 MMU(Memory Management Unit)를 통해 가상 주소를 물리 주소로 변환한다고 배웠다. 그렇기 때문에 Hardware-based Relocation이라고 배웠는데, 왜 소제목에 Dynamic Relocation이라고 설명되어 있는 걸까? 가장 간단한 Base and Bound 기법부터 살펴보며 생각해보자.
Base and Bound 기법을 구현하려면 MMU에 base register와 bound register, 두 개의 레지스터가 추가로 필요하다.
이름에서 알 수 있듯 기준점(base)과 경계(bound) 정보를 사용해 물리 메모리 위의 위치를 표현하는 것인데, 이 기법을 사용하면 위의 0KB ~ 16KB 예제처럼 모든 프로그램들이 일괄적으로 0KB부터 메모리를 사용하는 것처럼 작성해도 상관이 없어진다.
대신 프로그램이 메모리 위에 프로세스로서 생성될 때, 해당 프로세스가 사용할 물리 메모리 공간의 시작 주소를 base register에 저장해 놓아야 한다. 슬슬 감이 올 것 같은데 아래와 같이 가상 주소를 물리 주소로 변환하게 되는 것이다.
프로그램 안에 명시된 메모리 주소에 base만큼 더해주기만 하면 주소 변환 끝!
위에서 살펴본 예제를 기준으로 생각해보면 프로그램엔 0KB ~ 16KB 구간의 메모리를 사용하는 것처럼 되어 있지만, 실제로 프로세스가 될 때에는 base가 32KB로 지정되어 물리 메모리 위에서는 (0+16)KB ~ (16+32)KB 구간을 점유하게 된 상황임을 알 수 있다.
이제 어떤 부분이 Dynamic 하다는 건지 대답할 수 있게 되었다.
우선 프로그램이 프로세스로서 실행되는 시점에 주소 변환이 발생하기 때문이며, 또한 같은 원리로 프로세스가 실행되고 있는 중에도 주소 공간을 물리 메모리의 다른 위치로 옮길 수 있기 때문에 Dynamic Relocation이라 부르는 것이다! 와!
..
가만,, 근데 Base and Bound 기법이라고 하지 않았나? Bound는 아직 안나왔는데?
이것도 좀 생각해보면 답이 나오는데, 위에서 살펴본 프로세스 하나가 올라가 있는 메모리 상태에서 만약 다른 프로세스가 Base를 16KB ~ 32KB 구간 사이의 30KB쯤으로 잡아버리면? 30KB ~ 32KB 구간의 정보는 날아가야 하나?
물론 그렇지 않다. 결론부터 말하자면 위 예제에서는 Base가 16KB, Bound가 16KB로 설정되어 있기 때문에 이 영역을 침범하는 Dynamic realocation이 발생할 경우 Out of bounds 예외가 발생해 전에 배운 Trap handler가 이를 처리하게 될 것이다. 일종의 Protection인거지!
예제 하나 더 살펴보자.
모든 프로세스의 Bound가 16KB라고 가정하면, 위 예제에서는 0~16KB, 1KB~17KB, 3000~19384(=3000 + 16KB) 영역에 프로세스가 메모리를 점유하고 있는 상황이라 할 수 있다.
이때! 어떤 프로세스가 4400을 Base로 잡으면 메모리의 끝 경계가 4400 + 16KB = 20784로 4400~20784 영역을 점유해야 하는데, 물리 메모리의 4400 ~ 19384에 해당하는 영역이 이미 점유되어 있는 상황이기 때문에 Out of bounds 예외가 발생하게 될 것이다. (나는 이편이 더 직관적인 것 같기는 한데, 프로세스가 점유할 물리 메모리의 시작 주소와 끝 주소를 저장해 놓아도 논리적으로 같은 작업을 수행할 수 있다. 책에서는 이 Base & Bound 방법을 사용할 것!)
이 Base & Bound 기법을 사용하기 위해 어떤 HW가 필요할지, HW는 어떤 기능을 지원해야 할지 생각해보자.
Priviliged mode
전에 살짝 다뤘듯이, CPU든 메모리든 프로세스가 달라는대로 막 제한 없이 퍼주면 곤란하다. 그렇기 때문에 CPU Virtualization에서와 마찬가지로 User mode와 Kernel mode를 분리해서, 메모리와 관련된 민감한 작업들을 LDE 방식으로 처리할 수 있어야 한다.
Process status word (PSW)
위의 표에는 나와 있지 않지만 좀 자잘하게 짚고 넘어가자면, 지금 실행할 수 있는 명령을 제한하기 위한 mode 상태를 기록하기 위해 따로 레지스터의 한 비트를 사용해야 하는데, 이걸 Process status word라고 부른다고 한다. 그러니까, 모드의 전환은 이 비트의 전환이라는 거지.
Base/Bounds registers
위에서 얘기했듯이 MMU에 프로세스의 Base, Bound 값을 기록하고 있어야 주소 변환이 가능하다.
Ability to translate virtual address
주소 변환이니까!
..and check if within bounds
Memory Protection을 위해 지원되어야 한다.
Priviliged instruction(s) to update base/bounds
만약 base를 프로세스가 user mode에서 멋대로 변경할 수 있다면 프로세스가 시작될 때 남의 메모리를 침범할 수 있는 것은 물론 Dynamic relocation이기 때문에 심지어 실행하고 있는 도중에도 막 물리 메모리의 이곳 저곳을 누비고 다닐 수 있게 된다. bound 역시 말할 것도 없다!
Priviliged instruction(s) to register exception handlers
이것도 좀 자세하게 들어가는 것 같은데, HW에 Exception handler 코드의 위치를 알려 주는 것 역시 아무나 접근할 수 있어서는 안되기 때문에 Kernel mode로만 수행할 수 있어야 한다.
Ability to raise exceptions
7번 항목에서 handler는 준비했는데, 정작 위의 Out of bounds와 같은 예외를 발생시킬 수 없으면 안되겠지? 아마 메모리 침범을 시도했기 때문에 OS가 그냥 프로세스를 종료시켜버릴 확률이 높다고 한다.
말고도 이러한 작업을 수행할 수 있으려면, MMU에 단순히 base, bound 레지스터만 추가할게 아니라 bound를 활용한 물리 메모리에서의 시작 주소 계산과 메모리 끝 영역을 계산에 필요한 덧셈 회로, 그리고 이미 사용 중인 물리 메모리 영역을 침범하지 않는지 검사하기 위한 비교 회로도 필요할 것이다. 이게 MMU라는 HW에 포함되어 있기 때문에 빠르게 처리가 가능한 것을 이해하고 넘어가자!
ㅇㅋ. 위에서 하드웨어는 준비 되었으니, 이제 소프트웨어에 해당하는 OS에서 어떤걸 준비해야 할지 생각해보자. 하드웨어는 그냥 장치일 뿐 어떤 판단을 내릴 수는 없으므로, 특정 상황에서 OS가 어떤 판단을 내려야 할지 고민해보는 것이다.
프로세스가 생성될 때
OS는 프로세스가 생성될 때 물리 메모리 위의 어느 곳을 프로세스에 할당해주어야 할지 결정해야 한다.
아까 위에서 프로세스가 사용할 주소 공간의 크기가 물리 메모리 크기보다 작다고 가정했으니 공간이 부족해서 할당을 못해주는 상황은 고려하지 않아도 될 것 같고, 또 모든 프로세스가 같은 크기(위에서는 16KB)의 공간을 할당 받는다고 했으니, 얼만큼 메모리를 할당해주어야 할지 고민할 필요도 없다.
이 빈 공간은 OS에서 Free list라는 자료 구조를 통해 관리되는데, 프로세스가 점유할 메모리 공간의 크기가 모두 같다고 했으니 그냥 앞에서부터 빈 공간을 찾아 다니다가 빈 공간(위 예제에서는 16~32KB, 48~64KB)을 발견하면 찜! 해주면 된다. 노드에 해당하는 빈 공간의 크기가 달라지는 순간 문제가 더 복잡해지지만,, 이 문제는 뒤에서 다룬다. 지금은 그런갑다 하고 넘겨도 좋다.
프로세스가 종료될 때
죽은 프로세스를 위해 메모리 공간을 계속 점유하고 있어서는 안되므로, OS는 메모리를 다시 해제할 수 있어야 한다.
위의 Free list의 관점에서 생각해보자면, 자의든 타의든 프로세스가 죽게 되면 이 프로세스가 사용하고 있던 메모리 공간을 찾아 다시 free 상태로 전환해 주는 것으로 이해할 수 있을 것 같다.
Context switching이 발생했을 때
CPU Virtualization에서 context swtiching시 레지스터 상태를 PCB에 저장했듯, Memory Virtualization에서도 마찬가지로 base, bound register 값을 PCB에 저장해 주어야 context switching이 발생했을 때 원래 참조하고 있던 물리 메모리 공간의 위치로 찾아갈 수 있을 것이다.
이렇게 PCB에 base, bound를 저장해 두었기 때문에 OS가 임의로 해당 프로세스가 점유할 물리 메모리 공간을 쉽게 relocation 해줄 수 있다. 그냥 프로세스가 원래 쓰던 메모리를 통째로 복사해서 옮겨놓고, 그 자리의 base 값으로 PCB를 덮어 써주면 프로세스가 다시 실행되더라도 MMU의 주소 변환 메커니즘에 의해 위화감 없이 하던 대로 가상 주소를 사용해 프로세스를 실행할 수 있기 때문이다.
예외가 발생했을 때
아까 HW가 Trap handler를 받아들일 수 있어야 한다고 했는데, 이 Trap handler를 OS가 부팅될 때 Kernel mode로 HW에 알려줄 수 있어야 한다.
이제 HW, OS가 Base & Bound 방법으로 Memory virtualization을 구현하기 위해 어떤 것들을 지원해야 하는지 모두 배웠으니, 실제로 두 요소가 어떻게 사용되는지 한 눈에 살펴보자.
어우 길어서 잘린다(..)
OS가 프로세스를 실행할 준비를 모두 마친 상태에서, 이번엔 프로세스 A, B를 실행시키는 상황을 생각해보자.
HW가 주소 변환 요청의 유효함과 주소 변환을 수행해주고, OS는 CPU Virutlization에서와 마찬가지로 LDE 방식을 통해 HW와 소통하여 메모리를 할당 받아 온다던지, Context switching과 예외 처리 등의 다른 작업들을 수행해주고 있다. 신기하지 않은가?!
위의 메모리 상태를 보면, 프로세스에 할당되어 있는 32~48KB 구간의 Heap, Stack 사이의 영역은 전혀 사용되고 있지 않지만 프로세스에 그냥 빈 공간으로 낭비되고 있다. 이를 Internal fragmentation이라 하는데, 좀만 생각해보면 위에서 세운 '메모리를 고정된 크기만큼 할당한다'는 가정 때문에 이런 낭비가 발생함을 알 수 있다. 문제있구만!
Base & Bound 방법을 활용한 Memory Virtualization 기법에 대해 공부했다.
위에서 세운 가정 자체가 많이 비현실적이기도 하고, 위의 Internal fragmentation과 같은 문제점이 있기 때문에 다음 장에서 일반화된 Base & Bound 기법인 Segmentation에 대해 공부하게 될 것이다.
요며칠 운영체제 과제랑 다른 과목 과제 때문에 거의 복습을 못했는데 시험이 정말 얼마 남지 않았다 ㅠㅠ 내일 모레까지 들어야 하는 강의까지 7개의 velog를 써야 하는데 좀 막막하다. 내일은 좀 더 일찍 일어나서 공부해야겠다,,, 지쳐,,,,,,,,