普通视图

发现新文章,点击刷新页面。
昨天以前This cute world

我的 2025 - 步履不停

2025年12月27日 01:01

今年不论是生活上还是工作上都变化颇多。

2025 年 Highlights

工作

如同我 11 月份写的 大梦一场,我在今年 1 月份辞职后 gap 了几个月,在 5 月入职了新公司。新工作薪水大涨,而且是远程,很自由,我大部分时间都在老家上班,业余生活相比往年也丰富了很多。

新工作是机缘巧合之下,在 X 上发求职推文找到的:

更新下进展,真的通过 X 找到了挺满意的工作,在走入职流程了,感谢各位推友们的转发评论!🥰

新工作是 remote,已经退租深圳租房、暂时搬回老家了,后面也可能去杭州住几个月顺便跟大佬们交流。
顺利的话我下半年能自由不少,也更有机会去实现我 2025 年「精进技术、徒步世界」的目标🥳 https://t.co/VdooZ1UUhr

— ryan4yin | 二花 (@ryan4yin) May 9, 2025

在老家刚工作两周的时候也发过条 X 推文(其中提到的新购办公用品,公司也给报销了 USD$300):

Remote 工作第三周,置办了升降桌工学椅、显示器、千兆宽带等一堆物件,几周下来办公环境基本稳定了。
刚好到插秧季,家门口田一天比一天绿,赏心悦目。
业余没事就去山里转转,或者在村广场玩玩轮滑,或者在家折腾点技术,很自在。

