pstack 线程栈排查实战:从死锁到性能抖动的定位技巧
1. 从一次线上卡顿说起pstack 到底能帮你看到什么线上服务突然变慢CPU 不高、内存正常、日志也没报错但接口响应时间从 50ms 涨到了 2s。这种场景下很多人第一反应是加日志、重启、扩容但往往治标不治本。我遇到过好几次类似情况最后都是靠pstack定位到某个线程卡在一把锁上或者某个函数调用栈异常深导致栈溢出。pstack这个工具看起来简单——就是打印进程的线程栈——但真正用好了它能帮你省下大量排查时间。这篇文章面向的是日常需要排查线上问题、做性能分析、或者单纯想搞清楚自己程序到底在干什么的开发者。不管你是刚接触 Linux 调试工具的新手还是已经用过gdb、perf的老手pstack都值得放进你的工具箱。它不需要你重新编译程序不需要加任何埋点只要进程还在跑一条命令就能把当前所有线程的调用栈打出来。这篇文章会从pstack的基本原理讲起拆解它背后的实现机制然后给出完整的实操步骤、参数选择依据、常见问题排查方法以及我在实际使用中踩过的坑和总结的技巧。读完你就能直接在自己的环境里复现并且知道什么场景下该用它、什么场景下该换别的工具。2. pstack 的核心原理与适用边界2.1 pstack 本质上做了什么pstack不是一个独立的二进制程序大多数 Linux 发行版上它其实是一个 shell 脚本内部调用的是gdb。你执行pstack pid的时候它实际做的事情是启动一个gdb进程attach 到目标进程上然后对每个线程执行thread apply all bt最后 detach 并退出。所以pstack的输出格式和gdb的bt命令完全一致只是它帮你自动化了这个过程。理解这一点很重要因为它决定了pstack的能力边界。gdb能看到的栈信息pstack就能看到gdb看不到的pstack也看不到。比如如果程序没有调试符号pstack打出来的就是一堆地址你需要自己用addr2line或者nm去翻译。再比如如果进程处于不可中断睡眠状态D 状态gdbattach 可能会失败或者卡住pstack自然也会受影响。注意有些发行版把pstack做成了独立的二进制比如某些嵌入式环境或者定制系统。这种情况下它的行为可能略有不同但核心逻辑仍然是读取目标进程的栈内存并做 unwind。你可以用file $(which pstack)确认一下你系统上的pstack到底是什么。2.2 为什么选择 pstack 而不是其他工具排查线程栈问题的工具不止pstack一个常见的有gdb、perf、eu-stack、cat /proc/pid/stack等。我选pstack的理由主要有三个第一它足够简单一条命令就能拿到所有线程的栈不需要记复杂的gdb命令第二它对目标进程的侵入性相对可控attach 和 detach 的过程很快不会像gdb交互式调试那样长时间挂起进程第三它的输出格式通用几乎所有 Linux 开发者都能看懂。但pstack也有明显的短板。它依赖gdb所以如果目标环境没有装gdbpstack就用不了。它默认只能打印用户态栈看不到内核态的信息。它也不能像perf那样做采样和火焰图只能给你某一个瞬间的快照。所以我的习惯是快速定位用pstack深度分析用perf交互式调试用gdb三者配合使用。2.3 什么场景下 pstack 最有用根据我的经验pstack在以下几类场景中特别有效死锁排查多个线程互相等待锁pstack能直接看到每个线程卡在哪个锁的等待上配合源码就能定位到具体的锁对象。线程卡死某个线程长时间不推进pstack连续打几次对比栈帧变化就能判断是卡在 IO、锁还是死循环。性能抖动服务偶尔变慢pstack在变慢的瞬间抓几次往往能抓到某个不常见的调用路径。栈溢出递归太深导致栈溢出pstack能看到异常深的调用栈直接定位到递归函数。第三方库问题调用第三方库时行为异常pstack能帮你确认到底进了哪个库函数。反过来如果问题是 CPU 持续高负载、内存泄漏、或者需要精确的耗时分布pstack就不是最佳选择应该优先考虑perf、valgrind或者heaptrack这类工具。3. 环境准备与 pstack 安装实操3.1 确认系统是否自带 pstack很多发行版默认就带了pstack你可以先执行which pstack看看。如果返回了路径比如/usr/bin/pstack那直接用就行。如果没有再考虑安装。需要注意的是有些系统上pstack可能被放在/usr/sbin或者/usr/local/bin下which不一定能找到可以用find / -name pstack 2/dev/null搜一下。另外即使pstack存在也要确认它依赖的gdb是否可用。执行gdb --version如果能正常输出版本号说明环境基本没问题。如果gdb不存在pstack执行时会报错提示找不到gdb。3.2 安装 gdb 和 pstack在 Debian/Ubuntu 系上安装命令是sudo apt-get update sudo apt-get install gdb在 RHEL/CentOS 系上sudo yum install gdb在 Alpine 上sudo apk add gdb安装完gdb之后pstack通常也会一起被装上因为很多发行版把pstack放在gdb包或者gdb的推荐依赖里。如果装完gdb还是没有pstack可以手动创建一个脚本。内容如下#!/bin/sh if [ $# -ne 1 ]; then echo Usage: $0 pid exit 1 fi gdb -q -nx -batch -ex thread apply all bt -p $1 2/dev/null保存为/usr/local/bin/pstack然后chmod x /usr/local/bin/pstack即可。这个脚本比某些发行版自带的pstack更干净去掉了多余的输出只保留栈信息。3.3 权限与安全限制pstack需要 attach 到目标进程这要求你有足够的权限。普通用户只能 attach 到自己启动的进程如果要 attach 其他用户的进程需要root权限或者相应的ptrace能力。在容器环境里还需要注意ptrace相关的 seccomp 或 apparmor 配置有些容器默认禁止ptrace会导致pstack失败。提示如果你在容器里执行pstack报ptrace: Operation not permitted先检查容器的securityContext或者docker run参数确认没有禁用SYS_PTRACE能力。生产环境里如果确实需要排查可以临时给容器加上这个能力排查完再移除。另外有些系统开启了yama保护限制ptrace的范围。你可以用cat /proc/sys/kernel/yama/ptrace_scope查看当前值。0 表示无限制1 表示只能 attach 子进程2 表示只能 attach 同用户且同会话的进程3 表示完全禁止。如果值是 1 或 2而你又不是目标进程的父进程就可能 attach 失败。临时调整可以用echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope但生产环境不建议长期改成 0。4. pstack 完整实操流程与参数解析4.1 基本用法与输出解读最简单的用法就是pstack pid。比如目标进程 PID 是 12345执行pstack 12345输出会类似这样Thread 3 (Thread 0x7f8a2c3d1700 (LWP 12348)): #0 0x00007f8a2d4b5f47 in epoll_wait () from /lib64/libc.so.6 #1 0x0000000000401a2b in event_loop () at server.c:120 #2 0x0000000000401b3c in worker_thread (arg0x0) at server.c:200 #3 0x00007f8a2d6b6e25 in start_thread () from /lib64/libpthread.so.0 #4 0x00007f8a2d3f434d in clone () from /lib64/libc.so.6 Thread 2 (Thread 0x7f8a2cbd2700 (LWP 12347)): #0 0x00007f8a2d4b5f47 in epoll_wait () from /lib64/libc.so.6 #1 0x0000000000401a2b in event_loop () at server.c:120 #2 0x0000000000401b3c in worker_thread (arg0x0) at server.c:200 #3 0x00007f8a2d6b6e25 in start_thread () from /lib64/libpthread.so.0 #4 0x00007f8a2d3f434d in clone () from /lib64/libc.so.6 Thread 1 (Thread 0x7f8a2d8b5740 (LWP 12345)): #0 0x00007f8a2d4b5f47 in epoll_wait () from /lib64/libc.so.6 #1 0x0000000000401a2b in event_loop () at server.c:120 #2 0x0000000000401b3c in main (argc1, argv0x7ffd3c4e5a88) at server.c:300每一段对应一个线程Thread 3是线程编号括号里的LWP 12348是轻量级进程 ID也就是内核视角的线程 ID。下面的#0到#4是栈帧#0是当前执行位置数字越大表示调用层级越深。如果编译时带了-g选项你会看到函数名、文件名和行号如果没有就只能看到地址和库名。4.2 连续采样与对比分析单次pstack只是一个快照很多时候你需要连续打几次对比栈帧的变化。我通常用这样的循环for i in $(seq 1 5); do echo Sample $i pstack 12345 sleep 1 done这样能拿到 5 秒内的 5 个快照。如果某个线程的栈一直不变说明它卡住了如果栈在变化说明它在正常执行。对比的时候重点关注#0和#1帧这两个帧最能反映当前状态。还有一种做法是把输出重定向到文件然后用diff对比pstack 12345 /tmp/stack1.txt sleep 2 pstack 12345 /tmp/stack2.txt diff /tmp/stack1.txt /tmp/stack2.txtdiff的输出会直接告诉你哪些线程的栈发生了变化哪些没变。这个方法在排查死锁时特别有用因为死锁的线程栈通常完全不变。4.3 结合调试符号还原真实调用栈如果程序编译时没有加-gpstack输出里只有地址比如0x0000000000401a2b。这时候你需要用addr2line来翻译addr2line -e /path/to/binary -f -C 0x0000000000401a2b-e指定可执行文件-f输出函数名-C做符号 demangle。如果地址属于某个动态库你需要用对应的库文件来翻译比如addr2line -e /lib64/libc.so.6 0x00007f8a2d4b5f47。提示生产环境的二进制文件最好保留一份带调试符号的版本但不要部署到线上。排查问题时用带符号的版本去翻译地址既能拿到完整信息又不会因为符号文件增大部署体积。可以用objcopy --only-keep-debug把调试信息单独抽出来再用objcopy --add-gnu-debuglink关联回去。4.4 参数选择与性能影响pstack本身没有太多参数但底层gdb的行为会影响性能和稳定性。默认情况下gdbattach 会暂停目标进程的所有线程读取完栈信息后再恢复。这个暂停时间通常在毫秒级但如果进程线程很多、栈很深可能会达到几十毫秒甚至上百毫秒。对于延迟敏感的服务这个暂停可能造成请求超时。如果你担心影响可以改用eu-stack它是 elfutils 包里的工具不需要暂停进程直接读取栈内存。用法是eu-stack -p pid输出格式和pstack类似。但eu-stack对调试符号的依赖更强没有符号时可能只能打出地址。另一个选择是perf record -g -p pid -- sleep 5然后perf report。这种方式是采样对进程影响更小但拿到的是统计结果不是精确的某一时刻快照。我的建议是如果只是偶尔排查pstack完全够用如果要在高负载生产环境频繁抓栈优先考虑eu-stack或perf。5. 常见问题与排查技巧实录5.1 pstack 执行报错怎么办最常见的报错是ptrace: Operation not permitted。原因通常是权限不足或者ptrace_scope限制。排查步骤先确认当前用户是否有权限 attach 目标进程用ps -o user -p pid看进程属主然后检查ptrace_scope的值最后确认容器或安全模块是否禁用了ptrace。另一个常见报错是No such process这通常是因为 PID 写错了或者进程在你执行命令的瞬间退出了。可以先用ps -p pid确认进程还在。还有一种是gdb: command not found说明系统里没有gdb需要先安装。5.2 输出全是地址没有函数名这是没有调试符号的典型表现。解决办法有两个一是用带-g重新编译但生产环境通常不允许二是用addr2line手动翻译或者用gdb加载符号文件后再看。如果你有单独的调试符号文件可以在gdb里用symbol-file加载然后再执行thread apply all bt。还有一种情况是函数名被 strip 掉了但动态符号表还在。这时候pstack可能只能看到库函数名看不到你自己的函数名。可以用nm -D或者readelf -s确认符号表状态。5.3 栈信息不完整或明显异常有时候pstack打出来的栈只有一两帧或者栈帧之间明显不连续。这通常是因为栈被破坏或者编译器做了优化比如尾调用优化、帧指针省略。解决办法是编译时加上-fno-omit-frame-pointer这样栈帧会保留帧指针unwind 更可靠。但生产环境重新编译成本高所以更实际的做法是结合perf或者eu-stack交叉验证。如果栈里出现了??说明对应地址没有找到符号信息。可能是 JIT 生成的代码也可能是某个没有符号的第三方库。这时候只能靠地址范围和调用上下文来推断。5.4 高频调用导致进程卡顿前面提到过pstack会暂停进程。如果你在短时间内频繁执行pstack比如每秒一次持续几分钟目标进程可能会因为反复暂停恢复而出现明显延迟。我踩过一次坑在一个 QPS 很高的服务上连续跑了 100 次pstack结果监控显示 P99 延迟飙升。后来改成每 5 秒一次并且用eu-stack替代问题就消失了。所以我的经验是生产环境抓栈频率不要高于每 2 秒一次如果必须高频优先用eu-stack或perf抓栈时间尽量避开业务高峰。5.5 常见问题速查表问题现象可能原因排查方法解决建议ptrace: Operation not permitted权限不足或 ptrace_scope 限制检查进程属主和 ptrace_scope用 root 或调整 ptrace_scopeNo such processPID 错误或进程已退出ps -p pid确认重新获取 PID输出全是地址缺少调试符号file查看是否 stripped用 addr2line 翻译或加载符号文件栈帧不完整栈破坏或编译优化检查编译选项加-fno-omit-frame-pointer重编译进程卡顿频繁 attach 导致暂停观察延迟监控降低频率或改用 eu-stackgdb: command not found未安装 gdbwhich gdb安装 gdb6. 几个真实场景的排查思路6.1 死锁场景两个线程互相等待有一次线上服务完全无响应日志停止输出CPU 接近 0。我执行pstack后看到两个线程的栈分别是Thread 2: #0 __lll_lock_wait () #1 pthread_mutex_lock () #2 update_config () at config.c:88 #3 handle_request () at server.c:150 Thread 3: #0 __lll_lock_wait () #1 pthread_mutex_lock () #2 reload_config () at config.c:120 #3 timer_thread () at timer.c:60两个线程都卡在pthread_mutex_lock上一个在update_config一个在reload_config。看源码发现update_config先拿锁 A 再拿锁 B而reload_config先拿锁 B 再拿锁 A典型的 AB-BA 死锁。定位到之后改成统一的加锁顺序就解决了。这个案例里pstack的价值在于它直接告诉了我两个线程分别卡在哪个函数的哪一行不需要我去猜。如果没有pstack我可能要先加日志、重启、复现整个过程至少多花半小时。6.2 线程卡死场景IO 等待还是死循环另一个案例是某个工作线程不再处理任务但进程还在跑。pstack连续打三次发现该线程的栈一直是#0 0x00007f8a2d4b5f47 in epoll_wait () #1 0x0000000000401a2b in event_loop () at server.c:120栈完全不变说明它卡在epoll_wait上也就是没有事件到来。这本身不是 bug但结合业务逻辑发现上游应该发事件却没发问题出在上游。如果栈是变化的比如在某个循环里反复执行那就是死循环。所以连续采样对比是区分 IO 等待和死循环的关键手段。6.3 性能抖动场景偶发慢请求的根因服务 P99 延迟偶尔飙高但平均延迟正常。我在延迟飙高的瞬间抓了几次pstack发现某个线程偶尔会出现在malloc里#0 malloc () #1 std::string::_M_mutate () #2 process_request ()结合代码发现这个请求路径里有一个大字符串拼接每次都会触发内存分配。在内存紧张时malloc会变慢导致偶发延迟。后来改成预分配缓冲区问题就消失了。这个案例说明pstack不仅能排查故障还能辅助性能优化。7. 我个人的使用心得与建议pstack这个工具我用了很多年最大的体会是它简单到容易被忽视但关键时刻能救命。我的建议是把它加入你的日常排查清单和top、iostat、netstat放在一起。平时可以不用但一旦遇到线程卡死、死锁、响应慢这类问题第一时间想到它。另外不要只依赖pstack。它给的是快照不是全貌。配合perf做采样、配合strace看系统调用、配合gdb做交互式调试才能形成完整的排查能力。还有一点生产环境的二进制尽量保留调试符号哪怕不部署到线上也要存档。否则真出问题时你只能对着一堆地址干瞪眼。最后分享一个小技巧如果你经常需要抓栈可以写一个脚本自动连续采样、自动对比、自动高亮变化的栈帧。这样排查效率会高很多。我自己写过一个简单的版本核心逻辑就是循环调用pstack然后用awk提取每个线程的#0帧对比前后差异。这个脚本后来成了我们团队排查线程问题的标配工具。

