kernel如何保证cache数据一致性

本文链接:https://blog.csdn.net/skyflying2012/article/details/48023447

        在嵌入式系统中,cache位于CPU与DDR之间,是一段SRAM,读写性能远高于DDR,利用cache line提供了预取功能,平衡CPU与DDR之间的性能差异,提高系统的性能。

       据我了解,ARM/PPC/MIPS三款主流嵌入式处理器都是软件管理cache,即有专门的指令来进行cache操作,如PPC的iccci icbi,ARM的CP15协处理器也提供对cache的操作。

       cache的操作有2种:写回和无效。写回操作是将cache中数据写回到DDR中,无效操作是无效掉cache中原有数据,下次读取cache中数据时,需要从DDR中重新读取。这两种操作其实都是为了保证cache数据一致性。在kernel平台汇编代码中也封装了cache操作函数,这里以ARM v7处理器3.4.55内核为例,在arch/arm/mm/cache-v7.S中就封装了cache的操作函数,其中v7_dma_flush_range刷新函数是完成了写回和无效2种操作。其他平台(ARM MIPS)中cache操作函数也类似。

        对cache敏感是因为在cache问题上被坑过2次。。。,其中一次在移植uboot时,没有注意cache,网卡dma始终不能工作,表面上dma描述符该配置的我都已经配置了,但是dma就是不能运行,在将uboot整个启动代码都过了一遍后我才考虑到是cache的问题,我写入的描述符数据并没有完全写入DDR中,而是cache住了,uboot对系统运行性能要求不高,索性将cache关掉,发现dma果然正常了。这个问题前后折腾了我半个多月,cache的问题实在不好查,读取写入DDR中的数据是完全正确的(因为读到的其实是cache中的数据),只能靠猜测推断这个问题原因。导致我现在一看到cache操作就紧张。

        不扯题外话了,正因为嵌入式处理器软件管理cache,就需要我们代码主动去操作cache,但在内核开发中却很少会直接进行cache操作,那么kernel是在什么时候进行的cache操作。首先想明白一点,为什么要进行cache操作,只能说cache是天使也是魔鬼。cache在提高了系统性能同时却导致了数据的不一致性。嵌入式处理器软件管理cache的初衷就是保证数据一致性。那什么地方需要保证数据一致性呢?

        对于由CPU完全操作的数据,数据是完全一致的。

        也就是该数据完全由CPU写读操作,没有对CPU不透明的操作。

        这种情况下CPU读写的数据都是来自于cache,我们代码(代码由处理器执行,我们就应该站在处理器视角来看这个问题)完全不用考虑cache一致性的问题。

        想来想去,我觉得kernel中有2种情况是需要保证数据一致性的:

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

       (2)DMA缓冲区的地址空间。DMA操作对于CPU来说也是不透明的,DMA导致内存中数据更新,对于CPU来说是完全不可见的。反之亦然,CPU写入数据到DMA缓冲区,其实是写到了cache,这时启动DMA,操作DDR中的数据并不是CPU真正想要操作的。kernel中对于DMA操作是如何保证cache一致性的呢?在LDD3的内存映射和DMA一章中详细介绍了通用DMA操作层的一些函数,当时看这一章有些说法看得我挺晕的,现在站在cache的角度再来看就明了了好多。

       通用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);

     (2)流式DMA映射,代表函数:

        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);

        首先来看一致性映射如何保证cache数据一致性的。直接看下dma_alloc_coherent实现。
       


void *

dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)

{

    void *memory;

 

    if (dma_alloc_from_coherent(dev, size, handle, &memory))

        return memory;

 

    return __dma_alloc(dev, size, handle, gfp,

               pgprot_dmacoherent(pgprot_kernel),

               __builtin_return_address(0));

}

关键点在于pgprot_dmacoherent,实现如下:

#ifdef CONFIG_ARM_DMA_MEM_BUFFERABLE

#define pgprot_dmacoherent(prot) \

    __pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_BUFFERABLE | L_PTE_XN)

#define __HAVE_PHYS_MEM_ACCESS_PROT

struct file;

extern pgprot_t phys_mem_access_prot(struct file *file, unsigned long pfn,

                     unsigned long size, pgprot_t vma_prot);

#else

#define pgprot_dmacoherent(prot) \

    __pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_UNCACHED | L_PTE_XN)

#endif

         其实就是修改page property为uncached,这里需要了解,page property是kernel页管理的页面属性,在缺页异常填TLB时,该属性就会写到TLB的存储属性域中。保证了dma_alloc_coherent映射的地址空间是uncached的。

        并且__dma_alloc中调用__dma_alloc_buffer,在./arch/arm/mm/dma-mapping.c的100行处,如下:


    /*

     * Ensure that the allocated pages are zeroed, and that any data

     * lurking in the kernel direct-mapped region is invalidated.

     */

    ptr = page_address(page);

    memset(ptr, 0, size);

    dmac_flush_range(ptr, ptr + size);

    outer_flush_range(__pa(ptr), __pa(ptr) + size);

        在分配到缓冲区后,对缓冲区进行刷cache以及可能存在的外部cache(二级cache)的刷新。dma_alloc_coherent首先对分配到的缓冲区进行cache刷新,之后将该缓冲区的页表修改为uncached,以此来保证之后DMA与CPU操作该块数据的一致性。     LDD3中说一致性映射缓冲区可同时被CPU与DMA访问,就是通过uncached的TLB映射来保证的。再来看流式DMA映射,以dma_map_single为例,因代码调用太深,这里就只是列出调用关系,跟cache有关的调用如下:


