IOMMU

<kernel v5.0>

IOMMU(Input Output Memory Management Unit)

IOMMU는 다음과 같은 일들을 할 수 있다.

  • Transalation
    • 디바이스(IO 또는 버스) 주소를 물리 주소로 변환할 수 있도록 매핑을 제공한다.
    • DMA에 사용하는 버퍼는 연속된 물리 주소이어야 하는데, IOMMU를 사용하는 경우 그러한 제한이 없어진다.
      • IOMMU가 MMU와는 별도의 매핑 테이블을 사용하므로 시스템 메모리에 페이지가 반드시 연속되지 않아도 된다.
  • Isolation
    • 메모리에 대한 디바이스의 접근 제어를 제공한다.
  • IO Virtualization
    • 가상화를 지원하며 디바이스가 별개의 DMA 가상 주소 공간을 사용할 수 있다.
    • cpu에 있는 MMU도 가상화를 위해 별개의 MMU가 있는 것처럼 IOMMU도 유사하다.

 

아래 그림에서 좌측은 디바이스와 CPU가 메모리에 접근하는 것에 대한 논리적인 다이어그램이고, 우측은 ARM, ARM64 아키텍처에서 사용되는 ARM SMMU가 구성된 위치를 보여준다.

 

메모리 할당의 제한과 성능

시스템 메모리에 접근하는 다음 두 가지에 방법에 대한 성능을 생각해보자.

  • Direct DMA
    • 시스템이 DMA를 위해 메모리 영역의 일부문만을 접근하는 몇 가지 case를 고려해보자.
      • 32비트 시스템에서 제한된 ZONE_DMA 또는 ZONE_NORMAL 영역만을 사용할 수 있는 경우
      • 물리 메모리의 주소 영역이 4G를 초과하는 64비트 시스템에서 제한된 ZONE_DMA32(4G) 영역만을 사용할 수 있는 경우
    • DMA를 위해 극히 제한된 영역만을 사용할 수 있는 경우 DMA 버퍼만을 제한된 영역에 두고, DMA 전송 후에 시스템은 사용자의 버퍼 공간으로 복사하는 방법을 사용해야한다. 이러한 바운스 버퍼를 사용하는 swiotlb 구현을 사용하면 성능이 저하된다.
    • 연속된 물리 메모리의 할당만을 요구하므로 대규모 또는 큰 버퍼가 요구되는 경우 처리가 지연될 수 있다.
  • IOMMU
    • IOMMU를 사용하면 디바이스가 물리 메모리의 전체 영역을 사용할 수 있으므로 대용량 버퍼를 구성할 수 있다.
    • IOMMU의 매핑 테이블을 사용하여 디바이스가 분산된 물리 메모리에 접근할 수 있다.
      • DMA 버퍼 용도로 분산된 물리 메모리를 할당받고, IOMMU의 매핑 테이블을 이용하여 디바이스가 하나의 연속된 가상 주소에 액세스한다.
    • 특수한 제한: 물리 메모리의 주소 영역이 4G를 초과하는 64비트 시스템에서 IOMMU를 32비트 모드를 사용하는 경우 시스템 메모리의 모든 영역에 버퍼를 만들 수가 없다. 이러한 경우에도 swiotlb 방법을 사용하므로 이 때에도 성능이 저하된다. 그러나 이러한 사용사례 마저도 점점 64비트 IOMMU 모드를 사용하는 방법으로 migration 하므로 점점 찾아보기 힘들것이다.

 

대표적인 IOMMU

  • 인텔
    • North Bridge에 VT-D(Virtualization Technology for Directed I/O)를 채용하여 IO 허브를 제공하고 가상화도 지원한다.
  • AMD
    • dual IOMMU를 채용하여 IO 허브를 제공하고 가상화도 지원한다.
  • ARM 및 ARM64
    • 여러 버전의 SMMU가 제공되며 역시 가상화를 지원한다.
  • PCI-SIG
    • 내부에 IOMMU가 있고, I/O 가상화 (IOV)와 Address Translation Services (ATS) 기능을 제공한다.
  • Nvidia 그래픽 카드
    • 내부에 GARTGraphics Address Remapping Table 라는 IOMMU가 있고, 가상화를 지원한다.

 

IOMMU-API vs DMA-IOMMU API

IOMMU를 사용하기 위해  IOMMU API를 직접 사용하는 경우는 몇 개의 드라이버를 제외하곤 드물다. 대부분 DMA-IOMMU API를 통해 IOMMU와 연동하여 사용한다.

 

IOMMU 코어 API들과 이에 대응하는 드라이버로 ARM의 SMMU-500(compatible=”arm,mmu-500″) 코드를 위주로 분석한다.

  • 참고한 드라이버는 ARM smmu v1 및 v2를 지원하는 드라이버이다.
    • ARM smmu v3 드라이버는 별도의 코드를 사용한다.

 

Default Domain

  • 디바이스들의 각 그룹을 위해 하나의 디폴트 도메인을 할당한다.
  • 초기 모든 디바이스들은 디폴트 도메인에 속한다.
  • 디폴트 도메인은 공통 DMA-API 구현을 사용한다.

 

디바이스 트리

smmu v1 사용법

iommu 드라이버
.       /* SMMU with stream matching or stream indexing */
        smmu1: iommu {
                compatible = "arm,smmu-v1";
                reg = <0xba5e0000 0x10000>;
                #global-interrupts = <2>;
                interrupts = <0 32 4>,
                             <0 33 4>,
                             <0 34 4>, /* This is the first context interrupt */
                             <0 35 4>,
                             <0 36 4>,
                             <0 37 4>;
                #iommu-cells = <1>;
        };

 

iommu 디바이스
.       /* device with two stream IDs, 0 and 7 */
        master1 {
                iommus = <&smmu1 0>,
                         <&smmu1 7>;
        };

 

smmu v1 사용 사례)

smmu 드라이버

./arm/juno-base.dtsi

        smmu_pcie: iommu@2b500000 {
                compatible = "arm,mmu-401", "arm,smmu-v1";
                reg = <0x0 0x2b500000 0x0 0x10000>;
                interrupts = <GIC_SPI 40 IRQ_TYPE_LEVEL_HIGH>,
                             <GIC_SPI 40 IRQ_TYPE_LEVEL_HIGH>;
                #iommu-cells = <1>;
                #global-interrupts = <1>;
                dma-coherent;
                status = "disabled";
        };
smmu 디바이스

./arm/juno-base.dtsi

