34

我想尽快从启用 DMA 的 PCIe 硬件设备中获取数据到用户空间。

问:如何将“直接 I/O 到用户空间与/和/通过 DMA 传输”结合起来

  1. 通读LDD3,似乎需要执行几种不同类型的IO操作!?

    dma_alloc_coherent给了我可以传递给硬件设备的物理地址。但是需要在传输完成时进行设置get_user_pages并执行类型调用。copy_to_user这似乎是一种浪费,要求设备 DMA 进入内核内存(充当缓冲区),然后再次将其传输到用户空间。LDD3 p453:/* Only now is it safe to access the buffer, copy to user, etc. */

  2. 我理想中想要的是一些记忆:

    • 我可以在用户空间中使用(也许通过 ioctl 调用请求驱动程序来创建可 DMA 的内存/缓冲区?)
    • 我可以从中获取物理地址以传递给设备,以便所有用户空间所要做的就是对驱动程序执行读取
    • read 方法将激活 DMA 传输,阻塞等待 DMA 完成中断,然后释放用户空间读取(用户空间现在可以安全使用/读取内存)。

我是否需要用 映射的单页流映射、设置映射和用户空间缓冲区get_user_pages dma_map_page

到目前为止,我的代码设置get_user_pages在用户空间的给定地址(我称之为 Direct I/O 部分)。然后,dma_map_pageget_user_pages. 我给设备返回值dma_map_page作为 DMA 物理传输地址。

我正在使用一些内核模块作为参考:drivers_scsi_st.cdrivers-net-sh_eth.c. 我会查看 infiniband 代码,但找不到哪个是最基本的!

提前谢谢了。

4

6 回答 6

18

我现在实际上正在做完全相同的事情,而且我正在走这ioctl()条路。一般的想法是让用户空间分配将用于 DMA 传输的缓冲区,并将用于ioctl()将此缓冲区的大小和地址传递给设备驱动程序。然后,驱动程序将使用 scatter-gather 列表以及流式 DMA API 将数据直接传输到设备和用户空间缓冲区。

我正在使用的实现策略是ioctl()驱动程序中的 DMA 进入一个循环,该循环以 256k 的块为单位的用户空间缓冲区(这是硬件对它可以处理的分散/收集条目的数量施加的限制)。这是在一个函数内部隔离的,该函数会阻塞直到每次传输完成(见下文)。当所有字节都被传输或增量传输函数返回错误时ioctl()退出并返回到用户空间

的伪代码ioctl()

/*serialize all DMA transfers to/from the device*/
if (mutex_lock_interruptible( &device_ptr->mtx ) )
    return -EINTR;

chunk_data = (unsigned long) user_space_addr;
while( *transferred < total_bytes && !ret ) {
    chunk_bytes = total_bytes - *transferred;
    if (chunk_bytes > HW_DMA_MAX)
        chunk_bytes = HW_DMA_MAX; /* 256kb limit imposed by my device */
    ret = transfer_chunk(device_ptr, chunk_data, chunk_bytes, transferred);
    chunk_data += chunk_bytes;
    chunk_offset += chunk_bytes;
}

mutex_unlock(&device_ptr->mtx);

增量传递函数的伪代码:

/*Assuming the userspace pointer is passed as an unsigned long, */
/*calculate the first,last, and number of pages being transferred via*/

first_page = (udata & PAGE_MASK) >> PAGE_SHIFT;
last_page = ((udata+nbytes-1) & PAGE_MASK) >> PAGE_SHIFT;
first_page_offset = udata & PAGE_MASK;
npages = last_page - first_page + 1;

/* Ensure that all userspace pages are locked in memory for the */
/* duration of the DMA transfer */

down_read(&current->mm->mmap_sem);
ret = get_user_pages(current,
                     current->mm,
                     udata,
                     npages,
                     is_writing_to_userspace,
                     0,
                     &pages_array,
                     NULL);
up_read(&current->mm->mmap_sem);

/* Map a scatter-gather list to point at the userspace pages */

/*first*/
sg_set_page(&sglist[0], pages_array[0], PAGE_SIZE - fp_offset, fp_offset);

/*middle*/
for(i=1; i < npages-1; i++)
    sg_set_page(&sglist[i], pages_array[i], PAGE_SIZE, 0);

/*last*/
if (npages > 1) {
    sg_set_page(&sglist[npages-1], pages_array[npages-1],
        nbytes - (PAGE_SIZE - fp_offset) - ((npages-2)*PAGE_SIZE), 0);
}

/* Do the hardware specific thing to give it the scatter-gather list
   and tell it to start the DMA transfer */

/* Wait for the DMA transfer to complete */
ret = wait_event_interruptible_timeout( &device_ptr->dma_wait, 
         &device_ptr->flag_dma_done, HZ*2 );

if (ret == 0)
    /* DMA operation timed out */
else if (ret == -ERESTARTSYS )
    /* DMA operation interrupted by signal */
else {
    /* DMA success */
    *transferred += nbytes;
    return 0;
}

