ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] DMA - cache consistency
    Linux/kernel 2023. 12. 6. 12:37

    글의 참고

    - https://zhuanlan.zhihu.com/p/109919756

    - https://zhuanlan.zhihu.com/p/508439396

    - https://zhuanlan.zhihu.com/p/515450647

    - http://15418.courses.cs.cmu.edu/spring2013/article/21

    - https://blog.csdn.net/Adrian503/article/details/115536886

     


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - Overview

    " DMA 와 Cache 의 관계는 뭘까? 이걸 이해하기 위해서는 예를 하나 들어보자. 일반적으로, CPU 에 수정된 데이터들은 메모리가 아닌 cache 에 있게 된다(cache 정책을 write-back 임을 가정). 그리고, DMA 는 특정 데이터를 메모리에서 peripheral devices 들에게 transfer 한다. 그런데, 이 시점에 DMA 가 메모리에서 가져온 데이터는 new data 가 아닌, old data 가 된다. 이러한 문제를 방지하기 위해 DMA 는 read 동작시에 즉각적으로 메모리에서 read 하는 것이 아닌, cache 에 먼저 데이터가 있는지를 확인한다. 즉, cache hit 가 발생하는지를 검사한다. 만약, cache hit 라면, 메모리에서 read 하지 않고 cache 에서 read 한다. 그러나, 이러한 동작들은 대개 아키텍처 의존적인 부분이 강하기 때문에, 반드시 cache 를 먼저 검사한다고 보기는 어렵다.

     

     

     

     

    - Two instructions about cache in ARM [참고1]

    " arm 에서는 cache 관련 용어로 `invalidate` 와 `clean` 만 사용한다. flush 같은 경우는 언급되는 부분은 없지만, 대개 `clean + invalidate` 로 사용되는 것으로 확인된다. arm 에서 `invalidate` 명령어는 특정 cacheline 을 `invalid` 하게 만든다. 그리고, `clean` 명령어는 `dirty` 표시가 된 cacheline 을 memory 로 write-back 한다. 이 때, 중요한 점은 `invalid` 건 `clean` 이건 실제로 cacheline 을 delete 하지는 않는다는 것이다. 마치, 스택이 메모리를 정리할 때와 유사하다고 보면 될 것 같다. 즉, 데이터 자체가 유효하지 않기 때문에 정리할 필요가 없는 것이다.

    `Invalidate` simply marks a cache line as "invalid", meaning you won't hit upon.

    `Clean` causes the contents of the cache line to be written back to memory (or the next level of cache), but only if the cache line is "dirty".  That is, the cache line holds the latest copy of that memory.

    - 참고 : https://community.arm.com/support-forums/f/architectures-and-processors-forum/3731/clean-and-invalidate-cache-memory

     

     

     

    - Cache inconsistency

    " project 를 진행하다보면, cache inconsistency 관련 문제를 접하게 되는 경우가 있다. 이는 크게 2가지로 나눠 볼 수 있다.

    1. MMIO
    " 특정 peripehrals 들에게 MMIO 를 할당할 경우, peripherals 들은 자신들의 registers 를 RAM 에 mapping 시킬 수 있게 된다. 그 이후 CPU 는 MMIO 에 mapping 된 registers 들을 통해 peripherals 들과 통신할 수 있게 된다. 그런데, CPU 는 RAM 을 읽을 때, 직접적으로 RAM 을 읽기보다는 먼저 cache 를 읽는다. cache 에 해당 내용이 있다면, 대신 RAM 을 읽지 않는다. 여기서 device 의 status 가 변경되면서, device`s status register 의 값이 변경된 경우, CPU 가 해당 내용을 읽고 싶어도 cache 를 먼저 읽게 되는데, 이 때 cache hit 발생하면, RAM 을 읽지 못하게 된다. 결국, CPU 는 계속 device status register 를 읽지 못하게 될 수 있다. 그렇기 때문에, 커널에서 register 관련 operations 들은 모두 `consistency` 해야 한다. 리눅스를 register consistency 를 지원하기 위해 `ioremap` 을 지원한다. 즉, ioremap() 인터페이스를 통해서 mapping 된 address space 는 non-cacheable 로 설정된다. 이 말은, register 를 읽을 때, 직접적으로 memory 에서 읽는다는 것을 의미한다. 

    2. DMA
    " DMA operatons 들 또한 CPU 에게는 invisible 하다. 즉, DMA 가 memory(DMA buffer)를 update 하더라도 CPU 는 이 사실을 알 수 없다는 뜻이다. CPU operations 도 DMA 에게는 invisible 하다. 왜냐면, CPU 가 memory 에 뭔가를 쓰려고 하면, memory 가 아닌 cache 에 wrtie 되기 때문이다(write-back 을 가정). 이 문제는 크게 2가지로 나뉜다.
    1. DMA 는 memory 에 access 할 때, 직접적으로 system bus 를 이용해서 memory 에 access(read / write) 하는데, 이걸 CPU 가 인지하지 못한다. 즉, DMA 가 데이터를 변경해도 CPU 는 알지 못한다.

    2. DMA buffer 가 cached 상황에서 CPU 가 DMA buffer(cache) 의 내용을 수정할 경우, cache 에 내용만 변경되기 때문에(write-back 이라고 가정) DMA 는 이걸 인지하지 못한다. 결국, DMA 는 memory 안에 있는 old data 에 access 하게 된다. 

     

     

    " 위와 같은 DMA consistency 문제를 해결하기 위한 해결책으로 3가지 솔루션이 존재한다.

    1. CCI(cache coherent interconnect) 와 같이 hardware level 에서 cache consistency 를 지원하는 솔루션(CCI 를 지원할 경우, `dma_alloc_coherent()` 인터페이스는 cacheable memory 를 반환함).

    https://community.arm.com/arm-community-blogs/b/architectures-and-processors-blog/posts/extended-system-coherency---part-2---implementation-big-little-gpu-compute-and-enterprise

    2. non-cacheable memory region 을 DMA buffer 로 사용하는 것이다. 이 방법은 가장 심플하지만, efficient 가 좋지 못하다. 즉, 심각한 performance 저하와 power consumption 까지 증가한다.

    3. 최대한 소프트웨어 만을 활용해서 cache consistency 문제를 해결하는 것이다. 이 방식은 기존부터 사용되오던 방식인데, CCI 와 같은 cache coherence controller 가 도입되기 전까지는 주로 이 방식을 사용해왔다. 

     

     

    - Bus monitoring

    " bus monitoring 이란 무엇일까(사실, 이 bus monitoring 이라는 것이 위에 섹션에서 언급된 `DMA consistency` 에 대한 솔루션으로 언급된다)? cache controller 는 system bus 에 모든 memory accesses 들을 monitor 하고 이중에서 cache hit 가 있는지를 검사한다(이제부터 cache hit 가 됬다는 전제로 설명을 이어나간다). 참고로, DMA operations 들은 모두 physical address 를 기반으로 수행된다. 즉, CPU 와 다르게 DMA 는 physical address 를 기반으로 memory access 를 해야한다는 것을 의미한다. 그리고, cache controller 는 system bus 에서 지나다니는 모든 memory accesses 들을 monitor 한다고 했다. system bus 에 지나다니는 memory address 는 MMU 를 거친 address, 즉, physical address 를 의미한다. 이 말은 cache controller 가 컨트롤하는 address 는 physical address 를 의미하고, 이와 동시에 cache controller 가 컨트롤하는 cache 또한 physical address 를 기반으로한 lookup 이 되야한다는 뜻이다(아래 그림은 Cortex-A9 과 PL310(cache controller) 의 관계를 보여준다. Cortex-A9 과 AXI interface 로 연결되어 있으며, 이 말은 cache controller 가 processor unit 외부에 있는 것을 알 수 있다. 즉, 아래 그림은 하드웨어적으로 cache controller 는 physical address 를 받는다는 것을 보여주는 그림이다).

     

    " 이러한 bus monitoring 방식을 충족시키는 cache type 은 `PIPT` 다(VIVT 는 virtual address 를 기반으로 cache 를 lookup 한다. 그래서 bus monitoring 으로는 VIVT 가 사용되지 않는다). 그러나, bus monitoring 기술은 hardware level 동작하는 기술이고, 심지어 모든 하드웨어에서 bus monitoring 을 지원하는 것은 아니다. 그렇다면, operating system 은 이러한 하드웨어들 까지도 모두 지원해야 하는 책임있는데, 이걸 어떻게 software 구현했을까?

     

     

    1. non-cacheable(DMA consistency mapping)

    " `non-cacheable` 는 DMA buffer 로 지정된 memory region 을 caching 하지 않겠다는 뜻이다. 리눅스에서 dma_alloc_coherent() 함수를 호출할 경우, non-cacheable memory region 를 반환한다. 그러나, 일부 SoC 에서는 hardware 차원에서 CPU`s 들과 peripheral`s 들 사이에 `cache coherent interconnect` 가 존재한다. 그래서, dma_alloc_coherent() 인터페이스는 아키텍처마다 구현 방법이 달라진다. hardware 적으로 cache consistency 를 지원할 경우, dma_alloc_coherent() 인터페이스 또한 cacheable memory 을 반환할 수도 있다. 그러나, default 로 dma_alloc_coherent() 인터페이스는 non-cacheable memory 를 반환한다. 아래의 patch 내용은 ARMv7 의 내용이지만, 동일한 프로세서여도 SoC 의 종류에 따라(즉, hardware level 에서 DMA consistency 를 지원할 경우) dma_alloc_coherent() 인터페이스의 기능이 달라진다는 것을 보여주고 있다.

    `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.

    - 참고 : https://www.spinics.net/lists/arm-kernel/msg322447.html

     

     

    " DMA mapping 은 2 가지가 존재한다. 이 중에서 `DMA consistency mapping` 이 non-cacheable 을 의미한다(물론, 위에서 언급했다시피, hardware level 에서 CCI 가 지원될 경우 dma_alloc_coherent() 도 cacheable memory 를 반환한다).

    // include/linux/dma-mapping.h - v6.5
    static inline void *dma_alloc_coherent(struct device *dev, size_t size,
    		dma_addr_t *dma_handle, gfp_t gfp);
            
    static inline void dma_free_coherent(struct device *dev, size_t size,
    		void *cpu_addr, dma_addr_t dma_handle);

     

    " driver 에서 dma_alloc_coherent() 함수를 통해 DMA buffer 를 할당할 경우, DMA buffer 의 life cycle 은 driver 와 동일하다. 즉, driver 가 release 되기전까지는 계속 사용된다는 것을 의미한다. 

     

     

     

    2. maintains cache consistency via software

    " software 를 통해서 cache consistency 를 유지하는 것이다. 이것은 cache 의 이점을 최대한 활용한다. DMA transmission direction 에 따라 조치 방법이 달라진다.

    1. From device FIFO to memory(DMA buffer) via DMA
    " device 쪽에서 DMA buffer 에 데이터를 write 해야 하는 경우는 NIC 를 통해 network packets 들을 받는 상황과 유사하다. 이 상황을 분석해보면, DMA trasfer 가 발생하기전에 device FIFO 에 최신 데이터가 저장된 상황이라 볼 수 있다. 그렇다면, 당연히 CPU cache 에 있는 데이터는 old data 가 되고, DMA transfer 가 시작되기전에 먼저 CPU cache 를 invalidate 해야한다는 것을 알 수 있다. CPU cache 가 invalidated 되고 난 후에, DMA transfer 가 시작된다.


    2. From memory(DMA buffer) to device FIFO via DMA
    " `memory -> device FIFO via DMA` 케이스는 주로 CPU 가 기존 데이터가 아닌 new data 를 생성한 경우에 발생한다. 이 때, new data 는 CPU cache 에 cached 되고나서 `clean(arm) or flush(x86)` 명령어를 통해 최신 데이터가 저장된 CPU cache 의 내용이 DMA buffer 에게도 전달된다. 여기서 `clean/flush` 이란 DMA buffer 에 있는 modified datas 들을 cache 에 write-back 하는 것이다. 중요한 것은 DMA trasfer 이 수행되기전에 먼저 cache 를 clean / flush 해야 한다는 것이다.

     

    " 위에 상황을 정리하면 크게 2가지 관점에서 나눠볼 수 있다.

    1. DMA transfer 가 시작되기 전에 new data 의 source 가 CPU 냐 device 이냐? 
    2. DMA transfer 가 시작되기 전에 CPU cache 데이터가 new 인지 old 인지 

     

    " 그리고, 주의할 점이 있다. CPU 는 DMA transfer 이 완료되기 전까지 DMA buffer 에 access 하면 안된다. 예를 들어, DMA transfer 도중에 CPU 가 DMA buffer 에 access 할 경우, 얻게 되는 데이터는 consistency 가 보장되지 않는다. 그렇다면, DMA transfer 이 완료됬다는 것은 어떻게 알 수 있을까? DMA streaming mapping 같은 경우, dma_unmap_single() 함수를 호출할 경우, DMA transfer 가 완료되었음을 의미한다. 즉, dma_map_single() 함수를 호출한 시점과 dma_unmap_single() 함수가 호출된 시점 사이에 DMA buffer 를 건드리면 안된다는 뜻이다.

     

     

     

    - DMA buffer alignment

    " `temp` 와 `buffer` 라는 2개의 전역 변수가 있다고 가정하자. 그리고, buffer 변수는 DMA cache 로 사용된다. 초기 temp 값은 5 이다. 그리고, temp 와 buffer 전역 변수는 서로 전혀 관련없는 데이터라고 가정한다. 예를 들어, buffer 변수는 DMA operation process 에서 사용되고, temp 는 DMA 와는 전혀 관련없는 다른 process 에서 사용된다고 볼 수 있다.

    // https://zhuanlan.zhihu.com/p/109919756
    int temp = 5;
    char buffer[64] = { 0 };

     

    " cacheline 은 64-byte 이고, temp 와 buffer 는 동일 cacheline 에 존재한다고 가정하자. 동시에, buffer 는 두 번째 cacheline 까지 침범하게 된다.

     

     

    " 위와 같은 상항에서 DMA 가 peripheral 로부터 데이터를 읽어서 DMA buffer(memory) 에 써야한다고 상황을 가정해보자(즉, from device FIFO to memory). 그렇다면, 절차는 다음과 같다.

    1. 이전 섹션에서 설명한 내용에 따르면, 먼저 DMA buffer 에 대응하는 cacheline 2개를 invalidate 한다. 

    2. DMA transfer 를 시작한다.

    3. buffer[3] 에 DMA transfer 하고 있는데, 다른 process 가 temp 의 값을 `6` 으로 변경했다. 이 시점에 cacheline 이 64-byte 이기 때문에, temp 뒤에 선언된 buffer 배열에서 buffer[59:0] 이 temp 와 동일 cacheline 에 저장되게 된다. 그리고, temp 의 값이 변경되었기 때문에, temp 속한 cacheline 의 dirty bit 가 marked(`1`) 된다.

    4. 아직 DMA transfer 가 끝나지 않았다. buffer[50] 에 DMA transfer 하고 있는데, 다른 process 에서 temp 를 read 했다. 그런데, temp 가 속한 cacheline 은 dirty 이기 때문에, 해당 cacheline 은 memory 로 write-back 된다. 이 때, temp 와 buffer[59:0] 이 write-back 된다.

     

     

    " 4번 단계에서 문제가 발생한다. write-back 시에 DMA buffer 에서 충돌 발생의 우려가 있다. 즉, DMA 가 buffer[58] 에 DMA transfer 중에 write-back 때문에 CPU cache 에 있는 old buffer[59:0] 가 memory(DMA buffer) 를 overwrite 할 수 있다. 즉, DMA buffer 는 다른 데이터들과 cacheline 을 공유하면 안된다. 즉, 독단적으로 cacheline 을 차지해야 한다. 그러므로, DMA buffer 는 반드시 2 가지 조건을 충족해야 한다. DMA buffer 가 아래와 같은 2 가지 조건을 충족하게 되면, 다른 데이터들과 뒤섞여서 cacheline 에 저장될 요소가 사라진다.

    1. DMA buffer 의 first address 는 반드시 cacheline 에 aligned 되어야 한다.
    2. DMA buffer size 또한 반드시 cacheline 에 aligned 되어야 한다.

     

     

     

    - Linux DMA buffer

    " 리눅스에서 DMA buffer 는 stack or global variable 로 생성될 수 없다. 왜냐면, 이러한 변수들은 모두 cacheline alignment 를 보장하지 않기 때문이다. 그렇다면, DMA buffer 는 어떻게 생성되는 것일 좋을까? `kmalloc()` 함수를 사용해서 DMA buffer 를 생성할 수 있다. bus monitoring 을 지원하지 않는 일부 아키텍처는 반드시 kmalloc() 을 통해 할당되는 주소가 cacheline 에 alignment 하도록 보장해야 한다. 그래서 arm64 리눅스 같은 경우, kmalloc() 을 통해 반환되는 주소가 cacheline alignment 를 강제하기 위해 아래와 같은 매크로를 정의하고 있다.

    // arch/arm64/include/asm/cache.h - v6.5
    /*
     * Memory returned by kmalloc() may be used for DMA, so we must make
     * sure that all such allocations are cache aligned. Otherwise,
     * unrelated code may cause parts of the buffer to be read into the
     * cache before the transfer is done, causing old data to be seen by
     * the CPU.
     */
    #define ARCH_DMA_MINALIGN	(128)

     

    " arm64 같은 경우는 기본적으로 L1 cacheline 이 64-byte or 128-byte 다. cacheline 이 128-byte 일 경우, kmalloc() 을 통해 할당되는 주소는 모든 cacheline 에 저장될 수 있다. 그러나, cacheline 이 64-byte 일 경우, 짝수 번째(64*2 , 64*4, ...) cacheline 에만 저장될 수 있다. 그러나, kmalloc() 에서 128-byte alignment 주소를 반환하는 것은 메모리 낭비 문제가 있다. 예를 들어, arm64 에서는 8-byte 만 할당받고 싶은 경우에도, 최소 128-byte 를 할당하기 때문에 120-byte 가 낭비된다(x86_64 같은 경우는 hardware level 에서 DMA consistency 가 지원되기 때문에, ARCH_DMA_MINALIGN 같은 매크로가 정의될 필요가 없다).

Designed by Tistory.