背景
在蓝盾前端点击取消流水线任务后,任务依然会继续执行直至自然结束,无法立即终止,在一些执行时间较长的构建任务中,需等待较长时间,或者登陆构建机进行手动重启agent。
第一部分:问题现象验证
1.1 测试场景设计
在run shell插件里执行下面的测试脚本:
#!/bin/bashset -e
echo "=== 蓝盾信号测试脚本 ==="echo "当前PID: $$"echo "请在另一个终端执行以下任意命令进行测试:"echo ""echo "# 测试信号捕获:"echo "kill -TERM $$"echo "kill -INT $$"echo ""
# 信号处理函数cleanup() { echo "[$(date '+%H:%M:%S')] 捕获到信号,正在退出..." exit 0}
# 注册信号trap cleanup SIGTERM SIGINT
# 主循环(2分钟超时)end_time=$(( $(date +%s) + 120 ))count=0
while [ $(date +%s) -lt $end_time ]; do count=$((count + 1)) echo "[$(date '+%H:%M:%S')] 循环第 ${count} 次执行"
echo "执行测试命令..." sleep 5done
echo "2分钟超时,正常结束"1、会进行SIGTERM和SIGINT的捕获,如果监测到对应的信号会结束运行
2、最大等到超时阈值设置为2分钟,防止无法取消
测试步骤:
- 在BK-CI流水线中分别创建linux和mac构建环境的任务,执行上述脚本
- 运行约30秒后,在前端点击取消按钮
- 观察Agent日志和脚本行为
1.2 mac环境测试结果
前端显示日志:
=== 蓝盾信号测试脚本 ===当前PID: 17018请在另一个终端执行以下任意命令进行测试:# 测试信号捕获:kill -TERM 17018kill -INT 17018[20:19:57] 循环第 1 次执行执行测试命令...[20:20:02] 循环第 2 次执行执行测试命Start to execute the task(buildId=b-5e7077f4926647e398e82128c0f7c206|taskId=e-cfca44298b6f450c8eb34691628fe8f1)令...Cancelled by huari[20:20:07] 循环第 3 次执行执行测试命令......继续执行直到2分钟超时...1.3 linux环境测试结果
前端显示日志:
=== 蓝盾信号测试脚本 ===当前PID: 877012[21:43:43] 循环第 1 次执行执行测试命令...[21:43:48] 循环第 2 次执行执行测试命令...[21:43:53] 循环第 3 次执行执行测试命令...Cancelled by huari[21:43:58] 循环第 4 次执行执行测试命令.../root/bk-agent/build_tmp/devops_script_user_6992019006041720915.sh: line 28: 878285 Killed sleep 5
Fail to run the plugin because exit code not equal 0第二部分:mac环境手动测试验证
依然执行run shell插件的task,然后在构建机上手动发送结束信号给task的进程,观察是否可以争取捕捉信号。
kill -TERM 19987kill -INT 19987结果:
=== 蓝盾信号测试脚本 ===当前PID: 19987请在另一个终端执行以下任意命令进行测试:# 测试信号捕获:kill -TERM 19987kill -INT 19987[21:17:20] 循环第 1 次执行执行测试命令...[21:17:25] 循环第 2 次执行执行测试命令...[21:17:30] 循环第 3 次执行执行测试命令...[21:17:35] 捕获到信号,正在退出...备注:执行kill -INT 19987无法捕获到信号,无法正常退出
结论:通过蓝盾流水线的run shell插件执行的task,可以正确捕捉SIGTERM信号
第三部分:mac环境Agent日志深度分析
3.1 任务注册与启动
[23 十二月 2025;20:19:57.394][main] INFO c.t.devops.worker.common.Runner:loopPickup:212 - Start to execute the task(buildId=b-5e7077f4926647e398e82128c0f7c206|vmSeqId=1|status=DO|taskId=e-cfca44298b6f450c8eb34691628fe8f1|name=Shell ScriptstepId=null|type=linuxScript|paramSize=14|buildVarSize=59)[23 十二月 2025;20:19:57.428][pool-4-thread-1] INFO c.t.d.w.c.task.script.ScriptTask:execute:89 - Start to execute the script task(SHELL) (#!/bin/bashset -e# 此处省略打印的整个脚本内容
[23 十二月 2025;20:19:57.790][Exec Stream Pumper] INFO c.t.d.w.common.logger.LoggerService:addNormalLine:257 - LogMessage(tag='e-cfca44298b6f450c8eb34691628fe8f1', subTag='null', jobId='c-0807f596697f4c87a79727347a846bac', message='=== 蓝盾信号测试脚本 ===', timestamp=1766492397790), logType=LOG, executeCount=1)[23 十二月 2025;20:19:57.793][Exec Stream Pumper] INFO c.t.d.w.common.logger.LoggerService:addNormalLine:257 - LogMessage(tag='e-cfca44298b6f450c8eb34691628fe8f1', subTag='null', jobId='c-0807f596697f4c87a79727347a846bac', message='当前PID: 17018', timestamp=1766492397793), logType=LOG, executeCount=1)3.2 心跳机制与取消指令
[23 十二月 2025;20:20:09.218][Thread-4] INFO c.t.d.w.common.heartbeat.Heartbeat:run:121 - Heartbeat cancel build:b-5e7077f4926647e398e82128c0f7c206,heartBeatInfo:HeartBeatInfo(projectId=demo, buildId=b-5e7077f4926647e398e82128c0f7c206, vmSeqId=1, cancelTaskIds=[e-cfca44298b6f450c8eb34691628fe8f1])关键点: 取消指令在任务开始后12秒到达,任务ID完全匹配。
3.3 心跳持续进行
[20:20:11.261] [20:20:13.329] [20:20:15.374] ... 心跳持续每2秒一次说明: Agent在接收取消指令后仍在正常运行。
3.4 循环继续进行
[20:20:12.872] [20:20:17.895] [20:20:22.921] ... 心跳持续每5秒一次第四部分:linux环境Agent日志深度分析
4.1 任务正常执行
[25 Dec 2025;00:00:00.348][Exec Stream Pumper] INFO c.t.d.w.common.logger.LoggerService:addNormalLine:257 - LogMessage(tag='e-4d0ea85df62140b7a422c7ca8365a478', subTag='null', jobId='c-ae21e9e3600f4117a704ddc57e8921fa', message='[00:00:00] 循环第 13 次执行', timestamp=1766592000348), logType=LOG, executeCount=1)[25 Dec 2025;00:00:00.353][Exec Stream Pumper] INFO c.t.d.w.common.logger.LoggerService:addNormalLine:257 - LogMessage(tag='e-4d0ea85df62140b7a422c7ca8365a478', subTag='null', jobId='c-ae21e9e3600f4117a704ddc57e8921fa', message='执行测试命令...', timestamp=1766592000353), logType=LOG, executeCount=1)4.2 心跳检测与取消指令
[25 Dec 2025;00:00:01.000][Thread-4] INFO c.t.d.w.common.heartbeat.Heartbeat:run:121 - Heartbeat cancel build:b-3d54c10ccf1244ffb0a8d3eb56f93167,heartBeatInfo:HeartBeatInfo(projectId=demo, buildId=b-3d54c10ccf1244ffb0a8d3eb56f93167, vmSeqId=2, cancelTaskIds=[e-4d0ea85df62140b7a422c7ca8365a478])心跳响应包含取消指令,要求取消特定任务 e-4d0ea85df62140b7a422c7ca8365a478
4.3 进程终止
[25 Dec 2025;00:00:01.082][Thread-4] INFO com.tencent.process.BkProcessTree:log:90 - Recursively killing pid=1559839[25 Dec 2025;00:00:01.082][Thread-4] INFO com.tencent.process.BkProcessTree:log:90 - Killing pid=1559839[25 Dec 2025;00:00:01.083][Thread-4] INFO com.tencent.process.BkProcessTree:log:90 - Killing pid=1559839[25 Dec 2025;00:00:01.083][Exec Stream Pumper] INFO c.t.d.w.common.logger.LoggerService:addNormalLine:257 - LogMessage(tag='e-4d0ea85df62140b7a422c7ca8365a478', subTag='null', jobId='c-ae21e9e3600f4117a704ddc57e8921fa', message='##[error]/root/bk-agent/build_tmp/devops_script_user_2421826660222993218.sh: line 28: 1559839 Killed sleep 5', timestamp=1766592001083), logType=ERROR, executeCount=1)- 00:00:01.082 - 开始递归杀死进程PID 1559839
- 00:00:01.083 - 进程被强制终止,脚本第28行的
sleep 5命令被中断 - 退出码为137(SIGKILL信号)
4.4 异常处理
[25 Dec 2025;00:00:01.085][pool-4-thread-1] WARN c.t.d.w.c.utils.CommandLineUtils:execute:156 -com.tencent.devops.common.api.exception.TaskExecuteException: Script command execution failed with exit code(137)Error message tracking:/root/bk-agent/build_tmp/devops_script_user_2421826660222993218.sh: line 28: 1559839 Killed sleep 5
at com.tencent.devops.worker.common.utils.CommandLineUtils.execute(CommandLineUtils.kt:146) at com.tencent.devops.worker.common.utils.CommandLineUtils.execute$default(CommandLineUtils.kt:55) at com.tencent.devops.worker.common.utils.ShellUtil.executeUnixCommand(ShellUtil.kt:232) at com.tencent.devops.worker.common.utils.ShellUtil.execute(ShellUtil.kt:101) at com.tencent.devops.worker.common.utils.ShellUtil.execute$default(ShellUtil.kt:87) at com.tencent.devops.worker.common.task.script.shell.CommandShellImpl.execute(CommandShellImpl.kt:61) at com.tencent.devops.worker.common.task.script.ScriptTask.execute(ScriptTask.kt:105) at com.tencent.devops.worker.common.task.ITask.run(ITask.kt:90) at com.tencent.devops.worker.common.task.TaskDaemon.call(TaskDaemon.kt:55) at com.tencent.devops.worker.common.task.TaskDaemon.call(TaskDaemon.kt:47) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:750)[25 Dec 2025;00:00:01.086][pool-4-thread-1] INFO c.t.d.w.common.logger.LoggerService:addNormalLine:257 - LogMessage(tag='e-4d0ea85df62140b7a422c7ca8365a478', subTag='null', jobId='c-ae21e9e3600f4117a704ddc57e8921fa', message='##[error] ', timestamp=1766592001086), logType=ERROR, executeCount=1)[25 Dec 2025;00:00:01.086][pool-4-thread-1] INFO c.t.d.w.common.logger.LoggerService:addNormalLine:257 - LogMessage(tag='e-4d0ea85df62140b7a422c7ca8365a478', subTag='null', jobId='c-ae21e9e3600f4117a704ddc57e8921fa', message='Fail to run the plugin because exit code not equal 0', timestamp=1766592001086), logType=LOG, executeCount=1)- 00:00:01.085-01.089 - 任务因退出码137失败,抛出TaskExecuteException
- 错误信息显示脚本被外部强制终止
4.5 清理与关闭
- 00:00:01.090-01.119 - 上传5条日志到服务端
- 00:00:01.182-01.339 - 报告任务完成,清理临时文件
- 00:00:01.387 - 释放Agent运行配额
- 00:00:04.158-04.229 - 清理Agent进程树,杀死父进程1554738
第五部分:心跳取消流程
本文使用的是v2.0.0-beta.32版本查看代码,我们生产环境使用的是v2.0.0-beta.34,但是该版本的release已经被官方删除了。
5.1 入口
这里的流程是,会每次检查心跳信息,如果心跳信息里关闭进程ID列表(cancelTaskIds)不为空,则会启动一个线程调用KillCancelTaskProcessRunnable,来进行关闭操作。
@Synchronizedfun start(jobTimeoutMills: Long = TimeUnit.MINUTES.toMillis(900), executeCount: Int = 1) { if (running) { logger.warn("The heartbeat task already started") return } var failCnt = 0 executor.scheduleWithFixedDelay({ if (running) { try { logger.info("Start to do the heartbeat") val heartBeatInfo = EngineService.heartbeat(executeCount) val cancelTaskIds = heartBeatInfo.cancelTaskIds if (!cancelTaskIds.isNullOrEmpty()) { // 启动线程杀掉取消任务对应的进程 Thread(KillCancelTaskProcessRunnable(heartBeatInfo)).start() } failCnt = 0 } catch (e: Exception) { logger.warn("Fail to do the heartbeat", e) if (e is RemoteServiceException) { handleRemoteServiceException(e) } failCnt++ if (failCnt >= EXIT_AFTER_FAILURE) { logger.error("Heartbeat has been failed for $failCnt times, worker exit") exitProcess(-1) } } } }, 10, 2, TimeUnit.SECONDS)
/* #2043 由worker-agent.jar 运行时进行自监控,当达到Job超时时,自行上报错误信息并结束构建 */ executor.scheduleWithFixedDelay({ if (running) { LoggerService.addErrorLine("Job timout: ${TimeUnit.MILLISECONDS.toMinutes(jobTimeoutMills)}min") EngineService.timeout() exitProcess(99) } }, jobTimeoutMills, jobTimeoutMills, TimeUnit.MILLISECONDS) running = true}5.2 关闭任务:KillCancelTaskProcessRunnable
实现了Runnable接口,用于异步处理取消任务的操作
进程层面:通过killProcessTree终止系统进程,简单理解为用来关闭执行构建任务进程拉起的进程树里面的所有子进程。
应用层面:通过shutdownNow停止Java线程池中的任务,简单理解为用来关闭执行构建任务进程本身。
5.2.1日志记录
logger.info("Heartbeat cancel build:$buildId,heartBeatInfo:$heartBeatInfo")记录取消任务的构建ID和心跳信息,便于调试和问题追踪
5.2.2 终止进程树
KillBuildProcessTree.killProcessTree( projectId = heartBeatInfo.projectId, buildId = buildId, vmSeqId = heartBeatInfo.vmSeqId, taskIds = cancelTaskIds, forceFlag = true)调用KillBuildProcessTree工具类,强制终止与取消任务相关的所有进程
- forceFlag = true:表示强制终止,不给进程优雅退出的机会
5.2.3 停止任务执行器
if (!cancelTaskIds.isNullOrEmpty()) { val taskExecutorMap = TaskExecutorCache.getAllPresent(cancelTaskIds) taskExecutorMap?.forEach { taskId, executor -> logger.info("Heartbeat taskId[$taskId] executor shutdownNow") executor.shutdownNow() TaskExecutorCache.invalidate(taskId) }}从缓存中获取任务执行器并停止:
- 从
TaskExecutorCache获取所有相关任务的执行器 - 对每个执行器调用
shutdownNow()立即停止 - 从缓存中移除对应的任务执行器
5.3 killProcessTree:关闭构建任务拉起的进程树
这是一个用于终止构建相关进程树的核心工具函数,专门用于清理构建过程中产生的子进程。
5.3.1 获取当前进程ID
val currentProcessId = if (AgentEnv.getOS() == OSType.WINDOWS) { getCurrentPID() // Windows: 通过ManagementFactory获取} else { getUnixPID() // Unix/Linux: 通过shell获取父进程ID}- 跨平台支持:针对不同操作系统使用不同的方法获取当前进程ID
- 安全检查:如果获取失败(PID <= 0),直接返回空列表,避免误杀进程
5.3.2 遍历进程树
val processTree = BkProcessTree.get()val processTreeIterator = processTree.iterator()- 使用
BkProcessTree工具类获取系统进程树 - 遍历所有进程进行检查
5.3.3 进程过滤逻辑
对每个进程进行多层过滤:
5.3.3.1 获取环境变量
try { envVars = osProcess.environmentVariables} catch (ignore: Throwable) { continue // 获取失败则跳过}5.3.3.2 保护机制检查
val dontKillProcessTree = envVars["DEVOPS_DONT_KILL_PROCESS_TREE"]if ("true".equals(dontKillProcessTree, ignoreCase = true)) { logger.info("DEVOPS_DONT_KILL_PROCESS_TREE is true, skip") continue}- 特殊标记保护:如果进程设置了
DEVOPS_DONT_KILL_PROCESS_TREE=true则跳过 - 这允许某些关键进程避免被意外终止
5.3.3.3 排除自身
if (osProcess.pid == currentProcessId) { continue}避免终止当前运行的进程自身
5.3.3.4 匹配条件判断
val envProjectId = envVars["PROJECT_ID"]val envBuildId = envVars["BUILD_ID"]val envVmSeqId = envVars["VM_SEQ_ID"]var flag = projectId.equals(envProjectId, ignoreCase = true) && buildId.equals(envBuildId, ignoreCase = true) && vmSeqId.equals(envVmSeqId, ignoreCase = true)核心匹配逻辑:
- 项目ID匹配
- 构建ID匹配
- 虚拟机序列ID匹配
5.3.3.5 可选的任务ID过滤
if (!taskIds.isNullOrEmpty()) { val envTaskId = envVars[PIPELINE_ELEMENT_ID] flag = flag && taskIds.contains(envTaskId)}- 当指定了
taskIds时,只终止特定任务的进程 - 这是从心跳检测中取消特定任务的关键
5.3.4 终止进程
if (flag) { osProcess.killRecursively(forceFlag) // 递归终止子进程 osProcess.kill(forceFlag) // 终止当前进程 killedProcessIds.add(osProcess.pid) // 记录已终止的PID}- 双重终止保证:先递归终止子进程,再终止当前进程
- forceFlag控制:决定是否强制终止(不给进程清理的机会)
第六部分:根因定位
6.1 前置猜测
结合蓝盾的代码进行分析,依然猜测问题在killProcessTree里面因为环境变量的问题,导致递归删除构建任务子进程的逻辑无法被执行到。
if (flag) { osProcess.killRecursively(forceFlag) // 递归终止子进程 osProcess.kill(forceFlag) // 终止当前进程 killedProcessIds.add(osProcess.pid) // 记录已终止的PID}6.2 环境变量获取方式
6.2.1 linux获取环境变量实现
public synchronized EnvVars getEnvironmentVariables() { if (this.envVars == null) { this.envVars = new EnvVars();
try { byte[] environ = Linux.this.readFileToByteArray(this.getFile("environ")); int pos = 0;
for (int i = 0; i < environ.length; ++i) { byte b = environ[i]; if (b == 0) { this.envVars.addLine(new String(environ, pos, i - pos)); pos = i + 1; } } } catch (IOException var5) { log(var5.getMessage()); }
} return this.envVars;}Linux 获取方式:直接读取 /proc/[pid]/environ 文件,该文件包含了进程的所有环境变量,以空字符分隔。
6.2.2 mac获取环境变量实现
private void parse() { try { this.arguments = new ArrayList<>(); this.envVars = new EnvVars();
// 1. 获取系统参数最大长度 IntByReference argmaxRef = new IntByReference(0); IntByReference size = new IntByReference(BkProcessTree.Darwin.sizeOfInt); if (GNUCLibrary.LIBC.sysctl(new int[]{1, 8}, 2, argmaxRef.getPointer(), size, Pointer.NULL, defaultSize) != 0) { throw new IOException("Failed to get kernl.argmax"); }
int argmax = argmaxRef.getValue();
// 2. 使用 sysctl 获取进程参数和环境变量 StringArrayMemory m = new StringArrayMemory((long)argmax); size.setValue(argmax);
// kern.procargs2 调用获取进程信息 if (GNUCLibrary.LIBC.sysctl(new int[]{1, 49, this.pid}, 3, m, size, Pointer.NULL, defaultSize) != 0) { throw new IOException("Failed to obtain ken.procargs2"); }
// 3. 解析数据... int argc = m.readInt(); // 读取参数数量 m.readString(); // 跳过可执行文件路径 m.skip0(); // 跳过对齐字节
// 读取命令行参数 for (int i = 0; i < argc; ++i) { this.arguments.add(m.readString()); }
// 4. 读取环境变量 while(m.peek() != 0) { this.envVars.addLine(m.readString()); } } catch (IOException var10) { log(var10.getMessage()); // 错误仅记录日志 }}6.2.2.1 获取 kern.argmax
// 代码执行:获取参数最大长度if (GNUCLibrary.LIBC.sysctl(new int[]{1, 8}, 2, argmaxRef.getPointer(), size, Pointer.NULL, defaultSize) != 0) { throw new IOException("Failed to get kernl.argmax: " + GNUCLibrary.LIBC.strerror(Native.getLastError()));}- OID:
{1, 8}对应kern.argmax - 作用: 获取系统参数的最大长度(用于分配缓冲区)
6.2.2.2 获取 kern.procargs2
// 代码执行:获取进程参数和环境变量if (GNUCLibrary.LIBC.sysctl(new int[]{1, 49, this.pid}, 3, m, size, Pointer.NULL, defaultSize) != 0) { throw new IOException("Failed to obtain ken.procargs2: " + GNUCLibrary.LIBC.strerror(Native.getLastError()));}- OID:
{1, 49, pid}对应kern.procargs2 - 作用: 获取指定进程的命令行参数和环境变量
6.3 验证:kern.procargs2缺失
6.3.1 kern.procargs2 验证工具
使用c语言模仿java的实现,编写一个调用kern.procargs2的工具,验证kern.procargs2的存在与否:
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/sysctl.h>#include <errno.h>#include <unistd.h>
// 打印美观的分隔线void printSeparator(const char* title) { printf("\n┌─────────────────────────────────────────────────────────────┐\n"); printf("│ %-57s │\n", title); printf("└─────────────────────────────────────────────────────────────┘\n");}
int main(int argc, char *argv[]) { int pid;
// 确定目标PID if (argc == 2) { pid = atoi(argv[1]); } else { pid = getpid(); printf("ℹ️ 未指定PID,默认解析当前进程 (PID = %d)\n", pid); }
printSeparator("开始解析进程参数"); printf("目标进程ID: %d\n", pid);
// 1. 获取 argmax (KERN_ARGMAX) int argmax; size_t argmax_size = sizeof(argmax); int mib_argmax[2] = {CTL_KERN, KERN_ARGMAX};
if (sysctl(mib_argmax, 2, &argmax, &argmax_size, NULL, 0) == -1) { perror("❌ 获取 KERN_ARGMAX 失败"); return 1; }
// 2. 获取 KERN_PROCARGS2 原始数据 char *buffer = (char*)malloc(argmax); if (!buffer) { perror("❌ 内存分配失败"); return 1; }
int mib_procargs2[3] = {CTL_KERN, KERN_PROCARGS2, pid}; size_t data_size = argmax;
if (sysctl(mib_procargs2, 3, buffer, &data_size, NULL, 0) == -1) { perror("❌ 获取进程数据失败"); if (errno == EPERM) { printf("💡 提示:尝试使用 'sudo' 获取更高权限\n"); } else if (errno == ESRCH) { printf("💡 提示:进程 %d 不存在\n", pid); } free(buffer); return 1; }
printf("✅ 数据获取成功\n"); printf(" 原始数据大小: %zu 字节\n", data_size);
// 3. 解析数据 int offset = 0;
// 3.1 解析 argc (参数个数) int argc_val = *(int*)(buffer + offset); offset += sizeof(int);
printSeparator("基本信息"); printf("• 参数个数 (argc): %d\n", argc_val);
// 3.2 获取可执行文件路径 (argv[0] 之前) printf("• 可执行文件路径: "); while (offset < data_size && buffer[offset] != '\0') { putchar(buffer[offset]); offset++; }
// 检查并跳过路径终止符 if (offset < data_size && buffer[offset] == '\0') { offset++; } else { printf("\n⚠️ 警告:路径未正常终止,解析可能出错\n"); } printf("\n");
// 3.3 关键修复:更灵活地跳过填充字节 // 不再严格假设填充全为\0或必须4字节对齐 int before_args = offset; while (offset < data_size && buffer[offset] == '\0') { offset++; } if (offset > before_args) { printf("• 跳过 %d 个填充空字节\n", offset - before_args); }
// 3.4 解析命令行参数 (增强容错) if (argc_val > 0) { printSeparator("命令行参数"); for (int i = 0; i < argc_val && offset < data_size; i++) { printf(" %2d. ", i);
int arg_start = offset; int arg_len = 0;
// 尝试读取一个参数:尽可能打印可打印字符,直到遇到\0或数据末尾 while (offset < data_size && buffer[offset] != '\0') { // 只打印可打印ASCII字符,其他显示为? unsigned char c = buffer[offset]; if (c >= 32 && c <= 126) { putchar(c); } else { printf("[0x%02x]", c); // 非打印字符显示为十六进制 } offset++; arg_len++; }
if (arg_len == 0) { printf("(空字符串)"); }
// 如果当前位置是\0,则跳过它,准备下一个参数 if (offset < data_size && buffer[offset] == '\0') { offset++; }
printf("\n"); } } else { printSeparator("命令行参数"); printf(" (无命令行参数)\n"); }
// 3.5 解析环境变量 printSeparator("环境变量"); int env_count = 0;
while (offset < data_size && buffer[offset] != '\0') { printf(" %3d. ", ++env_count);
// 打印变量名(到'='为止) while (offset < data_size && buffer[offset] != '\0' && buffer[offset] != '=') { putchar(buffer[offset]); offset++; }
// 打印'='和变量值 if (offset < data_size && buffer[offset] == '=') { putchar('='); offset++;
while (offset < data_size && buffer[offset] != '\0') { putchar(buffer[offset]); offset++; } } else { // 没有'='的环境变量 while (offset < data_size && buffer[offset] != '\0') { putchar(buffer[offset]); offset++; } }
// 跳过当前环境变量的终止符 if (offset < data_size && buffer[offset] == '\0') { offset++; }
printf("\n"); }
if (env_count == 0) { printf(" (无环境变量或数据解析完毕)\n"); }
printSeparator("解析完成"); printf("✅ 成功解析 %d 个参数和 %d 个环境变量\n", argc_val, env_count); printf("📊 总计处理数据: %zu 字节,最终偏移: 0x%x\n", data_size, offset);
free(buffer); return 0;}编译工具:
gcc -o show_procargs show_procargs.c若gcc缺失,可以如此安装:xcode-select —install
使用方式:
./show_procargs pidpid 替换为蓝盾task的实际pid,pid的获取可以直接复用“1.1测试场景设计里面的脚本”
6.3.2 i5芯片构建机验证kern.procargs2
找it同学申请了一台老的macmini机器:
- macOS:Sequoia
- 版本:15.7.3
使用蓝盾插件执行1.1测试场景设计中的脚本,然后在机器上通过工具查看线程的信息(调用kern.procargs2),然后在前端执行取消,看能否正常取消。
执行工具查看蓝盾task的结果:
┌─────────────────────────────────────────────────────────────┐│ 开始解析进程参数 │└─────────────────────────────────────────────────────────────┘目标进程ID: 5080✅ 数据获取成功 原始数据大小: 100 字节
┌─────────────────────────────────────────────────────────────┐│ 基本信息 │└─────────────────────────────────────────────────────────────┘• 参数个数 (argc): 2• 可执行文件路径: /bin/bash• 跳过 6 个填充空字节
┌─────────────────────────────────────────────────────────────┐│ 命令行参数 │└─────────────────────────────────────────────────────────────┘ 0. /bin/bash 1. /Users/m200002359/bk-ci/build_tmp/devops_script4772461533909296104.sh
┌─────────────────────────────────────────────────────────────┐│ 环境变量 │└─────────────────────────────────────────────────────────────┘ (无环境变量或数据解析完毕)
┌─────────────────────────────────────────────────────────────┐│ 解析完成 │└─────────────────────────────────────────────────────────────┘✅ 成功解析 2 个参数和 0 个环境变量📊 总计处理数据: 100 字节,最终偏移: 0x64至此已可以确定,老机器上kern.procargs2存在且可以正常使用。
前端日志:
=== 蓝盾信号测试脚本 ===当前PID: 5080[17:35:43] 循环第 1 次执行执行测试命令...[17:35:48] 循环第 2 次执行执行测试命令...[17:35:53] 循环第 3 次执行执行测试命令...[17:35:58] 循环第 4 次执行执行测试命令...[17:36:03] 循环第 5 次执行执行测试命令...[17:36:08] 循环第 6 次执行执行测试命令...Cancelled by huari[17:36:13] 循环第 7 次执行执行测试命令...
Fail to run the plugin because exit code not equal 0因为kern.procargs2存在且可以正常使用,所以蓝盾agent可以正常获取到目标线程的信息,所以可以正确终结任务子进程。
6.3.3 m3芯片构建机验证kern.procargs2
使用蓝盾插件执行1.1测试场景设计中的脚本,因为已知构建任务无法正常取消,所以预期在机器上无法通过工具查看线程的信息(即无法调用kern.procargs2)。
执行工具查看蓝盾task的结果:
┌─────────────────────────────────────────────────────────────┐│ 开始解析进程参数 │└─────────────────────────────────────────────────────────────┘目标进程ID: 28855✅ 数据获取成功 原始数据大小: 96 字节
┌─────────────────────────────────────────────────────────────┐│ 基本信息 │└─────────────────────────────────────────────────────────────┘• 参数个数 (argc): 2• 可执行文件路径: /bin/bash• 跳过 6 个填充空字节
┌─────────────────────────────────────────────────────────────┐│ 命令行参数 │└─────────────────────────────────────────────────────────────┘ 0. /bin/bash 1. /Users/king/bk-agent/build_tmp/devops_script942180256175598069.sh
┌─────────────────────────────────────────────────────────────┐│ 环境变量 │└─────────────────────────────────────────────────────────────┘ (无环境变量或数据解析完毕)
┌─────────────────────────────────────────────────────────────┐│ 解析完成 │└─────────────────────────────────────────────────────────────┘✅ 成功解析 2 个参数和 0 个环境变量📊 总计处理数据: 96 字节,最终偏移: 0x60在m3芯片的macmini上,也可以正常调用kern.procargs2。
前端日志:
=== 蓝盾信号测试脚本 ===当前PID: 28855[19:09:37] 循环第 1 次执行执行测试命令...[19:09:42] 循环第 2 次执行执行测试命令...[19:09:47] 循环第 3 次执行执行测试命令...[19:09:52] 循环第 4 次执行执行测试命令...[19:09:57] 循环第 5 次执行执行测试命令...[19:10:02] 循环第 6 次执行执行测试命令...[19:10:07] 循环第 7 次执行执行测试命令...[19:10:12] 循环第 8 次执行执行测试命令...[19:10:17] 循环第 9 次执行执行测试命令...[19:10:22] 循环第 10 次执行执行测试命令...[19:10:27] 循环第 11 次执行执行测试命令...[19:10:32] 循环第 12 次执行执行测试命令...[19:10:37] 循环第 13 次执行执行测试命令...[19:10:42] 循环第 14 次执行执行测试命令...[19:10:47] 循环第 15 次执行执行测试命令...Cancelled by huari[19:10:52] 循环第 16 次执行执行测试命令...[19:10:57] 循环第 17 次执行执行测试命令...[19:11:02] 循环第 18 次执行执行测试命令...[19:11:07] 循环第 19 次执行执行测试命令...[19:11:12] 循环第 20 次执行执行测试命令...[19:11:18] 循环第 21 次执行执行测试命令...[19:11:23] 循环第 22 次执行执行测试命令...[19:11:28] 循环第 23 次执行执行测试命令...[19:11:33] 循环第 24 次执行执行测试命令...2分钟超时,正常结束目前的现状是,kern.procargs2存在且可正常调用,但是agent的task依然无法正常退出。
6.3.4 kern.procargs2结论
通过c语言调用kern.procargs2,可以验证在新老macOS机器上均存在kern.procargs2,但是老机器(i5芯片)可以正常取消任务,而新机器(m3芯片)无法正常取消任务。
所以可以判定根音并不是kern.procargs2缺失造成的。
6.4 验证:JNA库架构不兼容
目前已经可以确定根音并不是kern.procargs2缺失,而引起的获取目标进程信息失败无法退出任务线程。
6.4.1 验证工具
import java.lang.reflect.Method;import java.util.Map;import java.util.Iterator;
public class DirectTest {
// Java 8兼容的字符串重复方法(替代String.repeat()) private static String repeatString(String str, int count) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < count; i++) { sb.append(str); } return sb.toString(); }
public static void main(String[] args) throws Exception { if (args.length != 1) { System.out.println("=================================================="); System.out.println("蓝盾Agent进程环境变量读取测试 (Java 8兼容版)"); System.out.println("=================================================="); System.out.println("功能:直接调用Agent内置的BkProcessTree类,触发 kern.procargs2 系统调用。"); System.out.println("用法:java DirectTest <要检查的PID>"); System.out.println("示例:"); System.out.println(" java DirectTest $$ # 检查当前Shell进程"); System.out.println(" java DirectTest 1 # 检查系统launchd进程(需要sudo)"); System.out.println(" java DirectTest [PID] # 检查任意进程"); System.out.println("=================================================="); return; }
String targetPidStr = args[0]; System.out.println("🔍 开始检查进程 PID: " + targetPidStr); System.out.println(" 使用Agent内置类路径和Java " + System.getProperty("java.version")); System.out.println();
try { // 1. 获取 BkProcessTree 单例实例 (调用公开的静态方法) Class<?> bkProcessTreeClass = Class.forName("com.tencent.process.BkProcessTree"); Method getMethod = bkProcessTreeClass.getMethod("get"); Object processTreeInstance = getMethod.invoke(null); System.out.println("✅ 步骤1: 获取 BkProcessTree 实例成功");
// 2. 获取进程树迭代器 Method iteratorMethod = processTreeInstance.getClass().getMethod("iterator"); Iterator<?> iterator = (Iterator<?>) iteratorMethod.invoke(processTreeInstance); System.out.println("✅ 步骤2: 获取进程树迭代器成功");
// 3. 遍历查找目标PID boolean found = false; int searchCount = 0;
while (iterator.hasNext()) { Object osProcess = iterator.next(); searchCount++;
// 获取当前进程的PID Method getPidMethod = osProcess.getClass().getMethod("getPid"); int pid = (int) getPidMethod.invoke(osProcess);
if (String.valueOf(pid).equals(targetPidStr)) { found = true; System.out.println("\n🎯 找到目标进程!"); System.out.println(" 进程PID: " + pid); System.out.println(" 已扫描进程数: " + searchCount); System.out.println("\n⚠️ 即将触发 kern.procargs2 系统调用...");
// 4. 关键步骤:获取环境变量 (这里会调用 kern.procargs2) try { Method getEnvMethod = osProcess.getClass().getMethod("getEnvironmentVariables"); // 修复:设置方法为可访问,绕过Java访问控制 getEnvMethod.setAccessible(true);
System.out.println(" ⏳ 正在调用 getEnvironmentVariables()..."); long startTime = System.currentTimeMillis(); Object envVars = getEnvMethod.invoke(osProcess); long endTime = System.currentTimeMillis();
System.out.println(" ⏱️ 系统调用耗时: " + (endTime - startTime) + "ms");
// 5. 处理返回的环境变量 if (envVars == null) { System.out.println("\n❌ 结果:getEnvironmentVariables() 返回了 null"); System.out.println(" 这意味着 kern.procargs2 可能返回了空数据。"); } else if (envVars instanceof Map) { Map<?, ?> envMap = (Map<?, ?>) envVars; System.out.println("\n✅ 结果:成功获取到环境变量!"); System.out.println(" 环境变量总数: " + envMap.size() + " 个");
// 重点显示蓝盾关心的变量 System.out.println("\n📋 蓝盾关键环境变量检查:"); System.out.println(" " + repeatString("-", 50));
String[] blueKingKeys = { "PROJECT_ID", "BUILD_ID", "VM_SEQ_ID", "PIPELINE_ELEMENT_ID", "DEVOPS_DONT_KILL_PROCESS_TREE" };
boolean foundBlueKingVar = false; for (String key : blueKingKeys) { if (envMap.containsKey(key)) { System.out.printf(" %-30s = %s%n", key, envMap.get(key)); foundBlueKingVar = true; } }
if (!foundBlueKingVar) { System.out.println(" ⚠️ 未找到蓝盾关键环境变量"); if (envMap.size() > 0) { System.out.println(" 前5个环境变量示例:"); int count = 0; for (Object key : envMap.keySet()) { if (count >= 5) break; System.out.printf(" %-30s = %s%n", key, envMap.get(key)); count++; } } } System.out.println(" " + repeatString("-", 50));
} else { System.out.println("\n⚠️ 结果:getEnvironmentVariables() 返回了非Map对象"); System.out.println(" 返回类型: " + envVars.getClass().getName()); System.out.println(" 返回值: " + envVars); }
} catch (Exception e) { System.out.println("\n❌ 关键错误:获取环境变量时发生异常!"); System.out.println(" 这很可能意味着 kern.procargs2 系统调用失败"); System.out.println("\n📋 异常详情:"); System.out.println(" 异常类型: " + e.getClass().getName());
// 提取根本原因 Throwable cause = e; while (cause.getCause() != null) { cause = cause.getCause(); }
System.out.println(" 根本原因: " + cause.getClass().getName()); System.out.println(" 错误信息: " + cause.getMessage());
// 如果是IOException,很可能是sysctl调用失败 if (cause instanceof java.io.IOException) { System.out.println("\n💡 诊断:这是 kern.procargs2 系统调用失败的直接证据!"); System.out.println(" 在 macOS 上,通常意味着:"); System.out.println(" 1. 进程不存在或已退出"); System.out.println(" 2. 权限不足(某些系统进程需要root权限)"); System.out.println(" 3. macOS版本不兼容(特别是M1/M3芯片的Mac)"); System.out.println(" 4. kern.procargs2 接口已被限制或废弃"); }
// 打印部分堆栈跟踪(只显示关键部分) System.out.println("\n🔍 异常堆栈(关键部分):"); StackTraceElement[] stack = cause.getStackTrace(); for (int i = 0; i < Math.min(stack.length, 5); i++) { System.out.println(" at " + stack[i]); } }
break; // 找到目标进程后停止遍历 } }
if (!found) { System.out.println("\n⚠️ 未在进程树中找到PID为 " + targetPidStr + " 的进程"); System.out.println(" 已扫描进程数: " + searchCount); System.out.println(" 可能的原因:"); System.out.println(" 1. 进程确实不存在"); System.out.println(" 2. 进程是内核线程或特殊进程"); System.out.println(" 3. BkProcessTree 的进程列表不完整"); }
} catch (Exception e) { System.out.println("\n❌ 程序初始化失败:"); e.printStackTrace(); }
System.out.println("\n=================================================="); System.out.println("测试完成"); System.out.println("=================================================="); }}使用方法:
# 1. 设置 JAVA_HOME,指向agent本地的JDK目录(先进入蓝盾agent的安装目录!!!)export JAVA_HOME=./jdk/Contents/Home
# 2. 将 JDK 的 bin 目录添加到 PATH 的最前面,确保系统优先使用它export PATH=$JAVA_HOME/bin:$PATH
# 3. 验证配置是否生效:检查 javac 和 java 的版本javac -versionjava -version
# 4. 编译(确保在 bk-agent 目录)javac -cp "./worker-agent.jar" DirectTest.java
# 5. 运行测试java -cp ".:./worker-agent.jar" DirectTest <PID>6.4.2 i5芯片构建机验证jna兼容性
验证结果:
🔍 开始检查进程 PID: 7797 使用Agent内置类路径和Java 1.8.0_372
✅ 步骤1: 获取 BkProcessTree 实例成功✅ 步骤2: 获取进程树迭代器成功
🎯 找到目标进程! 进程PID: 7797 已扫描进程数: 513
⚠️ 即将触发 kern.procargs2 系统调用... ⏳ 正在调用 getEnvironmentVariables()... ⏱️ 系统调用耗时: 6ms
✅ 结果:成功获取到环境变量! 环境变量总数: 0 个
📋 蓝盾关键环境变量检查: -------------------------------------------------- ⚠️ 未找到蓝盾关键环境变量 --------------------------------------------------
==================================================测试完成==================================================因为查看的进程pid的原因,没有获取到变量,但是可以看见完整链路是调用成功的.
即:java(test)->BkProcessTree(agent的jar包)->jna->kern.procargs2
6.4.5 m3芯片构建机验证jna兼容性
验证结果:
🔍 开始检查进程 PID: 30573 使用Agent内置类路径和Java 1.8.0_352
❌ 程序初始化失败:java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at DirectTest.main(DirectTest.java:40)Caused by: java.lang.UnsatisfiedLinkError: /private/var/folders/20/21dxn7fj2c9bzcg_7xbt5lbw0000gp/T/jna-3292055/jna2398815628179584771.tmp: dlopen(/private/var/folders/20/21dxn7fj2c9bzcg_7xbt5lbw0000gp/T/jna-3292055/jna2398815628179584771.tmp, 0x0001): tried: '/private/var/folders/20/21dxn7fj2c9bzcg_7xbt5lbw0000gp/T/jna-3292055/jna2398815628179584771.tmp' (fat file, but missing compatible architecture (have 'i386,x86_64,unknown', need 'arm64e' or 'arm64')), '/System/Volumes/Preboot/Cryptexes/OS/private/var/folders/20/21dxn7fj2c9bzcg_7xbt5lbw0000gp/T/jna-3292055/jna2398815628179584771.tmp' (no such file), '/private/var/folders/20/21dxn7fj2c9bzcg_7xbt5lbw0000gp/T/jna-3292055/jna2398815628179584771.tmp' (fat file, but missing compatible architecture (have 'i386,x86_64,unknown', need 'arm64e' or 'arm64')) at java.lang.ClassLoader$NativeLibrary.load(Native Method) at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1934) at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1817) at java.lang.Runtime.load0(Runtime.java:782) at java.lang.System.load(System.java:1100) at com.sun.jna.Native.loadNativeDispatchLibraryFromClasspath(Native.java:761) at com.sun.jna.Native.loadNativeDispatchLibrary(Native.java:736) at com.sun.jna.Native.<clinit>(Native.java:131) at com.tencent.process.BkProcessTree$Darwin.<clinit>(BkProcessTree.java:242) at com.tencent.process.BkProcessTree.get(BkProcessTree.java:134) ... 5 more
==================================================测试完成==================================================错误日志里很清晰的注明了:java.lang.UnsatisfiedLinkError
可以确定问题是:蓝盾Agent worker-agent.jar 中内置的JNA本地库是仅适用于Intel芯片(x86_64)的版本,无法在ARM64架构的M系列芯片上加载。
官方issue的标题修复arm64mac进程无法清理的问题也在重点强调arm64,因为在v2.0版本的agent中,内置的JNA本地库是仅适用于Intel芯片(x86_64)的版本,无法在ARM64架构的M系列芯片上加载
6.5 i5芯片和m3芯片构建机日志对比
6.5.1 i5芯片构建机取消任务日志
心跳检测到取消指令:
[10:15:38.690][Thread-4] Heartbeat cancel build:b-0bfad0c8103045f2866485bade09cda7,heartBeatInfo:HeartBeatInfo(... cancelTaskIds=[e-04346788b42848d491e8d7ec2dfaa61b])心跳机制接收到取消任务的指令,任务ID在取消列表中
进程终止尝试:
[10:15:38.991] Found 678 processes[10:15:38.995 - 10:15:39.237] 持续出现 "Failed to obtain ken.procargs2: Invalid argument"系统尝试获取进程信息,但在macOS系统上遇到权限/兼容性问题,无法正常获取进程参数
执行器被强制关闭:
[10:15:39.057] Heartbeat taskId[e-04346788b42848d491e8d7ec2dfaa61b] executor shutdownNow脚本执行失败:
[10:15:39.060] Script command execution failed with exit code(-559038737)[10:15:39.060] Fail to run the plugin because exit code not equal 0[10:15:39.061] Fail to run the script task关键信息:
- 退出码:
-559038737(十六进制:0xDEADBEEF) - 这是一个特殊的退出码,通常表示进程被强制终止
6.5.2 m3芯片构建机取消任务日志
取消指令接收:
[05 一月 2026;01:02:20.023][Thread-4] INFO c.t.d.w.common.heartbeat.Heartbeat:run:121 - Heartbeat cancel build:b-026925439f6244d19d27f74bd8983a5f,heartBeatInfo:HeartBeatInfo(projectId=demo, buildId=b-026925439f6244d19d27f74bd8983a5f, vmSeqId=1, cancelTaskIds=[e-04346788b42848d491e8d7ec2dfaa61b])心跳线程接收到明确的取消指令,指定要取消的任务ID为 e-04346788b42848d491e8d7ec2dfaa61b。
脚本继续执行:
脚本任务继续正常执行并输出日志:
[01:02:21.269] 循环第 3 次执行[01:02:21.271] 执行测试命令...[01:02:26.300] 循环第 4 次执行[01:02:26.303] 执行测试命令......[01:03:56.727] 循环第 22 次执行[01:03:56.728] 执行测试命令...接收到取消指令后,脚本进程并未立即停止,继续执行
6.6 i5 VS m3 故障现象对比分析
| 对比维度 | i5 (Intel) Mac | M3 (Apple Silicon) Mac | 分析与结论 |
|---|---|---|---|
| 流程触发 | ✅ 正常:Heartbeat cancel build 日志出现。 | ✅ 正常:Heartbeat cancel build 日志出现。 | 取消指令的接收和线程创建逻辑正常,问题出在后续执行。 |
进程树初始化 (BkProcessTree.get()) | ✅ 成功:能打印 Found 678 processes。 | ❌ 完全失败:因 UnsatisfiedLinkError (JNA架构不兼容) 导致线程直接崩溃。 | 根本区别:M3上卡在更前置的库加载阶段;i5上能通过此阶段。 |
核心系统调用 (kern.procargs2) | 持续出现 Failed to obtain ken.procargs2: Invalid argument。但以sudo模式安装agent,则会仅出现一次!!! | (未到达此步) | 共同瓶颈:即使进程树可用,在m系列芯片构建机上,可能也需要走兜底逻辑 |
| 任务取消结果 | ⚠️ 部分成功:任务执行器被 shutdownNow(),脚本以 0xDEADBEEF 被强制终止。 | ❌ 完全失败:任务继续执行直至完成,无任何终止行为。 | i5上因接口失败,触发了强制终止的兜底逻辑;M3上因库加载崩溃,兜底逻辑都未能触发。 |
| 根本原因 | 可能因为macOS系统限制或权限问题,导致调用 kern.procargs2 接口调用获取线程信息失败。 | 双重问题: 1. JNA库架构不兼容 (主要)。 2. kern.procargs2 接口调用问题。 | M3的问题更底层、更致命,导致功能完全瘫痪。 |
6.7 问题总结
根因总结 蓝盾Agent在macOS M系列芯片上无法取消构建任务,主要原因为:
- JNA库架构不兼容:Agent内置的JNA本地库仅为x86_64版本,无法在ARM64架构的M芯片上加载,导致进程树功能完全失效。
- macOS系统限制或权限问题:依赖的
kern.procargs2系统调用在较新macOS版本中可能失败,影响进程信息获取。
结果对比
- Intel Mac(i5):JNA可加载,但
kern.procargs2调用失败,触发强制终止兜底逻辑,任务可被取消。 - Apple Silicon Mac(M3):JNA加载失败,取消流程完全中断,任务无法终止。
结论:问题核心是ARM64架构兼容性缺失,需更新JNA库为通用或ARM64版本。
第八部分:官方修复
官方已经在正式版本v3.0.0,rc版本v3.0.0-rc.1进行过修复:
- issues:https://github.com/TencentBlueKing/bk-ci/issues/10252
- pr:https://github.com/TencentBlueKing/bk-ci/pull/10297
- changelog:
在升级蓝鲸7.2后,bk-ci的版本将从v3.0.11.beta5开始,该问题将解决:https://bk.tencent.com/docs/markdown/ZH/DeploymentGuides/7.2/update.md
部分信息可能已经过时









