前言

在这篇文章中,我想要梳理一下几种 reserved memory 类型以及它们在初始化/使用时的关键调用路径。
关于这部分的内容,网上的资料解释有点太过于一笔带过了,于是看了看代码,写下来以备忘。
平台是高通 sm8250,msm-4.19 内核。

物理内存区域的维护

在开始之前得先来讨论一下物理内存区域的问题。
这里的物理内存区域,指的是物理地址空间中属于内存的区域。
物理地址空间很大,但其中往往只有一小部分属于物理内存。除开物理内存以外的物理地址空间中,还分布着许多 MMIO 寄存器,再者就是大量的未使用地址了。

我们可以使用 cat /proc/iomem 来查看物理地址空间的布局:

00100000-002effff : cc_base
00408000-00408fff : qcom,ipcc@408000
00784000-00786fff : qfprom@780000
00884000-00887fff : i2c@884000
00888000-0088bfff : i2c@888000
008c0000-008c1fff : qcom,qupv3_2_geni_se@8c0000
00984000-00987fff : i2c@984000
009c0000-009c1fff : qcom,qupv3_0_geni_se@9c0000
00a8c000-00a8ffff : i2c@a8c000
00a94000-00a97fff : i2c@a94000
00ac0000-00ac1fff : qcom,qupv3_1_geni_se@ac0000
0188101c-0188101f : sp2soc_irq_status
01881024-01881027 : sp2soc_irq_clr
01881028-0188102b : sp2soc_irq_mask
0188103c-0188103f : rmb_err
01882014-01882017 : rmb_err_spare2
01d84000-01d86fff : ufs_mem
01d87000-01d87dff : phy_mem
01d90000-01d97fff : ufs_ice
03d00000-03d3ffff : kgsl-3d0
03d90000-03d98fff : cc_base
03da0000-03daffff : base
03dc2200-03dc2207 : status-reg
03dc2208-03dc220f : status-reg
03dc5000-03dc5fff : base
03dc9000-03dc9fff : base
06002000-06002fff : stm-base
  06002000-06002fff : stm-base
06004000-06004fff : tpda-base
  06004000-06004fff : tpda-base
06005000-06005fff : funnel-base
  06005000-06005fff : funnel-base
06010000-06010fff : cti-base
  06010000-06010fff : cti-base
06011000-06011fff : cti-base
  06011000-06011fff : cti-base
06012000-06012fff : cti-base
  06012000-06012fff : cti-base
06013000-06013fff : cti-base
  06013000-06013fff : cti-base
06014000-06014fff : cti-base
  06014000-06014fff : cti-base
06015000-06015fff : cti-base
  06015000-06015fff : cti-base
06016000-06016fff : cti-base
  06016000-06016fff : cti-base
06017000-06017fff : cti-base
  06017000-06017fff : cti-base
06018000-06018fff : cti-base
  06018000-06018fff : cti-base
06019000-06019fff : cti-base
  06019000-06019fff : cti-base
0601a000-0601afff : cti-base
  0601a000-0601afff : cti-base
0601b000-0601bfff : cti-base
  0601b000-0601bfff : cti-base
0601c000-0601cfff : cti-base
  0601c000-0601cfff : cti-base
0601d000-0601dfff : cti-base
  0601d000-0601dfff : cti-base
0601e000-0601efff : cti-base
  0601e000-0601efff : cti-base
0601f000-0601ffff : cti-base
  0601f000-0601ffff : cti-base
06041000-06041fff : funnel-base
  06041000-06041fff : funnel-base
06042000-06042fff : funnel-base
  06042000-06042fff : funnel-base
06045000-06045fff : funnel-base
  06045000-06045fff : funnel-base
06046000-06046fff : replicator-base
  06046000-06046fff : replicator-base
06048000-06048fff : tmc-base
  06048000-06048fff : tmc-base
06830000-06830fff : tpdm-base
  06830000-06830fff : tpdm-base
06831000-06831fff : cti-base
  06831000-06831fff : cti-base
06832000-06832fff : funnel-base
  06832000-06832fff : funnel-base
06844000-06844fff : tpdm-base
  06844000-06844fff : tpdm-base
06845000-06845fff : cti-base
  06845000-06845fff : cti-base
06846000-06846fff : funnel-base
  06846000-06846fff : funnel-base
0684c000-0684cfff : tpdm-base
  0684c000-0684cfff : tpdm-base
06850000-06850fff : tpdm-base
  06850000-06850fff : tpdm-base
06870000-06870fff : tpdm-base
  06870000-06870fff : tpdm-base
06900000-06900fff : tpdm-base
  06900000-06900fff : tpdm-base
06902000-06902fff : funnel-base
  06902000-06902fff : funnel-base
