Cache一致性

序言

带着问题去学习,关于cache的一些思考:
1、L1/L2/L3 cache到底在哪里?L1/L2/L3 cache分别都是多大?
2、L1/L2/L3 cache的组织形式都是怎样的?n路组相连?
3、你见过VIVT的cache吗?你为什么要学习VIVT的cache? 非常干扰你对cache的理解,还不如不学呢.
4、那么cache是VIPT还是PIPT? 还是在一个core中既有VIPT,也有PIPT?
5、你要学习MESI的原理吗?你能记得住吗?你是不懂MESI,还是不懂cache架构?
6、MOESI又是啥玩意?现在主流的core是MESI,还是MOESI?
7、MESI仅仅是一个协议,总得有硬件来执行这个协议,硬件是谁?
8、MESI这个协议有4个状态,这4个状态记录在哪里?
9、L1/L2/L3 cache中,或者说core cache/cluster cache中,哪些cache的维护遵守了MESI协议,哪些没有遵守?为什么这样设计?
10、cache line中的data是多少个字节? 在分析问题时,你为什么总是按照条件分析,16bytes的cache line是怎样的,64bytes的cacheline是怎样的?难道你不知道,现在主流的arm core的cache line全部都是64bytes?
11、cache的TAG是什么玩意,里面都有什么?别说cache TAG是物理地址?
12、cache line中又都有什么? 为什么没有index?
13、L2 cache到底是在core中,还是在cluster中?
14、假设一块内存配置成了non-cacheable,为什么就不缓存到cache了?
15、页表entry的属性中定义了cache的缓存策略,那如果disable mmu后,那么cpu读写内存时候的缓存策略是什么?
16、做为一名软件工程师,对于L1/L2/L3 cache的缓存策略,哪些可以修改?哪些是硬件定死的不可以修改?
而这些的替换策略又都是怎样的?
17、什么是inclusive cache? 什么是exclusive cache? Strictly和Weakly呢?
18、一些概念的理解,如CCI、SCU、DSU、ACE、CHI ?
19、如何配置一个页面的cacheable属性? 如何配置页表的cacheable属性?

前言

做为一名底层安全工程师、一名一线支持客户的FAE,工作的内容涉及到TF-A、TEE、TA、Linux Kernel、Linux native程序等众多模块,也会涉及到一些硬件模块driver。在这些不同的硬件或系统软件之中,有着不同的memory属性的配置,不同的缓存策略,那么我们在这多硬件多软件通过share memory通信时,就会遇到各种各样的问题,其实很多时候,也都是客户的灵魂一问,为了给客户一个专业的感觉,身为FAE也不得不去弄懂底层深层次的原理…

本人不是什么专家,更不是什么的大佬,也就是看了一些arm文档,加上自己的理解,然后总结出如下文章,当然我在总结的时候,一切都以官方资料为准,尽量不瞎说不乱说,有些查不到的资料我求证了一些ASIC专家。其实cache同其它模块(如MMU、异常、gic…)相比,cache应该算上最难的,不过好在它的大多数行为都是硬件帮我们做好了,所以我们软件就简单了,但是越是硬件自动的行为,对于我们软件工程师理解起来就会吃力,因为看不到资料看不到设计,很多都得靠猜。

最后,希望这系列文章,能够对大家有所帮助。好好学习、天天向上,卷起来同志们。

说明:

  • 本系列所讲述的,都是以armv8/armv9架构位基准,如有涉及执行状态,则是aarch64. 如有涉及具体core,则是A710和A53
  • 大多数内容来自arm官方文档、少部分咨询了ASIC同事,再加上部分自己的理解…

在介绍 cache 一致性之前,先明确一些概念~

1、之前谈到内存空间的时候,我们知道内存空间包括 内存 和 寄存器空间。

2、CPU  、cache、内存、外设 之间的关系。以及 cache 一致性 是如何导致的

CPU写内存的时候有两种方式:
1. write through: CPU直接写内存,不经过cache。
2. write back: CPU只写到cache中。cache的硬件使用LRU算法将cache里面的内容替换到内存。通常是这种方式。

我们假设MEM里面有一块红色的区域,并且CPU读过它,于是红色区域也进CACHE:

但是,假设现在DMA把外设的一个白色搬移到了内存原本红色的位置:

这个时候,内存虽然白了,CPU读到的却还是红色,因为CACHE命中了,这就出现了cache的不coherent。
当然,如果是CPU写数据到内存,它也只是先写进cache(不一定进了内存),这个时候如果做一个内存到外设的DMA操作,外设可能就得到错误的内存里面的老数据。


所以cache coherent的最简单方法,自然是让CPU访问DMA buffer的时候也不带cache。事实上,缺省情况下,dma_alloc_coherent()申请的内存缺省是进行uncache配置的。
 

但是,由于现代SoC特别强,这样有一些SoC里面可以用硬件做CPU和外设的cache coherence,如图中的cache coherent interconnect:

