dma-buf 由淺入深(三) —— map attachment

dma-buf 由淺入深(一) —— 最簡單的 dma-buf 驅動程序
dma-buf 由淺入深(二) —— kmap / vmap
dma-buf 由淺入深(三) —— map attachment
dma-buf 由淺入深(四) —— mmap
dma-buf 由淺入深(五) —— File
dma-buf 由淺入深(六) —— begin / end cpu_access
dma-buf 由淺入深(七) —— alloc page 版本
dma-buf 由淺入深(八) —— ION 簡化版


前言

在上一篇《dma-buf 由淺入深(二)—— kmap/vmap》中,我們學習瞭如何使用 CPU 在 kernel 空間訪問 dma-buf 物理內存,但通常這種操作方法在內核中出現的頻率並不高,因爲 dma-buf 設計之初就是爲滿足那些大內存訪問需求的硬件而設計的,如GPU/DPU。在這種場景下,如果使用CPU直接去訪問 memory,那麼性能會大大降低。因此,dma-buf 在內核中出現頻率最高的還是它的 dma_buf_attach()dma_buf_map_attachment() 接口。本篇我們就一起來學習如何通過這兩個 API 來實現 DMA 硬件對 dma-buf 物理內存的訪問。

DMA Access

dma-buf 提供給 DMA 硬件訪問的 API 主要就兩個:

  • dma_buf_attach()
  • dma_buf_map_attachment()

這兩個接口調用有嚴格的先後順序,必須先 attach,再 map attachment,因爲後者的參數是由前者提供的,所以通常這兩個接口形影不離。

與上面兩個 API 相對應的反向操作接口爲: dma_buf_dettach()dma_buf_unmap_attachment(),具體我就不細說了。

sg_table

由於 DMA 操作涉及到內核中 dma-mapping 諸多接口及概念,本篇爲避重就輕,無意講解。但 sg_table 的概念必須要提一下,因爲它是 dma-buf 供 DMA 硬件訪問的終極目標,也是 DMA 硬件訪問離散 memory 的唯一途徑!

sg_table 本質上是由一塊塊單個物理連續的 buffer 所組成的鏈表,但是這個鏈表整體上看卻是離散的,因此它可以很好的描述從 高端內存 上分配出的離散 buffer。當然,它同樣可以用來描述從 低端內存 上分配出的物理連續 buffer。
如下圖所示:
在這裏插入圖片描述

sg_table 代表着整個鏈表,而它的每一個鏈表項則由 scatterlist 來表示。因此,1個scatterlist 也就對應着一塊 物理連續 的 buffer。我們可以通過如下接口來獲取一個 scatterlist 對應 buffer 的物理地址長度

  • sg_dma_address(sgl)
  • sg_dma_len(sgl)

有了 buffer 的物理地址和長度,我們就可以將這兩個參數配置到 DMA 硬件寄存器中,這樣就可以實現 DMA 硬件對這一小塊 buffer 的訪問。那如何訪問整塊離散 buffer 呢?當然是用個 for 循環,不斷的解析 scatterlist,不斷的配置 DMA 硬件寄存器啦!

對於現代多媒體硬件來說,IOMMU 的出現,解決了程序員編寫 for 循環的煩惱。因爲在 for 循環中,每次配置完 DMA 硬件寄存器後,都需要等待本次 DMA 傳輸完畢,然後才能進行下一次循環,這大大降低了軟件的執行效率。而 IOMMU 的功能就是用來解析 sg_table 的,它會將 sg_table 內部一個個離散的小 buffer 映射到自己內部的設備地址空間,使得這整塊 buffer 在自己內部的設備地址空間上是連續的。這樣,在訪問離散 buffer 的時候,只需要將 IOMMU 映射後的設備地址(與 MMU 映射後的 CPU 虛擬地址不是同一概念)和整塊 buffer 的 size 配置到 DMA 硬件寄存器中即可,中途無需再多次配置,便完成了 DMA 硬件對整塊離散 buffer 的訪問,大大的提高了軟件的效率。

dma_buf_attach()

該函數實際是 “dma-buf attach device” 的縮寫,用於建立一個 dma-buf 與 device 的連接關係,這個連接關係被存放在新創建的 dma_buf_attachment 對象中,供後續調用 dma_buf_map_attachment() 使用。

該函數對應 dma_buf_ops 中的 attach 回調接口,如果 device 對後續的 map attachment 操作沒有什麼特殊要求,可以不實現。

dma_buf_map_attachment()

該函數實際是 “dma-buf map attachment into sg_table” 的縮寫,它主要完成2件事情:

  1. 生成 sg_table
  2. 同步 Cache

選擇返回 sg_table 而不是物理地址,是爲了兼容所有 DMA 硬件(帶或不帶 IOMMU),因爲 sg_table 既可以表示連續物理內存,也可以表示非連續物理內存。

