Linux reserved-memory 框架

前言

随着内核的运行,内核中的物理内存越来越趋向于碎片化,但某些特定设备在使用时用到的 DMA 需要大量的连续物理内存,这可能导致设备在真正使用时因为申请不到满足要求的物理内存而无法使用,这显然是不能接受的。

最简单的方式就是为特定设备保留一部分物理内存专用,这部分内存不受系统的管理,绑定到特定的设备,在设备需要使用时再对这部分内存进行管理,这就是内核中提供的 reserved memory 机制。

在实际应用中,为特定设备保留内存虽然实现和操作都相对简单,但是有个明显的缺点:正因为这部分内存不能被系统管理,也就造成了一定的浪费,在设备不使用时,这部分内存就无法利用,而最理想的情况应该是:当被保留的内存在不使用时同样可以被系统利用,而设备需要使用时就返回给其绑定的设备,这就是内核中的 CMA 机制。

有时设备驱动程序需要采用 DMA 的方式使用预留的内存,对于这种场景,可以将 dts 中的节点属性设置为 shared-dma-pool,从而生成为特定设备驱动预留的 DMA 内存池,此时驱动可通过 DMA API 来使用预留内存。


memory

对应设备树节点:

/ {
	#address-cells = <1>;
	#size-cells = <1>;

	memory {
		reg = <0x80000000 0x20000000>;
	};
}

address-cellssize-cells 在设备树中属于比较重要的概念,用于指定其同一级节点中描述一个单元的信息所需要的位数,cell 值为 1 表示使用 32bit 描述,比如在 memory 节点中,reg 中一共有 64bit,表示描述两个单元,第一个 32bit 表示物理内存起始地址,而第二个 32bit 表示物理内存的 size,reg 属性的解析方式是由内核规定的。内核在启动时通过扫描 memory 节点可以知道当前系统中所有物理内存的相关信息。

note:实际上这里并不是最终的物理内存信息,最终实际的物理内存信息是由 bootloader 传递给内核的。


reserved memory

对应设备树节点:

reserved-memory {
	#address-cells = <1>;
	#size-cells = <1>;
	ranges;

	display_reserved: framebuffer@8e000000 {
		reg = <0x8e000000 0x02000000>;
	};
};

fb0: video@12300000 {
	memory-region = <&display_reserved>;
};

reg:保留内存的地址以及 size(静态保留内存)。相对应的,动态保留内存只需要指定一个保留内存的 size,而具体保留哪片内存由内核自行决定。例如:

reserved-memory {
	#address-cells = <1>;
	#size-cells = <1>;
	ranges;

	display_reserved: framebuffer@8e000000 {
		size = <0x02000000>;
		alignment = <0x2000>;
	};
};

alignment:可选的。保留内存的起始地址需要向该参数对齐。

memory-region:对保留内存节点进行引用,表明当前设备需要使用到对应的保留内存。
这种设备与保留内存之间的节点引用,可以让设备更方便地使用相应的设备树接口获取该片物理内存的相关信息,实际上即使没有建立这种设备节点之间的关联,只要特定设备在启用时能够知道保留内存的相关信息,也是可以直接使用,毕竟这片物理内不会被系统征用,至于该设备在启动时如何使用这片物理内存,取决于具体地业务场景。

如下图,通过此方式保留的内存会被计算到 reserved 中,而不会被统计到 MemTotal 中:





note:若设备树中同时还指定了 no-map,则保留的内存不会被统计到 reserved 和 Memory available 中,对系统不可见。此时需要自己建立地址映射,如下:

/* Get reserved memory region from Device-tree */
np = of_parse_phandle(dev->of_node, "memory-region", 0);
if (!np) {
	dev_err(dev, "No %s specified\n", "memory-region");
	goto error1;
}

rc = of_address_to_resource(np, 0, &r);
if (rc) {
	dev_err(dev, "No memory address assigned to the region\n");
	goto error1;
}
  
lp->paddr = r.start;
lp->vaddr = memremap(r.start, resource_size(&r), MEMREMAP_WB);
dev_info(dev, "Allocated reserved memory, vaddr: 0x%0llX, paddr: 0x%0llX\n", (u64)lp->vaddr, lp->paddr);

CMA memory pool

相关代码:

登记 cma 区域,设置一些参数:kernel/dma/contiguous.c
cma 真正的初始化:mm/cma.c

内核的相关 API:

cma_alloc()     // 申请 CMA 内存
cma_release()   // 释放 CMA 内存

