컴퓨터 이론/CS:APP

[CS:APP] Chap 9.1 - 9.8 : Virtual Memory

pwerty 2025. 4. 23. 22:54

가상 메모리가 사용된 뒷 배경을 먼저 보자. 주된 내용은 메모리 공유에 있어 어려움을 겪었다는 것이다.

  • 여러 프로세스가 CPU와 메모리를 공유했고, CPU는 수유가 증가해도 점진적으로 느려지지만 메모리는 한계가 명확했다.
  • 메모리가 부족하면 일부 프로세스는 실행을 할 수 없다.
  • 마구 우겨넣겠다고 되는것도 아니지. 한 프로세스가 다른 프로세스의 메모리를 침범하면 원하는 작동도 안되는데다가, 그 결과는 예측 할 수가 없다.

 

  • 가상 메모리의 주요 기능 3가지는 이와 같다.
    1. 효율성 : 물리 메모리를 디스크에 있는 주소 공간의 캐시처럼 사용한다. 필요한 데이터만 메모리에 유지한다.
    2. 단순성 : 각 프로세스에 균일한 주소 공간을 제공한다. → 즉, 이것은 메모리 관리가 쉬워지게 한다.
    3. 보호 : 각 프로세스의 주소 공간은 다른 프로세스로부터 격리된다.

 

  • 프로그래머가 왜 알아야 할까?
    • 가상 메모리는 시스템 전반에 걸쳐 핵심적인 개념이다.
      • 예외 처리, 링커, 로더, 공유 객체 등과 깊게 연결 되어있기 때문이다.
    • 가상 메모리는 강력한 기능을 제공한다.
      • 디스크 파일을 메모리처럼 다를 수 있다.
    • 가상 메모리는 위험 할 수 있다.
      • 포인터의 잘못된 사용은 segmantation fault 같은 오류를 본다.
      • 원인을 찾기 어려운 오류도 많다.

우리는 가상 메모리의 작동 원리를 이해하고 응용 프로그램에서의 사용 방법을 습득 하고, 메모리 관련 버그를 피하는 방법을 볼 것이다.

9.1 Physical and Virtual Addressing

컴퓨터 시스템의 메모리는 M개의 연속적인 바이트 크기 셀로 구성된 배열로 조직되어 있다.

  • 각 바이트는 고유한 물리 주소를 가진다.
    • 첫 번째 바이트의 주소는 0이고, 다음 바이트는 1, 그 다음은 2 이런 식이다.
    • 이처럼 단순한 구조를 고려할 때, CPU가 메모리에 접근하는 가장 자연스러운 방법은 물리 주소를 사용하는 것이다.
    • 이 방식을 물리 주소 지정 (Physical Addressing)이라고 한다. 

  • 위에서 볼 수 있는 Figure 9.1 그림은 물리 주소 지정을 사용하는 예시이다.
    • 물리 주소 4번지부터 시작하는 4바이트 단어를 읽는 load 명령의 맥락을 보여준다.
    • CPU가 해당 load 명령을 실행하면, 효과적인 물리 주소를 생성하여 메모리 버스를 통해 메인 메모리에 전달한다.
    • 메인 메모리는 물리 주소 4번지부터 시작하는 4바이트 단어를 가져와 CPU에 반환하며, CPU는 이것을 레지스터에 저장한다.

 

초기의 PC는 물리 주소 지정을 사용했으며, DSP, 임베디드 MCR, Cray 슈퍼컴퓨터 같은 시스템은 여전히 이 방식을 사용한다.

  • 그러나 현대의 프로세서는 가상 주소 지정이라는 방식을 사용한다. 이에 대한 예는 바로 아래 있는 Figure 9.2 그림에서 볼 수 있다.

  • 가상 주소 지정에서는 CPU가 메인 메모리에 접근 할 때 가상 주소를 생성하고, 이 주소는 메인 메모리에 전달되기 전에 적절한 물리주소로 변환된다.
    • 이러한 가상 주소를 물리 주소로 변환하는 작업을 주소 변환이라고 한다.
  • 예외 처리와 마찬가지로, 주소 변환에는 CPU 하드웨어와 운영체제 간의 긴밀한 협력이 필요하다.
    • CPU 칩 내의 메모리 관리 장치(MMU)라 불리는 전용 하드웨어가 메인 메모리에 저장된 변환 테이블을 참조하여 실시간으로 가상 주소를 변환한다. 이 테이블의 내용은 운영체제가 관리한다.

9.2 Address Spaces

주소 공간(Address space)는 0 이상의 정수 주소들로 이루어진 순서 있는 집합이다. {0, 1, 2, …}

  • 이 주소 공간의 정수들이 연속적이라면, 우리는 이를 선형 주소 공간(linear address space)이라고 부른다.
  • 가상 메모리를 사용하는 시스템에서, CPU는 N = 2^n 개의 주소를 가지는 가상 주소 공간으로부터 가상 주소를 생성한다. {0, 1, 2, ..., N - 1}
    • 주소 공간의 크기는 가장 큰 주소를 표현하기 위해 필요한 비트 수로 특징지어진다.
      • 예를 들어 N = 2^n 개의 주소를 가진 가상 주소 공간은 n비트 주소 공간이라고 불린다.
      • 현대 시스템은 일반적으로 32비트 혹은 64비트 가상 주소 공간을 지원한다.
    • 시스템은 또한 물리 메모리의 M 바이트에 해당하는 물리 주소 공간도 가진다. {0, 1, 2, ..., M - 1}
      • M은 반드시 2의 거듭제곱일 필요는 없지만, 논의를 단순하게 하기 위해 여기서는 M = 2^m이라 가정한다.
  • 주소 공간이라는 개념은 데이터 객체(바이트)와 그것의 속성(주소) 사이를 명확히 구분해주기 때문에 중요하다.
    • 이 구분을 인식하게 되면, 우리는 각 데이터 객체가 서로 독립적인 여러 주소를 가질 수 있도록 일반화 할 수 있다.
  • 이게 가상 메모리 주소의 기본 아이디어이다. 메인 메모리의 각 바이트는 가상 주소 공간에서 선택된 가상 주소와 물리 주소 공간에서 선택된 물리 주소를 가진다.

 

9.3 VM as a Tool for Caching

개념적으로, 가상 메모리는 디스크에 저장된 N개의 연속적인 바이트 크기 셀로 이루어진 배열로 조직된다.

  • 각 바이트는 고유한 가상 주소를 가지며, 이 주소는 배열에 대한 인덱스 역할을 한다.
  • 디스크에 있는 배열의 내용은 메인 메모리에 캐시된다.
  • 메모리 계층에서 다른 캐시들과 마찬가지로, 디스크의 데이터(하위 레벨)은 디스크와 메인 메모리(상위 레벨) 사이의 전송 단위로 사용되는 block으로 분할 된다.

 

  • 가상 메모리 시스템은 이를 처리하기 위해 가상 메모리는 고정 크기 블록으로 나눈다.
    • 이 블록을 가상 페이지라고 하며, 각 가상 페이지는 P = 2^p바이트 크기를 가진다.
    • 마찬가지로, 물리 메모리도 물리 페이지(Physical Pages)로 분할되며, 물리 페이지도 P 바이트 크기이다.
      • 물리 페이지는 page frame 이라고도 불린다.
  • 어떤 시점에서, 가상 페이지의 집합은 세 가지 상호 배타적인 부분 집합으로 나뉘게 된다 :
    1. 할당되지 않음 | Unallocated
      • VM 시스템에 의해 아직 할당되거나 생성되지 않은 페이지이다. 할당되지 않은 블록은 데이터가 없으며, 따라서 디스크에서 공간을 차지하지 않는다.
    1. 캐시됨 | Cached
      • 현재 물리 메모리에 캐시된 할당된 페이지이다.
    1. 캐시되지 않음 | Uncached
      • 물리 메모리에 캐시되지 않은 할당된 페이지이다. 이 예시는 8개의 가장 페이지로 구성된 작은 가상 메모리를 보여준다.
       
    2.  
    • 가상 페이지 0과 3은 아직 할당되지 않았으므로 디스크에 존재하지 않는다. 1, 4, 6은 물리 메모리에 캐시되어있다.
    • 페이지 2, 5, 7은 할당되었지만 현재 물리 메모리에 캐시되지 않았다.