dma_map_single ==> __dma_map_page ==> __dma_page_cpu_to_dev ==> ___dma_page_cpu_to_dev 

___dma_page_cpu_to_dev实现如下:

void ___dma_page_cpu_to_dev(struct page *page, unsigned long off,

    size_t size, enum dma_data_direction dir)

{

    unsigned long paddr;

 

    dma_cache_maint_page(page, off, size, dir, dmac_map_area);

 

    paddr = page_to_phys(page) + off;

    if (dir == DMA_FROM_DEVICE) {

        outer_inv_range(paddr, paddr + size);

    } else {

        outer_clean_range(paddr, paddr + size);

    }

    /* FIXME: non-speculating: flush on bidirectional mappings? */

}

dma_cache_maint_page会对映射地址空间调用dmac_map_area,该函数最终会调用到arch/arm/mm/cache-v7.S中v7处理器的cache处理函数v7_dma_map_area,如下:

/*

 *  dma_map_area(start, size, dir)

 *  - start - kernel virtual start address

 *  - size  - size of region

 *  - dir   - DMA direction

 */

ENTRY(v7_dma_map_area)

    add r1, r1, r0

    teq r2, #DMA_FROM_DEVICE

    beq v7_dma_inv_range

    b   v7_dma_clean_range

ENDPROC(v7_dma_map_area)

       指定方向为DMA_FROM_DEVICE,则v7_dma_inv_range无效掉该段地址cache。方向为DMA_TO_DEVICE,则v7_dma_clean_range写回该段地址cache。

       保证了cache数据一致性。之后在___dma_page_cpu_to_dev中还对外部cache进行了刷新。dma_map_single中并没有对cpu_addr指定缓冲区映射的存储属性进行修改,还是cached的,但是对该缓冲区根据数据流向进行了cache写回或者无效,这样也保证了cache数据一致性。

       再来看dma_unmap_single,调用关系如下:
 


dma_unmap_single ==> __dma_unmap_page ==> __dma_page_dev_to_cpu ==>

___dma_page_dev_to_cpu ==> dmac_unmap_area ==》v7_dmac_unmap_area

dmac_unmap_area实现如下:

/*

 *  dma_unmap_area(start, size, dir)

 *  - start - kernel virtual start address

 *  - size  - size of region

 *  - dir   - DMA direction

 */

ENTRY(v7_dma_unmap_area)

    add r1, r1, r0

    teq r2, #DMA_TO_DEVICE

    bne v7_dma_inv_range

    mov pc, lr

ENDPROC(v7_dma_unmap_area)

        指定方向为DMA_TO_DEVICE,不做任何操作。方向为DMA_FROM_DEVICE,则v7_dma_unmap_area无效掉该段地址cache。我的理解因为对于指定为CPU需要读取的数据,在释放该缓冲区后必须保证cache数据一致性,接下来CPU就要读取数据进行处理了。而对于指定为CPU写的数据,缓冲区释放后CPU不会再去操作该缓冲区,所以不做任何操作。

      从这里就可以看出来,LDD3讲到,流式DMA映射对于CPU何时可以操作DMA缓冲区有严格的要求,只能等到dma_unmap_single后CPU才可以操作该缓冲区。究其原因,是因为流式DMA缓冲区是cached,在map时刷了下cache,在设备DMA完成unmap时再刷cache(根据数据流向写回或者无效),来保证了cache数据一致性,在unmap之前CPU操作缓冲区是不能保证数据一致的。

       因此kernel需要严格保证操作时序。当然kernel也提供函数dma_sync_single_for_cpu与dma_sync_single_for_device,可以在未释放时操作缓冲区,很明显这2个函数实现中肯定是再次进行刷新cache的操作保证数据一致性。到这里DMA的2种类型映射都分析完了,很清晰的看出一致性映射与流式DMA映射核心区别就是在于缓冲区页表映射是否为cached,一致性映射采用uncached页表保证了CPU与外设都可以同时访问。不过这些都是内核为驱动开发者已经封装好的接口函数,驱动开发者并不需要关心cache问题,只需要按照LDD3的规定调用这些接口即可。这也就是为什么在驱动中很少见到cache操作,内核代码将cache操作做到对驱动不透明了。

       这也让我想起了在开发网卡驱动时,DMA描述符的分配是一致性映射,是因为DMA描述符需要CPU与设备同时操作。而数据收发缓冲区分配是流式的,随用随分配,用完释放后CPU才可以操作数据!

       到目前为止,我所接触到的内核TLB映射,做了uncached映射的只有2个:寄存器空间(ioremap)和一致性DMA缓冲区(dma_alloc_coherent),其他地址空间都是cached,来保证系统性能。内核操作cache的时机是在操作DMA缓冲区时,其他时候内核代码不需要关心cache问题。这篇文章就分析到这!
————————————————
版权声明:本文为CSDN博主「kerneler_」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/skyflying2012/article/details/48023447

 


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值