这些SoC的厂商就可以把内核的通用实现overwrite掉,变成dma_alloc_coherent()申请的内存也是可以带cache的。这部分还是让大牛Arnd Bergmann童鞋来解释:

来自:https://www.spinics.net/lists/arm-kernel/msg322447.html

Arnd Bergmann:

dma_alloc_coherent() is a wrapper around a device-specific allocator,

based on the dma_map_ops implementation. The default allocator

from arm_dma_ops gives you uncached, buffered memory. It is expected

that the driver uses a barrier (which is implied by readl/writel

but not __raw_readl/__raw_writel or readl_relaxed/writel_relaxed)

to ensure the write buffers are flushed.

If the machine sets arm_coherent_dma_ops rather than arm_dma_ops,

the memory will be cacheable, as it's assumed that the hardware

is set up for cache-coherent DMAs.
 

当我grep内核源代码的时候,我发现部分SoC确实是这样实现的:

所以 dma_alloc_coherent()申请的内存 也是可以带cache的,这个要看硬件的,看厂商的,不过一般默认是不带cache的。

所以要解决 cache的不一致性 一般有三种方法。

1、一种是硬件方案,例如上面介绍的在SoC中集成了叫做“Cache Coherent interconnect”的硬件,它可以做到让DMA踏到CPU的cache或者帮忙做cache的刷新。这样的话,dma_alloc_coherent()申请的内存就没必要是非cache的了。

2、一种是软件上禁用 cache (kernel 机制 ------ 一致性映射)

3、一种是 DMA Streaming Mapping (kernel 机制 ------ 流式映射)

下面主要介绍一下后面两种情况~

工程中一般有两种情况,会导致 cache的不一致性,不过kernel都有对应的机制。

(1)操作寄存器地址空间。

寄存器是CPU与外设交流的接口,有些状态寄存器是由外设根据自身状态进行改变,这个操作对CPU是不透明的。有可能这次CPU读入该状态寄存器,下次再读时,该状态寄存器已经变了,但是CPU还是读取的cache中缓存的值。但是寄存器操作在kernel中是必须保证一致的,这是kernel控制外设的基础,IO空间通过ioremap进行映射到内核空间。ioremap在映射寄存器地址时页表是配置为uncached的。数据不走cache,直接由地址空间中读取。保证了数据一致性。

这种情况kernel已经保证了data的一致性,应用场景简单。

(2)DMA申请的内存空间。

DMA操作对于CPU来说也是不透明的,DMA导致内存中数据更新,对于CPU来说是完全不可见的。反之亦然,CPU写入数据到DMA缓冲区,其实是写到了cache,这时启动DMA,操作DDR中的数据并不是CPU真正想要操作的。

这种情况是,CPU 和DMA 都可以异步的对mem 进行操作,导致data不一致性。

对于cpu 和 dma 都能访问的mem ,kernel有专业的管理方式,分为两种:

1. 给DMA申请的内存,禁用 cache ,当然这个是最简单的,不过性能方面是有影响的。

2. 使用过程中,通过flush cache / invalid cache 来保证data 一致性。

通用DMA层主要分为2种类型的DMA映射:

(1)一致性映射,代表函数:

void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle);
一般驱动使用多,申请一片uncache mem ,这样无需考虑data 一致性。代码流程:对page property,也就是是kernel页管理的页面属性设置成uncache,在缺页异常填TLB时,该属性就会写到TLB的存储属性域中。保证了dma_alloc_coherent映射的地址空间是uncached的。

dma_alloc_coherent首先对分配到的缓冲区进行cache刷新,之后将该缓冲区的页表修改为uncached,以此来保证之后DMA与CPU操作该块数据的一致性。

对于通常的硬件平台(不带硬件cache 一致性的组件),dma_alloc_coherent 内存操作,CPU 直接操作内存,没有cache 参与。

但是也有例外,有的CPU 很强,dma_alloc_coherent 也是可以申请到 带 cache的内存的。

2.流式DMA映射(DMA Streaming Mapping),

实际工程中,我们自己写的驱动,当然可以使用 dma_alloc_coherent 申请一致性的内存,但是实际工程中,我们并不能使用dma_alloc_coherent, 我们很难控制内存是自己申请的,举个例子:

dma_addr_t dma_map_single(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction dir)
void dma_unmap_single(struct device *dev, dma_addr_t handle, size_t size, enum dma_data_direction dir)
 
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t handle, size_t size, enum dma_data_direction dir)
void dma_sync_single_for_device(struct device *dev, dma_addr_t handle, size_t size, enum dma_data_direction dir)
 
 
int dma_map_sg(struct device *, struct scatterlist *, int, enum dma_data_direction);
void dma_unmap_sg(struct device *, struct scatterlist *, int, enum dma_data_direction);

