본문 바로가기

OS/Linux

리눅스에서 사용되는 메모리 모델을 이해

리눅스 디자인과 구현을 이해하는 첫 번째 단계는 리눅스에서 사용되는 메모리 모델을 이해하는 것이다. 리눅스 메모리 모델과 관리는 매우 중요하다.
리눅스는 슈퍼바이저 모드에서 실행되는 여러 모듈에서 프로세스 관리, 동시성, 메모리 관리 같은 운영 체계 서비스를 구현하기 위해 프리머티브 또는 시스템 호출을 정의하는 방식을 사용한다. 리눅스는 호환성을 위해 상징적 표시로서 세그먼트 제어 단위 모델(segment control unit model)을 관리하더라도 작은 레벨에서는 이 모델을 사용한다.
다음은 메모리 관리와 관련된 문제이다.
  • 가상 메모리 관리, 애플리케이션 메모리 요청과 물리적 메모리간 논리적 레이어
  • 물리적 메모리 관리
  • 커널 가상 메모리 관리/커널 메모리 할당자, 메모리에 대한 요청을 만족시키는 컴포넌트. 요청은 커널 내에서 또는 사용자로부터 온다.
  • 가상 주소 공간 관리
  • 스와핑과 캐싱
이 글에서 운영 체계 내에서의 메모리 관리를 중심으로 설명하겠다.
  • 일반적인 세그먼트 제어 단위 모델과 리눅스의 세그먼트 제어 단위 모델
  • 일반적인 페이징 모델과 리눅스의 페이징 모델
  • 물리적 메모리 영역 상세
리눅스 커널에서 메모리가 어떻게 관리되는지를 상세히 설명하지는 않겠다. 하지만 전체 메모리 모델에 대한 정보와 이것이 어드레싱 되는 방식을 이해하는 것으로도 충분히 도움이 된다. x86 아키텍처에 초점을 맞추지만 다른 하드웨어 구현에도 적용할 수 있다.


페이징 단위는 리니어 어드레스를 물리적 어드레스로 변환한다.(그림 1) 리니어 어드레스들은 한데 묶여 페이지들을 형성한다. 이러한 리니어 어드레스들은 근본적으로 연속적이다. 페이징 단위는 이러한 연속적인 메모리를 페이지 프레임(page frames)이라고 하는 상응하는 연속적인 물리적 어드레스로 매핑한다. 페이징 단위는 램을 시각화 하여 고정된 크기의 페이지 프레임으로 나눈다.
페이징에는 다음과 같은 장점이 있다.
  • 페이지에 정의된 액세스 권한은 페이지를 구성하는 리니어 어드레스 그룹에 좋다.
  • 페이지의 길이는 페이지 프레임 길이와 같다.
페이지들을 페이지 프레임에 매핑하는 데이터 구조를 page table이라고 한다. Page Table들은 주 메모리에 저장되고 페이징 단위를 실행하기 전에 커널에 의해 초기화 된다.

그림 5. Page Table이 페이지들을 페이지 프레임으로 매핑한다. 


Page1에 포함된 어드레스 세트는 Page Frame1에 포함된 상응하는 어드레스 세트와 매칭된다.
리눅스는 세그멘테이션 단위 보다는 페이징 단위를 사용한다. 이전 섹션에서 보았던 것 처럼, 각 세그먼트 디스크립터는 리니어 어드레싱에 같은 어드레스 세트를 사용하기 때문에 논리적 어드레스를 리니어 어드레스로 변환 할 세그멘테이션 단위를 사용할 필요가 적어진다. 세그멘테이션 단위 대신 페이징 단위를 사용함으로서 리눅스는 메모리 관리와 다른 하드웨어 플랫폼들간 이식성 까지 활용할 수 있다.
다음은 x86 아키텍처에서 페이징을 지정하는데 사용되는 필드의 디스크립션이다. 페이징 단위는 세그멘테이션 단위의 아웃풋으로서 리니어 어드레스에 들어간다. 이들은 나중에 다음 필드로 나뉜다.
  • 디렉토리(Directory)는 10 MSB이다. (Most Significant Bit는 가장 큰 값을 가진 바이너리 숫자에 있는 비트 위치이다- MSB는 맨 왼쪽 비트라고도 한다.)
  • 테이블(Table)은 중간 10 비트이다.
  • 오프셋(Offset)은 12 LSB이다. (Least Significant Bit는 짝수인지 홀수인지를 결정하는 단위 값을 주는 바이너리 정수에 있는 비트 위치이다. LSB는 맨 우측 비트라고도 한다. 맨 오른쪽에 있다.)
