MMU 알림(MMU Notifier)은 리눅스 커널의 가상 머신(Virtual Machine)과 디바이스 드라이버에서 사용되는 메모리 동기화 메커니즘입니다. 가상 머신은 자체적인 2차 MMU(Second-stage MMU)를 사용하여 게스트 가상 주소를 호스트 물리 주소로 변환하는데, 호스트 측에서 페이지 테이블이 변경될 때 2차 MMU도 함께 동기화해야 합니다. MMU 알림은 이러한 동기화를 위해 커널이 등록된 드라이버에게 페이지 테이블 변경 이벤트를 통보하는 인터페이스를 제공합니다.
MMU 알림은 크게 두 가지 유형으로 나뉩니다: 전통적인 mmu_notifier(hlist 기반)와 성능이 향상된 mmu_interval_notifier(interval tree 기반)입니다. interval notifier는 특정 가상 주소 범위에 대해 효율적인 유효화(invalidation)를 가능하게 하며, KVM의 경우 TDP(Two-Dimensional Paging) 세컨드 스테이지 변환 관리에 주로 사용됩니다.
한 줄로 보면, 페이지 테이블이 "서가 배치표"라면 MMU notifier는 책이 옮겨지기 전후로 관련 드라이버에게 새 위치를 알려 주는 안내 방송입니다. 그래서 munmap(), mprotect(), mremap(), migrate_vma_collect() 같은 경로와 TLB 무효화를 함께 봐야 합니다.
소스 파일 경로:
mm/mmu_notifier.c // MMU 알림 핵심 구현
include/linux/mmu_notifier.h // 인터페이스 정의
drivers/gpu/drm/i915/gem/i915_gem_userptr.c // GPU 드라이버 사용 예시
drivers/gpu/drm/amd/amdgpu/amdgpu_hmm.c // HMM/interval notifier 사용 예시
drivers/iommu/intel/svm.c // IOMMU SVA 연동 예시
# MMU 알림 활성화 확인
cat /boot/config-$(uname -r) | grep CONFIG_MMU_NOTIFIER
# KVM/MMU 알림 동작 확인
dmesg | grep -i "mmu.*notif" | tail -20
sudo trace-cmd record -p function_graph -l __mmu_notifier_invalidate_range_start -l __mmu_notifier_invalidate_range_end -l mmu_interval_notifier_remove sleep 5
sudo trace-cmd report | head -20
# MMU 알림 디버그 정보
cat /proc/mmu_notifiers 2>/dev/null || echo "지원하지 않는 커널"
# 현재 mm 구조체의 notifier 구조 확인
sudo cat /proc/1/maps | head -5
# (실제 MMU 알림 정보는 ftrace로 확인)
grep -E 'mmu_notifier|mmu_interval' /sys/kernel/debug/tracing/available_filter_functions 2>/dev/null | head -20
sudo trace-cmd report | head -20
# interval notifier 트리 확인 (KVM 사용 시)
sudo cat /sys/kernel/debug/kvm/mmu_notifier_count 2>/dev/null || echo "디버그 지원 안 함"
# 관련 커널 모듈 확인
lsmod | grep -E "(kvm|vfio|i915)"
# MMU 알림 이벤트 모니터링
sudo perf probe -a 'mmu_notifier_invalidate_range_start'
sudo perf record -e probe:mmu_notifier_invalidate_range_start -aR sleep 5
sudo perf script
# 페이지 폴트와 주소 공간 점검
cat /proc/vmstat | grep -E 'pgfault|pgmajfault'
cat /proc/pressure/memory
grep -E 'PageTables|Vmalloc|KReclaimable' /proc/meminfo
sudo cat /sys/kernel/debug/kernel_page_tables 2>/dev/null | head -40
mm/mmu_notifier.c:39-50 — MM의 모든 MMU 알림을 관리하는 구조체입니다.
struct mmu_notifier_subscriptions {
/* 이 MM에 등록된 모든 MMU 알림이 이 리스트에 큐잉됨 */
struct hlist_head list; // hlist 기반 알림 목록
bool has_itree; // interval tree 사용 여부
spinlock_t lock; // 리스트 수정 시리얼라이제이션
unsigned long invalidate_seq; // 유효화 시퀀스 번호
unsigned long active_invalidate_ranges; // 활성 유효화 범위 수
struct rb_root_cached itree; // interval tree 루트
wait_queue_head_t wq; // 대기 큐
struct hlist_head deferred_list; // 지연된 추가/제거 목록
};
include/linux/mmu_notifier.h:228-234 — 개별 MMU 알림 구독 구조체입니다.
struct mmu_notifier {
struct hlist_node hlist; // 해시 리스트 노드
const struct mmu_notifier_ops *ops; // 콜백 함수 테이블
struct mm_struct *mm; // 연결된 mm 구조체
struct rcu_head rcu; // RCU 정리용
unsigned int users; // 참조 카운트
};
include/linux/mmu_notifier.h:248-254 — interval tree 기반 알림 구조체입니다.
struct mmu_interval_notifier {
struct interval_tree_node interval_tree; // interval tree 노드
const struct mmu_interval_notifier_ops *ops; // 콜백 함수
struct mm_struct *mm; // 연결된 mm 구조체
struct hlist_node deferred_item; // 지연 목록 노드
unsigned long invalidate_seq; // 유효화 시퀀스 번호
};
include/linux/mmu_notifier.h:64-215 — MMU 알림 콜백 함수 테이블입니다.
struct mmu_notifier_ops {
/* 모든 페이지 해제 전 MM 종료 시 호출됨 */
void (*release)(struct mmu_notifier *subscription, struct mm_struct *mm);
/* PTE young 비트 테스트 및 클리어 후 젊은 페이지 플러시 */
int (*clear_flush_young)(struct mmu_notifier *subscription,
struct mm_struct *mm,
unsigned long start, unsigned long end);
/* PTE young 비트 젊은 페이지 플러시 없이 클리어 */
int (*clear_young)(struct mmu_notifier *subscription,
struct mm_struct *mm,
unsigned long start, unsigned long end);
/* PTE young 비트 테스트만 수행 */
int (*test_young)(struct mmu_notifier *subscription,
struct mm_struct *mm, unsigned long address);
/* 유효화 시작 알림 (SPTES 설정 금지) */
int (*invalidate_range_start)(struct mmu_notifier *subscription,
const struct mmu_notifier_range *range);
/* 유효화 종료 알림 (페이지 해제 완료) */
void (*invalidate_range_end)(struct mmu_notifier *subscription,
const struct mmu_notifier_range *range);
/* 세컨드 TLB 유효화 (非-sleeping) */
void (*arch_invalidate_secondary_tlbs)(struct mmu_notifier *subscription,
struct mm_struct *mm,
unsigned long start, unsigned long end);
/* 알림 메모리 할당 콜백 */
struct mmu_notifier *(*alloc_notifier)(struct mm_struct *mm);
/* 알림 메모리 해제 콜백 */
void (*free_notifier)(struct mmu_notifier *subscription);
};
include/linux/mmu_notifier.h:262-269 — 유효화 범위를 나타내는 구조체입니다.
struct mmu_notifier_range {
struct mm_struct *mm; // 대상 mm
unsigned long start; // 시작 가상 주소
unsigned long end; // 종료 가상 주소
unsigned flags; // 플래그 (MMU_NOTIFIER_RANGE_BLOCKABLE 등)
enum mmu_notifier_event event; // 이벤트 타입
void *owner; // 소유자 (MIGRATE/EXCLUSIVE용)
};
아래 그림은 공통 mmu_notifier_subscriptions 아래에서 두 알림 타입이 어떻게 갈라지는지 보여줍니다.
include/linux/mmu_notifier.h:51-60 — MMU 알림 이벤트 타입입니다.
enum mmu_notifier_event {
MMU_NOTIFY_UNMAP = 0, // munmap/mremap로 매핑 해제
MMU_NOTIFY_CLEAR, // PTE 클리어 (madvise 등)
MMU_NOTIFY_PROTECTION_VMA, // VMA 보호 변경 (mprotect)
MMU_NOTIFY_PROTECTION_PAGE, // 페이지 보호 변경 (PTE 직접 수정)
MMU_NOTIFY_SOFT_DIRTY, // 소프트 더티 계정
MMU_NOTIFY_RELEASE, // MM 해제 (interval notifier)
MMU_NOTIFY_MIGRATE, // 페이지 마이그레이션
MMU_NOTIFY_EXCLUSIVE, // 디바이스 독점 매핑
};
mm/mmu_notifier.c:698-708 — MM에 MMU 알림을 등록합니다.
int mmu_notifier_register(struct mmu_notifier *subscription,
struct mm_struct *mm)
{
int ret;
mmap_write_lock(mm);
ret = __mmu_notifier_register(subscription, mm);
mmap_write_unlock(mm);
return ret;
}
동작 원리: mmap_lock 쓰기 잠금을 획득한 후 __mmu_notifier_register()를 호출합니다. 이 함수는 notifier_subscriptions가 없으면 할당하고, 모든 잠금(mm_take_all_locks)을 획득한 후 알림을 리스트에 추가합니다.
mm/mmu_notifier.c:796-836 — MMU 알림 등록을 해제합니다.
void mmu_notifier_unregister(struct mmu_notifier *subscription,
struct mm_struct *mm)
{
BUG_ON(atomic_read(&mm->mm_count) <= 0);
if (!hlist_unhashed(&subscription->hlist)) {
/* SRCU가 ->release가 끝날 때까지 exit_mmap을 기다리게 합니다. */
int id;
id = srcu_read_lock(&srcu);
/* exit_mmap은 페이지를 해제하기 전에 ->release가 호출되도록 기다립니다. */
if (subscription->ops->release)
subscription->ops->release(subscription, mm);
srcu_read_unlock(&srcu, id);
spin_lock(&mm->notifier_subscriptions->lock);
/* __mmu_notifier_release가 먼저 지울 수 있으므로 list_del_rcu()는 쓰지 않습니다. */
hlist_del_init_rcu(&subscription->hlist);
spin_unlock(&mm->notifier_subscriptions->lock);
}
/* 실행 중인 모든 메서드가 끝날 때까지 기다립니다. */
synchronize_srcu(&srcu);
mmdrop(mm);
}
동작 원리: 먼저 ->release 콜백을 호출하여 모든 SPTES(세컨드 PTE)를 해제하고, 리스트에서 제거한 후 SRCU 동기화를 통해 진행 중인 콜백이 완료될 때까지 대기합니다.
mm/mmu_notifier.c:750-776 — 기존 알림을 재사용하거나 새로 할당하여 등록합니다.
struct mmu_notifier *mmu_notifier_get_locked(const struct mmu_notifier_ops *ops,
struct mm_struct *mm)
{
struct mmu_notifier *subscription;
int ret;
mmap_assert_write_locked(mm);
if (mm->notifier_subscriptions) {
subscription = find_get_mmu_notifier(mm, ops);
if (subscription)
return subscription;
}
subscription = ops->alloc_notifier(mm);
if (IS_ERR(subscription))
return subscription;
subscription->ops = ops;
ret = __mmu_notifier_register(subscription, mm);
if (ret)
goto out_free;
return subscription;
out_free:
subscription->ops->free_notifier(subscription);
return ERR_PTR(ret);
}
동작 원리: 먼저 동일한 ops 포인터를 가진 기존 알림이 있는지 확인합니다. 없으면 ops->alloc_notifier()로 새 알림을 할당하고 등록합니다. 이는 KVM과 같은 드라이버에서 여러 vCPU가 동일한 MM에 대해 중복 등록하는 것을 방지합니다.
mm/mmu_notifier.c:871-887 — MMU 알림에 대한 참조를 해제합니다.
void mmu_notifier_put(struct mmu_notifier *subscription)
{
struct mm_struct *mm = subscription->mm;
spin_lock(&mm->notifier_subscriptions->lock);
if (WARN_ON(!subscription->users) || --subscription->users)
goto out_unlock;
hlist_del_init_rcu(&subscription->hlist); // 마지막 참조시 리스트 제거
spin_unlock(&mm->notifier_subscriptions->lock);
call_srcu(&srcu, &subscription->rcu, mmu_notifier_free_rcu); // RCU 콜백
return;
out_unlock:
spin_unlock(&mm->notifier_subscriptions->lock);
}
mm/mmu_notifier.c:971-991 — interval tree 기반 알림을 삽입합니다.
int mmu_interval_notifier_insert(struct mmu_interval_notifier *interval_sub,
struct mm_struct *mm, unsigned long start,
unsigned long length,
const struct mmu_interval_notifier_ops *ops)
{
struct mmu_notifier_subscriptions *subscriptions;
int ret;
might_lock(&mm->mmap_lock);
subscriptions = smp_load_acquire(&mm->notifier_subscriptions);
if (!subscriptions || !subscriptions->has_itree) {
ret = mmu_notifier_register(NULL, mm);
if (ret)
return ret;
subscriptions = mm->notifier_subscriptions;
}
return __mmu_interval_notifier_insert(interval_sub, mm, subscriptions,
start, length, ops);
}
동작 원리: interval tree 기반 알림을 특정 VA 범위에 등록합니다. notifier_subscriptions가 없거나 itree를 사용하지 않으면 mmu_notifier_register(NULL, mm)로 초기화한 후 삽입합니다.
mm/mmu_notifier.c:915-952 — 활성 유효화 중인지에 따라 즉시 삽입하거나 지연 목록으로 넘깁니다.
/*
* If some invalidate_range_start/end region is going on in parallel
* we don't know what VA ranges are affected, so we must assume this
* new range is included.
*
* If the itree is invalidating then we are not allowed to change
* it. Retrying until invalidation is done is tricky due to the
* possibility for live lock, instead defer the add to
* mn_itree_inv_end() so this algorithm is deterministic.
*
* In all cases the value for the interval_sub->invalidate_seq should be
* odd, see mmu_interval_read_begin()
*/
spin_lock(&subscriptions->lock);
if (subscriptions->active_invalidate_ranges) {
if (mn_itree_is_invalidating(subscriptions))
hlist_add_head(&interval_sub->deferred_item,
&subscriptions->deferred_list);
else {
subscriptions->invalidate_seq |= 1;
interval_tree_insert(&interval_sub->interval_tree,
&subscriptions->itree);
}
interval_sub->invalidate_seq = subscriptions->invalidate_seq;
} else {
WARN_ON(mn_itree_is_invalidating(subscriptions));
/* 유효화 중이 아닌 구독의 시작 시퀀스는 홀수여야 합니다. */
interval_sub->invalidate_seq =
subscriptions->invalidate_seq - 1;
interval_tree_insert(&interval_sub->interval_tree,
&subscriptions->itree);
}
spin_unlock(&subscriptions->lock);
return 0;
mm/mmu_notifier.c:187-261 — 세컨드 스테이지 유효화와 충돌 회피를 위한 읽기 임계 구간을 시작합니다.
unsigned long
mmu_interval_read_begin(struct mmu_interval_notifier *interval_sub)
{
struct mmu_notifier_subscriptions *subscriptions =
interval_sub->mm->notifier_subscriptions;
unsigned long seq;
bool is_invalidating;
spin_lock(&subscriptions->lock);
seq = READ_ONCE(interval_sub->invalidate_seq); // 현재 시퀀스 읽기
is_invalidating = seq == subscriptions->invalidate_seq; // 유효화 중인지 확인
spin_unlock(&subscriptions->lock);
/* 유효화 중이면 완료될 때까지 대기 */
if (is_invalidating)
wait_event(subscriptions->wq,
READ_ONCE(subscriptions->invalidate_seq) != seq);
return seq;
}
mm/mmu_notifier.c:1037-1078 — interval 알림을 제거하고, 진행 중인 유효화가 있으면 끝날 때까지 기다립니다.
void mmu_interval_notifier_remove(struct mmu_interval_notifier *interval_sub)
{
struct mm_struct *mm = interval_sub->mm;
struct mmu_notifier_subscriptions *subscriptions =
mm->notifier_subscriptions;
unsigned long seq = 0;
might_sleep();
spin_lock(&subscriptions->lock);
if (mn_itree_is_invalidating(subscriptions)) {
/* insert 이후 deferred list에 들어갔지만 아직 처리되지 않은 경우입니다. */
if (RB_EMPTY_NODE(&interval_sub->interval_tree.rb)) {
hlist_del(&interval_sub->deferred_item);
} else {
hlist_add_head(&interval_sub->deferred_item,
&subscriptions->deferred_list);
seq = subscriptions->invalidate_seq;
}
} else {
WARN_ON(RB_EMPTY_NODE(&interval_sub->interval_tree.rb));
interval_tree_remove(&interval_sub->interval_tree,
&subscriptions->itree);
}
spin_unlock(&subscriptions->lock);
/* 유효화 콜백이 잡은 락과 겹칠 수 있으므로 잠들 수 있습니다. */
lock_map_acquire(&__mmu_notifier_invalidate_range_start_map);
lock_map_release(&__mmu_notifier_invalidate_range_start_map);
if (seq)
wait_event(subscriptions->wq,
mmu_interval_seq_released(subscriptions, seq));
/* mmu_interval_notifier_insert()의 mmgrab과 짝을 이룹니다. */
mmdrop(mm);
}
KVM 드라이버 초기화
└── kvm_mmu_notifier_init()
└── mmu_notifier_get_locked()
├── find_get_mmu_notifier() // 기존 알림 검색
│ └── [있으면] 참조 카운트 증가 후 반환
└── [없으면]
├── ops->alloc_notifier() // 새 알림 할당
└── __mmu_notifier_register()
├── notifier_subscriptions 할당 (최초 1회)
├── mm_take_all_locks()
└── hlist_add_head_rcu() // 리스트에 추가
이 흐름은 munmap(), mprotect(), mremap(), migrate_vma_collect() 같은 경로에서 시작됩니다.
호스트 커널 (PTE 변경)
├── mmu_notifier_invalidate_range_start()
│ ├── [itree 사용 시] mn_itree_invalidate()
│ │ └── interval_sub->ops->invalidate() // 세컨드 스테이지 SPTES 제거
│ └── [hlist 사용 시] mn_hlist_invalidate_range_start()
│ └── subscription->ops->invalidate_range_start()
├── [PTE 실제 변경]
└── mmu_notifier_invalidate_range_end()
├── [itree 사용 시] mn_itree_inv_end()
└── [hlist 사용 시] mn_hlist_invalidate_end()
mmu_interval_read_begin() mn_itree_inv_start_range()
├── seq = interval_sub->invalidate_seq ├── seq = ++subscriptions->invalidate_seq
├── seq == subs->invalidate_seq? └── interval_sub->ops->invalidate()
│ └── [같으면] wait_event() └── user_lock()
│ ├── mmu_interval_set_seq()
│ │ └── interval_sub->invalidate_seq = seq
│ └── user_unlock
│
└── mmu_interval_read_retry(seq)
└── interval_sub->invalidate_seq != seq? → true면 재시도 필요
| 비교 항목 | mmu_notifier (hlist) | mmu_interval_notifier (itree) |
|---|---|---|
| **데이터 구조** | 해시 리스트 | 인터벌 트리 (rb-tree) |
| **검색 복잡도** | O(n) 전체 순회 | O(log n) + 겹치는 범위만 |
| **사용처** | 전통적 MMU 알림 | KVM 세컨드 스테이지, VFIO |
| **콜백 수** | 7개 (release, clear_flush_young, clear_young, test_young, invalidate_range_start/end, arch_invalidate_secondary_tlbs) | 1개 (invalidate) |
| **시퀀스 번호** | 사용 안 함 | 충돌 회피를 위한 시퀀스 기반 |
| **동기화** | SRCU + spinlock | SRCU + spinlock + wait_queue |
| **지연 처리** | 없음 | deferred_list로 tree 업데이트 지연 |
| **블로킹 지원** | 블로킹/논블로킹 분리 | invalidate 콜백에서 블로킹 가능 |
| **등록 해제** | unregister (즉시) / put (지연) | remove (지연 대기) |
| 이벤트 타입 | 설명 | 사용 시나리오 |
|---|---|---|
| `MMU_NOTIFY_UNMAP` | munmap/mremap로 매핑 해제 | KVM VM 종료 시 |
| `MMU_NOTIFY_CLEAR` | PTE 클리어 (교체 등) | 페이지 교체, madvise |
| `MMU_NOTIFY_PROTECTION_VMA` | VMA 보호 변경 | mprotect 시스템 콜 |
| `MMU_NOTIFY_PROTECTION_PAGE` | 페이지 보호 변경 | PTE 직접 수정 |
| `MMU_NOTIFY_SOFT_DIRTY` | 소프트 더티 계정 | 페이지 추적, COW |
| `MMU_NOTIFY_RELEASE` | MM 해제 | VM 파괴 시 |
| `MMU_NOTIFY_MIGRATE` | 페이지 마이그레이션 | NUMA 밸런싱 |
| `MMU_NOTIFY_EXCLUSIVE` | 디바이스 독점 매핑 | GPU/DPU 메모리 할당 |
| 함수 | 역할 | 잠금 |
|---|---|---|
| `mmu_notifier_register()` | MM에 알림 등록 | mmap_write_lock |
| `mmu_notifier_unregister()` | 알림 등록 해제 (즉시) | SRCU |
| `mmu_notifier_get()` | 알림 재사용/할당 | mmap_write_lock |
| `mmu_notifier_put()` | 참조 해제 (지연 해제) | spinlock |
| `mmu_interval_notifier_insert()` | interval 알림 삽입 | might_lock(mmap_lock) |
| `mmu_interval_notifier_remove()` | interval 알림 제거 (대기) | spinlock + wait_event |