06980000-06980fff : tpdm-base
  06980000-06980fff : tpdm-base
06982000-06982fff : cti-base
  06982000-06982fff : cti-base
06983000-06983fff : funnel-base
  06983000-06983fff : funnel-base
0698b000-0698bfff : cti-base
  0698b000-0698bfff : cti-base
069c0000-069c0fff : tpdm-base
  069c0000-069c0fff : tpdm-base
069c1000-069c1fff : tpda-base
  069c1000-069c1fff : tpda-base
069c2000-069c2fff : funnel-base
  069c2000-069c2fff : funnel-base
069d0000-069d0fff : tpdm-base
  069d0000-069d0fff : tpdm-base
06ac0000-06ac0fff : tpdm-base
  06ac0000-06ac0fff : tpdm-base
06ac1000-06ac1fff : tpda-base
  06ac1000-06ac1fff : tpda-base
06ac2000-06ac2fff : funnel-base
  06ac2000-06ac2fff : funnel-base
06b00000-06b00fff : cti-base
  06b00000-06b00fff : cti-base
06b01000-06b01fff : cti-base
  06b01000-06b01fff : cti-base
06b02000-06b02fff : cti-base
  06b02000-06b02fff : cti-base
06b03000-06b03fff : cti-base
  06b03000-06b03fff : cti-base
06b04000-06b04fff : funnel-base
  06b04000-06b04fff : funnel-base
06b05000-06b05fff : tmc-base
  06b05000-06b05fff : tmc-base
06b06000-06b06fff : replicator-base
  06b06000-06b06fff : replicator-base
06b08000-06b08fff : tpda-base
  06b08000-06b08fff : tpda-base
06b09000-06b09fff : tpdm-base
  06b09000-06b09fff : tpdm-base
06b0a000-06b0afff : tpdm-base
  06b0a000-06b0afff : tpdm-base
06b0b000-06b0bfff : tgu-base
  06b0b000-06b0bfff : tgu-base
06c08000-06c08fff : tpdm-base
  06c08000-06c08fff : tpdm-base
06c09000-06c09fff : cti-base
  06c09000-06c09fff : cti-base
06c0a000-06c0afff : cti-base
  06c0a000-06c0afff : cti-base
06c0b000-06c0bfff : funnel-base
  06c0b000-06c0bfff : funnel-base
06c28000-06c28fff : tpdm-base
  06c28000-06c28fff : tpdm-base
06c29000-06c29fff : tpdm-base
  06c29000-06c29fff : tpdm-base
06c2a000-06c2afff : cti-base
  06c2a000-06c2afff : cti-base
06c2b000-06c2bfff : cti-base
  06c2b000-06c2bfff : cti-base
06c2c000-06c2cfff : cti-base
  06c2c000-06c2cfff : cti-base
06c2d000-06c2dfff : funnel-base
  06c2d000-06c2dfff : funnel-base
06c38000-06c38fff : cti-base
  06c38000-06c38fff : cti-base
06c39000-06c39fff : funnel-base
  06c39000-06c39fff : funnel-base
06c40000-06c40fff : tpdm-base
  06c40000-06c40fff : tpdm-base
06c41000-06c41fff : tpdm-base
  06c41000-06c41fff : tpdm-base
06c42000-06c42fff : cti-base
  06c42000-06c42fff : cti-base
06c43000-06c43fff : cti-base
  06c43000-06c43fff : cti-base
06c44000-06c44fff : funnel-base
  06c44000-06c44fff : funnel-base
06c47000-06c47fff : tpdm-base
  06c47000-06c47fff : tpdm-base
06c4b000-06c4bfff : cti-base
  06c4b000-06c4bfff : cti-base
06c60000-06c60fff : tpdm-base
  06c60000-06c60fff : tpdm-base
06c61000-06c61fff : cti-base
  06c61000-06c61fff : cti-base
06e01000-06e01fff : cti-base
  06e01000-06e01fff : cti-base
06e02000-06e02fff : cti-base
  06e02000-06e02fff : cti-base
06e03000-06e03fff : cti-base
  06e03000-06e03fff : cti-base
06e04000-06e04fff : funnel-base
  06e04000-06e04fff : funnel-base
06e0c000-06e0cfff : cti-base
  06e0c000-06e0cfff : cti-base
06e0d000-06e0dfff : cti-base
  06e0d000-06e0dfff : cti-base
06e0e000-06e0efff : cti-base
  06e0e000-06e0efff : cti-base
06e10000-06e10fff : tpdm-base
  06e10000-06e10fff : tpdm-base
06e11000-06e11fff : cti-base
  06e11000-06e11fff : cti-base
06e12000-06e12fff : funnel-base
  06e12000-06e12fff : funnel-base
