ARM64高速缓存,内存属性及MAIR配置
高速缓存管理
ARM64 指今集提供了对高速缓存进行管理的指令,包括管理无效高速缓存和清除高速缓存的指令。高速缓存的管理主要有如下 3 种情况。
- 使整个高速缓存或者某个高速缓存行无效。
- 清除(clean)整个高速缓存或者某个高速缓存行。之后,相应的高速缓存行会被标为脏的,数据会写回到下一级高速缓存中或者主存储器中。
- 清零(zero)操作。在某些情况下,对高速缓存进行清零操作以起到一个预取和加速的作用。例如,当程序需要使用较大的临时内存时,如果在初始化阶段对这个内存行清零操作,高速缓存控制器就会主动把这些零数据写入高速缓存行中。若程序主动使用高速缓存的清零操作,那么将大大减少系统内部总线的带宽。
对高速缓存的操作可以指定如下不同的范围。
- 整块高速缓存
- 某个虚拟地址
- 特定的高速缓存行或者组和路
另外,在 ARMv8 架构中最多可以支持 7 级的高速缓存,即 L1~L7 高速缓存。当对一个高速缓存行进行操作时,我们需要知道高速缓存操作的范围。ARMv8 架构中将从以下角度观察内存。
-
全局缓存一致性角度(Point of Coherency,PoC):系统中所有可以发起内存访问的硬件单元(如处理器、DMA 设备、GPU 等)都能保证观察到的某一个地址上的数据是一致的或者是相同的副本。通常 PoC 表示站在系统的角度来看高速缓存的一致性问题。
-
处理器缓存一致性角度(Point of Unification,PoU):表示站在处理器角度来看高速缓存的一致性问题。对于一个内部共享(inner shareable)的 PoU,所有的处理器都能看到相同的内存副本 假设在一个双核处理器系统中,每一个处理器都有独自的 L1 高速缓存,它们共享一个 L2 高速缓存,它们都可以共同访问 DDR4 内存。另外,系统中还有 GPU 等硬件单元。
-
如果以 PoU 看高速缓存,那么这个观察点就是 L2 高速缓存,因为两个处理器都可以在 L2 高速缓存中看到相同的副本。
-
如果以 PoC 看高速缓存,那么这个观察点是 DDR4 内存,因为 CPU 和 GPU 都能共同访问 DDR4 内存。
ARMv8 架构提供 DC 和 IC 两条与高速缓存相关的指令,它们根据不同的辅助操作符可以有不同的含义,如下所示。
//DC 指令的格式 DC <dc_op>, <Xt> //IC 指令的格式 Ic <ic_op> <Xt> |
- DC:
- cisw:清除并使指定的组和路的高速缓存无效
- civac:从 PoC,清除并使指定的虚拟地址对应的高速缓存无效
- csw:清除指定的组或路的高速缓存
- cvac:从 PoC,清除指定的虚拟地址对应的高速缓存
- cvau:从 PoU,清除指定的虚拟地址对应的高速缓存
- isw:使指定的组或路的高速缓存无效
- ivac:从 PoC,使指定的虚拟地址中对应的高速缓存无效
- zva:把虚拟地址中的高速缓存清零
- IC:
- ialluis:从 PoU,使所有的指令高速缓存无效,内部共享属性
- iallu:从 PoU,使所有的指令高速缓存无效
- ivau:从 PoU,使指定虚拟地址对应的指令高速缓存无效
Linux 内核提供了多个与高速缓存管理相关的接口函数,它们定义在 arch/arm64/include/asm/cacheflush.h 头文件中,它们实现在 arch/arm64/mm/cache.S 汇编文件中,如下:
flush_cache_mm(mm) //在修改页表之前清除和无效该进程的进程地址空间中所有的高速缓存页表项 flush_icache_range(start, end)//用于同步由虚拟地址 start 和 end 组成的区域的指令高速缓存与数据高速缓存的-致性 flush_cache_page(vma, addr, pfin)//用于清除由虚拟地址 addr 和页帧号 pfn 对应的高速缓存页表项 flush_cache_range(vma, start, end)//用于清除由虚拟地址 start 和 end 组成的区域中所有的高速缓存 extern void caches_clean_inval_pou(unsigned long start, unsigned long end); extern void icache_inval_pou(unsigned long start, unsigned long end); extern void dcache_clean_inval_poc(unsigned long start, unsigned long end); extern void dcache_inval_poc(unsigned long start, unsigned long end); extern void dcache_clean_poc(unsigned long start, unsigned long end); extern void dcache_clean_pop(unsigned long start, unsigned long end); extern void dcache_clean_pou(unsigned long start, unsigned long end); extern long caches_clean_inval_user_pou(unsigned long start, unsigned long end); extern void sync_icache_aliases(unsigned long start, unsigned long end); |
dma在执行dma_sync_single_for_cpu/device的时候调用:
arch_sync_dma_for_cpu->dcache_inval_poc(start, start + size)。
/** dcache_inval_poc(start, end)** Ensure that any D-cache lines for the interval [start, end)* are invalidated. Any partial lines at the ends of the interval are* also cleaned to PoC to prevent data loss.** - start - kernel start address of region* - end - kernel end address of region*/
SYM_FUNC_START(__pi_dcache_inval_poc)dcache_line_size x2, x3sub x3, x2, #1tst x1, x3 // end cache line aligned?bic x1, x1, x3b.eq 1fdc civac, x1 // clean & invalidate D / U line
1: tst x0, x3 // start cache line aligned?bic x0, x0, x3b.eq 2fdc civac, x0 // clean & invalidate D / U lineb 3f
2: dc ivac, x0 // invalidate D / U line
3: add x0, x0, x2cmp x0, x1b.lo 2bdsb syret
SYM_FUNC_END(__pi_dcache_inval_poc)
SYM_FUNC_ALIAS(dcache_inval_poc, __pi_dcache_inval_poc)
内存属性
普通内存
普通内存是弱一致性的(weakly ordered),没有额外的约束,可以提供最高的内存访问性能。通常代码段、数据段以及其他数据都会放在普通内存中。普通内存可以让处理器做很多的优化,如分支预测、数据预取、高速缓存行预取和填充、乱序加载等硬件优化。sram或者dram那样的内存空间,一般都是过cache的(当然也可不过cache,如外设访问的地址空间,标记为NC)
设备内存
处理器访问设备内存会有很多限制,如不能进行预测访问等,设备寄存器那样的io空间,都不会过cache。Device属性的内存空间还有下面三种子属性,都有打开和关闭的定义。设备内存是严格按照指令顺序来执行的。ARMv8 架构定义了多种设备内存的属性:
- Device-nGnRnE
- Device-nGnRE
- Device-nGRE
- Device-GRE
Device 后的字母是有特殊含义的。
- G 和 nG:分别表示聚合(Gathering)与不聚合(non Gathering)。聚合表示在同一个内存属性的区域中允许把多次访问内存的操作合并成一次总线传输:
- 若一个内存地址标记为“nG”,则会严格按照访问内存的次数和大小来访问内存,不会做合并优化
- 若一个内存地址标记为“G”,则会做总线合并访问,如合并两个相邻的字节访问为一次多字节访问。若程序访问同一个内存地址两次,那么处理器只会访问内存一次,但是在第二次访问内存指令后返回相同的值。若这个内存区域标记为“nG”,那么处理器则会访问内存两次
- R 和 nR:分别表示指令重排(Re-ordering)与不重排(non Re-ordering)。
- E 和 nE:分别表示提前写应答(Early Write Acknowledgement)与不提前写应答 Early Write Acknowledgement)往外部设备写数据时,处理器先把数据写入写终 (write huffer)中,若使能了提前写应答,则数据到达写缓冲区时会发送写应答: 有使能提前写应答,则数据到达外设时才发送写应答。 Linux 内核中定义了如下几个内存属性。
- arch/arm64/include/asm/pgtable-prot.h
#define MT_NORMAL 0 #define MT_NORMAL_TAGGED 1 #define MT_NORMAL_NC 2 #define MT_DEVICE_nGnRnE 3 #define MT_DEVICE_nGnRE 4 |
- MT_DEVICE_nGnRnE:设备内存属性,不支持聚合操作,不支持指令重排,不支提前写应答
- MT_DEVICE_nGnRE:设备内存属性,不支持聚合操作,不支持指令重排,支持提写应答
- MT_DEVICE_GRE:设备内存属性,支持聚合操作,支持指令重排,支持提前写应
- MT_NORMAL_NC:普通内存属性,关闭高速缓存,其中 NC 是 Non-Cacheable 意思
- MT_NORMAL:普通内存属性
- MT_NORMAL_WT:普通内存属性,高速缓存的回写策略为直写(write through)策内存属性并没有存放在页表项中,而是存放在 MAIR_ELn (Memory Attribute IndirectRegister Eln)中。页表项中使用一个 3 位的索引值来查找 MAIR_ELn
MAIR_ELn 分成 8 段,每一段都可以用于描述不同的内存属性。
MAIR寄存器定义如下:
Linux预先定义了6种内存属性,分别存在MAIR寄存器的attr0~attr5。内存页表属性部分可以选择这个寄存器的某个index,范围(0~5)作为自己的属性。
/*
* 上面内容我们说到了, 页表entry表明内存是普通内存, 就是结合这里的初始化来指明的,
* PMD_ATTRINDX(MT_NORMAL)是4, 这其实是一个index, 指向MAIR寄存器的[4*8+7:4*8],
* MAIR寄存器一共有8组, KERNEL用了6组, 每组有8bit, 每个bit都有相应的含义.
* 具体参考手册, 这里就不细说了, 点到为止
*/
arch/arm64/include/asm/pgtable-prot.h
/** Memory region attributes for LPAE:** n = AttrIndx[2:0]* n MAIR* DEVICE_nGnRnE 000 00000000* DEVICE_nGnRE 001 00000100* DEVICE_GRE 010 00001100* NORMAL_NC 011 01000100* NORMAL 100 11111111* NORMAL_WT 101 10111011*/
arch/arm64/mm/proc.S#define MAIR_EL1_SET \(MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRnE, MT_DEVICE_nGnRnE) | \MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRE, MT_DEVICE_nGnRE) | \MAIR_ATTRIDX(MAIR_ATTR_NORMAL_NC, MT_NORMAL_NC) | \MAIR_ATTRIDX(MAIR_ATTR_NORMAL, MT_NORMAL) | \MAIR_ATTRIDX(MAIR_ATTR_NORMAL, MT_NORMAL_TAGGED))SYM_FUNC_START(__cpu_setup)
...mov_q mair, MAIR_EL1_SET
...
SYM_FUNC_END(__cpu_setup)
mov_q mair, MAIR_EL1_SET 写入预定义的5种内存属性值
如上所述,Linux在cpu初始化时建立了6种页表属性索引
ARM64 cpu可以通过页表中设置的页表属性配置,决定其内存或寄存器访问行为(DEVICE_nGnRE/nGnRE/GRE、NORMAL_NC/WT/NORMAL)。
#define ioremap(addr, size) __ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))#define ioremap_nocache(addr, size) __ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))#define ioremap_wc(addr, size) __ioremap((addr), (size), __pgprot(PROT_NORMAL_NC))#define ioremap_wt(addr, size) __ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))#define PROT_DEVICE_nGnRnE (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRIDX(MT_DEVICE_nGnRnE))#define PROT_DEVICE_nGnRE (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRIDX(MT_DEVICE_nGnRE))#define PROT_NORMAL_NC (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRIDX(MT_NORMAL_NC))#define PROT_NORMAL_WT (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRIDX(MT_NORMAL_WT))#define PROT_NORMAL (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRIDX(MT_NORMAL))#define PTE_ATTRINDX(t) (_AT(pteval_t, (t)) << 2)
PTE_ATTRINDX在页表中的位置 bit[4:2]
关于使用页表的场景:
1、内核代码中使用alloc_pages从伙伴系统内存中申请
2、设备驱动代码使用device-tree预留内存地址建立页表映射访问(内存或寄存器,使用ioremap较多)
ioremap() / ioremap_wc()pci_iomap() ==> pci_iomap_range() ==> ioremap()pci_iomap_wc() ==> pci_iomap_wc_range() ==> ioremap_wc()
3、设备驱动一致性内存,dma_alloc_coherent如何使用内存?attrindex使用哪个?
arch/arm64/mm/dma-mapping.c swiotlb_dma_opsdma_alloc_coherentdma_alloc_attrs->dma_direct_alloc(dev, size, dma_handle, flag, attrs); ---------------------------A directelse->ops->alloc(dev, size, dma_handle, flag, attrs); ------------------------B iommu}A:
void *dma_direct_alloc(struct device *dev, size_t size,dma_addr_t *dma_handle, gfp_t gfp, unsigned long attrs)
{
...省略if (!dev_is_dma_coherent(dev)) {remap = IS_ENABLED(CONFIG_DMA_DIRECT_REMAP);}page = __dma_direct_alloc_pages(dev, size, gfp & ~__GFP_ZERO, true);if (remap) {pgprot_t prot = dma_pgprot(dev, PAGE_KERNEL, attrs); ---dma_pgprot最终将PAGE_KERNEL设置了PTE_ATTRINDX(MT_NORMAL_NC)if (force_dma_unencrypted(dev))prot = pgprot_decrypted(prot);/* remove any dirty cache lines on the kernel alias */arch_dma_prep_coherent(page, size);/* create a coherent mapping */ret = dma_common_contiguous_remap(page, size, prot, ---------重新映射页表到vmalloc,并设置页表属性为不可缓存__builtin_return_address(0));if (!ret)goto out_free_pages;}
}B:
iommu_dma_alloc(struct device *dev, size_t size,dma_addr_t *handle, gfp_t gfp, unsigned long attrs)if (gfpflags_allow_blocking(gfp) &&!(attrs & DMA_ATTR_FORCE_CONTIGUOUS)) {return iommu_dma_alloc_remap(dev, size, handle, gfp,dma_pgprot(dev, PAGE_KERNEL, attrs), attrs); -------------优先走这里,设置pgprot_t属性}pages = __iommu_dma_alloc_noncontiguous(dev, size, &sgt, gfp, prot,attrs); -----分配pagevaddr = dma_common_pages_remap(pages, size, prot,__builtin_return_address(0)); ----映射页表,到vmalloc
Linux 内存管理(二)ARM64 的虚拟地址转换在 linux 中的实现 | Matrix