本章将深入分析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的内核实现机制:
-
数据结构组织与注册、注销:
- per-mm订阅管理结构
- notifier的注册与注销
-
事件触发路径:
- unmap路径(munmap)
- migration路径(页面迁移)
- protection change路径(mprotect)
-
核心API实现:
- 失效通知的实现
- 序号更新机制
下一章将介绍使用模式与最佳实践,包括如何正确使用这些API,常见错误和调试技巧。
🔗 导航
- 上一章: 第3章: MMU Interval Notifier核心原理
- 下一章: 审核中...
- 返回目录: Linux MMU Notifier 机制与应用系列目录