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_ms
或 ro.llk.stack.timeout_ms
,llkd
就会终止该进程(即使向前调度作业仍在继续也会终止该进程)。如果后续扫描显示同一进程仍然存在,llkd
会确认活锁情况,并通过提供有关该情况的最详细 bug 报告向内核发出紧急警报。
lldk
检查会在活锁情况存在期间持续进行,并在 Linux 上的 /proc/pid/stack
文件中查找组合字符串 " symbol+0x"
或 " symbol.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_ram
或 ro.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+0x"
或 " symbol.cfi+0x"
的匹配项。仅适用于 userdebug 或 eng build;在 user build 中,考虑安全问题会导致权限受限,无法进行此检查。
ro.llk.blacklist.process
llkd
不监控指定的进程。默认值为 0,1,2
(kernel
、init
和 [kthreadd]
)加上进程名称 init,[kthreadd],[khungtaskd],lmkd,llkd,watchdogd, [watchdogd],[watchdogd/0],...,[watchdogd/get_nprocs-1]
。进程可以是 comm
、cmdline
或 pid
引用。自动默认值可以大于当前最大属性大小(即 92 个字符)。
ro.llk.blacklist.parent
llkd
不监控具有指定父进程的进程。默认值为 0,2,adbd&[setsid]
(kernel
、[kthreadd]
和 adbd
仅适用于僵尸进程 setsid
)。“逻辑与” (&) 分隔符指定忽略父进程(仅在与目标子进程结合使用时)。之所以选择“逻辑与”符号,是因为进程名称绝不会使用该符号;不过,虽然通常指定 setprop 的 init rc
文件没有这个问题,但 shell 中的 setprop
需要对“逻辑与”符号进行转义或加引号。父进程或目标进程可以是 comm
、cmdline
或 pid
引用。
ro.llk.blacklist.uid
llkd
不监控与指定 uid 匹配的进程。其为 uid 号或名称的逗号分隔列表。默认值为空或 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
。该函数会返回到此处理程序的下一次预期调用所用的时间。