另外也拿到了 KubeCon China 2025 的最终用户门票,下个月香港见( pic.twitter.com/e77MD2o0Av

— ryan4yin | 二花 (@ryan4yin) May 28, 2025

在新公司的工作内容跟我前司区别不大,仍然是云计算成本优化、网络链路优化、CICD、监控告警、日常故障排查之类的内容。

全年做出的工作成果还不错,给公司省了不少钱,也做了几个新项目,拿到了很不错的绩效,但是更多是吃以前的技术老本,并没有做出什么值得称道的技术探索。

坐飞机

2025 年是我的飞行元年,1 月 30 号(大年初二)是我第一次坐飞机,从深圳飞回老家邵阳。

第一次坐飞机 - 坐了 20 分钟摆渡车才找到飞机...

在云彩上了

如前所述,我一月份辞职了嘛,然后一直 gap 到 4 月中旬才开始找工作,这期间全国到处旅游跟找同学朋友玩又坐了四五次飞机跟几次高铁。6 月份的时候又去香港参加 KubeCon China, 坐了高铁去深圳,回来是飞机。然后 11 月份公司在云南西双版纳团建飞了个来回,再加上 12 月我跟 @Aspirin 去日本玩了一圈,长沙-上海-日本一个来回又飞了 4 趟。

航旅纵横 APP 显示我累计飞行 12 次,总共 22h19min, 累计里程 15225km. 算是一个从 0 到 1 的突破。

航旅纵横 - 飞行里程统计

旅行

今年 2 月到 4 月中旬这段时间在国内逛了很多地方,再加上后面找同学朋友玩、参加技术会议啥的, 今年一整年基本就是在长沙、邵阳、深圳、上海、苏州,这几个地方来回跑,也到香港、张家界、凤凰古城、重庆、安徽安庆以及南京玩过一圈,除了凤凰古城有点太商业化了之外,其他几个体验都很好。

苏州的周庄古镇很有江南韵味,南京的烤鸭是一绝(比北京烤鸭好吃),张家界的鬼斧神工云蒸霞蔚蔚为壮观,重庆的豆花饭跟火锅超级好吃,安徽同学的婚宴菜肴也很有特点。大城市方面,上海跟香港一样摩天大楼跟近代历史建筑交相辉映,跟深圳这样的年轻城市区别明显。

12 月则是跟 @Aspirin 到日本玩了一周时间,时间安排上是东京 3 天、京都 2 天,再大阪 3 天这样子,吃了很多日本特色料理,有豚骨拉面、蘸面、鳗鱼饭、寿司、牛肉寿喜锅、他人丼、生牛肉丼、大阪烧,还有知名的日本穷鬼三件套——吉野家、松屋、食其家。总体感觉就是很有新鲜感,不难吃但是也谈不上喜欢吃。以及日本的很多菜都偏咸,辣味跟国内也有区别,这个是真有点吃不惯,以致于我回到长沙后连吃了好几天红油馄饨才缓过来,感觉在日本呆一周像是呆了一个月…

物价上感觉日本跟香港基本持平,是内地的 2 到 3 倍,日本甚至还有个别商品价格几乎跟大陆一致, 这方面比香港要强。住房价格方面东京 > 京都 > 大阪,高铁(新干线)地铁(JR、电铁跟各种私铁) 跟公交也基本都是内地 3 倍的样子(比香港九巴低不少),铁路系统非常有特色但也很复杂,像是大陆地铁跟高铁的混合体,分啥急行、准急、特急,还有指定席,有的要换站乘车,又的又不用,甚至还有中途解离的车——前 4 节车厢跟后 4 节车厢分别前往不同的终点。作为世界知名的大都市圈,熟悉了这套系统后,出行确实非常方便,感觉长三角珠三角可以借鉴下其中部分元素。

从日本淘了几本 CD 回来,都很有纪念意义:

  • Supercell: 初音未来早期非常经典的专辑,所有歌曲都很好听。
  • Greatest Hits 2011-2017「Alter Ego」:罪恶王冠是我的日漫入坑作之一,当时看完后一直单曲循环「エウテルペ」
  • シングルコレクション 2002-2008:Humbert Humbert 是一个日本民谣组合,我从 16 年一直听到现在。
  • Frieren: Beyond Journey’s End (Original Soundtrack):最近两年看过的印象最深刻的一部动漫,其中的歌曲我也经常单曲循环。
  • 还给老妹带了 Bang Dream 的几本 CD,其中有一本是「迷跡波

其实还很想买 Rolling Girls 的专辑,不过当时没想起来,下次再补吧。

回国后就买了台 CD 机跟新耳机,可能是功放的区别,听起来确实比我七八百块的无线蓝牙耳机效果更好些,听着更舒服,但是听起来跟我用 Macbook Pro 插上耳机听网易云的歌曲没啥区别,可能更多的还是一个仪式感吧 hhh

CD 机与碟

顺带一提,我们离开日本前的最后一顿是天下茶屋站旁边的食其家吃的,然后在从长沙黄花机场回租房的路上又看到路边有一家食其家,瞬间产生了点我好像还在日本的感觉。查了下全长沙一共才三家店,就巧到被我碰到其中一家。

今年应该算是我真正走出去的一年,在这之前我几乎只在湖南邵阳(老家)、安徽合肥(上大学)以及深圳(打工仔)呆过,人比较宅,基本没去远地方旅游过,最远的可能也就是近两年到武功山、香港徒步了几次。

运动

如果说 2024 年我最大的运动成就是徒步,那 2025 年无疑是游泳。

今年差不多是新工作略微稳定后才开始尝试各种运动,先是 6 月份玩了个把月的陆地冲浪板跟跑步,7 月份买了辆山地车之后骑行了一个月,但是 8 月份学游泳找到点乐趣后,基本就放弃其他运动,只练游泳了。

最开始是 7 月份的时候天气变热,开始在老家小水潭游泳,但还跟之前一样只会个狗刨式。8 月份的时候去上海参加 AOSCC 2025 跟 NixOS Meetup, 住的民宿刚好带一个 10 米长度的小泳池,在这个民宿跟着 B 站各种视频教程学了 6 天游泳,把蛙泳给入了门,开始觉得游泳很有意思。之后就在日常老家小水潭游,出门玩的时候也在长沙、深圳游过几次 50 米的标准泳池,游泳技术越发精进。11 月公司在云南团建,我跟同事在酒店的小泳池(长度感觉是 20 米)游了两天。之后回到长沙,发现租房周边就有个游泳馆,25 米的池子,游了几次觉得不错就办了张半年卡,花费 1111 大洋,现在只要呆在长沙,我基本是游三休一的节奏。

最近学习波蛙已经掌握了些诀窍,同时也尝试了一点自由泳。总体感觉游泳很有意思,而且通过游泳, 今年我的形体也出现了很好的变化,倒三角身材初现端倪了,只是肚子上的赘肉仍需努力。

附上 2025 最后一游,也刚好是今年的最佳成绩:

2025 年最后一次游泳记录

婚礼

今年是周边同龄人集体结婚的一年,不过我貌似仍然没啥找对象的想法,感觉 30 岁后再考虑这个问题也未尝不可,再爽玩两三年先(

今年已经参加过的婚礼:

  • 7/4:一个三十多岁的堂哥突然开窍,相亲没多久就火速成婚
  • 8/1:大学室友 Zhan 在安徽大别山的山坳坳里结婚,寝室 6 个人到齐了 5 个

预计春节前还要参加的婚礼:

  • 2026/1/7: 去安徽砀山参加另一个大学室友 Hu 的婚礼,已经买好了高铁票就等出发了
  • 未知:堂弟今年下半年换工作后跟一位同事擦出爱情火花,也是火速订婚,预定年前结婚
  • 2026/2/14:还是大学室友,Zhao,预计春节前结婚

其中 8 月 1 号这场婚礼,是我今年印象最深刻的一次极限操作:

7 月底的时候我在上海参加完 AOSCC,本想先在上海干几天活,然后顺路请个假去安徽吃喜酒,应该是件很轻松惬意的事情。结果 7/30 派到我手上的一个小故障越查越炸裂,升级成 P0,和同事熬夜搞到第二天凌晨 2 点多才临时止血。7/31 下午我先是乘高铁到苏州,跟其他 3 位大学室友汇合,草草吃完晚饭就开车上高速 4 小时飙到安庆。0 点入住酒店,睡前我翻了下后台记录又发现个更灾难性的问题,又跟领导同事一起鏖战到凌晨 3 点多才初步解决。第二天一早又开 3 个多小时盘山公路才到新郎 Z 的家里,鞭炮作响锣鼓震天,合影、随礼、举杯一条龙。下午 2 点多我们吃完午宴就立即返程苏州了。还好我不会开车 hhh 他们 3 个人又轮班开 7 个多小时,人歇车不歇。回到苏州我又立即高铁返回了上海,到 8 月 1 号夜幕降临的时候,我已经在上海商场,跟 ddl 还有 nobody 在排队等着干饭了。

婚礼 24 小时,往返 1000 km,线上救火+线下道喜+无缝社交,血条见底,那叫一个惊险刺激。

或许该叠个甲,今年像这样猛猛加班仅此一次,整体工作节奏我仍是相当满意的。

技术

技术方面今年有点乏善可陈,今年读完了 Linux/Unix 系统编程手册 上下两册,借助 AI 写了一个Linux 桌面系统 系列,但是没怎么实践这方面的知识,现在又忘得差不多了 2333

在现在 AI 发展这么快的当下,技术博客的受众是越来越少了,常见的技术问题跟细节基本都能直接跟 AI 沟通。我今年写「Linux 桌面系统」系列也主要是让 AI 生成,我主要负责验证相关内容的正确性,顺便学习相关技术。这个过程中学到了很多,也修正了不少 AI 自己臆想出来的虚假内容。实际写出来有点类似一个 Wiki, 它比 Arch Wiki 更精炼更成体系,相比直接问 AI 它的准确性要更高(毕竟经过了人工校对与实际测试)。

得益于新工作的灵活性,我今年参加了 4 次技术会议:

  • 6 月份的时候去香港参加了 KubeCon China,内容基本都是 AI, 也写了一篇KubeCon China 2025 见闻

    6/10 欢迎光临 KubeCon China 2025

  • 7 月底的时候到上海参加了 AOSCC 2025, 技术氛围真好,还感受了一波大学生们的青春气息

    7/27 上海 AOSCC 纪念墙

  • 8 月份又在上海参加了 Nix Meetup, 一起干了好几顿饭。我还在台上讲了下自己的入坑经历,被大家叫 Ryan 老师有点受宠若惊(
    • 从打算搞到最终举办就十来天的时间,结果群友们还搞到了 NixOS 基金会的赞助,整了很多精美小礼物,很有爱了

      8/9 又回上海参加 Nix Meetup 了

  • 9 月份又到上海参加 PyCon China 2025, 见到了很多 X 上认识很久的 saka yihong 等大佬,领了许多小礼物,还跟上海 nonebot 群友们一起干了顿饭,顺便给他们安利了 NixOS(

    9/20 PyCon China 2025 跟 nonebot 的朋友们合影

以及 GitHub 上 followers 终于突破了 1000, 获得的 stars 也超过了 6000, 不过这仍然基本都来自我 2023 年创建的几个 Nix 项目。

2025/01/01 GitHub 统计数据

2025/12/27 GitHub 统计数据

AI

今年开始把 AI 应用到各个方面,公司给整了 Cursor Pro 年费会员,我自己也给阿里千问、智谱 GLM 跟月之暗面 Kimi 充了不少钱。总体感觉,借助 claude/cursor 这类工具来写代码跟文档,可用性已经很高了,指令得当的情况下需要人工介入的次数不多,简化了不少繁琐的工作。但整体的定位仍然是辅助工具,我用 cursor 时也仍然不喜欢它的 Agent UI, 传统的 VSCode UI 布局更让我有掌控感。

今年年底全球开始传出各种通过 AI 提效然后裁员的新闻,包括我现在的公司也裁了一大批客服人员, 并且提倡让开发人员自己完成简单的 UI 设计类工作,AI 替代人确实是正在进行时,但短期内我的岗位还算安全。

总的来说,AI 方面我今年的感想是:能做的越来越多了,但是绝对不能全盘信任它!人工审查仍然是必不可少的,尤其是对自身权限极高的 infra/SRE 同学而言

再次强调,infra/SRE 绝对不应该在 prd 环境中使用 Cursor Auto-Run Mode 这种自动驾驶模式!希望体验这些最新技术的话,最好是放在没有 PRD 环境权限的环境中跑,譬如单独的 VM.

年底组里就有 infra team 的同事在使用 Cursor 的 Auto-Run Mode 为某数据中间件添加备份功能时,备份功能还没搞好,就把重要数据删掉了。直接原因是 Cursor AI 自动执行了kubectl replace -f xxx.yaml --force 导致一个 StatefulSet 被 Delete 再重新 Create,进而导致该数据中间件挂掉。次要原因是用了默认的 StorageClass, ReclaimPolicy 是 Delete,这导致前述流程中 PV 也被级联删除,数据完全丢失。还好其他地方还有一个最近一段时间的备份,勉强救回了大部分数据,不然都不知道这个事该怎么收场。

同事甚至一开始还没意识到是 Cursor 的锅,怀疑是我们的 ArgoCD 强制更新触发了类似 Delete 的操作,最后是我翻遍了 ArgoCD 跟 K8s Audit Log,定位到 Delete 请求来自同事自己的账号,他才进一步找到对应的 Cursor 操作记录与 Delete 日志。

理财

今年可能也算我的理财元年,去年底辞职前去香港开了几张港卡,又开了几家港股美股交易所,国内的 A 股账户也开了两个。在股市上投入了少许资金,巅峰的时候浮盈 20%,但是一年接下来收益是负的 hhh 不过算上我在银行支付宝的其他基金理财产品,总体收益还是正的,赚了一两万块钱吧。

加密货币在几年前玩过一点,今年基本没咋动,就留了点 ETH/SOL/UNI 随波逐流。

目前并没想着靠这些赚钱,只是觉得需要去拿点小钱去玩一玩,熟悉下现代金融是怎么回事。一年下来实际投入比最初预期高不少,港股 A 股跌宕起伏好几次,心慌意乱做了不少错误的决策,算是对这套玩法有了个大概的了解。

2026 年展望

总的来看,今年是我一直在旅途上的一年,可能也是我过去十年中最健康的一年,我 2025 年的运动量远超以往。

我在去年年终总结的文末写了,对自己 2025 年的期许是「深入浅出 Linux,徒步中国、徒步世界」,一年过去,勉强算是达成了一半。我读完了《Linux/Unix 系统编程手册》,写了好几篇 Linux 桌面系统相关的文章,徒步走过了中国的多个城市,在日本 city walk 了一周多,还学了小半年的游泳,相比 2024 年,生活又丰富了许多。

接下来的 2026 年,我对自己的期望是「精进英语、泳技与 Linux,探索世界」,用 OKR 类比的话,这个 Object 对应了如下 Key Results:

  • 英语:口语词汇量进阶,综合水平达到 B2 级别,满足我旅游及全英文工作的需求
  • 日语:学完标日初级,达到 N4 级别。要求不高,能看懂简单动漫生肉+日常沟通就行
  • 游泳(25m 短池):蛙泳百米 1m50s、千米 22mins,潜泳 25 米(单趟)
  • 旅游:拿到驾照,自驾游中国,以及拓展亚洲跟世界地图的足迹
  • Linux 学习
    • 积极参与 NixOS 上游贡献
    • 尝试写几个 Linux 小工具,或者参与相关项目的开发,避免手艺生疏

图集

最后,附上我今年的精选照片吧,大致按时间顺序排列:

1/19 香港 利源东西街

Gap 期间:

2/15 跟朋友们在上海K歌,瓜哥在唱 MyGo

2/17 上海百联ZX - miku 好可爱

3/3 周庄古镇 - 下雨了

3/3 周庄古镇 - 小巷灯影

3/5 南京穹窿山 上真观 三清阁

3/9 南京栖霞山

3/14 南京中山陵

3/26 凤凰古城

3/27 张家界 - 武陵源

3/27 张家界 - 袁家界天下第一桥

3/28 张家界 胜似仙境

3/28 张家界 花都结冰了

3/29 张家界 小猴子是真不怕冷

3/30 涪陵火锅

3/31 涪陵豆花饭

4/3 重庆

4/4 对面就是洪崖洞,到了晚上简直人山人海

4/8 重庆独特的轻轨

5/3 东莞松山湖小火车

5/5 深圳 东西都打好包了,就等着运回老家了

在老家的工作状态记录:

5/9 邵阳 回老家第一天,家门口的景色

5/23 山里的蒲公英

林间小路

5/24 山里的覆盆子/树莓

5/27 插秧季

5/28 新置办了升降桌跟双显示器用于工作

然后就上海、老家两头跑:

6/10 香港 欢迎光临 KubeCon China 2025

香港 大 SUSE 上一只小 SUSE

香港 Switch 店在宣传 Miku Boxing

香港 累计有三个朋友 KubeCon 期间在这里买了 Switch 2,它这波血赚

6/17 邵阳 家门口的奇特天气 - 局部降雨

6/20 后山超美的瞬间

7/23 在后山骑行

7/27 上海 AOSCC 纪念墙

7/27 上海 AOSCC 纪念墙 - 最终效果

8/1 室友在安徽大别山山坳坳里结婚,寝室 6 个人到了 5 个

8/9 又回上海参加 Nix Meetup 了

8/13 我的各种参会证以及 keep 跑步纪念奖牌

9/20 PyCon China 2025 跟 nonebot 的朋友们合影

9/20 ddl 送给我的 nonebot 纪念品,以及从方块那批发的 NixOS 挂坠

在西双版纳团建两天:

11/21 西双版纳 千年绞杀滕

11/21 西双版纳 星光夜市 - 好看 但是东西贼贵

年底的日本游:

12/7 秋叶原的魔禁广告牌

12/7 秋叶原

12/7 台場的 Telecom Center 展望台

12/8 京都 我在日本吃过最贵的一顿饭 鳗鱼饭

12/9 周恩来总理写的雨中岚山

12/9 周恩来总理写的雨中岚山

12/9 12 月份的岚山挺好看的

12/9 岚山

12/9 岚山

12/10 生田神社 穿传统服装来祈福的日本家庭

12/10 神户 生牛肉丼

12/10 神户 JR 舞子站

12/10 神户 舞子海上散步道

12/12 大阪烧

12/13 日本 老太太童心未泯

12/13 日本 准备返回上海了 - 在等去关西机场的电车

大梦一场

2025年11月4日 22:38

如今

回到乡下老家,我把二楼大客厅布置成了办公室,置办了张大号升降桌,配上两台大显示器,宽带也升级到了千兆。

今年大部分时间,我就在这里上班。

上班累了,一抬头,落地窗外就是一片稻田。离家 50 米是村活动广场,我偶尔去玩玩滑板;天热了, 每天中午就去山里水潭练习游泳;下午下班后,常沿着进山路跑上一两个小时,或者骑行;晚上,就刷点动漫,或者研究点技术。每隔一两个月,我会去一线城市参加些感兴趣的技术会议,感受下氛围,或者干脆找个地方旅游上班,顺道联络联络老朋友老同学。

现在这样的状态,就是我目前理想中的生活。


过去

很难想象,就在六年前,我曾心灰意冷,觉得前途无比黑暗。

那时的我学业彻底失败,孤注一掷地奔到深圳,想找一个进入 IT 行业的机会。幸运的是,我入职了一家小作坊当「全干工程师」(啥都得干),在城中村 10 平米的单间里,用两年青春换来了技术经验和一点自信心。接着跳槽,职业生涯才算步入正轨。又过了四年,因为一些事情选择了辞职,Gap 3 个月后,机缘巧合下才入职了现在的公司,开始了如今的生活。

这其中种种,我之前在《我的四分之一人生》 中已讲得很详细,只是自那之后到现在,又是两年过去了。

上周一口气把《凡人修仙传》动画刷完,看到主角韩立结丹时,里面一句评语让我感慨万千:

伪灵根、散修,能走到今天这般境地,还真是不容易啊。

是啊,不容易。年岁渐深,码龄渐涨,薪水也水涨船高,我终于走到了一个能喘口气的阶段。虽然我现在的薪资可能只是很多人的起点,但知足常乐,开心比啥都重要。只要不背上买房、结婚这些重担,即使不刻意去省钱,到 35 岁我也应能攒下一笔可观的财富。到时候,就算 IT 这碗青春饭真没得吃了, 只要手里有钱,不管接下来干啥,底气总会足很多。


因缘际会

当然,能从那段黑暗里走出来,光靠努力是不够的。正如一位长者的名言:「一个人的命运啊,当然要靠自我奋斗,但也要考虑到历史的行程。」

我很难说清自己是否有 IT 天赋。大学自学编程时,经常憋好久都写不出几行代码,无数次想过放弃瞎折腾,老老实实把声学学好算了。即便是现在,代码能力也算不上多强。

要说有什么比天赋更重要,可能还是兴趣吧。因为是在做着自己真正喜欢的东西,所以不觉得苦不觉得累。在困难面前,我往往诉诸行动,而不是怨天尤人。还有就是,我尽量让自己每个错都只犯一次。可能就是这些不起眼的习惯让我慢慢攒下了现在这份不错的 GitHub Profile、持续更新的技术博客,以及在 X 上靠分享获得的一点知名度。

当这些个人积累,恰好又遇上了 IT 行业的时代浪潮,再加上一点点运气作为催化剂,便产生了奇妙的化学反应。正是这些因素凑在一起,才得以让一个本科结业的学渣,也能次次找到满意的工作。


回归

年轻时用健康和时间换钱,压榨精力,忽略家人,也压抑着心底的小念想。这两年,我开始各种「找补」:带父母妹妹看牙洗牙,用徒步、游泳、骑行找回健康,把厨房电器填满,到处旅游结交朋友。

2025 年已经临近尾声,我这份新工作还有乐乐的学业都逐渐稳定了下来,Q3 在工作上做得还不错,领导给出了「Exceeds Expectations」的评价,算是个很不错的新开始。

总之,我又回到了我出生的地方。安徽建筑大学那朦胧的易海,图书馆里陪伴我四年的 IT 书架区,那张写着我挂掉十多门课的成绩单;深圳摩天大楼里的工位,早晚高峰的地铁公交,以及城中村那 20 平的单间…… 这一切,都渐渐成了回忆,有时甚至觉得那只是大梦一场。

梦醒,我渐渐睁开双眼,拉开窗帘,打开落地窗,迎接新一天穿过稻田的阳光。看来,又会是一个风和日丽的日子呢。

未来又待如何呢?「且行且寻」。

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

2025年10月19日 10:22

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

前言

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

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

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


系统关机流程

1.1 关机流程概览

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

关机阶段

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

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

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

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

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

1.2 用户会话清理

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

会话清理流程

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

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

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

关键操作

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

监控用户会话清理

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

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

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

1.3 系统服务停止

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

服务停止顺序

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

关键服务处理

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

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

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

文件系统卸载

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

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

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

1.4 内核资源释放

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

文件系统操作

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

进程管理

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

Watchdog 监控

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

资源清理

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

1.5 硬件关机

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

ACPI 操作

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

固件接管

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

强制关机保护

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

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

1.6 关机故障排查

常见关机问题与优化

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

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

服务停止超时优化:

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

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

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

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

文件系统卸载优化:

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

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

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

# 查看块设备状态
lsblk -f

设备占用优化:

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

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

强制关机处理与优化

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

# 安全强制关机
systemctl poweroff -f

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

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

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

强制关机方法:

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

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

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

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

关机优化最佳实践:

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

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

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


系统休眠与挂起

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

3.1 休眠(Hibernate)功能

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

休眠的工作原理

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

休眠配置

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

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

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

启用休眠功能

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

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

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

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

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

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

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

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

休眠故障排查

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

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

# 测试休眠功能
sudo systemctl hibernate

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

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

常见休眠问题

  1. 交换空间不足

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

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

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

3.2 挂起(Suspend)功能

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

挂起的工作原理

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

挂起类型

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

挂起配置

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

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

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

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

挂起故障排查

# 测试挂起功能
sudo systemctl suspend

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

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

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

常见挂起问题

  1. 挂起后无法唤醒

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

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

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

3.3 电源管理模式对比

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

选择建议

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

混合使用策略

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

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

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

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

电源管理优化

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

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

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

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


实战案例:综合故障排查

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

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

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

排查步骤

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

常见原因

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

解决方法

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

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

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

排查步骤

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

常见原因

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

解决方法

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

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

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

排查步骤

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

常见原因

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

解决方法

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

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

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

排查步骤

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

常见原因

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

解决方法

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

2.5 综合排查方法

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

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

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

总结

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

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

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

🔗 相关资源

Linux 桌面系统故障排查指南(五) - 网络

2025年10月19日 10:21

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

前言

网络连接是现代桌面的基础功能,涉及硬件驱动、固件加载、网络管理和 DNS 解析等多个环节。

本文将从网卡驱动开始,经过内核网络栈,到达应用层,了解 Linux 网络系统的完整架构,包括如何配置网络连接,如何设置防火墙规则,以及如何诊断各种网络问题。


网络连接与管理

网络连接是现代桌面的基础功能,涉及硬件驱动、固件加载、网络管理和 DNS 解析等多个环节。网络故障是最常见的桌面问题之一,理解其工作原理有助于快速定位和解决连接问题。

1.1 网络架构概览

现代 Linux 桌面大多使用 systemd-networkd 配合 iwd 进行网络管理,形成完整的网络解决方案。

虽然目前仍有部分系统默认使用 NetworkManager 管理网络,用 wpa_supplicant 管理 WiFi, 但这已经不够「现代」了(逃

网络协议栈

  • 硬件层:网卡驱动和固件
  • 链路层:MAC 地址管理和链路检测
  • 网络层:IP 地址配置和路由管理
  • 传输层:TCP / UDP 连接管理
  • 应用层:DNS 解析和服务发现

主要组件

  • systemd-networkd:网络接口管理,处理 DHCP 和静态配置
  • iwd:无线网络管理,支持 WPA2 / WPA3
  • systemd-resolved:DNS 解析和缓存

1.2 网络连接流程

有线网络

  1. 内核加载网卡驱动
  2. 检测链路状态(网线连接)
  3. systemd-networkd 通过 DHCP 获取 IP 配置
  4. 配置路由和 DNS

无线网络

  1. 加载无线网卡驱动和固件
  2. iwd 扫描可用网络
  3. 选择网络并进行认证(WPA2 / WPA3)
  4. 建立连接后通过 DHCP 获取 IP

网络管理命令

# 查看接口状态
ip link show
ip addr show

# 无线网络管理(iwd)
iwctl station wlan0 scan
iwctl station wlan0 connect "SSID"

# 网络服务状态
systemctl status systemd-networkd iwd

# DNS 解析测试
resolvectl query example.com
resolvectl status

1.3 IPv4 / IPv6 双栈配置

现代网络正在往 IPv6 迁移的过程中,目前仍有许多站点都只支持 IPv6,因此 IPv4+IPv6 双栈成为一个过渡方案,systemd-networkd 提供完整的双栈支持。

双栈特点

  • IPv4:通过 DHCP 获取配置,32 位地址
  • IPv6:通过 Router Advertisement 获取,128 位地址
  • 并行工作:两个协议栈同时运行
  • IPv6 优先:通常有 IPv6 的会优先走 IPv6 网络,没有才走 IPv4.
    • Linux 中通过 glibc 的 getaddrinfo() 来实现该逻辑,可通过 /etc/gai.conf 调整该函数的地址排序算法。因为 APP 通常直接使用第一条记录发起连接,所以 /etc/gai.conf 通常能直接决定系统中是 IPv6 优先还是 IPv4 优先。

双栈验证

# 查看 IPv4 配置
ip -4 addr show
ip -4 route

# 查看 IPv6 配置
ip -6 addr show
ping -6 2001:4860:4860::8888

# DNS 双栈测试
nslookup -type=A google.com
nslookup -type=AAAA google.com

1.4 网络故障排查

连接问题诊断流程

  1. 硬件层面
# 检查接口存在
ip link show

# 查看驱动加载
dmesg | grep -i firmware
lspci | grep -i network
  1. 链路层面
# 有线:检查链路状态
ethtool eth0

# 无线:扫描网络
iw dev wlan0 scan | grep SSID
  1. 网络配置
# DHCP 状态
journalctl -u systemd-networkd

# IP 配置检查
ip addr show dev eth0

# 路由表
ip route
  1. DNS 解析
# DNS 配置
resolvectl status
cat /etc/resolv.conf

# 解析测试
dig @8.8.8.8 example.com
nslookup example.com

常见问题与解决

  • 无法获取 IP:检查 DHCP 服务、网线连接、无线密码
  • DNS 解析失败:验证 DNS 服务器配置、检查 systemd-resolved 状态
  • IPv6 无连接:确认路由器支持 IPv6、检查 IPv6AcceptRA 配置
  • 连接不稳定:查看信号强度、检查驱动兼容性

防火墙与网络安全

2.1 nftables 防火墙配置

nftables 是现代 Linux 的防火墙解决方案,它提供比 iptables 更简洁的语法和更好的性能。

基本概念

  • 表(Table):包含链和规则的容器
  • 链(Chain):规则的有序列表
  • 规则(Rule):匹配条件和动作
  • 集合(Set):用于批量匹配的地址或端口列表

nftables 的四表五链、规则等概念跟 iptables 是完全一致的,这一部分可以参考我之前的文章iptables 及 docker 容器网络分析, 这里不再赘述。

NixOS 配置示例

# configuration.nix
networking.nftables = {
  enable = true;
  ruleset = ''
    # 定义表
    table inet filter {
      # 定义链
      chain input {
        type filter hook input priority 0; policy drop;

        # 允许回环接口
        if lo accept

        # 允许已建立的连接
        ct state established,related accept

        # 允许 SSH
        tcp dport 22 accept

        # 允许 HTTP/HTTPS
        tcp dport {80, 443} accept

        # 允许 DNS
        udp dport 53 accept
        tcp dport 53 accept

        # 允许 DHCP
        udp dport 67 accept
        udp dport 68 accept

        # 允许 ICMP
        icmp type {echo-request, echo-reply, destination-unreachable} accept
        ip6 nexthdr icmpv6 icmpv6 type {echo-request, echo-reply, destination-unreachable} accept
      }

      chain forward {
        type filter hook forward priority 0; policy drop;
      }

      chain output {
        type filter hook output priority 0; policy accept;
      }
    }
  '';
};

常用 nftables 命令

# 查看当前规则
nft list ruleset

# 查看特定表
nft list table inet filter

# 临时添加规则
nft add rule inet filter input tcp dport 8080 accept

# 删除规则
nft delete rule inet filter input handle <handle>

# 清空表
nft flush table inet filter

2.2 网络地址转换(NAT)

端口转发配置

networking.nftables.ruleset = ''
  table inet nat {
    chain prerouting {
      type nat hook prerouting priority 0;

      # 端口转发:将外部 8080 端口转发到内网 192.168.1.100:80
      tcp dport 8080 dnat to 192.168.1.100:80
    }

    chain postrouting {
      type nat hook postrouting priority 100;

      # 源地址转换(SNAT)
      oifname "eth0" masquerade
    }
  }
'';

虚拟网络技术

3.1 VPN 连接管理

WireGuard 配置

# configuration.nix
networking.wireguard.interfaces = {
  wg0 = {
    ips = [ "10.0.0.2/24" ];
    privateKeyFile = "/etc/wireguard/private.key";
    peers = [
      {
        publicKey = "peer-public-key";
        allowedIPs = [ "0.0.0.0/0" ];
        endpoint = "vpn.example.com:51820";
        persistentKeepalive = 25;
      }
    ];
  };
};

3.2 虚拟网络接口

TUN/TAP 接口

# 创建 TUN 接口
ip tuntap add dev tun0 mode tun
ip addr add 10.0.0.1/24 dev tun0
ip link set tun0 up

# 创建 TAP 接口
ip tuntap add dev tap0 mode tap
ip addr add 192.168.100.1/24 dev tap0
ip link set tap0 up

桥接网络

# 创建网桥
ip link add name br0 type bridge
ip link set dev br0 up

# 添加接口到网桥
ip link set dev eth1 master br0
ip link set dev tap0 master br0

# 配置网桥 IP
ip addr add 192.168.1.1/24 dev br0

3.3 容器网络

Docker 网络管理

# 查看网络
docker network ls

# 创建自定义网络
docker network create --driver bridge --subnet=172.20.0.0/16 mynetwork

# 连接容器到网络
docker network connect mynetwork container_name

# 查看网络详情
docker network inspect mynetwork

Podman 网络配置

# 创建网络
podman network create mynet

# 运行容器
podman run --network mynet -d nginx

# 查看网络
podman network ls

网络性能优化

4.1 网络参数调优

内核网络参数

# configuration.nix
boot.kernel.sysctl = {
  # TCP 缓冲区大小
  "net.core.rmem_max" = 134217728;
  "net.core.wmem_max" = 134217728;
  "net.ipv4.tcp_rmem" = "4096 87380 134217728";
  "net.ipv4.tcp_wmem" = "4096 65536 134217728";

  # TCP 拥塞控制
  "net.ipv4.tcp_congestion_control" = "bbr";

  # 连接跟踪
  "net.netfilter.nf_conntrack_max" = 1048576;
  "net.netfilter.nf_conntrack_tcp_timeout_established" = 3600;

  # 网络队列
  "net.core.netdev_max_backlog" = 5000;
  "net.core.netdev_budget" = 600;
};

网络参数调优:

TCP 缓冲区优化:

net.core.rmem_max = 134217728 设置 TCP 接收缓冲区的最大值为 128MB。更大的接收缓冲区可以处理突发的高流量,减少丢包,提高网络吞吐量,特别适合高带宽网络环境,适用于高带宽、高延迟网络,如光纤网络、VPN 连接。

net.core.wmem_max = 134217728 设置 TCP 发送缓冲区的最大值为 128MB。更大的发送缓冲区可以缓存更多待发送数据,提高发送效率,减少发送阻塞,提高网络传输效率,适用于大文件传输、流媒体上传、高并发网络应用。

net.ipv4.tcp_rmem = "4096 87380 134217728" 设置 TCP 接收缓冲区的初始值、默认值和最大值。参数说明:初始值 4KB,默认值 87KB,最大值 128MB。动态调整接收缓冲区大小,根据网络条件自动优化,在低延迟和高吞吐量之间自动平衡。

net.ipv4.tcp_wmem = "4096 65536 134217728" 设置 TCP 发送缓冲区的初始值、默认值和最大值。参数说明:初始值 4KB,默认值 64KB,最大值 128MB。动态调整发送缓冲区大小,适应不同的网络负载,在内存使用和网络性能之间找到最佳平衡点。

TCP 拥塞控制优化:

net.ipv4.tcp_congestion_control = "bbr" 使用 BBR(Bottleneck Bandwidth and RTT)拥塞控制算法。BBR 是 Google 开发的现代拥塞控制算法,基于带宽和延迟测量,在高带宽、高延迟网络环境下性能更好,减少延迟和丢包,适用于现代网络环境,特别是高带宽网络和长距离连接。

连接跟踪优化:

net.netfilter.nf_conntrack_max = 1048576 增加连接跟踪表大小到 100 万条记录。支持更多并发网络连接,避免连接跟踪表溢出,支持高并发网络应用,如 P2P 下载、多用户服务,适用于服务器环境、高并发网络应用。

net.netfilter.nf_conntrack_tcp_timeout_established = 3600 设置已建立连接的超时时间为 1 小时。延长连接跟踪时间,减少连接重建的频率,减少连接重建开销,提高长连接应用的性能,适用于长连接应用,如数据库连接、WebSocket 连接。

网络队列优化:

net.core.netdev_max_backlog = 5000 增加网络设备接收队列大小到 5000 个数据包。更大的接收队列可以处理突发流量,减少丢包,提高网络处理能力,减少因队列满而导致的丢包,适用于高流量网络环境,如服务器、网络设备。

net.core.netdev_budget = 600 增加每次网络处理的数据包数量到 600 个。提高网络处理效率,减少处理开销,提高网络吞吐量,减少 CPU 使用率,适用于高负载网络环境,需要优化网络处理性能。

优化效果评估:通过缓冲区优化,网络吞吐量可提升 20-50%;BBR 拥塞控制算法可显著减少网络延迟;连接跟踪优化支持更多并发连接;队列优化减少丢包,提高网络稳定性。

4.2 网络监控与分析

网络流量监控

# 实时流量监控
iftop -i eth0

# 网络连接监控
netstat -tuln
ss -tuln

# 网络统计
cat /proc/net/dev
cat /proc/net/snmp

# 带宽测试
iperf3 -s  # 服务器端
iperf3 -c server_ip  # 客户端

网络延迟分析

# ping 测试
ping -c 10 8.8.8.8

# 路由跟踪
traceroute 8.8.8.8
mtr 8.8.8.8

# 网络质量测试
qperf server_ip tcp_bw tcp_lat

4.3 网络故障诊断

连接问题排查

# 检查网络接口状态
ip link show
ip addr show

# 检查路由表
ip route show
ip route get 8.8.8.8

# 检查 ARP 表
ip neigh show

# 检查网络统计
cat /proc/net/dev
cat /proc/net/snmp

DNS 问题排查

# 测试 DNS 解析
dig @8.8.8.8 example.com
nslookup example.com

# 检查 DNS 配置
resolvectl status
cat /etc/resolv.conf

# 测试 DNS 性能
dig @8.8.8.8 example.com +stats

防火墙问题排查

# 检查防火墙规则
nft list ruleset
iptables -L -v -n

# 测试端口连通性
telnet server_ip port
nc -zv server_ip port

# 检查连接跟踪
cat /proc/net/nf_conntrack

高级网络配置

5.1 多网卡绑定

网卡绑定配置

# configuration.nix
networking.bonds = {
  bond0 = {
    interfaces = [ "eth0" "eth1" ];
    driverOptions = {
      mode = "802.3ad";
      lacp_rate = "fast";
      xmit_hash_policy = "layer3+4";
    };
  };
};

networking.interfaces.bond0.ipv4.addresses = [{
  address = "192.168.1.100";
  prefixLength = 24;
}];

5.2 VLAN 配置

VLAN 网络配置

# configuration.nix
networking.vlans = {
  vlan100 = { id = 100; interface = "eth0"; };
  vlan200 = { id = 200; interface = "eth0"; };
};

networking.interfaces.vlan100.ipv4.addresses = [{
  address = "192.168.100.1";
  prefixLength = 24;
}];

networking.interfaces.vlan200.ipv4.addresses = [{
  address = "192.168.200.1";
  prefixLength = 24;
}];

5.3 网络命名空间

创建网络命名空间

# 创建命名空间
ip netns add ns1
ip netns add ns2

# 创建 veth 对
ip link add veth1 type veth peer name veth2

# 将接口移到命名空间
ip link set veth1 netns ns1
ip link set veth2 netns ns2

# 配置命名空间内的网络
ip netns exec ns1 ip addr add 10.0.1.1/24 dev veth1
ip netns exec ns1 ip link set veth1 up
ip netns exec ns2 ip addr add 10.0.1.2/24 dev veth2
ip netns exec ns2 ip link set veth2 up

# 测试连通性
ip netns exec ns1 ping 10.0.1.2

总结

网络是计算机科学中最复杂的技术之一,数据在互联网中的流动造就了现代信息社会,现代 AI 的发展也与现代网络中产生的超大规模数据密不可分。

本文只是对 Linux 网络的一个简单介绍,下一篇文章我们会聊聊系统关机和故障排查,看看系统是如何优雅地关机的,以及遇到问题时该如何处理。

快速参考

常用网络管理命令

# 网络接口管理
ip link show                           # 查看网络接口
ip addr show                          # 查看 IP 地址
ip route show                         # 查看路由表
ip neigh show                         # 查看 ARP 表

# 网络连接管理
ss -tuln                              # 查看网络连接
netstat -tuln                         # 传统网络连接查看
lsof -i                               # 查看端口占用

# 网络测试
ping -c 4 8.8.8.8                    # ping 测试
traceroute 8.8.8.8                   # 路由跟踪
mtr 8.8.8.8                          # 网络质量测试

常用防火墙命令

# nftables 管理
nft list ruleset                      # 查看所有规则
nft list table inet filter            # 查看特定表
nft add rule inet filter input tcp dport 8080 accept  # 添加规则
nft delete rule inet filter input handle <handle>     # 删除规则

# iptables 管理(传统)
iptables -L -v -n                     # 查看规则
iptables -A INPUT -p tcp --dport 22 -j ACCEPT  # 添加规则
iptables -D INPUT -p tcp --dport 22 -j ACCEPT  # 删除规则

常用网络诊断命令

# DNS 解析测试
dig @8.8.8.8 example.com              # DNS 查询
nslookup example.com                  # 传统 DNS 查询
resolvectl query example.com          # systemd-resolved 查询

# 网络监控
iftop -i eth0                         # 实时流量监控
tcpdump -i eth0                       # 网络包捕获
wireshark                             # 图形化网络分析

# 带宽测试
iperf3 -s                             # 启动 iperf3 服务器
iperf3 -c server_ip                   # 客户端测试

重要配置文件位置

# 网络配置
/etc/systemd/network/                 # systemd-networkd 配置
/etc/nftables.conf                    # nftables 配置
/etc/resolv.conf                      # DNS 配置

# 网络服务
/etc/systemd/system/                  # systemd 服务配置
/etc/wireguard/                       # WireGuard 配置
/etc/openvpn/                         # OpenVPN 配置

# 网络状态
/proc/net/dev                         # 网络接口统计
/proc/net/snmp                        # 网络协议统计
/proc/net/nf_conntrack                # 连接跟踪表

Linux 桌面系统故障排查指南(四) - 多媒体处理与中文支持

2025年10月19日 10:20

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

前言

Linux 桌面系统的多媒体处理和中文支持涉及多个子系统。音频延迟、字体渲染质量、输入法响应速度等问题看似简单,背后却涉及 PipeWire、fontconfig、fcitx5 等多个组件的协同工作。

本文将深入探讨 Linux 桌面系统的多媒体处理能力,了解 PipeWire 如何统一管理音频和视频,fontconfig 如何优化字体显示,以及 fcitx5 如何提供流畅的中文输入体验。


多媒体处理

现代 Linux 桌面(Wayland)由 PipeWire 统一处理音频和视频,取代了传统的 PulseAudio 和 JACK。PipeWire 提供了更低的延迟、更好的硬件兼容性,以及统一的媒体处理框架。

1.1 PipeWire 架构概览

https://docs.pipewire.org/page_overview.html

PipeWire 作为媒体服务器的核心,连接应用程序和硬件设备,提供音频混合、视频处理和路由功能。它从一开始就定位为"通用多媒体处理框架",而非仅局限于音频,这种设计源于现代多媒体场景(如视频会议、屏幕共享、直播、跨应用媒体协作等)对"音频+视频"统一处理的强需求。Pipewire 支持所有接入 PulseAudio,JACK,ALSA 和 GStreamer 的程序。

核心组件

  • pipewire:核心守护进程,管理媒体流图
  • wireplumber:会话管理器,处理设备连接和路由策略
  • pipewire-pulse:PulseAudio 兼容层
  • pipewire-jack:JACK 专业音频兼容层
  • pipewire-alsa:ALSA 兼容层

技术特点

  • 统一架构:同时处理音频、视频、MIDI
  • 低延迟:相比 PulseAudio 显著降低音频延迟
  • 硬件兼容:支持专业音频设备和消费级硬件
  • 安全隔离:通过权限控制保护媒体数据

NixOS 配置

services.pipewire = {
  enable = true;
  alsa.enable = true;      # ALSA 兼容
  pulse.enable = true;     # PulseAudio 兼容
  jack.enable = true;      # JACK 兼容
};

services.pipewire.wireplumber.enable = true;

# 禁用 PulseAudio 避免冲突
hardware.pulseaudio.enable = false;

配置文件路径

  • /etc/pipewire/pipewire.conf:主配置文件
  • /etc/pipewire/pipewire-pulse.conf:PulseAudio 兼容配置
  • /etc/wireplumber/:WirePlumber 会话管理器配置

1.2 音频处理流程

应用播放音频的典型流程

  1. API 连接:应用通过 ALSA / PulseAudio / JACK API 连接到 PipeWire
  2. 流创建:在 PipeWire 图中创建音频流节点
  3. 路由决策:WirePlumber 根据策略路由到输出设备
  4. 音频处理:混合多个应用流,执行格式转换、音量调节、调整音频效果
  5. 硬件输出:通过 ALSA 驱动将 PCM 数据发送给声卡 DAC,最终输出模拟音频输出

音频节点管理

# 查看音频设备
pw-cli list-objects | grep -E "(Audio|Sink|Source)"

# 实时监控音频流
pw-top

# 图形界面管理
pavucontrol

# 查看 ALSA 设备
aplay -l
arecord -l

音频路由控制

# 设置默认输出设备
pactl set-default-sink alsa_output.pci-0000_00_1f.3.analog-stereo

# 应用音量控制
pactl list sink-inputs
pactl set-sink-input-volume 123 50%

# 创建自定义连接
pw-cli create-link <source-node> <sink-node>

1.3 视频处理与屏幕共享

1.3.1 为什么 PipeWire 要支持视频流处理?

传统 Linux 系统中,音频和视频处理长期处于"各自为战"的状态:

  • 音频:由 PulseAudio(桌面)、JACK(专业)等系统负责
  • 视频:依赖 V4L2(摄像头捕获)、X11/Wayland(屏幕截图)、GStreamer(流处理)、FFmpeg(编解码)等分散组件

这种碎片化导致了诸多问题:

  • 跨应用同步困难:直播时麦克风声音与摄像头画面延迟不一致
  • 权限管理混乱:沙盒应用如 Flatpak 访问摄像头/屏幕需单独适配
  • 现代场景支持不足:Wayland 下的屏幕共享、HDR 视频渲染缺乏统一支持
  • 硬件加速复杂:GPU 编解码需各组件单独对接,兼容性差

PipeWire 的设计初衷就是打破这种割裂:通过一套统一的框架同时管理音频和视频流,让"音频+ 视频"的协作(如会议软件同时捕获麦克风和摄像头、直播工具混合游戏画面与解说声音)变得简单高效。因此,视频处理是其"统一多媒体管道"目标的自然延伸。

1.3.2 PipeWire 视频处理的核心优势

PipeWire 作为现代 Linux 桌面系统的多媒体框架,相比传统方案具有以下核心优势:

统一的"管道"模型

  • 音频流和视频流都被抽象为"节点"
  • 通过统一的"节点-端口-连接"模型实现跨应用的音视频混合
  • 框架内置时间戳同步机制,确保音视频流始终保持时序一致(延迟误差可控制在毫秒级)

原生适配现代桌面协议

  • 作为 Wayland 官方推荐的多媒体中间层
  • 通过与 xdg-desktop-portal 框架协同工作,实现标准化的"授权式"屏幕共享
  • 支持 HDR 视频和高分辨率流传输,性能损耗远低于传统 X11 截图
  • 遵循 Wayland 安全隔离原则,所有跨应用资源访问都通过用户授权的门户接口

简化沙盒应用权限

  • 通过 Polkit 权限系统集中管理设备访问
  • 沙盒应用无需直接操作硬件设备,只需通过 PipeWire API 请求流数据
  • 支持动态权限调整

高效硬件加速整合

  • 内置统一的硬件加速抽象层
  • 通过 GStreamer 或 FFmpeg 后端自动适配底层硬件加速接口
  • 支持"零拷贝"传输,CPU 占用率可降低 50% 以上

灵活的动态路由

  • 允许实时调整视频流路径
  • 用户可通过图形工具拖拽节点实现流的动态切换
  • 支持自动故障恢复和流的动态转换

1.3.3 Wayland 屏幕共享与 xdg-desktop-portal

在 Wayland 环境中,屏幕共享功能是通过 xdg-desktop-portal 和 PipeWire 协同工作实现的。这与 X11 有很大的不同,后者通过其自身的扩展(如 X11R6 的 XFIXES 扩展)直接访问屏幕内容。

工作原理

在 Wayland 下,每个应用程序只能访问自己的窗口、键盘鼠标事件等,无法随意截屏或访问全局资源。屏幕共享的完整流程是:

  1. 应用发起请求:视频会议软件调用 org.freedesktop.portal.ScreenCast 接口,请求屏幕共享
  2. 用户授权:xdg-desktop-portal 显示原生对话框,请求用户确认共享范围(整个屏幕/特定窗口/区域)
  3. 合成器提供数据:获得授权后,xdg-desktop-portal 通知合成器,合成器将相应的屏幕内容通过 PipeWire 流返回给 xdg-desktop-portal.
  4. PipeWire 流传输:xdg-desktop-portal 将流信息返回给应用,应用程序通过 PipeWire 接收屏幕数据进行处理和传输

可以看到应用程序只需要先与 xdg-desktop-portal 交互获得 PipeWire 流信息,然后直接访问 PipeWire, 全程都不直接与合成器交互。

协议优势

  • 安全性:基于 xdg-desktop-portal 的授权机制,需要用户明确同意
  • 隐私保护:用户可以精确控制共享范围,只共享特定窗口而非整个屏幕
  • 性能:直接访问合成器缓冲区,避免额外的内存拷贝
  • 兼容性:支持多显示器、不同分辨率和刷新率
  • 标准化:所有应用都使用相同的 portal 接口,确保跨桌面环境的一致体验

门户实现要求

要使用 Wayland 屏幕共享,系统需要安装 DE/WM 所支持的 xdg-desktop-portal 实现。

主流应用支持:目前主流的 OBS、Discord、Zoom、Chrome/Chromium 等应用都已经支持基于 xdg-desktop-portal 的 Wayland 屏幕共享机制。

1.3.4 视频设备管理

摄像头设备管理

# 查看 PipeWire 视频设备
pw-cli list-objects | grep -i video

# 查看 V4L2 设备
v4l2-ctl --list-devices

# 摄像头格式查询
v4l2-ctl --device=/dev/video0 --list-formats

# 摄像头权限检查
ls -l /dev/video*
groups $USER  # 确认在 video 组

# 测试摄像头
ffplay /dev/video0

屏幕共享环境配置

# Wayland 环境检查
echo $WAYLAND_DISPLAY
echo $XDG_SESSION_TYPE

# 设置桌面环境标识(重要!)
export XDG_CURRENT_DESKTOP=sway  # 或 gnome, kde, xfce 等

# 检查 PipeWire 服务状态
systemctl --user status pipewire-session-manager
systemctl --user status pipewire

# 检查桌面门户服务
systemctl --user status xdg-desktop-portal
systemctl --user status xdg-desktop-portal-wlr  # Sway/Hyprland
# 或
systemctl --user status xdg-desktop-portal-gnome  # GNOME

1.3.5 视频流处理配置

PipeWire 视频配置

NixOS 中可通过 services.pipewire.extraConfig.pipewire."10-video"."context.properties" 来声明这部分配置。

# 编辑 PipeWire 主配置
vim ~/.config/pipewire/pipewire.conf

# 视频相关配置示例
context.properties = {
    # 视频缓冲区配置
    default.video.rate = 30
    default.video.size = "1920x1080"

    # 硬件加速配置
    gstreamer.plugins = [
        "vaapi"      # Intel/AMD GPU 硬件加速
        "nvenc"      # NVIDIA GPU 硬件加速
    ]
}

1.3.6 视频处理性能优化

硬件加速配置

# 检查硬件加速支持
vainfo  # VA-API 支持检查
nvidia-smi  # NVIDIA GPU 状态

# 环境变量设置
export LIBVA_DRIVER_NAME=i965  # Intel GPU
export LIBVA_DRIVER_NAME=radeonsi  # AMD GPU
export LIBVA_DRIVER_NAME=nvidia  # NVIDIA GPU

# GStreamer 硬件加速测试
gst-launch-1.0 videotestsrc ! vaapih264enc ! mp4mux ! filesink location=test.mp4

视频编码优化

# FFmpeg 硬件加速编码
ffmpeg -f v4l2 -i /dev/video0 -c:v h264_vaapi -b:v 2M output.mp4

# OBS 硬件编码配置
# 设置 -> 输出 -> 编码器选择 "FFmpeg VAAPI" 或 "NVENC"

内存和 CPU 优化

# 调整视频缓冲区大小
vim ~/.config/pipewire/pipewire.conf

context.properties = {
    # 减少视频缓冲区延迟
    default.video.quantum = 1/30  # 30fps
    default.video.min-quantum = 1/30
    default.video.max-quantum = 1/15  # 最大 15fps 延迟
}

1.4 故障排查

屏幕共享问题

  1. xdg-desktop-portal 服务状态:确认门户服务正常运行
  2. 门户实现安装:检查是否安装了适合的 portal 实现
  3. 环境变量设置:正确设置 XDG_CURRENT_DESKTOP
  4. 权限配置:检查摄像头和屏幕录制权限
  5. 合成器支持:确认合成器支持相应的 portal 接口
  6. 应用兼容性:部分应用需要特定版本的 PipeWire 和 portal 支持

音频设备识别问题

  • 检查设备存在
aplay -l
arecord -l
  • 验证 PipeWire 运行
systemctl --user status pipewire wireplumber
journalctl --user -u pipewire -f
  • 权限检查
ls -l /dev/snd/
groups $USER  # 确认在 audio 组

音频延迟优化

# 编辑用户配置
vim ~/.config/pipewire/pipewire.conf

# 低延迟配置示例
context.properties = {
    default.clock.rate = 48000
    default.clock.quantum = 32
    default.clock.min-quantum = 32
    default.clock.max-quantum = 32
}

PipeWire 低延迟配置:

default.clock.rate = 48000 设置音频采样率为 48kHz,平衡音质和性能。48kHz 是专业音频的标准采样率,提供良好的音质同时保持合理的计算开销。相比 44.1kHz 提供更好的音质,相比 96kHz 减少 CPU 和内存使用,适用于大多数音频应用,特别是需要低延迟的实时音频处理。

default.clock.quantum = 32 设置音频缓冲区大小为 32 个样本,约 0.67ms 延迟。较小的缓冲区减少音频延迟,但需要更频繁的音频处理。计算方式:32 样本 ÷ 48000Hz = 0.67ms 延迟,适用于实时音频应用,如音乐制作、游戏、视频会议。

default.clock.min-quantum = 32 设置最小缓冲区大小,防止系统动态调整到更小的值。固定最小缓冲区大小,避免系统在低负载时过度优化导致的不稳定,确保延迟的一致性,避免音频处理的不稳定。

default.clock.max-quantum = 32 设置最大缓冲区大小,防止系统动态调整到更大的值。固定最大缓冲区大小,避免系统在高负载时增加延迟,确保延迟的上限,保持低延迟特性。

延迟优化效果:约 0.67ms 的音频延迟,适合实时应用;适度的 CPU 使用增加,但通常可接受;固定缓冲区大小提供更稳定的音频处理;特别适合音乐制作、游戏、实时通信等对延迟敏感的应用。

注意事项:过小的缓冲区可能导致音频断断续续或 CPU 使用率过高;需要根据具体硬件和应用需求调整参数;某些音频设备可能不支持极小的缓冲区大小。


中文支持

中文支持是中文用户桌面体验的核心组成部分,包括字体渲染配置和中文输入法设置。本章节将详细介绍如何在 Linux 桌面环境中正确配置中文字体和输入法,解决常见的显示和输入问题。

2.1 字体渲染

字体渲染是桌面应用显示质量的关键因素,特别是对于中文用户,CJK(中日韩)字体的正确配置直接影响阅读体验。Linux 桌面通过 fontconfig 系统统一管理字体配置,解决字体匹配、渲染和显示问题。

2.1.1 fontconfig 架构概览

fontconfig 是 Linux 桌面系统的字体配置框架,负责:

  • 字体发现:扫描系统字体目录,建立字体索引
  • 字体匹配:根据应用请求的字体特征(族名、样式、语言等)选择最合适的字体
  • 字体渲染:配置字体渲染参数(抗锯齿、子像素渲染、提示等)
  • 字体替换:当请求的字体不存在时,提供合适的替代字体

核心组件

  • fc-cache:字体缓存生成工具
  • fc-list:字体列表查询工具
  • fc-match:字体匹配测试工具
  • 配置文件:XML 格式的字体配置规则

配置文件层次

# 系统级配置(优先级从高到低)
/etc/fonts/fonts.conf                    # 主配置文件
/etc/fonts/conf.d/                       # 配置片段目录

# 用户级配置
~/.config/fontconfig/fonts.conf          # 用户主配置
~/.config/fontconfig/conf.d/             # 用户配置片段

2.1.2 CJK 字体配置基础

常见 CJK 字体族

字体族 特点 适用场景
Source Han Sans Adobe 开源,专业设计 现代应用,网页显示
Source Han Serif Adobe 开源,衬线字体 设计软件,印刷
Source Han Mono 思源等宽字体 编程,代码显示
Noto Sans CJK Google 开源,与 Source Han 为同一字体 系统界面,兼容性
WenQuanYi 文泉驿,轻量级 系统界面,终端

说明:Source Han 系列和 Noto CJK 系列实际上是同一套字体,只是分别由 Adobe 和 Google 以自己的品牌名发布。

以及一些新兴的开源字体:

字体族 特点 适用场景
LXGW WenKai Screen 霞鹜文楷屏幕版 屏幕阅读,文档
Maple Mono NF CN 中英文等宽字体 编程,终端

NixOS 字体配置示例

# configuration.nix
fonts = {
  # 禁用默认字体包,使用自定义配置
  enableDefaultPackages = false;
  fontDir.enable = true;

  # 安装常用 CJK 字体和图标字体
  packages = with pkgs; [
    # 图标字体
    material-design-icons
    font-awesome
    nerd-fonts.symbols-only
    nerd-fonts.jetbrains-mono

    # Noto 是 Google 开发的开源字体家族
    # 名字的含义是「没有豆腐」(no tofu),因为缺字时显示的方框或者方框被叫作 tofu
    #
    # Noto 系列字族只支持西文,命名规则是 Noto + Sans 或 Serif + 文字名称。
    noto-fonts # 大部分文字的常见样式,不包含汉字
    noto-fonts-color-emoji # 彩色的表情符号字体
    # Noto CJK 为「思源」系列汉字字体,由 Adobe + Google 共同开发
    # Google 以 Noto Sans/Serif CJK SC/TC/HK/JP/KR 的名称发布该系列字体。
    # 这俩跟 noto-fonts-cjk-sans/serif 实际为同一字体,只是分别由 Adobe/Google 以自己的品牌名发布
    # noto-fonts-cjk-sans # 思源黑体
    # noto-fonts-cjk-serif # 思源宋体

    # Adobe 以 Source Han Sans/Serif 的名称发布此系列字体
    source-sans # 无衬线字体,不含汉字。字族名叫 Source Sans 3,以及带字重的变体(VF)
    source-serif # 衬线字体,不含汉字。字族名叫 Source Serif 4,以及带字重的变体
    # Source Hans 系列汉字字体由 Adobe + Google 共同开发
    source-han-sans # 思源黑体
    source-han-serif # 思源宋体
    source-han-mono # 思源等宽
  ];

  # 字体渲染配置
  fontconfig = {
    enable = true;
    antialias = true;        # 启用抗锯齿
    hinting.enable = false;  # 高分辨率下禁用字体微调
    subpixel.rgba = "rgb";   # IPS 屏幕使用 RGB 子像素排列

    # 默认字体族配置
    defaultFonts = {
      serif = [
        "Source Serif 4"        # 西文衬线字体
        "Source Han Serif SC"   # 中文宋体
        "Source Han Serif TC"   # 繁体宋体
      ];
      sansSerif = [
        "Source Sans 3"         # 西文无衬线字体
        "Source Han Sans SC"    # 中文黑体
        "Source Han Sans TC"    # 繁体黑体
      ];
      monospace = [
        "Maple Mono NF CN"      # 中英文等宽字体
        "Source Han Mono SC"    # 中文等宽
        "JetBrainsMono Nerd Font"  # 西文等宽
      ];
      emoji = [ "Noto Color Emoji" ];
    };
  };
};

字体渲染配置参数:

antialias = true 启用字体抗锯齿,让字体边缘更平滑,提升显示质量。通过灰度插值技术平滑字体边缘,减少锯齿效果,显著提升文字显示质量,特别是在高分辨率屏幕上,适用于所有现代显示设备,特别是高分辨率屏幕。

hinting.enable = false 在高分辨率屏幕(如 4K)上禁用字体微调,避免过度渲染。字体微调 (hinting)是为低分辨率屏幕设计的优化技术,在高分辨率下可能造成过度渲染,在高分辨率屏幕上提供更自然的字体显示效果,适用于高分辨率屏幕(通常 200+ DPI),如 4K 显示器、高分辨率笔记本屏幕。

subpixel.rgba = "rgb" 针对 IPS 屏幕的 RGB 子像素排列优化,提升字体清晰度。利用 LCD 屏幕的 RGB 子像素结构,通过子像素渲染技术提升字体清晰度,在 LCD 屏幕上显著提升字体清晰度,减少模糊感,适用于 IPS、TN、VA 等 LCD 屏幕,不适用于 OLED 屏幕。

字体渲染优化效果:抗锯齿和子像素渲染显著提升文字显示质量;在高分辨率屏幕上禁用微调提供更自然的显示效果;合理的字体回退机制确保各种文字的正确显示;优化的渲染配置在提升质量的同时保持良好性能。

重要说明:Source Han 系列(Adobe 发布)和 Noto CJK 系列(Google 发布)实际上是同一套字体,只是分别由 Adobe 和 Google 以自己的品牌名发布。在 NixOS 中,source-han-sansnoto-fonts-cjk-sans 指向的是同一套字体文件。

2.1.3 常见 CJK 字体问题与解决方法

问题 1:中文字符显示为方块或问号

原因:系统缺少中文字体或字体匹配规则不正确

排查步骤

# 1. 检查已安装的 CJK 字体
fc-list :lang=zh-cn

# 2. 测试字体匹配
fc-match "sans-serif:lang=zh-cn"
fc-match "serif:lang=zh-cn"

# 3. 查看字体详细信息
fc-list | grep -i "noto\|source\|wqy"

使用上面提供的示例配置通常可解决问题。

问题 2:中文字体中夹杂日语字体

原因:CJK 字体通常包含中文、日文、韩文字符,当系统缺少专门的中文字体时,会使用包含日文字符的 CJK 字体,导致中文字符显示为日语字形。

排查步骤

# 检查当前使用的字体
fc-match "sans-serif:lang=zh-cn"
fc-match "serif:lang=zh-cn"

# 查看字体包含的语言支持
fc-list :lang=zh-cn
fc-list :lang=ja

解决方法

# configuration.nix
fonts.fontconfig = {
  enable = true;
  defaultFonts = {
    sansSerif = [
      "Source Han Sans SC"    # 简体中文优先
      "Source Han Sans TC"    # 繁体中文备选
      "Source Sans 3"         # 西文备选
    ];
    serif = [
      "Source Han Serif SC"   # 简体中文优先
      "Source Han Serif TC"   # 繁体中文备选
      "Source Serif 4"        # 西文备选
    ];
  };
};

2.1.4 字体调试与优化工具

字体信息查询

# 列出所有字体
fc-list

# 按语言过滤字体
fc-list :lang=zh-cn
fc-list :lang=en

# 查看字体详细信息
fc-list -v "Source Han Sans SC"
fc-list -v "LXGW WenKai Screen"

# 测试字体匹配
fc-match -v "sans-serif:lang=zh-cn"
fc-match -v "serif:lang=zh-cn"
fc-match -v "monospace:lang=zh-cn"

字体渲染测试

# 临时安装字体测试工具
nix shell nixpkgs#pango

# 创建测试文本文件
echo "中文测试 Chinese Test 123" > test.txt

# 使用不同字体渲染测试
pango-view --font="Source Han Sans SC 12" test.txt
pango-view --font="LXGW WenKai Screen 12" test.txt
pango-view --font="Maple Mono NF CN 12" test.txt

2.2 中文输入法

现代 Linux 桌面主要使用 fcitx5 作为中文输入解决方案,它通过插件系统支持多种输入引擎,并与图形环境深度集成。

2.2.1 输入法框架架构

核心组件

  • fcitx5-daemon:主守护进程,管理输入法状态
  • 输入引擎:拼音、五笔、仓颉等具体输入法实现
  • 图形前端:负责候选词界面显示
  • 配置工具:fcitx5-configtool 提供图形化配置

配置文件路径

  • ~/.config/fcitx5/config:主配置文件
  • ~/.config/fcitx5/profile:输入法引擎配置
  • ~/.config/fcitx5/conf/:各输入法引擎的详细配置

2.2.2 Wayland 原生输入法流程

Wayland text-input 协议流程

  1. 按键捕获:键盘事件首先到达 Wayland 合成器
  2. 协议通信:合成器通过 text-input 协议与客户端应用通信
  3. 输入法服务:fcitx5 作为 Wayland 输入法服务接收事件
  4. 候选生成:fcitx5 处理按键并生成候选词
  5. 候选显示:通过 Wayland 协议在光标位置显示候选窗口
  6. 文本提交:用户选择后通过 text-input 协议提交最终文本

text-input 协议有 v1 跟 v3 两个版本,目前(2025-09)Electron/Chrome 以及其他大部分程序框架都已经支持了 text-input-v3. 桌面环境方面所有主流 Compositor 也都支持 text-input-v3. 所以目前 wayland 下输入法的可用性已经很高了。

2.2.3 X11 / XWayland 输入法流程

XWayland 使用场景

  • 尚未支持 Wayland 的旧版应用
  • 需要特定 X11 功能的专业软件
  • 通过应用启动脚本单独设置环境变量

XWayland 应用输入流程

  1. 按键捕获:键盘事件首先进入 Wayland 合成器(Hyprland、KWin 等)。
  2. 事件转发给 XWayland(例如xwayland-satellite
    • 如果目标是 X11 应用窗口,合成器会将事件交给 XWayland
    • XWayland 将 Wayland 输入事件转换为 X11 协议事件(如 KeyPress/KeyRelease),并交付给目标应用。
  3. 应用侧的输入法模块拦截事件
    • X11 应用(GTK/Qt 程序)内部加载了 fcitx5-gtk / fcitx5-qt 插件(通常根据环境变量加载,后面会介绍这些环境变量)。
    • 这些插件拦截来自 XWayland 的键盘事件,并通过 D-Bus 将事件上报给 fcitx5
    • 此时应用相当于是「把键盘输入交给 fcitx5 代管」。
  4. fcitx5 处理输入逻辑
    • fcitx5 收到键盘序列后,进入输入法逻辑:拼音解析、候选词生成。
    • fcitx5 控制候选窗口的显示位置(通常跟随输入光标),候选窗口本身可能是 X11 窗口(由 fcitx5 自己创建,并通过 XWayland 显示)。
  5. 输入结果返回应用
    • 当用户选定候选词后,fcitx5 通过 D-Bus 调用 IM 插件接口直接把确认后的字符串传给应用。
    • 应用的 IM 插件收到字符串后,调用应用内的「输入上下文 API」插入文本。
    • 在应用看来,它就像直接得到了「输入了一串中文」的事件。

XWayland 环境变量设置

# GTK 应用使用 fcitx(通过 GTK IM 模块)
export GTK_IM_MODULE=fcitx

# Qt 应用使用 fcitx(通过 Qt IM 模块)
export QT_IM_MODULE=fcitx

# X11 应用使用 fcitx(通过 XIM 协议)
export XMODIFIERS=@im=fcitx

输入法机制说明

GTK IM 模块、Qt IM 模块以及 XIM 协议,都是 X11 下的东西,在 wayland 下只需要 text-input 协议即可,不需要这些幺蛾子。

2.2.4 混合环境管理策略

推荐配置策略

  1. 默认 Wayland 优先

    • 让现代应用使用原生 Wayland text-input 协议
  2. 按需 XWayland

    • 使用 GDK_BACKEND=x11 强制特定应用使用 XWayland
    • 为特定应用创建启动脚本设置 IM_MODULE 相关环境变量
  3. 应用启动脚本示例

#!/bin/bash
# 强制特定应用使用 XWayland
export GTK_IM_MODULE=fcitx  # 使用 GTK IM 模块
export QT_IM_MODULE=fcitx   # 使用 Qt IM 模块
export GDK_BACKEND=x11      # 强制使用 X11 后端
your-application

2.2.5 故障排查与优化

输入法无响应问题

  1. 进程状态检查

    ps aux | grep fcitx5
    systemctl --user status fcitx5
  2. 环境变量验证(仅 xwayland 场景):

    echo $GTK_IM_MODULE $QT_IM_MODULE $XMODIFIERS
    echo $XDG_RUNTIME_DIR $DBUS_SESSION_BUS_ADDRESS
  3. D-Bus 通信检查

    busctl --user tree org.fcitx.Fcitx5
    dbus-monitor --session "interface='org.fcitx.Fcitx5'"
  4. 诊断工具使用

    fcitx5-diagnose
    fcitx5-configtool

候选框显示问题

  1. Wayland 原生应用排查

    # 检查 Wayland 环境
    echo $WAYLAND_DISPLAY $XDG_RUNTIME_DIR
    
    # 检查 text-input 协议支持
    wayland-info | grep text-input
    
    # 查看合成器日志中 text-input 相关错误
    journalctl --user -u fcitx5
  2. XWayland 应用排查

    # 检查 XWayland 环境变量
    echo $GTK_IM_MODULE $QT_IM_MODULE $XMODIFIERS
    
    # 检查 XWayland 连接
    echo $DISPLAY
    
    # 验证 XIM 连接
    xdpyinfo | grep -i input
  3. 权限和会话检查

    # 确认 fcitx5 在正确的用户会话中运行
    loginctl show-session $(loginctl | grep $USER | awk '{print $1}')
    
    # 检查 D-Bus 会话
    echo $DBUS_SESSION_BUS_ADDRESS
  4. 应用兼容性

    • Wayland 应用:部分应用需要重新启动才能识别输入法
    • XWayland 应用:需要正确设置 XMODIFIERS 环境变量
    • 混合环境:某些应用可能在不同环境下表现不同

性能优化

# 调整 fcitx5 配置
vim ~/.config/fcitx5/profile

# 禁用不需要的输入引擎
# 减少候选词数量提高响应速度

# 云拼音配置
vim ~/.config/fcitx5/conf/cloudpinyin.conf

特殊场景处理

  1. 多显示器环境

    • Wayland:候选框通常能正确跟随光标位置
    • XWayland:候选框可能在错误屏幕显示,需要调整 X11 配置
  2. 高分屏适配

    • Wayland:自动适配系统缩放比例
    • XWayland:可能需要手动设置 GDK_SCALEQT_SCALE_FACTOR
  3. 游戏和全屏应用

    • Wayland:部分游戏可能需要 gamescope 等工具
    • XWayland:传统全屏游戏通常工作正常
  4. 终端应用

    • Wayland 终端:需要终端模拟器支持 text-input 协议
    • XWayland 终端:使用 X11 的 XIM 协议或 GTK/Qt IM 模块

总结

本文详细介绍了 Linux 桌面系统的多媒体处理能力,重点阐述了 PipeWire 如何统一管理音频和视频,以及 fontconfig 和 fcitx5 如何提供完善的中文支持。

PipeWire 的革命性意义

PipeWire 支持视频流处理,本质是为了解决 Linux 多媒体生态中长期存在的"音频-视频割裂"“传统协议适配困难"“沙盒权限复杂"等问题。相比传统方法,它通过统一管道模型、原生适配现代桌面、简化权限管理、整合硬件加速、动态路由等特性,让视频流的捕获、传输、处理和协作变得更高效、更安全、更易用。

如今,PipeWire 已成为 Linux 桌面视频处理的事实标准(如 GNOME 45+、KDE Plasma 6 均默认依赖),未来还将进一步整合 AI 处理(如实时美颜、降噪)等新功能,成为连接硬件、应用与用户的"多媒体中枢”。

中文支持的重要性

中文支持方面,虽然配置稍微复杂一些,但一旦搞定就基本不用再操心了。fontconfig 的字体匹配机制和 fcitx5 的输入法框架为中文用户提供了完整的桌面体验。

下一篇文章我们会聊聊网络架构,看看系统是如何处理网络连接和管理的。


Linux 桌面系统故障排查指南(三) - 桌面会话与图形渲染

2025年10月19日 10:19

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


前言

Systemd 及各项系统服务启动后会进入登录页面,从这一刻开始的 Linux 桌面使用过程涉及会话管理、窗口合成、图形渲染和输入处理等多个组件。

本文将探讨 Linux 桌面系统的图形架构,从用户登录到应用渲染的完整流程,包括 Wayland 和 X11 的区别,图形驱动的工作原理,以及如何诊断和解决各种图形问题。


用户会话:从登录到桌面

用户从登录到进入桌面环境的过程涉及多个组件的协调:display manager 负责认证,systemd-logind 管理会话,window compositor 提供图形环境。这个阶段的故障往往表现为登录失败、权限错误或图形界面异常。

1.1 登录流程

典型的图形登录流程:

  1. 显示管理器启动:greetd / GDM 等显示管理器显示登录界面
  2. 用户认证:通过 PAM 验证用户名 / 密码
  3. 会话创建:Display Manager 请求 logind 创建 session
  4. 用户服务启动:systemd 用户实例启动,运行用户配置的服务
  5. 合成器启动:获得环境变量和设备访问权限

关键观察点

# 查看显示管理器日志
journalctl -u greetd
journalctl -b _COMM=greetd
# 检查会话状态
loginctl list-sessions
loginctl show-session <id> --property=Name,UID,State
# 查看用户服务日志
journalctl --user -b

故障排查示例:用户登录后合成器未启动

  1. 检查用户服务日志:journalctl --user -u hyprland.service
  2. 验证会话状态:loginctl show-session <id> -p Active -p State
  3. 查看 PAM 认证日志:journalctl -t login

1.2 会话管理

systemd-logind 是连接登录、会话、设备权限和电源管理的核心服务。它通过 D-Bus 暴露 API,管理用户会话并分配设备 ACL。

核心职责

  • 会话管理:创建和维护用户会话,映射 session -> UID -> TTY / seat
  • 设备访问:基于 udev 标签分配设备 ACL 给当前会话
  • 电源管理:处理电源键事件,根据策略触发 suspend / shutdown
  • 多座席支持:支持 seat 概念,管理多用户场景

seat(座席)概念

https://www.freedesktop.org/wiki/Software/systemd/multiseat/

  • seat(座席)是 systemd/logind 引入的术语,用来表示「一组物理设备的集合」(例如一个显示器 + 一套键盘和鼠标 + 音频设备),以及与之关联的会话。
  • 所有设备默认都会被分配给 seat0, 想再搞一个 seat1 实现多人图形化登录,必须通过 udev 规则完成如下操作:
    1. 必须拥有第二张显卡,这是硬性的前提!为了让 seat1 实际可用,还必须拥有第二套键鼠与声卡。
    2. 给第二块显卡写 udev 规则,打上 TAG+="master-of-seat" 并设置ENV{ID_SEAT}="seat1"
    3. 把第二套键盘、鼠标、声卡等设备也写规则改成 ENV{ID_SEAT}="seat1"
    4. 重启系统。
  • logind 会把 VT/图形会话绑定到具体 seat,从而按 seat 粒度做电源管理、设备访问控制、空闲检测等策略。
  • 远程 SSH 登录不生成也不归属任何 seat;logind 仅为其建立会话对象,seat 字段留空。因此 seat 概念对 SSH 完全透明。

注意:虽然 SSH 会话不归属任何 seat,但这不影响大多数设备的访问。设备权限管理有两套并行的机制:传统的 Unix 权限模型(基于用户组,如 videoaudioinput 等)和现代的 systemd-logind ACL 机制(基于 seat 和会话)。SSH 会话主要依赖前者,因此只要用户具有相应的设备权限,仍可正常访问 GPU、声卡、存储设备等硬件资源。seat 机制主要影响的是需要图形界面交互的设备(如显示器、键盘鼠标)的访问控制。

现代 Linux 桌面系统基本都是单用户使用,因此后续讨论默认聚焦单 seat 场景。

常用命令

# 会话管理
loginctl list-sessions                    # 列出所有会话
loginctl show-session <id> -p Name -p UID -p Seat  # 会话详情
loginctl terminate-session <id>           # 终止会话
# seat 管理
loginctl seat-status                      # 查看 seat 状态
loginctl seat-status seat0                # 特定 seat 详情
# D-Bus 接口调试
busctl --system call org.freedesktop.login1 \
  /org/freedesktop/login1 org.freedesktop.login1.Manager \
  ListSessions

设备权限问题排查

Wayland compositor 启动但无法打开 /dev/dri/card0(GPU 权限问题)

排查:

  1. 确认 ls -l /dev/dri/card0 的 owner/group。通常应为 root:video,并且当前会话应被授予设备 ACL。
  2. loginctl seat-status seat0 查看是否列出 /dev/dri/card0 并显示 ACL 给当前 session。
  3. 若无,通过 udevadm info /dev/dri/card0 检查 udev 是否为 GPU 设备打上了TAG+="uaccess"TAG+="seat"
  4. 查看 journalctl -u systemd-logind,看是否在用户登录时有关于设备分配的错误。
  5. 若服务是以 system user 的方式启动,确保 compositor 的进程是在用户 session 下,而不是 systemd 服务或 root 启动的进程(起进程身份不同会导致权限问题)。
意外挂起/关机(电源键/睡眠按钮不按用户设置工作)
  • 检查 logind.conf(NixOS 对应位置请用 NixOS config 来覆写)中 HandlePowerKey,HandleLidSwitch 的配置。
  • journalctl -u systemd-logind 查看触发事件时间点;通常按键会以 D-Bus 事件或 ACPI 事件入日志。
  • 若某桌面环境或应用拦截了按键,会阻止 logind 行为。可以通过 busctl monitor 监听org.freedesktop.login1 的消息,看是否收到请求。
  • 若需要监控 logind 在登录/登出时做了什么,可以用busctl monitor --system org.freedesktop.login1 或:
    sudo dbus-monitor --system "interface='org.freedesktop.login1.Manager'"
    这能观察到 session 创建、移除、seat 分配、锁屏请求等信号。

Linux 图形系统基础概念

在深入讨论桌面会话和图形渲染之前,需要先理解 Linux 图形系统的基础组件和概念。

2.1 TTY 与 VT(Virtual Terminal)

TTY(Teletype) 是 Linux 系统中终端设备的抽象概念,源于早期计算机的终端设备。在现代 Linux 系统中:

  • 物理 TTY:通过串口连接的终端设备(多用于嵌入式或服务器调试)。

  • 虚拟 TTY:通过键盘和显示器模拟的终端,通常有 63 个(tty1-tty63)。在许多经典发行版中,tty1-tty6 默认为文本 VT,图形会话(如 X11 或 Wayland 合成器)通常在 tty7 或 tty1/tty2 启动。

  • 伪 TTY(PTY):用于网络连接(如 SSH)或终端模拟器(如 GNOME Terminal)的虚拟终端。

    VT(Virtual Terminal) 是内核中的虚拟终端子系统(drivers/tty/vt/),负责管理多个虚拟终端:

    • 每个 VT 对应一个 struct vc_data 结构体。
    • 维护字符矩阵、光标位置、字体等信息。
    • 支持两种显示模式:KD_TEXT(文本模式)和 KD_GRAPHICS(图形模式)。
    • 只有前台 VT 接收键盘输入。

2.2 内核显示模式切换

Linux 内核 VT 子系统支持两种显示模式,通过 KDSETMODE ioctl 进行切换:

KD_TEXT 模式(默认):

  • 内核 VT 子系统负责字符到像素的转换和刷新。
  • 使用 fbcon(framebuffer console) 将字符矩阵渲染到显存。
  • 支持光标闪烁、滚动、字体切换等文本终端功能。
  • 典型的黑底白字文本界面。

KD_GRAPHICS 模式

  • 内核停止字符刷新,fbcon 不再更新显存。
  • 用户空间进程(如图形服务器)获得显存控制权,直接进行像素级操作。
  • 图形界面(X11、Wayland)的基础模式。

fbcon(framebuffer console) 是内核中的帧缓冲控制台驱动,负责在 KD_TEXT 模式下将字符矩阵渲染到显存:

  • 将字符矩阵转换为像素数据
  • 管理字体渲染、光标显示、屏幕滚动
  • 在 KD_TEXT 模式下持续刷新显存
  • 在 KD_GRAPHICS 模式下停止工作

fbcon 基于 fbdev(framebuffer device) 框架工作,通过 /dev/fb0 等设备文件访问显存。fbcon 可以在不安装专用显卡驱动(如 NVIDIA/AMD 驱动)时工作,这是因为它依赖于显卡固件提供的标准化接口

  1. VESA BIOS Extensions (VBE):在传统 BIOS 系统上,内核的 vesafb 驱动通过 VBE 接口 (由显卡 BIOS 实现)请求一个标准的显示模式(如 1024x768),并获取一个指向显存的「线性帧缓冲区」(LFB)地址。
  2. UEFI Graphics Output Protocol (GOP):在现代 UEFI 系统上,内核的 efifb 驱动通过 GOP 接口实现相同的功能。

关键点在于: 无论是 VBE 还是 GOP,它们都只提供最基本的功能——设置模式并返回一块内存(帧缓冲区)地址。fbcon 驱动(运行在 CPU 上)负责向这块内存中写入像素数据来显示文本。这种方式非常可靠(因为它是固件标准,总能工作),但不提供任何硬件加速。这就是为什么文本界面 (KD_TEXT)总是能显示,而图形界面(KD_GRAPHICS)则必须加载专用的 DRM/KMS 驱动,以利用 GPU 的 2D/3D 加速、高级显示设置和电源管理功能。

2.3 输入设备处理

evdev 是 Linux 输入子系统的事件接口:

  • 提供统一的输入事件格式。
  • 支持键盘、鼠标、触摸板等设备。
  • 通过 /dev/input/event* 设备文件访问。
  • 注意:在 KD_TEXT 模式下,键盘输入由内核 VT 子系统直接处理,绕过了 evdev;只有在 KD_GRAPHICS 模式下,图形服务器才会接管 evdev 设备。

libinput 是用户空间的输入处理库:

  • 提供设备枚举和事件回调。
  • 处理手势识别、边缘滚动、指针加速等高级功能。
  • 被 X11(通过 xf86-input-libinput 驱动)和 Wayland 合成器(原生)广泛使用。
  • 图形界面专用:需要 evdev 支持,因此只在图形模式下工作。

图形驱动与渲染栈

现代 Linux 桌面系统的图形渲染涉及多个层次的组件,从底层的硬件驱动到高层的图形 API,各层协同工作实现高效的图形渲染。

3.1 图形栈架构

架构层次

  • 硬件层:GPU 和显示设备
  • 驱动层:Mesa 图形驱动和内核 DRM
  • 系统层:Wayland 协议和合成器 / X Server
  • 工具包层:GTK、Qt 等图形界面库
  • 应用层:具体的桌面应用程序

核心组件

  • DRM(Direct Rendering Manager):内核中的图形驱动框架,是现代 Linux 图形栈的基石。它将 GPU 硬件抽象为 /dev/dri/card0 等设备文件,并提供两大核心功能:
    • KMS(Kernel Mode Setting):Linux 内核中专门负责控制显卡输出、设置显示器分辨率和刷新率等模式(Modesetting)的子系统。主要特点:
      • 内核级控制:由内核直接管理显示模式,避免用户空间程序直接操作硬件
      • 无闪烁启动:系统启动时直接设置到显示器原生分辨率,避免分辨率切换时的闪烁
      • 热插拔支持:可以动态检测和配置新连接的显示器
      • 多显示器支持:支持多显示器配置和扩展桌面
      • 稳定切换:VT 切换(Ctrl+Alt+F1 等)瞬时且稳定
      • 权限安全:用户空间程序无需 root 权限即可请求显示模式切换
    • GEM(Graphics Execution Manager):图形执行管理器。DRM 提供的缓冲区管理框架,负责分配和管理 GPU 显存,并控制 2D/3D 引擎的执行。
  • DRM-Master:设备主控权限。这是内核 DRM 提供的一种独占锁,用于仲裁哪个进程有权请求 KMS 操作(即设置显示模式)。systemd-logind 会将这个权限授予「活动」的图形会话(如 Wayland合成器或 X Server),确保同一时间只有一个「主宰者」能控制屏幕输出。
  • Mesa:用户空间的 3D 图形驱动库,提供了 OpenGL 和 Vulkan 等图形 API 的开源实现。
  • EGL:Khronos 组织定义的接口,是 Mesa 和 Wayland(或 X11)之间的「胶水」,负责将 OpenGL/Vulkan 渲染 API 与本地窗口系统连接起来。
  • GBM(Generic Buffer Manager):Mesa 提供的一个 API,允许合成器(Compositor)通过 DRM/KMS 框架,以「非 EGL」的方式直接分配和管理图形缓冲区(Buffers)。
  • libdrm:一个用户空间库,封装了与内核 DRM 驱动进行 ioctl 通信的复杂细节,简化了 Mesa 和合成器对 DRM/KMS/GEM 的调用。

3.2 渲染管线

完整渲染流程

  1. 应用创建渲染上下文
    • 应用(如 Firefox)调用 OpenGL/Vulkan API 创建渲染上下文。
    • EGL 负责将图形 API 与 Wayland 窗口系统连接。
    • Mesa 驱动加载并初始化 GPU 上下文。
  2. GPU 渲染执行
    • 应用调用 API 绘制界面内容(如网页)。
    • Mesa 将 API 调用转换为 GPU 指令。
    • GPU 执行渲染,将结果写入一个图形缓冲区(Buffer)
  3. 缓冲区管理
    • GBM 负责为应用分配这个缓冲区。
    • 应用将渲染完成的缓冲区(通过 Wayland 协议)提交给合成器(Compositor)
  4. 合成与展示
    • 合成器收集所有应用的缓冲区(如 Firefox 的、终端的、输入法的)。
    • 合成器将这些缓冲区组合成一个最终帧。
    • 合成器通过DRM/KMS接口,请求内核将这个最终帧显示到屏幕上。

Wayland 图形架构

Wayland 是现代 Linux 桌面系统的图形协议,采用客户端-服务器模型。合成器同时扮演显示服务器和窗口管理器的角色,直接与内核的 DRM/KMS 和输入设备交互。

4.1 架构对比:X11 vs Wayland

  • X11(传统):在 X11 架构中,X Server(例如 Xorg)是显示服务器,直接与显卡驱动和输入设备交互; 窗口管理器 / 桌面环境(例如 i3、GNOME)则作为 X client 连接到 X Server,负责窗口摆放、装饰以及用户界面。使用 startx(实际上调用 xinit)启动图形会话时,本质流程是:先启动 X Server,再在其中运行窗口管理器或桌面环境(如exec i3)。Display Manager(如 GDM、SDDM)在图形登录时会自动启动 X Server,并完成用户认证、设置 DISPLAY 等环境变量,然后再运行会话。
  • Wayland(现代)Wayland 合成器本身既是显示服务器,又是窗口管理器。它直接通过内核的 DRM/KMS 控制显示模式,通过 evdev/libinput 采集并分发输入事件。Wayland 客户端应用通过 Wayland socket(通常位于 $XDG_RUNTIME_DIR/wayland-0,但具体名字可变)与合成器通信。因为合成器本身直接控制显示和输入设备,所以它可以直接从一个已登录的 TTY 启动,作为该 TTY 的图形会话的「display server」,无需先用 startx 启动一个独立的 X Server。如果使用 Display Manager 登录 Wayland 会话,则由 DM 在合适的 TTY 启动合成器并准备_会话_环境。

TTY 到图形界面的切换机制

当从 TTY 启动 Wayland 合成器时,涉及以下关键步骤:

  1. 设备权限获取:合成器通过 systemd-logind 获得 seat 和 GPU 的 DRM-Master 权限。
  2. 显示模式切换:调用 KDSETMODE ioctl 将 VT 从 KD_TEXT 切换到 KD_GRAPHICS,内核停止 fbcon 刷新。
  3. 输入设备接管:打开 /dev/input/event* 并执行 EVIOCGRAB,或通过 logind 的TakeControl() 获得输入控制权。完成后,合成器通过 libdrm/EGL/GBM 直接渲染到 framebuffer,通常首帧显示黑屏和鼠标指针。

退出/切换 VT(Ctrl+Alt+F⟂)时:

  • 释放 DRM-Master:drmDropMaster()
  • 恢复文本模式:KDSETMODE 切回 KD_TEXT
  • 释放输入控制:关闭 evdev fd,logind 收回设备控制权

fbcon 重新开始刷新,文本界面恢复显示。若合成器异常退出,logind 的 PauseDevice() 会收回 DRM-Master,系统可恢复文本模式。

架构差异带来的实际影响

  • 安全与权限:Wayland 把合成器放在更核心的位置(它有直接设备访问),因此确保合成器运行在正确会话(由 logind 管理)下至关重要。错误地以 root 或 system service 启动合成器会导致权限/ACL 不一致(compositor 无法访问设备或安全级别问题)。
  • 简化流程:Wayland 把多个角色合并到合成器进程,消除了 X11 时代客户端/窗口管理器与服务器的分离复杂度,令直接从 tty 启动合成器成为可行且常见的做法。
  • 兼容性:Xwayland 提供对 legacy X11 应用的兼容,合成器负责在启动时/按需启动 Xwayland 以支持老应用。

4.2 Wayland 协议与通信

客户端-服务器架构

  • 客户端-服务器模型:应用作为客户端,合成器作为服务器。
  • Unix 域套接字:通过 $XDG_RUNTIME_DIR/wayland-0 进行通信。
  • 协议扩展:支持 xdg-shell、text-input 等扩展协议。
  • 安全隔离:应用只能访问自己的窗口和输入事件。

核心协议

  • wayland-core:基础协议,定义 surface、buffer 等核心对象。
  • xdg-shell:窗口管理协议,定义窗口、对话框等。
  • wl_seat:输入设备协议,处理键盘、鼠标、触摸板。
  • wl_output:显示输出协议,管理显示器配置。

4.3 合成器架构

输入处理组件

  • libinput:从 /dev/input/* 读取事件并做预处理(手势识别、触摸板边缘、键盘元键处理等)。
  • 合成器使用 libinput 的 API 进行设备枚举与事件回调。

设备访问

  • 合成器通过 /dev/dri/card0 与内核 DRM 交互。
  • 通过 /dev/input/event* 访问输入设备。
  • 通过 PipeWire 处理音频、视频和屏幕共享(详见后续多媒体章节)。

4.4 xdg-desktop-portal:Wayland 桌面访问控制

XDG Desktop Portal 是一套用于在 Linux 桌面环境下提供统一安全接口的框架,最初为 Flatpak 等沙盒应用访问沙箱外部资源而设计。它通过 D-Bus 暴露一系列「门户(Portal)」接口,让沙箱化或受限应用能够安全地请求文件选择、截图、屏幕共享、打开 URI 等操作。

在 Wayland 环境下,每个应用程序只能访问自己的窗口、键盘鼠标事件等等,无法随意截屏或访问全局资源。在 Wayland 发展过程中,早期各 DE 与 WM 各自为战,实现了许多私有协议去完成这些工作,碎片化严重、客户端程序兼容困难。之后社区逐渐形成了使用 XDG Desktop Portal 作为桌面访问控制框架的共识,如今几乎所有的 DE/WM 与客户端应用都广泛采用了这一框架,它已成为 Wayland 中资源访问控制的事实标准。

如今绝大部分应用在 X11 环境下仍然会使用 X11 原生接口(如 XShm、XRecord、XSelectInput 等) 实现屏幕共享、文件选择、打开 URI 等功能,而在 Wayland 下则必须使用 xdg-desktop-portal.

NOTE: 许多命令行截图/录屏工具(如wl-screenrec,wf-recorder)选择了使用 wlr-screencopy-unstable-v1 / ext-image-copy-capture-v1 等 Wayland 原生的协议来实现截图功能,这些工具完全绕过了 XDG Desktop Portal, 通常只在 wlroots-based compositors 上能正常使用,Gnome/KDE 目前都要求走 Portal 接口、不支持此类协议。

核心门户服务

https://flatpak.github.io/xdg-desktop-portal/docs/api-reference.html

文件操作

  • 文件选择器org.freedesktop.portal.FileChooser 统一的文件选择对话框
  • 文件传输org.freedesktop.portal.FileTransfer 通过拖拽或复制粘贴等方式在 Apps 之间传输文件

屏幕与媒体访问

  • 截屏org.freedesktop.portal.Screenshot 安全截屏功能
  • 录屏org.freedesktop.portal.ScreenCast 屏幕录制和窗口共享,视频会议应用的核心依赖
  • 摄像头org.freedesktop.portal.Camera 摄像头访问控制

系统访问

  • 打印机org.freedesktop.portal.Print 统一的打印接口
  • 通知org.freedesktop.portal.Notification 跨桌面环境的通知发送
  • 位置服务org.freedesktop.portal.Location 地理位置信息访问

账户与权限

  • 账户信息org.freedesktop.portal.Account 获取用户基本信息
  • 密码管理org.freedesktop.portal.Secret 与系统密钥环集成
  • 设备授权org.freedesktop.portal.Usb USB 设备等外设访问控制

门户实现:不同桌面环境的适配器

xdg-desktop-portal 是框架本身,具体的功能实现由各个桌面环境提供:

  • xdg-desktop-portal-gtk:实现了 Portal 最基础的功能,是 Niri/Hyprland 等大部分 Compositors 的默认 Portal.
  • xdg-desktop-portal-wlr:wlroots 的通用 portal 组件,实现了通用的屏幕共享与截图两项功能,所有基于 wlroots 的 Compositors 都可使用它。
  • xdg-desktop-portal-gnome:被 Niri 等部分 Compositor 用于实现屏幕共享与截图功能。
  • gnome-keyring: 实现了 Portal 的密码管理 API, Niri/Hyprland 等 Compositor 都使用它作为密码管理组件。

与多媒体和 PipeWire 的关系

在后续的多媒体章节中会详细介绍,PipeWire 的屏幕共享功能完全依赖 xdg-desktop-portal

  • 屏幕捕获流程:视频会议软件 → PipeWire → xdg-desktop-portal → 用户授权 → 合成器提供屏幕内容
  • 权限管理:用户可以精细控制哪些应用可以访问屏幕,以及访问的范围
  • 安全保证:即使应用获得了屏幕访问权限,也只能在用户授权的范围内工作

这种设计解决了 Wayland 隔离原则与实际功能需求的矛盾,是典型的"安全与便利的平衡"方案。

工作原理与通信流程

典型交互流程

  1. 应用发起请求:应用通过 D-Bus 调用相应的门户接口
  2. 门户服务转发:xdg-desktop-portal 根据配置将请求转发给对应的实现
  3. 用户界面显示:具体实现显示原生对话框,请求用户授权
  4. 权限授予:用户确认后,门户服务返回授权令牌或结果
  5. 资源访问:应用使用令牌通过受限接口访问资源

D-Bus 架构

# 查看已安装的门户实现
ls /usr/share/xdg-desktop-portal/portals/
# 或在 NixOS 上
ls /run/current-system/sw/share/xdg-desktop-portal/portals/

# 查看当前激活的门户
busctl --user list-units | grep portal

# 监控门户活动
busctl monitor --user org.freedesktop.portal.*

配置与故障排查

优先级配置

系统按优先级选择门户实现,优先级文件通常位于:

# 系统级配置
/etc/xdg-desktop-portal/*-portals.conf
# 用户级配置
~/.config/xdg-desktop-portal/*-portals.conf

常见问题排查

# 检查门户服务状态
systemctl --user status xdg-desktop-portal
systemctl --user status xdg-desktop-portal-gtk
systemctl --user status xdg-desktop-portal-gnome

# 查看门户日志
journalctl --user -u xdg-desktop-portal -f

# 测试门户功能
gdbus introspect --session --dest org.freedesktop.portal.Desktop \
  --object-path /org/freedesktop/portal/desktop

# 检查特定门户支持
gdbus call --session --dest org.freedesktop.portal.Desktop \
  --object-path /org/freedesktop/portal/desktop \
  --method org.freedesktop.portal.Request.Response

NixOS 配置示例

{
  # 启用 xdg-desktop-portal 服务
  xdg.portal = {
    enable = true;
    extraPortals = with pkgs; [
      xdg-desktop-portal-gtk  # GTK 门户
      xdg-desktop-portal-wlr  # Wayland 合成器门户
    ];
    xdgOpenUsePortal = true;  # 使用门户处理 xdg-open
  };
}

应用程序与工具包

GUI 应用程序是用户与 Linux 桌面交互的主要方式。在 Wayland 环境下,应用通过标准化的协议与合成器通信,实现窗口管理、输入处理和图形渲染。

5.1 应用启动流程

标准启动过程

  1. 环境准备
    • 设置 WAYLAND_DISPLAYXDG_RUNTIME_DIR
    • 加载图形工具包库(GTK/Qt)
    • 初始化 Wayland 连接
  2. 窗口创建
    • 创建 Wayland 表面
    • 设置窗口属性和装饰
    • 注册事件监听器
  3. 渲染初始化
    • 创建 EGL 上下文
    • 加载 Mesa 驱动
    • 配置图形缓冲区
  4. 内容绘制
    • 应用调用 OpenGL/Vulkan API 绘制界面内容
    • Mesa 将 API 调用转换为 GPU 指令
    • 在 GPU 上执行渲染,生成帧缓冲数据
    • 应用将渲染完成的缓冲区提交给合成器
  5. 合成与展示
    • 合成器接收缓冲区后进行最终合成和显示
    • 合成器将多个应用的缓冲区组合成最终帧
    • 通过 DRM/KMS 将最终帧提交到显示设备

调试启动问题

# 查看 Wayland 环境
echo $WAYLAND_DISPLAY $XDG_RUNTIME_DIR
# 检查应用日志
journalctl --user -u <application>.service
# Wayland 调试变量
export WAYLAND_DEBUG=1
export MESA_DEBUG=1
# 跟踪系统调用
strace -f -e trace=network,ipc <application>

5.2 工具包支持

GTK 应用

  • GTK3/4 原生支持 Wayland
  • 自动检测运行环境
  • 可通过 GDK_BACKEND 强制指定后端
# 强制使用 Wayland
GDK_BACKEND=wayland gtk-application
# 强制使用 X11(通过 Xwayland)
GDK_BACKEND=x11 gtk-application

Qt 应用

  • Qt5/6 支持 Wayland
  • 需要安装 Wayland 平台插件
  • 自动选择最佳后端
# 查看 Qt 平台插件(NixOS)
ls /run/current-system/sw/lib/qt*/plugins/platforms/
# 传统发行版
ls /usr/lib/qt*/plugins/platforms/
# Qt 调试信息
export QT_LOGGING_RULES="qt.qpa.*=true"

SDL 应用

  • SDL2 内置 Wayland 支持
  • 主要用于游戏和多媒体应用
  • 自动适配运行环境

图形栈调试与优化

6.1 图形驱动信息查询

首先,需要判断您当前所处的环境。在终端中运行 tty 命令:

  • 输出 /dev/pts/0 等:您在图形界面下的伪 TTY (pts) 中。
  • 输出 /dev/tty1 等:您在 Ctrl+Alt+F1 切换的虚拟 TTY (tty) 文本控制台中。

1. 判断图形会话 (pts) 驱动

在伪 TTY 中,您查询的是整个图形界面的内核 DRM 驱动

lspci -k | grep -A 3 -i vga

示例输出:

01:00.0 VGA compatible controller: NVIDIA Corporation GP107 [GeForce GTX 1050 Ti] (rev a1)
	Subsystem: ZOTAC International (MCO) Ltd. GP107 [GeForce GTX 1050 Ti]
	Kernel driver in use: nvidia
	Kernel modules: nvidiafb, nouveau, nvidia_drm, nvidia
  • Kernel driver in use: nvidia:表明 NVIDIA 专有驱动正在使用。
  • 常见的驱动有:i915 (Intel), amdgpu (AMD), nouveau (NVIDIA 开源), nvidia (NVIDIA 专有)。

2. 判断文本控制台 (tty) 驱动

在虚拟 TTY 中(或在 pts 中查询 TTY 的日志),您查询的是帧缓冲 驱动

dmesg | grep -i fbcon

常见的输出及含义:

  1. 现代 DRM 驱动 (最优情况):
    [   20.709925] fbcon: nvidia-drmdrmfb (fb0) is primary device
    [    1.512345] fbcon: i915drmfb (fb0) is primary device
    含义fbcon 已绑定到主内核图形驱动(nvidia-drmi915)提供的帧缓冲区 (drmfb)上。这表明 KMS 已正常启动,文本控制台将使用显示器原生分辨率,且 TTY 切换 (Ctrl+Alt+F...)会非常平滑。
  2. UEFI 固件驱动 (UEFI 回退情况):
    [    1.234567] fbcon: efifb (fb0) is primary device
    含义fbcon 正在使用 UEFI 固件提供的帧缓冲区(efifb)。这通常发生在内核的 DRM 驱动尚未加载或被 nomodeset 参数禁用时。
  3. 传统 VESA 驱动 (legacyBIOS 回退情况):
    [    1.345678] fbcon: vesafb (fb0) is primary device
    含义fbcon 正在使用 vesafb 驱动,通过 VBE 接口工作。

3. 其他驱动信息查询

# 查看 DRM 设备文件
ls -la /dev/dri/
# 查看 Mesa/OpenGL renderer 信息
glxinfo | grep "OpenGL renderer"
# 查看 Vulkan GPU 信息
vulkaninfo | grep "GPU id"

6.2 渲染器选择与参数优化

GTK 应用渲染器选择

# GTK 应用渲染器选择
export GSK_RENDERER=vulkan     # 使用 Vulkan 渲染
export GSK_RENDERER=opengl     # 使用 OpenGL 渲染
export GSK_RENDERER=cairo      # 使用软件渲染
  • GSK_RENDERER=vulkan:使用现代低级别图形 API Vulkan,提供更好的多线程支持和更低的 CPU 开销。性能最佳,支持现代 GPU 特性,适用于现代 GPU 和需要最佳性能的应用,但需要支持 Vulkan 的 GPU 驱动。
  • GSK_RENDERER=opengl:使用传统硬件加速渲染 OpenGL,兼容性好,性能稳定。支持广泛的硬件和驱动,适用于大多数现代 GPU 和需要稳定兼容性的应用,特点是单线程渲染,CPU 开销相对较高。
  • GSK_RENDERER=cairo:使用 CPU 软件渲染,不依赖 GPU 硬件加速。兼容性最好,不依赖 GPU 驱动,适用于 GPU 驱动问题时的备选方案,或对性能要求不高的应用,缺点是性能最低,CPU 占用高。

Qt 应用渲染器选择

# Qt 应用渲染器选择
export QT_OPENGL=desktop     # 使用桌面 OpenGL
export QT_OPENGL=software    # 使用软件渲染
export QT_OPENGL=angle       # 使用 ANGLE(Windows 兼容层)
  • QT_OPENGL=desktop:使用桌面版 OpenGL,支持完整的 OpenGL 功能集。功能完整,性能良好,适用于大多数桌面应用,需要完整 OpenGL 支持。
  • QT_OPENGL=software:使用 CPU 软件渲染,完全绕过 GPU。兼容性最好,不依赖 GPU,适用于 GPU 驱动问题,或需要确保兼容性的场景。
  • QT_OPENGL=angle:使用 ANGLE 将 OpenGL ES 转换为 DirectX,主要用于 Windows 兼容性。在某些 Windows 兼容层环境下性能更好,适用于 Wine 等 Windows 兼容层环境。

Mesa 驱动优化参数

# Mesa 驱动版本覆盖
export MESA_GL_VERSION_OVERRIDE=4.5
export MESA_GLSL_VERSION_OVERRIDE=450
# 调试信息
export MESA_DEBUG=1            # 启用 Mesa 调试信息
export LIBGL_DEBUG=verbose     # 启用 OpenGL 调试信息
  • MESA_GL_VERSION_OVERRIDE=4.5:强制使用指定版本的 OpenGL,解决某些应用的兼容性问题。覆盖应用请求的 OpenGL 版本,适用于应用要求过高 OpenGL 版本导致无法启动时。
  • MESA_GLSL_VERSION_OVERRIDE=450:强制使用指定版本的 GLSL 着色器语言,确保着色器兼容性。覆盖着色器编译器版本,避免版本不匹配问题,适用于着色器编译错误或版本不匹配时。
  • MESA_DEBUG=1:启用详细的 Mesa 调试信息,帮助诊断图形问题。
  • LIBGL_DEBUG=verbose:启用 OpenGL 库的详细调试输出,用于深入分析 OpenGL 调用问题。

6.3 调试 Wayland 通信

# 查看 Wayland 环境变量
echo $WAYLAND_DISPLAY $XDG_RUNTIME_DIR
# 启用 Wayland 调试输出(客户端)
export WAYLAND_DEBUG=1
# 检查合成器支持的协议
wayland-info | grep text-input
# 跟踪系统调用(查看 socket 通信)
strace -f -e trace=network,ipc <application>

故障排查

7.1 会话管理问题

登录失败排查

# 检查显示管理器状态
systemctl status display-manager
journalctl -u display-manager -b
# 查看用户会话
loginctl list-sessions
loginctl show-session <session_id>
# 检查 PAM 认证
journalctl -t login -f

权限问题排查

# 检查设备权限
loginctl seat-status seat0
ls -la /dev/dri/card0
# 查看 ACL 分配
getfacl /dev/dri/card0

7.2 图形渲染问题

应用崩溃诊断

  • 核心转储分析
    # 查看核心转储
    coredumpctl list
    coredumpctl info <pid>
    # 调试核心文件
    coredumpctl debug <pid>
  • GPU 问题诊断
    # 检查 GPU 重置
    dmesg | grep -i "gpu hang\|reset"
    # Mesa 调试信息
    export MESA_DEBUG=1
    export LIBGL_DEBUG=verbose
  • Wayland 协议错误
    # Wayland 调试输出
    export WAYLAND_DEBUG=1
    # 合成器日志
    journalctl --user -u <compositor> -f

性能问题分析

# GPU 使用率
nvidia-smi  # NVIDIA
radeontop   # AMD
# CPU 使用率分析
perf top -p <pid>
# 内存使用
smem -p | grep <application>
# 帧率监控
export __GL_SHOW_GRAPHICS_OSD=1  # NVIDIA

兼容性问题

  • Xwayland 问题:部分 X11 应用在 Xwayland 下运行异常
  • Wayland 协议缺失:某些功能需要特定的 Wayland 扩展
  • 驱动兼容性:GPU 驱动可能不完全支持某些 Wayland 特性

解决方法

  • 更新 Mesa 和 GPU 驱动
  • 检查合成器对必要 Wayland 扩展的支持
  • 对于顽固问题,可临时使用 X11 会话

总结

从用户登录到画面显示,这一整套流程确实挺复杂的,展开说那可能得好几本大部头了。

Wayland 虽然还在发展中,但确实比 X11 要现代化很多,性能和安全性的提升是实实在在的,而且在 2025 年的今天 Wayland 生态的可用性已经很不错了。

下一篇文章我们会聊聊多媒体和中文支持,看看系统是如何处理音频视频和中文显示的。


快速参考

常用会话管理命令

# 会话管理
loginctl list-sessions                    # 列出所有会话
loginctl show-session <id> -p Name -p UID -p Seat  # 会话详情
loginctl terminate-session <id>           # 终止会话
# seat 管理
loginctl seat-status                      # 查看 seat 状态
loginctl seat-status seat0                # 特定 seat 详情
# 设备权限检查
ls -la /dev/dri/card0                     # GPU 设备权限
ls -la /dev/input/event*                  # 输入设备权限

常用图形调试命令

# 图形驱动信息
glxinfo | grep "OpenGL renderer"          # OpenGL 信息
vulkaninfo | grep "GPU id"                # Vulkan 信息
lspci -k | grep -A 3 -i vga               # 显卡驱动
ls -la /dev/dri/                          # DRM 设备
# Wayland 环境
echo $WAYLAND_DISPLAY $XDG_RUNTIME_DIR    # 环境变量
wayland-info | grep text-input            # 协议支持
# 调试变量
export WAYLAND_DEBUG=1                    # Wayland 调试
export MESA_DEBUG=1                       # Mesa 调试
export GSK_RENDERER=vulkan                # GTK 渲染器
export QT_OPENGL=desktop                  # Qt 渲染器

重要配置文件位置

# 会话相关
/etc/systemd/logind.conf                  # logind 配置
~/.config/systemd/user/                   # 用户服务配置
# 图形相关
~/.config/wayland/                        # Wayland 配置
~/.config/gtk-3.0/                        # GTK 配置
~/.config/qt5ct/                          # Qt 配置
~/.config/mesa/                           # Mesa 配置
# 设备权限
/etc/udev/rules.d/                        # udev 规则
/dev/dri/                                 # GPU 设备
/dev/input/                               # 输入设备
# 显示管理器
/etc/gdm/                                 # GDM 配置
/etc/lightdm/                             # LightDM 配置
/etc/sddm.conf                            # SDDM 配置

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

2025年10月19日 10:18

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

概述

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

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

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

1. systemd 核心功能

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

1.1 systemd 概览与基本操作

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

核心功能

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

常用命令

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

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

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

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

配置文件路径

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

1.2 服务单元类型与配置

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

主要单元类型

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

服务单元配置示例

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

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

[Install]
WantedBy=multi-user.target

1.3 systemd 依赖关系与启动顺序

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

依赖关系类型

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

示例配置

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

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

[Install]
WantedBy=multi-user.target

2. systemd 生态系统服务

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

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

systemd 全家桶,你值得拥有(

2.1 日志系统:systemd-journald

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

2.1.1 核心特性

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

2.1.2 日志的4个收集入口

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

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

2.1.3 日志优先级与核心配置

1. 日志优先级简述

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

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

可用于筛选关键日志。

2. journald 配置

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

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

生产配置示例

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

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

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

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

首先写入日志:

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

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

然后查询日志:

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

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

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

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

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

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

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

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

2.1.6 日志写入最佳实践

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

2.1.7 运维命令速查

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

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

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

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

2.2 内存管理:systemd-oomd

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

工作原理

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

配置示例

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

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

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

监控与调试

# 查看 oomd 状态
systemctl status systemd-oomd

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

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

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

2.3 DNS 解析:systemd-resolved

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

主要功能

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

配置方法

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

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

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

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

使用命令

# DNS 状态查看
resolvectl status

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

# 缓存管理
resolvectl flush-caches
resolvectl statistics

# DNS 服务器状态
resolvectl dns

2.4 时间同步:systemd-timesyncd

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

功能特点

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

NixOS 配置

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

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

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

时间同步管理

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

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

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

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

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

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

3.1 udev 与 systemd-udevd

3.1.1 udev 设备管理框架

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

udev 的核心功能

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

udev 的工作原理

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

3.1.2 systemd-udevd 实现

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

systemd-udevd 的优势

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

systemd-udevd 服务管理

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

# 重启服务
sudo systemctl restart systemd-udevd

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

3.1.3 工作流程

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

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

3.1.4 配置示例

基本规则示例

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

规则说明

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

高级规则示例

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

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

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

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

3.2 设备权限与 ACL

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

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

检查设备权限分配

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

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

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

3.3 故障排查

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

排查步骤:

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

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

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

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

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

  6. 检查 systemd-udevd 的日志:

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

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

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

配置文件路径

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

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

D-Bus 是 Linux 系统中主流的进程间通信(IPC)机制,旨在解决不同进程(尤其是桌面应用、系统服务)间的高效、安全通信问题,广泛用于 GNOME、KDE 等桌面环境及系统服务管理(如 systemd)。它本质是 “消息总线”,通过中心化的 “总线守护进程” 实现多进程间的消息路由。名字虽然有点奇怪, 功能倒是挺实在的。

4.1 D-Bus 项目背景

D-Bus 并非 systemd 社区的项目,而是 freedesktop.org 的独立项目。D-Bus 在 systemd 出现之前就已经存在,是 Linux 桌面环境标准化进程间通信的重要基础设施。

D-Bus 与 systemd 的关系

  • 独立项目:D-Bus 由 freedesktop.org 维护,有自己的发布周期和开发团队
  • 深度集成:systemd 将 D-Bus 作为核心依赖,深度集成到其架构中
  • 服务管理:systemd 负责启动和管理 D-Bus 守护进程(dbus-daemon)
  • 统一接口:systemd 通过 D-Bus 提供统一的服务管理接口
    • systemd 本身就是一个 D-Bus 服务,我们在使用 systemctl 命令与 systemd 交互时,实际上就是 D-Bus 与 org.freedesktop.systemd1 通信。

4.2 关键概念

D-Bus 通过 「对象 - 接口」 模型(跟面向对象编程(OOP)中的概念有些类似)封装功能,以下结合systemd1logind1 的真实定义,对应核心概念:

概念 定义与作用 示例(systemd1/logind1)
总线(Bus) D-Bus 消息传输的基础通道,分系统 / 会话两大类 系统总线 /var/run/dbus/system_bus_socketsystemd1/logind1 唯一使用的总线)
服务名(Name) 服务端在总线上的 ID,通常每个应用程序一个 org.freedesktop.systemd1systemd 服务名)、org.freedesktop.login1logind 服务名)
对象(Object) 服务内部的功能组织单元,通过对象路径进行标识。每个对象可以代表不同的资源。 /org/freedesktop/systemd1systemd1 根对象)、/org/freedesktop/login1logind1 根对象)
接口(Interface) 每个接口定义了一组方法和信号 org.freedesktop.systemd1.Managersystemd1 核心接口)、org.freedesktop.login1.Managerlogind1 核心接口)
方法(Method) 方法是对象接口中定义的函数,可以被远程调用。方法属于某个接口,而接口由对象实现。(有请求有返回) systemd1StartUnit(启动系统单元,如 nginx.service)、logind1ListSessions(查询所有活跃用户会话)
信号(Signal) 对象发出的单向事件通知,支持多播(无返回值) systemd1UnitActiveChanged(单元状态变化,如 nginxinactive 变为 active)、logind1SessionNew(新用户登录创建会话)
属性(Property) 对象的 「状态数据」,支持读取 / 写入 / 变更通知 systemd1ActiveUnits(所有活跃系统单元列表)、logind1CanPowerOff(当前系统是否允许关机,布尔值)

