ALTERNATIVE()

<kernel v5.0>

ALTERNATIVE() 매크로

아키텍처마다 인자 개수가 조금씩 다르게 구성되었고, 이 글에서는 ARM64를 기반으로 설명한다.

컴파일 타임에 실행할 명령(old-instruction)을 준비하고, 조건에 따라 기존 명령을 새 명령(new-instruction)으로 대체시켜 동작시키는 기법으로 조건 처리를 했어도 수행 성능에 영향이 가지 않게 하는 기법이다.

  • ARM에서는 ALTERNATIVE 기능이 적용되지 않았다.
  • ARM64의 경우 ARMv8.1 코드와 ARMv8.2 코드를 준비해두고 부트업시 cpu capabilities(features)를 확인하여 가능하면 성능이 더 좋은 ARMv8.2 코드로 replacement 하도록 사용한다.

 

대체(replacement)
  • 대체하는 시점은 SMP 시스템에서 부트 cpu가 초기화를 완료한 후 secondary cpu가 초기화될 때 조건을 만족하는 ALTERNATIVE() 매크로의 replacement를 수행하게 한다.
  • 모듈인 경우 모듈이 로드될 때 조건을 만족하는 경우 replacement를 수행하게 한다.

 

조건
  • cpu에 지정한 기능(capabilities, features)이 있는 경우
  • _ALTERNATIVE_CFG() 매크로를 사용하는 경우 커널 옵션을 추가로 조건으로 지정할 수 있다.

 

예) ALTERNATIVE(llsc, lse, ARM64_HAS_LSE_ATOMICS)

  • ARMv8.1의 경우 llsc 명령을 사용하고, ARMv8.2의 경우 LSE atomic 명령을 사용할 수 있다. 이러한 경우 lse 명령을 사용하게 한다.

 

arch/arm64/include/asm/alternative.h

/*
 * Usage: asm(ALTERNATIVE(oldinstr, newinstr, feature));
 *
 * Usage: asm(ALTERNATIVE(oldinstr, newinstr, feature, CONFIG_FOO));
 * N.B. If CONFIG_FOO is specified, but not selected, the whole block
 *      will be omitted, including oldinstr.
 */
#define ALTERNATIVE(oldinstr, newinstr, ...)   \
        _ALTERNATIVE_CFG(oldinstr, newinstr, __VA_ARGS__, 1)

SMP 시스템에서 처음 부트 cpu는 첫 번째 인자에 있는 old 명령을 사용하여 명령이 진행된다. 그러다가 다른 cpu들의 설정이 완료될 때 세 번째인자의 cpu capabilities가 동작하는 경우 해당 코드들이 두 번째 인자 명령으로 치환된다. 이를 통해 더 빠른 성능을 수행하게 한다.

  •  oldinstr
    • 부트업 cpu 또는 조건을 만족시키지 못할 때 수행할 1개의 assembly 명령
  •  newinstr
    • secondary cpu 들에서 조건을 만족시킬 때 대치될 1개의 assembly 명령
  • 3번 째 인자 (feature)
    • cpu 아키텍처가 지원하는 기능(capabilities)

 

#define _ALTERNATIVE_CFG(oldinstr, newinstr, feature, cfg, ...) \
        __ALTERNATIVE_CFG(oldinstr, newinstr, feature, IS_ENABLED(cfg), 0)
  • 4번 째  인자 (cfg)
    • 커널에 enable되어야 할 옵션을 추가로 조건을 지정할 수 있다.

 

/*
 * alternative assembly primitive:
 *
 * If any of these .org directive fail, it means that insn1 and insn2
 * don't have the same length. This used to be written as
 *
 * .if ((664b-663b) != (662b-661b))
 *      .error "Alternatives instruction length mismatch"
 * .endif
 *
 * but most assemblers die if insn1 or insn2 have a .inst. This should
 * be fixed in a binutils release posterior to 2.25.51.0.2 (anything
 * containing commit 4e4d08cf7399b606 or c1baaddf8861).
 *
 * Alternatives with callbacks do not generate replacement instructions.
 */
#define __ALTERNATIVE_CFG(oldinstr, newinstr, feature, cfg_enabled, cb) \
        ".if "__stringify(cfg_enabled)" == 1\n"                         \
        "661:\n\t"                                                      \
        oldinstr "\n"                                                   \
        "662:\n"                                                        \
        ".pushsection .altinstructions,\"a\"\n"                         \
        ALTINSTR_ENTRY(feature,cb)                                      \
        ".popsection\n"                                                 \
        " .if " __stringify(cb) " == 0\n"                               \
        ".pushsection .altinstr_replacement, \"a\"\n"                   \
        "663:\n\t"                                                      \
        newinstr "\n"                                                   \
        "664:\n\t"                                                      \
        ".popsection\n\t"                                               \
        ".org   . - (664b-663b) + (662b-661b)\n\t"                      \
        ".org   . - (662b-661b) + (664b-663b)\n"                        \
        ".else\n\t"                                                     \
        "663:\n\t"                                                      \
        "664:\n\t"                                                      \
        ".endif\n"                                                      \
        ".endif\n"

.altinstructions 섹션에 push한 newinstr 코드들은 secondary cpu를 부트업한 후 apply_alternatives_all() 함수에서 oldinstr 코드들을 모두 replace한다.

  • start_kernel() -> kernel_init() -> kernel_init_freeable() -> smp_init() -> smp_cpus_done() -> apply_alternatives_all()

 

arch/arm64/include/asm/alternative.h

#define ALTINSTR_ENTRY(feature,cb)                                            \
        " .word 661b - .\n"                             /* label           */ \
        " .if " __stringify(cb) " == 0\n"                                     \
        " .word 663f - .\n"                             /* new instruction */ \
        " .else\n"                                                            \
        " .word " __stringify(cb) "- .\n"               /* callback */        \
        " .endif\n"                                                           \
        " .hword " __stringify(feature) "\n"            /* feature bit     */ \
        " .byte 662b-661b\n"                            /* source len      */ \
        " .byte 664f-663f\n"

 

다음 그림은 ALTERNATIVE() 매크로가 저장하는 섹셕들과 alt_instr 구조체의 각 offset 값들에 대해 보여준다.

 

Cpu Capabilities(Features)

다음 항목들은 ALTERNATIVE() 매크로 함수에서 사용할 수 있는 cpu capabilities이다. cpu features라고도 불린다.

arch/arm64/include/asm/cpucaps.h

#define ARM64_WORKAROUND_CLEAN_CACHE            0
#define ARM64_WORKAROUND_DEVICE_LOAD_ACQUIRE    1
#define ARM64_WORKAROUND_845719                 2
#define ARM64_HAS_SYSREG_GIC_CPUIF              3
#define ARM64_HAS_PAN                           4
#define ARM64_HAS_LSE_ATOMICS                   5
#define ARM64_WORKAROUND_CAVIUM_23154           6
#define ARM64_WORKAROUND_834220                 7
#define ARM64_HAS_NO_HW_PREFETCH                8
#define ARM64_HAS_UAO                           9
#define ARM64_ALT_PAN_NOT_UAO                   10
#define ARM64_HAS_VIRT_HOST_EXTN                11
#define ARM64_WORKAROUND_CAVIUM_27456           12
#define ARM64_HAS_32BIT_EL0                     13
#define ARM64_HARDEN_EL2_VECTORS                14
#define ARM64_HAS_CNP                           15
#define ARM64_HAS_NO_FPSIMD                     16
#define ARM64_WORKAROUND_REPEAT_TLBI            17
#define ARM64_WORKAROUND_QCOM_FALKOR_E1003      18
#define ARM64_WORKAROUND_858921                 19
#define ARM64_WORKAROUND_CAVIUM_30115           20
#define ARM64_HAS_DCPOP                         21
#define ARM64_SVE                               22
#define ARM64_UNMAP_KERNEL_AT_EL0               23
#define ARM64_HARDEN_BRANCH_PREDICTOR           24
#define ARM64_HAS_RAS_EXTN                      25
#define ARM64_WORKAROUND_843419                 26
#define ARM64_HAS_CACHE_IDC                     27
#define ARM64_HAS_CACHE_DIC                     28
#define ARM64_HW_DBM                            29
#define ARM64_SSBD                              30
#define ARM64_MISMATCHED_CACHE_TYPE             31
#define ARM64_HAS_STAGE2_FWB                    32
#define ARM64_HAS_CRC32                         33
#define ARM64_SSBS                              34
#define ARM64_WORKAROUND_1188873                35
#define ARM64_HAS_SB                            36
#define ARM64_WORKAROUND_1165522                37
#define ARM64_HAS_ADDRESS_AUTH_ARCH             38
#define ARM64_HAS_ADDRESS_AUTH_IMP_DEF          39
#define ARM64_HAS_GENERIC_AUTH_ARCH             40
#define ARM64_HAS_GENERIC_AUTH_IMP_DEF          41

#define ARM64_NCAPS                             42

시스템에 위의 cpu capabilities가 사용되는지 여부를 알아오기 위해 init_cpu_features() 함수가 여러 개의 ARM64 레지스터들을 읽어 cpu features를 파악한다. 그런 후 cpu_hwcap_keys[] 전역 배열에 capabilities 여부를 저장하고 성능을 위해 이를 static key로 제공한다.

  • capabilities(features) detection에 대한 범위는 다음과 같이 다르다.
    • SCOPE_LOCAL_CPU
      • 시스템 내의 모든 cpu들에 대해 하나 cpu라도 기능이 들어가 있는 경우
      • 부트 cpu를 제외한 나머지 cpu들이 seconary_start_kernel() -> check_local_cpu_capabilities() 함수를 통해 capabilities를 파악한다.
    • SCOPE_SYSTEM
      • 시스템 내의 모든 cpu들에 기능이 모두 들어가 있는 경우
      • 부트 cpu를 제외한 cpu들에 대해 setup_system_capabilities() 함수를 통해 capabilities를 파악한다.
    • SCOPE_BOOT_CPU
      • 시스템 내의 부트 cpu에 기능이 들어가 있는 경우
      • 부트 cpu만 setup_boot_cpu_capabilities()를 통해 capabilities를 파악한다.
  • 이에 대한 코드 해석은 방대한 데이터 시트의 설명을 필요로하여 생략한다.

 

코드 대체(replacement)

다음 그림은 부트업 마지막 과정에서 cpu에 capabilities(feature)가 지원하는 새 코드로 대체하는 과정을 보여준다.

 

apply_alternatives_all()

arch/arm64/kernel/alternative.c

void __init apply_alternatives_all(void)
{
        /* better not try code patching on a live SMP system */
        stop_machine(__apply_alternatives_multi_stop, NULL, cpu_online_mask);
}

모든 cpu를 멈춘 후 alternative 매크로를 사용한 명령들에 대해 조건을 만족시키면 대체 명령으로 replace한다.

 

__apply_alternatives_multi_stop()

arch/arm64/kernel/alternative.c

/*
 * We might be patching the stop_machine state machine, so implement a
 * really simple polling protocol here.
 */
static int __apply_alternatives_multi_stop(void *unused)
{
        struct alt_region region = {
                .begin  = (struct alt_instr *)__alt_instructions,
                .end    = (struct alt_instr *)__alt_instructions_end,
        };

        /* We always have a CPU 0 at this point (__init) */
        if (smp_processor_id()) {
                while (!READ_ONCE(alternatives_applied))
                        cpu_relax();
                isb();
        } else {
                BUG_ON(alternatives_applied);
                __apply_alternatives(&region, false);
                /* Barriers provided by the cache flushing */
                WRITE_ONCE(alternatives_applied, 1);
        }

        return 0;
}

alternative 매크로를 사용하여 .altinstructions 섹션에 위치한 명령들에 대해 조건을 만족시키면 대체 명령으로 replace한다.

  • __alt_instructions ~ __alt_instructions_end는 .altinstructions 섹션을 포함한 범위 이다.

 

__apply_alternatives()

arch/arm64/kernel/alternative.c

static void __apply_alternatives(void *alt_region, bool is_module)
{
        struct alt_instr *alt;
        struct alt_region *region = alt_region;
        __le32 *origptr, *updptr;
        alternative_cb_t alt_cb;

        for (alt = region->begin; alt < region->end; alt++) {
                int nr_inst;

                /* Use ARM64_CB_PATCH as an unconditional patch */
                if (alt->cpufeature < ARM64_CB_PATCH &&
                    !cpus_have_cap(alt->cpufeature))
                        continue;

                if (alt->cpufeature == ARM64_CB_PATCH)
                        BUG_ON(alt->alt_len != 0);
                else
                        BUG_ON(alt->alt_len != alt->orig_len);

                pr_info_once("patching kernel code\n");

                origptr = ALT_ORIG_PTR(alt);
                updptr = is_module ? origptr : lm_alias(origptr);
                nr_inst = alt->orig_len / AARCH64_INSN_SIZE;

                if (alt->cpufeature < ARM64_CB_PATCH)
                        alt_cb = patch_alternative;
                else
                        alt_cb  = ALT_REPL_PTR(alt);

                alt_cb(alt, origptr, updptr, nr_inst);

                if (!is_module) {
                        clean_dcache_range_nopatch((u64)origptr,
                                                   (u64)(origptr + nr_inst));
                }
        }

        /*
         * The core module code takes care of cache maintenance in
         * flush_module_icache().
         */
        if (!is_module) {
                dsb(ish);
                __flush_icache_all();
                isb();
        }
}

주어진 영역의 모든 alt_instr들에 대해 루프를 돌며 cpu feature가 있는 경우 replacement를 수행한다.

  • 코드 라인 8~14에서 요청한 영역 범위를 루프를 돌며 alt->feature가 cpu가 지원하지 않는 기능이면 skip 한다.
  • 코드 라인 16~21에서 길이 체크를 하고 커널 코드를 변경한다는 메시지 정보를 출력한다.
  • 코드 라인 23~25에서 오리지널 명령 주소(dest), 변경할 명령 주소(alt)와 명령 개 수를 준비한다.
  • 코드 라인 28~33에서 ARM64 표준 feature를 사용한 경우 patch_alternative() 함수를 사용하여 명령을 replace 하고, 그렇지 않은 경우 ALTERNATIVE_CB() 매크로를 통해 등록한 콜백 함수를 사용한다.
    • kvm 사용 시 kvm_update_va_mask() 콜백 함수를 사용한다.
  • 코드 라인 35~38에서 모듈에서 사용되지 않은 경우 replace한 영역에 대해 d-cache를 클린한다.
  • 코드 라인 45에서 영역의 패치가 끝난 후 모듈에서 호출된 경우가 아니면 캐시 조작의 완료를 위해 dsb를 수행한다. 그런 후 명령 캐시와 명령 파이프를 모두 flush한다.
    • 코드 replacement가 발생하면 관련 영역의 데이터 클린 및 명령 캐시 및 명령 파이프를 flush 해야 한다.

 

ALT_ORIG_PTR() & ALT_REPL_PTR()

arch/arm64/kernel/alternative.c

#define __ALT_PTR(a,f)          ((void *)&(a)->f + (a)->f)
#define ALT_ORIG_PTR(a)         __ALT_PTR(a, orig_offset)
#define ALT_REPL_PTR(a)         __ALT_PTR(a, alt_offset)
  • ALT_ORIG_PTR()
    • original 명령이 위치한 주소를 가리킨다. (이 영역이 변경될 곳이다.)
  • ALT_REPL_PTR()
    • replace 명령이 위치한 주소를 가리킨다. (이 영역을 copy하여)

 

arch/arm64/include/asm/alternative.h

typedef void (*alternative_cb_t)(struct alt_instr *alt,
                                 __le32 *origptr, __le32 *updptr, int nr_inst);

현재 아래 두 종류의 callback 함수를 사용한다.

  • patch_alternative()
    • ALTERNATIVE() 매크로 사용 시
  • kvm_update_va_mask()
    • ALTERNATIVE_CB() 매크로 사용 시

 

alt_instr 구조체

arch/arm64/include/asm/alternative.h

struct alt_instr {
        s32 orig_offset;        /* offset to original instruction */
        s32 alt_offset;         /* offset to replacement instruction */
        u16 cpufeature;         /* cpufeature bit set for replacement */
        u8  orig_len;           /* size of original instruction(s) */
        u8  alt_len;            /* size of new instruction(s), <= orig_len */
};
  • orig_offset
    • alt_instr 구조체 위치를 기준으로 original 명령들이 위치한 주소 offset이 담긴다.  (이 영역이 변경될 곳이다.)
  • alt_offset
    • alt_instr 구조체 위치를 기준으로 alt 명령들이 위치한 주소 offset이 담긴다. (이 영역을 copy하여)
  • cpufeature
    • 이 cpu feature가 있으면 replace를 하려고 한다.
  • orig_len
    • 오리지널 명령들의 길이
  • alt_len
    • alt 명령들의 길이

 

DMA -4- (DMA Mapping)

<kernel v5.0>

DMA -4- (DMA Mapping)