06e20000-06e20fff : tpdm-base
  06e20000-06e20fff : tpdm-base
06e21000-06e21fff : cti-base
  06e21000-06e21fff : cti-base
06e22000-06e22fff : funnel-base
  06e22000-06e22fff : funnel-base
07020000-07020fff : cti-base
  07020000-07020fff : cti-base
07040000-07040fff : etm@7040000
07120000-07120fff : cti-base
  07120000-07120fff : cti-base
07140000-07140fff : etm@7140000
07220000-07220fff : cti-base
  07220000-07220fff : cti-base
07240000-07240fff : etm@7240000
07320000-07320fff : cti-base
  07320000-07320fff : cti-base
07340000-07340fff : etm@7340000
07420000-07420fff : cti-base
  07420000-07420fff : cti-base
07440000-07440fff : etm@7440000
07520000-07520fff : cti-base
  07520000-07520fff : cti-base
07540000-07540fff : etm@7540000
07620000-07620fff : cti-base
  07620000-07620fff : cti-base
07640000-07640fff : etm@7640000
07720000-07720fff : cti-base
  07720000-07720fff : cti-base
07740000-07740fff : etm@7740000
07800000-07800fff : funnel-base
  07800000-07800fff : funnel-base
07810000-07810fff : funnel-base
  07810000-07810fff : funnel-base
07860000-07860fff : tpdm-base
  07860000-07860fff : tpdm-base
07861000-07861fff : tpdm-base
  07861000-07861fff : tpdm-base
07863000-07863fff : tpda-base
  07863000-07863fff : tpda-base
078a0000-078a0fff : tpdm-base
  078a0000-078a0fff : tpdm-base
078b0000-078b0fff : tpdm-base
  078b0000-078b0fff : tpdm-base
078e0000-078e0fff : cti-base
  078e0000-078e0fff : cti-base
078f0000-078f0fff : cti-base
  078f0000-078f0fff : cti-base
07900000-07900fff : cti-base
  07900000-07900fff : cti-base
088e0000-088e1fff : eud_base
088e2000-088e2003 : eud_enable_reg
088e3000-088e310f : hsusb_phy_base
088e4000-088e410f : hsusb_phy_base
088e7000-088e7083 : refgen-regulator@88e7000
088eb88c-088eb88f : pcs_clamp_enable_reg
09095000-090952ff : lagg-base
09200000-093cffff : llcc_base
09600000-0964ffff : llcc_broadcast_base
09800000-0980ffff : qdsp6ss
09810000-0981ffff : qdsp6ss_pll
09980000-0998ffff : cc
0a60c100-0a60d93b : dwc3@a600000
0abf0000-0abfffff : cc_base
0ad00000-0ad0ffff : cc_base
0af00000-0af1ffff : cc_base
0af20000-0af2ffff : drv-0
0b2e5510-0b2e5acf : tcs_cmd
0c222000-0c222003 : tsens_srot_physical
0c223000-0c223003 : tsens_srot_physical
0c263000-0c2631fe : tsens_tm_physical
0c264000-0c264003 : pshold-base
0c265000-0c2651fe : tsens_tm_physical
0c40a000-0c42ffff : cnfg
0c440000-0c4410ff : core
0c600000-0e5fffff : chnls
0e600000-0e6fffff : obsrvr
0e700000-0e79ffff : intr
0f000000-0fffffff : pinctrl@f000000
15000000-150fffff : base
15182200-15182207 : status-reg
15182208-1518220f : status-reg
15182210-15182217 : status-reg
15182218-1518221f : status-reg
15182220-15182227 : status-reg
15182228-1518222f : status-reg
15182230-15182237 : status-reg
15182238-1518223f : status-reg
15182240-15182247 : status-reg
15182248-1518224f : status-reg
15185000-15185fff : base
15189000-15189fff : base
1518d000-1518dfff : base
15191000-15191fff : base
15195000-15195fff : base
15199000-15199fff : base
1519d000-1519dfff : base
151a1000-151a1fff : base
151a5000-151a5fff : base
151a9000-151a9fff : base
16280000-163fffff : stm-stimulus-base
17c10000-17c10fff : msm-watchdog
17c21000-17c21fff : arch_mem_timer
18220000-1822ffff : drv-2
18591000-18591fff : freq-domain0
18592000-18592fff : freq-domain1
18593000-18593fff : freq-domain2
60300000-63ffffff : qcom,pcie@1c00000
  60300000-605fffff : PCI Bus 0000:01
    60300000-603fffff : 0000:01:00.0
      60300000-603fffff : cnss
  60600000-607fffff : PCI Bus 0000:01
  60800000-60800fff : 0000:00:00.0