可使用 busctl list 查看系统中的所有 D-Bus 对象:

# 所有 system bus 对象
› busctl --system list --no-pager | grep org.
org.blueman.Mechanism                     - -               -                (activatable) -                         -       -
org.bluez                              1421 bluetoothd      root             :1.6          bluetooth.service         -       -
org.bluez.mesh                            - -               -                (activatable) -                         -       -
org.freedesktop.Avahi                  1420 avahi-daemon    avahi            :1.7          avahi-daemon.service      -       -
org.freedesktop.DBus                      1 systemd         root             -             init.scope                -       -
org.freedesktop.Flatpak.SystemHelper      - -               -                (activatable) -                         -       -
org.freedesktop.GeoClue2                  - -               -                (activatable) -                         -       -
org.freedesktop.PolicyKit1             2216 polkitd         polkituser       :1.22         polkit.service            -       -
org.freedesktop.RealtimeKit1           2539 rtkit-daemon    root             :1.41         rtkit-daemon.service      -       -
org.freedesktop.UDisks2                2492 udisksd         root             :1.31         udisks2.service           -       -
org.freedesktop.home1                     - -               -                (activatable) -                         -       -
org.freedesktop.hostname1                 - -               -                (activatable) -                         -       -
org.freedesktop.import1                   - -               -                (activatable) -                         -       -
org.freedesktop.locale1                   - -               -                (activatable) -                         -       -
org.freedesktop.login1                 1504 systemd-logind  root             :1.8          systemd-logind.service    -       -
org.freedesktop.machine1                  - -               -                (activatable) -                         -       -
org.freedesktop.network1               1292 systemd-network systemd-network  :1.3          systemd-networkd.service  -       -
org.freedesktop.oom1                    934 systemd-oomd    systemd-oom      :1.1          systemd-oomd.service      -       -
org.freedesktop.portable1                 - -               -                (activatable) -                         -       -
org.freedesktop.resolve1               1293 systemd-resolve systemd-resolve  :1.0          systemd-resolved.service  -       -
org.freedesktop.systemd1                  1 systemd         root             :1.4          init.scope                -       -
org.freedesktop.sysupdate1                - -               -                (activatable) -                         -       -
org.freedesktop.timedate1                 - -               -                (activatable) -                         -       -
org.freedesktop.timesync1              1148 systemd-timesyn systemd-timesync :1.2          systemd-timesyncd.service -       -
org.opensuse.CupsPkHelper.Mechanism       - -               -                (activatable) -                         -       -

