[OSTEP] 스터디 6주차 - 메모리 가상화 2 Part.1

주소 변환의 원리

메모리 가상화는 가상화를 제공하는 동시에 효율성과 제어 모두를 추구한다.

  • 효율성: 레지스터, TLB(Translation Lookaside Buffer) 등의 하드웨어 지원을 활용해 주소 변환의 효율을 높인다.
    • 레지스터: CPU 내부에 있는 고속의 작은 메모리로, 자주 사용되는 데이터나 명령어를 저장하여 빠른 접근을 가능하게 한다.
    • TLB: 최근에 사용된 가상 주소와 물리 주소의 매핑 정보를 캐시로 저장하여, 주소 변환 속도를 향상시킨다.
  • 제어: 운영체제는 각 프로세스의 주소 공간을 분리하여 관리함으로써, 한 프로세스가 다른 프로세스의 메모리에 무단으로 접근하는 것을 방지한다. 이를 통해 시스템의 안정성과 보안을 높인다.
  • 유연성: 가상 메모리 시스템은 프로그래머가 물리 메모리의 제약에 구애받지 않고 편리하게 프로그래밍할 수 있는 환경을 제공한다. 프로세스는 자신만의 독립된 주소 공간을 가지므로, 메모리 할당과 관리가 용이해진다.

주소 변환(Address translation)은 프로그램이 사용하는 가상 주소를 실제 물리 메모리 상의 주소로 매핑하는 과정이다. 주소 변환은 하드웨어적으로 이루어지며, 프로세서가 메모리 참조 명령어(데이터 읽기, 쓰기, 명령어 가져오기 등)를 수행할 때마다 발생한다.

운영체제는 메모리 관리자 역할을 수행하며, 물리 메모리의 할당 및 회수, 가상 주소 공간과 물리 주소 공간의 매핑 등을 담당한다. 이를 위해 운영체제는 메모리의 사용 현황을 파악하고 있어야 하며, 프로세스 간의 메모리 보호와 공유를 적절히 제어해야 한다.


동적 (하드웨어 기반) 재배치

동적 재배치(Dynamic Relocation)는 프로세스의 주소 공간을 실행 시간에 동적으로 물리 메모리 상의 다른 위치로 이동할 수 있게 해주는 기술이다. 이를 통해 운영체제는 메모리 관리를 보다 유연하게 할 수 있으며, 메모리 단편화 문제를 완화할 수 있다. 동적 재배치를 구현하기 위해 CPU에는 베이스 레지스터(Base Register)와 바운드 레지스터(Bound Register)라는 두 개의 하드웨어 레지스터가 사용된다.

  1. 베이스 레지스터 (Base Register)
    • 베이스 레지스터는 프로세스의 주소 공간이 실제 물리 메모리 상에 위치한 시작 주소를 저장하는 레지스터다.
    • 주소 변환 과정에서 프로세스가 생성한 가상 주소에 베이스 레지스터 값을 더하여 실제 물리 주소를 계산한다.
    • 이를 통해 프로세스는 가상 주소 0번지에서 시작하는 것처럼 인식하지만, 실제로는 물리 메모리의 다른 영역에 위치할 수 있다.
    • 베이스 레지스터 값을 변경함으로써 프로세스의 주소 공간을 물리 메모리 상의 다른 위치로 이동시킬 수 있다.
  2. 바운드 레지스터 (Bound Register)
    • 바운드 레지스터는 프로세스의 주소 공간 크기 또는 주소 공간의 마지막 주소를 저장하는 레지스터다.
    • 하드웨어는 프로세스가 생성한 가상 주소가 바운드 레지스터에 저장된 범위 내에 있는지 확인하여 메모리 보호 기능을 수행한다.
    • 만약 가상 주소가 바운드 레지스터에 저장된 범위를 벗어나면 하드웨어는 예외(Exception)를 발생시켜 운영체제에 알린다.
    • 이를 통해 프로세스가 자신의 주소 공간 외부의 메모리 영역에 접근하는 것을 방지하여 메모리 보호를 강화한다.
  3. 주소 변환 과정
    • 프로세스가 생성한 가상 주소에 베이스 레지스터 값을 더하여 실제 물리 주소를 계산한다.
    • 계산된 물리 주소가 바운드 레지스터 값의 범위 내에 있는지 확인한다.
    • 물리 주소가 유효한 범위 내에 있다면 해당 주소의 메모리에 접근할 수 있도록 허용한다.
    • 만약 물리 주소가 바운드 레지스터 값의 범위를 벗어난다면 하드웨어는 예외를 발생시켜 운영체제에 알린다.
  4. 동적 재배치 과정
    • 운영체제는 필요에 따라 프로세스의 주소 공간을 물리 메모리 상의 다른 영역으로 이동시킬 수 있다.
    • 주소 공간을 이동시킬 때, 운영체제는 해당 프로세스의 베이스 레지스터 값을 변경하여 새로운 물리 메모리 영역을 가리키도록 한다.
    • 또한 바운드 레지스터 값도 새로운 주소 공간 크기에 맞게 조정한다.
    • 프로세스의 상태 정보를 저장하는 자료구조인 프로세스 제어 블록(PCB, Process Control Block)에 변경된 베이스와 바운드 레지스터 값을 저장한다.
    • 프로세스가 다시 실행될 때, PCB에 저장된 베이스와 바운드 레지스터 값이 CPU의 레지스터에 로드되어 프로세스는 새로운 물리 메모리 영역에서 실행된다.
  5. 메모리 관리 장치 (MMU, Memory Management Unit)
    • 주소 변환과 메모리 보호 기능은 CPU 내부의 하드웨어 컴포넌트인 메모리 관리 장치(MMU)에 의해 수행된다.
    • MMU는 베이스 레지스터와 바운드 레지스터를 활용하여 가상 주소를 물리 주소로 변환하고, 변환된 주소가 유효한 범위 내에 있는지 검사한다.
    • 이 과정은 하드웨어적으로 이루어지므로 소프트웨어 오버헤드 없이 빠르게 처리할 수 있다.
    • MMU는 주소 변환 테이블(Address Translation Table)을 관리하여 가상 주소와 물리 주소 간의 매핑 정보를 저장한다.
    • 주소 변환 테이블은 운영체제에 의해 설정되며, MMU는 이를 참조하여 주소 변환을 수행한다.