64300000-67ffffff : qcom,pcie@1c10000
  64300000-645fffff : PCI Bus 0002:01
    64300000-64300fff : 0002:01:00.0
      64300000-64300fff : mhi
    64301000-64301fff : 0002:01:00.0
  64600000-647fffff : PCI Bus 0002:01
  64800000-64800fff : 0002:00:00.0
80600000-806fffff : System RAM
80880000-808fffff : System RAM
8d800000-8dbfffff : System RAM
8e21c000-8e2fffff : System RAM
98700000-9fdfffff : System RAM
  9c000000-9e3fffff : reserved
a0000000-bbafffff : System RAM
  a0080000-a23fffff : Kernel code
  a2400000-a27fffff : reserved
  a2800000-a34d5fff : Kernel data
  afc13000-afcd0fff : reserved
  afe14000-afffefff : reserved
  b0000000-b03fffff : reserved
c0000000-ffbfffff : System RAM
  e9000000-ffbfffff : reserved
ffc20000-27fffffff : System RAM
  276c00000-2779fffff : reserved
  277acc000-27f9fffff : reserved
  27fa19000-27fa19fff : reserved
  27fa1a000-27fbc9fff : reserved
  27fbcc000-27fbccfff : reserved
  27fbcd000-27fbcdfff : reserved
  27fbce000-27fbd0fff : reserved
  27fbd1000-27fffefff : reserved
  27ffff000-27fffffff : reserved

其中只有 System RAM 的区间(以及其下面包含的 reserved )属于物理内存,上面那些乱七八糟的全是 MMIO 寄存器。
对于这些 MMIO 寄存器,它们所处的物理地址是体系结构相关的,不同芯片完全可以有不同的设计,需要具体的翻阅芯片手册才能确定。
之所以这里能够将这些寄存器的地址打印出来,是因为这些寄存器的地址是和与之相关联的设备一起,被写死在 devicetree 当中的(platform devices 的 reg 属性),当设备驱动使用这些地址进行 request_mem_region 时,这些地址就被内核记录并标记,于是就有了上面的这张表。

你大可将 MMIO 寄存器类比为 8051 中的 SFR ,不过 MMIO 的地址一般是独立于物理内存所在的区间的,即不会叠加在物理内存区间上,不像 8052 那样存在阴间的 SFR 地址和内存地址的重复(以至于需要使用的不同的寻址方式)。

物理内存地址区间的确定

这是一个很有意思的东西,因为在 devicetree 中存在这么一行:

kona.dtsi

......
    memory { device_type = "memory"; reg = <0 0 0 0>; };
......

诶,这相当于没有规定内存区域吧,再结合一下现在的手机往往都有搭载不同内存大小的版本,直觉告诉我应该存在某种自动检测物理内存区域和大小的机制,可是它在哪里呢?在内核里看了半天都没有看到相关的东西。

直到:

$ adb shell su -c "cat /proc/device-tree/memory/reg" | hexdump -C
00000000  00 00 00 00 80 00 00 00  00 00 00 00 3b b0 00 00  |............;...|
00000010  00 00 00 01 80 00 00 00  00 00 00 01 00 00 00 00  |................|
00000020  00 00 00 00 c0 00 00 00  00 00 00 00 c0 00 00 00  |................|
00000030

被耍了,物理内存的区间其实就写在 devicetree 中,只不过不是由我们写进去,而是由 bootloader 自动扫描然后动态的修改进去,上面的 0 0 0 0 会自动被替换为对应的内存区间。

比如对于这台设备来说,实际的 reg 是 <0 0x80000000 0 0x3bb00000 0x1 0x80000000 0x1 0x00000000 0 0xc0000000 0 0xc0000000>,即物理内存空间被分为三段:

  • 0x80000000 的物理地址开始,存在 1,001,390,080 Bytes 的内存空间。
  • 0x180000000 的物理地址开始,存在 4,294,967,296 Bytes 的内存空间。
  • 0xc0000000 的物理地址开始,存在 3,221,225,472 Bytes 的内存空间。

总空间为 8,517,582,848 Bytes ,即为 7.93 GiB ,对于一台标注为 8 G 内存的设备来说,并没有什么问题。

物理内存的管理

在设备启动初期,linux 内核会使用 memblock 对物理内存进行管理,说是管理,不如说是进行简单的标记。
这里不展开 memblock 的细节,网上有很多其它优秀的文章。
简单来说,memblock 维护了两张表,一张名为 memory ,描述的是物理内存在物理地址空间上的布局(即,物理内存在哪些地址上),另一张表名为 reserved 描述的是哪些空间已经被使用(被分配出去)了。