同步 Cache 是爲了防止該 buffer 事先被 CPU 填充過,數據暫存在 Cache 中而非 DDR 上,導致 DMA 訪問的不是最新的有效數據。通過將 Cache 中的數據回寫到 DDR 上可以避免此類問題的發生。同樣的,在 DMA 訪問內存結束後,需要將 Cache 設置爲無效,以便後續 CPU 直接從 DDR 上(而非 Cache 中)讀取該內存數據。通常我們使用如下流式 DMA 映射接口來完成 Cache 的同步:

  • dma_map_single() / dma_unmap_single()
  • dma_map_page() / dma_unmap_page()
  • dma_map_sg() / dma_unmap_sg()

關於更多 dma_map_*() 函數的說明,推薦大家閱讀 落塵紛擾 的博客《Linux內存管理 —— DMA和一致性緩存》,言簡意賅,適合初學者。

dma_buf_map_attachment() 對應 dma_buf_ops 中的 map_dma_buf 回調接口,如第一篇《最簡單的 dma-buf 驅動程序》所述,該回調接口(包括 unmap_dma_buf 在內)被強制要求實現,否則 dma_buf_export() 將執行失敗。

爲什麼需要 attach 操作 ?

同一個 dma-buf 可能會被多個 DMA 硬件訪問,而每個 DMA 硬件可能會因爲自身硬件能力的限制,對這塊 buffer 有自己特殊的要求。比如硬件 A 的尋址能力只有0x0 ~ 0x10000000,而硬件 B 的尋址能力爲 0x0 ~ 0x80000000,那麼在分配 dma-buf 的物理內存時,就必須以硬件 A 的能力爲標準進行分配,這樣硬件 A 和 B 都可以訪問這段內存。否則,如果只滿足 B 的需求,那麼 A 可能就無法訪問超出 0x10000000 地址以外的內存空間,道理其實類似於木桶理論。
因此,attach 操作可以讓 exporter 驅動根據不同的 device 硬件能力,來分配最合適的物理內存

通過設置 device->dma_params 參數,來告知 exporter 驅動該 DMA 硬件的能力限制。

但是在上一篇的示例中,dma-buf 的物理內存都是在 dma_buf_export() 的時候就分配好了的,而 attach 操作只能在 export 之後才能執行,那我們如何確保已經分配好的內存是符合硬件能力要求的呢?這就引出了下面的話題。

何時分配內存?

答案是:既可以在 export 階段分配,也可以在 map attachment 階段分配,甚至可以在兩個階段都分配,這通常由 DMA 硬件能力來決定。

首先,驅動人員需要統計當前系統中都有哪些 DMA 硬件要訪問 dma-buf;
然後,根據不同的 DMA 硬件能力,來決定在何時以及如何分配物理內存。

通常的策略如下(假設只有 A、B 兩個硬件需要訪問 dma-buf ):

  • 如果硬件 A 和 B 的尋址空間有交集,則在 export 階段進行內存分配,分配時以 A / B 的交集爲準;
  • 如果硬件 A 和 B 的尋址空間沒有交集,則只能在 map attachment 階段分配內存。

對於第二種策略,因爲 A 和 B 的尋址空間沒有交集(即完全獨立),所以它們實際上是無法實現內存共享的。此時的解決辦法是: A 和 B 在 map attachment 階段,都分配各自的物理內存,然後通過 CPU 或 通用DMA 硬件,將 A 的 buffer 內容拷貝到 B 的 buffer 中去,以此來間接的實現 buffer “共享”。

另外還有一種策略,就是不管三七二十一,先在 export 階段分配好內存,然後在首次 map attachment 階段通過 dma_buf->attachments 鏈表,與所有 device 的能力進行一一比對,如果滿足條件則直接返回 sg_table;如果不滿足條件,則重新分配符合所有 device 要求的物理內存,再返回新的 sg_table。

關於更多 dma_buf_map_attachment() 的說明,詳見參考資料中的 ELCE-DMABUF.pdf 文檔。


示例代碼

本示例基於第一篇的 exporter-dummy.c 進行修改,對 dma_buf_ops 中的 attachmap_dma_buf 回調接口進行實現。當然,爲了方便演示,我們仍然像之前那樣,在 exporter_alloc_page() 中事先分配好了 dma-buf 的物理內存。

exporter-sg.c

#include <linux/dma-buf.h>
#include <linux/module.h>
#include <linux/slab.h>

struct dma_buf *dmabuf_exported;
EXPORT_SYMBOL(dmabuf_exported);

static int exporter_attach(struct dma_buf *dmabuf, struct device *dev,
			struct dma_buf_attachment *attachment)
{
	pr_info("dmabuf attach device: %s\n", dev_name(dev));
	return 0;
}

static void exporter_detach(struct dma_buf *dmabuf, struct dma_buf_attachment *attachment)
{
	pr_info("dmabuf detach device: %s\n", dev_name(attachment->dev));
}

static struct sg_table *exporter_map_dma_buf(struct dma_buf_attachment *attachment,
					 enum dma_data_direction dir)
{
	void *vaddr = attachment->dmabuf->priv;
	struct sg_table *table;

	table = kmalloc(sizeof(*table), GFP_KERNEL);

	sg_alloc_table(table, 1, GFP_KERNEL);
	sg_dma_len(table->sgl) = PAGE_SIZE;
	sg_dma_address(table->sgl) = dma_map_single(NULL, vaddr, PAGE_SIZE, dir);