.       pcie_ctlr: pcie@40000000 {
                compatible = "arm,juno-r1-pcie", "plda,xpressrich3-axi", "pci-host-ecam-generic";
                device_type = "pci";
                reg = <0 0x40000000 0 0x10000000>;      /* ECAM config space */
                bus-range = <0 255>;
                linux,pci-domain = <0>;
                #address-cells = <3>;
                #size-cells = <2>;
                dma-coherent;
                ranges = <0x01000000 0x00 0x00000000 0x00 0x5f800000 0x0 0x00800000>,
                         <0x02000000 0x00 0x50000000 0x00 0x50000000 0x0 0x08000000>,
                         <0x42000000 0x40 0x00000000 0x40 0x00000000 0x1 0x00000000>;
                #interrupt-cells = <1>;
                interrupt-map-mask = <0 0 0 7>;
                interrupt-map = <0 0 0 1 &gic 0 0 0 136 4>,
                                <0 0 0 2 &gic 0 0 0 137 4>,
                                <0 0 0 3 &gic 0 0 0 138 4>,
                                <0 0 0 4 &gic 0 0 0 139 4>;
                msi-parent = <&v2m_0>;
                status = "disabled";
                iommu-map-mask = <0x0>; /* RC has no means to output PCI RID */
                iommu-map = <0x0 &smmu_pcie 0x0 0x1>;
        };

 

IOMMU 코어 초기화

iommu_init()

static int __init iommu_init(void)
{
        iommu_group_kset = kset_create_and_add("iommu_groups",
                                               NULL, kernel_kobj);
        BUG_ON(!iommu_group_kset);

        iommu_debugfs_setup();

        return 0;
}
core_initcall(iommu_init);

“/sys/kernel/iommu_groups” 디렉토리를 생성한다.

  • iommu 도메인에 iommu 그룹이 추가되는 경우 위의 디렉토리 아래에 추가된 iommu 그룹의 id 번호로 디렉토리가 생성된다.

 

IOMMU Core API

기본이 되는 파란색 함수들에 한해 소스를 자세히 분석하였다.

 

IOMMU 디바이스
  • iommu_device_register()
    • iommu 디바이스를 시스템에 등록한다.
  • iommu_device_unregister()
    • iommu 디바이스를 시스템에서 등록 해제한다.

 

IOMMU 도메인
  • iommu_domain_alloc()
    • 새 iommu 도메인을 시스템에 추가한다.
  • iommu_domain_free()
    • iommu 도메인을 시스템에서 삭제한다.
  • iommu_domain_window_enable()
  • iommu_domain_window_disable()
  • iommu_domain_set_attr()
    • 도메인 속성에 따른 데이터를 지정한다.
      • DOMAIN_ATTR_GEOMETRY
      • DOMAIN_ATTR_PAGING
      • DOMAIN_ATTR_WINDOWS
      • DOMAIN_ATTR_FSL_PAMU_STASH
      • DOMAIN_ATTR_FSL_PAMU_ENABLE
      • DOMAIN_ATTR_FSL_PAMUV1
      • DOMAIN_ATTR_NESTING
      • DOMAIN_ATTR_DMA_USE_FLUSH_QUEUE
  • iommu_domain_get_attr()
    • 도메인 속성에 따른 데이터를 알아온다.

 

IOMMU 그룹
  • iommu_group_alloc()
    • iommu 드라이버에서 새로운 iommu 그룹을 할당하기 위해 호출된다.
  • iommu_group_release()
    • 새로운 iommu 그룹을 할당 해제한다.
  • iommu_group_get_by_id()
    • id 번호로 iommu 그룹을 알아온다.
  • iommu_group_get_iommudata()
    • iommu 그룹에서 저장된 데이터를 가져온다.
  • iommu_group_set_iommudata()
    • iommu 그룹에 데이터를 저장한다.
  • iommu_group_set_name()
    • iommu 그룹 이름을 지정한다.
  • iommu_group_get()
    • 디바이스를 위해 그룹을 알아온다. 그리고 참조 카운터를 증가시킨다.
  • iommu_group_put()
    • 디바이스가 iommu 그룹의 사용이 완료되었다. 따라서 참조 카운터를 감소시킨다.
  • iommu_group_register_notifier()
    • iommu 그룹에 디바이스의 추가 및 삭제 시마다 호출될 notifier call-back을 등록한다.
    • blocking_notifier_call_chain() 함수 호출시 위의 call-back이 동작된다.
  • iommu_group_unregister_notifier()
    • iommu 그룹에 디바이스의 추가 및 삭제 시마다 호출되던 notifier call-back을 등록 해제한다.
  • iommu_group_id()
    • iommu 그룹 id를 반환한다.
  • iommu_get_group_resv_regions()
    • /sys/kernel/iommu_groups/reserved_regions 파일을 통해 등록된 그룹을 확인할 수 있다.

 

IOMMU 디바이스
  • iommu_group_add_device()
    • iommu 그룹에 디바이스를 추가한다.
  • iommu_group_remove_device()
    • iommu 그룹에서 디바이스를 제거한다.
  • iommu_group_for_each_dev()
    • iommu 그룹내에서의 디바이스 iteration
  • iommu_attach_device()
    • 디바이스가 포함된 그룹을 도메인에 attach 한다.
  • iommu_detach_device()
    • 디바이스가 포함된 그룹을 도메인에서 detach 한다.
  • iommu_get_domain_for_dev()
    • 디바이스가 소속한 그룹의 도메인을 알아온다.
  • iommu_attach_group()
    • iommu 그룹을 iommu 도메인에 attach 한다.
  • iommu_detach_group()
    • iommu 그룹을 iommu 도메인에서 detach 한다.

 

IOMMU 매핑
  • iommu_map()
    • 페이지들을 매핑한다.
  • iommu_map_sg()
    • scatter/gather 분산된 메모리를 매핑한다.
  • iommu_unmap()
    • 페이지들을 매핑 해제한다.
  • iommu_unmap_fast()
    • iommu_unmap()과 동일하나 iotlb sync를 하지 않는다.

 

디바이스 트리

  • iommu_fwspec_init()
  • iommu_fwspec_add_ids()
  • iommu_fwspec_free()

 

기타

  • iommu_set_fault_handler()
  • report_iommu_fault()
  • iommu_iova_to_phys()

 

IOMMU 디바이스 등록/해제

iommu_device_register()

drivers/iommu/iommu.c

int iommu_device_register(struct iommu_device *iommu)
{
        spin_lock(&iommu_device_lock);
        list_add_tail(&iommu->list, &iommu_device_list);
        spin_unlock(&iommu_device_lock);

        return 0;
}

iommu 디바이스를 시스템에 등록한다.

  • iommu 디바이스를 전역 리스트인 iommu_device_list에 추가한다.

 

iommu_device_unregister()

drivers/iommu/iommu.c

void iommu_device_unregister(struct iommu_device *iommu)
{
        spin_lock(&iommu_device_lock);
        list_del(&iommu->list);
        spin_unlock(&iommu_device_lock);
}

iommu 디바이스를 시스템에서 등록 해제한다.

  • iommu 디바이스를 전역 리스트인 iommu_device_list에서 제거한다.

 

IOMMU Domain

게스트 VM을 지원(보호)하기 위해 도메인 기능을 제공한다.

  • 디바이스는 호스트 및 VM들 간에 이동할 수 있다.

 

 