Coherent 매핑이 아닌 Streaming 매핑을 사용하는 경우 DMA 전송 전/후로 매번 매핑 및 매핑 해제(sync 동작)가 필요하다.

  • DMA 전/후로 캐시 sync 동작 및 iommu 매핑/매핑 해제를 수행한다.
    • DMA 방향에 따라 캐시를 clean 하거나, invalidate 한다.
    • DMA를 사용하는 디바이스가 iommu 사용 시 매핑 및 매핑 해제를 수행한다.
  • 시스템 DRAM 보다 DMA 영역이 작은 경우 bounce buffer가 필요하다.
    • 이러한 경우에는 데이터 copy가 필요한 swiotlb를 사용한다.
    • 디폴트 bounce buffer는 64MB이다.
      • 접근 가능한 DMA 주소 범위안에서 bounce buffer가 할당된다.
      • ARM64의 경우 ZONE_DMA32를 사용하는 경우 4GB 물리 주소로 제한된다.

 

DMA 매핑 종류

다음과 같이 두 종류의 dma 매핑 중 하나를 선택하여 사용한다.

  • constant dma 매핑
    • driver 초기화 때 개별 매핑이 필요 없어 dma 전송 전/후로 매핑을 하지 않는다.
  • streaming dma 매핑
    • dma 전송 전/후로 매핑이 필요한 경우 사용한다.

 

스트리밍 DMA 매핑 종류

  • Single
    • 물리적으로 연속된 하나의 DMA 버퍼를 사용한다.
  • Scatter/Gather
    • 물리적으로 분산된 DMA 버퍼를 사용한다.
    • 한 번의 DMA 동작에 여러 개의 DMA 버퍼에 전송하도록 하는데, 물리적으로 연속된 DMA 버퍼처럼 동작한다.
    • 진보된 dma 컨트롤러에서만 이 기능을 사용할 수 있다.

 


스트리밍 dma – single 매핑/해제

 

다음 그림은 RAM에서 Device로 DMA 전송할 때의 캐시에 대한 single 스트리밍 매핑/해제 과정을 보여준다.

 

다음 그림은 Device로부터 RAM으로 DMA 전송할 때의 캐시에 대한 single 스트리밍 매핑/해제 과정을 보여준다.

 

single 매핑

 

다음 그림은 single 매핑에 대해 함수간 호출 관계를 보여준다.

 

dma_map_single()

include/linux/dma-mapping.h

#define dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, 0)

single 공간을 대상으로 dma 스트리밍 매핑을 수행한다.

  • d: 디바이스
  • a: 가상 주소
  • s: 사이즈
  • r: DMA 방향

 

dma_map_single_attrs()

include/linux/dma-mapping.h

static inline dma_addr_t dma_map_single_attrs(struct device *dev, void *ptr,
                size_t size, enum dma_data_direction dir, unsigned long attrs)
{
        debug_dma_map_single(dev, ptr, size);
        return dma_map_page_attrs(dev, virt_to_page(ptr), offset_in_page(ptr),
                        size, dir, attrs);
}

single 공간을 대상으로 dma 스트리밍 매핑을 수행하는데, 옵션으로 속성 값을 지정할 수 있다.

 

dma_map_page_attrs()

include/linux/dma-mapping.h

static inline dma_addr_t dma_map_page_attrs(struct device *dev,
                struct page *page, size_t offset, size_t size,
                enum dma_data_direction dir, unsigned long attrs)
{
        const struct dma_map_ops *ops = get_dma_ops(dev);
        dma_addr_t addr;

        BUG_ON(!valid_dma_direction(dir));
        if (dma_is_direct(ops))
                addr = dma_direct_map_page(dev, page, offset, size, dir, attrs);
        else
                addr = ops->map_page(dev, page, offset, size, dir, attrs);
        debug_dma_map_page(dev, page, offset, size, dir, addr);

        return addr;
}

한 개의 페이지를 대상으로 dma 스트리밍 매핑을 수행하는데, 옵션으로 속성 값을 지정할 수 있다.

  • 코드 라인 9~12에서 디바이스에 IOMMU dma 매핑 오퍼레이션이 제공되는 경우 (*map_page) 후크 함수를 호출한다. 그렇지 않은 경우 주소 변환 없이 사용하는 direct 매핑을 하도록 호출한다.

 

dma_direct_map_page()

kernel/dma/direct.c

dma_addr_t dma_direct_map_page(struct device *dev, struct page *page,
                unsigned long offset, size_t size, enum dma_data_direction dir,
                unsigned long attrs)
{
        phys_addr_t phys = page_to_phys(page) + offset;
        dma_addr_t dma_addr = phys_to_dma(dev, phys);

        if (unlikely(!dma_direct_possible(dev, dma_addr, size)) &&
            !swiotlb_map(dev, &phys, &dma_addr, size, dir, attrs)) {
                report_addr(dev, dma_addr, size);
                return DMA_MAPPING_ERROR;
        }

        if (!dev_is_dma_coherent(dev) && !(attrs & DMA_ATTR_SKIP_CPU_SYNC))
                arch_sync_dma_for_device(dev, phys, size, dir);
        return dma_addr;
}
EXPORT_SYMBOL(dma_direct_map_page);

한 개의 페이지를 대상으로 주소 변환 없는 direct 매핑을 수행한다.

  • 코드 라인 8~12에서 DMA 영역이 제한되어 bounce buffer를 사용하는 sw-iotlb 매핑이 필요한 경우 이를 수행한다.
  • 코드 라인 14~15에서 디바이스가 coherent 연동되지 않고 skip cpu sync 속성 요청되지 않은 경우 디바이스의 DMA 전송 전에 아키텍처별로 제공되는 sync를 요청한다.

 

arch_sync_dma_for_device() – ARM64

arch/arm64/mm/dma-mapping.c

void arch_sync_dma_for_device(struct device *dev, phys_addr_t paddr,
                size_t size, enum dma_data_direction dir)
{
        __dma_map_area(phys_to_virt(paddr), size, dir);
}

ARM64 아키텍처의 경우 디바이스의 DMA 전송 전에 디바이스 턴을 위해 DMA 방향에 따른 캐시 sync를 요청한다.

 

__dma_map_area()

arch/arm64/mm/cache.S

/*
 *      __dma_map_area(start, size, dir)
 *      - start - kernel virtual start address
 *      - size  - size of region
 *      - dir   - DMA direction
 */
ENTRY(__dma_map_area)
        cmp     w2, #DMA_FROM_DEVICE
        b.eq    __dma_inv_area
        b       __dma_clean_area
ENDPIPROC(__dma_map_area)

ARM64 아키텍처의 경우 디바이스의 DMA 전송 전에 DMA 방향에 따른 캐시 sync를 다음과 같이 수행한다.

  • DEVICE -> RAM 방향인 경우 기존 값은 의미가 없으므로 성능 향상을 위해 캐시를 clean 하지 않고 invalidate를 수행한다.
  • 그 외의 방향은 clean을 수행한다.

 

single 매핑 해제

 

다음 그림은 single 매핑 해제에 대해 함수간 호출 관계를 보여준다.

 

dma_unmap_single()

include/linux/dma-mapping.h

#define dma_unmap_single(d, a, s, r) dma_unmap_single_attrs(d, a, s, r, 0)

single 공간을 대상으로 dma 스트리밍 매핑을 해제한다.

  • d: 디바이스
  • a: 가상 주소
  • s: 사이즈
  • r: DMA 방향

 

dma_unmap_single_attrs()

include/linux/dma-mapping.h

static inline void dma_unmap_single_attrs(struct device *dev, dma_addr_t addr,
                size_t size, enum dma_data_direction dir, unsigned long attrs)
{
        return dma_unmap_page_attrs(dev, addr, size, dir, attrs);
}

single 공간을 대상으로 dma 스트리밍 매핑을 해제하는데, 옵션으로 속성 값을 지정할 수 있다.

 

dma_unmap_page_attrs()

include/linux/dma-mapping.h

static inline void dma_unmap_page_attrs(struct device *dev, dma_addr_t addr,
                size_t size, enum dma_data_direction dir, unsigned long attrs)
{
        const struct dma_map_ops *ops = get_dma_ops(dev);

        BUG_ON(!valid_dma_direction(dir));
        if (dma_is_direct(ops))
                dma_direct_unmap_page(dev, addr, size, dir, attrs);
        else if (ops->unmap_page)
                ops->unmap_page(dev, addr, size, dir, attrs);
        debug_dma_unmap_page(dev, addr, size, dir);
}

한 개의 페이지를 대상으로 dma 스트리밍 매핑을 해제하는데, 옵션으로 속성 값을 지정할 수 있다.

  • 코드 라인 7~10에서 디바이스에 IOMMU dma 매핑 해제 오퍼레이션이 제공되는 경우 (*unmap_page) 후크 함수를 호출한다. 그렇지 않은 경우 주소 변환 없이 사용하는 direct 매핑 해제를 하도록 호출한다.

 

dma_direct_unmap_page()

kernel/dma/direct.c

void dma_direct_unmap_page(struct device *dev, dma_addr_t addr,
                size_t size, enum dma_data_direction dir, unsigned long attrs)
{
        phys_addr_t phys = dma_to_phys(dev, addr);

        if (!(attrs & DMA_ATTR_SKIP_CPU_SYNC))
                dma_direct_sync_single_for_cpu(dev, addr, size, dir);

        if (unlikely(is_swiotlb_buffer(phys)))
                swiotlb_tbl_unmap_single(dev, phys, size, dir, attrs);
}
EXPORT_SYMBOL(dma_direct_unmap_page);

한 개의 페이지를 대상으로 주소 변환 없는 direct 매핑을 해제한다.

  • 코드 라인 6~7에서 skip cpu sync 속성 요청되지 않은 경우 디바이스의 DMA 전송 후에 아키텍처별로 제공되는 sync를 요청한다
  • 코드 라인 9~10에서 DMA 영역이 제한되어 bounce buffer를 사용하면 sw-iotlb 매핑 해제를 수행한다.

 

dma_direct_sync_single_for_cpu()

kernel/dma/direct.c

void dma_direct_sync_single_for_cpu(struct device *dev,
                dma_addr_t addr, size_t size, enum dma_data_direction dir)
{
        phys_addr_t paddr = dma_to_phys(dev, addr);

        if (!dev_is_dma_coherent(dev)) {
                arch_sync_dma_for_cpu(dev, paddr, size, dir);
                arch_sync_dma_for_cpu_all(dev);
        }

        if (unlikely(is_swiotlb_buffer(paddr)))
                swiotlb_tbl_sync_single(dev, paddr, size, dir, SYNC_FOR_CPU);
}
EXPORT_SYMBOL(dma_direct_sync_single_for_cpu);

디바이스의 DMA 전송 후 cpu 턴을 위해 DMA 방향에 따른 캐시 sync를 요청한다.

  • 코드 라인 6~9에서 아키텍처에 따른 sync를 수행한다.
  • 코드 라인 11~12에서 DMA 영역이 제한되어 bounce buffer를 사용하면 sw-iotlb 싱크를 수행한다.

 

arch_sync_dma_for_cpu() – ARM64

arch/arm64/mm/dma-mapping.c

void arch_sync_dma_for_cpu(struct device *dev, phys_addr_t paddr,
                size_t size, enum dma_data_direction dir)
{
        __dma_unmap_area(phys_to_virt(paddr), size, dir);
}

ARM64 아키텍처의 경우 디바이스의 DMA 전송 후 cpu 턴을 위해 DMA 방향에 따른 캐시 sync를 요청한다.

 

__dma_unmap_area()

arch/arm64/mm/cache.S

/*
 *      __dma_unmap_area(start, size, dir)
 *      - start - kernel virtual start address
 *      - size  - size of region
 *      - dir   - DMA direction
 */
ENTRY(__dma_unmap_area)
        cmp     w2, #DMA_TO_DEVICE
        b.ne    __dma_inv_area
        ret
ENDPIPROC(__dma_unmap_area)

ARM64 아키텍처의 경우 디바이스의 DMA 전송 후에 DMA 방향에 따른 캐시 sync를 다음과 같이 수행한다.

  • DEVICE <- RAM 방향인 경우 디바이스가 캐시에 write 접근을 하지 않은 경우이므로 캐시를 invalidate하거나 clean 할 필요 없다.
  • 또한 그 외의 방향에서는 디바이스가 메모리에 기록하였을지도 모르므로 CPU의 캐시들은 모두 invalidate를 하여 메모리에 아무런 변경을 하지 못하게 한다.

 


스트리밍 dma – scatter/gather 매핑/해제

 

분산된 물리 메모리를 DMA 버퍼용도로 할당 받은 후 이들의 정보를 scaterlist 구조체에 대입하고 이를 배열로 요청하여 DMA 전송 요청 전/후로 등록된 DMA 버퍼들에 대해 DMA Streaming 매핑을 한꺼번에 수행할 때 scatter/gather 매핑을 사용한다.

  • DMA  전송 시 물리메모리가 연속된 것처럼  한꺼번에 이루어진다.

 

다음 그림은 3개의 DMA 버퍼로 구성된 scatterlist 배열을 보여준다.

 

scatter/gather 매핑

DMA 전송할 여러 개의 영역을 한 꺼번에 매핑을 수행한다.

 

dma_map_sg()

include/linux/dma-mapping.h

#define dma_map_sg(d, s, n, r) dma_map_sg_attrs(d, s, n, r, 0)

여러 개의 공간을 대상으로 dma 스트리밍 매핑을 수행한다.

  • d: 디바이스
  • s: 여러 영역 정보를 리스트한 scatterlist
  • n: 엔트리 수
  • r: DMA 방향

 

dma_map_sg_attrs()

include/linux/dma-mapping.h

/*
 * dma_maps_sg_attrs returns 0 on error and > 0 on success.
 * It should never return a value < 0.
 */
static inline int dma_map_sg_attrs(struct device *dev, struct scatterlist *sg,
                                   int nents, enum dma_data_direction dir,
                                   unsigned long attrs)
{
        const struct dma_map_ops *ops = get_dma_ops(dev);
        int ents;

        BUG_ON(!valid_dma_direction(dir));
        if (dma_is_direct(ops))
                ents = dma_direct_map_sg(dev, sg, nents, dir, attrs);
        else
                ents = ops->map_sg(dev, sg, nents, dir, attrs);
        BUG_ON(ents < 0);
        debug_dma_map_sg(dev, sg, nents, ents, dir);

        return ents;
}

인자 @sg로 전달받은 여러 공간을 대상으로 dma 스트리밍 매핑을 수행하는데, 옵션으로 속성 값을 지정할 수 있다.

  • 코드 라인 9~12에서 디바이스에 IOMMU dma 매핑 오퍼레이션이 제공되는 경우 (*map_sg) 후크 함수를 호출한다. 그렇지 않은 경우 주소 변환 없이 사용하는 direct sg 매핑을 하도록 호출한다.

 

dma_direct_map_sg()

kernel/dma/direct.c

int dma_direct_map_sg(struct device *dev, struct scatterlist *sgl, int nents,
                enum dma_data_direction dir, unsigned long attrs)
{
        int i;
        struct scatterlist *sg;

        for_each_sg(sgl, sg, nents, i) {
                sg->dma_address = dma_direct_map_page(dev, sg_page(sg),
                                sg->offset, sg->length, dir, attrs);
                if (sg->dma_address == DMA_MAPPING_ERROR)
                        goto out_unmap;
                sg_dma_len(sg) = sg->length;
        }

        return nents;

out_unmap:
        dma_direct_unmap_sg(dev, sgl, i, dir, attrs | DMA_ATTR_SKIP_CPU_SYNC);
        return 0;
}
EXPORT_SYMBOL(dma_direct_map_sg);

인자 @sgl로 전달받은 여러 공간을 대상으로 엔트리 수(@nents)만큼 dma 페이지 매핑을 수행하는데, 옵션으로 속성 값을 지정할 수 있다.

 

scatter/gather 매핑 해제

DMA 전송할 여러 개의 영역을 한 꺼번에 매핑해제를 수행한다.

 

dma_unmap_sg()

include/linux/dma-mapping.h

#define dma_unmap_sg(d, s, n, r) dma_unmap_sg_attrs(d, s, n, r, 0)

여러 개의 공간을 대상으로 dma 스트리밍 매핑 해제를 수행한다.

  • d: 디바이스
  • s: 여러 영역 정보를 리스트한 scatterlist
  • n: 엔트리 수
  • r: DMA 방향

 

dma_unmap_sg_attrs()

include/linux/dma-mapping.h

static inline void dma_unmap_sg_attrs(struct device *dev, struct scatterlist *sg,
                                      int nents, enum dma_data_direction dir,
                                      unsigned long attrs)
{
        const struct dma_map_ops *ops = get_dma_ops(dev);

        BUG_ON(!valid_dma_direction(dir));
        debug_dma_unmap_sg(dev, sg, nents, dir);
        if (dma_is_direct(ops))
                dma_direct_unmap_sg(dev, sg, nents, dir, attrs);
        else if (ops->unmap_sg)
                ops->unmap_sg(dev, sg, nents, dir, attrs);
}

인자 @sg로 전달받은 여러 공간을 대상으로 dma 스트리밍 매핑 해제를 수행하는데, 옵션으로 속성 값을 지정할 수 있다.

  • 코드 라인 9~12에서 디바이스에 IOMMU dma 매핑 오퍼레이션이 제공되는 경우 (*unmap_sg) 후크 함수를 호출한다. 그렇지 않은 경우 주소 변환 없이 사용하는 direct sg 매핑 해제를 호출한다.

 

dma_direct_unmap_sg()

kernel/dma/direct.c

void dma_direct_unmap_sg(struct device *dev, struct scatterlist *sgl,
                int nents, enum dma_data_direction dir, unsigned long attrs)
{
        struct scatterlist *sg;
        int i;

        for_each_sg(sgl, sg, nents, i)
                dma_direct_unmap_page(dev, sg->dma_address, sg_dma_len(sg), dir,
                             attrs);
}
EXPORT_SYMBOL(dma_direct_unmap_sg);

인자 @sgl로 전달받은 여러 공간을 대상으로 엔트리 수(@nents)만큼 dma 페이지 매핑 해제를 수행하는데, 옵션으로 속성 값을 지정할 수 있다.

 

scatter/gather 테이블

sg_kmalloc()

lib/scatterlist.c

/*
 * The default behaviour of sg_alloc_table() is to use these kmalloc/kfree
 * helpers.
 */
static struct scatterlist *sg_kmalloc(unsigned int nents, gfp_t gfp_mask)
{
        if (nents == SG_MAX_SINGLE_ALLOC) {
                /*
                 * Kmemleak doesn't track page allocations as they are not
                 * commonly used (in a raw form) for kernel data structures.
                 * As we chain together a list of pages and then a normal
                 * kmalloc (tracked by kmemleak), in order to for that last
                 * allocation not to become decoupled (and thus a
                 * false-positive) we need to inform kmemleak of all the
                 * intermediate allocations.
                 */
                void *ptr = (void *) __get_free_page(gfp_mask);
                kmemleak_alloc(ptr, PAGE_SIZE, 1, gfp_mask);
                return ptr;
        } else
                return kmalloc_array(nents, sizeof(struct scatterlist),
                                     gfp_mask);
}

@nents개의 scatterlist 배열을 담을 메모리를 할당받는다.

  • 보통 1페이지를 채워서 받을 수 있도록 @nents=SG_MAX_SINGLE_ALLOC을 지정할 수 있다.
  • sg_kfree()
    • 할당 해제 API

 

sg_init_table()

lib/scatterlist.c

/**
 * sg_init_table - Initialize SG table
 * @sgl:           The SG table
 * @nents:         Number of entries in table
 *
 * Notes:
 *   If this is part of a chained sg table, sg_mark_end() should be
 *   used only on the last table part.
 *
 **/
void sg_init_table(struct scatterlist *sgl, unsigned int nents)
{
        memset(sgl, 0, sizeof(*sgl) * nents);
        sg_init_marker(sgl, nents);
}
EXPORT_SYMBOL(sg_init_table);

sg 테이블을 초기화한다. 마지막 엔트리는 SG_END 마킹을 한다.

 

/**
 * sg_init_marker - Initialize markers in sg table
 * @sgl:           The SG table
 * @nents:         Number of entries in table
 *
 **/
static inline void sg_init_marker(struct scatterlist *sgl,
                                  unsigned int nents)
{
        sg_mark_end(&sgl[nents - 1]);
}

sgl의 마지막 엔트리를 SG_END 마킹 한다.

 

/**
 * sg_mark_end - Mark the end of the scatterlist
 * @sg:          SG entryScatterlist
 *
 * Description:
 *   Marks the passed in sg entry as the termination point for the sg
 *   table. A call to sg_next() on this entry will return NULL.
 *
 **/
static inline void sg_mark_end(struct scatterlist *sg)
{
        /*
         * Set termination bit, clear potential chain bit
         */
        sg->page_link |= SG_END;
        sg->page_link &= ~SG_CHAIN;
}

요청한 sg를 SG_END 마킹한다. (SG_CHAIN이 있는 경우 제거)

 

기타 Scatter/Gather API

  • sg_next()
  • sg_next_ptr()
  • sg_chain()
  • sg_nents()
  • sg_nents_for_len()
  • sg_last()
  • sg_set_page()
  • sg_set_buf()
  • sg_mark_end()

 

Chained Scatter/Gather Table

  • sg 테이블을 여러 개 할당받아 chain으로 연결하여 대규모 전송을 통한 성능 향상을 위해 사용한다.
  • block io 전송에 사용되고 있다.
  • 관련 API
    • sg_alloc_table()
      • __sg_alloc_table()
    • sg_alloc_table_from_pages()
      • __sg_alloc_table_from_pages()
    • sgl_alloc()
      • sgl_alloc_order()
    • sgl_free()
      • sgl_free_order()
        • sgl_free_n_order()
    • sg_copy_from_buffer()
      •  sg_copy_buffer()
        • sg_miter_start()
        • sg_miter_skip()
        • sg_miter_stop()
    • sg_copy_to_buffer()
    • sg_zero_buffer()
  • 참고

 

참고

DMA -3- (DMA Pool)

<kernel v5.0>

DMA -3- (DMA Pool)

Coherent per-device Memory는 페이지 단위로 할당관리를 하는데 이보다 더 작은 단위의 사이즈를 관리하는 DMA pool을 여러 개 추가하여 사용할 수 있다.

다음 그림은 3개의 DMA pool을 생성한 모습을 보여준다.

  • 디바이스 전용으로 5M bytes coherent 메모리가 구성되어 있다.
  • 256, 512, 8192 바이트를 제공하는 각각의 DMA pool이 생성되어 있다.

 

DMA Pool 생성

dma_pool_create()

mm/dmapool.c

/**
 * dma_pool_create - Creates a pool of consistent memory blocks, for dma.
 * @name: name of pool, for diagnostics
 * @dev: device that will be doing the DMA
 * @size: size of the blocks in this pool.
 * @align: alignment requirement for blocks; must be a power of two
 * @boundary: returned blocks won't cross this power of two boundary
 * Context: !in_interrupt()
 *
 * Returns a dma allocation pool with the requested characteristics, or
 * null if one can't be created.  Given one of these pools, dma_pool_alloc()
 * may be used to allocate memory.  Such memory will all have "consistent"
 * DMA mappings, accessible by the device and its driver without using
 * cache flushing primitives.  The actual size of blocks allocated may be
 * larger than requested because of alignment.
 *
 * If @boundary is nonzero, objects returned from dma_pool_alloc() won't
 * cross that size boundary.  This is useful for devices which have
 * addressing restrictions on individual DMA transfers, such as not crossing
 * boundaries of 4KBytes.
 */
struct dma_pool *dma_pool_create(const char *name, struct device *dev,
                                 size_t size, size_t align, size_t boundary)
{
        struct dma_pool *retval;
        size_t allocation;
        bool empty = false;

        if (align == 0)
                align = 1;
        else if (align & (align - 1))
                return NULL;

        if (size == 0)
                return NULL;
        else if (size < 4)
                size = 4;

        if ((size % align) != 0)
                size = ALIGN(size, align);

        allocation = max_t(size_t, size, PAGE_SIZE);

        if (!boundary)
                boundary = allocation;
        else if ((boundary < size) || (boundary & (boundary - 1)))
                return NULL;

        retval = kmalloc_node(sizeof(*retval), GFP_KERNEL, dev_to_node(dev));
        if (!retval)
                return retval;

        strlcpy(retval->name, name, sizeof(retval->name));

        retval->dev = dev;

        INIT_LIST_HEAD(&retval->page_list);
        spin_lock_init(&retval->lock);
        retval->size = size;
        retval->boundary = boundary;
        retval->allocation = allocation;

        INIT_LIST_HEAD(&retval->pools);

        /*
         * pools_lock ensures that the ->dma_pools list does not get corrupted.
         * pools_reg_lock ensures that there is not a race between
         * dma_pool_create() and dma_pool_destroy() or within dma_pool_create()
         * when the first invocation of dma_pool_create() failed on
         * device_create_file() and the second assumes that it has been done (I
         * know it is a short window).
         */
        mutex_lock(&pools_reg_lock);
        mutex_lock(&pools_lock);
        if (list_empty(&dev->dma_pools))
                empty = true;
        list_add(&retval->pools, &dev->dma_pools);
        mutex_unlock(&pools_lock);
        if (empty) {
                int err;

                err = device_create_file(dev, &dev_attr_pools);
                if (err) {
                        mutex_lock(&pools_lock);
                        list_del(&retval->pools);
                        mutex_unlock(&pools_lock);
                        mutex_unlock(&pools_reg_lock);
                        kfree(retval);
                        return NULL;
                }
        }
        mutex_unlock(&pools_reg_lock);
        return retval;
}
EXPORT_SYMBOL(dma_pool_create);

요청한 디바이스용으로 블럭 사이즈(@size)를 할당해줄 수 있는 DMA coherent 메모리 풀을 생성한다. 보통 블럭 사이즈는 한 개 페이지보다 작은 단위의 크기(4~)를 사용하지만 클 수도 있다. 블럭 사이즈의 정렬 단위는 @align 값을 사용하고 2의 제곱승 단위 값만 허용한다.(1, 2, 4, 8, 16, …)  dma 페이지 내에서 @boundary 경계에 할당 블럭들이 자리잡지 못하게 막는 기능이다.  이 함수는 인터럽트 context에서 호출되면 안된다.

  • 코드 라인 8~11에서 @align 값이 지정되지 않은 경우 1 바이트로 지정된다. 또한 2의 제곱승 단위가 아닌 경우 실패 값인 null을 반환한다.
    • 예) @align=1, 2, 4, 8, 16, …
  • 코드 라인 13~16에서 @size 값이 지정되지 않은 경우 실패 값인 null을 반환한다. @size 값은 최소 4로 제한한다.
  • 코드 라인 18~19에서 @size 값은 @align 단위로 올림 처리한다.
    • 예) @align=8인 경우
      • @size=8, 16, 24, 32, … 와 같이 8 단위로 올림 정렬된다.
  • 코드 라인 21에서 실제 할당되는 크기는 1 페이지 또는 요청한 블럭 사이즈(@size) 중 큰 값을 사용한다.
  • 코드 라인 23~26에서 @boundary가 지정되지 않은 경우 할당 사이즈와 같다. 만일 @boundary가 지정되었지만 블럭 사이즈(@size)보다 작거나 2의 제곱승 단위로 지정되지 않은 경우 실패 값인 null을 반환한다.
  • 코드 라인 28~42에서 슬랩 메모리에서 dma_pool 구조체 사이즈만큼 할당해온 후 초기 설정 값들로 초기화한다.
  • 코드 라인 52~72에서 할당한 dma_pool 구조체를 디바이스의 dma_pools 리스트에 추가하고 “pools” 속성 파일을 생성한다.
    • 이 파일을 통해 이 풀의 통계 정보를 출력할 수 있다.

 

다음 그림은 256 바이트 @size와 @align 값을 사용하여 dma pool 정보를 디바이스에 추가하는 모습을 보여준다.

  • @size가 1 페이지 미만이므로 allocation 값은 최소값인 1 페이지에 해당하는 바이트를 지정한다.
  • @boundary 값으로 0을 사용하여 allocation 값과 동일하게 한다. 즉 allocation 내에 boundary가 없는 것이다.

 

DMA Pool에서 블럭 할당

dma_pool_alloc()

mm/dmapool.c

/**
 * dma_pool_alloc - get a block of consistent memory
 * @pool: dma pool that will produce the block
 * @mem_flags: GFP_* bitmask
 * @handle: pointer to dma address of block
 *
 * This returns the kernel virtual address of a currently unused block,
 * and reports its dma address through the handle.
 * If such a memory block can't be allocated, %NULL is returned.
 */
void *dma_pool_alloc(struct dma_pool *pool, gfp_t mem_flags,
                     dma_addr_t *handle)
{
        unsigned long flags;
        struct dma_page *page;
        size_t offset;
        void *retval;

        might_sleep_if(gfpflags_allow_blocking(mem_flags));

        spin_lock_irqsave(&pool->lock, flags);
        list_for_each_entry(page, &pool->page_list, page_list) {
                if (page->offset < pool->allocation)
                        goto ready;
        }

        /* pool_alloc_page() might sleep, so temporarily drop &pool->lock */
        spin_unlock_irqrestore(&pool->lock, flags);

        page = pool_alloc_page(pool, mem_flags & (~__GFP_ZERO));
        if (!page)
                return NULL;

        spin_lock_irqsave(&pool->lock, flags);

        list_add(&page->page_list, &pool->page_list);
 ready:
        page->in_use++;
        offset = page->offset;
        page->offset = *(int *)(page->vaddr + offset);
        retval = offset + page->vaddr;
        *handle = offset + page->dma;
#ifdef  DMAPOOL_DEBUG
        {
                int i;
                u8 *data = retval;
                /* page->offset is stored in first 4 bytes */
                for (i = sizeof(page->offset); i < pool->size; i++) {
                        if (data[i] == POOL_POISON_FREED)
                                continue;
                        if (pool->dev)
                                dev_err(pool->dev,
                                        "dma_pool_alloc %s, %p (corrupted)\n",
                                        pool->name, retval);
                        else
                                pr_err("dma_pool_alloc %s, %p (corrupted)\n",
                                        pool->name, retval);

                        /*
                         * Dump the first 4 bytes even if they are not
                         * POOL_POISON_FREED
                         */
                        print_hex_dump(KERN_ERR, "", DUMP_PREFIX_OFFSET, 16, 1,
                                        data, pool->size, 1);
                        break;
                }
        }
        if (!(mem_flags & __GFP_ZERO))
                memset(retval, POOL_POISON_ALLOCATED, pool->size);
#endif
        spin_unlock_irqrestore(&pool->lock, flags);

        if (mem_flags & __GFP_ZERO)
                memset(retval, 0, pool->size);

        return retval;
}
EXPORT_SYMBOL(dma_pool_alloc);

요청한 DMA pool에서 블럭을 하나 할당해온다. 할당한 블럭의 가상 주소가 반환되며, @handle에는 물리 주소가 저장된다.

  • 코드 라인 9에서 preemption point로 blocking 가능한 할당 요청인 경우 스케줄링 요청에 따라 sleep할 수 있다.
  • 코드 라인 11~18에서 dma pool에 등록된 dma 페이지를 대상으로 할당하지 않고 free한 페이지가 있는지 확인한다.
    • 모든 블럭이 할당 상태인 dma 페이지는 dma->offset이 dma->allocation을 초과한다.
  • 코드 라인 20~22에서 free한 상태의 dma 페이지가 없으므로 dma pool에서 dma 페이지를 할당해온다.
  • 코드 라인 24~26에서 할당받은 dma 페이지를 dma pool에 추가한다.
  • 코드 라인 28에서사용 블럭 수를 의미하는 in_use를 증가시킨다.
  • 코드 라인 29~30에서 page->offset이 빈블럭의 offset을 지정한다.
    • dma_page 가상 주소 + 기존 page->offset의 값을 읽어와서 다시 page->offset에 대입한다.
  • 코드 라인 31~32에서 할당받은 블럭에 해당하는 가상 주소 retval과 물리 주소 *handle에 지정한다.
  • 코드 라인 33~60에서 DMAPOOL_DEBUG가 설정된 경우 할당 받은 메모리가 POOL_POISON_FREED(0xa7) 상태가 아닌 경우 이를 경고 출력한다. 정상적인 경우 할당 받은 메모리를  POOL_POISON_ALLOCATED(0xa9) 값으로 채운다.
  • 코드 라인 63~64에서 __GFP_ZERO gfp 플래그로 요청한 경우 할당 블럭을 0으로 모두 채운다.

 

다음 그림은 DMA pool에 등록된 1개의 DMA page에 8개의 DMA 블럭이 있고, 현재 3번째 DMA 블럭 할당 요청이 수행된 후의 상태를 보여준다.

  • page->offset 값이 빈 DMA 블럭(4번)에 해당하는 offset이 지정된 것을 알 수 있다.
  • DMA 블럭이 하나도 할당되지 않았을 때의 page->offset은 0이다.
    • 할당될 때마다 pool->size 만큼 offset이 지정된다.
      • 512, 1024, 1536, …

 

pool_alloc_page()

mm/dmapool.c

static struct dma_page *pool_alloc_page(struct dma_pool *pool, gfp_t mem_flags)
{
        struct dma_page *page;

        page = kmalloc(sizeof(*page), mem_flags);
        if (!page)
                return NULL;
        page->vaddr = dma_alloc_coherent(pool->dev, pool->allocation,
                                         &page->dma, mem_flags);
        if (page->vaddr) {
#ifdef  DMAPOOL_DEBUG
                memset(page->vaddr, POOL_POISON_FREED, pool->allocation);
#endif
                pool_initialise_page(pool, page);
                page->in_use = 0;
                page->offset = 0;
        } else {
                kfree(page);
                page = NULL;
        }
        return page;
}

