内存管理-63-VMA-2-vmacache

基于msm-5.4

一、vmacache简介

1. 实现文件

mm/vmacache.c
include/linux/vmacache.h

默认编译,没有配置宏。

2. 它要解决的问题

把海量重复的、局部性很强的 VMA 查询挡在红黑树查询之前。内核里大量路径都会查 VMA,例如:

(1) page fault 时判断地址落在哪个 VMA;
(2) brk/mmap/munmap/mprotect 做区间检查;
(3) copy_to_user/copy_from_user 相关异常路径;
(4) 栈自动扩展判断;
(5) /proc 相关地址空间遍历前的定位;

这些查询里最常见的模式不是“随机地址查询”,而是:连续访问相邻页、多次访问同一个堆区、多次访问同一个栈区、多次访问同一个共享库映射区、也就是说,同一个线程短时间内经常反复命中同一个 VMA。

如果每次都走红黑树,即使复杂度只有O(logn),在高频 fault 和高频地址检查场景下,成本也不低。于是 Linux 加了 vmacache:先查最近命中过的几个 VMA,命中就直接返回; 只有 miss 才走红黑树慢路径。

3. 实现原理

vmacache 可以理解成:挂在当前线程上的一个极小的 VMA 命中缓存。它不是全局缓存,也不是 mm 公共缓存,而是 current 私有。通常很小,默认只有 4 个槽位。只缓存最近命中过的 VMA。只优化“地址落在某个 VMA 内”的情况,不负责完整实现 find_vma() 的全部语义。

vmacache 在 find_vma(addr) 中充当快速路径。先查 current->vmacache,若命中直接返回,若未命中再查 mm->mm_rb,查到后顺手回填 vmacache。标准的“快路径 + 慢路径”设计。####

4. 典型命中场景

(1) page fault 连续发生在同一 VMA
如果涉及多个缺页,很多 fault 都会落在同一堆 VMA 中。第一次慢路径找到 VMA 后,后面就能不断从 vmacache 命中。

(2) 用户栈访问
函数调用、局部变量访问、异常返回等,很多访问集中在栈 VMA。

(3) 共享库代码执行
指令 fetch 和相关异常处理往往落在同一个 .so 的 VMA。

(4) 频繁访问某个匿名映射
比如 mmap 出来的一块大缓冲区。

5. 局限

(1) 它不优化随机地址访问
如果访问点高度离散,跨很多 VMA,命中率就低。

(2) 它只对当前线程有效
别的线程查同一个 mm,用的是自己的 vmacache。

(3) 只能做快路径,不能单独承担完整查找职责
只返回满足:vma->vm_start <= addr && vma->vm_end > addr 的VMA,而不是像 find_vma(mm, addr) 返回第一个满足:vma->vm_end > addr 的VMA.


二、相关结构

1. struct vmacache

#define VMACACHE_BITS 2
#define VMACACHE_SIZE (1U << VMACACHE_BITS) //1<<2=4
#define VMACACHE_MASK (VMACACHE_SIZE - 1) //4-1=3

struct vmacache {
    u64 seqnum;
    struct vm_area_struct *vmas[VMACACHE_SIZE]; //4
};

此结构描述一个每线程的vamcache结构。

成员介绍:

seqnum: 表示这个线程的VMA缓存是基于哪个地址空间版本建立的。用于和 mm->vmacache_seqnum 比较,判断合法性,只要两者不一致,就说明缓存可能过期,不能再信任。

vmas[]: 是个指针数组,保存最近缓存的几个VMA。默认4个元素,因为 vmacache 追求的是“非常便宜地挡掉大量重复查询”,不是追求理论最优命中率。


2. struct task_struct

struct task_struct {
    struct vmacache    vmacache;
};

将VMA缓存放在线程本地缓存的好处是:不需要复杂共享同步; 访问局部性更强; 每个线程缓存自己的热点区域; 实现简单,收益稳定。如果把 cache 放在 mm 级别,共享给所有线程,会遇到两个问题:多线程并发更新,锁和同步成本会上升; 不同线程访问的热点 VMA 可能完全不同,互相污染缓存。


