QQ群中关于TCR_EL1寄存器、shareable 概念以及内核地址空间,用户地址空间的讨论
本文是原「蜗窝讨论区」的历史存档(2016-05-31),来自版块「Linux kernel技术问答」,共 5 帖。讨论区已停止服务,此处仅供查阅。
说明:这些文字来自蜗窝QQ群的讨论,因为觉得比较有价值,因此保留下来,方便后续查阅
Question from harvey: 请教蜗窝,发现一个问题。关于cache的分配方式(如写分配、读分配等)在TCR_EL1寄存器中有设置 cache分配方式的语句,在MMU表中的memory attribute 中依然可以设置cache分配方式?这两处设置有什么要求吗?需要一致?不一致会如何?非常感谢!
Answer from Shaman庆: tcr是设置页表本身需不需要cache
Answer from linuxer: 当PE访问一个虚拟地址的时候,需要遍历各级translation table来进行地址翻译过程,因此,在Translation table中就会有关于该虚拟地址的各种属性(memory type 和 attribute)。 在进行上面过程的时候,需要访问保存在memory中的各级translation table,PE在访问各级translation table使用的是物理地址,没有地址翻译过程(否则会出现这样的问题:是现有鸡还是现有蛋呢?),也就无法定义其cacheability 和 shareability,因此,ARMv8在寄存器TCR_EL1中定义了访问Translation table的memory attribute。
Question from harvey: 再向各位大神请教一下。 1,shareable 是什么概念?起到什么作用? 2,用户态使用TTBR0_EL1,内核态使用TTBR1_EL1?这个是需要寄存器指定吗?还是说TTBR0_EL1 对应的表中地址范围为低端,TTBR1_EL1对应的地址范围为高端。用户态在使用低端虚拟地址时自动落到TTBR0_EL1 对应区域。这样理解释放可以? 谢谢先
Answer from Shaman庆: 对于第一个问题:例如一个4 cluster,每个cluster 4个core的系统,如果配置inner域为cluster内部,那么innershare内存只能在本cluster的4个core保持一致性,outeshare的能在16个core保持一致性,当然nonshare就不和任何core保持一致了
Answer from linuxer: 你问的第一个问题我是这样理解的: 本身shareability的概念是和多核环境相关的,当系统中有多个PE,有多个level的cache的时候,事情变得复杂了。对于一个memory location,需要考虑下面的问题: 1、该memory location可以被多少个PE(或者说observer)访问? 2、谁来维护多个PE之间的cache coherence?
如果一个memory location的shareable attribute是Non-shareable,那么则说明该memory不会在多个cpu core之间共享,只是对一个core可见 如果一个memory location可以被多个observer(PE,GPU,DMA controller等)共享,那么我们可以设定该memory的shareable attribute是inner shareable,或者设定为outer shareable。outer shareable domain一般会包括多个inner shareable domain,具体如何划分inner和outer区域是和具体的实现相关。 对于Non-shareable,不需要维护cache coherence,对于inner shareable或者outer shareable attribute而言,硬件会维护cache coherence(data cache或者unified cache)。 为什么要区分inner shareable和outer shareable呢?我都设定为outer shareable多好,反正硬件会帮助维护一致性,不是吗?实际上,设计总是在多个因素之间平衡,虽然用硬件维持cache coherency对于软件来说比较方便,但是增加了硬件设计就是增加了SOC上的晶体管,增加了数据访问的delay,增加了功耗。 因此,把一个大的,包括多个observer的outer shareable domain分成几个inner shareable domain可以降低上面说的那些开销。
Answer from linuxer: 你问的第二个问题我是这样理解的:首先,选择那一个Translation table应该是和虚拟地址相关,对于ARMv8,整个虚拟地址空间被分成了两个subrange: ------------------- Lower VA subrange : 从0x0000_0000_0000_0000 到 (2^(64-T0SZ) - 1) Upper VA subrange : 从(2^64 - 2^(64-T1SZ)) 到 0xFFFF_FFFF_FFFF_FFFF ---------------------- 如果VA落入Lower VA subrange,那么使用TTBR0_EL1,如果落入Upper VA subrange,那么将使用TTBR1_EL1。具体如何定义VA subrange的size可以参考TCR_EL1寄存器的描述。 具体用户空间和内核空间都是linux kernel定义的,硬件并不关心(硬件只关心Lower VA subrange还是Upper VA subrange),正因为用户空间和内核空间都是linux kernel定义的,因此kernel中要为TTBR0_EL1和TTBR1_EL1设定正确的页表基地址,具体可以参考内核代码。
Question from harvey: 我有一个疑问,在ARM 32bit系统中,内核和用户态共用一个转换表。我一直认为,所有进程的内核态空间应该都是一样的(还包括内核线程空间),当然,用户态空间是私有的。 那么问题来了,如果一个进程调用一个驱动模块,在内核空间申请了一段内存(未释放,以后还会使用)。那么这段内存空间的映射是否需要更新到所有进程的内核空间中?如果更新的话,系统开销岂不很大?
Answer from linuxer: 对于ARM32系统,硬件只支持一个Translation table,这个Translation table既包括了userspace,也包括了kernel space的页表。当然,说页表有些笼统,我们使用下面的术语好了: 1、PGD (对于ARM而言,应该是level0的translation table) 2、PMD (对于ARM而言,应该是level1的translation table) 3、PTE (对于ARM而言,应该是level2的translation table) 当我们说一个进程的地址空间的时候,往往指的是一个PGD的地址,而该地址就保存在某个硬件寄存器中。例如对于X86就是CR3寄存器,对于ARM而言,就是TTBR寄存器。PGD有若干的entry,每个entry又指向了下一阶的translation table,也就是PMD(如果是4级映射,那么还有PUD,这里我们就不考虑那么复杂的场景了)。同样的,每个PMD也是由若干的entry组成,每个entry又指向了PTE。 在完成上面的基本硬件描述之后,我们回头看看软件抽象,一个进程的地址空间是由mm struct中的pgd成员描述。以pgd为起始点,辅以pmd和pte,这些数据结构包括了一个进程需要地址翻译的全部信息(内核空间加上用户空间)。当进程切换的时候,所谓切换mm也就是切换pgd而已。 虽然以pgd为起点,可以构建整个地址空间,但是在fork进程的时候,是否需要一并将kernel space和userspace的页表项(包括PGD entry,PMD entry,PTE entry)全部建立起来呢?不需要的,如果有CLONE_VM的flag,那么其实两个进程指向同一个mm struct即可,也就是说,所有的页表都是共享的。如果没有CLONE_VM,那么需要调用dup_mmap来copy该进程的页表信息,但是,dup_mmap并不会copy kernel space的页表。换句话说,一个新创建的进程,其页表(地址翻译表)中没有任何关于内核空间的条目,一旦该进程运行起来,并通过系统调用进入内核的时候,一定会发生page fault,因为该进程要访问的内核空间的地址在pgd中对应的entry是空的。产生了异常之后,内存管理模块会帮你处理一切的:根据init_mm来填写pgd entry的内容(具体参考do_translation_fault函数)。
OK,在完成上面的基础知识后,我们来看看你的问题: Q、是否所有进程的内核空间都是一样的? A、Yes
Q、是否一个进程的内核空间以及用户空间共享一个地址翻译表 A、当然,都是以该mm struct的pgd成员为起点的各个level的地址翻译表,当然,在pgd或者说level 0 Translation table这个层面上,是共享的,不过对于PMD和PTE,内核空间和用户空间是不同的
Q、是否所有进程(包括内核线程)的内核空间是一样的? A、的确是一样的。但是,由于各个用户进程都有自己的pgd,因此在pgd或者说level 0 Translation table这个层面上,各个进程不可能共享。我们知道,各个进程在访问内核空间地址的时候,其翻译过程包括PGD PMD PTE,各个进程通过各自的PGD中的entry指向了相同的PMD以及PTE,或者说PMD和PTE是共享的
Q、如果一个进程调用一个驱动模块,在内核空间申请了一段内存(未释放,以后还会使用)。那么这段内存空间的映射是否需要更新到所有进程的内核空间中?如果更新的话,系统开销岂不很大? A、当然不需要了,其实,一个进程陷入内核,并在内核空间进行了地址映射,这时候创建的相关的页表并不会进入该进程的页表(mm_struct->pgd),也就是说该地址对应的pgd entry是null。取而代之的是在init_mm的pgd中建立相关entry(当然包括该entry对应的pmd和pte)。这时候,你心里肯定想:这是什么鬼?不在自己的translation tables中建立地址映射关系,反而去init_mm去建立,有没有搞错。但是,实际上这是内核巧妙的设计之处。一旦在init_mm(就是表示内核地址空间)中建立了地址映射关系,实际上就是为所有的进程(包括内核线程)建立的地址映射关系,因为当各个进程在访问该地址的时候,由于对应的PGD entry是null,因此产生异常,然后用init_mm pgd 对应的entry copy到该进程的pgd就OK了,仅仅是copy一个entry而已,因此不存在开销很大的问题。
BTW,多谢你的问题,让我又重温一下内存管理的内容,多谢!上面是我的理解,不一定对,大家可以探讨。
harvey群友追问了一个问题: 对于TCR寄存器中有如下的描述: ---------------------------- ORGN0, bits [11:10] Outer cacheability attribute for memory associated with translation table walks using TTBR0_EL1. 00 Normal memory, Outer Non-cacheable 01 Normal memory, Outer Write-Back Write-Allocate Cacheable 10 Normal memory, Outer Write-Through Cacheable 11 Normal memory, Outer Write-Back no Write-Allocate Cacheable
IRGN0, bits [9:8] Inner cacheability attribute for memory associated with translation table walks using TTBR0_EL1. 00 Normal memory, Inner Non-cacheable 01 Normal memory, Inner Write-Back Write-Allocate Cacheable 10 Normal memory, Inner Write-Through Cacheable 11 Normal memory, Inner Write-Back no Write-Allocate Cacheable --------------------------------------- 假设上面的设置是配置各级translation 表是否被cache到各级cache的话。TTBR0和TTBR1、pgd、pud、pmd、pte等表中的地址都是物理地址(读取这些表本身的时候都是使用的物理地址)。对memory空间的一般数据进行访问时,cache命中过程使用的应该是虚拟地址吧? translation 的访问使用的是物理地址,一般memory访问使用的是虚拟地址,如果他们存在同一cache中是否会冲突?
另外,tlb的作用不就是cache translation表的吗?为何还需要将translation表 load到普通的cache中?
对于“cache命中过程使用的应该是虚拟地址吧?”这个问题,整理大家的回答如下:
非也,确定cache hit or miss包括两个步骤: 1、通过地址中的index来找到cache set 2、当找到了cache set后,通过地址中的Tag来判断是否cache hit
上面的描述中,我没有说是什么样的地址,因此根据地址的不同可以分成下面几个类别: (1)VIVT(Virtual index Virtual tag)。寻找cache set的index和匹配cache line的tag都是使用虚拟地址。 (2)PIPT(Physical index Physical tag)。寻找cache set的index和匹配cache line的tag都是使用物理地址。 (3)VIPT(Virtual index Physical tag)。寻找cache set的index使用虚拟地址,而匹配cache line的tag使用的是物理地址。
不同的处理器,不同level的cache可能会采取不同的策略。例如S900 A53处理器,L1包括三种cache,instruction cache, data cache和Translation cache(就是TLB了),l2 cache则不区分,是unified cache。 我们再看看具体的各个cache的策略:L1 instruction cache采用的是VIPT,而L1 data cache是PIPT,L2 unified cache以及后面level的cache都是PIPT的了。 最后,我们通过cpu取指动作把整个过程穿起来: 1、首先是CPU执行取指动作,这时候,CPU发出虚拟地址试图获取要执行的指令 2、该虚拟地址分别送往L1 instruction cache和TLB 3、通过虚拟地址的index可以找到L1 instruction cache中的cache set(L1 instruction cache是VIPT的) 4、在step 3的过程中,TLB完成了翻译,得到了物理地址,输入到L1 instruction cache进行cache hit的判断
对于CPU进行数据访问,概念类似,这里就不赘述了。 所有大于L1的cache,其实都统一到了物理地址上了,因此不存在冲突问题。
针对第二个问题,整理大家的回答如下: --------------------- 我们用特定的A53处理器来回答这个问题。对于A53处理器,在L1 level,instruction、data和页表都有各自独立的cache,但是,之后,会在L2 level上汇合,也就是说,不论是取指、数据访问,还是页表访问,都是用一个cache(L2),如果不这样,硬件需要直接去main memory中加载页表到TLB,那么这样开销太大了。
另外,有一个术语叫做PoU,Ponit of Unification,就是描述从某个PE的角度来看,cache汇合的那一个点
非常有价值的讨论