리니어 어드레스를 상응하는 물리적 위치로 변환하기 위해서는 두 단계 프로세스가 필요하다. 첫 번째 단계에서는 Page Directory(Page Directory에서 Page Table 까지)라는 변환 테이블을 사용하고, 두 번째 단계에서는 Page Table이라고 하는 변환 테이블을 사용한다. (이것은 필요한 페이지 프레임에 대한 Page Table과 Offset이다.) (그림 6)

그림 6. 페이징 필드

 

Page Directory의 물리적 어드레스는 cr3 레지스터로 로딩된다. 리니어 어드레스 내의 디렉토리 필드는 알맞은 Page Table을 가리키는 Page Directory의 엔트리를 결정한다. 테이블 필드의 어드레스는 페이지를 포함하고 있는 페이지 프레임의 물리적 어드레스를 포함하고 있는 Page Table의 엔트리를 결정한다. 오프셋 필드는 페이지 프레임 내의 상대적 위치를 결정한다. 이 오프셋 길이는 12 비트이기 때문에 각 페이지에는 4 KB 데이터가 포함된다.
물리적 어드레스 계산을 요약해 보면,
  1. cr3 + Page Directory (10 MSBs) = table_base
  2. table_base + Page Table (10 intermediate bits) = page_base
  3. page_base + Offset = 물리적 어드레스 (페이지 프레임)
Page Directory와 Page Table의 길이는 10 비트이기 때문에 가능한 어드레스 한계는 1024*1024 KB이고 Offset은 2^12 (4096 bytes)까지 어드레싱 할 수 있다. 따라서 Page Directory의 어드레싱 한계는 1024*1024*4096 (4 GB의 2^32 메모리 셀과 같음)이다. 따라서 x86 아키텍처에서 총 어드레스 한계는 4 GB이다.
확장 페이징은 Page Table 변환 테이블을 제거할 때 얻을 수 있다. 리니어 어드레스의 분할은 Page Directory(10 MSB)와 Offset(22 LSB) 사이에 수행된다.
22 LSB는 페이지 프레임(2^22)에 4MB의 영역을 형성한다. 확장 페이징은 일반 페이징과 공존하고 큰 연속 리니어 어드레스를 상응하는 물리적 어드레스로 매핑할 때 사용된다. 운영 체계는 Page Table을 제거하고 확장 페이징을 제공한다. 이는 PSE(페이지 크기 확장) 플래그를 설정함으로서 실행된다.
36-bit PSE는 36-bit 물리적 어드레스 지원을 4 MB 페이지로 확장하면서 4 바이트 페이지-디렉토리 엔트리를 관리하고, 운영 체계에 주요한 디자인 변경을 요구하지 않고 4GB 이상의 물리적 메모리에 어드레스 하는 메커니즘을 제공한다. 이러한 방식은 수요 페이징과 관련하여 실질적인 한계를 지니고 있다.
리눅스의 페이징은 일반 페이징과 비슷하다. 하지만 x86 아키텍처에는 세 가지 레벨의 페이지 테이블 메커니즘이 도입되었다.
  • Page Global Directory (pgd): 멀티 레벨 페이지 테이블에서 추상화 된 탑 레벨. 페이지 테이블의 각 레벨은 다른 크기의 메모리를 관리한다. 이 글로벌 디렉토리는 4MB 크기의 영역을 관리한다. 각 엔트리는 보다 작은 디렉토리의 작은 테이블에 대한 포인터가 되기 때문에 pgd는 페이지 테이블의 디렉토리이다. 코드가 이 구조를 트래버스하는 것을 페이지 테이블을 "걷는다(walk)"라고 표현한다.
  • Page Middle Directory (pmd): 페이지 테이블의 중간 레벨. x86 아키텍처에서 pmd는 하드웨어에는 없지만 커널 코드에서 pgd에 포함된다.
  • Page Table Entry (pte): 페이지에서 직접 다루어 지는 하위 레벨(PAGE_SIZE 찾기) 페이지의 물리적 어드레스와 엔트리가 유효하고 관련 페이지들이 실제 메모리에 존재한다는 것을 나타내는 관련 비트를 포함하고 있는 값이다.
