Skip to the content.

DMA heap

Android 多媒体访问的内存接口由内核的 heap 模块提供,heap 模块内部由多种类型的 heap 可以在不同情况下使用。通过 heap 拿到内存后可以通过 smmu 映射接口转成虚拟地址给多媒体使用。

[TOC]

目前系统支持 system heap, carveout heap, cma heap,首先需要了解这几种 heap 的使用场景,以及怎样使用的。

数据结构

按照老规矩,先看数据结构。

用户态

在用户态都是调用 libdmabufheap 的接口,只需要获取到 fd 即可,可以参考 aosp/system/memory/libdmabufheap。

内核态

dma_heap

每种 heap 都有一个 dma_heap 表示(理论上来说,dma_heap 里面应该有个链表,管理所有的 dma_buf),

/**
 * struct dma_heap - represents a dmabuf heap in the system
 * @name:		used for debugging/device-node name
 * @ops:		ops struct for this heap
 * @heap_devt		heap device node
 * @list		list head connecting to list of heaps
 * @heap_cdev		heap char device
 * @heap_dev		heap device struct
 *
 * Represents a heap of memory from which buffers can be made.
 */
struct dma_heap {
	const char *name;
	const struct dma_heap_ops *ops; // 在初始化的时候注册
	void *priv;
	dev_t heap_devt;
	struct list_head list;
	struct cdev heap_cdev;
	struct kref refcount;
	struct device *heap_dev; // 每个 heap 都是以字符设备的方式在 /dev/dma_heap/ 目录下提供给用户态
};
dma_buf

heap 以 dma_buf 为单位管理内存,dma_buf 可以在不同设备间共享,

/**
 * struct dma_buf - shared buffer object
 *
 * This represents a shared buffer, created by calling dma_buf_export(). The
 * userspace representation is a normal file descriptor, which can be created by
 * calling dma_buf_fd().
 *
 * Shared dma buffers are reference counted using dma_buf_put() and
 * get_dma_buf().
 *
 * Device DMA access is handled by the separate &struct dma_buf_attachment.
 */
struct dma_buf {
	size_t size;
	struct file *file;
	struct list_head attachments; // 管理所有使用该 dma_buf 的设备
	const struct dma_buf_ops *ops;

    ...

	struct dma_buf_map vmap_ptr;
	const char *name;

	/** @list_node: node for dma_buf accounting and debugging. */
	struct list_head list_node;

	/** @priv: exporter specific private data for this buffer object. */
	void *priv;

    ...

};
dma_buf_attachment

每个设备在使用一块 dma_buf 之前,需要进行 attach 操作,所有 attach 到 dma_buf 的设备都由下面这个结构统一管理,

/**
 * struct dma_buf_attachment - holds device-buffer attachment data
 * @dmabuf: buffer for this attachment.
 * @dev: device attached to the buffer.
 * @node: list of dma_buf_attachment, protected by dma_resv lock of the dmabuf.
 * @sgt: cached mapping.
 * @dir: direction of cached mapping.
 * @peer2peer: true if the importer can handle peer resources without pages.
 * @priv: exporter specific attachment data.
 * @importer_ops: importer operations for this attachment, if provided
 * dma_buf_map/unmap_attachment() must be called with the dma_resv lock held.
 * @importer_priv: importer specific attachment data.
 * @dma_map_attrs: DMA attributes to be used when the exporter maps the buffer
 * through dma_buf_map_attachment.
 *
 * This structure holds the attachment information between the dma_buf buffer
 * and its user device(s). The list contains one attachment struct per device
 * attached to the buffer.
 */
struct dma_buf_attachment {
	struct dma_buf *dmabuf;
	struct device *dev;
	struct list_head node;
	struct sg_table *sgt;
	enum dma_data_direction dir;
	bool peer2peer;
	const struct dma_buf_attach_ops *importer_ops;
	void *importer_priv;
	void *priv;
	unsigned long dma_map_attrs;

    ...
};

使用流程

在用户态申请 dma buf 是通过 andorid 实现的 libdmabufheap 来申请的,其实现了如下接口,

BufferAllocator* CreateDmabufHeapBufferAllocator();

void FreeDmabufHeapBufferAllocator(BufferAllocator* buffer_allocator);

int DmabufHeapAlloc(BufferAllocator* buffer_allocator, const char* heap_name, size_t len,
                    unsigned int heap_flags, size_t legacy_align);
int DmabufHeapAllocSystem(BufferAllocator* buffer_allocator, bool cpu_access, size_t len,
                          unsigned int heap_flags, size_t legacy_align);

int DmabufHeapCpuSyncStart(BufferAllocator* buffer_allocator, unsigned int dmabuf_fd,
                           SyncType sync_type, int (*legacy_ion_cpu_sync)(int, int, void *),
                           void *legacy_ion_custom_data);

int DmabufHeapCpuSyncEnd(BufferAllocator* buffer_allocator, unsigned int dmabuf_fd,
                         SyncType sync_type, int (*legacy_ion_cpu_sync)(int, int, void*),
                         void* legacy_ion_custom_data);

BufferAllocator

在使用时,其先调用 CreateDmabufHeapBufferAllocator 来创建一个 BufferAllocator 实例,该实例有对应的 Alloc 等成员函数,

// 1. 创建 BufferAllocator
BufferAllocator* CreateDmabufHeapBufferAllocator() {
    return new BufferAllocator();
}

// 2. 申请 dma buf,返回 fd
int DmabufHeapAlloc(BufferAllocator* buffer_allocator, const char* heap_name, size_t len,
                    unsigned int heap_flags, size_t legacy_align) {
    if (!buffer_allocator)
        return -EINVAL;
    return buffer_allocator->Alloc(heap_name, len, heap_flags, legacy_align);
}

// 3. 只是一个封装
int BufferAllocator::Alloc(const std::string& heap_name, size_t len,
                           unsigned int heap_flags, size_t legacy_align) {
    int fd = DmabufAlloc(heap_name, len);

    return fd;
}

// 4. 首先根据 heap name 获取要申请的 heap 的 fd
// 然后使用 iotcl 系统调用,向内核发起请求,最终返回的也是 dma buf 的 fd
int BufferAllocator::DmabufAlloc(const std::string& heap_name, size_t len) {
    int fd = OpenDmabufHeap(heap_name);
    if (fd < 0) return fd;

    struct dma_heap_allocation_data heap_data{
        .len = len,  // length of data to be allocated in bytes
        .fd_flags = O_RDWR | O_CLOEXEC,  // permissions for the memory to be allocated
    };

    auto ret = TEMP_FAILURE_RETRY(ioctl(fd, DMA_HEAP_IOCTL_ALLOC, &heap_data));

    ...

    return heap_data.fd;
}

dma_heap_buffer_alloc

DMA_HEAP_IOCTL_ALLOC 在内核态的处理流程是 dma_heap_ioctl_allocate -> dma_heap_bufferfd_alloc -> dma_heap_buffer_alloc -> heap->ops->allocate 这里 ops->allocate 每种 heap 在初始化时都会注册,以 cma 为例,是cma_heap_allocate,其实这个命名我觉得有点问题,因为这个函数是分配 dma_buf 的,叫 cma_heap_buf_allocate 比较好。

static struct dma_buf *cma_heap_allocate(struct dma_heap *heap,
					 unsigned long len,
					 unsigned long fd_flags,
					 unsigned long heap_flags)
{
	struct cma_heap *cma_heap = dma_heap_get_drvdata(heap);
	struct cma_heap_buffer *buffer; // 这个 buffer 就是在内核中表示一块物理内存的结构
	DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
	size_t size = PAGE_ALIGN(len);
	pgoff_t pagecount = size >> PAGE_SHIFT;
	unsigned long align = get_order(size);
	struct page *cma_pages;
	struct dma_buf *dmabuf;
	int ret = -ENOMEM;
	pgoff_t pg;

	buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);

	INIT_LIST_HEAD(&buffer->attachments);
	mutex_init(&buffer->lock);
	buffer->len = size;

	if (align > CONFIG_CMA_ALIGNMENT)
		align = CONFIG_CMA_ALIGNMENT;

    // 分配内存
    // cma 采用 bitmap 的方式管理内存
    // 未来的开发中,如果有管理小块内存(100MB左右)的需求,可以参考这种管理方式
	cma_pages = cma_alloc(cma_heap->cma, pagecount, align, false);
	if (!cma_pages)
		goto free_buffer;

	/* Clear the cma pages */
    // 将这块 cma_pages 空间写 0
	if (PageHighMem(cma_pages)) {
        ...
	} else {
		memset(page_address(cma_pages), 0, size);
	}

	buffer->pages = kmalloc_array(pagecount, sizeof(*buffer->pages), GFP_KERNEL);

	for (pg = 0; pg < pagecount; pg++)
		buffer->pages[pg] = &cma_pages[pg];

	buffer->cma_pages = cma_pages;
	buffer->heap = cma_heap;
	buffer->pagecount = pagecount;

	/* create the dmabuf */
	exp_info.exp_name = dma_heap_get_name(heap);
	exp_info.ops = &cma_heap_buf_ops; // 这里就是该 buf 所有的回调函数,之后的处理都在里面,后面分析
	exp_info.size = buffer->len;
	exp_info.flags = fd_flags;
	exp_info.priv = buffer;
	dmabuf = dma_buf_export(&exp_info); // 做个转化

	return dmabuf;
}

这样就完成了从用户态到内核态申请一个 dma_buf 的过程,不过返回到用户态的是该 dma_buf 对应的 fd,所以在返回前还需要通过 dma_buf_fd 申请一个 fd,之后使用只需要这个 fd

设备使用 dma_buf

接下来我们看看怎样使用。Application 通过上述的操作是申请到一块用 dma_buf 表示的物理内存,并得到 fd,但是要使用这块内存还需要对应的驱动做一些操作,Application 通过系统调用将 fd 及需要做的操作传到驱动,然后驱动调用对应的回调函数,通过一个简单的例子来看一下,

static int heap_test(struct heap_dma_buf *dma_buf, int len) // 这个 dma_buf 需要包含上述申请到的 fd
{
	...

	misc_fd = open(XXX, O_RDWR | O_CLOEXEC); // 获取对应的设备节点 fd

	ret = heap_test_map(misc_fd, dma_buf); // 该设备节点使用 dma_buf

	...

	heap_dma_buf_free(dma_buf);
	close(misc_fd);

	return 0;
}

static inline int heap_test_ioctl_helper(int misc_fd,
			struct dma_buf_priv *dma_buf)
{
	struct dma_heap_allocation_data data;
	int ret;

	memset(&data, 0, sizeof(struct dma_heap_allocation_data));
	data.fd = dma_buf->fd; // 操作申请到的 fd
	data.len = dma_buf->len;
	ret = ioctl(misc_fd, HEAP_TEST_IOCTL_MAP, &data); // 向对应的驱动发起系统调用

	return ret;
}

驱动中需要响应用户态发起的系统调用,可以参考下面的代码,以 map 为例,

static long heap_test_ioctl(struct file *file, unsigned int ioctl,
			unsigned long arg)
{
	struct dma_heap_allocation_data heap_data;
	long ret = 0;

	if (copy_from_user(&heap_data, (void __user *)arg,
			sizeof(struct dma_heap_allocation_data))) {
		pr_err("heap_test: copy_from_user fail\n");
		return -1;
	}

	switch (ioctl) {
	case HEAP_TEST_IOCTL_KERN_MAP:
		ret = heap_test_map(file, &heap_data);
		break;
	...

	default:
		break;
	}

	return ret;
}

驱动开发者需要按照 google 的规则来响应请求,下面是响应 map 请求的示例,

static struct heap_test_dma_buf *heap_test_map(struct device *dev, int fd)
{
	struct heap_test_dma_buf *priv = NULL;
	struct scatterlist *sgl = NULL;
	int i = 0;
	int ret = 0;

	priv = kzalloc(sizeof(struct heap_test_dma_buf), GFP_KERNEL);

	priv->dma_buf = dma_buf_get(fd); // 这个 fd 就是从用户态传下来的,fd 可以唯一指定一个 dma_buf

    // 1. 这个 dma_buf 其实任何设备都可以使用,使用前需要 attach 一下
    // 而 attach 就是将该设备添加到 dma_buf 的 attachments list 里面
    // 这些都是内核提供的接口,按照流程使用即可
	priv->attachment = dma_buf_attach(priv->dma_buf, dev);

    // 2. 这个操作还没搞懂
    // sg_table 是用于管理散布/聚集(scatter/gather)I/O 的数据结构
    // 它允许多个物理内存块(散布)或者多个数据片段(散布)组合成一个连续的内存区域
    // 以便 I/O 进行高效的数据传输
    // 这个对应的流程还是很复杂的,暂时不看
	priv->sgt = dma_buf_map_attachment(priv->attachment, DMA_BIDIRECTIONAL);

	heap_test_dump_sg_info(priv->sgt);

    // 映射到内核地址空间
    // 以 cma 为例,调用的是 cma_heap_vmap
    // 最终也是调用 vmap 进行映射
	/* map to kernel and access the mem */
	ret = dma_buf_vmap(priv->dma_buf, &priv->map);

	priv->fd = fd;
	priv->buf = (char *)priv->map.vaddr;
	priv->len = 0;
	for_each_sg(priv->sgt->sgl, sgl, priv->sgt->nents, i) {
		priv->len += sg_dma_len(sgl);
	}
	memset(priv->buf, 0, priv->len);

	return priv;
}

之后就可以正常的往 dma_buf 中读写,

static int xxx_write(struct dma_buf_priv *dma_buf)
{
	int i;

	printf("%s\n", __func__);
	for (i = 0; i < dma_buf->len; i++) {
		dma_buf->buf[i] = i % 256;
		printf("buf[%d]=0x%x\n", i, dma_buf->buf[i]);
	}
	dma_buf_cpu_sync_end(dma_buf, DMA_BUF_SYNC_WRITE); // 刷 cache

	return 0;
}

总结

整个来看,使用 dma_buf 首先在需要内核中分配物理内存,每个 dma_buf 指定一个 fd,传到用户态,然后在驱动中将 struct device *dev attach 到 dma_buf 的 attachment list。

所以按照上面的方式,用户态程序或设备驱动想要使用 dma_buf 只需要知道 fd 就行,共享数据十分简单,这里要确保 fd 的正确传递。

cma_heap

初始化

cma_heap 是 dma-heap 的类型之一,它使用的是 CMA 内存,在系统内存不足时,会被回收。一般系统都会有一块 default cma,在 dts 中一般按如下方式定义:

linux,cma {
            compatible = "shared-dma-pool";
            reg = <0x? 0x? 0X0 0x?>;
            reusable;
            linux,cma-default;
        };

rmem_cma_setup

这种类型的预留内存在系统初始化时完成解析 RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup); ,这个函数有些地方需要注意,

static const struct reserved_mem_ops rmem_cma_ops = {
	.device_init	= rmem_cma_device_init,
	.device_release = rmem_cma_device_release,
};

static int __init rmem_cma_setup(struct reserved_mem *rmem)
{
	phys_addr_t align = PAGE_SIZE << max(MAX_ORDER - 1, pageblock_order);
	phys_addr_t mask = align - 1;
	unsigned long node = rmem->fdt_node;
	bool default_cma = of_get_flat_dt_prop(node, "linux,cma-default", NULL);
	struct cma *cma;
	int err;

	...

    // 所有的 cma 都会保存在 cma_area 数组中
	err = cma_init_reserved_mem(rmem->base, rmem->size, 0, rmem->name, &cma);

    ...


	if (default_cma)
		dma_contiguous_default_area = cma;

    // 这个地方需要注意,设置的是这个 reserved_mem 的 ops
    // device_init = rmem_cma_device_init 只有在调用 of_reserved_mem_device_init 时才会被调用
    // 如果该设备节点使用 memory-region 属性引用了某个预留内存节点
    // 那么就会调到这个 ops
    // device_init 就是配置 dev->cma_area 为该 reserved_mem 的
	rmem->ops = &rmem_cma_ops;
	rmem->priv = cma;

	pr_info("Reserved memory: created CMA memory pool at %pa, size %ld MiB\n",
		&rmem->base, (unsigned long)rmem->size / SZ_1M);

	return 0;
}

add_default_cma_heap

default cma 初始化由 cma_heap.ko 完成,主要是设置 cma_heap_ops 以及将 default cma 挂载到 /dev/dma-heap 上。

static const struct dma_heap_ops cma_heap_ops = {
	.allocate = cma_heap_allocate,
};

int __add_cma_heap(struct cma *cma, void *data)
{
	struct cma_heap *cma_heap;
	struct dma_heap_export_info exp_info;

	cma_heap = kzalloc(sizeof(*cma_heap), GFP_KERNEL);
	if (!cma_heap)
		return -ENOMEM;
	cma_heap->cma = cma;

	exp_info.name = cma_get_name(cma);
    // 用户态申请 cma 内存都会走到这个函数,在这个函数里面又会设置 cma_heap_buf_ops
    // 这个函数就是 cma heap 和 heap core 建立关联的地方
    // 在用户态通过 ioctl 向 dma heap core 发起请求,会根据不同的 heap 走到对应的回调函数
	exp_info.ops = &cma_heap_ops;
	exp_info.priv = cma_heap;

	cma_heap->heap = dma_heap_add(&exp_info); // 这里调用 device_create 添加 char 设备

    ...

	return 0;
}
EXPORT_SYMBOL(__add_cma_heap);

static int add_default_cma_heap(void)
{
    // 这里获取 default cma
    // 其他的 cma 默认是不会加入到 /dev/dma-heap 中的
    // 但是可以通过 of_reserved_mem_lookup 遍历所有的 reserved mem
    // 然后添加
	struct cma *default_cma = dev_get_cma_area(NULL);
	int ret = 0;

	if (default_cma)
		ret = __add_cma_heap(default_cma, NULL);

	return ret;
}
module_init(add_default_cma_heap);

dma_heap_add

这个函数主要是添加 cma_heap 设备到 /dev/dma_heap 目录下。其实所有的 dma heap 设备的创建都要走到这里,

struct dma_heap *dma_heap_add(const struct dma_heap_export_info *exp_info)
{
	struct dma_heap *heap, *err_ret;
	unsigned int minor;
	int ret;

	...

	heap = kzalloc(sizeof(*heap), GFP_KERNEL);
	if (!heap)
		return ERR_PTR(-ENOMEM);

	kref_init(&heap->refcount);
	heap->name = exp_info->name;
	heap->ops = exp_info->ops;
	heap->priv = exp_info->priv;

	/* Find unused minor number */
	ret = xa_alloc(&dma_heap_minors, &minor, heap,
		       XA_LIMIT(0, NUM_HEAP_MINORS - 1), GFP_KERNEL);
	if (ret < 0) {
		pr_err("dma_heap: Unable to get minor number for heap\n");
		err_ret = ERR_PTR(ret);
		goto err0;
	}

	/* Create device */
	heap->heap_devt = MKDEV(MAJOR(dma_heap_devt), minor);

    // 这个 ops 是该 heap 对应的回调函数,之后操作该 heap 都是这个回调函数完成的
	cdev_init(&heap->heap_cdev, &dma_heap_fops);
	ret = cdev_add(&heap->heap_cdev, heap->heap_devt, 1);

    ...

	heap->heap_dev = device_create(dma_heap_class,
				       NULL,
				       heap->heap_devt,
				       NULL,
				       heap->name);

	/* Make sure it doesn't disappear on us */
	heap->heap_dev = get_device(heap->heap_dev);

	/* Add heap to the list */
	mutex_lock(&heap_list_lock);
	list_add(&heap->list, &heap_list);
	mutex_unlock(&heap_list_lock);

	return heap;
}
EXPORT_SYMBOL_GPL(dma_heap_add);

上述过程是在内核中完成 cma_heap 的初始化。

问题

Reference