9.3.1 DRAM Cache Organization

메모리 계층 구조에서 서로 다른 캐시들을 명확히 구분하기 위해, 우리는 CPU와 메인 메모리 사이에 위치한 L1, L2, L3 캐시 메모리를 SRAM 캐시라고 부른다.

  • 그리고 가상 페이지를 메인 메모리에 캐시하는 가상 메모리 시스템의 캐시를 DRAM 캐시라고 부른다.

 

DRAM 캐시가 메모리 계층 내에서 차지하는 위치에 따라 그것의 구조에 큰 영향을 미친다.

  • DRAM은 SRAM보다 최소 10배 느리며, 디스크는 DRAM보다 약 100,000배 느리다는 점을 상기하자.
  • 따라서 DRAM 캐시의 miss는 SRAM 캐시 miss에 비해 훨씬 더 큰 비용이 든다.
    • 이는 DRAM 캐시 miss는 디스크에서 데이터를 불러와야 하지만, SRAM 캐시 miss는 보통 DRAM 기반 메인 메모리에서 데이터를 가져오기 때문이다.

 

또한, 디스크 섹터에서 첫 번째 바이트를 읽는 비용은 그 섹터의 연속적인 바이트를 읽는 것보다 약 100,000배 더 크다.

  • 요약하자면, DRAM 캐시의 구조는 miss가 매우 비싸다는 점에 전적으로 영향을 받는다.

 

이러한 큰 miss penalty와 첫 바이트 접근 비용 때문에, 가상 페이지의 크기는 일반적으로 크다. 보통 4KB - 2MB 사이이다.

  • miss penalty가 큰 점 때문에, DRAM 캐시는 fully associative (완전 연관 방식)을 사용한다.
    • 즉, 어떤 가상 페이지도 어떤 물리 페이지에나 배치 될 수 있다.

 

miss시 잘못된 가상 페이지를 교체하게 될 경우 큰 비용이 발생하기 때문에, 교체 정책 역시 중요한 요소가 된다.
여기서 다루지 않지만, 운영체제는 SRAM 캐시보다 훨씬 정교한 교체 알고리즘을 DRAM 캐시에 적용한다는 것을 알아두자.

디스크 접근 시간이 매우 크기 때문에, DRAM 캐시는 항상 write-through가 아닌 write-back 방식을 사용한다.

  • 이게 무 ㅓ임?

 

9.3.2 Page Tables

다른 캐시들과 마찬가지로, 가상 메모리 시스템(VM System)도 어떤 가상 페이지가 DRAM 어딘가에 캐시되어 있는지를 판단 할 수 있는 방법이 필요하다.

  • 만약 캐시되어 있다면, 어느 물리 페이지에 저장되어 있는지를 알아야 한다.
    만약 캐시되어 있지 않다면, 해당 가상 페이지가 디스크의 어디에 저장되어 있는지, 물리 메모리에서 어떤 페이지를 희생시킬지, 그리고 디스크에서 DRAM으로 해당 가상 페이지를 복사하는 작업을 수행해야 한다.

 

이러한 기능은 운영체제, MMU(메모리 관리 장치)에 내장된 주소 변환 하드웨어, 그리고 물리 메모리에 저장된 페이지 테이블이라는 데이터 구조의 협력을 통해 제공된다.

주소 변환 하드웨어는 가상 주소를 물리 주소로 변환할 때마다 페이지 테이블을 참조한다.
운영체제는 페이지 테이블의 내용을 관리하고, 디스크와 DRAM 간에 페이지를 전송하는 책임을 가진다.

  • 이 그림은 페이지 테이블의 기본적인 구성 방식을 보여준다.
    • 페이지 테이블은 페이지 테이블 엔트리(PTE)의 배열로 되어있다.
      • 가상 주소 공간의 각 페이지는 페이지 테이블 내 고정된 오프셋에 해당 PTE를 가진다.
      • 여기서는 각 PTE가 유효 비트와 n비트의 주소 필드로 구성되어 있다고 가정한다.
        • 유효 비트가 설정되어 있으면, 해당 가상 페이지가 현재 DRAM에 캐시되어 있음을 나타낸다.
          • 주소 필드는 가상 페이지가 캐시된 물리 페이지의 시작 주소를 가리킨다.
        • 유효 비트가 설정되어 있지 않으면, 주소 필드가 null이면 아직 할당되지 않은 페이지이다.
          • null이 아니면 해당 가상 페이지가 디스크 상의 어디에 있는지를 나타낸다.
    • 여기서는 8개의 가상 페이지와 4개의 물리 페이지가 있는 시스템의 페이지 테이블을 보여준다.
      • VP 1, 2, 4, 7은 현재 DRAM에 캐시되어 있으며, 0, 5는 아직 할당되지 않았다.
      • VP 3, 6은 할당되었지만 현재 DRAM에 캐시 되어있지 않다.
    • 주목해야 할 것은, DRAM 캐시는 완전 연관 방식이기 때문에 어떤 물리 페이지든 어떤 가상 페이지를 담을 수 있다는 것이다.

 

9.3.3 Page Hits

  • CPU가 가상 메모리 VP 2에 포함된 가상 메모리 상의 워드를 읽는 상황을 생각해보자.
  • 이 페이지는 DRAM에 캐시되어 있다. 이후에 설명할 기술을 사용하여, 주소 변환 하드웨어는 가상 주소를 인덱스로 사용해 PTE 2를 찾아 메모리에서 읽는다.
    • 유효 비트가 설정되어 있으므로, 주소 변환 하드웨어는 VP 2가 DRAM에 캐시되어 있음을 알 수 있다.
    • 따라서 PTE 안의 물리 메모리 주소(이 주소는 PP 1에 캐시된 페이지의 시작을 가리킨다)를 사용하여, 해당 워드의 물리 주소를 구성한다.

 

9.3.4 Page Faults

DRAM 캐시 미스인 경우를 page fault 라고 부른다.

  • 이 그림은 페이지 폴트가 발생하기 이전의 예시 페이지 테이블 상태를 보여준다.

 

  • CPU는 VP 3의 워드를 참조했지만, 이는 DRAM에 캐시되어 있지 않다.
    • 주소 변환 하드웨어는 PTE 3을 메모리에서 읽고, **유효 비트(valid bit)**가 꺼져 있는 것을 확인하여 VP 3이 캐시되어 있지 않음을 알아낸다.
    • 그 결과, 페이지 폴트 예외(page fault exception)가 발생한다.
      • 이 예외는 **커널의 페이지 폴트 예외 처리기(page fault exception handler)**를 호출하며, 커널은 대체할 희생 페이지(victim page)를 선택한다—이 경우 PP 3에 저장된 VP 4이다.
      • 만약 VP 4가 수정된 상태라면, 커널은 그 내용을 디스크에 다시 기록한다.

 

  • 그 다음, 커널은 VP 4가 더 이상 메인 메모리에 존재하지 않음을 반영하기 위해 페이지 테이블 항목을 수정한다.
    • 그 후 커널은 디스크에서 VP 3을 읽어 PP 3에 복사하고, PTE 3을 갱신한 뒤, 예외 처리기를 종료한다.
    • 처리기가 반환되면, 페이지 폴트를 일으킨 명령어가 재시작되며, 동일한 가상 주소가 다시 주소 변환 하드웨어로 전달된다.
  • 하지만 이제는 VP 3이 메인 메모리에 캐시되어 있으므로, 이번에는 페이지 히트(page hit)로 정상 처리된다.

 



  • 위 그림은 페이지 폴트 이후의 페이지 테이블 상태를 보여준다.
     
    • 가상 메모리는 1960년대 초에 고안되었으며, 이는 CPU - 메모리 간의 속도 격차로 인해 SRAM 캐시가 등장하기 이전이었다.
      • 그 결과, 가상 메모리 시스템은 SRAM 캐시와는 다른 용어 체계를 사용하지만, 많은 개념이 유사하다.
      • 가상 메모리 용어에서 블록은 페이지라고 한다.
      • 디스크와 메모리 사이에 페이지를 전송하는 행위는 swapping 또는 paging 이라고 한다.
      • 페이지가 디스크에서 DRAM으로 옮겨지는 것은 swap in 또는 page in 이라고 한다.
      • 반대로 DRAM에서 디스크로 옮겨지는 것은 swap out 또는 page out 이라고 한다.
    • 그리고 미스가 발생 할 때까지 기다렸다가 그 시점에 페이지를 스왑 인 하는 전략을 수요 페이징 (demand paging)이라고 한다.
      • 참조되지 전에 미스를 예측하여 페에지를 미리 스왑인하려는 다른 접근도 가능하지만, 현대의 모든 시스템은 수요 페이징을 사용한다.