相关接口为 dma_map_sg(), dma_unmap_sg(),dma_map_single(),dma_unmap_single()。
一致性缓存的方式是内核专门申请好一块内存给DMA用。而有时驱动并没这样做,而是让DMA引擎直接在上层传下来的内存里做事情。

例如从协议栈里发下来的一个包,想通过网卡发送出去。
但是协议栈并不知道这个包要往哪里走,因此分配内存的时候并没有特殊对待,这个包所在的内存通常都是可以cache的。传过来的socket的 buffer 并不是你申请的,而且不是 dma_alloc_coherent 申请的一致性内存,这个时候你要把 buffer的内容发出去,或者把收到的报文扔到这个 buffer里面去,那这个时候怎么办呢?

这时,内存在给DMA使用之前,就要调用一次dma_map_sg()或dma_map_single(),取决于你的DMA引擎是否支持聚集散列(DMA scatter-gather),支持就用dma_map_sg(),不支持就用dma_map_single()。DMA用完之后要调用对应的unmap接口。

由于协议栈下来的包的数据有可能还在cache里面,调用dma_map_single()后,CPU就会做一次cache的flush,将cache的数据刷到内存,这样DMA去读内存就读到新的数据了。

dma_map_single (做一遍cache的flush ,将cache的内容flush到内存)
 
dma 发报文 (由于做过flush ,发的就是正确的包)
 
dma_unmap_single 
 
 
 
dma_map_single (做一遍cache的invalid,将cache的内容无效,重新读取内存)
 
dma 发报文 (由于做过invalid ,收到的就是正确的包)
 
dma_unmap_single 
 
 
dma_map_single 是有一个方向的参数的,来决定是invalid 还是 flush
注意,在map的时候要指定一个参数,来指明数据的方向是从外设到内存还是从内存到外设:
从内存到外设:CPU会做cache的flush操作,将cache中新的数据刷到内存。
从外设到内存:CPU将cache置无效 invalidate,这样CPU读的时候不命中,就会从内存去读新的数据。

(CPU读取cache 都是自动硬件完成的,软件不能干预,但是可以控制cache,可以invalid 可以 flush)

还要注意,这几个接口都是一次性的,每次操作数据都要调用一次map和unmap。并且在map期间,CPU不能去操作这段内存,因此如果CPU去写,就又不一致了。
同样的,dma_map_sg()和dma_map_single()的后端实现也都是和硬件特性相关。

其他方式
上面说的是常规DMA,有些SoC可以用硬件做CPU和外设的cache coherence,例如在SoC中集成了叫做“Cache Coherent interconnect”的硬件,它可以做到让DMA踏到CPU的cache或者帮忙做cache的刷新。这样的话,dma_alloc_coherent()申请的内存就没必要是非cache的了。
 

番外篇
我们在思维上,一定要想到 dma_alloc_coherent 的接口,只是一个前端,它具体的实现是和硬件有关系的,是和平台有关系的。

(1) 下面的例子是 CPU 内部有 保证 cache 一致性的组件,所以dma_alloc_coherent 也是可以申请到 带 cache的内存的。

(2) DMA scatter-gather (DMA引擎是否支持聚集散列) -- 不需要物理内存连续 ,流式映射。

DMA scatter-gather (DMA引擎是否支持聚集散列)

Scatter:离散(不连续)

Gather:聚合 (连续)

这个时候 DMA的内存就可以是物理不连续的了,可以将 连续的内存中的数据 搬移 到 不连续的内存。 可以将不连续的内存 搬移到 连续的内存。

硬件上可以连续传送多个buffer ,不需要物理内存的连续。

这个时候,如果要进行流式映射,要用到 dma_map_sg() , 会将 多个不连续的 内存,都从 cache 上 flush一下,或者 invalid cache。

(3) 带 MMU 的 DMA引擎 (叫IOMMU 或者 SMMU)

dma_alloc_coherent 只是一个前端,后端直接可以跟 buddy 要内存,也可以跟CMA要内存,也可以通过 IOMMU / SMMU。

IOMMU 或者 SMMU 会 将不连续的物理地址,通过页表映射的方式,统一变成虚拟地址连续的地址,然后给到DMA使用。

这些都是硬件来做的。所以  dma_alloc_coherent 申请到的物理内存是可以不连续的。

具体的dma的使用,可参考一下:

Documentation/Dmaengine.txt
Documentation/DMA-API-HOWTO.txt
Documentation/DMA-API
drivers/dma/dmatest.c

linux下DMA驱动测试代码
————————————————
版权声明:本文为CSDN博主「Adrian503」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Adrian503/article/details/115536886

还可以查看文章:https://www.cnblogs.com/dream397/p/15660063.html

DMA多核一致性

查看文章: 图解 | CPU-Cache | 一致性 - 知乎

文章:cache的基本原理 和 多核cache的一致性 - 简书 

猜你喜欢

转载自blog.csdn.net/u012294613/article/details/132321861
今日推荐