Post

[운영체제 아주 쉬운 세 가지 이야기 - Virtualization] 15. Address Translation

[운영체제 아주 쉬운 세 가지 이야기 - Virtualization] 15. Address Translation

이 글은 제 개인적인 공부를 위해 작성한 글입니다.
틀린 내용이 있을 수 있고, 피드백은 환영합니다.


개요


CPU 가상화 부분에서, 제한적 직접 실행(LDE)라는 기법을 집중적으로 다루었다. LDE의 아이디어는 간단하다. 대부분의 경우 프로그램은 하드웨어에서 직접 실행된다. 그러나, 프로세스가 시스템 콜을 호출하거나 타이머 인터럽트가 발생할 때 등의 특정 순간에는 운영체제가 개입하여 문제가 발생하지 않도록 한다. 운영체제는 약간의 하드웨어 지원을 받아 효율적인 가상화를 제공하기 위해 실행 프로그램에게 방해가 되지 않도록 최선을 다한다. 중요한 순간에 운영체제가 관여하여 하드웨어를 직접 제어한다. 효율성과 제어가 현대 운영체제의 목표이다.

메모리 가상화 전략에서 비슷한 전략을 추구할 것이다. 가상화를 제공하는 동시에 효율성과 제어 모두를 추구한다. 효율성을 높히려면 하드웨어 지원을 활용할 수 밖에 없다. 처음에는 몇 개의 레지스터만 사용하는 정도부터 TLB, 페이지 테이블 등으로 점차 복잡한 하드웨어를 사용하게 될 것이다.

제어는 응용 프로그램이 자기자신의 메모리 이외에는 다른 메모리에 접근하지 못한다는 것을 운영체제가 보장하는 것을 의미한다. 프로그램을 다른 프로그램으로부터 보호하고 운영체제를 프로그램으로부터 보호하기 위하여 하드웨어 도움이 필요하다.

마지막으로 유연성(flexibility) 측면에서 VM 시스템에서 필요한 사항이 있다. 프로그래머가 원하는 대로 주소 공간을 사용하고, 프로그래밍하기 쉬운 시스템을 만들기 원한다.

그렇다면 어떻게 효율적이고 유연하게 메모리 가상화를 구축할 수 있을까? 프로그램이 필요로 하는 유연성을 어떻게 제공하고, 프로그램이 접근할 수 있는 메모리의 위치에 대한 제어를 어떻게 유지하는가? 그리고 메모리 접근을 어떻게 적절히 제한할 수 있는가?

우리가 다룰 기법은 하드웨어-기반 주소 변환(hardware-based address translation) 또는 그냥 짧게 주소 변환(address translation)이다. 이 기술은 제한적 직접 실행 방식에 부가적으로 사용되는 기능이라고 생각할 수 있다.

주소 변환을 통해 하드웨어는 명령어 반입, 탑재, 저장 등의 가상 주소를 정보가 실재 존재하는 물리 주소로 변환한다. 프로그램의 모든 메모리 참조를 실제 메모리 위치로 재지정하기 위하여 하드웨어가 주소를 변환한다.

물론, 하드웨어에 의해 제공되는 저수준(low-level) 기능들은 변환을 가속화 시키는 도움을 주지만, 하드웨어만으로 메모리 가상화를 구현할 수는 없다. 정확한 변환이 일어날 수 있도록 하드웨어를 셋업하기 위해 운영체제가 관여해야 한다. 운영체제는 메모리의 빈 공간과 사용 중인 공간을 항상 알고 있어야 하고, 메모리 사용을 제어하고 관리한다.

이 모든 작업들의 목표는 다음과 같다.

프로그램이 자신의 전용 메모리를 소유하고 그 안에 자신의 코드와 데이터가 있다는 환상을 만드는 것이다

