尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

rttsh:脚本化J-Link RTT调试工具,支持Lua与CI集成

rttsh:脚本化J-Link RTT调试工具,支持Lua与CI集成 嵌入式调试这件事最让人抓狂的往往不是代码写错了而是你明明知道问题在哪却没法方便地看到变量值。J-Link RTT 算是解决了看的问题但原生的 RTT Viewer 和 RTT Client 在批量操作、脚本化、CI 集成这些场景下就显得力不从心了。我最近花了不少时间写了一个叫 rttsh 的命令行工具核心目标就一个把 RTT 的读写能力变成可脚本化、可管道化、可自动化的一等公民。它支持 Lua 脚本驱动能直接对接 CI 流水线也能在 AI 辅助调试的场景里当手和眼用。这篇文章我会把整个工具的设计思路、核心实现、踩过的坑和实际使用心得完整拆一遍适合正在做嵌入式调试自动化、想摆脱 GUI 点点点的朋友参考。1. 为什么原生 RTT 工具在自动化场景下不够用1.1 RTT Viewer 的交互模式与批量需求的根本矛盾J-Link RTT 的官方工具链里RTT Viewer 是最常用的一个。它的工作模式很直观连上 J-Link选好目标芯片打开 RTT 控制块然后就能在终端窗口里看到目标板通过SEGGER_RTT_printf输出的日志也能手动输入命令发回去。对于我盯着看日志这种场景它够用。但问题在于一旦你想做下面这些事RTT Viewer 就完全帮不上忙了每隔 100ms 自动往目标板发一条查询命令把返回结果存成 CSV在 CI 里跑完固件烧录后自动抓取 30 秒 RTT 日志检查有没有ASSERT关键字用脚本控制目标板进入不同工作模式采集每种模式下的功耗数据让 AI 助手通过命令行接口读取当前变量值辅助定位问题这些需求的共同点是需要程序化地控制 RTT 的读写节奏而不是靠人手动操作。RTT Viewer 是个 GUI 程序没有提供命令行接口也没有脚本扩展能力你没法在 shell 脚本里调用它。有人可能会说那用 J-Link 的 SDK 自己写个 C 程序不就行了可以但成本很高。你要处理 J-Link DLL 的加载、RTT 控制块的搜索、缓冲区读写、连接管理等一堆底层细节写出来还得自己维护。对于我就想快速搞个自动化脚本的需求来说这个投入产出比太低了。1.2 RTT Client 的能力边界在哪里RTT Client 比 RTT Viewer 轻量一些它是个纯命令行程序可以通过 telnet 方式连接 RTT 端口。这看起来好像能脚本化了实际用起来还是有不少限制。首先RTT Client 的 telnet 接口是单向的文本流你没法方便地区分这次读取的数据和上次残留的数据也没有结构化的返回格式。其次它的连接管理比较脆弱目标板复位或者 J-Link 重新枚举之后连接经常需要手动重连。再者它不支持 Lua 或其他脚本语言嵌入你没法在工具内部做复杂的逻辑判断只能在外层用 shell 拼凑很容易写出脆弱的脚本。我实际试过用 RTT Client expect 脚本做自动化结论是能跑但维护起来很痛苦。expect 对二进制数据的处理很别扭超时控制也不够精细稍微复杂一点的交互逻辑就会变成一堆难以阅读的 spawn/send/expect 嵌套。1.3 脚本化 RTT 工具需要具备哪些核心能力基于上面的分析我梳理了一个合格的脚本化 RTT 工具应该具备的能力能力维度具体要求原生工具是否满足连接管理自动搜索 RTT 控制块支持指定芯片型号和接口速度部分满足读写接口提供结构化的读/写 API支持超时和重试不满足脚本嵌入支持 Lua 等脚本语言可在工具内做逻辑判断不满足管道友好输出可重定向到文件或其他程序支持 stdin 输入部分满足CI 集成非交互模式运行返回码可判断成功失败不满足数据导出支持 CSV、JSON 等结构化格式导出不满足rttsh 的设计就是围绕这张表来的。它用 C 写核心的 J-Link 交互层用 Lua 做脚本层命令行接口设计成管道友好的风格整体是一个能嵌进任何自动化流程的工具。2. rttsh 的整体架构与 J-Link 底层交互逻辑2.1 分层设计C 核心 Lua 脚本层 CLI 外壳rttsh 的架构分三层这个分层不是拍脑袋定的而是根据哪些部分需要高性能、哪些部分需要灵活性来划分的。最底层是C 核心层负责和 J-Link DLL 打交道。这一层做的事情包括加载 J-Link 动态库、建立和目标芯片的调试连接、搜索 RTT 控制块在目标内存中的地址、读写 RTT 上行和下行缓冲区。这些操作对性能敏感而且需要直接操作内存和硬件接口用 C 写最合适。中间层是Lua 脚本层。Lua 在这里的角色是胶水和逻辑处理器。C 核心层把 RTT 读写能力暴露成 Lua 函数比如rtt.read()、rtt.write()、rtt.read_until()Lua 脚本就可以用这些函数组合出复杂的交互逻辑。选 Lua 而不是 Python 或 JavaScript主要考虑是 Lua 解释器体积极小、嵌入成本低、启动速度快而且语法简单嵌入式工程师上手门槛低。最外层是CLI 外壳负责解析命令行参数、加载脚本文件、管理执行流程、处理信号和退出码。这一层决定了工具怎么被调用是rttsh -s script.lua还是rttsh --read-until ASSERT --timeout 30s都在这层定义。三层之间的数据流是这样的CLI 解析参数后初始化 C 核心层建立 J-Link 连接然后加载 Lua 脚本并把 C 核心层的 API 注册进去脚本执行过程中通过 API 读写 RTT 数据最后 CLI 根据脚本执行结果返回退出码。2.2 RTT 控制块搜索从内存扫描到地址缓存RTT 工作的前提是找到目标内存中的 RTT 控制块。这个控制块是 SEGGER RTT 库在目标端定义的一个结构体包含了上行缓冲区、下行缓冲区的描述信息。J-Link 提供了JLINK_RTTERMINAL_Control这个 API 来获取控制块信息但它的工作方式是在目标内存的特定区域搜索 RTT 控制块的签名。这里有个实际使用中很容易踩的坑搜索范围。J-Link 默认的搜索范围是有限的如果你的 RTT 控制块被链接器放在了比较特殊的位置比如某些 RTOS 把 RTT 缓冲区放在特定的 RAM 段默认搜索可能找不到。rttsh 里我加了--search-addr和--search-size两个参数允许手动指定搜索的起始地址和范围。另一个坑是搜索时机。如果目标板还没运行到 RTT 初始化完成控制块还没建立搜索就会失败。rttsh 的处理策略是先尝试搜索失败后等待一段时间重试最多重试 N 次。这个 N 和等待间隔可以通过--search-retry和--search-delay配置。实测下来对于大多数应用重试 5 次、每次间隔 200ms 就能覆盖绝大多数启动场景。搜索到控制块之后rttsh 会把控制块地址缓存起来。后续的读写操作直接用这个地址不再重复搜索。但这里要注意如果目标板复位了控制块地址可能会变。所以 rttsh 在检测到连续读失败时会自动触发一次重新搜索。这个逻辑在长时间运行的采集场景里特别重要。2.3 上行/下行缓冲区的读写机制与超时控制RTT 的通信模型是双缓冲的目标板通过上行缓冲区Up Buffer发数据给主机主机通过下行缓冲区Down Buffer发数据给目标板。rttsh 的读写 API 就是围绕这两个缓冲区设计的。读操作的核心逻辑是轮询上行缓冲区的读指针如果发现读指针和写指针不一致说明有新数据就把数据取出来。这里的关键是轮询间隔。间隔太短会浪费 CPU间隔太长会丢数据如果缓冲区满了目标板的新数据会覆盖旧数据。rttsh 默认的轮询间隔是 1ms对于大多数场景够用。如果目标板输出速率很高可以调到 100us。写操作相对简单把数据写入下行缓冲区更新写指针。但要注意下行缓冲区的大小。如果一次写入的数据超过缓冲区大小就需要分片写入。rttsh 的rtt.write()会自动处理分片但分片之间需要等待目标板消费数据否则会覆盖未读数据。这个等待逻辑我用了一个简单的流控写入一片后检查下行缓冲区的剩余空间空间不足就等待一段时间再写下一片。超时控制是脚本化工具的生命线。rttsh 的每个读写操作都支持超时参数。比如rtt.read_until(OK, 5000)表示最多等 5 秒如果 5 秒内没读到 OK 就返回超时错误。这个超时不是简单的 sleep而是带轮询的等待一旦读到目标字符串就立即返回不会浪费时间。3. Lua 脚本层的 API 设计与典型用法3.1 核心 API 清单与参数说明Lua 脚本层是 rttsh 的灵魂它的 API 设计直接决定了工具好不好用。我把 API 分成四类连接管理、读操作、写操作、辅助工具。连接管理类rtt.connect(options)建立 J-Link 连接。options 是个 table支持device芯片型号、speed接口速度 kHz、interfaceSWD/JTAG等字段。rtt.disconnect()断开连接。rtt.reconnect()重新连接用于目标板复位后的恢复。读操作类rtt.read(timeout_ms)读取当前上行缓冲区中的所有数据返回字符串。timeout_ms 是等待超时。rtt.read_until(pattern, timeout_ms)持续读取直到匹配到 pattern返回匹配到的完整数据。pattern 支持普通字符串和 Lua 模式。rtt.read_line(timeout_ms)读取一行以换行符为界。rtt.read_bytes(n, timeout_ms)读取指定字节数。写操作类rtt.write(data)写入字符串或字节数组。rtt.write_line(data)写入并追加换行符。rtt.write_then_read(data, pattern, timeout_ms)写入后等待特定响应这是最常用的组合操作。辅助工具类rtt.log(msg)输出日志到 stderr不影响 stdout 的数据流。rtt.sleep(ms)休眠指定毫秒数。rtt.export_csv(filename, rows)把数据导出为 CSV 文件。rtt.export_json(filename, data)把数据导出为 JSON 文件。这些 API 的设计原则是常用操作一行搞定复杂操作可以组合。比如发命令等响应这个最高频的操作直接用write_then_read就行不用自己拼 write 和 read_until。3.2 用 read_until 实现命令-响应模式的交互命令-响应模式是嵌入式调试里最常见的交互方式。目标板收到一条命令执行后返回一个结果主机等待这个结果。用 rttsh 的 Lua API 实现这个模式非常直接-- 发送命令并等待响应 local resp rtt.write_then_read(GET_TEMP\n, TEMP%d, 3000) if resp then local temp resp:match(TEMP(%d)) rtt.log(当前温度: .. temp) else rtt.log(读取温度超时) end这里有几个细节值得说。第一write_then_read的 pattern 参数用的是 Lua 模式%d匹配一个或多个数字比普通字符串匹配灵活得多。第二返回值 resp 是匹配到的完整数据你可以用string.match进一步提取需要的字段。第三超时返回 nil脚本里要判断 nil 并处理超时情况不能假设一定有响应。实际使用中我建议给每个命令-响应交互都设一个合理的超时。超时太短会误判目标板还在处理超时太长会拖慢整个脚本。经验值是简单查询 1-2 秒复杂操作 5-10 秒固件升级之类的操作 30 秒以上。3.3 批量脚本验证循环采集与条件触发批量脚本验证是 rttsh 的强项。比如你要验证目标板在不同电压下的工作状态可以写一个循环每次设置电压、等待稳定、采集数据local voltages {3300, 3000, 2700, 2400, 2100} local results {} for _, mv in ipairs(voltages) do rtt.write_line(SET_VOLTAGE .. mv) rtt.read_until(VOLTAGE_SET, 2000) rtt.sleep(500) -- 等待电压稳定 local data rtt.read_until(STATUS_OK, 3000) if data then local current data:match(CURRENT(%d)) table.insert(results, {voltage mv, current current}) rtt.log(string.format(电压 %dmV, 电流 %s, mv, current)) else rtt.log(string.format(电压 %dmV 采集失败, mv)) end end rtt.export_csv(voltage_sweep.csv, results)这个脚本展示了几个实用技巧。第一用 table 存采集结果最后统一导出避免频繁写文件。第二每次操作后都有超时判断失败不会导致整个脚本崩溃。第三用rtt.log输出进度信息到 stderr这样即使 stdout 被重定向到文件你也能在终端看到进度。条件触发是另一个常见需求。比如你想在目标板输出特定错误码时自动抓取上下文while true do local line rtt.read_line(1000) if line and line:match(ERROR_CODE(%d)) then local code line:match(ERROR_CODE(%d)) rtt.log(捕获到错误码: .. code) -- 抓取错误前后的上下文 local context rtt.read(2000) rtt.export_json(error_ .. code .. .json, { code code, context context, timestamp os.time() }) end end这种持续监听 条件触发的模式在长时间稳定性测试里特别有用。你可以让它跑一整夜第二天早上看抓到了哪些异常。4. 把 rttsh 塞进 CI 流水线的实操细节4.1 非交互模式运行与退出码约定CI 环境里没有人在旁边看着工具必须能非交互运行并且用退出码告诉流水线成功还是失败。rttsh 的退出码约定是这样的退出码含义CI 处理建议0脚本执行成功所有断言通过继续下一步1脚本执行失败断言未通过标记构建失败2连接失败无法建立 J-Link 连接检查硬件连接3脚本语法错误或运行时错误检查脚本4超时操作未在指定时间内完成检查目标板状态在 CI 脚本里你可以这样用#!/bin/bash set -e # 烧录固件 JLinkExe -device STM32F407VG -if SWD -speed 4000 -autoconnect 1 -CommanderScript flash.jlink # 运行 RTT 测试脚本 rttsh -s test_boot.lua --device STM32F407VG --speed 4000 if [ $? -ne 0 ]; then echo RTT 测试失败 exit 1 fi # 抓取启动日志并检查 rttsh --read-until BOOT_COMPLETE --timeout 30s --output boot.log if ! grep -q BOOT_COMPLETE boot.log; then echo 启动未完成 exit 1 fi这里的关键是set -e和显式的退出码检查。rttsh 在非交互模式下不会等待用户输入所有操作要么成功要么超时失败不会卡住流水线。4.2 日志采集与断言检查的脚本模板CI 里最常见的 RTT 用途是抓日志 查关键字。我整理了一个通用的脚本模板可以直接改改用-- ci_check.lua local keywords { {pattern ASSERT, desc 断言失败, fatal true}, {pattern HardFault, desc 硬件错误, fatal true}, {pattern WARN, desc 警告, fatal false}, } local log_file io.open(rtt_full.log, w) local fatal_count 0 local warn_count 0 local start_time os.time() -- 采集 30 秒 while os.time() - start_time 30 do local data rtt.read(1000) if data and #data 0 then log_file:write(data) log_file:flush() for _, kw in ipairs(keywords) do if data:find(kw.pattern) then rtt.log(检测到: .. kw.desc) if kw.fatal then fatal_count fatal_count 1 else warn_count warn_count 1 end end end end end log_file:close() rtt.log(string.format(采集完成: 致命错误 %d, 警告 %d, fatal_count, warn_count)) if fatal_count 0 then os.exit(1) end这个模板的要点日志实时写文件并 flush避免程序崩溃时丢数据致命错误和警告分开计数只有致命错误才让 CI 失败采集时间用os.time()控制简单可靠。4.3 多设备并行测试时的资源隔离如果你在 CI 里同时测多个设备比如一个流水线跑多个 DUT就要注意 J-Link 的资源隔离。每个 rttsh 实例会占用一个 J-Link 探针如果多个实例抢同一个探针会报J-Link is already in use。解决方案有两个。一是用序列号指定探针rttsh 支持--serial参数你可以在 CI 配置里给每个并行任务分配不同的 J-Link 序列号。二是用文件锁做互斥在脚本开头用flock或者 Lua 的文件锁机制确保同一时间只有一个实例访问特定探针。# 用 flock 做互斥 flock /tmp/jlink_001.lock rttsh -s test.lua --serial 123456789实测下来用序列号指定是最干净的方式前提是你的 J-Link 探针都有唯一序列号正版都有这个不用担心。5. AI 辅助调试场景下 rttsh 的独特价值5.1 让 AI 助手通过命令行读取目标板状态现在用 AI 辅助调试越来越普遍但 AI 助手有个天然短板它看不到你的硬件。你告诉它程序跑飞了它只能根据代码猜没法直接看目标板的实际状态。rttsh 在这里可以当 AI 的眼睛和手。具体做法是把 rttsh 的命令行接口暴露给 AI 助手比如通过一个简单的 shell 工具封装AI 就可以执行rttsh --read-var g_system_state这样的命令直接读取目标板上的变量值。rttsh 支持通过 RTT 读取任意内存地址只要你知道变量的地址从 map 文件里查就能读。# 读取指定地址的 4 字节变量 rttsh --read-mem 0x20000000 --size 4 --format hex # 读取并解析为整数 rttsh --read-mem 0x20000000 --size 4 --format int这个能力配合 AI 的推理能力可以做出很有意思的调试流程AI 先读几个关键变量根据值判断程序状态然后决定下一步读什么或者发什么命令。整个过程不需要人干预。5.2 结构化输出让 AI 更容易解析AI 助手解析文本的能力很强但结构化数据永远比自由文本更好处理。rttsh 支持--format json输出把读取结果包装成 JSON{ timestamp: 2024-01-15T10:30:00Z, operation: read_mem, address: 0x20000000, size: 4, value: 42, raw: 2A000000 }这种格式 AI 可以直接解析不需要做复杂的文本提取。我在实际使用中发现给 AI 提供结构化输出它的判断准确率明显高于给它一堆原始日志让它自己找。5.3 脚本化交互降低 AI 的操作复杂度AI 直接操作硬件有个风险它可能发出错误的命令把目标板搞挂。rttsh 的 Lua 脚本层可以起到安全护栏的作用。你可以预定义一组安全的操作脚本AI 只能调用这些脚本不能直接发任意命令。比如定义一个safe_query.lua-- 只允许查询不允许修改 local allowed_queries { [status] GET_STATUS, [version] GET_VERSION, [error] GET_LAST_ERROR, } local query arg[1] if not allowed_queries[query] then rtt.log(不允许的查询: .. query) os.exit(3) end local resp rtt.write_then_read(allowed_queries[query] .. \n, .\n, 2000) print(resp)AI 只能通过rttsh -s safe_query.lua status这样的方式查询没法执行危险操作。这个设计在多人协作或者 AI 自主调试的场景里特别重要。6. 实际使用中踩过的坑与性能调优经验6.1 缓冲区溢出导致的数据丢失RTT 的上行缓冲区大小是固定的默认 1KB可以在目标端配置。如果目标板输出速率超过主机读取速率缓冲区会满新数据会覆盖旧数据。我踩过一次坑目标板在 100ms 内输出了 2KB 日志而我的脚本每 500ms 才读一次结果丢了将近一半的数据。解决办法有两个。一是提高读取频率把轮询间隔从 500ms 降到 50ms 甚至更低。二是增大目标端缓冲区在SEGGER_RTT_Conf.h里把BUFFER_SIZE_UP调大。实测下来对于日志量大的场景把上行缓冲区调到 4KB 或 8KB配合 100ms 的读取间隔基本不会丢数据。还有一个隐蔽的坑Lua 脚本里的 sleep 会阻塞读取。如果你在脚本里写了rtt.sleep(5000)这 5 秒内工具不会读 RTT缓冲区可能就满了。正确的做法是用rtt.read(5000)代替 sleep这样在等待的同时还在持续读取。6.2 目标板复位后的连接恢复目标板复位是调试过程中的常态但复位会导致 RTT 控制块地址变化原来的连接就失效了。rttsh 的处理策略是自动重连但重连的时机和方式有讲究。我最初的实现是检测到读失败就立即重连。结果发现目标板复位过程中 J-Link 连接会短暂断开立即重连经常失败。后来改成检测到连续 3 次读失败后等待 500ms 再重连重连失败则指数退避重试。这个策略稳定多了。-- 带重连的读取封装 function safe_read(timeout) local data, err rtt.read(timeout) if err then rtt.log(读取失败尝试重连...) rtt.sleep(500) if rtt.reconnect() then rtt.log(重连成功) return rtt.read(timeout) else rtt.log(重连失败) return nil end end return data end6.3 高频读写下的 CPU 占用优化rttsh 默认的 1ms 轮询间隔在低频场景下没问题但如果你同时跑多个实例或者目标板输出速率很高CPU 占用会明显上升。我做过测试单实例 1ms 轮询CPU 占用约 3-5%4 个实例并行CPU 占用能到 20%。优化手段有几个。一是动态调整轮询间隔没有数据时逐渐增大间隔比如从 1ms 增到 10ms有数据时立即降回 1ms。这个自适应策略能把空闲时的 CPU 占用降到 1% 以下。二是用事件驱动代替轮询J-Link 的 RTT API 其实支持回调模式但配置起来比较复杂我目前还没在 rttsh 里实现算是一个后续优化方向。还有一个容易忽略的点Lua 脚本本身的性能。Lua 很快但如果你在脚本里做大量的字符串拼接比如log log .. data性能会下降。正确的做法是用 table 收集数据最后用table.concat一次性拼接。7. 从 rttsh 延伸出的几个实用场景7.1 固件升级过程中的进度监控固件升级比如通过 Bootloader 升级是个耗时操作期间目标板会输出进度信息。用 rttsh 可以实时监控升级进度并在异常时自动中止rtt.write_line(START_UPDATE) local last_progress 0 local stall_count 0 while true do local line rtt.read_line(5000) if not line then stall_count stall_count 1 if stall_count 3 then rtt.log(升级卡住中止) rtt.write_line(ABORT_UPDATE) os.exit(1) end else stall_count 0 local progress line:match(PROGRESS(%d)) if progress then progress tonumber(progress) if progress last_progress then rtt.log(string.format(升级进度: %d%%, progress)) last_progress progress end end if line:match(UPDATE_DONE) then rtt.log(升级完成) break end if line:match(UPDATE_FAIL) then rtt.log(升级失败) os.exit(1) end end end这个脚本的关键是卡住检测如果连续多次读不到数据说明升级可能卡住了主动中止比干等要好。7.2 长时间稳定性测试的数据记录稳定性测试通常要跑几个小时甚至几天期间需要持续记录关键指标。rttsh 的 CSV 导出功能在这里很好用local csv io.open(stability.csv, w) csv:write(timestamp,heap_free,stack_usage,task_count,error_count\n) local start os.time() while os.time() - start 86400 do -- 跑 24 小时 local data rtt.read_until(STATS, 10000) if data then local heap data:match(HEAP(%d)) local stack data:match(STACK(%d)) local tasks data:match(TASKS(%d)) local errors data:match(ERRORS(%d)) csv:write(string.format(%d,%s,%s,%s,%s\n, os.time(), heap, stack, tasks, errors)) csv:flush() end rtt.sleep(60000) -- 每分钟采集一次 end csv:close()注意这里用了rtt.sleep(60000)而不是rtt.read(60000)因为采集间隔是 1 分钟不需要持续读取。但前面说过 sleep 会阻塞读取所以这种场景下要确保目标板的 RTT 缓冲区足够大能撑过 1 分钟的输出量。如果撑不住就得改成短间隔读取 累积判断。7.3 多目标板同步采集的脚本编排有些测试需要多个目标板同步工作比如一个发命令、一个收响应。rttsh 支持多实例你可以用 shell 脚本编排#!/bin/bash # 启动两个采集实例分别连不同的 J-Link rttsh -s sender.lua --serial 111111 PID1$! rttsh -s receiver.lua --serial 222222 PID2$! # 等待两个实例完成 wait $PID1 RET1$? wait $PID2 RET2$? if [ $RET1 -ne 0 ] || [ $RET2 -ne 0 ]; then echo 同步测试失败 exit 1 fi这种编排方式简单直接适合两个到三个设备的场景。设备再多的话建议用更专业的测试框架来管理。8. 关于 rttsh 后续可以怎么扩展rttsh 目前的核心功能已经能覆盖大部分脚本化 RTT 的需求但还有几个方向值得继续做。一个是事件驱动的 RTT 读取用 J-Link 的回调机制代替轮询进一步降低 CPU 占用和延迟。另一个是内置的断言库把常见的日志检查、变量范围检查封装成 Lua 函数让 CI 脚本写起来更简洁。还有一个是和主流测试框架的集成比如把 rttsh 包装成 pytest 的 fixture这样用 Python 写测试的团队也能直接用。我在实际使用中最大的体会是脚本化调试工具的价值不在于功能多而在于接口稳。rttsh 的 API 我尽量保持简单和稳定因为一旦你的 CI 脚本依赖了某个接口改起来成本很高。所以每次加新功能我都会先问这个功能能不能用现有的 API 组合出来如果能就不加新 API。这个原则让 rttsh 的接口数量一直控制在一个很小的范围内维护起来轻松很多。最后分享一个小技巧如果你在用 rttsh 做长时间采集建议在脚本里加一个心跳输出每隔一段时间往 stderr 打一行日志。这样即使 stdout 被重定向到文件你在终端也能看到工具还活着。这个习惯帮我避免了好几次以为在跑其实早就挂了的尴尬。
返回列表