dma-buf 由淺入深(五) —— File

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 由淺入深(四)—— mmap》中,曾提到過 dma_buf_fd() 這個函數,該函數用於創建一個新的 fd,並與 dma-buf 的文件關聯起來。本篇我們一起來重點學習 dma-buf 與 file 相關的操作接口,以及它們的注意事項。

file

早在第一篇《最簡單的 dma-buf 驅動程序》就曾說過,dma-buf 本質上是 buffer 與 file 的結合,不僅如此,該 file 還是個被 open 過的 file。從我們調用 dma_buf_export() 開始,這個 file 就已經被 open 了。而且該 file 還是個匿名文件,因此應用程序無法通過 fd = open(“name”) 的方式來獲取它所對應的 fd,只能依託於 exporter 驅動的 ioctl 接口,通過 dma_buf_fd() 來獲取,就像上一篇的示例一那樣。

fd

如下內核 API 實現了 dma-buf 與 fd 之間的相互轉換:

  • dma_buf_fd():dma-buf --> new fd
  • dma_buf_get():fd --> dma-buf

通常使用方法如下:

fd = dma_buf_fd(dmabuf);
dmabuf = dma_buf_get(fd);

get / put

只要是文件,內部都會有一個引用計數(f_count)。當使用 dma_buf_export() 函數創建 dma-buf 時,該引用計數被初始化爲1;當這個引用計數爲0時,則會自動觸發 dma_buf_opsrelease 回調接口,並釋放 dma-buf 對象。

在 linux 內核中操作 file 引用計數的常用函數爲 fget()fput(),而 dma-buf 又在此基礎上進行了封裝,如下:

  • get_dma_buf()
  • dma_buf_get()
  • dma_buf_put()

爲了不讓大家混淆,我做了如下表格區分:

函數 區別
get_dma_buf() 僅引用計數加1
dma_buf_get() 引用計數加1,並將 fd 轉換成 dma_buf 指針
dma_buf_put() 引用計數減1
dma_buf_fd() 引用計數不變,僅創建 fd

release

通常 release 回調接口用來釋放 dma-buf 所對應的物理 buffer。當然,凡是所有和該 dma-buf 相關的私有數據也都應該在這裏被 free 掉。

前面說過,只有當 dma-buf 的引用計數遞減到0時,纔會觸發 release 回調接口。因此

  • 如果不想讓你正在使用的 buffer 被突然釋放,請提前 get;
  • 如果想在 kernel space 釋放 buffer,請使勁 put;
  • 如果想從 user space 釋放 buffer,請嘗試 close;

這就是爲什麼在內核設備驅動中,我們會看到那麼多 dma-buf get 和 put 的身影。

這也是爲什麼在第一篇《最簡單的 dma-buf 驅動程序》中,一旦 exporter-dummy.ko 被成功加載了,就無法被 rmmod 的原因。因爲沒有任何程序來修改該 dma-buf 的引用計數,自始自終都保持爲1,所以也就無法執行 release 接口,更不會執行 module put。

示例

在前面所有的 exporter 驅動中,都定義了一個 dmabuf_exported 全局變量,方便 importer 驅動通過 extern 關鍵字來引用。這就造成了 exporter 驅動與 importer 驅動之間的強耦合,不僅編譯時 importer 需要依賴 exporter 的文件,就連運行時也要依賴 exporter 模塊先加載。

在這裏插入圖片描述

這次,我們將 dmabuf_exported 全局變量改爲 static 靜態變量,並藉助於 dma_buf_fd()dma_buf_get() 來徹底解除 importer 與 exporter 驅動之間的耦合。

exporter 驅動

基於上一篇示例一中的 exporter 驅動,將 dmabuf_exported 全局變量修改爲 static 靜態變量,其它代碼不做修改。

exporter-fd.c

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

static struct dma_buf *dmabuf_exported;

...

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 long exporter_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	int fd = dma_buf_fd(dmabuf_exported, O_CLOEXEC);
	copy_to_user((int __user *)arg, &fd, sizeof(fd));

	return 0;
}
 
static struct file_operations exporter_fops = {
	.owner		= THIS_MODULE,
	.unlocked_ioctl	= exporter_ioctl,
};
 
static struct miscdevice mdev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "exporter",
	.fops = &exporter_fops,
};
 
static int __init exporter_init(void)
{
	dmabuf_exported = exporter_alloc_page();
	return misc_register(&mdev);
}

static void __exit exporter_exit(void)
{
	misc_deregister(&mdev);
}

module_init(exporter_init);
module_exit(exporter_exit);

在 ioctl 中,通過 dma_buf_fd() 創建一個新的 fd,並通過 copy_to_user() 將該 fd 的值傳給上層應用程序。

importer 驅動

我們基於《dma-buf 由淺入深(二) —— kmap/vmap》中的 importer-kmap.c 進行修改。