# 所有 session bus 对象
› busctl --user list --no-pager | grep org.
...
org.fcitx.Fcitx-0                                                                 76699 fcitx5          ryan :1.284        user@1000.service -       -
org.fcitx.Fcitx5                                                                  76699 fcitx5          ryan :1.282        user@1000.service -       -
org.freedesktop.DBus                                                               2127 systemd         ryan -             user@1000.service -       -
org.freedesktop.FileManager1                                                          - -               -    (activatable) -                 -       -
org.freedesktop.Notifications                                                      3539 .mako-wrapped   ryan :1.81         user@1000.service -       -
org.freedesktop.ReserveDevice1.Audio0                                              2542 wireplumber     ryan :1.50         user@1000.service -       -
org.freedesktop.ReserveDevice1.Audio1                                              2542 wireplumber     ryan :1.50         user@1000.service -       -
org.freedesktop.ScreenSaver                                                        2192 niri            ryan :1.9          user@1000.service -       -
org.freedesktop.a11y.Manager                                                       2192 niri            ryan :1.13         user@1000.service -       -
org.freedesktop.impl.portal.PermissionStore                                        2410 .xdg-permission ryan :1.28         user@1000.service -       -
org.freedesktop.impl.portal.Secret                                                    - -               -    (activatable) -                 -       -
org.freedesktop.impl.portal.desktop.gnome                                             - -               -    (activatable) -                 -       -
org.freedesktop.impl.portal.desktop.gtk                                            2475 .xdg-desktop-po ryan :1.33         user@1000.service -       -
org.freedesktop.portal.Desktop                                                     2350 .xdg-desktop-po ryan :1.26         user@1000.service -       -
org.freedesktop.portal.Documents                                                   2428 .xdg-document-p ryan :1.30         user@1000.service -       -
org.freedesktop.portal.Fcitx                                                      76699 fcitx5          ryan :1.283        user@1000.service -       -
org.freedesktop.portal.Flatpak                                                        - -               -    (activatable) -                 -       -
org.freedesktop.portal.IBus                                                       76699 fcitx5          ryan :1.285        user@1000.service -       -
org.freedesktop.secrets                                                            2161 .gnome-keyring- ryan :1.55         session-1.scope   1       -
org.freedesktop.systemd1                                                           2127 systemd         ryan :1.1          user@1000.service -       -
...