相关新闻

WebSQL数据泄露防线:导出管控与数字水印溯源实践

WebSQL数据泄露防线:导出管控与数字水印溯源实践

一个周末的凌晨,我收到了一份从外部渠道流转回来的疑似泄露数据文件:一张几百行的客户信息表,字段完整,格式规整。按照惯例,首先要做来源定位——是开发环境泄露的?还是某个运营同学的本地文件流出去的&…

2026/10/13 7:22:01 阅读更多 →
Maven Archetype实战:构建可维护、可追溯的项目脚手架

Maven Archetype实战:构建可维护、可追溯的项目脚手架

1. 为什么“新建项目”不该从零敲代码——原型模板不是锦上添花,而是工程效率的分水岭我带过三届某高校软件工程方向的实训项目,每届都有至少12个小组。第一年,我让所有学生“从空文件夹开始搭建Spring Boot项目”:手动建src/main…

2026/10/13 7:22:01 阅读更多 →
强力卸载删注册表:彻底清理残留的完整指南与避坑实践

强力卸载删注册表:彻底清理残留的完整指南与避坑实践

简介:这份资源面向需要彻底卸载顽固软件、清理注册表残留的Windows用户与系统维护人员,核心是UninstallTool工具及其配套组件,用于解决常规卸载不干净、恶意软件隐藏痕迹、注册表键值被保护而难以删除等问题。压缩包共11个文件,约…

