2011-04-04 285 views
25

我想从支持DMA的PCIe硬件设备中尽快获取数据到用户空间。Linux内核设备驱动程序将DMA从设备传输到用户空间内存

问:我如何结合“直接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'able内存/缓存器?)
    • 我可以得到一个物理地址从而传递给设备,这样所有的用户空间所要做的就是对驱动程序执行读操作
    • 读方法会激活DMA传输,阻塞等待DMA完成中断然后释放后续读取的用户空间(用户空间现在可安全地使用/读取内存)。

我是否需要单页流映射,建立映射和用户空间的缓冲区与get_user_pagesdma_map_page映射?

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

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

非常感谢提前。

+0

以来,它一直在Linux下我最后的驱动程序开发的年龄,但我一直保留DMA可用内存,然后被映射到用户页面。在过去,并非所有的地址都可以用于DMA传输。现在可能并非如此。另一个提示可能是查看Linux驱动程序的视频,尤其是涉及BT848/BT878芯片的视频。我希望你能在那里找到有用的东西。 – jdehaan 2011-04-04 13:51:37

+0

我知道这个问题是类似的:http://stackoverflow.com/q/3333959/119790 – 2011-04-04 14:08:45

+0

我正在处理类似的问题。我很想知道你最终走了哪条路。 – mksuth 2011-06-30 14:47:25

回答

13

我实际上正在处理完全相同的事情,现在我要去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); 

请注意,这只是一个一般的方法,我一直工作在这个驱动程序在过去几个星期了,还没有实际测试它......所以,请不要把这个伪代码为福音,是一定要仔细检查所有的逻辑和参数;-)。

+0

虽然解决方案没有直接帮助我,但它是一个有趣的阅读。作为唯一的答案,我会默认接受这个。但是,很好的书面答案应该得到很多尊重和信任。 – 2011-04-12 08:53:16

+0

我知道这个线程是在5年前开始的,但这里希望我的问题不会被忽略。看来,除了user405725的回答之外,必须等待DMA完成。如果不这样做,那么可能意味着,如果出现问题,用户页面可能会被不必要地留下。我对么?这似乎是一个非常同步的操作。正确? – 2015-07-07 23:43:10

+0

正确。我想,可以异步执行DMA,但需要向用户空间应用程序提供某种通知,以便他们知道何时可以安全地使用/覆盖内存。虽然这是我选择的路径,但同步很容易实现。至于事情出错了,如果他们这样做了,你就会有点不爽,而且我不确定它甚至可以检测到。我在5年内没有看过这个,尽管如此,我肯定会过时的;-) – Rakis 2015-07-08 15:05:44

12

你基本上有正确的观念:在2.1,你可以有用户空间分配任何旧内存。你确实希望它与页面对齐,所以posix_memalign()是一个方便使用的API。

然后在这个缓冲器某种方式的用户空间虚拟地址和大小的用户空间通; ioctl()是一个很好的快速和肮脏的方式来做到这一点。在内核中,分配适当大小的缓冲区数组struct page* - user_buf_size/PAGE_SIZE条目 - 并使用get_user_pages()获取用户空间缓冲区的struct page *列表。

一旦你有了,你可以分配一个struct scatterlist的数组,它与你的页面数组大小相同,并循环遍历sg_set_page()的页面列表。神光列表设置后,您的散布的阵列上做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。

+0

如果我们使用连续内存(而不是SG)和dma_alloc_coherent() - 它会返回连续的内存 - 那么我们不能只在用户空间中调用mmap吗? – ransh 2016-12-19 15:16:40

6

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

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

基本上,它归结为一个自定义实现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,并禁用了中断。

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

+0

你的[link](http://lazarenko.me/2013/05/12/kernel-bypass-technique/)看起来是404? – Chiggs 2014-01-03 12:35:01

1

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

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

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

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

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

0
first_page_offset = udata & PAGE_MASK; 

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

first_page_offset = udata & ~PAGE_MASK; 

first_page_offset = udata & (PAGE_SIZE - 1) 
+0

这是正确的。在Linux内核中,'PAGE_MASK'几乎被普遍定义为'(〜(PAGE_SIZE-1))',所以'udata&PAGE_MASK'将屏蔽页面偏移量而不是保留它。 – apriori 2017-06-03 12:22:43

相关问题