2024-08-27 10:10:22
很早就有了解到今年的 KubeCon China 会在香港举办,虽然有些兴趣,但我最初是有被 KubeCon 高昂的门票价格劝退了的。
有时候不得不相信运气的魔力,机缘巧合之下,我从朋友 @Kev 处得知了 KubeCon 的「最终用户门票计划」并借此 0 元购了门票,又邀上了 0xFFFF 社区 的@Chever-John @0xdeadbeef @茗洛 三位朋友一起参加,在香港租了个 airbnb 住宿,期间也逛了香港城市中的不少地方,收货颇丰。
其实本来也尝试过邀请其他认识的朋友同事,但都因为种种原因无法参加,略感遗憾。
本文多图,也挺多技术无关的内容,为了方便想了解技术的朋友,我先做个大致的总结。
从 KubeCon 回来后我又听了些 CNCF 其他的会议视频,其中比较有印象的是这几个:
结合 KubeCon China 三天的经历,以及上面这些视频的内容,我大概的感觉是:
KubeCon China 2024 的会议视频将会陆续被添加到如下这个 Youtube Playlist 中,有兴趣的朋友可以一观:
视频相关的 PPT 可以在这里下载:
这次我主要关注的是 Istio、Gateway API 相关的议题,最近在研究 Istio 的 Ambient Mode,因此希望能够从会议中了解到更多的实现细节与其中的权衡。
三天下来听到的内容也很好的满足了我的期待,Istio / Envoy Gateway / Ingress Controller 的几位核心贡献者分享了很多这些项目的最新进展,实现细节,以及未来的发展方向。
Ambient Mode 在最近 beta 了,是我关注的重点,总结下目前了解到的几个关键点:
以及一些其他方案:
总体而言,KubeCon 是一次了解行业前沿技术动态、跟项目开发者及其他行业内的技术人面对面认识交流的好机会,可以帮助自己提升技术视野,维持技术热情与动力,不至于局限在公司业务中闭门造车。
因为要在香港呆三天,衣食住行是必须要考虑的事情。这方面我拉上的几位朋友都比较有旅行住宿的经验,我们最后在香港找了个离会场不远的 airbnb 住宿,最终的体验也是相当不错。房间干净整洁有格调,虽然我觉得稍微有点小了,但朋友说这个空间在香港都是一家三四口住的标准,已经吊打同价位的酒店了。
虽然提早定了住宿,做了点功课,但第一天就出了问题——深圳这边一直下雨导致 @Chever-John 的飞机直接被取消,改订了另一趟航班也晚点。虽然正点到达了会场,但他一晚上就睡了俩小时,在深圳定的前一晚的酒店也没住成,第一天看他整个人都听得迷迷糊糊的。不过没事,至少我听了个爽
说回正题,到了会场领完胸牌,我们就开始了为期三天的 KubeCon China 之旅。
具体的技术内容已经在前文总结过了,这里主要就贴些照片吧。
然后晚上@茗洛带着我们逛了香港的诚品书店,书店好几层,但感兴趣的书不多。后面又逛了好多电子商城、二次元周边店,我算是开了眼界。
第一天就差不多是这样,听了点技术,晚上逛了逛香港,回去休息。
首先是听了华为的演讲,介绍了 Kmesh 的方案创新,技术细节讲得很赞。想看 PPT 与视频请移步用内核原生无边车架构彻底改变服务网格 - Xin Liu, Huawei Technologies Co., Ltd.
还听了晋涛老师的十年云原生之旅:容器技术和Kubernetes生态系统的演变 - Jintao Zhang, Kong Inc.
然后晚上我们随便走了走逛了逛,看了看香港海边夜景。
第三天早上是我这次最期待的,Linus 的访谈,见到了本人,这次行程也圆满了。
第三天没什么我特别感兴趣的话题,听完 Linus 的访谈后随便逛了逛,跟几位 朋友合了个影,就搭地铁回家了。
另外朋友听了个 TiDB 的演讲,看 PPT 是有点意思的哈哈。
以及在项目展厅三天逛下来,我帆布袋领了四个,T恤领了三件,还有别的小礼品一堆,吃的喝的都不用说了,管够。另外看网上不少信息说香港的服务业态度很差,但这家酒店可能星级比较高,体验还是相当到位的。
总之,体验相当不错的,有机会的话明年还来!Love you, KubeCon China & Hong Kong!
2024-07-10 09:17:31
Kubernetes 具有非常丰富的动态伸缩能力,这体现在多个层面:
本文主要介绍新兴 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)来间接地控制节点(云服务器)的数量。这样的设计导致了一些问题:
而 Karpenter 则完全从零开始实现了一套节点管理系统,它直接管理所有节点(云服务器,如 AWS EC2),负责节点的创建、删除、修改等操作。
相较 Cluster Autoscaler, Karpenter 的优势主要体现在以下几个方面:
总之,个人的使用体验上,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 有一些功能上的重叠:
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-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
这是我第一次出境游,整个行程都是临时起意,没有做任何准备,即使是过了罗湖口岸,站在东铁线地铁上的时候,我对整个行程也并无太多期待,心里想着无非是不同的人与物,无甚特殊的。但总得出去走走,看看大陆之外究竟是个什么样子,所以就这么随意地来到了香港。
具体有多随意呢?就这么说,过了口岸没多久,手机就没信号了,我这才意识到香港终归不是大陆,慌得我又出站再往回坐,在罗湖口岸连上网络开了个漫游跟流量包,这才算是正式开始了我的香港之旅。
最初期待甚少,但现实常常超出我的想象。在香港徒步的一日,我切实感受到了更宽广更多元化的世界,这是在大陆境内无法体会到的。
刚上东铁线甚至刚出地铁站的时候,我对自己已经出境这一点尚无实感,因为香港的地铁跟深圳实在太像了,不论是地铁车厢的设计还是车站的建筑风格、干净整洁的程度,都跟深圳非常相似。
后面跟朋友聊起来,才知道国内深圳地铁跟香港地铁是有合作关系的,很多技术最初都来自香港,算是一脉相承了。
在去往维多利亚港的路上,看到香港多的双层巴士、随处的繁体字跟英文标识,才逐渐感觉到这里确实是个不同的地方。
接着就到了维港的海边步道,海景很美,两岸都是高楼大厦,海上有很多游船,还有很多钓鱼的人。
接着我意识到,从我住处可全程地铁直达维港,坐个地铁再浅浅走几步路就能看到这么漂亮的海景,这不比去深圳大鹏半岛方便多了!我为此兴奋起来。这风景吊打深圳湾,但深圳湾步道上的人甚至比维港还多,只能说深圳人真的太多了…
补充:听说深圳湾以前真的是海边,但填海造路使它成了现在的烂泥滩
沿着海岸线继续走,渐渐发现人群里外国游客相当的多,粗略估算超过一半是外国人,这让我感受到了香港的国际化氛围。
海边还有很多细节值得一看。
在维港逗留玩耍了一个小时,买了点面包当午餐吃了,然后就沿着海边步道继续往前走,也没啥明确的目的地,反正累了就回家。维港的人很多,但出了维港后步道上人就少了很多,走在这样的路上看着风景,相当闲适。
悠闲、轻快地随走随停,大概一个半小时后,到达了步道的终点——坚尼地城,我还有点失望,这步道居然就这么没了。只好掉转方向往城市内走,市内倒也别有一番风景。
在市内逛了逛,确认了香港各种店铺的特点是小而美,各种店铺都非常小,但弄得比较精致,能看得出花了心思。相对而言深圳的商场店铺就大气很多了,但也缺了香港这些店铺的种种细节。
玩得尽兴,又找了家店吃饱喝足,打道回府。
这一趟下来,我的感觉是,香港确实是国际化大都市,跟深圳的气息很不一样,海边随处可见的酒吧, 比例非常高的外国游客跟在香港工作的外国人,让我感觉到了一种国际化的氛围。至于城市的繁华程度,跟深圳各有千秋。
食髓知味,在香港海边徒步一日后,我对香港徒步来了兴趣,一直瞅着机会再来一次。
到了五一假期,我终于有了机会,随便在小红书上查了些资料,发现麦理浩径一二段挺有名,还能露营,于是就决定是它了。
看天气预报最近几天都有雨,五一那天下得比较大没出门,顺便就买了迪卡侬冲锋衣、冲锋裤、速干 T 恤、大号水杯,想着明天就算有雨也要去。
幸而天公作美,第二天天气转阴,上午东西到货,又做了大半天准备才出门。因为计划是连续步行两天,所以跟着小红书上的香港旅游攻略,吃的穿的用的都带了挺多。
下午出境,首先买了 OPPO 9.9 元的香港流量包(五一特惠),然后坐地铁到深水埗(bù)地铁站整了张八达通,200 港币,其中 50 块押金。另外考虑到以后会常去香港,又多充了 100 块。
接着就是各种地图导航,首先坐小巴到西贡码头,然后查半天导航,坐九巴 94 路到麦理浩径起点。
小巴要叫停车才停,真的 I 人(内向)天敌,决定以后尽量选九巴,下一站前按个 Stop (香港人称掀钟)这种才适合 I 人。
导航本来是说坐到麦理浩夫人度假村站,但我一下没注意就坐过了,然后下车时第一次「掀钟」不熟悉,又过了一站才按到 Stop,结果就是在「北潭凹管理站」才下车(16:34),然后往回走了半个小时才到「麦理浩径起点」(17:09)。
从「麦理浩径起点」往万宜水库的路上(大网仔路)看到很多出租车来来往往,联想到之前查的攻略, 就知道这些出租车是为了接送徒步的人。我是单人徒步,为了省 150 的打车费,就选择了坐九巴到起点然后步行。
路上很多烧烤点,貌似是政府修建的,可以免费用,但没遇到过人在用这些烧烤点,可能季节或天气原因?
到达万宜水库岔路口时已经是 17:26 了,走第一段晚上肯定到不了我的目标地址「咸田湾」,而且我了解到的是二段风景最好。一番心理斗争后,我直接走了左边的路线,绕过麦径一段,直接走到西湾亭走二段的路线。
沿着大路走了整整一个小时才到西湾亭,为了省个出租车费我也是拼了。不过倒也不算亏,这两个小时的路上风景相当不错,而且没遇到几个人,体验非常棒!有种我很喜欢的孤独感。
这时已经是 18:34,天色已经快黑了,我有点慌,刚好有辆出租送人到这,我向司机问路,他好心地帮我指了路。从这里开始才是山路,前面两个小时一直走在大马路上,而且我的脚在走了两个小时后已经很痛了。
接着就手机打着灯,一直走到 19:10 才到西湾村。
看到第一个租帐篷的店,立马就整了套帐篷。押金 300 租一晚 200,跟国内比可能很贵,但跟香港劳动节期间 1300+ 的旅馆比已经性价比爆棚了!
我之前的目标是到咸水湾租帐露营篷,但到西湾村的时候天已经完全黑了,而且一路心惊胆战地连续赶了三个小时路后我也累得不行了,在往西湾村的路上我就一直憧憬着西湾村总该能找到地方住,万幸它没让我失望。
本来想吃口热乎的,但看到店里 55 港币起步的面食,我还是决定吃点自己的能量棒当晚餐。
洗澡收费 30,但人太多了,我就没洗。晚上海浪声、旁边帐篷里人的说话声等,环境变化太大,睡不着,小说看到凌晨三点,然后跑去海滩上听了听海浪声,回来的路上一路被村民家的狗狂吠,惊出我一声冷汗,万幸我还懂点这种场面的处理方式,盯着狗看,慢慢后退。即使走得远了,我还是一步三回头,置到回到营地把门关上才松了一口气。并且明白一个道理 —— 晚上还是不要这么跳脱的好…
西湾村手机信号挺好,但用的人太多带宽不够,很多站点怎么刷都刷不出来… 想起 21 年看过的《走出荒野》,讲的是通过徒步自我救赎的故事,打算翻出来读一读但网络问题根本刷不出来。
然后迷糊睡到了早上 7 点,出帐篷发现我这个营地人都走差不多了。想洗漱但意识到我带了牙刷却没带牙膏…因为最初是打算住旅馆的,谁 TM 知道居然没找到个便宜旅馆,香港的物价太 TM 离谱了, 逼得我连夜赶路到这里来住帐篷。
厕所也没上,早餐看太贵了也没吃,买了瓶 0.6L 的水(20 港币),07:24 直接出发了,早餐仍然是能量棒配水。
第二天一早从西湾村出发。
没走几步就看到了西湾营地,原来西湾村过来没几步就是西湾营地,垃圾好多…
西湾营地这有黄色标牌,写着野猪出没。朋友也提醒过我,说它们不咬人,但如果你包里有吃的,它们会弄坏你的包。
过了西湾营地又开始爬山。
07:58 到达我印象中全程风景最赞的地方 —— 接近咸田湾沙滩的一段步道。
08:15 到达咸田湾沙滩,这里人很多,帐篷也很多。从这里开始接着是往赤径去,钻的就是山路了,这一段不是海景,但也别有韵味。
10:00 到达赤径,这一处水湾风景也很美!在这里玩了很久。
10:30 到达赤径公厕解决了下个人卫生问题。意外发现赤径公厕个别涂鸦很可爱!随手一拍。
接着是从赤径往北潭凹的路,同样是起起伏伏。
路上遇到一泡插着鲜花的牛粪,很有意思,前面的女生蹲下拍照,搞得我差点以为花是她插的… 一路上再怎么陡峭的步道上都能看得到牛粪,挺原生态的哈哈。甚至回去路上在西贡码头汽车站都看到了两头牛在绿化带上吃草。
接着一路上攀,脚踝已经酸痛得不行,非常痛苦。有一段累到直接坐在地上看了 20 分钟《走出荒野》。
11:57 到达麦径二段的终点,走不动了。
又坐九巴 94 路回西贡市区,来去都是这趟车,从「北潭凹管理站」下车又从「北潭凹」上车,几乎是围着这一片走了刚好一圈。
12:30 到西贡市区,饿得不行,找一圈吃饭的地儿发现,麦当劳居然是最实惠的——因为它价格跟国内差不多。于是搞了一顿麦当劳,发现公司有点事,因为今天我 Oncall,顺便拿出 MacBook 处理了下公司的事。是的没错,我背包里还有一台 1.4kg 的 MacBook Pro,真是要了老命。
累得不行,昨晚又没洗澡,一路上疯狂出汗,浑身气味比较感人。吃完饭随便逛了逛,就打道回府了。从西贡坐九巴 299x 路到沙田站,然后转乘东铁线到罗湖口岸,再坐深圳地铁回家。
总的来说,香港西贡这块开发得更好,步道很多,原生态的同时路线也足够成熟,而且过来比深圳大鹏半岛更方便,期待下次再来。
查攻略最有用的几个 APP:
上次徒步走完麦理浩径二段后就有点上头了,刚回到家累得不行,脚都要废了,但隔天还想找个步道走走。另外就是这两次徒步都太随意,缺乏登山杖、登山鞋、背包等专业装备,而且露营还是租的人家帐篷,接着就是看各种徒步教程攻略,疯狂买买买。
买的一堆装备陆续到货,在家试用了好几天,比如说穿登山鞋上班、空调开 16 度在室内搭帐篷露营、床上铺蛋巢垫盖睡袋、晚餐吃自热米饭,等等不一而足 emmm
接着 5 月 18 跟 19 两日又是个空闲的周末,这次做足了准备,计划两天走完麦理浩径三四段。
整理背包时,意识到气罐很难处理,带着上地铁、过海关,感觉都不太行,在香港我也不知道是否好买,所以把锅具跟炉子都踢出了背包,只带了两盒海底捞自热米饭跟一些能量胶、压缩饼干以及零食。另外水带得相当充足,3L 水袋 + 600ml 小水杯,光水就有 3.6kg.
大约 9 点多出发,首先是乘东铁线到沙田站,转程九巴 299x 路到麦边站,再转乘 94 路到麦径三段的起点。
在二段终点解决完个人卫生问题,热了个身,顺便帮一批反穿麦径二段的大陆人合了个影,接着就开始上山。
三段一开始就是急攀,路面很陡,而且透露着一股年久失修的味道,路况比一二段差远了。
三段的人流量相比二段那是断崖式下降,几乎依山遇不到人,有一种远离人世的孤独感。有的人可能会喜欢热闹,但我恰恰相反,相当享受这种孤独感。
上升太快,我又负重 17 公斤,没爬几步就累到要休息,甚至有点怀疑今天能不能走完三段。但辛苦带来的收获也挺大,越往上爬,风景越美,山景与海景交相辉映,让人心旷神怡。
在山顶还解锁了一些隐藏支线,因为走的人少,灌木丛茂盛,登山杖几乎没法用,这时候就很庆幸我穿了长袖紧身运动打底衣裤,不然走这种路小腿难免挂彩。
接着就是下降。
下降路段走完,到达嶂上营地跟士多店。
又开始上升,不过跟三段起始的那段比起来,这段路还算平缓。接着我高兴没多久,就到了一段相当陡峭,几乎没有台阶的路段。
又二十多分钟后,累得不行,寻个地坐下,顺便回头看看。
继续走,没多久就到了 17 点,天开始黑了,我也提前翻出了头灯准备着随时打开。
到达营地开始搭帐篷,营地很空旷,只有我跟另一伙人露营。
18 号这一天下来一共走了 33582 步,而且背着十七八公斤爬上爬下,最后两三公里完全是咬着牙拼命爬的。
7 点多,在流动厕所解决完卫生问题,在营地逛了一圈没找到水源,只好拿折叠水桶在流动厕所的洗手池接了点水洗脸顺便擦身体。
早餐随便吃了点东西,接着就收拾帐篷打算回家,发现收拾起来还挺费劲,慢慢吞吞弄了也大概 40 分钟,而且发现蛋巢垫下面有跟毛毛虫,帐篷内还有好几只蚂蚁,还发现一只跳蚤,另外内帐外面也爬了根看着就很毒的毛毛虫…
蛋巢垫下的毛毛虫、帐篷内的跳蚤大概是昨晚搭帐篷时,把东西放在一个石墩上,从石墩上爬上来的, 蚂蚁可能是从内帐的孔洞爬进来的。总之它们吓得我收拾东西的时候检查了一遍又一遍,生怕碰到虫子或者把虫子收拾进了行李中。
总的来说,计划两日麦理浩径徒步,实际只 18 号走完麦径三段就精疲力尽了,第二日早上直接打道回府。从二段起点走到终点,用时 12:45 - 20:30,在交界处吃晚饭、休息了 1 个小时。之后从 21:50 - 22:15 走到水浪窝营地,到 22:50 才搭好帐篷。这是我第一次重装徒步,积累了宝贵的经验,也发现了许多问题,下次再徒步麦理浩径肯定能更得心应手了~
只爬完第三段就精疲力尽,我分析了主要有这几个原因:
徒步完第三段后,休息没一周,又是周末,周五下班后赶紧跑去续签了香港签证,精简了一番装备,周六上午就再次出发徒步第四段了。
这次出发前根据上次的经验精简了装备,总总量应该轻了大概 1kg,但仍旧有 16kg。主要变化:
四段是麦径难度最高的一段,而且终点基维尔营地不接受个人预约,只接受团体预约。但我还是抱着侥幸心理背着重装背包去了,想着路上总不会只有这一个营地吧(后面的经历证明我有点鲁莽了)。
早饭吃得饱饱的,又洗了澡、休息了会儿消消食,然后 10 点左右就从家里出发了。
走了约十分钟到达三段终点,在这里用直饮水机将 3L 水袋灌满,然后取出登山杖就出发了。
首先是到达上次露营过的水浪窝营地,然后沿着大路继续上山,路上没人。
沿大路到达山顶后,是沿着黄泥小路下山。这两天下雨,路面泥泞湿滑,我又背着个 16kg 的重装背包,走起来有点难度。还好有登山杖,倒不用怕滑倒。
走黄泥路下完山,接着又是沿着石阶路开始登山,石阶也有点湿滑。
登山没多久,就遇到了雾气,接着雾就越来越浓。
又到达一座山顶,在这里休息了一阵子,吃了点东西,接着突然想到我出发时没拉伸,想着补救一下, 就在这里做了个拉伸。
中间还遇到位外国女士背着个很小的跑步背包爬上来,也休息了一会儿喝了口水,往另一边去了,很快消失在了雾中。
休息好了开始下山,没多久就到了四段风景最好的一段,因为浓雾没远景看没,如果没雾这里视野会是很开阔的。
继续前行,到达昂平营地,这里是一块山顶平原草地,浓雾下也有点意境。走着走着旁边雾中出来一只狗跟一个人,说起来这路线上挺多人遛狗的,今天遇到两三波了。
因为阴天,下细雨,又这么大雾,天黑得很快,17 点后天就有点看不清路面了,开始需要灯光。头灯在包里懒得拿出来,就把背包背带上的手电夹到腰间别着的便携坐垫上照亮路面,还别说,效果不错。
到 20:10 左右,终于到达基维尔营地,听到了有人声,也看到了灯光。一番调查确认了跟之前了解到的一样,这里不接受个人露营。地图上往前看也没露营点,我有点慌了,但总之先到四段终点瞧瞧吧。走到终点发现就是基维尔营地下山的大水泥路面,既然有大路,那就能沿着它回到城市,这样想着终于有了一点安全感。
下了个山坡后实在累得不行,把便携坐垫一铺就坐下休息了,尝试用高德地图导航,但信号有点差,一直转不出来。
然后一辆车从山下开上来,看到我瘫坐在路边,停下来问我要去哪,了解清楚情况后又给我指路,还说这里徒步下去要一个多小时,路上没路灯,问我行不行,要不要他送我去车站。
手机一直没信号,我一开始是有点心动的,但不想麻烦别人,刚好手机终于加载出了导航,我对照了下跟他指的路是同一个方向,就婉拒了他,并给他展示我的手电筒表示我不担心走夜路。
高德地图给的教我先徒步到大老山隧道站乘九巴 74X 路至广福邨下车,再步行到地铁站乘车到罗湖口岸。
但它给的徒步路线有坑,我跟着导航越走就越荒凉,公路路面开叉,长满荒草,接着就直接没公路了。我慌了,仔细确认才意识到它教我往草丛里钻,仔细看草丛里还真有条路… 但这条小路显然已经半荒废,草木林立,不仔细看几乎分辨不出路线,让人忍不住怀疑这条路真的能走吗?不会走到一半成断头路吧。
还好这条灌木丛近期有人走过,沿途草木有明显被人趟过,沿途灌木上偶尔还扎了很干净显眼的飘带, 明显才扎上去没多久,这给我增加了一点对它的信心。
钻灌木丛,中间还过了条溪流,接着又是上山,好走的山路没走多远,接着又是在山上钻更深的灌木丛,我越来越慌——这真的是下山的路吗?同时我也有点担心被灌木遮挡的路面会有蛇,但现在已经很晚了,下山心切的我没时间顾虑这些,一路急行。
灌木钻了没多久就开始下山路,而且能看到山下明亮的高速公路跟城市夜景了,这让我放心了一点——至少确实是在下山朝城市里去。但这下山路可不好走,几乎是钻着灌木丛林走直线下降,而且是原生地形,非常的陡,即使有登山杖的辅助,也摔了好多跤,还好草木灌木比较密,有效减缓了摔倒的冲击, 也避免我滚下山。
下山路没走多久,我突然发现腰包里的水杯跟夹着的折叠坐垫都不见了,显然是在前面几次摔跤的时候掉了,不过也就三四十块钱,不管了,继续向下。
下山路仿佛没有尽头,万幸途中发现底下的山坳里有好几片亮光,一开始怀疑是山里废弃的房子,前面趟这条路的旅游队在房子里露营,这也给了我希望——或许能有人给点帮助跟一口吃的,一起露营也不错。
到九点半左右终于下到山脚的时候,发现是个电站之类的建筑,周围还有监控警示,挺失望的。不过到这里又是大路了,也能很明显听到不远处车辆来往的声音,悬着的心总算放了下来。
考虑到手表快没电了,先把登山记录停了,显示今天徒步了接近 21 公里,真是累到够呛。
在路边找了个地坐着休息,想到因为钻灌木林、摔跤、一路剧烈运动疯狂出汗,加之今天又下小雨,身上都是各种小叶子、木棍、汗水、沾了叶子上的雨水,这个样子可不好上车见人。见周围也没人,我直接把衣服都脱掉,换了身干净的。
登山鞋里也湿透了,刚刚钻林子导致石子叶子小木棍雨水也进去了不少,也换了备用的沙滩鞋。换鞋时不知道哪跑出来只蚂蟥在我脚面上爬,赶紧给拔掉丢了(也很庆幸我一直穿运动紧身内衣裤,不然这一趟灌木丛徒步下来,小腿刮伤不说,还可能被蚂蟥等各种虫子叮咬)。
衣服鞋子换好后又休息了挺久,然后沿着大路走了可能十多二十分钟,才终于到了山脚,远远看到不远处就有个公交站,再次导航一下,确认它就是大老山隧道站。
到达车站已经是 22:20 了,乘九巴 74X 路到广福邨下车,再步行到地铁站乘车到罗湖口岸、过关、再乘一号线时已经 23:40,这个点居然还有一趟末班车。最后到家已经过了 0 点,饿得不行搞完夜宵、休息、再洗澡,搞到两三点才睡觉。
总的来说这次徒步距离真的是到目前为止的人生巅峰了,另外这次下山路线也是我走过最险的一次,有点刺激跟后怕,高德地图坑我啊。
TODOs:
现在已经爱上了徒步这种运动方式,花钱折磨自己毫不手软(
如下是我近期发现的一些高质量徒步相关资料,对我有挺大帮助:
2024-02-21 16:26:21
本文最初发表于 如何评价NixOS? - 知乎,觉得比较有价值所以再搬运到我的博客。
我 23 年 4 月开始用 NixOS 之前看过(如何评价NixOS? - 知乎) 这个问答,几个高赞回答都从不同方面给出了很有意义的评价,也是吸引我入坑的原因之一。
现在是 2024 年 2 月,距离我入坑 NixOS 刚好 10 个月,我当初写的新手笔记已经获得了大量好评与不少的赞助,并成为了整个社区最受欢迎的入门教程之一。自 2023 年 6 月我为它专门创建一个 GitHub 仓库与单独的文档站点以来,它已经获得了 1189 个 stars,除我之外还有 37 位读者给它提了 PR:
那么作为一名已经深度使用 NixOS 作为主力桌面系统接近 10 个月的熟手,我在这里也从另一个角度来分享下我的入坑体会。
注意,这篇文章不是 NixOS 入门教程,想看教程请移步上面给的链接。
先澄清下一点,NixOS 的包非常多,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 文件系统快照啊,出问题回滚快照就行。它们最根本的问题是:
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 的这些优势:
这些都是 NixOS 的卖点,其中一些特性现在在传统发行版上也能实现,Fedora Silverblue 等新兴的不可变发行版也在这些方面有些不错的创新。但能解决所有这些问题的系统,目前只有 NixOS(以及更小众的 Guix. 据Guix 的 README 所言,它同样基于 Nix 包管理器)。
自 NixOS 项目创建至今二十多年来,Nix 包管理器与 NixOS 操作系统一直是非常小众的技术,尤其是在国内,知道它们存在的人都是少数 Linux 极客,更别说使用它们了。
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 的搜索热度有几个明显的上升时间点:
再看看 Nix/NixOS 社区从 2022 年启用的年度用户调查。
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
在接触电脑以来很长的一段时间里,我都没怎么在意自己的数据安全。比如说:
现在在 IT 行业工作了几年,从我当下的经验来看,企业后台的管理员如果真有兴趣,查看用户的数据真的是很简单的一件事,至少国内大部分公司的用户数据,都不会做非常严格的数据加密与权限管控。就算真有加密,那也很少是用户级别的,对运维人员或开发人员而言这些数据仍旧与未加密无异。对系统做比较大的迭代时,把小部分用户数据导入到测试环境进行测试也是挺常见的做法…
总之对我而言,这些安全隐患在过去并不算大问题,毕竟我 GitHub, Google 等账号里也没啥重要数据,银行卡里也没几分钱。
但随着我个人数据的积累与在 GitHub, Google 上的活动越来越多、银行卡里 Money 的增加(狗头),这些数据的价值也越来越大。比如说如果我的 GitHub 私钥泄漏,仓库被篡改甚至删除,以前我 GitHub 上没啥数据也没啥 stars 当然无所谓,但现在我已经无法忍受丢失 GitHub 两千多个 stars 的风险了。
在 2022 年的时候我因为对区块链的兴趣顺便学习了一点应用密码学,了解了一些密码学的基础知识, 然后年底又经历了几次可能的数据泄漏,这使我意识到我的个人数据安全已经是一个不可忽视的问题。因此,为了避免 GitHub 私钥泄漏、区块链钱包助记词泄漏、个人隐私泄漏等可能,我在 2023 年 5 月做了全面强化个人数据安全的决定,并在 0XFFFF 社区发了篇帖子征求意见——学习并强化个人的数据安全性(持续更新)。
现在大半年过去,我已经在个人数据安全上做了许多工作,目前算是达到了一个比较不错的状态。
我的个人数据安全方案,有两个核心的指导思想:
这篇文章记录下我所做的相关调研工作、我在这大半年的实践过程中逐渐摸索出的个人数据安全方案以及未来可能的改进方向。
注意这里介绍的并不是什么能一蹴而就获得超高安全性的傻瓜式方案,它需要你需要你有一定的技术背景跟时间投入,是一个长期的学习、实践与方案迭代的过程。另外如果你错误地使用了本文中介绍的工具或方案,可能反而会降低你的数据安全性,由此产生的任何损失与风险皆由你自己承担。
数据安全大概有这些方面:
就我个人而言,我的数据安全主要考虑以下几个部分:
下面就分别就这几个部分展开讨论。
硬件密钥的好处是可以防止密钥泄漏,但 YubiKey 在国内无官方购买渠道,而且价格不菲,只买一个 YubiKey 的话还存在丢失的风险。
另一方面其实基于现代密码学算法的软件密钥安全性对我而言是足够的,而且软件密钥的使用更加方便。或许在未来,我会考虑使用canokey-core、OpenSK、solokey 等开源方案 DIY 几个硬件密钥,但目前我并不觉得有这必要。
我们一般都是直接使用 ssh-keygen
命令生成 SSH 密钥对,OpenSSH 目前主要支持两种密钥算法:
RSA 跟 ED25519 都是被广泛使用的密码学算法,其安全性都是经过严格验证的,因此我们可以放心使用。但为了在密钥泄漏的情况下,能够尽可能减少损失,强烈建议给个人使用的密钥添加 passphrase 保护。
那这个 passphrase 保护到底有多安全呢?
有一些密码学知识的人应该知道,pssphrase 保护的实现原理通常是:通过 KDF 算法(或者叫慢哈希算法、密码哈希算法)将用户输入的 passphrase 字符串转换成一个二进制对称密钥,然后再用这个密钥加解密具体的数据。
因此,使用 pssphrase 加密保护的 SSH Key 的安全性,取决于:
那么,OpenSSH 的 passphrase 是如何实现的?是否足够安全?
我首先 Google 了下,找到一些相关的文章(注意如下文章内容与其时间点相关,OpenSSH 的新版本会有些变化):
在 OpenSSH release notes 中搜索 passphrase 跟 kdf 两个关键字,找到些关键信息如下:
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 中我进一步找到这个:
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 生成方式应该是:
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 密钥,这是我过去的做法,但这样做有几个问题:
因此,我现在的做法是:
我通过这种方式缩小了风险范围,即使某台机器的密钥泄漏,也只需要重新生成并替换这台机器上的密钥即可。
搜到些资料:
TODO 待研究。
我曾经大量使用了 Chrome/Firefox 自带的密码存储功能,但用到现在其实也发现了它们的许多弊端。有同事推崇 1Password 的使用体验,它的自动填充跟同站点的多密码管理确实做得非常优秀,但一是要收费,二是它是商业的在线方案,基于零信任原则,我不太想使用这种方案。
作为开源爱好者,我最近找到了一个非常适合我自己的方案:password-store.
这套方案使用 gpg 加密账号密码,每个文件就是一个账号密码,通过文件树来组织与匹配账号密码与 APP/站点的对应关系,并且生态完善,对 firefox/chrome/android/ios 的支持都挺好。
缺点是用 GPG 加密,上手有点难度,但对咱来说完全可以接受。
我在最近使用 pass-import 从 firefox/chrome 中导入了我当前所有的账号密码,并对所有的重要账号密码进行了一次全面的更新,一共改了二三十个账号,全部采用了随机密码。
当前的存储同步与多端使用方式:
我的详细 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 内容如下:
--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 密钥,其安全性是不够的。
为了获得最佳安全性,我们需要:
使用如下参数生成 GPG 密钥:
gpg --s2k-mode 3 --s2k-count 65011712 --s2k-digest-algo SHA512 --s2k-cipher-algo AES256 ...
加密、签名、认证都使用不同的密钥,每个密钥只用于特定的场景,这样即使某个密钥泄漏,也不会影响其他场景的安全性。
为了在全局使用这些参数,可以将它们添加到你的 ~/.gnupg/gpg.conf
配置文件中。
详见我的 gpg 配置ryan4yin/nix-config/gpg
我日常同时在使用 macOS 与 NixOS,因此不论是需要离线存储的灾难恢复数据,还是需要在多端访问的个人数据,都需要一个跨平台的加密备份与同步工具。
前面提到的 pass 使用 GnuPG 进行文件级别的加密,但在很多场景下这不太适用,而且 GPG 本身也太重了,还一堆历史遗留问题,我不太喜欢。
为了其他数据备份与同步的需要,我需要一个跨平台的加密工具,目前调研到有如下这些:
进一步调研后,我选择了 age, rclone 与 restic 作为我的跨平台加密备份与同步工具。这三个工具都比较活跃,stars 很高,使用的也都是比较现代的密码学算法:
对于 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/) 存储了,不需要加密。
对于不便公开的个人笔记,有这些考虑:
我一开始考虑直接使用基于 Git 仓库的方案,能获得 Git 的所有功能,同时还避免额外自建一个笔记服务。找到个 GitJournal ,数据存在 GitHub 私有仓库用了一个月,功能不太多但够用。但发现它项目不咋活跃,基于 SSH 协议的 Git 同步在大仓库上也有些毛病,而且数据明文存在 Git 仓库里,安全性相对差一些。
另外找到个 git-crypt 能在 Git 上做一层透明加密,但没找到支持它的移动端 APP,而且项目也不咋活跃。
在 https://github.com/topics/note-taking 下看了些流行项目,主要有这些:
在移动端使用 Synthing 或 Git 等第三方工具同步笔记数据,都很麻烦,而且安全性也不够。因此目前看在移动端也能用得舒服的话,最稳妥的选择是第一类笔记 APP,简单试用后我选择了最流行的 Joplin.
我的桌面电脑都是 macOS 与 NixOS,Homlab 虚拟机也已经 all in NixOS,另外我目前没有任何云上服务器。
另外虽然也有两台 Windows 虚拟机,但极少对它们做啥改动,只要做好虚拟机快照的备份就 OK 了。
对于 NixOS 桌面系统与 Homelab 虚拟机,我当前的方案如下:
--iter-time
,计算出 unlock
key 的用时,默认 2s,安全起见咱设置成了 5s)
cryptsetup --type luks2 --cipher aes-xts-plain64 --hash sha512 --iter-time 5000 --key-size 256 --pbkdf argon2id --use-urandom --verify-passphrase luksFormat device
ssh-add
这个新的私钥,使其能够访问到我的私有 secrets 仓库。/etc/ssh/ssh_host_ed25519_key.pub
发送到一台旧的可解密
secrets 仓库数据的主机上。如果该文件不存在则先用 sudo ssh-keygen -A
生成。nixos-rebuild switch
或 darwin-rebuild switch
成功部署我的 nix-config 了,agenix 会自动使用新主机的系统私钥/etc/ssh/ssh_host_ed25519_key
解密 secrets 仓库中的数据并完成部署工作。对于 macOS,它本身的磁盘安全我感觉就已经做得很 OK 了,而且它能改的东西也比较有限。我的安全设置如下:
在使用 nix-darwin 跟 NixOS 的情况下,整个 macOS/NixOS 的系统环境都是通过我的ryan4yin/nix-config 声明式配置的,因此桌面电脑的灾难恢复根本不是一个问题。
只需要简单的几行命令就能在一个全新的系统上恢复出我的 macOS / NixOS 桌面环境,所有密钥也会由 agenix 自动解密并放置到正确的位置。
要说有恢复难题的,也就是一些个人数据了,这部分已经在前面第七小节介绍过了,用 rclone/restic 就行。
/etc/ssh/ssh_host_ed25519_key.pub
公钥加密,在部署时自动使用对应的私钥解密。这套方案的大部分部署工作都是由我的 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 的长度都是不受限的,有两个思路(注意不要在密码中包含任何个人信息):
don't-do-evil_I-promise-this-would-become-not-a-dark-corner
这样的。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 都是很好的例子),因此定期更新这些密码跟密钥是很有必要的。
几个核心密码更新起来会简单些,可以考虑每年更新一遍,而密钥可以考虑每两三年更新一遍(时间凭感觉说的哈,没有做论证)。其他密码密钥则可以根据数据的重要性来决定更新频率。
前面已经基本都提到了,这里再总结下:
ssh-add
使用,只需要在系统启动后输入一次密码即可,也不麻烦。这里考虑我的 GPG 子密钥泄漏了、pass 密码仓库泄漏了等各种情况下的灾难恢复流程。
TODO 后续再慢慢补充。
目前我的主要个人数据基本都已经通过上述方案进行了安全管理。但还有这些方面可以进一步改进:
安全总是相对的,而且其中涉及的知识点不少,我 2022 年学了密码学算是为此打下了个不错的基础, 但目前看前头还有挺多知识点在等待着我。我目前仍然打算以比较 casual 的心态去持续推进这件事情,什么时候兴趣来了就推进一点点。
这套方案也可能存在一些问题,欢迎大家审阅指正。
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 直接挂载镜像中的各分区进行初步分析:
# 解压镜像
› 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
的内容:
› 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
可以看到:
/boot/extlinux/extlinux.conf
作为 U-Boot 的启动项配置,据U-Boot 官方的 Distro 文档
所言,这是 U-Boot 的标准配置文件。xxx.bin
文件,这些是一些硬件固件,其中的 light_c906_audio.bin
显然是玄铁 906 这个 IP 核的音频固件,其他的后面再研究。initrd
, dtbs
以及 Image
文件都是在 /boot/nixos
下,这三个文件也都是跟
Linux 的启动相关的,现在不用管它们,下一步会分析。再看下 /boot/extlinux/extlinux.conf
的内容:
› 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
从上述配置中能获得这些信息:
nixos-default
的启动项并将它设为了默认启动项,extlinux 在启动阶段会根据该配置启动 NixOS 系统LINUX
INITRD
FDT
三个参数分别指定了 kernel(Image 文件)、initrd 以及设备树(dtb)的位置,这三个文件我们在前面已经看到了,都在 /boot/nixos
下。
Image
)、dtb 设备树文件以及initrd
文件系统,然后以设备树跟 initrd 的地址为参数启动 Kernel.init
参数指定的可执行程序,这里是/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b/init
,
这个 init 程序会挂载真正的根文件系统,并在其上执行后续的启动流程。initrd
这样一个临时的内存盘,通常用于在系统启动阶段加载一些内核中未内置但启动却必需的驱动或数据文件供 init
程序使用,以便后续能够挂载真正的根文件系统。
APPEND
参数包含有许多关键信息:
/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b/init
,
这实际是一个 shell 脚本,我们下一步会重点分析它。
/sbin/init
,它会被链接到/lib/systemd/systemd
,也就是直接使用 systemd 作为 1 号进程。你可以在
Fedora/Ubuntu 等传统发行版中运行 ls -al /sbin/init
确认这一点,以及检查它们的/boot/grub/grub.cfs
启动项配置,看看它们有无自定义内核的 init
参数。/dev/disk/by-uuid/14e19a7b-0ae0-484d-9d54-43bd6fdc20c7
,使用的文件系统为 ext4.earlycon
(early console) 表示在系统启动早期就启用控制台输出,这样可以在系统启动阶段通过 UAER/HDMI 等接口看到相关的启动日志,方便调试。这样一分析就能得出结论:在执行 init
程序之前的启动流程都未涉及到真正的根文件系统,NixOS
与其他发行版在该流程中并无明显差异。
为了方便后续内容的理解,先看下 NixOS 系统在 LicheePi 4A 上的实际启动日志是个很不错的选择。
按我项目中的 README 正常烧录好系统后,使用 USB 转串口工具连接到 LicheePi 4A 的 UART0 串口,然后启动系统,就能看到 NixOS 的启动日志。
接线示例:
接好线后使用 minicom 查看日志:
› ls /dev/ttyUSB0
╭───┬──────────────┬─────────────┬──────┬───────────────╮
│ # │ name │ type │ size │ modified │
├───┼──────────────┼─────────────┼──────┼───────────────┤
│ 0 │ /dev/ttyUSB0 │ char device │ 0 B │ 6 minutes ago │
╰───┴──────────────┴─────────────┴──────┴───────────────╯
› minicom -d /dev/ttyusb0 -b 115200
一个正常的启动日志示例如下:
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.......
......
简单总结下日志中的信息:
initrd
阶段干的活,内核加载了 systemd udev 内核模块,然后使用 busybox 的 fsck 检查了根文件系统,接着挂载了根文件系统。activation script
,它首先设置好了 /etc
文件夹,然后检查了根分区文件系统的情况,并自动执行了分区与文件系统的扩容操作。nix-env -p /nix/vm...
大概是切换了个运行环境。有了上面这些信息,我们就可以比较容易地理解 init 这个程序了,它主要对应前面日志中的 NixOS Stage 2,即在真正挂载根文件系统之后,执行的第一个用户态程序。
在 NixOS 中这个 init 程序实际上是一个 shell 脚本,可以直接通过 cat
或者 vim
来查看它的内容:
› 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
简单总结下这个脚本的功能:
mount -o remount,ro,bind /nix/store
将 /nix/store
目录重新挂载为只读,确保 Nix
Store 的不可变性,从而使系统状态可复现。$systemConfig/activate
这个程序。前面的 init 程序其实没干啥,根据我们看过的启动日志,大部分的功能应该都是在$systemConfig/activate
这个程序中完成的。
再看看其中的 $systemConfig/activate 的内容,它同样是一个 shell 脚本,直接 cat
/vim
查看下:
› 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
这个脚本有点长,简单总结下它干了啥:
source /nix/store/vn0sga6rn69vkdbs0d2njh0aig7zmzi6-mounts.sh
挂载一些目录,看下这个文件内容就知道,挂的是 /proc
/sys
/dev
/rum
等几个临时目录。mkdir
/install
等指令自动创建 /home
/root
/bin
/usr
/usr/bin
等目录perl /nix/store/rg5rf512szdxmnj9qal3wfdnpfsx38qi-setup-etc.pl /nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc
配置生成 /etc
目录中的各种文件。ln
命令添加其他各种软链接,以及一些别的设置。其中第三步 etc 目录的设置,实际数据基本都来自该脚本的第二个参数:
› 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 和 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,官方给出的命令如下:
# 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 指令集相关的知识点:
根据我们前面的 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 这样的东西。
fw_dynamic.bin
: 我们 NixOS 镜像的 /boot
中就有这个固件,它是 OpenSBI 的编译产物。
u-boot-spl.bin
: 这个文件是 u-boot 的编译产物,它是二级加载器。
因为历史原因,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 成功启动后的登录的截图:
那么基于我们到目前为止学到的知识,要如何构建出一个可以在 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 感兴趣的读者们,快进我碗里来(