阅读视图

发现新文章,点击刷新页面。
🔲 ☆

GPU 计算的起源

本文永久链接 – https://tonybai.com/2026/04/17/the-origins-of-gpu-computing

大家好,我是Tony Bai。

在今天的人工智能时代,GPU 已成为数据中心的核心算力引擎,但它的崛起并非一夜之间的奇迹。ACM通讯文章《The Origins of GPU Computing》回溯了 GPU 计算的三十年发展史,揭示了从并行计算、图形系统到流处理等关键技术如何在政府资助的学术研究中逐步成熟,并最终汇聚成推动深度学习革命的基础设施。文章不仅梳理了技术脉络,也展示了学界与产业之间如何通过人才与思想的流动,共同塑造了现代 GPU 计算的格局。

本文是这篇文章的译文,供大家学习参考(格式有调整,更适合公众号阅读)。


政府资助的并行计算、流处理、实时着色语言和可编程图形处理单元(GPU)的学术研究直接推动了 GPU 计算的发展。GPU 被广泛应用于现代数据中心,并促成了当前的人工智能(AI)革命。生产 GPU 的英伟达(Nvidia)现已成为世界上最有价值的公司。这种计算变革及其产生的经济价值,得益于超过 30 年的政府资助研究。政府资助不仅有助于发展许多关键的技术创新,还培养了大量将这些技术带入行业的学生。

本文追溯了 GPU 计算的起源。我们首先描述了 GPU 计算所构建的技术(并行计算、并行图形系统、可编程着色器(shaders)和流处理)的发展,然后详细介绍了这些技术是如何转移到英伟达和其他公司,并最终应用于现代机器学习的。

赋能技术

GPU 计算建立在并行计算、并行图形系统和流处理的早期工作基础上。这些技术是通过超过 30 年的政府资助学术研究发展而来的。

并行计算

当你学习计算时,你了解到的是中央处理器(CPU)按顺序执行一系列指令。

实际上,芯片包含数十亿个并行切换并由导线连接的晶体管。开关和导线是物理计算机的基本构建块,它们同时运行。

此外,晶体管切换消耗的能量很少,而沿导线的通信消耗的能量要多得多。

通信需要功率来将信号从一点发送到另一点;功率随着距离的增加而增加,如果是在芯片之间进行信号传输,功率消耗将非常巨大。

虽然顺序计算机可能比并行计算机更容易理解,但顺序计算机必须通过同时切换的晶体管和同时传输信息的导线来实现。顺序计算机使用许多晶体管并行计算结果,然后仔细地以与顺序执行一致的方式组装这些结果。

创建这种执行是顺序的“幻觉”,在功率和性能上都是低效的。随着可用晶体管数量的增加,这种低效性也随之增加。在现代半导体技术中构建计算机的自然方式是设计并行计算机。GPU 比 CPU 更高效,因为它们是大规模并行计算机。

GPU 计算建立在并行计算的早期工作之上。与所有并行计算机一样,在 GPU 上运行的并行任务或线程必须相互同步和通信。

线程需要通信来使用由另一个线程产生的数据。同步是必要的,以在数据可用时发出信号,确保消耗的是正确的值。

并行计算、同步和通信的许多基础知识是由政府资助的学术研究开发的。由加州理工学院 Chuck Seitz 领导的 DARPA 资助的“宇宙立方”(Cosmic Cube)项目发展了并行计算的许多基础知识。在该项目上开发的硬件是英特尔 iPSC、Delta 和 Paragon 机器的蓝图,以及几台早期的能源部 ASCI 机器。“Cosmic-C”编程语言引入了异步消息传递和集合通信,后来以消息传递接口(MPI)的形式成为编程大型并行机器的标准。

麻省理工学院(MIT)的 DARPA 资助的 J-Machine 和 M-Machine 项目开发了用于通信和同步的低开销机制,以及现代互连网络的许多关键方面。这些机制使得并行性可以在非常细的粒度上被利用,最少只需 10 或 20 条指令即可作为一个可调度的工作单元。J-Machine 的许多特性被 Cray T3D 和 T3E 计算机直接采用。

并行计算有着超越这一特定历史分支的丰富历史。由于篇幅有限,我们无法进行完整的综述。Culler 等人的文章提供了一个很好的回顾。

GPU 计算与所有高性能计算一样,深受这一遗产的影响。它使用 MPI 进行节点间的通信,使用互连网络连接这些节点,并且在此研究过程中开发的许多通信和同步机制被用于协调并行计算。

并行图形系统

虽然不如传统的并行计算和超级计算机广为人知,但并行图形和成像计算机有着悠久的历史。

处理和生成图像需要巨大的计算量。例如,如果一台每秒处理一百万条指令的计算机(1MIPS)对百万像素图像的每个像素应用一次算术运算,计算机需要一秒钟来处理一张图像。

渲染电影和游戏中的 3D 虚拟世界比图像处理每像素需要的计算量大几个数量级。例如,为现代电影生成的图像每个像素需要大约十亿次浮点运算。因此,为了在实践中有用,图形和成像需要高性能的并行超级计算机。这些计算机在大规模数据集合上并行计算。

一个早期的 DARPA 资助研究项目是吉姆·克拉克(Jim Clark)在斯坦福大学领导的几何引擎(Geometry Engine)。

几何引擎促成了硅谷图形公司(Silicon Graphics)的成立,该公司率先开发了 3D 图形工作站。SGI 硬件架构和 OpenGL 软件库定义了现代 GPU 架构。

另一个值得注意的政府资助研究项目是亨利·福克斯(Henry Fuchs)及其合作者在北卡罗来纳大学领导的 Pixel Planes 系列高性能图形系统。事实上,Pixel Planes 5 是一台相当通用的单指令多数据(SIMD)计算机,它在 128 x 128 图像上运行并行计算。其他早期并行图形和图像计算机的例子包括 NASA 的大规模并行处理器(MPP)、Ikonas 图形系统和 Pixar 图像计算机。

早期 GPU 实现了类似于早期 SGI 工作站的固定功能图形流水线。当整个 OpenGL 图形流水线可以在单个芯片上实现时,英伟达引入了“GPU”一词。1999 年推出的英伟达 Geforce 256 由 1700 万个晶体管组成,是第一款商用 GPU。

在此之前,在皮克斯(Pixar)工作期间,Hanrahan 开发了 RenderMan,这是一个生成照片级逼真图像的系统。该系统彻底改变了电影行业,因为它能够生成可以与相机拍摄的实景无缝结合的图像。RenderMan 的一个关键组件是着色语言,它使用户能够扩展系统以模拟复杂的材质和光照。

虽然最初的 GPU 实现了固定功能流水线,但它们是由可编程组件构成的。不幸的是,这些处理单元因系统而异,因代而异。需要的是一种可移植的编程模型。由于 GPU 的主要应用是电脑游戏,因此将 RenderMan 着色语言适配到 GPU,以便游戏开发者可以创造新的光照和着色效果似乎是自然而然的。

在斯坦福大学的一个 DARPA 资助项目下,为当时的 GPU 设计并实现了一种实时着色语言(RTSL)。着色语言程序现在被称为着色器(shaders)。博士后学者 Bill Mark 领导了斯坦福 RTSL 的设计,后来加入了英伟达。他与另一位前斯坦福研究生 Kurt Akeley 一起增强了该技术,并创建了 Cg 着色语言。Cg 导致了微软 HLSL 和 OpenGL GLSL 的开发。

人们很快意识到,这些早期的着色语言足够灵活,可以实现科学计算中的许多算法。研究人员采用了诸如矩阵乘法、线性求解器、流体动力学求解器和分子动力学等算法在着色器上运行。这导致了 GPGPU(通用 GPU)计算运动的兴起。

流处理

DARPA 和 DOE 在斯坦福大学资助的关于 Imagine 流处理器和 Merrimac 流式超级计算机的工作发展了流处理,这是一种导致算术强度(计算与带宽之比)增加的并行计算形式。

如前所述,处理器消耗的大部分功率是在通信上。在芯片之间发送信号尤其耗电。芯片外通信也比芯片内通信慢得多。

流处理包含两个减少内存带宽需求的主要思想。

第一个是利用生产者-消费者局部性,使得一个阶段(生产者)将其结果转发给下一个阶段(消费者),而无需写入和读取内存。

第二个主要思想是将计算组织成称为内核(kernels)的函数。每个内核获取一个数据包,对该包执行函数,并输出另一个数据包。函数中的算术运算数量大于对内存的读写次数。这两种技术显著减少了内存访问次数,并提高了流处理架构的效率。

在流处理器中,计算被组织成产生和消耗数据流的内核。产生内核会将输出流写入流寄存器文件(SRF)。消费内核会从 SRF 读取输入,而数据无需写入或从内存中读取。通过适当的调度来匹配流的批处理大小与 SRF 的容量,这种组织使得应用程序能够维持非常高的算术强度(算术与内存带宽之比)。

一个设计和构建 Imagine 流处理器的 DARPA 资助项目于 1997 年在 MIT 启动,并于同年晚些时候转移到斯坦福大学。Imagine 是一台用于信号和图像处理工作负载的图形和媒体处理器。它由许多带有本地寄存器文件的并行算术单元、一个中央流寄存器文件和一个内存系统组成。内核从流寄存器文件读取流,通过本地寄存器文件传递中间结果,并将输出流写回流寄存器文件,供下一个内核读取。

Stream-C 编程语言被开发用于编程 Imagine。它扩展了 C 编程语言,增加了描述内核和流的构造。开发了众多的图形、信号处理和图像处理应用程序来调整和评估该架构。它在纹理映射光栅图形上的性能与当时的固定功能 GPU 相当。

在一次 DARPA 主要研究人员会议上,本文作者意识到这项技术可以应用于高性能计算,并构思了 Merrimac 项目。斯坦福 DOE ASCI 中心的计算机科学(CS)部分被重定向以追求这种高性能计算方法。该中心的年度报告提供了流处理发展史的详实记录。

Merrimac 架构被定义为将流处理适配到科学应用。与 Imagine 相比,主要变化是增加了科学计算所需的数据类型(如 FP64),将架构扩展到通过互连网络连接的多个节点以处理大规模问题,并增加了许多弹性特征,以支持在具有合理故障率的情况下进行大规模计算。

Stream-C 编程语言演变成了 Brook。Brook 背后的关键思想是将流编程的想法与更传统的数据并行计算合并。内核函数成为保持高算术强度的关键处理原语。

Brook 被适配以针对 2000 年代初的 GPU。这些 GPU 运行可编程顶点和片段着色器。着色器实现了内核,但指令数量有限且寄存器很少。常见的数据并行编程原语(如 map、reduce/scan、filter、gather 和 scatter)是通过在低级图形着色器之上构建虚拟数据并行计算机来实现的。这种抽象使得大量现有的并行算法可以在 GPU 上运行,并且早期着色器的局限性逐渐被消除。

早期利用内核执行高算术强度计算的一个很好的例子是稠密矩阵-矩阵乘法,它是现代神经网络算法的基础。在执行矩阵-矩阵乘法时,需要读取两个 n×n 矩阵并写入一个 n×n 矩阵。矩阵乘法需要 n³ 次乘加运算。因此,算术强度为 O(n)。这一事实众所周知,并导致了针对带有缓存的 CPU 进行矩阵乘法分块的有效方法。分块在 GPU 上运行时也非常有效。

斯坦福 ASCI 中心的数值科学家将几种科学代码移植到 Brook,以便在 Merrimac 模拟器上运行。这些代码包括计算流体动力学、磁流体动力学和 n 体模拟。n 体模拟是高效 GPU 应用的一个很好的例子。原子对之间的力由天体物理模拟中的引力定律给出,但非结合原子之间的相互作用由 Lennard-Jones 势(甚至更复杂的经验势)近似。这些函数需要许多算术运算。对于这些模拟,相邻原子存储在“邻居列表”中。分子动力学模拟立即成为 GPU 的主要应用。

GPU 和流处理器的一个关键特征是它们具有多种形式的硬件并行性。

每个 GPU 由许多核心组成。每个核心包含一个 SIMD 处理单元(通常为 32 宽)。

此外,每个核心都是多线程的。

回想一下,GPU 是为图形应用程序开发的,其性能取决于将纹理应用于三角形的效率。

纹理映射涉及计算三角形内每个像素片段的纹理坐标,然后使用这些坐标从图像中获取。这些纹理获取具有空间局部性,但时间局部性很小。空间局部性可以通过小型缓存来处理,但由于缺乏一致性,缓存无法处理时间局部性。

高效的纹理映射要求 GPU 隐藏这些纹理获取的延迟。早期 GPU 通过让片段请求纹理、挂起该片段的执行,并立即切换到处理另一个片段来实现这一点。这是多线程的简化版本,这意味着 GPU 需要有许多并行线程同时运行。任务总数是核心数乘以 SIMD 算术单元数(称为 warp)乘以线程数。Blackwell B200 GPU 拥有 384 个流多处理器(SMs)。每个 SM 有 64 个驻留 warp,每个 warp 有 32 个线程。因此,该 GPU 上有 786,432 个任务同时执行。

技术转移

流处理架构和编程系统通过人员流动从斯坦福转移到了英伟达。英伟达的一位架构师 John Nickolls 听说过流处理,并招募了 Bill Dally 在 2003 年为英伟达的 NV50 架构提供咨询。(NV50 于 2006 年作为 G80 发布)。流处理器的许多特性被合并到了该架构中。NV50 的“共享内存”发挥了 Imagine 和 Merrimac 中 SRF 的作用。

Ian Buck(Merrimac 项目的研究生和 Brook 的主要开发人员)于 2004 年加入英伟达。Ian 与 John Nickolls 合作将 Brook 演进为 CUDA。CUDA 合并了 Brook 和 Cg(一种图形着色语言)的最佳特性,并采纳了 Brook 程序员的反馈。关于该技术如何从斯坦福转移到英伟达的故事在一篇演示文稿中进行了描述。Mike Houston(该项目的另一位研究生)加入了 AMD,并直接使用 Brook 作为其 GPU 的编程语言。G80(NV50)和 CUDA 于 2006 年在超级计算大会上发布。

当 CUDA 于 2006 年发布时,很少有人了解并行编程,更不用说 GPU 流编程了。为了克服这一劳动力短缺,Wen-Mei Hwu 和 David Kirk 通过为教授讲授 CUDA 编程课程来推广 GPU 计算。参加这些课程的教师随后教授了成千上万的学生使用 CUDA 进行并行编程。从 Cosmic Cube、J-Machine 和 M-Machine 借来的并行计算技术既被应用于 GPU 内部(以协调多个 SM),也被应用于跨 GPU(构建多节点 GPU 系统以解决大型问题)。

赋能 AI

现代机器学习依赖于三个关键要素——海量数据集、具有许多层和权重的庞大模型,以及优化权重的计算能力。核心算法(深度神经网络、卷积网络、使用反向传播的训练和随机梯度下降)自 20 世纪 80 年代或更早以来就一直存在。大型标注数据集,例如 PASCAL 和 Imagenet,出现在 21 世纪初。最近的进展,例如将文本嵌入到向量空间中,使得自然语言深度学习成为可能。Transformers(“注意力就是你所需要的”)用带有历史记录的易于训练的神经网络取代了难以训练的循环神经网络。GPU 计算使得大规模数据集的网络训练在经济上变得可行。一旦展示了这种能力(Alexnet, GPT),AI 的能力就得到了迅速提升。AI 的快速采用为改进 GPU 计算系统提供了更大的动力。

英伟达的机器学习也得益于学术界与产业界的协同效应。2010 年,作者之一(Dally)与吴恩达(Andrew Ng)的一次早餐交谈促成了一个英伟达与斯坦福之间的联合项目,旨在 GPU 上构建深度神经网络。Bryan Catanzaro 领导了该项目的英伟达部分。在此项目中开发的软件成为了 CuDNN,它为英伟达 GPU 上的深度学习提供了一个现成的库——从而推动了深度学习的普及。

结论

GPU 计算背后的技术(已促成了现代机器学习)主要归功于 30 年的政府资助学术研究。

并行计算、并行图形系统和流处理的研究为 GPU 计算奠定了基础。在这些研究项目中培养的许多学生后来进入行业,转移了这些技术并利用其开发了创新产品。

从斯坦福流处理项目到 GPU 计算的转移非常直接,学术上的 Brook 语言演变为 CUDA,流处理器的功能被整合到 G80 GPU 中。

GPU 提供的高效、易于编程且性能极高的计算平台,通过计算着色器促成了当前的机器学习革命——提供了缺失的成分,以补充早已可用但一直缺乏计算能力的算法和数据。

资料链接:https://cacm.acm.org/federal-funding-of-academic-research/the-origins-of-gpu-computing/

关于作者

威廉·J·达利是美国加利福尼亚州圣克拉拉英伟达公司首席科学家兼高级副总裁,同时也是斯坦福大学电气工程与计算机科学的兼职教授。

帕特·汉拉汉是美国加利福尼亚州斯坦福大学电气工程与计算机科学的佳能荣休教授。


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

下半年的每一场消费电子发布会,都是坏消息

2026 年才过去四分之一,消费电子行业就已经被地震给震麻了。

原因也很简单:AI 狂潮导致的涨价——还不是单纯的内存涨价,而是全领域、全行业、全链路的「电子零部件」涨价。

别看你现在 AI 用得欢,今天消耗的每千个词元(token),都是射向半年后你买手机或者电脑时钱包的子弹。

内存显卡涨,CPU 也要涨

对于 PC 玩家来说,「9950X3D」是个相当让人兴奋的名字,它代表了目前市面上最强悍的游戏 CPU。

就在昨夜,AMD 为这个原本就闪着金光的招牌又添了一把柴,为我们带来了最新的 Ryzen 9 9950X3D2 Dual Edition:

▲ 图|AMD

字如其名,R9 9950X3D2 带来了期盼已久的双 3D V-Cache 堆栈技术

在保持原本多核心、多线程、高频率、全解锁的优点的同时,一举将 L2+L3 缓存推高到了 208MB。

从规格上看,R9 9950X3D2 是颗字面意义上的「鸡血版」CPU,在原本的游戏优势场景之外,也为内容创作和软件开发带来了不小的提升。

▲ 图|AMD

问题是,AMD 打算收多少钱?

上一代 R9 9950X3D 的官方价为 699 美元(约合 4830 元人民币),9950X3D2 尽管尚未公布价格,但普遍预估价格会来到 799 美元(约合 5520 元人民币)——

甚至都够买 48GB 的内存条了。

更惨的是,这种价格趋势不只限于新 CPU,现有货架产品也逃不了。

根据《日经亚洲》的报道,英特尔已经在上周通知客户,将会对现有 CPU 产品进行提价,业内指出 AMD 也会跟进提价,整体涨幅在 15% 左右。

▲ 图|Intel

原因自然是老生常谈:英特尔和 AMD 将产能转移到利润更高的企业级服务器 CPU 上,消费级 CPU 只能一边减产一边涨价。

这还只是 x86 领域,在 ARM 架构这边,2026 年中和下半年的状况同样不容乐观。

开年以来 ARM 架构最重要的新闻,莫过于原本只进行设计授权业务的 ARM 公司,也要进军实业、开始自己造 CPU 了。

作为成立 36 年以来最重大的转型,ARM 前两天推出了其首款自研芯片「ARM AGI CPU」。

这是一款专门为 AI 数据中心设计的处理器,核心目标是支持「代理式 AI」(agentic AI)应用:

▲ 图|ARM

根据 ARM 介绍,这款处理器是与 Meta 深度合作设计的,由子公司 Ampere 开发,基于台积电 3nm 工艺制造,计划在今年下半年进入量产阶段。

虽然这个消息对于 ARM 来说很好,对于消费者来说却算不上什么好消息——

此举标志着 ARM 不再仅仅是「设计局」,而是正式下场与英特尔、AMD 甚至它自己的授权客户(英伟达、亚马逊等等)争夺 AI 数据中心硬件市场的蛋糕。

▲ 图|ARM Newsroom

一旦 ARM 尝过癫狂的 2B 业务红利之后,未来是否会将业务重心全部转移到设计服务器 CPU 上、放弃公版消费级产品设计?

这些都是说不好的。

至于 ARM 的最大用户高通,2026 年的日子也不太平稳。

近日,有关高通下一代旗舰 SoC 骁龙 8 Elite Gen 6 的爆料频出,各家信源达成了两个共识:

  • 骁龙 8 Elite Gen 6 预计会分为标准版(SM8950)和 Pro 版(SM8975)
  • 两者均采用台积电 2nm 工艺制造,Pro 版 GPU 稍强,并且涨价幅度更狠

▲ 图|Wccftech

是的,坏消息还没有结束。

业内人士预估:上述骁龙 8 Elite Gen 6 系列两款 SoC 都将迎来一波大涨。

相比 8 Elite Gen 5 的 280 美元(约合 1934 元人民币)采购单价,Gen 6 的采购价预计会上涨 30% – 50% 。

这还只是手机厂商的采购单价,相同的涨幅传递到消费者身上,再叠加一些其他成本,下半年的手机平均涨幅可能会来到 1500 甚至 2000 元左右——

这么比较下来,前两天被大家口诛笔伐的一加 15T 的涨价幅度似乎也没有那么离谱了。

根据最新的研报数据,截至 2026 年第一季度,同规格的内存同比涨价幅度已经来到了约 400% ,16+512GB 存储组合的采购报价接近 200 美元。

在一些非旗舰机型上,稍不留意就会出现「内存比处理器还贵」的情况。

从前量大管饱、薄利多销的模式如今已经彻底走不通了。

同时,类似的全行业价格震荡也传递到了电脑和手机之外的领域——

彭博社日前报道:任天堂已经决定将 2026 年第一季度的 Switch 2 产量从原本规划的 600 万台下调至 400 万台,且减产可能会延续到第二季度。

▲ 图|彭博社

Switch 2 减产的原因除了 2025 年底购物季的销量表现不达预期之外,生产成本也是原因之一。

就拿最近的《耀西与不可思议图鉴》来说,在任天堂 eShop 购买的价格为 59.99 美元(约合 414 元人民币),但想要实体卡带,则必须再加 10 美元:

▲ 图|IGN

所以别说蚊子腿上不算肉了,在这个慢速 TF 卡都在涨价的时候,Switch 2 的卡带也是要算钱的。

好巧不巧的是,就在本文撰稿期间,索尼也宣布了对 PS5 系列产品的涨价。

这是继去年 8 月 PS5 普涨 50 美元之后的又一次抬价。本轮调整之后,PS5 标准版和数字版涨价 100 美元:

▲ 图|IGN

号称「买 SSD 送主机」的 PS5 Pro 则涨价 150 美元,来到了 900 美元起——

是啊,原因依然是「全球经济形势」。

涨价危机真的有尽头吗

坏消息是,似乎没有。

如果你关注了前几天的股市,就肯定不会错过这么一条消息:

3 月 24 日,谷歌公布了一篇关于全新量化算法 TurboQuant 的技术博客,引得包括闪迪、美光在内的存储股迎来了一波闪跌。

作为一项突破性的「低比特量化」算法方案,TurboQuant 旨在优化解决矢量量化中存在的「内存开销」难题,在不损失精度的前提下减小模型的体积。

用人话说就是:TurboQuant 算法将原本 AI 模型存储信息的「向量」(vectors)从三维坐标表示换成了极坐标表示,让存储上下文的 KV cache 体积急剧缩小,内存占用也大大减少。

▲ 图|Google Research

TurboQuant 之后,算法进步、模型变小、内存降价、生活回归正常……听着多么顺耳。

但现实世界不是这样运行的。

尽管 TurboQuant 的压缩率和精度经过了实验验证,它解决的依然只是推理(Inference)阶段的显存瓶颈,模型训练阶段的显存消耗依然是一座大山。

恰恰是厂商需要天量的内存来训练模型,才导致普通人买不到内存的,TurboQuant 在这一层面上无能为力。

▲ 图|Keymakr

另一方面,即使 TurboQuant 真的把内存价格替家人们打下来了,我们也会面临新一轮的杰文斯悖论(Jevon’s paradox):

内存利用率变高,内存降价,更多人可以买得到内存,大家都开始买内存,导致整体内存需求量不减反增。

最后的最后,TurboQuant 不仅距离正式发布还有一段时间,它本身的热度也来得很突兀——

相关基础论文早在去年 4 月就已经发表,却直到 2026 年才引起波澜。

这就让 TurboQuant 导致的股市波动更像是带着「天下苦内存厂商久矣」的市场情绪爆发,而不是真的技术投产,很难真的让存储价格下降。

▲ 图|ComputerBase

换言之:这不是结束,甚至不是结束的开始(Now this is not the end, it is not even the beginning of the end)。

对于最普遍的广大消费者们,无论是 ARM 造 CPU,还是谷歌发布新算法,都很难和我们直接产生关联。

▲ 图|澎湃新闻

反之,爱范儿从某头部国产手机厂商负责人处获悉:

行业期待 Q3 内存价格会回归理性,但如今存储采购周期急剧缩短,价格一天一变,最新的情况是,上涨将会持续到 Q3 Q4 甚至明年,不用担心会降下来

我们在 2026 年需要做的,就是明确自己的需求,该等就等、该买就买,千万不能过度纠结。

「等等党」们距离真正重见天日的距离,或许比我们想象的都要远。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

🔲 ☆

AGENTS.md

<p>在企业级私有代码库中,AI Agent(如 Claude Code, Codex, Windsurf 等)正逐渐从简单的辅助工具演变为具备自主执行能力的协同者。然而,由于私有库缺乏开源社区的公开语料支持,Agent 极易陷入“随机鹦鹉”(Stochastic Parrots)的困境——即通过概率模拟生成代码,却完全不理解内部复杂的架构逻辑。本文旨在通过标准化 AGENTS.md,为 Agent 提供精准的非显性约束,从而跨越 AI 开发的效能鸿沟。</p>
🔲 ☆

被嘲笑比 Python 还慢?扒开 Go 正则表达式的底层,看看它为了防范“系统猝死”付出了什么

本文永久链接 – https://tonybai.com/2026/03/17/why-is-go-regex-so-slow

大家好,我是Tony Bai。

如果有人问你:在处理纯 CPU 密集型的文本匹配时,Go 和 Python 哪个快?

相信 99% 的 Go 开发者会毫不犹豫地把票投给 Go。毕竟,一门编译型的静态语言,怎么可能输给拖着 GIL 锁的解释型脚本语言?

但现实往往比小说更魔幻。

最近,在 Reddit 的 r/golang 论坛上,一张残酷的 Benchmark 跑分图引发了整个 Go 社区的剧烈震荡。一位开发者,使用极其常见的日志解析正则表达式(提取 IP、时间、URI 等),对各大语言进行了一次横评。

结果令人大跌眼镜:同样的数据集,Rust 跑了 3.9 秒,Zig 跑了 1.3 秒,而 Go 居然跑了整整 38.1 秒!整整比第一名 Zig 慢了接近 30 倍!

如果你再去翻看 Go 官方的 Issue #26623,会看到更绝望的数据:早在2018年的一次正则基准测试中,Go 不仅被 C++ 和 Rust 碾压,甚至连 Python 3、PHP 和 Javascript 都能在正则上把 Go 按在地上摩擦。

一时间,无数 Gopher 信仰崩塌:“为什么 Go 的标准库 regexp 这么慢?”、“连简单的正则都做不好,Go 凭什么做云原生霸主?”

今天,我们就来硬核扒开 Go 语言 regexp 包的底层设计和实现。你会发现,这不是 Go 团队的技术拉跨,而是一场关于“性能、安全与工程哲学”的博弈。

原罪:你以为的慢,其实是替 CGO 负重前行

面对“为什么 Go 的正则比 Python 还慢”的灵魂拷问,Go 核心团队成员 Ian Lance Taylor 给出了第一层解释。

在 Python、PHP 甚至 Node.js 中,你以为你是在运行脚本,其实它们底层都在悄悄“作弊”。这些语言的正则表达式引擎,几乎全部是用高度优化的 C 语言库(主要是 PCRE,Perl Compatible Regular Expressions)编写的。

当你在 Python 里调用 re.match() 时,它瞬间就穿透到了 C 语言的底层,享受着现代 CPU 指令集的极致加速。

那 Go 为什么不用 C?因为 Go 是一门有着“极度洁癖”的语言。

如果 Go 的标准库引入了 C 语言的 PCRE,就必须通过 CGO 来调用。而 CGO 的上下文切换成本极高,更致命的是,它会彻底破坏 Go 引以为傲的“跨平台交叉编译”能力。你再也不能在一个简单的 go build 后,把二进制文件无痛丢到任何 Alpine 容器里了。

因此,Go 团队做出了第一个艰难的决定:完全使用纯 Go 语言,从零手写一个正则表达式引擎。

脱离了 C 语言几十年的底层优化积累,用原生代码去硬刚别人的 C 引擎,这是 Go 看起来“慢”的表层原因。

但这,仅仅是冰山一角。

路线之争:为了防止系统“猝死”,Go 抛弃了速度

真正让 Go 正则变得“慢”的,是算法架构上的降维选择。这牵扯到 Go 语言的缔造者之一、大神 Russ Cox (rsc) 的一段往事。