通常 cma_alloc 和 cma_release 接口并不会被直接调用,而是隐藏在 dma 的接口背后,在使用 dma 申请对应的物理内存时,使用 dma_alloc_from_contiguous 接口,该接口直接调用 cma_alloc 函数执行物理内存的分配。

struct page *dma_alloc_from_contiguous(struct device *dev, int count,
				       unsigned int align)
{
	...
	return cma_alloc(dev_get_cma_area(dev), count, align);
}

对应打印:

Reserved memory: created CMA memory pool at 0x0000000940000000, size 512 MiB
OF: reserved mem: initialized node linux-cma-buffers@931000000, compatible id shared-dma-pool

对应设备树节点(另外,CMA 也可以通过 cmdline 和 .config 文件进行配置,配置的优先级 dts > cmdline > .config):

reserved_memory: reserved-memory {
	#address-cells = <2>;
	#size-cells = <2>;
	ranges;

	linux_cma_region: linux-cma-buffers@931000000 {
		compatible = "shared-dma-pool";
		reusable;
		reg = <0x09 0x40000000 0x00 0x20000000>;
		linux,cma-default;
	};
}

compatible:通常,保留内存并不需要 compatible 属性,因为保留内存并不需要相应的驱动程序来处理它,而 CMA 内存在作为保留内存的同时还需要可以被 buddy 系统管理,需要做一些特殊的设置,因此需要相应的驱动程序,使用 compatible 属性来关联相应的驱动程序,并在内核的启动阶段调用,完成 CMA 保留内存的初始化。CMA 内存对应的 compatible 属性为:shared-dma-pool,这是内核规定的一个固定值。当然,如果保留内存需要驱动程序进行一些特殊处理,也可提供自定义的 compatible 属性以在内核启动阶段执行相应的驱动程序。

reusable:CMA 内存池必须指定。当驱动程序不使用这些内存时,OS 可以使用这些内存(可被 buddy 系统使用);而当驱动程序从这个 CMA area 分配内存时,OS 可以释放这些内存,让驱动可以使用它。对于一个保留内存池,不能同时指定 no-map 和 reusable 属性。

linux,cma-default:如果存在 linux,cma-default 属性,那么将使用该区域作为 CMA 的默认池。

如下图,通过 CMA 保留的内存会被计算到 cma-reserved 中,同时也会统计到 MemTotal 中,计算 MemFree 时也会将其计算进去:





DMA memory pool

对应 kernel/dma/coherent.c,对应打印:

Reserved memory: created DMA memory pool at 0x86000000, size 32 MiB
Reserved memory: initialized node dma-memory@86000000, compatible id shared-dma-pool

对应设备树节点:

reserved-memory {
	#address-cells = <1>;
	#size-cells = <1>;
	ranges;

	dma_memory_region: dma-memory@86000000 {
		compatible = "shared-dma-pool";
		reg = <0x86000000 0x02000000>;
		no-map;
	};
}

no-map:不要将这个内存区域映射到虚拟地址空间中。如果没有 no-map 属性,那么 OS 就会为这段 memory 创建地址映射。后续可以使用 ioremap 建立虚拟地址到物理地址的映射,像访问外设内存一样访问它(因为这片内存没有被系统接管,因此需要使用者自己来实现页面的管理)。

设备驱动程序中可以使用 DMA API 申请内存,它申请的内存不是来源于默认的 CMA 内存池,而是来源于该预留内存:

/* Initialize reserved memory resources */
rc = of_reserved_mem_device_init(dev);
if (rc) {
	dev_err(dev, "Could not get reserved memory\n");
	goto error1;
}

/* Allocate memory */
dma_set_coherent_mask(dev, 0xFFFFFFFF);
lp->vaddr = dma_alloc_coherent(dev, ALLOC_SIZE, &lp->paddr, GFP_KERNEL);
dev_info(dev, "Allocated coherent memory, vaddr: 0x%0llX, paddr: 0x%0llX\n", (u64)lp->vaddr, lp->paddr);

如下图,通过 no-map(和 "shared-dma-pool" 没关系)保留的内存不会被统计到 Memory available 和 MemTotal 中:





Reference

Documentation/devicetree/bindings/reserved-memory/reserved-memory.txt
https://zhuanlan.zhihu.com/p/363911384
https://blog.csdn.net/Rong_Toa/article/details/109558234
https://blog.csdn.net/shenfengchen/article/details/117428418
https://blog.csdn.net/qq_38131812/article/details/127328547

posted @ 2023-08-03 11:03  Ma-ZhiQiang  阅读(906)  评论(0编辑  收藏  举报