9.3.5 Allocating Pages

  • 아래 그림은 운영체제가 새로운 가상 메모리 페이지를 할당 할 때 예시 페이지 테이블에 미치는 영향을 보여준다.
    • 예를 들어 malloc 호출의 결과로 할당되는 경우이다.
    • 여기서는 VP 5가 할당되며, 이를 위해 디스크 상에 공간을 생성하고 PTE 5를 갱신하여 새로 생성된 디스크 상의 페이지를 가리키도록 설정한다.

9.3.6 Localtity to the Rescue Again

개념을 배우는 과정에서 많은 사람들은 가상 메모리가 매우 비효율적일 것이라는 첫 인상을 받는다.

  • 큰 miss penalty는 페이징이 프로그램 성능을 더 나쁘게 만들 것이라고 걱정하게 된다.
  • 하지만 실제로 가상 메모리는 잘 작동하는 것이 그 주된 이유는 지역성 떄문이다.

프로그램이 전체 실행 동안 참조하는 고유의 페이지 수가 물리적 메모리 크기를 초과 할 수는 있다.

  • 지역성의 원칙은 어느 시점에서든 프로그램이 작은 활성 페이지 집합인 작업 집합 또는 레지던트 집합을 처리한다고 예측한다.
  • 작업 집합이 메모리에 로드된 후 초기 오버헤드가 발생하면, 이후 작업 집합에 대한 참조는 hit로 처리되며 추가적인 디스크 트래픽이 발생하지 않는다.

프로그램이 좋은 시간적 지역성(temporal locality)를 가질 때, 가상 메모리 시스템은 매우 잘 작동한다.

  • 하지만 물론, 모든 프로그램이 좋은 시간적 지역성을 보이는 것은 아니다.
  • 작업 집합의 크기가 물리적 메모리 크기를 초과하면, 프로그램은 thrashing 이라고 알려진 불행 상황을 초래 할 수 있는데,
    • thrashing은 페이지가 지속적으로 swap in과 swap out을 반복하는 현상이다.
      가상 메모리는 대개 효율적이지만, 프로그램의 성능이 느려진다면, 현명한 프로그래머는 그것이 thrashing 일 가능성을 고려해야한다.

9.4 VM as a Tool for Memory Management

지금까지 우리는 단일 페이지 테이블이 단일 가상 주소 공간을 물리적 주소 공간에 매핑하는 방식으로 가정했었다.실제로 운영체제는 각 프로세스마다 별도의 페이지 테이블과 별도의 가상 주소 공간을 제공한다.

  • 여기서 기본 아이디어를 볼 수 있다.  
  • 예시에서, 프로세스 i의 페이지 테이블은 VP 1을 PP 2에 매핑하고 VP 2를 PP 7에 매핑한다.
  • 마찬가지로, 프로세스 j의 페이지 테이블은 VP 1을 PP 7에, VP 2를 PP 10에 매핑한다.
    • 여러 가상 페이지가 동일한 공유 물리적 페이지에 매핑 될 수 있음을 주목하자.
  • 요구 페이징과 별도의 가상 주소 공간의 결합은 시스템에서 메모리가 사용되고 관리되는 방식에 깊은 영향을 미친다.
    • 특히, 가상 메모리는 링크 및 로딩의 단순화, 코드 및 데이터 공유, 애플리케이션에 대한 메모리 할당을 간소화한다.

링크 단순화

  • 별도의 주소 공간을 사용하면 각 프로세스는 코드와 데이터가 실제로 물리적 메모리에 어디에 위치하든 상관없이 동일한 기본 메모리 이미지 포맷을 사용 할 수 있다.
    • 주어진 리눅스 시스템의 모든 프로세스는 유사한 메모리 포맷을 가진다.
    • 64비트 주소 공간에서는 코드 세그먼트가 항상 가상 주소 0x400000에서 시작한다.
    • 데이터 세그먼트는 적절한 정렬 간격을 두고 코드 세그먼트 뒤에 위치한다.
    • 스택은 사용자 프로세스 주소 공간의 가장 높은 부분을 차지하고 아래로 성장한다.
    • 이런 일관성은 링커 설계와 구현을 크게 단순화하여 코드와 데이터가 물리적 메모리에서 최종적으로 위치하는 곳에 독립적인 완전 링크 실행 파일을 생성 할 수 있게 한다.

로딩 단순화

  • 가상 메모리는 실행 파일과 공유 객체 파일을 메모리에 로드하는 것도 쉽게 만든다.
    • 객체 파일의 .text와 .data 세그먼트를 새로 생성된 프로세스에 로드하기 위해,
      • 코드와 데이터 세그먼트에 대한 가상 페이지를 할당하고 이를 유효하지 않음으로 표시한 후,
      • 해당 페이지 테이블 항목을 객체 파일의 적절한 위치를 가리키도록 설정한다.
    • 중요한 점은 로더가 실제로 디스크에서 메모리로 데이터를 복사하지 않는다는 것이다.
      • 데이터는 각 페이지가 처음 참조 될 때, CPU가 명령어를 가져올 때나 실행중인 명령어가 메모리 위치를 참조 할 때 가상 메모리 시스템에 의해 자동으로 페이지 인 된다.
  • 이런 방식으로 연속된 가상 페이지 세트를 임의의 위치에 있는 임의의 파일을 매핑하는 개념을 메모리 매핑이라고 한다.
    • 리눅스는 응용 프로그램이 자체 메모리 매핑을 할 수 있도록 하는 mmap이라는 시스템 호출을 제공한다.
    • 애플리케이션 수준의 메모리 매핑에 대해서는 이후에 설명한다.

공유 단순화

  • 별도의 주소 공간은 운영 체제에 사용자 프로세스와 운영체제 자체간의 공유를 관리하는 일관된 메커니즘을 제공한다.
    • 일반적으로 각 프로세스는 다른 프로세스와 공유되지 않는 고유한 코드, 데이터, 힙, 스택 영역을 가진다.
    • 이 경우 운영체제는 각 프로세스에 대해 해당 가상 페이지를 서로 다른 물리적 페이지에 매핑하는 페이지 테이블을 생성한다.
  • 그러나 일부 경우, 프로세스가 코드를 공유 할 수 있도록 하는 것이 바람직하다.
    • 예를 들어, 모든 프로세스는 동일한 운영 체제 커널 코드를 호출해야 하며, 모든 C 프로그램들은 printf 와 같은 표준 C 라이브러리의 루틴을 호출한다.
    • 커널과 표준 C 라이브러리를 각 프로세스에 별도로 포함하는 대신, 운영 체제는 여러 프로세스가 이 코드를 단일 복사본으로 공유하도록 할 수 있다.
    • 이를 위해 적절한 가상 페이지를 서로 다른 프로세스에서 동일한 물리적 페이지에 매핑하는 방식으로 위의 그림 9.9와 같이 구현한다.