요청한 DMA pool에 하나의 DMA 페이지를 할당한 후 추가한다.

  • 코드 라인 5~7에서 dma_page 구조체를 할당해온다.
  • 코드 라인 8~9에서 coherent per-device 메모리에서 pool->allocation 사이즈(페이지 단위)만큼 할당해온다.
  • 코드 라인 10~21에서 할당이 성공한 경우 할당한 공간을 초기화한다. dma 페이지 내부의 초기화는 블럭 사이즈(pool->size) 단위로 offset 값을 기록한다.

 

다음 그림은 DMA 페이지를 할당받은 후 초기화를 한 모습을 보여준다.

  • 아직 할당해준 블럭이 없으므로 page->in_use는 0이고, page->offset도 처음 블럭을 의미하는 0 값이다.
  • 각 블럭의 첫 4바이트는 다음 블럭에 해당하는 offset 값을 갖는다.

 

pool_initialise_page()

mm/dmapool.c

static void pool_initialise_page(struct dma_pool *pool, struct dma_page *page)
{
        unsigned int offset = 0;
        unsigned int next_boundary = pool->boundary;

        do {
                unsigned int next = offset + pool->size;
                if (unlikely((next + pool->size) >= next_boundary)) {
                        next = next_boundary;
                        next_boundary += pool->boundary;
                }
                *(int *)(page->vaddr + offset) = next;
                offset = next;
        } while (offset < pool->allocation);
}

 

요청한 DMA pool의 DMA 페이지를 초기화한다.

  • 블럭마다  다음 블럭의 offset 값에 해당하는 4바이트의 offset 값을 지정한다.
  • 만일 boundary가 설정된 경우 각 블럭이 boundary 경계를 침범하지 않도록 조정한다.

 

다음 그림은 boundary가 설정된 DMA 페이지의 초기화 모습을 보여준다.

  • boundary가 DMA 페이지 중간에 설정되어 있으므로 블럭이 그 경계에 할당되지 않도록 offset을 조절한다.

 

DMA Pool로 블럭 할당 해제

dma_pool_free()

mm/dmapool.c

/**
 * dma_pool_free - put block back into dma pool
 * @pool: the dma pool holding the block
 * @vaddr: virtual address of block
 * @dma: dma address of block
 *
 * Caller promises neither device nor driver will again touch this block
 * unless it is first re-allocated.
 */
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t dma)
{
        struct dma_page *page;
        unsigned long flags;
        unsigned int offset;

        spin_lock_irqsave(&pool->lock, flags);
        page = pool_find_page(pool, dma);
        if (!page) {
                spin_unlock_irqrestore(&pool->lock, flags);
                if (pool->dev)
                        dev_err(pool->dev,
                                "dma_pool_free %s, %p/%lx (bad dma)\n",
                                pool->name, vaddr, (unsigned long)dma);
                else
                        pr_err("dma_pool_free %s, %p/%lx (bad dma)\n",
                               pool->name, vaddr, (unsigned long)dma);
                return;
        }

        offset = vaddr - page->vaddr;
#ifdef  DMAPOOL_DEBUG
        if ((dma - page->dma) != offset) {
                spin_unlock_irqrestore(&pool->lock, flags);
                if (pool->dev)
                        dev_err(pool->dev,
                                "dma_pool_free %s, %p (bad vaddr)/%pad\n",
                                pool->name, vaddr, &dma);
                else
                        pr_err("dma_pool_free %s, %p (bad vaddr)/%pad\n",
                               pool->name, vaddr, &dma);
                return;
        }
        {
                unsigned int chain = page->offset;
                while (chain < pool->allocation) {
                        if (chain != offset) {
                                chain = *(int *)(page->vaddr + chain);
                                continue;
                        }
                        spin_unlock_irqrestore(&pool->lock, flags);
                        if (pool->dev)
                                dev_err(pool->dev, "dma_pool_free %s, dma %pad already free\n",
                                        pool->name, &dma);
                        else
                                pr_err("dma_pool_free %s, dma %pad already free\n",
                                       pool->name, &dma);
                        return;
                }
        }
        memset(vaddr, POOL_POISON_FREED, pool->size);
#endif

        page->in_use--;
        *(int *)vaddr = page->offset;
        page->offset = offset;
        /*
         * Resist a temptation to do
         *    if (!is_page_busy(page)) pool_free_page(pool, page);
         * Better have a few empty pages hang around.
         */
        spin_unlock_irqrestore(&pool->lock, flags);
}
EXPORT_SYMBOL(dma_pool_free);

할당받은 DMA 블럭 메모리를 할당 해제하여 다시 DMA pool로 회수한다.

  • 코드 라인 7~19에서 해제할 물리 주소(@dma)를 사용하여 관련된 DMA 페이지를 알아온다. 만일 발견되지 않는 경우 에러 메시지를 출력하고 함수를 빠져나간다.
  • 코드 라인 21에서 할당 해제하고자 하는 블럭에 해당하는 offset 값을 알아온다.
  • 코드 라인 22~52에서 DMAPOOL_DEBUG 커널 옵션을 사용하는 경우 DMA 블럭의 가상 주소 offset과 물리 주소 offset이 서로 다른 경우 에러 메시지를 출력하고 함수를 빠져나간다. 정상적으로 offset이 서로 일치하는 경우 page->offset부터 마지막 블럭까지 chain이 offset과 하나라도 같으면 에러 메시지를 출력하고 함수를 빠져나간다. 최종적으로 이상이 없는 경우 할당 해제한 블럭을 POOL_POISON_FREED(0xa7) 값으로 채운다.
  • 코드 라인 54에서 할당 블럭 수를 의미하는 in_use를 1 감소시킨다.
  • 코드 라인 55에서 할당 해제한 블럭의 첫 4바이트에 기존에 page->offset 값으로 치환한다.
  • 코드 라인 56에서 page->offset 값은 다시 할당 해제한 블럭에 대한 offset 값을 지정한다.

 

다음 그림은 할당된 3 개의 DMA 블럭 중 가운데에 있는 DMA 블럭을 할당해제하는 경우를 보여준다.

 

pool_find_page()

mm/dmapool.c

static struct dma_page *pool_find_page(struct dma_pool *pool, dma_addr_t dma)
{
        struct dma_page *page;

        list_for_each_entry(page, &pool->page_list, page_list) {
                if (dma < page->dma)
                        continue;
                if ((dma - page->dma) < pool->allocation)
                        return page;
        }
        return NULL;
}

DMA 블럭 주소가 위치한 DMA 페이지를 검색한다.

 

다음 그림은 DMA pool에 등록된 3개의 DMA 페이지를 검색하는 모습을 보여준다.

 

참고

DMA -2- (DMA Coherent Memory)

<kernel v5.0>

DMA -2- (DMA Coherent Memory)

DMA coherent 메모리 할당은 아키텍처 및 플랫폼마다 다르다. 다음 몇 가지 예를 들어본다.

  • 디바이스 전용 메모리가 구성되어 있고, Custom code 또는 디바이스 트리에서 지정한 reserved 메모리 영역을 DMA 영역으로 지정하고, 이 곳에서 할당하는 방법
    • TCM(Tightly Coupled Memory, SRAM)이 제공되는 일부 시스템에서 이 영역을 특정 디바이스의 DMA 영역으로 사용
    • 고속 네트워크 디바이스 등이 패킷 퍼버로 SRAM 등을 제공하고, 이 영역을 DMA 영역으로 사용
    • 시스템 메모리의 일부를 DMA 영역으로 지정하고 사용
  • 시스템 메모리의 CMA 영역 또는 시스템 메모리에서 DMA 메모리를 할당하는 방법
  • 시스템 메모리의 HIGHMEM zone의 메모리를 유저 메모리에 할당하고 DMA 용도로 사용하는 방법

 

DMA coherent 메모리 할당

DMA coherent 메모리를 할당하는 방법은 다음과 같이 3가지 방법으로 나뉜다.

  • Generic cohernet per-device memory
    • reserved 메모리를 디바이스 전용 coherent 메모리로 사용하는 방법
  • Direct DMA
    • 시스템 메모리의 물리 주소에 Direct로 연결된 디바이스가 시스템 메모리의 일부를 DMA coherent 메모리로 사용하는 방법
  • IOMMU
    • IOMMU를 통한 디바이스가 시스템 메모리의 일부를 DMA coherent 메모리로 사용하는 방법

 

다음 그림은 DMA coherent 메모리 할당에 관련된 함수들의 호출 관계이다.

 

DMA coherent 메모리 할당 API

dma_alloc_coherent()

include/linux/dma-mapping.h

static inline void *dma_alloc_coherent(struct device *dev, size_t size,
                dma_addr_t *dma_handle, gfp_t gfp)
{
        return dma_alloc_attrs(dev, size, dma_handle, gfp,
                        (gfp & __GFP_NOWARN) ? DMA_ATTR_NO_WARN : 0);
}

DMA에 사용하기 위해 coherent 메모리를 size 만큼 할당해온다. 성공하는 경우 할당한 가상 주소가 반환된다.

 

dma_alloc_attrs()

kernel/dma/mapping.c

void *dma_alloc_attrs(struct device *dev, size_t size, dma_addr_t *dma_handle,
                gfp_t flag, unsigned long attrs)
{
        const struct dma_map_ops *ops = get_dma_ops(dev);
        void *cpu_addr;

        WARN_ON_ONCE(dev && !dev->coherent_dma_mask);

        if (dma_alloc_from_dev_coherent(dev, size, dma_handle, &cpu_addr))
                return cpu_addr;

        /* let the implementation decide on the zone to allocate from: */
        flag &= ~(__GFP_DMA | __GFP_DMA32 | __GFP_HIGHMEM);

        if (!arch_dma_alloc_attrs(&dev))
                return NULL;

        if (dma_is_direct(ops))
                cpu_addr = dma_direct_alloc(dev, size, dma_handle, flag, attrs);
        else if (ops->alloc)
                cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs);
        else
                return NULL;

        debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr);
        return cpu_addr;
}
EXPORT_SYMBOL(dma_alloc_attrs);

DMA에 사용하기 위해 coherent 메모리를 size 만큼 할당해온다. 디바이스전용 dma pool이 제공되는 경우 우선 할당한다. 없는 경우 시스템 메모리를 DMA direct 또는 IOMMU 매핑을 사용하여 할당해온다. 성공하는 경우 할당한 가상 주소가 반환된다.

  • 코드 라인 9~10에서 디바이스 전용 coherent pool이 제공되는 경우 요청한 사이즈만큼 메모리를 할당한다. generic 할당이 성공한 경우 할당한 가상 주소를 결과로 함수를 빠져나간다.
    • DMA_MEMORY_EXCLUSIVE 플래그를 사용할 때 디바이스 전용 dma 영역보다 큰 페이지 할당 요청을 하여 할당을 못하는 경우가 있다. 이러한 경우 generic DMA 할당을 시도하도록 유도하게 거짓 성공(1)으로 반환한다. 출력 인자 cpu_addr는 null로 반환한다.
  • 코드 라인 13에서 zone 선택 관련 gfp 플래그에서 DMA, DMA32 및 HIGHMEM 영역을 제외한다.
  • 코드 라인 15~16에서 아키텍처 specific한 dma 할당이 구현되어 있지 않으면 실패로 함수를 빠져나간다.
    • x86 시스템의 경우에만 디바이스별로 판단한다. 그외의 아키텍처는 default로 항상 true이다.
  • 코드 라인 18~26에서 direct dma 방식 또는 IOMMU 방식으로 할당해온다.
    • IOMMU는 iommu 디바이스 드라이버에 구현된 dma_map_ops 구조체의 멤버 (*alloc) 함수를 호출하여 할당해온다.

 

Generic per-device DMA coherent 메모리

arm, arm64, x86, mips등 일부 아키텍처는 DMA coherent 메모리 할당을 위해 generic한 구현 코드를 지원한다.

  • CONFIG_HAVE_GENERIC_DMA_COHERENT 커널 옵션 사용
  • dma_coherent_mem 구조체를 사용하여 구현된다.
  • 디바이스 트리를 통해 지정된 reserved-memory 영역을 DMA coherent 메모리로 사용할 수 있다.
  • 지정된 영역은 DMA coherent 메모리 영역으로 비트맵으로 관리되며 1비트는 1페이지의 할당 여부를 관리한다.

 

dma_coherent_mem 구조체

kernel/dma/coherent.c

struct dma_coherent_mem {
        void            *virt_base;
        dma_addr_t      device_base;
        unsigned long   pfn_base;
        int             size;
        int             flags;
        unsigned long   *bitmap;
        spinlock_t      spinlock;
        bool            use_dev_dma_pfn_offset;
};
  • *virt_base
    • per-device DMA coherent 메모리가 매핑된 cpu 가상 주소
  • device_base
    • per-device DMA coherent 메모리의 cpu 물리 주소
  • pfn_base
    • per-device DMA coherent 메모리가 위치한 cpu 물리 주소위의 pfn
  • size
    • 사이즈
  • flags
    • DMA_MEMORY_EXCLUSIVE
      • 디바이스 전용 dma coherent 메모리를 할당
    • 그 외 플래그는 해당 드라이버의 custom 플래그
  • *bitmap
    • 비트맵으로 각 bit는 페이지의 할당 여부를 관리한다.
  • spinlock
    • dma coherent 메모리 할당/할당 해제 시 동기화를 위하여 사용한다.
  • use_dev_dma_pfn_offset
    • 디바이스 트리의 reserved-memory 영역 지정을 통해 dma 메모리가 지정된 경우 사용된다.
    • 디바이스에 지정된 dma_pfn_offset을 사용한다.

 

dma_alloc_from_dev_coherent()

kernel/dma/coherent.c

/**
 * dma_alloc_from_dev_coherent() - allocate memory from device coherent pool
 * @dev:        device from which we allocate memory
 * @size:       size of requested memory area
 * @dma_handle: This will be filled with the correct dma handle
 * @ret:        This pointer will be filled with the virtual address
 *              to allocated area.
 *
 * This function should be only called from per-arch dma_alloc_coherent()
 * to support allocation from per-device coherent memory pools.
 *
 * Returns 0 if dma_alloc_coherent should continue with allocating from
 * generic memory areas, or !0 if dma_alloc_coherent should return @ret.
 */
int dma_alloc_from_dev_coherent(struct device *dev, ssize_t size,
                dma_addr_t *dma_handle, void **ret)
{
        struct dma_coherent_mem *mem = dev_get_coherent_memory(dev);

        if (!mem)
                return 0;

        *ret = __dma_alloc_from_coherent(mem, size, dma_handle);
        if (*ret)
                return 1;

        /*
         * In the case where the allocation can not be satisfied from the
         * per-device area, try to fall back to generic memory if the
         * constraints allow it.
         */
        return mem->flags & DMA_MEMORY_EXCLUSIVE;
}

디바이스 전용 coherent pool에서 요청한 사이즈만큼 메모리를 할당한다. 결과가 0인 경우 할당이 실패한 경우이다.

  • 코드 라인 4~7에서 디바이스에 지정된 coherent 메모리를 가져온다.
  • 코드 라인 9~11에서 coherent 메모리에서 size 만큼을 할당해온다.
  • 코드 라인 18에서 DMA_MEMORY_EXCLUSIVE 옵션을 사용한 경우에 1을 반환한다.

 

__dma_alloc_from_coherent()

kernel/dma/coherent.c

static void *__dma_alloc_from_coherent(struct dma_coherent_mem *mem,
                ssize_t size, dma_addr_t *dma_handle)
{
        int order = get_order(size);
        unsigned long flags;
        int pageno;
        void *ret;

        spin_lock_irqsave(&mem->spinlock, flags);

        if (unlikely(size > (mem->size << PAGE_SHIFT)))
                goto err;

        pageno = bitmap_find_free_region(mem->bitmap, mem->size, order);
        if (unlikely(pageno < 0))
                goto err;

        /*
         * Memory was found in the coherent area.
         */
        *dma_handle = mem->device_base + (pageno << PAGE_SHIFT);
        ret = mem->virt_base + (pageno << PAGE_SHIFT);
        spin_unlock_irqrestore(&mem->spinlock, flags);
        memset(ret, 0, size);
        return ret;
err:
        spin_unlock_irqrestore(&mem->spinlock, flags);
        return NULL;
}

디바이스 전용 dma coherent 메모리 영역에서 요청한 size 만큼 coherent 메모리를 할당하고 그 가상 주소를 반환한다.

  • 코드 라인 4에서 size를 order 페이지로 환산한다.
    • 예) size=8192 (페이지=4K)
      • order=1
  • 코드 라인 11~12에서 dma 메모리 영역을 벗어나는 size 요청인 경우 에러(0)를 결과로 함수를 빠져나간다.
  • 코드 라인 14~16에서 dma 메모리 영역을 비트맵으로 관리하는데, 이 비트맵에서 order 페이지 수 만큼의 빈 페이지 공간을 찾는다.
    • 비트맵에서 1개의 비트는 1페이지의 할당 여부를 나타낸다.
  • 코드 라인 21~25에서 찾은 페이지 번호에 해당하는 가상 주소를 반환한다.
    • 예) mem->virt_base=0xffff_fff8_2000_0000, pageno=8 (페이지=4K)
      • ret=0xffff_fff8_2000_8000

 

direct-DMA 메모리 할당

디바이스가 IOMMU를 통해 시스템 메모리에 연결되지 않은 경우에 사용된다.

  • 디바이스 트리를 통해 reserved-memory 영역을 CMA 영역으로 선언하고 이를 이용하는 방법도 있고, CMA 영역이 아닌 일반 시스템 메모리를 할당해올 수 있다.

 

dma_direct_alloc()

kernel/dma/direct.c

void *dma_direct_alloc(struct device *dev, size_t size,
                dma_addr_t *dma_handle, gfp_t gfp, unsigned long attrs)
{
        if (!dev_is_dma_coherent(dev))
                return arch_dma_alloc(dev, size, dma_handle, gfp, attrs);
        return dma_direct_alloc_pages(dev, size, dma_handle, gfp, attrs);
}

시스템 메모리에서 주소 변환 없이 direct 매핑된 dma용 메모리를 할당해온다.

  • 코드 라인 4~5에서 dma coherent 기능을 사용할 수 있는 디바이스가 아니면 architecture 고유의 dma 할당을 시도하고 coherent 매핑을 수행한다.
  • 코드 라인 6에서 dma coherent 기능을 사용할 수 있는 디바이스인 경우이다. atomic 할당이 아닌 경우 cma영역에서 할당을 시도한다. 그렇지 않은 경우 연속된 페이지를 버디 시스템에서 할당해온다.

 

arch_dma_alloc()

kernel/dma/remap.c

void *arch_dma_alloc(struct device *dev, size_t size, dma_addr_t *dma_handle,
                gfp_t flags, unsigned long attrs)
{
        struct page *page = NULL;
        void *ret;

        size = PAGE_ALIGN(size);

        if (!gfpflags_allow_blocking(flags) &&
            !(attrs & DMA_ATTR_NO_KERNEL_MAPPING)) {
                ret = dma_alloc_from_pool(size, &page, flags);
                if (!ret)
                        return NULL;
                goto done;
        }

        page = __dma_direct_alloc_pages(dev, size, dma_handle, flags, attrs);
        if (!page)
                return NULL;

        /* remove any dirty cache lines on the kernel alias */
        arch_dma_prep_coherent(page, size);

        if (attrs & DMA_ATTR_NO_KERNEL_MAPPING) {
                ret = page; /* opaque cookie */
                goto done;
        }

        /* create a coherent mapping */
        ret = dma_common_contiguous_remap(page, size, VM_USERMAP,
                        arch_dma_mmap_pgprot(dev, PAGE_KERNEL, attrs),
                        __builtin_return_address(0));
        if (!ret) {
                __dma_direct_free_pages(dev, size, page);
                return ret;
        }

        memset(ret, 0, size);
done:
        *dma_handle = phys_to_dma(dev, page_to_phys(page));
        return ret;
}

architecture 고유의 dma 메모리를 할당하고 디폴트로 coherenet 매핑을 한다.

  • 코드 라인 9~15에서 블럭킹되면 안되는 atomic 할당 요청이고, 아직 커널 매핑이 없어 커널 매핑이 필요한 경우이다. 이 경우 빠른 dma 메모리 할당을 위해 atomic dma pool 영역을 사용하여 할당한다.
    • DMA_ATTR_NO_KERNEL_MAPPING
      • 할당 DMA 버퍼가 이미 매핑되어 DMA 메모리 할당 후 커널 매핑이 필요 없는 경우에 사용한다.
  • 코드 라인 17~19에서 DMA Direct 매핑(버스 주소와 CPU 물리 주소가 같거나 일률적으로 변환가능)된 메모리를 할당해온다.
  • 코드 라인 22에서 아키텍처별로 이 DMA 영역을 사용하기 전에 캐시 flush하도록 한다.
    • arm64의 경우 해당 페이지들 영역에 대한 명령 및 데이터 캐시에 대해 clean & invalidate를 하도록 한다.
  • 코드 라인 24~27에서 DMA_ATTR_NO_KERNEL_MAPPING 속성을 사용한 경우 커널에서 별도의 매핑 동작을 하지 않고 할당된 dma 페이지의 가상 주소를 결과로 함수를 빠져나간다.
  • 코드 라인 30~36에서 coherent 매핑을 수행한다. 할당 받은 연속된 페이지의 dma 페이지들을 vmalloc 공간의 연속된 가상 주소로 매핑한다.
    • coherent 매핑이 사용된 경우 스트리밍 매핑을 사용하지 않고도 cpu에서 기록한 내용이 device에서도 잘 읽히고, 그 반대도 잘 동작한다.
  • 코드 라인 38~41에서 할당받은 메모리를 0으로 초기화하고, dma 주소를 @dma_handle에 저장하고 dma v페이지의 가상 주소를 결과로 함수를 정상 종료한다.

 

dma_direct_alloc_pages()

kernel/dma/direct.c”

void *dma_direct_alloc_pages(struct device *dev, size_t size,
                dma_addr_t *dma_handle, gfp_t gfp, unsigned long attrs)
{
        struct page *page;
        void *ret;

        page = __dma_direct_alloc_pages(dev, size, dma_handle, gfp, attrs);
        if (!page)
                return NULL;

        if (PageHighMem(page)) {
                /*
                 * Depending on the cma= arguments and per-arch setup
                 * dma_alloc_from_contiguous could return highmem pages.
                 * Without remapping there is no way to return them here,
                 * so log an error and fail.
                 */
                dev_info(dev, "Rejecting highmem page from CMA.\n");
                __dma_direct_free_pages(dev, size, page);
                return NULL;
        }

        ret = page_address(page);
        if (force_dma_unencrypted()) {
                set_memory_decrypted((unsigned long)ret, 1 << get_order(size));
                *dma_handle = __phys_to_dma(dev, page_to_phys(page));
        } else {
                *dma_handle = phys_to_dma(dev, page_to_phys(page));
        }
        memset(ret, 0, size);
        return ret;
}

dma direct 방식으로 페이지를 size 만큼 할당해온다.  할당해온 페이지가 highmem인 경우 할당을 해제하고 null을 반환한다.

  • 코드 라인 7~9에서 dma direct 방식으로 페이지를 size 만큼 할당해온다.
  • 코드 라인 11~21에서 할당된 페이지가 highmem 페이지인 경우 할당을 해제하고 null을 반환한다.
  • 코드 라인 23~31에서 할당 영역의 메모리 decrytion을 수행한다.

 

__dma_direct_alloc_pages()

kernel/dma/direct.c

struct page *__dma_direct_alloc_pages(struct device *dev, size_t size,
                dma_addr_t *dma_handle, gfp_t gfp, unsigned long attrs)
{
        unsigned int count = PAGE_ALIGN(size) >> PAGE_SHIFT;
        int page_order = get_order(size);
        struct page *page = NULL;
        u64 phys_mask;

        if (attrs & DMA_ATTR_NO_WARN)
                gfp |= __GFP_NOWARN;

        /* we always manually zero the memory once we are done: */
        gfp &= ~__GFP_ZERO;
        gfp |= __dma_direct_optimal_gfp_mask(dev, dev->coherent_dma_mask,
                        &phys_mask);
again:
        /* CMA can be used only in the context which permits sleeping */
        if (gfpflags_allow_blocking(gfp)) {
                page = dma_alloc_from_contiguous(dev, count, page_order,
                                                 gfp & __GFP_NOWARN);
                if (page && !dma_coherent_ok(dev, page_to_phys(page), size)) {
                        dma_release_from_contiguous(dev, page, count);
                        page = NULL;
                }
        }
        if (!page)
                page = alloc_pages_node(dev_to_node(dev), gfp, page_order);

        if (page && !dma_coherent_ok(dev, page_to_phys(page), size)) {
                __free_pages(page, page_order);
                page = NULL;

                if (IS_ENABLED(CONFIG_ZONE_DMA32) &&
                    phys_mask < DMA_BIT_MASK(64) &&
                    !(gfp & (GFP_DMA32 | GFP_DMA))) {
                        gfp |= GFP_DMA32;
                        goto again;
                }

                if (IS_ENABLED(CONFIG_ZONE_DMA) &&
                    phys_mask < DMA_BIT_MASK(32) && !(gfp & GFP_DMA)) {
                        gfp = (gfp & ~GFP_DMA32) | GFP_DMA;
                        goto again;
                }
        }

        return page;
}

dma direct 방식으로 페이지를 size 만큼 할당해온다. atomic 할당 요청이 아닌 경우 cma 영역에서 할당한다. 그렇지 못한 경우 버디 시스템을 통해 할당해온다. 할당이 실패한 경우 null을 반환한다.

  • 코드 라인 9-10에서 DMA_ATTR_NO_WARN 속성을 사용한 메모리 요청 시 warnning 에러를 발생하지 않게한다.
  • 코드 라인 13에서GFP_ZERO 플래그를 제거한다.
  • 코드 라인 14~15에서 dma 할당을 위해 offset과 해당 zone 선택 비트를 추가한 gfp를 얻어온다.
  • 코드 라인 18~25에서 atomic 할당이 요청이 아니어서 blocking 가능한 경우 cma 영역에서 페이지 할당을 해온다. 만일 할당받은 해당 페이지가 coherent가 가능하지 않은 경우 cma 영역에서 할당한 페이지들을 다시 되돌린다.
  • 코드 라인 26~27에서 마지막으로 시도할 곳은 버디 시스템이다.
  • 코드 라인 29~31에서 버디 시스템에서 할당한 페이지가 coherent가 가능하지 않은 경우 다시 버디 시스템으로 되돌린다.
  • 코드 라인 33~45에서 zone DMA32 또는 zone DMA 에서 다시 시도해본다.

 

per-device DMA coherent 메모리 영역 지정

디바이스 전용 DMA coherent 메모리 영역을 지정하는 방법은 다음 그림과 같이 두 가지가 있다.

  • 1) Custom 드라이버에서 dma_declare_coherent_memory() 함수를 사용하여 등록한다.
  • 2) 디바이스 트리를 통해 등록한다.

 

dma_declare_coherent_memory()

kernel/dma/coherent.c

int dma_declare_coherent_memory(struct device *dev, phys_addr_t phys_addr,
                                dma_addr_t device_addr, size_t size, int flags)
{
        struct dma_coherent_mem *mem;
        int ret;

        ret = dma_init_coherent_memory(phys_addr, device_addr, size, flags, &mem);
        if (ret)
                return ret;

        ret = dma_assign_coherent_memory(dev, mem);
        if (ret)
                dma_release_coherent_memory(mem);
        return ret;
}
EXPORT_SYMBOL(dma_declare_coherent_memory);

디바이스 전용 dma coherent 메모리 영역을 지정하는 generic 코드이다.

  • 코드 라인 7~9에서 인자로 전달받은 물리 주소 영역을 write combine 매핑하고, 관리할 수 있도록 dma_coherent_mem 구조체를 할당하고 설정한다.
  • 코드 라인 11~13에서 매핑과 dma_coherent_mem 할당이 성공한 경우 디바이스에 할당된 정보를 지정한다.
    • dev->dma_mem = <할당된 dma_coherent_mem 구조체 주소>

 

dma_init_coherent_memory()

kernel/dma/coherent.c

static int dma_init_coherent_memory(
        phys_addr_t phys_addr, dma_addr_t device_addr, size_t size, int flags,
        struct dma_coherent_mem **mem)
{
        struct dma_coherent_mem *dma_mem = NULL;
        void __iomem *mem_base = NULL;
        int pages = size >> PAGE_SHIFT;
        int bitmap_size = BITS_TO_LONGS(pages) * sizeof(long);
        int ret;

        if (!size) {
                ret = -EINVAL;
                goto out;
        }

        mem_base = memremap(phys_addr, size, MEMREMAP_WC);
        if (!mem_base) {
                ret = -EINVAL;
                goto out;
        }
        dma_mem = kzalloc(sizeof(struct dma_coherent_mem), GFP_KERNEL);
        if (!dma_mem) {
                ret = -ENOMEM;
                goto out;
        }
        dma_mem->bitmap = kzalloc(bitmap_size, GFP_KERNEL);
        if (!dma_mem->bitmap) {
                ret = -ENOMEM;
                goto out;
        }

        dma_mem->virt_base = mem_base;
        dma_mem->device_base = device_addr;
        dma_mem->pfn_base = PFN_DOWN(phys_addr);
        dma_mem->size = pages;
        dma_mem->flags = flags;
        spin_lock_init(&dma_mem->spinlock);

        *mem = dma_mem;
        return 0;

out:
        kfree(dma_mem);
        if (mem_base)
                memunmap(mem_base);
        return ret;
}

인자로 전달받은 물리 주소 영역을 write combine 매핑하고, 관리할 수 있도록 dma coherent_mem 구조체를 할당하고 설정한다.

  • 코드 라인 11~14에서 size가 0인 경우 -EINVAL 결과로 함수를 빠져나간다.
  • 코드 라인 16~20에서 인자로 전달받은 물리 주소 영역을 write combine 매핑을 한다.
  • 코드 라인 21~30에서 dma_coherent_mem 구조체를 할당받고, 멤버 bitmap에 각 1비트가 1페이지를 관리할 수 있는 비트맵을 할당받아 대입한다.
  • 코드 라인 32~40에서 나머지 멤버 변수들을 대입하고 정상 결과(0)를 반환한다.

 


디바이스 트리의 DMA reserved 메모리 지원

  • CONFIG_OF_RESERVED_MEM 커널 옵션 사용
  • compatible = “shared-dma-pool”; 명시
  • ARM 아키텍처에서는 “linux,dma-default” 속성을 쓰면 이 영역을 per-device가 아닌 디폴트 영역으로 사용하게 할 수도 있다.
    • 거의 사용하지 않는다.
  • no-map 속성을 지정하여야 하며, 이 영역을 초기화할 때 coherent 메모리 속성을 얻을 수 있도록 write-combine 매핑을 이용한다.

 

다음 그림은 지정된 특정 영역을 DMA coherent 메모리 영역으로 등록하는 모습을 보여준다.

  • reserved-memory의 dma 메모리 영역이 no-map으로 되어 있음을 알 수 있으며, 이 메모리를 사용하는 사용자 디바이스 드라이버에서 이 메모리를 사용하기 전에 of_reserved_mem_device_init() 함수 등을 통해 write-combine 매핑하여 초기화한다.

 

사용자 Device Driver에서 전용(per-device) coherent 메모리 사용

 

디바이스 트리 샘플

foo.dts

        ...

        reserved-memory {
                #address-cells = <2>;
                #size-cells = <2>;
                ranges;

                linux,cma {
                        compatible = "shared-dma-pool";
                        reusable;
                        reg = <0x0 0x50000000 0x0 0x01000000>;
                        linux,cma-default;
                };

                foo_mem: foo_mem {
                        compatible = "shared-dma-pool";
                        reg = <0x0 0x60000000 0x0 0x01000000>;
                        alignment = <0 0x1000000>;
                        no-map;
                };
        };

        /* foo_dev use foo_mem */
        foo_dev: foo_dev {
              compatible = "foo,foo-dev";
              memory-region = <&foo_mem>;
        };
};

 

사용자 드라이버 샘플

foo.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/string.h>
#include <linux/version.h>      /* LINUX_VERSION_CODE & KERNEL_VERSION() */
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/dma-mapping.h>
#include <linux/dma-direct.h>
#include <linux/of_reserved_mem.h>

#define DMA_ALLOC_SIZE  (2 * 4096)
struct foo {
        struct platform_device *pdev;
        void *virt_addr;
        phys_addr_t phys_addr;
        dma_addr_t dma_addr;
};

typedef struct foo foo_t;

static int foo_probe(struct platform_device *pdev)
{
        foo_t *foo;

        foo = devm_kzalloc(&pdev->dev, sizeof(foo_t), GFP_KERNEL);
        if (foo == NULL)
                return -1;

        /* set private data */
        foo->pdev = pdev;
        platform_set_drvdata(pdev, foo);

        /* select foo-dev's memory */
        of_reserved_mem_device_init(&pdev->dev);

        /* test for dma buffer allocation */
        foo->virt_addr = dmam_alloc_coherent(&pdev->dev, DMA_ALLOC_SIZE, 
                        &foo->dma_addr, GFP_KERNEL);
        if (foo->virt_addr == NULL)
                return -2;

        foo->phys_addr = dma_to_phys(&pdev->dev, foo->dma_addr);

        printk("%s: dma=%llx, xphys=%llx, virt=%llx\n", __func__,
                        (uint64_t) foo->dma_addr, (uint64_t) foo->phys_addr,
                        (uint64_t) foo->virt_addr);
        return 0;
}

static int foo_remove(struct platform_device *pdev)
{
        foo_t *foo = (foo_t *)platform_get_drvdata(pdev);

        dmam_free_coherent(&pdev->dev, DMA_ALLOC_SIZE, 
                        foo->virt_addr, foo->dma_addr);

        printk("%s\n", __func__);

        return 0;
}

static const struct of_device_id of_foo_match[] = {
                { .compatible = "foo,foo-dev", },
                {},
};
MODULE_DEVICE_TABLE(of, of_foo_match);

static struct platform_driver foo_driver = {
        .driver = {
                .name = "foo_driver",
                .of_match_table = of_match_ptr(of_foo_match),
        },
        .probe          = foo_probe,
        .remove         = foo_remove,
};

module_platform_driver(foo_driver);
MODULE_LICENSE("GPL");
  • of_reserved_mem_device_init() 명령을 사용하면 해당 디바이스가 디바이스 트리의 memory-region = <&foo_mem>;을 파싱하여 해당 foo_mem 노드 정보를 찾아 해당 메모리를 사용할 수 있게 한다.
  • dma_alloc_coherent() API를 사용하여도 되지만, managed API인 dmam_alloc_coherent()를 사용하면 드라이버 unload 시에 실수로 free 되지 않은 메모리도 자동으로 할당 해제되지 사용하기 편하다. 결국 위 코드에서 dmam_free_coherent()를 삭제해도 된다.

 

실행 예)

$ insmod foo.ko
foo_driver foo_dev: assigned reserved memory node foo_mem
foo_probe: dma=60000000, xphys=60000000, virt=ffff800015000000

$ rmmod foo
foo_remove

 


RESERVEDMEM_OF_DECLARE() 매크로

 

RESERVEDMEM_OF_DECLARE() 매크로를 사용한 소스들은 다음과 같고, 그 중 “shared-dma-pool”을 사용한 곳은 rmem_dma_setup()과 rmem_cma_setup() 함수이다.

./drivers/soc/fsl/qbman/bman_ccsr.c:156:RESERVEDMEM_OF_DECLARE(bman_fbpr, "fsl,bman-fbpr", bman_fbpr);
./drivers/soc/fsl/qbman/qman_ccsr.c:480:RESERVEDMEM_OF_DECLARE(qman_fqd, "fsl,qman-fqd", qman_fqd);
./drivers/soc/fsl/qbman/qman_ccsr.c:491:RESERVEDMEM_OF_DECLARE(qman_pfdr, "fsl,qman-pfdr", qman_pfdr);
./drivers/memory/tegra/tegra210-emc-table.c:89:RESERVEDMEM_OF_DECLARE(tegra210_emc_table, "nvidia,tegra210-emc-table",
./kernel/dma/coherent.c:477:RESERVEDMEM_OF_DECLARE(dma, "shared-dma-pool", rmem_dma_setup);
./kernel/dma/swiotlb.c:888:RESERVEDMEM_OF_DECLARE(dma, "restricted-dma-pool", rmem_swiotlb_setup);
./kernel/dma/contiguous.c:598:RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup);

 

kernel/dma/coherent.c

RESERVEDMEM_OF_DECLARE(dma, "shared-dma-pool", rmem_dma_setup);

위의 RESERVEDMEM_OF_DECLARE() 매크로는 compatible 명으로 “shared-dma-pool”과 rmem_dma_setup() 함수를 __reservedmem_of_table에 등록한다.

  • 이렇게 등록된 테이블 정보는 fdt_init_reserved_mem() 함수에 의해 등록한 compatible 명이 디바이스 트리 노드에 존재하는 경우 대응하는 rmem_dma_setup() 함수를 호출한다.

 

kernel/dma/contiguous.c

RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup);

위의 RESERVEDMEM_OF_DECLARE() 매크로는 compatible 명으로 “shared-dma-pool”과 rmem_cma_setup() 함수를 __reservedmem_of_table에 등록한다.

  • 이렇게 등록된 테이블 정보는 fdt_init_reserved_mem() 함수에 의해 등록한 compatible 명이 디바이스 트리 노드에 존재하는 경우 대응하는 rmem_dma_setup() 함수를 호출한다.
  • rmem_cma_setup() 함수의 분석은 다음 문서의 후반부를 참고한다.

 

rmem_dma_setup()

디바이스 트리의 reserved-memory  노드에 등록된 child 노드들 중 compatible = “shared-dma-pool”; 으로 등록된 child 노드가 있는 경우 rmem_dma_setup()과 rmem_cma_setup() 함수가 호출된다.

 

kernel/dma/coherent.c

static int __init rmem_dma_setup(struct reserved_mem *rmem)
{
        unsigned long node = rmem->fdt_node;

        if (of_get_flat_dt_prop(node, "reusable", NULL))
                return -EINVAL;

#ifdef CONFIG_ARM
        if (!of_get_flat_dt_prop(node, "no-map", NULL)) {
                pr_err("Reserved memory: regions without no-map are not yet supported\n");
                return -EINVAL;
        }

        if (of_get_flat_dt_prop(node, "linux,dma-default", NULL)) {
                WARN(dma_reserved_default_memory,
                     "Reserved memory: region for default DMA coherent area is redefined\n");
                dma_reserved_default_memory = rmem;
        }
#endif

        rmem->ops = &rmem_dma_ops;
        pr_info("Reserved memory: created DMA memory pool at %pa, size %ld MiB\n",
                &rmem->base, (unsigned long)rmem->size / SZ_1M);
        return 0;
}

reserved 메모리의 ops에 아래 rmem_dma_ops를 추가한다.

  • 코드 라인 5~6에서 reserved memory 노드에서 “reusable” 속성은 허용하지 않는다.
  • 코드 라인 8~19에서 32비트 ARM 아키텍처에서는 “no-map” 속성이 없는 경우는 아직 지원하지 않는다. 또한 “linux,dma-default” 속성이 있는 경우 default로 지정한다.
  • 코드 라인 21에서 reserved 메모리의 ops에 아래 rmem_dma_ops를 추가한다.

 

아래의 reserved-memory에 대한 operation은 사용자 디바이스 드라이버 코드에서 of_reserved_mem_device_init() 함수 등에 의해 호출된다.

kernel/dma/coherent.c

static const struct reserved_mem_ops rmem_dma_ops = {
        .device_init    = rmem_dma_device_init,
        .device_release = rmem_dma_device_release,
};

 

rmem_dma_device_init()

kernel/dma/coherent.c

static int rmem_dma_device_init(struct reserved_mem *rmem, struct device *dev)
{
        struct dma_coherent_mem *mem = rmem->priv;
        int ret;

        if (!mem) {
                ret = dma_init_coherent_memory(rmem->base, rmem->base,
                                               rmem->size,
                                               DMA_MEMORY_EXCLUSIVE, &mem);
                if (ret) {
                        pr_err("Reserved memory: failed to init DMA memory pool at %pa, size %ld MiBB
\n",
                                &rmem->base, (unsigned long)rmem->size / SZ_1M);
                        return ret;
                }
        }
        mem->use_dev_dma_pfn_offset = true;
        rmem->priv = mem;
        dma_assign_coherent_memory(dev, mem);
        return 0;
}

인자로 전달받은 reserved 메모리 정보에 있는 dma_coherent_mem 정보를 사용하여 generic per-device dma coherent 메모리 영역을 지정한다.

 

dma_init_reserved_memory()

kernel/dma/coherent.c

static int __init dma_init_reserved_memory(void)
{
        const struct reserved_mem_ops *ops;
        int ret;

        if (!dma_reserved_default_memory)
                return -ENOMEM;

        ops = dma_reserved_default_memory->ops;

        /*
         * We rely on rmem_dma_device_init() does not propagate error of
         * dma_assign_coherent_memory() for "NULL" device.
         */
        ret = ops->device_init(dma_reserved_default_memory, NULL);

        if (!ret) {
                dma_coherent_default_memory = dma_reserved_default_memory->priv;
                pr_info("DMA: default coherent area is set\n");
        }

        return ret;
}

core_initcall(dma_init_reserved_memory);

dma_reserved_default_memory가 설정된 경우에만 호출되는 함수이다.

  • 현재 32비트 ARM 시스템에서 default 옵션을 사용할 수 있게 하였는데, 거의 사용되지 않는다.

 

참고

DMA -1- (Basic)

<kernel v5.0>

DMA(Direct Memory Access) -1- (Basic)

디바이스로 부터 데이터를  메모리에 읽어들일 때 DMA를 사용하지 않는 방법과 DMA를 사용하는 방법을 보여준다.

  • non-DMA
    • CPU가 디바이스로부터 직접 데이터를 읽거나 기록하는 것을 반복하여 cpu load가 상승한다.
  • DMA
    • CPU는 디바이스에게 DMA 시작 신호와 DMA 완료 인터럽트만 수신하고, 직접적인 전송에는 참여하지 않으므로 cpu load를 최소화 시킨다. 이 과정에서 디바이스가 직접 메모리에 접근하여 읽거나 기록한다.
    • 버스 아키텍처에 DMA 컨트롤러 h/w가 구성되어 있어야 한다.
      • ISA, EISA, AXI, AHB, …
      • 참고로 PCI 및 PCIe를 통한 DMA는 아래 그림의 DMA 컨트롤러와 동작 방법이 다르다.
        • CPU가 관여하지 않고(리눅스 커널 프로그래머가 하지 않고) , pci 디바이스가 pci 컨트롤러에 버스 마스터를 요청하여 버스를 소유한 후 pci 디바이스 내부에 있는 dma 컨트롤러를 통해 주도하여 직접 DMA한다. (많은 pci/pcie 디바이스들은 dma 기능을 내장하고 있다)

 

참고: DMA를 사용하면 cpu 커널 드라이버가 사용하는 cpu 사용률을 낮춰 유저 application등에 더 많은 cpu를 주어지게 하는데 가장 큰 장점이 있다. 그러나 많은 인터럽트가 발생되는 네트워크 디바이스 등 고성능 처리를 요구하는 디바이스 드라이버들은 NAPI가 사용되고 있는데, 커널 드라이버에서 cpu를 사용해서라도 polling 하여 처리를 하고 있다. 또한 10G 네트웍 및 infiniband 네트워크 장치 등에서 더 빠른 처리를 위해 유저 모드에서 polling하여 동작시킬 수 있는  방법들을 사용하고 있다. (예: DPDK, infiniband)

 

DMA 주소 체계

디바이스가 DMA 방식으로 접근할 메모리(Buffer)와 관련된 세 가지 주소 공간 유형을 알아본다.

  • CPU 가상 주소 공간
    • CPU가 사용하는 주소 공간
  • CPU 물리 주소 공간
    • CPU 가상 주소가 MMU에 의해 물리 주소로 변환하여 사용하는 주소 공간
  • 버스 주소 공간
    • 디바이스가 사용하는 주소 공간

 

다음 그림과 같이 CPU와 디바이스가 바라보는 주소 공간이 다름을 알 수 있다.

  • 64비트 시스템이더라도 전력의 소모를 줄이기 위해 물리 주소 공간의 크기를 보통 줄여 설계한다.
  • 버스 주소 공간의 크기도 여러 가지 버스(AXI, AMBA, ISA, PCI,…)가 존재하므로 사용하는 비트 크기는 각각 다르다.
  • 특정 디바이스들은 버스 주소의 일부 주소 공간에만 접근하게 제약하기도 한다.

 

아래 그림은 호스트가 소유한 메모리와 디바이스가 소유한 메모리의 관계를 3 가지 주소 공간을 통해 보여준다.

  • Case A) 디바이스 메모리
    • 디바이스가 소유한 메모리, 레지스터 또는 PCI BAR 영역이 호스트 브리지에 의해 매핑되어 사용된다.
    • 이 영역은 ioremap() 등의 API를 통해 매핑하여 사용한다.
    • 대부분 /proc/iomem을 통해 영역을 확인할 수 있다.
    • 아래 PCI/PCIe with DMA 글을 참고한다.
  • Case B) 호스트 CPU 메모리 – 디바이스가 IOMMU를 통해 접근
    • 호스트 CPU의 메모리 영역에 DMA 버퍼 메모리를 제공한다.
    • 디바이스가 사용하는 버스 주소 공간은 IOMMU를 통해 호스트 CPU의 물리 주소 공간에 접근할 수 있다.
      • IOMMU 매핑을 통해 cpu용 페이지 테이블이 아닌 디바이스용 IOMMU 페이지 테이블이 생성된다.
  • Case C) 호스트 CPU 메모리 – 디바이스가 Direct 접근
    • 호스트 CPU의 메모리 영역에 DMA 버퍼 메모리를 제공한다.
    • 디바이스가 사용하는 버스 주소 공간과 호스트 CPU의 물리 주소 공간이 같거나, 일괄적으로 변경(DTB:”dma-ranges”)되는 경우 디바이스는 주소 변환을 위해 IOMMU를 사용할 필요가 없다. 따라서 디바이스는 Direct로 호스트 CPU의 물리 주소 공간에 위치한 DMA 버퍼에 접근할 수 있다.
      • 주소 변환에 대해서 아래의 Case C)와 같이 물리 주소와 dma 주소가  일치하는 것이 일반적이지만, 특정 시스템의 경우 일률적으로 변환되는 경우도 있다.

 

PCI/PCIe with DMA

1) 호스트 메모리에 DMA

 

다음 그림은 ARM SoC에 내장된 DMA-330 controlloer를 보여준다.

 

다음 그림은 pci 디바이스가 내장 메모리 없이 호스트에 위치한 메모리에 DMA하는 전통적인 모습을 보여준다. (인텔 North Bridge 칩셋에 연결되어 있는 DRAM을 사용하여 DMA한다)

  • pci/pcie 컨트롤러에 버스 마스터를 요청하여 버스를 점유한 후 시스템에 위치한 dma 컨트롤러릍 통해 pci/pcie 디바이스가 주도하여 호스트 메모리에 직접 DMA한다.

 

다음 그림은 pcie 장치가 내장 메모리 없이 호스트에 위치한 메모리에 DMA하는 전통적인 모습을 보여준다.

 

2) 디바이스 메모리에 DMA

  • pci/pcie 컨트롤러에 버스 마스터를 요청하여 버스를 점유한 후 pci/pcie 디바이스 내부에 있는 dma 컨트롤러를 통해 pci/pcie 디바이스가 주도하여 내부 메모리에 직접 DMA한다.

 

다음 그림은 Xilinx사의 DMA IP를 보여주며 자체 메모리에 DMA를 사용하는 1G 네트웍 장치에 대한 사례를 보여준다.

 

다음 그림은 Altera Megacore사의 레퍼런스 디자인이며, 좌측 슬레이브 측에 있는 메모리를 대상으로 DMA가 사용되고 있는 모습을 보여준다.

 


DMA 주소 변환

IOMMU 매핑 vs Direct 매핑

디바이스는 다음과 같이 두 가지 io 주소 매핑 방법으로 DMA 영역에 접근한다.

  • IOMMU 매핑
    • 디바이스가 사용하는 버스 주소를 IOMMU 장치를 통해 CPU의 물리 주소로 변환하는 경우 사용된다.
    • 오늘날 CPU와 GPU 및 내장 고속 네트웍 장치등이 포함된 SoC들은 별도의 IOMMU를 가지고 있다. gpu 및 내장 고속 네트웍 장치를 운영할 때 이러한 IOMMU를 사용하여 DMA 접근을 하며, 그 외의 장치들은 아래의 Direct 매핑을 사용한다.
  • Direct 매핑
    • 디바이스가 사용하는 버스 주소와 CPU의 물리 주소가 일치하거나, 일률적인 변환을 사용하는 경우이다.
      • 물리 주소와 dma 주소가 같게 변환하기도 하지만, 오늘날에는 CPU core와 GPU core 등이 통합된 SoC의 경우 DMA 주소와 DRAM 물리 주소의 주소 변환이 부분적으로 특정 영역 몇 개가 일괄적으로 이루어진다.
    • legacy x86 시스템에서 디바이스들은 하위 16M 이하의 DMA 영역과 물리 주소가 같이 Direct 접근하였다.

 

다음 그림은 IOMMU 매핑과 Direct 매핑과의 구성 차이를 보여준다.

  • IOMMU를 이용할 수 없는 Device 들은 Direct 매핑을 사용하여 DRAM에 DMA한다.
  • 아래 그림에서 물리 주소와 bus 주소가 같은 경우도 있지만 IOMMU 없이도 일괄적으로 변환되기도 한다.

 

DMA 주소 제한(Limitations)

물리 주소나 버스 주소가 64비트 주소로 표현 가능한 영역을 가진다고 할지라도,  다음과 같은 예와 같이 디바이스의 DMA 주소 지정이 제한되는 경우 가 있다.

  • 64bit PCIX 버스에 32bit PCI 디바이스를 사용하는 경우
  • legacy x86 시스템에서 ISA 버스를 통해 연결된 디바이스인 경우
    • 20bit(1M) 또는 24bit(16M) 주소만을 사용하여 DMA 접근 가능하다.

 

DMA 주소 제한을 설정할 때 대표적으로 다음 3 가지 API를 사용한다.

  • dma_set_mask()
    • 스트리밍 매핑을 사용하는 경우에 사용한다.
    • 예) 32bit(4G) 제한이 필요한 경우
      • mask=0xffff_ffff
      • 또는 DMA_BIT_MASK(32)
  • dma_set_coherent_mask()
    • consistent 매핑에서 사용한다.
    • coherent mask는 dma_set_mask()에 사용하는 제한과 같은 mask 값 또는 작은 mask 값을 사용할 수 있다.
  • dma_set_mask_and_coherent()
    • 위의 두 가지를 같은 mask 값으로 사용하는 구성이다.

 

SWIOTLB(SoftWare I/O TransLation Lookaside Buffer) & Bounce Buffer

  • hardware IOMMU(IOTLB)를 사용하지 못하는 플랫폼에서 fall-back 용도로 운영한다.
    • IOMMU(IOTLB)는 버스 주소와 cpu 물리 주소간의 주소 변환 매핑을 수행한다.
    • SWIOTLB는 io 가상 주소와 io 물리 주소간의 주소 변환 매핑이 되는 것처럼 보이도록 software 기법을 사용하여 두 영역간의 데이터를 복사하는 방법을 사용한다.
  • 디바이스가 좁은 범위의 주소 영역에만 접근할 수 있는 경우 사용된다.
  • 커널 부트업 과정중에 디바이스가 접근할 수 있는 범위안의 일정량의 Bounce Buffer를 할당받아 운용한다.
    • 커널 파라메터 예) “swiotlb=64K”
  • 디바이스가 DMA를 통해 bounce 버퍼에 기록하면 이를 swiotlb 드라이버가 디바이스의 버퍼에 복사하여 운영된다.
    • 바운스 버퍼를 DMA window로 사용한다.
  • software 방법으로 운영하기 때문에 성능이 저하되는 단점이 있다.
  • 이러한 Bounce 버퍼 운영을 통해 디바이스가 접근하지 못하는 highmem에 있는 유저 매핑 메모리에 복사하여 사용하는 사례도 있다.

 

다음 그림은 bounce buffer로 64K를 사용하여 디바이스가 DMA 전송을 수행하는 모습을 보여준다.

 

PCI DAC(Dual Address Cycle)

x86 PCI-X 디바이스는 64비트 주소를 사용한다. 특정 플랫폼에서 IOMMU가 32비트 이하의 주소만을 지원할 때 PCI-X 디바이스에 32비트 주소를 두 번 보내는 방법이 있다.

 

DMA coherent 디바이스

dma coherent 디바이스는 h/w 차원에서 캐시와 연동되어 동작하므로 dma 전송 전/후로 캐시 sync 작업을 수행할 필요가 없다.

  • ARM/ARM64의 경우 CCI(Cache Coherent Interface)에 연결된 ACP 포트 등에 연결된 디바이스가 코히런트 디바이스이다.
    • 예) gpu
  • 주의: coherent device와 coherent memory는 coherent 동작을 똑같이 수행하지만 서로 동작하는 방식이 다르다.
    • coherent device는 캐시와 연동되어 동작하는 device이고, coherent memory는 캐시 없이 동작하는 메모리이다.

 

DMA coherent 디바이스 예)

                enet: ethernet@340000{
                        compatible = "brcm,amac";
                        reg = <0x00340000 0x1000>;
                        reg-names = "amac_base";
                        dma-coherent;
                        interrupts = <GIC_SPI 213 IRQ_TYPE_LEVEL_HIGH>;
                        status= "disabled";
                };

 


DMA용 버퍼 메모리

리눅스 커널은 여러 가지 메모리 할당자를 제공한다. DMA 버퍼 메모리를 위해서 커널에서 제공하는 메모리 영역의 사용 가부를 알아본다.

 

DMA 메모리 할당 가능

  • 캐시를 사용하는 일반 커널 메모리
    • 이 메모리를 사용하는 일반적인 디바이스들은 dma 전송 전/후로 캐시 sync 작업이 필요하다. 단 dma coherent 디바이스는 dma 전송 전/후로 캐시 sync 작업을 수행할 필요가 없다.
    • API들
      •  __get_free_page()
        • 페이지 할당자(버디 시스템)으로 부터 2의 제곱승 단위의 페이지 할당
      • kmalloc()
        • 슬랩 할당자를 사용하여 작은 사이즈부터 할당 가능
      • kmem_cache_alloc()
        • 많은 양의 반복되는 슬랩 할당이 예상되는 경우 슬랩 캐시 할당
  • dma coherent 메모리
    • dma 디바이스 전용 dma coherent 메모리로 선언하여 사용한다. 이렇게 할당한 메모리는 consistent dma 매핑 방법으로 사용할 수 있다.
      • coherent 메모리
        • sram 전용 메모리를 사용하는 경우 캐시를 사용할 필요 없이 빠르므로 캐시를 사용하는 매핑을 하지 않는다.
      • contiguous 메모리
        • 일반 커널 메모리를 캐시 사용 없이 사용하도록, 커널 메모리의 일부를 write-combine 매핑으로 바꾸어 dma 버퍼로 사용한다.
    • API들
      • dma_alloc_coherent()
        • 큰 영역의 dma coherent 메모리를 한 번에 할당한다.
      • dma_pool_create() & dma_pool_alloc()
        • dma coherent 메모리에서 메모리 pool을 만든 후 조금씩 할당하여 사용한다.

 

DMA 메모리 할당 불가능

  • 커널 이미지 주소
    • 컴파일 타임에 static하게 할당
    • 이 영역들은 ARM, ARM64의 경우 섹션 페이지 매핑되어 있다.
  • 모듈 이미지 주소
    • 모듈의 로드 타임에 할당
    • 이 영역들은 ARM, ARM64의 경우 섹션 페이지 매핑되어 있다.
  • 스택 주소
    • 현재 스택의 일부를 할당
  • vmalloc()
    • vmalloc(연속된 가상 주소 공간이지만 연속되지 않은 물리 주소 공간) 영역의 할당

 

Cache Coherent vs Non-Coherent

디바이스가 액세스할 수 있도록 물리적으로 연속된 메모리이다.

 

Cache Coherent
  • 캐시 효과에 대해 걱정할 필요없이 장치 또는 프로세서에서 기록한 내용이 또 다른 프로세서 또는 디바이스에 의해 언제든 읽을 수 있다.
  • 캐시를 사용하는 메모리 시스템에서 cpu, 캐시, 메모리(DRAM) 및 디바이스가 Cache Coherent Inter-connect 장치를 통해 연동된다.
    • 디바이스가 DMA를 수행하기 전/후로 hardware가 알아서 캐시의 invalidate 및 clean 등을 수행한다.
    • 예) ARM, ARM64에서 ACP(Accelerator Coherency Port)를 사용하는 디바이스의 경우 L1/L2 캐시와 h/w coherent 작용을 한다.
  • 캐시를 사용하지 않아도 될 정도로 빠른 메모리(SRAM)의 경우 캐시 없이 cpu, 메모리 및 디바이스가 버스와 연결된다.
    • SRAM 등의 고속 메모리는 DRAM에 비해 매우 cost가 높기 때문에 고속 네트웍 디바이스 전용의 버퍼 메모리로 종종 사용된다.
    • 리눅스 커널에서는 캐시를 사용하지 않도록 write-combine 매핑을 활용한다.
Non-Coherent
  • 디바이스 또는 프로세서가 기록한 데이터가 캐시 효과로 인해 양쪽에 동시에 업데이트된 데이터가 sync되지 않는다. 이의 sync 보정를 위해 software적으로 추가적인 조작이 필요한다.
  • cpu, 캐시, 메모리 및 디바이스 중 하나라도 Cache Coherent Inter-connect 장치에 연동하지 못한 경우이다.
    • 디바이스가 DMA를 수행하기 전/후로 software가 캐시의 invalidate 및 clean 등을 수행해야 한다. (그 외의 버퍼등)
    • 예) ARM에서 디바이스가 Cache Coherent Inter-connect 장치에 연동되지 않는 사례들이 많이 있다.

 


Consistent(coherent) DMA 매핑 vs Streaming DMA 매핑

리눅스 커널의 DMA 버퍼 메모리를 사용하기 전에 매핑하는 두 가지 방법에 대해 알아본다.

 

1. Consistent(coherent) DMA 매핑

DMA 요청 시 마다 매핑하지 않고, 보통 드라이버 초기화 시 한 번만 consistent(coherent) DMA 매핑을 수행한다. cpu와 디바이스가 동시에 접근이 가능한 상태이고, 명시적인 software flush 없이 서로 갱신된 값을 볼 수 있다. consistent DMA 매핑을 위해 사용되는 메모리 할당은 dma_alloc_coherent() API를 사용하며, 아래 3 가지 중 한 가지 방법으로 coherent 메모리를 할당한다.

 

1) generic coherent per-device memory

  • Reserved DMA 영역으로 지정된 디바이스 전용 메모리 영역을사용한다.
    • 디바이스 트리를 사용하여 정의하는 경우 compatible = “shared-dma-pool”을 지정하고, reusable 속성을 사용하지 않아야 한다.
      • ARM32의 경우 no-map 속성도 사용해야 한다.
      • 아주 일부의 시스템에서 “linux,dma-default” 속성을 사용하여 이 영역을 default dma coherent 메모리로 동작하게 한다.
    • 커널 cmdline 파라메터의 경우 “memmap=nn[KMG]$ss[KMG]”
    • 동적으로 선언하는 경우(for legacy) dma_declare_coherent_memory() 함수를 사용하여 영역을 지정한다.
    • 부팅 시 커널 로그 예)
      • Reserved memory: created DMA memory pool at 0x0000000060000000, size 16 MiB
    • 디바이스별로 비트맵을 통해 관리된다.
      • 비트맵의 1 비트는 1 페이지의 할당 여부를 관리한다.
  • 보통 다음과 같은 메모리를 사용한다. 이들은 캐시를 사용하지 않고 write 버퍼만 사용가능한 write-combine 매핑을 사용한다.
    • SRAM 등의 고속 메모리를 DMA 메모리로 사용
    • 호스트 메모리의 일부를 DMA 메모리로 사용

 

디바이스 트리 예) bman 디바이스가 bman-fbpr 영역의 메모리를 지정하여 사용한다.

arch/arm64/boot/dts/freescale/fsl-ls1046a.dtsi

        reserved-memory {
                #address-cells = <2>;
                #size-cells = <2>;
                ranges;

                bman_fbpr: bman-fbpr {
                        compatible = "shared-dma-pool";
                        size = <0 0x1000000>;
                        alignment = <0 0x1000000>;
                        no-map;
                };
         ...
       soc: soc {
                bman: bman@1890000 {
                        compatible = "fsl,bman";
                        reg = <0x0 0x1890000 0x0 0x10000>;
                        interrupts = <GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH>;
                        memory-region = <&bman_fbpr>;

                };
        ...

 

2) direct DMA memory -fall-back

  • 디바이스가 IOMMU를 사용하지 않아 주소 변환 없이 시스템 메모리에 DMA 전송할 메모리이다.
  • CMA 영역에서 할당 받은 페이지 또는 일반 버디시스템에서 할당 받은 페이지를 사용한다.
    • CMA 영역 지정은 시스템 메모리 내에서 사용된다.
      • 디바이스 트리의 경우 compatible = “shared-dma-pool”을 지정하고 reusable 속성을 사용해야 한다.
        • linux,cma-default 속성을 사용하면 defaul cma 영역으로 지정한다.
        • no-map 속성은 사용하면 안된다.
      • 커널 cmdline 파라메터의 경우 “cma=nn[MG]@[start[MG][-end[MG]]]”
      • 부팅 시 커널 로그 예)
        • Reserved memory: created CMA memory pool at 0x0000000048000000, size 16 MiB
  • 할당 받은 후 DMA coherent 디바이스가 아닌 경우 아키텍처가 지원하는 매핑 방법을 사용하여 다시 매핑한다.
    • ARM64의 경우 write-combine 매핑을 사용하고, 그 외의 경우 no-cache 매핑을 사용한다.

 

디바이스 트리 예) DMA를 사용하는 디바이스가 특정 메모리를 지정하지 않는 경우 cma 메모리 사용할 수 있다.

arch/arm64/boot/amlogic/meson-gx.dtsi

        reserved-memory {
                #address-cells = <2>;
                #size-cells = <2>;
                ranges;

                linux,cma {
                        compatible = "shared-dma-pool";
                        reusable;
                        size = <0x0 0x10000000>;
                        alignment = <0x0 0x400000>;
                        linux,cma-default;
                };
        ...

 

3) IOMMU memory – fall-back

  • 디바이스가 IOMMU를 사용하여 주소 변환 과정을 통해 시스템 메모리에 DMA 전송한다.
  • CMA 영역에서 할당 받은 페이지 또는 일반 버디시스템에서 할당 받은 페이지를 사용한다.
  • 할당 받은 후 IOMMU가 지원하는 매핑 방법을 사용하여 다시 매핑한다.

 

IOMMU를 사용하는 DMA controller와 MMC Nand controller 디바이스 트리 예)

arch/arm64/boot/dts/broadcom/stingray/stingray.dtsi

        scr {
                smmu: mmu@3000000 {
                        compatible = "arm,mmu-500";
                        reg = <0x03000000 0x80000>;
                        #global-interrupts = <1>;
                        interrupts = <GIC_SPI 704 IRQ_TYPE_LEVEL_HIGH>,
                                     ...
                                     <GIC_SPI 774 IRQ_TYPE_LEVEL_HIGH>;
                        #iommu-cells = <2>;
                };

 

        hsls {
                dma0: dma@310000 {
                        compatible = "arm,pl330", "arm,primecell";
                        reg = <0x00310000 0x1000>;
                        interrupts = <GIC_SPI 193 IRQ_TYPE_LEVEL_HIGH>,
                                     <GIC_SPI 194 IRQ_TYPE_LEVEL_HIGH>,
                                     <GIC_SPI 195 IRQ_TYPE_LEVEL_HIGH>,
                                     <GIC_SPI 196 IRQ_TYPE_LEVEL_HIGH>,
                                     <GIC_SPI 197 IRQ_TYPE_LEVEL_HIGH>,
                                     <GIC_SPI 198 IRQ_TYPE_LEVEL_HIGH>,
                                     <GIC_SPI 199 IRQ_TYPE_LEVEL_HIGH>,
                                     <GIC_SPI 200 IRQ_TYPE_LEVEL_HIGH>,
                                     <GIC_SPI 201 IRQ_TYPE_LEVEL_HIGH>;
                        #dma-cells = <1>;
                        #dma-channels = <8>;
                        #dma-requests = <32>;
                        clocks = <&hsls_div2_clk>;
                        clock-names = "apb_pclk";
                        iommus = <&smmu 0x6000 0x0000>;
                };

 

                sdio0: sdhci@3f1000 {
                        compatible = "brcm,sdhci-iproc";
                        reg = <0x003f1000 0x100>;
                        interrupts = <GIC_SPI 204 IRQ_TYPE_LEVEL_HIGH>;
                        bus-width = <8&gt;;
                        clocks = <&sdio0_clk>;
                        iommus = <&smmu 0x6002 0x0000>;
                        status = "disabled";
                };

 

DMA 버퍼 할당 및 consistent(coherent) DMA 매핑
  • 큰 페이지 할당 및 매핑
    • dma_alloc_coherent() API를 사용하여 페이지 단위의 큰 DMA 버퍼를 할당하고 cosistent 매핑한다.
  • 작은 페이지 할당 및 매핑
    • dma_alloc_coherent() API를 사용하여 할당 받은 DMA 버퍼를 여러 개로 나누어 사용하거나,
    • 자주 반복되어 사용되는 작은 블럭 사이즈의 DMA 버퍼를 위해 dma_pool_create() API로 DMA pool을 생성하고, dma_pool_alloc() API로 블럭을 할당 받아 사용한다.

 

페이지 매핑 타입

다음은 페이지를 매핑할 때 사용하는 방법에 대해 잠시 기억해본다.

  • WC(Write Combine) 매핑
    • 캐시는 사용하지 않고 쓰기 버퍼만 사용 가능하다. 이 때 weekly(in) 오더 메모리도 가능하다.
  • WT(Write Through) 매핑
    • 캐시 hit되는 경우 캐시와 메모리를 동시에 기록한다. 캐시 hit하지 않는 경우는 곧바로 메모리에 기록하게 한다.
  • WB(Write Back) 매핑
    • 캐시를 사용하도록 매핑한다. (커널이 사용하는 일반 DRAM 영역)

 

다음 그림은 cpu가 write-combine 매핑된 coherent(contiguous) 메모리에 데이터를 기록하는 모습을 보여준다.

  • L1, L2 캐시를 사용하지 않는다.

 


2. Streaming DMA 매핑

디바이스가 DMA를 수행하기 전에 매핑(map)을 하고 DMA 수행한 후에 매핑 해제(unmap)를 한다. 이 매핑들은 interrupt context에서 사용될 수 있다.

  • consistent DMA 매핑은 드라이버 로드 시 한 번만 수행하는 것과 다르게, Streaming DMA 매핑과 매핑 해제는 매번 DMA를 하기 전/후에 수행하여야 한다.

 

map & unmap

아키텍처 및 시스템 설계에 따라 cpu와 디바이스간에 공유된 메모리를 동시에 양쪽에서 업데이트를 하는 경우 서로 데이터가 싱크되지 않아 올바르게 보여지지 않을 수 있다. 따라서 cpu와 디바이스간의 데이터 일관성을 위해 DMA를 사용하기 전에 DMA 방향을 지정하고 DMA 전송 전/후로 다음 특별한 조작 등을 수행할 필요가 있다. 이러한 동작은 아키텍처마다 조금씩 다르다.

  • map
    • 디바이스가 메모리에 DMA 전송(read/write)을 하기 전에 처리해야 할 일을 수행한다.
    • 아키텍처 및 시스템에 따라 다양한 조작이 있을 수 있는데 그러한 조작은 다음과 같은 것들이 있다.
      • 캐시 조작(clean 또는 invalidate)
      • 버퍼 조작(flush)
      • IOMMU 사용 시 TLB 관련 매핑 레지스터 조작
      • SWIOTLB 사용 시 orginal DMA 버퍼와 디바이스가 실제 DMA 전송하는 Bounce 버퍼와의 데이터 복사(copy)
    • ARM64 캐시 조작 예)
      • coherent 메모리를 사용하는 경우 캐시 조작을 할 필요없다.
      • coherent 메모리를 사용하지 않는 경우 캐시 조작이 필요하다.
        • DMA_FROM_DEVICE 방향을 사용하는 경우 캐시를 invalidate 한다.
        • 그 외의 경우 캐시를 clean 한다.
  • unmap
    • 디바이스가 메모리에 DMA 전송(read/write)을 한 후에 처리해야 할 일을 수행한다.
    • ARM64 캐시 조작 예)
      • coherent 메모리를 사용하는 경우 캐시 조작을 할 필요없다.
      • coherent 메모리를 사용하지 않는 경우 DMA 방향에 따라 캐시 조작이 필요한 경우가 있다.
        • DMA_TO_DEVICE 방향을 사용하는 경우 아무런 동작을 하지 않는다.
        • 그 외의 경우 캐시를 invalidate 한다.

 

