Linux TLB 刷新的懒惰模式【转】

转自:https://blog.csdn.net/Henzox/article/details/41963271

 我们都知道,在切换页表时会刷新 TLB,这样就可以使用新的地址空间,那什么是 TLB 刷新的懒惰模式呢?

        TLB 是什么这里不作多的解释,可以简单理解为,为了加快 MMU 对虚拟地址的转换而增加的缓存,它记录了一个虚拟地址对应的内存页的物理地址。其实就是根据虚拟地址的前 20 位,来建立一个个条目,对应记录通过查找页表来记录的内存页的物理地址。 
        既然有缓存,那么被缓存的内容改变时,就涉及到缓存的刷新,就是 TLB 的刷新问题,当一个页表结构发生变化时,使用该页表节构的 CPU 就应该刷新自己的 TLB。
        显然进程切换时,由于地址空间发生了变化,TLB 应该得到刷新,然而,内核进程只访问内核空间的地址范围,而每个进程的内核空间的地址范围相同,所以如果 CPU 从一个用户进程切换到一个内核进程,由于用户进程和内核进程的内核地址空间部分相同,其实是不用切换页表的,内核进程依然可以使用前一个用户进程的内核部分的地址空间。这样就省去了刷新 TLB 带来的性能损耗。其实内核进程并不拥有自己的页表集。
        想像着是挺完美的,但是在 SMP 构架下,这将带来一些问题,例如,在某一核上 CPU0 刚从一用户进程切换到内核进程,该内核进程沿用该用户进程的地址空间,但它只访问内核空间部分,这不会有问题,然而,如果该用户进程在另一个 CPU1 核上被调度,并且在 CPU0 用它的地址空间时,它在 CPU1 上修改了自己内核空间的页表,此时,若 CPU0 如果访问它的地址空间是非常危险的,不管是被缓存的地址还是未被缓存的地址都将可能带来意想不到的严重后果。
        那么,难道这种美好的事情就要被上面的情况的发生扼杀,而每次都要刷新 TLB,重新加载页表么。显然还是有补救办法的,如果在 CPU0 上的内核进程执行期间,它所引用的用户进程的地址空间没有被调度并执行完毕的情况还是非常多的,这种不刷新 TLB 带来的性能提升还是可以利用一下的,谁让 Linux 是一个精打细算的内核呢。
        如何办到这一点,其实很简单,当内核空间页表集在其它 CPU 上更改时,会调用 flush_tlb_all,它会让每个 CPU 去刷新自己的 TLB。

        那么当用户空间的页表集在其它 CPU 上更改时,由于 CPU0 虽然引用了相同的地址空间,但由于它是一个内核进程,它不会去访问用户空间的地址,那么,那些失效和 TLB 项也不会造成危险,也就是它可以不去立即刷新 TLB。所以此时其它的 CPU 往往会发送一个 IPI 给其它引用该地址空间的 CPU,以通知他们自己更改了用户空间的页表,其它  CPU 就会根据自己的状态作出相应的处理,如果是懒惰模式,就不用刷新 TLB。

        这就引入了 TLB 刷新的懒惰模式。
        Linux 为每一个 CPU 创建了一个节构,它是一个每 CPU 数据,所以不需要加锁,每个 CPU 只访问自己的节构,它记录了该 CPU 的状态,TLBSTATE_OK 表示非懒惰模式, TLBSTATE_LAZY 表示懒惰模式。它还记录该 CPU 引用的地址空间节构,是一个 mm_struct 类型的节构体指针,它记录了一个进程的地址空间的所有信息,mm_struct 有一个成员 cpu_vm_mask, 是一个默认 32 位的掩码,如果某个 CPU 在使用这个地址空间,则相应位置会被置位,显然,它将支持最多 32 个 CPU。这样情况就简单了,当一个 CPU 从一个用户进程调出,调用一个内核进程时,它会设置自己的进入 TLBSTATE_LAZY 模式,并且把它引用的用户进程的 mm_struct 中相应的位置位,此时并不切换页表节构,即不加载内核空间的页目录,而如果其它 CPU 更改了用户空间的页表,它会发送一个 IPI 消息,此时使用该地址空间的 CPU 都会收到这个消息,消息的响应函数为 smp_invalidate_interrupt,代码如下:

void smp_invalidate_interrupt(struct pt_regs *regs)
{
    unsigned long cpu;
 
    cpu = get_cpu();
 
    if (!cpu_isset(cpu, flush_cpumask))
        goto out;
        /*
         * This was a BUG() but until someone can quote me the
         * line from the intel manual that guarantees an IPI to
         * multiple CPUs is retried _only_ on the erroring CPUs
         * its staying as a return
         *
         * BUG();
         */
 
    if (flush_mm == per_cpu(cpu_tlbstate, cpu).active_mm) {
        if (per_cpu(cpu_tlbstate, cpu).state == TLBSTATE_OK) {
            if (flush_va == TLB_FLUSH_ALL)
                local_flush_tlb();
            else
                __flush_tlb_one(flush_va);
        } else
            leave_mm(cpu);
    }
    ack_APIC_irq();
    smp_mb__before_clear_bit();
    cpu_clear(cpu, flush_cpumask);
    smp_mb__after_clear_bit();
out:
    put_cpu_no_resched();
    __get_cpu_var(irq_stat).irq_tlb_count++;
}

        其它 CPU 会比较,看自己引用的地址空间是否是正在销毁的地址空间,如果引用了相同地址空间,再判断自己的状态,如果不是懒惰模式,说明它在运行一个用户进程,此时需要刷新 TLB 以同步用户空间的页表,如果是懒惰模式,那么查看自己是否是懒惰模式,如果是懒惰模式,则调用 leave_mm ,代码如下:
void leave_mm(int cpu)
{
    if (per_cpu(cpu_tlbstate, cpu).state == TLBSTATE_OK)
        BUG();
    cpu_clear(cpu, per_cpu(cpu_tlbstate, cpu).active_mm->cpu_vm_mask);
    load_cr3(swapper_pg_dir);
}

        它会把自己从地址空间的掩码中清除,这样就不会再次接收到第二次 IPI 消息。也不用刷新 TLB。但你可能发现,最后调用了 load_cr3,它会引起 tlb 的刷新,其实在 x86 上的这种实现与 intel 的预取队列有关,在这里加载 cr3 虽然感觉没有实现完全的懒惰,但是不会有任何问题的,详细原因这里不谈。
————————————————
版权声明:本文为CSDN博主「Henzox」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Henzox/article/details/41963271

 

posted @ 2023-09-28 10:30  Sky&Zhang  阅读(87)  评论(0编辑  收藏  举报