hung 之 Android llkd

目录

1. llkd 简介

2. 原理

2.1 内核活锁

2.2 检测机制

2.3 为什么 persistent stack signature 检测机制不执行 ABA 检查?

2.4 为什么 kill 进程后,进程还存在就能判定发生了内核 live-lock?

3. 代码

3.1 内核 live-lock 检查

3.2 更新 state & count

3.3 persistent stack signature

3.3.1 开关

3.3.2 检测

4. 使能


1. llkd 简介

linux kernel 的 hungtaskd(由 CONFIG_DETECT_HUNG_TASK 使能)功能可以检测 hung tassk,即长时间处于 D 状态的进程。

lldk 是 hungtaskd 功能的用户空间平替(加强)。

llkd 在 debug、非 debug 版本上有不同的检测机制。

  • debug 版本,使用 persistent stack signature 检测机制

        目标是发现内核态调用栈长时间没有变化的进程。

  • 非 debug 版本,增加 persistent D or Z state 检测机制

        目标是发现长时间处于 D 或 Z 状态的进程。

llkd 的 AOSP 代码路径:system/core/llkd

llkd 官方介绍文档:https://source.android.google.cn/docs/core/architecture/kernel/llkd?hl=zh-cn

2. 原理

2.1 内核活锁

根据官方文档的描述,llkd 的作用是发现和减少内核死锁。

而由 llkd 的名字 live-lock daemon 可知,llkd 实际的作用是发现内核活锁。

活锁(live-lock)

活锁是一种情况:线程或进程虽然在不断运行,但实际上并未取得任何进展。

与死锁不同,活锁不会完全停止运行,而是陷入一个无效的循环。

即:死锁时,线程一般会一直处于 sleep 状态;活锁时,线程一般会不断运行。

广义的死锁包含活锁。

内核活锁:是指进程在内核态处于活锁状态。

  • persistent stack signature 检测机制

2.2 检测机制

设置一个检测周期,每个检测周期进行一次内核活锁检测。

在非 debug 版本上使用 persistent D or Z state 检测机制,检测标准是进程长时间处于 D 或 Z 状态且状态没有发生变化。

以 T1 为周期 check 目标进程(所有进程都是目标进程)状态。

每次 check

  • 用变量 count 记录进程在当前状态下持续的时间;
  • 用变量 nrSwitches 记录进程状态切换次数
  • 用变量 schedUpdate 记录进程最近一次调度时的时间戳

check 时

  • 如果进程状态改变,则将 count 置为 0,否则将 count 值 + T1
  • 如果当前状态为 D 或 Z,且 count 值 >= D 或 Z 的 timeout 阈值时,则认为可能发生了 live-lock,执行 kill 进程的动作,将该进程标记为 killed。
  • 下一次 check 时,如果发现 killed 进程仍然存在(即没有 kill 掉),则判定的确发生了内核 live-lock,触发 kernel panic。

在 debug 版本上增加了 persistent stack signature 检测机制,检测标准是进程的内核态调用栈长时间没有发生变化。

首先,persistent stack signature 检测机制不考虑 Z 状态的进程。

其次,persistent stack signature 只匹配特定的栈帧符号,这些符号存放在变量 llkCheckStackSymbols 中,用 idx 表示符号在 llkCheckStackSymbols 中的存放顺序。

每次 check

  • 用变量 stack 记录进程的内核态调用栈匹配到的特定栈帧符号的 idx(按 idx 顺序匹配到第一个即止)
  • 用变量 count_stack 记录进程在当前调用栈下持续的时间

check 时

  • 如果匹配到的 idx 变化,则将 count_stack 置为 0;否则将 count_stack 值 + T1
  • 如果 count_stack 值 >= timeout 阈值,则认为可能发生了 live-lock,执行 kill 进程的动作,将该进程标记为 killed。
  • 如果下一次 check 时,发现 killed 进程仍然存在(即没有 kill 掉),则判定的确发生了内核 live-lock,触发 kernel panic。

2.3 为什么 persistent stack signature 检测机制不执行 ABA 检查?

llkd 在以周期 T1 进行 persistent stack signature 检查时,有可能两次 check 时内核调用栈相同,但是在周期 T1 内调用栈实际上发生了变化,这就产生了 ABA 问题。

但是 llkd 没有进行 ABA 检查,这是因为 llkd 的 persistent stack signature 检查机制允许进程前向调度。即,persistent stack signature 检查并不会使目标进程停止运行,在 persistent stack signature 检查包括 T1 周期内目标进程照常运行,所以没办法做 ABA 检查。

即,非不为也,实不能也~

ABA 问题以及 ABA 检测

