操作系统笔记(7)-快速地址转换

简介

  • 使用分页作为核心来实现虚拟内存,可能会带来较高的性能开销:额外的 内存引用

  • 为了改善这个问题,我们需要硬件的帮助,它的名字叫 地址转换旁路缓冲存储器(translation-lookaside buffer),简称 TLB,也可以叫它 地址转换缓存

  • TLB 在处理器核心附近,设计的访问速度很快

TLB 的工作原理

  • 对每次内存访问,硬件先检查 TLB ,看看其中是否有期望的转换映射,如果有,就完成转换(很快),不用访问页表。因此 TLB 带来了巨大的性能提升

  • 算法流程如下:

    1. 从虚拟地址中 提取页号(VPN)

    2. 检查 TLB 中是否有该 VPN 对应的物理帧号 PFN

    3. 如果有,TLB 命中,转换成功

    4. 如果没有,TLB 未命中,硬件就访问 页表 来寻找地址映射

    5. 用找到的映射来更新 TLB

谁来处理 TLB 未命中

  • 早期的硬件有复杂指令集,可以全权处理 TLB 未命中,然后去遍历 页表 找到对应的地址映射(如 x86 架构)

  • 更现代的体系结构都是精简指令集计算机,通过软件管理 TLB,发生 TLB 未命中时,硬件系统会抛出一个异常,这会暂停当前的指令流,将特权级提升至内核模式,跳转至 陷阱处理程序,然后这个程序用于处理 TLB 未命中

TLB 的内容

  • 如果想要快速地缓存,TLB 的大小就不会很大,典型的 TLB32 项、64 项或者 128 项,如果每一项的大小是 4 B,那 TLB 的总大小还是很小的

  • TLB 项的内容应该包含虚拟页号 VPN 、物理帧号 PFN 以及一些其它的标志位,一般 TLB 会有一个 有效位 用于标志该项是不是有效的地址转换,还有 保护位 表示该页是否有访问权限,还有一些标志位如 脏位

上下文切换时对 TLB 的处理

  • TLB 中包含的虚拟到物理地址的映射只对当前进程有效,对其它进程是没有意义的,所以在不同的进程切换时需要对 TLB 进行处理,避免 误读 现象

  • 假如两个进程的虚拟地址相同,那么就容易产生 误读,因此 TLB 引入了一个 地址空间标识符(ASID) 用于标识不同的进程,与 PID 相似,但是通常比 PID 位数少

  • 通过使用 ASID ,不同的进程就可以共享 TLB

注意 : 我们讨论的 TLB 是指的单核机器,在多核机器中,每个 CPU 都有一个 TLB,类比即可

TLB 替换策略

  • 前面说到,TLB 是很小的,那么当 TLB 满了之后,下一次更新之前就需要替换掉已有的一项才行

  • 一般有两种方式采用,一个是替换 最近最少使用(LRU) 的项,另一个是 随机策略,相比来说 随机策略 比较好

练习:测量 TLB 未作用时每页的平均访问时间

  • 如何测量:使用 gettimeofday() 进行时间差计算

  • 定义一个很大的数组(跨越多个页存储),然后每次访问不同的页(保证 TLB 命中率为0),计算平均访问时间

  • 查看页表大小,在 shell 使用 getconf PAGE_SIZE 查看,我的是 4KB

  • 测试代码

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/time.h>
    #include <stdlib.h>
    
    // 结果查看, 页表的大小为 4KB
    #define PAGESIZE 4096
    
    int a[1024 * 1024 * 256];
    
    int main(int argc, char* argv[]) {
        if (argc < 3) {
            printf("请给出页的数目和尝试的次数\n");
            return 0;
        }
    
        int pagesNum = atoi(argv[1]);
    
        if (pagesNum > 1024 * 256) {
            printf("页的数目不能超过%d", 1024 * 256);
            return 0;
        }
    
        int cnt = atoi(argv[2]);
    
        int jump = PAGESIZE / sizeof(int);  // 跳到下一页需要的步数
        
    
        struct timeval start, end;
    
        gettimeofday(&start, NULL);
    
        for (int i = 0; i < cnt; i++) {
            for (int j = 0; j < pagesNum * jump; j += jump) {
                a[j] += 1;
            }
        }
    
        gettimeofday(&end, NULL);
    
        long int diff = 1000000L * (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec);
    
        printf("total time is : %ldus\n", diff);
        printf("avg time : %lfus\n", (float)diff / (pagesNum * cnt));
    
        return 0;
    }
  • 测试结果 :这里迭代次数都是 1 ,后面测试 TLB 开销可以通过增加迭代次数估算

    测试结果

  • 结果分析 :随着访问页数的增加,平均的页访问时间 逐渐增加 ,都在几毫秒的样子

  • 如果要测试 TLB 命中后的平均开销,只需要固定访问页数,然后提高迭代次数即可,当迭代次数足够高时,就能够近似得到 TLB 命中后的平均访问速度了

    TLB

  • 结果分析 :有了 TLB 的作用,平均访问时间提升到了 纳秒级