第4章: MMU notifier内核实现机制

本章将深入分析MMU Notifier在Linux内核中的具体实现,包括数据结构组织、关键API的实现细节、事件触发路径以及并发控制机制。

4.1 数据结构组织

前面两章讲了notifier,这些notifier要被管理起来,以便事件发生时进行调用。这就是下面的struct mmu_notifier_subscriptions

4.1.1 per-mm notifier链表

每个mm_struct通过notifier_subscriptions指针来管理订阅:

c 复制代码
/* include/linux/mm_types.h */
struct mm_struct {
    /* ... */
    struct mmu_notifier_subscriptions *notifier_subscriptions;
    /* ... */
};

订阅管理结构的完整定义如下:

c 复制代码
/* mm/mmu_notifier.c */
struct mmu_notifier_subscriptions {
    /* 传统mmu_notifier链表 */
    struct hlist_head list;
    
    /* interval tree相关 */
    bool has_itree;                         /* 是否有interval notifier */
    struct rb_root_cached itree;            /* interval notifier红黑树 */
    
    /* 同步控制 */
    spinlock_t lock;                        /* 保护tree和序号 */
    unsigned long invalidate_seq;           /* 全局失效序号 */
    unsigned long active_invalidate_ranges; /* 活跃失效范围计数 */
    
    /* 等待队列和延迟列表 */
    wait_queue_head_t wq;                   /* 等待失效完成 */
    struct hlist_head deferred_list;        /* 延迟操作列表 */
};

字段用途详解:

1. list - 传统notifier链表

所有传统mmu_notifier订阅者链接在此:

c 复制代码
/* 添加notifier到链表 */
hlist_add_head_rcu(&subscription->hlist, &subscriptions->list);

/* 遍历链表通知所有订阅者 */
hlist_for_each_entry_rcu(subscription, &subscriptions->list, hlist) {
    if (subscription->ops->invalidate_range_start)
        subscription->ops->invalidate_range_start(subscription, range);
}

2. itree - interval tree红黑树

基于增强红黑树实现的区间树,存储所有mmu_interval_notifier

c 复制代码
struct rb_root_cached {
    struct rb_root rb_root;      /* 红黑树根 */
    struct rb_node *rb_leftmost; /* 最左节点(缓存) */
};

缓存最左节点可以加速查找最小值。

3. invalidate_seq - 序号

关键的同步机制:

  • 偶数:稳定状态
  • 奇数:失效进行中

4. active_invalidate_ranges - 活跃计数

支持嵌套和并发失效:

c 复制代码
/* 第一个失效操作 */
if (++subscriptions->active_invalidate_ranges == 1)
    subscriptions->invalidate_seq |= 1;  /* 设置奇数位 */

/* 最后一个失效完成 */
if (--subscriptions->active_invalidate_ranges == 0) {
    subscriptions->invalidate_seq++;     /* 递增为偶数 */
    wake_up_all(&subscriptions->wq);     /* 唤醒等待者 */
}

5. deferred_list - 延迟操作队列

当失效正在进行时(序号为奇数),不能直接修改interval tree,需要延迟:

c 复制代码
if (mn_itree_is_invalidating(subscriptions)) {
    /* 加入延迟队列 */
    hlist_add_head(&interval_sub->deferred_item,
                   &subscriptions->deferred_list);
} else {
    /* 直接插入tree */
    interval_tree_insert(&interval_sub->interval_tree,
                         &subscriptions->itree);
}

延迟的操作会在mn_itree_inv_end()中统一处理。

可以看出,mmu notifier和mmu interval notifier是用mmu_notifier_subscriptions 这一个结构体管理的,这个结构实际上就是一个链表容器和树容器。

接下来看下notifier的注册和注销,我尽量简化了代码,如果还是觉得多,只看函数名就可以了。

4.1.2 注册与注销流程

mmu_notifier注册
c 复制代码
int mmu_notifier_register(struct mmu_notifier *subscription,
                         struct mm_struct *mm)
{
    struct mmu_notifier_subscriptions *subscriptions = NULL;
    int ret;
    
    /* 取mmap_lock写锁 */
    mmap_write_lock(mm);
    ret = __mmu_notifier_register(subscription, mm);
    mmap_write_unlock(mm);
    
    return ret;
}

int __mmu_notifier_register(struct mmu_notifier *subscription,
                            struct mm_struct *mm)
{
    struct mmu_notifier_subscriptions *subscriptions;
    int ret;
    
    /* 确保mm_users > 0 */
    if (WARN_ON(atomic_read(&mm->mm_users) <= 0))
        return -EINVAL;
    
