惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Privacy & Cybersecurity Law Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
D
Docker
V
V2EX
GbyAI
GbyAI
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - Franky
Jina AI
Jina AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
InfoQ
博客园 - 司徒正美
雷峰网
雷峰网
F
Full Disclosure
S
SegmentFault 最新的问题
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
IT之家
IT之家
MongoDB | Blog
MongoDB | Blog
D
DataBreaches.Net
M
MIT News - Artificial intelligence
V
Visual Studio Blog
H
Help Net Security
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
博客园_首页
O
OpenAI News
人人都是产品经理
人人都是产品经理
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Attack and Defense Labs
Attack and Defense Labs
Blog — PlanetScale
Blog — PlanetScale
爱范儿
爱范儿
罗磊的独立博客
P
Palo Alto Networks Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - 聂微东
Last Week in AI
Last Week in AI
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
Lohrmann on Cybersecurity
N
News and Events Feed by Topic
有赞技术团队
有赞技术团队
The Register - Security
The Register - Security
S
Security @ Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
酷 壳 – CoolShell
酷 壳 – CoolShell
AWS News Blog
AWS News Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 最新话题
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Threat Research - Cisco Blogs

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 读:AI Agent 生产化——一份从原型到上线的速查清单 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日 mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日 WSL9x —— 在 Windows 9x 里跑 Linux 内核 6.19 用 ox.el 做你想做的事 —— org-export 高级编程指南 读:Hot-wiring the Lisp Machine —— 用纯 Elisp 构建零依赖的 Org 静态站点生成器 Elisp 性能优化的六个实战教训 - 暗无天日 fcitx5 下 Emacs 无法切换输入法的排查 - 暗无天日 ERT 测试交互命令的三种方式 - 暗无天日 SEM Assistant: 当 Elisp 守护进程遇上 LLM 用 dmsg 给 Elisp 加上结构化调试日志 用 org-habit 追踪非每日习惯 - 暗无天日 Clojure X-Men:当编程语言特性变成超能力 - 暗无天日 TIL: 用 diff-hl 在 fringe 中显示 git 变更 读:llm-test —— 用 LLM agent 驱动 Emacs 测试 TIL: AI 时代的橡皮鸭调试 - 暗无天日 fcitx 启动后键盘输入卡顿的排查 - 暗无天日 TIL: 早期网页的图片热区导航 - 暗无天日 读 Seeing the Whole System 用 Emacs 自动生成每周链接推荐 - 暗无天日 读:ASCII control characters in my terminal 读 What to learn - 暗无天日 Lisp 的括号之痛——一个愚人节玩笑揭开的老伤疤 - 暗无天日 一本书该"线性读"还是"并行读" - 暗无天日 读 How to Monetize a Blog:一篇伪装成变现指南的讽刺文 Python Mock 第三方依赖的四种策略 - 暗无天日 Emacs Lisp 热重载实用指南 - 暗无天日 Prot 的 Emacs 配置哲学 - 暗无天日 TIL: 从直播对谈中学到的三个 Emacs 技巧 - 暗无天日 TIL: 自动使用项目虚拟环境的 Python - 暗无天日 TIL: 让 Help buffer 自动获得焦点 一条命令让本地开发用上 HTTPS —— slim 工具介绍 用 fsck 检查和修复 Linux 文件系统 PostgreSQL 索引:从基础到你可能不知道的高级用法 - 暗无天日 用 .pdbrc 自定义 Python 调试器 ANSI 转义码的标准化现状 - 暗无天日 终端程序的潜规则 - 暗无天日 PARA Org-mode 测试配置 - 暗无天日 AI越强越辣鸡?控制论说这是必然的 - 暗无天日 AI 越强越需要你盯着——反馈循环实操指南 - 暗无天日 你的AI代理正在偷你的密钥——四种你没想到的泄露通道 - 暗无天日 LLM 在 DevOps 中的三种角色 - 暗无天日 写作风格的反建议 - 暗无天日 反驳本质复杂性——Dan Luu 论为什么《没有银弹》错了 - 暗无天日 文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱 - 暗无天日 AI 时代的 PARA 方法:用 Org-mode 和 AI 打造个人知识管理系统 Linux 数据去重学习笔记 - 暗无天日 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field - 暗无天日 X11 Forwarding 排障指南 - 暗无天日 IP欺骗端口扫描:当别人冒充你去扫描别人 - 暗无天日 Linux 输入栈全景解析:从硬件按键到屏幕响应 - 暗无天日 Unix 系统中那些被埋没的配置开关——以 FontConfig 为例 - 暗无天日 在Linux上限制儿童使用电脑 - 暗无天日 GIF不仅仅是一种图片格式——用GIF流做些奇怪的事 - 暗无天日 Leiningen 学习笔记:Clojure 项目构建与管理从入门到实战配置 - 暗无天日 Google SRE Book 读书笔记 - 暗无天日 yes 管道 head 发生了什么 - 暗无天日 为什么 nohup 在 crontab 中不起作用 Bash中的Indirection与Nameref - 暗无天日 Linux PAM 简介 - 暗无天日 从Linux ISO文件启动计算机 - 暗无天日 用 Bash 打造一个Screen Locker 用GitHub Actions自动构建EGO博客 - 暗无天日 blocking I/O 的作用 - 暗无天日 mobileog 手机端同步提示Error:2 No such file 的解决方法 回收 WSL2 VHDX 文件占用空间 使用 org-mode columnview 生成任务列表 - 暗无天日 Emacs 作为 MPD 客户端 - 暗无天日 移动文件路径却不破坏org file link的方法 - 暗无天日 如何合理的导出help link 成HTML - 暗无天日 笑话理解之Biology - 暗无天日
排查Linux进程"卡死"实战:从strace到gdb全流程 - 暗无天日
2026-04-19 · via 暗无天日

