Android live-lock 守护程序 (llkd)

Android 10 包含 Android live-lock 守护程序 (llkd),该程序旨在发现和缓解内核死锁问题。llkd 组件提供默认的独立实现,但您也可以选择将 llkd 代码集成到其他服务中(作为主循环的一部分或作为单独的线程)。

检测场景

llkd 有两种检测场景:一种是持久 D 状态或 Z 状态,另一种是持久堆栈签名。

持久 D 状态或 Z 状态

如果某个线程一直处于 D(不间断休眠)状态或 Z(僵死)状态且超过 ro.llk.timeout_ms or ro.llk.[D|Z].timeout_ms 没有任何进展,llkd 就会终止该进程(或父进程)。如果后续扫描显示同一进程仍然存在,llkd 会确认活锁情况,并通过提供有关该情况的最详细 bug 报告向内核发出紧急警报。

llkd 包含一个自监控定时器,会在 llkd 锁定时发出警报;监控定时器的时间是主循环预计循环时间的两倍,采样频率为 ro.llk_sample_ms 一次。

持久堆栈签名

对于 userdebug 版本,llkd 可以使用持久堆栈签名检查来检测内核活锁。如果处于 Z 状态以外任意状态的线程持久列出 ro.llk.stack 内核符号,且这种情况的持续时间超过 ro.llk.timeout_msro.llk.stack.timeout_msllkd 就会终止该进程(即使向前调度作业仍在继续也会终止该进程)。如果后续扫描显示同一进程仍然存在,llkd 会确认活锁情况,并通过提供有关该情况的最详细 bug 报告向内核发出紧急警报。

lldk 检查会在活锁情况存在期间持续进行,并在 Linux 上的 /proc/pid/stack 文件中查找组合字符串 symbol+0xsymbol.cfi+0x。符号列表存储在 ro.llk.stack 中,并且默认为逗号分隔列表“cma_alloc,__get_user_pages,bit_wait_io,wait_on_page_bit_killable”。

符号的出现频率应极低且存在时间应足够短,在典型的系统上,于 ro.llk.stack.timeout_ms 的超时期限内符号函数只能在样本中出现一次(采样频率为 ro.llk.check_ms 一次)。由于缺少 ABA 保护,因此这是防止误触发的唯一办法。符号函数必须出现在调用会发生争用的锁的函数之后。如果锁位于符号函数之后或之中,则符号会出现在所有受影响的进程中,而不仅仅出现在导致锁定的进程中。

监控范围

llkd 的默认实现不监控 init[kthreadd][kthreadd] 衍生线程。为了让 llkd 监控 [kthreadd] 衍生的线程,需要满足以下条件:

  • 驱动程序不得持久处于 D 状态,

  • 驱动程序必须具有线程恢复机制(倘若线程是在外部终止的)。例如,使用 wait_event_interruptible() 而不是 wait_event()

如果满足上述其中一个条件,系统就可以调整 llkd 拒绝名单,将内核组件纳入监控范围内。堆栈符号检查包括一个额外的进程拒绝名单,用于防止阻止 ptrace 操作的服务发生 sepolicy 违规情况。

Android 属性

llkd 会响应多个 Android 属性(如下所示)。

  • 名为 prop_ms 的属性以毫秒为单位。
  • 对列表使用逗号 (,) 分隔符的属性使用前导分隔符来保留默认条目,然后分别使用可选的加号 (+) 和减号 (-) 前缀来加减条目。对于这些列表,字符串 false 与空列表同义,而空条目或缺失的条目则使用指定的默认值。

ro.config.low_ram

设备配置的内存有限。

ro.debuggable

设备已针对 userdebug 或 eng build 进行配置。

ro.llk.sysrq_t

如果属性为 eng,则默认值不是 ro.config.low_ramro.debuggable。如果为 true,则转储所有线程 (sysrq t)。

ro.llk.enable

允许启用 Live-Lock 守护程序。默认值为 false

llk.enable

已针对 eng build 进行评估。默认值为 ro.llk.enable

ro.khungtask.enable

允许启用 [khungtask] 守护程序。默认值为 false

khungtask.enable

