MoreRSS

site iconThis cute world修改

於清樂(二花),声学专业背景,从事全能运维和SRE工程师,热爱音乐、运动和茶文化。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

This cute world的 RSS 预览

KubeCon China 2024 之旅

2024-08-27 10:10:22

很早就有了解到今年的 KubeCon China 会在香港举办,虽然有些兴趣,但我最初是有被 KubeCon 高昂的门票价格劝退了的。

有时候不得不相信运气的魔力,机缘巧合之下,我从朋友 @Kev 处得知了 KubeCon 的「最终用户门票计划」并借此 0 元购了门票,又邀上了 0xFFFF 社区@Chever-John @0xdeadbeef @茗洛 三位朋友一起参加,在香港租了个 airbnb 住宿,期间也逛了香港城市中的不少地方,收货颇丰。

其实本来也尝试过邀请其他认识的朋友同事,但都因为种种原因无法参加,略感遗憾。

本文多图,也挺多技术无关的内容,为了方便想了解技术的朋友,我先做个大致的总结。

从 KubeCon 回来后我又听了些 CNCF 其他的会议视频,其中比较有印象的是这几个:

结合 KubeCon China 三天的经历,以及上面这些视频的内容,我大概的感觉是:

  1. (几乎)所有聊网络的人都在聊 eBPF, Envoy, Gateway API.
  2. Istio 的 Ambient Mode 吸引了很多曾经因为 sidecar 性能问题而放弃使用服务网格的公司。
  3. Karmada 多集群管理方案在许多公司得到了实际应用,挺多讲这个的。
  4. AI 与 WASM 方面的演讲也有不少,但感觉有些无趣,可能是我对这方面不太感兴趣。
  5. 蔚来汽车、中国移动等公司正在尝试将 K8s 应用在边缘计算场景(智能汽车、通信基站),但这些离普通互联网公司有点远。
  6. 云原生的未来十年会变成什么样?
    • Kubernetes, Service Mesh 等过去十年的新兴技术,现在已经成为了「Boring but useful infrastructrue」,它们将是其他云原生技术潮流的基石,被广泛应用,但自身不会再有太多的变化。
    • AI, eBPF, WASM, Rust 等技术也将在未来十年走向成熟,取代 Kubernetes 当前的地位。

KubeCon China 2024 的会议视频将会陆续被添加到如下这个 Youtube Playlist 中,有兴趣的朋友可以一观:

视频相关的 PPT 可以在这里下载:

这次我主要关注的是 Istio、Gateway API 相关的议题,最近在研究 Istio 的 Ambient Mode,因此希望能够从会议中了解到更多的实现细节与其中的权衡。

三天下来听到的内容也很好的满足了我的期待,Istio / Envoy Gateway / Ingress Controller 的几位核心贡献者分享了很多这些项目的最新进展,实现细节,以及未来的发展方向。

Ambient Mode 在最近 beta 了,是我关注的重点,总结下目前了解到的几个关键点:

  1. istio/ztunnel: 一个 userspace 的 l4 proxy,仅支持处理 L4 流量。
    • ztunnel 会分别与上游和下游建立连接,导致 A <=> B 之间的一个连接会变成 A <=> ztunnel <=> ztunnel <=> B 三个连接,这也会带来性能开销。
    • 因为所有流量都经由 ztunnel 转发,更新 ztunnel 会导致短暂的流量中断。感觉比较好的解决方案是采用 recreate 的更新策略 + 滚动更新节点组下的所有节点来更新 ztunnel.
    • ztunnel 使用的 HBONE 协议强制启用 mTLS,无法关闭,对于不要求安全性的场景会带来额外的性能开销。
  2. istio/proxy: 基于 envoy 的 l7 proxy,在 ambient mode 下它被单独部署为一个 waypoint, 用于处理 L7 流量。
    • waypoint 架构下 proxy 与上下游 Pod 很可能在不同的节点上,这会导致比 sidecar 模式多一次网络跳转,可能带来性能损耗,以及跨 Zone 流量上涨。
    • waypoint 与 sidecar 都是 envoy,它是通过减少 envoy 容器的数量来达到减少资源消耗的目的。

以及一些其他方案:

  • kmesh: 架构类似 Ambient Mode,特点是完全使用 eBPF 来实现 L4 proxy,好处是
    • eBPF 直接在内核空间修改网络包,不需要与上下游分别建立连接。因此性能更好,而且 eBPF 程序更新不会中断流量。
  • cilium service mesh: 特点是 per-node proxy,l7 的 envoy proxy 运行在每个节点上,而不是像 waypoint 一样单独通过 deployment 部署。但也存在一些问题:
    • per-node proxy 无法灵活地调整资源占用,可能会导致资源浪费。
    • 同一节点上的所有流量都由同一 envoy proxy 处理,无法实现 waypoint 那样的 namespace 级别的流量隔离。
    • 与 cilium cni 强绑定,必须使用 cilium cni 才能使用 cilium service mesh.
    • 据说使用起来较为复杂?

总体而言,KubeCon 是一次了解行业前沿技术动态、跟项目开发者及其他行业内的技术人面对面认识交流的好机会,可以帮助自己提升技术视野,维持技术热情与动力,不至于局限在公司业务中闭门造车。

因为要在香港呆三天,衣食住行是必须要考虑的事情。这方面我拉上的几位朋友都比较有旅行住宿的经验,我们最后在香港找了个离会场不远的 airbnb 住宿,最终的体验也是相当不错。房间干净整洁有格调,虽然我觉得稍微有点小了,但朋友说这个空间在香港都是一家三四口住的标准,已经吊打同价位的酒店了。

虽然提早定了住宿,做了点功课,但第一天就出了问题——深圳这边一直下雨导致 @Chever-John 的飞机直接被取消,改订了另一趟航班也晚点。虽然正点到达了会场,但他一晚上就睡了俩小时,在深圳定的前一晚的酒店也没住成,第一天看他整个人都听得迷迷糊糊的。不过没事,至少我听了个爽

说回正题,到了会场领完胸牌,我们就开始了为期三天的 KubeCon China 之旅。

具体的技术内容已经在前文总结过了,这里主要就贴些照片吧。

主会场过厅,海景不错的

去各会议室的过道,酒店的服务很到位

午休茶歇,吃饱喝足

冰镇饮料也可以随便喝,好哇

几位大佬聊 Istio 与 Gateway API 的未来

然后晚上@茗洛带着我们逛了香港的诚品书店,书店好几层,但感兴趣的书不多。后面又逛了好多电子商城、二次元周边店,我算是开了眼界。

《我推的孩子》

另一本

好多二次元钢琴谱,有《四月是我的谎言》

不知道逛到了哪,到处都是二次元周边

轻小说书店 1

轻小说书店 2

轻小说书店 3

第一天就差不多是这样,听了点技术,晚上逛了逛香港,回去休息。

好多的 CNCF 贴纸,可以随便拿,我给同事也带了一些

我收获的 CNCF 贴纸

首先是听了华为的演讲,介绍了 Kmesh 的方案创新,技术细节讲得很赞。想看 PPT 与视频请移步用内核原生无边车架构彻底改变服务网格 - Xin Liu, Huawei Technologies Co., Ltd.

华为介绍 Kmesh

介绍 Kmesh 如何借助 eBPF 实现热更新不中断连接

还听了晋涛老师的十年云原生之旅:容器技术和Kubernetes生态系统的演变 - Jintao Zhang, Kong Inc.

晋涛不愧是行业老将,这么早就开始玩 Docker 了

然后晚上我们随便走了走逛了逛,看了看香港海边夜景。

香港夜景,相当繁华哪

灯红酒绿,游人如织

路上碰到了《商务印书馆》

第三天早上是我这次最期待的,Linus 的访谈,见到了本人,这次行程也圆满了。

Linus

第三天没什么我特别感兴趣的话题,听完 Linus 的访谈后随便逛了逛,跟几位 朋友合了个影,就搭地铁回家了。

咱的合影

咱的 PC 与鲨鲨合影

另外朋友听了个 TiDB 的演讲,看 PPT 是有点意思的哈哈。

TiDB

以及在项目展厅三天逛下来,我帆布袋领了四个,T恤领了三件,还有别的小礼品一堆,吃的喝的都不用说了,管够。另外看网上不少信息说香港的服务业态度很差,但这家酒店可能星级比较高,体验还是相当到位的。

总之,体验相当不错的,有机会的话明年还来!Love you, KubeCon China & Hong Kong!

Kubernetes 集群伸缩组件 - Karpenter

2024-07-10 09:17:31

Kubernetes 具有非常丰富的动态伸缩能力,这体现在多个层面:

  1. Workloads 的伸缩:通过 Horizontal Pod Autoscaler(HPA)和 Vertical Pod Autoscaler(VPA)等资源,可以根据资源使用情况自动调整 Pod 的数量和资源配置。
  2. Nodes 的伸缩:根据集群的负载情况,可以自动增加或减少 Nodes 的数量,以适应负载的变化。
    • 相关项目:
      • kubernetes/autoscaler: 目前最流行的 Node 伸缩方案,支持绝大多数云厂商。
      • karpenter: AWS 捐给 CNCF 的一个新兴 Node 伸缩方案,目前仅支持 AWS/Azure,但基于其核心库可以很容易地扩展支持其他云厂商。

本文主要介绍新兴 Node 伸缩与管理方案 Karpenter 的优势、应用场景及使用方法。

