[OSTEP] 주소 변환의 원리

kshired·2021년 8월 1일
0

주소 변환의 원리

CPU 가상화에서는 시스템 콜이 호출되거나, 타이머 인터럽트가 발생하는 등의 특정 타이밍에 운영체제가 개입하여 문제가 발생하지 않도록 하였다.

→ 약간의 하드웨어 지원을 받아 효율적을 CPU를 제어하는 가상화 방법을 사용하였다.

메모리 가상화에서도 비슷한 전략을 추구할 것이다.

가상화를 제공하며, 동시에 효율성, 제어, 유연성을 추구할 것이다.

효율성을 높이기 위해 하드웨어의 지원을 활용할 것이다.

제어를 위해 자기자신의 메모리 이외에 다른 프로그램이 접근하지 못하도록 보호할 것이다.

프로그래머가 원하는 대로 주소공간을 활용할 수 있도록 유연성을 추구할 것이다.

우리가 메모리 가상화에서 사용할 기법은 하드웨어 기반 주소 변환 일반적으로 주소 변환 ( Address Translation ) 이라고 불리는 기술이다.

이 주소 변환을 통해 하드웨어는 virtual address를 physical address로 변환하게 됩니다.

물론, 하드웨어만으로 가상화르 구현할 수는 없습니다.

변환이 정확하게 일어날 수 있도록, OS가 셋업하기 위한 관여를 해야하며 OS는 메모리의 빈 공간과 사용 중인 공간을 항상 알고 잇어야합니다.

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

가정

이제 메모리 가상화를 알아 볼 것인데, 이전과 비슷한 방법으로 쉬운 이해를위해 몇가지 가정을 깔고 시작한 뒤

하나하나 제거하면서 실제적인 메모리 가상화를 배워볼 것입니다.

우리가 사용할 가정은 아래와 같습니다.

  1. 프로세스들은 물리 메모리에 연속적으로 배치된다.
  2. 프로세스들이 사용하는 주소 공간의 크기는 너무 크지 않을 것이다.
  3. 프로세스들이 사용하는 주소 공간은 물리 메모리보다 클 것이다.
  4. 각 프로세스들의 주소 공간의 크기는 모두 같을 것이다.

사례

아래와 같은 코드를 보면서, 주소 변환 구현을 위해 어떤 것이 필요한지 왜 그런 기법이 필요한지 알아보겠습니다.

void func(){
	int x = 3000;  
	x += 3; // 우리가 관심을 가질 코드
}

위 코드는 메모리에서 값을 탑재하고, 3을 증가시키고, 다시 메모리에 저장하는 짧은 코드입니다.

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

128: movl 0x0(\%ebx), \%eax ; 0+ebx를 eax에 저장
132: addl \$0x03, \%eax     ; eax 레지스터에 3을 더한다.
135: movl \%eax, 0x0(\%ebx) ; eax를 다시 메모리에 저장.

이제 메모리 그림을 통해 살펴보면, 코드와 데이터가 프로세스 주소 공간에 어떻게 배치되는지 볼 수 있습니다.

세 개 명령어의 코드는 주소 128에 위치하고, 변수 x의 값은 15KB에 위치합니다. 그림에서 x의 초기 값은 3000입니다.

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

  • 주소 128의 명령어를 fetch
  • 이 명령어를 execute ( 주소 15KB에서 load )
  • 주소 132의 명령어를 fetch
  • 이 명령어를 execute ( 메모리 접근 x )
  • 주소 135의 명령어를 fetch
  • 이 명령어를 execute ( 주소 15KB에 store )

이 프로그램 관점에서 주소 공간은 0부터 시작해서 최대 16KB까지이다.

프로그램이 생성하는 모든 메모리 참조는 이 범위 내에 있어야한다.

그런데, 우리는 메모리 가상화를 위해 실제로 0~16KB가 아닌 특정한 다른 곳으로 재배치를 하고 싶다.

재배치를 할 때 주의할 점은 프로세스가 모르게 메모리를 다른 위치에 재배치해야 한다는 것이다.

아까 알아본 프로세스의 주소 공간이 메모리에 배치되었을 때 가능한 물리 메모리 배치의 한 예시를 위 그림을 통해 볼 수 있다.