동적 재배치 기술을 사용하면 운영체제는 프로세스의 주소 공간을 물리 메모리 상의 임의의 위치로 이동시킬 수 있으므로, 메모리 관리의 유연성이 향상된다. 이를 통해 메모리 단편화 문제를 완화하고, 메모리 사용 효율을 높일 수 있다.

또한 베이스 레지스터와 바운드 레지스터를 활용한 주소 변환 및 메모리 보호 기능은 프로세스 간의 메모리 침범을 방지하여 시스템의 안정성과 보안성을 강화한다. 각 프로세스는 자신의 주소 공간 내에서만 메모리에 접근할 수 있으며, 다른 프로세스의 메모리 영역에는 접근할 수 없다.

동적 재배치는 초기의 메모리 관리 기법 중 하나로, 현대의 가상 메모리 시스템에서는 페이징(Paging)과 세그멘테이션(Segmentation) 등의 보다 발전된 기술이 사용된다. 하지만 동적 재배치의 기본 개념인 베이스와 바운드 레지스터를 활용한 주소 변환 및 메모리 보호 기능은 현대의 메모리 관리 기법에서도 중요한 역할을 담당하고 있다.


예제: 베이스와 바운드를 이용한 주소 변환

가상 주소 공간의 크기가 4KB인 프로세스가 있다고 가정해 보겠다. (현실에서는 매우 작은 크기이지만, 이해를 돕기 위한 예시) 이 프로세스는 물리 메모리의 16KB 지점에 적재되어 있다. 이 경우 주소 변환은 다음과 같이 이루어진다.

가상 주소 물리주소
0 16KB
1KB 17KB
3000 19384
4400 폴트 (바운드 초과)

이 예시에서 알 수 있듯이, 물리 주소를 얻으려면 가상 주소에 베이스 레지스터의 값(여기서는 16KB)을 더해주면 된다. 가상 주소는 프로세스의 주소 공간 내에서의 오프셋(offset)으로 생각할 수 있다.

만약 가상 주소가 주소 공간의 크기(여기서는 4KB)를 초과하거나 음수라면, 즉 바운드 레지스터로 정의된 범위를 벗어나면 폴트(fault)가 발생하고 예외(exception)가 처리된다.