    /* 获取或创建subscriptions */
    subscriptions = smp_load_acquire(&mm->notifier_subscriptions);
    if (!subscriptions) {
        subscriptions = kzalloc(sizeof(*subscriptions), GFP_KERNEL);
        if (!subscriptions)
            return -ENOMEM;
        
        INIT_HLIST_HEAD(&subscriptions->list);
        spin_lock_init(&subscriptions->lock);
        subscriptions->invalidate_seq = 2;  /* 初始为偶数 */
        subscriptions->itree = RB_ROOT_CACHED;
        init_waitqueue_head(&subscriptions->wq);
        INIT_HLIST_HEAD(&subscriptions->deferred_list);
        
        /* 原子发布 */
        smp_store_release(&mm->notifier_subscriptions, subscriptions);
    }
    
    if (subscription) {
        /* 初始化订阅者 */
        subscription->mm = mm;
        subscription->users = 1;
        
        /* 加入链表 */
        spin_lock(&subscriptions->lock);
        hlist_add_head_rcu(&subscription->hlist, &subscriptions->list);
        spin_unlock(&subscriptions->lock);
    } else {
        /* 只是确保subscriptions存在,用于interval notifier */
        subscriptions->has_itree = true;
    }
    
    return 0;
}
mmu_notifier注销
c 复制代码
void mmu_notifier_unregister(struct mmu_notifier *subscription,
                             struct mm_struct *mm)
{
    /* 从链表移除 */
    spin_lock(&mm->notifier_subscriptions->lock);
    hlist_del_rcu(&subscription->hlist);
    spin_unlock(&mm->notifier_subscriptions->lock);
    
    /* 调用release回调 */
    if (subscription->ops->release)
        subscription->ops->release(subscription, mm);
    
    /* 等待所有正在进行的回调完成 */
    synchronize_srcu(&srcu);
    
    /* 释放内存 */
    kfree(subscription);
}
interval_notifier注册
c 复制代码
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;
    
    /* 确保subscriptions存在 */
    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);
}

static int __mmu_interval_notifier_insert(
    struct mmu_interval_notifier *interval_sub,
    struct mm_struct *mm,
    struct mmu_notifier_subscriptions *subscriptions,
    unsigned long start,
    unsigned long length,
    const struct mmu_interval_notifier_ops *ops)
{
    /* 初始化字段 */
    interval_sub->mm = mm;
    interval_sub->ops = ops;
    RB_CLEAR_NODE(&interval_sub->interval_tree.rb);
    interval_sub->interval_tree.start = start;
    
    /* 计算last(包含结束点)*/
    if (length == 0 ||
        check_add_overflow(start, length - 1,
                          &interval_sub->interval_tree.last))
        return -EOVERFLOW;
    
    /* 增加mm引用计数 */
    mmgrab(mm);
    
    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));
        
        /* 设置为当前序号-1(保证为奇数且小于当前)*/
        interval_sub->invalidate_seq = subscriptions->invalidate_seq - 1;
        interval_tree_insert(&interval_sub->interval_tree,
                            &subscriptions->itree);
    }
    
    spin_unlock(&subscriptions->lock);
    
    return 0;
}

为什么设置invalidate_seq - 1?

c 复制代码
/*
 * 新注册的notifier的序号应该:
 * 1. 是奇数(确保read_retry能检测到)
 * 2. 不等于当前的invalidate_seq
 * 3. 远小于当前序号,避免序号回绕问题
 *
 * 例如:
 * - 当前 invalidate_seq = 100(偶数,稳定)
 * - 新notifier设置为 99(奇数)
 * - 任何 read_retry(99) 都会失败(因为notifier->seq != 99)
 * - 这会强制驱动重新开始操作,获取最新状态
 */

4.2 常用事件触发路径分析

4.2.1 unmap路径

复制代码
munmap(addr, len)
  ↓
__do_munmap()
  ↓
[1] mmu_notifier_invalidate_range_start()
  ├─ 通知所有mmu_notifier订阅者
  ├─ 通知所有重叠的interval_notifier
  └─ 设备必须停止访问,但页表仍有效
  ↓
unmap_region()
  ├─ unmap_vmas()      ← 清除PTE
  ├─ free_pgtables()   ← 释放页表
  └─ tlb_finish_mmu()  ← 刷新TLB
  ↓
[2] mmu_notifier_invalidate_range_end()
  ├─ 通知操作完成
  └─ 设备可以释放资源

关键保证:
- [1]时刻:页面仍可访问,设备应停止新访问
- [1]到[2]之间:清除页表、释放内存
- [2]时刻:页面已释放,设备应完成所有清理