故障现象

某天,我在 Orange Pi 上跑了一个批量处理 URL 的脚本,用 claude CLI 把网上的文章自动转成知识卡片。脚本跑了一晚上,第二天发现它好像"卡住了"——终端没有任何输出,进度也不再推进。

怎么确认它真的卡住了、又卡在哪里呢?下面是完整的排查过程。

排查过程

用 strace 查看进程在干什么

strace 是 Linux 下排查进程问题的神器,它能追踪一个进程正在调用的所有系统调用(system call)。系统调用是程序向内核请求服务的接口,比如读文件、写网络数据、等待定时器等。

先用 ps 找到卡住的进程 PID,然后 attach 上去:

sudo strace -f -p 169047 -o /tmp/169047.log
  • -f :追踪子进程和子线程
  • -p 169047 :attach 到 PID 为 169047 的进程
  • -o /tmp/169047.log :将输出写入日志文件

等了几秒钟后按 Ctrl+C 停止,然后查看日志。

分析 strace 输出

日志有 7049 行,初看很吓人,但关键是找到规律。先看前面几行:

169080 read(16,  <unfinished ...>
169059 epoll_pwait2(13,  <unfinished ...>
169047 epoll_pwait2(4,  <unfinished ...>
169048 futex(...) = -1 ETIMEDOUT (Connection timed out)
...

每一行的格式是: 线程ID 系统调用(参数) = 返回值

这里有多个不同的数字(169047、169048、169049……),它们是什么?同一个进程可以有多个 线程 ,每个线程有自己的 ID(TID),但它们共享同一个进程的内存和文件描述符。

初步归类

过滤掉重复的 sched_yield (让出 CPU)和 futex (线程同步)后,我发现了这些有意义的操作:

169047 (主线程) epoll_pwait2(4, [], ...) 反复超时返回空 事件循环在等事件,但什么都没来 169080read(16, ...) 从第1行卡到最后一行 一直阻塞在读操作上 169049-169053 大量 sched_yield 工作线程在空转 169059epoll_pwait2(13, ...) 周期性返回事件 有事件到来,但…… 169057-169061 周期性 statx 检查 .git 文件 正常的文件监控轮询
线程 系统调用 含义

主线程 169047 在做事件循环(event loop):等事件 → 超时 → 检查一些状态 → 回去继续等。epoll 每次返回的都是空数组 [] ,说明主事件循环上 没有事件就绪

线程 169080 从日志开始到结束一直卡在 read(16, ...) 上,从未返回。看到 read() 阻塞,我的第一反应是: 这个进程大概在等网络响应 。毕竟它是一个调用 API 的程序, read() 卡住最常见的原因就是网络请求发了出去,但服务端的响应迟迟没到。但这个猜测对不对呢?

小知识:什么是文件描述符(fd)?

在 Linux 中,所有 I/O 操作都通过文件描述符进行。打开一个文件、建立一个网络连接、创建一个管道,都会得到一个 fd。它就是一个整数编号(0=标准输入,1=标准输出,2=标准错误,3 以上由程序自己分配)。

fd 不仅对应普通文件,还可以对应网络 socket、管道、定时器、inotify 实例等。知道了 fd 的编号,不等于知道它的类型——我们需要到 /proc 文件系统中去查看。

确认 fd 16 的类型:不是网络 socket

ls -la /proc/169047/fd/16

输出:

lr-x------ 1 orangepi orangepi 64 /proc/169047/fd/16 -> anon_inode:inotify

fd 16 是一个 inotify 实例!inotify 是 Linux 的文件系统事件监控机制,用来监听文件变化(比如文件被修改、创建、删除)。 read() 在 inotify 上阻塞是 设计上就是这样的 ——它只在有文件变化时才返回数据,没有变化时阻塞等待是天经地义的事。

但是,一个 inotify 线程阻塞,并不能解释整个进程"卡住"的现象。这个线程只是一个后台的文件监控器,它的职责就是安静地等文件变化。进程之所以"卡住",是因为它无法完成自己的核心任务——处理 URL。所以 fd 16 不是故障原因,我需要继续找。

转向线程 169059:epoll fd 13 只收到定时器事件

strace 日志中,线程 169059 也在做一个 epoll_pwait2 。单独提取它的记录:

grep '169059' /tmp/169047.log

输出:

169059 epoll_pwait2(13,  <unfinished ...>
169059 <... epoll_pwait2 resumed>[{events=EPOLLIN, data=...}], ...) = 1
169059 read(14, "\1\0\0\0\0\0\0\0", 8)  = 8
169059 epoll_pwait2(13,  <unfinished ...>
169059 <... epoll_pwait2 resumed>[{events=EPOLLIN, data=...}], ...) = 1
169059 read(14, "\1\0\0\0\0\0\0\0", 8)  = 8
169059 epoll_pwait2(13,  <unfinished ...>
...(重复多次)

线程 169059 在反复做一件事: epoll_pwait2(13, ...) → 返回 1 个事件 → read(14, ...) → 回到 epoll_wait

注意一个规律:每次 epoll 返回后,线程读的都是 fd 14 (后面可以看到这是个定时器)。也就是说,线程在重复等待 epoll fd 13 上的事件。那 epoll fd 13 到底监控了哪些 fd?我们需要查两样东西:进程的完整 fd 列表(搞清楚每个 fd 是什么),以及 epoll fd 13 的监控清单(搞清楚它在等谁的数据)。

查看 fd 全貌和 epoll 监控清单

先看进程一共有哪些文件描述符:

ls -la /proc/169047/fd/

输出中能看到的 fd 类型包括:

0 /dev/pts/1 标准输入(终端) 1, 2, 11, 12 pipe 管道(stdout/stderr 重定向) 3, 9 /dev/urandom 随机数生成器 4 anon_inode:[eventpoll] 主事件循环的 epoll 实例 5, 7, 8 anon_inode:[timerfd] 定时器 6 anon_inode:[eventfd] 事件通知 10 /proc/169047/statm 进程内存统计 13, 32 anon_inode:[eventpoll] 另外两个 epoll 实例 16 anon_inode:inotify 文件系统监控 28socket:[616763]唯一的网络 socket!
fd 类型 说明

进程里有三个 epoll 实例(fd 4、fd 13、fd 32)。查看 epoll fd 13 的监控清单:

cat /proc/169047/fdinfo/13

输出:

tfd:       28 events:       19 data:2611c3c00a0    ← 网络 socket
tfd:       15 events: 80000019 data:2611c0a0100    ← eventfd
tfd:       14 events:       19 data:2611c0a00c0    ← timerfd

/proc/PID/fdinfo/EPOLL_FD 中的 tfd 字段就是被监控的目标 fd 编号。epoll fd 13 同时监控了三个 fd:fd 28(网络 socket)、fd 15(eventfd)、fd 14(定时器)。

strace 中只看到 fd 14(定时器)触发,fd 28 和 fd 15 都没有。但 fd 15 不触发并不奇怪——它是一个 eventfd ,这是一种线程间内部通知机制,只有当其他线程主动往里写数据时才会触发。如果内部没有需要通知的事情,它安静待着是正常的。

而 fd 28 就不同了。它是 网络 socket ,是进程与外部世界通信的唯一通道。这个进程是 claude CLI,它向 API 服务器发出了请求,正在等响应。socket 长时间不触发,意味着服务端的响应数据迟迟没到——这才是进程"卡住"的根本原因。

检查网络连接状态

找到了网络 socket fd 28,它的 inode 号是 616763。接下来要搞清楚这个 socket 连接的是谁、状态如何。

从 /proc/net/tcp 找到连接详情

/proc/net/tcp 文件列出了系统所有 TCP 连接。我们需要从中找到 inode 为 616763 的那一条:

cat /proc/169047/net/tcp

输出很长,但关键的一行是:

sl  local_address    rem_address   st tx_queue:rx_queue ... inode  ...
13: 191FA8C0:CF72   33551777:01BB 01 00000000:00000000  ... 616763 ...

inode 616763 匹配上了!但 local_addressrem_address 都是十六进制,怎么看懂?

解码 /proc/net/tcp 的地址格式

/proc/net/tcp 中的地址格式是 IP地址:端口 ,其中 IP 地址是 十六进制、小端序 编码的。以远端地址 33551777:01BB 为例:

远端 IP:   33551777 → 每 2 位拆成一个字节: 33 55 17 77 → 反转字节序: 77 17 55 33 → 119.23.85.51
远端端口:  01BB → 十六进制转十进制 → 443 (即 HTTPS)

同理,解码本地地址 191FA8C0:CF72

本地 IP:   191FA8C0 → 19 1F A8 C0 → 反转: C0 A8 1F 19 → 192.168.31.25
本地端口:  CF72 → 53106

所以这个连接是:本机 192.168.31.25:53106 → 远端 119.23.85.51:443

小知识:为什么要反转字节序?

/proc/net/tcp 中的 IP 地址使用 网络字节序 (大端序)存储,但在 x86/ARM 等 小端序 机器上,内核为了存储效率,按小端序输出十六进制。所以 33551777 实际上是 0x77175533 的小端序表示,对应 IP 119.23.85.51

简单记忆: 把十六进制字符串每两位切一刀,然后倒着拼回去,再转成十进制 IP

用 ss 查看连接的详细状态

拿到了远端 IP 后,用 ss 命令查看这个连接的详细信息:

ss -ti dst 119.23.85.51
  • -t :只显示 TCP 连接
  • -i :显示详细信息(RTT、超时、收发字节数等)
  • dst 119.23.85.51 :只看连接到这个 IP 的

输出中最关键的字段:

ESTAB  0  0  192.168.31.25:53106  119.23.85.51:https
  bytes_sent:2114416  bytes_acked:2114417
  bytes_received:586087
  lastsnd:10621788  lastrcv:10621508

解读:

状态 ESTABLISHED TCP 连接看起来还活着 bytes_sent / bytes_acked ~2MB / ~2MB 请求已完整发送并被服务端确认 bytes_received ~572KB 收到了部分响应 lastsnd10,621,788 ms ≈ 2.95 小时最后一次发送数据是近 3 小时前lastrcv10,621,508 ms ≈ 2.95 小时最后一次接收数据也是近 3 小时前
指标 含义

真相大白:TCP 半死连接

连接的状态是 ESTABLISHED,但近 3 小时没有任何数据流动。这说明:

  1. 进程发送了约 2MB 的 API 请求(完整到达服务端)
  2. 服务端回了约 572KB 数据后,传输中断了
  3. 中间的网络设备(NAT/防火墙)可能丢弃了这个长时间空闲连接的状态
  4. 由于没收到 TCP RST 或 FIN,进程端以为连接还活着,一直在等

这就是经典的 TCP 半死连接 (half-open connection)问题。TCP 本身不会因为长时间无数据就主动关闭连接(除非开启了 TCP keepalive),如果应用层也没有设置读取超时,进程就会 无限期等待

恢复过程

问题 1:能不能安全 kill?

在决定 kill 之前,需要了解进程之间的关系:

ps -ef --forest | grep -A5 url-to-cards
bash url-to-cards.sh (138905)        ← 主脚本
  └── bash (169046)                  ← command substitution 子 shell
      └── claude (169047)            ← 卡住的 claude 进程

脚本开头设置了 set -euo pipefailset -e 会让脚本在任何命令返回非零时立即退出。但脚本中有这样的写法:

output=$(claude ...) || true

|| true 的作用是:即使 claude 命令失败,表达式整体也会返回成功( true 的返回值是 0)。这样 set -e 就不会触发,脚本不会退出。

所以,kill 掉卡住的 claude 进程(169047)后:

  1. claude 收到信号退出
  2. 子 shell 也退出(非零)
  3. || true 兜住错误
  4. 脚本继续处理下一个 URL

问题 2:原始 URL 列表丢失了

脚本读取的 URL 文件 /tmp/feed2mail.urls.txt 已经被另一个程序覆盖了,原始内容丢失了。但 bash 进程(138905)启动时已经把 URL 列表读进了内存中的 urls 数组。

用 gdb 从进程内存恢复数据

gdb (GNU Debugger)虽然主要用来调试程序,但也能 attach 到运行中的进程,读取它的内存。

cat /proc/138905/maps | grep heap

sudo gdb -batch -p 138905 \
  -ex 'dump memory /tmp/bash_heap.bin 0xaaaaec588000 0xaaaaec60c000' \
  -ex 'detach'

strings /tmp/bash_heap.bin | grep -oE 'https?://[^[:space:]>"]+' | sort -u
  • dump memory :将指定地址范围的内存写入文件
  • strings :从二进制数据中提取可打印字符串
  • grep -oE 'https?://...' :用正则匹配 URL
  • sort -u :去重排序

成功恢复了 189 个 URL

小知识:为什么要从堆(heap)恢复?

进程的内存分为多个区域:

  • 代码段(text) :存放程序指令
  • 数据段(data) :存放全局变量
  • 堆(heap) :动态分配的内存(bash 变量存在这里)
  • 栈(stack) :函数调用栈

bash 的变量(包括数组)存储在堆内存中,所以我们从堆区域提取数据。

总结

排查流程回顾

进程卡住了
  │
  ├─ 1. strace -f 看进程在做什么系统调用
  │     → 发现多个线程:主线程 epoll 空转,线程 169080 read 阻塞,线程 169059 epoll 周期触发
  │
  ├─ 2. ls -la /proc/PID/fd/16 看 fd 16 指向什么
  │     → 是 inotify(文件监控),阻塞是正常行为,排除
  │
  ├─ 3. 回到 strace,细看线程 169059
  │     → epoll_pwait2(13) 每次返回后都读 fd 14(定时器),从不读 fd 28(网络 socket)
  │
  ├─ 4. cat /proc/PID/fdinfo/13 确认 epoll 监控清单
  │     → epoll fd 13 监控了 fd 28(网络 socket),但 socket 从未触发事件
  │
  ├─ 5. ss -ti 查看网络连接状态
  │     → lastsnd/lastrcv 约 3 小时前 → TCP 半死连接
  │
  ├─ 6. 分析代码确认 kill 安全(|| true 兜底)
  │
  └─ 7. gdb dump memory 恢复丢失的数据
        → 从堆内存中提取出 189 个 URL

工具速查

strace -f -p PID 追踪进程的系统调用 看进程卡在哪个 syscall ls -la /proc/PID/fd/ 查看进程打开的所有文件描述符 确认 fd 是 socket、pipe 还是 inotify cat /proc/PID/fdinfo/N 查看 fd 的详细信息 看 epoll 监控了哪些 fd cat /proc/PID/maps 查看进程内存布局 找到堆地址范围 ss -ti 查看 TCP 连接详情 看 RTT、超时、最后收发时间 ps -ef --forest 查看进程树 理解父子进程关系 gdb -batch -p PID attach 到运行中的进程 导出内存、查看变量
工具 用途 示例

反思与改进

这次事件暴露了几个可以改进的地方:

  1. 应用层超时 :claude CLI 应该给 API 请求设置合理的读取超时(如 5-10 分钟),避免死连接无限挂起
  2. TCP keepalive :开启 TCP keepalive 可以让内核自动检测死连接
  3. 脚本健壮性 :可以在脚本中为每个 claude 调用加上 timeout 命令:

    timeout 600 claude -p ...  
    
  4. URL 文件保护 :脚本应该在读入 URL 后立即备份,或者用管道传递而非依赖临时文件