← 返回

1024程序员节!

2026/10/24 22:13 日常 日常

1024 了,我搓了个 Linux,还欠一个竞态没还


今天这个日期,程序员看一眼就懂了。

1024。2 的 10 次方,10 个二进制位能表示的组合数,1 KiB 的字节数。别人过节看农历看公历,我们过节看幂运算,多少有点职业习惯。

我今年这个 1024 过得比较实在:桌上摊着两份东西,一份是刚发出去的 lfOS v1.0.0,一份是 NFrp v0.6.4 的构建日志。一个是我从零撸的 Linux 发行版,一个是我用 Rust 重写的 frp。

都不算大项目,但都是我自己一行一行写出来的。


先说说我今年搓的两个东西

lfOS(LumenFluxOS / 流光OS),简单说就是我按 LFS/BLFS 那套方法,从源码开始自己搭了一个 Linux 发行版。

不是”装了个 Arch 然后改改配置”那种。是真的自建交叉工具链、自编译内核、自己写 init、自己搭 apt/dpkg 和 CA 证书那一套。

五个版本走下来解决五个问题:

版本 干了啥
v0.0.1 跑通启动链路:自建工具链 → 自编译内核 → 自研 init → shell
v0.0.2 系统级性能调优,网络内存磁盘参数全线调
v0.0.3 内核瘦身 30%(9.5MB → 6.7MB)
v0.0.4 启动再快 0.24s
v1.0.0 安全基线:去掉所有硬编码凭据

最后出来长这样:内核 6.15.4-lfos,bzImage 6781 KB,ISO 163 MB,内存占用 121 MB,没有 systemd,没有图形界面。

对,没有 systemd。我写了个 9 步的 bash init 顶上。

NFrp 是另一个方向。frp 是个很好用的内网穿透工具,但原版是 Go 写的,我自己用 Rust 重写了一版,兼容 frp 协议。

这个项目我做了十轮安全审计、两轮外部渗透测试,累计 146 项修复。v0.6.4 的测试数量是 785 个。

之所以说这个,是因为下面要讲的坑,全都是这两个项目里真实踩的。


我以为对的地方,其实不对

写代码写久了会有个错觉:觉得自己对系统的理解已经到位了。

然后你就被系统教育了。

lfOS 这一路,真正卡住我的大多不是”不会写”,而是我以为对的地方其实不对:

  • /tmp 在启动早期根本不存在。init 里少写一行 mount,两个功能直接废掉,报错还绕得很远。
  • kconfig 对不存在的符号是静默忽略的。你写了个不存在的配置项,它不吭声,你以为配上了,其实没有。
  • chroot 里挂载的是**构建机的 /proc**。你以为在验证目标系统的行为,实际看的是构建机的。
  • 全树 chmod 会顺手把 setuid 位一起抹掉。系统看着一切正常,su 和 passwd 全废了。

最后这条折腾我最久。打完包发现 12 个关键程序的权限全丢了,su、passwd、mount 全成 755,系统看着能用,实际谁都不进来。查半天才反应过来是全树 chmod 的锅。

这些坑写进文档就一句话。真踩上去,一个能卡一整天。


一个我没修好的 Bug

这个得单独拿出来说,因为它是我今年收获最大的一次失败。

NFrp 的 plugin_bridge 在 Linux 上有 6 个端到端测试间歇性失败,而且是真实 TCP 链路也能复现的那种。同一个测试连跑 10 次:5 次通过,5 次 5 秒超时。失败的时候插件能收到连接,但请求从来没转给上游。

根因找到了:HttpIo::watch_closed() 用 read(&mut chunk) 读进一个局部数组,future 在 select! 里被取消时,那批字节就跟着消失了。丢掉的很可能正好是请求体的开头。

原注释写着”取消安全:tokio 的 AsyncRead::read 在取消时不消费数据”。

那句话是错的。

我试了两次修复:

  1. 改成 read_buf 直接读进 self.buf,缓冲非空就立即返回。结果调用方的 loop { select! } 变成了 100% CPU 忙等死循环。云端 4 个进程各烧了 20 分钟 CPU,本地单进程跑了 1288 秒。
  2. 再在循环里加短路判断。还是空转——read_buf 已经把数据吸进缓冲了,available() 恒大于 0。