4.3 系统总线与会话总线

总线类型 作用场景 典型用途 运行用户
系统总线(System Bus) 系统级服务通信 systemd1 单元管理(启动 / 停止服务)、logind1 用户会话 / 电源控制(关机 / 重启) root(特权)
会话总线(Session Bus) 单个用户会话内的应用通信 桌面应用交互(如窗口切换、通知) 当前登录用户

4.4 D-Bus 的三类角色

  1. 总线守护进程(dbus-daemon)

    架构的 「中枢」,每个总线对应一个守护进程,核心职责:

    • 管理进程的连接(如验证 普通用户 是否有权调用 logind1PowerOff 方法);

    • 路由消息(将客户端请求的 「启动 nginx 服务」 转发给 systemd1);

    • 维护服务注册表(记录 org.freedesktop.login1logind 进程的映射关系)。

  2. 服务端(Service)

    提供功能的进程(如 systemd 进程、logind 进程),核心操作:

    • 向总线注册 「服务名」(systemd1 注册 org.freedesktop.systemd1logind1 注册org.freedesktop.login1,均为唯一标识);

    • 暴露 「对象」 和 「接口」(如 systemd1 暴露 /org/freedesktop/systemd1 对象与org.freedesktop.systemd1.Manager 接口),供客户端调用。

  3. 客户端(Client)

    调用服务的进程(如 systemctl 命令、桌面电源菜单),核心操作:

    • 连接系统总线后,通过服务名(如 org.freedesktop.login1)找到 logind 服务;

    • 调用服务端暴露的方法(如通过 logind1ListSessions 查询当前用户会话),或订阅信号(如监听 systemd1UnitActiveChanged 单元状态变化)。

4.5 常见操作示例

下面我们通过一些命令来演示 D-Bus 总线的用途:

# 模拟 `systemctl status dbus` 的功能
busctl --system --json=pretty call \
  org.freedesktop.systemd1 \
  /org/freedesktop/systemd1/unit/dbus_2eservice \
  org.freedesktop.DBus.Properties GetAll s org.freedesktop.systemd1.Unit

# 模拟 `systemctl stop sshd`
sudo gdbus call --system \
  --dest org.freedesktop.systemd1 \
  --object-path /org/freedesktop/systemd1 \
  --method org.freedesktop.systemd1.Manager.StopUnit \
  "sshd.service" "replace"

# 模拟 `systemctl start sshd`
sudo gdbus call --system \
  --dest org.freedesktop.systemd1 \
  --object-path /org/freedesktop/systemd1 \
  --method org.freedesktop.systemd1.Manager.StartUnit \
  "sshd.service" "replace"

# 模拟 `notify-send "The Summary" "Here’s the body of the notification"`
nix shell nixpkgs#glib
gdbus call --session \
    --dest org.freedesktop.Notifications \
    --object-path /org/freedesktop/Notifications \
    --method org.freedesktop.Notifications.Notify \
    my_app_name \
    42 \
    gtk-dialog-info \
    "The Summary" \
    "Here’s the body of the notification" \
    [] \
    {} \
    5000

# 获取当前时区
busctl get-property org.freedesktop.timedate1 /org/freedesktop/timedate1 \
    org.freedesktop.timedate1 Timezone

# 查询主机名
busctl get-property org.freedesktop.hostname1 /org/freedesktop/hostname1 \
    org.freedesktop.hostname1 Hostname

4.6 调试与监控命令

# 看 systemctl 与 systemd 的完整交互(method-call + signal)
sudo busctl monitor --system | grep 'org.freedesktop.systemd1'
# 或者使用 --match 过滤,但这需要提前知道 interface 的全名
sudo busctl monitor --match='interface=org.freedesktop.systemd1.Manager'

# 跟 busctl monitor 功能几乎完全一致,也可通过 match rule 过滤
sudo dbus-monitor --system "interface='org.freedesktop.systemd1.Manager'"

# gdbus 只监听 signals,只能用来调试「服务有没有正确发出 signal」
sudo gdbus monitor --system -d org.freedesktop.systemd1.Manager

4.7 D-Bus 的权限管控

4.7.1 D-Bus 的原生权限管控机制

D-Bus 本身具备多层权限管控能力,从总线接入、消息路由到敏感操作授权,形成了系统级的基础安全保障,核心机制包括:

  1. 总线配置文件(静态规则管控)

    通过 XML 配置文件定义细粒度访问规则,实现对 「谁能访问哪些服务 / 方法」 的静态限制。例如:

    • 系统总线的服务级规则(如 /etc/dbus-1/system.d/org.freedesktop.login1.conf)可限制普通用户调用 org.freedesktop.login1.Manager.PowerOff(关机方法);

    • 全局规则(如 /etc/dbus-1/system.conf)可限定仅 rootdbus 组用户访问org.freedesktop.systemd1(systemd 服务)的核心接口。

      规则遵循 「deny 优先级高于 allow、服务级规则高于全局规则」 的逻辑,从总线层面直接拦截未授权请求。

  2. PolicyKit(动态授权管控)

    针对静态规则无法覆盖的动态场景(如普通用户临时需要执行敏感操作),D-Bus 集成 PolicyKit(现称 polkit)实现动态授权。系统服务(如 logind1systemd1)会在/run/current-system/sw/share/polkit-1/actions/(NixOS 中)或/run/current-system/sw/share/polkit-1/actions/(NixOS)或/usr/share/polkit-1/actions/(传统发行版中)定义 “可授权动作”,例如org.freedesktop.login1.power-off(对应 logind1 的关机方法):

    • 普通用户调用时,会触发认证流程(如输入管理员密码),认证通过后临时获得授权;

    • 活跃控制台用户可直接授权,无需额外验证,兼顾安全性与易用性。

  3. 连接层基础隔离

    D-Bus 总线套接字(如系统总线 /var/run/dbus/system_bus_socket)默认仅开放 rootdbus 组用户的读写权限,普通进程需通过 dbus-daemon 认证后才能建立连接。同时,每个连接会被分配唯一 ID(如 :1.42),并与进程的 PID/UID/GID 绑定,防止身份伪造与未授权接入。

4.7.2 Flatpak 对 D-Bus 权限的细粒度管控

在现代 Linux 桌面中,若需将商业软件等非信任应用运行在沙箱中,同时保障 「必要 D-Bus 交互不中断、越权访问被阻断」,Flatpak 采用 「底层沙箱隔离 + 上层代理过滤」 的双层方案 —— 其中 bubblewrap 是 Flatpak 依赖的底层沙箱工具,负责环境隔离;xdg-dbus-proxy 是上层过滤组件,负责 D-Bus 细粒度管控,两者协同实现完整安全隔离:

4.7.2.1 底层基础隔离:bubblewrap 的 “socket 隐藏与代理挂载”

Flatpak 以 bubblewrap(简称 bwrap)为底层沙箱基础,利用其 bind mountuser namespace 能力完成环境初始化,核心目标是切断沙箱应用与宿主 D-Bus 总线的直接联系:

  • 隐藏宿主 socketbubblewrap 会屏蔽宿主的 D-Bus 总线套接字(如不将/var/run/dbus/system_bus_socket 挂载进沙箱),避免应用绕过管控直接访问宿主总线;

  • 挂载代理 socket:同时,bubblewrap 会将 xdg-dbus-proxy 在宿主侧预先创建的 私有代理 socket,通过 bind mount 挂载到沙箱内的默认 D-Bus socket 路径(如沙箱内的/var/run/dbus/system_bus_socket)。

    此时沙箱应用感知到的 「D-Bus 总线」,实际是 xdg-dbus-proxy 提供的代理接口,无法直接接触宿主真实总线。

4.7.2.2 上层规则过滤:xdg-dbus-proxy 的 “白名单校验”

xdg-dbus-proxy 作为 Flatpak 内置的 D-Bus 代理组件,会随沙箱应用启动,加载 Flatpak 根据应用权限声明自动生成的过滤规则(粒度远细于 D-Bus 原生静态配置),例如:

    --see=NAME                   Set 'see' policy for NAME
    --talk=NAME                  Set 'talk' policy for NAME
    --own=NAME                   Set 'own' policy for NAME
    --call=NAME=RULE             Set RULE for calls on NAME
    --broadcast=NAME=RULE        Set RULE for broadcasts from NAME

TODO

这些规则可精确到 「服务名 + 接口 + 方法 + 对象路径」,弥补 D-Bus 原生配置在沙箱场景下 「动态性不足、粒度较粗」 的局限。

4.7.2.3 消息流转:代理的 “校验 - 转发” 逻辑

沙箱应用无需修改代码,会默认连接沙箱内的 「代理 socket」,所有 D-Bus 消息(方法调用、信号订阅)均需经过 xdg-dbus-proxy 的校验:

  • 若目标服务 / 方法在白名单内(如 org.freedesktop.portal.FileChooser.OpenFile),代理会将消息转发至宿主 D-Bus 总线,并把返回结果回传应用;

  • 若目标不在白名单内(如 org.freedesktop.login1.Manager.PowerOff),代理直接返回AccessDenied 错误,不向宿主总线转发任何消息,彻底阻断越权访问。


总结

本文深入探讨了 systemd 核心功能及其生态系统,从服务管理到各个专门化组件:

  1. systemd 核心功能:作为 PID 1 的服务管理器,专注于服务管理、依赖关系管理、资源控制和系统状态管理
  2. systemd 生态系统服务:包括日志管理(journald)、内存管理(oomd)、DNS 解析 (resolved)、时间同步(timesyncd)、设备管理(udevd)等
  3. 设备管理:udev 规则和设备权限分配,通过 systemd-udevd 确保硬件设备正确识别和访问
  4. D-Bus 系统总线:进程间通信机制,支持系统服务和桌面应用的交互

虽然 systemd 的争议一直存在,但不可否认的是,它确实让系统管理变得更加统一和高效。掌握了这些组件的使用方法,你在面对各种系统问题时就不会那么手足无措了。

下一篇文章我们会聊聊桌面会话和图形渲染,看看用户登录后系统是如何把漂亮的桌面呈现给你的。


Linux 桌面系统故障排查指南(一) - 系统启动与安全框架

2025年10月19日 10:17

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

前言

本文将简要介绍 Linux 桌面系统的启动机制,从 UEFI 引导到内核加载,从 initramfs 到 systemd 服务启动,再到桌面环境加载。同时还会探讨系统的安全框架,了解 PAM、PolicyKit 等组件如何保护系统安全。


1. 系统启动流程

启动的四个关键阶段

Linux 桌面系统的启动过程可以分为以下几个主要阶段:

  1. 固件阶段:UEFI 固件初始化硬件
  2. 引导加载器阶段:加载内核和 initramfs
  3. 内核阶段:硬件探测和驱动加载
  4. initramfs 阶段:准备根文件系统
  5. 用户空间阶段:systemd 接管系统管理

UEFI:系统启动的起点

现代系统普遍使用 UEFI 固件 代替 BIOS。UEFI 初始化硬件后,从 EFI System Partition (ESP) 中加载启动管理器。

NixOS 在 UEFI 系统上支持多种引导加载器。默认使用 GRUB;启用 Secure Boot 时通常使用systemd-boot 配合 lanzaboote

systemd-boot 的全局配置是 /boot/loader/loader.conf,具体的启动项配置需要分类讨论:

  • Type 1:手动配置 (Boot Loader Specification Type #1

    • 配置方式:/loader/entries/*.conf,位于 EFI 系统分区(ESP)或 Extended Boot Loader Partition(XBOOTLDR)下

    • 特点:

      • 可自定义启动项名称、内核参数、initrd 等
      • 描述 Linux 内核及其 initrd,也可以描述任意 EFI 可执行文件
      • 包括 fallback / rescue 内核
    • 示例:

      title   NixOS Linux
      linux   /vmlinuz-linux
      initrd  /initrd-linux.img
      options root=UUID=xxxx rw
  • Type 2:统一内核镜像 (Boot Loader Specification Type #2

    • 配置方式:将 EFI 格式的 UKI 镜像放在 ESP 分区的 /EFI/Linux/ 下即可
    • 工作原理:
      1. systemd-boot 在启动时扫描 ESP 的 /EFI/Linux/ 目录
      2. systemd-boot 会自动将扫描到的内核镜像添加到启动菜单,无需单独的 .conf 文件
    • 特点:
      • 免配置,自动出现在启动菜单中
      • vmlinuz-linux, initrd 跟 cmdline 等信息被统一打包成一个 EFI 镜像,一个镜像就包含了系统启动需要的所有数据,更方面简洁。
  • 其他自动识别的启动项

    • Microsoft Windows EFI boot manager(如果已安装)
    • Apple macOS boot manager(如果已安装)
    • EFI Shell 可执行文件(如果已安装)
    • 「Reboot Into Firmware Interface」选项(如果 UEFI 固件支持)
    • Secure Boot 变量注册(如果固件处于 setup 模式,且 ESP 提供了相关文件)

常用命令

  • efibootmgr -v:查看 / 修改固件启动顺序
  • bootctl status:检查 systemd-boot 安装与 ESP 状态
  • bootctl list:列出启动条目
  • ukify inspect /boot/EFI/Linux/nixos-xxx.efi: 查看 efi 镜像中包含的信息

示例:

# 查看固件启动顺序
$ nix run nixpkgs#efibootmgr -v

BootCurrent: 0000
Timeout: 0 seconds
BootOrder: 0000,0004
Boot0000* NixOS HD(1,GPT,34286f3b-d4df-456d-bf7a-eb67f2bf1a72,0x1000,0x12b000)/EFI\BOOT\BOOTX64.EFI
...
Boot0004* Windows Boot Manager  HD(1,GPT,34286f3b-d4df-456d-bf7a-eb67f2bf1a72,0x1000,0x12b000)/\EFI\Microsoft\Boot\bootmgfw.efi0000424f

# 检查 systemd-boot 安装与 ESP 状态
$ bootctl status

System:
      Firmware: UEFI 2.80 (American Megatrends 5.27)
 Firmware Arch: x64
   Secure Boot: enabled (user)
  TPM2 Support: yes
  Measured UKI: yes
  Boot into FW: supported

Current Boot Loader:
      Product: systemd-boot 257.7
     Features: ✓ Boot counting
               ✓ Menu timeout control
               ✓ One-shot menu timeout control
               ✓ Default entry control
               ✓ One-shot entry control
               ✓ Support for XBOOTLDR partition
               ✓ Support for passing random seed to OS
               ✓ Load drop-in drivers
               ✓ Support Type #1 sort-key field
               ✓ Support @saved pseudo-entry
               ✓ Support Type #1 devicetree field
               ✓ Enroll SecureBoot keys
               ✓ Retain SHIM protocols
               ✓ Menu can be disabled
               ✓ Multi-Profile UKIs are supported
               ✓ Boot loader set partition information
    Partition: /dev/disk/by-partuuid/34286f3b-d4df-456d-bf7a-eb67f2bf1a72
       Loader: └─EFI/BOOT/BOOTX64.EFI
Current Entry: nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi
...
Available Boot Loaders on ESP:
          ESP: /boot (/dev/disk/by-partuuid/34286f3b-d4df-456d-bf7a-eb67f2bf1a72)
         File: ├─/EFI/systemd/systemd-bootx64.efi (systemd-boot 257.7)
               └─/EFI/BOOT/BOOTX64.EFI (systemd-boot 257.7)
...
Default Boot Loader Entry:
         type: Boot Loader Specification Type #2 (.efi)
        title: NixOS Xantusia 25.11.20250830.d7600c7 (Linux 6.16.4) (Generation 848, 2025-09-01)
           id: nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi
       source: /boot//EFI/Linux/nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi (on the EFI System Partition)
     sort-key: lanza
      version: Generation 848, 2025-09-01
        linux: /boot//EFI/Linux/nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi
      options: init=/nix/store/gaj3sp3hrzjhp59bvyxhc8flg5s6iimg-nixos-system-ai-25.11.20250830.d7600c7/init nvidia-drm.fbdev=1 root=fstab loglevel=4 lsm=landlock,yama,bpf nvidia-drm.modeset=1 nvidia-drm.fbdev=1 nvidia.NVreg_PreserveVideoMemoryAllocations=1 nvidia.NVreg_OpenRmEnableUnsupportedGpus=1

# 查看上述启动项中 uki efi 文件的内容
$ nix shell nixpkgs#systemdUkify
$ ukify inspect /boot/EFI/Linux/nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi
.osrel:
  size: 141 bytes
  sha256: e486dea4910eb9262efc47464f533f96093293d37c3d25feb954c098865a4be6
  text:
    ID=lanza
    PRETTY_NAME=NixOS Xantusia 25.11.20250830.d7600c7 (Linux 6.16.4) (Generation 848, 2025-09-01)
    VERSION_ID=Generation 848, 2025-09-01
# 启动内核时使用的内核命令行参数
.cmdline:
  size: 284 bytes
  sha256: 7f94ffed08359eb1d2749176eba57e085113f46208702a8c0251376d734f19ce
  text:
    init=/nix/store/gaj3sp3hrzjhp59bvyxhc8flg5s6iimg-nixos-system-ai-25.11.20250830.d7600c7/init nvidia-drm.fbdev=1 root=fstab loglevel=4 lsm=landlock,yama,bpf nvidia-drm.modeset=1 nvidia-drm.fbdev=1 nvidia.NVreg_PreserveVideoMemoryAllocations=1 nvidia.NVreg_OpenRmEnableUnsupportedGpus=1
# initramfs 内容的引用,实际镜像位于 ESP 的 /EFI/nixos/initrd-*.efi
.initrd:
  size: 81 bytes
  sha256: 26d9b1f52806c48c6287272cb26b8a640b62d55f09149abf3415c76c38e0b56e
# 内核映像(vmlinuz)的引用,实际镜像位于 ESP 的 /EFI/nixos/kernel-*.efi
.linux:
  size: 81 bytes
  sha256: 41ff83e4cae160fb9ce55392943e6d06dbf9f37b710bf719f7fe2c28ec312be5

内核启动后,会探测 CPU、内存、PCI、USB、ACPI 等硬件,加载关键驱动,然后挂载 initramfs 并执行 option 中指定的 init 程序。

观察方法

# 查看内核早期日志
sudo dmesg --level=err,warn,info | less

# 查看本次启动的完整日志
journalctl -b

initramfs 阶段

initramfs(Initial RAM File System)是一个临时的根文件系统,在真正的根文件系统挂载之前提供必要的功能。它在启动阶段被加载到 RAM 中并被挂载为根目录。

initramfs 阶段的主要职责

  1. 硬件检测与驱动加载

    • 检测存储设备(SATA、NVMe、USB 等)
    • 加载必要的存储驱动模块
    • 识别网络设备(如果需要网络启动)
  2. 存储设备准备

    • 解密 LUKS 加密分区
    • 激活 LVM 逻辑卷
    • 处理 RAID 阵列
    • 挂载临时文件系统
  3. 根文件系统挂载

    • 根据内核参数 root= 找到根分区
    • 挂载根文件系统到 /new_root
    • 执行 switch_root 切换到真正的根文件系统
  4. 启动用户空间

    • 执行 /sbin/init(通常是 systemd)
    • 在 NixOS 中,init 程序是 /nix/store 中的一个 Shell 脚本,它首先完成一些必要的初始化工作,之后才启动 systemd.

常见故障与排查

  • 找不到根分区:检查 cat /proc/cmdlineroot= 参数与 blkid 输出是否一致
  • 缺少驱动模块:确保 NixOS 配置包含所需模块:boot.initrd.kernelModules = [ "nvme" "dm_mod" ];
  • LUKS 解密失败:检查密码输入或密钥文件配置
  • LVM 激活失败:确认 LVM 配置和卷组状态

排查步骤

  1. 编辑内核 cmdline,添加 init=/bin/shbreak=mount 进入 initramfs shell
  2. 运行 lsblkblkid 确认设备
  3. 查看 dmesg 中的磁盘或 LVM 错误
  4. 检查 /proc/cmdline 中的启动参数

2. 启动故障排查

启动故障排查流程

flowchart LR
    A[系统无法启动] --> B{能否进入 UEFI/BIOS?}
    B -->|否| C[硬件问题
检查电源、内存、CPU] B -->|是| D{能否看到启动菜单?} D -->|否| E[引导加载器问题
检查 ESP 分区和启动项] D -->|是| F{能否选择启动项?} F -->|否| G[启动项配置错误
检查 bootctl 配置] F -->|是| H{内核能否加载?} H -->|否| I[内核或 initramfs 问题
检查内核参数和驱动] H -->|是| J{能否进入 initramfs?} J -->|否| K[initramfs 问题
检查根分区和文件系统] J -->|是| L{能否挂载根分区?} L -->|否| M[文件系统或加密问题
检查 LUKS 和 LVM] L -->|是| N{systemd 能否启动?} N -->|否| O[用户空间问题
检查 systemd 服务] N -->|是| P[启动成功]

常见启动问题:症状与解决方案

在系统启动过程中,可能会遇到各种问题。以下是按启动阶段分类的常见问题及排查方法:

2.2.1 固件和引导加载器问题

问题症状

  • 系统无法启动,停留在固件界面
  • 显示 “No bootable device” 错误
  • 启动菜单不显示或显示异常

排查步骤

使用 USB 启动盘进入 LiveOS, 进行如下检查:

# 检查 UEFI 设置
efibootmgr -v

# 检查 ESP 分区状态
bootctl status

# 验证启动项配置
bootctl list

2.2.2 内核和 initramfs 问题

问题症状

  • 内核 panic 或无法加载
  • initramfs 阶段卡住
  • 找不到根分区

排查步骤

# 进入 initramfs shell 进行调试
# 在内核参数中添加:init=/bin/sh 或 break=mount

# 检查设备识别
lsblk
blkid

# 查看内核日志
dmesg | grep -i error

# 检查文件系统完整性
fsck /dev/sdX

启动性能优化

2.3.1 启动时间分析

# 使用 systemd-analyze 分析启动时间
systemd-analyze
systemd-analyze blame
systemd-analyze critical-chain

# 生成启动时间报告
systemd-analyze plot > boot-time.svg

这些工具可以帮助你分析系统启动性能:

  • systemd-analyze 显示总启动时间,包括内核和用户空间的启动耗时
  • systemd-analyze blame 按耗时排序显示各服务启动时间,找出最耗时的服务
  • systemd-analyze critical-chain 显示关键路径分析,找出阻塞启动的服务链
  • systemd-analyze plot 生成启动时间图表,可视化各服务的启动顺序和耗时

识别到启动阶段的性能瓶颈后,就能据此优化服务依赖关系,加快启动速度。

2.3.2 启动优化策略

优化启动速度可以从多个层面入手:

硬件层面

使用 SSD 存储是最直接有效的优化方法。固态硬盘的随机读写性能远超机械硬盘,能显著减少文件系统访问延迟。启动时间通常可减少 50-80%,特别是对于大量小文件读取的场景。适用于所有系统,特别是启动时间较长的系统。

内核层面

启用内核并行初始化可以提升启动速度。现代内核支持并行初始化硬件设备,减少串行等待时间。通过内核参数如 initcall_debugacpi=noirq 等可以优化启动流程,减少硬件初始化时间。

服务层面

优化 systemd 服务依赖关系可以减少启动延迟。减少不必要的服务依赖,避免串行启动造成的延迟。使用 systemctl list-dependencies 分析依赖关系,移除不必要的依赖,减少服务启动等待时间, 提升并行启动效率。

启动流程

使用 UKI(统一内核镜像)可以减少启动步骤。将内核、initramfs、cmdline 打包成单个 EFI 文件, 减少启动步骤和文件系统访问。减少文件系统挂载次数,简化启动流程。在 NixOS 中通过boot.loader.systemd-boot.enableboot.loader.efi.canTouchEfiVariables 启用。

3. 系统安全框架:认证、授权与密钥管理

现代 Linux 桌面系统的安全架构由多个相互协作的组件构成,包括 PAM(认证)、PolicyKit(授权)、以及桌面环境提供的密钥管理服务。这些组件共同构建了一个多层次的安全防护体系,既保证了系统的安全性,又提供了良好的用户体验。

NOTE: 注意 PAM 与 PolicyKit 的设计目的都是为普通用户提供权限提升手段。对 root 用户而言,这些框架的限制很少或几乎不存在。如果你希望限制整个系统全局的权限(包括 root 用户), 应该考虑 SELinux/AppArmor 等强制访问控制框架。


3.1 PAM - 可插拔认证模块

PAM(Pluggable Authentication Modules) 是 Linux 的统一认证框架,为系统中的各种程序 (如 loginsudosshdgdm 等)提供标准化的认证接口。借助 PAM,系统管理员可以通过配置文件灵活控制认证策略,而无需修改应用程序本身。它支持多种认证方式(密码、指纹、智能卡、双因子验证等),是现代 Linux 安全体系的核心组件之一。


3.1.1 工作原理与配置结构

PAM 采用模块化设计,将认证流程分为四个阶段。应用程序通过调用相应的 PAM 接口触发这些阶段, 系统根据 /etc/pam.d/ 下的配置文件执行相应的模块(.so 文件)。

(1)配置文件语法

https://linux.die.net/man/5/pam.d

每行的基本格式如下:

<type>  <control>  <module>  [arguments]
  • type:表示阶段类型
  • control:定义该模块的执行策略
  • module:具体的 PAM 模块路径(或名称)
  • arguments:传递给模块的参数
(2)四个认证阶段
阶段类型 调用函数 主要作用
auth pam_authenticate() 验证用户身份,通常会提示用户输出密码或指纹以完成验证
account pam_acct_mgmt() 检查账户状态(过期、锁定等)
password pam_chauthtok() 处理密码修改
session pam_open_session() / pam_close_session() 建立和清理用户会话
(3)控制标志(control)
标志 含义 行为说明
required 必须成功,失败不会立即终止,但最终结果会失败 无论成功失败,都会继续执行后续模块。最终只要有一个 required 失败,整个认证就失败。
requisite 必须成功,失败立即终止并返回失败 失败立即返回,不再执行后续模块。
sufficient 成功则立即通过认证(跳过所有后续模块);失败则继续由后续模块进行认证 若前面没有 required 失败,则成功直接通过;否则失败不影响后续。
optional 可选模块,结果通常被忽略 无论成功失败,对最终结果无直接影响,除非是栈中唯一的模块。
include 包含另一个文件的配置 将指定文件的配置内容包含进来,通常用于复用通用配置(如 system-auth)。
substack 调用子栈 类似 include,但子栈的失败不影响主栈(即子栈只能跳过其自身的后续步骤),除非主栈中另有设置。
(4)常用模块示例
pam_unix.so                 # 基于 /etc/passwd 与 /etc/shadow 的标准密码认证
pam_google_authenticator.so # 双因子认证(TOTP)
pam_fprintd.so              # 指纹认证
pam_ldap.so                 # LDAP 集中式认证
pam_gnome_keyring.so        # GNOME 密钥环集成
pam_limits.so               # 用户资源限制
pam_deny.so                 # 拒绝所有认证请求

3.1.2 执行流程示例

/etc/pam.d/sudo 为例:

#%PAM-1.0
auth       sufficient   pam_rootok.so
auth       sufficient   pam_timestamp.so
auth       required     pam_wheel.so use_uid
auth       required     pam_unix.so nullok try_first_pass

各模块的执行顺序如下:

  1. 执行 pam_rootok.so (sufficient)
    • 检查当前用户是否为 root。
    • 如果成功:PAM 认证流程立即成功,并跳过后续所有 auth 模块。用户直接获得 sudo 权限。
    • 如果失败:继续执行下一个模块。
  2. 执行 pam_timestamp.so (sufficient)
    • 检查是否存在有效的时间戳文件(默认为 5 分钟内)。
    • 如果成功:PAM 认证流程立即成功,并跳过后续所有 auth 模块。用户免密码获得 sudo 权限。
    • 如果失败:继续执行下一个模块。
  3. 执行 pam_wheel.so (required)
    • 检查当前用户是否在 wheel 组(或 sudo 组,取决于配置)中。
    • 无论成功还是失败,都必须继续执行下一个 required 模块。但其结果会被记录下来。
  4. 执行 pam_unix.so (required)
    • 使用 nulloktry_first_pass 参数进行密码验证。
    • nullok:允许空密码账户登录。
    • try_first_pass:尝试使用前面模块(如果有的话)提供的密码。对于 sudo,这通常指之前 sudo 成功时缓存的密码。
    • 如果密码正确:此模块成功。
    • 如果密码错误:此模块失败。
  5. 最终结果判定
    • 在所有 required 模块执行完毕后,PAM 会检查它们的结果。
    • 如果任何一个 required 模块(pam_wheel.sopam_unix.so)失败,整个认证流程失败。
    • 只有当所有 required 模块都成功时,认证才最终成功。

常用模块及其参数说明

  1. pam_unix.so 参数 (用于密码验证) 这是最核心的密码认证模块,常见于 authpassword 类型。
    • nullok:允许空密码账户通过认证。如果不加此参数,空密码账户将无法登录。
    • try_first_pass:在提示用户输入密码前,先尝试使用之前栈中已缓存的密码(例如,由pam_timestamp.sopam_kwallet.so 提供的)。
    • use_authtok强制使用之前栈中已缓存的密码,如果不存在缓存密码,则直接失败。它比try_first_pass 更严格,通常用在修改密码的 password 模块栈中,以确保用户输入的是旧密码。
    • shadow:使用 /etc/shadow 文件进行密码验证(现代系统默认启用)。
  2. pam_timestamp.so 参数 (用于时间戳认证) 常用于 sudo,实现免密码操作。
    • timestamp_timeout=600:设置时间戳的有效期,单位为秒。默认是 300 (5分钟)。
  3. pam_wheel.so 参数 (用于组成员资格检查) 用于限制只有特定组的用户才能使用 susudo
    • use_uid:检查发起请求的原始用户 ID,而不是当前用户 ID(在 sudo 场景下很重要)。
    • group=admins:指定检查的组名,默认是 wheel
  4. pam_gnome_keyring.so 参数 (用于会话管理) 这个模块与 sudo 的认证流程无关,主要用于用户登录时解锁密钥环。
    • auto_start:在会话启动时,如果用户密码与密钥环密码相同,则自动解锁密钥环。
    • 典型应用场景:在 /etc/pam.d/gdm-password/etc/pam.d/loginauthsession 部分。
      # 在 /etc/pam.d/gdm-password 中
      auth       optional    pam_gnome_keyring.so
      session    optional    pam_gnome_keyring.so auto_start

3.1.3 应用程序与 PAM 的交互

程序通过 pam_start() 指定服务名,系统据此加载对应的配置文件。

程序 服务名 配置文件 功能
login "login" /etc/pam.d/login 控制台登录
gdm "gdm" /etc/pam.d/gdm GNOME 登录界面
sudo "sudo" /etc/pam.d/sudo 提权命令
sshd "sshd" /etc/pam.d/sshd SSH 登录
greetd "greetd" /etc/pam.d/greetd 轻量显示管理器

一个典型的调用顺序如下(以 sudo 为例):

pam_start("sudo", user, &conv, &pamh);     // 初始化 PAM
pam_authenticate(pamh, 0);                 // 身份验证
pam_acct_mgmt(pamh, 0);                    // 账户检查
pam_open_session(pamh, 0);                 // 打开会话
// 执行用户命令
pam_close_session(pamh, 0);                // 关闭会话
pam_end(pamh, PAM_SUCCESS);                // 释放资源

如下是一个用户登录流程的 PAM 调用示例:

#include <stdio.h>
#include <stdlib.h>
#include <security/pam_appl.h>
#include <security/pam_misc.h>

static void log_result(pam_handle_t *pamh, int ret, const char *step)
{
    if (ret == PAM_SUCCESS) {
        printf("[✓] %s 成功\n", step);
    } else {
        fprintf(stderr, "[✗] %s 失败: %s(返回码 %d)\n",
                step, pam_strerror(pamh, ret), ret);
    }
}

int main(int argc, char *argv[])
{
    pam_handle_t *pamh = NULL;
    struct pam_conv conv = { misc_conv, NULL };
    const char *user;
    int ret;
    if (argc != 2) {
        fprintf(stderr, "用法: %s 用户名\n", argv[0]);
        return 1;
    }
    user = argv[1];
    /* 1. 初始化 */
    ret = pam_start("login", user, &conv, &pamh);
    if (ret != PAM_SUCCESS) {
        log_result(pamh, ret, "pam_start");
        return 1;
    }
    /* 2. 认证 */
    ret = pam_authenticate(pamh, 0);
    log_result(pamh, ret, "pam_authenticate");
    if (ret != PAM_SUCCESS) {
        pam_end(pamh, ret);
        return 1;
    }
    /* 3. 帐户检查 */
    ret = pam_acct_mgmt(pamh, 0);
    log_result(pamh, ret, "pam_acct_mgmt");
    if (ret != PAM_SUCCESS) {
        pam_end(pamh, ret);
        return 1;
    }
    /* 4. 打开会话 */
    ret = pam_open_session(pamh, 0);
    log_result(pamh, ret, "pam_open_session");
    if (ret != PAM_SUCCESS) {
        /* 常见原因提示 */
        fprintf(stderr,
                "\n提示:\n"
                "  1. 若您以普通用户运行,失败通常是权限不足(写 /var/run/utmp 等)。\n"
                "  2. 以 root 再次运行即可验证会话模块能否通过:sudo %s %s\n",
                argv[0], user);
        pam_end(pamh, ret);
        return 1;
    }
    printf("\n全部 PAM 阶段通过!\n");
    /* 5. 关闭会话并清理 */
    pam_close_session(pamh, 0);
    pam_end(pamh, PAM_SUCCESS);
    return 0;
}

