2011-04-04 287 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

相關問題