Linux内核设备驱动程序从设备DMA到用户空间内存

36

我想尽快将来自启用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可用内存/缓冲区?)
    • 我可以从中获取物理地址以传递给设备,以便所有用户空间只需对驱动程序执行读取操作
    • 读取方法将激活DMA传输,阻止等待DMA完成中断,并在此后释放用户空间读取(现在用户空间可以安全地使用/读取内存)。

我需要单页面流式映射,设置映射和使用get_user_pages dma_map_page的用户空间缓冲区吗?

到目前为止,我的代码在从用户空间给定地址处设置了get_user_pages(我称其为直接I/O部分)。然后,使用get_user_pages的页面进行dma_map_page。我将dma_map_page的返回值作为DMA物理传输地址提供给设备。

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

非常感谢。


自从我上次在Linux下进行驱动程序开发以来已经过去了很长时间,但我始终保留了可用于DMA的内存,然后将其映射到用户页面。在过去,并非所有地址都可以用于DMA传输。现在可能不是这种情况。另一个提示是查看有关视频Linux驱动程序的视频,特别是涉及BT848 / BT878芯片的视频。我希望您能在那里找到有用的东西。 - jdehaan
我知道这个问题很相似:https://dev59.com/p3A75IYBdhLWcg3wW3y8 - Ian Vaughan
我正在解决一个类似的问题。我很想知道你最终采取了什么方法。 - mksuth
1
如果我们使用dma_alloc_coherent()返回连续内存而不是SG,那么我们不能在用户空间中调用mmap吗? - ransh
@ransh:是的,如果你的驱动程序实现了mmap文件操作。 - Woodrow Barlow
6个回答

19

我正在做完全相同的事情,目前我正在使用ioctl()方法。一般的想法是用户空间分配用于DMA传输的缓冲区,并使用ioctl()将此缓冲区的大小和地址传递给设备驱动程序。然后,驱动程序将使用散射-聚集列表以及流式DMA API直接传输数据到设备和用户空间缓冲区。

我正在使用的实现策略是,在驱动程序中,ioctl()进入循环,以256k的块DMA传输用户空间缓冲区(这是硬件强制限制的散布/聚集项处理数量)。这被隔离在一个阻塞到每个传输完成的函数内(见下文)。当所有字节都传输完或增量传输函数返回错误时,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);

请注意,这只是一个通用的方法,我最近几周一直在开发这个驱动程序,但尚未实际测试它... 因此,请不要把这段伪代码当作圣经,并确保仔细检查所有逻辑和参数;-)。


1
尽管这个解决方案并没有直接帮助到我,但它是一篇很有意思的阅读材料。作为唯一的答案,我默认接受这个答案。但是对于这个写得很好的答案,我非常尊敬并给予赞誉。 - Ian Vaughan
我知道这个帖子是5年前开始的,但希望我的问题不会被忽视。除了user405725的回答外,似乎必须等待DMA完成。如果不这样做,可能会导致用户页面在出现问题时不必要地被固定。我正确吗?这似乎是一个非常同步的操作。对吗? - Andrew Falanga
正确。我想异步执行DMA是可能的,但需要向用户空间应用程序提供某种通知,以便他们知道何时可以安全地使用/覆盖内存。但同步实现很容易,因此我选择了这条路。至于出现问题,如果真的出现问题,你会感到困扰,而且我不确定它是否可检测。虽然我已经有5年没有关注过这个问题,所以我的知识可能有点过时;-) - Rakis
对于未来的读者:请注意,ioctl 通常比 dma 的性能差得多(但对于大多数用例来说通常还是可以接受的)。 - Woodrow Barlow
您还需要为sg创建一个流DMA映射,以便与(特定的)设备进行通信。dma_map_sg()。这就是构建scatterlist的全部意义。 - Brad
我成功地使用了这个答案来制作一个DMA内核模块。虽然它不完全符合问题的要求,但也许源代码会对人们有所帮助: https://github.com/esophagus-now/mpsoc_axidma - Marco Merlini