ABA 问题发生在一个线程或进程在检查共享资源的状态时,该状态在检查过程中被其他线程或进程更改并恢复到原始状态。具体来说:

  1. 线程 A 读取一个共享变量,发现其值为 A

  2. 线程 A 在进行一些处理时,线程 B 修改了该共享变量的值,从 A 到 B,再从 B 回到 A

  3. 线程 A 再次检查共享变量的值,发现它仍然是 A,于是认为该值从未被修改过,继续执行

这种情况下,线程 A 会误认为共享变量的状态没有发生变化,可能会导致程序错误。为了防止这种情况,需要进行 ABA 检测,即在每次修改共享变量时附加一个版本号或其他标识符,以便检测到状态的变化。

前向调度(Forward Scheduling)

前向调度,是指系统允许线程或进程在未来的某个时间点被调度和执行。这意味着系统可以根据某些条件提前安排线程的执行顺序,而不是严格按照先来先服务的原则。

2.4 为什么 kill 进程后,进程还存在就能判定发生了内核 live-lock?

按照活锁的定义,发生活锁的目标进程是持续运行的,应该不会一直处于 D 或 Z 状态,那么就应该有机会执行到 kill -9 信号,从而被 killed 掉。

但是,内核活锁不一样,如果 live-lock 发生在内核态,进程陷入到内核态的循环中,是没有机会返回用户态的。在这种情况下,常规的信号处理机制(如SIGKILL)就无法生效,因为这些信号通常在用户态中处理,而不是内核态。因此,发生内核活锁的进程杀不掉。

3. 代码

3.1 内核 live-lock 检查

检查内核 live-lock 的关键函数是 llkCheck。

// system/core/llkd/libllkd.cpp

milliseconds llkCheck(bool checkRunning) {
// 遍历进程

...
            // 更新目标进程的 count、state 值
            // ABA mitigation watching last time schedule activity happened
            llkCheckSchedUpdate(procp, piddir);

// DEBUG 版本 __PTRACE_ENABLED__ 使能
#ifdef __PTRACE_ENABLED__
            // 执行 persistent stack signature 检查
            auto stuck = llkCheckStack(procp, piddir);
            // 执行 persistent state 检查
            if (llkIsMonitorState(state)) {
                if (procp->count >= llkStateTimeoutMs[(state == 'Z') ? llkStateZ : llkStateD]) {
                    stuck = true;
                } else if (procp->count != 0ms) {
                    LOG(VERBOSE) << state << ' ' << llkFormat(procp->count) << ' ' << ppid << "->"
                                 << pid << "->" << tid << ' ' << process_comm;
                }
            }
            if (!stuck) continue;
#else
            // 执行 persistent state 检查
            if (procp->count >= llkStateTimeoutMs[(state == 'Z') ? llkStateZ : llkStateD]) {
                if (procp->count != 0ms) {
                    LOG(VERBOSE) << state << ' ' << llkFormat(procp->count) << ' ' << ppid << "->"
                                 << pid << "->" << tid << ' ' << process_comm;
                }
                continue;
            }
#endif
...
            // 代码执行到这里,说明目标进程可能发生了内核 live-lock。
            // 目标进程还没尝试 kill,则执行 kill。
            // 对于 Z 状态的进程,需要 kill 它的父进程。
            if (procp->killed == false) {
                procp->killed = true;
                // confirm: re-read uid before committing to a panic.
                procp->uid = -1;
                switch (state) {
                    case 'Z':  // kill ppid to free up a Zombie
                        // Killing init will kernel panic without diagnostics
                        // so skip right to controlled kernel panic with
                        // diagnostics.
                        if (ppid == initPid) {
                            break;
                        }
                        LOG(WARNING) << "Z " << llkFormat(procp->count) << ' ' << ppid << "->"
                                     << pid << "->" << tid << ' ' << process_comm << " [kill]";
                        if ((llkKillOneProcess(pprocp, procp) >= 0) ||
                            (llkKillOneProcess(ppid, procp) >= 0)) {
                            continue;
                        }
                        break;

                    case 'D':  // kill tid to free up an uninterruptible D
                        // If ABA is doing its job, we would not need or
                        // want the following.  Test kill is a Hail Mary
                        // to make absolutely sure there is no forward
                        // scheduling progress.  The cost when ABA is
                        // not working is we kill a process that likes to
                        // stay in 'D' state, instead of panicing the
                        // kernel (worse).
                    default:
                        LOG(WARNING) << state << ' ' << llkFormat(procp->count) << ' ' << pid
                                     << "->" << tid << ' ' << process_comm << " [kill]";
                        if ((llkKillOneProcess(llkTidLookup(pid), procp) >= 0) ||
                            (llkKillOneProcess(pid, state, tid) >= 0) ||
                            (llkKillOneProcess(procp, procp) >= 0) ||
                            (llkKillOneProcess(tid, state, tid) >= 0)) {
                            continue;
                        }
                        break;
                }
            }

            // 代码执行到这里,说明确认了内核活锁,触发 kernel panic
            // We are here because we have confirmed kernel live-lock
            std::vector<std::string> threads;
            auto taskdir = procdir + std::to_string(tid) + "/task/";
            dir taskDirectory(taskdir);
            for (auto tp = taskDirectory.read(); tp != nullptr; tp = taskDirectory.read()) {
                std::string piddir;
                if (getValidTidDir(tp, &piddir))
                    threads.push_back(android::base::Basename(piddir));
            }
            const auto message = state + " "s + llkFormat(procp->count) + " " +
                                 std::to_string(ppid) + "->" + std::to_string(pid) + "->" +
                                 std::to_string(tid) + " " + process_comm + " [panic]\n" +
                                 "  thread group: {" + android::base::Join(threads, ",") +
                                 "}";
            llkPanicKernel(dump, tid,
                           (state == 'Z') ? "zombie" : (state == 'D') ? "driver" : "sleeping",
                           message);
            dump = false;
        }
        LOG(VERBOSE) << "+closedir()";
    }