已针对 eng build 进行评估。默认值为 ro.khungtask.enable

ro.llk.mlockall

允许调用 mlockall()。默认值为 false

ro.khungtask.timeout

[khungtask] 的最长时间限制。默认值为 12 分钟。

ro.llk.timeout_ms

处于 D 状态或 Z 状态的最长时间限制。默认值为 10 分钟。将此值翻倍可为 llkd 设置警报监控定时器。

ro.llk.D.timeout_ms

处于 D 状态的最长时间限制。默认值为 ro.llk.timeout_ms

ro.llk.Z.timeout_ms

处于 Z 状态的最长时间限制。默认值为 ro.llk.timeout_ms

ro.llk.stack.timeout_ms

检查持久堆栈符号的最长时间限制。默认值为 ro.llk.timeout_ms仅在 userdebug 或 eng build 中有效

ro.llk.check_ms

D 状态或 Z 状态线程采样间隔时间。默认值为 2 分钟。

ro.llk.stack

检查内核堆栈符号,这些符号如果持久存在,则表明子系统被锁定。默认值为 cma_alloc,__get_user_pages,bit_wait_io,wait_on_page_bit_killable(内核符号的逗号分隔列表)。除了在 ro.llk.stack.timeout_ms 时间段内每 ro.llk_check_ms 轮询一次外,该检查不会向前调度 ABA,因此,堆栈符号的出现频率应极低且非常短暂(符号持久出现在所有堆栈样本中的可能性微乎其微)。在堆栈扩展中检查 symbol+0xsymbol.cfi+0x 的匹配项。仅适用于 userdebug 或 eng build;在 user build 中,考虑安全问题会导致权限受限,无法进行此检查。

ro.llk.blacklist.process

llkd 不监控指定的进程。默认值为 0,1,2kernelinit[kthreadd])加上进程名称 init,[kthreadd],[khungtaskd],lmkd,llkd,watchdogd, [watchdogd],[watchdogd/0],...,[watchdogd/get_nprocs-1]。进程可以是 commcmdlinepid 引用。自动默认值可以大于当前最大属性大小(即 92 个字符)。

ro.llk.blacklist.parent

llkd 不监控具有指定父进程的进程。默认值为 0,2,adbd&[setsid]kernel[kthreadd]adbd 仅适用于僵尸进程 setsid)。“逻辑与” (&) 分隔符指定忽略父进程(仅在与目标子进程结合使用时)。之所以选择“逻辑与”符号,是因为进程名称绝不会使用该符号;不过,虽然通常指定 setprop 的 init rc 文件没有这个问题,但 shell 中的 setprop 需要对“逻辑与”符号进行转义或加引号。父进程或目标进程可以是 commcmdlinepid 引用。

ro.llk.blacklist.uid

llkd 不监控与指定 uid 匹配的进程。其为 UIS 号或名称的逗号分隔列表。默认值为空或 false

ro.llk.blacklist.process.stack

llkd 不监控指定用于检查活锁堆栈签名的这部分进程。默认值为进程名称 init,lmkd.llkd,llkd,keystore,ueventd,apexd,logd。防止发生与阻止 ptrace 的进程(因为无法检查这些进程)相关联的 sepolicy 违规情况。仅在 userdebug 和 eng build 中有效。如需详细了解 build 类型,请参阅构建 Android

架构问题

  • 属性限定为 92 个字符(但是,对于源代码中的 include/llkd.h 文件中定义的默认值,可忽略此限制)。
  • 内置 [khungtask] 守护程序的通用性太高,并且驱动程序代码中处于 D 状态的 trip 过多。切换到 S 状态会使任务可终止(并可根据需要由驱动程序恢复)。

库接口(可选)

您可以选择性使用 libllkd 组件中的以下 C 接口将 llkd 整合到另一个特权守护程序中:

#include "llkd.h"
bool llkInit(const char* threadname) /* return true if enabled */
unsigned llkCheckMillseconds(void)   /* ms to sleep for next check */

如果提供了线程名称,线程就会自动生成,否则调用方必须在其主循环中调用 llkCheckMilliseconds。该函数会返回到此处理程序的下一次预期调用所用的时间。