하드웨어 지원 요약

  • 커널, 유저 모드 구분: 운영체제는 커널 모드에서 핵심 기능을 수행하고, 유저 모드에서는 응용 프로그램이 실행된다. 이는 보안과 안정성을 유지하기 위해 중요하다.
    • 커널 모드(kernel mode): 운영체제의 핵심 기능이 실행되는 모드로, 모든 시스템 자원에 접근할 수 있는 권한을 가진다. 커널 모드에서는 중요한 시스템 작업과 하드웨어 제어가 이루어진다.
    • 유저 모드(user mode): 응용 프로그램이 실행되는 모드로, 제한된 권한을 가진다. 유저 모드에서는 시스템 자원에 직접 접근할 수 없고, 운영체제가 제공하는 API를 통해 자원을 요청하고 사용한다.
  • 베이스, 바운드 레지스터: 각 프로세스가 사용하는 메모리 공간의 시작과 크기를 지정하는 레지스터다. 베이스 레지스터는 시작 위치를 가리키고, 바운드 레지스터는 공간의 크기를 제한한다.
  • 주소 변환 능력: 하드웨어는 가상 주소를 물리 주소로 변환한다. 이때 베이스 레지스터 값을 더하고, 바운드 레지스터를 사용하여 유효성을 확인한다.
    • 가상 주소(virtual address): 프로세스가 사용하는 논리적인 주소로, 프로세스의 주소 공간 내에서 유효하다. 가상 주소는 프로세스마다 독립적으로 할당되며, 0부터 시작한다.
    • 물리 주소(physical address): 실제 물리 메모리 상의 주소로, 가상 주소를 변환한 결과다. 물리 주소는 시스템 전체에서 유일하며, 실제 메모리 위치를 가리킨다.
  • 잘못된 주소 검출: 하드웨어는 가상 주소가 유효한 범위 내에 있는지 확인하여 보안과 안정성을 유지한다.
  • 오류 처리 능력: 잘못된 주소 접근이나 범위 초과와 같은 오류가 발생할 때, 하드웨어는 이를 신속하게 감지하고 적절히 처리한다.
    • 예외(exception): 프로그램 실행 중 발생하는 비정상적인 상황으로, 하드웨어나 소프트웨어에 의해 감지된다. 예외가 발생하면 현재 실행 흐름이 중단되고 예외 처리기(exception handler)로 제어가 전달된다.
    • 예외 처리기(exception handler): 예외가 발생했을 때 호출되는 코드 루틴으로, 예외 상황을 처리하고 프로그램의 실행을 정상화하거나 안전하게 종료시키는 역할을 한다.

운영체제 이슈

베이스와 바운드 방식의 가상 메모리 구현을 위해서 운영체제가 반드시 개입되어야 하는 중요한 세 개의 시점이 존재한다.

  1. 프로세스 생성 시
    • 운영체제는 새로운 프로세스의 주소 공간을 할당할 물리 메모리 영역을 찾아야 한다.
    • 운영체제는 물리 메모리를 슬롯(slot)의 배열로 관리하며, 각 슬롯의 사용 여부를 추적한다.
      • 슬롯(slot): 물리 메모리를 일정한 크기의 블록으로 나눈 것으로, 프로세스에 할당되는 메모리의 기본 단위다. 운영체제는 슬롯의 사용 여부를 비트맵이나 링크드 리스트 등의 자료구조로 관리한다.
    • 운영체제는 빈 슬롯을 검색하여 해당 영역을 프로세스의 주소 공간으로 할당하고, 사용 중으로 표시한다.
  2. 프로세스 종료 시
    • 운영체제는 종료된 프로세스가 사용하던 메모리 영역을 회수하여 다른 프로세스나 운영체제가 사용할 수 있게 한다.
    • 프로세스가 정상적으로 종료되거나 강제 종료될 때, 운영체제는 해당 프로세스의 메모리 영역을 빈 공간 리스트에 반환하고 관련 자료구조를 정리한다.
  3. 문맥 교환 시
    • CPU에는 한 쌍의 베이스-바운드 레지스터만 존재하므로, 실행 중인 프로세스마다 다른 값을 가져야 한다.
    • 운영체제는 프로세스 전환 시 현재 프로세스의 베이스와 바운드 레지스터 값을 저장하고, 새로운 프로세스의 값으로 설정해야 한다.
    • 이 값들은 프로세스 제어 블록(PCB)에 저장되며, 운영체제는 PCB에서 읽어와 CPU 레지스터에 로드한다.
      • 프로세스 제어 블록(Process Control Block, PCB): 프로세스의 메타데이터를 저장하는 운영체제의 자료구조로, 프로세스 상태, 레지스터 값, 메모리 할당 정보 등을 포함한다. 운영체제는 PCB를 통해 프로세스를 관리하고 문맥 교환을 수행한다.
  4. 메모리 보호 위반 시
    • 운영체제는 부팅 시 특권 명령어를 사용하여 예외 핸들러를 설치한다.
    • 프로세스가 할당된 주소 공간 밖의 메모리에 접근하려 할 경우, CPU는 예외를 발생시킨다.
    • 운영체제는 이 예외를 처리하여 해당 프로세스를 종료하거나 적절한 조치를 취한다.

세그멘테이션