中断处理程序非常简短:

/* Do hardware specific thing to make the device happy */

/* Wake the thread waiting for this DMA operation to complete */
device_ptr->flag_dma_done = 1;
wake_up_interruptible(device_ptr->dma_wait);

请注意,这只是一种通用方法,过去几周我一直在研究这个驱动程序,但还没有实际测试它......所以请不要把这个伪代码当作福音,一定要加倍检查所有逻辑和参数;-)。

于 2011-04-04T14:38:03.213 回答
14

您基本上有正确的想法:在 2.1 中,您可以让用户空间分配任何旧内存。您确实希望它与页面对齐,因此posix_memalign()可以使用方便的 API。

然后让用户空间以某种方式传入该缓冲区的用户空间虚拟地址和大小;ioctl() 是一种很好的快速而肮脏的方法。在内核中,分配一个适当大小的缓冲区数组struct page*——user_buf_size/PAGE_SIZE条目——并用于get_user_pages()获取用户空间缓冲区的 struct page* 列表。

一旦你有了它,你可以分配一个struct scatterlist与你的页面数组大小相同的数组,并循环遍历页面列表sg_set_page()。设置 sg 列表后,您dma_map_sg()在 scatterlist 数组上执行操作,然后您可以获得scatterlist中每个条目的sg_dma_addressand由 DMA 映射代码合并)。sg_dma_lendma_map_sg()

这为您提供了所有要传递给设备的总线地址,然后您可以触发 DMA 并根据需要等待它。您拥有的基于 read() 的方案可能很好。

您可以参考drivers/infiniband/core/umem.c,特别ib_umem_get()是一些构建此映射的代码,尽管该代码需要处理的一般性可能会使其有点混乱。

或者,如果您的设备不能很好地处理分散/收集列表并且您想要连续的内存,您可以使用get_free_pages()分配一个物理上连续的缓冲区并dma_map_page()在其上使用。要让用户空间访问该内存,您的驱动程序只需要实现一个mmap方法而不是如上所述的 ioctl。

于 2011-05-21T07:16:16.183 回答
6

在某些时候,我想允许用户空间应用程序分配 DMA 缓冲区并将其映射到用户空间并获取物理地址,以便能够完全从用户空间控制我的设备并执行 DMA 事务(总线主控),完全绕过Linux内核。不过,我使用了一些不同的方法。首先,我从一个初始化/探测 PCIe 设备并创建字符设备的最小内核模块开始。然后,该驱动程序允许用户空间应用程序做两件事:

  1. 使用函数将 PCIe 设备的 I/O 栏映射到用户空间remap_pfn_range()
  2. 分配和释放 DMA 缓冲区,将它们映射到用户空间并将物理总线地址传递给用户空间应用程序。

基本上,它归结为mmap()call 的自定义实现(尽管如此file_operations)。一个用于 I/O 栏很简单:

struct vm_operations_struct a2gx_bar_vma_ops = {
};

static int a2gx_cdev_mmap_bar2(struct file *filp, struct vm_area_struct *vma)
{
    struct a2gx_dev *dev;
    size_t size;

    size = vma->vm_end - vma->vm_start;
    if (size != 134217728)
        return -EIO;

    dev = filp->private_data;
    vma->vm_ops = &a2gx_bar_vma_ops;
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    vma->vm_private_data = dev;

    if (remap_pfn_range(vma, vma->vm_start,
                        vmalloc_to_pfn(dev->bar2),
                        size, vma->vm_page_prot))
    {
        return -EAGAIN;
    }

    return 0;
}

另一个分配 DMA 缓冲区的方法pci_alloc_consistent()稍微复杂一些:

static void a2gx_dma_vma_close(struct vm_area_struct *vma)
{
    struct a2gx_dma_buf *buf;
    struct a2gx_dev *dev;

    buf = vma->vm_private_data;
    dev = buf->priv_data;

    pci_free_consistent(dev->pci_dev, buf->size, buf->cpu_addr, buf->dma_addr);
    buf->cpu_addr = NULL; /* Mark this buffer data structure as unused/free */
}

struct vm_operations_struct a2gx_dma_vma_ops = {
    .close = a2gx_dma_vma_close
};