메모리 할당 단순화

  • 가상 메모리는 사용자 프로세스에 추가 메모리를 할당하는 간단한 메커니즘을 제공한다.
  • 사용자 프로세스에서 프로그램이 추가적인 힙 공간을 요청하면, 운영체제는 적절한 수 (k)의 연속적인 가상 메모리 페이지를 할당하고 이를 물리적 메모리의 임의의 k개의 페이지에 매핑한다.
  • 페이지 테이블의 방식 덕분에 운영 체제는 물리적 메모리에서 k개의 연속된 페이지를 찾아야 할 필요가 없다.
  • 페이지들은 물리적 메모리에서 무작위로 분산 될 수 있다.

9.5 VM as a Tool for Memory Protection

  • Memory Access Control | 메모리 접근 제어
    • 운영체제는 메모리 접근을 제어하는 수단을 제공한다.
    • 사용자 프로세스는 읽기 전용 코드 섹션을 수정 할 수 없어야한다.
    • 또, 사용자 프로세스는 커널의 코드 및 데이터 구조를 읽거나 수정 할 수 없어야 한다.
    • 또, 사용자 프로세스는 다른 프로세스의 개인 메모리를 읽거나 쓸 수 없어야 한다.
    • 또, 사용자 프로세스는 명시적 IPC 호출이 없는 한 공유 페이지를 수정 할 수 없어야 한다.
  • Isolation via Separate VA Spaces | 분리된 가상 주소 공간
    • 운영체제는 프로세스 별로 구분된 가상 주소 공간을 제공한다.
    • 주소 변환 하드웨어는 이 분리를 활용하여 메모리를 격리한다.
    • 운영체제는 주소 변환 메커니즘에 세부적 접근 제어 기능을 추가 할 수 있다.
  • PTE Permission Bits Extension | PTE 권한 비트 확장
    • 주소 변환 하드웨어는 CPU가 가상 주소를 생성 할 때마다 해당 페이지의 PTE를 읽는다.
    • 운영체제는 PTE에 SUP, READ, WRITE 세 가지 권한 비트를 정의한다.
    • SUP 비트는 커널 모드에서만 페이지 접근을 허용할지를 결정한다.
    • READ 비트는 페이지 읽기 권한, WRITE 비트는 페이지 쓰기 권한을 제어한다.
  • Violation & Exception Handling | 권한 위반 및 예외 처리
    • CPU는 권한 비트 위반 명령 실행 시 일반 보호 예외를 발생시킨다.
    • 커널 예외 처리기는 해당 프로세스에 SIGSEGV 신호를 보낸다.
    • Linux 셸은 이 신호를 “segmentation fault”로 보고한다.

9.6 Address Translation

이번 단원에서는 주소 변환의 기본적인 개념을 다룬다. 목표는 가상 메모리를 지원하기 위한 하드웨어의 역할을 이해하는 것이다.

  • 하드웨어 설계자에게는 중요한 타이밍 관련 세부사항들은 생략한다 : 겁나 어렵기 때문이다.
  • 그림 9.11에서 기호 정리를 해두었다.

형식적으로 주소 변환은, 크기가 N인 가상 주소 공간 | VAS 의 각 요소를 크기 M인 물리 주소 공간 | PAS의 요소로 변환하는 함수 MAP으로 표현 할 수 있다.

MAP: VAS → PAS ∪ ∅

  • 즉, 만약 어떤 가상 주소 A에 해당하는 데이터가 실제 물리 메모리에 존재하면 → MAP(A) = A' | A는 물리 주소 A’에 매핑된다
    • 그렇지 않다면 → MAP(A) = ∅ | A에 해당하는 데이터는 물리 메모리에 존재하지 않는다


9.12는 MMU가 페이지 테이블을 사용하여 변환을 실제로 어떻게 하는지를 보여준다.

  • CPU 안에는 페이지 테이블 기준 레지스터 (Page table base register)가 있다.
    • 이 레지스터는 형재 사용 중인 페이지 테이블의 시작 위치를 가리킨다.
  • n비트 크기의 가상 주소는 두 부분으로 나뉜다 :
    • VPO (가상 페이지 오프셋, p비트) : 페이지 안에서의 위치를 말한다.
    • VPN (가상 페이지 번호, n - p비트) : 전체 가상 주소 공간에서 어떤 페이지인지를 말한다.
  • MMU는 VPN 값을 사용해서 해당하는 PTE를 선택한다.
    • VPN 값을 그대로 선택한다. VPN이 5이면 PTE 5을 선택하는 느낌.
  • 최종 물리 주소는!
    • 페이지 테이블로부터 얻은 물리 페이지 번호(PPN)과 가상 주소에서 가져온 VPO를 이어 붙여서 만든다.
    • 참고로, 가상 페이지와 물리 페이지는 크기가 동일한 P 바이트라서, Virtual Page Offset == Physical Page Offset 이다.

페이지 히트 상황 | 그림 9.13 (a)

  • 하드웨어만으로 처리가 가능하다.
  • 처리 순서 :
    1. 가상 주소 생성 → CPU가 가상 주소를 생성하여 MMU에 전달한다.
    2. PTE 주소 계산 → MMU는 해당 주소에 대응하는 PTE의 주소를 계산하고, 그것을 캐시나 메모리에 요청한다.
    3. PTE 검색 → 캐시나 메인 메모리에서 해당 PTE를 MMU로 변환한다.
    4. 물리 주소 생성 → MMU는 PTE 정보를 이용해 물리 주소를 구성한 뒤, 캐시나 메인 메모리에 요청한다.
    5. 데이터 반환 → 캐시나 메인 메모리는 해당 데이터를 CPU로 반환한다.

페이지 폴트 상황 | 그림 9.13 (b)

  • 하드웨어와 운영체제의 협력 처리가 필요한 상황이다.
  • 처리 순서 :
    1. Valid Bit의 검사 실패 → MMU는 PTE의 valid bit가 0임을 확인하고, 페이지 폴트 예외를 발생시킨다. CPU는 커널의 페이지 폴트 핸들러로 제어를 넘긴다.
    2. 희생 페이지 선택 및 페이지 아웃 → 핸들러는 물리 메모리 내에서 교체 대상 페이지(희생 페이지)를 선택한다. 그 페이지가 수정되었을 경우 디스크에 저장한다. (page out)
    3. 새 페이지를 디스크에서 가져온다 (page in) → 디스크로부터 요청한 페이지를 물리 메모리에 적재하고, PTE를 업데이트한다.
    4. 원래 프로세스로 복귀 후 재시도 → 핸들러는 오류가 발생했던 프로세스로 돌아가고, CPU는 문제가 되었던 가상 주소를 다시 MMU로 보낸다. → 이번에는 페이지가 메모리에 있으니 페이지 히트가 발생한다. 결과적으로, 데이터는 CPU로 전달된다.
  • 1 - 3. 동일하다.
    • Valid Bit의 검사 실패 → MMU는 PTE의 valid bit가 0임을 확인하고, 페이지 폴트 예외를 발생시킨다. CPU는 커널의 페이지 폴트 핸들러로 제어를 넘긴다.
    • 희생 페이지 선택 및 페이지 아웃 → 핸들러는 물리 메모리 내에서 교체 대상 페이지(희생 페이지)를 선택한다. 그 페이지가 수정되었을 경우 디스크에 저장한다. (page out)
    • 새 페이지를 디스크에서 가져온다 (page in) → 디스크로부터 요청한 페이지를 물리 메모리에 적재하고, PTE를 업데이트한다.
    • 원래 프로세스로 복귀 후 재시도 → 핸들러는 오류가 발생했던 프로세스로 돌아가고, CPU는 문제가 되었던 가상 주소를 다시 MMU로 보낸다. → 이번에는 페이지가 메모리에 있으니 페이지 히트가 발생한다. 결과적으로, 데이터는 CPU로 전달된다.

9.6.1 Integrating Caches and VM

가상 메모리와 SRAM 캐시를 동시에 사용하는 시스템에서는 캐시 접근 시 가상 주소 / 물리 주소 중 하나를 결정해야한다.

  • 대부분의 시스템은 물리 주소 기반 접근을 선택한다.
    1. 여러 프로세스가 동시에 캐시를 사용하는 상황에서 유리하다.
      • 물리 주소를 사용하면, 서로 다른 프로세스의 페이지라도 캐시에 문제 없이 공존 할 수 있다.
      • 심지어 같은 가상 페이지를 공유하고 있더라도, 캐시 충돌 없이 사용 할 수 있다.
    2. 보호 권한 문제를 캐시에서 처리 할 필요 없다.
      • 주소 변환 과정에서 이미 접근 권한 검사가 완료된다. 캐시 자체에서는 보호 관련 처리를 하지 않아도 된다.

물리 주소 기반 캐시는 그림 9.14와 같이 작동한다 :

  • 가상 주소 → 물리 주소로 변환된다 | MMU가 먼저 주소 변환을 수행한다.
  • 물리 주소로 캐시 탐색 | 변환된 주소로 캐시에 접근한다.
  • PTE도 캐시에 저장 가능하다 | MMU가 사용하는 PTE도 일반 데이터처럼 캐시에 저장 될 수 있다.

9.6.2 Speeding Up Address Translation with a TLB

마참내 TLB가 등장했다! | Translation Lookaside Buffer

CPU는 가상 주소를 생성 할 때마다 MMU는 PTE를 참조해야한다.

  • 하지만, 이 PTE가 메인 메모리에 있다면 수십~수백 사이클이 소모 될 수 있다.
    • 만약 PTE가 L1 캐시에 있다면, 훨씬 빠르게 접근 할 수 있긴 하다.
    • 하지만 많은 시스템은 아예 이 과정을 더 빠르게 처리하기 위해 MMU 안에 작은 캐시인 TLB를 둔다.

TLB

  • TLB | Translation Lookaside Buffer는 PTE만을 위한 고속 캐시이다.
  • MMU 내부에 존재하며, 보통 높은 연관도(associativity)를 가진다.
  • 각 라인은 하나의 PTE를 저장한다.
  • 가상 주소의 Virtual Page Number로부터 TLB 인덱스와 태그를 생성해 탐색한다. (그림 9.15)
    • 만약 TLB가 총 T = 2^t 개의 set를 가진다면
      • TLB Index : VPN의 하위 t비트
      • TLB Tag : VPN의 상위 비트

TLB Hit (TLB에 해당 PTE가 있는 경우)

  • TLB 히트는 보통의 경우이며, 그림 9.16처럼 빠르게 처리된다.
    1. CPU가 가상 주소를 생성한다.
    2. ~ 3. MMU가 TLB에서 해당 PTE를 가져온다
    3.  (4) MMU가 가상 주소를 물리 주소로 변환해 캐시/메인 메모리에 전달한다.
    4.  (5) 캐시/메모리가 요청된 데이터를 CPU에 반환한다.
    → 모든 과정이 MMU 내부에서 이뤄지므로 매우 빠르다.

TLB Miss (TLB에 해당 PTE가 없는 경우)

  • TLB에 없다면, MMU는 L1 캐시에서 PTE를 가져와야 한다.
  • 가져온 PTE는 TLB에 저장되고, 이 과정에서 기존 항목이 교체(evict) 될 수 있다.

→ 이후에는 히트로 처리되어 성능이 향상된다.

 

9.6.3 Multi-Level Page Tables

지금까지는 가상 주소를 단일 페이지 테이블 하나를 물리 주소로 변환한다고 가정했다. 그런데 이런 조건을 생각해보자 :

  • 가상 주소 공간은 32비트, 페이지 크기는 4KB, 하나의 PTE 크기는 4바이트이다.
  • 이 경우 전체 페이지 수는
    • 4GB / 4KB = 2^20승, 약 100만 페이지 필요하다. 페이지 테이블은 4바이트 이니, 곱하면 4MB가 필요하다.
  • 프로그램이 실제로 몇 MB만 사용하지만 무조건 4MB짜리 테이블이 메모리에 상주해야하는 비효율이 생겼다. 64비트는 더 심각하다.

이런 *같은 상황을 해결하기 위해 다단계 페이지 테이블(Hierarchical Page Table)이 등장했다.

  • 핵심 아이디어 : 가상 주소 공간을 계층적으로 나눠서, 필요한 부분만 페이지 테이블을 구성하자.
  • 예시 (2-Level Page Table) | 32비트 가상 주소, 페이지 크기 4KB, PTE는 4바이트로 구성한다고 할 때
    • 1단계 | Level 1 테이블
      • 한 PTE가 4MB 가상 주소 영역(= 1,024 페이지)를 담당한다.
      • 전체 4GB 공간을 커버하려면 1,024개의 엔트리가 필요하다.
      • Level 테이블 크기 = 4KB | 딱 페이지 한 개의 크기
    • 2단계 | Level 2 테이블
      • 각 PTE가 4KB 가상 페이지를 매핑한다.
      • 필요한 영역만 만들어진다. 쓰지 않는 영역은 테이블 자체가 없다.
    • 예시 시나리오로 그림 9.17을 보면 :
      • 0 - 1 청크 : 코드와 데이터 (2K 페이지)
      • 2 - 7 청크 : 비어있다 (6K 페이지)
      • 8 청크 : 스택 (1 페이지)
        • 이 구조에서는 실제로는 3개의 Level 2 테이블만 메모리에 존재한다.

메모리 절약 효과

  • 할당되지 않은 주소 범위는 Level 2 테이블 자체가 존재하지 않는다.
  • 항상 메모리에 있어야하는 것은 Level 1 테이블 4KB 하나 뿐이다.
  • 나머지는 필요 할 때 VM 시스템이 페이지 인/아웃을 수행 할 수 있다.

 

그림 9.18을 보면서, k단계로 일반화 할 때..

  • 가상 주소는 VPN_1 | VPN_2 | … | VPN_k | VP0 으로 나뉜다.
  • 각 VPN은 해당 레벨의 페이지 테이블을 인덱싱한다.
  • Level k 테이블의 PTE가 실제 Physical Page Number(PPN)을 포함한다.
  • VPO는 PPO와 동일하고, 주소의 하위 비트로 그대로 사용한다.

왜 이게 쓸만할까?

  • PTE를 최대 k번 접근해야 하므로 느려 보일 수 있다.
  • 하지만 TLB가 여러 레벨의 PTE를 캐싱해주기 때문에 실제로는 빠르다.
  • 실무에서 성능은 단일 페이지 테이블과 거의 비슷하다.

9.6.4 Putting It Together: End-to-End Address Translation

 

작은 시스템에서 TLB와 L1 캐시를 포함한 전체 주소 변환 과정 (end-to-end translation)을 구체적인 예제를 통해 설명한다.

전제조건을 만들고 가겠다 :

  • 가상 주소 폭 : 14비트 (n = 14)
  • 물리 주소 폭 : 12비트 (m = 12)
  • 페이지 크기 : 64Byte = 2^6 → VPO/PPO는 하위 6비트이다.
  • 메모리 단위 : 1Byte
  • TLB : 4-way set associative, 16 entries = 4 sets
  • L1 D-캐시 : 물리주소 기반, direct-mapped, 4B 블록, 16 sets

