news 2026/10/19 12:22:52

SIGTERM vs SIGKILL:Android自动化测试中如何优雅清理monkey进程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SIGTERM vs SIGKILL:Android自动化测试中如何优雅清理monkey进程

有段时间我经常要处理自动化测试设备的残留进程问题。跑完Monkey压测后,设备上偶尔会留下几个monkey进程,拖慢后续任务。一开始清理方式很粗暴,用kill -9硬杀。结果某次批量跑完测试后,新启动的任务频繁报端口冲突和日志目录被占用。后来我把清场命令从pkill -9 monkey换成了pkill -15 monkey,配合等待逻辑,情况明显好转。这背后其实是15号信号SIGTERM和9号信号SIGKILL的差异。

如果你经常和adb shell、Linux命令、自动化测试打交道,这篇内容会很实用。我会把SIGTERM信号本身的机制讲透,说清楚pkill -15 monkey这条命令到底匹配了什么、信号发出后发生了什么、为什么它比直接kill -9更值得推荐,以及怎么把它正确写进测试脚本里。

1. 为什么偏偏是"15":SIGTERM和SIGKILL的本质差别

1.1 信号编号是怎么一回事

在Linux和Android终端里敲一下kill -l,会输出一整屏信号列表,能看到 1) SIGHUP、2) SIGINT、9) SIGKILL、15) SIGTERM。Python的signal模块、Java里的信号处理机制,其实映射的都是同一套系统信号。对类Unix系统来说,进程之间、用户态和内核态之间,一个核心的异步通知机制就是"信号"。

信号编号是历史沿革的产物,15在绝大多数POSIX系统上固定给SIGTERM。为什么会是15而不是别的数字,纯粹是历史原因,不用深究。真正需要理解的是每个编号对应信号背后的语义。就像错误码一样,只有理解了状态码的含义,才知道怎么处理。

1.2 默认动作和可捕获性

SIGTERM(15)的默认动作是终止进程。注意"终止"不等于"强制抹杀"。进程可以安装处理器(handler)来捕获这个信号,从而执行自己的清理动作,比如关闭文件、保存状态、通知监控系统自己马上要退出,甚至可以暂时忽略它、暂不处理。

SIGKILL(9)则完全不同。它的默认动作是立即强制终止进程,而且不可捕获、不可阻塞、不可忽略。内核收到针对某个进程的SIGKILL投递后,会直接把进程从调度器中摘除,释放资源,进程没有任何机会运行用户态的收尾代码。

打一个比方:SIGTERM是公司通知你"这个月底做完交接再离开",离职流程、文件夹整理、工作交接都还能走;SIGKILL是保安直接拔掉你电脑电源、收回门禁卡,一切程序都不给你走。前者给了进程体面的退场机会,后者没有任何缓冲。

1.3 命令行里的 -15、-TERM、-SIGTERM 有什么区别

kill、pkill指定信号有三种等价写法:-15、-TERM、-SIGTERM。在Linux和较新版本的Android toybox里,三者效果一致。脚本里之所以习惯写数字-15,是因为很多精简嵌入式环境(包括部分Android ROM)对信号名称的支持不完整,数字写法最兼容,也是一种跨平台保险。

所以pkill -15 monkey的意思就是:向所有匹配"monkey"的进程发送15号信号SIGTERM。如果你希望进程有机会整理现场,就用15;如果你只希望它立刻消失、什么都不管,才考虑9。

注意:如果进程没有安装任何SIGTERM处理器,默认情况下收到SIGTERM和SIGKILL,在"程序有没有机会运行自己的清理代码"这个层面上,差别其实没有想象中那么大。真正要想让日志完整、现场可回溯,需要配合代码里的handler。这一点后面会详细展开。

2. pkill -15 monkey 匹配范围:进程名、命令行和误伤风险

2.1 进程名和命令行不是一回事