static int a2gx_cdev_mmap_dma(struct file *filp, struct vm_area_struct *vma)
{
    struct a2gx_dev *dev;
    struct a2gx_dma_buf *buf;
    size_t size;
    unsigned int i;

    /* Obtain a pointer to our device structure and calculate the size
       of the requested DMA buffer */
    dev = filp->private_data;
    size = vma->vm_end - vma->vm_start;

    if (size < sizeof(unsigned long))
        return -EINVAL; /* Something fishy is happening */

    /* Find a structure where we can store extra information about this
       buffer to be able to release it later. */
    for (i = 0; i < A2GX_DMA_BUF_MAX; ++i) {
        buf = &dev->dma_buf[i];
        if (buf->cpu_addr == NULL)
            break;
    }

    if (buf->cpu_addr != NULL)
        return -ENOBUFS; /* Oops, hit the limit of allowed number of
                            allocated buffers. Change A2GX_DMA_BUF_MAX and
                            recompile? */

    /* Allocate consistent memory that can be used for DMA transactions */
    buf->cpu_addr = pci_alloc_consistent(dev->pci_dev, size, &buf->dma_addr);
    if (buf->cpu_addr == NULL)
        return -ENOMEM; /* Out of juice */

    /* There is no way to pass extra information to the user. And I am too lazy
       to implement this mmap() call using ioctl(). So we simply tell the user
       the bus address of this buffer by copying it to the allocated buffer
       itself. Hacks, hacks everywhere. */
    memcpy(buf->cpu_addr, &buf->dma_addr, sizeof(buf->dma_addr));

    buf->size = size;
    buf->priv_data = dev;
    vma->vm_ops = &a2gx_dma_vma_ops;
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    vma->vm_private_data = buf;

    /*
     * Map this DMA buffer into user space.
     */
    if (remap_pfn_range(vma, vma->vm_start,
                        vmalloc_to_pfn(buf->cpu_addr),
                        size, vma->vm_page_prot))
    {
        /* Out of luck, rollback... */
        pci_free_consistent(dev->pci_dev, buf->size, buf->cpu_addr,
                            buf->dma_addr);
        buf->cpu_addr = NULL;
        return -EAGAIN;
    }

    return 0; /* All good! */
}

一旦这些都到位,用户空间应用程序几乎可以做所有事情——通过读取/写入 I/O 寄存器来控制设备,分配和释放任意大小的 DMA 缓冲区,并让设备执行 DMA 事务。唯一缺少的部分是中断处理。我在用户空间进行轮询,烧毁我的 CPU,并禁用了中断。

希望能帮助到你。祝你好运!

于 2013-05-13T02:32:29.893 回答
2

我对实施的方向感到困惑。我想要...

在设计驱动程序时考虑应用程序。
数据移动的性质、频率、大小以及系统中可能发生的其他情况是什么?

传统的读/写 API 是否足够?直接将设备映射到用户空间可以吗?反射(半连贯)共享内存是否可取?

如果数据易于理解,手动操作数据(读/写)是一个很好的选择。对于内联副本,使用通用 VM 和读/写可能就足够了。直接将不可缓存的访问映射到外设很方便,但可能很笨拙。如果访问是大块相对不频繁的移动,那么使用常规内存、拥有驱动引脚、转换地址、DMA 和释放页面可能是有意义的。作为优化,页面(可能很大)可以预先固定和翻译;然后驱动器可以识别准备好的内存并避免动态翻译的复杂性。如果有很多小的 I/O 操作,让驱动器异步运行是有意义的。如果优雅很重要,VM 脏页标志可用于自动识别需要移动的内容,并且 (meta_sync()) 调用可用于刷新页面。也许上述作品的混合......

在深入研究细节之前,人们往往不关注更大的问题。通常最简单的解决方案就足够了。构建行为模型的一点努力可以帮助指导哪种 API 更可取。

于 2014-04-17T13:10:10.547 回答
0
first_page_offset = udata & PAGE_MASK; 

这似乎是错误的。它应该是:

first_page_offset = udata & ~PAGE_MASK;

或者

first_page_offset = udata & (PAGE_SIZE - 1)
于 2016-02-16T06:47:09.050 回答
0

值得一提的是,具有 Scatter-Gather DMA 支持和用户空间内存分配的驱动程序效率最高,性能最高。但是,如果我们不需要高性能,或者我们想在一些简化的条件下开发驱动程序,我们可以使用一些技巧。

放弃零拷贝设计。当数据吞吐量不太大时值得考虑。在这样的设计中,数据可以通过 copy_to_user(user_buffer, kernel_dma_buffer, count); user_buffer 复制给用户,例如字符设备 read() 系统调用实现中的缓冲区参数。我们仍然需要注意kernel_dma_buffer分配。例如,它可能通过从dma_alloc_coherent()调用中获得的内存。

另一个技巧是在启动时限制系统内存,然后将其用作巨大的连续 DMA 缓冲区。它在驱动程序和 FPGA DMA 控制器开发过程中特别有用,不推荐在生产环境中使用。假设 PC 有 32GB 的 RAM。如果我们添加mem=20GB到内核启动参数列表中,我们可以使用 12GB 作为巨大的连续 dma 缓冲区。要将此内存映射到用户空间,只需将 mmap() 实现为

remap_pfn_range(vma,
    vma->vm_start,
    (0x500000000 >> PAGE_SHIFT) + vma->vm_pgoff, 
    vma->vm_end - vma->vm_start,
    vma->vm_page_prot)

当然,这 12GB 被操作系统完全省略,只能由将其映射到其地址空间的进程使用。我们可以尝试通过使用连续内存分配器(CMA)来避免它。

同样,上述技巧不会取代完整的 Scatter-Gather、零复制 DMA 驱动程序,但在开发期间或在一些性能较低的平台中很有用。

于 2018-05-14T07:36:21.713 回答