最后我把代码回退了,只在注释里写清缺陷、复现方式、两次失败的原因,以及正确方向:让”读”只有一个所有者,单独一个读协程独占 stream,把字节推进共享队列,等待方只等队列信号。这需要重构 HttpIo 的所有权结构。

判断依据很朴素:宁可保留一个只在 Linux 上间歇出现的缺陷,也不引入一个会让 CPU 跑满的死循环。后者影响面大得多。

这件事之后我对”修 Bug”这件事的看法变了。修不掉的 Bug 不可怕,可怕的是为了让它消失而引入一个更大的问题,然后骗自己说已经解决了。


785 个测试到底值不值

有人问我,一个小项目搞 785 个测试是不是过度了。

我说你看上面那个竞态——6 个测试间歇失败,真实链路也能复现。如果我没有这些测试,这个缺陷会一直躺在代码里,等到某个用户的高并发场景才炸。

测试不是为了刷数字。它是为了让问题尽早暴露。

NFrp v0.6.4 的账是这么算的:

  • cargo test --workspace 串行跑:785 通过 0 失败
  • -j 2 连跑 5 次:5/5 全绿(专门验证那个端口竞态)
  • clippy:Windows 0 告警,Linux 云端也 0 告警
  • cargo audit:184 个 crate 零漏洞
  • cargo deny:四项全绿
  • 端到端冒烟:28/28

这里有个坑值得说:Windows 上 clippy 通过 ≠ Linux 通过。cfg(target_os) 分支不一样,平台特定代码必须上云端验。我那处 tokio 的 AsyncFd::new 弃用问题,就只在 Linux 下编译才暴露出来,Windows 的 clippy 完全看不到。


程序员节真正想要的礼物

我看过不少程序员节的稿子,写得都挺热闹,但大部分在讲”程序员改变世界”。

说实话,我们不太需要这个。

我更想要的是这些:

一个能复现的问题描述。 “打不开”这三个字后面能跟上的信息,决定了我要花十分钟还是三天。

一次说完整的需求。 目标是谁、必须解决什么、什么可以以后做、验收标准是什么。明确边界不是拖延,是减少返工。

一句准确的技术判断。 不是所有问题都能靠加个中间件解决。先理解约束,再选技术,通常更便宜。

一个不在周五 17:59 的上线计划。 发布窗口也是系统设计的一部分。

还有一个我觉得最重要的:允许承认没修好。

上面那个竞态,我最后是回退 + 记录。发版说明里我老老实实写了”1 个 Linux 竞态没修好,已回退并如实记录”。

有人可能会觉得这样显得不专业。但我觉得反过来——把一个没修好的东西藏起来,或者用一个更危险的方式强行掩盖,那才是不专业。


关于”从来如此,便对么”

我网站副标题是这句话,鲁迅《狂人日记》里的。

放在写代码这件事上,我理解是这样的:

  • “从来如此”——大家都这么写,框架默认这么干,文档这么说的
  • “便对么”——那你真去验证过吗

lfOS 里那条注释写着”取消安全”,我当时信了。结果它是错的。
kconfig 静默忽略不存在的符号,文档里没写,我踩了才知道。
setuid 位被 chmod 抹掉这件事,也是一堆人踩过但没人当回事的”从来如此”。

所以这两个项目做下来,我最大的收获不是写了多少行代码,而是对”我以为”这三个字变得警惕了。

不确定的地方就去量一遍。写着”理论上没问题”的地方,就跑一遍试试。注释里那种断言的语气,往往是最危险的地方。


最后

1024 这天,我不想许什么”改变世界”的愿。

我只想祝:

愿你今天提交的代码一次通过。
愿你收到的 Warning 都有人已经准备好了修法。
愿你遇到的 Bug 都有清晰的复现步骤。
愿你的构建日志最后一行永远是成功。
然后——愿你在成功之后还记得备份。

如果必须许一个更现实的:

少一点不明原因的线上告警,多一点可验证的确定;少一点”先上再说”,多一点”知道怎么回来”。

1024 快乐。

今天可以拆 Warning,但最好别拆 P0。


项目:lfOS | NFrp

评论

  • 还没有评论,来抢个沙发。