pkill默认匹配的是进程名,也就是内核里的comm字段,通常来自可执行文件名,而且会被截断到15个字符左右。这里有个非常容易翻车的细节:你在终端里执行adb shell monkey -p com.xxx 5000,写的是命令行,但设备上实际出现的进程名可能是com.android.commands.monkey,也可能是脚本包装下的临时子进程。

这种情况下,你执行pkill monkey默认按进程名匹配,就可能一个都匹配不到,或者匹配到一堆名字里带"monkey"的无关进程。想在动手之前看清楚目标,最好的方式是用pgrep,它可以同时显示进程名和完整命令行:

# 列出所有进程名或命令行中包含 monkey 的进程 pgrep -af monkey

输出里左侧是PID,右侧是完整命令行。看到实际内容,你才会知道当前设备上的monkey到底长什么样。

2.2 -f 子串匹配:命中率更高,误伤范围也更大

因为monkey测试通常是通过adb shell启动的,很多情况下进程的完整命令行里都会带上"monkey"这个字符串。于是有经验的人会改用:

pkill -15 -f monkey

-f参数让pkill基于/proc/pid/cmdline做全命令行匹配,命中率大幅提升。但它是子串匹配,只要命令行里包含"monkey"就会被视为目标。如果设备上有一个进程在跑monkey_helper.sh,或者某个守护进程的命令行里恰好带了"monkey"字样,它也会收到SIGTERM。

更隐蔽的情况是:某些统计代理程序、推送服务的进程名和业务字符串里包含"monkey",或者你自己写过一个检查工具叫check_monkey_status,这些都会被-f monkey误伤。

我的建议是:能用进程名精确匹配就不要轻易加-f;必须加-f时,尽量用更长的、唯一的模式串。比如:

# 更精确的匹配,避免误伤 pkill -15 -f "com.android.commands.monkey" pkill -15 -f "monkey -p com.business.app"

如果同一设备上有多个monkey实例并行(多模块压测、多设备联动测试),光靠进程名匹配非常危险。最好在模式串里带上测试包名、端口号等唯一标识,把批量操作限制在真正想要的目标上。

2.3 先 pgrep 后 pkill:动手前先看清敌我

我给自己定的一条操作纪律是:任何 pkill 清场之前,必须先跑一次 pgrep 确认目标范围,执行完 pkill 之后再跑一次 pgrep 验证结果。完整链路如下:

# 第一步:查看当前匹配到的所有进程,确认目标 pgrep -af monkey # 第二步:确认范围无误后,发送 SIGTERM pkill -15 -f "com.android.commands.monkey" # 第三步:等待几秒,再检查是否已经退出 sleep 3 pgrep -af monkey

如果你更习惯用PID精确清理,也可以这样做:

adb shell "kill -15 $(adb shell pidof com.android.commands.monkey)"

不过不同Android设备的pidof支持情况不完全一样,很多老设备上用ps -A | grep ... | awk '{print $2}'更稳妥。本质上,pkill越精确,环境越安全。

3. 信号发出之后:从SIGTERM到进程消亡的内核路径

3.1 没有处理器时,SIGTERM默认退出路径是怎样的

进程收到SIGTERM且没有安装handler时,内核会在进程从内核态返回用户态时执行do_exit逻辑。这个退出路径会释放进程占用的内存、关闭已打开的文件描述符、通知父进程子进程状态发生变化。相比SIGKILL的直接调度摘除,SIGTERM的默认动作更接近"进程自己正常退出"的回收路径。

但这里有个常见误解:很多人以为SIGTERM一定能保存日志。实际上,如果程序没有显式捕获SIGTERM,用户态缓冲区里的数据(比如还没flush的stdout输出)不一定会被写入文件。内核只负责系统层面的资源回收,不会帮你运行C库和业务代码里的flush逻辑。

