Direct Map
x86 Documentation에서 27.3. Memory Management을 보면 x86_64에서 4단계 or 5단계 페이징이냐에 따라서 가상 주소 공간이 각각 어떤 용도로 쓰이는지 명시되어있다. 이 가상 주소 공간 중 64TB (4-level paging) or 32PB (5-level paging)은 시스템 전체의 물리 메모리를 매핑하는 용도로 사용된다. 이 영역은 가상주소와 물리 주소가 직접 매핑된다. 다시 말해 물리 주소와 가상 주소가 PAGE_OFFSET 만큼만 차이난다. 실제로 물리 주소를 가상 주소로 바꿔주는 __va() 함수의 정의를 보면
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
이렇게 물리 주소에서 PAGE_OFFSET을 더하는게 전부이다. 이러한 특성 때문에 direct mapping에 해당하는 영역은 커널 코드에서 page table walking을 하지 않고도 가상 주소와 물리 주소 간의 변환을 (소프트웨어적으로) 빠르게 할 수 있다.
Page Size Extension
Transparent Huge Pages 글에서 언급했듯 최신 인텔 프로세서는 1GB/2MB 크기로도 가상 주소를 물리 주소로 매핑할 수 있다. THP 같은 경우에는 사용자 공간 프로세스를 위한 기능이라 페이지 폴트시에 2MB씩 매핑하거나 4K짜리 페이지들을 2M으로 합쳐버리는 등등 여러 가지 정책을 설정할 수 있었는데, direct map은 부팅 이후 언제든지 접근해야 하므로 그냥 부팅 과정에서 2M/1GB 크기로 매핑을 해버리면 된다. 실제로 init_mem_mapping()에서는 CPU가 기능을 지원하는 경우 direct map의 일부 영역을 1GB/2M 크기로 매핑해버린다. (해당 크기로 정렬된 경우) 그래서 커널은 사용자 공간 프로세스처럼 THP 같은 기능이 별도로 없어도 TLB 미스가 적게 난다.
PAGE_KERNEL
init_mem_mapping() 함수를 보면 direct map의 페이지 테이블 엔트리들은 PAGE_KERNEL으로 매핑이 된다는걸 알 수 있다. 하지만 종종 direct map 상의 메모리에 PAGE_KERNEL이 아닌 다른 권한을 부여할 때가 있다. 대표적인 예로 코드에 실행 권한을 부여해야 하는 모듈, kprobe, ftrace, BPF와 아예 메모리를 direct map상에서 unmap해버려서 guest에게 private한 메모리를 만드는 경우, 그리고 vmalloc 등이 예시이다. (vmalloc 코드를 보면 free할 때 direct map 상의 permission을 reset해버리는데 아직 이유는 모르겠다. 왜 vmalloc을 쓰는데 direct map이 바뀌어야 하지?)
이렇게 direct map에서 PAGE_KERNEL이 아닌 다른 protection bits를 사용하게 되는 경우 1GB/2MB짜리 매핑을 쪼갠 다음 protection bits를 수정해야 한다. 그런데 현재 x86에는 direct map에서 쪼개진 매핑들을 다시 합쳐주는 매커니즘이 없다. 따라서 위에 언급한 기능을 사용하면 할수록 direct map이 점점 단편화(?) 된다. 이때 매핑을 쪼개고 바꿀 때는 set_memory_*() API를 사용한다.
나도 최근에 컴퓨터를 사용하면서 direct map이 시간이 지남에 따라서 split되는 현상을 발견했다. 부팅 직후에는 대부분의 램이 대부분 1G 크기로 매핑되어 있는데 점점 2M, 4K 크기로 쪼개지는 것을 볼 수 있다. 매핑이 쪼개질 수록 커널 모드에서 TLB를 점점 많이 사용하게 된다.
Solutions
local cache
BPF, kprobe 등등 vmalloc의 사용자 별로 사용자 쪽에서 캐시를 두어 매핑을 쪼개는 것이 direct map 전체에 퍼지지 않도록 방지하는 방법이다.
global cache
사용자 쪽에 local cache를 두면 메모리 오버헤드가 커지기 때문에 시스템 전체에 하나의 캐시만 둬서 관리하는게 더 좋은 방법일 수 있다. 최근에 Mike Rapoport는 [RFC PATCH 0/3] Prototype for direct map awareness in page allocator라는 패치 시리즈를 메일링 리스트에 보냈다. 이 시리즈는 별도의 캐시를 두지는 않고 MIGRATE_UNMAPPED라는 migrate type을 만들어서 페이지 테이블을 쪼개는 할당들을 grouping하도록 하는 접근이다.
Thoughts
Mike의 시리즈는 페이지를 사용한 이후에 페이지 할당자 쪽에서 다시 매핑을 합치려고 하는데, 나는 매핑을 합치는 부분이 페이지 할당자보다는 x86 CPA 코드에서 이루어져야 한다고 생각한다. 그래서 요 며칠간 코드를 조금 수정해봤다. 아직 리스트에 보내볼 정도는 아니고 해결해야 할 이슈가 좀 있다.
Links
이 주제는 작년 LPC2021 주제였다. [youtube] [pdf] 올해도 LSFMM 2022에서 관련된 세션이 있었던 것 같은데 난 시간대가 안 맞아서 참여를 못했다... 영상이 올라올지 모르겠다. 올라오면 봐야지.
아, 그리고 CPU에 가까운 쪽을 보다보니 인텔 SDM을 보는게 도움이 좀 됐다.
아직 이해가 안 되는 부분은, 사실 BPF랑 모듈은 module_alloc()으로 메모리를 할당하기 때문에 direct map 주소 공간을 사용하지 않고 vmalloc 주소 공간을 사용하는게 맞는데, 왜 vmalloc()을 할 때마다 vmalloc에 사용된 메모리의 direct map상 주소의 permission을 리셋해야 하는지는 잘 모르겠다. 참고: [PATCH v5 00/23] x86: text_poke() fixes and executable lockdowns
'Kernel > Memory Management' 카테고리의 다른 글
struct page 메모 (2) | 2022.09.14 |
---|---|
Virtual Memory: vmalloc(), vm_map_ram() 분석 (0) | 2022.07.13 |
KFENCE: Kernel Electric-Fence (2) | 2022.04.17 |
KASAN: Kernel Address SANitizer (0) | 2022.04.09 |
Virtual Memory: Transparent Huge Pages (0) | 2022.03.23 |
댓글