3.2 更新 state & count

state 的更新比较简单,只需要在 check 时记录下目标进程当前的 state 即可。

而 count 的更新则要考虑目标进程在 check 周期内是否发生了变化,即需要做 ABA 检查。

是否可以考虑用进程在用户态、内核态的运行时间来表示"持续时间"呢,即只有当用户态、内核态运行时间没有增加时我们才增加"持续时间"?

即,通过 /proc/<pid>/stat 节点可以获取目标进程在用户态、内核态的运行时间。

比如下面的示例进程在用户态的运行时长为 86,内核态运行时间为 77~~~ 单位是 jiffies。

一个jiffy表示CPU调度(软件时钟)的周期,是 CONFIG_HZ 的倒数,比如 CONFIG_HZ 为100,则一个jiffy为10s。

yudi:/ # cat /proc/2523/stat
2523 (binder:2523_2) S 1 2523 0 0 -1 1077936384 3575 4242 0 3 86 77 5 9 20 0 7 0 18048945 11271012352 2012 18446744073709551615 405566062592 405566180224 548682767680 0 0 0 0 0 1073775864 0 0 0 17 1 0 0 0 0 0 405566267184 405566267184 405863604224 548682771308 548682771336 548682771336 548682776540 0

首先,用进程的内核态运行时间不变来认定 "持续" 是不行的,因为内核活锁的主要表现就是进程会在内核态持续运行。内核态运行时间增加,不能说明进程没有内核活锁。

用进程的用户态运行时间不变来认定 "持续" 也不行。虽然用户态运行时间增加,可以说明进程在用户态执行了,不是内核活锁;但是用户态运行时间不增加,却不能说明进程发生了内核 live-lock。

llkd 使用了另外一种方式,确保只有在进程状态在周期内没有变化时,才会增加"持续时间"。

  • 通过 /proc/<pid>/sched 的 last_update_time 字段获取进程状态最近一时更新的时间戳;
  • 通过 /proc/<pid>/schedstat 获取进程状态切换次数。

只要两次 check 时进程状态的切换次数或者更新时间不同,就认为状态发生过变化,将 count 置 0!

1|yudi:/ # cat /proc/2523/sched|grep last_update_time
se.avg.last_update_time                      :      179663295832064

yudi:/ # cat /proc/2523/schedstat
230446929 10297343 451

llkCheckSchedUpdate 方法更新 state & count 。