	return table;
}

static void exporter_unmap_dma_buf(struct dma_buf_attachment *attachment,
			       struct sg_table *table,
			       enum dma_data_direction dir)
{
	dma_unmap_single(NULL, sg_dma_address(table->sgl), PAGE_SIZE, dir);
	sg_free_table(table);
	kfree(table);
}

...

static const struct dma_buf_ops exp_dmabuf_ops = {
	.attach = exporter_attach,
	.detach = exporter_detach,
	.map_dma_buf = exporter_map_dma_buf,
	.unmap_dma_buf = exporter_unmap_dma_buf,
	...
};

static struct dma_buf *exporter_alloc_page(void)
{
	DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
	struct dma_buf *dmabuf;
	void *vaddr;

	vaddr = kzalloc(PAGE_SIZE, GFP_KERNEL);

	exp_info.ops = &exp_dmabuf_ops;
	exp_info.size = PAGE_SIZE;
	exp_info.flags = O_CLOEXEC;
	exp_info.priv = vaddr;

	dmabuf = dma_buf_export(&exp_info);

	sprintf(vaddr, "hello world!");

	return dmabuf;
}

static int __init exporter_init(void)
{
	dmabuf_exported = exporter_alloc_page();
	return 0; 
}

module_init(exporter_init);

在上面的 attach 實現中,我們僅僅只是打印了一句 log,其他什麼事情也不做。在 map_dma_buf 實現中,我們構造了一個 sg_table 對象,並通過調用 dma_map_single() 來獲取 dma_addr 以及實現 Cache 同步操作。

importer-sg.c

#include <linux/device.h>
#include <linux/dma-buf.h>
#include <linux/module.h>
#include <linux/slab.h>

extern struct dma_buf *dmabuf_exported;

static int importer_test(struct dma_buf *dmabuf)
{
	struct dma_buf_attachment *attachment;
	struct sg_table *table;
	struct device *dev;
	unsigned int reg_addr, reg_size;

	dev = kzalloc(sizeof(*dev), GFP_KERNEL);
	dev_set_name(dev, "importer");

	attachment = dma_buf_attach(dmabuf, dev);
	table = dma_buf_map_attachment(attachment, DMA_BIDIRECTIONAL);

	reg_addr = sg_dma_address(table->sgl);
	reg_size = sg_dma_len(table->sgl);
	pr_info("reg_addr = 0x%08x, reg_size = 0x%08x\n", reg_addr, reg_size);

	dma_buf_unmap_attachment(attachment, table, DMA_BIDIRECTIONAL);
	dma_buf_detach(dmabuf, attachment);

	return 0;
}

static int __init importer_init(void)
{
	return importer_test(dmabuf_exported);
}

module_init(importer_init);

示例描述:

  1. exporter 通過 kzalloc 分配了一個 PAGE 大小的物理連續 buffer;
  2. importer 驅動通過 extern 關鍵字導入了 exporter 的 dma-buf,並通過 dma_buf_map_attachment() 接口獲取到了該物理內存所對應的 sg_table,然後將該 sg_table 中的 address 和 size 解析到 reg_addr 和 reg_size 這兩個虛擬寄存器中。

開發環境

內核源碼 4.14.143
示例源碼 hexiaolong2008-GitHub/sample-code/dma-buf/03
開發平臺 Ubuntu14.04/16.04
運行平臺 my-qemu 仿真環境

運行

在 my-qemu 仿真環境中執行如下命令:

# insmod /lib/modules/4.14.143/kernel/drivers/dma-buf/exporter-sg.ko
# insmod /lib/modules/4.14.143/kernel/drivers/dma-buf/importer-sg.ko

將看到如下打印結果:

dmabuf attach device: importer
reg_addr = 0x7f6ee000, reg_size = 0x00001000
dmabuf detach device: importer

注意:執行 lsmod 命令時,必須先加載 exporter-sg.ko,後加載 importer-sg.ko,否則將出現符號依賴錯誤。

總結

  1. sg_table 是 DMA 硬件操作的關鍵;
  2. attach 的目的是爲了讓後續 map attachment 操作更靈活;
  3. map attachment 主要完成兩件事:生成 sg_table 和 Cache 同步;
  4. DMA 的硬件能力決定了 dma-buf 物理內存的分配時機;

通過本篇,我們學習瞭如何通過 dma_buf_attach()dma_buf_map_attachment() 來實現 DMA 硬件對 dma-buf 的訪問。在下一篇,我們將一起來學習如何在 userspace 來訪問 dma-buf 的物理內存。

參考資料

  1. ELCE-DMABUF.pdf
  2. DMA_Buffer_Sharing-_An_Introduction.pdf
  3. wowotech: Linux kernel scatterlist API介紹
  4. 落塵紛擾:《Linux內存管理 —— DMA和一致性緩存》



上一篇:《dma-buf 由淺入深(二)—— kmap/vmap》
下一篇:《dma-buf 由淺入深(四)—— mmap》
文章彙總:《DRM(Direct Rendering Manager)學習簡介》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章