3. struct mm_struct

struct mm_struct {
    u64 vmacache_seqnum;
};

成员介绍:

vmacache_seqnum: 表示这个 mm 的 VMA 布局当前版本号。


三、实现逻辑

1. 缓存失效判断

这是 vmacache 机制正确性的保证。只要 VMA 布局发生变化,旧缓存就可能不对。例如:
mmap() 新建映射、munmap() 删除映射、mprotect() 导致 VMA 分裂/合并、brk() 扩堆/缩堆、mremap() 移动映射、其它导致 VMA 链表或红黑树结构变化的操作。这些变化发生时,递增 mm->vmacache_seqnum,使旧缓存整体失效。然后 vmacache_valid(mm) 判断时发现:"current->vmacache.seqnum != mm->vmacache_seqnum" 就知道缓存已经过期,直接返回 miss。

这意味着 vmacache 的失效策略不是逐项精细删除,而是按地址空间版本号整体失效。

vmacache_invalidate() 的调用路径:

SYSCALL_DEFINE5(mremap //mremap.c
__split_vma //mmap.c
shift_arg_pages //fs/exec.c
    vma_adjust //linux/mm.h
    __vma_merge //mmap.c
        __vma_adjust //mmap.c
            __vma_unlink_prev //mmap.c
            __vma_adjust //mmap.c
                __vma_unlink_common //mmap.c
                    vmacache_invalidate(mm);

SYSCALL_DEFINE1(brk //mmap.c
do_munmap //mmap.c
__vm_munmap //mmap.c
SYSCALL_DEFINE5(mremap //mremap.c
    __do_munmap //mmap.c
        detach_vmas_to_be_unmapped //mmap.c
            vmacache_invalidate(mm);

这里说的VMA布局变化,应该可以理解为是VMA红黑树发生过变化。


2. 实现代码

代码实现很简单,这里直接全部贴出来:

#define VMACACHE_SHIFT    PMD_SHIFT //21

/* 用地址哈希出一个初始位置,可以让“相近地址”更容易落到相近槽位 */
#define VMACACHE_HASH(addr) ((addr >> VMACACHE_SHIFT) & VMACACHE_MASK) /* (addr>>21) & 3  */

static inline void vmacache_flush(struct task_struct *tsk) //vmacache.h
{
    memset(tsk->vmacache.vmas, 0, sizeof(tsk->vmacache.vmas));
}

/* 对外接口1:更新mm的缓存序号 */
static inline void vmacache_invalidate(struct mm_struct *mm)
{
    mm->vmacache_seqnum++;
}

/* 传的参数mm必须得是当前线程的,且当前线程不能是内核线程 */
static inline bool vmacache_valid_mm(struct mm_struct *mm)
{
    return current->mm == mm && !(current->flags & PF_KTHREAD);
}

static bool vmacache_valid(struct mm_struct *mm)
{
    struct task_struct *curr;

    /* 要求参数mm属于当前线程,且不是内核线程 */
    if (!vmacache_valid_mm(mm))
        return false;

    curr = current;
    /* 这个 mm 的 VMA 布局自缓存建立以来变化了,更新为mm的seqnum,并清空 vmas[] 数组 */
    if (mm->vmacache_seqnum != curr->vmacache.seqnum) {
        curr->vmacache.seqnum = mm->vmacache_seqnum;
        /* 清空 tsk->vmacache.vmas[] 数组 */
        vmacache_flush(curr);
        return false;
    }
    return true;
}

/* 对外接口2:更新vmacache缓存 */
void vmacache_update(unsigned long addr, struct vm_area_struct *newvma)
{
    /* newvma->mm必须得是当前线程的,且当前线程不能是内核线程 */
    if (vmacache_valid_mm(newvma->vm_mm))
        /* 直接进行覆盖着写 */
        current->vmacache.vmas[VMACACHE_HASH(addr)] = newvma;
}


/*
 * 对外接口3:根据 addr 在 vmacache缓存中查找包含的VMA
 *
 * 它是 find_vma() 的前置“快路径”, 用于确认 addr 是否落在小缓存中的某个VMA中,即 addr 属
 * 于 [vm_start, vm_end) ,不是 vm_end > addr 了, 和 find_vma 语义不同 
 */
struct vm_area_struct *vmacache_find(struct mm_struct *mm, unsigned long addr)
{
    /* (addr>>21) & 3 以2M为单位取hash, 决定初始访问下标。2M估计是经验值 */
    int idx = VMACACHE_HASH(addr);
    int i;

    /* 统计此函数被调用了多少次。默认不使能DEBUG,空实现 */
    count_vm_vmacache_event(VMACACHE_FIND_CALLS);

    /*
     * 若mm不是当前线程的,或者 mm 的 VMA 布局已经变化,这时旧 cache 就不能信任,
     * 必须直接返回 NULL。
     */
    if (!vmacache_valid(mm))
        return NULL;

    /*
     * 从哈希槽 idx 开始,线性探测整个 vmacache 只有4个。
     * 所以这里不是大哈希表,而是“一个很小的循环数组 + 顺序探测”。
     */
    for (i = 0; i < VMACACHE_SIZE; i++) { //4
        struct vm_area_struct *vma = current->vmacache.vmas[idx];
        /* 如果这个槽位上缓存了一个 vma,就检查 addr 是否落在其中。 */
        if (vma) {
            /* VMA区间是[vm_start, vm_end), 这里判断的是完全覆盖 */
            if (vma->vm_start <= addr && vma->vm_end > addr) {
                /* 统计缓存命中次数,可用于观察命中率 */
                count_vm_vmacache_event(VMACACHE_FIND_HITS);
                return vma;
            }
        }
        /* 线性探测下一个槽位。如果走到数组末尾,就回绕到 0 */
        if (++idx == VMACACHE_SIZE)
            idx = 0;
    }

    /* 整个小缓存都没命中,返回 NULL,让调用者走慢路径 */
    return NULL;
}

/* 对外接口4:查找start和end都严格相等的VMA */
struct vm_area_struct *vmacache_find_exact(struct mm_struct *mm,
                       unsigned long start, unsigned long end)
{
    /* 4个元素的索引开始遍历的下标 */
    int idx = VMACACHE_HASH(start);
    int i;

    /* 记录进入次数,和上面函数用的是同一个值 */
    count_vm_vmacache_event(VMACACHE_FIND_CALLS);

    /* 验证有效 */
    if (!vmacache_valid(mm))
        return NULL;

    /* 要求start和end都严格相等 */
    for (i = 0; i < VMACACHE_SIZE; i++) { //4
        struct vm_area_struct *vma = current->vmacache.vmas[idx];
        if (vma && vma->vm_start == start && vma->vm_end == end) {
            count_vm_vmacache_event(VMACACHE_FIND_HITS);
            return vma;
        }
        if (++idx == VMACACHE_SIZE)
            idx = 0;
    }

    return NULL;
}


四、调试

1. vmacache命中率

上文中使用 count_vm_vmacache_event(VMACACHE_FIND_CALLS/VMACACHE_FIND_HITS) 记录了查找vmacache的次数和名字的次数。

enum vm_event_item { //linux/vm_event_item.h
    ...
#ifdef CONFIG_DEBUG_VM_VMACACHE
    VMACACHE_FIND_CALLS,
    VMACACHE_FIND_HITS,
#endif
};

char * const vmstat_text[] = { //vmstat.c
#ifdef CONFIG_DEBUG_VM_VMACACHE
    "vmacache_find_calls",
    "vmacache_find_hits",
#endif

使能 CONFIG_DEBUG_VM_VMACACHE 后,通过 cat /proc/zoneinfo 可以查看,详见《内存管理-31-系统内存统计-3-/proc/zoneinfo


五、总结

1. 总结起来就是 vmacache 是一个线程本地、极小、按版本号整体失效的 VMA 热点缓存。当VMA红黑树发生变化时,会递增 mm->vmacache_seqnum 这个版本号。

 

posted on 2026-04-18 14:56  Hello-World3  阅读(4)  评论(0)    收藏  举报

导航