关于ARM/ARM64的写合并(NORMAL_NC) 内存属性、读写和内存屏障的一些问题
我想测试DDR的正确性,也就是向DDR的某个地址写入一个值,确保它确实被写入DDR,然后再从DDR读取该值,并核对是否等于写入的值。
例如,针对下面的C 代码:
unsigned long test(volatile unsigned long *vaddr, unsigned long val)
{
*vaddr = val;
__asm__ volatile ("dsb sy":::"memory");
return *vaddr;
}
假设输入的vaddr使用write combine(NORMAL_NC) 内存属性,且物理地址是DDR RAM,我希望确保val确实被写入DDR,然后再从DDR读取vaddr的内容。
这里有两点关键:
-
确保
val被写入DDR,至少要确保CPU向外部总线发出写请求,而不是仅在CPU的内部缓存中存在。 -
确保
vaddr能从DDR读取,至少要确保CPU向外部总线发出读请求,而不是使用来自CPU缓存或指令预测得到的数据。
我不是架构或硬件方面的专家。查阅了很多资料,但仍不太理解。下面是我整理的一些信息;如有不对,请指正:
(1) write combine 是x86的一个概念;在ARM/ARM64中,NORMAL\_NC 属性提供了类似的功能。在Linux中,pgprot\_writecombile 属性对应于ARM64的 NORMAL\_NC 映射属性。
https://elixir.bootlin.com/linux/v5.10.258/source/arch/arm64/include/asm/pgtable.h#L490
https://elixir.bootlin.com/linux/v5.10.258/source/arch/arm64/include/asm/memory.h#L136
https://elixir.bootlin.com/linux/v5.10.258/source/arch/arm64/mm/proc.S#L56
https://elixir.bootlin.com/linux/v5.10.258/source/arch/arm64/include/asm/sysreg.h#L636
oooo is encoded as follows:
0b0100 Normal memory, Outer Non-cacheable.
iiii is encoded as follows:
0b0100 Normal memory, Inner Non-cacheable.
根据arm64的文档,NORMAL_NC属性不使用L1/L2/L3缓存,这与x86的行为是一致的。
(2) 在x86中,write combine 内存属性确实会使用写缓冲区(Write Buffer),看起来这是它的专用缓冲区。ARM/ARM64的 NORMAL_NC模式是否也有类似的写缓冲区?
(3) 不存在读缓冲区,无论是x86/arm/arm64。
(4) 在x86/arm/arm64中,write combine (NORMAL\_NC) 属性可能实现内存重排序。对于相关内存的混合读写操作,我发现有两种可能的行为:
4.1读取操作将暂停,以确保前一个写缓冲区已经从CPU写出并清空,然后再进行读取操作:
https://fgiesen.wordpress.com/2013/01/29/write-combining-is-not-your-friend/
4.2读取操作可能会读到错误的值,因为前一个写操作可能仍然保存在写缓冲区中尚未写出,而读取操作会直接向CPU外部发送读取请求,不经过任何缓冲区。
不过,上述两种行为都针对x86;我没有找到太多与ARM/ARM64相关的资料。
(5) DMA内存可能通过写合并进行映射,由于DMA在 CPU外部运行;在查阅Linux代码时发现 dma\_mb() 使用 dmb(osh):
https://elixir.bootlin.com/linux/v5.10.258/source/arch/arm64/include/asm/barrier.h#L48
我相信在上面的C 示例中,至少一个osh barrier能确保写请求被发送到CPU的外部总线。
我的猜测:
-
在ARM/ARM64 NORMAL_NC内存属性下,CPU可能会进行推测执行。因此,在没有屏障的情况下,情景 (4).2可能发生,或者读取操作可能根本不会发生(CPU指令执行单元假设写入后紧接着读取的值一定刚刚被写入,因此直接不读取)。
-
在上述C 代码示例中,
dmb osh确保写操作将写请求发送到CPU的外部总线(它可能并未写入DDR,但肯定会到达外部CPU),只有这样读取操作才会进行。读取操作将肯定向CPU的外部总线发送读取请求,并使用总线返回的值。
我的猜测对吗?
解决方案
为了弄清楚这个问题,我查了很多资料
你第一个猜测是正确的,因为ARM64允许更激进的执行和乱序执行,CPU流水线可以将val的值直接路由到返回语句的目标寄存器,而不在DDR总线上执行读写传输。没有屏障,你是在测试CPU寄存器/缓冲区的跟踪,而不是你想要的。你的第二个猜测在某种程度上是正确的,DSB SY将暂停CPU的执行直到写缓冲区被排空并且写请求被确认。但是在写操作之后的屏障并不能让早先的读指令在推测执行时不被执行,或者不经过外设路径读取。
请把我说的都当作参考,我在这方面也并非专家。