베이스와 바운드 레지스터를 사용하면 운영체제는 프로세스를 물리 메모리의 다른 부분으로 쉽게 재배치할 수 있다. 그러나 이러한 형태의 주소 공간에서 재미있는 사실은 스택과 힙 사이에 사용되지 않는 큰 공간이 존재한다는 것이다. 이 공간은 주소 공간을 물리 메모리에 재배치할 때 물리 메모리를 차지하게 된다. 베이스와 바운드 레지스터 방식은 이러한 메모리 낭비가 심할 수 있으며, 또한, 주소 공간이 물리 메모리보다 큰 경우 실행이 어려워질 수 있다. 이러한 측면에서 볼 때, 베이스와 바운드 방식은 유연성이 부족한 것으로 여겨진다.


세그멘테이션: 베이스/바운드의 일반화

이 문제를 해결하기 위한 아이디어 중 하나가 세그멘테이션(segmentation)이다.

세그멘테이션은 주소 공간을 논리적으로 분할하여 각각의 세그먼트에 대해 별도의 베이스(base)와 바운드(bound) 쌍을 할당하여 메모리 관리 장치(MMU)에 저장하는 방식이다. 이러한 세그먼트는 특정 길이를 가지는 연속적인 주소 공간을 나타내며, 일반적으로 코드, 스택, 및 힙 등과 같이 프로그램이나 데이터의 논리적인 부분을 나타낸다.

  • 세그먼트(segment): 프로그램의 논리적인 구성 요소로, 연속적인 주소 공간을 차지하는 코드, 데이터, 스택 등을 나타냅니다. 각 세그먼트는 고유한 이름, 크기, 보호 속성 등을 가진다.

세그멘테이션을 사용하면 운영체제는 각 세그먼트를 메모리에 별도로 배치함으로써 프로그램이나 데이터를 물리 메모리의 다양한 위치에 할당할 수 있다. 또한 사용되지 않는 가상 주소 공간이 물리 메모리를 차지하는 것을 방지할 수 있다.

그림을 보면 64 KB의 물리 메모리에 3개의 세그먼트와 운영체제용으로 예약된 16 KB 영역이 존재한다. 그림에서 볼 수 있듯이, 사용 중인 메모리에만 물리 공간이 할당된다. 이러한 구조는 사용되지 않은 영역이 많은 대형 주소 공간(드문드문 사용되는 주소 공간(sparse address space)이라고도 부름)을 수용할 수 있다.

  • 드문드문 사용되는 주소 공간(sparse address space): 프로그램의 주소 공간 중 실제로 사용되는 부분이 드문드문 흩어져 있는 상태를 말한다. 이는 주소 공간의 상당 부분이 사용되지 않고 비어 있음을 의미한다.

세그먼트 지원을 위한 MMU 하드웨어 구조는 예상한 것과 같다. 이 예의 경우 3쌍의 베이스와 바운드 레지스터 집합이 필요하다.

각 바운드 레지스터는 세그먼트의 크기를 저장한다. 그림에서 코드 세그먼트가 물리 주소 32 KB에 배치되고 크기는 2 KB이며, 힙 세그먼트가 34 KB에 배치되고 역시 크기는 2 KB라는 것을 알 수 있다.

주소 공간을 사용하여 주소 변환을 살펴보겠다. 먼저, 가상 주소 100번지가 참조된다고 가정해 본다. 이 주소는 코드 세그먼트에 속한다. 참조가 발생하면, 하드웨어는 해당 세그먼트의 베이스 값에 가상 주소의 오프셋을 더한다. 이 경우, 100을 더하여 물리 주소는 100 + 32 KB로 계산되어 32868이 된다. 그 후, 하드웨어는 이 주소가 범위 내에 있는지 확인하고 (100은 2 KB보다 작으므로), 범위 내에 있다면 물리 메모리 주소 32868을 읽는다.

  • 오프셋(offset): 세그먼트의 시작 주소로부터 특정 주소까지의 거리를 나타내는 값이다. 오프셋은 세그멘트 내에서의 상대적인 위치를 나타낸다.

다음으로 가상 주소 4200번지를 힙에서 살펴보면, 힙의 베이스인 34 KB에 이를 더하면 물리 주소 39016을 얻게 된다. 그러나 이 주소는 올바른 물리 주소가 아니다. 먼저 힙 내에서의 오프셋, 즉 주소가 세그멘트의 시작으로부터 몇 번째 바이트인지를 확인해야 한다. 힙은 가상 주소 4 KB(4096)에서 시작하기 때문에 오프셋은 4200 - 4096으로 계산되어 104가 된다. 이 오프셋(104)을 힙의 베이스 레지스터의 물리 주소(34 KB)에 더하면 원하는 결과인 34920을 얻을 수 있다.

그러나 만약 잘못된 주소인 힙의 마지막을 벗어난 7 KB와 같은 주소에 접근하려고 한다면 어떻게 될까? 하드웨어는 이 주소가 범위를 벗어났다는 것을 감지하고 운영체제에 트랩을 발생시킨다. 운영체제는 문제의 프로세스를 종료시킬 가능성이 크다. 이렇게 잘못된 주소 접근으로 인해 발생하는 문제를 C 프로그래머들이 많이 겪는 유명한 용어의 기원을 알 수 있다: 세그멘트 위반(segment violation) 또는 세그멘트 폴트(segment fault).

세그멘트 위반(segment violation) 또는 세그멘트 폴트(segment fault)
세그먼트 위반(segment violation) 또는 세그먼트 폴트(segment fault)는 프로그래밍에서 주소 접근 오류가 발생했을 때 나타나는 용어로 기원적으로는 주소 접근 오류가 발생했을 때, 해당 프로세스가 접근한 메모리 주소가 메모리 세그멘트의 범위를 벗어났을 때 발생한 것이다. 이러한 오류는 프로그램이 메모리를 잘못 사용하거나 액세스하려고 할 때 발생한다.
세그멘트 위반 또는 세그멘트 폴트가 나타나면, 보통 운영체제는 프로그램이 메모리를 잘못 사용하는 오류를 신속하게 감지하여 프로세스나 시스템의 안정성을 유지하기 위해 해당 프로세스를 중단하거나 종료시킨다.
  • 트랩(trap): 프로세스가 잘못된 메모리 접근이나 불법적인 명령어 실행 등의 예외 상황을 발생시켰을 때, CPU가 현재 실행 중인 프로세스를 중단하고 운영체제에게 제어권을 넘기는 것을 말한다. 트랩이 발생하면 운영체제는 적절한 예외 처리기를 실행하여 상황을 처리한다.

세그멘테이션은 프로그램의 논리적 구조에 기반하여 메모리를 관리하므로, 프로그래머에게 직관적이고 유연한 메모리 모델을 제공한다. 또한 세그먼트 단위로 메모리 보호와 공유가 가능하므로 시스템의 안정성과 보안을 향상시킬 수 있다. 그러나 세그멘테이션은 외부 단편화(external fragmentation) 문제를 야기할 수 있으며, 세그먼트 테이블 관리에 오버헤드가 발생할 수 있다.

  • 외부 단편화(external fragmentation): 메모리에 할당되지 않은 빈 공간들이 여러 곳에 산재해 있어, 실제로는 충분한 메모리가 있음에도 불구하고 연속적인 메모리 공간을 할당하지 못하는 현상을 말한다. 세그멘테이션에서는 가변 크기의 세그먼트를 할당하므로 외부 단편화가 발생할 수 있다.

현대의 대부분의 운영체제는 세그멘테이션보다는 페이징(paging) 기법을 사용하여 메모리를 관리한다. 페이징은 고정된 크기의 페이지 단위로 메모리를 나누어 관리하므로, 외부 단편화 문제를 해결할 수 있다. 그러나 일부 운영체제에서는 세그멘테이션과 페이징을 혼합하여 사용하기도 한다.


세그멘트 종류의 파악

하드웨어는 주소 변환을 위해 세그멘트 레지스터를 사용한다. 한 가지 일반적인 접근 방법은 가상 주소의 최상위 몇 비트를 사용하여 주소 공간을 여러 세그먼트로 나누는 것이다. 이 방법은 예를 들어 VAX/VMS 시스템에서 사용되었다. 위의 예에서는 3개의 세그먼트가 있으므로 주소 공간을 세그먼트로 나누기 위해서는 2비트가 필요하다. 따라서 세그먼트를 표시하기 위해 가상 주소의 최상위 2비트를 사용하는 경우, 가상 주소의 형태는 다음과 같을 것이다.

예를 들어, 최상위 2비트가 00이면 하드웨어는 가상 주소가 코드 세그먼트를 가리킨다는 것을 인식하고, 이에 따라 코드 세그먼트의 베이스와 바운드 쌍을 활용하여 주소를 정확한 물리 메모리 위치로 재배치한다. 최상위 2비트가 01이면, 하드웨어는 주소가 힙 세그멘트를 가리킨다는 것을 인지하고, 힙의 베이스와 바운드를 사용하여 주소를 변환한다. 이해를 돕기 위해 이전에 언급한 힙에 해당하는 가상 주소인 4200을 변환해 보면 가상 주소 4200에 대한 이진 표현은 다음과 같다.

그림에서 볼 수 있듯이, 최상위 2비트 (01)는 하드웨어에게 참조하는 세그먼트의 종류를 알려준다. 그리고 하위 12비트는 해당 세그먼트 내의 오프셋을 나타낸다. 예를 들어, 이진 형식으로 표현된 주소 0000 0110 1000은 16진수로는 0x068 또는 10진수로는 104입니다. 하드웨어는 세그멘트 레지스터를 이해하기 위해 처음 2비트를 사용하고, 그 다음 12비트를 세그멘트 오프셋으로 취한다. 이 오프셋에 베이스 레지스터 값을 더하여 하드웨어는 최종적인 물리 주소를 계산한다. 또한, 오프셋을 사용하면 바운드 검사도 쉽게 수행할 수 있습니다. 바운드를 넘어선 오프셋인지를 검사하기만 하면 된다. 그렇지 않으면 주소가 잘못된 것이다. 만약 베이스와 바운드 쌍을 배열 형태로 저장한다면 (세그멘트당 하나의 항목), 원하는 물리 주소를 얻기 위해 다음과 같은 과정을 수행하게 된다.

// get top 2 bits of 14-bit VA
Segment = (VirtualAddress & SEG_MASK) >> SEG_SHIFT
// now get offset
Offset = VirtualAddress & OFFSET_MASK
if (Offset >= Bounds[Segment])
	RaiseException(PROTECTION_FAULT)
else
	PhysAddr = Base[Segment] + Offset
	Register = AccessMemory(PhysAddr)
  • SEG_MASK: 가상 주소에서 세그멘트 번호를 추출하기 위한 비트 마스크다. 이 마스크를 가상 주소와 AND 연산하면 세그멘트 번호만 남게 된다.
  • SEG_SHIFT: 세그먼트 번호를 오른쪽으로 시프트 하여 배열 인덱스로 사용하기 위한 시프트 양이다. 이 값은 세그먼트 번호를 나타내는 비트 수와 같다.
  • OFFSET_MASK: 가상 주소에서 세그멘트 내 오프셋을 추출하기 위한 비트 마스크다. 이 마스크를 가상 주소와 AND 연산하면 오프셋만 남게 된다.

우리는 현재 예를 기준으로 위 코드에서 사용된 상수 값들을 설정할 수 있다. SEG_MASK는 0x3000, SEG_SHIFT는 12, 그리고 OFFSET_MASK는 0 xFFF로 지정된다. 세그먼트 종류를 나타내는 데 최상위 2비트를 사용하고, 주소 공간에는 코드, 힙, 스택 세그먼트만 존재하기 때문에 지정 가능한 세그멘트 하나가 미사용으로 남게 된다. 즉, 전체 주소 공간의 1/4은 사용이 불가능하다. 이 문제를 해결하기 위해 일부 시스템은 코드와 힙을 하나의 세그멘트에 저장하고 세그멘트 선택을 위해 1비트만 사용한다.

특정 주소의 세그먼트를 하드웨어적으로 파악하는 다른 방법들도 있다. 묵시적(implicit) 접근 방식에서는 주소가 어떻게 형성되는지를 관찰하여 세그먼트를 결정한다. 예를 들어, 주소가 프로그램 카운터에서 생성된다면 해당 주소는 코드 세그멘트 내에 있을 것이다. 주소가 스택 또는 베이스 포인터에 의해 생성된다면 주소는 스택 세그멘트 내에 있을 것이다. 다른 주소는 모두 힙에 위치하고 있어야 한다.


스택

스택은 다른 세그먼트들과는 다르게 확장 방향이 반대라는 중요한 차이가 있다. 이에 따라 다른 방식의 주소 변환이 필요하다. 첫 번째, 간단한 하드웨어가 추가로 필요하다. 베이스와 바운드 값뿐만 아니라 하드웨어는 세그먼트가 어느 방향으로 확장하는지도 알아야 한다. 예를 들어, 하나의 비트를 사용하여 주소가 양의 방향으로 확장되는 경우에는 1로 설정하고, 음의 방향으로 확장되는 경우에는 0으로 설정할 수 있다.

  • 방향 비트(Direction Bit): 세그먼트가 양의 방향으로 확장되는지 음의 방향으로 확장되는지를 나타내는 비트, 이 비트를 통해 스택과 같이 음의 방향으로 확장되는 세그멘트를 지원할 수 있다.

하드웨어는 세그먼트가 반대 방향으로 확장될 수 있다는 것을 알기 때문에, 이러한 가상 주소에 대해서는 다른 방식으로 변환한다. 이 예에서 가상 주소 15 KB에 접근하려고 한다고 가정할 때, 이 주소는 물리 주소 27 KB에 매핑되어야 한다. 이 가상 주소를 이진 형태로 변환하면 11 1100 0000 0000 (16진수 0x3 C00)이 된다. 하드웨어는 상위 2비트 (11)를 사용하여 세그먼트를 지정한다. 이를 고려하면 3 KB의 오프셋이 남는다. 올바른 음수 오프셋을 얻기 위해 3 KB에서 세그먼트 최대 크기를 빼야 한다. 이 예에서는 세그먼트의 최대 크기가 4 KB이므로 올바른 오프셋은 3 KB에서 4 KB를 뺀 -1 KB다. 이 음수 오프셋 (-1 KB)을 베이스 (28 KB)에 더하면 올바른 물리 주소 27 KB를 얻게 된다. 바운드 검사는 음수 오프셋의 절댓값이 세그먼트의 크기보다 작다는 것을 확인하여 계산할 수 있다.


공유 지원

세그멘테이션 기법이 발전함에 따라 시스템 설계자들은 간단한 하드웨어 지원으로 새로운 종류의 효율성을 성취할 수 있다는 것을 깨달았다. 구체적으로, 메모리를 절약하기 위해 때로는 주소 공간들 간에 특정 메모리 세그먼트를 공유하는 것이 유용하다. 특히, 코드 공유가 일반적이며, 현재 시스템에서도 널리 사용되고 있다.

이러한 공유를 지원하기 위해, 하드웨어에 보호 비트(protection bit)의 추가가 필요하다. 각 세그먼트에 보호 비트를 추가하여 세그먼트를 읽거나 쓸 수 있는지, 혹은 세그멘트의 코드를 실행할 수 있는지를 나타낸다. 코드 세그멘트를 읽기 전용으로 설정하면 주소 공간의 독립성을 유지하면서도, 여러 프로세스가 주소 공간의 일부를 공유할 수 있다. 각 프로세스는 여전히 자신의 전용 메모리를 사용한다고 생각하지만, 운영체제는 이러한 변경이 불가능하도록 설정된 메모리 영역을 비밀리에 공유하여 그러한 환상을 유지한다.

  • 보호 비트(Protection Bit): 각 세그먼트에 할당된 비트로, 세그멘트에 대한 접근 권한을 나타낸다. 일반적으로 읽기, 쓰기, 실행 권한을 나타내는 비트들로 구성된다. 보호 비트를 통해 세그멘트 단위로 메모리 보호를 구현할 수 있다.

하드웨어 (및 운영체제)가 유지하는 부가 정보의 예시가 그림에 있다. 코드 세그먼트는 읽기 및 실행으로 설정되어, 같은 물리 세그먼트가 여러 가상 주소 공간에 매핑될 수 있다.

보호 비트를 사용하면 앞서 언급한 하드웨어 알고리즘이 수정되어야 한다. 가상 주소가 범위 내에 있는지 확인하는 것뿐만 아니라 특정 액세스가 허용되는지를 확인해야 한다. 사용자 프로세스가 읽기 전용 페이지에 쓰기를 시도하거나 실행 불가능한 페이지에서 실행하려고 할 때 하드웨어는 예외를 발생시켜서 운영체제가 위반 프로세스를 처리할 수 있도록 해야 한다.


소단위 대 대단위 세그멘테이션

우리 예제의 대부분은 지금까지 소수의 세그멘트 (예: 코드, 스택, 힙)만을 지원하는 시스템에 주로 초점을 맞추고 있다. 이러한 세그멘테이션은 대단위(coarse-grained)로 간주할 수 있다. 이는 주소 공간을 비교적 큰 단위의 공간으로 분할하기 때문이다. 그러나 일부 초기 시스템 (예: Multics)은 주소 공간을 작은 크기의 공간으로 잘게 나누는 것이 허용되어 소단위(fine-grained) 세그멘테이션이라고도 한다.

많은 수의 세그먼트를 지원하기 위해서는 여러 세그먼트의 정보를 메모리에 저장할 수 있는 세그먼트 테이블과 같은 하드웨어가 필요하다. 세그멘트 테이블을 이용하면 매우 많은 세그먼트를 손쉽게 생성하고 융통성 있게 세그멘트를 사용할 수 있다. 예를 들어, Burroughs B5000과 같은 초창기 시스템은 수천 개의 세그멘트를 지원했고, 컴파일러가 코드나 데이터를 여러 세그멘트로 분할할 경우 운영체제와 하드웨어가 이를 지원했다. 당시의 생각은 소단위 세그멘트로 관리하는 것이 운영체제가 사용 중인 세그멘트와 미사용인 세그멘트를 구분하여 메인 메모리를 더 효율적으로 활용할 수 있다는 것이었다.

  • 세그먼트 테이블(Segment Table): 각 세그멘트의 정보를 저장하는 자료구조다. 세그멘트 테이블은 세그멘트의 베이스 주소, 크기, 보호 비트 등을 포함한다. 세그멘트 테이블을 통해 많은 수의 세그멘트를 효율적으로 관리할 수 있다.

운영체제의 지원

시스템이 각 주소 공간 구성 요소를 별도로 물리 메모리에 재배치하기 때문에 전체 주소 공간이 하나의 베이스-바운드 쌍을 가지는 간단한 방식에 비해 물리 메모리를 효율적으로 절약할 수 있다. 특히, 스택과 힙 사이의 사용하지 않는 공간에 물리 메모리를 할당할 필요가 없어져서 같은 크기의 물리 메모리에 더 많은 주소 공간을 탑재할 수 있다.

그러나 세그멘테이션은 새로운 많은 문제를 제기한다.

첫 번째 문제는 오래된 문제다. 문맥 교환 시 운영체제가 수행해야 하는 작업은 무엇일까? 바로 세그멘트 레지스터의 저장과 복원이다. 각 프로세스는 자신의 가상 주소 공간을 가지며, 운영체제는 프로세스가 다시 실행하기 전에 레지스터들을 올바르게 설정해야 한다.

두 번째로, 더욱 중요한 문제는 미사용 중인 물리 메모리 공간의 관리다. 새로운 주소 공간이 생성되면 운영체제는 이 공간의 세그멘트를 위한 비어있는 물리 메모리 영역을 찾을 수 있어야 한다. 이전에는 각 주소 공간의 크기가 동일하다고 가정했지만, 이제는 프로세스가 많은 세그멘트를 가질 수 있고, 각 세그멘트의 크기도 다를 수 있다.

일반적으로 발생할 수 있는 문제는 물리 메모리가 빠르게 작은 크기의 빈 공간들로 채워진다는 것이다. 이 작은 빈 공간들은 새로이 생겨나는 세그먼트에 할당하기도 힘들 뿐만 아니라 기존 세그멘트를 확장하는 데에도 도움이 되지 않는다. 이러한 문제를 외부 단편화(external fragmentation)라고 부른다.

  • 외부 단편화(External Fragmentation): 메모리에 할당되지 않은 작은 빈 공간들이 여러 곳에 산재해 있는 현상을 말한다. 이러한 작은 빈 공간들은 새로운 메모리 할당 요청을 만족시키기에는 충분히 크지 않아서 메모리 낭비를 초래한다. 세그멘테이션에서는 가변 크기의 세그멘트를 할당하므로 외부 단편화가 발생할 수 있다.

이 예에서 새로운 프로세스가 생성되어 20 KB를 할당하려고 한다. 위 예에서 24 KB의 빈 공간이 존재하지만 하나의 연속된 공간이 아닌 세 개의 청크(chunk)로 나누어져 있다. 운영체제는 20 KB의 요청을 충족시킬 수 없다.

이 문제의 해결책 중 하나는 기존의 세그멘트를 정리하여 물리 메모리를 압축(compact)하는 것이다. 예를 들어, 운영체제는 현재 실행 중인 프로세스를 중단하고, 그들의 데이터를 하나의 연속된 공간에 복사한 후, 세그멘트 레지스터가 새로운 물리 메모리 위치를 가리키게 하여, 자신이 작업할 큰 빈 공간을 확보한다. 이렇게 함으로써 운영체제는 새로운 할당 요청을 충족시킬 수 있다. 그러나 세그멘트 복사는 메모리에 부하가 큰 연산이고 일반적으로 상당량의 프로세서 시간을 사용하기 때문에 압축은 비용이 많이 든다. 압축 작업 후의 물리 메모리는 그림 19.6(오른쪽)에 나와 있다.

  • 압축(Compaction): 메모리에 할당된 세그먼트들을 한쪽으로 모아서 연속적인 빈 공간을 만드는 작업이다. 압축을 통해 외부 단편화를 해소할 수 있지만, 모든 세그먼트를 이동시켜야 하므로 오버헤드가 발생한다.

간단한 방법은 빈 공간 리스트를 관리하는 알고리즘을 사용하는 것이다. 빈 공간 관리 알고리즘은 할당 가능한 메모리 영역을 리스트 형태로 유지한다. 최적 적합(best-fit), 최악 적합(worst-fit), 최초 적합(first-fit), 버디 알고리즘(buddy algorithm) 등 여러 가지 방식이 있다. 이 중 최적 적합은 빈 공간 리스트에서 요청된 크기와 가장 비슷한 크기의 공간을 할당한다. 알고리즘이 아무리 정교하게 동작한다 해도 외부 단편화는 여전히 존재하며, 좋은 알고리즘은 외부 단편화를 최소화하는 것이 목표다.