引言
Android 应用卡顿,表象千千万,根因却往往指向同一个方向------Linux 内核。当 UI 线程在 Perfetto 里呈现出 runnable、D 状态或 sleep 时,内核的调度器、内存管理、I/O 栈、Binder 驱动乃至温控模块,都可能是真正的"元凶"。
本文基于 Linux v7.2.2 源码,从机理、代码到日志锚点,系统梳理 Android 上层卡顿在内核中的七类典型反应,并给出可直接落地的排查流程与速查总表。
说明:Android 设备实际运行的是 ACK(Android Common Kernel,LTS + 厂商补丁),但以下子系统结构与 mainline v7.2.2 一致,路径可直接对照。所有代码段与日志字符串均已逐字核对 v7.2.2 源码。
总览:一张表看懂全链路
| 卡顿形态(Perfetto 线程状态) | 内核反应 | 核心源码路径 | 日志锚点 |
|---|---|---|---|
runnable 久(排队) |
调度延迟 / uclamp 选核失误 | kernel/sched/fair.c |
soft lockup、schedstat |
D 状态 + 内存压力 |
缺页 / 直接回收 | mm/memory.c、mm/vmscan.c |
oom-killer、pgmajfault |
D 状态 + IO 压力 |
块层等待 / 写限速 | block/blk-core.c、mm/page-writeback.c |
I/O error、PSI io |
sleep 在 IPC |
binder 等待 | drivers/android/binder.c |
traces.txt 栈在 IPCThreadState |
sleep 在 fence |
等 GPU 完成 | drivers/dma-buf/dma-fence.c |
fence timeout、FrameMissed |
running 但慢 |
被限频 / 被 softirq 抢 | cpufreq_schedutil.c、thermal_core.c、softirq.c |
critical temperature reached |
一、CPU 调度类:任务"排不上队"
1.1 机理
UI 线程就绪但得不到 CPU(runqueue latency 大),或被放到小核。
uclamp 参与选核------Android 给 UI/RenderThread 设 uclamp.min,内核唤醒选核时读取:
c
// kernel/sched/fair.c:5913
static inline int task_fits_cpu(struct task_struct *p, int cpu)
{
unsigned long uclamp_min = uclamp_eff_value(p, UCLAMP_MIN);
unsigned long uclamp_max = uclamp_eff_value(p, UCLAMP_MAX);
unsigned long util = task_util_est(p);
return (util_fits_cpu(util, uclamp_min, uclamp_max, cpu) > 0);
}