848  // Primary ABA mitigation watching last time schedule activity happened
849  void llkCheckSchedUpdate(proc* procp, const std::string& piddir) {
850      // Audit finds /proc/<tid>/sched is just over 1K, and
851      // is rarely larger than 2K, even less on Android.
852      // For example, the "se.avg.lastUpdateTime" field we are
853      // interested in typically within the primary set in
854      // the first 1K.
855      //
856      // Proc entries can not be read >1K atomically via libbase,
857      // but if there are problems we assume at least a few
858      // samples of reads occur before we take any real action.
859      std::string schedString = ReadFile(piddir + "/sched");
860      if (schedString.empty()) {
861          // /schedstat is not as standardized, but in 3.1+
862          // Android devices, the third field is nr_switches
863          // from /sched:
864          schedString = ReadFile(piddir + "/schedstat");
865          if (schedString.empty()) {
866              return;
867          }
868          auto val = static_cast<unsigned long long>(-1);
869          if (((::sscanf(schedString.c_str(), "%*d %*d %llu", &val)) == 1) &&
870              (val != static_cast<unsigned long long>(-1)) && (val != 0) &&
871              (val != procp->nrSwitches)) {
872              procp->nrSwitches = val;
873              procp->count = 0ms;
874              procp->killed = !llkTestWithKill;
875          }
876          return;
877      }
878  
879      auto val = getSchedValue(schedString, "\nse.avg.lastUpdateTime");
880      if (val == -1) {
881          val = getSchedValue(schedString, "\nse.svg.last_update_time");
882      }
883      if (val != -1) {
884          auto schedUpdate = nanoseconds(val);
885          if (schedUpdate != procp->schedUpdate) {
886              procp->schedUpdate = schedUpdate;
887              procp->count = 0ms;
888              procp->killed = !llkTestWithKill;
889          }
890      }
891  
892      val = getSchedValue(schedString, "\nnr_switches");
893      if (val != -1) {
894          if (static_cast<uint64_t>(val) != procp->nrSwitches) {
895              procp->nrSwitches = val;
896              procp->count = 0ms;
897              procp->killed = !llkTestWithKill;
898          }
899      }
900  }

3.3 persistent stack signature

3.3.1 开关

llkd 代码中通过宏 __PTRACE_ENABLED__ 控制 persistent stack signature 是否使能。

debug 版本编译时默认开启宏 __PTRACE_ENABLED__,因此 debug 版本才会使能 persistent stack signature。

// system/core/llkd/Android.bp

cc_library_static {
    name: "libllkd",

    srcs: [
        "libllkd.cpp",
    ],

    shared_libs: [
        "libbase",
        "libcutils",
        "liblog",
    ],

    export_include_dirs: ["include"],

    cflags: ["-Werror"],

    product_variables: {
        debuggable: {
            cppflags: ["-D__PTRACE_ENABLED__"],
        },
    },
}

这里有个疑问,persistent stack signature 功能并不需要 ptrace 目标进程,为什么功能开关要用 __PTRACE_ENABLED__ 这样一个看似与 ptrace 有关的宏控制?

persistent stack signature 功能需要读 /proc/<pid>/stack 节点,该节点返回进程(线程)的内核态调用栈。读 /proc/<pid>/stack 节点时,内核方法会检查 caller 是否有 ptrace 目标进程的权限,然后通过 unwind 获取调用栈。也就是说,如果要使能 persistent stack signature,llkd 需要设置 ptrace 权限(SYS_PTRACE capabilitiy)。

yudi:/ # cat /proc/7187/stack
[<0>] __switch_to+0x244/0x4e4
[<0>] binder_wait_for_work+0x1ac/0x77c
[<0>] binder_thread_read+0x3c8/0x35b0
[<0>] binder_ioctl_write_read+0x120/0x854
[<0>] binder_ioctl+0x294/0x1dc4
[<0>] __arm64_sys_ioctl+0x174/0x1f8
[<0>] el0_svc_common+0xd4/0x270
[<0>] el0_svc+0x28/0x88
[<0>] el0_sync_handler+0x8c/0xf0
[<0>] el0_sync+0x1b4/0x1c0

但是,SYS_PTRACE capabilitiy 权限只有在 debug 版本上才能获取到(sepolicy 限制)。

llkd 在 debug 版本上使用的 rc 配置文件是 llkd-debuggable.rc,在 llkd-debuggable.rc 文件中给 lldk 服务设置了 SYS_PTRACE capabilitiy 权限。

//system/core/llkd/llkd-debuggable.rc

service llkd-1 /system/bin/llkd
    class late_start
    disabled
    user llkd
    group llkd readproc
    capabilities KILL IPC_LOCK SYS_PTRACE DAC_OVERRIDE SYS_ADMIN
    file /dev/kmsg w
    file /proc/sysrq-trigger w
    task_profiles ServiceCapacityLow

3.3.2 检测

检测时,首先过滤掉 Z 状态的进程。

忽略掉 llkIgnorelistStack 中定义的进程。

然后检查目标进程的内核态调用栈是否包含 llkCheckStackSymbols 中定义的符号。

// system/core/llkd/libllkd.cpp