15
您的想法基本上是正确的:在2.1中,用户空间可以分配任意的内存。您需要将其页面对齐,因此posix_memalign()是一个方便的API。
然后让用户空间以某种方式传递用户空间虚拟地址和缓冲区大小;ioctl()是一种好的快速且不太正式的方法来实现这一点。在内核中,分配一个适当大小的struct page*缓冲区数组--user_buf_size/PAGE_SIZE项--并使用get_user_pages()获取用户空间缓冲区的struct page*列表。
一旦你有了这个,你可以分配一个与你的页面数组相同大小的struct scatterlist数组,并循环遍历页面列表来做sg_set_page()。在设置完sg列表之后,对于scatterlist数组进行dma_map_sg(),然后你就可以获取每个scatterlist中的sg_dma_addresssg_dma_len(请注意,你必须使用dma_map_sg()的返回值,因为DMA映射代码可能会合并条目导致映射的数量较少)。
这样就可以得到所有要传递给设备的总线地址,然后可以按照需要触发DMA并等待它。您已有的基于read()的方案可能很好。
您可以参考drivers/infiniband/core/umem.c,特别是ib_umem_get(),了解一些构建此映射的代码,尽管该代码需要处理的普遍性可能会使其有点混乱。
或者,如果您的设备不能很好地处理散列表(scatter/gather lists)且需要连续的内存,则可以使用get_free_pages()来分配物理上连续的缓冲区,并在其上使用dma_map_page()。为了让用户空间访问该内存,您的驱动程序只需实现一个mmap方法,而不是上面描述的ioctl。

如果我们使用dma_alloc_coherent()返回连续内存而不是SG,那么我们不能在用户空间中调用mmap吗? - ransh

7
有一段时间,我想允许用户空间应用程序分配DMA缓冲区并将其映射到用户空间,并获得物理地址以便能够在用户空间完全绕过Linux内核控制我的设备和执行DMA事务(总线主控)。然而,我使用了略微不同的方法。首先,我从一个最小的内核模块开始,该模块正在初始化/探测PCIe设备并创建字符设备。然后,该驱动程序允许用户空间应用程序做两件事:
1. 使用remap_pfn_range()函数将PCIe设备的I/O bar映射到用户空间。 2. 分配和释放DMA缓冲区,将它们映射到用户空间并将物理总线地址传递给用户空间应用程序。
基本上,这归结为一个自定义实现mmap()调用(虽然是file_operations)。其中一个易于处理的是I/O bar:
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;
}

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

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,并禁用了中断。
希望这可以帮助你。祝你好运!

您的链接似乎是404? - Chiggs

2

我对实现方向感到困惑。 我想...

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

传统的读/写 API 是否足够? 是否可以将设备直接映射到用户空间? 是否需要反射式(半一致性)共享内存?

如果数据易于理解,则手动操作数据(读/写)是一个很好的选择。使用通用 VM 和读/写可能足以进行内联复制。直接映射不可缓存的访问到外围设备很方便,但可能很笨拙。如果访问是相对不频繁的大块移动,则使用常规内存有意义,让驱动程序锁定,翻译地址,DMA 并释放页面。作为优化,页面(可能是巨大的)可以预先锁定和翻译;然后驱动程序可以识别准备好的内存,并避免动态翻译的复杂性。如果有大量的小 I/O 操作,则运行驱动程序异步运行是有意义的。如果要求优雅,则可以使用 VM 脏页标志自动识别需要移动的内容,并使用 (meta_sync()) 调用刷新页面。也许混合使用以上方法会起作用...

很多时候人们在深入细节之前不看更大的问题。通常最简单的解决方案就足够了。努力构建行为模型可以帮助确定哪种 API 更可取。


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驱动程序,但在开发时间或某些性能较低的平台上非常有用。


为什么要放弃零拷贝设计?将DMA内存映射到用户空间并从用户空间使用有什么问题? - Alexis
SGDMA和您所说的唯一区别在于连续的物理内存,而不是分配的整个内存大小。最终,用户空间将看到相同数量的连续虚拟内存,设备将看到PAGE_SIZE内存地址块。我错了吗? - Alexis

0
first_page_offset = udata & PAGE_MASK; 

这似乎是错误的。它应该是以下之一:

first_page_offset = udata & ~PAGE_MASK;

或者

first_page_offset = udata & (PAGE_SIZE - 1)

这是正确的。在Linux内核中,PAGE_MASK几乎普遍定义为(~(PAGE_SIZE-1)),因此udata & PAGE_MASK将屏蔽页面偏移而不是保留它。 - apriori

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接