4.2.2 migration路径

页面迁移比unmap更复杂,因为物理地址改变但虚拟地址不变。

复制代码
migrate_pages()
  ↓
unmap_and_move()
  ↓
[1] mmu_notifier_invalidate_range_start(&range)
  ├─ 初始化MMU_NOTIFY_MIGRATE类型的通知范围
  ├─ 通知所有mmu_notifier/interval_notifier订阅者开始迁移
  └─ 设备需停止访问旧物理页,旧页表仍有效
    ↓
  设备驱动回调 → gpu_invalidate()
  ├─ 检查是否自身触发迁移 → 是则忽略
  ├─ 加锁保护GPU缓冲区
  ├─ gpu_invalidate_pte_range() → 失效GPU侧旧页表
  ├─ 迁移场景下标记下次访问触发page fault(更新新物理地址)
  └─ 更新seq并解锁
  ↓
try_to_unmap(page, TTU_MIGRATION)  ← 解除旧页的映射
  ↓
move_to_new_page()                ← 拷贝页面内容到新物理页
  ↓
remove_migration_ptes()           ← 建立新物理页的虚拟地址映射
  ↓
[2] mmu_notifier_invalidate_range_end(&range)
  ├─ 通知所有订阅者迁移操作完成
  └─ 设备可更新物理地址映射并恢复访问

关键保证:
[1] 时刻:旧物理页仍可访问,设备应停止对旧页的新访问
[1] 到 [2] 之间:解除旧页映射、拷贝数据到新页、建立新页映射
[2] 时刻:新页映射已生效,设备应更新物理地址并完成清理

4.2.3 protection change 路径

权限改变路径涉及mprotect系统调用,核心是修改虚拟内存区域(VMA)的访问权限,并同步更新页表和通知设备。

复制代码
mprotect()  ← mprotect系统调用入口
  ↓
mmap_write_lock()  ← 加锁保护mm_struct
  ↓
find_vma()         ← 查找待修改权限的VMA
  ↓
mprotect_fixup()   ← 权限修改核心入口
  ├─ VMA拆分/合并(处理权限不连续场景)
  └─ change_protection()  ← 执行实际的权限修改
     ↓
     [1] mmu_notifier_invalidate_range_start(&range)
        ├─ 初始化MMU_NOTIFY_PROTECTION_VMA类型的通知范围
        ├─ 通知所有mmu_notifier/interval_notifier订阅者开始权限变更
        └─ 设备需停止基于旧权限的访问(页表仍有效但权限即将变更)
     ↓
     change_protection_range()  ← 遍历并修改页表权限
        ├─ pgd_offset() → 定位页全局目录项
        ├─ change_p4d_range() → 逐层遍历页表(p4d/pud/pmd/pte)
        ├─ 修改PTE的访问权限(读/写/执行)
        └─ flush_tlb_range() → 刷新TLB使新权限生效
     ↓
     [2] mmu_notifier_invalidate_range_end(&range)
        ├─ 通知所有订阅者权限变更操作完成
        └─ 设备可更新自身页表权限并恢复访问
  ↓
更新VMA的vm_flags  ← 持久化新的权限配置
  ↓
mmap_write_unlock()  ← 释放mm_struct锁

关键保证:
[1] 时刻:旧权限的页表仍有效,设备应停止基于旧权限的新访问,避免权限不一致导致的错误
[1] 到 [2] 之间:遍历页表层级修改 PTE 权限、刷新 TLB,核心的权限变更操作在此阶段完成
[2] 时刻:页表权限已更新且 TLB 已刷新,设备应同步更新自身(如 GPU)的页表权限,按新权限提供访问

4.3 核心API实现剖析

由前面两章我们了解了最重要的两个动作:失效通知和序号更新。

4.3.1 失效通知实现剖析