reserved 是相对于之后的 buddy 分配器而言的,并不是说这些内存被空留着不分配,而是说这些内存已经被使用,因此 buddy 分配器不能再次分配它们,否则会产生冲突。此外,只有 memory 表中包含的内存区域,才会在之后建立起虚拟地址映射,那些不在 memory 表中的物理地址并不会天生就带有虚拟地址,要访问它们必须借助 ioremap() ,就像访问 MMIO 寄存器那样。

一般来说,reserved 所指定的区间得是包含在 memory 当中的,不然我也不知道它会代表什么含义。

之所以这里要简单介绍一下 memblock ,是因为后面在 reserve memory 时,针对不同情况有着它的不同操作方式。

reserved-memory

终于开始进入正题了,在 devicetree 的根节点下,有一个名为 "reserved-memory" 的子节点,该子节点下的所有节点都将被用来描述所谓的“保留内存区域”。
对于这些内存区域,首先可以介绍一些比较 generic 的属性。

compatible 属性指定了这块内存区域会由哪个驱动程序进行处理,在接下来详细展开每种情况时在进行介绍。

reg 属性指定了这块内存区域的物理地址空间范围,其格式是 <起始地址 大小> ,具体的“起始地址”和“大小”所需要的 cell 数量,则由其父节点(即 reserved-memory) 的 #address-cells#size-cells 属性决定。带有 reg 的内存区域是静态分配的,也就是说其所处的物理空间位置是确定的。

除了静态分配,还有一种动态分配的方法,那就是在不指定 reg 的时候指定 size 属性,此时系统将会在空闲的物理地址上为其分配 size 大小的内存,你还可以额外指定 alloc-rangesalignment 属性来为动态分配进行一些位置上的约束。

no-map 属性代表这块区域不应该建立虚拟地址映射,换种说法就是应该被从 memblock 的 memory 表中移除。又或者说,是从物理内存的地址空间上抹去这块区域。
在不包含 no-map 属性时,这块内存区域会被增加到 memblock 的 reserved 表中,即被标记为已使用来避免二次分配。也就是说,no-map 属性实际上决定了保留内存的方式:是认为这块物理内存不存在(从 memory 表中移除),还是认为这块物理内存被使用(增加到 reserved 表)。将内存区域从 memory 表中移除会导致 /proc/iomem 中的 System RAM 被切断,这便是为什么在我的这台设备上,物理内存的地址本身只有三段,但是 /proc/iomem 中却出现了多段;将内存区域增加到 reserved 表会导致 /proc/iomem 中对应的 System RAM 项目下出现 reserved 项。

reusable 属性代表了这块内存区域可以被作为 CMA 区域,即在区域未使用时将其中的内存以 MIGRATE_CMA 的类型塞进 buddy 分配器中,供 movable 的页面使用,而在区域需要被使用时再通过页面迁移将这块区域空出来。需要注意的是,只有 compatibleshared-dma-pool 时,reusable 才会生效,其它所有情况下增加这个属性都是没有作用的,这将在下面进一步讨论。

接下来,我将会针对不同的几种 compatible 类型进行展开,来看看这些内存区域是如何被操作和管理的。

没有 compatible

这种类型的内存,要么是留着不使用,要么需要完全由驱动程序自己主动管理,即系统并不会主动的初始化它们。
一种典型的驱动程序使用它们的方式是是这样的:

/{
    ......
    reserved_memory: reserved-memory {
        ......
        my_mem: my_mem_region@11451419 {
            no-map;
            reg = <0x0 0x11451419 0x0 0x114514>;
        };
        ......
    };
    ......
    my_device {
        compatible = "my,device";
        memory-region = <&my_mem>;
    };
    ......
};

在与 my,device 对应的 platform 驱动中,需要自行 parse memory-region 的 phandle ,然后调用 of 对应的 api 去解析那个内存区域,再用自己的方式访问这个内存区域(比如 ioremap() 或者 memremap())。

CMA 池

compatibleshared-dma-pool ,且 reusable 属性被设置 而 no-map 属性未被设置时,该内存区域会被用于创建一个系统的 CMA 池。如果额外设置了 linux,cma-default 属性,则该区域会成为系统的默认 CMA 池。

需要注意的是,CMA 池对于内存地址的对齐是有要求的:

kernel/dma/contiguous.c

phys_addr_t align = PAGE_SIZE << max(MAX_ORDER - 1, pageblock_order);

因此假如你通过手动指定 reg 来确定 CMA 池的位置,得确保这个位置符合上面的对齐才行。我想,为了省去这一麻烦,是大多数 CMA 池都采用动态创建的原因吧。

CMA 池是可以被绑定到设备的,也就是由某个设备独享某个 CMA 池,只需要在对应平台设备的属性中增加 memory-region 并引用该 CMA 池即可(和上面的 devicetree 类似)。
这样,在该设备的驱动程序请求 CMA 时,系统会默认向设备所绑定的 CMA 池发起请求,假如该设备没有绑定独享的 CMA 池,系统则会向默认 CMA 池发起请求。

关于什么是 CMA ,上面已经做了简单的介绍,如果想进一步了解,欢迎参考网上的其它文章。

接下来看看负责相关过程的代码:

kernel/dma/contiguous.c

static int rmem_cma_device_init(struct reserved_mem *rmem, struct device *dev)
{
    dev_set_cma_area(dev, rmem->priv);
    return 0;
}

static void rmem_cma_device_release(struct reserved_mem *rmem,
                    struct device *dev)
{
    dev_set_cma_area(dev, NULL);
}

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)
{
    ......

    if (!of_get_flat_dt_prop(node, "reusable", NULL) ||
        of_get_flat_dt_prop(node, "no-map", NULL))
        return -EINVAL;

    ......

    err = cma_init_reserved_mem(rmem->base, rmem->size, 0, rmem->name, &cma);
    if (err) {
        pr_err("Reserved memory: unable to setup CMA region\n");
        return err;
    }
    ......

    if (of_get_flat_dt_prop(node, "linux,cma-default", NULL))
        dma_contiguous_set_default(cma);

    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;
}
RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup);

代码上还是非常简单的,RESERVEDMEM_OF_DECLARE 会将内存区域的 compatible 字符串与对应的回调进行绑定,保存在内核镜像的特定 section 中。当内核启动,扫描 dt 时,遇到与指定 compatible 相匹配的内存区域,就会调用指定的回调进行处理。对于这里,就是遇到 compatibleshared-dma-pool 的内存区域,就将其传递给 rmem_cma_setup 函数。

在 rmem_cma_setup 中,首先确保了区域是 reusable 且不是 no-map 的,然后将其注册到 CMA 框架,如果这个区域是默认的 CMA 区域,则设置一个全局变量指向它以方便后续使用。最后,一组回调被设置,即 rmem_cma_ops ,这组回调会在创建平台设备时被检查并调用,简单来讲是这样的一个过程:

  • 创建平台设备时,发现一个设备有名为 memory-region 的属性,于是根据这个属性找到对应的 struct reserved_mem ,并调用该处设置的回调。
  • 在回调中,设备对应的 struct devicecma_area 字段的值被设置,于是设备的驱动程序就可以通过 dev_get_cma_area() 轻松的访问对应的 CMA 区域,而无需做什么解析 dt 之类的脏活了。如果设备没有独享的 CMA 区域的话,dev_get_cma_area() 会直接返回系统默认的 CMA 区域,也省去了自己判断的麻烦。

对 CMA 区域的使用,主要有以下两种方式:

一是直接使用 dma_alloc_from_contiguous() 并传入设备对应的 struct device,内核会自动使用 dev_get_cma_area() 解析到设备独享的 CMA 区域或者是全局默认 CMA 区域,然后调用 cma_alloc() 从区域中分配指定数量的物理页面。这个 api 只会从 CMA 区域中分配内存,假如设备有别的类型(非 CMA)的独享内存,它还是会从全局 CMA 区域中进行分配。

二是使用 DMA 框架提供的 dma_alloc_coherent() / dma_alloc_attrs() 进行分配(毕竟 CMA 区域主要就是拿来做 DMA 的),这种分配方式相对比较“智能”,能够根据区域的类型调用不同的方法进行分配(对于 CMA 区域,其最终还是调用到 dma_alloc_from_contiguous())。

DMA 池

compatibleshared-dma-pool ,且 reusable 属性未被设置时,该区域会被创建为一个 DMA 池。
你没有看错,compatible 属性的值和上面的 CMA 池是一模一样的,唯一的区别是没有 reusable 属性,此时负责该区域的后端驱动就变得完全不同了。
对于 arm64 ,此时的 no-map 属性是可选的(见下方代码),但事实上,没有 no-map 属性的区域是无法成功创建 DMA 池的,因为稍后在映射这个区域时使用的 memremap() 会直接抛出错误。

kernel/dma/coherent.c

static int rmem_dma_device_init(struct reserved_mem *rmem, struct device *dev)
{
    struct dma_coherent_mem *mem = rmem->priv;
    int ret;

    if (!mem) {
        ret = dma_init_coherent_memory(rmem->base, rmem->base,
                           rmem->size,
                           DMA_MEMORY_EXCLUSIVE, &mem);
        if (ret) {
            pr_err("Reserved memory: failed to init DMA memory pool at %pa, size %ld MiB\n",
                &rmem->base, (unsigned long)rmem->size / SZ_1M);
            return ret;
        }
    }
    mem->use_dev_dma_pfn_offset = true;
    rmem->priv = mem;
    dma_assign_coherent_memory(dev, mem);
    return 0;
}

static void rmem_dma_device_release(struct reserved_mem *rmem,
                    struct device *dev)
{
    if (dev)
        dev->dma_mem = NULL;
}

static const struct reserved_mem_ops rmem_dma_ops = {
    .device_init    = rmem_dma_device_init,
    .device_release    = rmem_dma_device_release,
};

static int __init rmem_dma_setup(struct reserved_mem *rmem)
{
    ......

    if (of_get_flat_dt_prop(node, "reusable", NULL))
        return -EINVAL;

#ifdef CONFIG_ARM
    if (!of_get_flat_dt_prop(node, "no-map", NULL)) {
        pr_err("Reserved memory: regions without no-map are not yet supported\n");
        return -EINVAL;
    }

    if (of_get_flat_dt_prop(node, "linux,dma-default", NULL)) {
        WARN(dma_reserved_default_memory,
             "Reserved memory: region for default DMA coherent area is redefined\n");
        dma_reserved_default_memory = rmem;
    }
#endif

    rmem->ops = &rmem_dma_ops;
    ......
}

......

RESERVEDMEM_OF_DECLARE(dma, "shared-dma-pool", rmem_dma_setup);

可以看到,DMA 池在初始化时所采用的框架和上面的 CMA 池是完全相同的,同样是为这个保留内存区域赋予一组回调操作,然后在创建与这块内存相绑定的平台设备时调用回调操作,对内存区域进行初始化。初始化的过程主要包含以下两个操作:

  • 对内存区域进行 memremap() 以创建虚拟地址映射,并为这块内存区域的管理准备相关结构,比如用一个 bitmap 标记这个区域中的哪些页面已经被分配。需要注意的是,对于上面的 CMA 池,内存区域的管理实际上最终是交给了 buddy 分配器进行,而这里的 DMA 池则需要自己对这块内存区域进行管理。
  • 将这块内存区域赋值给 struct device 中的 dma_mem 字段,这样相关驱动不需要干解析 dt 之类的脏活也能方便的知道自己拥有 DMA 池,在操作对应内存区域时也可以直接传入对应的 dev 指针,方便了许多。

kernel/dma/coherent.c

static void *__dma_alloc_from_coherent(struct dma_coherent_mem *mem,
        ssize_t size, dma_addr_t *dma_handle)
{
    int order = get_order(size);
    ......

    pageno = bitmap_find_free_region(mem->bitmap, mem->size, order);
    if (unlikely(pageno < 0))
        goto err;

    /*
     * Memory was found in the coherent area.
     */
    *dma_handle = mem->device_base + (pageno << PAGE_SHIFT);
    ret = mem->virt_base + (pageno << PAGE_SHIFT);
    ......
}

在分配时,DMA 池会自动将分配的页面数量对齐到 2^order 个,这和使用 buddy 分配器时类似,但是这也意味着,在为 DMA 池选择大小时,必须留有足够的余量来使之能够完成这一操作。

举个例子,在 4KB 页面大小的设备上,我想从 DMA 池分配 5,242,880 Bytes 的内存空间,这对应着 1280 个页面。但是由于要求分配的页面数量必须是 2^order 个,而 2^10 = 1024 < 1280 ,不够,因此只能一次分配 2^11 = 2048 个页面,即我必须为这个 DMA 池预留 8,388,608 Bytes 的大小才能够满足 5,242,880 Bytes 的分配。这会造成一定的内部碎片,但是大概是有利于提升性能的吧。

对 DMA 池的使用,一般采用的是上面提到过的 dma_alloc_coherent() / dma_alloc_attrs()

removed 池

这是一个高通内核特有的东西,其实我并不是太理解它存在的意义。
而且,它只在 msm-4.19 及以下版本的内核中存在,在 msm-5.4 以上的内核就已经被移除掉,转而使用其它机制去替代了。

想要使用它,你需要确保对应内存区域的 compatibleremoved-dma-pool 。虽然对应代码中没有规定 no-map 属性是否要被设置,但和上面的 DMA 池类似,如果 no-map 没有被设置,在后续内存映射的过程中会直接失败。

来看看代码:

kernel/dma/removed.c

static int rmem_dma_device_init(struct reserved_mem *rmem, struct device *dev)
{
    struct removed_region *mem = rmem->priv;

    if (!mem && dma_init_removed_memory(rmem->base, rmem->size, &mem)) {
        pr_info("Reserved memory: failed to init DMA memory pool at %pa, size %ld MiB\n",
            &rmem->base, (unsigned long)rmem->size / SZ_1M);
        return -EINVAL;
    }
    set_dma_ops(dev, &removed_dma_ops);
    rmem->priv = mem;
    dma_assign_removed_region(dev, mem);
    return 0;
}

