노드(Node)는 용도에 따라서 메모리 영역을 한 개 이상의 영역으로 분리해서 관리한다. 이 때 각 영역을 존(Zone)이라고 한다. 이 글에선 존의 종류에 어떤 것이 있는지 알아본다. 존과 노드, NUMA에 대한 내용은 존, 노드를 정리한 글과 NUMA를 정리한 글에 정리해두었다.
Zone의 종류
리눅스에 어떤 존들이 있는지 살펴보자. 참고로 아래 서술한 존이 항상 모두 존재하는 건 아니다. 노드마다 존이 1개 이상 존재하는 건 맞지만 그렇다고 항상 모든 종류의 존이 존재하지는 않는다. 예를 들어서 어떤 시스템은 모든 메모리 영역이 ZONE_NORMAL일 수도 있다.
ZONE_DMA
DMA는 DMA 글에 정리해두었다. ZONE_DMA는 x86에서 하위 16MB의 메모리를 DMA용으로 예약된 존이다. ISA 디바이스나 일부 구식 PCI 디바이스는 24비트 주소만 접근할 수 있기 때문에, 하위 24비트 안에서 버퍼를 할당해야 한다. 꼭 DMA 용으로만 사용되는 것은 아니며 커널에 따라 별도의 용도로 예약하는 경우도 있다. (예를 들어 kdump 커널이 그렇다.)
참고로 아키텍처별로 DMA 영역을 다루는 방법이 상이하므로 ARM, SPARC, PowerPC, ... 등등 아키텍처에 따라서 ZONE_DMA/DMA32가 존재하지 않을 수도 있고 범위가 다를 수도 있다.
ZONE_DMA32
24비트 주소만 접근 가능한 구닥다리 디바이스는 오늘날에 많이 사용되지 않지만, 32비트 주소 제한을 갖는 디바이스는 생각보다 많다. x86_64에서는 32비트 주소만 접근할 수 있는 디바이스를 위해 ZONE_DMA32를 사용한다. 32비트 x86에서는 애초에 모든 주소가 32비트이므로 ZONE_DMA32는 별도로 존재하지 않는다.
ZONE_NORMAL
일반적으로 커널이 접근하는 영역이 ZONE_NORMAL이다. ZONE_NORMAL의 메모리는 항상 1:1로 매핑이 되어있다. 1:1로 매핑되었다는 뜻은 가상 주소와 물리 주소가 임의로 매핑된 것이 아니라, 가상 주소와 물리 주소가항상 특정 오프셋만큼만 차이가 난다는 뜻이다. 그리고 항상 이 영역의 가상 주소에 대한 물리 주소가 매핑되어있으므로 이 존에서 할당한 메모리는 언제든지 커널이 접근할 수 있다. 반면 ZONE_HIGHMEM의 주소는 커널이 페이지 테이블을 수정해서 매핑을 만들기 전까지는 직접적으로 접근할 수 없다. (물론 커널 기준이다. 사용자 프로세스는 자신이 사용하는 메모리에 대한 주소 매핑을 갖고있기 때문에 쉽게 접근할 수 있다. 또는 사용자 공간에서 커널로 시스템 호출을 한 경우에도 이미 사용자 공간에 대한 주소 매핑은 되어있으므로 커널에서 사용자 공간의 버퍼에 접근할 수 있다.)
ZONE_HIGHMEM
HIGHMEM은 64비트 컴퓨터에서는 사용되지 않는다. 이 존은 32비트 컴퓨터에서 작은 주소 공간으로 인한 제약때문에 사용하는 존이다. 우선 기본적인 내용을 설명하면, 리눅스에서는 커널과 사용자 프로세스가 같은 주소 공간을 공유한다. 이때 가상 주소 상의 하위 3GB(강조하지만 가상 주소이다. 물리 주소가 아니다.)는 사용자 프로세스가 사용하고, 상위 1GB는 커널이 사용한다. 커널 스레드인 경우에는 사용자 공간 프로세스에 대한 가상 주소 -> 물리 주소 매핑이 존재하지 않는다.
사용자 프로세스와 커널이 주소 공간을 공유하는 것이 기술적으로 꼭 필요한 요구사항은 아니다. 애초에 두 주소 공간을 분리하는 아키텍처도 있다. 다만 사용자 프로세스는 사용자 공간과 커널 공간을 왔다갔다 하는 일이 많은데 (예를 들어 시스템 호출이 그렇다.) 서로 다른 주소 공간을 사용하면 TLB flushing으로 인한 비용이 늘어날 뿐이다.
커널에서 사용하는 1GB 중, 항상 가상 주소와 물리 주소가 1:1로 매핑되어 있는 영역이 위에서 소개한 ZONE_NORMAL이다. 그리고 ZONE_NORMAL에서 사용하는 주소를 Kernel Logical Address라고 한다.
그런데 생각해보자. 커널이 사용하는 가상 주소 1GB 범위 전체를, 하위 1GB의 물리 주소와 매핑해서 ZONE_NORMAL로 잡아버리면 커널은 1GB의 물리 주소 바깥에 접근할 방법이 없다. 이런 문제를 막기 위해서 리눅스는 32비트에서 커널 주소 공간 1GB 중 일부는 사용하지 않은 채로 두고 필요에 따라서 자유롭게 매핑할 수 있도록 두는데, 그게 바로 High Memory이다. ZONE_HIGHMEM은 이러한 High Memory를 할당하기 위한 존이다. 64비트에서는 32비트처럼 주소 공간이 부족하지 않으므로 High Memory를 사용하지 않는다.
ZONE_MOVABLE
ZONE_MOVABLE은 페이지 할당시 플래그에 __GFP_MOVABLE와 __GFP_HIGHUSER가 모두 켜져있는 경우에만 할당할 수 있는 존이다. 쉽게 이동할 수 있는 페이지만 모아놓아서 하나의 존을 구성한 것이 ZONE_MOVABLE이다.
Virtual Memory: Grouping pages by mobility 글에서 설명했듯이 존의 종류와 상관없이 존 내에서 migrate type에 따라서 별도로 free_area를 관리한다. 글 내용을 요약하면 존 내에서 Movable한 페이지와 Unmovable한 페이지를 별도로 관리해서 단편화를 줄인다는 내용이다. 이런 방식을 list-based grouping이라고도 한다.
그러면 왜 존 내에서 list-based grouping을 함에도 불구하고 Movable한 페이지들을 모아서 별도의 존을 구성하는가? 그건 list-based grouping 방식도 메모리가 부족하면 서로 다른 migrate type에서 페이지를 빌려오게 되고, 시간이 지남에 따라 단편화가 발생하기 때문이다. 물론 list-based grouping 방식이 효과가 없는게 아니다. 이 방식을 도입하면 단편화가 현저하게 줄어든다. 다만 방식의 특성상 단편화가 발생하는 병적인 케이스가 존재할 뿐이다.
단순히 메모리 단편화를 줄이는 게 목적이라면 list-based grouping도 매우 잘 작동하지만, high-order allocation을 밥먹듯이 하는 상황이라면 이 방식이 살짝 부족할 수 있다. ZONE_MOVABLE에서는 Movable한 페이지만 할당하므로 다른 migrate type에서 페이지를 빌려올 일이 없으므로 좀 더 단편화를 줄일 수 있다.
ZONE_MOVABLE은 메모리 핫플러깅에도 사용된다. 이 존에 대한 내용은 기회가 되면 별도의 글로 써봐야겠다.
ZONE_DEVICE
디바이스에서 대용량의 메모리를 빠르게 접근할 때 사용한다고 하는데, (아직 내 관심사가 아니기도 하고) 구체적인 예시를 본 적이 없어서 천천히 정리해봐야겠다.
참고 문서
'Kernel > Memory Management' 카테고리의 다른 글
Virtual Memory: Transparent Huge Pages (0) | 2022.03.23 |
---|---|
Virtual Memory: Memory Compaction (0) | 2022.01.10 |
Virtual Memory: Grouping pages by mobility (0) | 2022.01.02 |
Virtual Memory: Node and Zone (5) | 2022.01.02 |
Virtual Memory: Folio in 5.16 (0) | 2021.12.12 |
댓글