HMM(Heterogeneous Memory Management)는 리눅스 커널에서 GPU, FPGA 같은 비-CPU 디바이스가 CPU 가상 주소 공간을 직접 매핑할 수 있도록 지원하는 서브시스템이다. 디바이스가 CPU의 페이지 테이블을 읽고, 필요시 페이지 폴트를 트리거하며, DMA 매핑까지 처리할 수 있는 프레임워크를 제공한다. 핵심 아이디어는 디바이스 드라이버가 hmm_range_fault()를 호출하여 특정 가상 주소 범위의 페이지 상태를 한꺼번에 확인하고, 필요시 자동으로 fault-in하는 것이다.
소스 파일 위치:
mm/hmm.c ← HMM 핵심 구현 (895줄)
include/linux/hmm.h ← hmm_range, hmm_pfn_flags 정의
include/linux/hmm-dma.h ← hmm_dma_map 구조체 정의
이 구현은 MMU interval notifier로 범위를 보호하면서 CPU 페이지 테이블 상태를 읽어 디바이스가 바로 사용할 PFN 배열을 채우는 데 초점을 둔다. default_flags와 pfn_flags_mask를 조합하면 전체 범위를 미리 fault-in하거나, 개별 PFN에만 fault/쓰기 요구를 섞어 넣는 방식도 가능하다. 이후에는 같은 PFN 결과를 hmm_dma_map_pfn()으로 넘겨 IOVA 또는 PCI P2P DMA 주소로 변환한다.
# HMM 관련 커널 설정 확인
grep -E "CONFIG_HMM|CONFIG_DEVICE_PRIVATE" /boot/config-$(uname -r)
# HMM 모듈 로드 상태 확인
lsmod | grep -i hmm
# /sys에서 HMM 관련 디버그 정보 확인 (해당 커널 빌드 시)
cat /sys/kernel/debug/hmm/* 2>/dev/null || echo "HMM debug not available"
# 디바이스의 DMA 주소 범위 확인
cat /sys/class/dma/*/dmaaddress 2>/dev/null || echo "N/A"
# 커널 로그에서 HMM 관련 메시지 확인
dmesg | grep -i "hmm\|device.private\|dma.map"
# 현재 시스템의 NUMA 노드 및 메모리 티어 확인
numactl --hardware
ls /sys/devices/system/memory/ | head -20
# 페이지 폴트 통계 확인 (HMM이 fault를 트리거할 때 활용)
vmstat 1 3
디바이스가 요청한 가상 주소 범위의 페이지 상태를 추적한다. MMU interval notifier를 통해 무효화(invalidation)로부터 보호받는다.
/* include/linux/hmm.h:111-120 */
struct hmm_range {
struct mmu_interval_notifier *notifier; /* MMU 무효화 알림 */
unsigned long notifier_seq; /* 무효화 시퀀스 번호 */
unsigned long start; /* 범위 시작 가상 주소 (inclusive) */
unsigned long end; /* 범위 끝 가상 주소 (exclusive) */
unsigned long *hmm_pfns; /* PFN 배열 (범위 크기만큼) */
unsigned long default_flags; /* 범위 기본 플래그 (읽기/쓰기 등) */
unsigned long pfn_flags_mask; /* 개별 PFN 마스크 */
void *dev_private_owner; /* 디바이스 프라이빗 페이지 소유자 */
};
페이지 PFN의 상태를 나타내는 비트 플래그. 입력 플래그와 출력 플래그가 구분된다.
/* include/linux/hmm.h:38-58 */
enum hmm_pfn_flags {
/* 출력 필드 및 플래그 */
HMM_PFN_VALID = 1UL << (BITS_PER_LONG - 1), /* 유효한 PFN */
HMM_PFN_WRITE = 1UL << (BITS_PER_LONG - 2), /* 쓰기 가능 */
HMM_PFN_ERROR = 1UL << (BITS_PER_LONG - 3), /* 접근 불가 (오류) */
HMM_PFN_DMA_MAPPED = 1UL << (BITS_PER_LONG - 4), /* DMA 매핑됨 */
HMM_PFN_P2PDMA = 1UL << (BITS_PER_LONG - 5), /* P2P 페이지 */
HMM_PFN_P2PDMA_BUS = 1UL << (BITS_PER_LONG - 6), /* 버스 매핑 P2P */
HMM_PFN_ORDER_SHIFT = (BITS_PER_LONG - 11),
/* 입력 플래그 */
HMM_PFN_REQ_FAULT = HMM_PFN_VALID, /* 폴트 요청 */
HMM_PFN_REQ_WRITE = HMM_PFN_WRITE, /* 쓰기 폴트 요청 */
HMM_PFN_FLAGS = ~((1UL << HMM_PFN_ORDER_SHIFT) - 1), /* PFN 비트 마스크 */
};
hmm_range_fault() 내부에서 페이지 테이블을 순회할 때 사용되는 임시 상태 구조체.
/* mm/hmm.c:33-36 */
struct hmm_vma_walk {
struct hmm_range *range; /* 현재 처리 중인 hmm_range */
unsigned long last; /* 마지막으로 처리된 주소 (재시작 지점) */
};
PFN 목록과 DMA 주소 배열을 관리하는 구조체. IOVA 기반 매핑과 일반 매핑을 지원한다.
/* include/linux/hmm-dma.h:19-24 */
struct hmm_dma_map {
struct dma_iova_state state; /* DMA IOVA 상태 */
unsigned long *pfn_list; /* PFN 배열 */
dma_addr_t *dma_list; /* DMA 주소 배열 */
size_t dma_entry_size; /* 각 DMA 엔트리 크기 */
};
HMM이 페이지 테이블을 순회할 때 호출되는 콜백 함수 테이블.
/* mm/hmm.c:631-638 */
static const struct mm_walk_ops hmm_walk_ops = {
.pud_entry = hmm_vma_walk_pud, /* PUD 엔트리 처리 */
.pmd_entry = hmm_vma_walk_pmd, /* PMD 엔트리 처리 */
.pte_hole = hmm_vma_walk_hole, /* PTE 공백 처리 */
.hugetlb_entry = hmm_vma_walk_hugetlb_entry, /* HugeTLB 처리 */
.test_walk = hmm_vma_walk_test, /* VMA 유효성 검사 */
.walk_lock = PGWALK_RDLOCK, /* 읽기 잠금 */
};
/* mm/hmm.c:659-686 */
int hmm_range_fault(struct hmm_range *range)
역할: 디바이스가 요청한 가상 주소 범위의 페이지를 fault-in하고, 해당 PFN을 range->hmm_pfns[] 배열에 기록한다.
분기 로직:
1. mmu_interval_check_retry() → 범위가 무효화되었으면 -EBUSY 반환 (재시작)
2. walk_page_range() 호출 → 페이지 테이블 순회
3. -EBUSY 반환 시 hmm_vma_walk.last에서 재시작 (do-while 루프)
/* mm/hmm.c:396-471 */
static int hmm_vma_walk_pmd(pmd_t *pmdp, unsigned long start,
unsigned long end, struct mm_walk *walk)
역할: PMD 수준의 페이지 테이블 엔트리를 처리한다. Huge Page, migration, 일반 PTE 디렉토리로 분기한다.
분기 로직:
1. pmd_none() → hmm_vma_walk_hole() (빈 영역 처리)
2. pmd_is_migration_entry() → 마이그레이션 대기 후 재시작
3. !pmd_present() → hmm_vma_handle_absent_pmd() (디바이스 프라이빗 등)
4. pmd_trans_huge() → hmm_vma_handle_pmd() (THP 처리)
5. pmd_bad() → HMM_PFN_ERROR 반환
6. 일반 PTE → PTE 루프로 진입
/* mm/hmm.c:235-332 */
static int hmm_vma_handle_pte(struct mm_walk *walk, unsigned long addr,
unsigned long end, pmd_t *pmdp, pte_t *ptep,
unsigned long *hmm_pfn)
역할: 단일 PTE의 상태를 확인하고, 필요시 폴트를 트리거하거나PFN 결과를 기록한다.
분기 로직:
1. pte_none() 또는 UFFD WP 마커 → 폴트 필요시 fault, 아니면 스킵
2. !pte_present() — softleaf 타입별 분기:
- Device Private (자기 소유) → HMM_PFN_VALID 직접 기록
- Swap → fault 트리거
- Device Private (타인) → fault 트리거
- Device Exclusive → fault 트리거
- Migration → migration_entry_wait() 후 -EBUSY
- 기타 → -EFAULT
3. pte_present() → pte_to_hmm_pfn_flags()로 플래그 변환 후 기록
/* mm/hmm.c:73-94 */
static int hmm_vma_fault(unsigned long addr, unsigned long end,
unsigned int required_fault, struct mm_walk *walk)
역할: 지정된 범위의 페이지를 fault-in한다. handle_mm_fault()를 호출하여 커널의 표준 페이지 폴트 처리를 활용한다.
분기 로직:
1. HMM_NEED_WRITE_FAULT && !(vma->vm_flags & VM_WRITE) → -EPERM
2. handle_mm_fault()에서 VM_FAULT_ERROR → -EFAULT
3. 성공 시 -EBUSY 반환 (재시작 유도)
/* mm/hmm.c:771-857 */
dma_addr_t hmm_dma_map_pfn(struct device *dev, struct hmm_dma_map *map,
size_t idx,
struct pci_p2pdma_map_state *p2pdma_state)
역할: HMM PFN을 DMA 가능한 주소로 변환한다. IOVA 기반 매핑과 일반 매핑을 모두 지원하며, PCI P2P DMA도 처리한다.
분기 로직:
1. HMM_PFN_DMA_MAPPED && !HMM_PFN_P2PDMA_BUS → 이미 매핑됨, IOVA/dma_addrs에서 반환
2. pci_p2pdma_state() 분기:
- MAP_THRU_HOST_BRIDGE → DMA_ATTR_MMIO 설정
- MAP_BUS_ADDR → 버스 주소 직접 반환
3. dma_use_iova() → IOVA 기반 매핑 (dma_iova_link + dma_iova_sync)
4. 일반 → dma_map_phys()로 직접 매핑
hmm_range_fault() ← 디바이스 드라이버가 호출
│
├─ do-while (-EBUSY 재시작 루프)
│ │
│ ├─ mmu_interval_check_retry() ← 무효화 확인
│ │
│ └─ walk_page_range() ← 페이지 테이블 순회 시작
│ │
│ ├─ hmm_vma_walk_test() ← VMA 유효성 검사 (VM_IO/VM_PFNMAP 스킵)
│ │
│ ├─ hmm_vma_walk_pud() ← PUD 수준 처리 (CONFIG_HAVE_ARCH_TRANSPARENT_HUGEPAGE_PUD)
│ │ ├─ pud_leaf() → pfn 기록
│ │ └─ ACTION_SUBTREE → PMD 수준으로 내려감
│ │
│ ├─ hmm_vma_walk_pmd() ← PMD 수준 처리
│ │ ├─ pmd_none() → hmm_vma_walk_hole()
│ │ ├─ pmd_is_migration_entry() → migration_entry_wait()
│ │ ├─ !pmd_present() → hmm_vma_handle_absent_pmd()
│ │ ├─ pmd_trans_huge() → hmm_vma_handle_pmd()
│ │ └─ 일반 PTE → hmm_vma_handle_pte() [for 루프]
│ │
│ ├─ hmm_vma_walk_hole() ← 빈 영역 처리
│ │ └─ hmm_vma_fault() 필요시 호출
│ │
│ └─ hmm_vma_walk_hugetlb_entry() ← HugeTLB 엔트리 처리
│
└─ 반환 (0: 성공, -EBUSY: 재시작 필요, 기타: 오류)
hmm_dma_map_pfn() ← DMA 매핑 시 호출
│
├─ 이미 매핑됨? → 기존 주소 반환
│
├─ pci_p2pdma_state() → P2P 분기
│ ├─ MAP_THRU_HOST_BRIDGE → MMIO 속성
│ ├─ MAP_BUS_ADDR → 버스 주소 반환
│ └─ MAP_NONE → 계속
│
├─ dma_use_iova() → IOVA 매핑
│ ├─ dma_iova_link()
│ └─ dma_iova_sync()
│
└─ 일반 → dma_map_phys()
| 플래그 | 의미 | 입력 시 | 출력 시 |
|---|---|---|---|
| `HMM_PFN_VALID` | 유효한 페이지 | `HMM_PFN_REQ_FAULT`로 재사용 | 페이지가 존재함 |
| `HMM_PFN_WRITE` | 쓰기 가능 | `HMM_PFN_REQ_WRITE`로 재사용 | PTE에 쓰기 권한 있음 |
| `HMM_PFN_ERROR` | 접근 불가 | - | 오류 페이지 (poisoned, no vma 등) |
| `HMM_PFN_DMA_MAPPED` | DMA 매핑됨 | sticky (입력→출력 유지) | DMA 매핑 완료 |
| `HMM_PFN_P2PDMA` | P2P 페이지 | sticky | PCI P2P 매핑 |
| `HMM_PFN_P2PDMA_BUS` | 버스 매핑 P2P | sticky | 버스 주소 기반 P2P |
| PTE 상태 | 처리 함수 | 동작 |
|---|---|---|
| `pte_none()` | `hmm_pte_need_fault()` | 폴트 필요시 fault-in, 아니면 스킵 |
| Device Private (자기 소유) | 직접 기록 | `HMM_PFN_VALID` + PFN 기록 |
| Device Private (타인) | fault 트리거 | `hmm_vma_fault()` 호출 |
| Swap | fault 트리거 | 스왑에서 인 |
| Migration | `migration_entry_wait()` | 대기 후 `-EBUSY` 반환 |
| `pte_present()` | `pte_to_hmm_pfn_flags()` | 플래그 변환 후 PFN 기록 |
| 조건 | 매핑 방식 | 함수 |
|---|---|---|
| IOVA 지원 + !dma_need_unmap | IOVA 기반 | `dma_iova_link()` + `dma_iova_sync()` |
| IOVA 미지원 + dma_need_unmap | 일반 DMA | `dma_map_phys()` |
| P2P (Host Bridge 경유) | MMIO 매핑 | `DMA_ATTR_MMIO` 설정 |
| P2P (Bus Address) | 버스 주소 | `pci_p2pdma_bus_addr_map()` |
| 이미 매핑됨 | 재사용 | 기존 주소 반환 |