返回

文章详情

追踪 Zsh 历史数据丢失的 bug

Hacker News2026年8月15日 21:39

目录 首先是好消息 症状 我的 Zsh 历史配置 跟踪行为 inotify fatrace strace bpftrace 让它崩溃! 问题是什么? 结论 附录 A:额外的隐患:导出的 HISTFILE 附录 B:额外问题:AI 能找到这个 bug 吗? 评估设置:症状 + bpftrace 评估变体:习惯暗示的 AI 结论 多年来,我有时发现我确信自己运行过的命令不再出现在我的 Z shell 历史文件(~/.zsh_history)中。在这篇文章中,我将向您展示我是如何追踪到这个 bug 的。剧透:最终,修补 Zsh 使其大声崩溃并分析崩溃的核心转储是获胜的策略! 首先是好消息 Zsh 5.9.2(发布于 2026 年 7 月 12 日)包含了这个问题的修复——在阅读完这次调查后打开它,以免破坏乐趣。 剧透:指向上游修复的链接 Zsh 修复 53454 症状 偶尔,我注意到我知道自己在前一天执行过的命令在我的 shell 历史中找不到,这意味着按 Ctrl+R 进行向后历史搜索没有结果。每当我注意到这一点时,我的 shell 历史文件只包含非常旧的条目,缺少数年的新条目。发生这种情况的前几次,我只是从每日备份中恢复了我的 shell 历史,没有进一步调查。但是,这个问题不断出现。我注意到 .zsh_history 中没有明显的损坏(没有不可打印字符或不完整的文本行),并且文件中的行数并不总是相同。我不清楚的是,这个问题是由 Zsh 本身、某个其他程序,还是多个 zsh(1) 进程的组合导致的。 我的 Zsh 历史配置 我在我的 ~/.zshrc 中设置了以下与历史相关的选项: # 加载 4000 行历史(用于 Ctrl+R 向后搜索),但保存 O(∞) HISTSIZE=4000 HISTFILE=~/.zsh_history SAVEHIST=10000000 # 不保存(相邻)重复条目 setopt HIST_IGNORE_DUPS # 当命令运行时,将历史记录条目追加到 `~/.zsh_history`。 setopt INC_APPEND_HISTORY # …但不共享历史(在 NixOS 的 /etc/zshrc 中默认启用)。 unsetopt SHARE_HISTORY 在实践中,这意味着我的 shell 是独立会话,所有会话都将命令流入一个共享的 ~/.zsh_history 中。历史记录故意不共享,因此当我想访问另一个 shell 写入的条目时,我会显式运行 exec zsh。 跟踪行为 当我在 2024 年 12 月的 Mastodon 上寻求帮助时(主要是希望其他人已经遇到并诊断了这个问题),我收到的一个建议是使用文件系统变更监控机制,如 inotify 或 fsevents,来找到截断(或更改?) Zsh 历史文件的罪魁祸首。接下来的部分将逐步介绍我尝试的 Linux 上的可用选项。 inotify inotify(7) Linux 内核子系统是 Linux 中最古老的文件系统变更监控 API 之一(于 2005 年发布)。要很好地理解 Zsh 如何修改历史文件,仅监控 .zsh_history 是不够的: midna ~ % inotifywait --monitor .zsh_history 设置监视。 监视已建立。 .zsh_history OPEN .zsh_history ACCESS .zsh_history ACCESS […] .zsh_history ACCESS .zsh_history CLOSE_NOWRITE,CLOSE .zsh_history ATTRIB .zsh_history CLOSE_WRITE,CLOSE .zsh_history DELETE_SELF ^C 文件被打开、访问(= 读取),然后……删除了?!通过监控包含的目录,我们可以看到整个过程: midna ~ % inotifywait --monitor ~ /home/michael/ OPEN .zsh_history /home/michael/ ACCESS .zsh_history /home/michael/ ACCESS .zsh_history […] /home/michael/ ACCESS .zsh_history /home/michael/ CLOSE_NOWRITE,CLOSE .zsh_history /home/michael/ CLOSE_WRITE,CLOSE .zsh_history /home/michael/ OPEN .zsh_history /home/michael/ CLOSE_WRITE,CLOSE .zsh_history /home/michael/ OPEN .zsh_history /home/michael/ ACCESS .zsh_history /home/michael/ CLOSE_NOWRITE,CLOSE .zsh_history /home/michael/ CREATE .zsh_history.new /home/michael/ OPEN .zsh_history.new /home/michael/ ATTRIB .zsh_history.new /home/michael/ MODIFY .zsh_history.new /home/michael/ CLOSE_WRITE,CLOSE .zsh_history.new /home/michael/ MOVED_FROM .zsh_history.new /home/michael/ MOVED_TO .zsh_history /home/michael/ CLOSE_WRITE,CLOSE .zsh_history 所以 Zsh 读取旧的历史文件内容,将其写入新文件,然后将新文件重命名为旧文件,从而删除旧文件。现在一切都明白了!不幸的是,我们没有看到导致文件系统事件的进程 ID(PID),即使使用同级实用程序 fsnotifywait(1),它使用 fanotify(7),一个提供此信息的 API!我检查过,内核确实发送了 PID,但是 fsnotifywait 不显示 PID。 fatrace 幸运的是,有 fatrace(8),可以显示进程名称和 PID。以下是 Zsh 的历史重写通过 fatrace(8) 的情况: zsh(197994): CWO /home/michael/.zsh_history zsh(197994): O /home/michael/.zsh_history zsh(197994): R /home/michael/.zsh_history

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