2026/10/13 7:22:01 阅读更多 →

最新新闻

Docker搭建MySQL 8.0主从复制(GTID+ROW模式)实战

Docker搭建MySQL 8.0主从复制(GTID+ROW模式)实战

1. 项目概述:为什么现在还要亲手搭MySQL主从?“Docker搭建基于binlog的MySQL主从同步”——这标题里藏着三个关键信号:容器化、数据一致性、实时复制能力。不是用云数据库一键开启高可用,也不是靠中间件兜底,而是回归底…

2026/10/13 8:03:28 阅读更多 →
Python开发高效桌面文件管理工具实战

Python开发高效桌面文件管理工具实战

1. 项目背景与核心价值作为一名长期与文件打交道的开发者,我深刻理解文件管理效率对工作效率的影响。Windows资源管理器虽然基础功能完善,但在批量处理、智能分类等场景下依然存在明显短板。市面上的专业文件管理工具要么功能过剩,要么定制性…

2026/10/13 8:03:28 阅读更多 →
Base64编码全解析:原理、应用与避坑指南

Base64编码全解析:原理、应用与避坑指南

我印象里第一次认真看 Base64,是因为一份配置文件怎么都传不过去。那文件里面全是二进制,我把它复制到聊天窗口,发出去再存回来,文件就坏了。折腾半天才明白,问题出在“不是所有的数据都能作为文本传输”。后来接触得多…