iommu domain 타입
  • IOMMU_DOMAIN_BLOCKED
    • 모든 DMA가 차단되었으므로 디바이스를 분리할 수 있다.
  • IOMMU_DOMAIN_IDENTITY
    • DMA 주소와 시스템의 물리 주소가 같다. (1:1 identity 매핑)
  • IOMMU_DOMAIN_UNMANAGED
    • 가상 머신에서 사용되며, DMA 매핑이 IOMMU-API에 의해 관리된다.
  • IOMMU_DOMAIN_DMA
    • 내부적으로 DMA-API 구현을 위해 사용되는 타입이다.
    • 이 플래그를 사용하면 IOMMU 드라이버가 이 도메인에 대해 특정 최적화를 구현할 수 있다.

 

iommu_domain_alloc()

drivers/iommu/iommu.c

struct iommu_domain *iommu_domain_alloc(struct bus_type *bus)
{
        return __iommu_domain_alloc(bus, IOMMU_DOMAIN_UNMANAGED);
}
EXPORT_SYMBOL_GPL(iommu_domain_alloc);

버스의 iommu 도메인을 할당하여 지정한다.

  • 이렇게 생성한 도메인은 IOMMU-API를 사용할 수 있다.
  • IOMMU-API들은 몇 개의 함수를 제외하고는 대부분 iommu로 시작한다.
    • iommu_*()

 

__iommu_domain_alloc()

drivers/iommu/iommu.c

static struct iommu_domain *__iommu_domain_alloc(struct bus_type *bus,
                                                 unsigned type)
{
        struct iommu_domain *domain;

        if (bus == NULL || bus->iommu_ops == NULL)
                return NULL;

        domain = bus->iommu_ops->domain_alloc(type);
        if (!domain)
                return NULL;

        domain->ops  = bus->iommu_ops;
        domain->type = type;
        /* Assume all sizes by default; the driver may override this later */
        domain->pgsize_bitmap  = bus->iommu_ops->pgsize_bitmap;

        return domain;
}

요청한 버스를 위해 iommu 도메인을 새로 할당한다.

  • 코드 라인 6~7에서 버스에 지정된 iommu ops가 구현되지 않은 경우 함수를 빠져나간다.
  • 코드 라인 9~11에서 iommu 드라이버에서 domain을 할당한다.
  • 코드 라인 13~16에서 할당한 도메인에 버스의 ops, pgsize_bitmap 및 인자로 요청한 타입을 지정한다.

 

arm_smmu_domain_alloc() – compatible=”arm,mmu-500″

drivers/iommu/arm-smmu.c

static struct iommu_domain *arm_smmu_domain_alloc(unsigned type)
{
        struct arm_smmu_domain *smmu_domain;

        if (type != IOMMU_DOMAIN_UNMANAGED &&
            type != IOMMU_DOMAIN_DMA &&
            type != IOMMU_DOMAIN_IDENTITY)
                return NULL;
        /*
         * Allocate the domain and initialise some of its data structures.
         * We can't really do anything meaningful until we've added a
         * master.
         */
        smmu_domain = kzalloc(sizeof(*smmu_domain), GFP_KERNEL);
        if (!smmu_domain)
                return NULL;

        if (type == IOMMU_DOMAIN_DMA && (using_legacy_binding ||
            iommu_get_dma_cookie(&smmu_domain->domain))) {
                kfree(smmu_domain);
                return NULL;
        }

        mutex_init(&smmu_domain->init_mutex);
        spin_lock_init(&smmu_domain->cb_lock);

        return &smmu_domain->domain;
}

ARM smmu의 도메인을 할당한다.

 

iommu_domain_free()

drivers/iommu/iommu.c

void iommu_domain_free(struct iommu_domain *domain)
{
        domain->ops->domain_free(domain);
}
EXPORT_SYMBOL_GPL(iommu_domain_free);

버스의 iommu 도메인을 할당 해제한다.

 

arm_smmu_domain_free() – compatible=”arm,mmu-500″

drivers/iommu/arm-smmu.c

static void arm_smmu_domain_free(struct iommu_domain *domain)
{
        struct arm_smmu_domain *smmu_domain = to_smmu_domain(domain);

        /*
         * Free the domain resources. We assume that all devices have
         * already been detached.
         */
        iommu_put_dma_cookie(domain);
        arm_smmu_destroy_domain_context(domain);
        kfree(smmu_domain);
}

ARM smmu 도메인을 할당 해제한다.

 

IOMMU 그룹

디바이스 개별로 isolation이 안될 수도 있다. 즉 IOMMU 그룹에 묶인 디바이스들을 대상으로 접근 제어를 제공할 수도 있다.

 

그룹명

그룹 생성 시 그룹 마다 부여되는 group_id는 0부터 시작하는 숫자이고, 곧바로 그룹 명으로 사용된다.

 

다음은 0번 iommu 그룹에 연결된 디바이스를 보여준다.

$ ls /sys/kernel/iommu_groups/0/devices/
ff650000.vpu_service

 

Grouping 구현

그루핑에 관계된 디바이스 구현 함수는 다음과 같이 3가지로 나뉜다.

  • PCI 디바이스 그루핑 함수
  • FSL-MC 디바이스 그루핑 함수
  • Generic 디바이스 그루핑 함수

 

그룹 할당

iommu_group_alloc()

drivers/iommu/iommu.c

/**
 * iommu_group_alloc - Allocate a new group
 *
 * This function is called by an iommu driver to allocate a new iommu
 * group.  The iommu group represents the minimum granularity of the iommu.
 * Upon successful return, the caller holds a reference to the supplied
 * group in order to hold the group until devices are added.  Use
 * iommu_group_put() to release this extra reference count, allowing the
 * group to be automatically reclaimed once it has no devices or external
 * references.
 */
struct iommu_group *iommu_group_alloc(void)
{
        struct iommu_group *group;
        int ret;

        group = kzalloc(sizeof(*group), GFP_KERNEL);
        if (!group)
                return ERR_PTR(-ENOMEM);

        group->kobj.kset = iommu_group_kset;
        mutex_init(&group->mutex);
        INIT_LIST_HEAD(&group->devices);
        BLOCKING_INIT_NOTIFIER_HEAD(&group->notifier);

        ret = ida_simple_get(&iommu_group_ida, 0, 0, GFP_KERNEL);
        if (ret < 0) {
                kfree(group);
                return ERR_PTR(ret);
        }
        group->id = ret;

        ret = kobject_init_and_add(&group->kobj, &iommu_group_ktype,
                                   NULL, "%d", group->id);
        if (ret) {
                ida_simple_remove(&iommu_group_ida, group->id);
                kfree(group);
                return ERR_PTR(ret);
        }

        group->devices_kobj = kobject_create_and_add("devices", &group->kobj);
        if (!group->devices_kobj) {
                kobject_put(&group->kobj); /* triggers .release & free */
                return ERR_PTR(-ENOMEM);
        }

        /*
         * The devices_kobj holds a reference on the group kobject, so
         * as long as that exists so will the group.  We can therefore
         * use the devices_kobj for reference counting.
         */
        kobject_put(&group->kobj);

        ret = iommu_group_create_file(group,
                                      &iommu_group_attr_reserved_regions);
        if (ret)
                return ERR_PTR(ret);

        ret = iommu_group_create_file(group, &iommu_group_attr_type);
        if (ret)
                return ERR_PTR(ret);

        pr_debug("Allocated group %d\n", group->id);

        return group;
}
EXPORT_SYMBOL_GPL(iommu_group_alloc);

iommu 그룹을 새로 할당한다.

  • 코드 라인 6~8에서 iommu_group 구조체를 할당한다.
  • 코드 라인 10~13에서 할당 받은 그룹을 초기화한다.
  • 코드 라인 15~20에서 전역 iommu_group_ida를 통해 그룹 id를 부여받는다.
  • 코드 라인 22~34에서 “/sys/kernel/iommu_groups/”뒤에 부여 받은 그룹 id 번호로 디렉토리를 생성하고, 그 아래로 “devices” 디렉토리도 생성한다.
  • 코드 라인 43~46에서 “/sys/kernel/iommu_groups/reserved_regions” 속성 파일을 생성하여 등록된 영역을 볼 수 있도록 한다.
  • 코드 라인   48~50에서 “/sys/kernel/iommu_groups/reserved_regions”속성 파일을 생성하여 그룹이 등록된 디폴트 도메인의 타입을 볼 수 있도록 한다.
  • 코드 라인 52~54에서 “allocated group %d” 메시지를 출력하고 할당한 그룹을 반환한다.

 

그룹 삭제

그룹 할당 해제는 그룹에 해당하는 속성 디렉토리를 삭제하면 iommu_group_release() 함수가 호출되어 제거된다.

iommu_group_release()

drivers/iommu/iommu.c

static void iommu_group_release(struct kobject *kobj)
{
        struct iommu_group *group = to_iommu_group(kobj);

        pr_debug("Releasing group %d\n", group->id);

        if (group->iommu_data_release)
                group->iommu_data_release(group->iommu_data);

        ida_simple_remove(&iommu_group_ida, group->id);

        if (group->default_domain)
                iommu_domain_free(group->default_domain);

        kfree(group->name);
        kfree(group);
}

iommu 그룹에 embed된 kboject를 지정하여 그룹을 삭제한다.

 

그룹에 디바이스 추가 및 제거

iommu_group_add_device()

drivers/iommu/iommu.c

/**
 * iommu_group_add_device - add a device to an iommu group
 * @group: the group into which to add the device (reference should be held)
 * @dev: the device
 *
 * This function is called by an iommu driver to add a device into a
 * group.  Adding a device increments the group reference count.
 */
int iommu_group_add_device(struct iommu_group *group, struct device *dev)
{
        int ret, i = 0;
        struct group_device *device;

        device = kzalloc(sizeof(*device), GFP_KERNEL);
        if (!device)
                return -ENOMEM;

        device->dev = dev;

        ret = sysfs_create_link(&dev->kobj, &group->kobj, "iommu_group");
        if (ret)
                goto err_free_device;

        device->name = kasprintf(GFP_KERNEL, "%s", kobject_name(&dev->kobj));
rename:
        if (!device->name) {
                ret = -ENOMEM;
                goto err_remove_link;
        }

        ret = sysfs_create_link_nowarn(group->devices_kobj,
                                       &dev->kobj, device->name);
        if (ret) {
                if (ret == -EEXIST && i >= 0) {
                        /*ㅑㅐㅡ
                         * Account for the slim chance of collision
                         * and append an instance to the name.
                         */
                        kfree(device->name);
                        device->name = kasprintf(GFP_KERNEL, "%s.%d",
                                                 kobject_name(&dev->kobj), i++);
                        goto rename;
                }
                goto err_free_name;
        }

        kobject_get(group->devices_kobj);

        dev->iommu_group = group;

        iommu_group_create_direct_mappings(group, dev);

        mutex_lock(&group->mutex);
        list_add_tail(&device->list, &group->devices);
        if (group->domain)
                ret = __iommu_attach_device(group->domain, dev);
        mutex_unlock(&group->mutex);
        if (ret)
                goto err_put_group;

        /* Notify any listeners about change to group. */
        blocking_notifier_call_chain(&group->notifier,
                                     IOMMU_GROUP_NOTIFY_ADD_DEVICE, dev);

        trace_add_device_to_group(group->id, dev);

        pr_info("Adding device %s to group %d\n", dev_name(dev), group->id);

        return 0;

err_put_group:
        mutex_lock(&group->mutex);
        list_del(&device->list);
        mutex_unlock(&group->mutex);
        dev->iommu_group = NULL;
        kobject_put(group->devices_kobj);
err_free_name:
        kfree(device->name);
err_remove_link:
        sysfs_remove_link(&dev->kobj, "iommu_group");
err_free_device:
        kfree(device);
        pr_err("Failed to add device %s to group %d: %d\n", dev_name(dev), group->id, ret);
        return ret;
}

디바이스를 iommu 그룹에 추가한다. 정상적으로 추가되는 경우 0을 반환한다.

  • 코드 라인 6~8에서 그룹을 만들기 위해 group_device 구조체를 할당받는다.
  • 코드 라인 12~14에서 “iommu_group” 이라는 이름의 링크를 생성하여 그룹에 연결한다.
    • 예) “/sys/devices/platform/ff650000.vpu_service/iommu_group”
      • “/sys/kernel/iommu_groups/0” 그룹을 가리킨다.
  • 코드 라인 16~37에서 디바이스명으로 링크를 생성하여 디바이스에 연결한다.
    • 예) “/sys/kernel/iommu_groups/0/devices/ff650000.vpu_service”
      • “/sys/devices/platform/ff650000.vpu_service”를 가리킨다.
    • 이름이 중복되는 경우가 생기면 “디바이스명.0″과 같이 숫자를 붙여서 재시도한다. 숫자는 계속 증가될 수 있다.
  • 코드 라인 41에서 디바이스에 소속 그룹을 연결한다.
  • 코드 라인 43에서 그룹에 디바이스를 Direct 매핑한다.
  • 코드 라인 45~51에서 그룹 락을 획득한 채로 디바이스를 그룹의 도메인에 attach한다.
  • 코드 라인 54~55에서 그룹에 디바이스가 추가되었음을 알리도록 notifier 콜 체인에 등록한 함수들을 호출한다.
  • 코드 라인 59~61에서 디바이스가 그룹에 추가되었음을 알리는 메시지를 출력하고 정상적으로 함수를 종료한다.

 

iommu_group_remove_device()

drivers/iommu/iommu.c