Karpenter 项目由 AWS 于 2020 年创建,其目标是解决 AWS 用户在 EKS 上使用 Cluster Autoscaler 做集群伸缩时遇到的一些问题。在经历了几年发展后,Karnepnter 于 2023 年底被捐献给 CNCF(kubernetes/org#4258),成为目前 (2024/07/10)唯二的官方 Node 伸缩方案之一。

我于 2022 年 4 月在做 Spark 离线计算平台改造的时候尝试了 Karpenter v0.8.2,发现它的确比 Cluster Autoscaler 更好用,并在随后的两年中逐渐将它推广到了更多的项目中。目前我司在 AWS 云平台上所有的离线计算任务与大部分在线服务都是使用 Karpenter 进行的集群伸缩。另外我还为 karpenter 适配了 K3s 与 DigitalOcean 云平台用于一些特殊业务,体验良好。

Karpenter 官方目前只有 AWS 与 Azure 两个云平台的实现,也就是说只有在这两个平台上 karpenter 才能开箱即用。但考虑到它在易用性与成本方面的优势以及在可拓展性、标准化方面的努力,我对它的未来发展持乐观态度。

Cluster Autoscaler 是目前社区最流行的 Node 伸缩方案,基本所有云厂商的 Kubernetes 服务默认都会集成它。

Karpenter 与 Cluster Autoscaler 的设计理念与实现方式有很大的不同。

Cluster Autoscaler 是 Kubernetes 平台上早期的集群伸缩方案,也是目前最流行的方案。但它做的事情比较有限,最大的问题是它本身并不直接管理集群的节点,而是借助云厂商的伸缩组 (AutoScaling Group)或节点池(Node Pool)来间接地控制节点(云服务器)的数量。这样的设计导致了一些问题:

  1. 部署与维护比较繁琐:需要先在云厂商的控制台上创建好伸缩组或节点池,然后再在 Kubernetes 集群上部署 Cluster Autoscaler,并将伸缩组或节点池的名称等信息填写到 Cluster Autoscaler 的配置文件中。增删节点池时也需要走一遍这个流程。
  2. 能力受限于云厂商的伸缩组或节点池服务:如果云厂商的伸缩组或节点池服务不支持某些功能,那么 Cluster Autoscaler 也无法使用这些功能。
    • 举例来说,AWS EKS 的 Node Group 功能非常难用,毛病一大堆。但如果要用 Cluster Autoscaler,你就没得选,Node Group 再难用也只能忍着。

而 Karpenter 则完全从零开始实现了一套节点管理系统,它直接管理所有节点(云服务器,如 AWS EC2),负责节点的创建、删除、修改等操作。

相较 Cluster Autoscaler, Karpenter 的优势主要体现在以下几个方面:

  1. 声明式地定义节点池: Karpenter 提供了一套 CRD 来定义节点池,用户只需要编写好 Yaml 配置部署到集群中,Karpenter 就会根据配置自动申请与管理节点。这比 Cluster Autoscaler 的配置要方便得多。
    • 以 AWS 为例,你简单地改几行 Yaml 配置,就可以修改掉节点池的实例类型、AMI 镜像、数量上下限、磁盘大小、节点 Labels 跟 Taints、EC2 Tags 等信息。
    • 借助 Flux 或 ArgoCD 等 GitOps 工具,你还可以实现自动化的节点池管理以及配置的版本控制。
  2. 成本感知的节点管理:Karpenter 不仅负责节点数量的伸缩,它还能根据节点的规格、负载情况、成本等因素来选择最优的节点类型,以达成成本、性能、稳定性之间的平衡。 - 具体而言,Karpenter 在成本优化方面具有这些 Cluster Autoscaler 不具备的功能:
    • Spot/On-Demand 实例调整: 在 AWS 上,Karpenter 可以设置为优先使用 Spot 实例,并在申请不到 Spot 实例时自动切换到 On-Demand 实例,从而大大降低成本。
    • 多节点类型支持: Karpenter 支持在同一个集群中使用多种不同规格的节点,并且支持控制不同实例类型的优先级、数量或占比,以满足不同的业务需求。
    • 节点替换策略:Karpenter 支持灵活的节点替换策略,可以通过 Yaml 控制每个节点池的节点替换条件、频率、比例等参数,以避免因节点替换导致的服务不可用。
    • 节点的生命周期管理:Karpenter 支持定义节点的生命周期策略,可以根据节点的年龄、负载、成本等因素来决定节点的续租、下线、销毁等操作。而 Cluster Autoscaler 只能控制节点的数量,它不直接管理节点,也就做不到此类节点的精细管理。
    • 主动优化:Karpenter 支持主动根据负载情况使用不同实例类型的节点替换高风险节点,或合并低负载节点,以节省成本。
    • Pod 精细化调度:Karpeneter 本身也是一个调度器,它能根据 Pod 的资源需求、优先级、Node Affinity、Topology Spread Constraints 等因素来申请节点并主动将 Pod 调度到该节点上。而 Cluster Autoscaler 只能控制节点的数量,并无调度能力。
  3. 快速、高效:因为 Karpenter 直接创建、删除节点,并且主动调度 Pod,所以它的伸缩速度与效率要比 Cluster Autoscaler 高很多。这是因为 Karpneter 能快速获知节点创建、删除、加入集群是否成功,而 Cluster Autoscaler 只能被动地等待云厂商的伸缩组或节点池服务完成这些操作,它无法主动感知节点的状态。

总之,个人的使用体验上,Karpenter 吊打了 Cluster Autoscaler.

这部分建议直接阅读官方文档Karpenter - Just-in-time Nodes for Any Kubernetes Cluster.

如果你使用的是 Proxmox VE, Aliyun 等其他云平台,或者使用的是 K3s, Kubeadm 等非托管 Kubernetes 发行版,那么你就需要自己适配 Karpenter 了。

Karpenter 官方目前并未提供详细的适配文档,社区建议以用于测试的Kwok Provider 为参考,自行实现。Kwok 是一个极简的 Karpenter Provider 实现,更复杂的功能也可以参考 AWS 与 Azure 的实现。

国内云服务方面, 目前已经有人做了 Aliyun 的适配,项目地址如下:

对于个人 Homelab 玩家来说,使用 Proxmox VE + K3s 这个组合的用户应该会比较多。我个人目前正在尝试为这个组合适配 Karpenter,希望能够在未来的文章中分享一些经验。项目地址如下:

Cluster API (CAPI) 是 Kubernetes 社区提供的一个用于管理多集群的项目,从介绍上看,它跟 Karpenter 好像没啥交集。但如果你有真正了解使用过 CAPI 的话,你会发现 Karpenter 与 CAPI 有一些功能上的重叠:

  1. CAPI 的 Infrastructure Provider 专门负责处理云厂商相关逻辑的组件。Karpenter 的标准实现内也包含了 cloud provider 相关代码,还提供了 NodeClass 这个 CRD 用于设定云服务器相关的参数。
  2. Cluster API Bootstrap Provider (CABP) 负责将云服务器初始化为 Kubernetes Node,实际上就是生成对应的 cloud-init user data. Karpenter 的 NodeClass 实现中同样也包含了 user data 的生成逻辑。

Cluster API 的目标是多集群管理,并且它的设计上将 Bootstrap, ControlPlane 跟 Infrastructure 三个部分分离出来了,好处是方便各云厂商、各 Kubernetes 发行版的接入,但也导致了它的架构比较复杂、出问题排查起来会比较麻烦。

历史案例:Istio 曾经就采用了微服务架构,结果因为性能差、维护难度高被不少人喷,后来才改成了单体结构。

而 Karpenter 则是一个单体应用,它的核心功能被以 Go Library 的形式发布,用户需要基于这个库来实现自己的云平台适配。这样的设计使得 Karpenter 的架构简单、易于维护。但这也意味着 Karpenter 的可扩展性、通用性不如 Cluster API.

从结果来看,现在 Cluster API 的生态相当丰富,从Provider Implementations - Cluster API Docs 能看到已经有了很多云厂商、发行版的适配. 而 Karpenter 2023 年底才捐给 CNCF,目前只有 AWS 与 Azure 的实现,未来发展还有待观察。

那么有没有可能结合两者的优势呢?Kubernetes 社区其实就有类似的尝试:

上面这个实验性质的项目尝试使用 Karpenter 作为 Cluster API 的 Node Autoscaler,取代掉现在的 Cluster Autoscaler.

我目前对 Cluster API 有些兴趣,但感觉它还是复杂了点。我更想试试在 Karpenter 的实现中复用 Cluster API 各个 Provider 的代码,快速适配其他云厂商与 Kubernetes 发行版。

2024 年香港徒步旅行记录(一)

2024-05-21 10:05:00

2023 年年度总结中,我给 2024 年定下的目标是「工作与业余爱好上稳中求进,生活上锻炼好身体、多关心家人」,为此今年我做了许多旅行的准备。

在深圳呆了快五年,挺长时间里一直将自己局限在技术与工作中,少有时间去关注周围的世界——生存已属不易,没有多余的精力去关心其他事情。2023 年是一个分界点,工作上越来越得心应手,手头也不再紧张,个人精力跟业余时间得以解放,我自然开始追求更多的生活体验。

说是要锻炼身体,多旅游,但也没啥明确的计划,只是想着该出国走一走开开眼界,于是 3 月份到出入境管理局搞定了护照跟港澳通行证。

香港离深圳近得很,自然就想着先去香港多走走,这是缘起。

而这件事情后来的发展,现在回看起来,跟我当初折腾 NixOS 或电子电路的故事如出一辙,大概性格使然吧哈哈。

简单总结下呢,就是 4 月份去香港海边徒步了一天,从维多利亚港沿着海岸线一路走到坚尼地城,然后就爱上了这种在海边徒步的感觉,回来后也一直念念不忘,查了许多香港徒步路线的资料。

五一假期的时候跟着小红书跟 Bilibili 上的攻略,徒步了香港麦理浩径第一段的部分以及第二段全程。因为准备不足,举着手机走了一个小时夜路,并且在西湾村营地租帐篷露营了一晚上。这次体验又让我迷上了夜间徒步跟露营。一回家就查各种徒步露营装备,疯狂下单,收了一大堆快递,花掉了一万多 RMB…

接着就是 5 月 18 日,背上我 65L 的重装背包,总重量大概有 17 kg,从深圳坐地铁 + 大巴直达北潭凹,爬了一遍麦理浩径三段,夜间在水浪窝营地露营一晚,第二天一早直接打道回府。本来计划是周末两天刷完三四段,甚至又余力再走完第五段的。但是我显然高估了自己的体力,而且背着 17 kg 的重装背包,这也不是一件轻松的事情。

休息了一周后,在 5 月 25 日,我又完成了麦理浩径第四段的徒步。这次出发前根据上次的经验精简了装备,总总量应该轻了大概 1kg,但仍旧有 16kg。四段是麦径难度最高的一段,而且终点基维尔营地不接受个人预约,又导航徒步到大老山隧道站乘九巴 74X 路至广福邨下车,再步行到地铁站乘车到罗湖口岸回家,到达车站时已经是 22:20。全程差不多 9 个小时,步行超过 21 公里,是我目前的人生巅峰。

以上就是我到目前为止的香港徒步经历,目前对重装徒步兴趣浓厚,计划今年先把麦理浩径全程走完, 积累经验,后续再看看香港跟国内外的其他徒步路线。

时间:2024-04-14

这是我第一次出境游,整个行程都是临时起意,没有做任何准备,即使是过了罗湖口岸,站在东铁线地铁上的时候,我对整个行程也并无太多期待,心里想着无非是不同的人与物,无甚特殊的。但总得出去走走,看看大陆之外究竟是个什么样子,所以就这么随意地来到了香港。

具体有多随意呢?就这么说,过了口岸没多久,手机就没信号了,我这才意识到香港终归不是大陆,慌得我又出站再往回坐,在罗湖口岸连上网络开了个漫游跟流量包,这才算是正式开始了我的香港之旅。

最初期待甚少,但现实常常超出我的想象。在香港徒步的一日,我切实感受到了更宽广更多元化的世界,这是在大陆境内无法体会到的。

刚上东铁线甚至刚出地铁站的时候,我对自己已经出境这一点尚无实感,因为香港的地铁跟深圳实在太像了,不论是地铁车厢的设计还是车站的建筑风格、干净整洁的程度,都跟深圳非常相似。

后面跟朋友聊起来,才知道国内深圳地铁跟香港地铁是有合作关系的,很多技术最初都来自香港,算是一脉相承了。

在去往维多利亚港的路上,看到香港多的双层巴士、随处的繁体字跟英文标识,才逐渐感觉到这里确实是个不同的地方。

出了会展地铁站,往维多利亚港去的路上

路上遇到的敞篷观光大巴

接着就到了维港的海边步道,海景很美,两岸都是高楼大厦,海上有很多游船,还有很多钓鱼的人。

海边

两岸都挺繁华

大海与高耸的大楼

钓鱼佬

从另一个角度看钓鱼佬

接着我意识到,从我住处可全程地铁直达维港,坐个地铁再浅浅走几步路就能看到这么漂亮的海景,这不比去深圳大鹏半岛方便多了!我为此兴奋起来。这风景吊打深圳湾,但深圳湾步道上的人甚至比维港还多,只能说深圳人真的太多了…

补充:听说深圳湾以前真的是海边,但填海造路使它成了现在的烂泥滩

沿着海岸线继续走,渐渐发现人群里外国游客相当的多,粗略估算超过一半是外国人,这让我感受到了香港的国际化氛围。

裹头巾的女游客在自拍,有点异域风情

外国游客们音箱歌一放,就地跳起了舞来

海边还有很多细节值得一看。

海岸上的微观建筑,挺有意思

天星小轮的港口,休息、舞蹈的人挺多

天星小轮

摩天轮旁的小摊贩,这小摩托车比较有年代感了...

维多利亚港的免费 WiFi

在维港逗留玩耍了一个小时,买了点面包当午餐吃了,然后就沿着海边步道继续往前走,也没啥明确的目的地,反正累了就回家。维港的人很多,但出了维港后步道上人就少了很多,走在这样的路上看着风景,相当闲适。

海与城市

沿着海岸线继续走,阳光正好

波光潋滟

充满涂鸦的转角,有人在拍照

似是废弃的小码头

被改造成游玩地点的码头

海边步道上的小店,小女孩在选冰棒

香港的叮叮车

悠闲、轻快地随走随停,大概一个半小时后,到达了步道的终点——坚尼地城,我还有点失望,这步道居然就这么没了。只好掉转方向往城市内走,市内倒也别有一番风景。

天快黑了,沿街商店亮起了灯牌

这家店门口人挺多,或许味道不错?

在市内逛了逛,确认了香港各种店铺的特点是小而美,各种店铺都非常小,但弄得比较精致,能看得出花了心思。相对而言深圳的商场店铺就大气很多了,但也缺了香港这些店铺的种种细节。

玩得尽兴,又找了家店吃饱喝足,打道回府。

坚尼地城地铁站,回家啦

这一趟下来,我的感觉是,香港确实是国际化大都市,跟深圳的气息很不一样,海边随处可见的酒吧, 比例非常高的外国游客跟在香港工作的外国人,让我感觉到了一种国际化的氛围。至于城市的繁华程度,跟深圳各有千秋。

我的路线(抄了近路) - Google Earth Map

食髓知味,在香港海边徒步一日后,我对香港徒步来了兴趣,一直瞅着机会再来一次。

到了五一假期,我终于有了机会,随便在小红书上查了些资料,发现麦理浩径一二段挺有名,还能露营,于是就决定是它了。

看天气预报最近几天都有雨,五一那天下得比较大没出门,顺便就买了迪卡侬冲锋衣、冲锋裤、速干 T 恤、大号水杯,想着明天就算有雨也要去。

幸而天公作美,第二天天气转阴,上午东西到货,又做了大半天准备才出门。因为计划是连续步行两天,所以跟着小红书上的香港旅游攻略,吃的穿的用的都带了挺多。

下午出境,首先买了 OPPO 9.9 元的香港流量包(五一特惠),然后坐地铁到深水埗(bù)地铁站整了张八达通,200 港币,其中 50 块押金。另外考虑到以后会常去香港,又多充了 100 块。

香港地铁

深水埗(同埠)办八达通实体卡,因为计划常来香港玩

接着就是各种地图导航,首先坐小巴到西贡码头,然后查半天导航,坐九巴 94 路到麦理浩径起点。

小巴要叫停车才停,真的 I 人(内向)天敌,决定以后尽量选九巴,下一站前按个 Stop (香港人称掀钟)这种才适合 I 人。

乘小巴到达西贡

乘上九巴 94 路去往麦理浩径起点

路边海湾

导航本来是说坐到麦理浩夫人度假村站,但我一下没注意就坐过了,然后下车时第一次「掀钟」不熟悉,又过了一站才按到 Stop,结果就是在「北潭凹管理站」才下车(16:34),然后往回走了半个小时才到「麦理浩径起点」(17:09)。

坐过了站,在北潭凹管理处下车

路上几乎没人,风景很好,很安静

麦理浩夫人渡假村

多走了半个小时公路才到达麦径起点,路上体验很好,主要是远离城市,很安静

从「麦理浩径起点」往万宜水库的路上(大网仔路)看到很多出租车来来往往,联想到之前查的攻略, 就知道这些出租车是为了接送徒步的人。我是单人徒步,为了省 150 的打车费,就选择了坐九巴到起点然后步行。

路上标牌

路上很多烧烤点,貌似是政府修建的,可以免费用,但没遇到过人在用这些烧烤点,可能季节或天气原因?

到达万宜水库岔路口时已经是 17:26 了,走第一段晚上肯定到不了我的目标地址「咸田湾」,而且我了解到的是二段风景最好。一番心理斗争后,我直接走了左边的路线,绕过麦径一段,直接走到西湾亭走二段的路线。

到达万宜水库岔路口,往右是正经麦径,不过比较晚了,我抄近路走了左边。

万宜水库

一路上依旧遇不到人,远离城市跟人群的感觉很奇妙

风景很不错

瞅一眼路边地图看看我在哪

抄近路到达西湾亭,从麦径起点算用了一个半小时

沿着大路走了整整一个小时才到西湾亭,为了省个出租车费我也是拼了。不过倒也不算亏,这两个小时的路上风景相当不错,而且没遇到几个人,体验非常棒!有种我很喜欢的孤独感。

这时已经是 18:34,天色已经快黑了,我有点慌,刚好有辆出租送人到这,我向司机问路,他好心地帮我指了路。从这里开始才是山路,前面两个小时一直走在大马路上,而且我的脚在走了两个小时后已经很痛了。

接着就手机打着灯,一直走到 19:10 才到西湾村。

走小路下山到西湾村

走到 M30 标距柱(每 500 米一个)时天已经黑了

快到西湾村了,路边开始出现路灯

看到第一个租帐篷的店,立马就整了套帐篷。押金 300 租一晚 200,跟国内比可能很贵,但跟香港劳动节期间 1300+ 的旅馆比已经性价比爆棚了!

我之前的目标是到咸水湾租帐露营篷,但到西湾村的时候天已经完全黑了,而且一路心惊胆战地连续赶了三个小时路后我也累得不行了,在往西湾村的路上我就一直憧憬着西湾村总该能找到地方住,万幸它没让我失望。

终于到了,租了个帐篷露营,200 港币一晚,因为港币不够付了人民币

我的帐篷

本来想吃口热乎的,但看到店里 55 港币起步的面食,我还是决定吃点自己的能量棒当晚餐。

洗澡收费 30,但人太多了,我就没洗。晚上海浪声、旁边帐篷里人的说话声等,环境变化太大,睡不着,小说看到凌晨三点,然后跑去海滩上听了听海浪声,回来的路上一路被村民家的狗狂吠,惊出我一声冷汗,万幸我还懂点这种场面的处理方式,盯着狗看,慢慢后退。即使走得远了,我还是一步三回头,置到回到营地把门关上才松了一口气。并且明白一个道理 —— 晚上还是不要这么跳脱的好…

西湾村手机信号挺好,但用的人太多带宽不够,很多站点怎么刷都刷不出来… 想起 21 年看过的《走出荒野》,讲的是通过徒步自我救赎的故事,打算翻出来读一读但网络问题根本刷不出来。

然后迷糊睡到了早上 7 点,出帐篷发现我这个营地人都走差不多了。想洗漱但意识到我带了牙刷却没带牙膏…因为最初是打算住旅馆的,谁 TM 知道居然没找到个便宜旅馆,香港的物价太 TM 离谱了, 逼得我连夜赶路到这里来住帐篷。

厕所也没上,早餐看太贵了也没吃,买了瓶 0.6L 的水(20 港币),07:24 直接出发了,早餐仍然是能量棒配水。

第二天一早从西湾村出发。

西湾村出发点 - M31 标距柱

没走几步就看到了西湾营地,原来西湾村过来没几步就是西湾营地,垃圾好多…

西湾营地

西湾营地这有黄色标牌,写着野猪出没。朋友也提醒过我,说它们不咬人,但如果你包里有吃的,它们会弄坏你的包。

过了西湾营地又开始爬山。

过了西湾营地开始上山,这张图漂亮

回望西湾营地

山上海景独好

07:58 到达我印象中全程风景最赞的地方 —— 接近咸田湾沙滩的一段步道。

这一段风景绝赞,全程最佳!

这段山路也很有味道

08:15 到达咸田湾沙滩,这里人很多,帐篷也很多。从这里开始接着是往赤径去,钻的就是山路了,这一段不是海景,但也别有韵味。

看到了咸田湾

咸田湾的独木桥

10:00 到达赤径,这一处水湾风景也很美!在这里玩了很久。

在赤径休息

10:30 到达赤径公厕解决了下个人卫生问题。意外发现赤径公厕个别涂鸦很可爱!随手一拍。

赤径公厕的简笔画

接着是从赤径往北潭凹的路,同样是起起伏伏。

继续往北潭凹走,左侧指路牌

路上遇到一泡插着鲜花的牛粪,很有意思,前面的女生蹲下拍照,搞得我差点以为花是她插的… 一路上再怎么陡峭的步道上都能看得到牛粪,挺原生态的哈哈。甚至回去路上在西贡码头汽车站都看到了两头牛在绿化带上吃草。

不知道是谁,在牛粪上插鲜花 emmm

接着一路上攀,脚踝已经酸痛得不行,非常痛苦。有一段累到直接坐在地上看了 20 分钟《走出荒野》。

到达 M044

柱子上的简笔画

11:57 到达麦径二段的终点,走不动了。

到达终点的公厕

终点的指示牌

又坐九巴 94 路回西贡市区,来去都是这趟车,从「北潭凹管理站」下车又从「北潭凹」上车,几乎是围着这一片走了刚好一圈。

又坐九巴 94 路回西贡市区

12:30 到西贡市区,饿得不行,找一圈吃饭的地儿发现,麦当劳居然是最实惠的——因为它价格跟国内差不多。于是搞了一顿麦当劳,发现公司有点事,因为今天我 Oncall,顺便拿出 MacBook 处理了下公司的事。是的没错,我背包里还有一台 1.4kg 的 MacBook Pro,真是要了老命。

到达市区,饿得不行到处找吃的

发现麦当劳是最实惠的,跟深圳差不多价。顺便处理个 Oncall...

累得不行,昨晚又没洗澡,一路上疯狂出汗,浑身气味比较感人。吃完饭随便逛了逛,就打道回府了。从西贡坐九巴 299x 路到沙田站,然后转乘东铁线到罗湖口岸,再坐深圳地铁回家。

这家店装修有点意思

理发店,香港物价...

这几家店面比较有年代感

西贡码头

总的来说,香港西贡这块开发得更好,步道很多,原生态的同时路线也足够成熟,而且过来比深圳大鹏半岛更方便,期待下次再来。

查攻略最有用的几个 APP:

上次徒步走完麦理浩径二段后就有点上头了,刚回到家累得不行,脚都要废了,但隔天还想找个步道走走。另外就是这两次徒步都太随意,缺乏登山杖、登山鞋、背包等专业装备,而且露营还是租的人家帐篷,接着就是看各种徒步教程攻略,疯狂买买买。

买的一堆装备陆续到货,在家试用了好几天,比如说穿登山鞋上班、空调开 16 度在室内搭帐篷露营、床上铺蛋巢垫盖睡袋、晚餐吃自热米饭,等等不一而足 emmm

试用露营锅具、炉子以及食物

试用了好几天帐篷(室内露营 emmm)

目前的所有装备,算上没拍进来的衣物,总重量大概 14kg

接着 5 月 18 跟 19 两日又是个空闲的周末,这次做足了准备,计划两天走完麦理浩径三四段。

整理背包时,意识到气罐很难处理,带着上地铁、过海关,感觉都不太行,在香港我也不知道是否好买,所以把锅具跟炉子都踢出了背包,只带了两盒海底捞自热米饭跟一些能量胶、压缩饼干以及零食。另外水带得相当充足,3L 水袋 + 600ml 小水杯,光水就有 3.6kg.

大约 9 点多出发,首先是乘东铁线到沙田站,转程九巴 299x 路到麦边站,再转乘 94 路到麦径三段的起点。

麦边站等九巴 94 路

在二段终点解决完个人卫生问题,热了个身,顺便帮一批反穿麦径二段的大陆人合了个影,接着就开始上山。

三段一开始就是急攀,路面很陡,而且透露着一股年久失修的味道,路况比一二段差远了。

麦径三段-开头的急攀路段

三段的人流量相比二段那是断崖式下降,几乎依山遇不到人,有一种远离人世的孤独感。有的人可能会喜欢热闹,但我恰恰相反,相当享受这种孤独感。

上升太快,我又负重 17 公斤,没爬几步就累到要休息,甚至有点怀疑今天能不能走完三段。但辛苦带来的收获也挺大,越往上爬,风景越美,山景与海景交相辉映,让人心旷神怡。

风景很不错

山路

山路上美美自拍

继续急攀

在山顶还解锁了一些隐藏支线,因为走的人少,灌木丛茂盛,登山杖几乎没法用,这时候就很庆幸我穿了长袖紧身运动打底衣裤,不然走这种路小腿难免挂彩。

接着就是下降。

开始下降

山水共长天一色

Me - 发现眼镜确实变色了欸

下降路段走完,到达嶂上营地跟士多店。

这里再往前就是障上士多店

麦径路标 - M61

又开始上升,不过跟三段起始的那段比起来,这段路还算平缓。接着我高兴没多久,就到了一段相当陡峭,几乎没有台阶的路段。

上升,没有台阶的陡坡

一小段平路

坐下休息一会儿,顺便自拍一个

又二十多分钟后,累得不行,寻个地坐下,顺便回头看看。

回望

继续走,没多久就到了 17 点,天开始黑了,我也提前翻出了头灯准备着随时打开。

继续走

晚上 7 点,天开始黑了

到达山顶,已经打开了头灯

山顶继续前行,远方城市灯火通明

遇到的轻装夜爬团,应该是香港学生,而重装徒步的我已经精疲力竭,一阶阶楼梯地蹒跚下降,被很快超越

到达 M68,离三段终点近了,全身无一处不疼,真真是行百里者半九十,最后这一公里路相当折磨人

到达三段终点,接水、自热米饭当晚餐,休息了一个多小时。

很想直接回家,但导航发现水浪窝营地只差 500 米了,内心天人交战后继续往营地进发

继续夜行

路标

500 米走了我 25 分钟,中间还有一段陡坡

到达营地开始搭帐篷,营地很空旷,只有我跟另一伙人露营。

18 号这一天下来一共走了 33582 步,而且背着十七八公斤爬上爬下,最后两三公里完全是咬着牙拼命爬的。

慢吞吞搭帐篷,花了半小时才搞定

第二日早上 6 点醒来

7 点多,在流动厕所解决完卫生问题,在营地逛了一圈没找到水源,只好拿折叠水桶在流动厕所的洗手池接了点水洗脸顺便擦身体。

随处走走,发现营地标牌

早餐随便吃了点东西,接着就收拾帐篷打算回家,发现收拾起来还挺费劲,慢慢吞吞弄了也大概 40 分钟,而且发现蛋巢垫下面有跟毛毛虫,帐篷内还有好几只蚂蚁,还发现一只跳蚤,另外内帐外面也爬了根看着就很毒的毛毛虫…

蛋巢垫下的毛毛虫、帐篷内的跳蚤大概是昨晚搭帐篷时,把东西放在一个石墩上,从石墩上爬上来的, 蚂蚁可能是从内帐的孔洞爬进来的。总之它们吓得我收拾东西的时候检查了一遍又一遍,生怕碰到虫子或者把虫子收拾进了行李中。

回家路上,取水点

在车站等车

总的来说,计划两日麦理浩径徒步,实际只 18 号走完麦径三段就精疲力尽了,第二日早上直接打道回府。从二段起点走到终点,用时 12:45 - 20:30,在交界处吃晚饭、休息了 1 个小时。之后从 21:50 - 22:15 走到水浪窝营地,到 22:50 才搭好帐篷。这是我第一次重装徒步,积累了宝贵的经验,也发现了许多问题,下次再徒步麦理浩径肯定能更得心应手了~

只爬完第三段就精疲力尽,我分析了主要有这几个原因:

  • 体力不够,还需要多加锻炼
  • 第一次重装徒步,装备不够精炼,一共得有十七八公斤,其中有许多东西完全没用上。
  • 带了过多食物,可以更精简。

徒步完第三段后,休息没一周,又是周末,周五下班后赶紧跑去续签了香港签证,精简了一番装备,周六上午就再次出发徒步第四段了。

这次出发前根据上次的经验精简了装备,总总量应该轻了大概 1kg,但仍旧有 16kg。主要变化:

  1. 去掉了折叠桌跟折叠凳:上次带了没用上,吃饭都是在营地或者烧烤点,路上补充能量都是直接拿出零食或能量胶随便啃两口,都用不上它们。
  2. 自热米饭也从两盒改成了一盒:因为计划就徒步一天然后露营,第二天回家。只晚上休息的时候来顿热乎的就 OK 了。

四段是麦径难度最高的一段,而且终点基维尔营地不接受个人预约,只接受团体预约。但我还是抱着侥幸心理背着重装背包去了,想着路上总不会只有这一个营地吧(后面的经历证明我有点鲁莽了)。

早饭吃得饱饱的,又洗了澡、休息了会儿消消食,然后 10 点左右就从家里出发了。

深圳地铁一号线上,我滴背包

12:13,香港沙田汽车站,排队上九巴 299X 路

到达大浪窝站,风景很好

去往四段起点路上,美美自拍

烧烤点与海湾,以及远方的城市

走了约十分钟到达三段终点,在这里用直饮水机将 3L 水袋灌满,然后取出登山杖就出发了。

首先是到达上次露营过的水浪窝营地,然后沿着大路继续上山,路上没人。

开始登山不久,发现起雾了

沿大路到达山顶后,是沿着黄泥小路下山。这两天下雨,路面泥泞湿滑,我又背着个 16kg 的重装背包,走起来有点难度。还好有登山杖,倒不用怕滑倒。

黄泥路,这两天下雨,路面湿滑,还好有登山杖

细竹林

岔路口

走黄泥路下完山,接着又是沿着石阶路开始登山,石阶也有点湿滑。

石阶,路面湿润

登山没多久,就遇到了雾气,接着雾就越来越浓。

登山路上也开始有雾了

跟山路上的雾来个合照

雾气加重

因为雾气跟汗水,头发已经湿了

下完一座小山又开始登另一座,路边好多棕叶

又到达一座山顶,在这里休息了一阵子,吃了点东西,接着突然想到我出发时没拉伸,想着补救一下, 就在这里做了个拉伸。

中间还遇到位外国女士背着个很小的跑步背包爬上来,也休息了一会儿喝了口水,往另一边去了,很快消失在了雾中。

到达山顶,雾相当的重

在山顶休息了一会儿,吃了点东西

好重的雾啊,啥都看不清

休息好了开始下山,没多久就到了四段风景最好的一段,因为浓雾没远景看没,如果没雾这里视野会是很开阔的。

这应该是四段风景最好的一段

跟浓雾来个合照

继续前行,到达昂平营地,这里是一块山顶平原草地,浓雾下也有点意境。走着走着旁边雾中出来一只狗跟一个人,说起来这路线上挺多人遛狗的,今天遇到两三波了。

昂平,山顶平原草地

M88 标距柱

因为阴天,下细雨,又这么大雾,天黑得很快,17 点后天就有点看不清路面了,开始需要灯光。头灯在包里懒得拿出来,就把背包背带上的手电夹到腰间别着的便携坐垫上照亮路面,还别说,效果不错。

17:12 了,走树林路已经需要借助灯光

17:40,天黑差不多了,先休息一会儿

可能因为下雨,路上挺多癞蛤蟆

到 20:10 左右,终于到达基维尔营地,听到了有人声,也看到了灯光。一番调查确认了跟之前了解到的一样,这里不接受个人露营。地图上往前看也没露营点,我有点慌了,但总之先到四段终点瞧瞧吧。走到终点发现就是基维尔营地下山的大水泥路面,既然有大路,那就能沿着它回到城市,这样想着终于有了一点安全感。

20:27,沿着大马路下山

下了个山坡后实在累得不行,把便携坐垫一铺就坐下休息了,尝试用高德地图导航,但信号有点差,一直转不出来。

然后一辆车从山下开上来,看到我瘫坐在路边,停下来问我要去哪,了解清楚情况后又给我指路,还说这里徒步下去要一个多小时,路上没路灯,问我行不行,要不要他送我去车站。

手机一直没信号,我一开始是有点心动的,但不想麻烦别人,刚好手机终于加载出了导航,我对照了下跟他指的路是同一个方向,就婉拒了他,并给他展示我的手电筒表示我不担心走夜路。

高德地图给的教我先徒步到大老山隧道站乘九巴 74X 路至广福邨下车,再步行到地铁站乘车到罗湖口岸。

但它给的徒步路线有坑,我跟着导航越走就越荒凉,公路路面开叉,长满荒草,接着就直接没公路了。我慌了,仔细确认才意识到它教我往草丛里钻,仔细看草丛里还真有条路… 但这条小路显然已经半荒废,草木林立,不仔细看几乎分辨不出路线,让人忍不住怀疑这条路真的能走吗?不会走到一半成断头路吧。

还好这条灌木丛近期有人走过,沿途草木有明显被人趟过,沿途灌木上偶尔还扎了很干净显眼的飘带, 明显才扎上去没多久,这给我增加了一点对它的信心。

钻灌木丛,中间还过了条溪流,接着又是上山,好走的山路没走多远,接着又是在山上钻更深的灌木丛,我越来越慌——这真的是下山的路吗?同时我也有点担心被灌木遮挡的路面会有蛇,但现在已经很晚了,下山心切的我没时间顾虑这些,一路急行。

灌木钻了没多久就开始下山路,而且能看到山下明亮的高速公路跟城市夜景了,这让我放心了一点——至少确实是在下山朝城市里去。但这下山路可不好走,几乎是钻着灌木丛林走直线下降,而且是原生地形,非常的陡,即使有登山杖的辅助,也摔了好多跤,还好草木灌木比较密,有效减缓了摔倒的冲击, 也避免我滚下山。

下山路没走多久,我突然发现腰包里的水杯跟夹着的折叠坐垫都不见了,显然是在前面几次摔跤的时候掉了,不过也就三四十块钱,不管了,继续向下。

下山路仿佛没有尽头,万幸途中发现底下的山坳里有好几片亮光,一开始怀疑是山里废弃的房子,前面趟这条路的旅游队在房子里露营,这也给了我希望——或许能有人给点帮助跟一口吃的,一起露营也不错。

到九点半左右终于下到山脚的时候,发现是个电站之类的建筑,周围还有监控警示,挺失望的。不过到这里又是大路了,也能很明显听到不远处车辆来往的声音,悬着的心总算放了下来。

考虑到手表快没电了,先把登山记录停了,显示今天徒步了接近 21 公里,真是累到够呛。

OPPO 健康 - 日行四万步

登山 21 公里

在路边找了个地坐着休息,想到因为钻灌木林、摔跤、一路剧烈运动疯狂出汗,加之今天又下小雨,身上都是各种小叶子、木棍、汗水、沾了叶子上的雨水,这个样子可不好上车见人。见周围也没人,我直接把衣服都脱掉,换了身干净的。

登山鞋里也湿透了,刚刚钻林子导致石子叶子小木棍雨水也进去了不少,也换了备用的沙滩鞋。换鞋时不知道哪跑出来只蚂蟥在我脚面上爬,赶紧给拔掉丢了(也很庆幸我一直穿运动紧身内衣裤,不然这一趟灌木丛徒步下来,小腿刮伤不说,还可能被蚂蟥等各种虫子叮咬)。

衣服鞋子换好后又休息了挺久,然后沿着大路走了可能十多二十分钟,才终于到了山脚,远远看到不远处就有个公交站,再次导航一下,确认它就是大老山隧道站。

到达车站已经是 22:20 了,乘九巴 74X 路到广福邨下车,再步行到地铁站乘车到罗湖口岸、过关、再乘一号线时已经 23:40,这个点居然还有一趟末班车。最后到家已经过了 0 点,饿得不行搞完夜宵、休息、再洗澡,搞到两三点才睡觉。

22:37,在乘九巴 74X 路往广福邨的路上,香港城市夜景

下车,步行前往地铁站

22:52,到达大埔墟地铁站

22:57,等地铁中

总的来说这次徒步距离真的是到目前为止的人生巅峰了,另外这次下山路线也是我走过最险的一次,有点刺激跟后怕,高德地图坑我啊。

  • 不应该带的装备:
    • 折叠桌跟折叠凳:
    • 沐浴露:许多营地没洗澡的地方,擦身体也用不上沐浴露,带这个不如带点湿巾。
    • 洗洁精:同理,就几天徒步,用纸巾擦一擦就行,回家后再用洗洁精清理一遍不迟。
    • 太阳能电板:三天以内,一个 30000 mah 的电源完全够用。如果 3-6 天,还不如带两个移动电源。目前根本没有徒步超过一周的计划,所以这个也没必要带。
    • 保温杯、餐具、炉子:现在是夏季,香港挺温暖,我全程靠能量胶跟压缩饼干维持体力,晚饭吃自热米饭,用不上这些装备。其实主要是气罐不好带,担心被海关查扣。
  • 起了重大作用的装备:
    • 曦途第三代 7075 登山杖:铝合金本身够轻,将部分腿部负重转移到上半身,减轻了腿部的负担, 在重装徒步时能起到保护膝盖、提升稳定性、延长行走时间的作用。
      • 目前发现的缺点:
        • 使用时登山杖中部会共振,声音稍微有点吵,不过问题不大。
        • 一次下雨徒步后放了一天多,就发现杖尖周围生锈了,看来是用的材料不够好(毕竟是不到 60 块钱一根,还要啥自行车)。
    • 挪客云径 65L 徒步背包:够大,带背负系统,非常好用。
      • 目前发现的缺点:
        • 腰封的带子会滑动,行走久了就会松掉,需要重新手动调整。这个很烦,走着走着肩部受力就越来越大,而且行走的时候调整带子松紧也很不方便。
    • 挪客 3L 水袋、600ml 水杯:喝水,必备
    • 迪卡侬 MH100 登山鞋:好的硬底登山鞋,防滑、防水、防磨损,能保护脚部,提供足够的支撑, 减少脚部疲劳。
    • 挪客夏季信封睡袋:即使是现在夏季,山上的营地(水浪窝)晚上还是有点冷的,盖一个夏季睡袋刚刚好。
    • 能量胶、盐丸、压缩饼干、零食:帮助保持体力。能量胶恢复体力但饿得快,压缩饼干能维持饱腹感,零食能提供口感。
      • 能量胶跟盐丸非常有用!
    • 折叠水桶、速干毛巾:打水洗头洗澡洗衣必备!(营地没洗澡的地方,用湿毛巾擦擦身体,也OK, 擦完舒服太多了)
    • 运动速干套装(紧身速干打底衣裤长袖 + 短袖短裤 + 薄外套):防止蚊虫叮咬、速干、防晒、防风,钻个灌木丛也不惧,我愿称之为徒步必备!
    • 速干汗带:吸汗,防止汗水流入眼睛,而且速干款比棉制品强多了,蒸发很快。
    • 变色防风眼镜:防风防晒、防雨、防风沙蚊虫。不管是白天也是夜间徒步,都很实用!
    • 保鲜袋:一时半会儿没吃完的食物,用这个装非常方便。
    • 一次性内裤、袜子:用完即抛,不用洗,方便得很。
    • 神火 A20 手电筒:一开始觉得好像没啥用,毕竟已经有头灯了,结果发现晚上搭帐篷、在营地逛逛,这个很好用也很有必要。
    • 30000 mah 移动电源(111Wh):显然这个起了大作用,用完一天到回家的时候,还有 70% 电量, 它大概能顶三天,650g 的重量物有所值。
      • 大概两年前买的,最近查了下,有个别能量密度更高的电源,但市面上大部分电源的能量密度都跟这差不多,没有太大的提升。
    • 曦途折叠坐垫:行走路上累了,随时坐下休息,这个很有用。
      • 本来是打算用折叠凳的,但折叠凳收纳还是麻烦许多,而且它建议承重 75Kg,我背着个接近 20 公斤的包,很怕一屁股给坐坏了。
  • 不是很满意的装备
    • Warsun W81s 头灯:用了两个半小时就没电了,而且还不能换电池!后面去露营地点的路上只能手举手电筒同时还要用登山杖,很不方便。必须得换个电池可更换的头灯,最好是能用 18650 电池
      • 下单了耐朗 B71 转角手电筒、不知名的万向手电筒 U 型夹(当手电腰夹用)、倍量的最高密度 18650 电池跟 21700 电池。这样转角手电 + 直角手电 + 手电腰夹 + 备用电池组合,能应对任何多日长线徒步的夜行情况了
    • 防晒袖套脖套:穿了长袖紧身衣的情况下袖套感觉就没啥必要了,脖套可以保留一个。
    • 迪卡侬 MH500 冲锋衣 + 普通雨裤:占地方,而且也挺重,这次没派上用场。但考虑到万一下大雨刮大风,这俩还是得带上。大风天气下雨披用处不大,只能靠冲锋衣。
    • 急救包:有 400g,感觉太重了,下次可以精简一下。
    • 牧高笛雨披天幕:至少在香港用这东西,有点太热了,而且我买的这个没有腰带,背部也没有加长,现在看应该买三峰出的那个会好很多。
  • 这次没带,但发觉应该补充的装备
    • 能放水杯、证件、手机的腰包:背包腰封会挡住裤子口袋,腰封的口袋也没法放手机,这个时候腰包就非常有用了。
    • 驱蚊水:驱蚊驱虫,一是防止叮咬,二是防止蚊虫进入帐篷,我还挺怕小虫子的。
    • 湿巾:一些营地淡水不太好获得(比如水浪窝营地,我这次是用了水袋里的饮水擦身体…),这时候湿巾就非常有用了,可以用来简单清理下身体。
    • 短款雪套:防止各种碎石、树枝、草叶进入鞋子,保护脚部。用短款是因为紧身运动裤已经提供了保护腿部的功能,长款雪套就显得多余了(下雨另当别论)。

TODOs:

  • 需要练习下雨披 + 背包的使用方式,这次路上下雨点,手忙脚乱地穿上,搞半天都盖不住背包。
  • 学习下雨天如何快速收拾帐篷,这次收拾都花了半个多小时,要是下大雨大概帐篷跟东西得全搞湿了。
  • 如何选择扎营地点?这次选在了树下的草地,结果恰好是少有人扎营的一块,虫子特别多(毛毛虫、跳蚤、蚂蚁、蜘蛛,等等…)。
  • 登山杖的手柄系带如何拆卸清洗,以及如何调整长短。走完一趟系带都被汗水浸湿了,而且有点长了。
  • 再复习一遍登山杖使用方法,这次发现登山杖起了大用,而且经过一天的高强度使用,经验也丰富了,现在再回头补充下理论知识,应该会有更深的体会。
  • 学习伞绳、急救包的使用方法,经常徒步的话,出了意外不会用就尴尬了。

现在已经爱上了徒步这种运动方式,花钱折磨自己毫不手软(

如下是我近期发现的一些高质量徒步相关资料,对我有挺大帮助:

OS as Code - 我的 NixOS 使用体会

2024-02-21 16:26:21

本文最初发表于 如何评价NixOS? - 知乎,觉得比较有价值所以再搬运到我的博客。

我 23 年 4 月开始用 NixOS 之前看过(如何评价NixOS? - 知乎) 这个问答,几个高赞回答都从不同方面给出了很有意义的评价,也是吸引我入坑的原因之一。

现在是 2024 年 2 月,距离我入坑 NixOS 刚好 10 个月,我当初写的新手笔记已经获得了大量好评与不少的赞助,并成为了整个社区最受欢迎的入门教程之一。自 2023 年 6 月我为它专门创建一个 GitHub 仓库与单独的文档站点以来,它已经获得了 1189 个 stars,除我之外还有 37 位读者给它提了 PR:

NixOS & Flakes Book

那么作为一名已经深度使用 NixOS 作为主力桌面系统接近 10 个月的熟手,我在这里也从另一个角度来分享下我的入坑体会。

注意,这篇文章不是 NixOS 入门教程,想看教程请移步上面给的链接。

先澄清下一点,NixOS 的包非常多,Repository statistics 的包仓库统计数据如下:

Repository statistics

上面这个 Nixpkgs 的包数量确实有挺多水分——Nixpkgs 还打包了许多编程语言的 Libraries(貌似挺多 Haskell 人用 nix 当语言包管理器用),比如Haskell Packages(18000+),R Packages(27000+),Emacs Packages(6000+) ,但即使把它们去掉后 Nixpkgs 的包数量也有大约 40000+,虽然逊色于 AUR,但这个数量再怎么算也跟「包太少」这个描述扯不上关系。

包仓库这里也是 NixOS 跟 Arch 不太同的地方,Arch 的官方包仓库收录很严格,相对的 AUR 生态相当繁荣。但任何人都能往 AUR 上传内容,虽然有一个投票机制起到一定审核作用,但个人感觉这个限制太松散了。

而 NixOS 就很不一样了,它的官方包仓库 Nixpkgs 很乐于接受新包,想为 Nixpkgs 提个 PR 加包或功能相对其他发行版而言要简单许多,这是 Nixpkgs 的包数量这么多的重要原因(GitHub 显示 Nixpkgs 有 5000+ 历史贡献者,这很夸张了)。

Nixpkgs 仓库的更新流程相对 AUR 也严格许多,PR 通常都需要通过一系列的 GitHub Actions 测试 + Maintainer Review + Ofborg 检查与自动构建测试后才能被合并,Nixpkgs 也鼓励维护者为自己的包添加测试(包的 doCheck 默认为 true),这些举措都提升了 Nixpkgs 的包质量。

NixOS 其实也有个与 AUR(Arch User Repository) 类似的 NUR(Nix User Repository),但因为 Nixpkgs 的宽松,NUR 反而没啥内容。

举例来说,QQ 能直接从 Nixpkgs 官方包仓库下载使用,而在 Arch 上你得用 AUR 或者 archlinux-cn.

这算是各有优势吧。NixOS 被人喷包少,主要是因为它不遵循 FHS 标准,导致大部分网上下载的 Linux 程序都不能直接在 NixOS 上运行。这当然有解决方案,我建议是首先看看 Nixpkgs 中是否已经有这个包了,有的话直接用就行。如果没有,再尝试一些社区的解决方案,或者自己给打个包。

用 NixOS 的话自己打包程序是不可避免的,因为即使 Nixpkgs 中已经有了这么多包,但它仍然不可能永远 100% 匹配你的需求,总有你想用但 Nixpkgs 跟 NUR 里边都没有的包,在 NixOS 上你常常必须要给你的包写个打包脚本,才能使它在 NixOS 上正常运行。

另外即使有些程序本身确实能在 NixOS 上无痛运行,但为了做到可复现,NixOS 用户通常也会选择自己手动给它打个包。

OK,闲话说完,下面进入正题。

首先,NixOS 比传统发行版复杂很多,也存在非常多的历史遗留问题。

举例来说,它的官方文档烂到逼得我一个刚学 NixOS 的新手自己边学边写入门文档。在我用自己的渣渣英语把笔记翻译了一遍发到 reddit (NixOS & Nix Flakes - A Guide for Beginners) 后,居然还获得了许多老外的大量好评(经过这么长时间的持续迭代,现在甚至已经变成了社区最受欢迎的新手教程之一),这侧面也说明官方文档到底有多烂。

NixOS 值不值得学或者说投入产出比是否够高?在我看来,这归根结底是个规模问题

这里的规模,一是指你对 Linux 系统所做的自定义内容的规模,二是指你系统更新的频繁程度,三是指你 Linux 机器的数量

下面我从个人经历的角度来讲下我以前用 Arch Linux、Ubuntu 等传统发行版的体验,以及我为什么选择了 NixOS,NixOS 又为我带来了什么样的改变。

举个例子,以前我用 Deepin Ubuntu 时我基本没对系统做过什么深入定制,一是担心把系统弄出问题修复起来头疼,二是如果不额外写一份文档或脚本记录下步骤的话,我做的所有定制都是黑盒且不可迁移的,一个月后我就全忘了,只能战战兢兢地持续维护这个随着我的使用而越来越黑盒、状态越来越混沌的系统。

如果用的是 Arch 这种滚动发行版还好,系统一点点增量更新,遇到的一般都是小问题。而对 Ubuntu Deepin 这种,原地升级只出小问题是很少见的,这基本就意味着我必须在某个时间点,在新版本的 Ubuntu 上把我以前做过的定制再全部重做一遍,更关键的是,我非常有可能已经忘了我以前做了什么,这就意味着我得花更多的时间去研究我的系统环境里到底都有些啥东西,是怎么安装配置的,这种重复劳动非常痛苦。

总之很显然的一点是,我对系统做的定制越多越复杂,迁移到新版本的难度就越大。

我想也正是因为这一点,Arch、Gentoo、Fedora 这种滚动发行版才在 Linux 爱好者圈子中如此受欢迎,喜欢定制自己系统的 Linux 用户也大都使用这类滚动发行版。

那么 Arch、Fedora 就能彻底解决问题了么?显然并不是。首先它们的更新频率比较高,这代表着你会更容易把你的系统搞出点毛病来。当然这其实是个小问题,现在 Linux 社区谁还没整上个 btrfs / zfs 文件系统快照啊,出问题回滚快照就行。它们最根本的问题是:

  1. 你的 Arch 系统环境、文件系统快照、或者虚拟机快照,它们仍然是个黑盒,仍然会随着你的持续使用而越来越混沌,也并不包含如何从零构建这个环境的「知识」,是不可解释的。
    • 我在工作中就见到过一些「祖传虚拟机快照」或「祖传云服务器快照」,没人知道这个环境是怎么搭建的,每一任接手的人都只能继续往上叠 Buff,然后再把这个定时炸弹传给下一任。这就像那个轮流往一个水杯里加水的游戏,最后在谁加水的时候溢出来了,那就算他倒霉。
  2. Arch 实质要求你持续跟着它的更新走,这意味着你必须要持续更新维护它。
    • 如果你把机器放了一年半载跑得很稳定,然后你想要更新一下,那出问题的风险会相当高。如果你因此而决定弄台最新版本的 Arch 机器再把旧环境还原出来,那就又回到了之前的问题——你得想办法从旧环境中还原出你的定制流程,这也不是个好差事。
  3. 快照与当前硬件环境强相关,直接在不同硬件的机器上使用很容易遇到各种奇怪的问题,也就是说这东西不可迁移
  4. 快照是一堆庞大的二进制文件,它的体积非常大,这使得备份与分享它的成本高昂。

Docker 能解决上述问题中的一部分。首先它的容器镜像可由 Dockerfile 完全描述,也就是说它是可解释的,此外容器镜像能在不同环境中复现出完全一致的环境,这表明它是可迁移的。对于服务器环境,将应用程序全都跑在容器中,宿主机只负责跑容器,这种架构使得你只需要维护最基础的系统环境,以及一些 Dockerfile 跟 yaml 文件,这极大地降低了系统的维护成本,从而成为了 DevOps 的首选。

但 Docker 容器技术是专为应用程序提供一致的运行环境而设计的,在虚拟机、桌面环境等场景下它并不适用(当然你非要这么弄也不是不行,很麻烦就是了)。此外 Dockerfile 仍旧依赖你所编写的各种脚本、命令来构建镜像,这些脚本、命令都需要你自己维护,其运行结果的可复现能力也完全看你自己的水平。

如果你因为这些维护难题而选择极简策略——尽可能少地定制任何桌面系统与虚拟机环境,能用默认的就用默认——这就是换到 NixOS 之前的我。为了降低系统维护难度,我以前使用 Deepin Manjaro EndeavourOS 的过程中,基本没对系统配置做任何大变动。作为一名 SRE/DevOps,我在工作中就已经踩了够多的环境问题的坑,写腻写烦各种安装脚本、Ansible 配置了,业余完全不想搞这些幺蛾子。

但如果你是个喜欢定制与深入研究系统细节的极客,随着你对系统所做的定制越来越多,越来越复杂, 或者你 Homelab 与云上的 Linux 机器越来越多,你一定会在某个时间点开始编写各种部署流程的文档、部署脚本或使用一些自动化工具帮自己完成一些繁琐的工作。

文档就不用说了,这个显然很容易过时,没啥大用。如果你选择自己写自动化脚本或选用自动化工具, 它的配置会越来越复杂,而且系统更新经常会破坏掉其中一些功能,需要你手动修复。此外它还高度依赖你当前的系统环境,当你某天装了台新机器然后信心满满地用它部署环境时,大概率会遇到各种环境不一致导致的错误需要手动解决。还有一点是,你写的脚本大概率并没有仔细考量抽象、模块化、错误处理等内容,这也会导致随着规模的扩大,维护它变得越来越痛苦。

然后你发现了 NixOS,它有什么声明式的配置,你仔细看了下它的实现,哦这声明式的配置,不就是把一堆 bash 脚本封装了下,对用户只提供了一套简洁干净的 api 么,它实际干的活不跟我自己这几年写的一堆脚本一模一样?好像没啥新鲜的。

嗯接着你试用了一下,发现 NixOS 的这套系统定制脚本都存在一个叫 Nixpkgs 的仓库中,有数千人在持续维护它,几十年积累下来已经拥有了一套非常丰富、也比较稳定的声明式抽象、模块系统、类型系统、专为这套超大型的软件包仓库与 NixOS 系统配置而开发的大规模 CI 系统 Hydra、以及逐渐形成的能满足数千人协作更新这套复杂配置的社区运营模式。

你立马学习 nix 语言,然后动手把这套维护了 N 年的脚本改写成 NixOS 配置。

越写就对它越满意,改造后的配置缩水了相当多,维护难度直线下降。

很大部分以前自己用各种脚本跟工具实现的功能,都被 Nixpkgs 封装好了,只需要 enable 一下再传几个关键参数,就能无痛运行。nixpkgs 中的脚本都有专门的 maintainer 维护更新,任何发现了问题的用户也可以提个 PR 修下问题,在没经过 CI 与 staging unstable 等好几个阶段的广泛验证前,更新也不会进入 stable.

上面所说的你,嗯就是我自己。

现在回想下我当初就为了用 systemd 跑个简单的小工具而跟 systemd 疯狂搏斗的场景,泪目… 要是我当初就懂 NixOS…

有过一定编程经验的人都应该知道抽象与模块化的重要性,复杂程度越高的场景,抽象与模块化带来的收益就越高。Terraform、Kubernetes 甚至 Spring Boot 的流行都体现了这一点。NixOS 的声明式配置也是如此,它将底层的实现细节都封装起来了,并且这些底层封装大都有社区负责更新维护,还有 PR Review、CI 与多阶段的测试验证确保其可靠性,这极大地降低了我的心智负担,从而解放了我的生产力。它的可复现能力则免除了我的后顾之忧,让我不再担心搞坏系统。

NixOS 构建在 Nix 函数式包管理器这上,它的设计理念来自 Eelco Dolstra 的论文 The Purely Functional Software Deployment Model(纯函数式软件部署模型),纯函数式是指它没有副作用, 就类似数学函数 $y = f(x)$,同样的 NixOS 配置文件(即输入参数 $x$ )总是能得到同样的 NixOS 系统环境(即输出 $y$)。

这也就是说 NixOS 的配置声明了整个系统完整的状态,OS as Code

只要你 NixOS 系统的这份源代码没丢,对它进行修改、审查,将源代码分享给别人,或者从别人的源代码中借鉴一些自己想要的功能,都是非常容易的。你简单的抄点其他 NixOS 用户的系统配置就能很确定自己将得到同样的环境。相比之下,你抄其他 Arch/Ubuntu 等传统发行版用户的配置就要麻烦的多,要考虑各种版本区别、环境区别,不确定性很高。

NixOS 的入门门槛相对较高,也不适合从来没接触过 Linux 与编程的小白,这是因为它的设计理念与传统 Linux 发行版有很大不同。但这也是它的优势所在,跨过那道门槛,你会发现一片新天地。

举例来说,NixOS 用户翻 Nixpkgs 中的实现源码实际是每个用户的基本技能,给 Nixpkgs 提 PR 加功能、加包或者修 Bug 的 NixOS 用户也相当常见这既是使新用户望而却步的拦路之虎,同时也是给选择了 NixOS 的 Linux 用户提供的进阶之梯

想象下大部分 Arch 用户(比如以前的我)可能用了好几年 Arch,但根本不了解 Arch 底层的实现细节,没打过自己的包。而 NixOS 能让翻源码成为常态,实际也说明理解它的实现细节并不难。我从两个方面来说明这一点。

第一,Nix 是一门相当简单的语言,语法规则相当少,比 Java Python 这种通用语言简单了太多。因此有一定编程经验的工程师能花两三个小时就完整过一遍它的语法。再多花一点时间,读些常见 Nix 代码就没啥难度了。

第二,NixOS 良好的声明式抽象与模块化系统,将 OS 分成了许多层来实现,使用户在使用过程中, 既可以只关注当前这一层抽象接口,也可以选择再深入到下一层抽象来更自由地实现自己想要的功能这种选择的权利,实际也给了用户机会去渐进式地理解 NixOS 本身)。举例来说,新手用户只要懂最上层的抽象就正常使用 NixOS。当你有了一点使用经验,想实现些自定义需求,挖下深挖一层抽象(比如说直接通过 systemd 的声明式参数自定义一些操作)通常就足够了。如果你已经是个 NixOS 熟手,想更极客一点,就可以再继续往下挖。

总之因为上面这两点,理解 Nixpkgs 中的源码或者使用 Nix 语言自己打几个包并不难,可以说每个有一定经验的 NixOS 用户同时也会是 NixOS 打包人。

我们看了许多人提到 NixOS 的优点,上面我也提到了不少。Nix 的圈外人听得比较多的可能主要是它解决了依赖冲突问题,能随时回滚,强大的可复现能力。如果你有实际使用过 NixOS,那你也应该知道 NixOS 的这些优势:

  1. NixOS 的 Flakes 特性使你能将系统锁定在一个特定的状态,你可以在任何想更新的时候才更新它,即使有个一年半载不更新也完全没毛病。NixOS 不会强迫你频繁更新系统,你可以选择是否这么做。因为系统的状态可以完全从你的 NixOS 配置中推断出来,所以从旧版本升级到最新版本也容易很多。
    1. 有的选总是好的,我不喜欢被强迫频繁更新(即使我实际更新还挺频繁的),公司里的系统管理员或者 DevOps 就更是如此了。
  2. 系统更新具有类似数据库事务的原子化特性,这意味着你的系统更新要么成功要么失败,(一般) 不会出现中间状态。
  3. NixOS 的声明式配置实际实现了 OS as Code,这使得这些配置非常便于分享。直接在 GitHub 上从其他 NixOS 用户那里 Copy 需要的代码到你的系统配置中,你就能得到一个一模一样的功能。新手用户也能很容易地从别人的配置中学到很多东西。
    1. 这也是近几年 GitHub 与 reddit r/unixporn 上使用 NixOS 做桌面 ricing 的用户越来越多的原因。
  4. 声明式配置为用户提供了高度便捷的系统自定义能力,通过改几行配置,就可以快速更换系统的各种组件。
  5. 等等

这些都是 NixOS 的卖点,其中一些特性现在在传统发行版上也能实现,Fedora Silverblue 等新兴的不可变发行版也在这些方面有些不错的创新。但能解决所有这些问题的系统,目前只有 NixOS(以及更小众的 Guix. 据Guix 的 README 所言,它同样基于 Nix 包管理器)。

自 NixOS 项目创建至今二十多年来,Nix 包管理器与 NixOS 操作系统一直是非常小众的技术,尤其是在国内,知道它们存在的人都是少数 Linux 极客,更别说使用它们了。

NixOS 很特殊,很强大,但另一方面它也有着相当多的历史债务,比如说:

  1. 文档混乱不说人话
  2. Flakes 特性使 NixOS 真正满足了它一直宣称的可复现能力,但从 2021 年正式发布到现在 2024 年,它仍旧处在实验状态。
  3. Nix 的 CLI 处在换代期,新版本的 CLI 优雅很多,但其实现目前与 Flakes 特性强绑定,导致两项功能都难以 stable,甚至还阻碍了许多其他特性的开发工作。
  4. 模块系统的缺陷与 Nix 错误处理方面的不足,导致长期以来它的报错信息相当隐晦,令人抓狂
  5. Nix 语言太过简单导致 Nixpkgs 中大量使用 Bash 脚本,以及 Nix 语言的大多数特性都完全是使用 C++ 实现的,从 Nix 语言的角度看这很黑盒。
  6. NixOS 的大量实现细节隐藏在 Nixpkgs 源码中,比如说软件包的分类、derivation 有哪些属性可被 override。
    • Nixpkgs 长期一直使用文件夹来对软件包进行分类,没有任何查看源码之外的手段来分类查询其中的软件包。
    • Nixpkgs 中的所有 derivation 相关信息,目前也只能通过查看源码来了解。
  7. https://nixos.wiki 站点维护者跑路,官方又长期未提供替代品,导致 NixOS 的文档在本来就很烂的基础上又雪上加霜。
  8. Nix/NixOS 近来快速增长的用户群体,使得它的社区运营模式也面临着挑战

这一堆历史债是 NixOS 一直没能得到更广泛使用的主要原因。但这些问题也是 NixOS 未来的机会,社区目前正在积极解决这些问题,我很期待看到这些问题被解决后, NixOS 将会有怎样的发展。

谁也不会对一项没前途的技术感兴趣,那么 NixOS 的未来如何呢?我是否看好它?这里我尝试使用一些数据来说明我对 NixOS 的未来的看法。

首先看 Nixpkgs 项目,它存储了 NixOS 所有的软件包及 NixOS 自身的实现代码:

上图能看到从 2021 年开始 Nixpkgs 项目的活跃度开始持续上升,Top 6 贡献者中有 3 位都是 2021 年之后开始大量提交代码,你点进 GitHub 看,能看到 Top 10 贡献者中有 5 位都是 2021 年之后加入社区的(新增的 @NickCao 与 @figsoda 都是 NixOS 中文社区资深用户)。

再看看 Nix 包管理器的提交记录,它是 NixOS 的底层技术:

上图显示 Nix 项目的活跃度在 2020 年明显上升,Top 6 贡献者中有 5 位都是在 2020 年之后才开始大量贡献代码的。

再看看 Google Trends 中 NixOS 这个关键词的搜索热度:

这个图显示 NixOS 的搜索热度有几个明显的上升时间点:

  1. 2021 年 12 年
    • 这大概率是因为在 2021 年 11 月Nix 2.4 发布了,它带来了实验性的 Flakes 特性与新版 CLI,Flakes 使得 NixOS 的可复现能力得到了极大的提升,新 CLI 也更符合用户直觉。
  2. 2023 年 6 月
    • 最重要的原因应该是,Youtube 上 Linux 相关的热门频道在这个时间点推出了好几个关于 NixOS 的视频,截至 2024-02-23,Youtube 上播放量最高的三个 NixOS 相关视频都是在 2023-06 ~ 2023-07 这个时间段推出的,它们的播放量之和超过了 130 万。
    • China 的兴趣指数在近期最高,这可能是因为国内的用户群一直很少,然后我在 6 月份发布了NixOS 与 Flakes - 一份非官方的新手指南, 并且在 科技爱好者周刊 等渠道做了些推广,导致 NixOS 的相对指数出现明显上升。
  3. 2024 年 1 月
    • 这个我目前不太确定原因。

再看看 Nix/NixOS 社区从 2022 年启用的年度用户调查。

  1. 2022 Nix Survey Results, 根据其中数据计算可得出:
    • 74.5% 的用户是在三年内开始使用 Nix/NixOS 的。
    • 关于如何拓展 Nixpkgs 的调查中,36.7% 的用户使用 Flakes 特性拓展 Nixpkgs,仅次于传统的 overlays.
  2. Nix Community Survey 2023 Results, 简单计算可得出,
    • 54.1% 的用户是在三年内开始使用 Nix/NixOS 的。
    • 关于如何拓展 Nixpkgs 的调查中,使用 Flakes 特性的用户占比为 49.2%,超过了传统的 Overlays.
    • 关于实验特性的调查中,使用 Flakes 特性的用户占比已经达到了 59.1%.

2024-04-12 更新:NixCon 2024 也有一个演讲提供了 Nix 社区的各种历史数据:Nix, State of the Union - NixCon 2024

另外 GitHub 的Octoverse 2023 也难得地提了一嘴 Nixpkgs:

Developers see benefits to combining packages and containerization. As we noted earlier, 4.3 million repositories used Docker in 2023.
> On the other side of the coin, Linux distribution NixOS/nixpkgs has been on the top list of open source projects by contributor for the last two years.

这些数据与我们前面提到的 Nixpkgs 与 Nix 项目的活跃度相符,都显示 Nix/NixOS 社区在 2021 年之后开始迅速增长壮大。

结合上面这些数据看,我对 NixOS 的未来持很乐观的态度。

从决定入坑 NixOS 到现在,短短 10 个月,我在 Linux 上取得的收获远超过去三年。我已经在 PC 上尝试了非常多的新技术新工具,我的 Homelab 内容也丰富了非常多(我目前已经有了十多台 NixOS 主机),我对 Linux 系统结构的了解也越来越深刻。

光是这几点收获,就完全值回票价了,欢迎入坑 NixOS~

个人数据安全不完全指南

2024-01-30 13:48:30

在接触电脑以来很长的一段时间里,我都没怎么在意自己的数据安全。比如说:

  1. 长期使用一个没有 passphrase 保护的 SSH 密钥(RSA 2048 位),为了方便我还把它存到了 onedrive 里,而且在各种需要访问 GitHub/Gitee 或 SSH 权限的虚拟机跟 PC 上传来传去。
  2. Homelab 跟桌面 PC 都从来没开过全盘加密。
  3. 在 2022 年我的 Homelab 坏掉了两块国产固态硬盘(阿斯加特跟光威弈 Pro 各一根),都是系统一启动就挂,没法手动磁盘格式化,走售后直接被京东换货了。因为我的数据是明文存储的,这很可能导致我的个人数据泄露…
  4. 几个密码在各种站点上重复使用,其中重要账号的随机密码还是我在十多年前用 lastpass 生成的,到处用了这么多年,很难说这些密码有没有泄露(lastpass 近几年爆出的泄漏事故就不少…)
  5. GitHub, Google, Jetbrains 等账号的 Backup Code 被我明文存储到了百度云盘,中间发现百度云盘安全性太差又转存到了 OneDrive,但一直是明文存储,从来没加过密。
  6. 一些银行账号之类的随机密码,因为担心遗忘,长期被我保存在一份印象笔记的笔记里,也是明文存储,仅做了些简单的内容替换,要猜出真正的密码感觉并不是很难。
  7. 以前也有过因为对 Git 操作不熟悉或者粗心大意,在公开仓库中提交了一些包含敏感信息的 commit,比如说 SSH 密钥、密码等等,有的甚至很长时间都没发现。

现在在 IT 行业工作了几年,从我当下的经验来看,企业后台的管理员如果真有兴趣,查看用户的数据真的是很简单的一件事,至少国内大部分公司的用户数据,都不会做非常严格的数据加密与权限管控。就算真有加密,那也很少是用户级别的,对运维人员或开发人员而言这些数据仍旧与未加密无异。对系统做比较大的迭代时,把小部分用户数据导入到测试环境进行测试也是挺常见的做法…

总之对我而言,这些安全隐患在过去并不算大问题,毕竟我 GitHub, Google 等账号里也没啥重要数据,银行卡里也没几分钱。

但随着我个人数据的积累与在 GitHub, Google 上的活动越来越多、银行卡里 Money 的增加(狗头),这些数据的价值也越来越大。比如说如果我的 GitHub 私钥泄漏,仓库被篡改甚至删除,以前我 GitHub 上没啥数据也没啥 stars 当然无所谓,但现在我已经无法忍受丢失 GitHub 两千多个 stars 的风险了。

在 2022 年的时候我因为对区块链的兴趣顺便学习了一点应用密码学,了解了一些密码学的基础知识, 然后年底又经历了几次可能的数据泄漏,这使我意识到我的个人数据安全已经是一个不可忽视的问题。因此,为了避免 GitHub 私钥泄漏、区块链钱包助记词泄漏、个人隐私泄漏等可能,我在 2023 年 5 月做了全面强化个人数据安全的决定,并在 0XFFFF 社区发了篇帖子征求意见——学习并强化个人的数据安全性(持续更新)

现在大半年过去,我已经在个人数据安全上做了许多工作,目前算是达到了一个比较不错的状态。

我的个人数据安全方案,有两个核心的指导思想:

  1. 零信任:不信任任何云服务提供商、本地硬盘、网络等的可靠性与安全性,因此任何数据的落盘、网络传输都应该加密,任何数据都应该有多个副本(本地与云端)。
    1. 基于这一点,应该尽可能使用经过广泛验证的开源工具,因为开源工具的安全性更容易被验证, 也避免被供应商绑架。
  2. Serverless: 尽可能利用已有的各种云服务或 Git 之类的分布式存储工具来存储数据、管理数据版本。
    1. 实际上我个人最近三四年都没维护过任何个人的公网服务器,这个博客以及去年搭建的 NixOS 文档站全都是用的 Vercel 免费静态站点服务,各种数据也全都优先选用 Git 做存储与版本管理。
    2. 我 Homelab 算力不错,但每次往其中添加一个服务前,我都会考虑下这是否有必要,是否能使用已有的工具完成这些工作。毕竟跑的服务越多,维护成本越高,安全隐患也越多。

这篇文章记录下我所做的相关调研工作、我在这大半年的实践过程中逐渐摸索出的个人数据安全方案以及未来可能的改进方向。

注意这里介绍的并不是什么能一蹴而就获得超高安全性的傻瓜式方案,它需要你需要你有一定的技术背景跟时间投入,是一个长期的学习、实践与方案迭代的过程。另外如果你错误地使用了本文中介绍的工具或方案,可能反而会降低你的数据安全性,由此产生的任何损失与风险皆由你自己承担。

数据安全大概有这些方面:

  1. 保障数据不会泄漏——也就是加密
  2. 保障数据不会丢失——也就是备份

就我个人而言,我的数据安全主要考虑以下几个部分:

  1. SSH 密钥管理
  2. 各种网站、APP 的账号密码管理
  3. 灾难恢复相关的数据存储与管理
    1. 比如说 GitHub, Twitter, Google 等重要账号的二次认证恢复代码、账号数据备份等,日常都不需要用到,但非常重要,建议离线加密存储
  4. 需要在多端访问的重要个人数据
    1. 比如说个人笔记、图片、视频等数据,这些数据具有私密性,但又需要在多端访问。可借助支持将数据加密存储到云端的工具来实现
  5. 个人电脑與 Homelab 的数据安全与灾难恢复
    1. 我主要使用 macOS 与 NixOS,因此主要考虑的是这两个系统的数据安全与灾难恢复

下面就分别就这几个部分展开讨论。

硬件密钥的好处是可以防止密钥泄漏,但 YubiKey 在国内无官方购买渠道,而且价格不菲,只买一个 YubiKey 的话还存在丢失的风险。

另一方面其实基于现代密码学算法的软件密钥安全性对我而言是足够的,而且软件密钥的使用更加方便。或许在未来,我会考虑使用canokey-coreOpenSKsolokey 等开源方案 DIY 几个硬件密钥,但目前我并不觉得有这必要。

我们一般都是直接使用 ssh-keygen 命令生成 SSH 密钥对,OpenSSH 目前主要支持两种密钥算法:

  1. RSA: 目前你在网上看到的大部分教程都是使用的 RSA 2048 位密钥,但其破解风险在不断提升,目前仅推荐使用 3072 位及以上的 RSA 密钥。
  2. ED25519: 这是密码学家 Dan Bernstein 设计的一种新的签名算法,其安全性与 RSA 3072 位密钥相当,但其签名速度更快,且密钥更短,因此目前推荐使用 ED25519 密钥。

RSA 跟 ED25519 都是被广泛使用的密码学算法,其安全性都是经过严格验证的,因此我们可以放心使用。但为了在密钥泄漏的情况下,能够尽可能减少损失,强烈建议给个人使用的密钥添加 passphrase 保护。

那这个 passphrase 保护到底有多安全呢?

有一些密码学知识的人应该知道,pssphrase 保护的实现原理通常是:通过 KDF 算法(或者叫慢哈希算法、密码哈希算法)将用户输入的 passphrase 字符串转换成一个二进制对称密钥,然后再用这个密钥加解密具体的数据。

因此,使用 pssphrase 加密保护的 SSH Key 的安全性,取决于:

  1. passphrase 的复杂度,这对应其长度、字符集、是否包含特殊字符等。这由我们自己控制。
  2. 所使用的 KDF 算法的安全性。这由 OpenSSH 的实现决定。

那么,OpenSSH 的 passphrase 是如何实现的?是否足够安全?

我首先 Google 了下,找到一些相关的文章(注意如下文章内容与其时间点相关,OpenSSH 的新版本会有些变化):

OpenSSH release notes 中搜索 passphrase 跟 kdf 两个关键字,找到些关键信息如下:

text

OpenSSH 9.4/9.4p1 (2023-08-10)

 * ssh-keygen(1): increase the default work factor (rounds) for the
   bcrypt KDF used to derive symmetric encryption keys for passphrase
   protected key files by 50%.

----------------------------------

OpenSSH 6.5/6.5p1 (2014-01-30)

 * Add a new private key format that uses a bcrypt KDF to better
   protect keys at rest. This format is used unconditionally for
   Ed25519 keys, but may be requested when generating or saving
   existing keys of other types via the -o ssh-keygen(1) option.
   We intend to make the new format the default in the near future.
   Details of the new format are in the PROTOCOL.key file.

所以从 2014 年发布的 OpenSSH 6.5 开始,ed25519 密钥的 passphrase 才是使用 bcrypt KDF 生成的。而对于其他类型的密钥,仍旧长期使用基于 MD5 hash 的密钥格式,没啥安全性可言。

即使 2023-08-10 发布的 9.4 版本增加了默认的 bcrypt KDF rounds 次数,它的安全性仍然很值得怀疑。bcrypt 本身的安全性就越来越差,现代化的加密工具基本都已经升级到了 scrypt 甚至 argon2. 因此要想提升安全性,最好是能更换更现代的 KDF 算法,或者至少增加 bcrypt KDF 的 rounds 数量。

我进一步看了 man ssh-keygen 的文档,没找到任何修改 KDF 算法的参数,不过能通过 -a 参数来修改 KDF 的 rounds 数量,OpeSSh 9.4 的 man 信息中写了默认使用 16 rounds.

考虑到大部分人都使用默认参数生成 Key,而且绝大部分用户都没有密码学基础,大概率不知道 KDF、Rounds 是什么意思,我们再了解下 ssh-keygen 默认参数。在 release note 中我进一步找到这个:

text

OpenSSH 9.5/9.5p1 (2023-10-04)

Potentially incompatible changes
--------------------------------

 * ssh-keygen(1): generate Ed25519 keys by default. Ed25519 public keys
   are very convenient due to their small size. Ed25519 keys are
   specified in RFC 8709 and OpenSSH has supported them since version 6.5
   (January 2014).

也就是说从 2023-10-04 发布的 9.5 开始,OpenSSH 才默认使用 ED25519。

结合上面的分析可以推断出,目前绝大部分用户都是使用的 RSA 密钥,且其 passphrase 的安全性很差,不加 passphrase 就是裸奔,加了也很容易被破解。如果你使用的也这种比较老的密钥类型,那千万别觉得自己加了 passphrase 保护就很安全,这完全是错觉(

即使是使用最新的 ssh-keygen 生成的 ED25519 密钥,其默认也是用的 bcrypt 16 rounds 生成加密密钥,其安全性在我看来也是不够的。

总结下,在不考虑其他硬件密钥/SSH CA 的情况下,最佳的 SSH Key 生成方式应该是:

bash

ssh-keygen -t ed25519 -a 256 -C "xxx@xxx"

rounds 的值根据你本地的 CPU 性能来定,我在 Macbook Pro M2 上测了下,64 rounds 大概是 0.5s,128 rounds 大概需要 1s,256 rounds 大概 2s,用时与 rounds 值是线性关系。

考虑到我的个人电脑性能都还挺不错,而且只需要在每次重启电脑后通过 ssh-add ~/.ssh/xxx 解锁一次,后续就一直使用内存中的密钥了,一两秒的时间还是可以接受的,因此我将当前使用的所有 SSH Key 都使用上述参数重新生成了一遍。

在所有机器上使用同一个 SSH 密钥,这是我过去的做法,但这样做有几个问题:

  1. 一旦某台机器的密钥泄漏,那么就需要重新生成并替换所有机器上的密钥,这很麻烦。
  2. 密钥需要通过各种方式传输到各个机器上,这也存在泄漏的风险。

因此,我现在的做法是:

  1. 对所有桌面电脑跟笔记本,都在其本地生成一个专用的 SSH 密钥配置到 GitHub 跟常用的服务器上。这个 SSH 私钥永远不会离开这台机器
  2. 对于一些相对不重要的 Homelab 服务器,额外生成一个专用的 SSH 密钥,配置到这些服务器上。在一些跳板机跟测试机上会配置这个密钥方便测试与登录到其他机器。
  3. 上述所有 SSH 密钥都添加了 passphrase 保护,且使用了 bcrypt 256 rounds 生成加密密钥。

我通过这种方式缩小了风险范围,即使某台机器的密钥泄漏,也只需要重新生成并替换这台机器上的密钥即可。

搜到些资料:

TODO 待研究。

我曾经大量使用了 Chrome/Firefox 自带的密码存储功能,但用到现在其实也发现了它们的许多弊端。有同事推崇 1Password 的使用体验,它的自动填充跟同站点的多密码管理确实做得非常优秀,但一是要收费,二是它是商业的在线方案,基于零信任原则,我不太想使用这种方案。

作为开源爱好者,我最近找到了一个非常适合我自己的方案:password-store.

这套方案使用 gpg 加密账号密码,每个文件就是一个账号密码,通过文件树来组织与匹配账号密码与 APP/站点的对应关系,并且生态完善,对 firefox/chrome/android/ios 的支持都挺好。

缺点是用 GPG 加密,上手有点难度,但对咱来说完全可以接受。

我在最近使用 pass-import 从 firefox/chrome 中导入了我当前所有的账号密码,并对所有的重要账号密码进行了一次全面的更新,一共改了二三十个账号,全部采用了随机密码。

当前的存储同步与多端使用方式:

  1. pass 的加密数据使用 GitHub 私有仓库存储,pass 原生支持基于 Git 的存储方案。
    1. 因为数据全都是使用 ECC Curve 25519 GPG 加密的,即使仓库内容泄漏,数据的安全性仍然有保障。
  2. 在浏览器与移动端,则分别使用这些客户端来读写 pass 中的密码:
    1. Android: https://github.com/android-password-store/Android-Password-Store
    2. IOS: https://github.com/mssun/passforios
    3. Brosers(Chrome/Firefox): https://github.com/browserpass/browserpass-extension
  3. 基於雞蛋不放在同一個籃子裏的原則,otp/mfa 的動態密碼則使用 google authenticator 保存與多端同步,並留有一份離線備份用於災難恢復。登錄 Google 賬號目前需要我 Android 手機或短信驗證,因此安全性符合我的需求。

我的详细 pass 配置见ryan4yin/nix-config/password-store.

其他相关资料:

遇到过的一些问题与解法:

GnuPG 是一个很有历史,而且使用广泛的加密工具,但它的安全性如何呢?

我找到些相关文档:

简单总结下,GnuPG 的每个 secret key 都是随机生成的,互相之间没有关联(即不像区块链钱包那样具有确定性)。生成出的 key 被使用 passphrase 加密保存,每次使用时都需要输入 passphrase 解密。

那么还是之前在调研 OpenSSH 时我们提到的问题:它使用的 KDF 算法与参数是否足够安全?

OpenPGP 标准定义了String-to-Key (S2K) 算法用于从 passphrase 生成对称加密密钥,GnuPG 遵循该规范,并且提供了相关的参数配置选项,相关参数的文档OpenPGP protocol specific options 内容如下:

text

--s2k-cipher-algo name

    Use name as the cipher algorithm for symmetric encryption with a passphrase if --personal-cipher-preferences and --cipher-algo are not given. The default is AES-128.
--s2k-digest-algo name

    Use name as the digest algorithm used to mangle the passphrases for symmetric encryption. The default is SHA-1.
--s2k-mode n

    Selects how passphrases for symmetric encryption are mangled. If n is 0 a plain passphrase (which is in general not recommended) will be used, a 1 adds a salt (which should not be used) to the passphrase and a 3 (the default) iterates the whole process a number of times (see --s2k-count).
--s2k-count n

    Specify how many times the passphrases mangling for symmetric encryption is repeated. This value may range between 1024 and 65011712 inclusive. The default is inquired from gpg-agent. Note that not all values in the 1024-65011712 range are legal and if an illegal value is selected, GnuPG will round up to the nearest legal value. This option is only meaningful if --s2k-mode is set to the default of 3.

默认仍旧使用 AES-128 做 pssphrase 场景下的对称加密,数据签名还是用的 SHA-1,这俩都已经不太安全了,尤其是 SHA-1,已经被证明存在安全问题。因此,使用默认参数生成的 GPG 密钥,其安全性是不够的。

为了获得最佳安全性,我们需要:

  1. 使用如下参数生成 GPG 密钥:

    text

    gpg --s2k-mode 3 --s2k-count 65011712 --s2k-digest-algo SHA512 --s2k-cipher-algo AES256 ...
  2. 加密、签名、认证都使用不同的密钥,每个密钥只用于特定的场景,这样即使某个密钥泄漏,也不会影响其他场景的安全性。

为了在全局使用这些参数,可以将它们添加到你的 ~/.gnupg/gpg.conf 配置文件中。

详见我的 gpg 配置ryan4yin/nix-config/gpg

我日常同时在使用 macOS 与 NixOS,因此不论是需要离线存储的灾难恢复数据,还是需要在多端访问的个人数据,都需要一个跨平台的加密备份与同步工具。

前面提到的 pass 使用 GnuPG 进行文件级别的加密,但在很多场景下这不太适用,而且 GPG 本身也太重了,还一堆历史遗留问题,我不太喜欢。

为了其他数据备份与同步的需要,我需要一个跨平台的加密工具,目前调研到有如下这些:

  1. 文件级别的加密
    1. 这个有很多现成的现代加密工具,比如 age/sops, 都挺不错,但是针对大量文件的情况下使用比较繁琐。
  2. 全盘加密,或者支持通过 FUSE 模拟文件系统
    1. 首先 LUKS 就不用考虑了,它基本只在 Linux 上能用。
    2. 跨平台且比较活跃的项目中,我找到了 rclonerestic 这两个项目,都支持云同步,各有优缺点。
    3. restic 相对 rclone 的优势,主要是天然支持增量 snapshots 的功能,可以保存备份的历史快照,并设置灵活的历史快照保存策略。这对可能有回滚需求的数据而言是很重要的。比如说 PVE 虚拟机快照的备份,有了 restic 我们就不再需要依赖 PVE 自身孱弱的快照保留功能,全交给 restic 实现就行。
  3. 多端加密同步
    1. 上面提到的 rclone 与 restic 都支持各种云存储,因此都是不错的多端加密同步工具。
    2. 最流行的开源数据同步工具貌似是 synthing,但它对加密的支持还不够完善,暂不考虑。

进一步调研后,我选择了 age, rclonerestic 作为我的跨平台加密备份与同步工具。这三个工具都比较活跃,stars 很高,使用的也都是比较现代的密码学算法:

  1. age: 对于对称加密的场景,使用 ChaCha20-Poly1305 AEAD 加密方案,对称加密密钥使用 scrypt KDF 算法生成。
  2. rclone: 使用基于 XSalsa20-Poly1305 的 AEAD 加密方案,key 通过 scrypt KDF 算法生成,并且默认会加盐。
  3. restic: 使用 AES-256-CTR 加密,使用 Poly1305-AES 认证数据,key 通过 scrypt KDF 算法生成。

对于 Nix 相关的 secrets 配置,我使用了 age 的一个适配库 agenix 完成其自动加解密配置,并将相关的加密数据保存在我的 GitHub 私有仓库中。详见 ryan4yin/nix-config/secrets. 关于这个仓库的详细加解密方法,在后面第八节「桌面电脑的数据安全」中会介绍。

相关数据包括:GitHub, Twitter, Google 等重要账号的二次认证恢复代码、账号数据备份、PGP 主密钥与吊销证书等等。

这些数据日常都不需要用到,但在账号或两步验证设备丢失时就非要使用到其中的数据才能找回账号或吊销某个证书,是非常重要的数据。

我目前的策略是:使用 rclone + 1024bits 随机密码加密存储到两个 U 盘中(双副本),放在不同的地方,并且每隔半年到一年检查一遍数据。

对应的 rclone 解密配置本身也设置了比较强的 passphrase 保护,并通过我的 secrets 私有 Git 仓库多端加密同步。

相关数据包括:个人笔记、重要的照片、录音、视频、等等。

因为日常就需要在多端访问,因此显然不能离线存储。

不包含个人隐私的笔记,我直接用公开 GitHub 仓库 [ryan4yin/knowledge] (https://github.com/ryan4yin/knowledge/) 存储了,不需要加密。

对于不便公开的个人笔记,有这些考虑:

  1. 我的个人笔记目前主要是在移动端编辑,因此支持 Android/iOS 的客户端是必须的。
  2. 要能支持 Markdown/Orgmode 等通用的纯文本格式,纯文本格式更容易编写与分析,而通用格式则可以避免被平台绑定。
  3. 因为主要是移动端编辑,其实不需要多复杂的功能。
    • 以后可能会希望在桌面端做富文本编辑,但目前还没这种私人笔记的需求。
  4. 希望具有类似 Git 的分布式存储与同步、笔记版本管理功能,如果能直接使用 Git 那肯定是最好的。
  5. 端到端的加密存储与同步
  6. 如果有类似 Git 的 Diff 功能就更好了。

我一开始考虑直接使用基于 Git 仓库的方案,能获得 Git 的所有功能,同时还避免额外自建一个笔记服务。找到个 GitJournal ,数据存在 GitHub 私有仓库用了一个月,功能不太多但够用。但发现它项目不咋活跃,基于 SSH 协议的 Git 同步在大仓库上也有些毛病,而且数据明文存在 Git 仓库里,安全性相对差一些。

另外找到个 git-crypt 能在 Git 上做一层透明加密,但没找到支持它的移动端 APP,而且项目也不咋活跃。

https://github.com/topics/note-taking 下看了些流行项目,主要有这些:

  1. Joplin
    • 支持 S3/WebDAV 等多种协议同步数据,支持端到端加密
  2. Outline 等 Wiki 系统
    • 它直接就是个 Web 服务,主要面向公开的 Wiki,不适合私人笔记
  3. Logseq/Obsidian 等双链笔记软件(其中 Obsidian 是闭源软件)
    • 都是基于本地文件的笔记系统,也没加密工具,需要借助其他工具实现数据加密与同步
    • 其中 Logseq 是大纲流,一切皆列表。而 Obsidian 是文档流,比较贴近传统的文档编辑体验。
    • Obsidian 跟 Logseq 的 Sync 功能都是按月收费,相当的贵。社区有通过 Git 同步的方案,但都很 trickk,也不稳定。
  4. AppFlowy/Affine/apitable 等 Notion 替代品
    • 都是富文本编辑,不适合移动端设备

在移动端使用 Synthing 或 Git 等第三方工具同步笔记数据,都很麻烦,而且安全性也不够。因此目前看在移动端也能用得舒服的话,最稳妥的选择是第一类笔记 APP,简单试用后我选择了最流行的 Joplin.

  1. Homelab 中的 Windows-NAS-Server,两个 4TB 的硬盘,通过 SMB 局域网共享,公网所有客户端 (包括移动端)都能通过 tailscale + rclone 流畅访问。
  2. 部分重要的数据再通过 rclone 加密备份一份到云端,可选项有:
    1. 青云对象存储七牛云对象存储 Kodo,它们都有每月 10GB 的免费存储空间,以及 1GB-10GB 的免费外网流量。
    2. 阿里云 OSS 也能免费存 5GB 数据以及每月 5GB 的外网流量,可以考虑使用。

我的桌面电脑都是 macOS 与 NixOS,Homlab 虚拟机也已经 all in NixOS,另外我目前没有任何云上服务器。

另外虽然也有两台 Windows 虚拟机,但极少对它们做啥改动,只要做好虚拟机快照的备份就 OK 了。

对于 NixOS 桌面系统与 Homelab 虚拟机,我当前的方案如下:

  • 桌面主机
    • 启用 LUKS2 全盘加密 + Secure Boot,在系统启动阶段需要输入 passphrase 解密 NixOS 系统盘才能正常进入系统。
      • LUKS2 的 passphrase 为一个比较长的密码学随机字符串。
      • LUKS2 的所有安全设置全拉到能接受的最高(比较重要的是 --iter-time,计算出 unlock key 的用时,默认 2s,安全起见咱设置成了 5s)

        text

        cryptsetup --type luks2 --cipher aes-xts-plain64 --hash sha512 --iter-time 5000 --key-size 256 --pbkdf argon2id --use-urandom --verify-passphrase luksFormat device
      • LUKS2 使用的 argon2id 是比 scrypt 更强的 KDF 算法,其安全性是足够的。
    • 桌面主機使用 tmpfs 作为根目录,所有未明确声明持久化的数据,都会在每次重启后被清空,这强制我去了解自己装的每个软件都存了哪些数据,是否需要持久化,使整个系统更白盒,提升了整个系统的环境可信度。
  • Homelab
    • Proxmox VE 物理机全部重装为 NixOS,启用 LUKS 全盘加密与 btrfs + zstd 压缩,买几个便宜的 U 盘用于自动解密(注意解密密钥的离线加密备份)。使用 K3s + KubeVirt 管理 QEMU/KVM 虚拟机。
  • Secrets 說明
    • 重要的通用 secrets,都加密保存在我的 secrets 私有仓库中,在部署我的 nix-config 时使用主机本地的 SSH 系统私钥自动解密。
      • 也就是说要在一台新电脑(不論是桌面主機還是 NixOS 虛擬機)上成功部署我的 nix-config 配置,需要的准备流程:
        • 本地生成一个新的 ssh key,将公钥配置到 GitHub,并 ssh-add 这个新的私钥,使其能够访问到我的私有 secrets 仓库。
        • 将新主机的系统公钥 /etc/ssh/ssh_host_ed25519_key.pub 发送到一台旧的可解密 secrets 仓库数据的主机上。如果该文件不存在则先用 sudo ssh-keygen -A 生成。
        • 在旧主机上,将收到的新主机公钥添加到 secrets 仓库的 secrets.nix 配置文件中,并使用 agenix 命令 rekey 所有 secrets 数据,然后 commit & push。
        • 现在新主机就能够通过 nixos-rebuild switchdarwin-rebuild switch 成功部署我的 nix-config 了,agenix 会自动使用新主机的系统私钥/etc/ssh/ssh_host_ed25519_key 解密 secrets 仓库中的数据并完成部署工作。
      • 这份 secrets 配置在 macOS 跟 NixOS 上通用,也与 CPU 架构无关,agenix 在这两个系统上都能正常工作。
    • 基于安全性考虑,对 secrets 进行分类管理与加密:
      • 桌面电脑能解密所有的 secrets
      • Homelab 中的跳板机只能解密 Homelab 相关的所有 secrets
      • 其他所有的 NixOS 虚拟机只能解密同类别的 secrets,比如一台监控机只能解密监控相关的 secrets.

对于 macOS,它本身的磁盘安全我感觉就已经做得很 OK 了,而且它能改的东西也比较有限。我的安全设置如下:

  • 启用 macOS 的全盘加密功能
  • 常用的 secrets 的部署与使用方式,与前面 NixOS 的描述完全一致

在使用 nix-darwin 跟 NixOS 的情况下,整个 macOS/NixOS 的系统环境都是通过我的ryan4yin/nix-config 声明式配置的,因此桌面电脑的灾难恢复根本不是一个问题。

只需要简单的几行命令就能在一个全新的系统上恢复出我的 macOS / NixOS 桌面环境,所有密钥也会由 agenix 自动解密并放置到正确的位置。

要说有恢复难题的,也就是一些个人数据了,这部分已经在前面第七小节介绍过了,用 rclone/restic 就行。

  1. secrets 私有仓库: 它会被我的 nix-config 自动拉取并部署到所有主力电脑上,包含了 homelab ssh key, GPG subkey, 以及其他一些重要的 secrets.
    1. 它通过我所有桌面电脑的 /etc/ssh/ssh_host_ed25519_key.pub 公钥加密,在部署时自动使用对应的私钥解密。
    2. 此外该仓库还添加了一个灾难恢复用的公钥,确保在我所有桌面电脑都丢失的极端情况下,仍可通过对应的灾难恢复私钥解密此仓库的数据。该私钥在使用 age 加密后(注:未使用 rclone 加密)与我其他的灾难恢复数据保存在一起。
  2. password-store: 我的私人账号密码存储库,通过 pass 命令行工具管理,使用 GPG 加密,GPG 密钥备份被通过 age/agenix 加密保存在上述 secrets 仓库中。
    1. 由于 GnuPG 自身导出的密钥备份数据安全性欠佳,因此我使用了 age + passphrase 对其进行了二次对称加密,然后再通过 agenix 加密(第三次加密,使用非对称加密算法)保存在 secrets 仓库中。这保障了即使我的 GPG 密钥在我所有的桌面电脑上都存在,但安全性仍旧很够。
  3. rclone 加密的备份 U 盘(双副本):离线保存一些重要的数据。其配置文件被加密保存在 secrets 仓库中,其配置文件的解密密码被加密保存在 password-store 仓库中。

这套方案的大部分部署工作都是由我的 Nix 配置自动完成的,整个流程的自动化程度很高,所以这套方案带给我的额外负担并不大。

secrets 这个私有仓库是整个方案的核心,它包含了所有重要数据(password-store/rclone/…)的解密密钥。如果它丢失了,那么所有的数据都无法解密。

但好在 Git 仓库本身是分布式的,我所有的桌面电脑上都有对应的完整备份,我的灾难恢复存储中也会定期备份一份 secrets/password-store 两个仓库的数据过去以避免丢失。

另外需要注意的是,为了避免循环依赖,secrets 与 password-store 这两个仓库的备份不应该使用 rclone 再次加密,而是直接使用 age 对称加密。这样只要我还记得 age 的解密密码、gpg 密钥的 passphrase 等少数几个密码,就能顺着整条链路解密出所有的数据。

绝大部分密码都建议设置为包含大小写跟部分特殊字符的密码学随机字符串,通过 pass 加密保存与多端同步与自动填充,不需要额外记忆。考虑到我们基本不会需要手动输入这些密码,因此它们的长度可以设置得比较长,比如 16-24 位(不使用更长密码的原因是,许多站点或 APP 都限制了密码长度,这种长度下使用 passphrase 单词组的安全性相对会差一点,因此也不推荐)。

再通过一些合理的密码复用手段,可以将需要记忆的密码数量降到 3 - 5 个,并且确保日常都会输入,避免遗忘。

不过这里需要注意一点,就是 SSH 密钥、GPG 密钥、系统登录密码这三个密码最好不要设成一样。前面我们已经做了分析,这三个 passphrase 的加密强度区别很大,设成一样的话,使用 bcrypt 的 SSH 密钥将会成为整个方案的短板。

而关于密码内容的设计,这个几核心 pssphrase 的长度都是不受限的,有两个思路(注意不要在密码中包含任何个人信息):

  1. 使用由一个个单词组成的较长的 passphrase,比如don't-do-evil_I-promise-this-would-become-not-a-dark-corner 这样的。
  2. 使用字母大小写加数字、特殊字符组成的密码学随机字符串,比如 fsD!.*v_F*sdn-zFkJM)nQ 这样的。

第一种方式的优点是,这些单词都是常用单词,记忆起来会比较容易,而且也不容易输错。

第二种方式的优点是,密码学随机字符串可以以更短的长度达到与第一种方式相当的安全性。但它的缺点也比较明显——容易输错,而且记忆起来也不容易。

两种方式是都可以,如果你选择第二种方式,可以专门编些小故事来通过联想记忆它们,hint 中也能加上故事中的一些与密码内容无直接关联的关键字帮助回忆。毕竟人类擅长记忆故事,但不擅长记忆随机字符。举个例子,上面的密码 fsD!.*v_F*sdn-zFkJM)nQ,可以找出这么些联想:

  • fs: 「佛说」这首歌里面的歌词
  • D!: 头文字D!
  • .*: 地面上的光斑(.),天上的星光(*)
  • v_: 嘴巴张开(v)睡得很香的样子,口水都流到地上了(_)
  • F*sdn: F*ck 软件定义网络(sdn)
  • zFkJM: 在政府(zf)大门口(k),看(k) 见了 Jack Ma (JM) 在跳脱yi舞…
  • )nQ: 宁静的夏夜,凉风习习,天上一轮弯月,你(n)问(Q)我,当下这一刻是否足够

