ARM64 cache

ARM64 cache


arm64 soc的cache操作指令

ARM64 提供了一组精细的指令来管理 Cache 和 TLB (Translation Lookaside Buffer)。这些指令通常在内核态(EL1/EL2/EL3)使用。

核心概念

操作对象:

数据 Cache (DC):缓存数据。

指令 Cache (IC):缓存指令。

TLB:缓存虚拟地址到物理地址的转换结果。

操作类型:

Clean (又称 Write-Back):将 Cache 中已修改(脏)的数据写回到主内存。操作后,Cache 和内存数据一致。

Invalidate:丢弃 Cache 中的数据。下次访问该数据时将需要从主内存重新加载。

Clean & Invalidate:先将数据写回内存,然后使该行失效。这是一个原子操作。

操作范围:

按虚拟地址 (VA):操作指定地址对应的 Cache 行。这是最常用的方式。

按 Set/Way:直接操作 Cache 的特定组和路。这通常只在初始化、禁用 Cache 或进行整个 Cache 的维护时(如休眠前)使用,软件需要非常了解底层 Cache 结构。

常用 Cache 操作指令 (DC & IC)

这些指令主要在 DC (Data Cache) 和 IC (Instruction Cache) 功能域下。

  1. 数据 Cache (DC) 操作
    语法通常为 DC , ,其中 是一个64位通用寄存器,存放要操作的虚拟地址。

| 指令 | 功能描述 | 典型应用场景 |
| :--- | :--- | :--- |
| DC CIVAC | Clean and Invalidate by VA to Point of Coherency | 最常用、最安全的数据清理指令。将指定地址的数据清理并使失效,一直影响到所有核心都能看到的一致性点(PoC)。适用于DMA传输前:确保设备读到的内存数据是CPU最新写的。 |
| DC CVAC | Clean by VA to PoC | 仅清理数据到内存,但不使失效。适用于DMA传输前(如果之后CPU还要读取该数据,则保留在Cache中效率更高)。 |
| DC IVAC | Invalidate by VA to PoC | 仅使指定地址的Cache行失效。适用于DMA传输后:设备已经写入了新数据到内存,CPU需要失效旧数据以保证读到最新数据。 |
| DC CVAP | Clean by VA to Point of Persistence | 清理数据到持久化点(PoP)。这与非易失性内存(NVM)或系统级持久化相关,用于确保数据在掉电后不丢失。 |
| DC CVAU | Clean by VA to Point of Unification | 清理数据到统一点(PoU)。PoU是保证指令和数据缓存一致性的点,通常用于自我修改代码场景:清理数据Cache后,需要使指令Cache失效,以便CPU能获取到新指令。 |
| DC ZVA | Zero by VA | 将一整块内存清零的高速指令。它会将指定地址对应的整个Cache行用零填充,而无需从内存读取。这比普通的STORE指令清零要快得多。 |

  1. 指令 Cache (IC) 操作
    语法通常为 IC ,

| 指令 | 功能描述 | 典型应用场景 |
| :--- | :--- | :--- |
| IC IALLU | Invalidate all I-cache to PoU | 使整个指令Cache失效。通常在启动新程序或进行大规模代码更新后使用。 |
| IC IVAU | Invalidate I-cache by VA to PoU | 按虚拟地址使指令Cache失效。这是自我修改代码流程的关键一步:

  1. 将新指令写入内存(通过数据Cache)。

  2. 使用 DC CVAU 清理数据Cache,确保指令数据已写回内存。

  3. 使用 IC IVAU 使对应地址的指令Cache失效。

  4. 执行一个上下文同步指令(ISB)以确保所有后续指令都能看到更新。 |

  5. TLB 操作
    语法通常为 TLBI ,

| 指令 | 功能描述 |
| :--- | :--- |
| TLBI ALLE1 | 使 EL1 下的所有 TLB 条目失效(例如,在 ASID 切换时)。 |
| TLBI VAAE1 | 使 EL1 下指定VA的所有ASID的TLB条目失效(例如,在修改页表后)。 |
| TLBI VALE1 | 使 EL1 下指定VA的当前ASID的TLB条目失效(更精确的失效)。 |

使用示例(C代码内联汇编)

示例 1:DMA 传输前清理数据缓存
假设一个设备要通过DMA读取一块由CPU准备的数据,你需要确保数据已经写回内存。

void dma_buffer_clean(const void *addr, size_t size) {
    const char *p = addr;
    uintptr_t start = (uintptr_t)p;
    uintptr_t end = start + size;
    /* 将地址对齐到Cache行大小(通常为64字节) */
    start = start & ~(CACHE_LINE_SIZE - 1);

    for (uintptr_t ptr = start; ptr < end; ptr += CACHE_LINE_SIZE) {
        asm volatile("dc civac, %0" : : "r" (ptr) : "memory");
    }
    asm volatile("dsb sy" : : : "memory"); // 等待所有清理操作完成
    asm volatile("isb" : : : "memory");    // 同步上下文
}

示例 2:DMA 传输后使数据缓存失效
设备已经将数据通过DMA写入内存,CPU需要读取这些新数据。

