Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

58. MMU 알림 (MMU Notifier)

개요

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

핵심 자료구조

struct mmu_notifier_subscriptions

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;  // 지연된 추가/제거 목록
};

struct mmu_notifier

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;                       // 참조 카운트
};

struct mmu_interval_notifier

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;            // 유효화 시퀀스 번호
};

struct mmu_notifier_ops

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);
};

struct mmu_notifier_range

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 아래에서 두 알림 타입이 어떻게 갈라지는지 보여줍니다.

MMU notifier 구조 관계도

enum mmu_notifier_event

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,           // 디바이스 독점 매핑
};

핵심 함수

mmu_notifier_register()

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)을 획득한 후 알림을 리스트에 추가합니다.

mmu_notifier_unregister()

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 동기화를 통해 진행 중인 콜백이 완료될 때까지 대기합니다.

mmu_notifier_get_locked()

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에 대해 중복 등록하는 것을 방지합니다.

mmu_notifier_put()

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);
}

mmu_interval_notifier_insert()

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)로 초기화한 후 삽입합니다.

__mmu_interval_notifier_insert()

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;

mmu_interval_read_begin()

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;
}

mmu_interval_notifier_remove()

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);
}

호출 흐름

MMU 알림 등록 흐름

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()

Interval Notifier 읽기-쓰기 충돌 회피

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 + spinlockSRCU + 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

관련 문서

  • 00-overview.html — 메모리 관리 개요
  • 03-vma_mmap.html — VMA / mmap
  • 08-oom.html — OOM Killer
  • 28-rmap.html — Reverse Mapping
  • 38-pagewalk.html — Page Table Walk
  • 13-numa.html — NUMA 메모리 관리
  • 51-arm64_vs_x86.html — 아키텍처별 차이점
  • 52-linux_6x_to_7x.html — Linux 6.x → 7.0 변경점