把上面这些联想串起来,就是一个怀旧、雷人、结尾又有点温馨的无厘头小故事了,肯定能令你自己印象深刻。故事写得够离谱的话,你可能想忘都忘不掉了。

总之就是用这种方式,然后把密码中的每个字符都与故事中的某个关键字联系起来,这样就能很容易地记住这个密码了。如果你对深入学习如何记忆这类复杂的东西感兴趣,可以看看这本我最想要的记忆魔法书.

最后一点,就是定期更新一遍这些密码、SSH 密钥、GPG 密钥。所有数据的加密安全性都是随着时间推移而降低的,曾经安全的密码学算法在未来也可能会变得不再安全(这方面 MD5, SHA-1 都是很好的例子),因此定期更新这些密码跟密钥是很有必要的。

几个核心密码更新起来会简单些,可以考虑每年更新一遍,而密钥可以考虑每两三年更新一遍(时间凭感觉说的哈,没有做论证)。其他密码密钥则可以根据数据的重要性来决定更新频率。

前面已经基本都提到了,这里再总结下:

  1. 重新生成了所有的 SSH Key,增强了 passphrase 强度,bcrypt rounds 增加到 256,通过ssh-add 使用,只需要在系统启动后输入一次密码即可,也不麻烦。
  2. 重新生成了所有的 PGP Key,主密钥离线加密存储,本地只保留了加密、签名、认证三个 PGP 子密钥。
  3. 重新生成了所有重要账号的密码,全部使用随机密码,一共改了二三十个账号。考虑到旧的 backup code 可能已经泄漏,我也重新生成了所有重要账号的 backup code.
  4. 重装 NixOS,使用 LUKS2 做全盘加密,启用 Secure Boot. 同时使用 tmpfs 作为根目录,所有未明确声明持久化的数据,都会在每次重启后被清空。
  5. 使用 nix-darwin 与 home-manager 重新声明式地配置了我的两台 MacBook Pro(Intel 跟 Apple Silicon 各一台),与我的 NixOS 共用了许多配置,最大程度上保持了所有桌面电脑的开发环境一致性,也确保了我始终能快速地在一台新电脑上部署我的整个开发环境。
  6. 注销印象笔记账号,使用 evernote-backup 跟 evernote2md 两个工具将个人的私密笔记遷移到了 Joplin + OneDrive 上,Homelab 中設了通過 restic 定期自動加密備份 OneDrive 中的 Joplin 數據。
  7. 比较有价值的 GitHub 仓库,都设置了禁止 force push 主分支,并且添加了 GitHub action 自动同步到国内 Gitee.
  8. All in NixOS,将 Homelab 中的 PVE 全部使用 NixOS + K3s + KubeVirt 替换。从偏黑盒且可复现性差的 Ubuntu、Debian, Proxmox VE, OpenWRT 等 VM 全面替换成更白盒且可复现性强的 NixOS、KubeVirt,提升我对内网环境的掌控度,进而提升内网安全性。

