各个进程的虚拟地址范畴都是相同的, 那么不同的进程对应着相同的虚拟地址, 在TLB当中是怎样进行区分的呢?

大家好,我是小林。
今天上午在群里有位读者面试时,被问到这么一个问题:

快表其实是 TLB,是 CPU 封装在芯片里的一个东西:

为什么要有 TLB ?
当下内存分页均为多级页表形式, 如此一来, 虚拟地址至物理地址的转换便增添了几道转换工序, 这无疑致使这两者地址转换的速度有所降低, 也就是说造成了时间方面的开销。
故而, TLB 乃是专门用于存放程序最为常访问的页表项的 Cache, 存在了 TLB 之后, 于是 CPU 在进行寻址之际, 会先去查 TLB, 要是没找到, 才会接着去查常规的页表。
存在这么一种情况, 每个进程的虚拟地址的范围都是相同的, 在此种状况下, 不同的进程对应着相同的虚拟地址, 那么, 在TLB当中究竟是怎样进行区分的呢?
正文
简称为translation lookaside buffer的TLB, 首先, 我们清楚MMU的功能是将虚拟地址转变为物理地址, 虚拟地址与物理地址的映射关联存于页表内, 并且当下页表是分级的, 64位系统通常是3至5级。
平常所见到的配置属于4级页表, 那就拿4级页表当作例子来进行说明, 4级页表指的是PGD、PUD、PMD、PTE这四级页表, 在硬件方面存在一个被称作页表基地址寄存器的东西, 它用于存储PGD页表的起始地址, MMU是依据页表基地址寄存器从PGD页表开始一路查找直至PTE, 最终找寻到物理地址, 因为PTE页表里存储着物理地址。
这如同于地图之上展示你家所处之位置那般, 我为寻觅你家的住址, 先是判定你所属之地为中国, 接着明确你位于某一省份, 进而再往下到某一城市, 最终寻得你家, 此乃相同之原理, 一级一级地探寻下去, 你也看到了这个过程, 极为繁杂琐碎。
要是头一回查到你家确切所在之处, 我要是记住了你姓名以及你家所处地址。那下回查找之际, 是不是只要跟我讲你叫什么名字, 我就能径直跟你说地址, 而用不着逐级去查找。四级页表查找流程需四次内存访问。延迟状况可想而知, 极其影响性能。
显示于下图的, 是页表查找过程的示例。往后等有机会时会详细展开, 在此仅作简单了解便可。

page table walk
TLB的本质是什么

TLB实际上等同于一块高速缓存, 数据cache对地址(虚拟地址或者物理地址)以及数据进行缓存, TLB对虚拟地址及其映射过来的物理地址予以缓存。
TLB依据虚拟地址去查找cache, 它没有别的选择, 仅仅能够依据虚拟地址来查找, 所以TLB是一种虚拟高速缓存。硬件当中存在着TLB之后,虚拟地址到物理地址的转换过程出现了改变。
先是将虚拟地址发送至TLB, 以此来确认是不是命中了cache, 要是cache hit, 那么能够直接获取到物理地址,不然的话, 就要一级一级地去查找页表从而获取物理地址。
TLB中缓存着虚拟地址与物理地址的映射关系。鉴于TLB属于虚拟高速缓存(VIVT), 那么是否会出现别名以及歧义这类问题呢? 又倘若存在此类问题, 软件跟硬件会怎样协同合作来处理这些问题呢?
TLB的特殊
以4KB作为虚拟地址映射物理地址的最小单位, 故而TLB实则无需存储虚拟地址以及物理地址的低12位, 这是由于低12位是相同的, 根本不存在存储的必要。
除此之外, 要是我们命中了cache, 那必然会一次性从cache里取出全部数据。因而虚拟地址并不需要offset域。那么index域需不需要呢? 这得看cache的组织样式而定。
若是全相连高速缓存, 那就无需index, 要是采用多路组相连高速缓存, 依旧得有index, 下图便是一个四路组相连TLB的示例。
现今, 64位CPU的寻址范围没有扩展至64位, 64位地址空间极大, 现今尚未用到那般大, 故而硬件为使设计简便或者解决成本问题, 实际虚拟地址位数仅使用了一部分, 这里以48位地址总线为例予以说明。