그림에서 물리 메모리에 첫 번째 슬롯은 OS가 사용하고, 위 예의 프로세스는 32KB에서 시작하는 슬롯에 재배치 된 것을 볼 수 있다.

우리는 어떻게 재배치를 할 수 있을까?

동적 재배치

우리는 가장 간단한 아이디어인 베이스와 바운드( Base and Bound )를 사용하는 동적 재배치(dynamic relocation)이라는 아이디어를 알아볼 것이다.

이 아이디어를 구현하기 위해서는 각 CPU마다 2개의 하드웨어 레지스터가 필요하다, 하나는 base 레지스터 또 다른 하나는 bound 레지스터가 필요하다.

이 base와 bound는 우리가 원하는 위치에 주소 공간을 배치할 수 있도록 보장한다.

그리고 배치와 동시에 프로세스가 오직 자신의 주소 공간에만 접근하는 것을 보장한다.

base 레지스터는 가상 주소 공간이 실제 메모리에 재배치 되었을 때 주소 공간의 시작 부분입니다.

그렇기에, 프로세스에 의해 생성되는 모든 주소가 다음과 같은 방법으로 프로세서에 의해 변환됩니다.

physical address = virtual address + base

예를 들어, 아까 알아본 프로세스에서 128번 주소에 접근하려고 한다면 그림 15.2에서 32KB에 재배치를 하게 되었으니 base가 32KB인 것을 알 수 있습니다.

그렇기에 하드웨어가 명령어를 fetch할 때, PC 값을 base 레지스터 값 32KB(32768)에 128을 더해서 32896이라는 물리주소를 얻습니다.

이렇게 우리는 base 레지스터를 통해 재배치가 가능하다는 것을 알 수 있게 되었습니다.

그렇다면, 또 다른 의문점이 생길 것입니다.

도대체 bound 레지스터는 어디에 쓰지?

이 bound 레지스터는 보호를 지원하기위해 사용됩니다.

프로세서는 메모리 참조가 합법인지 판단하기위해 가상 주소가 bound안에 있는지 확인합니다.

앞에서 다룬 예시에서 가상 메모리의 크기가 16KB였기 때문에, bound 레지스터 값은 16KB입니다.

만약 바운드보다 큰 주소 또는 음수인 가상주소를 접근하려고하면 CPU는 예외를 발생시키고 프로세스를 종료시킬 것입니다.

bound 레지스터는 두가지 방식 중 하나로 정의할 수 있습니다.

  • 주소 공간의 크기를 저장하는 방식
  • 주소 공간의 마지막 물리주소를 저장하는 방식

우리는 논의를 간단하게 하기 위해 전자를 사용하도록 하겠습니다.

그리고, bound와 base 같은 주소 변환에 도움을 주는 프로세서의 일부를 메모리 관리장치 MMU라고 부르기도합니다.

예시


주소 공간의 크기가 4KB(4096)인 프로세스가 물리주소 16KB에 탑재되어 있다고 가정하겠습니다.

이렇게 되면 base 레지스터의 값은 16KB, bound 레지스터 값은 4KB입니다.

예시 표의 3개의 시도에서는 bound 안에서 접근을 했기 때문에 문제없이 physical address로 변환이 되었습니다.

하지만, 마지막 시도는 bound를 넘어가기 때문에 out of bound라는 예외를 발생시키게 됩니다.

하드웨어 지원 : 요약

이제 주소 변환을 위해 필요한 하드웨어 지원을 요약해보겠습니다.

  • 커널모드와 유저모드를 구분해야합니다.
  • base와 bound 레지스터가 필요합니다.
  • 가상 주소를 변환하고, 가상 주소 범위안에 존재하는지 체크하는 능력이 필요합니다.
  • 프로그램 갱신 전에 base와 bound를 갱신하기 위한 커널 모드 명령어가 필요합니다.
  • 예외 핸들러 등록을 위한 특권 명령어가 필요합니다.
  • 예외가 발생되었을 때 예외를 발생시킬 수 있어야합니다.

운영체제 이슈

