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

时间:2011-04-04 13:44:08

标签: linux linux-kernel linux-device-driver dma

我希望尽快将支持DMA的PCIe硬件设备中的数据导入用户空间。

问:如何将“使用/和/通过DMA传输直接I / O连接到用户空间”

  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'able内存/缓冲区?)
    • 我可以从中获取物理地址以传递给设备,以便所有用户空间必须执行的是对驱动程序执行读取
    • read方法将激活DMA传输,阻塞等待DMA完成中断并随后释放用户空间读取(用户空间现在可以安全使用/读取内存)。
  3. 我是否需要使用get_user_pages dma_map_page映射的单页流映射,设置映射和用户空间缓冲区?

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

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

    非常感谢提前。

6 个答案:

答案 0 :(得分:15)

我实际上正在做同样的事情,我正在走ioctl()路线。一般的想法是用户空间分配将用于DMA传输的缓冲区,并且ioctl()将用于将此缓冲区的大小和地址传递给设备驱动程序。然后,驱动程序将使用分散 - 收集列表以及流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);

请注意,这只是一种常规方法,我在过去几周一直在研究这个驱动程序,并且尚未对它进行实际测试...所以请不要将这个伪代码视为福音并且是一定要仔细检查所有逻辑和参数; - )。

答案 1 :(得分:12)

你基本上有正确的想法:在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(),然后您可以为散点列表中的每个条目获取sg_dma_addresssg_dma_len(请注意,您必须使用返回值为dma_map_sg(),因为您可能最终会得到较少的映射条目,因为事物可能会被DMA映射代码合并。)

这会将所有总线地址传递给您的设备,然后您可以触发DMA并等待它,无论您想要什么。您拥有的基于read()的方案可能很好。

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

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

答案 2 :(得分:6)

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

  1. 使用remap_pfn_range()功能将PCIe设备的I / O栏映射到用户空间。
  2. 分配并释放DMA缓冲区,将它们映射到用户空间,并将物理总线地址传递给用户空间应用程序。
  3. 基本上,它归结为mmap()调用的自定义实现(尽管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;
    }
    

    另一个使用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,并禁用中断。

    希望它有所帮助。祝你好运!

答案 3 :(得分:1)

  

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

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

传统的读/写API是否足够? 是否将设备直接映射到用户空间? 是否需要反射(半连贯)共享存储器?

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

在深入研究细节之前,人们常常不会考虑更大的问题。通常最简单的解决方案就足够了。构建行为模型的一点努力可以帮助指导API的优先选择。

答案 4 :(得分:0)

first_page_offset = udata & PAGE_MASK; 

似乎错了。应该是:

first_page_offset = udata & ~PAGE_MASK;

first_page_offset = udata & (PAGE_SIZE - 1)

答案 5 :(得分: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,并且只能由将其映射到其地址空间的进程使用。我们可以尝试使用Contiguous Memory Allocator(CMA)来避免它。

上述技巧不会取代完整的Scatter-Gather,零拷贝DMA驱动程序,但在开发时或某些性能较差的平台上非常有用。