TLB的别名问题
先来让我思考第一个问题, 那就是别名是否有所存在。我们清楚, PIPT的数据cache并不存在别名方面的问题。物理地址是处于唯一状态的, 每一个物理地址肯定对应着一个数据。
然而, 存在着这样的情况, 不同, 物理地址, 有可能被存储着同样的数据。这意味着什么, 就是说, 物理地址跟数据, 存在着一种对应关系, 是一对一的那种关系。反过来, 又呈现出多对一的关系。因为, TLB具有特殊性, 它所存储的内容, 是虚拟地址以及物理地址的对应关系。
所以, 就单个进程而言, 在同一时刻, 一个虚拟地址会对应一个物理地址, 而一个物理地址能够被多个虚拟地址进行映射。把PIPT数据cache拿来类比TLB, 我们能够晓得TLB不存在别名方面的问题。
然而, VIVT Cache存在别名方面的问题, 究其缘由, 是因为VA需要进行转换, 转换成为PA, 而数据是存储在PA里面的。且由于在中间多经过一次传递过程, 因而才致使诸多问题被一并引发了出来。
TLB的歧义问题
我们清楚, 不同进程之间所见到的虚拟地址范围是相同的, 故而在多个进程的情形下, 不同进程里相同的虚拟地址能够映射不同的物理地址, 这便会导致歧义问题产生。
例如, 进程A把地址0x2000映射成物理地址0x4000。进程B将地址0x2000映射为物理地址0x5000。在进程A执行之际, 把0x2000对应0x4000的映射关系缓存到TLB里。当切换到B进程时, B进程访问0x2000的数据, 会因命中TLB而从物理地址0x4000获取数据。这便导致了歧义。
怎样把这种歧义给消除掉呢? 我们能够去借鉴VIVT数据cache的处理办法, 在进程发生切换之际把整个TLB给无效化。切换之后的进程全都不会命中TLB, 不过这会致使性能出现损失。

如何尽可能的避免flush TLB
先是要讲清楚的是呐, 这儿的flush理解作使其无效这个意思。我们清楚进程进行切换之际, 为了防止出现歧义, 我们务必主动使整个TLB无效。要是我们能够区分不一样进程的TLB表项那就能够避免让TLB无效呐。
我们清楚Linux怎样去区分各不相同的进程吗? 每一个进程都具备一个绝无仅有的进程ID。倘若TLB在判定是否命中之际, 除了去比较tag之外, 再多增加比较进程ID该有多好呀!如此一来便能够区分不同进程的TLB表项了。进程A以及进程B尽管虚拟地址是一样的, 然而进程ID却是不一样的, 自然而然就不会出现进程B命中进程A的TLB表项的情况。
故而, TLB于其中增添一项ASID(Address Space ID)的匹配, ASID恰似进程ID那般, 用以分辨不同进程的TLB表项, 如此一来在进程切换之际便无需对TLB进行flush操作, 然而依旧需要软件予以管理以及分配ASID。

如何管理ASID
ASID跟进程ID绝对是不一样的, 千万别把二者给混淆了。进程ID的取值范围十分 extensive。然而ASID通常是8或者16 bit。故而仅仅能够区分256个或者65536个进程。
我们是以8位ASID来说明例子的, 因此我们没办法把进程ID和ASID一一对应起来, 我们得给每个进程分配一个ASID, 进程ID跟每个进程的ASID通常是不相等的。每创建一个新进程, 就会给它分配一个新的ASID。当ASID分配完后, 就需要清除全部TLB, 并重新分配ASID。所以, 要是想彻底避免清除TLB, 在理想状况下, 运行的进程数目务必小于或等于256。
实施管理ASID这件事上, 是需要软件与硬件相互结合的, Linux kernel为了对每个进程展开管理, 会存在一个task_struct结构体, 在此处, 我们能够将依照当前进程所分配的ASID予以存储, 页表基地址寄存器存在空闲的位置, 这般情况亦可用于对ASID进行存储。
若进程切换之际, 能够把页表基地址以及可从task_struct获取的ASID一同存放于页表基地址寄存器内。于查找TLB之时, 硬件可两相.compare对比tag以及ASID是否等同, 也就是对比页表基地址寄存器所存的ASID和TLB表面被存储的ASID这个行为。设若两者均等同, 那就意味着TLB hit。不然则为TLB miss。一旦TLB miss, 就得历经多级遍历页表, 寻觅物理地址随后于TLB中进行缓存, 与此同时缓存当下现行的ASID。
更上一层楼
我们清楚, 内核空间跟用户空间是相互分开的, 而且内核空间是为所有进程所共享的。
鉴于内核空间是共享的, 当进程 A 切换至进程 B 时, 若进程 B 所访问的地址处在内核空间, 完全是能够运用进程 A 缓存的 TLB 的。然而当下因为 ASID 不同, 致使 TLB miss。
存在一种映射关系, 我们将其视为针对内核空间这种有着全局共享特性的映射关系, 把它称作global映射, 针对每个进程的映射, 我们则称之谓non - global映射, 所以, 在最后一级页表当中, 我们引入了一个bit(也就是non - global (nG) bit)来表示是不是global映射。
虚拟地址映射到物理地址的关系被缓存至TLB时, nG bit将会被存储下来。判断是否命中TLB之际, 当tag比较相等后, 再去判断是否为global映射, 若为global映射, 直接判定TLB hit, 无需对ASID进行比较。当并非global映射时, 最后通过比较ASID来判断是否TLB hit。

什么时候应该flush TLB
我们再来最后的总结,什么时候应该flush TLB。