c
static inline void update_misfit_status(struct task_struct *p, struct rq *rq)
{
int cpu = cpu_of(rq);
if (!sched_asym_cpucap_active())
return;
/*
* Affinity allows us to go somewhere higher? Or are we on biggest
* available CPU already? Or do we fit into this CPU ?
*/
if (!p || (p->nr_cpus_allowed == 1) ||
(arch_scale_cpu_capacity(cpu) == p->max_allowed_capacity) ||
task_fits_cpu(p, cpu)) {
rq->misfit_task_load = 0;
return;
}
/*
* Make sure that misfit_task_load will not be null even if
* task_h_load() returns 0.
*/
rq->misfit_task_load = max_t(unsigned long, task_h_load(p), 1);
}
容量判断用 fits_capacity()(fair.c:104);已在运行但核不够大时走 update_misfit_status()(fair.c:5925)标记 misfit 等迁移------迁移延迟本身就是卡顿。
1.2 日志关键字(dmesg)
| 关键字 | 出处 | 含义 |
|---|---|---|
BUG: soft lockup - CPU#%d stuck for %us! |
kernel/watchdog.c:884 |
CPU 被霸占,附栈 |
Watchdog detected hard LOCKUP |
kernel/watchdog.c:267 |
关中断死循环 |
INFO: task %s:%d blocked for more than %ld seconds |
kernel/hung_task.c:253 |
D 状态超时,直接打印内核栈 |
rcu: INFO: rcu_preempt detected stalls |
kernel/rcu/tree_stall.h |
RCU 被拖住 |
1.3 观测
/proc/pressure/cpu;/proc/<pid>/schedstat(等待总时长);ftrace sched_wakeup→sched_switch 间隔。
二、内存类:卡顿最大的内核来源
2.1 缺页中断
c
// mm/memory.c:6651
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
unsigned int flags, struct pt_regs *regs)
{
...
lru_gen_enter_fault(vma); /* MGLRU 已深度整合 */
if (unlikely(is_vm_hugetlb_page(vma)))
ret = hugetlb_fault(vma->vm_mm, vma, address, flags);
else
ret = __handle_mm_fault(vma, address, flags);
匿名页缺页是微秒级;文件页 major fault 走 mm/filemap.c 的 filemap_fault() 读存储,毫秒级,直接卡 UI。
2.2 直接回收:分配内存的线程自己去"扫垃圾"
c
// mm/vmscan.c:6676
unsigned long try_to_free_pages(struct zonelist *zonelist, int order,
gfp_t gfp_mask, nodemask_t *nodemask)
{
struct scan_control sc = {
.nr_to_reclaim = SWAP_CLUSTER_MAX,
.priority = DEF_PRIORITY,
.may_writepage = 1, .may_unmap = 1, .may_swap = 1,
...
};
...
trace_mm_vmscan_direct_reclaim_begin(sc.gfp_mask, order, NULL);
nr_reclaimed = do_try_to_free_pages(zonelist, &sc);
trace_mm_vmscan_direct_reclaim_end(nr_reclaimed, NULL);
这对 tracepoint 就是 Perfetto 里可见的 direct reclaim 事件。实际扫描在 shrink_node()(vmscan.c:6146),MGLRU 走 lru_gen_shrink_node(),传统 LRU 走 shrink_node_memcgs()。源码注释(vmscan.c:6181 附近)还自曝了回收与脏页回写互相拖累的场景。
2.3 PSI:LMKD 杀进程的内核依据
c
// kernel/sched/psi.c:1056
void psi_memstall_enter(unsigned long *flags)
{
...
current->in_memstall = 1;
psi_task_change(current, 0, TSK_MEMSTALL | TSK_MEMSTALL_RUNNING);
汇聚成 /proc/pressure/memory 的 some/full,LMKD 监听它杀后台 → 冷启动 → 新卡顿。
2.4 Workingset refault:回收"误伤"
mm/workingset.c:548 的 workingset_refault() 统计刚回收又被访问的页,/proc/vmstat 的 workingset_refault_* 升高 = 切后台再切回卡顿的直接证据。
2.5 日志关键字
| 来源 | 关键字 | 出处 |
|---|---|---|
| dmesg | invoked oom-killer: gfp_mask=... |
mm/oom_kill.c:458 |
| dmesg | Killed process %d (%s) ... oom_score_adj:%d |
mm/oom_kill.c:945 |
| dmesg | page allocation failure: order:%u |
mm/page_alloc.c:5021(高阶页失败,相机/图形大 buffer 常见) |
| logcat | lmkd: Reap 'com.xxx' (pid ...) |
用户态 LMKD |
2.6 观测命令
bash
adb shell grep -E "pgmajfault|allocstall|pgscan_direct|workingset_refault" /proc/vmstat
adb shell cat /proc/pressure/memory
三、I/O 存储类:D 状态睡眠与写限速
3.1 机理
注意 :v7.2.2 中该文件已从 fs/ 移至 mm/。写线程脏页超限被主动限速:
c
// mm/page-writeback.c:1802
static int balance_dirty_pages(struct bdi_writeback *wb,
unsigned long pages_dirtied, unsigned int flags)
{
...
for (;;) {
nr_dirty = global_node_page_state(NR_FILE_DIRTY);
balance_domain_limits(gdtc, strictlimit);
...
if (!writeback_in_progress(wb) &&
(nr_dirty > gdtc->bg_thresh || ...))
wb_start_background_writeback(wb);
文件头注释写明单次最多睡 200ms------主线程写数据库/日志撞上就是一卡一卡。块层入口为 block/blk-core.c 的 submit_bio(),存储慢时等待方长时间处于 TASK_UNINTERRUPTIBLE。
3.2 日志/观测
bash
adb shell cat /proc/pressure/io
adb shell cat /proc/<pid>/stack # D状态线程卡在块层哪一层
adb shell dmesg | grep -iE "I/O error|ufshcd|mmc.*error|EXT4-fs error"
ftrace:block_rq_issue→block_rq_complete 间隔 = 存储耗时。
四、Binder IPC 类:Android 特有
4.1 服务端等活
c
// drivers/android/binder.c:4658
static int binder_wait_for_work(struct binder_thread *thread, bool do_proc_work)
{
...
for (;;) {
prepare_to_wait(&thread->wait, &wait, TASK_INTERRUPTIBLE|TASK_FREEZABLE);
if (binder_has_work_ilocked(thread, do_proc_work))
break;
...
schedule(); /* 服务端 binder 线程在此睡眠 */
4.2 客户端等返回
binder.c:4762,binder_thread_read() 的 retry 段:
c
retry:
wait_for_proc_work = binder_available_for_proc_work_ilocked(thread);
thread->looper |= BINDER_LOOPER_STATE_WAITING;
trace_binder_wait_for_work(wait_for_proc_work,
!!thread->transaction_stack,
!binder_worklist_empty(proc, &thread->todo));
...
ret = binder_wait_for_work(thread, wait_for_proc_work);
trace_binder_wait_for_work 三参数直接暴露问题:整进程忙 / 有嵌套事务在途 / todo 队列积压。system_server 线程池打满时,客户端收不到 reply,上层就是掉帧或 ANR。
4.3 关键调试细节
binder_user_error() 默认不打印(binder.c:159,需 BINDER_DEBUG_USER_ERROR):
bash
adb shell "echo 1 > /sys/module/binder/parameters/debug_mask"
adb shell cat /sys/kernel/debug/binder/{transactions,stats,transaction_log}
打开后 dmesg 才出现 got transaction with invalid handle(binder.c:2303)等错误。ANR 时 /data/anr/traces.txt 主线程栈停在 IPCThreadState::transact 即为此类。
五、图形渲染类:等 GPU fence
c
// drivers/dma-buf/dma-fence.c:525
signed long
dma_fence_wait_timeout(struct dma_fence *fence, bool intr, signed long timeout)
{
...
dma_fence_enable_sw_signaling(fence);
...
trace_dma_fence_wait_start(fence);
if (ops && ops->wait) {
ret = ops->wait(fence, intr, timeout);
} else {
ret = dma_fence_default_wait(fence, intr, timeout);
}
... trace_dma_fence_wait_end(fence);
这对 tracepoint 框出 SurfaceFlinger/HWC 等 GPU 的时长;GPU 负载高或频率低 → fence 晚于 VSync → 掉帧。
日志/观测:
bash
adb shell dmesg | grep -iE "fence.*timeout|gpu.*hang|kgsl"
adb shell dumpsys gfxinfo com.xxx framestats # 帧各阶段耗时
adb shell dumpsys SurfaceFlinger --latency com.xxx
六、中断/软中断类:CPU 被"隐形"占用
v7.2.2 中 __do_softirq() 是 handle_softirqs(false) 的包装,核心循环在 kernel/softirq.c:579:
c
static void handle_softirqs(bool ksirqd)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
int max_restart = MAX_SOFTIRQ_RESTART;
...
while ((softirq_bit = ffs(pending))) {
...
trace_softirq_entry(vec_nr);
h->action();
trace_softirq_exit(vec_nr);
MAX_SOFTIRQ_TIME(2ms)/MAX_SOFTIRQ_RESTART(10 次)超限后甩给 ksoftirqd------无论哪种都在和 UI 线程抢 CPU,网络大流量场景(NET_RX)最典型。
观测 :/proc/softirqs(复现前后取增量)、ftrace softirq_entry/exit。
七、频率与温控类:性能被"压住"
7.1 schedutil 提频滞后
c
// kernel/sched/cpufreq_schedutil.c:419
static void sugov_update_single_freq(struct update_util_data *hook, u64 time,
unsigned int flags)
{
...
next_f = get_next_freq(sg_policy, sg_cpu->util, max_cap);
...
if (sg_policy->policy->fast_switch_enabled) {
cpufreq_driver_fast_switch(sg_policy->policy, next_f);
} else {
sugov_deferred_update(sg_policy); /* 异步切频,延迟更大 */
}
目标频率由 get_next_freq()(cpufreq_schedutil.c:193)按 map_util_freq(util, ...) 计算------util 是 PELT 滑动平均,突发负载爬升滞后 → 短帧 burst 在低频上跑。Android 用 uclamp.min 抬 util 正是对冲此滞后(呼应第一节)。
7.2 热限频
drivers/thermal/thermal_core.c:551 的 __thermal_zone_device_update() 读温后走 thermal_zone_handle_trips()(:503),越阈即调 cooling device 压频率上限:
c
list_for_each_entry_safe(td, next, &tz->trips_high, list_node) {
if (td->threshold > tz->temperature)
break;
thermal_trip_crossed(tz, td, governor, true);
日志/观测:
bash
adb shell dmesg | grep -i "critical temperature reached" # thermal_core.c:324
adb shell cat /sys/class/thermal/thermal_zone*/temp
adb shell cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq # 被钳=限频
adb shell cat /sys/devices/system/cpu/cpu*/online # 大核是否被下线
ftrace:power/cpu_frequency、thermal/thermal_temperature------帧耗时拉长时频率曲线被压平即为此类。
八、标准排查流程(抓一次卡顿的完整动作)
bash
# 1. 对齐时钟(logcat 是墙上时间,dmesg 是内核单调时钟)
adb shell "date '+%T.%3N'; cat /proc/uptime"
# 2. 后台落盘
adb logcat -b all -v threadtime > logcat.txt &
adb shell dmesg -w > dmesg.txt &
# 3. 复现前后取计数器增量
adb shell grep -E "pgmajfault|allocstall|workingset_refault" /proc/vmstat > vmstat.before
# ...复现卡顿...
adb shell grep -E "pgmajfault|allocstall|workingset_refault" /proc/vmstat > vmstat.after
# 4. Perfetto 抓 10s
adb shell perfetto -o /data/misc/perfetto-traces/jank.pftrace -t 10s \
sched freq idle binder_driver binder_lock memory gfx view wm am sf
adb pull /data/misc/perfetto-traces/jank.pftrace
分析五步走:
- 锚点 :logcat 找
Skipped xx frames/Davey! duration=/ANR in的时间戳; - 定状态:Perfetto 中该时刻看 UI 线程是 running / runnable / D / sleep;
- 落类别:runnable 久→调度;D 状态→内存或 I/O;sleep 在 binder/fence→IPC 或 GPU;running 但慢→频率/温控;
- 交叉确认:用该类别的 dmesg 关键字 + 计数器增量验证;
- 定位根因 :沿对应源码路径深挖(hung task 日志和
/proc/<pid>/stack通常直接给出卡住的函数)。
九、速查总表
| 类别 | 源码路径(v7.2.2) | dmesg/logcat 关键字 | 计数器/节点 | ftrace 事件 |
|---|---|---|---|---|
| 调度 | kernel/sched/fair.c:5913 |
soft lockup、blocked for more than |
PSI cpu、schedstat | sched_switch/wakeup |
| 缺页 | mm/memory.c:6651 |
--- | pgmajfault |
mm_filemap/* |
| 直接回收 | mm/vmscan.c:6676 |
invoked oom-killer |
allocstall、pgscan_direct |
mm_vmscan_direct_reclaim_* |
| 内存压力 | kernel/sched/psi.c:1056 |
lmkd Reap |
/proc/pressure/memory |
--- |
| 回收误伤 | mm/workingset.c:548 |
--- | workingset_refault_* |
--- |
| 写限速 | mm/page-writeback.c:1802 |
--- | PSI io、/proc/<pid>/stack |
block_rq_* |
| Binder | drivers/android/binder.c:4658/4762 |
Slow operation、开 debug_mask 后 got transaction... |
debugfs binder | binder_transaction |
| GPU fence | drivers/dma-buf/dma-fence.c:525 |
fence timeout、FrameMissed |
dumpsys gfxinfo |
dma_fence_wait_* |
| 软中断 | kernel/softirq.c:579 |
--- | /proc/softirqs |
softirq_entry/exit |
| 提频 | kernel/sched/cpufreq_schedutil.c:419 |
--- | scaling_cur_freq |
cpu_frequency |
| 热限频 | drivers/thermal/thermal_core.c:551 |
critical temperature reached |
scaling_max_freq、thermal_zone |
thermal_temperature |
总结
上层"卡一下",在内核里必属七类之一------调度排队、缺页/直接回收、I/O D 状态、binder 等待、fence 等待、中断抢占、限频。先用 PSI 和 Perfetto 定类别,再用日志关键字交叉确认,最后沿源码路径定位根因。