/**
 * iommu_group_remove_device - remove a device from it's current group
 * @dev: device to be removed
 *
 * This function is called by an iommu driver to remove the device from
 * it's current group.  This decrements the iommu group reference count.
 */
void iommu_group_remove_device(struct device *dev)
{
        struct iommu_group *group = dev->iommu_group;
        struct group_device *tmp_device, *device = NULL;

        pr_info("Removing device %s from group %d\n", dev_name(dev), group->id);

        /* Pre-notify listeners that a device is being removed. */
        blocking_notifier_call_chain(&group->notifier,
                                     IOMMU_GROUP_NOTIFY_DEL_DEVICE, dev);

        mutex_lock(&group->mutex);
        list_for_each_entry(tmp_device, &group->devices, list) {
                if (tmp_device->dev == dev) {
                        device = tmp_device;
                        list_del(&device->list);
                        break;
                }
        }
        mutex_unlock(&group->mutex);

        if (!device)
                return;

        sysfs_remove_link(group->devices_kobj, device->name);
        sysfs_remove_link(&dev->kobj, "iommu_group");

        trace_remove_device_from_group(group->id, dev);

        kfree(device->name);
        kfree(device);
        dev->iommu_group = NULL;
        kobject_put(group->devices_kobj);
}
EXPORT_SYMBOL_GPL(iommu_group_remove_device);

디바이스를 iommu 그룹에서 제거한다.

  • 코드 라인 6에서 디바이스가 그룹에서 제거되었음을 알리는 메시지를 출력한다.
  • 코드 라인 9~10에서 iommu 그룹에서 디바이스가 제거되었음을 알리도록 notifier 콜 체인에 등록한 함수들을 호출한다.
  • 코드 라인 12~23에서 그룹에 등록된 디바이스를 제거한다.
  • 코드 라인 25에서 iommu 그룹에서 디바이스로 연결된 링크를 제거한다.
    • 예) “/sys/kernel/iommu_groups/0/devices/ff650000.vpu_service”
  • 코드 라인 26에서 디바이스 디렉토리에서 “iommu_group” 링크를 제거한다.
    • 예) “/sys/devices/platform/ff650000.vpu_service/iommu_group”
  • 코드 라인 30~34에서 group_device를 할당해제한다.

 

그룹내 디바이스들의 attach/detach

iommu_attach_group()

drivers/iommu/iommu.c

int iommu_attach_group(struct iommu_domain *domain, struct iommu_group *group)
{
        int ret;

        mutex_lock(&group->mutex);
        ret = __iommu_attach_group(domain, group);
        mutex_unlock(&group->mutex);

        return ret;
}
EXPORT_SYMBOL_GPL(iommu_attach_group);

iommu 그룹 락을 소유한 채로 iommu 그룹에 포함된 디바이스들을 요청한 iommu 도메인에 attach 한다.

 

iommu_attach_device()

drivers/iommu/iommu.c

int iommu_attach_device(struct iommu_domain *domain, struct device *dev)
{
        struct iommu_group *group;
        int ret;

        group = iommu_group_get(dev);
        if (!group)
                return -ENODEV;

        /*
         * Lock the group to make sure the device-count doesn't
         * change while we are attaching
         */
        mutex_lock(&group->mutex);
        ret = -EINVAL;
        if (iommu_group_device_count(group) != 1)
                goto out_unlock;

        ret = __iommu_attach_group(domain, group);

out_unlock:
        mutex_unlock(&group->mutex);
        iommu_group_put(group);

        return ret;
}
EXPORT_SYMBOL_GPL(iommu_attach_device);

iommu 그룹 락을 소유한 채로 iommu 그룹에 포함된 디바이스들을 요청한 iommu 도메인에 attach 한다.

  • 코드 라인 16~17에서 iommu 그룹에 포함된 디바이스가 하나도 없는 경우 실패 결과로 함수를 빠져나간다.
  • 코드 라인 19에서 iommu 그룹에 포함된 디바이스들을 요청한 iommu 도메인에 attach 한다.

 

__iommu_attach_group()

drivers/iommu/iommu.c

static int __iommu_attach_group(struct iommu_domain *domain,
                                struct iommu_group *group)
{
        int ret;

        if (group->default_domain && group->domain != group->default_domain)
                return -EBUSY;

        ret = __iommu_group_for_each_dev(group, domain,
                                         iommu_group_do_attach_device);
        if (ret == 0)
                group->domain = domain;

        return ret;
}

요청한 iommu 그룹(@group)에 포함된 디바이스들을 요청한 iommu 도메인(@domain)에 attach 한다.

 

__iommu_group_for_each_dev()

drivers/iommu/iommu.c

/**
 * iommu_group_for_each_dev - iterate over each device in the group
 * @group: the group
 * @data: caller opaque data to be passed to callback function
 * @fn: caller supplied callback function
 *
 * This function is called by group users to iterate over group devices.
 * Callers should hold a reference count to the group during callback.
 * The group->mutex is held across callbacks, which will block calls to
 * iommu_group_add/remove_device.
 */
static int __iommu_group_for_each_dev(struct iommu_group *group, void *data,
                                      int (*fn)(struct device *, void *))
{
        struct group_device *device;
        int ret = 0;

        list_for_each_entry(device, &group->devices, list) {
                ret = fn(device->dev, data);
                if (ret)
                        break;
        }
        return ret;
}

요청한 iommu 그룹(@group)에 포함된 디바이스들을 대상으로 함수(fn)을 호출한다. 호출 할 때 인자 @data를 전달한다.

 

iommu_group_do_attach_device()

drivers/iommu/iommu.c

/*
 * IOMMU groups are really the natural working unit of the IOMMU, but
 * the IOMMU API works on domains and devices.  Bridge that gap by
 * iterating over the devices in a group.  Ideally we'd have a single
 * device which represents the requestor ID of the group, but we also
 * allow IOMMU drivers to create policy defined minimum sets, where
 * the physical hardware may be able to distiguish members, but we
 * wish to group them at a higher level (ex. untrusted multi-function
 * PCI devices).  Thus we attach each device.
 */
static int iommu_group_do_attach_device(struct device *dev, void *data)
{
        struct iommu_domain *domain = data;

        return __iommu_attach_device(domain, dev);
}

디바이스를 도메인에 attach 한다.

 

__iommu_attach_device()

drivers/iommu/iommu.c

static int __iommu_attach_device(struct iommu_domain *domain,
                                 struct device *dev)
{
        int ret;
        if ((domain->ops->is_attach_deferred != NULL) &&
            domain->ops->is_attach_deferred(domain, dev))
                return 0;

        if (unlikely(domain->ops->attach_dev == NULL))
                return -ENODEV;

        ret = domain->ops->attach_dev(domain, dev);
        if (!ret)
                trace_attach_device_to_domain(dev);
        return ret;
}