在正则表达式的底层世界里,存在着两大流派:

  1. 基于回溯(Backtracking)的 NFA 引擎:代表人物是 PCRE(被 Python、Java、PHP 广泛使用)。
  2. 基于 Thompson NFA / DFA 的引擎:代表人物是 RE2(被 Go、Rust 采用)。

PCRE 引擎极快,它支持各种花里胡哨的语法(如前瞻断言 Lookaround、反向引用 Backreferences)。它的算法逻辑是“不撞南墙不回头”的深度优先搜索(DFS)。在匹配正常字符串时,它快如闪电。

但它有一个极其致命的死穴:ReDoS(正则表达式拒绝服务攻击)。

想象一下你写了一个看似无害的正则:

^([a-zA-Z0-9]+\s?)+$

如果黑客故意传入一个极其恶意的字符串:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!(注意最后的感叹号)。

PCRE 引擎会陷入可怕的“灾难性回溯”。它会尝试所有可能的组合,时间复杂度瞬间飙升到 O(2^n) 级。短短几十个字符的输入,能让单核 CPU 满载运行几年都算不出结果!

2019 年,互联网巨头 Cloudflare 就因为在 WAF 防火墙中写错了一个极其简单的正则表达式,CPU资源瞬间耗尽,导致全球80% 的通过 Cloudflare 代理的网站受到影响,陷入瘫痪长达 27 分钟。这就是 PCRE 回溯引擎的恐怖破坏力。

Russ Cox 在设计 Go 的 regexp 包时,定下了一条铁律:系统安全与可预测性,绝对高于单次请求的极限性能。

因此,Go 彻底抛弃了危险的回溯引擎,选择了基于 Thompson NFA 的算法(源自他之前在Google主导设计的 C++ RE2 引擎)。这种算法保证了匹配时间永远是线性复杂度 O(n)

无论黑客传入多么恶意的字符串,Go 的正则引擎绝对不会发生灾难性回溯。它牺牲了在美好情况下的极致快感,换取了在极端恶劣环境下的金身不坏。

这算是 Go 团队最顶级的“克制”吧。

硬核剖析:Go 的正则,时间到底去哪了?

既然算法是 O(n) 的,为什么 Go 依然比同样采用 RE2/DFA 思想的 Rust 慢那么多呢?

如果你去追踪 Go 官方的 Issue #19629Issue #11646,通过 pprof 分析 Go 正则匹配的 CPU 耗时,你会看到几个令人头疼的瓶颈:

1. 沉重的 UTF-8 解析税

Rust 和 C 的很多正则引擎,底层是直接在“字节(Byte)”级别游走的。而 Go 为了贯彻它对 Unicode 的原生支持,regexp 包在内部极其频繁地将输入流解码为 Rune(Go 的 Unicode 字符单位)。这种逐个解析 Rune 的操作,带来了巨大的计算开销。

2. NFA 虚拟线程的内存震荡

在 Go 的底层源码中,你可以看到耗时最高的两个函数是 (machine).add 和 (machine).step。

Go 是通过维护两个“状态队列(稀疏集)”来模拟 NFA 的并行推进的。每读取一个字符,引擎就要把所有可能的状态添加到下一个队列中。这导致了海量的内存重分配(Allocation)和切片拷贝。哪怕是匹配一个简单的长字符串,底层都在疯狂地挪动内存。

既然这么慢,为什么不把 C++ RE2 里那个极速的 DFA(确定性有限状态自动机)移植到 Go 里呢?

Issue #11646 记录了这次尝试。开发者 Michael Matloob 曾经试图将 RE2 的 DFA 移植过来,但被 Russ Cox 拦下了。原因很直接:DFA 虽然快,但它在运行时会动态生成大量的状态,如果不加以严格限制,极易引发内存耗尽(OOM)。在 Go 带有 GC 的内存模型下,频繁创建和销毁庞大的 DFA 状态缓存,会让垃圾回收器不堪重负。

于是,Go 的标准库在“安全、内存、性能”的三角博弈中,选择了妥协于现状。

社区的探索:SIMD 降维打击与 100倍加速的 coregex

官方的克制固然令人敬佩,但对于身处一线的业务开发者来说,由于正则太慢导致的 CPU 告警,是实实在在的痛点。

“既然官方不愿意改,那我们就自己造轮子!”

在近期的 Issue #26623 中,一位名为 kolkov 的开发者带着他的开源库 coregex 杀入了战场,向 Go 标准库发起了直接的挑战。

coregex 是一个完全用纯 Go 编写的正则库,它的出现直接将 Go 的正则性能拉到了与 Rust 并驾齐驱,甚至在某些场景下超越 Rust 的境地。

它是怎么做到的?它在底层祭出了几个大杀器:

  1. SIMD 预过滤(Prefilters):它使用了手写的汇编代码(AVX2/SSSE3 指令集),将正则中的静态字符串提取出来,利用 CPU 的向量化指令,一次性对比 32 个字节。像匹配 .*.txt 这种正则,速度直接飙升了 1500倍
  2. 带缓存的 Lazy DFA:它绕过了标准库每次都重算 NFA 的毛病,在运行时动态构建 DFA 缓存,大幅消除了内存分配。
  3. 写时复制(COW)的捕获组:标准库在处理提取子串时会疯狂分配切片。coregex 通过切片状态共享,让内存分配直接减少了 50%。

在 kolkov 提供的 CI 跑分中,在 6MB 的输入下,coregex 处理邮箱、URI 的耗时仅为 1.5 毫秒,而标准库耗时高达 260 毫秒。足足快了 170 倍!

然而,这段极其硬核的改进,依然很难入Go团队法眼,更不用谈在短期内被合并进 Go 的标准库。