#ifdef __PTRACE_ENABLED__
bool llkCheckStack(proc* procp, const std::string& piddir) {
    if (llkCheckStackSymbols.empty()) return false;
    if (procp->state == 'Z') {  // No brains for Zombies
        procp->stack = -1;
        procp->count_stack = 0ms;
        return false;
    }

    // Don't check process that are known to block ptrace, save sepolicy noise.
    // 忽略掉 llkIgnorelistStack 中定义的进程
    if (llkSkipProc(procp, llkIgnorelistStack)) return false;
    auto kernel_stack = ReadFile(piddir + "/stack");
    if (kernel_stack.empty()) {
        LOG(VERBOSE) << piddir << "/stack empty comm=" << procp->getComm()
                     << " cmdline=" << procp->getCmdline();
        return false;
    }
    // A scheduling incident that should not reset count_stack
    if (kernel_stack.find(" cpu_worker_pools+0x") != std::string::npos) return false;
    char idx = -1;
    char match = -1;
    std::string matched_stack_symbol = "<unknown>";
    // 检查目标进程的内核态调用栈是否包含 llkCheckStackSymbols 中定义的符号
    for (const auto& stack : llkCheckStackSymbols) {
        if (++idx < 0) break;
        if ((kernel_stack.find(" "s + stack + "+0x") != std::string::npos) ||
            (kernel_stack.find(" "s + stack + ".cfi+0x") != std::string::npos)) {
            match = idx;
            matched_stack_symbol = stack;
            break;
        }
    }
    if (procp->stack != match) {
        procp->stack = match;
        procp->count_stack = 0ms;
        return false;
    }
    if (match == char(-1)) return false;
    procp->count_stack += llkCycle;
    if (procp->count_stack < llkStateTimeoutMs[llkStateStack]) return false;
    LOG(WARNING) << "Found " << matched_stack_symbol << " in stack for pid " << procp->pid;
    return true;
}
#endif

llkCheckStackSymbols 包含目标 symbols。

#define LLK_CHECK_STACK_DEFAULT         \
    "cma_alloc,__get_user_pages,bit_wait_io,wait_on_page_bit_killable"

llkIgnorelistStack 包含要忽略的进程。原因是这个名单中的进程不能被 ptrace(ptrace 时会 block),所以不能 check 这些进程的 stack,这个名单包含 init、llkd 等进程...

// system/core/llkd/include/llkd.h

#define LLK_IGNORELIST_STACK_DEFAULT    "init,lmkd.llkd,llkd,keystore,keystore2,ueventd,apexd"

4. 使能

本地的 Android 设备没有使能 lldk。

yudi:/ # ps -A|grep llkd
yudi:/ #

llkd 服务默认 disable,通过 prop llk.enable 启动。

在非 debug 版本上,启动 llkd-0 服务,

在 debug 版本上,启动 llkd-1 服务(定义在 llkd-debuggable.rc 中)。

可以通过手动设置 llk.enable 为 1 或 true 来启动服务。

// system/core/llkd/llkd.rc

on property:llk.enable=true
    start llkd-${ro.debuggable:-0}

service llkd-0 /system/bin/llkd
    class late_start
    disabled
    user llkd
    group llkd readproc
    capabilities KILL IPC_LOCK
    file /dev/kmsg w
    file /proc/sysrq-trigger w
    task_profiles ServiceCapacityLow

debug 版本如果设置了 ro.llk.enable 为 1,则开机时会自动设置 llk.enable 为 true 并启动 llkd 服务。

// // system/core/llkd/llkd.rc

# eng default for ro.llk.enable and ro.khungtask.enable
on property:ro.debuggable=*
    setprop llk.enable ${ro.llk.enable:-0}
    setprop khungtask.enable ${ro.khungtask.enable:-0}

on property:ro.llk.enable=true
    setprop llk.enable true

on property:llk.enable=1
    setprop llk.enable true

on property:llk.enable=0
    setprop llk.enable false

相关推荐

  1. hung hung task 检测

    2024-07-18 18:14:02       21 阅读
  2. kafka hang 问题记录

    2024-07-18 18:14:02       48 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-18 18:14:02       70 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-18 18:14:02       74 阅读
  3. 在Django里面运行非项目文件

    2024-07-18 18:14:02       62 阅读
  4. Python语言-面向对象

    2024-07-18 18:14:02       72 阅读

热门阅读

  1. Reflect使用的一些总结

    2024-07-18 18:14:02       16 阅读
  2. day02.09.逻辑运算符·二

    2024-07-18 18:14:02       24 阅读
  3. NSGA和MOGA 算法的异同点

    2024-07-18 18:14:02       24 阅读
  4. linux中创建一个循环定时器(C++)

    2024-07-18 18:14:02       23 阅读
  5. 关于HDFS、Hive和Iceberg

    2024-07-18 18:14:02       22 阅读
  6. Leetcode 3218. Minimum Cost for Cutting Cake I

    2024-07-18 18:14:02       23 阅读