세 레벨의 페이징 스킴은 리눅스에서 큰 메모리 영역을 지원한다. 큰 메모리 영역 지원이 필요하지 않을 경우 pmd를 "1"로 정의하여 2 레벨 페이징으로 좁힐 수 있다.
이 레벨은 컴파일 시 최적화 되어 제 2 레벨과 제 3 레벨을 실행한다. 중간 디렉토리를 실행하거나 실행 불가로 하면 된다. 32-bit 프로세서는 pmd 페이징을 사용하고 64-bit 프로세서는 pgd 페이징을 사용한다.

그림 7. 페이징의 세 레벨 


64-bit 프로세서에서,
  • 21 MSB는 사용되지 않는다.
  • 13 LSB는 페이지 오프셋에 의해 표현된다.
  • 남은 30 bit는,
    • Page Table 당 10 bit
    • Page Global Directory 당 10 bit
    • Page Middle Directory 당 10 bit
실제로 43 비트가 어드레싱에 사용된다. 따라서 64-bit 프로세서에서는 사용할 수 있는 가상 메모리는 2이다.
각 프로세스는 고유의 페이지 디렉토리와 페이지 테이블을 갖고 있다. 실제 사용자 데이터를 포함하고 있는 페이지 프레임을 참조하기 위해 운영 체계는 (x86 아키텍처에서) pgd를 cr3 레지스터에 로딩함으로서 시작된다. 리눅스는 TSS 세그먼트에서 cr3 레지스터의 컨텐트를 저장하고, 새로운 프로세스가 CPU에서 실행될 때 마다 TSS 세그먼트에서 또 다른 값을 cr3 레지스터에 로딩한다. 결과적으로 페이징 단위가 정확한 페이지 테이블 세트를 참조하게 된다.
pgd 테이블로 가는 각 엔트리는 pmd 엔트리의 어레이를 포함하고 있는 페이지 프레임을 가리킨다. pmd 엔트리는 pte를 포함하고 있는 페이지 프레임을 가리킨다. pte는 사용자 데이터를 포함하고 있는 페이지 프레임을 가리킨다. 검색된 페이지들이 교체되면 스왑 엔트리가 pte 테이블에 저장된다. 이 테이블은 메모리에 재 로드 되기 위해 페이지 프레임을 찾는데 사용된 것이다.
그림 8은 각 페이지 테이블 레벨에서 오프셋을 상응하는 페이지 프레임 엔트리로 추가하고 있는 모습이다. 세그멘테이션 단위로부터 아웃풋으로 받은 리니어 어드레스들을 나누어서 오프셋들을 얻는다. 각 페이지 테이블 컴포넌트에 상응하는 리니어 어드레스를 나누기 위해 다양한 매크로들이 커널에서 사용된다. 리니어 어드레스가 나뉜 모습을 보자.

그림 8. 어드레스가 다양한 어드레스 길이를 가진다. 

 

