Re: [PATCH v3 07/17] dma-mapping: Implement link/unlink ranges API

Will Deacon
2024-11-18T14:59:42Z

On Sun, Nov 10, 2024 at 03:46:54PM +0200, Leon Romanovsky wrote:
> From: Leon Romanovsky <leonro@nvidia.com>
>
> Introduce new DMA APIs to perform DMA linkage of buffers
> in layers higher than DMA.
>
> In proposed API, the callers will perform the following steps.
> In map path:
> if (dma_can_use_iova(...))
> dma_iova_alloc()
> for (page in range)
> dma_iova_link_next(...)
> dma_iova_sync(...)
> else
> /* Fallback to legacy map pages */
> for (all pages)
> dma_map_page(...)
>
> In unmap path:
> if (dma_can_use_iova(...))
> dma_iova_destroy()
> else
> for (all pages)
> dma_unmap_page(...)
>
> Signed-off-by: Leon Romanovsky <leonro@nvidia.com>
> ---
> drivers/iommu/dma-iommu.c | 259 ++++++++++++++++++++++++++++++++++++
> include/linux/dma-mapping.h | 32 +++++
> 2 files changed, 291 insertions(+)

[...]

> +/**
> + * dma_iova_link - Link a range of IOVA space
> + * @dev: DMA device
> + * @state: IOVA state
> + * @phys: physical address to link
> + * @offset: offset into the IOVA state to map into
> + * @size: size of the buffer
> + * @dir: DMA direction
> + * @attrs: attributes of mapping properties
> + *
> + * Link a range of IOVA space for the given IOVA state without IOTLB sync.
> + * This function is used to link multiple physical addresses in contigueous
> + * IOVA space without performing costly IOTLB sync.
> + *
> + * The caller is responsible to call to dma_iova_sync() to sync IOTLB at
> + * the end of linkage.
> + */
> +int dma_iova_link(struct device *dev, struct dma_iova_state *state,
> + phys_addr_t phys, size_t offset, size_t size,
> + enum dma_data_direction dir, unsigned long attrs)
> +{
> + struct iommu_domain *domain = iommu_get_dma_domain(dev);
> + struct iommu_dma_cookie *cookie = domain->iova_cookie;
> + struct iova_domain *iovad = &cookie->iovad;
> + size_t iova_start_pad = iova_offset(iovad, phys);
> +
> + if (WARN_ON_ONCE(iova_start_pad && offset > 0))
> + return -EIO;
> +
> + if (dev_use_swiotlb(dev, size, dir) && iova_offset(iovad, phys | size))
> + return iommu_dma_iova_link_swiotlb(dev, state, phys, offset,
> + size, dir, attrs);
> +
> + return __dma_iova_link(dev, state->addr + offset - iova_start_pad,
> + phys - iova_start_pad,
> + iova_align(iovad, size + iova_start_pad), dir, attrs);
> +}
> +EXPORT_SYMBOL_GPL(dma_iova_link);
> +
> +/**
> + * dma_iova_sync - Sync IOTLB
> + * @dev: DMA device
> + * @state: IOVA state
> + * @offset: offset into the IOVA state to sync
> + * @size: size of the buffer
> + *
> + * Sync IOTLB for the given IOVA state. This function should be called on
> + * the IOVA-contigous range created by one ore more dma_iova_link() calls
> + * to sync the IOTLB.
> + */
> +int dma_iova_sync(struct device *dev, struct dma_iova_state *state,
> + size_t offset, size_t size)
> +{
> + struct iommu_domain *domain = iommu_get_dma_domain(dev);
> + struct iommu_dma_cookie *cookie = domain->iova_cookie;
> + struct iova_domain *iovad = &cookie->iovad;
> + dma_addr_t addr = state->addr + offset;
> + size_t iova_start_pad = iova_offset(iovad, addr);
> +
> + return iommu_sync_map(domain, addr - iova_start_pad,
> + iova_align(iovad, size + iova_start_pad));
> +}
> +EXPORT_SYMBOL_GPL(dma_iova_sync);
> +
> +static void iommu_dma_iova_unlink_range_slow(struct device *dev,
> + dma_addr_t addr, size_t size, enum dma_data_direction dir,
> + unsigned long attrs)
> +{
> + struct iommu_domain *domain = iommu_get_dma_domain(dev);
> + struct iommu_dma_cookie *cookie = domain->iova_cookie;
> + struct iova_domain *iovad = &cookie->iovad;
> + size_t iova_start_pad = iova_offset(iovad, addr);
> + dma_addr_t end = addr + size;
> +
> + do {
> + phys_addr_t phys;
> + size_t len;
> +
> + phys = iommu_iova_to_phys(domain, addr);
> + if (WARN_ON(!phys))
> + continue;
> + len = min_t(size_t,
> + end - addr, iovad->granule - iova_start_pad);
> +
> + if (!dev_is_dma_coherent(dev) &&
> + !(attrs & DMA_ATTR_SKIP_CPU_SYNC))
> + arch_sync_dma_for_cpu(phys, len, dir);
> +
> + swiotlb_tbl_unmap_single(dev, phys, len, dir, attrs);
> +
> + addr += len;
> + iova_start_pad = 0;
> + } while (addr < end);
> +}
> +
> +static void __iommu_dma_iova_unlink(struct device *dev,
> + struct dma_iova_state *state, size_t offset, size_t size,
> + enum dma_data_direction dir, unsigned long attrs,
> + bool free_iova)
> +{
> + struct iommu_domain *domain = iommu_get_dma_domain(dev);
> + struct iommu_dma_cookie *cookie = domain->iova_cookie;
> + struct iova_domain *iovad = &cookie->iovad;
> + dma_addr_t addr = state->addr + offset;
> + size_t iova_start_pad = iova_offset(iovad, addr);
> + struct iommu_iotlb_gather iotlb_gather;
> + size_t unmapped;
> +
> + if ((state->__size & DMA_IOVA_USE_SWIOTLB) ||
> + (!dev_is_dma_coherent(dev) && !(attrs & DMA_ATTR_SKIP_CPU_SYNC)))
> + iommu_dma_iova_unlink_range_slow(dev, addr, size, dir, attrs);
> +
> + iommu_iotlb_gather_init(&iotlb_gather);
> + iotlb_gather.queued = free_iova && READ_ONCE(cookie->fq_domain);
> +
> + size = iova_align(iovad, size + iova_start_pad);
> + addr -= iova_start_pad;
> + unmapped = iommu_unmap_fast(domain, addr, size, &iotlb_gather);
> + WARN_ON(unmapped != size);

Does the new API require that the 'size' passed to dma_iova_unlink()
exactly match the 'size' passed to the corresponding call to
dma_iova_link()? I ask because the IOMMU page-table code is built around
the assumption that partial unmap() operations never occur (i.e.
operations which could require splitting a huge mapping). We just
removed [1] that code from the Arm IO page-table implementations, so it
would be good to avoid adding it back for this.

Will

[1] https://git.kernel.org/pub/scm/linux/kernel/git/iommu/linux.git/commit/?h=arm/smmu&id=33729a5fc0caf7a97d20507acbeee6b012e7e519