ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] DMA - overview
    Linux/kernel 2023. 12. 1. 21:33

    글의 참고

    - Linux Device Drivers, Third Edition

    - https://docs.kernel.org/core-api/dma-api-howto.html

    - https://www.jianshu.com/p/e1b622234d13

    - http://www.wowotech.net/linux_kenrel/dma_engine_overview.html

    - https://www.bilibili.com/read/cv21554003/

    - http://www.wowotech.net/linux_kenrel/dma_engine_api.html

    - http://www.wowotech.net/memory_management/DMA-Mapping-api.html

    - https://blog.csdn.net/weixin_71478434/article/details/126559562

    - https://docs.oracle.com/cd/E19683-01/806-5222/hwovr-22/

    - https://www.cnblogs.com/wanglouxiaozi/p/15045622.html

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

    - https://blog.csdn.net/m0_52840978/article/details/125493519

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

    - https://binaryterms.com/direct-memory-access-dma.html#WhatisDMAandWhyitisused?

    - https://electronicsdesk.com/dma-controller.html

    - http://www.wowotech.net/tag/dma

    - https://stackoverflow.com/questions/35444326/pcie-dma-consistent-vs-streaming-memory

    - https://geidav.wordpress.com/2014/04/27/an-overview-of-direct-memory-access/

    - https://lwn.net/Articles/75780/


    글의 전제

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

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

    - kernel document `DMA-API-HOWTO` 문서를 기반으로 다양한 DMA 관련 내용들을 추가한 글.


    글의 내용

    - Overview

    " DMA 를 알아보기 전에, 먼저 PIO 를 알아야 한다. PIO 는 2가지 의미로 해석될 수 있다.

    1. Programmed Input/Output(Programmable Input/Output)
    2. Port-Mapped I/O

     

     

    " 먼저 `Programmed Input/Output(Programmable Input/Output)` 에 대해 알아보자. PIO 는 CPU 가 I/O 장치들과 데이터를 주고 받는 방법을 의미한다. 여기서 I/O 장치란 프로세서 외부에 존재하는 모든 장치를 의미한다(램도 외부이지만, 예외로 놓는다). 이 PIO 에는 2가지 방법이 존재한다.

    1. MMIO : Memory-Mapped I//O 
    2. PMIO : Port-Mapped I/O

     

     

    " 이 2가지 방법 모두 CPU 가 직접 명령어를 실행한다. 즉, 데이터 전송 작업에 CPU 가 직접적으로 참여한다는 소리다. MMIO 같은 경우, CPU 는 메모리를 통해서 외부 장치와 데이터 통신을 하게 된다(`x86` 같은 경우 mov 명령어`). PMIO 는 별도의 I/O 영역을 통해서 CPU 가 외부 디바이스와 통신하는 것이다(`x86` 같은 경우, in & out 명령어). 여기서 중요한 점은 PIO 방식은 CPU 가 외부 디바이스와 데이터를 주고 받기 위해 직접 명령어를 수행한다는 것을 꼭 기억하자.  

     

     

    " 그런데, 외부 장치는 속도가 상당히 느리다. 즉, CPU 가 MMIO or PMIO 를 하든 외부 장치의 응답 속도가 상대적으로 너무 느리다 보니, CPU 파이프라인에서 많은 양의 `bubble` 이 발생할 우려가 있다. 그래서 등장한 것이 `DMA` 다. DMA 는 RAM 에 직접 액세스할 수 있기 때문에, CPU 가 DMA 에게 요청을 하면 DMA 가 외부 디바이스와 데이터 통신을 주고 받은 뒤, 해당 결과물을 특정 영역에 쓴다. 그리고, 인터럽트를 발생시켜 CPU 가 RAM 에서 결과물을 읽어가도록 하게 한다. 결과적으로, CPU 입장에서 DMA 는 PIO 가 아닌, `Interrupt driven I/O` 방식을 사용해서 data transfer 를 수행한다고 볼 수 있다.

     

     

    - DMA operating modes [참고1 참고2]

    " 위에서 본 programmed I/O 와 Interrupt driven I/O 는 기본적으로 large data block 을 전송하는데 최적화 되어있지 않다. 그렇다면, DMA 는 large data block 을 전송하는데 최적화 되어있을까? 이건 DMA operating mode 에 따라 다르다. 총 3 가지를 지원한다.

    1. Burst Mode - DMA controller 가 system bus 의 제어권을 얻게 되면, data transfer 가 완료될 때 까지 system bus 를 release 하지 않는다. 즉, data transfer 이 완료될 때만, system bus 를 release 한다. 즉, DMA controller 가 data transfer 중일 때(system bus 제어권을 획득했을 때), CPU 가 작업을 수행하고 싶다면, system bus 를 기다려야 한다. 그래서, 이 모드는 `Block Transfer Mode` 라고도 불린다.
     
    2. Cycle Stealing Mode - 이 모드에서 DMA controller 는 burst mode 와 마찬가지로 `bus request` 와 `bus grant` 라인을 통해서 system bus 에 대한 제어권을 획득한다. 이 2 개의 signals 은 CPU 와 DMA controller 의 사이에 control interface 를 의미한다. cycle stealing mode 에서는 DMA controller 가 one or two bytes 데이터를 전송한 후에, system bus 제어권을 release 한다. 그리고, 다시 system bus 제어권을 다시 요청한다. 이 과정은 data transfer 이 완료될 때 까지 계속된다. 이 모드에서는 burst mode 만큼 data transfer 가 빠르지는 않지만, CPU 가 IDLE 상태로 있는게 burst mode 때와 비교해서 상대적으로 적다.

    3. Transparent Mode - processor 가 system bus 를 사용하지 않는 operation 을 수행 중 일때만, DMA controller 가 bus 를 획득한다. 즉, 이 모드는 DMA modes 들 중에서 data transfer 를 수행하는데, 가장 오랜시간이 걸리지만, system performance 측면에서 가장 효율적인 모드다.

     

     

     

    - Direct Memory Access Controller & it`s Working

    " 아래 그림은 DMA controller 의 실제 hardware schematic 을 간추린 모습이다. 이 그림을 통해서 간략하게 DMA 동작 과정에 대해 알아본다.

     

     

    1. I/O device 가 data 를 memory 에 전달하거나 memory 에서 데이터를 꺼내오고 싶을 경우, I/O device 는 DMA controller 에게 DMA request(DRQ) 를 trigger 한다. DMA controller 는 DRQ 를 인지하고 Hold request(HLD) 핀을 trigger 함으로써, CPU 가 잠시 동안 멈추게 만든다(CPU holds a few clock cycles). 

    2. CPU 가 HLD 를 인지하고, bus 를 포기한다(relinquishe). 그리고, CPU 는 DMA controller 에게 `Hold acknowledgement(HDLA)` 를 trigger 한다.

    3. HLDA 를 받은 DMA controller 는 `DACK` 를 trigger 함으로써, I/O devie 에게 이제 data transfer 가 가능함을 알리고, 시스템 버스의 bus master 가 되어 memory 에 data transfer 를 시작한다. 

    4. data transfer 가 완료되면, DMA controller 는 interrupt 를 발생시켜 CPU 에게 data transfer 가 완료되었음을 알린다. 이후에 CPU 에게 bus 에 대한 제어권이 넘어가고 이전 멈췄던 곳부터 다시 작업을 이어나간다.

     

     

    " 아래 block diagram 은 좀 더 회로적인 관점에서 DMA operation 을 살펴볼 수 있다. 처음에는 system bus 의 bus master 는 CPU 다. 그러나, CPU 가 HRQ 시그널을 받는 시점에 bus master 권한을 DMA controller 에게 넘겨주기 위해 CPU 는 systme bus 를 tristate 상태로 만든다. CPU 가 system bus 를 free 하는 시점에 HLDA 시그널을 DMA controller 에게 전송한다. HLDA 시그널을 받은 시점에 X 에서 Y 로 switching 되면서, DMA controller 는 system bus 에 대한 권한을 갖게 된다.

     

     

     

    " 이제 DMA controller 는 Disk controller 에게 DACK 시그널을 전송함으로써, 이제부터 data transfer 이 가능함을 알린다. 이후에 DMA controller 는 어떤 동작을 수행해야 하는지에 따라 control bus 에 operation signal 을 trigger 한다. 예를 들어, write operation 을 수행하야 하는 경우에(from device to memory) IOR`, MEMW` 라인을 low 로 trigger 한다. IOR` 시그널을 trigger 되면, disk controller 는 block data 를 data bus 에 load 한다. 또한, MEMW` 시그널이 trigger 되면, address bus 에 data 가 transfer 되어야 하는 memory address 를 나타나게 된다. disk controller 는 address bus 를 참고해서 data 를 특정 memory address 에 CPU 의 개입없이 직접 전송하게 된다. diks controller 의 data transfer 이 완료되면, DMA controller 는 Y 에서 X 로 switching 시킴으로써 interrupt 발생시킨다. 이 시점에 micro processor 는 data transfer 가 모두 완료되었음을 알게된다.

     

     

    - Direct Memory Access Diagram

    " `Direct Memory Access Controller & it`s Working` 섹션에서는 어떻게 DMA 가 동작하는지를 알아봤다. 이번에는 DMA controller 의 internal block diagram 을 통해서 어떻게 DMA controller 가 구성되어 있는지 확인해보자.

     

     

     

    " CPU 가 특정 device 로부터 블락 데이터를 read or write 하라는 요청을 받을 때 마다, CPU 는 DMA controller 에게 아래의 정보들을 전달한다.

    1. 제일 첫 번째 정보는 memory 에서 data 를 read 해야 하는지 혹은 write 하는지를 나타내는 정보다. 이 정보는 Control Login 에 있는 `read` 혹은 `write` 핀을 통해서 전달한다.
     
    2. 또한 CPU 는 data block 의 starting address of memory 를 전달한다. DMA controller 는 이 주소를 starting address register(address register) 에 저장한다.

    3. 또한 CPU 는 word count 를 전달함으로써, 얼마나 많은 words 들이 read or write 되야 하는지를 word count(data count) 에 저장한다.

    4. 가중 중요한 정보는 data 를 read 혹은 write 하길 원하는 I/O device 의 address 다. 이 정보는 data register 에 저장된다.

     

     

     

    - ISA DMA(Third-Party DMA) vs PCI DMA(First-Party DMA or Bus masterring) [참고1 참고2]

    " bus mastering 이란 bus architecture 의 특징중 하나로, bus mastering 을 지원하는 bus 에 attached 된 devices 들은 DMA controller 없이 자체적으로 DMA transfer 를 수행할 수 있다. 이 말은 CPU 에 개입없이 device 가 자기가 원할 때, main memory 에 data 를 read / write 할 수 있다는 뜻이다. Bus mastering 에 대해 자세히 알아보기 전에 2 가지 DMA 구현 방식에 알아보자.

    1. Third-Party DMA(Old) - Ex. ISA DMA Mechanism
    2. First-Party DMA(New) - Ex. PCI DMA Mechanism

     

     

    " ISA Bus 가 사용되던 시절에는 devices 들이 직접 DMA transfer 에 참여할 수 없었다. 이 때는 device 가 main memory 에 data 를 read / write 하고 싶을 경우, 반드시 DMA controller 를 통해서 DMA transfer 를 진행해야 했다. 이렇게 DMA controller 를 통해서 DMA transfer 가 수행되는 방식을 `third-party DMA` 라고 한다. 아래 그림에서 Intel 8237 이 DMA controller 다. 이 방식은 ISA devices 들이 DMA transfer 를 이용하려면, DMA controller(Intel 8237) 에 의존했기 때문에, performance 가 떨어지는 단점이 있다.

     

     

     

    " 1992 년 PCI 버스가 등장하면서, bus mastering or first-party DMA 라는 기술이 같이 소개되었다. 그러면서, DMA controller 는 점점 구식 방식이 되어갔고, 현재는 ISA bus 를 제외한 다른 곳에서는 더 이상 사용되지 않는다. PCI DMA transfer 는 한 시점에는 딱 하나의 디바이스만 PCI bus 에 access 할 수 있다. 이 때, PCI bus 에 제어권을 얻은 디바이스를 `bus master` 라고 한다. bus master 는 bus 를 hold 하는 동시에 CPU 의 개입없이 memory transfer 를 수행할 수 있다.

     

     

     

    " 그렇다면, ISA DMA 와 PCI DMA 의 가장 큰 차이는 무엇일까? DMA compatible devices 들은 반드시 memory transfer 를 수행할 수 있는 `DMA engine` 을 내장하고 있어야 한다. 예를 들어, 대다수의 PCI devices 들은 DMA engine 을 내장하고 있어서 system 에 DMA controller 가 존재하지 않더라도, PCI bus 를 기반으로 DMA transfer 를 수행할 수 있다. 그러나, 2 개 이상의 PCI devices 가 bus master 가 되기 위해서 bus 에 대한 제어권을 얻으려고 할 것이기 때문에, `arbitration mechanism(중재 메커니즘)` 이 필요하다. bus mastering 의 장점은 DMA transfer 시에 third-party DMA 가 필요없기 때문에 latency 를 줄일 수 있다. 

     

     

     

    - DMA engine overview in aspect of hardware

    " 위 섹션들에서 우리는 DMA controller 내부 로직에 대해 알아봤다. 이번에는 리눅스 커널에서 사용하는 DMA framewor 를 이해하기 위한 DMA hardware 에 대한 기본적인 정보들에 대해 알아본다. 먼저, DMA 는 I/O deivces 들과 memory 사이에 데이터를 옮기는 역할을 한다. 데이터를 옮기는 방식을 기준으로 DMA transfer 을 구분해보면 총 4가지로 나뉜다.

    1. dev-to-mem
    2. mem-to-dev
    3. mem-to-mem
    4. dev-to-dev

     

     

    " 리눅스 커널은 peripheral 을 slave 로 보기 때문에, 디바이스와 관련있는 data transfer(mem-to-dev, dev-to-mem, dev-to-dev) 를 `Slave-DMA transfer` 라고 부른다. 그리고, mem-to-mem transfer 는 `Async TX` 라고 부른다. 왜, mem-to-mem 만 별도의 API 로 빼났을까? mem-to-mem API(async_memcpy, async_memset, async_xor 등)는 사실 memcpy() 처럼 구현 자체가 상당히 심플하다(사실 mem-to-mem 은 굳이 DMA 를 호출하지 않아도 상관이없다). 그리고, 가장 중요한 I/O devices 들과의 data trasnfer 은 포함되어 있지 않기 때문에, 이 글에서는 Slave-DMA transfer 에 대해서만 다루도록 한다.

     

     

     

    1. DMA channels

    " 하나의 DMA controller 가 한 번에 수행할 수 있는 DMA transfer 의 개수는 제한되어 있다. 왜냐면, 물리적으로 DMA transfer 을 전송하는 통로의 개수가 제한되어 있기 때문이다. 여기서, DMA trasnfer 이 발생하는 통로를 DMA channel 이라고 부른다. 물론, DMA channel 이라는 개념은 `software` 다. 그 이유는 2가지가 있다.

    1. bus access conflict
    2. memory consistency

     

    " 물리적인 관점에서 NUMA 가 아니라면, 일반적으로 시스템에 존재하는 RAM 은 한개다. 거기다가, RAM 에 access 할 수 있게 해주는 system bus 또한 1개다. 즉, 물리적으로 DMA channel 이 여러 개라고 하더라도, 동시에 RAM 에 access 할 수 없다는 뜻이다. 만약, RAM 에 다수의 bus 가 존재해서 여러 개의 DMA channels 들이 동시에 RAM 에 access 하더라도, memory consistency 문제가 기다리고 있다. 

     

    " 결국 이 말은 DMA consumers 들에게 편의성을 제공하기 위해서 다수의 virtual DMA channels 을 제공함으로써 실제 하드웨어에서 지원하지 못하는 기능을 소프트웨어적으로 추상화한 것이다. DMA consumer 에서 DMA transfer 을 요청하면, 소프트웨어적으로 해당 요청들을 queueing 한 뒤, 이전 DMA transfer 가 끝나면, DMA FIFO 에서 최신 DMA request 를 꺼내서 수행한다. 즉, 소프트웨적으로는 `병렬적`으로 실행되는 것 같지만, 하드웨어적으로는 `직렬적` 으로 처리가 되고 있는것이다. 

     

     

    2. DMA request lines

    " DRQ, HLDA 와 같은 핀들을 통해서 실제 DMA transfer 가 진행된다. 이 핀들에 대한 기능 및 동작은 위에 섹션들을 참고하자.

     

     

    3. DMA transmission parameters

    " 메모리는 대부분이 1-byte 단위의 이동은 존재하지 않는다. 옛날에나 1-byte 씩 데이터를 이동시키는 경우가 있었지만, 지금은 cache 만 보더라도, cacheline size 는 default 로 64-bytes 가 설정된다. DMA 도 마찬가지다. data trasfer 시에 데이터를 byte 단위가 아닌 block or chunk 단위로 옮긴다면 performance 가 향상될 것이다. 이러한 부분을 충족시키기 위해 DMA controller 에게 `trasfer width` 라는 parameter 를 전달한다. 

     

     

    4. scatter-gather [참고1]

    " 일반적으로, DMA 는 물리적으로 주소가 연속적인 메모리(physically continuous memory) 만 처리할 수 있다. 그러나, 몇몇 경우에는 다수의 비-연속적인 메모리(non-continuous memory) chenks 들을 복사해서 하나의 연속적인 메모리 영역에 써야하는 경우가 존재한다. 이러한 동작을 `scatter gather` 라고 부른다.

     

    " 다수의 non-continuous memory chenks 들을 하나의 연속적인 메모리 영역에 transfer 할 때는 software 적으로 이루어지는 경우가 대부분이다. 그러나, performance 를 향상시키기 위해 하드웨어 차원에서 지원되는 경우도 많다(뒤에서 별도로 다루도록 한다).

     

     

     

    1. CPU and DMA address

    " DMA address mapping 은 총 3 가지 타입이 존재한다.

    1. CPU virtual address - CPU 가 사용하는 주소를 의미한다.
    2. CPU physical address - CPU 가 사용하는 가상 주소로 
    3. Bus address - device 에 의해서 사용되는 address space 를 의미한다.

     

    " kernel 에서 주로 사용하는 address 는 virtual address 다. 우리가 kmalloc(), vmalloc() 혹은 이와 비슷한 함수를 호출할 경우, 반환되는 주소는 virtual address 를 의미한다. 그리고, 이 주소는 `void *` 으로 반환되는 경우가 대부분이다. virtual memory system(TLB, page table, etc) 는 virtual address(program perspective) 를 physical address(CPU perspective) 로 변환하는데, physical address 는 `phys_addr_t` 혹은 `resource_size_t` 구조체에 저장된다.

     

     

    " kernel 은 device resource 중에서, 예를 들어, register 와 같은 resource 들은 virtual address 가 아닌 physical address 로 관리한다. /proc/iomem 을 통해서 device I/O 관련된 physical address 를 확인할 수 있다. 그러나, 아쉽게도 /proc/iomem 에 명시된 주소들은 physical address 이기 대문에, driver 에서 직접적으로 사용할 수 는 없다. 만약, driver 에서 physical addess 에 access 할 케이스가 생기면, `ioremap()` 함수를 사용해서 access 하고 싶은 physical addresses 를 전달한다. 그러면, ioremap() 가 전달된 physical addresses 와 mapped 된 virtual addresses 를 반환한다. 

     

     

    " 위에서는 CPU 가 사용하는 address 에 대해 알아봤다. 이번에는 I/O device 입장에서 살펴보자. I/O 디바이스 같은 경우는 bus address 라는 것을 사용한다. 일반적으로, device 가 bus address 를 사용하는 경우는 2 가지가 있다. 흔하지는 않지만, bus addess 와 physical address 와 동일한 경우도 있지만, 일반적으로는 서로 다르다. 왜냐면, IOMMU & Host-Bridge 가 bus address 와 physical addess 의 arbitrary mapping 을 생성하기 때문이다.

    1. device register 가 MMIO address 에 mapping 된 경우
    2. device 가 system memory 에 read / write 하기 위해 DMA 를 사용하는 경우

     

     

    " 한 가지 주의할 점이 있다. 디바이스 관점에서, 자신의 역량과는 별개로 주소 사용에 대한 부분이 제한될 수 가 있다. 예를 들어, 하드웨어적으로 64-bit main memory 와 64-bit PCI BARs 가 장착된 system 이 있더라도, IOMMU 및 DMA 가 32-bit 만 지원할 경우, PCI devices 들 또한 32-bit DMA addresses 만 사용해야 한다[참고1].

     

    " 왜 kernel 이 bus address 를 신경써야 하는 것일까? CPU 는 virtual address 를 사용해서 memory 에 access 한다. 이 때, 내부적으로 MMU 에 의해서 virtual address 가 physical address 로 변경되면서 memory 에 access 할 수 있게 된다. 그러나, I/O devices 들은 bus address 를 사용해서 memory 에 access 한다. 이 때, 내부적으로 Host-Bridge 혹은 IOMMU 를 통해서 bus address 가 physical address 로 변경되면서 memory 에 access 할 수 있게 된다. 즉, memory access 방법이 CPU 와 I/O devices 들이 다르다는 것을 알 수 있다. 정리하면 다음과 같다.

    1. CPU : MMU 에 의해서 virtual address 가 physical address 로 변경되면서 memory 에 access.

    2. I/O devices : Host-Bridge 혹은 IO-MMU 를 통해서 bus address 가 physical address 로 변경되면서 memory 에 access. 정리하면 다음과 같다.

    1. Host-Bridge with MMIO : CPU 가 device registers 에 접근하기 위해서는 MMIO 를 사용해야 한다. CPU 가 device register 를 읽기 위해 `J` 영역에 access 할 경우, Host-Bridge 는 physical address `J` 를 컨버팅해서 bus address `X` 를 읽을 수 있게해준다.

    2. IOMMU with DMA : DMA 를 사용할 경우, bus address 는 IOMMU 를 통해서 physical address 로 translate 된다. 

    3. Direct-DMA : 특수한 경우에 bus address 와 physical address 가 동일한 경우가 있다. 이러한 경우 DMA 는 IOMMU or Host-Bridge 와 같이 중간에 거치는 것 없이 bus address 로 사용해서 physical address 에 access 한다. 

     

     

    " 위에 내용에서 핵심은 CPU 가 virtual address 를 bus address 로 변환해야 한다는 것이다. 왜냐면, DMA controller 는 I/O devices 들을 기준으로 동작하기 때문이다. DMA transfer, 즉, `from device to memory` & `to device from memory` 를 하기 위해서는 결국 DMA controller 가 I/O devices 의 bus address 를 알고 있어야 한다. 엄밀히 따지면, I/O device 의 bus address 보다는 DMA buffer 의 bus address 가 맞는 표현이다. 왜냐면, DMA 를 통해서 CPU 와 I/O devices 들이 통신할 때는 DMA buffer 를 통해서 이루어지기 때문이다(그런데, 참고로 bus 마다 address translation 방식이 다르다).

     

     

    " PCI device enumeration(initialization) 과정에서, kernel 은 모든 I/O devices 들과 해당 디바이스들에 대응하는 MMIO address spaces 들을 인지하게 된다(MMIO 는 physical address space 에 속한다). 예를 들어, PCI 같은 경우, PCI Host Bridge 는 processor 와 PCI devices 간에 data access mapping 을 지원한다. 아래 그림을 보면 모든 PCI devices 들을 `Host address domain(위쪽 박스)` 에 mapping 시킴으로써, processor 는 programmed I/O 를 통해서 PCI devices 에 access 할 수 있다. `PCI address doamin` 관점에서 보면, PCI devices 들이 host memory 에 acess 하기 위해서 PCI Host Bridge 는 system memory 를 PCI address domain 에 mapping 한다.

     

     

     

    " 여깃서 잠깐 PCI address 에 대해 알아보고 가자. PCI devcies 들 마다 별도의 BAR(base address register) 라고 하는 memory region 이 가지고 있다. 이 BAR address 는 PCI Bus 에서 할당되는 주소이기 때문에, PCI Bus 에서만 유효하다. 그래서, CPU 는 BAR address 를 직접적으로 access 가 불가능하다. 예를 들어, 32-bit PCI device address 는 아래와 같이 `Bus, Device, Function` 으로 구성되어 있다(이 주소를 해석하는 방법은 이 글을 참고하자). 예를 들어, 32-bit 시스템에서 PCI device address 가 0xCF28_0120 라면, 이 주소는 physical address 로 해석하면 안된다. 왜냐면, 이 주소는 PCI address 이기 때문이다.

     

     

     

    " CPU 가 이 주소에 access 하려면, PCI Host Bridge 가 필요하다. PCI Host Bridge 는 PCI bus address 를 MMIO 에 매핑한다(그 반대로 매핑). 결국, CPU 가 PCI devices 가 mapping 된 MMIO 영역에 read / write 를 할 경우, PCI Host Bridge 가 해당 MMIO address 를 BAR address 로 translate 시킨다. 

     

     

    " MMIO address(CPU physical address space)는 `struct resource` 구조체에 저장되며, user space 에서 kernel 및 device driver 들이 사용하고 있는 physical address space 를 확인하려면, `/proc/iomem` 파일을 읽어보면 된다. device driver 에서 physical address 에 접근하고 싶은 경우가 있다. 예를 들어, 위 그림에서 `device X(bus address X)` 의 register 를 읽고 싶은 것이다. 이 때, 생각해보자. device driver 코드는 누가 실행하는 것일까? CPU 가 실행한다. 즉, device driver 가 bus address X 를 읽는 것은 CPU 가 bus address X 를 읽는 것과 같다. 우리는 CPU 가 직접 bus address X 를 읽는 필요가 없다는 것을 알고 있다. 왜냐면, CPU 가 `physical address J` 를 읽으면, PCI Host Bridge 가 `physical address J` 를 `bus address X` 로 컨버팅 해줄 것을 알고 있다. 그런데, MMU 가 활성화되면, 더 이상 코드 레벨에서 physical address 에는 접근이 불가능하다. 즉, virtual address 만 사용해야 한다는 것이다. 이 때, ioremap() 함수를 통해서 access 하길 원하는 physical address 를 하드 코딩으로 전달하면, 해당 physical address 에 mapping 된 virtual address 를 반환한다. 아래 코드는 physical address 0x100000 - 0x100004 를 virtual address(regs) 에 mapping 시키는 코드다. physical address 0x100000 - 0x100004 에 값을 쓰려면, iowrite32() 함수를 사용할 수 있다. 이 때, 해당 물리 주소에 mapping 된 가상 주소인 regs 를 전달해야 한다. 

     

    // https://oneemptymind.wordpress.com/2018/09/11/ioremap-how-to-acess-the-physical-address-from-linux-kernel-space/
    #define PHYSICAL_ADDRESS 0x100000
    
    void __iomem *regs = ioremap(PHYSICAL_ADDRESS, 4);
    iowrite32(value, regs);

     

     

    " 여태까지 언급된 내용에서는 DMA 가 언급되지 않았다. 여기서 만약, PCI 에서 DMA 를 지원하는 경우, kmalloc() or 이와 유사한 메모리 할당 함수들을 통해서 `DMA buffer` 를 virtual address 할당할 수 있다. 이 때, MMU 는 DMA buffer 의 virtual address 를 physical address 로 mapping 한다. 결국, DMA buffer 의 physical address 는 system memory(RAM) 어딘가에 위치하게 된다. 이제부터 driver 는 DMA buffer 의 virtual address 를 통해서 DMA buffer 에 access 할 수 있게 된다. 그러나, PCI devices 입장에서는 DMA buffer 의 virtual address 를 통해서 DMA buffer 에 access 할 수 없다. 왜냐면, PCI device 의 DMA 는 CPU virtual address 를 사용하지 않기 때문이다.

     

     

    " 일부 시스템에서는, 예를 들어, 구조가 간단한 시스템에서는 DMA 를 통해서 직접적으로 physical address 에 access 할 수 있다. 예를 들어, PCI or USB 와 같이 별도의 bus address 가 있는 devices 들은 physical address 로 컨버팅이 필요하지만, 특별한 주소 메커니즘을 사용하지 않는 bus 에 연결된 devices 들은 직접적으로 physical address 에 access 할 수가 있다. 그러나, 많은 경우에 IOMMU 라는 hardware block 을 통해서 DMA address 를 physical address 로 traslate 하게 된다. 예를 들어, `bus address Y` 를 `physical address K` 로 translate 하는 것이다. 이렇게 DMA 가 hardware-level 에서 어떻게 동작하는지에 대해서 이해하게 되면, `dma_map_single()` 함수의 동작 과정을 이해한 것이라고 볼 수 있다.

     

     

    " driver code 에서 dma_map_single() 함수와 같은 함수를 호출할 경우, dma_map_single() 함수에 virtual address B 를 전달한다. 이 함수 내부에서는 IOMMU 를 통해서 virtual address B 를 bus address Y 에 mapping 한다. 그리고, bus address Y 를 반환한다. 이제부터 driver 는 device 에서 DMA 작업이 필요한 경우, `bus address Y` 수행하면 된다. 그리고, IOMMU 는 bus address Y 를 physical address K 에 maping 한다.

     

     

    " 위 설명을 읽다보면 한 가지 사실을 알 수 있을 것이다. 바로, 리눅스에서는 `dynamic DMA mapping` 을 지원한다. dynamic DMA mapping 이란, DMA buffer virtual address 와 bus address 의 mapping 관계를 DMA 를 사용하고 있을 때만 유지하는 것을 의미한다. 즉, DMA transfer 이 완료되면, 기존에 존재했던 mapping 관계는 파기된다.

     

     

    " DMA Mapping Framework 에서 제공하는 DMA API 는 CPU 아키텍처 및 Bus 타입에 관계없이 모든 하드웨어에서 잘 동작해야 한다. 그리고, driver engineer 라면, bus-specific API`s(pci_map_*() 함수 계열) 보다는 DMA API`s(dma_map_*() 함수 계열) 를 사용해야 한다. driver code 에서 DMA API 를 호출한 후에, 반환받은 DMA address 는 `dma_addr_t` 데이터 타입에 저장된다. 이 데이터 타입에 저장된 DMA address 는 bus 및 CPU 아키텍처에 관계없이 항상 valid 하다는 것을 보장한다.

     

     

     

    2. What memory is DMA`able? [참고1]

    " driver engineer 가 DMA 를 사용할 때, 가장 먼저 고려해야 할 내용이 뭘까? 바로, driver 가 사용할 DMA memory region(DMA buffer) 를 찾아야 한다. system memory 에서 어떤 영역을 얼마만큼 DMA buffer 로 할당해야 할까? 이건 각 DMA controller device specification 을 확인해봐야 한다. 즉, DMA controller 가 사용할 DMA buffer 는 몇 가지 조건이 충족되어야 하는데, 이건 각 제조사의 DMA controller device specification 을 참고해야 한다는 뜻이다. 그러나, 다수의 DMA controller 에서 사용하는 DMA buffer 조건은 대개가 비슷하다. 이 섹션에서는 해당 조건들에 대해 알아본다.

     

     

    " 만약, driver code 에서 DMA buffer 에 virtual address 를 할당할 때, __get_free_page*() 혹은 general memory allocation 계열 함수(kmalloc(), kmem_cache_alloc(), etc) 를 사용하면, 반환되는 주소 VA 는 PA 와 linear 한 관계를 형성한다. 즉, 이 주소들은 DMA mapping API 에서 직접적으로 사용 가능한 주소를 의미한다.

     

     

    " DMA buffer 의 virtual address 를 할당할 때, vmalloc() 함수를 사용해도 될까? 결론적으로, 좋지 않다. 왜냐면, vmalloc() 함수를 통해 할당받게 되는 address 는 virtual address space 상에서는 continuous 하지만, physical 적으로는 discontinuous 하다. 그런데, DMA 관련 operation 수행하는데, 하드웨어에서 continuous physical memory 를 요구할 경우, vmalloc() 함수를 통해 할당받은 memory 는 이 요구를 충족하지 못한다. 만약, hardware level 에서 `scatter-gather` 를 지원하면 어떨까? 그래도, vmalloc() 함수를 통해 virtual address 를 할당받는 것은 좋지 못하다. 왜냐면, vmalloc() 함수를 통해 할당받은 virtual address 는 mapping 되는 physical address 와 선형적인 관계(linear)가 아니기 때문이다. 예를 들어, kmalloc() or __get_free_page*() 함수들을 통해 할당되는 virtual address 에 mapping 되는 physical address 들은 서로 fixed offset 을 통해 translate 가 가능하다. 즉, linear 한 관계를 유지하고 있다. 예를 들어, kmalloc() 함수를 통해 3개의 virtual addresses(A, A+4, A+8) 를 할당받고, 이에 대응하는 physical addresses 가 X, X+4, X+8 라고 가정하자. 이 때, fixed offset 이 3 일 경우, A+(-)3 = X, A+4+3 = X+4, A+8+3 = X+7 이 된다.

     

     

    " 그리고, DMA mapping 시에, physical address 를 알고 있어야 한다. 이 때, 만약에 virtual address 와 physical address 의 관계가 linear 하다면, virtual address 에 mapping 된 physical address 를 얻기가 수월하다. 그러나, vmalloc() 함수를 통해 할당받은 virtual address 에 mapping 된 physical address 는 불규칙적이게 메모리에 퍼져있기 때문에, physical address 를 찾기 위해서는 page table 을 탐색해야 하는 번거로움이 따른다.

     

     

    " 전역 변수는 DMA buffer 로 사용될 수 있을까? kernel 컴파일시에, 전역 변수는 data segment or bss 섹션에 저장된다. 그런데, kernel 초기화 과정에서 kenrel image mapping 이 수행되므로, 전역 변수에 할당된 메모리는 continuous 하다. 결과적으로, 전역 변수의 VA 와 PA 는 linear 관계를 갖게되고 단지 fixed offset 을 통해서 VA <-> PA 변환이 가능하다. 즉, 전역 변수로 선언된 변수들은 모두 DMA buffer 로 사용될 수 있다. 그러나, 전역 변수로 DMA buffer 를 선언할 때는 반드시 cacheline 에 align 되어있어야 하며, cache coherence 를 피하기 위해서 CPU 와 DMA controller 사이에 반드시 symchronization mechnism 이 필요하다.

     

     

    " 또 다른 케이스를 보자. 만약, driver 가 module 로 컴파일되면 어떻게 될까? 이 때는 driver code 에서 선언된 전역 변수를 DMA buffer 로 사용할 경우, 문제가 발생한다. 왜냐면, 이 시점에 전역 변수로 선언된 DMA buffer 가 kernel 의 linear mapping area 에 있지 않기 때문이다. 좀 더 디테일하게 설명하자면, module 이 loaded 될 때, module 은 vmalloc() 함수를 통해서 virtual address 를 할당받게 되고 이 때, 만약에 DMA buffer 사이즈가 page frame 한 개 보다 크다면, physical address 가 continuous 하다는 것을 보장할 수 가 없다. 거기다가, VA 와 PA 의 linear 관계도 성립될 수 없다. 결국, driver 가 module 로 빌드될 때 와 built-in 으로 빌드될 때, address 관계의 문제가 있다는 것을 알 수 있다.

     

     

    " 참고로, kmap() 함수를 통해 할당된 address 는 DMA buffer 로 사용할 수 있을까? 안된다. 왜냐면, kmap() 함수가 할당하는 virtual address 가 결과적으로 vmlloc() 함수가 할당하는 virtual address 와 비슷하기 때문이다. 그렇다면, block devices 에서 사용하는 I/O buffers 들이나 network devices 에서 data 를 send / receive 하기 위해 사용하는 buffers 들은 DMA buffer 로 사용할 수 있을까? 가능하다. 왜냐면, block device I/O subsystem 과 network subsystem 은 buffers 를 할당할 때, DMA buffer 로 사용될 것을 고려해서 virtual address 를 할당하기 때문이다.

     

     

     

     

    3. DMA addessing capabilities [참고1 참고1 참고2]

    " 리눅스 커널에는 `ZONE_DMA` 라는 영역이 있다. 이 영역은 커널의 가장 앞단에 0B ~ 16MB(0x00FF_FFFF) 를 차지하고 있다. 이 영역에서 메모리를 할당받기 위해서는 `DMA_ZONE` 이라는 플래그를 사용하면 된다. 그런데, 커널은 왜 ZONE_DMA 라는 영역을 만들었을까? 옛날 x86 8086 시절에는 ISA 버스를 사용했는데, 이 버스의 data bus 의 대역폭이 24-bit 였다. 그리고, 이 당시에 DMA 가 도입되었기 때문에 DMA 도 24-bit 에 맞춰져서 만들어졌다. 결국, ISA 버스를 사용하던 몇몇 디바이스들은 24-bit 를 넘어서는 memory 에 access 가 불가능한 것이다. 32-bit bus address 를 지원하는 PCI 의 등장으로 이 문제가 해결될 것으로 보였으나, 일부 ISA 버스에 맞춰져 제작된 PCI devices 들은 여전히 physical address 0x00FF_FFFF 넘어서는 memory 에는 access 가 불가능했다. 예를 들어, 특정 device 가 physical address 를 24-bits 만 access 할 수 있는지를 체크하기 위해서는 아래와 같은 코드를 작성할 수 있다.

    // https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt
    if (dma_set_mask(dev, DMA_BIT_MASK(24))) {
    	dev_warn(dev, "mydev: 24-bit DMA addressing not available\n");
    	goto ignore_this_device;
    }

     

     

    " 기본적으로 리눅스 커널은 모든 peripheral devices 들이 full 32-bit 를 지원할 수 있다고 가정한다. 만약, 64-bit capable devices 가 있다면, DMA mask 를 증가시켜야 한다. 만약, full 32-bit 를 지원할 수 없는 디바이스가 있다면, 위와 같이 DMA mask 를 32-bit 보다 작게 설정해야 한다.

     

    " 본격적인 DMA transfer 를 수행하기 전에, 반드시 DMA 를 수행할 device 의 addressing capabilities 를 kernel 에게 알려야 한다(위에서 얘기했다 싶이, 커널은 default 로 모든 devices 들이 full 32-bit DMA 를 수행할 수 있다고 가정하기 때문이다). 아래의 함수를 통해서 DMA addressing limitation 을 설정할 수 있다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    int dma_set_mask_and_coherent(struct device *dev, u64 mask);

     

     

    " 그런데, 위 함수는 streaming & cohrent 를 모두 설정한다. 만약, 별도로 설정하고 싶다면 아래의 함수를 사용하면 된다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    
    // For streaming mapping
    int dma_set_mask(struct device *dev, u64 mask);
    
    // For consistent mapping
    int dma_set_coherent_mask(struct device *dev, u64 mask);

     

     

    " 위에서 `dev` 는 DMA 를 사용할 device 를 의미한다. 그리고, mask 는 DMA 를 사용할 device 의 DMA addressing limitation 을 전달하면 된다. 대개 struct device 는 다른 `bus-specific device struct` 에서 선언되는 경우가 많다. 예를 들어, PCI 같은 경우 struct pci_dev 구조체안에 struct device 가 선언되어 있는 것을 확인할 수 있다. 만약, PCI device 의 addressing limitation 을 설정하기 위해서 dma_set_mask() 함수에 `&pdev->dev` 를 전달할 수 있다.

    // include/linux/pci.h - v6.5
    /* The pci_dev structure describes PCI devices */
    struct pci_dev {
    	....
    	struct device	dev;
    	....
    };

     

     

    " 만약, dma_set_mask 계열 함수의 반환값은 2 가지 타입으로 구분할 수 있다.

    1. zero - 실제 하드웨어적으로 DMA controller 및 bus 에서 device 가 제시하는 addressing limitation(mask) 를 지원한다는 뜻. 

    2. non-zero - 실제 하드웨어적으로 DMA controller 및 bus 에서 device 가 제시하는 addressing limitation(mask) 를 지원한다는 뜻. 만약, 이걸 무시하고 강제로 DMA transfer 를 사용할 경우, fatal issue 가 발생할 확률이 높다.

     

     

    " DMA 를 사용하는 driver engineers 들은 반드시 dma_set_mask 계열 함수의 반환값을 확인해야 한다. non-zero 라면, 2 가지 선택권이 있다.

    1. DMA transfer 를 사용하지 마라. 대신에, 기존에 사용하던 I/O tranfer 를 이용해서 data transfer 를 수행해라. 예를 들어, MMIO or serial interface 등.

    2. 이 device 를 아예 무시한다. 즉, device initialization 도 하지마라. 아예 없는 디바이스 처럼 생각하는 것이다.

     

     

    " standard 64-bit addressing mode(streaming & coherent 모두 64-bit 를 지원) 를 지원하는 device 에 addressing limitation 을 설정하려면, 아래와 같이 작성해볼 수 있다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))) {
            dev_warn(dev, "mydev: No suitable DMA available\n");
            goto ignore_this_device;
    }

     

     

    " coherent mapping address 와 streaming mapping address 가 다를 수 있다. 예를 들어, coherent mapping 은 32-bit 만 지원학, streaming mapping 은 full 64-bit 를 지원할 경우 다음과 같이 작성하면 된다. coherent mask 는 항상 streaming mask 보다 작거나 같도록 설정할 수 있다. 

    // https://docs.kernel.org/core-api/dma-api-howto.html
    if (dma_set_mask(dev, DMA_BIT_MASK(64))) {
            dev_warn(dev, "mydev: No suitable DMA available\n");
            goto ignore_this_device;
    }

     

     

    " 마지막으로, low 24-bit address 만 지원하는 device 같은 경우는 아래와 같이 작성할 수 있다. dma_set_mask 계열 함수 호출이 성공할 경우, kernel 은 이후에 DMA mapping 을 생성할 때, 연관되어 있는 mask 를 사용해서 validation 을 체크한다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    if (dma_set_mask(dev, DMA_BIT_MASK(24))) {
            dev_warn(dev, "mydev: 24-bit DMA addressing not available\n");
            goto ignore_this_device;
    }

     

     

    " 특별한 케이스를 하나만 더 보도록 하자. 만약, DMA 를 사용하는 device 가 MFD(Multiple Function Device) 라면(예를 들어, sound card, PMIC, audio codec 등등), 다양한 기능들을 가지고 있을 것이다. 이 때, 각 기능들마다 개별적인 DMA addressing limitations 들이 가질 수 있다. 여기서 driver engineer 라면, 실제 하드웨어적으로 지원되는 DMA addresses 들과 이에 대응하는 functions 만 사용하고 싶을 것이다. 그럴 때는 아래와 같이 코드를 작성해볼 수 있다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    #define PLAYBACK_ADDRESS_BITS   DMA_BIT_MASK(32)
    #define RECORD_ADDRESS_BITS     DMA_BIT_MASK(24)
    
    struct my_sound_card *card;
    struct device *dev;
    
    ...
    if (!dma_set_mask(dev, PLAYBACK_ADDRESS_BITS)) {
            card->playback_enabled = 1;
    } else {
            card->playback_enabled = 0;
            dev_warn(dev, "%s: Playback disabled due to DMA limitations\n",
                   card->name);
    }
    if (!dma_set_mask(dev, RECORD_ADDRESS_BITS)) {
            card->record_enabled = 1;
    } else {
            card->record_enabled = 0;
            dev_warn(dev, "%s: Record disabled due to DMA limitations\n",
                   card->name);
    }

     

     

    " 위 예시로 sound card 가 사용되었다. 위 예시는 bus compatible 에서 발생하는 이슈 때문이다. ISA bus 기반으로 만들어진 디바이스들은 대개 24-bit addressing mode 를 지원한다. 그러나, PCI bus 가 등장하면서 32-bit addressing mode 를 지원하는 devices 들이 등장했다. 그런데, 이미 산업에 ISA bus 기반의 devices 들이 점령되어 있었기 때문에, legacy PCI devices 들은 ISA 를 지원하는 형태로 개발되었다. 이런 종류의 PCI devices 들은 ISA 의 16MB DMA addressing limitation 을 계속 유지하는 것처럼 보이기 때문에 이와 같은 검사가 필요하다.

     

     

    4. Types of DMA mappings [참고1 참고2 참고3]

    " DMA mapping type 은 크게 2가지로 나뉜다.

    1. Consistent DMA mapping(non-cacheable)
    2. Streaming DMA mapping

     

     

     

    1. Consistent DMA mapping

    " consistent DMA mapping 은 다음과 같은 특징을 가지고 있다.

    - DMA buffer 를 일회성이 아닌, 영구적으로 mapping 하는 방식을 의미한다. 그래서, driver 가 초기화되는 시점(driver->probe) 에 consistent DMA mapping 방식을 이용해서 만든 DMA buffer 는 중간에 unmap 되지 않는다. unmap 은 driver shutdown 과정에서 일어난다.

     

     

    " consistent DMA mapping 방식을 지원하기 위해서는 하드웨어적으로 반드시 2 가지를 보장해줘야 한다.

    1. CPU 와 deivce 는 동시에 DMA buffer 에 병렬적으로 accessible 할 수 있음을 보장해야 한다.
    2. CPU 와 deivce 는 명시적으로 software flushing 을 하지 않아도 서로가 업데이트한 내용을 확인할 수 있음을 보장해야 한다.

     

    " 위와 같은 특성 때문에, consistent 를 `synchronous` or `coherent` 라고 부른다. 그리고, 현재 default current coherent memory 는 호환성을 위해서 low 32-bit 만 반환하고(사용되고) 있다. 즉, consistent mask 의 lower 32-bit 는 0xFFFFFFFF 로 설정되어 있다는 뜻이다. 미래에는 완전히 64-bit 로 넘어갈 수 있기 때문에, consistent mask 은 driver 단에서 변경이 가능하도록 설계되어 있다. consistent DMA mapping 를 사용하는 일반적인 경우는 다음과 같다.

     

    1. Network card DMA ring descriptors.
    2. SCSI adapter mailbox command data structures.
    3. Device firmware microcode executed out of main memory.

     

     

    " 위의 케이스들에서 볼 수 있는 공통점은 CPU 에 의해서 메모리의 내용이 updated 될 경우, peripheral devices 들이 해당 변화를 곧 바로 인지한다는 것이다. 물론, 반대도 마찬가지다(즉, peripheral devices 들에 의해서 수정된 메모리의 내용이 CPU 에게 곧 바로 인지됨). 이 말이 무슨 뜻일까? consistent DMA mapping 는 결국 non-cacheable 이라는 뜻이다. 즉, consistent DMA mapping 방식으로 할당된 DMA buffer 는 cache 에 저장하지 않기 때문에, CPU 가 직접 memory 에 access 함으로 CPU 와 devices 들 모두가 즉각적으로 updated 된 내용을 볼 수 있게된다.

     

     

    " 그런데, 주의할 점이 있다. 지금까지 마치 consistent DMA mapping 은 coherent 문제가 없다는 것처럼, 즉, 동기화 문제가 없다는 것처럼 얘기했지만 실제로는 그렇지 않다. consistent DMA mapping 을 사용하더라도, CPU 는 performance 를 향상시키기 위 해 instructions 들에 순서를 re-order 할 수 있기 때문이다. DMA buffer 를 통해서 디바이스와 CPU 가 특별한 시퀀스에 따라 데이터를 저장하고 읽어야 한다면, 반드시 CPU 측에서 memory barrier 를 사용할 필요가 있다. 예를 들어, consistent DMA mapping 방식으로 할당된 DMA buffer 에 2개의 변수 word0 과 word1 이 있다고 치자. 이 때, 프로토콜 규약에 따라 device 측에서 반드시 word1 보다 word0 이 먼저 updated 되야 한다고 치자. 이럴 경우, code 를 다음과 같이 작성할 필요가 있다.

    // https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt
    ....
    	desc->word0 = address;
    	wmb();
    	desc->word1 = DESC_VALID;

     

    " 모든 plaform 에서 올바른 동작 순서를 보장하기 위해서 반드시 위와 같이 작성해야 한다.

     

     

     

    2. streaming DMA mapping 

    " streaming DMA mapping 은 일회성 mapping 을 의미한다. 그렇다면, mapping 은 언제 이루어질까? DMA transmission 이 발생할 때마다, mapping 이 이루어진다. 그렇다면, unmap 은? DMA transmission 이 완료되면, unmap 이 즉각적으로 수행된다(dma_sync_*() 함수를 호출하지 않았다면). 그렇다면, 여기서 streaming 은 무슨뜻일까? asynchronous 혹은 coherent memory 가 아님을 의미한다.

     

    " streaming DMA mapping 을 사용하는 시나리오는 다음과 같다.

    1. Networking buffers transmitted/received by a device.
    2. Filesystem buffers written/read by a SCSI device.

     

     

    " streaming DMA mapping 인터페이스들은 hardware 의 performance 를 최적화 시키기 위해서 고안된 기법이다. consistent DMA mapping 에서 봤다시피, non-cacheable 이기 때문에 peformance 가 좋을 수 가 없다. 그렇다면, cache 를 enable 하면서 DMA controller 의 performance 최적화를 위해서 streaming DMA mapping 인터페이스를 사용할 경우, 반드시 streaming DMA mapping 인터페이스가 내부적으로 어떻게 구현되어 있는지를 알고 사용해야 한다. 이 말은 프로젝트 도입 시, streaming DMA mapping 인터페이스 관련된 업무를 맡았다면, 해당 디바이스(예를 들어, NIC or SCSI, etc) 가 어떻게 DMA 를 사용하는지 정확히 알고있어야 하며, DMA controller 에 대한 device specification 도 명확하게 인지하고 있어야 한다.

     

     

     

    " 많은 리눅스 커널 개발자들은 consistent DMA mapping 방식보다 streaming DMA mapping 방식을 더 선호한다. 그 이유는 2 가지가 있다.

    1. mapping reigsters 를 지원하는 시스템에서 lifetime 이 긴 consistent DMA mapping 을 사용할 경우, 사용되지 않을 때도 mapping register 하나를 차지하기 때문에, 공간을 낭비하게 된다[참고1]. 참고로, mapping register 는 device address space <->  physical address space 를 mapping 관계를 저장한 registers 들을 의미한다.


    https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/map-registers

    2. 많은 시스템들에서 streaming DMA mapping 방식에 optimizied 되어있다. cache 사용, 방향 설정, mapping register 최적화 등등의 이유가 있다.

     

     

    - Using consistent DMA mapping ?

    " page size or 이와 유사한 크기의 consistent DMA memory 를 할당하고, 이에 mapping 시키기  위해서는 아래와 같은 코드를 작성해야 한다.

    // https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt
    dma_addr_t dma_handle;
    cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);

     

     

    " DMA controller 에게 DMA operation 을 요청하는 주체는 결국 device 다. 이 dev 는 struct device 를 나타낸다. 즉, dma_alloc_coherent() 함수를 통해 DMA buffer 를 할당하는 주체가 `dev` 를 의미한다. `size` 파라미터는 할당 받을 DMA buffer 의 size 를 의미한다(단위는 byte). 그리고, dma_alloc_coherent() 함수는 interrupt context 에서도 호출할 수 있다. 대신 이 때, gfp 파라미터에 GFP_ATOMIC 플래그를 전달해야 한다.

     

     

    " 주의할 점이 있다. dma_alloc_coherent() 함수를 통해 할당받은 memory 의 starting address 와 size 는 page 에 정렬되어 있다는 것이다(__get_free_pages() 함수와 동일). 만약에, driver 에서 요청한 DMA buffer size 가 한 개의 page size 와 같거나 작다면, dma_pool() 함수를 사용하는 것이 더 선호된다.

     

     

    " 기본적으로, consistent DMA memory API`s 들은 32-bit DMA address 를 반환한다. device 가 `DMA mask` 를 upper 32-bit 를 addressable 할 수 있다고 설정하더라도, consistent DMA memory allocation 은 `dma_set_coherent_mask()` 함수를 통해 명시적으로 DMA mask 를 변경했을 경우에만, upper 32-bit DMA addresses 를 반환한다. 즉, 변수를 직접 세팅하지말고, dma_set_coherent_mask() 함수를 통해서 DMA mask 를 세팅하라는 뜻이다.

     

     

    " dma_alloc_coherent() 함수는 2개의 virtual address 를 반환한다.

    1. CPU 측에서 DMA buffer 에 access 할 수 있는 virtual address - cpu_addr
    2. device 측에서 DMA buffer 에 access 할 수 있는 bus address - dma_handle

     

     

    " 비록 요청한 DMA buffer size 가 PAGE_SIZE 보다 작더라도, dma_alloc_coherent() 함수에서 반환되는 cpu virtual address 와 DMA bus address 는 가장 작은 PAGE_SIZE order(여기서 order 는 buddy allocation 에서 사용되는 order 를 의미한다)  에 aligned 된다(요청한 사이즈와 같거나 큰 사이즈를  의미). 이와 같은 특징으로 인해, DMA buffer 는 다음과 같은 특징들을 보장된다. 

     - 예를 들어, page size 가 64KB 라고 가정하면, 비록 driver 가 64KB 와 같거나 작은 DMA buffer 를 할당받더라도 DMA buffer 는 smallest PAGE SIZE 인 64KB 를 할당받게 된다. 즉, DMA buffer 는 페이지가 PAGE SIZE(64KB) 보다 클 수 없다.

     

     

    " DMA buffer 를 unmap or free 하기 위해서 아래의 함수를 사용하면 된다. `dev` 와 `size` 는 dma_alloc_coherent() 함수를 사용할 때와 동일한 값을 넣으면 된다. 그리고, `cpu_addr` 와 `dma_handle` 또한 dma_alloc_coherent() 함수에서 반환받은 값들을 그대로 넣어주면 된다.

    // https://docs.kernel.org/core-api/dma-api-howto.html 
    dma_free_coherent(dev, size, cpu_addr, dma_handle);

     

     

    - dma pool

    " 한 개의 large DMA buffer 보다는 다수의 small DMA buffer 가 필요한 경우라면, 2 가지 방법으로 DMA buffer 를 할당받을 수 있다.

    1. dma_alloc_coherent() 함수를 통해 반환받은 DMA buffer 를 여러 개의 pages 들을 쪼개서 사용한다.
    2. dma_pool API 를 사용한다.

     

     

    " dma_pool 은 keme_cache 와 유사하다. 그러나, dma_pool 은 __get_free_pages() 함수가 아닌, dma_alloc_coherent() 함수를 사용한다. dma_pool 을 생성하는 방법은 다음과 같다. 이 함수에서 중요한 파라미터는 `align` 과 `boundary` 다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    struct dma_pool *pool;
    
    pool = dma_pool_create(name, dev, size, align, boundary);

     

     

    " align 은 하드웨어 디펜던시가 있다. 예를 들어, 대부분의 DMA controller 는 1 bytes 단위의 data transfer 를 하지 않는다. 2 의 제곱단위로 data transfer 가 진행되고, DMA 와 cache 사이에 coherent 문제 때문에 대개 cacheline alignment 를 하는 경우가 많은 듯 하다. dma_pool_create() 함수에 `align` 파라미터도 마찬가지다. bytes 단위로 전달 가능하지만, 2 의 거듭제곱에 정렬된 값을 전달해야 한다.

     

    " boundary 는 size limit 가 같다고 보면된다. 예를 들어, 4096 을 전달하면, 4KB 를 넘지말라는 것을 의미한다(memory boundary 에 대한 개념은 이 글을 참고하자). 0 을 전달하면, boundary 에 대한 제약이 없음을 의미한다.

     

    " DMA pool 을 이용해서 memory 를 할당받는 방법은 다음과 같다. flags 는 DMA buffer 에 access 중에, blocking 이 허용되면(즉, context swiching 이 되도 상관없다면), GFP_KERNEL 을 설정하고, 아니라면 GFP_ATOMIC(반드시 방해없이 한 번에 처리해야 한다) 을 설정한다. dma_pool_alloc() 함수도 dma_alloc_coherent() 함수와 마찬가지로 2 개의 addresses(cpu_addr(virtual address) & dma_handle(bus address)) 를 반환한다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    cpu_addr = dma_pool_alloc(pool, flags, &dma_handle);

     

     

    " DMA Pool 을 통해 할당받은 DMA buffer 를 해제하려면, 아래 함수를 호출하면 된다. 아래 인자들은 dma_pool_alloc() 함수에서 반환받은 값들을(pool, cpu_addr, dma_handle) 그대로 전달하면 된다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    dma_pool_free(pool, cpu_addr, dma_handle);

     

     

    "  DMA Pool 를 release 하고 싶다면, 아래 함수를 호출하면 된다. 주의할 점은 dma_pool_destroy() 함수를 호출하기 전에 먼저 dma_pool_free() 함수를 통해 DMA Pool 로 부터 할당받은 DMA buffer 를 모두 free 해야 한다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    dma_pool_destroy(pool);

     

     

    - DMA direction

    " DMA direction 이란 DMA transfer 의 방향을 의미한다. 리눅스 커널에서 실제로 방향에 의미를 갖는 매크로는 2 개 뿐이다(DMA_TO_DEVICE, DMA_FROM_DEVICE).

    1. DMA_BIDIRECTIONAL - DMA transfer 방향을 정말 전혀 모를 때, 이 플래그를 사용한다.

    2. DMA_TO_DEVICE - from main memory to the device 

    3. DMA_FROM_DEVICE - from the device to main memory

    4. DMA_NONE - for debugging

     

     

    " 최적화를 위해서는 반드시 실제 DMA trasnfer 과 일치하는 구체적인 방향을 명시해야 한다. 만약, 방향을 모르겠다면, DMA_BIDIRECTIONAL 를 명시한다. DMA_BIDIRECTIONAL 가 설정되면, DMA operation 이 어느 방향으로든 수행될 수 있음을 의미한다. 그러나, 이 방법은 performance 가 좋지 못한다는 단점이 있다.

     

    " streaming mapping 에서만 `DMA direction` 을 명시한다. consistency mapping 에서는 암묵적으로 DMA_BIDIRECTIONAL 이 사용된다. streaming mapping 관련해서 예시를 하나 들어보자. network card driver 에서 streaming DMA mapping 은 굉장히 흔하다.

    1. 패킷을 전송할 때 - DMA_TO_DEVICE
    2. 패킷을 수신할 때 - DMA_FROM_DEVICE

     

     

    " data 를 전송(send)해야 할 경우, CPU 에서 실제 유무선으로 데이터를 전송할 수 있는 device(NIC) 로 데이티를 전송해야 한다. 이 때, dma_map_single() 함수를 호출하면서 DMA_TO_DEVICE 를 전달하면 된다. 만약, 데이터를 받는 것이라면, NIC 에서 받아서 CPU 로 전송된다. 그렇다면, dma_map_single() 함수를 호출하면서 DMA_FROM_DEVICE 를 전달하면 된다.

     

     

     

     

    - Using streaming DMA mapping [참고1]

    " streaming DMA mapping 은 2 가지 map / unmap 이 있다.

    1. single memory region 에 대한 map / unmap
    2. scatterlist 에 대한 map / unmap

     

     

    1. a single region

    " single memory region 을 DMA buffer 로 사용하는 방법은 다음과 같다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    struct device *dev = &my_dev->dev;
    dma_addr_t dma_handle;
    void *addr = buffer->ptr;
    size_t size = buffer->len;
    
    dma_handle = dma_map_single(dev, addr, size, direction);
    if (dma_mapping_error(dev, dma_handle)) {
            /*
             * reduce current DMA mapping usage,
             * delay and try again later or
             * reset driver.
             */
            goto map_error_handling;
    }

     

     

    " 위에서 생성된 DMA buffer 를 unmap 하기 위해서는 아래의 함수를 사용하면 된다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    dma_unmap_single(dev, dma_handle, size, direction);

     

     

    " dma_map_single() 함수를 호출한 후에 error 가 반환될 수 있다. 이 때, erorr handling 위해서 dma_mapping_error() 함수를 호출해야 한다. 커널에서 이렇게 에러 처리를 하는 subsystem 사실 드물다. DMA 에서는 왜 이런 방식으로 에러 처리를 하는 것일까? 이렇게 함으로써, 아키텍처에 따른 세부 구현 사항들에 의존적이지 않은 코드를 작성할 수 있다(커널 legacy 버전에서는dma_mapping_error() 함수는 내부적으로 dma_map_ops.mapping_error() 함수를 호출했지만, 최신 버전에서는 dma_map_ops 구조체에서 mapping_error 필드는 사라졌다. 대신, dma_mapping_error() 함수는 내부적으로 debug_dma_mapping_error() 함수를 호출한다. 이 말은 DMA error handling 방법이 이제는 아키텍처 의존적인 코드가 아닌, 커널에서만 관리되는 코드로 보인다).

     

     

    " dma_map_single() 함수에서 반환하는 return address 를 검사하지 않으면, fatal issue 를 야기할 수 있다. 아래 예시들은 잘못된 방식으로 return address 를 체크하는 코드다. 아래 코드들이 잘못된 검사인 이유는 DMA 내부 구현에 의존해서 에러 코드를 작성했기 때문이다. DMA 는 일반적으로 각 아키텍처에 따라 구현방식이 다르다. 그러므로, DMA 내부 구현을 추상화 시켜주는 dma_mapping_error() 함수를 통해 error handling 을 하는 것이 옳다. 첫 번째 케이스는 다음과 같다.

    // https://android.googlesource.com/kernel/msm/+/android-msm-bullhead-3.10-marshmallow-dr/Documentation/DMA-API-HOWTO.txt
    dma_addr_t dma_handle;
    
    dma_handle = dma_map_single(dev, addr, size, direction);
    if ((dma_handle & 0xffff != 0) || (dma_handle >= 0x1000000)) {
        goto map_error;
    }

     

     

    " 두 번째 케이스는 다음과 같다.

    // https://android.googlesource.com/kernel/msm/+/android-msm-bullhead-3.10-marshmallow-dr/Documentation/DMA-API-HOWTO.txt
    
    dma_addr_t dma_handle;
    dma_handle = dma_map_single(dev, addr, size, direction);
    if (dma_handle == DMA_ERROR_CODE) {
    	goto map_error;
    }

     

     

    " DMA 가 작업을 마치면, 반드시 dma_unmap_single() 함수를 호출해야 한다. 예를 들어, DMA transfer 가 끝났다는 것을 알리는 interrupt 가 발생했을 때, dma_unmap_single() 함수를 호출해서 리눅스 커널에게도 이 사실을 알려야 한다. 참고로, streaming DMA mapping 은 interrupt context 에서 호출이 가능하다.

     

     

    " single mapping(`dma_[map|unmap]_siggle()`) 은 단점이 하나있다. 바로, CPU pointer(virtual address) 를 사용하기 때문에, HIGHMEM(> 4GB) 를 참조할 수 가 없다. 위에서 예시에서는 `void *addr` 는 32-bit 일 경우, 4GB 범위안에서만 DMA buffer 를 할당할 수 있다. 이 문제를 해결하기 위해 CPU pointer(virtual address) 가 아닌 page 와 offset 쌍을 사용하는 인터페이스를 사용할 수 있다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    struct device *dev = &my_dev->dev;
    dma_addr_t dma_handle;
    struct page *page = buffer->page;
    unsigned long offset = buffer->offset;
    size_t size = buffer->len;
    
    dma_handle = dma_map_page(dev, page, offset, size, direction);
    if (dma_mapping_error(dev, dma_handle)) {
            /*
             * reduce current DMA mapping usage,
             * delay and try again later or
             * reset driver.
             */
            goto map_error_handling;
    }
    
    ...
    
    dma_unmap_page(dev, dma_handle, size, direction);

     

     

    " dma_map_page() 함수는 인자로 struct page 를 받고, 전달받은 하나의 page 에 DMA buffer 를 mapping 한다. 이 때, offset 은 한 페이지 내에서의 DMA buffer start offset 을 의미하고(단위는 byte), size 는 말 그대로 얼만큼 할당할 것인지를 나타낸다. 그러나, 한 개의 page 에서 일부를 mapping 하는 방식을 사용할 때(dma_map_page() 함수를 사용할 때), 한 개의 cache line 일부 만큼만 할당받을 경우(예를 들어, cache line 이 64B 라고 가정할 때, 32B 만 할당받은 경우), cache coherency 및 false sharing 문제를 야기할 수 있다. 

     

    " dma_map_single() 함수 때와 마찬가지로 dma_map_page() 함수를 호출한 후에 DMA error 처리를 위해서, dma_mapping_error() 함수를 호출해야 한다. 만약, dma_map_page() 함수에서 에러가 발생하지 않았다면, DMA transfer 가 시작된다. DMA transfer 가 완료되면, DMA buffer 를 모두 사용했음으로 release 해도 된다는 것을 알리기 위해 반드시 dma_unmap_page() 함수를 호출한다(dma_unmap_single() 함수 때와 마찬가지로 DMA transfer 가 완료되면, interrupt 가 발생하고, 대개 dma_unmap_*() 함수들은 interrupt handler 에서 호출된다).

     

     

     

    2. scatterlists [참고1]

    " scatterlists 를 사용하면, 여러 개의 흩어져있는 non-continuous regions 들을 하나의 a continuouse region 에 mapping 할 수 있다. 아래의 코드를 보자.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    int i, count = dma_map_sg(dev, sglist, nents, direction);
    struct scatterlist *sg;
    
    for_each_sg(sglist, sg, count, i) {
            hw_address[i] = sg_dma_address(sg);
            hw_len[i] = sg_dma_len(sg);
    }

     

    " 위에서 `nents` 는 sglist 의 elements 개수를 의미한다. 그리고, dma_map_sg() 함수의 내부 구현 메커니즘은 아키텍처마다 다르다. dma_map_sg() 함수는 scatterlist array 에 있는 multiple discontinuous memory blocks 들을 a large single virtual address 로 mapping 한다. 만약, DMA mapping 이 PAGE_SIZE 단위로 진행될 경우, scatterlist array 안에 물리적으로 맞닿아 있는 entiries 들은 하나로 병합될 수 있다.  예를 들어, 아래에서 형광색 점선 박스에 포함된 2 개의 physical addresses 들이 PAGE SIZE 에 정렬되어 있고, 서로 맞닿아 있다면, 하나의 scatterlist 로 판단한다는 것이다. 이 말은 dma_map_sg() 함수 호출 시에 전달한 nents 와 실제 dma_map_sg() 함수의 반환값이 서로 다를 수 있음을 나타낸다(반환값은 nents 와 같거나 더 작을 수 있다).

     

    " 그리고, legacy system 에서는 IOMMU 가 없거나, `scatter / gather I/O` 를 지원하지 않는 DMA controller 가 있는 경우가 많다. dma_map_sg() 함수는 software 적으로 `scatter / gather I/O` 기능을 지원하기 때문에 `scatter / gather I/O` 를 지원하지 않는 DMA controller 들을 사용하더라도 상관없이 사용이 가능하다. 참고로, dma_map_sg() 함수가 0 을 반환하면 mapping 이 실패했음을 의미한다.

     

     

     

    " dma_map_sg() 함수가 성공적으로 호출되면, for_each_sg() 매크로문은 mapping 이 성공한 sglist 들을 대상으로 탐색을 진행한다. 이 때, count 는 mapping 이 성공한 scatterlist 가 몇 개 인지를 알려준다(`nents` 보다 작거나 같다).  sg 에는 각 sglist 의 scatterlist 가 할당된다. 이 때, sg_dma_address() / sg_dma_len() 함수를 통해서 mapping 된 DMA address 와 length 를 알 수 있다. sctterlist 하나를 unmap 하려면, 다음의 함수를 호출하면 된다. 주의할 점은 반드시 DMA operation 이 모두 완료된 다음에 호출해야 한다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    dma_unmap_sg(dev, sglist, nents, direction);

     

     

    " 한 가지 더 주의할 점이 있다. dma_unmap_sg() 함수에 전달하는 `nents` 는 dma_map_sg() 함수에 전달했던 `nents` 와 동일한 값을 전달해야 한다. 절대로 dma_map_sg() 함수의 반환값을 전달하면 안된다.

     

     

     

    3. sync & ownership [참고1 참고2]

    " streaming mapping 은 한 번 혹은 짧게 몇 번만 사용될 DMA buffer 를 만드는 방식을 의미한다. CPU 아키텍처에 따라서, streaming mapping 과 관련된 주제들은 bounce buffers, IOMMU, cache flush 등이 있을 수 있다. 그 만큼 어렵고 실제 개발시에도 퍼포먼스 최적화하기 눈 여겨볼 만한 파트다. 그러나, 안전성 측면에서 보면 streaming mapping 에서 가장 중요한 내용은 역시나 DMA buffer 에 대한 `ownership` 일 것이다. streaming mapping 을 통해 DMA buffer 가 하나 생성되면, 해당 buffer 는 device 가 ownership 을 갖는다. 즉, processor 는 해당 buffer 에 건드릴 수 없다. 만약, device driver(CPU) 가 이 rule 을 무시하면, data corruption 과 같은 빡쌘 에러를 마주하게 될 확률이 높다. 그런데, 특별한 케이스에서는 processor 가 buffer 를 access 할 수 있도록 해줘야 하는 경우가 있다. 그렇다면, 특별한 케이스가 뭘까? DMA transfer 가 시작될 때, dma_map_*() 함수가 호출된 시점을 기준으로 dma_unmap_*() 함수가 호출되기전에 CPU 의 access 가 필요하다면, dma_sync_*() 함수를 사용하는 것이다(일반적으로 CPU 는 dma_unmap_*() 함수가 호출되기 전에는 DMA buffer 에 access 하면 안된다). 이 때는 DMA buffer 의 ownership 을 CPU 에게 줘야한다.

     

    " `dma_sync_*_for_cpu()` 함수들은 DMA buffer 의 ownership 을 CPU 쪽으로 가져온다. dma_sync_*_for_cpu() 함수들이 호출된 후에 driver code(driver code 는 CPU 가 실행한다) 에서 DMA buffer 에 read & modify 가 가능해진다. 그러나, 이제 device 쪽에서는 DMA buffer 에 access 할 수 없다.

    // include/linux/dma-mapping.h - v6.5
    void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr, size_t size,
    		enum dma_data_direction dir);
    void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg,
    		    int nelems, enum dma_data_direction dir);

     

     

    " `dma_sync_*_for_device()` 함수들은 다시 DMA buffer 의 ownership 을 device 쪽으로 가져온다.

    // include/linux/dma-mapping.h - v6.5
    void dma_sync_single_for_device(struct device *dev, dma_addr_t addr,
    		size_t size, enum dma_data_direction dir);
    void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg,
    		       int nelems, enum dma_data_direction dir);

     

     

    " dma_sync_*() 함수를 사용하는 예시 코드를 보자.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    my_card_setup_receive_buffer(struct my_card *cp, char *buffer, int len)
    {
            dma_addr_t mapping;
    
            mapping = dma_map_single(cp->dev, buffer, len, DMA_FROM_DEVICE);
            if (dma_mapping_error(cp->dev, mapping)) {
                    /*
                     * reduce current DMA mapping usage,
                     * delay and try again later or
                     * reset driver.
                     */
                    goto map_error_handling;
            }
    
            cp->rx_buf = buffer;
            cp->rx_len = len;
            cp->rx_dma = mapping;
    
            give_rx_buf_to_card(cp);
    }
    
    ...
    
    my_card_interrupt_handler(int irq, void *devid, struct pt_regs *regs)
    {
            struct my_card *cp = devid;
    
            ...
            if (read_card_status(cp) == RX_BUF_TRANSFERRED) {
                    struct my_card_header *hp;
    
                    /* Examine the header to see if we wish
                     * to accept the data.  But synchronize
                     * the DMA transfer with the CPU first
                     * so that we see updated contents.
                     */
                    dma_sync_single_for_cpu(&cp->dev, cp->rx_dma,
                                            cp->rx_len,
                                            DMA_FROM_DEVICE);
    
                    /* Now it is safe to examine the buffer. */
                    hp = (struct my_card_header *) cp->rx_buf;
                    if (header_is_ok(hp)) {
                            dma_unmap_single(&cp->dev, cp->rx_dma, cp->rx_len,
                                             DMA_FROM_DEVICE);
                            pass_to_upper_layers(cp->rx_buf);
                            make_and_setup_new_rx_buf(cp);
                    } else {
                            /* CPU should not write to
                             * DMA_FROM_DEVICE-mapped area,
                             * so dma_sync_single_for_device() is
                             * not needed here. It would be required
                             * for DMA_BIDIRECTIONAL mapping if
                             * the memory was modified.
                             */
                            give_rx_buf_to_card(cp);
                    }
            }
    }

     

     

    " my_card_setup_receive_buffer() 함수를 호출하면, device -> memory 로 DMA transfer 가 동작한다. 이 때, 당연히 CPU 와는 비동기적으로 동작하기 때문에, CPU 는 DMA controller 에게 request 만 날리고 이후에 코드를 실행한다. 이제 DMA transfer 가 완료되고, interrupt 가 발생했다. 이 때, DMA interrupt 의 인터럽트 핸들러는 my_car_interrupt_handler 다.

     

    " 위 코드를 작성한 driver engineer 는 interrupt 가 발생한 시점에, 즉, my_car_interrupt_handler 가 호출되는 시점에 DMA buffer 를 확인하고 싶다. 그런데, 문제가 있다. DMA buffer 에서 먼저 데이터를 확인한 후에 데이터가 유효하다면, DMA buffer 를 해제하는 시나리오가 필요하다. 즉, driver code(CPU) 에서 DMA buffer 에 access 해야 한다. 이 때, `dma_sync_single_for_cpu()` 함수를 호출해서 CPU 가 DMA buffer 에 access 할 수 있도록 하고, DMA buffer 데이터를 읽는다. 그리고, 데이터가 유효하다면 dma_unmap_single() 함수를 호출해서 DMA buffer 를 해제한다.

     

    " 그런데, 왜 device 쪽으로 다시 돌려줄 필요는 없을까? `DMA_FROM_DEVICE` 플래그를 통해서 owneship 을 얻어낸 DMA buffer 는 CPU 입장에서 read 만 가능하다. 그래서, dma_sync_single_for_device() 함수를 호출할 필요가 없다. 만약, CPU 가 메모리에 read & modify 를 수행해야 한다면, `DMA_BIDIRECTIONAL` 플래그를 설정해야 한다.

     

    " 그런데, CPU 가 read 만 할 경우, dma_sync_single_for_device() 함수를 호출할 필요가 없다는 뜻은 무슨 말일까? 먼저 아래 내용을 보자. CPU 에서 memory 에 write 할 데이터가 없는 경우에, cache 를 flush 할 필요가 없다. 만약, CPU 가 memory 에 write 할 데이터가 있다면, CPU 는 작업이후에 반드시 dma_sync_single_for_device() 함수를 호출해야 한다.

    1. dma_sync_single_for_cpu() - invalidation
    2. dma_sync_single_for_device() - flush + invalidation

     

     

     

    - Handling Errors

    " 일부 CPU architecture 에서 DMA address space 가 제한되는 경우가 있다. 즉, DMA buffer allocation 및 DMA mapping 이 실패할 수 도 있다는 것을 의미한다. 이럴 경우, 아래 제시된 방법들을 통해서 어떤 에러가 발생했는지를 파악해볼 수 있다.

    1. dma_alloc_coherent() 함수에서 NULL 을 리턴하거나 or dma_map_sg() 함수에서 0 을 리턴하는 경우

    2. `dma_mapping_error()` 함수를 이용해서 dma_map_single() or dma_map_page() 함수에서 반환된 `dma_addr_t` 를 검사한다.
    // https://docs.kernel.org/core-api/dma-api-howto.html
    dma_addr_t dma_handle;
    
    dma_handle = dma_map_single(dev, addr, size, direction);
    if (dma_mapping_error(dev, dma_handle)) {
            /*
             * reduce current DMA mapping usage,
             * delay and try again later or
             * reset driver.
             */
            goto map_error_handling;
    }​



    3. DMA buffer 에 여러 pages 들을 mapping 하려고 할 때, 중간 page 에서 mapping error 가 발생하면, 이미 mapped 된 pages 들을 모두 unmap 한다. 이 때, 2 가지 방법으로 unmap 을 해볼 수 있다.

    3.1 첫번째는 page 개수가 작은 경우다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    dma_addr_t dma_handle1;
    dma_addr_t dma_handle2;
    
    dma_handle1 = dma_map_single(dev, addr, size, direction);
    if (dma_mapping_error(dev, dma_handle1)) {
            /*
             * reduce current DMA mapping usage,
             * delay and try again later or
             * reset driver.
             */
            goto map_error_handling1;
    }
    dma_handle2 = dma_map_single(dev, addr, size, direction);
    if (dma_mapping_error(dev, dma_handle2)) {
            /*
             * reduce current DMA mapping usage,
             * delay and try again later or
             * reset driver.
             */
            goto map_error_handling2;
    }
    
    ...
    
    map_error_handling2:
            dma_unmap_single(dma_handle1);
    map_error_handling1:



    3.2 page 개수가 많으면, 루프문을 통해 해제하는 게 좋다. save_index 는 이미 mapped 된 pages 들의 개수를 나타낸다.
    unmap 은 이미 mapped 된 pages 들만 해야하므로, `map_error_handling:` 쪽을 보면, 0 ~ save_index 까지만 loop 하는 것을 볼 수 있다.

    // https://docs.kernel.org/core-api/dma-api-howto.html
    /*
     * if buffers are allocated in a loop, unmap all mapped buffers when
     * mapping error is detected in the middle
     */
    
    dma_addr_t dma_addr;
    dma_addr_t array[DMA_BUFFERS];
    int save_index = 0;
    
    for (i = 0; i < DMA_BUFFERS; i++) {
    
            ...
    
            dma_addr = dma_map_single(dev, addr, size, direction);
            if (dma_mapping_error(dev, dma_addr)) {
                    /*
                     * reduce current DMA mapping usage,
                     * delay and try again later or
                     * reset driver.
                     */
                    goto map_error_handling;
            }
            array[i].dma_addr = dma_addr;
            save_index++;
    }
    
    ...
    
    map_error_handling:
    
    for (i = 0; i < save_index; i++) {
    
            ...
    
            dma_unmap_single(array[i].dma_addr);
    }

     

     

    " network driver or SCSI driver 는 별도의 DMA error handling 을 가지고 있다. 그러므로, 해당 업무 개발자들은 이 글의 `Handling Errors` 섹션을 참고하도록 하자.

Designed by Tistory.