LibXZR 的小本本 - linux kernel
type: channel
link: https://blog.xzr.moe/tag/linux-kernel/
items: Horizon Clang based on ClangBuiltLinux
items: 看看 reserved-memory
items: 从 LKML 优雅的摘取补丁
items: 借助 uprobe 实现对应用帧生成时间的监听
items: 一种代替 pstore 的 panic 日志转储方案
items: 借助 atomic 实现 smp-safe 的“单次运行”
items: 聊一聊内核模块和 GKI 相关的问题
items: Linux Kernel 调度策略与优先级
items: 记一下这几天关于QTI EAS的一点研究
items: 解决一加sm8250平台数据流量下难以休眠

Horizon Clang based on ClangBuiltLinux
type: item
date: 2023-06-06T11:03:00+00:00
description: 简介这是一个适用于内核编译的编译器，是 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
type: item
date: 2023-05-27T12:57:00+00:00
description: 前言在这篇文章中，我想要梳理一下几种 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-0…

从 LKML 优雅的摘取补丁
type: item
date: 2023-04-29T13:38:00+00:00
description: 前言当我们在做 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…

借助 uprobe 实现对应用帧生成时间的监听
type: item
date: 2023-03-28T08:56:00+00:00
description: 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…

一种代替 pstore 的 panic 日志转储方案
type: item
date: 2023-03-26T07:56:00+00:00
description: 前言我手里有两台一加 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 Scre…

借助 atomic 实现 smp-safe 的“单次运行”
type: item
date: 2023-03-19T15:35:00+00:00
description: 前言最近有个非常奇怪的需求，我想让一段代码只被运行一次，但是又不想借助内核自带的 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 和读取，不存在上面的那种读取了却没有写入的状态。你可能会怀…

聊一聊内核模块和 GKI 相关的问题
type: item
date: 2022-12-25T13:14:00+00:00
description: 前言这篇文章，打算比较系统的梳理一下内核模块的问题，顺便聊一聊 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…

Linux Kernel 调度策略与优先级
type: item
date: 2021-08-01T09:28:00+00:00
description: 前言本文基于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 { ....…

记一下这几天关于QTI EAS的一点研究
type: item
date: 2021-02-28T11:09:00+00:00
description: 前言几天前心血来潮，给内核关掉了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，既然看过了就写…

解决一加sm8250平台数据流量下难以休眠
type: item
date: 2021-02-23T15:01:00+00:00
description: 前言这个问题由来已久，一直搁着没看，它在官方系统+官方内核上同样发生，导致一加的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打印出来遗憾的是，没有什么有用的东西（主要是特征不是很明显…