这里考虑我的 GPG 子密钥泄漏了、pass 密码仓库泄漏了等各种情况下的灾难恢复流程。

TODO 后续再慢慢补充。

目前我的主要个人数据基本都已经通过上述方案进行了安全管理。但还有这些方面可以进一步改进:

  • 针对 Homelab 的虚拟机快照备份,从我旧的基于 rclone + crontab 的明文备份方案,切换到了基于 restic 的加密备份方案。
  • 手机端的照片视频虽然已经在上面设计好了备份同步方案,但仍未实施。考虑使用 roundsync 加密备份到云端,实现多端访问。
  • 进一步学习下 appamor, bubblewrap 等 Linux 下的安全限制方案,尝试应用在我的 NixOS PC 上。
  • Git 提交是否可以使用 GnuPG 签名,目前没这么做主要是觉得 PGP 这个东西太重了,目前我也只在 pass 上用了它,而且还在研究用 age 取代它。
  • 尝试通过 hashcat,wifi-cracking 等手段破解自己的重要密码、SSH 密钥、GPG 密钥等数据,评估其安全性。
  • 使用一些流行的渗透测试工具测试我的 Homelab 与内网环境,评估其安全性。

安全总是相对的,而且其中涉及的知识点不少,我 2022 年学了密码学算是为此打下了个不错的基础, 但目前看前头还有挺多知识点在等待着我。我目前仍然打算以比较 casual 的心态去持续推进这件事情,什么时候兴趣来了就推进一点点。