다음 그림은 ARM64 시스템에서 DMA 사용 전 후로 map/unmap 시 시스템 메모리의 캐시에 관여를 하는 모습을 보여준다.

 

DMA directions

다음과 같이 DMA 전송 시 데이터가 움직이는 방향을 지정할 수 있다.

  • DMA_BIDIRECTIONAL
    • 메인 메모리 및 디바이스 양방향
    • 정확한 방향을 모를 때 양방향 보낼 수 있도록 지정한다. 다만 단방향 지정보다 cost가 더 높다.
  • DMA_TO_DEVICE
    • 메인 메모리에서 디바이스 방향
  • DMA_FROM_DEVICE
    • 디바이스에서 메인 메모리 방향
  • DMA_NONE
    • 디버그 시 사용

 

single vs sg(scatter/gather)

스트리밍 매핑은 DMA 요청 시 마다(one-shot) 매핑과 매핑 해제를 수행하고, 다음과 같이 두 가지 버전을 지원한다.

  • single map/unmap
    • 물리적으로 연속된(contiguous) 하나의 영역을 대상으로 DMA 전송 시 사용
      • 예) large 영역으로 구성된 ring 버퍼
    • 제약: highmem 영역은 사용하지 못한다.
    • DMA 전송 시작 전에 dma_map_single()을 호출하고 DMA 전송이 완료되면 dma_unmap_single()을 호출한다.
  • scatter/gather map/unmap
    • 물리적으로 연속되지 않고 fragment된 여러 버퍼를 대상으로 한꺼번에 DMA 전송 시 사용
      • 물리적으로 연속된 대용량의 DMA 버퍼를 준비하지 않아도되는 장점이 있다.
    • scatterlist에서 지정한 개개의 메모리 영역은 물리적으로 연속된 메모리를 사용해야 하지만, 전체 DMA 버퍼가 연속되지 않고 fragment 되었다.
    • scatterlist 배열에 DMA 버퍼로 사용할 영역의 offset 및 길이 정보등을 담아 요청한다.
    • DMA 전송 시작 전에 dma_map_sg()를 호출하고 DMA 전송이 완료되면 dma_unmap_sg()를 호출한다.

 

다음 그림은 single DMA 스트리밍과 scatter/gather DMA 스트리밍의 모습을 보여준다.

 

다음 그림은 연속된(contiguous) single 메모리 영역을 대상으로 디바이스에서 메모리 방향의 스트리밍 DMA 매핑을 보여준다.

 

다음 그림은 fragment된 여러 개의 메모리 영역을 대상으로 디바이스에서 메모리 방향의 스트리밍 DMA 매핑을 보여준다.

 


DMA 관련 헤더 파일 구성

arm 아키텍처는 dma 영역 설정 및 dma 할당을 위해 arm 아키텍처 전용 코드가 많이 남아 있다. arm64 아키텍처는 일부에 전용 코드를 사용하고 대부분 디바이스 트리 및 generic 구현을 통해 구성된다. x86 아키텍처는 대부분 generic 코드로 구현되어 있다. 다음은 DMA 관련한 주요 구현 파일들이다.

  • 기본 파일
    • include/linux/dma-buf.h
    • include/linux/dma-contiguous.h
    • include/linux/dma-direct.h
    • include/linux/dma-fence.h
    • include/linux/dma-fence-array.h
    • include/linux/dma-iommu.h
    • include/linux/dma-noncoherent.h
    • include/linux/dmaengine.h
    • include/linux/dmapool.h
    • include/linux/dmar.h
    • include/linux/dma-mapping.h
    • include/linux/dma-direction.h
    • include/linux/scatterlist.h
    • mm/dmapool.c
    • kernel/dma/coherent.c
    • kernel/dma/contiguous.c
    • kernel/dma/direct.c
    • kernel/dma/mapping.c
    • kernel/dma/remap.c
  • Generic 헤더 파일
    • include/asm-generic/dma-mapping.h
    • include/asm-generic/dma-contiguous.h
    • include/asm-generic/dma.h
  • 아키텍처별 헤더 파일
    • arm
      • arch/arm/include/asm/dma-contiguous.h
      • arch/arm/include/asm/dma-iommu.h
      • arch/arm/include/asm/dma.h
      • arch/arm/include/asm/dma-direct.h
      • arch/arm/include/asm/dma-mapping.h
      • arch/arm/mm/dma-mapping.c
    • arm64
      • arch/arm/include/asm/dma-mapping.h
      • arch/arm/mm/dma-mapping.c
    • x86
      • arch/x86/include/asm/dma-mapping.h

 

DMA 메모리 할당/해제

DMA 메모리를 사용하기 위한 할당/해제 관리를 알아본다.

  • 디바이스 트리를 사용하면서 reserved-memory 영역을 대상으로 dma 또는 cma 영역으로 지정하여 관리할 수 있게 되었다.
    • default cma 영역을 의미하는 “linux,cma-default” 속성을 사용할 수 있다.
    • 잘 사용되지는 않지만 ARM 아키텍처에서는 default dma 영역을 의미하는 “linux,dma-default” 속성도 사용할 수 있다.
  • 디바이스 트리를 사용하지 않는 PC 서버의 경우도 ACPI를 통해 dma 또는 cma 영역을 지정하여 관리할 수 있다.
  • 그 외 디바이스 트리 및 ACPI를 사용하지 않는 경우 아키텍처 및 드라이버 specific한 custom 코드를 통해 수행될 수 있다.
  • 페이지 단위의 DMA coherent 메모리 할당을 원할때에는 dma_alloc_coherent()를 사용하고, 더 작은 단위의 할당을 반복하여 사용하고자 할 때에는 dma pool을 만든 후 dma_pool_alloc() 함수를 사용하여 할당 할 수 있다.
  • dma 영역은 coherent 메모리로 사용하기 위해 no-map 속성을 사용하는 경우 별도의 write-combine 매핑을 사용하여 이용할 수 있다.

 

다음 그림은 전체적인 DMA 관리체계와 주요 API를 한 눈에 보여준다.

  • DMA가 버스와 연동하여 사용되지만 PCI 같은 버스 specific API를 사용하는 것보다 버스 독립적인 DMA-API를 사용해야 한다.
  • 예) pci_map_*() 보다 dma_map_*()을 사용한다.

 


Generic DMA Coherent per-device Memory 영역 선언

주로 성능이 빠른 SRAM 등의 디바이스 전용 메모리로 coherent 메모리 영역을 선언하는데 사용한다. 이렇게 선언된 메모리 영역은 디바이스가 dma 메모리의 할당/해제를 위해 비트맵으로 관리하며, 비트맵 1비트는 1개 페이지에 해당한다.

  • dma_declare_coherent_memory()
    • dmam_declare_coherent_memory() – Managed API
  • dma_release_declared_memory()
    • dmam_release_declared_memory() – Managed API
  • dma_mark_declared_memory_occupied()

 

메모리 페이지 할당/해제 API

DMA Coherent 메모리 페이지 할당/해제 API

위에서 선언한 coherent 메모리를 order 페이지 단위로 할당/해제한다. interrupt context에서 사용해야 하는 경우 GFP_ATOMIC gfp 플래그를 지정하여 사용할 수 있다.

  • dma_alloc_coherent()
    • dmam_alloc_coherent() – Managed API
  • dma_free_coherent()
    • dmam_free_coherent() – Managed API

 

DMA Contiguous 메모리 페이지 할당/해제 API

dma 메모리에서 order 페이지 단위로 할당/해제하되, write-combine  매핑 속성을 사용한다.

  • dma_alloc_wc()
  • dma_free_wc()

 

다양한 종류의 옵션 속성을 제공하는 DMA 메모리 페이지 할당/헤제 API

dma_alloc_coherent() 및 dma_alloc_wc() API를 사용할 수 없는 특수한 메모리 환경에서 옵션 속성을 주어 DMA 메모리 페이지 할당/해제를 수행할 수 있다.

  • dma_alloc_attrs()
    • dma_alloc_coherent()와 dma_alloc_wc()도 직접 호출한다.
  • dma_free_attrs()
    • dma_free_coherent()와 dma_free_wc()가 호출하는 함수이다.

 

DMA Attributes

메모리 할당 시 옵션 속성을 지정할 수 있다.

  • 무속성: fully coherent 메모리를 할당한다.
  • DMA_ATTR_WRITE_COMBINE
    • ARM과 ARM64 및 일부 mips(avr32) 아키텍처에서 제공된다.
    • dma_alloc_wc()에서 호출할 때 사용한다.
  • DMA_ATTR_WEAK_ORDERING
    • powerpc(cell), sparc 등의 아키텍처에서 read 및 write 들이 weakly order될 수 있다고 알려준다.
  • DMA_ATTR_NON_CONSISTENT
    • non-coherent 메모리를 사용해도 sync 포인트를 통해 적절히 DMA를 수행할 수 있게된다.
  • DMA_ATTR_WRITE_BARRIER
    • 지연된 DMA write가 완료될 때까지 기다린다.
    • 현재 ia64의 infiniband/core/umem.c에서 사용되고 있다.
  • DMA_ATTR_FORCE_CONTIGUOUS
    • ARM, ARM64의 특정 gpu 디바이스에서 반드시 물리 주소 및 가상 주소 모두 contiguous 메모리 매핑을 사용해야할 때 요청된다.
  • DMA_ATTR_ALLOC_SINGLE_PAGES
    • 1 페이지만 할당할 때 사용된다.
    • ARM에서만 지원하는 기능이다.

유저 매핑 속성.

  • DMA_ATTR_NO_KERNEL_MAPPING

기타

  • DMA_ATTR_SKIP_CPU_SYNC
    • DMA 전/후에 cpu에서 sync 작업을 할 필요가 없는 경우이다.
  • DMA_ATTR_NO_WARN
    • 메모리 할당 실패 경고를 출력하지 못하게 한다.
    • 현재 powerpc 에서만 적용되어 있다.
  • DMA_ATTR_PRIVILEGED
    • 유저 및 커널 레벨 양쪽에서 접근 가능하도록 요청한다.
    • arm의 pl330 DMA 컨트롤러 드라이버에서만 사용되고 있다.

 

DMA Pool 종류

  • DMA Pool
    • 현재 가장 많이 사용하는 dma pool 자료 구조이며, 실제 우선 할당되는 메모리는 device가 지정한 coherent 메모리이며, 지정한 coherent 메모리가 없는 경우 아래 dma coherent pool로 생성된 메모리 영역을 이용한다.
  • DMA Coherent pool (atomic pool)
    • kernel/dma/pool.c
    • 커널 파라미터로 preallocated 용량을 지정할 수 있으며, 지정하지 않는 경우 디폴트로 메모리 용량 1G당 128KB를 선택하되, 128~4MB 범위를 사용한다.
    • kernel parameter ex) “coherent_pool=1M”
    • log) DMA: preallocated 1024 KiB pool for atomic allocations
  • DMA Global pool
    • 거의 사용하지 않는다.
    • “linux,dma-default” with arm & !MMU
    • kernel/dma/coherent.c
  • 기타 custom dma pool
    • dma pool의 관리는 다양한 각사의 방법들을 사용하여 왔다.

 

DMA Coherent Memory Pool API

페이지 단위가 아닌 작은 용량의 dma coherent 메모리를 반복하여 사용하고자 할 때 DMA coherent 메모리 풀을 생성한 후 그 풀 내부에서 작은 메모리 할당을 할 필요성이 생겼다. 다음은 풀을 생성하고 해제하는데 사용하는 API이다.

  • dma_pool_create()
    • 이 함수 내부에서 dma_alloc_coherent() 함수를 호출하여 요청한 크기의 dma coherent pool을 생성한다.
    • dmam_pool_create() – Managed API
  • dma_pool_destroy()
    • dmam_pool_destroy() – Managed API

 

다음은 지정한 dma coherent pool에서 1개의 작은 블럭 메모리를 할당/해제하는 API 이다.

  • dma_pool_alloc()
  • dma_pool_free()

 

DMA Mask

DMA 영역 제한용 마스크를 설정하는 API 이다.

  • dma_set_mask()
    • streaming dma 매핑을 위해 사용되는 dma 제한이다.
  • dma_set_coherent_mask()
    • consistent dma 매핑을 위해 사용되는 dma 제한이다.
  • dma_set_mask_and_coherent()
    • 위의 두 가지 매핑을 한꺼번에 설정한다.

 

DMA 유저스페이스 매핑

유저 스페이스에서 사용할 수 있는 매핑도 지원한다.

  • dma_mmap_attrs()
  • dma_mmap_coherent()
  • dma_mmap_writecombine()

 

DMA 매핑

두 매핑 중 하나의 전략을 사용한다.

  • consistent dma 매핑
    • 디바이스 드라이버 초기화 시 dma 매핑(iommu 및 캐시 sync)이 필요한 경우 미리 매핑을 한다.
    • 드라이버 종료까지 개별 dma 매핑을 수행할 필요 없다
  • 스트리밍 dma 매핑
    • iommu 사용 시 매핑/언매핑을 동반한다.
    • 코히런트 메모리를 사용하지 않는 경우 캐시 sync 함수를 동반한다.

 

스트리밍 dma 매핑

dma 버퍼를 활용하는 다음 방법들이 제공된다.

  • 싱글 매핑
    • 하나의 영역에 대한 요청이고,
  • 페이지 매핑
    • 한 페이지에 대한 요청이다.
  • scatter/gather 매핑
    • fragment된 여러 영역에 대한 요청이다.

 

for_cpu

DMA 전에 sync가 필요한 경우

  • dma_sync_*_for_cpu() 함수를 사용할 수 있고,

 

for_device

DMA 후에 sync가 필요한 경우

  • dma_sync_*_for_device() 함수를 사용할 수 있다.

 

  • dma_map_single_attrs()
  • dma_unmap_single_attrs()
  • dma_map_single()
  • dma_unmap_single()
  • dma_map_page()
  • dma_unmap_page()
  • dma_map_sg_attrs()
  • dma_unmap_sg_attrs()
  • dma_map_sg()
  • dma_unmap_sg()
  • dma_mapping_error()
  • dma_sync_single_for_cpu()
  • dma_sync_single_for_device()
  • dma_sync_single_range_for_cpu()
  • dma_sync_single_range_for_device()
  • dma_sync_sg_for_cpu()
  • dma_sync_sg_for_device()

 

IOMMU

iommu 도메인을 할당하고 이를 이용하는 디바이스를 붙인 후 매핑을 지원하는 API이다.

  • iommu_domain_alloc()
  • iommu_domain_free()
  • iommu_attach_device()
  • iommu_detach_device()
  • iommu_map()
  • iommu_unmap()

 

참고