그림 9.19를 보면 주소 구조를 좀 더 자세하게 볼 수 있다 :

  • 가상 주소 14비트는
    • [13:6] → VPN | 8비트
      • VPN (8비트) = [TLBT(6비트)][TLBI(2비트)]
    • [5:0] → VPO | 6비트
  • 물리 주소 12비트는
    • [11:6] → PPN | 6비트
    • [5:0] → PPO | 6비트
  • 캐시용 물리 주소 분해
    • [11:6] → CT (Cache Tag, 6비트)
    • [5:2] → CI (Cache Index, 4비트)
    • [1:0] → CO (Block Offset, 2비트)

예제에서는 주소 0x03d4 접근을 시도한다!

  1. 가상 주소 0x03d4
    • 16진수 0x03d4 → 이진수 0000 0011 1101 0100
    • 필드가 VPN : 비트 범위는 [13:6], 이진수 값은 00001111, 16진수로는 0x0F이다.
    • 필드가 VPO : 비트 범위는 [5:0], 이진수 값은 010100, 16진수로는 0x14이다

 

  1. TLB 접근
    • TLB Index : VPN의 하위 2비트 → 0x0f & 0b11 = 0x03
    • TLB Tag : VPN의 상위 6비트 → 0x0F >> 2 = 0x03
    • TLB Set 3의 엔트리 중 Tag가 0x3인 엔트리를 검사한다. 예시에서는 TLB Hit 발생 : PPN = 0x0D

3. 물리 주소 생성

  • PPN 0x0D = 001101 (6비트)
  • VPO 0x14 = 010100 (6비트)

→ 물리 주소 : PPN | PPO = 0011 0101 0100 = 0x354

마지막으로 캐시 접근이 이루어진다.

필드
물리 주소 0x354
Cache Block Offset | CO [1:0] : 0x0
Cache Set Index | CI [5:2] = 0x5
Cache Tag | CT [11:6] = 0x0D
  • Set 5의 태그가 0x0D인지 확인한다. 예시에서는 Cache Hit가 발생했고, 오프셋 0의 바이트 값 0x36이 반환된다.

다른 시나리오도 있긴 한데..

  • TLB Miss라면, MMU가 메모리에서 PTE를 로드한다.
  • PTE Invalid이면 Page Fault를 발생시키고 OS의 개입을 유도한다.
  • Cache Miss라면, 메모리에서 블록을 로드한다.

 

9.7 Case Study : The Intel Core i7/Linux Memory System

  1. 주소 공간 및 호환 모드
    • 가상 주소 공간 : 48비트 | 최대 256TB
    • 물리 주소 공간 : 52비트 | 최대 4PB
    • 호환 모드 : 32비트 | 4GB (가상 및 물리 주소 공간)
      • 하스웰 아키텍처는 64비트를 지원하지만, 실제 구현은 48/52비트 수준에서 제한된다.
      • 32비트 모드는 이전 소프트웨어와의 호환을 위해 제공된다.
  2. Core i7 프로세서 구성 | 그림 기반

  • 프로세서 패키지 1개에 4개의 코어가 포함된다.
  • L3 캐시는 모든 코어가 공유된다. DDR3 메모리 컨트롤러가 탑재되었다.
  • 각 코어는 다음과 같은 구조를 가진다 :
    • TLB 계층
    • 데이터 및 명령어 캐시 계층
    • QuickPath 기반 고속 P2P 링크 : 다른 코어 및 I/O 브릿지와 통신한다.

3. TLB 구조 | Virtually Addressed, 4-way Set Associative, 계층적 구조 (L1 TLB, L2 TLB 등)

4. Cache 계층 구조

캐시 레벨 주소 방식 연관도 블록 크기
L1 물리 주소 8-way 64바이트
L2 물리 주소 8-way 64바이트
L3 물리 주소 16-way 64바이트
  • L1, L2, L3 모두 물리 주소 기반 캐시이다. 각 계층마다 연관도와 크기가 다르다.
  • L3는 공유 캐시, 나머지는 각 코어 전용이다.

5. 페이지 크기

  • 페이지 크기를 선택 할 수 있다 : 4KB | 기본, Linux에서 사용, 4MB | 대용량 페이지 옵션

 

9.7.1 Core i7 Address Translation

  • 이 그림은 Core i7의 주소 변환 전체 흐름도를 보여준다.
    • CPU가 가상 주소를 생성한다.
    • TLB를 거치고, miss인 경우 4단계의 페이지 테이블을 탐색한다.
    • 물리 주소를 결정하게 되고, 캐시/메모리에 접근하게 된다.
  • 각 단계가 어떻게 물리 주소에 도달하는지 전체 플로우를 도식화 하게 된다.

  • 이 그림은 Level 1, 2, 3 PTE 구조를 나타낸다.
    • P | Present Bit : 해당 항목이 유효한지를 나타낸다.
    • 40-bit PPN : 다음 레벨 페이지 테이블의 물리적 시작 주소를 말한다.
    • 4KB 정렬이 필요하다. L1, L2, L3 모두 동일한 포맷을 사용한다.

  • 이 그림은 Level 4 PTE 구조를 나타낸다.
    • P = 1이면 유효한 것은 하위 레벨 PTE와 동일하다.
    • 40-bit PPN : 최종 물리 페이지의 시작 주소를 말한다.
    • 이 항목은 실제 물리 메모리를 가리키며, 가상 주소의 최종 매핑 단계이다!
    • 여기도 4KB 정렬이 필요하다.

  • 이 그림은 가상 주소 → 물리 주소로 변환 하는 과정을 나타낸다.
    • 가상 페이지 번호 | Virtual Page Number는 36비트, 9비트 씩 4단계로 나뉜다.
    • 각 VPN 파편(VPN1 - VPN4)은 순차적으로 L1 - L4 테이블을 인덱싱 하게 된다.
    • 최종적으로, L4 PTE에서 물리 주소를 획득하게 된다.

9.7.2 Linux Virtual Memory System

리눅스 가상 메모리 시스템 개요

  • 하드웨어와 커널의 협력
    • 가상 메모리 시스템은 MMU와 같은 하드웨어 등과 운영체제 커널의 협력이 필요하다.
    • 구현 세부사항은 커널 버전마다 상이하다.
    • 이 절에서는 리눅스의 가상 메모리 시스템의 실질적인 구조를 개략적으로 이해하는 것이 목적이다.

  • 이 그림은 리눅스 프로세스의 가상 주소 공간을 볼 수 있다.
    • 이 구조는 이미 여러 번 등장했던 프로세스의 주소 공간 그림의 확장된 내용이다.
      • User Space
        • .code, .data. .bss .. Heap.. Shared Lib.. Stack
      • Kernel Space | 유저의 스택 부분 위쪽부터 시작한다
        • 커널 코드 및 커널 데이터 : 모든 프로세스가 공유하는 내용이다.
        • 연속된 가상 페이지 → 연속된 물리 페이지로 매핑된다.
          • 시스템 전체 DRAM 크기 만큼
          • 이 목적은 물리 메모리의 특정 위치에 쉽게 접근하는데에 있다.
            • 페이지 테이블 접근, 메모리 매핑 I/O
        • 프로세스마다 개별적인 영역은 이 내용들이다 :
          • 각 프로세스의 페이지 테이블
          • 커널 스택 (시스템 콜 처리 중 사용)
          • 현재 프로세스의 주소 공간 관리 구조체