importer-fd.c

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

static int importer_test(struct dma_buf *dmabuf)
{
	void *vaddr;

	vaddr = dma_buf_kmap(dmabuf, 0);
	pr_info("read from dmabuf kmap: %s\n", (char *)vaddr);
	dma_buf_kunmap(dmabuf, 0, vaddr);

	vaddr = dma_buf_vmap(dmabuf);
	pr_info("read from dmabuf vmap: %s\n", (char *)vaddr);
	dma_buf_vunmap(dmabuf, vaddr);

	return 0;
}

static long importer_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	int fd;
	struct dma_buf *dmabuf;

	copy_from_user(&fd, (void __user *)arg, sizeof(int));

	dmabuf = dma_buf_get(fd);
	importer_test(dmabuf);
	dma_buf_put(dmabuf);

	return 0;
}
 
static struct file_operations importer_fops = {
	.owner	= THIS_MODULE,
	.unlocked_ioctl	= importer_ioctl,
};
 
static struct miscdevice mdev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "importer",
	.fops = &importer_fops,
};
 
static int __init importer_init(void)
{
	return misc_register(&mdev);
}

static void __exit importer_exit(void)
{
	misc_deregister(&mdev);
}

module_init(importer_init);
module_exit(importer_exit);

與 importer-kmap 驅動相比,上面的驅動新增了 misc driver 部分,通過 ioctl 接口來接收上層傳下來的 fd,並通過 dma_buf_get() 將 fd 轉換成 dma-buf 指針。隨後便在 kernel 空間通過 kmap/vmap 來訪問該 dma-buf 的物理內存。

需要注意的是,dma_buf_get() 會增加 dma-buf 的引用計數,所以在使用完 dma-buf 後,要記得用 dma_buf_put() 將引用計數再減回來,否則引用計數不匹配,將導致 dma-buf 的 release 接口無法被回調,從而導致 buffer 無法被釋放,造成內存泄漏。

userspace 程序

share_fd.c

int main(int argc, char *argv[])
{
	int fd;
	int dmabuf_fd = 0;

	fd = open("/dev/exporter", O_RDONLY);
	ioctl(fd, 0, &dmabuf_fd);
	close(fd);

	fd = open("/dev/importer", O_RDONLY);
	ioctl(fd, 0, &dmabuf_fd);
	close(fd);

	return 0;
}

該應用程序做的事情很簡單,就是將 dma-buf 的 fd 從 exporter 傳遞給 importer 驅動。這裏爲了簡單起見,ioctl() 第二個參數沒有任何意義,可以忽略。

開發環境

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

運行

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

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

將看到如下打印結果:

read from dmabuf kmap: hello world!
read from dmabuf vmap: hello world!

通過上面的運行結果我們看到,即使 importer 驅動先加載,也不會影響應用程序的輸出結果,真正實現了 importer 驅動與 exporter 驅動之間的解耦合。

跨進程 fd

做 Linux 應用開發的同事都知道,fd 屬於進程資源,它的作用域只在單個進程空間範圍內有效,即同樣的 fd 值,在進程 A 和 進程 B 中所指向的文件是不同的。因此 fd 是不能在多個進程之間共享的,也就是說 dma_buf_fd()dma_buf_get() 只能是在同一進程中調用。

但是有的小夥伴就會問了:在 Android 系統中,dma-buf 幾乎都是由 ION 來統一分配的,ION 所在進程(Allocator)在分配好 buffer 以後,會將該 buffer 所對應的 fd 傳給其它進程,如 SurfaceFlinger 或 CameraService,而這些進程在收到 fd 後在各自的底層驅動中都能正確的轉換成相應的 dma-buf,那這又是如何做到的呢?

fd 並不是完全不能在多進程中共享,而是需要採用特殊的方式進行傳遞。在 linux 系統中,最常用的做法就是通過 socket 來實現 fd 的傳遞。而在 Android 系統中,則是通過 Binder 來實現的。需要注意的是,傳遞後 fd 的值可能會發生變化,但是它們所指向的文件都是同一文件。關於 Binder 如何實現 fd 跨進程共享,請見參考資料中的第一篇文章,這裏不做贅述。總之,有了 Binder,dma_buf_fd()dma_buf_get() 就可以不用嚴格限制在同一進程中使用了。

總結

  • 爲什麼需要 fd ?
  1. 方便應用程序直接在 user space 訪問該 buffer(通過 mmap);
  2. 方便該 buffer 在各個驅動模塊之間流轉,而無需拷貝;
  3. 降低了各驅動之間的耦合度;
  • 如何實現 fd 跨進程共享? Binder!
  • get / put 將影響 dma-buf 的內存釋放

參考資料

  1. Android Binder傳遞文件描述符原理分析
  2. 進程間傳遞文件描述符–sendmsg,recvmsg



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

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