디바이스를 도메인에 attach 하기 위해 iommu 도메인에 등록된 (*attach_dev)  후크 함수를 호출한다.

  • amd iommu의 경우 iommu 드라이버의 (*is_attach_deferred) 함수를 호출하여 유예되는 case에서는 함수를 그냥 정상 종료시킨다.

 

arm_smmu_attach_dev() – compatible=”arm,mmu-500″

drivers/iommu/arm-smmu.c

static int arm_smmu_attach_dev(struct iommu_domain *domain, struct device *dev)
{
        int ret;
        struct iommu_fwspec *fwspec = dev_iommu_fwspec_get(dev);
        struct arm_smmu_device *smmu;
        struct arm_smmu_domain *smmu_domain = to_smmu_domain(domain);

        if (!fwspec || fwspec->ops != &arm_smmu_ops) {
                dev_err(dev, "cannot attach to SMMU, is it on the same bus?\n");
                return -ENXIO;
        }

        /*
         * FIXME: The arch/arm DMA API code tries to attach devices to its own
         * domains between of_xlate() and add_device() - we have no way to cope
         * with that, so until ARM gets converted to rely on groups and default
         * domains, just say no (but more politely than by dereferencing NULL).
         * This should be at least a WARN_ON once that's sorted.
         */
        if (!fwspec->iommu_priv)
                return -ENODEV;

        smmu = fwspec_smmu(fwspec);

        ret = arm_smmu_rpm_get(smmu);
        if (ret < 0)
                return ret;

        /* Ensure that the domain is finalised */
        ret = arm_smmu_init_domain_context(domain, smmu);
        if (ret < 0)
                goto rpm_put;

        /*
         * Sanity check the domain. We don't support domains across
         * different SMMUs.
         */
        if (smmu_domain->smmu != smmu) {
                dev_err(dev,
                        "cannot attach to SMMU %s whilst already attached to domain on SMMU %s\n",
                        dev_name(smmu_domain->smmu->dev), dev_name(smmu->dev));
                ret = -EINVAL;
                goto rpm_put;
        }

        /* Looks ok, so add the device to the domain */
        ret = arm_smmu_domain_add_master(smmu_domain, fwspec);

rpm_put:
        arm_smmu_rpm_put(smmu);
        return ret;
}

smmu 도메인에 디바이스를 attach 한다.

  • 코드 라인 4에서 디바이스가 디바이스 트리를 통해 iommu(smmu) 드라이버에 전달할 가변 인자값들을 알아온다.
  • 코드 라인 8~11에서 fwspec이 SMMU 드라이버와 attach하지 않은 경우 에러를 반환한다.
  • 코드 라인 20~21에서 smmu 마스터 정보가 없는 경우 에러를 반환한다.
  • 코드 라인 23에서 smmu가 절전 상태인 경우 깨운다.
  • 코드 라인 26~28에서 smmu의 페이지 테이블을 구성하고 매핑을 동작시킨다.
  • 코드 라인 34~40에서 도메인에 smmu 디바이스를 가리키지 않는 경우 에러 메시지를 출력하고 함수를 빠져나간다.
  • 코드 라인 43에서 디바이스를 도메인에 추가한다.

 

다음은 iommu  각 그룹에 attach된 디바이스들을 보여준다.

find /sys/kernel/iommu_groups/ -type l
/sys/kernel/iommu_groups/0/devices/ff650000.vpu_service
/sys/kernel/iommu_groups/1/devices/ff660000.rkvdec
/sys/kernel/iommu_groups/2/devices/ff8f0000.vop
/sys/kernel/iommu_groups/3/devices/ff900000.vop
/sys/kernel/iommu_groups/4/devices/ff910000.cif_isp

 

IOMMU 매핑

iommu_map()

drivers/iommu/iommu.c

int iommu_map(struct iommu_domain *domain, unsigned long iova,
              phys_addr_t paddr, size_t size, int prot)
{
        unsigned long orig_iova = iova;
        unsigned int min_pagesz;
        size_t orig_size = size;
        phys_addr_t orig_paddr = paddr;
        int ret = 0;

        if (unlikely(domain->ops->map == NULL ||
                     domain->pgsize_bitmap == 0UL))
                return -ENODEV;

        if (unlikely(!(domain->type & __IOMMU_DOMAIN_PAGING)))
                return -EINVAL;

        /* find out the minimum page size supported */
        min_pagesz = 1 << __ffs(domain->pgsize_bitmap);

        /*
         * both the virtual address and the physical one, as well as
         * the size of the mapping, must be aligned (at least) to the
         * size of the smallest page supported by the hardware
         */
        if (!IS_ALIGNED(iova | paddr | size, min_pagesz)) {
                pr_err("unaligned: iova 0x%lx pa %pa size 0x%zx min_pagesz 0x%x\n",
                       iova, &paddr, size, min_pagesz);
                return -EINVAL;
        }

        pr_debug("map: iova 0x%lx pa %pa size 0x%zx\n", iova, &paddr, size);

        while (size) {
                size_t pgsize = iommu_pgsize(domain, iova | paddr, size);

                pr_debug("mapping: iova 0x%lx pa %pa pgsize 0x%zx\n",
                         iova, &paddr, pgsize);

                ret = domain->ops->map(domain, iova, paddr, pgsize, prot);
                if (ret)
                        break;

                iova += pgsize;
                paddr += pgsize;
                size -= pgsize;
        }

        /* unroll mapping in case something went wrong */
        if (ret)
                iommu_unmap(domain, orig_iova, orig_size - size);
        else
                trace_map(orig_iova, orig_paddr, orig_size);

        return ret;
}
EXPORT_SYMBOL_GPL(iommu_map);

가상 주소 @iova를 물리 주소 @paddr로 @size 만큼 변환하도록 매핑을 요청한다.  정상 매핑된 경우 0을 반환한다.

  • 코드 라인 10~12에서 (*map) 후크 함수가 없거나 사이즈가 지정되지 않은 경우 에러 결과로 함수를 빠져나간다.
  • 코드 라인 14~15에서 매핑 변환 플래그가 없는 요청인 경우 에러 결과로 함수를 빠져나간다.
  • 코드 라인 18에서 지원하는 페이지 사이즈 중 가장 작은 단위의 페이지 사이즈를 알아온다.
    • 예) pgsize_bitmap=0x10_1000
      • 1M 페이지와 4K 페이지를 지원
  • 코드 라인 25~29에서 가상 주소와 물리 주소 및 사이즈 가 최소 페이지 단위로 정렬되지 않은 경우 에러 결과로 함수를 빠져나간다.
  • 코드 라인 31에서 iommu 매핑 정보를 디버그 레벨로 출력한다.
  • 코드 라인 33~46에서 @size 만큼 매핑을 하기 위해 페이지 사이즈 단위로 매핑을 하며 반복한다.
  • 코드 라인 39~44에서 매핑이 실패한 경우 언맵을 수행하고, 정상 처리가 된 경우 0을 반환한다.

 

drivers/iommu/arm-smmu.c – ARM & ARM64

