Android 上层卡顿在内核中的反应(基于 Linux v7.2.2)

引言

Android 应用卡顿,表象千千万,根因却往往指向同一个方向------Linux 内核。当 UI 线程在 Perfetto 里呈现出 runnableD 状态或 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.cmm/vmscan.c oom-killerpgmajfault
D 状态 + IO 压力 块层等待 / 写限速 block/blk-core.cmm/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 timeoutFrameMissed
running 但慢 被限频 / 被 softirq 抢 cpufreq_schedutil.cthermal_core.csoftirq.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_wakeupsched_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.cfilemap_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/memorysome/full,LMKD 监听它杀后台 → 冷启动 → 新卡顿。

2.4 Workingset refault:回收"误伤"

mm/workingset.c:548workingset_refault() 统计刚回收又被访问的页,/proc/vmstatworkingset_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.csubmit_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_issueblock_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:4762binder_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 handlebinder.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_frequencythermal/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

分析五步走

  1. 锚点 :logcat 找 Skipped xx frames / Davey! duration= / ANR in 的时间戳;
  2. 定状态:Perfetto 中该时刻看 UI 线程是 running / runnable / D / sleep;
  3. 落类别:runnable 久→调度;D 状态→内存或 I/O;sleep 在 binder/fence→IPC 或 GPU;running 但慢→频率/温控;
  4. 交叉确认:用该类别的 dmesg 关键字 + 计数器增量验证;
  5. 定位根因 :沿对应源码路径深挖(hung task 日志和 /proc/<pid>/stack 通常直接给出卡住的函数)。

九、速查总表

类别 源码路径(v7.2.2) dmesg/logcat 关键字 计数器/节点 ftrace 事件
调度 kernel/sched/fair.c:5913 soft lockupblocked for more than PSI cpu、schedstat sched_switch/wakeup
缺页 mm/memory.c:6651 --- pgmajfault mm_filemap/*
直接回收 mm/vmscan.c:6676 invoked oom-killer allocstallpgscan_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 timeoutFrameMissed 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 定类别,再用日志关键字交叉确认,最后沿源码路径定位根因。

相关推荐
H_oRIZoN_1 小时前
Linux入门DAY36(TCP 并发服务器)
linux·服务器·tcp/ip
灵晔君1 小时前
【Linux】基础IO(一)——文件、上层C库文件操作、底层linux原生IO
linux·运维·c语言
小五传输1 小时前
聚焦卫健数字化建设 文件传输服务器守护跨机构敏感医疗数据交换
大数据·运维·安全
机器人梦想家1 小时前
Debian 12/13 关闭开机图形界面(仅保留命令行)
运维·debian
Fcy6481 小时前
Linux下 应⽤层⾃定义协议与序列化(JSON方法)
linux·json
凡泰AI1 小时前
如何通过小程序多端框架,让一个小程序同时运行在iOS、安卓、鸿蒙和微信客户端,实现开发层面的降本增效
android·ios·微信小程序·小程序·harmonyos
智购科技无人售货机工厂1 小时前
2026自动售货机云端API设计规范:从RESTful到GraphQL的接口演进~YH
android·人工智能·驱动开发·单片机·云原生·pandas·设计规范
遇见小修修1 小时前
佛山工厂打印机频繁漏粉、页面脏污、机身飞粉?零基础排查+长效解决教程
运维·打印机维修·打印机故障·专业维修
koi77u2 小时前
嵌入式学习————————TCP并发服务器(1)
linux·学习