void dma_buffer_invalidate(void *addr, size_t size) {
    char *p = addr;
    uintptr_t start = (uintptr_t)p;
    uintptr_t end = start + size;
    start = start & ~(CACHE_LINE_SIZE - 1);

    for (uintptr_t ptr = start; ptr < end; ptr += CACHE_LINE_SIZE) {
        asm volatile("dc ivac, %0" : : "r" (ptr) : "memory");
    }
    asm volatile("dsb sy" : : : "memory"); // 等待所有失效操作完成
    asm volatile("isb" : : : "memory");    // 同步上下文
}

示例 3:自我修改代码
动态生成或修改代码后,需要让CPU识别新指令。

void flush_instruction_cache(const void *code_addr, size_t size) {
    const char *p = code_addr;
    uintptr_t start = (uintptr_t)p;
    uintptr_t end = start + size;
    start = start & ~(CACHE_LINE_SIZE - 1);

    // 1. 将数据Cache中的数据清理到PoU (DC CVAU)
    // 2. 使对应地址的指令Cache失效 (IC IVAU)
    for (uintptr_t ptr = start; ptr < end; ptr += CACHE_LINE_SIZE) {
        asm volatile("dc cvau, %0" : : "r" (ptr) : "memory");
    }
    asm volatile("dsb ish" : : : "memory"); // 等待DC操作完成

    for (uintptr_t ptr = start; ptr < end; ptr += CACHE_LINE_SIZE) {
        asm volatile("ic ivau, %0" : : "r" (ptr) : "memory");
    }
    asm volatile("dsb ish" : : : "memory"); // 等待IC操作完成
    asm volatile("isb" : : : "memory");     // 同步流水线,确保后续执行新指令
}

重要注意事项

内存屏障 (Barriers):

DSB (Data Synchronization Barrier):确保在它之前的所有内存访问指令都完成。

ISB (Instruction Synchronization Barrier):清空处理器流水线,确保在此之后执行的指令都是重新从指令Cache中获取的。在修改系统控制寄存器(如SCTLR_EL1, 用于启用/禁用MMU/Cache)或自我修改代码后必须使用。

在Cache操作后几乎总是需要DSB来确保操作完成,然后再继续执行。

对齐:Cache操作是按行进行的。传入的地址最好对齐到Cache行大小(CACHE_LINE_SIZE,通常为32或64字节),否则你可能需要操作多个行。

权限:这些指令是特权指令,只能在EL1或更高异常级别执行。在Linux内核中,你需要编写内核模块或使用已有的API(如flush_cache_range())。

Linux内核接口:在实际开发中,应优先使用内核提供的便携接口,而不是直接写汇编。例如:

dma_map_single() / dma_unmap_single()

flush_icache_range()

clflush_cache_range()

总结

ARM64 的 Cache 操作指令设计得非常精细,允许软件根据不同的场景(PoC, PoU, VA, Set/Way)进行高效的管理。理解并正确使用 DC CIVAC, DC IVAC, IC IVAU 以及配套的 DSB/ISB 屏障,是确保多核系统、DMA设备与CPU之间缓存一致性的关键。

申请dma内存,这块内存是uncache的,配置内存的属性为unchacheable的是如何实现的?

关键步骤:在设置页表项时,使用 pgprot_dmacoherent() 或类似的函数来设置 PTE 的属性位,将其标记为 MT_DEVICE_nGnRE(一种 Device 内存类型)。
Device 类型的内存属性本身就是 Non-Cacheable 的。

Linux 内核为不同的用途预定义了多种内存属性组合(定义在 arch/arm64/include/asm/pgtable-prot.h):

#define PROT_DEFAULT		(PTE_TYPE_PAGE | PTE_AF | PTE_SHARED)
#define PROT_NORMAL		(PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_WRITE | PTE_ATTRINDX(MT_NORMAL))
#define PROT_DEVICE_nGnRnE	(PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_WRITE | PTE_ATTRINDX(MT_DEVICE_nGnRnE))
#define PROT_DEVICE_nGnRE	(PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_WRITE | PTE_ATTRINDX(MT_DEVICE_nGnRE))
#define PROT_NORMAL_NC		(PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_WRITE | PTE_ATTRINDX(MT_NORMAL_NC))

PTE_ATTRINDX(MT_DEVICE_nGnRE):这就是 dma_alloc_coherent 通常使用的属性,它将内存标记为 Device 类型,从而是 Non-Cacheable 的。

PTE_ATTRINDX(MT_NORMAL_NC):这是一种 Non-Cacheable 的 Normal 内存。有时也会用于某些特定的 DMA 场景。

当 MMU 进行地址翻译时,它看到 PTE 中的 MT_DEVICE_* 属性,就不会让 Load/Store 指令去访问 Cache,而是直接发起对总线的访问,从而实现了 Uncacheable 的行为。



reference:
https://mp.weixin.qq.com/s/DWlHgoqESUgY5TjA49D0Vw



posted @ 2022-02-11 20:41  王阳开  阅读(837)  评论(0)    收藏  举报