invalidate_range_start实现
c 复制代码
int __mmu_notifier_invalidate_range_start(struct mmu_notifier_range *range)
{
    struct mmu_notifier_subscriptions *subscriptions =
        range->mm->notifier_subscriptions;
    int ret;
    
    /* 先处理interval tree */
    if (subscriptions->has_itree) {
        ret = mn_itree_invalidate(subscriptions, range);
        if (ret)
            return ret;
    }
    
    /* 再处理传统notifier链表 */
    if (!hlist_empty(&subscriptions->list))
        return mn_hlist_invalidate_range_start(subscriptions, range);
    
    return 0;
}
interval tree失效流程
c 复制代码
static int mn_itree_invalidate(struct mmu_notifier_subscriptions *subscriptions,
                               const struct mmu_notifier_range *range)
{
    struct mmu_interval_notifier *interval_sub;
    unsigned long cur_seq;
    
    /* 开始失效,获取序号 */
    interval_sub = mn_itree_inv_start_range(subscriptions, range, &cur_seq);
    
    /* 遍历所有重叠的interval notifier */
    while (interval_sub) {
        bool ret;
        
        /* 调用驱动的invalidate回调 */
        ret = interval_sub->ops->invalidate(interval_sub, range, cur_seq);
        
        if (!ret) {
            /* 返回false表示无法阻塞但需要睡眠 */
            if (WARN_ON(mmu_notifier_range_blockable(range)))
                continue;
            
            /* 非阻塞模式失败,清理并返回 */
            mn_itree_inv_end(subscriptions);
            return -EAGAIN;
        }
        
        /* 下一个重叠的notifier */
        interval_sub = mn_itree_inv_next(interval_sub, range);
    }
    
    return 0;
}

4.3.2 序号更新机制

mmu_interval_read_begin实现
c 复制代码
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;
    
    /*
     * 检查notifier的序号是否等于全局序号
     * 如果相等,说明正在失效中
     */
    spin_lock(&subscriptions->lock);
    seq = READ_ONCE(interval_sub->invalidate_seq);
    is_invalidating = (seq == subscriptions->invalidate_seq);
    spin_unlock(&subscriptions->lock);
    
    /* lockdep检查 */
    lock_map_acquire(&__mmu_notifier_invalidate_range_start_map);
    lock_map_release(&__mmu_notifier_invalidate_range_start_map);
    
    /* 如果正在失效,等待完成 */
    if (is_invalidating) {
        wait_event(subscriptions->wq,
                   READ_ONCE(subscriptions->invalidate_seq) != seq);
    }
    
    /*
     * 返回的序号可能已经与notifier->invalidate_seq不同了
     * 这没关系,read_retry会检测到并要求重试
     */
    return seq;
}

等待机制详解:

c 复制代码
/*
 * wait_event会睡眠等待条件满足:
 * 
 * wait_event(wq, condition)
 * 等价于:
 *   while (!condition) {
 *       prepare_to_wait(&wq, &wait, TASK_UNINTERRUPTIBLE);
 *       if (!condition)
 *           schedule();  // 睡眠
 *       finish_wait(&wq, &wait);
 *   }
 * 
 * 当mn_itree_inv_end调用wake_up_all(&wq)时,
 * 所有等待的线程会被唤醒并重新检查条件。
 */
mmu_interval_set_seq实现
c 复制代码
static inline void
mmu_interval_set_seq(struct mmu_interval_notifier *interval_sub,
                    unsigned long cur_seq)
{
    /*
     * 必须使用WRITE_ONCE确保:
     * 1. 编译器不优化掉这次写入
     * 2. 其他CPU能看到最新值
     */
    WRITE_ONCE(interval_sub->invalidate_seq, cur_seq);
}

本章小结

本章深入分析了MMU Notifier的内核实现机制:

  1. 数据结构组织与注册、注销

    • per-mm订阅管理结构
    • notifier的注册与注销
  2. 事件触发路径

    • unmap路径(munmap)
    • migration路径(页面迁移)
    • protection change路径(mprotect)
  3. 核心API实现

    • 失效通知的实现
    • 序号更新机制

下一章将介绍使用模式与最佳实践,包括如何正确使用这些API,常见错误和调试技巧。


🔗 导航

相关推荐
摸鱼仙人~17 小时前
RAG 系统中的 TOC Enhance:用“目录增强”提升检索与生成效果
linux·运维·服务器
xingzhemengyou117 小时前
Linux dmesg 查看系统启动日志
linux
华如锦17 小时前
一.2部署——大模型服务快速部署vLLM GPU 安装教程 (Linux)
java·linux·运维·人工智能·后端·python·vllm
Jacob程序员17 小时前
Linux scp命令:高效远程文件传输指南
linux·运维·服务器
corpse201017 小时前
Transparent Huge Pages(透明大页)对redis的影响
linux·redis
Cx330❀17 小时前
Linux进程前言:从冯诺依曼体系到操作系统的技术演进
linux·运维·服务器
阿巴~阿巴~17 小时前
帧长、MAC与ARP:解密局域网通信的底层逻辑与工程权衡
linux·服务器·网络·网络协议·tcp/ip·架构·以太网帧
oMcLin17 小时前
如何在 Manjaro Linux 上实现高效的 Ceph 存储集群,提升大规模文件存储的冗余性与性能?
linux·运维·ceph
Chennnng18 小时前
ubuntu重装系统但是不改动文件的方法
linux·运维·ubuntu