这套方案也可能存在一些问题,欢迎大家审阅指正。

NixOS 在 Lichee Pi 4A 上是如何启动的

2024-01-29 00:58:57

文章是 2023-08-07 写的,后面就完全忘掉这回事了,今天偶然翻到它才想起要整理发布下…所以注意文章中的时间线是 2023 年 8 月。

我从今年 5 月份初收到了内测板的 Lichee Pi 4A,这是当下性能最高的 RISC-V 开发板之一,不过当时没怎么折腾。

6 月初的时候我开始尝试在 Orange Pi 5 上运行 NixOS,在NixOS on ARM 的 Matrix 群组 中得到了俄罗斯老哥 @K900 的帮助,没费多大劲就成功了,一共就折腾了三天。

于是我接着尝试在 Lichee Pi 4A 上运行 NixOS,因为已经拥有了 Orange Pi 5 上的折腾经验,我以为这次会很顺利。但是实际难度远远超出了我的预期,我从 6 月 13 号开始断断续续折腾到 7 月 3 号,接触了大量的新东西,包括 U-Boot、OpenSBI、SPL Flash、RISCV Boot Flows 等等,还参考了 @chainsx 的 Fedora for Lichee Pi 4A 方案,请教了 @NickCao 许多 NixOS 相关的问题,@revy 帮我修了好几个 revyos/thead-kernel 在标准工具链上编译的 bug,期间也请教过 @HougeLangley 他折腾 Lichee Pi 4A 的经验。我在付出了这么多的努力后,才最终成功编译出了 NixOS 的系统镜像(包含 boot 跟 rootfs 两个分区)。