在Android设备上,monkey进程通常是没有自定义handler的普通进程,收到SIGTERM后的默认动作就是退出。它的stdout输出管道、日志文件句柄会由内核关闭,内核侧写缓冲会尽量落盘,但如果你程序中还有自己的内存缓冲,那一部分仍然可能丢失。这就是为什么单纯依赖SIGTERM保证不了日志完整性的原因。

3.2 程序装了信号处理器之后,一切才开始有意义

如果你在monkey测试的主控脚本、runner或者监控进程里写了SIGTERM的捕获处理器,收到信号时会先执行清理代码,然后进程才退出。这样pkill -15 monkey就成了一个触发"优雅下线"的开关。

Python里通常这样写:

import signal import subprocess import sys def cleanup(signum, frame): # 先温和地停掉monkey子进程 subprocess.run(["adb", "shell", "pkill", "-15", "-f", "monkey"], stdout=subprocess.DEVNULL) # 再做日志归档等收尾工作 collect_logs() sys.exit(130) # 128 + SIGTERM def collect_logs(): print("collecting crash logs and moving to archive...", file=sys.stderr) # 拷贝、重命名、压缩崩溃堆栈等 signal.signal(signal.SIGTERM, cleanup)

在Java/Kotlin里,可以通过注册shutdown hook实现类似效果:

Runtime.getRuntime().addShutdownHook(Thread { // 关闭输出流、归档日志、释放资源 })

注意,JVM正常情况下对SIGTERM的响应就是运行shutdown hook,这对长时间运行的测试服务很有用。外部pkill -15打过来时,进程不是直接被干掉,而是先执行这些收尾逻辑。

3.3 信号可能被挂起、也可能"假成功"

有几个边界情况在自动化脚本里特别容易踩到。

第一,进程处于D状态(不可中断睡眠,比如正在等待块设备I/O完成)时,信号不会立即处理,而是排在pending队列里。此时pkill -15返回0,但进程仍然存在。

第二,进程变成僵尸(Z状态)后,信号完全无效,只能等它的父进程调用wait()回收。你向僵尸进程发送任何信号都不会改变它的状态。

第三,权限不足会导致信号投递失败。比如非root用户向root进程发送SIGTERM,会得到Operation not permitted。Android设备上常见的情况是:adb shell默认是shell用户,向system进程发信号不一定成功,需要root权限。这些边界情况决定了我们不能只发信号不管后续,必须在脚本里加上等待和验证逻辑。

4. 真实测试里我遇到过的三种信号事故

4.1 事故一:用-9清场,崩溃日志残缺

某个版本的压力测试脚本,停止monkey时用的是pkill -9 monkey。连跑一周后,我发现部分崩溃堆栈文件只有几百KB,内容戛然而止。最开始的排查方向放在抓取逻辑上,日志明明抓到了目录里,怎么会写一半就断了?后来把日志文件的生成时间、大小和monkey进程的退出时间放到一起比对,才确认是SIGKILL导致写日志线程瞬间消失,输出缓冲区没有落盘。

模拟项目X里,我们把停止流程改成"先发SIGTERM给主控进程,让主控进程的handler主动刷新输出流、关闭文件、归档崩溃现场,然后再退出",日志完整性的问题基本消失。真正的功臣不是SIGTERM信号本身,而是我们利用SIGTERM的"请求语义",在代码里写出了配套的收尾逻辑。

4.2 事故二:匹配太宽,把无关进程一起带走了

有一次设备上同时跑着monkey压测和一个推送代理,推送代理的进程名本身没问题,但它的命令行里带了"monkey"字样。我执行的pkill -15 -f monkey把两个进程一起放倒了。测试环境里推送能力临时不可用,后续大量用例超时,排查了很久才发现是-f子串匹配把无辜进程带走了。

从那以后,我在批量清理命令里养成了一个习惯:模式串尽量带完整包名或进程名,不直接在-f后面放裸关键字。比如pkill -15 -f "com.android.commands.monkey",而不是pkill -15 -f monkey。多打几个字符,避免一次大事故。

4.3 事故三:发完信号立即清目录,撞上退出窗口期

另一个自动化平台上,清理任务执行完pkill -15后马上就删除日志目录。结果日志目录被进程占用,删除失败,整个执行单元被判FAIL。排查后发现:SIGTERM只是请求进入退出流程,进程的退出需要时间。如果外部立刻去操作它占用的文件,就会撞上这个窗口期。

后面我在所有pkill之后都加了一段"优雅退出等待"逻辑:

for i in $(seq 1 15); do if ! pgrep -f "com.android.commands.monkey" > /dev/null 2>&1; then break fi sleep 1 done

超过15秒还活着,才用SIGKILL兜底。这个等待窗口后来成了所有测试清理步骤的标配。回头看,这三种事故都指向同一个结论:理解信号的语义,工程上才会少踩坑。

5. 更稳的工程做法:把SIGTERM写进测试的退场流程

5.1 一个结构化的优雅停止脚本

我现在常用的一套停止monkey逻辑,按顺序分四步:检查目标、发送SIGTERM、等待优雅退出、超时后SIGKILL兜底。

#!/bin/bash # 目标进程模式串,尽量带完整包名,别用裸monkey PATTERN="com.android.commands.monkey" # 第一步:确认目标进程是否存在 if ! adb shell pgrep -f "$PATTERN" > /dev/null 2>&1; then echo "no target process" exit 0 fi # 第二步:发送SIGTERM,给进程体面退出的机会 adb shell "pkill -15 -f '$PATTERN'" # 第三步:等待优雅退出,最多给15秒 for i in $(seq 1 15); do if ! adb shell pgrep -f "$PATTERN" > /dev/null 2>&1; then echo "process exited after ${i}s" exit 0 fi sleep 1 done # 第四步:超时未退出,兜底SIGKILL echo "graceful exit timeout, force kill" adb shell "pkill -9 -f '$PATTERN'"

这里有几个设计意图:SIGTERM是首选,等待窗口给了handler执行时间,兜底SIGKILL保证清理流程不会无限阻塞。如果你完全相信SIGTERM而不写兜底,一旦进程在handler里卡住,整个清理流程就停摆。有了兜底,最坏情况也只是损失最后几秒的缓冲数据。

5.2 让主控脚本捕获SIGTERM做真正的清理

外部pkill -15打过来时,如果目标是裸的monkey进程,它通常只会默默退出;要让15号信号发挥最大价值,主控脚本必须能响应它。Python主控脚本里通常这样做:

  1. 捕获SIGTERM;
  2. 先温和地停掉monkey子进程;
  3. 把崩溃日志、ANR trace移动到独立归档目录;
  4. 释放测试用到的端口、锁文件;
  5. 以非零状态退出,让外层识别这是"外部终止"。

一个带日志采集骨架的写法:

import signal import subprocess import sys import shutil def graceful_shutdown(signum, frame): print("received SIGTERM, start cleanup...", file=sys.stderr) # 1. 让monkey进程也优雅停止 subprocess.run(["adb", "shell", "pkill", "-15", "-f", "com.android.commands.monkey"]) # 2. 把崩溃日志保存到独立目录 try: shutil.move("/sdcard/crash_log/", "/data/archive/crash_log_backup/") except Exception: pass sys.exit(130) signal.signal(signal.SIGTERM, graceful_shutdown)

这样一来,pkill -15 monkey就不只是"杀进程",而是触发了整个测试系统的下线流程。这套思路在长时间跑Monkey、需要保留崩溃现场的场景里非常实用。

5.3 日志和临时文件的保护思路

日志完整性的根本保障,不靠信号魔法,而是让数据及时落盘。几个实操技巧:

  • 用stdbuf -oL启动monkey,让输出行及时写入日志文件。Android shell对stdbuf支持不一,能用就用,不能用就在代码里主动flush。
  • 代码里定期flush输出流,关闭文件前务必flush。不要只依赖进程退出时的隐式行为。
  • 日志不要长期留在临时目录,清理流程里先把归档拷贝完成,再删除原目录。顺序搞反是常见翻车原因。

即使最后SIGKILL兜底,损失也被限制在最近几秒的缓冲数据内,这对压力测试来说完全可接受。

5.4 一点个人体会

用pkill -15 monkey的次数多了,我越来越把信号看成一种"治理机制":SIGTERM用来给程序主动响应外部控制的机会,SIGKILL用来在治理失灵时强制兜底。在Android自动化测试这种多进程、多日志、多临时文件的场景里,稳妥的做法从来不是单选某一个信号,而是把SIGTERM、等待窗口、SIGKILL兜底组合起来,设计成完整的退出流程。

最后再分享一个小习惯:清理脚本执行完后,我会顺手把pgrep的结果记录到日志文件里留着复盘。出现问题时,回看"哪些进程收到了信号、什么时间退出的、有没有等满15秒",比凭感觉排查高效得多。理解了信号15,下次写清理脚本的时候,你会给自己多留一层余地。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 12:22:48

QT多线程实战:QThread、锁与信号槽的踩坑与对策

简介:一套面向Qt/C开发者的多线程学习资源,聚焦QThread线程类、run()重写、moveToThread对象迁移及信号槽跨线程交互等核心机制,适合需要掌握并发编程、优化后台任务处理的中级开发者。压缩包内共十三个文件,整体约2.16MB&#xf…

作者头像 李华
网站建设 2026/10/19 12:20:33

生产计划排程实战:从建模到Python排程引擎的避坑指南

简介:生产计划排程1.1.rar 是一套基于 VB6.0 开发的生产计划与排程桌面应用源码,面向希望学习数据库操作与工业排程逻辑的开发者及制造业信息化人员。系统围绕产能分析、计划导入与生产计划控制三大模块展开,通过 Access 等数据库存储产能信息…

作者头像 李华
网站建设 2026/10/19 12:19:20

IDA Pro逆向核心认知:三层真相与IDB数据库本质

简介:本资源是一份面向网络安全初学者的IDA Pro反汇编工具入门与逆向实战教学笔记,聚焦Windows PE文件分析与CrackMe解密实践,适用于零基础或刚接触逆向工程的学习者。内容系统涵盖IDA Pro工具简介、32/64位工程新建流程、IDA View/Hex View/…

作者头像 李华
网站建设 2026/10/19 12:17:07

导弹发射与爆炸烟雾火焰数据集:从标注到YOLO训练全流程

简介:这是一份面向计算机视觉与目标检测方向的导弹发射、爆炸场景烟雾火焰数据集,适合从事YOLO系列算法改进、模型训练与效果验证的研究者及学生使用。数据主要从视频中抽帧制作,共包含923幅1280720分辨率的影像,标注覆盖烟雾&…

作者头像 李华
网站建设 2026/10/19 12:17:06

iText图片转高清PDF:DPI、缩放与避坑实战指南

简介:一套基于iText的图片转PDF实现方案,面向Java开发者和需要批量生成高清PDF文档的技术人员。iText是处理PDF的主流Java库,该demo通过完整示例演示将PNG等图片以自定义DPI和缩放方式输出为高清PDF,解决默认导出清晰度不足的问题…

作者头像 李华
网站建设 2026/10/19 12:16:50

ComfyUI+AnimateDiff+ControlNet:线稿转动画的完整工作流

简介:这是一份面向AI绘画与动画生成玩家的ComfyUI工作流资源,聚焦AnimateDiff与ControlNet的Lineart控制,解决从单帧线稿到动画序列的批量生成问题。压缩包共包含27个文件:4个JSON工作流配置(时间从2023年10月到2024年…

作者头像 李华