SW Maestro 6th 연수를 진행하면서 멘토님에게 들은
리눅스의 가상메모리 관련 정보를 정리해보고자 작성해봤습니다.
아래글은 Intel Arch와 linux x64 커널울 기준으로 서술한 내용이지만 대부분의 아키텍쳐와 커널레벨에서
유사하게 동작합니다.

가상메모리는?

Intel기준으로 리얼모드(real mode, 16bit)에서 보호모드(protect mode, 32bit)로 넘어가면서 새롭게 생긴 기능으로 ARM, PPC 등 다양한 마이크로 프로세서에서 지원하는 기능입니다. 이 기능을 이용해서 얻을 수 있는 장점은.

  1. 프로세스 마다 독립적인 메모리 공간을 갖을 수 있다.
  2. swap memory를 구현하기 용이하다.
  3. 프로세스간 서로의 메모리를 침범할 수 없게 만든다.

위와 같은 장점이 있습니다.

일반적으로 C언어를 배우면 C언어 책에 전반부에 나오는 흔한 그림입니다. 대학에서 프로그래밍을 배웠다면 한번쯤은 볼 그림입니다.

일반적인 운영체제 위에서 동작하는 프로그램들이라면 위와같이 heap, data, text 와 같은 형식으로 메모리를 나누어서 영역을 할당할 것입니다. 리눅스라면 0x0000000000000000 ~ 0x00007FFFFFFFFFFF 까지 48bit의 영역만을 사용합니다. (따라서 한 프로세스의 가용메모리는 128TB)

한 프로세스가 전체 메모리를 바라보는 시점으로 보면, 위와같습니다. higher half 영역은 커널의 가상메모리 영역이고(이 영역도 128TB다.), lower half는 위에서 말한 48bit의 영역입니다, 가운데에 있는 non-canonical address 영역은 할당이 될 수 없는 영역입니다.

하지만 위와 같은 메모리는 어디까지나 프로세스의 관점이고 물리적인 시스템에 들어갈 경우에는 128TB나 메모리를 갖고 있는 시스템은 그렇게 흔하지 않습니다. 따라서 이는 어디까지나 가상의 메모리 공간이고 이를 물리적 메모리와 매칭 시켜주는 일을 해주는게 우리가 사용중인 운영체제이고 이를 구현하기 위해서 페이징 기법을 활용합니다.

페이징

page는 가상 메모리에서 4KB 사이즈로 분할 하는 단위입니다.(x86에서는 page 크기를 4KB, 2MB, 1GB를 지원하지만 리눅스 시스템에서는 4KB를 기본으로 한다.) 그리고 메모리의 이미지가 디스크 이미지가 아닌 경우( 예. heap, stack 반례. text) anonymous page라 부릅니다. 그리고 이를 매칭하고 있을 물리적인 메모리의 4KB로 분할한 단위를 page frame이라 하는데 이를 매칭하는 기법은 크게 paging과 segmentation 두 가지가 있습니다.

잠깐 segmentation에 대하여 설명하면 프로그래머가(커널 개발 혹은 임베디드 수준의 개발) 논리적으로 연관된 것들을 Global 과 Local 영역으로 분리할 수 있는 기능을 제공합니다.

하지만 Linux에서는 이 segmentation 기법을 극히 제한적으로 사용합니다. 이유는..

  1. 리눅스는 멀티 아키텍쳐를 지원하는 커널이기에 segmentation을 지원하지 않는 다른 아키텍쳐에서 이를 포팅하는 것이 매우 힘들어진다.
  2. 일단 paging이라는 조금 더 진보하고 여러가지 아키텍쳐를 지원하는 기능이 있다.
  3. 세그멘테이션과 페이징 기법을 동시에 사용하면 힘들어진다. 모든 프로세스가 같은 segment를 사용하도록 설정하여 메모리 관리를 쉽게한다.

그래서 리눅스 x86커널에서는 세그멘테이션을 매우 제한적으로 사용하고 있으며 보호 모드 접근시에는 모든 프로세스가 같은 segment를 보게 합니다. x86이 아래와 같이 segmentation을 한번 거쳐서 페이징으로 메모리를 바라보고 있기 때문이다. 세그멘테이션 되기 전의 메모리 주소를 논리주소라 하고 세그멘테이션을 통하여 참조한 주소를 선형주소 이를 페이징 기법을 이용하여 물리주소로 변환합니다.

페이징의 기본 목표는 우선 가상메모리 주소를 실제 물리주소에 할당해주는 것입니다. 우선 아래표를 보자.

CR3는 x86 CPU에 있는 레지스터다. 저 레지스터에는 Page global directory(PDG)를 가르키고 있는 주소를 말합니다. 그리고 가상메모리의 주소에서 상위 9 비트가 테이블을 찾기위한 오프셋으로 활용됩니다. 그리고 이를 통해서 다음 Page upper directory(PUD)와 Page middle directory(PMD)를 그리고 마지막으로 page table(PT)을 찾습니다. 이를 CPU내에 있는 MMU라는 전용 칩이 정상적인 directory인지 확인하고 빠르게 다음 메모리를 확인하게 됩니다. 만약 포멧이 조금이라도 틀리다면 page fault를 interrupt하게 되고 이는 커널이 판단하기에 정상적인 접근이였다면 페이지를 새로 생성하지만 그렇지 않은 접근, 즉 잘못된 참조면 우리가 흔하게 아는 segmentation fault 시그널이 발생하게됩니다. 정상적인 page directory 인지 판단하기 위하여 MMU는 메모리에서 다음 directory를 참조할때는 다음과 같은 포멧인지를 검사하게 됩니다.

위의 표에 보이든 R/W에 관한 권한 최근 사용했는가 등 여러가지 정보를 담고 있다. 자세한 사항은 인텔과 상담하길.. 이를 통하여 해당 메모리에 쓰기 권한 읽기권한등을 판별해 문제되는 사항이 있으면 이 또한 인터럽트 처리되고 위에서 말했듯 참조 도중 문제가 된다면 인터럽트를 발생시킵니다. 일단 리눅스에서는 페이지를 on-demand(lazy)로 생성하기 때문에 page fault가 발생한다고 하여 항상 문제되는 순간은 아닙니다. 아직 생성되지 않은 페이지에 접근하는 것 때문에 발생되는 page fault는 페이지를 생성해 다시 참조를 하는 경우도 있습니다.

위 방식을 구현하려면 우선 CR3에 값이 어떤 값이 들어갈지에 대해서 정해야합니다. 이를 위해서 리눅스는 각각의 프로세스마다 갖고 있는 task_struct에 task_struct->mm->pgd를 참조하여 스케줄러가 context를 switching하는 순간 CR3에 해당 값을 적재합니다.

위에서 말한 용어는 linux에서 사용되는 용어다 현재 리눅스 커널에서는 메모리 참조를 위해서 약 4번의 참조를 거치는데 이는 x86인지 x86 PAE인지 x64인지에 따라 아키텍쳐에서 지원하는 참조회수가 다릅니다.(위의 세그멘테이션 설명에서 사용한 그림만봐도 2회 참조한다)

Architecture PGD PUD PMD PTE
i386 22-31     12-21
i386 (PAE mode) 30-31   21-29 12-20
x86-64 39-46 30-38 21-29 12-20

Reference