但是!现在要说「但是」了。

镜像是有了,系统却无法启动…找了各种资料也没解决,也没好意思麻烦各位大佬,搞得有点心灰意冷,就先把这部分工作放下了。

接着就隔了一个多月没碰 Lichee Pi 4A,直到 8 月 5 号,外国友人 @JayDeLux 在Mainline Linux for RISC-V TG 群组中询问我 NixOS 移植工作的进展如何(之前有在群里提过我在尝试移植),我才决定再次尝试一下。

在之前工作的基础上一番骚操作后,我在 8 月 6 号晚上终于成功启动了 NixOS,这次意外的顺利,后续也成功通过一份 Nix Flake 配置编译出了可用的 NixOS 镜像。

最终成果:https://github.com/ryan4yin/nixos-licheepi4a

整个折腾过程相当曲折,虽然最终达成了目标,但是期间遭受的折磨也真的不少。总的来说仍然是一次很有趣的经历,既学到了许多新技术知识、认识了些有趣的外国友人(@JayDeLux 甚至还给我打了 $50 美刀表示感谢),也跟 @HougeLangley 、@chainsx 、@Rabenda(revy) 等各位大佬混了个脸熟。

这篇文章就是记录下我在这个折腾过程中学到的所有知识,以飨读者,同时也梳理一下自己的收获。

本文的写作思路是自顶向下的,先从 NixOS 镜像的 boot 分区配置、启动脚本开始分析,过渡到实际的启动日志,再接续分析下后续的启动流程。NixOS 分析完了后,再看看与 RISC-V 相关的硬件固件与 bootloader 部分要如何与 NixOS 协同工作,使得 NixOS 能够在 Lichee Pi 4A 上正常启动。

LicheePi 4A 是当前市面上性能最高的 RISC-V Linux 开发板之一,它以 TH1520 为主控核心 ([email protected], RV64GCV,4TOPS@int8 NPU, 50GFLOP GPU),板载最大 16GB 64bit LPDDR4X,128GB eMMC,支持 HDMI+MIPI 双4K 显示输出,支持 4K 摄像头接入,双千兆网口(其中一个支持POE供电)和 4 个 USB3.0 接口,多种音频输入输出(由专用 C906 核心处理)。

以上来自 Lichee Pi 4A 官方文档Lichee Pi 4A - Sipeed Wiki.

总之它是我手上性能最高的 RISC-V 开发板。

LicheePi 4A 官方主要支持 RevyOS—— 一款针对 T-Head 芯片生态的 Debian 优化定制发行版。根据猴哥(@HougeLangley)文章介绍,它也是目前唯一且确实能够启用 Lichee Pi 4A 板载 GPU 的发行版,

这个感觉就不用多说了,我在这几个月已经给 NixOS 写了非常多的文字了,感兴趣请直接移步ryan4yin/nixos-and-flakes-book.

在 4 月份接触了 NixOS 后,我成了 NixOS 铁粉。作为一名铁粉,我当然想把我手上的所有性能好点的板子都装上 NixOS,Lichee Pi 4A 自然也不例外。

我目前主要完成了两块板子的 NixOS 移植工作,一块是 Orange Pi 5,另一块就是 Lichee Pi 4A。 Orange Pi 5 是 ARM64 架构的,刚好也遇到了拥有该板子的 NixOS 用户 @K900,在他的帮助下我很顺利地就完成了移植工作。

而 Lichee Pi 4A 就比较曲折,也比较有话题性。所以才有了这篇文章。

一个完整的嵌入式 Linux 系统,通常包含了 U-Boot、kernel、设备树以及根文件系统(rootfs)四个部分。

其中 U-Boot,kernel 跟设备树,都是与硬件相关的,需要针对不同的硬件进行定制。而 rootfs 的大部分内容(比如说 NixOS 系统的 rootfs 本身),都是与硬件无关的,可以通用。

我的移植思路是,从 LicheePi4A 官方使用的 RevyOS 中拿出跟硬件相关的部分(也就是 U-Boot, kernel 跟设备树这三个),再结合上跟硬件无关的 NixOS rootfs,组合成一个完整的、可在 LicheePi4A 上正常启动运行的 NixOS 系统。

RevyOS 针对 LicheePi4A 定制的几个项目源码如下:

思路很清晰,但因为 NixOS 本身的特殊性,实际操作起来,现有的 Gentoo, Arch Linux, Fedora 的移植仓库代码全都无法直接使用,需要做的工作还是不少的。

要做移植,首先就要了解 NixOS 系统本身的文件树结构以及系统启动流程,搞明白它跟 Arch Linux, Fedora 等其他发行版的区别,这样才好参考其他发行版的移植工作,搞明白该如何入手。

这里方便起见,我直接使用我自己为 LicheePi4A 构建好的 NixOS 镜像进行分析。首先参照ryan4yin/nixos-licheepi4a 的 README 下载解压镜像,再使用 losetup 跟 mount 直接挂载镜像中的各分区进行初步分析:

bash

# 解压镜像
› mv nixos-licheepi4a-sd-image-*-riscv64-linux.img.zst nixos-lp4a.img.zst
› zstd -d nixos-lp4a.img.zst
# 将 img 文件作为虚拟 loop 设备连接到系统中
› sudo losetup --find --partscan nixos-lp4a.img
# 查看挂载的 loop 设备
› lsblk | grep loop
loop0               7:0    0  1.9G  0 loop
├─loop0p1         259:8    0  200M  0 part
└─loop0p2         259:9    0  1.7G  0 part
# 分别挂载镜像中的 boot 跟 rootfs 分区
› mkdir boot root
› sudo mount /dev/loop0p1 boot
› sudo mount /dev/loop0p2 root
# 查看 boot 分区内容
› ls boot/
╭───┬───────────────────────────┬──────┬─────────┬──────────────╮
# │           name            │ type │  size   │   modified   │
├───┼───────────────────────────┼──────┼─────────┼──────────────┤
0 │ boot/extlinux             │ dir  │  4.1 KB │ 44 years ago │
1 │ boot/fw_dynamic.bin       │ file │ 85.9 KB │ 24 years ago │
2 │ boot/light_aon_fpga.bin   │ file │ 50.3 KB │ 24 years ago │
3 │ boot/light_c906_audio.bin │ file │ 16.4 KB │ 24 years ago │
4 │ boot/nixos                │ dir  │  4.1 KB │ 44 years ago │
╰───┴───────────────────────────┴──────┴─────────┴──────────────╯
# 查看 root 分区内容
› ls root/
╭───┬────────────────────────────┬──────┬──────────┬──────────────╮
# │            name            │ type │   size   │   modified   │
├───┼────────────────────────────┼──────┼──────────┼──────────────┤
0 │ root/boot                  │ dir  │   4.1 KB │ 54 years ago │
1 │ root/lost+found            │ dir  │  16.4 KB │ 54 years ago │
2 │ root/nix                   │ dir  │   4.1 KB │ 54 years ago │
3 │ root/nix-path-registration │ file │ 242.0 KB │ 54 years ago │
╰───┴────────────────────────────┴──────┴──────────┴──────────────╯

可以看到 NixOS 整个根目录(/root)下一共就四个文件夹,其中真正保存有系统数据的文件夹只有/boot/nix 这两个,这与传统的 Linux 发行版大相径庭。有一点 Linux 使用经验的朋友都应该清楚,传统的 Linux 发行版遵循 UNIX 系统的FHS 标准,根目录下会有很多文件夹,比如/bin/etc/home/lib/opt/root/sbin/srv/tmp/usr/var 等等。

那 NixOS 它这么玩,真的能正常启动么?这就是我在构建出镜像后却发现无法在 LicheePi 4A 上启动时,最先产生的疑问。在询问 @chainsx 跟 @revy 系统无法启动的解决思路的时候,他们也一脸懵逼,觉得这个文件树有点奇葩,很怀疑是我构建流程有问题导致文件树不完整。

但实际上 NixOS 就是这么玩的,它 rootfs 中所有的数据全都存放在 /nix/store 这个目录下并且被挂载为只读,其他的文件夹以及其中的文件都是在运行时动态创建的。这是它实现声明式系统配置、可回滚更新、可并行安装多个版本的软件包等等特性的基础。

下面继续分析,先仔细看下 /boot 的内容:

bash

› tree boot
boot
├── extlinux
│   └── extlinux.conf
├── fw_dynamic.bin
├── light_aon_fpga.bin
├── light_c906_audio.bin
└── nixos
    ├── 2n6fjh4lhzaswbyacaf72zmz6mdsmm8l-initrd-k-riscv64-unknown-linux-gnu-initrd
    ├── l18cz7jd37n35dwyf8wc8divm46k7sdf-k-riscv64-unknown-linux-gnu-dtbs
    │   ├── sifive
    │   │   └── hifive-unleashed-a00.dtb
    │   └── thead
    │       ├── fire-emu-crash.dtb
    │       ├── fire-emu.dtb
    │       ├── ...... (省略)
    │       ├── light-fm-emu-audio.dtb
    │       ├── light-fm-emu-dsi0-hdmi.dtb
    │       ├── light-fm-emu-dsp.dtb
    │       ├── light-fm-emu-gpu.dtb
    │       ├── light-fm-emu-hdmi.dtb
    │       ├── light-lpi4a-ddr2G.dtb
    │       └── light_mpw.dtb
    └── l18cz7jd37n35dwyf8wc8divm46k7sdf-k-riscv64-unknown-linux-gnu-Image

6 directories, 64 files

可以看到:

  1. 它使用 /boot/extlinux/extlinux.conf 作为 U-Boot 的启动项配置,据U-Boot 官方的 Distro 文档 所言,这是 U-Boot 的标准配置文件。
  2. 另外还有一些 xxx.bin 文件,这些是一些硬件固件,其中的 light_c906_audio.bin 显然是玄铁 906 这个 IP 核的音频固件,其他的后面再研究。
  3. NixOS 的 initrd, dtbs 以及 Image 文件都是在 /boot/nixos 下,这三个文件也都是跟 Linux 的启动相关的,现在不用管它们,下一步会分析。

再看下 /boot/extlinux/extlinux.conf 的内容:

bash

› cat boot/extlinux/extlinux.conf
# Generated file, all changes will be lost on nixos-rebuild!

# Change this to e.g. nixos-42 to temporarily boot to an older configuration.
DEFAULT nixos-default

MENU TITLE ------------------------------------------------------------
TIMEOUT 50

LABEL nixos-default
  MENU LABEL NixOS - Default
  LINUX ../nixos/l18cz7jd37n35dwyf8wc8divm46k7sdf-k-riscv64-unknown-linux-gnu-Image
  INITRD ../nixos/2n6fjh4lhzaswbyacaf72zmz6mdsmm8l-initrd-k-riscv64-unknown-linux-gnu-initrd
  APPEND init=/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b/init console=ttyS0,115200 root=UUID=14e19a7b-0ae0-484d-9d54-43bd6fdc20c7 rootfstype=ext4 rootwait rw earlycon clk_ignore_unused eth=$ethaddr rootrwoptions=rw,noatime rootrwreset=yes loglevel=4
  FDT ../nixos/l18cz7jd37n35dwyf8wc8divm46k7sdf-k-riscv64-unknown-linux-gnu-dtbs/thead/light-lpi4a.dtb

从上述配置中能获得这些信息:

  1. 它创建了一个名为 nixos-default 的启动项并将它设为了默认启动项,extlinux 在启动阶段会根据该配置启动 NixOS 系统
  2. 启动项中的 LINUX INITRD FDT 三个参数分别指定了 kernel(Image 文件)、initrd 以及设备树(dtb)的位置,这三个文件我们在前面已经看到了,都在 /boot/nixos 下。
    1. 根据 Linux 官方文档Using the initial RAM disk (initrd) 所言,在使用了 initrd 这个内存盘的情况下,Linux 的启动流程如下:
      1. bootloader(这里是 u-boot) 根据配置加载 kernel 文件(Image)、dtb 设备树文件以及initrd 文件系统,然后以设备树跟 initrd 的地址为参数启动 Kernel.
      2. Kernel 将传入的 initrd 转换成一个内存盘并挂载为根文件系统,然后释放 initrd 的内存。
      3. Kernel 接着运行 init 参数指定的可执行程序,这里是/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b/init, 这个 init 程序会挂载真正的根文件系统,并在其上执行后续的启动流程。
      4. initrd 文件系统被移除,系统启动完毕。
    2. initrd 这样一个临时的内存盘,通常用于在系统启动阶段加载一些内核中未内置但启动却必需的驱动或数据文件供 init 程序使用,以便后续能够挂载真正的根文件系统。
      1. 比如说挂载一个 LUKS 加密的根文件系统,这通常会涉及到提示用户输入 passphrase、从某个地方读取解密用的 keyfile 或者与插入的 USB 硬件密钥交互,这会需要读取内核之外的 keyfile 文件、用到内核之外的加密模块、USB 驱动、HID 用户输入输出模块或者其他因为许可协议、模块大小等问题无法被静态链接到内核中的各种内核模块或程序。initrd 就是用来解决这些问题的。
  3. APPEND 参数包含有许多关键信息:
    1. 系统的 init 程序,也就是传说中的 1 号进程(PID 1),被设置为/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b/init, 这实际是一个 shell 脚本,我们下一步会重点分析它。
      1. 在传统的 Linux 发行版中,init 通常使用默认值 /sbin/init,它会被链接到/lib/systemd/systemd,也就是直接使用 systemd 作为 1 号进程。你可以在 Fedora/Ubuntu 等传统发行版中运行 ls -al /sbin/init 确认这一点,以及检查它们的/boot/grub/grub.cfs 启动项配置,看看它们有无自定义内核的 init 参数。
    2. 系统的 rootfs 分区为 /dev/disk/by-uuid/14e19a7b-0ae0-484d-9d54-43bd6fdc20c7,使用的文件系统为 ext4.
    3. earlycon(early console) 表示在系统启动早期就启用控制台输出,这样可以在系统启动阶段通过 UAER/HDMI 等接口看到相关的启动日志,方便调试。
    4. 其他参数先不管。

这样一分析就能得出结论:在执行 init 程序之前的启动流程都未涉及到真正的根文件系统,NixOS 与其他发行版在该流程中并无明显差异。

为了方便后续内容的理解,先看下 NixOS 系统在 LicheePi 4A 上的实际启动日志是个很不错的选择。

按我项目中的 README 正常烧录好系统后,使用 USB 转串口工具连接到 LicheePi 4A 的 UART0 串口,然后启动系统,就能看到 NixOS 的启动日志。

接线示例:

LicheePi4A UART 调试接线 - 正面

LicheePi4A UART 调试接线 - 反面