리눅스의 Virtual Memory Area 구조를 살펴보자.

  • 일단 VMA가 뭔데?
    • Virtual Memory Area = 영역, 또는 세그먼트
    • 하나의 프로세스는 여러 개의 VMA로 구성된 가상 메모리 공간을 가진다.
    • 각 VMA는 연속된 가상 주소 범위이며, 연관된 목적을 가진 페이지들로 구성된다.
      • 유저 공간에서 봤던 code data heap stack sharedLib..
    • 중요한 특징은 :
      • VMA에 포함되지 않은 가상 주소는 존재하지 않는다. → 접근 할 수 없다.
      • 주소 공간에 빈틈이 생기는 것을 허용한다.
        • 커널은 존재하지 않는 페이지에 대한 정보를 따로 관리하지 않는다.
        • → 메모리, 디스크, 커널 리소스를 절약 할 수 있다.

  • 이 그림은 리눅스 커널의 가상 메모리 추적 구조를 보여준다.
    • 각 프로세스에 대한 커널의 주요 데이터 구조를 도식화한 그림이다.
  • task_struct
    • 각 프로세스마다 하나씩 존재한다. 다음 정보를 포함하거나, 참조한다.
      • PID, 사용자 스택 주소, 실행 파일 이름, Program Counter → 전반적인 실행 정보
  • mm_struct
    • task_struct 가 가리키는 가상 메모리 상태 정보를 말한다.
    • 주요 필드 :
      • pgd : Level 1 페이지의 물리 주소 (Page Global Directory)
        • CPU의 CR3 레지스터에 저장된다.
      • mmap : VMA들의 연결 리스트 시작점 → vm_area_struct 로 구성된다.
  • vm_area_struct : 하나의 VMA
    • 하나의 VMA를 설명하는 구조체이다. 필드는 :
      • vm_start : 영역의 시작 가상 주소이다.
      • vm_end : 영역의 끝 가상 주소이다.
      • vm_prot : 읽기/쓰기 등의 접근 권한 정보를 담는다.
      • vm_flags : 공유 여부 등 기타 플래그가 담겨있다.
      • vm_next : 다음 VMA를 가리키는 포인터가 포함되어 있다 (연결 리스트)

리눅스에서의 페이지 폴드 예외 처리 과정

  1. 주소 자체가 유효합니까? | 주소 범위 검사
    • 주소 A가 프로세스의 가상 메모리 영역에 속하나?
      • 커널은 vm_area_struct 리스트 또는 트리를 순회하여 A가 포함되는지 확인한다.
      • 속하지 않는다면 Segmentation Fault → 프로세스 종료
      • 그림의 상황 1번에서 재현된다.
    • VMA는 리스트로 유지되지만, 실제 구현에서는 검색 속도를 높이기 위해 밸런스 트리를 덧붙인다.
  2. 적합한 시도입니까? 즉, 권한을 가지고 있습니까? | 접근 권한 검사
    • 그 주소에 실제로 R/W/X 권한이 있는가?
      • 코드 세그먼트의 읽기 전용 페이지에 쓰기를 요청한경우?
      • 사용자 모드 프로세스가 커널 메모리 읽기를 시도한경우?
    • 불법 접근이면, Protection Fault → 프로그램 종료
    • 그림의 상황 2번에서 재현된다.
  3. 1, 2를 모두 통과했다?
    • 여기서 커널은 적합한 가상 주소에 적합한 명령을 내렸다는 것을 안다.
    • 페이지 폴트 처리 절차 :
      1. 페이지 교체 정책에 따라 Victim Page 선택
      2. Victim 페이지가 Dirty면 디스크에 쓴다.
      3. 필요한 페이지를 디스크에서 읽어온다. (swap-in)
      4. 페이지 테이블을 갱신한다.
    • 이후 커널은 예외 복귀하며, CPU는 해당 명령어를 재실행한다.
      • 이 때는 MMU가 페이지 테이블에서 주소를 제대로 찾을 수 있다. → 페이지 폴드 없이 정상 수행이 가능

9.8 Memory Mapping

여기선 리눅스에서 가상 메모리 영역의 초기화 및 메모리 매핑 과정을 설명한다.

크게 두 가지 유형의 객체에 매핑 할 수 있다. 정규 파일과 익명 파일.

  1. 정규 파일 매핑
    • 정규 파일은 실행 가능한 오브젝트 파일 같은 것을 말한다. 여기에 가상 메모리 영역을 매핑한다.
    • 파일 섹션을 페이지 크기 단위로 나누어, 각 페이지가 해당 가상 메모리 페이지의 초기 내용을 담도록 설정한다.
      • 수요 페이지 | Demand Paging :
        • 실제 물리적 메모리에 올려지지 않은 페이지는 CPU가 처음 접근할 때 물리 메모리로 로딩된다.
        • 영역이 파일보다 크면, 남는 공간은 0으로 채워진다. (메모리 부족 상황을 방지하기 위해)
  2. 익명 파일 매핑
    • 익명 파일은 커널이 생성하는 모든 값이 0인 파일을 말한다. (즉 값이 모두 binary zeros 인 영역)
    • CPU가 해당 영역의 가상 페이지에 처음 접근하면,
      1. 물리 메모리에서 Victim 페이지를 선택한다.
      2. Dirty 상태인 경우 교체 후, 새로운 페이지에 0 값을 덮어씌운다.
      3. 페이지 테이블을 갱신하여, 해당 페이지를 레지던트 상태로 설정한다.
    • 여기서 중요한 점은..
      • 디스크와 메모리 간 데이터 전송이 없다.
      • 이런 페이지를 Demand-Zero 페이지라고 부른다.

알고 있어야 할 것은 :

  • 스왑 공간 | Swap Space :
    • 페이지가 디스크와 메모리 간 스왑 될 때, 이를 위한 별도의 스왑 파일이 사용된다.
    • 스왑 파일은 현재 실행 중인 프로세스들이 할당 할 수 있는 가상 페이지의 최대 범위를 정한다.
    • 스왑 공간은 시스템의 물리 메모리 크기를 넘어 설 수 있지만, 사용 가능한 가상 메모리의 한계를 설정한다.

9.8.1 Shared Objects Revisited