리눅스는 커널 코드와 데이터 구조를 위해 페이지 프레임을 보유한다. 이 페이지들은 디스크에 절대 교체되지 않는다. 0x0에서 0xc0000000((PAGE_OFFSET)까지의 리니어 어드레스는 사용자 코드와 커널 코드에 의해 참조된다. (PAGE_OFFSET부터 0xffffffff 까지 커널 코드에 의해 어드레스 된다.)
4GB 중에서, 단 3GB만 사용자 애플리케이션에 사용할 수 있다는 의미이다.
리눅스 프로세스에 의해 사용되는 페이징 메커니즘은 두 단계로 설정된다.
  • 부트스트랩 시, 시스템은 페이지 테이블을 8MB의 물리적 메모리로 설정한다.
  • 두 번째 단계에서 나머지 물리적 메모리 매핑을 완료한다.
부트스트랩 단계에서 startup_32() 호출은 페이징을 초기화 한다. 이것은arch/i386/kernel/head.S 파일 내에서 구현된다. 8MB의 매핑은 PAGE_OFFSET 이상 어드레스에서 발생한다. 초기화는 swapper_pg_dir라고 하는 정적으로 정의된 컴파일 시 어레이로 시작한다. 이는 컴파일 시 특정 어드레스(0x00101000)에 배치된다.
이 액션은 코드에서 정적으로 정의된 두 개의 페이지(pg0과 pg1)용 페이지 테이블 엔트리를 구현한다. 페이지 크기 확장 비트가 설정되어 있지 않는 한, 이 페이지 프레임의 크기는 기본이 4KB 이다. (확장 페이징 섹션 참조) 각각 크기는 4MB이다. 글로벌 어레이가 가리킨 데이터 어드레스는 cr3 레지스터에 저장된다. 이것이 리눅스 프로세스용 페이징 단위를 설정하는 첫 단계이다. 나머지 페이지 엔트리들은 두 번째 단계에서 설정된다.
두 번째 단계는 메소드 호울 paging_init() 때문에 주의해야 한다.
RAM 매핑은 PAGE_OFFSET과 x86 32-bit 아키텍처의 4 번째 GB 제한 (0xFFFFFFFF)으로 표현된 어드레스 사이에 수행된다. 약 1GB의 RAM이 리눅스가 시작할 때 매핑될 수 있다는 의미이다. 하지만 누군가 HIGHMEM_CONFIG를 설정했다면 1GB 이상의 물리적 메모리가 커널에 매핑될 수도 있다. 이것은 임시적인 배열이다. 이는 kmap() 호출로 수행된다.
(32-bit 아키텍처 상의) 리눅스 커널은 가상 메모리를 3:1 비율로 나누며, 3GB 가상 메모리는 사용자 공간에, 1GB는 커널 공간에 쓴다. 커널 코드와 데이터 구조는 1GB의 어드레스 공간에 상주해야 하지만 이 어드레스 공간의 큰 소비자는 물리적 메모리용 가상 매핑이다.
커널이 어드레스 공간으로 매핑되지 못한다면 메모리를 조작할 수 없기 때문에 이것이 수행된다. 따라서 커널에 의해 핸들 될 수 있는 최대 물리적 메모리는, 커널 코드 자체를 매핑하는데 필요한 공간을 제외한 커널의 가상 메모리 공간으로 매핑될 수 있는 양이었다. 결과적으로 x86 기반 리눅스 시스템은 1GB 미만의 물리적 메모리로 작동될 수 있다.
많은 사용자들에게 공급하기 위해, 더 많은 메모리를 지원하기 위해, 퍼포먼스를 높이기 위해, 아키텍처와 독립된 방식으로 메모리를 기술하기 위해서 리눅스 메모리 모델은 진화해야 했다. 이를 위해 더욱 새로운 모델이 메모리를 각 CPU에 할당된 뱅크로 배열했다. 각 뱅크를 노드(node)라고 한다. 각 노드는 존(zone)으로 나뉜다. 존은 다음과 같은 유형이 있다.
  • ZONE_DMA (0-16 MB): 특정 ISA/PCI 장치가 필요로 하는 더 적은 물리적 메모리 영역에 상주하는 메모리 범위.
  • ZONE_NORMAL (16-896 MB): 커널에 의해 직접 물리적 메모리의 상위 영역으로 매핑되는 메모리 범위. 모든 커널 작동들은 이 메모리 존을 사용하여 발생할 수 있다. 가장 퍼포먼스 중심적인 존이다.
  • ZONE_HIGHMEM (896 MB and higher): 커널에 의해 매핑되지 않은 시스템에 남아있는 가용 메모리.
노드 개념은 struct pglist_data 구조를 사용하여 커널에서 구현된다. 존은 structzone_struct 구조를 사용하여 기술된다. 물리적 페이지 프레임은 struct Page 구조에 의해 표현되고 모든 struct들은 글로벌 구조 어레이인 struct mem_map에서 유지된다. 이는NORMAL_ZONE 시작 시 저장된다. 그림 9는 노드, 존, 페이지 프레임 간 기본적인 관계를 보여준다.

그림 9. 노드, 존, 페이지 프레임 간 관계 

 
 

(32-bit 시스템에서 PAE(Physical Address Extension)에 의해 최대 64GB 까지 액세스 하기 위해) Pentium II의 가상 메모리 확장의 지원이 구현되었을 때와 4GB의 물리적 메모리(32-bit 시스템)용 지원이 구현되었을 때 높은 메모리 존은 커널 메모리 관리에 나타났다. 이것은 x86과 SPARC 플랫폼에 적용된 개념이다. 일반적으로 4GB 메모리는ZONE_HIGHMEM을 ZONE_NORMAL에 매핑하여 kmap()에 의해 액세스 될 수 있다. 32-bit 아키텍처 상에 16GB 이상의 RAM을 두지 않도록 한다. PAE가 실행될 때도 마찬가지이다.
(PAE는 Intel에서 제공하는 메모리 어드레스 확장으로서 프로세서가 물리적 메모리에 어드레스 하는데 사용될 수 있는 비트의 수를 32 비트에서 36 비트로 확장할 수 있도록 한다. Address Windowing Extensions API를 사용하는 애플리케이션용 호스트 운영 체계의 지원을 통해 가능하다.)
물리적 메모리 영역의 관리는 존 할당자(zone allocator)에 의해 수행된다. 이것은 메모리를 여러 존들로 나눈다. 각 존을 할당용 단위로 취급한다. 특정 할당 요청은 할당이 시도되는 존의 리스트를 활용한다.
예를 들어,
  • 사용자 페이지에 대한 요청은 "일반" 존에서 먼저 채워져야 한다. (ZONE_NORMAL);
  • 실패하면 ZONE_HIGHMEM부터 채운다.
  • 역시 실패하면 ZONE_DMA부터 채운다.
이 같은 할당을 위한 존 리스트는 ZONE_NORMALZONE_HIGHMEMZONE_DMA로 구성된다. 한편, DMA 페이지의 요청은 DMA 존에서만 수행된다. 따라서 이 같은 요청의 존 리스트에는 DMA 존만 포함된다.
메모리 관리는 크고, 복잡하고, 시간이 많이 드는 일이다. 실제 멀티 프로그래밍 환경에서 시스템이 어떻게 작동하는지를 모델링 하는 것이기 때문에 매우 까다로운 작업이다. 스케줄링, 페이징 작동, 멀티 프로세서 인터랙션 같은 컴포넌트들은 상당한 도전 과제이다. 이 글이 여러분에게 도움이 되었기를 바란다.

'OS > Linux' 카테고리의 다른 글

vim 설정  (0) 2012.03.09
vi(vim) 을 source insight 처럼 사용하기  (0) 2012.03.09
gdb 및 gdbserver 컴파일 및 간단한 사용기  (0) 2012.03.09
리눅스 압축 - tar zip gz bz2 정리  (0) 2012.03.09
VIM. Plugin - Taglist  (0) 2012.03.09