将上述配置保存为 pam_test.c, 再创建一个 shell.nix 内容如下:

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  buildInputs = with pkgs; [
    pam
    gcc
  ];
}

最后编译运行:

# 进入引入了 pam 链接库的环境
nix-shell
# 编译
gcc pam_test.c -o pam_test -lpam -lpam_misc
# 测试
./pam_test ryan

3.1.4 模块间的数据传递

PAM 模块可通过 pam_set_data()pam_get_data() 共享状态。例如:

pam_set_data(pamh, "authenticated", "true", NULL);
const char *ok;
pam_get_data(pamh, "authenticated", (const void **)&ok);

这使多个模块在同一认证过程中共享信息。


3.1.5 调试与故障排查

PAM 的问题通常来源于配置错误或模块加载失败,可按以下思路排查:

(1)测试与验证
nix shell nixpkgs#pamtester
# 模拟特定服务的认证流程
pamtester sudo $USER authenticate
pamtester login $USER open_session
(2)检查模块与依赖
# 验证模块存在与架构匹配
ls /run/current-system/sw/lib/security/pam_unix.so
ldd /run/current-system/sw/lib/security/pam_unix.so
(3)查看系统日志
journalctl -b | grep -i pam
(4)跟踪调用行为
strace -f -e trace=openat,read,write -o sudo_trace.log sudo true
grep pam sudo_trace.log
(5)常见问题
问题 可能原因
模块加载失败 模块路径错误或权限不足
认证成功但无法建立会话 会话模块执行失败(如无法写入 /var/run/utmp
GNOME Keyring 不自动解锁 pam_gnome_keyring.so 未启用或未配置 auto_start
PAM 配置无效 程序服务名与配置文件不匹配,默认使用 other

3.2 PolicyKit - 细粒度的系统权限管理

PolicyKit(现称 polkit)是一个用于控制系统级权限的框架,它提供了一种比传统 Unix 权限更细粒度的授权机制。在现代 Linux 桌面系统中,PolicyKit 允许非特权用户执行某些需要特权的系统操作 (如关机、重启、挂载设备、修改系统时间等),而无需获取完整的 root 权限。

3.2.1 PolicyKit 的核心概念

配置文件路径

  • /etc/polkit-1/:NixOS 声明式配置中定义的自定义规则(优先级最高)
  • /run/current-system/sw/share/polkit-1/(NixOS)或 /usr/share/polkit-1/(传统发行版):软件包提供的默认规则

上述文件夹中又包含两类配置:

  • 动作(Actions)
    • 定义在配置文件夹的 actions 目录中的 XML 文件(如 /etc/polkit-1/actions/),描述可授权的操作。每个动作都有唯一的标识符,如 org.freedesktop.login1.power-off 表示关机操作。
  • 规则(Rules)
    • JavaScript 文件,定义授权决策逻辑,位于上述配置文件夹的 rules.d/ 目录中(如/etc/polkit-1/rules.d/)。规则决定了在特定条件下是否授权某个操作。在 NixOS 中,推荐使用声明式配置而非直接修改 /etc 目录。

身份认证代理(Authentication Agents):桌面环境提供的图形界面组件,用于在用户需要身份验证时弹出认证对话框。例如,当普通用户尝试关机时,认证代理会提示输入管理员密码。

举例来说,我使用的是 Niri 窗口管理器,它的 Nix Flake 启用了 pokit-kde-agent-1 作为其 Authentication Agent, 配置参见sodiboo/niri-flake.

3.2.2 PolicyKit 的工作原理

当应用程序请求执行需要特权的操作时,系统服务会询问 PolicyKit 是否授权。PolicyKit 的评估过程如下:

  1. 身份识别:确定请求者的身份(用户、组、会话等)
  2. 规则匹配:检查是否有适用的规则文件
  3. 权限评估:根据规则返回以下结果之一:
    • yes:直接允许,无需认证
    • no:直接拒绝
    • auth_self:需要用户自己认证(输入当前用户密码)
    • auth_admin:需要管理员认证(输入 root 密码)
    • auth_self_keep/auth_admin_keep:认证后在一段时间内保持授权

3.2.3 PolicyKit 的配置示例

在传统的 Linux 发行版中,管理员可以通过创建自定义规则来修改默认行为。例如,允许 wheel 组的用户无需密码即可关机:

// /etc/polkit-1/rules.d/10-shutdown.rules
polkit.addRule(function (action, subject) {
  if (action.id == "org.freedesktop.login1.power-off" && subject.isInGroup("wheel")) {
    return polkit.Result.YES
  }
})

NixOS 中的配置方法:在 NixOS 中,推荐使用声明式配置而非直接修改 /etc 目录。可以通过security.polkit 配置项来管理 PolicyKit 规则:

# configuration.nix
{
  security.polkit.enable = true;

  # 添加自定义规则
  security.polkit.extraConfig = ''
    polkit.addRule(function(action, subject) {
      if (action.id == "org.freedesktop.login1.power-off" &&
          subject.isInGroup("wheel")) {
        return polkit.Result.YES;
      }
    });
  '';
}

3.2.4 PolicyKit 与 D-Bus 的集成

PolicyKit 与 D-Bus 深度集成,为 D-Bus 服务提供动态授权机制。许多系统服务(如 systemd、NetworkManager、udisks 等)都使用 PolicyKit 来控制对其 D-Bus 接口的访问。当客户端通过 D-Bus 调用需要特权的方法时,服务会调用 PolicyKit 进行授权检查。

PolicyKit 调试主要涉及服务状态检查、权限测试和规则验证。常用的调试方法包括:

  • 服务状态检查:验证 PolicyKit 守护进程的运行状态
  • 权限测试:使用 pkcheck 工具测试特定操作的授权情况
  • 日志分析:查看 PolicyKit 的授权决策日志
  • 规则验证:检查当前生效的 PolicyKit 规则配置

具体的调试命令请参考 3.5.3 故障排查 章节。

3.3 桌面密钥管理

现代 Linux 桌面环境提供了统一的密钥管理服务,用于安全存储用户的密码、证书、密钥等敏感信息。

GNOME Keyring 和 KDE Wallet 分别是 GNOME 和 KDE 桌面环境的密钥管理解决方案,它们通过加密存储和自动解锁机制,为用户提供了便捷而安全的密码管理体验。

GNOME Keyring 和 KDE Wallet 都实现了标准的Secrets API, 可以根据需要任选一个使用。不过据我观察大部分窗口管理器的用户都是用的 GNOME Keyring.

3.3.1 密钥管理系统架构

GNOME Keyring 架构

  • 密钥环(Keyring):加密的存储容器,每个密钥环有独立的密码
  • 密钥环守护进程(gnome-keyring-daemon):管理密钥环的生命周期和访问控制
  • API:Gnome 原生支持 org.freedesktop.secrets DBus API, 目前流行的 secrets 客户端库 libsecret 也是 gnome 开发的。
  • PAM 集成:通过 pam_gnome_keyring.so 实现登录时自动解锁

KDE Wallet 架构

  • KWalletManager:图形界面管理工具
  • kwalletd:钱包守护进程
  • API:KDE Wallet 从 5.97.0 (2022 年 8 月)开始支持org.freedesktop.secrets DBus API, 因此可以直接通过 libsecret 往 KDE Wallet 中存取 passwords 等 secret.
  • PAM 集成:通过 pam_kwallet.so 实现自动解锁

核心组件路径

# GNOME Keyring 组件(NixOS 中位于 nix store)
/run/current-system/sw/bin/gnome-keyring-daemon
/run/current-system/sw/lib/libsecret-1.so
/run/current-system/sw/lib/security/pam_gnome_keyring.so

# KDE Wallet 组件(NixOS 中位于 nix store)
/run/current-system/sw/bin/kwalletd5
/run/current-system/sw/bin/kwalletmanager5
/run/current-system/sw/lib/security/pam_kwallet.so

# 配置文件位置
~/.local/share/keyrings/     # GNOME 密钥环存储目录
~/.local/share/kwalletd/     # KDE 钱包文件存储目录
~/.config/kwalletrc          # KDE 钱包配置文件

3.3.2 密钥环类型与用途

密钥环类型 用途 解锁时机
login 登录密钥环,存储用户密码 用户登录时自动解锁
default 默认密钥环,存储应用密码 首次访问时解锁
session 会话密钥环,临时存储 会话开始时创建
crypto 加密密钥环,存储证书和私钥 按需解锁

3.3.3 钱包创建与管理

图形界面管理

# GNOME 密钥环管理器
seahorse

# KDE 钱包管理器
kwalletmanager5

通过图形界面可以:

  • 创建新的密钥环/钱包
  • 设置密码和加密算法
  • 管理存储的密码和证书
  • 配置自动解锁策略
  • 备份和恢复密钥环

基本命令行操作

# 使用 secret-tool 管理 GNOME Keyring
secret-tool store --label="My Password" application myapp
secret-tool lookup application myapp

# 使用 kwallet-query 管理 KDE Wallet
kwallet-query --write password "MyApp" "username" "password"
kwallet-query --read password "MyApp" "username"

3.3.4 应用程序集成

常见应用程序集成

VSCode

  • 自动集成系统密钥管理服务
  • 存储 Git 凭据、扩展设置等敏感信息
  • 通过 git credential.helper 配置自动使用

GitHub CLI

# 配置 GitHub CLI 使用系统密钥管理
gh auth login --web
# 凭据会自动存储到系统密钥环中

浏览器集成

  • Firefox、Chrome 等现代浏览器支持系统密钥管理
  • 网站密码自动保存到密钥环/钱包中
  • 跨设备同步(如果启用)

API 集成示例

3.3.5 配置与优化

NixOS 配置示例

# configuration.nix
# 启用 GNOME Keyring
services.gnome.gnome-keyring.enable = true;
# GNOME Keyring GUI 客户端
programs.seahorse.enable = true;
# 启用 PAM 集成
security.pam.services.login.enableGnomeKeyring = true;

3.4 安全故障排查

3.4.1 认证问题排查

常见认证失败场景

  1. 用户无法登录

    • 检查 PAM 配置是否正确
    • 查看认证日志中的错误信息
    • 验证用户账户状态和密码
  2. sudo 权限问题

    • 确认用户在正确的用户组中
    • 检查 sudoers 配置
    • 验证 PAM 认证流程
  3. SSH 登录失败

    • 检查 SSH 服务状态
    • 查看 SSH 认证日志
    • 验证网络连接和防火墙设置

3.4.2 权限管理问题排查

PolicyKit 权限问题

  • 无法关机/重启:检查 PolicyKit 规则配置和用户组权限
  • 无法挂载设备:检查 udisks2 服务和 PolicyKit 集成
  • 无法修改系统时间:检查时间同步服务权限和用户组设置

3.4.3 密钥管理问题排查

GNOME Keyring 问题

  • 检查密钥环守护进程是否正常运行
  • 验证 PAM 集成是否正确配置
  • 查看密钥环状态和自动解锁设置

KDE Wallet 问题

  • 检查钱包守护进程状态
  • 验证钱包配置和访问权限
  • 测试钱包的读写功能

具体的调试命令和排查步骤请参考 3.5.3 故障排查 章节。

3.5 安全组件集成与最佳实践

3.5.1 组件协作流程

现代 Linux 桌面的安全组件协作流程:

  1. 用户登录:PAM 验证用户身份
  2. 密钥环解锁:PAM 模块自动解锁用户密钥环/钱包
  3. 应用启动:应用程序通过 libsecret/KWallet API 访问存储的密码
  4. 特权操作:PolicyKit 控制需要特权的系统操作
  5. 会话结束:密钥环/钱包自动锁定

3.5.2 安全最佳实践

密钥管理

  • 使用强密码保护密钥环/钱包
  • 定期备份密钥环文件
  • 避免在脚本中硬编码密码
  • 使用应用程序专用的密钥环

认证配置

# 启用双因子认证
auth required pam_google_authenticator.so
auth required pam_unix.so

# 配置密码策略
password required pam_cracklib.so retry=3 minlen=8 difok=3
password required pam_unix.so use_authtok

权限管理

// PolicyKit 规则示例:限制特定操作
polkit.addRule(function (action, subject) {
  if (action.id == "org.freedesktop.login1.power-off" && subject.user == "guest") {
    return polkit.Result.NO
  }
})

3.5.3 故障排查

PAM 认证调试

nix shell nixpkgs#pamtester

# 测试 PAM 配置
pamtester login $USER authenticate
pamtester sudo $USER authenticate

# 查看 PAM 配置
cat /etc/pam.d/login
cat /etc/pam.d/greetd
cat /etc/pam.d/sudo

# 检查 PAM 模块
ldd /run/current-system/sw/lib/security/pam_unix.so
ldd /run/current-system/sw/lib/security/pam_gnome_keyring.so

# 查看认证日志
journalctl -t login -f
journalctl -t greetd -f
journalctl -t sshd -f
journalctl -t sudo

# 验证程序与配置的对应关系
strace -e trace=pam_start login 2>&1 | grep pam_start
strace -e trace=openat login 2>&1 | grep pam.d

PolicyKit 权限调试

# 检查 PolicyKit 服务状态
systemctl status polkit

# 测试特定权限
pkcheck --action-id org.freedesktop.login1.power-off --process $$ --allow-user-interaction

# 查看 PolicyKit 日志
journalctl -u polkit -f

# 查看 PolicyKit 动作定义
ls -la /run/current-system/sw/share/polkit-1/actions/

# 查看当前生效的 PolicyKit 规则
ls -la /etc/polkit-1/rules.d/

密钥管理调试

# GNOME Keyring 检查
ps aux | grep gnome-keyring
seahorse  # GNOME Keyring GUI

# KDE Wallet 检查
ps aux | grep kwalletd
kwalletmanager5  # KDE Wallet GUI
kwallet-query kdewallet --list-entries

# 系统日志检查
sudo journalctl -u systemd-logind

调试技巧

  • 使用 strace 跟踪应用程序的密钥访问
  • 通过 journalctl 查看认证和授权日志
  • 使用 pamtester 测试 PAM 配置
  • 通过 pkcheck 测试 PolicyKit 权限

通过理解这些安全组件的协作机制,用户可以更好地配置和管理 Linux 桌面的安全策略,在保证安全性的同时提供良好的用户体验。

总结

从 UEFI 到 systemd,从 PAM 到 PolicyKit,本文详细介绍了 Linux 桌面系统启动与安全框架的核心组件。

下一篇文章将深入探讨 systemd 全家桶与服务管理,包括 D-Bus 系统总线、日志系统和设备管理等核心功能,这些组件为桌面环境提供了强大的基础设施支持。

快速参考

常用启动排查命令

# 启动时间分析
systemd-analyze
systemd-analyze blame
systemd-analyze critical-chain

# 引导加载器检查
bootctl status
bootctl list
efibootmgr -v

# 内核和硬件信息
dmesg | grep -i error
lspci -k
lsusb
lsblk

# 进入救援模式
# 在内核参数中添加:init=/bin/sh 或 break=mount

常用安全排查命令

安全相关的调试命令请参考 3.5.3 故障排查 章节,该章节提供了完整的 PAM、PolicyKit 和密钥管理调试命令。

重要配置文件位置

# 启动相关
/boot/loader/loader.conf          # systemd-boot 全局配置
/boot/EFI/Linux/                  # UKI 镜像位置
/etc/pam.d/                       # PAM 配置文件
/etc/polkit-1/                    # PolicyKit 配置

# 密钥管理
~/.local/share/keyrings/          # GNOME Keyring 存储
~/.local/share/kwalletd/          # KDE Wallet 存储
~/.config/kwalletrc               # KDE Wallet 配置

Linux 桌面系统故障排查指南(零) - 组件概览

2025年9月9日 20:17

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

定位与目标

Linux 桌面包含了相当多的系统组件,这些组件组合形成了一个精密的系统,它们共同管理着从硬件设备到用户会话的方方面面。

即使我已经有七八年的 Linux 使用经验,在遇到系统的各种大小毛病时,还是常常觉得问题的定位跟解决很是艰难。倘若我们能像庖丁那样"目无全牛",对整个系统的架构了如指掌,在定位问题时顺着骨节筋脉下刀,那解决起问题来自然也将游刃有余。

而这就是这个系列文章的目的——搭建起一幅 Linux 桌面系统的完整「解牛图」。

本系列面向已经有一定 Linux 桌面使用经验的读者。我们用一条从「开机」到「APP 运行」再到「关机/ 断电」的完整时间线为轴,深入讲解每一步发生了什么、哪里能看到证据(日志 / 设备节点 / D‑Bus 信号)、可通过哪些命令排查验证,以及常见问题的修复思路。

本文作为系列概览,主要起导航和架构梳理的作用,帮助读者建立整体认知框架。


文章系列导航

📋 Linux 桌面系统故障排查指南(一) - 系统启动与安全框架

涵盖内容

  • 系统启动流程:从 UEFI 固件到 systemd 用户空间的完整启动过程,包括 systemd-boot 配置、UKI 统一内核镜像、initramfs 阶段详解
  • 安全框架深度解析:PAM 认证机制、PolicyKit 权限管理、GNOME Keyring 密钥管理,以及各组件间的协作关系

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

涵盖内容

  • systemd 核心功能:服务管理、依赖关系、并行启动、单元类型配置和生命周期管理
  • systemd 生态系统服务:着重介绍 systemd-journald 日志系统、systemd-oomd 内存管理、systemd-resolved DNS 解析、systemd-timesyncd 时间同步
  • 设备管理:udev 规则系统、systemd-udevd 用户空间实现、设备权限分配和故障排查
  • D-Bus 系统总线:进程间通信机制、权限管控、Flatpak 沙盒环境下的 D-Bus 代理过滤
  • 服务管理最佳实践:服务配置优化、依赖关系管理、性能调优

🖥️ Linux 桌面系统故障排查指南(三) - 桌面会话与图形渲染

涵盖内容

  • 用户会话管理:登录流程详解、systemd-logind 会话控制、seat 概念和多用户场景
  • Wayland 图形架构:与 X11 的深度对比、客户端-服务器模型、协议扩展和安全性优势
  • 图形渲染栈:DRM/KMS 显示管理、Mesa 驱动、EGL/GBM 接口、OpenGL/Vulkan 渲染管线
  • 输入处理系统:libinput 事件处理、evdev 内核接口、手势识别、多点触控支持
  • 设备访问控制:ACL 权限分配、GPU 设备管理、输入设备权限、systemd-logind 集成
  • 应用程序架构:启动流程、图形驱动选择、工具包支持(GTK/Qt/SDL)、渲染器优化
  • 应用兼容性:Wayland/XWayland 兼容性、沙盒化应用(Flatpak)、性能调优

🎵 Linux 桌面系统故障排查指南(四) - 多媒体处理与中文支持

涵盖内容

  • PipeWire 统一多媒体架构:音频视频处理、屏幕共享、兼容层(PulseAudio/JACK/ALSA)
  • 视频处理与屏幕共享:Wayland screen-capture 协议、硬件加速、DMA-BUF 传递、权限管理
  • 音频处理流程:低延迟配置、音频路由控制、设备管理和性能优化
  • 字体渲染系统:fontconfig 配置、CJK 字体管理、渲染参数优化、字体匹配规则
  • 中文输入法:fcitx5 框架、Wayland text-input 协议、XWayland 兼容性、混合环境管理

🌐 Linux 桌面系统故障排查指南(五) - 网络

涵盖内容

  • 网络连接与管理:从硬件驱动到应用层的完整协议栈,systemd-networkd 和 iwd 的现代网络管理
  • IPv4/IPv6 双栈技术:地址分配机制、路由表管理、协议优先级配置、双栈验证方法
  • 防火墙与网络安全:nftables 现代防火墙、NAT 配置、端口转发、流量控制规则
  • 虚拟网络技术:TUN/TAP 接口、VPN 连接管理(WireGuard)、桥接网络、容器网络
  • 网络性能优化:内核参数调优、TCP 拥塞控制(BBR)、连接跟踪优化、网络监控分析
  • 高级网络配置:多网卡绑定、VLAN 配置、网络命名空间、网络故障诊断

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

涵盖内容

  • 系统关机流程详解:介绍完整关机过程,从用户会话清理到硬件关机的每个步骤
  • 电源管理功能:休眠(Hibernate)和挂起(Suspend)的配置、工作原理和故障排查
  • 关机故障排查:服务停止超时、文件系统卸载失败、设备繁忙等问题的诊断和解决
  • 实战故障案例:桌面环境启动失败、应用崩溃、网络异常、系统关机卡住等综合问题
  • 系统化排查方法:日志分析、逐层排查、工具使用技巧、最佳实践总结
  • 电源管理优化:自动挂起配置、定时休眠设置、功耗优化、硬件兼容性处理

技术栈说明

本系列文章基于以下现代 Linux 桌面技术栈:

  • 引导系统:UEFI + systemd-boot
  • 初始化系统:systemd
  • 显示协议:Wayland
  • 音频系统:PipeWire
  • 网络管理:systemd-networkd + iwd
  • 防火墙:nftables
  • 输入法:fcitx5
  • 字体系统:fontconfig
  • 发行版:主要基于 NixOS,同时补充说明与传统发行版的差异

技术选择说明

  • systemd-boot:相比 GRUB 更简洁,支持 UKI 和 Secure Boot,启动速度更快
  • Wayland:相比 X11 更安全、性能更好
  • PipeWire:统一的多媒体处理框架,相比 PulseAudio 延迟更低,支持统一处理音频跟视频
  • systemd-networkd + iwd:相比 NetworkManager + wpa_supplicant 更现代、更轻量
  • nftables:相比 iptables 语法更简洁,性能更好
  • fcitx5:相比 ibus 对 Wayland 支持更好,配置更灵活

虽然以 NixOS 为例,但涉及的核心概念、配置方法和故障排查技巧同样适用于其他现代 Linux 发行版 (如 Arch Linux、Fedora、Ubuntu 等)。


总结

Linux 桌面系统虽复杂,但每个组件都有明确作用和逻辑关系。

希望这份完整的"解牛图"能成为你探索 Linux 桌面世界的有力工具,让你的 Linux 之旅更加顺畅与愉快。

参考

KubeCon China 2025 见闻

2025年6月15日 17:43

前言

今年 1 月底辞职后,在家过了个年,接着在上海、张家界、重庆、苏州、南京玩了一圈,4 月中旬才回深圳开始找工作。本来看到 6 月就是 KubeCon China 2025,还不太确定自己到时候会不会有时间去。不过很幸运,最后确定 offer 的公司非常重视技术,leader 在面试的时候就说看到我博客里写了 KubeCon 的经历,公司非常鼓励参加这种技术交流活动,去报个 Talk 也完全可以,公司报销所有费用。

于是我在入职还没满一个月的时候,就直接公费出差去香港 KubeCon China 2025 玩了一圈(

也问过同事们是否有想法,但种种原因最后还是只有我一个人参加了(悲

TL;DR

简单的说,今年的 KubeCon China 几乎全都是聊 AI on Kubernetes 的,感觉都可以改名叫 CloudNative AI Con 了。

今年 KubeCon China 只有两天,Talks 明显比去年少了很多,几乎只有去年的一半,所以我也在线上看了许多 KubeCon Europe 2025 的 Talks 作为补充。

总的来说我今年的感觉是:

  • Kubernetes 已经成为一个相当成熟的基座,任何可以在 K8s 上跑的东西最终都会被搬到 K8s 上跑 (
  • AI 让 CloudNative 社区焕发了新生,围绕 AI 在过去两年间涌现了许多新的 CloudNative 项目。AI 话题已经成为了 KubeCon 绝对的主旋律。
    • AI 部署部分主要在讨论 AI 推理,关键技术点:分布式推理、扩缩容与 LLM-Aware 的负载均衡以及 AI 模型分发
    • AIOps 也有好几个讨论,简单的用法就是 ChatBot,复杂点的会尝试使用 Multi-Agent 完成更复杂的任务(比如云成本分析优化)。
      • 快手尝试在超大规模集群中利用 Logs/Metrcis 为每个服务训练一个模型用于动态调整 HPA,实现 SLA 与成本的平衡(如果我记错了概不负责 hhh)。
  • OpenTelemetry 日渐成熟,已经很接近它统一 Logs/Traces/Metrics 三大 Signals 的目标了。
    • 目前已经出现了 Uptrace 之类的大一统观测平台,充分利用了 OTel 的标签来关联 Logs/Traces.
    • 当前的最佳实践是,在 Infra 层面仍然使用传统方式采集 Logs 与 Metrics,而在 APP 层面则改由 OTel 统一采集所有 Logs, Traces 与 Metrics,OTel 会通过 Span ID 把这些数据关联起来, 而且标签语义完全一致。
  • WASM 仍在探寻自己的应用场景,今年介绍的场景主要是在边缘侧跑小模型。

KubeCon China 2025 与 KubeCon Europe 2025 的视频列表如下:

视频相关的 PPT 可以在这里下载(NOTE: 不是所有 Talks 都会上传 PDF):

接下来我会把我听过的一些比较有意思的内容分 Topic 大概介绍下,也会附上对应的视频跟可能的 PPT 链接。

Talks

大一统的 LLM 推理解决方案

Introducing AIBrix: Cost-Effective and Scalable Kubernetes Control Plane for VLLM - Jiaxin Shan & Liguang Xie, ByteDance

AIBrix 是一整套在 K8s 上跑 LLM 分布式推理的解决方案,它包含了:

  • 分布式推理的部署
  • LLM 扩缩容
  • LLM 请求路由(负载均衡)
  • 分布式 KV 缓存
    • 主要是中心化存储这些数据,减少对 HBM 显存的使用,降低显存需求。
  • LoRa 的动态加载

代码:

AIBrix 目前放在了 vllm-project 项目下,stars 也不少,感觉项目还是挺健康的,值得关注。

分布式 LLM 推理的部署

More Than Model Sharding: LWS & Distributed Inference - Peter Pan & Nicole Li, DaoCloud & Shane Wang, Intel

全场最有意思的 Talks 之一,大概介绍了分布式推理的架构、优化点,以及 LWS 的优点与用法。

代码:

简单的说 LWS 是一个专门为 LLM 分布式推理的部署而设计的 CRD, 主要是支持了 LLM 任务的分组调度。

NOTE: 看 issue AIBrix 还有跟 LWS 结合使用的可能性(甚至可能被官方支持):https://github.com/vllm-project/aibrix/issues/843#issuecomment-2728305020

LLM 扩缩容与负载均衡

AI 模型分发

AI Model Distribution Challenges and Best Practices

几位开发者聊怎么在集群里分发数百 GB 大小的 LLM 模型,业界目前的手段:

  • dragonfly
  • juicefs
  • oci model spec + oci volume (k8s 1.33+)

可观测性

  • Antipatterns in Observability: Lessons Learned and How OpenTelemetry Solves Them - Steve Flanders, Splunk
    • 这位也讲得挺有意思,而且有干货。他列举的可观测性方面的 Antipatterns 有
      • Telemetry Data
        • IncompleteInstrumentation - 需要引入zero-code 的 otel sdk 实现自动数据采集
          • metrcis/logs/metrics 三类 signals 不一定都默认启用,具体得看对应的 agent 实现情况
          • 在 k8s 中建议同时禁用将日志输出到 stdout 的功能以及传统的给 prometheus pull 的 /metrics 端点,由 otel agent 全权负责 App-level 三大信号的处理。daemonset 模式的 otel (或者 vector/fluentbit)则主要用于采集 sidecar/k8s 等 Infra-level 的日志。
        • Over-Instrumentation - 需要在 otel-collector 层过滤精简指标,再发送到对应的后端存储。
        • Inconsistent Naming Conventions - 全盘替换为 OpenTelemetry 方案,即可享受统一的命名。
      • Observability Platform
        • Vendor Lock-in - 只选用支持 OTel 标准的平台并使用 Otel 命名规范。
        • Tool Sprawl - 使用大一统的观测平台,如 Uptrace, 支持自动关联 Logs 与 Traces.
        • Underestimating Scalability Requirements - 使用 OTel 采集信号,并选用可拓展性好的后端存储,如 VictoriaMetrics.
      • Company Culture
        • Silos and Lack of Collaboration
        • Lack of Ownership & Accountability
  • KubeCon EU 2025 - From Logs To Insights: Real-time Conversational Troubleshooting for Kubernetes With GenAI - Tiago Reichert & Lucas Duarte, AWS
    • 开场的 OnCall 小品就很真实… 不过 pod pending 1 分钟就电话告警有点夸张了…
    • 演完小品才开始讲正式内容,大体上就是把日志用 embed 模型编码后存在 OpenSearch 里做 RAG,还给了 ChatBot k8s readonly 的权限(ban 掉了 secrets access),然后通过 Deepseek/Claude 问答来解决问题。
    • 代码: https://github.com/aws-samples/sample-eks-troubleshooting-rag-chatbot
  • Portrait Service: AI-Driven PB-Scale Data Mining for Cost Optimization and Stability Enhancement - Yuji Liu & Zhiheng Sun, Kuaishou
    • 讲快手怎么在 20 万台机器的超大规模集群上做稳定性管理与性能优化。
    • 介绍得比较浅,大概就是会收集集群中非常多的信息,用一套大数据系统持续处理,再丢给后面训练专用模型,每个服务都可能有一个专门的资源优化模型,用它来做最终的资源优化。
    • 这一套可能太重了,可以借鉴,但是在我目前的工作场景中不太有用(规模太小)。

Service Mesh

Ingress-Nginx

The Next Steps for Ingress-NGINX and the Ingate Project - Jintao Zhang, Kong Inc.

Ingress-NGINX 终于要寿终正寝了,它的继任者叫 InGate,不过 InGate 目前还几乎是个空壳(

代码

安全性

Keynote: Who Owns Your Pod? Observing and Blocking Unwanted Behavior at eBay With eBPF

主要就介绍 cilium 家的 tetragon, 一个基于 eBPF 的 K8S 安全工具,跟 apparmor 感觉会有点类似,但是能做到更精细的权限管理。

朋友跟我 Argue 这种工具不是很有必要,应该用 GitOps 流程,然后将安全检查前置在 CICD 流水线中。

云成本分析与优化

KubeCon EU 2025 - Autonomous Al Agents for Cloud Cost Analysis - Ilya Lyamkin, Spotify

实现一个会自动做 Plan,编写 SQL 与 Python 进行云成本分析的 Multi-Agent 系统,很有参考价值。

WASM 相关

Keynote: An Optimized Linux Stack for GenAI Workloads - Michael Yuan, WasmEdge

讲怎么用 WasmEdge + LlamaEdge 在边缘设备上跑 LLM 小模型,还是挺有意思的。

如何搭建一个 AI 工作流

KubeCon EU 2025 - Tutorial: Build, Operate, and Use a Multi-Tenant AI Cluster Based Entirely on Open Source

长度超过一个小时的教程,IBM 出品。装了一堆东西,包括 Kueue, Kubeflow, PyTorch, Ray, vLLM, and Autopilot

Non-Tech

参加 KubeCon 其实不仅仅是听一听过去一年技术方面的变化与进展,还有个很重要的目的是跟各个方向的开发者们 Social, 也可以说是某种大型网友见面会(

今年拉到了 @scruelt, @ox-warrior 等几位朋友一起去 KubeCon 玩,然后在会场又陆续跟@cookie, @rizumu, @ayakaneko 以及 @dotnetfx35 见面闲聊瞎扯了一波,收获了 @rizumu@ayakaneko 用 3D 打印机打印的 Kubernetes 跟 Go 小饼干各一枚,顺便传教了 NixOS(

面基成功!顺便传教 NixOS

拿到的 K8s/Go 小饼干以及 Istio 冰箱贴

Day 2 上午发现没啥想听的,发现有个 Peer Group Meeting 参加,不过需要先 sign up. 跟@scruelt 一起去报了名,本来还担心只提前 20 分钟 sign up 会不会没机会了,结果到会议室发现只有 3 个 mentors 在场,于是就我们俩跟他们随便闲聊 emmm 三位 mentors 分别是 Nate Waddington (Head of Mentorship & Documentatio, Canada),Kohei Ota(CNCF Ambassador, Japan)以及 Amit DSouza(co-founder of Odyssey Cloud, Australia),另外聊到半途一位 Cisico 的老哥也加入了进来。

基本就是闲聊,@scruelt 口语比我好,而且刚辞职也有许多问题想问,绝大部分话题都是他提出来的。我因为最近诸事皆顺,反而没啥想问的。

进了 Peer Group Meeting 发现只有 Mentors hhh

最后就放些图吧。

欢迎光临 KubeCon China 2025

先领个 T 恤嘿嘿

茶歇时间

SUSE 的毛绒玩具好想要!

大 SUSE 上一只小 SUSE

用 tetragon 限制文件访问

LWS 的 Talk,在讲 PD 分离

Switch 店在宣传 Miku Boxing

累计有三个朋友 KubeCon 期间在这里买了 Switch 2,它这波血赚

我的所有'战利品' hhh

登机了,再见深圳

这是我第几次坐飞机来着?

总之玩得很开心,明年再见!

我的 2024 - 稳中求进、热爱生活

2025年1月7日 17:43

前言

相比跌宕起伏的 2023 年, 2024 年我少了一些焦虑与内耗, 花在技术上的时间也少了不少. 我将大量的精力转移到了徒步旅行上, 享受了诸多旅行的快乐.

可能因为 23 年写的太多,24 年少了些创作的热情,也因此这份年终一直拖着。本来想效仿去年的风格过一遍一整年中比较有意义的事情,但是不太能下手。

不过,总得写点什么给这一年画上一个句号,今天总算交差了.

2024 年 Highlights

1. 旅行与徒步

2024 年跟 2023 年最大的变化, 是我 3 月份抽时间办了港澳通行证跟护照, 在香港跟内地完成了多次徒步旅行, 今年的最后一天也是在香港维多利亚港的烟花中度过的. 这篇文章的封面图就是香港维多利亚港的跨年烟花(因为自己拍摄的角度不太好, 所以网上找了这张图).

我在 2024 年的徒步旅行与 City Walk 记录如下:

  • 3/30 - SRE 小组第一次以户外运动的形式进行团建,一起爬了凤凰山(鲲鹏径)
  • 4/4 - 跟我妹一起逛了仙湖植物园,很多奇花异草,另外回程意外爬上了梧桐山,给我俩都累坏了, 当然也很开心
  • 4/14 - 第一次去香港玩,从维多利亚港沿着海岸线一路徒步到坚尼地城,然后坐地铁回家,海岸线很美,香港也有独特的风土人情在
    • 解锁成就 - 第一次出境中国大陆
  • 5/2 ~ 5/3 - 单人刷了一遍香港麦里浩径二段,从北潭凹管理站下车开始徒步,沿着麦理浩径二段又走回到北潭凹站,算是环线,大概 20 到 30 公里的样子,中间在西湾村租帐篷露营了一晚上
    • 解锁成就 - 第一次露营、第一次在山林里孤身赶夜路

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

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

  • 5/18 - 5/19 - 单人背着 17 公斤的背包重装徒步麦理浩径三段,中间还解锁了一些支线,全程走了 14 公里,走走停停近 8 个小时(体力不够所以走得很慢),夜间在水浪窝营地露营了一晚上
    • 解锁成就 - 第一次重装徒步

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

      清晨 7 点多,解决卫生问题,顺便随处走走,发现营地标牌

  • 5/25 - 继续单人徒步麦径四段,坐九巴 299X 路到大浪窝站下车开始徒步,从四段起点出发的时间为 13:20,到达大老山隧道站时已经是 22:20, 全程差不多 9 个小时,超过 21 公里,背的还是 16kg 的重装背包
    • 解锁成就 - 第一次重装徒步超过 20 公里,到目前为止的人生巅峰了

      这应该是四段风景最好的一段, 可惜雾太大

  • 6/22 - 6/23 - 徒步至铅矿坳营地露营
    • 解锁成就 - 这次带了卡式炉气罐跟炊具,第一次户外做饭,很香
  • 6/29 - 与同事四人组团麦理浩径一二段徒步
    • 从北潭凹反穿,一路走到万宜水库东坝,因为计划单日徒步,这次只背了 30 升小包,运动量相比之前几次并不大
    • 解锁成就 - 第一次与同事组团长距离徒步、第一次大雨中徒步(有风险,不建议冒雨上山)
  • 08/03 - 跟老妹一起在香港维多利亚港沿海漫步,人比之前五一假期少了太多,体验非常好!可以悠闲地慢慢走,拍照,聊天
  • 8/21 - 8/23 - 在香港参加为期三天的 KubeCon China 2024, 顺便跟着朋友逛香港

    主会场过厅,海景不错的

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

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

    香港夜景,相当繁华哪

    Linus

    咱的合影

  • 10/18 - 10/19 - 公司团建,在惠州东江玩皮划艇,18 公里,挺愉快

    跟同事划皮划艇

    江边放点烟花,不得不说公司是会玩的

  • 10/26 - 10/27 - 跟同事武功山徒步,10/25 提前下班坐高铁到长沙休息,10/26 早上坐高铁到萍乡再叫车送到武功山下开始徒步。我们是反穿,第一天徒步到云中峰客栈住宿,第二天上午继续徒步到武功山大门口,中间乘了两段下山索道。两天武功山都起大雾,没看到日出,视野也差了许多,但云海也还算不错,在山脊线上走,两边都是悬崖,而且还好大的风,还是有点刺激的

    10/27 凌晨, 喝着热水欣赏早晨四五点的山景

    10/27 快清晨六点了,对面山上的早餐叫卖声隔这么远都听得到

    10/27 从云中峰客栈再次出发

  • 11/23 - SRE 小组深圳梅林登山徒步, 路程大概 14 公里, 早上 9 点 30 从梅林水库大坝出发,下午 14 点 50 到终点, 全程 5 个多小时
  • 11/24 - 到香港招商局码头看海南舰(075 两栖攻击舰), 不过没拿到门票上船参观
  • 11/30 - 陪朋友香港办银行卡, 顺便在皇后大道跟维多利亚港一直 City Walk 到晚上九点

    沿着皇后大道走到了一条市集小街,节日氛围浓厚

    K11 商场海边的圣诞树布景,好多人拍照

    维港渡轮上回头,能看到标志性的摩天轮

  • 12/31 - 2024 年最后一天, 在香港 City Walk, 晚上到维多利亚港看跨年烟花,人山人海,很有氛围

    我拍的烟花,位置不好效果差挺多

    我拍的烟花 - 2

    我拍的烟花 - 3

关于香港徒步旅行的细节, 我之前专门写过篇文章, 有兴趣的可以看看:

总的来说, 我 2024 年的运动量远超 2023 年, 这是一个很好的开始.

2. 业余技术

今年业余技术上的进展比较符合去年底的期望.

首先是在我 Homelab 上更深入地使用了 NixOS 系统, 其次也发表了一些不错的 NixOS 文章, 还给 Nixpkgs 提了一些 PR, 另外去年做的几个 Nix 相关开源项目的 stars 也持续增长.

其次是在 Linux 系统编程跟 Rust 语言方面取得了不错的进展, 学习这些技术的过程中, 对过去遇到的许多 Linux 系统故障也有了更深的理解. 算是年底两个月最有价值的技术突破.

2024 年我写过的一些技术文章:

24 年我写的文章相较 23 年少了不少, 不过整体质量是有所提高的. 考虑到 24 年我在旅行徒步以及关心家人上花了许多时间, 这个成绩也可以接受.

最后再对比下从 2024 年 12 月 31 日到现在,我的 GitHub Metrics 统计数据:

2023/12/31 GitHub 统计数据

2025/01/01 GitHub 统计数据

2024 年我没有开什么新的项目, 上述成绩基本都是 2023 年的旧项目 Stars 稳步增长带来的.

3. 工作

工作上, 2024 也仍然是按部就班的一年, 我有做一些新技术的尝试, 但总体来说变化不多.

与往年不同的是, 今年在工作上遇到的更多是技术之外的问题. 一些团队协作、沟通、管理等问题, 让我认识到了公司与各个团队的另一面, 以及人的复杂性.

单纯从工作内容的角度看, 工作越来越得心应手, 相对的也就越来越难以激发我的兴趣与动力, 对 ADHDer 而言要按部就班地把这类工作做好, 挑战很大.

总之多方因素影响下, 我在 24 年底不想干了, 遂向 leader 提出了辞职, 目前已经确定 last day 是 2025 年 1 月底, 正在交接工作中.

我 2021 年入职这家公司, 到离开大概是 3 年零 10 个月, 一段说长不长说短不短的时光.

这是我从业生涯的第二份工作, 回过头看, 21 年刚入职时我还是萌新一个, 做事情都很小心翼翼, 当时我对公司的评价

梦幻般的待遇,不限量的三餐供应,窗明几净的落地窗工位,这一切都像是在做梦

还有 22 年初发过的推文也是相当正面的:

新办公区真好呐~

值此良辰美景,好想整个榻榻米坐垫,坐在角落的落地窗边工作🤣
那种使用公共设施工(mo)作(yu)的乐趣,以及平常工位见不到的景色交相辉映,是不太好表述的奇妙体验 pic.twitter.com/FASffzw8N3

— ryan4yin | 於清樂 (@ryan4yin) January 17, 2022

从入职一直到 24 年上半年, 我在这里的工作体验都是很不错的.

只能说很感慨吧, 三年多的时间, 我在这里学到了很多. 我很感谢我的两任 leader, 他们都给我了很多机会, 让我能够在工作中不断成长. 也很感谢 SRE 的其他同事, 在我遇到困难时给予了我很多帮助.

后会有期!

4. 阅读

2024 年我在阅读方面, 最大的亮点应该是终于读完了《Linux/Unix 系统编程手册(上册)》, 并且使用 Rust 做了不少习题.

2024 年完整的已读书目:

  • 《户外旅行终极指南:基础装备、露营技能、交通方式、饮食、环境和急救》:内容很多,但都比较入门级,好处是图很多,读着很轻松,几个小时就能走马观花全过一遍。
  • Programming Kubernetes - Developing Cloud Native Applications: 2022 年开始读的书,但当时没啥兴趣。最近在照猫画虎写 karpenter provider,有了些编程经验后又对它产生了兴趣。书不厚,花了三个小时走马观花全读了一遍,代码内容大都跳过了(不少也过时了,譬如还在介绍 dep),做了些笔记。挺有帮助,帮我系统地梳理了最近折腾 karpenter 学到的 operator 编程相关知识。
  • 走出荒野
    • 2021 年 2 月读此书的评价:「没读书的内容前,我完全没预料到作者的人生曾如此不堪。 最近刚离职,毕业后的第一份工作就这样结束了。心里好多想法,也好想多看看这个世界。 嗯,有点想来上一次徒步旅行了哈哈。」
    • 2024 年 7 月重读评价:「今年我爱上了徒步,重读此书,又有新知。我想徒步也类似跑步,也存在村上春树所言的跑者蓝调。今年已经在香港麦理浩径上完成了 5 次徒步,越发上瘾。我想我也该带老妹体验下,见山见水见自我。」
  • Linux/Unix 系统编程手册(上册)

未读完书目:

年初定的目标是每月一本书, 但实际上只读完了 4 本, 25 年再接再厉吧!

2025 年展望

我在去年年终总结的文末写了, 我对自己 2024 年的期许是「工作与业余爱好上稳中求进,生活上锻炼好身体、多关心家人」,感觉确实应验了。由衷地喜欢与感谢这一年来乐观、开朗、积极的自己, 也感谢身边的亲人、朋友、同事.

人的一生, 尤其是 ADHDer 的一生要怎么过才能拥有鲜活、快乐且充实的人生? 我们天生只有在自己喜欢的事情上才能摆脱拖延症并获得足够的专注力, 这就注定了我们无法适应「稳定、枯燥」的工作与生活.

2025 年, 我不急着找下一份工作, 计划先 gap 几个月, 调整下自己的心态, 重新审视自己的职业生涯, 以及未来的规划.

世界那么大, 我想去看看, 也许在旅行中能找到一些答案.

因此, 我给自己定的 2025 年目标是:

深入浅出 Linux, 徒步中国、徒步世界

作为一名从未出过国的 IT 农民工, 我对世界上其他国家的认识仅仅停留在书本与各种网络资料上. 为了能够亲眼见识下中国以外的世界, 我计划在 2025 年开始走出国门, 亲身体验不同国家的文化与风景.

我已经办好了日本签证, 正在办韩国签证, 打算先去这两个国家徒步旅行.

如果签证顺利的话, 我在日韩之后还想去尼泊尔、马来西亚、澳洲跟欧洲旅行. 但这个并没有那么急, 如果 2025 年 gap 的这几个月不够用的话, 未来还有很多机会.

除了去国外旅行满足我对「外国」的好奇心, 我也很想在 2025 年去亲眼见证 960 万平方公里的中国大地, 亲眼看看这片土地上的鬼斧神工. 不过暂时还没有很明确的计划, 中国的风景太多太美, 也许我会先去青海, 又或者是广西?

路还很长, 2025 年, 让我用双脚去丈量这个世界

Carpe Diem. Seize The Day, Boys. Make Your Lives Extraordinary. – 《死亡诗社》

个人数据安全不完全指南

2024年1月30日 13:48

零、前言

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

  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 在国内无官方购买渠道,而且价格不菲,只买一个 YubiKey 的话还存在丢失的风险。

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

三、SSH 密钥管理

2.1 SSH 密钥的生成

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

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

2.2 SSH 密钥的安全性

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

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

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

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

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

那么,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.
时间阶段 (OpenSSH 版本) ssh-keygen 默认密钥类型 ssh-keygen 默认密钥长度 私钥 KDF 算法 (带 passphrase 时) 默认/主要 KEX 算法 默认/主要对称加密算法
OpenSSH 4.x (约2005 - 2008) RSA 2048 位 (RSA, 自 4.0 起) 基于 MD5 (OpenSSL PEM 格式) diffie-hellman-group1-sha1, diffie-hellman-group-exchange-sha1 AES-CBC (更普遍), 3DES-CBC; HMAC-SHA1
OpenSSH 5.x (约2009 - 2013) RSA 2048 位 (RSA) 基于 MD5 (OpenSSL PEM 格式) ecdh-sha2-nistp256 等 ECDH 系列引入 (自 5.7), DH 仍常见 AES-CTR (自 5.2 起优先于 CBC), AES-CBC; HMAC-SHA1
OpenSSH 6.x (约2014 - 2015) RSA (Ed25519 于 6.5 引入) 2048 位 (RSA) bcrypt_pbkdf (新格式, 自 6.5; Ed25519 默认, RSA 需 -o) curve25519-sha256 (自 6.5 起引入并优先), ECDH 系列 chacha20-poly1305@openssh.com (自 6.5), AES-GCM (自 ~6.2); CBC 模式于 6.7 默认禁用
OpenSSH 7.x (约2015 - 2018) RSA 2048 位 (RSA) bcrypt_pbkdf (自 7.8 起所有新密钥默认) curve25519-sha256, ECDH 系列; diffie-hellman-group1-sha1 于 7.0 禁用; rsa-sha2-256/512 签名 (自 7.2) chacha20-poly1305, AES-GCM (AEAD 优先); 3DES-CBC 从客户端默认移除 (7.4)
OpenSSH 8.x (约2019 - 2021) RSA (Ed25519 逐渐流行) 3072 位 (RSA, 自 8.0 起) bcrypt_pbkdf curve25519-sha256, ECDH 系列; ssh-rsa (SHA1 签名) 于 8.8 禁用主机认证; Ed25519 签名优先 (自 8.5) chacha20-poly1305, AES-GCM
OpenSSH 9.x (约2022 - 至今) Ed25519 (自 9.5 起) 256 位 (Ed25519); 3072 位 (若选 RSA) bcrypt_pbkdf PQC 混合 KEX: sntrup761x25519-sha512@openssh.com (自 9.0 默认); mlkem768x25519-sha256 (自 9.9 默认提供); Terrapin 缓解 (9.6) chacha20-poly1305, AES-GCM
OpenSSH 10.0 (预计 2025年4月) Ed25519 256 位 (Ed25519); 3072 位 (若选 RSA) bcrypt_pbkdf mlkem768x25519-sha256 (PQC KEX 默认); 服务器端默认禁用有限域 DH (modp); DSA 完全移除 chacha20-poly1305 (最优先), AES-GCM (优先于 AES-CTR)

所以从 2014 年 1 月发布的 OpenSSH 6.5 开始,才可使用 ed25519 密钥,它的 passphrase 默认使用 bcrypt_pbkdf 生成的。而对于 RSA 类型的密钥,一直到 2018-08-24 发布的 OpenSSH 7.8 才从 MD5 改到 bctypt_pbkdf.

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

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

再看下各主流操作系统与 OpenSSH 的对应关系:

OS Distro Version Year 大致的 OpenSSH 版本
Ubuntu (LTS)
Ubuntu 18.04 LTS 2018 OpenSSH 7.6p1
Ubuntu 20.04 LTS 2020 OpenSSH 8.2p1
Ubuntu 22.04 LTS 2022 OpenSSH 8.9p1
Ubuntu 24.04 LTS 2024 OpenSSH 9.6p1
Debian (稳定版)
Debian 9 (Stretch) 2017 OpenSSH 7.4p1
Debian 10 (Buster) 2019 OpenSSH 7.9p1
Debian 11 (Bullseye) 2021 OpenSSH 8.4p1
Debian 12 (Bookworm) 2023 OpenSSH 9.2p1
macOS (主要版本)
macOS 10.14-10.15 2018 OpenSSH 7.9p1, LibreSSL 2.7.3
macOS 11 (Big Sure) 2020 OpenSSH 8.1p1+
macOS 12 (Monterey) 2021 OpenSSH 8.6p1+
macOS 13 (Ventura) 2022 OpenSSH 9.0p1+
macOS 14 (Sonoma) 2023 OpenSSH 9.3p1+
macOS 15 (Sequoia) 2024 OpenSSH 9.6p1+

考虑到大部分 Linux 用户或 SysAdm 都没有密码学基础,大概率不知道 KDF、Rounds 是什么意思,有理由怀疑很多人会使用默认参数生成密钥,由此可以推断出:

  • 2023 年及之前发布的 Linux/macOS 使用的 OpenSSH 版本都低于 9.5
    • 结论:绝大部分用户都仍然在使用 RSA 密钥
  • 2020 年各主流 OS 才陆续升级到 OpenSSH 8.x
    • 结论:在这之前,绝大部分用户都仍然在使用 RSA 2048 位密钥
  • 2019 年之前各主流 OS 的 OpenSSH 发行版大都低于 7.8
    • 结论:在这些老系统上生成的密钥,几乎全部都仍然在使用 PEM 格式的 RSA 密钥,这种密钥使用 MD5 派生密钥,加了密码也几乎等于裸奔。

如果你使用的也这种比较老的密钥类型,那千万别觉得自己加了 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 都使用上述参数重新生成了一遍。

2.3 SSH 密钥的分类管理

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

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

因此,我现在的做法是:

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

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

最后再说明一点:OpenSSH 密钥并不是生成一次然后就可以高枕无忧了,为了确保足够安全性,也必须隔几年更换一次新密钥

2.4 SSH CA - 更安全合理的 SSH 密钥管理方案?

搜到些资料:

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.

其他相关资料:

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

3.1 pass 使用的 GPG 够安全么?

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

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

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

    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 仓库多端加密同步。

七、需要在多端访问的重要个人数据

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

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

1. 个人笔记

不包含个人隐私的笔记,我直接用公开 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.

2. 照片、视频等其他个人数据

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

八、桌面电脑與 Homelab 的数据安全

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

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

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

  • 桌面主机
    • 启用 LUKS2 全盘加密 + Secure Boot,在系统启动阶段需要输入 passphrase 解密 NixOS 系统盘才能正常进入系统。
      • LUKS2 的 passphrase 为一个比较长的密码学随机字符串。
      • LUKS2 的所有安全设置全拉到能接受的最高(比较重要的是 --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
      • 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 的描述完全一致

macOS/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 密钥将会成为整个方案的短板。

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

  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 的心态去持续推进这件事情,什么时候兴趣来了就推进一点点。

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

EE 入门(二) - 使用 ESP32 与 SPI 显示屏绘图、显示图片、跑贪吃蛇

2023年3月5日 21:57

零、硬件准备与依赖库调研

之前淘货买了挺多显示屏的,本文使用的是这一块:

开发板是 ESP-WROOM-32 模组开发板。其他需要的东西:杜邦线、面包板、四个 10 K$\Omega$ 电阻、四个按键。

至于需要的依赖库,我找到如下几个 stars 数较高的支持 ILI9488 + ESP32 的显示屏驱动库:

  • Bodmer/TFT_eSPI: 一个基于 Arudino 框架的 tft 显示屏驱动,支持 STM32/ESP32 等多种芯片。
  • lv_port_esp32: lvgl 官方提供的 esp32 port,但是几百年不更新了,目前仅支持到 esp-idf v4,试用了一波被坑了,不建议使用。
  • esp-idf/peripherals/lcd: ESP 官方的 lcd 示例,不过仅支持部分常见显示屏驱动,比如我这里用的 ili9488 官方就没有。

总之强烈推荐 TFT_eSPI 这个库,很好用,而且驱动支持很齐全。

一、开发环境搭建、电路搭建与测试

1. 创建项目并配置好环境

ESP32 开发有好几种方式:

  1. vscode 的 esp-idf 插件 + 官方的 esp-idf 工具
  2. vscode 的 platformio 插件 + arudino 框架

Bodmer/TFT_eSPI 这个依赖库两种方式都支持,不过看了下官方文档,仓库作者表示 ESP-IDF 的支持是其他人提供的,他不保证能用,所以稳妥起见我选择了 PlatformIO + Arduino 框架作为开发环境。

首先当然是创建一个空项目,点击 VSCode 侧栏的 PlatformIO 图标,再点击列表中的PlatformIO Core CLI 选项进入 shell 执行如下命令:

pio project init --ide=vscode -d tft_esp32_arduino

这条命令会创建一个空项目,并配置好 vscode 插件相关配置,这样就算完成了一个空的项目框架。

1. 显示屏接线与项目参数配置

网上简单搜了下 ESP32 pinout,找到这张图,引脚定义与我的 ESP32 开发板完全一致,用做接线参考:

可以看到这块 ESP32 开发板有两个 SPI 端口:HSPI 跟 VSPI,这里我们使用 HSPI,那么 MOSI/MISO/SCK 三个引脚的接线必须与上图的定义完全一致。而其他引脚随便找个普通 GPIO 口接上就行。

此外背光灯的线我试了下接 GPIO 口不好使,建议直接接在 3V3 引脚上(缺点就是没法通过程序关闭背光,问题不大)。

我的接线如下:

使用 wokwi.com 制作的示意图

接线实操

线接好后需要更新下 PlatformIO 项目根目录 platformio.ini 的配置,使其显示屏引脚相关的参数与我们的接线完全对应起来,这样才能正常驱动这个显示屏。

这里我以驱动库官方提供的模板Bodmer/TFT_eSPI/docs/PlatformIO 为基础,更新了其构建参数对应的引脚,加了点注释,得到的内容如下(如果你的接线与我一致,直接抄就行):

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
  bodmer/TFT_eSPI@^2.5.0
  Bodmer/TFT_eWidget@^0.0.5
monitor_speed = 115200
build_flags =
  -Os
  -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
  -DUSER_SETUP_LOADED=1

  ; Define the TFT driver, pins etc here:
  ; 显示屏驱动要对得上
  -DILI9488_DRIVER=1
  # 宽度与高度
  -DTFT_WIDTH=480
  -DTFT_HEIGHT=320
  # SPI 引脚的接线方式,
  -DTFT_MISO=12
  -DTFT_MOSI=13
  # SCLK 在显示屏上对应的引脚可能叫 SCK,是同一个东西
  -DTFT_SCLK=14
  -DTFT_CS=15
  # DC 在显示屏上对应的引脚可能叫 RS 或者 DC/RS,是同一个东西
  -DTFT_DC=4
  -DTFT_RST=2
  # 背光暂时直接接在 3V3 上
  ; -DTFT_BL=27
  # 触摸,暂时不用
  ;-DTOUCH_CS=22
  -DLOAD_GLCD=1
  # 其他配置,保持默认即可
  -DLOAD_FONT2=1
  -DLOAD_FONT4=1
  -DLOAD_FONT6=1
  -DLOAD_FONT7=1
  -DLOAD_FONT8=1
  -DLOAD_GFXFF=1
  -DSMOOTH_FONT=1
  -DSPI_FREQUENCY=27000000

修好后保存修改,platformio 将会自动检测到配置文件变更,并根据配置文件下载 Arduino/ESP32 工具链,更新构建配置、拉取依赖库(建议开个全局代理,不然下载会贼慢)。

3. 测试验证

现在找几个 demo 跑跑看,新建文件 src/main.ino,从如下文件夹中随便找个 demo copy 进去然后编译上传,看看效果:

可以直接从 libdeps 中 copy examples 代码过来测试:cp .pio/libdeps/esp32dev/TFT_eSPI/examples/480\ x\ 320/TFT_Meters/TFT_Meters.ino src/main.ino

我跑出来的效果:

二、显示图片、文字

这需要首先将图片/文字转换成 bitmap 格式的 C 代码,可使用在线工具javl/image2cpp 进行转换,简单演示下:

注意高度与宽度调整为与屏幕大小一致,设置放缩模式,然后色彩改为 RGB565,最后上传图片、生成代码。

将生成好的代码贴到 src/test_img.h 中:

// We need this header file to use FLASH as storage with PROGMEM directive:

// Icon width and height
const uint16_t imgWidth = 480;
const uint16_t imgHeight = 320;

// 'evt_source', 480x320px
const uint16_t epd_bitmap_evt_source [] PROGMEM = {
  // 这里省略掉图片内容......
}

然后写个主程序 src/main.ino 显示图像:

#include <TFT_eSPI.h>       // Hardware-specific library

TFT_eSPI tft = TFT_eSPI();  // Invoke custom library

// Include the header files that contain the icons
#include "test_img.h"

void setup()
{
  Serial.begin(115200);
  tft.begin();
  tft.setRotation(1);	// landscape

  tft.fillScreen(TFT_BLACK);
  // Swap the colour byte order when rendering
  tft.setSwapBytes(true);

  // 显示图片
  tft.pushImage(0, 0, imgWidth, imgHeight, epd_bitmap_evt_source);

  delay(2000);
}

void loop() {}

编译上传,效果如下:

三、写个极简贪吃蛇游戏

N 年前我写的第一篇博客文章,是用 C 语言写一个贪吃蛇,这里把它移植过来玩玩看~

我的旧文章地址为:贪吃蛇—C—基于easyx图形库(下):从画图程序到贪吃蛇【自带穿墙术】 , 里面详细介绍了程序的思路。

那么现在开始代码移植,TFT 屏幕前面已经接好了不需要动,要改的只有软件部分,还有就是添加上下左右四个按键的电路。

首先清空 src 文件夹,新建文件 src/main.ino,内容如下,其中主要逻辑均移植自我前面贴的文章:

#include <math.h>
#include <stdio.h>
#include <TFT_eSPI.h> // Hardware-specific library

#define WIDTH 480
#define HEIGHT 320

// 四个方向键对应的 GPIO 引脚
#define BUTTON_UP_PIN     5
#define BUTTON_LEFT_PIN   18
#define BUTTON_DOWN_PIN   19
#define BUTTON_RIGHT_PIN  21

TFT_eSPI tft = TFT_eSPI(); // Invoke custom library

typedef struct Position // 坐标结构
{
  int x;
  int y;
} Pos;

Pos SNAKE[3000] = {0};
Pos DIRECTION;
Pos EGG;
long SNAKE_LEN;

void setup()
{
  Serial.begin(115200);
  tft.begin();
  tft.setRotation(1); // landscape

  tft.fillScreen(TFT_BLACK);
  // Swap the colour byte order when rendering
  tft.setSwapBytes(true);

  // initialize the pushbutton pin as an input: the default state is LOW
  pinMode(BUTTON_UP_PIN, INPUT);
  pinMode(BUTTON_LEFT_PIN, INPUT);
  pinMode(BUTTON_DOWN_PIN, INPUT);
  pinMode(BUTTON_RIGHT_PIN, INPUT);

  init_game();
}

void loop()
{
  command(); // 获取按键消息
  move();    // 修改头节点坐标-蛇的移动
  eat_egg();
  draw(); // 作图
  eat_self();
  delay(100);
}

void init_game() {
  // 初始化小蛇
  SNAKE_LEN = 1;
  SNAKE[0].x =  random(50, WIDTH - 50); // 头节点位置随机化
  SNAKE[0].y =  random(50, HEIGHT - 50);
  DIRECTION.x = pow(-1, random()); // 初始化方向向量
  DIRECTION.y = 0;
  creat_egg();

  Serial.println("GAM STARTED, Having Fun~");
}

void creat_egg()
{
  while (true)
  {
    int ok = 0;
    EGG.x = random(50, WIDTH - 50); // 头节点位置随机化
    EGG.y = random(50, HEIGHT - 50);
    for (int i = 0; i < SNAKE_LEN; i++)
    {
      if (SNAKE[i].x == 0 && SNAKE[i].y == 0)
        continue;
      if (fabs(SNAKE[i].x - EGG.x) <= 10 && fabs(SNAKE[i].y - EGG.y) <= 10)
        ok = -1;
      break;
    }
    if (ok == 0)
      return;
  }
}

void command() // 获取按键命令命令
{
  if (digitalRead(BUTTON_LEFT_PIN) == HIGH) {
      if (DIRECTION.x != 1 || DIRECTION.y != 0)
      { // 如果不是反方向,按键才有效
        Serial.println("Turn Left!");
        DIRECTION.x = -1;
        DIRECTION.y = 0;
      }
  } else if (digitalRead(BUTTON_RIGHT_PIN) == HIGH) {
      if (DIRECTION.x != -1 || DIRECTION.y != 0)
      {
        Serial.println("Turn Right!");
        DIRECTION.x = 1;
        DIRECTION.y = 0;
      }
  } else if (digitalRead(BUTTON_UP_PIN) == HIGH) {
      if (DIRECTION.x != 0 || DIRECTION.y != 1)
      {  // 注意 Y 轴,向上是负轴,因为屏幕左上角是原点 (0,0)
        Serial.println("Turn Up!");
        DIRECTION.x = 0;
        DIRECTION.y = -1;
      }
  } else if (digitalRead(BUTTON_DOWN_PIN) == HIGH) {
      if (DIRECTION.x != 0 || DIRECTION.y != -1)
      {
        Serial.println("Turn Down!");
        DIRECTION.x = 0;
        DIRECTION.y = 1;
      }
  }
}

void move() // 修改各节点坐标以达到移动的目的
{
  // 覆盖尾部走过的痕迹
  tft.drawRect(SNAKE[SNAKE_LEN - 1].x - 5, SNAKE[SNAKE_LEN - 1].y - 5, 10, 10, TFT_BLACK);

  for (int i = SNAKE_LEN - 1; i > 0; i--)
  {
    SNAKE[i].x = SNAKE[i - 1].x;
    SNAKE[i].y = SNAKE[i - 1].y;
  }
  SNAKE[0].x += DIRECTION.x * 10; // 每次移动10pix
  SNAKE[0].y += DIRECTION.y * 10;

  if (SNAKE[0].x >= WIDTH) // 如果越界,从另一边出来
    SNAKE[0].x = 0;
  else if (SNAKE[0].x <= 0)
    SNAKE[0].x = WIDTH;
  else if (SNAKE[0].y >= HEIGHT)
    SNAKE[0].y = 0;
  else if (SNAKE[0].y <= 0)
    SNAKE[0].y = HEIGHT;
}

void eat_egg()
{
  if (fabs(SNAKE[0].x - EGG.x) <= 5 && fabs(SNAKE[0].y - EGG.y) <= 5)
  {
    // shade old egg
    tft.drawCircle(EGG.x, EGG.y, 5, TFT_BLACK);
    creat_egg();
    // add snake node
    SNAKE_LEN += 1;
    for (int i = SNAKE_LEN - 1; i > 0; i--)
    {
      SNAKE[i].x = SNAKE[i - 1].x;
      SNAKE[i].y = SNAKE[i - 1].y;
    }
    SNAKE[0].x += DIRECTION.x * 10; // 每次移动10pix
    SNAKE[0].y += DIRECTION.y * 10;
  }
}

void draw() // 画出蛇和食物
{
  for (int i = 0; i < SNAKE_LEN; i++)
  {
    tft.drawRect(SNAKE[i].x - 5, SNAKE[i].y - 5, 10, 10, TFT_BLUE);
  }
  tft.drawCircle(EGG.x, EGG.y, 5, TFT_RED);
}

void eat_self()
{
  if (SNAKE_LEN == 1)
    return;
  for (int i = 1; i < SNAKE_LEN; i++)
    if (fabs(SNAKE[i].x - SNAKE[0].x) <= 5 && fabs(SNAKE[i].y - SNAKE[0].y) <= 5)
    {
      delay(1000);
      tft.setTextColor(TFT_RED, TFT_BLACK);
      tft.drawString("GAME OVER!", 200, 150, 4);
      delay(3000);

      setup();
      break;
    }
}

代码就这么点,没几行,接下来我们来接一下按键电路,这部分是参考了 arduino 的官方文档How to Wire and Program a Button

接线方式如下,主要原理就是通过 GND 接线,使四个方向键对应的 GPIO 口默认值为低电平。当按键按下时,GPIO 口会被拉升成高电平,从而使程序识别到该按键被按下。

接线示意图如下(简单起见,省略了前面的显示屏接线部分):

使用 wokwi.com 制作的示意图

现在运行程序,效果如下(手上只有两个按键,所以是双键模式请见谅…):

写给开发人员的实用密码学(五)—— 密钥交换 DHKE 与完美前向保密 PFS

2022年3月1日 17:15

本文主要翻译自 Practical-Cryptography-for-Developers-Book,笔者额外补充了 DHKE/ECDH 的代码示例,以及「PFS 完美前向保密协议 DHE/ECDHE」一节。

一、前言

在密码学中密钥交换是一种协议,功能是在两方之间安全地交换加密密钥,其他任何人都无法获得密钥的副本。通常各种加密通讯协议的第一步都是密钥交换。密钥交换技术具体来说有两种方案:

  • 密钥协商:协议中的双方都参与了共享密钥的生成,两个代表算法是 Diffie-Hellman (DHKE) 和 Elliptic-Curve Diffie-Hellman (ECDH)
  • 密钥传输:双方中其中一方生成出共享密钥,并通过此方案将共享密钥传输给另一方。密钥传输方案通常都通过公钥密码系统实现。比如在 RSA 密钥交换中,客户端使用它的私钥加密一个随机生成的会话密钥,然后将密文发送给服务端,服务端再使用它的公钥解密出会话密钥。

密钥交换协议无时无刻不在数字世界中运行,在你连接 WiFi 时,或者使用 HTTPS 协议访问一个网站,都会执行密钥交换协议。密钥交换有很多手段,常见手段有匿名的 DHKE 密钥协商协议、密码或预共享密钥、数字证书等等。有些通讯协议只在开始时交换一次密钥,而有些协议则会随着时间的推移不断地交换密钥。

认证密钥交换(ACHE)是一种会同时认证相关方身份的密钥交换协议,比如个人 WiFi 通常就会使用 password-authenticated key agreement (PAKE),而如果你连接的是公开 WiFi,则会使用匿名密钥交换协议。

目前有许多用于密钥交换的密码算法。其中一些使用公钥密码系统,而另一些则使用更简单的密钥交换方案(如 Diffie-Hellman 密钥交换);其中有些算法涉及服务器身份验证,也有些涉及客户端身份验证;其中部分算法使用密码,另一部分使用数字证书或其他身份验证机制。下面列举一些知名的密钥交换算法:

  • Diffie-Hellman Key Exchange (DHКЕ):传统的、应用最为广泛的密钥交换协议
  • Elliptic-curve Diffie–Hellman (ECDH):基于椭圆曲线密码学的密钥交换算法,DHKE 的继任者
  • RSA-OAEP 和 RSA-KEM(RSA 密钥传输)
  • PSK(预共享密钥)
  • SRP(安全远程密码协议)
  • FHMQV(Fully Hashed Menezes-Qu-Vanstone)
  • ECMQV(Ellictic-Curve Menezes-Qu-Vanstone)
  • CECPQ1(量子安全密钥协议)

二、Diffie–Hellman 密钥交换

迪菲-赫尔曼密钥交换(Diffie–Hellman Key Exchange)是一种安全协议,它可以让双方在完全没有对方任何预先信息的条件下通过不安全信道安全地协商出一个安全密钥,而且任何窃听者都无法得知密钥信息。这个密钥可以在后续的通讯中作为对称密钥来加密通讯内容。

DHKE 可以防范嗅探攻击(窃听),但是无法抵挡中间人攻击(中继)。

DHKE 有两种实现方案:

  • 传统的 DHKE 算法:使用离散对数实现
  • 基于椭圆曲线密码学的 ECDH

为了理解 DHKE 如何实现在「大庭广众之下」安全地协商出密钥,我们首先使用色彩混合来形象地解释下它大致的思路。

跟编程语言的 Hello World 一样,密钥交换的解释通常会使用 Alice 跟 Bob 来作为通信双方。现在他俩想要在公开的信道上,协商出一个秘密色彩出来,但是不希望其他任何人知道这个秘密色彩。他们可以这样做:

分步解释如下:

  • 首先 Alice 跟 Bob 沟通,确定一个初始的色彩,比如黄色。这个沟通不需要保密。
  • 然后,Alice 跟 Bob 分别偷偷地选择出一个自己的秘密色彩,这个就得保密啦。
  • 现在 Alice 跟 Bob,分别将初始色彩跟自己选择的秘密色彩混合,分别得到两个混合色彩
  • 之后,Alice 跟 Bob 再回到公开信道上,交换双方的混合色彩
    • 我们假设在仅知道初始色彩混合色彩的情况下,很难推导出被混合的秘密色彩。这样第三方就猜不出 Bob 跟 Alice 分别选择了什么秘密色彩了。
  • 最后 Alice 跟 Bob 再分别将自己的秘密色彩,跟对方的混合色彩混合,就得到了最终的秘密色彩。这个最终色彩只有 Alice 跟 Bob 知道,信道上的任何人都无法猜出来。

DHKE 协议也是基于类似的原理,但是使用的是离散对数(discrete logarithms)跟模幂(modular exponentiations)而不是色彩混合。

三、经典 DHKE 协议

基础数学知识

首先介绍下「模幂(modular exponentiations)」,它是指求 $g$ 的 $a$ 次幂模 $p$ 的值 $c$ 的过程,其中 $g$ $a$ $p$ $c$ 均为整数,公式如下:

$$ g^a \mod p = c $$

而「离散对数(discrete logarithms)」,其实就是指模幂的逆运算,它使用如下公式表示:

$$ Ind_{g}c \equiv a {\pmod {p}} $$

上述公式,即指在已知整数 $g$,质数 $p$,以及余数(p 的一个原根) $c$ 的情况下,求使前面的模幂等式成立的幂指数 $a$。

已知使用计算机计算上述「模幂」是非常快速的,但是在质数 $p$ 非常大的情况下,求「离散对数」却是非常难的,这就是「离散对数难题」。

然后为了理解 DHKE 的原理,我们还需要了解下模幂运算的一个性质:

$$ g^{ab} \mod p = {(g^a \mod p)}^b \mod p $$

懂了上面这些基础数学知识,下面就开始介绍 DHKE 算法。

DHKE 密钥交换流程

下面该轮到 Alice 跟 Bob 出场来介绍 DHKE 的过程了,先看图(下面绿色表示非秘密信息,红色表示秘密信息):

  • Alice 跟 Bob 协定使用两个比较独特的正整数 $p$$g$
    • 假设 $p=23$, $g=5$
  • Alice 选择一个秘密整数 $a$,计算$A$$= g^a \mod p$ 并发送给 Bob
    • 假设 $a=4$,则$A$$= 5^4 \mod 23 = 4$
  • Bob 也选择一个秘密整数 $b$,计算$B$$= g^b \mod p$ 并发送给 Alice
    • 假设 $b=3$,则$B$$= 5^3 \mod 23 = 10$
  • Alice 计算 $S_1 = B^a \mod p$
    • $S_1 = 10^4 \mod 23 = 18$
  • Bob 计算 $S_2 = A^b \mod p$
    • $S_2 = 4^3 \mod 23 = 18$
  • 已知 $B^a \mod p = g^{ab} \mod p = A^b \mod p$,因此$S_1 = S_2 = S$
  • 这样 Alice 跟 Bob 就协商出了密钥 $S$
  • 因为离散对数的计算非常难,任何窃听者都几乎不可能通过公开的 $p$ $g$ $A$ $B$ 逆推出 $S$ 的值

在最常见的 DHKE 实现中(RFC3526),基数是 $g = 2$, 模数 $p$ 是一个 1536 到 8192 比特的大素数。而整数 $A$ $B$ 通常会使用非常大的数字(1024、2048 或 4096 比特甚至更大)以防范暴力破解。

DHKE 协议基于 Diffie-Hellman 问题的实际难度,这是计算机科学中众所周知的离散对数问题(DLP) 的变体,目前还不存在有效的算法。

使用 Python 演示下大概是这样:

python

# pip install cryptography==36.0.1
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import dh

# 1. 双方协商使用两个独特的正整数 g 与 p
## generator => 即基数 g,通常使用 2, 有时也使用 5
## key_size => 模数 p 的长度,通常使用 2048-3072 位(2048 位的安全性正在减弱)
params = dh.generate_parameters(generator=2, key_size=2048)
param_numbers = params.parameter_numbers()
g = param_numbers.g  # => 肯定是 2
p = param_numbers.p  # => 一个 2048 位的整数
print(f"{g=}, {p=}")

# 2. Alice 生成自己的秘密整数 a 与公开整数 A
alice_priv_key = params.generate_private_key()
a = alice_priv_key.private_numbers().x
A = alice_priv_key.private_numbers().public_numbers.y
print(f"{a=}")
print(f"{A=}")

# 3. Bob 生成自己的秘密整数 b 与公开整数 B
bob_priv_key = params.generate_private_key()
b = bob_priv_key.private_numbers().x
B = bob_priv_key.private_numbers().public_numbers.y
print(f"{b=}")
print(f"{B=}")

# 4. Alice 与 Bob 公开交换整数 A 跟 B(即各自的公钥)

# 5. Alice 使用 a B 与 p 计算出共享密钥
## 首先使用 B p g 构造出 bob 的公钥对象(实际上 g 不参与计算)
bob_pub_numbers = dh.DHPublicNumbers(B, param_numbers)
bob_pub_key = bob_pub_numbers.public_key()
## 计算共享密钥
alice_shared_key = alice_priv_key.exchange(bob_pub_key)

# 6. Bob 使用 b A 与 p 计算出共享密钥
## 首先使用 A p g 构造出 alice 的公钥对象(实际上 g 不参与计算)
alice_pub_numbers = dh.DHPublicNumbers(A, param_numbers)
alice_pub_key = alice_pub_numbers.public_key()
## 计算共享密钥
bob_shared_key = bob_priv_key.exchange(alice_pub_key)

# 两者应该完全相等, Alice 与 Bob 完成第一次密钥交换
alice_shared_key == bob_shared_key

# 7. Alice 与 Bob 使用 shared_key 进行对称加密通讯

四、新一代 ECDH 协议

Elliptic-Curve Diffie-Hellman (ECDH) 是一种匿名密钥协商协议,它允许两方,每方都有一个椭圆曲线公钥-私钥对,它的功能也是让双方在完全没有对方任何预先信息的条件下通过不安全信道安全地协商出一个安全密钥。

ECDH 是经典 DHKE 协议的变体,其中模幂计算被椭圆曲线的乘法计算取代,以提高安全性。

ECDH 跟前面介绍的 DHKE 非常相似,只要你理解了椭圆曲线的数学原理,结合前面已经介绍了的 DHKE,基本上可以秒懂。我会在后面「非对称算法」一文中简单介绍椭圆曲线的数学原理,不过这里也可以先提一下 ECDH 依赖的公式(其中 $a, b$ 为常数,$G$ 为椭圆曲线上的某一点的坐标 $(x, y)$):

$$ (a * G) * b = (b * G) * a $$

这个公式还是挺直观的吧,感觉小学生也能理解个大概。下面简单介绍下 ECDH 的流程:

  • Alice 跟 Bob 协商好椭圆曲线的各项参数,以及基点 G,这些参数都是公开的。
  • Alice 生成一个随机的 ECC 密钥对(公钥:$alicePrivate * G$, 私钥: $alicePrivate$)
  • Bob 生成一个随机的 ECC 密钥对(公钥:$bobPrivate * G$, 私钥: $bobPrivate$)
  • 两人通过不安全的信道交换公钥
  • Alice 将 Bob 的公钥乘上自己的私钥,得到共享密钥 $sharedKey = (bobPrivate * G) * alicePrivate$
  • Bob 将 Alice 的公钥乘上自己的私钥,得到共享密钥 $sharedKey = (alicePrivate * G) * bobPrivate$
  • 因为前面提到的公式,Alice 与 Bob 计算出的共享密钥应该是相等的

这样两方就通过 ECDH 完成了密钥交换。

而 ECDH 的安全性,则由 ECDLP 问题提供保证。这个问题是说,「通过公开的 $kG$ 以及 $G$ 这两个参数,目前没有有效的手段能快速求解出 $k$ 的值。」

从上面的流程中能看到,公钥就是 ECDLP 中的 $kG$,另外 $G$ 也是公开的,而私钥就是 ECDLP 中的 $k$。因为 ECDLP 问题的存在,攻击者破解不出 Alice 跟 Bob 的私钥。

代码示例:

python

# pip install tinyec  # ECC 曲线库
from tinyec import registry
import secrets

def compress(pubKey):
    return hex(pubKey.x) + hex(pubKey.y % 2)[2:]

curve = registry.get_curve('brainpoolP256r1')

alicePrivKey = secrets.randbelow(curve.field.n)
alicePubKey = alicePrivKey * curve.g
print("Alice public key:", compress(alicePubKey))

bobPrivKey = secrets.randbelow(curve.field.n)
bobPubKey = bobPrivKey * curve.g
print("Bob public key:", compress(bobPubKey))

print("Now exchange the public keys (e.g. through Internet)")

aliceSharedKey = alicePrivKey * bobPubKey
print("Alice shared key:", compress(aliceSharedKey))

bobSharedKey = bobPrivKey * alicePubKey
print("Bob shared key:", compress(bobSharedKey))

print("Equal shared keys:", aliceSharedKey == bobSharedKey)

五、PFS 完美前向保密协议 DHE/ECDHE

前面介绍的经典 DHKE 与 ECDH 协议流程,都是在最开始时交换一次密钥,之后就一直使用该密钥通讯。因此如果密钥被破解,整个会话的所有信息对攻击者而言就完全透明了。

为了进一步提高安全性,密码学家提出了「完全前向保密(Perfect Forward Secrecy,PFS)」的概念,并在 DHKE 与 ECDH 的基础上提出了支持 PFS 的 DHE/ECDHE 协议(末尾的 Eephemeral 的缩写,即指所有的共享密钥都是临时的)。

「完全前向保密 PFS」是指长期使用的主密钥泄漏不会导致过去的会话密钥泄漏,从而保护过去进行的通讯不受密码或密钥在未来暴露的威胁。

下面使用 Python 演示下 DHE 协议的流程(ECDHE 的流程也完全类似):

python

# pip install cryptography==36.0.1
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import dh

# 1. 双方协商使用两个独特的正整数 g 与 p
## generator => 即基数 g,通常使用 2, 有时也使用 5
## key_size => 模数 p 的长度,通常使用 2048-3072 位(2048 位的安全性正在减弱)
params = dh.generate_parameters(generator=2, key_size=2048)
param_numbers = params.parameter_numbers()
g = param_numbers.g  # => 肯定是 2
p = param_numbers.p  # => 一个 2048 位的整数
print(f"{g=}, {p=}")

# 2. Alice 生成自己的秘密整数 a 与公开整数 A
alice_priv_key = params.generate_private_key()
a = alice_priv_key.private_numbers().x
A = alice_priv_key.private_numbers().public_numbers.y
print(f"{a=}")
print(f"{A=}")

# 3. Bob 生成自己的秘密整数 b 与公开整数 B
bob_priv_key = params.generate_private_key()
b = bob_priv_key.private_numbers().x
B = bob_priv_key.private_numbers().public_numbers.y
print(f"{b=}")
print(f"{B=}")

# 4. Alice 与 Bob 公开交换整数 A 跟 B(即各自的公钥)

# 5. Alice 使用 a B 与 p 计算出共享密钥
## 首先使用 B p g 构造出 bob 的公钥对象(实际上 g 不参与计算)
bob_pub_numbers = dh.DHPublicNumbers(B, param_numbers)
bob_pub_key = bob_pub_numbers.public_key()
## 计算共享密钥
alice_shared_key = alice_priv_key.exchange(bob_pub_key)

# 6. Bob 使用 b A 与 p 计算出共享密钥
## 首先使用 A p g 构造出 alice 的公钥对象(实际上 g 不参与计算)
alice_pub_numbers = dh.DHPublicNumbers(A, param_numbers)
alice_pub_key = alice_pub_numbers.public_key()
## 计算共享密钥
bob_shared_key = bob_priv_key.exchange(alice_pub_key)

# 上面的流程跟经典 DHKE 完全一致,代码也是从前面 Copy 下来的
# 但是从这里开始,进入 DHE 协议补充的部分

shared_key_1 = bob_shared_key # 第一个共享密钥

# 7. 假设 Bob 现在要发送消息 M_b_1 给 Alice
## 首先 Bob 使用对称加密算法加密消息 M_b
M_b_1 = "Hello Alice, I'm bob~"
C_b_1 = Encrypt(M_b_1, shared_key_1)  # Encrypt 是某种对称加密方案的加密算法,如 AES-256-CTR-HMAC-SHA-256
## 然后 Bob 需要生成一个新的公私钥 b_2 与 B_2(注意 g 与 p 两个参数是不变的)
bob_priv_key_2 = parameters.generate_private_key()
b_2 = bob_priv_key.private_numbers().x
B_2 = bob_priv_key.private_numbers().public_numbers.y
print(f"{b_2=}")
print(f"{B_2=}")

# 8. Bob 将 C_b_1 与 B_2 一起发送给 Alice

# 9. Alice 首先解密数据 C_b_1 得到原始消息 M_b_1
assert M_b_1 == Decrypt(C_b_1, shared_key_1)  # Dncrypt 是某种对称加密方案的解密算法,如 AES-256-CTR-HMAC-SHA-256
## 然后 Alice 也生成新的公私钥 a_2 与 A_2
alice_priv_key_2 = parameters.generate_private_key()
## Alice 使用 a_2 B_2 与 p 计算出新的共享密钥 shared_key_2
bob_pub_numbers_2 = dh.DHPublicNumbers(B_2, param_numbers)
bob_pub_key_2 = bob_pub_numbers_2.public_key()
shared_key_2 = alice_priv_key_2.exchange(bob_pub_key_2)

# 10. Alice 回复 Bob 消息时,使用新共享密钥 shared_key_2 加密消息得到 C_a_1
# 然后将密文 C_a_1 与 A_2 一起发送给 Bob

# 11. Bob 使用 b_2 A_2 与 p 计算出共享密钥 shared_key_2
# 然后再使用 shared_key_2 解密数据
# Bob 在下次发送消息时,会生成新的 b_3 与 B_3,将 B_3 随密文一起发送

## 依次类推

通过上面的代码描述我们应该能理解到,Alice 与 Bob 每次交换数据,实际上都会生成新的临时共享密钥,公钥密钥在每次数据交换时都会更新。即使攻击者花了很大的代价破解了其中某一个临时共享密钥 shared_key_k(或者该密钥因为某种原因泄漏了),TA 也只能解密出其中某一次数据交换的信息 M_b_k,其他所有的消息仍然是保密的,不受此次攻击(或泄漏)的影响。

参考

❌
❌