一方面,Go 官方目前正在推进自己的内建 SIMD 方案(Issue #73787),不想接入手写的汇编代码;另一方面,社区大牛 Ben Hoyt 在使用 coregex 时发现,如果开启 Longest() 模式(最长匹配模式),这个库的性能会发生严重退化。

这再次印证了标准库开发的残酷:在某几个特定场景下跑到全宇宙第一很容易,但要在一套 API 里无死角地兜底全世界所有的奇葩正则输入,难如登天。

在 Go 中写正则的正确姿势

大致了解了底层原理,回到日常开发中,我们该如何应对 Go 正则的性能瓶颈?作为高级 Go 开发者,请务必将以下三条军规刻在脑子里:

第一条:能不用正则,就坚决不用

如果你只是想检查字符串是否包含子串,或者进行简单的前后缀匹配,永远优先使用 strings.Contains()、strings.HasPrefix() 等内置函数。 它们底层有优化的实现,在这样简单场景下,速度是 regexp 包不可比拟的。

第二条:将编译前置,远离循环

如果你翻看新手代码,最常见的低级错误就是在 for 循环或者每次 HTTP 请求里调用 regexp.Compile()。

正则的编译过程(生成 NFA 字节码)极其消耗 CPU。请永远在全局变量或 init() 函数中使用 regexp.MustCompile(),将其编译好并复用。Go 的 Regexp 对象是并发安全的,随便多 Goroutine 调用。

第三条:在极端性能要求下,打破“洁癖”

如果你的核心业务(比如高频日志清洗、海量数据 ETL)确实被 regexp 卡住了脖子,不要硬抗。

你可以选择引入通过 CGO 调用 PCRE的Go binding库(比如https://github.com/GRbit/go-pcre),但要注意防范 ReDoS 攻击,或google/re2的Go binding(比如https://github.com/wasilibs/go-re2),又或是在业务侧尝试社区的野路子 coregex。在生存面前,架构的“洁癖”是可以适当妥协的。

小结

“为什么 Go 的正则这么慢?”

这并非一个简单的工程失误。它是一道分水岭,隔开了“追求跑分好看的玩具代码”与“守护千万级并发集群的生产级设计”。

Russ Cox 宁愿忍受整个开源界的群嘲,也没有为了刷榜而去引入危险的回溯引擎。这或许就是 Go 语言能够成为云原生时代头部语言的原因:不盲目追求上限的巅峰,而是死死守住安全下限。

参考资料

  • https://www.reddit.com/r/golang/comments/1rr2evh/why_is_gos_regex_so_slow/
  • https://github.com/golang/go/issues/26623
  • https://github.com/golang/go/issues/19629
  • https://github.com/golang/go/issues/11646
  • https://swtch.com/~rsc/regexp/

今日互动探讨:

在你的日常开发中,有没有被由于“写了糟糕的正则表达式”而导致 CPU 飙升 100% 的惨痛经历?你又是如何排查和优化的?

欢迎在评论区分享你的血泪史


认知跃迁:读懂底层机制,才能看透系统架构的本质

从放弃 CGO 选择纯 Go 实现,到防范 ReDoS 采用 NFA,再到社区为了榨干 CPU 性能而引入 SIMD。Go 语言的每一个看似“不合理”的设计背后,都隐藏着深邃的系统级考量。

然而,令人遗憾的是,很多开发者写了五六年的 Go 代码,遇到性能瓶颈依然只能靠“瞎猜”和“重启”。他们对 Go 的内存逃逸、Goroutine 调度机制以及标准库的底层数据结构一无所知。

如果你渴望突破“熟练调包侠”的瓶颈,想要像 Russ Cox 这样的顶级大厂架构师一样,看透 Go 语言背后的底层逻辑,建立起自己坚不可摧的技术护城河——

我的极客时间专栏 Tony Bai·Go语言进阶课 正是为你量身定制。

在这 30+ 讲极其硬核的内容中,我不仅带你剥开语法糖,深挖 Goroutine 调度、Channel 哲学;更会带你全面吃透 Go 的工程化实践,把底层性能调优背后的逻辑一次性讲透。

目标只有一个:助你完成从“Go 熟练工”到“能做顶级架构决策的 Go 专家”的蜕变!

扫描下方二维码,加入专栏。不要用战术上的勤奋,掩盖战略上的懒惰。让我们一起用架构师的视角,重新认识 Go 语言。


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!

我们致力于打造一个高品质的 Go 语言深度学习AI 应用探索 平台。在这里,你将获得:

  • 体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏,夯实你的 Go 内功。
  • 前沿 Go+AI 实战赋能: 紧跟时代步伐,学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等,掌握 AI 时代新技能。
  • 星主 Tony Bai 亲自答疑: 遇到难题?星主第一时间为你深度解析,扫清学习障碍。
  • 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术,碰撞思想火花。
  • 独家资源与内容首发: 技术文章、课程更新、精选资源,第一时间触达。

衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

img{512x368}


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

Go 1.26 中值得关注的几个变化:从 new(expr) 真香落地、极致性能到智能工具链

本文永久链接 – https://tonybai.com/2026/02/14/some-changes-in-go-1-26

大家好,我是Tony Bai。

北京时间 2026 年 2 月 10 日,Go 团队正式发布了 Go 1.26

时光飞逝,距离我在博客中写下《Go 1.26 新特性前瞻》已经过去了两三个月。在那篇文章中,我们基于Go 1.26开发分支对这一版本进行了初步的探索。如今,随着正式版的落地,那些曾经躺在 proposal 里的构想、存在于草案中的特性,终于尘埃落定,成为了我们手中实实在在的工具。

官方 Go 1.26 Release Notes 中平实的语言背后,隐藏着巨大的工程价值。如果用一个词来形容 Go 1.26,我认为是“精益求精的工程化胜利”

与引入泛型的 Go 1.18 或引入函数迭代器Go 1.23 不同,Go 1.26 并没有带来颠覆性的语言范式改变,但它在编码体验、底层性能以及工具链智能化这三个维度上,都交出了一份令人惊艳的答卷。从千呼万唤始出来的 new(expr) 语法糖,到默认启用的 Green Tea GC,再到重构后的 go fix,每一个改动都切中了工程实践中的痛点。

本文将基于官方发布的 Release Notes,结合我之前的深度分析,为你全景式解析 Go 1.26 中那些最值得关注的变化。

语言变化:不仅是语法糖,更是生产力

new(expr):指针初始化的终极解法

在 Go 语言的日常开发中,我们经常面临一个尴尬的场景:如何获取一个字面量(Literal)或表达式结果的指针?

在 Go 1.26 之前,我们无法直接对字面量取地址(&10 是非法的)。为了初始化一个包含指针字段的结构体(这在 JSON/Protobuf 的可选字段、数据库 ORM 映射中极其常见),我们不得不引入临时变量,或者定义辅助函数:

// Go 1.26 之前:繁琐的临时变量或辅助函数
func IntP(i int) *int { return &i }

timeoutVal := 30
conf := Config{
    Timeout: &timeoutVal,   // 必须先定义变量
    Retries: IntP(3),       // 或者依赖辅助函数
}

这种写法不仅啰嗦,还打断了代码的阅读流。社区为此发明了无数个 ptr 库,甚至很多项目里都有一个 util.go 专门放这些 helper。

Go 1.26 终于原生解决了这个问题。 内置函数 new() 的语法得到了扩展,现在它允许接收一个表达式作为参数,并返回指向该表达式值的指针。

// Go 1.26:优雅的内联初始化
// 完整代码:https://go.dev/play/p/kEYZC3W6-sa
conf := Config{
    Timeout: new(30),          // 直接获取整型字面量的指针
    Role:    new("admin"),     // 直接获取字符串字面量的指针
    Active:  new(true),        // 布尔值也不在话下
    Start:   new(time.Now()),  // 甚至是函数调用的结果
}

这不仅是一个语法糖,它极大地提升了配置对象、API 请求体构建时的代码可读性,消除了大量无意义的中间变量,让代码变成了声明式的“一行流”。

关于这个特性的演变历程以及社区的讨论细节,可以参考我之前的文章《从 Rob Pike 的提案到社区共识:Go 或将通过 new(v) 彻底解决指针初始化难题》。

泛型约束的自我引用

Go 1.26 解除了泛型类型在类型参数列表中引用自身的限制。这意味着我们现在可以定义更加复杂的递归数据结构或接口约束。

// 以前这是非法的,现在合法了
type Adder[A Adder[A]] interface {
    Add(A) A
}

func algo[A Adder[A]](x, y A) A {
    return x.Add(y)
}

这一改变虽然对日常业务代码影响较小,但对于编写通用库、ORM 框架或复杂算法库的开发者来说,它消除了一个长期存在的类型系统痛点,让泛型的表达能力更上一层楼,简化了复杂数据结构的实现。

关于这个特性的演变历程以及社区的讨论细节,可以参考我之前的文章《Go 泛型再进化:移除类型参数的循环引用限制》。

运行时与编译器:看不见的性能飞跃

Go 1.26 在“看不见的地方”下了苦功,不仅让 GC 焕然一新,还解决了 Cgo 和切片分配的性能瓶颈。

“Green Tea” GC:默认启用的性能引擎

Go 1.25 作为实验特性登场后,代号为 “Green Tea” 的新一代垃圾回收器在 Go 1.26 正式转正,成为默认 GC。

Green Tea GC 是 Go 运行时团队针对现代硬件特性和分配模式进行的一次深度重构。它主要优化了小对象的标记和扫描过程,通过更好的内存局部性(Locality)和 CPU 扩展性,显著提升了 GC 效率。

  • 开销降低:根据官方发布说明,在重度依赖 GC 的真实应用中,GC CPU 开销降低了 10% – 40%。这意味着你的微服务可能在不增加硬件资源的情况下,吞吐量获得直接提升。
  • 向量化加速:在支持 AVX 等向量指令集的现代 CPU(如 Intel Ice Lake 或 AMD Zen 4 及更新架构)上,Green Tea GC 会利用 SIMD 指令加速扫描,带来额外的性能提升。

这对于微服务、高并发 Web 应用等存在大量临时小对象分配的场景来说,是一次免费的性能升级。你无需修改一行代码,只需升级 Go 版本。

关于 Green Tea GC 的深层原理和架构演进,我在《Go 官方详解“Green Tea”垃圾回收器:从对象到页,一场应对现代硬件挑战的架构演进》一文中有详细解读。

Cgo 调用提速 30%

对于依赖 SQLite、图形库、系统底层 API 或其他 C 库的 Go 应用,这是一个巨大的利好。Go 1.26 将 Cgo 调用的基准运行时开销(Baseline Runtime Overhead)降低了约 30%。这意味着跨语言调用的“税”被进一步降低,Go 在系统编程和嵌入式领域的竞争力再次提升。

编译器进化:栈上分配切片底层数组

对于 Go 开发者而言,“栈分配(Stack Allocation)”由于无需 GC 介入,其效率远高于堆分配。

Go 1.26 的编译器进一步增强了逃逸分析能力。编译器现在能够在更多场景下,将切片的底层数组(Backing Store)直接分配在栈上。这主要针对那些使用 make 创建但大小非固定(但在一定范围内)的切片场景。

这一改进直接减少了堆内存的分配次数,进而降低了 GC 扫描的压力。如果你对这一编译器优化技术感兴趣,或者想了解如何利用 PGO 驱动逃逸分析,推荐阅读《PGO 驱动的“动态逃逸分析”:w.Write(b) 中的切片逃逸终于有救了?》。

实验性特性:Goroutine 泄露分析

Goroutine 泄露一直是 Go 并发编程中隐蔽且棘手的难题。Go 1.26 引入了一个名为 goroutineleak 的实验性 Profile(需通过 GOEXPERIMENT=goroutineleakprofile 开启)。

与传统的泄露检测工具不同,该功能基于 GC 的可达性分析。它能检查那些处于阻塞状态的 Goroutine,看它们等待的并发原语(如 Channel、Mutex)是否已经“不可达”。如果一个 Goroutine 等待的 Channel 没有任何活跃的 Goroutine 能够引用到,那么这个 Goroutine 就被判定为“永久泄露”。

这种检测机制在理论上保证了极低的误报率。这源自 Uber 的内部实践,我在《Goroutine泄漏防不胜防?Go GC或将可以检测“部分死锁”,已在Uber生产环境验证》一文中对此进行了详细介绍。

工具链:更智能、更规范

go fix 的重生:Modernizers 与内联

Go 1.26 对 go fix 命令进行了彻底重写。它不再是一个简单的语法修补工具,而是基于 Go Analysis Framework 构建的强大现代化引擎。

新版 go fix 引入了 “Modernizers” 的概念。它包含了几十个分析器,不仅能修复错误,还能主动建议并将你的代码升级为使用最新的语言特性或标准库 API。

除了 “Modernizers”,新版 go fix 另一个重磅功能是基于 //go:fix inline 指令的自动内联与迁移机制。

  • 函数内联:如果一个函数被标记了 //go:fix inline,go fix 分析器会建议(并自动执行)将所有对该函数的调用替换为函数体的内容。这对于废弃旧 API 极为有用。例如:

    // Deprecated: prefer Pow(x, 2).
    //go:fix inline
    func Square(x int) int { return Pow(x, 2) }
    

    当用户调用 Square(10) 时,go fix 会将其自动重写为 Pow(10, 2),从而实现平滑迁移。

  • 常量内联:同样的机制也适用于常量。如果一个常量定义引用了另一个常量并标记了 //go:fix inline,所有对旧常量的引用都会被自动替换为新常量。

    //go:fix inline
    const Ptr = Pointer // Ptr 的使用者会被自动迁移到 Pointer
    
  • 跨包/跨版本迁移:这一机制甚至支持跨包迁移。例如,当库升级到 v2 版本时,可以在 v1 包中定义一个内联函数,将调用转发给 v2 包。go fix 会自动将用户代码中的 v1 调用替换为 v2 调用,从而实现低风险的大规模自动化重构。

这种基于源码注释的指令机制,为库作者提供了一种标准化的手段来引导用户升级,彻底改变了过去手动修改或编写复杂迁移脚本的痛苦历史。

go mod init 的版本策略变更:兼容为先

这是一个容易被忽视但影响深远的改动。

在以前,当你用 Go 1.25 工具链运行 go mod init mymod 时,生成的 go.mod 会默认写入 go 1.25。这意味着你的模块无法被 Go 1.24 的用户引用。

从 Go 1.26 开始,go mod init 变得更加“克制”:

  • 稳定版工具链:默认生成 1.(N-1).0 版本。例如,使用 Go 1.26 初始化,go.mod 将写入 go 1.25.0
  • 预览版工具链:默认生成 1.(N-2).0 版本。

这一策略鼓励开发者创建兼容性更好的模块,避免无意中切断了对次新版 Go 用户的支持。这是一个对生态系统非常友好的改动。在后续的文章中,我们会专题对此特性进行说明。

Pprof 默认火焰图

go tool pprof -http 现在默认展示火焰图(Flame Graph)视图,而不是原来的有向图。这顺应了性能分析领域的趋势,火焰图在展示调用栈耗时占比时更为直观,利于快速定位热点。

标准库:补齐短板,拥抱未来

testing 包:测试产物归档 ArtifactDir

在 CI/CD 环境中,集成测试失败时,我们往往希望能看到当时的日志文件、截图或数据库 Dump。过去,我们需要自己拼接临时目录路径,并祈祷它没有被清理。

Go 1.26 为 testing.T 和 B 新增了 ArtifactDir() 方法:

  • 该方法返回一个专门用于存放测试产物的目录路径。
  • 配合 go test -artifacts=./out 参数,可以自动将这些产物收集到指定位置。

这结束了每个项目自己造轮子管理测试临时文件的混乱局面。关于这一特性的详细讨论,可以参考《Go testing包将迎来新增强:标准化属性与持久化构件API即将落地》。

log/slog:原生多路输出 MultiHandler

自 slog 引入以来,如何将日志同时输出到控制台和文件一直是个高频问题。Go 1.26 新增了 slog.NewMultiHandler,正式在标准库层面支持了日志的“扇出(Fan-out)”。

它会将日志分发给多个 Handler,只要任意一个子 Handler 处于 Enabled 状态,日志就会被处理。这意味着我们不再需要引入第三方库来实现这一基础功能。更多背景参考《slog 如何同时输出到控制台和文件?MultiHandler 提案或将终结重复造轮子》。

errors:泛型版 AsType

errors.As 一直是 Go 错误处理中容易“踩坑”的 API(需要传递指针的指针,否则会 Panic)。Go 1.26 引入了泛型版本的 errors.AsType

// Old: 容易写错,运行时反射
var pathErr *fs.PathError
if errors.As(err, &pathErr) { ... }

// New (Go 1.26): 类型安全,编译期检查
if pathErr, ok := errors.AsType[*fs.PathError](err); ok { ... }

这不仅更安全,而且由于省去了复杂的运行时反射开销,性能也更好。详见《泛型重塑Go错误检查:errors.As的下一站AsA?》。

拥抱迭代器与零拷贝

  • reflect 包迭代器:新增 Type.Fields(), Type.Methods() 等方法,返回迭代器序列,允许使用 for range 循环遍历结构体字段,替代了笨拙的索引遍历。
  • bytes.Buffer.Peek:新增 Peek 方法,允许在不推进读取位置的情况下查看缓冲区数据,为高性能解析场景提供了便利。详见《Go 零拷贝“最后一公里”:Peek API背后的设计哲学与权衡》。

安全增强

小结

Go 1.26 是一个务实、丰满且充满诚意的版本。

它没有追求华而不实的新奇法,而是通过 new(expr) 和 go fix 提升开发者的幸福感;通过 Green Tea GC 和编译器优化提升运行时的性能;通过 go mod init 的策略调整和标准库的补全,提升生态系统的健壮性。

建议大家在详细阅读官方 Release Notes 后,尽快制定升级计划,享受 Go 1.26 带来的红利。


你的升级计划是?

Go 1.26 带来了诸多实惠的工程优化。在你看完这些变化后,最想立刻在项目里用起来的特性是哪个?你所在的团队是否已经开始规划升级到这个版本了?

欢迎在评论区聊聊你的看法!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ⭐

你的 CLAUDE.md 写错了:为什么指令越多,AI 越笨?

本文永久链接 – https://tonybai.com/2026/01/29/write-a-good-claude-md

大家好,我是Tony Bai。

在使用 Claude Code、Cursor 或 Gemini Cli 等 AI 编程工具时,你是否遇到过这样的情况:

明明在项目根目录写了 CLAUDE.md(或 AGENTS.md),洋洋洒洒列了几十条项目规范:“使用 TypeScript”、“不要用 any”、“单元测试覆盖率要达标”……

但 AI 就像个叛逆的实习生,经常对这些指令视而不见,反复犯同样的低级错误。

是你写的 Prompt 不够严厉吗?还是模型不够聪明?

最近,HumanLayer 团队发布的一篇深度分析文章揭示了真相:

问题恰恰在于你写得太多了。

Claude Code 的内部机制会给模型注入一条系统级提醒:

“IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant.”
(重要:此上下文可能与您的任务无关。除非高度相关,否则请忽略。)

这意味着,你的 CLAUDE.md 越臃肿,包含的无关信息越多,它被系统判定为“噪音”并整体忽略的概率就越大。

今天,我们来聊聊如何用工程化的思维,重构你的 AI 上下文管理。

第一性原理:AI 是无状态的“新员工”

要写好 CLAUDE.md,首先要理解 LLM 的本质:它是无状态的(Stateless)

每次你开启一个新的 Session,对于 AI 来说,都是入职第一天。它对你的代码库一无所知。CLAUDE.md 是它唯一的“入职手册”和“长期记忆”。

但这并不意味着你要把公司历史全塞给它。一个优秀的入职手册应该包含什么?

HumanLayer 的文章中 总结了 “The Trinity”(三要素)

  1. WHAT(地图): 技术栈是什么?项目结构是怎样的?(特别是 Monorepo,告诉它哪里是 App,哪里是 Lib)。
  2. WHY(目标): 这个项目的核心价值是什么?核心模块的职责边界在哪里?
  3. HOW(工具): 怎么构建?怎么跑测试?用 npm 还是 bun?

除此之外的任何废话,都是在消耗 AI 宝贵的注意力。

最佳实践一:少即是多 (Less is More)

研究表明,即便是最前沿的推理模型(Thinking Models),能稳定遵循的指令上限也就在 150-200 条左右。而小模型(如 Haiku 或 GPT-4o-mini)的遵循能力随着指令数量增加呈指数级下降

更糟糕的是 “位置偏见(Position Bias)”。LLM 高度关注开头(System Prompt)和结尾(最新对话),夹在中间的 CLAUDE.md 如果过长,极易沦为“被遗忘的中间层”。

因此,请遵循以下黄金法则:

  • 不要试图涵盖所有边缘情况。
  • 将根目录的 CLAUDE.md 控制在 300 行以内,HumanLayer 的生产环境甚至控制在 60 行以内
  • 每一行都要问自己:“如果没有这句话,AI 真的干不了活吗?”

最佳实践二:渐进式披露 (Progressive Disclosure)

如果你确实有很多规范要交代,怎么办?

答案是:不要把所有鸡蛋放在一个篮子里。

想象一下,HR 会在入职第一天把 500 页的《数据库运维手册》扔给一个前端开发吗?不会。

你需要构建一套“渐进式”的文档体系:

1. 建立文档库:

在项目中创建一个 .ai/docs/ 目录,存放特定领域的指南:

  • testing.md:详细的测试编写与运行指南。
  • database.md:Schema 定义与迁移规范。
  • architecture.md:核心架构图与数据流。

2. 建立索引:

在主 CLAUDE.md 中,只保留“触发器”:

  • “编写或运行测试前,请务必阅读 .ai/docs/testing.md。”
  • “涉及数据库变更时,请参考 .ai/docs/database.md。”

这样,当 AI 只是在写一个前端组件时,它的上下文窗口就不会被无关的后端 Schema 污染。保持注意力的纯净,是提升 AI 智商的关键。

最佳实践三:别把 AI 当 Linter 用

这是最常见的资源浪费:在 CLAUDE.md 里写了 50 行关于代码风格的规定——“缩进用 2 个空格”、“花括号不换行”、“变量名用驼峰”……

请记住:LLM 是昂贵的推理引擎,不是廉价的格式化工具。

让 AI 去关注缩进,就像让法拉利去送外卖,既贵又慢。而且,AI 即使知道规则,也经常因为概率性生成而“手滑”。

正确的解法是:Tooling > Prompting。

  1. 配置工具: 使用 Prettier, Biome, ESLint, govet 等确定性工具来处理格式。
  2. 设置 Hook: 配置 Claude Code 的 Stop Hook。如果 AI 生成的代码格式不对,让 Linter 报错,把错误信息喂回给 AI。
    • AI: (生成代码)
    • System: “Lint Error: Missing semicolon.”
    • AI: “Ah, fixing it.”

AI 极其擅长修复报错,但并不擅长凭空遵守“隐形的规则”

小结:杠杆的层级

在 AI 原生开发中,CLAUDE.md 处于“杠杆层级”的顶端。

  • 写错一行代码 = 1 个 Bug。
  • 写错一行 CLAUDE.md = 成百上千次错误的规划、错误的检索、错误的代码。

不要盲目依赖 /init 自动生成的文件,那只是个起点。你需要像维护核心代码一样,精心雕琢你的 CLAUDE.md。

现在,打开你的项目,检查一下那个文件。

删掉那些废话,把长文档拆分,把格式化交给工具。

给你的数字员工减负,它才能跑得更快。

资料链接:https://www.humanlayer.dev/blog/writing-a-good-claude-md


你的 CLAUDE.md 有几行?

读完这篇文章,不妨现在就去检查一下你项目里的 CLAUDE.md。它是清爽的“入职手册”,还是臃肿的“裹脚布”?你目前的行数是多少?

欢迎在评论区晒出你的“瘦身”成果!让我们一起给 AI 减负。

如果这篇文章帮你解决了 AI “不听话”的难题,别忘了点个【赞】和【在看】,并转发给你的团队,规范大家的 Prompt 工程!


构建工程化的 AI 工作流

CLAUDE.md 只是构建 AI 原生工作流的起点。

  • 如何配置 Hooks 来实现自动化代码审查?
  • 如何编写 Sub-Agents 来处理隔离的脏活累活?
  • 如何设计 Slash Commands 来固化团队的 SOP?

如果你想深入掌握 Claude Code 的高阶玩法,不仅仅是写 Prompt,而是构建一套“自动纠错、按需加载”的工程化体系。

欢迎关注我的极客时间专栏AI 原生开发工作流实战

我们不聊虚的,只讲能在生产环境中落地的 AI 工程实践。

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

别读代码了,看着它流过就行:ClawdBot 作者的 AI 开发工作流

本文永久链接 – https://tonybai.com/2026/01/28/clawdbot-author-ai-development-workflow

大家好,我是Tony Bai。

在过去的一年里,我们见证了 AI 编程工具的井喷。从 Copilot 到 Cursor,从 Windsurf 到 Claude CodeGemini CLI和Codex,每个人都在寻找那个“完美的开发助手”。

最近,爆火的个人AI助理开源项目 ClawdBotPSPDFKit 的创始人 Peter Steinberger 发布了一系列关于他AI 开发工作流的深度博文。他以一种近乎“未来主义”的视角,描述了一个令传统程序员既兴奋又恐惧的景象:

“I stopped reading code and started watching it stream by.”
(我不再读代码了,我只是看着它流过。)

这可真不是一句狂言,而是一种全新的且现实可行的工程范式

当 AI 的可靠性达到临界点,软件交付的速度不再受限于人类的打字速度,而是受限于模型的推理速度(Inference Speed)

今天,我们结合 Peter 的最新实践,为你拆解这套“以人为核心、AI 为手脚”的顶级开发工作流。

质变时刻:学会了“深思熟虑”的模型+工具链的“极简回归”

根据 Peter 的观察,真正的质变发生在 GPT-5.2 这一代模型发布之后。

早期的模型(如 Claude 3.5 Sonnet),虽然聪明但急躁,往往“顾头不顾腚”。而新一代的 Codex 模型学会了“沉默”

在面对一个复杂的重构任务时,模型可能会静默阅读代码长达 10 到 15 分钟,一言不发。这种“Think before Act”的特性,让它能够构建出极其完整的上下文图谱。结果就是:它能一次性(One-shot)搞定跨越数十个文件的大型重构,且几乎零 Bug。

这也宣告了 Plan Mode(规划模式)的消亡。以前我们需要强制 AI 先写计划再写代码,那是为了给旧模型的智商打补丁。现在,开发者可以直接与 AI 对话,像与一位资深架构师协作一样流畅。

此外,在尝试了市面上几乎所有工具(VS Code, Zed, Cursor, Gemini)之后,Peter 最终回归了一套极简的组合:
Ghostty + Claude Code + Minimal Tooling

为什么?因为 “Less is More”

  • 终端的复兴: 他抛弃了不稳定的 VS Code 终端,全面回归 Ghostty。因为在 AI 时代,终端才是最纯粹的交互界面。
  • 屏幕即战场: 他使用 Dell 40寸带鱼屏(3840×1620),同时平铺 4 个 Claude 实例 + Chrome。他不需要切换窗口,他像监控仪表盘一样监控着 4 个并发任务的进展。
  • 摒弃复杂 MCP: 他甚至反主流地删除了大部分 MCP(Model Context Protocol)。因为 AI 有时候会自作聪明地启动 Playwright 去抓取网页,而直接读取代码反而更快、更准、更省 Token。

Peter的这些实践告诉我们:不要被花哨的工具迷了眼。一个稳定、高性能的终端,加上一个聪明的 CLI Agent,就是最强大的武器。

像工厂一样生产:并行工程学

当“写代码”不再占用人类的脑力带宽时,Peter 的工作方式从“工匠”变成了“工厂厂长”

并行处理 (Parallel Processing)

他通常同时推进 3 到 8 个项目

  • 窗口 1:重构后端架构;
  • 窗口 2:优化前端交互;
  • 窗口 3:跑全链路测试。

开发者只需要在这些 Session 之间切换,确认结果,给出下一个指令。

线性推进,绝不回滚 (Never Revert)

“软件开发就像登山,走错路了就绕回来,而不是读档重来。”

他几乎不再使用 git reset。如果 AI 写歪了,直接告诉它“换个思路”,它会在现有基础上自我修正。甚至连 Plan Mode(规划模式) 都变得不再必要,就像前面提到的,新一代模型(GPT-5.2等)学会了“深思熟虑”,能一次性搞定复杂重构。

跨项目“抄作业” (Cross-Referencing)

代码复用从未如此简单。他不再写 Prompt 描述需求,而是直接说:

“Look at ../vibetunnel project, and implement the same logging system here.”

AI 会自动跨目录读取代码,提炼模式,并完美适配到当前项目。

基础设施的重构:CLI First

为了配合这种极速开发,Peter 彻底重构了他的技术栈选择逻辑。

拥抱 CLI (Command Line Interface)

“Whatever you build, start with a CLI.”

无论做什么 App,先做 CLI 版本。因为 Agent 调用 CLI 最方便,测试 CLI 最容易。GUI 只是 CLI 的一层皮。只要内核跑通了,让 AI 套个 React 壳只是分分钟的事。

Oracle(预言机)

当 Agent 遇到知识盲区(比如最新的 API 变动)时,它会自动调用 Oracle ——一个Peter开源实现的、联网的、专门负责爬取文档并总结答案的“元智能体”。知识获取的闭环,彻底自动化了。

文档驱动 (Docs-Driven)

他不再维护复杂的 Prompt 库,而是维护项目的 docs/ 目录。

想规范 AI 的行为?写一个 docs/architecture.md。

想让 AI 学会用 Vercel?在 CLAUDE.md 里加一行:logs: axiom or vercel cli。

文档,就是 AI 的“长期记忆”和“员工手册”。

给开发者的启示:核心竞争力迁移

在 Peter 的工作流中,我们看到了程序员核心竞争力的转移:

  1. 系统设计 (System Design) 是王道:
    当前的 AI 搞不定分布式系统设计,搞不定数据库 Schema 的前瞻性规划。这些“硬骨头”,才是人类的领地。
  2. 选择 AI 友好的生态:
    TypeScript (Web), Go (CLI), Swift (App),这三者是 AI 掌握得最好的。Peter 特别提到了 Go——以前他并不感冒,但后来发现 AI 写 Go 写得极好。为什么?因为 Go 简单的类型系统让 Lint 检查极快,AI 能迅速修正错误。相比之下,那些类型系统过于复杂或编译检查极其严格的语言,可能会增加 AI“一次做对”的难度,拖慢你的推理速度。

  3. 自动化一切 (Automate Everything):
    不要手动注册域名,写个 Skill 让 AI 去做。不要手动发推特,写个 CLI 让 AI 去发。为你自己,也为你的 AI 员工,构建大量的自定义基建

小结:享受创造

有人担心 AI 会让程序员失业,但 Peter 的实录告诉我们:这可能是程序员最好的时代。

在这个时代,限制你产出的不再是你的手速,也不再是你对某个库的熟悉程度,而仅仅是你的想象力

当你可以以推理速度交付软件,当你看着代码像瀑布一样流过屏幕时,编程就不再是枯燥的搬砖,而是一场纯粹的、创造性的游戏

资料链接:

  • https://steipete.me/posts/2025/optimal-ai-development-workflow
  • https://steipete.me/posts/2025/shipping-at-inference-speed

你的“未来工作流”

Peter 的工作流让我们看到了未来的一角。你敢想象自己“不再读代码”的那一天吗?在你的理想中,AI 应该帮你接管哪些“脏活累活”,让你能专注于更高维度的创造?

欢迎在评论区分享你的脑洞或对未来的担忧!让我们一起定义属于自己的 AI 工作流。

如果这篇文章点燃了你对 AI 编程的全新想象,别忘了点个【赞】和【在看】,并转发给你的极客朋友,邀请他们一起见证未来!


还在为“复制粘贴喂AI”而烦恼?

Peter 描述的“看着代码流过”的未来并非遥不可及。我的新专栏 AI原生开发工作流实战 将带你提前掌握这套高效范式:

  • 告别低效: 摒弃“聊天式编程”,重塑以文档和 CLI 为核心的开发范式。
  • 驾驭 Agent: 深入实战 Claude Code,像 Peter 一样构建自动化工作流。
  • 角色进化: 从“手动写代码”进化为“规范驱动开发”的工作流指挥家

扫描下方二维码,开启你的 AI 原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

记一次软 RAID1 坏盘的恢复过程

记一次软 RAID1 坏盘的恢复过程

背景

最近遇到一个运维场景,两个 SATA 盘组了一个 RAID1,Linux 的根系统也在上面,启动时能进内核,但是内核一直在报错 link is too slow to respond, please be patient 以及 COMRESET failed (errno=-16)。下面记录一下故障排查以及恢复的过程。

恢复过程

考虑到 Linux 系统也在 RAID1 上面,所以找了另一台机器,接上两个 SATA 盘,然后观察到,其中一个盘直接无法识别,另一个盘可以正常访问,但它分区表里只有一个分区,参与到了 md 组的 RAID1 当中。遇到盘坏了又是 RAID,第一反应是买一个新盘,然后重建 RAID。但是一通询价,发现最近硬盘价格涨的比较多,所以先尝试如何单盘启动。由于是 UEFI 启动,推测 ESP 在已经坏的那个盘上面,好的盘上并没有 ESP,但它唯一的分区已经占满了整个空间,所以第一步是对 RAID 分区缩容,这就需要:

  1. 首先用 fsck -f /dev/md0 && resize2fs /dev/md0 newsize 对根分区进行缩容
  2. mdadm --grow --size=newsize /dev/md0 对 RAID 进行缩容
  3. 停止 RAID:mdadm --stop /dev/md0
  4. 重新分区,缩小 RAID 分区大小:cfdisk /dev/sda
  5. 重新启动 RAID,更新 device size:mdadm --assemble --update=devicesize /dev/md0 /dev/sda1

这些步骤完成以后,就可以在空余的空间里建 ESP 分区了:建分区,mkfs.vfat,挂载到 /mnt/boot/efi(假设 /dev/sda1 已经挂载到了 /mnt),接着 arch-chroot /mnt(或者手抄 Archlinux Wiki),进去 grub-install,修改 /etc/fstab,重新 update-grub

这个过程中,踩了一些小坑,比如:

  1. 重启以后直接进 grub shell,没有菜单显示出来,后来发现是 UEFI 启动项里有之前的旧残留,导致 grub 没有能够正确加载 ESP 里面的 grub.cfg,如果在 grub shell 里手动 source 一下是正常的
  2. 如果不更新 device size,那么 assemble 的时候会说 does not have a valid v1.2 superblock 报错,实际上就是它记录了旧的分区大小,和新的分区大小不匹配,此时要强制修改它
  3. 最后买了个新盘,但是不够大:960GB vs 1TB,导致如果要重组 RAID1 还得再缩小一次已有的 RAID1 分区,之前缩小的时候只给 ESP 预留了足够的空间,但分区还不够小到能够在新盘里建一个相同大小的分区

🔲 ☆

从“手搓 Prompt”到“无限循环”:AI 编码的下一个形态是“Ralph”吗?

本文永久链接 – https://tonybai.com/2026/01/21/ai-coding-evolution-from-prompting-to-ralph

大家好,我是Tony Bai。

“如果你把 AI 放在一个死循环里,给它足够的权限和上下文,会发生什么?”

2025 年底,一个名为 Ralph Wiggum Technique” (Ralph 循环) 的 AI 编程技巧在硅谷极客圈一夜爆红。它没有复杂的架构,没有花哨的界面,其核心代码甚至只有一行 Bash 脚本。

但就是这个看似简陋、甚至有些“诅咒”意味的技巧,却让开发者们在一夜之间重构了 6 个代码库,构建了全新的编程语言,甚至引发了 Anthropic 官方下场发布插件。

什么是 Ralph?为什么它如此有效?它又预示着怎样的 AI 编程未来?

Ralph 的诞生——一行代码的暴力美学

Ralph 的故事始于 Geoff Huntley 的一个疯狂实验。他没有使用复杂的 Agent 框架,而是写下了这样一行 Bash 脚本:

while :; do cat PROMPT.md | npx --yes @sourcegraph/amp ; done

这就是 Ralph 的全部。

  • PROMPT.md:这是唯一的输入,包含了项目的目标、规范、当前状态的描述(通常由 AI 自动更新)。
  • @sourcegraph/amp:这是一个极其简单的 CLI 工具,它读取提示词,调用 LLM,并在当前目录下执行命令(修改文件、运行测试等)。
  • while :; do … done:这就是灵魂所在。无限循环。

Ralph 不会停下来问你“这样行吗?”。它只是不断地读取目标、执行操作、再次读取目标、再次执行……直到你手动杀掉进程,或者它把代码库变成一团乱麻(所谓的“Overbaking”)。

为什么 Ralph 有效?—— Context Engineering 的胜利

乍一看,Ralph 似乎只是一个不可控的随机代码生成器。但实际上,它的成功揭示了 AI 编程的一个核心真理:上下文工程 (Context Engineering) 远比 Prompt 技巧更重要。

Ralph 的核心不在于那个 Bash 循环,而在于那个 PROMPT.md(或者更高级的“Specs”)。

声明式而非命令式

传统的 AI 辅助编程是“命令式”的:你告诉 AI “修改这个函数”、“修复那个 Bug”。

Ralph 是“声明式”的:你在 PROMPT.md 中描述项目的终局状态(Desired State),比如“所有的 React 组件必须使用 TypeScript 且没有 default exports”。Ralph 的工作就是不断逼近这个状态。

小切口,高频迭代

Ralph 并不试图一次性完成所有工作。它在每次循环中只处理一小块任务。这种“切碎”的工作方式,完美契合了 LLM 当前的上下文窗口限制,避免了“一次性生成几千行代码然后全错”的灾难。

自动化反馈循环

在 Ralph 的循环中,测试结果、Linter 报错、编译失败信息,都会成为下一个循环的输入。它不仅是在写代码,更是在自我修复

Ralph 的进化——从玩具到生产力

随着社区的介入,Ralph 迅速从一个 Bash 玩具进化为一种严肃的开发范式。

  • 重构利器:这是一次真实的重构经历。面对一个混乱的 React 前端,没有人工介入手动修改,而是花 30 分钟写了一份 REACT_CODING_STANDARDS.md(编码规范),然后让 Ralph 跑了 6 个小时。结果?Ralph 自主完成了一个人类可能需要数天才能完成的枯燥重构。
  • Cursed Lang:Geoff 甚至用 Ralph 构建了一门全新的编程语言 Cursed Lang,包含编译器、标准库,且实现了自举。
  • 官方下场:Anthropic 甚至推出了官方的 Ralph 插件。虽然被社区吐槽“过度设计”且不如 Bash 脚本好用,但这标志着这种模式已被主流认可。

警惕“Overbaking”——AI 也会“把菜烧焦”

Ralph 并非完美。它最大的风险在于 “Overbaking”(过度烘焙)

如果你让 Ralph 跑得太久,且 PROMPT.md 的约束不够紧,它可能会开始产生“幻觉”般的优化:添加没人需要的 Post-Quantum 密码学支持、过度拆分文件、甚至为了通过测试而删除测试。

这给我们的启示是:AI 是强大的引擎,但人类必须是方向盘。

  • 写好 Spec:如果你的 Spec(规格说明书)是垃圾,Ralph 产出的代码也是垃圾。
  • 监控循环:不要让它无限制地跑下去,设置检查点。
  • 小步快跑:最好的 Ralph 实践是“一夜重构一个模块”,而不是“一夜重构整个系统”。

小结:Agentic Coder 的未来

Ralph Wiggum Technique 可能只是 AI 编程进化史上的一朵浪花,但它留下的遗产是深远的。

它告诉我们,未来的编程可能不再是编写具体的逻辑,而是编写和维护一份完美的 Spec(规范说明书)。我们将成为“系统架构师”和“验收测试员”,而将那个枯燥、重复、且容易出错的“编码循环”,交给不知疲倦的 Ralph 们。

所以,下一次当你面对一座巨大的“屎山”代码时,不妨试着写一份清晰的 Spec,然后启动那个神奇的 Bash 循环。

资料链接:

  • https://ghuntley.com/ralph/
  • https://www.humanlayer.dev/blog/brief-history-of-ralph

从“暴力循环”到“优雅指挥”

Ralph Wiggum 的故事让我们看到了 AI 自主编程的雏形:只要有正确的 Spec(规范)和自动化的 Loop(循环),奇迹就会发生。

但 Ralph 毕竟只是一个 5 行代码的 Bash 脚本,粗糙且容易“烤糊”。在真实的工程实践中,我们不能只靠运气的“无限循环”,我们需要一套更稳定、更可控、更专业的AI 原生开发体系

如果你不想止步于 Ralph 这样的极客实验,而是想真正掌握驾驭 AI Agent 的系统方法,欢迎加入我的新专栏 AI原生开发工作流实战

这是关于如何构建你的“自动化流水线”:

  • 告别低效:不再做“复制粘贴喂 AI”的搬运工,建立自动化闭环。
  • 驾驭神器:深度实战 Claude Code 等前沿工具,它是比 Ralph 更成熟的“神灯精灵”。
  • 身份跃迁:从被动的“AI 使用者”,进化为定义规范、掌控全局的“工作流指挥家”

扫描下方二维码,别让 AI 只有暴力,让我们赋予它工程的优雅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

技术考古:Markdown 为何从博客工具演变成统治 AI 世界的“通用语”?

本文永久链接 – https://tonybai.com/2026/01/13/how-markdown-took-over-the-world

大家好,我是Tony Bai。

在这个由科技巨头主导、充斥着复杂算法和封闭生态的数字世界里,有一种技术显得格格不入。它没有专利壁垒,没有复杂的构建流程,甚至不需要特定的软件就能阅读。

它是 Markdown

近期,知名科技博主 Anil Dash 发布了一篇题为《How Markdown Took Over the World》的长文。他在文中深情回顾了这一格式的诞生与崛起,并指出:在这个由科技巨头主导、充斥着封闭生态的数字世界里,Markdown 是一场属于普通人的胜利。

如今,从 GitHub 上的亿万代码仓库,到 ChatGPT等大模型 生成的每一个回答,再到你随手记下的 Apple Notes,Markdown 无处不在。它不仅成为了技术人员的“普通话”,更意外地成为了 AI 时代的“通用语”。

这一切,都始于 20 年前一位“固执”的苹果博主为了偷懒而写的一个小脚本。今天,让我们跟随 Anil Dash 的视角,回顾这段充满偶然与必然的技术传奇。

缘起:一个博主的“偷懒”计划

2002 年,John Gruber 做了一个在当时看来极其不理性的决定:全职运营一个只关注苹果公司动态的博客——Daring Fireball

在那个博客刚刚兴起的蛮荒时代,发布内容并不容易。你要么忍受简陋的输入框,要么得手写复杂的 HTML 标签。为了能在写文章时(比如加粗、插入链接)不被繁琐的 HTML 标记打断思路,John 决定为自己开发一套工具。

他的核心理念是:既然 HTML (HyperText Markup Language) 太复杂,那就叫它 Markdown 吧。

如果你想加粗,就用 **;想引用,就用 >;想列表,就用 -。这些符号并非凭空创造,而是深受电子邮件时代纯文本格式习惯的影响。John 的天才之处在于,他将这些约定俗成的习惯标准化,并写了一个 Perl 脚本将它们转换为合法的 HTML。

2004 年 3 月,在 Aaron Swartz(那位早逝的天才少年)的协助测试下,Markdown 正式发布。没有人预料到,这个小小的工具将改变互联网的未来。

统治世界:从程序员到 AI

Markdown 的崛起并非一夜之间,但它的生命力却异常顽强。

  1. 开发者的拥抱:GitHub 的出现是关键转折点。它将 README.md 设为项目标配,使得 Markdown 成为了开发者描述项目的标准格式。
  2. 应用的普及:从 Slack 到 Discord,从 Notion 到 Obsidian,现代生产力工具几乎全部内置了 Markdown 支持。哪怕是 Google Docs 和 Apple Notes 这样的大众软件,最终也向用户需求妥协,加入了 Markdown 支持。
  3. AI 的通用语:最令人意想不到的转折发生在当下。当最前沿的 LLM(大型语言模型)需要一种格式来输出结构化内容时,它们不约而同地选择了 Markdown。因为它既对人类可读,又对机器友好,且完全开放。

Anil Dash 在他的回顾文章中总结了 Markdown 成功的 10 个技术原因,其中几点尤为深刻:

  • 解决真实问题:它不是为了“发明一种新格式”,而是为了解决“手写 HTML 太痛苦”这个具体痛点。
  • 利用现有习惯:它没有强迫用户学习新符号,而是沿用了电子邮件时代的纯文本习惯(如 > 表示引用)。
  • 没有知识产权 (IP) 负担:John Gruber 从未试图将其商业化或申请专利,这种彻底的开放性消除了所有采用者的顾虑。
  • “查看源码”的哲学:Markdown 文件本身就是教程。你只需要看一眼源文件,就能立刻学会怎么写。

硬币的另一面:自由的代价

当然,Markdown 这种彻底的自由和缺乏中央控制,也带来了一个长期的副作用——碎片化

正因为 John Gruber 当年只给出了一个 Perl 脚本而没有定义极其严谨的规范,导致后来出现了各种“方言”。GitHub 有自己的 GitHub Flavored Markdown (GFM),Reddit 有自己的解析规则,Obsidian 和 Notion 也都添加了各自的私有语法(如双向链接 [[Link]])。

这导致了一个尴尬的现实:虽然 Markdown 到处都是,但你的 Markdown 文件未必能在所有地方都完美渲染。 表格的语法支持不一,数学公式的写法各异,甚至连换行符的处理都有微妙差别。

直到后来 CommonMark 等项目的出现,才试图事后诸葛亮式地去修补这种分裂。

但幸运的是,Markdown 的核心语法(标题、列表、粗体、引用、链接)已经足够稳固,成为了事实上的标准。正是这最基础的 80% 功能,支撑起了它在 AI 时代的通用性。对于大语言模型而言,这些细微的方言差异完全可以忽略不计——它只需要用最基础的语法,就能让全世界读懂。

这也再次印证了那个道理:在规模化面前,简单且“足够好”的方案,往往能战胜完美但复杂的方案。

启示:善良与开放的力量

Markdown 的故事,是对当代科技行业的一种温柔提醒。

真正的互联网基础设施,往往不是由拿了巨额风投的初创公司在董事会里规划出来的。它们往往源于像 John Gruber 或 Aaron Swartz 这样的人——他们有正职工作,但也充满热情;他们为了解决自己的问题而造轮子,然后慷慨地将其分享给世界。

在这个被“护城河”、“生态闭环”和“商业化变现”充斥的时代,Markdown 证明了:一个好的点子,加上一颗慷慨的心,依然可以改变世界。

下次当你用 ** 加粗文字,或者看着 ChatGPT 逐行吐出格式完美的回答时,请记得:这背后没有复杂的商业算计,只有一位在费城看球赛的博主,想让你打字时能稍微轻松一点。

资料链接:https://www.anildash.com/2026/01/09/how-markdown-took-over-the-world/


你的 Markdown 记忆

Markdown 已经陪伴了我们 20 年。你还记得自己第一次接触 Markdown 是在什么场景下吗?是写 GitHub README,还是做笔记?你最喜欢的 Markdown 编辑器又是哪一款?

欢迎在评论区分享你的 Markdown 故事和神器推荐! 让我们一起致敬这个简单而伟大的工具。

如果这篇文章让你对 Markdown 有了全新的认识,别忘了点个【赞】和【在看】,并转发给你的朋友,哪怕他只是个爱记笔记的非程序员!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

Cusdis - 轻量、保护隐私、开源的 Disqus 评论系统替代方案

由于之前将博客全静态化了导致评论无法使用,原本是不打算做的,但是还是又挺多朋友通过各种各样的渠道联系我交流问题,故打算解决一下这个问题,方便后续交流。

我的解决方案是能自建就自建,无法自建的基本上就不考虑了。Disqus 不能自建,加之实际使用起来效果并不理想,隐私问题,还有界面不太喜欢就直接被我 Pass 掉了。

相交于 Disqus,Commento++也是不错的解决方案,但是界面不太喜欢,以及不够轻量,故没有选择,综合来考虑来看,Cusdis是非常不错的解决方案,足够轻量、支持自建、UI简洁,致使我最终选择了 Cusdis。

此处介绍两种方式 Docker、手工部署 Cusdis,均基于宝塔面板。


项目介绍

项目地址:Cusdis

Cusdis是Disqus的开源、轻量级(约5kb gzip)、隐私友好的绝佳替代品,主要用于纯静态化网站。

Cusdis 并非旨在成为 Disqus 的完整替代品。它的目的是为小型网站(例如您的静态博客)实现一个极简主义和可嵌入的评论系统。

官方文档:Cusdis Docs

特性

  • Cusdis 是开源的并且可以自我托管。因此,您拥有自己的数据。
  • SDK 是轻量级的(~5kb gzipped)。
  • Cusdis 不需要您的用户登录即可发表评论。
  • Cusdis 根本不使用 cookie。

缺点:

  • Cusdis 正处于其开发的早期阶段。
  • 没有垃圾邮件过滤器,因此,您必须手动审核您的评论部分,并且在您批准之前不会显示评论。

其UI简洁、支持自托管、支持邮件推送,在邮件内即可进行批准以及回复是我比较看重的功能。


环境配置

由于 Cusdis 基于Node.js 开发,采用 SQLite 或 MySQL 或 Postgresql 数据库存储数据,所以配置要求如下:

  • 系统上需要安装 Node.js 和 yarn
  • 服务器上安装了 MySQL 或 Postgresql

本文基于宝塔面板,使用 SQLite 进行安装

安装配置 Node.js

如您已安装 Node.js 和yarn 并且配置成功,则此步可以跳过。

安装 Node.js 命令如下

1
2
3
4
5
6
cd /www/server/nodejs
#国外服务器选择
wget https://nodejs.org/dist/v14.15.0/node-v14.15.0-linux-x64.tar.gz
#国内服务器选择
wget https://registry.npmmirror.com/-/binary/node/v14.15.0/node-v14.15.0-linux-x64.tar.gz
tar -zvxf node-v14.15.0-linux-x64.tar.gz

设置 Node.js 全局变量:

在宝塔面板打开 /etc/profile 文件,将以下配置输入文件最后面,并保存

1
2
export NODE_HOME=/www/server/nodejs/node-v14.15.0-linux-x64
export PATH=$NODE_HOME/bin:$PATH

输入以下命令用于重载全局配置。

1
source /etc/profile

输入 node -v 和 npm -v 返回以下信息即配置完成

img配置成功

安装 yarn

1
npm install -g yarn

查看版本信息,如遇 yarn:未找到命令,请看:《NodeJS 和 npm 配置全局变量

国内服务器此步骤由于 Node.js 没有大陆节点,速度较慢。请耐心等待

千万不要使用淘宝的 registry 镜像源!!!!!!!!

否则安装依赖会出错!!!!!!!!

部署 Cusdis

此处介绍两种方式部署,手动部署与使用Dokcer

手动部署

一、克隆仓库

1
2
cd /www/wwwroot
git clone https://github.com/djyde/cusdis.git

img

二、安装 Cusdis 所需依赖

国内服务器此步骤由于 Node.js 没有大陆节点,速度较慢。请耐心等待

千万不要使用淘宝的 registry 镜像源!!!!!!!!

否则安装依赖会出错!!!!!!!!

1
2
cd cusdis
yarn install

img

三、配置 .env 文件

在 cusdis 文件夹中新建一个名为 .env 的文件

img

文件具体配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
USERNAME=admin
PASSWORD=password
JWT_SECRET=ofcourseistillloveyou
NEXTAUTH_URL=http://IP_ADDRESS_OR_DOMAIN
HOST=http://IP_ADDRESS_OR_DOMAIN
DB_TYPE=sqlite
DB_URL=file:./data.db
#以下配置为 EMAIL 配置 可选
SMTP_HOST=smtp.gmail.com
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=your gmail email
SMTP_PASSWORD=<app password>
SMTP_SENDER=your gmail email

其中

为用户名
1
2
3
4
5
6
7
8
9
10
PASSWORD 为密码
JWT_SECRET 为 JWT 令牌
NEXTAUTH_URL 与 HOST 需要填写项目所用的域名/IP
DB_TYPE、DB_URL 为 数据库类型、数据地址
SMTP_HOST 为 SMTP 主机
SMTP_USER 为 SMTP 用户名
SMTP_PASSWORD 为 SMTP 密码
SMTP_SENDER 为发件人电子邮件地址
SMTP_PORT 为 SMTP 端口 默认为 587
SMTP_SECURE 为是否启用 SMTP 安全,默认值为true

img

四、构建 Cusdis

1
yarn run build:without-migrate

img

这一步可能会出现错误,错误代码如下:

1
2
3
4
5
6
7
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! cusdis@ db:generate: `prisma generate --schema ./prisma/$DB_TYPE/schema.prisma`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the cusdis@ db:generate script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

这一步需要到 ./cusdis/prisma 选择需要设置的数据库类型,然后将该文件夹内的 schema.prisma 文件 复制到 ./cusdis/prisma 构建即可解决。

五、运行 Cusdis

1
yarn run start:with-migrate

img

设置程序守护,此处以systemd为例

打开目录 /usr/lib/systemd/system/

新建文件:cusdis.service

文件配置如下(以下配置仅供参考):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=CusdisServer

[Unit]
Description=CusdisServer

[Service]
ExecStart=/www/server/nodejs/node-v14.15.0-linux-x64/bin/npm start
Restart=always
Environment=PATH=/usr/bin:/usr/local/bin:/www/server/nodejs/node-v14.15.0-linux-x64/bin
Environment=NODE_ENV=production
WorkingDirectory=/www/wwwroot/cusdis/

[Install]
WantedBy=multi-user.target

此时保存即可,运行:

1
2
3
4
5
6
# 更新配置
systemctl daemon-reload
# 启动服务
systemctl start cusdis
# 设置开机启动
systemctl enable cusdis

详细的管理命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
# 启动服务
systemctl start cusdis
# 设置开机启动
systemctl enable cusdis
# 停止服务
systemctl stop cusdis
# 重启服务
systemctl restart cusdis
# 查看状态
systemctl status cusdis
# 更新配置
systemctl daemon-reload

Docker部署

一、获取镜像

镜像名如下:

1
djyde/cusdis

img

国内服务器获取镜像会极慢,请耐心等待。

二、创建容器

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
配置如下:
容器端口:3000
容器目录:/data
环境变量:
-e USERNAME=admin \
-e PASSWORD=password \
-e JWT_SECRET=ofcourseistillloveyou \
-e DB_URL=file:/data/db.sqlite \
-e NEXTAUTH_URL=http://IP_ADDRESS_OR_DOMAIN \
其中
USERNAME 为用户名
PASSWORD 为密码
JWT_SECRET 为 JWT 令牌
NEXTAUTH_URL 需要填写项目所用的域名/IP
DB_TYPE、DB_URL 为 数据库类型、数据地址

#以下配置为 EMAIL 配置 可选
SMTP_HOST=smtp.gmail.com
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=your gmail email
SMTP_PASSWORD=<app password>
SMTP_SENDER=your gmail email

img

启动完成。

后语

设置反向代理

宝塔新建站点 -> 打开SSL - > 反向代理 ->添加反向代理

目标 URL 填写:http://127.0.0.1:3000

img

img

配置站点

进入页面登陆,然后创建一个默认的网站,选择 Embed Code ,复制代码到网站的合适位置即可。

img

WordPress 配置

以下配置仅供参考,主要包括 data-page-id、data-page-url、data-page-title

1
2
3
data-page-id="<?php the_ID(); ?>"
data-page-url="<?php $id =get_the_ID();echo get_permalink($id); ?>"
data-page-title="<?php the_title(); ?>"

跨域问题

最开始使用的时候可能会出现跨域问题。

在反向代理的配置文件中添加:

1
add_header 'Access-Control-Allow-Origin' 'yousitedomain';

两种方法,更推荐 DOCKER 如选择手工安装,可能会遇到各种各样的问题。

🔲 ☆

Linux 桌面系统故障排查指南(六) - 系统关机与电源管理

AI 创作声明:本系列文章由笔者借助 ChatGPT, Kimi K2, 豆包和 Cursor 等 AI 工具创作,有很大篇幅的内容完全由 AI 在我的指导下生成。如有错误,还请指正。

前言

系统关机看似简单,但背后涉及了繁杂的资源清理和状态管理过程。当你点击关机按钮,系统却卡在那里不动,或者出现各种奇怪的错误信息时,理解关机流程和故障排查方法就显得尤为重要。

除了关机,Linux 还提供了休眠和挂起两种重要的电源管理功能,它们可以让系统快速进入低功耗状态,同时保持工作状态,是日常使用中非常实用的功能。

作为这个系列的最后一篇文章,本文将探讨系统关机的完整流程,以及休眠和挂起功能的配置与故障排查,从优雅关闭到强制关机,从服务停止到资源清理,从电源管理到状态恢复,全面了解系统的电源管理机制。


系统关机流程

1.1 关机流程概览

systemd 管理的关机过程分为四个主要阶段,每个阶段都有明确的目标和顺序,确保数据完整性和系统稳定性。

关机阶段

  1. 用户会话清理阶段(约 1-5 秒):

    • 通知所有用户会话即将关机
    • 优雅关闭用户应用程序
    • 回收用户设备权限
  2. 系统服务停止阶段(约 2-10 秒):

    • 按依赖关系逆向停止系统服务
    • 卸载文件系统(除根文件系统外)
    • 网络服务断开连接
  3. 内核资源释放阶段(约 1-3 秒):

    • 同步所有文件系统到磁盘
    • 卸载根文件系统为只读
    • 终止所有剩余进程
  4. 硬件关机阶段(约 1-2 秒):

    • 通过 ACPI 发送关机信号
    • 固件接管系统控制权
    • 所有硬件设备断电

1.2 用户会话清理

当用户发起关机时,systemd 首先处理用户会话的清理工作,确保用户数据得到妥善保存。

会话清理流程

# systemd 发送关机信号
systemctl start shutdown.target

# 用户会话收到终止信号
loginctl terminate-session <session_id>

# 用户服务停止
systemctl --user stop graphical-session.target

关键操作

  • 会话通知:通过 D-Bus 向桌面环境发送关机信号
  • 应用关闭:等待应用保存未保存的数据
  • 权限回收:logind 回收分配给用户的设备访问权限
  • 服务停止:用户 systemd 实例停止所有用户服务

监控用户会话清理

# 查看会话状态变化
journalctl -b | grep -E "(session|Session)"

# 用户服务停止日志
journalctl --user -b | grep -E "(Stopping|Stopped)"

# 设备权限回收
journalctl -u systemd-logind -b | grep -i "device"

1.3 系统服务停止

用户会话清理完成后,systemd 开始按依赖关系的逆向顺序停止系统服务。

服务停止顺序

  • 图形服务:合成器、显示管理器
  • 网络服务:网络管理器、DNS 解析器
  • 存储服务:磁盘管理、LVM
  • 基础服务:日志、设备管理

关键服务处理

# 查看关机时的服务停止顺序
systemd-analyze critical-chain shutdown.target

# 监控服务停止状态
watch -n 1 'systemctl list-units --state=deactivating'

# 检查服务停止日志
journalctl -b -1 | grep -E "(Stopping|Stopped)" | tail -20

文件系统卸载

# 查看挂载点卸载情况
mount | grep -v "on / type"

# 文件系统同步状态
sync
echo 3 > /proc/sys/vm/drop_caches

# 检查卸载错误
journalctl -b -1 | grep -i "unmount\|busy"

1.4 内核资源释放

当所有用户空间服务停止后,systemd 执行最终的系统清理:

文件系统操作

  • 调用 sync() 同步所有已挂载文件系统的数据到磁盘
  • 按照逆向挂载顺序卸载所有挂载点
  • 卸载外接硬盘分区等外部存储设备

进程管理

  • 向所有剩余进程发送 SIGTERM,给它们最后清理机会
  • 等待超时后,对顽固进程发送 SIGKILL 强制终止
  • 清理僵尸进程和孤儿进程

Watchdog 监控

  • systemd 的看门狗机制监控服务关闭进度
  • 如果服务停止超过 TimeoutStopSec,强制终止服务
  • 防止系统在关机过程中无限挂起

资源清理

  • GPU 驱动重置显卡状态,释放 VRAM
  • 网络设备完全断电
  • 音频设备硬件重置

1.5 硬件关机

当所有用户空间和内核资源处理完毕后,系统进入硬件关机:

ACPI 操作

  • systemd 通过 ACPI 向固件发出关机指令
  • 进入 ACPI S5 状态,告诉固件关闭电源

固件接管

  • BIOS/UEFI 接管系统控制权
  • 执行电源关断,所有设备(CPU、内存、GPU、外部设备)断电
  • 固件执行最后的清理工作

强制关机保护

  • 如果系统未能正常关机,硬件看门狗可能强制切断电源
  • 用户长按电源键也会触发强制关机

此时机器完全断电,关机过程结束。下次开机将重新开始完整的启动周期。

1.6 关机故障排查

常见关机问题与优化

  1. 服务停止超时
# 查看超时服务
journalctl -b -1 | grep -i "timeout"

# 检查特定服务配置
systemctl cat <service> | grep Timeout

服务停止超时优化:

TimeoutStopSec 参数控制服务停止的最大等待时间,默认值为 90 秒。systemd 在停止服务时会等待服务自行退出,超时后强制终止。对于快速停止的服务,可以设置较短的超时时间(如 10-30 秒), 配置示例:TimeoutStopSec=30s 设置 30 秒超时。

服务停止优化包括:服务应该正确处理 SIGTERM 信号,完成必要的清理工作;避免在停止过程中进行耗时的操作;确保及时释放文件句柄、网络连接等资源。

  1. 文件系统卸载失败
# 查找占用文件系统的进程
lsof | grep <mountpoint>

# 检查文件系统状态
fsck -n /dev/<device>

文件系统卸载优化:

进程占用检查使用 lsof 命令查找仍在使用文件系统的进程。常见原因是应用程序未正确关闭文件句柄,或进程仍在运行。解决方案是强制终止占用进程,或等待进程自然结束。

文件系统状态检查包括:使用 fsck -n 进行只读检查,不修复文件系统;检查文件系统是否正确挂载,是否有错误标记;定期进行文件系统检查,及时发现和修复问题。

  1. 设备繁忙
# 检查设备占用
lsof | grep /dev/<device>

# 查看块设备状态
lsblk -f

设备占用优化:

设备占用分析检查哪些进程仍在使用设备文件。常见设备包括 USB 设备、外部存储、网络设备等。解决方案是确保应用程序正确关闭设备,或强制卸载设备。

块设备状态检查包括:使用 lsblk 查看设备挂载状态和文件系统类型;检查设备是否处于忙碌状态; 在关机前确保所有外部设备已安全移除。

强制关机处理与优化

当正常关机失败时,可以使用以下方法:

# 安全强制关机
systemctl poweroff -f

# 紧急关机(立即执行)
systemctl poweroff -ff

# 内核强制重启
echo b > /proc/sysrq-trigger

# 内核强制关机
echo o > /proc/sysrq-trigger

强制关机方法:

systemctl poweroff -f 强制关机,跳过某些检查和服务停止。强制终止所有进程,直接进入关机流程,可能导致数据丢失,应谨慎使用,适用于系统响应缓慢但仍有基本功能时。

systemctl poweroff -ff 紧急关机,立即执行,不等待任何操作完成。立即终止所有进程,强制关机,高数据丢失风险,仅在紧急情况下使用,适用于系统完全无响应,需要立即关机。

echo b > /proc/sysrq-trigger 内核级别的强制重启。直接调用内核重启功能,绕过用户空间,即使系统完全无响应也能执行,适用于系统完全卡死,无法响应用户命令。

echo o > /proc/sysrq-trigger 内核级别的强制关机。直接调用内核关机功能,立即断电,最高数据丢失风险,适用于极端紧急情况,需要立即断电。

关机优化最佳实践:

预防措施:定期检查服务配置,确保服务能正常停止;监控文件系统状态,及时处理问题;避免在关机前进行大量 I/O 操作。

优雅关机:优先使用正常的关机命令;给系统足够时间完成清理工作;避免频繁使用强制关机。

故障预防:定期更新系统和驱动;监控系统资源使用情况;及时处理系统警告和错误。


系统休眠与挂起

除了关机,Linux 还提供了两种重要的电源管理功能:休眠(Hibernate)挂起 (Suspend)。这两种功能可以让系统快速进入低功耗状态,同时保持工作状态,是日常使用中非常实用的功能。

3.1 休眠(Hibernate)功能

休眠是将系统内存中的所有数据保存到磁盘(通常是交换分区或交换文件),然后完全关闭电源。当系统从休眠中恢复时,会从磁盘读取保存的数据,恢复到休眠前的状态。

休眠的工作原理

  1. 内存数据保存:将 RAM 中的所有数据写入到交换分区或专门的休眠文件
  2. 系统状态保存:保存 CPU 状态、设备状态、网络连接等
  3. 完全断电:系统完全关闭,所有硬件断电
  4. 快速恢复:开机时直接从磁盘恢复内存状态,跳过正常启动过程

休眠配置

# 检查当前休眠配置
cat /sys/power/state
cat /sys/power/disk

# 检查交换分区大小(需要足够容纳内存数据)
swapon --show
free -h

# 检查休眠文件(如果使用文件而非交换分区)
ls -lh /swapfile

启用休眠功能

# 方法一:使用交换分区
# 1. 确保有足够大的交换分区(建议为内存大小的 1.5-2 倍)
sudo swapon --show

# 2. 获取交换分区的 UUID
sudo blkid | grep swap

# 3. 更新 GRUB 配置
sudo nano /etc/default/grub
# 添加:GRUB_CMDLINE_LINUX_DEFAULT="resume=UUID=your-swap-uuid"

# 4. 更新 GRUB 配置
sudo update-grub

# 5. 重新生成 initramfs
sudo update-initramfs -u

# 方法二:使用交换文件
# 1. 创建交换文件(大小建议为内存的 1.5-2 倍)
sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# 2. 永久挂载交换文件
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# 3. 配置休眠到交换文件
echo 'RESUME=UUID=$(findmnt -no UUID -T /swapfile)' | sudo tee /etc/initramfs-tools/conf.d/resume
sudo update-initramfs -u

休眠故障排查

# 检查休眠支持
cat /sys/power/state | grep disk

# 检查休眠目标
cat /sys/power/disk

# 测试休眠功能
sudo systemctl hibernate

# 查看休眠日志
journalctl -b | grep -i hibernate
dmesg | grep -i hibernate

# 检查交换空间使用情况
swapon --show
free -h

常见休眠问题

  1. 交换空间不足

    • 问题:交换分区或文件太小,无法容纳内存数据
    • 解决:增加交换空间大小,建议为内存的 1.5-2 倍
  2. 休眠文件损坏

    • 问题:休眠文件损坏导致恢复失败
    • 解决:删除损坏的休眠文件,重新创建
  3. 硬件不支持

    • 问题:某些硬件不支持休眠功能
    • 解决:检查 BIOS/UEFI 设置,更新固件

3.2 挂起(Suspend)功能

挂起是将系统进入低功耗状态,保持内存供电,CPU 和大部分硬件断电。系统可以快速恢复到挂起前的状态,但需要持续供电。

挂起的工作原理

  1. 内存保持供电:RAM 继续供电,保持数据不丢失
  2. CPU 进入睡眠状态:CPU 进入深度睡眠,功耗极低
  3. 外设断电:硬盘、USB 设备、网络设备等断电
  4. 快速唤醒:通过键盘、鼠标、网络唤醒等快速恢复

挂起类型

  • S1(Power On Suspend):CPU 停止执行,但保持供电
  • S2(CPU Off):CPU 断电,但保持缓存
  • S3(Suspend to RAM):CPU 和缓存断电,仅内存供电
  • S4(Suspend to Disk):等同于休眠

挂起配置

# 检查支持的挂起状态
cat /sys/power/state

# 检查当前挂起模式
cat /sys/power/mem_sleep

# 设置挂起模式(deep 为 S3,s2idle 为 S2)
echo deep | sudo tee /sys/power/mem_sleep

# 永久设置挂起模式
echo 'mem_sleep_default=deep' | sudo tee -a /etc/default/grub
sudo update-grub

挂起故障排查

# 测试挂起功能
sudo systemctl suspend

# 查看挂起日志
journalctl -b | grep -i suspend
dmesg | grep -i suspend

# 检查挂起相关服务
systemctl status systemd-suspend
systemctl status systemd-hibernate

# 检查挂起钩子脚本
ls -la /usr/lib/systemd/system-sleep/

常见挂起问题

  1. 挂起后无法唤醒

    • 问题:系统挂起后无法通过键盘、鼠标唤醒
    • 解决:检查 BIOS 设置,启用 USB 唤醒功能
  2. 挂起后系统重启

    • 问题:挂起后系统自动重启而不是恢复
    • 解决:检查硬件兼容性,更新驱动
  3. 挂起功耗过高

    • 问题:挂起状态下功耗仍然很高
    • 解决:检查外设电源管理,禁用不必要的设备

3.3 电源管理模式对比

模式 功耗 恢复时间 数据保持 适用场景
关机 0W 30-60秒 不保持 长时间不使用
休眠 0W 10-30秒 完全保持 长时间不使用,需要快速恢复
挂起 1-5W 1-3秒 完全保持 短时间不使用,需要快速恢复

选择建议

  • 短时间离开(几分钟到几小时):使用挂起
  • 长时间离开(几小时到几天):使用休眠
  • 长期不使用(几天以上):使用关机

混合使用策略

# 设置自动挂起(当系统空闲时)
sudo systemctl enable systemd-suspend.timer

# 设置定时休眠(夜间自动休眠)
sudo systemctl edit systemd-hibernate.timer
# 添加:
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

在实际使用中,大多数用户通过桌面环境的设置界面来配置电源管理功能。GNOME、KDE Plasma、XFCE 等桌面环境都提供了图形化的电源管理设置,可以方便地配置自动挂起和休眠时间。

对于使用 Wayland 合成器(如 Sway、Hyprland)的用户,通常使用专门的 idle 守护进程来管理电源状态。swayidle、hypridle 等工具可以配置系统在空闲时自动锁屏、关闭显示器或进入挂起状态。

电源管理优化

# 检查电源管理配置
cat /sys/power/pm_async
cat /sys/power/pm_freeze_timeout

# 优化挂起延迟
echo 5000 | sudo tee /sys/power/pm_freeze_timeout

# 检查设备电源管理
ls /sys/bus/usb/devices/*/power/
cat /sys/bus/usb/devices/*/power/control

通过合理配置和使用休眠、挂起功能,可以显著提高 Linux 桌面系统的使用体验,既节省电力又保持工作状态的连续性。


实战案例:综合故障排查

在实际使用 Linux 桌面系统时,往往会遇到多层次、多组件交织的故障。通过系统化的排查方法,可以快速定位问题并制定解决方案。本章通过几个典型案例,讲解如何综合使用日志、调试工具和系统命令进行故障排查。

2.1 案例一:桌面环境无法启动

现象:用户登录后,屏幕闪烁后回到登录界面,桌面无法显示。

排查步骤

  1. 检查显示管理器状态
systemctl status display-manager
journalctl -u display-manager -b
  1. 确认用户会话
loginctl list-sessions
loginctl show-session <session_id>
  1. 检查合成器日志(Wayland 示例):
journalctl --user -u sway -f
export WAYLAND_DEBUG=1
  1. 检查 GPU 驱动状态
lspci -k | grep -A 3 -i vga
dmesg | grep -i drm

常见原因

  • 驱动不匹配或未加载
  • 合成器启动失败
  • 用户环境变量设置错误

解决方法

  • 更新或切换 GPU 驱动
  • 使用默认配置启动合成器
  • 检查 $XDG_RUNTIME_DIR$WAYLAND_DISPLAY 是否正确

2.2 案例二:应用程序崩溃或无响应

现象:某些应用程序启动后立即崩溃,或运行中无响应。

排查步骤

  1. 查看用户服务日志
journalctl --user -b -u <application>.service
  1. 启用应用调试信息
export GDK_DEBUG=all    # GTK 应用
export QT_LOGGING_RULES="qt.qpa.*=true"  # Qt 应用
export WAYLAND_DEBUG=1
  1. 分析核心转储
coredumpctl list
coredumpctl info <pid>
coredumpctl debug <pid>
  1. 检查依赖库版本
ldd $(which <application>)

常见原因

  • 缺少或版本不匹配的库
  • Wayland/Xwayland 支持不完整
  • GPU 驱动异常

解决方法

  • 安装或升级依赖库
  • 强制应用使用 X11 或 Wayland 后端
  • 检查驱动更新或使用回滚版本

2.3 案例三:系统关机或重启异常

现象:系统关机卡住,服务停止超时,最终需要强制关机。

排查步骤

  1. 查看关机日志
journalctl -b -1 -e
systemd-analyze blame shutdown.target
  1. 检查服务停止状态
systemctl list-units --state=deactivating
journalctl -b -1 | grep -E "(Stopping|Stopped)"
  1. 文件系统状态
mount | grep -v "on / type"
lsof | grep <mountpoint>
  1. 硬件设备状态
lsblk -f
dmesg | grep -i "error\|fail\|timeout"

常见原因

  • 某些服务或进程未能及时停止
  • 文件系统被占用或损坏
  • 设备驱动异常导致无法卸载

解决方法

  • 强制停止顽固服务:
systemctl stop <service> -i
  • 检查并修复文件系统:
fsck -n /dev/<device>
  • 临时使用强制关机:
systemctl poweroff -ff

2.4 案例四:网络异常导致应用无法访问

现象:应用启动正常,但无法连接网络资源。

排查步骤

  1. 检查网络接口和状态
ip addr
ip route
nmcli device status
  1. 测试 DNS 和连通性
ping 8.8.8.8
dig www.example.com
  1. 查看网络服务日志
journalctl -u NetworkManager -b
  1. 检查防火墙和权限
sudo iptables -L -v -n
sudo nft list ruleset

常见原因

  • DHCP 或静态 IP 配置错误
  • DNS 配置异常
  • 防火墙阻塞访问

解决方法

  • 修复网络配置
  • 检查防火墙规则
  • 重启网络服务

2.5 综合排查方法

面对复杂问题,单靠经验可能难以定位故障,推荐遵循以下方法:

  1. 日志为先:系统日志、用户服务日志、应用日志是最直接的线索
  2. 逐层排查:从硬件 → 驱动 → 系统服务 → 用户会话 → 应用
  3. 最小复现:关闭非必要服务和应用,简化环境重现问题
  4. 工具辅助journalctlstracecoredumpctllsofperf
  5. 文档与社区:查阅官方文档和社区经验,快速定位常见故障

通过上述方法,可以系统化地分析并解决大多数 Linux 桌面问题,提高系统稳定性和用户体验。

总结

至此,我们已经完成了《Linux 桌面系统故障排查指南》系列的全部六篇文章。通过这个系列,我们全面了解了 Linux 桌面系统的各个组件,从启动安全到网络配置,从多媒体输入到会话管理,从系统服务到电源管理。

Linux 桌面系统虽然有时候会出各种奇怪的问题,但理解其工作原理后,大部分问题都能找到解决思路。关键是要有耐心,多实践,多总结。特别是在电源管理方面,合理使用关机、休眠和挂起功能,可以显著提高系统的使用体验和电力效率。

这个系列到这里就结束了,希望这些内容能帮助你在 Linux 桌面的道路上走得更顺畅一些。

🔗 相关资源

🔲 ☆

Linux 桌面系统故障排查指南(二) - systemd 全家桶与服务管理

AI 创作声明:本系列文章由笔者借助 ChatGPT, Kimi K2, 豆包和 Cursor 等 AI 工具创作,有很大篇幅的内容完全由 AI 在我的指导下生成。如有错误,还请指正。

概述

本文是《Linux 桌面系统故障排查指南》系列的第二篇,专注于 systemd 生态系统与服务管理。在上一篇中,我们了解了系统启动与安全框架,现在让我们深入探讨 systemd 核心功能以及 systemd 生态系统中的各个专门化组件。

⚙️ 本文主要介绍如下内容:

  • systemd 核心功能:服务管理、依赖关系、并行启动、单元类型配置
  • systemd 生态系统服务:systemd-journald、systemd-oomd、systemd-resolved、systemd-timesyncd、systemd-udevd 等
  • 设备管理:udev 规则和设备权限分配、故障排查
  • D-Bus 系统总线:进程间通信机制、权限管控、调试方法

1. systemd 核心功能

systemd 作为 PID 1,是现代 Linux 系统的初始化系统和服务管理器。它负责并行启动服务、维护依赖关系、管理 cgroups,并提供统一的系统管理接口。

1.1 systemd 概览与基本操作

systemd 作为现代 Linux 系统的初始化系统和服务管理器,主要专注于服务管理和系统控制。

核心功能

  • 服务管理:并行启动 units,维护依赖关系
  • 资源控制:通过 cgroups 实现进程隔离和资源限制
  • 系统状态管理:通过 target 管理不同的系统运行状态
  • 单元生命周期管理:管理各种类型单元(service、mount、timer 等)的启动、停止和重启

常用命令

# 系统状态查看
systemctl get-default                     # 默认 target
systemctl list-units --type=service       # 列出服务
systemctl status sshd.service             # 服务状态

# 性能分析
systemd-analyze blame                     # 启动耗时分析
systemd-analyze critical-chain            # 关键路径分析

# 服务管理
systemctl start/stop/restart service      # 服务控制
systemctl enable/disable service          # 开机自启控制
systemctl reload service                  # 重载配置

NixOS 特殊说明:在 NixOS 中,/etc/systemd/system 下的配置文件都是通过声明式参数生成的软链接,指向 /nix/store。修改配置应通过 NixOS 配置系统,而非直接编辑这些文件。NixOS 没有传统的 /usr/lib 等 FHS 目录,所有软件包都存储在 /nix/store 中,通过/run/current-system/sw/ 等符号链接提供访问。

配置文件路径

  • /etc/systemd/system/:系统级服务配置
  • /run/current-system/sw/lib/systemd/system/(NixOS)或 /usr/lib/systemd/system/(传统发行版):软件包提供的默认配置
  • /etc/systemd/user/:用户级服务配置

1.2 服务单元类型与配置

systemd 支持多种单元类型,每种类型都有其特定的用途和配置方式。

主要单元类型

  • service:服务单元,管理后台进程
  • target:目标单元,用于系统状态管理
  • mount:挂载单元,管理文件系统挂载
  • timer:定时器单元,替代 cron 任务
  • socket:套接字单元,按需启动服务

服务单元配置示例

[Unit]
Description=My Custom Service
After=network.target
Wants=network.target

[Service]
Type=simple
ExecStart=/usr/bin/my-service
Restart=always
User=myuser
Group=mygroup

[Install]
WantedBy=multi-user.target

1.3 systemd 依赖关系与启动顺序

systemd 通过依赖关系管理服务的启动顺序,确保服务按正确的顺序启动。

依赖关系类型

  • Requires:强依赖,被依赖服务失败时,依赖服务也会失败
  • Wants:弱依赖,被依赖服务失败时,依赖服务仍可启动
  • After:启动顺序依赖,确保在指定服务之后启动
  • Before:启动顺序依赖,确保在指定服务之前启动

示例配置

[Unit]
Description=Web Server
After=network.target
Wants=network.target
Requires=nginx.service

[Service]
Type=forking
ExecStart=/usr/sbin/nginx
Restart=always

[Install]
WantedBy=multi-user.target

2. systemd 生态系统服务

除了基本的服务管理外,systemd 还提供了多个专门化的系统服务来支持现代 Linux 桌面的核心功能,包括日志管理、内存管理、DNS 解析和时间同步等。

本节内容仅介绍最核心的几个 systemd 服务。

systemd 全家桶,你值得拥有(

2.1 日志系统:systemd-journald

systemd-journald 是 systemd 内置的日志收集守护进程,统一处理内核、系统服务及应用的日志,是现代 Linux 系统日志管理的核心组件。

2.1.1 核心特性

特性 说明
统一收集 内核日志、systemd 单元(stdout/stderr)、普通进程、容器、第三方 syslog 均汇总到同一日志流。
二进制索引 以 B+树(有序索引)+偏移量建立字段索引,支持精确查询与时间/优先级范围查询,速度远超文本 grep。
字段化存储 自动生成 _PID_UID_SYSTEMD_UNIT 等可信字段(不可伪造);支持自定义 FOO=bar 字段。
自动轮转与压缩 按「大小、时间、文件数」回收日志;轮转后默认用 LZ4 压缩,节省 60% 以上空间。
速率限制 可通过 RateLimitIntervalSec=/RateLimitBurst= 调整。
日志防篡改 配置 Seal=yes 后,用 journalctl --setup-keys 生成密钥,之后可用该密钥验证日志完整性。

2.1.2 日志的4个收集入口

journald 仅通过标准化入口收集日志,确保来源可追溯:

  1. 内核日志:内核 printk() 输出 → /dev/kmsg → journald(会自动添加 _PID/_COMM 等字段);
  2. systemd 单元 stdout/stderr:单元进程输出自动捕获,会附加_SYSTEMD_UNIT=xxx.service 等 systemd 相关字段;
  3. 本地 Socket/run/systemd/journal/socket 等,接收 logger/systemd-cat 及旧 syslog 应用日志;
  4. 显式 APIsd_journal_send(),仅需自定义复杂结构化日志时使用(譬如 Docker daemon), 一般直接 print 即可。

2.1.3 日志优先级与核心配置

1. 日志优先级简述

日志按严重程度分 8 级(数字越小,级别越高),常用级别:

  • err:错误(部分功能异常),级别 3
  • warning:警告(潜在风险),级别 4
  • info:信息(常规运行日志),级别 6
  • debug:调试(开发细节),级别 7

可用于筛选关键日志。

2. journald 配置

主配置文件:/etc/systemd/journald.conf,支持通过 /etc/systemd/journald.conf.d/*.conf 覆盖配置,核心配置项如下:

配置项 说明 示例
Storage= 存储策略 persistent(存 /var/log/journal,推荐)/volatile(存内存)
SystemMaxUse= 持久存储最大占用 1G
MaxRetentionSec= 日志最大保留时间 1month
ForwardToSyslog= 是否转发到旧日志系统 yes(兼容传统文本日志)
Seal= 是否启用日志防篡改 yes

生产配置示例

# /etc/systemd/journald.conf.d/00-production.conf
[Journal]
Storage=persistent
SystemMaxUse=2G
MaxRetentionSec=3month
ForwardToSyslog=yes
Seal=yes

配置生效需重启服务:sudo systemctl restart systemd-journald

2.1.4 实验:用 logger 验证日志收集

下面演示如何使用 logger结构化日志直接写进 journal,并立即用 journalctl 检索。

首先写入日志:

logger --journald <<EOF
SYSLOG_IDENTIFIER=myapp
PRIORITY=3
MESSAGE=用户登录失败
USER_ID=alice
LOGIN_RESULT=fail
EOF

其中的 SYSLOG_IDENTIFIER, PRIORITY, MESSAGE 在 journald 中都有属性对应,而后两个USER_IDLOGIN_RESULT 则属于自定义的日志标签。

然后查询日志:

# 2. 按标识符过滤
journalctl -t myapp
# 等价于
journalctl SYSLOG_IDENTIFIER=myapp

# 3. 按优先级+自定义字段精确定位
journalctl -p err LOGIN_RESULT=fail

2.1.5 旧日志系统与 /var/log/ 解析

旧日志系统:基于 syslog 的文本管理

在 systemd 普及前,Linux 依赖 syslog 协议+文本文件 管理日志,核心组件是rsyslog(syslog 主流实现,功能强于早期 syslogd)。

  • 旧系统工作流:应用通过 syslog(3) 接口输出日志 → rsyslog 接收 → 按「设施+优先级」写入 /var/log/ 文本文件;
  • 现代系统中的角色:rsyslog 不再是核心收集器,而是作为「兼容层」——接收 journald 转发的日志,生成传统文本文件(如 /var/log/auth.log),或转发到远程日志服务器(支持 TCP/TLS 加密)。
/var/log/ 常见文件及功能

现代系统中,这些文件由 rsyslog 生成(兼容旧习惯),不同发行版名称略有差异,但都为纯文本格式:

文件(或目录) 主要发行版差异 功能说明
/var/log/messages RHEL/CentOS/SUSE 系统通用日志:服务启停、内核提示、非专项应用消息。
/var/log/syslog Ubuntu/Debian 等价于 RHEL 的 messages,存储内核及一般系统日志。
/var/log/auth.log(Ubuntu) / /var/log/secure(RHEL) 名称不同 认证与授权事件:SSH 登录、su/sudo、用户添加/删除、PAM 告警。安全审计必看。
/var/log/kern.log 通用 仅内核环控输出:硬件故障、驱动加载、OOM、segfault。
/var/log/cron 通用 crond 执行记录:任务启动/结束、错误输出、邮件发送结果。
/var/log/btmp 通用 二进制文件,记录失败登录(lastb 读取);大小随暴力破解增长。
/var/log/wtmp 通用 二进制文件,记录成功登录/注销/重启(last、who 读取)。
/var/log/lastlog 通用 二进制文件,记录每个用户最近一次登录时间(lastlog 读取)。
/var/log/journal/ 启用 systemd-journald 后可见 目录;若 Storage=persistent,则二进制 journal 文件存于此。

2.1.6 日志写入最佳实践

场景 推荐做法
Shell脚本(独立运行) logger -t 脚本名 -p daemon.err "错误:$msg"(如 logger -t backup -p err "备份失败"
应用程序 优先考虑使用 systemd service, 少数场景可考虑直接调用 sd_journal_send() API
容器 Docker/Podman 加 --log-driver=journald(容器内正常输出即可)
高频日志 RateLimitIntervalSec=0 关闭限制(需评估风险),或批量写入
敏感信息 脱敏处理(如 PASSWORD=***),避免明文存储

2.1.7 运维命令速查

# 一、日志查询(含优先级过滤)
# 实时跟踪服务日志(仅看 err 及以上级别)
journalctl -f -p err -u sshd.service
# 等价于
journalctl -f -p err _SYSTEMD_UNIT=sshd.service
# 按时间+优先级过滤(过去1小时 warning 及以上)
journalctl --since "1h ago" -p warning
# -p 的参数既可使用名称,也可使用对应的数字,warning 对应 4
journalctl --since "1h ago" -p 4
# 内核日志(本次启动的 err 日志)
journalctl -k -p err -b
# 按自定义字段过滤(USER_ID=1001 + 优先级 err)
journalctl USER_ID=1001 -p err
# 通过 Perl 格式的正则表达式搜索日志
journalctl --grep "Auth"

# 二、日志管理
# 查看 journal 占用空间
sudo journalctl --disk-usage
# 清理日志(保留最近2周/500M)
sudo journalctl --vacuum-time=2weeks
sudo journalctl --vacuum-size=500M
# 手动轮转日志
sudo journalctl --rotate

# 三、旧日志文件操作
# 实时查看认证日志(Ubuntu)
tail -f /var/log/auth.log
# 实时查看认证日志(CentOS)
tail -f /var/log/secure

# 四、日志防篡改验证
sudo journalctl --setup-keys > /etc/journal-seal-key
sudo chmod 600 /etc/journal-seal-key
sudo journalctl --verify --verify-key=$(cat /etc/journal-seal-key)

2.2 内存管理:systemd-oomd

systemd-oomd 是 systemd 提供的内存不足(OOM)守护进程,用于在系统内存紧张时主动终止进程, 防止系统完全卡死。听起来有点"残忍",不过总比系统彻底死机要好。

工作原理

  • 内存监控:实时监控系统内存使用情况和内存压力
  • 智能选择:基于 cgroup 层次结构和内存使用量选择要终止的进程
  • 用户空间保护:优先终止用户空间进程,保护系统关键服务
  • 渐进式处理:逐步释放内存,避免过度 kill 进程

配置示例

# NixOS 配置
systemd.oomd.enable = true;

systemd.oomd.extraConfig = ''
  [OOM]
  DefaultMemoryPressureLimitSec=20s
  DefaultMemoryPressureLimit=60%
'';

配置文件路径/etc/systemd/oomd.conf

监控与调试

# 查看 oomd 状态
systemctl status systemd-oomd

# 内存压力信息
cat /proc/pressure/memory

# 查看 oomd 日志
journalctl -u systemd-oomd -f

# 内存使用统计
systemctl status user@$(id -u).service

2.3 DNS 解析:systemd-resolved

systemd-resolved 提供统一的 DNS 解析服务,支持 DNSSEC 验证、DNS over TLS 等现代 DNS 特性。名字是长了点,不过功能倒是挺全面的,基本上把 DNS 解析这件事包圆了。

主要功能

  • 统一接口:为系统提供单一的 DNS 解析入口
  • 本地缓存:缓存 DNS 查询结果,提高解析速度
  • DNSSEC 支持:验证 DNS 响应的真实性
  • 隐私保护:支持 DNS over TLS(DoT), 但截止目前(2025 年)尚未支持 DNS over HTTPS(DoH).

配置方法

# 启用 systemd-resolved
services.resolved.enable = true;

# 配置 DNS 服务器
networking.nameservers = [
  "8.8.8.8" "1.1.1.1"                    # IPv4
  "2001:4860:4860::8888" "2606:4700:4700::1111"  # IPv6
];

# 高级配置
services.resolved.extraConfig = ''
  [Resolve]
  DNSSEC=yes
  DNSOverTLS=yes
  Cache=yes
'';

配置文件路径/etc/systemd/resolved.conf

使用命令

# DNS 状态查看
resolvectl status

# DNS 查询测试
resolvectl query example.com
resolvectl query -t AAAA ipv6.google.com

# 缓存管理
resolvectl flush-caches
resolvectl statistics

# DNS 服务器状态
resolvectl dns

2.4 时间同步:systemd-timesyncd

systemd-timesyncd 是轻量级 NTP 客户端,负责保持系统时间与网络时间服务器同步。功能简单直接,就是确保你的系统时间不会跑偏,避免出现"时间穿越"的尴尬情况。

功能特点

  • 轻量级设计:相比完整 NTP 服务占用更少资源
  • 自动同步:定期与时间服务器同步
  • SNTP 协议:使用简单网络时间协议
  • systemd 集成:与 systemd 服务管理深度集成

NixOS 配置

# 启用时间同步
services.timesyncd.enable = true;

# 配置 NTP 服务器
services.timesyncd.servers = [
  "pool.ntp.org"
  "time.google.com"
  "ntp.aliyun.com"
];

配置文件路径/etc/systemd/timesyncd.conf

时间同步管理

# 时间状态查看
timedatectl status
timedatectl timesync-status

# 手动控制
timedatectl set-ntp true   # 启用 NTP
timedatectl set-timezone Asia/Shanghai

# 查看同步日志
journalctl -u systemd-timesyncd -f

# 时间精度检查
chronyc tracking  # 如果安装了 chrony

3. 设备管理:udev 与 systemd-udevd

udev 是 Linux 用户空间的设备管理员,负责处理内核的设备事件,创建节点并设置权限。在现代 systemd 系统中,udev 功能由 systemd-udevd 守护进程实现,它是 systemd 生态系统的重要组成部分。

3.1 udev 与 systemd-udevd

3.1.1 udev 设备管理框架

udev 是 Linux 内核的用户空间设备管理框架,负责处理内核的设备事件并管理 /dev 目录下的设备节点。

udev 的核心功能

  • 动态设备管理:当硬件设备插入或移除时,自动创建设备节点
  • 设备命名:提供一致的设备命名规则,如 /dev/disk/by-uuid//dev/input/by-id/
  • 权限控制:根据设备类型和用户需求设置适当的设备权限
  • 规则系统:通过规则文件实现复杂的设备处理逻辑

udev 的工作原理

  1. 内核检测到硬件变化,通过 netlink socket 发送 uevent 到用户空间
  2. udev 守护进程接收 uevent,解析设备属性
  3. 根据规则文件匹配设备,执行相应的动作(创建设备节点、设置权限等)

3.1.2 systemd-udevd 实现

在现代 systemd 系统中,udev 用户空间的功能由 systemd-udevd 守护进程实现,它是 systemd 生态系统的重要组成部分。

systemd-udevd 的优势

  • systemd 集成:作为 systemd 服务运行,享受 systemd 的服务管理、日志记录、依赖管理等功能
  • 性能优化:相比传统的 udevd,systemd-udevd 在启动速度和资源使用上有所优化
  • 统一管理:与 systemd 的其他组件(如 systemd-logind)深度集成,提供统一的设备权限管理

systemd-udevd 服务管理

# 查看服务状态
systemctl status systemd-udevd

# 重启服务
sudo systemctl restart systemd-udevd

# 查看服务日志
journalctl -u systemd-udevd -f

3.1.3 工作流程

完整的设备管理流程如下:

  1. 硬件检测:内核检测到硬件变化(插入、移除、状态改变)
  2. 事件发送:内核通过 netlink socket 发送 uevent 到用户空间
  3. 事件接收systemd-udevd 接收 uevent,解析设备属性
  4. 规则匹配:根据规则文件(/run/current-system/sw/lib/udev/rules.d/(NixOS)或/usr/lib/udev/rules.d/(传统发行版)、/etc/udev/rules.d/)匹配设备
  5. 动作执行:执行匹配规则中定义的动作(RUN 脚本、设置 OWNER/GROUP/MODE、创建 symlink、设置权限)
  6. systemd 集成:通知 systemd,可能触发 device units

3.1.4 配置示例

基本规则示例

# /etc/udev/rules.d/90-mydevice.rules
SUBSYSTEM=="input", ATTRS{idVendor}=="abcd", ATTRS{idProduct}=="1234", MODE="660", GROUP="input", TAG+="uaccess"

规则说明

  • SUBSYSTEM=="input":匹配输入设备子系统
  • ATTRS{idVendor}=="abcd":匹配厂商 ID
  • ATTRS{idProduct}=="1234":匹配产品 ID
  • MODE="660":设置设备权限为 660
  • GROUP="input":设置设备组为 input
  • TAG+="uaccess":添加 uaccess 标签,让 systemd-logind 接管设备权限

高级规则示例

# /etc/udev/rules.d/99-custom-storage.rules
# 为特定 USB 存储设备创建符号链接
SUBSYSTEM=="block", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", SYMLINK+="myusb"

# 为特定网卡设置持久化名称
SUBSYSTEM=="net", ATTRS{address}=="aa:bb:cc:dd:ee:ff", NAME="eth0"

# 为特定设备运行自定义脚本
SUBSYSTEM=="usb", ATTRS{idVendor}=="abcd", RUN+="/usr/local/bin/my-device-handler.sh"

TAG+="uaccess" 是现代桌面用来让 systemd-logind 接管设备权限与 session ACL(由 logind 配置),确保只有当前活动会话能访问输入、音频、GPU 等设备。

3.2 设备权限与 ACL

现代 systemd + logind 使用 udev tag uaccessseat 标签来由 logind 把设备 ACL 授予当前的登录 session。具体流程:

  • systemd-udevd 创建 /dev/input/eventX 并打上 TAG+="uaccess".
  • systemd-logind 对应的 PAM/session 系统会把该设备的 ACL 授予当前会话的用户,这样运行在会话内的 Wayland compositor 与其子进程可以访问设备。

检查设备权限分配

# 查看某设备的 udev 属性
$ udevadm info -a -n /dev/input/event5

# 实时监控 udev 事件
$ sudo udevadm monitor --udev --property

# 查看 seat 状态与 ACL
$ loginctl seat-status seat0
# 或
$ loginctl show-session <id> -p Remote -p Display -p Name

3.3 故障排查

场景:插入外接键盘后,Wayland 会话收不到键盘事件(键盘无效)

排查步骤:

  1. 检查 systemd-udevd 服务状态:

    systemctl status systemd-udevd
  2. 在主机上用 udevadm monitor 插入键盘,观察是否有 udev 事件被触发:

    sudo udevadm monitor --udev
  3. 检查 /dev/input/ 是否生成新节点:ls -l /dev/input/by-id

  4. udevadm info -a -n /dev/input/eventX 查看该设备的属性,确认 TAG 是否包含uaccessseat.

  5. 使用 loginctl seat-status seat0 看设备是否分配给当前会话。若没有,可能是 PAM/session 未正确建立或 udev 规则没有打上 tag。

  6. 检查 systemd-udevd 的日志:

    journalctl -b -u systemd-udevd
    journalctl -k | grep -i udev
  7. 临时解决:用 chmod/chown 修改设备权限验证是否恢复(不建议长期采用):

    sudo chown root:input /dev/input/eventX
    sudo chmod 660 /dev/input/eventX
  8. 永久修复:在 /etc/udev/rules.d/ 中添加规则确保 TAG+="uaccess" 或正确的OWNER/GROUP。然后 udevadm control --reload-rules && sudo udevadm trigger

注意:NixOS 下直接编辑 /etc/udev/rules.d 可能是临时的(Nix 管理的文件会被系统重建覆盖),正确做法是在 configuration.nix 中配置 services.udev.extraRules 或把规则放在environment.etc 并由 Nix 管理。

配置文件路径

  • /etc/udev/rules.d/:系统管理员自定义规则(优先级最高)
  • /run/current-system/sw/lib/udev/rules.d/(NixOS)或 /usr/lib/udev/rules.d/(传统发行版):软件包提供的默认规则

4. D-Bus 系统总线 - 应用间通信的主要通道

D-Bus 是 Linux 系统中主流的进程间通信(IPC)机制,旨在解决不同进程(尤其是桌面应用、系统服务)间的高效、安全通信问题,广泛用于 GNOME、KDE 等桌面环境及系统服务管理(如 systemd)。它本质是 “消息总线”,通过中心化的 “总线守护进程” 实现多进程间的消息路由。名字虽然有点奇怪, 功能倒是挺实在的。

4.1 D-Bus 项目背景

D-Bus 并非 systemd 社区的项目,而是 freedesktop.org 的独立项目。D-Bus 在 systemd 出现之前就已经存在,是 Linux 桌面环境标准化进程间通信的重要基础设施。

D-Bus 与 systemd 的关系

  • 独立项目:D-Bus 由 freedesktop.org 维护,有自己的发布周期和开发团队
  • 深度集成:systemd 将 D-Bus 作为核心依赖,深度集成到其架构中
  • 服务管理:systemd 负责启动和管理 D-Bus 守护进程(dbus-daemon)
  • 统一接口:systemd 通过 D-Bus 提供统一的服务管理接口
    • systemd 本身就是一个 D-Bus 服务,我们在使用 systemctl 命令与 systemd 交互时,实际上就是 D-Bus 与 org.freedesktop.systemd1 通信。

4.2 关键概念

D-Bus 通过 「对象 - 接口」 模型(跟面向对象编程(OOP)中的概念有些类似)封装功能,以下结合systemd1logind1 的真实定义,对应核心概念:

概念 定义与作用 示例(systemd1/logind1)
总线(Bus) D-Bus 消息传输的基础通道,分系统 / 会话两大类 系统总线 /var/run/dbus/system_bus_socketsystemd1/logind1 唯一使用的总线)
服务名(Name) 服务端在总线上的 ID,通常每个应用程序一个 org.freedesktop.systemd1systemd 服务名)、org.freedesktop.login1logind 服务名)
对象(Object) 服务内部的功能组织单元,通过对象路径进行标识。每个对象可以代表不同的资源。 /org/freedesktop/systemd1systemd1 根对象)、/org/freedesktop/login1logind1 根对象)
接口(Interface) 每个接口定义了一组方法和信号 org.freedesktop.systemd1.Managersystemd1 核心接口)、org.freedesktop.login1.Managerlogind1 核心接口)
方法(Method) 方法是对象接口中定义的函数,可以被远程调用。方法属于某个接口,而接口由对象实现。(有请求有返回) systemd1StartUnit(启动系统单元,如 nginx.service)、logind1ListSessions(查询所有活跃用户会话)
信号(Signal) 对象发出的单向事件通知,支持多播(无返回值) systemd1UnitActiveChanged(单元状态变化,如 nginxinactive 变为 active)、logind1SessionNew(新用户登录创建会话)
属性(Property) 对象的 「状态数据」,支持读取 / 写入 / 变更通知 systemd1ActiveUnits(所有活跃系统单元列表)、logind1CanPowerOff(当前系统是否允许关机,布尔值)

可使用 busctl list 查看系统中的所有 D-Bus 对象:

# 所有 system bus 对象
› busctl --system list --no-pager | grep org.
org.blueman.Mechanism                     - -               -                (activatable) -                         -       -
org.bluez                              1421 bluetoothd      root             :1.6          bluetooth.service         -       -
org.bluez.mesh                            - -               -                (activatable) -                         -       -
org.freedesktop.Avahi                  1420 avahi-daemon    avahi            :1.7          avahi-daemon.service      -       -
org.freedesktop.DBus                      1 systemd         root             -             init.scope                -       -
org.freedesktop.Flatpak.SystemHelper      - -               -                (activatable) -                         -       -
org.freedesktop.GeoClue2                  - -               -                (activatable) -                         -       -
org.freedesktop.PolicyKit1             2216 polkitd         polkituser       :1.22         polkit.service            -       -
org.freedesktop.RealtimeKit1           2539 rtkit-daemon    root             :1.41         rtkit-daemon.service      -       -
org.freedesktop.UDisks2                2492 udisksd         root             :1.31         udisks2.service           -       -
org.freedesktop.home1                     - -               -                (activatable) -                         -       -
org.freedesktop.hostname1                 - -               -                (activatable) -                         -       -
org.freedesktop.import1                   - -               -                (activatable) -                         -       -
org.freedesktop.locale1                   - -               -                (activatable) -                         -       -
org.freedesktop.login1                 1504 systemd-logind  root             :1.8          systemd-logind.service    -       -
org.freedesktop.machine1                  - -               -                (activatable) -                         -       -
org.freedesktop.network1               1292 systemd-network systemd-network  :1.3          systemd-networkd.service  -       -
org.freedesktop.oom1                    934 systemd-oomd    systemd-oom      :1.1          systemd-oomd.service      -       -
org.freedesktop.portable1                 - -               -                (activatable) -                         -       -
org.freedesktop.resolve1               1293 systemd-resolve systemd-resolve  :1.0          systemd-resolved.service  -       -
org.freedesktop.systemd1                  1 systemd         root             :1.4          init.scope                -       -
org.freedesktop.sysupdate1                - -               -                (activatable) -                         -       -
org.freedesktop.timedate1                 - -               -                (activatable) -                         -       -
org.freedesktop.timesync1              1148 systemd-timesyn systemd-timesync :1.2          systemd-timesyncd.service -       -
org.opensuse.CupsPkHelper.Mechanism       - -               -                (activatable) -                         -       -

# 所有 session bus 对象
› busctl --user list --no-pager | grep org.
...
org.fcitx.Fcitx-0                                                                 76699 fcitx5          ryan :1.284        user@1000.service -       -
org.fcitx.Fcitx5                                                                  76699 fcitx5          ryan :1.282        user@1000.service -       -
org.freedesktop.DBus                                                               2127 systemd         ryan -             user@1000.service -       -
org.freedesktop.FileManager1                                                          - -               -    (activatable) -                 -       -
org.freedesktop.Notifications                                                      3539 .mako-wrapped   ryan :1.81         user@1000.service -       -
org.freedesktop.ReserveDevice1.Audio0                                              2542 wireplumber     ryan :1.50         user@1000.service -       -
org.freedesktop.ReserveDevice1.Audio1                                              2542 wireplumber     ryan :1.50         user@1000.service -       -
org.freedesktop.ScreenSaver                                                        2192 niri            ryan :1.9          user@1000.service -       -
org.freedesktop.a11y.Manager                                                       2192 niri            ryan :1.13         user@1000.service -       -
org.freedesktop.impl.portal.PermissionStore                                        2410 .xdg-permission ryan :1.28         user@1000.service -       -
org.freedesktop.impl.portal.Secret                                                    - -               -    (activatable) -                 -       -
org.freedesktop.impl.portal.desktop.gnome                                             - -               -    (activatable) -                 -       -
org.freedesktop.impl.portal.desktop.gtk                                            2475 .xdg-desktop-po ryan :1.33         user@1000.service -       -
org.freedesktop.portal.Desktop                                                     2350 .xdg-desktop-po ryan :1.26         user@1000.service -       -
org.freedesktop.portal.Documents                                                   2428 .xdg-document-p ryan :1.30         user@1000.service -       -
org.freedesktop.portal.Fcitx                                                      76699 fcitx5          ryan :1.283        user@1000.service -       -
org.freedesktop.portal.Flatpak                                                        - -               -    (activatable) -                 -       -
org.freedesktop.portal.IBus                                                       76699 fcitx5          ryan :1.285        user@1000.service -       -
org.freedesktop.secrets                                                            2161 .gnome-keyring- ryan :1.55         session-1.scope   1       -
org.freedesktop.systemd1                                                           2127 systemd         ryan :1.1          user@1000.service -       -
...

4.3 系统总线与会话总线

总线类型 作用场景 典型用途 运行用户
系统总线(System Bus) 系统级服务通信 systemd1 单元管理(启动 / 停止服务)、logind1 用户会话 / 电源控制(关机 / 重启) root(特权)
会话总线(Session Bus) 单个用户会话内的应用通信 桌面应用交互(如窗口切换、通知) 当前登录用户

4.4 D-Bus 的三类角色

  1. 总线守护进程(dbus-daemon)

    架构的 「中枢」,每个总线对应一个守护进程,核心职责:

    • 管理进程的连接(如验证 普通用户 是否有权调用 logind1PowerOff 方法);

    • 路由消息(将客户端请求的 「启动 nginx 服务」 转发给 systemd1);

    • 维护服务注册表(记录 org.freedesktop.login1logind 进程的映射关系)。

  2. 服务端(Service)

    提供功能的进程(如 systemd 进程、logind 进程),核心操作:

    • 向总线注册 「服务名」(systemd1 注册 org.freedesktop.systemd1logind1 注册org.freedesktop.login1,均为唯一标识);

    • 暴露 「对象」 和 「接口」(如 systemd1 暴露 /org/freedesktop/systemd1 对象与org.freedesktop.systemd1.Manager 接口),供客户端调用。

  3. 客户端(Client)

    调用服务的进程(如 systemctl 命令、桌面电源菜单),核心操作:

    • 连接系统总线后,通过服务名(如 org.freedesktop.login1)找到 logind 服务;

    • 调用服务端暴露的方法(如通过 logind1ListSessions 查询当前用户会话),或订阅信号(如监听 systemd1UnitActiveChanged 单元状态变化)。

4.5 常见操作示例

下面我们通过一些命令来演示 D-Bus 总线的用途:

# 模拟 `systemctl status dbus` 的功能
busctl --system --json=pretty call \
  org.freedesktop.systemd1 \
  /org/freedesktop/systemd1/unit/dbus_2eservice \
  org.freedesktop.DBus.Properties GetAll s org.freedesktop.systemd1.Unit

# 模拟 `systemctl stop sshd`
sudo gdbus call --system \
  --dest org.freedesktop.systemd1 \
  --object-path /org/freedesktop/systemd1 \
  --method org.freedesktop.systemd1.Manager.StopUnit \
  "sshd.service" "replace"

# 模拟 `systemctl start sshd`
sudo gdbus call --system \
  --dest org.freedesktop.systemd1 \
  --object-path /org/freedesktop/systemd1 \
  --method org.freedesktop.systemd1.Manager.StartUnit \
  "sshd.service" "replace"

# 模拟 `notify-send "The Summary" "Here’s the body of the notification"`
nix shell nixpkgs#glib
gdbus call --session \
    --dest org.freedesktop.Notifications \
    --object-path /org/freedesktop/Notifications \
    --method org.freedesktop.Notifications.Notify \
    my_app_name \
    42 \
    gtk-dialog-info \
    "The Summary" \
    "Here’s the body of the notification" \
    [] \
    {} \
    5000

# 获取当前时区
busctl get-property org.freedesktop.timedate1 /org/freedesktop/timedate1 \
    org.freedesktop.timedate1 Timezone

# 查询主机名
busctl get-property org.freedesktop.hostname1 /org/freedesktop/hostname1 \
    org.freedesktop.hostname1 Hostname

4.6 调试与监控命令

# 看 systemctl 与 systemd 的完整交互(method-call + signal)
sudo busctl monitor --system | grep 'org.freedesktop.systemd1'
# 或者使用 --match 过滤,但这需要提前知道 interface 的全名
sudo busctl monitor --match='interface=org.freedesktop.systemd1.Manager'

# 跟 busctl monitor 功能几乎完全一致,也可通过 match rule 过滤
sudo dbus-monitor --system "interface='org.freedesktop.systemd1.Manager'"

# gdbus 只监听 signals,只能用来调试「服务有没有正确发出 signal」
sudo gdbus monitor --system -d org.freedesktop.systemd1.Manager

4.7 D-Bus 的权限管控

4.7.1 D-Bus 的原生权限管控机制

D-Bus 本身具备多层权限管控能力,从总线接入、消息路由到敏感操作授权,形成了系统级的基础安全保障,核心机制包括:

  1. 总线配置文件(静态规则管控)

    通过 XML 配置文件定义细粒度访问规则,实现对 「谁能访问哪些服务 / 方法」 的静态限制。例如:

    • 系统总线的服务级规则(如 /etc/dbus-1/system.d/org.freedesktop.login1.conf)可限制普通用户调用 org.freedesktop.login1.Manager.PowerOff(关机方法);

    • 全局规则(如 /etc/dbus-1/system.conf)可限定仅 rootdbus 组用户访问org.freedesktop.systemd1(systemd 服务)的核心接口。

      规则遵循 「deny 优先级高于 allow、服务级规则高于全局规则」 的逻辑,从总线层面直接拦截未授权请求。

  2. PolicyKit(动态授权管控)

    针对静态规则无法覆盖的动态场景(如普通用户临时需要执行敏感操作),D-Bus 集成 PolicyKit(现称 polkit)实现动态授权。系统服务(如 logind1systemd1)会在/run/current-system/sw/share/polkit-1/actions/(NixOS 中)或/run/current-system/sw/share/polkit-1/actions/(NixOS)或/usr/share/polkit-1/actions/(传统发行版中)定义 “可授权动作”,例如org.freedesktop.login1.power-off(对应 logind1 的关机方法):

    • 普通用户调用时,会触发认证流程(如输入管理员密码),认证通过后临时获得授权;

    • 活跃控制台用户可直接授权,无需额外验证,兼顾安全性与易用性。

  3. 连接层基础隔离

    D-Bus 总线套接字(如系统总线 /var/run/dbus/system_bus_socket)默认仅开放 rootdbus 组用户的读写权限,普通进程需通过 dbus-daemon 认证后才能建立连接。同时,每个连接会被分配唯一 ID(如 :1.42),并与进程的 PID/UID/GID 绑定,防止身份伪造与未授权接入。

4.7.2 Flatpak 对 D-Bus 权限的细粒度管控

在现代 Linux 桌面中,若需将商业软件等非信任应用运行在沙箱中,同时保障 「必要 D-Bus 交互不中断、越权访问被阻断」,Flatpak 采用 「底层沙箱隔离 + 上层代理过滤」 的双层方案 —— 其中 bubblewrap 是 Flatpak 依赖的底层沙箱工具,负责环境隔离;xdg-dbus-proxy 是上层过滤组件,负责 D-Bus 细粒度管控,两者协同实现完整安全隔离:

4.7.2.1 底层基础隔离:bubblewrap 的 “socket 隐藏与代理挂载”

Flatpak 以 bubblewrap(简称 bwrap)为底层沙箱基础,利用其 bind mountuser namespace 能力完成环境初始化,核心目标是切断沙箱应用与宿主 D-Bus 总线的直接联系:

  • 隐藏宿主 socketbubblewrap 会屏蔽宿主的 D-Bus 总线套接字(如不将/var/run/dbus/system_bus_socket 挂载进沙箱),避免应用绕过管控直接访问宿主总线;

  • 挂载代理 socket:同时,bubblewrap 会将 xdg-dbus-proxy 在宿主侧预先创建的 私有代理 socket,通过 bind mount 挂载到沙箱内的默认 D-Bus socket 路径(如沙箱内的/var/run/dbus/system_bus_socket)。

    此时沙箱应用感知到的 「D-Bus 总线」,实际是 xdg-dbus-proxy 提供的代理接口,无法直接接触宿主真实总线。

4.7.2.2 上层规则过滤:xdg-dbus-proxy 的 “白名单校验”

xdg-dbus-proxy 作为 Flatpak 内置的 D-Bus 代理组件,会随沙箱应用启动,加载 Flatpak 根据应用权限声明自动生成的过滤规则(粒度远细于 D-Bus 原生静态配置),例如:

    --see=NAME                   Set 'see' policy for NAME
    --talk=NAME                  Set 'talk' policy for NAME
    --own=NAME                   Set 'own' policy for NAME
    --call=NAME=RULE             Set RULE for calls on NAME
    --broadcast=NAME=RULE        Set RULE for broadcasts from NAME

TODO

这些规则可精确到 「服务名 + 接口 + 方法 + 对象路径」,弥补 D-Bus 原生配置在沙箱场景下 「动态性不足、粒度较粗」 的局限。

4.7.2.3 消息流转:代理的 “校验 - 转发” 逻辑

沙箱应用无需修改代码,会默认连接沙箱内的 「代理 socket」,所有 D-Bus 消息(方法调用、信号订阅)均需经过 xdg-dbus-proxy 的校验:

  • 若目标服务 / 方法在白名单内(如 org.freedesktop.portal.FileChooser.OpenFile),代理会将消息转发至宿主 D-Bus 总线,并把返回结果回传应用;

  • 若目标不在白名单内(如 org.freedesktop.login1.Manager.PowerOff),代理直接返回AccessDenied 错误,不向宿主总线转发任何消息,彻底阻断越权访问。


总结

本文深入探讨了 systemd 核心功能及其生态系统,从服务管理到各个专门化组件:

  1. systemd 核心功能:作为 PID 1 的服务管理器,专注于服务管理、依赖关系管理、资源控制和系统状态管理
  2. systemd 生态系统服务:包括日志管理(journald)、内存管理(oomd)、DNS 解析 (resolved)、时间同步(timesyncd)、设备管理(udevd)等
  3. 设备管理:udev 规则和设备权限分配,通过 systemd-udevd 确保硬件设备正确识别和访问
  4. D-Bus 系统总线:进程间通信机制,支持系统服务和桌面应用的交互

虽然 systemd 的争议一直存在,但不可否认的是,它确实让系统管理变得更加统一和高效。掌握了这些组件的使用方法,你在面对各种系统问题时就不会那么手足无措了。

下一篇文章我们会聊聊桌面会话和图形渲染,看看用户登录后系统是如何把漂亮的桌面呈现给你的。


🔲 ☆

Linux 桌面系统故障排查指南(一) - 系统启动与安全框架

AI 创作声明:本系列文章由笔者借助 ChatGPT, Kimi K2, 豆包和 Cursor 等 AI 工具创作,有很大篇幅的内容完全由 AI 在我的指导下生成。如有错误,还请指正。

前言

本文将简要介绍 Linux 桌面系统的启动机制,从 UEFI 引导到内核加载,从 initramfs 到 systemd 服务启动,再到桌面环境加载。同时还会探讨系统的安全框架,了解 PAM、PolicyKit 等组件如何保护系统安全。


1. 系统启动流程

启动的四个关键阶段

Linux 桌面系统的启动过程可以分为以下几个主要阶段:

  1. 固件阶段:UEFI 固件初始化硬件
  2. 引导加载器阶段:加载内核和 initramfs
  3. 内核阶段:硬件探测和驱动加载
  4. initramfs 阶段:准备根文件系统
  5. 用户空间阶段:systemd 接管系统管理

UEFI:系统启动的起点

现代系统普遍使用 UEFI 固件 代替 BIOS。UEFI 初始化硬件后,从 EFI System Partition (ESP) 中加载启动管理器。

NixOS 在 UEFI 系统上支持多种引导加载器。默认使用 GRUB;启用 Secure Boot 时通常使用systemd-boot 配合 lanzaboote

systemd-boot 的全局配置是 /boot/loader/loader.conf,具体的启动项配置需要分类讨论:

  • Type 1:手动配置 (Boot Loader Specification Type #1

    • 配置方式:/loader/entries/*.conf,位于 EFI 系统分区(ESP)或 Extended Boot Loader Partition(XBOOTLDR)下

    • 特点:

      • 可自定义启动项名称、内核参数、initrd 等
      • 描述 Linux 内核及其 initrd,也可以描述任意 EFI 可执行文件
      • 包括 fallback / rescue 内核
    • 示例:

      title   NixOS Linux
      linux   /vmlinuz-linux
      initrd  /initrd-linux.img
      options root=UUID=xxxx rw
  • Type 2:统一内核镜像 (Boot Loader Specification Type #2

    • 配置方式:将 EFI 格式的 UKI 镜像放在 ESP 分区的 /EFI/Linux/ 下即可
    • 工作原理:
      1. systemd-boot 在启动时扫描 ESP 的 /EFI/Linux/ 目录
      2. systemd-boot 会自动将扫描到的内核镜像添加到启动菜单,无需单独的 .conf 文件
    • 特点:
      • 免配置,自动出现在启动菜单中
      • vmlinuz-linux, initrd 跟 cmdline 等信息被统一打包成一个 EFI 镜像,一个镜像就包含了系统启动需要的所有数据,更方面简洁。
  • 其他自动识别的启动项

    • Microsoft Windows EFI boot manager(如果已安装)
    • Apple macOS boot manager(如果已安装)
    • EFI Shell 可执行文件(如果已安装)
    • 「Reboot Into Firmware Interface」选项(如果 UEFI 固件支持)
    • Secure Boot 变量注册(如果固件处于 setup 模式,且 ESP 提供了相关文件)

常用命令

  • efibootmgr -v:查看 / 修改固件启动顺序
  • bootctl status:检查 systemd-boot 安装与 ESP 状态
  • bootctl list:列出启动条目
  • ukify inspect /boot/EFI/Linux/nixos-xxx.efi: 查看 efi 镜像中包含的信息

示例:

# 查看固件启动顺序
$ nix run nixpkgs#efibootmgr -v

BootCurrent: 0000
Timeout: 0 seconds
BootOrder: 0000,0004
Boot0000* NixOS HD(1,GPT,34286f3b-d4df-456d-bf7a-eb67f2bf1a72,0x1000,0x12b000)/EFI\BOOT\BOOTX64.EFI
...
Boot0004* Windows Boot Manager  HD(1,GPT,34286f3b-d4df-456d-bf7a-eb67f2bf1a72,0x1000,0x12b000)/\EFI\Microsoft\Boot\bootmgfw.efi0000424f

# 检查 systemd-boot 安装与 ESP 状态
$ bootctl status

System:
      Firmware: UEFI 2.80 (American Megatrends 5.27)
 Firmware Arch: x64
   Secure Boot: enabled (user)
  TPM2 Support: yes
  Measured UKI: yes
  Boot into FW: supported

Current Boot Loader:
      Product: systemd-boot 257.7
     Features: ✓ Boot counting
               ✓ Menu timeout control
               ✓ One-shot menu timeout control
               ✓ Default entry control
               ✓ One-shot entry control
               ✓ Support for XBOOTLDR partition
               ✓ Support for passing random seed to OS
               ✓ Load drop-in drivers
               ✓ Support Type #1 sort-key field
               ✓ Support @saved pseudo-entry
               ✓ Support Type #1 devicetree field
               ✓ Enroll SecureBoot keys
               ✓ Retain SHIM protocols
               ✓ Menu can be disabled
               ✓ Multi-Profile UKIs are supported
               ✓ Boot loader set partition information
    Partition: /dev/disk/by-partuuid/34286f3b-d4df-456d-bf7a-eb67f2bf1a72
       Loader: └─EFI/BOOT/BOOTX64.EFI
Current Entry: nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi
...
Available Boot Loaders on ESP:
          ESP: /boot (/dev/disk/by-partuuid/34286f3b-d4df-456d-bf7a-eb67f2bf1a72)
         File: ├─/EFI/systemd/systemd-bootx64.efi (systemd-boot 257.7)
               └─/EFI/BOOT/BOOTX64.EFI (systemd-boot 257.7)
...
Default Boot Loader Entry:
         type: Boot Loader Specification Type #2 (.efi)
        title: NixOS Xantusia 25.11.20250830.d7600c7 (Linux 6.16.4) (Generation 848, 2025-09-01)
           id: nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi
       source: /boot//EFI/Linux/nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi (on the EFI System Partition)
     sort-key: lanza
      version: Generation 848, 2025-09-01
        linux: /boot//EFI/Linux/nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi
      options: init=/nix/store/gaj3sp3hrzjhp59bvyxhc8flg5s6iimg-nixos-system-ai-25.11.20250830.d7600c7/init nvidia-drm.fbdev=1 root=fstab loglevel=4 lsm=landlock,yama,bpf nvidia-drm.modeset=1 nvidia-drm.fbdev=1 nvidia.NVreg_PreserveVideoMemoryAllocations=1 nvidia.NVreg_OpenRmEnableUnsupportedGpus=1

# 查看上述启动项中 uki efi 文件的内容
$ nix shell nixpkgs#systemdUkify
$ ukify inspect /boot/EFI/Linux/nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi
.osrel:
  size: 141 bytes
  sha256: e486dea4910eb9262efc47464f533f96093293d37c3d25feb954c098865a4be6
  text:
    ID=lanza
    PRETTY_NAME=NixOS Xantusia 25.11.20250830.d7600c7 (Linux 6.16.4) (Generation 848, 2025-09-01)
    VERSION_ID=Generation 848, 2025-09-01
# 启动内核时使用的内核命令行参数
.cmdline:
  size: 284 bytes
  sha256: 7f94ffed08359eb1d2749176eba57e085113f46208702a8c0251376d734f19ce
  text:
    init=/nix/store/gaj3sp3hrzjhp59bvyxhc8flg5s6iimg-nixos-system-ai-25.11.20250830.d7600c7/init nvidia-drm.fbdev=1 root=fstab loglevel=4 lsm=landlock,yama,bpf nvidia-drm.modeset=1 nvidia-drm.fbdev=1 nvidia.NVreg_PreserveVideoMemoryAllocations=1 nvidia.NVreg_OpenRmEnableUnsupportedGpus=1
# initramfs 内容的引用,实际镜像位于 ESP 的 /EFI/nixos/initrd-*.efi
.initrd:
  size: 81 bytes
  sha256: 26d9b1f52806c48c6287272cb26b8a640b62d55f09149abf3415c76c38e0b56e
# 内核映像(vmlinuz)的引用,实际镜像位于 ESP 的 /EFI/nixos/kernel-*.efi
.linux:
  size: 81 bytes
  sha256: 41ff83e4cae160fb9ce55392943e6d06dbf9f37b710bf719f7fe2c28ec312be5

内核启动后,会探测 CPU、内存、PCI、USB、ACPI 等硬件,加载关键驱动,然后挂载 initramfs 并执行 option 中指定的 init 程序。

观察方法

# 查看内核早期日志
sudo dmesg --level=err,warn,info | less

# 查看本次启动的完整日志
journalctl -b

initramfs 阶段

initramfs(Initial RAM File System)是一个临时的根文件系统,在真正的根文件系统挂载之前提供必要的功能。它在启动阶段被加载到 RAM 中并被挂载为根目录。

initramfs 阶段的主要职责

  1. 硬件检测与驱动加载

    • 检测存储设备(SATA、NVMe、USB 等)
    • 加载必要的存储驱动模块
    • 识别网络设备(如果需要网络启动)
  2. 存储设备准备

    • 解密 LUKS 加密分区
    • 激活 LVM 逻辑卷
    • 处理 RAID 阵列
    • 挂载临时文件系统
  3. 根文件系统挂载

    • 根据内核参数 root= 找到根分区
    • 挂载根文件系统到 /new_root
    • 执行 switch_root 切换到真正的根文件系统
  4. 启动用户空间

    • 执行 /sbin/init(通常是 systemd)
    • 在 NixOS 中,init 程序是 /nix/store 中的一个 Shell 脚本,它首先完成一些必要的初始化工作,之后才启动 systemd.

常见故障与排查

  • 找不到根分区:检查 cat /proc/cmdlineroot= 参数与 blkid 输出是否一致
  • 缺少驱动模块:确保 NixOS 配置包含所需模块:boot.initrd.kernelModules = [ "nvme" "dm_mod" ];
  • LUKS 解密失败:检查密码输入或密钥文件配置
  • LVM 激活失败:确认 LVM 配置和卷组状态

排查步骤

  1. 编辑内核 cmdline,添加 init=/bin/shbreak=mount 进入 initramfs shell
  2. 运行 lsblkblkid 确认设备
  3. 查看 dmesg 中的磁盘或 LVM 错误
  4. 检查 /proc/cmdline 中的启动参数

2. 启动故障排查

启动故障排查流程

flowchart LR
    A[系统无法启动] --> B{能否进入 UEFI/BIOS?}
    B -->|否| C[硬件问题
检查电源、内存、CPU] B -->|是| D{能否看到启动菜单?} D -->|否| E[引导加载器问题
检查 ESP 分区和启动项] D -->|是| F{能否选择启动项?} F -->|否| G[启动项配置错误
检查 bootctl 配置] F -->|是| H{内核能否加载?} H -->|否| I[内核或 initramfs 问题
检查内核参数和驱动] H -->|是| J{能否进入 initramfs?} J -->|否| K[initramfs 问题
检查根分区和文件系统] J -->|是| L{能否挂载根分区?} L -->|否| M[文件系统或加密问题
检查 LUKS 和 LVM] L -->|是| N{systemd 能否启动?} N -->|否| O[用户空间问题
检查 systemd 服务] N -->|是| P[启动成功]

常见启动问题:症状与解决方案

在系统启动过程中,可能会遇到各种问题。以下是按启动阶段分类的常见问题及排查方法:

2.2.1 固件和引导加载器问题

问题症状

  • 系统无法启动,停留在固件界面
  • 显示 “No bootable device” 错误
  • 启动菜单不显示或显示异常

排查步骤

使用 USB 启动盘进入 LiveOS, 进行如下检查:

# 检查 UEFI 设置
efibootmgr -v

# 检查 ESP 分区状态
bootctl status

# 验证启动项配置
bootctl list

2.2.2 内核和 initramfs 问题

问题症状

  • 内核 panic 或无法加载
  • initramfs 阶段卡住
  • 找不到根分区

排查步骤

# 进入 initramfs shell 进行调试
# 在内核参数中添加:init=/bin/sh 或 break=mount

# 检查设备识别
lsblk
blkid

# 查看内核日志
dmesg | grep -i error

# 检查文件系统完整性
fsck /dev/sdX

启动性能优化

2.3.1 启动时间分析

# 使用 systemd-analyze 分析启动时间
systemd-analyze
systemd-analyze blame
systemd-analyze critical-chain

# 生成启动时间报告
systemd-analyze plot > boot-time.svg

这些工具可以帮助你分析系统启动性能:

  • systemd-analyze 显示总启动时间,包括内核和用户空间的启动耗时
  • systemd-analyze blame 按耗时排序显示各服务启动时间,找出最耗时的服务
  • systemd-analyze critical-chain 显示关键路径分析,找出阻塞启动的服务链
  • systemd-analyze plot 生成启动时间图表,可视化各服务的启动顺序和耗时

识别到启动阶段的性能瓶颈后,就能据此优化服务依赖关系,加快启动速度。

2.3.2 启动优化策略

优化启动速度可以从多个层面入手:

硬件层面

使用 SSD 存储是最直接有效的优化方法。固态硬盘的随机读写性能远超机械硬盘,能显著减少文件系统访问延迟。启动时间通常可减少 50-80%,特别是对于大量小文件读取的场景。适用于所有系统,特别是启动时间较长的系统。

内核层面

启用内核并行初始化可以提升启动速度。现代内核支持并行初始化硬件设备,减少串行等待时间。通过内核参数如 initcall_debugacpi=noirq 等可以优化启动流程,减少硬件初始化时间。

服务层面

优化 systemd 服务依赖关系可以减少启动延迟。减少不必要的服务依赖,避免串行启动造成的延迟。使用 systemctl list-dependencies 分析依赖关系,移除不必要的依赖,减少服务启动等待时间, 提升并行启动效率。

启动流程

使用 UKI(统一内核镜像)可以减少启动步骤。将内核、initramfs、cmdline 打包成单个 EFI 文件, 减少启动步骤和文件系统访问。减少文件系统挂载次数,简化启动流程。在 NixOS 中通过boot.loader.systemd-boot.enableboot.loader.efi.canTouchEfiVariables 启用。

3. 系统安全框架:认证、授权与密钥管理

现代 Linux 桌面系统的安全架构由多个相互协作的组件构成,包括 PAM(认证)、PolicyKit(授权)、以及桌面环境提供的密钥管理服务。这些组件共同构建了一个多层次的安全防护体系,既保证了系统的安全性,又提供了良好的用户体验。

NOTE: 注意 PAM 与 PolicyKit 的设计目的都是为普通用户提供权限提升手段。对 root 用户而言,这些框架的限制很少或几乎不存在。如果你希望限制整个系统全局的权限(包括 root 用户), 应该考虑 SELinux/AppArmor 等强制访问控制框架。


3.1 PAM - 可插拔认证模块

PAM(Pluggable Authentication Modules) 是 Linux 的统一认证框架,为系统中的各种程序 (如 loginsudosshdgdm 等)提供标准化的认证接口。借助 PAM,系统管理员可以通过配置文件灵活控制认证策略,而无需修改应用程序本身。它支持多种认证方式(密码、指纹、智能卡、双因子验证等),是现代 Linux 安全体系的核心组件之一。


3.1.1 工作原理与配置结构

PAM 采用模块化设计,将认证流程分为四个阶段。应用程序通过调用相应的 PAM 接口触发这些阶段, 系统根据 /etc/pam.d/ 下的配置文件执行相应的模块(.so 文件)。

(1)配置文件语法

https://linux.die.net/man/5/pam.d

每行的基本格式如下:

<type>  <control>  <module>  [arguments]
  • type:表示阶段类型
  • control:定义该模块的执行策略
  • module:具体的 PAM 模块路径(或名称)
  • arguments:传递给模块的参数
(2)四个认证阶段
阶段类型 调用函数 主要作用
auth pam_authenticate() 验证用户身份,通常会提示用户输出密码或指纹以完成验证
account pam_acct_mgmt() 检查账户状态(过期、锁定等)
password pam_chauthtok() 处理密码修改
session pam_open_session() / pam_close_session() 建立和清理用户会话
(3)控制标志(control)
标志 含义 行为说明
required 必须成功,失败不会立即终止,但最终结果会失败 无论成功失败,都会继续执行后续模块。最终只要有一个 required 失败,整个认证就失败。
requisite 必须成功,失败立即终止并返回失败 失败立即返回,不再执行后续模块。
sufficient 成功则立即通过认证(跳过所有后续模块);失败则继续由后续模块进行认证 若前面没有 required 失败,则成功直接通过;否则失败不影响后续。
optional 可选模块,结果通常被忽略 无论成功失败,对最终结果无直接影响,除非是栈中唯一的模块。
include 包含另一个文件的配置 将指定文件的配置内容包含进来,通常用于复用通用配置(如 system-auth)。
substack 调用子栈 类似 include,但子栈的失败不影响主栈(即子栈只能跳过其自身的后续步骤),除非主栈中另有设置。
(4)常用模块示例
pam_unix.so                 # 基于 /etc/passwd 与 /etc/shadow 的标准密码认证
pam_google_authenticator.so # 双因子认证(TOTP)
pam_fprintd.so              # 指纹认证
pam_ldap.so                 # LDAP 集中式认证
pam_gnome_keyring.so        # GNOME 密钥环集成
pam_limits.so               # 用户资源限制
pam_deny.so                 # 拒绝所有认证请求

3.1.2 执行流程示例

/etc/pam.d/sudo 为例:

#%PAM-1.0
auth       sufficient   pam_rootok.so
auth       sufficient   pam_timestamp.so
auth       required     pam_wheel.so use_uid
auth       required     pam_unix.so nullok try_first_pass

各模块的执行顺序如下:

  1. 执行 pam_rootok.so (sufficient)
    • 检查当前用户是否为 root。
    • 如果成功:PAM 认证流程立即成功,并跳过后续所有 auth 模块。用户直接获得 sudo 权限。
    • 如果失败:继续执行下一个模块。
  2. 执行 pam_timestamp.so (sufficient)
    • 检查是否存在有效的时间戳文件(默认为 5 分钟内)。
    • 如果成功:PAM 认证流程立即成功,并跳过后续所有 auth 模块。用户免密码获得 sudo 权限。
    • 如果失败:继续执行下一个模块。
  3. 执行 pam_wheel.so (required)
    • 检查当前用户是否在 wheel 组(或 sudo 组,取决于配置)中。
    • 无论成功还是失败,都必须继续执行下一个 required 模块。但其结果会被记录下来。
  4. 执行 pam_unix.so (required)
    • 使用 nulloktry_first_pass 参数进行密码验证。
    • nullok:允许空密码账户登录。
    • try_first_pass:尝试使用前面模块(如果有的话)提供的密码。对于 sudo,这通常指之前 sudo 成功时缓存的密码。
    • 如果密码正确:此模块成功。
    • 如果密码错误:此模块失败。
  5. 最终结果判定
    • 在所有 required 模块执行完毕后,PAM 会检查它们的结果。
    • 如果任何一个 required 模块(pam_wheel.sopam_unix.so)失败,整个认证流程失败。
    • 只有当所有 required 模块都成功时,认证才最终成功。

常用模块及其参数说明

  1. pam_unix.so 参数 (用于密码验证) 这是最核心的密码认证模块,常见于 authpassword 类型。
    • nullok:允许空密码账户通过认证。如果不加此参数,空密码账户将无法登录。
    • try_first_pass:在提示用户输入密码前,先尝试使用之前栈中已缓存的密码(例如,由pam_timestamp.sopam_kwallet.so 提供的)。
    • use_authtok强制使用之前栈中已缓存的密码,如果不存在缓存密码,则直接失败。它比try_first_pass 更严格,通常用在修改密码的 password 模块栈中,以确保用户输入的是旧密码。
    • shadow:使用 /etc/shadow 文件进行密码验证(现代系统默认启用)。
  2. pam_timestamp.so 参数 (用于时间戳认证) 常用于 sudo,实现免密码操作。
    • timestamp_timeout=600:设置时间戳的有效期,单位为秒。默认是 300 (5分钟)。
  3. pam_wheel.so 参数 (用于组成员资格检查) 用于限制只有特定组的用户才能使用 susudo
    • use_uid:检查发起请求的原始用户 ID,而不是当前用户 ID(在 sudo 场景下很重要)。
    • group=admins:指定检查的组名,默认是 wheel
  4. pam_gnome_keyring.so 参数 (用于会话管理) 这个模块与 sudo 的认证流程无关,主要用于用户登录时解锁密钥环。
    • auto_start:在会话启动时,如果用户密码与密钥环密码相同,则自动解锁密钥环。
    • 典型应用场景:在 /etc/pam.d/gdm-password/etc/pam.d/loginauthsession 部分。
      # 在 /etc/pam.d/gdm-password 中
      auth       optional    pam_gnome_keyring.so
      session    optional    pam_gnome_keyring.so auto_start

3.1.3 应用程序与 PAM 的交互

程序通过 pam_start() 指定服务名,系统据此加载对应的配置文件。

程序 服务名 配置文件 功能
login "login" /etc/pam.d/login 控制台登录
gdm "gdm" /etc/pam.d/gdm GNOME 登录界面
sudo "sudo" /etc/pam.d/sudo 提权命令
sshd "sshd" /etc/pam.d/sshd SSH 登录
greetd "greetd" /etc/pam.d/greetd 轻量显示管理器

一个典型的调用顺序如下(以 sudo 为例):

pam_start("sudo", user, &conv, &pamh);     // 初始化 PAM
pam_authenticate(pamh, 0);                 // 身份验证
pam_acct_mgmt(pamh, 0);                    // 账户检查
pam_open_session(pamh, 0);                 // 打开会话
// 执行用户命令
pam_close_session(pamh, 0);                // 关闭会话
pam_end(pamh, PAM_SUCCESS);                // 释放资源

如下是一个用户登录流程的 PAM 调用示例:

#include <stdio.h>
#include <stdlib.h>
#include <security/pam_appl.h>
#include <security/pam_misc.h>

static void log_result(pam_handle_t *pamh, int ret, const char *step)
{
    if (ret == PAM_SUCCESS) {
        printf("[✓] %s 成功\n", step);
    } else {
        fprintf(stderr, "[✗] %s 失败: %s(返回码 %d)\n",
                step, pam_strerror(pamh, ret), ret);
    }
}

int main(int argc, char *argv[])
{
    pam_handle_t *pamh = NULL;
    struct pam_conv conv = { misc_conv, NULL };
    const char *user;
    int ret;
    if (argc != 2) {
        fprintf(stderr, "用法: %s 用户名\n", argv[0]);
        return 1;
    }
    user = argv[1];
    /* 1. 初始化 */
    ret = pam_start("login", user, &conv, &pamh);
    if (ret != PAM_SUCCESS) {
        log_result(pamh, ret, "pam_start");
        return 1;
    }
    /* 2. 认证 */
    ret = pam_authenticate(pamh, 0);
    log_result(pamh, ret, "pam_authenticate");
    if (ret != PAM_SUCCESS) {
        pam_end(pamh, ret);
        return 1;
    }
    /* 3. 帐户检查 */
    ret = pam_acct_mgmt(pamh, 0);
    log_result(pamh, ret, "pam_acct_mgmt");
    if (ret != PAM_SUCCESS) {
        pam_end(pamh, ret);
        return 1;
    }
    /* 4. 打开会话 */
    ret = pam_open_session(pamh, 0);
    log_result(pamh, ret, "pam_open_session");
    if (ret != PAM_SUCCESS) {
        /* 常见原因提示 */
        fprintf(stderr,
                "\n提示:\n"
                "  1. 若您以普通用户运行,失败通常是权限不足(写 /var/run/utmp 等)。\n"
                "  2. 以 root 再次运行即可验证会话模块能否通过:sudo %s %s\n",
                argv[0], user);
        pam_end(pamh, ret);
        return 1;
    }
    printf("\n全部 PAM 阶段通过!\n");
    /* 5. 关闭会话并清理 */
    pam_close_session(pamh, 0);
    pam_end(pamh, PAM_SUCCESS);
    return 0;
}

将上述配置保存为 pam_test.c, 再创建一个 shell.nix 内容如下:

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  buildInputs = with pkgs; [
    pam
    gcc
  ];
}

最后编译运行:

# 进入引入了 pam 链接库的环境
nix-shell
# 编译
gcc pam_test.c -o pam_test -lpam -lpam_misc
# 测试
./pam_test ryan

3.1.4 模块间的数据传递

PAM 模块可通过 pam_set_data()pam_get_data() 共享状态。例如:

pam_set_data(pamh, "authenticated", "true", NULL);
const char *ok;
pam_get_data(pamh, "authenticated", (const void **)&ok);

这使多个模块在同一认证过程中共享信息。


3.1.5 调试与故障排查

PAM 的问题通常来源于配置错误或模块加载失败,可按以下思路排查:

(1)测试与验证
nix shell nixpkgs#pamtester
# 模拟特定服务的认证流程
pamtester sudo $USER authenticate
pamtester login $USER open_session
(2)检查模块与依赖
# 验证模块存在与架构匹配
ls /run/current-system/sw/lib/security/pam_unix.so
ldd /run/current-system/sw/lib/security/pam_unix.so
(3)查看系统日志
journalctl -b | grep -i pam
(4)跟踪调用行为
strace -f -e trace=openat,read,write -o sudo_trace.log sudo true
grep pam sudo_trace.log
(5)常见问题
问题 可能原因
模块加载失败 模块路径错误或权限不足
认证成功但无法建立会话 会话模块执行失败(如无法写入 /var/run/utmp
GNOME Keyring 不自动解锁 pam_gnome_keyring.so 未启用或未配置 auto_start
PAM 配置无效 程序服务名与配置文件不匹配,默认使用 other

3.2 PolicyKit - 细粒度的系统权限管理

PolicyKit(现称 polkit)是一个用于控制系统级权限的框架,它提供了一种比传统 Unix 权限更细粒度的授权机制。在现代 Linux 桌面系统中,PolicyKit 允许非特权用户执行某些需要特权的系统操作 (如关机、重启、挂载设备、修改系统时间等),而无需获取完整的 root 权限。

3.2.1 PolicyKit 的核心概念

配置文件路径

  • /etc/polkit-1/:NixOS 声明式配置中定义的自定义规则(优先级最高)
  • /run/current-system/sw/share/polkit-1/(NixOS)或 /usr/share/polkit-1/(传统发行版):软件包提供的默认规则

上述文件夹中又包含两类配置:

  • 动作(Actions)
    • 定义在配置文件夹的 actions 目录中的 XML 文件(如 /etc/polkit-1/actions/),描述可授权的操作。每个动作都有唯一的标识符,如 org.freedesktop.login1.power-off 表示关机操作。
  • 规则(Rules)
    • JavaScript 文件,定义授权决策逻辑,位于上述配置文件夹的 rules.d/ 目录中(如/etc/polkit-1/rules.d/)。规则决定了在特定条件下是否授权某个操作。在 NixOS 中,推荐使用声明式配置而非直接修改 /etc 目录。

身份认证代理(Authentication Agents):桌面环境提供的图形界面组件,用于在用户需要身份验证时弹出认证对话框。例如,当普通用户尝试关机时,认证代理会提示输入管理员密码。

举例来说,我使用的是 Niri 窗口管理器,它的 Nix Flake 启用了 pokit-kde-agent-1 作为其 Authentication Agent, 配置参见sodiboo/niri-flake.

3.2.2 PolicyKit 的工作原理

当应用程序请求执行需要特权的操作时,系统服务会询问 PolicyKit 是否授权。PolicyKit 的评估过程如下:

  1. 身份识别:确定请求者的身份(用户、组、会话等)
  2. 规则匹配:检查是否有适用的规则文件
  3. 权限评估:根据规则返回以下结果之一:
    • yes:直接允许,无需认证
    • no:直接拒绝
    • auth_self:需要用户自己认证(输入当前用户密码)
    • auth_admin:需要管理员认证(输入 root 密码)
    • auth_self_keep/auth_admin_keep:认证后在一段时间内保持授权

3.2.3 PolicyKit 的配置示例

在传统的 Linux 发行版中,管理员可以通过创建自定义规则来修改默认行为。例如,允许 wheel 组的用户无需密码即可关机:

// /etc/polkit-1/rules.d/10-shutdown.rules
polkit.addRule(function (action, subject) {
  if (action.id == "org.freedesktop.login1.power-off" && subject.isInGroup("wheel")) {
    return polkit.Result.YES
  }
})

NixOS 中的配置方法:在 NixOS 中,推荐使用声明式配置而非直接修改 /etc 目录。可以通过security.polkit 配置项来管理 PolicyKit 规则:

# configuration.nix
{
  security.polkit.enable = true;

  # 添加自定义规则
  security.polkit.extraConfig = ''
    polkit.addRule(function(action, subject) {
      if (action.id == "org.freedesktop.login1.power-off" &&
          subject.isInGroup("wheel")) {
        return polkit.Result.YES;
      }
    });
  '';
}

3.2.4 PolicyKit 与 D-Bus 的集成

PolicyKit 与 D-Bus 深度集成,为 D-Bus 服务提供动态授权机制。许多系统服务(如 systemd、NetworkManager、udisks 等)都使用 PolicyKit 来控制对其 D-Bus 接口的访问。当客户端通过 D-Bus 调用需要特权的方法时,服务会调用 PolicyKit 进行授权检查。

PolicyKit 调试主要涉及服务状态检查、权限测试和规则验证。常用的调试方法包括:

  • 服务状态检查:验证 PolicyKit 守护进程的运行状态
  • 权限测试:使用 pkcheck 工具测试特定操作的授权情况
  • 日志分析:查看 PolicyKit 的授权决策日志
  • 规则验证:检查当前生效的 PolicyKit 规则配置

具体的调试命令请参考 3.5.3 故障排查 章节。

3.3 桌面密钥管理

现代 Linux 桌面环境提供了统一的密钥管理服务,用于安全存储用户的密码、证书、密钥等敏感信息。

GNOME Keyring 和 KDE Wallet 分别是 GNOME 和 KDE 桌面环境的密钥管理解决方案,它们通过加密存储和自动解锁机制,为用户提供了便捷而安全的密码管理体验。

GNOME Keyring 和 KDE Wallet 都实现了标准的Secrets API, 可以根据需要任选一个使用。不过据我观察大部分窗口管理器的用户都是用的 GNOME Keyring.

3.3.1 密钥管理系统架构

GNOME Keyring 架构

  • 密钥环(Keyring):加密的存储容器,每个密钥环有独立的密码
  • 密钥环守护进程(gnome-keyring-daemon):管理密钥环的生命周期和访问控制
  • API:Gnome 原生支持 org.freedesktop.secrets DBus API, 目前流行的 secrets 客户端库 libsecret 也是 gnome 开发的。
  • PAM 集成:通过 pam_gnome_keyring.so 实现登录时自动解锁

KDE Wallet 架构

  • KWalletManager:图形界面管理工具
  • kwalletd:钱包守护进程
  • API:KDE Wallet 从 5.97.0 (2022 年 8 月)开始支持org.freedesktop.secrets DBus API, 因此可以直接通过 libsecret 往 KDE Wallet 中存取 passwords 等 secret.
  • PAM 集成:通过 pam_kwallet.so 实现自动解锁

核心组件路径

# GNOME Keyring 组件(NixOS 中位于 nix store)
/run/current-system/sw/bin/gnome-keyring-daemon
/run/current-system/sw/lib/libsecret-1.so
/run/current-system/sw/lib/security/pam_gnome_keyring.so

# KDE Wallet 组件(NixOS 中位于 nix store)
/run/current-system/sw/bin/kwalletd5
/run/current-system/sw/bin/kwalletmanager5
/run/current-system/sw/lib/security/pam_kwallet.so

# 配置文件位置
~/.local/share/keyrings/     # GNOME 密钥环存储目录
~/.local/share/kwalletd/     # KDE 钱包文件存储目录
~/.config/kwalletrc          # KDE 钱包配置文件

3.3.2 密钥环类型与用途

密钥环类型 用途 解锁时机
login 登录密钥环,存储用户密码 用户登录时自动解锁
default 默认密钥环,存储应用密码 首次访问时解锁
session 会话密钥环,临时存储 会话开始时创建
crypto 加密密钥环,存储证书和私钥 按需解锁

3.3.3 钱包创建与管理

图形界面管理

# GNOME 密钥环管理器
seahorse

# KDE 钱包管理器
kwalletmanager5

通过图形界面可以:

  • 创建新的密钥环/钱包
  • 设置密码和加密算法
  • 管理存储的密码和证书
  • 配置自动解锁策略
  • 备份和恢复密钥环

基本命令行操作

# 使用 secret-tool 管理 GNOME Keyring
secret-tool store --label="My Password" application myapp
secret-tool lookup application myapp

# 使用 kwallet-query 管理 KDE Wallet
kwallet-query --write password "MyApp" "username" "password"
kwallet-query --read password "MyApp" "username"

3.3.4 应用程序集成

常见应用程序集成

VSCode

  • 自动集成系统密钥管理服务
  • 存储 Git 凭据、扩展设置等敏感信息
  • 通过 git credential.helper 配置自动使用

GitHub CLI

# 配置 GitHub CLI 使用系统密钥管理
gh auth login --web
# 凭据会自动存储到系统密钥环中

浏览器集成

  • Firefox、Chrome 等现代浏览器支持系统密钥管理
  • 网站密码自动保存到密钥环/钱包中
  • 跨设备同步(如果启用)

API 集成示例

3.3.5 配置与优化

NixOS 配置示例

# configuration.nix
# 启用 GNOME Keyring
services.gnome.gnome-keyring.enable = true;
# GNOME Keyring GUI 客户端
programs.seahorse.enable = true;
# 启用 PAM 集成
security.pam.services.login.enableGnomeKeyring = true;

3.4 安全故障排查

3.4.1 认证问题排查

常见认证失败场景

  1. 用户无法登录

    • 检查 PAM 配置是否正确
    • 查看认证日志中的错误信息
    • 验证用户账户状态和密码
  2. sudo 权限问题

    • 确认用户在正确的用户组中
    • 检查 sudoers 配置
    • 验证 PAM 认证流程
  3. SSH 登录失败

    • 检查 SSH 服务状态
    • 查看 SSH 认证日志
    • 验证网络连接和防火墙设置

3.4.2 权限管理问题排查

PolicyKit 权限问题

  • 无法关机/重启:检查 PolicyKit 规则配置和用户组权限
  • 无法挂载设备:检查 udisks2 服务和 PolicyKit 集成
  • 无法修改系统时间:检查时间同步服务权限和用户组设置

3.4.3 密钥管理问题排查

GNOME Keyring 问题

  • 检查密钥环守护进程是否正常运行
  • 验证 PAM 集成是否正确配置
  • 查看密钥环状态和自动解锁设置

KDE Wallet 问题

  • 检查钱包守护进程状态
  • 验证钱包配置和访问权限
  • 测试钱包的读写功能

具体的调试命令和排查步骤请参考 3.5.3 故障排查 章节。

3.5 安全组件集成与最佳实践

3.5.1 组件协作流程

现代 Linux 桌面的安全组件协作流程:

  1. 用户登录:PAM 验证用户身份
  2. 密钥环解锁:PAM 模块自动解锁用户密钥环/钱包
  3. 应用启动:应用程序通过 libsecret/KWallet API 访问存储的密码
  4. 特权操作:PolicyKit 控制需要特权的系统操作
  5. 会话结束:密钥环/钱包自动锁定

3.5.2 安全最佳实践

密钥管理

  • 使用强密码保护密钥环/钱包
  • 定期备份密钥环文件
  • 避免在脚本中硬编码密码
  • 使用应用程序专用的密钥环

认证配置

# 启用双因子认证
auth required pam_google_authenticator.so
auth required pam_unix.so

# 配置密码策略
password required pam_cracklib.so retry=3 minlen=8 difok=3
password required pam_unix.so use_authtok

权限管理

// PolicyKit 规则示例:限制特定操作
polkit.addRule(function (action, subject) {
  if (action.id == "org.freedesktop.login1.power-off" && subject.user == "guest") {
    return polkit.Result.NO
  }
})

3.5.3 故障排查

PAM 认证调试

nix shell nixpkgs#pamtester

# 测试 PAM 配置
pamtester login $USER authenticate
pamtester sudo $USER authenticate

# 查看 PAM 配置
cat /etc/pam.d/login
cat /etc/pam.d/greetd
cat /etc/pam.d/sudo

# 检查 PAM 模块
ldd /run/current-system/sw/lib/security/pam_unix.so
ldd /run/current-system/sw/lib/security/pam_gnome_keyring.so

# 查看认证日志
journalctl -t login -f
journalctl -t greetd -f
journalctl -t sshd -f
journalctl -t sudo

# 验证程序与配置的对应关系
strace -e trace=pam_start login 2>&1 | grep pam_start
strace -e trace=openat login 2>&1 | grep pam.d

PolicyKit 权限调试

# 检查 PolicyKit 服务状态
systemctl status polkit

# 测试特定权限
pkcheck --action-id org.freedesktop.login1.power-off --process $$ --allow-user-interaction

# 查看 PolicyKit 日志
journalctl -u polkit -f

# 查看 PolicyKit 动作定义
ls -la /run/current-system/sw/share/polkit-1/actions/

# 查看当前生效的 PolicyKit 规则
ls -la /etc/polkit-1/rules.d/

密钥管理调试

# GNOME Keyring 检查
ps aux | grep gnome-keyring
seahorse  # GNOME Keyring GUI

# KDE Wallet 检查
ps aux | grep kwalletd
kwalletmanager5  # KDE Wallet GUI
kwallet-query kdewallet --list-entries

# 系统日志检查
sudo journalctl -u systemd-logind

调试技巧

  • 使用 strace 跟踪应用程序的密钥访问
  • 通过 journalctl 查看认证和授权日志
  • 使用 pamtester 测试 PAM 配置
  • 通过 pkcheck 测试 PolicyKit 权限

通过理解这些安全组件的协作机制,用户可以更好地配置和管理 Linux 桌面的安全策略,在保证安全性的同时提供良好的用户体验。

总结

从 UEFI 到 systemd,从 PAM 到 PolicyKit,本文详细介绍了 Linux 桌面系统启动与安全框架的核心组件。

下一篇文章将深入探讨 systemd 全家桶与服务管理,包括 D-Bus 系统总线、日志系统和设备管理等核心功能,这些组件为桌面环境提供了强大的基础设施支持。

快速参考

常用启动排查命令

# 启动时间分析
systemd-analyze
systemd-analyze blame
systemd-analyze critical-chain

# 引导加载器检查
bootctl status
bootctl list
efibootmgr -v

# 内核和硬件信息
dmesg | grep -i error
lspci -k
lsusb
lsblk

# 进入救援模式
# 在内核参数中添加:init=/bin/sh 或 break=mount

常用安全排查命令

安全相关的调试命令请参考 3.5.3 故障排查 章节,该章节提供了完整的 PAM、PolicyKit 和密钥管理调试命令。

重要配置文件位置

# 启动相关
/boot/loader/loader.conf          # systemd-boot 全局配置
/boot/EFI/Linux/                  # UKI 镜像位置
/etc/pam.d/                       # PAM 配置文件
/etc/polkit-1/                    # PolicyKit 配置

# 密钥管理
~/.local/share/keyrings/          # GNOME Keyring 存储
~/.local/share/kwalletd/          # KDE Wallet 存储
~/.config/kwalletrc               # KDE Wallet 配置

🔲 ☆

AMD ROCm 追赶 NVIDIA CUDA:AI 芯片格局将变

AMD ROCm 追赶 NVIDIA CUDA:AI 芯片格局将变

AI 解决方案开发商 Tiny Corp 近日表示,AMD 在软件方面取得重大进步,已大幅缩小与 NVIDIA CUDA 系统的差距,甚至可能在 NVIDIA 出现技术失误时超越其在 AI 市场的主导地位。虽然 NVIDIA 目前在 2025 年第一季度取得 92% GPU 市场占有率,但 AMD 正通过 ROCm 平台快速赶上。

软件差距快速缩小

专注于开发消费端 AI 解决方案的 Tiny Corp 认为,AMD 在软件方面的进步已使其接近 NVIDIA 水平。该公司表示:“就像 Intel 在 CPU 领域一样,如果 NVIDIA 一代产品犯错误,AMD 就能获得大部分市场占有率,并且市场占有率转移比游戏领域更容易。”这观点在当前 AI GPU 竞争激烈背景下显得格外重要。虽然 NVIDIA 凭借强大 CUDA 生态系统长期占据主导地位,但 AMD 正通过 ROCm 平台迅速追赶。

AMD is closer to NVIDIA than most people think, we're working hard on the software gap.

similar to Intel with CPUs, if NVIDIA stumbles for a generation AMD can capture majority market share. this is easier than gaming for market share to shift.

— the tiny corp (@__tinygrad__) August 18, 2025

ROCm 7 带来显著性能提升

AMD 在 6 月“Advancing AI”活动中推出 ROCm 新版本,支持包括 vLLM v1、llm-d、SGLang 在内的多种增强框架,并专注于分布式推理、预填充等优化功能。据报告,ROCm 7 平台能将 AI 推理性能提升达 3.5 倍。AMD ROCm 7 主要关注推理工作负载,带来明显性能提升,特别是在 DeepSeek R1 FP8 吞吐量和增强训练性能方面,甚至声称其性能优于 NVIDIA CUDA。最新发布的 ROCm 6.4.3 版本进一步解决性能问题,修复通信操作中延迟问题。

扩展消费市场支持

AMD 计划在今年稍晚时间在基于 Ryzen 的笔记本电脑和工作站上开放 ROCm 支持,并提供 Linux 和 Windows 全面支持。这意味着 AMD 希望其 ROCm 平台能被更多用户使用,挑战 NVIDIA 在专业和消费市场的主导地位。行业竞争加剧另一例证是,中国 AI 团队最近在全球获奖,成功开发出以工业芯片替代 NVIDIA GPU 的视频生成 AI 模型,显示替代方案正在涌现。

虽然面临挑战,NVIDIA 在 2025 年第一季度市场表现依然强劲,其市场占有率较上季增加 8 个百分点,而 AMD 则下降 7.3 个百分点至 8%。不过随着 AMD ROCm 技术不断成熟,AI GPU 市场竞争格局可能出现变化。

🔲 ☆

2025新主机

Foreword

要换电脑了,老电脑已经八年了,内存老是出问题,动不动开机就卡启动界面,新3A都玩不动了,显卡够用但是CPU不够,小马拉大车,很难受,也是时候换一个新的了

主机

很久之前就被背插种草了,距离第一款背插也都出了2年了,应该可选择性比较大了吧?实际并没有,这种需求还是太过小众了。

这次主要就是冲着极尽简洁的机箱正面,没有多余的线材显示出来,看起来模块化的思路来弄。

主板选型

优先选背插主板,实际市面上能买的很少,可选择性很少,只能凑活

华硕 TUF GAMING B850-BTF WIFI W,能买,1900,有BTF显卡槽,但是没有USB4,音频接口也偏弱

华硕 ROG Crosshair X870E Hero BTF,难买

技嘉 X870 AORUS STEALTH ICE,难买

技嘉 B850 AORUS STEALTH ICE,能买,2450,相对完美一些,但是没有板U套装

技嘉 B650E AORUS STEALTH ICE,能买,2200,只有一个PCIe插槽

微星 B650M APE WIFI,上一代

微星 B650M PROJECT ZERO,难买

微星 B850 GAMING PLUS WIFI PZ,难买

微星 PRO Z890-S WIFI PZ,难买

实际只有3个能选的,每个都有点缺点,选起来很难受。

BTF显卡槽供电是600W,刚刚够5090,虽然看了测评,这个接口抗高温、抗大电流都还行,但是那毕竟是拿来测试的,不是量产版本。

image-20250729172534277

最后选择只能是:技嘉 B850 AORUS STEALTH ICE

CPU选型

9950x3D,多核,但是CCD问题

9900x3D,也有CCD问题

9800x3D,无CCD问题

image-20250729172916459

游戏上差距其实非常小,2-3%,CCD问题可以通过手动或者自动调节来解决,9950x3D的好处是他可以多开,主力CPU0开游戏,后台挂一堆其他东西都是ok的,最后还是选了9950x3D

机箱选型

乔思伯 TK3,539

image-20250713221334377

乔思伯 TK2,789

image-20250713221354439

乔思伯 D300,439

image-20250713221632694

尺寸上,D300更高一些,TK2和3偏矮胖一些,都支持背插,以海景房来说,TK系列更好看一些。TK3是2的青春版。

但是TK3的前面版IO更多一些,同时支持的显卡也更长一些,只是机箱相对软一些。

机箱最后选了TK3

散热选型

360和280冷排,散热效果基本差不多,看能放进去哪个,280少了一个风扇,明显噪音效果会更好一些

420更强,但是机箱不容易适配

image-20250729122523754

联立隐流二代,侧向水管,可以固定隐藏部分水管,水管可以很短,整个面上看起来更清爽一些

二代有两种TL、CL,还有无风扇版本,我有猫扇就不需要他的风扇了

散热选型:联立隐流二代无扇叶版本

内存选型

白色可选也比较少,而且我还要48G大小的,基本就只有这么几个选择了

威刚 D500G 釉白 48GX2 6400 C32

女武神二代RGB-白色 8000 DDR5 48G(24Gx2) I 海力士M-die颗粒CL38

金百达 星刃白灯【8400/C38】24G*2

宏碁掠夺者DDR5冰刃24G*2套装8200MHz 海力士内存

频率越高,越难大容量,正常游戏48G就够用了,如果日后还需要拓展,96就行了。

所以选2个24G高频率的就行,主板只能做到8200,再高也没啥用,剩下位置给拓展用

image-20250729173449918

内存选择:宏碁掠夺者DDR5冰刃24G*2套装8200MHz 海力士内存,主板方有官方验证过

电源选型

航嘉 MVP P1200(白) 1200W白金 ATX3.1

image-20250729173517412

电源选择比较少,直接选航嘉就行了,功率选的比较大为了后续升级使用,也是白色

其他

风扇有多的,倒是不需要

硬盘本身固态就很多,也不需要,移动过来就行了

显卡用之前的3080Ti,移动过来即可

汇总

主板:技嘉 B850 AORUS STEALTH ICE

CPU:AMD 9950x3D

机箱:乔思伯 TK3

散热:联立隐流二代无扇叶版本

内存:宏碁掠夺者DDR5冰刃24G*2套装8200MHz 海力士内存

电源:航嘉 MVP P1200(白) 1200W白金 ATX3.1

显卡:索泰天启3080Ti

风扇:猫头鹰*6

总体算下来价格1w出头

image-20250811015834995

唯一问题,显卡是黑色的,等60系出了弄个白色的或者巧克力色的,再把显卡线给隐藏掉,正面就很简洁了

装机

等待收获

显示器

老显示器是LG34UC88B,34寸,3440*1440,75Hz,主要是刷新有点拉跨,分辨率也有点不够看了

想换一个新的,目标尺寸大概是40寸以内,比34大,IPS的屏幕,曲面带鱼屏21:9,支持HDR,同时分辨率可以达到5K,120Hz就行,最近显示面板有点百花齐放的意思,各种屏幕都有,想要这个标准的还有点不好找。

选型

联想,P40W-20,40寸,5120*2160,75Hz,淘汰

INNOCN,40C1U,40寸,5120*2160,100Hz,IPS,直屏,淘汰

INNOCN,45C1R,44.5寸,5120*1440,120Hz,HDR400,VA,淘汰

飞利浦,40B1U6903CH,40寸,5120*2160,75Hz,淘汰

惠普,Z40C,40寸,5120*2160,75Hz,淘汰

戴尔,U4025QW,40寸,5120*2160,120Hz,IPS Black,HDR600

川升,CS40X,40寸,5120*2160,120Hz,Nano-IPS,HDR700,淘汰

MXHA,M40C,40寸,5120*2160,100Hz,IPS Black,基本和川升一样,淘汰

LGKC,无型号,40寸,5120*2160,120Hz,LG-Black IPS Black,HDR600,淘汰

星华辰,UP40R2,40寸,5120*2160,120Hz,Nano-IPS,HDR600,淘汰

iPlaoe,W40A Max,40寸,5120*2160,120Hz,Nano-IPS,HDR400,淘汰

AOC,AG405UXC,40寸直屏,3440*1440,144Hz,IPS,HDR400,淘汰

LG,40U990A,40寸,5120*2160,120Hz,Nano IPS Black,HDR600

LG,39GX90SA,39寸,3440*1440,240Hz,MLA+OLED,HDR400,淘汰

LG,45GX90SA,45寸,3440*1440,240Hz,MLA+OLED,HDR400,淘汰

LG,42C4/C5,42寸,3840*2160,144Hz,OLED,性价比很高,但是办公、文字、白屏问题很大,由于是电视,启动很慢,启停要用遥控很不方便,淘汰

三星,42S90F,42寸,3840*2160,144Hz,OLED,淘汰

基本全淘汰了,只有LG和戴尔,而且他俩的这个型号基本应该是同一个面板出来的,参数基本是一样的

这两个均价都1w左右,国内没有代理,保修没有保障,感觉不划算,很快就会被打下来

纠结半天,找不到合适的产品,退而求其次,换成32寸的4k显示器,刷新率有120Hz就行,但是面板得要QD OLED或者MLA+OLED

AOC,AG326UD,32寸,165Hz

LG,32GS95UV,32寸,240Hz

三星,S32FG812SC,32寸,240Hz

三星,S32DG802SC,32寸,240Hz

image-20250812003136601

AOC最便宜,也能满足要求,其他的几个就算了,他们这个价格我再加点都能上戴尔的5K了

实测体验确实比之前的LCD显示器强了好多,效果确实不错,但是亮度很明显下降了,然后这个AOC的音响说是2个8w,对比了一下还不如我老LG的5w,发声位置也不太对,双声道感觉都是从中间出来的,左右声道不是很明显,音量开上来感觉也不是很大声,容易听不清

这款AOC的面板已经第三代QD OLED,而其他对比的LG三星都是第四代产品了,第四代面板亮度更高一些,但是也就一些,烧屏控制得更好,彩边也优化了一点,刷新率也上去了,不过没啥用,现在4K是谁也不能真的这么高刷新,只能糊弄一下眼睛。

品牌背调

HKC慧科,65亿注册资金,还是挺有实力的,有自己的面板生产厂,不是其他杂牌能碰瓷的

INNOCN联合创新、泰坦军团是其子品牌,深圳市世纪创新显示电子有限公司,注册资金1亿

LGKC,名字碰瓷,背后是华辰电子(广州)有限公司,注册资金100w,可想而知啥水平了

星华辰,同LGKC,一家的另外一个马甲

iPlaoe,这个名字碰瓷iphone+aoc,广州为铭电子商务有限公司,注册资金100w

川升,深圳市川升科技有限公司,注册资金100w,母公司四川川升科技有限公司,注册资金200w,看了下Boss,没有研发,只有销售,顶多是个组装代工厂,还是很小的那种,软文、买量弄了不少。

MXHA,四川云昊众联科技有限公司,跟川升差不多

这几个没注册资金的小厂,应该都是拿的尾货面板,多少都有一些瑕疵点,然后人工组装以后包装成自己的产品,本质上还是LG、三星等厂商的面板,和自己DIY差距不大,主要是给你做好了结构适配,弄出来了显示器固件,各种参数HDR、FPS、拖影等等一些认证是完全没有的,属于是有啥用啥,不挑的人才能用,售后是没保障的,3年售后,不到1年一个产品就没货了,后续只能通过换货或者其他渠道进行处理。

IPS区分

Nano IPS,色彩比普通IPS更好,但是对比度不高

Nano IPS Black,在Nano IPS基础上提高了对比度,解决了漏光问题

IPS Black ,在IPS基础上提高了对比度,解决了漏光问题

Fast IPS,响应速度最快

LED区分

QLED,可以认为是MiniLED+WOLED的合体,但是这种文字边缘泛彩色,没办法解决

  • ULED、Neo QLED 量子点LED都可以认为是一种QLED
  • 早年三星的QLED不是OLED,属于是单纯给LED加了一个背光叫做了QLED,但是这种技术不如现在的QD OLED,后期三星又把QLED捡起来了,但是此时指的是类似QD OLED的技术了,这个名词就被三星弄乱了
  • 这个一整套技术的峰值亮度上不去,这里似乎有一些不可能三角,要高亮度、对比度就没色准,要色准、高亮度就没刷新率,三者不能兼得,只能选其二

QD OLED,TCL的商标,类似于QLED技术,QDEL

WOLED,多个白色发光,本质上还是OLED,但是总体看色彩发白,饱和度降低

Mini LED,分区背光,分区数量有限,经常能看到各种分区数不够造成的黑泛白

Micro LED,可以说Mini的进阶版,分区数超高,但是价格逆天

MLA OLED,LG的技术,对标QLED

目前来说QD OLED应该算是现在能买到最好的了,但是OLED这种屏幕对于办公、大量文字白屏显示略有一点不够好,字体容易有彩边,看着容易眼花。

分辨率/接口

image-20250812230327713

DP 1.4a,DP 1.4b

DP接口为了提高带宽,推出了DSC的压缩技术,带了DSC的就是b,没带DSC的就是a

但总的来说1.4支持到4K 144Hz 10bit

HDMI则有点尴尬了,比较老的HDMI完全拉不动4K 120以上,还好我这里是DP1.4a和HDMI2.1,165Hz支持,如果买三星或者LG的240Hz,还有点点危险。

DP好在可以双线同时连接来扩展带宽,也就不怕对未来的扩展性不够了。

Summary

后续等到Zen6出来以后,考虑再把9950x3D换掉,下一代GPU出来以后再把3080Ti换掉,等5K普及了再把这个显示器换了,还得好久。

实测3080Ti还是能勉强4K 60Hz的,就是画面一激烈掉的有点厉害

Quote

https://www.chiphell.com/forum.php?mod=viewthread&tid=2710927

https://bbs.nga.cn/read.php?tid=43522406

https://zhuanlan.zhihu.com/p/1897378386336256709

🔲 ☆

AMD Zen 3 的 BTB 结构分析

AMD Zen 3 的 BTB 结构分析

背景

在之前,我们分析了 AMD Zen 1AMD Zen 2 的 BTB,接下来分析它的再下一代微架构:2020 年发布的 AMD Zen 3 的 BTB,看看 AMD 的 Zen 系列的 BTB 是如何演进的。

官方信息

AMD 在 Software Optimization Guide for AMD EPYC™ 7003 Processors (Publication No. 56665) 中有如下的表述:

The branch target buffer (BTB) is a two-level structure accessed using the fetch address of the previous fetch block.

Zen 3 的 BTB 有两级,相比 Zen 1 和 Zen 2 少了一级。BTB 是用之前 fetch block 的地址去查询,而不再是当前 fetch block 的地址。用当前 fetch block 的地址查询 BTB 很好理解,要寻找某个地址开始的第一个分支,就用这个地址去查询 BTB,Zen 1 和 Zen 2 都是如此;用之前 fetch block 的地址,则是用更早的信息,去获取当前 fetch block 的信息,例如:

entrypoint1:  jmp entrypoint2  entrypoint2:  # what's the first branch after entrypoint2? 

在查询从 entrypoint2 开始的第一条分支指令的时候,如果使用当前 fetch block,就是用 entrypoint2 的地址去查询,那就必须等到前面 jmp entrypoint2 指令的目的地址被计算得出;如果使用之前 fetch block,就是用 entrypoint1 的地址去查询,不用等到 jmp entrypoint2 指令的目的地址被计算得出。因此,如果用之前 fetch block,可以更早地进行 BTB 的访问,从而减少 BTB 的延迟,或者在相同延迟下获得更大的容量。但是,代价是:

  • 从 entrypoint1 跳转到的 fetch block 可能有多个,例如最后一条是间接分支指令,那就需要找到正确的分支的信息
  • 可能会从不同的地址跳转到 entrypoint2 这个 fetch block,因此它的信息可能会保存多份

Each BTB entry can hold up to two branches if the last bytes of the branches reside in the same 64-byte aligned cache line and the first branch is a conditional branch.

Zen 3 的 BTB entry 有一定的压缩能力,一个 entry 最多保存两条分支,前提是两条分支在同一个 64B 缓存行中,并且第一条分支是条件分支。这样,如果第二条分支是无条件分支,分支预测的时候,可以根据第一条分支的方向预测的结果,决定要用哪条分支的目的地址作为下一个 fetch block 的地址。虽然有压缩能力,但是没有提到单个周期预测两条分支,所以只是扩大了等效 BTB 容量。和 Zen 1、Zen 2 一样。

L1BTB has 1024 entries and predicts with zero bubbles for conditional and unconditional direct branches, and one cycle for calls, returns and indirect branches.

Zen 3 的第一级 BTB 可以保存 1024 个 entry,但不确定这个 entry 是否可以保存两条分支,也不确定这个 entry 数量代表了实际的 entry 数量还是分支数量,后续会做实验证实;针对条件和无条件直接分支的预测不产生气泡,意味着它的延迟是一个周期。相比 Zen 2 容量翻倍,且延迟降低一个周期,猜测和使用 previous fetch block 有关。

L2BTB has 6656 entries and creates three bubbles if its prediction differs from L1BTB.

Zen 3 的第二级 BTB 可以保存 6656 个 entry,但不确定这个 entry 是否可以保存两条分支,也不确定这个 entry 数量代表了实际的 entry 数量还是分支数量,后续会做实验证实;预测会产生三个气泡,意味着它的延迟是四个周期。

简单整理一下官方信息,大概有两级 BTB:

  • 1024-entry L1 BTB, 1 cycle latency
  • 6656-entry L2 BTB, 4 cycle latency

相比 Zen 1 和 Zen 2 有比较大的不同:去掉了原来很小的 L0 BTB,扩大了 L1 BTB,同时延迟缩短了一个周期;虽然 L2 BTB 有所缩小,但是延迟也变短了一个周期。

下面结合微架构测试,进一步研究它的内部结构。

微架构测试

在之前的博客里,我们已经测试了各种处理器的 BTB,在这里也是一样的:按照一定的 stride 分布无条件直接分支,构成一个链条,然后测量 CPI。

考虑到 Zen 3 的 BTB 可能出现一个 entry 保存两条分支的情况,并且还对分支的类型有要求,因此下面的测试都会进行四组,分别对应四种分支模式:

  • uncond:所有分支都是无条件分支:uncond, uncond, uncond, uncond, ...
  • cond:所有分支都是条件分支:cond, cond, cond, cond, ...
  • mix (uncond + cond):条件分支和无条件分支轮流出现,但 uncond 在先:uncond, cond, uncond, cond, ...
  • mix (cond + uncond):条件分支和无条件分支轮流出现,但 cond 在先:cond, uncond, cond, uncond, ...

虽然 Zen 3 使用 previous fetch block 来访问 BTB,但在这几种分支模式中,使用 previous fetch block 还是访问 current fetch block,结果都是唯一的,所以并不会对结果带来影响。

stride=4B

首先是 stride=4B 的情况:

可以看到,图像上出现了三个比较显著的拐点:

  • 第一个拐点是 4 条分支,CPI=1,对应 L1 BTB,没有达到完整容量,可能是因为分支太过密集
  • 第二个拐点是 2048 条分支,CPI=3.6;第三个拐点是 4096 条分支,CPI=4/4.2/4.4

Zen 3 在 stride=4B 的情况下 L1 BTB 表现比较一般,应该是牺牲了高密度分支下的性能;而主要命中的是 L2 BTB,在不同的分支模式下,测出来差不多的结果。为了验证这一点,统计了如下的性能计数器(来源:Processor Programming Reference (PPR) for AMD Family 19h Model 21h, Revision B0 Processors):

PMCx08B [L2 Branch Prediction Overrides Existing Prediction (speculative)] (Core::X86::Pmc::Core::BpL2BTBCorrect)

它代表了 L2 BTB 提供预测(准确地说,L2 BTB 提供了预测且和 L1 BTB 提供的预测结果不同,覆盖了 L1 BTB 的预测结果)的次数,当分支数不大于 4 的时候,这个计数器的值约等于零;此后快速上升,说明后续都是 L2 BTB 在提供预测。

更进一步观察,发现 2048 到 4096 的 CPI 上升,来自于 L1 BTB 完全失效:2048 条分支时,L1 BTB 还能提供约 10% 的预测,所以 CPI=0.1*1+0.9*4=3.7,但到 4096 条分支的时候,完全由 L2 BTB 提供分支,此时 CPI=4。

超过 4096 以后,则 L2 BTB 也开始缺失,出现了译码时才能发现的分支,如果这是一条 uncond 分支,那么会在译码时回滚,这一点可以通过如下性能计数器的提升来证明(来源:Processor Programming Reference (PPR) for AMD Family 19h Model 21h, Revision B0 Processors):

PMCx091 [Decode Redirects] (Core::X86::Pmc::Core::BpDeReDirect): The number of times the instruction decoder overrides the predicted target.

但在 L2 BTB 缺失后,如果译码器发现了 cond 分支,会把它预测为不跳转,所以要等到执行才能发现分支预测错误。这就导致了 cond 模式下 L2 BTB 溢出时 CPI=16,而 uncond 模式下 L2 BTB 溢出时 CPI=12,提前在译码阶段发现了 uncond 分支并纠正。

但译码器的纠正能力不是万能的:假如它首先发现了一条 cond 分支,在它其后又发现了一条 uncond 分支,它会用 uncond 分支去纠正,但实际上前面的 cond 分支会跳转,所以此时译码器纠正也无法提升性能,即使 BpDeReDirect 计数器的值看起来很大。

stride=8B

接下来观察 stride=8B 的情况:

  • 第一个台阶在所有分支模式下都是 1024 个分支,CPI=1,对应 1024-entry 的 L1 BTB
  • 第二个台阶不太明显,但是在 4096 附近在所有分支模式下都是一个拐点,CPI=4,对应 L2 BTB;在 mix (uncond + cond) 模式下,超过 4096 分支后 CPI 缓慢上升,到 6144 条分支 CPI=4.25,到 6656 条分支 CPI=4.85,之后 CPI 快速上升;在 mix (cond + uncond) 模式下,到 5888 条分支 CPI=5。

L2 BTB 的容量不太确定,超过 4096 后需要一个 entry 保存两条分支才能获得更多容量,但也带来了一定的额外的延迟。与此同时 4096 也对应了 32KB ICache 的容量,这会对分析带来干扰。

从 BpDeReDirect 计数器来看,uncond 分支模式下,当分支数量超过 4096 后,L2 BTB 从 4096 时无缺失,之后缺失快速提升,说明此时 L2 BTB 容量确实是 4096。在 mix (cond + uncond) 模式下,分支数超过 4096 时,BpDeReDirect 计数器略微上升,直到 6144 条分支后才有明显的上升。

stride=16B

继续观察 stride=16B 的情况:

相比 stride=8B,L1 BTB 的行为没有变化。4096 对应的 CPI 有所下降,从 BpL2BTBCorrect 性能计数器可以发现是 L1 BTB 起了一定的作用。在 mix (cond + uncond) 模式下,直到 5632 条分支还维持了 CPI=3.25,之后 CPI 缓慢上升,到 6656 条分支时 CPI=3.75,到 6912 条分支时 CPI=4。

CPI=3.25 可能是来自于 1 和 4 的加权平均:25% 的时候是 1 周期,75% 的时候是 4 周期,平均下来就是 1*0.25+4*0.75=3.25。这意味着 L1 BTB 还要保持 25% 的命中率。观察 BpL2BTBCorrect 性能计数器,发现它的取值等于 75% 的分支执行次数,意味着 L1 BTB 确实提供了 25% 的预测,L2 BTB 提供了剩下 75% 的预测。这一点是挺有意思的,意味着 L1 BTB 可能采用了一些对这种循环访问模式友好的替换策略:朴素的 LRU(或类 LRU)替换策略会导致 L1 BTB 出现 100% 缺失。

stride=32B

继续观察 stride=32B 的情况:

相比 stride=16B,L1 BTB 的行为没有变化,但是出现了一些性能波动。所有分支模式下,L2 BTB 的拐点都出现在 5120,但性能波动比较大,mix (cond + uncond) 模式下的 CPI 达到了 4.6。通过 BpDeReDirect 性能计数器的变化,可以确认这个拐点确实是来自于 L2 BTB 的缺失。

前面提到,译码器的纠正能力可能会给出错误的答案,在 stride=32B 时,就会出现一个很有意思的现象:

  • 超出 L2 BTB 容量后,mix (uncond + cond) 模式下 BpDeReDirect 占分支数量的 50%
  • 超出 L2 BTB 容量后,mix (cond + uncond) 模式下 BpDeReDirect 占分支数量的接近 100%

解释起来也并不复杂:stride=32B 的情况下,一个 64B cacheline 只有两条分支,那么:

  • mix (uncond + cond) 模式下,第一条分支是 uncond,译码器会发现并 redirect;第二条分支是 cond,译码器会无视它,不进行 redirect;所以最后是 50% 的 redirect 比例
  • mix (cond + uncond) 模式下,第一条分支是 cond,译码器会看到后面的 uncond 分支并 redirect;第二条分支是 uncond,译码器会发现并 redirect;所以最后是接近 100% 的 redirect 比例

顺带一提,uncond 模式下的 BpDeReDirect 占分支数量的接近 100%,cond 模式下的 BpDeReDirect 占分支数量的 0%,都是符合预期的。

stride=64B

继续观察 stride=64B 的情况:

相比 stride=32B,L1 BTB 的容量减半,达到了 512。之后出现了比较明显的性能波动,但四种分支模式下,拐点依然都是出现在 5120 条分支的位置。通过 BpDeReDirect 性能计数器的变化,可以确认这个拐点确实是来自于 L2 BTB 的缺失。由于 uncond 模式下,BTB sharing 不会工作,意味着 L2 BTB 至少有 5120 个 entry。

stride=128B

继续观察 stride=128B 的情况:

相比 stride=64B,L1 BTB 的容量进一步减小,达到了 256;L2 BTB 的性能依然波动剧烈,但四种分支模式下,拐点依然都是出现在 5120 条分支的位置。

考虑到 5120 这个拐点频繁出现,认为 L2 BTB 在不考虑 BTB entry sharing 的情况下,实际容量应该是 5120。那么剩下的 1536 个分支就是来自于压缩。

小结

测试到这里就差不多了,更大的 stride 得到的也是类似的结果,总结一下前面的发现:

  • L1 BTB 是 1024-entry,1 cycle latency,容量随着 stride 变化,大概率是 PC[n:5] 这一段被用于 index,使得 stride=64B 开始容量不断减半
  • L2 BTB 是 5120-entry,4 cycle latency;其中有 1536 个 entry 最多保存两条分支,前提是这两条分支在同一个 cacheline 当中,并且第一条是 cond,第二条是 uncond

Zen 1 到 Zen 3 的 BTB 的对比

下面是对比表格:

uArch AMD Zen 1 AMD Zen 2 AMD Zen 3
L0 BTB size 4+4 branches 8+8 branches N/A
L0 BTB latency 1 cycle 1 cycle N/A
L1 BTB size 256 branches 512 branches 1024 branches
L1 BTB latency 2 cycles 2 cycles 1 cycle
L2 BTB size w/o sharing 2K branches 4K branches 5K branches
L2 BTB size w/ sharing 4K branches 7K branches 6.5K branches
L2 BTB latency 5 cycles 5 cycles 4 cycles
Technology Node 14nm 7nm 7nm
Release Year 2017 2019 2020

Zen 3 在 Zen 2 的基础上,没有更换制程,而是通过 previous fetch block 的方式,减少 L1 BTB 的延迟到 1 cycle,顺带去掉了 L0 BTB。L2 BTB 的大小进行了调整,减少了共享的部分,而增加了不限制分支类型的 BTB entry 数量,同时减少了一个周期的延迟,不确定这个延迟是单纯通过优化容量实现的,还是说也依赖了 previous fetch block 的方法来减少周期,更倾向于是后者,因为 L1 和 L2 BTB 都减少了一个周期的延迟。

如果按照 Intel 的 tick-tock 说法,那么 Zen 2 相比 Zen 1 是一次 tick,更换制程,微架构上做少量改动;Zen 3 相比 Zen 2 是一次 tock,不更换制程,但是在微架构上做较多改动。Zen 4 是 2022 年发布的,使用的是 5nm 制程;Zen 5 是 2024 年发布的,使用的是 4nm 制程。总结一下规律,AMD 会花费两年的时间来升级制程,并且实际上,Zen 4 和 Zen 5 不仅更新了制程,还在前端微架构上有较大的改动。

AMD Zen 3 和 ARM Neoverse V1 的 BTB 的对比

AMD Zen 3 和 ARM Neoverse V1 都是在 2020 发布的处理器,下面对它们进行一个对比:

uArch AMD Zen 3 ARM Neoverse V1
L1/Nano BTB size 1024 branches 48*2 branches
L1/Nano BTB latency 1 cycle 1 cycle
L1/Nano BTB throughput 1 branch 1-2 branches
L2/Main BTB size w/o sharing 5K branches 4K*2 branches
L2/Main BTB size w/ sharing 6.5K branches 4K*2 branches
L2/Main BTB latency 4 cycles 2 cycles
L2/Main BTB throughput 1 branch 1-2 branches
Technology Node 7nm 5nm

虽然 AMD Zen 3 通过 previous fetch block 优化,实现了 1 cycle 下更大的 L1 BTB,但这一点在 2022 年发布的 ARM Neoverse V2 上被追赶:ARM Neoverse V2 的 L1/Nano BTB 也做到了 1024 的容量。

在 L2 BTB 方面,ARM Neoverse V1 占据了领先,无论是延迟还是容量;当然了,ARM Neoverse V1 的制程也要更加领先,ARM 采用的 5nm 对比 AMD 采用的 7nm。

更进一步,ARM Neoverse V1 实现了一个周期预测两条分支,即 two taken(ARM 的说法是 two predicted branches per cycle),在 2 cycle 的 Main BTB 上可以实现接近 AMD Zen 3 的 L1 BTB 的预测吞吐。AMD 也不甘示弱,在 2022 年发布的 AMD Zen 4 处理器上,实现了 two taken。

🔲 ☆

AMD Zen 2 的 BTB 结构分析

AMD Zen 2 的 BTB 结构分析

背景

在之前,我们分析了 AMD Zen 1 的 BTB,接下来分析它的下一代微架构:2019 年发布的 AMD Zen 2 的 BTB,看看 AMD 的 Zen 系列的 BTB 是如何演进的。

官方信息

AMD 在 Software Optimization Guide for AMD EPYC™ 7002 Processors (Publication No. 56305) 中有如下的表述:

The branch target buffer (BTB) is a three-level structure accessed using the fetch address of the current fetch block.

Zen 2 的 BTB 有三级,是用当前 fetch block 的地址去查询,和 Zen 1 一样。

Each BTB entry includes information for branches and their targets. Each BTB entry can hold up to two branches if the branches reside in the same 64-byte aligned cache line and the first branch is a conditional branch.

Zen 2 的 BTB entry 有一定的压缩能力,一个 entry 最多保存两条分支,前提是两条分支在同一个 64B 缓存行中,并且第一条分支是条件分支。这样,如果第二条分支是无条件分支,分支预测的时候,可以根据第一条分支的方向预测的结果,决定要用哪条分支的目的地址作为下一个 fetch block 的地址。虽然有压缩能力,但是没有提到单个周期预测两条分支,所以只是扩大了等效 BTB 容量。和 Zen 1 一样。

L0BTB holds 8 forward taken branches and 8 backward taken branches, and predicts with zero bubbles

Zen 2 的第一级 BTB 可以保存 8 条前向分支和 8 条后向分支,预测不会带来流水线气泡,也就是说每个周期都可以预测一次。相比 Zen 1 容量翻倍。

L1BTB has 512 entries and creates one bubble if prediction differs from L0BTB

Zen 2 的第二级 BTB 可以保存 512 个 entry,但不确定这个 entry 是否可以保存两条分支,也不确定这个 entry 数量代表了实际的 entry 数量还是分支数量,后续会做实验证实;预测会产生单个气泡,意味着它的延迟是两个周期。相比 Zen 1 容量翻倍。

L2BTB has 7168 entries and creates four bubbles if its prediction differs from L1BTB.

Zen 2 的第三级 BTB 可以保存 7168 个 entry,但不确定这个 entry 是否可以保存两条分支,也不确定这个 entry 数量代表了实际的 entry 数量还是分支数量,后续会做实验证实;预测会产生四个气泡,意味着它的延迟是五个周期。

简单整理一下官方信息,大概有三级 BTB:

  • (8+8)-entry L0 BTB, 1 cycle latency
  • 512-entry L1 BTB, 2 cycle latency
  • 7168-entry L2 BTB, 5 cycle latency

从表述来看,除了容量以外基本和 Zen 1 一致,猜测是在 Zen 1 的基础上扩大了容量。

下面结合微架构测试,进一步研究它的内部结构。

微架构测试

在之前的博客里,我们已经测试了各种处理器的 BTB,在这里也是一样的:按照一定的 stride 分布无条件直接分支,构成一个链条,然后测量 CPI。

考虑到 Zen 2 的 BTB 可能出现一个 entry 保存两条分支的情况,并且还对分支的类型有要求,因此下面的测试都会进行四组,分别对应四种分支模式:

  • uncond:所有分支都是无条件分支:uncond, uncond, uncond, uncond, ...
  • cond:所有分支都是条件分支:cond, cond, cond, cond, ...
  • mix (uncond + cond):条件分支和无条件分支轮流出现,但 uncond 在先:uncond, cond, uncond, cond, ...
  • mix (cond + uncond):条件分支和无条件分支轮流出现,但 cond 在先:cond, uncond, cond, uncond, ...

stride=4B

首先是 stride=4B 的情况:

可以看到,图像上出现了三个比较显著的台阶:

  • 所有分支模式下,第一个台阶都是到 8 条分支,CPI=1,8 对应了 8-entry 的 L0 BTB
  • 所有分支模式下,第二个台阶都是到 256 条分支,CPI=2,对应了 512-entry 的 L1 BTB,只体现出了一半的容量;但在 mix (uncond + cond) 和 mix (cond + uncond) 模式下,分支从 256 到 512 时 CPI 缓慢上升,意味着 L1 BTB 的 512-entry 还是可以完整访问,只是带来了一定的开销:CPI 从 2 增加到了 2.5
  • 在 uncond 和 cond 模式下,第三个台阶到 4096 条分支,CPI=5,对应 L2 BTB,没有显现出完整的 7168 的大小
  • 在 mix (uncond + cond) 模式下,第三个台阶延伸到了 5120,超出了 4096,依然没有显现出完整的 7168 的大小
  • 在 mix (cond + uncond) 模式下,第三个台阶延伸到了 7168,显现出完整的 7168 的大小

和 Zen 1 不同,Zen 2 的 L1 BTB 出现了不同模式下容量不同的情况,原因未知,后续还会看到类似的情况。

Zen 2 的 L2 BTB 依然是带有压缩的,只有在 mix (cond + uncond) 模式下才可以尽可能地用上所有的容量,而其余的三种模式都有容量上的损失。

stride=8B

接下来观察 stride=8B 的情况:

现象和 stride=4B 基本相同,L1 BTB 从 256 到 512 部分的变化斜率有所不同,其余部分一致。

stride=16B

继续观察 stride=16B 的情况:

相比 stride=4B/8B,L0 BTB 和 L2 BTB 的行为没有变化;除了 cond 模式以外,L1 BTB 的容量减半到了 128,意味着 L1 BTB 采用了组相连,此时有一半的 set 不能被用上。此外,比较特别的是,从 stride=16B 开始,CPI=5 的平台出现了波动,uncond 模式下 CPI 从 5 变到 4 再变到了 5,猜测此时 L1 BTB 也有一定的比例会介入。

stride=32B

继续观察 stride=32B 的情况:

相比 stride=16B,L0 BTB 的行为没有变化;除了 cond 模式以外,L1 BTB 的容量进一步减到了 64,符合组相连的预期;L2 BTB 在 mix (uncond + cond) 模式下不再能体现出 5120 的容量,而是 4096:此时在一个 64B cacheline 中只有两条分支,第一条分支是 uncond,第二条分支是 cond,不满足 entry 共享的条件(必须 cond + uncond,不能是 uncond + cond),此时 uncond 和 cond 分别保存在两个 entry 中,每个 entry 只保存一条分支,因此 L2 BTB 只能体现出 4096 的容量。而 mix (cond + uncond) 模式依然满足 entry 共享的条件,所以依然体现出 7168 的容量。特别地,在 mix (cond + uncond) 模式下出现了非常剧烈的 CPI 抖动,可能出现了一些预期之外的性能问题。

stride=64B

继续观察 stride=64B 的情况:

相比 stride=32B,L0 BTB 的行为没有变化;除了 cond 模式以外,L1 BTB 的容量进一步减到了 32,符合组相连的预期,但 cond 模式下依然保持了 512 的容量;L2 BTB 在 mix (cond + uncond) 模式下只能体现出 4096 的容量,此时每个 64B cacheline 都只有一条分支,不满足两条分支共享一个 entry 的条件。

stride=128B

继续观察 stride=128B 的情况:

相比 stride=64B,L0 BTB 的行为没有变化;除了 cond 模式以外,L1 BTB 的容量进一步减到了 16,符合组相连的预期,而 cond 模式下 L1 BTB 容量也减少到了 256;L2 BTB 的容量减半到了 2048,意味着 L2 BTB 也是组相连结构。

小结

测试到这里就差不多了,更大的 stride 得到的也是类似的结果,总结一下前面的发现:

  • L0 BTB 是 (8+8)-entry,1 cycle latency,不随着 stride 变化,全相连
  • L1 BTB 是 512-entry,2 cycle latency,容量随着 stride 变化,大概率是 PC[n:3] 这一段被用于 index,使得 stride=16B 开始容量不断减半;但 cond 模式下的行为和其余几种模式不同,直到 stride=128B 才开始容量减半
  • L2 BTB 是 4096-entry,5 cycle latency,容量随着 stride 变化,大概率是 PC[n:6] 这一段被用于 index,使得 stride=128B 开始容量不断减半;其中有 3072 个 entry 最多保存两条分支,前提是这两条分支在同一个 cacheline 当中,并且第一条是 cond,第二条是 uncond;因此最多保存 7168 条分支

Zen 1 和 Zen 2 的 BTB 的对比

下面是对比表格:

uArch AMD Zen 1 AMD Zen 2
L0 BTB size 4+4 branches 8+8 branches
L0 BTB latency 1 cycle 1 cycle
L1 BTB size 256 branches 512 branches
L1 BTB latency 2 cycles 2 cycles
L2 BTB size w/o sharing 2K branches 4K branches
L2 BTB size w/ sharing 4K branches 7K branches
L2 BTB latency 5 cycles 5 cycles
Technology Node 14nm 7nm
Release Year 2017 2019

可见 Zen 2 在容量上做了一定的扩展,但机制上比较类似;特别地,可能是观察到 cond + uncond 的压缩能够生效的比例没有那么高,所以只允许其中一部分 entry 被压缩,例如 4 路组相连,只有前 3 个 way 是可以保存两条分支;剩下的一个 way 只能保存一条分支。

AMD Zen 2 和 ARM Neoverse N1 的 BTB 的对比

AMD Zen 2 和 ARM Neoverse N1 都是在 2019 发布的处理器,下面对它们进行一个对比:

uArch AMD Zen 2 ARM Neoverse N1
L0/Nano BTB size 8+8 branches 16 branches
L0/Nano BTB latency 1 cycle 1 cycle
L1/Micro BTB size 512 branches 64 branches
L1/Micro BTB latency 2 cycles 2 cycles
L2/Main BTB size w/o sharing 4K branches 3K*2 branches
L2/Main BTB size w/ sharing 7K branches 3K*2 branches
L2/Main BTB latency 5 cycles 2-3 cycles
Technology Node 7nm 7nm

可见 AMD Zen 2 在 BTB 容量上有优势,但是延迟要更长;两者都在最后一级 BTB 上做了压缩,但是压缩的方法和目的不同:

  • AMD Zen 2 的压缩方法是,把同一个 64B cacheline 内一条 cond 和一条 uncond 指令放在同一个 entry 当中。这样做的好处是,当预测到 cond 分支不跳转的时候,可以直接根据 uncond 指令的信息,得到下一个 fetch block 的地址;但是也对代码的结构有要求,必须是在同一个 cacheline 中,依次出现一个 cond 和一个 uncond
  • ARM Neoverse N1 的压缩方法是,根据立即数范围对分支进行分类,如果分支的立即数范围比较小,就只占用一个 entry 的一半也就是 41 bit;如果分支的立即数范围过大,就占用一个完整的 82 bit 的 entry;这主要是一个减少 SRAM 占用的优化,避免了所有的分支都要记录完整的 82 bit 信息;对代码的结构要求比较小,只要是跳转距离不太远的分支,都可以存到 41 bit 内

二者都没有实现一个周期预测两条分支,即 two taken(ARM 的说法是 two predicted branches per cycle)。这要等到 2020 年的 ARM Neoverse N2/V1,或者 2022 年的 AMD Zen 4 才被实现。

注意到 AMD 的 Software Optimization Guide for AMD EPYC™ 7002 Processors (Publication No. 56305) 文档里,有这么一段表述:

Branches whose target crosses a half-megabyte aligned boundary are unable to be installed in the L0 BTB or to share BTB entries with other branches.

也就是说,如果两个分支要共享一个 BTB entry,那么它们的目的地址不能跨越 512KB 边界,也就是和分支地址的偏移量不超过 19 位。按 48 位虚拟地址计算,如果 BTB entry 只记录一条分支,最多需要记录目的地址的完整 48 位地址;如果现在 BTB entry 要存两条分支,这两条分支的目的地址都只需要记录 19 位,加起来也就 38 位,还可以空余 10 位的信息用来维护 BTB sharing 所需的额外信息。

所以说到底,无论是 AMD 还是 ARM,做的事情都是对一个固定长度的 entry 设置了不同的格式,一个格式保存的地址位数多,但是只能保存一个分支;另一个格式保存的地址位数少,但是可以保存两个分支。区别就是 AMD 对两个分支的类型和位置有要求,而 ARM 允许这两个分支毫无关系。这就是不同厂商的取舍了。

❌