static void rmem_dma_device_release(struct reserved_mem *rmem,
                    struct device *dev)
{
    dev->dma_mem = NULL;
}

static const struct reserved_mem_ops removed_mem_ops = {
    .device_init    = rmem_dma_device_init,
    .device_release = rmem_dma_device_release,
};

static int __init removed_dma_setup(struct reserved_mem *rmem)
{
    rmem->ops = &removed_mem_ops;
    pr_info("Removed memory: created DMA memory pool at %pa, size %ld MiB\n",
        &rmem->base, (unsigned long)rmem->size / SZ_1M);
    return 0;
}
RESERVEDMEM_OF_DECLARE(dma, "removed-dma-pool", removed_dma_setup);

初始化过程的整体逻辑和上面类似,内存区域最终会被赋值到 struct deviceremoved_mem 字段中,以方便设备驱动的使用。
对 removed 区域的使用,一般也会采用 dma_alloc_coherent() / dma_alloc_attrs() 的方式。

接下来讲一讲 removed 池和 DMA 池的不同点。

首先是映射创建的时机:

kernel/dma/removed.c

void *removed_alloc(struct device *dev, size_t size, dma_addr_t *handle,
            gfp_t gfp, unsigned long attrs)
{
    ......

    size = PAGE_ALIGN(size);
    nbits = size >> PAGE_SHIFT;
    ......

    pageno = bitmap_find_next_zero_area(dma_mem->bitmap, dma_mem->nr_pages,
                        0, nbits, align);

    if (pageno < dma_mem->nr_pages) {
        ......

        addr = ioremap_wc(base, size);
        ......
    }
    ......
}

上面的 DMA 池,是在为设备初始化该内存区域时就会使用 memremap() 创建虚拟地址映射,分配内存时则直接返回对映射的偏移。而 removed 池则将虚拟地址映射的创建时机推迟到了要求分配内存的时候,映射的创建方式也变成了 ioremap_wc()

我并不是很清楚这一点是为了什么,而且 DMA 池所使用的 memremap() + MEMREMAP_WC 达成的实际效果和 removed 池所使用的 ioremap_wc() 应该并没有本质上的区别。

还有一个不同点,便是 removed 池在分配时并不会将区域大小对齐到 2^order 个页面,也就是说 removed 池设置的大小是可以和所需的大小完全一致的。

在某些上古文档中,是这样介绍 removed 池的:

- removed-dma-pool: This indicates a region of memory which is meant to
                    be carved out and not exposed to kernel.

我并不是很认同这种解释。达成这种效果的应该是 no-map 属性,而不是 removed 池,而且 removed 池所在的内存区域本身就是可以通过创建映射来进行访问的。

CAF 在增加 removed 池的 commit 中是这么写的:

drivers: Add dma removed ops
The current DMA coherent pool assumes that there is a kernel
mapping at all times for the entire pool. This may not be
what we want for the entire times. Add the dma_removed ops to
support this use case.

Conflicts:
    drivers/base/Makefile

Change-Id: Ie4f1e9bdf57b79699fa8fa7e7a6087e6d88ebbfa
Signed-off-by: Laura Abbott <[email protected]>
Signed-off-by: Patrick Daly <[email protected]>
Signed-off-by: Liam Mark <[email protected]>
Signed-off-by: Swathi Sridhar <[email protected]>

也许,就单纯的是想要在创建映射上变得懒一点,或者,这是属于高通屎山代码的一部分?
如果你对此比较了解,欢迎在评论区补充。

自定义 compatible

还有一类预留内存空间带有自定义的 compatible 属性,比如 ramoops
这类空间将被交给对应的平台驱动进行处理。

不过需要的是,由于 devicetree 中其父节点 reserved-memory 并不是一个 simple-bus ,因此内核并不会为预留内存区域创建平台设备,这也就意味着与平台设备的 match 默认是无法进行的。

于是,dt 解析的逻辑中写死了一个表,compatible 与这个表相匹配的节点将会强制创建平台设备:

drivers/of/platform.c

static const struct of_device_id reserved_mem_matches[] = {
    { .compatible = "qcom,rmtfs-mem" },
    { .compatible = "qcom,cmd-db" },
    { .compatible = "ramoops" },
    {}
};

于是,上面的问题就被解决掉了。

后记

这篇文章仅作为对这段时间看到的东西的一点备忘,可能会有认识不准确的地方,欢迎在评论区指正。

参考资料

https://adtxl.com/index.php/archives/595.html
http://devicetree-org.github.io/devicetree-specification/
http://www.wowotech.net/memory_management/memory-layout.html
http://www.wowotech.net/memory_management/cma.html
https://android.googlesource.com/kernel/msm/+/android-7.1.0_r0.2/Documentation/devicetree/bindings/reserved-memory/reserved-memory.txt