static int arm_smmu_map(struct iommu_domain *domain, unsigned long iova,
                        phys_addr_t paddr, size_t size, int prot)
{
        struct io_pgtable_ops *ops = to_smmu_domain(domain)->pgtbl_ops;
        struct arm_smmu_device *smmu = to_smmu_domain(domain)->smmu;
        int ret;

        if (!ops)
                return -ENODEV;

        arm_smmu_rpm_get(smmu);
        ret = ops->map(ops, iova, paddr, size, prot);
        arm_smmu_rpm_put(smmu);

        return ret;
}

smmu용 페이지 테이블 드라이버를 통해 가상 주소 @iova 를 물리 주소 @paddr로 @size 만큼 매핑한다.

 

arm_lpae_map() – ARM & ARM64

drivers/iommu/io-pgtable-arm.c

static int arm_lpae_map(struct io_pgtable_ops *ops, unsigned long iova,
                        phys_addr_t paddr, size_t size, int iommu_prot)
{
        struct arm_lpae_io_pgtable *data = io_pgtable_ops_to_data(ops);
        arm_lpae_iopte *ptep = data->pgd;
        int ret, lvl = ARM_LPAE_START_LVL(data);
        arm_lpae_iopte prot;

        /* If no access, then nothing to do */
        if (!(iommu_prot & (IOMMU_READ | IOMMU_WRITE)))
                return 0;

        if (WARN_ON(iova >= (1ULL << data->iop.cfg.ias) ||
                    paddr >= (1ULL << data->iop.cfg.oas)))
                return -ERANGE;

        prot = arm_lpae_prot_to_pte(data, iommu_prot);
        ret = __arm_lpae_map(data, iova, paddr, size, prot, lvl, ptep);
        /*
         * Synchronise all PTE updates for the new mapping before there's
         * a chance for anything to kick off a table walk for the new iova.
         */
        wmb();

        return ret;
}

smmu용 페이지 테이블 드라이버를 통해 가상 주소 @iova 를 물리 주소 @paddr로 @size 만큼 매핑한다.

  • 코드 라인 10~11에서 페이지 속성 @iommu_prot에 read 또는 write 속성이 지정되지 않은 경우 아무 일도 하지 않고 성공(0)을 반환한다.
  • 코드 라인 13~15에서 @iova 및 @paddr이 smmu가 처리할 수 있는 영역을 벗어난 경우 -ERANGE 에러를 반환한다.
  • 코드 라인 17에서 @iommu_prot를 smmu의 pte 엔트리에 기록할 속성으로 변환한다.
  • 코드 라인 18에서 시작 페이지 테이블 레벨부터 smmu를 위해 매핑 테이블을 구성한다.

 

__arm_lpae_map() – ARM & ARM64

drivers/iommu/io-pgtable-arm.c

static int __arm_lpae_map(struct arm_lpae_io_pgtable *data, unsigned long iova,
                          phys_addr_t paddr, size_t size, arm_lpae_iopte prot,
                          int lvl, arm_lpae_iopte *ptep)
{
        arm_lpae_iopte *cptep, pte;
        size_t block_size = ARM_LPAE_BLOCK_SIZE(lvl, data);
        size_t tblsz = ARM_LPAE_GRANULE(data);
        struct io_pgtable_cfg *cfg = &data->iop.cfg;

        /* Find our entry at the current level */
        ptep += ARM_LPAE_LVL_IDX(iova, lvl, data);

        /* If we can install a leaf entry at this level, then do so */
        if (size == block_size && (size & cfg->pgsize_bitmap))
                return arm_lpae_init_pte(data, iova, paddr, prot, lvl, ptep);

        /* We can't allocate tables at the final level */
        if (WARN_ON(lvl >= ARM_LPAE_MAX_LEVELS - 1))
                return -EINVAL;

        /* Grab a pointer to the next level */
        pte = READ_ONCE(*ptep);
        if (!pte) {
                cptep = __arm_lpae_alloc_pages(tblsz, GFP_ATOMIC, cfg);
                if (!cptep)
                        return -ENOMEM;

                pte = arm_lpae_install_table(cptep, ptep, 0, cfg);
                if (pte)
                        __arm_lpae_free_pages(cptep, tblsz, cfg);
        } else if (!(cfg->quirks & IO_PGTABLE_QUIRK_NO_DMA) &&
                   !(pte & ARM_LPAE_PTE_SW_SYNC)) {
                __arm_lpae_sync_pte(ptep, cfg);
        }

        if (pte && !iopte_leaf(pte, lvl)) {
                cptep = iopte_deref(pte, data);
        } else if (pte) {
                /* We require an unmap first */
                WARN_ON(!selftest_running);
                return -EEXIST;
        }

        /* Rinse, repeat */
        return __arm_lpae_map(data, iova, paddr, size, prot, lvl + 1, cptep);
}

smmu용 페이지 테이블을 첫 레벨부터 마지막 레벨의 엔트리까지  드라이버를 통해 가상 주소 @iova 를 물리 주소 @paddr로 @size 만큼 매핑한다.

  • 코드 라인 6에서 @lvl에 해당하는 블럭 사이즈를 구해온다.
    • 4K 페이지 테이블을 사용하는 경우 가장 마지막 레벨에서는 블럭 크기는 4K이다.
  • 코드 라인 7에서 할당 준비할 테이블 크기를 구한다.
  • 코드 라인 11에서 @lvl에 해당하는 페이지 테이블의 @iova에 해당하는 엔트리 주소를  알아온다.
  • 코드 라인 14~15에서 만일 매핑 범위가 최종 페이지 테이블의 pte 엔트리인 경우 해당 pte를 갱신한다.
  • 코드 라인 18~19에서 이 함수는 마지막 페이지 테이블 레벨까지 재귀 호출된다. 마지막 테이블 레벨까지 이미 처리한 경우 -EINVAL 결과로 함수를 빠져나간다.
  • 코드 라인 22에서 연결된 페이지 테이블을 과 연결된 엔트리 값을 읽어온다.
  • 코드 라인 23~30에서 엔트리가 구성되지 않은 상태면 다음 단계용 페이지 테이블을 할당하고 할당한 테이블을 가리키도록 엔트리를 수정한다.코드 라인 31~34에서 만일 NO_DMA 요청을 사용하지 않으면서 pte에 대한 SW 싱크 플래그가 없으면 누군가 수정 중이므로 coherency sync를 수행한다.
  • 코드 라인 36~42에서 leaf 레벨의 pte가 아닌 경우 pte 엔트리가 가리키는 가상 주소 값을 알아온다. 만일 pte 엔트리가 이미 매핑된 경우 -EEXIST 에러를 반환한다.
  • 코드 라인 45에서 다음 레벨로 바꾸고 이 함수를 다시 재귀호출한다.

 

arm_lpae_install_table() – ARM & ARM64

drivers/iommu/io-pgtable-arm.c