이 가상 현실의 이면에는 불편한 진실이 존재한다. CPU가 실행 중인 한 프로그램에서 다음 프로그램으로 전환하는 것처럼, 많은 프로그램이 메모리를 공유한다. 운영체제는 하드웨어의 도움을 받아 쓸모없는 기계를 유용하고, 강력하며 사용하기 쉬운 개념으로 변환시킨다.


가정


메모리 가상화를 위한 첫 번째 시도는 간단하다. 사용자 주소 공간은 물리 메모리에 연속적으로 배치되어야 한다고 가정한다. 그리고 쉬운 설명을 위해 주소 공간의 크기가 너무 크지 않다고 가정한다. 주소 공간은 물리 메모리 크기보다 작고 각 주소 공간의 크기는 같다고 가정한다.


사례


주소 변환 구현을 위해 어떤 것이 필요한지, 왜 그런 기법이 필요한지 알기 위해 간단한 예를 살펴보자.

img

주소 공간이 위와 같은 프로세스가 있다고 가정하자. 여기서 우리가 검토하려는 코드는 메모리에서 값을 탑재하고, 3을 증가시키고, 다시 메모리에 저장하는 짧은 코드이다.

1
2
3
4
5
void func()
{
  int x = 3000;
  x = x + 3;
}

컴파일러는 이를 어셈블리 코드로 변환하고 그 결과는 x86 어셈블리로 다음고 ㅏ같을 것이다.

movel 0x0(%ebx), %eax
addl 0x03, %eax
movl %eax, 0x0(%ebx)

코드와 데이터가 프로세스 주소 공간에 어떻게 배치되어 있는지 볼 수 있다. 세 개 명령어의 코드는 상단 코드 섹션인 주소 128에 위치하고, 변수 x의 값은 아래 쪽 스택인 주소 15KB에 위치한다 그림에서 x의 초기 값은 3000이다.

이 명령어가 실행되면 프로세스 관점에서 다음과 같은 메모리 접근이 일어난다.

  1. 주소 128의 명령어를 반입
  2. 이 명령어 실행 (주소 15KB에서 탑재)
  3. 주소 132의 명령어를 반입
  4. 이 명령어 실행 (메모리 참조 없음)
  5. 주소 135의 명령어를 반입
  6. 이 명령어 실행 (주소 15KB에 저장)

프로그램 관점에서 주소 공간은 주소 0부터 시작하여 최대 16KB까지이다. 프로그램이 생성하는 모든 메모리 참조는 이 범위 내에 있어야 한다.

메모리 가상화를 위해 운영체제는 프로세스를 물리 메모리 주소 0이 아닌 다른 곳에 위치시키고 싶다. 어떻게 하면 프로세스 모르게 메모리를 다른 위치에 재배치 하느냐가 우리가 해결해야 할 문제이다.

프로세스 주소 공간이 실제로는 다른 물리 주소에 배치되어 있을 때, 주소 0번지부터 시작하는 가상 주소 공간의 환상을 제공할 수 있을까?

이 프로세스의 주소 공간이 메모리에 배치되었을 때 가능한 물리 메모리 배치의 한 예시가 아래 그림에 나와 있다.

img

위 그림에서 물리 메모리의 첫 번째 슬롯은 운영체제 자신이 사용하고, 프로세스는 물리 주소 32KB에서 시작하는 슬롯에 재배치되었다. 다른 두 슬롯 (16~32KB, 48~64KB)은 비어있다.


하드웨어 기반 동적 재배치


하드웨어 기반 주소 변환을 이해하기 위하여 베이스와 바운드(base and bound)라는 간단한 아이디어를 알아야 한다. 이 기술은 동적 재배치(dynamic relocation)라고도 한다.

각 CPU마다 2개의 하드웨어 레지스터가 필요하다. 하나는 베이스(base) 레지스터이고, 다른 하나는 바운드(bound) 혹은 한계(limit) 레지스터라고 불린다.

이 베이스와 바운드 쌍은 우리가 원하는 위치에 주소 공간을 배치할 수 있게 한다. 배치와 동시에 프로세스가 오직 자신의 주소 공간에만 접근한다는 것을 보장한다.

이 설정에서 각 프로그램은 주소 0에 탑재되는 것처럼 작성되고 컴파일된다. 프로그램 시작 시, 운영체제가 프로그램이 탑재될 물리 메모리 위치를 결정하고 베이스 레지스터를 그 주소로 결정한다. 위에 예에서는 운영체제는 프로세스를 물리 주소 32KB에 저장하기로 결정하고 베이스 레지스터를 이 값으로 설정한다.

프로세스가 실행되면서 프로세스에 의해 생성되는 모든 메모리 주소가 아래와 같은 방법으로 프로세서에 의해 변환된다.

1
physical_address = base + virtual_address

프로세스가 생성하는 메모리 참조는 가상 주소이다. 하드웨어는 베이스 레지스터의 내용을 이 주소에 더하여 물리 주소를 생성한다.

가상 주소에서 물리 주소로의 변환이 주소 변환이라고 부르는 바로 그 기술이다. 하드웨어는 프로세스가 참조하는 가상 주소를 받아들여 데이터가 실제로 존재하는 물리 주소로 변환한다. 이 주소의 재배치는 실행 시에 일어나고, 프로세스가 실행을 시작한 이후에도 주소 공간을 이동할 수 있기 때문에 동적 재배치라고 불리낟.

바운드 레지스터는 보호를 지원하기 위해 존재한다. 프로세스는 먼저 메모리 참조가 합법적인가를 확인하기 위해 가상 주소가 바운드 안에 있는지 확인한다. 위의 간단한 예시에서는 바운드 레지스터는 16KB로 설정되고, 프로세스가 바운드보다 큰 가상 주소 또는 음수인 가상 주소를 참조하면 CPU는 예외를 발생시키고 프로세스는 종료될 것이다.

바운드 레지스터의 중요한 점은 프로세스가 생성한 모든 주소가 합법적이고 프로세스의 “범위”에 있다는 것을 확인하는 것이다.

베이스와 바운드 레지스터는 CPU 칩 상에 존재하는 하드웨어 구조임을 명심하자. (CPU당 1쌍) 주소 변환에 도움을 주는 프로세서의 일부를 메모리 관리 장치(memory management unit, MMU)라고 부르기도 한다.

바운드 레지스터는 두 가지 방식 중 하나로 정의될 수 있다. 첫 번째 방법은 앞에서처럼 주소 공간의 크기를 저장하는 방식으로 하드웨어는 가상 주소를 베이스 레지스터에 더하기 전에 먼저 바운드 레지스터와 비교한다.

두 번째 방법은 주소 공간의 마지막 물리 주소를 저장하는 방식으로 하드웨어는 먼저 베이스 레지스터를 더하고 그 결과가 바운드 안에 있는지 검사한다.

두 방법 모두 논리적으로 동일하고, 설명을 간단하게 하기 위해 첫 번째 방법을 사용하자.


하드웨어 지원 요약


먼저 CPU 가상화에서 배웠듯이 두 가지 CPU 모드가 필요하다. 운영체제는 특권 모드 혹은 커널 모드에서 실행하며 컴퓨터 전체에 대한 접근 권한을 가진다.

응용 프로그램은 사용자 모드에서 실행되며, 할 수 있는 일에 제한이 있다. 프로세서 상태 워드(processor status word) 레지스터의 한 비트가 CPU의 현재 실행 모드를 나타낸다. 시스템 콜 또는 다른 종류의 예외나 인터럽트 발생 시 같은 특정한 순간에 CPU는 모드를 전환한다.

하드웨어는 베이스와 바운드 레지스터를 자체적으로 제공한다. CPU는 메모리 관리 장치(MMU)의 일부인 추가의 레지스터 쌍을 가진다. 프로그램이 실행 중인 경우, 하드웨어는 프로그램이 생성한 가상 주소에 베이스 값을 더하여 주소를 변환한다. 하드웨어는 주소가 유효한지 검사할 수 있어야 한다. 이 검사는 바운드 레지스터와 CPU 내의 일부 회로를 사용하여 이루어진다.

하드웨어은 베이스와 바운드 레지스터 값을 변경하는 명령어를 제공해야 한다. 다른 프로세스를 실행시킬 때 운영체제가 이 명령어를 사용하여 베이스와 바운드 레지스터 값을 변경할 수 있다. 이 명령어들은 특권 명령어이다. 커널 모드에서만 레지스터를 변경할 수 있다.

마지막으로 CPU는 사용자 프로그램이 바운드를 벗어난 주소로 불법적인 메모리 접근을 시도하려는 상황에서 예외을 발생시킬 수 있어야 한다. 이 경우 CPU는 사용자 프로그램의 실행을 중지하고 운영체제의 바운드 벗어남 예외 핸들러(exception handler)가 실행되도록 조치해야 한다. 운영체제 핸들러는 어떻게 대처할지 결정할 수 있고 이 경우에 프로그램을 종료시킬 확률이 매우 높다.

마찬가지로 사용자 프로그램이 특권이 필요한 베이스와 바운드 레지스터 값의 변경을 시도하면 CPU는 예외를 발생시키고 “사용자 모드에서의 특권 연산 발생” 핸들러를 실행시켜야 한다. CPU는 이 핸들러들의 주소를 파악하기 위한 방법을 제공해야 한다. 몇 개의 특권 명령어가 더 필요하다.


운영체제 이슈


동적 재배치 지원을 위해 하드웨어가 새로운 기능을 제공하는 것과 마찬가지로 운영체제에도 새로운 이슈가 등장한다. 하드웨어 지원과 운영체제 관리가 결합되면 간단한 가상 메모리를 구현할 수 있다. 베이스와 바운드 방식의 가상 메모리 구현을 위해서 운영체제가 반드시 개입되어야 하는 중요한 세 개의 시점이 존재한다.

첫째, 프로세스가 생성될 때 운영체제는 주소 공간이 저장될 메모리 공간을 찾아 조치를 취해야 한다. 이를 위해 새로운 프로세스가 생성되면 운영체제는 새로운 주소 공간 할당에 필요한 영역을 찾기 위해 빈 공간 리스트(free list) 자료 구조를 탐색한다.

둘째, 프로세스가 종료할 때, 즉 정상적으로 종료될 때 또는 잘못된 행동을 하여 강제적으로 죽게될 때 프로세스가 사용하던 메모리를 회수하여 다른 프로세스나 운영체제가 사용할 수 있게 해야 한다. 프로세스가 종료하면, 운영체제는 종료한 프로세스의 메모리를 다시 빈 공간 리스트에 넣고 연관된 자료 구조를 모두 정리한다.

셋째, 운영체제는 문맥 교환이 일어날 때에도 몇 가지 추가 작업을 수행해야 한다. CPU마다 한 쌍의 베이스-바운드 레지스터만 존재하고 각 프로그램은 다른 물리 주소에 탑재되어야 하기 때문에 실행 중인 프로그램마다 다른 값을 가진다. 운영체제는 프로세스 전환 시 베이스와 바운드 쌍을 저장하고 복원해야 한다.

운영체제가 실행 중인 프로세스를 중단시키면 운영체제는 메모리에 존재하는 프로세스 별 자료 구조 안에 베이스와 바운드 레지스터 값을 저장해야 한다. 이 자료구조는 프로세스 구조체(process structure) 또는 프로세스 제어 블록(process control block, PCB)라고 불린다. 마찬가지로 운영체제는 실행 중인 프로세스를 다시 시작할 떄 또는 처음 실행시킬 때, 이 프로세스에 맞는 값으로 CPU의 베이스와 바운드 값을 설정해야 한다.

프로세스가 중단되면 운영체제가 메모리의 현 위치에서 다른 위치로 주소 공간을 비교적 쉽게 옮길 수 있다는 사실에 주목해야 한다. 프로세스의 주소 공간을 이동시키려면 운영체제는 먼저 프로세스의 실행을 중지시킨다. 그런 후 운영체제는 현재 위치에서 새 위치로 주소 공간을 복사한다. 마지막으로 운영체제는 PCB에 저장된 베이스 레지스터를 갱신하여 새 위치를 가리키도록 한다.

프로세스가 실행을 재개하면 새로운 베이스 레지스터가 복원되고 다시 실행을 시작하지만, 명령어와 데이터가 전혀 다른 새 위치에 존재한다는 사실을 인식하지 못한다.

넷째, 위에서 언급한 것처럼 운영체제는 예외 핸들러 또는 호출될 함수를 제공해야 한다. 운영체제는 부팅할 때 특권 명령어를 사용하여 이 핸들러를 설치한다. 예를 들어 프로세스가 바운드 밖의 메모리에 접근하려는 경우 CPU는 예외를 발생시킨다.

운영체제는 이러한 예외가 발생할 때 조치를 취할 준비가 되어 있어야 한다. 운영체제는 자신이 실행되는 컴퓨터를 보호해야 하기에 일반적으로 프로세스를 종료시키는 것처럼 적대적인 양상을 보인다.


정리


  • 메모리 가상화

각 프로세스에게 자신만의 독립적인 메모리 공간을 가지고 있다는 환상을 제공하는 것을 목표로 한다.

이를 통해 여러 프로세스가 하나의 물리 메모리를 안전하고 효율적으로 공유할 수 있다.

이를 달성하기 위해 하드웨어 기반 주소 변환(hardware-based address translation) 기술을 사용한다.


  • 하드웨어 기반 주소 변환 (== 동적 재배치)

CPU 내의 MMU에 두 개의 하드웨어 레지스터를 사용한다.

베이스 레지스터는 프로세스의 주소 공간이 시작되는 물리 메모리 주소를 저장한다.

바운드 레지스터는 프로세스의 주소 공간 크기를 저장하여 보호 목적으로 접근 범위를 제한한다.

프로세스가 실행될 때, 프로세스에서 생성되는 모든 주소는 가상 주소이다.

하드웨어는 이 가상 주소를 물리 주소로 변환한다.

physical_address = base + virtual_address

프로세스가 바운드 밖의 주소를 참조하려고 하면 CPU는 예외를 발생시킨다.


  • 하드웨어의 지원
  1. 커널 모드와 사용자 모드를 지원해야 한다.
  2. 베이스와 바운드 레지스터를 제공해야 한다.
  3. 베이스와 바운드 레지스터 값을 변경하는 커널 모드에서만 실행 가능한 특권 명령어를 제공해야 한다.
  4. 잘못된 메모리 접근 시 CPU가 실행을 중단하고 예외를 발생시켜 운영체제 핸들러를 호출할 수 있어야 한다.


  • 운영체제의 역할
  1. 프로세스 생성 시 빈 공간 리스트를 탐색하여 주소 공간을 할당하고, 해당 위치를 베이스 레지스터 값으로 설정한다.
  2. 프로세스 종료 시 사용하던 메모리를 빈 공간 리스트에 다시 반환하고 관련 자료 구조를 정리한다.
  3. 문맥 교환 시 현재 실행 중인 프로세스의 베이스와 바운드 레지스터 값을 PCB에 저장하고, 새로 실행할 프로세스의 값을 CPU 레지스터에 복원한다.
  4. 메모리 접근 위반과 값은 예외가 발생하면 해당 예외를 처리하는 핸들러를 호출한다.


참고

This post is licensed under CC BY 4.0 by the author.