메모리 매핑이라는 아이디어는 가상 메모리 시스템을 기존 파일 시스템에 통합한다면, 프로그램과 데이터를 메모리에 간단하고 효율적으로 로드 할 수 있다는 기발한 통찰에서 시작했다.

  • 프로세스는 각자 자신의 가상 주소 공간을 가지며, 다른 프로세스의 잘못된 읽기 쓰기로부터 보호받는다.
    • 그러나.. 많은 프로세스는 읽기 전용 코드 영역이 동일하다. 이 부분을 각각의 프로세스가 공유되지 않은채로 두기에는 너무 낭비겠지.
  • 다행히 메모리 매핑은 여러 프로세스가 객체를 공유하는 방법을 깔끔하게 제어 할 수 있는 메커니즘을 제공한다.
    • 객체는 가상 메모리 영역에 공유 객체로 매핑되거나 개인 객체로 매핑 될 수 있다. 만약 프로세스가 공유 객체를가상 주소 공간의 영역에 매핑한다면, 해당 영역에 대해 프로세스가 수행하는 모든 쓰기는 같은 공유 객체를 매핑한 다른 프로세스에게도 보인다.
  • 또한, 공유 객체로 매핑된 영역에 이루어진 변경 사항은 디스크에 있는 원본 객체에도 반영된다.
    • 반면, 개인 객체로 매핑된 영역에 이루어진 변경 사항은 다른 프로세스에게 보이지 않으며, 해당 영역에 대해 프로세스가 쓰기를 하더라도 디스크의 객체에는 반영되지 않는다. 이렇게 공유 객체가 매핑된 가상 메모리 영역을 shared area라고 부른다.

  • 마찬가지로, 개인 영역에도 비슷한 개념이 적용된다.
    • 가령, 프로세스 1이 공유 객체를 자신의 가상 메모리의 한 영역에 매핑했다고 가정해보자. (9.29 (a))
    • 이제 프로세스 2가 동일한 공유 객체를 자신의 주소 공간이 매핑한다고 가정한다.
      • (프로세스 1과 동일한 가상 주소에 매핑 할 필요는 없다 | 9.29 (b)를 확인하기)

  • 각 객체는 고유한 파일명을 가지고 있기 때문에, 커널은 프로세스 1이 이미 이 객체를 매핑했다는 사실을 빠르게 확인 할 수 있다.
    • 그런 다음, 프로세스 2의 PTE를 적절한 물리적 페이지로 연결한다.
    • 중요한 점은, 여러 공유 영역에 매핑되더라도 물리적 메모리에 공유 객체의 단일 복사본만 저장된다는 것이다.
    • 참고로, 편의를 위해 물리적 페이지를 연속적으로 표시했지만, 실제로는 그렇지 않을 수 있다.
  • 개인 객체는 복사 시 쓰기(Copy On Write)라는 기술을 사용해 가상 메모리에 매핑된다.
    • 처음에는 개인 객체도 공유 객체처럼 시작하며, 물리적 메모리에 단일 복사본만 저장된다. 예를 들어 9.30(a)는 두 개의 프로세스가 동일한 개인 객체를 각각의 가상 메모리 영역에 매핑했다.
    • 하지만, 물리적 메모리의 같은 복사본을 공유하는 경우를 볼 수 있다.
  • 개인 객체를 매핑한 각 프로세스에 대해, 해당 개인 영역의 PTE는 read only로 표시된다.
    • 영역 구체는 private Copy On Write로 플래그가 설정된다. 이 상태에서 쓰기를 시도하지 않는 한, 물리적 메모리에 있는 객체 단일 복사본을 공유한다. 하지만, 쓰기를 시도하는 즉시 보호 오류로 프로그램이 종료되겠지?
  • Fault Handler는 프로세스가 private copy on write 영역 페이지에 쓰기를 시도했다는 것을 감지한다. 이 때, 물리적 메모리에 해당 페이지의 새 복사본을 생성하고, PTE를 새 복사본으로 업데이트한다. 또, 페이지에 쓰기 권한을 복원한다.
    • Fault Handler가 작업을 완료하고 돌아오면 CPU는 해당 쓰기를 다시 실행 하고, 새로 생성된 페이지에서 정상적으로 진행된다.
  • 이처럼 페이지 복사를 최대한 늦게까지 미루는 Copy on Write 기술은 부족한 물리적 메모리를 효율적으로 활용 할 수 있게 한다. (9.30(b))

9.8.2 The fork Function Revisited

이제 가상 메모리와 메모리 매핑을 이해했으니, fork() 가 새로운 프로세스를 생성하면서 각 프로세스에 독립적인 가상 주소 공간을 어떻게 제공하는지를 명확하게 이해 할 수 있다.

fork() 가 호출되면, 커널은 새 프로세스를 위해 다양한 데이터 구조를 생성하고, PID를 할당한다.

  • 새로운 프로세스의 가상 메모리 생성을 위해, 현재 프로세스의 mm_struct , 영역 구조체, 페이지 테이블의 복사본을 만든다. 커널은 두 프로세스의 각각의 페이지를 read only로 설정하고, 각 영역 구조체를 private copy-on-write로 플래그 설정한다.

fork() 가 새로운 프로세스에서 반환 될 때, 새로운 프로세스는 fork()가 호출되었던 시점에 존재하던 가상 메모리의 정확한 복사본을 가지게 된다. 이후, 두 프로세스 중 하나가 해당 메모리 영역에 쓰기 작업을 시도하면, copy on write 메커니즘이 새로운 페이지를 생성한다.

이것을 통해 각 프로세스는 독립된 주소 공간의 추상화를 유지 할 수 있다.

9.8.3 The execve Function Revisited

가상 메모리와 메모리 매핑은 메모리 로드 과정에서도 중요한 역할을 한다.

  • execve() 도 이해해보자.
    • execve("a.out", NULL, NULL); 이런식으로 호출한다고 하자.
    • execve() 는 a.out에 담긴 프로그램을 현재 프로세스에서 로드하고 실행한다.
      • 즉, 현재 실행 중인 프로그램이 a.out 프로그램으로 완전히 교체하는 것이다.
      • a.out을 로드하고 실행하려면 다음 단계가 필요하다.
  1. 기존 사용자 영역 삭제 : 현재 프로세스의 가상 주소에서 사용자 부분의 기존 영역 구조체를 지운다.
  2. 개인 영역 매핑 : 새로운 프로그램의 개인 영역을 위한 영역 구조체를 생성한다.
    • 이 모든 영역은 private copy on write로 설정된다.
    • 코드, 데이터는 a.out의 .text .data에 매핑된다.
    • .bss는 익명에 매핑되고, a.out은 정의된 크기로 할당된다. 아래 그림은 이 다양한 매핑을 요약한 것.

 

      3. 공유 영역 매핑 : a.out 프로그램이 libc.so 와 같은 공유 객체로 링크되었다면, 이 공유 객체들은 프로그램에 동적으로 링크되고, 사용자의 가상 주소 공간의 공유 영역에 매핑된다.

      4. Program Counter 설정 : execve() 가 마지막으로 수행하는 작업은, 현재 프로세스의 컨텍스트에서 코드 영역의 진입 지점을 가리키도록 프로그램 카운터를 설정하는 것이다.

      다음에 이 프로세스가 스케줄링 되면, entry point 에서 실행을 시작한다.

      Linux는 필요한 경우 코드 및 데이터 페이지를 스왑하여 불러온다.

      9.8.4 User-Level Memory Mapping with the mmap Function

      Linux 프로세스는 mmap 함수를 사용하여 가상 메모리의 새로운 영역을 만들고 객체의 해당 영역에 매핑 할 수 있다 :

      #include <unistd.h>
      #include <sys/mman.h>
      void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
      
      // Returns: pointer to mapped area if OK, MAP_FAILED (−1) on error
      • mmap 함수의 역할
        • 커널에 요청하여 새로운 가상 메모리 영역을 생성하고, 파일 디스크립터(fd)로 지정된 객체의 연속적인 chunk를 이 새 영역에 매핑한다.
        • 매핑된 객체의 chunk 크기는 length byte이며, 파일의 시작점에서 offset 바이트만큼 떨어진 곳에서 시작한다.
        • start 주소는 단순히 힌트로 사용되며. 일반적으로 NULL 로 지정한다. 본문에서는 항상 NULL 로 가정한다.
      • 9.32 그림은 mmap 함수 인수의 의미를 시각적으로 설명한다.

       

      prot 인수 (접근 권한)

      • PROT_EXEC : CPU가 실행 할 수 있는 명령어가 포함된 페이지이다.
      • PROT_READ : 페이지를 읽을 수 있다.
      • PROT_WRITE : 페이지를 쓸 수 있다.
      • PROT_NONE : 페이지에 접근 불가하다.

       

      flags 인수 (매핑된 객체의 유형)

      • MAP_ANON : 익명 객체를 나타내며, 가상 페이지가 demand-zero로 설정된다.
      • MAP_PRIVATE : 개인 copy on write 객체를 나타낸다.
      • MAP_SHARED : 공유 객체를 나타낸다.

       

      예시

      bufp = Mmap(NULL, size, PROT_READ, MAP_PRIVATE|MAP_ANON, 0, 0);

      커널에 요청하여 크기가 size 바이트인 읽기 전용, 개인, demand-zero 가상 메모리 영역을 생성한다.

      • 성공하면, bufp는 새로운 영역의 주소를 가진다.
      #include <unistd.h>
      #include <sys/mman.h>
      int munmap(void *start, size_t length);

      munmap 함수

      • start 가상 함수에서 시작하여 length 바이트로 구성된 영역을 삭제한다.
      • 삭제된 영역에 대한 이후의 참조는 Segmentation Fault 를 유발한다.
      • 성공 시 0을 반환하고, 실패 시 -1을 반환한다.
      댓글수0