동적 재배치를 지원하기 위해 운영체제가 반드시 개입되어야 하는 중요한 세 개의 지점이 존재합니다.

  • 프로세스가 생성될 때 운영체제는 주소 공간이 저장될 메모리 공간을 찾아 조치를 취해야합니다.
    • 물리 메모리의 크기보다 작고, 크기는 일정하다는 가정하에 운영체제는 쉽게 처리가 가능합니다.
    • 운영체제는 free list라는 자료구조를 검색해 새로운 주소 공간 할당에 필요한 영역을 찾습니다.
  • 프로세스가 종료할 때, 메모리를 회수하여 다른 프로세스나 운영체제에서 사용할 수 있도록 해야합니다.
  • context switching이 일어날 때 몇가지 추가 조치를 진행해야합니다.
    • CPU마다 한쌍의 base-bound 레지스터만 존재합니다.
    • 각 프로그램은 각기 다른 물리 주소에 탑재하기 때문에 실행중인 프로그램마다 base-bound 레지스터의 값이 다릅니다.
    • 이렇기 때문에 프로세스 전환시 base-bound 레지스터 쌍을 저장하고 복원해야합니다.
    • 운영체제는 프로세스를 중단하기로 결정하면, 이 값들은 PCB(Process Control Block, 프로세스 제어블럭)라는 자료구조에 저장하게됩니다.
    • 프로세스 전환시 주소공간을 이동하려면, 운영체제는 먼저 프로세스의 스케줄링을 취소하고 현재 위치에서 새 위치로 주소 공간을 복사합니다. 마지막으로, 운영체제는 저장해둔 base 레지스터 정보로 base 레지스터를 업데이트하여 새로운 위치를 가리키게 됩니다.
  • 예외가 일어나면 호출할 핸들러나 함수를 제공해야합니다.
    • 프로세스가 bound 밖의 주소 공간을 접근하려고하면, CPU는 예외를 발생시켜야합니다.


OS는 부팅할 때 trap table, process table, free list를 초기화 하고 timer interrupt를 시작합니다.

이때 하드웨어는 system call handler, timer handler, illegal mem-access handler, illegal instruction handler의 주소를 기억하게 됩니다.

이제 실제로 프로세스가 실행될 때 어떻게 주소 변환이 이루어지는지 알아보겠습니다.


위의 예시에는 A와 B 프로세스가 있습니다.

일단 A가 실행되면, base와 bound 레지스터를 설정하게 됩니다.

그리고 주소 변환을 통해, 실제 주소를 얻게 되고 명령어를 실행하게 됩니다.

그러던 도중 타이머 인터럽트를 통해 , B 프로세스로 context switching이 일어나게 됩니다.

context switching이 일어나는 동안 B의 base와 bound 레지스터를 PCB에서 복원하고 B의 실제 주소를 얻게됩니다.

이 때, B 프로세스가 bound 밖의 명령어를 실행하려고 시도하게 되고 예외가 발생하게 되어 B 프로세스가 kill 되게 됩니다.

그림에서 볼 수 있듯이, 프로세스는 LDE의 기본적인 접근 방식을 따르고 있습니다.

운영체제는 하드웨어를 적절하게 설정하고, 프로세스가 CPU에서 직접 실행할 수 있게 합니다.

그리고, 프로세스가 잘못된 행동을 했을 때만 OS가 개입하게 됩니다.

요약

주소변환을 통해 운영체제가 프로세스의 모든 메모리 접근을 제어하고 보호할 수 있다는 것을 배웠습니다.

이 주소 변환의 효율성은 하드웨어의 지원을 통해 이루어졌고, 프로세스는 투명한 방식을 통해 자신이 가상화되는지도 모르는 환상을 겪게 됩니다.

그리고 base와 bound를 이용한 동적 재배치로 메모리를 가상화하는 방법을 알보았습니다.

간단한 하드웨어 회로만 추가하면 되기때문에, 아주 효율적인 가상화 방법이라고 생각될 수 있습니다.

하지만, 동적 재배치는 비효율적입니다.

우리가 아까 보았던 그림 15.2 예시에서, 스택과 힙이 아주 크지 않기 때문에 둘 사이 공간이 단순히 낭비되고 있음을 알 수 있습니다.

이는 메모리를 낭비하게 되는 것이며 이런 유형의 낭비를 내부 단편화라고 합니다.

이제 이것을 해결하기위해, 일반화된 base-bound 기법인 segmentation을 다음 포스트에서 알아보겠습니다.

profile
글 쓰는 개발자

0개의 댓글