接好线后使用 minicom 查看日志:

bash

› ls /dev/ttyUSB0
╭───┬──────────────┬─────────────┬──────┬───────────────╮
# │     name     │    type     │ size │   modified    │
├───┼──────────────┼─────────────┼──────┼───────────────┤
0 │ /dev/ttyUSB0 │ char device │  0 B │ 6 minutes ago │
╰───┴──────────────┴─────────────┴──────┴───────────────╯

› minicom -d /dev/ttyusb0 -b 115200

一个正常的启动日志示例如下:

text

Welcome to minicom 2.8
brom_ver 8
[APP][E] protocol_connect failed, exit.
OpenSBI v0.9
   ____                    _____ ____ _____
  / __ \                  / ____|  _ \_   _|
 | |  | |_ __   ___ _ __ | (___ | |_) || |
 | |  | | '_ \ / _ \ '_ \ \___ \|  _ < | |
 | |__| | |_) |  __/ | | |____) | |_) || |_
  \____/| .__/ \___|_| |_|_____/|____/_____|
        | |
        |_|

Platform Name             : T-HEAD Light Lichee Pi 4A configuration for 8GB DDR board
Platform Features         : mfdeleg
Platform HART Count       : 4
Platform IPI Device       : clint
Platform Timer Device     : clint
Platform Console Device   : uart8250
Platform HSM Device       : ---
Platform SysReset Device  : thead_reset
Firmware Base             : 0x0
Firmware Size             : 132 KB
Runtime SBI Version       : 0.3

Domain0 Name              : root
Domain0 Boot HART         : 0
Domain0 HARTs             : 0*,1*,2*,3*
Domain0 Region00          : 0x000000ffdc000000-0x000000ffdc00ffff (I)
Domain0 Region01          : 0x0000000000000000-0x000000000003ffff ()
Domain0 Region02          : 0x0000000000000000-0xffffffffffffffff (R,W,X)
Domain0 Next Address      : 0x0000000000200000
Domain0 Next Arg1         : 0x0000000001f00000
Domain0 Next Mode         : S-mode
Domain0 SysReset          : yes

Boot HART ID              : 0
Boot HART Domain          : root
Boot HART ISA             : rv64imafdcvsux
Boot HART Features        : scounteren,mcounteren,time
Boot HART PMP Count       : 0
Boot HART PMP Granularity : 0
Boot HART PMP Address Bits: 0
Boot HART MHPM Count      : 16
Boot HART MHPM Count      : 16
Boot HART MIDELEG         : 0x0000000000000222
Boot HART MEDELEG         : 0x000000000000b109
[    0.000000] Linux version 5.10.113 (nixbld@localhost) (riscv64-unknown-linux-gnu-gcc (GCC) 13.1.0, GN0
[    0.000000] OF: fdt: Ignoring memory range 0x0 - 0x200000
[    0.000000] earlycon: uart0 at MMIO32 0x000000ffe7014000 (options '115200n8')
[    0.000000] printk: bootconsole [uart0] enabled
[    2.292495] (NULL device *): failed to find vdmabuf_reserved_memory node
[    2.453953] spi-nor spi0.0: unrecognized JEDEC id bytes: ff ff ff ff ff ff
[    2.460971] dw_spi_mmio ffe700c000.spi: cs1 >= max 1
[    2.466001] spi_master spi0: spi_device register error /soc/spi@ffe700c000/spidev@1
[    2.497453] sdhci-dwcmshc ffe70a0000.sd: can't request region for resource [mem 0xffef014064-0xffef01]
[    2.509014] misc vhost-vdmabuf: failed to find vdmabuf_reserved_memory node
[    3.386036] debugfs: File 'SDOUT' in directory 'dapm' already present!
[    3.392692] debugfs: File 'Playback' in directory 'dapm' already present!
[    3.399524] debugfs: File 'Capture' in directory 'dapm' already present!
[    3.406262] debugfs: File 'Playback' in directory 'dapm' already present!
[    3.413067] debugfs: File 'Capture' in directory 'dapm' already present!
[    3.425466] aw87519_pa 5-0058: aw87519_parse_dt: no reset gpio provided failed
[    3.432752] aw87519_pa 5-0058: aw87519_i2c_probe: failed to parse device tree node

<<< NixOS Stage 1 >>>

running udev...
Starting systemd-udevd version 253.6
kbd_mode: KDSKBMODE: Inappropriate ioctl for device
Gstarting device mapper and LVM...
checking /dev/disk/by-label/NIXOS_SD...
fsck (busybox 1.36.1)
[fsck.ext4 (1) -- /mnt-root/] fsck.ext4 -a /dev/disk/by-label/NIXOS_SD
NIXOS_SD: recovering journal
NIXOS_SD: clean, 148061/248000 files, 818082/984159 blocks
mounting /dev/disk/by-label/NIXOS_SD on /...

<<< NixOS Stage 2 >>>

running activation script...
setting up /etc...
++ /nix/store/2w8nachmhqvbjswrrsdia5cx1afxxx60-util-linux-riscv64-unknown-linux-gnu-2.38.1-bin/bin/findm/
+ rootPart=/dev/disk/by-label/NIXOS_SD
++ lsblk -npo PKNAME /dev/disk/by-label/NIXOS_SD
+ bootDevice=/dev/mmcblk1
++ lsblk -npo MAJ:MIN /dev/disk/by-label/NIXOS_SD
++ /nix/store/zag1z2yvsh2ccpsbgsda7xhv4sfha7mj-gawk-riscv64-unknown-linux-gnu-5.2.1/bin/awk -F: '{print '
+ partNum='26 '
+ echo ,+,
+ sfdisk -N26 --no-reread /dev/mmcblk1
GPT PMBR size mismatch (8332023 != 122894335) will be corrected by write.
The backup GPT table is not on the end of the device. This problem will be corrected by write.
warning: /dev/mmcblk1: partition 26 is not defined yet
Disk /dev/mmcblk1: 58.6 GiB, 62921900032 bytes, 122894336 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 58B10C85-BB4D-F94A-9194-82020FC9DC23

Old situation:

Device          Start     End Sectors  Size Type
/dev/mmcblk1p1  16384  425983  409600  200M Microsoft basic data
/dev/mmcblk1p2 425984 8331990 7906007  3.8G Linux filesystem

/dev/mmcblk1p26: Created a new partition 3 of type 'Linux filesystem' and of size 54.6 GiB.

New situation:
Disklabel type: gpt
Disk identifier: 58B10C85-BB4D-F94A-9194-82020FC9DC23

Device           Start       End   Sectors  Size Type
/dev/mmcblk1p1   16384    425983    409600  200M Microsoft basic data
/dev/mmcblk1p2  425984   8331990   7906007  3.8G Linux filesystem
/dev/mmcblk1p3 8333312 122892287 114558976 54.6G Linux filesystem

The partition table has been altered.
Calling ioctl() to re-read partition table.
Re-reading the partition table failed.: Device or resource busy
The kernel still uses the old table. The new table will be used at the next reboot or after you run part.
Syncing disks.
+ /nix/store/wm9ynqbkqi7gagggb4y6f4l454kkga32-parted-riscv64-unknown-linux-gnu-3.6/bin/partprobe
+ /nix/store/yln7ma9dldr3f2dva4l0iq275s4brxml-e2fsprogs-riscv64-unknown-linux-gnu-1.46.6-bin/bin/resize2D
resize2fs 1.46.6 (1-Feb-2023)
Filesystem at /dev/disk/by-label/NIXOS_SD is mounted on /; on-line resizing required
old_desc_blocks = 1, new_desc_blocks = 1
The filesystem on /dev/disk/by-label/NIXOS_SD is now 988250 (4k) blocks long.

+ /nix/store/f5w7dd1f195bxkashhr5x0a788nxrxvc-nix-riscv64-unknown-linux-gnu-2.13.3/bin/nix-store --load-b
+ touch /etc/NIXOS
+ /nix/store/f5w7dd1f195bxkashhr5x0a788nxrxvc-nix-riscv64-unknown-linux-gnu-2.13.3/bin/nix-env -p /nix/vm
+ rm -f /nix-path-registration
starting systemd...

Welcome to NixOS 23.05 (Stoat)!

[  OK  ] Created slice Slice /system/getty.
[  OK  ] Created slice Slice /system/modprobe.
[  OK  ] Created slice Slice /system/serial-getty.......
......

简单总结下日志中的信息:

  1. 整个启动流程被分成了三个阶段,分别是:
    1. OpenSBI: 这个阶段貌似进行了一些硬件相关的初始化,比如说串口、SPI、SD 卡等,貌似还有些报错,先不管。
    2. NixOS Stage 1: 这应该就是 initrd 阶段干的活,内核加载了 systemd udev 内核模块,然后使用 busybox 的 fsck 检查了根文件系统,接着挂载了根文件系统。
    3. NixOS Stage 2:
      1. 运行了一个什么activation script,它首先设置好了 /etc 文件夹,然后检查了根分区文件系统的情况,并自动执行了分区与文件系统的扩容操作。
      2. 接着通过 nix-env -p /nix/vm... 大概是切换了个运行环境。
      3. 最后启动了 systemd,这之后的流程就跟其他发行版没啥区别了(都是 systemd)。

有了上面这些信息,我们就可以比较容易地理解 init 这个程序了,它主要对应前面日志中的 NixOS Stage 2,即在真正挂载根文件系统之后,执行的第一个用户态程序。

在 NixOS 中这个 init 程序实际上是一个 shell 脚本,可以直接通过 cat 或者 vim 来查看它的内容:

bash

› cat /nix/store/a5gnycsy3cq4ix2k8624649zj8xqzkxc-nixos-system-nixos-23.05.20230624.3ef8b37/init
#! /nix/store/91hllz70n1b0qkb0r9iw1bg9xzx66a3b-bash-5.2-p15-riscv64-unknown-linux-gnu/bin/bash

systemConfig=/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b

export HOME=/root PATH="/nix/store/fifbf1h3i83jvan2vkk7xm4fraq7drm7-coreutils-riscv64-unknown-linux-gnu-9.1/bin:/nix/store/2w8nachmhqvbjswrrsdia5cx1afxxx60-util-linux-riscv64-unknown-linux-gnu-2.38.1-bin/bin"


if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then
    # Process the kernel command line.
    for o in $(</proc/cmdline); do
        case $o in
            boot.debugtrace)
                # Show each command.
                set -x
                ;;
        esac
    done


    # Print a greeting.
    echo
    echo -e "\e[1;32m<<< NixOS Stage 2 >>>\e[0m"
    echo


    # Normally, stage 1 mounts the root filesystem read/writable.
    # However, in some environments, stage 2 is executed directly, and the
    # root is read-only.  So make it writable here.
    if [ -z "$container" ]; then
        mount -n -o remount,rw none /
    fi
fi


# Likewise, stage 1 mounts /proc, /dev and /sys, so if we don't have a
# stage 1, we need to do that here.
if [ ! -e /proc/1 ]; then
    specialMount() {
        local device="$1"
        local mountPoint="$2"
        local options="$3"
        local fsType="$4"

        # We must not overwrite this mount because it's bind-mounted
        # from stage 1's /run
        if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" = true ] && [ "${mountPoint}" = /run ]; then
            return
        fi

        install -m 0755 -d "$mountPoint"
        mount -n -t "$fsType" -o "$options" "$device" "$mountPoint"
    }
    source /nix/store/vn0sga6rn69vkdbs0d2njh0aig7zmzi6-mounts.sh
fi


if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" = true ]; then
    echo "booting system configuration ${systemConfig}"
else
    echo "booting system configuration $systemConfig" > /dev/kmsg
fi


# Make /nix/store a read-only bind mount to enforce immutability of
# the Nix store.  Note that we can't use "chown root:nixbld" here
# because users/groups might not exist yet.
# Silence chown/chmod to fail gracefully on a readonly filesystem
# like squashfs.
chown -f 0:30000 /nix/store
chmod -f 1775 /nix/store
if [ -n "1" ]; then
    if ! [[ "$(findmnt --noheadings --output OPTIONS /nix/store)" =~ ro(,|$) ]]; then
        if [ -z "$container" ]; then
            mount --bind /nix/store /nix/store
        else
            mount --rbind /nix/store /nix/store
        fi
        mount -o remount,ro,bind /nix/store
    fi
fi


if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then
    # Use /etc/resolv.conf supplied by systemd-nspawn, if applicable.
    if [ -n "" ] && [ -e /etc/resolv.conf ]; then
        resolvconf -m 1000 -a host </etc/resolv.conf
    fi


    # Log the script output to /dev/kmsg or /run/log/stage-2-init.log.
    # Only at this point are all the necessary prerequisites ready for these commands.
    exec {logOutFd}>&1 {logErrFd}>&2
    if test -w /dev/kmsg; then
        exec > >(tee -i /proc/self/fd/"$logOutFd" | while read -r line; do
            if test -n "$line"; then
                echo "<7>stage-2-init: $line" > /dev/kmsg
            fi
        done) 2>&1
    else
        mkdir -p /run/log
        exec > >(tee -i /run/log/stage-2-init.log) 2>&1
    fi
fi


# Required by the activation script
install -m 0755 -d /etc /etc/nixos
install -m 01777 -d /tmp


# Run the script that performs all configuration activation that does
# not have to be done at boot time.
echo "running activation script..."
$systemConfig/activate


# Record the boot configuration.
ln -sfn "$systemConfig" /run/booted-system


# Run any user-specified commands.
/nix/store/91hllz70n1b0qkb0r9iw1bg9xzx66a3b-bash-5.2-p15-riscv64-unknown-linux-gnu/bin/bash /nix/store/cmvnjz39iq4bx4cq3lvri2jj0sjq5h24-local-cmds


# Ensure systemd doesn't try to populate /etc, by forcing its first-boot
# heuristic off. It doesn't matter what's in /etc/machine-id for this purpose,
# and systemd will immediately fill in the file when it starts, so just
# creating it is enough. This `: >>` pattern avoids forking and avoids changing
# the mtime if the file already exists.
: >> /etc/machine-id


# No need to restore the stdout/stderr streams we never redirected and
# especially no need to start systemd
if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then
    # Reset the logging file descriptors.
    exec 1>&$logOutFd 2>&$logErrFd
    exec {logOutFd}>&- {logErrFd}>&-


    # Start systemd in a clean environment.
    echo "starting systemd..."
    exec /run/current-system/systemd/lib/systemd/systemd "$@"
fi

简单总结下这个脚本的功能:

  1. 通过 mount -o remount,ro,bind /nix/store/nix/store 目录重新挂载为只读,确保 Nix Store 的不可变性,从而使系统状态可复现。
  2. 直接开始执行 $systemConfig/activate 这个程序。
  3. activate 完毕后,启动真正的 1 号进程 systemd,进入后续启动流程。

前面的 init 程序其实没干啥,根据我们看过的启动日志,大部分的功能应该都是在$systemConfig/activate 这个程序中完成的。

再看看其中的 $systemConfig/activate 的内容,它同样是一个 shell 脚本,直接 cat/vim 查看下:

bash

› cat root/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b/activate
#!/nix/store/91hllz70n1b0qkb0r9iw1bg9xzx66a3b-bash-5.2-p15-riscv64-unknown-linux-gnu/bin/bash

systemConfig='/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b'

export PATH=/empty
for i in /nix/store/fifbf1h3i83jvan2vkk7xm4fraq7drm7-coreutils-riscv64-unknown-linux-gnu-9.1 /nix/store/x3hfwbwcqgl9zpqrk8kvm3p2kjns9asm-gnugrep-riscv64-unknown-linux-gnu-3.7 /nix/store/qn0yhj5d7r432rdh1885cn40gz184ww9-findutils-riscv64-unknown-linux-gnu-4.9.0 /nix/store/slwk77dzar2l1c4h9fikdw93ig4wdfy1-getent-glibc-riscv64-unknown-linux-gnu-2.37-8 /nix/store/yrf57f5h1rwmf3q70msx35a2p9f0rsjr-glibc-riscv64-unknown-linux-gnu-2.37-8-bin /nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13 /nix/store/2imxx6v9xhy8mbbx9q1r2d991m81inar-net-tools-riscv64-unknown-linux-gnu-2.10 /nix/store/2w8nachmhqvbjswrrsdia5cx1afxxx60-util-linux-riscv64-unknown-linux-gnu-2.38.1-bin; do
    PATH=$PATH:$i/bin:$i/sbin
done

_status=0
trap "_status=1 _localstatus=\$?" ERR

# Ensure a consistent umask.
umask 0022

#### Activation script snippet specialfs:
_localstatus=0
specialMount() {
  local device="$1"
  local mountPoint="$2"
  local options="$3"
  local fsType="$4"

  if mountpoint -q "$mountPoint"; then
    local options="remount,$options"
  else
    mkdir -m 0755 -p "$mountPoint"
  fi
  mount -t "$fsType" -o "$options" "$device" "$mountPoint"
}
source /nix/store/vn0sga6rn69vkdbs0d2njh0aig7zmzi6-mounts.sh


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "specialfs" "$_localstatus"
fi

#### Activation script snippet binfmt:
_localstatus=0
mkdir -p -m 0755 /run/binfmt



if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "binfmt" "$_localstatus"
fi

#### Activation script snippet stdio:
_localstatus=0


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "stdio" "$_localstatus"
fi

#### Activation script snippet binsh:
_localstatus=0
# Create the required /bin/sh symlink; otherwise lots of things
# (notably the system() function) won't work.
mkdir -m 0755 -p /bin
ln -sfn "/nix/store/4y83vxk3mfk216d1jjfjgckkxwrbassi-bash-interactive-5.2-p15-riscv64-unknown-linux-gnu/bin/sh" /bin/.sh.tmp
mv /bin/.sh.tmp /bin/sh # atomically replace /bin/sh


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "binsh" "$_localstatus"
fi

#### Activation script snippet check-manual-docbook:
_localstatus=0
if [[ $(cat /nix/store/xzgmgymf510dicgppghq27lrh9fjpxfi-options-used-docbook) = 1 ]]; then
  echo -e "\e[31;1mwarning\e[0m: This configuration contains option documentation in docbook." \
          "Support for docbook is deprecated and will be removed after NixOS 23.05." \
          "See nix-store --read-log /nix/store/n232fhpqqqnlfjl0rj59xxms419glja2-options.json.drv"
fi


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "check-manual-docbook" "$_localstatus"
fi

#### Activation script snippet domain:
_localstatus=0


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "domain" "$_localstatus"
fi

#### Activation script snippet users:
_localstatus=0
install -m 0700 -d /root
install -m 0755 -d /home

/nix/store/6fap9xv6snx5fr2m7m804v4gc23pb1jh-perl-riscv64-unknown-linux-gnu-5.36.0-env/bin/perl \
-w /nix/store/gx91fdp4a099jpfwdkbdw2imvl3lalsk-update-users-groups.pl /nix/store/1zj6fk93qkqd3z8n34s4r40xnby2ci21-users-groups.json


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "users" "$_localstatus"
fi

#### Activation script snippet groups:
_localstatus=0


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "groups" "$_localstatus"
fi

#### Activation script snippet etc:
_localstatus=0
# Set up the statically computed bits of /etc.
echo "setting up /etc..."
/nix/store/habrmd12my31s9r9fdby78l2dg5p7qyx-perl-riscv64-unknown-linux-gnu-5.36.0-env/bin/perl /nix/store/rg5rf512szdxmnj9qal3wfdnpfsx38qi-setup-etc.pl /nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "etc" "$_localstatus"
fi

#### Activation script snippet hashes:
_localstatus=0
users=()
while IFS=: read -r user hash tail; do
  if [[ "$hash" = "$"* && ! "$hash" =~ ^\$(y|gy|7|2b|2y|2a|6)\$ ]]; then
    users+=("$user")
  fi
done </etc/shadow

if (( "${#users[@]}" )); then
  echo "
WARNING: The following user accounts rely on password hashing algorithms
that have been removed. They need to be renewed as soon as possible, as
they do prevent their users from logging in."
  printf ' - %s\n' "${users[@]}"
fi


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "hashes" "$_localstatus"
fi

#### Activation script snippet hostname:
_localstatus=0
hostname "lp4a"


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "hostname" "$_localstatus"
fi

#### Activation script snippet modprobe:
_localstatus=0
# Allow the kernel to find our wrapped modprobe (which searches
# in the right location in the Nix store for kernel modules).
# We need this when the kernel (or some module) auto-loads a
# module.
echo /nix/store/wv00igsmj6mkk1ybssdch52hx0hx0x67-kmod-riscv64-unknown-linux-gnu-30/bin/modprobe > /proc/sys/kernel/modprobe


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "modprobe" "$_localstatus"
fi

#### Activation script snippet nix:
_localstatus=0
install -m 0755 -d /nix/var/nix/{gcroots,profiles}/per-user


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "nix" "$_localstatus"
fi

#### Activation script snippet nix-channel:
_localstatus=0
# Subscribe the root user to the NixOS channel by default.
if [ ! -e "/root/.nix-channels" ]; then
    echo "https://nixos.org/channels/nixos-23.05 nixos" > "/root/.nix-channels"
fi


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "nix-channel" "$_localstatus"
fi

#### Activation script snippet systemd-timesyncd-init-clock:
_localstatus=0
if ! [ -f /var/lib/systemd/timesync/clock ]; then
  test -d /var/lib/systemd/timesync || mkdir -p /var/lib/systemd/timesync
  touch /var/lib/systemd/timesync/clock
