简介这是一个适用于内核编译的编译器，是 ClangBuiltLinux 项目的直接编译产物，并启用了 Thin LTO 与 PGO 优化，支持的编译目标包括 arm、arm64 与 x86_64 。它集成了 clang 编译器与 binutils ，因此不再需要 gcc 作为内核编译的辅助。参考的内核编译启动参数：CLANG_PATH="path/to/your/clang" export PATH="$CLANG_PATH/bin:$PATH" args="-j$(nproc --all) \ O=out \ ARCH=arm64 \ CLANG_TRIPLE=aarch64-linux-gnu- \ CROSS_COMPILE=aarch64-linux-gnu- \ CROSS_COMPILE_COMPAT=arm-linux-gnueabi- \ LLVM=1 " make $args vendor/kona-perf_defconfig make $args下载17.0.616.0.6使用方法解压：$ mkdir clang $ tar -xJvf Horizon-Clang-<版本>.tar.xz -C clang然后参照上面的启动脚本使用即可。
前言在这篇文章中，我想要梳理一下几种 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-ranges 和 alignment 属性来为动态分配进行一些位置上的约束。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 的页面使用，而在区域需要被使用时再通过页面迁移将这块区域空出来。需要注意的是，只有 compatible 为 shared-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 池在 compatible 为 shared-dma-pool ，且 reusable 属性被设置 而 no-map 属性未被设置时，该内存区域会被用于创建一个系统的 CMA 池。如果额外设置了 linux,cma-default 属性，则该区域会成为系统的默认 CMA 池。需要注意的是，CMA 池对于内存地址的对齐是有要求的：kernel/dma/contiguous.cphys_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.cstatic 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 相匹配的内存区域，就会调用指定的回调进行处理。对于这里，就是遇到 compatible 为 shared-dma-pool 的内存区域，就将其传递给 rmem_cma_setup 函数。在 rmem_cma_setup 中，首先确保了区域是 reusable 且不是 no-map 的，然后将其注册到 CMA 框架，如果这个区域是默认的 CMA 区域，则设置一个全局变量指向它以方便后续使用。最后，一组回调被设置，即 rmem_cma_ops ，这组回调会在创建平台设备时被检查并调用，简单来讲是这样的一个过程：创建平台设备时，发现一个设备有名为 memory-region 的属性，于是根据这个属性找到对应的 struct reserved_mem ，并调用该处设置的回调。在回调中，设备对应的 struct device 的 cma_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 池在 compatible 为 shared-dma-pool ，且 reusable 属性未被设置时，该区域会被创建为一个 DMA 池。你没有看错，compatible 属性的值和上面的 CMA 池是一模一样的，唯一的区别是没有 reusable 属性，此时负责该区域的后端驱动就变得完全不同了。对于 arm64 ，此时的 no-map 属性是可选的（见下方代码），但事实上，没有 no-map 属性的区域是无法成功创建 DMA 池的，因为稍后在映射这个区域时使用的 memremap() 会直接抛出错误。kernel/dma/coherent.cstatic 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.cstatic 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 以上的内核就已经被移除掉，转而使用其它机制去替代了。想要使用它，你需要确保对应内存区域的 compatible 为 removed-dma-pool 。虽然对应代码中没有规定 no-map 属性是否要被设置，但和上面的 DMA 池类似，如果 no-map 没有被设置，在后续内存映射的过程中会直接失败。来看看代码：kernel/dma/removed.cstatic 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 device 的 removed_mem 字段中，以方便设备驱动的使用。对 removed 区域的使用，一般也会采用 dma_alloc_coherent() / dma_alloc_attrs() 的方式。接下来讲一讲 removed 池和 DMA 池的不同点。首先是映射创建的时机：kernel/dma/removed.cvoid *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 <lauraa@codeaurora.org> Signed-off-by: Patrick Daly <pdaly@codeaurora.org> Signed-off-by: Liam Mark <lmark@codeaurora.org> Signed-off-by: Swathi Sridhar <swatsrid@codeaurora.org>也许，就单纯的是想要在创建映射上变得懒一点，或者，这是属于高通屎山代码的一部分？如果你对此比较了解，欢迎在评论区补充。自定义 compatible还有一类预留内存空间带有自定义的 compatible 属性，比如 ramoops 。这类空间将被交给对应的平台驱动进行处理。不过需要的是，由于 devicetree 中其父节点 reserved-memory 并不是一个 simple-bus ，因此内核并不会为预留内存区域创建平台设备，这也就意味着与平台设备的 match 默认是无法进行的。于是，dt 解析的逻辑中写死了一个表，compatible 与这个表相匹配的节点将会强制创建平台设备：drivers/of/platform.cstatic 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.htmlhttp://devicetree-org.github.io/devicetree-specification/http://www.wowotech.net/memory_management/memory-layout.htmlhttp://www.wowotech.net/memory_management/cma.htmlhttps://android.googlesource.com/kernel/msm/+/android-7.1.0_r0.2/Documentation/devicetree/bindings/reserved-memory/reserved-memory.txt
前言当我们在做 backport 时，常常会面临这种情况——难以找到一个 commit 的上下文。在 backport 时，我们往往会盯住某一个模块，去查看该模块的 commit history 中有哪些上游更新，从而忽略了某些逻辑上相关而路径上不相关的内容。举个例子：我在尝试上游更新 arch/arm64/lib/ 时，查看的是该目录的 commit history ，从而忽略了针对 lib/ 或者是 include/linux/ 目录修改的前置 commit 。这种忽略轻则导致编译失败，重则导致隐性的不生效。所以，有没有什么办法能够查看一个 commit 相关的上下文呢？另外，当我们在做 backport 时，有时还会面临这样一种情况：我明确的想要某个 feature 或者 fix ，但是上游中并没有，那么有没有什么比上游还新的地方，可能有我想要的东西呢？↑ 为了解决上述问题，隆重请出 Linux Kernel Mailing List (LKML) ：Linux Kernel Mailing List（LKML）是 Linux 内核开发的主要邮件列表，它由 Linus Torvalds 于 1991 年创建，目的是协调 Linux 内核的开发和维护。 LKML 是一个公开的邮件列表，任何人都可以订阅并参与讨论。内核开发人员、Linux 用户、学生、研究人员等等都可以在这个列表上发帖，并且每个帖子都会被发送到订阅列表的所有人手中。订阅者可以回复这些帖子，对其中的问题提出自己的看法或建议，或者对代码进行评审和修复。 在 LKML 上，讨论的主题涉及 Linux 内核的各个方面，包括设计、开发、维护、测试和发布。邮件列表中的讨论非常活跃，许多内核开发者和爱好者都会在这里讨论 Linux 内核的各个方面。 LKML 是 Linux 内核开发的重要组成部分，也是 Linux 社区交流的重要平台之一。在这里，开发者们可以共同探讨 Linux 内核的发展方向和技术细节，使得 Linux 内核不断发展壮大。 （来自 ChatGPT）↑ 呐，没错，Linux 内核开发的主战场还是在使用老掉牙的邮件系统。接下来，我们将从 Mailing List 开始，简单的介绍一下该如何优雅的从内核最上游摘取补丁。Mailing List 如何工作？众所周知，Mailing List 是可以订阅的，订阅后可以收到大家的讨论邮件。它的订阅方式在网上有许多的介绍，这里就不再赘述了。现在，让我们来关心一个最基本的问题：邮件，大家都知道，是一个人发给另一个人的，那么为什么我订阅了邮件列表我就可以收到大家的讨论邮件？难道大家在发送邮件时都手动指定了我为收件人吗？并不是这样的，这个过程是这样实现的：每个人在发送邮件时，需要额外抄送给“邮件列表”，“邮件列表”则会把收到的邮件再转发给它的订阅者，于是这封邮件就被“广播”了出去。常见的邮件列表可以在这里找到。不同的邮件列表往往有不同的主题（不同子系统/平台），如果你只关心某些特定的内容，查看/订阅特定的邮件列表即可。当然，还有一个主邮件列表，叫做 linux-kernel ，其邮件地址为 linux-kernel@vger.kernel.org ，它的流量非常大，一天往往有上百封邮件，因此除非你的邮箱容量足够大，否则千万不要订阅 xD 。以下是一个发送邮件的样例：git send-email \ --to=i@xzr.moe \ --cc=linux-arm-kernel@lists.infradead.org \ --cc=linux-kernel@vger.kernel.org \ path/to/my/mail这封邮件的主收件人是 i@xzr.moe ，并同时抄送给了邮件列表 linux-arm-kernel@lists.infradead.org 和 linux-kernel@vger.kernel.org ，于是这俩邮件列表的订阅者也都会收到这封邮件。（可以看到，不是所有邮件列表都是 host by kernel.org 的，也有许多“第三方”的邮件列表）往往，在发送内核相关邮件时，除了会抄送给主题相关的“特定邮件列表”，也会抄送给“主邮件列表”一份，这也是“主邮件列表”流量巨大的原因之一。按照我的理解，邮件列表其实是一种抽象的“论坛”组织形式。不同的邮件列表就像论坛中不同的版块，发送给邮件列表的邮件就像论坛中的帖子。既然是论坛，那它也应该要提供组织邮件之间关系的方法，就好比“主题帖”与“回帖”。接下来我们将会浅浅的看一下邮件协议，来理解一下邮件之间的关系是如何进行组织的。邮件协议在这里，我想要基于常见的 RFC 5322 规范对邮件头部的部分字段进行介绍，并结合一下 git send-email 命令。RFC 5322是一份Internet标准，它规定了电子邮件消息的格式和传输规范。该标准在2008年发布，取代了之前的RFC 2822标准。 RFC 5322描述了电子邮件的组成部分、消息头和消息体、邮件地址的格式、日期和时间格式、消息编码和传输规范等方面的细节。它还包括了对一些早期邮件标准的修订和更新，以适应现代电子邮件的需求。 RFC 5322规定了邮件格式的基本要求，包括了邮件头、主题、发件人和收件人等重要信息。邮件头包含了所有关于邮件的元信息，例如主题、发件人、收件人、抄送、密送、日期和时间等。邮件体则包含了实际的邮件内容。 RFC 5322为电子邮件的传输提供了一些规范，例如SMTP（Simple Mail Transfer Protocol）协议，以确保邮件能够在网络上正确地传输。它还指定了如何使用MIME（Multipurpose Internet Mail Extensions）来支持在电子邮件中包含各种类型的附件和多媒体内容。 总之，RFC 5322为电子邮件的格式和传输提供了一个详细的标准，使不同的电子邮件客户端和服务器能够互相兼容，确保电子邮件能够在互联网上稳定、高效地传输。 （来自 ChatGPT）收件人 和 抄送首先，我想先说说邮件头部的 To 字段和 Cc 字段，它们分别对应 git send-email 中的 --to 和 --cc 。在常见的网页版邮箱中，To 对应的是“收件人”，而 Cc 对应的是“抄送”。这俩字段在实际效果上并没有什么区别，被指定在邮箱都能够收到这封邮件。它们之间的区别更多的是在邮件礼仪上。To 更多的表示希望收件者能够处理和回应这封邮件；而 Cc 则表示这封邮件并非是针对收件者的，邮件内容仅供参考。在 To 和 Cc 字段中出现的邮箱，互相都可以看到。也就是说，当你收到一封邮件时，无论你是“收件人”还是“被抄送者”，你都可以看到这封邮件原始的收件人和抄送者有谁。试想这样一种情况：甲要找乙办事，但是考虑到乙可能不会理他，于是甲就在发送邮件时 Cc 了乙的上级领导丙。于是乙在收到邮件时一看抄送者，“wok 这封邮件我的领导丙也能收到，看来这事不得不认真办”。（←借他人施压）在 kernel mailing 中，To 一般被设置为直接相关人（比如对应模块的维护者），Cc 则被设置为“可能对此感兴趣的人”以及“邮件列表”。To 和 Cc 都可以有多个，完全没有问题。唯一标识符邮件头部还有一个名为 Message-Id 的字段。这个字段是邮件的唯一标识符，每一封邮件的这个字段内容都应该不同。这个字段由发送方的客户端或是邮件服务器填入，其内容一般是随机字符串或是由时间戳、随机字符以及发送方邮箱地址拼接而成的一长串。Message-Id 可能长这样：20180827110245.14812-1-ard.biesheuvel@linaro.orgADsAxwDFFmQ8gOTrSjHk8aoU.1.1677723823079.Hmail.kf@office.163.com30646027.20230425162525.6447fef5d41b97.31335891@mail128-83.atl41.mandrillapp.com又或者干脆完全随机：6050123042401634172总之，它对这封邮件起到唯一标识作用，而且需要在邮件被发送时就准备好，因为接收方可以通过这个字段来判断一些意外情况 e.g. 由于网络原因重复收到了相同的邮件。当然，Message-Id 的作用远大于此，将在下面继续展开。回复与引用手动的引用先不讲邮件协议，先来讲一种约定俗成。在回复一封邮件时，一般而言我们会在邮件的标题加上 Re: ，并在邮件内容中使用 >> 来引用想要回复的文本。举个栗子。原邮件：标题：我觉得小明做错了 他把 A 错弄成了 B ，并造成了 C 的结果。这一切的责任都应该由他来承担。回复邮件：标题：Re: 我觉得小明做错了 >> 他把 A 错弄成了 B 不对，他是把 A 错弄成了 C 。 >> 并造成了 C 的结果 造成的结果是 D。 >> 这一切的责任都应该由他来承担 是小王叫他这么做的，小王也有责任。回复邮件的回复邮件：标题：Re: 我觉得小明做错了 >>>> 他把 A 错弄成了 B >> >> 不对，他是把 A 错弄成了 C 。 >> >>>> 并造成了 C 的结果 >> >> 造成的结果是 D。 >> >>>> 这一切的责任都应该由他来承担 >> >> 是小王叫他这么做的，小王也有责任。 我同意你的观点。总之，就是使用 >> 来引用之前的邮件内容，并针对性的发表你的意见，从而使对方能够更加高效的理解。这一切是建立在自觉的基础上的，你完全可以不遵守这种格式，甚至可以捣乱去强行引用对方没说过的话（因为引用的内容你完全也可以自己输入）。而且，在回复时，对方的一句话很有可能会被拆分为多次引用来加强针对性（比如上面的）。再者，如果有多次来回回复，>> 可能会越堆越高，影响体验，说不定总有一次会被清掉。说了这么多，我想表达的就是：这种约定俗成是无法被用来可靠的追踪邮件之间的关系的，邮件协议必须提供一种可靠的组织邮件之间关系的方法。而这正是我接下来想要介绍的内容。回复与引用字段于是，邮件的头部中提供了 In-Reply-To 和 References 两个字段，来可靠的追踪邮件之间的关系。Basically，它们是靠你在发送邮件时手动指定“回复的 Message-Id”来工作的（一般来说网页版邮箱或者邮件客户端会自动帮你做这件事）。上面已经说到了，Message-Id 是唯一标识符，那么在回复时通过标识符来指定自己回复的是谁，就非常的可靠了。举个例子，你收到了一封邮件 A ，假设其 Message-Id 为 aaaaa 。现在，你想要回复这封邮件，于是你使用 git send-email 发送邮件 B ，指定 --in-reply-to=aaaaa 来表示你回复的是 A 。假设你所发送的邮件 B 的 Message-Id 为 bbbbb ，那么对方在收到你的回复后，可以使用 --in-reply-to=bbbbb 来回复你的回复。在上面的来回中，邮件头部的 In-Reply-To 会被设置为 --in-reply-to= 所指定的值，而 References 则会在先前的基础上 append 上新的 In-Reply-To 值，展开看看就是这样：邮件 A ：Message-ID: <aaaaa>（回复给 A ）邮件 B ：Message-ID: <bbbbb> References: <aaaaa> In-Reply-To: <aaaaa>（假设对方继续回复 B ）邮件 C ：Message-ID: <ccccc> References: <aaaaa> <bbbbb> In-Reply-To: <bbbbb>（假设你继续回复 C ）邮件 D ：Message-ID: <ddddd> References: <aaaaa> <bbbbb> <ccccc> In-Reply-To: <ccccc>In-Reply-To 总是指定着想要回复的邮件标识符，而 References 则记录着整个邮件来往中涉及到的邮件标识符。借助这两个字段，我们完全可以可靠的组织邮件，在邮件之间形成一种树状的关系。如果说那个最初的邮件是论坛的“主题帖”的话，我们就可以通过这两个字段轻松的追踪所有的“回帖”，并建立“回帖”之间的关系。具体的东西等下面介绍 lore.kernel.org 的时候再进一步介绍。其它邮件的头部还有一些别的有趣的字段，但是由于它们和接下来的内容关系不大，于是就懒得展开解释了。比如 Received 能够提供邮件传输过程中的接收记录，你可以轻松的知晓这封邮件经过了哪些邮件服务器。还有比如 Reply-To 和 Mail-Reply-To 能够指定“如果想要回复这封邮件的话，应该回复给谁”，因为有时发件者的邮箱并不接受回复，想要回复需要发到别的邮箱地址。当然，还有一些非常基础的字段，比如 From、Date、Subject ，它们分别标识了邮件的发送者、发送日期和标题。它们都是属于一看就能明白含义的那种，故也没有什么解释的必要了（不过接下来的内容中它们会出现）。邮件与补丁接下来，我想要聊聊 git 生成的补丁。这里所说的补丁，是指使用 git format-patch 命令生成的 .patch 文件。从历史上来说，这种补丁从设计之初开始就是为了作为邮件发送的，甚至可以说是量身定制的。我们可以随便生成两个补丁看看：# 把我的仓库中最新的两条 commit 作为一组补丁进行生成 $ git format-patch -2 0001-oplus_charger-Initialize-works-before-using-them.patch 0002-dsi_panel-Make-sure-the-gpio-is-valid-before-directi.patch来看看它们的内容（只截取了部分）:...... From: LibXZR <i@xzr.moe> Date: Sun, 16 Apr 2023 20:45:31 +0800 Subject: [PATCH 1/2] oplus_charger: Initialize works before using them ............ From: LibXZR <i@xzr.moe> Date: Mon, 17 Apr 2023 12:15:05 +0800 Subject: [PATCH 2/2] dsi_panel: Make sure the gpio is valid before direction input ......可以看到，补丁的头部字段是和邮件格式相对应的，当把补丁作为邮件发送时，这些字段也会自然而然的成为邮件头部的一部分。再者，补丁文件本身就是纯文本，不包含任何不可识读的二进制数据，被塞进邮件中一点问题也没有（按照规范，补丁是作为邮件的内容（以及部分头部）来发送的，而不是附件，这很重要，不过 git send-email 会自动帮你处理好这一点）。其实 git am 除了能够打补丁，还支持打邮件（即包含补丁的原始邮件数据），它会自动处理掉邮件格式中的补丁无关内容，从中提取出真正的补丁然后进行应用（下面会进一步介绍）。小特性：前缀序号在上面的例子中能够看到 git format-patch 为生成补丁的标题自动加上了如 [PATCH 1/2] 这样的前缀，这其实是一种便于邮件整理的特性。补丁往往是一系列的，而且是具有前后依赖关系的，在经过了邮件这一中间介质后，这种顺序可能会是错乱或是丢失的。（你可能会说，我们生成的补丁文件的文件名不是有 0001、0002 这样的前缀么？这不是标识着顺序么？那为何要多此一举？别忘了，发送邮件时补丁可不是作为附件的，这里的文件名压根就只是给你自己看的，邮件的接收方可看不到）于是 git format-patch 会自动在你的 commit message 前面加上这种前缀，表示这是某个补丁序列中的第几个补丁（以及这个序列总共有多少个补丁）。别担心，这个前缀在 git am 时会自动被去掉，没什么不良影响。小特性：前缀版本有时候，我们会在改进了自己的补丁后重新发送。在这种情况下，又该如何快速区分哪个补丁更新呢？emmm ，邮件的 Subject 可是由 commit message 生成的啊，难道我得重新写 commit message 吗？并不需要，其实你只要在 git format-patch 加上选项 -v 即可。举个例子：$ git format-patch -2 -v2 v2-0001-oplus_charger-Initialize-works-before-using-them.patch v2-0002-dsi_panel-Make-sure-the-gpio-is-valid-before-dire.patch $ cat v2-0001-oplus_charger-Initialize-works-before-using-them.patch | grep Subject Subject: [PATCH v2 1/2] oplus_charger: Initialize works before using them cat v2-0002-dsi_panel-Make-sure-the-gpio-is-valid-before-dire.patch | grep Subject Subject: [PATCH v2 2/2] dsi_panel: Make sure the gpio is valid before呐，是不是很方便？v2 代表第二版补丁，v3 代表第三版，依此类推。当然，这些信息在 git am 时也会自动被去除掉。小特性：邮件封面这里的“邮件封面”是对 cover letter 的生硬翻译。在我们发送一系列的补丁时，总需要总结一下这一系列补丁大概完成了什么样的工作吧？cover letter 起到的就是这样的“总结”作用。我们可以在 git format-patch 时使用 --cover-letter 选项来让 git 帮我们（部分的）完成这个过程。$ git format-patch -2 --cover-letter 0000-cover-letter.patch 0001-oplus_charger-Initialize-works-before-using-them.patch 0002-dsi_panel-Make-sure-the-gpio-is-valid-before-directi.patch生成出来的 cover letter 长这样：From 86746dbc34d68fd8392064ead51e2ba0b7afc56a Mon Sep 17 00:00:00 2001 From: LibXZR <i@xzr.moe> Date: Thu, 27 Apr 2023 23:28:57 +0800 Subject: [PATCH 0/2] *** SUBJECT HERE *** *** BLURB HERE *** LibXZR (2): oplus_charger: Initialize works before using them dsi_panel: Make sure the gpio is valid before direction input oplus/kernel/charger/oplus_charger.c | 18 ++++++++++-------- techpack/display/msm/dsi/dsi_panel.c | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) -- 2.34.1可以看到，其标题的前缀为 [PATCH 0/2] ，即这个补丁的序号为 0 ，同时这个补丁也没有什么实质上的内容。没错，这是一个补丁，但是是一个空的补丁，其除了 commit message 以外什么都没有。它里面的 *** SUBJECT HERE *** 和 *** BLURB HERE *** 是让你自己填写标题和内容的地方，就在那里对你的这一系列工作进行一个介绍和总结吧。嗯，它也是可以通过 git send-email 进行发送的。小结在这一部分内容中，我们认识到了 git patch 和邮件之间的血缘关系，也了解到了 git 所提供的邮件标题前缀的一些约定。接下来，我们将会打开邮件列表，来看看这些东西在实践中是如何被运用的。lore.kernel.orglore.kernel.org 是一个邮件列表的归档网站，说白了就是把邮件列表中出现的邮件们保存起来并提供如搜索、关系整理等附加功能。接下来，我将用一个例子来对 lore 的页面以及 kernel mailing 中的一些规范进行进一步的解释。这个例子的地址是 https://lore.kernel.org/lkml/20180827110245.14812-1-ard.biesheuvel@linaro.org/ ，你也可以打开这个网址，和我的解释相互对照。从网址开始嗯，lore 的页面设计的非常阴间，非常容易让人晕头转向。但是，它的页面是静态的纯文本，因此我们可以简单的通过网址来判断当前处于一个什么样的状态下。就比如：https://lore.kernel.org/lkml/20180827110245.14812-1-ard.biesheuvel@linaro.org/thttps://lore.kernel.org/lkml/20180827110245.14812-1-ard.biesheuvel@linaro.org/T这俩，只是在上面地址的基础上简单加上了一个后缀，其页面的内容就有大不同。这后缀的具体含义将在下面再进一步解释，现在，让我们聚焦于最干净原始地址：https://lore.kernel.org/lkml/20180827110245.14812-1-ard.biesheuvel@linaro.org/在这里，前面的 lore.kernel.org 就是这个站的域名，没什么好说的。再往后，lkml 代表了处于“主邮件列表”中，对于不同的邮件列表，这里的值可能会发生变化，具体可用的邮件列表可以从 lore 的首页点进去然后看一看网址。再往后，20180827110245.14812-1-ard.biesheuvel@linaro.org 则是这一封邮件的 Message-Id 。众所周知，Message-Id 是唯一的，因此 lore 可以使用 Message-Id 来轻松的定位邮件。所以，我们在这个“原始地址”中看到的页面，其绝大部分内容都由这封邮件的内容组成。看看页面我们打开上面的，不带后缀的，干净的“原始地址”。可以看到，页面主要由三大部分组成。第一部分，邮件的内容：↑ 这一部分对邮件的内容以及部分头部信息进行了完整的展示。由于这个链接指向的邮件是一封 conver letter ，故其只有邮件内容，而没有代码的 diff 部分。点击上图中的 raw 可以打开这封邮件的原始数据，点击 [thread overview] 则可以跳转到第二部分。第二部分，“帖子概览”：↑ 这一部分的内容是根据邮件头部的 In-Reply-To 和 References 字段自动生成的，它反映的是一系列邮件之间的关系，以及当前邮件在这一系列关系中的位置。可能看着有点乱，但是，看到那些日期了吗？日期所对应的每一行都是一封邮件，无论右边的超链接是人名还是邮件标题，它们都是完整的邮件！至于什么时候只显示人名，什么时候显示完整标题，下面再说。你只需要点击右侧的超链接，就可以跳转到这封邮件，并查看其内容。你可以按照论坛的“主题帖”与“回帖”来理解这种关系：简单来说，你只需要观察缩进即可。没有缩进的是“主题帖”，也就是一封没有指定 In-Reply-To 的邮件。具有一层缩进的是“一级回帖”，也就是 In-Reply-To 被设置为“主题帖”的邮件。具有两层缩进的是“二级回帖”，也就是 In-Reply-To 被设置为“一级回帖”的邮件。依此类推，缩进越多，所对应的回帖级数也越多。于是，邮件之间的树形关系就被用这种形式展现出来了，简单而优雅。那么，为什么这些邮件在显示的时候，有时候只显示发送者的名字，有时候又显示邮件的完整标题呢？据我的观察，它的显示规律大概是这样的：当前所在的邮件，无论如何都会被显示为 发送者名字 [this message]。回复与评论性质的邮件，一般只显示发送者名字。补丁性质的邮件，一般显示标题。（发送者名字作为非超链接放在旁边）这种总结只是针对大多数情况，并非是完全准确的，就以上面的例子来说，Theodore Y. Ts'o 所发送的“一级回帖”，是一封评论性质的邮件，但是它的标题被显示了（而且不是完整显示，因为它的标题前面本身还有个 Re: ）。↑ 我确实不太能够摸清楚它的显隐规则是怎样的，甚至我觉得它的显隐规则是非常 buggy 的。但是这无论是对于查找邮件还是浏览邮件都没有任何影响，你只需要点进去看一看内容就能清楚地知道这封邮件到底是个啥了。从这里，我想你能够发现一个重要的规范：对于一系列的补丁，cover letter 一般会先被发送来创建一个“主题帖”，而系列中的其它补丁则会作为“一级回帖”来 In-Reply-To cover letter ，于是这个补丁集就被组织在了一起，查找起来也就方便了许多。 而这，也正是 cover letter 为何会被称为 cover letter 。把上面的这个例子整理成树形图后，我想应该能够看的更加清楚：总结：日期的每一行都是一封邮件。看缩进在脑子里构建树形图。Cover letter 是主题帖。补丁集是回帖。对 cover letter 的评论可能和补丁集混在同一层（“一级回帖”）。别靠 发送者名字/邮件标题 的显隐来分辨内容，点进去看看才知道到底是啥。对了，版本不同的补丁（比如 v2 和 v3 ），按照规范是要放在不同的“主题帖”中的。第三部分，邮件回复这一部分的内容就相当容易理解了，lore 会自动告诉你如果想要回复这封邮件的话该怎么做。lore 会自动为你生成 git send-email 命令，并填入相关参数。平铺的页面上面的一切，针对的都是不带有后缀的，干净的“原始地址”。接下来，介绍一下带有特殊后缀的地址，以及 lore 中的跳转操作。在这里，有俩链接：它们都可以对这个帖子下的所有内容进行展开，也就是说，把这个“主题帖”下的所有回帖全部铺开显示在一个网页上。铺开模式有两种：flat ：只是将全部回帖的内容平铺开，等于在网址后面加上 /T 。nested ：平铺开并以缩进的形式展示回帖之间的树状关系，等于在网址后面加上 /t 。嗯，铺开了就更像论坛了是吧 xD 。无论是在哪种铺开模式下，你都可以通过点击这里的 permalink 来回到不带有后缀的“原始地址”。从 lore 的列表中打开邮件 / 从谷歌搜索打开邮件时的页面一般都默认带有 /T 的后缀。由于我不太喜欢这种铺开的模式，因为看着很乱，于是我从谷歌搜索打开的第一件事就是点击 permalink 。有时候 lore 还会自动在链接后面加上页内定位符，比如 #u 。由于这种东西对页面内容没有影响，只是滚动网页罢了，于是这里就不展开介绍了。打邮件（不推荐）所谓的打邮件，其实就是用 git am 打补丁，不过呢，是把邮件当成补丁来打（上面提到过了）。接下来讲一讲该如何下载和应用整个补丁集。继续上面的例子，我们可以点击 mbox.gz 把整个帖子下载过来（包括“主题帖”以及其下面的所有“回帖”）：↑ 只要是在这个“主题”中，无论是从哪个邮件下载 mbox.gz ，得到的内容都是一样的，都是整个帖子的内容。所谓整个帖子的内容，其实就是把帖子中的原始邮件文本全部连接在了一起。↑ 从之前的分析中可以看到，所谓的补丁集，其实就是“主题帖”（cover letter）的“一级回帖”，此时我们下载了整个帖子，因此整个补丁集就已经被包含在这个邮件文件中了。在解压缩后，我们可以直接应用这个邮件集合，git 会自动处理掉邮件协议中与补丁不相关的部分。# 下载邮件集，相当于点击 `mbox.gz` 。 $ wget https://lore.kernel.org/lkml/20180827110245.14812-1-ard.biesheuvel@linaro.org/t.mbox.gz # 解压缩 $ gunzip t.mbox.gz # 应用 $ git am t.mbox -3这里的 -3 是在告诉 git am 要使用三方合并的冲突处理方式，也就是像 git cherry-pick 那样处理冲突。我想，没人喜欢用阴间的 --reject 来处理补丁冲突吧？接下来，git 将会按顺序应用这个邮件集合中的所有补丁内容。那个，上面也说过了，cover letter 本质上也是一个补丁，只不过这个补丁的内容是空的。所以，最先被应用的补丁，会是空的 cover letter ？没错，是这样的：$ git am t.mbox -3 Patch is empty. When you have resolved this problem, run "git am --continue". If you prefer to skip this patch, run "git am --skip" instead. To restore the original branch and stop patching, run "git am --abort".在“打邮件”时，假如你正在应用的是一个补丁集，那遇到这样的情况非常正常，只需要使用 git am --skip 跳过即可。除了 cover letter 以外，回复和讨论相关的邮件（即没有 diff 内容的邮件）都会被当成空补丁处理，需要手动进行跳过。哦对了，上面所说的“按顺序应用”，按的并不是 patch 的序号，而是它出现在这个 mbox 文件中的顺序。这两者的顺序可能是不同的，因此可能会造成意外的问题，这也是我不推荐使用这种方法打补丁的重要原因。另外，应用单个补丁（而不是整个帖子）也是可行的，你只需要将那个邮件的 raw 文件下载过来即可。b4 工具我想，这才是从 lore 拉取补丁的正统方法。在过去几周中，我一直在开发一个工具，用于从 lore.kernel.org 中获取补丁并执行大多数维护者常见的后处理操作，包括： - 将补丁重新排列为正确的顺序 - 统计各种后续跟踪信息，例如已审核、已确认等 - 检查是否存在更新的补丁系列修订版本，并自动获取它。 - 这个工具最初被称为 "get-lore-mbox" ，但现在已经成为一个名为 "b4" 的独立项目。你可以在 git.kernel.org 和 pypi 上找到它。 （来自 https://people.kernel.org/monsieuricon/introducing-b4-and-patch-attestation ）这个工具的源码可以在 git.kernel.org 和 pypi 找到。安装它非常简单，首先安装 python 和 pip ，然后 pip install b4 即可。这个工具带有许多内核维护向的功能，你可以打开上面的链接自己去具体阅读一下相关文档。在这里，我们只关心如何使用它来从 lore 上拉取和应用补丁。非常简单：b4 am -o - "补丁对应的 Message id " | git am -3上面这一行命令做了以下几件事：从 lore 拉取指定的补丁。将补丁通过管道传递给 git 。在应用补丁时采用三方合并。你甚至可以把命令封装成一个脚本：~/bin/pw#! /usr/bin/bash b4 am -o - "$1" | git am -3确保它在 PATH 里，给足权限，然后像 cherry-pick 一样简单的使用，比如：pw 20180827110245.14812-1-ard.biesheuvel@linaro.org 。b4 工具在拉取补丁时会自动完成以下事情：查找所给 Message-Id 对应的“主题帖”。拉取“主题帖”下的所有补丁邮件（非补丁相关邮件会被直接去除掉）。去除邮件协议中的非补丁相关部分。还原补丁的正确顺序。......总的来说，开箱即用，非常友好。补丁的搜索对于 lore 来说，搜索补丁只需要在其首页或对应 mailing list 的搜索框中输入关键词（比如补丁的标题），然后点击 search all inboxes 或 search 即可。只不过，这个搜索体验实在是不怎么友好，经常会出现一大堆的无关内容。所以，对于搜索补丁，我还是更倾向于使用谷歌。只要随便一个邮件列表归档网站收录了这个补丁，那么我都可以轻松的获得其 Message-Id ，然后反过来用拼凑网址的方法在 lore 上查看它或者直接使用 b4 工具拉取它。后记这篇文章粗略的介绍了 Linux Kernel Mailing List ，并展示了邮件与 git 补丁之间千丝万缕的联系。现在，可以回答前言中的那个问题了：有没有什么办法能够查看一个 commit 相关的上下文呢？只需要使用任意方式搜索它的 commit message ，找到它对应的邮件 Message-Id ，然后利用像 lore 这样的邮件列表归档找到它所属的“主题帖”，自然而然的，上下文相关的补丁也都会展示在你的面前了。当然，除了 lore ，还有许多界面更加友善的邮件列表归档，比如 https://patchwork.kernel.org/ 。不过它们收录的补丁似乎远不及 lore 来的齐全，而且界面有点太友善以至于没什么介绍的必要了...参考资料https://en.wikipedia.org/wiki/Conversation_threadinghttps://docs.kernel.org/translations/zh_CN/process/submitting-patches.htmlhttps://en.wikipedia.org/wiki/Email#Internet_Message_Formathttps://lpc.events/event/11/contributions/983/attachments/759/1421/Doing%20more%20with%20lore%20and%20b4.pdfhttps://people.kernel.org/monsieuricon/introducing-b4-and-patch-attestationhttps://adam.younglogic.com/2022/10/apply-linux-kernel-patches-from-lkml/https://blog.csdn.net/jianchi88/article/details/124960869
借助 uprobe 实现对应用帧生成时间的监听
English前言前段时间有个概念很火，它被称为 FAS (Frame time Aware Scheduling) ，即根据帧生成时间而非处理器负载来调控处理器频率。这种操作能够在不影响帧率的前提下尽可能的降低处理器频率，从而改善游戏的能效。不少手机厂商也采取了类似的思路来改善游戏功耗，如小米的 FEAS 。接下来，我想要明确一下接下来会用到的概念。首先是“帧时间”和“帧生成时间”，这两者看着类似但实际上是完全不同的。“帧时间”是两次显示内容改变之间的间隔，“帧生成时间”则是生成这一帧花费的时间。以上面手搓的这张图为例，这张图描述了简化的帧渲染——显示流程(1)。上图中，横向为时间轴，绿色竖线代表在该时刻发生了 vsync 信号，红色部分代表新的帧正在生成。红色标号代表第 n 帧正在生成，蓝色标号代表第 n 帧正显示在屏幕上。在 vsync 信号发生时，之前已经生成完毕的帧会被显示在屏幕上，而新一帧的生成也会开始。我们来详细解释一下上面这张图：首先，第 1 帧开始生成，这一帧的生成很快就完成了，但是必须等到 vsync 信号到来时才会被显示在屏幕上，于是它凭空等待了一会儿。vsync 信号到来，第 1 帧被显示在了屏幕上，此时第 2 帧的生成开始。第 2 帧的生成也很快就完成了，但必须继续等待下一个 vsync 信号。vsync 信号到来，第 2 帧被显示在了屏幕上，此时第 3 帧的生成开始。第 3 帧的生成看起来有点慢。vsync 信号到来，但是此时第 3 帧的生成还没有完成，于是屏幕显示的内容没有改变，第 2 帧继续显示在屏幕上。此时，我们便可以认为发生了丢帧。vsync 信号到来，这下第 3 帧已经生成好了，于是它被显示在了屏幕上，同时第 4 帧的生成开始了。通过上面的这些解释，我想你应该能够更好的区分“帧时间”和“帧生成时间”了。在上面的图中，被蓝色括号包住的部分即为第 n 帧的“帧时间”，被红色括号包住的部分即为第 n 帧的“帧生成时间”。可以看到，“帧生成时间”能够直接反应当前性能，假如某一帧的“帧生成时间”超过了 vsync 间隔（即屏幕刷新间隔），丢帧势必发生。在丢帧已经开始发生时，“帧时间”也在一定程度上能够反应性能。对于上面所说的 FAS ，最佳的实践方式是：使“帧生成时间”尽量从左侧逼近（小于等于）“屏幕刷新间隔”。但是，实际上，这并不是一件容易做到的事情，原因在于“帧生成时间”的数据实在是有些难以获取。一般而言，想要获取“帧生成时间”，是要对应用进程下手的，毕竟帧的渲染流程主要发生在对应的应用进程中(2)。“帧时间”则可以直接从 surfaceflinger 中获取，相对来说就会方便许多。于是，现在常见的 FAS 实现，如 Scene FAS ，采用的都是使用“帧时间”来替代“帧生成时间”的方案，比如控制处理器频率使得“帧时间”尽量稳定。对于游戏类应用，这种方式其实就已经能够起到不错的效果了，因为游戏画面的帧率往往是稳定且连续的，但是对于日常应用，这可是要翻车的。日常应用中，我们可能会遇到许多画面完全静止的情况，比如在阅读时。在这种情况下，是不存在帧的刷新的(3)，那么此时的“帧时间”将会变得非常大，但是，这能够说明此时因为性能不足而丢帧吗？并不能，因为此时是压根没有新的帧，而不是因为性能不足而丢帧。“帧时间”存在的这种缺陷使得 FAS 思想迟迟难以被使用到日常应用中。这篇文章所展示的内容将会帮助解决这一困境，但也只能解决基于 libhwui 的普通 Android 应用的“帧生成时间”获取问题，对于采用自己渲染引擎的游戏类应用，到目前为止，使用“帧时间”代替“帧生成时间”仍然是最好的妥协方案。注释：(1) 没错，这真的简化了很多东西，但拿来帮助理解我觉得完全没有问题。在真实情况下，由于 GPU 的异步性，还存在三重缓冲的问题，而且 vsync-sf 和 vsync-app 信号发生的时间也不一定是同步的，这些信号的发生时间甚至可能不是固定的，详见 (3) 。(2) 我曾经想过在 surfaceflinger 中把分发 vsync 信号到应用 queuebuffer() 之间的间隔作为“帧生成时间”，但是没有成功，因为这对于在内核中操作来说太不友好了，需要大量的逆向和面向寄存器编程工作。但是，对于第三方 ROM ，这也许是一个可行的思路。不过鉴于应用在收到 vsync 后不一定会更新帧，那就还得弄清楚这个 queuebuffer() 属于哪一次 vsync 信号，还是有些难度👀。(3) surfaceflinger 自身使用的（vsync-sf）和向应用程序分发的（vsync-app） vsync 信号并不直接来自于 hwcomposer 。surfaceflinger 自身维护了一个软件 vsync 发生器，hwcomposer 发来的硬件 vsync 信号会被送去校准这一“软件发生器”。不必要时，硬件 vsync 信号甚至会被直接关闭掉。这也就意味着，软件发生的 vsync 信号不一定是规律且连续的，vsync-sf 和 vsync-app 信号之间也可以存在相位差。以 vsync-sf 信号为例，我的理解是这样的（不一定准确）：只有在应用提交了 Graphic Buffer 后，surfaceflinger 才会 schedule 下一个 vsync-sf 来刷新屏幕；假如压根就没有应用提交 Graphic Buffer ，就不存在下一个 vsync-sf 信号，屏幕也就不会刷新。从 uprobe 开始Linux 内核提供了一种机制，通过替换应用内存页面中指定地址上的指令为“断点异常指令”，从而使应用在指定的时机陷入内核态并执行回调代码，这一机制被称为 uprobe 。总的来说，这个过程是这样的：替换指定地址上的指令为“断点异常指令”。应用执行到该指令，陷入内核态。在内核态查询注册的 uprobes 及其 handler ，并执行指定的回调。单步执行之前被替换的指令。恢复应用程序，使其在断点指令之后继续正常执行。在陷入内核态时，刚刚应用处于用户态时的寄存器信息会被保存起来。在 uprobe 回调函数中，我们可以访问这些信息，甚至可以修改这些信息。uprobe 的典型使用方法是基于 tracefs 的，就比如 官方使用指南 。但很遗憾，这种东西对于内核侧开发并没有什么帮助。在内核侧使用 uprobe api 相关东西的资料在网上也比较少，那接下来就简单介绍一下，投喂一下 new bing 。以下内容基于 4.19 内核。#include <linux/fs.h> #include <linux/uprobes.h> #define BINARY_PATH "/bin/my_binary" #define OFFSET 0x114514 static int my_handler( struct uprobe_consumer *self, struct pt_regs *regs) { pr_info("Here we are in uprobe callback"); return 0; } static struct uprobe_consumer my_consumer = { .handler = &my_handler }; static void my_uprobe_init(void) { struct path p; struct inode *i; int ret; ret = kern_path(BINARY_PATH, 0, &p); if (ret) goto error; i = d_inode(p.dentry); ret = uprobe_register(i, OFFSET, &my_consumer); if (ret) goto clean; return; clean: path_put(&p); error: pr_err("Unable to register uprobe, ret = %d", ret); }可以看到，注册 uprobe 的关键函数是 uprobe_register(struct inode *inode, loff_t offset, struct uprobe_consumer *uc) 。这个函数需要传入三个参数，首先是那个需要被替换指令的文件的 inode ，其次是需要被替换的指令在文件中的偏移量，最后则是用以指定处理方式的 uprobe_consumer 。inode 我们可以使用上面代码中所示的方式简单的拿到；偏移量则非常的关键，我们留到下面专门说；uprobe_consumer 中其实除了用来指定回调的 handler 外，还有用于指定 uretprobe 回调的 ret_handler ，以及用于过滤的 filter 。这些内容由于这里用不到就不再展开了，有兴趣可以自己去阅读相关代码。接下来讲一讲非常关键的偏移量，这个点我感觉官方文档并没有讲清楚。这里的偏移指的是需要被替换的指令在文件中的偏移量，既不是该指令在虚拟地址空间中的地址，也不是该指令相对于 .text 段的偏移量。不知道为什么，官方文档 把在 .text 段中的偏移量当成在文件中的偏移量了。接下来用一个小例子来看看这个偏移量该如何计算。#include <stdio.h> void hello() { printf("Hello\n"); } int main(int argc, char *argv[]) { int i = 10; while (i--) { hello(); } return 0; }这是一段非常简单的代码，它会调用 hello() 十次。编译完成后(1)，我们可以使用 readelf -s 命令打印 hello 函数在虚拟地址空间中的地址：$ readelf -s obj/local/arm64-v8a/up-test | grep hello 46: 0000000000001744 28 FUNC GLOBAL DEFAULT 14 hello可以看到，其地址为 0x1744 。接下来，我们使用 readelf -S 寻找 hello 所在的 .text 段在虚拟内存空间中的地址：$ readelf -S obj/local/arm64-v8a/up-test | grep .text [14] .text PROGBITS 00000000000016ac 000006ac在这里，0x16ac 即为 .text 段在虚拟内存空间中的地址。那么，0x6ac 又是什么呢？它是 .text 段在该文件中的偏移量。此时，我们想要创建一个监控 hello 调用情况的 uprobe ，我们不妨就把断点放在 hello 的入口处吧，我们需要计算 hello 的入口处指令在文件中的偏移量。我们已经掌握的是：hello 函数的虚拟地址、hello 所在的 .text 段的虚拟地址、.text 段在文件中的偏移量。那这就是一个小学数学题了。hello 的文件中偏移 = hello 的虚拟地址 - .text 段的虚拟地址 + .text 段在文件中的偏移 0x1744 - 0x16ac + 0x6ac = 0x7440x744 就是那个我们该用来注册 uprobe 的文件中偏移量。当然，更简单的方法也不是没有，像 ida 之类的逆向软件，就会直接把文件内偏移量显示在底下：噢对了，回调函数还有个返回值，如果不想进行什么骚操作（比如触发了这个 uprobe 就移除它之类的），返回 0 即可。注释：(1) 对于这种代码，编译器很容易把这个函数 inline 优化掉，所以记得加上 -O0 。此外，如果编译采用的是 Android NDK ，那么它会自动 strip 掉符号表，导致 readelf -s 一片空白。解决方案是使用 obj/ 下生成的中间产物，而非 libs/ 下的最终产物。帧生成时间数据源这是一个相当头疼的点，上面我有写到 surfaceflinger 也许可以作为一个入手点，但是要从中区分出不同应用、区分出哪次 queuebuffer() 对应哪个 vsync 的难度极大。而且更关键的是编译完的 surfaceflinger 不会携带调试符号表，再加上它是 c++ 写的，逆向起来简直就是依托答辩：即使这次定位到了目标方法，下一次、换一个人、换台设备可能就只能以失败告终。我突然想起了以前有个玄学叫做 “GPU 呈现模式分析” ：这玩意儿的数据源是什么？图表又是由谁画出来的？如果数据源在系统服务，那它是怎么搜集数据的？如果数据源在应用本身，那它是怎么把这些数据发给系统服务来画图的？简单看了看代码，我发现我一个都没猜对，真实的情况是：应用自己搜集数据，应用自己画图。也就是说，这个图并不是系统叠加上去了什么东西，而是应用在绘制完界面后又主动绘制了这幅图，从搜集数据到画图的过程中不存在任何的 IPC 。那，为什么我一个开关就能控制一堆应用开始自己画图，这不是很奇怪吗？在这背后，“GPU 呈现模式分析”的开关控制的是 debug.hwui.profile 这个 prop ，当这个 prop 的值为 visual_bars 时，各个应用就会开始主动画图了。那，为什么应用们会千篇一律的监听这个 prop ，会自主搜集数据，还会画样式统一的图表呢？难道应用开发者们都提前约好了吗？并没有约好，只是因为一般的 Android 应用都是基于平台 api 所提供的组件（比如 View ）开发，而这些组件的硬件加速渲染过程会调用到系统自带的 libhwui 库，我们所熟知的 skiavk、skiagl 就是 libhwui 中渲染管线的一部分。事实上，上面监听 prop 和画图的过程本身就被做在 libhwui 当中，而数据搜集的过程是由预埋在平台 api 中的“时间采样点”完成的。所以简单回答上面的问题：虽然大家的应用会一起主动的搜集数据和画图，但这不是因为大家都写了搜集数据和画图的代码，而是因为大家使用的平台 api 本身就包含了这些东西。并不是所有的应用都有这种默契，比如浏览器类和游戏类应用，它们往往有自己的渲染引擎，不会使用 libhwui 所提供的渲染管线。对于这些应用，我们就没法使用这种方法来获取“帧生成时间”了。至此，我们明确了一般的应用其实都会默默搜集渲染相关性能数据的事实，这些数据原本是为 “GPU 呈现模式分析” 服务的，但是，现在轮到它们来服务我们了。数据的搜集过程接下来展开讲一讲平台 api 自带的东西是如何进行数据搜集的。在这之前，得先了解一下应用是如何对外界做出响应的。响应过程主要牵扯到两个线程，一个是大名鼎鼎的 UI 线程，另一个则是对应用开发者相对透明的渲染线程（RenderThread）。surfaceflinger 会向应用程序发出 vsync-app 信号，应用在收到这一信号后即要进行下列的工作：(UI 线程) 处理输入事件（如屏幕滑动）（外加使得下一步中牵扯到的组件发生改变，于是就响应起来了）。(UI 线程) 遍历 View 树，调用相关组件的 draw() ，并记录产生的绘制调用（比如“在(0,0)画个圆”）。(UI 线程) 为正在进行的动画记录所需的绘制调用。(UI 线程) ......(UI 线程) 递交所记录的绘制调用给渲染线程。(渲染线程) 整理、优化绘制调用。(渲染线程) 递交绘制调用给 GPU 。(渲染线程) 递交 Graphic Buffer 给 surfaceflinger(1)。需要注意的是，上面的这一整套过程都已经被包含在了平台 api 中，需要应用开发者开发的，往往只是这个过程中牵扯到的一些回调，比如 onTouchEvent() 和 onDraw() 。总的来说，UI 线程负责产生绘制调用，渲染线程负责递交绘制调用给 GPU 。上面这一整套过程需要在一帧（甚至更短）的时间内完成(2)，否则就会造成跳帧和卡顿。我们常说不要在 UI 线程上进行耗时操作，从上面的过程中可以看到，在 UI 线程上进行耗时操作会耽误绘制调用的产生，从而使上面的一整套流程难以在一帧的时间内跑完。众所周知，普通的 Android 应用可以由 java 和 native 部分组成，平台 api 也是如此，我们可以将上面的这套流程分为 java 和 native 部分：(java 层) 处理输入事件（如屏幕滑动）（外加使得下一步中牵扯到的组件发生改变，于是就响应起来了）。(java 层) 遍历 View 树，调用相关组件的 draw() ，并记录产生的绘制调用（比如“在(0,0)画个圆”）。(java 层) 为正在进行的动画记录所需的绘制调用。(java 层) ......(native 层) 递交所记录的绘制调用给渲染线程。(native 层) 整理、优化绘制调用。(native 层) 递交绘制调用给 GPU 。(native 层) 递交 Graphic Buffer 给 surfaceflinger(1)。总的来说，java 层的部分用来为应用开发者提供方便的开发接口，native 层则是渲染引擎的主场，这里的 native 部分主要就由 libhwui 负责。接下来开始正题，现在我们已经了解了应用从收到 vsync 信号到产生帧的大致流程，为了追踪这一整套流程的性能，Android 在平台 api 中埋入了许多“时间采样点”。以 java 层为例：frameworks/base/core/java/android/view/Choreographer.javavoid doFrame(long frameTimeNanos, int frame, DisplayEventReceiver.VsyncEventData vsyncEventData) { ...... ...... ...... mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos, vsyncEventData.preferredFrameTimeline().vsyncId, vsyncEventData.preferredFrameTimeline().deadline, startNanos, vsyncEventData.frameInterval); ...... ...... mFrameInfo.markInputHandlingStart(); ...... mFrameInfo.markAnimationsStart(); ...... mFrameInfo.markPerformTraversalsStart(); ...... ...... }这些“时间采样点”会记录 vsync 信号开始的时间以及那些关键且耗时的流程的发生时间，从而为性能追踪提供依据。同样的，native 层的 libhwui 渲染库中也有类似的采样点，这里我就只展示头文件中的采样方法了：frameworks/base/libs/hwui/FrameInfo.hvoid markSyncStart() { set(FrameInfoIndex::SyncStart) = systemTime(SYSTEM_TIME_MONOTONIC); } void markIssueDrawCommandsStart() { set(FrameInfoIndex::IssueDrawCommandsStart) = systemTime(SYSTEM_TIME_MONOTONIC); } void markSwapBuffers() { set(FrameInfoIndex::SwapBuffers) = systemTime(SYSTEM_TIME_MONOTONIC); } void markSwapBuffersCompleted() { set(FrameInfoIndex::SwapBuffersCompleted) = systemTime(SYSTEM_TIME_MONOTONIC); } void markFrameCompleted() { set(FrameInfoIndex::FrameCompleted) = systemTime(SYSTEM_TIME_MONOTONIC); }这些方法会在渲染管线的关键位置被调用，记录下对应的时间，并为后续的性能追踪提供依据。注释：(1) 此时 GPU 渲染不一定完成了，这里牵扯到 fence ，内容非常复杂，故不再展开了。(2) 完成并不一定意味着要产生帧，也不一定需要递交 surfaceflinger ，比如你的应用就是一个静态界面时。这里强调的是没有及时完成一定会跳帧。数据利用接下来，就该研究一下该如何利用这些本属于“GPU 呈现模式分析”的数据了。我们已经知道了，画那幅玄学分析图的逻辑是被做在 native 层 libhwui 的渲染管线中的，而数据采集过程则又同时发生在 java 层和 native 层中。这个过程中有什么弱点吗？或者说，有什么地方比较适合 uprobe 插入断点指令吗？在 java 层，显然是没有的，这东西牵扯到虚拟机和 JIT ，依托答辩难以下手。纯的 native 层，好像也没有，它压根就没有保留符号表，找一个函数都费劲。但是，在 java 层搜集的数据总需要被传递到 native 层吧？毕竟用这些数据画图可发生在 native 层。诶，这个传递过程总是固定的吧？毕竟 jni call 可是需要固定的符号的。这就是一个比较简单的下手点。先来看看 java 层是怎么把数据传递到 native 层的：frameworks/base/graphics/java/android/graphics/HardwareRenderer.java/** * Syncs the RenderNode tree to the render thread and requests a frame to be drawn. * * @hide */ @SyncAndDrawResult public int syncAndDrawFrame(@NonNull FrameInfo frameInfo) { return nSyncAndDrawFrame(mNativeProxy, frameInfo.frameInfo, frameInfo.frameInfo.length); }这个方法，便是把刚刚采集的数据塞进 native 层的通道。其中，frameInfo.frameInfo 是一个数组，数组的不同下标代表了不同的数据（比如各个阶段的开始时间），具体定义可参见 FrameInfo.java 。它调用的 nSyncAndDrawFrame() 是一个 native 方法，不过这个方法并没有作为符号导出，而采用的是动态注册：frameworks/base/libs/hwui/jni/android_graphics_HardwareRenderer.cppstatic const JNINativeMethod gMethods[] = { ...... {"nSyncAndDrawFrame", "(J[JI)I", (void*)android_view_ThreadedRenderer_syncAndDrawFrame}, ...... }; ...... int register_android_view_ThreadedRenderer(JNIEnv* env) { ...... return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods)); }它实际被绑定到的 native 方法是 android_view_ThreadedRenderer_syncAndDrawFrame 。我们来看看它：frameworks/base/libs/hwui/jni/android_graphics_HardwareRenderer.cppstatic int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz, jlong proxyPtr, jlongArray frameInfo, jint frameInfoSize) { LOG_ALWAYS_FATAL_IF(frameInfoSize != UI_THREAD_FRAME_INFO_SIZE, "Mismatched size expectations, given %d expected %zu", frameInfoSize, UI_THREAD_FRAME_INFO_SIZE); RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr); env->GetLongArrayRegion(frameInfo, 0, frameInfoSize, proxy->frameInfo()); return proxy->syncAndDrawFrame(); }java 层传进来的数组到了 native 层变成了一个 handle ，需要通过调用虚拟机的对应方法来取得数组元素，比如 env->GetLongArrayRegion() 就把数组元素全部 copy 到了 proxy->frameInfo() 。到了这里，应该能够意识到我们所在的 android_view_ThreadedRenderer_syncAndDrawFrame 是一个非常适合插入 uprobe 断点的方法。这个方法本身虽然没被作为符号导出，但是注册它时使用的 nSyncAndDrawFrame 是一个常量，那么顺藤摸瓜的找出这个方法的地址应该不成问题。再看看这个方法的内容，在调用完 env->GetLongArrayRegion() 后，proxy->frameInfo() 不就存储着我们想要的帧数据吗？那可太棒了，这个方法又好找，又短，又能够方便的获取目标地址，我想应该很难找到一个更完美的 uprobe 对象了。断点选择接下来，拿起逆向工具，打开 /system/lib64/libhwui.so ，来确定用于 uprobe 的 offset ！这里以 ida 为例，我们可以直接搜索关键的 nSyncAndDrawFrame ，并找到使用它的地方（注意，不是定义它的地方）：这里，便是我们在上面看到的 gMethods 注册表，而标黄的地方便是目标 android_view_ThreadedRenderer_syncAndDrawFrame 的指针（ida 给它起了个额外的名字）。双击它，我们可以跳转到 android_view_ThreadedRenderer_syncAndDrawFrame 开始的地方：（如果你的图是流程图形式，可以按空格切换到这种代码形式）接下来，我们把汇编对应到上面的原始代码，整理一下这个函数的过程。首先是红色括号，它所对应的原始代码是 LOG_ALWAYS_FATAL_IF() ，做的是比较和跳转执行，我们不需要管它。接下来的蓝色括号则是关键部分，它负责为 GetLongArrayRegion() 准备参数。绿色箭头所指的指令，便是跳转执行 GetLongArrayRegion() 的地方。橙色部分下面再说。接下来简单介绍一下 fastcall 函数调用约定：它约定了调用函数时，采用寄存器而不是栈来传递参数（除非参数太多）。这里的函数调用采用的就是 fastcall 。在 arm64 上，整型参数会被塞进 X 寄存器，比如 this 指针会被塞进 X0 ，第一个参数会被塞进 X1 ，第二个参数被塞进 X2 ，依此类推（前提是参数不要太多）。可以看到，用来存储帧数据的目标地址 proxy->frameInfo() 是第四个参数，于是在准备参数时，我们最最想要的目标地址将会被放在 X4 寄存器中。粉色箭头所指向的指令，便把目标地址塞进了 X4 寄存器。那么，是不是在绿色箭头所指向的指令执行完成后，我们对 X4 寄存器中存储的地址取值，就能够获得帧数据的第一个元素呢？不对，因为执行绿色箭头所指向的指令，会跳转执行另一个函数，在那个函数中，X4 寄存器的值可能早就被覆盖掉了。这就意味着，我们需要两个 uprobe 断点才能读取到帧数据。第一个断点，应该被放在 X4 寄存器赋值之后，即标号为 1 的橙色箭头位置。我们将在此处借助 uprobe 保存 X4 寄存器的值，从而取得存储帧数据的地址。第二个断点，应该被放在 “跳转执行” 指令之后，即标号为 2 的橙色箭头位置。此时帧数据刚刚被保存到对应地址中，我们需要借助 uprobe 把帧数据取出来。于是，帧生成时间有了，我们就可以进行任何想要的操作了。你可能会问，为什么是“之后”，而不是就放在那条指令上？那，可以回上面阅读一下 uprobe 的时序。造好的轮子https://github.com/libxzr/kernel-playground/commits/playground/hwui_mon我已经把从操作 uprobe 到计算帧时间的相关逻辑造成了一个轮子，基于 4.19 内核。你所需要做的，仅仅只是把所需的断点位置以及目标寄存器号填进下面这张表里：static struct { ...... } hwui_info[] = { // Here, record info to support different versions of libhwui.so. #define INFO_COUNT 1 { .checksum = "dea5f6def7787d5c89fe66e7f888455953fd8880", .inject1_offset = 0x26868C, .reg = 4, .inject2_offset = 0x2686A0 } };checksum 代表这个 libhwui.so 的 sha1 值，不同机型、不同系统版本的断点位置是很有可能不同的。inject1_offset 代表上面“标号为 1 的橙色箭头”所指指令的文件内偏移。reg 代表那个用来存储目标地址的寄存器号。inject2_offset 代表上面“标号为 2 的橙色箭头”所指指令的文件内偏移。你大可为不同的系统版本增加不同的信息，记得把上面的 INFO_COUNT 一起改了就行。这些信息会根据当前 libhwui.so 的 sha1 值进行自动匹配。接下来讲一讲使用。上面的链接里已经提供了一个 demo：#include <linux/hwui_mon.h> #include <linux/module.h> static void handler(unsigned int ui_frame_time) { pr_info("Detect jank in %s with frametime = %d", current->comm, ui_frame_time); } static struct hwui_mon_receiver receiver = { .jank_frame_time = 4000, .jank_callback = handler }; static int __init demo_init(void) { return register_hwui_mon(&receiver); } module_init(demo_init);注册时需要指定“卡顿时间” jank_frame_time ，只有在“帧生成时间”大于等于“卡顿时间”时，jank_callback 才会被调用。需要注意的是 jank_callback 运行在应用的 UI 线程上，因此一定不要在其中进行耗时操作，如果有耗时操作请 offload 到 kthreads or workqueues 。另外，由于它运行在应用进程中，所以你可以使用 current->comm 打印出当前应用的（部分）包名，这对于分应用处理也许会有一些帮助。帧生成时间？聪明的你也许已经发现了，我们“帧生成时间”的数据源来自应用进程中 java 层向 native 层传递的数据，这意味着我们得到的所谓“帧生成时间”只是应用在 java 层的耗时，并不完整。没错，在我们的“帧生成时间”之上，还得加上 native 层渲染引擎的耗时和 GPU 的渲染耗时，才算得上是真正意义上的“帧生成时间”。但是，不同的应用之间采用的都是相同的 libhwui 库啊，大家跑的 native 渲染引擎代码逻辑是几乎相同的。只要给的绘制调用不是太阴间，native 渲染引擎耗时和 GPU 耗时大家都大差不差。确实，卡顿主要发生在 java 层的垃圾代码中，这种部分的“帧生成时间”是能够确切反应应用的性能需求的。但不管怎么样，这都是在找理由，要是完整的“帧生成时间”能轻松拿到，谁还用这个啊（笑虽然是个妥协，但效果也还不错吧。因此，你得记住，选择 jank_frame_time 时得为渲染引擎和 GPU 留足余量。比如对 120hz 来说，选择如 4000 us 这样更小的值，而不要卡在 8333 us ，后者即使是掉帧了不一定能够检测出来。后记算是比较系统的讲述了一下在内核侧提取帧生成时间的原理以及设计思路，期待你能够在自己的设备上复现上述操作(●'◡'●)另外，如此获取“帧生成时间”的思路应该还可以通过 Zygisk 实现，只是需要解决一下应用进程与帧时间消费者之间的通信问题（直接用 logcat 也不是不行 xD ）↑ 要是哪天有人做出来了记得给我个 credit 👀@shadow3 的一个 zygisk 实现参考资料https://wizzie.top/Blog/2021/04/14/2021/210414_android_VsyncStudy/https://www.cnblogs.com/roger-yu/p/16167404.htmlhttps://www.androidperformance.com/2019/12/01/Android-Systrace-Vsynchttps://medium.com/@workingkills/understanding-the-renderthread-4dc17bcaf979https://docs.kernel.org/trace/uprobetracer.htmlhttps://jayce.github.io/public/posts/trace/user-space-probes/https://gist.github.com/fntlnz/96a6d7bdd9881420c28e6454b8f7b91e
前言我手里有两台一加 8250 平台的机子，一台小 8 ，一台 8T 。照理说这两台机子的硬件上的区别应该不大，但是这台 8T 从出厂系统开始，我就没有成功获得过一次 pstore ，warm reset 之后开机看 pmic 的日志还是 cold boot，非常的诡异。相反，这台小 8 的 pstore 则一直工作正常。我甚至非常怀疑这台 8T 的 pmic 固件是不是有什么猫病。无奈，小 8 是主力机，没法天天拿来调试。于是糊了个在 panic 时转储日志到本地文件 / 指定分区的方案，也算是对 pstore 的一个小替代吧。源码https://github.com/libxzr/kernel-playground/commits/playground/logstore是基于高通 4.19 平台的，别的平台可能需要移植一下。细节单纯转储日志还是很简单的，利用 kmsg_dump 相关的 api 把日志从 buffer 里拿出来，再转储到文件里即可。这里的关键点是在于，该在何时转储日志。panic 的流程大概可以概括为以下几个阶段：进入 panic() 函数。在本 CPU 上禁用中断与抢占。打印 panic 信息与（本 CPU 的）调用栈。关闭其它 CPU。调用各个模块注册的 panic notifier。调用各个模块注册的 kmsg dumper。准备重启 / halt 住。在越是后面的阶段转储日志，得到的日志越是详细。比如在回调各个模块的 panic notifier 和 kmsg dumper 时，它们就很有可能往日志中增加额外的内容。我们所熟悉的 pstore 就是注册在 kmsg dumper 中的。但是，这一套流程中有一个对转储影响巨大的因素，那就是“关闭其它 CPU”（smp_send_stop()）。这里的关闭可不是温柔的 cpu_down()，而是直接发送 IPI 强行下线其它的处理器（ARM64）。要知道，对于正常的热插拔流程，在关闭 CPU 之前至少要把运行在它上面的任务迁移走，还要通知中断控制器不再给这个 CPU 分发中断。但是，panic 时真的是什么都没做就把 CPU 强行下线了，所有上面没有来得及迁移的东西都直接被毁灭了。这一操作其实是导致 panic 时设备直接 freeze 住的直接原因——别的 CPU 啥也不能执行，当前 CPU 被独占只能执行 panic 代码，自然而然设备看起来就像死机了一样。对于 pstore ，这一操作影响不大，反正它只需要写入指定的内存区域罢了。但是，对于我们这种正儿八经的日志转储，这种操作可能就是毁灭性的。就不说 vfs 了，光光是最底层的存储器驱动就可能依赖于一堆中断和 kthread / queue_work ，这些原本可能跑在别的 CPU 上的东西都被摧毁了，它该怎么转储日志？所以呐，想要和 pstore 在同一阶段转储日志已经变得非常不靠谱。对于我们这种非常复杂的转储方式来说，最好的选择就是在 down 掉别的 CPU 之前。此时我们虽然不能得到完整的 panic 日志（比如别的 CPU 的调用栈），但是对于得到 panic 的原因以及本 CPU 的调用栈已经非常足够了。此时我们得到的一个典型日志是长这样的：[ 19.122168] Unable to handle kernel NULL pointer dereference at virtual address 0000000000000000 [ 19.122175] Mem abort info: [ 19.122177] ESR = 0x96000046 [ 19.122179] Exception class = DABT (current EL), IL = 32 bits [ 19.122181] SET = 0, FnV = 0 [ 19.122182] EA = 0, S1PTW = 0 [ 19.122183] Data abort info: [ 19.122185] ISV = 0, ISS = 0x00000046 [ 19.122187] CM = 0, WnR = 1 [ 19.122189] user pgtable: 4k pages, 39-bit VAs, pgdp = 000000003746e1fd [ 19.122191] [0000000000000000] pgd=000000015722b003, pud=000000015722b003, pmd=0000000000000000 [ 19.122195] Internal error: Oops: 96000046 [#1] PREEMPT SMP [ 19.122198] Process ScreenDecoratio (pid: 4483, stack limit = 0x00000000b107786d) [ 19.122202] CPU: 4 PID: 4483 Comm: ScreenDecoratio Tainted: G S W 4.19.157-Horizon-03261204 #3 [ 19.122204] Hardware name: Qualcomm Technologies, Inc. kona MTP 19805 (DT) [ 19.122206] pstate: 40400005 (nZcv daif +PAN -UAO) [ 19.122214] pc : hwui_inject1_handler+0x10/0x24 [ 19.122219] lr : uprobe_notify_resume+0x530/0xca4 [ 19.122220] pc : ffffffa33a4b42b8 [ 19.122221] lr : ffffffa339212e3c [ 19.122223] sp : ffffff801e6dbc50 [ 19.122224] x29: ffffff801e6dbcb0 x28: 0000000000000080 [ 19.122227] x27: 00000000004000c0 x26: ffffffa33c2b52a8 [ 19.122229] x25: 0000000000000001 x24: 0000000000000000 [ 19.122231] x23: ffffffd3d5f00d18 x22: ffffffd3d5f00d00 [ 19.122233] x21: ffffffd3d5f00dc0 x20: ffffffd40d5b0000 [ 19.122234] x19: ffffff801e6dbec0 x18: 0000000000000000 [ 19.122236] x17: 0000000000000000 x16: 0000000000000000 [ 19.122238] x15: 0000000000000000 x14: 0000003103bac000 [ 19.122240] x13: ffffffa33bf6b9a0 x12: 16157c9af96779a3 [ 19.122242] x11: e9895543204eefdc x10: e9ea834ef4b11023 [ 19.122244] x9 : ffffffa33a4b42a8 x8 : 0000000000000000 [ 19.122246] x7 : 0000000000000000 x6 : 000000000000003f [ 19.122247] x5 : 0000000000000040 x4 : 0000000000000000 [ 19.122249] x3 : 0000000000000004 x2 : 0000000000000000 [ 19.122251] x1 : ffffff801e6dbec0 x0 : 0000000000000000 [ 19.122253] Call trace: [ 19.122256] hwui_inject1_handler+0x10/0x24 [ 19.122260] do_notify_resume+0xe0/0x23cc [ 19.122262] work_pending+0x8/0x10 [ 19.122264] Code: aa1f03e8 90000009 910aa129 2a1f03e0 (f9000109) [ 19.122267] ---[ end trace cb552490e52b4757 ]--- [ 19.126648] binder: context.name[binder] tr.size:152 service:qti.radio.extphone [ 19.126649] bt_configure_gpios: BTON:Turn Bt Off bt-reset-gpio(1121) value(0) [ 19.126710] Kernel panic - not syncing: Fatal exception [ 19.126735] save_dump_reason_to_smem: info : Fatal exception [ 19.126739] save_dump_reason_to_smem: dump_reason : Fatal exception reason_len=15 function caused panic :die name_len=3 [ 19.126770] bt_configure_gpios: BTON:Turn Bt Off [ 19.126777] bt_configure_gpios: bt-sw-ctrl-gpio(1224) value(1) [ 19.126780] bt_enable_bt_reset_gpios_safely: bt_enable_bt_reset_gpios_safely: BTON: Asserting BT_EN [ 19.138935] oplus_binder_stats binder_stats_driver_ioctl: unknown ioctl cmd:120 [ 19.139622] oplus_binder_stats binder_stats_driver_ioctl: unknown ioctl cmd:120其实还不错。这种转储方式有一个非常重要的前提假设是：panic 既不是 vfs 相关东西导致的，也不是存储驱动导致的。不然真就直接升天了。而且这种转储方式并不适用于启动早期造成的 panic ，此时相关的文件系统可能根本就还没挂载好，想要转储日志也是无稽之谈。还有一个点，也许我们真的需要区分一下什么才是 panic 。我所认为的 panic 必然是需要调用到 panic() 函数的，就比如上面的空指针异常，是由缺页中断的相关处理函数经过一系列判断后主动调用的 panic() ，这整个调用过程都是稳定可控的，全程代码并没有“跑飞”，也没有发生死锁。但是还有一种情况，那就是代码“跑飞”或死锁了，系统整个死掉了，panic() 函数并没有被调用。此时展现出的状况也是死机重启，但是这时的重启往往是由 watchdog 触发的，而非在 panic() 函数中调用的重启方法。这种情况下，很显然日志是不会转储的，甚至 pstore 也大概率不会生效。有一种测试代码是否“跑飞”的方法是把 CONFIG_PANIC_TIMEOUT 设为 0 。此时系统如果 panic ，那么将会一直 freeze 住不会重启（因为 panic 函数会主动喂狗），而如果是代码“跑飞”了，那么 watchdog 照样会按时触发系统重启。为了方便测试，有一种触发典型 panic 的方法是 echo c > /proc/sysrq-trigger ，它依赖于 CONFIG_MAGIC_SYSRQ 。就到这里吧 88。
前言最近有个非常奇怪的需求，我想让一段代码只被运行一次，但是又不想借助内核自带的 init 系列宏，因为这段代码的运行需要特殊的“上下文”环境。单纯实现这样的操作很简单，随便弄个布尔变量把对应的代码包住就好了：// 外层方法可能被多次调用 void foo(void) { static bool done = false; if (!done) { done = true; // 在这里运行你只想要运行一次的东西 run_single_time(); } } void run_single_time(void) { }（别被放在方法里的 static bool 迷惑了，它不是一个局部变量，而是一个 static local variable ，更像一个全局变量）但是，这种操作并不是 smp-safe 的。想象一下：假如两个 CPU 同时运行这段代码，那么 !done 是不是可能同时在两个 CPU 上成立，于是 run_single_time() 就可能在不同的 CPU 上被调用两次。更何况，即使两个 CPU 先后运行了这段代码，缓存一致性问题也可能导致目标代码被运行两次。很明显，这不是设计目标。传统的实现正常来说，很容易想到 run_single_time() 其实是一个临界区，因为无论如何都只能有一个 CPU 在运行其中的代码。于是，顺理成章的就会想到使用 spin lock 来保护整个临界区：DEFINE_SPINLOCK(foo_lock); // 外层方法可能被多次调用 void foo(void) { static bool done = false; spin_lock(&foo_lock); if (!done) { done = true; // 在这里运行你只想要运行一次的东西 run_single_time(); } spin_unlock(&foo_lock); } void run_single_time(void) { }借助 spin lock ，我们在解决并发问题的同时还利用其自带的内存屏障顺带解决了缓存一致性问题。但是，只是为了保护这一个变量而使用 spin lock ，开销是否会太高了呢？。所以，有没有什么 lock-free 的实现方式呢？借助 atomic说到 lock-free 自然而然的就会想到 atomic operations 。atomic operations 即原子操作，在执行原子操作时没有其它线程能够修改共享资源，从而避开了上面所描述的竞争问题。现代处理器往往会提供专门的指令来实现原子操作，这也就意味着对于上面的那种单变量情况，原子操作比 spin lock 具有更高的效率。以 ARM 为例，其提供了如 LDREX、STREX 指令来专门实现原子操作，在 ARMv8.1+ 指令集上更是提供了可选的 LSE 系列指令，进一步提高原子操作的性能。比如我手里的骁龙 865 平台，就是支持 LSE 指令的，其原子操作也会采用 LSE 指令进行实现：那么，该如何借助 atomic 实现“单次运行”呢？很显然，像这样的简单替换成 atomic 是不行的：// 外层方法可能被多次调用 void foo(void) { static atomic_t done = ATOMIC_INIT(0); if (!atomic_read(&done)) { atomic_set(&done, 1); // 在这里运行你只想要运行一次的东西 run_single_time(); } } void run_single_time(void) { }这样能够解决的只有缓存一致性问题，多个 CPU 仍然可能同时执行完 atomic_read() ，然后同时进入临界区。只有确保 read + set 的整个过程都是原子的，才能对临界区真正的起到保护作用。这时，我们就需要使用 atomic api 提供的组合操作：// 外层方法可能被多次调用 void foo(void) { static atomic_t done = ATOMIC_INIT(0); // 它返回的是 increment 之后的值 if (atomic_inc_return(&done) == 1) { // 在这里运行你只想要运行一次的东西 run_single_time(); } } void run_single_time(void) { }在这里，我们使用 atomic_inc_return() 来原子的实现 +1 与读取，这整个过程都是不可打断的，因此上面所说的“同时进入临界区”的问题也将不复存在——即，在某个 CPU 上，要么进行了 +1 和读取，要么完全没有进行 +1 和读取，不存在上面的那种读取了却没有写入的状态。你可能会怀疑，假如 foo() 被频繁执行，是否存在溢出的风险？嗯，这种风险确实是存在的，不过我们还可以使用 atomic_fetch_add_unless() api。// 外层方法可能被多次调用 void foo(void) { static atomic_t done = ATOMIC_INIT(0); // 它返回的是 add 之前的值 if (!atomic_fetch_add_unless(&done, 1, 1)) { // 在这里运行你只想要运行一次的东西 run_single_time(); } } void run_single_time(void) { }这样，当 done 的值达到 1 时，add 操作将不再进行，溢出也就被解决掉了。事实上，在现代处理器上，读/写指令往往本身就已经是原子的了，它们可以在一个时钟周期内完成，是不可再拆分的。因此，如果你使用了 atomic api ，但是只使用了 atomic_read() 和 atomic_write() 却没有使用任何组合操作，那就该怀疑一下在你的用例下是否真的需要 atomic 了，或许 READ_ONCE() 和 WRITE_ONCE() 反倒是更好的选择？这么多奇怪的组合 atomic 操作，都是怎么实现的？在不同的体系结构下，它们的实现方式可能是不一样的。以 atomic_fetch_add_unless 为例，ARMv7 就有自己采用汇编进行的实现：arch/arm/include/asm/atomic.hstatic inline int atomic_fetch_add_unless(atomic_t *v, int a, int u) { int oldval, newval; unsigned long tmp; smp_mb(); prefetchw(&v->counter); __asm__ __volatile__ ("@ atomic_add_unless\n" "1: ldrex %0, [%4]\n" " teq %0, %5\n" " beq 2f\n" " add %1, %0, %6\n" " strex %2, %1, [%4]\n" " teq %2, #0\n" " bne 1b\n" "2:" : "=&r" (oldval), "=&r" (newval), "=&r" (tmp), "+Qo" (v->counter) : "r" (&v->counter), "r" (u), "r" (a) : "cc"); if (oldval != u) smp_mb(); return oldval; } #define atomic_fetch_add_unless atomic_fetch_add_unless而对于 ARM64 ，我没有找到相关的实现，于是它大概率会 fallback 到内核自带的一份实现：include/linux/atomic.h/** * atomic_fetch_add_unless - add unless the number is already a given value * @v: pointer of type atomic_t * @a: the amount to add to v... * @u: ...unless v is equal to u. * * Atomically adds @a to @v, if @v was not already @u. * Returns the original value of @v. */ #ifndef atomic_fetch_add_unless static inline int atomic_fetch_add_unless(atomic_t *v, int a, int u) { int c = atomic_read(v); do { if (unlikely(c == u)) break; } while (!atomic_try_cmpxchg(v, &c, c + a)); return c; } #endif这自带的实现其实挺有意思的，它基于经典的 compare-and-swap (CAS) 思想，这里就不展开了。atomic 比上锁更高效？在上面，我提及了 这也就意味着对于上面的那种单变量情况，原子操作比 spin lock 具有更高的效率 。我把范围限制在了“单变量情况”，因为 atomic 虽然有硬件指令实现，但并不是没有争用的。以 ARM64 为例，在遭遇对共享内存区域的争用时，atomic store 指令会失败且需要重试，这种重试本质上就是一种忙等待，和 spin lock 原地自旋并没有本质上的区别。而且，为了保证缓存一致性，每执行一次 atomic 操作都需要刷新缓存，这也会造成额外的性能开销。想象一下这样一种情况：你有一百个 int 需要保护，这一百个 int 在同一个函数中被不同线程读写。此时你可以选择将这一百个 int 全部转为 atomic_t ，你也可以选择使用一个 spin lock 来保护它们全部。很显然，使用一个 spin lock 保护住这一个函数会更加高效，因为采用 atomic_t 反而会带来百倍的征用量，还需要反复刷新缓存。由此可见，该如何在 atomic 和 spin lock 之间进行选择，完全取决于你的上下文环境，lock-free 不一定总是带来性能提升。参考资料https://stackoverflow.com/questions/75597629/is-lock-free-synchronization-always-superior-to-synchronization-using-lockshttps://zhuanlan.zhihu.com/p/89299392
前言这篇文章，打算比较系统的梳理一下内核模块的问题，顺便聊一聊 GKI 。对于 Android 平台，在 GKI 诞生之前（ 5.4 以下内核版本），我们对于内核模块的认知往往是：内核模块必须和内核一起编译才能被正确加载。从结果来看，这种基于经验得出的结论其实没错。但是从过程来看，这句话的成立其实少不了一些机缘巧合。现在，就让我们重新梳理一下这个问题，看看这个过程中到底牵扯到了什么。洞见模块签名之所以第一个就聊模块签名，是因为它就是使我们得出上面那个结论的直接原因。在非 GKI 的高通设备内核上，以下几个 config 选项默认呈打开状态：CONFIG_MODULE_SIG=y CONFIG_MODULE_SIG_FORCE=y CONFIG_MODULE_SIG_SHA512=y其中，第一个选项 CONFIG_MODULE_SIG 打开了对模块签名的支持，它至少会做以下事情：生成密钥对。若 CONFIG_MODULE_SIG_KEY 选项的内容没有被特别指定(1)（默认行为），则在编译内核时生成签名所需的密钥对。保存公钥。将上面生成的密钥对或 CONFIG_MODULE_SIG_KEY 所特别指定密钥对的公钥保存在内核本体中(2)。启用校验。存储在内核本体中的公钥将被用于对模块的签名验证。但是，单纯开启 CONFIG_MODULE_SIG 并不会拒绝加载签名验证失败的模块，而只是会打印一下警告。第二个选项， CONFIG_MODULE_SIG_FORCE 则是对第一个选项进行了加强：拒绝加载任何未签名或者签名校验失败的模块。第三个选项，则是指定了一下模块签名使用的算法。可以发现，密钥对是在编译时生成的，而模块想要成功被加载，就必须使用与“内核本体中所保存公钥”相匹配的私钥进行签名，也就意味着签名的私钥必须来自这一编译时生成的密钥对。于是，上面的“内核模块必须和内核一起编译才能被正确加载”似乎就得到了印证。但这，其实并没有那么准确。在默认情况下，选项 CONFIG_MODULE_SIG_ALL 是没有被打开的，此时内核并不会自动为生成的模块进行签名。也就是属于：生成了密钥对，保存了公钥，做好了校验准备，但是把密钥对丢在 out 目录里就不管了，私钥你自己拿去玩吧。很显然，签名模块的过程是由编译系统的其它部分完成的——拿着刚刚编译内核时生成的私钥进行签名。这其实是强调了：想要通过签名验证，内核和模块也并不一定真的需要来自同一次编译，只要公私钥匹配就可以了，比如：重新 dirty build 了内核本体，但密钥对没有重新生成，那之前编译 + 签名的模块自然还可以通过签名验证。重新编译了内核模块，但是使用了上次编译内核时生成的私钥进行签名，那这次编译的模块也能在上次编译的内核上通过签名验证。对了，作为第三方开发者，在单独编译内核时如果你想生成带有签名的模块，别忘了打开 CONFIG_MODULE_SIG_ALL 选项，否则生成的模块是需要手动使用 scripts/sign-file 工具进行签名才能过验证的。注释：(1) CONFIG_MODULE_SIG_KEY 的默认值是 certs/signing_key.pem ，这并不是一个有效的签名文件而只是一个占位字符串，代表在编译时生成密钥对。(2) 其实还可以通过指定 CONFIG_SYSTEM_TRUSTED_KEYS 来额外保存一些别的公钥到内核中。匹配性检测上面那一坨签名相关的东西，主要保证的还是模块的来源可信，毕竟并不是所有人都会选择在编译时随机生成一个签名密钥对，那自然而然拿它作为模块与内核匹配性的参照是不够格的。匹配性的检测方式主要有两种——简单而优雅的 vermagic 和看似高级但实际上没那么靠谱的 modversions 。接下来，就来简单的分别介绍一下两种方式。vermagicvermagic 是一串字符串，而且很幸运的是它还是人类可读的，我们可以使用 modinfo 指令来查看一个模块的 vermagic：$ modinfo -F vermagic snd-soc-core.ko 6.1.1-zen1-1-zen SMP preempt mod_unloadvermagic 反映了内核的版本信息和部分特性。 vermagic 不仅会保存在内核本体中，也会保存在内核模块中。因此，只需要简单的比较内核本体和模块的 vermagic 值，即可得知其匹配情况。你可能会说：这玩意儿不就是手动维护一个版本号吗，那假如遇到 Linux 版本号相同但是自己打了点补丁导致接口发生变化的情况咋办？嗯，没错，就是手动维护版本号。在接口变化时，其实还有个在 Android 上没怎么用到的 EXTRAVERSION 也是可以修改的对象：# SPDX-License-Identifier: GPL-2.0 VERSION = 4 PATCHLEVEL = 19 SUBLEVEL = 157 EXTRAVERSION = NAME = "People's Front"这个 EXTRAVERSION 会 append 在主版本号的后面，比如 4.19.157-2 这样作为辅助，这也是大多数 Linux 发行版维护版本号的方式。所以，vermagic 的核心原理就是：在接口发生改变（或者干脆每次编译时）修改版本号，拒绝加载可能不匹配的模块。modversionsmodversions 有一个更加高级的理想：不看版本号，只看模块接口的匹配情况。那么这该如何实现呢？modversions 是这样做的：为内核中所有的模块接口计算校验值，并在模块中保存所有其调用的接口的校验值。于是，在加载模块时，只需要匹配“模块所期待的校验值”和“内核所提供的校验值”即可确定这个接口有没有被更改过。我们可以使用 modprobe --dump-modversions 指令来查看一个模块保存的所有校验值：$ modprobe --dump-modversions qca_cld3_qca6390.ko 0xfd03be2d module_layout 0x62d6013e PDE_DATA 0x47ce0010 ipa_get_wdi_stats ......接口校验值的计算由 scripts/genksyms 负责，校验值采用的是 CRC32 的形式，我们可以简单的“试用”一下它(1)：test.h#ifndef TEST_H #define TEST_H struct a { int a; long b; }; #endiftest.c#include "test.h" int test(struct a *a) { } EXPORT_SYMBOL(test);$ gcc -E test.c | genksyms __crc_test = 0x8ebc093d;于是，我们拿到了 test 方法的校验值：0x8ebc093d 。校验值的计算是有一系列的参照的，包括方法本身的名称、参数、返回值等等。我们可以加上 -d 选项看看详细的输出：$ gcc -E test.c | genksyms -d Defn for struct a == <struct a { int a ; long b ; } > Defn for type0 test == <int test ( s#a * ) > __crc_test = 0x8ebc093d; Hash table occupancy 2/4096 = 0.000488281可以看到，参数 struct a * 被识别并展开到了成员变量级别(2)，此时 struct a {}; 的结构体布局将会直接影响到最终的校验值，这也是维护接口稳定的一方面。当然，不是所有的结构体都需要被展开，比如上面的代码传入的仅仅只是 struct a 的指针，我们并不对其取值，那结构体的布局对接口稳定性就没有任何影响，于是我们可以将头文件中 struct a {}; 这样的具体声明改为 struct a; 这样的“前向声明”（forward declaration），此时接口校验值的计算将不会牵扯到 struct a 的成员，于是起到了扩大接口兼容性的效果。可以这样形象的理解一下：有一个接口 int test(struct a *) ，位于源文件 A 。struct a {}; 这样的具体声明放在头文件 B 中。struct a; 这样的前向声明放在头文件 C 中。A 并不关心 struct a 的具体细节，而且因为采用指针传递，也无需关心其具体细节，于是 A 引用了 C。这样，接口的兼容性实现了扩大化，修改 B 中的具体声明将不会对接口的校验值产生任何影响。该接口内部可能会调用别的源文件（D）中的函数进一步对 struct a * 进行处理，如果想要取得 struct a * 的具体内容，则 D 需要引用头文件 B，当然这对 A 是没有影响的，自然也不会影响到接口的校验值。假如 A 引用的头文件从 C 换成了 B ，即使结构体的具体声明没有被修改，那接口的校验值也会发生改变，这被称为 数据类型可见性变更 。这种采用指针 + 前向声明的方式进行数据可见性屏蔽的操作其实蛮常见的，不仅是内核。那么，为什么说 modversions 看似高级但实际上没那么靠谱呢？继续上面的“形象理解”。正常情况下，模块是只允许引用头文件 C 的，即不允许访问 struct a 的具体内容。但是假如我们在模块代码中引用了头文件 B ，然后对 struct a * 取值并修改呢？试想：在某次提交后，B 中的结构体具体声明发生了变化，由于 A 引用的是 C ，因此接口的校验值不受影响；而我们只重新编译了内核本体，却没有重新编译模块，模块被加载了，校验值匹配通过了，对 struct a * 一取值修改，boom (3)。此外，还有像是由编译器版本变化导致的 ABI 不匹配，modversions 压根就没办法捕获了吧。我想，难以处理这些奇怪的情况便是文档中 (hopefully) spot any changes 的原因吧。在 LWN.net 的文章 The end of modversions? 也有对该功能不靠谱性的提及：By "quite limited," he is referring to the fact that many changes will elude the modversions check. In particular, changes to a structure passed to a function will not be caught.注释：(1) 对于这种“试用”，EXPORT_SYMBOL 宏无需定义，它将直接被 genksyms 读取（即使真正的编译不一定能通过）。(2) struct 的声明必须放在头文件里，如果简单的放在源码文件的顶部是无法被 genksyms 识别并展开到成员的。(3) 不知道为什么，AOSP 的文档认为这种情况可以被 modversions 捕获 ，然而按照我自己的理解，这种情况是不行的。如果你对此有一些认知，欢迎在评论区讨论。实际应用在实际工作过程中，CONFIG_MODVERSIONS 选项决定了上述 modversions 匹配性检测方式的开启状况。在 CONFIG_MODVERSIONS 关闭的情况下，vermagic 会成为唯一的匹配性检测方法，此时内核将会完整比较 vermagic 字符串的全部内容，并拒绝加载 vermagic 字符串与内核本体不一致的模块。在 CONFIG_MODVERSIONS 开启的情况下，内核首先会比较 vermagic 字符串中的“特性段”（即版本号之后的部分，比如 SMP preempt mod_unload），在“特性段”匹配的情况下再去比较 modversions 生成的接口校验值。只有在“特性段”和接口校验值全都成功匹配的情况下，模块才会允许被加载。需要注意的是，modversions 本身也是“特性段”的内容之一：$ modinfo -F vermagic qca_cld3_qca6390.ko 4.19.157-perf+ SMP preempt mod_unload modversions aarch64因此，尝试加载 CONFIG_MODVERSIONS 选项开关状态不匹配的模块，无论如何都是不能够成功的（即，别想通过关闭 CONFIG_MODVERSIONS 选项来绕过 modversions 检查）。在高通设备内核上，CONFIG_MODVERSIONS 默认都呈打开状态，即会采用上述的第二种方式对模块匹配性进行检测。强制加载接下来聊一聊模块强制加载相关的问题。在原厂系统上，有许多的内核组件都被编译成了模块的形式。当这些模块无法被正确加载时，常常会发生：wifi 失效、音频失效、触摸失效...借助上面的知识，先来思考这样一个问题：既然高通设备内核默认都开着 CONFIG_MODVERSIONS ，那是不是意味着只要关闭 CONFIG_MODULE_SIG_FORCE ，原厂系统自带的模块就能够顺利加载了呢？按照上面我们的分析从理论上想想，好像确实是这样。但事实基本不会这么友好(1)：OnePlus8T:/vendor/lib/modules # modprobe -d . qca_cld3_qca6390.ko Loading module /vendor/lib/modules/qca_cld3_qca6390.ko with args '' modprobe: Failed to insmod '/vendor/lib/modules/qca_cld3_qca6390.ko' with args '': Exec format error modprobe: LoadWithAliases was unable to load qca_cld3_qca6390.ko modprobe: Failed to load module qca_cld3_qca6390.ko: Exec format error OnePlus8T:/vendor/lib/modules # dmesg ...... [ 99.950252] (6)[8263:modprobe]wlan: disagrees about version of symbol module_layout ......可以看到，模块的加载被拒绝了，原因是接口 module_layout 的校验值不匹配。这个 module_layout 并不是一个真正意义上的接口，它的存在仅仅只是为了通过复用接口校验机制的方式来确保模块相关的关键结构体没有改变：kernel/module.c#ifdef CONFIG_MODVERSIONS /* Generate the signature for all relevant module structures here. * If these change, we don't want to try to parse the module. */ void module_layout(struct module *mod, struct modversion_info *ver, struct kernel_param *kp, struct kernel_symbol *ks, struct tracepoint * const *tp) { } EXPORT_SYMBOL(module_layout); #endif即，校验值不匹配意味着：方法参数中牵扯到的这堆结构体存在布局上的变化。我很难知道具体的原因，因为这些结构体在层层展开之后会牵扯到很多很多的东西。但是从根本上说，原厂系统自带的模块无法被加载是因为厂商开源的代码和内部真正使用的代码压根不是同一份(2)。可能是因为内部代码有一些额外的修改，也有可能是因为内部代码额外开关了一些 config ，不管原因是什么，结果就是：某些结构体的布局发生变化并被 modversions 校验值所捕获，从而导致原厂模块被拒绝加载。面对这种情况，最好的解决方法永远是重新编译新的模块或者干脆把模块们编译进内核，因为接口不匹配的情况已经存在，模块要是真的被加载进去是有跑飞风险的。但是别忘了这段内容的主题是“强制加载”，那么，该如何强制加载这些不匹配的模块呢？内核提供了一个选项 CONFIG_MODULE_FORCE_LOAD 来启用模块强制加载的支持，但遗憾的是，打开这个选项并不意味着模块就会被强制加载，打开它仅仅只是允许了在执行 modprobe 指令时加上 -f / --force-modversion / --force-vermagic 之类的选项来忽略掉相关的校验。往深层了讲，它所做的事情是允许在执行 finit_module syscall 时通过在 flags 中标记指定的位来跳过校验。但更遗憾的是，为了绕过 GPL ，Android 并没有使用 module-init-tools 提供的 modprobe 指令，而是自己在 toolbox 中 rewrite 了一个 ，但是，这个 rewrite 的版本压根就不支持 -f 之类的强制加载选项。因此，想要走正常渠道利用 CONFIG_MODULE_FORCE_LOAD 看似是无缘了。天无绝人之路，想要强制加载模块你当然可以去编译适用于 Android 的 module-init-tools ，但是更简单的方法是直接在内核中修改上面所说的 flags ，以此达到与 modprobe -f 相同的效果：diff --git a/kernel/module.c b/kernel/module.c index 9dfc374bfeb4..743b8064364e 100644 --- a/kernel/module.c +++ b/kernel/module.c @@ -3704,6 +3704,9 @@ static int load_module(struct load_info *info, const char __user *uargs, long err = 0; char *after_dashes; + flags |= MODULE_INIT_IGNORE_MODVERSIONS; + flags |= MODULE_INIT_IGNORE_VERMAGIC; + err = elf_header_check(info); if (err) goto free_copy;想要它生效，别忘了打开 CONFIG_MODULE_FORCE_LOAD 。注释：(1) 真正加载 qca_cld3_qca6390.ko 时是需要传入一系列参数的，我这里只是为了复现报错，故没有传入参数。(2) 某些厂商甚至开出来的 Linux 版本号和原厂系统都不一样。强制不加载看起来这个需求有些莫名其妙，但有人确实会想要这么做，特别是在 GKI 内核上，如果你想砍掉某些功能的话。好消息是，Linux 内核本身就提供了强制不加载某些模块的机制，实现这个功能并不需要对内核本体进行任何修改。想要强制不加载某个模块，只需要在内核的 cmdline 中加入 module_blacklist=xxx 即可。如果你想要强制不加载多个模块，只需要使用逗号分隔开模块名即可，如 module_blacklist=aaa,bbb,ccc 。至于模块名从哪里来，只需要 modinfo -F name 某个模块.ko 即可得到，如：$ modinfo -F name qca_cld3_qca6390.ko wlan修改 cmdline 的工作可以借助 AnyKernel3 完成。如果不方便编辑 cmdline ，模块黑名单还可以通过 sysfs 传入，只需要将模块名（列表）写入 /sys/module/kernel/parameters/module_blacklist 。需要注意的是，你需要在系统加载模块之前配置完这个表，否则就太晚了。此外，如果你的手上有内核源码，还可以通过一行 config 来方便的追加 cmdline ，如 CONFIG_CMDLINE="module_blacklist=aaa,bbb,ccc" 。GKI接下来聊一聊 GKI 。总体看下来，GKI 是对原有内核模块机制的增强应用，并没有逃出我们上面所介绍的那些内容。事先声明，由于电子阳痿，我并没有真正上手过 GKI 内核，因此以下内容只是基于部分代码和官方文档的片面之谈，如有不准确的地方欢迎在评论区补充和讨论。先说说 GKI 的目标是什么。我们不谈那些高大上“内核镜像通刷”，聚焦于具体目标，GKI 其实就是在实现内核模块和内核本体之间的“跨版本匹配”，即保证一次编译的内核模块能够在不同的内核本体上顺利加载和运行。正确加载和运行内核模块的前提是“接口匹配”。然而，这并不是一件非常容易的事情，因为 Linux 内核并不保证模块接口的稳定性，即使是小版本更新也可能破坏模块接口（比如某个 struct 里新增了一个成员，那相关接口可能就会炸一片）。而内核源码中定义的模块接口又实在是太多了，想要维护所有接口的稳定是十分困难的。所以，GKI 选出了其中一部分 vendor 模块常用的接口，将它们记录为 KMI(Kernel Module Interface) ，它们的稳定性将由 Android Common Kernel(ACK) 负责。那么，ACK 是如何维护 KMI 的稳定性的呢？其实很简单，来自 Linux 上游的哪个提交破坏了接口稳定性，就 revert 掉它，比如： https://github.com/aosp-mirror/kernel_common/commit/202ee063496e2050f55e508ba2c03be1444a6dea 。然后，如果有需要（比如这是一个重要的漏洞修复），那再尝试用不破坏接口的方式绕着圈重新应用它。KMI 是根据需要挑选出来的，相关内容被保存在内核源码的 android/ 目录下，AOSP 提供了一系列的工具用来追踪 KMI 的稳定性 。而那些没有被选中的内核接口，自然而然还是不稳定的，于是，GKI 内核的模块会被分为两类：第一类被称为 “GKI 模块” (GKI module) ，它们使用的模块接口不受限制，但是需要随 GKI 内核本体一起更新；另一类被称为 “供应商模块” (vendor module) ，它们只能使用 KMI 中列出的稳定接口，但是在 GKI 内核本体更新后仍然能够正常工作(1) 。接下来结合一下上面的知识：GKI 内核全部默认开启了 CONFIG_MODVERSIONS ，因此 vermagic 中的内核版本号字段并不会影响模块的加载（方便跨版本加载“供应商模块”），模块的兼容性则根据记录的接口校验值进行辅助判断。GKI 内核不允许开启 CONFIG_MODULE_SIG_FORCE ，因为它需要加载不同来源的“供应商模块”。同理，“供应商模块”也不会被签名，防篡改则由 AVB 负责（将模块们装在受 AVB 保护的分区里）。此外，上面我们还提到了“GKI 模块”和“供应商模块”能够调用的内核模块接口范围是不同的，那么有没有一个机制能够将“供应商模块”的接口调用范围限制在 KMI 中呢?有这样的尝试，但是还不太完善：GKI 在正式编译时 会打开内核中的 CONFIG_TRIM_UNUSED_KSYMS 选项，修剪掉未使用的模块接口，KMI 接口则会通过 CONFIG_UNUSED_KSYMS_WHITELIST 保留下来。在没有编译 “GKI 模块” 时，这招能够确保暴露在外的只有 KMI 接口，从而屏蔽“供应商模块”对非 KMI 接口的访问。在 5.10+ 内核上，GKI 引入了对 “GKI 模块” 的签名保护，即所有 “GKI 模块” 都需要被签名，只有签名验证通过的模块才能访问非 KMI 接口，剩下的模块只能使用稳定的 KMI 接口。这一特性由 CONFIG_MODULE_SIG_PROTECT 负责，由于需要签名校验的支持，这些内核还需要打开 CONFIG_MODULE_SIG （但是上面的 CONFIG_MODULE_SIG_FORCE 绝对不可以）。签名保护的接口列表被写死在 android/abi_gki_modules_protected 和 android/abi_gki_modules_exports ，并在编译时生成头文件 + 塞到内核本体中。不过，这俩列表到目前为止都还是空的，config 却打开了，可能是这项功能还没正式启用？非常值得一提的是，GKI 的分发是要严格遵守流程的，从编译脚本、编译器版本到使用的 config 都有 规定 ，毕竟它们都是可能影响接口稳定性的因素。注释：(1) 确切的来说，是在 GKI 内核本体的 KMI 更新之前都能正常工作。在 Android 大版本更新时，ACK 会创建新的分支提供更新的 KMI ，此时“供应商模块”可能需要重新编译。参考资料https://www.kernel.org/doc/html/v4.10/admin-guide/module-signing.htmlhttp://lishiwen4.github.io/linux-kernel/linux-kernel-module-version-checkhttps://blog.oasisfeng.com/2008/08/12/all-about-modversions/https://source.android.com/docs/core/architecture/kernel也可以看看来自真实开发者的 GKI 实践 —— 论如何维护好一个自定义 GKI
前言本文基于Android 4.19 Kernel展开讨论，使用的二进制文件来自Android Toybox，可能与真正的Linux系统略有不同本文探究的主要内容内核空间是如何接收用户传来的优先级的优先级在Linux Kernel中的存储方式Top命令中PR、NI两个优先级的含义及其在内核中的对应前情提要调度策略概述Linux内核中存在以下几种任务调度策略include/uapi/linux/sched.h /* * Scheduling policies */ #define SCHED_NORMAL 0 #define SCHED_FIFO 1 #define SCHED_RR 2 #define SCHED_BATCH 3 /* SCHED_ISO: reserved but not implemented yet */ #define SCHED_IDLE 5 #define SCHED_DEADLINE 6 其中比较稳定和常用的是SCHED_NORMAL、SCHED_FIFO和SCHED_RRSCHED_NORMAL在用户空间有对应的别名SCHED_OTHER，这个名字被使用在如chrt之类的改变调度策略的命令中其中，SCHED_OTHER是基于CFS算法的时间片轮转，而SCHED_FIFO和SCHED_RR则是实时(RT)调度策略实时调度策略的优先级始终高于SCHED_OTHER此处可以引入RT优先级的概念便于理解（对应chrt中的scheduling_priority、内核中的rt_priority）（一个任务的rt优先级可以直接使用chrt -p <pid>查看到）可以认为，RT优先级高的任务可以抢占RT优先级低的任务，此处的抢占是指：高优先级的任务不完成，低优先级任务就无法运行而SCHED_FIFO和SCHED_RR的RT优先级范围是[1,99]，SCHED_OTHER的RT优先级必须为0，因此RT任务可以抢占普通任务对于RT优先级相同(>0)的任务，SCHED_FIFO采用先进先出的策略，而SCHED_RR则采用平均的时间片轮转（以上阐述是符合用户侧的状态且易于理解的，但rt优先级在内核侧的工作方式可能并没有想的那么简单，因为它并不会直接决定实时任务能抢占普通任务，而且在内核中实时任务的rt优先级是可以为0的，这和用户空间规定的[1,99]确实不同）CFS算法上面提到了：SCHED_OTHER是基于CFS算法的时间片轮转而CFS算法则是一种在时间片轮转过程中，能针对不同进程的CFS优先级，对时间片长短进行偏置的方式此处的CFS优先级和上述的RT优先级是完全不一样的概念，RT优先级意味着完全的抢占，而CFS优先级则是建立在进程都能运行的前提下因此，CFS优先级只对具有相同RT优先级的进程有意义，CFS算法只存在于SCHED_OTHER中，此时所有进程的RT优先级均为0而这个CFS优先级，它有个真正的名字，叫NICE值在Linux内核中，可以通过以下这个数组将NICE值转换为权重const int sched_prio_to_weight[40] = { /* -20 */ 88761, 71755, 56483, 46273, 36291, /* -15 */ 29154, 23254, 18705, 14949, 11916, /* -10 */ 9548, 7620, 6100, 4904, 3906, /* -5 */ 3121, 2501, 1991, 1586, 1277, /* 0 */ 1024, 820, 655, 526, 423, /* 5 */ 335, 272, 215, 172, 137, /* 10 */ 110, 87, 70, 56, 45, /* 15 */ 36, 29, 23, 18, 15, };然后再通过下面这条公式，算出允许的进程负载上面这堆数值带来的规律是，对于两个进程来说，每提升1的NICE值，会使进程能取得的CPU时间减少10%优先级的修改用户空间可以使用chrt修改进程的调度策略和RT优先级renice修改进程的CFS NICE值内核空间可以使用int sched_setscheduler(struct task_struct *p, int policy, const struct sched_param *param)修改进程调度策略和实时优先级void set_user_nice(struct task_struct *p, long nice)修改进程的NICE值正文以下内容才是这篇文章探究的关键点优先级在内核侧的存储在Linux关键的PCB中，存在以下内容include/linux/sched.h struct task_struct { ...... int prio; int static_prio; int normal_prio; unsigned int rt_priority; unsigned int policy; ...... };其中包含了各种优先级和调度策略，接下来就逐一探究其存储了什么内容NICE值直接从设置NICE值的函数入手，看看东西被存到了哪里kernel/sched/core.c void set_user_nice(struct task_struct *p, long nice) { bool queued, running; int old_prio, delta; struct rq_flags rf; struct rq *rq; if (task_nice(p) == nice || nice < MIN_NICE || nice > MAX_NICE) return; /* * We have to be careful, if called from sys_setpriority(), * the task might be in the middle of scheduling on another CPU. */ rq = task_rq_lock(p, &rf); update_rq_clock(rq); /* * The RT priorities are set via sched_setscheduler(), but we still * allow the 'normal' nice value to be set - but as expected * it wont have any effect on scheduling until the task is * SCHED_DEADLINE, SCHED_FIFO or SCHED_RR: */ if (task_has_dl_policy(p) || task_has_rt_policy(p)) { p->static_prio = NICE_TO_PRIO(nice); goto out_unlock; } queued = task_on_rq_queued(p); running = task_current(rq, p); if (queued) dequeue_task(rq, p, DEQUEUE_SAVE | DEQUEUE_NOCLOCK); if (running) put_prev_task(rq, p); p->static_prio = NICE_TO_PRIO(nice); set_load_weight(p, true); old_prio = p->prio; p->prio = effective_prio(p); delta = p->prio - old_prio; if (queued) { enqueue_task(rq, p, ENQUEUE_RESTORE | ENQUEUE_NOCLOCK); /* * If the task increased its priority or is running and * lowered its priority, then reschedule its CPU: */ if (delta < 0 || (delta > 0 && task_running(rq, p))) resched_curr(rq); } if (running) set_curr_task(rq, p); out_unlock: task_rq_unlock(rq, p, &rf); } EXPORT_SYMBOL(set_user_nice);可以看到，无论是RT进程还是普通进程，最终nice值都通过了一个NICE_TO_PRIO的宏，存储到了static_prio中而这个宏是长这样的include/linux/sched/prio.h #define MAX_NICE 19 #define MIN_NICE -20 #define NICE_WIDTH (MAX_NICE - MIN_NICE + 1) /* * Priority of a process goes from 0..MAX_PRIO-1, valid RT * priority is 0..MAX_RT_PRIO-1, and SCHED_NORMAL/SCHED_BATCH * tasks are in the range MAX_RT_PRIO..MAX_PRIO-1. Priority * values are inverted: lower p->prio value means higher priority. * * The MAX_USER_RT_PRIO value allows the actual maximum * RT priority to be separate from the value exported to * user-space. This allows kernel threads to set their * priority to a value higher than any user task. Note: * MAX_RT_PRIO must not be smaller than MAX_USER_RT_PRIO. */ #define MAX_USER_RT_PRIO 100 #define MAX_RT_PRIO MAX_USER_RT_PRIO #define MAX_PRIO (MAX_RT_PRIO + NICE_WIDTH) #define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2) /* * Convert user-nice values [ -20 ... 0 ... 19 ] * to static priority [ MAX_RT_PRIO..MAX_PRIO-1 ], * and back. */ #define NICE_TO_PRIO(nice) ((nice) + DEFAULT_PRIO)翻译一下就是，static_prio = 120 + nice此时我们已经明白，对于任何任务，即使是NICE值无效的RT任务，其NICE值也会在换算后被存储到static_prio中rt_prioritystatic void __setscheduler_params(struct task_struct *p, const struct sched_attr *attr) { int policy = attr->sched_policy; if (policy == SETPARAM_POLICY) policy = p->policy; /* Replace SCHED_FIFO with SCHED_RR to reduce latency */ p->policy = policy == SCHED_FIFO ? SCHED_RR : policy; if (dl_policy(policy)) __setparam_dl(p, attr); else if (fair_policy(policy)) p->static_prio = NICE_TO_PRIO(attr->sched_nice); /* * __sched_setscheduler() ensures attr->sched_priority == 0 when * !rt_policy. Always setting this ensures that things like * getparam()/getattr() don't report silly values for !rt tasks. */ p->rt_priority = attr->sched_priority; p->normal_prio = normal_prio(p); set_load_weight(p, true); }rt_priority就是来自于sched_setscheduler中传递的param中的sched_priority，而这又对应到了syscall使用的sys_sched_setscheduler，也就是说，上方的两条设置rt优先级的命令，都将rt优先级赋值给了rt_priority，其值与赋值的值相等normal_prionormal不是普通，而是normalize的缩写这个东西被称为归一化的优先级，也就是说，这个东西存在的意义主要在于，使得不同调度策略的任务在优先级上能够方便的比较而它，其实就是通过一种算法，把RT优先级和NICE值两者融合在了一起，为所有任务呈现一个通用的优先级include/linux/sched/prio.h #define MAX_NICE 19 #define MIN_NICE -20 #define NICE_WIDTH (MAX_NICE - MIN_NICE + 1) /* * Priority of a process goes from 0..MAX_PRIO-1, valid RT * priority is 0..MAX_RT_PRIO-1, and SCHED_NORMAL/SCHED_BATCH * tasks are in the range MAX_RT_PRIO..MAX_PRIO-1. Priority * values are inverted: lower p->prio value means higher priority. * * The MAX_USER_RT_PRIO value allows the actual maximum * RT priority to be separate from the value exported to * user-space. This allows kernel threads to set their * priority to a value higher than any user task. Note: * MAX_RT_PRIO must not be smaller than MAX_USER_RT_PRIO. */ #define MAX_USER_RT_PRIO 100 #define MAX_RT_PRIO MAX_USER_RT_PRIO #define MAX_PRIO (MAX_RT_PRIO + NICE_WIDTH) #define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2) /* * Convert user-nice values [ -20 ... 0 ... 19 ] * to static priority [ MAX_RT_PRIO..MAX_PRIO-1 ], * and back. */ #define NICE_TO_PRIO(nice) ((nice) + DEFAULT_PRIO) include/linux/sched/deadline.h /* * SCHED_DEADLINE tasks has negative priorities, reflecting * the fact that any of them has higher prio than RT and * NORMAL/BATCH tasks. */ #define MAX_DL_PRIO 0 kernel/sched/core.c /* * __normal_prio - return the priority that is based on the static prio */ static inline int __normal_prio(struct task_struct *p) { return p->static_prio; } /* * Calculate the expected normal priority: i.e. priority * without taking RT-inheritance into account. Might be * boosted by interactivity modifiers. Changes upon fork, * setprio syscalls, and whenever the interactivity * estimator recalculates. */ static inline int normal_prio(struct task_struct *p) { int prio; if (task_has_dl_policy(p)) prio = MAX_DL_PRIO-1; else if (task_has_rt_policy(p)) prio = MAX_RT_PRIO-1 - p->rt_priority; else prio = __normal_prio(p); return prio; }从上面这段代码中可以看到，对于SCHED_OTHER类型的任务，其normal_prio就是static_prio而对于RT类型的任务，其normal_prio为99-rt_priority在于RT任务的优先级范围[0,99]对照之后我们可以得出下方的结论normal_prio的分布是连续的，其中区间[0,99]属于RT任务，大小由RT优先级决定[100,139]属于普通任务，大小由NICE值决定嗯？有没有发现奇怪的地方，为什么99是闭区间呢？RT任务的RT优先级的范围不是从1开始的么，这我在上面有提到过，内核中任务的RT优先级是可以被设置为0的，即使用户空间并不允许这么做prio别忘了task_struct里面还有个就叫做prio的东西这个东西有另一个叫法：effective_prio 有效优先级这个东西似乎是RT任务调度优先的真正参考依据，而不是rt_priorityrt.c:274: return rq->rt.highest_prio.curr > prev->prio && rt.c:386: plist_node_init(&p->pushable_tasks, p->prio); rt.c:390: if (p->prio < rq->rt.highest_prio.next) rt.c:391: rq->rt.highest_prio.next = p->prio; rt.c:402: rq->rt.highest_prio.next = p->prio; rt.c:511: if (rt_rq->highest_prio.curr < curr->prio) rt.c:546: return p->prio != p->normal_prio; rt.c:914: return rt_task_of(rt_se)->prio; rt.c:1542: curr->prio <= p->prio))) { rt.c:1553: p->prio < cpu_rq(target)->rt.highest_prio.curr)) rt.c:1596: if (p->prio < rq->curr->prio) { rt.c:1614: if (p->prio == rq->curr->prio && !test_tsk_need_resched(rq->curr)) rt.c:1960: if (lowest_rq->rt.highest_prio.curr <= task->prio) { rt.c:1991: if (lowest_rq->rt.highest_prio.curr > task->prio) rt.c:2051: if (unlikely(next_task->prio < rq->curr->prio)) { rt.c:2351: if (p && (p->prio < this_rq->rt.highest_prio.curr)) { rt.c:2363: if (p->prio < src_rq->curr->prio) rt.c:2399: rq->curr->prio <= p->prio)) rt.c:2475: if (p->prio < rq->curr->prio && cpu_online(cpu_of(rq))) rt.c:2496: if (oldprio < p->prio) rt.c:2503: if (p->prio > rq->rt.highest_prio.curr) rt.c:2507: if (oldprio < p->prio) rt.c:2516: if (p->prio < rq->curr->prio)据说是因为一些普通任务在某些情况下也会被boost成RT类型的任务？/* * Calculate the current priority, i.e. the priority * taken into account by the scheduler. This value might * be boosted by RT tasks, or might be boosted by * interactivity modifiers. Will be RT if the task got * RT-boosted. If not then it returns p->normal_prio. */ static int effective_prio(struct task_struct *p) { p->normal_prio = normal_prio(p); /* * If we are RT tasks or we were boosted to RT priority, * keep the priority unchanged. Otherwise, update priority * to the normal priority: */ if (!rt_prio(p->prio)) return p->normal_prio; return p->prio; }不过按照这东西的生成方法来看，大多数情况下，它都是和normal_prio保持一致的TOP命令中的优先级NI很明显是NICE值，那PR又是什么，看这个范围，好像啥也不是，有时甚至还会出现字符串RT？此处忽略实验过程，直接说结论NI = normal_prio - 100不过也有可能是prio - 100，不过这俩东西基本都是相等的，我也不知道到底是哪个当NI<=-10时，TOP中显示为RT，并不是网上有些说法中所谓的最优先才显示为RT结至于这些优先级在内核中具体被使用的方式，有待进一步研究
前言几天前心血来潮，给内核关掉了WALT强行切换到了PELT负载追踪，但遗憾的是高通的魔改用力过猛，以至于直接切换回PELT（并修复编译）后并不能正常进行负载分配（负载迁移到大核之后难以摊开，EAS下负载全部堆在一个核心上，直到满频过载之后fall back to 传统的调度才会把负载摊开。。。）。当然，去谷歌Pixel的内核那边掏修复补丁是个办法（谷歌不走寻常路的把高通打磨了半天的WALT换回了PELT），但是，我觉得谷歌其实，emmmm也不是这么可信吧（其实是懒得解决移植的冲突，意外情况太多）。那就顺着逻辑，4.4内核上自带的是PELT，WALT是移植的，那就打磨PELT，现在WALT变成了原生支持，PELT反而需要一堆补丁来修复，那就打磨WALT吧。其实还有一个点让我突然想好好研究一下WALT的，那就是我发现PELT下小核的负载比默认WALT下的正常的多，WALT下酷安滑屏小核负载基本都在15%-20%（PELT下在40%以上），而三个中核却有较高的负载（30%以上），这是不科学的，因为能量模型应该已经告诉了内核小核最高频的能效高于大核最低频，而且此时小核性能反而更强，这种莫名的负载迁移确实让我很想调查一下。一些节点的结论懒得写过程了，摸鱼所得，毕竟这些调度算法也不是一时半会儿能看懂的，也都只知道了个大概schedtune节点sched_boost_no_override见下方sched_boostschedtune.colocate指定一个cgroup的任务是不是相关线程组，这在负载迁移和选择核心时会起作用，下面再说sysctl节点位于/proc/sys/kernelsched_boost这个其实很久以前就研究过了，不过还是记录下吧0 -> 小核优先负载 + 相关线程组负载如果超过sched_group_upmigrate则该线程组优先中核 （详细见下）1 -> 大核优先负载2 -> 指定了sched_boost_no_override的任务中核优先负载，其他任务小核优先（相关线程组会被忽略）3 -> 0的内容 + 负载缩放使频率调度更激进？sched_busy_hysteresis_enable_cpussched_busy_hyst_nssched_coloc_busy_hysteresis_enable_cpussched_coloc_busy_hyst_ns这几个是涉及cpuidle的控制，其中hysteresis_enable_cpus是指定CPU的bitmask，hyst_ns是时间，这里的时间大概是指任务执行后暂停cpuidle的时间？默认情况下powerhal会在触摸屏幕时对sched_busy_hysteresis_enable_cpus指定所有核心，对sched_busy_hyst_ns指定一个忘了是多少的时间，手指离开屏幕时复原，这个造成的效果是在中核/大核优先负载时产生明显的jitter下降，小核优先负载时效果不明显。有没有coloc的区别是在于是否针对相关线程组，指针对所有任务还是仅在相关线程组活跃时在指定的核心上进行。sched_dynamic_ravg_window_enablesched_ravg_window_nr_ticks这两个玩意儿提供了神奇的动态控制WALT window size的功能，当内核的HZ被设为250时会默认启用。注意：当HZ不为250时，就算你打开这个功能它也不会工作，所以这节点完全多余。。它默认为90Hz刷新率使用12ms的时间窗口为120Hz刷新率使用8ms时间窗口（display那边在刷新率改变时主动调用）你也可以通过sched_ravg_window_nr_ticks设置更小的时间窗口，窗口的大小算法是1000/HZ得到最小窗口大小，最小窗口大小 * sched_ravg_window_nr_ticks得到指定窗口大小注意：你只能设置更小的时间窗口，更大的时间窗口（比刷新率指定的要大）会被直接忽略（里面有个min的比较）（所以我觉得这两个节点完全没有意义）更小的时间窗口意味着更快的负载响应速度，但是也意味着轻负载更容易被判断为重负载导致不必要的任务迁移prefer_spread_on_idle决定了是否要主动把负载迁移到空闲的核心上，迁移到空闲核心意味着从（idle状态唤醒这些核心） | （阻止刚刚结束任务的核心进入idle）状态，这可能导致更高的耗电，在某些情况下能够提升性能0 -> 否1 -> 小核 是2 -> 全核 是3 -> 小核 仅在刚刚idle时是 （意味着不刻意唤醒深度idle的核心）4 -> 全核 仅在刚刚idle时是 （意味着不刻意唤醒深度idle的核心）sched_force_lb_enable这个东西本身不属于WALT，既然看过了就写一下决定是否要在小核出现需要向上迁移的重负载时，对非小核进行负载平衡sysctl_sched_user_hint临时性的对负载计算进行一点点干预（对负载进行放缩）？最大可写入1000执行强制负载向上转移？这个节点的值会在请求被处理后马上还原为0sched_window_stats_policy牵扯到window内计算负载的几种方式#define WINDOW_STATS_RECENT 0 #define WINDOW_STATS_MAX 1 #define WINDOW_STATS_MAX_RECENT_AVG 2 #define WINDOW_STATS_AVG 3默认是2 想知道到底有啥区别可以理解一下下面的代码，我觉得默认的2就已经挺合理的了 /* Push new 'runtime' value onto stack */ widx = sched_ravg_hist_size - 1; ridx = widx - samples; for (; ridx >= 0; --widx, --ridx) { hist[widx] = hist[ridx]; sum += hist[widx]; if (hist[widx] > max) max = hist[widx]; } for (widx = 0; widx < samples && widx < sched_ravg_hist_size; widx++) { hist[widx] = runtime; sum += hist[widx]; if (hist[widx] > max) max = hist[widx]; } p->ravg.sum = 0; if (sysctl_sched_window_stats_policy == WINDOW_STATS_RECENT) { demand = runtime;//0 } else if (sysctl_sched_window_stats_policy == WINDOW_STATS_MAX) { demand = max;//1 } else { avg = div64_u64(sum, sched_ravg_hist_size); if (sysctl_sched_window_stats_policy == WINDOW_STATS_AVG) demand = avg;//3 else demand = max(avg, runtime);//2 }sched_group_upmigratesched_group_downmigrate这两个玩意儿决定了相关线程组任务的集体向上、向下迁移阈值当负载大于sched_group_upmigrate时，这个组会被设为skip_min，而此时组内任务会中核优先负载（同一组的新任务也是中核优先负载）（不过还是有条件可以让某个组内任务负载在小核上，下面再说）当负载小于sched_group_upmigrate时，skip_min会消失，整个组的任务小核优先负载丢在哪个核心上决定了要通过哪个核心知道任务的实际性能需求（丢上去之前是不知道的），而这也决定了任务在因为性能不足/过剩而迁移前的性能sched_min_task_util_for_colocationsched_task_unfilter_period这就是上一条所说的“不过还是有条件可以让某个组内任务负载在小核上”如果相关线程组内的某个任务，负载小于sched_min_task_util_for_colocation的时间大于sched_task_unfilter_period，那么它就无法和线程组内的其他任务呆在一起，而被迫被丢回小核了sched_min_task_util_for_boost类似上一条，决定了在sched_boost=2时要把哪些boost在中/大核上的任务丢回小核剩下的节点基本就是字面意思了，另外有一两个节点暂且还没搞清楚作用，那就忽略吧~可Hack的点永远小核优先负载 kernel/sched/fair.c | 1 + 1 file changed, 1 insertion(+) diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c index be82b1c345ce..4fbf0b027abf 100644 --- a/kernel/sched/fair.c +++ b/kernel/sched/fair.c @@ -6920,6 +6920,7 @@ static inline bool get_rtg_status(struct task_struct *p) static inline bool task_skip_min_cpu(struct task_struct *p) { return sched_boost() != CONSERVATIVE_BOOST && + sched_boost() != NO_BOOST && get_rtg_status(p) && p->unfilter; }作用：在sched_boost = 0时屏蔽schedtune.colocate（相关线程组）的作用，所有任务在小核上测试负载水平，然后决定是否迁移到中/大核，当然，可能有亿点卡，可以通过调window size 或 HZ 来缓解，直接解决了前言中所说的小核负载不高的问题禁用hysteresis特性一行干掉上面的四个相关节点( kernel/sched/sched_avg.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kernel/sched/sched_avg.c b/kernel/sched/sched_avg.c index 1e7bb64e3c2d..9981b2488bb2 100644 --- a/kernel/sched/sched_avg.c +++ b/kernel/sched/sched_avg.c @@ -221,6 +221,8 @@ u64 sched_lpm_disallowed_time(int cpu) u64 now = sched_clock(); u64 bias_end_time = atomic64_read(&per_cpu(busy_hyst_end_time, cpu)); + return 0; + if (now < bias_end_time) return bias_end_time - now;总结只是简单的摸了一下kernel/sched的相关代码，调速器的具体工作细节还没有摸清，也没有办法解释调度器中一些参数的作用（所以有个pl相关的节点还是没法解释），有生之年会再看看的（笑
解决一加sm8250平台数据流量下难以休眠
前言这个问题由来已久，一直搁着没看，它在官方系统+官方内核上同样发生，导致一加的865设备的数据流量下的待机续航普遍较差，idle drain高达4%-5%/h，这显然是无法接受的。今天决定了要把这个问题瞧一瞧，一瞧就是半个下午+一个晚上。问题描述数据流量下待机续航极差深度睡眠时间极少（截图来自官方内核）日志排查dmesg中存在大量类似这样的日志[ 750.094062] PM: Device alarmtimer failed to suspend: error -16 [ 750.094065] PM: Some devices failed to suspend, or early wake event detected [ 753.496178] active wakeup source: alarmtimer [ 756.421102] active wakeup source: alarmtimer [ 757.386560] active wakeup source: alarmtimer [ 760.735522] active wakeup source: alarmtimer [ 763.220499] active wakeup source: alarmtimer [ 764.955187] active wakeup source: alarmtimer [ 767.227574] active wakeup source: alarmtimer [ 769.694145] active wakeup source: alarmtimer [ 771.424980] active wakeup source: alarmtimer [ 773.686699] active wakeup source: alarmtimer [ 775.367871] pm_system_irq_wakeup: 351 triggered pm8xxx_rtc_alarm另外再附上一张来自官方内核的日志截图非常类似源码排查alarmtimer 来自 kernel/time/alarmtimer.c ，这是一个负责定时器的核心驱动，Android系统层的AlarmManager就是由它在内核层实现的这个驱动能发生问题就很神奇，首先，确定阻止唤醒的原因排查一 为什么它阻止了休眠？经过一次测试，马上锁定了问题的发生点static int alarmtimer_suspend(struct device *dev) { .............. if (ktime_to_ns(min) < 2 * NSEC_PER_SEC) { __pm_wakeup_event(ws, 2 * MSEC_PER_SEC); return -EBUSY; } .............. }错误代码-16就是在这里返回的，可以看到在这里跳出的条件是ktime_to_ns(min) < 2 * NSEC_PER_SEC，结合上下文可知此处是判断下一个定时器的发生时间如果在两秒以内，则阻止休眠（就休眠这两秒没什么用，而且可能导致问题（比如rtc时钟未即时响应导致下一次唤醒时间被设置在休眠时间之前了））我也尝试了移除这段代码，原来的难以休眠变为了诡异的休眠一点点时间（大概一秒）就唤醒，循环进行排查二 诡异的定时器哪里来？定时器创建函数 alarmtimer_enqueuestatic void alarmtimer_enqueue(struct alarm_base *base, struct alarm *alarm) { if (alarm->state & ALARMTIMER_STATE_ENQUEUED) timerqueue_del(&base->timerqueue, &alarm->node); timerqueue_add(&base->timerqueue, &alarm->node); alarm->state |= ALARMTIMER_STATE_ENQUEUED; } 我尝试在里面打印出current->comm从而显示执行这段代码的任务通过比较wifi下与数据流量下的定时器日志，我发现主要区别来自一大堆kworker/xx的进程（排除了来自用户空间的可能性，反而是松了一口气，因为一加的用户空间脏的不行 + 现在的动态分区 使得修改用户空间变得非常困难）而很明显，我无法直接追踪这些kworker到底是哪里的于是我尝试使用工具调试perf 工具我尝试使用perf record -g -a sleep 10记录了10秒内的系统活动情况然后通过perf report打印出来遗憾的是，没有什么有用的东西（主要是特征不是很明显，现在找到了问题再去回看日志，确实是能找到对应函数的痕迹的）定时器激活函数 alarmtimer_firedstatic enum hrtimer_restart alarmtimer_fired(struct hrtimer *timer) { struct alarm *alarm = container_of(timer, struct alarm, timer); struct alarm_base *base = &alarm_bases[alarm->type]; unsigned long flags; int ret = HRTIMER_NORESTART; int restart = ALARMTIMER_NORESTART; spin_lock_irqsave(&base->lock, flags); alarmtimer_dequeue(base, alarm); spin_unlock_irqrestore(&base->lock, flags); if (alarm->function) restart = alarm->function(alarm, base->gettime()); spin_lock_irqsave(&base->lock, flags); if (restart != ALARMTIMER_NORESTART) { hrtimer_set_expires(&alarm->timer, alarm->node.expires); alarmtimer_enqueue(base, alarm); ret = HRTIMER_RESTART; } spin_unlock_irqrestore(&base->lock, flags); trace_alarmtimer_fired(alarm, base->gettime()); return ret; }这个函数在定时器激活时被调用，其中有一个回调函数用于确定定时器是否应该被再次设定这里似乎是可以得到线索的我使用pr_err("%pF", 函数名)的方式从函数指针alarm->function还原出了函数名它指向了函数qmi_rmnet_work_alarm而在这里我也发现了定时器的调用频率超乎想象，每秒就有一次定时器激活（这直接解释了上面对于休眠的观察），这在打开wifi后会立即消失，而再启用流量后又会再次出现它又发生了什么？于是通过函数名我顺利的定位到了问题，来自drivers/soc/qcom/qmi_rmnet.c结合上下文我发现函数qmi_rmnet_check_stats在rmnet_work_quit==false的情况下就会不断通过开启延迟为1s的alarm执行自己，而这正是造成这个问题的原因于是我进行了一些临时的修复首先是Revert这个提交这个提交将原来的workqueue换成了alarm，按提交里的意思是这样唤醒更精确，更省电，但是不知道为什么rmnet_work_quit始终没有被设为true，导致alarm持续唤醒，反而造成了巨量的耗电其次是为工作队列加上unbound和freezable的flag来尽可能降低对功耗的影响一点额外调查我尝试弄清rmnet_work_quit的值在何时会发生改变很遗憾只有这唯一一条日志[ 311.346071] libxzr: rmnet_work_quit=false它就没有被设为true过结暂且不知道这bug应该甩锅给谁，一加还是高通？但是我的代码来自CAF rebase，问题却依然在发生。。嗯？又是帮厂商擦屁股的一天解决后的效果 正常了很多