文章摘要
作者追踪并修复了一个Zsh历史记录数据丢失的bug。通过inotify、strace等工具定位问题,发现是多个Zsh进程同时写入历史文件时导致截断,并给出了解决方案。
文章总结
好的,这是根据您的要求,对原文主要内容进行的中文重述,保留了关键细节,并删减了与核心主题无关的内容(如附录中详细的AI评估表格和部分技术细节)。
追踪Zsh历史记录数据丢失的Bug
核心问题: 作者多年来偶尔发现,自己确信执行过的命令在Zsh历史记录文件(~/.zsh_history)中神秘消失。经过深入调查,最终定位并修复了一个存在约10年的数据丢失Bug。
好消息: Zsh 5.9.2版本(2026年7月12日发布)已修复此问题。
症状: 作者发现,有时历史记录文件中的条目只剩下非常陈旧的内容,而近几年的新记录全部丢失。文件本身没有损坏迹象,但行数不一致。作者不确定是Zsh自身、其他程序,还是多个Zsh进程共同导致的问题。
Zsh历史配置: 作者配置了INC_APPEND_HISTORY选项,使得每个shell会话都将命令实时追加到共享的历史文件中,但未启用SHARE_HISTORY(历史共享)。这意味着不同shell会话各自独立地向同一个历史文件写入。
追踪过程:
inotify: 通过监控文件系统事件,作者发现Zsh并非直接修改历史文件,而是执行“读取旧文件 -> 写入新文件 -> 重命名新文件覆盖旧文件”的操作。但inotify无法显示执行操作的进程ID(PID)。
fatrace: 使用
fatrace工具,作者确认了执行读写操作的是Zsh进程本身,并获得了PID。但这仍无法揭示每个Zsh进程具体读写了多少数据。strace: 作者认为对每个交互式Zsh进程都附加
strace追踪不切实际,且可能改变行为,因此未采用。bpftrace: 作者编写了bpftrace程序,监控Zsh对历史文件的所有系统调用。通过长期后台运行,作者捕获了一次历史记录被截断时的日志。关键发现是:在截断事件中,Zsh没有像正常情况那样读取文件直到末尾(
read = 0),而是读取了较少的数据(11575296字节),然后写入了同样较少的数据(11572944字节),并用这个不完整的新文件替换了原文件。
定位Bug:
- 制造崩溃: 为了在问题发生时获得更多信息,作者修改了Zsh 5.9.1的源代码,使其在写入的新历史文件行数少于50000行时主动崩溃。几天后,系统收集到了崩溃的核心转储文件。
- 崩溃分析: 通过GDB分析核心转储,作者发现:
- 崩溃发生在
savehistfile函数中,它只写入了45546行历史记录。 - 关键线索是
errflag变量被设置为2(表示ERRFLAG_INT,即中断标志),且lasthist.interrupted变量为1。 - 这表明
savehistfile在写入历史文件时,其内部的readhistfile函数被一个信号中断了。
- 崩溃发生在
根本原因:
- 触发场景: 作者有在一天工作结束时,通过反复按
Ctrl+D(退出会话)和Ctrl+C(中断)来关闭所有终端会话的习惯。当退出一个Zsh会话(Ctrl+D)时,Zsh会调用savehistfile来压缩历史文件。如果此时恰好有Ctrl+C信号(SIGINT)到达,就会中断savehistfile内部的readhistfile函数。 - Bug所在:
readhistfile函数在读取历史文件时,会检查中断标志(errflag & ERRFLAG_INT),如果收到信号会提前跳出读取循环。然而,savehistfile函数在退出时写入历史文件的过程中,没有检查中断标志。因此,当readhistfile被中断并返回一个不完整的历史记录后,savehistfile会忠实地将这个不完整的内容写入新文件,并覆盖掉完整的旧文件,从而导致数据丢失。
结论: 这个导致数据丢失的Bug在流行的Zsh shell中存在了约10年之久。作者非常高兴此问题最终在Zsh 5.9.2中得到修复。
评论总结
根据评论内容,总结如下:
主要观点与论据:
Zsh历史记录丢失是常见问题(认可度:高)
- 多位用户反映曾遭遇类似问题,如aclindsa表示“感觉丢失过zsh历史但未深究”,nubinetwork指出“bash中同时打开两个终端时,最后关闭的终端会覆盖历史记录”。
- 关键引用:aclindsa "I have felt like I lost zsh history before but never looked into it";nubinetwork "That's a bug? Happens to me with bash all the time... whichever one I close last is the one that writes its history."
现有解决方案存在争议(认可度:中等)
- 部分用户推荐替代工具,如fragmede建议使用atuin.sh;petters则采用sqlite数据库存储命令历史。
- 关键引用:fragmede "Are people not just using atuin.sh these days?";petters "I store all commands executed in an sqlite database... The Bash history is useful for Ctrl+R"
对系统架构的批评(认可度:中等)
- TZubiri批评默认2000条命令限制,认为“需要革命而非渐进优化”,并指出用户不应拥有删除自己命令历史的权限。
- 关键引用:TZubiri "You can't expect much from a system that has those defaults... a revolution is needed, not incremental optimizations"
对调试工作的赞赏(认可度:高)
- myshapeprotocol称赞“追踪shell历史内部数据丢失边缘案例需要绝对精确”,conferza表示“非常有趣的调查”。
- 关键引用:myshapeprotocol "Tracking down subtle data loss edge cases in shell history internals requires absolute precision";conferza "A very interesting investigation"
替代方案建议(认可度:低)
- pratyahava建议“将每个会话的命令历史写入单独文件,而非合并到一个文件”,认为这是更简单的文件系统方案。
- 关键引用:pratyahava "why do this 'heroic' effort of 'cleverly' putting everything in one file... if we could just write command history of every session into a new separate file"
平衡性总结: - 多数用户认可问题存在且值得解决,但对解决方案存在分歧:部分支持现有修复,部分推荐第三方工具,部分主张根本性架构改革。 - 对调试工作的专业性和细致程度普遍表示赞赏,但对其实际效果存在不同看法。