fi


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "systemd-timesyncd-init-clock" "$_localstatus"
fi

#### Activation script snippet udevd:
_localstatus=0
# The deprecated hotplug uevent helper is not used anymore
if [ -e /proc/sys/kernel/hotplug ]; then
  echo "" > /proc/sys/kernel/hotplug
fi

# Allow the kernel to find our firmware.
if [ -e /sys/module/firmware_class/parameters/path ]; then
  echo -n "/nix/store/ann0ayjx9qf296pssrk2b26fry235idz-firmware/lib/firmware" > /sys/module/firmware_class/parameters/path
fi


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "udevd" "$_localstatus"
fi

#### Activation script snippet usrbinenv:
_localstatus=0
mkdir -m 0755 -p /usr/bin
ln -sfn /nix/store/fifbf1h3i83jvan2vkk7xm4fraq7drm7-coreutils-riscv64-unknown-linux-gnu-9.1/bin/env /usr/bin/.env.tmp
mv /usr/bin/.env.tmp /usr/bin/env # atomically replace /usr/bin/env


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "usrbinenv" "$_localstatus"
fi

#### Activation script snippet var:
_localstatus=0
# Various log/runtime directories.

mkdir -m 1777 -p /var/tmp

# Empty, immutable home directory of many system accounts.
mkdir -p /var/empty
# Make sure it's really empty
/nix/store/yln7ma9dldr3f2dva4l0iq275s4brxml-e2fsprogs-riscv64-unknown-linux-gnu-1.46.6-bin/bin/chattr -f -i /var/empty || true
find /var/empty -mindepth 1 -delete
chmod 0555 /var/empty
chown root:root /var/empty
/nix/store/yln7ma9dldr3f2dva4l0iq275s4brxml-e2fsprogs-riscv64-unknown-linux-gnu-1.46.6-bin/bin/chattr -f +i /var/empty || true


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "var" "$_localstatus"
fi

#### Activation script snippet wrappers:
_localstatus=0
chmod 755 "/run/wrappers"

# We want to place the tmpdirs for the wrappers to the parent dir.
wrapperDir=$(mktemp --directory --tmpdir="/run/wrappers" wrappers.XXXXXXXXXX)
chmod a+rx "$wrapperDir"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/chsh"
echo -n "/nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13/bin/chsh" > "$wrapperDir/chsh.real"

# Prevent races
chmod 0000 "$wrapperDir/chsh"
chown root:root "$wrapperDir/chsh"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/chsh"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/dbus-daemon-launch-helper"
echo -n "/nix/store/jqk530wxiq3832zyiqn8qi6i2pr3snnl-dbus-riscv64-unknown-linux-gnu-1.14.8/libexec/dbus-daemon-launch-helper" > "$wrapperDir/dbus-daemon-launch-helper.real"

# Prevent races
chmod 0000 "$wrapperDir/dbus-daemon-launch-helper"
chown root:messagebus "$wrapperDir/dbus-daemon-launch-helper"

chmod "u+s,g-s,u+rx,g+rx,o-rx" "$wrapperDir/dbus-daemon-launch-helper"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/fusermount"
echo -n "/nix/store/2d68cpnlqls47ijrwss83swjk2q1v953-fuse-riscv64-unknown-linux-gnu-2.9.9/bin/fusermount" > "$wrapperDir/fusermount.real"

# Prevent races
chmod 0000 "$wrapperDir/fusermount"
chown root:root "$wrapperDir/fusermount"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/fusermount"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/fusermount3"
echo -n "/nix/store/06w8lm5k9i2n1xhkszsf4pa9hw9l0r5s-fuse-riscv64-unknown-linux-gnu-3.11.0/bin/fusermount3" > "$wrapperDir/fusermount3.real"

# Prevent races
chmod 0000 "$wrapperDir/fusermount3"
chown root:root "$wrapperDir/fusermount3"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/fusermount3"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/mount"
echo -n "/nix/store/2w8nachmhqvbjswrrsdia5cx1afxxx60-util-linux-riscv64-unknown-linux-gnu-2.38.1-bin/bin/mount" > "$wrapperDir/mount.real"

# Prevent races
chmod 0000 "$wrapperDir/mount"
chown root:root "$wrapperDir/mount"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/mount"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/newgidmap"
echo -n "/nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13/bin/newgidmap" > "$wrapperDir/newgidmap.real"

# Prevent races
chmod 0000 "$wrapperDir/newgidmap"
chown root:root "$wrapperDir/newgidmap"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/newgidmap"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/newgrp"
echo -n "/nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13/bin/newgrp" > "$wrapperDir/newgrp.real"

# Prevent races
chmod 0000 "$wrapperDir/newgrp"
chown root:root "$wrapperDir/newgrp"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/newgrp"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/newuidmap"
echo -n "/nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13/bin/newuidmap" > "$wrapperDir/newuidmap.real"

# Prevent races
chmod 0000 "$wrapperDir/newuidmap"
chown root:root "$wrapperDir/newuidmap"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/newuidmap"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/passwd"
echo -n "/nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13/bin/passwd" > "$wrapperDir/passwd.real"

# Prevent races
chmod 0000 "$wrapperDir/passwd"
chown root:root "$wrapperDir/passwd"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/passwd"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/ping"
echo -n "/nix/store/mckzq3q58m31d8ax04gnjqx43niamis0-iputils-riscv64-unknown-linux-gnu-20221126/bin/ping" > "$wrapperDir/ping.real"

# Prevent races
chmod 0000 "$wrapperDir/ping"
chown root:root "$wrapperDir/ping"

# Set desired capabilities on the file plus cap_setpcap so
# the wrapper program can elevate the capabilities set on
# its file into the Ambient set.
/nix/store/z2gpziznsj8rnv55vyq5n287g5cvx7lg-libcap-riscv64-unknown-linux-gnu-2.68/bin/setcap "cap_setpcap,cap_net_raw+p" "$wrapperDir/ping"

# Set the executable bit
chmod u+rx,g+x,o+x "$wrapperDir/ping"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/sg"
echo -n "/nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13/bin/sg" > "$wrapperDir/sg.real"

# Prevent races
chmod 0000 "$wrapperDir/sg"
chown root:root "$wrapperDir/sg"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/sg"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/su"
echo -n "/nix/store/gbp100zp8a8gja22dyjz4nwv0qsxb7qy-shadow-riscv64-unknown-linux-gnu-4.13-su/bin/su" > "$wrapperDir/su.real"

# Prevent races
chmod 0000 "$wrapperDir/su"
chown root:root "$wrapperDir/su"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/su"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/sudo"
echo -n "/nix/store/scywdc7rd6cjfvji166a6d0bsjj90vys-sudo-riscv64-unknown-linux-gnu-1.9.13p3/bin/sudo" > "$wrapperDir/sudo.real"

# Prevent races
chmod 0000 "$wrapperDir/sudo"
chown root:root "$wrapperDir/sudo"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/sudo"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/sudoedit"
echo -n "/nix/store/scywdc7rd6cjfvji166a6d0bsjj90vys-sudo-riscv64-unknown-linux-gnu-1.9.13p3/bin/sudoedit" > "$wrapperDir/sudoedit.real"

# Prevent races
chmod 0000 "$wrapperDir/sudoedit"
chown root:root "$wrapperDir/sudoedit"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/sudoedit"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/umount"
echo -n "/nix/store/2w8nachmhqvbjswrrsdia5cx1afxxx60-util-linux-riscv64-unknown-linux-gnu-2.38.1-bin/bin/umount" > "$wrapperDir/umount.real"

# Prevent races
chmod 0000 "$wrapperDir/umount"
chown root:root "$wrapperDir/umount"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/umount"

cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/unix_chkpwd"
echo -n "/nix/store/cn72qv0n576vg61mgaran7g2vj6gdjwn-linux-pam-riscv64-unknown-linux-gnu-1.5.2/bin/unix_chkpwd" > "$wrapperDir/unix_chkpwd.real"

# Prevent races
chmod 0000 "$wrapperDir/unix_chkpwd"
chown root:root "$wrapperDir/unix_chkpwd"

chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/unix_chkpwd"


if [ -L /run/wrappers/bin ]; then
  # Atomically replace the symlink
  # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
  old=$(readlink -f /run/wrappers/bin)
  if [ -e "/run/wrappers/bin-tmp" ]; then
    rm --force --recursive "/run/wrappers/bin-tmp"
  fi
  ln --symbolic --force --no-dereference "$wrapperDir" "/run/wrappers/bin-tmp"
  mv --no-target-directory "/run/wrappers/bin-tmp" "/run/wrappers/bin"
  rm --force --recursive "$old"
else
  # For initial setup
  ln --symbolic "$wrapperDir" "/run/wrappers/bin"
fi


if (( _localstatus > 0 )); then
  printf "Activation script snippet '%s' failed (%s)\n" "wrappers" "$_localstatus"
fi


# Make this configuration the current configuration.
# The readlink is there to ensure that when $systemConfig = /system
# (which is a symlink to the store), /run/current-system is still
# used as a garbage collection root.
ln -sfn "$(readlink -f "$systemConfig")" /run/current-system

# Prevent the current configuration from being garbage-collected.
mkdir -p /nix/var/nix/gcroots
ln -sfn /run/current-system /nix/var/nix/gcroots/current-system

exit $_status

这个脚本有点长,简单总结下它干了啥:

  1. 通过 source /nix/store/vn0sga6rn69vkdbs0d2njh0aig7zmzi6-mounts.sh 挂载一些目录,看下这个文件内容就知道,挂的是 /proc /sys /dev /rum 等几个临时目录。
  2. 通过 mkdir/install 等指令自动创建 /home /root /bin /usr /usr/bin 等目录
  3. 通过perl /nix/store/rg5rf512szdxmnj9qal3wfdnpfsx38qi-setup-etc.pl /nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc 配置生成 /etc 目录中的各种文件。
  4. 通过 ln 命令添加其他各种软链接,以及一些别的设置。

其中第三步 etc 目录的设置,实际数据基本都来自该脚本的第二个参数:

bash

› ls root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc
╭────┬──────────────────────────────────────────────────────────────────────────┬─────────┬────────┬──────────────╮
# │                                   name                                   │  type   │  size  │   modified   │
├────┼──────────────────────────────────────────────────────────────────────────┼─────────┼────────┼──────────────┤
0 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/bashrc           │ symlink │   54 B │ 54 years ago │
1 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/binfmt.d         │ dir     │ 4.1 KB │ 54 years ago │
2 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/dbus-1           │ symlink │   50 B │ 54 years ago │
3 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/default          │ dir     │ 4.1 KB │ 54 years ago │
4 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/dhcpcd.exit-hook │ symlink │   60 B │ 54 years ago │
5 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/fonts            │ symlink │   69 B │ 54 years ago │
6 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/fstab            │ symlink │   53 B │ 54 years ago │
7 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/fuse.conf        │ symlink │   57 B │ 54 years ago │
8 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/host.conf        │ symlink │   57 B │ 54 years ago │
9 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/hostname         │ symlink │   56 B │ 54 years ago │
10 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/hosts            │ symlink │   49 B │ 54 years ago │
11 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/inputrc          │ symlink │   51 B │ 54 years ago │
12 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/issue            │ symlink │   49 B │ 54 years ago │
13 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/kbd              │ symlink │   61 B │ 54 years ago │
14 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/locale.conf      │ symlink │   55 B │ 54 years ago │
15 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/login.defs       │ symlink │   54 B │ 54 years ago │
16 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/lsb-release      │ symlink │   59 B │ 54 years ago │
17 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/lvm              │ dir     │ 4.1 KB │ 54 years ago │
18 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/man_db.conf      │ symlink │   59 B │ 54 years ago │
19 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/modprobe.d       │ dir     │ 4.1 KB │ 54 years ago │
20 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/modules-load.d   │ dir     │ 4.1 KB │ 54 years ago │
21 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/nanorc           │ symlink │   54 B │ 54 years ago │
22 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/netgroup         │ symlink │   56 B │ 54 years ago │
23 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/nix              │ dir     │ 4.1 KB │ 54 years ago │
24 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/nscd.conf        │ symlink │   57 B │ 54 years ago │
25 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/nsswitch.conf    │ symlink │   61 B │ 54 years ago │
26 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/os-release       │ symlink │   58 B │ 54 years ago │
27 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/pam              │ dir     │ 4.1 KB │ 54 years ago │
28 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/pam.d            │ dir     │ 4.1 KB │ 54 years ago │
29 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/pki              │ dir     │ 4.1 KB │ 54 years ago │
30 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/profile          │ symlink │   55 B │ 54 years ago │
31 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/protocols        │ symlink │   75 B │ 54 years ago │
32 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/pulse            │ dir     │ 4.1 KB │ 54 years ago │
33 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/resolvconf.conf  │ symlink │   63 B │ 54 years ago │
34 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/rpc              │ symlink │   90 B │ 54 years ago │
35 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/samba            │ dir     │ 4.1 KB │ 54 years ago │
36 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/services         │ symlink │   74 B │ 54 years ago │
37 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/set-environment  │ symlink │   59 B │ 54 years ago │
38 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/shells           │ symlink │   54 B │ 54 years ago │
39 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/ssh              │ dir     │ 4.1 KB │ 54 years ago │
40 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/ssl              │ dir     │ 4.1 KB │ 54 years ago │
41 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/sudoers          │ symlink │   51 B │ 54 years ago │
42 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/sudoers.gid      │ file    │    3 B │ 54 years ago │
43 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/sudoers.mode     │ file    │    5 B │ 54 years ago │
44 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/sudoers.uid      │ file    │    3 B │ 54 years ago │
45 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/sysctl.d         │ dir     │ 4.1 KB │ 54 years ago │
46 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/systemd          │ dir     │ 4.1 KB │ 54 years ago │
47 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/terminfo         │ symlink │   70 B │ 54 years ago │
48 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/tmpfiles.d       │ dir     │ 4.1 KB │ 54 years ago │
49 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/udev             │ dir     │ 4.1 KB │ 54 years ago │
50 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/vconsole.conf    │ symlink │   57 B │ 54 years ago │
51 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/zoneinfo         │ symlink │   97 B │ 54 years ago │
├────┼──────────────────────────────────────────────────────────────────────────┼─────────┼────────┼──────────────┤
# │                                   name                                   │  type   │  size  │   modified   │
╰────┴──────────────────────────────────────────────────────────────────────────┴─────────┴────────┴──────────────╯

这个 perl 脚本基本就是根据这个 nix store 中的 etc 文件夹,生成 /etc 目录中的各种文件或软链接。

NixOS 要能在 LicheePi 4A 上正常启动,还需要有硬件固件的支持,因此光了解 NixOS 的启动流程还不够,还需要了解硬件固件的启动流程。这里简要介绍下 Linux 在 RISC-V 上的启动流程。

U-Boot 是嵌入式领域最常用的 bootloader,

对于一般嵌入式系统而言只需要一个 u-boot 作为 bootloader 即可,但入今的嵌入式 IC 已经转向 SOC 片上系统,其内部不仅仅是一颗 CPU 核,还可能包含各种各样的其他 IP,因而相关的上层软件也需要针对性的划分不同的功能域,操作域,安全域等上层应用。为了支持这些复杂而碎片化的应用需求,又或者因为 SRAM 太小以致无法放下整个 bootloader,SOC 的 Boot 阶段衍生出了多级 BootLoader,u-boot 为此定义了二三级加载器:

  • spl:Secondary Program Loader,二级加载器
  • tpl:Tertiary Program Loader,三级加载器

spl 和 tpl 走 u-boot 完全相同的 boot 流程,不过在 spl 和 tpl 中大多数驱动和功能被去除了, 根据需要只保留一部分 spl 和 tpl 需要的功能,通过 CONFIG_SPL_BUILD 和 CONFIG_TPL_BUILD 控制;一般只用 spl 就足够了,spl 完成 ddr 初始化,并完成一些外设驱动初始化,比如 usb,emmc, 以此从其他外围设备加载 u-boot,但是如果对于小系统 spl 还是太大了,则可以继续加入 tpl,tpl 只做 ddr 等的特定初始化保证代码体积极小,以此再次从指定位置加载 spl,spl 再去加载 u-boot。

LicheePi4A 就使用了二级加载器,它甚至写死了 eMMC 的分区表,要求我们使用 fastboot 往对应的分区写入 u-boot-spl.bin,官方给出的命令如下:

bash

# flash u-boot into spl partition
sudo fastboot flash ram u-boot-with-spl.bin
sudo fastboot reboot
# flash uboot partition
sudo fastboot flash uboot u-boot-with-spl.bin

网上找到的一个图,涉及到一些 RISC-V 指令集相关的知识点:

RISCV 开发版当前的引导流程

根据我们前面的 NixOS 启动日志,跟这个图还是比较匹配的,但我们没观察到任何 U-Boot 日志,有可能是因为 U-Boot 没开日志,暂时不打算细究。

前面的 NixOS 启动日志跟启动流程图中都出现了 OpenSBI,那么 OpenSBI 是什么呢?为什么 ARM 开发版的启动流程中没有这么个玩意儿?

查了下资料,大概是说因为 RISC-V 是一个开放指令集,任何人都可以基于 RISC-V 开发自己的定制指令集,或者定制 IC 布局。这显然存在很明显的碎片化问题。OpenSBI 就是为了避免此问题而设计的, 它提供了一个标准的接口,即 Supervisor Binary Interface, SBI. 上层系统只需要适配 SBI 就可以了,不需要关心底层硬件的细节。IC 开发商也只需要实现 SBI 的接口,就可以让任何适配了 SBI 的上层系统能在其硬件平台上正常运行。

而 OpenSBI 则是 SBI 标准的一个开源实现,IC 开发商只需要将 OpenSBI 移植到自己的硬件平台上即可支持 SBI 标准。

而 ARM 跟 X86 等指令集则是封闭的,不允许其他公司修改与拓展其指令集,因此不存在碎片化的问题,也就不需要 OpenSBI 这样的东西。

  1. fw_dynamic.bin: 我们 NixOS 镜像的 /boot 中就有这个固件,它是 OpenSBI 的编译产物。
    1. RevyOS 的定制 OpenSBI 构建方法:https://github.com/revyos/thead-opensbi/blob/lpi4a/.github/workflows/build.yml
  2. u-boot-spl.bin: 这个文件是 u-boot 的编译产物,它是二级加载器。
    1. RevyOS 的定制 u-boot 构建方法:https://github.com/revyos/thead-u-boot/blob/lpi4a/.github/workflows/build.yml

因为历史原因,TH1520 设计时貌似 RVV 还没出正式的规范,因此它使用了一些非标准的指令集,GCC 官方貌似宣称了永远不会支持这些指令集…(个人理解,可能有误哈)

因此为了获得最佳性能,LicheePi4A 官方文档建议使用 T-Head 提供的工具链编译整个系统。

但我在研究了 NixOS 的工具链实现,以及咨询了 @NickCao 后,确认了在 NixOS 上这几乎是不可行的。NixOS 因为不遵循 FHS 标准,它对 GCC 等工具链做了非常多的魔改,要在 NixOS 上使用 T-Head 的工具链,就要使这一堆魔改的东西在 T-Head 的工具链上也能 Work,这个工作量很大,也很有技术难度。

所以最终选择了用 NixOS 的标准工具链编译系统,@revy 老师也为此帮我做了些适配工作,解决了一些标准工具链上的编译问题。

Issue 区也有人提到了这个问题,Revy 老师也帮助补充了些相关信息:https://github.com/ryan4yin/nixos-licheepi4a/issues/14

到这里,NixOS 在 LicheePI4A 上启动的整个流程就基本讲清楚了, NixOS 跟其他传统发行版在启动流程中最大的区别是它自定义了一个 init 脚本,在启动 systemd 之前,它会先执行这个脚本进行文件系统的初始化操作,准备好最基础的 FHS 目录结构,使得后续的 systemd 以及其他服务能正常启动。正是因为这个 init 脚本,NixOS 才能在仅有 /boot/nix 这两个目录的情况下正常启动整个系统。

NixOS 数据的集中化只读存储使更多的骚操作成为可能,比如直接使用 tmpfs 作为根文件系统,将需要持久化的目录挂载到外部存储设备上,这样每次重启系统时,所有预期之外的临时数据都会被清空,进一步保证了系统的可复现性与安全性。如果你有系统洁癖,而且有兴趣折腾,那就快来看看 @LanTian 写的NixOS 系列(四):「无状态」操作系统 吧~

最终在 LicheePi4A 成功启动后的登录的截图:

NixOS 成功启动

那么基于我们到目前为止学到的知识,要如何构建出一个可以在 LicheePi 4A 上运行的 NixOS 镜像呢?

这个讲起来就很费时间了,涉及到了 NixOS 的交叉编译系统内核 override,flakes,镜像构建等等,要展开讲的话也是下一篇文章了,有兴趣的可以直接看我的 NixOS on LicheePi4A 仓库:https://github.com/ryan4yin/nixos-licheepi4a.

简单的说,NixOS 跟传统 Linux 发行版的系统镜像构建思路是一致的,但因为其声明式与可复现性的特点,实际实现时出现了非常大的区别。以我的项目仓库为例,整个项目完全使用 Nix 语言声明式编写(内嵌了部分 Shell 脚本…),而且这份配置也可用于系统后续的持续声明式更新部署(我还给出了一个 demo)。

最后,再推荐一波我的 NixOS 入门指南:ryan4yin/nixos-and-flakes-book, 对 NixOS 感兴趣的读者们,快进我碗里来(