2026/10/13 8:03:28 阅读更多 →
Oracle 12c ADG安装部署实战:从裸机到高可用灾备

Oracle 12c ADG安装部署实战:从裸机到高可用灾备

1. 为什么今天还要花力气部署Oracle 12c ADG?——不是怀旧,是现实约束下的理性选择在云原生和分布式数据库被反复宣讲的当下,突然看到“Oracle 12c ADG安装部署”这个标题,很多人第一反应是:这不早就该进博物馆了吗&am…

2026/10/13 8:03:28 阅读更多 →
Oracle 12c ADG部署实战:从原理到高可用闭环验证

Oracle 12c ADG部署实战:从原理到高可用闭环验证

1. 项目概述:为什么在今天还要认真对待ORACLE12C ADG部署?如果你正在维护一套运行着核心业务的Oracle数据库,比如某高校教务系统的选课模块、某制造企业ERP中的BOM管理、或是某金融公司交易流水归档库——那么你大概率已经经历过主库突发宕机…

2026/10/13 8:03:28 阅读更多 →
鸿蒙音频播放引擎优化:从700ms到150ms的秒播与无缝流动实践

鸿蒙音频播放引擎优化:从700ms到150ms的秒播与无缝流动实践

前段时间调 SmartXPlayer 音频库的时候,我一直在日志里盯着一个指标:从手指点击曲目到扬声器真正出声,到底要多少毫秒。最开始的结果让我有点头疼,本地无损音乐要 700ms 多,在线歌曲更是动不动就超过 1 秒。排查下来发…

2026/10/13 8:02:28 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →