我们都知道,在切换页表时会刷新 TLB,这样就可以使用新的地址空间,那什么是 TLB 刷新的懒惰模式呢?
TLB 是什么这里不作多的解释,可以简单理解为,为了加快 MMU 对虚拟地址的转换而增加的缓存,它记录了一个虚拟地址对应的内存页的物理地址。其实就是根据虚拟地址的前 20 位,来建立一个个条目,对应记录通过查找页表来记录的内存页的物理地址。
既然有缓存,那么被缓存的内容改变时,就涉及到缓存的刷新,就是 TLB 的刷新问题,当一个页表结构发生变化时,使用该页表节构的 CPU 就应该刷新自己的 TLB。
显然进程切换时,由于地址空间发生了变化,TLB 应该得到刷新,然而,内核进程只访问内核空间的地址范围,而每个进程的内核空间的地址范围相同,所以如果 CPU 从一个用户进程切换到一个内核进程,由于用户进程和内核进程的内核地址空间部分相同,其实是不用切换页表的,内核进程依然可以使用前一个用户进程的内核部分的地址空间。这样就省去了刷新 TLB 带来的性能损耗。
想像着是挺完美的,但是在 SMP 构架下,这将带来一些问题,例如,在某一核上 CPU0 刚从一用户进程切换到内核进程,该内核进程沿用该用户进程的地址空间,但它只访问内核空间部分,这不会有问题,然而,如果该用户进程在另一个 CPU1 核上被调度,并且在 CPU0 用它的地址空间时,它在 CPU1 上执行完毕,并退出,那么它的地址空间将被销毁,此时,若 CPU0 如果访问它的地址空间是非常危险的,不管是被缓存的地址还是未被缓存的地址都将可能带来意想不到的严重后果。
那么,难道这种美好的事情就要被上面的情况的发生扼杀,而每次都要刷新 TLB,重新加载页表么。显然还是有补救办法的,如果在 CPU0 上的内核进程执行期间,它所引用的用户进程的地址空间没有被调度并执行完毕的情况还是非常多的,这种不刷新 TLB 带来的性能提升还是可以利用一下的,谁让 Linux 是一个精打细算的内核呢。
如何办到这一点,其实很简单,就是当地址空间销毁时,通知一下,当时正在使用这个地址空间的内核进程。所以只需要记录当前地址空间有哪些 CPU 在引用就行了,当地址空间销毁时,发一个 IPI 给其它引用该地址空间的 CPU,让它们重新加载自己的页表结构,就可以了。
这就引入了 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 中相应的位置位,此时并不切换页表节构,即不加载内核空间的页目录,而如果它引用的地址空间的用户进程退出,地址空间被销毁时,销毁的逻辑中会根据 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++; }
它会比较,看自己引用的地址空间是否是正在销毁的地址空间,如果是,那么查看自己是否是懒惰模式,如果是懒惰模式,则调用 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); }
它会把自己从地址空间的掩码中清除,然后加载 swapper_pg_dir 页目录,该页目录就是内核空间的页目录,它会引起 TLB 的刷新,它本该在 CPU 调度入该内核进程时被加载的,而此时才加载,所以就被称为懒惰刷新 TLB。