static arm_lpae_iopte arm_lpae_install_table(arm_lpae_iopte *table,
                                             arm_lpae_iopte *ptep,
                                             arm_lpae_iopte curr,
                                             struct io_pgtable_cfg *cfg)
{
        arm_lpae_iopte old, new;

        new = __pa(table) | ARM_LPAE_PTE_TYPE_TABLE;
        if (cfg->quirks & IO_PGTABLE_QUIRK_ARM_NS)
                new |= ARM_LPAE_PTE_NSTABLE;

        /*
         * Ensure the table itself is visible before its PTE can be.
         * Whilst we could get away with cmpxchg64_release below, this
         * doesn't have any ordering semantics when !CONFIG_SMP.
         */
        dma_wmb();

        old = cmpxchg64_relaxed(ptep, curr, new);

        if ((cfg->quirks & IO_PGTABLE_QUIRK_NO_DMA) ||
            (old & ARM_LPAE_PTE_SW_SYNC))
                return old;

        /* Even if it's not ours, there's no point waiting; just kick it */
        __arm_lpae_sync_pte(ptep, cfg);
        if (old == curr)
                WRITE_ONCE(*ptep, new | ARM_LPAE_PTE_SW_SYNC);

        return old;
}

smmu용 페이지 테이블 엔트리 @ptep와 요청한 페이지 테이블 @table을 연결하여 기록한다.

  • 코드 라인 8에서 엔트리에 기록하고자 하는 값으로 새로 할당받은 테이블의 물리 주소와 테이블 플래그를 추가한다.
  • 코드 라인 9~10에서 non-secure quirk 플래그로 요청된 경우 기록할 값에도 non-secure 테이블 플래그를 추가한다.
  • 코드 라인 17~19에서 베리어를 먼저 수행한 후 엔트리를 교체한다.
  • 코드 라인 21~23에서 no dma 요청이었거나 기존 값에 sw sync가 설정된 경우 기존 엔트리를 반환한다.
    • ARM_LPAE_PTE_SW_SYNC
      • coherency race를 방지하기 위해 기록한다.
        • 기존 엔트리 값에 sw sync 플래그가 기록된 경우 엔트리 변경이 완벽히 완료된 상태이다.
        • 기존 엔트리값에 sw sync 플래그가 없는 경우 누군가 이 엔트리를 변경 중인 상태이다.
  • 코드 라인 26에서 아직 sync 되지 않은 상태이다. 따라서 다른 modifier를 위해 coherency sync를 수행한다.
  • 코드 라인 27~28에서 cmpxchg64 명령에서 경쟁 상황 없이 변경이 이루어진 경우 sw sync 플래그를 추가로 기록한다.

 

iommu_map_sg()

drivers/iommu/iommu.c

size_t iommu_map_sg(struct iommu_domain *domain, unsigned long iova,
                    struct scatterlist *sg, unsigned int nents, int prot)
{
        size_t len = 0, mapped = 0;
        phys_addr_t start;
        unsigned int i = 0;
        int ret;

        while (i <= nents) {
                phys_addr_t s_phys = sg_phys(sg);

                if (len && s_phys != start + len) {
                        ret = iommu_map(domain, iova + mapped, start, len, prot);
                        if (ret)
                                goto out_err;

                        mapped += len;
                        len = 0;
                }

                if (len) {
                        len += sg->length;
                } else {
                        len = sg->length;
                        start = s_phys;
                }

                if (++i < nents)
                        sg = sg_next(sg);
        }

        return mapped;

out_err:
        /* undo mappings already done */
        iommu_unmap(domain, iova, mapped);

        return 0;

}
EXPORT_SYMBOL_GPL(iommu_map_sg);

 

참고

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 동작)가 필요하다.

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

 

스트리밍 DMA 매핑 종류

  • Single
    • 물리적으로 연속된 하나의 DMA 버퍼를 사용한다.
  • Scatter/Gather
    • 물리적으로 분산된 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를 하여 메모리에 아무런 변경을 하지 못하게 한다.

 

 

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 영역으로 사용
  • 시스템 메모리의 일부를 디바이스 트리에서 지정한 reserved 메모리 영역으로 설정하고, 이 영역을 DMA 영역으로 사용
    • 이 영역을 CMA 영역으로 지정하기도 함.
    • legacy 시스템에서 DMA 주소 제한 문제로 CMA 영역의 위치를 낮게 설정하는 경우가 가끔 있음.
  • 시스템 메모리의 DMA 및 NORMAL zone에서 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페이지의 할당 여부를 관리한다.
  • 디바이스 전용 메모리로 보통 고속 전송이 요구되는 경우 SRAM을 사용하는 경우가 많이 있다.

 

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를 통해 시스템 메모리에 연결되지 않은 경우에 사용된다. DMA coherent 메모리로 할당된 시스템 메모리는 디바이스가 주소 변환 없이 접근한다.

  • 디바이스 트리를 통해 reserved-memory 영역을 CMA 영역으로 선언하고 이를 이용하는 방법도 있고, CMA 영역이 아닌 일반 시스템 메모리를 할당해올 수 있다.
  • 이렇게 할당한 페이지는 coherent한 속성을 얻기 위해 기존에 시스템 메모리가 WB(write-back) 또는 WBWA(write-back-write-allocation) 매핑을 사용하는데 이를 WC(write-combine) 매핑으로 바꿔 운영한다.
  • WC(write-combine) 매핑을 사용하면 캐시를 사용하지 않고, write buffer만 사용하므로 WB 또는 WBWA보다 성능이 저하되는 단점이 있지만 캐시와 무관하므로 coherence 동작은 가능하다.
  • 단 write-combine 매핑 방법을 사용해도 write-buffer의 기능은 사용하므로 필요시 마다 flush와 같은 sync 동작이 필요하다.

 

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에서 디바이스가 coherent 연동된 경우 architecture 고유의 dma 할당을 시도한다.
  • 코드 라인 6에서 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 메모리를 할당해온다.

  • 코드 라인 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 페이지의 가상 주소를 결과로 함수를 빠져나간다.
  • 코드 라인 30~36에서 할당 받은 연속된 페이지의 dma 페이지들을 vmalloc 공간의 연속된 가상 주소로 매핑한다.
  • 코드 라인 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가 아닌 디폴트 영역으로 사용하게 할 수도 있다.
    • 거의 사용하지 않는다.
  • “linux,cma-default” 속성
    • 속성을 사용하는 경우 시스템 메모리를 reserved 영역으로 지정하고 CMA 영역으로 만든다.
    • 속성을 사용하지 않으면 디바이스 전용 메모리를 reserved 영역으로 지정하고 사용하게 한다.
      • 주로 높은 성능이 요구되는 디바이스에서 속도가 빠른 SRAM을 사용한다.

 

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

 

RESERVEDMEM_OF_DECLARE() 매크로

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

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를 추가한다.

 

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 옵션을 사용할 수 있게 하였는데, 거의 사용되지 않는다.

 

참고