> This is part 1 of 7 of the full documentation (pages 1–100 of 691).
> The content is paginated: fetch every part to see all of it.
> Next part: https://freebsd-journal-cn.bsdcn.org/llms-full.txt/1
> Page index: https://freebsd-journal-cn.bsdcn.org/llms.txt

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
This site has an agentic ask interface you may use to query the documentation dynamically by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://freebsd-journal-cn.bsdcn.org/index.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

---

# FreeBSD 期刊

所有期刊均经过校对与审阅。

FreeBSD 期刊原文位于：<https://freebsdfoundation.org/our-work/journal>。

本书部署网站：<https://book.bsdcn.org>。

FreeBSD 中文社区译制。本翻译项目遵循 [BSD 2-Clause 许可证](https://github.com/FreeBSD-Ask/freebsd-journal-cn/tree/main/LICENSE/README.md)。

本作品仅为教学演示之目的，版权归 FreeBSD 基金会和/或每篇文章开头或结尾注明的作者所有。如认为本作品存在侵权情形，请通过电子邮件联系 ykla <yklaxds@gmail.com>。


# 编辑日志

* 2026.10.7
  * 新增 2026-789 期（生产部署，Production Deployments）全部 8 篇文章翻译
    * 《基金会来信》（letter-from-the-foundation.md，Anne Dickison）：完整翻译
    * 《活动日历》（events-calendar.md，Florine Kamdem）：完整翻译；3 张图片离线至 `png/2026-789/`
    * 《用 Sylve 整合家庭实验室》（consolidating-a-home-lab-with-sylve.md，Sven Rüdiger）：完整翻译；5 个代码块保留
    * 《使用 FreeBSD 计算资源开展云运维》（cloud-operations-using-freebsd-compute.md，Jason Tubnor）：完整翻译；4 个代码块保留
    * 《用户空间中的 USB》（usb-in-user-space.md，Rick Parrish）：完整翻译；29 个代码块逐一保留
    * 《在 FreeBSD 上进行可审计的凭证泄露检查，无需发送完整密码》（Emre Çapan）：完整翻译；4 个代码块保留并补 `sh` 语言标记
    * 《EuroBSDCon 参会报告》（eurobsdcon-trip-report.md，Federico Angelilli）：完整翻译；1 张图片离线至 `png/2026-789/`
    * 《读者来信》（we-get-letters.md，Michael W Lucas）：完整翻译
  * 补齐 2026-456 期（改进软件质量）遗漏的 2 篇文章
    * 《FreeBSD WiFi 开发第三部分：调试》（wifi-development-part-3.md，Tom Jones）：完整翻译；20 个代码块保留
    * 《打包、CloudInit，以及重温“好的、坏的和丑陋的”》（packaging-cloudinit.md，Tom Jones 采访 Baptiste Daroussin）：完整翻译
  * SUMMARY.md 新增 `## 2026-070809 生产部署` 章节，并在 2026-456 期补入上述 2 篇条目
  * 作者行统一为 2026 期新版格式：`- 原文：[英文标题](URL)` + `- 作者：**姓名**`
  * 校验：markdownlint 0 错误；代码块与源文逐行比对一致（仅注释翻译）；URL、数字、技术标识符比对无缺失；SUMMARY 全部 691 个条目对应文件存在；H1 与 SUMMARY 标题一致
* 2026.7.10
  * 新增 2026-456 期（改进软件质量）全部 6 篇文章翻译，英文源已归档至 `en/2026-456-improving-software-quality/`
    * 《基金会来信》（letter-from-the-foundation.md，Deb Goodkin）：完整翻译
    * 《活动日历》（events-calendar.md，Anne Dickison）：完整翻译；图片离线至 `png/2026-456/events-calendar-01.png`
    * 《使用 CHERI 调试》（debugging-with-cheri.md，John Baldwin）：完整翻译；代码块标注 \`\`\`sh 语言标签
    * 《FreeBSD 上的现代 Java》（modern-java-on-freebsd.md，Tom Jones 采访 Harald Eilertsen）：完整翻译
    * 《来吧，友好的 SBOM》（come-friendly-sboms.md，Alice Sowerby）：完整翻译；MD060 表格分隔行对齐修复
    * 《读者来信》（we-get-letters.md，Michael W Lucas）：完整翻译
    * markdownlint 全部 6 篇通过（0 错误）
* 2026.7.10
  * 校对 2023-1112 期（FreeBSD 14.0）全部 7 篇文章，英文源已归档至 `en/2023-1112-freebsd-14-0/`
  * 4 篇新翻译：
    * 《基金会来信》（letter-from-the-foundation.md，John Baldwin）：完整翻译；避免 "之一" 滥用（"魅力之一" → "一大魅力"）
    * 《LinuxBoot：从 Linux 启动 FreeBSD》（linuxboot-cong-linux-qi-dong-freebsd.md，Warner Losh）：完整翻译；移除英中混排（"freely available（自由可用）" → "自由可用"）；路径加粗；man page 格式（`kexec.2`）
    * 《现在用 Webhook 触发我》（xian-zai-yong-webhooks-chu-fa-wo.md，Dave Cottlehuber）：完整翻译；代码块注释翻译为中文；路径加粗（如 **/var/log/messages**）；命令反引号（如 `openssl dgst`、`logger.1`）
    * 《新的 Ports 提交者：Joel Bodenmann》（xin-de-port-ti-jiao-zhe-oel-bodenmann-jbofreebsd.org.md，Tom Jones 采访）：完整翻译；修正 SUMMARY.md 标题拼写（"oel" → "Joel"，CI 自动同步 H1）；头部格式统一（`- 原文`/`- 作者` 顺序）；图片 alt 文本修正（"oel" → "Joel"）；欧化汉语修正（长定语 "设计用来执行特定任务的系统" → "为执行特定任务而设计的系统"、抽象名词做主语 "随着我熟悉程度的增加" → "随着我越来越熟悉"、三连 "的" 重构、"更新" → "升级" 准确性修正、"系统/组件" → "系统或组件"、删除 "已经" 滥用、句号改逗号修正句流）；作者姓名格式修正（"TOM JONES" → "Tom Jones"）
  * 3 篇校对（已有翻译）：
    * 《活动日历》（events-calendar.md）：逐段对照英文原文，翻译准确，无需修改
    * 《FreeBSD 容器镜像》（freebsd-rong-qi-jing-xiang.md）：逐段对照英文原文，翻译准确，无需修改
    * 《读者来信》（we-get-letters.md）：格式修正（"原文链接" → "原文"、作者加粗为 **Michael W Lucas**、添加 `---` 分隔符、移除期刊页脚 "——《FreeBSD 期刊》，2023 年 11/12 月"）
* 2026.7.9
  * 三工具全书清理第二轮（AutoCorrect、md-padding\@latest、markdownlint），spec：three-tools-full-sweep-rerun-2
  * 重新生成三工具全量清单：markdownlint 28 处违规、AutoCorrect 112 条建议、md-padding 212 文件 +1496/-1475 行
  * markdownlint 逐项审核：采纳 0 处（27 处 spec 文件 + 1 处 prometheus.md 裸 URL 均为预存问题）
  * AutoCorrect 逐项审核：采纳 1 条（2023-0506/Promise.md:63 全角引号前后删空格），拒绝 111 条
  * md-padding 逐项审核：采纳 0 条，拒绝全部 1496 条
  * 总复核：无新错误，无批量接受，无 `--fix`，无脚本/sed/awk
* 2026.7.9
  * 校对 2023-0506 期（FreeBSD 三十周年纪念特刊）全部 13 篇文章
  * 子批次 1（5 篇，早期会话已完成）：基金会来信、CheriBSD、AArch64、Timeline、Installing
  * 子批次 2（4 篇）：ZFS、Promise（早期会话已完成）、Recollections、Unix
    * 校对《回忆》（Recollections.md，20 处修改）：删除 "一个" 滥用、改写万能动词 "给予...提升"、删除 "关于" 滥用、"进行操作" → 删除、"在...情况下" → "无论...都" 等
    * 校对《Unix》（Unix.md，31 处修改）：删除 "一个" 滥用、改写万能动词 "做出/进行"、删除 "在...中" 滥用、"由于" 滥用改写等
  * 子批次 3（4 篇）：
    * 校对《早期的 FreeBSD 移植》（Ports.md，17 处修改）："需要在许多不同的领域进行修改" → "许多不同领域都需要修改"、"进行条件执行" → "条件执行"、"进行分支预测" → "分支预测"、"对内存进行读-改-写操作" → "对内存做读-改-写操作"、"John Baldwin 对原子框架进行了扩展" → "John Baldwin 扩展了原子框架" 等万能动词 "进行" 改写
    * 校对《FreeBSD 30 周年：成功的秘诀》（30.md，13 处修改）：头部格式修正（作者加粗、"- 原文" 格式）、删除 3 处 pull quote 残留段、"对项目仓库进行修改" → "修改项目仓库"、"从光盘启动系统进行试用" → "从光盘启动系统试用"、"对硬件进行精挑细选" → "精挑细选硬件"、"它是一个用于创建..." → "它是用于创建..."、"构建一个定制的操作系统" → "构建定制的操作系统" 等
    * 校对《FreeBSD 在日本》（Japan.md，13 处修改）：作者加粗、"与美国计算机的一个主要区别" → "与美国计算机的主要区别"、翻译错误修复 "直到 1975 年，外国公司才能进入日本市场" → "1975 年之前，外国公司无法进入日本市场"、"并且还支持" → "也支持"、"在...期间/过程中" 滥用、"一个/一款" 滥用、"们" 字滥用、"之一" 滥用、被动句改主动、万能动词 "进行" 改写、"对...不容乐观" 结构混乱等
    * 校对《读者来信》（we-get-letters.md，15 处修改）：删除 PDF 页脚残留 "28 FreeBSD 期刊 • 2023 年 5/6 月"、作者加粗、"原文链接" → "原文"、删除 pull quote 残留、修复两处 PDF 分页截断的不完整句子（"但这些与"+"与真正的写作扯不上关系"、"经历过"+"让我看到"）、"一个/一种/一位/一家" 滥用、"们" 字滥用等
  * 英文原文已归档至 en/2023-0506-freebsd-30th-anniversary/（部分英文源 PDF 抓取失败，仅做欧化汉语排查）
* 2026.7.9
  * 校对 2023-0304 期（嵌入式）全部 7 篇文章
  * 校对《基金会来信》（格式头部统一 `- 原文链接` → `- 原文`、作者加粗）
  * 校对《活动日历》（格式头部统一；"商业供应商方面的来稿" → "商业供应商来稿" 删除 "方面的" 冗余；"将在会上举办" → "很高兴在会上举办" 翻译修正）
  * 校对《会议报告：SCaLE20X》（格式头部统一 `- 原文链接` → `- 原文`、作者加粗为 **Drew Gurkowski**）
  * 校对《读者来信》（格式头部统一；"在生产线部署" → "在生产环境部署" 用词修正；重构打断句子的 pull quote 为引用块）
  * 校对《CheriBSD Ports 与软件包》（格式头部统一；`***MACHINE_ARCH***` → `**MACHINE_ARCH**` 三重星号改为双星号；`git add -i` 加反引号；图 2/图 3 alt 文本修正，原误写为 "图 1"）
  * 校对《让我们来试试 ChatGPT》（作者加粗为 **Benedict Reuschling**；16 处 `***BCR***`/`***ChatGPT***` 三重星号改为双星号 `**BCR**`/`**ChatGPT**`；"展示一个解决方案时" → "展示解决方案时" 删除 "一个" 滥用；"一个争议问题" → "争议问题" 删除 "一个" 滥用）
  * 校对《GPU 直通》（作者加粗为 **Corvin Köhne** 和 **Daniel Kerkhoff**；3 处 pull quote 三重星号改为双星号；"显然选择" → "理所当然的选择" 翻译修正避免非标准搭配）
  * 英文原文已归档至 en/2023-0304-embedded/（全部 7 篇，PDF 兜底）
* 2026.7.9
  * 校对 2023-0102 期（构建 FreeBSD Web 服务器）全部 8 篇文章
  * 校对《活动日历》（格式头部统一 `- 原文链接` → `- 原文`；作者名加粗为 **ANNE DICKISON**；"所有演讲均在线上交付" → "所有演讲均在线上进行" 避免 "交付" 用于演讲不自然）
  * 校对《ZFS 的原子 I/O 与 PostgreSQL》（格式头部统一 `- 原文链接` → `- 原文`；翻译质量良好，保留译者注）
  * 校对《虚拟实验室——BSD 编程研讨会》（格式头部统一；ping 命令顺序调整与英文原文一致：`ping -c 3 10.66.6.1` 在前 `ping -c 3 bsd.pw` 在后）
  * 校对《ZFS 简介》（格式头部统一；代码块引号从双引号 `"da[0-9]|cd[0-9]"` 改为单引号 `'da[0-9]|cd[0-9]'` 与英文原文一致）
  * 校对《会议报告：落基山庆祝女性计算机科学家》（格式头部统一；3 处段落拆分——英文 7 段中文合并为 4 段，已拆分为 7 段与英文原文一致）
  * 校对《进行中的工作/征求反馈：数据包批处理》（格式头部统一；URL 错误修正 `bsdjhb:-freebsd:cxgbe_batching` → `bsdjhb:freebsd:cxgbe_batching` 删除多余的 `-`，联网复查确认）
  * 校对《基金会与 FreeBSD 桌面》（格式头部统一；2 处段落拆分——英文段 2+3 合并、段 7+8 合并，已拆分与英文原文一致）
  * 校对《读者来信》（格式头部统一；作者名加粗为 **MICHAEL W LUCAS**；段落重组——删除重复内容 "那文件系统呢？文件系统需要调优！胡说八道，我说！"、删除添加内容 "行了。你可以向管理层汇报...回去玩 nethack 吧"、按英文原文顺序重新组织段落、英文段 5-8 拆分为 4 个短段；删除文末 PAM Mastery 书籍广告 PDF 残留）
  * 英文原文已归档至 en/2023-0102-building-a-freebsd-web-server/（全部 8 篇，PDF 兜底）
* 2026.7.9
  * 校对 2022-1112 期（可观测性和衡量标准）全部 11 篇文章
  * 校对《基金会来信》（格式头部统一 `- 原文链接` → `- 原文`）
  * 校对《活动日历》（格式头部统一）
  * 校对《DDB 内核调试器》（格式头部统一）
  * 校对《DTrace：旧追踪系统的新增功能》（格式头部统一）
  * 校对《Icinga 证书监控》（格式头部统一）
  * 校对《activitymonitor.sh》（格式头部统一）
  * 校对《实用 IPv6（第四部分）》（格式头部统一）
  * 校对《EuroBSDCon 2022 会议报告》（格式头部统一）
  * 校对《实用 Port：Prometheus 的安装与配置》（补漏译 "左侧" 对应 on the left；"是一个不错的选择" → "是不错的起点" 避免 "一个" 滥用；删除增译 "具有清晰的架构" 和 "随时"；补漏译 "黑暗深处" 对应 dark depths；"文档贡献者" → "文档提交者" 术语一致性）
  * 校对《书评：Kill It with Fire》（作者行 `**BOOK REVIEW** & **TOM JONES**` → `审阅：**TOM JONES**` 参照 2022-0910 书评格式；"UNIX 已经五十与年" 错别字 → "UNIX 几年前已满五十"；"初衷" → "最初的主张" 翻译修正；"书中的教训" → "书中的经验" 用词修正；"填补管理和维护现有系统的团队之间的空白" → "弥合与现有系统维护和管理人员之间的鸿沟" 翻译修正；补漏译 "与团队一起" 和 "以更好地匹配现代实践"；"更有价值的数据" → "更确凿的数据" 翻译修正）
  * 校对《读者来信》（作者名加粗为 **Michael W Lucas**；文末署名 `**MICHAEL W LUCAS**` → **Michael W Lucas**；补漏译 "烤" 对应 roasted garlic；"因为出于一种合理的恐惧" → "出于一种合理的恐惧" 删除重复连词；删除文末 PAM Mastery 广告块 PDF 残留）
  * 英文原文已归档至 en/2022-1112-observability-and-metrics/（全部 11 篇，PDF 兜底）
* 2026.7.9
  * 校对 2022-0910 期（安全性）全部 11 篇文章
  * 校对《基金会来信》（格式头部统一）
  * 校对《活动日历》（格式头部统一）
  * 校对《CARP 防火墙故障转移》（格式统一）
  * 校对《内核安全》（格式统一）
  * 校对《PAM》（格式统一）
  * 校对《SSH》（格式统一）
  * 校对《实用 IPv6（第三部分）》（`IPV6-SERVER`、`ROUTED-IPV6-PREFIX`、`rtadvd(8)`、`service(8)`、`ipv6_defaultrouter`、`gif0`、`sshd(8)`、`tcp/22`、`syslogd(8)`、`gif(4)` 等手册页/参数由加粗改为反引号；`LLA` 去加粗；"可能无法正常工作" → "无法正常工作" 翻译语气修正）
  * 校对《书评》（删除 "一位"/"一个" 滥用 4 处；"谷歌" → "Google" 2 处品牌名一致性；"九章" 去加粗）
  * 校对《访谈：保障 FreeBSD 安全性》（PDF 提取人名空格修复：`PAMBAKER` → **PAM BAKER**、`ANNEDICKISON` → **ANNE DICKISON**、`GordonTetlow` →Gordon Tetlow、`EdMaste` →Ed Maste、`MarkJohnston` →Mark Johnston；术语空格修复：`ProductSecurityIncidentResponseTeam` →Product Security Incident Response Team、`DecisionIntelligenceForDummies` →Decision Intelligence For Dummies；`ports` → `Ports` 2 处首字母大写；"是一位多产" → "是多产" 删除 "一位" 滥用）
  * 校对《MCH 2022 会议报告》（"以及黑客实践" → "、黑客实践"；补全漏译 "UbaBot" 自动调酒机名称；"以及一个关于" → "、关于" 删除 "以及" 和 "一个" 滥用；"及旋钮" → "、旋钮"；"以及在父母的花园里" → "、在父母的花园里"）
  * 校对《读者来信》（作者名加粗为 **Michael W Lucas**；"HP/UX 以及其他 Unix" → "、其他 Unix"；"正义的背叛，以及那种" → "、那种"；"Java 以及其他诱人" → "、其他诱人"；"进行正经的投票" → "正经投票" 避免万能动词 "进行" 滥用）
  * 英文原文已归档至 en/2022-0910-security/（全部 11 篇，PDF 兜底）
* 2026.7.9
  * 校对 2022-0708 期（科研、系统与 FreeBSD）全部 11 篇文章
  * 校对《基金会来信》（`- 原文链接` → `- 原文`、作者改为 **John Baldwin** 去除 "编辑委员会"）
  * 校对《活动日历》（`- 原文链接` → `- 原文`、作者加粗为 **Anne Dickison**、"落基山庆祝女性计算机科学家 2022" → "落基山庆祝计算领域女性 2022" 翻译修正）
  * 校对《在 FreeBSD 上构建 Loom 框架》（`- 原文链接` → `- 原文`、作者 `BRIAN KIDNEY` → **Brian Kidney**）
  * 校对《教授本科生 Unix 课程》（`- 原文链接` → `- 原文`、作者 `BENEDICT REUSCHLING` → **Benedict Reuschling**）
  * 校对《FreeBSD 入门研讨会》（`- 原文链接` → `- 原文`、作者 `ROLLER ANGEL` → **Roller Angel**、文末署名同步修改）
  * 校对《实用 IPv6（第二部分）》（`- 原文链接` → `- 原文`、作者 **佐藤広生** 保持不变）
  * 校对《在 2022 年及以后推广 FreeBSD》（`- 原文链接` → `- 原文`、作者 `**ANNE DICKISON**` → **Anne Dickison**、文末署名同步修改、"坚定推广...的重要性" → "坚定地宣传...的重要性" 语法修正）
  * 校对《进行中的工作/征求反馈：Socket 缓冲区》（`- 原文链接` → `- 原文`、作者 `**TOM JONES** & **GLEB SMIRNOFF**` → **Tom Jones**、**Gleb Smirnoff**、文末署名同步修改、"寻找一个能让他为开源贡献的工作" → "寻找能让他为开源贡献的工作" 避免 "一个" 滥用）
  * 校对《FreeBSD 开发者峰会报告》（`- 原文链接` → `- 原文`、作者 `**ANNE DICKISON**` → **Anne Dickison**、文末署名同步修改、重写 "特别是她在...期间，帮助她树立了...信念" 句式为 "特别是她在...的工作经历，使她更加坚定地宣传...的重要性" 修正语法）
  * 校对《支持 Electromagnetic Field 2022》（`- 原文链接` → `- 原文`、作者 `**TOM JONES**` → **Tom Jones**、文末署名同步修改）
  * 校对《读者来信》（`- 原文链接` → `- 原文`、作者 `Michael W Lucas` → **Michael W Lucas** 加粗、文末署名 `**MICHAEL W LUCAS**` → **Michael W Lucas**、"一个从磁盘读过的文件" → "从磁盘读过的文件" 避免 "一个" 滥用）
  * 英文原文已归档至 en/2022-0708-science-systems-and-freebsd/（全部 11 篇，PDF 兜底）
* 2026.7.9
  * 校对 2022-0506 期（灾难恢复）全部 9 篇文章
  * 校对《基金会来信》（格式统一、标点统一全角）
  * 校对《活动日历》（补漏译 URL 多处、格式统一）
  * 校对《云上灾难恢复》（术语不一致修正：5 处指代 guest 虚拟机的 "客户端" 改为 "客户机"、"客户端 VPN" 保留；代码块内英文注释翻译为中文）
  * 校对《使用 LLDB 调试 FreeBSD》（图 4 说明中 `PT_DETACH` 和 `PT_CONTINUE` 加反引号）
  * 校对《IPv6 故障排除》（头部 `- 原文链接` → `- 原文`、多处命令/手册页加反引号：`grep`、`AddressFamily`、`nd6 options`、`AUTO_LINKLOCAL`、`ping6(8)`、`ifconfig(8)`、`scopeid`、`-alias`、`%zoneid`；"IPv6 节点配置" 补充 `###` 标题标记；**/etc/rc.conf** 路径加粗；"已经配置了" → "配置了" 避免 "已" 字滥用）
  * 校对《Netdump》（`NETDUMP_VMCORE`、`NETDUMP_KDH`、`NETDUMP_FINISHED` 消息类型常量加反引号；`dumpon(8)` 手册页引用加反引号；`netdumpd` 守护进程名加反引号）
  * 校对《FreeBSD 启动性能》（头部 `- 原文链接` → `- 原文`、FlameGraphs URL 从方括号 `[URL]` 改为尖括号 `<URL>`、`options TSLOG` 加反引号、`rtsold` 加反引号、"一个良好起点" → "良好起点" 避免 "一个" 滥用、sysctl 名称 `boottrace`/`runtrace`/`shuttrace` 加反引号）
  * 校对《实用 Port：在 OpenZFS 上设置 NFSv4 文件服务器》（头部 `- 原文链接` → `- 原文`、`readonly=on` 加反引号、多处路径从反引号改为加粗：**/etc/rc.conf**、**/etc/exports**、**/fileshare**、**/etc/fstab**、**/media**；`nfsuserd` 加反引号；`on` 加反引号与 `off` 格式一致）
  * 校对《读者来信》（头部 `- 原文链接` → `- 原文`、作者名加粗为 **Michael W Lucas**）
  * 英文原文已归档至 en/2022-0506-disaster-recovery/（全部 9 篇，PDF 兜底，已用 pdfplumber 提取为 Markdown）
* 2026.7.9
  * 三工具全书清理（AutoCorrect、md-padding\@latest、markdownlint），spec：three-tools-full-sweep-rerun
  * 重新生成三工具全量清单：markdownlint 1 处违规（MD009）、AutoCorrect 109 条建议、md-padding 209 文件 +1209/-1188 行
  * markdownlint 逐项审核：采纳 1 处（2018-0102/book-review\.md:12 MD009 行尾空格删除）
  * AutoCorrect 逐项审核：采纳 1 条（2017-0506/book-review-designing-bsd-rootkits.md:12 删除全角冒号前多余空格），拒绝 108 条（全角标点转半角、连字符加空格、书名号转引号、`+` 加空格等）
  * md-padding 逐项审核：采纳 0 条，拒绝全部 1209 条（代码块内加空格、sup 标签加空格、全角引号加空格、加粗标记加空格等）
  * 总复核：原 MD009 已修复；AutoCorrect 105 条建议均为已拒绝类别；md-padding 211 文件（原 209 + 2 新 spec 文件）0 采纳；无批量接受、无 `--fix`、无脚本/sed/awk
* 2026.7.9
  * 校对 2022-0304 期（ARM64 是一级架构）全部 8 篇文章
  * 校对《基金会来信》（`- 原文链接` → `- 原文`、URL 修正 2022/03→2022/04、作者改为 **Ed Maste** 去除 "编辑委员会"、删除 "已" 字 "已获得" → "获得"、调整工具链描述句式）
  * 校对《活动日历》（`- 原文链接` → `- 原文`、URL 修正 2022/03→2022/04、作者加粗为 **Anne Dickison**、Office Hours 段落末尾添加 YouTube 链接）
  * 校对《FreeBSD/ARM64 上的数据科学》（`- 原文链接` → `- 原文`、"所有基于 ARM 的 FreeBSD 系统" → "各种 ARM 平台上支持 FreeBSD"、"不仅仅是另一个 Web 服务器" → "不是又一个 Web 服务器或 NFS 存储阵列"）
  * 校对《Pinebook Pro 上的 FreeBSD》（`- 原文链接` → `- 原文`、多处 `ports` → `Ports` 首字母大写 4 处、"原版 Manjaro 和 Debian" → "原版 Manjaro 或 Debian"、"Port 和修改后的软件包" → "Ports 和修改后的软件包"、RockPRO64 注意事项中 `u-boot` → "U-Boot"）
  * 校对《嵌入式控制器的 ACPI 支持》（`- 原文链接` → `- 原文`、"FreeBSD ARM64 端口" → "FreeBSD ARM64 移植" 事实性错误修正、"已允许" → "现在允许" 避免 "已" 字滥用、"已在 FreeBSD 内核中实现" → "FreeBSD 内核已实现该方案"、"已探测到的驱动程序" → "探测到的驱动程序"、"这种灵活性经常被厂商滥用" → "厂商经常滥用这种灵活性" 被动句改主动句）
  * 校对《进行中的工作/征求反馈：Lumina 桌面征集开发人员》（`- 原文链接` → `- 原文`、链接文本改为英文原标题、作者加粗为 **TOM JONES** 和 **JT PENNINGTON**）
  * 校对《实用 Port：如何设置 Apple 时间机器》（`- 原文链接` → `- 原文`、"一切最新 ARM 板" → "任何较新的 ARM 板" 翻译修正、代码块 `grouadd` → `groupadd` 和 `groumod` → `groupmod` 拼写修正 3 处、路径格式去除反引号 `/usr/local/etc/afp.conf` 和 `/var/log/afp.log`、补全漏译段落 "unix charset" 和 "zeroconf" 两段、`\$u` → `` `$u` `` 转义字符修正）
  * 校对《读者来信》（`- 原文链接` → `- 原文`、URL 修正 2022/03/letters.pdf→2022/04/MarchApril-Letters.pdf、作者加粗为 **Michael W Lucas**、"花了太多十年" → "花了太多个十年" 量词缺失修正）
  * 英文原文已归档至 en/2022-0304-arm64-is-a-tier-1-architecture/（全部 8 篇）
* 2026.7.9
  * 校对 2025-789 期（嵌入式）全部 11 篇文章
  * 校对《基金会来信》（添加格式头部：原文链接 + 作者 **John Baldwin**）
  * 校对《活动日历》（添加格式头部：原文链接 + 作者 **Anne Dickison**、修正日期 en-dash 为连字符：25–28→25-28、6–7→6-7）
  * 校对《开启 FreeBSD 开发之路 —— 专访 Igor Ostapenko》（格式头部 `- [Starting...]` → `- 原文：[Starting...]`、"通往 FreeBSD 开发之路的有很多因素" → "通往 FreeBSD 开发之路有很多条"、"我更多专注在" → "我更专注于"、补全课程主讲人 Marshall Kirk McKusick、修正 "George Neville-Neil" → "George V. Neville-Neil"、"事实证明是最有效的方法" → "事实证明这是最有效的方法"、补全 VNET Jails 针对新实现的功能或修复的 bug 的说明、补全源码路径 **src/tests/sys/netpfil**、补全 Kristof 的 YouTube 演讲链接、补全漏译段落 "使用语言服务器协议（LSP）可以大大简化代码理解和导航"、"他们的积累可以" → "他们积累的知识和经验可以"、"虽然不是必须的" → "虽然不是必须的或所期望的"、"是一位 FreeBSD 提交者" → "是 FreeBSD 提交者"、"导航设备测试系统" → "导航设备操作与测试系统"、补全 "这里打个基于网络的比方"、补全漏译 "通往贡献的路径有很多种"、补全 Netlink 实现迁移说明 "（伴随现有工具的迁移工作）"、补全 VPP 全称 "Vector Packet Processing"、补全漏译段落 "每篇文章都不是涵盖所有细节..."）
  * 校对《CHERIoT》（前期已完成）
  * 校对《FreeBSD、Home Assistant 与 rtl\_433》（前期已完成）
  * 校对《撰写有效的 Bug 报告》（前期已完成）
  * 校对《实现量子安全网站》（前期已完成）
  * 校对《FreeBSD WiFi 开发第二部分：驱动开发》（前期已完成）
  * 校对《嵌入式 FreeBSD：回顾与展望》（前期已完成）
  * 校对《会议报告：2025 BSDCan》（前期已完成）
  * 校对《读者来信》（添加格式头部：原文链接 + 作者 **Michael W Lucas**、"所以送货卡车之后才到" → "所以邮件在送货卡车之后才到" 补全 it 指代、"没记过任何有用的东西" → "没有记录过任何有用的东西"、修复损坏的邮箱链接 `[letters@freebsdjournal.](mailto:letters@freebsdjournal.org)org` → `<letters@freebsdjournal.org>`、作者署名加粗）
  * 英文原文已归档至 en/2025-789-embedded-2/（全部 11 篇）
* 2026.7.9
  * 校对 2022-0102 期（软件与系统管理）全部 10 篇文章
  * 校对《基金会来信》（`- 原文链接` → `- 原文`、作者行加粗、`贡献新 port` → `贡献新 Port`、`向 ports 贡献` → `向 Ports 贡献`、`同样主题下` → `类似地`、签名块加空行）
  * 校对《活动日历》（补全缺失 URL：SCALE、FreeBSD Fridays、Office Hours、Open Source 101 各自链接、调整段落顺序与英文原文一致、`- 原文链接` → `- 原文`）
  * 校对《为 FreeBSD Ports 做贡献》（`- 原文链接` → `- 原文`、多处小写 ports 改为 Ports："从 ports 构建" → "从 Ports 构建"、"自己编译 ports" → "自己编译 Ports"、"因为 ports 使得" → "因为 Ports 使得"、"为 FreeBSD ports 开发" → "为 FreeBSD Ports 开发"、"折腾 FreeBSD ports" → "折腾 FreeBSD Ports"、"ports 提交者" → "Ports 提交者"、"ports 补丁" → "Ports 补丁"、"ports 树" → "Ports 树"）
  * 校对《git-arc》（`- 原文链接` → `- 原文`、"创建或更新 port" → "创建或更新 Port"、"为树贡献新 port" → "为树贡献新 Port"、"扫描请求的 port 列表" → "扫描请求的 Port 列表"、"创建新 port" → "创建新 Port"、`<revision_id>` → `<revision>` 与代码块一致）
  * 校对《CBSD》（`- 原文链接` → `- 原文`）
  * 校对《将 OpenBSD 的 pf syncookie 代码移植到 FreeBSD 的 pf》（`- 原文链接` → `- 原文`）
  * 校对《进行中的工作/征求反馈：mkjail》（`- 原文链接` → `- 原文`、删除文末 PAM Mastery 广告块 PDF 残留）
  * 校对《书评：编程智慧》（`- 原文链接` → `- 原文`、日文引言翻译为中文 "去年 の 癡僧..." → "去年，一位愚僧。今年，依然如故。"、"良寛大愚" → "良宽大愚" 日文汉字转简体中文、`**PP BENEDICT REUSCHLING**` → `**BENEDICT REUSCHLING**` 删除 PDF 残留的专栏签名 PP）
  * 校对《会议报告：EuroBSDCon 2021》（`- 原文链接` → `- 原文`、删除 PDF 侧栏文本残留两处 "通常，EuroBSDCon 是一场线下活动，" 和 "但 COVID 疫情改变了规则。"、ports→Ports 首字母大写 2 处）
  * 校对《读者来信》（`- 原文链接` → `- 原文`、URL 修正 letters.pdf→JanFeb\_letters.pdf、作者格式标准化为 `- 作者：**MICHAEL W LUCAS**`）
  * 英文原文已归档至 en/2022-0102-software-and-systems-management/
* 2026.7.9
  * 校对 2021-1112 期（存储）全部 8 篇文章
  * 校对《基金会来信》（添加原文链接 + 作者行、简化 "度过一个愉快的假期" → "假期愉快"、修正 "在这个又一个" 冗余、改 "享受了" 为 "喜欢"）
  * 校对《活动日历》（添加原文链接 + 作者行、去内联作者、改 "加入 FreeBSD 社区成员参加" 为 "与 FreeBSD 社区成员一起参加"）
  * 校对《开放通道 SSD》（原文链接文本 "Open Channel 固态硬盘" → "Open Channel SSD"、**sys/dev/nvme** 路径加粗、"实时反馈" → "主动反馈"、**lightnvm/control** 路径加粗、"划分并创建目标" → "划分并行单元并创建目标"、简化冗余缓存描述、"接下来" → "又一次"）
  * 校对《构建 FreeBSD 社区》（"FreeBSD 是个开源社区" → "FreeBSD 是一个开源社区"、"友好的小型技术会议" → "Friendly Wee Tech Conference"、"BSD 聚焦事件" → "BSD 主题活动"、"一同玩耍" → "一起钻研技术"、"在疫情爆发前" → "在疫情袭来之际"、"基于工作冒险的" → "基于 WorkAdventure 的"、"心理上要有准备接受失落" → "做好心理准备接受失望"、"一场自我肯定的赌注" → "对自尊心的考验"、"回顾着我曾经参与组织的日子" → "人们讲述着我参与组织的那天的故事"）
  * 校对《与完美操作系统同行 27 年》（ports→Ports 首字母大写、jail→Jail 首字母大写、Shell→shell、"更容易被记录和文档化" → "更容易文档化"、"待我离开公司" → "我离开公司后"、FreeBSD jail→FreeBSD Jail、FreeBSD ports→FreeBSD Ports、"更新一个包" → "更新包"、句子语序调整、"以及" → "、"、"进行日志记录" → "记录日志"、补全 "我在 FreeBSD 上的常规测试"、"两十年前" → "二十年前"、"图形功能" → "图形界面"、"待我再次拥有" → "一旦我再次拥有"、"跟踪 bug" → "关注 bug 跟踪系统"、补全 "关于 sudo 和 syslog-ng 的演讲"、"PPC 和 ARM" → "PPC 或 ARM"）
  * 校对《进行中的工作/征求反馈：OccamBSD》（原文链接文本改为英文、"本期" → "首期"、"无论这些功能" → "无论其"、jail→Jail、"和...以及" → "、"、"提供了一个关于...的一瞥" → "让人一窥"、"一个独特的机会" → "独特的机会"、"应该从哪里开始的地方" → "可以说应该开始的地方"、"一次很有价值的练习" → "有价值的练习"、"一个极简的系统实例" → "极简系统的实例"、"一个更小的学习环境" → "更小的学习环境"、"FreeBSD 基础的项目" → "基于 FreeBSD 的项目"）
  * 校对《通过 iSCSI 导入 ZFS ZIL》（原文链接加冒号、"适应 ARC" → "放入 ARC"、"待我们开始" → "一旦我们开始"、**/etc/ctl.conf** 等路径加粗、补全 "要么"、"ZIL 会确认" → "ZIL 会向应用程序（数据库）确认"、"集成问题" → "集成有多么糟糕"、"只要时间问题" → "这只是时间问题"、"缺失的 ZIL 设备挂载" → "带有缺失 ZIL 设备的池挂载"、"待完成" → "完成后"、`zpool status` 反引号、"应该休息" → "本该睡觉"、`net/ceph14` 等命令反引号、**volumes/ssdvolume** 路径加粗、`--id` 等选项反引号、**/dev/ggate0** 路径加粗、"zfs 发送" → "`zfs send`"、"衡量性能丧失的地方" → "测量性能损失发生的位置"、"记录每一步" → "为自己和同事记录每一步"、"教授...课程给本科生" → "为本科生教授...课程"）
  * 校对《读者来信》（作者格式标准化为 `- 作者：**Michael W Lucas**`、"以及" → "、"）
  * 英文原文已归档至 en/2021-1112-storage/
* 2026.7.9
  * 校对 2021-0910 期（FreeBSD 开发）全部 7 篇文章
  * 校对《基金会来信》（添加格式头部：原文链接 + 作者）
  * 校对《活动日历》（格式头部从内联改为标准格式）
  * 校对《FreeBSD 代码审查与 git-arc》（路径加粗 tools/tools/git 和 /usr/local/bin、jq 链接补全、"待提交的审查" → "候选提交" 误译修正）
  * 校对《如何为 FreeBSD 实现简单的 USB 驱动程序》（补译 2 段漏译正文、清单标题统一为 "清单"、清单 10/11 标题误复制修正、"KMOD 调试工具" → "KMOD 变量" 误译修正、路径加粗）
  * 校对《内核开发技巧》（SoC 误译修正 "掌中宝般的小型系统" → "掌心大小的 SoC"、"待从" 误译修正、万能动词 "进行" 多处修正、"加快构建时间" → "缩短构建时间" 搭配错误修正、路径加粗 /tmp/kernel、/boot/loader.conf、/etc/rc.local、tools/test/stress2 等、转义字符 ${SRCDIR} 加粗、"提供一个第二个" → "提供第二个" 冗余修正等，共 21 处修改）
  * 校对《程序员编程杂谈》（ports→Ports 首字母大写、"三角象限" → "德尔塔象限" delta quadrant 误译修正）
  * 校对《读者来信》（添加格式头部：原文链接 + 作者、作者署名 MICHAEL W LUCAS 加粗）
  * 英文原文已归档至 en/2021-0910-freebsd-development/
* 2026.7.9
  * 校对 2021-0708 期（桌面/无线网）全部 9 篇文章
  * 校对《基金会来信》（补译 "recent"、添加作者元数据）
  * 校对《活动日历》（作者元数据格式规范）
  * 校对《通往 FreeBSD 桌面的直接路径》（误加 "之一" 删除、误加 "可能" 删除）
  * 校对《FreeBSD 13 中的 HID 支持》（"later in 2000" 误译修正、驱动名 cons→hcons、漏译 game controllers 补全、item 误译修正）
  * 校对《FreeBSD 上的 Panfrost 驱动》（tiling/tiler/tile 混淆修正、Vulcan→Vulkan、缺陷 → 怪癖、路径加粗、补冒号、FDT 语序修正、您 → 你）
  * 校对《用 Git 更新 FreeBSD》（您 → 你 3 处、补译漏译 -CURRENT、语序修正、URL 拼写修正 gitstash→git-stash 和 gitmerge→git-merge、修复重复代码块）
  * 校对《FreeBSD 的新面孔》（补译漏译句 "我在 Linux 上曾用过一段时间"、作者名加粗）
  * 校对《想给你的桌面加点佐料？》（翻译质量高，逐段对照后无需修改）
  * 校对《读者来信》（作者署名 MICHAEL W LUCAS 加粗，与本 issue 其他文章格式一致）
  * 英文原文已归档至 en/2021-0708-desktop-wireless/
* 2026.7.9
  * 校对 2021-0506 期（安全）全部 8 篇文章
  * 校对《基金会来信》
  * 校对《活动日历》
  * 校对《提升 FreeBSD 安全性的七种方法》
  * 校对《copyinout 框架》（清单标题格式修正、ABIs→ABI、PDF 残留 stray 代码块删除、拷贝 → 复制、一个 XX 删除、已字删除等）
  * 校对《使用 TLS 改善 NFS 安全性》（host/@REALM 补全、一个 XX 删除、进行万能动词删除、待完成 → 完成后误译修正、POSIX 用户凭证空括号补全、路径加粗等）
  * 校对《Capsicum 案例研究：Got》（一个 XX 删除、因此冗余删除、隐性 → 隐蔽、为其使用 → 供其使用、精细 → 微妙、/tmp 加粗、文件路径加粗、1Capsicum helpers\` typo 修复、黑客操作 → 拼凑、AT\_FDCWD 反引号等）
  * 校对《对 Jail 进行安全扫描》（以及 → 和、命令反引号、/tmp 和 /var 加粗、家庭目录 → 主目录误译修正、导致问题或阻止工作 → 导致或避免问题、/etc/login.conf 加粗等）
  * 校对《读者来信》（签名补全 "为勒索软件"，匹配英文 TBMfR 缩写）
  * 英文原文已归档至 en/2021-0506-security/
* 2026.7.9
  * 校对 2021-0304 期（FreeBSD 13.0）全部 10 篇文章
  * 校对《基金会来信》
  * 校对《活动日历》
  * 校对《展望未来》（作者简介格式统一）
  * 校对《FreeBSD 13 工具链》（作者简介格式统一）
  * 校对《FreeBSD 13 中的新加载器》（路径加粗、teken 译名修正、多处欧化汉语修正）
  * 校对《TCP Cubic 准备起飞》（作者元数据去加粗、误译修正 running these numbers/aggression、多处欧化修正）
  * 校对《OpenZFS 中的 Zstandard 压缩》（作者元数据去加粗、量词删除、万能动词删除、命令行参数格式修复、术语统一包 →Port）
  * 校对《会议报告：FreeBSD 供应商峰会》（作者元数据去加粗、误译 update→ 工作汇报、被动句改主动、个别视频 → 各场视频）
  * 校对《Git 不够吗？》（作者元数据去加粗、补译漏译专栏引言、习语 rolling them back 修正、多处欧化修正）
  * 校对《读者来信》（补加缺失元数据头部、补译 incorruptible/tantrum/meat machine、误译修正）
  * 英文原文已归档至 en/2021-0304-freebsd-13-0/
  * 全部作者元数据与文末简介统一为不加粗格式（参照 2023-0708 期规范）
* 2026.7.9
  * 三工具全书清理（AutoCorrect、md-padding\@latest、markdownlint）
  * markdownlint：680 个 .md 文件检查，0 违规
  * AutoCorrect：121 条建议逐项审核，23 条采纳（CJK-Latin 加空格，如 "Ports 中" → "Ports 中"），1 条特殊修复（FOSDEM 括号位置），97 条拒绝（全角标点/连字符复合词/书名号/脚注标记/CLAUDE.md 表格）
  * md-padding：784 条建议逐项审核，全部拒绝（237 条引号两侧加空格、69 条代码内加空格、4 条 HTML sup 标签、2 条脚注标记、1 条 ^W 修辞符号、其余为标点/代码内容修改误报）
  * 修改涉及 16 个文件，23 处采纳修改 + 1 处特殊修复
  * 总复核：重新运行三工具验证，markdownlint 0 违规、AutoCorrect 99 条建议均为已拒绝类型、md-padding 183 文件 +784/-765 均为已拒绝误报
  * 全部采纳均使用 Edit 工具逐个手动修改，无 --fix、无批量接受
* 2026.7.9
  * 校对 2021-0102 期（案例研究）全部 9 篇文章
  * 校对《基金会来信》（无需修改）
  * 校对《活动日历》（1 处语法修正）
  * 校对《Tarsnap 的 FreeBSD 集群》（6 处修正，含漏译补全、因果关系颠倒、弯引号、官方称号误译）
  * 校对《BALLY WULFF》（3 处修正，含万能动词、误译、时态错误）
  * 校对《Netflix Open Connect》（3 处修正，含漏译整段引言、语序混乱）
  * 校对《FreeBSD 的新面孔》（1 处译者注格式修正）
  * 校对《写作学者的 FreeBSD》（6 处修正，含译者注格式、句子结构、指代补全、之一滥用、术语一致性）
  * 校对《在世界之巅》（1 处术语一致性修正）
  * 校对《读者来信》（5 处段落拆分，恢复原文喜剧节奏）
  * 英文原文已归档至 en/2021-0102-case-studies/
* 2026.7.9
  * 校对 2023-0708 期（容器与云）全部 9 篇文章
  * 校对《基金会来信》（3 处欧化修正）
  * 校对《悼念：Hans Petter Selasky》（13 处修正）
  * 校对《活动日历》（1 处格式修正）
  * 校对《在 Firecracker 上的 FreeBSD》（2 处修正）
  * 校对《使用 pot 和 nomad 管理 Jail》（5 处修正，含 1 处漏译补全）
  * 校对《会议报告：C 与 BSD 正如拉丁语与我们——一位神学家的旅程》（17 处修正）
  * 校对《抒怀之旅：与 Doug Rabson 的访谈》（约 45 处修正）
  * 校对《基于 Jail 的广告拦截教程》（15 处修正）
  * 校对《我们收到的来信》（9 处修正）
  * 英文原文已归档至 en/2023-0708-containers-and-cloud/
* 2026.7.8
  * 校对 2023-0910 期（Ports and Packages）全部 8 篇文章
  * 校对《基金会来信》
  * 校对《活动日历》
  * 校对《回忆录：与 Warner Losh（@imp）的访谈》
  * 校对《在你自己的仓库中定制 Poudriere 源》
  * 校对《Wazuh 和 MITRE Caldera 在 FreeBSD Jail 中的使用》
  * 校对《PEP 517》
  * 校对《CCCamp 2023 旅行报告》
  * 校对《读者来信》
  * 归档英文原文至 en/2023-0910-ports-and-packages/
* 2024.11.16 校对《实用软件：开发定制 Ansible 模块》
* 2024.11.15
  * 校对《TCP LRO 简介》
  * 校对《TCP/IP 历险记：TCP BBLog》
  * 校对《FreeBSD 中的 RACK 栈和替代 TCP 栈》
  * 校对《FreeBSD 14 中有关 TCP 的更新》
  * 校对《FreeBSD 中的 NVMe-oF》
  * 校对《使用 ZFS 原生加密保护数据》
  * 校对《嵌入式 FreeBSD：打造自己的镜像》
  * 校对《基于 Samba 的时间机器备份》
  * 校对《从零开始的 ZFS 镜像及 makefs -t zfs》
  * 校对《if\_ovpn 还是 OpenVPN》
* 2024.11.14
  * 校对《FreeBSD 与 KDE 持续集成（CI）》
  * 校对《实用软件：使用 Zabbix 监控主机》
  * 校对《BATMAN：更优的可移动热点网络方式》
  * 校对《FreeBSD 接口 API（IfAPI）》
  * 校对《FreeBSD 内核开发工作流程》
  * 校对《更现代的内核调试工具》
  * 校对《配置自己的 VPN——内置 FreeBSD、Wireguard、IPv6 和广告拦截（基于 FreeBSD）》
  * 校对《SR-IOV 已成为 FreeBSD 的重要功能》
  * 校对《FreeBSD iSCSI 入门》
* 2024.11.10
  * 校对《新的 Port 提交者：oel Bodenmann (<jbo@freebsd.org>)》
  * 重新机器翻译 2024-0708、2024-0506、2024-0102、2024-0304、2023-1112
  * 校对《现在用 Webhook 触发我》
* 2024.8.16 校对《提升 Git 使用体验》、重命名为“期刊”，杂志不符合原意。
* 2024.8.15 校对《嵌入式 FreeBSD 面包板》
* 2024.8.14 校对《2024 年 5-6 月来信》
* 2024.8.12 校对《Hashicorp Vault》、《在 GitHub 上向 FreeBSD 提交 PR》
* 2024.7.30 校对《rdist》
* 2024.7.29 校对《基本系统中的 mfsBSD》
* 2024.7.9 机器翻译 2024-0506、2024-0102、2024-0304、2023-1112。校对《悼念 Mike Karels》


# 目录

* [FreeBSD 期刊](/)
* [编辑日志](/changelog)

***

* [目录](/mu-lu)

## 2026-070809 生产部署

* [基金会来信](/2026070809-sheng-chan-bu-shu/letter-from-the-foundation)
* [活动日历](/2026070809-sheng-chan-bu-shu/events-calendar)
* [用 Sylve 整合家庭实验室](/2026070809-sheng-chan-bu-shu/consolidating-a-home-lab-with-sylve)
* [使用 FreeBSD 计算资源开展云运维](/2026070809-sheng-chan-bu-shu/cloud-operations-using-freebsd-compute)
* [用户空间中的 USB](/2026070809-sheng-chan-bu-shu/usb-in-user-space)
* [在 FreeBSD 上进行可审计的凭证泄露检查，无需发送完整密码](/2026070809-sheng-chan-bu-shu/auditable-credential-exposure-checks-on-freebsd-without-sending-full-passwords)
* [EuroBSDCon 参会报告](/2026070809-sheng-chan-bu-shu/eurobsdcon-trip-report)
* [读者来信](/2026070809-sheng-chan-bu-shu/we-get-letters)

## 2026-040506 改进软件质量

* [基金会来信](/2026040506-gai-jin-ruan-jian-zhi-liang/letter-from-the-foundation)
* [活动日历](/2026040506-gai-jin-ruan-jian-zhi-liang/events-calendar)
* [使用 CHERI 调试](/2026040506-gai-jin-ruan-jian-zhi-liang/debugging-with-cheri)
* [FreeBSD 上的现代 Java](/2026040506-gai-jin-ruan-jian-zhi-liang/modern-java-on-freebsd)
* [来吧，友好的 SBOM](/2026040506-gai-jin-ruan-jian-zhi-liang/come-friendly-sboms)
* [FreeBSD WiFi 开发第三部分：调试](/2026040506-gai-jin-ruan-jian-zhi-liang/wifi-development-part-3)
* [打包、CloudInit，以及重温“好的、坏的和丑陋的”](/2026040506-gai-jin-ruan-jian-zhi-liang/packaging-cloudinit)
* [读者来信](/2026040506-gai-jin-ruan-jian-zhi-liang/we-get-letters)

## 2026-010203 笔记本与桌面

* [基金会来信](/2026010203-bi-ji-ben-yu-zhuo-mian/letter-from-the-foundation)
* [活动日历](/2026010203-bi-ji-ben-yu-zhuo-mian/events-calendar)
* [内核黑客的慰藉](/2026010203-bi-ji-ben-yu-zhuo-mian/consolations-for-kernel-hackers)
* [让休眠的 CPU 安睡 — S0ix](/2026010203-bi-ji-ben-yu-zhuo-mian/let-sleeping-cpus-lie-s0ix)
* [基金会的笔记本电脑支持与可用性项目是如何形成的](/2026010203-bi-ji-ben-yu-zhuo-mian/how-the-foundations-laptop-support-usability-project-came-together)
* [TCP/IP 历险记：改进 TCP 对重排序的响应](/2026010203-bi-ji-ben-yu-zhuo-mian/adventures-in-tcp-ip-improving-tcps-responses-to-reordering)
* [复制你的系统：使用 duplicity 备份你的 FreeBSD 桌面](/2026010203-bi-ji-ben-yu-zhuo-mian/duplicating-your-system)
* [读者来信](/2026010203-bi-ji-ben-yu-zhuo-mian/we-get-letters)

## 2025-101112 FreeBSD 15.0

* [基金会来信](/2025-101112-freebsd-15.0/letter-from-the-foundation)
* [活动日历](/2025-101112-freebsd-15.0/events-calendar)
* [FreeBSD 15.0：改进与特性](/2025-101112-freebsd-15.0/freebsd-15-0-fixes-and-features)
* [FreeBSD 上的通用闪存存储（UFS）](/2025-101112-freebsd-15.0/universal-flash-storage-on-freebsd)
* [Vox FreeBSD：如何让音频工作](/2025-101112-freebsd-15.0/vox-freebsd-how-sound-works)
* [使用 mdo(1) 与 mac\_do(4) 进行凭证转换](/2025-101112-freebsd-15.0/credentials-transitions-with-mdo1-and-mac_do4)
* [printf("Hello, srcmgr\n");](/2025-101112-freebsd-15.0/printfhello-srcmgrn)
* [FreeBSD 与 GSoC 2025](/2025-101112-freebsd-15.0/freebsd-and-google-summer-of-code-2025)
* [嵌入式 FreeBSD：构建 U-boot](/2025-101112-freebsd-15.0/building-u-boot)
* [读者来信](/2025-101112-freebsd-15.0/we-get-letters)

## 2025-070809 嵌入式

* [基金会来信](/2025070809-qian-ru-shi/letter-from-the-foundation)
* [活动日历](/2025070809-qian-ru-shi/events-calendar)
* [开启 FreeBSD 开发之路 —— 专访 Igor Ostapenko](/2025070809-qian-ru-shi/interview-with-igor-ostapenko)
* [CHERIoT](/2025070809-qian-ru-shi/embedded)
* [FreeBSD、Home Assistant 与 rtl\_433](/2025070809-qian-ru-shi/freebsd-home-assistant-and-rtl_433)
* [撰写有效的 Bug 报告](/2025070809-qian-ru-shi/writing-effective-bug-reports)
* [实现量子安全网站](/2025070809-qian-ru-shi/implementing-a-quantum-safe-website)
* [FreeBSD WiFi 开发第二部分：驱动开发](/2025070809-qian-ru-shi/freebsd-wifi-development-part-2-working-on-a-driver)
* [嵌入式 FreeBSD：回顾与展望](/2025070809-qian-ru-shi/embedded-freebsd-looking-back-and-forward)
* [会议报告：2025 BSDCan](/2025070809-qian-ru-shi/bsd)
* [读者来信](/2025070809-qian-ru-shi/we-get-letters)

## 2025-040506 网络

* [基金会来信](/2025040506-wang-luo/letter-from-the-foundation)
* [活动日历](/2025040506-wang-luo/events-calendar)
* [采访 Kajetan Staszkiewicz](/2025040506-wang-luo/staszkiewicz)
* [使安装程序易于使用](/2025040506-wang-luo/usability)
* [Overlord：让部署 Jail 像编程一样快](/2025040506-wang-luo/code)
* [Netgraph 大众教程](/2025040506-wang-luo/us)
* [FreeBSD WiFi 开发第一部分——尝试 WiFi](/2025040506-wang-luo/development)
* [嵌入式 FreeBSD：自定义硬件](/2025040506-wang-luo/hardware)
* [TCP/IP 探险记：静态 Pacing](/2025040506-wang-luo/pacing)
* [读者来信](/2025040506-wang-luo/we-get-letters)

## 2025-010203 下游项目

* [活动日历](/2025010203-xia-you-xiang-mu/events-calendar)
* [FreeBSD 发布工程：新主管上任](/2025010203-xia-you-xiang-mu/release)
* [GhostBSD：从易用到挣扎与重生](/2025010203-xia-you-xiang-mu/ghostbsd)
* [BSD Now 与将来](/2025010203-xia-you-xiang-mu/now)
* [字符设备驱动教程（第三部分）](/2025010203-xia-you-xiang-mu/device)
* [学会走路——连接 GPIO 系统](/2025010203-xia-you-xiang-mu/gpio)
* [FreeBSD 中对 SYN 段的处理](/2025010203-xia-you-xiang-mu/syn)
* [FreeBSD 2024 年秋季峰会](/2025010203-xia-you-xiang-mu/fall)
* [会议报告：FOSDEM 2025](/2025010203-xia-you-xiang-mu/conference-report-fosdem-2025)
* [读者来信](/2025010203-xia-you-xiang-mu/we-get-letters)

## 2024-1112 虚拟化

* [活动日历](/20241112-xu-ni-hua/events-calendar)
* [字符设备驱动程序教程（第二部分）](/20241112-xu-ni-hua/zifu)
* [面向 Linux 和 Windows 用户的 bhyve](/20241112-xu-ni-hua/bhyce)
* [Xen 与 FreeBSD](/20241112-xu-ni-hua/xen)
* [Wifibox：一种嵌入式虚拟化无线路由器](/20241112-xu-ni-hua/wifibox)
* [嵌入式 FreeBSD：Fabric——起步阶段](/20241112-xu-ni-hua/qianrushi)
* [DGP：一种新的数据包控制方法](/20241112-xu-ni-hua/dgp)
* [会议报告：我在都柏林的 EuroBSDCon 体验](/20241112-xu-ni-hua/hui)
* [读者来信](/20241112-xu-ni-hua/we-get-letters)

## 2024-0910 内核开发

* [基金会来信](/20240910-nei-he-kai-fa/letter-from-the-foundation)
* [活动日历](/20240910-nei-he-kai-fa/events-calendar)
* [字符设备驱动程序教程](/20240910-nei-he-kai-fa/character)
* [VPP 移植到了 FreeBSD：基础用法](/20240910-nei-he-kai-fa/vpp)
* [利用 Kyua 的 Jail 功能提升 FreeBSD 测试套件的并行效率](/20240910-nei-he-kai-fa/enhancing)
* [FreeBSD 上的 Valgrind](/20240910-nei-he-kai-fa/valgrind)
* [嵌入式 FreeBSD：探索 bhyve](/20240910-nei-he-kai-fa/embedded)
* [TCP/IP 历险记：FreeBSD TCP 协议栈中的 Pacing](/20240910-nei-he-kai-fa/tcp)
* [实用软件：实现无纸化（Paperless）](/20240910-nei-he-kai-fa/go)
* [读者来信](/20240910-nei-he-kai-fa/we-get-letters)

## 2024-0708 存储与文件系统

* [基金会来信](/20240708-cun-chu-yu-wen-jian-xi-tong/letter-from-the-foundation)
* [活动日历](/20240708-cun-chu-yu-wen-jian-xi-tong/events-calendar)
* [FreeBSD 中的 NVMe-oF](/20240708-cun-chu-yu-wen-jian-xi-tong/nvme-of)
* [FreeBSD iSCSI 入门](/20240708-cun-chu-yu-wen-jian-xi-tong/iscsi)
* [使用 ZFS 原生加密保护数据](/20240708-cun-chu-yu-wen-jian-xi-tong/zfs)
* [嵌入式 FreeBSD：打造自己的镜像](/20240708-cun-chu-yu-wen-jian-xi-tong/freebsd)
* [TCP LRO 简介](/20240708-cun-chu-yu-wen-jian-xi-tong/tcp)
* [基于 Samba 的时间机器备份](/20240708-cun-chu-yu-wen-jian-xi-tong/samba)
* [会议报告：BSDCan 2024](/20240708-cun-chu-yu-wen-jian-xi-tong/conference-report-bsdcan-2024)
* [读者来信](/20240708-cun-chu-yu-wen-jian-xi-tong/we-get-letters)

## 2024-0506 配置管理对决

* [基金会来信](/20240506-pei-zhi-guan-li-dui-jue/letter-from-the-foundation)
* [活动日历](/20240506-pei-zhi-guan-li-dui-jue/events-calendar)
* [基本系统中的 mfsBSD](/20240506-pei-zhi-guan-li-dui-jue/ji-ben-xi-tong-zhong-de-mfsbsd)
* [rdist](/20240506-pei-zhi-guan-li-dui-jue/rdist)
* [Hashicorp Vault](/20240506-pei-zhi-guan-li-dui-jue/hashicorp-vault-bao-xian-ku)
* [在 GitHub 上向 FreeBSD 提交 PR](/20240506-pei-zhi-guan-li-dui-jue/zai-github-shang-xiang-freebsd-ti-jiao-pr)
* [悼念 Mike Karels](/20240506-pei-zhi-guan-li-dui-jue/dao-nian-mike-karels)
* [2024 年 5-6 月来信](/20240506-pei-zhi-guan-li-dui-jue/2024-nian-5-yue-6-yue-lai-xin)
* [嵌入式 FreeBSD 面包板](/20240506-pei-zhi-guan-li-dui-jue/qian-ru-shi-freebsdbreadcrumbs)
* [TCP/IP 历险记：TCP BBLog](/20240506-pei-zhi-guan-li-dui-jue/tcpip-li-xian-ji-tcp-bblog)
* [实用软件：开发定制 Ansible 模块](/20240506-pei-zhi-guan-li-dui-jue/shi-yong-ruan-jian-kai-fa-ding-zhi-ansible-mo-kuai)

## 2024-0304 开发工作流与集成

* [基金会来信](/20240304-kai-fa-gong-zuo-liu-yu-ji-cheng/letter-from-the-foundation)
* [活动日历](/20240304-kai-fa-gong-zuo-liu-yu-ji-cheng/events-calendar)
* [FreeBSD 内核开发工作流程](/20240304-kai-fa-gong-zuo-liu-yu-ji-cheng/freebsd-nei-he-kai-fa-gong-zuo-liu)
* [FreeBSD 与 KDE 持续集成（CI）](/20240304-kai-fa-gong-zuo-liu-yu-ji-cheng/kde-chi-xu-ji-cheng-ci-he-freebsd)
* [更现代的内核调试工具](/20240304-kai-fa-gong-zuo-liu-yu-ji-cheng/geng-xian-dai-de-nei-he-tiao-shi-gong-ju)
* [从零开始的 ZFS 镜像及 makefs -t zfs](/20240304-kai-fa-gong-zuo-liu-yu-ji-cheng/cong-ling-kai-shi-de-zfs-jing-xiang-ji-makefstzfs)
* [提升 Git 使用体验](/20240304-kai-fa-gong-zuo-liu-yu-ji-cheng/ti-sheng-git-shi-yong-ti-yan)
* [读者来信](/20240304-kai-fa-gong-zuo-liu-yu-ji-cheng/we-get-letters)

## 2024-0102 网络（十周年）

* [基金会来信](/20240102-wang-luo-shi-zhou-nian/letter-from-the-foundation)
* [活动日历](/20240102-wang-luo-shi-zhou-nian/events-calendar)
* [FreeBSD 中的 RACK 栈和替代 TCP 栈](/20240102-wang-luo-shi-zhou-nian/rack-he-freebsd-ke-yong-de-tcp-dui-zhan)
* [FreeBSD 14 中有关 TCP 的更新](/20240102-wang-luo-shi-zhou-nian/freebsd-14-you-guan-tcp-de-geng-xin)
* [if\_ovpn 还是 OpenVPN](/20240102-wang-luo-shi-zhou-nian/ifovpn-hai-shi-openvpn)
* [SR-IOV 已成为 FreeBSD 的重要功能](/20240102-wang-luo-shi-zhou-nian/sriov-yi-cheng-wei-freebsd-zhong-zhong-yao-de-gong-neng)
* [FreeBSD 接口 API（IfAPI）](/20240102-wang-luo-shi-zhou-nian/freebsd-jie-kou-apiifapi)
* [BATMAN：更优的移动 Ad-hoc 网络方案](/20240102-wang-luo-shi-zhou-nian/batman-geng-jia-de-ke-yi-dong-adhoc-wang-luo-fang-shi)
* [配置自己的 VPN——基于 FreeBSD、Wireguard、IPv6 和广告拦截](/20240102-wang-luo-shi-zhou-nian/zhi-zuo-zi-ji-de-vpn-nei-zhi-freebsdwireguardipv6-he-guang-gao-lan-jie)
* [实用 Port：使用 Zabbix 监控主机](/20240102-wang-luo-shi-zhou-nian/shi-yong-ruan-jian-shi-yong-zabbix-jian-kong-zhu-ji)

## 2023-1112 FreeBSD 14.0

* [基金会来信](/2023-1112-freebsd-14.0/letter-from-the-foundation)
* [活动日历](/2023-1112-freebsd-14.0/events-calendar)
* [LinuxBoot：从 Linux 启动 FreeBSD](/2023-1112-freebsd-14.0/linuxboot-cong-linux-qi-dong-freebsd)
* [FreeBSD 容器镜像](/2023-1112-freebsd-14.0/freebsd-rong-qi-jing-xiang)
* [现在用 Webhook 触发我](/2023-1112-freebsd-14.0/xian-zai-yong-webhooks-chu-fa-wo)
* [新的 Ports 提交者：Joel Bodenmann (](/2023-1112-freebsd-14.0/xin-de-port-ti-jiao-zhe-oel-bodenmann-jbofreebsd.org)<jbo@freebsd.org>)
* [读者来信](/2023-1112-freebsd-14.0/we-get-letters)

## 2023-0910 Port 与软件包

* [基金会来信](/20230910-port-yu-ruan-jian-bao/letter-from-the-foundation)
* [活动日历](/20230910-port-yu-ruan-jian-bao/events-calendar)
* [回忆录：与 Warner Losh（@imp）的访谈](/20230910-port-yu-ruan-jian-bao/recollections-an-interview-with-warner-losh-imp)
* [在你自己的仓库中定制 Poudriere 源](/20230910-port-yu-ruan-jian-bao/custom-poudriere-packages-in-your-own-repository)
* [Wazuh 和 MITRE Caldera 在 FreeBSD Jail 中的使用](/20230910-port-yu-ruan-jian-bao/wazuh-and-mitre-caldera-using-freebsd-jails)
* [PEP 517](/20230910-port-yu-ruan-jian-bao/pep-517)
* [CCCamp 2023 旅行报告](/20230910-port-yu-ruan-jian-bao/cccamp-2023-trip-report)
* [读者来信](/20230910-port-yu-ruan-jian-bao/we-get-letters)

## 2023-0708 容器与云

* [基金会来信](/20230708-rong-qi-yu-yun/letter-from-the-foundation)
* [悼念：Hans Petter Selasky](/20230708-rong-qi-yu-yun/in-memoriam-hans-petter-selasky)
* [活动日历](/20230708-rong-qi-yu-yun/events-calendar)
* [在 Firecracker 上的 FreeBSD](/20230708-rong-qi-yu-yun/freebsd-on-firecracker)
* [使用 pot 和 nomad 管理 Jail](/20230708-rong-qi-yu-yun/jail-orchestration-with-pot-and-nomad)
* [会议报告：C 与 BSD 正如拉丁语与我们——一位神学家的旅程](/20230708-rong-qi-yu-yun/conference-report-c-is-to-bsd-what-latin-is-to-us)
* [抒怀之旅：与 Doug Rabson 的访谈](/20230708-rong-qi-yu-yun/recollections-an-interview-with-doug-rabson)
* [基于 Jail 的广告拦截教程](/20230708-rong-qi-yu-yun/practical-ports)
* [我们收到的来信](/20230708-rong-qi-yu-yun/we-get-letters)

## 2023-0506 FreeBSD 三十周年纪念特刊

* [基金会来信](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/letter-from-the-foundation)
* [CheriBSD 近十多年的历程](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/cheribsd)
* [AArch64：成为 FreeBSD 新的一级架构](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/aarch64)
* [岁月如梭：我个人的时间线](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/timeline)
* [安装 FreeBSD 1.0：回顾 30 年前](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/installing)
* [ZFS 是如何进入 FreeBSD 的呢？](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/zfs)
* [我不是来自约克郡的，我保证！](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/promise)
* [回忆录：采访 David Greenman Lawrence](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/recollections)
* [FreeBSD 和早期的 Unix 社区](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/unix)
* [早期的 FreeBSD 移植](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/ports)
* [FreeBSD 30 周年：成功的秘诀](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/30)
* [FreeBSD 在日本：回忆之旅与今日之实](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/japan)
* [读者来信](/20230506-freebsd-san-shi-zhou-nian-ji-nian-te-kan/we-get-letters)

## 2023-0304 嵌入式

* [基金会来信](/20230304-qian-ru-shi/letter-from-the-foundation)
* [活动日历](/20230304-qian-ru-shi/events-calendar)
* [CheriBSD Port 和软件包](/20230304-qian-ru-shi/cheribsd)
* [让我们来试试 ChatGPT](/20230304-qian-ru-shi/gpt)
* [GPU 直通](/20230304-qian-ru-shi/gpu)
* [会议报告：SCaLE20X](/20230304-qian-ru-shi/conference-report)
* [读者来信](/20230304-qian-ru-shi/we-get-letters)

## 2023-0102 构建 FreEBSD Web 服务器

* [活动日历](/20230102-gou-jian-freebsd-web-fu-wu-qi/events-calendar)
* [ZFS 的原子 I/O 与 PostgreSQL](/20230102-gou-jian-freebsd-web-fu-wu-qi/zfs)
* [虚拟实验室——BSD 编程研讨会](/20230102-gou-jian-freebsd-web-fu-wu-qi/lab)
* [ZFS 简介](/20230102-gou-jian-freebsd-web-fu-wu-qi/zfs2)
* [会议报告：落基山庆祝女性计算机科学家](/20230102-gou-jian-freebsd-web-fu-wu-qi/women)
* [进行中的工作/征求反馈：数据包批处理](/20230102-gou-jian-freebsd-web-fu-wu-qi/batch)
* [基金会与 FreeBSD 桌面](/20230102-gou-jian-freebsd-web-fu-wu-qi/desktop)
* [读者来信](/20230102-gou-jian-freebsd-web-fu-wu-qi/we-get-letters)

## 2022-1112 可观测性和衡量标准

* [基金会来信](/20221112-ke-guan-ce-xing-he-heng-liang-biao-zhun/letter-from-the-foundation)
* [活动日历](/20221112-ke-guan-ce-xing-he-heng-liang-biao-zhun/events-calendar)
* [在 FreeBSD 的 DDB 内核调试器中编写自定义命令](/20221112-ke-guan-ce-xing-he-heng-liang-biao-zhun/ddb)
* [DTrace：老式跟踪系统的新扩展](/20221112-ke-guan-ce-xing-he-heng-liang-biao-zhun/dtrace)
* [基于证书的 Icinga 监控](/20221112-ke-guan-ce-xing-he-heng-liang-biao-zhun/icinga)
* [活动监控脚本（activitymonitor.sh）](/20221112-ke-guan-ce-xing-he-heng-liang-biao-zhun/act)
* [实用 IPv6（第四部分）](/20221112-ke-guan-ce-xing-he-heng-liang-biao-zhun/ipv6)
* [EuroBSDCon 会议报道](/20221112-ke-guan-ce-xing-he-heng-liang-biao-zhun/huiyi)
* [实用 Port：Prometheus 的安装与配置](/20221112-ke-guan-ce-xing-he-heng-liang-biao-zhun/prometheus)
* [书评：*Kill It with Fire: Manage Aging Computer Systems (and Future Proof Modern Ones)*](/20221112-ke-guan-ce-xing-he-heng-liang-biao-zhun/book)
* [读者来信](/20221112-ke-guan-ce-xing-he-heng-liang-biao-zhun/we-get-letters)

## 2022-0910 安全性

* [基金会来信](/20220910-an-quan-xing/letter-from-the-foundation)
* [活动日历](/20220910-an-quan-xing/events-calendar)
* [CARP 简介](/20220910-an-quan-xing/carp)
* [重构内核加密服务框架](/20220910-an-quan-xing/kernel)
* [PAM 小窍门](/20220910-an-quan-xing/pam)
* [SSH 小窍门](/20220910-an-quan-xing/ssh)
* [实用 IPv6（第三部分）](/20220910-an-quan-xing/ipv6)
* [书评：《深入理解软件性能——一种动态视角》—— Richard L. Sites 著](/20220910-an-quan-xing/book)
* [访谈：保障 FreeBSD 安全性](/20220910-an-quan-xing/anquan)
* [MCH 2022 会议报告](/20220910-an-quan-xing/mch)
* [读者来信](/20220910-an-quan-xing/we-get-letters)

## 2022-0708 科研、系统与 FreeBSD

* [基金会来信](/20220708-ke-yan-xi-tong-yu-freebsd/letter-from-the-foundation)
* [活动日历](/20220708-ke-yan-xi-tong-yu-freebsd/events-calendar)
* [在 FreeBSD 上构建 Loom 框架](/20220708-ke-yan-xi-tong-yu-freebsd/loom)
* [教授本科生 Unix 课程](/20220708-ke-yan-xi-tong-yu-freebsd/unix)
* [FreeBSD 入门研讨会](/20220708-ke-yan-xi-tong-yu-freebsd/bsd)
* [实用 IPv6（第二部分）](/20220708-ke-yan-xi-tong-yu-freebsd/ipv6)
* [在 2022 年及以后推广 FreeBSD](/20220708-ke-yan-xi-tong-yu-freebsd/2022)
* [进行中的工作/征求反馈：Socket 缓冲区](/20220708-ke-yan-xi-tong-yu-freebsd/socket)
* [FreeBSD 开发者峰会报告](/20220708-ke-yan-xi-tong-yu-freebsd/huiyi)
* [支持 Electromagnetic Field 2022](/20220708-ke-yan-xi-tong-yu-freebsd/ele)
* [读者来信](/20220708-ke-yan-xi-tong-yu-freebsd/we-get-letters)

## 2022-0506 灾难恢复

* [基金会来信](/20220506-zai-nan-hui-fu/letter-from-the-foundation)
* [活动日历](/20220506-zai-nan-hui-fu/events-calendar)
* [使用 FreeBSD 构建高弹性的私有云](/20220506-zai-nan-hui-fu/cloud)
* [LLDB 14 —— FreeBSD 新调试器](/20220506-zai-nan-hui-fu/lldb)
* [实用 IPv6（第一部分）](/20220506-zai-nan-hui-fu/ipv6)
* [利用 **netdump(4)** 进行事后内核调试](/20220506-zai-nan-hui-fu/netdump)
* [进行中的工作/征求反馈：FreeBSD 启动性能](/20220506-zai-nan-hui-fu/freebsd)
* [实用 Port：在 OpenZFS 上设置 NFSv4 文件服务器](/20220506-zai-nan-hui-fu/port)
* [读者来信](/20220506-zai-nan-hui-fu/we-get-letters)

## 2022-0304 ARM64 是一级架构

* [基金会来信](/20220304-arm64-shi-yi-ji-jia-gou/letter-from-the-foundation)
* [活动日历](/20220304-arm64-shi-yi-ji-jia-gou/events-calendar)
* [FreeBSD/ARM64 上的数据科学](/20220304-arm64-shi-yi-ji-jia-gou/datascience)
* [Pinebook Pro 上的 FreeBSD](/20220304-arm64-shi-yi-ji-jia-gou/pinebook)
* [嵌入式控制器的 ACPI 支持](/20220304-arm64-shi-yi-ji-jia-gou/acpi)
* [进行中的工作/征求反馈：Lumina 桌面征集开发人员](/20220304-arm64-shi-yi-ji-jia-gou/mkjail)
* [实用 Port：如何设置 Apple 时间机器](/20220304-arm64-shi-yi-ji-jia-gou/apple)
* [读者来信](/20220304-arm64-shi-yi-ji-jia-gou/we-get-letters)

## 2022-0102 软件与系统管理

* [基金会来信](/20220102-ruan-jian-yu-xi-tong-guan-li/letter-from-the-foundation)
* [活动日历](/20220102-ruan-jian-yu-xi-tong-guan-li/events-calendar)
* [为 FreeBSD Ports 做贡献](/20220102-ruan-jian-yu-xi-tong-guan-li/ports)
* [使用 Git 贡献到 FreeBSD Ports](/20220102-ruan-jian-yu-xi-tong-guan-li/git)
* [CBSD：第一部分——生产环境](/20220102-ruan-jian-yu-xi-tong-guan-li/cbsd)
* [将 OpenBSD 的 pf syncookie 代码移植到 FreeBSD 的 pf](/20220102-ruan-jian-yu-xi-tong-guan-li/pf)
* [进行中的工作/征求反馈：mkjail](/20220102-ruan-jian-yu-xi-tong-guan-li/mkjail)
* [书评：《编程智慧：编程鬼才的经验和思考》](/20220102-ruan-jian-yu-xi-tong-guan-li/book)
* [会议报告：EuroBSDCon 2021 我的第一次 EuroBSDCon：一位新组织者的视角](/20220102-ruan-jian-yu-xi-tong-guan-li/huiyi)
* [读者来信](/20220102-ruan-jian-yu-xi-tong-guan-li/we-get-letters)

## 2021-1112 存储

* [基金会来信](/20211112-cun-chu/letter-from-the-foundation)
* [活动日历](/20211112-cun-chu/events-calendar)
* [开放通道 SSD](/20211112-cun-chu/ssd)
* [构建 FreeBSD 社区](/20211112-cun-chu/shequ)
* [与完美操作系统同行 27 年](/20211112-cun-chu/27)
* [进行中的工作/征求反馈：OccamBSD](/20211112-cun-chu/bsd)
* [通过 iSCSI 导入 ZFS ZIL——不要在工作中这样做——就像我做的那样](/20211112-cun-chu/port)
* [读者来信](/20211112-cun-chu/we-get-letters)

## 2021-0910 FreeBSD 开发

* [基金会来信](/20210910-freebsd-kai-fa/letter-from-the-foundation)
* [活动日历](/20210910-freebsd-kai-fa/events-calendar)
* [FreeBSD 代码审查与 git-arc](/20210910-freebsd-kai-fa/git)
* [如何为 FreeBSD 实现简单的 USB 驱动程序](/20210910-freebsd-kai-fa/usb)
* [内核开发技巧](/20210910-freebsd-kai-fa/kernel)
* [程序员编程杂谈](/20210910-freebsd-kai-fa/programmers)
* [读者来信](/20210910-freebsd-kai-fa/we-get-letters)

## 2021-0708 桌面/无线网

* [基金会来信](/20210708-zhuo-mian-wu-xian-wang/letter-from-the-foundation)
* [活动日历](/20210708-zhuo-mian-wu-xian-wang/events-calendar)
* [通往 FreeBSD 桌面的直线路径](/20210708-zhuo-mian-wu-xian-wang/desktop)
* [FreeBSD 13 中的人机接口设备 (HID) 支持](/20210708-zhuo-mian-wu-xian-wang/hid)
* [Panfrost 驱动程序](/20210708-zhuo-mian-wu-xian-wang/panfrost)
* [用 Git 更新 FreeBSD](/20210708-zhuo-mian-wu-xian-wang/git)
* [FreeBSD 的新面孔](/20210708-zhuo-mian-wu-xian-wang/new)
* [想给你的桌面加点佐料？](/20210708-zhuo-mian-wu-xian-wang/desk)
* [读者来信](/20210708-zhuo-mian-wu-xian-wang/we-get-letters)

## 2021-0506 安全

* [基金会来信](/20210506-an-quan/letter-from-the-foundation)
* [活动日历](/20210506-an-quan/events-calendar)
* [七种提升新安装 FreeBSD 安全性的方法](/20210506-an-quan/security)
* [copyinout 框架](/20210506-an-quan/copyinout)
* [使用 TLS 改善 NFS 安全性](/20210506-an-quan/tls)
* [Capsicum 案例研究：Got](/20210506-an-quan/got)
* [对 Jail 进行安全扫描](/20210506-an-quan/jail)
* [读者来信](/20210506-an-quan/we-get-letters)

## 2021-0304 FreeBSD 13.0

* [基金会来信](/2021-0304-freebsd-13.0/letter-from-the-foundation)
* [活动日历](/2021-0304-freebsd-13.0/events-calendar)
* [展望未来](/2021-0304-freebsd-13.0/future)
* [FreeBSD 13.0 工具链](/2021-0304-freebsd-13.0/toolchain)
* [FreeBSD 13.0 中有新加载器吗？](/2021-0304-freebsd-13.0/loader)
* [TCP Cubic 准备起飞](/2021-0304-freebsd-13.0/tcp)
* [OpenZFS 中的 Zstandard 压缩](/2021-0304-freebsd-13.0/openzfs)
* [会议报告：FreeBSD 供应商峰会](/2021-0304-freebsd-13.0/report)
* [Git 不够吗？](/2021-0304-freebsd-13.0/git)
* [读者来信](/2021-0304-freebsd-13.0/we-get-letters)

## 2021-0102 案例研究

* [基金会来信](/20210102-an-li-yan-jiu/letter-from-the-foundation)
* [活动日历](/20210102-an-li-yan-jiu/events-calendar)
* [Tarsnap 的 FreeBSD 集群](/20210102-an-li-yan-jiu/tarsnap)
* [BALLY WULFF](/20210102-an-li-yan-jiu/bally)
* [Netflix Open Connect](/20210102-an-li-yan-jiu/netflix)
* [FreeBSD 的新面孔](/20210102-an-li-yan-jiu/faces)
* [写作学者的 FreeBSD](/20210102-an-li-yan-jiu/writing)
* [在世界之巅](/20210102-an-li-yan-jiu/top)
* [读者来信](/20210102-an-li-yan-jiu/we-get-letters)

## 2020-1112 工作流/持续集成（CI）

* [基金会来信](/20201112-gong-zuo-liu-chi-xu-ji-cheng-ci/letter-from-the-foundation)
* [活动日历](/20201112-gong-zuo-liu-chi-xu-ji-cheng-ci/events-calendar)
* [FreeBSD Git 快速入门](/20201112-gong-zuo-liu-chi-xu-ji-cheng-ci/git)
* [使用 syzkaller 进行内核 Fuzzing](/20201112-gong-zuo-liu-chi-xu-ji-cheng-ci/syzkaller)
* [书评：《Mastering Vim Quickly》](/20201112-gong-zuo-liu-chi-xu-ji-cheng-ci/book)
* [线上会议实用技巧](/20201112-gong-zuo-liu-chi-xu-ji-cheng-ci/online)
* [在控制台上进行网络监控](/20201112-gong-zuo-liu-chi-xu-ji-cheng-ci/ports)
* [读者来信](/20201112-gong-zuo-liu-chi-xu-ji-cheng-ci/we-get-letters)

## 2020-0910 贡献与入门

* [基金会来信](/20200910-gong-xian-yu-ru-men/letter-from-the-foundation)
* [活动日历](/20200910-gong-xian-yu-ru-men/events-calendar)
* [采访：Warner Losh，第 2 部分](/20200910-gong-xian-yu-ru-men/warner)
* [代码审查](/20200910-gong-xian-yu-ru-men/code)
* [撰写良好的提交消息](/20200910-gong-xian-yu-ru-men/commit)
* [如何在不是程序员的情况下做出贡献——成为 FreeBSD 译者](/20200910-gong-xian-yu-ru-men/programmer)
* [如何成为文档提交者](/20200910-gong-xian-yu-ru-men/committer)
* [谷歌编程之夏](/20200910-gong-xian-yu-ru-men/google)
* [为 FreeBSD 期刊撰写文章](/20200910-gong-xian-yu-ru-men/journal)
* [你为什么使用 FreeBSD](/20200910-gong-xian-yu-ru-men/why)
* [FreeBSD 的新面孔](/20200910-gong-xian-yu-ru-men/faces)
* [读者来信](/20200910-gong-xian-yu-ru-men/we-get-letters)

## 2020-0708 基准测试/调优

* [基金会来信](/20200708-ji-zhun-ce-shi-tiao-you/letter-from-the-foundation)
* [活动日历](/20200708-ji-zhun-ce-shi-tiao-you/events-calendar)
* [FreeBSD Friday](/20200708-ji-zhun-ce-shi-tiao-you/friday)
* [采访：Warner Losh，第 1 部分](/20200708-ji-zhun-ce-shi-tiao-you/losh)
* [构建和运行开源社区](/20200708-ji-zhun-ce-shi-tiao-you/community)
* [在 FreeBSD 上轻松搭建我的世界（Minecraft）服务器](/20200708-ji-zhun-ce-shi-tiao-you/minecraft)
* [FreeBSD 的新面孔](/20200708-ji-zhun-ce-shi-tiao-you/faces)
* [读者来信](/20200708-ji-zhun-ce-shi-tiao-you/we-get-letters)

## 2020-0506 网络性能

* [基金会来信](/20200506-wang-luo-xing-neng/letter-from-the-foundation)
* [活动日历](/20200506-wang-luo-xing-neng/events-calendar)
* [内核中的 TLS 卸载](/20200506-wang-luo-xing-neng/tls)
* [访谈：Michael W Lucas](/20200506-wang-luo-xing-neng/lucas)
* [FreeBSD 桌面发行版](/20200506-wang-luo-xing-neng/desktop)
* [使用 Poudriere 进行 Ports 批量管理](/20200506-wang-luo-xing-neng/port)
* [FreeBSD 的新面孔](/20200506-wang-luo-xing-neng/faces)
* [读者来信](/20200506-wang-luo-xing-neng/we-get-letters)

## 2020-0304 文件系统

* [会议报告：SCALE 18x](/20200304-wen-jian-xi-tong/conference-report-scale-18x)
* [活动日历](/20200304-wen-jian-xi-tong/events-calendar)
* [基金会来信](/20200304-wen-jian-xi-tong/foundation-letter)
* [FreeBSD 基金会项目开发更新：工具链现代化](/20200304-wen-jian-xi-tong/freebsd-foundation-project-development-update-toolchain-modernization)
* [FUSE 驱动更新](/20200304-wen-jian-xi-tong/fuse-driver-update)
* [FreeBSD 新面孔](/20200304-wen-jian-xi-tong/new-faces-of-freebsd)
* [OpenZFS 加密功能登陆 FreeBSD](/20200304-wen-jian-xi-tong/openzfs-encryption-arrives-on-freebsd)
* [朴素的网桥](/20200304-wen-jian-xi-tong/the-humble-bridge)
* [读者来信](/20200304-wen-jian-xi-tong/we-get-letters)

## 2020-0102 FreeBSD 在研究中

* [活动日历](/20200102-freebsd-zai-yan-jiu-zhong/events-calendar)
* [基金会来信](/20200102-freebsd-zai-yan-jiu-zhong/foundation-letter)
* [FreeBSD 基金会报告](/20200102-freebsd-zai-yan-jiu-zhong/freebsd-foundation-report)
* [FreeBSD 在网络研究与标准化中的应用](/20200102-freebsd-zai-yan-jiu-zhong/freebsd-in-network-research-and-standardization)
* [FreeBSD/RISC-V 入门](/20200102-freebsd-zai-yan-jiu-zhong/getting-started-with-freebsd-risc-v)
* [Trenton Schulz 专访](/20200102-freebsd-zai-yan-jiu-zhong/interview-with-trenton-schulz)
* [Jail vnet 实例教程](/20200102-freebsd-zai-yan-jiu-zhong/jail-vnet-by-examples)
* [FreeBSD 新面孔](/20200102-freebsd-zai-yan-jiu-zhong/new-faces-of-freebsd)
* [读者来信](/20200102-freebsd-zai-yan-jiu-zhong/we-get-letters)

## 2019-1112 网络虚拟化

* [Kirk McKusick 访谈](/20191112-wang-luo-xu-ni-hua/an-interview-with-kirk-mckusick)
* [在 FreeBSD 上组织你的虚拟网络](/20191112-wang-luo-xu-ni-hua/arranging-your-virtual-network-on-freebsd)
* [我的 BSD 会议旅行之年](/20191112-wang-luo-xu-ni-hua/conference-reports-my-year-of-bsd-conference-talks)
* [活动日历](/20191112-wang-luo-xu-ni-hua/events-calendar)
* [Jail 迁移](/20191112-wang-luo-xu-ni-hua/jail-migration)
* [基金会来信](/20191112-wang-luo-xu-ni-hua/letter-from-the-foundation)
* [FreeBSD 新面孔](/20191112-wang-luo-xu-ni-hua/new-faces-of-freebsd)
* [读者来信](/20191112-wang-luo-xu-ni-hua/we-get-letters)

## 2019-0910 安全

* [2019 年 Capsicum 更新](/20190910-an-quan/capsicum-update-2019)
* [会议报告](/20190910-an-quan/conference-reports)
* [在 FreeBSD 上配置全盘加密](/20190910-an-quan/configuring-full-disk-encryption)
* [活动日历](/20190910-an-quan/events-calendar)
* [改进 FreeBSD 中的内存权限](/20190910-an-quan/improving-memory-permissions-in-freebsd)
* [采访 Pawel Dawidek](/20190910-an-quan/interview-with-pawel-dawidek)
* [基金会来信](/20190910-an-quan/letter-from-the-foundation)
* [读者来信](/20190910-an-quan/we-get-letters)

## 2019-0708 容器化

* [活动日历](/20190708-rong-qi-hua/events-calendar)
* [基金会来信](/20190708-rong-qi-hua/letter-from-the-foundation)
* [FreeBSD 新面孔](/20190708-rong-qi-hua/new-faces-of-freebsd)
* [读者来信](/20190708-rong-qi-hua/we-get-letters)
* [会议报告](/20190708-rong-qi-hua/conference-report)
* [使用 Ezjail 进行高级 Jail 管理](/20190708-rong-qi-hua/advanced-jail-management-with-ezjail)
* [iocage：真正的 Jail 管理器](/20190708-rong-qi-hua/iocage-the-one-true-jail-manager)
* [HashiCorp Nomad](/20190708-rong-qi-hua/hashicorp-nomad)

## 2019-0506 面向创客的 FreeBSD

* [会议报告](/20190506-mian-xiang-chuang-ke-de-freebsd/conference-report)
* [活动日历](/20190506-mian-xiang-chuang-ke-de-freebsd/events-calendar)
* [FreeBSD 硬件黑客入门](/20190506-mian-xiang-chuang-ke-de-freebsd/introduction-to-hardware-hacking-on-freebsd)
* [基金会来信](/20190506-mian-xiang-chuang-ke-de-freebsd/letter-from-the-foundation)
* [非 RPi 上的媒体服务器](/20190506-mian-xiang-chuang-ke-de-freebsd/media-servers-on-non-rpi)
* [FreeBSD 新面孔](/20190506-mian-xiang-chuang-ke-de-freebsd/new-faces-of-freebsd)
* [在树莓派 3 上运行 Icinga2](/20190506-mian-xiang-chuang-ke-de-freebsd/running-icinga2-on-the-raspberry-pi3)
* [SVN 动态](/20190506-mian-xiang-chuang-ke-de-freebsd/svn-update)
* [读者来信](/20190506-mian-xiang-chuang-ke-de-freebsd/we-get-letters)

## 2019-0304 调试与测试

* [会议报告](/20190304-tiao-shi-yu-ce-shi/conference-report)
* [在 FreeBSD 上使用 GDB 调试](/20190304-tiao-shi-yu-ce-shi/debugging-with-gdb-on-freebsd)
* [活动日历](/20190304-tiao-shi-yu-ce-shi/events-calendar)
* [用 DTrace 排查 LDAP 连接过剩](/20190304-tiao-shi-yu-ce-shi/excess-ldap-connection-using-dtrace)
* [基金会来信](/20190304-tiao-shi-yu-ce-shi/letter-from-the-foundation)
* [FreeBSD 新面孔](/20190304-tiao-shi-yu-ce-shi/new-faces-of-freebsd)
* [SVN 动态](/20190304-tiao-shi-yu-ce-shi/svn-update)
* [自动化测试框架](/20190304-tiao-shi-yu-ce-shi/the-automated-testing-framework)
* [读者来信](/20190304-tiao-shi-yu-ce-shi/we-get-letters)

## 2019-0102 FreeBSD 入门

* [会议报告：MeetBSD 2018 与 FreeBSD 开发者峰会](/20190102-freebsd-ru-men/conference-report)
* [FreeBSD 入门指南：在虚拟机与真实硬件上安装](/20190102-freebsd-ru-men/download-a-guide-to-getting-started-with-freebsd-on-virtual-and-real-hardware)
* [活动日历](/20190102-freebsd-ru-men/events-calendar)
* [面向开发者的 FreeBSD](/20190102-freebsd-ru-men/freebsd-for-developers)
* [在 FreeBSD/GhostBSD 上开始 Java 开发](/20190102-freebsd-ru-men/getting-started-with-java-development)
* [基金会来信](/20190102-freebsd-ru-men/letter-from-the-foundation)
* [FreeBSD 的新面孔](/20190102-freebsd-ru-men/new-faces-of-freebsd)
* [SVN 动态](/20190102-freebsd-ru-men/svn-update)
* [向上游提交一处文档 bugfix](/20190102-freebsd-ru-men/upstreaming-a-documentation-bugfix)
* [读者来信](/20190102-freebsd-ru-men/we-get-letters)
* [编写手册页](/20190102-freebsd-ru-men/writing-manual-pages)

## 2018-1112 FreeBSD 12

* [会议报告](/2018-1112-freebsd-12/conference-report)
* [调试 FreeBSD 内核](/2018-1112-freebsd-12/debugging-the-freebsd-kernel)
* [活动日历](/2018-1112-freebsd-12/events-calendar)
* [基金会来信](/2018-1112-freebsd-12/foundation-letter)
* [FreeBSD 12.0 工具链更新](/2018-1112-freebsd-12/freebsd-12-0-toolchain-update)
* [FreeBSD 新面孔](/2018-1112-freebsd-12/new-faces-of-freebsd)
* [SVN 动态](/2018-1112-freebsd-12/svn-update)
* [The Daemon’s Dozen——FreeBSD 12.0 简介](/2018-1112-freebsd-12/the-daemons-dozen-introducing-freebsd-12-0)
* [读者来信](/2018-1112-freebsd-12/we-get-letters)
* [ZPool 检查点](/2018-1112-freebsd-12/zpool-checkpoint)

## 2018-0910 网络

* [使用 Mininet 快速浏览 SDN](/20180910-wang-luo/a-quick-tour-of-sdn-using-mininet)
* [会议报告](/20180910-wang-luo/conference-report)
* [活动日历](/20180910-wang-luo/events-calendar)
* [基金会来信](/20180910-wang-luo/foundation-letter)
* [FreeBSD 与处理器趋势](/20180910-wang-luo/freebsd-and-processor-trends)
* [FreeBSD 中的网络互联](/20180910-wang-luo/internetworking-in-freebsd)
* [FreeBSD 新面孔](/20180910-wang-luo/new-faces-of-freebsd)
* [SVN 动态](/20180910-wang-luo/svn-update)
* [Netflix 的 TCP 栈验证](/20180910-wang-luo/tcp-stack-validation-at-netflix)
* [读者来信](/20180910-wang-luo/we-get-letters)

## 2018-0708 大数据

* [活动日历](/20180708-da-shu-ju/events-calendar)
* [基金会来信](/20180708-da-shu-ju/foundation-letter)
* [FreeBSD 在科学计算中的应用](/20180708-da-shu-ju/freebsd-in-scientific-computing)
* [在 FreeBSD 上使用 ZFS 部署 Hadoop 教程](/20180708-da-shu-ju/hadoop-on-freebsd-with-zfs-tutorial)
* [高性能计算与 FreeBSD](/20180708-da-shu-ju/high-performance-computing-and-freebsd)
* [NVM Express 与 FreeBSD](/20180708-da-shu-ju/nvm-express-and-freebsd)
* [pNFS](/20180708-da-shu-ju/pnfs)
* [SVN 动态](/20180708-da-shu-ju/svn-update)
* [读者来信](/20180708-da-shu-ju/we-get-letters)

## 2018-0506 安全

* [最佳安全实践](/20180506-an-quan/best-security-practices)
* [Capsicum：动手应用](/20180506-an-quan/capsicum-just-apply-me)
* [选择 FreeBSD 作为生产环境](/20180506-an-quan/choosing-freebsd-as-a-production-environment)
* [会议报告](/20180506-an-quan/conference-report)
* [活动日历](/20180506-an-quan/events-calendar)
* [基金会来信](/20180506-an-quan/foundation-letter)
* [FreeBSD 新面孔](/20180506-an-quan/new-faces-of-freebsd)
* [用 YubiKey 保护你的秘密](/20180506-an-quan/protect-your-secrets-with-yubikey)
* [SVN 动态](/20180506-an-quan/svn-update)

## 2018-0304 桌面/笔记本

* [书评：《凤凰项目：一个 IT 运维的传奇故事》](/20180304-zhuo-mian-bi-ji-ben/book-review)
* [会议报告](/20180304-zhuo-mian-bi-ji-ben/conference-report)
* [活动日历](/20180304-zhuo-mian-bi-ji-ben/events-calendar)
* [基金会来信](/20180304-zhuo-mian-bi-ji-ben/foundation-letter)
* [FreeBSD 笔记本评测](/20180304-zhuo-mian-bi-ji-ben/freebsd-laptop-shootout)
* [唤醒之路上的痛与苦](/20180304-zhuo-mian-bi-ji-ben/pain-and-suffering-on-the-road-to-resume)
* [SVN 动态](/20180304-zhuo-mian-bi-ji-ben/svn-update)
* [ZFS 2018 及展望](/20180304-zhuo-mian-bi-ji-ben/zfs-2018-and-onward)

## 2018-0102 存储

* [存储多路径综述](/20180102-cun-chu/a-review-of-storage-multipathing)
* [书评：*FreeBSD Mastery: Specialty Filesystems*](/20180102-cun-chu/book-review)
* [会议报告：BSDTW 2017](/20180102-cun-chu/conference-report)
* [活动日历](/20180102-cun-chu/events-calendar)
* [基金会来信](/20180102-cun-chu/foundation-letter)
* [SVN 动态](/20180102-cun-chu/svn-update)
* [磁带未死](/20180102-cun-chu/tapes-not-dead)
* [从用户空间到设备驱动追踪 ifconfig 命令](/20180102-cun-chu/tracing-ifconfig-commands-from-userspace-to-device-driver)

## 2017-1112 监控与度量

* [基金会来信](/20171112-jian-kong-yu-du-liang/foundation-letter)
* [什么是 Icinga？](/20171112-jian-kong-yu-du-liang/what-is-icinga)
* [监控 ZFS](/20171112-jian-kong-yu-du-liang/monitoring-zfs)
* [STDDEV，否则就是没发生过！](/20171112-jian-kong-yu-du-liang/stddev-it-didnt-happen)
* [用 Prometheus 进行监控与趋势分析](/20171112-jian-kong-yu-du-liang/monitoring-and-trending-with-prometheus)
* [SVN 动态](/20171112-jian-kong-yu-du-liang/svn-update)
* [FreeBSD 新面孔](/20171112-jian-kong-yu-du-liang/new-faces-of-freebsd)
* [会议报告](/20171112-jian-kong-yu-du-liang/conference-report)
* [活动日历](/20171112-jian-kong-yu-du-liang/events-calendar)

## 2017-0910 FreeBSD vs Linux

* [基金会来信](/2017-0910-freebsd-vs-linux/letter-from-the-foundation)
* [活动日历](/2017-0910-freebsd-vs-linux/events-calendar)
* [FreeBSD 不是 Linux 发行版](/2017-0910-freebsd-vs-linux/freebsd-is-not-a-linux-distro)
* [FreeBSD vs Linux: ZFS](/2017-0910-freebsd-vs-linux/freebsd-vs-linux-zfs)
* [Unix 沙箱技术比较](/2017-0910-freebsd-vs-linux/a-comparison-of-unix-sandboxing-techniques)
* [FreeBSD 管理架构的演变](/2017-0910-freebsd-vs-linux/the-evolution-of-freebsd-governance)
* [会议报告](/2017-0910-freebsd-vs-linux/conference-report)
* [FreeBSD 新面孔](/2017-0910-freebsd-vs-linux/new-faces-of-freebsd)
* [SVN 动态](/2017-0910-freebsd-vs-linux/svn-update)

## 2017-0708 资源控制

* [OpenRC：专访 Joe Maloney](/20170708-zi-yuan-kong-zhi/openrc)
* [Lynx 网络流量分析](/20170708-zi-yuan-kong-zhi/network-traffic-analysis-lynx)
* [会议报告](/20170708-zi-yuan-kong-zhi/conference-report)
* [面向 FreeBSD 用户的 Docker 入门](/20170708-zi-yuan-kong-zhi/understanding-docker-for-freebsd-users)
* [iohyve 入门](/20170708-zi-yuan-kong-zhi/getting-started-with-iohyve)
* [基金会致信](/20170708-zi-yuan-kong-zhi/letter-from-the-foundation)
* [FreeBSD 新面孔](/20170708-zi-yuan-kong-zhi/new-faces-of-freebsd)
* [SVN 动态](/20170708-zi-yuan-kong-zhi/svn-update)
* [活动日历](/20170708-zi-yuan-kong-zhi/events-calendar)

## 2017-0506 安全

* [活动日历](/20170506-an-quan/events-calendar)
* [CloudABI 的最新进展](/20170506-an-quan/whats-going-on-with-cloudabi)
* [走向无感知沙箱：Capsicum](/20170506-an-quan/toward-oblivious-sandboxing-wit-capsicum)
* [SVN 动态](/20170506-an-quan/svn-update)
* [pfSense 与安全](/20170506-an-quan/pfsense-and-security)
* [FreeBSD 的新面孔](/20170506-an-quan/new-faces-of-freebsd)
* [基金会来信](/20170506-an-quan/letter-from-the-foundation)
* [游戏中的 BSD](/20170506-an-quan/gaming-bsd)
* [FreeBSD 的防火墙盛宴](/20170506-an-quan/freebsds-firewall-feast)
* [CADETS：在 FreeBSD 上融合追踪与安全](/20170506-an-quan/cadets-blending-tracing-and-security-on-freebsd)
* [书评：*Designing BSD Rootkits: An Introduction to Kernel Hacking*](/20170506-an-quan/book-review-designing-bsd-rootkits)

## 2017-0304 配置管理

* [配置管理的白日梦](/20170304-pei-zhi-guan-li/the-configuration-management-pipe-dream)
* [书评：*DTrace: Dynamic Tracing in Oracle Solaris, Mac OS X and FreeBSD*](/20170304-pei-zhi-guan-li/book-review)
* [基金会致信](/20170304-pei-zhi-guan-li/letter-from-the-foundation)
* [会议报告：FOSDEM 2017](/20170304-pei-zhi-guan-li/conference-report)
* [没错，CFEngine 能管那个](/20170304-pei-zhi-guan-li/yes-cfengine-can-manage-that)
* [Vagrant](/20170304-pei-zhi-guan-li/vagrant)
* [FreeBSD 上的 SaltStack：入门指南](/20170304-pei-zhi-guan-li/saltstack-on-freebsd)
* [FreeBSD 上的 Puppet](/20170304-pei-zhi-guan-li/puppet-on-freebsd)
* [SVN 动态](/20170304-pei-zhi-guan-li/svn-update)
* [FreeBSD 新面孔](/20170304-pei-zhi-guan-li/new-faces-of-freebsd)
* [活动日历](/20170304-pei-zhi-guan-li/events-calendar)

## 2017-0102 全球可访问的 FreeBSD

* [基金会的全球努力](/20170102-quan-qiu-ke-fang-wen-de-freebsd/the-foundations-global-efforts)
* [SVN 动态](/20170102-quan-qiu-ke-fang-wen-de-freebsd/svn-update)
* [FreeBSD 新面孔](/20170102-quan-qiu-ke-fang-wen-de-freebsd/new-faces-of-freebsd)
* [基金会来信：欢迎来到 2017！](/20170102-quan-qiu-ke-fang-wen-de-freebsd/letter-from-the-foundation)
* [改进 FreeBSD 翻译工具](/20170102-quan-qiu-ke-fang-wen-de-freebsd/improving-the-freebsd-translation-tools)
* [由 FreeBSD 驱动的中国 LCD 广告显示屏](/20170102-quan-qiu-ke-fang-wen-de-freebsd/freebsd-powered-lcd-advertising-displays)
* [FlightAware 与 FreeBSD](/20170102-quan-qiu-ke-fang-wen-de-freebsd/flightaware-and-freebsd)
* [活动日历](/20170102-quan-qiu-ke-fang-wen-de-freebsd/events-calendar)
* [书评](/20170102-quan-qiu-ke-fang-wen-de-freebsd/book-review)

## 2016-1112 编程语言

* [FreeBSD 新面孔](/20161112-bian-cheng-yu-yan/new-faces-of-freebsd)
* [LLD 链接器](/20161112-bian-cheng-yu-yan/the-lld-linker)
* [SVN 动态](/20161112-bian-cheng-yu-yan/svn-update)
* [教 BSD](/20161112-bian-cheng-yu-yan/teach-bsd)
* [Swift 编程语言](/20161112-bian-cheng-yu-yan/swift-programming-language)
* [创办 BSD 用户组](/20161112-bian-cheng-yu-yan/starting-a-bsd-user-group)
* [基金会来信：外联与支持](/20161112-bian-cheng-yu-yan/letter-from-the-foundation)
* [Rust 比磨损更好](/20161112-bian-cheng-yu-yan/its-better-to-rust-than-wear-out)
* [活动日历](/20161112-bian-cheng-yu-yan/events-calendar)
* [EuroBSDcon：幕后](/20161112-bian-cheng-yu-yan/eurobsdcon-behind-the-scenes)
* [会议报道：MeetBSD 2016](/20161112-bian-cheng-yu-yan/conference-report)
* [书评：《人月神话》](/20161112-bian-cheng-yu-yan/book-review)

## 2016-0910 欢迎来到 FreeBSD 11

* [SVN 动态](/20160910-huan-ying-lai-dao-freebsd-11/svn-update)
* [书评：*FreeBSD Mastery: Advanced ZFS*](/20160910-huan-ying-lai-dao-freebsd-11/book-review)
* [提升 FreeBSD 内核中的高带宽 TLS](/20160910-huan-ying-lai-dao-freebsd-11/improving-high-bandwidth-tls-in-the-freebsd-kernel)
* [GELI 与 ZFS 改进](/20160910-huan-ying-lai-dao-freebsd-11/geli-and-zfs-improvements)
* [FreeBSD 工具链](/20160910-huan-ying-lai-dao-freebsd-11/freebsd-toolchain)
* [FreeBSD 11 中的 TCP 改进](/20160910-huan-ying-lai-dao-freebsd-11/tcp-improvements-in-freebsd-11)
* [在 Azure 上运行 FreeBSD](/20160910-huan-ying-lai-dao-freebsd-11/running-freebsd-azure)
* [FreeBSD 的新面孔](/20160910-huan-ying-lai-dao-freebsd-11/new-faces-of-freebsd)
* [基金会来信](/20160910-huan-ying-lai-dao-freebsd-11/letter-from-the-foundation)
* [欢迎来到 FreeBSD 11](/20160910-huan-ying-lai-dao-freebsd-11/welcome-to-freebsd-11)

## 2016-0708 FreeBSD 与 RTEMS

* [ZFS 调优与 FreeBSD](/20160708-freebsd-yu-rtems/zfs-tuning-and-freebsd)
* [SVN 动态](/20160708-freebsd-yu-rtems/svn-update)
* [ScaleEngine 与 FreeBSD](/20160708-freebsd-yu-rtems/scaleengine-and-freebsd)
* [FreeBSD 新面孔](/20160708-freebsd-yu-rtems/new-faces-of-freebsd)
* [基金会致信](/20160708-freebsd-yu-rtems/letter-from-the-foundation)
* [与 FreeBSD 项目互动](/20160708-freebsd-yu-rtems/interacting-with-the-freebsd-project)
* [把活干完：init 系统辩论再起](/20160708-freebsd-yu-rtems/getting-the-job-done-the-init-system-debate-relaunched)
* [FreeBSD 与 RTEMS：实时操作系统中的 UNIX](/20160708-freebsd-yu-rtems/freebsd-and-rtems)
* [活动日历](/20160708-freebsd-yu-rtems/events-calendar)
* [会议报告](/20160708-freebsd-yu-rtems/conference-report)
* [业余无线电与 FreeBSD](/20160708-freebsd-yu-rtems/amateur-radio-and-freebsd)

## 2016-0506 ARMv8

* [会议报告：AsiaBSDCon（令人惊叹的友善之人）](/2016-0506-armv8/conference-report)
* [活动日历](/2016-0506-armv8/events-calendar)
* [FreeBSD 本月动态](/2016-0506-armv8/this-month-in-freebsd)
* [SVN 动态](/2016-0506-armv8/svn-update)
* [围绕谷歌编程之夏构建社区](/2016-0506-armv8/building-community-around-the-google-summer-of-code)
* [bhyve ATA/ATAPI 仿真](/2016-0506-armv8/bhyve-ata-emulation)
* [FreeBSD 在 Cavium ThunderX 片上系统上的运行](/2016-0506-armv8/freebsd-on-cavium-thunderx-system-on-a-chip)
* [ARMv8 成为一级架构：协作开发项目](/2016-0506-armv8/armv8-enabled-as-tier-1-architecture)
* [基金会来信：FreeBSD 为最新 64 位架构做好了更充分的准备](/2016-0506-armv8/letter-from-the-foundation)

## 2016-0304 教学

* [FreeBSD 本月动态：访谈 Gleb Smirnoff](/20160304-jiao-xue/this-month-in-freebsd)
* [SVN 动态](/20160304-jiao-xue/svn-update)
* [Ports 报告](/20160304-jiao-xue/ports-report)
* [会议报道：台北/台湾 PortsCamp](/20160304-jiao-xue/meeting-report)
* [基金会来信：现在开课](/20160304-jiao-xue/letter-from-the-foundation)
* [活动日历](/20160304-jiao-xue/events-calendar)
* [会议报道：FOSDEM](/20160304-jiao-xue/conference-report)
* [CHERI：为安全可信计算基奠定基础](/20160304-jiao-xue/cheri-building-a-foundation-for-secure-trusted-computing-bases)
* [书评：《算法设计手册》](/20160304-jiao-xue/book-review-the-algorithm-design-manual)
* [BSD 快速文件系统简史](/20160304-jiao-xue/a-brief-history-of-the-bsd-fast-filesystem)

## 2016-0102 性能调优

* [用模糊测试打造工业级强度的系统](/20160102-xing-neng-tiao-you/using-fuzzy-testing-to-build-industrial-strength-systems)
* [FreeBSD 本月动态](/20160102-xing-neng-tiao-you/this-month-in-freebsd)
* [文件系统基准测试的注意事项](/20160102-xing-neng-tiao-you/the-dos-and-donts-of-file-system-benchmarking)
* [SVN 动态](/20160102-xing-neng-tiao-you/svn-update)
* [Ports 报告](/20160102-xing-neng-tiao-you/ports-report)
* [基金会来信：令人期待的这一年！](/20160102-xing-neng-tiao-you/letter-from-the-foundation)
* [活动日历](/20160102-xing-neng-tiao-you/events-calendar)
* [使用 Apache Zookeeper 构建分布式应用](/20160102-xing-neng-tiao-you/building-distributed-applications-with-apache-zookeeper)
* [书评：《FreeBSD 设备驱动程序开发》——写给无畏者的指南](/20160102-xing-neng-tiao-you/book-review-freebsd-device-drivers)

## 2015-1112 迁移 Jail 管理

* [在 FreeBSD 上使用 Vagrant 进行测试](/20151112-qian-yi-jail-guan-li/using-vagrant-to-test-on-freebsd)
* [FreeBSD 本月动态](/20151112-qian-yi-jail-guan-li/this-month-in-freebsd)
* [BSD 路由器项目](/20151112-qian-yi-jail-guan-li/the-bsd-router-project)
* [基金会聚焦：感恩](/20151112-qian-yi-jail-guan-li/spotlight-on-the-foundation-gratitude)
* [Ports 报道](/20151112-qian-yi-jail-guan-li/ports-report)
* [将 Jail 管理从 Warden 迁移到 iocage](/20151112-qian-yi-jail-guan-li/migrating-jail-management-from-warden-to-iocage)
* [基金会致信](/20151112-qian-yi-jail-guan-li/letter-from-the-foundation)
* [用 Doc Lounge 理念培养 FreeBSD 贡献者](/20151112-qian-yi-jail-guan-li/growing-freebsd-contributors-with-the-doc-lounge-concept)
* [会议报道](/20151112-qian-yi-jail-guan-li/conference-report)

## 2015-0910 CloudABI

* [活动日历](/2015-0910-cloudabi/events-calendar)
* [FreeBSD 本月动态](/2015-0910-cloudabi/this-month-in-freebsd)
* [SVN 动态](/2015-0910-cloudabi/svn-update)
* [Ports 报告](/2015-0910-cloudabi/ports-report)
* [书评：《云系统管理：大规模分布式系统设计与运营》](/2015-0910-cloudabi/book-review)
* [改进 FreeBSD 上 MemGuard 对 UMA 的支持](/2015-0910-cloudabi/improving-memguard-support-for-uma-on-freebsd)
* [FreeBSD 虚拟化方案](/2015-0910-cloudabi/freebsd-virtualization-options)
* [基金会来信](/2015-0910-cloudabi/foundation-letter)
* [会议报告](/2015-0910-cloudabi/conference-report)
* [ZFS 与启动环境](/2015-0910-cloudabi/zfs-and-boot-environments)
* [CloudABI](/2015-0910-cloudabi/cloudabi)

## 2015-0708 企业级 FreeBSD

* [FreeBSD 本月动态](/20150708-qi-ye-ji-freebsd/this-month-in-freebsd)
* [SVN 动态](/20150708-qi-ye-ji-freebsd/svn-update)
* [反思 FreeBSD.org 软件包](/20150708-qi-ye-ji-freebsd/reflections-on-freebsd-org-packages)
* [Ports 报告](/20150708-qi-ye-ji-freebsd/ports-report)
* [走进 FreeBSD 基金会](/20150708-qi-ye-ji-freebsd/inside-the-freebsd-foundation)
* [Groupon 在 FreeBSD 上的实践](/20150708-qi-ye-ji-freebsd/groupon-s-deal-on-freebsd)
* [FreeBSD：Isilon 经验](/20150708-qi-ye-ji-freebsd/freebsd-the-isilon-experience)
* [基金会来信](/20150708-qi-ye-ji-freebsd/foundation-letter)
* [活动日历](/20150708-qi-ye-ji-freebsd/events-calendar)
* [会议报告](/20150708-qi-ye-ji-freebsd/conference-report)
* [书评：《系统管理与网络管理技术实践》](/20150708-qi-ye-ji-freebsd/book-review)

## 2015-0506 测量两次，代码一次写好

* [FreeBSD 本月动态](/20150506-ce-liang-liang-ci-dai-ma-yi-ci-xie-hao/this-month-in-freebsd)
* [SVN 动态](/20150506-ce-liang-liang-ci-dai-ma-yi-ci-xie-hao/svn-update)
* [Ports 报告](/20150506-ce-liang-liang-ci-dai-ma-yi-ci-xie-hao/ports-report)
* [测量两次，代码一次写好](/20150506-ce-liang-liang-ci-dai-ma-yi-ci-xie-hao/measure-twice-code-once)
* [如何在 FreeBSD 上安装 Bacula 并完成首次备份](/20150506-ce-liang-liang-ci-dai-ma-yi-ci-xie-hao/how-to-install-bacula-on-freebsd-and-get-your-first-backup-running)
* [FreeBSD 上基于 Go 的内容过滤软件](/20150506-ce-liang-liang-ci-dai-ma-yi-ci-xie-hao/go-based-content-filtering-software-on-freebsd)
* [FreeBSD 项目资助申请提交](/20150506-ce-liang-liang-ci-dai-ma-yi-ci-xie-hao/freebsd-project-grant-proposal-submission)
* [FreeBSD 基金会动态](/20150506-ce-liang-liang-ci-dai-ma-yi-ci-xie-hao/freebsd-foundation-updates)
* [基金会来信](/20150506-ce-liang-liang-ci-dai-ma-yi-ci-xie-hao/foundation-letter)
* [活动日历](/20150506-ce-liang-liang-ci-dai-ma-yi-ci-xie-hao/events-calendar)
* [会议报道](/20150506-ce-liang-liang-ci-dai-ma-yi-ci-xie-hao/conference-report)
* [书评：*The Book of PF: A No-Nonsense Guide to the OpenBSD Firewall*](/20150506-ce-liang-liang-ci-dai-ma-yi-ci-xie-hao/book-review)

## 2015-0304 ZFS 最佳实践

* [ZFS 最佳实践](/20150304-zfs-zui-jia-shi-jian/zfs-best-practices)
* [FreeNAS 9.3 新功能](/20150304-zfs-zui-jia-shi-jian/what)
* [FreeBSD 本月动态](/20150304-zfs-zui-jia-shi-jian/this-month-in-freebsd)
* [FreeBSD 基金会成立 15 周年](/20150304-zfs-zui-jia-shi-jian/the-freebsd-foundation-at-15)
* [Ports 报告](/20150304-zfs-zui-jia-shi-jian/ports-report)
* [如何构建 Port](/20150304-zfs-zui-jia-shi-jian/how-to-build-a-port)
* [基金会来信](/20150304-zfs-zui-jia-shi-jian/foundation-letter)
* [活动日历](/20150304-zfs-zui-jia-shi-jian/events-calendar)
* [书评：《DNSSEC Mastery》](/20150304-zfs-zui-jia-shi-jian/book-review)

## 2015-0102 MIPS 与 arm64

* [从采用到贡献之旅](/20150102-mips-yu-arm64/a-journey-from-adoption-to-contribution)
* [Ports 报告](/20150102-mips-yu-arm64/ports-report)
* [参与其中](/20150102-mips-yu-arm64/getting-involved)
* [活动日历](/20150102-mips-yu-arm64/events-calendar)
* [FreeBSD 本月动态](/20150102-mips-yu-arm64/this-month-in-freebsd)
* [SVN 动态](/20150102-mips-yu-arm64/svn-update)
* [基金会来信](/20150102-mips-yu-arm64/foundation-letter)
* [会议报告](/20150102-mips-yu-arm64/conference-report)
* [书评：《FreeBSD 操作系统设计与实现第 2 版》](/20150102-mips-yu-arm64/book-review)
* [arm64](/20150102-mips-yu-arm64/arm64)
* [点亮 MIPS](/20150102-mips-yu-arm64/bringing-up-mips)

## 2014-1112 视频驱动程序

* [FreeBSD 本月动态](/20141112-shi-pin-qu-dong-cheng-xu/this-month-in-freebsd)
* [活动日历](/20141112-shi-pin-qu-dong-cheng-xu/events-calendar)
* [SVN 动态](/20141112-shi-pin-qu-dong-cheng-xu/svn-update)
* [Ports 报告](/20141112-shi-pin-qu-dong-cheng-xu/ports-report)
* [会议报告](/20141112-shi-pin-qu-dong-cheng-xu/conference-report)
* [书评：《HTTPS 权威指南：在服务器和 Web 应用上部署 SSL-TLS 和 PKI》](/20141112-shi-pin-qu-dong-cheng-xu/book-review)
* [pfSense：三小时之旅](/20141112-shi-pin-qu-dong-cheng-xu/pfsense-a-3-hour-tour)
* [告别 printf：用 DTrace 深入内核](/20141112-shi-pin-qu-dong-cheng-xu/no-more-printfs-digging-into-the-kernel-with-dtrace)
* [基金会来信](/20141112-shi-pin-qu-dong-cheng-xu/foundation-letter)
* [视频驱动程序](/20141112-shi-pin-qu-dong-cheng-xu/video-drivers)

## 2014-0910 科学、系统与 FreeBSD

* [活动日历](/20140910-ke-xue-xi-tong-yu-freebsd/events-calendar)
* [FreeBSD 本月动态](/20140910-ke-xue-xi-tong-yu-freebsd/this-month-in-freebsd)
* [SVN 动态](/20140910-ke-xue-xi-tong-yu-freebsd/svn-update)
* [会议报告](/20140910-ke-xue-xi-tong-yu-freebsd/conference-report)
* [为什么要为 FreeBSD 系统设置 PF？](/20140910-ke-xue-xi-tong-yu-freebsd/why-set-up-pf-on-your-freebsd-systems)
* [Ports 报告](/20140910-ke-xue-xi-tong-yu-freebsd/ports-report)
* [基金会来信](/20140910-ke-xue-xi-tong-yu-freebsd/foundation-letter)
* [ULE](/20140910-ke-xue-xi-tong-yu-freebsd/ule)
* [LLDB](/20140910-ke-xue-xi-tong-yu-freebsd/lldb)
* [科学、系统与 FreeBSD](/20140910-ke-xue-xi-tong-yu-freebsd/science-systems-and-freebsd)

## 2014-0708 虚拟化

* [Xen](/20140708-xu-ni-hua/xen)
* [USE 方法](/20140708-xu-ni-hua/the-use-method)
* [使用 bhyve 进行 FreeBSD 开发](/20140708-xu-ni-hua/using-bhyve-for-freebsd-development)
* [Amazon EC2 云与 FreeBSD](/20140708-xu-ni-hua/freebsd-in-the-amazon-ec2-cloud)
* [FreeBSD 本月动态](/20140708-xu-ni-hua/this-month-in-freebsd)
* [Ports 报告](/20140708-xu-ni-hua/ports-report)
* [基金会致信](/20140708-xu-ni-hua/letter-from-the-foundation)
* [SVN 动态](/20140708-xu-ni-hua/svn-update)
* [活动日历](/20140708-xu-ni-hua/events-calendar)
* [会议报告](/20140708-xu-ni-hua/conference-report)

## 2014-0506 网络

* [FreeBSD 本月动态](/20140506-wang-luo/this-month-in-freebsd)
* [TCP 连接速率扩展](/20140506-wang-luo/tcp-connection-rate-scaling)
* [SVN 动态](/20140506-wang-luo/svn-update)
* [Ports 报告](/20140506-wang-luo/ports-report)
* [基金会致信](/20140506-wang-luo/letter-from-the-foundation)
* [Kqueue 狂想](/20140506-wang-luo/kqueue-madness)
* [IPFW 概述](/20140506-wang-luo/ipfw-an-overview)
* [用 DTrace 探索网络活动](/20140506-wang-luo/exploring-network-activity-with-dtrace)
* [活动日历](/20140506-wang-luo/events-calendar)
* [用 DNSSEC 分发数据](/20140506-wang-luo/data-distribution-with-dnssec)

## 2014-0304 pkg(8)

* [用 hwpmc(4) 理解应用与系统性能](/2014-0304-pkg-8/understanding-application-and-system-performance-with-hwpmc4)
* [日志化软更新](/2014-0304-pkg-8/journaled-soft-updates)
* [从 1.x 到 11.x：PBI 格式的持续演进](/2014-0304-pkg-8/from-1-x-to-11-x-the-ongoing-evolution-of-the-pbi-format)
* [Poudriere](/2014-0304-pkg-8/poudriere)
* [SVN 动态](/2014-0304-pkg-8/svn-update)
* [Ports 报告](/2014-0304-pkg-8/ports-report)
* [FreeBSD 本月动态](/2014-0304-pkg-8/this-month-in-freebsd)
* [基金会致信](/2014-0304-pkg-8/letter-from-the-foundation)
* [活动日历](/2014-0304-pkg-8/events-calendar)

## 2014-0102 FreeBSD 10

* [FreeBSD 本月动态](/2014-0102-freebsd-10/this-month-in-freebsd)
* [存储的未来：Z 文件系统](/2014-0102-freebsd-10/the-future-of-storage)
* [SVN 动态](/2014-0102-freebsd-10/svn-update)
* [Ports 报告](/2014-0102-freebsd-10/ports-report)
* [基金会致辞](/2014-0102-freebsd-10/letter-from-the-foundation)
* [实现系统控制节点（sysctl）](/2014-0102-freebsd-10/implementing-system-control-nodes-sysctl)
* [BeagleBone Black 上的 FreeBSD 入门](/2014-0102-freebsd-10/getting-started-with-freebsd-on-beaglebone-black)
* [FreeBSD 与商业工作负载：NYI 的托管服务](/2014-0102-freebsd-10/freebsd-and-commercial-workloads-managed-services-at-nyi)
* [活动日历](/2014-0102-freebsd-10/events-calendar)
* [FreeBSD 10 中的 Clang](/2014-0102-freebsd-10/clang-in-10)


# 基金会来信

* 原文：[Foundation Letter](https://freebsdfoundation.org/our-work/journal/browser-based-edition/production-deployments/foundation-letter-7/)
* 作者：**Anne Dickison**

欢迎阅读最新一期《FreeBSD 期刊》。

本期聚焦 FreeBSD 始终珍视的一点：可以按自己的需求灵活构建系统。无论是整合家庭实验室、在云端运行 FreeBSD、直接操作 USB 设备，还是搭建注重隐私的安全工作流，这些文章都展示了各种可能。

你还会读到 Michael W Lucas 的最新《读者来信》，一如既往地评点构建和运行系统时的取舍与权衡。

上一期我们宣布停止以 PDF 文件形式发布文章，随后很快收到读者反馈。经过慎重考虑，我们正在试验新的流程，并计划以改良的方式恢复制作《FreeBSD 期刊》文章的 PDF。本期的文章提供网页版和 PDF 版，祝你阅读愉快。

《FreeBSD 期刊》对所有读者免费，这要归功于投资 FreeBSD 基金会的个人和组织，正是他们让这样的工作成为可能。

希望你喜欢本期，并从中找到几个值得探索的新想法。

**Anne Dickison**

FreeBSD 基金会副主任


# 活动日历

* 原文：[Q3 Calendar of Events](https://freebsdfoundation.org/our-work/journal/browser-based-edition/production-deployments/q3-calendar-of-events/)
* 作者：**Florine Kamdem**

![FreeBSD logo](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4d2bb642c491f540beb7b2e16bdf2f6b17b8b19d%2Fevents-calendar-01.png?alt=media\&token=08d3f9c8-3f3f-42a3-b3aa-11788c3c1adc)

## 黑客马拉松

2026 年 10 月 27-30 日

挪威奥斯陆

<https://wiki.freebsd.org/Hackathon/202610>

奥斯陆黑客马拉松汇聚 FreeBSD 贡献者，协作推进开源项目，覆盖 FreeBSD 生态的各个领域。与会者可以处理 PR 和代码审查、编写测试、维护 Ports、改进文档，或与志同道合者组队。

Entersekt 将在奥斯陆市中心的办公室主办本次黑客马拉松，时间为 2026 年 10 月 27-30 日，地点在挪威奥斯陆。

![Code and Compliance](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-bacf189c867e8c5096b84399c68aa886368060c8%2Fevents-calendar-02.png?alt=media)

## Code & Compliance 2026

2026 年 10 月 27 日

比利时布鲁塞尔

<https://codeandcompliance.orcwg.org/>

Code & Compliance 2026 是面向开源开发者、项目管理者与行业相关方的社区活动，聚焦软件合规、治理与开源安全。活动将于 2026 年 10 月 27 日在比利时布鲁塞尔举行。这次聚会为协作、分享最佳实践、讨论合规挑战提供了平台，也让开源社区有机会增进信任、透明度与长期可持续性。

![FreeBSD logo](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4d2bb642c491f540beb7b2e16bdf2f6b17b8b19d%2Fevents-calendar-01.png?alt=media\&token=08d3f9c8-3f3f-42a3-b3aa-11788c3c1adc)

## 2026 年 11 月 FreeBSD 厂商峰会

2026 年 11 月 5-6 日

美国加利福尼亚州圣何塞（NetApp 圣何塞园区）

<https://freebsdfoundation.org/news-and-events/event-calendar/november-2026-freebsd-vendor-summit/>

这场为期两天的峰会由 NetApp 主办、FreeBSD 基金会赞助，让商业 FreeBSD 用户得以与开发者和贡献者面对面交流，争取想要的功能、解决问题、满足需求。会议将在 FreeBSD Meetings YouTube 频道直播。报名即将开放。

![CHERI Tech](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-9dbede3639d44bdd0550f0d11166f759b20978e6%2Fevents-calendar-03.webp?alt=media)

## CHERITech’26

2026 年 11 月 12-13 日

德国慕尼黑

<https://cheri-alliance.org/events/cheritech26/>

本次会议与 SEMICON Europa 同期举办，汇聚研究人员、技术专家、网络安全专家和产品开发者，共同探讨 CHERI 技术与安全计算的进展，重点关注塑造“设计即安全”未来的技术创新。


# 用 Sylve 整合家庭实验室

* 原文：[Consolidating a Home Lab with Sylve](https://freebsdfoundation.org/our-work/journal/browser-based-edition/production-deployments/consolidating-a-home-lab-with-sylve/)
* 作者：**Sven Rüdiger**

## 面向 FreeBSD 上 bhyve、Jail 与 ZFS 的浏览器管理平面

多年来，我一直想找个“省事”的办法，在 FreeBSD 服务器上原生运行虚拟机。该有的东西其实都在：bhyve 是出色的 hypervisor，ZFS 自有 ZFS 的本事，FreeBSD 网络栈更不必向谁推销。可真要日复一日把它们凑在一起用，还是得靠我手工维护的 shell 脚本和配置文件。没有任何界面能架在我已有的系统之上，又不试图接管整个系统。

我在 [bhyveCon 2025](https://bhyvecon.org/) 上看了[那场关于 Sylve 的演讲](https://www.youtube.com/watch?v=wo4oD5UON30\&pp=ygUVIlN5bHZlIiBiaHl2ZUNvbiAyMDI10gcJCaMLAYcqIYzv)，看完当天就动手试了。当晚，Windows 就作为 bhyve 客户机跑在我的一台开发服务器上了，我还写了一篇[简短的博客文章](https://hackacad.net/post/windows-10-on-freebsd-with-sylve/)记录此事，主要是因为我不太敢相信整个过程竟没出多少岔子。

本文讲的是我如何用 Sylve 收拾已经失控的家庭实验室，它为什么契合我运行 FreeBSD 的既有方式，还有我把最后一台独立机器迁入之后结果如何。文末有一节循序渐进的安装说明。

### 我想解决的问题

我的实验室按大多数家庭实验室的方式长大（只是快了一点），也就是说，没怎么规划。多年来我一直把 Home Assistant 直接跑在 FreeBSD 上。起初一切正常，后来就不行了。大约两年前，Python 依赖在每次升级时都变成一场移植苦战，为了让它保持正常运转，我搭进去的夜晚越来越多，实在不划算。这方面 Home Assistant 并不特殊；不少项目其实只期望 Linux 主机，我手里也悄悄攒了几摊处境相同的负载。

虚拟化是显而易见的出路。我以前摆弄过 bhyve，老实说，问题多半出在我自己身上，而非 hypervisor。手工管理它们很痛苦，vm-bhyve 也只帮上一点忙。我没有硬着头皮上，而是绕了个弯：把 Linux 负载放到 Raspberry Pi CM4 板子上，一块板子干一件事。这办法行得通，但如今我有了小小一架单一用途的机器，名叫 ComputeBlades。后来 CM5 支持迟迟不见踪影，这条路的前景也就不明朗了。

我真正想要的，是把那些 Linux 负载拉回自己熟悉的 FreeBSD 主机上，顺带把 Intel NUC 上那一两个甩不掉的专有 Windows 工具也搬过来，而且不必抛弃多年来学到的一切。Sylve 正好补上了这道缺口。

### Sylve 为何契合以 FreeBSD 为先的环境

Sylve 最讨我喜欢的一点，是它不试图成为平台本身。它驱动的每样东西本就属于 FreeBSD 基本系统。bhyve 担任 hypervisor，ZFS 负责存储，网络栈打理底层管道。Sylve 只管调度这些部件，通过 Go 后端和 REST API 编排虚拟机、Jail、数据集、虚拟交换机与 DHCP。底层的操作系统分毫未动。

这就是不去碰 Proxmox 之类方案的全部理由。Proxmox 是好产品，可用上它就意味着放弃 FreeBSD，连同我每天依赖的各种习惯和肌肉记忆一起放弃。Sylve 让我保住自己的主机。我在意的功能留在基本系统里，那里的东西大家都很熟悉。Sylve 只是坐在上层的管理器，正如 Bastille 之于 Jail，我也只希望它做到这一步。我不需要为了界面就装整套 hypervisor 发行版，ZFS 快照和复制仍然是原汁原味的 ZFS，而不是被埋在厂商抽象层下的东西。

### 初印象与早期开发

我很早就开始用 Sylve，但从未后悔。界面有 bug，也有软件常见的毛糙之处，对年轻、尚在实验阶段的项目来说，这完全在意料之中。让我意外的是，这些问题都没波及虚拟机。哪怕我把 Sylve 装坏了，底下的负载照样在跑。因为 Sylve 管理的是基本系统的部件，而不是替换它们，界面崩掉从不会变成虚拟机停摆。管理层次与实际负载之间的这道间隙，让我决定把它真正用起来，而不是当玩具。此后它进步很大。我刚开始用时要自己从 GitHub 源码构建；如今它已经打包，安装步骤短了许多。

### 整合实践

Sylve 一就位，清理就快了起来。开几台 Linux 客户机不费吹灰之力，我还添了一台 Windows 客户机，用来运行一款没有 FreeBSD 版本的专有安全软件。手工折腾 bhyve 时始终没能养成的工作流程，突然就行得通了，因为现在无非是填个表单：CPU、内存、数据存储、交换机，外加用来看安装程序的控制台。几周前，我把最后一台独立机器也迁进了 Sylve，那架单一用途的机器就此收拢成几台性能不错的 FreeBSD 主机。

有一点值得点出，因为它很好地说明 Sylve 在一套有历史包袱的系统上表现如何。它自带 Jail 管理器，但我用 Bastille 跑二十多个 Jail 已经很长时间。我不想迁移它们，结果发现根本不必迁。两者相安无事地共存。我把现有的 Jail 留在 Bastille 里，让 Sylve 管虚拟机。旧 Jail 待在原本就能工作的地方，就不必为迁移额外花时间。那样做毫无收益，而这样还能让整套配置尽可能简单。随之而来的还有我一贯的做法：依赖越少越好，让每个工具干它真正擅长的那部分。

维护也遵循同样的思路。底下有 pkgbase，又有启动环境可作退路，让平台保持最新很容易。我可以在全新的启动环境里试跑一次升级，一旦出问题就回滚；而且因为 Sylve 并不占有基本系统，更新主机不会演变成跟管理层的拉锯战。

### 结果

简短版本：我从散落一地的单一用途机器、Pi 板子和 NUC，缩减到几台 FreeBSD 服务器。当初逼我绕道 CM4 的那些 Linux 负载，回到了我最熟悉的硬件和操作系统上，Windows 虚拟机也一样。我的 Bastille Jail 原封未动。

还有一点也该坦白说明：Sylve 同样能以集群方式运行。写作本文时，在主机之间迁移虚拟机的功能刚刚发布，用起来限制不多，比如要求两台主机上的 ZFS 池同名。对我的家庭实验室来说，一键迁移已经是很大的助力。

如果你是 FreeBSD 用户，一直在等待一款能与你的系统配合的 bhyve 界面，那 Sylve 大概就是你一直在找的工具。

### 安装 Sylve

#### 系统需求

Sylve 只需要 **FreeBSD 15.0 或更高版本**，再加上可用的 **ZFS 池**。只跑 Jail 的主机，一颗 CPU 加 512 MB 内存就能撑起来；但若要运行 bhyve 虚拟机，至少要有 **2 个 vCPU、4 GB 内存**，ZFS 池也得在 20 GB 以上。还有一点值得知道：许多 VPS 实例不支持嵌套虚拟化，在那里 bhyve 虚拟机恐怕无从谈起。话说回来，何必非要在虚拟机里套一层 hypervisor？Jail 在哪儿都能跑。

#### 安装软件包

Sylve 如今已经打包，因此在 FreeBSD 15 或更新版本上，安装本身就是一条命令：

```sh
pkg install sylve
```

开箱即用，Sylve 使用自带的证书以 HTTPS 启动，因此安装完就能直接访问界面，无须生成任何东西。它也能提供纯 HTTP 服务，但我把它排除在配置之外，只保留 HTTPS。如果你不想用随附的证书，自己又没有 CA，可以通过 Sylve 界面创建自签名证书。

证书是自签名的，所以浏览器第一次会报警告；接受例外，或者把证书导入信任库，就能让它安静下来。对于可从外部访问的主机，直接使用内置的 Let's Encrypt 或任何公共 CA 即可，但要记住，你也许并不想把 hypervisor 的管理界面放到公网上，尤其考虑到这个项目还这么年轻。

#### 创建配置

Sylve 读取 JSON 配置文件。用编辑器改 **/usr/local/etc/sylve** 下的默认 `config.json`，填入基本设置，如果证书和密钥由你自己提供，再加上它们的路径：

```sh
{
"environment": "production",
"proxyToVite": false,
"auth": {
"enablePAM": true
},
"admin": {
"email": "admin@sylve.local",
"password": "your-strong-password"
},
"logLevel": 0,
"port": 8181,
"raft": {
"reset": false
}
}
```

有几点说明。这里故意没有 `httpPort`：加上它，Sylve 就会提供纯 HTTP 服务，但我只保留 HTTPS，所以略去不写。`enablePAM` 为 `true`，这样你可以用系统账户登录——如果只允许管理员用户（以及你稍后在界面里添加的账号）进入，就把它设为 `false`。搭建期间 `logLevel` 用 `0`，会记录一切；环境稳定后改成 `3`，只记录错误。集群至少需要三个节点。把 `raft reset` 设为 `true`，就能轻松重建集群，且不丢失虚拟机。

#### 加载器与内核可调参数

如果打算用 Jail 和 ZFS，就把下面这些加到 **/boot/loader.conf**，好让 ZFS 载入并开启资源统计：

```sh
zfs_load="YES"
kern.racct.enable=1
```

我还会给 ZFS ARC 设个上限，免得它把客户机饿死。在 4 GB 的机器上，512 MB 是合理的起点，但应通过 Sylve 界面按你的负载调整。太小，ZFS 会变慢；太大，它会吃掉内存。

#### 启用并启动 Sylve

软件包已经放好了 rc.d 脚本，剩下的只是启用并启动服务：

```sh
sysrc sylve_enable=YES
service sylve start
```

换作我，这里会直接重启，而不是只启动服务，一来验证加载器可调参数是否生效，二来确认 Sylve 能自行启动。无论哪种方式，它随后都应该在 `https://<your-server-ip>:8181` 上响应。用 `config.json` 里 `admin` 的凭据登录。

#### 创建虚拟交换机

在创建第一台虚拟机之前，得先有虚拟交换机供它接入。在界面里依次进入 Network → Switches → Standard → + New，对多数家庭环境来说，在这里启用 DHCP。你之后创建的虚拟机和 Jail 都把自己的网络挂在这个交换机上。

#### 你的第一台虚拟机：Windows

以下是在 Sylve 上运行 Windows 的简要版本，取自我在自己博客上写的文章。

先从存储入手：在 Datacenter → your host → Storage → ZFS → Datasets → Volumes → + New 下为虚拟机创建 ZFS 卷，大小随你定。然后搞一个 Windows 10 ISO（LTSC 评估版就很好用），通过 Utilities → Downloader → + New 粘贴下载链接把它拉进来。

现在创建虚拟机。在 Storage 标签页选择 ZFS 和你刚创建的卷，磁盘总线为 Windows 10 选 NVMe（Windows 11 目前用 AHCI 更顺）。在 Network 标签页选择你的交换机和 VirtIO 仿真。在 Advanced 标签页，如果你想用外部 VNC 客户端连接控制台，就复制 VNC 密码。创建之后打开 Summary，点 Start。

要进入安装程序，你要么及时抓住启动提示符，要么进入 UEFI shell 自己启动它：

```sh
fs0:
cd EFI\BOOT       # 也可能是 EFI\Microsoft\Boot，取决于 ISO
BOOTX64.EFI       # 也可能是 bootmgfw.efi
```

Windows 起来之后，安装 VirtIO 驱动。同样通过 Downloader 下载，在 Windows 里挂载该 ISO，然后运行安装程序。如果 Windows 看不到磁盘或网卡，几乎总是因为缺少 VirtIO 驱动，在安装时或安装后补上驱动，问题自然解决。

#### 结语

Sylve 帮我把家庭实验室里运行的 FreeBSD 节点从 16 个缩减到 4 个。现在一切都跑在几台 FreeBSD 主机上：Linux 负载、Windows 虚拟机，还有我原有的 Bastille Jail，彼此相邻，互不干扰。

唯一的缺点是功耗——Intel 硬件的空闲功耗比一摞 ARM 板子高。但随着节点数量增长，这一差距会缩小。主机上的带外管理是个不错的附加好处，便于维护节点固件，也能应对故障（即便有启动环境，故障也可能发生）。

接下来的事，主要是看着 Sylve 的集群功能成熟起来，尤其是实时迁移。本文完成时，0.3.0 刚刚发布，带来基本的迁移功能，文档也更完善。<https://sylve.io/>

***

**Sven** 自 2000 年起就是 FreeBSD 的老用户，对开源技术怀有根深蒂固的热情。过去十年间，他一直积极为 FreeBSD Ports 集合贡献代码，最近五年担任 port 维护者。他拥有数据科学硕士学位，喜欢把大数据和数据科学工具引入 FreeBSD 生态。


# 使用 FreeBSD 计算资源开展云运维

* 原文：[Cloud Operations Using FreeBSD Compute](https://freebsdfoundation.org/our-work/journal/browser-based-edition/production-deployments/cloud-operations-using-freebsd-compute/)
* 作者：**Jason Tubnor**

## 在云 IaaS 中开展操作系统多样化时，FreeBSD 是出色的选择

现代云已经存在约二十年。亚马逊提出面向零售的计算服务（EC2）这一想法后，AWS 随之成立，出租这些计算资源，并提供对象存储、数据库等其他服务。其他厂商也纷纷加入：微软有 Azure，谷歌有 GCP，甲骨文有 OCI，为客户提供丰富多样的 Tier 1 云基础设施。

起初能用的只有 Linux，主要受限于当时的技术条件：KVM 出现之前的半虚拟化不允许其他操作系统内核运行。

不过，情况开始改变。2008 年，也就是 AWS EC2 公开可用两年之后，FreeBSD 开发者 Colin Percival 开始把 FreeBSD 操作系统移植到 AWS。两年的努力得到回报，2010 年 12 月，Colin 在 AWS EC2 基础设施上成功启动 FreeBSD。又打磨了 5 年，2015 年 4 月，FreeBSD 成为 EC2 上的标准产品。

工作并未止步于此。FreeBSD 继续完善，进入其他 Tier 1 提供商的应用市场，也进入一些 Tier 2 云提供商。遗憾的是，Tier 2 提供商对许多操作系统产品的支持有所收缩，不只是 FreeBSD。不过现在，只要你准备自行提供支持，有些提供商可以让你轻松自带 ISO。这些提供商通常通过 VirtIO 框架提供设备，而 VirtIO 本就内置在 FreeBSD 基本系统中，因此在这些环境里启动原版通用系统很容易。

在云基础设施即服务（IaaS）环境中开展操作系统多样化时，FreeBSD 是出色的选择。与任何技术一样，某些环节应当多样化，万一某个平台出现 bug，服务仍能不中断。FreeBSD 为你提供这份保障：设备驱动原生实现，新近加入的开放容器计划（OCI）让你可以轻松用现有工具管理 FreeBSD IaaS 计算实例上的工作负载。

云上 FreeBSD 镜像还有其他特性：支持 IPv6，有些还支持 ZFS，有些则只支持 UFS。如果你只需要一台基本的 FreeBSD 设备，后者或许就够用；但如果应用与管理需要“Root on ZFS”，也可以选用 ZFS。

FreeBSD 进驻各家云应用市场还有一层好处：使用免费，在其上开发也免费。没有持续的订阅费用，你可以开一个实例，也可以开一千个。除计算资源外，一切成本为零。有些应用市场确实会让你走与订阅制操作系统相同的流程，但点击订阅按钮时不会产生费用。

不过，好处还不止这些。你打算在 ARM64 架构上运行吗？FreeBSD 同样支持。从 2024 年起，AWS 等提供商开始为自家的 Graviton ARM CPU 提供 ARM64，用户可以选择成本更低、功耗更小的平台。越来越多组织转向环境可持续模式，运营的各个环节都需要减少碳足迹，这也是 FreeBSD 和 ARM 计算能帮上忙的地方。

启动 FreeBSD 计算实例，可以通过 Web 控制台，也可以通过 AWS CLI API 工具。

FreeBSD 桌面用户可以轻松获得 CLI 工具，用来管控各家云厂商的租户。只需从 FreeBSD 软件包仓库安装：

```sh
# pkg install py312-awscli
```

安装完成后，用你的 AWS 管理控制台凭据认证：

```sh
$ aws login
```

此时你会在一定时间内保持认证状态，一旦长时间无操作便会自动登出。

这里假定你已经定义好 SSH 公钥、安全组、子网。实例创建并启动后，为把暴露面降到最低，这些都是必需的。完成 EC2 创建还需要其他信息：实例类型、需要多少台，以及你希望它位于哪个区域。

要创建实例，执行：

```sh
$ aws ec2 run-instances --image-id ami-[freebsd AMI ID] --count 1 --instance-type t4g.micro --region ap-southeast-2 --key-name key-[my SSH public key ID] --security-group-ids sg-[security group ID] --subnet-id subnet-[subnet ID]
```

视安全组和子网的设置而定，如果实例设为公开可用，你就可以直接从工作站 SSH 登录：

```sh
$ ssh -I .ssh/id_awsec2 ec2@mynewhost.example.com
```

系统启动时由 cloud-init 完成最基本的主机设置，让它运转起来。获取 root 权限只需执行 su。注意：本文不讨论实例的安全加固。

FreeBSD 计算可以对接云厂商提供的其他 PaaS 服务。需要 ZFS 存储时，可以给计算实例挂载多个块存储卷。用 FreeBSD NFS 客户端可以挂载弹性文件系统，访问按用量计费的文件存储。FUSE 文件系统可以把 S3 对象存储挂载到实例上。或者直接让 FreeBSD 实例上的应用通过网络连接托管数据库。FreeBSD 只提供你创建系统所需的组件，由此得到的系统攻击面小、活动部件少，每台主机的启用成本也随之降低——大型项目需要上千台主机时，这些成本会不断累积。

本文只触及了 FreeBSD 作为计算操作系统在功能与灵活性上的一角，但希望在你需要把“下一个大事件”部署到云上时，能给你一些启发。

***

**Jason Tubnor** 拥有超过 28 年 IT 行业经验，涉猎领域众多，目前是 Latrobe Community Health Service（澳大利亚维多利亚州）的 ICT 高级安全主管。他在 20 世纪 90 年代中期接触 Linux 与开源，2000 年又结识 OpenBSD，此后用这些工具解决了不同行业组织中的各种问题。Jason 还是 [BSDNow Podcast](https://www.bsdnow.tv/) 的联合主持人。


# 用户空间中的 USB

* 原文：[USB in User Space](https://freebsdfoundation.org/our-work/journal/browser-based-edition/production-deployments/usb-in-user-space/)
* 作者：**Rick Parrish**

## USB 工具简介：如何编写自己的自定义 USB 工具或用户空间驱动

本文先概述 FreeBSD 自带的 USB 工具。随后面向开发者，介绍 **libusb20(3)** 访问库。这个库提供了一种对开发者友好的方式，用来与通用 USB 内核模块交互，无需再把字节硬塞进 **ioctl(2)** 调用。

### FreeBSD 上的 USB 工具

下面概述几款无需编写任何代码就能使用的工具，并给出对应手册章节的参考。想了解 FreeBSD 上 USB 的总体情况，可以看看[架构手册第 13 章](https://docs.freebsd.org/en/books/arch-handbook/usb/index.html)。

**USB 存储与 OTG**

[存储（第 21 章）](https://docs.freebsd.org/en/books/handbook/disks/)完全是另一回事，本文不讨论。OTG 或 [gadget 模式（第 29 章）](https://docs.freebsd.org/en/books/handbook/usb-device-mode/)也一样。

**USB HID**（键盘与鼠标）

工具 [**usbhidaction(1)**](https://man.freebsd.org/cgi/man.cgi?query=usbhidaction\(1\)) 可以监视 HID 接口，捕捉遥控器发来的按键。典型用例是把键盘配置成：按下特殊的“Fn”键时执行与 OEM 丝印相符的动作，例如静音、调高或调低音量、调节屏幕亮度、开关 WiFi 等。作为 **usbhidaction(1)** 的配套工具，另见 [**usbhidctl(1)**](https://man.freebsd.org/cgi/man.cgi?query=usbhidctl)。

**USB 网络共享**（[第 35 章](https://docs.freebsd.org/en/books/handbook/advanced-networking/#network-usb-tethering)）

每当我需要笔记本上网而 WiFi 又不配合时，都可以退回到 USB 网络共享，使用智能手机提供的移动数据。把智能手机连到笔记本，在手机上打开 USB 网络共享。查看 **dmesg(8)**，应当能看到一个新设备“ue0”。唯一的小技巧是在新接口上运行 dhclient，其实一点也不麻烦——以 root 身份运行 `dhclient ue0` 即可。下面这条 **devd(8)** 规则可以让它自动完成。

```sh
notify 20 {
    match "system" "ETHERNET";
     match "type"  "IFATTACH";
     match "subsystem" "ue[0-9]+";
     action "/sbin/dhclient $subsystem";
};
```

**ls(1)**

也许你还不清楚，USB 设备节点出现在 **/dev/ugenX.Y** 和 **/dev/usb/X.Y.Z** 下，其中 X、Y、Z 都是十进制数字。我认为 **ls(1)** 最有用之处是查看节点的组和用户访问权限。**/dev/ugenX.Y** 设备节点指整个 USB 设备。例如：

```sh
$ ls -la /dev/ugen*
lrwxr-xr-x  1 root wheel 9 Jul 27 19:59 /dev/ugen0.1 -> usb/0.1.0
lrwxr-xr-x  1 root wheel 9 Jul 27 19:59 /dev/ugen0.2 -> usb/0.2.0
lrwxr-xr-x  1 root wheel 9 Jul 27 19:59 /dev/ugen0.3 -> usb/0.3.0
lrwxr-xr-x  1 root wheel 9 Jul 27 19:59 /dev/ugen1.1 -> usb/1.1.0
lrwxr-xr-x  1 root wheel 9 Jul 27 19:59 /dev/ugen1.2 -> usb/1.2.0
lrwxr-xr-x  1 root wheel 9 Jul 27 19:59 /dev/ugen1.3 -> usb/1.3.0
lrwxr-xr-x  1 root wheel 9 Jul 27 19:59 /dev/ugen1.4 -> usb/1.4.0
lrwxr-xr-x  1 root wheel 9 Jul 27 19:59 /dev/ugen2.1 -> usb/2.1.0
lrwxr-xr-x  1 root wheel 9 Jul 27 19:59 /dev/ugen2.2 -> usb/2.2.0
```

注意，**/dev/ugenX.Y** 设备节点都是指向 **/dev/usb/X.Y.0** 设备节点的符号链接（即第三位数字始终为零的设备节点）。下面是 **/dev/usb/X.Y.Z** 设备节点的示例列表：

```sh
$ ls -la /dev/usb
dr-xr-xr-x   2 root wheel     512 Jul 27 19:59 .
dr-xr-xr-x  13 root wheel     512 Jul 27 14:59 ..
crw-------   1 root operator 0x2f Jul 27 19:59 0.1.0
crw-------   1 root operator 0x45 Jul 27 19:59 0.1.1
crw-------   1 root operator 0x80 Jul 27 19:59 0.2.0
crw-------   1 root operator 0x84 Jul 27 19:59 0.2.4
crw-------   1 root operator 0x85 Jul 27 19:59 0.2.5
crw-------   1 root operator 0x86 Jul 27 19:59 0.2.6
crw-------   1 root operator 0x87 Jul 27 19:59 0.2.7
crw-------   1 root operator 0x88 Jul 27 19:59 0.2.8
crw-------   1 root operator 0x89 Jul 27 19:59 0.2.9
crw-------   1 root operator 0x8e Jul 27 19:59 0.3.0
crw-------   1 root operator 0x90 Jul 27 19:59 0.3.1
crw-------   1 root operator 0x91 Jul 27 19:59 0.3.2
```

……为节省篇幅，此处省略……

```sh
crw-------   1 root operator 0x33 Jul 27 19:59 2.1.0
crw-------   1 root operator 0x43 Jul 27 19:59 2.1.1
crw-------   1 root operator 0x82 Jul 27 19:59 2.2.0
crw-------   1 root operator 0x8a Jul 27 19:59 2.2.1
```

许多 **/dev/usb** 设备节点并不以零结尾。以非零数字结尾的节点，是同一设备提供的附加端点/接口组合。在上面的 **/dev/usb** 示例中，2.1.0 和 2.1.1 属于同一个设备；2.2.0 与 2.2.1 同理，也是同一个设备。

**dmesg 与 usbconfig**

运行 **usbconfig(8)** 可以查看已连接的 USB 设备。以 root 身份运行时，输出内容要丰富得多。以普通用户身份运行时，它会告诉你哪些设备可以访问。如果想要的设备没有出现，你可能需要创建一条 devd 规则，在设备接入时授予自己访问权限。Linux 上与之对应的工具大概是 lsusb。

**dmesg(8)** 会记录 USB 设备的接入和拔出事件。插上设备后不久，查看 **dmesg(8)** 的尾部，就能看到分配了哪个设备节点。下面这个例子中，我先拔下、再重新插入两个不同的 USB 适配器。

```sh
ugen0.2: <Realtek 802.11ac WLAN Adapter> at usbus0 (disconnected)
ugen0.2: <Realtek 802.11ac WLAN Adapter> at usbus0
ugen0.5: <Logitech USB Receiver> at usbus0 (disconnected)
ugen0.5: <Logitech USB Receiver> at usbus0
```

大多数 USB 设备都有相应的 FreeBSD 内核模块支持，这些模块把主机层的操作转换成 USB 设备能理解的形式，例如读取鼠标按键，或者为 WiFi 网卡收发 IP 数据包。

有些设备太过冷门，没有对应的内核模块；又或者厂商改为提供用户态库，从而免去编写内核模块的必要。

如今，从用户进程直接与 USB 设备通信，最可移植——也最流行——的方式是使用 **libusb(3)** 访问库，它在众多操作系统上都能用。FreeBSD 自带自己的实现。

FreeBSD 的 **libusb(3)** API 实现，底层是一套 FreeBSD 原生访问库 API，相比 libusb API 具备一些优势。本文关注的就是 FreeBSD 独有的这套原生访问库 API。

接下来，我们会看原生的 USB API——**libusb20(3)**——并完全用 FreeBSD 原生的组件，自己实现一份 **usbconfig(8)** / lsusb。除了 FreeBSD 标准自带的内容，我们不需要任何新的内核模块。这套 USB API 提供了所需的一切：定位想要的 USB 设备，像打开文件或 tty 设备那样打开它，再发送命令控制设备。

与任何 USB 设备通信的第一步，是从当前已连接的 USB 设备列表中找出目标设备。在 shell 提示符下，用 root 权限登录后运行 `usbconfig list`，就能看到有哪些设备。输出示例：

```sh
ugen0.1: <XHCI root HUB Intel> at usbus0, cfg=0 md=HOST spd=SUPER (5.0Gbps) pwr=SAVE (0mA)
ugen1.1: <EHCI root HUB Intel> at usbus1, cfg=0 md=HOST spd=HIGH (480Mbps) pwr=SAVE (0mA)
ugen2.1: <EHCI root HUB Intel> at usbus2, cfg=0 md=HOST spd=HIGH (480Mbps) pwr=SAVE (0mA)
ugen0.2: <AC600 wireless Realtek RTL8811AU [Archer T2U Nano] TP-Link> at usbus0, cfg=0 md=HOST spd=HIGH (480Mbps) pwr=ON (500mA)
ugen2.2: <Integrated Rate Matching Hub Intel Corp.> at usbus2, cfg=0 md=HOST spd=HIGH (480Mbps) pwr=SAVE (0mA)
ugen1.2: <Integrated Rate Matching Hub Intel Corp.> at usbus1, cfg=0 md=HOST spd=HIGH (480Mbps) pwr=SAVE (0mA)
ugen0.3: <Nano Receiver Logitech, Inc.> at usbus0, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (98mA)
```

USB 设备主要靠一对 16 位标识来区分——厂商 ID（VID）和产品 ID（PID）。要查看每个设备的 VID 和 PID，运行 `usbconfig dump_device_desc`。VID 和 PID 通常显示为四位十六进制值。如果有两个或更多设备的 VID 和 PID 相同，往往还能靠序列号区分。在 USB 领域，序列号是字母数字字符串（不是整数），并不限于十进制数字。如果两个或更多设备的 VID、PID、序列号都相同，或许还能通过 USB 总线区分。

为特定设备编写的程序可以枚举所有设备，寻找 VID 和 PID 符合要求的候选设备。程序可以把匹配结果列给用户，也可以直接选第一个。

**接口与配置**

USB 设备会提供一个或多个接口，这样单个 USB 设备通常就能承担多种功能。处理批量传输和等时传输时，你需要指明使用哪个接口。**usbconfig(8)** 工具可以列出指定设备通告的所有接口。

```sh
usbconfig -d ugen0.1 dump_curr_config_desc
```

本文只使用默认配置，不过有些设备支持多种配置。**usbconfig(8)** 可以列出全部配置。

```sh
usbconfig -d ugen0.1 dump_all_config_desc
```

注意，我没有详细介绍接口和配置。它们的含义和数量因设备而异，可能性太多，这里无法深入。你只要知道它们存在就够了。

**devd(8)**

本文的重点是编写用户态——而非内核——代码来访问设备，并且无需提升到 root 权限。

在设备节点上精准地执行一次 **chmod(1)** 或 **chown(1)**，就不再需要 root。

一条简短的 **devd(8)** 规则就能让这一步自动完成。

有个取巧的办法：先把目标设备拔下来。把它插到空闲的 USB 插槽，查看 **dmesg(8)**，看分配到了哪个设备节点。知道了 VID 和 PID，一条简单的 devd 规则就能在设备每次重新接入主机时替你调整权限。用 **ls(1)** 验证 ugen 设备节点的权限。

为了方便，我们可以写一条只用来记录日志的 **devd(8)** 规则。

```sh
notify 100 {
       match "system"          "USB";
       match "subsystem"       "DEVICE";
       match "type"            "ATTACH";
       match "vendor"          "0x1234";
       match "product"         "0x5678";
       action "logger $cdev device ";
};
```

*match “vendor”* 子句匹配四位 VID，0x 前缀表明该值是十六进制。*match “product”* 子句匹配四位 PID，同样带 0x 十六进制前缀。上面的片段会在设备接入时记录设备节点。看到这样的日志消息，就说明规则里的匹配子句生效了。

USB 设备一般支持多个接口。如果能够成功记录设备接入的日志，我们就可以再加一条规则，作用于接口以修改组和用户权限。把命令写进 **devd(8)** 规则之前，应当先手动试一试。**chown(1)** 可以同时指定组和用户，像这样：

```sh
chown ordinary:wheel /dev/ugen1.2
chown ordinary:wheel /dev/usb/1.2.*
```

这里假定目标用户是“ordinary”，而他恰好属于“wheel”组。你可以按需修改。真正的 **devd(8)** 规则 action 子句要稍复杂一些，因为我想同时处理 **/dev/ugenX.Y** 节点和关联的 **/dev/usbX.Y.Z** 节点。完整片段如下：

```sh
notify 100 {
       match "system"          "USB";
       match "subsystem"       "INTERFACE";
       match "type"            "ATTACH";
       match "vendor"          "0x1234";
       match "product"         "0x5678";
       action "cdev=$cdev; usb=$(expr ${cdev} : 'ugen\([0-9]\.[0-9]\)'); chown -h ordinary:wheel /dev/${cdev}; chown -h ordinary:wheel /dev/usb/${usb}.*";
};
```

记得用 **ls(1)** 验证所有者是否设置正确。

**usbipd(8)**

**usbipd(8)** 守护进程和配套的 **usbip(8)** 命令可以连接远程主机上的 USB 设备。这个工具与 Linux 上一套知名的 USB-over-IP 实现兼容。你的 FreeBSD 主机既可以作客户端，也可以作服务端，或者两者兼作。

我在本机上通过 localhost 测试过：挑了一个在我的主机上显示为 ugen0.3 的设备。下面是具体步骤和结果。

```sh
git clone https://github.com/hackermaskee/freebsd-usbip
cd freebsd-usbip
make kmod
make
[sudo|doas|mdo]make install

[sudo|doas|mdo]vi /etc/rc.conf
usbipd_devices="ugen0.3"
usbipd_enable="YES"

[sudo|doas|mdo]kldload vhci
[sudo|doas|mdo]service usbipd start
[sudo|doas|mdo]usbip attach -r loocalhost -b ugen0.3
```

此时，usbconfig 会把该设备列出两次：

```sh
$ usbconfig
ugen3.2:  at usbus3, cfg=0 md=HOST spd=HIGH (480Mbps) pwr=ON (500mA)
ugen0.3:  at usbus0, cfg=0 md=HOST spd=HIGH (480Mbps) pwr=ON (500mA)
```

ugen3.2 设备节点是 ugen0.3 的复制设备。访问其中任何一个设备（这里用 rtl\_test）效果都一样。值得一提的是，新设备节点也遵循了我的 devd 规则。

### libusb20(3)

**libusb20(3)** 是 FreeBSD 自己的访问库，让进程直接与 USB 设备通信，而不必经由某个领域专用的内核模块间接通信。通用 USB 内核模块对外提供 **ioctl(2)** 接口，把常用的 USB 操作开放给用户空间。**libusb20(3)** 访问库提供了一种对开发者友好的方式，用来与通用 USB 模块交互，无需再把字节硬塞进 **ioctl(2)** 调用。

管理 USB 设备大致涉及以下几方面：

1. **devd(8)**
2. 设备枚举
3. 传输
   * 控制传输
   * 批量传输
   * 中断传输
   * 等时传输

**devd(8)** 前面已经讲过。

**枚举**

要找到应用程序需要的特定设备，就必须能够枚举 USB 设备。下面是一些实现枚举的代码片段。我假定你会读写 C 或 C++。你的源文件需要包含以下头文件。

```c
#include <libusb20.h>
#include <libusb20_desc.h>
#include <dev/usb/usb_ioctl.h>
```

枚举涉及以下函数：

* `libusb20_be_alloc_default`
* `libusb20_be_device_foreach`
* `libsub20_dev_get_device_desc`
* `libusb20_be_free`

**libusb20(3)** 引入了后端（backend，BE）的概念，用来定位设备。说明一下，这与 ZFS 的 BE 毫无关系，完全是两码事。理论上你可以编写自己的后端，用来创建模拟 USB 设备以便测试，或者实现类似 USB over TCP 的东西。到目前为止，我还没见过有人用这种方式实现 USB over IP。这里我们只需要默认后端——不过本文末尾会给出一个纯软件的模拟 USB 设备示例。

调用 **libusb20\_device\_foreach(3)** 可以逐个节点枚举设备列表。**libusb20\_dev\_get\_device\_desc(3)** 能提供设备节点的更多细节，对枚举可能有用。

这段代码假定有一个外部函数 `candidate`，当 VID 和 PID 匹配时返回 true，否则返回 false。

```c
bool (*candidate)(uint16_t vid, uint16_t pid);
```

它还假定有一个函数 `acceptable`，当 usb\_device\_info 数据对应用程序来说可接受时返回 true。

```c
bool (*acceptable)(struct usb_device_info *info);
```

至于什么才算可接受，由你决定。

```c
libusb20_device *handle = nullptr;

libusb20_device *walker = nullptr;
auto be = libusb20_be_alloc_default();
if (be == nullptr)
{
   printf(stderr, "libusb20_be_alloc()\n");
   return false;
}

// true 表示找到了合适的设备，可以停止查找。
bool okay = false;
walker = libusb20_be_device_foreach(be, walker);
while (walker != NULL)
{
   auto ddp = libusb20_dev_get_device_desc(walker);
   // 匹配 vid 和 pid 吗？
   if ( candidate(ddp->idVendor, ddp->idProduct) )
   {
       // 是：打开它以获取更多信息。
       if (libusb20_dev_open(walker, reserve) == LIBUSB20_SUCCESS)
       {
           struct usb_device_info info{};
           libusb20_dev_get_info(walker, &info);
           // 我们可以检查 usb_device_info 的内容，
           // 进一步把这个设备与其他设备区分开。
           okay  = acceptable(walker, info);
           if (okay)
           {
               handle = walker;
               libusb20_be_dequeue_device(be, walker);
               break;
           }
           // 不是我们想要的设备，继续查找。
           libusb20_dev_close(walker);
       }
   }
   walker = libusb20_be_device_foreach(be, walker);
}
libusb20_be_free(be);
return okay;
```

上面的代码可以改成把一组 usb\_device\_info 对象作为选择列表呈现给用户。就目前的写法而言，代码返回指向它认为可接受的第一个候选设备的设备指针。

注意 **libusb20\_dev\_open(3)** 的 `reserve` 参数。讲到传输时我们会解释它的用途。

找到合适的设备后，调用 **libusb20\_be\_dequeue\_device(3)** 把设备从设备列表中移除。这是预留设备的一种方式，不过并没有我希望的那么有用。如果你重新枚举同一个设备列表，它不会出现在里面。但是，在同一进程或另一个进程中调用 **libusb20\_be\_device\_foreach(3)** 会创建一份新列表，设备依然在里面。这并不会给你对设备的独占访问权。

现在我们有了设备指针，下一步是用 **libusb20\_dev\_open(3)** 打开设备。此后一直使用同一个设备指针，没有单独的设备句柄。这不会授予独占访问权。一种解决办法是在设备上放一个劝告锁，但只有当所有相关方都遵守这个劝告锁时才有效。我个人觉得允许两个进程共享一个 USB 设备（比如温度/湿度传感器）挺酷，不过这属于少见场景。有人在讨论扩展 **libusb20(3)** 的 ABI 以接受独占标志，但至今没有下文。

设备打开之后，我们就可以做些有意思的事情了。

其中之一是获取设备信息，另一个是执行四种传输中的一种。那么这四种传输分别是什么呢？

**控制传输**最常见。它们是一些简短的、设备专用的命令。每条命令由 8 字节头部和可选负载组成。有的向设备发送数据，有的从设备取回数据。为简单起见，控制传输通常同步完成。

**中断传输**属于低带宽、偶发的传输。键盘和鼠标是利用中断传输的两个主要例子。这类设备通常由内核模块处理，本文不讨论中断传输。

**等时传输**和**批量传输**非常相似，区别在于等时传输对交付提供一些额外保证（若无法满足这些保证就会失败）。我主要使用批量传输，但两者的机制极为相似，需要做等时传输的人可以参照批量的例子。

在介绍传输之前，先做个不太严格的类比：把“ugen”（通用 USB 设备）比作 **/dev/tty** 串口。在串口上收发字符数据之前，必须先设置波特率、每字符位数、起始位、停止位、校验位。这一步相当于给 USB 设备下达命令的控制传输。串口字符数据的收发，则相当于 USB 的批量传输或等时传输。

**控制传输**

控制传输常用于初始化设备，或让设备进入某种工作模式。

控制传输有两个方向——IN 或 OUT。它包含一个固定部分（`LIBUSB20_CONTROL_SETUP_DECODED`）和一个可选的数据部分。`libusb20_dev_request_sync` 函数带有一个参数，用来捕获实际传输的字节数。这是除 setup 头部所占字节之外的“额外”字节数。

```c
struct LIBUSB20_CONTROL_SETUP_DECODED setup;
```

这个宏把上面的结构体初始化为合理的起始值。

```c
LIBUSB20_INIT(LIBUSB20_CONTROL_SETUP, &setup);
```

成员 `bmRequestType` 通常是两个值之一——用于输入或用于输出。

```c
setup.bmRequestType = LIBUSB20_REQUEST_TYPE_VENDOR | LIBUSB20_ENDPOINT_OUT;

setup.bmRequestType = LIBUSB20_REQUEST_TYPE_VENDOR | LIBUSB20_ENDPOINT_IN;

setup.bRequest = request;
setup.wIndex = index
setup.wValue = value;
setup.wLength = length;
```

上述四个参数的具体取值由设备决定。`wLength` 是额外字节数。对于 `LIBUSB20_ENDPOINT_OUT` 请求类型，它是要写入 USB 设备的额外数据字节数。对 Web 开发者来说，这大致相当于 HTTP POST。当然，你是向 USB 设备“POST”，而不是向 HTTP Web 服务器。

```c
int status = libusb20_dev_request_sync(dev, &setup, data, &actual, TIMEOUT, 0);
```

`dev` 是设备指针。`setup` 是 `LIBUSB20_CONTROL_SETUP_DECODED` 的实例。`data` 是指向可选额外数据的指针。`actual` 捕获实际传输的额外字节数。对于 `LIBUSB20_ENDPOINT_IN`，`actual` 可能小于或等于 `setup.wLength` 中指定的值。对 Web 开发者来说，这大致相当于 HTTP GET。

`libusb20_dev_request_sync` 会阻塞，直到控制传输完成。下面的批量传输示例不是同步的，因此你可以发出重叠的请求。我避免让控制传输重叠，因为下一次控制传输往往取决于上一次控制传输是否成功（以及它返回的数据）。

**中断传输**

大多数支持中断传输的设备都有对应的内核模块，因此很少需要以编程方式从用户层访问。

**批量传输**

批量传输可以发送或接收数据块，很像通过套接字或文件句柄传输数据。

有些设备对批量/等时传输的大小有限制。我支持过的一些芯片组要求传输大小必须是 512 字节的整数倍。

对于流式数据（如音频或视频），你需要在延迟和开销之间取得平衡。如果把 1 MB 数据切成一个个 512 字节的小块来传输，就要发出 2048 个请求。你也可以用单个请求发送同样的数据。数据块越大，CPU 开销越低，但如果你想中途停止流式传输或更换内容，延迟代价也越大。把数据块大小调整到与目标帧率（比如 30 fps）相匹配，就能比用——比方说——一分钟的数据块更早暂停传输。

假设你要以 192 ksps 传输立体声音频采样。每个采样 4 字节（每声道 16 位），也就是每秒 192k 采样 × 每采样 4 字节 = 768 KB。按 30 fps 计算，一帧就是 25600 字节。注意这恰好是 512 的整数倍。如果你的帧大小不是 512 的整数倍，就需要向上或向下取整。这意味着实际帧率不会正好是 30 fps，不过在大多数场合都没问题。

要连续流式传输，就需要管理多个未完成的传输。你希望有一个传输正在进行，另外至少还有两个传输在等待。一个传输完成后，缓冲区就可以复用——并重新提交。按上面的例子，你至少需要三个各 25600 字节的传输。要双向流式传输，每个方向都需要这么多传输。

我觉得把流式传输拆成四步最有用：

1. 预留，
2. 启动，
3. 停止，
4. 释放。

**预留**负责分配一组传输缓冲区。为了便于管理，我会创建一个传输上下文，把与传输相关的零碎信息放在一起。

```c
// USB 传输上下文。
struct Transfer
{
   void(unsigned char *data, unsigned size) *callback;
   unsigned char *buffer;
   struct libusb20_transfer *transfer;
   uint32_t size;
   uint32_t timeout;
};

struct libusb20_transfer *transfer = libusb20_tr_get_pointer(dev, i);
Transfer *context = new Transfer(back, transfer, bucket, 3 * timeout);
result = libusb20_tr_open(context->transfer, context->size, 1, 0x81);
// 每个缓冲区约 30 毫秒，因此整整一秒的时间足够完成传输。
libusb20_tr_setup_bulk(context->transfer, context->buffer, context->size, context->timeout);
libusb20_tr_set_callback(context->transfer, bulk);
libusb20_tr_set_priv_sc1(context->transfer, context);
```

注意，上面的上下文只跟踪一个传输。要做三个传输，就需要三个 Transfer 对象。还记得 **libusb20\_dev\_open(3)** 的 `reserve` 参数吗？它告诉宿主内核预期有多少个传输。对于只做批量和控制传输的设备，`reserve` 等于批量传输数加上控制传输数。本文使用 2 个控制传输和 3 个批量传输，所以 `reserve` 参数为 5。

**libusb20\_tr\_open(3)** 接受帧数和端点。我把每个传输都作为一个帧发出。IN 和 OUT 端点各有 16 个。给出的示例——0x81——是一个批量 IN 端点。与配置和接口一样，端点的编号因设备而异。注意，按 USB 术语，发往特定端点的一组传输也称为管道（pipe）。

> **提示**：如果控制传输正常，而 **libusb20\_tr\_open(3)** 失败，请检查你的配置索引。

**libusb20\_tr\_set\_priv\_sc1(3)** 让我们能把一个裸指针与传输关联起来。我把这个指针设成管理该传输的上下文对象。它的用处很快就会显现。

**启动**为每个传输缓冲区调用一次 **libusb20\_tr\_start(3)**。

**停止**为每个传输缓冲区调用一次 **libusb20\_tr\_stop(3)**。

**释放**释放每个传输关联的内存和资源。

在预留阶段，我喜欢用一次 malloc 调用分配一大块内存，而不是分多次分配小块。这样释放阶段就简化为一次 free 调用。

**libusb20\_tr\_set\_callback(3)** 设置完成例程的函数指针。对于出站流，这个例程用新数据填充传输缓冲区；对于入站流，它从传输缓冲区复制数据。它还会重新提交传输，让流持续下去。

完成例程看起来很像预留和启动两步的结合。

```c
uint8_t status = libusb20_tr_get_status(transfer);
uint32_t actual = libusb20_tr_get_actual_length(transfer);
uint32_t size = libusb20_tr_get_max_total_length(transfer);
Transfer *context = (Transfer *)libusb20_tr_get_priv_sc1(transfer);
// 向缓冲区复制数据或从缓冲区复制数据的步骤放在这里。
libusb20_tr_setup_bulk(transfer, context->buffer, context->size, context->timeout);
libusb20_tr_set_callback(transfer, bulk);
libusb20_tr_set_priv_sc1(transfer, context);
libusb20_tr_submit(transfer);
```

检查 `status` 就能知道有没有出错。理想情况下，`actual` 应当与 `size` 相等。向缓冲区复制数据或从缓冲区复制数据之后，再调用剩下的函数。注意 **libusb20\_tr\_get\_priv\_sc1(3)** 如何让我们从传输中取回上下文指针。

你可以先做预留这一步，然后任意多次执行启动/停止步骤。除非你想退出，或者想改变传输缓冲区的规模（更多缓冲区、更少缓冲区、更大缓冲区、更小缓冲区），否则不需要执行释放这一步。如果新的规模超出 **libusb20\_dev\_open(3)** 中的 `reserve` 参数，就必须关闭设备，再用新的 `reserve` 值重新打开。

务必在释放之前先停止。如果尚未完成的 I/O 修改了你已经释放的数据缓冲区，堆会非常生气——这就成了“释放后使用”漏洞。

**等时传输**

我一直没有需要处理等时传输的场合，所以只好让你自己去摸索了。据我了解，它的设置过程与批量传输非常相似。参见 **libusb20\_tr\_setup\_isoc(3)**。

**事件泵**

最后一块是事件泵。它与 X.org 应用程序中的事件泵类似。区别在于泵送的不是 X 窗口事件，而是 USB 事件。这个执行线程最终会调用各个完成例程。没有事件泵，你的代码就不知道传输何时完成。事件泵的代码很短。大多数程序把它放在后台线程里。下面的例子每秒轮询 30 次。

```c
while (flag)
{
   int milliseconds = 1000/30;
   if (libusb20_dev_process(device) == 0)
   {
       libusb20_dev_wait_process(device, milliseconds);
   }
}
```

**测量延迟**

测量延迟有助于判断进程跟上输入数据的能力。**libusb20\_tr\_pending(3)** 函数会询问 USB 协议栈是否认为某个传输已经完成。统计已完成（但尚未处理）的缓冲区数量，就能大致看出进程落后了多少。如果所有缓冲区都已完成，进程很可能丢了数据，因为没有空闲传输来捕获数据。短期解决办法是增加缓冲区数量。更好的办法是优化处理代码，缩短每个传输的处理周转时间。

**清除暂停**

设备可以把某个端点标记为暂停（halt）。暂停对控制传输一般不成问题，但它会让批量传输和等时传输真正停下来。根本原因可能是线缆松动，或者其他古怪的毛病。清除暂停后，主机就能恢复与该端点的收发。**libusb20\_tr\_clear\_stall\_sync(3)** 函数需要一个传输指针。与其复用某个批量传输指针，我会在 **libusb20\_dev\_open(3)** 中多预留一个传输指针。这样我总有一个专用于清除暂停的备用传输指针。

举例来说，你的应用程序需要 N 个批量传输指针，于是以最大传输数 N + 1 调用 `libusb20_dev_open`，下面是清除端点 0x81 暂停的步骤。

```c
const uint8_t ep = 0x81;
auto transfer = libusb_tr_get_pointer(handle, N);
auto transfer = libusb_tr_open(handle, 0, 1, ep);
if (transfer != nullptr)
{
   libusb20_tr_clear_stall_sync(transfer);
   libusb_tr_close(handle);
}
```

注意 `libusb20_tr_open` 的参数——尤其是大小为 0、数量为 1。清除端点暂停所需的就这些。

**修改后端**

下面这个依赖注入的小技巧，利用 USB 后端机制注入一个完全存在于软件中的伪设备。所有交互都通过 **libusb20(3)**（以及 libusb）库进行。首先，创建你自己的 **libusb20\_be\_alloc\_default(3)** 实现。

你的实现会用 **dlsym(3)** 找到并调用真正的 **libusb20\_be\_alloc\_default(3)**。

它会返回一份 **libusb20\_be\_alloc\_default(3)** 所返回后端结构的修改副本。这个修改后的结构里含有函数指针。你可以把自己的代码接进去，拦截对默认后端的调用。由此就能做一些事情，比如拦截对 **libusb20\_dev\_open(3)** 的调用，返回一个能响应控制传输和批量或等时传输的伪设备。

这样就能对 **libusb20(3)** 和 **libusb(3)** 的客户端代码做相当细粒度的单元测试，无需真实硬件。编写自己的后端需要以下两个头文件，顺序如下：

```c
#include <sys/queue.h>
#include <libusb20_int.h>
```

**DTrace**

围绕访问库编写代码虽然让调试轻松许多，但有时你需要看清原始的 ioctl。例如，对 **libusb20\_tr\_open(3)** 的调用失败，而库只返回一个非常笼统的 `LIBUSB20_ERROR_INVALID_PARAM`。下面这段 dtrace 会告诉你实际的返回值。知道这个值，你就可以查看 ugen 驱动的内核源码，寻找失败原因的线索。

```sh
dtrace -n 'fbt:kernel:usb_ioctl:return /execname == "my_usb_code"/ { printf("%d\n", (int)arg1); }'
```

**libusb20(3) 相对于 libusb(3) 和 WinUSB 的优势**

我喜欢 **libusb20(3)** ABI 的简洁直接。它的复杂度刚好够用，同时让代码保持可读。

最大的优势是软件许可。许可和版权很重要。Linux 和 Windows 上真正的 **libusb(3)** 库采用 LGPL，而在很多场合 LGPL 是完全可以接受的。

**libusb20(3)**（以及兼容 **libusb(3)** 的 shim）是 FreeBSD 的一部分，采用 BSD-2-Clause 许可。这是非常宽松的许可。FreeBSD 版 pthreads 也是如此。在 FreeBSD 上，更容易避开 (L)GPL 和链接例外方面的合规问题。许可和版权就说到这里。

一项主要的技术优势是：即使设备已被另一个进程占用，我仍能找到该设备，并从设备读取字符串描述符。在 Linux 和 Windows 的 **libusb(3)** 实现中，设备会从设备列表中消失。这历来让最终用户困惑：他们常用的软件提示那个闪亮的 USB 设备不存在——只因为别的进程先占用了它。

像 **usbconfig(8)** 这样的工具没法用 **libusb(3)** 实现——至少在非 FreeBSD 平台上不行，因为这些平台上 **libusb(3)** 的枚举实现方式不允许。看不见的东西，自然描述不出来。

我还把微软的 WinUSB 拿来一起比较。在 WinUSB 中发起传输的方式非常相似，所以在这个层面上差别不大。不过，Windows 的 USB 设备枚举糟糕透顶：FreeBSD 或 Linux 只需一段代码，它却要几百行。曾几何时，Windows 各版本之间（7 与 8 之间）还有破坏兼容的 WinUSB（通过 SetupAPI）变更。就我个人而言，还没见过 **libusb20(3)** 出现破坏兼容的变更。

**libusb20(3) 相对于 libusb(3) 和 WinUSB 的劣势**

前面已经暗示过，它也有劣势。

一项主要劣势是缺少独占设备访问。在 Windows 和 Linux 上，如果两个进程枚举到同一个设备并尝试打开它，先到者胜出。在 FreeBSD 上，两者都能打开，这可能造成一些混乱。对那些假定 Windows 或 Linux 行为的 Ports 来说，这是个值得注意的问题。

劝告锁可以解决独占问题，但只有当所有相关方都采用时才有效。

最大的劣势是缺乏可移植性。除了 FreeBSD 及 FreeBSD 衍生操作系统，你在别处看不到 **libusb20(3)**。就我的用例而言，这完全没问题。

如果你有一个想在 FreeBSD 上支持的 USB gadget，可以考虑用 **libusb20(3)** 写些代码来测试这个设备。从用户态调试比调试内核模块容易。等代码能正常工作了，事情就完成了。如果你坚持要做内核模块，那就以这份用户态代码为基础，编写用同样方式驱动该设备的新内核模块。

下面是一个用 libusb20 实现的最简 usb-list 程序。

```c
#include <assert.h>
#include <unistd.h>
#include <stdio.h>
#include <libusb20_desc.h>
#include <libusb20.h>
#include <sys/queue.h>
#include <libusb20_int.h>
#include <dev/usb/usb_ioctl.h>

int main(int, const char *[])
{
   auto be = libusb20_be_alloc_default();
   struct libusb20_device *device = nullptr;
   while ( (device = libusb20_be_device_foreach(be, device)) != nullptr)
   {
       int error = libusb20_dev_open(device, 0);
       if (error == LIBUSB20_SUCCESS)
       {
           struct usb_device_info info{};
           libusb20_dev_get_info(device, &info);
           fprintf(stdout, "%s %04hX (%s) %04hX (%s) %s [%d]\n",
                   libusb20_dev_get_desc(device),
                   info.udi_vendorNo,
                   info.udi_vendor,
                   info.udi_productNo,
                   info.udi_product,
                   info.udi_serial,
                   info.udi_config_index);
           libusb20_dev_close(device);
       }
   }
   libusb20_be_free(be);
   return 0;
}
```

下面是用 libusb 实现的大致等价版本，应当可以移植到其他宿主操作系统。

```c
#include <assert.h>
#include <unistd.h>
#include <stdio.h>
#include <libusb.h>

int main(int, const char *[])
{
   libusb_context *context = nullptr;
   libusb_device **list = nullptr;
   unsigned index = 0;
   libusb_device *device = nullptr;
   int result = libusb_init(&context);
   if (result == LIBUSB_SUCCESS)
   {
       if (libusb_get_device_list(context, &list) >= 0)
       {
           while ( (device = list[index++]) != nullptr )
           {
               libusb_device_handle *handle = nullptr;
               struct libusb_device_descriptor desc{};
               result = libusb_get_device_descriptor(device, &desc);
               if (result == LIBUSB_SUCCESS)
               {
                   result = libusb_open(device, &handle);
                   if (result == LIBUSB_SUCCESS)
                   {
                       int config = 0;
                       uint8_t make[128]{};
                       uint8_t product[128]{};
                       uint8_t serial[128]{};
                       result = libusb_get_string_descriptor_ascii(handle,
                           desc.iManufacturer, make, sizeof make);
                       if (result <= 0)
                           sprintf((char *)make, "vendor %04hX", desc.idVendor);
                       result = libusb_get_string_descriptor_ascii(handle,
                           desc.iProduct, product, sizeof product);
                       if (result <= 0)
                           sprintf((char *)product, "product %04hX", desc.idProduct);
                       libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber,
                           serial, sizeof serial);
                       libusb_get_configuration(handle, &config);
                       fprintf(stdout, "ugen%d.%d %04hX (%s) %04hX (%s) %s [%d]\n",
                       libusb_get_bus_number(device),
                               libusb_get_device_address(device),
                               desc.idVendor,
                               make,
                               desc.idProduct,
                               product,
                               serial,
                               config);
                       libusb_close(handle);
                   }
               }
           }
           libusb_free_device_list(list, 0);
       }
       libusb_exit(context);
   }
   return 0;
}
```

**延伸阅读**

DTrace —— 第 28 章（FreeBSD 手册） <https://docs.freebsd.org/en/books/handbook/dtrace/>

USB 设备 —— 第 13 章（架构手册） <https://docs.freebsd.org/en/books/arch-handbook/usb/index.html>

USB 设备 —— 第 29 章（FreeBSD 手册） <https://docs.freebsd.org/en/books/handbook/usb-device-mode/index.html>

***

**Rick Parrish** 是一位资深 C++ 开发者，他对 USB 设备的兴趣让他发现了 FreeBSD 的 libusb20 访问库。他发现该库文档存在空白，于是向 FreeBSD 文档团队提交了一些小的改进。


# 在 FreeBSD 上进行可审计的凭证泄露检查，无需发送完整密码

* 原文：[Auditable Credential-Exposure Checks on FreeBSD Without Sending Full Passwords](https://freebsdfoundation.org/our-work/journal/browser-based-edition/production-deployments/auditable-credential-exposure-checks-on-freebsd-without-sending-full-passwords/)
* 作者：**Emre Çapan**

## 保护隐私的实用范围查询工作流：有界披露、严格响应处理、有用的审计证据

密码泄露检查，就是查某人打算使用的密码是否已经出现在泄露密码集合里。Have I Been Pwned（HIBP）的 Pwned Passwords 这类服务，维护着可供检索的哈希，对应的是从数据泄露及其他入侵数据中找回的密码值。用户创建或修改密码时，应用可以查询该集合，拒绝已被攻击者掌握的候选密码。

例如，某人可能选了对其账户而言全新的密码，但该密码已在别处使用并泄露。命中就是换用其他密码的理由。它不能证明此人的账户已被入侵，不能指认具体受害者，也不能揭示密码的来源。未命中只说明该值在受检集合中找不到，并不保证密码强度或安全。这项检查只是对周边身份验证控制的补充。

范围查询本身带来一个问题：客户端如何询问某个机密，又不把这个机密交给回答问题的服务？通过 HTTPS 发送完整密码，接收服务仍然会看到它。发送完整的未加盐哈希，同样给服务方或监听者留下校验器，可用来验证密码猜测。

范围查询可以限制这种披露。客户端在本地计算密码的 SHA-1 哈希，只发送前五个十六进制字符。服务返回该范围内的哈希后缀，客户端在 FreeBSD 主机上完成最后比较。完整密码和完整哈希都不会发给范围服务。这种做法通常称为 k-匿名查询，但它并非完全匿名：服务仍能看到前缀、源 IP 地址、请求时间。SHA-1 在这里只是该语料库要求的查询格式，不是密码存储方案。

本文围绕公开的 Pwned Passwords 范围接口，在 FreeBSD 上构建交互式检查器。文章沿着候选密码的路径展开：从终端输入，经过 HTTPS 请求、严格的响应校验，到最小的审计记录。程序区分命中、未命中、检查不可用或无效三种情况，超时绝不能当作干净的结果。文中还会说明响应填充、缓存限制、测试，还有何时适合使用 Jail、Capsicum 或由 rc.d 管理的工作进程。整个工作流不依赖任何商业产品。

### 真正的问题在查询，不在哈希函数

泄露检查器的回答范围很窄：候选密码是否出现在先前从数据泄露或其他公开入侵数据中观察到的值的语料库里？它不能证明使用该密码的账户已被入侵，不能指出密码来自何处，也不能在答案为否时让密码变得安全。有用的结果是决策信号：命中说明候选密码不应设置或复用，未命中只排除一项已知风险指标，常规的密码质量与账户安全控制仍要保留。

最朴素的实现把密码发给远程服务。HTTPS 保护传输中的连接，但服务仍然收到机密。表面上有改进的实现改为发送完整的 SHA-1 摘要。这样做避开了线路上的明文，但摘要仍是稳定的校验器，可用来验证猜测。常见密码可以廉价地测试，服务还会拿到确切的查询值。

前缀范围协议改变了披露内容。客户端在本地算出 160 位的 SHA-1 摘要，发送前五个十六进制字符，接收与该前缀关联的全部已知后缀。五个十六进制字符披露摘要的 20 位，从 1,048,576 个可能范围中选定一个。客户端在本地比较其余 35 个字符。远程服务只知道请求的范围，不知道完整摘要或密码。这种模式通常称为 k-匿名范围查询。不要把这个名称误当成完全匿名：返回分组的大小会有变化，服务仍然能观察到来源和前缀。

SHA-1 不适合存储密码、签署数据，也不适合用来保证抗碰撞能力。FreeBSD 自带的 **md5(1)** 手册页警告，SHA-1 容易遭受实用的碰撞攻击 **\[3]**。在这个工作流里，SHA-1 不是密码存储结构，而是公开语料库的兼容键，因为该语料库已经按 SHA-1 建立索引。这一区别至关重要。本地账户数据库仍然需要专门设计的加盐密码哈希方案；新协议不应仅仅因为该范围接口用了 SHA-1 就跟着选它。

### 威胁模型与明确的限制

写代码之前，先确定哪些观察和失败值得关注。本文的工作流假定 FreeBSD 主机在输入那一刻是可信的。如果键盘记录器、恶意内核模块、敌意终端或拥有足够调试权限的进程攻陷了主机，范围查询无法保护候选密码。工作流还假定配置的 HTTPS 端点是预期服务，主机的信任库和时间都有效。

设计针对以下风险：

1. **通过进程参数泄露。** 密码从不作为命令行参数接收。查看进程就能看到参数，而且参数常被复制到 shell 历史、作业记录、支持会话记录里。
2. **通过环境变量泄露。** 密码不经环境提供。环境值可能通过诊断信息、某些权限模型下的进程检查、崩溃处理、自动化配置泄露出去。
3. **向范围服务泄露。** 离开主机的只有五个字符的前缀。完整摘要和后缀留在本地。
4. **响应大小的观察。** 请求要求服务对响应做填充。Pwned Passwords 文档描述的目标是 800 到 1,000 条结果，并用计数为零的合成条目来标识填充 **\[9]**。稠密范围可能本身就包含多于该目标的真实记录，所以客户端不能强制 1,000 行的上限。填充可以减少从流量大小获得的信息，但不能消除。
5. **不安全的网络行为。** 客户端只允许 HTTPS，保留常规的证书与主机名验证，设置较短的连接超时和总超时，并把网络状态不明当作未知结果，而不是当作干净的密码。
6. **解析器混淆。** 响应在每一行获得信任之前，必须先匹配狭义的文法。过大、为空、格式错误或混合格式的响应体，一律按失败即拒绝（fail closed）处理。
7. **日志里带机密。** 审计记录省略密码、完整摘要、前缀、后缀、出现次数、响应体、命令输出。

还有若干风险仍在。服务能看到源 IP 地址、请求时间、用户代理、20 位前缀。只能看到加密流量的网络监听者，仍能看到时间和大致大小。反复检查或逐字符增量检查会形成序列，缩小机密范围。HIBP 文档特别警告不要在每输入一个字符后就检查，因为监听者可以合并这些请求 **\[9]**。等候选密码输入完整，只发一次请求。

被攻陷的服务可能返回假数据。TLS 验证的是证书中指定的端点，而不是其语料库的真实性或完整性。未命中只说明该确切摘要在当时接受的响应中不存在。可用性故障、TLS 无效、超时、响应体格式错误，都必须产生不确定结果，绝不能产生未命中。

最后，shell 进程无法承诺从全部内存中确定地擦除变量。清空并 unset 变量只能缩短它的有效寿命，不提供正式的归零保证。要求更高保障的客户端应当使用小型编译程序，用 FreeBSD 的 **explicit\_bzero(3)** 这类原语清除敏感缓冲区；该函数的设计目标就是不被编译器的死存储优化删除 **\[7]**。

### 测试目标与依赖

这套实现最初于 2026 年 8 月 31 日在 FreeBSD 15.1-RELEASE 上验证，基本系统工具接口也对照 14.4 的文档做了核对。截至 2026 年 9 月 10 日，FreeBSD 安全页面列出的受支持版本为 15.1-RELEASE、15.0-RELEASE、14.5-RELEASE、14.4-RELEASE **\[2]**。FreeBSD 14.5 于 9 月 8 日发布；它出现在该列表中，并不表示本示例在其上测试过。发布页面把生产版本、旧版本与安全支持区分开来 **\[1]**。示例使用基本系统的 **sh(1)**、**sha1(1)**、**mktemp(1)**、**logger(1)**，并为了满足一项特定需求而引入 curl：范围服务的填充选项要用自定义 HTTP 请求头表达。FreeBSD 自带的 **fetch(1)** 提供 HTTPS 获取和证书控制，但它的命令行接口没有通用的自定义请求头选项 **\[4]**。

从 FreeBSD 软件包仓库安装 curl：

`# pkg install curl`

FreeBSD 手册把 `pkg install curl` 记录为标准二进制软件包流程 **\[5]**。该软件包通过包管理器引入它所需的 TLS 依赖。不要用 curl 的 `--insecure` 选项绕过信任库问题。应当修复信任库、系统时间、代理拦截或端点配置。

脚本把继承来的搜索路径换成一组简短的标准目录，这些目录由管理员控制。随后脚本解析包客户端和保留在变量里的基本系统工具；其他标准命令都走同一条固定路径。这一点在两个目标发行版系列之间很重要，因为基本系统工具的安装位置可能变化，即使其命令行接口仍然兼容。管理员应当在目标版本上用 `command -v curl sha1 logger uuidgen` 核对全部四项解析结果，而不是悄悄换成别的程序。软件包路径位于 **/usr/local** 之下；基本系统工具可以从已安装的手册页和文件系统确认。特别是，解析 `uuidgen` 可以避免依赖某个版本专属的绝对路径。

下面四个步骤中的 shell 代码块是同一个脚本的连续片段。请按顺序拼装。第一个代码块包含 shebang 和全部设置；后面的每个代码块都在同一个 shell 进程中继续。

### 步骤 1：读取候选密码，且不留在历史记录中

输入包装器不接受位置参数，并要求交互式终端。它临时关闭终端回显，读取一整行，恢复先前的终端状态，并在清空 shell 变量之前对确切字节做哈希。读取候选密码之前，它会清空用于机密材料的继承变量，并关闭 shell 跟踪。不要用调试器或跟踪包装器拿真实密码运行它。

```sh
#!/bin/sh
set +x
set -eu
umask 077

unset candidate digest prefix suffix count result http_code
result=

PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
export PATH

CURL=$(command -v curl) || exit 69
SHA1=$(command -v sha1) || exit 69
LOGGER=$(command -v logger) || exit 69
UUIDGEN=$(command -v uuidgen) || exit 69

[ "$#" -eq 0 ] || {
    echo "usage: exposure-check" >&2
    exit 64
}

[ -t 0 ] || {
    echo "refusing non-interactive password input" >&2
    exit 64
}

old_tty=$(stty -g)
restore_tty() { stty "$old_tty" 2>/dev/null || true; }
trap restore_tty EXIT
trap 'restore_tty; exit 129' HUP
trap 'restore_tty; exit 130' INT
trap 'restore_tty; exit 143' TERM

printf "Candidate password: " >&2
stty -echo
IFS= read -r candidate || {
    printf "\ninput failed\n" >&2
    exit 65
}
restore_tty
trap - EXIT HUP INT TERM
printf "\n" >&2

digest=$(printf '%s' "$candidate" | "$SHA1" -q)
candidate=
unset candidate
```

引号不是装饰。`printf '%s' "$candidate"` 保留空格，并防止通配符展开。不要用 echo，它对选项和反斜杠的处理会随输入和实现而变化。不要用 `sha1 -s "$candidate"`，因为那样密码会成为另一个进程的参数。

这个接口有意拒绝管道输入。这一选择让交互式工具难以被 CI 系统或 cron 作业误用。集成同一引擎的应用，应当通过进程内 API 或范围很窄的文件描述符传入候选密码，而不是放宽包装器去接受经参数传递的机密。

### 步骤 2：拆分摘要，发一次有界请求

把摘要规范化为大写，确认它恰好包含 40 个十六进制字符，然后切成五个字符的前缀和 35 个字符的后缀。只有前缀会插入 URL。

```sh
digest=$(printf '%s'
"$digest" | tr '[:lower:]' '[:upper:]')

[ "${#digest}" -eq 40 ] || exit 70
case "$digest" in
    *[!0-9A-F]*) exit 70 ;;
esac

prefix=$(printf '%.5s' "$digest")
suffix=${digest#?????}
digest=
unset digest

response=$(mktemp -t exposure-check) || exit 70
trap 'rm -f "$response"' EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

sleep 1

http_code=
if ! http_code=$("$CURL" -q \
    --fail \
    --silent \
    --show-error \
    --proto '=https' \
    --tlsv1.2 \
    --connect-timeout 5 \
    --max-time 20 \
    --max-filesize 1048576 \
    --retry 2 \
    --retry-delay 1 \
    --retry-max-time 45 \
    --user-agent 'freebsd-exposure-check/1.1' \
    --header 'Add-Padding: true' \
    --write-out '%{http_code}' \
    --output "$response" \
    "https://api.pwnedpasswords.com/range/$prefix")
then
    result=unknown
elif [ "$http_code" != 200 ]; then
    result=unknown
fi

prefix=
http_code=
unset prefix http_code
```

`-q` 选项必须是 curl 的第一个参数。它禁止自动加载 curlrc 文件，这样个人配置就无法悄悄加入跟踪、跟随重定向或关闭证书检查 **\[10]**。`--proto` 限制只允许 HTTPS。没有启用重定向跟随。客户端单独捕获 HTTP 状态，只接受 200，所以 3xx 响应不会被误当成范围数据。curl 默认验证对端证书和主机名。`--tlsv1.2` 设定最低 TLS 版本，同时不禁止客户端和服务器支持的更高版本。20 秒上限适用于每次传输尝试。`--retry-max-time 45` 防止重试窗口过去后再发起新重试，不过在限制之前已经开始的一次尝试可以在限制之后完成 **\[10]**。两次重试足以应对暂时性故障，又不会形成无界循环。

一秒延迟是本地运维策略，并不表示示例服务要求如此。它的文档目前写明 Pwned Passwords 端点没有速率限制 **\[9]**。本地设上限仍然有用，因为即使上游提供方没有公布固定配额，配置失误、UI 循环和批处理作业也可能造成滥用流量。

重试策略需要细致权衡。请求发出后超时，并不能证明服务没有做事，但这个操作是幂等的 GET，所以少量重试可以接受。如果端点契约发生变化，就要重新审视这一假设。绝不要在账户创建路径上无限重试，也绝不要把重试耗尽重新解释为未命中。

### 步骤 3：先校验，再比较

文档记载的 SHA-1 范围响应由若干行组成，每行是 35 个字符的十六进制后缀、冒号、十进制出现次数。填充行的形状相同，计数为零 **\[9]**。搜索之前先校验整个响应。curl 的 `--max-filesize` 选项让 curl 在响应超过上限时失败。其手册页说明，在没有声明可用大小时，curl 可能只在传输途中才发现大小，所以这是一道护栏，而不是文件系统配额 **\[10]**。下载后的检查会拒绝任何仍然留下的空响应体或超大响应体。这些都属于应用层限制，不能替代文件系统配额或更广泛的资源控制。

```sh
if [ "${result:-}" !=
unknown ]; then
    bytes=$(wc -c < "$response" | tr -d ' ')
    [ "$bytes" -gt 0 ] && [ "$bytes" -le 1048576 ]
|| result=unknown
fi

if [ "${result:-}" != unknown ]; then
    if ! awk -F: '
        {
            sub(/\r$/, "")
            if (NF != 2 || length($1) != 35 ||
                $1 !~ /^[0-9A-F]+$/ || $2 !~ /^[0-9]+$/) {
                exit 1
            }
        }
        END { if (NR == 0) exit 1 }
    ' "$response"
    then
        result=unknown
    fi
fi

if [ "${result:-}" != unknown ]; then
    if ! count=$(printf '%s\n' "$suffix" | awk -F: '
        FILENAME == "-" {
            if (FNR != 1 || length($0) != 35 ||
                $0 !~ /^[0-9A-F]+$/) exit 2
            wanted=$0
            next
        }
        {
            sub(/\r$/, "")
            if ($1 == wanted && ($2 + 0) > 0) {
                print $2
                exit
            }
        }
        END { if (wanted == "") exit 2 }
    ' - "$response")
    then
        result=unknown
    elif [ -n "$count" ]; then
        result=exposed
    else
        result=not_found
    fi
fi

suffix=
count=
unset suffix count
```

shell 的 printf 内建命令通过管道把选定的后缀传给 awk。不要用 `awk -v wanted="$suffix"`，那会把由密码派生出的 140 位值放进进程参数表。比较进程失败按未知处理。只匹配计数为正的后缀，可以丢弃填充记录。确切的出现次数有意不写入日志。UI 可以选择显示它，但团队应当先判断这个数字是否会改变处置方式。通常不会。有一次观察结果，就足以拒绝或轮换候选密码；而非常大的计数反而会让人对语料库是否新鲜、来源是否可靠产生虚假的精确感。

这个解析器对大写输出要求严格，因为示例端点规定后缀为大写。其他提供方可能规定别的大小写规则或别的哈希族。应当按提供方公布的契约调整文法，而不是加入宽泛的容错转换。宽松的解析器会把 HTML 错误页、代理警告或部分损坏的响应变成错误的安全决策。

### 步骤 4：让失败有自己的状态

安全检查需要三种结果，而不是两种：

* `exposed`：本地后缀匹配到一条校验通过的计数为正的记录；
* `not_found`：取到响应，大小受控，完整校验通过，且不含正匹配；
* `unknown`：检查无法确定以上任何一种。

把退出码分开，脚本就不会把运维失败误当成安全结果：

```sh
if !
event_id=$("$UUIDGEN" -r); then
    echo "Unable to create an audit event ID. Treat the result as
unknown." >&2
    exit 20
fi

case "$result" in
    exposed)
        if ! "$LOGGER" -p auth.notice -t exposure-check \
            "event=$event_id outcome=exposed source=pwned-passwords-range client=freebsd-exposure-check/1.1"
        then
            echo "Audit logging failed. Treat the result as unknown." >&2
            exit 20
        fi
        echo "Match found. Do not set or reuse this password."
        exit 10
        ;;
    not_found)
        if ! "$LOGGER" -p auth.notice -t exposure-check \
            "event=$event_id outcome=not_found source=pwned-passwords-range client=freebsd-exposure-check/1.1"
        then
            echo "Audit logging failed. Treat the result as unknown." >&2
            exit 20
        fi
        echo "No match found in the checked corpus. This is not a safety guarantee."
        exit 0
        ;;
    *)
        "$LOGGER" -p auth.warning -t exposure-check \
            "event=$event_id outcome=unknown source=pwned-passwords-range client=freebsd-exposure-check/1.1" || true
        echo "Check unavailable or invalid. Treat the result as unknown." >&2
        exit 20
        ;;
esac
```

FreeBSD 的 **logger(1)** 是 syslog 的 shell 接口，它的设施和优先级可以通过 **syslog.conf(5)** 路由 **\[6]**。上面的记录所证明的事实，比许多审计设计声称的要小：某个具名客户端在系统日志所记录的时间点记下了一个结果，并带有不透明的事件标识符。它不能以密码学方式证明检查了哪个密码、服务语料库是否正确，或者用户是否依据该结果采取行动。

这一限制是有益的。日志里记下前缀，就会保留关于候选密码的 20 位信息。记下完整摘要，会造出高效的离线猜测目标。记下摘要的确定性 HMAC，会带来关联能力，而且对后来拿到 HMAC 密钥的人来说，它又会变成校验器。如果受监管的工作流需要更强的证据，就把事件绑定到内部账户操作上：在应用审计系统里存入无关的应用事件 ID 和策略决定，例如 `password_change_rejected`。把机密派生材料挡在证据记录之外。

日志访问和保留期仍然重要。FreeBSD 的日志框架可以把本地消息路由到文件或远程收集器，它的 BSM 审计设施可以捕获细粒度的安全事件 **\[11, 12]**。日志更多并不自动带来更多保障。要确定谁可以读取事件、如何轮转、证据在多长时间内仍然有用。没有存储和复查计划，就不要开启大批量审计。

### 缓存，但不在磁盘上造出密码预言机

同一前缀的所有密码共用范围响应，所以缓存可以减少重复的网络调用。它也改变了数据模型。像 `21BD1.txt` 这样的缓存文件名，记录了主机上有人请求过该前缀。响应里包含来自公开语料库的数百个后缀。两者都不是明文密码，但在小型环境里，或者与其他观察结合起来时，请求历史仍然可能是敏感的。

对交互式管理工具来说，最简单的安全策略是不做持久缓存。临时响应在严格的 `umask` 下创建，只用一次，由能响应信号的 trap 删除。对繁忙的密码设置服务，改用带短生存时间的内存缓存。如果确实需要持久化：

1. 把响应存在专用服务账户拥有的目录里，权限 0700；
2. 只用前缀文件名，路径里绝不放用户 ID 或账户名；
3. 设置较短且有文档记载的过期时间；
4. 用与网络内容相同的文法校验缓存内容；
5. 获取成功后原子替换文件；
6. 绝不存储选定的后缀、完整摘要、候选密码或用户级匹配结果；
7. 在监控里包含缓存年龄，但除非策略规定了最大年龄，否则不要把它放进面向用户的通过或拒绝决定。

整个公开语料库的离线副本可以消除向远程服务的查询披露，但会引入一个庞大的数据集，必须下载、更新、校验、存储、搜索。总体上看，它并不自动更私密。正确的选择取决于查询量、出站策略、存储控制、更新纪律，还有本地请求元数据有多敏感。

### Jail、Capsicum、rc.d：选对边界

FreeBSD Jail 虚拟化文件系统、用户和网络访问，可以围绕长期运行的集成提供一层有用的边界 **\[13]**。它不能让不安全的客户端变安全，对于单个管理员把候选密码敲进短命本地进程的场景，也没有必要。价值最高的控制仍然简单：不要让机密经由参数传递，不要记录机密派生值，约束出站流量，失败即拒绝。

当泄露检查成为组织级服务的一部分时，Jail 就变得合理。用专用的非特权账户运行队列消费者。只给 Jail 它需要的文件。需要独立的出站控制时，使用 VNET Jail 或主机防火墙规则。FreeBSD 基本系统包含 PF、IPFW、IPFILTER，手册说明了如何用防火墙规则控制入站和出站流量 **\[14]**。把工作进程限制在策略要求的 DNS 和 HTTPS 目标上，同时为端点地址变化和证书验证做好安排。

Capsicum 是专用客户端的另一个选择。FreeBSD 把 Capsicum 描述为能力与沙箱框架，在打开所需的描述符之后限制对全局命名空间的访问 **\[8]**。编译版检查器可以先打开终端、信任库、解析器通道、日志套接字、输出目标，然后在解析不可信响应数据之前进入能力模式。这种设计需要仔细拆分，因为名称解析和新建出站连接通常涉及全局命名空间。它是值得采取的下一步，光靠 shell 脚本里拨个开关做不到。

同样的克制适用于 rc.d。FreeBSD 的 rc.d 框架通过使用 **/etc/rc.subr** 的小型 **sh(1)** 脚本来启动和管理服务 **\[15]**。交互式检查器不是守护进程，不应在启动时运行。如果应用使用常驻队列工作进程，就把该工作进程装到 **/usr/local/libexec** 下，在 **/usr/local/etc/rc.d** 下放一个常规控制脚本，默认禁用，并通过 `rc.subr` 调用 `run_rc_command`。把密码输入留在应用边界之内。rc.d 脚本只应管理该工作进程的生命周期，绝不能包含凭证或候选密码值。

### 可复现的测试计划

把纯解析和决策逻辑与机密输入分开测试。开发时不要使用真实账户密码。使用有意公开的测试数据，或者随机生成的合成值，这些值从不指派给任何账户。

#### 1. 已知命中

使用公开记录的演示字符串，而不是私人凭证。确认本地 SHA-1 是 40 个十六进制字符，请求只包含其中五个，校验通过的响应中含有计数为正的匹配后缀，程序以 10 退出，审计记录不含任何机密派生字段。

#### 2. 预期未命中

在本地生成一个很长的随机合成值，检查一次，预期得到 `not_found`。任何值理论上都可能出现在语料库里，所以这个断言应当表述为预期测试结果，并在单元测试中绑定到一份捕获且校验过的响应。实时集成测试可能随时间变化。

#### 3. 填充的零计数条目

给解析器喂一个有效的 35 字符后缀，计数为零。确认它被忽略。然后在另一份固定数据里放入同一后缀但计数为正，确认它能匹配。

#### 4. 格式错误的响应体

测试空文件、HTML 页面、长度错误的后缀、非十六进制字符、缺少冒号、负数计数、多余字段、超过配置上限的响应体。每种情况都必须产生 `unknown` 并以 20 退出。

#### 5. 网络与 TLS 失败

测试连接被拒、DNS 失败、超时、受控测试环境中的不可信证书、指向 HTTP URL 的 HTTPS 重定向。任何一种都不得产生 `not_found`。确认向用户显示的错误不含摘要或响应体。

#### 6. 终端中断

输入候选密码时按 Control-C，请求尚未返回时也按一次。确认回显恢复、临时文件删除。在测试 shell 中发送 HUP 和 TERM，再重复一次检查。

#### 7. 审计复查

检查配置的日志目标。可接受的记录格式应当只包含事件 ID、结果、来源标识符、由日志路径提供的系统时间，也许还有客户端版本。在测试日志中搜索候选密码、摘要、前缀、后缀、出现次数、响应行。所有搜索都必须无结果。再用合成候选密码检查子进程参数和导出的变量：候选密码、完整摘要、选定后缀都不应出现。curl 请求 URL 中的五个字符前缀仍可被本地进程检查看到；这属于有界披露的一部分，并不承诺对主机隐藏请求。

#### 8. 并发与限流

如果集成进服务，就启动并发检查，确认共享限流器限制的是总流量，而不只是每个进程的流量。确认缓存替换是原子的，刷新失败后要么留下仍然有效的旧条目，要么明确进入未知状态。

编辑或审稿人应当能够在不调用实时服务的情况下复现解析器测试。把小型合成响应固定数据放进源码树，把网络集成测试设为可选。这样的拆分让日常测试既快又确定，也不会滥用公开端点。

### 运维策略比查询本身更重要

范围检查做得再好，也可能支撑糟糕的安全策略。这项控制应当放在密码创建和修改环节，也就是候选密码提交之前。发现命中时，拒绝该候选密码，并说明它出现在已知语料库里。不要透露泄露记录，也不要暗示用户的具体账户已被入侵。把这项控制与账户工作流的速率限制、多因素认证、安全恢复、会话复查、账户接管信号监控配合起来。

组织获得授权后使用时，要明确界定范围。检查你自己身份验证工作流中提供的候选密码，或者组织在法律上有权管理的资产。不要收集无关用户的密码，不要测试第三方账户，也不要把范围端点变成枚举工具。

定期复查端点契约。前缀长度、填充行为、响应文法、可接受使用规则、客户端要求都可能变化。把假设写死在代码里又不监控文档，会形成安静的失效模式。在审计事件里记录客户端版本，软件包升级后做测试，遇到意外响应就停下来，而不是放宽校验。

### 结论

保护隐私的凭证泄露检查，靠的不是把密码哈希后调用 API。它来自完整的数据路径：候选密码如何进入进程，什么离开主机，网络请求如何受约束，不可信数据如何解析，缓存了什么，存在哪些结果，还有审计线索拒绝保留什么。

在 FreeBSD 上，小型实现可以用上熟悉的系统组件。**sha1(1)** 提供与语料库兼容的查询摘要，**mktemp(1)** 创建私有响应文件，**logger(1)** 记录最小事件，软件包系统提供 curl 以发起带填充的 HTTPS 请求。当检查器成长为一套服务时，Jail、防火墙、Capsicum、rc.d 就会派上用场，但它们应当强化健全的协议，而不是弥补带有机密的参数或日志。

最重要的结果是 `unknown`。能区分不确定与不存在的系统，失败时才安全。把超时和解析错误合并成“not found”的系统，最终会告诉别人某个未检查的密码是干净的。保留这第三种状态，只披露有界前缀，不留下可复用的校验器，就能把一次巧妙的范围查询变成可审计的安全控制。

### 参考文献

1. The FreeBSD Project，“发布信息”。 <https://www.freebsd.org/releases/>
2. The FreeBSD Project，“FreeBSD 安全信息：受支持的 FreeBSD 版本”。 <https://www.freebsd.org/security/>
3. The FreeBSD Project，**md5(1)**，其中包含 **sha1(1)**。 <https://man.freebsd.org/cgi/man.cgi?query=md5&sektion=1>
4. The FreeBSD Project，**fetch(1)**。 <https://man.freebsd.org/cgi/man.cgi?format=html&query=fetch&sektion=1>
5. The FreeBSD Documentation Project，“安装应用：软件包与 Ports”。 <https://docs.freebsd.org/en/books/handbook/ports/>
6. The FreeBSD Project，**logger(1)**。 <https://man.freebsd.org/cgi/man.cgi?format=html&query=logger&sektion=1>
7. The FreeBSD Project，**explicit\_bzero(3)**。 <https://man.freebsd.org/cgi/man.cgi?format=html&query=explicit_bzero&sektion=3>
8. The FreeBSD Documentation Project，“安全：Capsicum”。 <https://docs.freebsd.org/en/books/handbook/security/#capsicum>
9. Have I Been Pwned，“API 文档：Pwned Passwords”。 <https://haveibeenpwned.com/API/v3>
10. The curl Project，“**curl(1)** 手册”。 <https://curl.se/docs/manpage.html>
11. The FreeBSD Documentation Project，“配置、服务、日志与电源管理：配置系统日志”。 <https://docs.freebsd.org/en/books/handbook/config/#configtuning-syslog>
12. The FreeBSD Documentation Project，“安全事件审计”。 <https://docs.freebsd.org/en/books/handbook/audit/>
13. The FreeBSD Documentation Project，“Jail 与容器”。 <https://docs.freebsd.org/en/books/handbook/jails/>
14. The FreeBSD Documentation Project，“防火墙”。 <https://docs.freebsd.org/en/books/handbook/firewalls/>
15. The FreeBSD Documentation Project，“BSD 中的实用 rc.d 脚本编写”。 <https://docs.freebsd.org/en/articles/rc-scripting/>

***

### 利益披露

作者隶属于 CyberVisir Solutions Ltd.，该公司运营 LeakData.io。本工作流与产品无关；提及公司是为了说明作者的隶属关系。文中使用公开文档和合成测试数据，没有复现任何私有泄露记录或真实凭证。AI 工具协助了研究组织、语言编辑和代码复查。最初的实现在 FreeBSD 15.1-RELEASE 上验证过；九月修订版加入了上述澄清和对实现的修正。

***

**Emre Çapan** 是 CyberVisir Solutions Ltd. 旗下 LeakData（<https://leakdata.io>）的联合创始人兼 CTO。他的工作聚焦于数字风险监控、泄露事件分级、注重隐私的安全工作流，还有把不确定的安全信号变成可落地的处置步骤。


# EuroBSDCon 参会报告

* 原文：[EuroBSDCon Trip Report](https://freebsdfoundation.org/our-work/journal/browser-based-edition/production-deployments/eurobsdcon-trip-report/)
* 作者：**Federico Angelilli**

## 在比利时布鲁塞尔 EuroBSDCon ’26 上演讲（2026 年 9 月）

![EuroBSDCon 2026](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-41540cc9ab996dfc66440a0efd88d055ecd88341%2Feurobsdcon-trip-report-01.jpg?alt=media)

今年我有幸参加了 FreeBSD 项目的谷歌 Summer of Code（GSoC）。每年夏天，谷歌都会赞助这项活动，为学生配上开源项目的导师。学生参与项目几周（我这边是 12 周），之后获得一笔津贴作为报酬。

我是偶然得知这个机会的。由于我喜欢做底层内核编程，选择 FreeBSD 项目显得很自然。我的提案“FreeBSD 内核的实时补丁（Live-patching）”被接受后，我花了三个月做出该子系统及相关工具的第一个修订版本。我的导师 Bojan Novkovic 和 Mateusz Piotrowski 建议我参加 EuroBSDCon，并邀请我到开发者峰会上展示工作、收集反馈以便改进。

我决定采纳他们的建议。因为还是学生，我向基金会申请了差旅资助，基金会慷慨批准。今年的活动持续五天（9 月 9 日至 13 日）：前两天是 FreeBSD 开发者峰会，一天用于教程，之后是正式会议。场地选在布鲁塞尔这座可爱城市的布鲁塞尔自由大学（VUB）校园，此前我只来过这里一次。往返扎芬特姆的布鲁塞尔机场都很顺利，我找的住处离会场只有 2 公里。

我和开发者峰会的组织者 Benedict Reuschling 确认了演讲时段。出乎意料，我拿到了第一场演讲，在第一天 10:30。结果这反倒成了好事，我得以轻松打破僵局。紧张的几分钟过后，我顺利地讲完了。所幸大家都很友善，提出了有用的反馈和值得思考的问题。我起初很担心资深开发者会不喜欢。我这种想法大概来自其他圈子：在那里，年轻人或外来者的贡献一旦触及关键组件，就会遭到恶评。但在 FreeBSD 社区，我完全没看到这种情况，这一点我很喜欢。相反，我立即得到了好问题和好反馈，就像给“同行”开发者提的那样。例如 markj@ 问，如何把安全公告的补丁作为实时补丁模块来应用，以判断未来能否这样部署。

我的场次结束后，我继续听了当天的其他演讲。我有很多机会结识与会者并与他们交谈：Robert Clausecker、Olivier Certner、Aymeric Wibo、Kyle Evans、Mark Johnston、Kristien Nielsen、Christos Longro、Charlie Li，仅举几例。总体而言，我的实时补丁工作反响相当不错，这很令人鼓舞。

第二天我继续听演讲。我特别喜欢 olce@ 关于调度器改进的那场。我和两位同样年轻的与会者 George（Polarian）和 Goran 聊了不少。我也很高兴在会议上见到另外几位意大利人，比如 Vanja Cvelbar、Toni Tiveron 和 Stefano Marinelli。我和 Eirik Overby 聊得很好，他带了几台老式计算机到会场，其中有一台 486。后来他邀请我和他以及他在 Entersekt 的几位同事共进晚餐。

第三天上午我听了 EuroBhyveCon 的几场演讲，下午参加了 Mateusz 的“Ports 任意% 速通”教程。这一天以一顿美妙的晚餐收尾：Aymeric 为我和其他年轻与会者在当地一家餐厅做了安排。在那里，我还有幸与 Kirk McKusik 和 Eric Allman 共进晚餐。

周六是正式会议的第一天。那天我听了以下演讲：

* 《我那 142 台服务器升上云端的那一夜。字面意义上。》——Stefano Marinelli
* 《面向现代异构系统的运行时重优化》——gnn@
* 《BSD 作为日本全国半导体教育的基础平台》——Hiroki Sato
* 《生产环境中的基本系统软件包：实用概览》——Lukas Engelhardt

当晚的社交活动在城郊的非洲博物馆举行。我们乘坐一辆漂亮的老式有轨电车前往。活动的食物、同伴和场地都让我十分享受。

周日是活动的最后一天，相当忙碌。上午 Pontus Stenetorp 办了个小型抽奖，我最终赢到一套 nRF52 开发套件。那天我参加的场次有：

* 《bhyve 虚拟机管理程序的热插拔历险》——Bojan
* 《用 CHERI 为 BSD 带来内存安全》——Brook Davis
* 《ELKE——加密且可爱的 Kage 环境，基于 FreeBSD》——Vinícius
* 《跟上 Ports 中基于语言的打包系统（尤其是 Python）》——Charlie Li

我还参加了为会议收尾的最后一场活动，其中包括传统的失物拍卖。

最终，我从这次会议中学到了很多，其中不少难以用言语表达。但我要说：这个社区虽然相对较小，却十分珍贵，必须守护下去。对所有的同学，我想说：一定要来参加会议，哪怕你不是 BSD“专家”。我交谈过的每个人都非常友善，乐于帮助你融入社区。

再次感谢 FreeBSD 基金会赞助我参加我的第一次 BSD 会议。——Federico Angelilli

***

**Federico Angelilli** 是意大利学生，正在罗马大学（Sapienza University of Rome）完成计算机工程学士学位。他热衷于操作系统、系统编程和参与开源社区。


# 读者来信

* 原文：[Choice of Agonies](https://freebsdfoundation.org/our-work/journal/browser-based-edition/production-deployments/choice-of-agonies/)
* 作者：**Michael W Lucas**

**亲爱的建议君：**

我得到机会从零搭建一个全新的洁净室生产环境，操作系统和应用栈都由我来选！这是一生一次的机会，我却发现自己被各种选项淹没了。我知道你开玩笑说生产环境用算盘跑就行，实在不行用手指。可说正经的，我该怎么选？

**——困在分析瘫痪中**

**分析瘫痪君：**

写建议专栏最烦人的部分，并不是想建议。我快 60 岁了。我这辈子的失败经历随时可以为你提供教益——只要它们认为你会听劝。我的经验多到编委会不允许我写出来，那些胆小鬼还明文规定：任何建议里脏话的重量占比不得超过一成。

更不是你以为我在开玩笑这件事。我说的是实话。你的大脑出于可悲的自我保护本能，偏要把实话当成玩笑。

不，这份差事最让人恼火的，是那些明目张胆的假来信。

别误会。我完全清楚你们的来信都是假的。你们有些人连一丝最吝啬的想象力都不肯花，来给骗局遮片无花果叶。看看这个格外幼稚的例子：从零搭建一套新的生产环境？那数据中心大概是 Kongamato 管着，设备是圣诞老人送的吧——因为这种事根本不会发生。一切都建立在别的东西之上、之内、之外。计算这锅炖菜在灶上咕嘟了不知多少年：你父母从祖父母那儿继承，祖父母从曾祖父母那儿继承，曾祖父母当年坐着泰坦尼克号把它从旧大陆带过来。锅里的汤一少，你就往里扔点新鲜货，比如一棵芜菁。或者一个实习生。有时你切一块出来塞进肠衣，或者兑水做成汤，可只有不知羞的人才会管这叫新东西。

你说你有机会做洁净室安装，可洁净室这东西压根是个神话。

可惜啊，分析瘫痪君，你这个假来信是我这个月收到的唯一一封勉强沾边的假货，那就用它吧——但别以为，哪怕经过 NTP 校准的一瞬，你就能蒙混过关。

特里·普拉切特有一句名言：“希望是最好的礼物。”我绝不敢跟这位二十与二十一世纪最伟大的哲学家争辩。不过《系统管理法则》第 20 条提醒我们：“希望会让你做不好本职工作。”从这里入手正合适。

“完美环境”的神话弥漫在这摊乱糟糟的行当里。要是当初架构师选对了方案该多好！等你成了架构师才明白：没有正确答案，只有在各种痛苦之间做选择。挑一种疼法吧。我毫不掩饰把所有数据都放在 ZFS 上，但代价是：我用一个老实把块写到磁盘上的简单文件系统，换来了一个自带内存缓存算法、动态块大小、可调冗余度，以及不可避免、令人叹为观止的各种毛病的存储操作系统。你那台只有 1 TB 磁盘的虚拟机，会不会需要 3 TB 的物理底层存储？用 ZFS 的魔法，完全可能！

不，等等。有好处才叫“魔法”，没好处就叫“愚蠢”。

魔法与愚蠢，是同一件事从相反方向看到的样子。

抱怨生产环境问题，是系统管理员的一项神圣传统。在会议酒吧里泡一会儿，你很快就能认出这种讨论的套路。“新系统部署慢得要命，因为我们用 Ansible！”“用 Chef 更新软件简直是噩梦！”“我们倒是想，可我们用的是 Salt！”Apache 不行，nginx 不肯，lighthttpd 什么都做，唯独不做那些毫无疑问必须做的事，于是在五个不同的 Web 服务器前面塞一个 Caddy 反向代理，管这叫 HTTP/3。从零开始，只允许 Postgres 加 Python？太棒了！可惜每个市场部都要 WordPress，于是整个 PHP/MariaDB 生态被一并拖进来；哪怕你有架构上的全权，只要在会议上说出“污染”之类的词，也会被请去人力资源部。

生产环境能否成功，关键不在操作系统的选择。应用栈本身无关紧要。XMLRPC 还是 SOAP，还是 System V IPC？谁在乎？

生产环境部署成功与否，取决于一条简单的原则。

“把恐惧降到最低。”

就这一条。

如果管理、升级或调试某个工具会让你恐惧，就别用它。

这不是让你抱着熟悉的工具不放的借口。系统管理员必须不断学新东西。干这行的人迟早都会被拖到“白发议会”面前接受粗暴的盘问。如果认定你大致够格，那位充当 root 的人就会敲下法槌，宣布你是真正的系统管理员，并判处你终身强制学习。还好，意识会屏蔽掉这段恐怖经历，只留下一点底层创伤的残余。

你知道得越多，能察觉的糟糕之处就越多。恐惧不断增长，直到压垮你的事业或者你的循环系统。

如果无法把恐惧降到最低，就把它变成无聊。研究你环境里最糟糕的部分，反复练习它们。挑几种恐惧请来吃晚饭，来一场双方自愿但足够彻底的亲昵。没有无缘无故的可怕。人们总爱宣称某个软件或协议之所以可怕，是因为设计者是个白痴，但这种情况基本不存在。许多商业产品之所以可怕，是因为设计者的目的本就是从你口袋里掏钱，而不是解决你面对的任何问题。而在开源世界里，能活下来的糟糕工具，绝大多数是因为缺少不那么糟糕的替代品。那个糟糕的工具填的是一个更丑陋的窟窿，但理解这个窟窿，就能超越恐惧。

如果全都不管用呢？那就靠脚本、检查清单和流程——最好是把绝望外包给某个还没收到议会传票的幼稚青年的流程。把计算行业真正的可憎之处摊开给新手看，是你最负责任的一次帮忙——呃，是“为”新手做的最负责任的事。否则他们最后可能会写些假来信，试图钓鱼某个建议专栏作家。或者更糟：生出希望。

***

**Michael W Lucas**（<https://mwl.io>）写过太多书，包括 [*Absolute FreeBSD*](https://mwl.io/static/books/absolute-freebsd-third-edition.html) 和 [*$ git commit murder*](https://mwl.io/static/books/git-commit-murder.html)。他目前正一边躲着假释官，一边与 Allan Jude 合写 [*OpenZFS Mastery*](https://www.tiltedwindmillpress.com/product/openzfs-sponsor/)。


# 基金会来信

**亲爱的《FreeBSD 期刊》社区：**

欢迎阅读本年度第二季度《FreeBSD 期刊》。

本期聚焦 **改进软件质量**。文章探讨了这项工作在 FreeBSD 生态系统中的多种体现，从在 FreeBSD 上使用 CHERI 调试内存安全缺陷，到与系统各部分及社区相关工作者的访谈。

你还会看到 Michael Lucas 一如既往精彩的读者来信、Tom Jones 对 Harald Eilertsen 的访谈、一篇关于欧洲网络弹性法案与 SBOM 的文章，以及基金会的常规更新和活动日历。

本期也是期刊的重要过渡期。James Maurer 自 2014 年《FreeBSD 期刊》创刊起担任编辑，如今即将卸任。James 在期刊初创时期便参与塑造其发展方向，十余年来引领着期刊的基调、一致性与质量。我们深深感谢他的卓越工作、稳健领导，以及对 FreeBSD 社区的持久贡献。

同时，我们很高兴欢迎 Matt Slaybaugh 接任这一职务。Matt 在技术出版领域拥有多年经验，曾在 Ziff-Davis 和 ACM 等出版机构磨练技艺，服务过纽约大学和联合国等组织，还经历过许多初创公司。他的背景、编辑经验和对技术受众的理解，使他成为《FreeBSD 期刊》下一阶段的合适人选。

请与我们一起感谢 James 为期刊所做的一切，并欢迎 Matt 加入 FreeBSD 社区。

希望你享受本期内容。

一如既往，我们乐于倾听读者的声音。如果你有反馈、对未来文章主题的建议，或有意撰写文章，请发送邮件至 <info@freebsdjournal.com>。

**Anne Dickison**

FreeBSD 基金会副主任


# 活动日历

* 作者：**Anne Dickison**

本文列出了截至 2026 年 9 月的 BSD 相关活动。如有任何未在此列出的 FreeBSD 相关活动或对 FreeBSD 用户感兴趣的活动，请发送至 <freebsd-doc@FreeBSD.org>。

![FreeBSD logo](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4d2bb642c491f540beb7b2e16bdf2f6b17b8b19d%2Fevents-calendar-01.png?alt=media)

## 2026 FreeBSD 黑客马拉松

2026 年 9 月 4-6 日

德国柏林

<https://wiki.freebsd.org/Hackathon/202609>

欢迎加入 FreeBSD 社区，参加 2026 年 9 月的 FreeBSD 黑客马拉松。活动将在德国柏林举办，时间在 EuroBSDCon 2026 前的周末。黑客马拉松汇聚 FreeBSD 开发者和贡献者，协作推进项目、分享想法、并肩工作，随后一同前往 EuroBSDCon。

## 2026 年 9 月 FreeBSD 开发者峰会

2026 年 9 月 9-10 日

比利时布鲁塞尔

<https://wiki.freebsd.org/DevSummit/202609>

2026 年 9 月 FreeBSD 开发者峰会与 EuroBSDCon 2026 同期举办，将汇聚开发者、贡献者和工作组，进行深入讨论与协作。开发者峰会提供了面对面交流、分享进展、提出问题、与 FreeBSD 社区同仁共同探讨想法的宝贵机会。

## EuroBSDCon 2026

2026 年 9 月 9-13 日

比利时布鲁塞尔

<https://2026.eurobsdcon.org/>

EuroBSDCon 是年度国际技术大会，每年在欧洲不同国家举办。大会聚焦于汇聚使用 4.4BSD（伯克利软件发行版）操作系统家族及相关项目的用户和开发者。FreeBSD 基金会很荣幸成为本次活动的银级赞助商。


# 使用 CHERI 调试

## CHERI 旨在提升既有与未来软件的安全性。

### John Baldwin

[CHERI](https://cheri.cst.cam.ac.uk/) 是一种面向安全的新型处理器技术。CHERI 借鉴历史能力系统（capability system）的原理，通过扩展 C/C++ 的隐式与显式指针，使其包含边界和权限等元数据，从而提升用 C 和 C++ 编写的既有软件的安全性。例如，在 CHERI 架构上，调用 `malloc()` 返回的指针会被限定在所请求的大小范围内，若试图访问分配边界之外的数据，将触发处理器异常。类似地，指向栈上缓冲区的指针在传递给另一函数时，会被限定在该缓冲区的大小范围内，而非授予对整个线程栈的访问权限。为存储这些额外信息，CHERI 定义了一种新的硬件类型：CHERI capability（能力）。

CHERI 能将某些未定义行为（UB）转化为崩溃，从而缓解漏洞。这种缓解从安全角度看很有价值，但 CHERI 还能辅助调试，既能为用户提供更多信息（例如内存分配的边界），也能更早捕获错误。

随着时间推移，支持 CHERI 的软件库不断壮大，包括 FreeBSD 的完整移植版——[CheriBSD](https://www.cheribsd.org/)，以及若干第三方应用，其中涵盖 KDE 图形桌面环境的大部分组件。CHERI 研究团队还维护着 GNU 调试器的一个 [分支](https://github.com/CTSRD-CHERI/gdb)，支持检查 CHERI 架构（包括 ARM 的 Morello 和 CHERI-RISC-V）。CHERI GDB 既支持调试 CheriBSD 用户进程，也支持调试内核。由于 CheriBSD 紧跟 FreeBSD 的开发进展，针对 FreeBSD 报告的 bug 通常也能在 CheriBSD 上复现并排查。

本文将考察 FreeBSD bug 数据库中几份高质量 bug 报告在 CheriBSD 上的排查过程，借此展示在 CHERI 上调试的体验，以及使用 CHERI 时可能遇到的一些出乎意料的结果。排查期间，我使用了运行在 [ARM Morello](https://www.arm.com/architecture/cpu/morello) 系统上的 CheriBSD，以及在我 FreeBSD/amd64 桌面机上通过 QEMU 运行的 CHERI 镜像。在 Morello 系统上用 GDB 交互调试更顺手一些，尤其是排查用户态问题时。不过，QEMU 的调试服务器在排查内核 bug 时非常有用。QEMU 方案也可在任何运行 FreeBSD、Linux 或 macOS 的普通系统上使用。使用 QEMU 时，我借助 [cheribuild](https://github.com/CTSRD-CHERI/cheribuild) 工具构建 CheriBSD 磁盘镜像，以及 LLVM、GDB 和 QEMU 等所需工具。在 Morello 系统上，我使用 CheriBSD 的软件包仓库安装 LLVM 和 GDB，并使用 FreeBSD 的标准构建系统构建和测试基本系统组件的新版本。

### 第一个 Bug

Bug [271955](https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=271955) 描述了 [ppp.8](https://man.freebsd.org/ppp/8) 工具 CHAP 解析代码中的缓冲区溢出问题。除了非常详细的描述外，该 bug 还附带一个触发溢出的测试程序，便于复现 bug 和测试潜在修复。我首先在 Morello 系统上下载测试程序源码，编译并执行。虽然这确实 fork 并 exec 了一个崩溃的 ppp.8 进程，但崩溃与 bug 中描述的问题并不一致。相反，ppp.8 在更早的位置就崩溃了，如图 1 所示。与非 CHERI 架构上使用 GDB 相比，首先注意到的一点是：指针在方括号中标注了额外数据。例如，`ID0open()` 函数的参数 `path` 标注了字符串 `[rwRW,0x1d8aa0-0x1f1f60]`。逗号前的第一个字段表示指针的权限。此处该指针既可读写数据（小写字母），也可读写其他指针（大写字母）。逗号后的地址范围给出该指针的边界，描述了可访问地址的范围。另一个值得注意的细节是，此次崩溃关联了一种新信号 SIGPROT，CheriBSD 用它表示 CHERI 异常。本例中，一次内存操作使用了无效 capability，导致崩溃。这行代码中的指针解引用可能并不显眼，但 `va_arg()` 的调用实际上是一次隐式内存引用。查看图 2 中的周边代码，可以看到该函数假定参数 `flags` 之后总存在第三个整数参数，这看上去有些可疑。可变参数的意义就在于它们并非总是存在。向上回溯栈帧查看调用者（图 3），调用者实际上只向 `ID0open()` 传递了两个参数。在 C 中，试图获取不存在的可变参数属于未定义行为（UB）；只不过在 CHERI 上比 FreeBSD 支持的其他架构上后果更严重罢了。

图 1：GDB 崩溃摘要

```sh
Core was generated by `ppp'.
Program terminated with signal SIGPROT, CHERI protection violation.
Capability tag fault.
#0 0x0000000000195ed0 in ID0open (
   path=0x1d8af4 <bundle_Create.bundle+84> [rwRW,0x1d8aa0-0x1f1f60] "/dev/tun0",
   flags=flags@entry=2) at /usr/home/john/work/git/cheribsd/usr.sbin/ppp/id.c:148
148	ret = open(path, flags, va_arg(ap, int));
```

图 2：ID0open 源码列表

```sh
(gdb) l
143	int ret;
144	va_list ap;
145
146	va_start(ap, flags);
147	ID0set0();
148	ret = open(path, flags, va_arg(ap, int));
149	log_Printf(LogID0, "%d = open(\"%s\", %d)\n", ret, path, flags);
150	ID0setuser();
151	va_end(ap);
152	return ret;
```

图 3：ID0open 的调用者

```sh
(gdb) up
#1 0x0000000000158f90 in bundle_Create (
   prefix=0x1322b8 [rR,0x1322b8-0x1322c1] "/dev/tun", type=1,
   type@entry=1254072, unit=unit@entry=-1) at /usr/home/john/work/git/cheribsd/usr.sbin/ppp/bundle.c:726
726	bundle.dev.fd = ID0open(bundle.dev.Name, O_RDWR);
```

从图 2 列出的代码和函数名可以看出，该函数的用途是封装 [open.2](https://man.freebsd.org/open/2) 系统调用，以 root 身份执行 open.2。open.2 的文档说明：若传入标志 `O_CREAT`，则接受可选的第三个参数。因此，针对 ppp.8 中该 bug 的 [修复](https://reviews.freebsd.org/D57137) 方案是：仅当指定 `O_CREAT` 时才获取可变参数。解决了这个问题之后，可以回到原始 bug 的调试。

用修复后的 ppp.8 二进制文件重新运行测试程序后，迎来了图 4 所示的内核崩溃。为便于调试，我改在 FreeBSD/amd64 桌面系统上用 QEMU 测试。我通过 `-s` 命令行参数在 QEMU 中启动 GDB 服务器，并用 `kgdb-native` cheribuild 目标构建的原生 CHERI GDB 二进制文件连接到 QEMU 客户机。

图 4：内核崩溃消息

```sh
panic: Capability abort from kernel space: bounds violation
cpuid = 0
time = 1779294044
KDB: stack backtrace:
db_trace_self() at db_trace_self
db_trace_self_wrapper() at db_trace_self_wrapper+0x3c
vpanic() at vpanic+0x1a0
panic() at panic+0x48
cap_abort() at cap_abort+0x288
handle_el1h_sync() at handle_el1h_sync+0x18
--- exception, esr 0x9600002a
_memcpy() at _memcpy+0xf4
make_packet() at make_packet+0x90
pppoe_start() at pppoe_start+0x180
ng_pppoe_rcvmsg() at ng_pppoe_rcvmsg+0xba4
ng_apply_item() at ng_apply_item+0x348
ng_snd_item() at ng_snd_item+0x364
ngc_send() at ngc_send+0x1f4
```

连接到客户机后，我向上回溯栈帧到 `make_packet()` 函数的栈帧。查看图 5 中传给 `bcopy()` 的参数，代码试图从仅 4 字节长的缓冲区复制 12 字节。阅读 `bcopy()` 周围循环中的其余代码，可以看出这些 tag 结构有一个 4 字节的首部，包含类型和长度（以网络字节序表示），其后是数据缓冲区，长度由首部给出。变量 `tlen` 包含总长度，那么为何指针的边界过小？

图 5：make\_packet() 栈帧

```sh
#18 0xffff0000b0c90b04 in make_packet (
   sp=0xffffa0000180bd80 [rwRW,0xffffa0000180bd80-0xffffa0000180be00]) at /usr/home/john/work/git/cheribsd/sys/netgraph/ng_pppoe.c:431
431	bcopy(*tag, (char *)dp, tlen);
(gdb) p *tag
$1 = (const struct pppoe_tag *) 0xffffa0000112ee74 [rwRW,0xffffa0000112ee74-0xffffa0000112ee78]
(gdb) p tlen
$2 = 12
```

下一步是查明这些 tag 指针的来源。`make_packet()` 中的循环在遍历存储于 `sp->neg->tags` 中的 tag 指针数组。在 ng\_pppoe.c 源文件中搜索，找到了向该数组添加 tag 指针的 `insert_tag()` 函数。接下来需要弄清该数据包的 tag 指针从何而来。向上移动到调用者的栈帧并列出周边代码，可以看到图 6 中 `pppoe_start()` 通过若干次 `insert_tag()` 调用产生了这些 tag。

图 6：pppoe\_start() 栈帧

```sh
(gdb) up
#19 0xffff0000b0c90868 in pppoe_start (
   sp=0xffffa0000180bd80 [rwRW,0xffffa0000180bd80-0xffffa0000180be00]) at /usr/home/john/work/git/cheribsd/sys/netgraph/ng_pppoe.c:1305
1305	make_packet(sp);
(gdb) l
1300	init_tags(sp);
1301	insert_tag(sp,&neg->host_uniq.hdr);
1302	insert_tag(sp,&neg->service.hdr);
1303	if (privp->max_payload.data != 0)
1304	insert_tag(sp, &privp->max_payload.hdr);
1305	make_packet(sp);
1306	/*
1307	 * 发送数据包并准备在超时后重传。
1308	 */
1309	ng_callout(&neg->handle, node, hook, PPPOE_INITIAL_TIMEOUT * hz,
```

这时我恍然大悟。在 CHERI 中，指针的边界会被收窄到逻辑分配的大小。例如，`malloc()` 返回的指针可访问整个已分配缓冲区，指向全局变量或栈变量的指针可访问整个变量。若某变量包含一个含多个字段的结构体，指向该变量的指针可访问所有字段。类似地，若某变量包含一个对象数组，指向该变量的指针可访问数组中的所有元素。使用取地址运算符（&）取某个字段或数组元素的地址时，会生成一个地址可能不同（以反映字段或数组元素的偏移）但边界和权限相同的指针。

CHERI LLVM 实现了一项名为 subobject bounds（子对象边界）的可选特性，它会改变取地址运算符（&）的行为，将所得指针的边界收窄到仅允许访问单个字段或数组元素。某些情况下这很有用。例如，若结构体中包含一个持有字符串的字符数组，在将该结构体字段的指针传给 `strcpy()` 时把边界收窄到仅该字符数组，可防止溢出蔓延到其他结构体字段。然而，也有不少现实中的 C 代码依赖取某个字段的指针后再将其转换回指向包含对象的指针。Linux 中使用的宏 `container_of()` 就是一例。BSD 上 **\<sys/queue.h>** 中的某些宏也存在类似模式。C 中另一种常见模式是通过定义一个包含基类字段的结构体，并将该结构体的实例作为第一个字段嵌入到定义子类的结构体中，来实现 C++ 风格的面向对象编程。这通常与函数指针配合使用以实现虚方法。函数指针接受指向基类结构体类型的指针，但会将其转换为子类结构体类型。FreeBSD 内核中有若干子系统采用了这种模式。由于这些常见的 C 惯用法与 subobject bounds 不兼容，CHERI LLVM 默认不启用该特性。不过，CHERI LLVM 允许通过源码注解针对特定类型或类型的特定字段禁用 subobject bounds。当字段以这种方式标注后，取地址运算符不会收窄所得指针的边界。CheriBSD 对内核启用了 subobject bounds，既是为提升安全性，也是作为评估 subobject bounds 的研究案例。某些情况下（例如使用宏 `container_of()` 时），若字段未标注禁用 subobject bounds 的注解，CHERI LLVM 能在编译时发出警告。但在其他情况下，编译器无法检测取地址运算符是否被用于生成随后将用于访问包含对象的指针。不幸的是，这些情况目前只能在运行时执行到相关代码路径时因边界错误 panic 才被发现。

回到 ng\_pppoe 模块的 panic。这里，该模块采用了上述子类模型的一种变体：`struct pppoe_tag` 类型在每个 PPPoE tag 的起始处定义公共字段。模块另外定义了 `struct datatag` 和 `struct maxptag` 两种类型，二者都将一个 `struct pppoe_tag` 对象作为第一个字段（名为 “`hdr`”）嵌入，其后是存放 tag 特定数据的附加字段。当 `pppoe_start()` 调用 `insert_tag()` 将其使用的每个 tag 插入图 6 所示的 tag 指针数组时，使用了取地址运算符取 `struct datatag` 的 `hdr` 字段指针。结果，指针的边界被缩减到 4 字节的首部结构。注意，将边界收窄到单个 `struct datatag` 是没问题的，收窄到首部才有问题。例如，对于表达式 `&neg->host_uniq.hdr`，将边界收窄到 `neg` 结构体的 `host_uniq` 成员是可以的，问题仅出在进一步收窄到 `hdr` 字段。为修复此问题，我只需添加几条源码注解，为 `hdr` 成员禁用 subobject bounds。应用该修复后，再次运行测试程序。

终于，我得以复现 bug 报告中描述的原始崩溃，图 7 所示的崩溃确实非常相似。与 bug 报告中崩溃的不同之处在于，CHERI 版本死于 CHERI 故障，原因是读取了未标记的指针。然而，若原始 bug 如报告所言是缓冲区溢出，为何 CHERI 没有更早检测到缓冲区溢出并崩溃？根据 bug 报告，溢出发生在 `chap_Input()` 函数将 CHAP challenge 读入 `chap->challenge.peer` 数组时。确实，向上回溯栈帧并查看图 8 中包含无效指针的数据结构，可以明显看到 peer 数组及其后直到结构体末尾的若干字段都被填充了 `0xff` 字节。那么 CHERI 为何没有捕获 `peer` 数组的溢出？答案仍是 subobject bounds。

回想一下，CHERI LLVM 默认不启用 subobject bounds，CheriBSD 也仅对内核启用，不对用户态启用。因此，当 `chap_Input()` 函数获取指向 `peer` 数组的指针时，该指针具有包含对象的边界，此例中即整个 `authp->physical->dl` 对象。若溢出写到了 `dl` 对象的末尾之外，CHERI 本会触发边界违例异常，但测试程序小心地将溢出控制在 `peer` 数组内，同时停留在包含对象的边界之内。

图 7：第二次 GDB 崩溃摘要

```sh
Core was generated by `ppp -nat -foreground pppoe-client'.
Program terminated with signal SIGPROT, CHERI protection violation.
Capability tag fault.
#0 strlen (
   str=0x11f [rwxRWE,0xffffffffffffffff-0xffffffffffffffff] (invalid,sealed) <error: Cannot access memory at address 0x11f>) at /usr/home/john/work/git/cheribsd/lib/libc/string/strlen.c:143
143	while (*p != '\0')
```

图 8：调查无效指针

```sh
(gdb) up
#1 0x0000000000160ac4 in chap_ChallengeInit (
   authp=0x42a74a30 [rwRW,0x42a72000-0x42a75000]) at /usr/home/john/work/git/cheribsd/usr.sbin/ppp/chap.c:493
493	len = strlen(authp->physical->dl->bundle->cfg.auth.name);
(gdb) p authp->physical->dl->bundle
$2 = (struct bundle *) 0xffffffffffffffff [rwxRWE,0xffffffffffffffff-0xffffffffffffffff] (invalid,sealed)
(gdb) p authp->physical->dl
$3 = (struct datalink *) 0x42a72000 [rwRW,0x42a72000-0x42a75000]
(gdb) p *authp->physical->dl
$4 = {desc = {type = 5,
...
local = "\000", '\245' <repeats 147 times>,
peer = '\377' <repeats 148 times>}, NTRespSent = 1,
peertries = -1,
authresponse = '\377' <repeats 48 times>}, mp = {seq = 4294967295,
bandwidth = 4294967295},
bundle = 0xffffffffffffffff [rwxRWE,0xffffffffffffffff-0xffffffffffffffff] (invalid,sealed),
next = 0xffffffffffffffff [rwxRWE,0xffffffffffffffff-0xffffffffffffffff] (invalid,sealed)}
```

确认 bug 确实是 `peer` 数组溢出后，最明显的修复方式是对 challenge 长度添加边界检查，并拒绝长度无效的 challenge。我好奇 CHAP challenge 是否有标准化上限。快速搜索网络后找到 RFC 1994。阅读对 Challenge 数据包的描述，Value 字段的长度除了 255 字节的隐式限制外没有其他限制，因为单字节 Value-Size 字段能指定的最大值就是 255。ppp.8 此前人为设定了 48 字节的过低上限，因此我采取的修复方案是将最大 challenge 长度提升到 255，以支持任何合法的 challenge。

### 第二个 Bug

Bug [271820](https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=271820) 描述了 ppp.8 守护进程 NAT 处理中的另一处缓冲区溢出。具体而言，ppp.8 的 NAT 处理包含若干针对特定网络协议的处理函数，该 bug 描述的是 NetBIOS over TCP/IP 数据报服务的处理函数中的问题。尽管名字中有 TCP/IP，该协议实际使用 UDP。该 bug 具体与 IP 分片有关：一个大的 UDP 报文可被拆分为多个 IP 数据包。这些较小的 IP 数据包（即分片）各包含原始 UDP 数据报的一个子集。NetBIOS 处理函数试图解析 UDP 负载的内容，并小心避免读出 UDP 数据包的末尾。该处理函数确实假定所检查的数据包包含完整的 UDP 负载。然而，若所检查的数据包是 IP 分片，它可能只包含 UDP 负载的一个子集。

与上一个 bug 一样，该 bug 报告也包含一个复现缓冲区溢出的测试程序。但提交者指出，在传统（非 CHERI）架构上执行时，该溢出并不能可靠地使 ppp.8 进程崩溃。在 CHERI 系统上运行测试程序时，ppp.8 在溢出发生时可靠地因边界错误崩溃，如图 9 所示。这使得验证潜在修复更为容易，因为一旦阻止溢出，ppp.8 便不再崩溃。一个值得注意的奇怪之处是，`AliasHandleName()` 的参数 `pmax` 被标记为无效。若查看 `AliasHandleUdpNbtNs()` 函数的起始处，`p` 和 `pmax` 最终都是通过在 IP 数据包起始地址上加一个偏移量计算得出的，如图 10 所示。因此，两个指针本应具有相同的元数据，如边界和权限。然而，`pmax` 的边界覆盖的范围与 `p` 不同，尽管两个范围长度相同。在 CHERI 中，边界表示为当前 capability 地址附近的一个有界范围。这使得 CHERI capability 所用的额外元数据字中能高效表示这些边界，但也意味着：若指针地址远离其原始地址到一定程度，元数据便无法再描述正确的边界。发生这种情况时，CHERI capability 会被标记为无效，这就是为何 `pmax` 虽由对有效指针加偏移计算得出，却被标记为无效的原因。`p` 相对于数据包原始起始处虽有偏移，但仍足够接近原始指针地址，因此即使 `p` 本身已越界，仍被标记为有效。

图 9：第三次 GDB 崩溃摘要

```sh
Core was generated by `ppp -nat -direct'.
Program terminated with signal SIGPROT, CHERI protection violation.
Capability bounds fault.
#0 AliasHandleName (p=0x42b52419 [rwRW,0x42b51000-0x42b52400] "",
pmax=0x42b6114f [rwRW,0x42b61000-0x42b62400] (invalid) "")
at /usr/home/john/work/git/cheribsd/sys/netinet/libalias/alias_nbt.c:268
268	while ((*p & 0x3f) != 0x00) {
(gdb) where
#0 AliasHandleName (p=0x42b52419 [rwRW,0x42b51000-0x42b52400] "",
pmax=0x42b6114f [rwRW,0x42b61000-0x42b62400] (invalid) "")
at /usr/home/john/work/git/cheribsd/sys/netinet/libalias/alias_nbt.c:268
#1 AliasHandleQuestion (count=32673, q=<optimized out>,
pmax=0x42b6114f [rwRW,0x42b61000-0x42b62400] (invalid) "",
nbtarg=0x44e8af80 [rwRW,0x44a8b000-0x44e8b000])
at /usr/home/john/work/git/cheribsd/sys/netinet/libalias/alias_nbt.c:424
#2 AliasHandleUdpNbtNS (la=<optimized out>, pip=<optimized out>,
lnk=<optimized out>, alias_address=<optimized out>,
alias_port=0x43dffe50 [rwRW,0x43dffe50-0x43dffe52],
original_address=0x43dffe58 [rwRW,0x43dffe58-0x43dffe5c],
original_port=0x42b51152 [rwRW,0x42b51000-0x42b52400])
at /usr/home/john/work/git/cheribsd/sys/netinet/libalias/alias_nbt.c:805
```

图 10：pmax 的计算

```sh
(gdb) l AliasHandleUdpNbtNS
...
776
777	/* 计算 UDP 数据包的数据长度 */
778	uh = (struct udphdr *)ip_next(pip);
779	nbtarg.uh_sum = &(uh->uh_sum);
780	nsh = (NbtNSHeader *)udp_next(uh);
781	p = (u_char *)(nsh + 1);
782	pmax = (char *)uh + ntohs(uh->uh_ulen);
783
784	if ((char *)(nsh + 1) > pmax)
785	return (-1);
```

阅读 alias\_nbt.c 源文件中各函数的代码，它们总体上表现良好：遵守只在 `p` 和 `pmax` 之间读取数据的约束，并在任何试图越过 `pmax` 的访问时中止。因此，此处的 bug 在于 `pmax` 在该情况下被设为了错误的值。修复方案是：也用 IP 首部中的长度字段计算底层 IP 数据包的末尾，并在所得指针小于 `pmax` 原值时将其作为 `pmax` 的值。应用此修复后，测试程序不再触发 ppp.8 崩溃。奇怪的是，alias\_nbt.c 中有两个函数包含相同的 `pmax` 计算：`AliasHandleUdpNbt()` 和 `AliasHandleUdpNbtNs()`。我第一次尝试修复时，误只修补了第一个函数，导致 ppp.8 继续崩溃。意识到错误后，我在最终修复中修补了两个函数。

最后，bug 报告者还指出复现程序使用了值为 0 的无效 IP 首部长度，而 libalias 代码未检测到这一点。我又写了一个后续补丁，在数据包传递给库中其他处理函数之前，添加对 IP 首部的额外验证，包括版本和长度字段。由于该 bug 未触发崩溃，我依靠 GDB 单步调试 ppp.8 进程，验证数据包在 `libAliasInLocked()` 函数中被提前拒绝。

### 结论

CHERI 旨在提升既有与未来软件的安全性。CHERI 的关键组件是 capability 硬件类型及其作为 C 和 C++ 中语言级指针实现的用途。使用 CHERI capability 的一个副作用是，调试器在运行时能获得额外的细节层次。这些细节的某些方面需要理解 CHERI 概念（如指针可表示性）才能解读。

另一点值得借鉴的是高质量 bug 报告的价值。本文考察的两份 bug 报告都提供了对底层 bug 的详尽彻底分析，以及便于他人复现问题的简单方法。这些细节大大简化了推演解决方案和验证潜在修复的过程。

***

**John Baldwin** 是一名系统软件开发者。他直接为 FreeBSD 操作系统提交改动已逾二十年，涉及内核的多个部分（包括 x86 平台支持、SMP、各种设备驱动和虚拟内存子系统）以及用户态程序。除编写代码外，John 还曾任职于 FreeBSD 核心团队和发布工程团队，并为 GDB 调试器贡献过代码。John 与妻子 Kimberly 和三个孩子 Janelle、Evan、Bella 居住在弗吉尼亚州阿什兰。


# FreeBSD 上的现代 Java

## 专访 Harald Eilertsen

### Tom Jones

2025 年 9 月，Harald Eilertsen 在 EuroBSDcon 上发表了题为 “[改进 FreeBSD 上的 OpenJDK——我们的进展与我所学到的](https://events.eurobsdcon.org/2025/speaker/LPKAQ9/)” 的演讲。Harald 一直与上游 OpenJDK 项目合作，改进对 FreeBSD 的支持。与 Harald 对谈的是 Tom Jones。Tom 是一名提交者，《FreeBSD 期刊》编辑委员会成员，也是期刊的定期撰稿人。

**Tom:**

你是怎么开始用 FreeBSD 的？

**Harald:**

我或多或少用 FreeBSD 很久了。我最早是在 2000 年代初尝试在桌面系统上用它。具体记不清了，但至少对我来说效果不太理想。当时在桌面上运行它有不少问题。

我在服务器上用它很久了。基本上我在朋友的服务器上有一个 Jail，算是有了自己的虚拟小空间，里面托管了各种 web 服务和我自己的社交网络、我自己的 fediverse 实例，还有……对，很多不同的博客、我的乐队、我的各种东西，有时也帮别人托管。

**Tom:**

你从 FreeBSD 用户转向开发的过程是怎样的？

**Harald:**

我一辈子基本上都在做开发，但之所以开始做 FreeBSD 上的 OpenJDK 这个项目，主要是因为 FreeBSD 基金会在找人来做这件事。

**Tom:**

你之前做过 Java 开发吗？

**Harald:**

我做过一些 Java 开发，但那是几十年前的事了。对我来说，JVM 比 Java 语言本身更有意思。

它更多是 C++ 加一点汇编，而不是 Java 本身。它是驱动 Java 运行的系统。我看到 Java 语言本身也有不少进展。所以过去十年里，它变得比最初现代多了。

**Tom:**

嗯，我很久没碰 Java 了，对此我很高兴。

**Harald:**

大多数人都这样。

**Tom:**

通过这项工作，你了解到人们想在 FreeBSD 上的 OpenJDK 里运行哪些东西吗？

**Harald:**

嗯，了解一些，部分是因为我认识一些在生产环境里用 FreeBSD 跑 Java 的人。

也因为从 bug 报告和其他渠道看到的情况来看，它似乎主要跟那些大型关键系统有关。

比如 Elasticsearch，比如 Tomcat，至少目前我的印象是大多是服务端的东西。

我以前工作过的一家公司用 Java 做 “卡不在场” 支付认证。认证的是卡片，不是支付。那套系统跑在 Java 上。所以看起来 Java 仍用于很多关键系统和重要系统，而这些系统在某种程度上对普通人是隐藏的。

**Tom:**

你刚开始做的时候，情况如何？

**Harald:**

当时有个 JDK 23 的 Port。那是在上游即将发布 JDK 24 之前。

我花了一些时间熟悉代码，熟悉它怎么运作，熟悉怎么跑单元测试之类的。我遇到的问题之一是很多单元测试……不是很多，但有几个相当关键的单元测试跑不过。

原因很多。所以我到目前为止做的工作，一方面是修复单元测试或测试框架，让它们在 FreeBSD 上能跑，另一方面是修复单元测试或测试标记为不工作的代码。

我觉得现在的状况是，JDK 24，还有我弄好之后即将推出的 25，是一个比以往版本更扎实的 Java 或 JDK 实现，在 FreeBSD 上跑得更好，性能也更好。

**Tom:**

你对性能提升的幅度和体现在哪些地方有概念吗？

**Harald:**

一部分是因为有用户告诉我，哦，我们在某处有性能问题。

有个相当大的 Elasticsearch 安装在内存超过 128 GB 的系统上有性能问题。看代码，那部分是 Elasticsearch 想知道已提交内存的量以及我们怎么核算它，而我们之前的做法不太理想。

一旦找到问题所在，改动相当简单。

FreeBSD 基本上有个 API 能直接给你这个数值，我们不必费劲遍历文件、做一堆计算。所以一旦改好，用户确认了，对，这能用。就是这类问题。

**Tom:**

有没有随手就能说出的百分比提升？

**Harald:**

没有，而且我觉得这也很大程度上取决于你的工作负载。我正在做的一件事，也是 [bapt@](https://reviews.freebsd.org/p/bapt/) 一直在帮忙的，就是减少对 `procfs` 文件系统的依赖。

代码从 Linux 那边来，严重依赖 `procfs` 系统，而这在 Linux 上似乎比在 FreeBSD 上快得多。

在 FreeBSD 上，我们通常有更好、更简单的方式获取这些信息。所以完全去掉它会是一种改进。

我没做过严格的性能测试，尤其是我说的这个问题，我没有那样的机器来测。我没有那种内存的机器。而且我猜，搭建一个高负载的 Elasticsearch 集群或安装要花些力气，也要花些时间去测量。

**Tom:**

我的经验确实是这样：一部分问题是大型机器不好弄。要是能联系到社区里更多的人，能说 “我把机器跑在我们的测试负载上，这是怎么做的”，那就太好了。

**Harald:**

这个 Elasticsearch 的事也是一样。我和报告问题的人联系得不错，他们非常乐意帮忙，这种情况下我基本上就说 “这看起来不太理想，试试别的办法。” 然后我把补丁发给他们，他们基本就确认了，对，这解决了问题，所以只能这样测。这种情况下是他们做的测试。我则想办法弄清该改代码里的哪里。

**Tom:**

对我来说，当我把 VPP 移植到 FreeBSD 时，`procfs` 里有很多东西只是从文件读字符串来做配置，相关的改动大多数时候都很直接，要么用 `sysctl` 接口，要么我们有原生接口。

**Tom:**

感觉我们应该有一份移植软件的指南，比如，`grep` 这个模式，然后替换掉。

**Harald:**

我想，对，这基本就是我选的方案。要么用 `sysctl`，要么有东西直接给我结果，所以快得多。我觉得 OpenJDK 至少有个问题，就是传统上，尤其是 OpenJDK 的 hotspot 代码里，源码树的 BSD 部分包含了 Mac OS，因为他们传统上把 Mac OS 看作 BSD。

虽然我们需要的代码通常更接近 Linux 代码（但不一定完全相同），而 Linux 代码在源码树的另一个子目录里，所以有时会有点混乱，因为有时你有这种 BSD 专用代码，基本分成一份、两份，有时更多，因为不同的 BSD 做事方式也不同，但 Mac OS 部分通常和这些都完全不同。

**Tom:**

上游接受补丁的情况怎么样？

**Harald:**

他们很友善，也很严格。所以把补丁推到上游有一套定义得很清楚的工作流和流程。他们不希望看到的情况之一是，一个几乎被支持的平台有一套不完整的补丁集。

他们非常支持把 BSD 纳入上游，但有一个明确的流程要做。

你得按他们的流程来，所以我们现在做的是在 github 上的 OpenJDK 组织下有了自己的仓库，我们会在里面创建一些分支来开发，并与上游保持同步。一旦到了这能工作、测试通过、一切就绪的时候，我们还需要写一份 Java Enhancement Proposal，声明我们想把 FreeBSD 或整个 BSD 或不管我们怎么做，变成上游支持的平台。

这需要获得通过，一旦我们到了那一步，基本上所有东西会一次性合并。但我已经有一些补丁，不是严格和 BSD 相关，但在过程中有助于被上游接受，他们的反应就是 “好”。他们对此一直很友善，也很支持。

这就是 Java 那句老口号：“一次编写，到处运行。” 看来他们真把它当回事。

**Tom:**

你看到尽头了吗？我们离做那次大合并、加入 FreeBSD 支持还有多近？

**Harald:**

我想我们现在正在开始做这件事。我不知道要多久，但我觉得大概六个月到一年之间能成。也许会更快，因为 Port 能用，我们基本上有了所有改动。

还有几个测试我暂时还跑不过，但我觉得一旦搞定，再加上有能在他们系统里跑的 FreeBSD CI，就行了。这是最近一直在做的，看起来有进展，所以我也说不好。

我是个程序员，我不会规划时间。

**Tom:**

一旦我们成为支持平台，是不是意味着不破坏支持的责任就落到别人头上了？

**Harald:**

对，因为那时我们就是 CI 流程的一部分，如果某个补丁导致 CI 失败，它就不会被合并。这不意味着他们非得自己修。可能是，对，他们联系我或 BSD 移植组的其他人就行。

**Tom:**

非常感谢你今天抽时间，在我们结束之前还有什么想聊的吗？

**Harald:**

我刚旅行回来，去了 EuroBSDcon，到处见了很多人，所以脑子有点空白。今年是我第一次参加 EuroBSDcon，我觉得体验不错。能见到真人很好，还有很多有趣的演讲。

我觉得，至少对我来说，作为某种程度上从外部来的人，我不算 BSD 社区的一员。能看到所有正在发生的事、人们在做什么，还有，怎么说呢，所有投入到构建系统里的手艺，对我来说很好。

**Tom:**

人们知道你做 OpenJDK 的工作后，向你提了很多功能请求吗？

**Harald:**

倒没有，看起来更多的是我得到了很多支持，比如：

“有人在做这个真好，我们在努力让 OpenJDK 变得更好、跑得更好，这真好。”

我觉得这是件好事，当然，把它推到上游让它被支持也是。

我觉得那才是更重要的部分。至少从人们的反应来看是这样。

**Tom:**

你有没有告诉人们去感谢 FreeBSD 基金会？

**Harald:**

嗯，我确实说了。这也正是关键。就像：“你为什么做这个工作？”

就是，“对，我是拿钱做的。”

但这也是我觉得有意思的项目，也觉得有挑战性。我做软件开发做了很久，在各种领域，不同系统，快 30 年了。我有很多经验。

既能引起兴趣、又大致符合我对自由软件和开源信仰的工作，并不那么容易遇到。所以能把这两者结合起来，得到一份有挑战性、我觉得——不是人人都能做的工作。

而且，工作机会其实很多。至少看可用的活儿的话，很多是 web 开发、JavaScript，还有随便什么最新框架。而我对那些没兴趣。

**Tom:**

很多——感觉像是为变而变。搞点新的、刺激的。

**Harald:**

所以这就是我的感受。

这份活儿让我能用上一些知识，也挑战我。我有很多东西要学，关于 FreeBSD，关于 OpenJDK，还有一切怎么运作。我注意到这两边都有非常博学的人。我能向他们学习。

这——我觉得很有价值。

***

**Harald Eilertsen** 是一名软件开发者和信息安全从业者。他从 1980 年代初开始编程，曾在 Tandberg、Cisco、Modirum 和 Automattic 等公司工作。目前他自雇，作为独立顾问工作。

**Tom Jones** 是一名 FreeBSD 提交者，致力于让网络栈保持快速。


# 来吧，友好的 SBOM

## 利用软件物料清单为你保驾护航

### Alice Sowerby

我本想给本文起名《我如何学会停止担忧并爱上 SBOM》（*How I learned to stop worrying and love the SBOM*），却发现这个美式俏皮话已在该话题上被广泛使用。于是我改用一个更具英伦风味的备选标题，灵感来自约翰·贝杰曼爵士（Sir John Betjeman）脍炙人口的诗作《斯劳》（[*Slough*](http://www-cdr.stanford.edu/intuition/Slough.html)），其首句为 “来吧，友好的炸弹，落在斯劳！”。该诗于 1937 年出版，痛惜一战以后伦敦附近一座英国小镇的工业化。

诗中呼唤炸弹摧毁这座小镇，因为在贝杰曼看来，它已败落不堪、无可救药。我希望能借喻一种更乐观的愿景——邀请友好的 SBOM（软件物料清单，Software Bill of Materials）落在代码之上。不是要摧毁代码——我们手头这份伟大的代码遗产有太多值得珍视和守护之处（推倒重来恐怕不可能）——而是要支持它、改进它，让它为全世界所用时更安全。

我们先从 SBOM 所处的监管与文化背景谈起。我会聊聊我们是如何走到这一步的、为什么安全愈发重要、SBOM 如何发挥作用，然后深入分享 FreeBSD 在这方面的工作。

我们故事的切入点是欧盟出台的一项软件安全法规，名为 [《网络弹性法案》（CRA）](https://eur-lex.europa.eu/eli/reg/2024/2847/oj)。听起来或许不那么激动人心，但它对软件供应商影响深远，将重塑整个软件行业（包括开源）的格局。

CRA 规定，凡是将 “含数字元素的产品” 投放欧盟市场的主体，都要对这些产品的安全负责。实际操作上，这意味着制造商必须以安全方式设计产品，留存相关证据，并在欧洲市场监督机构要求时提供这些证据。制造商还必须以尽可能安全的状态向消费者交付产品，一旦产品出现被实际利用的漏洞或安全事件，必须遵守严格的报告和补救时限。

报告要求自 2026 年 9 月 11 日起强制合规，“安全设计”（secure by design）和 “安全默认”（secure by default）要求自 2027 年 12 月 11 日起强制合规。

违规罚款数额相当惊人。最高一档罚款针对违反核心要求的情况，金额为 1500 万欧元或全球年营业总额的 2.5%，取两者中较高者。

我们来看看这件事为何现在如此重要，又为何与不起眼的开源开发者扯上了关系。

好消息是，开源项目终于有了监管层面的支持，要求下游制造商负责任地构建和维护软件。该法规强制使用 SBOM，要求制造商对其代码库中每个组件（无论自研还是来自第三方）的安全状况进行尽职调查。

[欧盟网络安全局（ENISA）](https://www.enisa.europa.eu/) 也在制定关于何谓 “安全设计” 和 “安全默认” 的指南，而市场监督机构负责评估制造商的合规情况。

如果你正感到不祥的预感，担心这最终会落到我们这些小角色头上，那还是有理由抱有希望的。多亏过去几年开源社区出色的协作，[欧盟委员会（EC）](https://commission.europa.eu/index_en) 对开源软件开发的本性及其在软件行业中的角色有了更深入的认识和理解。在 CRA 制定过程中，开源社区的若干个人和组织与 EC 持续沟通。CRA 现已包含针对开源软件的专门豁免和保护条款。甚至还有一些为开源项目创收的机会。具体而言，目前有一项 “自愿安全认证”（voluntary security attestations）框架的提案。若按当前设想落地（细节仍在制定中），该框架将允许开源项目就安全认证向制造商收费。

开源项目及其开发者不受 CRA 约束，开源基金会虽可能承担部分报告义务，但不会因违规而被罚款。

监管负担与相应的处罚——也就是大棒——主要瞄准商业制造商。

这些工作最重要的成果，或许是 EC 如今在机构层面认识到开源的价值，并投入维护其在欧洲科技创新与技术主权中的角色。随着 EC 继续完善 CRA 指南，推进诸如自愿安全认证等举措，我们可以确信，他们将开源视为一等利益相关方。

注：你可能还记得美国曾发布过类似的指令：[*2021 年 5 月 12 日第 14028 号美国行政令《改进国家网络安全》（2021）*](https://www.nist.gov/itl/executive-order-14028-improving-nations-cybersecurity)。该行政令后于 2026 年 1 月 29 日部分撤回，相关规范转为可选项。

从某种角度看，软件安全竟花了这么久才迎来这种方式的监管，着实令人意外。要弄清原因，我们不妨简短回顾一下历史（建议手边备一杯热饮？）。

20 世纪中叶，约莫我祖父在 English Electric Computers 造计算机那会儿，计算机还是个小众领域。计算机昂贵、稀缺，通常由政府、大学和大公司等可信机构运行，安全靠物理隔离和限制已知用户访问来实现。让这些系统正常运转起来、保证可靠性和性能，远比防御恶意行为者更迫在眉睫。

20 世纪后期，随着个人计算日益普及，我父亲刚入行做软件工程师时，业界主流是构建网络、开发开放且可互操作的协议，而非安全的协议，往往假设参与者是可信的。连接性和采用率被视为主要挑战。与此同时，商业软件开发也激励着不断交付新功能——客户容易评估这些功能并为之付费，而安全薄弱的代价直到出事之前都难以衡量。

到了互联网时代，我开始在软件安全公司 Sophos 工作时，普通大众开始上网。随着首批著名恶意软件攻击事件登上头条，公众对 “病毒” 的认识大幅提升。安全常常被当作一门单独的学科，通过防病毒软件、防火墙、安全设备和渗透测试等多层防护来应对，而非在软件设计之初就纳入考量。

2010 年代带来了云计算和移动计算，以及由 DevOps 支撑的持续交付模式。既有的 “外挂式” 安全做法已不适应持续部署的时代——软件一天可更新多次，并即时部署到联网环境中。靠定期渗透测试来保障安全已不再合适，DevSecOps 正是对这一挑战的回应，也是催生 DevOps 的 “左移” 原则的自然延伸。

如今，软件安全已超越代码缺陷的范畴，涵盖供应链、开源依赖、云基础设施、身份管理以及 AI 驱动的系统。多起高调事件表明，复杂生态系统中的任何环节都可能出现漏洞。

可以看到，软件安全实践随着软件构建与使用方式的变化而演进。然而它常常是滞后的话题，因为驱动软件开发的激励——功能、易用性、连接性和交付速度——往往被置于应对相关安全风险之上。再加上恶意行为者的创造力和相反的动机，许多教训只能用惨痛的方式习得。

近年来，全球各国政府日益聚焦于软件安全监管。政府所用的软件、作为关键行业基础设施的软件、嵌入消费产品的软件，其安全已成为国家安全的关键——民族国家行为者和其他恶意组织已注意到并企图利用软件无处不在所带来的机会。国家及其所代表个体的安全，都依赖于社会各层所用软件的安全。

前文提到，CRA 要求制造商采用 “安全设计” 的做法。此外，还明确要求他们采用一种称作软件物料清单（SBOM）的产物。

SBOM 是代码库中每个第三方组件的清单。清单包含元数据，如包名、许可证和版本号。该清单以机器可读文件提供，支持工具交叉检查已报告的可利用漏洞、未打补丁的包以及许可证冲突。

SBOM 出现已有一段时间，如今正逐步走向标准化和广泛采用，相关工具也在成熟。

SBOM 正成为围绕软件代码库和软件供应链管理的诸多元活动的基础。掌握软件中所有组件的关键元数据，你就能做很多事——比如将自己持有的包版本号与已知的最新包进行比对，检查代码库中是否有未打补丁的依赖。你还可以将 SBOM 与已知漏洞列表比对，从而快速判断产品是否会受其影响。

SBOM 最初也旨在支持许可证管理，也就是弄清你是否可以按预期方式使用开源组件。即便你主要是出于安全原因需要 SBOM，它在这方面依然有用。

可以想见，SBOM 可包含递归元素，让你能看透供应链的多个层级。这对理解和管理产品风险水平至关重要。

学习并掌握 SBOM 的使用，对任何开发者来说不仅是一项有市场价值的技能，也是提升安全意识、培养更安全开发实践的好途径。

对开源项目而言，提供 SBOM 并非必需，因为下游制造商可以用第三方工具扫描代码，从而生成 SBOM。

不过，这种方式并不算理想。如此生成的 SBOM 可能存在多种不准确之处，尤其因为它对构建环境一无所知。

因此，尽管开源项目并不一定要在其项目中提供 SBOM 或 SBOM 工具，但若能提供，对各方都大有裨益。对下游用户而言，这意味着可以按需生成准确的 SBOM；对项目自身而言，践行 SBOM 思维与实践将提升项目的整体安全。开源项目自行创建 SBOM 后，便能主动做些 “内务整理”，比如给依赖打补丁、修复可利用的漏洞。

### 拥有一份自己的 SBOM

看来我们在这篇文章里可以全程引用文学作品。这次的典故出自英国作家弗吉尼亚·伍尔夫的长篇随笔《一间自己的房间》（[*A Room of One’s Own*](https://en.wikisource.org/wiki/A_Room_of_One%27s_Own_\(Harcourt_1929\))）。她在书中写道：“女人如果要写小说，就必须有钱和一间自己的房间”，借此点出经济独立在为艺术创作创造条件方面所起的作用。

我们或许能注意到，开源项目同样需要资源才能创建属于自己的 SBOM——这类工作惠及众人，却难以找到商业赞助或志愿贡献者来着手。

所幸，过去几年间 FreeBSD 基金会已争取到若干投资来源，用于支持 FreeBSD 的若干 SBOM 相关项目。

#### 了解 FreeBSD 的依赖

2025-26 年间，基金会运作了名为 “海滩清理”（[*Beach Cleaning*](https://github.com/FreeBSDFoundation/alpha-omega-beach-cleaning)）的项目，由 Alpha Omega 项目（隶属 OpenSSF）赞助。该项目旨在提升我们对 FreeBSD 开源依赖及其相关风险的认识。FreeBSD 基本系统中的各组件经过人工风险分析，并制定了与上游项目协作以缓解这些风险的行动计划。

我们基于这些数据和其他组件元数据，构建了一个数据库，整理各组件的维护者和依赖信息，还包含安全审查注释以及在发现风险时降低风险的计划。创建 SBOM 所需的数据正好与数据库中收录的内容相吻合。

我们还开发了工具，可按需生成关于基本系统组件的报告。每种报告类型聚焦组件群体中某一特定信息领域，便于纵览 FreeBSD 依赖的全貌、风险分析结果以及缓解计划的当前进展。该工具还能按 GitHub 要求的标准生成 CODEOWNERS 文件。这套工具现已可长期使用，帮助维护者更轻松地持续管理 FreeBSD 依赖中的风险。

#### SBOM 基础

基金会管理的另一个项目，是德国政府 [主权技术机构（STA）](https://www.sovereign.tech/) 出资委托 FreeBSD 开发 SBOM 基础能力。STA 关注 “改进和维护支撑软件创建的基础数字技术”，以此作为通向国家数字主权的路径，确保开源方案足以替代国际供应商的商业产品。

该项目由 FreeBSD 关键利益相关方提出，旨在探索为 FreeBSD 生成 SBOM 的可选方案，提出解决方案，并在可用时间内尽可能落实。

注：该项目由 STA 委托，原定于 2025 年 4 月至 12 月执行。基金会后续追加资助，将项目延期至 2026 年 10 月底。

SBOM 解决方案的需求如下：

* SBOM 必须采用被广泛使用且可持续的格式。
* SBOM 必须基于源代码，而非编译后的二进制文件。
* 必须能够按需为正在构建的镜像生成 SBOM。
* FreeBSD 官方发布的二进制镜像必须随附 SBOM。
* 解决方案必须有完善文档，便于社区使用。

项目自 2025 年 4 月启动，首先评估了现有的 SBOM 标准。最受欢迎的两种开源方案是 [CycloneDX](https://cyclonedx.org/) 和 [SPDX](https://spdx.dev/)。我们最终选择 SPDX，理由在于其流行度、ISO 标准地位以及工具生态。SPDX 在许可证信息处理上也更细致，这对 FreeBSD 这样的开源项目很有利——尽管项目偏好 2 条款 BSD 许可证，但源代码树中包含多种不同许可证下的代码。为提升 SBOM 解决方案的实用性和兼容性，我们还探索了未来将 CycloneDX 格式作为辅助输出予以支持的方式，让终端用户可自行选择使用哪种格式。

随后，我们着手建立一套可行的方案，为 FreeBSD 基本系统创建 SBOM。

我们来看看 SBOM 里都有什么。SBOM 是一种数据文件，包含代码库中每个包的关键元数据。

SBOM 包含一份包摘要清单，内容包括：

* Name：提供包的人类可读名称。
* Version：包的版本。
* Requires、Conflicts：指定依赖关系，在 FreeBSD SBOM 中用于生成依赖树。（FreeBSD SBOM 解决方案目前不使用 Conflicts。）
* Copyright：应用程序的版权信息。
* License：应用程序的许可证，以对应的 SPDX 短标识符表示（如 BSD-4-Clause AND BSD-3-Clause）。许可证信息通过 scancode-toolkit 以及源文件或头文件中的 SPDX-License-Identifier 标签（若存在）收集。
* Source：获取源代码的位置。
* URL：应用程序主页。若无其他可用来源，则使用 man 手册页，尤其是非第三方应用程序。

有了这些数据，便可以实现一些有力的洞察与能力。

借助 Name 和 Version，可以通过与公开的通用漏洞披露（CVE）数据库交叉比对，判断代码库中是否存在已知漏洞。借助 Requires、Conflicts 信息，还可选择性地将这一检查扩展到依赖关系上。

License 字段意味着你可以核查自己是否在许可证条款范围内使用每个包。若你希望规避许可证限制更严格的组件，这就是关键信息。Copyright 字段则支持许多许可证都有的要求——保留版权声明。

URL 和 Source 字段有助于追溯包的确切来源。在可用时，我们会引用每个包的 PURL（包 URL，Package URL），因为这是一种日益被采用的格式，用于为包标识符提供可预测的 URL 结构。

所有这些信息都有助于在 FreeBSD 之上构建的任何人更深入地了解其中包含的内容，并能更彻底、更高效地管理风险、合规与安全。

下一步是为 FreeBSD 选择正确的 SBOM 构建方式。虽然现有不少优秀的工具可以检查二进制文件并生成 SBOM，但金标准是在构建时生成 SBOM。这样能提供最准确的 SBOM，因此成为我们的目标。我们必须寻找合适的工具来实现这一点。

FreeBSD 项目主要由 C 语言编写，能用于构建 SBOM 工具链的工具不如其他语言那么多。不过我们了解到 pkgconf 的新能力——这是一种可以读取和解析 pkg-config 文件（`.pc` 文件）的工具。它与 pkg-config 相关，但支持 `.pc` 文件中用于创建 SBOM 的额外标签。要让 pkgconf 担当此任，我们还需克服几个障碍。首先，pkgconf 并不在基本系统中。虽然它在 FreeBSD Ports 集合中可用，但若要在构建时于基本系统中使用，就必须将其纳入基本系统。其次，pkgconf 虽可通过 bomtool 设施输出 SPDX 2.0，但我们希望使用更新的 SPDX Lite 3.0 标准。第三，基本系统中没有任何组件具备合适的 `.pc` 文件。（少数第三方 contrib 软件包包含 `.pc` 文件，但并未集成到构建系统中。）

因此我们的任务清单是：

1. 将 pkgconf 加入基本系统；
2. 为 pkgconf 增加名为 spdxtool 的功能，输出 RDF JSON-LD 格式的 SPDX Lite 3.0.1 SBOM（并将改动提交至上游）；
3. 在基本系统中补充缺失的 `.pc` 文件（并将其提交至尚无该文件的第三方组件上游）；
4. 将 SBOM 生成集成到基本系统构建流程中。

截至撰稿时，第一项已完成，遵循标准的 vendor import 流程。第二项已在上游完成——新的 spdxtool 工具刚刚随 pkgconf 2.9.90 发布。第三项和第四项正在评审中——我们用 scancode-toolkit 自动审查代码库，提取许可证和版权信息，然后审查提取出的数据，对边缘案例手工修正或判定信息。接下来需要剔除数据中不必要的字段，并放入 `.pc` 文件中，这些文件位于 **share/sbom/pkgconfig**。我们用 Lua 编写了相应工具来完成这一工作。

目前 **lib**、**bin**、**sbin**、**usr/bin** 和 **usr/sbin** 中的软件已可提供 SBOM 信息。

等 SBOM 生成完全集成进基本系统后，便可在常规构建过程中按需创建 SBOM，比如用 `make buildworld` 命令。生成的文件预计会安装到 **/usr/share/sbom/spdx/**，扩展名为 `.spdx`（对应 2.2 版本），同时也会安装到 **/usr/share/sbom/jsonld**，扩展名为 `.jsonld`（对应 3.0.1 Lite 版本）。

#### 现代化 FreeBSD 的漏洞数据格式

另一个项目是把 FreeBSD 的 Ports 和软件包漏洞数据从自有格式迁移到行业标准格式。该项目同样由 STA 委托，与 SBOM 项目并行开展。

与许多开源项目一样，FreeBSD 习惯上将漏洞数据保存在机器可读格式中——这里用的是 VuXML。VuXML 于 2005 年引入，是一种用于描述影响软件系统安全问题的文档格式。VuXML 从未在 FreeBSD 及其衍生项目之外的生态中获得采用。

随着安全形势演进，这一状况难以为继。2021 年，开源漏洞（OSV）项目应运而生。它专门为满足开源软件社区的需求而开发，采用 OpenSSF OSV 格式提供轻量、高保真的 JSON 输出，使用 git 哈希或包管理器版本来描述漏洞。

OSV 项目旨在解决开源使用者在将 CVE 条目映射到所用包版本时所面临的难题。其解决方案由一个分布式数据库和一套通用 schema 组成。没有通用标准，这项工作既耗时又易出错——而你想确认代码库中是否存在可利用漏洞时，最不希望看到的就是这种结果。

采用业界公认的格式大有裨益，它简化了外部各方获取和利用 FreeBSD 漏洞数据的方式，也让我们能借助更丰富的既有上游工具管理数据，减少自定义开发。

以标准格式提供漏洞数据，让更广泛的安全工具和服务能无缝获取这些数据，从而加速所有 FreeBSD 及衍生版用户对威胁的检测与缓解，提升 FreeBSD 生态的安全性。

经与 FreeBSD 安全团队和更广泛的 FreeBSD 社区协商，我们考虑了采用 OSV 格式的益处。我们也评估了其他标准，但因其在满足预期需求方面存在局限而未予采用。

该项目主要聚焦于建立处理新格式数据的结构。将既有数据迁移到新格式，则需通过持续采用该格式逐步完成，这一任务由 Ports 安全团队负责。

### 全世界都是 SBOM

“全世界都是舞台”（All the world’s a stage）这句话出自莎士比亚戏剧《皆大欢喜》（As You Like It），位于一段描述人生七阶段的独白开头。或许我们可以设想，SBOM 正处于第二阶段——“接着是哼哼唧唧的学童，背着书包 / 闪烁着晨光的脸庞，像蜗牛般 / 不情愿地向学校爬去”。显然，构建 SBOM 工具生态系统的艰辛工作仍在大力推进之中，通往精通的希望还在前方。

现在正是环顾 SBOM 工具生态的好时机。让我们看看有哪些工具能帮你搭建所需的、基于 SBOM 的流程。

要理解 SBOM 相关工具的使用方式，不妨参考美国国家电信与信息管理局制定的 SBOM 工具分类法（SBOM Tool Classification Taxonomy）。

该分类法按照工具的使用方式对 SBOM 相关工具进行分类。我从备受好评的 awesome-sbom 仓库收录的精选列表中，补充了一些示例工具。

| 类别                 | 类型            | 描述                                       | 工具                                                                                               |
| ------------------ | ------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Produce（生产）        | Build（构建）     | SBOM 作为构建软件制品的一部分自动生成，并包含构建相关信息。         | Syft Microsoft SBOM Tool CycloneDX Maven Plugin spdx-sbom-generator cdxgen                       |
| Analyse（分析）        |               | 通过检查制品及任何相关源代码，分析源文件或二进制文件以生成 SBOM。      | Syft Trivy Tern cdxgen                                                                           |
| Edit（编辑）           |               | 辅助人员手工录入或编辑 SBOM 数据的工具。                  | SBOM-Manager                                                                                     |
| Consume（消费）        | View（查看）      | 能以人类可读形式（如图片、图表、表格、文本等）理解内容。用于支持决策和业务流程。 | SBOM-Manager SBOM Viewer OSS Review Toolkit Interlynk Platform —— 支持导入、管理、查询和导出 SBOM。            |
| Diff（差异）           |               | 能够比较多个 SBOM 并清晰看出差异（例如比较某软件的两个版本）。       | AIsbom CycloneDX CLI SBOMDiff                                                                    |
| Import（导入）         |               | 能够发现、检索并将 SBOM 导入系统以便进一步处理和分析。           | SBOM-Manager                                                                                     |
| Transform（转换）      | Translate（翻译） | 在保留相同信息的前提下，从一种文件类型转换为另一种文件类型。           | CycloneDX CLI SBOM2doc SBOM2dot Interlynk SBOM Move                                              |
| Merge（合并）          |               | 可将来自多个来源的 SBOM 和其他数据合并，用于分析和审计。          | CycloneDX CLI OSV-Scanner sbomasm Interlynk SBOM Assembler OSS Review Toolkit                    |
| Tool support（工具支持） |               | 通过 API、对象模型、库、传输或其他参考来源支持在其他工具中使用。       | Snyk Interlynk（GraphQL API、上传/下载 SBOM API） OSS Review Toolkit（ORT） CycloneDX Microsoft SBOM Tool |

本文就写到这里。希望你觉得了解 CRA、SBOM 以及如何利用它们为自己保驾护航是有趣且有用的。

你可以在 FreeBSD 基金会的 GitHub 仓库跟进基金会在为 FreeBSD 项目开发 SBOM 工具链方面的进展，那里有一个名为 “CRA Readiness” 的项目文件夹，其中包含每月的进展更新。

***

**Alice Sowerby** 自 2024 年中起以合约服务提供者的身份为 FreeBSD 基金会提供支持。她是一位多面手的程序经理和开源领导者，在云原生、AI/ML 和 DevOps 领域的技术岗位上有逾 15 年经验。她目前活跃于 FreeBSD 基金会、CHAOSS 和 TODO Group，专长于程序管理和战略领导。Alice 曾任 Equinix 程序总监，其他经验涵盖产品管理、UX 和开发者关系。她以协作精神和对有影响力、社区驱动倡议的投入而闻名。


# FreeBSD WiFi 开发第三部分：调试

* 原文：[Getting started with WiFi development Part 3: Debugging](https://freebsdfoundation.org/our-work/journal/browser-based-edition/improving-software-quality/getting-started-with-wifi-development-part-3-debugging/)
* 作者：**Tom Jones**

## FreeBSD WiFi 栈经得起追问。

这套 WiFi 开发入门系列里，调试这部分原本完全可以充当第一、第二、第三节，那样我们也许永远都不会去看驱动代码。

WiFi 具备一切让调试变难的特征：它是并发分布式状态机；时间、物理与随机效应会改变它的行为；它揉进了微波射频设计的黑魔法；硬件专有、没有文档，用起来还反直觉。

话虽如此，FreeBSD 的 WiFi 性能很高，过去二十年里许多人为驱动和协议栈支持出过力，大多是利用业余时间。

说到底，人们愿意做 WiFi，是因为一旦有了进展，许多调试环节会让人很有成就感。

本文介绍在 FreeBSD 上做驱动开发时调试 WiFi 的两种途径：被动抓包和系统日志。

抓包让你从外部观察系统。你可以在第三台计算机上抓包，了解你的 AP 与工作站之间发生了什么（或者没发生什么）。它能给出独立于驱动影响的事实依据，说明哪些数据真正穿过了网络。

系统日志让我们看清 FreeBSD 依据收到的包认为正在发生什么。包和事件会在许多地方记入日志，我们由此看出流量如何引起状态机变化，也能看到那些在空口上通过、一进 FreeBSD 机器就消失的流量去了哪里。

### 初步调试

先从针对常见问题的调试说起。

我想多数人调试 WiFi 时，想知道的是自己的工作站为什么连不上网络。多数情况下，连不上网的原因很容易找到，事后往往觉得是个愚蠢的错误。

如果你把 `wpa` 网络的 PSK 填错，wpa\_supplicant 会提示问题就在这里：

```sh
<3>Trying to associate with 94:83:c4:58:ed:32 (SSID='o2-enc' freq=2437 MHz)
<3>Associated with 94:83:c4:58:ed:32
<3>CTRL-EVENT-DISCONNECTED bssid=94:83:c4:58:ed:32 reason=0
<3>WPA: 4-Way Handshake failed - pre-shared key may be incorrect
<3>CTRL-EVENT-SSID-TEMP-DISABLED id=8 ssid="o2-enc" auth_failures=1 duration=10 reason=WRONG_KEY
<3>Added BSSID 94:83:c4:58:ed:32 into ignore list, ignoring for 10 seconds
<3>CTRL-EVENT-SCAN-RESULTS
```

这个例子是 wpa\_cli 工具的输出，它让你直接对接 wpa\_supplicant 的日志等内容。

输错密钥任何工具都救不了，但生成正确的条目可以用 wpa\_passphrase 完成。

```sh
$ wpa_passphrase "example network" "excellentpsk"
network={
   ssid="example network"
   #psk="excellentpsk"
   psk=27231323cedae4b8036557f8dcd5f38bfb6e0ac9a5ab894a4ee5104f7998fc93
}
```

尝试加入网络时的其他问题，可能出在距离，或者网卡收不到那些宣告网络存在的 beacon。过去只有一台设备时，这很难查清。但现在我们几乎总还有另一台 WiFi 设备，可以验证网络在这里确实能用，这就说明 FreeBSD 设备上的配置有问题。

第二类想调试的问题是性能。要么网络覆盖不到你想要的位置，要么配置有问题，导致性能比你参照同一网络中其他系统所预期的更低。

这要难查得多，能帮上忙的人也更少。你大概不知道墙里面有什么，互联网上的陌生人也猜不准到底出了什么问题。不过他们可以帮你看症状。

好在拖慢性能的不只是玄学的射频问题，有些配置和排查能帮你提高网速。想弄清究竟能看见哪些网络时，wpa\_cli 是很好的起点：

```sh
> scan_results
bssid / frequency / signal level / flags / ssid
c8:e3:06:4d:1d:83       2437    -81     [RSN-SAE-CCMP][MESH]    19c14
c8:e3:06:5d:3d:48       2437    -81     [WPA2-PSK-CCMP][ESS]    Computer Network
18:e8:b9:c2:18:1a       2412    -87     [WPA2-PSK+SAE-CCMP][ESS]        HomeWifi
a8:e3:06:5d:fd:47       2437    -81     [ESS]
20:12:34:c6:18:bd       2412    -87     [ESS]   Guest-Wifi
```

`scan_results` 命令显示 wpa\_supplicant 当前已知的网络。它通过 net80211 子系统从绑定的驱动获取这些信息。如果你想连的网络在这里看不到，那可能就是运气不好。可以的话，试着靠近 AP，再慢慢走远，直到信号消失。

如果你在追查性能问题，第一步应该看接收信号强度指示（RSSI）。信号电平太低，就说明设备离接入点不够近。信号电平以 dBm 表示，这个值总是负数。作为相对度量，越接近 1 强度越高。刚好能检测到的极弱信号约为 \~87dBm，强信号可能约为 50dBm。许多协议特性要求信号强度达到某一电平才能使用。

另一种可能限制性能的配置问题非常难调。WiFi 使用的无线电频谱分布在多个频段上。有些地方这些频段完全开放给 WiFi 设备使用，而在世界其他地区，WiFi 可能只是频谱的次级用户，部分频段因此被挡住无法使用。还有些地区必须在 5GHz 频段检测气象雷达。

各个频段与它们的可用情况合称[监管域](https://wiki.freebsd.org/WiFi/RegulatoryDomainSupport)。如果工作站的监管域与接入点不一致，你可能会发现设备只能用较旧的 WiFi 标准。

不一致可能出于许多原因。如果你用的是 ISP 提供的无线路由器，那它*应该*已经针对你所在地正确配置。但你的设备可能来自不清楚它会被卖到哪个监管域的厂商，也可能你随身带着旅行路由器。这种情况下，你应当核实工作站、接入点、邻近接入点的监管域是否一致。没错，别人的接入点确实可能配错，或者配置不同，而你网卡里的固件就据此认定自己身处世界上的另一个地区。

你可以用 ifconfig 查看工作站的监管域：

```sh
$ ifconfig wlan0
wlan0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
             options=0
             ether 74:da:38:33:c0:62
             groups: wlan
             ssid "" channel 10 (2457 MHz 11g)
             regdomain FCC country US authmode WPA1+WPA2/802.11i privacy MIXED
             deftxkey UNDEF txpower 30 bmiss 7 scanvalid 60 protmode CTS wme
             roaming MANUAL bintval 0
             parent interface: rtwn0
             media: IEEE 802.11 Wireless Ethernet autoselect (autoselect)
             status: no carrier
             nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
```

默认情况下，FreeBSD 把我基本没配置过的 `rtwn` 设备归入 `US` 监管域：

```sh
regdomain FCC country US authmode WPA1+WPA2/802.11i privacy MIXED
```

ifconfig 还能用来查看它看得见的接入点的监管域，不过输出相当难读：

```sh
$ ifconfig -v wlan0 scan results
SSID/MESH ID                      BSSID              CHAN RATE    S:N     INT CAPS
HomeWifi                          18:e8:b9:c2:18:1a    1   54M  -87:-95   100 EPS  SSID<HomeWifi> RATES<B2,B4,B11,B22,12,18,24,36> DSPARMS<1> COUNTRY<GB  1-13,20> ERP<0x0> RSN<v1 mc:AES-CCMP uc:AES-CCMP km:8021X-PSK+?> XRATES<48,72,96,108> BSSLOAD<sta count 3, chan load 56, aac 18> RRM_ENCAPS<460573d000000c> HTCAP<cap 0x1ac param 0x1b mcsset[0-15] extcap 0x0 txbf 0x0 antenna 0x0> HTINFO<ctl 1, 8,4,0,0 basicmcs[]> OVERLAP_BSS<4a0e14000a002c01c8-> EXTCAP<7f080500000200000040> WME<qosinfo 0x0 BE[aifsn 3 cwmin 4 cwmax 10 txop 0] BK[aifsn 7 cwmin 4 cwmax 10 txop 0] VO[aifsn 2 cwmin 3 cwmax 4 txop 94] VI[aifsn 2 cwmin 2 cwmax 3 txop 47]> ATH<0x7fff> VEN<dd3900156d00010100010217e5810618e8-> VEN<dd168cfdf0040000490000030209720100-> VEN<dd088cfdf00101020100>
```

扫描结果末尾 caps 字段中的 `COUNTRY` 元素显示接入点认为自己在哪里：

```sh
COUNTRY<GB  1    -13,20>
```

解决办法是用 ifconfig 指定监管域和国家：

```sh
$ ifconfig wlan0 list countries
Country codes:
DEBUG Debug        ZW Zimbabwe        YE Yemen           VN Viet Nam      
VE Venezuela       UZ Uzbekistan      UY Uruguay         US United States  
GB United Kingdom  AE United Arab Emi UA Ukraine         TR Turkey        
TN Tunisia         TT Tobago          TH Thailand        TW Taiwan        
SY Syria           CH Switzerland     SE Sweden          LK Sri Lanka      
ES Spain           ZA South Africa    SI Slovenia        SK Slovak Republic
SG Singapore       SA Saudi Arabia    RU Russia          RO Romania        
QA Quatar          PR Puerto Rico     PT Portugal        PL Poland        
PH Phillipines     PE Peru            PA Panama          PK Pakistan      
OM Oman            NO Norway          NZ New Zealand     NL Netherlands    
NP Nepal           MA Morocco         MC Monaco          MX Mexico        
MT Malta           MY Malaysia        MK Macedonia       MO Macau          
LU Luxemborg       LT Lithuania       LI Liechtenstein   LB Lebanon        
LV Latvia          KW Kuwait          K2 Korea Republic2 KR Korea Republic
KP North Korea     KZ Kazakhstan      JO Jordan          J5 Japan5        
J4 Japan4          J3 Japan3          J2 Japan2          J1 Japan1        
JP Japan           JM Jamaica         IT Italy           IL Israel        
IE Ireland         IR Iran            ID Indonesia       IN India          
IS Iceland         HU Hungary         HK Hong Kong       HN Honduras      
GT Guatemala       GR Greece          DE Germany         GE Georgia        
F2 France2         FR France          FI Finland         EE Estonia        
SV El Salvador     EG Egypt           EC Ecuador         DO Dominican Repub
DK Denmark         CZ Czech Republic  CY Cyprus          HR Croatia        
CR Costa Rica      CO Colombia        CN China           CL Chile          
CA Canada          BG Bulgaria        BN Brunei          BR Brazil        
BO Bolivia         BZ Belize          BE Belgium         BY Belarus        
BD Bangladesh      BH Bahrain         AZ Azerbaijan      AT Austria        
AU Australia       AM Armenia         AR Argentina       DZ Algeria        
AL Albania        
Regulatory domains:
XC900M          GZ901           XR9             SR9            
NONE            ROW             TAIWAN          KOREA          
APAC3           APAC2           APAC            ETSI3          
ETSI2           ETSI            JAPAN           FCC4          
FCC3            FCC             DEBUG          
$ sudo ifconfig wlan0 down
$ sudo ifconfig wlan0 country GB regdomain ETSI
```

### 抓包

想了解空口和 WiFi 接口上的流量时，抓取数据包（即 pcap）是很有用的调试步骤。FreeBSD 中的 wlan 接口能在两个层次生成 pcap：一是作为逻辑以太网设备，类似你用 em 这样的有线接口；二是在链路层，让你访问 80211 包类型。把接口配置成工作站时得到的是第一种，而监视模式的 wlan 接口会给出更多底层链路的信息。

本系列[第一部分](https://freebsdfoundation.org/wp-content/uploads/2025/07/jones_wifi.pdf)里，我们为示例硬件创建了三种不同的 VAP（虚拟接入点）：工作站、host AP 和监视模式。

监视模式 VAP 让底层硬件进入混杂模式，收到的所有包都连着链路头部一起交给 bpf。这样既有更多链路信息，也能拿到某个信道上能听到的全部包，而不只是发往本设备或广播地址的包。

在驱动里，所有包都可以带上接收时的无线电环境信息，这些信息放在名为 radiotap 头部的字段中。

radiotap 头部给出有用的元数据，说明收包时 WiFi 射频的状态，比如接收强度（RSSI）、WiFi 信道和调制方式。这些字段对理解某件事为什么能工作（更多时候是为什么不能工作）至关重要。

监视模式下，我们能拿到射频能收到的所有包。仅此一点就能带来大量线索，说明本地射频环境里正在发生什么。

#### tcpdump

用抓包调试 WiFi 时有两个工具可用，其中 tcpdump 是以最小开销获取信息的关键。tcpdump 是标准抓包工具，随 FreeBSD 基本系统一起发布。

通常 tcpdump 在 IP 层显示包信息，只做少量内省，看看包在做什么。要充分利用 WiFi 接口，我们需要让 tcpdump 包含链路层头部，具体来说是 `IEEE802_11_RADIO`。

```sh
# ifconfig wlan create wlandev rtwn0 wlanmode monitor
```

加上 WiFi 头部以后，我们不再能看到流量内容，但会得到大量描述射频环境的附加字段和 ieee80211 MAC 层的信息。

```sh
$ sudo tcpdump -i wlan0 -y IEEE802_11_RADIO
tcpdump: data link type IEEE802_11_RADIO
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wlan0, link-type IEEE802_11_RADIO (802.11 plus radiotap header), snapshot length 262144 bytes
10:27:07.724396 74143272us tsft 1.0 Mb/s 2437 MHz 11g -32dBm signal -95dBm noise Beacon (o2-enc) [1.0* 2.0* 5.5* 11.0* 9.0 18.0 36.0 54.0 Mbit] ESS CH: 6, PRIVACY
10:27:07.938361 74362602us tsft 6.0 Mb/s 2437 MHz 11g -58dBm signal -95dBm noise Clear-To-Send RA:e0:3e:44:08:34:e1 (oui Unknown)
10:27:07.953755 74375625us tsft 6.0 Mb/s 2437 MHz 11g -58dBm signal -95dBm noise Clear-To-Send RA:e0:3e:44:08:34:e1 (oui Unknown)
10:27:07.958151 74382377us tsft 11.0 Mb/s 2437 MHz 11g -68dBm signal -95dBm noise Beacon () [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 6
10:27:07.973090 74394982us tsft 6.0 Mb/s 2437 MHz 11g -58dBm signal -95dBm noise Clear-To-Send RA:e0:3e:44:08:34:e1 (oui Unknown)
10:27:08.003137 74424986us tsft 6.0 Mb/s 2437 MHz 11g -58dBm signal -95dBm noise Clear-To-Send RA:e0:3e:44:08:34:e1 (oui Unknown)
10:27:08.007497 74432483us tsft 6.0 Mb/s 2437 MHz 11g -59dBm signal -95dBm noise Clear-To-Send RA:e0:3e:44:08:34:e1 (oui Unknown)
10:27:08.033133 74454986us tsft 6.0 Mb/s 2437 MHz 11g -61dBm signal -95dBm noise Clear-To-Send RA:e0:3e:44:08:34:e1 (oui Unknown)
10:27:08.037498 74462485us tsft 6.0 Mb/s 2437 MHz 11g -59dBm signal -95dBm noise Clear-To-Send RA:e0:3e:44:08:34:e1 (oui Unknown)
10:27:08.063204 74486734us tsft 11.0 Mb/s 2437 MHz 11g -68dBm signal -95dBm noise Data IV:d43 Pad 20 KeyID 1
10:27:08.063206 74486734us tsft 11.0 Mb/s 2437 MHz 11g -68dBm signal -95dBm noise Data IV:d44 Pad 20 KeyID 1
10:27:08.063211 74486734us tsft 11.0 Mb/s 2437 MHz 11g -68dBm signal -95dBm noise Data IV:d45 Pad 20 KeyID 1
10:27:08.063211 74486734us tsft 11.0 Mb/s 2437 MHz 11g -68dBm signal -95dBm noise Data IV:d46 Pad 20 KeyID 1
```

运行上面的命令时，你很可能会被本地工作站发来的管理（mgmt）流量淹没。要继续下去，得回想一下前几篇文章讨论过的 WiFi 会话建立过程。

在[上一篇文章](https://freebsdfoundation.org/our-work/journal/browser-based-edition/embedded-2/freebsd-wifi-development-part-2-working-on-a-driver/)里，我们谈到工作站如何加入网络，概括起来就是：

* 探测网络（probe request、response）
* 认证（auth request、response）
* 关联（assoc request、response）

每个阶段都是两种不同的 mgmt 帧：请求和响应。不过淹没你的多半根本不是这些帧！

为了加快入网过程，并给客户端一份可加入网络的清单，所有接入点都会不停地 “beacon” 自己的存在。在非常繁忙的环境里，beacon 可能耗掉大量可用空口时间，那份长长的可用 WiFi 网络列表其实不是什么好事。

beacon 帧携带大量描述网络和自身能力的信息，我们可以在 tcpdump 抓包中过滤出只显示 beacon 帧，像这样：

```sh
10:27:07.724396 74143272us tsft 1.0 Mb/s 2437 MHz 11g -32dBm signal -95dBm noise Beacon (o2-enc) [1.0* 2.0* 5.5* 11.0* 9.0 18.0 36.0 54.0 Mbit] ESS CH: 6, PRIVACY
```

beacon 里的信息远多于 tcpdump 单独能显示的内容，要拿到 beacon 承载的更多数据，得改用 wireshark，本例中则是它的命令行界面 tshark。

首先用 tcpdump 导出 pcap：

```sh
$ sudo tcpdump -i wlan0 -y IEEE802_11_RADIO -w testcaputre.pcap -c 1000
```

把抓到的文件交给 tshark，让它显示之前看到的测试网络的第一个 beacon 包：

```sh
$ tshark -V -r testcaputre.pcap -a "packets: 1" 'wlan.ssid == "o2-enc"'
```

对于这个 393 字节的 beacon 帧，它会输出近 900 行包描述，下面是一小段，显示 beacon 帧和部分射频信息。

```sh
802.11 radio information
      PHY type: 802.11b (HR/DSSS) (4)
      Short preamble: False
      Data rate: 11.0 Mb/s
      Channel: 6
      Frequency: 2437MHz
      Signal strength (dBm): -66 dBm
      Noise level (dBm): -95 dBm
      Signal/noise ratio (dB): 29 dB
      TSF timestamp: 1853402918
      [Duration: 461µs]
             [Preamble: 192µs]
             [IFS: 15572µs]
             [Start: 1853402457µs]
             [End: 1853402918µs]
IEEE 802.11 Beacon frame, Flags: ........
      Type/Subtype: Beacon frame (0x0008)
      Frame Control Field: 0x8000
             .... ..00 = Version: 0
             .... 00.. = Type: Management frame (0)
             1000 .... = Subtype: 8
             Flags: 0x00
                    .... ..00 = DS status: Not leaving DS or network is operating in AD-HOC mode (To DS: 0 From DS: 0) (0x0)
                    .... .0.. = More Fragments: This is the last fragment
                    .... 0... = Retry: Frame is not being retransmitted
                    ...0 .... = PWR MGT: STA will stay up
                    ..0. .... = More Data: No data buffered
                    .0.. .... = Protected flag: Data is not protected
                    0... .... = +HTC/Order flag: Not strictly ordered
      .000 0000 0000 0000 = Duration: 0 microseconds
      Receiver address: Broadcast (ff:ff:ff:ff:ff:ff)
             .... ..1. .... .... .... .... = LG bit: Locally administered address (this is NOT the factory default)
             .... ...1 .... .... .... .... = IG bit: Group address (multicast/broadcast)
      Destination address: Broadcast (ff:ff:ff:ff:ff:ff)
             .... ..1. .... .... .... .... = LG bit: Locally administered address (this is NOT the factory default)
             .... ...1 .... .... .... .... = IG bit: Group address (multicast/broadcast)
      Transmitter address: GLTechnologi_58:ed:32 (94:83:c4:58:ed:32)
             .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
             .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
      Source address: GLTechnologi_58:ed:32 (94:83:c4:58:ed:32)
             .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
             .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
      BSS Id: GLTechnologi_58:ed:32 (94:83:c4:58:ed:32)
             .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
             .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
      .... .... .... 0000 = Fragment number: 0
      1010 0010 1011 .... = Sequence number: 2603
      [WLAN Flags: ........]
```

### 系统日志

调试 WiFi 问题的第三个信息来源是系统工具和日志。多数时候系统刻意保持安静，它打印的消息通常只在出现错误这类异常情况时才会出现。开发者喜欢内置实时调试的选项，我们可以提高调试级别，让内核消息缓冲区装满 WiFi 栈的大量有用信息。

#### wlandebug

```sh
/usr/sbin/wlandebug
```

FreeBSD 自带 **wlandebug(8)** 工具，它提供了更友好的界面，用来配置 net80211 栈输出信息的详细程度。

```sh
# wlandebug -i wlan1 scan+auth+assoc
```

消息直接打印到内核缓冲区，想实时跟踪事件时会觉得很烦。这些新打印的消息可以用 dmesg 显示。

```sh
$ dmesg
wlan0: ieee80211_scanreq: vap 0xfffff80139506000 iv_state 0x1 (SCAN) flags 0x20052 duration 0x7fffffff mindwell 0 maxdwell 0 nssid 1
wlan0: ieee80211_check_scan: active scan, append, nojoin, once
wlan0: ieee80211_swscan_start_scan_locked: active scan, duration 2147483647 mindwell 0 maxdwell 0, desired mode auto, append, nojoin, once
wlan0: scan set
1g, 6g, 11g, 7g, 2g, 3g, 4g, 5g, 8g, 9g, 10g                              
wlan0:  dwell min 20ms max 200ms                                          
wlan0: scan_curchan_task: loop start; scandone=0, scanstop=0, ss_iflags=0x2, ss_next=0, ss_last=11
wlan0: scan_curchan_task: chan   6n ->   1g [active, dwell min 20ms max 200ms]
wlan0: scan_curchan: calling; maxdwell=200                                
wlan0: scan_curchan_task: waiting                
[22:e8:29:c7:48:1a] new probe_resp on chan 1 (bss chan 1) "Guest-Wifi" rssi 15
[22:e8:29:c7:48:1a] caps 0x1421 bintval 100 erp 0x100 country [GB  1-13,20]
wlan0: ieee80211_swscan_add_scan: chan   1g min dwell met (2167162969 > 18446744071581747289)
wlan0: scan_mindwell: called
wlan0: scan_curchan_task: loop start; scandone=0, scanstop=0, ss_iflags=0x21, ss_next=1, ss_last=11
wlan0: scan_curchan_task: chan   1g ->   6g [active, dwell min 20ms max 200ms]
wlan0: scan_curchan: calling; maxdwell=200
wlan0: scan_curchan_task: waiting        
[c8:e3:06:5d:fd:45] new probe_resp on chan 6 (bss chan 6) 0x48c3a47474652c2048c3a47474652c204661687261646b65747465 rssi 29
[c8:e3:06:5d:fd:45] caps 0x1431 bintval 100 erp 0x100 country [GBI 1-13,20]
[c8:e3:06:5d:fd:45] new probe_resp on chan 6 (bss chan 6) 0x48c3a47474652c2048c3a47474652c204661687261646b65747465 rssi 29
[c8:e3:06:5d:fd:45] caps 0x1431 bintval 100 erp 0x100 country [GBI 1-13,20]
[c8:e3:06:5d:fd:45] new probe_resp on chan 6 (bss chan 6) 0x48c3a47474652c2048c3a47474652c204661687261646b65747465 rssi 29
[c8:e3:06:5d:fd:45] caps 0x1431 bintval 100 erp 0x100 country [GBI 1-13,20]
wlan0: ieee80211_swscan_add_scan: chan   6g min dwell met (2167162992 > 18446744071581747312)
wlan0: scan_mindwell: called
[c8:e3:06:5d:fd:45] new probe_resp on chan 6 (bss chan 6) 0x48c3a47474652c2048c3a47474652c204661687261646b65747465
wlan0: scan_curchan_task: loop start; scandone=0, scanstop=0, ss_iflags=0x21, ss_next=2, ss_last=11 rssi 29
wlan0: scan_curchan_task: chan   6g ->  11g [active, dwell min 20ms max 200ms]
[c8:e3:06:5d:fd:45] caps 0x1431 bintval 100 erp 0x100 country [GBI 1-13,20]
wlan0: scan_curchan: calling; maxdwell=200
wlan0: scan_curchan_task: waiting
wlan0: scan_curchan_task: loop start; scandone=0, scanstop=0, ss_iflags=0x20, ss_next=3, ss_last=11
wlan0: scan_curchan_task: chan  11g ->   7g [active, dwell min 20ms max 200ms]
wlan0: scan_curchan: calling; maxdwell=200                                
wlan0: scan_curchan_task: waiting                                        
```

这些消息由每个设备的 sysctl 掩码控制。我们可以手动设置，但 wlandebug 提供了更顺手的界面。配置好之后，系统日志里会出现额外的日志消息，可以用 dmesg 命令取出。

每条打印出来的消息都出自 `IEEE80211_DPRINTF` 内核宏。

示例中最后那些消息来自 `scan_curchan`，在源码里它们长这样：

```sh
IEEE80211_DPRINTF(vap, IEEE80211_MSG_SCAN,
      "%s: calling; maxdwell=%lu\n",
      __func__,
      maxdwell);

IEEE80211_DPRINTF(ss->ss_vap, IEEE80211_MSG_SCAN,
      "%s: loop start; scandone=%d, scanstop=%d, ss_iflags=0x%x, ss_next=%u, ss_last=%u\n",
      __func__,
      scandone,
      scanstop,
      (uint32_t) ss_priv->ss_iflags,
      (uint32_t) ss->ss_next,
      (uint32_t) ss->ss_last);
```

借助 wlandebug，你可以用上内核里内建的调试语句，既不必重新构建内核，也不会一直被输出消息淹没。

#### wlanstat

它通过 `SIOCG80211STATS` ioctl 收集统计信息。在 net80211 内部，这些数据存放在 vap 的 `iv_stats` 成员中，该成员是 `ieee80211_stats` 的实例，这个约 150 个成员的结构体记录 wlan 栈里发生过什么。

好在 wlanstat 打印时很聪明，只显示含有相关信息的统计：

```sh
$ wlanstat                                                  
1821              rx from wrong bssid
2                 rx discard mgt frames
2127              rx beacon frames
17629             rx element unknown
53                rx frame chan mismatch
5                 active scans started
18                ccmp crypto done in s/w
2221              rx management frames
2                 rx action frames
59                A-MSDU frames received
3                 A-MPDU frames discarded for out of range seqno
5034              total data frames received
5016              unicast data frames received
18                multicast data frames received
6044              total data frames transmit
6044              unicast data frames sent
58.5M             current transmit rate
31.5              current rssi
-95               current noise floor (dBm)
-63.5             current signal (dBm)
```

从 wlanstat 我们能得到一些历史信息和当前信息。输出末尾的当前信息显示信号强度、本底噪声和当前传输速率。它们会随你的移动和最近发送流量的多少而变化。

历史信息告诉我们见过哪些类型的帧。调试时一项有用的统计是收到的 `A-MSDU` 帧数量。这个数字偏低，往往是无法达到更高吞吐率的重要线索。

#### ifconfig

ifconfig 能告诉你 wlan 接口配置的大量信息。

```sh
$ ifconfig -v wlan0
wlan0: flags=8802<BROADCAST,SIMPLEX,MULTICAST> metric 0 mtu 1500
      options=0
      ether 74:da:38:33:c0:62
      groups: wlan
      ssid "" channel 1 (2412 MHz 11b) bssid 00:00:00:00:00:00
      regdomain FCC country US anywhere -ecm authmode OPEN -wps -tsn
      privacy OFF deftxkey UNDEF
      powersavemode OFF powersavesleep 100 txpower 30 txpowmax 50.0 -dotd
      rtsthreshold 2346 fragthreshold 2346 bmiss 7
      11b     ucast NONE    mgmt  1 Mb/s mcast  1 Mb/s maxretry 6
      11g     ucast NONE    mgmt  1 Mb/s mcast  1 Mb/s maxretry 6
      11ng    ucast NONE    mgmt  1 Mb/s mcast  1 Mb/s maxretry 6
      scanvalid 60 -bgscan bgscanintvl 300 bgscanidle 250
      roam:11b     rssi    7dBm rate  1 Mb/s
      roam:11g     rssi    7dBm rate  5 Mb/s
      roam:11ng    rssi    7dBm  MCS  1    
      -pureg protmode CTS ht20 htcompat ampdu ampdulimit 64k
      ampdudensity 16 amsdu shortgi htprotmode RTSCTS -puren -smps -rifs
      -stbc -ldpc -uapsd -vht wme -burst -dwds roaming AUTO bintval 0
      AC_BE cwmin  0 cwmax  0 aifs  0 txopLimit   0 -acm ack
              cwmin  0 cwmax  0 aifs  0 txopLimit   0 -acm
      AC_BK cwmin  0 cwmax  0 aifs  0 txopLimit   0 -acm ack
              cwmin  0 cwmax  0 aifs  0 txopLimit   0 -acm
      AC_VI cwmin  0 cwmax  0 aifs  0 txopLimit   0 -acm ack
              cwmin  0 cwmax  0 aifs  0 txopLimit   0 -acm
      AC_VO cwmin  0 cwmax  0 aifs  0 txopLimit   0 -acm ack
              cwmin  0 cwmax  0 aifs  0 txopLimit   0 -acm
      parent interface: rtwn0
      media: IEEE 802.11 Wireless Ethernet autoselect (autoselect)
      status: no carrier
      nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
      drivername: wlan0
```

ifconfig 的详细输出给出大量实用信息，说明 wlan 接口如何配置、连在哪个网络与信道（前提是它连着网络；本例中什么都没连，所以 ssid 是空的）。

ifconfig 输出告诉我们可调的参数，例如发射功率、国家和监管域，也包含从所连网络获取的参数。`AC_BE`、`AC_BK`、`AC_VI`、`AC_VO` 描述网络在管理帧中通告的接入类别参数。

### 运用工具

FreeBSD WiFi 栈能提供大量信息帮你调试问题，但我认为对多数人来说，现实是你很快就会淹没在技术细节里。多数情况下，从较高层的工具入手、再逐层向下，能更快得到有价值的结果。如果连不上网络，运行 wpa\_cli 并留意它报告的消息，会让你处于最有利的位置。只要底层设备配置正确、接入点在范围内，你的问题多半能靠 wpa\_supplicant 的建议解决。

如果你怀疑问题出在 net80211 层或设备驱动，下一步就该双管齐下：用抓包验证空口上的流量，并核实 net80211 的行为。这需要更多对 WiFi 工作原理的理解，但这里往往能找到问题所在的好线索。

人很容易自己吓自己，把某个字段看成并不存在的错误。从本文和整个系列里，我最想让你记住的一点是：FreeBSD WiFi 栈多么经得起追问。想调试某个问题，联系邮件列表是很好的一步。对方可能会请你提供调试信息，比如本文讨论的某些内容。

这没什么可怕的。启用大多数可用日志机制都很简单，而且只要重启，它们全都会恢复为关闭。

WiFi 能达到的速度和性能令人难以置信，从 1Mbit/s 的链路一路发展到超过千兆，而且多数时候结果相当稳定。我们的 WiFi 栈一直在进步，随着用户和测试增多，它还会更好。

***

**Tom Jones** 是 FreeBSD 提交者，关注如何让网络栈保持高速。


# 打包、CloudInit，以及重温“好的、坏的和丑陋的”

* 原文：[Packaging, CloudInit, and Revisiting “The Good, The Bad, and The Ugly”](https://freebsdfoundation.org/our-work/journal/browser-based-edition/improving-software-quality/packaging-cloudinit-and-revisiting-the-good-the-bad-and-the-ugly/)
* 作者：**Tom Jones**

## 专访 Baptiste Daroussin

**Tom Jones：** 你从 1998 年起就在用 FreeBSD，2010 年成为提交者。跟你当年在桌面上开始用它的时候相比，这个项目有哪些变化？

**Baptiste Daroussin：** 我觉得变化不大，不过项目里有很多人拧着我的胳膊，把我拉成了提交者。1998 年我从一台笔记本开始。我试的第一件事是从 Ports 构建 OpenOffice。我记不清当时它已经进了 Ports，还是很快就要进去。有人跟我说：“嘿，你要是疯到想在 FreeBSD 上把 OpenOffice 跑起来，就该加入这个项目。”十年后我确实加入了。

2010 年，我记不清是谁邀请我去 BSDCan 做演讲。那时我已经在做 pkgng。我必须把这项工作在 BSDCan 上讲出来，就这样直接参与到项目里。

**TJ：** 那么 pkg 最初进入项目时，叫 pkgng？

**BAPT：** 对，我最初管它叫 pkgng。我抱怨当时没有任何东西能直接当作二进制软件包使用，而我的一位朋友（Emile Heitor <imil@netbsd.org>）正在为 NetBSD 做 pkgin。我先把他那个工具移植到 FreeBSD，能用。很快我就撞上了 pkgin 之下那些软件包工具的限制。

于是我和 Julien Laffaye（他当时已经是提交者）开始做自己的项目。最初只是我俩私下试试，看从零做一套东西到底有多复杂。与此同时，我们不断给基本系统里的包安装工具打补丁，推动改进。我很快发现，这件事并没有多难。最后我做出了一套能用的东西，发邮件告诉他们：“嘿，它跑起来了。”他们因此邀请我去加拿大讲讲我的成果，我就加入了项目。

**TJ：** 那时你想到 15 年后自己还在做 pkg 吗？

**BAPT：** 显然没想到。pkg 的搭档 Poudriere 从最开始就在，因为我需要点东西来快速测试 pkg、快速构建软件包，好验证 pkg 本身。现在它由 Bryan Drewery 照看。我很高兴自己不用同时管这两样，但我还在做 pkg，它还是挺有意思的。事情多得做不完。我当初设想的，到现在连一半都没做完。

**TJ：** 也就是说，还有很多活要干？

**BAPT：** 是的。回头看历史，大约十年前，我在 BSDCan 2015 上讲过如何让 PkgBase 成为现实。而我们直到现在才真正动手。

我有很多针对软件包的安全计划正在慢慢落地，比如默认禁止任何随机的 root 脚本运行，这样一切都能被审查。现在所有脚本都是某个随机 Port 里的某个人放进去的，显然是“极其小心”（才怪）。这是我非常讨厌的一点。所以我们现在有了可以在同一个地方审查的触发器，也有了沙箱化的脚本。所有跑在 Lua 里的东西都在 Capsicum 之下沙箱化，这同样限制了你能做的事。我的长期目标是有一天让软件包里的任何脚本都无法以 root 身份运行。我不确定自己能否做到，但仍在朝这个方向努力。

**TJ：** 你觉得让更多人参与软件包开发的障碍是什么？

**BAPT：** 不知道。大概多少跟我有关。我们有八个人有合并权限，但没人愿意动手。这是高度复杂的软件，尽管乍看之下很简单：不过是在文件系统上解包 tar 而已，能出什么错？你不必把它做复杂。你也不必做完整套，只是一点小任务而已。

可它随后就变得非常复杂：如果你想支持升级，又想加入人们想要的灵活性，最终就会得到一个复杂的求解器；说实话，连我自己都看不懂它。我们为此用了 SAT 求解。那是 Vsevolod Stakhov（他也是 FreeBSD 提交者）在编程之夏期间做的。他大概至今比我更懂它，只是他已不再活跃于这个领域。

另一个问题是，这类软件需要清晰、长远的视角，而我极不擅长写路线图。有人想加入时，会担心自己改动的方向跟我心里的设想不符，这种担心会挡住他们贡献的脚步。不过我们的贡献者仍比其他子系统多，所以归根结底，我大概还没那么糟。

**TJ：** 你提到 PkgBase 是十年前的目标。你觉得软件包工具链是主要障碍，还是说走向真正的软件包另有其他因素？

**BAPT：** 我认为是其他因素。现在回头看，pkg 的待办清单确实很长，但当时没人有动力去做。这是个先有鸡还是先有蛋的问题。没人愿意去做 PkgBase 系统本身。项目里有过一阵子想做的意愿，但没人愿意投入精力把它做完。

真正的动力出现在一年半以前。是我投入精力做出了最初的软件包和最初的框架，结果什么也没发生。后来 OCI 那批人贡献了一点动力，他们非常想在 FreeBSD 基础上做极小的 OCI 镜像，于是又推了一把。最近，Ed Maste、Kyle Evans 和 Mark Johnston 做了很多推动，再往后 Lexi 也做了大量工作。

我老是忘掉这件事：我在源码树里搭起构建软件包的最初框架之后，manu@ 做了海量工作，因为那时他有动力。起初他为此拿到了报酬，这对起步很有帮助。他有动力去构建一些需要 PkgBase 的设备。他现在的公司也想从另一套基本系统打包方案迁移到这套。

基本系统打包之所以复杂，是因为我们得让整个项目走上同一条路线。过去还有过更多尝试。在 pkg 之前，我记得有家公司在做基本系统打包。他们在某处做过演讲，用的还是旧的包安装工具。后来我在 2015 年开始谈这件事时，一家俄罗斯公司把这家公司的成果接过去，改成了当时的 pkgng。接着 iX Systems 提出了一种机制，严格来说并不是真正的软件包，而是把现有的集合变成包——也就是 FreeBSD 基本系统、FreeBSD 内核源码等等。粒度细了一些，但也细不到哪去。

**TJ：** 你觉得这里有没有给 FreeBSD 的项目教训——我们该如何对待这类牵动全系统的大改动？

**BAPT：** 我想过这个问题，但问题在于我们大多是志愿者。如果某家公司不是因为有巨大需求而从头到尾推动这项工作，并给参与者足够的时间，那它就行不通。我们就是这样一个项目。iX 是这样，那家俄罗斯公司也一样。他们做出了对自己可用的东西，却始终没有足够时间完成它。

我理解这一点；我不是要怪他们，他们在工作时间里必须转去做别的事。所以，花时间把所有关联和流程都妥当整合，实在太难。通常只有一个英雄能把它推到终点。我很高兴 PkgBase 没有落到那个下场。

**TJ：** 软件就是这样。

**BAPT：** 是啊，是啊。

**TJ：** 有很多东西对公司的出货产品来说够好了，对 FreeBSD 来说却不够好。

**BAPT：** 一点没错。

**TJ：** 有时感觉我们的标准高到代码进不来。

**BAPT：** 我不确定标准是不是太高。人们总这么说，但我看过一些开源项目，它们往往比我们更难贡献。比如跟 Linux 内核或其他操作系统打交道，代码往往更难进去。做那些操作系统的公司多得多。可你看 Linux，很多公司什么都没往上游提，因为门槛太高，但最终贡献量依然很大。

门槛本身没问题；问题是，你怎么说服人们去做超出他们最初设想范围的事情？比如：是的，这对服务器有好处，但想想桌面用户会怎样……“我不在乎桌面用户。我该为此付员工工资吗？”这就是我们面临的问题。我待过的每家公司，无论是哪个操作系统、哪个开源项目，都有这个毛病。

问题始终一样：就我与那些贡献过代码的公司打交道的经验来说，只要你在项目里认识能帮你的人，把东西送进 FreeBSD 比其他领域更容易。问题在于人太少，想找到能带你的人并不容易。

**TJ：** 项目成员有没有办法让自己对企业更方便接触？

**BAPT：** 对公司来说这是个热门话题，因为基本上到处是 Linux，人们期望从它那里获得所有技术，所以我不知道你能怎么改变。如果谈的是贡献，那该改变的不是项目成员是否更容易被企业接触。当然，那样也有用，但更应该改变的是公司的文化。

就像我说的，我合作过的每家公司，要么有一个英雄式的人物把一切都往上游送，要么就是坐拥一大堆补丁，也许某天会贡献出去。我现在在 OVHcloud 做的就是这件事：确保哪怕一开始看起来更难，也先跟社区一起做，而不是堆自家补丁。一旦习惯了、在社区里参与得足够深，事情就会变得更容易、更简单。另一个好处是，升级不再是一场噩梦——不必再变基并重新测试你攒下的所有补丁。

在我之前待过的一家公司，因为我们跟某个开源项目（不是 FreeBSD）建立了关系，得以进入许多补丁的评审路径，或者至少能及早得到通知，从而对它们发表意见。虽然团队依然很小，但正因为如此，每次大版本升级都能基本无缝，心里踏实。

这很容易，只因为我们身处社区；反过来说，我们也因此不得不做很多事。现在新版本发布前，我们得把所有补丁变基。而它偏偏在你忙着路线图里某个重要项目时到来，你根本没空理会。于是你往后拖，半年后就得迈一大步。所以可以说项目变得更容易接触了，但这取决于对面那批人的心态。

**TJ：** 你提到了 OVH，这正好可以引出 FreeBSD 15 里另一个工具：你的 CloudInit 工具 NuageInit。

**BAPT：** 对，它在 14 里就有了，从 14.3 开始比较好用。我加入 OVHcloud 时，那里没有可用的 FreeBSD 镜像，这让我很难受，因为以前是有的，而我自己就是客户。据我所知，过去公司里总有个 FreeBSD 的人来处理这件事，但除了有客户提出需求，我们实在没时间维护这些镜像。

我发现 FreeBSD 提供的镜像几乎能在裸机上开箱即用，靠的是“自带镜像”功能——基本上你只要把 raw 镜像写到裸机磁盘上，然后启动。我还发现他们为裸机的“自带镜像”做的一件事就是使用 CloudInit。

他们实现 CloudInit 的方式，不是虚拟机或云环境里那种精致的网络手段。他们用的是一个机制：给机器挂一块磁盘，启动时磁盘里就带着数据。所以本质上就是把 CloudInit 配置放在磁盘上。看到这个之后，我想到：如果你想开箱即用地安装 FreeBSD，而不借助 mfsBSD 之类需要 qemu 并覆盖磁盘的手法，那就得有一个能用的 CloudInit 镜像。

我必须有一个能用的 CloudInit 镜像。我看过官方的 CloudInit，完全不喜欢，因为它太庞大了。它对 FreeBSD 的支持很脆弱，我不怪他们——要把 FreeBSD 专有的东西塞进去得花很多功夫。它太大，因为它需要 Python 和相关工具，而基本系统里没有 Python。我想要能在基本系统里运行的东西。做出一个足以引导机器的实现，应该不难。NuageInit 就是这么写的，我只实现了我们裸机使用所需的最低限度。

后来我发现有客户希望在我们的公有云上使用原生镜像。我看了一下那是什么：它是一个 OpenStack。所以它也遵循 CloudInit，只是默认从网络而不是从磁盘获取配置。它也能从磁盘工作，但用户得自己找到那个选项，而且默认不开。我给 NuageInit 加上了从网络获取配置的能力，于是我们提供了这个镜像。我想我们从六月起就在公有云上提供原生镜像了。

接着 VPS 团队来找我，希望向 VPS 客户提供 FreeBSD。他们自己做完了工作，做了大量测试，还提供了很多 bug 修复（由我提交入库）。

看来大家还挺喜欢 NuageInit，因为现在有很多人给它加功能。FreeBSD 15.0 里的版本，我不会说它完整——能加的东西还有很多——但已经足够完善，几乎到处都能用。任何基于 OpenStack 的环境它都能跑。

**TJ：** 你在 2018 年的 EuroBSDCon 上讲过“[FreeBSD 项目：好的、坏的和丑陋的](https://papers.freebsd.org/2018/bsdcan/daroussin-introducing_freebsd_in_new_environment/)”。对照其中的“坏”和“丑陋”，你觉得我们现在做得怎么样？

**BAPT：** 我觉得我们在变好。网络和 Wi-Fi 曾经很难啃。即便我不同意所采取的方法，也得承认它现在运行良好，而且我正在用它。你做了另一条路线的方案，它也同样能用。这又把我们带回了曾经的位置，好坏都算上。同一个问题我们有两套方案，就像我们有三个防火墙一样。这大概也是我们喜欢 FreeBSD 的部分原因。你知道，这让它显得有人情味：大家口味不同。有些地方我们无法就方案达成一致。那好，我们就有两套方案，而且有人愿意维护。没关系，这让它像个家庭。

我觉得蓝牙依然头疼。能用，但还是头疼。回头看我当年列为“丑陋”的清单：默认 CSH，这个我处理了。我有 CSH，但我处理了。现在默认是 sh。防火墙和邮件，我想我们搞定了。Vi 后来升级了。所以还算可以。

PXE 启动仍然是个问题，尽管我们改善了很多。我记得当时在 Gandi 的工作场景。我们需要部署一些开发机。至于存储设备，我们把所有 Linux 机器都用 PXE 启动，而 FreeBSD 在这件事上很痛苦。我们没法用 iPXE，只能绕道 MFS 镜像之类的东西，非常麻烦；而在 Linux 上就很容易。好在十年之后，Warner 在几个月前终于修好了：内核支持 iPXE Memdisk，于是我们可以直接从 iPXE 启动一个裸 FreeBSD 镜像。我在 OVH 也这么用，因为你可以提供自己的 iPXE 脚本来启动裸机。这很棒。我们花了十年才修好，但没关系。

说到“坏”的一面，我提过缺少开箱即用的云镜像。这个问题已经解决，现在我们有了。我们在本地 FreeBSD 更新镜像方面仍有问题，这大概能靠转向 PkgBase 解决，最新的基础设施讨论帖里也谈到了。我还不知道发布工程团队会怎么做，但一切都会符合 CDN 规范。只要符合 CDN 规范，我们就能提供一套在本地做镜像的机制。对拥有大量 FreeBSD 的公司来说，每次更新都能从自家镜像获取，是件令人高兴的事。

我们没有 bhyve 前端。今年有一些 GSoC 代码在为 QEMU 添加 bhyve 后端。所以，一个与 QEMU 对齐的前端对 bhyve 会很有用。我还提过 bhyve 缺失的另一项功能：实时迁移。我们现在有了直通，但仍没有实时迁移。而这正是 bhyve 在企业里的主要问题。如果你是提供虚拟机的云公司，这事很关键，因为它把硬件问题抽象掉了。你需要在客户察觉不到的情况下迁移机器。所以，没有它，笔记本上无所谓，在企业里就没法用。

我还提过缺少动态 CPU，这一点我们仍然没有。这有点意思，因为我觉得当年那个用例放到今天已经不太成立了。不过当时在 Gandi，我们提供一些云服务，可以在不重启虚拟机的情况下动态增加 CPU 核心。这在 Linux 上可行，在 FreeBSD 上不行，必须重启。但我不确定今天人们还这样使用虚拟机。

NFS 被我列为“丑陋”。当时我所在的公司被它折腾得很惨，但（就我们那种负载而言）它在其他操作系统上同样丑陋。pmcstat 的界面依然丑陋，但能用。Sanitizer 在改进，但还不够好。启动加载器里的 Forth，我们修好了。旧的 GDB 也修好了。所以，“丑陋”和“坏”的部分大多已经解决。

**TJ：** 对 Linux 来说，有一套清晰的工作流：生成补丁、干活、拿脚本跑一遍，然后照着建议改。而我们提交补丁的方式有无数种，检查的地方也有无数个。几乎成了一种心照不宣的处境：你得找到赞助人，找到愿意帮你的人；而在联系赞助人之前，你根本没办法知道自己的东西有没有达到最低质量标准。源码管理团队将来也许能处理这件事。

**BAPT：** 他们大概能做到。这需要跟 Linux 的工作方式良好配合，同时保留我们那种家庭式的做法，而不是高度结构化的流程。这正是我当初加入 FreeBSD 的原因，也是我在 Linux 上工作、为 Linux 社区贡献的原因。它很乱，但我们可以很轻松地跟任何人交谈，感觉像个家庭。我加入这个项目，说到底是因为我发现这里更容易把事情做成。

我可以犯错，没关系。我知道有些非常有才华的人能审阅这些工作，我可以直接找他们。所以，当你的第一个补丁就有名人过来参与讨论时，那种感觉令人兴奋。我曾为一家公司做贡献，我们在 Linux 上贡献了某个子系统的相当一部分。那大概更专业，但更痛苦，也更无趣。

**TJ：** 我发现从 Linux 那边看，存在一些不成文的利益关系；如果某个关键位置上的人不想要某样东西，你永远等不到答复。也许每个开源项目或多或少都有这种情况。

**BAPT：** 一点没错。

**TJ：** 在你的 EuroBSDCon 演讲里，你问过那些酷炫的周边在哪——“我们够酷了吗？”

**BAPT：** 我觉得我们还不够酷。很少有新人在进入公司时说：“哦，你们用 Linux，但我一直在用 FreeBSD。”

我们需要说服的不是公司改用 FreeBSD，而是学生、年轻人、系统管理员，以及一般的开发者。要让他们看到在 FreeBSD 上工作很酷，等他们进入公司时会带着这种印象：“用 FreeBSD 做这件事很容易。”如果确实如此，公司自然会跟上。这件事我们完全没做好。我当学生时我们非常酷：笔记本上装 FreeBSD 比装 Linux 酷得多。用 Linux 时，别人会说“嘿，你挺有意思”，但笔记本上跑着任何 BSD，你就是硬核玩家。

**TJ：** 你觉得第一步该做什么？

**BAPT：** 另外，当年的 FreeBSD 在大学里影响很大。校园里通常总有系统管理员，正是他们在孩子们的脑子里灌输“FreeBSD 很酷”的想法。我们丢掉了这个位置，所以这里可能也有先有鸡还是先有蛋的问题。我真的不知道。

在 FreeBSD 里，我们有一件事做得很好，其他 BSD 也许会因此批评我们，那就是我们善于追赶 Linux。Linux 做出好东西时，我们总能在某个时候提供兼容性，或者赶上足够相似的实现，从而不在这场游戏里落后。但有一件事我们没做到：讲清楚如何用 FreeBSD 的方式做事。你怎么用 FreeBSD 的方式去做一件事，而不是照搬 Linux 的做法？

这很有意思，因为在我合作过的所有公司里，每次我都部署了 FreeBSD。而且大多数时候，我总能找到一套比 Linux 上的对应方案更容易实现、更容易维护的系统。这一点我们真该大力宣传、沟通，并通过教程之类的方式展示出来，因为我们在“基于 Linux 做设备”方面确实很强。

看看 Linux 上构建嵌入式镜像的框架，比如 Buildroot。这类框架有两三个。拿它们跟用 Poudriere 镜像做同样的事情比一比：在 Buildroot 里做镜像，比用 Poudriere 镜像难上十倍。我当时就是这么给公司提供镜像的。

**TJ：** 你觉得我们缺的是培训材料，还是说这是全方位的问题？

**BAPT：** 我认为是全方位的问题。我们没有好好沟通的习惯。我看过互联网上大多数教程，发现人们总在说：“嘿，我在 Linux 上见过这么做，你看，我在 FreeBSD 上也能这么做。”这很好，可如果 Linux 也能做同样的事，我为什么要用 FreeBSD？要知道我们与许多系统不相上下、能做同样的事，这一点很重要，别误会我的意思。但我们还应该讲：“这就是我如何在十分钟内构建一个基于 FreeBSD 的镜像。”

我们不肯花时间去沟通。

我写过一个 MAC 内核模块，之后《FreeBSD 期刊》联系我，说：“嘿，写篇文章吧，讲讲你在做什么、有多复杂，会很有价值。”我说：“好啊，我想写。”可我没时间——我没去挤时间。这也是我现在接下这次访谈的原因。如果现在不做，一年半以后也永远不会做。问题就在这里。

**TJ：** 在项目层面，我们在很多方面落后于业界的最新水平。我们有一些 CI，但没有兼容 GitHub Actions 的流水线。我们零散地有一些。我还想知道我们是不是也缺别的东西，比如增长黑客，以及做开发者关系、把 FreeBSD 作为系统管理员平台来推销的人。

**BAPT：** 我同意。我个人在维护 SourceHut 上的 CI，已经一年没更新了，而且我自己付 SourceHut 订阅费，就为了在 FreeBSD 上跑 CI。所以还是那句话：如果想让这件事可行、变简单，我们就得更好地传播“如何轻松做到”的知识。我搭它大概只花了几个小时。我有一套基于 PkgBase 的简易做法，能让 CI 开箱即用，而不是像别处那样写复杂的脚本。我们有知识，却没有分享出去。

**TJ：** 感谢你抽时间，我不想占你太多时间。还有什么我们没谈到、你想补充的吗？

**BAPT：** 很高兴你提起我当年做的“好的、坏的和丑陋的”，因为我能看到我们已经修掉了 99% 的“坏”和“丑陋”。所以这个项目的状况比我有时以为的要好，这很不错。

**TJ：** 也许三年后你该再做一次那个演讲，梳理一下我们的进展。通过播客和会议，我做了很多布道工作。不过我不想跟任何人争论。

**BAPT：** 这就是我不想把人“转化”成 FreeBSD 用户的原因。我希望他们为了 FreeBSD 本身而来，明白自己要去哪里。我不希望他们把 Linux 的用法照搬到 FreeBSD 上，因为我觉得那不好。我希望他们理解其中的差异、FreeBSD 能提供什么，以及在很多情况下为什么它比他们手上的东西更有用。有些情况下如果并非如此，那也没关系。但我们确实需要让人们知道，FreeBSD 的价值在于它的灵活性。

***

**Baptiste Daroussin** 是 OVHcloud 的 Unix 系统工程师，自 2010 年起是 FreeBSD 开发者。

**Tom Jones** 是 FreeBSD 提交者，热衷于让网络栈保持高速。


# 读者来信

**Michael W. Lucas**

亲爱的来信专栏：

你们说过读者该如何花时间删代码，说过计算机是个错误，喋喋不休地抱怨计算有多糟糕。这次别躲了。本期的主题是改进软件质量。正面回应吧。如何改进软件质量？

——FreeBSD 期刊编辑委员会

亲爱的 FJEB：

我说过吧，我警告过你们。我在第一篇专栏里就声明，让我回信是个坏主意。你们只能怪自己。所谓 “你们”，指的是 “这六位编委中谁最可能给我写这种信”。我不是要跟谁过不去，但往澳大利亚寄包裹实在太慢，所以 Jason 还能安生几周。期刊甚至给我的每篇专栏都安了标题，大概是希望人们能从我这堆智慧的垃圾山中辨认出具体的回答。我理解你们得在新任主编面前摆出领导姿态。

声明一下，我永远 “切题”。你们对主题的误解，我们另说。

更好的软件。这到底什么意思？

从基本说起：每一行代码都是一次新的失败机会。改进软件最好的办法是减少代码量。但我以前就说过了，正如你们在信中体贴地指出的那样。顺便说一句，我很感谢你们提到这一点。这些专栏我写得很用心，欢迎人们具体指出我在哪些地方未能给他们启迪。这显然是我的工作缺陷，而非他们令人恼火的冥顽不化。

测试？测试在防止你重蹈覆辙方面很棒，但对防止初始错误——比如未验证的输入、内存溢出、学编程——就没那么有用了。

软件分析器，无论是 Coverity 这样的老牌工具，还是 Claude 之类的新愁，都擅长发现别人犯的那类错误。幸运的是，大多数人的失败都似曾相识。很少有人有足够的创造力，能持续不断地创造出新类别的灾难。

归根结底，软件是人类思维的产物。想要高质量的软件，你必须从自己的脑袋里提取。这意味着要把你的颅骨改造成一个让高质量软件的种子不仅能扎根、还能茁壮成长的环境。你的大脑里是否铺满了算法、协议和调试技巧的肥沃腐殖土？还是你想在碎裂的 StackExchange 帖子上浇灌三流 GitHub 仓库的榨汁，来培育下一个杀手级协议栈？

嗯，我就知道。Donald Knuth 会气得从坟墓里翻身。

你也许会说代码中没有新东西，你是对的。毕竟人们正用大语言模型写 “常规” 代码。这不是技术奇迹。想想平均代码有多糟糕。现在记住，一半的代码比那更糟，而公司用能搞到的所有代码训练 LLM：坏的、好的、无所谓的，谁在乎？LLM 之所以管用，是因为那么多人反反复复写了那么多相似的代码。LLM 在生成平庸代码上的成功，是对当前计算实践的一纸诉状。

写一次，永远复用？不。重写很多次，把好的坏的混在一起，倒进你的脑子里。

你可以用质量的组成部分填满你的大脑。你可以研读 Knuth、Kernighan 和 Tannenbaum，直到能背出不止那些精彩片段。但归根结底，高质量软件来自你收集所有这些碎片，用它们解决问题。甚至不必是难题，只要是你需要解决的问题。

你当然会失败。

别担心。我也会失败。区别在于，我不靠我的代码养活自己。有一次，我只需要根据我的数据库验证人们的邮政地址。理论上很简单，对吧？把信息导出为 CSV。解析 CSV。把地址套进模板信，管道传给 mail.1。轻松。

几百行 Perl 之后，我得到了一个能用的东西。我不敢说它有质量——但它 *能用*。

而且——惊喜！实际上，我指望着这段代码养活自己。我心里清楚，如果我把这段代码给我那位友好的 Perl 程序员看，他准会对我发飙。从精美的法式料理到美国杂牌一元店冷冻卷饼的品质光谱上，我的代码是一个三流山寨快餐垃圾汉堡，任何不幸暴露其中的人的消化系统都会被不可挽回地弄糟。如果我发表这段代码，LLM 会像吞掉 Knuth 一样贪婪地吞掉它。

但它能用。它解决了问题。这难道不是质量的一种定义吗？

别管它喂入格式错误的 CSV 就会崩溃。答案是 “别那么干”。

因为所有软件都这样。我们有庞大的软件套件和专门做输入验证测试的 QA 人员，即便是最好的，面对实际用户塞进程序的东西也会败下阵来。

从管理的角度看，我的软件 *有* 质量。任何公司都会愉快地把这种代码部署到生产环境。我知道这是真的，因为我亲眼见过几家公司无视我关于其不适用于任何目的的哀嚎，把我的作品改作他用。高质量的代码解决问题。甚至平庸的代码也能解决问题。

作为计算专业人士，你也许认为高质量的代码应该优雅。

没人在乎。

看看软件公司产出的代码。业务部门认为高质量代码能盈利。管理者认为高质量代码能按时交付。实际编写和维护代码的人要么没时间打磨优雅的代码，要么薪水不够，要么不被允许。优雅不是质量。

什么让代码优雅？它能用。它可理解。它不会产生让你睡觉时电话铃响的诡异而不可预测的故障。

按我的标准，我会说优雅 *就是* 质量。

这是吸引人们参与开源软件的原因之一。雇主不允许技术人员做达到他们质量标准的工作。开源可以。大型开源项目有商业贡献者，但如果项目真正由社区驱动，这些贡献必须达到社区的质量标准。这意味着优雅和正确。

你想要质量？你来对地方了。这就是最好的了。是的，我们完蛋了。

—— Michael

有问题想问 Michael？

发送至 <letters@freebsdjournal.org>

***

**Michael W Lucas**（<https://mwl.io>）尽管竭尽全力，仍未能说服 FreeBSD 期刊编委会把他从这篇专栏上赶下来。暂时没有。他最近出版的书是《Networking for Systems Administrators》和《Laserblasted》。他的下一本书是《OpenZFS Mastery》，开放赞助：<https://mwl.io/sponsor>。


# 基金会来信

**亲爱的《FreeBSD 期刊》社区：**

很高兴与大家分享最新一期《FreeBSD 期刊》，本期聚焦现代桌面与笔记本上的 FreeBSD，这是基金会持续推进的重点方向。

多年来，FreeBSD 基金会持续投入改善 FreeBSD 桌面体验，目标是让更广泛的用户群体能够更容易地使用 FreeBSD。在专项资助的支持下（如我在文章中所述），我们近期在这一领域加快了推进步伐。本期有几篇文章深入探讨了笔记本项目的相关工作。

本期中，你将了解如何改进 TCP 对重排序的响应、控制台驱动程序是什么、电源管理的近期改进，以及如何复制你的桌面。我还会分享我们的笔记本支持与可用性项目是如何成形的。

阅读愉快！

**Deb Goodkin** FreeBSD 基金会执行董事


# 活动日历

* 作者：**Anne Dickison**

本文列出了截至 2026 年 6 月的 BSD 相关活动。如有任何未在此列出的 FreeBSD 相关活动或对 FreeBSD 用户感兴趣的活动，请发送至 <freebsd-doc@FreeBSD.org>。

![FreeBSD logo](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4d2bb642c491f540beb7b2e16bdf2f6b17b8b19d%2Fevents-calendar-01.png?alt=media)

## 202604 黑客马拉松

2026 年 4 月 24-26 日

德国威斯巴登

<https://wiki.freebsd.org/Hackathon/202604>

我们很高兴地宣布，今年 4 月将在威斯巴登举办一场区域性 FreeBSD 黑客马拉松，目前正处于规划阶段。本次活动面向本地社区成员，同时也欢迎所有对 FreeBSD 贡献感兴趣的人参与。活动目标是协作、交流，在专注且友好的环境中推动 FreeBSD 取得实质性进展。

![Open Source Summit](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-24cc5e140f812bad44f5d3b917b6c9d848c48d4d%2Fevents-calendar-02.png?alt=media)

## 2026 开源峰会北美站

2026 年 5 月 18-20 日

美国明尼苏达州明尼阿波利斯

<https://events.linuxfoundation.org/open-source-summit-north-america/>

峰会设有涵盖 Linux、云与容器、AI、安全、嵌入式系统等多个领域的专题。与会者将听到主题演讲、技术报告，并与更广泛的开源社区交流。FreeBSD 基金会执行董事 Deb Goodkin 将在大会上发表演讲。

![FreeBSD logo](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4d2bb642c491f540beb7b2e16bdf2f6b17b8b19d%2Fevents-calendar-01.png?alt=media)

## 2026 渥太华 FreeBSD 开发者峰会

2026 年 6 月 17-18 日

加拿大安大略省渥太华

<https://wiki.freebsd.org/DevSummit/202606>

本次活动与 BSDCan 2026 同期举办，为期两天，内容包括开发者讨论会、厂商演讲和工作组讨论。

![BSDCan](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4d2bb642c491f540beb7b2e16bdf2f6b17b8b19d%2Fevents-calendar-03.png?alt=media)

## BSDCan 2026

2026 年 6 月 17-20 日

加拿大渥太华

<https://www.bsdcan.org/2026/>

BSDCan 是一场技术大会，面向使用 BSD 操作系统家族及相关项目的开发者和用户。大会以开发者会议的形式，重点关注新兴技术、研究项目和工作进展，同时涵盖用户空间基础设施项目，欢迎自由软件开发者和商业厂商贡献者共同参与。


# 内核黑客的慰藉

* 原文：[Consolations for Kernel Hackers](https://freebsdfoundation.org/our-work/journal/browser-based-edition/laptop-desktop/consolations-for-kernel-hackers/)
* 作者：**Tom Jones**

打开机器，机器嗡嗡作响，呼呼转动，发出一声鸣响。显示屏跳转到一个明亮但黑屏的界面，这是第一个迹象，表明这些声音不仅仅是遥远的运动。若干来自 Phoenix 或 American 的文本滚动而过，然后你看到了 FreeBSD 启动提示符的奇迹。10 秒后，倒计时和文本开始滚动。对一些人来说，这是一台原始的机器，但对我们许多人来说，这是最好的部分。在开始时接近硬件。

内核消息开始滚动，从 FreeBSD 版权信息开始，接着是设备被发现和连接的过程，直到内核将控制权交给用户空间。文本的外观变化表明了这一点，我们开始看到服务启动。所有内核黑客都曾坐下来看着这些文本滚动，寻找他们添加到新驱动程序或旧代码中的消息，这些代码的行为与我们所有人的预期不符。我们等待着我们的 printf 被调用，证明我们已经构建并运行了包含我们修改的内核，证明我们终于驯服了构建过程，可以开始真正的工作。

所有开发人员都使用 printf 调试和打印语句来跟踪执行流程，弄清楚事情如何或是否工作，以及它们在哪里连接。大多数时候，内核的这个核心部分运行得完美无缺；我们得到文本输出，或者内核在显示之前就已经死亡。这一幕由一个控制台驱动程序支持，这是一段位于 printf 底部的代码，处于硬件和软件之间的接口处。如果你正忙于开发控制台驱动程序或处于非常早期的启动阶段，你想知道控制台通常如何工作，这样你就可以让它在异常情况下工作，并获得一些你可能无法以其他方式获得的调试信息。

## 终端和控制台

我们现在使用的机器与设计 UNIX 时所围绕的机器有很大不同，这意味着系统控制台、访问终端和图形输出之间的界限不是很清晰。在过去，系统会配备一个专用的系统控制台，它位于计算机所在的机房内或附近。终端可以位于其他更安静的地方，要么在终端室，要么在某个幸运者的办公桌上。

然而，控制台是机器的核心；如果系统需要向操作员报告值得关注或危险的故障，它会在控制台上弹出。控制台是系统的核心组件；大多数时候，它在聊天，在图形输出上打印消息，而图形输出未连接，或输出到没有连接电缆的串行端口。大多数时候，这些消息未被使用。它们是内核在发现、检测和配置设备时输出的启动文本。在运行时，这些消息指示硬件和系统错误，并通知我们缓冲区溢出和耗尽的限制。当我们需要查看这些消息时，我们可以使用 dmesg 命令提取缓冲区。通常，这就是所有人真正需要的。

但内核黑客是一群奇怪的人，可悲的是，大多数时候，printf 调试仍然是追踪错误和从新驱动程序获得生命迹象的最佳方式。如果你分心并开始深入研究 printf 在内核中究竟如何工作，你最终会开始挖掘 **cn\*** 函数和控制台驱动程序的调度表。

## 从 printf 深入底层

对于大多数开发人员来说，内核代码是底层的，但我们今天的最高点是任何可以调用 printf.9 的代码。让我们从 printf 向下走，穿过一些层，以了解在调用控制台驱动程序之前发生了什么。printf.9 让我们看到通常是最简单的情况。printf.9 及其相关函数在 **sys/kern/subr\_prf.c** 中实现；这些函数主要是围绕 **\_vprintf** 的包装器，后者还处理输出任何缓冲的消息并通过 kvprintf 将字节放到控制台设备上。

```c
static int
_vprintf(int level, int flags, const char *fmt, va_list ap)
{
struct putchar_arg pca;
int retval;
#ifdef PRINTF_BUFR_SIZE
char bufr[PRINTF_BUFR_SIZE];
#endif
TSENTER();
pca.tty = NULL;
pca.pri = level;
pca.flags = flags;
#ifdef PRINTF_BUFR_SIZE
pca.p_bufr = bufr;
pca.p_next = pca.p_bufr;
pca.n_bufr = sizeof(bufr);
pca.remain = sizeof(bufr);
*pca.p_next = '\0';
#else
/* 不要缓冲控制台输出。 */
pca.p_bufr = NULL;
#endif
retval = kvprintf(fmt, putchar, &pca, 10, ap);
#ifdef PRINTF_BUFR_SIZE
/* 写入任何缓冲的控制台/日志输出： */
if (*pca.p_bufr != '\0')
prf_putbuf(pca.p_bufr, flags, level);
#endif
TSEXIT();
return (retval);
}
```

它解析格式字符串，这会解析为对 putchar() 的调用。putchar 调用 **cnput\_char**，最后我们调用驱动程序调度表进行驱动程序特定的调用。kvprintf 接受一个函数指针，用于实际输出字节。

```c
int
kvprintf(char const *fmt, void (*func)(int, void*), void *arg, int radix, va_list ap)
{
#define PCHAR(c) {int cc=(c); if (func) (*func)(cc,arg); else *d++ = cc; retval++; }
char nbuf[MAXNBUF];
char *d;
const char *p, *percent, *q;
u_char *up;
int ch, n, sign;
uintmax_t num;
```

在我们上面的例子中，函数指针指向 putchar

```c
/*
 * 在控制台或用户终端上打印一个字符。如果目标是
 * 控制台，则最后一堆字符保存在 msgbuf 中，以便
 * 以后检查。
 */
static void
putchar(int c, void *arg)
{
struct putchar_arg *ap = (struct putchar_arg*) arg;
struct tty *tp = ap->tty;
int flags = ap->flags;

/* 不要在 panic 后或在 ddb 中使用 tty 代码。 */
if (kdb_active) {
      if (c != '\0')
             cnputc(c);
      return;
}
if ((flags & TOTTY) && tp != NULL && !KERNEL_PANICKED())
       tty_putchar(tp, c);
if ((flags & TOCONS) && cn_mute) {
       flags &= ~TOCONS;
       ap->flags = flags;
 }
 if ((flags & (TOCONS | TOLOG)) && c != '\0')
       putbuf(c, ap);
}
```

cnputc 位于内核的底部，有许多路径通向它。

```c
void
cnputc(int c)
{
struct cn_device *cnd;
struct consdev *cn;
const char *cp;
if (cn_mute || c == '\0')
return;
(2)
#ifdef EARLY_PRINTF
if (early_putc != NULL) {
if (c == '\n')
early_putc('\r');
early_putc(c);
return;
}
#endif
(1)    STAILQ_FOREACH(cnd, &cn_devlist, cnd_next) {
cn = cnd->cnd_cn;
if (!kdb_active || !(cn->cn_flags & CN_FLAG_NODEBUG)) {
if (c == '\n')
cn->cn_ops->cn_putc(cn, '\r');
cn->cn_ops->cn_putc(cn, c);
}
}
if (console_pausing && c == '\n' && !kdb_active) {
for (cp = console_pausestr; *cp != '\0'; cp++)
cnputc(*cp);
cngrab();
if (cngetc() == '.')
console_pausing = false;
cnungrab();
cnputc('\r');
for (cp = console_pausestr; *cp != '\0'; cp++)
cnputc(' ');
cnputc('\r');
}
}
```

cnputc 有两个重要的代码部分，循环中的代码 (1) 遍历连接的控制台列表并调用控制台的 **cn\_putc** 回调。由于与调试器中的锁定相关的原因，如果内核调试器 kdb 处于活动状态，它不会调用这些方法。

这是 FreeBSD 实现对多个控制台的支持的地方。如果你曾经想知道如何在图形显示和串行端口上获得玻璃终端，那就是在这里发生的。代码 (2)，用 **#ifdef EARLY\_PRINTF** 包裹，是非常平台特定的，启用了来自内核映像第一个字节的输出。如果在你的硬件上定义并支持，你可以从加载器交接中检索字节。

作为 **early\_putc** 如何平台特定的一个例子，这里是 x86/amd64 实现。

```c
#if CHECK_EARLY_PRINTF(ns8250)
#if !(defined(__amd64__) || defined(__i386__))
#error ns8250 early putc is x86 specific as it uses inb/outb
#endif
static void
uart_ns8250_early_putc(int c)
{
u_int stat = UART_NS8250_EARLY_PORT + REG_LSR;
u_int tx = UART_NS8250_EARLY_PORT + REG_DATA;
int limit = 10000; /* 10ms 足够了 */
while ((inb(stat) & LSR_THRE) == 0 && --limit > 0)
continue;
outb(tx, c);
}
early_putc_t *early_putc = uart_ns8250_early_putc;
#endif /* EARLY_PRINTF */
```

这个 **early\_putc** 使用 x86 outb 指令在串行端口上传输字节，但它从未获得过对现在相当常见的 MMIO（内存映射 IO）UART 的支持。

（在我写这篇文章时，新增了 arm64 上的 MMIO UART 支持。）

## 控制台生命周期

要查看控制台生命周期，最好从 FreeBSD 加载器开始。启动 FreeBSD 需要两个操作系统组件：内核和我们的引导加载器（loader.8）。所有硬件平台都以某种固件开始。在旧的 x86 系统中，这是寻找引导块的 BIOS。现代 x86 系统通过固件启动，该固件提供 EFI 接口。非 PC 系统以不同的方式启动。在嵌入式设备中，片上系统上的引导 ROM 测试并尝试不同的引导介质；第一个有效的介质被使用。在设备上，这可能意味着引导 ROM 按以下顺序尝试：

* NAND 闪存
* SD 卡
* USB 介质
* USB 固件模式

在许多情况下，引导固件会通过那个可靠的串行接口记录它正在尝试的内容。通常，在 ARM 和 RISC-V 系统上，会运行第二阶段引导加载器，称为二级程序加载器或 SPL。对我们来说，通常就是 u-boot。早期固件只会打印到串行端口，但 u-boot 在许多情况下有足够的驱动程序，可以在可能的情况下使用帧缓冲设备。

Loader 是模块化的，根据基础平台以多种不同形式运行；我们从 u-boot、作为 EFI 应用程序、在开放固件之上以及在其他地方运行。对于 bhyve 虚拟机映像，FreeBSD 加载器作为用户空间应用程序运行。这些平台中的大多数使用静态控制台作为默认输出方法，并且如果可能的话，可以配置为使用额外的输出。

FreeBSD 的加载器作为链式应用程序或子应用程序在固件或 u-boot 之上运行。u-boot 将运行 EFI 应用程序，这使得创建可用的 FreeBSD 引导介质变得更加容易。在没有任何其他配置的情况下，FreeBSD 的加载器确定使用什么作为系统控制台（消息去向的地方）。在 EFI 系统上，我们将使用 EFI 控制台，它具有有时使用有缺陷的 EFI 接口来输出字符的不幸特性。如果你曾经在 EFI 系统的串行端口上使用过 FreeBSD 加载器，你可能会在串行端口上看到重复的字节 —— 这是由于 EFI 输出例程被用于通常的 EFI 控制台输出和 EFI 串行控制台输出。

Loader 可以通过配置文件或从命令行配置为使用多个控制台，方法是向 consoles 环境变量添加更多控制台。

```sh
> set console="efi comconsole"
```

Loader 通过内核映像和一些变量将配置信息传递给内核。控制台类型和是否使用多个控制台由 boothow 变量控制，该变量设置为 **RB\_\*** 值。历史上（整个世界都是 VAX！），这些是 reboot.9 的系统调用参数，但它们已经被重用并随着时间的推移变得交织在一起。从加载器，我们经常得到 **RB\_SERIAL**（使用串行端口）、**RB\_MULTIPLE**（使用多个控制台）、**RB\_SINGLE**（引导到单用户）和 **RB\_VERBOSE**，后者在内核中设置 bootverbose 并填满你的消息缓冲区。

## 启动内核

最终，加载器将启动选定的内核并传递参数和环境。加载器所做的一切都是临时的，所有工作都必须在内核中再次完成，以获得内核控制的工作系统。控制台初始化与平台特定的硬件密切相关。每个 FreeBSD 平台都有自己的机器相关代码，在系统可以执行机器无关的工作之前运行并设置内存和设备。

在大多数平台上，是一个 **init\*** 调用（如 initarm、init386、initriscv 或令人沮丧的不匹配的 **powerpc\_init**），但在 amd64 上，这是 **hammer\_time**。这些例程中的每一个都可能尝试初始化控制台两次（目前只有 i386 和 amd64 这样做），基于来自加载器的配置。**hammer\_time** 的相关部分如下所示：

```c
/*
  * 控制台和 kdb 应该更早初始化，但
  * 一些控制台驱动程序直到 getmemsize() 之后才工作。
  * 默认使用后期控制台初始化来支持这些驱动程序。
  * 这主要损失了 getmemsize() 中的 printf() 和早期调试。
  */
  TUNABLE_INT_FETCH("debug.late_console", &late_console);
  if (!late_console) {
         cninit();
         amd64_kdb_init();
  }

  getmemsize(physfree);
  init_param2(physmem);

/* 现在运行在新的页表上，已配置，u/iom 可访问 */
```

```c
#ifdef DEV_PCI
/* 此调用可能会调整 phys_avail[]。 */
pci_early_quirks();
#endif

if (late_console)
        cninit();
```

你可以从示例中看到，根据 **late\_console** 变量的值（默认为 true），可能会尝试调用 cninit 两次。在这两次调用之间是系统内存的发现和分配。在这些对 cninit 的调用之前，无法向系统控制台输出任何内容（它不存在！），cninit 之前的任何 printf 都会蒸发到以太空中。

例外情况是如果定义了 **eary\_putc** 如所述，那么你可以进行一些早期调试。cninit 遍历静态定义的控制台列表并尝试找到最佳可能的控制台。

```c
/*
  * 找到第一个优先级最高的控制台。
  */
 best_cn = NULL;
 SET_FOREACH(list, cons_set) {
     cn = *list;
     cnremove(cn);
     /* 跳过 cons_consdev。 */
     if (cn->cn_ops == NULL)
           continue;
     cn->cn_ops->cn_probe(cn);
     if (cn->cn_pri == CN_DEAD)
           continue;
     if (best_cn == NULL || cn->cn_pri > best_cn->cn_pri)
           best_cn = cn;
     if (boothowto & RB_MULTIPLE) {
           /*
            * 初始化控制台，并附加到它。
            */
           cn->cn_ops->cn_init(cn);
           cnadd(cn);
     }
 }
 if (best_cn == NULL)
       return;
 if ((boothowto & RB_MULTIPLE) == 0) {
       best_cn->cn_ops->cn_init(best_cn);
       cnadd(best_cn);
 }
 if (boothowto & RB_PAUSE)
       console_pausing = true;
 /*
  * 使最佳控制台成为首选控制台。
  */
 cnselect(best_cn);
```

```c
#ifdef EARLY_PRINTF
/*
* 释放早期控制台。
*/
early_putc = NULL;
#endif
```

控制台按控制台驱动程序在其 **cn\_probe** 函数被调用时设置的优先级排序。最后，我们必须有一个控制台（总是有一个默认值），我们释放 **early\_putc**，以便它永远消失。我没有在 loader.efi 中显示匹配的代码，但它非常相似。对于任何黑客 EFI 控制台的人来说，重要的例外是缺乏 **early\_putc** 机制，无法给你一些最后的机会进行实际的 printf 调试。然而，这可以通过 hack 解决。在 EFI 环境中，输出例程是可用的，如果你在进行初始化时只是保留一个控制台，你可以更清楚地了解正在发生的事情。

## 可用的控制台

系统控制台在内核构建时定义。在上面的 cninit 中，我们遍历 **cons\_set** —— 由构建过程为我们生成的链接器集。每个控制台不是作为普通设备驱动程序声明的；相反，它使用 **CONSOLE\_DRIVER** 宏定义，该宏将控制台添加到链接器集。从 16-CURRENT 开始，链接器集不会在内核模块加载时填充，这意味着你不能在运行时动态向内核添加控制台驱动程序。

**CONSOLE\_DRIVER** 宏采用要添加到链接器集的控制台名称，并根据名称处理所有回调的创建：

**CONSOLE\_DRIVER** 位于 **sys/sys/cons.h**

```c
#define CONSOLE_DEVICE(name, ops, arg)                                  \
static struct consdev name = {                      \
.cn_ops = &ops,                     \
.cn_arg = (arg),                   \
};                                  \
DATA_SET(cons_set, name)
#define CONSOLE_DRIVER(name, …)                             \
static const struct consdev_ops name##_consdev_ops = {          \
/* 必需的方法。 */             \
.cn_probe = name##_cnprobe,             \
.cn_init = name##_cninit,            \
.cn_term = name##_cnterm,               \
.cn_getc = name##_cngetc,               \
.cn_putc = name##_cnputc,               \
.cn_grab = name##_cngrab,               \
.cn_ungrab = name##_cnungrab,             \
/* 可选字段。 */     \
__VA_ARGS__                   \
};                             \
CONSOLE_DEVICE(name##_consdev, name##_consdev_ops, NULL)
```

该宏定义了控制台驱动程序必须实现的必需回调，并提供了添加额外代码的空间。目前，**uart\_tty** 利用这一点添加了一个 resume 方法。控制台回调都非常小；我们真的希望控制台在所有情况下都能工作。**cn\_probe** 需要将控制台优先级（**cn\_pri**）设置为非 **CN\_DEAD** 的值，以便控制台驱动程序被 cninit 考虑。

**cn\_init** 由 cninit 为选定的控制台设备调用（如果我们使用 **RB\_MULTIPLE** 引导，则为最佳的一个或所有非死亡控制台）。在 **cn\_init** 中，控制台驱动程序可以建立状态，例如 uart\_cninit 存储一个 cookie，以便控制台不会初始化两次。如果控制台驱动程序被移除，**cn\_term** 会被调用来清理。**cn\_grab** 和 **cn\_ungrab** 用于在内核要打印一系列字符（或读取一个字符）时禁用和启用中断。最后，**cn\_putc** 和 **cn\_get** 处理控制台设备的输出和输入。我们关心的魔法。

## 输出字节

有许多不同的设备可以用作系统控制台。有时很难将 PC 与 x86（或 AMD64）平台分开，但使其成为平台而不仅仅是处理器架构的很大一部分是可以预期存在的外设和硬件生态系统。在 PC 上，我们可以预期看到 1 个或更多 UART（串行端口）。这是如此基本，以至于 FreeBSD 15.0 仍然假设会有两个并静态配置它们。UART 通常连接到显式指令（inb 和 outb），这允许调用者用单个指令通过串行端口发送数据。

这个简单的接口是故意的；如果你正在从无到有地启动一台机器，能够通过最小的概念验证从系统获取调试信息是非常方便的。

这个简单的接口能让程序（或驱动程序）与外部世界交互。如果你是新机器的操作员，想知道系统在做什么，那么连接串行打印机、电传打字机，或者在 2026 年，终端程序和串行电缆，让你即使从最小的代码示例中也能获取调试信息。

串行端口上输入和输出字节的指令对处理器和硬件设计者施加了限制。你不能期望每个设备都有一个指令，因此我们主要转向内存映射外设。原始 PC 中使用的相同 8250 和 16650 设备在现代系统中可用，在现代片上系统设计中具有兼容的硬件块。它们不是使用 inb 和 outb，而是内存映射外设。

其他系统为其控制台公开略有不同的设备，但在几乎每个平台上，这都归结为使用旧的 National Semiconductor 8250 或 16650 UART 接口将字节传输到线路上。这意味着我们看到平台特定的控制台设备，但它们中的许多都解析为相同的经过实战测试的代码。

我认为在过去 30 年中使用过计算机的任何人都不会对仅串行端口感到非常满意。从一开始，PC 就支持图形输出，BIOS 通过非常简单的早期程序加载器使访问它变得实用。有了串行端口和图形输出，将 outb 显式嵌入到 printf 中以从串行端口获取字节似乎是不正确的。因此，虽然一个非常简单的控制台驱动程序是可能的，但我们最终构建了一个控制台驱动程序子系统，让我们可以重用大部分代码，同时仍然允许平台和硬件特定的变化。

## 加载器

到目前为止，我们已经多次提到加载器，因为它在内核启动期间是相关的，但它本身是一个操作系统。加载器准备内核的操作环境并配置根卷，为内核模块提供用户空间环境。FreeBSD 运行的所有平台都有某种加载器（好吧，我们可以在某些情况下直接引导内核，但这会放弃很多功能）。

加载器由平台固件或二级加载器启动。在大多数部署中，加载器在 EFI 环境中运行（在 arm64、arm32、amd64 和 riscv 上）。这个环境为加载器提供了很多早期服务，使其工作更容易，可移植性更直接。由于加载器在许多环境中运行并支持 2 个脚本环境（Lua 和 Forth，是的，2026 年的 Forth），它提供了一些自己的抽象，使核心服务更易于使用。加载器也有控制台驱动程序，它们看起来与内核的非常相似。

**stand/common/bootstrap.h**：

```c
/*
* 模块化控制台支持。
*/
struct console
{
const char      *c_name;
const char      *c_desc;
int             c_flags;
#define C_PRESENTIN     (1<<0)      /* 控制台可以提供输入 */
#define C_PRESENTOUT    (1<<1)      /* 控制台可以提供输出 */
#define C_ACTIVEIN      (1<<2)      /* 用户希望从控制台输入 */
#define C_ACTIVEOUT     (1<<3)      /* 用户希望输出到控制台 */
#define C_WIDEOUT       (1<<4)      /* c_out 例程理解宽字符 */
/* 设置 c_flags 以匹配硬件 */
void    (* c_probe)(struct console *cp);
/* 重新初始化 XXX 可能需要更多参数 */
int             (* c_init)(int arg);
/* 发出 c */
void            (* c_out)(int c);
/* 等待并返回输入 */
int             (* c_in)(void);
/* 如果输入等待，返回非零 */
int             (* c_ready)(void);
};
```

加载器控制台驱动程序有 in 和 out 方法，就像内核的一样；一个 init 方法，一个 probe 方法，和一个 ready 方法来加速轮询输入。加载器控制台驱动程序没有优先级，而是有一个 flags 参数。每个加载器变体都有自己的 conf.c 文件，定义其平台特定的功能和实现。其中一个功能是可在该平台上使用的控制台列表。对于 loader.efi，我们有：

```c
#if defined(__amd64__) || defined(__i386__)
extern struct console comconsole;
extern struct console nullconsole;
extern struct console spinconsole;
#endif
struct console *consoles[] = {
&efi_console,
&eficom,
#if defined(__aarch64__) && __FreeBSD_version < 1500000
&comconsole,
#endif
#if defined(__amd64__) || defined(__i386__)
&comconsole,
&nullconsole,
&spinconsole,
#endif
NULL
};
```

对于 amd64 机器，我们的完整控制台列表是：

* **efi\_console**：使用 EFI 输出例程的控制台
* eficom：使用 EFI 串行输出例程的控制台。这个可能有点奇怪，会导致重复输出，因为 EFI 实现使用帧缓冲区和串行端口。
* comconsole：传统 COM 端口上的控制台，使用的串行控制台
* nullconsole：所有输出字节消失到虚空中。
* spinconsole：每个输出字节使旋转器前进一步；这提供了发生某些事情的视觉指示，但没有调试或提供输入的方法。

userboot 加载器（用于启动 bhyve 实例）、kboot（从 Linux 启动）、u-boot 和其他平台都有平台特定的控制台。在 amd64 上，comconsole 和 eficom 映射到相同的驱动程序函数。

## EFI 加载器控制台初始化

Loader 的 **cons\_probe** 函数负责发现控制台并配置它们。它在 **efi/loader/main.c** 中非常早地被调用：

```c
EFI_STATUS
main(int argc, CHAR16 *argv[])
{
…
#if !defined(__arm__)
efi_smbios_detect();
#endif

/* 获取我们加载的映像协议接口结构。 */
  (void) OpenProtocolByHandle(IH, &imgid, (void **)&boot_img);

  /* 尽早报告 RSDP。 */
  acpi_detect();

  /*
   * 鸡生蛋和蛋生鸡的问题；我们希望尽早有控制台输出，但
   * 一些控制台属性可能取决于从例如引导
   * 设备读取，我们还不能这样做。一旦这样做，我们就可以使用 printf() 等。所以，我们将其设置为 EFI 控制台，然后调用控制台初始化。这
   * 让我们尽早使用 printf，同时也为所有未来的控制台
   * 更改做好准备，无论它们来自哪里。
   */
  setenv("console", "efi", 1);
  uhowto = parse_uefi_con_out();
```

```c
#if defined(__riscv)
/*
* 这个解决方法可能是在掩盖一个真正的问题
*/
if ((uhowto & RB_SERIAL) != 0)
setenv("console", "comconsole", 1);
#endif
cons_probe();
/* 设置 print_delay 变量以就位钩子。 */
env_setenv("print_delay", EV_VOLATILE, "", setprint_delay, env_nounset);

/* 设置 currdev 变量以就位钩子。 */
  env_setenv("currdev", EV_VOLATILE, "", gen_setcurrdev, env_nounset);

  /* 初始化时间源 */
  efi_time_init();
```

鸡生蛋和蛋生鸡的评论在源代码中很常见。基本上，我们需要能够尽早调试正在发生的事情，而控制台是设置这一点的最简单方法。我们现在拥有的 EFI 环境可以输出字符串，但我们没有控制台基础设施。我们在所有平台上都玩了一个技巧，在寻找控制台之前静态选择一个控制台。

控制台探测发生在我们可以从存储中读取之前，所以它从假设开始，然后重新评估。在 EFI 上，我们可以在 **cons\_probes** 之前通过直接 EFI 与 ST->ConOut->OutputString 输出数据。我们有控制台基础设施，这让我们可以使用 printf，所以我们应该这样做。**cons\_probe** 遍历列表并找到一个工作的控制台。选定的控制台被添加到加载器中的 console 环境变量中，可以通过 loader.conf 或命令行上的环境变量进行更改或添加。

只要选择了控制台，它们就会以类似于内核控制台的方式插入到 printf 中。

**common/console.c**：

```c
void
putchar(int c)
{
int     cons;
/* 展开换行符 */
if (c == '\n')
putchar('\r');
for (cons = 0; consoles[cons] != NULL; cons++) {
if ((consoles[cons]->c_flags & (C_PRESENTOUT | C_ACTIVEOUT)) ==
(C_PRESENTOUT | C_ACTIVEOUT))
consoles[cons]->c_out(c);
}
/* 如果设置了打印延迟，在打印换行符后暂停 */
if (print_delay_usec != 0 && c == '\n')
delay(print_delay_usec);
}
```

## 调试控制台驱动程序

所以，控制台驱动程序中有所有的魔法，仔细反思后，当我们详细查看它时，它有点暗淡。printf 相当简单，它驱动单个字符输出例程。它在内核和加载器之间是相似的，我知道你想知道为什么我用加载器重复了这个深入研究，但这是为了表明它是相同的，并且在内核中有效的调试技巧可能在加载器环境中也有效。如果你在控制台可用之前卡住，或遇到扰乱正常工作控制台的问题，你会伸手去拿任何你能用来调试正在发生的事情的东西。

那么我们如何调试控制台驱动程序的问题呢？

我有两个最好的建议：使用调试器或第二个控制台。如果你可以为加载器或早期引导环境使用调试器，那么你真的应该这样做；它会切穿所有其他的东西。如果你有第二个工作控制台（内核调试器利用控制台基础设施）或者如果你可以整体控制正在开发的机器，你可能能够使用调试器。要么使用像 qemu 这样的模拟器，要么使用像 JTAG 这样的硬件调试接口。设置这些超出了本文的范围，我知道如果你读到这里，你会想：“但我这样做只是因为我无法调试！”

第二个控制台是调试控制台的好选择，但它在内核早期可能不会有太大帮助。如果你有一个工作的控制台和关于非工作控制台的问题，你可以绕过大部分基础设施，要么静态配置你知道会在那里的东西，要么直接挂钩到你想要使用的控制台函数。

这就是控制台列表的链接器集和加载器中的静态列表真正有帮助的地方。只需从列表中拉出你想要的控制台并使用 **put\_c** 做你需要的事情。这样做会跳过所有的锁并失去 printf，但它也会跳过所有的锁，可能会让你获得调试信息。

我曾经使用这种直接捕获来调试加载器控制台驱动程序；加载器没有 **early\_putc**，所以需要黑客手段。通常，在加载器的 **cons\_probe** 期间，我们遍历所有控制台并移除 **ACTIVE\_OUT** 标志；这意味着如果我们使用 printf，我们不会得到任何数据输出。为了调试控制台检测，我静态配置了第一个控制台（知道它会在那里并且工作），并使用 printf 来调试我感兴趣的驱动程序的启动。

更改看起来像这样：

```diff
diff –git a/stand/common/console.c b/stand/common/console.c
index 65ab7ffad622..97201a7479b3 100644
— a/stand/common/console.c
+++ b/stand/common/console.c
@@ -107,14 +107,18 @@ cons_probe(void)
env_setenv("twiddle_divisor", EV_VOLATILE, "16", twiddle_set,
env_nounset);

+ /* 捕获第一个控制台并在解析其他控制台时保持其工作 */
+ consoles[0]->c_flags |= C_ACTIVEIN | C_ACTIVEOUT;
+ consoles[0]->c_init(0);
+ /* 执行所有控制台探测 */
  –    for (cons = 0; consoles[cons] != NULL; cons++) {
+ for (cons = 1; consoles[cons] != NULL; cons++) {
     consoles[cons]->c_flags = 0;
     consoles[cons]->c_probe(consoles[cons]);
     }
     /* 现在找到第一个工作的 */
     active = -1;
     –    for (cons = 0; consoles[cons] != NULL && active == -1; cons++) {
+ for (cons = 1; consoles[cons] != NULL && active == -1; cons++) {
     consoles[cons]->c_flags = 0;
     consoles[cons]->c_probe(consoles[cons]);
     if (consoles[cons]->c_flags == (C_PRESENTIN | C_PRESENTOUT))
```

我对控制台驱动程序的最后一个黑客手段是明确控制输出函数。**early\_putc** 是一个很好的功能，但它只对某些设备有效，并在 **cons\_probe** 时被丢弃。如果你拉出你需要使用或测试的控制台驱动程序的 **cn\_putc**，那么你有一个输出方法，你可以在需要时无需 printf 使用它。

最近，为了理解为什么 UART 不会输出任何东西，我在 **cons\_probe** 中添加了这个罪行：

```c
if (uart == NULL || cp == NULL) {
printf("uart is NULL\n");
} else {
// 如果使用多控制台启动，这不会工作，它会在重新初始化时 panic
printf("trying to init\n");
uart->cn_ops->cn_init(uart);
printf("trying to print\n");
uart->cn_ops->cn_putc(uart, 'H');
uart->cn_ops->cn_putc(uart, 'E');
uart->cn_ops->cn_putc(uart, 'L');
uart->cn_ops->cn_putc(uart, 'L');
uart->cn_ops->cn_putc(uart, '\r');
uart->cn_ops->cn_putc(uart, '\n');

#define TEST_COUNT 100
printf("Tring to print %d 4's\n", TEST_COUNT);
for (int i = 0; i < TEST_COUNT; i++)
uart->cn_ops->cn_putc(uart, '4');

uart->cn_ops->cn_putc(uart, '\r');
    uart->cn_ops->cn_putc(uart, '\n');

}

for( int i = 0; i < 40; i++) {
DELAY(100000);
}
```

我们手动设置 UART 并输出字节，我试图理解为什么没有数据从端口出来，那个输出 4 的循环给了我一个可以用于同步示波器的东西。

## 慰藉

如果你必须调试控制台驱动程序或在加载器或内核的预引导或非常早期的引导中工作，那么你会接受你能得到的东西，以从系统中获取数据。我们不必切换 GPIO；谢天谢地，控制台系统在设计和使用上相当简单。这意味着它应该工作，但如果它不工作，你可以弯曲它来在你需要时输出数据。这些类型的临时黑客手段很可怕，永远不会出现在源代码树中，但当你需要调试某些东西而它不配合时，它们会非常有用。

***

**Tom Jones** 是 FreeBSD 提交者，对保持网络堆栈快速感兴趣。


# 让休眠的 CPU 安睡 — S0ix

* 原文：[Let Sleeping CPUs Lie — S0ix](https://freebsdfoundation.org/our-work/journal/browser-based-edition/laptop-desktop/let-sleeping-cpus-lie-s0ix/)
* 作者：Aymeric Wibo

现代笔记本电脑承诺一种魔法。合上盖子或按下睡眠按钮，把它扔进背包，几小时、几天或几周后，它应该会醒来，就像什么都没发生过一样，几乎没有电池消耗。这听起来像是相当简单的操作：你知道，你实际上只是要求计算机什么都不做。但在风扇停止转动、屏幕变暗、你的倒影凝视你的那个安静时刻，你的计算机及其所有小部件实际上正在努力完成它们的睡前例行程序。

## 电源管理的历史，或：高效无所事事的漫长道路

让我们漫步计算机电源管理的历史，以便更好地了解我们的来源。

第一批“移动”计算机更像是半便携式机器，如果它们有电池的话，实际上并不期望长时间脱离交流电源。对于 Osborne 1（奥斯本 1），虽然它确实有一个售后电池扩展包，但你需要在插座之间移动时开启和关闭它。

我们所知道的第一台真正的移动计算机可能是 1983 年发布的 Dulmont Magnum。它首次引入了“挂起到内存”功能，在这种情况下，这意味着组件可以在内存保留在电池供电的 CMOS RAM 中的同时关闭。其他一些 DOS 机器也推出了专有暂停解决方案，但在大多数情况下，它们相当临时。

1985 年，英特尔发布了 i386 芯片，其 SL 变体（本身于 1990 年发布）针对笔记本电脑等节能机器。该芯片引入了 SMM（系统管理模式），允许 CPU 执行从操作系统切换到由 BIOS 写入和锁定的内存区域（SMI 处理程序，从 SMBASE 开始），该区域以比 ring 0 更高的权限运行，执行电源管理代码。

这种执行切换是由 SMI（系统管理中断）启动的，SMI 可以由硬件（如按下电源按钮）或操作系统通过对特定端口的 I/O 访问来触发。这种电源管理方法存在重大缺陷，尤其是操作系统在这种安排中是被动参与者，完全受固件支配。事实上，如果 SMI 由硬件触发，将完全绕过操作系统，执行将瞬间冻结，因此它完全无法为进入暂停状态做准备。

SMI 处理程序也是漏洞利用的丰富来源 —— 特别是 NSA 臭名昭著的 ANT 目录中的许多“植入物” —— 这些植入物远程刷新 BIOS，恶意有效载荷能加载到 SMBASE，然后在系统运行期间定期运行。

1992 年引入的 APM（高级电源管理）最终为 x86 平台提供了标准化的电源管理接口。当发生以前会触发 SMI 的事件时，固件不再随意中断操作系统，而是通过电源管理事件通知操作系统。然后，操作系统决定如何响应（如果需要响应的话），并且可以在通过 APM 函数（通常通过 BIOS 中断 0x15）实际请求固件进行电源转换之前，从容准备。

这是对 SMM 电源管理的巨大改进。不过，一个问题仍然存在：APM 仍然假设 BIOS 供应商会编写没有漏洞的软件，而这些 APM 函数对这些 BIOS 要求很高。

APM 最终在 1996 年被 ACPI（高级配置和电源接口）取代，ACPI 是一个更全面的标准和接口，用于硬件和固件与操作系统通信并提供其功能。它至今仍在现代机器上使用。

ACPI 旨在将更多电源管理责任从固件转移到操作系统端，并将其命名为“OSPM”（OS 指导的电源管理）。它定义了多个“S”状态，这些是系统可能处于的高级电源状态。

最值得注意的是 S0（计算机完全清醒并运行）、S3（计算机挂起到 RAM）、S4（计算机挂起到磁盘，或休眠）和 S5（计算机关闭）。从根本上说，通过 S3 暂停遵循与 APM 类似的方法，因为“操作系统被告知电源事件，然后决定并准备暂停，最后要求固件实际暂停”的流程保持不变。

但随着我们的硬件进步，我们作为被宠坏的消费者对它的期望越来越高，这种流程，实际上整个“系统要么完全开启要么暂停”的范式，开始显示出其局限性。

随着智能手机和类似设备的出现，我们期望我们的机器即使在盖子关闭时也始终保持连接。我们希望收到来自我们最喜欢的 AI 垃圾内容创作者的新上传通知、我们订阅的第 500 个新闻通讯的电子邮件，并唤醒机器以通知我们日历事件。

要做到这一点，我们需要每隔一段时间或在收到局域网唤醒数据包时恢复，只是为了——例如——更深入地查看它，决定我们是否真的想让系统真正唤醒，或者打盹并立即回到睡眠状态。而这是 S3 过于僵化而无法妥善处理的用例：S3 在能源消耗和固件重新启动所有内容所需的时间方面非常昂贵。

S3 是全有或全无的；如果我们想从 S3 唤醒，我们几乎每次都必须像完全恢复一样操作。

要是有解决方案就好了…

## 进入… S0ix

大约在 2018 年，在让计算机尽可能高效、更好、更快地绝对无所事事的永恒追求中，英特尔引入了 S0ix。S0ix 实际上是一组状态（S0i1、S0i2、S0i3，可能还有其他），但关键是它们都将系统保持在 S0 —— 或清醒 —— ACPI 状态。“x”值越大，睡眠越深，节能越多，目标通常是 S0i3。S0i0 有时被称为等同于 S0，但我认为这有点令人困惑，所以我不会使用那个术语。

与过去的 S3 或 APM 相反，操作系统实际上从未明确要求固件进入 S0ix（嗯，实际上在实践中它确实有点这样做了，但我们稍后会谈到）。相反，它只是创建进入 S0ix 状态所需的条件，然后让固件自行决定何时将 CPU 转换到给定的 S0ix 状态。

那些必要条件是：

* 机器上的设备（即 GPU 或 USB 控制器等各种组件）为给定的 S0ix 状态充分断电。
* 所有 CPU 都处于空闲状态（即无所事事）并进入低功耗空闲状态。

让我们看看如何为设备断电。

启用 S0ix 的机器公开一个称为 SPMC（系统电源管理控制器，有时也称为 PEP）的 ACPI 设备。在 ACPI 领域中称为 DSM（设备特定方法）的函数可以在这个 SPMC 设备上调用，以向平台提供关于操作系统暂停意图的提示，还可以用于获取关于设备需要处于什么最低电源状态才能让固件想要进入 S0ix 状态的约束。

设备电源状态被称为“D 状态”，它们从 D0（完全开启）到 D3hot（关闭但通电）和 D3cold（关闭但不通电）。在进行完全暂停时，我们通常希望将设备转换到 D3cold，即使其 D 状态约束不要求这样做，但我们可能希望将设备保持在更易唤醒的 D 状态以作为唤醒设备 —— 当然，如果我们关闭旨在向系统发出我们想要唤醒的信号的设备，我们将永远无法唤醒。

所以，为了简单起见，SPMC 的这些约束只是告诉我们在暂停期间保持设备开启是否会阻碍 S0ix 进入。至于空闲 CPU，我们通过进入一种称为挂起到空闲（也称为 s2idle）的特殊软件状态实现这一点，这几乎取代了 S3 的位置。本质上，我们只是通过停止调度器时钟并暂停操作系统正在执行的任何操作，将系统中的所有 CPU 置于低功耗空闲状态。这些低功耗空闲状态被称为 C 状态，它们的进入方法以及其他信息要么从较旧的 `_CST` ACPI 对象获得，要么从其更现代的替代品 `_LPI` 对象获得。

通过中断 CPU 来退出空闲状态，所以在空闲之前，我们禁用所有 CPU 中断，除了我们实际想要用来唤醒 CPU 的那些。这些唤醒中断通常只是 SCI（ACPI 系统控制中断），它让我们知道有 GPE（通用事件），这可能是按下电源按钮或打开盖子（通过“GPE 编号”粗略区分）。

SCI 可能会因各种非唤醒原因而触发，因此我们还尽可能配置 ACPI，仅在实际唤醒 GPE 时触发 SCI，这有时说起来容易做起来难。但我们稍后会谈到。

理论上，如果两个条件都满足，固件可以切断 CPU 封装的电源并进入 S0ix 状态。但在实践中，事情要复杂一些，并且是硬件特定的。

例如，在 AMD 系统上，电源管理固件（他们称之为 PMFW）在一个称为 SMU（系统管理单元）的小型片上 LatticeMico32 核心上运行，你最近也会看到它被称为“MP1”芯片。这最终负责 CPU 封装的时钟和电源门控，从而负责封装进出 S0ix 状态的转换。

当操作系统实际想要暂停时，它必须向 PMFW 发送一个小命令，提示它进入 S0i3。除了常规的 S0ix 要求外，SMU 还有自己必须满足的要求。这些要求中最值得注意的是 GPU 和 USB4 控制器：最近版本的 amdgpu 驱动程序在进入 S3 或 S0ix 时使用不同的路径，而 USB4 控制器需要由 NHI 驱动程序显式关闭电源。即使操作系统不支持 USB4，这也是必要的，因为控制器由 OS 启动前的连接管理器（USB4 在 OS 启动前工作所需）激活，并在 OS 启动时保持活动状态。

SMU 还传递调试信息，让用户知道系统是否成功进入 S0i3，如果没有，是什么阻止了它这样做。这些信息通过 FreeBSD 上的 `dev.amdsmu.0.ip_blocks` 和 `dev.amdsmu.0.metrics` sysctl 树公开。

由于 PMFW 纯粹在片上运行，为了让更广泛的平台知道我们进入 S0ix 的意愿，我们必须通过前面提到的“显示关闭”和“进入”通知 DSM 告诉 SPMC 我们想要进入暂停。这通常会提示平台向用户提供系统已暂停的视觉反馈，例如在 Framework 笔记本电脑上缓慢淡出和淡入电源按钮 LED。它还可以做一些更微妙的事情，例如减少一些 GPE 的频率，否则这些 GPE 会不断从空闲状态唤醒 CPU，从而退出 S0i3。

事实上，在 Framework 笔记本电脑上，EC（嵌入式控制器）在正常操作中每秒发送一次电池设备的 GPE，我们无法在进入暂停前抑制这个 GPE，因为电池 GPE 与盖子 GPE 共享一个编号。所以如果我们想禁用那些嘈杂的电池 GPE，我们就必须牺牲用盖子唤醒系统的功能，这不好。

向 SPMC 提示进入暂停，会让 EC 仅每分钟发送一次电池 GPE，考虑到我们能快速退出并重新进入 S0i3，这完全没问题。让我们的系统每隔一段时间唤醒实际上对于跟踪我们在挂起到空闲期间的功耗和电池状态随时间的变化是相当方便的（尽管我们可能可以用 RTC 警报实现类似功能，但我离题了）。

由于 CPU 可能会因各种不一定意味着我们想唤醒整个系统的原因从空闲状态唤醒，我们在 CPU 空闲时将其置于“s2idle 循环”中。这允许我们实际查看从系统获得的 GPE 并决定是否值得唤醒。这个 GPE 可能像嘈杂的电池事件一样简单，也可能像来自 NIC 的局域网唤醒事件一样复杂，如前所述，我们可能想在决定是否唤醒系统之前更详细地检查数据包。

总而言之，在启用 S0ix 的系统上进入暂停的完整流程如下：

* 暂停所有用户空间执行和挂载的文件系统。
* 遍历设备层次结构并暂停所有设备，通常通过将它们设置为 D3 状态之一，并至少将它们设置为 SPMC 报告的 S0ix 所需的最低 D 状态。
* 停止所有 CPU 上的调度器时钟并禁用所有中断，除了 SCI。
* 启动“s2idle 循环”并让 CPU 空闲。

恢复几乎是前三个步骤的逆向。

现在，亲爱的读者，在这一点上，你可能会对自己说：“好吧，这很好，但我不需要我的笔记本电脑在睡眠时响应通知。为什么我要费心这个新的 S0ix 东西，而我可以坚持使用 S3？”，对此的不幸回答是，现在大多数现代系统根本不再提供 S3 支持，完全用 S0ix 取代了它。

对供应商来说，同时支持两者非常复杂，所以大多数供应商都不费心，尤其是考虑到大多数主要操作系统现在都很好地支持 S0ix。幸运的是，FreeBSD 正在顺利添加对挂起到空闲和 S0ix 的支持。

你可以通过检查 `kern.power.supported_stype` sysctl 来查看你的系统是否支持挂起到空闲，并且你可以通过 `hw.acpi` sysctl 树配置 ACPI 在某些事件上进入挂起到空闲。然后，如果你的系统支持它并且 FreeBSD 支持它，S0ix 状态应该会自动进入。

目前，没有统一的方法来检查这一点，但如果你在带有 SMU 芯片的 AMD 系统上，你可以使用前面提到的 SMU sysctl。

## 休眠和 ACPI S4

在所有这些让 S0i3 工作起来的努力之后，你可能会有些失望地了解到，在 S0i3（以及 S3）中，你甚至没有获得很好的节能效果。在这方面，系统之间存在很大差异，但系统在 S0i3 中只能持续几天并不罕见。这就是休眠（或挂起到磁盘）的用武之地。

当挂起到内存（如 S3 或 S0i3）时，你的机器仍然必须不断刷新 DRAM，这不是一个微不足道的功耗来源，并且随着你向系统添加更多内存，这实际上会增加。通过挂起到磁盘，你将内存复制到硬盘并实际上完全关闭计算机 —— 包括 DRAM —— 就像你正在关闭它一样（好吧，实际上，你正在关闭它）。这意味着你的机器几乎不消耗任何电力。唯一仍在消耗电力的组件是 EC（如果你的机器有的话），但这与你的机器断电时的情况相同。

在 ACPI S 状态术语中，休眠是 S4。

在最开始，当恐龙还在地球上漫游时，BIOS 供应商通过一种称为 S4BIOS 的东西在固件中实现了这一点，以简化采用。在这种安排中，BIOS 完全负责将 OS 内存复制到磁盘，就像 APM 之前的挂起到 RAM 一样。

虽然 FreeBSD 支持 S4BIOS，但它早就无关紧要了，因为现在大多数操作系统只支持常规的 S4 休眠。在 S4 中，操作系统完成了将其内存复制到磁盘的大部分工作，而固件基本上只是关闭机器。事实上，你完全可以在没有固件 S4 支持的情况下实现休眠，只需使用平台的关机功能（这意味着 ACPI 平台上的 S5，或非 ACPI 平台上的其他功能）。

休眠的主要缺点是进入和退出时的延迟。你必须等待操作系统将所有页面复制到磁盘，然后在恢复时，经历大部分启动过程，最后将这些页面从磁盘恢复到内存。但我们实际上可以将 S0ix 和休眠的优点结合成一种混合方法！

通过设置 RTC 警报，我们可以让系统在 S0i3 中停留——例如一个小时——然后再次唤醒并进入休眠，如果用户在此期间没有唤醒系统。这意味着如果在暂停后不到一小时内唤醒，我们会从 S0ix 获得超快的恢复时间，但如果我们让系统暂停过夜或多天，我们会获得休眠带来的大幅改善的电池寿命。这意味着如果在一小时内没有唤醒它，下次唤醒机器时恢复会花费更长的时间，但考虑到所有因素，这是一个相当好的折衷方案。

我们还可以用这种混合方法做其他很酷的事情，比如配置 ACPI `_BLT`（电池电量阈值）控制方法，当电池达到某个阈值（比如 5%）时唤醒机器，这样如果我们在 S0ix 中电池耗尽，就可以进入休眠并防止硬关机。

## FreeBSD 中的现状

FreeBSD 基金会作为其“笔记本电脑支持和可用性项目”的一部分，一直在带头赞助 S0ix 和休眠的工作。大部分挂起到空闲和 S0ix 代码落地，休眠最近启动，但进入 S0i3 尤其是从它恢复还不完全可靠，并且让 SMU 正常工作的 USB4 驱动程序的必要部分尚未落地。

此外，大部分焦点都放在 AMD 系统上，而不是英特尔（尽管最近开始了对初始 PMC 驱动程序 —— 相当于 SMU 的驱动程序 —— 的工作）。即使在 AMD 平台中，到目前为止唯一的测试也发生在 Phoenix 上，`amdsmu` 驱动程序理论上只支持 Cezanne、Rembrandt 和 Phoenix 芯片。

唯一使用正在开发中的 USB4 驱动程序（`thunderbolt`）测试过的 USB4 控制器是 Pink Sardine（Phoenix 系统上搭载），尽管任何通用无怪癖的 USB4 控制器也应该得到支持。

在撰写本文时，大部分剩余工作都集中在可靠地进入最深的 C 状态（Phoenix 系统上的 C3），因为 CPU 经常意外唤醒，这反过来导致系统退出 S0i3。但愿如此，FreeBSD 很快就会支持现代待机和休眠，完全追赶上 Linux 和 Windows！

有关 S0ix 及其在 FreeBSD 中的实现的更技术性、更聚焦的介绍，以及一些额外的参考资料，你可以查看我的博客：<https://obiw.ac/s0ix>

***

**Aymeric Wibo** 是一位来自比利时的软件自由职业者，专门从事内核开发和图形编程。他从高中开始就参与 FreeBSD，目前为 FreeBSD 基金会工作，负责笔记本电脑电源管理，并在 Klara Inc. 工作。


# 基金会的笔记本电脑支持与可用性项目是如何形成的

* 原文：[How the Foundation’s Laptop Support & Usability Project Came Together](https://freebsdfoundation.org/our-work/journal/browser-based-edition/laptop-desktop/how-the-foundations-laptop-support-usability-project-came-together/)
* 作者：**Deb Goodkin**

多年来，我们不断从用户那里听到同样的反馈：是的，FreeBSD 能在笔记本电脑上运行，但实现这一点往往很困难。人们可以让它工作，但通常需要更多的故障排除、耐心和时间，远超应有的程度。对于学生、新用户和试图将 FreeBSD 作为日常工作一部分的开发人员来说，这种摩擦确实影响了他们。

这不仅仅是偶尔的反馈。我们在邮件列表、论坛、博客文章、社交媒体以及与用户的直接对话中都看到了这种情况。我们也从商业用户那里听到了同样的反馈，特别是那些希望他们的开发人员能够在 FreeBSD 上构建产品，同时在自己的系统上使用 FreeBSD 的用户。在基金会，我们也看到了同样的情况，合作学生和实习生试图在笔记本电脑上安装 FreeBSD 时遇到了类似的问题。

时间一长，我们逐渐意识到这并不只是反复出现的抱怨。这阻碍了新用户采用 FreeBSD，也制约了社区成长。

## 认识到需求

退一步从更宏观的视角看，笔记本电脑支持显然是基金会能真正发挥作用的领域。更好的笔记本电脑支持不仅会改善当前用户的体验，还可以帮助学生、开发人员和新用户更轻松地入门，让 FreeBSD 成为更实用的日常选择。

这很重要，因为人们每天使用的系统往往会影响他们是否坚持使用某个平台、向他人推荐它，或者最终为它做出贡献。

像许多基金会的工作一样，这并不是从一个大型、全额资助的项目开始的。我们从力所能及处着手，首先关注那些最令人沮丧的问题。无线是最明显的例子之一。几乎任何人被问到最想在笔记本电脑上修复什么，无线都名列前茅。因此它成为我们首批资助的领域。

## 将广泛目标转化为更具体的内容

大约在同一时间，基金会董事会正在讨论长期战略目标以及可能对 FreeBSD 产生最大影响的工作类型。其中一个目标是增加采用率。

这是一个广泛的目标，只有将其转化为更具体的内容时才有意义。我们将其化为一个简单的问题：如何让 FreeBSD 更易于大学生作为日常主力机使用？

这让我们能以更实际的方式思考这项工作。也帮助我们理清了对笔记本电脑支持的思路。这不仅仅是修复一些孤立的技术问题。关键在于改善整体体验，让 FreeBSD 在现代笔记本电脑硬件上用起来更顺手。

## 构建项目

当我们开始更全面地规划这项工作时，发现需求远远超出了我们一次性所能资助的范围。早期估计成本超过一百万美元。这超出了基金会当时的能力范围。

即便如此，我们知道这项工作很重要，所以我们继续在力所能及处推进。我们增加了资金，引入了用户空间开发人员，并在资源到位时持续投入。

然后我们收到了一封改变局面的电子邮件。它简短而直接。有人写信说他们喜欢 FreeBSD，使用了 25 年以上，并希望资助相关工作，解决他们遇到的一些限制。这种支持是项目得以扎实推进的重要因素。

笔记本电脑支持与可用性项目于 2024 年第四季度正式启动。

## 项目涵盖内容

从一开始，该项目就专注于最直接影响笔记本电脑体验的领域：Wi-Fi、图形、音频、安装程序和睡眠状态。这些因素决定了一台系统是仅仅能运行，还是足够好用、可以日常使用。

2025 年是该项目的第一个完整年度，基金会为此投入了超过 75 万美元。这笔投资带来了真正的进展。

在 Wi-Fi 方面，2025 年带来了对关键硬件上 Wi-Fi 4 和 5 的支持，并启动了 Wi-Fi 6 的工作。适用于 Intel 和 Realtek 的 Wi-Fi 4 和 5 驱动程序在 FreeBSD 15.0 中可用，额外的 Realtek 和 Mediatek 支持仍在进行中。

显卡支持也取得了重要进展。显卡驱动程序已升级到 Linux 6.10，该版本在 15.0 中提供。Linux 6.11 正在等待最终审查，6.12 的工作正在进行中。

音频在 15.0 中也有重要改进。用户现在可以使用新的 sndctl.8 和 mididump.1 实用程序，以及错误修复、更广泛的笔记本电脑支持，并初步改善了 HDA 声卡的自动声音重定向。

安装程序也针对笔记本电脑用户的需求做了改进。在 FreeBSD 15.0 中，安装程序现在支持在基本系统安装完成后下载和安装固件包。在 15.1 中，用户还可以在安装过程中安装 KDE 图形桌面环境。

睡眠状态仍然是该项目的另一个主要部分。现代待机（S0i3）的工作是 15.1 的一部分，而休眠（S4）则属于更广泛工作的一部分。这项工作还包括相关领域，例如从现代待机过渡到休眠、处理休眠期间的磁盘加密，以及改进虚拟机的睡眠状态行为。

2026 年的工作将以类似的投资水平和范围继续进行。当前的重点领域包括睡眠状态、升级至 Linux 6.18 的图形驱动程序、Wi-Fi 6 支持、USB4 和 Thunderbolt 支持、HDMI 改进、UVC 网络摄像头支持以及蓝牙改进。

更广泛的测试工作也是 2026 年工作的一部分。让单个组件能工作是一回事。测试这些部分如何在一系列真实硬件上协同工作是另一回事。如果该项目要在实践中真正有用，这种测试很重要。

## 幕后发生的事情

对于这样一个项目，很容易忽略在开发工作真正推进前，幕后还要做多少工作。需要制定计划。必须有人协助管理这项工作并推动其进展。我们必须找到具备合适技能、有空并愿意做这项工作的开发人员。作为一家非营利组织，我们总是在所需与有限资金之间取得平衡。

这对这个项目和基金会资助的更广泛工作都是如此。

当我们决定是否资助一个项目时，我们会考虑几个基本因素。它对 FreeBSD 是否有益、是否能带来影响？它对用户是否实用、是否易于上手？它是否需要基金会可以提供的那种基础设施和支持？我们是否有资金稳妥推进？

同样的现实也适用于社区提案。有些提案向前推进，有些则没有。这不是因为我们不关心或不感兴趣。我们知道人们在这些提案上投入了大量时间和思考。资金有限，我们必须确保工作契合整体规划并产生实质影响。

## 为什么这项工作很重要

基金会的预算反映了这些优先事项。当前预算的 62% 以上分配给直接用于改进 FreeBSD 的软件开发。另外 17% 支持宣传和教育，包括外展、讲故事和社区教育。基金会还继续投资于基础设施，包括支持该项目长期发展所需的系统和硬件。

笔记本电脑支持与可用性项目不仅关乎笔记本电脑。关键在于让 FreeBSD 更好用、更易上手、更实用，契合那些能把人引入社区并留住他们的日常使用场景。

这项工作的成形过程也是同类工作的一个范例。需求明确，社区反复提出，随着时间推移，资金、规划和组织架构逐步到位，真正的进展才成为可能。

从本质上讲，这个项目体现了社区与基金会协作所能达成的成果。社区不断提出问题，分享实际使用经验，测试什么有效什么无效，并帮助明确哪里的需求最大。基金会将这些反馈转化为有资金支持、有组织的工作，并推动其向前推进。这种进步不是孤立发生的。只有社区成员、开发人员、捐赠者、测试人员和基金会共同参与、齐心建设，这种进步才会发生。

***

**Deb Goodkin** 是 FreeBSD 基金会的执行董事，于 2005 年作为第一位员工加入。当她不管理基金会或折腾 FreeBSD 时，你会发现她在陪她的狗玩耍、在博尔德的乡间小路上跑步，或读一本好书。


# TCP/IP 历险记：改进 TCP 对重排序的响应

* 原文：[Adventures in TCP/IP: Improving TCP’s Responses to Reordering](https://freebsdfoundation.org/our-work/journal/browser-based-edition/laptop-desktop/adventures-in-tcp-ip-improving-tcps-responses-to-reordering/)
* 作者：**Randall Stewart**

本专栏与本系列中的其他专栏略有不同。它介绍了使用 TCP 时出现的一系列问题，并详细描述了如何排查这些问题，展示了之前专栏中介绍的信息。它还将参考本系列之前专栏中介绍的一些 TCP 机制。最后，将把所有内容联系起来，不仅展示问题是如何解决的，还展示由于发现并修复了驱动程序 bug，RACK 栈如何变得更能抵抗数据包重排序。

## 问题

去年某个时候，Drew Gallatin 向我提出了他遇到的 TCP 问题。问题始于他尝试使用 FreeBSD 软件包工具 (pkg) 在新位置下载一组要更新的软件包。速度慢得令人痛苦。他期望达到 Mbps 级别的速度，但实际上下载速度极其缓慢。于是，他开始使用他可以访问的另一个 FreeBSD 站点进行测试。这促使他尝试改用 RACK 栈，因为过去对他来说性能更好。果然，正如他告诉我的，使用 RACK 栈的性能是 FreeBSD 默认栈的三倍。但 10 kbps 的三倍只有 30 kbps，而他期望的性能至少比这高两个数量级。他将这个问题发给我，询问为什么 RACK 栈和 FreeBSD 栈中的 TCP 在他的“高速”互联网上表现如此之差。

### 识别问题

正如在关于黑盒日志记录的专栏 \[1] 中所讨论的，黑盒日志记录 (BBlog) 提供了详细的调试信息，尤其是对于 RACK 栈。首先要在发送方和接收方都启用 BBlog。有关如何启用 BBlog 的详细说明，请参阅该专栏。

针对 Drew 的情况，我最终为他提供了几个自定义的客户端-服务器程序，这些程序会发送并丢弃文件，同时启用 BBlogs。然后 Drew 运行 BBlog 收集器 (tcplog\_dumper) 和特定程序来发送大小相当的文件，类似于他试图下载到新位置的文件。获取 BBlogs 后，他便让我阅读并解读。我不会用 BBlog 包含的大量文本来烦你，但它们很快就描绘了发送方和接收方的状况，立刻揭示了 TCP 层发生的情况。基本上，发送方会以大约 10 MSS（14,600 字节）的初始窗口开始，并向内部驱动程序 TSO 机制发送突发数据：

![TCP 发送方突发数据示意图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-7ae698c779f5f0645429dfd879c059b90238024c%2Fadventures-in-tcp-ip-improving-tcps-responses-to-reordering-01.png?alt=media)

数据包将穿过互联网并到达 TCP 接收方，如下所示：

![TCP 接收方数据包重排序示意图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-60b95c85e68919b6e50c1ac8335725c752adee0e%2Fadventures-in-tcp-ip-improving-tcps-responses-to-reordering-02.png?alt=media)

接收方发送了 SACK 和 ACK，表明接收方按 P4、P4、P2，最后是 P1 的顺序处理了数据包。

现在，从 BBlogs 中，我无法确定重排序来自哪里——毕竟你只看到了 TCP 的视角。FreeBSD LRO 层以下（即驱动程序和网络）发生的情况在 BBlogs 中不可见（请注意，BBlogs 中有一些 LRO 日志）。正如 Bennett、Partridge 和 Shectman \[2] 所示，在网络中看到重排序也并不罕见。因此，我要求 Drew 调查为什么他的网络会出现大规模重排序——而且确实非常大规模。

似乎每 4-5 个数据包一组，都会以与发送顺序相反的顺序到达。因此，你会看到 10 个初始数据包的突发变成 P4、P3、P2、P1 的到达模式，然后是 P8、P7、P6、P5，接着是 P10 和 P9。这种行为会导致 FreeBSD TCP 栈不断进入恢复状态，因为它看到三个重复的 ACK/SACK，这会就在所有数据包的 ACK 到达时开始重传。一旦进入快速恢复，如 \[3] 中所讨论的，在确认所有在途数据后退出恢复，并最终将拥塞窗口减半，将 ssthresh 点（我们从慢启动切换到拥塞避免的点）设置为相同的值。

这意味着 FreeBSD TCP 栈永远无法真正长时间摆脱恢复状态，这很容易解释 Drew 看到的糟糕性能。一旦恢复结束，它会回到拥塞避免状态。每当拥塞窗口允许四包突发时，栈会再次看到三个重复的 ACK/SACK 并重新进入恢复状态。

### 问题的根本原因

Drew 是 FreeBSD 开发者（他喜欢处理驱动程序），经过一番调查后发现，实际上是驱动程序与 LRO 的接口存在错误，导致了重排序。基本上是硬件禁用了多队列，因此硬件停止计算 RSS 哈希，因为不再需要。然而，驱动程序仍然将 RSS 哈希标记为有效。FreeBSD LRO 代码使用 RSS 哈希对接收的数据包进行排序。这种错误标记导致 LRO 对数据包进行错误排序。修补他的驱动程序以将数据包按正确顺序发送到 LRO 后，RACK 和 FreeBSD 栈的性能都大幅改善，达到了他期望的数量级。用 BBlogs 帮助 Drew 找到这个问题很棒，但整个事件从 TCP 角度引发了我更多疑问。如果你不熟悉 RACK 栈，甚至可能在你脑海中产生一个疑问。

## 由 bug 引发的进一步问题

### 为什么 RACK 栈比 FreeBSD 栈表现更好？

对于那些不熟悉 RACK 的人，你可能想知道为什么 RACK 的性能是 FreeBSD 栈的三倍（尽管只有 30 kbps，但仍然有相当大的差异）。这是因为，如 \[4] 中所讨论的，RACK 栈具有防止重排序的保护措施。它通过结合使用计时器和丢失报告来实现这一点。当 SACK 到达表明数据丢失时，如果经过的时间不够长，它会等待重传。计时器通常是最新的 RTT 加上小的额外延迟，但当 RACK 通过 DSACK（DSACK 是表明 TCP 段被接收方多次接收的 SACK）看到网络中的重排序时，它会将这个计时器扩展到最近 RTT 的两倍。这意味着在初始恢复后，当额外延迟较小时，DSACK 的到达会增加 RACK 延迟计时器，足以使其不会不断进入恢复状态，而是等待足够长的时间让所有确认流回。

### 为什么 RACK 栈仍然如此缓慢？

对我来说，这是这个 bug 引发的根本问题。RACK 有这种奇妙的机制来防止重排序，那么为什么在初始恢复后，RACK 没有迅速加速以达到 Drew 预期的兆比特每秒？通过深入研究 BBlog，我最终找到了答案。

这个问题要回到拥塞控制的工作原理。一旦因丢失/恢复事件退出初始慢启动，ssthresh 就设置为 5 个数据包（初始 10 个减半）。这意味着 RACK 永远处于拥塞避免状态。这对 TCP 造成了显著的拖累。在拥塞避免状态下，每次确认一个完整拥塞窗口的数据时，你会将拥塞窗口增加一个 MSS。所以，你发送并确认了五个数据包，现在将拥塞窗口提高到六个数据包。现在你发送并确认了六个数据包，然后将其提高到七个。这一直持续到发生其他错误或连接数据全部发送完毕，连接完成。

因此，看看他的往返时间（大约 45 毫秒），这意味着每 45 毫秒你会将拥塞窗口增加一个数据包。这听起来不错，直到你意识到要充分利用 1,000 Mbps 的网络，在 45 毫秒 RTT 下，你的拥塞窗口需要打开到大约 3,850-3,900 个数据包。所以，RACK 需要大约 380 次往返才能开始充分利用他的网络，这大约需要 170-180 秒。如果他只有一次超大传输，这在整体大局中不会太明显，但每次传输完成时，它会启动新的 TCP 连接，再次遇到相同的问题。这让他的“快速”网络在他看来非常缓慢。

所以，这让我产生了根本性的疑问。我们如何改变 RACK 栈以更好地从这种情况中恢复？

### 现在集成到 RACK 栈中的改进是什么？

所以，我想到一种方法，让 RACK“重新进入”慢启动（请注意，慢启动在我看来名不副实，因为它是指数增长，每个 RTT 拥塞窗口翻倍），这样它可以更快地打开窗口。回顾初始情况时，RACK 会看到以下情况：

![RACK 栈重传时序示意图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-5b91039c5cd27d29c471a70787be2dd9879d32d5%2Fadventures-in-tcp-ip-improving-tcps-responses-to-reordering-03.png?alt=media)

这里的关键是时间标记 T3（与传输时间相比）刚好比 RTT 长一点，让 RACK 知道该重传数据包了。这是因为 RACK 到目前为止还没有看到任何重复的确认，并且从初始发送到 T3 的时间长于 RTT 加上 RACK 使用的小初始增量。但更重要的是，T4——移动累积点到 P4 的确认实际到达——用时远少于 RTT。基本上，T4 和 T3 之间的时间总是少于 RTT 的一半。这为如何调整 RACK 以应对这种情况提供了思路。

RACK 现在跟踪已重传的数据量（以字节为单位）。任何时候到达的 SACK 或 ACK 只涉及一次重传，并且确认（SACK 或 ACK）距离重传发送的时间少于 SRTT 的一半，它会减少该计数。如果计数减少来自 ACK（累积确认点向前移动）并且计数降至零，这意味着我们遇到了实际进入恢复是误判的情况，我们应该恢复之前的 ssthresh 和拥塞窗口，并重新开始慢启动。

为 RACK 添加上述小改动后，其糟糕的性能会接近没有重排序时的表现，并让 RACK 栈更好地防止重排序。

1. R. Stewart, M. Tüxen, “Adventures in TCP/IP: TCP Black Box Logging”, in The FreeBSD Journal May/June, 2024.
2. C. R. Bennett, C. Partridge and N. Shectman, “Packet reordering is not pathological network behavior,” in IEEE/ACM Transactions on Networking, vol. 7, no. 6, pp. 789-798, Dec. 1999.
3. R. Stewart, “Dynamic Goodput Pacing: A New Approach to Packet Pacing”, in The FreeBSD Journal November/December, 2024.
4. R. Stewart, M. Tüxen, “RACK and Alternate TCP Stacks for FreeBSD”, in The FreeBSD Journal January/February, 2024.

***

**RANDALL STEWART** (<rrs@freebsd.org>) 从事操作系统开发 40 余年，自 2006 年以来一直是 FreeBSD 开发者。他专门研究包括 TCP 和 SCTP 在内的传输层，但也以涉足操作系统的其他领域而闻名。他目前是一名独立顾问。


# 复制你的系统：使用 duplicity 备份你的 FreeBSD 桌面

* 原文：[Duplicating your System Using duplicity to back up your FreeBSD desktop](https://freebsdfoundation.org/our-work/journal/browser-based-edition/laptop-desktop/duplicating-your-system/)
* 作者：**Jason Tubnor**

你刚刚安装了新的 FreeBSD 桌面（或笔记本电脑），把它调成了你喜欢的样子，正要开始创作新小说，或把你最喜欢的软件供应商刚发布的新软件移植到 FreeBSD。然后你意识到，只需一个有缺陷的 NVMe 控制器，你就可能面临数据完全丢失的风险，你的所有工作和时间都会随之消失。

在现代，我们有 ZFS 快照和复制功能，使得备份任务变得极其简单。然而，你可能无法访问另一个 ZFS 端点进行复制，需要通过使用非 ZFS 工具进行备份来分散风险，或者你只是使用 UFS2，过着复古的生活。

对于这些情况，首选是依靠 Tarsnap，因为它易于使用且简单，使恢复与备份一样容易。但某些情况需要不同的方法。也许你公司有严格的防火墙，不允许 Tarsnap 数据流从公司网络流出，或者你可以方便地访问内部存储端点，例如兼容 S3 的对象存储或具有 SFTP 访问权限的大文件存储位置。

当你面临后者时，duplicity（Ports 中的 sysutils/duplicity）工具可以作为易于安装的软件包安装到你的 FreeBSD 系统上：

```sh
# pkg install duplicity par2cmdline
```

在上述情况下，还安装了 par2cmdline 软件包，因为它不是 duplicity 的依赖项，但本文（以及 duplicity）会用到它来避免备份存档中的位腐烂。

Duplicity（<https://duplicity.gitlab.io/>）将把你的各种文件和文件夹备份到加密的 tar 格式卷中，同时使用 librsync 来提高压缩率并对数据执行差分操作。虽然它没有 Tarsnap 那些真正出色的功能，但确实提供了一个适用于许多用例的替代方案。

虽然 duplicity 有许多目标存储选项，但我们将在这里专注于使用兼容 S3 的对象存储，因为它可以从许多云提供商那里轻松获得，具有不同的成本、访问速度和耐用性。duplicity 的工作方式更像是一种交互式方法（尤其是在清理旧数据集时）。虽然你可能成功地将备份提交到 AWS Glacier 或类似的对象存储，但数据的恢复和管理将极其缓慢、复杂且昂贵；在此先提醒你。不要追求冷、便宜的存储；选择稍高/更热的存储以获得更好的体验。虽然它是交互式的，但当从 cron 调用、在脚本中执行或在交互式命令行上使用时，它会完美工作。

开始之前，我们需要定义备份数据的参数和最佳实践。关键考虑因素是：

* 每月新的完整备份 — 一长串增量备份虽然备份速度快，但可能会超出你的恢复时间目标（RTO）。引入频繁的完整备份将确保 RTO 窗口保持较低。
* 加密你的备份 — 你正在备份到别人的计算机上。除非你控制远程计算机，否则数据应该在你的计算机上加密，然后再通过网络传输。
* 不要信任端点耐用性 — 使用 par2 功能在备份中加入奇偶校验块，以防位腐烂。默认值为 10%，这可能会增加大型存档的成本。评估你的风险并相应调整。在本示例中，我们将使用 5%。

准备 AWS 环境变量。这些可以放入你主目录中的 **.duplicity.env** 文件中。如果你在多用户系统上，确保 **.duplicity.env** 文件的权限为 `chmod 0600`，以防止其他系统用户获得对私有 API 密钥的未授权访问：

```sh
~/.duplicity.env

export AWS_ACCESS_KEY_ID="<key_id>"
export AWS_SECRET_ACCESS_KEY="<access_key>"
export AWS_ENDPOINT_URL="https://sg-s3.storage.bunnycdn.com"
```

然后加载我们的环境文件以供使用：

```sh
$ . .duplicity.env
```

在我们开始使用 duplicity 之前的最后一个要求是，当使用 GPG 时（默认使用加密），为你的数据的对称加密定义一个 PASSPHRASE 密钥。可以使用 GPG 公钥加密，但这超出了本文的范围（有关更多信息，请参阅 duplicity 手册页）。PASSPHRASE 变量也可以在我们上面使用的环境文件中定义，然后从你的备份脚本中调用。为了便于说明，我们将在此处导出：

```sh
$ export PASSPHRASE="4640f58235c4ff7ad359fdcb5f2d8ac48f71c26bcff4a40f6d85503d0288a45e"
```

现在我们准备在新配置的系统上执行主目录的第一次备份：

```sh
$ duplicity backup –full-if-older-than 1M –exclude ./.cache \
–par2-redundancy 5 ./ par2+s3:///mybucket-s3backup/computer
```

上述命令启动 duplicity 的备份组件，如果前一个完整备份超过一个月，则创建一个完整备份，排除 **.cache** 目录，将 par2 冗余设置为 5%，从当前目录（用户主目录的根目录）获取所有文件和文件夹，并将备份写入 mybucket-s3backup 存储桶中的 **/computer** 文件夹，使用 par2 包装器进行块冗余。

duplicity 完成运行后，你将看到备份的统计信息：

```sh
Last full backup date: none
Last full backup is too old, forcing full backup
————–[ Backup Statistics ]————–
StartTime 1773460673.41 (Sat Mar 14 14:57:53 2026)
EndTime 1773460673.42 (Sat Mar 14 14:57:53 2026)
ElapsedTime 0.01 (0.01 seconds)
SourceFiles 15
SourceFileSize 6302 (6.15 KB)
NewFiles 15
NewFileSize 6302 (6.15 KB)
DeletedFiles 0
ChangedFiles 0
ChangedFileSize 0 (0 bytes)
ChangedDeltaSize 0 (0 bytes)
DeltaEntries 15
RawDeltaSize 6277 (6.13 KB)
TotalDestinationSizeChange 3884 (3.79 KB)
Errors 0
————————————————-
```

由于这是一个干净的系统，我们的主目录还没有太多活动，并且相当小，所以上面的备份没有花很长时间执行。我们备份的初始总量：

```sh
$ du -sh .
62K    .
```

如你所见，即使在小数据集上，62KB 的文件（分配的磁盘空间），6.15KB 是实际文件总量，考虑到压缩和加密后，目标大小为 3.79KB。

测试表明，duplicity 的工作速度与你的硬件允许的一样快；限制将由你的对象存储速度和网络出站吞吐量的大小决定。要观察备份的执行情况，请在命令行上执行 duplicity 时使用 `–progress` 开关。

此时，你的数据在远程存储上是加密的，如果你丢失了对称加密密钥（PASSPHRASE），即使是你也无法访问。花时间将密钥的副本放在安全的地方，以防你需要将数据恢复到新系统。

让我们查看我们第一次备份中的内容：

```sh
$ duplicity ls par2+s3:///mybucket-s3backup/computer/

Last full backup date: Sat Mar 14 14:57:52 2026
Sat Mar 14 14:00:35 2026 .
Sat Mar 14 13:33:22 2026 .boto
Sat Mar  7 15:55:14 2026 .cshrc
Thu Mar 12 22:15:04 2026 .gnupg
Thu Mar 12 22:15:04 2026 .gnupg/common.conf
Thu Mar 12 22:15:04 2026 .gnupg/private-keys-v1.d
Sat Mar 14 14:56:10 2026 .gnupg/random_seed
Sat Mar 14 14:00:35 2026 .lesshst
Sat Mar  7 15:55:14 2026 .login
Sat Mar  7 15:55:14 2026 .login_conf
Sat Mar  7 15:55:14 2026 .mail_aliases
Sat Mar  7 15:55:14 2026 .mailrc
Thu Mar 12 22:13:12 2026 .profile
Sat Mar 14 13:47:32 2026 .sh_history
Sat Mar  7 15:55:14 2026 .shrc
```

duplicity 中的 list (ls) 命令显示存档，类似于 `tar -t`。同样，这里已经使用 par2 包装器连接到 mybucket-s3backup，以验证每个块的一致性并实时修复。

现在是时候在我们的主目录中开始工作了。让我们先克隆 Ports 分支：

```sh
$ git clone –depth 1 https://git.freebsd.org/ports.git
Cloning into 'ports'…

$ du -sh .
1.0G    .
```

这大大增加了我们的主目录。现在该备份我们涉及多个 Ports 的工作了：

```sh
$ duplicity backup –full-if-older-than 1M –exclude ./.cache \
–par2-redundancy 5 ./ par2+s3:///mybucket-s3backup/computer

Local and Remote metadata are synchronized, no sync needed.
Last full backup date: Sat Mar 14 14:57:52 2026
————–[ Backup Statistics ]————–
ElapsedTime 102.56 (1 minute 42.56 seconds)
SourceFiles 214568
SourceFileSize 682126695 (651 MB)
NewFiles 214554
NewFileSize 682120407 (651 MB)
RawDeltaSize 681815400 (650 MB)
TotalDestinationSizeChange 224163465 (214 MB)
————————————————-
```

如你所见，duplicity 非常高效。我们的备份仅增长了 214MB，即使当 Ports 树被检出时，我们的系统报告为 1GB。

恢复备份与备份一样简单：

```sh
$ duplicity restore –path-to-restore ports \
par2+s3:///mybucket-s3backup/computer /tmp/temp/ports
Local and Remote metadata are synchronized, no sync needed.
Last full backup date: Sat Mar 14 14:57:52 2026
```

此命令仅从之前使用 par2 包装器备份到 S3 存储桶的主目录中恢复 Ports 目录。Ports 目录的目标位置将是 **/tmp/temp/**

```sh
jtubnor@disk:~ $ du -sh /tmp/temp && ls -lsa /tmp/temp

1.0G    /tmp/temp
total 18
1 drwxr-xr-x   3 jtubnor wheel    3 Mar 14 16:56 .
9 drwxrwxrwt  16 root    wheel   16 Mar 14 16:57 ..
9 drwxr-xr-x  70 jtubnor jtubnor 82 Mar 14 15:18 ports
```

上面的输出显示，我们的 1GB Ports 目录现在已被恢复到 **/tmp/temp** 目录。如果你遇到了完整的机器故障，恢复你的主目录就像这样简单：

```sh
$ cd / && duplicity restore par2+s3:///mybucket-s3backup/computer /home/jtubnor/
```

你需要确保你当前不在要恢复到的目录中；这就是为什么我们在发出恢复命令之前先切换到根目录。

本文几乎没有触及 duplicity 中包含的许多功能的表面。如果需要，通过必要的调整，它甚至适用于整个系统配置备份。有关功能和用例示例的更多信息，请查看 duplicity 手册页（<https://duplicity.gitlab.io/stable/duplicity.1.html>）

Duplicity 是一个小型、成熟的程序，只有少数依赖项，但它在大多数情况下都能很好地工作，并且可以在任何 FreeBSD 系统上轻松安装。

***

**Jason Tubnor** 在广泛的学科领域拥有 30 余年的 IT 行业经验，目前是 Latrobe Community Health Service（澳大利亚维多利亚州）的技术运营经理。Jason 在 1990 年代中期接触了 Linux 和开源，2000 年又接触到 OpenBSD，此后他使用各种 BSD 解决不同行业组织的问题。Jason 还是每周 BSDNow 播客（[https://bsdnow.tv](https://bsdnow.tv/)）的联合主持人。


# 读者来信

哦，AWOL，我多汁的夏日孩子，

你是最新款的 Human™，数字原住民，理论上被人类物种积累的智慧所启迪。你那些有益身心健康、言之有据且切实可行的建议来源就如此匮乏，以至于向一位饱经沧桑的系统管理员求助显得明智？没错，这是个建议专栏。我提供建议。我也提供环锯术和实验性流行病学服务，但没见谁排队体验这两样，对吧？互联网上有 Captain Awkward、Doctor Nerdlove、Dear Prudence、Ask A Manager 和 Dear Abby，而你却决定不，你要活得离经叛道，于是向一位以给老鼠驾驶的车辆加装一氧化二氮助推器闻名的系统管理员乞求“智慧”。如果你来自文明国家，你甚至有医疗保障，可以咨询心理医生，或者我应该说密集心理咨询，因为在你第一次咨询开始几分钟后，心理医生就会按下那个红色大按钮，你将拥有一个舒适的新家，一日三餐热饭，有自己一张床，保证 98% 没有节肢动物侵扰，还有精心定制的药物方案，因为毕竟你认为系统管理员会为美好生活提供明智建议。相反，你却向一个类人生物寻求指导——这家伙用装在腰部的分体式键盘工作，因为他实施的计算量之大已对其肉身造成了不可逆的生理改变。

好吧。你想要更好的生活。

放弃电脑。学会自给农业、家庭制药和养鸡。在大崩溃之后这些都会派上用场。大崩溃何时到来很难说，因为地球上每个有两年以上经验的系统管理员合力都难以排期，尽管我们这帮人很固执，所以毫无疑问会完成这项伟大工程，但我们中有些人正在用 AI 加速奔向永久停机，所以我建议你快点？AI 也许是个污水坑，但若你追求停机，它无可匹敌。

还在？行。你来到我的建议专栏，我就得提供建议。

我建议受苦。

弄一个干净、最小化的安装，不要任何花哨的桌面环境。弄一个基于控制台的网页客户端，比如 lynx.1。这样你就能访问邮件列表存档和支持论坛，而不用碰视频、社交媒体或那些吮吸时间的寄生虫。你想玩电子游戏？学会让图形控制台跑起来。或者玩 Nethack。

这不舒服。不舒服正是关键。

痛苦是最好的老师，但没人自愿上她的课。

拥抱痛苦。

使用 Unix 桌面比以前容易多了。KDE 和 Gnome 之类的窗口管理器提供点击式体验，与你声称要逃离的操作系统别无二致。这些是给那些想要商业操作系统体验却不想被剥削的人准备的，这有点像想要雨却不想淋湿。从 FVWM 这样的经典窗口管理器开始，会拓宽你对可能性的认知。OpenBSD 的 cwm 之类的窗口管理器会让你摆脱鼠标，让你意识到自己把多少时间浪费在伸手去抓那只啮齿动物上，并把你推下山，奔向你自己的肉身不可逆的生理改变。

用过几周不同的窗口管理器后，你会纳闷自己当初是怎么忍受那片点击式的商业荒原的。

当你着手其他任务时也请记住这一点。商业操作系统呈现的狭窄选项是轨道。它们只让你走一条路。当你脱离轨道步行前进，会发现一个充满为无数种大脑编写的软件的世界。试试这个程序，再试试那个。没错，尝试是痛苦的。

你想要图形化邮件客户端？

你想要观看受 DRM 保护的流媒体？

你想要做点真正荒唐的事，比如出版一本书？一本使用 **多种字体** 的书？

这些都是痛苦。痛苦会不断加剧，直到你解决根本问题（系统管理法则第 22 条）。你通过学习来克服每一种痛苦。你知道得越多，克服痛苦的能力就越强。练习足够多，你几乎能征服一切极致的、无法解决的剧痛，而忍受这些剧痛对于你生活中非计算部分（也就是“重要”的部分）是极好的训练。

不过，为了你的长进，我会给几条提示。如果你用的是较新的桌面或笔记本，大概率有至少 2GB 内存，因此可以用 ZFS。如果你在意单磁盘系统上的数据，给那些重要的数据分配独立的数据集，并设置 `copies=2`。这样就能启用 ZFS 自愈。镜像或 RAID-Z 之类的冗余磁盘配置本身已保留完整性数据以支持自愈。使用启动环境。如果你用的是低内存的小系统，只能用 UFS。UFS 虽然缺少 ZFS 的那些花哨功能，但自 1983 年以来它就像烘干机里的毛绒兔子一样被摔打。所有合理的以及大多数不合理的 bug 都被敲打出来了。你会没事的。

从控制台起步，逐步搭建出一个可用的桌面，这教给你的东西会超出你的想象。具体来说，它会教会你：你当初应该去买鸡。

有问题想问 Michael？ 发送至 <letters@freebsdjournal.org>

***

**Michael W Lucas** 是《Absolute FreeBSD》《Run Your Own Mail Server》和《$ git commit murder》的作者。腰部安装键盘这件事完全是真的。了解更多请访问 <https://mwl.io>。


# 基金会来信

感谢你今年与《FreeBSD 期刊》共度的时光。我们感谢读者，也感谢让每一期得以付梓的作者、编辑和志愿者。你的热情与付出，让《期刊》持续前行，也将项目各处的进展传播开来。**FreeBSD 基金会** 全体同仁祝你度过一个愉快、安宁的假期。

**Deb Goodkin** 执行董事 FreeBSD 基金会

![节日图片](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-56d2420f05eaccf060030ec7ecc32a11163e0e60%2Fletter-from-the-foundation-01.png?alt=media)


# 活动日历

* 作者：**Anne Dickison**

截至 2026 年 3 月的 BSD 活动。如有任何 FreeBSD 相关活动或对 FreeBSD 用户有吸引力的活动未在此处列出，请将详情发送至 <freebsd-doc@FreeBSD.org>。

![Code & Compliance](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-5fc7fd41e9bc9cee49c9231f0f7b35f3e0e4c49a%2Fevents-calendar-01.png?alt=media)

## Code & Compliance FOSDEM 特别版

2026 年 1 月 26 日

比利时布鲁塞尔

<https://www.eclipse-foundation.events/event/code-compliance-2026/>

欢迎来布鲁塞尔参加下一届 Code & Compliance 聚会。在这场开放、社区驱动的活动中，开源开发者、项目维护者和行业领袖齐聚一堂，深入探讨欧盟《网络弹性法案》，改进开源合规实践，并分享提升软件安全性的实用方法。

![FOSDEM](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-c3036e48d52e8135e9b695e0cc93a329bb43c323%2Fevents-calendar-02.png?alt=media)

## FOSDEM 2026

2026 年 1 月 31 日 – 2 月 1 日

比利时布鲁塞尔

<https://fosdem.org/2026/>

FOSDEM 是由志愿者组织的为期两天的活动，旨在推广自由和开源软件的广泛使用。FOSDEM 于 2026 年 1 月 31 日至 2 月 1 日举办，为开源和自由软件开发者提供交流、分享想法与协作的场所。活动以高度面向开发者著称，汇聚来自世界各地的 8000 多名开发者。今年还将设立 BSD 开发者房间和展台。

![SCALE 23X](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4e1f13edf5927c5b11dcb5e93f09cd0b2c25f5e3%2Fevents-calendar-03.png?alt=media)

## SCALE 23X

2026 年 3 月 5–8 日

美国加州帕萨迪纳

<https://www.socallinuxexpo.org/scale/23x>

SCaLE 23X——第 23 届南加州 Linux 年度博览会，是北美规模最大的社区主办的开源与自由软件大会。每年在大洛杉矶地区举办。基金会今年将设展参展。

![AsiaBSDCon](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-bcfaa7dbbb2e64200e07921f53e66b8ab2d06261%2Fevents-calendar-04.png?alt=media)

## AsiaBSDCon 2026

2026 年 3 月 19–22 日

中国台湾台北

<https://2026.asiabsdcon.org/>

AsiaBSDCon 是一场面向基于 BSD 系统用户和开发者的会议。这是一场技术大会，旨在汇集最优秀的技术论文和演讲，确保开源社区中的最新进展能传播给尽可能广泛的受众。


# FreeBSD 15.0：改进与特性

* 原文：[FreeBSD 15.0: Fixes and Features](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/freebsd-15-0-fixes-and-features/)
* 作者：John Baldwin

FreeBSD 社区持续推进 15.0 的发布。相较于 2023 年 11 月发布的 14.0，15.0 版本包含了大量新特性、改进和 bug 修复。下面列出了一些变更要点，但更多细节可参见 [发布说明](https://www.freebsd.org/releases/15.0R/relnotes/)。

## 改进项目结构

在过去两年中，FreeBSD 的开发者虽然合并了大量补丁，同时也重构了项目的若干流程和结构。这些变更旨在优化开发工作流程，合理规划开发者的时间。

在 14.0 发布后不久，Colin Percival 提出了对 FreeBSD 发布计划的若干修改。正如他在今年早些时候的 [期刊文章](https://freebsdfoundation.org/our-work/journal/browser-based-edition/downstreams/freebsd-release-engineering-a-new-sheriff-is-in-town/) 中详细说明的那样，新计划设定了主版本和次版本发布的固定节奏。15.0 是遵循该新计划的第一个主版本。

在去年的 BSDCan 大会上，FreeBSD 核心团队宣布成立了新的源代码管理团队来管理源代码仓库。将诸如 src commit 权限等任务委派给该团队，使核心团队能够将精力集中于整个项目的战略规划。

## 打包的基本系统

[**pkg(8)**](https://man.freebsd.org/pkg/8) 工具已被证明是一款成熟的二进制包管理系统。多年来，FreeBSD 一直使用 **pkg(8)** 来管理从 Ports 构建的第三方包。在过去几年中，一组开发者致力于使用 **pkg(8)** 为基本系统提供二进制更新。这项工作既包括对 **pkg(8)** 工具本身的增强，也包括对基本系统构建流程的修改，以便与 **pkg(8)** 工具集成。15.0 将是首个通过 **pkg(8)** 支持基本系统二进制更新的主版本。由 **freebsd-update(8)** 管理的旧式发布集系统在 15.x 中仍将得到支持，使终端用户能够在这些系统之间平滑过渡。FreeBSD 的开发者预计将在下一个主版本中切换安装器，使其使用打包的基本系统。

## 将开发精力集中于未来系统

开发者的时间和精力是稀缺资源。为了提供高质量的系统，FreeBSD 长期以来一直专注于当代且被广泛部署的系统。在最近几个主版本中，FreeBSD 选择逐步弃用对一些在业界使用率下降、且开发者支持有限的旧 CPU 架构的支持。14.0 已弃用若干 32 位架构，这些架构在 15.0 中将不再作为独立架构受到支持，例如 32 位 x86 和 32 位 PowerPC。这两种架构的 64 位版本在 15.0 及以后版本中仍将继续支持运行 32 位二进制程序。然而，这些架构的 32 位内核在 15.0 中已不再受支持，并且 15.0 将不再提供相应的发布产物，例如安装镜像。

## 网络

15.0 包含了对新型网络设备的支持和对 TCP 的改进。Nvidia 贡献了对内联 IPsec 卸载的支持，使智能 NIC 能够将 IPsec 的加密 / 解密从主机 CPU 卸载到 NIC 上。这类似于内核 TLS 卸载，但针对的是 IPsec。**mlx5en(4)** 驱动在 ConnectX-6 及之后的适配器上支持 IPsec 卸载。15.0 中对本地（UNIX 域）套接字做了重构，从而提升了本地流式套接字的吞吐量并降低了延迟。

## 存储

即将发布的版本中包含了多项新的存储特性。三星贡献了用于通用闪存存储（Universal Flash Storage, UFS）标准的驱动，这是嵌入式闪存存储中使用的 eMMC 标准的替代方案。该驱动的作者 Jaeyoon Choi 在本期的《Universal Flash Storage on FreeBSD》中对此有更为详细的介绍。15.0 还包含了对基于 TCP 传输的 NVMe over Fabrics（NVMe-oF）的支持，此前已有相关文章介绍：[journal article](https://freebsdfoundation.org/our-work/journal/browser-based-edition/storage-and-filesystems/nvme-over-fabrics-in-freebsd-2/) 。自该文章发布以来，对 NVMe-oF 的支持已经合并进了守护进程 [**ctld(8)**](https://man.freebsd.org/ctld/8)，而 `nvmfd` 守护进程已被移除。

15.0 还包含了 [**inotify(2)**](https://man.freebsd.org/inotify/2) 系列系统调用的原生实现。该实现与 Linux 中相同系统调用在 API 层面保持兼容，并可用于原生 FreeBSD 二进制程序和在 [Linux 兼容层](https://man.freebsd.org/linux/4) 下运行的 Linux 二进制程序。对于许多使用场景而言，**inotify(2)** 相比通过 [**kevent(2)**](https://man.freebsd.org/kevent/2) 提供的 EVFILT\_VNODE 内核事件更加可靠且效率更高。它也是现有桌面软件（如 KDE）中广泛使用的 API。

## 虚拟化

在 15.0 中，FreeBSD 的二类（Type 2）虚拟机管理程序 bhyve 也有多项更新。内核监控器和用户态虚拟机管理程序现在均支持 64 位 ARM 和 RISC-V 架构。一些高级功能，如 PCI 直通（PCI pass-through），尚未支持，但使用现有 bhyve 设备模型（如 VirtIO）的 FreeBSD 和 Linux 客机在这两种新架构上均可完全运行。

除了增加架构支持外，bhyve 现在可以使用 **net/libslirp** 包为网络设备提供用户态后端。这使得主机无需额外配置网络（如 [**tap(4)**](https://man.freebsd.org/tap/4) 设备）即可通过网络连接与客机通信。

## 架构相关

15.0 内置了处理器跟踪框架 [**hwt(4)**](https://man.freebsd.org/hwt/4)，用于收集 CPU 记录的事件流。这些事件包括软件执行的详细信息，如控制流变化、异常和时间信息。该框架支持由 ARM 的 Coresight 和 Statistical Profiling Extension（SPE, 统计分析扩展）和 Intel 的 Processor Trace（PT）记录的事件。

该版本还支持 AMD 的 IOMMU，这在多核系统上尤其有用。x86 系统上的 IOMMU 提供了多项功能。IOMMU 的主要用途是为设备 DMA 请求提供备用地址空间，这对虚拟化（如 PCI 直通）和安全性（限制不受信任设备的内存访问）都很有帮助。在 x86 上，IOMMU 还可在中断传递中进行干预，使设备中断可以路由到编号更大的 CPU。FreeBSD 先前版本已支持 Intel 的 IOMMU（DMAR），而 15.0 引入了对 AMD IOMMU 的支持。

## 扩展错误报告

在传统 POSIX 系统中，系统调用在执行过程中通过返回整数错误码报告错误。该错误码保存在特殊全局变量 `errno` 中，可通过 [**strerror(3)**](https://man.freebsd.org/strerror/3) 等函数转换为字符串。15.0 在内核中引入了新的扩展错误机制，它保存关于错误的额外信息，包括附加的字符串描述和错误在源代码中的位置。字符串描述可在系统调用失败后通过 **uexterr\_gettext(3)** 函数获取。系列函数 [**err(3)**](https://man.freebsd.org/err/3)、[**errx(3)**](https://man.freebsd.org/errx/3)、[**warn(3)**](https://man.freebsd.org/warn/3) 和 [**warnx(3)**](https://man.freebsd.org/warnx/3) 会自动在输出到 stderr 的消息中包含扩展字符串描述。也可通过 [**ktrace(1)**](https://man.freebsd.org/ktrace/1) 获取扩展错误信息。

## 第三方软件

FreeBSD 基本系统内置了多款由外部维护的组件。与每个版本一样，15.0 通过导入上游软件的新版更新了许多这些组件。更新列表过长，这里仅列出部分重点更新：OpenZFS 已更新至最新版本 2.4.0；OpenSSL 已升级至当前长期支持版本 3.5，确保 stable/15 分支全生命周期的上游支持；MIT Kerberos 已导入基本系统，取代旧版 Heimdal 实现。工具链实用程序，如 [**ar(1)**](https://man.freebsd.org/ar/1) 和 [**size(1)**](https://man.freebsd.org/size/1)，现在由 LLVM 提供，而非 ELF 工具链项目，从而使基本系统工具链支持链接时优化（LTO）。

## 结论

FreeBSD 15.0 汇聚了过去两年间广泛社区的修复与新特性。感谢所有通过测试快照、报告错误、提交补丁、在社交媒体上协助用户和完成无数其他工作的贡献者。希望大家喜欢 FreeBSD 15.0，并请继续关注我们在 FreeBSD 16 开发中的进展！

***

John Baldwin 是系统软件开发人员，二十多年来直接向 FreeBSD 操作系统提交了各类更改，涉及内核的多个部分（包括 x86 平台支持、SMP、各类设备驱动和虚拟内存子系统）和用户态程序。除了编写代码外，John 还曾在 FreeBSD 核心团队和发布工程团队任职，也为 GDB 调试器贡献过代码。John 与妻子 Kimberly 及三个孩子 Janelle、Evan 和 Bella 一同居住在加利福尼亚州康科德。


# FreeBSD 上的通用闪存存储（UFS）

* 原文：[Universal Flash Storage on FreeBSD](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/universal-flash-storage-on-freebsd/)
* 作者：Jaeyoon Choi

通用闪存存储（UFS）是一种高性能、低功耗的存储接口，专为移动设备、汽车和嵌入式环境设计。如今，UFS 已被广泛应用，并在大多数 Android 旗舰智能手机中取代了 eMMC 成为主流存储方案。它还出现在部分平板电脑、笔记本电脑和汽车系统中。Linux 自约 2012 年起支持 UFS，OpenBSD 自 7.3 版本起支持，Windows 自 Windows 10 起支持。

然而，FreeBSD 之前并没有 UFS 驱动。如果将 UFS 在其他生态系统中经过验证的成熟技术引入 FreeBSD 存储架构，FreeBSD 将在许多移动和嵌入式领域成为可行方案。“为什么 FreeBSD 还没有 UFS 驱动？”这个问题成为了我个人的动力，而本文将说明我为解决这一问题所走的路径。

直到去年，我从未使用过 FreeBSD。在大约六个月的时间里，我学习了 FreeBSD、分析了其代码库，并最终实现了 UFS 驱动。我希望这个案例能够表明，哪怕是 FreeBSD 新手，也可以通过系统化的方法开发设备驱动。

本文简要介绍了 UFS，解释了其架构、开发过程和驱动设计，分享了当前状态与后续计划，并最终展示了一个基于 QEMU 的实践环境，方便读者跟随操作。

注：FreeBSD 也有传统的 UFS（Unix 文件系统）。本文中的 `UFS` 指的是通用闪存存储，而非文件系统 UFS。代码库中该驱动名称为 **ufshci(4)**。

## 通用闪存存储概览

在移动存储中，低延迟和低功耗至关重要，同时与现有系统的兼容性也非常重要。UFS 并非完全发明全新标准，而是通过整合现有标准来实现这些目标。因此，它迅速被市场接受。UFS 保留了其组件标准的低功耗和可靠性优势，但在实现上复杂度有所增加。

在互连层，UFS 使用 MIPI M-PHY（一种可靠的差分高速串行接口）结合 MIPI UniPro（具有强大电源管理功能的链路层）。在此基础上，传输协议层使用 UTP，应用层使用经过验证的 SCSI 命令子集，其可靠性和兼容性已被充分证明。

## UFS 的应用场景

对 UFS 可能感觉陌生，但你很可能已经在使用它。大多数 Android 旗舰智能手机的内部存储采用 UFS。UFS 还被用于部分低功耗平板、超轻便笔记本，和可靠性要求极高的汽车信息娱乐系统中。

许多高性能 ARM 应用处理器集成了 UFS 主控器，一些低功耗 x86 平台（如 Intel N100）也支持 UFS 主控器。任天堂最近发布的掌上游戏机 [Nintendo Switch 2](https://en.wikipedia.org/wiki/Nintendo_Switch_2) 内部存储使用的也是 UFS。

随着 UFS 采用率持续增长，在 FreeBSD 中增加原生 UFS 支持可以扩大其在移动和嵌入式系统中的适用性。基于这一需求，我启动了该项目。

## UFS 架构

![UFS 系统模型示意图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-8edc34ce16a9934e2e42ec466e222efa4f676843%2Funiversal-flash-storage-on-freebsd-01.png?alt=media)

图 1. UFS 系统模型

典型的 UFS 系统由 UFS 目标设备（通常为 PCB 上的 BGA 封装）和集成在应用处理器 SoC 中的 UFS 主控器组成。两者通过高速串行链路通信。

当 I/O 请求到达时，FreeBSD 的 CAM（Common Access Method）子系统会在 CCB（CAM 控制块）中构建 SCSI 命令，并将其传递给驱动程序。驱动程序将 SCSI 命令封装到 UPIU（UFS 协议信息单元）中，并将其加入 UFS 主控器的队列。

主控器将请求入队并触发门控；数据通过 DMA 传输，完成状态通过中断返回。UFS 设备执行 SCSI 子集命令（例如 READ/WRITE），访问 NAND 闪存，并将完成或错误状态返回给主控器。

通过这种方式，UFS 设备驱动控制主控器对 UFS 设备执行读写操作。

![UFS 分层架构示意图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-9fe86b9569adda05fca6a743c50d498fb9b30c85%2Funiversal-flash-storage-on-freebsd-02.png?alt=media)

图 2. UFS 分层架构

如前所述，UFS 将多个现有标准叠加，并组织为三个层次：互连层、传输层和应用层。各层的作用如下：

* **UIC（UFS 互连层）：** 管理链路。负责通过 M-PHY/UniPro 启动链路，调整档位和通道设置以平衡性能与功耗，支持电源状态（Active、Hibernate），检测错误并执行恢复。UFS 驱动通过寄存器和 UIC 命令控制该层。
* **UTP（UFS 传输协议层）：** 传输管理命令和 SCSI 命令。主控器维护管理队列和 I/O 队列；驱动将请求入队并触发门控。数据通过 DMA 传输，完成状态通过中断返回。
* **UAP（UFS 应用层）：** 处理命令（如 READ/WRITE）及命令队列控制。尽管 UFS 定义了多个命令集，但实际仅使用 SCSI 命令子集。UFS 驱动不会生成 SCSI 命令，而是将 CAM 生成的 SCSI 命令封装到 UPIU 中传递给 UTP，从而允许重用 CAM 的标准路径完成扫描、错误处理和重试。通过该层，CAM 将 UFS 视为标准 SCSI 设备。

## 主要优势（高性能、低功耗）

* **性能：** 相比 eMMC 的半双工并行接口，UFS 的全双工高速串行接口提供更高带宽和更低延迟。提交路径轻量化，基于队列、DMA 和中断构建。因此，即使仅使用单队列，性能也很稳定，多循环队列（MCQ）在多核系统上进一步提高了可扩展性。WriteBooster 利用 NAND 的 SLC 区域进一步提升突发写入性能。
* **功耗效率：** UIC 链路仅在 I/O 活跃时提升档位，空闲时迅速降至低功耗状态。标准定义了电源状态切换，从而在热量和功耗受限的移动设备中延长电池寿命。实际上，有报道显示，使用 UFS 而非 NVMe 的平板电脑可额外延长 [约 30-90 分钟的续航](https://psref.lenovo.com/syspool/Sys/PDF/IdeaPad/IdeaPad_Duet_3_11IAN8/IdeaPad_Duet_3_11IAN8_Spec.pdf)。
* **兼容性：** UFS 使用 SCSI 命令子集，现有 SCSI 基础设施可以复用。在 FreeBSD 中，CAM 处理 SCSI 命令，而 UFS 驱动将 CAM 生成的 SCSI 命令封装到 UPIU 中传递给 UTP，使其与 CAM 的集成十分简便。

## UFS 的历史与后续

UFS 标准以 JESD220（UFS）发布，主控器接口以 JESD223（UFSHCI）发布。UFS/UFSHCI 1.0 于 2011 年发布。2015 年，UFS 2.1 设备首次出现在三星 Galaxy S6 中，标志着广泛商业化的开始。UFS 3.0 提升了链路速度，并引入 WriteBooster 用于突发写入。UFS 4.0 增加了多循环队列（MCQ），改善了多核扩展性。UFS 4.1 是当前版本，UFS 已部署在大多数旗舰智能手机中。

随着智能手机和其他移动设备上的本地 AI 工作负载增长，UFS 正在发展以加快将 LLM 模型传输到 DRAM 的速度。本地 LLM 模型需要快速加载，因此需要更高带宽；针对 UFS 5.0 的工作目标是通过提高串行总线时钟速率以提升链路速度，从而提升带宽。

## 驱动概览

我于 2024 年 7 月在 freebsd-hackers 邮件列表上提出了 UFS 设备驱动的构想，并于 2025 年 1 月开始进行分析与设计。幸运的是，Warner Losh（现为我的导师）回复并在 FreeBSD 存储架构方面提供了宝贵指导。FreeBSD 手册和 BSD 大会的演讲也提供了帮助。经过约两个月对 CAM、SCSI 和 NVMe 驱动的分析，我设计了 UFS 驱动。由于 NVMe 与 UFS 结构相似，我复用了许多相同的设计思路。我在 2025 年 5 月 16 日提交了 [早期代码评审](https://reviews.freebsd.org/D50370)。经过多轮审查，该工作于 2025 年 6 月 15 日被合并，并内置在 FreeBSD 15.0 中。

开发主要通过 QEMU 的 UFS 仿真完成，随后在实际硬件上验证：包括配备 UFS 2.0/3.1/4.0 设备的 Intel Lakefield 和 Alder Lake 平台。我也曾计划在 ARM SoC 上测试，但合适硬件难以获取。

UFS 驱动与 CAM 子系统紧密集成。在初始化过程中，驱动向 CAM 注册；此后，用于配置和读写的 SCSI 命令由 CAM 传递给驱动。为了保持对 UFS 3.0 及更早版本的向后兼容性，驱动同时支持单门控队列（SDQ）路径和多循环队列（MCQ）路径。

## 设备初始化与注册

初始化遵循 UFS 分层架构，自底向上。

* **UFSHCI 寄存器：** 启用主控器，配置所需寄存器，并启用中断。
* **UIC（UFS 互连层）：** 发送 Link Startup 命令以建立主机与设备之间的链路，并验证连通性。
* **UTP（UFS 传输协议层）：** 创建 UTP 命令队列并启用 UTP 中断。发送 NOP UPIU 命令以验证传输路径。
* **配置档位与通道：** 协商档位和通道数量，然后配置链路以实现最大带宽运行。
* **UAP（UFS 应用层）：** 向 CAM 注册以开始基于 SCSI 的初始化；CAM 扫描总线上的目标设备和 LUN，并将 SCSI 命令传递给驱动。

CAM（Common Access Method）是 FreeBSD 的存储子系统，分为三个层次：CAM 外设层、CAM 传输层（XPT）和 CAM SIM 层。初始化完成后，UFS 驱动通过 `cam_sim_alloc()` 创建 SIM 对象，并通过 `xpt_bus_register()` 向 XPT 注册。随后，XPT 扫描总线上的目标设备和 LUN，以发现 SCSI 设备。当找到有效 LUN 时，它调用 `cam_periph_alloc()` 在 CAM 外设层创建并注册 Direct Access（da）外设。

在 Direct Access（da）外设注册后，当对 UFS 磁盘发起 I/O 请求时，CAM 外设层会自动构建 SCSI 命令。驱动注册在 SIM 上的 `ufshci_cam_action()` 处理函数接收携带这些命令的 CCB，将其封装到 UPIU 中，入队到 UTP 队列，并在完成后调用 `xpt_done()` 通知 XPT。

CAM 处理标准 SCSI 路径，如扫描、排队、错误处理和重试，因此许多逻辑不需要在 UFS 驱动中实现。驱动的主要工作是通过 UTP 将 SCSI 命令转发到目标设备。

## 队列架构：SDQ 与 MCQ

队列架构是关键设计决策之一。UFS 4.0 引入了多循环队列（MCQ），其概念上类似于 NVMe 的模型。为了向后兼容，仍需支持单门控队列（SDQ），且驱动必须在运行时在 SDQ 与 MCQ 之间选择，因为 UFS 3.1 及更早版本仅支持 SDQ。为此，我定义了函数指针操作接口（ufs\_qop），用于抽象队列操作，以便在运行时选择具体实现（MCQ 尚未实现，将在近期添加）。

## 当前状态与后续开发计划

UFS 驱动正在积极开发中，目前仅实现了 UFS 4.1 特性的子集。我的目标是实现全部功能特性，随后将完善电源管理和 MCQ。目前支持的平台仅限于基于 PCIe 的 UFS 主控器，我计划增加对 ARM 系统总线平台的支持。同时，我也希望在新 UFS 规范发布后能够及时跟进和采纳。

## 使用 UFS 驱动入门

要测试 UFS 驱动，通常需要配备 UFS 的硬件。幸运的是，QEMU 可以在没有实际硬件的情况下开发和测试。本节演示如何在 QEMU 中模拟 UFS 设备并使用驱动（UFS 模拟自 QEMU 8.2 起支持）。

首先准备一个 FreeBSD 快照镜像。

```sh
$ wget https://download.freebsd.org/releases/VM-IMAGES/15.0-RELEASE/amd64/Latest/FreeBSD-15.0-RELEASE-amd64-zfs.qcow2.xz
$ xz -d FreeBSD-15.0-RELEASE-amd64-zfs.qcow2.xz
```

创建 1 GiB 文件，用作 UFS 逻辑单元的后端设备：

```sh
$ qemu-img create -f raw blk1g.bin 1G
```

启动 QEMU 并模拟 UFS 设备：

```sh
$ qemu-system-x86_64 -smp 4 -m 4G \
-drive file=FreeBSD-15.0-RELEASE-amd64-zfs.qcow2,format=qcow2 \
-net user,hostfwd=tcp::2222-:22 -net nic -display curses \
-device ufs -drive file=/home/jaeyoon/blk1g.bin,format=raw,if=none,id=luimg \
-device ufs-lu,drive=luimg,lun=0
```

在 amd64 平台上，GENERIC 内核配置已包含 UFS 驱动模块（见 `sys/amd64/conf/GENERIC`）：

```sh
# Universal Flash Storage Host Controller Interface support
device          ufshci                  # UFS host controller
```

如需显式加载该模块，请编辑 **/boot/loader.conf**：

```sh
ufshci_load="YES"
```

重启后，通过 `camcontrol` 验证 UFS 设备是否附加为 `ufshci0`/`da0`：

```sh
$ camcontrol devlist -v
scbus2 on ufshci0 bus 0:
<QEMU QEMU HARDDISK 2.5+>          at scbus2 target 0 lun 0 (pass2,da0)
<>                                 at scbus2 target -1 lun ffffffff ()
```

使用 `fio` 做基础性能测试：

```sh
$ fio --name=seq_write  --filename="/dev/da0" --rw=write --bs=128k --iodepth=4 --size=1G --time_based --runtime=60s --direct=1 --ioengine=posixaio --group_reporting
$ fio --name=seq_read  --filename="/dev/da0" --rw=read --bs=128k --iodepth=4 --size=1G --time_based --runtime=60s --direct=1 --ioengine=posixaio --group_reporting
$ fio --name=rand_write  --filename="/dev/da0" --rw=randwrite --bs=4k --iodepth=32 --size=1G --time_based --runtime=60s --direct=1 --ioengine=posixaio --group_reporting
$ fio --name=rand_read  --filename="/dev/da0" --rw=randread --bs=4k --iodepth=32 --size=1G --time_based --runtime=60s --direct=1 --ioengine=posixaio --group_reporting
```

QEMU 是一个模拟器，因此适合检查功能行为。对于性能测试，我使用了 Galaxy Book S。

Galaxy Book S 搭载了 Intel 第十代 i5-L16G7（1.4 GHz，5 核心）和内部 UFS 3.1 设备，我在实验中将其升级到 UFS 4.0（在 3.1 主控器上以 HS-Gear 4 运行）。

| 队列深度 | 顺序读取 (MiB/s) | 顺序写入 (MiB/s) | 随机读取 (kIOPS) | 随机写入 (kIOPS) |
| ---- | ------------ | ------------ | ------------ | ------------ |
| 1    | 709          | 554          | 7.1          | 12.1         |
| 2    | 1,395        | 556          | 14.8         | 29.4         |
| 4    | 1,416        | 559          | 31.6         | 68.2         |
| 8    | 1,417        | 554          | 63.5         | 102.3        |
| 16   | 1,399        | 555          | 103.7        | 105.5        |
| 32   | 1,361        | 556          | 114.2        | 106.6        |

**表 1. FreeBSD UFS 性能**

根据队列深度，顺序写入峰值为 559 MiB/s，顺序读取达到 1,417 MiB/s，对于移动设备而言表现非常出色。

| 队列深度 | 顺序读取 (MiB/s) | 顺序写入 (MiB/s) | 随机读取 (kIOPS) | 随机写入 (kIOPS) |
| ---- | ------------ | ------------ | ------------ | ------------ |
| 1    | 542          | 479          | 6.1          | 11.0         |
| 2    | 1,358        | 548          | 13.0         | 21.0         |
| 4    | 1,351        | 550          | 29.7         | 53.1         |
| 8    | 1,352        | 550          | 61.1         | 84.1         |
| 16   | 1,351        | 552          | 119.0        | 114.0        |
| 32   | 1,355        | 553          | 142.0        | 120.0        |

**表 2. Linux UFS 性能**

在相同条件下，Linux 的性能与 FreeBSD 相当。

## 结论

UFS 是一项快速发展的接口标准。FreeBSD 的 UFS 驱动同样在不断增加新功能并优化，使 UFS 能够在各种设备上运行。

希望本文能促进 UFS 在 FreeBSD 上的更广泛使用。欢迎为 UFS 驱动贡献代码。我感谢所有帮助实现这一工作的评审人员，并将继续为社区做出贡献。

**ufshci(4)** UFS 设备驱动的开发得到了三星电子的支持。

***

Choi Jaeyoon 是三星电子内存部门的软件工程师，致力于扩展 SSD 和 UFS 的开源生态系统。他自 2024 年开始使用 FreeBSD，并于 2025 年 9 月成为 FreeBSD src 提交者。他曾为 Fuchsia OS 的 F2FS 文件系统做出贡献，目前维护着 Fuchsia OS 的 UFS 驱动，对存储系统的开源工作充满好奇。


# Vox FreeBSD：如何让音频工作

* 原文：[Vox FreeBSD: How Sound Works](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/vox-freebsd-how-sound-works/)
* 作者：Christos Margiolis

FreeBSD 的声音支持始于 1993 年，当时 Jordan K. Hubbard 将通用 Linux 声卡驱动移植到了 FreeBSD，该驱动后来被称为 VoxWare 声卡驱动，由 Hannu Savolainen 编写。1993 年至 1997 年间，多个新版本的 VoxWare 驱动被引入（并修改）到 FreeBSD 基本系统。当时，Amancio Hasty 和 Jim Lowe 负责了 FreeBSD 声音支持的大部分工作。VoxWare 驱动最终发展成为我们今天所知的 OSS（Open Sound System，开放声音系统），由 Hannu 及其团队在 4Front Technologies 维护。

然而，1997 年情况发生了变化，当时 Luigi Rizzo 构建了现代 FreeBSD 声音系统的基础。1999 年，Cameron Grant 为 FreeBSD 4.0 重写了声卡系统，使用了 newbus 接口来支持大量硬件。这取代了 Luigi 的驱动，其驱动在一个月后被从代码树中移除。随后几年，声音领域经历了诸多变化。针对不同硬件芯片的驱动数量显著增加（尤其是 PCI 和 USB 设备），声卡基础设施也得到了多项改进，这都得益于 Cameron Grant 和 Orion Hodson。Cameron 于 2005 年离世，这是 FreeBSD 社区的一大损失。

2005 年，Ariff Abdullah 接管了 FreeBSD 声音代码的维护工作。自此，FreeBSD 声音支持经历了显著变化，并有多轮设备驱动重构。

## OSS

FreeBSD 的声音 API 被称为 OSS，即“Open Sound System”（开放声音系统）。有趣的是，它曾经也是 Linux 的默认声音 API，后被 ALSA 取代。而 FreeBSD 仍在使用 OSS，它通过标准设备文件提供了简单清晰的接口（`/dev/dsp*` 对应声卡，`/dev/mixer*` 对应混音器），并通过少量常用 POSIX 系统调用操作：

| 系统调用                                      | 说明                                                                                                                                                                |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **open(2)**                               | 打开设备。                                                                                                                                                             |
| **close(2)**                              | 关闭设备。                                                                                                                                                             |
| **read(2)**                               | 录制音频。                                                                                                                                                             |
| **write(2)**                              | 播放音频。                                                                                                                                                             |
| **ioctl(2)**                              | 查询或操作设置（采样率、格式、音量等）。                                                                                                                                              |
| **select(2)**, **poll(2)**, **kqueue(2)** | 事件轮询。**kqueue(2)** 为 FreeBSD 独有（从 [15.0 版本起](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/vox-freebsd-how-sound-works/) 支持）。 |
| **mmap(2)**                               | 内存映射 I/O。                                                                                                                                                         |

官方手册可在此获取：<http://manuals.opensound.com/developer/>

与声卡的交互使用的是常用系统调用，因此在终端中可以执行如下操作：

1. 播放白噪声：`cat /dev/random > /dev/dsp`
2. 将原始 PCM（Pulse Code Modulation，脉冲编码调制）流录入文件：`cat /dev/dsp > foo`
3. 非常粗略的输入监控，可用于检查麦克风是否工作：`cat /dev/dsp > /dev/dsp`

然而，要实现更复杂的功能，则需要编写实际的程序——幸运的是，OSS API 使用起来非常直观。`/usr/share/examples/sound` 有多个示例程序，并且在本文撰写时仍在不断增加。

除了 `/dev/dsp*` 设备外，还有一个 `/dev/dsp` 设备，上述示例中使用的就是它。它并不是真实的设备，而是当前默认设备的别名。建议应用程序在需要使用当前默认设备时访问该别名，而不要硬编码特定设备。

OSS 的另一个优点是它只依赖 POSIX 系统调用，因此可以非常容易地在任何编程语言中使用，无需语言绑定。若想将 OSS 完全“移植”到其他语言，只需定义一些全局 OSS API 常量和结构体，可在 `/usr/include/sys/soundcard.h` 中找到上述定义。

## sound(4)

OSS API 在 **sound(4)** 中实现。它将一些通用功能抽象为内核模块，使得各个声卡驱动（如 **snd\_hda(4)**、**snd\_uaudio(4)** 等）无需重复实现这些通用代码。通用功能包括：

* 设备的创建、删除和访问。
* 缓冲区管理。
* 音频处理。
* 提供全局和大部分设备特定的 sysctl 接口。

驱动在初始化阶段必须附加到 **sound(4)** 并建立与其的通信管道。换言之，**sound(4)** 是用户空间与设备驱动之间的桥梁。这非常方便，因为 FreeBSD 内核为每个连接的声卡都暴露相同的 `/dev/dsp*` 文件和 sysctl（`hw.snd.*` 和 `dev.pcm.*`），为用户和应用程序提供了统一访问和配置，同时避免驱动层的重复实现。

**sound(4)** 还提供了 `/dev/sndstat`，用以显示所有已连接声卡和活动通道的信息，并被 **virtual\_oss(8)** 和 **sndctl(8)** 内部使用。

**sound(4)** 处理 PCM 音频流，其具体参数（采样率、采样格式等）可由用户通过 **sndctl(8)** 手动配置。这意味着，对于使用其他格式（如 WAV、Opus、MP3 等）的音频，应用程序在播放时需要将其转换为 PCM 流，录制时则需从 PCM 转回目标格式。

## 高层实现概览

### 设备访问

如前所述，并在示例程序中展示，应用程序通过 `/dev/dsp*` 访问声音子系统，使用 **open(2)** 系统调用。可指定以下标志：

| **open(2)** 标志 | 功能    |
| -------------- | ----- |
| O\_RDONLY      | 录音    |
| O\_WRONLY      | 播放    |
| O\_RDWR        | 录音和播放 |

在 **open(2)** 调用中，还可以额外指定标志 `O_NONBLOCK` 和 `O_EXCL`，分别用于非阻塞 I/O 和对设备的独占访问。为了让 **sound(4)** 知道哪些通道属于哪个文件描述符，并正确路由音频和信息，它使用 **DEVFS\_CDEVPRIV(9)**。

如前所述，还有 `/dev/mixer*` 设备。这是一个遗留接口，主要用于音量设置和录音源选择，例如被 **mixer(8)** 等应用程序使用。`/dev/mixer*` 设备与 `/dev/dsp*` 设备一一对应，例如 `/dev/mixer0` 对应 `/dev/dsp0`。该接口还提供部分物理混音器功能。从 OSS 4.0 版本开始，混音器 API 已被重写，但在 FreeBSD 上尚未完全实现。

### 通道（Channel）

通道保存了其状态的重要信息（如配置、使用该通道的进程 PID 与名称、诊断值等），和最关键的组件：缓冲区，也就是实际的音频流。通道除了设备级的主音量外，还具有自己的独立音量。这一点尤其有用，因为应用程序可以拥有自己的音量控制，通常无需调整主音量。

需要注意的是，**sound(4)** 中有两类通道：**主通道/“硬件”通道** 和 **虚拟通道**。

主通道由设备驱动分配，通常硬件支持一个播放通道和一个录音通道。但某些驱动可能会将主通道数量设置为硬件提供的物理播放和录音端口数。每个主通道包含一对缓冲区：一个面向软件（userland），一个面向硬件。面向软件的缓冲区负责与用户空间交换音频数据，面向硬件的缓冲区则用于与硬件交换数据。当设备驱动准备好从硬件读取或向硬件写入数据时，会触发对 **sound(4)** 的中断，然后数据在两个缓冲区之间复制。播放时，数据写入硬件，软件缓冲区的数据会复制到硬件缓冲区，以便驱动将其发送给硬件；录音时则反向操作。

虚拟通道（通常称为 VCHAN）被视为主通道的子通道，但与主通道不同，它们不与硬件直接连接，因此只使用面向软件的缓冲区。虚拟通道存在的原因是希望允许无限数量的应用程序同时访问设备。如果没有虚拟通道，同时访问设备的进程数将等于主通道的数量，因为每个主通道只有一个面向软件的缓冲区，这意味着所有进程必须共享同一个缓冲区，这对于音频处理并不理想，因此每个缓冲区必须专用于单一进程。

前文介绍了如何通过主通道在用户空间与硬件之间交换音频流。当启用虚拟通道（默认情况）时，**sound(4)** 不再只是简单地将主通道的软件缓冲区复制到硬件缓冲区（播放时），或反向操作（录音时）；它首先需要混合主通道所有子虚拟通道的音频流，然后将最终混合后的流提供给主通道的硬件缓冲区。可以想象，这一额外层会引入轻微的开销，对于大多数场景影响不大，但在某些低延迟的音乐制作工作流程中可能不理想。对于这些场景，可以选择禁用虚拟通道。

要查看通道状态，可以使用 **sndctl(8)**：

```sh
$ sndctl -v
```

### 处理链（Processing chain）

**sound(4)** 的有趣功能是其处理链，包括以下内容：

* **混音（Mixing）**：实际上就是前一节中讲的虚拟通道音频流的混合与解混合。
* **音量控制（Volume control）**。
* **通道矩阵（Channel matrixing）**：**sound(4)** 可以做任意通道映射，例如单声道到立体声，或立体声到 5.1 环绕声。这是通过将流从一种交错 PCM 格式转换为另一种实现的。
* **基础参数均衡（Basic parametric equalization）**。
* **格式转换（Format conversions）**。
* **重采样（Resampling）**，包括三种类型：
  * 线性（Linear）
  * 零阶保持（Zero-order-hold, ZOH）
  * 正弦基函数（Sine Cardinal, SINC）

每个通道都有自己的处理链，只包含必要的组件。例如，如果通道配置的采样率为 44100Hz，但应用程序提供的音频是 48000Hz，则该通道的处理链中需要包含重采样组件；如果音频流的采样率与通道相同，则该组件不需要。其他组件也是类似逻辑。

一个直观的方法是使用 **sndctl(8)** 打印处理链。下面的命令可以打印每个活动通道的处理链：

```sh
$ sndctl feederchain
```

在下一节中，我们将讨论在某些特殊情况下，为什么和如何完全绕过处理链。

### 内存映射 I/O 与比特完美音频

**sound(4)** 的两个特性，尤其受到低延迟应用开发者和音频爱好者的欢迎：**比特完美模式** 和 **内存映射 I/O**。

* **比特完美模式（Bit-perfect mode）**：意味着音频流会跳过 **sound(4)** 的所有处理链，几乎直接送入声卡。使用该模式的应用程序需要自行确保音频流的配置（采样率、格式、通道矩阵）与声卡匹配。例如，如果应用程序播放 48000Hz 的音频，但声卡不支持此采样率，则应用程序必须负责重采样。比特完美模式默认是关闭的，仅由自行实现音频处理的应用或确信声卡支持比特完美模式的用户启用。
* **内存映射 I/O（Memory-mapped I/O）**：与比特完美类似，音频流跳过 **sound(4)** 的所有处理；实际上，使用内存映射 I/O 必须先启用比特完美模式。主要区别在于，内存映射 I/O 将所有音频缓冲区管理责任完全交给应用程序。应用不仅需要处理比特完美模式下的事项，还必须确保缓冲区正确同步，读写操作按时完成（在一定程度上借助 **sound(4)** 的帮助）。在合适的环境下正确实现，可以提高性能，但实现起来非常繁琐且容易出错，因此通常不推荐使用，除非开发者非常清楚自己在做什么。

## 设备驱动程序

正如 **sound(4)** 是用户空间与设备驱动之间的桥梁，设备驱动则是 **sound(4)** 与实际硬件之间的桥梁。除了所有声卡驱动都需要附加到 **sound(4)** 并与其通信之外，驱动的其他功能取决于驱动本身。在未来的文章中，可以介绍如何从零编写一个声卡驱动。

FreeBSD 内置支持以下声卡：

| 驱动程序                  | 支持的声卡                                  | 默认启用平台      |
| --------------------- | -------------------------------------- | ----------- |
| **snd\_ai2s(4)**      | Apple I2S                              | powerpc     |
| **snd\_als4000(4)**   | Avance Logic ALS4000                   |             |
| **snd\_atiixp(4)**    | ATI IXP                                |             |
| **snd\_cmi(4)**       | CMedia CMI8338/CMI8738                 | amd64, i386 |
| **snd\_cs4281(4)**    | Crystal Semiconductor CS4281           |             |
| **snd\_csa(4)**       | Crystal Semiconductor CS461x/462x/4280 | amd64, i386 |
| **snd\_davbus(4)**    | Apple Davbus                           | powerpc     |
| **snd\_emu10k1(4)**   | SoundBlaster Live! 和 Audigy            |             |
| **snd\_emu10kx(4)**   | Creative SoundBlaster Live! 和 Audigy   | amd64, i386 |
| **snd\_envy24(4)**    | VIA Envy24 及兼容型号                       |             |
| **snd\_envy24ht(4)**  | VIA Envy24HT 及兼容型号                     |             |
| **snd\_es137x(4)**    | Ensoniq AudioPCI ES137x                | amd64, i386 |
| **snd\_fm801(4)**     | Forte Media FM801                      |             |
| **snd\_hda(4)**       | Intel 高保真音频 (High Definition Audio)    | amd64, i386 |
| **snd\_hdsp(4)**      | RME HDSP                               |             |
| **snd\_hdspe(4)**     | RME HDSPe                              |             |
| **snd\_ich(4)**       | Intel ICH AC’97 及兼容型号                  | amd64, i386 |
| **snd\_maestro3(4)**  | ESS Maestro3/Allegro-1                 |             |
| **snd\_neomagic(4)**  | NeoMagic 256AV/ZX                      |             |
| **snd\_solo(4)**      | ESS Solo-1/1E                          |             |
| **snd\_spicds(4)**    | I2S SPI                                |             |
| **snd\_t4dwave(4)**   | Trident 4DWave                         |             |
| **snd\_uaudio(4)**    | USB 音频和 MIDI                           | 插入设备时自动加载   |
| **snd\_via8233(4)**   | VIA Technologies VT8233                | amd64, i386 |
| **snd\_via82c686(4)** | VIA Technologies 82C686A               |             |
| **snd\_vibes(4)**     | S3 SonicVibes                          |             |

此外，还支持以下 ARM 芯片：

* Allwinner A10/A20 和 H3
* Broadcom BCM2835
* Freescale Vybrid
* Freescale i.MX6

如果你的声卡在你使用的架构上默认驱动未启用，或者你使用的是未编译声卡支持的自定义内核配置，并且不确定声卡使用的是哪个驱动，可以运行以下命令：

```sh
# kldload snd_driver
```

`snd_driver` 是元驱动，会加载所有可用驱动。若确定哪个驱动与你的声卡匹配，就可以仅加载该驱动。

## 最近改进

在过去两年里，声音子系统经历了许多改进，包括多个错误和崩溃修复、引入不断扩展的 Kyua 测试套件和测试驱动（**snd\_dummy(4)**），和多次代码清理和重构。

几个重要的面向用户的改进包括：

* 现在支持热拔插。使用旧版 FreeBSD 的 USB 声卡用户可能记得，热拔插声卡通常会导致 USB 总线卡住，直到手动终止使用已断开设备的应用程序 ([PR 194727](https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=194727))。
* 浮点音频支持。这有点容易误解，因为至少目前在设备驱动层并未真正支持浮点音频，而是允许用户空间应用程序使用 OSS 做浮点音频处理。已修复了不少 Port，例如 Wine，需要 OSS 提供浮点音频支持。
* **sound(4)** 现在每个设备只暴露一个 `/dev/dsp*` 文件，并在内部完成所有音频流路由，通过 **DEVFS\_CDEVPRIV(9)** 实现，而不是为每个分配的音频流暴露一个 `/dev/dsp*` 文件。当前方法在实现和向用户空间暴露的内容上都更清晰。
* 对高保真音频卡（**snd\_hda(4)**）的开箱支持更好。这类声卡一直是开发者和用户的痛点，因为它们往往带有非标准配置，需要在驱动或 **/boot/device.hints** 内添加手动补丁来补偿。常见问题是插入耳机时声音不会自动切换，反之亦然。最近已有针对多款声卡的补丁，尤其是 Framework 笔记本。自 FreeBSD 15.0 起，提供了 **devd(8)** 配置 **/etc/devd/snd.conf** 尝试自动解决该问题。基本思路是，当 **snd\_hda(4)** 检测到插孔被（拔）插时，会发送 **devd(8)** 通知，而 **/etc/devd/snd.conf** 会通过 **virtual\_oss(8)** 将声音重定向到合适的设备。此功能仍处于实验阶段，未来会根据反馈进一步优化。
* 为 **sound(4)** 添加 **kqueue(2)** 支持。

## 用户空间工具

可在各自的手册页中找到以下工具的示例和更多信息。

### sndctl(8)

**sndctl(8)** 用于列出和操作声卡设置，例如采样率、采样格式、比特完美模式（bit-perfect）和实时模式（realtime）设置等。它旨在替代 `/dev/sndstat`（实际上内部就是使用它）和部分 **sound(4)** 的 sysctl，至少在大多数使用场景下如此。

```sh
$ sndctl
pcm3: <Realtek ALC295 (Analog 2.0+HP/2.0)> on hdaa1 (play/rec)
    name                = pcm3
    desc                = Realtek ALC295 (Analog 2.0+HP/2.0)
    status              = on hdaa1
    devnode             = dsp3
    from_user           = 0
    unit                = 3
    caps                = INPUT,MMAP,OUTPUT,REALTIME,TRIGGER
    bitperfect          = 0
    autoconv            = 1
    realtime            = 0
    play.format         = s16le:2.0
    play.rate           = 48000
    play.vchans         = 1
    play.min_rate       = 1
    play.max_rate       = 2016000
    play.min_chans      = 2
    play.max_chans      = 2
    play.formats        = s16le,s32le
    rec.rate            = 48000
    rec.format          = s16le:2.0
    rec.vchans          = 1
    rec.min_rate        = 1
    rec.max_rate        = 2016000
    rec.min_chans       = 2
    rec.max_chans       = 2
    rec.formats         = s16le,s32le
```

### mixer(8)

**mixer(8)** 用于控制音量、静音/取消静音、录音源选择和默认设备设置。它在 FreeBSD 14.0 中被完全重写，提供了改进的界面和功能。

```sh
$ mixer
pcm3:mixer: <Realtek ALC295 (Analog 2.0+HP/2.0)> on hdaa1 (play/rec) (default)
    vol       = 0.75:0.75     pbk
    pcm       = 1.00:1.00     pbk
    rec       = 0.37:0.37     pbk
    ogain     = 1.00:1.00     pbk
    monitor   = 0.67:0.67     rec src
```

### virtual\_oss(8)

**virtual\_oss(8)** 是由已故 Hans Petter Selasky 编写的 OSS 强大音频服务器。多年来它一直作为 Ports（audio/virtual\_oss）提供，但自 FreeBSD 15.0 起已成为基本系统的一部分。它目前正在积极开发中，已有许多重要改进在推进和规划中。

如 [15.0 发布说明](https://cgit.freebsd.org/src/commit/?id=c457acb4ee821cf015930a94f52c3870786468a7) 所述，FreeBSD 15.0 之前的 **virtual\_oss(8)** 用户可以卸载 Port audio/virtual\_oss 并使用基本系统版本。唯一需要注意的是，某些依赖第三方库的功能已移至独立 Port，包括：

* sndio 后端支持：`audio/virtual_oss_sndio`
* 蓝牙后端支持：`audio/virtual_oss_bluetooth`
* **virtual\_equalizer(8)**：`audio/virtual_oss_equalizer`

### mididump(1)

**mididump(1)** 是一款简单的工具，可用于实时打印指定设备的 MIDI 事件。这对于确认 MIDI 设备工作正常，和按键是否正确映射，非常实用。

```sh
$ mididump /dev/umidi0.0
Note on                 channel=1, note=53 (F3, 174.61Hz), velocity=109
Note off                channel=1, note=53 (F3, 174.61Hz), velocity=127
Note on                 channel=1, note=55 (G3, 196.00Hz), velocity=100
Note off                channel=1, note=55 (G3, 196.00Hz), velocity=127
Pitch bend              channel=1, change=1
```

### beep(1)

**beep(1)**，顾名思义，用于播放蜂鸣声。这是验证声音是否正常工作的简单方法。

### 其他工具

除了前面提到的工具，声音子系统还提供了以下内容：

| 功能                     | 说明                                                                              | 文档            |
| ---------------------- | ------------------------------------------------------------------------------- | ------------- |
| **mixer(3)**           | 用于与 OSS mixer 交互的 C 语言库。                                                        | man 3 mixer   |
| **sndstat(4)**         | 一个 **nv(9)** 接口，用于列出设备信息，和注册用户态声音设备。被 **sndctl(8)** 和 **virtual\_oss(8)** 内部使用。 | man 4 sndstat |
| hw\.snd.\*             | 全局 **sysctl(8)** 变量。                                                            | man 4 sound   |
| dev.pcm.\*             | 设备特定的 **sysctl(8)** 变量。                                                         | man 4 sound   |
| 驱动特定的 **sysctl(8)** 变量 |                                                                                 | 请参考相应驱动的手册页。  |

## FreeBSD 用于音乐制作？！

你可能会觉得在开玩笑，但实际上，近年来这个话题越来越多地被提及，我们也已经在一些会议上看到了相关演讲，例如：

* Goran Mekić, FOSDEM 2019
* Goran Mekić, EuroBSDCon 2022
* Charlie Li, BSDCan 2024
* Christos Margiolis, FreeBSD DevSummit 09/2024
* Christos Margiolis, BSDCan 2025
* Charlie Li, EuroBSDCon 2025

毫无疑问，FreeBSD 并不是音乐人或制作人在考虑音乐制作系统时的首选操作系统，但部分原因在于缺乏“宣传”。实际上，FreeBSD 提供了稳定、高速、可高度配置的声音子系统，拥有不断增长的开源数字音频工作站（DAW）、LV2 插件以及其他类型的音乐/制作软件，并且在 OSS 不适用或不理想的情况下，还可以兼容其他非原生声音子系统（如 ALSA、sndio、JACK、Pulseaudio、Pipewire 等）。

我真诚地认为，如果我们继续保持和开发声音子系统，移植和开发更多软件，并在实践中展示 FreeBSD 在音频和音乐制作方面的潜力，无论是在会议上还是在线，有朝一日，我们可能会看到 FreeBSD 在发烧友和音乐人群体中获得显著关注和认可。

## 报告与解决 Bug

所有软件都有可能出现 Bug，声音子系统也不例外。提供足够的信息非常必要，仅仅提交“我的机器上声音无法工作”这样的陈述，并不能真正帮助定位问题。通常附上以下命令的输出即可满足大部分情况的需求：

1. `uname -a`
2. `sndctl -v`
3. `mixer -a`
4. `sysctl hw.snd dev.pcm`，和任何驱动相关的 sysctl（如果有的话）。
5. dmesg，在设置 `hw.snd.verbose=4` 并重现 bug 后获取。
6. 与重现 bug 的应用程序相关的日志（如果有）。

## 结论

希望本文能够帮助读者了解当前 FreeBSD 声音子系统的整体结构。后续如果能有更多人关注 FreeBSD 的声音系统，那再好不过了！大部分讨论集中在邮件列表 <freebsd-multimedia@FreeBSD.org>，因此建议持续关注该列表。

***

**Christos Margiolis** 是来自希腊的独立承包人，同时也是 FreeBSD 源代码提交者（src committer）。


# 使用 mdo(1) 与 mac\_do(4) 进行凭证转换

* 原文：[Credentials Transitions with mdo(1) and mac\_do(4)](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/)
* 作者：Olivier Certner

在本文中，我们探讨如何使用 **mdo(1)** 程序，轻松快速地以不同的凭据启动新进程，和系统管理员如何利用内核模块 **mac\_do(4)**，使非特权用户能够发起凭据转换，从而在简单的基于角色的场景中，无需安装诸如 **sudo(8)** 或 **doas(1)** 等第三方程序。

传统的 UNIX 访问控制方法，本质上依赖于以下概念和组件：

* 用户与组。组旨在通过在某些方面对组内所有用户统一处理，从而简化管理。
* 进程，作为代表某个用户和组行事的主体，这些用户和组被称为其凭据。
* 文件所有权（一个用户、一个组）与权限，它们分别控制所有者、文件所属组的成员以及其他用户的访问。
* 特殊的 root 用户 [1](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor003)，其拥有全部特权，尤其是不受访问控制约束。
* set-user-ID / set-group-ID 可执行文件，这类程序在启动时，其进程会分别将可执行文件的所有者作为用户、将可执行文件的组作为“主”组认可 [2](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor000)。

系统管理员的一项主要职责，是为其用户提供对系统各类资源的适当访问权限。在大多数情况下，这意味着定义用户和组，并确保文件权限符合预期的安全策略。

UNIX 访问控制模型具有这样的灵活性：用户不必对应真实的、具体的人，而也可以表示角色，由多个需要访问特定资源和信息的真实用户来扮演。事实上，在除最简单的文件共享场景之外的所有情况下，这种依赖 UNIX 用户而不仅仅是组的基于角色的方法都是必要的。这使得临时采用另一组凭据——即基于目标用户建立的凭据——成为系统的一项重要功能，而这一功能传统上由程序 **su(1)** 来完成。

然而，**su(1)** 在切换到新用户的凭据之前，需要对该用户身份验证，通常是要求输入该用户的密码 [3](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor005)。这对于已经被分配了角色且本身已经完成认证的人类用户来说并不方便，对于自动化场景而言同样如此。它还必然会启动目标用户的 shell，这使其无法用于那些没有有效登录 shell 的用户，而这正是角色用户通常所期望的设置——任何人都不应当能够直接以其身份登录。此外，它还使得以指定参数启动某个特定程序变得比应有的更加繁琐 [4](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor006)。

为克服这些限制，系统管理员通常会安装其他用于代表其他用户运行命令的程序，例如 **sudo(8)** 或 **doas(1)**。然而，像 **sudo(8)** 这样的程序具有不可忽视的攻击面，部分原因在于其包含大量不常用的功能，尤其是其模块化设计，从安全角度看可能是危险的。更一般地说，安装可执行文件所有者为 root 且设置了 set-user-ID 模式位的程序，本身就是一项安全隐患，因为一旦这些程序被攻破，就可能通过以 root 用户身份执行代码而获得完整的管理权限。但传统的 UNIX 并未提供其他更改凭据的方式，这也是 **su(1)** 和 **login(1)** 等程序必须以这种方式安装的原因。

作为设置了 set-user-ID 模式位的可执行文件（通常称为“setuid 可执行文件”）的替代方案，我们提供了内核模块 **mac\_do(4)**，它构建于 FreeBSD 的 MAC 框架之上 [5](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor007)。其目的在于仅允许来自非特权进程的特定凭据转换，从而无需将相应的可执行镜像安装为“setuid”。

**mdo(1)** 是 **mac\_do(4)** 的配套程序，负责向内核实际请求所需的凭据转换。**mdo(1)** 可以由拥有全部特权的 root 用户单独使用；否则，其请求将根据管理员的配置，由内核模块 **mac\_do(4)** 审核。

在本文中，我们首先通过一系列示例，说明如何使用 **mdo(1)** 在新的凭据下启动命令。随后，我们解释如何配置 **mac\_do(4)**，以在宿主系统和 jail 中启用对特定凭据转换的支持，从而实现基于角色的方案，并对当前设计提供一些见解。最后，我们征求用户反馈，了解短期内应当提供的内容，和可能的长期未来规划。

## 使用 mdo(1)

**mdo(1)** 的设计目标，是以任意一组凭据运行任意命令。如果你尚未配置 **mac\_do(4)**（将在下节介绍），仍然可以以 root 身份运行下面的所有示例。对于大多数示例，需要 FreeBSD 15.0，因为 FreeBSD 14.3 中的 **mdo(1)** 仅支持选项 `-u` 和 `-i` [6](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor008)。

出于安全原因，目标进程的凭据必须被完整指定：要么通过显式列出所有用户和组及其所请求的值，要么通过建立基线，为每一项提供默认值，然后再通过附加选项修订。

在基于角色的设置中，最常见的使用场景，可以说是认可某个用户的凭据，就好像该用户刚刚登录一样。因此，**mdo(1)** 以尽可能简单的形式支持这一点，唯一需要的选项是 `-u`（表示“user”），用于基于指定用户建立基线，例如：

```sh
$ mdo -u www /usr/local/bin/occ
```

对于那些 **sudo(8)** 和 **doas(1)** 用户来说，这条命令行看起来应该与你使用这些工具时的方式极为相似：基本上，`mdo` 取代了 `sudo` 或 `doas`，其余部分完全一致。

显然，如果传递的是某个用户的数值 ID 而不是用户名，这种方式就无法奏效，因为完整的登录凭据是由密码数据库和组数据库确定的，而这些数据库是按名称索引的 [7](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor009)。使用 `-u` 并指定用户数值 ID 时，只会指定用户本身（实际上是实际、有效和保存的用户 ID），此时还需要告知 **mdo(1)** 所有目标组。这要么像下面将看到的那样显式指定，要么通过 `-i` 来完成，`-i` 表示以当前的组作为基线；否则，**mdo(1)** 将直接报错。

保留当前的组，例如：

```sh
$ mdo -u 10002 -i
```

这在某些情况下会很有用，例如，当你需要临时处理一些“外来”的文件，而其所有者并不存在于你的密码数据库中时。

`-i` 也可以与使用用户名的 `-u` 一起使用，且含义相同。例如，如果 foo 是你密码数据库中的某个用户，其 ID 为 10002，那么下面这条命令与前一个示例完全等价：

```sh
$ mdo -u foo -i
```

假设现在你想显式指定组，无论是因为你像上文那样使用了数值用户 ID，还是因为你想覆盖某个用户关联的组。你可以使用以下选项：

* `-g`：设置或覆盖主组 [8](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor010)。
* `-G`：设置或覆盖完整的附加组集合。你在此提供的以逗号分隔的列表被视为完整列表，即应包含所有附加组。请注意，从 FreeBSD 15 开始，用户登录时，其初始组（在密码数据库中指定）也会包含在进程的附加组集合中。
* `-s`：修改附加组集合。此选项的参数由一系列逗号分隔的指令组成。使用 `+` 指令可以确保某组包含在附加组中，使用 `-` 指令可以确保某组不在附加组中，使用 `@` 指令可以重置列表，使 `-s` 的功能类似于 `-G`，但语法不同 [9](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor011)。

一些示例：

```sh
$ mdo -u unprivileged_user -g wheel -G wheel,staff,operator
```

该命令会以 unprivileged\_user 用户启动一个 shell，但会忽略密码数据库和组数据库中指定的组，而是用传入的组来替换它们。`-g` 和 `-G` 对于测试某个用户在特定组集合下将获得哪些权限非常有用。

然而，如果目的只是以某个用户身份登录，并额外附加一些组（例如 wheel 和 operator），即所谓“增强”的角色场景，那么应当使用选项 `-s`：

```sh
$ mdo -u unprivileged_user -s +wheel
```

或者，相反地，如果需要临时撤销某个组的成员资格，例如当该组被用作通过 **ugidfw(8)**（和 **mac\_bsdextended(4)**）访问控制的标记时：

```sh
$ mdo -u unprivileged_user -s -tag_group
```

如果用户本身不需要改变，就没有必要用 `-u` 显式指定。相反，可以直接使用 `-k`（表示“keep”），其含义是以当前所有用户和组作为基线。`-k` 与 `-u` 互斥，并且隐含 `-i`（从当前组开始）。

需要注意的是，在所有情况下，都可以使用前面提到的显式选项（`-g`、`-G` 和 `-s`）来覆盖凭据中的组设置。

最后，如有需要，还可以分别覆盖用户和主组的 real、effective 和 saved 变体，分别使用 `--ruid`、`--euid`、`--svuid`，和 `--rgid`、`--egid` 和 `--svgid`。当这三种变体都被指定时，就不再需要分别使用 `-u` 或 `-g`，尽管在使用用户名时指定 `-u` 仍然有助于获取其关联的组。

可以看到，除了在最常见使用场景下的简洁性之外，**mdo(1)** 相较于 **su(1)**、**sudo(8)** 或 **doas(1)** 的优势在于，它允许对目标凭据的各个方面精确控制。这使其成为在修改密码数据库和组数据库之前，用于测试或临时使用任意凭据的首选工具，或者用于这样一种基于角色的设置：角色的认可来源于加入额外的组，而不是切换用户。

**mdo(1)** 仅关注于更改凭据。因此，其代码相对简单，并且在设计时特别强调尽可能清晰和最小化，同时保持“显然正确”。这使得程序易于审计，并生成极小的二进制文件，在我的 **stable/14** 机器上仅略超过 7kB。相比之下，**doas(1)** 的体积略超 27kB，而 **sudo(8)** 为 229kB，加上其默认安全策略插件 **sudoers(5)** 为 628kB，两者都是以“setuid 可执行文件”形式安装，这与 **mdo(1)** 形成对比。

目前，**mdo(1)** 面向基于角色的场景，它在请求新凭据时不要求输入任何密码或其他形式的认证，而是完全依赖请求者自身的凭据。作为未来可能的发展方向（在本文结论中提及），我们可能会增加支持要求当前已登录用户输入密码的功能。根据用户反馈，还可能考虑增加与切换到其他用户相关的附加功能，例如登录类（login class）、登录名、调度优先级等。

## 配置 mac\_do(4)

非 root 用户要使用 **mdo(1)**，必须配置 **mac\_do(4)**，因为 **mdo(1)** 设计上并未以“setuid”方式安装。

**mac\_do(4)** 默认不会编译进内核，但可以很容易以模块方式加载：

```sh
# kldload mac_do
```

然后，你可以通过 **sysctl(8)** 项 `security.mac.do` 访问其参数。目前（FreeBSD 14.3 和 15.0）可用的参数如下：

* `enabled`：模块是否启用（默认值为 true）。这是一个全局开关。也可以通过规则（下一个选项）或 jail 参数（见下文对应小节）在宿主系统或任意 jail 中选择性地停用 **mac\_do(4)**。
* `rules`：规则列表，用于指示允许哪些凭据转换。我们将在下一小节中研究多个示例。`rules` 默认值为空，意味着 **mac\_do(4)** 本身不会允许任何凭据更改。
* `print_parse_error`：当设置规则失败时，是否在控制台和系统日志中打印解析错误。

下面我们先通过示例说明规则，然后再讲如何配置 jail。

## 规则

结合上文给出的 **mdo(1)** 示例，我们这里授权用户 unprivileged\_user（UID 10001）认可 www 用户（UID 80），代表网站管理员角色：

```sh
# sysctl security.mac.do.rules='uid=10001>uid=80,gid=80,+gid=80'
```

在这个示例中，只有一条规则。规则的 `>` 符号用于分隔两部分，左边是“from”部分，也称为“match”，右边是“to”部分，也称为“target”。历史上使用 `:` 作为分隔符，现在仍然可用，但我们认为 `>` 更易读，尤其对于习惯 UNIX 的用户来说，容易将 `:` 误解为类似元素之间的列表分隔符。`>` 是 shell 特殊字符，需要以某种方式引用。为简便起见，我们建议总是将传给 **sysctl(8)** 的值加引号。两个 token 之间可以使用任意数量的空格，这对人工阅读也有帮助，同时也需要 shell 引号以确保正确解析。

“from”部分（上述规则中的 uid=10001）非常直接，用于匹配用户 ID [10](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor012) 为 10001 的进程，从而匹配 unprivileged\_user（和可能其他具有相同用户 ID 的用户）。注意，这里只能使用数值 ID，不能使用用户名。内核确实不识别用户名，从凭据角度看无关紧要。

“to”部分（`uid=80,gid=80,+gid=80`）稍微复杂一些。它包含三条由逗号分隔的子条款。`uid=80` 和 `gid=80` 非常直观：允许在用户 ID 和初始（“主”）组 ID 方面切换到 `www`。最后一条子条款 `+gid=80` 与附加组有关，表示附加组 ID 80 是允许的，但不是强制的。通常，带标志的 gid（这里是 `+`）应用于附加组。其他可能的标志包括 `!` 和 `-`，将在下面示例中说明。

这样的规则允许例如上一节中看到的示例命令，由 unprivileged\_user 执行：

```sh
$ mdo -u www /usr/local/bin/occ
```

需要注意的是，`uid=10001>uid=80,gid=80,+gid=80` 这条规则相当严格，例如，如果用户 www 还属于除 www 之外的其他组，`mdo -u www` 就无法成功执行，因为 `mdo -u www` 会尝试安装密码 [11](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor013) 和组数据库中要求的附加组，而该其他组未出现在规则中。

它同样禁止例如 `mdo -u www -i` 的操作，即切换到用户 www 但保留当前组（假设这些组是与 unprivileged\_user 关联的，且中间未被更改）。如果管理员希望这种操作可以生效，就需要放宽对组的检查。假设 unprivileged\_user 仅属于与其同名且 GID 为 10001 的组，则可以使用：

```sh
# sysctl security.mac.do.rules='uid=10001>uid=80,gid=80,gid=10001,+gid=80,+gid=10001'
```

从这个示例中你大概可以推断出，指定多个目标子条款并使用 `gid` 和 `+gid`，意味着目标凭据中可以存在任意一个指定的组 [12](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor014)。

除了前面提到的两个 **mdo(1)** 使用场景外，这条规则还允许 unprivileged\_user 在切换到 www 的同时，认可组 80 和 10001 [29](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor031)。如果完全不希望出现这种情况，则可以使用以下设置替代：

```sh
# sysctl security.mac.do.rules='uid=10001>uid=80,gid=80,+gid=80;uid=10001>uid=80,gid=10001,+gid=10001'
```

这一次，有两条规则用 `;` 分隔。当存在多条规则时，只要其中一条验证通过，该转换就被允许 [13](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor015)。这种设置仍然允许 `mdo -u www` 和 `mdo -u www -i` 正常工作，同时排除了像 `mdo -u www -i -s +www` 或 `mdo -u www -g 10001` 这样的操作。

如果出于某种原因，即使当前组与 unprivileged\_user 的数据库信息不符，也希望 `mdo -u www -i` 能生效，可以改用：

```sh
# sysctl security.mac.do.rules='uid=10001>uid=80,gid=80,+gid=80;uid=10001>uid=80'
```

上面第二条规则 `uid=10001>uid=80` 允许在不改变当前组的情况下切换用户 ID，因此非常适合在使用 **mdo(1)** 的选项 `-i` 时保留任意当前组。实际上，这条规则是 `uid=10001>uid=80,gid=.,!gid=.` 的简写，其中 `.` 在 gid 的情况下表示当前主组，在 `!gid` 的情况下表示当前附加组，更广泛地说，gid 前带其他标志时亦同。需要注意的是，这个默认部分 `gid=.,!gid=.` 仅在没有目标子条款以 gid 为类型（无论是否带标志）时才被隐含使用。特别地，以下规则 `uid=10001>uid=80,gid=.` 会阻止任何未删除所有附加组的切换，因为规则中没有带标志的 gid 子条款。

另一个 gid 标志 `-` 可用于表示某个组不应包含在最终附加组中。乍一看可能会觉得奇怪，因为允许的组必须在规则中显式指定（除了上一段解释的默认情况）。实际上，这在与 `.` 配合 `+gid` 或 `!gid` 使用时非常有用，可用于排除当前的一些组。例如，如果希望允许 unprivileged\_user 切换到用户 www 并保留其当前组，同时确保 wheel 不出现在最终附加组中，则可以使用：

```sh
uid=10001>uid=80,gid=.,+gid=.,-gid=0
```

[14](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor016)

最后，在规则中，你可以用 `*` 或 `any` 来表示任意可能的用户或组 ID。例如，如果你确实希望允许 wheel 组的成员切换为 root，可以使用如下规则：

```sh
gid=0>uid=0,gid=*,+gid=*
```

基本上表示任意一组目标组都可以。进一步，如果你不想强制必须先切换到 root 再成为其他用户，也可以使用：

```sh
gid=0>uid=*,gid=*,+gid=*
```

这可以简写为：

```sh
gid=0>any
```

需要提醒的是，目前 **mdo(1)** 面向基于角色的方案，因此在任何情况下，即使目标用户是 root，也不会要求输入密码来切换用户。

我们刚刚展示了 **mac\_do(4)** 规则提供的丰富实用可能性，如你所见，它们非常灵活，能够精确表达允许的目标凭据 [15](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor017)。在设计时，我们努力保持语法尽可能易于理解，同时受限于 **sysctl(8)** 值本质上为单行的特性，这要求语法简洁、表达能力足够，并且内核只处理数值 ID，不访问密码和组数据库。即便你一开始不完全理解 `security.mac.do.rules` 的某个具体设置，也不必担心，稍加研究很快就能掌握，因此不要被示例淹没，根据需要花时间学习即可。

规则的更完整和正式的规范，请参见 **mac\_do(4)** 手册页。

## Jail

FreeBSD 中的 Jail 形成一个层级结构 [16](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor018)，其顶层是宿主系统 [17](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor019)。每个单独的 jail 都有参数，其中一些只能在创建时设置，另一些则可以在 jail 运行时从外部修改。

**mac\_do(4)** 支持每个 jail 的配置，通过以下参数实现：

* `mac.do`：每个 jail 的模块模式。
* `mac.do.rules`：适用于该 jail 的规则。

参数 `mac.do.rules` 包含适用规则，其格式与上一节中看到的 **sysctl(8)** 项 `security.mac.do.rules` 完全相同。

通常，希望能够从 jail 外部控制安全参数，这实际上也是创建 jail 时唯一可行的方法。不过，也希望 jail 的行为尽可能接近宿主系统。**mac\_do(4)** 是管理员用来授权凭据转换的工具，jail 中的管理员也应能使用它。

为此，**sysctl(8)** 项 `security.mac.do.rules` 被设计为感知 jail，即它反映当前 jail 的设置，并且可以从 jail 内部设置。jail 内的 `security.mac.do.rules` 与对应 jail 的参数 `mac.do.rules` 实际上是同一个变量，因此它们的值始终一致。外部修改 `mac.do.rules` 会立即在 jail 内生效，反之，从 jail 内读取参数也会反映对 `security.mac.do.rules` 的任何内部修改。

参数 `mac.do` 指示 **mac\_do(4)** 在 jail 中的工作方式。对于支持 jail 的模块的主控参数来说，通常接受或报告以下值：

* `new`：jail 的配置独立于父 jail。
* `inherit`：jail 的配置继承自父 jail。
* `disable`：在该 jail 中禁用 **mac\_do(4)**。

出于显而易见的安全原因，默认值为 `disable`，除非显式设置了 `mac.do.rules`。

你可能会想，这个参数与 `mac.do.rules` 的具体交互关系如何，因为两者似乎有些冗余。如本节开头所述，将 rules 设置为空字符串会导致 **mac\_do(4)** 忽略凭据更改请求，并且 rules 是每个 jail 独立的，这也可以作为每个 jail 的开关来禁用 **mac\_do(4)**，类似于 `disable` 的作用。反之，从 jail 外部设置 `mac.do.rules`，或在 jail 内部设置 `security.mac.do.rules`，总是会建立每个 jail 的设置，从概念上对应于 `new`。

我们引入 [18](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor020) `mac.do` jail 参数有两个原因。首先，大多数支持 jail 的内核模块都会提供一个单一的开关来启用或禁用其在 jail 内的功能，我们认为设置这样一个开关既有利于系统一致性，也提供了比将 rules 设置为空字符串更自然的方式来禁用 **mac\_do(4)**。其次，它提供了引入新的继承模式的机会，即 `inherit` 值，这对于希望一组 jail 行为一致的管理员非常有用。

在探讨继承的具体含义之前，先看看 `mac.do` 与 `mac.do.rules` 如何保持一致。内部上，每个 jail 都有一个标志，用于指示它是否继承自父 jail；如果不继承，则会保存一份规则设置（`mac.do.rules`）及其内部表示，从而避免信息冗余 [19](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor021)。实际上，我们并不存储任何直接对应参数 `mac.do` 的值。该参数在读取时是根据现有数据生成的。由此可知，当继承标志被设置时，读取 `mac.do` 返回 `inherit`；否则，如果没有指定规则（空字符串）返回 `disable`；否则返回 `new`。在显式设置 `mac.do` 时，**mac\_do(4)** 会检查其值是否与 `mac.do.rules` 一致。如果 `mac.do` 设置为 `new`，则必须指定 `mac.do.rules`。对于其他情况，我们应用鲁棒性原则 [20](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor022)，即使严格来说 jail 参数中不应存在空字符串的 `mac.do.rules`，也会容忍其存在。

当 `mac.do` 设置为 `inherit` 时，**mac\_do(4)** 会直接使用适用于父 jail 的配置，而父 jail 本身可能继承自更高层的 jail。主要结果是，对父 jail 中任意规则的更改（直到第一个不继承的父 jail）会自动且立即在继承的 jail 中生效。这减轻了管理员在树状结构中保持多个 jail 配置同步时的工作量。如前所述，在 jail 上显式设置规则（无论通过 `mac.do.rules` 还是 `security.mac.do.rules`）会建立独立的 per-jail 配置，有效打破继承。之后随时可以重新启用继承，只需再次将 `mac.do` 设置为 `inherit`。

与其他 jail 参数一样，你可以在创建 jail 时轻松使用这些参数配置，例如直接在 **jail(8)** 命令行上：

```sh
jail -c name=test_jail path=/ mac.do=inherit
```

或者通过 **jail.conf(5)** 配置。在 jail 运行时修改某些参数，可照常使用 `jail -m`，例如：

```sh
jail -m name=test_jail mac.do=disable
```

## 启动配置

宿主系统上的 **mac\_do(4)** 配置通过 **sysctl(8)** knob（同时也是可调参数）完成，可以使用基本系统提供的两种机制之一在启动时设置。

第一种方式是调整 **loader.conf(5)** 配置，添加如下行：

```sh
security.mac.do.rules='uid=10001>uid=80,gid=80,+gid=80;uid=10001>uid=80'
```

这种方式适用于规则必须在启动早期就可用的情况，或者例如你没有使用基本系统的 **rc(8)** 启动框架时。

否则，你也可以将完全相同的行添加到 **sysctl.conf(5)** 中，当 **rc(8)** 执行时，**sysctl(8)** knob 会相应设置 [21](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor023)。

我们收到一些有限反馈，有少数人觉得 **mac\_do(4)** 仅处理数值 ID 并且 **sysctl(8)** knob 语法过于简洁，不够实用。这本质上是由于将转换规则放在内核中，以应对某些用户态组件可能被破坏的强威胁模型。然而，我们理解大多数人并不需要如此高的安全级别，从密码和组数据库内容生成最终规则的用户态工具对他们可能更有用。曾有人提议设计专用可执行文件和配置文件来支持 **mac\_do(8)**，但目前处于搁置状态，因为我们仍在反思整体设计，包括如何组织可执行文件、未来可能的配置文件和如何避免冲突。如果更多人对此功能感兴趣，这一方向可能会尽快取得进展。

另见本文最后一节，我们计划在 **mdo(1)** 中添加的短期功能，其中之一将弥补这一差距的重要部分。

## 关于 **mac\_do(4)** 设计的一些说明

Baptiste Daroussin 最初启动 **mac\_do(4)**/**mdo(1)** 项目的目标，是在不使用“setuid 可执行文件”的情况下实现基于角色的凭据转换。在高安全性、规范严格的环境中，即便可以安装这些可执行文件，也可能需要经历冗长复杂的安全审计，而且随着可执行文件的升级，这些审计通常需要重新进行。因此，**mac\_do(4)** 被设计为基于内核的替代方案，借助 MAC 框架 \[5]，能够授权非特权进程成功更改凭据。除了减少对“setuid 可执行文件”的依赖，这种架构还能立即降低凭据更改程序被攻击或出现编程漏洞时的潜在影响。

**mac\_do(4)** 的最初实现仅监控 `setuid()` 系统调用，根据匹配原始用户和目标用户的规则授权特定调用。为了让 **mdo(1)** 按目标用户的密码和组数据库修改组信息，**mac\_do(4)** 必须接受任何 `setgroups()` 和 `setgid()` 系统调用。为了避免任意程序独立利用这些调用，**mac\_do(4)** 仅授权来自 **mdo(1)** 程序生成的进程提出的凭据转换请求。

因为允许 `setgroups()` 和 `setgid()` 的任意请求，会严重削弱减少攻击或漏洞影响的效果，我们修改了 **mac\_do(4)**，验证完整的凭据转换，并通过规则指定哪些组可以出现在最终凭据中。

验证或拒绝完整转换从根本上要求原子性，这意味着需要修改传统 UNIX 的安全 API。一种自然的方法是为其添加事务模式，在该模式下连续修改凭据的调用不会立即生效，而是累积这些更改，最终在“提交”时一次性应用。这种方法在一定程度上可以简化对现有程序的修改，和可能的凭据属性扩展，但对内核代码的侵入性较大，并且改变了现有系统调用 MAC 钩子的范式 [22](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor024)。因此，我们采用了另一种方案：新增独立系统调用 **setcred(2)**，能够一次性设置所有凭据属性。这些属性通过一个结构体传递，并可根据需要通过标志扩展或版本控制。新增的 MAC 钩子会传入当前凭据和请求的凭据，使 **mac\_do(4)** 能一次性看到当前状态和目标状态，并据此做出决策。

即便在这些修改之后，我们仍保留了 **mac\_do(4)** 只能授权由 **mdo(1)** 可执行文件生成的进程的限制，因为这可能允许在 **mdo(1)** 内实现额外的转换限制。2025 年谷歌 Summer of Code（GSoC 2025）的一名学生 Kushagra Srivastava 被委派的任务之一是引入对该限制的可配置性，使管理员能够指定 **mac\_do(4)** 可以授权哪些可执行文件。

有些人可能会觉得，将根据规则检查并决定凭据转换的代码移入内核而不是在用户态执行，这一做法显得奇怪，甚至可能构成安全隐患。诚然，一旦内核被攻破，其后果无疑比“setuid 可执行文件”更加灾难性，但我们认为，前者发生的可能性远低于后者，原因如下。

首先，**mac\_do(4)** 所接受的规则是完全明确定义的、自包含的，解析相对简单，也希望是容易理解的。

其次，执行凭据变更的“setuid 可执行文件”通常涉及的组件远多于 **mac\_do(4)** 实际使用的部分。后者本质上只依赖 MAC framework、jail 和 OSD 子系统，这些组件被广泛使用和测试，且不常发生频繁或深层次的变更；而前者则依赖用于读取密码和组数据库的库，这些库可能涉及网络访问，还包括用户态配置解析器，和用于建立新会话所有特征（包括凭据）的代码，这些代码有时甚至属于独立的库，更不用说常见的用户态支撑代码，例如动态链接器。

第三，我们在设计和编写 **mac\_do(4)** 时格外注意清晰性与简洁性，特别关注底层子系统的约束，并确保所依赖的部分不会在不被察觉的情况下被修改，通过断言监控。结果是，尽管经过大量测试，我们至今尚未在 **mac\_do(4)** 核心功能中发现漏洞（这可能是著名的“最后一句话”）。在收到的少量错误报告中，仅有两个在实际场景下被证实为真实问题，而这些场景起初确实未充分考虑或测试 [23](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor025)，这促使我们再次审计代码。我们的 GSoC 学生还被委派开发自动化测试，这些测试将在未来几周内加入官方源代码树。它们将作为额外保障，并在 **mac\_do(4)** 及其依赖子系统演进时帮助维护代码质量。

## 展望未来

这里的核心信息是，尽管我们有一些简单的短期计划，和更宽泛的长期设想，未来方向在很大程度上将取决于当前或潜在用户的反馈。我们渴望听取对小改进或全新功能的建议，无论你是已经在使用 **mac\_do(4)**/**mdo(1)**、计划使用，还是希望使用但现有功能无法满足需求。这将帮助我们在保持整体设计合理性的前提下，选择优先开发的内容。即便仅仅告知你正在使用这些工具，也属于有价值的反馈，因为了解用户数量和使用方式同样重要。

短期内，我们计划为 **mac\_do(4)**/**mdo(1)** 增加类似审计的功能。显示传递给内核的最终凭据有助于检查调用是否符合预期目标。生成 **mac\_do(4)** 规则中授权特定 **mdo(1)** 调用的目标部分，可以帮助管理员构建 **mac\_do(4)** 配置，或更好理解为何某些规则未按预期工作。与 **audit(4)** 子系统集成将允许事后跟踪凭据变更。通过 **syslog(3)** 记录失败尝试，将与 **login(1)** 和其他凭据变更程序的行为保持一致。**mac\_do(4)** 很快还将允许配置它所考虑的可执行文件，以支持 thin-jails 场景和其他用户态程序 [24](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor026)。此外，它还将监控传统系统调用，如 **setuid(2)**，而不仅仅是 **setcred(2)**，每个调用都被视为一次完整的凭据转换。

长期来看，我们可能考虑提供类似 `su` 或 `doas` 的功能，例如要求输入密码，或者更广泛利用 **pam(3)**、设置资源限制和其他属性（如完整登录过程），或仅允许启动特定命令。然而，目前尚不清楚这些功能如何整合到 **mdo(1)** 中，因为它并非“setuid 可执行文件”，也不确定是否应走不同的实现路径。

例如，我们做过初步研究，探讨如何为某些凭据转换增加密码请求支持。**mdo(1)** 可以被任何用户启动，需要一种机制来检查密码，而密码数据库并非所有人都可直接读取 [25](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor027)。这种情况类似于使用 `CAPSICUM` 能力模式 [26](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor028) 的程序，有时需要访问比其直接权限更高的数据。这可以通过让一个不受限制的进程代表能力模式下的进程执行必要的访问来解决。**libcasper(3)** 是 FreeBSD 针对多个服务实现这一思路的方案，包括提供 **cap\_pwd(3)** 服务以访问密码和组数据库。不幸的是，直接使用 **libcasper(3)** 不可行，因为 `cap_enter()` 会创建并连接到一个使用相同凭据启动的进程。**mdo(1)** 将需要一个具有特权的外部守护进程来提供 **cap\_pwd(3)** 服务。

我们还可以设想多种替代方案，开发成本各不相同，包括：将密码配置完全推入 **mac\_do(4)**（类似规则的处理）、将 **mdo(1)** 转为“setuid”可执行文件，但在大部分操作中及调用 **setcred(2)** 时放弃 root 权限，或保持 **mdo(1)** 原样，同时为这些需求提供另一个“setuid”可执行文件 [27](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor029)。然而，除第一种方案外，其他方案提供的安全保证均低于初始方案，而第一种方案则灵活性较低，因为它不支持其他形式的认证，也不支持由用户态最佳施加的额外转换限制 [28](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/credentials-transitions-with-mdo1-and-mac_do4/centner.html#_idTextAnchor030)。

我们希望你会觉得 **mac\_do(4)**/**mdo(1)** 有用！请分享你的反馈，和更广泛的安全需求，即便不一定与本文介绍的框架直接相关。

### 注释

1. 实际上，是特殊的用户 ID 0。“root”这个名称解析为 ID 0，其他名称例如“toor”也可能如此。
2. 更准确地说，分别是有效用户 ID 和保存的用户 ID，和有效组 ID 和保存的组 ID。保存的用户 ID 和组 ID 在 POSIX 规范中正式称为“Saved Set-User-ID”和“Saved Set-Group-ID”。
3. 其他认证机制可以通过 PAM 配置，入门可参见 **pam(3)**，针对特定应用的配置参见 **pam.conf(5)**，规范模块参见 **pam\_unix(8)**。
4. 传递给 **su(1)** 的附加参数会被交给目标用户的 shell，因此程序及其参数必须通过 shell 的参数 `-c`（或等价方式）传递。对于 **sh(1)** 及其后代，它们必须被组合为一个单一参数，由启动的 shell 解释，有时还需要额外一层引用。
5. Mandatory Access Control。参见 **mac(4)**。
6. 此处描述的更新版 **mdo(1)** 通常会随 FreeBSD 14.4 一同发布。
7. 可能存在多个用户映射到同一个数值 ID。**doas(1)** 的一个缺陷是它会静默采用第一个匹配的用户名。**mdo(1)** 通常采取更为保守的做法，不会静默执行非显而易见的操作，这里即使只有一个匹配的用户名，也不会尝试使用它。
8. 即真实、有效和保存的组 ID，相对于补充组而言。
9. 为方便脚本使用，`-s` 实际上与 `-G` 兼容，可用于修改它，因此实际上在处理顺序上会在 `-G` 之后生效，即便它在命令行中出现在 `-G` 之前。不过，目前同时使用 `@` 和 `-G` 会被视为错误（重复指定），这一限制未来可能会被解除。
10. 匹配真实用户 ID，因为它代表用户身份，而非有效用户 ID，从而默认防止另一套规则应用于“setuid 可执行文件”。也就是说，由于在 FreeBSD 上允许非特权用户将真实用户 ID 设置为有效用户 ID，这一区别目前不是绝对限制。
11. 自 FreeBSD 15 起，密码数据库中用户的初始组也会被安装为补充组，这在 Linux/glibc、NetBSD、OpenBSD 和 illumos 上也是如此。为兼容 FreeBSD 14.3，这里演示了目标子句 `+gid=80`，在 15.0 上同样有效，而非 `!gid=80`，后者只允许在 15.0 上转换。
12. 更正式的说法，`gid` 和 `+gid` 目标子句构成逻辑“或”关系。
13. 更正式的说法，规则构成逻辑“或”关系。
14. 如果将 `+gid=.` 替换为 `!gid=.`，规则仅在当前补充组不包含 0 时允许转换，而不是允许转换到除 0 外的所有当前组。后续可能放宽此限制。
15. 有一些例外。我们在前一个脚注中已经看到一个。另一个是：一方面，真实、有效和保存的用户 ID；另一方面，真实、有效和保存的组 ID，会被不加区分对待。将它们分别处理会引入额外复杂性，而收益甚微，因为 FreeBSD 当前的 **setresuid(2)** 允许非特权进程将其任一用户 ID 设置为其他任一用户 ID 的值。我们未来可能会禁止这种行为。
16. 自 FreeBSD 8.0 起。
17. 其全局 jail ID 始终为 0。jail ID 是全局的，但每个进程都会将其直接所属的 jail 的 ID 视为 0。
18. 在较早的实现中，该参数名为 `mdo`，本意是按此处描述的方式工作，但由于 bug 并未实现。
19. 因而会产生一致性问题。
20. 亦称 Postel 定律。“对你所接受的要宽容，对你所发送的要保守。”
21. 由脚本 **/etc/rc.d/sysctl** 完成。
22. 即要么这些钩子的现有实现需要开始支持事务模式，要么我们将完全绕过这些钩子，而后者对使用者来说过于出乎意料。
23. 即在启用资源记账功能时使用 **mac\_do(4)**，和在 64 位架构上运行 32 位的 **mdo(1)**。
24. 该功能的大部分代码是在 GSoC 2025 期间编写的，应当很快会集成。
25. 为了避免泄露可用于离线攻击的密码哈希。
26. 一种进程模式，其中对全局命名空间的大多数访问都会受到限制，只能使用已有的文件描述符。
27. 这可以采取多种形式，例如先将 **doas(1)** 引入基本系统，然后再针对我们独有的安全特性进行定制，尽管这在目标凭据粒度上是一种退步。或者，我们也可以创建一个可执行文件，与 **mdo(1)** 共享部分代码和命令行接口。结合两种方法以取长补短，也可能是可行的。
28. 但它的好处在于不会削弱当前已有的安全保证，因为密码同样会由内核校验。
29. 指不同的真实、有效和保存的组 ID。

***

自 2004 年底起，**Olivier Certner** 在他自己的机器和部分他曾服务公司的机器上长期使用 FreeBSD。在此期间，他积累了一系列私有定制，包括对 rc 脚本和部分内核模块的修改。在 CAD 和金融行业工作 15 余年后，他最近回到纯粹的 IT 领域，尤其专注于操作系统开发。他的主要兴趣集中在内核开发，尤其关注电源管理、安全、调度、文件系统和 jail。他现在是 FreeBSD 基金会的合同开发者。


# printf("Hello, srcmgr\n");

* 原文：[printf(“Hello, srcmgr\n”);](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/freebsd-and-google-summer-of-code-2025/)
* 作者：Mark Johnston

FreeBSD 的开发者和 DevSummit（开发者峰会）的与会者可能对新成立不久的 srcmgr（“源代码管理”）团队有所耳闻。不过，自 srcmgr 开始定期召开会议已有约一年时间，现在似乎是向更广大社区介绍我们的好时机。

srcmgr 是由 FreeBSD src 开发者组成的团队，其目标是帮助组织和协调 FreeBSD 的 src 开发。作为背景，FreeBSD 的开发模式在开源项目中相对独特：它并非由单个开发者或小团队主导项目和做重大决策，而是由一群 src 开发者组成相对扁平的层级结构，当然仍需遵循维护者规则和惯例。开发者们共同承担推动 src 开发的责任，包括发现或修复 bug、撰写文档、代码审查、增加功能和测试等工作。

这种开发模式运作得相当不错——例如，FreeBSD 项目本身的历史比部分开发者的资历还要久——但这种开发模式也存在不足。单个开发者的正式责任较少；期望他们贡献代码并相互协作，但缺乏严格的正式监督。这种模式在开发者人数较少、彼此熟悉且互相信任的小团队中表现良好，并能相互激励；这也是 FreeBSD 早期开发的特点。然而，随着时间推移，问题开始出现：资深开发者逐渐离开，系统变得更复杂、维护难度增加，同时新开发者数量上升，这给经验丰富的导师带来压力，而这些导师通常自由时间有限。

以往，FreeBSD 核心团队（Core Team）提供后备支持：每当出现争议或树结构中被忽视的部分出现问题时，他们会介入。这种方式在历史上行之有效，因为核心团队长期主要由 src 开发者组成，尽管它代表的是整个 FreeBSD 项目。但在最近几个任期中，Ports 团队在核心团队中的代表性有所增加。因此，近期的核心团队在处理 src 相关问题上的资源相对有限，同时核心团队的注意力应更多集中于项目的长期战略方向，而非日常事务。

这就是 srcmgr 的由来。srcmgr 于 2024 年 10 月 8 日在 FreeBSD 内部开发者邮件列表上正式公布，目前成员包括我（Mark Johnston）、Ed Maste、Warner Losh 和 John Baldwin。我们还有五名“潜水员”，即参加 srcmgr 会议并参与讨论但尚未正式成为成员的开发者；他们通过这种方式试水，以决定是否愿意正式加入。简而言之，srcmgr 在 src 树中的角色类似于 portmgr 和 doceng 团队在 Ports 树和 doc 树中的作用：我们提供监督，并帮助解决该领域特有的挑战。

尽管早些时候已有组建 srcmgr 团队的想法，但我最初的接触是在 2023 年 BSDCan 与 John Baldwin 和 Ed Maste 的交流中。我们认识到核心团队（Core）往往负担过重，无法主动引导 src 的开发。同时，我对我们处理 bug 报告、新贡献者和代码评审的整体方式感到沮丧。作为个人开发者，要在完成常规有偿工作之余，还兼顾这些事务，实在是太耗费精力。

那么，srcmgr 在实际工作中做什么呢？我们的 [章程](https://www.freebsd.org/srcmgr/charter/) 概述了主要职责。我们每两周开一次会，约两小时；前一小时用于审查议程事项、讨论并提供状态更新，后一小时通常用于处理最近的 [src bug 报告](https://bugs.freebsd.org/bugzilla/) 和/或 [GitHub pull requests](https://github.com/freebsd/freebsd-src/pulls)。

该团队的核心职责是对 src commit bit 投票：当一名 src committer 与贡献者合作，并认为该贡献者能够合理使用 src commit 权限时，可以向 srcmgr 提出申请，由团队投票决定是否授予 commit 权限。这个流程通常比较直接、无争议，实际上消耗的时间很少。

此外，我们还会处理一些“维护”任务，例如禁用不活跃开发者的 commit 权限、推进废弃占用项目资源的过时或无人维护的功能、更新开发者政策，和在 FreeBSD 集群管理员团队的大力协助下，维护一些基础设施，比如 git commit hooks。

我们的大部分时间都花在推动各项计划上，主要目标是：1）提高有经验开发者的生产力，2）让新来者更容易参与贡献。针对第一个目标，我们一直在推动创建各种工具，以降低 FreeBSD 的开发难度。当前，对新的 FreeBSD 开发者（有兴趣的贡献者、GSoC 学生、使用 FreeBSD 的公司员工等）而言，搭建一个能够快速可靠测试 src 树修改的环境仍然过于困难。我们拥有一套规模庞大的回归测试套件，但真正运行它需要相当多的前期配置，并且难以自动化。建立一个高效、交互式的“编译-编辑-测试”循环同样不容易。许多开发者都有各自的定制脚本和工作流来实现这一点，但这意味着很多人实际上是在重复造轮子；此外，我们也缺乏一套可供新来者快速采用并定制的成熟“现成”方案。从总体上解决这一问题本身就是一项挑战，因为 FreeBSD 是一个规模庞大、包含众多组件的代码库，而这些组件往往需要专门化的开发方式。即便如此，仍然存在大量改进空间。

围绕工具这一主题，我们还在开发脚本，以简化 MFC 操作，并以程序化方式捕捉某些类型的问题，例如 stable 分支中缺失回溯提交的情况，尤其是在提交 B 修复了提交 A 中的缺陷，但在合并提交 A 时却遗漏了提交 B 的场景。另一个重点方向是为项目成员分流和甄别进入的工作：Bug 报告、代码评审请求和贡献者补丁。尽管单个 FreeBSD 开发者在处理这类日常请求上投入了大量时间，但目前缺乏一种有效的整体监督机制，来确保高优先级问题不会被遗漏。srcmgr 由熟悉 src 树的开发者组成，能够迅速判断在某个具体问题上应当“标记”谁来参与协助，从而推动问题向前解决。

最后，一项正在持续推进的计划是举办“bug 分拣”活动，通常通过 Zoom 或 meet.freebsd.org 进行。我们会提前在开发者邮件列表上公布这些活动，并邀请大家参与大约 3 小时的分流处理和缺陷修复工作。我通常会通过逐条审阅具体的 Bug 报告来主持这些活动，并寻找能够快速取得进展的机会：将问题指派给相关领域的专家，向提交者提出后续问题，和与通话中的其他人讨论问题。参与方式完全自由；有些人会在后台默默处理特定 Bug，同时留意讨论内容，另一些人则会跟随整体流程，或并行对 Bug 分流处理。

这些活动（和 srcmgr 的整体参与）在很大程度上保持了我的动力：与其他开发者的实时互动有助于维持我们这些远程工作的人的投入感，而能够在 Bug 和拉取请求（PR）上迅速取得具体、可见的进展，也让积压问题不至于显得难以承受。当我们能够及时处理不断进入的问题和工作请求时，就更容易对项目产生自豪感，而这种协作也有助于维系我们共同的责任感。

srcmgr 还有很多希望为项目提供帮助的工作内容，我们也有许多计划将在来年持续推进，尤其是在 FreeBSD 15.0 发布日期日益临近、并且我们能在假期中获得一些休整时间的情况下。

如果你有任何反馈或想法，欢迎随时通过电子邮件与我们联系：<srcmgr@FreeBSD.org>。

***

**Mark Johnston** 是居住在加拿大安大略省多伦多的 FreeBSD 开发者。不坐在电脑前时，他喜欢和朋友一起参加城市躲避球联赛。


# FreeBSD 与 GSoC 2025

* 原文：[FreeBSD and Google Summer of Code 2025](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/freebsd-and-google-summer-of-code-2025/)
* 作者：Joe Mingrone

GSoC 2025（谷歌 Summer of Code）的圆满完成标志着 FreeBSD 第 21 年参与该项目。今年有三大亮点。首先，我们收到了 64 份申请，是去年的两倍多，约 2023 年的四倍。人工智能工具可能推动了申请数量的激增，虽然产生了一些低质量提交，但大部分申请整体质量仍然很高。其次，我们注意到来自南亚的兴趣显著增加。鉴于 FreeBSD 社区调查显示 85% 的受访者来自欧洲或北美，来自亚洲和其他地区的关注尤为可喜。第三，被接受项目的数量和质量令人鼓舞。在 185 个参与组织中，共有 1200 个 GSoC 项目，而 FreeBSD 的 12 个被接受项目几乎是每个组织平均值的两倍，而且与去年不同，今年 12 个项目均顺利完成。

在讨论各个项目之前，先回顾一下我们参与 GSoC 的原因。该项目需要大量投入，从组织申请流程、定义项目创意，到指导贡献者。投入这些时间和资源是否值得——这些本可直接用于开发——短期技术角度来看尚可争议；虽然部分项目带来了可提交的代码，但很多并未如此。然而，从长期来看，答案更明确。GSoC 在吸引和培养新贡献者方面发挥重要作用。自 2022 年以来，已有五名 FreeBSD 新提交者通过 GSoC 加入，而一名 2017 年参与者在后来成为了第 12 届核心团队成员。

## GSoC 2025 项目

### 改进 Sockstat UI

对于那些不熟悉 **sockstat(1)** 的人来说，它是用于列出打开的互联网或 Unix 域套接字的命令。Damin Rido 的项目目标是通过支持动态列宽和集成 libxo 提供结构化输出，从而增强命令输出的灵活性。以下三条 Damin 的 pull request 均已合并到 src 中：

* 添加自动列宽功能并移除选项 `-w`：[freebsd/freebsd-src#1720](https://github.com/freebsd/freebsd-src/pull/1720)
* 重新引入标志 `-w` 以自动调整列宽：[freebsd/freebsd-src#1746](https://github.com/freebsd/freebsd-src/pull/1746)
* 添加 libxo 支持：[freebsd/freebsd-src#1770](https://github.com/freebsd/freebsd-src/pull/1770)

### vmm(4) 对 QEMU 的加速支持

VMM（Virtual Machine Monitor）模块是 **bhyve(4)** 虚拟机监控器的内核组件，可通过 **vmm(4)** 访问。除了 bhyve，另一款在 FreeBSD 上被官方支持的广泛使用的机器模拟器和虚拟化器是 QEMU。然而，在本项目之前，FreeBSD 上的 QEMU 只能使用软件模拟（通过 Tiny Code Generator），因为缺乏硬件加速虚拟化支持。换句话说，QEMU 在 FreeBSD 上无法利用宿主 CPU 的虚拟化扩展直接运行虚拟机代码。这导致相比硬件辅助虚拟化，CPU 开销显著增加，来宾性能下降。

Abhinav Chavali 的 GSoC 2025 项目主要目标是将 VMM 加速支持集成到 FreeBSD 上的 QEMU。他通过修改 QEMU 的内存管理层，使其与 VMM 内核分配的来宾内存协作，实现了这一目标。同时，他调整了 VMM，使某些非关键设备（如 HPET 和 RTC）成为可选项，从而允许 QEMU 在用户空间模拟它们。这实现了一种混合中断模型（内核处理虚拟 LAPIC，用户空间模拟 IOAPIC），有潜力在 FreeBSD 下达到与 bhyve 相当的性能水平。

最新进展是，Abhinav 成功在 QEMU 上使用 VMM 加速启动了 FreeBSD 14。相关代码可查看 [这里](https://github.com/freebsd/freebsd-src/compare/main...dumrich:freebsd-src:vmm-qemu-mods-16)。

### Rust FreeBSD 设备驱动的测试与开发

近年来，在 FreeBSD 和 Linux 社区中引入 Rust 的讨论不断增加。相关讨论示例可参考 Linux 内核邮件列表上发布的 [Rust 支持 RFC](https://lkml.org/lkml/2021/4/14/1023) 和 [FreeBSD hackers 邮件列表的讨论](https://lists.freebsd.org/archives/freebsd-hackers/2024-January/002823.html)。在这两个社区中，讨论焦点各有不同：一些反对者担心会“使构建时间翻倍”，而支持者则认为 Rust 可以使某些工具的实现变得更容易甚至成为可能。除了讨论之外，已经取得了实际进展：Johannes Lundberg 在其 [硕士论文项目](https://github.com/johalun/rustkpi) 中创建了 Rust KPI 和网络驱动，而 David Young 则开发了简单的 FreeBSD 内核 Rust 模块，并在 [文章中总结了社区采用 Rust 的现状](https://www.nccgroup.com/research-blog/writing-freebsd-kernel-modules-in-rust/)。

本项目“Rust FreeBSD 设备驱动的测试与开发”基于此前在 FreeBSD 中引入 Rust 的努力。其主要目标之一是为 Rust 内核模块创建测试与持续集成框架。Aaron 在此 [视频](https://www.youtube.com/watch?v=y82-t1tDLWg) 中概述了他的 Rust echo 驱动，并在此 [总结文档](https://gist.github.com/Acesp25/8928e35e710fdce1896b5448fc6327df) 中说明了项目内容。相关代码库如下：

* <https://github.com/Acesp25/rustdrv>
* <https://github.com/Acesp25/freebsd-kernel-module-rust>
* <https://github.com/Acesp25/RustKLD>

### FreeBSD 磁盘管理工具

在 Braulio Rivas 的 GSoC 2025 项目之前，FreeBSD 缺少类似 Linux 上 GParted 的用户友好型磁盘管理工具，用于分区、调整大小、移动和管理文件系统。本项目旨在填补这一空白，开发了新的分区管理工具 [geomman](https://www.freshports.org/sysutils/geomman)。项目完成后，geomman 支持以下操作：

* 在同一块磁盘或不同磁盘之间复制和粘贴分区
* 扩展 UFS、NTFS、ext2、ext3 和 ext4 文件系统
* 缩小 NTFS、ext2、ext3 和 ext4 文件系统
* 可视化选择空闲空间以放置分区
* 创建 exFAT、NTFS、ext2、ext3 和 ext4 文件系统
* 检查文件系统：`fsck_ufs` (UFS)、`fsck_msdos` (FAT)、`fsck.exfat` (exFAT)、`ntfsfix` (NTFS) 和 `e2fsck` (ext)
* 创建并标记分区
* 创建并加密分区

剩余工作：

* 添加 ZFS 支持
* 解决移动分区时的问题
* 编写测试用例

上游代码库：<https://gitlab.com/brauliorivas/geomman>

### 为 mkimg 添加 QCOW2 压缩镜像支持

QCOW2（QEMU Copy-On-Write version 2）是一种广泛使用的虚拟化磁盘镜像格式，支持精简配置（thin provisioning）和内建压缩等功能。FreeBSD 的 **mkimg(1)** 工具能够创建多种格式的磁盘镜像，包括 QCOW2。但此前，`mkimg` 对 QCOW2 的支持有限，无法直接创建压缩的 QCOW2 镜像。

在本次 GSoC 项目中，Christos Komis 改进了 `mkimg`，实现了以下目标：

* 支持 QCOW2 v2 压缩镜像
* 支持 QCOW2 v3 压缩与非压缩镜像
* 更新用户界面以提供新功能
* 扩展测试套件以验证功能正确性
* 更新 man 页以反映新增功能
* 代码重构以提升可读性与可维护性

实现已通过全面测试，并可直接提交至主代码库。用户现在可以直接使用 `mkimg` 生成压缩 QCOW2 镜像，简化虚拟机镜像生成流程，减少对外部转换工具的依赖。Christos 的代码可在此查看：<https://github.com/ckkomis/freebsd-src/commits/mkimg/qcow2-compression/>

### 在 Loader 中初始化 ACPI 并添加 Lua 绑定

Kayla Powell 的项目将 ACPICA 库的初始化扩展到 FreeBSD 的 loader 中，确保在内核加载前完整的 ACPI 命名空间可用。该工作替换了原本较为零散的 bootloader ACPI 处理方式，通过在 loader 内调用标准 ACPICA 函数（如 AcpiInitializeSubsystem、AcpiLoadTables、AcpiWalkNamespace、AcpiEvaluateObject）实现。这使得在启动早期就能一致发现和查询 ACPI 对象。为了保持 loader 的轻量化，仅移植了初始化或脚本所必需的 ACPICA 组件，省略了许多非必要功能。

在此基础层之上，项目引入了 Lua 绑定，使脚本在 loader 中可以访问 ACPI 命名空间和对象评估功能。换言之，现在可以在内核加载前编写 Lua loader 脚本来遍历 ACPI 树、检查设备表条目、读取或附加 ACPI 节点数据。项目同时包含了单元测试和回归测试，例如对比 C 与 Lua 的命名空间输出，和在不同架构下构建 loader。

Kayla 的项目总结可见她的博客：<https://kmpow.com/content/gsoc-writeup> 相关 pull requests：[1818](https://github.com/freebsd/freebsd-src/pull/1818)、[1819](https://github.com/freebsd/freebsd-src/pull/1819) 和 [1843](https://github.com/freebsd/freebsd-src/pull/1843)

### 改进 mac\_do(4) 与 mdo(1)

Kushagra Srivastava 的项目旨在通过改进内核端 MAC 模块 **mac\_do(4)** 及其配套的用户空间工具 **mdo(1)**，增强 FreeBSD 的凭证转换基础设施。该项目目标是避免依赖传统的 setuid 可执行文件（存在固有风险），在 FreeBSD 的 MAC 框架下实现可控的、精细化的凭证转换。

在内核方面，**mac\_do(4)** 被扩展以支持针对每个 jail 的授权可执行文件配置，使管理员能够明确指定某个 jail 内哪些二进制文件允许请求凭证转换，而不局限于硬编码路径。此外，它现在会拦截标准的凭证更改系统调用（如 **setuid(2)**、**setgid(2)**、**setgroups(2)**），并将其视为完整的转换请求，受 **mac\_do(4)** 策略模块管理。

在用户空间方面，**mdo(1)** 工具改进了凭证转换请求的精细控制。它现在支持通过 `-g`、`-G`、`-s` 等标志明确覆盖用户/组 ID 及补充组，并新增了选项 `--print-rule`，用于显示匹配请求转换的 **mac\_do(4)** 规则，从而帮助管理员创建规则并调试。

这些增强功能使凭证转换更加灵活、安全，并与 FreeBSD 的 jail 和 MAC 框架紧密集成，减少了对高风险 setuid 二进制文件的依赖，同时提升了审计能力和控制力。

Kushagra 的项目详细介绍可参见：<https://thesynthax.hashnode.dev/my-google-summer-of-code-journey-part-3>

### 缩短 FreeBSD 启动过程

Lahiru Gunathilake 的项目延续了以往提升 FreeBSD 启动速度的工作，通过分析启动序列、识别瓶颈并实施优化来缩短启动时间。利用内置的 TSLOG 跟踪框架，Lahiru 生成了启动路径的火焰图，以了解时间消耗集中在哪些环节，和可以消除哪些不必要的延迟。

分析发现的热点环节包括设备附加阶段、大型文件系统子系统（尤其是 ZFS）的初始化，和 `vfs_mountroot`（根文件系统挂载）中的 sleep 延时，随后优化工作进入实施阶段，包括：

* 将基准测试缓冲区从 16MB 减小到 256KB，使启动时间从 989 ms 缩短到 67 ms
* 缩短键盘和鼠标初始化中的长等待循环，并引入可调参数 `hw.atkbd.short_delay`
* 消除对 USB 设备的不必要等待

整体来看，Lahiru 报告称在内核初始化阶段减少了 8.2 秒，ZFS 和输入设备优化后减少了 3.5 秒，跳过 USB 启动等待后减少了 1.9 秒。

### WiFi 管理界面

Muhammad Saheed 承担了开发 FreeBSD 上 WiFi 网络管理工具的项目，包括 CLI 工具 **wutil** 和 TUI 工具 **wutui**。目标是覆盖“工作站模式操作，例如扫描、连接/断开无线网络”，并将这些功能封装到更清晰、一致的用户界面中。其他完成的工作包括：

* 更新相关 man 手册
* 创建 [wutil 的 Ports](https://www.freshports.org/net/wutil)
* [为 Port security/wpa\_supplicant 添加 libwpa\_client 构建选项](https://cgit.freebsd.org/ports/commit/?id=edaddcd1a5bb374e58de0d4f99a7cccf6aed09ec)
* 创建 [libifconfig 的 Port](https://www.freshports.org/net/libifconfig)
* 将所需的 ifconfig 辅助功能提取到 libifconfig 中 ([D52130](https://reviews.freebsd.org/D52130), [D52131](https://reviews.freebsd.org/D52131))

更多内容可参考 Muhammad 的 [博客](https://saheed.tech/writings)。

### FreeBSD ExtFS 日志功能

Pau Sum 的项目旨在为 FreeBSD 的 ext2fs 文件系统实现 Linux 兼容的日志功能。FreeBSD 原有的 ext2fs 驱动已能挂载和使用 ext2/3/4 文件系统，但缺乏日志功能，因此非正常关机后需要通过 fsck 较长时间恢复。Pau 的工作引入了磁盘日志感知和事务记录，提高了崩溃恢复能力和文件系统完整性，使 FreeBSD 能够在 ext3/4 卷上挂载和重放日志，并与 Linux 系统更好互操作。

设计上没有完全复制 Linux 的日志框架，而是实现了传统的元数据日志（metadata-only journal），使用相同的磁盘结构以保持兼容性。新代码定义了关键数据结构：

* **ext2fs\_journal**：表示活动日志
* **ext2fs\_journal\_transaction**：分组原子元数据更新
* **ext2fs\_journal\_buf**：跟踪每个块的状态

核心文件系统操作（如 `ext2_link`、`ext2_mkdir`、`ext2_write`）扩展了日志钩子，用于开始事务、标记脏数据和结束事务。提交事务时会写入描述符块、元数据块、撤销块和提交块，并执行检查点操作以刷新磁盘更新。恢复分三步进行：验证事务范围、收集撤销块、重放未撤销的元数据。

项目结束时，12 个日志钩子中已有 11 个完成，正开发截断和基于 extents 的操作。计划的扩展包括对 extents 和截断的日志支持、日志完整性校验、更全面的崩溃模拟和文档清理。已通过 [fsx](https://www.freshports.org/devel/fsx/) 和 [dirconc](https://www.netbsd.org/~riastradh/tmp/dirconc.c) 测试该实现。可在 [他 Fork 的 FreeBSD src 仓库](https://github.com/pxsum/freebsd-ext34/tree/extfs-journaling) 获取 Pau 的代码。

### 电源分析工具

Kasyap Jannabhatla 的项目旨在为 FreeBSD 提供细粒度的、基于进程的电源使用分析，以弥补 ACPI 全系统功耗统计的局限。受 [Performance Co-Pilot (PCP)](https://github.com/pxsum/freebsd-ext34/tree/extfs-journaling) 和 RAPL（Running Average Power Limit）启发，该项目实现了 FreeBSD 原生框架，而非移植 Linux 的 PowerTop。解决方案包括：

* **内核组件**：收集与功耗相关的指标
* **用户态守护进程**：提供命令行界面，跟踪每个进程的 CPU 使用和能耗

通过结合 RAPL 读数和由 `kvm_getprocs` 得到的每进程 CPU 利用率，该工具能够估算单个进程和线程的能耗，为精细化电源分析和 FreeBSD 电源管理的后续增强提供基础。

开发过程中，重点包括：

* 构建轻量级的守护进程架构
* 实现用于结构化访问 RAPL 数据的库 **librapl**
* 与进程计数整合，计算每线程的能耗

测试阶段使用了 OpenSSL Speed 等压力基准，并在多核运行环境下使用线程 ID 精确处理能耗计算。项目结束时，框架可可靠报告每进程随时间变化的能耗，所有交付物（守护进程、库和文档）均已完成。

实现代码可在 [GitHub 仓库](https://github.com/cheeseburger9309/freebsd-src/tree/gsoc2025-powerprofilingtool) 获取。

### 将 FreeBSD 移植到 QEMU microvm

[QEMU microvm](https://www.qemu.org/docs/master/system/i386/microvm.html) 是受 [Firecracker](https://firecracker-microvm.github.io/) 启发的极简虚拟机。虽然 FreeBSD 已被移植到 Firecracker，但该平台仅支持 Linux，限制了可移植性。本项目由 Wyatt Geckle 负责，目标是开发 FreeBSD 内核的 QEMU microvm 版本，借鉴 Firecracker 移植经验。为此，Wyatt 复现了之前的移植尝试，并分析了内核初始化问题，尤其是导致长启动时间的定时器配置问题。通过研究 NetBSD 的 microvm 实现和 Intel 文档，项目发现了 FreeBSD 的 timecounter 与 APIC 初始化存在的差异。

当前 FreeBSD microvm 内核可在 QEMU microvm 下启动，但未调用 `tc_init`，限制了可用定时器。Firecracker 移植仍存在 MPTables 问题，需要进一步研究。尽管存在这些限制，该项目拓展了 Wyatt 对 FreeBSD 与 NetBSD 内核内部机制、虚拟化和 microvm 平台的理解，并生成了详细文档，涵盖 FreeBSD 在 microvm 和 Firecracker 中的构建、运行与调试方法。该工作为未来对 FreeBSD、QEMU microvm 和 Firecracker 的支持，和可复现的 microvm 调试流程奠定了基础。

更多信息可见 [Wyatt 的博客](https://wgeckle80.github.io/blog/categories/port-freebsd-to-qemu-microvm/)，代码在 [Wyatt 的 src 仓库 fork](https://github.com/wgeckle80/freebsd-src/tree/microvm-port)。

### 导师峰会

Robert Clausecker，FreeBSD GSoC 联合管理员及导师，代表 FreeBSD 参加了 10 月 23–25 日在德国慕尼黑举办的导师峰会。会议讨论了 AI 生成及垃圾申请的问题，目前尚无确定方案。其中一个考虑方向是要求申请者在提交申请前与潜在导师会面，以保证导师、贡献者与项目的良好匹配，这也是 FreeBSD 过去几年已推广的做法。Robert 还与 Linux 基金会代表会面，探讨两家基金会在吸引学生参与操作系统开发方面的潜在合作。

峰会还涉及开源项目资金问题。Robert 与 RTEMS（一种用于多种设备的实时操作系统）的开发者交流，了解到其系统中集成了 FreeBSD 的网络栈。他还会见了 GSoC 组织者 Stephanie Taylor，分享了 GSoC 对 FreeBSD 的积极影响。当然，他也带回了一些周边，包括一件 T 恤和一对 GSoC(k)s 纪念袜。

## 最终思考

回顾 GSoC 2025 项目的成功令人欣慰，但 GSoC 2026 很快就会到来。和往常一样，要重复今年的成功，最大的挑战在于设计合适的项目、寻找投入的导师，和将申请者与导师匹配。幸运的是，GSoC 最近的变化将有助于缓解这些问题。

* **灵活的时间表与项目规模**：项目时间安排更加灵活。贡献者和导师可选择小型（90 小时）、中型（175 小时）或大型（350 小时）项目，总项目时间可延长，超出标准 8 周（小型）或 12 周（中型和大型）的限制。
* **扩展的贡献者池**：申请者群体扩大。参与者不必是大学生，任何开源新手都有资格参与。

但目前，让我们先享受这一集体成功，并肯定今年项目付出的巨大努力。

## 致谢

感谢所有向 <https://wiki.freebsd.org/SummerOfCodeIdeas> 提供项目创意的人，尤其是 2025 年的导师们：

* John Baldwin
* Olivier Certner
* Robert Clausecker
* Pedro Giffuni
* Tom Jones
* Warner Losh
* Ed Maste
* Getz Mikalsen
* Joe Mingrone
* Mehdi Mokhtari
* George Neville-Neil
* Colin Percival
* Alfonso Sabato Siciliano
* Alan Somers
* Toomas Soome
* Fedor Uporov
* Aymeric Wibo

***

**Joe Mingrone** 是 FreeBSD Ports 开发者，为 FreeBSD 基金会工作。他与妻子及两只猫共同居住在加拿大新斯科舍省的达特茅斯。


# 嵌入式 FreeBSD：构建 U-boot

* 原文：[Embedded FreeBSD: Building U-boot](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-15-0/building-u-boot/)
* 作者：Christopher R. Bowman

![嵌入式 FreeBSD](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-51144a079f1d1f619f70b6128505568a3888d73c%2Fbuilding-u-boot-01.png?alt=media)

一位读者写信告诉我，他在编译 U-boot 时遇到了困难，所以我想走一遍这个过程，因为我也打算启动另一块 Zynq 开发板，反正也得经历这一流程。我需要声明：下面所写内容是准确的，但这些系统很复杂，我可能在某些细节上有误。如果你发现我写错了，欢迎指正。

正如我们之前讨论的，[U-boot](https://u-boot.org/) 是第二阶段和第三阶段的引导加载程序，它运行后会加载 FreeBSD 的加载器，而 FreeBSD 加载器则负责加载 FreeBSD 内核本身。U-boot 是个开源社区的项目，广泛用于各种系统以提供启动服务。相关文档可通过其官网获取：[U-boot 文档](https://docs.u-boot.org/en/latest/index.html)。

在 AMD/Xilinx Zynq 芯片上，第一阶段引导加载程序存放在 Zynq 芯片本身的 BootROM 中。Zynq 的启动流程可参考 [Zynq 7000 SoC 技术参考手册](https://docs.u-boot.org/en/latest/index.html) 第 6 章“Boot and Configuration”。简要描述是：设备上电时，会采样一些引脚，根据其状态选择多种启动方式之一。这允许开发板上的跳线（根据 [Zybo Z7 参考手册](https://digilent.com/reference/programmable-logic/zybo-z7/reference-manual?redirect=1) 图 2.1 的 JP5）选择启动方式，其中之一是从 SD 卡启动。如果选择这种方式，BootROM 代码会在 SD 卡的 FAT16 或 FAT32 分区上寻找名为 `boot.bin` 的文件。这就是 U-boot 所称的二级程序加载器（SPL）。Zynq 芯片包含少量板载 RAM，因此 BootROM 能加载的程序大小受到限制。在非裸机应用中，SPL 必须包含足够的代码来配置 Zynq 芯片（PLL、内存接口等），以便启动内存系统并加载完整的 U-boot。完整的 U-boot 是功能更丰富的版本（支持文件系统等）。

我们非常幸运，因为 Zybo Z7 已经在 U-boot 中得到支持。所以，我们只需要让编译流程能够正常运行即可。U-boot 通常是在主机系统上为目标系统构建的。前面提到的 U-boot 文档建议，我们应通过环境变量来设置用于交叉编译的编译器。我们将使用 GCC 编译器和 GNU 工具交叉编译，因此需要安装这些软件包并设置交叉编译器。U-boot 还使用 `gmake` 构建，而不是标准的 BSD `make`，因此我们需要安装 `gmake`，和其他一些软件包：

```sh
pkg install gmake
pkg install arm-none-eabi-gcc
pkg install bison
pkg install gnutls
pkg install gmake
pkg install pkgconf
pkg install coreutils
pkg install dtc
pkg install gdd

setenv CROSS_COMPILE arm-none-eabi-
```

奇怪的是，你使用的是 `arm-none-eabi-` 而不是 `arm-none-eabi-gcc`，但这不是笔误。

接下来，我们需要为目标板配置 U-boot 源代码树。Zybo Z7 开发板由 `configs` 目录下的 `xilinx_zynq_virt_defconfig` 支持。该配置支持多块板，其中就包括 Zybo Z7。要配置源代码树，我们运行：

```sh
make xilinx_zynq_virt_defconfig
```

但我们必须注意使用 GNU `make`，而不是 BSD `make`。为此，我创建了一个目录，其中有一个名为 `make` 的符号链接指向 **/usr/local/bin/gmake**，并将该目录放在我的 `PATH` 的最前面。这种方法运行得很好。

接下来，我们只需调用 `make` 并等待（如果你有多余的 CPU 核心，我强烈建议使用参数 `-j`）。它像我一样报错了吗？

我得到的输出如下：

```sh
make[1]: *** [scripts/Makefile.xpl:257: spl/U-boot-spl-align.bin] Error 1
make: *** [Makefile:2358: spl/U-boot-spl] Error 2
make: *** Deleting file 'spl/U-boot-spl'
```

**scripts/Makefile.xpl** 中的相关行如下：

```sh
$(obj)/$(SPL_BIN)-align.bin: $(obj)/$(SPL_BIN).bin
        @dd if=$< of=$@ conv=block,sync bs=4 2>/dev/null;
```

如果你去掉输出重定向到 **/dev/null**，你会看到 `dd` 的报错信息：

```sh
dd: record operations require cbs
```

看来 FreeBSD 的 `dd` 与 GNU 版本在命令行上并不完全兼容。最初，我只是通过安装 GNU 版本的 `dd` 并在本地 `bin` 目录创建符号链接来使用，但实际上你只需从 `dd` 命令中删除 `block` 即可。

此外，可以通过设置这个 `make` 变量 `V` 来控制构建输出的详细程度。如果你的构建失败，我强烈建议使用单核并设置 `V=1` 再次运行：

```sh
make V=1
```

如果一切顺利构建完成，你应该会得到一个 `U-boot.img` 文件和一个 **spl/boot.bin** 文件。它们分别是完整的 U-boot 和二级程序加载器。将它们复制到 SD 卡上，然后尝试启动吧！

等等，没成功？嗯！正如我之前所说，这个配置支持多块开发板，而默认的设备树并不是针对 Zybo Z7 的。参考前面提到的板级文档，我们可以通过设置 `DEVICE_TREE` 来指定默认的设备树：

```sh
setenv DEVICE_TREE zynq-zybo-z7
```

这会覆盖配置文件中的默认 DTS。重新构建并尝试启动。等等，怎么又出了问题？内核可以加载，但在探测硬件时崩溃？哦，对。FreeBSD 对 DTS 的要求与 Linux 不同。某些硬件识别所需的 `compat` 字符串不同，而且 FreeBSD 似乎要求一些 `clock-frequency` 属性，虽然我不确定这些值是否被实际使用。或许在 FreeBSD 驱动中添加与 Linux 期望一致的 `compat` 值是合理的，但我并不是提交者。我不得不在 DTS 文件 **arch/arm/dts/zynq-zybo-z7.dts** 中添加以下内容：

```sh
&sdhci0 {
        compatible = "arasan,sdhci-8.9a", "xlnx,zy7_sdhci";
   U-boot,dm-pre-reloc;
        status = "okay";
};

&devcfg {
compatible = "xlnx,zynq-devcfg-1.0", "xlnx,zy7_devcfg";
status = "okay";
};

&global_timer {clock-frequency = <50000000>;};
&ttc0 {clock-frequency = <50000000>;};
&ttc1 {clock-frequency = <50000000>;};
&scutimer {clock-frequency = <50000000>;};
```

既然我们已经学会了如何构建 U-boot，现在看看是否可以将其做成 Port。U-boot 有一整套 Ports，它们都是基于 Port `U-boot-master` 构建的。要使用这些 Ports，我们需要包含 master Port 的 Makefile。我们必须指定开发板、型号和应使用的配置。对于上面所做的修改，我们还有一些补丁，最终可以得到如下内容。

```sh
MASTERDIR=      ${.CURDIR}/../U-boot-master

MODEL=          zybo-z7
BOARD_CONFIG=   xilinx_zynq_virt_defconfig
FAMILY=         zynq_7000

EXTRA_PATCHES=  ${.CURDIR}/files

BUILD_DEPENDS+= gdd:sysutils/coreutils

COMMENT=        ported by Christopher R. Bowman <my_initials>@ChrisBowman.com

.include "${MASTERDIR}/Makefile"
```

希望这些专栏对你有所帮助。欢迎提出你的评论或反馈，你可以通过 <articles@ChrisBowman.com> 与我联系。

***

**Christopher R. Bowman** 最早在 1989 年在约翰斯·霍普金斯大学应用物理实验室地下两层的 VAX 11/785 上使用 BSD。90 年代中期，他在马里兰大学设计第一颗 2 微米 CMOS 芯片时使用了 FreeBSD。从那时起，他一直是 FreeBSD 用户，并对硬件设计及其驱动的软件感兴趣。在过去 20 年里，他一直从事着半导体设计自动化行业的工作。


# 读者来信

作者：Michael W Lucas

![mailbox graphic](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-12746a0308c9748fcf51d490759b60144062dd04%2Fwe-get-letters-01.png?alt=media)

亲爱的“他们怎么让你写这专栏”：

FreeBSD 15 已经发布，基本系统以包的形式提供。在 2022 年 1/2 月刊的《FreeBSD 期刊》中，你整篇专栏都在抱怨 pkgbase。你错了。显然错了。这够不够让你终于闭嘴，不再回我们的信？

——想要有用答案的人

亲爱的“想抓把柄的”：

让我闭嘴是个早已解决的问题。每小时收费 200 美元（<https://givebutter.com/c/Penguicon24/auction/items/258146>）。至于他们为什么让我写这个？因为《FreeBSD 期刊》编辑 John Baldwin 急需一个来信专栏。如今有了专栏，他更急了，但原因完全不同。换个新鲜，权当休息。

言归正传，回到你真正的问题。打包的基本系统。怎么回事？为什么？

我并没有“整篇专栏抱怨 pkgbase”。是的，我说过升级 BSD 唯一正确的方式是从源码构建并自行安装。我还说过，打包的基本系统是“FreeBSD 的恐怖巨龙，吞噬每一个试图征服它的开发者”。

巨龙占据着重要的生态位。它们是警告，也是界限：**你到此为止，不得逾越**。我们称其为邪恶，只因人们厌恶限制。遇到限制或巨龙时，我们总想将其斩杀。我们至今还记得圣乔治斩杀无辜巨龙救公主的故事。我们之所以记得，是因为王室不断传颂这个故事，指望我们从下一条龙口中救出他们，却绝口不提国王曾认为龙吞食百姓虽可惜但可接受，直到他自己的女儿上了菜单。

总之，斩杀巨龙，你将名留青史。

真正的问题在哪？开发者和其他所谓的“技术人”一样聪明。但智力与现实世界几乎无关，对同辈压力也毫无防御之力。

“自举”——从系统上的代码和工具构建系统的能力——是原始 BSD 不可或缺的一部分，至今仍至关重要。BSD 系统应随附重建自身所需的全部源码和工具。BSD 系统的天然状态是一个单一、完整的整体。将其拆分成碎片，就像把苹果切片出售。不过打包确实 **感觉** 很方便。酷酷的企鹅小子们都有基本系统打包！我出生后不久就放弃了变酷的希望，所以同辈压力只能引起我的倦怠。

有人觉得从源码构建 FreeBSD“慢”且“烦人”。在未开悟者看来或许如此，但关键在于技术正确性。正确性是每一个 BSD 的首要目标，看着编译器第无数次递归重建编译器，是为达到如此纯粹巅峰所付出的微薄代价。技术正确性还不是最糟的正确性，只是感觉如此。

2006 年，Colin Percival 发布了 **freebsd-update(8)**，让没耐性的人能跳过这道成人礼，从而推动了 FreeBSD 的更广泛采用和部署。

FreeBSD 还有 Ports 系统。Ports 让系统管理员按需构建附加软件，并打包成方便的 tar 包，随处安装。Ports 中三万四千多个附加程序支持着一团乱麻般的选项和依赖，恰好印证了我们为何要将 BSD 构建为单体整体，和应对所有这些交互所需的包管理软件。**pkg(8)** 中的现代包管理工具于 2012 年底首次纳入 FreeBSD 9.1。

新的包管理工具刚一推出，那些痴迷于所谓“易用性”的人就抛弃了 Unix“许多小工具各司其职”的哲学，开始喋喋不休地谈论用单一工具同时管理基本系统和附加软件的便利。能有多难？毕竟，Emacs 和 vim 之类臃肿的文本编辑器也是由一大堆文件构建而成的。把整个基本系统打包成 tar，附上清单，不就完事了？

嗯，不行。

无数开发者屈服于将 FreeBSD 整理成友好包的诱惑，直到 2016 年发出公开测试号召。不出所料，一切就此崩坏。若你损坏了安装并想移除所有包从头再来，你会失去操作系统。这个细节很快修复了，但事实证明边界情况数不胜数，它们之间的交互更是数以百计。

最初的测试号召声称 FreeBSD 11 将随附打包的基本系统。此后一版再版推迟，残留的 bug 被逐一发现和修复。

和所有志愿者软件项目一样，pkgbase 卡在 99% 完成度长达数年。

FreeBSD 以其开放管理为荣，但项目中有一个人掌握着非凡的宇宙级权力：发布工程师。FreeBSD 15 的发布工程师一锤定音：“我们不仅要在 15 中打包基本系统，在基本系统打包完成之前，我将推迟发布。“众人退缩，但仍动手干活。`pkgbaseify` 工具将 FreeBSD 14 主机转换为使用 pkgbase，以便通过打包工具平滑升级到 FreeBSD 15。

经过这番测试，pkgbase 对大多数人来说应该都能用了。我毫不怀疑，某个过于聪明的人会用它把主机搞到无法启动，或者把 Rails 装成新内核，但用户要为自己的主意负责，而我已经备好一大桶爆米花。

我在自己的测试机上干净安装 FreeBSD 15，显示有 311 个包，除一个外，名称都以“FreeBSD”开头。例外当然是“pkg”。坦白说，如果你在生产服务器上删除 `FreeBSD-kernel-generic` 包，你得到的就是你应得的，即一条警告：你不能删除内核。或者 libc，或者链接器，或其他任何明显的候选。你可以移除编译器，但正如我在最初的 pkgbase 讨论中所说，“真正的操作系统在默认安装中随附功能完备的编译器。”我还对那些对编译器病态恐惧的人说了些不中听的话。

这样的转变值得深入审视并考量历史背景。十九年前，Colin 出于简化升级和打补丁的一时冲动发布了 `freebsd-update`。而那位把 FreeBSD 15 扣作人质、直到纳入打包基本系统才放行的发布工程师？

Colin Percival。

我想我看到了 **真正的** 问题所在……

有问题想问 Michael？ 发送至 <letters@freebsdjournal.org>

Michael W Lucas 是《Absolute FreeBSD》《Dear Abyss》《SSH Mastery》等众多书籍的作者。了解更多请访问 <https://mwl.io>。


# 基金会来信

* 原文：[Foundation Letter](https://freebsdfoundation.org/our-work/journal/browser-based-edition/embedded-2/foundation-letter-5/)
* 作者：**John Baldwin**

欢迎阅读 2025 年第三期《FreeBSD 期刊》！北半球的夏季已经结束，许多地方的天气也随之转变。在 FreeBSD 项目中，我们正朝着 15.0 的终点线冲刺，发布定于年底前。下一期将以 15.0 为主题，重点介绍 15.0 的几项新功能与变化。

本期文章话题广泛。Chris Bowman 继续撰写他出色的系列文章，介绍如何在 Arty Z7 上使用 FreeBSD。Christos Margiolis 描述了他在 BSDCan 2025 及同期 FreeBSD 开发者峰会上的见闻。David Chisnall 介绍 CHERIoT，这是 CHERI 架构在 32 位 RISC-V 上的一种应用。Vanja Cvelbar 讲述他将 SDR 与 FreeBSD 结合用于家庭自动化系统的探索之旅；Gergely Poór 探讨在 FreeBSD 上使用量子安全密码学。最后，Tom Jones 在本期格外忙碌，他采访了 Igor Ostapenko，继续他的 WiFi 开发系列，并就如何撰写有效的 Bug 报告给出睿智建议。

一如既往，我们乐于倾听读者的声音。如果你有反馈、对未来文章主题的建议，或者有兴趣撰写文章，请发邮件至 <info@freebsdjournal.com>。

**John Baldwin** FreeBSD 期刊编辑委员会主席


# 活动日历

* 原文：[2025 Events Calendar](https://freebsdfoundation.org/our-work/journal/browser-based-edition/embedded-2/2025-events-calendar-4/)
* 作者：**Anne Dickison**

截至 2025 年 11 月的 BSD 活动。若有 FreeBSD 相关活动或 FreeBSD 用户感兴趣的活动未在此列出，请将详情发送至 <freebsd-doc@FreeBSD.org>。

![OpenZFS](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-b83b6685a33d62363708da45b5ec2de23fa4d62b%2Fevents-calendar-01.png?alt=media)

## OpenZFS 用户与开发者峰会 2025

2025 年 10 月 25-28 日

美国俄勒冈州波特兰

<https://openzfs.org/wiki/OpenZFS_Developer_Summit_2025>

今年的用户峰会将探讨广泛话题，旨在支持并连接 OpenZFS 社区。

![FreeBSD logo](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4a5e471be837bddefae134b52cd5e00ceecc7d8f%2Fevents-calendar-02.png?alt=media)

## 2025 年 11 月 FreeBSD 厂商峰会

2025 年 11 月 6-7 日

美国加州圣何塞

<https://freebsdfoundation.org/news-and-events/event-calendar/fall-2025-freebsd-summit/>

峰会为商业 FreeBSD 用户提供了独特机会，可与开发者和贡献者面对面交流，推动所需功能落地、解决问题、满足需求。峰会也开启关于改进和增强操作系统的讨论。


# 开启 FreeBSD 开发之路 —— 专访 Igor Ostapenko

* 原文：[Starting FreeBSD Development An Interview with with Igor Ostapenko](https://freebsdfoundation.org/our-work/journal/browser-based-edition/embedded-2/interview-with-igor-ostapenko/)
* 作者：Tom Jones

![采访](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-d16ed5aee9137acda3e291344d13c8938694b50c%2Finterview-with-igor-ostapenko-01.png?alt=media)

**TJ:** 通往 FreeBSD 开发之路有很多条，有些通过大学课程，有些通过工作经验。你是如何了解到这个项目的？最初又是什么吸引你走向操作系统开发的？

**IO:** 在上世纪九十年代，我在上学期间有机会接触到了几种编程语言和技术。比如，我花了很多时间鼓捣 TR-DOS，小心规划行号（这在我开始用 ipfw 时勾起了回忆），为自己设想的另一个游戏写 DATA 和 GOTO，还尝试过在 MS-DOS 下用中断向量开发常驻程序。因为有这些经历，极简的命令行界面对我来说并不陌生。后来我在大学学习计算机科学时初次接触到 FreeBSD，当时唯一的问题就是要买哪些书。

很明显，掌握 FreeBSD 会很有挑战，但也会非常值得，因为我必须一路补充基础知识。为什么是 FreeBSD？高年级同学推荐它，因为我们整个宿舍的网络都是基于 FreeBSD 搭建的。这是在千禧年初，还延续着上一个十年的惯性，当时 FreeBSD 是网络领域的事实标准。近年来，我更专注于操作系统开发。凭借广泛的软件开发背景，我积累了一系列想法，思考如何利用操作系统内部机制来支持更高层的解决方案。

**TJ:** 你最初是如何迈出修改 FreeBSD 的第一步的？最初又是怎么决定要做什么工作的？

**IO:** 第一步是准备。我希望能完善自己已有的 FreeBSD 知识，填补空白，形成更系统的认知。一本非常著名的参考书是《FreeBSD 操作系统设计与实现》，作者是 Marshall Kirk McKusick、George V. Neville-Neil 和 Robert N.M. Watson。

FreeBSD 源码对我来说并不完全陌生，多年来我已经对其结构有了整体印象。但我想要专业的指导，避免错过关键概念、风格细节或结构要点。世界有幸拥有 Marshall Kirk McKusick 主讲的 FreeBSD Kernel Internals: An Intensive Code Walkthrough 课程，它帮我节省了很多时间，解答了我的问题，并以最佳方式提供了历史背景来回答“为什么”。另外，George V. Neville-Neil 的《FreeBSD Networking from the Bottom Up》（FreeBSD 网络体系结构自底向上）课程则进一步完善了我在网络栈方面的理解。

我考虑过先做大项目还是小项目的利弊，并和 `mckusick@` 以及 `kib@` 讨论过。Konstantin Belousov 建议我从小任务入手，比如修复 bug，事实证明这是最有效的方法。我最初处理的是一些最新报告的 pf 漏洞，这又衍生出对 jail 子系统的改进、对 Kyua 的 `execenv=jail` 测试工具的改进，甚至还开发了一个新的模块 `dummymbuf` 用于特定的网络测试。因此，我继续和 Kristof Provost、Mark Johnston 和其他 FreeBSD 开发者一起推动项目改进。

**TJ:** 修 bug 是新手进入一个项目的好办法。你对 2025 年的新 FreeBSD 贡献者有没有推荐的入门方向？

**IO:** 项目官网已经提供了针对新贡献者的正式指导和具体方向，例如 [IdeasPage](https://wiki.freebsd.org/IdeasPage)。我更愿意提出一种替代思路：通往贡献的路径有很多种，最好的往往和个人兴趣一致。

比如，如果有人对学习或使用 FreeBSD 的网络工具或内核模块（如 `netstat`、`route`、`pf`、`ipfw`、`netgraph` 等）感兴趣，那么阅读相关文档和手册页的同时，可能会发现可以通过补充示例、重写复杂概念或补充缺失部分来改进它们。如果 FreeBSD 里没有相应工具或模块，那么将有用的程序加入 Ports 或保持其更新，也是非常重要的参与方式。这类项目通常既有趣又有教育意义，因为可能需要更深入地理解 FreeBSD 内核接口。

如果目标是深入理解内核代码，也可以采用类似的方法——选择自己使用或计划使用的功能，能获得更多收益。比如防火墙：理解其规则在幕后是如何运作的，可以给高级用户带来优势；或者对路由机制进行研究，以解决非通用问题。可能还需要在内核中实现缺失的功能或 RFC。偶尔也会有把其他平台的方案移植到 FreeBSD 的需求，例如正在进行中的 Netlink 实现（伴随现有工具的迁移工作），或是 Vector Packet Processing（VPP）框架的移植。这些都为进一步改进留下了空间。最终，使用现有代码时总会发现优化机会——减少每单位数据传输或处理所耗费的资源（如时间），这对所有依赖 FreeBSD 的企业都有利。

如果贡献不止一个小补丁，我建议两个起步步骤：做好功课并沟通。与 FreeBSD 开发者联系的方式很多（见 [community 页面](https://www.freebsd.org/community/)），最基本的是邮件列表，例如 <hackers@FreeBSD.org>。先讨论潜在项目，可以达成方向上的共识，也可以发现是否有人已经在做。对于开源项目来说，准备工作越充分，沟通效果越好。

**TJ:** 开发过程往往令人望而生畏，还有很多死胡同。你能分享一些捷径，帮助新开发者更轻松地调试和开发吗？

**IO:** 我认为《FreeBSD 期刊》是一项非常棒的专业经验分享渠道。我建议浏览过往期刊的目录，寻找能填补知识空白或提供新视角的文章。比如，Mark Johnston 的《Kernel Development Recipes》（内核开发秘籍）和《DeBUGGING the FreeBSD Kernel》（调试 FreeBSD 内核），Navdeep Parhar 的《FreeBSD Kernel Development Workflow》（FreeBSD 内核开发工作流），以及你写的《More Modern Kernel Debugging Tools》（更现代的内核调试工具）。这些文章可以快速概览构建系统的功能和一些技巧（比如避免耗时的完整重建），以及如何利用虚拟化或第三方软件提升效率。每篇文章都不是涵盖所有细节或列举所有可用选项的完整书籍，但它们提供了良好的起点。

要养成项目推荐的良好实践，我建议阅读 Ed Maste 的《Writing Good FreeBSD Commit Messages》（撰写优质的 FreeBSD 提交信息）。迟早也该熟悉 John Baldwin 的《FreeBSD Code Review with git-arc》（使用 git-arc 进行 FreeBSD 代码审查），这个工具大大提升了补丁发布、审查、更新和合并的效率。

在内核网络方面，我建议仔细研究 FreeBSD 的 Jail 和 VNET 功能。如果工作不涉及特定硬件支持或其他非通用主题，那么基于 VNET 的 Jail 可以大大简化开发时测试新网络功能的过程。对于感兴趣的新开发者，基于 VNET 的 Jail 可以粗略当作轻量级虚拟机网络来测试特定数据包路径或网络栈行为。这比其他方式更简单，即使同一个 `mbuf` 数据包缓冲在场景中扮演了所有角色，因为数据都不会离开主机。同时，这样的实验场景也可以成为针对新实现的功能或修复的 bug 开发新自动化测试的良好起点。Kristof Provost 的《The Automated Testing Framework》（自动化测试框架）文章介绍了 FreeBSD 测试套件。基于 VNET Jail 的现有防火墙测试源码（**src/tests/sys/netpfil**）和 Kristof 的 [YouTube 演讲](https://www.youtube.com/watch?v=gTyt7KLz1mw) 都能提供灵感。

不过，投入时间来熟悉源码环境也很重要。内核是一种特殊的软件，它支持多种架构、编译器和特殊场景，预处理器的 ifdefs 并不足以解决所有问题，有些条件甚至是在代码之外解决的。另外，源码中包含一些自动生成的代码，比如系统调用或 VFS 操作的模板。因此，单靠 grep 或使用默认设置的编辑器/IDE 是不够的。我个人的配置是 `Neovim + clangd + intercept-build`，效果不错，但最好先了解有哪些可选方案：<https://docs.freebsd.org/en/articles/freebsd-src-lsp/>

使用语言服务器协议（LSP）可以大大简化代码理解和导航，这对新开发者来说至关重要。

**TJ:** 感谢你接受采访。最后你有没有给新贡献者的建议，或者想补充的内容？

**IO:** 也谢谢你。我的总体建议适用于任何有大量志愿者的国际开源项目：保持开放心态和一定的耐心。这里打个基于网络的比方：在社区网络中有许多“主机”（贡献者），并不是随时都能通过任何协议访问。每个“主机”都可能忙于自己的工作，网络接口可能已经过载。因此，对我们的连接超时并不意味着严格的拒绝（`RST`），更可能是需要更多时间来处理我们的 `SYN`。像电子邮件这样的沟通方式能提供深度缓冲，通常“先到达的请求”可能最后才被考虑，所以礼貌的重发往往是实用的做法。还需要重新设定对延迟的预期，因为存在时区、优先级（工作优先于志愿贡献）或停机（周末、假期等）的问题。

这个网络已经运行了数十年，一些“主机”从早期就一直参与。他们积累的知识和经验可以帮助我们应对新挑战，给出方向，或提醒我们避免一些尚未显现的问题。同时，新加入的“主机”也能带来新想法或新视角。每位贡献者都为拼图增添了独特的一块。因此，保持开放心态，努力理解别人信息背后的意图，对新贡献者建立更好的连接至关重要。

虽然不是必须的或所期望的，但如果你能在“带宽”上做到对等（即也能接受外部连接），这将提升整个网络的容量并促进扩展。换句话说，要准备好在自己这边“打开一些端口”，接纳他人的输入。

***

**Tom Jones** 是 FreeBSD 提交者，专注于保持网络栈的高性能。

**Igor Ostapenko** 是 FreeBSD 和 OpenZFS 的贡献者，拥有广泛的软件开发经验，涉及导航设备操作与测试系统、企业流程优化解决方案、逆向工程，以及 B2B/B2C 创业项目等多个领域。


# CHERIoT

* 原文：[CHERIoT](https://freebsdfoundation.org/our-work/journal/browser-based-edition/embedded-2/27494/)
* 作者：David Chisnall

[CHERI](https://cheri-cpu.org/) 项目与 FreeBSD 一直关系密切。最初的出发点是这样的洞察：基于 Capsicum 的分区隔离对新代码非常有效，但要把它改造进既有库（每个库实例对应一个进程）却很难，原因有二：

首先，库希望共享复杂的数据结构；当把接口改造成通过某种进程间通信（IPC）通道发送消息时，会引入大量序列化开销。在普通库中，函数调用只需传递对象的指针即可共享数据结构。某个特权分离的库则需要对调用方与被调方之间移动的一切进行授权。库还常常需要长期共享，这又会带来额外的同步开销。

其次，进程由内存管理单元（MMU）隔离。MMU 提供了一种虚拟内存抽象，将虚拟地址空间中的地址映射到底层物理内存。现代 MMU 很快，因为它们有转换后备缓冲区（TLB），这是快速的地址翻译缓存。TLB 会缓存虚拟地址到物理地址的翻译。如果同一页被 10 个进程共享，就需要占用 10 条 TLB 表项。MMU 在隔离方面很强，但在共享方面很差。

这两个问题引出了普遍结论：隔离容易，共享难。

[CHERI](https://cheri-cpu.org/) 是一组体系结构扩展，为从汇编代码往上的一切提供细粒度的内存安全。与 Capsicum 一样，CHERI 是个能力（capability）系统。在能力系统中，每个动作都必须伴随一项能力 —— 一种不可伪造的授权令牌 —— 来授权该动作。

与之相对，许多其他优化机制包含“环境权限”（ambient authority）的概念：你可以仅因“你是你”而执行某个动作。比如，在传统的 UNIX 系统上（没有类似 FreeBSD MAC 框架或 SELinux 的机制），如果某进程以 UID 0 运行，它就能执行大量特权操作，不管它本意是否如此。

以 root 身份运行的进程往往是安全问题的根源，因为它们很容易被诱导去做不该做的事。在能力系统中，特权进程改为持有一组令牌，每个令牌分别授权某个具体动作。进程在每个时刻都需要选择要使用哪个令牌，从而避免无意中使用提升的权限。

MMU 也有类似的环境权限概念。正在运行的程序可以访问一切具有有效映射的内存。如果出现缓冲区溢出，MMU 并不会在意你并非有意访问相邻对象：只要你“有权”访问那段内存，你就能访问。相关的代码片段也可能持有指向另一个对象的指针，因此被授权访问该对象，但它在那个时间点并非有意如此。

Capsicum 将文件描述符扩展成了能力。

在进入能力模式（通过 `cap_enter` 系统调用）之后，带有丰富权限集合的 Capsicum 文件描述符开始发挥作用：进程如果不向系统调用提供具有正确权限的有效文件描述符（能力），就无法访问自身内存之外的任何东西。

例如，在普通的 POSIX 进程中，你可以调用 `open` 来访问用户（或某个涉及用户、进程以及其他因素的 MAC 策略）所能访问的任意文件。相比之下，能力模式下的进程必须调用 `openat`，并向其传入一个授权访问特定目录的能力。这样就贯彻了“有意性”的原则，避免了一大类漏洞。

再比如，如果你“本意上”要访问的是临时目录中的某个东西，但“碰巧”被给了你不该访问的、更重要目录的路径（例如 **../etc/rc.conf**），那么使用指向临时目录的文件描述符调用 `openat` 会失败（正确地阻止了利用），而 `open` 则会成功。

CHERI 保护内存访问的方式，类似于 Capsicum 保护文件系统访问的方式。传统的指令集架构提供取数、存数和跳转指令（有时如 x86，还与更复杂的操作组合在一起），这些指令都以某个地址作为基址。这种内存模型与传统 UNIX 对文件系统的看法很相似：只要进程被允许在某个位置执行取 / 存操作，任何取 / 存都可能成功。缓冲区溢出或释放后使用（use-after-free）之于内存，就如同路径遍历漏洞之于文件系统。

在 CHERI 系统中，这个“地址”被 CHERI 能力所取代；它是一种不可伪造的值，授权访问地址空间中的一个范围。这些值存放在寄存器和内存中，并由硬件防止篡改。

这如何让编程变得容易？

程序员是否必须把能力当作某种额外的东西来跟踪？

不，完全不用。

事实证明，大多数编程语言早就有一种抽象，用于表示“授权你访问一段内存的令牌”。它们把它称为指针（或在某些情况下称为引用）。作为一个以 CHERI 系统为目标的程序员，你大多数时候并不会想着 CHERI 能力，而只会想着指针。

如果你做的指针算术把指针带出了对象范围，那么它就不能再用于加载或存储。而且，由于硬件能精确知道内存中哪些东西是指针、哪些不是，所以在对象被释放时可以让指针失效。这意味着，在 CHERI 系统上，你只需把对象的指针作为参数传给库暴露的函数，就能与库共享对象。

最早期的 CHERI 操作系统是一款很小的微内核，但绝大多数工作都是在 FreeBSD 上完成的。CheriABI（面向 FreeBSD 的 CHERI 用户态 ABI）在 FreeBSD 的友好分支（[CheriBSD](https://cheribsd.org/)）上展示了一项完全内存安全的用户态与内核，该分支的目标是在 16.x 发布前将这些变更上游合入。CHERI 基础架构目前正作为 RISC-V 的一部分标准化，而 FreeBSD 16 预计将对所有即将推出、实现该指令集的应用级 CHERI 内核提供一等公民级支持。

FreeBSD 对 CHERI 项目至关重要。CHERI 是一项长期的软硬件协同设计项目，需要同时修改栈中的硬件与软件部分，以探索各种理念在何处、如何实现最佳。这要求一款便于修改的生产级操作系统。FreeBSD 清晰的结构和定义良好的抽象使这一切变得容易。我们后来在把 Linux 运行在 CHERI 上的工作中看到，相比之下，适配 FreeBSD 的工作量低得多；如果一开始就以 Linux 为目标，项目大概没有足够的软件工程师来完成同样的事情。其宽松许可也便于向其他操作系统的厂商展示各类硬件特性的使用方式。FreeBSD 还是 LLVM 的早期采用者，在易于修改性和许可方面同样具有优势。使用经过修改的 LLVM 编译整个 FreeBSD 基本系统非常容易，从而让测试新的 CPU 特性变得轻而易举。Brooks Davis 在 2023 年 5/6 月的 FreeBSD 期刊上撰写了一篇更长的 [文章](https://freebsdfoundation.org/wp-content/uploads/2023/06/davis_cambridge.pdf)，介绍了 FreeBSD 对 CHERI 研究的益处。

## 缩减 CHERI

CheriABI 展示了，你可以在内存安全的世界里运行真正的 POSIX 应用，包括像 Chromium 这样的大型程序，以及带有 Wayland 和 3D 驱动的完整 KDE 桌面环境。它可以通过 COMPAT64 层与现有二进制共存，这一层的工作方式与 FreeBSD 上允许 64 位系统运行 32 位程序的 COMPAT32 层类似。从手机到服务器的大多数系统都采用了这一套已被验证有效的抽象。

2019 年，我们在微软的一些人决定尝试，看看能否将同样的抽象缩减到最小的系统中。

我们有三个问题：

* 在 64 位系统上运行良好的 CHERI 机制，能否在 32 位系统上也能运作？
* 如果有了 CHERI，可以舍弃哪些东西？
* 如果从零开始假定存在 CHERI，这个操作系统会是什么样子？

第一个问题并不是显而易见的。CHERI 提供的抽象似乎与地址大小无关，除了一个问题：CHERI 能力的所有元数据必须放进与地址同样多的比特中。这意味着在 32 位系统上，我们只有一半的空间存放元数据。

CHERI 使用一种压缩编码来表示边界，它利用了对象基址、对象顶部和指针指向该对象的地址三者之间的冗余。在较小的系统中，冗余更少，边界可用空间也更小。幸运的是，在嵌入式系统中，总的地址空间往往更小，因此没有必要精确表示非常大的地址区域。高端微控制器通常总 RAM 远低于 4 MiB，所以大多数对象都非常小。

我们在权限上可用的比特位也比 64 位系统少，因此必须压缩权限编码，去掉那些对安全有害、无用或无意义的组合。

## 我如何在开发 CHERIoT 时使用 FreeBSD

当我们构建第一个 [CHERIoT](https://cheriot.org/) 原型时，有三个核心组件：

* 用 BlueSpec SystemVerilog 编写的 CPU 内核；
* 移植版的 LLVM；
* 从零开始的 RTOS。

BlueSpec SystemVerilog 是一种基于 Haskell 的高级硬件描述语言，使快速原型开发变得容易。[BlueSpec 编译器](https://github.com/B-Lang-org/bsc) 在 FreeBSD 上编译只需做少量小改动，我们把这些修改上游合并了，现在 FreeBSD 是受支持的平台。它目前还没进入 Ports，但这会是很好的改进。有了它，我们就能在 FreeBSD 上构建运行 CPU 内核的模拟器。后来我们转向了用 SystemVerilog 实现的生产级代码，并使用（来自软件包的）verilator 在 FreeBSD 上构建模拟器。

在 FreeBSD 上 LLVM 开发非常容易。FreeBSD 是 LLVM 的一等公民目标，LLVM 的构建也非常简单。在某种程度上甚至“太容易了”：当我们需要支持一些 Linux 长期支持版本的用户时，发现引导我们使用的 LLVM 版本很困难，因为它依赖的 C++ 版本比他们系统自带的工具链更新。

而在前沿开发上，FreeBSD 更容易：Ports 里提供多个版本的 GCC 和 LLVM。这也让我们很容易在其他系统上复现一些构建失败问题 —— 只需安装他们自带的旧版 GCC，然后配置构建使用它即可。一旦这些工作正常，交叉编译和测试 RTOS 就很容易了。

## 从 FreeBSD 汲取的经验教训

与 FreeBSD 一样，CHERIoT RTOS 是一款采用宽松许可、由社区开发的操作系统。

最重要的是，CHERIoT 旨在借鉴 FreeBSD“先设计再实现”的模式。改代码的最佳时机是“还没写出来的时候”。FreeBSD 之所以拥有 Jail、kqueue 和 Capsicum 等特性，正是源于在向用户抛出 API 之前，充分思考与反复推敲，而不是寄望它“也许能用”，然后承受后续的种种后果。

我们从 Capsicum 学到：可以把对程序员来说看起来像传统文件描述符或句柄的东西，做成能力（capability）。我们的软件抽象遵循这一模式。

我们也从 `kqueue` 学到：用一种单一、简单、统一的方式轮询任意阻塞事件是可行的。`kqueue` 的设计并不太适合实施特权分离的 RTOS，但其核心思想适用。我们的调度器把 futex（即“比较且相等则等待”的简单原子操作）作为唯一的阻塞事件源。中断被映射为 futex，因此线程只需等待 futex 就能等待中断。调度器在此之上叠加了多等待者 API，允许线程同时等待多个来自硬件或软件的事件。

或许更重要的是，我们从 FreeBSD 得到的启示是：文档为王。再优秀的系统，如果没有人能看懂并用起来，也无济于事。凭借撰写良好的手册页和《Handbook》，FreeBSD 容易上手。为希望学习 CHERIoT RTOS 的开发者撰写一本书是我们的优先事项，该书已于今年早些时候出版。除此之外，我们为每个 API 都写了文档注释，现代 IDE（以及使用我们版本 `clangd` 作为语言服务器协议实现的 vim）都能解析这些注释。

## FreeBSD 可以从 CHERIoT 学到的东西

CHERIoT RTOS 目前完全用 C++ 编写。相较于 C，C++ 在系统编程上有许多优势。它便于创建在编译期即可校验的丰富抽象。举例来说，我们有消息队列设计：为生产者和消费者指针各使用计数器，并需要正确处理这些值发生回绕的情形。

在 C++ 中，我们可以定义执行自增的 `constexpr` 函数，然后编写模板对其行为进行 `static_assert`。每次编译定义这些逻辑的文件时，编译器都会对某些小队列规模下生产者与消费者指针的所有可能取值，穷尽式检查其溢出行为。

使用丰富的类型还能让我们在“构造时”就避免大量错误。比如，我们有 `PermissionSet` 类来管理 CHERIoT 能力上的权限集合。它是 `constexpr` 集合，允许你按名称构造权限，并生成相关指令所需的位图。

在装载器中，我们用它同时描述我们希望赋予某个能力的权限，和各个根对象各自具备的权限。如果我们试图派生原始权限中不存在的权限，编译就会失败。这比在后续某条指令因为其某个操作数缺少预期权限而失败，要容易调试得多。

我们大量采用这样一种模式：用内联模板函数做一些编译期检查（最终会被优化掉），然后调用类型擦除的函数。我们的类型安全日志就是这样实现的。我们有类似 `printf` 的函数，接受用于打印的参数数组及其联合体判别值。模板会基于类型生成这些判别值，因此无论你要记录的是指针、枚举值还是 MAC 地址，都能得到正确输出，而无需在格式字符串里手工匹配类型，同时还有编译期检查。

这套机制是可扩展的。比如 MAC 地址并非内建；由网络栈定义其回调和处理映射的模板特化。

我们目前还没有 Rust 编译器（很快就会有！），但一旦具备，我们预计也会在部分组件中使用 Rust。具备更丰富类型的系统编程语言，既能在编译期避免缺陷，也能让我们写更少的代码。

如果使用 C，在 RTOS 的许多关键部位我们需要写更多源代码，这些代码也更难维护；同时更难找到熟悉该语言的开发者。就我们当下看到的各类系统编程语言的新代码行数而言，排序如下：

1. C++
2. C
3. Rust

如今找 C++ 开发者比另两者更容易，但这忽略了趋势。自 C++11 引入以来，C++ 一直在缓慢增长；同一时期，C 则在更陡峭、稳定地下滑。Rust 过去几年保持稳定增长，并在最近三年加速。我预计，很快找 Rust 开发者会比找 C 开发者更容易；再过大约十年，找 Rust 开发者也许会比找 C++ 更容易。

这并不意外。

开发者往往偏爱能让自己更高效的语言。FreeBSD 正在推进在基本系统中支持 Rust，但要采用现代 C++ 的路径更简单；C++ 能比 C 更容易表达复杂概念，并在编译期提供更多检查。

FreeBSD 还在非绝对性能关键的领域（包括用户态部分、引导加载器中的策略、和 ZFS channel programs）出色地运用了 Lua。与上述语言相比，Lua 学习起来要简单得多，也更容易让经验尚浅的程序员快速产出。我们在构建系统中使用 Lua（以及为 CHERIoT 书籍排版！），但遗憾的是，Lua 虚拟机占用的内存大致等于典型微控制器的全部可用内存，因此我们无法在 RTOS 中使用它。

更丰富的系统编程语言很重要，但 FreeBSD 对 Lua 的运用提醒我们：很多事情——即便在内核里——其实并不一定需要系统编程语言来完成。

## 在 FreeBSD 上开发 CHERIoT

CHERIoT 工具链已经进入 FreeBSD 的 Ports，`xmake` 构建工具也同样在其中，因此你可以很简单地通过以下命令来安装：

```sh
# pkg ins cheriot-llvm xmake-io git
```

这样你就具备了构建 CHERIoT 固件所需的前置条件。需要注意的是，在撰写本文时，季度分支中的 CHERIoT LLVM 版本是 18，而最新分支中的版本是 20（并即将更新到 21），因此在开发时最好使用最新分支。

现在，你应该可以尝试克隆 RTOS 并构建简单的固件镜像。

首先，克隆 RTOS：

```sh
$ git clone --recurse https://github.com/CHERIoT-Platform/cheriot-rtos
$ cd cheriot-rtos
```

接下来，你需要配置构建：

```sh
$ cd examples/01.hello_world/
$ xmake config --sdk=/usr/local/llvm-cheriot
checking for platform ... cheriot
checking for architecture ... cheriot
Board file saved as build/cheriot/cheriot/release/hello_world.board.json
Remapping priority of thread 1 from 1 to 0
generating /tmp/cheriot-rtos/sdk/firmware.rocode.ldscript.in ... ok
generating /tmp/cheriot-rtos/sdk/firmware.ldscript.in ... ok
generating /tmp/cheriot-rtos/sdk/firmware.rwdata.ldscript.in ... ok
$ xmake
...
[100%]: build ok, spent 13.796s
```

如果你有 lowRISC 的 Sonata 开发板，可以在 `xmake config` 命令行的末尾添加 `--board=sonata`，这样就会生成面向该开发板的 ELF 文件。该开发板会作为带有 FAT 文件系统的 USB 大容量存储设备出现，能够加载 UF2 文件。如果你运行 `xmake run`，它会提示你需要从 `pip` 安装用于将 ELF 转换为 UF2 文件的缺失 Python 软件包。安装完成后，如果 `SONATA` 文件系统挂载在常见位置，它要么会告诉你需要复制哪个文件，要么无需进一步交互就会直接完成复制。

如果你没有这块板卡，那么可以在模拟器中运行生成的固件。默认情况下，这些示例将以 Sail 模拟器为目标。这需要 OCaml，并且可以在 FreeBSD 上构建，但需要一些手动步骤。该项目还提供了 Linux 开发容器，在 FreeBSD 的 Linux 兼容层中配合 Podman 使用效果很好：

```sh
# pkg ins podman-suite
# podman run --rm -it --os linux -v path/to/cheriot-rtos:/home/cheriot/cheriot-rtos ghcr.io/cheriot-platform/devcontainer:x86_64-latest
```

这会让你进入临时容器，其中包含挂载在 **/home/cheriot/cheriot-rtos** 的 RTOS 源代码克隆。需要注意的是，Ports 中当前版本的 Podman 并不喜欢在宿主操作系统与容器 OS 不匹配时使用指向多架构容器的标签。我们提供了 `x86-64` 和 `AArch64` 的二进制文件，所以如果你是在 AArch64 上，只需把上面命令中的 `x86_64` 替换为 `aarch64` 即可。

开发容器在 **/cheriot-tools** 中安装了所有工具，因此你可以像之前一样再次尝试构建示例：

```sh
$ cd examples/01.hello_world/
$ xmake f --sdk=/cheriot-tools
...
$ xmake
...
[100%]: build ok, spent 13.524s
$ xmake run
Board file saved as build/cheriot/cheriot/release/hello_world.board.json
Remapping priority of thread 1 from 1 to 0
Running file hello_world.
ELF Entry @ 0x80000000
tohost located at 0x80006448
Hello world compartment: Hello world
SUCCESS
```

恭喜你，你已经在由 ISA 形式化模型构建的模拟器中运行了内存安全的 C++ 代码！

开发容器还包含另外三个模拟器：

* 针对 Ibex 内核的逐周期精确（cycle-accurate）verilator 模拟器，带有最小化的外设集。
* 针对 lowRISC Sonata 开发板的模拟器。
* 来自谷歌的 MPact 模拟器，它提供了高性能模拟器，并带有集成调试器。

MPact 模拟器与 Sail（简化）机器兼容。

你可以直接运行它：

```sh
$ /cheriot-tools/bin/mpact_cheriot build/cheriot/cheriot/release/hello_world
Starting simulation
Hello world compartment: Hello world
Simulation halted: exit 0
Simulation done: 106508 instructions in 0.2 sec (0.5 MIPS)
Exporting counters
```

你可以在 FreeBSD 上开发，只在容器中运行模拟器；也可以选择在容器中完成所有开发。在其他平台上，大多数开发者会使用带有 dev container 集成功能的编辑器（例如 Visual Studio Code）。在 FreeBSD 上同样也可以这样做，只需配置 Podman 来运行容器的 Linux 版本即可。

另外，你也可以在 FreeBSD 上原生构建所有模拟器。相关的操作步骤过长，无法在本文中全部展开，但你可以参考 RTOS 仓库中的文档说明。它们所需的所有依赖已经在 Ports 中，模拟器本身也有望很快进入 Ports。

***

**David Chisnall** 的背景涵盖操作系统、编译器、硬件与安全领域。他是《The Definitive Guide to the Xen Hypervisor》的作者，自 2008 年起担任 LLVM 提交者，并在 2012 年至 2016 年期间担任 FreeBSD 核心小组成员。他于 2012 年加入 CHERI 项目，负责剑桥大学该研究方向中的编译器与语言部分。2018 年至 2023 年，他继续在微软从事 CHERI 相关工作，包括主导创建 CHERIoT。他现为 SCI Semiconductor 的联合创始人兼系统架构总监，该公司生产 CHERIoT SoC，同时也是 CHERIoT 平台开源项目的共同维护者。


# FreeBSD、Home Assistant 与 rtl\_433

* 原文：[FreeBSD, Home Assistant, and rtl\_433](https://freebsdfoundation.org/our-work/journal/browser-based-edition/embedded-2/freebsd-home-assistant-and-rtl_433/)
* 作者：Vanja Cvelbar

在很久以前，我就开始玩 Home Assistant 了，我对自己做出的几个自动化很自豪。浴室里有个运动传感器，能开灯。如果太阳已经落山，灯会以极暗的橙色亮起；否则，它会最大化亮度并使用白光。还有个自动化，当相对湿度超过预设值时，自动开启除湿机。另外还有个安全措施。不幸的是，我们有一扇地下室窗户，如果从南边下雨，就容易被淹（虽然这种情况比较少见）。我内外各安装了一个湿度传感器；当它们被触发时，我们会在 Telegram 上收到一条消息，并能据此采取措施。整个系统托管在运行于 HAOS 的 bhyve 虚拟机中。

## 需求

在阳台和花园里有两个无线电控温度计，我希望能有个简单的概览，更重要的是一张清晰的室外温度曲线图。这些设备很简单，定期通过 433MHz 无线电发送数据。研究之后发现，用标准的软件无线电（SDR）适配器就能监听这些信号并进一步处理。将它与优秀的 rtl\_433 和 rtl-sdr 项目结合，就是答案。

把一根基于 RTL2832 的接收棒插到笔记本电脑上后，我开始收到温度计的数据，同时也收到了邻居的设备，甚至是路过汽车的胎压监测系统（TPMS）的数据。

好在 rtl\_433 软件已经支持将数据发布到 MQTT 服务器。Home Assistant 里本来就有一项用于 ZigBee 网络的 MQTT 服务，所以只需要创建新用户来发布数据即可。

```sh
/var/log/rtl_433.log
{"time" : "2025-04-13 18:42:55.702554", "protocol" : 19, "model" : "Nexus-TH", "id" : 209, "channel" : 1, "battery_ok" : 1, "temperature_C" : 13.200, "humidity" : 61, "mod" : "ASK", "freq" : 433.910, "rssi" : -0.431, "snr" : 30.360, "noise" : -30.791}
{"time" : "2025-04-13 18:42:56.539182", "protocol" : 8, "model" : "LaCrosse-TX", "id" : 31, "temperature_C" : 11.300, "mic" : "PARITY", "mod" : "ASK", "freq" : 433.892, "rssi" : -12.208, "snr" : 19.251, "noise" : -31.459}
```

JSON 日志记录了 rtl\_433 解码外部温度计无线电传输的数据。

## 第一次实现

主机架位于地下室，因此那里的无线信号很差。我把 SDR 放在一楼，并将其连接到现有的 pfSense 路由器。这需要手动安装软件包，并费力地让它在启动时运行。务实的解决方案是启动 screen 会话，在其中运行 rtl\_433。

这确实有效，数据被发送到了 MQTT，Home Assistant 绘制出了我选择的温度传感器的漂亮曲线图。但这种方案不够稳定。每次重启、升级或防火墙故障后，我都得记得重新启动它。它不够可靠，我也对此不满意。

![Home Assistant 花园温度传感器温度曲线图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-1c25ba7cde72c1b5f73ef1ac11cce1e0685d5d29%2Ffreebsd-home-assistant-and-rtl_433-01.png?alt=media)

Home Assistant 顺利绘制出了由花园温度传感器捕获的温度曲线。

![两个传感器温度曲线对比图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-42ecae9e91dde5b96d3006d93cc22ff00a302b13%2Ffreebsd-home-assistant-and-rtl_433-02.png?alt=media)

Home Assistant 系统捕获的两个传感器的温度曲线图。

## 第二次实现

我手头有一块闲置的 Pine A64+。我开始验证它对 FreeBSD 的支持，但很快放弃了，因为它在重启时有些问题。该问题在软件重启后仍然存在，表明是硬件问题。我记得必须物理接触主板并断开电源供应，这太麻烦，也不实用。

## EuroBSDCon 来解围

2019 年在挪威举办的 EuroBSDCon 上，Tom Jones 做了题为《使用 FreeBSD 进行硬件黑客入门》的教程。每个参与者都收到了一只小盒子，里面有开关、LED、电缆和其他组件，最重要的是一块 Arm SBC。

那是一块 FriendlyELEC 的 NanoPi NEOLTS。会议结束后，我用了一段时间，但现在它主要闲置在抽屉里。所以，我开始评估它作为运行 rtl\_433 并获取无线数据的平台。

安装的软件已经过时，所以我买了一张新的存储卡，并按照 Tom Jones 页面上发布的说明，成功把采集系统迁移到了上面。(<https://adventurist.me/posts/00297>)

一开始一切运行正常，也很有趣，但几天后系统突然停止工作，无法通过 SSH 访问，所以我只好给它断电重启。最好是连接串口控制台来找出问题所在，但当时我没时间去正确排查。

怀疑是与操作系统版本存在某些不兼容，我后来写了个粗糙的 shell 脚本，基于 Tom 的说明来自动化镜像创建。运行了几次之后，我开始把它参数化，以便更快地更换版本，并添加了一些其它实用的小功能。

![NanoPi 与 USB SDR 接收棒最终实现](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-a294c7f1b8e0229761be0af755dd5d8265e34861%2Ffreebsd-home-assistant-and-rtl_433-03.png?alt=media)

最终实现 —— NanoPi 与插在防火墙上的 USB SDR 接收棒。

## 做成脚本

最终版本的脚本比第一版多了几个步骤。首先，我们定义想要下载的 FreeBSD 版本和源 URL。

```sh
!/bin/sh
VERSION="14.2"
RELEASE_VERSION="$VERSION-RELEASE"
URL_IMG="https://download.freebsd.org/releases/arm/armv7/ISO-IMAGES/$VERSION/FreeBSD-$RELEASE_VERSION-arm-armv7-GENERICSD.img.xz"
URL_CHECKSUM="https://download.freebsd.org/releases/arm/armv7/ISO-IMAGES/$VERSION/CHECKSUM.SHA512-FreeBSD-$RELEASE_VERSION-arm-armv7-GENERICSD"
```

接下来，我们定义目标镜像名称、目录树、所需软件包、u-boot 引导加载程序位置、时区、要在目标镜像上安装的软件包，和初始目标镜像大小。

```sh
TARGET_IMG="nanopi-rtl-433-FreeBSD-$RELEASE_VERSION.img"
IMG_TARGET_DIR="img-target"
IMG_MNT_DIR="img-mnt"
IMG_RELEASE_DIR="img-release"
CUSTOM_DIR="customization"
PACKAGES="u-boot-nanopi_neo-2024.07"
BOOTLOADER="/usr/local/share/u-boot/u-boot-nanopi_neo/u-boot-sunxi-with-spl.bin"
TIME_ZONE="/usr/share/zoneinfo/CET"
TARGET_PACKAGES="rtl-433 monit"
SD_SIZE="6G"
```

出于安全原因，我们会设置个性化的用户密码。

```sh
# 用户密码
TARGET_USER_PW=`date +%s | sha256sum | base64 | head -c 13`
```

接下来的步骤包括准备目录树，并定义校验和文件和镜像的位置。

```sh
# 准备目录树
echo "Preparing the directory tree ..."
mkdir -p "$IMG_TARGET_DIR"
mkdir -p "$IMG_MNT_DIR"
mkdir -p "$IMG_RELEASE_DIR"
mkdir -p "$CUSTOM_DIR/usr/local/etc"
digest="$IMG_RELEASE_DIR/CHECKSUM.SHA512-FreeBSD-$RELEASE_VERSION-arm-armv7-GENERICSD"
image="$IMG_RELEASE_DIR/FreeBSD-$RELEASE_VERSION-arm-armv7-GENERICSD.img.xz"
digest_ext="$IMG_RELEASE_DIR/CHECKSUM.SHA512-FreeBSD-$RELEASE_VERSION-arm-armv7-GENERICSD.img"
image_ext="$IMG_RELEASE_DIR/FreeBSD-$RELEASE_VERSION-arm-armv7-GENERICSD.img"
```

镜像会下载到 `$IMG_RELEASE_DIR`，但仅当镜像的校验和与现有的不同时才会下载。

```sh
if ! [ -f $digest_ext ]; then
    echo "Digest=Initial" > $digest_ext
fi
# get the release image checksum and check the existing image
echo "Checking compressed image checksum"
fetch -o "$IMG_RELEASE_DIR" "$URL_CHECKSUM"
if ! sha512 -q -c $(cut -f2 -d= $digest) $image; then
   echo "Download compressed image"
   fetch -o "$IMG_RELEASE_DIR" "$URL_IMG"
   # reset extracted image checksum
   echo "Digest=Refresh" > $digest_ext
   rm -f $image_ext
else
   echo "Compressed image OK"
fi
```

如果镜像的校验和与现有的不一致，就会解压该镜像。

```sh
# 提取镜像
echo "Checking extracted image ..."
if ! sha512 -q -c $(cut -f2 -d= $digest_ext) $image_ext; then
   echo "Extracting image and creating checksum"
   rm -f $image_ext
   xz -dk $image
   sha512 $image_ext > $digest_ext
else
   echo "Extracted image OK"
fi
```

现在会安装主机系统所需的软件包。

```sh
# 安装所需的软件包
echo "Installing packages"
pkg install -y $PACKAGES
```

会创建使用上述定义大小的目标镜像，并将其连接到内存磁盘。

```sh
# 创建目标镜像
echo "Create target image and connect to the memory disk"
truncate -s $SD_SIZE $IMG_TARGET_DIR/$TARGET_IMG
mdisk=`mdconfig -f $IMG_TARGET_DIR/$TARGET_IMG`
echo $mdisk
```

我们将镜像和引导加载程序写入目标。

```sh
# 将解压后的镜像写入内存盘
echo "Writing the extracted image to the memory disk"
dd if=$image_ext of=/dev/$mdisk bs=1m
# write the bootloader to the memory disk
echo "Writing the bootloader to the memory disk"

if [ -f $BOOTLOADER ]; then
   dd if=$BOOTLOADER of=/dev/$mdisk bs=1k seek=8 conv=sync
else
   echo "The bootloader $BOOTLOADER does not exist. Aborting ..."
   exit
fi
```

最后，目标镜像会被挂载，并使用选项 `pkg -chroot` 安装软件包，这使我们能够优雅地为不同的硬件平台安装软件包。需要注意的是，目标系统上必须存在正确的 **resolv.conf** 文件。

```sh
# 挂载目标镜像
echo "Mounted target image on $IMG_MNT_DIR"
mount /dev/"$mdisk"s2a $IMG_MNT_DIR

# 安装目标软件包
echo "Installing packages on target"
cp /etc/resolv.conf $IMG_MNT_DIR/etc/
pkg --chroot $IMG_MNT_DIR install -y $TARGET_PACKAGES
```

正在目标系统上启用服务。同样，我们使用 `sysrc` 工具的便捷选项，直接在已挂载的镜像上访问配置文件。虽然不是严格必须的，但我们还启用了远程 syslog，它应在目标主机上通过 `-a <peer>` 启用。这一功能是在调试系统时启用的，当时系统已经挂起了一天半，如上所述。

```sh
# 对目标启用服务
sysrc -f $IMG_MNT_DIR/etc/rc.conf hostname="nanopi" ntpdate_enable="YES"
ntpd_enable="YES" rtl_433_enable="YES" monit_enable="YES" syslogd_enable="YES"
syslogd_flags="-s -v -v"
```

在自定义目录中，我们复制系统树中所需的部分：

```sh
customization/
├── home
│  └─ freebsd
│      └─ .ssh
│           └─ authorized_keys
├── usr
│  └─ local
│       └─ etc
│           ├─ monitrc
│           ├─ rtl_433.conf
│           └─ syslog.d
│               └─ remote_anirul.d112.conf
└── var
    └─ log
        └─ rtl_433.log
```

```sh
# 复制自定义内容
echo "Copy $CUSTOM_DIR to $IMG_MNT_DIR"
cp -a  $CUSTOM_DIR/* $IMG_MNT_DIR
```

时区的设置很简单，只需将定义复制到目标系统的 **/etc/localtime** 即可。

有趣的是 —— 在桌面机器上复制 CET 定义会导致 Mozilla 产品出现问题。比如在 Thunderbird 中，邮件列表里的时间会显示为 UTC，但打开邮件后，时间又显示正确。在这种情况下，更好的做法是使用 `tzsetup` 并指定正确的城市，或者复制某个城市的时区定义。我至今仍需要想办法报告这个 bug。

```sh
# 设置时区
echo "Set the timezone: copy $IMG_MNT_DIR/$TIME_ZONE to $IMG_MNT_DIR/etc/localtime"
cp $IMG_MNT_DIR/$TIME_ZONE $IMG_MNT_DIR/etc/localtime
```

接下来，会为 FreeBSD 用户设置新密码，并将其复制到主目录中的文件，和镜像文件旁边。最后的清理步骤包括卸载镜像、移除内存磁盘，并向用户提供一些最终的使用说明。

```sh
# 配置用户
echo "Configuring users on target"
# change freebsd user password
echo "$TARGET_USER_PW" | pw -R $IMG_MNT_DIR mod user freebsd -h 0
echo "$TARGET_USER_PW" > $IMG_MNT_DIR/"home/freebsd/.pwd"

# 卸载目标镜像
echo "Unmounted target image from $IMG_MNT_DIR"
umount $IMG_MNT_DIR

# 创建密码文件
echo "$TARGET_USER_PW" > $IMG_TARGET_DIR/$TARGET_IMG".pwd"

# 移除内存盘
echo "Removing memory disk $mdisk"
mdconfig -d -u $mdisk

# 最终说明
echo "You can copy the image with \"dd if=$IMG_TARGET_DIR/$TARGET_IMG of=/dev/<USB Disk> status=progress bs=1m\""
echo "The password for the user freebsd is \"$TARGET_USER_PW\""
echo "The password is also available in the file \""$IMG_TARGET_DIR/$TARGET_IMG".pwd\""
```

在自定义部分中，我们准备了服务的配置文件。`Monit` 用于监控 `rtl_433` 的状态，并在需要时重新启动它。下面是 `monitrc` 中定义 `rtl_433` 服务的几行配置：

```sh
# rtl_433
check process rtl_433 with pidfile /var/run/rtl_433.pid
start program = "/usr/local/etc/rc.d/rtl_433 start" with timeout 60 seconds
stop program = "/usr/local/etc/rc.d/rtl_433 stop"
```

当然，我们还必须配置 `rtl_433`。与示例配置相比，差异如下：

```sh
output mqtt://<homeassistant>,usr=<mqtt user>,pass=<mqtt user pwd> ,retain=0,devices=rtl_433[/model][/id]
output json:/var/log/rtl_433.log
output log
```

输出被发送到可供 Home Assistant 访问的 MQTT broker，在我的场景中，它运行在同一台虚拟机中。标志 `retain` 被设为 `false`，因为我不希望用瞬时设备的数据弄乱 MQTT 主题，并且可以接受在 Home Assistant 中重新发现设备的速度慢一些。

JSON 输出也会写入 **/var/log** 中的 JSON 文件。

## 经验教训

rtl\_433 配置文件中的最后一行才是关键。在我的原型系统里，日志被写入 **/tmp**，大约 36 小时就会填满，从而导致系统宕机。所以，如果你不打算定期清理，千万不要把日志写到 **/tmp**。

硬件必须可靠。我在 Pine A64+ 上遇到了很多问题，尤其是它的重启问题。

我学到了不少 FreeBSD 基础配置的知识，再次体会到了其工具的优雅之处。

## 当前状态

自上次暴风雨和停电以来，该系统已经保持了 57 天的正常运行时间。在那次事件中，它在无人干预的情况下自动完成了启动。该系统可靠、易于复制，并且能够轻松升级到新的 FreeBSD 版本。只需更换 SD 卡，就可以轻松测试新版本或新配置，并在需要时恢复到原有状态。

日后，我希望探索使用 `nanobsd` 开发的可能性，并考虑这个项目是否可以作为深入研究 `Crochet/Poudriere` 镜像创建的好机会。

***

**Vanja Cvelbar** 是意大利国家研究委员会材料车间研究所的技术合作者，担任 IT 组负责人，负责管理支撑科研与运行的 IT 基础设施与服务。他的专长涵盖系统管理、网络和开源技术。在以 Linux 为主的工作二十年后，他又回归了 FreeBSD —— 他的首个系统是 3.2 版。如今，他专注于部署和维护基于 FreeBSD 的环境，为研究所提供核心 IT 服务。


# 撰写有效的 Bug 报告

* 原文：[Writing Effective Bug Reports](https://freebsdfoundation.org/our-work/journal/browser-based-edition/embedded-2/writing-effective-bug-reports/)
* 作者：Tom Jones

![Bug 报告](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-e9344ee5b7cf43fac7a658ac7f016cdc274aa79d%2Fwriting-effective-bug-reports-01.jpg?alt=media)

几年前，我们深受敬重的前 FreeBSD 期刊编辑委员会成员 Kristof Provost 写过一篇关于理想 Bug 报告方式的好文章。遗憾的是，Michael W Lucas 总会在年初就用光我们每期讽刺的额度，所以为了避免“讽刺透支”，决定由我改写 Kristof 的原文，以免出现“反讽破产”。

每个系统维护者都有自己偏好的 Bug 报告与改进请求接收方式。由于 Kristof 是 FreeBSD 中 `pf` 开发的核心人物，他正是为期刊撰写这篇文章的理想人选。

在被要求改写原文时，Kristof 的回应是：

**“我无法在完美的基础上再做改进。”**

因此，我来重述 Kristof 的原话，并给你一些入门建议，帮助你更快整理出最重要、最棘手的 Bug。你可以在这里找到他的博文：<https://www.sigsegv.be/blog/2014/Mar#bug_reports>。

## Bug

软件项目常常让人觉得就是由 Bug 堆砌而成；开发者会说一切都是鞋油和胶带粘起来的。造成最大麻烦的问题往往最难捉摸。“深夜，当 12 个并发请求同时打到我们的测地负载均衡器上时……”并不是复杂 Bug 的罕见开头。有时问题的描述甚至是“运行 1000 小时之后……”这种令人头疼的情况。

幸运的是，并非所有 Bug 都如此。FreeBSD 在许多情况下会 panic，对用户而言可能是糟糕的一天，但对开发者而言，panic 消息可能是定位问题的重要线索。和无声的数据损坏相比，我宁愿碰到 panic。

有些 Bug 只是表面问题，比如字段显示不佳，或文档不清晰（是的，我们也认为这算 Bug！）。

无论你的 Bug 是逻辑上的不可能，还是拼写错误，我会给你展示一个框架，帮助你推动修复。

## Bug 生命周期

在介绍如何写好 Bug 报告之前，先说说之后会发生什么，因为这能解释为什么你需要在最初报告时尽可能多提供信息。

在 FreeBSD 中，大多数 Bug 会通过邮件列表或 Bug 跟踪系统（[bugs.freebsd.org](https://bugs.freebsd.org/)）传播。开发者会定期阅读邮件列表，但最能引起关注的方法还是在 Bug 跟踪系统上提交工单。

新创建的 Bug 会被标记为“new”，处于新 Bug 状态。这时它只是被用户提交，还没有通知到具体的项目领域。

Bugmeister 团队会检查新提交，将其从“new”状态重新分配，并尽量设置正确的分类。开发者可以选择订阅某些类别的 Bug。相关的项目邮件列表也会收到新 Bug 的通知。另外，还会定期发送“该组关注的 Bug”摘要邮件。

Bug 可能会被 Bugmeister 或其他 FreeBSD 项目成员分配给某个开发者。

某个开发者可能决定“接手” Bug，成为该问题的负责人。这通常意味着他决定修复这个 Bug 或分析它。

随后可能会有一些往返沟通，开发者会要求更多信息；如果一切顺利，开发者最终会创建代码审查（在 [reviews.freebsd.org](https://reviews.freebsd.org) 上）。代码审查可能会在 Bug 中引用，也可能只是开发者向熟悉该子系统的同行寻求反馈。

如果需要测试以确认 Bug 是否解决，这会被特别指出。

最终，待代码审查与测试完成，修复就会被提交到 FreeBSD；提交消息通常会包含 Bug 的引用，并附加自动更新。有时修复 Bug 需要多个补丁。

修复提交后，你（用户）就能再次无 Bug 使用软件，前提是你运行的是 CURRENT 快照。许多修复会从 CURRENT 合并到 STABLE 分支（称为 MFC）。如果 Bug 被 MFC，修复就会出现在 STABLE 分支，并包含在下一个点版本中。

从报告问题到修复的过程可能很长；有的问题 20 分钟就能解决，有的问题可能需要数月甚至数年。我见过调试超过十年才关闭的 Bug。

解决问题所需的时间，很大程度上取决于你报告的时机。如果你在提交刚落地后就发现 Bug，很可能当天就能修复。其他 Bug 可能更晚才出现，或只出现在某些 FreeBSD 发行版本的环境中。

修复这些 Bug 的速度，主要取决于 Bug 报告的质量和围绕问题的持续讨论。接下来我们看看好的 Bug 报告该包含什么。

## 哪里提交 Bug 报告

FreeBSD 项目鼓励用户通过多种渠道分享使用体验。按优先顺序，新 Bug 或有趣 Bug 的报告渠道如下：

* 在 [reviews.freebsd.org](https://reviews.freebsd.org/) 上提交带有清晰测试的易于应用的变更
* 在 [reviews.freebsd.org](https://reviews.freebsd.org/) 上提交临时修补的解决方案
* 在 [bugs.freebsd.org](https://bugs.freebsd.org/) 上提交新的 Bug
* 直接给开发者发邮件（或其他形式的沟通）
* 在论坛上抱怨并推测原因
* 在 IRC、Matrix、Discord 或其他聊天渠道留言
* **在社交媒体上愤怒发帖，说 FreeBSD 是最差的**

前两种方式假定了较高的技术水平。如果这对你来说太难，也没关系，我们依然欢迎 Bug 报告。

我们不要求每个人都能做高级开发，只希望每位报告者在每一步尽可能提供相关细节。

对于拼写错误，大多数用户可以立即提出修复建议，但修改文档需要使用开发者工具，我们并不要求每个人都做到。如果你能使用这些工具，创建审查比提交新 Bug 更好，因为最终的变更无论如何都需要审查。

更复杂的 Bug 也许能立即解决；有些复杂的 Bug 答案很简单。然而，另一些 Bug 很难诊断，在很长一段时间里，我们只能通过一些看似无关的失败来侧面推断。

如果你有任何质量的修复建议，最重要的是把你的建议公开发布。顺着列表往下找，直到找到合适的位置。

## 如何写好 Bug 报告

### 应包含的信息

好的 Bug 报告应该包含什么，已经有很多资料可供参考；FreeBSD 中软件的广度使得任何单一模板都很难套用。以下是一些经验法则，告诉你应该包含什么：

* 关于你环境的详细信息
* 你期望发生什么
* 实际发生了什么
* 期望与实际的差异
* 这一点并不总是显而易见
* 触发 Bug 的前因后果
* 时间因素有助于开发者调试问题。如果 Bug 在重启后立即出现，或只在运行 5 天后才出现，所需的调试策略是不同的

### 提供哪些资料

系统运行时会生成很多有助于调试的信息，你可能会被要求提供日志，例如：

* `dmesg` 的输出
* panic 消息的完整文本和堆栈跟踪
* 硬件问题时 `pciconf -lv` 的输出
* syslog 输出
* 工具输出和错误信息

提供的资料宁多勿少；如果开发者不得不再来问你一次，可能会耽误至少一周。

### 环境信息

你需要在描述财富 500 强公司的 Anycast 负载均衡配置和只展示单条防火墙规则之间找到平衡。通常，报告中提供的环境信息少于能够提供的；工具与网络配置的组合有助于开发者理解 Bug。

那么，你的环境包括什么？

我们需要知道你使用的 FreeBSD 版本和有多少台主机。Bug 出现在 14 台和 15 台主机之间这一事实，往往比只知道是 14 台更有意义。

我们需要了解自定义和偏离常规的情况；如果你自己编译了 FreeBSD 或使用软件包，都应该说明。这两种方式我们都支持，但如果你运行的不是 release engineering 版本，那就需要考虑这一点。

如果你运行的不是 FreeBSD，而是 pfSense 或 HardenedBSD 这样的下游发行版，需要一并说明。这并不意味着 FreeBSD 开发者不会帮助你，但隐瞒这一点很可能会惹恼某些人。

这一点很重要，因为下游发行版会使用不同的编译标志、软件包构建 FreeBSD，并附带自己的调优。所有这些都可能导致 Bug 在一个平台上存在而在另一个平台上不存在。

我们真正需要的是你的实际环境，而不是理想化的版本或教程中的示例。

### 期望与实际

报告问题的另一个关键部分是描述你期望发生什么。有些软件无法做到你想要的，要么现在做不到（由于功能尚在开发），要么永远做不到（因为这根本不是 Bug！）。

`ls` 不会复制文件或播放视频，但 `pfctl` 应该能够清除规则状态计数器。请告诉我们你期望的输出和效果，以便我们复核你的所有断言。

提供工具的输出，描述你能测量的实际效果，并标出任何问题。

对你来说工具坏了是显而易见的。你提交的是 Bug，但作为分拣问题的开发者，在发现问题所在方面获得额外帮助至关重要。

许多 Bug 在文本报告中不容易看出，性能问题可能难以关联（也难以诊断和修复）。

请非常清楚说明发生了什么和哪里不正确。

### 触发 Bug 的过程

对于许多 Bug，触发 Bug 前的事件很重要。“做了 X 之后无法再做 Y” 是一份优秀的 Bug 报告——它暗示了一条通往开发者黄金（见下文）的道路。

“`ls` 在磁盘满了之后报告错误的文件大小” 能告诉我们很多信息，而 “`ls` 报告错误的文件大小” 则不能。

在 Bug 出现前后查看 `dmesg` 可能会有帮助；内核子系统会在那里报告错误和资源耗尽。在 `dmesg` 中，我们可以看到接口不断上下波动。

## 提供信息的方式

你提供的信息最好是文本形式。如果有错误消息，应该全部包含；有些软件在执行操作时总是会打印错误消息或奇怪的诊断信息。在 Bug 报告中，你应该尽量包含这些信息。

如果你遇到 panic，可能难以从某处复制粘贴文本。这种情况下，用手机拍照是一种可行的策略；但是，你应该尽量在文件大小和图像清晰度之间取得平衡。理想情况下，你应该拍照并尽可能准确抄录文字。

你的图片托管服务将来很可能会消失，但 FreeBSD Bug 跟踪系统中的文本字符串可以保存几十年。

## 优秀 Bug 报告的关键：复现步骤

一份优秀的 Bug 报告会包含上述大部分信息，但还会包含开发者黄金：复现用例。

对于简单问题，这可能只是命令调用；但对于某些问题，你可能需要编写协同工作的 shell 脚本。网络 Bug 的复现用例可能很难编写，但得益于 vnet jail，实际上只需一个 shell 脚本就可以做到（事实上，防火墙测试套件就是这样工作的）。

复现用例的目标是将问题的复现简化到最基本的组件。

## 主动协助

提交 Bug 后，这可能已经足以让开发者感兴趣。如果你能在提交后的 20 分钟内指出回归或新 Bug，很可能立刻引起开发者关注。大多数开发者在提交变更后会关注邮件列表和 src-commits 列表，正是为了捕捉这类报告。

如果你的 Bug 被搁置或过了一段时间才分配给团队，不要慌；这是相当常见的经历。你可以主动联系在相关领域或类似 Ports 上工作的开发者。只要保持礼貌；你在使用志愿者的时间。

开发者接手 Bug 后，可能会问你问题；时间久了，可能会问：“在最新快照上还会出现吗？”

他们可能会请你提供更多信息或测试补丁。某些只出现在特定硬件上的 Bug，如果没人验证，补丁可能会长时间搁置。

有些 Bug 确实很难搞清楚。如果开发者正在和你合作，欢迎提出你认为可能触发 Bug 的理论。有些 Bug 听起来像鬼故事，直到我们找到最终的修复方案。

## 祝你好运！

撰写并提交 Bug 报告是一件耗费精力的事，有时你会觉得自己的努力无人理睬。但 FreeBSD 是一项志愿者项目，开发者根据兴趣分配时间。我们拥有一群优秀的开发者，他们愿意追踪复杂而隐蔽的问题，但他们需要你的帮助与合作，收集足够的信息来复现和测试。

如果你遵循这里的建议，你会更顺利让 Bug 获得关注，并推动它们被修复。

***

**Tom Jones**，FreeBSD 提交者，对保持网络栈高性能充满兴趣。


# 实现量子安全网站

* 原文：[implementing-a-quantum-safe-website](https://freebsdfoundation.org/our-work/journal/browser-based-edition/embedded-2/implementing-a-quantum-safe-website/)
* 作者：Gergely Poór

不久前，在我目前的工作单位里，有人提醒我：传统密码学将不再被视为安全。

据多方消息预计，在未来 10 年内，量子计算的进步将达到这样的水平：我们习以为常的现代密码算法将会在几秒钟内被轻易攻破。很自然，我开始研究这个话题，想弄清楚情况到底有多糟，和我们今天能做些什么来为这种情形做准备。所谓的“量子威胁”通常被描述为日后某个时点，量子计算机能拥有足够的处理能力，可以在几分钟甚至几秒钟内破解传统加密。听起来很糟，对吧？更糟的是，还有相关现象叫“现在存储——以后解密”（Store now – decrypt later）或“现在收集——以后解密”（Harvest now – decrypt later），它的基本意思是：当前在互联网上传输的“安全”数据会被拦截并保存，等到足够强大的量子计算机广泛可用时，再用它来解密此前捕获的数据。

但什么是“当前的安全”呢？就非对称加密而言，存在密钥交换，其中使用最广的是 RSA，密钥长度为 2048、3072 或 4096 位。这些密钥交换（不仅限于 RSA）可以出现在到远程服务器的 SSH 会话、用于输入你银行账号信息的 TLS 加密网站，甚至两个远程位置之间基于 IKEv2 的 IPSec VPN 隧道中。通过这些通道发送的所有数据都可能被拦截并保存，等以后再解密。可问题在于，如何解密 RSA 密钥？答案是秀尔（Shor）算法，它由 Peter Shor 在 20 世纪 90 年代提出，旨在用量子计算机分解大整数的因数。但这与密钥交换算法有什么关系？以 RSA 为例。简单说，它通过将两个大素数相乘来得到更大的数。两枚素数被保密，并与其他若干数一起构成私钥的一部分。它们的乘积与一些额外数值一起构成公钥的一部分。使其脆弱的地方在于：若设法找到了这两个起始素数，你就可以用它们计算出私钥的一部分，进而求得私钥的其余部分。有了私钥，你就能解密一方发送的内容；再解一次，就能完全解开这段已捕获的信息交换。当然，这需要巨大的计算能力，因为公钥越大，需要的计算就越多。秀尔算法提供了一种方法，利用量子计算机一次性并行计算所有可能的解，从而加速这一过程。

我们该如何抵御这类攻击？有三种答案：

你可以继续使用标准算法，但采用更长的密钥。目前认为，超出 4096 位的 RSA 密钥在短期内仍难以被破解。如果想更保险，可以使用 8192 位或更长的密钥。

不过，还有其他替代方法来解决这个问题。第二种是 PQC（后量子密码学，Post-Quantum Cryptography）。研究人员和数学家开始研制能对抗秀尔算法的其他算法。这些算法基于所谓的数学“陷门”函数：从方程到结果很容易，但反过来（几乎）不可能。美国国家标准与技术研究院（NIST）开始收集这些算法，众多密码学专家与数学家测试它们，以判断其是否能抵抗秀尔算法。这就是“后量子密码学项目”。多年来经历了数轮遴选与淘汰。2024 年，NIST 发布了 FIPS 203 标准，指定 ML-KEM（此前称为 CRYSTALS-Kyber 或 Kyber）为通用加密的主要标准；并在 2025 年表示，若 ML-KEM 出现问题，将以 HQC 作为后备。

第三种选择是 QKD（量子密钥分发，Quantum Key Distribution），它基于对光子粒子的量子物理操控来安全产生密钥。该方法极其昂贵，需要专用设备，和在通信双方之间预先存在的光纤网络连接，目前两端点之间的距离上限约为 100 km。

## 项目目标

我有之前做的小网站，仅使用 nginx，没有什么 HTML、CSS、PHP 等。它是简单的网站，会返回客户端的 IP 地址（类似 icanhazip.com 或 ifconfig.me）。它最初是我学习 nginx 时的兴趣项目，但后来我在更大的网络里部署它，用于测试客户端是否在 NAT 之后。这正好是我做 PQC 实验的良好起点。需求很简单：既要能适配广泛的软件（网页浏览器、**fetch(1)** 或 **curl(1)** 等命令行工具），又要遵循我在使用 FreeBSD 与 Linux 多年中体会到的 UNIX 哲学——尽可能简单，同时要稳定，因为它以后也可能跑在云端 VPS 上。因此，我自然选择 FreeBSD 作为操作系统、nginx 作为平台。剩下的就是 PQC 的实际实现。

我做了一些研究，找到了 Open Quantum Safe（OQS）项目的子项目——[oqs-provider](https://github.com/open-quantum-safe/oqs-provider)。它是一款适用于 OpenSSL 3 版本的开源 C 库与 provider，实现了包括 ML-KEM 在内的多种算法。它在 FreeBSD 与多种 Linux 发行版上均可用。

## 工作原理

简单讲，它把各种用于密钥交换与签名的 PQC 算法集成到了 OpenSSL 中。就密钥交换（和其他）而言，它支持 ML-KEM 与多种基于椭圆曲线的 Diffie-Hellman 密钥交换，如 X25519、p384、p521、SecP384r1。它们可以从名称上直观识别，比如 X25519MLKEM768，表示使用 Curve 25519 搭配 768 位的 ML-KEM 密钥。PQC 算法需要 TLSv1.3，但出于兼容性考虑，我们会将最低版本设置为 TLSv1.2，这样旧系统仍能访问该网站。要注意的是，可能会出现“[TL; DR fail](https://tldr.fail/)”错误：如果客户端软件没有正确配置以在 TLSv1.3 上支持 PQC 算法，就会导致 TLS 失败；不过，随着软件在客户端上更新与推广，这个问题会逐渐缓解。如果你不在乎旧客户端或有缺陷的软件，并且想要 100% 量子安全的网站，可以完全禁用 TLSv1.2（稍后你会在 nginx 配置里看到）。理论部分说完，进入有趣的部分：实际实现！

## 实现

oqsprovider 需要 OpenSSL 3.2 或更高版本。根据其 GitHub 页面说明，从 3.4 版本开始又新增了一些功能。我的 FreeBSD 安装里是 OpenSSL 3.0.16，不支持 oqsprovider。

在撰写本文时，OpenSSL 3.5.0 已发布，带有原生的 PQC 算法支持，但 **pkg(8)** 的说明指出它处于 beta 阶段，不适合生产环境。因此，在余下实现中，我将继续使用 OpenSSL 3.4.1。

首先，我把虚拟机更新到 FreeBSD 14.3-RELEASE。我们还需要安装更新版本的 OpenSSL 和 nginx，并且要让 nginx 能使用较新的 OpenSSL。为了尽量减少折腾，我们将通过 **pkg(8)** 安装 openssl34 与 openssl-oqsprovider，而 nginx 则通过 Ports 系统构建。为此需要在 **/usr/ports** 下准备好 Ports。我的系统里没有 **security/openssl34**，因此我将拉取 Ports 树的 2025Q2 分支。我需要它来让 nginx 链接到 openssl34。首先，我会安装 **git(1)**，这是 FreeBSD 手册推荐的安装 / 更新 Ports 树的方法。

```sh
# pkg install -y git
```

待系统上安装了 **git(1)**，就可以用它来管理 Ports 树了。不过，由于我之前通过基本系统安装过 Ports 树，所以现在会先删除现有的 **/usr/ports** 目录。这样在克隆仓库时，就不会提示 **/usr/ports** 已存在。当然，还有其他办法解决这个问题，但我更喜欢从一个干净的环境开始。

```sh
# rm -rf /usr/ports
# git clone --depth 1 https://git.FreeBSD.org/ports.git -b 2025Q2 /usr/ports
```

之后，我们需要安装 OpenSSL 3.4.1 和 oqsprovider。

```sh
# pkg install -y openssl34 openssl-oqsprovider
```

然后，正如安装信息所提示的那样，我们需要将 **/usr/local/openssl/oqsprovider.cnf** 的内容合并到 **/usr/local/openssl/openssl.cnf** 中。由于我们刚刚安装了新版 OpenSSL，合并之后，**/usr/local/openssl/openssl.cnf** 的内容将如下所示：

```ini
…
[provider_sect]
default = default_sect
oqsprovider = oqsprovider_sect
….
[default_sect]
activate = 1

[oqsprovider_sect]
activate = 1
module = /usr/local/lib/ossl-modules/oqsprovider.so
…
```

现在我们要编译 nginx。

```sh
# cd /usr/ports/www/nginx
```

我将设置一些环境变量，以便让 nginx 链接到新安装的 OpenSSL 3.4.1。

```sh
# export OPENSSL_BASE=/usr/local
# export OPENSSL_LIBS="-L/usr/local/lib"
# export OPENSSL_CFLAGS="-I/usr/local/include"
```

接下来我们将配置 nginx，确保启用了 `HTTP_SSL`（它应该是默认开启的，但最好还是确认一下）。我不会调整其他任何设置。

```sh
# make config
```

现在我们已经准备好开始编译 nginx 了。设置一些环境变量来指定新安装的 OpenSSL 的路径，然后按下回车即可。

```sh
# make OPENSSLBASE=/usr/local OPENSSLDIR=/usr/local/openssl install clean
```

nginx 编译时，可以去拿一杯你喜欢的饮料，处理一些工单，或者把编译输出展示给朋友们，让他们看看你有多酷。

编译完成后，我们来验证 nginx 是否已经链接到了安装在 **/usr/local** 下的 OpenSSL 3.4.1：

```sh
# nginx -V 2>&1 | grep -i openssl
built with OpenSSL 3.4.1 11 Feb 2025
# ldd /usr/local/sbin/nginx | grep ssl
    libssl.so.16 => /usr/local/lib/libssl.so.16 (0x16a83b849000)
```

注意：括号中的十六进制标识符可能会有所不同。

如果你的输出和我的一样，那么你就已经成功为 nginx 添加了 OpenSSL 3.4 的支持。接下来，我们将为网站创建一个包含 PQC 的配置文件。让我们进入 **/usr/local/etc/nginx** 目录，首先备份原始的 **nginx.conf** 文件：

```sh
# cd /usr/local/etc/nginx
# mv nginx.conf nginx.conf.orig
```

现在我们来创建新的配置：

```sh
# vi nginx.conf
```

在文件中添加以下若干行：

```sh
events{}
http{
     server{
         listen 443 ssl;
         ssl_certificate /usr/local/etc/nginx/server.crt;
         ssl_certificate_key /usr/local/etc/nginx/private.key;
         ssl_protocols TLSv1.2 TLSv1.3; # 如果不需要向后兼容，可以移除 TLSv1.2
         ssl_prefer_server_ciphers off;
         ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305; # 如果你使用纯 TLSv1.3，可以根据 Mozilla SSL Configuration Generator 的 "modern" 设置移除此行
         ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1; #这是 PQC 部分，其余内容仅用于非 PQC 的兼容性。
         location /{
            return 200 "$remote_addr\n";
         }
     }
     server{
         listen 80;
         location /{
            return 301 https://$host$request_uri;
         }
     }
}
```

此配置实现了以下功能：

* 监听 80 和 443 端口
* 将明文 HTTP 请求重定向到 HTTPS
* 使用 TLS 1.3 和 TLS 1.2 以保证兼容性
* 使用平衡的加密套件，在提供较好安全性的同时兼顾广泛的兼容性（加密套件和曲线列表来源于 Mozilla SSL Configuration Generator）

接下来，在本演示中我将创建一张自签名证书，但在生产环境中（和为了兼容性），你应当获取由可信 CA 签发的有效证书。你可以使用 Let’s Encrypt 证书，并通过 **certbot(1)** 来自动化证书续期。为此，只需运行以下命令：

```sh
# pkg install -y py311-certbot
```

之后，按照 Certbot 的使用说明操作。

如果要创建自签名证书，可以使用下面这行命令（记得将 DNS 和 IP 字段改为与你的环境相匹配）：

```sh
# openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout private.key -out server.crt \
  -subj "/CN=quantum" \
  -addext "subjectAltName=DNS:quantum,IP:192.168.2.40"
```

它将使用 2048 位的 RSA 密钥，但你也可以将其提升到 8192 位（记住，目前认为超过 4K 的密钥在量子计算面前仍是安全的）。不过要注意，这可能会导致某些旧系统的兼容性问题。证书的有效期为 1 年，这对测试目的来说已经绰绰有余。（此时我想起那句话：“没有什么比临时解决方案更永久的了。”）同时，证书中包含了我 FreeBSD 虚拟机的 IP 地址和主机名。如果没有这些，一些软件（例如 PowerShell）会抱怨无法信任证书，从而无法与网站建立或完成 TLS 会话。

当证书和私钥都准备好之后，你就可以启动 nginx 了。但在此之前，我们需要在 **/etc/rc.conf** 中指定它应当在开机时自动启动。

```sh
# sysrc nginx_enable=YES
# service nginx start
```

它会在启动之前先验证 **nginx.conf** 的语法，并简单测试配置。如果一切正常，你应该会看到如下输出：

```sh
Performing sanity check on nginx configuration:
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful
Starting nginx.
```

## 测试

现在我们已经有正在运行的网站，接下来让我们检查它是否正常工作。在测试时，我将使用多种方法：网页浏览器和一些命令行客户端，例如 **curl(1)**。我已经安装好了 curl，如果你还没有，可以通过 `pkg` 来安装：

```sh
# pkg install -y curl
```

你可以使用 `curl` 来检查网站（注意由于使用的是自签名证书，因此需要加上参数 `--insecure`）：

```sh
# curl --insecure https://127.0.0.1
```

它会返回 **127.0.0.1** 和换行符。如果想获取更多信息，可以加上参数 `-v`，让输出更详细。

```sh
# curl --insecure -v https://127.0.0.1
```

在我的环境中，由于 curl 链接的是系统默认的 OpenSSL 版本，它并不支持 PQC 算法，因此会回退到 X25519：

```sh
…
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
…
```

你也可以使用以下命令来验证重定向：

```sh
# curl --insecure -vL http://127.0.0.1
```

如果你看到如下输出，就说明它成功了：

```sh
…
* Request completely sent off
< HTTP/1.1 301 Moved Permanently
….
* Clear auth, redirects to port from 80 to 443
…
```

对于基于 Microsoft Windows 的主机，如果你使用的是自签名证书，就需要将该证书（在我们的示例中是 **server.crt**）导入到 **“受信任的根证书颁发机构”** 存储区中，然后运行以下 PowerShell 命令：

```powershell
(Invoke-WebRequest https://192.168.2.40).Content
```

或者，如果你想要更详细的输出，可以使用：

```powershell
Invoke-WebRequest https://192.168.2.40
```

使用 curl 也可以，不过在 Windows 上它只是 `Invoke-WebRequest` 的前端，不具备 FreeBSD 或 Linux 版本中的那些参数。

为了测试与其他硬件的兼容性，我登录到了我的 MikroTik 路由器，并在命令行中调用了该 URL：

```sh
/tool fetch url="https://192.168.2.40" output=user check-certificate=no
    status: finished
  downloaded: 0KiB
     total: 0KiB
    duration: 1s
     data: 192.168.2.1n
```

注意末尾的那个 “n”。如果你想去掉它，可以在 **/usr/local/etc/nginx/nginx.conf** 中修改以下这一行：

```sh
return 200 "$remote_addr\n";
```

改为：

```sh
return 200 "$remote_addr";
```

这样一来，如果使用 curl 之类的工具调用，它就不会再返回换行符了。你可以根据自己的需求决定保留哪种版本。如果你打算在脚本中使用这个 IP 地址，最好去掉 **\n**；而对我来说，目前只是调试用途，所以我会保持原样。

对于网页浏览器，如果你希望启用 PQC 支持，在某些版本中可能需要手动开启相关功能。我正在使用 Firefox 139.0.4，从 132 版本起就已经默认启用了 PQC 支持。而 Chrome 从 124 版本开始也支持 PQC，但在某些情况下需要你手动启用：

```sh
chrome://flags/#enable-tls13-kyber
```

你可以通过进入 `about:config` 来检查 Firefox 是否支持该功能，并查找以下配置项：

```sh
security.tls.enable_kyber
```

接下来，按 `F12` 打开开发者工具：

* Firefox 切换到 “network” 选项卡
* Chrome 进入 “隐私和安全”

然后输入你的 FreeBSD 安装的 IP 地址并按回车。屏幕上应该只显示 IP 地址（即请求的来源）。在开发者工具中，点击对应你 FreeBSD IP 地址的请求条目：

* 如果使用 Firefox，还需要点击右侧的 “Security” 标签页。

如果你的浏览器支持 PQC，你会看到密钥交换方式是：

* `mlkem768x25519`（Firefox）
* `X25519MLKEM768`（Chrome）

![浏览器 PQC 密钥交换结果图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-e729551154e89c9a43de13fd97db874e086f6baf%2Fimplementing-a-quantum-safe-website-01.png?alt=media)

如果看到了上述结果，那么恭喜你！你已经成功在 FreeBSD 上部署了带有 PQC 支持、同时兼容传统加密的安全网站。欢迎来到未来！

## 总结

为 TLS 密钥交换添加 PQC 支持并不算太难，但整体流程包含了一些额外却必要的步骤。关键问题是你必须从源码编译 nginx 才能使用新版 OpenSSL。这意味着每次有安全补丁发布时，你都需要重新编译，而不像使用 **freebsd-update(8)** 或 **pkg(8)** 热修复那样方便。

不过，OpenSSL 3.5.0 已经发布，它原生支持多种 PQC 算法，并且是长期支持（LTS）版本，官网称将支持至 2030 年。随着量子威胁的加剧和全行业急于尽快部署 PQC，我非常希望这一版本能被集成到 FreeBSD 基本系统中。这将省去绝大部分让 nginx（和可能的其他软件）支持 PQC 的额外步骤。

当然，FreeBSD 对稳定性的要求很高，在 OpenSSL 3.5 被充分测试和验证之前，基础安装中很可能仍会保留较旧的版本。本文展示的网站只是量子安全加密的小例子，但可以想象更多软件能从 PQC 中获益。比如银行、医疗或政府网站若部署 ML-KEM，将几乎不可能被未来的量子计算机解密，银行账户信息、患者资料和个人身份信息在传输过程中将得到保护。

虽然这种普及不会马上发生，但随着越来越多人接触到“量子威胁”这一概念，意识也会逐渐提高，我们也会更接近那样的世界：后量子密码学成为日常生活的一部分。

***

**Gergely Poór** 是一名 Linux/BSD 系统与网络工程师、电工，同时也是一位 FreeBSD 爱好者，自 2018 年高中毕业起一直从事 IT 工作。他的经验涵盖了从中小企业桌面支持到企业级混合云和工业/物联网系统。他始终热衷于学习新知识。现居住在匈牙利布达佩斯，与深爱的妻子 Kriszti 一起生活，业余时间喜欢使用 sh/bash 编程，并开发自己的智能家居系统。


# FreeBSD WiFi 开发第二部分：驱动开发

* 原文：[FreeBSD WiFi Development Part 2: Working on a Driver](https://freebsdfoundation.org/our-work/journal/browser-based-edition/embedded-2/freebsd-wifi-development-part-2-working-on-a-driver/)
* 作者：Tom Jones

![WiFi 开发](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-e2c2873146252b3fe827c95c193357bf6f6167d5%2Ffreebsd-wifi-development-part-2-working-on-a-driver-01.png?alt=media)

这是 FreeBSD 上 WiFi 开发系列的第二篇文章。在 [第一篇文章](https://freebsdfoundation.org/our-work/journal/browser-based-edition/networking-3/freebsd-wifi-development/) 中，我们介绍了 WiFi/80211 网络的一些术语，简要讲解了典型的网络架构，并展示了如何使用 `ifconfig` 和一些无线网卡创建 station、host ap 和 monitor 模式的 WLAN 接口。我们还介绍了实现 WiFi 子系统的两个不同内核层 —— **驱动** 和 **net80211**。

**驱动**：如 iwx、rtwn 和 ath，通过 USB 或 PCIe 等物理总线与无线网卡通信，通常通过固件接口实现。

**net80211**：加入网络、发送数据包和执行其他复杂操作所需的抽象状态机，在许多驱动中是通用的。

为了在硬件需求上保持灵活性，**net80211** 层本身能实现 IEEE 802.11 状态机的大多数部分。这种架构使我们能够用标准接口与整个网络栈集成，屏蔽不同网卡支持能力的差异。同时，它还能支持纯软件的 WLAN 适配器，这在测试环境中非常有用。

网卡对功能的支持程度不同，范围从 **FullMAC 接口**（几乎所有处理都在网卡上直接完成），到几乎完全依赖 **net80211** 堆栈，仅由网卡管理射频。在 FullMAC 卡中，固件会为操作系统驱动暴露配置接口，所有的数据包收发和管理操作（如切换信道、扫描）都由固件完成。OpenBSD 和 NetBSD 中的 **bwfm Broadcom 驱动** 就是 FullMAC 驱动的例子。

其他网卡需要 **net80211** 堆栈提供多种服务以支持驱动的运行。一些设备（如 iwx）提供扫描和加入网络等管理接口，但大多数操作仍由 **net80211** 完成。

较老的驱动则必须自己实现更多的 **net80211** 状态机逻辑，而不是重复大量类似的代码。

实际上，所有 WiFi 驱动都处在这样一个范围内：一端是固件几乎完成所有工作，另一端是操作系统管理大部分射频和传输。要实际了解其运行方式，最好的方法就是直接看驱动。

接下来我们先看看驱动是如何附加并出现在 `net.wlan.devices` 列表中的，然后再看数据包是如何从 **net80211** 堆栈发往 WiFi 射频的。

本文将主要关注 `if_iwx` 驱动，原因有两点：第一，我对它非常熟悉，因为我曾将该驱动从 Future Crew 的源码引入 FreeBSD 树；第二，作为新驱动，它还有很多“低垂的果实”（容易改进的地方）。

## 将驱动连接到硬件

驱动的生命周期通常包括：

* **probe**
* **attach** （执行一些初始化工作）
* **detach**

许多驱动只有在关机时才会经历 detach。**probe** 和 **attach** 阶段是我们在为已有驱动添加新硬件支持或编写新驱动时需要重点处理的。

当总线发现某设备时，会依次询问所有已注册的驱动是否能支持该硬件。在所有驱动被询问后，总线会按照探测响应顺序，请求驱动是否能 `attach` 设备。第一款成功附加的驱动“获胜”。

在之后的某个时刻，驱动可能会被移除：可能由于总线错误、设备移除（如 USB 拔出），或者系统关机、重启。

在这些阶段中，每一步都通过与总线注册的回调来实现。举例来说，下面是 `if_iwx.c` 中的 `pci_methods` 结构体。

```c
static device_method_t iwx_pci_methods[] = {
        /* 设备接口 */
        DEVMETHOD(device_probe,         iwx_probe),
        DEVMETHOD(device_attach,        iwx_attach),
        DEVMETHOD(device_detach,        iwx_detach),
        DEVMETHOD(device_suspend,       iwx_suspend),
        DEVMETHOD(device_resume,        iwx_resume),

        DEVMETHOD_END
};
```

`if_iwx` 注册了 probe、attach 和 detach 方法，和 suspend 和 resume 方法，这些方法都会在需要时被调用。

## Probe

WiFi 设备通常由一颗芯片组和一些辅助硬件组成。芯片组由 Realtek 或 Intel 这样的公司制造，但实际设备则是由另一家公司基于芯片组生产的。这种模式意味着我们会得到基于 rtwn 的设备，但它们可能由 TP-Link 这样的公司制造。围绕芯片组构建设备的公司会提供驱动和配置信息，从而使少量驱动可以支持更多的设备 ID。

这也意味着，FreeBSD 新贡献者常见的首个补丁，就是为某些尚未覆盖的设备添加设备 ID（我自己的第一个补丁就是在 MIPS 路由器中添加了一颗闪存芯片 ID！）。

在 FreeBSD WiFi 中，你的第一个改动也可能很直接：买一台你认为应该能用的设备并测试它（按照本系列第一篇文章中的说明操作）。

如果没有驱动能探测到该硬件，你可以列出 USB 或 PCIe 设备 ID，然后参考其他平台来判断应该由哪个驱动支持它们。

我最近为一位外部贡献者合并的两个 FreeBSD 改动就是这样的：为 `if_run` 和 `if_rum` 驱动添加了硬件设备 ID。下面是 run 驱动部分的改动示例：

```c
diff --git a/sys/dev/usb/wlan/if_run.c b/sys/dev/usb/wlan/if_run.c
index 00e005fd7d4d..97c790dd5b81 100644
--- a/sys/dev/usb/wlan/if_run.c
+++ b/sys/dev/usb/wlan/if_run.c
@@ -324,6 +324,7 @@ static const STRUCT_USB_HOST_ID run_devs[] = {
     RUN_DEV(SITECOMEU,         RT2870_3),
     RUN_DEV(SITECOMEU,         RT2870_4),
     RUN_DEV(SITECOMEU,         RT3070),
+    RUN_DEV(SITECOMEU,         RT3070_1),
     RUN_DEV(SITECOMEU,         RT3070_2),
     RUN_DEV(SITECOMEU,         RT3070_3),
     RUN_DEV(SITECOMEU,         RT3070_4),
```

你在 FreeBSD 上的第一个改动可能就只是为某个设备的设备 ID 列表添加一行代码。在完成这一步之后，你需要在相关驱动里加上对应条目，测试它，然后把 diff 邮件发送给我 <thj@freebsd.org>，我会帮你提交。

## 连接到 net80211

WiFi 驱动所需的状态存储在驱动 softc 中的变量 `ieee80211com` 里（通常命名为 `ic`）。

驱动使用 `ic` 来设置能力标志，并通过覆盖函数指针来挂接或替换 **net80211** 栈提供的默认功能。

在本系列的上一篇文章中，我展示了如何通过 `ifconfig` 命令在驱动之上创建虚拟 WLAN 接口（VAP）。VAP 允许我们在同一块网卡上创建多个接口，并让它们在不同模式下运行，例如 **sta**、**host ap**、**monitor** 等。驱动通过 `ic_caps` 位字段来管理这些模式的可用性。

这些字段的值会在驱动的 attach 过程中设置。下面是 `if_iwx` 驱动中 `iwx_attach` 函数的示例：

```c
...
ic->ic_softc = sc;
ic->ic_name = device_get_nameunit(sc->sc_dev);
ic->ic_phytype = IEEE80211_T_OFDM; /* 不仅仅是 OFDM，但此处未使用 */
ic->ic_opmode = IEEE80211_M_STA;   /* 默认设置为 BSS 模式 */

/* 设置设备能力 */
ic->ic_caps =
    IEEE80211_C_STA |          /* 支持 STA 模式 */
    IEEE80211_C_MONITOR |      /* 支持监控模式 */
    IEEE80211_C_WPA |          /* 支持 WPA/RSN */
    IEEE80211_C_WME |          /* 支持 WME (QoS) */
    IEEE80211_C_PMGT |         /* 支持电源管理 */
    IEEE80211_C_SHSLOT |       /* 支持短时隙 */
    IEEE80211_C_SHPREAMBLE |   /* 支持短前导码 */
    IEEE80211_C_BGSCAN         /* 支持后台扫描 */
...
```

[来自 `if_iwx` 的 attach](https://cgit.freebsd.org/src/tree/sys/dev/iwx/if_iwx.c#n10127)

这段代码位于 `if_iwx` 的 attach 方法末尾。前面的 attach 代码则负责执行驱动的初始化工作，例如识别具体的 PCIe 设备，和确定该网卡的具体 Intel Wireless 型号。

`if_iwx` 驱动支持 **station 模式**（`IEEE80211_C_STA`）和 **monitor 模式**（`IEEE80211_C_MONITOR`）；如果它支持 **host AP 模式**（像 rtwn 那样），那么在能力位掩码中就会额外包含标志 `IEEE80211_C_HOSTAP`。

除了模式以外，iwx 还支持：WPA 加密（`IEEE80211_C_WPA`）、差分服务的多媒体扩展（`IEEE80211_C_WME`）、电源管理（`IEEE80211_C_PMGT`）、短时隙（`IEEE80211_C_SHSLOT`）、短前导码（`IEEE80211_C_SHPREAMBLE`）和后台扫描（`IEEE80211_C_BGSCAN`）。

完整的能力标志列表在 `ieee80211.h` 头文件中。驱动能声明哪些能力，既取决于硬件特性，也取决于驱动是否实现。在驱动开发阶段，某些功能（例如 WPA 硬件卸载）可能尚未实现，因此缺少某个标志并不意味着硬件不支持该特性。

驱动在 attach 阶段执行的第二个任务，是接管或实现 **net80211** 的功能，这通过 `iwx_attach_hook` 配置回调完成。在这里，驱动会覆盖 `ic_caps` 位字段所声明的大量特性的函数指针。

首先，`if_iwx` 会创建 **信道映射表**。对于这类网卡，驱动必须向网卡固件请求，以获取一组受支持的信道。

```c
iwx_init_channel_map(ic, IEEE80211_CHAN_MAX, &ic->ic_nchans,
        ic->ic_channels);

ieee80211_ifattach(ic);
ic->ic_vap_create = iwx_vap_create;
ic->ic_vap_delete = iwx_vap_delete;
ic->ic_raw_xmit = iwx_raw_xmit;
ic->ic_node_alloc = iwx_node_alloc;
ic->ic_scan_start = iwx_scan_start;
ic->ic_scan_end = iwx_scan_end;
ic->ic_update_mcast = iwx_update_mcast;
ic->ic_getradiocaps = iwx_init_channel_map;

ic->ic_set_channel = iwx_set_channel;
ic->ic_scan_curchan = iwx_scan_curchan;
ic->ic_scan_mindwell = iwx_scan_mindwell;
ic->ic_wme.wme_update = iwx_wme_update;
ic->ic_parent = iwx_parent;
ic->ic_transmit = iwx_transmit;

sc->sc_ampdu_rx_start = ic->ic_ampdu_rx_start;
ic->ic_ampdu_rx_start = iwx_ampdu_rx_start;
sc->sc_ampdu_rx_stop = ic->ic_ampdu_rx_stop;
ic->ic_ampdu_rx_stop = iwx_ampdu_rx_stop;

sc->sc_addba_request = ic->ic_addba_request;
ic->ic_addba_request = iwx_addba_request;
sc->sc_addba_response = ic->ic_addba_response;
ic->ic_addba_response = iwx_addba_response;

iwx_radiotap_attach(sc);
ieee80211_announce(ic);
```

接着，驱动会替换或拦截 net80211 通过设备的 IC 所发起的调用。`ic_vap_create` 和 `ic_raw_xmit` 由驱动提供实现，而 `sc_ampdu_rx_start` 和 `stop` 等调用则是被拦截的。

最后，驱动会附加到 **radiotap 子系统**，这使得原始数据包能够传递给 BPF，然后驱动会向 net80211 系统声明自身的存在。

在 attach 方法中的两个 `ieee80211_` 调用就是我们与 net80211 系统交互的例子。第一个调用会把我们的驱动附加到 net80211 子系统（这一步会让驱动被加入到 `net.wlan.devices` sysctl 后面的列表中）。这样一来，驱动就能被 `ifconfig` 使用。

第二个调用（`ieee80211_announce`）负责声明设备已被创建；此时会打印出该网卡的信道与特性支持情况。

待驱动附加到 net80211 子系统，它就会处于空闲状态，直到外部事件触发它进入运行状态。运行的下一部分由 net80211 处理，它会调用我们在 `attach_hook` 回调中覆盖的方法。

## 实现 station 模式

在第一篇文章中，我们为示例创建了 station 模式的 VAP。我们运行的命令是：

```sh
ifconfig wlan create wlandev iwx0
```

参数 `wlan` 让系统为我们分配一个设备号，而 `iwx0` 则告诉 net80211 子系统使用名为 `iwx0` 的设备来创建这个 VAP。

该命令会通过 `ifconfig` 的库转换为一次 `net80211_ioctl` 调用。最终结果是 net80211 会在我们的驱动 `ic` 上调用 `ic->ic_vap_create` 回调。正如前文所述，这个回调映射到 `iwx_vap_create`。

```c
struct ieee80211vap *
iwx_vap_create(struct ieee80211com *ic, const char name[IFNAMSIZ], int unit,
    enum ieee80211_opmode opmode, int flags,
    const uint8_t bssid[IEEE80211_ADDR_LEN],
    const uint8_t mac[IEEE80211_ADDR_LEN])
{
        struct iwx_vap *ivp;
        struct ieee80211vap *vap;

        if (!TAILQ_EMPTY(&ic->ic_vaps))         /* 一次只允许一个 */
                return NULL;

        ivp = malloc(sizeof(struct iwx_vap), M_80211_VAP, M_WAITOK | M_ZERO);
        vap = &ivp->iv_vap;

        ieee80211_vap_setup(ic, vap, name, unit, opmode, flags, bssid);
        vap->iv_bmissthreshold = 10;            /* 覆盖默认值 */

        /* 用驱动方法覆盖默认方法 */
        ivp->iv_newstate = vap->iv_newstate;
        vap->iv_newstate = iwx_newstate;

        ivp->id = IWX_DEFAULT_MACID;
        ivp->color = IWX_DEFAULT_COLOR;

        ivp->have_wme = TRUE;
        ivp->ps_disabled = FALSE;

        vap->iv_ampdu_rxmax = IEEE80211_HTCAP_MAXRXAMPDU_64K;
        vap->iv_ampdu_density = IEEE80211_HTCAP_MPDUDENSITY_4;

        /* 硬件加密支持 */
        vap->iv_key_alloc = iwx_key_alloc;
        vap->iv_key_delete = iwx_key_delete;
        vap->iv_key_set = iwx_key_set;
        vap->iv_key_update_begin = iwx_key_update_begin;
        vap->iv_key_update_end = iwx_key_update_end;

        ieee80211_ratectl_init(vap);

        /* 完成设置 */
        ieee80211_vap_attach(vap, ieee80211_media_change,
            ieee80211_media_status, mac);

        ic->ic_opmode = opmode;

        return vap;
}
```

`iwx_vap_create` 会做一些内务处理来管理内存，并建立 net80211 系统需要使用的回调。对于 `iwx`，它会建立特定于驱动的状态（`IWX_DEFAULT_MACID` 和 `IWX_DEFAULT_COLOR` 值），用于与固件协调，确定默认使用哪个 station。

对于 `iwx_vap_create` 所挂接的一些函数，我们保留默认方法，但会拦截对它的调用。例如，我们覆盖了 `iv_newstate` 回调，并通过 `iwx_newstate` 过滤。

`iwx` 的固件自身管理了大量状态；例如在 **探测 (probe)** 时，可以请求硬件在受支持的信道上发送探测报文，而我们无法直接自己构造并发送这些报文。

因此，iwx 驱动必须挂接 `newstate` 方法，以便向固件发出请求并更新其状态机。通过这种方式，**net80211** 与固件的状态机能够保持与主机层面变化同步。

## 发送数据包

到目前为止，我们已经覆盖了足够的驱动部分，可以用 `ifconfig` 启动接口，并让操作系统开始发送数据包。

在测试接口时，我们可能会使用 `ifconfig` 按如下流程操作：

```sh
# ifconfig wlan0 ssid open-network up
```

这些命令指示 `ifconfig` 启用该接口，并请求 **net80211** 堆栈加入开放 Wi-Fi 网络 `open-network`。它还为接口设置了地址，但这并不会在物理链路上产生任何数据包（准确地说，是在空中）。

接下来看看这一系列命令会映射到哪些驱动方法。

在我们的 attach 钩子中，为 **net80211** 层设置了两个用于发送数据包的回调：`ic_transmit` 和 `ic_raw_transmit`，和一个用于控制接口状态的回调：`ic_parent`。

```c
ic->ic_raw_xmit = iwx_raw_xmit;
...
ic->ic_parent = iwx_parent;
ic->ic_transmit = iwx_transmit;
```

`ifconfig` 命令中的 `up` 部分最终会调用 `ic_parent` 回调。对于 iwx，这个回调是 `iwx_parent`：

```c
static void
iwx_parent(struct ieee80211com *ic)
{
        struct iwx_softc *sc = ic->ic_softc;
        IWX_LOCK(sc);

        if (sc->sc_flags & IWX_FLAG_HW_INITED) {
                iwx_stop(sc);
                sc->sc_flags &= ~IWX_FLAG_HW_INITED;
        } else {
                iwx_init(sc);
                ieee80211_start_all(ic);
        }
        IWX_UNLOCK(sc);
}
```

`iwx_parent` 会直接控制硬件：如果正在运行，就调用 `iwx_stop` 来清除所有硬件状态；如果尚未运行，就调用 `iwx_init` 让硬件完成初始配置。只要硬件准备就绪，我们就调用 `ieee80211_start_all` 通知 net80211 栈可以开始工作了。

看似简单的 `ifconfig up` 动作，实际上会导致 `iwx` 驱动修改大量硬件状态。这也是为什么“把接口关掉再打开”常常被当作解决网络问题的“魔法修复”的原因。

`ifconfig` 命令的第二部分让 net80211 栈执行更多操作。通过向 `ifconfig` 传入 `ssid open-network`，我们请求 net80211 子系统去发现并加入名为 `open-network` 的网络。

加入 IEEE 802.11 网络的过程分为几个步骤：

* **探测 (probe)** 网络
* **认证 (authenticate)** 到网络
* **关联 (associate)** 到网络

每一步都需要设备发送管理帧。首先要发现目标网络 —— 网络会定期通过 beacon 报文广播自己的存在（这就是你菜单栏里能看到的 Wi-Fi 列表）。操作系统据此获得候选网络列表。当设备要加入某个网络时，它会向目标网络发送探测请求 (probe request)，并等待探测响应 (probe response)。这一过程在主机和网络之间传递配置信息，确认该网络确实可用。

接下来是认证和关联。在这一步完成后，接口进入 **RUN** 状态，就可以像普通网络接口一样开始使用了。

随着协议栈在各个状态之间切换，它会触发对 `iv_newstate` 函数的调用。在 `iwx` 驱动中，这个调用会首先经过 `iwx_newstate` 拦截，从而允许驱动控制状态转换时的报文发送。这是必要的，因为在 `iwx` 中，某些状态转换是由固件处理的，而不是通过 net80211 栈直接发包完成的。

例如，探测请求并不是直接发出的，而是通过固件接口触发一次信道扫描。一旦发现网络并决定加入，就向固件发送一条“添加站点”的消息，而不是从 net80211 栈发出关联报文。

并非所有管理帧都通过固件抽象发送，在这些情况下，系统会调用 `iwx_raw_xmit` 回调。如果你在调试驱动时发现发送路径并不总是被触发，那可能是因为管理帧是通过原始路径 (raw path) 发出的。

## 总结

本文介绍了驱动如何探测、附加和发送首批数据包。通过使用现有驱动，我们可以很快覆盖驱动的大量逻辑。不过，如果你查看源码，就会发现 `if_iwx.c` 长达一万多行，这远远超出了本文的范围。

这篇文章作为 Wi-Fi 驱动入门，省略了许多细节。要真正加入网络，我们必须能够从接口既发送又接收数据包。

如果收不到任何数据包，我们能如何调试？系统提供了哪些工具？

在本系列的第三部分，我们将介绍 **net80211 栈的内置调试功能**，和它们是如何与驱动结合，用于开发、测试和故障排查的。

***

**Tom Jones** 是一名 FreeBSD 提交者，关注于保持网络栈的高速性能。


# 嵌入式 FreeBSD：回顾与展望

* 原文：[Embedded FreeBSD: Looking Back and Forward](https://freebsdfoundation.org/our-work/journal/browser-based-edition/embedded-2/embedded-freebsd-looking-back-and-forward/)
* 作者：Christopher R. Bowman

在过去一年中，我们已经探索了相当多的内容。虽然我猜大多数人运行 FreeBSD 还是在传统的 AMD64 架构 PC 上，但我们研究了一块可运行 FreeBSD 的嵌入式开发板：[Digilent Arty Z7-20](https://digilent.com/shop/arty-z7-zynq-7000-soc-development-board/)。虽然价格不算低，但 Arty Z7 提供了与 CPU 相连的 FPGA 结构，这使它区别于价格更低的树莓派或 Beagle Boards。

我们首先讨论了如何获取该开发板的预构建镜像，和如何通过串口与其通信。在接下来的文章中，我们研究了如何自己构建镜像，并利用 FreeBSD 的交叉编译基础设施为 Arty 板上的 ARMv7 系统编译。这大大加快了开发速度。我们还讨论了如何定制 FreeBSD 的构建，并将其写入 SD 卡，从而生成我们自己的定制镜像。

在学会构建和定制镜像后，我们学习了如何设置 bhyve 实例来运行 AMD/Xilinx 的 FPGA 软件，以便实验 FPGA 结构电路。

当我们有了 Linux 实例后，就研究了构建电路并将其加载到 FPGA 结构中的基本流程。这里涉及了很多细节：我们必须用一种全新的硬件设计语言 **Verilog** 来创建电路；还要学习如何使用 AMD/Xilinx 工具将电路连接到芯片上的引脚，再通过这些引脚驱动开发板上的 LED。我们使用了一个代码仓库，把这些工作结合起来，最终能在 Linux bhyve 实例中构建电路。最后，我们学会了两种方式将电路加载到芯片中：一种是在系统启动前加载，另一种是在 FreeBSD 内加载。最终，我们看到了闪烁的 LED，就像圣诞树上的灯光。

在让第一个电路成功运行后，我们开始探索了更复杂的硬件，让 CPU 和 FPGA 结构电路之间通信。为此，我们使用了 FreeBSD 的 GPIO 系统。但一开始在镜像构建中发现 GPIO 不工作。我们调查了 GPIO 驱动的探测，发现系统缺少该驱动的原因是硬件在 **设备树二进制文件 (DTB)** 中没有描述。这引出了对 **扁平设备树 (FDT)** 文件的简短讨论，以及它如何描述许多嵌入式板子的硬件。我们学习了如何修改 FDT 文件，并用 **DTC (设备树编译器)** 编译生成 DTB，再让 FreeBSD loader 在内核启动前加载定制 DTB。完成这些步骤后，我们终于能在用户态调用 GPIO 系统来切换外部引脚，再次点亮 LED。

在最近的一篇文章中，事情变得更有趣。我们使用了一个 PMOD 模块：双七段数码管显示器。我们在 FPGA 结构中构建了电路，让它能在两个显示器上显示数值，并通过 AXI 总线向 CPU 提供寄存器接口。我们在 FDT 中添加了条目，描述硬件的寄存器接口，并编写了驱动来控制七段数码管的显示。最后，我们使用 Unix 的 **sysctl 框架** 作为用户态 API 来设置数码管的数值。

至此，我们已经能用 Verilog 设计电路并放到 Zynq 芯片的 FPGA 结构中；我们能构建通过 AXI 总线通信的寄存器接口，让 CPU 能方便地和定制硬件交互；我们能在内核中描述这些硬件，并编写驱动让 FreeBSD 系统与之交互；我们还能从用户空间与这些硬件交互。接下来会怎样呢？

在有了基本的电路构建与 FPGA 部署能力后，我们开始探索如何在 Zynq 芯片的硬件与 CPU 子系统之间通信。这开启了广阔的探索与实现空间，但也存在限制。其中一个限制是 **带宽和并发性**。虽然寄存器接口功能强大且灵活，但带宽有限。CPU 写寄存器的速度有限，尤其当它还要执行其他任务时。目前，我们的硬件带宽受限。对于七段显示器这样的应用很好用，但如果需要带宽密集型任务，它就不够了。

举例来说，视频显示。我们的 Arty 板有 HDMI 输出接口。寄存器接口或许能支持字符显示，但对位图图形就不够用了。一台 24 位色深、1280x720\@60Hz 的显示器需要大约 **166 MB/s** 的数据吞吐。我们显然不可能通过寄存器接口来实现。传统的做法是分配一块内存，CPU 写入数据，显示硬件读取数据。我们需要探索如何构建硬件来直接访问主存，而不依赖 CPU 搬运数据。寄存器接口知识仍然有用，因为 CPU 需要配置参数（比如基地址），但理想情况是硬件能每秒 60 次自动抓取显示缓冲，而无需 CPU 干预。

让 CPU 能描述硬件应读写的内存对象，开启了 Zynq 系统设计的新可能性。这也让我好奇，这种带宽竞争会对双核 Arm Cortex A9 系统产生什么影响。Digilent 还生产另一款基于 Zynq 的板子，和 Arty Z7-20 类似：[Digilent Zybo Z7](https://digilent.com/shop/zybo-z7-zynq-7000-arm-fpga-soc-development-board/)。它比 Arty Z7 更贵（双核版 399 美元，而 Arty 是 249 美元），但 Zybo 的内存总线宽度是 Arty 的两倍，频率几乎相同。此外，Zybo 提供 **6 个 PMOD 接口**，而 Arty 只有两个。不过，Zybo 没有 Arduino 外壳引脚。我更感兴趣 PMOD 接口。除此之外，两者基于相同的芯片，不需要新驱动，FDT 基本不变。研究它需要的改动会很有意思。

另外，还可以研究新的 PMOD 模块。在 [Digilent 网站](https://digilent.com/shop/products/fpga-boards/expansion-modules/pmods/?CommunicationProtocol=GPIO) 上可以找到很多。我们之前用过 PMOD SSD：七段数码管。Digilent 已经下架了 [PMOD GPS](https://digilent.com/reference/pmod/pmodgps/start?srsltid=AfmBOorJwOUISt4P6L25EAjCmwiWvRD4LpNuHDajzb_vP1fvAtEdZxD0)，但我在下架前买了一块。它使用 UART 接口，而 UART 恰好是 Zynq 芯片上的片上外设，可以通过 FPGA 连接到外部引脚。这应该很容易连接。我猜有开源软件能通过 UART 与它通信，实现定位和授时等 GPS 功能。更有趣的是它提供 **每秒脉冲 (PPS)** 输出。我知道 Poul-Henning Kamp 过去做过一些基于 FPGA 的计时研究，我想看看这是否能应用在这里。

目前我们还没做过中断实验。但让 FPGA 电路生成中断传递给处理器并不难，同时保持一个寄存器来记录 PPS 与当前时间之间的时钟周期数。当中断服务软件运行时，可以读取该寄存器，补偿中断与驱动运行之间的延迟。这可能对 NTP 软件有用。我不敢确定，但这是我感兴趣的探索。甚至有可能实现一个本地 GPS 同步的 Stratum 1 时间服务器。

我手头还有各种 PMOD 模块，比如加速度计、OLED 显示器和 LCD。将它们接入也很有趣。例如，如果你在板子上运行一个 NTP 服务器（也许利用上面描述的硬件提高精度），你可以用 LCD 持续显示原子时间和位置。

差点忘了，Zynq 板子还内置了 **模数转换器 (ADC)**。这无疑是另一个有趣的探索方向，但可能需要一些外部模拟电路（如信号调理或缓冲）才能传给 FPGA，这可能超出 FreeBSD 期刊文章的范围。不过，研究如何访问这些片上外设也是很有意义的。

当然，你也可以自己构建硬件，并很容易通过 FPGA 引脚连接。我很好奇，如果你能自己设计，你会做什么？

还有一个我原本打算研究但没提到的方向：在 FreeBSD 上运行 Vivado。如果你看过我的一些代码仓库，可能会注意到 FreeBSD 下运行 Vivado 有一些实验性支持。不过看来有人已经先我一步了：Michał Kruszewski 写了一篇详细的 [博文](https://m-kru.github.io/posts/freebsd-vivado-chroot/freebsd-vivado-chroot.html)。对我来说，这已经够用了，我能在 FreeBSD 上构建和模拟电路。但还不行的是直接从 FreeBSD 主机加载比特流，和使用 Vivado Logic Analyzer。这两点在我的 bhyve Linux 实例里也不行，不过也许等 FreeBSD 15.0 发布时，我会尝试直通实验。

希望你觉得这些专栏有用。我很欢迎你的意见或反馈。你可以通过 <articles@ChrisBowman.com> 联系我。

***

**Christopher R. Bowman** 最早在 1989 年使用 BSD，当时他在约翰·霍普金斯大学应用物理实验室地下二层的 VAX 11/785 上工作。后来在 90 年代中期，他在马里兰大学用 FreeBSD 设计了他的第一款 2 微米 CMOS 芯片。从那时起，他就一直是 FreeBSD 用户，对硬件设计和驱动它的软件都有着浓厚兴趣。他在半导体设计自动化行业已有 20 年经验。


# 会议报告：2025 BSDCan

* 原文：[Conference Report: BSDCan 2025](https://freebsdfoundation.org/our-work/journal/browser-based-edition/embedded-2/conference-report-bsdcan-2025/)
* 作者：Christos Margiolis

今年，我在 [BSDCan 2025](https://www.bsdcan.org/2025/) 上做了一场题为 [《Vox FreeBSD: How sound(4) works》](https://www.bsdcan.org/2025/timetable/timetable-Vox-FreeBSD-How.html) 的演讲。

我于 6 月 9 日抵达蒙特利尔，比会议早两天，不过当天没做什么，主要是休息。

![蒙特利尔街景](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-04bcd204a39d4940e12e9b87915bef93e7ee0ae8%2FBSD-01.png?alt=media)

第二天 6 月 10 日，我乘巴士从蒙特利尔前往渥太华（会议举办地）。一下车就偶遇了 Olivier Certner（olce@），我们一起在渥太华大学附近餐厅吃了午饭——会议和演讲者住宿都在这里——之后办理入住。晚上，我和 Mateusz Piotrowski（0mp@）、Bojan Novković（bnovkov@）、Kyle Evans（kevans@）在 Father & Sons 餐厅喝酒聚餐。在这个常见的聚会点，我们还遇到了会议的其他人。

会议的前两天（6 月 11–12 日）是 [FreeBSD DevSummit](https://wiki.freebsd.org/DevSummit/202506)。

第一天的亮点是核心团队发起的关于 FreeBSD 项目中 **AI 使用** 的公开讨论，休息时间也在继续。核心团队主要关注 AI 生成代码的许可问题。而我坚持认为应该 **坚决反对 AI 的任何使用**，主要基于伦理和质量方面的担忧。许可问题在我看来只是次要的。比如：我们是否要主动参与可能让世界变得更糟的事？如果放宽 AI 政策，会吸引什么样的人加入项目？这会怎样影响项目的长期质量？我们真的 **需要** AI 及其带来的复杂性吗？如果没有许可问题，那用 AI 就没问题吗？还有很多类似的疑问。好在不少人（有些人犹豫着）支持了我的观点。

我还与 Mark Johnston（markj@）、Joseph Mingrone（jrm@）、Bojan 和 Charlie Li（vishwin@）展开了技术和非技术讨论。意外的是，Charlie 也对 FreeBSD 音频/音乐制作感兴趣，还用它做 DJ 表演。

DevSummit 第一天结束后，我们在大学宿舍里吃了披萨，但我比较早回房间继续准备幻灯片。

第二天 6 月 12 日的日程以 AlphaOmega 的安全审计演讲开始，接着是关于 [FreeBSD 15.0 技术规划](https://hackmd.io/@jhb/ByWrxQmr2) 的讨论，包括 PkgBase。午饭后，Brooks Davis（brooks@）带来了一场极其精彩的演讲——大概是整个 DevSummit 我最喜欢的技术讲座——主题是 [CheriBSD](https://www.cheribsd.org/) 分支的上游合并。他不仅讲了上游过程，还详细介绍了 CHERI、CheriBSD 及能力机制，并展示了其内建的内存安全特性能如何捕捉 FreeBSD 代码中的各种 bug。随后，[FreeBSD 基金会](https://freebsdfoundation.org/) 汇报了近期工作和资金使用情况，包括我参与的 [Laptop Support and Usability Project](https://github.com/FreeBSDFoundation/proj-laptop)。在会议尾声，我和 Alexander Ziaee（ziaee@）花了一些时间，他一直在努力改进文档，我们尝试调试他笔记本上的声音问题。晚上，我与 Benedict Reuschling（bcr@）等人去了一家不错的海鲜餐厅。

![渥太华街道场景](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4f4fa11e81feb8a6d35893c46baffd8670fceccd%2FBSD-02.png?alt=media)

BSDCan 正式会议于 6 月 13–14 日举行。开幕主题演讲由著名计算机科学家 Margot Seltzer 主讲，题为 [《Hardware Support for Memory-Hungry Applications》](https://www.bsdcan.org/2025/timetable/timetable-Keynote-Hardware-Support.html)。

这两天我听了许多讲座，其中印象深刻的有：

* [ShengYi Hung：《ABI stability in FreeBSD》](https://www.bsdcan.org/2025/timetable/timetable-ABI-stability-in.html)，展示了检测 CTF 数据差异的实验性工具，用于发现 ABI 变化。之后我们（包括我、Mark Johnston、John Baldwin、Warner Losh 和讲者本人）展开了关于工具局限性及如何在实际应用中的讨论。
* [Marshall Kirk McKusick：《A History of the BSD Daemon》](https://www.bsdcan.org/2025/timetable/timetable-A-History-of.html)，和即将出版的《FreeBSD 操作系统设计与实现》（*The Design and Implementation of the FreeBSD Operating System*）第三版更新。我一向喜欢 Kirk 的演讲风格。
* [Bojan Novković：《Hardware-accelerated program tracing on FreeBSD》](https://www.bsdcan.org/2025/timetable/timetable-Hardware-accelerated-program-tracing.html)，介绍了他在 **hwt(8)** 框架上的最新工作。
* [Zhuo Ying Jiang Li：《Improvements to FreeBSD KASAN》](https://www.bsdcan.org/2025/timetable/timetable-Improvements-to-FreeBSD.html)，讲解了 FreeBSD 内核地址消毒器 (KASAN) 的改进工作，这是她参与 CheriBSD 的一部分。

我没能参加但本想听的讲座有：

* [John Baldwin：《ELF Nightmares: GOTs, PLTs, and Relocations Oh My》](https://www.bsdcan.org/2025/timetable/timetable-ELF-Nightmares-GOTs,.html)
* [Andrew Hewus Fresh：《The state of 3D-printing from OpenBSD》](https://www.bsdcan.org/2025/timetable/timetable-The-state-of.html)
* [Hans-Jörg Höxer：《Confidential Computing with OpenBSD — The Next Step》](https://www.bsdcan.org/2025/timetable/timetable-Confidential-Computing-with.html)
* [Andreas Kirchner, Benedict Reuschling：《Enhancing Unix Education through Chaos Engineering and Gamification using FreeBSD》](https://www.bsdcan.org/2025/timetable/timetable-Enhancing-Unix-Education.html)

我在 6 月 14 日，也就是 BSDCan 的最后一天做了演讲。引发了大量问题和交流，甚至在讲座结束后仍在继续。显然，比我预想的更多人希望能在 FreeBSD 上做音乐和音频制作，或用于大型音频系统，这场演讲似乎启发了他们，这是非常好的事情。

会议闭幕式后，我们去附近的市场广场参加社交活动。我大部分时间和 Mark Johnston、Andreas Kirchner、Mateusz Piotrowski 在一起，聊了很多有趣的话题。

第二天，我返回蒙特利尔，休息了几天才回家。

一如既往，会议是弥补编程孤独的绝佳机会，让我们能见到每天通过邮件交流的幕后人。除了完成工作和交换技术想法，我更享受那些意外发生的、深入的讨论，包括与此前未曾见过的人。

***

**Christos Margiolis** 是来自希腊的独立开发者和 FreeBSD src 提交者。


# 读者来信

* 原文：[We Get Letters](https://freebsdfoundation.org/our-work/journal/browser-based-edition/embedded-2/we-get-letters-5/)
* 作者：**Michael W Lucas**

![邮箱插图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-12746a0308c9748fcf51d490759b60144062dd04%2Fwe-get-letters-01.png?alt=media)

来信专栏作家，你好：

在你把这封信像那些 IRS 通知一样扔进 **/dev/null** 之前，请注意——如果我时间掐得准——马上就会有一位送货司机敲你的门，送上一加仑手工意式冰淇淋，全都为了从你那里换到一个有用的回答。

普通系统管理员对嵌入式系统需要了解什么？

——多半预期被嘲笑，遗憾

亲爱的 BAD：

英勇的尝试。不过，你的邮件被 greylisting 拦了一下，所以邮件在送货卡车之后才到。我不知道你在哪儿找到腌三文鱼和榴莲口味的意式冰淇淋，但我钦佩你为达到新的恶俗高度所付出的努力。干得漂亮。你天生就是当系统管理员的料。

系统管理员对嵌入式系统需要了解什么？嵌入式系统就是计算机。嵌入式系统和普通主机有什么不同？毫无不同，只是你必须把每件事都做对。

普通主机可以调优。当 Web 服务器上的 **/var/log/** 占用过多磁盘空间时，你可以调整日志轮转条件，或微调日志记录内容。即便是在那些僵化企业里——其严苛的变更控制流程由一位专制走卒执行，此人发现自己在出生时机上晚了点，未能给 Gaius Julius Caesar Germanicus 当无薪实习生而心灵受创——系统调优也比你改动真正的嵌入式系统要宽松。改动嵌入式系统需要系统更新，而这些设备多久重新刷写一次？没错，**从来不**。也许你会让流媒体设备在下载新固件后重启，可你上次更新冰箱是什么时候？我处理这类问题的方法是走进家电卖场，说：“卖我什么都行，只要它完全不具备互联网连接。”他们会立刻把我领到“20 世纪陈列室”。我买回一台抵抗僵尸网络的洗碗机。贵，但值得。如果你连这个问题都要问，那你的选择已经错了。

配置不够完美的嵌入式系统会停止工作。也许是“停止正确工作”，也许重启一下又能用，也许彻底变砖。无所谓。当然可以修。许多嵌入式系统按“换件维修”设计。少数有终端可以查看磁盘是否满或进程是否卡死，但都被藏起来。硬盘都有固件，许多厂商提供隐秘技巧访问终端。从 DB9 串口接几根线到硬盘的指定针脚，敲入密码，砰——你就直接连入硬盘那可怜的小脑袋了。居然还有菜单系统。剔除那些坑蒙拐骗之后，菜单主要还是谎言，但确实存在。

一切都是嵌入式系统。一路向下都是。

你的 RAID 控制器？它运行一个嵌入式操作系统，让它能模拟一个完全不同的 1980 年代 RAID 控制器，因为系统管理员坚持要能设置一个“条带大小”，而这个大小与实际存储介质中的任何东西都对不上。开机时按 F1 或 DELETE，你就进入了主板的嵌入式系统。顺便说一句，“BIOS Log”是满的。满了好多年了。自从你的前任入职以来，没有记录过任何有用的东西。你计算机里的一切都是独立的嵌入式系统，支撑着你的非嵌入式系统。你的“裸金属”服务器并不是裸金属，因为“裸金属”根本不存在。

我抱怨过虚拟化许多次，但一切都是虚拟的，整个世界都值得同样的鄙夷。

为什么要把操作系统塞进硬盘、主板或 USB 芯片组里？硬件改动昂贵，需要和厂商打交道。软件改动便宜，可以塞进既有硬件。我们用另一种有 bug 的软件绕过硬件 bug。没错，发布正确的硬件会更好，但掌权者告诉我们那不可能，于是现实又一次证明：看似好主意的东西其实是可怕的主意。嵌入式系统为厂商省钱，却增加了痛苦。

别那样看我。所有计算机都增加痛苦。

系统的目的就是它所做的事，而计算机做的就是制造痛苦。

我们用更多计算机来应对痛苦。

系统管理的第一条法则是“计算机是个错误”，而你计算机里那摇摇欲坠的灾难性系统堆栈对心理健康的影响就证明了这一点——更别说你车里的那些系统了。别提你那辆车。要求美国每个人借、买或以其他方式“获取”一件多吨动能武器本来就够糟糕了，再给刹车、油门、空调分别配上独立计算机，恭喜！你造出了一辆“滚动灾难”。当你的线控转向捕获到 `SIGABRT` 时，唯一的问题是这场灾难轮到谁——你还是路人。我本想说“无辜的路人”，但如今人人都在用计算机，所以谁都不无辜。即便那些想保持洁净的人也被迫使用网站或 app，因为有罪之人通过把所有人都拖下水来让自己心里好过些。（“来我的派对吧！会很棒的！别管水里漂着什么！”）

你还能找到一台没有任何多余嵌入式系统的计算机吗？当然。那叫算盘。我的算盘有一串五位字节，外加一个两位字节的独立寄存器。相当精巧。操作系统由手部位置实现。最棒的是，没人给算盘写过 TCP/IP 协议栈。

坏消息是，嵌入式系统比以往任何时候都更容易接触。你最不讨厌的那家零售商就能买到廉价低功耗系统，让你想搭什么小系统来控制整个家都行。如果你想给存放多吨动能武器的大库房门做一个定制遥控器，只需花上百来块硬件、几个人月的工作量，再加上你的求生欲。

那么，你对嵌入式系统应该了解什么？如果你摊上一台嵌入式系统，它坏了，就装傻，打电话给技术支持。否则，你会钻进系统里，发现设计师为了所谓的系统稳定犯下了哪些暴行。那不会通向任何美好的地方。

有问题想问 Michael？ 发送至 <letters@freebsdjournal.org>

**Michael W Lucas** 是《Absolute FreeBSD》《Dear Abyss》《SSH Mastery》等书的作者。《Networking for System Administrators》的新版刚刚逃出牢笼。<https://mwl.io>


# 基金会来信

欢迎阅读本期《FreeBSD 期刊》！本期的主题是网络——不只是技术本身，还包括那些让人与系统之间更便捷、更安全地互联的工具和改进。

在本期中，你将了解到 Overlord——一种在编写代码时快速灵活地部署 Jail 的方式。我们还会深入探讨安装程序的易用性，包括与其他操作系统安装程序的对比、经验教训，和 FreeBSD 自身安装体验的改进进展。如果你对 FreeBSD 的无线网络支持感兴趣，不要错过《WiFi 开发入门》。此外，还有你期待已久的常规栏目和专栏。

《期刊》能够出版，离不开像你这样的捐赠者的慷慨支持。如果你从这些文章中——或从 FreeBSD 项目整体中——获得了价值，请考虑向 FreeBSD 基金会 [捐赠](https://freebsdfoundation.org/donate/)。你的支持帮助我们资助开发、改进工具与基础设施，并壮大社区。

感谢阅读，祝阅读愉快！

**Deb Goodkin** FreeBSD 基金会执行董事


# 活动日历

* 作者：**Anne Dickison**

截至 2025 年 11 月举办的 BSD 活动。如有任何 FreeBSD 相关活动或 FreeBSD 用户感兴趣的活动未在此列出，请将详情发送至 <freebsd-doc@FreeBSD.org>。

![EuroBSD](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-3fbd5619d46af67f71b7e55ab707c8e23717a263%2Fevents-calendar-01.png?alt=media)

## EuroBSDCon 2025

2025 年 9 月 25-28 日

克罗地亚萨格勒布

<https://2025.eurobsdcon.org/>

这一年度大会提供了绝佳机会，让你了解 BSD 领域的最新动态，见证当代部署案例，并亲自结识其他使用 BSD 技术的用户与企业。EuroBSDCon 也是创意、讨论与信息交流的温床，这些交流常常会催生编程项目。

![OpenZFS](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-b83b6685a33d62363708da45b5ec2de23fa4d62b%2Fevents-calendar-02.png?alt=media)

## OpenZFS 用户与开发者峰会 2025

2025 年 10 月 25-28 日

美国俄勒冈州波特兰

<https://openzfs.org/wiki/OpenZFS_Developer_Summit_2025>

今年的用户峰会将探讨广泛的主题，旨在支持并连接 OpenZFS 社区。

![FreeBSD logo](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4a5e471be837bddefae134b52cd5e00ceecc7d8f%2Fevents-calendar-03.png?alt=media)

## 2025 年 11 月 FreeBSD 厂商峰会

2025 年 11 月 6-7 日

美国加利福尼亚州圣何塞

该峰会为商业 FreeBSD 用户提供了独特的机会，让他们能与开发者和贡献者面对面交流，推动所需功能的实现、问题的解决和需求的满足。峰会也为改进和增强操作系统开启了讨论。


# 采访 Kajetan Staszkiewicz

* [Starting FreeBSD Development An Interview with Kajetan Staszkiewicz](https://freebsdfoundation.org/our-work/journal/browser-based-edition/networking-3/starting-freebsd-development/)
* 作者：Tom Jones

![采访](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-d16ed5aee9137acda3e291344d13c8938694b50c%2FStaszkiewicz-01.png?alt=media)

**TJ：** 你能简单介绍一下你自己和你与 FreeBSD 的背景吗？

**KS：** 我叫 Kajetan Staszkiewicz。1984 年出生于波兰神奇的城市克拉科夫。大约在 8 岁，我在计算机俱乐部接触了 ZX Spectrum 和 Atari 65XE 等 8 位机器。后来父母给我买了一台 Commodore 64。我对这台机器记忆犹新，直到现在我的桌上还摆着一台能正常工作的机器。

1996 年，我们家买了第一台 PC 克隆机（注：IBM PC 兼容机），不久波兰家用用户就能通过拨号连入互联网。但这种方式可持续时间不长。大约 1999 年，我和邻居们决定搭建邻里网络（注：小区或街道搭建的局域网）。大约 15 个人通过悬挂在屋顶到阳台的 10 Mb/s 以太网线共享一家网吧的 115 kb/s 连接。

网络共享由一台运行 IPchains（注：一款网络数据包过滤与地址转换工具）的 Linux 机器来完成，最初由网吧的系统管理员提供，很快就被我们网络里年长的朋友搭建的另一套系统取代。后来我接手管理，用 Perl 写了几段 CGI 脚本，通过 Web 界面管理 DHCP 服务器和流量整形。

凭借这些经验，我在一家公司找到了兼职，网络架构和我们当初的类似，不过是商业用途。那时我在雅盖隆大学（注：位于波兰克拉科夫，由卡齐米日三世创建于 1364 年）学习计算机科学。一位同学向我介绍了 FreeBSD，大概是 5.0 版。在新工作中，路由和流量整形由 Linux 系统完成，但 FreeBSD 的 Jail 看起来是托管平台的完美方案。我们需要提供网页、电子邮件和 Webmail 等服务。虚拟化尚未普及，Jail 帮助我们实现了这些服务的隔离。

我的下一份工作是在一家大多数服务都使用 FreeBSD 的公司，甚至有员工大胆地在工作站上运行 FreeBSD。

目前我居住在德国汉堡。在现在的工作中，FreeBSD 是我们路由器和负载均衡器的首选系统。

**TJ：** 你是如何开始提交第一个 FreeBSD 贡献的？当时你如何选择起步的项目？

**KS：** 选择很简单：修复那些出问题的部分。在 $WORK，我们依赖 **pf(4)** 的源跟踪，期间发现一个棘手的 Bug：删除源节点时，每个源节点会扫描所有状态。数百个源节点与数十万条状态会导致内核代码出现大量循环，系统因此被锁死。

在 glebius@ 的帮助下，我成功修复了该问题，并提交了我的第一个补丁到 FreeBSD。

**TJ：** 你是如何定位这个问题的？FreeBSD 中是否有现成的工具可以帮助，还是你需要自行开发？

**KS：** 当时我已经大致知道问题出在哪，因为它是由我们用于负载均衡的自定义工具引发的。

那时我对 FreeBSD 尤其是内核开发经验尚浅，所以采用了最原始的暴力调试法：不断在代码里加 `printf`，直到找到卡住的位置。

不过系统自带了构建自定义内核所需的所有工具，这点对我帮助很大。

此外，还能按需生成内存转储，再用调试器分析，也非常方便。

**TJ：** 在这个过程中你一定学到了很多。有没有什么技巧能让新开发者的调试和开发更轻松？

**KS：** 不只是知识，额外的工具和工作流也很关键。

第一是要有完善的开发和测试环境。在 $WORK，我们只在裸机上跑 FreeBSD，最初的补丁必须部署到这些“真”服务器上——它们启动要花几分钟 POST，并且承载着真实的生产流量，影响客户体验。

在那种环境下测试补丁又紧张又慢。现在我有三层系统：

1. 可以在几秒内重启新内核的虚拟机；
2. 承载少量流量、即便挂了也不影响客户的测试机；
3. 承载更多流量、但仍不影响付费客户的预演机。

第二是 FreeBSD 的测试套件，真的能救命！我能在几分钟内（支持并行）跑完所有测试，确保没破坏现有功能。虽然偶尔会发现有些功能还没对应的测试，但我们正在逐步完善。

最后是在编译内核时启用各种调试选项——可以监测锁顺序、变量断言，甚至捕获内核中的非法内存访问。FreeBSD 开发者手册对这些选项都有详细说明。

**TJ：** 对新贡献者有什么建议或其他想补充的吗？

**KS：** 开源项目（包括 FreeBSD）依赖贡献者。FreeBSD 已经很成熟，虽然代码复杂，但总有贡献的空间——从小而简单的改动开始也完全没问题。社区友好又乐于助人，会在代码审查中给出建设性反馈，别被吓到！

希望大家的贡献既能带来新功能，也能让新手学习新技能、职业成长，甚至享受与计算机打交道的乐趣。

***

**Tom Jones** 是一位致力于保持高效网络栈的 FreeBSD 提交者。

**Kajetan Staszkiewicz** 是专注于网络和虚拟化的系统管理员，偶尔为 FreeBSD 的 **pf(4)** 贡献代码。


# 使安装程序易于使用

* [Installer Usability](https://freebsdfoundation.org/our-work/journal/browser-based-edition/networking-3/installer-usability/)
* 作者：Pierre Pronchery

亲爱的读者：

在撰写本文时，我完全不知道你是谁。这可能是你首次接触有关 FreeBSD 的文章；你也可能已经对系统非常熟悉了——毕竟你可能亲自编写了其中一半的代码。我也不清楚你正在使用何种硬件来阅读这篇文章；它甚至可能尚未问世，也不知你是否有任何残障或不便。

这正是操作系统安装程序所要应对的挑战。它必须在各种情况下都能正常工作，同时对所有人都要具有吸引力和可用性，甚至需要预见未来的演进。这是一项艰巨的任务。

安装程序往往是潜在用户与系统第一次交互的入口，因此它负责形成第一印象。与此同时，专业用户在个人硬件上不太需要频繁使用安装程序——基本上只在设置新系统时用到——但他们可能需要自动化安装多台系统来执行各种任务，或者在专业场景下为客户部署。

综上所述，这些不同方面可以从一个角度来看：可用性。但什么是可用？更重要的是，与其他系统相比，FreeBSD 安装程序的可用性又如何？

## 现状

### Microsoft Windows 的演进

安装程序历史悠久。我的初次接触要追溯到 1989 年，家里引入计算机的时候。那时通常先安装 MS-DOS，然后在其上安装 Windows 3.1。显然，安装 MS-DOS 是在文本模式（80 列 VGA）下进行的，但可以禁用颜色——这也起到了一种粗糙的无障碍功能：高对比度。

![MS-DOS 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-380b770982cfab2889429c0418617abcb20d3159%2FUsability-01.png?alt=media)

![MS-DOS 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-fcf06787b5c233c4120c093deec884a704776801%2FUsability-02.png?alt=media)

虽然安装过程在文本模式下进行，但进度条和针对用户输入的高亮已经问世。

![MS-DOS 安装进度条界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-7f01107f47e93069bc08db9faf3aedec454656db%2FUsability-03.png?alt=media)

![MS-DOS 安装进度条界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-9e3be47211c51d9709fdf3fdde48ac1d15ce0619%2FUsability-04.png?alt=media)

另一方面，Windows 安装程序（此处为 3.11 版）很快使用了自身的图形界面安装，如下所示。

![Windows 3.11 图形安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-052668b0ff42187743f2b044f4668584dff6411b%2FUsability-05.png?alt=media)

![Windows 3.11 图形安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4bdae94716d0920b36faa76d35ed9b2a69a6be54%2FUsability-06.png?alt=media)

值得一提的是，我在 VirtualBox 中，基于 archive.org 上的公共领域材料，自己制作了这些截图 ¹²³。

接下来是 Windows 95。和 Windows 3.11 一样，它在安装程序中使用了自身的用户界面，不过有两个中间步骤。通过以下截图可以看出，安装过程相对无痛，但需要用户交互的步骤明显增多 ⁴。

![Windows 95 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-fafc3fd4130748f686d928e238d636e6505842f2%2FUsability-07.png?alt=media)

![Windows 95 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-bccbd518c96f2bc957da03577236e300f62afeaa%2FUsability-08.png?alt=media)

![Windows 95 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-d4d4c775fcef84f5f35197674459cf28e867a3da%2FUsability-09.png?alt=media)

![Windows 95 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-7d1aa67a7154698e3bd51a6a59ecfbcc43ebd634%2FUsability-10.png?alt=media)

将 Windows 98 纳入讨论值得一提，因为它带来了一些改进：安装步骤总览列表和剩余时间估算 ⁵。

![Windows 98 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-1ded2a3be8afd0d239333ea55566bf2dd95cf795%2FUsability-11.png?alt=media)

![Windows 98 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-58ca11a4a57f67523104d98d29f0fe65b1d56dd8%2FUsability-12.png?alt=media)

我就不再展示其他版本的 Windows 安装过程了。只要加上 Windows 7，就能总结出以下有关安装程序可用性的模式 ⁶：

![Windows 7 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-8d43821d279c379d446504d1975b57a07f5e2ee3%2FUsability-13.png?alt=media)

![Windows 7 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-af1785b785a200f4353b348b6319fc9700398849%2FUsability-14.png?alt=media)

我们可以无休止地争论 FreeBSD 是否真正与 Microsoft Windows 竞争。从 FreeBSD 项目的官方网站来看，它确实有瞄准桌面系统的雄心，如今已有 Laptop Desktop Working Group（LDWG，笔记本桌面工作组）为证。不过，无论这场争论的结果如何，我认为我们都能从消费级市场中获益，哪怕只是作为参考。以下是在实际体验中浮现出的若干模式和标准：

|    评估标准   | MS-DOS 6.22 | Windows 3.11 | Windows 95 | Windows 98 |     Windows 7     |
| :-------: | :---------: | :----------: | :--------: | :--------: | :---------------: |
|    语言支持   |  为每种语言分别发布  |   为每种语言分别发布  |  为每种语言分别发布 |  为每种语言分别发布 |         支持        |
|   无障碍功能   |   支持（高对比度）  |      不支持     |     不支持    |     不支持    | 支持（放大镜、屏幕键盘、高对比度） |
|    许可协议   |     不支持     |      不支持     |     支持     |     支持     |         支持        |
|   操作步骤数量  |  9（共 3 张软盘） |  15（前 5 张软盘） |     21     |     17     |         10        |
|    支持导航   |     不支持     |      不支持     |     支持     |     支持     |        不支持        |
|    高级模式   |     不支持     |      混合      |     支持     |     支持     |         支持        |
|    步骤清单   |     不支持     |      不支持     |     不支持    |     支持     |         混合        |
|   是否先提问   |     不支持     |      不支持     |     不支持    |     不支持    |        不支持        |
|    时间预估   |     不支持     |      不支持     |     不支持    |     支持     |         支持        |
| Live 系统支持 |     不支持     |      不支持     |     不支持    |     不支持    |        不支持        |
|   系统修复功能  |      混合     |      混合      |     混合     |     混合     |         支持        |
|   文本模式界面  |      支持     |      不支持     |     不支持    |     不支持    |        不支持        |
|   图形模式界面  |     不支持     |      混合      |     混合     |     混合     |         支持        |
|  图形化安装结果  |     不支持     |      支持      |     支持     |     支持     |         支持        |
|   自动化安装   |     不支持     |      不支持     |     不支持    |     不支持    |         支持        |

如果要总结这套安装器的优势，我会指出以下几点：

* 通常会提供整体进度的指示。
* 即便时间预估可能不够精准，它对终端用户来说依然具有参考价值。
* 仅保留最关键的决策项由用户选择（如目标磁盘和安装目录），而将技术性决策默认隐藏：
  * 文件系统的选择，
  * 驱动程序列表，
  * 桌面环境（显而易见），
  * 预装软件列表；或提供一个明确的入口进入高级模式。
* 安装过程中通常可以回退并更改之前的选择；有趣的是，Windows 7 移除了这个选项，但它同时也是安装流程最简单的版本，仅需 10 步。
* 安装完成后的系统可以直接用于桌面环境。

尽管在 Windows 7 发布之后又发布了三个主要版本，但在可用性层面上，我并未观察到显著的变化。

## FreeBSD 的现状

截至撰写本文时，FreeBSD 的最新稳定版本是 14.2。注 7、8

如果单从可用性（usability）的角度来看，FreeBSD 的安装器显得有些落伍了，尤其是在体验了 Windows 7 之后，这一点更加明显。

![FreeBSD 安装器界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-ba6ab740786a94a2b72b31d51353b9dcfa2386fd%2FUsability-15.png?alt=media)

![FreeBSD 安装器界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-e2cee1bbbc4ce456a9fbfafea3a1ad161d1b4833%2FUsability-16.png?alt=media)

|    评估标准   | FreeBSD 14.2 |
| :-------: | :----------: |
|    语言支持   |      不支持     |
|   无障碍功能   |      不支持     |
|    许可协议   |      不支持     |
|   操作步骤数量  |      23      |
|    支持导航   |      不支持     |
|   是否先提问   |      不支持     |
|    高级模式   |      支持      |
|    步骤清单   |      不支持     |
|    时间预估   |      不支持     |
| Live 系统支持 |    混合（命令行）   |
|   系统修复功能  |    混合（命令行）   |
|   文本模式界面  |      支持      |
|   图形模式界面  |      不支持     |
|  图形化安装结果  |      不支持     |
|   自动化安装   |      支持      |

不过，这样的比较并不完全公平：**Microsoft Windows 明确面向非技术用户，追求“一套通吃”的图形化安装体验，并提供了完善的客户支持体系**，而 FreeBSD 更偏向资深用户和系统管理员。

那么，如果将它与更接近的竞品做比较，会是什么样的结果？

## Linux：Ubuntu Server（服务器版）

Ubuntu 会根据用途提供不同的安装镜像。为了保持对比的公平性，本文使用的是 Ubuntu Server 24.04.02 LTS。注 9

![Ubuntu Server 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-10e38281eaa165b6fcf056ae81d7dcb7ff4b0173%2FUsability-17.png?alt=media)

![Ubuntu Server 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-976347b3435ed654fb802863f14802644f24ceb8%2FUsability-18.png?alt=media)

乍一看，这套安装器和 FreeBSD 十分相似：整个安装过程都保持在纯文本界面（text mode）中，首启后也是登录到命令行。这种形式更符合 FreeBSD 用户的预期。不过，**实际操作中 Ubuntu 的安装流程显得更加简单直接**。

|    评估标准   | Ubuntu Server 24.04 |
| :-------: | :-----------------: |
|    语言支持   |          支持         |
|   无障碍功能   |         不支持         |
|    许可协议   |         不支持         |
|   操作步骤数量  |          15         |
|    支持导航   |          支持         |
|   是否先提问   |          支持         |
|    高级模式   |          支持         |
|    步骤清单   |         不支持         |
|    时间预估   |         不支持         |
| Live 系统支持 |       混合（命令行）       |
|   系统修复功能  |       混合（命令行）       |
|   文本模式界面  |          支持         |
|   图形模式界面  |         不支持         |
|  图形化安装结果  |         不支持         |
|   自动化安装   |          支持         |

### 相较于 FreeBSD 的明显优势

* 安装器支持多种语言选择。
* 安装问题集中一次性提问，实际安装过程一气呵成。
* 安装期间可以选择预定义用途（如通过 Snaps 安装服务组件）。

这些细节虽然小，却对用户体验有明显的正向影响。如果 FreeBSD 安装器希望跟上当今的趋势，这些正是可以借鉴的改进方向。

## Linux Ubuntu（桌面版）

接下来，我尝试了 Ubuntu Desktop，版本同样是 24.04.02 LTS。

正如预期，这个版本提供了图形化的用户体验。但更重要的是，在我个人的感受中，其整体的质量与打磨程度处于另一个层次。

![Ubuntu Desktop 图形安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-92404b71eb0903457814877febec1296adeb580c%2FUsability-19.png?alt=media)

![Ubuntu Desktop 图形安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-a5977246488ed7b6def1b50e1095ee3547075521%2FUsability-20.png?alt=media)

|   评估标准  | Ubuntu（桌面版） |
| :-----: | :---------: |
|   语言支持  |      有      |
|   辅助功能  |      有      |
|   许可协议  |      无      |
|  安装步骤数量 |     15 步    |
|    导航   |      有      |
|  先提问再操作 |      有      |
|   高级模式  |      有      |
|   步骤列表  |      无      |
|   时间估算  |      无      |
| Live 系统 |      有      |
|   修复系统  |      混合     |
|   文本模式  |      无      |
|   图形模式  |      有      |
|  图形化结果  |      有      |
|  自动化安装  |      有      |

在可用性方面，这无疑是一个参考标准。至于 Microsoft Windows，这种表现可以说是商业支持下的解决方案应有的水平。但为了更公平地比较 FreeBSD，我们应将其与另一个社区项目对比。因此，在深入探讨 FreeBSD 自身的安装程序之前，不妨先看看 Ubuntu 的母项目、非商业发行版：Debian。

## Debian GNU/Linux

**译者注：Debian 亦有高级安装模式（Expert Mode），原作者测评的是普通安装模式。在高级安装模式下可对每个选项精确规划。**

与 Ubuntu 不同，这个比较是一个“二合一”：Debian 的 netinstall 安装器体积仅有 632 MB，却同时包含了文本模式和图形化模式两种形式。

顺带一提：在我主要转向 BSD 系统之前，我长期使用的类 UNIX 发行版就是 Debian。在惊喜地发现其混合式安装方式之后，我再次遇到了令人沮丧的熟悉体验：安装过程提出一连串看似无止境的问题，而且每一组问题之间还伴随着耗时的处理过程。

![Debian 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-43fb7a62b7633388c8915cf5839d19716e0b8d93%2FUsability-21.png?alt=media)

![Debian 安装界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-194ba3c2fb103ae4bcfc10801417541d7a8b6ff1%2FUsability-22.png?alt=media)

|   评估标准  | Debian GNU/Linux |
| :-----: | :--------------: |
|   语言支持  |         有        |
|   辅助功能  |         无        |
|   许可协议  |         无        |
|  安装步骤数量 |       26 步       |
|    导航   |        混合        |
|  先提问再操作 |         否        |
|   高级模式  |         否        |
|   步骤列表  |         无        |
|   时间估算  |        混合        |
| Live 系统 |      有（其他镜像）     |
|   修复系统  |      混合（命令行）     |
|   文本模式  |         有        |
|   图形模式  |         有        |
|  图形化结果  |         有        |
|   自动化   |         有        |

尽管提供了图形模式，但其视觉外观相当粗糙。我也感到惊讶，除了启动菜单中的高对比度模式外，并未发现其他辅助功能；当然，也有可能是我漏看了。

不过，从非常积极的方面来说，要获得一个适用于服务器或桌面用途的安装环境是非常容易的。这一点与其安装器的混合设计非常契合。

那么，FreeBSD 能否也达成类似的成果？

### 特别提及：macOS

最后但同样重要的一点，我想提及 macOS。虽然 macOS 最初也基于 BSD 系统，但 macOS 能够依赖一套众所周知的自家硬件设备数据库。系统固件内置了多种备用机制，用于以不同方式（重新）安装系统——包括无需任何安装介质：整个系统可以由固件直接从互联网下载并恢复。

这一点令人印象深刻，堪比前文提到的商业解决方案；但同样，这样的比较对我们心爱的 FreeBSD 并不公平。不过，我仍想强调一点：系统中应当至少提供足够的基本工具，以便分析和修复；这在意外情况下可能就是救命稻草。

## 深入探讨 FreeBSD

根据我自己阅读其源码的经验，目前 FreeBSD 安装器的状况，直接源自一些架构层面的设计决策。我想强调的是，这并非对其实现方式或当时决策的全盘否定；重点在于理解我们如今所处的位置：这个安装器是否已到重写的时机，或者是否还有一些战役可以通过简单的手段去赢得？

### 架构

大多数 FreeBSD 安装器由 shell 脚本组成，其中部分步骤通过专门的 C 程序实现。可在 `src.git` 仓库中找到其完整源码，但其底层架构实际上由三大组件构成：

* **bsdconfig**：位于 **usr.sbin/bsdconfig**，作为一套 shell 例程库被 **bsdinstall** 调用使用。
* **bsddialog**：位于 **usr.bin/bsddialog**，是与用户交互的工具，负责收集输入和显示进度。
* 最后是 **bsdinstall** 本身，位于 **usr.sbin/bsdinstall**。

当启动 FreeBSD 安装镜像时，有专门的 `rc.local` 启动脚本（来自 **release/rc.local**）在启动过程结束时运行 **startbsdinstall**。这个脚本提供欢迎界面，并允许将安装介质作为 Live 系统使用。在默认情况下，Live 模式只是启动一个 shell，这对有经验的 UNIX 用户来说是熟悉的环境，但对一般用户而言，并不算是真正意义上的 Live 或修复系统。

聚焦安装过程，其整体流程如下：

* 启动 **startbsdinstall**（如上所述）。
* **bsdinstall** 可直接调用特定操作，其子命令位于 **/usr/libexec/bsdinstall**，默认使用 `auto`（本次选择）。
* `auto` 会依次执行以下操作：

  * 如果存在，运行 **local.pre-everything**。
  * **keymap**：键盘布局选择。
  * **hostname**：设置目标系统的主机名。
  * 选择系统组件，根据 **MANIFEST** 文件（至少应包含 **base.txz** 和 **kernel.txz**）。

  如果安装介质中缺少某些系统组件：

  * 启动 **netconfig**。
  * （如存在）运行 **local.pre-partition**。
  * 根据检测到的平台（如 SMBIOS 或启动架构）应用已知的兼容性修正。
  * 选择分区方案（UFS 自动分区、手动或支持则可选择 ZFS）。
  * 应用分区方案并挂载目标文件系统。
  * （如存在）运行 **local.pre-fetch**。
  * 如果缺少系统组件：
    * 执行 **fetchmissingdists**（将缺失组件保存在目标系统的 **/usr/freebsd-dist** 中）。
  * 使用 **checksum** 与 **distextract** 验证并解压系统组件。
  * 执行 **bootconfig** 配置启动加载器。
  * （如存在）执行 **local.pre-configure**。
  * 执行 **rootpass** 设置 root 密码。
  * 如尚未配置网络，则再次运行 **netconfig**。
  * 一系列问题：
    * **time**：设置日期、时间与时区。
    * **services**：选择开机启动的服务。
    * **hardening**：应用若干加固选项。
    * **firmware**：选择安装所需固件包。
    * **adduser**：添加用户账户，此操作通过 **chroot(8)** 执行 **adduser(8)**，会带来与其他步骤略异的体验。
  * **finalconfig**：一个“并不终结”的菜单，能在重启前做进一步更改。
  * 非交互式步骤 **config**：应用上述所有配置项。
  * 清除下载的系统组件。
  * （如存在）运行 **local.post-configure**。
  * 最后一个菜单：允许用户进入 **chroot(8)** 环境中的目标系统。
  * 非交互式安全步骤：保存熵（entropy）。
  * 最终运行 **umount**，以便安全重启。
* 回到 **startbsdinstall**，如果 **bsdinstall(8)** 成功运行，用户将看到提示信息，并可选择重启、关机，或启动一个 shell。

正面的第一点是，即便安装介质中缺少部分系统组件，安装流程也能妥善处理。这允许创建不同大小的安装介质，而无需改动安装逻辑。

但另一方面，这一过程对用户提问过多。虽然每一个问题单独来看都显得重要而有用，但前文描述的其他系统，仅需三分之一的交互步骤就能构建出完整可用的系统。不幸的是，用户界面的问题还不仅限于此。

## 限制与使用场景

对 **bsddialog(1)**（或 C 程序中使用的 **libbsddialog**）的高度耦合，引入了与 Debian 安装器类似的限制：每一步的用户界面都相当基础，且无法根据用户输入动态调整交互。例如，网络配置步骤不能依据常见习惯自动建议合适的网络掩码、DNS 服务器或网关。这不仅会节省用户一些按键输入和不必要的精力，还会让整体体验显得更现代、更精致。

**可能的解决方案：Lua**

这个具体问题可以通过在 **libbsddialog**（或 **bsddialog(1)**）中引入 Lua 脚本支持来解决。这个想法由 **bsddialog** 的作者 Alfonso Siciliano（asiciliano@）提出，他目前正考虑将该扩展实现到项目中。

**初学者用户**

对于初学者，或者说一般桌面用户来说，显而易见的改进方向是：能够方便地安装一个在首次重启后就可直接使用的桌面系统。虽然这意味着需要增加一个安装步骤，但可以借鉴上文提到的 Debian 安装器，为服务器和桌面系统都提供适配的选项。

目前在这个方向已有实验性工作（专门由 Alfonso 牵头），但“写起来容易，实现起来吃力”。其中一些挑战包括对应固件文件和驱动程序的安装，尤其是内核模块 DRM 一直是个难点。当前已有一项尝试（致谢 manu@）试图将这些模块从 Ports 移回基本系统中，这应该能缓解当前二进制 Ports 中常见的版本不匹配问题。

**企业用途**

这次要说点积极的方面：FreeBSD 的安装器可以调整并自动化，以适应企业部署中的典型需求。

首先，它默认就支持设置 Jail，而不是执行常规安装。这种行为可以通过选择 `jail` 模式（而不是默认的 `auto`）来实现。操作如下所示：

```sh
# bsdinstall jail /path/to/the/new/jail
```

如果未来要对 FreeBSD 安装器进行重构或大幅修改，保留这一功能将是值得考虑的。另一方面，目前已有许多 Jail 管理方案存在于 **bsdinstall(8)** 之外。

更重要的是，安装器还支持 `script` 模式，此时需要提供一个安装脚本，在 **bsdinstall** 上下文中执行。这种方式由系统管理员编写脚本，从而实现完全自动化的 FreeBSD 安装过程。

在本节结尾，我必须提及一个至今仍缺失的功能，而其他对比对象都已具备这一能力：安装器默认假设无需代理服务器即可访问互联网。这个问题记录在 Bugzilla 的条目 #214390 中。根据我自己的经验，在为数十家客户提供渗透测试部署的场景中，这种限制往往直接导致 FreeBSD 安装器无法采用。

## 开发环境

公平地说，开发 **bsdinstall(8)** 并不容易。测试某些修改通常需要构建一个完整的可启动镜像，在虚拟环境中运行它，并重复整个安装流程，直到抵达需要测试的那一特定步骤。为了提高开发安装器时的效率，我尝试构建了一个专门适配此目标的开发环境。

它由一个基于 PXE 的设置组成，可针对传统的 PXE 环境和更现代的、基于 UEFI 的网络引导调整。我将此引导序列与一台 TFTP 服务器（由 `inetd` 管理）、DHCP 服务（使用 Port `isc-dhcp44-server`）以及只读的 NFS 挂载结合在一起。完整描述该设置超出了本文的范围，但归根结底，FreeBSD 安装器的可用性也取决于其开发环境的便利性。你可以根据自己的需求自由调整以下配置片段。

**对于 /etc/exports：**

```ini
/jail/bsdinstall -ro -network 192.168.x.0 -mask 255.255.255.0 -maproot 0:0
```

**对于 /etc/inetd.conf：**

```ini
tftp dgram udp   wait root /usr/libexec/tftpd   tftpd -l -s /tftpboot
tftp dgram udp6  wait root /usr/libexec/tftpd   tftpd -l -s /tftpboot
```

**对于 /etc/rc.conf：**

```ini
mountd_enable="YES"
nfs_server_enable="YES"
nfs_server_flags="-h 192.168.x.y"
rpcbind_enable="YES"
rpcbind_flags="-h 192.168.x.y"
```

**对于 /usr/local/etc/dhcpd.conf：**

```ini
option subnet-mask 255.255.255.0;
default-lease-time 600;
max-lease-time 7200;

subnet 192.168.x.0 netmask 255.255.255.0 {
  range 192.168.x.128 192.168.x.254;
  option routers 192.168.x.y;
  option subnet-mask 255.255.255.0;
}

option arch code 93 = unsigned integer 16;

host example {
  hardware ethernet aa:bb:cc:dd:ee:ff;
  fixed-address 192.168.x.127;
  next-server 192.168.x.y;
  if option arch = 0:07 {
    # UEFI
    filename "FreeBSD/boot/loader.efi";
  } else {
    # BIOS
    filename "FreeBSD/boot/pxeboot";
  }
  option root-path "192.168.x.y:/jail/bsdinstall/";
}
```

请将 `loader.efi` 和 `pxeboot` 文件放置于 **/tftpboot/FreeBSD/boot** 目录中。

你可以将 **/jail/bsdinstall** 文件夹设置为一个典型的 Jail，并根据安装器的常规启动环境调整，或者也可以直接暴露 release 构建中的 `disc1` 目录。无论哪种方式，这种方法都能避免在开发 `bsdinstall` 时频繁地生成、传输或重启镜像文件。

需要注意的是，这一设置在物理硬件和虚拟机中都同样适用，因为 VirtualBox 等虚拟化平台支持从 PXE 引导。

## 图形模式

最后，是时候回答这个问题了：我们能否改进 FreeBSD 安装器，使其功能达到例如 Debian 安装器的同等水平？

### 背景

2024 年初，FreeBSD 基金会委托我调研开源领域中图形化安装器的最新进展。其根本目标是为 FreeBSD 项目寻找一种可行方案来增加图形安装功能。Linux 上一个新的安装框架 Calamares¹² 是显而易见的候选者——前提是确认它可以用于 FreeBSD 作为目标系统。不幸的是，由于 Calamares 使用 GPL 许可证，与 FreeBSD 基本系统的目标用户群不兼容。

另一个当时关注度较高、且特定于 FreeBSD 的替代方案是 PC-BSD。它经过首次品牌更名为 TrueOS 后，于 2020 年停止维护。随后建议的替代品包括 GhostBSD、MidnightBSD 和 NomadBSD。

GhostBSD 提供了图形安装器 `gbi`。虽然是个不错的候选，但它与当前 `bsdinstall` 代码库差异较大，同时还需要完整的 Python 和 Gtk+ 环境。在我看来，更高效的方案是可行的。

据我所知，MidnightBSD 和 NomadBSD 均未提供图形安装器（**译者注：NomadBSD 有图形安装器，这里原作者是错误的**）。

### 最小侵入式方案

相反，鉴于我已熟悉 **bsdinstall** 和 **bsddialog**，我设想可以重用现有代码库与架构，同时用图形界面执行安装流程。通过用功能等效且同样采用 BSD 许可证的工具替换 **bsddialog(1)**，可以在项目分配的几周时间内交付一个功能完善的安装镜像。

我本人对 Gtk+ 也较为熟悉，考虑到仅需做一个可用的演示，这是一种合理的折衷。Gtk+ 采用 LGPL 许可证，仍然与目标用户群的许可兼容性不完全吻合。但我还是在几天内成功实现了 **gbsddialog**，它是 **bsddialog** 的桌面应用等效版本。

![gbsddialog 图形安装器界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-3083065f4efdf8e5a2c90653616e8900ef41337b%2FUsability-23.png?alt=media)

![FreeBSD 图形 Live 系统替代界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-e89c9e455bf8253e1578a68721da4964de6d59ba%2FUsability-24.png?alt=media)

![gbsddialog 图形安装器界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-ef52f20d422eac7ecf511e4690c4874773be7c39%2FUsability-25.png?alt=media)

![gbsddialog 图形安装器界面](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-c411479cae5fb1323db3389e5b7f42b5f63b371a%2FUsability-26.png?alt=media)

我对这个工具的最初设想略有不同，希望能做一些额外的改进或变通：

* 防止对话窗口在每次调用之间消失再重新出现，比如通过使用 GtkPlug 和 GtkSocket 小部件来实现。
* 在解决了上述问题后，使用 GtkAssistant 小部件来提升界面的美观和用户体验。
* **bsddialog** 中有一个特定的小部件 **mixedgauge**，其实现方式不适合用于图形化版本的工具：在 **bsdinstall** 中，通过滥用 **bsddialog** 控制台输出的持续存在感来营造进度感，实际上每次更新时都会执行新的 **bsddialog** 实例。在图形安装器中，这种监控只能用普通的 **gauge** 小部件替代，牺牲了部分可用性。

另一方面，我对桌面窗口的实现感到相当满意，它相当准确地复刻了原始 **bsddialog** 工具的外观与交互体验。尽管这部分代码目前还不够优雅，但它确实完成了任务。

### 修复与升级功能

去年，我有幸与一位非常积极的学生 Leaf Yen（你好！）共同指导谷歌编程之夏项目。该项目旨在为 FreeBSD 安装器引入新功能：通过可移动介质升级或修复现有系统。

项目成功完成，并在 GitHub 上提交了三个合并请求：

* \#1395，GSoC 2024 —— 改进安装器以支持修复和升级。
* \#1424，bsdinstall：在 live 环境中添加 pkg 安装支持。
* \#1427，bsdinstall：向安装菜单添加修复脚本。

我尚未将这些工作集成到图形版本安装器中，但我认为它们是现代安装器同样重要的组成部分。也请把这篇文章看作是对该项目的关注呼吁——一旦完善并合并，应该能同样适配当前安装器和图形版本。

### 作为网页应用的图形安装器

如果不提这个潜在的额外加分项，文章就不完整了。自 Gtk+ 3 起，Gtk+ 应用可以直接在网页浏览器内渲染，而不是仅限于电脑屏幕，这得益于 GDK Broadway 后端。

虽然我还没尝试过这个功能，但这似乎意味着可以把图形安装器版本变成一个网页服务器，从而实现无任何额外改动的网络安装 FreeBSD 系统！这显然是另一个潜在的轻松突破点。

### 可访问性

我想特别提到采用 Gtk+ 作为图形安装器演示器的另一个标准：可访问性。

虽然我自己也有一些残障状况，但尚未严重到让我深入尝试 Gtk+ 的可访问性特性。不过我知道它在这方面的能力，也明白对于部分需要辅助功能的人群，操作系统安装器的可用性十分关键。事实上，我听说过无论是私下还是会议上，都有人提出安装器需要支持辅助功能。

目前还有一个资助项目，旨在为 FreeBSD 安装器引入辅助功能，由 Alfonso Siciliano 负责调研。（再次问候！）

回到 Gtk+，它支持以下可访问性需求：

* 屏幕阅读器，如 Orca，用于转换语音或盲文。
* 完整的键盘导航。
* 标准小部件的无障碍行为。
* 高对比度主题或其它视觉增强主题。
* 文本和界面缩放。
* 插件系统以支持更多增强功能。

虽然我还没机会与有需求者一起实际体验这些功能，但我在开发 **gbsddialog** 期间始终牢记这一点，始终遵循核心 Gtk+ API。因此，该工具兼容 Gtk+ 2 和 Gtk+ 3。

在文章结尾前，我想提一下 DeforaOS 桌面环境。它是另一个轻量级桌面环境，大部分由一位个人开发（你好！），坦白说它的完成度和质量远未达到我追求的目标。它部分被打包到 FreeBSD 的 Ports 中（感谢 Olivier！），我在此图形安装器演示中也使用了它——哪怕只是为了展示其能力。

不过，我很高兴提及它带有虚拟键盘程序，并且我成功将其与图形安装器版本配合使用。

![DeforaOS 桌面环境与虚拟键盘](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-9c16845cbb6a1bd74e9a512c0471cc957372052c%2FUsability-27.png?alt=media)

基于本文所述的进展，我更新了 FreeBSD 功能对照表如下：

|   评估标准  |   FreeBSD 14.2   |
| :-----: | :--------------: |
|   语言支持  |         否        |
|   辅助功能  | 是（放大镜、屏幕键盘、高对比度） |
|   许可协议  |         否        |
|   步骤数   |        22        |
|    导航   |         否        |
|   优先提问  |         否        |
|   高级模式  |         是        |
|   步骤列表  |         否        |
|   时间估计  |         否        |
| Live 系统 |         是        |
|   修复系统  |         是        |
|  纯文本模式  |         是        |
|   图形模式  |         是        |
|   图形结果  |      混合（进行中）     |
|   自动化   |         是        |

## 结论

这确实是一个容易引发分歧且颇具主观性的议题。我希望本文对 FreeBSD 安装流程的改进有所助益，推动未来版本的优化。不幸的是，基于现有代码库工作并非易事，每次测试都需生成和启动安装介质，极为耗时且考验耐心。简单的改动可能无意中影响硬件支持或整体用户体验质量。

显而易见，FreeBSD 安装器还有大量改进空间。在工作期间，我也听到过彻底重写的呼声。但这既不是一个容易做出的决定，也不是一个轻松实现的项目——尤其要兼顾各种使用场景、满足各方需求，同时控制在合理预算内。

坦白说，我并不觉得自己有资格判断或强加某种方案。我更希望能提供并维护一项简单的、现有安装器的渐进式演进方案，在不引入不必要变动的前提下，扩展其能力。正如本文所述，通过复用现有代码，对基于控制台的安装器所做的 bug 修复和改进同样会体现在图形版本中，反之亦然。

## 参考文献

1. [MS DOS 6.22 Microsoft](https://archive.org/details/MS_DOS_6.22_MICROSOFT)
2. [Windows for Workgroups](https://archive.org/details/windows-for-workgroups)
3. [VirtualBox 相关讨论](https://forums.virtualbox.org/viewtopic.php?t=102121)
4. [Windows 95 OSR 2](https://archive.org/details/win-95-osr-2)
5. [Windows 98 SE ISO 文件](https://archive.org/details/windows-98-se-isofile)
6. [Windows 7 Professional with SP1 x64 DVD](https://archive.org/details/en_windows_7_professional_with_sp1_x64_dvd_u_676939_202302)
7. [FreeBSD 14.2 amd64 ISO 镜像](https://download.freebsd.org/releases/amd64/amd64/ISO-IMAGES/14.2/)
8. [FreeBSD 14.2 aarch64 ISO 镜像](https://download.freebsd.org/releases/arm64/aarch64/ISO-IMAGES/14.2/)
9. [Ubuntu Server 下载](https://ubuntu.com/download/server)
10. [Ubuntu Desktop 下载](https://ubuntu.com/download/desktop)
11. [Debian amd64 ISO 镜像](https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/)
12. [Calamares 安装器框架](https://calamares.io/)

***

**Pierre Pronchery** 是 FreeBSD 基金会的安全工程师，自 2023 年 5 月起担任用户空间软件开发者。他于 2001 年安装了第一套 FreeBSD 系统，对操作系统设计与实现充满热情。


# Overlord：让部署 Jail 像编程一样快

* 原文：[Overlord: Deploy Jails as Fast as You Code](https://freebsdfoundation.org/our-work/journal/browser-based-edition/networking-3/overlord-deploy-jails-as-fast-as-you-code/)
* 作者：Jesús Daniel Colmenares Oviedo

当我创建 [AppJail](https://github.com/DtxdF/AppJail) 时——这是一款完全用 **sh(1)** 和 C 语言编写的、基于 BSD-3 许可证的开源框架，用于利用 FreeBSD Jail 创建隔离、便携且易于部署的类似应用程序的环境——我的初衷是用它来测试 Ports，以避免破坏我的主环境。如今，AppJail 不仅仅是一个用于测试 Ports 的脚本，它变得高度灵活，具备许多非常实用的自动化功能。

当 AppJail 达到稳定阶段并被用于多种系统后，我意识到为每个想要的服务都部署一个 Jail 并不可行，尤其是在需要部署越来越多服务的情况下。于是，Director 应运而生。

[AppJail Director](https://github.com/DtxdF/director) 是一款基于 AppJail 的多 Jail 环境管理工具，使用简单的 YAML 配置文件定义如何配置组成应用程序的一个或多个 Jail。有了 Director 文件，你只需一条命令 `appjail-director up` 即可创建并启动你的应用程序。

Director 是 AppJail“一切皆代码”理念的首次实践。它将 Jail 组织成项目，让你以声明式方式创建含有一个或多个 Jail 的项目；当你修改了该配置文件或相关文件（例如 Makejail 文件），Director 会检测到变更，并毫不犹豫地销毁并重新创建 Jail。听起来有点激进，但用《The Ephemeral Concept》（易失的概念）来解释最为恰当：

Director 将每个 Jail 视为“易失”的。这并不意味着 Jail 停止或系统重启后数据会丢失，而是意味着 Director 认为销毁 Jail 是安全的，因为你已经明确区分了应持久保存的数据和被视为易失的数据。

更多细节见 **appjail-ephemeral(7)** 手册页，原则和上述相同。

值得注意的是，Director 本身不负责部署 Jail，它依赖执行配置、安装包等操作的指令，因此大量利用了 AppJail 的 Makejails 功能——这是一种简易文本文件，自动化创建 Jail 的步骤。Centralized Repository（集中仓库）中已有许多 Makejail 文件，但你也可以使用自己的仓库来托管。

AppJail 和 Director 极大简化了我的工作，但有一个问题二者都未能解决——即如何在多台服务器上协调管理 Jail。对少量服务器，结合 SSH 使用 AppJail 和 Director 尚可，但随着服务器数量增多，这会变得非常痛苦。因此 Overlord 应运而生。

[Overlord](https://github.com/DtxdF/overlord) 是一个面向 GitOps 的快速分布式 FreeBSD Jail 编排器。你只需定义一个文件说明集群上运行的服务，部署就能在几秒到几分钟内完成。

幸好 Overlord 诞生时，AppJail 和 Director 已经相当成熟，重用这两个经过充分测试的工具并与 Overlord 结合，是个明智的选择。继承 Director“一切皆代码”的哲学，使得 Overlord 易于使用。另一个设计决策是 Overlord 采用全异步架构。大型服务的部署可能耗时较长，但即使部署很快，声明式发送指令并让 Overlord 处理工作也是更优的体验。本文后续会详细介绍上述内容的诸多细节。

## 架构

Overlord 架构被描述为一种树形链式架构。每个运行 API 服务器的 Overlord 实例都可以配置为管理其他链（chains）的分组。每个成员链还可以进一步配置为管理更多的链，层层递进。虽然这种层级结构几乎可以无限扩展，但不加控制地扩展会引入延迟，因此了解你计划如何组织服务器是非常重要的。

选择这种架构的原因在于它非常简单且具备良好的可扩展性，通过将多个链连接起来形成集群，即可在多台服务器间共享资源，从而方便地部署项目。

该架构同时抽象了项目的部署方式。想要部署项目的用户无需了解每个链的具体终端节点，只需知道第一个链（也称根链，root chain）即可。这是因为每条链都会被标记一个任意的字符串标签，用户只需在部署文件中指定根链的终端地址、访问令牌和标签即可。虽然标签本质上是主观的，但它们可以表达需求。例如，我们可以用字符串 vm-only 标记那些具备部署虚拟机能力的服务器，用 db-only 标记数据库服务器，实际上标签是完全任意的。

```sh
bravo
                           /
           main <---> alpha
                           \
                            charlie <---> delta
```

假设只有 charlie 和 delta 拥有 `db-only` 标签。要将项目部署到带有指定标签的 API 服务器，客户端必须向 main 发起 HTTP 请求，指定链为 `alpha.charlie` 和 `alpha.charlie.delta`。这个过程是透明进行的，无需用户干预。

```sh
main . alpha . charlie
                          &
             main . alpha . charlie . delta
```

如果某条链宕机，会发生什么情况？如果根链宕机，则无法进行任何操作，虽然可以指定多个根链（但本文档其余部分只使用一个）。然而，如果根链之后的某条链宕机，会出现一个有趣的处理机制。

当根链之后的某条链检测到错误时，它可以将该失败的链暂时加入黑名单。黑名单的作用是不显示失败的链，尽管并不禁止通过其他链尝试连接到被标记为失败的链，因为一旦成功连接到黑名单中的链，它会被自动重新启用。

每个被列入黑名单的链都会被分配一个保持黑名单状态的时间。过了一段时间后，该链会从黑名单中移除；但如果链依旧失败，它会被重新加入黑名单，且保持时间更长。这个时间延长机制有上限，避免链被永久禁用。

上述机制的好处是，HTTP 请求的完成时间可以缩短，因为不必去尝试连接那些已知失败的链。

## 项目部署

演示 Overlord 最好的方式是部署一个小项目。

需要注意的是，一个项目可以包含多个 Jail，但在以下示例中只需一个 Jail。

filebrowser.yml:

```yml
kind: directorProject
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - desktop
projectName: filebrowser
projectFile: |
  options:
    - virtualnet: ':<random> default'
    - nat:
  services:
    filebrowser:
      makejail: 'gh+AppJail-makejails/filebrowser'
      volumes:
        - db: filebrowser-db
        - log: filebrowser-log
        - www: filebrowser-www
      start-environment:
        - FB_NOAUTH: 1
      arguments:
        - filebrowser_tag: 14.2
      options:
        - expose: '8432:8080 ext_if:tailscale0 on_if:tailscale0'
  default_volume_type: '<volumefs>'
  volumes:
    db:
      device: /var/appjail-volumes/filebrowser/db
    log:
      device: /var/appjail-volumes/filebrowser/log
    www:
      device: /var/appjail-volumes/filebrowser/www
```

你可能已经注意到，我并未直接指定访问令牌和入口点，而是通过环境变量来传递，这些变量是通过 `.env` 文件加载的：

.env:

```ini
ENTRYPOINT=http://127.0.0.1:8888
TOKEN=<access token>
```

那么另一个问题是：访问令牌是如何生成的？这很简单，令牌是由运行你想要访问的 Overlord 实例的机器生成的，但只有拥有访问密钥权限的人才能生成令牌。这个密钥默认是伪随机生成的。

```sh
# OVERLORD_CONFIG=/usr/local/etc/overlord.yml overlord gen-token
```

下一步就是简单地应用部署文件。

```sh
$ overlord apply -f filebrowser.yml
```

如果没有任何输出，说明一切正常，但这并不意味着项目已经部署完成。当应用一个部署文件且其中包含部署项目的规格（如上例中的 directorProject）时，项目会进入队列等待执行。由于当前没有其他项目正在运行，我们的项目会尽快被部署。

```yml
$ overlord get-info -f filebrowser.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    filebrowser:
      state: DONE
      last_log: 2025-04-22_17h57m45s
      locked: False
      services:
        - {'name': 'filebrowser', 'status': 0, 'jail': 'e969b06736'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 7 minutes and 42.41 seconds
        job_id: 14
        restarted: False
        labels:
         error: False
         message: None
```

## 元数据

元数据用于创建小型文件（例如配置文件），这些文件可以在部署项目或虚拟机时使用。虽然像 GitLab、GitHub、Gitea 等 Git 托管服务与 Makejails 配合使用非常方便，但你也可以使用元数据来代替依赖 Git 托管，以进一步配置你正在部署的服务或虚拟机。

元数据的另一个优点是可以在不同部署之间共享。例如，通过部署共享相同 **sshd\_config(5)** 和 `authorized_keys` 文件的虚拟机，实现配置复用。

tor.yml:

```yml
kind: directorProject
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
    deployIn:
      labels:
        - desktop
    projectName: tor
    projectFile: |
      options:
        - virtualnet: ':<random> address:10.0.0.50 default'
        - nat:
    services:
      tor:
        makejail: !ENV '${OVERLORD_METADATA}/tor.makejail'
        volumes:
          - data: '/var/db/tor'

volumes:
  data:
    device: '/var/appjail-volumes/tor/data'
```

metadata.yml:

```yml
kind: metadata
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
    deployIn:
      labels:
        - desktop
    metadata:
      tor.makejail: |
        OPTION start
        OPTION overwrite=force

        INCLUDE gh+DtxdF/efficient-makejail

        PKG tor

        CMD echo "SocksPort 0.0.0.0:9050" > /usr/local/etc/tor/torrc
        CMD echo "HTTPTunnelPort 0.0.0.0:9080" >> /usr/local/etc/tor/torrc

        SERVICE tor oneenable
        SERVICE tor start
```

.env:

```ini
ENTRYPOINT=http://127.0.0.1:8888
TOKEN=<access token>
```

从用户的角度来看，部署项目和部署元数据没有区别。然而，元数据不会进入队列，而是直接（异步地）写入磁盘。

```sh
$ overlord apply -f metadata.yml
$ overlord apply -f tor.yml
$ overlord get-info -f metadata.yml -t metadata

datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
metadata:
  tor.makejail: |
    OPTION start
    OPTION overwrite=force

    INCLUDE gh+DtxdF/efficient-makejail

    PKG tor

    CMD echo "SocksPort 0.0.0.0:9050" > /usr/local/etc/tor/torrc
    CMD echo "HTTPTunnelPort 0.0.0.0:9080" >> /usr/local/etc/tor/torrc

    SERVICE tor oneenable
    SERVICE tor start

$ overlord get-info -f tor.yml -t projects --filter-per-project

datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
projects:
  tor:
    state: UNFINISHED
    last_log: 2025-04-22_18h40m30s
    locked: True
    services:
      - {'name': 'tor', 'status': 0, 'jail': '7ce0dfdcef'}
    up:
      operation: RUNNING
      last_update: 38.01 seconds
      job_id: 16
```

## 部署 FreeBSD 虚拟机

Overlord 能够借助出色的 [vm-bhyve](https://github.com/churchers/vm-bhyve) 项目部署虚拟机。虚拟机可以隔离很多 Jail 无法做到的部分，尽管这样会带来一定的开销，但根据你的使用场景，这种开销可能并不是问题。

这个部署过程如下：会创建一个 director 文件（由 Overlord 内部完成），该文件用于进一步创建一个 Jail，代表一个必须安装了 [vm-bhyve](https://github.com/churchers/vm-bhyve) 的环境，且需要配置使用 FreeBSD 支持的防火墙，和配置虚拟机使用的桥接网络。听起来很复杂，但有一个 [Makejail](https://github.com/DtxdF/vm-makejail) 专门完成这些工作，可以查看该项目了解细节。上述 Makejail 会创建一个安装了 [vm-bhyve-devel](https://freshports.org/sysutils/vm-bhyve-devel) 的环境，配置 **pf(4)** 防火墙，并创建一个带有分配的 IPv4（192.168.8.1/24）的桥接，因此我们必须给虚拟机分配一个该网段内的 IPv4 地址。**pf(4)** 并未配置进一步隔离连接，因此虚拟机内的应用程序可以“逃逸”访问其他服务，这是否理想取决于应用的具体需求。

vm.yml:

```yaml
kind: vmJail
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
    deployIn:
      labels:
        - !ENV '${DST}'
    vmName: !ENV '${VM}'
    makejail: 'gh+DtxdF/vm-makejail'
    template:
      loader: 'bhyveload'
      cpu: !ENV '${CPU}'
      memory: !ENV '${MEM}'
      network0_type: 'virtio-net'
      network0_switch: 'public'
      wired_memory: 'YES'
    diskLayout:
      driver: 'nvme'
      size: !ENV '${DISK}'
      from:
        type: 'components'
        components:
          - base.txz
          - kernel.txz
    osArch: amd64
    osVersion: !ENV '${VERSION}-RELEASE'
    disk:
      scheme: 'gpt'
      partitions:
        - type: 'freebsd-boot'
          size: '512k'
          alignment: '1m'
        - type: 'freebsd-swap'
          size: !ENV '${SWAP}'
          alignment: '1m'
        - type: 'freebsd-ufs'
          alignment: '1m'
          format:
            flags: '-Uj'
          bootcode:
            bootcode: '/boot/pmbr'
            partcode: '/boot/gptboot'
            index: 1
    fstab:
      - device: '/dev/nda0p3'
        mountpoint: '/'
        type: 'ufs'
        options: 'rw,sync'
        dump: 1
        pass: 1
      - device: '/dev/nda0p2'
        mountpoint: 'none'
        type: 'swap'
        options: 'sw'
        dump: 0
        pass: 0
    script-environment:
      - HOSTNAME: !ENV '${HOSTNAME}'
    script: |
      set -xe
      set -o pipefail

      . "/metadata/environment"

      sysrc -f /mnt/etc/rc.conf ifconfig_vtnet0="inet 192.168.8.2/24"
      sysrc -f /mnt/etc/rc.conf defaultrouter="192.168.8.1"
      sysrc -f /mnt/etc/rc.conf fsck_y_enable="YES"
      sysrc -f /mnt/etc/rc.conf clear_tmp_enable="YES"
      sysrc -f /mnt/etc/rc.conf dumpdev="NO"
      sysrc -f /mnt/etc/rc.conf moused_nondefault_enable="NO"
      sysrc -f /mnt/etc/rc.conf hostname="${HOSTNAME}"

      if [ -f "/metadata/resolv.conf" ]; then
        cp -a /metadata/resolv.conf /mnt/etc/resolv.conf
      fi

      if [ -f "/metadata/loader.conf" ]; then
        cp /metadata/loader.conf /mnt/boot/loader.conf
      fi

      if [ -f "/metadata/zerotier_network" ]; then
        pkg -c /mnt install -y zerotier

        zerotier_network='head -1 -- "/metadata/zerotier_network"'

        cat << EOF > /mnt/etc/rc.local
      while :; do
        if ! /usr/local/bin/zerotier-cli join ${zerotier_network}; then
          sleep 1
          continue
        fi

        break
      done

      rm -f /etc/rc.local
      EOF

        sysrc -f /mnt/etc/rc.conf zerotier_enable="YES"
      elif [ -f "/metadata/ts_auth_key" ]; then
        pkg -c /mnt install -y tailscale

        ts_auth_key='head -1 -- "/metadata/ts_auth_key"'

        echo "/usr/local/bin/tailscale up --accept-dns=false --auth-key=\"${ts_auth_key}\" && rm -f /etc/rc.local" > /mnt/etc/rc.local

        sysrc -f /mnt/etc/rc.conf tailscaled_enable="YES"
      fi

      if [ -f "/metadata/timezone" ]; then
        timezone='head -1 -- "/metadata/timezone"'

        ln -fs "/usr/share/zoneinfo/${timezone}" /mnt/etc/localtime
      fi

      if [ -f "/metadata/sshd_config" ]; then
        sysrc -f /mnt/etc/rc.conf sshd_enable="YES"
        cp /metadata/sshd_config /mnt/etc/ssh/sshd_config
      fi

      if [ -f "/metadata/ssh_key" ]; then
        cp /metadata/ssh_key /mnt/etc/ssh/authorized_keys
      fi

      if [ -f "/metadata/sysctl.conf" ]; then
        cp /metadata/sysctl.conf /mnt/etc/sysctl.conf
      fi

      if [ -f "/metadata/pkg.conf" ]; then
        mkdir -p /mnt/usr/local/etc/pkg/repos
        cp /metadata/pkg.conf /mnt/usr/local/etc/pkg/repos/Latest.conf
      fi
metadata:
  - resolv.conf
  - loader.conf
  - timezone
  - sshd_config
  - ssh_key
  - sysctl.conf
  - pkg.conf
  - ts_auth_key
```

metadata.yml:

```yml
kind: directorProject
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - desktop
projectName: tor
projectFile: |
  options:
    - virtualnet: ':<random> address:10.0.0.50 default'
    - nat:
  services:
    tor:
      makejail: !ENV '${OVERLORD_METADATA}/tor.makejail'
      volumes:
        - data: '/var/db/tor'
  volumes:
    data:
      device: '/var/appjail-volumes/tor/data'
```

.profile-vmtest.env:

```ini
ENTRYPOINT=http://127.0.0.1:8888
TOKEN=<access token>
VM=vmtest
CPU=1
MEM=256M
DISK=10G
VERSION=14.2
SWAP=1G
HOSTNAME=vmtest
DST=provider
```

与其每次部署虚拟机都复制粘贴部署文件，不如创建多个环境（或类似配置文件）来管理。

```yml
$ overlord -e .profile-vmtest.env apply -f metadata.yml
$ overlord -e .profile-vmtest.env apply -f vm.yml
$ overlord -e .profile-vmtest.env get-info -f vm.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - provider
    - vm-only
  projects:
    vmtest:
      state: UNFINISHED
      last_log: 2025-04-22_20h19m34s
      locked: True
      services:
        - {'name': 'vm', 'status': 0, 'jail': 'vmtest'}
      up:
        operation: RUNNING
        last_update: 58.85 seconds
        job_id: 17
```

根据安装类型，安装过程可能需要一些时间。以上例中，我们选择从 FreeBSD 组件安装，因此如果服务器尚未拥有这些组件，或者已有组件但远程发生了变化（例如修改时间变动），Overlord 会自动下载最新组件。

```sh
$ overlord -e .profile-vmtest.env get-info -f vm.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - provider
    - vm-only
  projects:
    vmtest:
      state: DONE
      last_log: 2025-04-22_20h19m34s
      locked: False
      services:
        - {'name': 'vm', 'status': 0, 'jail': 'vmtest'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 6 minutes and 10.02 seconds
        job_id: 17
        restarted: False
$ overlord -e .profile-vmtest.env get-info -f vm.yml -t vm --filter-per-project
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - provider
    - vm-only
  projects:
    vmtest:
      virtual-machines:
          operation: COMPLETED
          output: |
            md0 created
             md0p1 added
             md0p2 added
             md0p3 added
             /dev/md0p3: 9214.0MB (18870272 sectors) block size 32768, fragment size 4096
                using 15 cylinder groups of 625.22MB, 20007 blks, 80128 inodes.
                with soft updates
             super-block backups (for fsck_ffs -b #) at:
              192, 1280640, 2561088, 3841536, 5121984, 6402432, 7682880, 8963328, 10243776,
              11524224, 12804672, 14085120, 15365568, 16646016, 17926464
             Using inode 4 in cg 0 for 75497472 byte journal
             bootcode written to md0
             partcode written to md0p1
             ifconfig_vtnet0:  -> inet 192.168.8.2/24
             defaultrouter: NO -> 192.168.8.1
             fsck_y_enable: NO -> YES
             clear_tmp_enable: NO -> YES
             dumpdev: NO -> NO
             moused_nondefault_enable: YES -> NO
             hostname:  -> vmtest
             [vmtest.appjail] Installing pkg-2.1.0...
             [vmtest.appjail] Extracting pkg-2.1.0: .......... done
             Updating FreeBSD repository catalogue...
             [vmtest.appjail] Fetching meta.conf: . done
             [vmtest.appjail] Fetching data.pkg: .......... done
             Processing entries: .......... done
             FreeBSD repository update completed. 35950 packages processed.
             All repositories are up to date.
             The following 2 package(s) will be affected (of 0 checked):

             New packages to be INSTALLED:
                ca_root_nss: 3.108
                tailscale: 1.82.5

             Number of packages to be installed: 2

             The process will require 35 MiB more space.
             11 MiB to be downloaded.
             [vmtest.appjail] [1/2] Fetching tailscale-1.82.5.pkg: .......... done
             [vmtest.appjail] [2/2] Fetching ca_root_nss-3.108.pkg: .......... done
             Checking integrity... done (0 conflicting)
             [vmtest.appjail] [1/2] Installing ca_root_nss-3.108...
             [vmtest.appjail] [1/2] Extracting ca_root_nss-3.108: ....... done
             Scanning /usr/share/certs/untrusted for certificates...
             Scanning /usr/share/certs/trusted for certificates...
             Scanning /usr/local/share/certs for certificates...
             [vmtest.appjail] [2/2] Installing tailscale-1.82.5...
             [vmtest.appjail] [2/2] Extracting tailscale-1.82.5: ...... done
             =====
             Message from ca_root_nss-3.108:

             --
             FreeBSD does not, and can not warrant that the certification authorities
             whose certificates are included in this package have in any way been
             audited for trustworthiness or RFC 3647 compliance.

             Assessment and verification of trust is the complete responsibility of
             the system administrator.

             This package installs symlinks to support root certificate discovery
             for software that either uses other cryptographic libraries than
             OpenSSL, or use OpenSSL but do not follow recommended practice.

             If you prefer to do this manually, replace the following symlinks with
             either an empty file or your site-local certificate bundle.

               * /etc/ssl/cert.pem
               * /usr/local/etc/ssl/cert.pem
               * /usr/local/openssl/cert.pem
             tailscaled_enable:  -> YES
             sshd_enable: NO -> YES
             vm_list:  -> vmtest
             Starting vmtest
               * found guest in /vm/vmtest
               * booting...
            newfs: soft updates journaling set
             + set -o pipefail
             + . /metadata/environment
             + export 'HOSTNAME=vmtest'
             + sysrc -f /mnt/etc/rc.conf 'ifconfig_vtnet0=inet 192.168.8.2/24'
             + sysrc -f /mnt/etc/rc.conf 'defaultrouter=192.168.8.1'
             + sysrc -f /mnt/etc/rc.conf 'fsck_y_enable=YES'
             + sysrc -f /mnt/etc/rc.conf 'clear_tmp_enable=YES'
             + sysrc -f /mnt/etc/rc.conf 'dumpdev=NO'
             + sysrc -f /mnt/etc/rc.conf 'moused_nondefault_enable=NO'
             + sysrc -f /mnt/etc/rc.conf 'hostname=vmtest'
             + [ -f /metadata/resolv.conf ]
             + cp -a /metadata/resolv.conf /mnt/etc/resolv.conf
             + [ -f /metadata/loader.conf ]
             + cp /metadata/loader.conf /mnt/boot/loader.conf
             + [ -f /metadata/zerotier_network ]
             + [ -f /metadata/ts_auth_key ]
             + pkg -c /mnt install -y tailscale
             + head -1 -- /metadata/ts_auth_key
             + ts_auth_key=[REDACTED]
             + echo '/usr/local/bin/tailscale up --accept-dns=false --auth-key="[REDACTED]" && rm -f /etc/rc.local'
             + sysrc -f /mnt/etc/rc.conf 'tailscaled_enable=YES'
             + [ -f /metadata/timezone ]
             + head -1 -- /metadata/timezone
             + timezone=America/Caracas
             + ln -fs /usr/share/zoneinfo/America/Caracas /mnt/etc/localtime
             + [ -f /metadata/sshd_config ]
             + sysrc -f /mnt/etc/rc.conf 'sshd_enable=YES'
             + cp /metadata/sshd_config /mnt/etc/ssh/sshd_config
             + [ -f /metadata/ssh_key ]
             + cp /metadata/ssh_key /mnt/etc/ssh/authorized_keys
             + [ -f /metadata/sysctl.conf ]
             + cp /metadata/sysctl.conf /mnt/etc/sysctl.conf
             + [ -f /metadata/pkg.conf ]
             + mkdir -p /mnt/usr/local/etc/pkg/repos
             + cp /metadata/pkg.conf /mnt/usr/local/etc/pkg/repos/Latest.conf
          last_update: 5 minutes and 12.6 seconds
          job_id: 17
```

由于我使用了 Tailscale，上述虚拟机即将被配置加入我的 tailnet，过一段时间后，它应该会出现在节点列表中：

```sh
$ tailscale status
...
100.124.236.28  vmtest               REDACTED@    freebsd -
$ ssh root@100.124.236.28
The authenticity of host '100.124.236.28 (100.124.236.28)' can't be established.
ED25519 key fingerprint is SHA256:Oc61mU8erpgS2evkwL9WhOOl4Ze94sSNfhImLy3b4UQ.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '100.124.236.28' (ED25519) to the list of known hosts.
root@vmtest:~ #

```

## 服务发现

服务已经部署完成，然而根据你集群中服务器的数量，确定服务端点可能会变得复杂。你当然知道使用的端口和外部接口，IP 地址也相对容易获取，但更方便的做法是使用 DNS，这正是 DNS 的主要用途。服务虽然可以部署在不同服务器上，但它的访问端点始终保持一致。

SkyDNS 是一种较为老牌但功能强大的协议，结合 Etcd 使用时，可以实现便捷的服务发现。Overlord 可以配置使用 Etcd 和 SkyDNS。接下来，我们来部署 Etcd 集群。

etcd.yml:

```yml
kind: directorProject
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - desktop
    - r2
    - centralita
projectName: etcd-cluster
projectFile: |
  options:
    - alias:
    - ip4_inherit:
  services:
    etcd:
      makejail: 'gh+AppJail-makejails/etcd'
      arguments:
        - etcd_tag: '14.2-34'
      volumes:
        - data: etcd-data
      start-environment:
        - ETCD_NAME: !ENV '${NAME}'
        - ETCD_ADVERTISE_CLIENT_URLS: !ENV 'http://${HOSTIP}:2379'
        - ETCD_LISTEN_CLIENT_URLS: !ENV 'http://${HOSTIP}:2379'
        - ETCD_LISTEN_PEER_URLS: !ENV 'http://${HOSTIP}:2380'
        - ETCD_INITIAL_ADVERTISE_PEER_URLS: !ENV 'http://${HOSTIP}:2380'
        - ETCD_INITIAL_CLUSTER_TOKEN: 'etcd-demo-cluster'
        - ETCD_INITIAL_CLUSTER: !ENV '${CLUSTER}'
        - ETCD_INITIAL_CLUSTER_STATE: 'new'
        - ETCD_HEARTBEAT_INTERVAL: '5000'
        - ETCD_ELECTION_TIMEOUT: '50000'
        - ETCD_LOG_LEVEL: 'error'
  default_volume_type: '<volumefs>'
  volumes:
    data:
      device: /var/appjail-volumes/etcd-cluster/data
environment:
  CLUSTER: 'etcd0=http://100.65.139.52:2380,etcd1=http://100.109.0.125:2380,etcd2=http://100.96.18.2:2380'
labelsEnvironment:
  desktop:
    NAME: 'etcd0'
    HOSTIP: '100.65.139.52'
  r2:
    NAME: 'etcd1'
    HOSTIP: '100.109.0.125'
  centralita:
    NAME: 'etcd2'
    HOSTIP: '100.96.18.2'
```

收获：

```yml
$ overlord apply -f etcd.yml
$ overlord get-info -f etcd.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    etcd-cluster:
      state: DONE
      last_log: 2025-04-23_02h28m36s
      locked: False
      services:
        - {'name': 'etcd', 'status': 0, 'jail': 'f094a31c46'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 8 minutes and 11.51 seconds
        job_id: 20
        restarted: False
        labels:
         error: False
         message: None
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: centralita
  labels:
    - all
    - centralita
    - services
  projects:
    etcd-cluster:
      state: DONE
      last_log: 2025-04-23_02h28m37s
      locked: False
      services:
        - {'name': 'etcd', 'status': 0, 'jail': '1ff836df47'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 5 minutes and 37.82 seconds
        job_id: 2
        restarted: False
        labels:
         error: False
         message: None
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: r2
  labels:
    - all
    - r2
    - services
  projects:
    etcd-cluster:
      state: DONE
      last_log: 2025-04-23_02h28m38s
      locked: False
      services:
        - {'name': 'etcd', 'status': 0, 'jail': '756ae9d5ca'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 5 minutes and 5.04 seconds
        job_id: 1
        restarted: False
        labels:
         error: False
         message: None
```

部署完成后，就可以使用以下参数配置每个 Overlord 实例。

**/usr/local/etc/overlord.yml**:

```yml
etcd:
  100.65.139.52: {}
  100.109.0.125: {}
  100.96.18.2: {}
```

请记得重启 Overlord 进程以使更改生效。

```sh
supervisorctl restart overlord:
```

下一个需要部署的服务是 CoreDNS。多亏了它，我们可以通过 [Etcd 插件](https://coredns.io/plugins/etcd/) 使用 SkyDNS。

coredns.yml:

```yml
kind: directorProject
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - desktop
    - r2
projectName: dns-server
projectFile: |
  options:
    - alias:
    - ip4_inherit:
  services:
    coredns:
      makejail: !ENV '${OVERLORD_METADATA}/coredns.makejail'
```

metadata.yml:

```yml
kind: metadata
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - desktop
    - r2
metadata:
  Corefile: |
    .:53 {
      bind tailscale0
      log
      errors
      forward . 208.67.222.222 208.67.220.220
      etcd overlord.lan. {
        endpoint http://100.65.139.52:2379 http://100.109.0.125:2379 http://100.96.18.2:2379
      }
      hosts /etc/hosts namespace.lan.
      cache 30
    }
  coredns.hosts: |
    100.65.139.52    controller.namespace.lan
    100.96.18.2      centralita.namespace.lan
    100.127.18.7     fbsd4dev.namespace.lan
    100.123.177.93   provider.namespace.lan
    100.109.0.125   r2.namespace.lan
    172.16.0.3      cicd.namespace.lan
  coredns.makejail: |
    OPTION start
    OPTION overwrite=force
    OPTION healthcheck="health_cmd:jail:service coredns status" "recover_cmd:jail:service coredns restart"

    INCLUDE gh+DtxdF/efficient-makejail

    CMD mkdir -p /usr/local/etc/pkg/repos
    COPY ${OVERLORD_METADATA}/coredns.pkg.conf /usr/local/etc/pkg/repos/Latest.conf

    PKG coredns

    CMD mkdir -p /usr/local/etc/coredns
    COPY ${OVERLORD_METADATA}/Corefile /usr/local/etc/coredns/Corefile

    COPY ${OVERLORD_METADATA}/coredns.hosts /etc/hosts

    SYSRC coredns_enable=YES
    SERVICE coredns start
  coredns.pkg.conf: |
    FreeBSD: {
      url: "pkg+https://pkg.FreeBSD.org/${ABI}/latest",
      mirror_type: "srv",
      signature_type: "fingerprints",
      fingerprints: "/usr/share/keys/pkg",
      enabled: yes
    }
```

正如你在 CoreDNS 配置文件中看到的，假设使用了 `overlord.lan` 域名区域，但默认情况下 Overlord 仅使用根域名 `.`，这对于当前场景没有意义，因此请根据这一点配置 Overlord 并部署 CoreDNS。

**/usr/local/etc/overlord.yml**：

```yml
skydns:
  zone: 'overlord.lan.'
```

注意：请记得重启 Overlord 进程，使配置更改生效。

```yml
$ overlord apply -f metadata.yml
$ overlord apply -f coredns.yml
$ overlord get-info -f coredns.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    dns-server:
      state: DONE
      last_log: 2025-04-23_13h32m49s
      locked: False
      services:
        - {'name': 'coredns', 'status': 0, 'jail': '8106aaca6d'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 2 minutes and 30.14 seconds
        job_id: 25
        restarted: False
        labels:
         error: False
         message: None
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: r2
  labels:
    - all
    - r2
    - services
  projects:
    dns-server:
      state: DONE
      last_log: 2025-04-23_13h32m54s
      locked: False
      services:
        - {'name': 'coredns', 'status': 0, 'jail': '9516eb48aa'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 3 minutes and 26.9 seconds
        job_id: 4
        restarted: False
        labels:
         error: False
         message: None
```

我们的 Etcd 集群和 DNS 服务器均已启动运行。客户端应配置为通过这些 DNS 服务器解析主机名，因此请在它们的 **resolv.conf(5)** 或类似文件中配置。

示例 **/etc/resolv.conf** 配置如下：

```ini
nameserver 100.65.139.52
nameserver 100.109.0.125
```

我们的“弗兰肯斯坦”（**译者注：弗兰肯斯坦是个制造怪物的科学家，参见小说《科学怪人》**）活过来了！下一步是部署一个服务，并测试所有部分是否如预期般正常工作。

homebox.yml:

```yml
kind: directorProject
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - centralita
projectName: homebox
projectFile: |
  options:
    - virtualnet: ':<random> default'
    - nat:
  services:
    homebox:
      makejail: gh+AppJail-makejails/homebox
      options:
        - expose: '8666:7745 ext_if:tailscale0 on_if:tailscale0'
        - label: 'overlord.skydns:1'
        - label: 'overlord.skydns.group:homebox'
        - label: 'overlord.skydns.interface:tailscale0'
      volumes:
        - data: homebox-data
      arguments:
        - homebox_tag: 14.2
  default_volume_type: '<volumefs>'
  volumes:
    data:
      device: /var/appjail-volumes/homebox/data
```

如此简单。Overlord 会拦截我们在 [Director](https://github.com/DtxdF/director) 文件中定义的标签，并基于这些标签创建 DNS 记录。

```yml
$ overlord apply -f homebox.yml
$ overlord get-info -f homebox.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: centralita
  labels:
    - all
    - centralita
    - services
  projects:
    homebox:
      state: DONE
      last_log: 2025-04-23_15h44m38s
      locked: False
      services:
        - {'name': 'homebox', 'status': 0, 'jail': '1f97e32f36'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 4 minutes and 4.1 seconds
        job_id: 6
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             homebox:
               error: False
               message: None
         skydns:
           services:
             homebox:
               error: False
               message: (project:homebox, service:homebox, records:[address:True,ptr:None,srv:None] records has been updated.
```

最后，我们的服务端点是 <http://homebox.overlord.lan:8666/>

## 负载均衡

关于 SkyDNS 的一个有趣特性是，多个域名可以被分组管理。因此，如果我们在多台服务器上部署同一个服务，且它们使用相同的分组，DNS 请求将返回三个 A 记录（对于 IPv4），也就是三个 IPv4 地址。

hello-http.yml:

```yml
kind: directorProject
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - services
projectName: hello-http
projectFile: |
  options:
    - virtualnet: ':<random> default'
    - nat:
  services:
    darkhttpd:
      makejail: 'gh+DtxdF/hello-http-makejail'
      options:
        - expose: '9128:80 ext_if:tailscale0 on_if:tailscale0'
        - label: 'appjail.dns.alt-name:hello-http'
        - label: 'overlord.skydns:1'
        - label: 'overlord.skydns.group:hello-http'
        - label: 'overlord.skydns.interface:tailscale0'
      arguments:
        - darkhttpd_tag: 14.2
```

这带来的一个额外好处是，服务会以轮询（round-robin）的方式实现负载均衡，尽管这完全取决于客户端，但大多数现代客户端都会这么做。

```yml
$ overlord apply -f hello-http.yml
$ overlord get-info -f hello-http.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_16h26m08s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': '7c2225c5fe'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 2 minutes and 43.3 seconds
        job_id: 28
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: None
         skydns:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, records:[address:True,ptr:None,srv:None] records has been updated.
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: centralita
  labels:
    - all
    - centralita
    - services
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_16h26m09s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': '3822f65e97'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 2 minutes and 18.56 seconds
        job_id: 13
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: None
         skydns:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, records:[address:True,ptr:None,srv:None] records has been updated.
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: r2
  labels:
    - all
    - r2
    - services
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_16h26m10s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': '0e0e64eb3c'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 51.17 seconds
        job_id: 8
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: None
         skydns:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, records:[address:True,ptr:None,srv:None] records has been updated.
$ host -t A hello-http.overlord.lan
hello-http.overlord.lan has address 100.65.139.52
hello-http.overlord.lan has address 100.109.0.125
hello-http.overlord.lan has address 100.96.18.2
$ curl http://hello-http.overlord.lan:9128/
curl http://hello-http.overlord.lan:9128
Hello, world!
UUID: 472ffbdb-9472-4aa2-95ff-39f4bde214df
$ curl http://hello-http.overlord.lan:9128/
Hello, world!
UUID: 7db3b268-87bb-4e81-8be3-e888378fa13b
```

不过，我知道在大多数情况下，需要更复杂的配置。更糟糕的是，正如前面提到的，这还取决于客户端，因此可能符合，也可能不符合你的预期。

幸运的是，Overlord 提供了与 HAProxy 的集成，更准确地说，是与 Data Plane API 的集成，因此你的配置可以复杂到满足任何需求。

haproxy.yml:

```yml
kind: directorProject
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - desktop
    - r2
projectName: load-balancer
projectFile: |
  options:
    - alias:
    - ip4_inherit:
  services:
    haproxy:
      makejail: !ENV '${OVERLORD_METADATA}/haproxy.makejail'
      arguments:
        - haproxy_tag: 14.2-dataplaneapi
      options:
        - label: 'overlord.skydns:1'
        - label: 'overlord.skydns.group:revproxy'
        - label: 'overlord.skydns.interface:tailscale0'
```

metadata.yml:

```yml
kind: metadata
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - desktop
    - r2
metadata:
  haproxy.makejail: |
    ARG haproxy_tag=13.5
    ARG haproxy_ajspec=gh+AppJail-makejails/haproxy

    OPTION start
    OPTION overwrite=force
    OPTION copydir=${OVERLORD_METADATA}
    OPTION file=/haproxy.conf

    FROM --entrypoint "${haproxy_ajspec}" haproxy:${haproxy_tag}

    INCLUDE gh+DtxdF/efficient-makejail

    SYSRC haproxy_enable=YES
    SYSRC haproxy_config=/haproxy.conf

    SERVICE haproxy start

    STOP

    STAGE start

    WORKDIR /dataplaneapi

    RUN daemon \
            -r \
            -t "Data Plane API" \
            -P .master \
            -p .pid \
            -o .log \
                ./dataplaneapi \
                    -f /usr/local/etc/dataplaneapi.yml \
                    --host=0.0.0.0 \
                    --port=5555 \
                    --spoe-dir=/usr/local/etc/haproxy/spoe \
                    --haproxy-bin=/usr/local/sbin/haproxy \
                    --reload-cmd="service haproxy reload" \
                    --restart-cmd="service haproxy restart" \
                    --status-cmd="service haproxy status" \
                    --maps-dir=/usr/local/etc/haproxy/maps \
                    --config-file=/haproxy.conf \
                    --ssl-certs-dir=/usr/local/etc/haproxy/ssl \
                    --general-storage-dir=/usr/local/etc/haproxy/general \
                    --dataplane-storage-dir=/usr/local/etc/haproxy/dataplane \
                    --log-to=file \
                    --userlist=dataplaneapi
  haproxy.conf: |
    userlist dataplaneapi
      user admin insecure-password cuwBvS5XMphtCNuC

    global
      daemon
      log 127.0.0.1:514 local0
      log-tag HAProxy

    defaults
      mode http
      log global
      option httplog
      timeout client 30s
      timeout server 50s
      timeout connect 10s
      timeout http-request 10s

    frontend web
      bind :80
      default_backend web

    backend web
      option httpchk HEAD /
      balance roundrobin
```

我们即将在两台服务器上部署 HAProxy / Data Plane API。这样做的原因是为了避免出现 SPOF（单点故障）。至少在任意时刻，如果某个 HAProxy / Data Plane API 实例宕机，另一个实例仍能提供服务。

然而，Overlord 只能指向一个 Data Plane API 实例。因此，如果我们使用如下的两台服务器，就必须在每台机器上指定不同的 Data Plane API 实例。

如你在 [Director](https://github.com/DtxdF/director) 文件中所见，我们使用了 SkyDNS，这样客户端就可以使用 `revproxy.overlord.lan` 这个域名，而无需直接使用各自的 IP 地址。其优势在于，即使某个 HAProxy / Data Plane API 实例宕机，另一个仍可接手，客户端也能继续向其发送请求。

```yml
$ overlord apply -f metadata.yml
$ overlord apply -f haproxy.yml
$ overlord get-info -f haproxy.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    load-balancer:
      state: DONE
      last_log: 2025-04-23_17h04m01s
      locked: False
      services:
        - {'name': 'haproxy', 'status': 0, 'jail': '8d92fc6d2d'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 2 minutes and 20.12 seconds
        job_id: 30
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             haproxy:
               error: False
               message: None
         skydns:
           services:
             haproxy:
               error: False
               message: (project:load-balancer, service:haproxy, records:[address:True,ptr:None,srv:None] records has been updated.
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: r2
  labels:
    - all
    - r2
    - services
  projects:
    load-balancer:
      state: DONE
      last_log: 2025-04-23_17h04m02s
      locked: False
      services:
        - {'name': 'haproxy', 'status': 0, 'jail': '05c589c8a1'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 2 minutes and 53.27 seconds
        job_id: 10
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             haproxy:
               error: False
               message: None
         skydns:
           services:
             haproxy:
               error: False
               message: (project:load-balancer, service:haproxy, records:[address:True,ptr:None,srv:None] records has been updated.
```

**/usr/local/etc/overlord.yml** (centralita) 总机

```yml
dataplaneapi:
auth:
username: 'admin'
password: 'cuwBvS5XMphtCNuC'
entrypoint: 'http://100.65.139.52:5555'
```

**/usr/local/etc/overlord.yml** (provider): 提供者

```yml
dataplaneapi:
  auth:
    username: 'admin'
    password: 'cuwBvS5XMphtCNuC'
  entrypoint: 'http://100.65.139.52:5555'
```

hello-http.yml:

```yml
kind: directorProject
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - centralita
    - provider
projectName: hello-http
projectFile: |
  options:
    - virtualnet: ':<random> default'
    - nat:
  services:
    darkhttpd:
      makejail: 'gh+DtxdF/hello-http-makejail'
      options:
        - expose: '9128:80 ext_if:tailscale0 on_if:tailscale0'
        - label: 'overlord.load-balancer:1'
        - label: 'overlord.load-balancer.backend:web'
        - label: 'overlord.load-balancer.interface:tailscale0'
        - label: 'overlord.load-balancer.interface.port:9128'
        - label: 'overlord.load-balancer.set.check:"enabled"'
      arguments:
        - darkhttpd_tag: 14.2
```

收获！

```yml
$ overlord apply -f hello-http.yml
$ overlord get-info -f hello-http.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: provider
  labels:
    - all
    - provider
    - vm-only
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_17h57m16s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': '79f16243de'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 1 minute and 22.1 seconds
        job_id: 1
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, backend:web, serverid:fa8f94b1-6b2b-4cb4-a808-e9da46014c86, code:202, transaction_id:8fcf5d67-12df-4fcd-a2e3-1f5f18fe1844, commit:1) server has been successfully added.
         skydns:
           services:
             darkhttpd:
               error: False
               message: None
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: centralita
  labels:
    - all
    - centralita
    - services
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_17h57m16s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': '52dfa071cb'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 1 minute and 19.53 seconds
        job_id: 15
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, backend:web, serverid:f4b9e170-67bb-403e-88da-112c55b45fce, code:202, transaction_id:0fa886c8-68a6-4716-aa6b-824aa3e776ad, commit:1) server has been successfully added.
         skydns:
           services:
             darkhttpd:
               error: False
               message: None
$ curl http://revproxy.overlord.lan
Hello, world!
UUID: 8579af73-7d11-40b3-8444-6dac62e34b8e
$ curl http://revproxy.overlord.lan
Hello, world!
UUID: e463b1d5-13eb-4f04-9b0a-caf4339a8058
```

## 横向自动扩展

即使在拥有数百台服务器的情况下，部署项目也变得非常容易。然而，这种方式的问题在于资源的浪费，因为客户端很可能只使用了集群资源的不到 5%；或者相反，你可能将项目部署在少数几台你认为“够用”的服务器上，直到某一时刻你发现资源根本不够用，更糟糕的是，这些服务器还可能因各种原因随时宕机。这正是 Overlord 的自动扩展机制能够解决的问题。

hello-http.yml:

```yml
kind: directorProject
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - desktop
projectName: hello-http
projectFile: |
  options:
    - virtualnet: ':<random> default'
    - nat:
  services:
    darkhttpd:
      makejail: 'gh+DtxdF/hello-http-makejail'
      options:
        - expose: '9128:80 ext_if:tailscale0 on_if:tailscale0'
        - label: 'overlord.load-balancer:1'
        - label: 'overlord.load-balancer.backend:web'
        - label: 'overlord.load-balancer.interface:tailscale0'
        - label: 'overlord.load-balancer.interface.port:9128'
        - label: 'overlord.load-balancer.set.check:"enabled"'
      arguments:
        - darkhttpd_tag: 14.2
autoScale:
  replicas:
    min: 3
  labels:
    - services
    - provider
```

正如你可能已经注意到的，我们指定了两种类型的标签。这体现了自动扩缩部署与非自动扩缩部署之间的细微差别。与非自动扩缩部署不同，`deployIn.labels` 中的标签用于匹配相应的服务器以进行自动扩缩和监控，换句话说，匹配这些标签（在本例中为 `desktop`）的服务器将负责部署、监控，和在必要时重新部署。另一方面，匹配 `autoScale.labels` 中标签（在本例中为 `services` 和 `provider`）的服务器则用于以非自动扩缩部署的方式部署项目。我们已指定该项目至少包含三个副本。我们还可以指定其他内容，例如 **rctl(8)** 规则，但为了简洁起见，这样就足够了。

```yml
$ overlord apply -f hello-http.yml
$ overlord get-info -f hello-http.yml -t projects --filter-per-project --use-autoscale-labels
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_19h36m17s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': '0524bcf91b'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 58.24 seconds
        job_id: 31
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, backend:web, serverid:0d67d160-61af-4810-b277-5fb9e20da8eb, code:202, transaction_id:baa5b939-f724-4bd3-9d65-2ef769def3f5, commit:1) server has been successfully added.
         skydns:
           services:
             darkhttpd:
               error: False
               message: None
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: provider
  labels:
    - all
    - provider
    - vm-only
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_20h00m11s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': '2c2d22d2a5'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 4 minutes and 46.3 seconds
        job_id: 6
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, backend:web, serverid:fa8f94b1-6b2b-4cb4-a808-e9da46014c86, code:202, transaction_id:6792e6fe-a
778-44a7-b23a-1b2c23fe5904, commit:1) server has been successfully updated.
         skydns:
           services:
             darkhttpd:
               error: False
               message: None
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: centralita
  labels:
    - all
    - centralita
    - services
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_20h04m25s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': 'a6549318ce'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 33.34 seconds
        job_id: 21
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, backend:web, serverid:f4b9e170-67bb-403e-88da-112c55b45fce, code:202, transaction_id:00e632ce-c215-4784-9e61-9507d914ba6a, commit:1) server has been successfully updated.
         skydns:
           services:
             darkhttpd:
               error: False
               message: None
$ overlord get-info -f hello-http.yml -t autoscale --filter-per-project
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    hello-http:
      autoScale:
        last_update: 7.65 seconds
        operation: COMPLETED
        output:
         message: None
$ curl http://revproxy.overlord.lan
Hello, world!
UUID: 08951a86-2aef-4e85-9bfc-7fe68b5cc62d
$ curl http://revproxy.overlord.lan
Hello, world!
UUID: 5a06a89d-6109-438e-bc04-1ef739473994
```

假设 `provider` 中的服务由于某种原因宕机。

```yml
$ overlord get-info -f hello-http.yml -t projects --filter-per-project --use-autoscale-labels
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_19h36m17s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': '0524bcf91b'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 13 minutes and 37.64 seconds
        job_id: 32
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, backend:web, serverid:0d67d160-61af-4810-b277-5fb9e20da8eb, code:202, transaction_id:a2ba93d7-6ce6-4a36-aab2-09be13a00c17, commit:1) server has been successfully updated.
         skydns:
           services:
             darkhttpd:
               error: False
               message: None
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: provider
  labels:
    - all
    - provider
    - vm-only
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_20h11m58s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 66, 'jail': '2c2d22d2a5'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 1 minute and 13.9 seconds
        job_id: 7
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, backend:web, serverid:fa8f94b1-6b2b-4cb4-a808-e9da46014c86, code:202, transaction_id:b19e7997-871c-4293-a8a9-51ce03f2bbaa, commit:1) server has been successfully updated.
         skydns:
           services:
             darkhttpd:
               error: False
               message: None
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: centralita
  labels:
    - all
    - centralita
    - services
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_20h04m25s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': 'a6549318ce'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 8 minutes and 29.3 seconds
        job_id: 21
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, backend:web, serverid:f4b9e170-67bb-403e-88da-112c55b45fce, code:202, transaction_id:00e632ce-c215-4784-9e61-9507d914ba6a, commit:1) server has been successfully updated.
         skydns:
           services:
             darkhttpd:
               error: False
               message: None
```

在没有任何人工干预的情况下，看看魔法是如何发生的。

```yml
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_19h36m17s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': '0524bcf91b'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 14 minutes and 30.7 seconds
        job_id: 32
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, backend:web, serverid:0d67d160-61af-4810-b277-5fb9e20da8eb, code:202, transaction_id:a2ba93d7-6ce6-4a36-aab2-09be13a00c17, commit:1) server has been successfully updated.
         skydns:
           services:
             darkhttpd:
               error: False
               message: None
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: provider
  labels:
    - all
    - provider
    - vm-only
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_20h13m37s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': '2c2d22d2a5'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 27.47 seconds
        job_id: 8
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, backend:web, serverid:fa8f94b1-6b2b-4cb4-a808-e9da46014c86, code:202, transaction_id:98ca3a6a-65e4-450b-a4c3-4f135e36be37, commit:1) server has been successfully updated.
         skydns:
           services:
             darkhttpd:
               error: False
               message: None
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: centralita
  labels:
    - all
    - centralita
    - services
  projects:
    hello-http:
      state: DONE
      last_log: 2025-04-23_20h04m25s
      locked: False
      services:
        - {'name': 'darkhttpd', 'status': 0, 'jail': 'a6549318ce'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 9 minutes and 22.37 seconds
        job_id: 21
        restarted: False
        labels:
         error: False
         message: None
         load-balancer:
           services:
             darkhttpd:
               error: False
               message: (project:hello-http, service:darkhttpd, backend:web, serverid:f4b9e170-67bb-403e-88da-112c55b45fce, code:202, transaction_id:00e632ce-c215-4784-9e61-9507d914ba6a, commit:1) server has been successfully updated.
         skydns:
           services:
             darkhttpd:
               error: False
               message: None
```

服务已经恢复运行。

## 信息、指标及更多内容……

多亏了 [AppJail](https://github.com/DtxdF/AppJail)，我们可以从 Jail 中获取大量信息。Overlord 有一个特殊的部署方式叫做 `readOnly`，它可以与 `get-info` 命令完美结合。

info.yml:

```yml
kind: readOnly
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - all
$ overlord get-info -f info.yml -t projects --filter adguardhome
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: centralita
  labels:
    - all
    - centralita
    - services
  projects:
    adguardhome:
      state: DONE
      last_log: 2025-04-07_17h32m40s
      locked: False
      services:
        - {'name': 'server', 'status': 0, 'jail': '2a67806954'}
$ overlord get-info -f info.yml -t jails --filter 2a67806954
datacenter: http://127.0.0.1:8888
  entrypoint: main
  chain: centralita
  labels:
    - all
    - centralita
    - services
  jails:
    2a67806954:
      stats:
        cputime: 91
        datasize: 8400896 (8.01 MiB)
        stacksize: 0 (0 bytes)
        coredumpsize: 0 (0 bytes)
        memoryuse: 75104256 (71.62 MiB)
        memorylocked: 0 (0 bytes)
        maxproc: 4
        openfiles: 296
        vmemoryuse: 1367982080 (1.27 GiB)
        pseudoterminals: 0
        swapuse: 0 (0 bytes)
        nthr: 13
        msgqqueued: 0
        msgqsize: 0
        nmsgq: 0
        nsem: 0
        nsemop: 0
        nshm: 0
        shmsize: 0 (0 bytes)
        wallclock: 363548
        pcpu: 0
        readbps: 0 (0 bytes)
        writebps: 0 (0 bytes)
        readiops: 0
        writeiops: 0
      info:
        name: 2a67806954
        network_ip4: 10.0.0.3
        ports: 53/tcp,53/udp,53/tcp,53/udp
        status: UP
        type: thin
        version: 14.2-RELEASE
      cpuset: 0, 1
      expose:
        - {'enabled': '1', 'name': None, 'network_name': 'ajnet', 'hport': '53', 'jport': '53', 'protocol': 'udp', 'ext_if': 'tailscale0', 'on_if': 'tailscale0', 'nro': '3'}
        - {'enabled': '1', 'name': None, 'network_name': 'ajnet', 'hport': '53', 'jport': '53', 'protocol': 'tcp', 'ext_if': 'jext', 'on_if': 'jext', 'nro': '0'}
        - {'enabled': '1', 'name': None, 'network_name': 'ajnet', 'hport': '53', 'jport': '53', 'protocol': 'tcp', 'ext_if': 'tailscale0', 'on_if': 'tailscale0', 'nro': '2'}
        - {'enabled': '1', 'name': None, 'network_name': 'ajnet', 'hport': '53', 'jport': '53', 'protocol': 'udp', 'ext_if': 'jext', 'on_if': 'jext', 'nro': '1'}
      fstab:
        - {'enabled': '1', 'name': None, 'device': '/var/appjail-volumes/adguardhome/db', 'mountpoint': 'adguardhome-db', 'type': '<volumefs>', 'options': 'rw', 'dump': '0', 'pass': None, 'nro': '0'}
      labels:
        - {'value': '1', 'name': 'overlord.skydns'}
        - {'value': 'adguardhome', 'name': 'appjail.dns.alt-name'}
        - {'value': 'tailscale0', 'name': 'overlord.skydns.interface'}
        - {'value': 'adguardhome', 'name': 'overlord.skydns.group'}
      nat:
        - {'rule': 'nat on "jext" from 10.0.0.3 to any -> ("jext:0")', 'network': 'ajnet'}
      volumes:
        - {'mountpoint': 'usr/local/etc/AdGuardHome.yaml', 'type': '<pseudofs>', 'uid': None, 'gid': None, 'perm': '644', 'name': 'adguardhome-conf'}
        - {'mountpoint': '/var/db/adguardhome', 'type': '<pseudofs>', 'uid': None, 'gid': None, 'perm': '750', 'name': 'adguardhome-db'}
```

## 后续工作

Overlord 能为你做的事情远不止本文件所展示的内容，更多示例请参见 [Wiki](https://github.com/DtxdF/overlord/wiki)。

Overlord 是一个较新的项目，仍有很大的改进空间，未来也将加入更多功能以提升可用性。如果你愿意支持该项目，请 [考虑捐款](https://www.patreon.com/AppJail)。

***

**Jesús Daniel Colmenares Oviedo** 是一位系统管理员、开发者及 Ports 提交者，同时也是 AppJail 和它的相关项目（如 Director、Makejails、Reproduce、Overlord 等）的创建者。


# Netgraph 大众教程

将 FreeBSD 强大的网络框架带给所有运行 Jail 和虚拟机操作系统的用户

* 原文：[Netgraph for the Rest of Us](https://freebsdfoundation.org/our-work/journal/browser-based-edition/networking-3/netgraph-for-the-rest-of-us/)
* 作者：Daniel J. Bell

在几年前，我写过一篇文章，讨论如何在数据中心中利用 FreeBSD 作为一种高性能、低成本的云计算替代方案，依靠的是 FreeBSD 基本系统中的工具。随着我的基础设施不断扩展，我开始探索提升性能和管理效率的方法，而构建理想的网络栈成为我的主要目标之一。这促使我尝试使用 Netgraph，而我对它的喜爱程度甚至让我基于 rc 服务机制创建了一个简化其基础配置的工具 —— **ngbuddy(8)**。

Netgraph 是款网络工具包，在 1999 年被引入了 FreeBSD，它采用“节点与连接”的结构，可用于构建复杂而强大的配置，适用于各类特定的网络工作流。你可以将它想象成一个模块化的搭建系统：节点（即网络构件）通过钩子（即连接节点或其他网络对象的链路）连接在一起，从而构建出多层交换机、流量整形器或自定义虚拟网络。最常见的用途，是将虚拟机和 Jail guest 连接到宿主机的物理或虚拟网络上，但 Netgraph 也支持以复杂而有趣的方式组合这些组件。举个例子：我在 BSDCan 遇到的一位 FreeBSD 专家，就利用巧妙的 Netgraph 配置，在多个物理链路上“镜像”流量，就像 RAID 那样冗余地传输，从而极大提升了关键 VoIP 业务的稳定性与弹性。

我将在本文中介绍 Netgraph 的核心概念，探讨像 `ngbuddy` 和 `jng` 这样的简化配置工具，演示其底层机制的手动配置过程，并提供进一步探索的资源。无论你是刚接触 FreeBSD 网络的新手，还是想深入研究的资深管理员，都能从中获得切实可用的方法，尝试解锁这一强大的网络框架。

## 为什么 Netgraph 很棒（但也有局限）

在 FreeBSD 中，为虚拟机（VM）和 Jail 设置网络的传统方式通常依赖于接口 **if\_bridge(4)**、**tap(4)** 和 **epair(4)**。这种做法已经非常成熟，而且还在持续优化：例如在 2020 年，`if_bridge` 获得了一次巨大的性能提升。但不到一年后，FreeBSD 13 为 bhyve 引入了对 Netgraph 的支持，从而为用户提供了一项非常有吸引力的替代方案。

使用 Netgraph 有充分的理由。老实说，如果 `if_bridge` 始终可以跑得更快，我可能不会考虑它。但我在几个不同年代的服务器上，针对 FreeBSD 14.3 Jail 和 bhyve VM 之间的网络通信做了一些简单的 `iperf3` 性能测试，对比了 `if_bridge/tap/epair` 与纯 Netgraph 通信方式。尽管测试不够科学严谨，Netgraph 无论在哪种硬件上都始终表现更快。

![性能对比图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-e30afab11a5bc1ba847081a8d8ab2752b0a7e25d%2FUs-01.png?alt=media)

当然，在理想条件之外，Netgraph 在高包速场景下的扩展性会受到限制。其瓶颈主要在于钩子查找机制的锁竞争。目前已经有一个解决方案正在开发中，希望能赶上 FreeBSD 15，但在那之前，对于像 CDN 前端这种需要高吞吐量的应用来说，Netgraph 仍然不是最佳选择。

不过，对于我们正在讨论的虚拟化密度（每台主机运行几十个混合用途的 bhyve VM 或 Jail），Netgraph 的表现是稳定且值得信赖的。在我多个生产环境中的类似部署中，Netgraph 多年来一直保持着优异的吞吐量和稳定性。

除了性能之外，Netgraph 还有一些明显优势：

* **配置更清爽：** 没有太多 ifconfig 虚拟接口的杂乱，命令更聚焦，环境更干净
* **内建指标：** 易于访问的流量统计，便于监控与排错
* **极高的灵活性：** 通过 Netgraph 你可以使用其完整的网络模块功能集
* **可视化能力：** 拓扑是图结构，容易理解主机间的网络结构，还有内建工具可以可视化网络图

Netgraph 的主要劣势就是：**上手门槛较高**。它在 FreeBSD 中拥有 40 余篇 man 页面，涵盖各种概念、工具和内核模块，新手一眼望过去可能完全不知道从哪里开始。但实际上，大多数常见的场景只需要掌握其中少数几个要点就能用起来。

## 学习 Netgraph 基础：节点与钩子

首先，进入节点（nodes）与钩子（hooks）的思维模式。

* **节点（Nodes）**：这些是执行特定网络功能的处理模块。
* **钩子（Hooks）**：节点上的连接点，允许数据在节点之间流动。

要在 bhyve 和 Jail 中配置基础网络，你应该了解以下几种节点类型：

* **ng\_bridge**：虚拟交换机，用于将各个节点连接起来以实现通信。
* **ng\_ether**：Netgraph 对现有以太网接口的表示，例如将虚拟设备桥接到物理网络时使用。
* **ng\_eiface**：由 Netgraph 创建的虚拟以太网接口，适用于 Jail 的网络接口。
* **ng\_socket**：供用户进程（如 bhyve）与 Netgraph 交互的连接节点。

Netgraph 的强大之处在于这些组件之间的连接方式。例如，你可以创建虚拟交换机（ng\_bridge），将物理接口（ng\_ether）与虚拟接口（ng\_eiface）连接起来，用于 Jail 或虚拟机（VM）。你甚至可以多层嵌套桥接，在单个 FreeBSD 系统上优雅地建模复杂的网络拓扑结构。

## 使用 ngbuddy 简化 Netgraph

尽管 Netgraph 功能强大，但创建和管理节点的语法可能有些繁琐。它的精确性要求你明确指定某些事项，而这些在仅使用 **ifconfig(8)** 时是自动处理的。我编写了 ngbuddy（Netgraph Buddy）工具，用于自动化混合 VM 与 Jail 环境下的配置，并确保这些更改在系统启动时持久生效。

在接下来的示例中，我们将从一个全新的 FreeBSD 14 安装开始，创建并使用一个“公共”的以太网连接桥接网络和一个“私有”的主机专用桥接网络，这类似于流行的虚拟机管理工具中的默认网络设置。我们还将使用 vm-bhyve（它在 2022 年 7 月添加了对 Netgraph 的支持）来进一步简化虚拟机管理。

## 设置基础虚拟网络

我们将模仿传统的虚拟网络设置方式，用 `if_bridge`、`tap` 和 `epair` 接口来为 Jail 或 VM 创建虚拟网络。借助 Netgraph 与 ngbuddy，我们将改为配置 `ng_bridge` 节点，将 `public` 桥接连接到主机上与默认路由关联的接口。

首先，从 Ports 安装 ngbuddy：

```sh
# pkg install sysutils/ngbuddy
```

然后启用服务以创建一个基础配置：

```sh
# service ngbuddy enable
Adding default bridges.
ngbuddy_public_if: -> ix0
ngbuddy_private_if: -> nghost0
```

这将创建两个桥接：

* **public**：连接到物理接口（此示例中为 ix0）
* **private**：连接到一个新的虚拟接口（nghost0），用于主机专用网络（host-only networking）

如果你希望使用不同的桥接名称（我更偏好如“lanbridge”这类更具体的名字），可以编辑 **/etc/rc.conf** 中的 ngbuddy 配置行以符合你的偏好。

启动服务以创建 Netgraph 桥接节点：

```sh
# service ngbuddy start
Created 3 links.
```

此时，`public` 交换机已经就绪，应像连接到主机网络的任意设备一样工作。对于 `private` 主机专用网络，你很可能还需要配置其他网络组件，通常包括为 `nghost0` 接口设置 NAT 规则集和 DHCP 服务器。你可以像配置标准接口一样用 `ifconfig` 命令或向 **/etc/rc.conf** 添加对应条目来配置 `nghost0`。

## 配置 vm-bhyve

如果你使用的是 vm-bhyve 1.5.0 及更新版本，可以利用 ngbuddy 来配置虚拟交换机（注意不要与已有的 vm-bhyve 桥接名称冲突）：

```sh
# service ngbuddy vmconf
```

这将向你的 vm-bhyve 配置文件（定义于 **$VM\_DIR/.config/system.conf**）添加以下内容：

```sh
switch_list="public private"
type_public="netgraph"
type_private="netgraph"
```

完成此配置后，vm-bhyve 就可以使用了。在配置你的 VM 时，只需在每个 VM 的配置中包含相应的交换机名称，例如：

```sh
network0_switch="public"
```

## 为 Jail 创建接口

在你的 Jail 配置中添加或替换以下行，以便在 Jail 启动时自动创建 `ng_eiface` 设备：

```ini
my_jail_name {
   if_name = "$name";
   $bridge = "public";
   vnet.interface = "$if_name";
   exec.prestart = "service ngbuddy jail $if_name $bridge";
   exec.prestop = "service ngbuddy unjail $if_name $name";
…
```

这个示例中使用 Jail 名称变量 `$name` 作为接口名，以保证名称的唯一性与一致性。你也可以采用任何适合你环境的接口命名方案，只要每个接口的名称都是唯一的即可。

## 监控流量

使用 ngbuddy 可以轻松监控 Netgraph 节点的流量，它提供了一个简化版的 `ngctl … getstats` 命令视图，展示所有已检测到的接口的流量情况：

```sh
# service ngbuddy status
public
  vtnet0 (upper): RX 1.25 MB, TX 4.37 MB
  vtnet0 (lower): RX 4.37 MB, TX 1.25 MB
  jail1: RX 256.32 KB, TX 128.16 KB
private
  nghost0: RX 0B, TX 0B
  jail2: RX 0B, TX 0B
```

这提供了每个接口上流量的快速总览，对于故障排查与性能监控非常有用。你还可以使用 `service ngbuddy status` 或 `service ngbuddy vmname` 命令来识别 vm-bhyve 节点，否则它们在 `ngctl` 输出中将以“unnamed”的 `ng_socket` 设备形式出现。

## 可视化你的网络

我们可以使用 `ngctl dot` 命令快速生成主机虚拟网络的图形表示，该命令会输出如下的有向图文本：

```sh
graph netgraph {
    edge [ weight = 1.0 ];
    node [ shape = record, fontsize = 12 ] {
       "45" [ label = "{nghost0:|{eiface|[45]:}}" ];
       "49" [ label = "{private:|{bridge|[49]:}}" ];
       "4e" [ label = "{public:|{bridge|[4e]:}}" ];
       "52" [ label = "{jpub1:|{eiface|[52]:}}" ];
       "12" [ label = "{bge0:|{ether|[12]:}}" ];
       "13" [ label = "{bge0_42:|{ether|[13]:}}" ];
       "7b" [ label = "{vmpriv:|{socket|[7b]:}}" ];
    };
    subgraph cluster_disconnected {
       bgcolor = pink;
       "12";
    };
    node [ shape = octagon, fontsize = 10 ] {
       "45.ether" [ label = "ether" ];
    };
…
```

这段文本可以直接用 **dot(1)** 命令（来自 Port `graphics/graphviz`）转化为 SVG 或 PNG 图像。下面是简单 Netgraph 配置的清理后输出示例。

![简单 Netgraph 配置的清理后输出示例](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-8147a275b30cc6dc5354ca9638c713c7a1b1f5b7%2FUs-02.png?alt=media)

虽然这个输出已经很不错，ngbuddy 还包含一个我编写的辅助脚本 `ngbuddy-mmd.awk`，可以将上述输出转换为更美观、更清晰的 mermaid-js 可视化图形。我们公司的文档系统支持 mermaid-js，因此借助一些 API 魔法，我总能在文档数据库中保持所有主机 Netgraph 拓扑图的实时更新。

![Netgraph 拓扑 mermaid 可视化图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-38db89b6031a8236e74bfefeaa9cad9a147c804b%2FUs-03.png?alt=media)

该图示在顶部标注了主机名，中间是 public 与 private 桥接的图形，下方是 Jail（椭圆）和 VM（圆柱）。这种可视化在你需要管理带有不同租户网络和不同功能的服务器时尤其有用。

## 管理 MAC 地址冲突

无论我使用哪种虚拟网络技术，MAC 地址冲突总是让我头疼。即使是在只有不超过 5 台 FreeBSD 主机的小型家庭实验室中，也曾有过几个下午苦苦排查，最后才发现原来是某些 Jail 争用了同一个 MAC 地址。为缓解此问题，并确保 MAC 地址的确定性生成（这对于虚拟机迁移非常关键），你可以在所有运行 ngbuddy 的主机上执行以下命令：

```sh
sysrc ngbuddy_set_mac=YES
sysrc ngbuddy_set_mac_prefix=02
```

此配置会让 ngbuddy 为它创建的接口分配唯一的 MAC 地址，使用你指定的前缀。前缀 `02` 表示这是一个本地管理地址（locally administered address），这非常适合在你完全控制的虚拟环境中使用。

更多技巧和功能，请参见 ngbuddy 的联机手册（man page）。

## 使用 jng：无需 Ports

`jng` 工具仍是最简单的方式，用于在 Jail 中快速启用 Netgraph，并且它是 FreeBSD 系统自带的。该脚本会在需要时智能地创建桥接，非常适合多数 Jail 主机的网络配置。首先，将 `jng` 脚本复制到你偏好的目录中：

```sh
install -m 755 /usr/share/examples/jails/jng /usr/local/sbin/jng
```

在 Jail 的配置中，可以像使用 ngbuddy 那样调用 `jng`。注意，`jng` 需要你指定想要桥接的物理接口名称。

```ini
my_jail_name {
        $if_uplink = "em0";
        $if_name = "ng0_$name";
        vnet.interface = "$if_name";
        exec.prestart += "jng bridge $if_name $if_uplink";
        exec.poststop += "jng shutdown $if_name";
…
```

如果你有兴趣自己编写网络工作流，不妨阅读 `jng` 脚本，它拥有非常优秀的文档注释。

## 以传统方式实现 Netgraph

现在我们来看看为何配置 Netgraph 有时会让人感到烦恼，并探索前文提到的这些工具实际上做了哪些底层操作。FreeBSD 的虚拟网络配置通常通过 `ifconfig` 完成，这些对象可以直接用 `ifconfig` 查看。而 Netgraph 创建的某些对象只能通过 `ngctl` 和其他支持 Netgraph 的工具看到，这一点可能会让人有些困惑。

此外，不同于 ifconfig 操作的网络接口，Netgraph 中的钩子之间可能存在复杂的连接关系，因此我们必须跟踪并记录每一个钩子。我们将手动创建前面提到的 `private` 与 `public` 的 `ng_bridge` 示例，并添加一个用于主机与虚拟 `private` 桥接通信的 `ng_eiface` 设备，构建一个主机专用网络。在这个例子中，我们将使用 `em0` 作为要桥接的物理接口。

```sh
# 加载 Netgraph 内核模块
kldload ng_ether ng_bridge

# 创建一个 ng_bridge，命名为 "private"
ngctl mkpeer ngeth0: bridge ether link0
ngctl name ngeth0:ether private

# 将 em0 上的流量共享给连接的 Netgraph 节点
ngctl msg em0: setpromisc 1

# 创建一个 ng_bridge 设备，命名为 "public"
ngctl mkpeer em0: bridge lower link1
ngctl name em0:lower public
ngctl connect em0: public: upper link2

# 关闭虚拟交换机无法正常处理的性能特性
ifconfig em0 -lro -tso

# 创建一个虚拟以太网设备 ngeth0（将会出现在 ifconfig 输出中）
ngctl mkpeer eiface ether ether
```

这里的关键在于，我们必须为每个对象创建并分配一个唯一的 `link#`，然后给它命名，才能有希望有效地跟踪管理它。接下来，我们再为一个 Jail 创建一个新的 `ng_eiface` 设备。

```sh
# 创建一个新接口，然后打印它的名字
ngctl mkpeer public: eiface link3 ether
ngctl show -n public:link3 | cut -w -f3
```

在这个例子中，我们的新接口名称将是 `ngeth1`。你可以像配置普通接口一样用 `ifconfig` 来配置它。如果这个新接口被用作 VNET Jail，它仍然可以通过与桥接的 Netgraph 连接正常通信。

虽然使用 Netgraph 手动配置的命令行操作可能并不比标准的 FreeBSD 虚拟网络多，但由于语法更复杂且需要跟踪 `link` 号，Netgraph 并不太适合手工频繁修改。

## 继续你的 Netgraph 之旅

Netgraph 是 FreeBSD 的一大隐藏宝石——一个强大的网络框架，一旦掌握，就能优雅地解决复杂的网络问题。虽然一开始可能感觉有些难以驾驭，但像 ngbuddy 和 jng 这类工具让它变得对各级管理员都更友好。

无论你是在管理小型家庭实验室还是复杂的生产环境，Netgraph 都能提供灵活强大的功能，帮助你构建精准符合需求的网络拓扑。它的性能优势、更清晰的配置方式和可视化能力，都使其值得在虚拟化基础设施中考虑使用。我鼓励你深入探索这个强大的子系统，发现它如何简化你的虚拟网络搭建。

## 资源

* [Netgraph 在线手册](https://man.freebsd.org/cgi/man.cgi?query=netgraph\&sektion=4)（man 4 netgraph）
* [ngbuddy GitHub 仓库](https://github.com/bellhyve/ngbuddy)（包含完整的 jail.conf 示例）
* [FreeBSD 手册：Jails 与容器](https://docs.freebsd.org/en/books/handbook/jails/)
* [FreeBSD 手册：虚拟化](https://docs.freebsd.org/en/books/handbook/virtualization/)
* [测试征集：Jail 与 bhyve 用户招募](https://callfortesting.org/)

\--

**Daniel J. Bell** 是 Bell Tower 的创始人，Bell Tower 是一家位于纽约市的混合云服务提供商，专注于云效率、合规监管和基于 ZFS 的高级存储。Daniel 积极参与开源项目并在多场会议上发表演讲，包括 2024 年 BSDCan 大会上关于 ZFS 和 Netgraph 的主题演讲。


# FreeBSD WiFi 开发第一部分——尝试 WiFi

* 原文：[FreeBSD WiFi Development Part 1 - Experimenting with WiFi](https://freebsdfoundation.org/our-work/journal/browser-based-edition/networking-3/freebsd-wifi-development/)
* 作者：Tom Jones

我在过去六个月中一直在 FreeBSD 上从事 WiFi 相关工作，该项目由 FreeBSD 基金会资助。这个项目的主要成果是将 OpenBSD 的 `iwx` 驱动移植到 FreeBSD，用于支持 Intel 802.11ac/ax 网卡。通过这个项目，我接触到了 WiFi 协议栈的大部分内容，也让我深刻认识到，我们需要更多的人参与 WiFi 的开发，同时也需要更简单的途径让新人能够开始开发。

WiFi 出现已经有 25 年了，已经成为我们生活的核心部分，以至于人们进入一个新地方的第一个问题往往是“这有 WiFi 吗？”

FreeBSD 缺乏 WiFi 驱动。在过去五年中，引入了带有 WiFi 支持的 linuxkpi 层，到 2025 年，这些驱动已经开始支持数百兆比特的 IEEE 802.11ac 速度。市面上的 WiFi 7 设备已经能达到 2Gbit 的速度。

FreeBSD 处于落后状态，这是无可争辩的事实。我们需要更多的人投入到 WiFi 相关的开发中。如果你对操作系统开发感兴趣，我想没有比参与改进 FreeBSD WiFi 协议栈更好的选择了。虽然这不会是最容易的事情，但正因为落后，我们有大量难度不一的开放任务，迫切需要更多贡献者来追赶。

本系列文章将讲述在 FreeBSD 上开发 WiFi 的路径。关于驱动开发的底层细节和 LLVM 编译优化等内容，已经有很多优秀的资料。本篇作为三篇文章中的第一篇，将解释相关术语，并演示如何在 FreeBSD 上配置 WLAN 接口用于测试。这个最小化配置足以开始发现问题。后续文章将讨论 WiFi 驱动的工作原理，和它们如何与 FreeBSD 中更大的 `net80211` 协议栈交互。

## 术语

网络通信就是信息的有效传输和准确接收。我在互联网标准的开发和撰写方面工作了十年，但即使是通信领域的专家，我们仍然没能完全讲清楚通信的全貌。从 IETF 背景来看，WiFi 是那个让电子跳舞的怪异 IEEE 的东西。

要实现良好的通信，我们需要对术语达成共识，这让我想起多年前在奥斯陆大学（**译者注：这是挪威最大及最古老的大学**）食堂与一位德国同事的对话。

**我：** “我很确定它是 why-phi，就是 wireless fidelity（无线保真），就像 high fidelity（高保真）那样。”

**他：** “不，是 wee-fee（**译者注：发音相近**），像 hee-fee。”

在这些文章中我会用到很多缩写和术语，这是避免不了的。遇到不懂的，搜索互联网是你的好帮手。我刚提到的两个缩写 IETF（互联网工程任务组）和 IEEE（电气电子工程师学会）中，本文重点会围绕 IEEE。遗憾的是，在文档和代码中，FreeBSD WiFi 基础设施源代码里对 IEEE、net80211 和 IEEE80211 的称呼反复出现多种变体。

WiFi 联盟（负责认证的组织）在标准（来自 IEEE）和品牌名之间造成了一团乱。使用标准名称更准确，比如 `IEEE80211n`（或者简称 `11n`、`n`）比用 WiFi 4 更清晰。无论用哪个术语，都能帮助你搞清楚事物的名称、产品功能，或者别人问你的内容。FreeBSD 往往会倾向于使用标准名称，甚至是文档修订版本（如果幸运的话），而非营销名称。

理解 WiFi 的概念就像记名字一样困难。我建议你阅读一些资料，Matthew Ghast 的第一本关于 WiFi 的书《802.11 Wireless Networks》（《802.11 无线网络权威指南》ISBN: 9787564103163）是非常好的入门读物，涵盖了所有主要概念，适合初学者。不用担心书的年代（2002），基础依旧，只是数字更大，调制方式更复杂罢了。

|                     术语                     |                                               解释                                              |
| :----------------------------------------: | :-------------------------------------------------------------------------------------------: |
|                    WiFi                    | IEEE 制定的一系列标准和 WiFi 联盟的市场品牌名称的总称。通俗来说，就是“你笔记本用来上网的那个东西”，这个定义对我们来说已经足够。如果有人过于纠正你的术语，那他并不是你的朋友。 |
|                  IEEE80211                 |                                  IEEE 802.11 是定义 WiFi 的标准家族。                                  |
|                  net80211                  |                             也称为协议栈，指 FreeBSD 中实现 IEEE80211 状态机的代码。                            |
|                  Band（频段）                  |                      客户端或接入点可能使用的频率范围。（例如常见的 2.4GHz 频段，2462 MHz 频率所在的频段）                      |
|                 Channel（信道）                |                                   一个射频频率和它的参数，有时与“频段”可互换使用。                                   |
|                 Station（站点）                |                                   你的设备、网络中的其他客户端（也是一种工作模式）。                                   |
|              Access Point（接入点）             |                                      你连接的网络设备（也是一种工作模式）。                                      |
|                Monitor（监视模式）               |                                   一种使网络适配器捕获该频段上所有数据包的工作模式。                                   |
|           Network Adapter（网络适配器）           |             含有所有无线电设备的设备，使你能够使用 WiFi（也称为网卡——虽然 USB 网卡不是卡，可能是网络接口，但最好避免混淆硬件和软件模型）。             |
|                 Driver（驱动）                 |                                   FreeBSD 中直接或通过固件与网卡通信的代码。                                   |
|                Firmware（固件）                |                          运行在网卡上的代码，帮你完成部分工作。一般只在需要由操作系统或驱动加载时才会特别提及。                          |
| MAC（Media Access Control Address，媒体访问控制地址） |                                  802.11 协议中负责共享媒介（介质访问控制）的部分。                                 |
|               Full MAC（全 MAC）              |                        用于驱动描述——全 MAC 设备实现了 MAC 层，net80211 做的许多工作可以被跳过。                        |
|                     HT                     |                               高吞吐量（High Throughput，也称为 802.11n）。                              |
|                     VHT                    |                   非常高吞吐量（Very High Throughput，也称为 802.11ac）（**译者注：WIFI5**）。                   |
|                     EHT                    |                 极高吞吐量（Extremely High Throughput，也称为 802.11ax）（**译者注：WIFI6**）。                 |

本文还涉及一些你需要熟悉的核心概念。我认为对大多数人来说，这就是他们日常网络访问的工作方式，但值得注意的是，一些技术对我们中的某些人（包括我）来说是从小就接触的，或者是我们这代人才被引入的（刚才让不少人感觉自己老了），这些技术其实比部分读者的年龄还要长十年。

大多数家庭中最常见的 WiFi 网络是一种接入点（AP），它作为站点（或客户端）通往更大互联网的网关。在许多部署场景中，很可能就是你的家中，接入点通常是一个路由器，负责将客户端的流量转发到互联网（可能通过调制解调器）、分配地址和执行其他大量网络任务。

要加入网络，站点需要经历一系列与接入点通信的状态。简而言之，它会：

* 扫描（scan）
* 探测（probe）
* 接收信标（receive beacons）
* 认证（authenticate）
* 关联（associate）
* 协商加密密钥（negotiate encryption keys）
* 获取 IP 地址（acquire an IP address）

除了最后一步外，其余步骤都是 WiFi 特有的，从某种角度看，这些步骤相当于你找到了网线、接口，并把电脑插入路由器。最后一步发生在 IP 层，使用的工具和有线网络一样。

所有这些阶段和状态都由 `net80211` 协议栈和设备驱动处理。具体谁来做什么取决于硬件支持，有些功能由网络适配器上的固件完成。当硬件无法完成时，`net80211` 协议栈可以自行实现大部分功能，但有些依赖无线电硬件的功能无法用软件模拟。

在 FreeBSD 上开发 WiFi 时，我们需要了解硬件和 `net80211` 层分别承担的任务。上面是简短的总结，实际上还有许多其他状态和认证模式。`IEEE80211` 标准已接近 30 年历史，拥有丰富的发展历程。

## 在 FreeBSD 上试验 WiFi

在开始阅读代码和做改动之前，我们先讨论一下如何在 FreeBSD 上管理 WLAN 适配器。

`net80211` 协议栈在网络适配器之上提供了一个抽象层，给我们虚拟接口。在代码中，这些接口被称为 VAP（虚拟访问点），具体功能可参考 `ieee80211_vap` 的手册页。

这意味着我们必须先从物理设备创建一个 WLAN 接口，才能使用。

相比 OpenBSD 中简单的接口管理命令（如 `ifconfig iwx0 up`），这种方式看起来有些笨重，但它能在单个适配器上实现虚拟功能。如果硬件支持，你甚至可以同时作为接入点和站点，或者同时作为站点和监控模式。

通常，这种管理工作由 `rc.conf` 中的配置自动完成，安装程序会添加类似如下行：

```ini
wlans_iwlwifi0="wlan0"
ifconfig_wlan0="WPA SYNCDHCP"
```

第一行让 `rc` 系统从 `iwlwifi0` 适配器创建一个名为 wlan0 的接口，第二行是类似有线接口的常见 `ifconfig` 配置。

内核开发时控制设备创建很有帮助，接下来我们先看看如何手动创建接口。

## 手动创建接口

可以通过读取 sysctl `net.wlan.devices` 来列出使用 `net80211` 注册的 WLAN 设备：

```sh
$ sysctl net.wlan.devices
net.wlan.devices: iwx0 rtwn0
```

在我的笔记本上，你能看到它有一块基于 `iwx` 的网卡（PCIe 上的 `iwx0`）和一块 `rtwn` 网卡（USB 上的 `rtwn0`）。

我们可以使用 ifconfig 命令从这些设备创建接口，如下所示：

```sh
# ifconfig wlan create wlandev iwx0
wlan0
# ifconfig wlan create wlandev rtwn0 wlanmode ap
wlan1
# ifconfig wlan create wlandev rtwn0 wlanmode monitor
wlan2
```

第一个例子中，我们在没有额外参数的情况下创建了 `wlan0`，默认工作模式是站点（station）。FreeBSD 中的 WLAN 设备在创建时只能工作于一种模式，具体支持哪些模式取决于硬件和驱动。

驱动支持的模式会在对应的手册页中列出。对比 `iwm`、`iwlwifi` 和 `iwx`（它们支持部分相同硬件），你会发现目前 `iwm` 和 `iwlwifi` 只能工作在站点模式，而 `iwx` 支持站点和监控模式。`iwx` 支持的 Intel 硬件能在 2.4GHz 频段有限度地作为主机接入点（host AP），但这部分支持尚未实现。

`rtwn` 驱动支持站点、adhoc、主机接入点（host AP）和监控模式。`net80211` 支持的完整模式列表可在 **ifconfig(8)** 手册页中查阅：

```sh
wlanmode mode
        指定此克隆设备的操作模式。mode 可为 sta、ahdemo（或 adhoc-demo）、ibss（或 adhoc）、AP（或 hostap）、wds、tdma、mesh 和 monitor。克隆接口的操作模式不能更改。tdma 模式实际上是具有特殊属性的 adhoc-demo 接口。
```

配置完成后，`ifconfig` 会显示一个接口，很多信息还未填充：

```sh
$ ifconfig wlan0
wlan0: flags=8802<BROADCAST,SIMPLEX,MULTICAST> metric 0 mtu 1500
        options=0
        ether e4:5e:37:af:13:5b
        groups: wlan
        ssid "" channel 1 (2412 MHz 11b)
        regdomain FCC country US authmode OPEN privacy OFF txpower 30
        bmiss 10 scanvalid 60 bgscan bgscanintvl 300 bgscanidle 250
        roam:rssi 7 roam:rate 1 wme bintval 0
        parent interface: iwx0
        media: IEEE 802.11 Wireless Ethernet autoselect (autoselect)
        status: no carrier
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
```

相比有线设备，`ifconfig` 输出中多了很多参数，我们来看几个关键的：

* `ssid "" channel 1 (2412 MHz 11b)` 作为站点，我们有一个指定的 SSID（这里为空），当前处于信道 1，频率和模式也显示出来了。
* `regdomain FCC country US authmode OPEN privacy OFF txpower 30` 监管域和区域代码默认设置为 FCC 和美国，启动接口后会更新以匹配本地监管域和区域。
* `bmiss 10 scanvalid 60 bgscan bgscanintvl 300 bgscanidle 250 roam:rssi 7 roam:rate 1 wme bintval 0` 驱动相关参数，控制扫描行为、网络切换和多媒体扩展（用于服务质量，不是播放 MP3 那个）。
* `parent interface: iwx0` 父接口，方便管理多个 WiFi 接口。
* `media: IEEE 802.11 Wireless Ethernet autoselect (autoselect)` 当前媒体模式，通常为自动选择，但调试或优化时可以强制设置。

如果看 `rtwn` 接口上创建的 VAP，虽然相似但略有不同，原因是它们的工作模式不同。

```sh
$ ifconfig wlan1
wlan1: flags=8802<BROADCAST,SIMPLEX,MULTICAST> metric 0 mtu 1500
        options=0
        ether 74:da:38:33:c0:62
        groups: wlan
        ssid "" channel 1 (2412 MHz 11b)
        regdomain FCC country US authmode OPEN privacy OFF txpower 30
        scanvalid 60 wme dtimperiod 1 -dfs bintval 0
        parent interface: rtwn0
        media: IEEE 802.11 Wireless Ethernet autoselect <hostap> (autoselect <hostap>)
        status: no carrier
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
$ ifconfig wlan2
wlan2: flags=8802<BROADCAST,SIMPLEX,MULTICAST> metric 0 mtu 1500
        options=0
        ether 74:da:38:33:c0:62
        groups: wlan
        ssid "" channel 1 (2412 MHz 11b)
        regdomain FCC country US authmode OPEN privacy OFF txpower 30
        scanvalid 60 wme bintval 0
        parent interface: rtwn0
        media: IEEE 802.11 Wireless Ethernet autoselect <monitor> (autoselect <monitor>)
        status: no carrier
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
```

当我们完成操作，或者如果不小心创建了错误模式的设备，可以用 ifconfig 来删除它：

```sh
# ifconfig wlan0 destroy
```

## 使用接口

我之前有点偷懒，FreeBSD 的 WiFi 栈主要由两个部分组成，但很多 WiFi 状态是由两个用户空间程序驱动的，分别是 `wpa_supplicant` 和 `hostapd`。

### 使用 `wpa_supplicant` 的站点模式

`wpa_supplicant` 最初是一款管理 WPA（无线保护访问）加密状态的程序，适用于站点模式下的设备。它已发展成为完整的用户空间 WiFi 管理接口。Linux 上无线配置常用 `wpa_supplicant`。

`hostapd` 是同一项目的主机接入点用户空间守护进程，功能与 `wpa_supplicant` 相似，但它处理的是主机任务而非客户端任务。

我们可以用 ifconfig 管理 WLAN 接口，以下命令会启用站点接口并配置它连接名为“Test”的 SSID：

```sh
# ifconfig wlan0 ssid "Test" up
```

这会让 FreeBSD 站点尝试关联到名为“Test”的接入点。FreeBSD 的 `net80211` 协议栈不直接支持 WPA 状态机，因此大多数网络需要配合使用 `wpa_supplicant`。之前提到的 `rc.conf` 第二行配置即用于启动接口上的 `wpa_supplicant` 并启用 DHCP。

手动运行 `wpa_supplicant` 需要配置文件，`wpa_supplicant.conf` 的手册页里有很多示例，当中最简单的配置文件如下：

```ini
ctrl_interface=/var/run/wpa_supplicant
ctrl_interface_group=wheel

network={
        ssid="Open Network"
        key_mgmt=NONE
}
```

然后可以这样启动 `wpa_supplicant`：

```sh
# wpa_supplicant -i wlan0 -c /etc/wpa_supplicant.conf -D bsd
```

在默认情况下，`wpa_supplicant` 会在前台运行，加上参数 `-B` 则会后台运行。可以通过控制接口获取它的日志信息。

`wpa_passphrase` 命令可用来为 `wpa_supplicant` 配置文件生成 WPA 网络配置，例如：

```ini
$ wpa_passphrase "Closed Network" superpassword
network={
        ssid="Closed Network"
        #psk="superpassword"
        psk=852c26a07d84c48e4bfeec71289214a39bcd9d881bc66aedf6a2d11372f59752
}
```

这个命令只生成添加到 `wpa_supplicant.conf` 所需的配置，免去学习复杂语法的麻烦。

FreeBSD 自带 `wpa_cli` 工具，用于与 `wpa_supplicant` 交互。通过 `wpa_cli`，可以列出网络、连接（选择）、重新配置和断开连接。下面是连接 WPA 保护网络的示例会话。

```sh
$ wpa_cli
wpa_cli v2.11
Copyright (c) 2004-2024, Jouni Malinen <j@w1.fi> and contributors

This software may be distributed under the terms of the BSD license.
See README for more details.


Selected interface 'wlan0'

Interactive mode

> list_networks
network id / ssid / bssid / flags
0       Open Network       [DISABLED]
1       Closed Network     [DISABLED]
> select_network 1
OK
<3>CTRL-EVENT-SCAN-RESULTS
<3>WPS-AP-AVAILABLE
<3>Trying to associate with 20:05:b6:fa:13:f1 (SSID='Closed Network' freq=5180 MHz)
<3>Associated with 20:05:b6:fa:13:f1
<3>WPA: Key negotiation completed with 20:05:b6:fa:13:f1 [PTK=CCMP GTK=CCMP]
<3>CTRL-EVENT-CONNECTED - Connection to 20:05:b6:fa:13:f1 completed [id=0 id_str=]
disable_network disconnect
> disconnect
OK
<3>CTRL-EVENT-DISCONNECTED bssid=20:05:b6:fa:13:f1 reason=3 locally_generated=1
<3>CTRL-EVENT-DSCP-POLICY clear_all
```

### 使用 hostapd 的 AP 模式

`hostapd` 使用较少，但它能让你启动自己的接入点（AP），并在其中做完整的内核调试和数据包捕获，这对调试非常有帮助。

下面是针对我们示例中“Closed Network”的配置示例：

hostapd.conf:

```ini
ctrl_interface=/var/run/hostapd
ctrl_interface_group=wheel

interface=wlan1

# hw_mode=g
channel=8
ieee80211d=0
ieee80211n=0
wmm_enabled=0

# 接入点的名称
ssid="Closed Network"
# 1=wpa, 2=wep, 3=两者都支持
auth_algs=1
wpa=2
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
wpa_passphrase="superpassword"
```

我们可以这样在前台运行 hostapd：

```sh
# hostapd hostapd.conf
```

运行 hostapd 后，我们就拥有了一个大部分功能齐全的接入点——WiFi 部分很简单！接下来我们还需要给 `wlan1` 分配一个地址，通常还会运行一个进程提供动态地址分配。

我们可以像平常一样给接口分配 IP 地址：

```sh
# ifconfig wlan1 inet 192.168.2.1/24 up
```

动态地址分配需要安装 Port dhcpd 并创建配置文件。可以在 **dhcpd.conf(5)** 手册页中找到最简配置示例，示例如下：

```ini
/usr/local/etc/dhcpd.conf:

subnet 192.168.2.0 netmask 255.255.255.0 {
    range 192.168.2.100 192.168.2.200
}
```

然后启用并启动 dhcpd 服务：

```sh
# service enable dhcpd
# service start dhcpd
```

### 监控模式（Monitor mode）

我们示例中创建的最后一个 VAP 是监控模式。虽然其他模式下也能抓包，但接口并非混杂模式，只会收到发给该接口地址的数据包。监控模式允许我们接收信道上所有的数据包（具体取决于硬件对不同速率的支持）。

一个简单的验证方法是使用带有选项 `-y`（设置链路层头部类型）的 `tcpdump`：

```sh
# tcpdump -L -i wlan2
Data link types for wlan0 (use option -y to set):
  EN10MB (Ethernet)
  IEEE802_11_RADIO (802.11 plus radiotap header)
```

我经常记不清这个变量中下划线的顺序，但 `tcpdump` 使用参数 `-L` 加接口名可以显示该接口类型支持的链路层头部类型。

```sh
# tcpdump -i wlan2 -y IEEE802_11_RADIO
tcpdump: data link type IEEE802_11_RADIO
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wlan2, link-type IEEE802_11_RADIO (802.11 plus radiotap header), snapshot length 262144 bytes
14:33:48.656430 3757399us tsft 1.0 Mb/s 2412 MHz 11g -76dBm signal -95dBm noise Data IV:c1ed Pad 20 KeyID 1
14:33:50.657087 5759270us tsft 1.0 Mb/s 2412 MHz 11g -72dBm signal -95dBm noise
14:33:50.796280 5895802us tsft 1.0 Mb/s 2412 MHz 11g -76dBm signal -95dBm noise Beacon (HomeWifi) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 2, PRIVACY                            
14:33:53.151514 8251009us tsft 1.0 Mb/s 2412 MHz 11g -74dBm signal -95dBm noise Beacon (HomeWifi) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 2, PRIVACY
14:33:53.970729 9070213us tsft 1.0 Mb/s 2412 MHz 11g -74dBm signal -95dBm noise Beacon (HomeWifi) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 2, PRIVACY
14:34:10.183336 25285260us tsft 6.0 Mb/s 2437 MHz 11g -62dBm signal -95dBm noise Beacon (a2-enc) [6.0* 9.0 12.0* 18.0 24.0* 36.0 48.0 54.0 Mbit] ESS CH: 6, PRIVACY
14:34:10.204045 25306099us tsft 11.0 Mb/s 2437 MHz 11g -68dBm signal -95dBm noise Beacon () [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 6                                            
14:34:10.253438 25356305us tsft 11.0 Mb/s 2437 MHz 11g -58dBm signal -95dBm noise Beacon () [1.0* 2.0* 5.5* 11.0* 6.0* 9.0 12.0* 18.0 Mbit] IBSS CH: 6, PRIVACY
14:34:10.253441 25356305us tsft 11.0 Mb/s 2437 MHz 11g -58dBm signal -95dBm noise Beacon () [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 6
14:34:10.253445 25356305us tsft 11.0 Mb/s 2437 MHz 11g -58dBm signal -95dBm noise Beacon (HM-CM-$tte, HM-CM-$tte, kette) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 6, PRIVACY
14:34:10.285355 25387273us tsft 6.0 Mb/s 2437 MHz 11g -63dBm signal -95dBm noise Beacon (a2-enc) [6.0* 9.0 12.0* 18.0 24.0* 36.0 48.0 54.0 Mbit] ESS CH: 6, PRIVACY
14:34:10.297973 25399988us tsft 11.0 Mb/s 2437 MHz 11g -68dBm signal -95dBm noise Beacon (HM-CM-$tte, HM-CM-$tte, adkette) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 6, PRIVACY
14:34:10.355834 25458704us tsft 11.0 Mb/s 2437 MHz 11g -58dBm signal -95dBm noise Beacon () [1.0* 2.0* 5.5* 11.0* 6.0* 9.0 12.0* 18.0 Mbit] IBSS CH: 6, PRIVACY
14:34:10.355836 25458704us tsft 11.0 Mb/s 2437 MHz 11g -58dBm signal -95dBm noise Beacon () [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 6
```

这个 `tcpdump` 输出聚焦于 IEEE80211 无线电帧。深入分析则需要使用其他工具，比如 Wireshark。

## 测试流量

现在我们已经拥有了构建纯 FreeBSD AP 站点并调试无线流量的所有条件，接下来讨论如何测试。

我们可以利用 AP 站点和监控模式来验证和调查关联过程中和数据发送时的无线包。

测试的第一步是让站点成功连接到网络，能向 AP 发送 `ping`。通过站点上的 `wpa_supplicant`，我们可以选择网络并请求其完成关联。`wpa_supplicant` 会打印关联过程中的消息，记录握手包。

连接成功后，站点需要获取 IP 地址。如果没有自动完成，可以运行：

```sh
# dclient wlan0
```

通常这会让它正常工作。拿到 dhcpd 分配的地址后，下一步测试能否 ping 通 AP：

```sh
# ping 192.168.2.1
```

如果失败，就是排查的开始。很可能是配置问题（示例不一定完美，但经过测试）。调试无法连接时的原因往往不是最愉快的 WiFi 开发体验，但这是我们都遇到过的。

拿到地址后，简单的吞吐量测试通常是第一步的好指标。如果测试的是某个分支上的补丁，只要编译了带补丁的内核，跑网络吞吐量测试就足够了。我喜欢用 `iperf3`，它可以测试到你网络内某主机或互联网的吞吐量。用 `iperf` 在站点和 AP 端互测，可以直观了解该硬件组合可能达到的吞吐率。

如果系统够稳定，可以运行浏览器，我觉得用 `fast.com` 测试也很有参考价值。

这两种测试分别反映了不同的限制：`iperf` 测试给出 WiFi 网络内站点的吞吐能力，`fast.com` 测试显示从你网络到互联网的速率。`fast.com` 的数值可能明显更低。如果快于 `iperf`，那就不太正常。

TCP 和 UDP 测试的吞吐量会有差异。UDP 更能饱和 WiFi 无线电。两者都测试效果好，但若要快速评估，TCP 吞吐量即可。

## 准备开始工作

本文介绍了在 FreeBSD 上开始开发 WiFi 所需的背景知识和术语，但还未涉及任何代码。使用这里的示例搭建测试网络是 WiFi 开发的核心部分，如果你迫不及待，不想等待第二部分，我相信只要配备足够硬件测试，你就能开始发现问题。

下一篇文章中，我们将探讨 WiFi 驱动的生命周期，它需要具备的核心功能，和与 `net80211` 交互的接口，这些接口能够承担驱动的大量工作。

**Tom Jones** 是一位 FreeBSD 提交者，致力于保持网络栈的高速性能。


# 嵌入式 FreeBSD：自定义硬件

* 原文：[Embedded FreeBSD: Custom Hardware](https://freebsdfoundation.org/our-work/journal/browser-based-edition/networking-3/embedded-freebsd-custom-hardware/)
* 作者：Christopher R. Bowman

当我开始踏上 FPGA 和 FreeBSD 的这段旅程时，我选择了 [Digilent Arty Z7-20](https://digilent.com/shop/arty-z7-zynq-7000-soc-development-board/)，因为它是基于 Zynq 的开发板中价格较低且具有良好扩展性的型号。它既带有符合 Arduino Shield 物理规格的一组引脚，也配备了符合 [PMOD](https://en.wikipedia.org/wiki/Pmod_Interface) 标准的一组连接器。事后看来，我觉得我会选择放弃 Arduino Shield 接口（我其实从未使用过），换成更多的 [PMOD](https://en.wikipedia.org/wiki/Pmod_Interface) 接口。市面上有很多 [PMOD 设备](https://digilent.com/reference/pmod/start)，而且价格普遍较低。本文将使用其中的 [PMOD SSD](https://digilent.com/reference/pmod/pmodssd/start) 作为示例。

![PMOD SSD 模块实物图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-ed0487c7c3a48c051b1a7398ad86e58db6d00b8d%2FHardware-01.png?alt=media)

图 1 PMOD SSD

这是一个双七段数码管显示器（SSD，这里指 Seven Segment Display，不要与固态硬盘 Solid-State Disk 混淆），可以插到 Arty Z7 开发板上的两个 PMOD 接口中。查看其 [原理图](https://digilent.com/reference/pmod/pmodssd/reference-manual)，可以发现只需将几个引脚设置为高电平或低电平，就能控制点亮对应的 LED。但文档中有一句话提到：“由于任意时刻只能点亮一位数码管，想要同时显示两位数字的用户，需要以不低于 20 毫秒（50Hz）的频率交替点亮两个数码管。”也就是说，同一时间只能点亮其中一位。想要同时显示两个数字，必须每 20 毫秒切换 AA-AF 引脚的状态，再切换 C 引脚。我还没尝试过，但我怀疑自己能否稳定地以 50 次每秒的速率调用 GPIO 系统。这个场景很适合用硬件来实现，电路并不复杂。我们可以用两个寄存器，配合一个计数器和多路复用器（MUX）以 50Hz 的频率切换，将寄存器连接到引脚。另一种简易方案是使用 14 个 GPIO 引脚，只把计数器和多路复用器放到硬件里，这样比较快速简单。但最终，你会想做一个带有常规寄存器接口的硬件模块。那么，我们来构建一个简单的内存映射寄存器设备，并把它挂到 AXI 总线上。它将是 AXI 总线的简单应用，但同时能为更复杂的设计提供很好的示例。由于这个选择，我们需要自己编写驱动程序，不过这同样是个很好的示例。

这个项目包含硬件、软件和文档三部分。你可以用下面的命令下载代码（**译者注：已经 404 了**）：

```sh
# git clone http://github.com/axi_mm_ssd
```

现在的情况稍复杂，有些组件需在 Linux 机器或虚拟机上构建（FPGA 配置比特流），另一些则需要在 FreeBSD 上构建（其他部分）。我沿用了之前专栏介绍的 bhyve 配置作为 Linux 端，这样只需一台机器，无需每次修改硬件后都重启系统重新构建 FPGA。硬件部分应在 Linux 下构建（除非你成功让 Xilinx/AMD Vivado 工具通过 Linux 兼容层原生运行在 FreeBSD 上，如果你做到的话，请告诉我如何做到的）。因为 Linux 下的标准构建工具是 GNU make，项目里有符合 GNU make 规范的 Makefile。你会发现硬件构建脚本相当复杂。

我一直在苦恼如何给 Vivado 的硬件构建写个好用的脚本。这次我在 GUI 里完成了大部分设计，用 Vivado 的命令 `write_project_tcl` 导出创建项目的脚本，然后对它进行了修改和扩展。虽然不完美，但目前看起来运行稳定。

借助 Vivado 的 GUI 和 IP 生成工具，我创建了一个简单的 AXI 从设备示例，它生成了完整的 AXI4-Lite 内存映射从接口的 Verilog 代码。我在此基础上根据自己的需求做了一些修改，增加了计数器和多路复用器，并将引脚连通。之后，我用 GUI 将 AXI4-Lite 从设备连接到 AXI 总线，并用 `assign_bd_address` 指定了地址。你只需运行 `make`，脚本会自动完成这些操作。

我们先来看看将要构建的硬件。如果进入项目文档目录并用 `make` 构建文档，会看到一节介绍硬件接口。简要来说，我们要构建一个有三个寄存器的内存映射设备。第一个寄存器包含一个只读的常量值，用于唯一标识设备。我觉得这个设计很实用，尤其是从零开始、不确定硬件或软件是否正确工作时。如果设备不按预期工作，但内存里能看到那个魔数，至少说明你走对了方向。我也在尝试用 git 的哈希值和构建日期为设计打标签，这样每次电路加载到 FPGA 里时，就能清楚知道加载的是哪一版本。对于成熟商业厂商的成品板这不算大问题，但自己做设计、硬件和软件都刚入门、还会不断引入 bug 的情况下，这帮助很大。不同于芯片制造，不用为每次硬件迭代花费五百万美元做掩膜版，我们可以承受这点额外开销，待全部调通后再去除。

另外两个寄存器分别控制数码管的两个数字，每个寄存器的每个位对应数字上的一个段。通过向寄存器的内存地址写入值，就能简单地点亮或关闭该数字的所有段。

既然我们已经讨论了硬件和它的构建方式，而本文又刊载于《FreeBSD 期刊》，接下来仔细看看软件部分。在项目的 **software/driver/freebsd/kld/ssd.c** 文件中，你会找到一个相当直观的 KLD（内核模块），它添加了几个 sysctl 条目，最终通过写入内存映射寄存器来驱动七段数码管显示。这段 KLD 代码是一个比较直观的示例，类似于 Joseph Kong 的经典著作 [*《FreeBSD 设备驱动程序开发：A Guide for the Intrepid*》](https://www.amazon.com/FreeBSD-Device-Drivers-Guide-Intrepid-ebook/dp/B007W8OL0S/ref=sr_1_1?crid=3F1MA42Q4G4SN\&keywords=FreeBSD+Device+Drivers\&qid=1696841016\&sprefix=freebsd+device+drivers%2Caps%2C224\&sr=8-1) （《深入理解 FreeBSD 设备驱动程序开发》ISBN: 9787111411574）中提供的驱动示例。

不过，这个驱动和你在常见的带有标准 PCIe 总线的 AMD64 PC 工作站上看到的驱动有些不同，有几个有趣的地方值得关注。下面是它的 probe 函数：

```c
static int
ssd_probe(device_t dev)
{


//   device_printf(dev, "probe of ssd\n");
     if (!ofw_bus_status_okay(dev))
          return (ENXIO);

     if (!ofw_bus_is_compatible(dev, "crb,ssd-1.0")){
          return (ENXIO);
   }

     //device_printf(dev, "matched ssd\n");
     device_set_desc(dev, "AXI MM seven segment display");
     return (BUS_PROBE_DEFAULT);
}
```

注意 `ofw_*` 系列函数，我们在上一期专栏中提到过它们，并讨论了如何用它们从 FDT/DTS 文件中读取属性。这次，我们将在 FDT overlay 中创建自己的条目，来描述设备类型、地址和寄存器集合。我们的 overlay 源代码位于 **software/driver/freebsd/kld/arrtyz7\_ssd\_overlay.dts**，其中比较有趣的部分如下所示：

```c
&{/axi} {
     axissd: ssd@043c00000 {
               compatible = "crb,ssd-1.0";
               reg = <0x43c00000 0x0004>;
          };
};
```

正如我们在上一期专栏中描述的，`compatible` 行用厂商名和版本号（以逗号分隔）来标识设备。在这个例子中，我是设备的设计者，所以用我的姓名缩写作为厂商名。这个项目基本完成，我也不太可能再修改它，但如果我修改了它并产出 V2 及后续版本，驱动程序就能通过区分不同的版本来适配可能不同的寄存器布局或编程方式。这样一个驱动就能支持多种类似设备，并根据 FDT 中的版本信息识别当前设备。

`reg` 这行告诉内核设备寄存器在物理地址空间的位置和寄存器所占用的地址范围。在本例中，设备被放置在物理地址 `0x43c00000`，共有四个寄存器。虽然接口只需要三个寄存器，且文档中也只描述了三个，但 Verilog 实际实现了四个寄存器。

接下来，我们看看它的 `attach` 函数，代码大致如下：

```c
static int
ssd_attach(device_t dev)
{
     struct ssd_softc *sc;

     device_printf(dev, "attaching ssd\n");
     sc = device_get_softc(dev);
     sc->dev = dev;

     int rid;

AXI_MM_SSD_LOCK_INIT(sc);

     /* 分配内存 */
     rid = 0;
     sc->mem_res = bus_alloc_resource_any(dev,
               SYS_RES_MEMORY, &rid, RF_ACTIVE);
     if (sc->mem_res == NULL) {
device_printf(dev, "Can't allocate memory for \
device\n");
          ssd_detach(dev);
          return (ENOMEM);
     }
#define MAGIC_SIGNATURE 0xFEEDFACE
#ifdef CHECKMAGIC
int32_t value = RD4(sc, AXI_MM_SSD_SGN);
     if (value != MAGIC_SIGNATURE) {
     device_printf(dev, "MAGIC_SIGNATURE 0xFEEDFACE \
not found! value = %x\n", value);
          ssd_detach(dev);
          return (ENXIO);
     }
#endif
   axi_mm_ssd_sysctl_init(sc);
   device_printf(dev, "ssd attached\n");

   return (0);

}
```

这段代码首先初始化了一个锁，稍后我会详细讨论。接着，驱动程序分配了总线内存资源。我认为设置 `rid=0` 表示请求总线 DMA 系统分配与设备相关联的第一个总线资源。虽然我没追踪过这段代码，但由于我们的 FDT overlay 里有 `reg = <0x43c00000 0x0004>;`，这个内存区域应该就是被分配的资源。如果有多条类似 `reg` 行，它们应该对应连续的资源 ID。如果你对此了解更深，欢迎告诉我。

因为这是我构建的第一个设备和第一个为它写的驱动，我加了一段用 `#ifdef` 包裹的代码，用来读取第一个寄存器并查找我的“幻数”——`0xFEEDFACE`。文件里定义了一个宏 `RD4` 如下：

```c
#define RD4(sc, off) bus_read_4((sc)->mem_res, (off))
```

这个宏将通过 `bus_alloc_resource_any()` 获取的总线资源 `mem_res` 传给 `bus_read_4()`，并返回寄存器中对应偏移处的值。

借助总线资源系统，我们避免了在驱动里硬编码寄存器地址。这样如果我在设计中更改硬件地址，只需修改 FDT/DTS 即可，非常方便。此外，也避免了必须将物理地址转换成内核虚拟地址。正如我吃了苦头才学到的，内核地址并不直接映射物理地址（而且第一个寄存器的魔数也帮我确认了这一点）。

通过这段代码，我可以比较确信，如果读出第一个寄存器返回了正确的魔数，就说明我确实在和我设计的设备通信。对于第一次使用整套工具链构建设备并把它挂载到 AXI 总线的新手来说，如果不了解内核物理地址和虚拟地址的区别，这种确认是非常安心的。如果我是个纯软件工程师，我甚至可能会尝试在 FDT/DTS overlay 里添加一个自定义资源，指定预期读取的魔数值，而不是硬编码 `0xFEEDFACE`。这很有用，不同的常量可以代表设备的不同版本或不同寄存器布局或不同语义，这样驱动就能校验 FDT/DTS 是否和设备匹配。这里就留给读者作为练习（欢迎提交补丁）。

现在我们能探测（probe）和附加（attach）设备，也可以比较确定设备真实存在。接下来我们看看如何使用它。从用户空间看，我还不太清楚软件接口该怎么设计。Unix 哲学认为“一切皆文件”，但这个设备似乎不太像一个文件。我可以在 **/dev** 下创建一个设备节点，通过打开它并定义 ioctl 函数，允许设置两个控制七段显示的寄存器值，这样还能用文件权限控制访问。但我选择了另一种类似的方案：没有使用文件系统设备节点，也没有 ioctl，而是用 sysctl。

在 `ssd_attach()` 返回前，它创建了两个 sysctl，一个用于获取和设置一个七段数码管的值，另一个用于另一个数码管的值。这两个 sysctl 会出现在 sysctl 树的 `dev.ssd.0.tens` 和 `dev.ssd.0.ones` 下。根据你板子放置方向不同，它们看起来可能是反的。

`axi_mm_ssd_sysctl_init()` 函数负责注册这两个 sysctl，下面是其中一个的代码示例：

```c
static int
axi_mm_ssd_proc0(SYSCTL_HANDLER_ARGS)
{
     int error;
     static int32_t value0 = 1;
     struct ssd_softc *sc;
     sc = (struct ssd_softc *)arg1;

     AXI_MM_SSD_LOCK(sc);

     value0 = RD4(sc, AXI_MM_SSD_SR2);

     AXI_MM_SSD_UNLOCK(sc);

     error = sysctl_handle_int(oidp, &value0,
                   sizeof(value0), req);
     if (error != 0 || req->newptr == NULL)
          return (error);

     WR4(sc, AXI_MM_SSD_SR2, value0);

     return (0);
}
```

这段代码使用了我们在 `attach` 函数中创建的锁，并通过前面提到的宏 `RD4` 读取第一个数码管对应的设备寄存器。考虑到我这里只做单次读写操作，不确定是否真的需要加锁，但我用锁是为了确保多个竞争进程在设置寄存器时不会互相干扰。也许我应该把整个过程都包裹在锁中，但我是内核编程新手，不确定哪些函数在持锁状态下是安全调用的（如果你知道，欢迎告诉我）。`sysctl_handle_int` 会读取旧值、传回用户空间，并返回新的值写入寄存器，写操作由 `WR4` 完成。John Baldwin 写过一篇关于 sysctl 系统的优秀 [文章](https://freebsdfoundation.org/wp-content/uploads/2014/01/Implementing-System-Control-Nodes-sysctl.pdf)，如果你想了解 sysctl 的工作原理，推荐阅读。

现在我们有了能探测、附加和设置两个硬件寄存器的驱动，只需构建包含该驱动的 KLD，构建 FDT overlay，安装到 **/boot/dtb/overlays**，并像上一篇文章中那样把它接入 `loader.rc` 脚本。重启系统，启动后先加载 FPGA 的 bitstream，再加载 KLD，`dmesg` 应该会显示探测成功的信息。

这就是驱动的所有关键部分。我们来看看它实际运行的效果。在 Git 仓库里，你会看到一个位于 **software/app/test.csh** 的简短 C shell 脚本，内容如下：

```sh
#!/bin/tcsh
set digits = (126 48 109 121 51 91 31 112 127 115)
set delay = $argv[1]
foreach tens ($digits)
  sysctl dev.ssd.0.tens=$tens
  foreach ones ($digits)
    sysctl dev.ssd.0.ones=$ones
    sleep $delay
  end
end
```

这里有一个数组，里面的值对应着需要设置到寄存器中的位，用来编码数字 0 到 9。脚本接受一个延迟参数，从零计数到 99，每次数字变化后都会暂停。

我花了大量时间盯着数码管计数。虽然这个设备本身用处不大，但它完整地展示了如何通过内存映射寄存器与硬件通信，并让芯片外的世界发生变化。

希望这些专栏对你有所帮助，欢迎留言反馈。你可以通过 <articles@ChrisBowman.com> 联系我。

***

**Christopher R. Bowman** 最早于 1989 年在约翰霍普金斯大学应用物理实验室工作期间使用 BSD，当时在地下两层的 VAX 11/785 上工作。90 年代中期，他在马里兰大学用 FreeBSD 设计了他的第一个 2 微米 CMOS 芯片。从那以后，他一直是 FreeBSD 用户，对硬件设计和相关软件很感兴趣。过去 20 年他一直在半导体设计自动化行业工作。


# TCP/IP 探险记：静态 Pacing

* 原文：[Static Pacing in FreeBSD](https://freebsdfoundation.org/our-work/journal/browser-based-edition/networking-3/adventures-in-tcp-ip-static-pacing/)
* 作者：Randall Stewart、Michael Tüxen

之前的文章介绍了 FreeBSD 通过其高精度定时系统（High Precision Timing System，HPTS）支持的 pacing 机制，以及 RACK 协议栈中一种名为动态吞吐率 pacing（Dynamic Goodput Pacing，DGP）的软件 pacing 方法，该方法能根据当前网络状况动态调整到最佳速率。RACK 协议栈还提供另一种 pacing 方法，本文将介绍尚未描述的静态 pacing。

静态 pacing 结构简单，是最早添加到 RACK 协议栈中的 pacing 方法之一，主要用于测试和改进 pacing 功能（如 HPTS）。尽管最初是作为测试方法设计的，但在某些情况下，应用程序也可以使用静态 pacing。使用静态 pacing 时，pacing 速率不会由 RACK 协议栈计算，而是需要应用程序通过套接字选项（socket options）提供。因此，需要应用程序中的代码来使用静态 pacing。

## TCP 分段发送

有三种事件会触发 TCP 分段的发送：

1. 应用程序通过 `send()` 系统调用向 TCP 协议栈提供用户数据。
2. TCP 定时器（例如重传定时器或延迟确认定时器）到期。
3. 收到一个 TCP 分段。

在这些事件中可发送的分段数量主要由两种机制控制：

1. 流量控制：保护较慢的接收方不被发送方淹没。接收方通过在发送给发送方的 TCP 分段的 `SEG.WND` 字段中通告发送方允许发送的字节数来实现流控。
2. 拥塞控制：保护较慢的网络不被发送方淹没。发送方通过计算允许发送的字节数，也就是拥塞窗口（`CWND`），来实现拥塞控制。

发送方只会发送流量控制和拥塞控制允许的字节。

拥塞控制有多种算法。FreeBSD 长期以来默认的拥塞控制算法是 New Reno。New Reno 包含两个阶段：

1. 慢启动：这是初始阶段和基于定时器重传后的阶段。在此阶段，`CWND` 指数增长。
2. 拥塞避免：这是 TCP 连接大部分时间运行的阶段。在此阶段，`CWND` 线性增长。

TCP 连接开始时，`CWND` 被设置为初始拥塞窗口，其大小由 `sysctl` 变量 `net.inet.tcp.initcwnd_segments` 控制，默认值为 10 个 TCP 分段。

TCP 端点重传用户数据的方式有两种：一种是重传定时器到期时触发；另一种是检测到 TCP 分段丢失后，TCP 端点在恢复状态下运行。

以上描述表明，在流量控制和拥塞控制允许的情况下，TCP 端点可能会发送一连串突发的 TCP 分段。

下面的 `packetdrill` 脚本演示了 FreeBSD 在 TCP 连接建立后，应用程序立刻提供 10 个 TCP 分段数据时的行为。`packetdrill` 是一种用于测试 TCP 协议栈的工具，其脚本包含应用程序的系统调用和 TCP 协议栈发送和接收的 TCP 分段。脚本中的每行以秒为单位的时间戳开始。如果时间信息以 `+` 开头，则表示相对于前一事件的时间。例如，`+0.100` 表示事件发生在前一事件之后 100 毫秒。

```sh
-ip_version=ipv4

0.000 `kldload -n tcp_rack`
+0.000 `kldload -n cc_newreno`
+0.000 `sysctl kern.timecounter.alloweddeviation=0`

+0.000 socket(…, SOCK_STREAM, IPPROTO_TCP) = 3
+0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0.000 setsockopt(3, IPPROTO_TCP, TCP_FUNCTION_BLK, {function_set_name="rack",
                                                     pcbcnt=0}, 36) = 0
+0.000 setsockopt(3, IPPROTO_TCP, TCP_CONGESTION, "newreno", 8) = 0
+0.000 bind(3, …, …) = 0
+0.000 listen(3, 1) = 0
+0.000 < S      0:0(0)                  win 65535 <mss 1460,sackOK,eol,eol>
+0.000 > S.     0:0(0)        ack     1 win 65535 <mss 1460,sackOK,eol,eol>
+0.050 <  .     1:1(0)        ack     1 win 65535
+0.000 accept(3, …, …) = 4
+0.000 close(3) = 0
+0.100 send(4, …, 14600, 0) = 14600
+0.000 >  .     1:1461(1460)  ack     1 win 65535
+0.000 >  .  1461:2921(1460)  ack     1 win 65535
+0.000 >  .  2921:4381(1460)  ack     1 win 65535
+0.000 >  .  4381:5841(1460)  ack     1 win 65535
+0.000 >  .  5841:7301(1460)  ack     1 win 65535
+0.000 >  .  7301:8761(1460)  ack     1 win 65535
+0.000 >  .  8761:10221(1460) ack     1 win 65535
+0.000 >  . 10221:11681(1460) ack     1 win 65535
+0.000 >  . 11681:13141(1460) ack     1 win 65535
+0.000 > P. 13141:14601(1460) ack     1 win 65535
+0.050 <  .     1:1(0)        ack  2921 win 65535
+0.000 <  .     1:1(0)        ack  5841 win 65535
+0.000 <  .     1:1(0)        ack  8761 win 65535
+0.000 <  .     1:1(0)        ack 11681 win 65535
+0.000 <  .     1:1(0)        ack 14601 win 65535
+0.000 close(4) = 0
+0.000 > F. 14601:14601(0)    ack     1 win 65535
+0.050 < F.     1:1(0)        ack 14602 win 65535
+0.000 >  . 14602:14602(0)    ack     2
```

假设往返时间（RTT）为 50 毫秒。脚本显示，调用 `send()` 会触发一次同时发送 10 个 TCP 分段的突发。

## 静态 Pacing

静态 pacing 是一种缓解流量突发性的方法。它用一系列较小的突发代替一次大的突发。较小突发的大小称为 pacing 突发大小（pacing burst size）。这些较小突发之间的时间间隔由 pacing 速率决定。

例如，发送速率为 12,000,000 比特/秒，相当于 1,500,000 字节/秒。若数据包大小为 1500 字节，则意味着每毫秒发送一个包，或者每两毫秒发送两个包，依此类推。

通过指定 pacing 速率和 pacing 突发大小，可以计算出突发之间的时间间隔，从而使发送速率达到设定的 pacing 速率。

在 RACK 协议栈中，静态 pacing 需要应用程序分别为慢启动（slow start）、拥塞避免（congestion avoidance）和恢复（recovery）状态提供单独的 pacing 速率和 pacing 突发大小。包大小计算时会考虑 IP 头部大小、TCP 头部大小和 TCP 载荷大小，但不考虑链路层头部和尾部大小。

静态 pacing 通过应用程序源码中使用的 `IPPROTO_TCP` 级别套接字选项进行控制。这些套接字选项中，有三个用于控制拥塞控制算法不同状态下的 pacing 速率，有一个用于设置 pacing 突发大小，还有一个用于启用或禁用静态 pacing。

以下表格列出了这些套接字选项。

| 套接字选项名称                  | 值类型        | 说明                                                            |
| ------------------------ | ---------- | ------------------------------------------------------------- |
| `TCP_RACK_PACE_RATE_CA`  | `uint64_t` | 设置拥塞控制算法处于拥塞避免（congestion avoidance）阶段时的静态 pacing 速率，单位为字节每秒。 |
| `TCP_RACK_PACE_RATE_SS`  | `uint64_t` | 设置拥塞控制算法处于慢启动（slow start）阶段时的静态 pacing 速率，单位为字节每秒。            |
| `TCP_RACK_PACE_RATE_REC` | `uint64_t` | 设置拥塞控制算法进入恢复（recovery）阶段时的静态 pacing 速率，单位为字节每秒，用于恢复丢失的数据包。    |
| `TCP_RACK_PACE_ALWAYS`   | `int`      | 启用或禁用 pacing 功能的选项。                                           |
| `TCP_RACK_PACE_MAX_SEG`  | `int`      | 设置 pacing 突发大小（pacing burst size）。                            |

需要注意以下四点：

1. 设置套接字选项（和任何其他系统调用）可能会失败，应用程序必须检查返回结果。导致上述套接字选项设置失败的一个原因是当前套接字使用的 TCP 协议栈不是 RACK。FreeBSD 默认不支持静态 pacing。此外，如果使用 pacing 的 TCP 连接数达到系统整体限制，启用静态 pacing 也会失败。该限制由 `sysctl` 变量 `net.inet.tcp.pacing_limit` 控制。
2. 在设置第一个 pacing 速率时，该速率不仅会应用于所指定的模式，还会同时应用于拥塞避免（congestion avoidance）、慢启动（slow start）和恢复（recovery）三种模式。
3. 仅设置 pacing 速率并不能启用静态 pacing，必须显式使用 `TCP_RACK_PACE_ALWAYS` 套接字选项来启用静态 pacing。
4. 如果不设置 pacing 突发大小，则默认使用值 40。

下面的 `packetdrill` 脚本演示了在慢启动阶段，RACK 协议栈以 12,000,000 比特/秒的 pacing 速率和 pacing 突发大小为 1 执行 pacing 的情况。

```sh
-ip_version=ipv4

0.000 `kldload -n tcp_rack`
+0.000 `kldload -n cc_newreno`
+0.000 `sysctl kern.timecounter.alloweddeviation=0`

+0.000 socket(…, SOCK_STREAM, IPPROTO_TCP) = 3
+0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0.000 setsockopt(3, IPPROTO_TCP, TCP_FUNCTION_BLK, {function_set_name="rack",
                                                     pcbcnt=0}, 36) = 0
+0.000 setsockopt(3, IPPROTO_TCP, TCP_CONGESTION, "newreno", 8) = 0
+0.000 bind(3, …, …) = 0
+0.000 listen(3, 1) = 0
+0.000 < S      0:0(0)                  win 65535 <mss 1460,sackOK,eol,eol>
+0.000 > S.     0:0(0)        ack     1 win 65535 <mss 1460,sackOK,eol,eol>
+0.050 <  .     1:1(0)        ack     1 win 65535
+0.000 accept(3, …, …) = 4
+0.000 close(3) = 0

+0.000 setsockopt(4, IPPROTO_TCP, TCP_RACK_PACE_RATE_SS, [1500000], 8) = 0 # 黄色高亮
+0.000 setsockopt(4, IPPROTO_TCP, TCP_RACK_PACE_MAX_SEG, [1], 4) = 0 # 黄色高亮
+0.000 setsockopt(4, IPPROTO_TCP, TCP_RACK_PACE_ALWAYS, [1], 4) = 0 # 黄色高亮

+0.100 send(4, …, 14600, 0) = 14600
+0.000 >  .     1:1461(1460)  ack     1 win 65535

+0.001 >  .  1461:2921(1460)  ack     1 win 65535 # 灰色部分开始
+0.001 >  .  2921:4381(1460)  ack     1 win 65535
+0.001 >  .  4381:5841(1460)  ack     1 win 65535
+0.001 >  .  5841:7301(1460)  ack     1 win 65535
+0.001 >  .  7301:8761(1460)  ack     1 win 65535
+0.001 >  .  8761:10221(1460) ack     1 win 65535
+0.001 >  . 10221:11681(1460) ack     1 win 65535
+0.001 >  . 11681:13141(1460) ack     1 win 65535
+0.001 > P. 13141:14601(1460) ack     1 win 65535
+0.042 <  .     1:1(0)        ack  2921 win 65535
+0.002 <  .     1:1(0)        ack  5841 win 65535
+0.002 <  .     1:1(0)        ack  8761 win 65535
+0.002 <  .     1:1(0)        ack 11681 win 65535
+0.002 <  .     1:1(0)        ack 14601 win 65535 # 灰色部分结束

+0.000 close(4) = 0
+0.000 > F. 14601:14601(0)    ack     1 win 65535
+0.050 < F.     1:1(0)        ack 14602 win 65535
+0.000 >  . 14602:14602(0)    ack     2
```

启用静态 pacing 所需的代码更改用黄色高亮显示，而线路上行为的相应变化用灰色标出。需要说明的是，变化仅体现在 TCP 分段的发送时序上，TCP 分段本身未发生改变。现在发送包含用户数据的 TCP 分段之间有了 1 毫秒的延迟。

以下 packetdrill 脚本展示了在慢启动阶段使用相同的 pacing 速率，但 pacing 突发大小为 4 的情况：

```sh
-ip_version=ipv4

0.000 `kldload -n tcp_rack`
+0.000 `kldload -n cc_newreno`
+0.000 `sysctl kern.timecounter.alloweddeviation=0`

+0.000 socket(…, SOCK_STREAM, IPPROTO_TCP) = 3
+0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0.000 setsockopt(3, IPPROTO_TCP, TCP_FUNCTION_BLK, {function_set_name="rack",
                                                     pcbcnt=0}, 36) = 0
+0.000 setsockopt(3, IPPROTO_TCP, TCP_CONGESTION, "newreno", 8) = 0
+0.000 bind(3, …, …) = 0
+0.000 listen(3, 1) = 0
+0.000 < S      0:0(0)                  win 65535 <mss 1460,sackOK,eol,eol>
+0.000 > S.     0:0(0)        ack     1 win 65535 <mss 1460,sackOK,eol,eol>
+0.050 <  .     1:1(0)        ack     1 win 65535
+0.000 accept(3, …, …) = 4
+0.000 close(3) = 0
+0.000 setsockopt(4, IPPROTO_TCP, TCP_RACK_PACE_RATE_SS, [1500000], 8) = 0

+0.000 setsockopt(4, IPPROTO_TCP, TCP_RACK_PACE_MAX_SEG, [4], 4) = 0 # 黄色部分

+0.000 setsockopt(4, IPPROTO_TCP, TCP_RACK_PACE_ALWAYS, [1], 4) = 0
+0.100 send(4, …, 14600, 0) = 14600

+0.000 >  .     1:1461(1460)  ack     1 win 65535 # 灰色部分开始
+0.000 >  .  1461:2921(1460)  ack     1 win 65535
+0.000 >  .  2921:4381(1460)  ack     1 win 65535
+0.000 >  .  4381:5841(1460)  ack     1 win 65535
+0.004 >  .  5841:7301(1460)  ack     1 win 65535
+0.000 >  .  7301:8761(1460)  ack     1 win 65535
+0.000 >  .  8761:10221(1460) ack     1 win 65535
+0.000 >  . 10221:11681(1460) ack     1 win 65535
+0.004 >  . 11681:13141(1460) ack     1 win 65535
+0.000 > P. 13141:14601(1460) ack     1 win 65535
+0.042 <  .     1:1(0)        ack  2921 win 65535
+0.000 <  .     1:1(0)        ack  5841 win 65535
+0.004 <  .     1:1(0)        ack  8761 win 65535
+0.000 <  .     1:1(0)        ack 11681 win 65535
+0.004 <  .     1:1(0)        ack 14601 win 65535 # 灰色部分结束

+0.000 close(4) = 0
+0.000 > F. 14601:14601(0)    ack     1 win 65535
+0.050 < F.     1:1(0)        ack 14602 win 65535
+0.000 >  . 14602:14602(0)    ack     2
```

正如预期，现在每四毫秒发送四个 TCP 分段。因此，灰色高亮的 TCP 分段的时序受到了影响。

## 其他注意事项

上一个例子中，12Mbps 的 pacing 速率将应用于所有拥塞控制状态，突发大小限制为四个分段，最终启用 pacing。也就是说，RACK 堆栈会发送四个分段，等待四毫秒，然后再发送四个分段，循环往复，直到所有数据包发送完毕。

需要注意的是，RACK 堆栈不会为了等待整整四个分段而阻塞发送机会。如果套接字缓冲区中可用的分段少于四个，堆栈会立即发送现有的分段，并启动一个调整后的 pacing 定时器，使该突发按照请求的速率间隔发送。另一个重要因素是拥塞控制和流控；如果堆栈达到拥塞或流控限制，pacing 速率可能会低于设定速率。因此，RACK 堆栈有时可能只发送一、二或三个 TCP 分段，这并非因为用户数据不足，而是受到拥塞或流控的限制。静态 pacing 可能导致吞吐量降低，因为它无法在竞争流量消失后“补偿”超出设定速率的带宽。

使用静态 pacing 的开发者还需考虑至少四个交互因素。其中之一是与比例速率减少（Proportional Rate Reduction，PRR）的交互。PRR 在 RACK 进入恢复状态时触发，限制发送量大致为每收到一个入站确认就发送一个分段。这意味着数据发送既受 pacing 定时控制，也受对端确认的影响。在多数静态 pacing 应用场景中，这种交互是不可取的。因此，存在一个 `IPPROTO_TCP` 级别的套接字选项 `TCP_NO_PRR` 用于禁用 PRR，该选项的值类型为 `int`。

另一个恢复机制是快速恢复（rapid recovery）；该功能允许 RACK 根据丢包和传输时间更快恢复，而不仅仅依赖三个重复确认。但这可能改变恢复期间的数据发送速率，从而影响预期的 pacing 行为。可以使用 `IPPROTO_TCP` 级别的套接字选项 `TCP_RACK_RR_CONF` 来调整此行为。该选项允许的整数值为 0、1、2 和 3，默认值为 0，表示 RACK 可完全控制快速恢复。不同值提供调用者对恢复行为的微调。对于静态 pacing，建议将此值设置为 3，以确保仅使用指定速率。

启用 TCP 分段卸载（TSO）时，设置 pacing 突发大小会影响 CPU 负载。使用较小的突发大小会增加 CPU 负载。例如，发送 40 个 TCP 分段时，pacing 突发大小为 40 只需一次 TSO 操作，而设置为 4 则需做 10 次 TSO 操作。

设置 pacing 突发大小时还需考虑确认延迟。大多数 TCP 堆栈启用此功能，即等待两个（或更多）分段或延迟确认计时器超时（通常 40 至 200 毫秒之间，规范建议 200 毫秒，但许多系统缩短了此时间）后才发送确认。因此，如果将 pacing 突发大小设置为 1 而非 4，可能导致确认延迟计时器与 pacing 计时器交互，从而大幅降低实际 pacing 速率。为避免此类问题，建议 pacing 突发大小不要小于 2。

***

**RANDALL STEWART**（<rrs@freebsd.org>）是一位操作系统开发者，拥有 40 余年经验，自 2006 年起成为 FreeBSD 开发者，专注于 TCP 和 SCTP 等传输协议，也涉猎操作系统其他领域，目前是独立顾问。

**MICHAEL TÜXEN**（<tuexen@freebsd.org>）是明斯特应用科技大学教授，Netflix 兼职承包商，自 2009 年起为 FreeBSD 源代码贡献者，专注于 SCTP 和 TCP 传输协议、IETF 标准化和它们在 FreeBSD 中的实现。


# 读者来信

作者：Michael W Lucas

![邮箱插图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-12746a0308c9748fcf51d490759b60144062dd04%2Fwe-get-letters-01.png?alt=media)

亲爱的 “摸电脑的” 建议专栏作家： FreeBSD 的 Jail 有几种不同的联网方式。我该如何决定用哪一种？ ——困惑又苦恼的人

亲爱的 BAD：

我写这篇文字时正在 BSDCan 2025 现场。我们头顶上两层楼，有人在用冲击钻。钻头穿过混凝土建筑时震动发生偏移，频率恰好与人牙齿的共振频率一致。后来才发现，这位 “冲击钻先生” 需要证明自己在干活，才能拿到那天 50 块的工钱。我给了他 60 块，让他收工回家。别担心，这笔意外开支没影响我的 gelato 基金；我从大会经费里挪用了这笔钱。

这件事之所以落到我头上，仅仅是因为我躲闪得不够快，他们让我当了大会主席，这说明社区的某些部分需要更好的领袖选拔机制。被 “征召” 让我既困惑又苦恼。

关键是，我们都困惑又苦恼。只是困惑和苦恼的事情不同罢了。

Jail 联网是那种看起来简单、一动才发现处处是坑的话题。无论你用哪种虚拟化系统——Jail、bhyve 或某些更次要的系统——都要面对类似的抉择。Jail 可以共享宿主机的网络栈，也可以拥有自己的网络栈。在共享栈下，Jail 可以有自己的地址，或绑定到 loopback 接口上的私有地址，通过 NAT 访问外部世界。你还可以用 VNET 给 Jail 一个完整的网络栈。这几种方式都是灾难，但每种灾难都有独特的特性，可能让它更适合你的场景。

记住，Jail 是一个谎言。

Jail 自认为是一个完整的操作系统，期望对它访问的任何网络都拥有完全控制权。如果你给 Jail 分配一个 IP（公网或私网），Jail 就会认为自己完全掌控了那个地址。Jail 的 `rc.conf` 中并不包含配置接口的信息。在共享栈下，宿主机可以对 Jail 的网络为所欲为。想在 Jail 里运行 **sshd(8)**？默认情况下，SSH 守护进程会监听系统上每个 IP 地址。你必须重新配置才能改变这一行为。这不难，但很容易被忽视。同样，如果你想让多个 Jail 运行同一个数据库服务器？它们不能都用同一个端口和 IP 地址。你需要做的管理工作量，与共享该网络栈的 Jail 数量成正比。

既然如此，何不给每个 Jail 自己的网络栈？因为更复杂。你必须在宿主机上创建一个 bridge，然后为每个 Jail 创建一个虚拟接口。接口的一端进入 Jail；另一端插入 bridge。每多一层复杂，就多一次闪亮的失败机会。

VNET 的好处在于，每个 Jail 真正掌控自己的 IP 地址。你必须在 Jail 的 `rc.conf` 中设置它，并可以让每个服务独占那个地址。

VNET Jail 也不完全像宿主机。最常见的接口是 **epair(4)**。你无法在 epair 上抓包；如果想调试网络或捕获 netflow，必须在宿主机上做。你可能觉得 netflow 是网络管理员才关心的事，但并非如此。Netflow 什么问题都不解决，却能识别一切。如果我写 netflow 那本书时用的是 nfdump 而不是 flow tools，那本书现在还有用；可惜我在做糟糕选择方面天赋异禀，比如以系统管理为业。我不是说懂得 promiscuous mode、fdisk 或窗口缩放就会让人痛苦，但我敢说，快乐的人对这些一无所知。总之，epair 接口上用不了 promiscuous mode。提前规划。

共享栈 Jail 和 VNET Jail 都可以用私有或公网地址。如果用私有地址，没有出站 NAT，Jail 就访问不了外部世界。入站连接必须通过 NAT 映射。按照 “系统管理第 19 条定律”，“任何私有地址方案的目的，都是为了增加管理开销。”

你可能会合理地问我选择了哪种方案。我的托管环境有数量有限的公网 IPv4 地址，和一整个 IPv6 /64。我本可以用一种 NAT/直连混合方案，但我没有，我直接宣布某些 Jail 是 IPv6-only，只能访问互联网中 “现代” 的部分。我毫不犹豫地宣布某些人不配连接到我的私有 Port 服务器——反正我那个 /64 之外的人也下载不到。

只用公网地址会简单得多。可惜 IPv4 地址目前零售单价约 55 美元，最少要买 256 个，我挪用不出那么多钱，BSDCan 没那么多预算。

有人知道 EuroBSDCon 的财务状况吗？他们缺大会主席吗？我那位新助手用冲击钻可是一把好手。

想向 Michael 提问？ 请发送至 <letters@freebsdjournal.org>

尽管 Michael W Lucas（<https://mwl.io>）竭尽全力，仍未能说服 FreeBSD 期刊董事会把他从这个专栏里开除。暂时还没有。他最近的著作是 Run Your Own Mail Server 和 Laserblasted。他的下一本书是 Networking for System Administrators 的第二版，详见 <https://mwl.io/n4sa2e>


# 活动日历

* 作者：**Anne Dickison**

截至 2025 年 9 月的 BSD 活动。如有任何 FreeBSD 相关活动，或对 FreeBSD 用户有益却未在此列出的活动，请将详情发送至 <freebsd-doc@FreeBSD.org>。

![BSD Canada](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-21b2cdfafc5738f1b0009af04ac190c66f37cdcf%2Fevents-calendar-01.png?alt=media)

## 2025 年 6 月 FreeBSD 开发者峰会

2025 年 6 月 11-12 日

加拿大渥太华

<https://wiki.freebsd.org/DevSummit/202506>

欢迎参加 2025 年 6 月 FreeBSD 开发者峰会。本次峰会与 BSDCan 2025 同地举办，地点位于加拿大渥太华。为期两天的活动于 2025 年 6 月 11-12 日举行，内容包括开发者讨论会、厂商演讲和工作组。

![FreeBSD logo](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-985114ab68f87a01c3abd23afd9188d54e339f4e%2Fevents-calendar-02.png?alt=media)

## BSDCan 2025

2025 年 6 月 11-14 日

加拿大渥太华

<https://www.bsdcan.org/2025/>

BSDCan 是一场技术大会，面向使用 BSD 操作系统与相关项目的开发者和用户。大会以开发者会议的形式，重点关注新兴技术、研究项目和工作进展，同时涵盖用户空间基础设施项目，欢迎自由软件开发者和商业厂商贡献者共同参与。

![EuroBSDCon](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-2831b317ec96bbbcd3193a6450e5e73f7dcea733%2Fevents-calendar-03.png?alt=media)

## EuroBSDCon 2025

2025 年 9 月 25-28 日

克罗地亚萨格勒布

<https://2025.eurobsdcon.org/>

年度大会提供了难得机会：了解 BSD 领域最新动态、观摩当代部署案例，与使用 BSD 相关技术的用户和公司面对面交流。EuroBSDCon 也是思想、讨论与信息交流的起点，常常催生编程项目。


# FreeBSD 发布工程：新主管上任

* 原文链接：[FreeBSD Release Engineering: A New Sheriff is in Town](https://freebsdfoundation.org/our-work/journal/browser-based-edition/downstreams/freebsd-release-engineering-a-new-sheriff-is-in-town)
* 作者：Colin Percival

![新主管上任](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-165a45ba68c8694e4625e8f7b22c87108e15e240%2Frelease-01.png?alt=media)

2023 年 11 月 17 日，Glen Barber 在管理 FreeBSD 发布的十年后，正式卸任了 FreeBSD 发布工程主管一职。在 FreeBSD 核心团队的认可下，我接替了这一角色。

## 连续性，以及

Glen 在发布工程师岗位上表现出色，因此当我接手时，首要任务就是确保工作的延续性——尽可能沿用他之前的工作方式。得益于我担任发布工程副主管三年的经验（虽然只主导过一次版本发布——在 Glen 因肺炎住院期间负责的 FreeBSD 13.2-RELEASE），通过长期观摩 Glen 的工作方式，我对如何延续他的工作模式已经有了整体把握。

但这并不意味着我没有做出任何改变——如果是那样的话，这篇文章不会这么长。我的首要目标是简化发布流程，以免发生像 FreeBSD 13.2 那样的情况——FreeBSD 13.2 推迟了将近一个月，且发布了六个发布候选（RC）版本才解决了所有问题，才能继续发布。为此，我制定了一些发布周期的基本规则：

* 发布候选版本（RC）应该正如其名——是发布的候选版本（Release Candidate）——因此，到达 RC1 时应该没有已知妨碍发布的问题。我们的目标是构建 RC1，进行一周的测试（或略少于一周，考虑到构建完成的时间），然后构建最终的 RELEASE 镜像。如果在 RC1 中发现问题，我们可以再进行第二个或第三个发布候选版本——但这应该仅发生在出现新问题时；应该在 RC1 之前就已经解决了任何此前已知的问题。
* 尽管开发者常常有代码“需要”在下个版本中发布，但如果他们迟到了，我不会因此延误发布流程——因为这样做对所有按时将代码合并到源代码树中的开发者来说是不公平的，他们希望能尽快把代码交到用户手中。直到 -BETA1 之前，源代码树是开放的，所有人都可以合并补丁（我们曾经会“冻结”Stable 分支，但近年来这已经不这么做了）。在 BETA1 和 BETA2 之间，我通常会接受那些不那么庞大和危险的修改，但在 BETA2 和 BETA3 之间，就要考虑“我们 **确定** 这个补丁不会引发任何新问题吗？”，而在 BETA3 和 RC1 之间，补丁的要求更高，必须满足“……并且它解决了用户遇到的实际问题”，因为如果问题纯粹是理论上的，就不值得冒险，怕补丁本身出问题。当然，RC1 之后，只会接受最关键的补丁——在理想情况下，甚至不接受补丁——因为此时的任何更改都需要增加一个新的发布候选版本，并推迟发布日期。
* 如果在接近发布时出现新特性——比如在“代码软冻结阶段（code slush）”后，BETA1 前两周——并且发现无法立即且轻易地修复的问题，**我将从发布中移除这些特性**。过去发布日程经常拖延的部分原因是，特性到得太晚，然后需要多轮的错误修复才能完成发布；与晚到的代码一样，如果某个开发者在最后一刻合并了有问题的代码，导致了发布的延误，那对其他人来说也不公平。

我还要求 FreeBSD 开发者——虽然有时效果不一，但似乎逐渐有所改善——在提交代码到即将发布的版本时，要更加主动地提醒发布工程团队；只要得知了问题，我会更积极地通过邮件联系开发者，询问状态更新。曾多次收到类似“我没意识到 BETA2 已经发布了；我会尽快合并那个代码”的回复——FreeBSD 是个志愿者项目，所以开发者在生活中会有其他事情分心，从而忽视了 FreeBSD 的工作，这完全可以理解，但我们最不希望看到的就是因为某人失去时间感而导致发布日程推迟。

## 发布计划

一旦我们确认 FreeBSD 项目能够在可预测的时间内完成发布，我就开始考虑发布的具体安排。三个 BETA 镜像和一个 RC 总共需要一个月时间，我们在 BETA1 发布前有两周的“代码软冻结阶段”，并且在此之前还会有两周的提前警告期（也就是“赶紧把代码合并”）。若一切顺利，这将总共耗时 2 个月；但因为无论如何我们总是会有一些延误（至少因为我们总是会因最后时刻的安全修复而推迟发布），我们需要额外留出第三个月的时间来为发布日程提供灵活性，并给发布工程团队在发布之间留出一些时间。

在知道我们每三个月可以有效地发布一个版本后，发布的时间安排就变得显而易见：每个日历季度发布一个版本。如果我们从季度的第一个月开始，先是两周的“警告期”和两周的“代码软冻结阶段”，然后在第二个月每周发布 BETA 和 RC1，那么 RELEASE 公告就会出现在第三个月的开始。如果发布推迟了几周，我们仍然能在第三个月结束之前完成。这为后续的发布提供了一个目标日程——我们永远不会发布不成熟的版本，但明确我们方向是什么是一个必要的起点：

* BETA1 前 28 天：向开发者发出警告。
* BETA1 前 14 天：开始代码软冻结阶段。
* 第二个月的第一个星期五：发布 BETA1。
* BETA1 + 7 天：发布 BETA2。
* BETA1 + 14 天：发布 BETA3。
* BETA1 + 21 天：发布 RC1。
* BETA1 + 28 天：开始构建 RELEASE。
* BETA1 + 32 天：发布 RELEASE 公告。
* BETA1 + 39 天：将发布分支交给安全团队。

唯一的例外是 `.0` 版本：这些版本会在 Stable 分支创建之前从 ALPHA 构建开始，显然无法将整个 `.0` 版本的过程压缩到三个月内；因此，为了使其他版本的日程能够与日历季度对齐，我决定为这些版本预留六个月的时间——即两个日历季度。关于这六个月内的具体时间安排，我尚未决定——并且由于我没做过 `.0` 版本的发布，因此可能会在 15.0 上犯错——但我确实希望最终能够为这些版本建立一个可再现的发布计划。

一旦确定了每个版本所需的时间，剩下的就是确定版本的发布顺序，并决定多久发布一次 `.0` 版本。第一个问题很容易回答：在同一个 Stable 分支发布之后的三个月内没有必要再发布一个新版本，所以我们将 Stable 分支之间交替发布，即 14.0、13.3、14.1、13.4、14.2 等等。第二个问题的答案来源于多年来 FreeBSD 开发者之间的讨论：我们希望每两年发布一个新的大版本。过去，`.0` 版本的发布周期通常是在 FreeBSD 开发者峰会后不久开始的——这确保了大量开发者可以参与讨论，希望在发布前完成的特性——由于最大规模的开发者峰会通常在 5 月或 6 月的 BSDCan 举行，因此将 `.0` 版本的发布安排在每个奇数年份的下半年，大约在 11 月或 12 月发布是合适的。

这就为每个 FreeBSD 版本的发布提供了时间表：

* 2025 年 6 月：FreeBSD 14.3
* （2025 年 9 月：这一季度没有发布；正在开发 15.0）
* 2025 年 12 月：FreeBSD 15.0
* 2026 年 3 月：FreeBSD 14.4
* 2026 年 6 月：FreeBSD 15.1
* 2026 年 9 月：FreeBSD 14.5
* 2026 年 12 月：FreeBSD 15.2
* 2027 年 3 月：FreeBSD 14.6
* 2027 年 6 月：FreeBSD 15.3
* （2027 年 9 月：没有发布；正在开发 16.0）
* 2027 年 12 月：FreeBSD 16.0

## 支持周期

从 FreeBSD 11.0-RELEASE 开始，FreeBSD 对发布版本的支持政策是，次要版本在下一个来自同一 Stable 分支的版本发布后的三个月内得到支持，而 Stable 分支则从 `.0` 版本发布日起支持五年。

这一政策的前半部分——次要版本的支持持续时间——与新的季度发布计划非常契合：正如我们几乎每个季度末都会发布新版本一样，每个版本也会在每个季度末达到其生命周期的结束。此外，由于现在每个版本都会在每个季度的第三个月发布，“次要版本之间有三个月的重叠”这一政策实际上就变成了“一个季度的重叠”。

新发布计划与已确立的支持时间线不太契合的地方是对 Stable 版本的支持周期：每两年发布一次新的 `.0` 版本（从而创建新的 Stable 分支），并将每个 Stable 分支支持五年，会导致三个 Stable 分支同时受支持——经验告诉我们，这对于我们这样的项目来说并不容易做到。

因此，为了更好地将支持时间线与发布计划对齐，核心团队、安全团队和 Ports 管理团队批准了调整 Stable 分支的支持时间线：从 15.x 开始，Stable 分支将支持 4 年而不是 5 年。这个变化可能没有听起来那么大：实际上，在第五年，Stable 分支的支持始终不太好。

通过这一改变，将始终有两个 Stable 分支得到支持，除了每两年会有一个持续几周的短暂窗口期，在此期间，当 `(N+2).0` 发布时，N.x 分支即将达到生命周期结束时，三个 Stable 分支会同时得到支持。

## 过时（legacy）版本

多年来，FreeBSD 网站的部分内容（有时被注释掉）提到过“过时（legacy）”版本，但据我所知，直到现在并没有明确说明“过时（legacy）”版本的定义。在 Glen 担任发布工程师期间，这一概念大多被弃用，因为他认为将一个版本称为“过时（legacy）”会误导用户，使其认为该版本质量较差，而实际上我们对所有 FreeBSD 版本的质量要求都是一样的。

我决定重新引入这一概念，帮助用户（尤其是新用户）决定应该安装哪个版本。为此，我给出了以下定义：

**“过时（legacy）”版本是指尚未达到生命周期结束的版本，但发布工程团队不建议用于新系统的部署。**

换句话说，它是为了那些已经安装了 FreeBSD 并且不想升级到最新版本的用户准备的；但如果你是安装新系统，应该选择一个较新的版本。

一般来说，一旦 FreeBSD 的 N.1 版本发布，`(N-1).x` Stable 分支将被视为“过时（legacy）”，而当从 Stable 分支发布新的次要版本时，该分支的前一个版本立即变为“过时（legacy）”。换句话说：通常，除最新 Stable 分支的最新版本外，其他版本都被视为“过时（legacy）”，但在新的 Stable 分支以 `.0` 版本发布后，之前 Stable 分支的最新版本将保持为“非过时（non-legacy）”。

最终，FreeBSD 用户可以使用他们喜欢的任何版本，这并不意味着会限制 FreeBSD 开发者关于合并功能和修复错误的决策；这一点纯粹是为了帮助新手和经验不足的用户选择正确的版本进行下载。

## 接下来是什么？

在 2024 年，我已经做出了上述变化，并管理了 13.3、14.1、13.4 和 14.2 版本。那么接下来呢？

首先，我们已经安排了 2025 年的三个发布版本：13.5、14.3 和 15.0。到目前为止，我所做的次要版本每个都花费了 50 到 100 小时的时间（13.x 版本用时较少，14.x 版本用时较多，因为 14.x 有更多新代码）。我很幸运得到了来自亚马逊的赞助，用于我的 FreeBSD 工作（不仅是为了在 EC2 平台上专门维护 FreeBSD，还包括一般的发布工程工作）——没有这个赞助，FreeBSD 14.2 版本将缺少某些晚期加入的功能，因为如果没有足够的时间，问题就无法及时解决。当然，FreeBSD 15.0 将是一个新的主要版本——这是我以前从未做过的——我确信它会花费更多的发布工程时间。

但除了“常规”流程，即每周发布快照和（大多数情况下）每季度发布之外，未来还有两项重要工作：首先，FreeBSD 15.0 应该会推出一款基于软件包的系统——pkgbase 已经“即将推出”太长时间了；其次，在主权技术机构资助下，FreeBSD 基金会正在资助一个项目来现代化 FreeBSD 的构建过程（特别是使得尽可能多的构建可以在没有 root 权限的情况下完成）。这两项工作都将涉及发布工程过程中的重大变化，但它们都不应影响我们按可预测的时间表推出 Stable 且经过充分测试的版本的目标。

***

**Colin Percival** 是 FreeBSD 发布工程负责人和 FreeBSD/EC2 平台的维护者，2019 年因其贡献被评为“AWS Hero”。他平时的本职工作是 Tarsnap 的创始人兼主要开发者，Tarsnap 是一家在线备份服务。


# GhostBSD：从易用到挣扎与重生

* 原文链接：[GhostBSD: From Usability to Struggle and Renewal](https://freebsdfoundation.org/our-work/journal/browser-based-edition/downstreams/ghostbsd-from-usability-to-struggle-and-renewal)
* 作者：Eric Turgeon

这篇文章并不是为了呈现技术细节，而是从一个更高层次的角度回顾 GhostBSD 这些年来的发展历程，项目的当下状态，以及我们的后续方向。如你所知，GhostBSD 是一款基于 FreeBSD，用户友好的桌面操作系统。其使命是为那些想要 FreeBSD 强大功能，但不想面对手动设置复杂性的用户，提供简单、稳定和易于使用的桌面体验。在开始这段旅程时，我还是名非技术用户，梦想着有一天会有一款所有人都能使用的 BSD 操作系统。

## GhostBSD 的起源

2007 年，我读了 Eric S. Raymond 的《How To Become A Hacker》（如何成为一名黑客）。书中提到了 BSD Unix 是个让有志贡献者学习和成长的地方。这激发了我对 BSD 的好奇心，促使我开始探索 FreeBSD。那时，我正在使用 Ubuntu，心中产生了一个问题：FreeBSD 能否成为像 Ubuntu 一样适合非技术用户的桌面操作系统？当时，我自己也是非技术用户。我只是喜欢 Ubuntu 的简便性，并且好奇为什么 FreeBSD 不能像 Ubuntu 那样。2008 年，这个问题激发了我创建 GhostBSD 的梦想，我开始学习一切我需要的知识来实现这个项目，使 FreeBSD 对我来说变得像 Ubuntu 一样容易上手。

我花了将近两年的时间才理清一切，尝试使用像 FreeSBIE 这样的工具来制作一款 live CD，并寻找一些代码来构建 GNOME。对初学者来说，FreeSBIE 很难驾驭。作为一位讲法语的加拿大人，《FreeBSD 手册》对我有很大帮助，但我的英语水平有限，我强迫自己去学习它。我翻阅论坛和文档，拼凑 GNOME 的构建，常常是更容易把事情搞乱，而不是让它们正常工作。一个有趣的事实是，“GhostBSD”这个名字来自我的妻子。当时，她还是我的女朋友。她提议的这个名字，我就采纳了。我们在 2009 年 9 月结婚，正好是在项目初具雏形的时候。2009 年 11 月，GhostBSD 1.0 Beta 发布，作为一款运行 GNOME 的 FreeBSD 8.0 live CD。它非常粗糙，充满了问题，但为 GhostBSD 后来的发展奠定了基础。来自 FreeBSD 社区的反馈促进了早期的进展。在这个过程中，有一些人加入进来，帮助我学习源代码控制版本管理和其他技术。像 Ovidiu Angelescu 这样的人，通过他的 shell 脚本技巧让我向 SVN 迈进。我学到了很多东西。现在我们使用 Git，但那时一切都是 SVN。

## 初期阶段

首个版本是 GhostBSD 1.0 Beta，它让用户体验到了 FreeBSD 和 GNOME 的组合。尽管是个不稳定的开始，但它证明了这一概念的可行性。到 2010 年，1.5 版本通过使用来自 PC-BSD 的 pc-sysinstall 工具增加了一款基于文本的安装器，使得设置过程更加简便。那些早期的几年涉及到深入学习 FreeBSD、shell 脚本编程和与像 Ovidiu Angelescu 这样的人合作。我向 Ovidiu 请教了很多 shell 脚本的技巧。2011 年，2.5 版本引入了图形化的 GBI 安装器，基于同样的 pc-sysinstall 后端构建。这个后端至今仍然是核心组件。它的可靠性意味着我不必重新造轮子。这是一个坚持使用有效方法的教训。

大约在 2012 年开始了 NetworkMgr 的基础工作，NetworkMgr 是一款图形化工具，可用来管理以太网和 Wi-Fi 连接。这是朝着提高可用性迈出的第一步，灵感来自 Linux 的 NetworkManager。到 2015 年，我将它从 ghostbsd-build 中提取出来，单独进行优化以作为一款独立工具。2013 年，3.5 版本发生了一个重大转折。GNOME 3 在 FreeBSD 上的发布是笨重且不稳定的，存在延迟和大量资源占用的问题。这种情况与 GhostBSD 的目标相冲突。我们尝试了其他桌面环境，制作了多个带有不同桌面环境（如 LXDE、XFCE 和 Openbox）的 ISO 文件，GhostBSD 的含义——“在 BSD 上运行 GNOME”——面临了身份危机。随着 MATE（GNOME 2 的一个分支）的出现，这次危机得到了挽救。我们转向了 MATE，因为它简单且熟悉：它感觉像 GNOME 2 一样舒适亲切，在 FreeBSD 上运行时比 GNOME 2 更加轻便。当时，我不确定该如何处理其他桌面环境，因为一些人已经从项目中离开了。我开始放弃所有其他的桌面环境，但 XFCE 仍然作为一种方案保留下来。MATE 成为了旗舰桌面，重新定义了 GhostBSD 对可用性的重视，而非追求炫技。

## 系统基石的转变

我在 2014 年开始着手开发 Update Station，旨在为 GhostBSD 用户带来图形化更新工具。我原本以为这项工作是在之后才开始的，但在回顾 GitHub 后，我发现大约在那个时候，它已开始成型。最初，GhostBSD 依赖于 FreeBSD 的发布源、tar 包和官方包。曾经有一段时间，我们需要开始为像 NetworkMgr 这样的工具提供更新，这促使我们建立了自己的包仓库。我们的自定义包经常与 FreeBSD 的版本升级发生冲突，产生摩擦，要求我们寻求更好的解决方案。此外，对于 Update Station 的自动化来说，`freebsd-update` 并不容易实现。`freebsd-update` 无法满足我们“图形化优先”目标的需求。于是我们开始关注 TrueOS 的做法。我注意到他们使用了 PkgBase，这引起了我的兴趣。他们由 pkg 驱动的操作系统更新承诺带来了图形化自由。

在 2018 年，GhostBSD 18.10 转向 TrueOS，将其作为基石。TrueOS 提供了 PkgBase，让我们可以使用 pkg 工具更新操作系统，并且我们放弃了 `freebsd-update`。TrueOS 还带来了 OpenRC，这是一款现代的服务管理器，非常适合构建用于管理服务的图形化界面，虽然这个目标最终并未实现。转向 TrueOS 使得能从图形界面使用 `Update Station` 升级软件和操作系统。对我们来说，这是一次革命性的变化：让我们的用户能够通过 `Update Station` 升级操作系统。随后，TrueOS 引入了 OS Ports，让我们可以使用 poudriere 从 Ports 树构建操作系统包，并为更新提供了更精细的控制。

## 遇到的挑战

TrueOS 于 2020 年关闭了，这给我们带来了压力，需要维系我们所获得的一切。我最初想保留 OpenRC，但维护 ghostbsd-ports 和 ghostbsd-src 上的所有服务变成了一个独自一人的斗争。那时，我几乎是独自一人维护项目，这耗费了我大量的时间，使我无法专注于改进和管理 GhostBSD。到 2022 年，我放弃了 OpenRC，转而采用 FreeBSD 更简单但可靠的 RC 系统。这样做意味着我可以减少工作量，更加专注于目标。到了 2023 年，维护 OS Ports 变得过于繁重。2024 年，我决定将操作系统包的构建从 FreeBSD 维护的 PkgBase 转移到 SRC 中，从而简化了维护负担，并重新聚焦于用户体验。

今年 1 月，我意识到从 STABLE 构建 GhostBSD 对我们的小团队来说太过沉重。在与其他贡献者讨论后，我决定切换回 FreeBSD RELEASE。是的，我们失去了对早期驱动程序的利用，但我们节省了调试 STABLE 变更的时间，获得了更稳定的基础来构建。我并不是说 STABLE 总是出问题，但有时一些变更确实会导致问题。

这些年来，通过做出重大抉择，我尽我所能地管理 GhostBSD，这些选择虽然增加了困难，但也为 GhostBSD 缺失的部分带来了收益。PkgBase 和 OS Ports 提供了来自图形界面的操作系统更新，但 OpenRC 增加了额外的工作负担。然而，这也意味着增加了项目的维护负担，超出了项目的处理能力。所有这些最新的变更标志着 GhostBSD 回归其根本，并重新聚焦于可用性，而不是过度复杂化 GhostBSD 的维护。这是一个艰难的教训——保持简单。

## 当前 GhostBSD 的状况

在我写这篇文章时，我们刚刚发布了 GhostBSD 25.01-R14.2p1。这标志着从 FreeBSD STABLE 切换到 FreeBSD RELEASE，使用 14.2-RELEASE-p1 能提供更好的稳定性。新的 GhostBSD 版本号分解如下：25 代表 2025，01 代表 GhostBSD 补丁，R 代表 RELEASE，14.2 代表 FreeBSD 版本，p1 代表 FreeBSD 补丁。这个版本号旨在为用户明确发布内容，不再让用户猜测版本含义。经过这些最近的变动，我感觉我们现在处于一个很好的位置，能够专注于改进我们的工具。

其中一些工具包括：

* **NetworkMgr**：一款用于以太网和 WiFi 的图形化软件，模仿 Linux 的 NetworkManager。通过简单点击替代命令行的混乱。
* **Update Station**：一款图形化软件，用于软件和操作系统升级，升级前会创建 Boot Environment 的备份。安全第一！
* **Software Station**：一款图形化软件，利用 pkg 安装软件。点击、选择、完成。
* **ghostbsd-build**：用于构建 GhostBSD，包括 Joe Maloney 的 ZFS reroot hack，用于在内存中的读写 ZFS 池实时会话。对于演示和安装来说，速度非常快。
* **Backup Station**：由 Mike Jurbala 于 2022 年 9 月引入，它是一款图形化软件，使用 pybectl，这是一款与 bectl 接口的内部 Python 模块，用于管理 Boot Environments。简化系统快照操作。
* **GBI 和 pc-sysinstall**：GhostBSD 的图形安装器最近在界面中去除了 UFS，转而利用 ZFS 的优势。ZFS 的强大优势超越了旧的方式。

## GhostBSD 未来展望

对于 2025 年，我计划记录一些标准操作程序（SOP），以便让更多人能够轻松参与 GhostBSD，希望新的贡献者不需要太多的指导。我将为自己家里的服务器部署一台更快的构建服务器，以便更迅速地构建软件包，当前我正等待 PDU 到货。我还希望完成一款 OEM 友好的安装器，以扩大我们的影响力，重新设计 Update Station 以便在启动时安装更新，并改进 NetworkMgr 与 devd 的集成以提高稳定性。如果可能的话，我还计划为 GBI 添加创建主目录数据集的支持，并且可以选择进行加密。FreeBSD 即将在 2025 或 2026 年提供的 AC 和 AX WiFi 支持将带来更好的连接速度，我对此感到非常兴奋。我们的笔记本电脑非常需要这个。

我不能代表其他贡献者发言，但我们在 GitHub 上有一长串任务。我们确实有一份路线图，可以在 GhostBSD.org 的开发标签下找到它。如果你感兴趣，可以去看看。

从长远来看，我们在期待更多的捐款，以便购买一台 ARM（Ampere）服务器，开始构建 GhostBSD 的 arm64 版本。与此同时，GhostBSD 仍然致力于成为一个完全由图形界面驱动的操作系统，利用 ZFS，这非常适合那些希望享受 FreeBSD 提供的功能但不懂技术的用户。我们正在讨论创建一些桌面组件，逐步替代 MATE，这些组件将更好地与 FreeBSD/GhostBSD 对接，比如一个利用 ZFS 的文件管理器。然而，目前这些讨论还没有形成实质性的成果。

## 结论

GhostBSD 是一段经历，一连串的选择，从 2009 年的 Live CD 到今天的模样。它起步于一名非技术用户的梦想，逐渐发展成了一个社区项目。每一步，像是定制包、TrueOS、ZFS reroot 实时会话和 PkgBase 都解决了一个挑战。过去的经历教会了我坚韧，当前带来了稳定，而未来邀请你们一同塑造。如果你有兴趣参与其中，请访问 GhostBSD.org。你会找到属于你的位置。

***

**ERIC TURGEON** 是 GhostBSD 的创始人和领导者。他还是 FreeBSD Ports 的提交者，专注于维护 MATE 和 NetworkMgr 的 Ports。Eric 居住在加拿大，自 2000 年代末期以来一直对 BSD 怀有热情。他在 GhostBSD、FreeBSD 的贡献、工作和个人生活之间平衡时间。他的动力来自于让 BSD 更加普及，他欢迎所有人加入 GhostBSD 社区。


# BSD Now 与将来

* [BSD Now and Then](https://freebsdfoundation.org/our-work/journal/browser-based-edition/downstreams/bsd-now-and-then)
* 作者：Benedict Reuschling

![BSD Now 与将来](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-252f2e38c47f3e2fb174a325e4078983925f1118%2Fnow-01.png?alt=media)

BSD Now 播客最近庆祝了其第 600 集，这似乎是一个绝佳的机会，让 FreeBSD 期刊的读者深入了解这个长期运行的 BSD 节目的幕后故事。

## 谦逊的开始

BSD Now 始于 2013 年，由 Allan Jude 和 Kris Moore 主持。当时我还是名普通听众，兴奋地等待收听 BSD 领域的最新消息。BSD 领域迅速变成了“BSD 之地”，正如 Kris 曾著名地说过的那样。这个节目提供了新闻和教程，其中后者是 Allan 单独录制的。他曾告诉我，这很困难，因为你不仅要打字，还要解释自己在做什么。打错字和其他意外的计算机故障意味着要么撤销更改并剪辑录音，要么完全重新开始。从一开始，节目就提供了一个反馈渠道，让听众通过电子邮件提交想法、节目内容和关于 BSD 领域的讨论，而这些反馈会在每集的结尾被读出。人们经常用这个渠道来询问关于安装或使用 BSD 的问题。有时，Allan 会提供他在 ZFS 方面的丰富知识，帮助用户在家构建 NAS 或理解一些难以理解的概念。

Kris 也分享了来自 PC-BSD 方面的视角，讲述了现代 Unix 桌面的一切。这种结合包含了我作为 BSD 用户所寻找的所有优点。因为你只需要等一周就能听到下一期 BSD Now，这意味着我的 BSD 电池（每次参加完会议后都会充满电）不会很快耗尽。这种情况持续了好几年，而这简短的早期节目的描述并不能完全呈现其意义——Allan 和 Kris 还可以讲述更多那个时代的故事！

我清楚地记得，BSD Now 对我来说不再仅仅是我收听的播客，而是变成了更多东西。我们当时在 FOSDEM，并且以小组的形式去了布鲁塞尔众多好餐厅中的一家。当我们坐在一张长桌子旁等待晚餐时，Allan 提到 Kris Moore 因为生活越来越忙而打算从播客中退出。他们正在寻找一位接替的主持人。听到这个消息，我天真地问了一句，这个过程是什么样的。我有时忍不住说话，而那时我甚至没考虑过自己要接手。Allan 简单地解释了这个过程，如何每周见面一个小时进行录音，以及节目的技术后勤工作。这一切听起来都很令人兴奋，而我一直愿意在有需要时提供帮助，所以我说我愿意加入。Allan 喜欢这个想法，我们在其他渠道上继续讨论（顺便说一句，晚餐已经上桌了）。

这个变化直到 2017 年 3 月 16 日在 AsiaBSDCon 会议的一个独立房间里进行的录音时才被宣布。第 185 集的交接节目给了 Kris 一个告别的机会，也让我有机会介绍自己。这些早期的节目确实有点坎坷，但最终我掌握了它。每周练习我的英语并与时俱进地了解 BSD 领域的情况，感觉非常好。

## 独立之路

BSD Now 最初是作为 Jupiter Broadcasting 播客网络的一部分运行的。这意味着节目之间会有一些交叉推广，因为听众很可能也会尝试该网络下的其他节目。此外，自 TechSNAP 时代以来，Allan 一直是位受欢迎的人物，这可能帮助提高了听众数量。该网络还提供了大量后勤支持，如后期制作。对他们来说，这只是另一个要剪辑并按时发布的节目，这使我们能够完全专注于录制和内容。

播客领域经历了一些并购整合。2018 年秋季，Linux Academy 宣布与 Jupiter Broadcasting 合并，随后又被在线教育平台 A Cloud Guru 收购。尽管这类并购通常伴随着业务重组，我们节目仍得以在新公司旗下继续制作。主持人 Allan 与 Jupiter Broadcasting 的创始团队——包括 2008 年创立该网络的 Chris Fisher 和 Bryan Lunduke——保持着良好关系。2020 年 4 月 23 日第 347 期节目中，我们宣布将从 Jupiter Broadcasting 独立（注：Chris Fisher 于 2020 年 8 月重启了独立运营的 Jupiter Broadcasting）。这一转变如同作家选择自出版，意味着我们需要处理大量节目运营事务。所幸我们留住了 Angela Fisher 负责幕后工作，包括将成品节目发布至各大播客平台，因此仍能像以前一样维持每周正常更新。我们的制作人 JT 从第 100 期左右开始参与制作。他自 2013 年起就担任《Linux Action Show》的制作人，后因 Chris Fisher 调整节目方向不再需要其协助。恰逢 Allan 正在寻找接替原制作人 TJ 的人选——鉴于 JT 曾与 Ken Moore 共同开发 Lumina 桌面环境并参与 PC-BSD 项目，Allan 便邀请他加入。由此 JT 成为我们的常驻制作人，其精湛的后期剪辑技术（每期约一小时工作量）使 Allan 和我能专注节目内容创作，无需额外耗时。虽然我们从不依赖点击量或观众导向的营销模式获取资金，但支付团队报酬仍是必要开支。长期以来，Colin Percival 的 Tarsnap 始终是我们的赞助商，我们从不吝于称赞他运营的备份服务。这份稳定的赞助支持着我们这个小播客保持独立运作，我们对其持续慷慨的资助始终心怀感激。

## 节目制作内幕

每期节目的诞生始于两位主持人敲定录制档期。确定时间后通知制作人 JT，他会在共享文档中搭建节目提纲框架。我们都会为 BSD Now 贡献新闻素材，并发送给制作人。来源包括科技新闻网站、论坛、个人博客、听众邮件投稿，以及那些常年产出优质内容（篇幅符合我们偏好）的“老面孔”：要么是专注 BSD 的垂直媒体，要么是长期深耕 BSD 报道的独立平台。

JT 会根据多重标准筛选内容：新闻价值、时效性（突发新闻）、篇幅长短，以及下期录制人选——最后这点尤为重要，因为某些主持人更擅长特定领域（除了我......或许我的作用就是维持平衡？），能在相关环节提供深度解读。重大时事通常作为头条，短讯则归入新闻快报或 Beastie Bits 板块。所有内容都需精心编排以适应 45 分钟时长，若某则报道篇幅过长则需剪辑。为此我们严格把控时间，仅访谈和特辑会适当延长。

当提纲草案完成并上传至不断膨胀的节目文档库后，我会补充元数据：录制日期、基于节目中新闻项目的标签，当然还有标题。没错，那些让你在播客客户端里看到就皱眉的奇葩标题都出自我手。节目早期曾有条不成文规定：标题必须由三个单词组成。不确定这个限制从何而来，但也许是营销方面的考虑（标题朗朗上口之类）。这种限制如同双刃剑：简洁标题不易冗长且便于记忆，但要从节目内容提炼出妙题却非易事。比如报道 NetBSD 运行巨型摩天轮的新闻，该起什么标题？“命运之轮”？“NetBSD 巨轮”？还是“旋转的 NetBSD”？我常为此绞尽脑汁（不过至今没有同事在录制前抗议或要求修改）。

当这些筹备工作就绪，我们就拥有了一份可立即录制的完整提纲。理想情况下，提纲应提前足够时间供全员预习内容，但现实往往事与愿违——有时我们不得不在录制时才首次接触某些报道。虽非最佳状态，这种“初见反应”反而能带来最真实的即时反馈。

## 录制现场

正式录制前，我们会协商分配报道板块，确保各自能负责最擅长的内容。通常采用轮换制推进流程——这种模式既能让他人从长篇解说中稍作休整，也为后续内容预留准备时间。

录制过程中，视频连线窗口始终开启着（尽管我们身处不同空间，也非专业录音棚），这种“视觉同步”被证明至关重要：能判断对方何时结束发言，通过点头、手势甚至举起正在讨论的资料进行无声交流。请注意，我们使用的虽是统一专业设备，意外仍可能随时降临——网络中断、窗外施工、飞机轰鸣、快递员按门铃都曾导致录制中断。所幸这些状况并不频繁，后期制作能修复部分音频故障，但绝非万能。若事故严重，我们不得不局部重录甚至推翻整期内容（最坏的情况）。这对连贯性创作是巨大挑战：我们采用一气呵成的录制模式，强行插入补录片段或复现原话都会让听众察觉不自然的断裂感。

音频问题可谓家常便饭，有时甚至需听众反馈才察觉异常。可能是某方音量过低、音轨错位，或者互相抢话，或是麦克风参数莫名改变。最近我才发现，从首期沿用至今的耳机竟早已单边失灵——最初还误以为是对方音量设置问题。更换新耳机后，搭档们的声音清晰得仿佛近在耳畔。音频工程这门学问，实在是门玄学！

## 特别企划

Allan 在创办 BSD Now 之前就深耕播客领域，我们持续保持着采访 BSD 业界人士的传统。会议现场也是绝佳的录制场所——只需把采访对象拉进安静角落即可展开对话。这类即兴采访充满刺激与未知，因为我们无法预先准备完整的采访提纲。常规访谈会预先设计问题，但会议现场的采访必须根据受访者身份临时发挥。我始终很高兴有人愿意这样做。他们通过我们的平台触及更广受众，分享项目进展、招募测试人员，并讲述与 BSD 结缘的故事——这是 Allan 和 Kris 总是作为第一个问题提出的，我们保持着这个传统。

第 103 期对 Bryan Cantrill 的访谈令我着迷。尽管当时对他一无所知，但那些 Unix 秘辛与犀利观点让我反复重温这期“常青树”内容。众多听众的强烈反响促使我们在第 117 期安排了后续访谈。这些触及行业痛点的对话，如今回望竟与 IT 领域的发展轨迹形成了奇妙呼应。

多位业界翘楚也曾慷慨受访，提供他们独特的经验和专业知识：Michael W. Lucas 堪称“这些年来最频繁受访的嘉宾”，我们既追踪他书籍创作的博客动态，也总在新书发布时邀他畅谈。他的访谈为自出版行业提供了珍贵洞见，文字与语音在此形成了绝妙互补。

当主持人需要长期休假时（人生总有意外），我们会邀请客座主持。我曾与因 BSDCan 而闻名的 Dan Langille（这只是他众多成就之一）合作过至少两期。第 404 期《BSD Now 主持人大失踪》堪称史无前例——全体常规主持被临时替换，这需要制作人 JT 提前筹划，因为我们需要寻找并说服替代主持人，而他似乎乐在其中。

某些特殊编号的节目也值得纪念：386、486 等极客数字自然要有特别安排。虽然对 666 期有些忐忑，但我已开始期待 686 期。最近我们还在讨论：999 期是否该为这趟漫长旅程画上句号？也许突破千期大关后继续下去——天空才是极限，现在下定论还为时过早。

节目排期从非易事。节假日、会议行程、突发疾病或其他事情时常发生。如今我们总会预留一期缓冲节目来应对一周的缺席。圣诞新年等假期则提前录制特别内容——这种繁忙假期中的额外工作，换来的是在常规录制日与亲友共度的时光。

## 人员更迭

随着生活日渐忙碌，Allan 需要从每周录制中抽身。我们首先改为一次录制两期，这样另一周就有空闲时间，之后在第 400 期（注意到数字规律了吗？）迎来第三位主持人 Tom Jones。新模式下 Allan 和 Tom 每月只需参与一次录制，而我保持每两周固定出勤。Tom 带来的网络工程专长完美弥补了我的知识盲区。

颇具戏剧性的是，在轮休期间，早已是播客老手的 Allan 又创办了《2 1/2 admins》节目。若没有他当年勇敢尝试 BSD 主题播客，就不会有我们每周的精彩内容。作为创始团队最后留守的成员，他于 2023 年宣布将专注 Klara Systems 公司运营，并在 2023 年 6 月 BSDCan 大会现场观众面前录制的第 512 期（数字玄学再现）中公开这一决定。为确保节目延续，他推荐了资深播客人 Jason Tubnor 加入。如今 Allan 仍活跃在我们的内部制作频道，随时提供建议。制作人 JT 则全面接管了节目前后期制作及商业运营，包括建立 Patreon 赞助体系、整理历史节目库、维护网站等重任。

Jason 为节目注入了丰富的 OpenBSD/FreeBSD 实战经验，无论是系统安全、生产环境部署还是 bhyve 虚拟化，他总能提供深度解读。我尤其期待与 Tom 或 Jason 在正式录制前的闲聊环节——这些充满真知灼见的对话后来被制作成 Patreon 高级赞助者专属内容，涵盖当前项目、洞察、小段吐槽、即将举办的活动和其他事务。之后，我们就像以往那样开始正式的节目录制。

## 听众回声

每周稳定产出节目堪称壮举，听众也期待每周都有新节目。听众反馈令人欣慰：有人坦言追不上更新进度只能跳着听，有人对我们报道的内容发表看法，或对我们未解问题补充解答，更有被报道的博主专程致谢。于我而言，推荐优质博客既能丰富节目内容，又能为创作者带去额外流量——这种良性循环或许能激励他们继续撰写 BSD 或 Unix 相关博客，这些内容将在未来的节目中再次出现，甚至促成听众与作者间的合作契机。

不止一次有人凭声音认出我。无论是在 BSDCan 上从未谋面的人走过来打招呼，还是大学里偶尔有学生提及这个播客。这样的对话开始总是很棒，我通常想知道他们喜欢节目的哪些方面。虽然远未到名人级别（也未必向往那种生活），但知道有人每周期待我们的节目、感激我们投入的努力，这就足够了。反馈邮件支撑着我们继续前行。这是一种有价值的体验，其价值不在于我们有多少听众，而在于每个人从中获得的个人收益。多年来我们回答了大量技术问题，我确信这推动了 BSD 的普及。

## 未来之路

常有听众询问为何不恢复早期视频形式。首先，剪辑时同步视频口型很困难，而且不只是展示我们的脸，还要展示网站甚至视频素材，工作量翻倍。JT 将承担大部分剪辑工作。对我们主持人来说，这不是太大问题，我们的软件在录制时确实会记录视频。一些听众也评论说视频没有给他们带来任何附加价值。无论是因为我的脸型，还是他们在通勤时听音频（45 分钟似乎是个理想时长），对一些人来说视频并非福利。作为对想看到我们的听众的妥协，我们近期向 Patreon 赞助者开放了未剪辑原始视频源。这是未经任何剪辑的原始素材，但如果你想看到说话的人头，它们就在那里（需要付费）。

节目形式会否再有变革，无论是节目运行方式还是谁在对着麦克风说话？我们没有具体的计划，但正如节目的历史所示，我们有时必须适应变化的时代和环境。节目是独特的，就像 BSD 一样。播客仍然像以往一样受欢迎，我们似乎已经开辟了自己的小天地。六百多期走来，所有这些都得益于我们的赞助者和听众，他们陪伴我们超过了 600 期。如果没有人们写博客、写代码、举办活动和撰写关于 BSD 的文章，我们就没有什么可报道的。我每周都对我们将报道什么以及听众会如何反响感到兴奋。如果这一点不变，那么这里就永远是每个人的 BSD 之地……

***

**BENEDICT REUSCHLING** 是 FreeBSD 项目的文档提交者。过去，他曾任两届 FreeBSD 核心团队成员，并曾担任 FreeBSD 基金会副主席。他在德国达姆施塔特应用科技大学管理大数据集群八年多，还为本科生教授“Unix for Developers”课程。Benedict 也是每周 bsdnow\.tv 播客的主持人之一。


# 字符设备驱动教程（第三部分）

* [Character Device Driver Tutorial (Part 3)](https://freebsdfoundation.org/our-work/journal/browser-based-edition/downstreams/character-device-driver-tutorial-part-3)
* 作者：John Baldwin

在第 1 部分和第 2 部分中，我们实现了一款简单的字符设备驱动程序，该驱动程序支持了基本的 I/O 操作。在本系列的最后一篇文章中，我们将探讨字符设备如何为用户进程中的内存映射提供后备存储。与上一篇文章不同，我们不会扩展回显设备驱动程序，而是将实现新的驱动程序来演示内存映射。可以在与回显驱动程序相同的仓库中找到这些驱动程序，网址为 <https://github.com/bsdjhb/cdev_tutorial>。

## FreeBSD 中的内存映射

要理解字符设备中内存映射的工作原理，首先必须了解 FreeBSD 内核如何管理内存映射。FreeBSD 的虚拟内存子系统源自于 Mach 虚拟内存子系统，后者继承自 4.4BSD。虽然 FreeBSD 的虚拟内存（VM）在过去三十年里经历了重大的变化，但核心抽象依然保持不变。

在 FreeBSD 中，虚拟内存地址空间由虚拟内存映射（`struct vm_map`）表示。一个虚拟内存映射包含一组条目（`struct vm_map_entry`）。每个条目定义了一个连续地址空间范围的属性，包括权限和后备存储。虚拟内存对象（`struct vm_object`）用于描述映射的后备存储。一个虚拟内存对象拥有自己的逻辑地址空间页面。例如，磁盘上的每个常规文件都与一个虚拟内存对象相关联，其中虚拟内存对象中的页面逻辑地址对应文件中的偏移量，而逻辑页面的内容则是文件中给定偏移量处的文件内容。每个虚拟内存映射条目将其后备存储标识为从单个虚拟内存对象的特定偏移量开始的一系列逻辑连续页面。图 1 展示了如何使用单个虚拟内存映射条目将 C 运行时库的 `.data` 部分映射到进程的地址空间中。

**图 1：C 运行时库 `.data` 部分的映射**

![图 1：C 运行时库 .data 部分的映射](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-16c6c19b2fff3d1bab3da930851d2c3a965c4d00%2Fdevice-01.png?alt=media)

每个虚拟内存对象（VM object）都与一个分页器（pager）相关联，分页器提供一组用于确定与虚拟内存对象关联的页面内容的函数。vnode 分页器用于与常规文件相关联的虚拟内存对象，这些文件来自块存储文件系统和网络文件系统。其函数从关联的文件中读取数据以初始化页面，并将修改后的页面写回关联的文件。交换分页器用于与常规文件无关的匿名虚拟内存对象。在首次使用时，系统会为这些对象分配填充为零的页面。如果系统内存不足，交换分页器会将使用较少的脏页面写入交换分区，直至它们再次被需要。

虚拟内存对象中的逻辑页面由虚拟内存页面（`struct vm_page`）表示。在启动时，内核分配了一个虚拟内存页面数组，使得每个物理内存页面都与一个虚拟内存页面对象相关联。虚拟内存页面通过使用特定架构的页表项（PTE）映射到地址空间中。受管理的虚拟内存页面通过使用特定架构的结构体（称为 PV 条目）维护一个映射链表。这个链表可以用于通过使关联的页表项无效来移除虚拟内存页面的所有映射，从而使虚拟内存页面能够重新用于表示不同的逻辑页面，不论是为另一个虚拟内存对象，还是为同一虚拟内存对象中的不同逻辑页面地址。

每次调用系统调用 [**mmap(2)**](https://man.freebsd.org/mmap/2) 时，都会在调用进程中创建一个新的虚拟内存映射条目。系统调用的参数提供了新条目的各种属性，包括权限、长度和偏移量，后者指向虚拟内存对象中的位置。文件描述符参数用于标识要映射到调用进程地址空间中的虚拟内存对象。为了映射字符设备的内存，进程将字符设备的打开文件描述符作为文件描述符参数传递给系统调用 `mmap()`。字符设备驱动程序的角色是决定哪个虚拟内存对象用于满足内存映射请求，并决定支持虚拟内存对象的页面内容。

## 默认字符设备分页器

4.4BSD 内置了一款设备虚拟内存（VM）分页器，用于支持字符设备内存映射。这个设备分页器旨在映射在操作系统运行时不会改变的物理内存区域。例如，它可以直接将 MMIO 区域（如帧缓冲区）暴露给用户空间。

设备分页器假设每个设备虚拟内存对象中的页面都映射到一个物理地址空间的页面。这个页面可以是个 RAM 页面，也可以与 MMIO 区域关联。重要的是，一旦设备虚拟内存对象中的逻辑地址与物理页面关联，该映射就不能被改变。这个假设是双向的，因为设备分页器也假设一旦物理地址空间中的页面与设备虚拟内存对象关联，该物理页面就永远不能被用于其他用途。因此，设备分页器使用的虚拟内存页面是无管理的（没有 PV 条目）。然而，这也意味着虚拟内存系统无法轻易找到这些虚拟内存页面的现有映射，以撤销现有的映射。尤其是通过 [**destroy\_dev(9)**](https://man.freebsd.org/destroy_dev/9) 销毁字符设备并不会撤销现有的映射。

默认字符设备分页器使用字符设备的 `mmap` 方法来验证映射请求，并确定与每个逻辑页面地址关联的物理地址。`mmap` 方法应验证偏移量和保护参数。如果偏移量不是有效的逻辑页面地址，或者请求的保护不被支持，方法应通过返回错误代码来失败。否则，方法应将请求的偏移量的物理地址存储到物理地址参数中，并返回零。如果页面应以 `VM_MEMATTR_DEFAULT` 以外的内存属性进行映射，成功时也应返回该内存属性。当创建映射时，设备分页器在请求的每个逻辑页面地址上调用此方法以验证请求。对于逻辑页面地址的首次页面错误，设备分页器调用 `mmap` 方法以获取后备页面的物理地址和内存属性。

列表 1 显示了一个简单字符设备驱动程序的 `mmap` 方法，该驱动程序使用默认的设备分页器。此设备在加载时分配一个单独的 RAM 页面，并将该页面的指针保存在 `si_drv1` 字段中。由于字符设备分页器的限制，此驱动程序无法卸载。示例 1 展示了设备加载后的一些交互，使用 `maprw` 测试程序来读取和写入设备映射。

**列表 1：使用默认设备分页器**

```c
static int
mappage_mmap(struct cdev *dev, vm_ooffset_t offset, vm_paddr_t *paddr,
    int nprot, vm_memattr_t *memattr)
{
      if (offset != 0)
            return (EINVAL);

      *paddr = pmap_kextract((uintptr_t)dev->si_drv1);
      return (0);
}
```

**示例 1：使用 /dev/mappage 设备**

```c
# maprw read /dev/mappage 16 | hexdump
0000000 0000 0000 0000 0000 0000 0000 0000 0000
0000010
# jot -c -s "" 16 'A' | maprw write /dev/mappage 16
# maprw read /dev/mappage 16
ABCDEFGHIJKLMNOP
```

## 映射任意虚拟内存对象

由于默认字符设备分页器的限制，FreeBSD 扩展了对字符设备内存映射的支持。FreeBSD 8.0 引入了新的字符设备方法 `mmap_single`。每次在调用映射字符设备 `mmap()` 时，都会调用此方法。`mmap_single` 方法必须验证整个 `mmap()` 请求，包括偏移量、大小和请求的保护。如果请求有效，该方法应返回一个虚拟内存对象（VM object）的引用，以供映射使用。该方法可以创建一个新的虚拟内存对象，也可以返回对现有虚拟内存对象的额外引用。如果 `mmap_single` 方法返回 `ENODEV` 错误（默认行为），`mmap()` 将使用默认字符设备分页器。

`mmap_single` 方法还可以在返回虚拟内存对象时修改用于映射的偏移量（但不能修改大小）。这能让字符设备使用映射的初始偏移量作为标识特定虚拟内存对象的键。比如，驱动程序可能有两个内部虚拟内存对象，使用偏移量 0 映射第一个虚拟内存对象，并使用 `PAGE_SIZE` 的偏移量映射第二个虚拟内存对象。在第二种情况下，`mmap_single` 方法将重置有效偏移量为 0，以便生成的映射从第二个虚拟内存对象的开头开始。

然而，字符设备不一定需要使用多个虚拟内存对象来受益于 `mmap_single` 方法。使用其他分页器的虚拟内存对象的能力可能很有用。例如，物理分页器创建由物理 RAM 中的固定页面支持的虚拟内存对象。与默认设备分页器不同，这些页面是受管理的，可以在虚拟内存对象被销毁时安全地释放。列表 2 更新了先前的 `mappage` 设备驱动程序，改为使用物理分页器虚拟内存对象，而不是默认的字符设备分页器。此版本的设备驱动程序可以安全卸载，因为虚拟内存对象将在驱动程序卸载后继续存在，直到所有映射被销毁。

**列表 2：使用物理分页器**

```c
static int
mappage_mmap_single(struct cdev *cdev, vm_ooffset_t *offset, vm_size_t size,
    struct vm_object **object, int nprot)
{
      vm_object_t obj;

      obj = cdev->si_drv1;
      if (OFF_TO_IDX(round_page(*offset + size)) > obj->size)
            return (EINVAL);

      vm_object_reference(obj);
      *object = obj;
      return (0);
}

static int
mappage_create(struct cdev **cdevp)
{
      struct make_dev_args args;
      vm_object_t obj;
      int error;

      obj = vm_pager_allocate(OBJT_PHYS, NULL, PAGE_SIZE,
          VM_PROT_DEFAULT, 0, NULL);
      if (obj == NULL)
            return (ENOMEM);
      make_dev_args_init(&args);
      args.mda_flags = MAKEDEV_WAITOK | MAKEDEV_CHECKNAME;
      args.mda_devsw = &mappage_cdevsw;
      args.mda_uid = UID_ROOT;
      args.mda_gid = GID_WHEEL;
      args.mda_mode = 0600;
args.mda_si_drv1 = obj;
      error = make_dev_s(&args, cdevp, "mappage");
      if (error != 0) {
            vm_object_deallocate(obj);
            return (error);
      }
      return (0);
}

static void
mappage_destroy(struct cdev *cdev)
{
      if (cdev == NULL)
            return;

      vm_object_deallocate(cdev->si_drv1);
      destroy_dev(cdev);
}
```

## 每个打开的状态

在本系列的开篇文章中，我们演示了如何使用 `si_drv1` 字段支持每个实例的数据。某些字符设备驱动程序需要为每个打开的文件描述符维护独特的状态。也就是说，如果一个字符设备被多次打开，驱动程序希望对每个打开的引用提供不同的行为。

FreeBSD 通过一系列函数提供了这个功能。通常，字符设备驱动程序会在 `open` 方法中创建每个打开状态的新实例，并通过调用 [**devfs\_set\_cdevpriv(9)**](https://man.freebsd.org/devfs_set_cdevpriv/9) 将该实例与新的文件描述符关联。此函数接受一个 `void` 指针参数和一个析构函数回调。析构函数在文件描述符的最后一个引用被关闭时调用，用于清理每个打开状态。其他字符设备开关方法会调用 [**devfs\_get\_cdevpriv(9)**](https://man.freebsd.org/devfs_get_cdevpriv/9) 来检索与当前文件描述符关联的 `void` 指针。请注意，这些函数始终在由调用者上下文隐式确定的当前文件描述符上操作，驱动程序不会显式传递文件描述符的引用给这些函数。

列表 3 展示了一个新的字符设备驱动程序 `memfd` 的 `open` 和 `mmap_single` 方法，以及析构函数 `cdevpriv`。这个简单的驱动程序提供了类似于 FreeBSD [**shm\_open(2)**](https://man.freebsd.org/shm_open/2) 实现中 `SHM_ANON` 扩展的功能。这个设备的每个打开的文件描述符都与一个匿名的虚拟内存对象（VM object）关联。当映射时，虚拟内存对象的大小会在必要时增长。虚拟内存对象可以通过共享文件描述符与其他进程共享，例如，通过在 UNIX 域套接字上传递文件描述符来实现。为了实现这一点，驱动程序在 `open` 方法中分配一个新的虚拟内存对象，并将该虚拟内存对象与新的文件描述符关联。`mmap_single` 方法获取当前文件描述符的虚拟内存对象，必要时增长它，并返回对该对象的引用。最后，析构函数会删除文件描述符对虚拟内存对象的引用。

**列表 3：每个打开的匿名内存**

```c
static int
memfd_open(struct cdev *cdev, int fflag, int devtype, struct thread *td)
{
      vm_object_t obj;
      int error;

      /* 只读和只写的打开方式没有意义。 */
      if ((fflag & (FREAD | FWRITE)) != (FREAD | FWRITE))
            return (EINVAL);

/*
 * 为每一个打开的文件描述符创建一个初始大小为 0 的匿名 VM 对象。
 */

      obj = vm_object_allocate_anon(0, NULL, td->td_ucred, 0);
      if (obj == NULL)
            return (ENOMEM);
      error = devfs_set_cdevpriv(obj, memfd_dtor);
      if (error != 0)
              vm_object_deallocate(obj);
      return (error);

}

static void
memfd_dtor(void *arg)
{
      vm_object_t obj = arg;

      vm_object_deallocate(obj);
}

static int
memfd_mmap_single(struct cdev *cdev, vm_ooffset_t *offset, vm_size_t size,
    struct vm_object **object, int nprot)
{
      vm_object_t obj;
      vm_pindex_t objsize;
      vm_ooffset_t delta;
      void *priv;
      int error;

      error = devfs_get_cdevpriv(&priv);
      if (error != 0)
            return (error);
      obj = priv;

/* 如有必要，扩展对象。 */
      objsize = OFF_TO_IDX(round_page(*offset + size));
      VM_OBJECT_WLOCK(obj);
      if (objsize > obj->size) {
            delta = IDX_TO_OFF(objsize - obj->size);
            if (!swap_reserve_by_cred(delta, obj->cred)) {
                 VM_OBJECT_WUNLOCK(obj);
                  return (ENOMEM);
            }
            obj->size = objsize;
            obj->charge += delta;
      }

      vm_object_reference_locked(obj);
      VM_OBJECT_WUNLOCK(obj);
      *object = obj;
      return (0);
}
```

## 扩展字符设备分页器

`mmap_single` 方法通过允许字符设备使用由任何分页器支持的虚拟内存对象（VM objects），并允许字符设备将不同的虚拟内存对象与不同的偏移量关联，从而缓解了默认字符设备分页器的一些限制。然而，也还存在一些限制。设备分页器在所有分页器中是独特的，因为它可以映射与物理 RAM 无关的物理地址，例如 MMIO 区域。由于使用了未管理的页面，因此无法撤销设备分页器的映射，也无法让驱动程序知道所有映射是否已被移除。FreeBSD 9.1 引入了一个新的设备分页器接口，提供了针对这两个问题的解决方案。

新的接口要求字符设备驱动程序显式地创建设备虚拟内存对象。这些虚拟内存对象随后由 `mmap_single` 方法使用，以提供映射的支持存储。在新的接口中，`mmap` 字符设备方法被一个新的方法结构体（`struct cdev_pager_ops`）所替代。该结构体包含以下方法：当虚拟内存对象被创建时调用的 `cdev_pg_ctor`，当发生页面故障并请求虚拟内存对象的页面时调用的 `cdev_pg_fault`，以及虚拟内存对象被销毁时调用的 `cdev_pg_dtor`。使用扩展设备分页器的虚拟内存对象通过调用 `cdev_pager_allocate()` 来创建。该函数的第一个参数是一个存储在新虚拟内存对象的句柄成员中的不透明指针。该指针还作为第一个参数传递给构造函数和析构函数分页器方法。`cdev_pager_allocate()` 的第二个参数是对象类型，可以是 `OBJT_DEVICE` 或 `OBJT_MGTDEVICE`。第三个参数是指向 `struct cdev_pager_ops` 实例的指针。

`cdev_pager_allocate()` 函数每次只为每个不透明指针创建一个虚拟内存对象。如果相同的不透明指针被传递给 `cdev_pager_allocate()` 的后续调用，函数将返回指向现有虚拟内存对象的指针，而不是创建一个新的虚拟内存对象。在这种情况下，虚拟内存对象的引用计数会增加，因此 `cdev_pager_allocate()` 总是返回指向返回的虚拟内存对象的新引用。

利用这个接口扩展原始版本的 `mappage` 驱动程序（来自列表 1），使其在没有活动映射的情况下可以安全卸载。在该例中，我们将使用 `OBJT_DEVICE` 虚拟内存对象。这仍然使用驱动程序加载时分配的单个固定页面的未管理映射。然而，现在需要额外的状态来确定该分配的页面是否正在使用，因此这个版本的驱动程序定义了一个 `softc` 结构，包含指向页面的指针、一个布尔变量用于跟踪页面是否处于活动映射状态、一个布尔变量用于跟踪驱动程序是否正在卸载（在这种情况下，不允许新的映射），以及一个互斥锁来保护对布尔变量的访问。指向 `softc` 结构的指针存储在字符设备的 `si_drv1` 字段中，并作为虚拟内存对象的不透明句柄使用。`mmap_single` 字符设备方法验证每个映射请求（包括在卸载待处理时失败的请求），并调用 `cdev_pager_allocate()` 获取指向映射固定页面的虚拟内存对象的引用。请注意，`mmap_single` 方法不需要单独处理创建新虚拟内存对象或重用现有虚拟内存对象的情况。构造函数分页器方法将布尔变量 `mapped`（位于 `softc` 中）设置为 `true`。若移除虚拟内存对象的最后一个映射，并且虚拟内存对象被销毁，析构函数分页器方法就会被调用，将 `mapped` 设置为 `false`。如果在请求卸载时 `mapped` 成员为 `true`，则 `mappage_destroy()` 函数会因 `EBUSY` 错误而无法卸载。

页面故障分页器方法比它所替代的 `mmap` 字符设备方法更为复杂。页面故障方法与虚拟内存（VM）系统以及页面故障通常如何被虚拟内存分页器处理的方式更加直接。当发生页面故障时，虚拟内存系统会分配一个空闲的内存页面，并调用分页器方法将该页面填充为适当的内容。交换分页器和物理分页器会在此方法中将新页面填充为零，而 vnode 分页器则从相关联的文件中读取适当的内容。默认的设备分页器采取了不同的路线。由于它通常设计用于映射非 RAM 地址，例如 MMIO 区域，默认设备分页器分配一个与 `mmap` 方法返回的物理地址相关联的“伪”虚拟内存页面，并将虚拟内存系统分配的新虚拟内存页面替换为这个“伪”虚拟内存页面（新的虚拟内存页面会作为空闲页面返回给系统）。页面故障分页器方法通过传入虚拟内存系统分配的新虚拟内存页面的指针，允许驱动程序实现这两种方法中的任意一种。页面故障分页器方法负责要么将该页面填充为适当的内容，要么用一个“伪”虚拟内存页面替换它。对于我们的驱动程序，我们计算固定页面的物理地址与之前相同，但使用该物理地址来构造一个“伪”虚拟内存页面。

列表 4 显示了 `mmap_single` 字符设备方法、三个设备分页器方法以及在模块卸载期间调用的 `mappage_destroy()` 函数。在示例 2 中，我们暂停了 `maprw` 测试程序，当它映射了来自 `mappage` 设备的页面时，尝试卸载驱动程序，但失败。之后恢复测试程序，并让它通过退出来取消映射设备，驱动程序成功卸载。

**列表 4：使用扩展设备分页器**

```c
static struct cdev_pager_ops mappage_cdev_pager_ops = {
      .cdev_pg_ctor = mappage_pager_ctor,
      .cdev_pg_dtor = mappage_pager_dtor,
      .cdev_pg_fault = mappage_pager_fault,
};

static int
mappage_mmap_single(struct cdev *cdev, vm_ooffset_t *offset, vm_size_t size,
    struct vm_object **object, int nprot)
{
      struct mappage_softc *sc = cdev->si_drv1;
      vm_object_t obj;

      if (round_page(*offset + size) > PAGE_SIZE)
            return (EINVAL);

      mtx_lock(&sc->lock);
      if (sc->dying) {
            mtx_unlock(&sc->lock);
            return (ENXIO);
      }
      mtx_unlock(&sc->lock);

      obj = cdev_pager_allocate(sc, OBJT_DEVICE, &mappage_cdev_pager_ops,
          OFF_TO_IDX(PAGE_SIZE), nprot, *offset, curthread->td_ucred);
      if (obj == NULL)
            return (ENXIO);
/*
 * 如果在我们分配 VM 对象时开始卸载，
 * dying 将被设置，卸载线程将会在 destroy_dev() 中等待。
 * 只需释放 VM 对象并失败映射请求。
 */

      mtx_lock(&sc->lock);
      if (sc->dying) {
            mtx_unlock(&sc->lock);
            vm_object_deallocate(obj);
            return (ENXIO);
      }
       mtx_unlock(&sc->lock);

      *object = obj;
      return (0);
}

static int
mappage_pager_ctor(void *handle, vm_ooffset_t size, vm_prot_t prot,
    vm_ooffset_t foff, struct ucred *cred, u_short *color)
{
      struct mappage_softc *sc = handle;

      mtx_lock(&sc->lock);
      sc->mapped = true;
      mtx_unlock(&sc->lock);

      *color = 0;
      return (0);
}

static void
mappage_pager_dtor(void *handle)
{
      struct mappage_softc *sc = handle;

      mtx_lock(&sc->lock);
      sc->mapped = false;
      mtx_unlock(&sc->lock);
}

static int
mappage_pager_fault(vm_object_t object, vm_ooffset_t offset, int prot,
    vm_page_t *mres)
{
      struct mappage_softc *sc = object->handle;
      vm_page_t page;
      vm_paddr_t paddr;

      paddr = pmap_kextract((uintptr_t)sc->page + offset);

      /* 参见 device_pager.c 中 old_dev_pager_fault 的结尾部分。 */
      if (((*mres)->flags & PG_FICTITIOUS) != 0) {
            page = *mres;
            vm_page_updatefake(page, paddr, VM_MEMATTR_DEFAULT);
      } else {
            VM_OBJECT_WUNLOCK(object);
            page = vm_page_getfake(paddr, VM_MEMATTR_DEFAULT);
            VM_OBJECT_WLOCK(object);
            vm_page_replace(page, object, (*mres)->pindex, *mres);
            *mres = page;
      }
      vm_page_valid(page);
      return (VM_PAGER_OK);
}

...

static int
mappage_destroy(struct mappage_softc *sc)
{
      mtx_lock(&sc->lock);
      if (sc->mapped) {
            mtx_unlock(&sc->lock);
            return (EBUSY);
      }
      sc->dying = true;
      mtx_unlock(&sc->lock);

      destroy_dev(sc->dev);
      free(sc->page, M_MAPPAGE);
      mtx_destroy(&sc->lock);
      free(sc, M_MAPPAGE);
      return (0);
}
```

**示例 2：通过扩展设备分页器安全卸载**

```sh
# maprw write /dev/mappage 16
^Z
Suspended
# kldunload mappage
kldunload: can't unload file: Device busy
# fg
maprw write /dev/mappage 16
maprw: empty read
# kldunload mappage
```

扩展设备分页器接口还增加了一种新的设备分页器类型。`OBJT_MGTDEVICE` 分页器与 `OBJT_DEVICE` 的不同之处在于，它总是使用受管理的页面进行映射，而不是使用未管理的页面。这意味着，即使页面已映射，也可以强制撤销页面的映射。对于映射非 RAM 页面（虚构页面），必须通过 `vm_phys_fictitious_reg_range()` 函数显式创建“伪”虚拟内存页面，然后才能在分页器中使用它们。

## 结论

在本文中，我们探讨了一些字符设备更罕见的使用案例，包括内存映射和每次打开状态。感谢你阅读此系列文章。希望它能为你提供有关 FreeBSD 中字符设备驱动程序的有用介绍。

***

**John Baldwin** 是一位系统软件开发人员。在过去的二十多年里，他在 FreeBSD 操作系统的各个部分（包括 x86 平台支持、SMP、各种设备驱动程序和虚拟内存子系统）以及用户空间程序中直接提交了修改。除了编写代码，John 还曾担任过 FreeBSD 核心团队和发布工程团队的成员。他还为 GDB 调试器做出了贡献。John 目前与妻子 Kimberly 和三个孩子 Janelle、Evan、Bella 一起住在弗吉尼亚州的阿什兰。


# 学会走路——连接 GPIO 系统

* [Learning to Walk–Interfacing to the GPIO System](https://freebsdfoundation.org/our-work/journal/browser-based-edition/downstreams/learning-to-walk-interfacing-to-the-gpio-system)
* 作者：Christopher R. Bowman

在上篇文章中，我们创建了简单的电路，使主板上的 LED 灯闪烁，并且学习了两种不同方法来把电路加载到 FPGA。遗憾的是，当我们加载电路时，CPU 停止运行了。此外，尽管这有点有趣，但并没有与芯片上的 CPU 进行交互。在本篇中，我们将深入了解 Vivado，学习如何在加载电路时保持 CPU 运行，并探索 FreeBSD 中的 GPIO 系统。

之前，当我们使用 U-boot 或者 xbit2bin 和 FreeBSD 下的 **/dev/devcfg** 时，我们发现 FreeBSD 停止运行了。我认为发生的问题是处理器的系统停止了运行。原来，我们使用的电路 FPGA.bit 文件并未包含处理器系统的配置信息。在本期中，我们将修复这个问题。

在如何呈现本期的内容上，我犹豫了一下。从学习的角度来看，最自然的方式可能是使用 Vivado 的 GUI（图形化界面）。然而，GUI 并不适合自动化，原因显而易见——它需要人工运行 GUI。此外，描述 GUI 步骤也既困难又繁琐。幸运的是，Vivado 有两个特性使得我们可以相对轻松地绕过这些问题。在使用 GUI 时，Vivado 工具会生成一个 `.jou` 文件，这是 GUI 在后台执行的所有 TCL 命令的日志。Vivado 还提供了一个 TCL 命令 `write_project_tcl`，可以用来重新创建使用 GUI 时 Vivado 创建的项目文件。我通常倾向于使用 `.jou` 文件，因为我发现这些脚本更简洁、易于理解，而且如果我运行这些脚本，我可以直接启动 GUI，或者使用 `write_project_tcl` 来写一个项目脚本。脚本似乎也更适合像 git 这样的版本控制系统。

如果我们查看“图 1-1：Zynq-7000 SoC 概述”（来自《UG585：Zynq-7000 SoC 技术参考手册》），我们可以看到有多种外设模块（如 UART、I2C、SPI 等），这些模块可以通过多路复用器连接到外部引脚。该手册的“1.2.3 I/O 外设”部分详细描述了更多的功能。出于我们当下的需求，我们只需要注意到，我们可以相当灵活地将 GPIO 设备的信号路由到芯片上的引脚。如果我们再查看 [Arty Z7 参考手册](https://reference.digilentinc.com/reference/programmable-logic/arty-z7/reference-manual?_gl=1*c286n6*_ga*MTg4NjczMDI1NC4xNzExMzUwMjY2*_ga_JSPEFFCPBT*MTcxMjM2NzMxNi4yLjAuMTcxMjM2NzMzMy40My4wLjA.) 的第 12 节“基本 I/O”，我们可以看到，板上的绿色 LED 与芯片引脚相连，这些引脚通过电流设置电阻接地。如果我们将这些引脚设为高电平，LED 灯就会亮起；反之，设为低电平时，LED 灯就会熄灭。

为了切换这些引脚，我们将使用 Vivado 软件将 GPIO 设备的输出路由到 LED 引脚，从而能让 GPIO 设备控制这些引脚。在开始这段旅程时我并不知道，但 FreeBSD 确实有一款 GPIO 子系统，且有人甚至好心编写了驱动程序，使得这一切能从用户空间使用。

要开始，首先把 [git 仓库](https://github.com/christopher-bowman/zynq_gpio_leds) 克隆到安装了 Vivado 工具的 Linux 主机上（在之前的文章中我展示了如何设置 bhyve），然后在仓库根目录下输入 `make`。如果 Vivado 工具已经正确配置，并且一切顺利，它应该会运行 Vivado，并拉取一个脚本，这个脚本会实例化处理器子系统，并将 GPIO 设备的前四个 EMIO 引脚连接到 LED 引脚。包含处理器子系统将解决我们之前在用 bit 流加载设备时处理器停止运行的问题。

在运行 `make` 后，在 git 仓库顶层目录查找构建的 zynq\_gpio\_leds.bit 文件。像上次那样使用 xbit2bin 程序将其编程到芯片中。

```sh
# xbit2bin zynq_gpio_leds.bit
```

你应该会看到什么也没有发生。虽然不是很激动人心，但至少处理器应该仍在运行。

现在，我们需要使用 FreeBSD 的 GPIO 子系统。输入 `man gpioctl` 会给出关于可用功能的简洁总结。

以 root 用户，我们可运行 `gpioctl` 程序来列出可用的引脚：

```sh
# gpioctl -f /dev/gpioc0 -l
```

没能成功吧？是的，我对此也有点惊讶。查看 **/usr/src/sys/arm/xilinx/zy7\_gpio.c** 中的 GPIO 源代码，我看到驱动程序中有 probe 和 attach 函数，但查看我的 ARTYZ7 系统的 `dmesg` 输出，我没有看到任何表明设备已被发现的内容。仔细看一下 probe 函数：

```c
static int
zy7_gpio_probe(device_t dev)
{

        if (!ofw_bus_status_okay(dev))
                return (ENXIO);

        if (!ofw_bus_is_compatible(dev, "xlnx,zy7_gpio"))
               return (ENXIO);

        device_set_desc(dev, "Zynq-7000 GPIO driver");
        return (0);
}
```

可以看到，找到设备所需的唯一条件就是 `ofw_bus_is_compatible(dev, "xlnx,zy7_gpio")` 函数返回 `true`。

在嵌入式系统中，像大多数 ARM 系统一样，硬件通常不像现代 PCIe 总线那样是自识别的。软件无法自动识别硬件的存在以及其控制寄存器在内存地址空间中的位置。出于这个原因，许多操作系统使用 FDT（Flattened Device Trees，扁平设备树）来描述其设备内存映射。FDT 是文本文件，描述了嵌入式系统的信息，包括其他内容：如有哪些设备存在，以及它们在内存中的位置。这使得软件能够处理各种设备，而不需要硬编码信息。通过使用不同的 FDT，相同的内核通常可以与略有差异的设备兼容。FDT 是通过名为 dtc（设备树编译器）的工具将 DTS 文件（设备树源文件）编译成 DTB 文件（设备树二进制文件）。dtc 提供了选项，可以用来编译 DTS 或反编译 DTB。后者非常有用。例如，你可以请求内核返回它正在使用的 DTB，并使用 dtc 将其转换为文本：

```sh
# sysctl -b hw.fdt.dtb | dtc -I dtb -O dts
```

如果我们查找 gpio 部分，我们会看到（其中包括）以下内容：

```c
             gpio@e000a000 {
                  compatible = "xlnx,zy7_gpio";
                };
```

DTB 中的兼容字符串不是驱动程序所期望的，因此它假设设备不存在。如果你查看 FreeBSD 的启动输出，你会发现内核正在使用 U-boot 提供的 EFI 固件模拟的 DTB。我们可以通过修复 U-boot 提供的内容来更改这一点，但那将需要补丁和重新编译 U-boot。相反，FreeBSD 提供了在内核加载器提示符或使用 **loader.conf** 变量加载 DTB 文件的功能。你可以使用以下 **/boot/loader.conf** 变量从加载器加载 DTB 文件：

```sh
# fdt_name="/boot/dtb/zynq-artyz7.dtb"
# fdt_type="dtb"
# fdt_load="YES"
```

这可以工作，但还有第三种方法。事实证明，你可以创建 FDT/DTS 叠加文件，这些文件是对 FDT/DTS/DTB 的补丁。我们只需要一个添加正确兼容字符串的 DTS 叠加文件，这样就可以了。以下是我在项目仓库的 dts 目录中包含的 DTS 叠加文件的核心部分，并附带一个 Makefile：

```sh
&{/axi/gpio@e000a000} { compatible = "xlnx,zy7_gpio"; };
```

我们可能会在后续的专栏中更详细地查看 FDT 文件，所以这里我不做解释。而是让你对此文件有个印象。构建完 DTB 叠加文件后，将生成的 DTB 文件放入 **/boot/dtb/overlays**，并将以下内容添加到 **/boot/loader.conf**：

```ini
fdt_overlays="artyz7_gpio_overlay.dtb"
```

重新启动并注意新的 dmesg 输出：

```sh
gpio0: <Zynq-7000 GPIO driver> mem 0xe000a000-0xe000afff irq 5 on simplebus0
```

现在我们再次尝试运行 gpioctl 命令，你应该能在其他输出中看到类似如下内容的一行：

```sh
pin 64: 0 EMIO_0<IN>
```

我们需要告诉 GPIO 子系统将引脚配置为输出，然后可以尝试切换它：

```sh
gpioctl -f /dev/gpioc0 -c EMIO_0 OUT
gpioctl -f /dev/gpioc0 -t EMIO_0
```

我会等着。灯光亮起时，请尽量保持坐姿，不要起身跳舞。看看你能否弄明白如何打开其他 LED，然后运行脚本 **scripts/blink.sh**，并传入参数 2。你应该会看到 LED 按照二进制计数闪烁。

如果你对此有什么问题、评论、反馈或吐槽，我很乐意听到你的声音。你可通过 <articles@ChrisBowman.com> 联系我。

***

1989 年，**Christopher R. Bowman** 首次在 VAX 11/785 上使用了 BSD，当时他在 Johns Hopkins University 应用物理实验室的地下二层工作。后来，在 90 年代中期，他在马里兰大学使用 FreeBSD 设计了他的第一款 2 微米 CMOS 芯片。从那时起，他一直是 FreeBSD 用户，并对硬件设计和驱动硬件的软件感兴趣。他在半导体设计自动化行业工作了 20 年。


# FreeBSD 中对 SYN 段的处理

* [The Handling of SYN Segments in FreeBSD](https://freebsdfoundation.org/our-work/journal/browser-based-edition/downstreams/the-handling-of-syn-segments-in-freebsd)
* 作者：Randall Stewart & Michael Tüxen

## TCP 连接设置

传输控制协议（TCP）是一种面向连接的传输协议，提供了可靠的双向字节流服务。TCP 连接设置需要交换三个 TCP 段，这被称为三次握手。发起 TCP 连接并发送第一个 TCP 段（SYN 段）的 TCP 端点称为客户端。等待接收第一个 TCP 段的端点称为服务器，服务器响应接收到的 SYN 段并发送一个 SYN ACK 段。当客户端接收到这个 SYN ACK 段时，它通过发送 ACK 段完成握手。

TCP 握手不仅用于同步两个端点之间的状态，包括提供可靠性的初始序列号，还用于通过 TCP 选项协商使用 TCP 扩展。如今的互联网中，在握手过程中（SYN 和 SYN ACK 段中包含）最广泛部署的 TCP 选项是：

1. 最大报文段长度（MSS）选项\
   MSS 选项包含一个 16 位数字（介于 0 和 65535 之间），它表示发送此选项的端点愿意在单个 TCP 段中接收的最大负载字节数。假设此数字不使用 IP 层和 TCP 层的选项。如果使用了这样的选项，则该数字必须减去选项的大小。这有助于 TCP 发送方避免发送需要在 IP 层进行分段的 TCP 段。
2. SACK-允许选项\
   此选项宣布发送方能够处理选择性确认（SACK）选项。这在发生数据包丢失时有助于提高性能。
3. TCP 窗口缩放选项\
   此选项包含一个介于 0 到 14 之间的自然数。如果双方都发送了此选项，则启用接收窗口缩放。这允许使用比 TCP 头格式允许的更大的接收窗口，因为接收窗口在 TCP 头中被限制为 16 位（因此为 65535 字节）。这避免了 TCP 中接收窗口字段的大小限制了 TCP 连接的吞吐量的问题。
4. TCP 时间戳选项\
   此选项包含两个 32 位数字，通常以毫秒级别编码一些时间信息。它用于提高 TCP 性能。

TCP 使用状态事件机来指定。最初，一个端点处于 CLOSED 状态。当端点愿意接受 TCP 连接（在服务器端），TCP 端点进入 LISTEN 状态。当接收到来自客户端的 SYN 段并回复 SYN ACK 段时，端点进入 SYN RECEIVED 状态。一旦 TCP 端点接收到客户端发送的 ACK 段，TCP 端点进入 ESTABLISHED 状态。可以使用 `netstat` 或 `sockstat` 命令行工具来观察这些状态。

用于控制 TCP 端点的应用程序编程接口（API）是套接字 API。程序通常使用监听套接字，告诉 TCP 实现可以在此端点上接受 TCP 连接，并且对于每个已接受的 TCP 连接，程序为每个 TCP 连接使用一个单独的套接字。应用程序可以设置监听套接字的参数，并且大多数情况下这些设置会被接受的套接字继承。本文重点讨论服务器端的 TCP 连接设置。需要注意的是，这一功能适用于所有 TCP 栈（默认的、RACK、BBR 等）。

## SYN 洪水攻击

当 TCP 最初实现时，每当接收到一个 SYN 段时，都会为 LISTEN 状态的 TCP 端点创建一个新的 TCP 端点。这需要分配内存，并且导致在 SYN RECEIVED 状态下创建一个新的 TCP 端点。所有必要的信息，包括与接收到的 SYN 段中的 TCP 选项相关的信息，都存储在 TCP 端点中。这个过程没有验证提供的信息、IP 地址和 TCP 端口号。

这会让攻击者向服务器发送大量的 SYN 段，而服务器会不断分配 TCP 端点，直到资源耗尽。因此，攻击者可以进行拒绝服务攻击，因为只要服务器没有更多资源，它将无法再接受来自有效客户端的 SYN 段。攻击者只需要发送 SYN 段，特别是，攻击者不会响应接收到的任何 SYN ACK 段。攻击者甚至可以使用伪造的 IP 地址（即攻击者不拥有的 IP 地址）。

SYN 洪水攻击的目的是使接收方耗尽资源，因此无法提供其预期的服务。在 FreeBSD 中，TCP 栈实现了两种缓解 SYN 洪水攻击的机制：

1. **减少内存分配**：当 TCP 端点从 CLOSED 状态转移到 SYN RECEIVED 状态时，通过使用 SYN 缓存减少分配的内存量，如下一节所述。
2. **不分配内存**：在处理传入的 SYN 段时，不分配任何内存。通过使用 SYN cookie 来实现，如下节所述。

## SYN 缓存

SYN 缓存的初始实现是在 2001 年 11 月加入到 FreeBSD 源代码树中的。它通过不分配完整的 TCP 端点，而是分配一个 TCP SYN 缓存条目（`struct syncache`，在 **sys/netinet/tcp\_syncache.h** 中定义），来减少 SYN RECEIVED 状态下的 TCP 端点的内存开销。一个 TCP SYN 缓存条目比 TCP 端点小，仅允许存储 SYN RECEIVED 状态下相关的信息。此信息包括：

* 本地和远程 IP 地址以及 TCP 端口号。
* 用于执行基于定时器的 SYN ACK 段重传的信息。
* 本地和远程 TCP 初始序列号。
* 接收到的 SYN 段中对方在 MSS 选项中报告的 MSS 值。
* 在 SYN 和 SYN ACK 段中交换的本地和远程窗口缩放位移值。
* 是否协商了窗口缩放、时间戳和 SACK 支持。
* 精确的 ECN 状态。
* 额外的 IP 层信息。

当接收到一个针对监听端点的 SYN 段时，会分配一个 SYN 缓存条目，并将相关信息存储其中，同时发送一个 SYN ACK 段作为响应。如果禁用 SYN cookie 且发生桶溢出，则会丢弃桶中最旧的 SYN 缓存条目。如果收到相应的 ACK 段，则使用 SYN 缓存条目中的数据创建一个完整的 TCP 端点，然后释放该 SYN 缓存条目。SYN 缓存还确保在未及时收到相应 ACK 段的情况下重传 SYN ACK 段。

`sysctl` 变量 `net.inet.tcp.syncookies`（默认值为 `1`）控制着是否使用 SYN cookie，结合 SYN 缓存来覆盖无法分配或查找 SYN 缓存条目的情况。

SYN 缓存是特定于 vnet 的，并且以哈希表形式组织。桶的数量由加载器可调参数 `net.inet.tcp.syncache.hashsize`（默认值为 512）控制。每个哈希桶中的最大 SYN 缓存条目数由加载器可调参数 `net.inet.tcp.syncache.bucketlimit`（默认值为 30）控制。还有一个总的 SYN 缓存条目限制，由加载器可调参数 `net.inet.tcp.syncache.cachelimit`（默认值为 15360，即 512 \* 30）控制。当前使用的 SYN 缓存条目数通过只读的 `sysctl` 变量 `net.inet.tcp.syncache.count` 报告。

还有一些与 SYN 缓存相关的其他 `sysctl` 变量。这些变量包括：

* `net.inet.tcp.syncache.rst_on_sock_fail`：控制在无法成功创建套接字时是否发送 RST 段（默认值为 1）。
* `net.inet.tcp.syncache.rexmtlimit`：SYN ACK 段的最大重传次数（默认值为 3）。
* `net.inet.tcp.syncache.see_other`：控制 SYN 缓存条目的可见性（默认值为 0）。

TCP SYN 缓存能让服务器端在最小化内存资源的情况下执行完整的握手。对于 SYN RECEIVED 状态下的 TCP 端点，使用完整的 TCP 端点与使用 SYN 缓存没有功能上的区别。即使是像 `netstat` 或 `sockstat` 这样的工具也会报告 SYN 缓存中的条目。

支持额外的 TCP 选项也不是问题，因为 TCP SYN 缓存条目可以扩展。

`sysctl` 变量 `net.inet.tcp.syncookies_only`（默认值为 0）可以用来禁用 SYN 缓存的使用。在这种情况下，只会使用下一节描述的 SYN cookie。

## SYN Cookie

2001 年 12 月，SYN 缓存实现中增加了额外的防护层以防范 SYN 洪水攻击。在处理接收到的 SYN 段时，服务器不再分配较少的内存，而是将相关信息存储在一个所谓的 SYN cookie 中，并在 SYN ACK 段中发送给客户端。然后，客户端需要在 ACK 段中反射该 SYN cookie。当服务器处理 ACK 段时，所有相关信息都包含在 SYN cookie 和 ACK 段中。因此，服务器可以基于这些信息创建一个 ESTABLISHED 状态的 TCP 端点。通过这种方式，SYN 洪水攻击不会导致内存资源的耗尽。然而，SYN cookie 的生成不能消耗过多的 CPU 资源。如果生成 SYN cookie 的过程过于耗费 CPU，可能会导致另一种拒绝服务攻击：这次攻击的目标不是内存资源，而是 CPU 资源。

在 TCP 头中，唯一可以由服务器任意选择并被客户端反射的字段是服务器的初始序列号。该字段是一个 32 位整数，因此用作 SYN cookie。

在 FreeBSD 中，这 32 位被分割为一个 24 位的消息认证码（MAC）和 8 位，具体如下所示：

* 3 位用于编码 8 个 MSS 值中的一个：216、536、1200、1360、1400、1440、1452、1460。如果客户端在 MSS 选项中发送的值不在这个列表中，则使用不超过该值的最大值。
* 3 位用于编码对端是否不支持窗口缩放或使用以下 7 个值之一：0、1、2、4、6、7、8。如果客户端发送的值不在此列表中，则使用不超过该值的最大值。
* 1 位用于编码客户端是否发送了 SACK-permitted 选项。
* 1 位用于选择两个密钥中的一个。

MAC 使用一个秘密密钥，该密钥每 15 秒更新一次。当前和上一个秘密密钥都会被保留下来，并根据 SYN cookie 中的位来选择使用哪个密钥。

MAC 的计算包括本地和远程 IP 地址、客户端的初始序列号、上述 8 位和一些内部信息。从 MAC 中生成 24 位，并与上述 8 位结合，构造出 SYN cookie。

当服务器接收到三次握手的 ACK 段时，会验证 MAC。如果验证成功，服务器将根据 SYN cookie 中的信息创建 TCP 端点，该信息提供了 MSS 选项的近似值、窗口位移的近似值以及客户端是否声明支持 SACK 扩展。所有其他相关信息必须从 ACK 段中恢复。这些恢复的信息包括本地和远程 IP 地址和端口号、本地和远程初始序列号、是否使用 TCP 时间戳选项，以及如果使用，当前的时间戳参数。

## SYN 缓存与 SYN Cookies 的比较

与 SYN 缓存相比，SYN cookies 的优势非常明显：接收到新的 SYN ACK 段时无需进行内存分配。然而，使用 SYN cookies 也有其缺点：

* MSS 被 8 个值近似，所有值都小于或等于 1460。因此，不支持 IPv4 中大于 1500 字节的 MTU。
* 用于窗口缩放的位移被 7 个值近似，所有值都小于或等于 8。这意味着不支持大于 8 的窗口位移，因此连接的窗口大小会更小。
* 不支持除了当前广泛部署的 TCP 选项之外的其他 TCP 选项。这使得支持用于协商新 TCP 特性的新的 TCP 选项变得困难。
* 如果 SYN ACK 段丢失，则不会重新传输 SYN ACK 段，发起连接的端点必须重试发送其 SYN 段。
* 不可见 SYN RECEIVED 状态下的 TCP 端点。
* 不支持 IP 层信息。

使用 SYN 缓存没有这些限制，并且是透明的，但需要为 SYN RECEIVED 状态下的每个 TCP 端点分配内存。

## SYN 缓存与 SYN Cookies 的联合使用

仅使用 SYN cookies 相比使用 SYN 缓存能更好地缓解 SYN 洪水攻击，但也带来了一些功能限制。因此，FreeBSD 的默认配置启用了 SYN 缓存，并与 SYN cookies 结合使用。这意味着，当接收到一个 SYN 段时，系统会生成一个 SYN 缓存条目，而发送的 SYN ACK 段则包含一个 SYN cookie。如果 SYN 缓存的一个桶溢出，系统会认为这是由于正在进行的 SYN 洪水攻击导致的，因此暂停使用 SYN 缓存。在此期间，仅使用 SYN cookies。

这一额外功能于 2019 年 9 月引入，在正常操作期间提供 SYN 缓存的优势，但在 SYN 洪水攻击发生时也能提供 SYN cookies 的改进保护。

***

**RANDALL STEWART**（<rrs@freebsd.org>）是一位操作系统开发人员，拥有 40 余年的经验，并自 2006 年起成为 FreeBSD 开发者。他专注于传输协议，包括 TCP 和 SCTP，但也涉足操作系统的其他领域。目前，他是一名独立顾问。

**MICHAEL TÜXEN**（<tuexen@freebsd.org>）是明斯特应用技术大学的教授，同时也担任 Netflix 的兼职承包商，自 2009 年起成为 FreeBSD 源代码提交者。他的研究重点是传输协议，如 SCTP 和 TCP，以及它们在 IETF 的标准化和在 FreeBSD 中的实现。


# FreeBSD 2024 年秋季峰会

* 链接：[Fall 2024 FreeBSD Summit](https://freebsdfoundation.org/our-work/journal/browser-based-edition/downstreams/fall-2024-freebsd-summit)
* 作者：Alice Sowerby

我参加了 2024 年秋季在 NetApp 圣何塞校园（Santana Row）举办的 FreeBSD 峰会。此次活动让我有机会与 FreeBSD 社区建立联系、交流思想，并讨论正在进行的项目。我的主要目标是通过演讲以及非正式的交流，强调直接资助项目在 FreeBSD 基金会工作中的作用。

面对面交流让我深刻体会到亲自参与的重要价值。我与许多人进行了富有成效的讨论，探讨了基金会资助项目的影响，同时也看到了大家对 FreeBSD 在笔记本电脑可用性方面的兴趣，这也是我演讲的核心内容。

在活动期间，我遇到了几位有趣的人，他们分享了自己使用 FreeBSD 的经历以及对 FreeBSD 的抱负。同一行程中，我也有幸见到了 Framework 团队，这让我更好地了解了基金会笔记本电脑项目的背景。

我于 11 月 8 日（星期五）进行了演讲，[Learning as We Grow: Managing FreeBSD Infrastructure and Laptop Projects at The FreeBSD Foundation（边成长边学习：FreeBSD 基金会的基础设施与笔记本项目管理）](https://www.youtube.com/watch?v=2IwqWiC4KZg)。听众提出了很多深思熟虑的问题，我也非常感激有机会分享我们如何构建和资助这些项目的见解。

让我印象特别深刻的是 Marshall McKusick 的演讲《The History of the BSD Daemon》，它为我们提供了有关 BSD 历史的有趣见解。他还带来了既往 BSD 会议的 T 恤，并将其赠送给与会者，这一温馨的举动令所有人感动。

Santana Row 为此次峰会提供了一个充满活力且便利的环境。NetApp 大楼宽敞而友好，使得与人会面和进行有意义的讨论变得十分容易。活动组织得井然有序，这种环境也促进了富有成效的交流。

自参加峰会以来，我已经感受到了由此建立的联系和讨论带来的益处。这些讨论和获得的见解帮助塑造了正在进行的项目，并强化了社区驱动的开发工作的重要性。

参加 2024 年秋季的 FreeBSD 峰会让我有机会与 FreeBSD 社区互动、分享知识并加深合作。此次活动突显了 FreeBSD 基金会投资的价值，并为讨论 FreeBSD 的未来提供了极好的平台。我期待着继续这些交流，并在峰会带来的势头上进一步发展。

***

**Alice Sowerby** 在打造团队和培养科技领域领导者方面拥有丰富的经验。


# 会议报告：FOSDEM 2025

* 原文链接：[Conference Report: FOSDEM 2025](https://freebsdfoundation.org/our-work/journal/browser-based-edition/downstreams/conference-report-fosdem-2025)
* 作者：Tom Jones

![布鲁塞尔街景](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-c24fba105aa2d5b3ffd4353ad35254a2dd5d4753%2Fconference-report-fosdem-2025-01.png?alt=media)

参与开源软件有很多方式。开发是大家首先想到的显而易见的方式，但许多人并不是开发者，还有其他贡献方式。

你可以写代码，但也可以测试进行中的开发和发布、阅读并修正文档、撰写新文档、在上千个地方回答问题、做推广、筹款、协调贡献者，或者组织活动。组织活动是回馈社区的好方式，但光是凑齐十个人就需要大量工作。

在 BSD 世界里，我们很幸运有三大地理上分散的会议，每年的日程都塞满了活动。问问任何想塞进一场黑客马拉松的人，他们必须协调多少冲突。

大多数开源项目不具备举办常规会议所需的规模和基础设施。见面很难，有些人或许有幸得到雇主支持，每年能与其他开发者见几次面。举办会议的成本对大多数小项目来说太高，互动往往只限于线上。

在欧洲，FOSDEM 填补了重要的生态位。FOSDEM 是世界上最大的开源会议，向所有人开放。这场活动完全占据了布鲁塞尔的 ULB 校园，参会者约 8000 人。FOSDEM 处理了寻找场地的复杂后勤，为项目提供见面、讨论和演讲的场地，以及展示 demo 的空间。如此大规模的活动不仅让项目开发者得以见面，还创造了项目之间的大量交叉授粉。

为让大家都有事可做，会议设有主议题演讲、数百个项目或主题专用的开发者房间（dev rooms），以及供项目展示的摊位。FOSDEM 需要各种规模的项目来运转，作为交换，它让参会者有机会接触到所有来布鲁塞尔的人。

## 在 FOSDEM 2025 推广 FreeBSD

我志愿帮忙照看 FOSDEM 的 FreeBSD 摊位。活动筹备期间，一大袋贴纸和一条桌旗送到了我家，我还协助把一条横幅运到比利时。

为帮助项目向外界介绍自己，FOSDEM 提供桌位或展位。这些桌位是新老用户前来提问的固定地点，也让项目有机会吸引路过的访客偶然发现一个有趣的项目。

两天的大部分时间我都站在桌后回答问题、分发贴纸，与其他项目成员讨论 FreeBSD 想法。

在开源项目的摊位值班是检验你知识水平的好方式。有时人流近乎源源不断，从当前用户、过去用户到完全新接触项目的人都有。以下是 FOSDEM 期间我被问到的一些关于我们如何推广 FreeBSD 的问题。

* FreeBSD 跑什么桌面？
* FreeBSD 能做什么？
* 我能拿一张贴纸吗？
* 谁在用 FreeBSD？
* 为什么选 FreeBSD 而不是 << 我最喜欢的 >> Linux 发行版？
* 说服我这位朋友用 FreeBSD。
* FreeBSD 上能跑容器吗？
* 你们项目怎么筹钱？

你的回答是什么？

我发现这些问题以及其他问题——尤其是那些用法语问的，我完全听不懂也完全没听清——是了解开源社区心态的绝佳切入点。

在这种环境下听到人们提出的问题，能反映出那些考虑其他操作系统的人（或者那些可怜又困惑、被拒绝了五次还以为我们是 Linux 发行版的人）感兴趣的是什么。

对切换和桌面的关注，暴露了我们在参加活动时展示项目方式的巨大缺口。贴纸很棒，横幅能让人找到我们，但我们缺少一些简单的 demo 来展示——是的，FreeBSD 是桌面，能做 Linux 上几乎一切能做的事。桌面环境的问题很有意思，许多提问者已经把我们划入了与围绕单一桌面环境构建的小型、专注的 Linux 发行版同一类。我们能跑所有桌面环境，虽然这让我们自己满意，但对来访摊位的人来说还不够有说服力，难以让人记住。

这些问题让我满怀想法，想如何更好地推广 FreeBSD——用桌面使用案例，也用我们那些酷而吸引人的功能。带上几块 SBC，展示同一版 FreeBSD 在所有设备上运行——无需配置——并不需要太多。

## 更多推广

回看这些问题。你准备好答案了吗？

如果你参加过会议、社区活动，或者在商场看到过慈善机构的摊位，你会熟悉这种场景。我们能怎样更好地回答这类问题？

开源会议上的摊位是高接触地点，你有数小时源源不断的机会让人对 FreeBSD 产生兴趣。我们带来了贴纸、马克杯和速查表，但最大的成功将来自我们能在这些场合带给人们持久的正面体验。

如果你对如何在会议上更好地推广 FreeBSD 有想法，请给我发邮件（<thj@freebsd.org>），我们可以开始讨论如何向开源社区推介 FreeBSD。

Tom Jones 是 FreeBSD committer，致力于让网络栈保持快速。


# 读者来信

![mailbox graphic](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-814b64e2758639a8e530c8d80cec175df2203491%2Fwe-get-letters-01.png?alt=media)

回信先生：

许多人基于 FreeBSD 构建东西，打包起来，做成可安装的产品。我也有类似的想法。你觉得如何？

——Gonna Bring My Ideas to Life

亲爱的 GBMItL：

想法很有意思。我敢打赌，你还年轻到能记得希望是什么滋味。

当我用 FreeBSD 2.0 组装出第一台 Unix 桌面机时，我以为自己能征服这个操作系统。它有源代码，而我能读懂 C！我真诚地相信，只要逐行审视操作系统的内部，就能获得一种技能水平——只有 freebsd-questions 邮件列表上那些匿名、面目模糊、慷慨奉献的大师们才拥有。

和你一样，我那时还没学会仔细审视那些突如其来的、闻起来很香的、引你走向背叛的诱惑。

我在本专栏的其他地方写过我与 FTP 源代码的折腾经历，没必要再用第二次复述来玷污你的灵魂——尽管大量心理治疗，加上微剂量服用从二手店淘来的 Shirley McClain“揭示你的前世”音频磁带，又揭示了那段漂泊历程的更多细节。让我告诉你：我曾用脚本识别 FTP 代码中哪个函数调用了哪个函数，按此顺序整理源文件，然后把结果送到雇主的工业级打印机，结果打印了几千页之后才发现代码是递归的——这件事足以打消你可能怀有的、认为我有任何专业能力的任何幻想，但对你理解递归、下游项目，或者希望，都没有更多帮助。它只能说明，经验就是雇主逼你买新的碳粉盒时你得到的东西。

它还顺手把我那本已短小的希望又磨掉了一层。

我也在本专栏提到过 FWTK——如果没记错，是第四次。让我查一查我那本《Dear Abyss: The FreeBSD Journal Letters Column, Years 1-6》来核实——这本书在你附近的可疑可疑零售店里有售——没错，第四次。但我当时没怎么细谈。

FWTK。Fire Wall Tool Kit（防火墙工具箱）。没错，是 Fire Wall。在 1990 年代，我们还没发明复合名词。防火墙动辄数万美元，但 FWTK 让你能在装有两块网卡的廉价 Unix 主机上为 HTTP、SMTP 等安装代理。组织内部的所有连接都终止在 FWTK 主机上，由它去获取所请求的资源，再交还给你。它支持所有现代协议，比如 HTTP 1.0 和 Gopher，通过 plug 接口还能连到你 ISP 的 Usenet 服务器。你还需要什么呢？它干活很可靠。

许可证禁止商业用途。你不能转售 FWTK。

但你可以做独立顾问，为客户安装和维护 FWTK。

我那时是独立顾问，带着笔记本电脑、戴着系铃铛的帽子在一家家公司之间游走，靠“用交叉线级联集线器”、“被空气中飘浮的玻璃纤维 *绝缘层* 堵塞的 PC 会过热起火，因为，你懂的，绝缘”这类不朽原则修网络。脚本小子的时代刚刚开始，本地一大批律师事务所都想要一个那种花哨的防火墙玩意儿。律师事务所看起来是绝佳客户：自己收费 250 美元/小时的人，看到顾问只收一半价钱也不会皱眉。毕竟，你只声称自己的价值是他们的一半。

安装 FWTK 不难。下载源代码，编译，把二进制文件塞进 **/usr/local/bin**。配置几个文件——砰——你就有了与昂贵大牌防火墙不相上下的防火墙，最棒的是，客户的钱进了 *你的* 口袋。

FreeBSD 的 FWTK Port 让整个流程轻而易举，但我满足吗？不，我不满足。因为，你瞧，我手上有源代码。源代码让人野心勃勃。源代码让人以为自己无所不能。世上到处是这样的人：发现自己能读懂源代码，就突然以为可以无所不能——比如革新互联网支付、重塑汽车传动系统、造出真正的 1950 年代风格火箭飞船，或者重构政府，而对其中任何一件事都一无所知。他们有时半路发了财，但最终必然在尖刻的耻辱与刻薄的羞愧中失败，因为能读懂源代码的聪明，跟是否胜任、是否是有价值的人毫无关系。

我以为自己能改进一切。啊，希望，多么甜美的毒药！

如果我能构建自己的 FreeBSD，已经内置 FWTK 和适当的配置文件，甚至让这些文件预先初始化好 RCS 版本控制，那安装时间就能从四小时压缩到一小时。想想那多出来的三小时我能干些什么！我能……拼命去找下一个需要安装 FWTK 的客户，就这么回事。别管什么花重金请昂贵顾问的全部意义就在于客户能看到你为他们卖力流汗。

我只要把源代码塞进 **/usr/src/contrib**，改一个 `Makefile`，然后砰——构建操作系统时它就会构建这个 Port。（那个年代也没有 **freebsd-update(8)**。每个安全补丁都得从源代码构建受影响的组件。FreeBSD 没有 Open Group 的 UNIX™ 认证，但它毫无疑问是捶胸顿足的真 Unix。）

就像参与“摸电脑”这项丑陋极客运动的所有事情一样，它失败了。

我把错误信息复制到 Stack Exchange——不，等等，那时还没有这种网站。1990 年代那些强大的搜索引擎主要是按恋物癖查询设计的。不过 FreeBSD 网站有搜索引擎，邮件列表归档也有索引。我学，我改错，又制造新错误。不是令人激动的错误。也不是有用的错误。只是错误，被磨碎、被克服，又用来制造更多错误。

但一旦整合完毕，我就能运行自己的发行版，把它刻到 CD 上，得到一张全补丁、可安装的防火墙。别管 FreeBSD 那时还没有 **release(7)** 手册页。邮件列表归档里有怎么做的笔记！

我把自己几周的空闲时间都花在了那个项目上，其中“空闲”的定义是“从家人、洗碗和洗澡那里偷来的”。

请记住，我只是在尝试整合贡献的软件。我没在做诸如那些 HardenedBSD 疯子试图同时修改核心内核代码又保持与 FreeBSD 本身同步之类的事情。不过话说回来，HardenedBSD 有使用 git 而非 CVS 的“优势”。（让人相信软件分叉是合理、可维护或可持续的，可能是 git 犯下的最大罪过，但说远了。）

即便在需要防火墙的律师事务所都用光之后，我仍坚持下来。我不会让一坨愚蠢的计算机代码打败我。毕竟，像 *那位* Jordan Hubbard——他出了名地读了 **wall(1)** 手册页，心想“它不可能这么用”，然后立刻给全世界每一个互联网用户发了消息——这种人能做到，我当然也能！

我失败了。

我和 Jordan 的区别在这里。他愿意，甚至渴望，大规模地失败。在所有人面前失败。

Jordan、Rod Grimes、Nate Williams、Mike Smith 和所有那些人把自己的工作放到了世界上，吸引其他人加入他们的愿景。他们聪明地知道源代码不会赋予他们了不起的宇宙级力量，但仍凭倔强和创造既酷又实用之物的意愿坚持下来，而不像我是出于倔强和贪婪。

偶尔，他们甚至会把这个愿景写进文档。

于是就有了今天的 FreeBSD。

那么，你该做下游项目吗？你有愿景吗？你有无限克服自己错误的能力吗？你愿意告诉全世界你做了什么吗？那就去为一个崇高的事业做志愿者，帮助那些不如你的人吧，因为那才是改变世界的方式！

对，那意味着离开你的键盘。抱歉。

行吧。去做下游项目吧。看我在不在乎。不过别重蹈我的覆辙。去犯你自己的错误。那是学习的最佳方式！

另外：律师事务所邀请你工作时，先联系他们用过的其他承包商打听一下。和律师事务所讨要未付的发票很难。毕竟，他们手底下全是律师。

有问题想问 Michael？发送至 <letters@freebsdjournal.org>

Michael W Lucas 是《Absolute FreeBSD》《Run Your Own Mail Server》等太多其他书的作者。他大概又需要调整药物了。再次。了解更多请访问 <https://mwl.io>。


# 活动日历

* 原文链接：[2025 Events Calendar](https://freebsdfoundation.org/our-work/journal/browser-based-edition/virtualization-2/events-calendar/)
* 作者：**Anne Dickison**

截至 2025 年 3 月的 BSD 活动。如发现此处未列出的 FreeBSD 相关活动或对 FreeBSD 用户有益的活动，请将详情发送至 <freebsd-doc@FreeBSD.org>。

![FOSDEM](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-b69b7170ee82d764ae45ef4fc00d1b28f15f4fad%2Fevents-calendar-1.png?alt=media)

## FOSDEM 2025

2025 年 2 月 1-2 日

比利时布鲁塞尔

FOSDEM 是由志愿者组织的为期两天的活动，旨在推动自由和开源软件的广泛应用。FOSDEM 于 2025 年 2 月 1-2 日举办，为开源和自由软件开发者提供一个相聚、交流想法、协作的平台。

![SCALE 22X](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-f63187b58c1417ff02b59d59c22f3b5d94dbcb70%2Fevents-calendar-2.png?alt=media)

## SCALE 22X

2025 年 3 月 6-9 日

美国加州帕萨迪纳

SCALE 22X——第 22 届年度南加州 Linux 大会——将于 2025 年 3 月 6-9 日在加州帕萨迪纳举行。

SCALE 是北美规模最大的社区主办的开源和自由软件大会。

![FreeBSD](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-82f2cb6a4a3af244b21cd18a1cdf602e02017e3a%2Fevents-calendar-3.png?alt=media)

## AsiaBSDCon 2025

2025 年 3 月 20-23 日

日本东京

AsiaBSDCon 是面向基于 BSD 的系统用户和开发者的大会。下一届大会将于 2025 年 3 月 20-23 日在日本东京举行。大会面向所有开发、部署和使用基于 FreeBSD、NetBSD、OpenBSD、DragonFlyBSD、Darwin 和 macOS X 系统的人。AsiaBSDCon 是一场技术大会，旨在汇集最优秀的技术论文和演讲，确保开源社区中的最新进展能被最广泛的受众所了解。


# 字符设备驱动程序教程（第二部分）

* 原文链接：[Character Device Driver Tutorial (Part 2)](https://freebsdfoundation.org/our-work/journal/browser-based-edition/virtualization-2/character-device-driver-tutorial-part-2/)
* 作者：**John Baldwin**

![字符设备驱动教程配图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-709b592ee697a3dfec9e948170ff2f940154247b%2Fzifu-1.png?alt=media)

在这篇三部分系列的 [上一篇文章](https://freebsdfoundation.org/our-work/journal/browser-based-edition/kernel-development/character-device-driver-tutorial/) 中，我们构建了一个简单的字符设备驱动程序，该程序支持由固定缓冲区支持的 I/O 操作。在本文中，我们将扩展此驱动程序，以支持 FIFO 数据缓冲区，并支持非阻塞 I/O 和事件报告。每个版本的驱动程序的完整源代码可以在 <https://github.com/bsdjhb/cdev_tutorial> 找到。

然而，在继续之前，我们必须处理上一篇文章中未完成的部分。细心的读者 Virus-V [指出](https://github.com/bsdjhb/cdev_tutorial/issues/1)，文章中的 echo 驱动程序的最终版本没有在卸载时销毁 **/dev/echo** 设备，并且在卸载后访问该设备会触发内核 panic。这个 bug 的第一个线索出现在内核在卸载模块时发出的关于内存泄漏的警告消息，panic 发生之前就已经出现了这个警告。如前文所述，这是内核模块在可能的情况下应该使用专用 malloc 类型的原因之一。该 bug 出现在最后一组更改中添加的 `echodev_create()` 函数中。我们未能通过将值存储在 `*scp` 中将指向新分配的 softc 结构的指针返回给调用者。因此，变量 `echo_softc` 始终为 NULL，softc 在模块卸载时未被销毁。修复方法是在 `echodev_create()` 中添加一行代码，在成功时将指针存储到 `*scp` 中，指向新的 softc。

## 使用 FIFO 数据缓冲区

上一篇文章中的 echo 驱动程序使用了一个平面的数据缓冲区进行 I/O 操作。读取和写入可以访问数据缓冲区的任何区域，并且数据缓冲区的整个范围始终有效。这些语义类似于访问一个在写入末尾时不会增长的文件。然而，字符设备驱动程序可以实现一系列不同的语义。对于本文，我们将修改 echo 驱动程序，使其像 [pipe](https://man.freebsd.org/pipe/2) 或 [fifo](https://man.freebsd.org/mkfifo/2) 这样的 FIFO 流设备一样对待用户 I/O 数据。I/O 写请求将数据追加到逻辑数据缓冲区的尾部，读取请求将从数据缓冲区的头部读取数据。像 [**pread(2)**](https://man.freebsd.org/pread/2) 这样的文件偏移量将被忽略。驱动程序将继续使用内核中的缓冲区来保存用户数据的临时副本。写入将数据存储在此缓冲区中，读取将从此缓冲区中消耗数据。这意味着驱动程序现在需要跟踪缓冲区中有效数据的数量以及缓冲区的长度。为了简化实现，数据缓冲区的开始部分将始终被视为缓冲区的头部。读取请求如果读取了部分可用数据，将把剩余数据复制到缓冲区的前面。

然而，这确实提出了几个额外的问题。首先，如何处理希望从缓冲区中读取超过可用数据量的读取请求？其次，如何处理希望存储比缓冲区能容纳的更多数据的写入请求？为了简单起见，我们将首先对读取请求返回可用字节的短读取，并将写请求截断，只存储缓冲区中有足够空间的数据量。第三，对于一个 `ECHODEV_SBUFSIZE` 请求，如果将缓冲区缩小到小于有效数据量的大小，应该如何处理？我们选择使此类请求失败并返回错误。也可以选择丢弃一些数据，但必须决定丢弃哪些数据。列表 1 提供了更新后的读写方法。注意，softc 中新增了一个 `valid` 成员，用于跟踪缓冲区中有效数据的数量。示例 1 展示了这个更新后的驱动程序的一些场景。最初，设备为空，但待提供输入，就可以读取数据。最后几个命令跨两个请求读取了一系列字节。

**列表 1：使用 FIFO 数据缓冲区的读取和写入**

```c
static int
echo_read(struct cdev *dev, struct uio *uio, int ioflag)
{
      struct echodev_softc *sc = dev->si_drv1;
      size_t todo;
      int error;

      sx_xlock(&sc->lock);
      todo = MIN(uio->uio_resid, sc->valid);
      error = uiomove(sc->buf, todo, uio);
      if (error == 0) {
            sc->valid -= todo;
            memmove(sc->buf, sc->buf + todo, sc->valid);
      }
      sx_xunlock(&sc->lock);
      return (error);
}

static int
echo_write(struct cdev *dev, struct uio *uio, int ioflag)
{
      struct echodev_softc *sc = dev->si_drv1;
      size_t todo;
      int error;

      sx_xlock(&sc->lock);
      todo = MIN(uio->uio_resid, sc->len - sc->valid);
      error = uiomove(sc->buf + sc->valid, todo, uio);
      if (error == 0)
            sc->valid += todo;
      sx_xunlock(&sc->lock);
      return (error);
}
```

**示例 1：简单的 FIFO I/O**

```sh
# hd < /dev/echo

# echo "foo" > /dev/echo
# cat /dev/echo
foo
# echo "12345678" > /dev/echo
# dd if=/dev/echo bs=1 count=4 status=none | hd
00000000 31 32 33 34 |1234|
00000004
# cat /dev/echo
5678
```

## 阻塞 I/O

虽然这个版本的回显驱动实现了一个简单的数据流，但它也有一些局限性。如果一个进程想要使用这个设备共享一个比数据缓冲区大的数据块，它必须等到读者消耗完之前缓冲区中的数据，才能写入额外的缓冲区数据。这要求写入进程要么与读取进程协调，要么使用定时器并定期重试写操作。两种解决方案都不太实际。相反，驱动程序可以通过在缓冲区满时在写请求中睡眠，直到请求完成，从而允许更大的写入。读者在空间可用时会唤醒等待的写入者，从而允许写入者继续进行写入。类似地，读者可以阻塞等待数据返回。为了更贴近管道和套接字的语义，我们选择使读取请求仅在请求开始时阻塞，并在数据可用时尽快返回短读取。然而，对于写入操作，我们尝试一次性清空整个缓冲区。为了处理阻塞，我们使用了 [**sx\_sleep(9)**](https://man.freebsd.org/sx_sleep/9) 函数，它在将当前线程置于休眠状态的同时，原子地释放设备的锁。将 `PCATCH` 传递给该函数允许信号中断休眠，若中断发生，`sx_sleep()` 将返回一个非零的错误值。列表 2 显示了更新后的读取方法。写入方法也进行了类似的更新，但增加了一个额外的循环，直到写入完全完成。请注意，在写入方法中，如果写入操作部分完成，我们不需要“隐藏”错误。`dofilewrite()` 函数中的通用写系统调用处理将 `sx_sleep()` 的错误映射为成功，只要至少有一些数据被写入。一些 `ioctl` 处理程序也需要更新，以便在缓冲区增大或清空内容时唤醒正在休眠的写入者。

在测试这个版本的驱动时，示例 2 显示了一些可能令人惊讶的行为。尽管先前写入的数据会被返回，但 [**cat(1)**](https://man.freebsd.org/cat/1) 会继续等待更多数据，直到被信号终止。

**列表 2**：阻塞读取方法

```c
static int
echo_read(struct cdev *dev, struct uio *uio, int ioflag)
{
      struct echodev_softc *sc = dev->si_drv1;
      size_t todo;
      int error;

      if (uio->uio_resid == 0)
            return (0);

      sx_xlock(&sc->lock);

      /* Wait for bytes to read. */
      while (sc->valid == 0) {
            error = sx_sleep(sc, &sc->lock, PCATCH, "echord", 0);
            if (error != 0) {
                  sx_xunlock(&sc->lock);
                  return (error);
            }
      }

      todo = MIN(uio->uio_resid, sc->valid);
      error = uiomove(sc->buf, todo, uio);
      if (error == 0) {
            /* 唤醒任何等待的写入者。 */
            if (sc->valid == sc->len)
                  wakeup(sc);

            sc->valid -= todo;
            memmove(sc->buf, sc->buf + todo, sc->valid);
      }
      sx_xunlock(&sc->lock);
      return (error);
}
```

**示例 2**：阻塞 I/O 永久挂起

```sh
# echo "12345678" > /dev/echo
# cat /dev/echo
12345678
^C
```

## 安全卸载

我们稍后会讨论示例 2 中的令人惊讶的行为。当前的驱动程序还有另一个问题。当一个进程在读取或写入方法中被阻塞时，卸载驱动程序会导致卸载模块的进程挂起，直到第一个进程被信号杀死。这不是管理员卸载模块时所期望的行为。相反，回显驱动程序应该在设备销毁时唤醒任何休眠的线程，并确保它们能够从驱动程序方法中返回，而不会再次进入休眠状态。为了支持这一点，下一个修改添加了一个标志 `dying` 到 `softc` 中，并且如果该标志被设置，读取和写入方法将返回 `ENXIO` 错误，而不是阻塞。在设备销毁期间，标志 `dying` 被设置，并且在调用 `destroy_dev()` 之前，所有休眠的线程都会被唤醒。列表 3 显示了读取方法中的更改行和更新的 `echodev_destroy()` 函数。

**列表 3：卸载时唤醒线程**

```c
static int
echo_read(struct cdev *dev, struct uio *uio, int ioflag)
{
      ...
      /* 等待读取字节。 */
      while (sc->valid == 0) {
            if (sc->dying)
                  error = ENXIO;
            else
                  error = sx_sleep(sc, &sc->lock, PCATCH, "echord", 0);
            if (error != 0) {
                  sx_xunlock(&sc->lock);
                  return (error);
            }
      }
      ...
}

...

static void
echodev_destroy(struct echodev_softc *sc)
{
      if (sc->dev != NULL) {
            /* 强制任何正在休眠的线程退出驱动程序。*/
            sx_xlock(&sc->lock);
            sc->dying = true;
            wakeup(sc);
            sx_xunlock(&sc->lock);

            destroy_dev(sc->dev);
      }
      free(sc->buf, M_ECHODEV);
      sx_destroy(&sc->lock);
      free(sc, M_ECHODEV);
}
```

## 读取时的条件阻塞

在示例 2 中，令人惊讶的是，在读取了设备上的可用数据后，**cat(1)** 仍然继续阻塞。然而，这种行为确实是我们驱动程序的自然结果，因为 **cat(1)** 只是循环调用 [**read(2)**](https://man.freebsd.org/read/2)，直到收到 EOF，而第二次调用 **read(2)** 会阻塞，等待更多数据。其他流设备（如管道和 FIFO）的语义是，如果没有进程打开设备进行写入，读取将返回 EOF 而不是阻塞。如果有设备被打开进行写入，则读取将阻塞，等待更多数据。

我们可以很容易地在回显驱动程序中实现这些语义。我们向 `softc` 中添加一个写入者计数器，只有当计数器非零时，读取方法才会阻塞。为了检测写入者，我们添加了一个打开方法，当每个请求写权限的打开操作时增加计数。可以通过检查传递给打开方法的文件标志中的标志 `FWRITE` 来确定这一点。一个新的关闭方法在写入者关闭时减少计数。默认情况下，关闭字符设备开关方法仅在没有剩余文件描述符时为设备的最后一次关闭调用。相反，我们设置了字符设备开关标志 `D_TRACKCLOSE`，这样每次文件描述符关闭时都会调用关闭方法。如果最后一个写入者关闭，关闭方法会唤醒任何正在等待的读取者。列表 4 显示了新的打开和关闭方法，以及读取方法中的更改行。重试示例 2 中的步骤后，**cat(1)** 现在在读取完可用数据后会退出，不再出现令人惊讶的行为。

**列表 4：跟踪打开的写入者**

```c
static int
echo_open(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
      struct echodev_softc *sc = dev->si_drv1;

      if ((fflag & FWRITE) != 0) {
            /* Increase the number of writers. */
            sx_xlock(&sc->lock);
            if (sc->writers == UINT_MAX) {
                  sx_xunlock(&sc->lock);
                  return (EBUSY);
            }
            sc->writers++;
            sx_xunlock(&sc->lock);
      }
      return (0);
}

static int
echo_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
      struct echodev_softc *sc = dev->si_drv1;

      if ((fflag & FWRITE) != 0) {
            sx_xlock(&sc->lock);
            sc->writers--;
            if (sc->writers == 0) {
                  /* 唤醒任何等待的读取者。 */
                  wakeup(sc);
            }
            sx_xunlock(&sc->lock);
      }
      return (0);
}

static int
echo_read(struct cdev *dev, struct uio *uio, int ioflag)
{
      ...
      /* 等待读取字节。 */
      while (sc->valid == 0 && sc->writers != 0) {
      ...
}
```

## 非阻塞 I/O

现在我们的回显设备支持阻塞 I/O。然而，一些消费者可能希望使用非阻塞 I/O。进程可以通过将标志 `O_NONBLOCK` 传递给 [**open(2)**](https://man.freebsd.org/open/2)，或者通过 [**fcntl(2)**](https://man.freebsd.org/fcntl/2) 切换已打开文件描述符的标志 `O_NONBLOCK` 来请求非阻塞 I/O。字符设备驱动程序可以通过检查传递给读取和写入方法的文件标志中是否存在标志 `O_NONBLOCK` 来检查是否启用了非阻塞 I/O。如果请求了非阻塞 I/O，则应返回错误 `EWOULDBLOCK`，而不是在驱动程序会阻塞时阻塞。对于回显设备，这意味着在读取和写入方法中添加额外的检查，以确保在阻塞之前处理非阻塞 I/O。这本身足以处理在打开时请求的非阻塞 I/O。然而，为了支持通过 **fcntl(2)** 切换标志，还需要额外的步骤。

每次尝试通过 **fcntl(2)** 的 `F_SETFL` 操作设置文件标志时，都会在字符设备上调用两个 I/O 控制命令：`FIONBIO` 和 `FIOASYNC`。即使请求没有更改关联的标志 `O_NONBLOCK` 和 `O_ASYNC` 的状态，也会调用这些 I/O 控制命令。如果任何一个 I/O 控制命令失败，整个 `F_SETFL` 操作将失败，文件标志保持不变。因此，想要支持 `F_SETFL` 的字符设备驱动程序必须实现对这两个 I/O 控制命令的支持。

`FIONBIO` 和 `FIOASYNC` 将一个 int 值作为命令参数。如果新文件标志中清除了关联的文件标志，则该 int 值为零；如果新文件标志中设置了关联的标志，则该 int 值为非零。I/O 控制处理程序应该在请求的标志设置被支持时返回零，或者在请求的设置不被支持时返回错误。回显设备支持设置标志 `O_NONBLOCK`，但不支持设置标志 `O_ASYNC`，因此，如果 int 参数非零，回显设备的 `FIOASYNC` 处理程序会失败。

列表 5 显示了支持非阻塞 I/O 的读取和 I/O 控制方法的相关更改。

**列表 5：支持非阻塞 I/O**

```c
static int
echo_read(struct cdev *dev, struct uio *uio, int ioflag)
{
      ...
      /* 等待读取字节。 */
      while (sc->valid == 0 && sc->writers != 0) {
            if (sc->dying)
                  error = ENXIO;
            else if (ioflag & O_NONBLOCK)
                  error = EWOULDBLOCK;
            else
                  error = sx_sleep(sc, &sc->lock, PCATCH, "echord", 0);
      ...
}

...

static int
echo_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
struct thread *td)
{
      ...
      switch (cmd) {
      ...
      case FIONBIO:
            /* 支持 O_NONBLOCK*/
            error = 0;
            break;
      case FIOASYNC:
            /* 支持 O_ASYNC */
            if (*(int *)data != 0)
                  error = EINVAL;
            else
                  error = 0;
            break;
      ...
}
```

## 投票 I/O 状态

使用非阻塞 I/O 的应用程序通常使用事件循环来处理多个文件描述符的请求。每次循环迭代，应用程序会阻塞，等待一个或多个文件描述符准备好（例如，数据可供读取，或有空间写入更多数据）。然后，应用程序会处理每个已准备好的文件描述符，之后再次等待。FreeBSD 为此类事件循环支持两个系统调用：[**select(2)**](https://man.freebsd.org/select/2) 和 [**poll(2)**](https://man.freebsd.org/poll/2)。在内核中，**select(2)** 和 **poll(2)** 是使用一个公共框架实现的。每个请求的文件描述符都被单独轮询，以确定它是否准备好。如果没有文件描述符准备好，则调用系统调用的线程可以睡眠，直到至少一个文件描述符变为可用。如果文件描述符在一个线程等待时变得可用，则该文件描述符必须唤醒正在休眠的线程。

[**selrecord(9)**](https://man.freebsd.org/selrecord/9) 函数族管理线程的休眠和唤醒。支持轮询的文件描述符必须为它支持的事件类型创建一个 `struct selinfo` 对象。该对象应通过将整个对象清零来初始化（例如，使用 `memset()`）。如果文件描述符的轮询函数发现文件描述符没有准备好，它必须对每个请求的事件调用 `selrecord()`，并将关联的 `struct selinfo` 对象传递给它。每当发生一个事件可能使文件描述符准备好时，必须调用 `selwakeup()` 在该事件的 `struct selinfo` 对象上。最后，在销毁 `struct selinfo` 对象之前，应该使用 `seldrain()` 唤醒任何剩余的线程。

对于字符设备，文件描述符轮询函数调用字符设备的轮询方法。此方法接受一个 **poll(2)** 事件的位掩码作为函数参数，并必须返回当前为真的事件掩码。此外，如果没有请求的事件为真，该函数还负责调用 `selrecord()`。请注意，字符设备不支持通过读写方法提供不同类型的优先数据，仅支持正常数据。

对于回显设备，我们支持读和写事件。我们在 softc 中添加了两个 `struct selinfo` 对象，每个事件一个。由于整个 softc 在创建时已被清零，因此在初始化 softc 时不需要进一步的更改。每个读写方法都会调用 `selwakeup()` 来唤醒可能等待的线程，针对的是 *另一个* 事件。其他一些地方也可以使回显设备变为准备状态，需要调用 `selwakeup()`。如果某个 I/O 控制命令扩展了缓冲区或清除了其内容，则可能会使设备准备好进行写操作。如果最后一个写入者关闭了设备，也可以使设备准备好进行读取。一个新的轮询方法确定设备的当前状态，并根据需要调用 `selrecord()`。最后，在销毁设备时，每个事件都会调用 `seldrain()`。列表 6 显示了在读取方法中对 `selwakeup()` 的添加调用以及新的轮询方法。请注意，对于读取方法，`selwakeup()` 用于写事件。

**列表 7：设备轮询**

```c
static int
echo_read(struct cdev *dev, struct uio *uio, int ioflag)
{
      ...
      error = uiomove(sc->buf, todo, uio);
      if (error == 0) {
            /* Wakeup any waiting writers. */
            if (sc->valid == sc->len)
                  wakeup(sc);

            sc->valid -= todo;
            memmove(sc->buf, sc->buf + todo, sc->valid);
            selwakeup(&sc->wsel);
      }
      ...
}

...

static int
echo_poll(struct cdev *dev, int events, struct thread *td)
{
      struct echodev_softc *sc = dev->si_drv1;
      int revents;

      revents = 0;
      sx_slock(&sc->lock);
      if (sc->valid != 0 || sc->writers == 0)
            revents |= events & (POLLIN | POLLRDNORM);
      if (sc->valid < sc->len)
            revents |= events & (POLLOUT | POLLWRNORM);
      if (revents == 0) {
            if ((events & (POLLIN | POLLRDNORM)) != 0)
                  selrecord(td, &sc->rsel);
            if ((events & (POLLOUT | POLLWRNORM)) != 0)
                  selrecord(td, &sc->wsel);
      }
      sx_sunlock(&sc->lock);
      return (revents);
}
```

一对通用的 I/O 控制命令对于检查文件描述符的状态也很有用。`FIONREAD` 和 `FIONWRITE` 分别返回可以在不阻塞的情况下读取或写入的字节数。字节数作为类型为 `int` 的控制命令参数返回。列表 8 显示了回显设备对这些 I/O 控制命令的支持。请注意，返回的值被限制为 `INT_MAX` 以避免溢出。

**列表 8：FIONREAD 和 FIONWRITE**

```c
static int
echo_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
struct thread *td)
{
      ...
      switch (cmd) {
      ...
      case FIONREAD:
            sx_slock(&sc->lock);
            *(int *)data = MIN(INT_MAX, sc->valid);
            sx_sunlock(&sc->lock);
            error = 0;
            break;
      case FIONWRITE:
            sx_slock(&sc->lock);
            *(int *)data = MIN(INT_MAX, sc->len - sc->valid);
            sx_sunlock(&sc->lock);
            error = 0;
            break;
      ...
}
```

为了使这个功能更容易演示，我们在 `echoctl` 工具中添加了一个新的轮询命令。这个命令使用 **poll(2)** 查询回显设备的当前状态。如果设备可读，它会使用 `FIONREAD` I/O 控制命令输出可读取的字节数。如果设备可写，它会使用 `FIONWRITE` 输出可以写入的字节数。示例 3 显示了这个命令的几次调用，以及其他对回显设备的操作。请注意，由于在这个示例中没有其他写入者，设备即使为空也仍然是可读的。

**示例 3：轮询 I/O 状态**

```sh
# echoctl poll
Returned events: POLLIN|POLLOUT
0 bytes available to read
room to write 64 bytes
# echo "foo" > /dev/echo
# echoctl poll
Returned events: POLLIN|POLLOUT
4 bytes available to read
room to write 60 bytes
# cat /dev/echo
foo
# echoctl poll -r
Returned events: POLLIN
0 bytes available to read
```

## 通过 kqueue(2) 报告 I/O 状态

FreeBSD 提供了 [**kqueue(2)**](https://man.freebsd.org/kqueue/2) 内核事件通知机制，这是一个与 **select(2)** 和 **poll(2)** 独立的 API。通过 **kqueue(2)**，应用程序为每个所需事件在内核中注册一个持久性的通知。内核生成一系列事件，应用程序可以消费并采取相应的行动。与 **select(2)** 和 **poll(2)** 不同，应用程序无需每次等待新事件时都注册它关心的所有事件。这减少了应用程序的开销，同时也允许内核更高效地跟踪所需的事件。

一个内核事件由一个过滤器（事件类型）和标识符组成。某些事件的行为可以通过不同的标志进一步定制。对于文件描述符的 I/O，两个主要的过滤器是 `EVFILT_READ` 和 `EVFILT_WRITE`，分别用于判断文件描述符是否可读或可写。这些事件过滤器的标识符字段是整数文件描述符。此外，对于读写事件，内核事件结构还返回可以读取或写入的数据量，作为一个单独的字段。这避免了单独调用 `FIONREAD` 和 `FIONWRITE` I/O 控制命令的需要。

在内核中，内核事件由 `struct knote` 对象描述。此结构包含用于生成返回给应用程序的事件的事件字段副本。活动事件的列表存储在 `struct knlist` 对象中。由于 **select(2)** 和 **poll(2)** 处理的 I/O 事件通常与内核事件关联，因此 `struct selinfo` 将 `struct knlist` 作为其 `si_note` 成员嵌入其中。每个 knote 还与指向 `struct filterops` 对象的 `kn_fop` 成员相关联。这个结构以及用于操作 knote 和 knote 列表的 API 在 [**kqueue(9)**](https://man.freebsd.org/kqueue/9) 中有描述。

对于字符设备，`kqfilter` 方法负责将 `struct filterops` 对象附加到 knote 上。这包括设置 `kn_fop` 成员并将 knote 添加到正确的 knote 列表中。因此，字符设备的 `struct filterops` 对象不使用 `f_attach` 成员。`struct knote` 的 `kn_hook` 成员是一个不透明指针，`kqfilter` 方法可以设置该指针来将状态传递给 `struct filterops` 方法，类似于 `struct cdev` 中的 `si_drv1` 字段。

对于回显驱动程序，我们定义了两个 `struct filterops` 对象：一个用于读事件，另一个用于写事件。每个事件包括 `f_detach` 和 `f_event` 方法。我们重用了现有的读写 `struct selinfo` 对象中嵌入的 knote 列表。由于回显驱动程序使用了 **sx(9)** 锁，我们定义了自定义的锁回调函数，在创建回显设备时与 `knlist_init()` 一起使用。`f_detach` 方法使用 `knlist_remove()` 从关联的 knote 列表中移除 knote。`f_event` 方法将 `kn_data` 字段设置为适当的字节数，并在字节数非零时标记事件为已就绪。对于读事件的 `f_event` 方法，如果没有写入者，它还会设置 `EV_EOF`。新的 kqfilter 字符设备方法将 knote 附加到 `EVFILT_READ` 和 `EVFILT_WRITE` 过滤器的新 `struct filterops` 对象上。它还将 knote 的 `kn_hook` 成员设置为 softc 指针。最后，驱动程序中所有调用 `selrecord()` 以唤醒来自 **poll(2)** 或 **select(2)** 的睡眠线程的地方，现在还会调用 `KNOTE_LOCKED()` 来报告与读或写事件关联的 knote 的事件。列表 9 显示了用于读取过滤器的 `struct filterops` 对象及其关联方法。列表 10 显示了新的 `kqfilter` 字符设备方法。

**列表 9：EVFILT\_READ 过滤器**

```c
static struct filterops echo_read_filterops = {
      .f_isfd =      1,
      .f_detach =      echo_kqread_detach,
      .f_event =      echo_kqread_event
};

...

static void
echo_kqread_detach(struct knote *kn)
{
      struct echodev_softc *sc = kn->kn_hook;

      knlist_remove(&sc->rsel.si_note, kn, 0);
}

static int
echo_kqread_event(struct knote *kn, long hint)
{
      struct echodev_softc *sc = kn->kn_hook;

      kn->kn_data = sc->valid;
      if (sc->writers == 0) {
            kn->kn_flags |= EV_EOF;
            return (1);
      }
      kn->kn_flags &= ~EV_EOF;
      return (kn->kn_data > 0);
}
```

**列表 10：kqfilter 设备方法**

```c
static int
echo_kqfilter(struct cdev *dev, struct knote *kn)
{
      struct echodev_softc *sc = dev->si_drv1;

      switch (kn->kn_filter) {
      case EVFILT_READ:
            kn->kn_fop = &echo_read_filterops;
            kn->kn_hook = sc;
            knlist_add(&sc->rsel.si_note, kn, 0);
            return (0);
      case EVFILT_WRITE:
            kn->kn_fop = &echo_write_filterops;
            kn->kn_hook = sc;
            knlist_add(&sc->wsel.si_note, kn, 0);
            return (0);
      default:
            return (EINVAL);
      }
}
```

与 **poll(2)** 支持一样，我们通过向 echoctl 工具添加另一个命令来演示 **kevent(2)** 支持。新的 events 命令为 echo 设备注册读取和写入事件，并在接收到每个事件时输出一行。由于读取和写入事件默认是按级别触发的，echoctl 在为 echo 设备注册事件时设置了标志 `EV_CLEAR`。这样，仅在设备状态变化时才报告事件，触发驱动程序内的 `KNOTE_LOCKED()` 调用。示例 4 显示了跨一系列操作的 events 命令输出。前两个事件报告的是 echo 设备处于空闲状态时的初始状态，此时没有打开的读取器或写入器。在另一个 shell 中，我们执行命令 `jot -c -s "" 80 48 > /dev/echo` 向 echo 设备写入 81 字节数据。由于默认缓冲区大小为 64 字节，此命令在写入 64 字节后会在 **write(2)** 系统调用中阻塞。64 字节的写入触发了下一个 `EVFILT_READ` 事件，报告 64 字节可供读取。最后，在第三个 shell 中，我们执行命令 “`cat /dev/echo`” 从 echo 设备读取所有数据。**cat(1)** 的第一次 **read(2)** 系统调用读取了 64 字节输出，并触发了一个 `EVFILT_WRITE` 事件。然而，在 echoctl 进程查询 echo 设备状态之前，**jot(1)** 进程已被唤醒，并将剩余的 17 字节数据写入缓冲区，腾出了 47 字节的空间。这解释了第三个事件块中报告的第一个 `EVFILT_WRITE` 事件。写入剩余的 17 字节也触发了一个 `EVFILT_READ` 事件。然而，在此事件被报告时，**jot(1)** 进程已经退出，**cat(1)** 已经读取了剩余的 17 字节，因此 `EVFILT_READ` 事件报告 `EV_EOF`，并且没有字节可读。**cat(1)** 读取 17 字节时也触发了一个 `EVFILT_WRITE` 事件，这被报告为倒数第二个事件。最后，**cat(1)** 第三次调用 **read(2)**，返回 0 以表示 EOF。此读取还触发了一个 `EVFILT_WRITE` 事件，这是最后一个报告的事件。此最后一系列事件是非确定性的，在不同的运行中可能以不同的顺序或略有不同的值出现（例如，如果 **jot(1)** 尚未写入剩余的 17 字节，第一个 `EVFILT_WRITE` 可能报告 64 字节可写）。

**示例 4：通过内核事件报告 I/O 状态**

```sh
# echoctl events -W
EVFILT_READ: EV_EOF 0 bytes
EVFILT_WRITE: 64 bytes
...
EVFILT_READ: 64 bytes
...
EVFILT_WRITE: 47 bytes
EVFILT_READ: EV_EOF 0 bytes
EVFILT_WRITE: 64 bytes
EVFILT_WRITE: 64 bytes
```

## 结论

在本文中，我们扩展了 echo 设备，以支持具有阻塞和非阻塞 I/O 的 FIFO 数据缓冲区。我们还添加了通过 **poll(2)** 和 **kevent(2)** 查询设备状态的功能。本系列的最后一篇文章将描述字符设备如何为通过 [**mmap(2)**](https://man.freebsd.org/mmap/2) 创建的内存映射提供后备存储。

***

John Baldwin 是一名系统软件开发人员。他在过去二十多年中，直接向 FreeBSD 操作系统提交了多项更改，涵盖了内核的各个部分（包括 x86 平台支持、SMP、各种设备驱动程序和虚拟内存子系统）以及用户空间程序。除了编写代码外，John 还曾在 FreeBSD 核心团队和发布工程团队任职。他还为 GDB 调试器做出了贡献。John 与妻子 Kimberly 和三个孩子 Janelle、Evan 和 Bella 一起居住在弗吉尼亚州的阿什兰。


# 面向 Linux 和 Windows 用户的 bhyve

* 原文链接：[bhyve for the Linux and Windows Users](https://freebsdfoundation.org/our-work/journal/browser-based-edition/virtualization-2/bhyve-for-the-linux-and-windows-users/)
* 作者：**Jason Tubnor**

![bhyve 文章配图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-db77d7696256aaaac22a595af5cd11a31344fc9a%2Fbhyce-1.png?alt=media)

FreeBSD bhyve 虚拟化程序在 2011 年 5 月由 Neel Natu 和 Peter Grehan 向全世界宣布，随后由 NetApp 赠送给 FreeBSD。这为 FreeBSD 提供了与 Linux KVM 虚拟化程序竞争的机会。然而，bhyve 还有其他好处，它不仅小巧且稳健，性能优异，并且高度依赖 CPU 指令集，而不是通过解释执行。

最初的实现仅适用于 FreeBSD 客户机，直到过了一段时间，我们才看到 bhyve 能够运行其他操作系统。

首先是 Linux，然后有了一种方法来重新打包 Windows 8 或 Windows Server 2012 以使其安装。这对于普通用户来说管理起来过于复杂，直到 bhyve 引入 UEFI 启动特性，事情才真正起飞。

UEFI 启动是 bhyve 所等待的杀手级特性。它使得可以在 FreeBSD bhyve 上安装并运行各种操作系统。当 FreeBSD 11 发布时，我们终于拥有了与其他操作系统相媲美的虚拟化组件。

虽然 UEFI 启动是 bhyve 的杀手级特性，但 bhyve 的杀手级应用是 Windows Server 2016。这是一个转折点，企业能够在没有修改的情况下运行 bhyve 和 Windows，获得一个可靠的企业级虚拟化程序，以稳定的方式运行商业工作负载。

突然间，企业能够广泛部署设备，使用一个具有 2-Clause BSD 许可证的解决方案，并能够通过硬件或软件调整虚拟化程序，以解决其问题。

然而，问题仍然存在，因为 Windows 需要安装多个驱动程序，无论是在镜像中还是在安装后，以避免性能问题。在 2018 年 7 月，通过实现 PCI-NVMe 存储仿真，这个问题得到了部分解决，最终让 bhyve 在一般工作负载的存储性能上超越了 KVM。

如今，Windows 在 bhyve 上运行仍然至少需要 RedHat 提供的 VirtIO-net 驱动程序，以确保网络传输可靠且超过 1Gb/s。RedHat 提供的 Windows MSI 包中还有其他驱动程序，建议在生产环境实现前加载这些驱动程序。对于 Linux，大多数发行版都包括所有相关驱动程序，包括本文中使用的 AlmaLinux。对于 Linux 安装，推荐使用 NVMe 仿真存储作为后端，然而，如果你计划将 Linux 工作负载在 bhyve 和 KVM 之间迁移，建议将客户机设置为使用 VirtIO-blk 存储。

以下配置适用于典型的类型 2 虚拟化程序配置中的标准 FreeBSD 工作站，或用于仅托管客户机工作负载并与客户机相关联的存储和网络的专用 FreeBSD 服务器，适用于类型 1 虚拟化程序。

## 准备工作

通常，过去十年的所有现代处理器都适合用于 bhyve 虚拟化。

确保你的硬件配置启用了虚拟化技术，并且启用了 VT-d 支持。PCI 直通的使用超出了本文的范围，但建议启用 VT-d，以便在需要时可以使用。在你的计算机 BIOS/固件中配置好之后，你可以通过查看 CPU 的 Features2 中是否包含 POPCNT 来检查 FreeBSD 是否能够识别到。

```sh
# dmesg | grep Features2
  Features2=0x7ffafbff<SSE3,PCLMULQDQ,DTES64,MON,DS_CPL,VMX,SMX,EST,TM2,SSSE3,SDBG,FMA,
  CX16,xTPR,PDCM,PCID,SSE4.1,SSE4.2,x2APIC,MOVBE,POPCNT,TSCDLT,AESNI,XSAVE,OSXSAVE,AVX,
  F16C,RDRAND>
```

现在我们已经确认 CPU 准备就绪，我们需要安装一些软件包，以便轻松创建和管理客户操作系统：

```sh
pkg install openntpd vm-bhyve bhyve-firmware
```

简而言之，OpenNTPD 是来自 OpenBSD 项目的一个简单时间守护进程。它可以防止主机时间偏移。当虚拟化程序由于客户工作负载而面临极大压力时，这可能导致系统时间迅速失去同步。OpenNTPD 通过确保你的上游时间源通过 HTTPS 协议报告正确的时间，来保持时间的准确性。bhyve-firmware 是个元包，用于从软件包中加载 bhyve 支持的最新 EDK2 固件。最后，vm-bhyve 是用于 bhyve 的管理系统，使用 shell 编写，避免了复杂的依赖关系。

使用 vm-bhyve 为机器引导并做好准备非常简单，但需要注意一些 ZFS 选项，以确保客户在底层存储上能够保持良好的性能，特别是对于一般工作负载：

```sh
# zfs create -o mountpoint=/vm -o recordsize=64k zroot/vm
# cat <<EOF >> /etc/rc.conf
vm_enable="YES"
vm_dir="zfs:zroot/vm"
vm_list=""
vm_delay="30"
EOF
# vm init
```

在我们深入讨论之前，我们应该先下载稍后将使用的 ISO 文件，以便 vm-bhyve 安装程序可以使用它们。要将 ISO 下载到 vm-bhyve ISO 存储中，可以使用 `vm iso` 命令：

```sh
# vm iso https://files.bsd.engineer/Windows11-bhyve.iso
```

(sha256 – 46c6e0128d1123d5c682dfc698670e43081f6b48fcb230681512edda216d3325)

```sh
# vm iso https://repo.almalinux.org/almalinux/9.5/isos/x86_64/AlmaLinux-9.5-x86_64-dvd.iso
```

(sha256 – 3947accd140a2a1833b1ef2c811f8c0d48cd27624cad343992f86cfabd2474c9)

这些文件将被下载到目录 **/vm/.iso** 中。注意：AlmaLinux ISO 文件直接从项目官网下载，校验和可以上游验证。而 Windows11-bhyve ISO 文件是从 Microsoft 下载的，并经过修改，确保它能够安装在 Microsoft 认为是“未支持”硬件的设备上，并已提供用于协助本文。因此，该 ISO 文件仅应在实验室环境中使用。它已移除 CPU 和 TPM 要求，并且不再需要创建 Microsoft 帐户。

## 网络配置

默认情况下，vm-bhyve 使用桥接将系统的物理接口与分配给每个虚拟机的 tap 接口连接。在将物理接口添加到桥接时，某些功能，如 TCP 分段卸载（TSO）和大接收卸载（LRO），不会被禁用，但需要禁用这些功能，以确保虚拟机的网络功能正常工作。如果主机有 **em(4)** 接口，可以通过以下命令禁用它：

```sh
# ifconfig em0 -tso -lro
```

为了避免每次重启后都需要禁用这些功能，可以将其添加到系统的 `/etc/rc.conf` 文件中：

```sh
# ifconfig_em0_ipv6="inet6 2403:5812:73e6:3::9:0 prefixlen 64 -tso -lro"
```

根据使用的网卡不同，以上步骤可能不是每种情况都需要，但如果你遇到虚拟机网络性能问题，这就是可能的原因。

要配置虚拟交换机（桥接），可以使用 `switch` vm 子命令：

```sh
# vm switch create public
# vm switch add public em0
```

这将创建一个名为 `public` 的虚拟交换机，并将 em0 物理接口添加到该虚拟交换机中。

## 模板

模板用于帮助设置虚拟机配置，确保虚拟机使用正确的虚拟硬件和其他必要的设置。使用 root 用户，添加以下内容到模板仓库：

```sh
# cat <<EOF > /vm/.templates/linux-uefi.conf
loader="uefi"
graphics="yes"
cpu=2
memory=1G
disk0_type="virtio-blk"
disk0_name="disk0.img"
disk0_dev="file"
graphics_listen="[::]"
graphics_res="1024x768"
xhci_mouse="yes"
utctime="yes"
virt_random="yes"
EOF

# cat <<EOF > /vm/.templates/windows-uefi.conf
loader="uefi"
graphics="yes"
cpu=2
memory=4G
disk0_type="nvme"
disk0_name="disk0.img"
disk0_dev="file"
graphics_listen="[::]"
graphics_res="1024x768"
xhci_mouse="yes"
utctime="no"
virt_random="yes"
EOF
```

## 创建虚拟机

在准备好存储、网络、安装程序和模板后，现在可以创建虚拟机了。

```sh
# vm create -t windows-uefi -s 100G windows-guest
# vm add -d network -s public windows-guest

# vm create -t linux-uefi -s 100G linux-guest
# vm add -d network -s public linux-guest
```

上述操作使用相应的模板创建了 Windows 和 Linux 虚拟机，并为每个虚拟机分配了 100GB 的存储（使用文件存储）。同时，为每个虚拟机添加了一个连接到 `public` vSwitch 的网络接口。

Windows 虚拟机在安装过程中需要稍作调整，以便在安装 VirtIO 驱动程序之前能够进行网络访问。编辑虚拟机配置，将网络接口从 virtio-net 更改为 e1000：

```sh
# vm configure windows-guest
```

将 `network0_type="virtio-net"` 修改为 `network0_type="e1000"`。

待安装了 RedHat VirtIO 驱动程序，可以恢复为 `virtio-net`。

## 安装和使用虚拟机

要安装每个虚拟机：

```sh
# vm install windows-guest Windows11-bhyve.iso
# vm install linux-guest AlmaLinux-9.5-x86_64-dvd.iso
```

待安装启动，bhyve 将进入等待状态。它将在与 VNC 控制台端口建立连接之前不会开始 ISO 启动过程。要确定哪个虚拟机控制台正在相应的 VNC 端口上运行，可以使用子命令 `list` ：

```sh
# vm list
NAME DATASTORE LOADER CPU MEMORY VNC AUTO STATE
linux-guest default uefi 2 1G [::]:5901 No Locked (host)
windows-guest default uefi 2 8G [::]:5900 No Locked (host)
```

使用像 TigerVNC 和 TightVNC 这样的 VNC 查看器，连接每个虚拟机的 IPv6 地址和端口以开始安装：

```sh
[2001:db8:1::a]:5900 # if the host is remote
[::1]:5900 # if it is on your local machine using localhost
```

安装完 Windows 客户端后，可以加载 VirtIO 驱动程序。可以在以下位置找到驱动程序：

<https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.266-1/virtio-win-gt-x64.msi>

(sha256 – 37b9ee622cff30ad6e58dea42fd15b3acfc05fbb4158cf58d3c792b98dd61272)

安装完 Windows 后，使用 Edge 浏览器访问上述链接下载并安装这些驱动程序。安装完成后，关闭主机，将虚拟机配置文件中的网络接口切换回 virtio-net，系统即可正常启动。

现在，我们已经安装了可用的客户端，但需要对其进行控制，以便根据需要启动和停止。以下命令将对你的客户端执行基本操作，如启动、停止或立即关闭：

```sh
# vm start linux-guest
# vm stop windows-guest
# vm poweroff windows-guest
```

`stop` 和 `poweroff` 之间的区别在于，`stop` 向客户端发出 ACPI 关机请求，而 `poweroff` 立即终止 bhyve 进程，并不会干净地关闭客户端。

## 总结

本文简要介绍了如何控制 bhyve 并使用 FreeBSD 包管理库中直接提供的工具安装常见操作系统。vm-bhyve 能做的远不止本文所述的内容，详细信息可以参考 **vm(8)** 手册页。

***

Jason Tubnor 拥有超过 28 年的 IT 行业经验，涉及多个领域，目前是 Latrobe Community Health Service（澳大利亚维多利亚州）的 ICT 高级安全负责人。自 1990 年代中期接触 Linux 和开源软件，并于 2000 年开始使用 OpenBSD，Jason 利用这些工具在不同行业的组织中解决了各种问题。Jason 还是 BSDNow 播客的共同主持人。


# Xen 与 FreeBSD

* 原文地址：[Xen and FreeBSD](https://freebsdfoundation.org/our-work/journal/browser-based-edition/virtualization-2/xen-and-freebsd/)
* 作者：**Roger Pau Monné**

Xen 虚拟机监视器（hypervisor）最初始于 1990 年代末剑桥大学计算机实验室，项目名称为 Xenoservers。当时，Xenoservers 的目标是提供“一种新的分布式计算范式，称为‘全球公共计算’，能让任何用户在任何地方运行任何代码。这种平台对计算资源进行定价，并最终按资源消耗向用户收费。”

使用虚拟机监视器可以安全地在多个操作系统之间共享物理机器的硬件资源。虚拟机监视器是管理所有操作系统（通常称为客户机或虚拟机）的软件，并提供它们之间的分离和隔离。Xen 于 2003 年首次作为开源虚拟机监视器在 GPLv2 许可下发布，其设计不依赖操作系统，这使得添加 Xen 支持到新操作系统中变得非常容易。自 20 多年前首次发布以来，Xen 已获得个人开发者和企业贡献者社区的广泛支持。

## 架构

虚拟机监视器可以分为两类：

* 第 1 类：直接运行在裸金属上，直接控制硬件。
* 第 2 类：作为操作系统的一部分的虚拟机监视器。

常见的第 1 类虚拟机监视器有 VMware ESX/ESXi 和 Microsoft Hyper-V，而 VMware Workstation 和 VirtualBox 则是第 2 类虚拟机监视器的典型代表。Xen 是一种第 1 类的虚拟机监视器，但它的设计与微内核有许多相似之处。Xen 本身仅控制 CPU、本地和 I/O APIC、MMU、IOMMU 和一个定时器。其他部分由控制域（Dom0）处理，Dom0 是一个由虚拟机监视器授予特权的专用虚拟机。这样，Dom0 就能管理系统中的所有硬件，以及运行在虚拟机监视器上的所有其他虚拟机。还需要注意的是，Xen 几乎没有硬件驱动程序，这样可以避免与操作系统中已有的驱动程序发生代码重复。

### 架构

![Xen 架构与驱动程序域示意图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-393e03be828917e5cbc7d456e4aed591bb52f038%2Fxen-1.png?alt=media)

在最初设计 Xen 时，x86 架构上没有硬件虚拟化扩展；虚拟化的选项要么是全软件仿真，要么是二进制翻译。这两种选项在性能上都非常昂贵，因此 Xen 采取了不同的方式。Xen 并不打算模拟当前的 x86 接口，而是为虚拟机提供了新的接口。这个新接口的目的是避免虚拟机监视器在处理硬件接口仿真时的开销，而是使用新的接口，这个接口对虚拟机和 Xen 来说都更易于实现。这个虚拟化方法也被称为 Ring Deprivileging（特权等级下降）。

然而，这要求虚拟机知道它是在 Xen 下运行，并使用一组与原生运行时不同的接口。这一接口集被指定为 ParaVirtualized，因此使用这些接口的虚拟机通常被称为 PV 虚拟机。以下接口在 PV 虚拟机上被 PV 替代：

* 磁盘和网络。
* 中断和定时器。
* 内核入口点。
* 页表。
* 特权指令。

这种方法的主要限制是它需要对虚拟机操作系统的核心部分进行大量修改。目前，仍然支持 x86 Xen PV 的操作系统只有 Linux 和 NetBSD。曾经有个 Windows 的初步移植版可以作为 PV 虚拟机运行，但它从未发布，Solaris 也曾支持 PV。

随着 x86 CPU 硬件虚拟化扩展的加入，Xen 也获得了支持运行未修改（非 PV）虚拟机的能力。这些虚拟机依赖于硬件虚拟化以及硬件设备的仿真。在 Xen 系统中，这种仿真要么由虚拟机监视器本身完成（对于性能关键的设备），要么通过默认的 QEMU 外部仿真器在用户空间中完成。这种仿真完整 PC 兼容环境的硬件虚拟化虚拟机，在 Xen 术语中被称为 HVM。

因此，我们现在已经讨论了两种非常不同类型的虚拟机，一方面是使用 PV 接口避免仿真的 PV 虚拟机，另一方面是依赖硬件支持和软件仿真以运行未修改虚拟机的 HVM 虚拟机。

HVM 虚拟机使用的仿真 IO 设备，如磁盘或网卡，由于处理数据传输所需的逻辑和仿真旧接口的开销，性能通常不太好。为了避免这一性能损失，Xen HVM 虚拟机还可以选择使用 PV 接口来处理 IO。一些其他 PV 接口也可供 HVM 虚拟机使用（例如一次性 PV 定时器），以减少使用仿真设备时可能产生的开销。

虽然 HVM 允许每个可能的未修改 x86 虚拟机运行，但由于要仿真所有用于 PC 兼容环境的设备，它也具有较大的攻击面。为了减少暴露给虚拟机的接口数量（从而减少攻击面），Xen 创建了一个稍微修改过的 HVM 虚拟机版本，称为 PVH。这是 HVM 的精简版本，其中很多在 HVM 虚拟机中存在的仿真设备不可用。例如，PVH 虚拟机仅提供仿真的本地 APIC 和可能的仿真 IO APIC，但没有仿真的 HPET、PIT 或传统的 PIC（8259）。然而，PVH 模式可能需要修改虚拟机操作系统内核，以便它意识到自己是在 Xen 下运行，并且一些设备不可用。PVH 模式还使用特定的内核入口点，可直接启动到虚拟机内核，而无需依赖仿真固件（SeaBIOS 或 OVMF），因此大大加快了启动过程。然而需要注意的是，当启动速度不重要并且更注重易用性时，OVMF 也可以在 PVH 模式下运行，以便链式加载操作系统特定的启动加载程序。以下是对 x86 上不同虚拟机模式的简要比较表：

\| | PV | PVH | HVM | | | -- | --- | --- | | I/O 设备 | PV (xenbus) | PV (xenbus) | 仿真 + PV | | 传统设备 | 无 | 无 | 是 | | 特权指令 | PV | 硬件虚拟化 | 硬件虚拟化 | | 系统配置 | PV (xenbus) | ACPI + PV (xenbus) | ACPI + PV (xenbus) | | 内核入口点 | PV | PV + 原生 ① | 原生 |

① 当使用固件启动时，PVH 虚拟机可以重用原生入口点，但这需要在原生入口点中添加逻辑，以检测是否在 PVH 环境中启动。并非所有操作系统都支持此功能。

PVH 方法也被其他虚拟化技术采用，例如 AWS 的 Firecracker。尽管 Firecracker 基于 KVM，但它重用了 Xen 的 PVH 入口点，并通过不暴露（从而不仿真）传统的 x86 设备来实现相同的攻击面减少。

在 ARM 架构方面，Xen 的移植是在 ARM 已经支持硬件虚拟化扩展的情况下开发的，因此与 x86 相比采取了不同的方法。ARM 只有一种虚拟机类型，它相当于 x86 上的 PVH。该方法的重点是尽量不暴露过多的仿真设备，以减少复杂性和攻击面。

预计即将推出的 RISC-V 和 PowerPC 移植版本也将采取相同的策略，只支持一种虚拟机类型，更类似于 x86 上的 HVM 或 PVH。这些平台也具有硬件虚拟化扩展，因此不需要像经典的 PV 支持那样的东西。

## 使用和独特功能

Xen 的首次商业应用严格集中在服务器虚拟化上，要么是本地使用基于 Xen 的产品，要么是通过云服务提供。然而，由于其多功能性，Xen 现在也扩展到客户端和嵌入式领域。Xen 的小巧体积和安全性重点使其适用于广泛的环境。

Xen 在客户端（桌面）应用的一个很好的例子是 QubesOS，这是一款基于 Linux 的操作系统，专注于通过将不同进程隔离到虚拟机中来提高安全性，所有虚拟机都运行在 Xen 虚拟机监视器之上，甚至支持 Windows 应用程序的使用。QubesOS 在一些关键的 Xen 特性上依赖较大：

* 驱动程序域：网络卡和 USB 驱动程序运行在单独的虚拟机中，从而避免了这些设备的安全问题危及整个系统。请参见关于驱动程序域的图示。
* 存根域：处理每个 HVM 虚拟机仿真的 QEMU 实例不是在 dom0 中运行，而是在一个单独的 PV 或 PVH 域中运行。这种隔离防止了 QEMU 中的安全问题危及整个系统。
* 限制内存共享：通过使用授权共享接口，一个域可以决定哪些内存页面共享给哪些域，从而防止其他域（甚至是半特权域）访问所有虚拟机内存。

与 QubesOS 类似，还有 OpenXT：这是一个基于 Xen 的 Linux 发行版，专注于客户端安全，供政府使用。

### 驱动程序域

![Xen 驱动程序域示意图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-393e03be828917e5cbc7d456e4aed591bb52f038%2Fxen-2.png?alt=media)

Xen 在 x86 上的一些独特功能，广泛应用于多种产品：

* 自省：允许外部监视器（通常在用户空间的另一个虚拟机中运行）请求有关虚拟机执行的操作的通知。例如，这种监视可以包括访问某个寄存器、MSR 或更改执行特权级别。这个技术的一个实际应用是 DRAKVUF，一款恶意软件分析工具，它不需要在虚拟机操作系统中安装任何监视器。
* VM-fork：类似于进程分叉，Xen 允许分叉正在运行的虚拟机。这个功能虽然不能创建一个完全功能的分叉，但足以用于内核模糊测试。KF/x 模糊测试项目将内核置于一个特定状态，然后通过创建虚拟机的分叉开始模糊测试。所有分叉从相同的指令开始执行，但输入不同。在非常特定的状态下快速并行分叉虚拟机，对于实现每分钟极高的迭代次数至关重要。

自 ARM 移植版发布以来，Xen 在嵌入式部署（从工业到汽车领域）上受到了广泛关注。除了小巧体积和安全性重点外，Xen 还有一些关键特性，使其在这些应用中具有吸引力。首先，与第 2 类虚拟机监视器相比，Xen 的代码量相当有限，因此可以设想尝试对其进行安全认证。目前，上游正在努力使 Xen 符合 MISRA C 标准的相关部分，以便进行安全认证。

一些使 Xen 在嵌入式应用中非常有吸引力的独特功能包括：

* 小巧的代码库：使其可以进行审计和安全认证，此外代码库正在调整以符合 MISRA C 标准。
* CPU 池：Xen 可以将 CPU 分区为不同的组，并为每个组分配不同的调度器。然后，可以将虚拟机分配到这些组，从而使一组虚拟机使用实时调度器（如 RTDS 或 ARINC653）运行，而另一组虚拟机则使用通用调度器（如 credit2）运行。请参见 CPU 池示意图。
* CPU pinning：还可以对哪些主机 CPU 调度哪些虚拟机 CPU 进行限制，例如，当运行延迟敏感工作负载时，可以将虚拟机 CPU 独占分配给主机 CPU。
* 确定性中断延迟：Xen 在确保中断延迟保持低且确定性方面投入了大量工作，即使在有噪音邻居引起缓存压力的情况下也能保持这一特性。目前正在审查的补丁系列为 Xen 增加了缓存着色支持。此外，Xen 正在移植到 Arm-v8R MPU（内存保护单元）基础的系统上。这是 Xen 架构的一个重要变化，因为它一直以来都支持基于内存管理单元（MMU）的系统。在 MPU 中，虚拟地址和物理地址之间有平坦映射，因此可以实现实时效果，因为不涉及地址转换。Xen 可以创建有限数量的内存保护区域，以强制执行不同内存范围上的内存类型和访问限制。
* 无 dom0/hyperlaunch：这是一个起源于 ARM 的功能，目前也在为 x86 实现，可在启动时静态创建多个虚拟机。这对于静态分区系统非常有用，其中虚拟机的数量是固定的并且提前已知。在这种设置中，初始（特权）域的存在是可选的，因为某些设置不需要对最初创建的虚拟机进行进一步操作。

### CPU 池

![Xen CPU 池示意图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-232729e188bf3cda29817beadddaba81b8d0315e%2Fxen-3.png?alt=media)

## FreeBSD Xen 支持

与其他操作系统相比，FreeBSD 对 Xen 的支持加入得相对较晚。例如，NetBSD 是首个正式宣布支持 Xen PV 的操作系统，因为 Linux 完整的 PV 支持补丁直到 Linux 3.0（大约 2012 年）才合并。

FreeBSD 对 PV 有一些初步支持，但该移植版本仅支持 32 位且功能不完全。开发停止后，该版本在实现 PVH 支持时被删除。2010 年初，FreeBSD 增加了在作为 HVM 虚拟机运行时的 PV 优化，使 FreeBSD 可以利用 PV 设备进行 I/O，并使用一些额外的 PV 接口来加速，如 PV 定时器。

2014 年初，FreeBSD 获得了作为 PVHv1 虚拟机的支持，随后不久也支持作为 PVHv1 初始域。遗憾的是，PVH（也称为 PVHv1）的第一次实现设计错误，包含了过多与 PV 相关的限制。PVHv1 试图将经典 PV 虚拟机迁移到 Intel VMX 容器中运行。这是相当有限的，因为虚拟机仍然继承了许多来自经典 PV 的限制，且仅限于英特尔硬件。

在发现这些设计限制后，开始转向 PVH 的不同实现。新方法从一个 HVM 虚拟机开始，并尽可能去除仿真，包括 QEMU 所做的所有仿真。大部分工作实际上是在 FreeBSD 上开发的，因为这是我的主要开发平台，我进行了大量工作，最终实现了后来称为 PVHv2，现在称为纯 PVH。

FreeBSD x86 既可以作为 HVM 虚拟机也可以作为 PVH 虚拟机运行，并支持作为 PVH dom0（初始域）运行。实际上，x86 PVH 支持比 Linux 更早地合并到 FreeBSD 中。然而，与经典 PV dom0 相比，在 PVH 模式下运行仍然缺少一些功能。其中最大的缺失是 PCI 直通支持，这需要对 FreeBSD 和 Xen 进行更改才能实现。目前，Xen 上游正在努力为 PVH dom0 添加 PCI 直通支持，但仍在进行中，完成后还需要修改 FreeBSD 来使该功能可用。

在 ARM 方面，正在进行工作以使 FreeBSD 能够作为 Aarch64 Xen 虚拟机运行。这需要将 FreeBSD 中的 Xen 代码拆分，分离架构特定的部分和通用部分。进一步的工作正在进行，以将 Xen 中断多路复用与 ARM 中的原生中断处理集成。

## Xen 社区的最新进展

除了之前提到的旨在使 x86 上 PV 和 PVH dom0 达到特性一致性的持续努力外，Xen 上游还在进行许多其他工作。自上一个 Xen 发布版本（4.19）以来，PVH dom0 已成为支持的操作模式，尽管由于一些关键功能仍然缺失，存在一些限制。

RISC-V 和 PowerPC 的移植工作正在进展中，希望在几个版本后它们能够达到一个功能完整的状态，届时初始域可以启动，并且可以创建虚拟机。

至少在 x86 上，近年来大量时间花费在缓解硬件安全漏洞方面。自 2018 年初 Meltdown 和 Spectre 攻击爆发以来，硬件漏洞的数量稳步增加。这需要 Xen 方面付出大量的工作和关注。虚拟机管理程序本身需要修复，以避免成为漏洞的目标，同时很可能需要暴露一些新的控制功能，让虚拟机自我保护。为了减轻未来硬件漏洞对 Xen 的影响，我们正在开发一个新功能，称为地址空间隔离（也被称为 Secret Free Xen），该功能旨在删除直接映射以及所有敏感映射，使其不再永久映射到虚拟机管理程序的地址空间。这将使 Xen 不再容易受到推测执行攻击，从而可移除大量针对虚拟机管理程序入口点的缓解措施，并可能不再需要对未来的推测性问题应用更多缓解措施。

自 2021 年初以来，所有 Xen 提交都通过 Cirrus CI 测试系统在 FreeBSD 上进行构建测试。这极大地帮助了 Xen 在 FreeBSD 上的构建，因为使用 Clang 和 LLVM 工具链有时会产生或显示使用 GNU 工具链时不会显现的问题。我们目前测试 Xen 在所有受支持的 FreeBSD stable 分支上构建，并测试 HEAD 开发分支。Xen 最近停用了其自定义测试系统 osstest，现在完全依赖 Gitlab CI、Cirrus CI 和 Github actions 进行测试。这使得测试基础设施更加开放和文档完善，也便于新贡献者参与并添加测试。未来该领域的工作应包括在 FreeBSD 上进行运行时测试，即便最初使用 QEMU 而非真实硬件平台。

最近的版本还添加了工具栈支持，用于向 Xen 虚拟机暴露 VirtIO 设备。Linux 和 QEMU 都支持使用 VirtIO 设备，通过授予而非使用虚拟机内存地址作为前端和后端之间内存共享的基础。这一新增功能并不需要修改 VirtIO 协议，而是作为一个新的传输层实现的。还在努力引入一种不基于内存共享的传输层，因为这是某些安全环境的要求。未来，这将让 Xen 在保持安全性和隔离性的同时使用 VirtIO 设备，这种安全性和隔离性是使用原生 Xen PV IO 设备时所保证的。整体目标是能够在 Xen 部署中将 VirtIO 驱动程序作为一等接口进行重用。

安全认证和采用 MISRA C 规则也是过去几个版本的主要任务之一。最后一个 Xen 版本（4.19）已扩展以支持符合 MISRA C 规范的 18 条指令和 182 条规则中的 7 条指令和 113 条规则。采用过程是逐步进行的，每条规则或指令都可以在被采纳之前进行讨论和达成一致。由于 Xen 的代码库最初并未考虑到 MISRA 合规性，某些规则将需要全局或每个实例的局部偏离。此外，作为安全认证计划的一部分，Xen 已开始添加安全要求和使用假设。安全要求提供了软件（Xen）所有预期行为的详细描述，从而能够进行独立测试和验证这些行为。

## Xen 的未来

回顾 x86 PVH 支持首次在 FreeBSD 上添加的时候，这是一段漫长且并不总是顺利的过程。FreeBSD 是早期采用 PVH 的 dom0 模式的操作系统，很多 Xen 开发工作是在使用 FreeBSD PVH dom0 的过程中进行的。值得注意的是，FreeBSD 在近年来已经成为 Xen 的一等公民，现在每次提交到 Xen 仓库的代码都会在 FreeBSD 上进行构建测试。

最近，FreeBSD 移植到作为 Xen Aarch64 虚拟机运行的工作也取得了一些进展，考虑到 ARM 基础平台在服务器、客户端和嵌入式环境中的日益普及，这无疑是一个值得期待的功能。

看到 Xen 被应用于如此多不同的使用场景，且这些场景与其最初专注于服务器端（云）虚拟化的设计目的大相径庭，令人欣慰。我只能希望未来会有更多新的 Xen 部署和使用案例。

## 如何联系

Xen 社区通过 [xen-devel 邮件列表](https://xenproject.org/resources/mailing-lists/) 进行代码审查。对于更为非正式的交流和讨论，我们还设有一些供所有人访问的 [Matrix 房间](https://xenproject.org/resources/matrix/)。对于 FreeBSD/Xen 特定的问题，还可以通过 [freebsd-xen 邮件列表](https://lists.freebsd.org/subscription/freebsd-xen) 进行咨询，当然，你还可以通过 [FreeBSD Bugzilla](https://bugs.freebsd.org/) 报告与 FreeBSD/Xen 相关的任何问题。

***

**Roger Pau Monné** 是 Cloud Software Group 的软件工程师和 FreeBSD 提交者。他在 Xen 社区中的角色包括 x86 维护者、Xen 安全团队成员以及 Xen 提交者。他在 Xen 和 FreeBSD 的 x86 PVH 实现方面做了大量工作，现在他大部分时间都在处理与安全相关的功能或追踪错误。


# Wifibox：一种嵌入式虚拟化无线路由器

* 原文链接：[Wifibox: An Embedded Virtualized Wireless Router](https://freebsdfoundation.org/our-work/journal/browser-based-edition/virtualization-2/wifibox-an-embedded-virtualized-wireless-router/)
* 作者：**Gábor Páli**

![Wifibox 文章配图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-fc0407233eb94f015b47b35cf77c018812d41cbc%2Fwifibox-1.png?alt=media)

由于生活优先级的变化，我在 2017 年左右远离了 FreeBSD 几年。后来我重新回归，开始为自己构建一台新的基于 FreeBSD 的工作站，是一台联想 ThinkPad X220。我注意到，尽管它能够工作，但无线支持仍然远未达到最佳状态，驱动程序 `iwm` 既不稳定，性能也不足以日常使用。

我意识到，在无线网络领域，FreeBSD 仍然在努力追赶 Linux 系统的性能，原因在于缺乏最新的硬件支持。这背后有一个原因：FreeBSD 常常不被视为一等公民，因此它不会成为这些开发的目标，而它的网络子系统部分需要提升，以满足最新的要求。这不是一个简单的问题，FreeBSD 基金会正在资助一个长期项目，旨在为该堆栈带来更新，并建立一个框架，便于重用来自 Linux 的无线网卡驱动程序。

这一点让我感到困扰，我不能再耐心等待问题得到解决。我想重新参与 FreeBSD 的开发，因为我不仅喜欢使用它，还喜欢学习它。然而，我没有足够的时间在业余时间精通网络和驱动程序代码的开发，因此我需要寻找其他机会。

## 原型

当我了解到另一种方法时，我感到非常兴奋（感谢 Gábor Zahemszky 的提醒！）。这个方法的核心是利用 `bhyve` 的 PCI 直通功能来运行 Linux 虚拟机，借此可以直接与硬件通信，设置无线连接，并将网络共享给 FreeBSD 主机。我发现了 [David Schlachter 的一篇优秀博客文章](https://www.davidschlachter.com/misc/t480-bhyve-wifi-pci-passthrough)，文中详细介绍了整个过程，我也通过这篇文章的帮助成功构建了自己的原型。

在试验整个过程的过程中，我从至少两个角度进行了研究。首先，考虑到系统升级，是否这个方法可以长期维持；其次，没有深刻理解此解决方案的用户是否可以轻松安装和移除该方法。作为一名前 Ports 开发者，我知道我必须能够维护这些组件，并使它们与基本系统隔离，同时与其他第三方集成。那么为什么不利用现有的 Ports 框架呢？于是，在 2021 年 4 月诞生了 Port `net/wifibox` 的概念。

最初，我使用 Port `sysutils/vm-bhyve` 来构建和管理基于 [Alpine Linux](https://alpinelinux.org/) 的虚拟机。Alpine 是一款轻量级的 Linux 发行版，采用 OpenRC 作为 init 系统，使用 `musl` 标准 C 库使得能够创建小型应用程序，并集成了 BusyBox 用于最常用的命令行工具。最初，它是作为嵌入式优先的发行版创建的。我在使用 Docker 容器镜像时了解到了它，并记住了它的小巧和易用性。它得到了积极维护，提供了大量通过“aports”系统管理的软件包。回顾过去，整个系统在许多方面与 FreeBSD 类似，我也变得很喜欢它。

尽管 `vm-bhyve` 是一个出色的工具，但我觉得它对于这个特定的用例来说有些过于复杂。相反，我将其作为基本用户界面的模型，例如提供一个控制台让用户与虚拟机进行交互，以及一些基本的编排例程。由于这些例程需要与命令行的 `bhyve` 工具进行交互，我决定坚持使用基于 shell 的方法。如果我尝试用其他更高级的语言（如 Python）实现所有这些操作，可能不会有更好的体验，因为那样会不必要地增加构建时间，并引入对其他第三方软件包的依赖。

最终，Wifibox 的用户界面包括命令 `start`、`stop`、`restart`、`status` 和 `resume` 。恢复操作需要特别处理，因为已知在笔记本电脑睡眠时，虚拟机会失去与虚拟化 PCI 设备的连接，故必须以某种方式恢复这种连接。经过一些实验，我发现通过停止虚拟机、重新加载 `vmm` 内核模块并重新启动虚拟机，可以缓解这一问题。最近，Joshua Rogers 提出了一个 [解决方案](https://joshua.hu/brcmfmac-bcm43602-suspension-shutdown-hanging-freeze-linux-freebsd-wifi-bug-pci-passthru)，在内核层面解决了这个问题，但这个修复还没有被加入到基本系统中。

虚拟机与 PCI 设备的交互常常被证明是一个弱点，这通常限制了 Wifibox 本身的可用性。作为这种解决方法的改进，恢复方法的范围已经得到扩展。某些硬件配置对设备关闭和重启的方式有不同的反应。例如，得益于 Joshua 的工作，发现设备是否在虚拟机的关机过程中正确关闭非常重要。还发现，Linux 内核模块 `ath11k_pci` 在虚拟化环境中的运行并不理想，因为它假设消息信号中断（MSI）表的位置与主机一致。只有在 FreeBSD 主机以某种方式支持为虚拟机注入主机物理 MSI 信息或禁用 MSI 虚拟化时，才能处理这个问题。

## VirtFS/9P 支持

Wifibox 的一个主要设计原则是用户无需了解底层虚拟机，就能够将其作为本地应用直接在主机上运行。为了实现这一点，探索了最近完成的 `bhyve` VirtFS/9P 文件系统直通支持的工作。通过在主机上挂载适当的目录到 `bhyve` 虚拟机，所需的配置文件可以导入，日志文件也可以导出。这样，用户就不必手动移动或保持虚拟机与 FreeBSD 主机之间的文件同步。

从 FreeBSD 13 开始有 VirtFS/9P 支持，但我希望将其扩展到当时的旧版本（12 和 11），以扩大其用户群。幸运的是，这个功能包含在一个单独的模块中，我能够创建另一个 Port，名为 `sysutils/bhyve+`，以自动修补基本系统中的 `bhyve` 源代码，使其包含该模块。有了这个，用户不必等待原作者回溯这个功能，但可以根据需要为 Port `net/wifibox` 拉入这个额外的依赖项。除了添加 `virtfs-9p`，`bhyve+` Port 还包含了许多其他修复，使得 Wifibox 可以运行。基本目标是组合一个版本的 `bhyve`，使其在每个主要的 FreeBSD 版本中都相同，最大程度地减少差异。这个版本本来是基于 13.x 线的，但相关的架构变更使得这一点变得不简单，后来这个想法被放弃了。多年来，它的相关性逐渐减弱，最终变得过时。

为虚拟机创建磁盘镜像面临许多挑战。主要问题是，最初的镜像是一个预安装的 Alpine 系统。它在我的工作站上本地维护，难以追踪其包含内容，并且由于写入各种临时和工作文件，它不断变化。从用户的角度来看，这引发了一个有效的问题：是否信任“别人家的虚拟机”。最初的版本大约是 640 MB，与典型的嵌入式系统镜像相比显得很庞大。这个大小部分是由于镜像包含了所有可能有用的工具和文件，因此，下一步的逻辑就是使其更小巧和模块化。

## 版本 1.0

在 2022 年 5 月发布的版本 1.0 中，进行了大量工作来重构镜像的创建过程。主要目标是使整个过程可重复且精简。从技术上讲，从零开始安装系统组件的复杂性被转化为 Port 的 `Makefile`。镜像获得了自己的子 Port `net/wifibox-alpine`，而协调脚本被拆分为 `net/wifibox-core`，`net/wifibox` 成为了一个元 Port。通过不断实验 Alpine 安装文件、根文件系统包及其包管理器 `apk` 工具，它们被调整为在 FreeBSD 上运行，并借助 Linux 兼容层实现。此外，为了实现自动化安装，将创建一个 `Makefile`，将系统安装到主机上指定的目录，并可选择扩展额外文件，会在那里下载并安装 Alpine 包。包本身提供了一种模块化构建镜像的方法，并使用户可以通过各种 Port 选项进行选择。例如，可以单独安装各大无线网卡品牌的固件文件，并为它们创建 FreeBSD 包变体。

基于包的方式使得创建额外的包以及修改现有包成为可能。不幸的是，许多上游的 Alpine 包含有一些额外的内容，如文档或附加的二进制文件，因此需要将其删除。但它也使得像 `mDNSResponder` 这样的应用可以被移植到这个平台，并作为解决方案的一部分运行。Linux 内核本身的包必须经过大量编辑，去除所有未使用的组件，减少其资源消耗并缩小攻击面。Wifibox 不需要标准的基于 `initrd` 的启动过程，因此初始的临时根文件系统被完全移除，直接从其根文件系统进行启动。AMD64 以外架构的配置文件和补丁被移除，因为 Wifibox 仅支持该特定架构。

虚拟机镜像通过 SquashFS 压缩，保持整体大小在 15 MB 左右。此方法还使用了一个只读的根文件系统，使得即使是 `root` 用户也无法篡改。它通过一个内存支持的临时文件系统扩展，挂载在 `/tmp` 下，管理运行时的文件写入，此外还使用 VirtFS/9P 挂载来读取来自主机的应用配置文件。启动过程通过 GRUB 进行，因此 Linux 内核（没有其模块）不包含在此镜像中，而是通过 `sysutils/grub2-bhyve` 预加载。

## 包框架

为了推出所需的 Wifibox 包，除了导入的上游包外，还采用了 Alpine Linux 的包框架。为了透明性和可重复性，每个自定义包都有自己的 `APKBUILD` 文件和版本控制的额外文件，这些文件存储在 `git` 中。包是在一个干净的、专用的 Alpine Linux `bhyve` 虚拟机上构建的，该虚拟机通常被称为 `wifibox-dev`，并在每次小版本的 Alpine 发布时重新创建。生成的包会上传到 GitHub，供用户方便下载。在过去，也曾尝试在 Linux 的 `chroot` 环境中构建包，但 FreeBSD 原生的 Linux 模拟支持不足以满足这个目的。跨编译的结果也类似，因此我最终也选择使用 `bhyve` 来实现这一点。根据 Bernhard Fröhlich 的建议，我目前正在考虑利用 GitHub Actions 在原生 Linux 环境中自动独立地构建 Wifibox 包。

## 基于 Linux 的无线网络栈

一个常规的基于 Linux 的无线网络栈在虚拟机中运行。首先，Linux 内核用于检测 PCI 无线设备，并通过其驱动程序以及相应的固件（如有必要）使设备运行。由标准的 OpenRC 服务启动无线网络接口 `wlan0`。接着，WPA Supplicant 或 `hostapd` 会连接到该接口以完成配置。除了无线接口外，`bhyve` 还暴露了一个虚拟的 `eth0` 以太网接口。在主机上，定义了一个桥接接口 `wifibox0`，并通过一个 `tap` 软件隧道将其与虚拟机中的 `eth0` 接口连接起来。使用 `ip-tables`，应用了网络地址转换（NAT）和数据包转发，使得流量在 `wlan0` 和 `eth0` 之间双向流动。IP 地址通过 Busybox 内置的 `udhcpd` 获取（在主机上通过 `eth0`），而在虚拟机内（通过 `wlan0`），则使用 `dhcpcd` 或 `udhcpd` 获取 IP 地址。主机的 IP 地址范围可以在 Wifibox 配置中控制，并根据用户需求进行调整。

![Wifibox 架构示意图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-27d4ea90324e2e31e444451cf8ebe8e23c51fa91%2Fwifibox-2.png?alt=media)

由于引入了 NAT，请注意，Wifibox 为 `eth0` 和 `wlan0` 使用了不同的 IP 地址范围。因此，某些应用程序可能无法直接正常工作，需要部署额外的工具和配置，例如端口转发。从安全角度来看，这可以视为一个优点，因为我们自动安装了防火墙。但同样，这也是一个缺点，因为它打破了端到端的连接性——这是互联网的核心原则。为了解决这个问题，曾进行过实验，将数据包转发推到以太网层级。例如，存在 [WLAN Kabel](https://github.com/escitalopram/wlan_kabel)，它实现了在以太网和无线接口之间移动数据包。它的使用效果中等，虽然流量能够正常流动，但 DHCP 通信无法穿过这个障碍，且观察到的性能较低。尽管如此，这仍然是一个有趣的方案，值得在未来进一步探索。

使用 Wifibox 的一个优势是所谓的“Unix 域套接字透传”，它帮助像 `wpa_cli` 和 `wpa_gui` 这样的工具直接与虚拟机内部运行的 WPA Supplicant 通信，从而用户不必在虚拟机内运行这些工具并与虚拟机交互。这是额外的功能，因为虚拟机文件系统中的 Unix 域套接字并未通过 VirtFS/9P 导出，因此主机无法看到它们。为了解决这个问题，虚拟机中运行了一个专门的 `uds_passthru` 进程，该进程在后台运行精简版的 `socat`，将套接字转换为 TCP 端口。主机一侧有一对进程，它通过这些端口与虚拟机通信，并将数据转回本地的套接字。通过这种方式，Wifibox 可以模拟该套接字的存在，实现平滑的通信。与虚拟机配合，协调脚本通过一个特定的配置文件自动管理套接字透传。

## 作为产品的 Wifibox

虽然 Wifibox 客户操作系统主要基于 Alpine Linux，但它通常包括额外的补丁。例如，它从 Arch Linux 导入了一些补丁和软件包，因为 Arch 对无线设备的支持更好。但也发现，旧款博通网卡的驱动程序根本不支持 MSI，这是虚拟化环境中必须具备的功能。某些驱动程序的初始化延迟过慢，导致 WPA Supplicant 在启动时无法找到 `wlan0` 接口，因此实现了指数退避机制以增强其鲁棒性。正如经验所示，专门解决这些问题的小型操作系统发行版是非常必要的。

协调器脚本使得可以将 `bhyve` 与其他工具结合，进一步塑造其行为。由于 Anton Saietskii 的观察，`nice` 被用于帮助控制负责运行虚拟机的进程的优先级，避免过度加载主机。同样，增加了一个与 `daemon` 配合的层，用于监控状态，如果虚拟机崩溃或被故意重启，则重新启动它。

截至目前，正在积极维护 Wifibox，并且每年三月和九月发布半年度更新。在 Ashish Shukla 的帮助下，这些发布被推送到 FreeBSD Ports，因此可以通过 `pkg` 工具安装。需要注意的是，Wifibox 并未出现在 FreeBSD 发行版的安装介质上，这可能使得在通过无线网络安装 FreeBSD 时，使用 Wifibox 更加困难。然而，可以将所有必需的二进制包添加到安装介质中，并在开始安装过程之前使用它们初始化网络连接。

在项目的 [主页](https://github.com/pgj/freebsd-wifibox) 上，可以找到源代码及相应的 GitHub 仓库的指向，可以打开问题单并开始讨论。还有一个单独的仓库 [用于存放 Port](https://github.com/pgj/freebsd-wifibox-port)，发布的开发版本提供了预览下一步进展并测试修复问题的机会。

## 文档

Wifibox 附带了大量的文档，因此我推荐读者进一步研究。我还想强调一个隐含但重要的组织原则，即关于文档的编写。Wifibox 遵循分阶段的“边读边走”模式。这意味着它没有详尽的在线文档，主 GitHub 仓库中的 `README` 文件涵盖了介绍、基本安装说明和已知兼容的硬件配置列表。用户随后必须安装 Wifibox 才能访问手册页，手册页提供了有关如何使用该工具和配置文件位置的详细信息。然后，在配置文件中，提供了更多的说明，指导用户完成相关步骤，同时在遇到错误时提供有用的错误信息以帮助用户。虚拟机镜像有自己的专门手册页。感谢 John Grafton、Warner Losh 及许多其他用户提供反馈，帮助我改进这些文档。

## 总结

这一切是好奇心的产物，旨在为 FreeBSD 无线网络之旅中的挑战提供快速解决方案，并发掘 PCI 直通技术和 `bhyve` 虚拟化设计所带来的技术优势的另一种用法。Wifibox 仍然存在一些缺点，远不能替代原生解决方案。因此，它被称为一种嵌入式虚拟化无线路由器，避免了购买专用硬件的需求，并创造性地将 CPU 的现有虚拟化能力呈现为这种设备的模拟。然而，我相信这一方法仍然有一些值得追求的想法，例如，使用 `bhyve` 只在虚拟机中运行 Linux 内核及其驱动程序，并将其直接作为无线网络设备暴露出来。这将使该方法更加接近原生解决方案，但目前尚不清楚它是否可行，且需要多少工作量。在此期间，我希望 Wifibox 能够减轻将原生解决方案推向生产环境的压力，并向用户保证，FreeBSD 依然是一个出色的选择，他们不必放弃通过无线连接获得良好的速度和可靠连接的可能性。

***

**Gábor Páli** 是位多年来始终愉快且忠诚的 FreeBSD 用户，他也曾在文档编写和 Ports 开发方面获得过乐趣。他与妻子一起住在美丽的匈牙利城镇埃斯泰尔戈姆（Esztergom）的边缘。他支持在行业中推广函数式编程，现如今他为 Apache CouchDB 做贡献，编写 Erlang 和 Scala 代码。


# 嵌入式 FreeBSD：Fabric——起步阶段

* 原文地址：[Fabric – Baby Steps](https://freebsdfoundation.org/our-work/journal/browser-based-edition/virtualization-2/embedded-freebsd-fabric-baby-steps/)
* 作者：**Christopher R. Bowman**

![嵌入式 FreeBSD](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-51144a079f1d1f619f70b6128505568a3888d73c%2Fqianrushi-1.png?alt=media)

在之前的专栏中，我们简要地介绍了 Zynq 芯片，提及了它的结构。自那以后，我们对它没有太多讨论。但在上篇专栏中，我们成功地在 bhyve 上运行了 CentOS 镜像，现在是时候来看我们的第一个结构电路了。本篇专栏将专注于电路，不涉及 FreeBSD，但在接下来的几期中，我们将开始研究结合这两者的系统。

除了 [Zynq-7000 SoC 技术参考手册](https://docs.amd.com/r/en-US/ug585-zynq-7000-SoC-TRM/Zynq-7000-SoC-Technical-Reference-Manual)，该手册记录了构成 Arty Z7-20 功能核心的 Zynq 芯片外，Digilent 的 [Arty Z7 参考手册](https://digilent.com/reference/programmable-logic/arty-z7/reference-manual) 提供了大量关于 Zynq 芯片如何在板上连接的有价值的信息。查看第 12 节，我们可以看到该板上有 4 个 LED 直接连接到 ZYNQ 芯片（R14、P14、N16、M14）。如果我们将这些引脚设置为逻辑 1 或高电平电压，则这些引脚将源源不断地提供电流，电流通过 LED，再通过限流电阻流向地线，导致 LED 点亮。

既然我们刚刚开始，尝试构建最小且最简单的电路来点亮这些引脚。最简单的电路就是在结构中将这些引脚静态接高电平。好，我们怎么做呢？我们需要引入 Verilog 来实现这一点。

就像编程早期，程序是用机器码、汇编语言编写的，最终发展到使用像 C 这样的高级语言一样，集成电路最初也是手工绘制或用 Mylar 膜带布置的。随着电路规模的增大和 CAD 程序的发展，电路设计开始通过程序来完成——电子设计自动化（EDA）。原理图捕获程序可以通过图形化方式将设备（最初是晶体管，后来是门电路）连接起来，使用图形用户界面（GUI）进行设计。最终，出现了可以用文本描述电路的语言。我在 1.2 微米时代使用过一种早期的语言 SFL 来设计我前 3 个芯片中的两个。但在过去十年中，VHDL 和 Verilog 两种语言真正主导了芯片设计。这些语言在许多概念上类似于 Ada 和 C。Ada 和 VHDL 语言冗长，并具有强类型检查。它们都被指定为美国国防部工作的首选语言，尽管两者仍然存在，今天它们在各自领域的受欢迎程度差不多。另一方面，Verilog 就像 C 一样，已经成为电路设计中最流行的语言。它不像 VHDL 那样强类型，就像 C 不像 Ada 那样强类型一样。总之，现如今，如果你想设计一个数字（非模拟）电路，超过几个门电路，你将使用 VHDL 或 Verilog，而大多数新的设计都使用 Verilog。

Verilog 中的电路设计会通过综合工具转化为电路，就像我们用编译器将 C 转换为可运行程序一样。但过程不止于此。例如，除了将 Verilog 转换为实现设计的连接门电路外，在设计计算机芯片时，你还需要使用工具来放置门电路并布线。幸运的是，所有这些功能都包含在 Xilinx/AMD 的 Vivado 工具中。Xilinx/AMD 不仅提供 Vivado，而且它是免费下载安装的，你还可以免费获取一个许可证来解锁 Zynq 芯片的大部分功能。如果你想在 Arty Z7 开发板上做除了编程以外的其他工作，你将需要下载并在 Linux/Windows 机器上安装此软件。在上一期专栏中，我们已经介绍了如何设置一个运行 Linux 的 bhyve 实例，以便在 FreeBSD 机器上的 bhyve 虚拟机中运行 Vivado。

现在，我们已经做好准备，开始设计第一个简单电路来点亮板上的 LED。首先，我们需要 Verilog 描述来实现这个电路。我没法在一篇小文章中教会你 Verilog，因此我将展示设计并尽量介绍它的工作原理。

```c
module top(
    output [3:0]led
);

wire [3:0]led;

assign led = {1'b1, 1'b0, 1'b1, 1'b0};

endmodule
```

首先，设计通过模块捕获，本设计包含一个名为 `led` 的 4 位总线。这条总线由一组 4 个拼接常量驱动。这些常量是 1 位信号，其中一半是逻辑“高”或 1，另一半是逻辑“低”或 0。我选择了交替的值，以便可以分辨 LED 的连接方式：从最高位到最低位（msb to lsb）或从最低位到最高位（lsb to msb）。这样，我就不必通过板上的标记和文档来推测连接方式。

接下来，我们需要提供一个约束文件。约束文件有两个主要功能：它们传达时序信息，并且在 FPGA 领域，它们还传达一些布局信息。在我们的第一个实验中，我们需要告诉工具哪些芯片引脚连接到 `led` 总线的线，并且还需要告诉工具如何设置我们希望使用的 IO 引脚。Digilent 非常贴心地提供了一个包含此板和其他许多板信息的主 XDC 文件，存放在一个 GitHub 仓库中。不幸的是，他们没有在文件或 readme 中包含版权声明，因此我无法在我的项目中包含它。这里提供了几个相关的行，如果你使用我在 [static\_leds 仓库](https://github.com/christopher-bowman/static_leds) 中为本文提供的 make 文件，它会从 GitHub 下载该文件并取消注释相关行。你需要在系统中安装 GNU make 和 wget。

```c
set_property -dict { PACKAGE_PIN R14 IOSTANDARD LVCMOS33 } \
     [get_ports { led[0] }]; #IO_L6N_T0_VREF_34 Sch=LED0
set_property -dict { PACKAGE_PIN P14 IOSTANDARD LVCMOS33 } \
     [get_ports { led[1] }]; #IO_L6P_T0_34 Sch=LED1
set_property -dict { PACKAGE_PIN N16 IOSTANDARD LVCMOS33 } \
     [get_ports { led[2] }]; #IO_L21N_T3_DQS_AD14N_35 Sch=LED2
set_property -dict { PACKAGE_PIN M14 IOSTANDARD LVCMOS33 } \
     [get_ports { led[3] }]; #IO_L23P_T3_35 Sch=LED3
```

最后，我们需要运行 Vivado 将其转换为 BIT 文件，这是我们设计的电路的表示形式。遗憾的是，这并不像将这两个文件传递给 Vivado 那么简单。电路设计是一个复杂的过程，涉及许多选项。Vivado，像许多 EDA（电子设计自动化）工具一样，是一个基于 TCL 的工具，需要脚本来运行。在我的仓库中，我创建了最简单的 `GNUMakefile`，用于自动下载 XDC 文件，修补它并使用 TCL 脚本运行 Vivado。我建议你查看它，但如果你只是想继续进行，更新路径变量后，在 Linux 上运行简单的 `make` 命令，应该就能完成所有操作，并生成一个名为 `implementation/static.bit` 的文件，这是我们需要加载到 Zynq 芯片中的电路文件。

那么，我已经有了我的电路文件，接下来怎么做？U-boot 包含一个 FPGA 比特流加载程序，我们将从这里开始。把 `FPGA.bit` 文件复制到 SD 卡的 MSDOS 分区，将卡插入板中并按下重置按钮。在 U-boot 提示符下，打断启动过程并运行以下命令：

```c
Zynq> fatload mmc 0 0x4000000 static.bit
4045663 bytes read in 249 ms (15.5 MiB/s)
Zynq> fpga loadb 0 0x4000000 4045663
```

第一个命令将 `static.bit` 文件从 SD 卡的 FAT 分区加载到内存中。第二个命令告诉 U-boot 用当前在内存地址 `0x4000000` 中的文件内容来编程 FPGA。此时，你应该看到板上的四个 LED 中有两个亮起了红色。恭喜！你已经构建并加载了第一个 FPGA 设计！

我们可以在这里结束，但在结束之前，再做两件事。首先，让 LED 闪烁，而不仅仅是亮起；其次，看看如何在 FreeBSD 下加载 FPGA。这将为更酷的事情打下基础。

为了让 LED 闪烁，我们需要通过更改 Verilog 来修改电路。这个新的电路有一个新的 [仓库](https://github.com/christopher-bowman/blinky_leds)，但我将在这里总结这些更改。首先是我们的新 Verilog：

```c
module top(
    input clk,
    output [3:0] led
);

localparam cycles_per_second = 125000000;

reg [3:0]leds;
reg [31:0]counter;

always @ (posedge clk)
begin
  if (counter == 0) begin
    leds <= leds + 1;
    counter <= cycles_per_second;
  end else counter <= counter - 1;
end

assign led = leds;

endmodule
```

我们现在添加了一个时钟输入，它将驱动一个 31 位计数器，并且我们还添加了一个 4 位计数器。31 位计数器加载值 125,000,000，并递减到零。当它达到零时，4 位的 `leds` 计数器递增。我们选择 125,000,000，因为根据 Arty 参考手册第 11 节，我们看到以太网 PHY 在 H16 引脚上提供了一个 125MHz 的时钟。

接下来，我们需要告诉 Vivado H16 引脚上的时钟：

```sh
set_property -dict { PACKAGE_PIN H16 \
     IOSTANDARD LVCMOS33 } \
     [get_ports { clk }]; #IO_L13P_T2_MRCC_35 Sch=SYSCLK
create_clock -add -name sys_clk_pin -period 8.00 \
     -waveform {0 4} [get_ports { clk }];#set
```

同样，一个简单的 `make` 命令应该会构建出一个 `implementation/blinky.bit` 文件，该文件可以像上面一样转移到 SD 卡并加载到 FPGA 中。

现在，你应该能看到 LEDs 按二进制计数闪烁。

好了，在结束本期专栏之前，谈谈最后一件事。看看如何从 FreeBSD 中编程 FPGA。事实证明，这很简单。有一个 `/dev/devcfg` 设备，最初是为了让你直接将 bit 文件 `cat` 到该设备上，但我认为它的工作并未完全完成。有一个简单的 C 程序 `xbin2bit`，它有自己的 [`git 仓库`](https://github.com/christopher-bowman/xbin2bit)。如果你有 root 权限（默认情况下 `/dev/devcfg` 是 root 所有），你可以直接运行它并传递你的 bit 文件：

```sh
# xbin2bit blinky.bit
```

你运行了这个程序并看到 FreeBSD 系统停止了，但 LEDs 仍然在闪烁吗？没错，事实证明我们的 Verilog 设计需要稍微复杂一点，以确保处理器不会停止。我们将在下期专栏中进一步探讨这个问题。

如果你对这些内容有任何问题、评论、反馈或批评，我非常乐意听取。你可以通过 <articles@ChrisBowman.com> 联系我。

***

**Christopher R. Bowman** 在 1989 年首次在 VAX 11/785 上使用 BSD 系统，当时他在约翰霍普金斯大学应用物理实验室的地下两层工作。他随后在 90 年代中期使用 FreeBSD 在马里兰大学设计了自己的第一块 2 微米 CMOS 芯片。从那时起，他一直是 FreeBSD 用户，并对硬件设计以及驱动硬件的软件感兴趣。在过去的 20 年里，他一直在半导体设计自动化行业工作。


# DGP：一种新的数据包控制方法

* 原文链接：[Dynamic Goodput Pacing: A New Approach to Packet Pacing](https://freebsdfoundation.org/our-work/journal/browser-based-edition/virtualization-2/dynamic-goodput-pacing-a-new-approach-to-packet-pacing/)
* 作者：**Randall Stewart**

![TCP/IP 历险记](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-840bbc79b25e3e5808ca7a5f401930516e98323b%2Fdgp-1.png?alt=media)

本系列的上一篇文章集中讨论了支持 TCP 堆栈的 FreeBSD 基础设施中的 pacing。本篇文章继续探讨 FreeBSD 中的 pacing，重点介绍目前在 FreeBSD 开发版本中的 RACK 堆栈中可用的 pacing 方法。这种 pacing 方法被称为动态有效吞吐量 pacing（Dynamic Goodput Pacing，DGP），它代表了一种新的 pacing 形式，可以提供良好的性能，同时仍然保证网络的公平性。为了理解 DGP，我们首先需要讨论拥塞控制，因为 DGP 是通过结合两种传统上没有一起使用的拥塞控制形式来工作的。因此，本篇文章将首先讨论拥塞控制是什么，以及 DGP 结合的两种拥塞控制方式，这两种方式共同构成一个无缝的 pacing 机制。

## 拥塞控制

当 TCP 最初被引入到新兴的互联网时，它并没有包含所谓的拥塞控制。它有流量控制，即确保发送方不会超出接收方的处理能力，但完全没有考虑 TCP 对网络的影响。这导致了一系列被称为“拥塞崩溃”的故障，并促使对 TCP 进行修改，加入了一个“网络感知”组件，旨在确保 TCP 的操作不会对互联网造成问题。这个“网络感知”组件被称为拥塞控制。

### 基于丢包的拥塞控制

最早引入互联网的拥塞控制是基于丢包的拥塞控制。今天，它是最广泛部署的拥塞控制形式之一，尽管它也有一些缺点。基于丢包的拥塞控制主要有两种算法（虽然也有其他算法），一种叫做 New Reno \[1]，另一种叫做 Cubic \[2]。New Reno 和 Cubic 都共享一个基本的设计理念——加法增大和乘法减小（Additive Increase and Multiplicative Decrease，AIMD）。我们将更详细地看看 New Reno，因为它更容易理解。

TCP 会首先设置一些基本变量，默认值如下：

* **拥塞窗口（cwnd）** — 允许向网络发送的最大数据量，而不会造成拥塞。初始化为初始窗口的值。
* **慢启动阈值（ssthresh）** — 当 cwnd 达到该阈值时，增加机制将从“慢启动”切换到“拥塞避免”。ssthresh 初始设置为无穷大，但在检测到丢包时会设置为当前 cwnd 的一半。
* **飞行大小（FS）** — 向对端发送的尚未确认的字节数。显然，它从零开始，并在每次发送数据时增加，在每次数据累计确认时减去。
* **初始窗口（IW）** — 设置到 cwnd 的初始值，通常为 10 个数据段（10 x 1460），但可以更多或更少（最初 TCP 将其设置为 1 个数据段）。
* **算法 - “慢启动”（SS）** — 慢启动算法是加法增大部分使用的算法之一。在慢启动中，每当收到确认时，cwnd 会增加已确认数据的数量。
* **算法 - “拥塞避免”（CA）** — 拥塞避免算法会在每次确认完一个完整的拥塞窗口的数据后，将 cwnd 增加 1 个数据包。

因此，最初 cwnd 设置为 IW，增加算法设置为 SS，飞行大小设置为 0 字节。假设有无限量的数据要发送，实施过程会向对端发送 IW 大小的数据，同时将 FS 移动到 IW 大小。对端会每隔一个数据包发送一次确认。（某些实现，如 macOS，可能会每第八个数据包或每个数据包进行确认。）这意味着每收到一个确认，FS 会减少两个数据包，cwnd 会增加两个数据包，这意味着我们可以再发送四个数据包（如果在确认到达之前的飞行大小已经达到最大值，即 cwnd）。这个序列将持续进行，直到检测到丢包。

我们可以通过两种方式检测丢包：一种是通过返回确认中的丢包指示（即累计确认点没有前进），另一种是通过超时。在前一种情况下，我们将 cwnd 减半，并将这个新值存储在 ssthresh 变量中。如果是后一种情况，我们将 cwnd 设置为 1 个数据包，并再次将 ssthresh 设置为丢包前旧 cwnd 值的一半。

在任意一种情况下，我们开始重传丢失的数据，待所有丢失的数据都被恢复，我们就开始以新的较低的 cwnd 值发送新数据，并更新 ssthresh。注意，每当 cwnd 超过 ssthresh 点时，我们会将用于增加 cwnd 的算法更改为拥塞避免。这意味着待整个 cwnd 的数据被确认，我们将 cwnd 增加 1 个数据包。

在简要总结基于丢包的拥塞控制是如何工作的过程中，我跳过了一些更细节的部分（例如，如何识别丢包）和其他一些细微之处。但我希望给你一个概念上的了解，这样我们就可以将注意力转向互联网中的路由器，专注于这些基于丢包的机制引起的后果。

路由器通常与它们的链路关联有缓冲区。这样，如果一阵数据包到达（这种情况很常见），它们就不需要丢弃任何数据包，而是可以将数据包通过连接到下一个跳点的链路转发出去，尤其是当入站和出站链路速度不匹配时。看一下下面的图 1。

![具有缓冲区的瓶颈路由器示意图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-1541b4b4977c8ab297485d6fb349b548833bd25a%2Fdgp-2.png?alt=media)

**图 1：一个具有缓冲区的瓶颈路由器**

在这里，我们看到 P3 以 100Mbps 到达，它将被放入路由器缓冲区的第三个插槽。P1 当前正在传输到 10Mbps 的链路上，P2 正在等待传输。假设每个数据包大小为 1500 字节，P3 需要大约 120 微秒才能通过 100Mbps 的网络进行传输。当它轮到传送到 10Mbps 的目标网络时，它将需要 1200 微秒（即 120 微秒的 10 倍）。这意味着，路由器缓冲区中的每个数据包都会给刚到达的包增加 1200 微秒的额外延迟。

现在回过头来考虑一下我们的拥塞控制算法将要优化的目标。它会尽可能快地发送数据包，直到发生丢包。如果我们是网络上唯一的发送者，这意味着我们必须完全填满路由器的缓冲区，直到发生丢包。这就意味着我们在优化路由器，使其缓冲区始终保持满。由于内存便宜，路由器的缓冲区变得相当大。这就意味着，当通过基于丢包的拥塞控制算法进行大规模传输时，会出现长时间的延迟。在图 1 中，我们只看到 6 个数据包的插槽，但在实际的路由器中，路由器的缓冲区可能会有数百个甚至数千个数据包在等待发送。这就意味着，TCP 连接看到的往返时间可能会从几毫秒（当没有数据包在队列中时）变为几秒钟，原因是路由器的缓冲和 TCP 的 AIMD 拥塞控制算法总是试图保持缓冲区满。

你可能听说过“缓冲膨胀”（buffer bloat）这一术语，它会影响任何实时应用（如视频通话、语音通话或游戏），这正是基于丢包的拥塞控制所导致的问题。

### 基于延迟的拥塞控制

很长一段时间以来，研究人员和开发者都知道基于丢包的拥塞控制会导致缓冲区的填满。在关于缓冲区膨胀的讨论之前，已经提出了替代性的拥塞控制方法来解决这个问题。其中一个最早的提案就是 TCP Vegas \[3]。TCP Vegas 的基本思想是，堆栈会跟踪它所见过的最小 RTT，称为“基础 RTT”（Base RTT）。它在拥塞避免阶段使用这个信息来确定预期带宽，即：

*预期带宽 = cwnd / BaseRTT*

实际带宽也被计算出来，即：

*实际带宽 = cwnd / 当前 RTT*

然后进行简单的减法来计算差值，即：

*差值 = 预期带宽 - 实际带宽*

差值 *Diff* 然后用来决定是否应根据两个阈值 α < ꞵ 来增加或减少 cwnd。这些阈值帮助定义瓶颈处的缓冲区应有多少数据。如果差值小于 α，则增加 cwnd；如果差值大于 ꞵ，则减少 cwnd。每当差值在这两个阈值之间时，就不会对 cwnd 进行任何改变。这个聪明的公式，使用低值（通常是 1 和 3），使瓶颈处的缓冲区保持非常小，优化连接，使缓冲区始终仅足够满，以达到连接的最佳吞吐量。

在慢启动（Slow Start）阶段，TCP Vegas 修改了增长方式，通过每隔一个 RTT 来交替进行。第一个 RTT，慢启动按 New Reno 或其他基于丢包的拥塞控制机制的方式增加。然而，在下一个 RTT，TCP Vegas 不增加 cwnd，而是使用 cwnd 测量差值，再次计算路由器的缓冲区是否已饱和。当实际速率比预期速率低出一个路由器缓冲区时，慢启动就会退出。

### 混合两者的危险

使用 TCP Vegas 进行测试表明，RTT 和吞吐量都有所改善。那么为什么我们没有完全部署 TCP Vegas，从而获得所有的好处呢？

答案就在于，当基于丢包的拥塞控制流量与基于延迟的流量竞争时会发生什么。假设你的 TCP Vegas 连接忠实地调整连接，以保持瓶颈路由器缓冲区中只有 1 或 2 个数据包。RTT 低，吞吐量达到了最大份额。然后，一个基于丢包的流开始了，它当然会填满路由器的缓冲区，直到发生丢包，这是它唯一学会减速的方式。对于 TCP Vegas 流来说，它不断接收到一个信号，表示它的速度过快，导致它继续减少 cwnd，直到几乎没有吞吐量。与此同时，基于丢包的流占据了所有带宽。基本上，当这两种类型的拥塞控制混合时，基于延迟的机制总是会出现不良结果。由于基于丢包的拥塞控制在互联网中广泛部署，这就提供了一个巨大的阻力，阻止了基于延迟的拥塞控制的部署。

## 在 DGP 中混合丢包和延迟基方法

DGP 尝试在选择其 pacing 速率时整合基于丢包和基于延迟的方法。对于基于延迟的部分，选择了 Timely \[4]（并进行了适应性修改以适应互联网），尽管可以辩称，任何基于延迟的方法（包括 TCP Vegas）都可以为此目的进行适配。Timely 使用延迟梯度来计算一个乘数，该乘数与当前基于丢包的拥塞控制计算（无论是 New Reno 还是 Cubic）结合，从而得出总体 pacing 速率，使用以下公式：

```sh
   *Bw = max(GPest, LTbw) * TimelyMultiplier*
   *FillCwBw = cwnd / CurrentRTT*
   *PaceRate = max(Bw, ((FCC != 0) ? FillCwBw : min(FillCwBw, FCC)))*
```

我们将在接下来的子节中讨论上述公式的每个部分，以便让你了解 DGP 的工作原理。有关 Timely 的详细信息，我们建议阅读相关论文 \[4]。

### 良好吞吐量（GPest）

DGP 跟踪的基础测量之一是良好吞吐量（goodput）。这与 BBR 的 \[5] 传输速率相似，但在细微之处有所不同。传输速率计算的是到达 TCP 接收端的所有数据的到达速率。当没有丢包时，传输速率和 DGP 的良好吞吐量是相同的。但在丢包的情况下，DGP 的速率会减小。这是因为良好吞吐量是严格根据累积确认（cum-ack）的进展来衡量的，当丢包发生时，cum-ack 停止前进。恢复丢失数据包所需的时间会折合进良好吞吐量的估算中，从而降低 GPest 的值。

为了初步测量良好吞吐量，允许 IW 以突发方式发送，这开始了第一个测量窗口。良好吞吐量通常在 1 到 2 个往返时间的数据内进行测量，并根据该期间 cum-ack 的进展来计算。在测量期间，还会计算一个独立的 RTT，即 curGpRTT（稍后将作为输入传递给 Timely）。待 IW 被确认，我们就有了第一个测量的种子。接下来的三个测量会对估算值进行平均。进行第四次测量后，未来的估算将使用加权移动平均来更新当前的 GPest。每次开始新的 GPest 测量时，都会将 curGpRTT 保存到 prevGpRTT 中，并且也会开始新的加权移动平均 RTT，这将成为新的 curGpRTT（请注意，这个 RTT 是与 TCP 继续进行的平滑往返时间测量不同的独立测量）。当数据在传输过程中时，发送方会不断测量 GPest。如果发送方变为应用程序限制，当前测量会结束。请注意，若实现变为拥塞窗口限制，当前测量不会停止。这个描述比较简略，可能需要未来的文章详细阐述 RACK 堆栈如何测量良好吞吐量。

### 长期带宽 (LTbw)

DGP 还跟踪另一个带宽测量，称为 LTbw。LTbw 是所有被累计确认的字节总和，除以数据处于未确认状态的总时间。这个值通常会比当前的良好吞吐量（goodput）值小，但在带宽测量急剧下降的情况下，它可以为当前带宽估算提供稳定性。

### 延迟梯度与 Timely（TimelyMultiplier）

Timely 提供了一个乘数，一般在估算带宽的 50% 到 130% 之间。Timely 使用以下公式（来自论文）：

![Timely 算法公式](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-a682a822bc80fc846e8248c31ee34429b208c168%2Fdgp-3.png?alt=media)

Timely 是为数据中心环境设计的，其中不同点的 RTT 和带宽是已知的。在 DGP 中使用时，情况并非如此，因此我们将上面公式中的 `new_rtt` 和 `prev_rtt` 分别替换为 `curGpRTT` 和 `prevGpRTT`。我们只在完成良好吞吐量估算后进行 Timely 计算。计算出的乘数将保持连接不变，直到下一个良好吞吐量估算完成，并且乘数会在良好吞吐量更新时再次更新。请注意，Timely 使用了一个 `minRTT`，即最小预期 RTT。与数据中心中的已知 RTT 不同，互联网上的 RTT 不是已知的，因此它是通过在过去 10 秒内观察到的最低 RTT 推导出来的，类似于 BBR。此外，就像 BBR 一样，为了定期重新建立最小 RTT，DGP 会进入“probeRTT”模式，将 cwnd 降至 4 个分段，以便找到一个“新的”低 RTT。请注意，添加类似 BBR 的 probe-RTT 阶段也有助于 DGP 更好地与它所竞争的 BBR 流兼容。

通过这些调整，Timely 算法被适配进 DGP。有关 probeRTT 或 Timely 的详细信息，建议阅读相关论文。

### 基于丢包的节流或填充拥塞窗口（FillCwBw）

为了进行基于丢包的拥塞控制节流，存在一种简单的方法。取当前的 RTT（在任何执行 Recent Acknowledgement \[6] 的堆栈中保存），并将拥塞窗口除以它。这告诉节流机制应该以什么速率发送数据，以便将当前的拥塞窗口分布在当前的 RTT 上。任何基于丢包的拥塞控制方法，如 New Reno 或 Cubic，都可以与这种方法结合使用，简单地推导出一个由拥塞控制算法决定的节流速率。我们将此速率称为“填充拥塞窗口速率”（Fill Congestion Window rate，FillCWBw），因为它旨在在一个 RTT 内填满拥塞窗口。

需要注意的是，通过在整个拥塞窗口上进行节流，发送方很可能会减少丢包。这是因为通过在每次数据包微爆发之间留出时间，可以减轻瓶颈的压力，允许瓶颈在下一个微爆发到达之前排空一些数据。较少的丢包意味着拥塞窗口会增加，因为丢包是导致 cwnd 减少的唯一原因。

### 支点：填充拥塞窗口上限 (FCC)

到目前为止，DGP 基于良好吞吐量估算结合 Timely 算法，增加或减少速率，基于 RTT 梯度（即基于延迟的组件）。我们还根据拥塞窗口的值（通过使用任何正在使用的拥塞控制算法）和当前 RTT（基于丢包的组件）计算了节流带宽。这给了我们两个不同的带宽值，可以用来进行节流。

这就是填充拥塞窗口上限（FCC）发挥作用的地方。如果设置了 FCC（可以设置为零以始终获得最快的带宽），它将成为我们允许基于丢包的速率应用的限制。FreeBSD 中的当前默认值设置为 30Mbps。因此，例如，如果计算出的 FillCwBw 为 50Mbps，而 Bw（考虑了 Timely 乘数后估算带宽）为 20Mbps，那么我们将在 FCC 限制下进行节流，即在默认设置下为 30Mbps。如果 Timely 计算的 Bw 为 80Mbps，那么我们将以 80Mbps 进行节流。

在这里，FCC 充当了支点，并限制了名义上基于丢包的拥塞控制算法对节流速率的影响。FCC 声明，如果可能的话，你的连接将与其他基于丢包的流量竞争，以保持至少 FCC 的速率，基于拥塞控制值。如果两个值都未达到 FCC 限制，那么较大的一个值将占主导地位。

### 在互联网上测试时的总体性能

在我之前公司的一次大规模实验中，DGP 在设置 FCC 限制为 30Mbps（现在为默认设置）时，最多减少了 100 多毫秒的 RTT，并且没有导致质量体验（QoE）指标的实际下降。如果将 FCC 设置提高到 50Mbps，QoE 指标得到改善，例如播放延迟和缓冲情况有所改善，同时 RTT 的减少几乎可以忽略不计。由于 RTT 减少表明路由器的缓冲行为得到了很好的改善，因此在这次测试后，30Mbps 设置被作为默认设置。

## 在 FreeBSD 中启用 DGP

在已加载 RACK 堆栈并设置为默认堆栈的 FreeBSD 系统上，至少有两种方法可以启用 DGP。如果应用程序的源代码可用，可以将以下代码添加到源代码中，以将套接字选项 `TCP_RACK_PROFILE` 设置为值 `1`：

```c
socklen_t slen;
int profileno, err, sd;

….
profileno = 1;
slen = sizeof(profileno);
err = setsockopt(sd, IPPROTO_TCP, TCP_RACK_PROFILE, &profileno, slen);
```

上面的代码片段将在与 `sd` 相关的套接字上启用 DGP。

如果无法访问源代码，另一种机制是使用 `sysctl` 将所有使用 RACK 堆栈的 TCP 连接的默认配置文件设置为值‘1’。可以按如下方式操作：

```sh
sysctl net.inet.tcp.rack.misc.defprofile = 1
```

请注意，设置此值后，所有使用 RACK 堆栈的 TCP 连接将使用 DGP，默认的 FCC 值为 30Mbps。你还可以使用 `sysctl` 更改此默认值（FCC），以更好地匹配你的网络条件。`sysctl` 变量 `net.inet.tcp.rack.pacing.fillcw_cap` 保存 FCC 值，以字节每秒为单位。例如，如果我想将值设置为 50Mbps，可以使用以下命令：

```sh
sysctl net.inet.tcp.rack.pacing.fillcw_cap = 6250000
```

默认值为 3750000，即 30Mbps，你可以将你想设置的值（以比特每秒为单位）除以 8。所以，50,000,000 / 8 = 6,250,000。

如果你有源代码的访问权限，还可以使用套接字选项 `TCP_FILLCW_RATE_CAP` ，如下所示：

```c
socklen_t slen;
int err, sd;
uint64_t fcc;
….
fcc = (50000000 / 8);
slen = sizeof(fcc);
err = setsockopt(sd, IPPROTO_TCP, TCP_RACK_PROFILE, &fcc, slen);
```

请注意，这只会更改指定连接的 FCC 值，而不会影响整个系统。

你还可以通过将 FCC 值设置为 `0` 来关闭 FCC 功能，并始终以 Timely 或拥塞控制允许的最大速度进行分配。这可能会提供最佳性能，但不会减少路由器缓冲区的使用，因此不会减少缓冲膨胀。

### 如何设置参数？

那么，哪些设置适合你的网络？在大多数情况下，瓶颈出现在你的家庭网关，因此了解你的互联网连接带宽可以为你提供一个合理的 FCC 值设置。例如，我管理的两个站点，一个是对称的 1Gbps 连接，我为该机器设置的 FCC 值保持默认的 30Mbps。当然，这只影响使用 RACK 堆栈的出站 TCP 连接，其中服务器正在发送数据。保持默认设置意味着大部分基于延迟的性能将从我的服务器中出现，每个连接只会推送以维持 3% 的网络上传带宽，并使用基于丢包的机制。

我的第二个系统是一个非对称的有线调制解调器，上传速率只有 40Mbps。在这种情况下，我将 FCC 设置为 5Mbps。如果有超过 7 个连接，它们将开始相互推挤，使用基于丢包的机制，所有连接都会尽量获得至少 5Mbps 的带宽。

## 后续工作

目前，FCC 值在整个系统中是静态设置的。这意味着该值通常不是最优的，可能存在可以选择的更好的值（可能会同时提高性能和降低 RTT）。作者目前正在研究一种更动态的机制来设置 FCC 值。基本想法是连接在一段时间内测量实际路径容量。然后，一旦获得“路径容量测量”（PCM）值，系统将该值的一定百分比作为 FCC 值。理论上，这将使 DGP 在调整网络路径时更加动态，同时保留并争取为每种网络类型分配一定的带宽。希望这项工作将在 2025 年完成。完成后，RACK 堆栈将更改其默认设置以启用 DGP。

## 参考文献

1. S. Floyd, T. Henderson: “The NewReno Modification to TCP’s Fast Recovery Algorithm”, RFC 6582, April 1999.
2. S. Ha, I. Rhee, L. Xu: “Cubic: A New TCP-Friendly High-Speed TCP Variant”, in: ACM SIGOPS Operating Systems Review, Volume 42, Issue 5, July 2008.
3. L. Brakmo, L. Peterson: “TCP Vegas: End to End Congestion Avoidance on a Global Internet”, in: IEEE Journal on Selected Areas in Communications, Volume 13, No. 8, October 1995.
4. R. Mittal, V. Lam, N. Dukkipati, E. Blem, H. Wassel, M. Gohabdi, A. Vahdat, Y. Wang, D. Wetherall, D. Zats: “TIMELY: RTT-based Congestion Control for the Datacenter”, in: ACM SIGCOMM Computer Communication Review, Volume 45, Issue 4, August 2015.
5. N. Cardwell, Y. Cheng, C. Gunn, S. Yeganeh, V. Jacobson: “BBR: Congestion-Based Congestion Control”, in: Queue, Volume 14, Issue 5, December 2016.
6. Y. Cheng, N. Cardwell, N. Dukkipati, P. Jah: “The RACK-TLP Loss Detection Algorithm for TCP”, RFC 8985, February 2021.

***

**RANDALL STEWART** (<rrs@freebsd.org>) 已从事操作系统开发 40 余年，自 2006 年以来一直是 FreeBSD 开发者。他专注于传输协议，包括 TCP 和 SCTP，但也曾涉足操作系统的其他领域。目前他是独立顾问。


# 会议报告：我在都柏林的 EuroBSDCon 体验

* 原文链接：[会议报告：我在都柏林的 EuroBSDCon 体验](https://freebsdfoundation.org/our-work/journal/browser-based-edition/virtualization-2/conference-report-my-eurobsdcon-experience-in-dublin/)
* 作者：**Stefano Marinelli**

## 在会议前

最初产生参加 EuroBSDCon 的念头是在 2023 年。当时，[Coimbra](https://2023.eurobsdcon.org/) 本会是很适合多年后重返会议的好地点，但遗憾的是我未能成行。当 [都柏林的注册通知](https://2024.eurobsdcon.org/) 发布时，我决定这就是合适的时机。与此同时，[BSD Cafe](https://bsd.cafe/) 项目也已启动，其所获得的热烈反响给了我很大的动力。我想可以提议一场 [演讲](https://events.eurobsdcon.org/2024/talk/LNMLZX/)。我工作中每天接触 BSD 系统，服务于各种客户，分享我如何管理（以及现在仍然在管理）从 Linux 到 BSD 系统的过渡，可能会引起社区的兴趣。

我还记得那一刻，6 月时我点击了“提交”。我对妻子说：“无论是作为讲者还是参与者，九月我们都会去都柏林。”她和我一起工作，所以她也会参加这次会议。这是近年来我最好的决定。

我还记得在去办公室的路上，我瞥了一眼智能手表上的通知：我的演讲被接受了。刹那间，我感到两种截然不同的强烈情绪：兴奋与恐惧。我不怕公开演讲，但在这样的场合再次用英语演讲……事实上，这种恐惧是多余的，两个月后我才意识到这一点。

![都柏林酒店爱尔兰海景](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-e05fb7c2d9e66fec78a8f4634feb105d33622a7e%2Fhui-1.jpg?alt=media)

随着活动临近，组织者提供了所有必要的信息。他们非常耐心，即使我问了一些看似琐碎的问题。这是我第一次参加 EuroBSDCon，而“BSD”作为名字的核心，本该教会我每个 BSD 用户都知道的事情：一切都被详尽地记录在案。常见问题解答（FAQ）经常更新。这是一个公认的 *特性*：与 BSD 相关的文档总是无可挑剔的。

我们在周四离开，因为我们不会参加教程。这是我第一次到爱尔兰，第一次参加 BSD 大会，也是第一次作为演讲者。到达后，一切都安排得非常妥当：交通指引、为讲者安排的酒店，酒店还可以看到壮丽的爱尔兰海景。组织者非常懂得如何宠爱他们的讲者。 那天晚上，我们下楼去吃晚餐，我感到有些紧张，因为我非常不擅长认脸。我确信会碰到一些我在网上认识很久的人，但我却认不出他们来。

## 演讲前一天

周五早上专门用来复习笔记和幻灯片。每个演讲的时间为 45 分钟，包括提问，我几乎可以肯定我会稍微超时。在测试演练中，我控制在 50 分钟以内，但没有在 45 分钟以内。最后一场演讲恰好在社交活动之前，好处是小小的超时不会影响其他讲者，我也可以将问题推迟到社交活动上。无论如何，我对结果感到满意。我还不知道观众会如何接纳我的演讲，但社区的热情和完美的组织已经让我感到自己是特别群体的一部分。

早上，我收到 Philipp Buehler 的电子邮件，建议下午去会场快速测试投影仪。目的是避免演讲前出现浪费宝贵时间的临时技术问题。因此，午饭后，我们愉快地漫步穿过都柏林，前往会议场地。

## 会场和初步印象

![EuroBSDCon 讲者胸卡](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-76329d429982298c169c53a21b68ee1163872413%2Fhui-2.jpg?alt=media)

EuroBSDCon 在 [都柏林大学学院（UCD）的 O’Reilly Hall](https://www.ucd.ie/universityclub/conferenceandbanqueting/theoreillyhall/) 举行——我觉得这是非常合适且舒适的地点。在入口处，有两个区域：右侧是签到和注册处（有三种颜色的胸卡：橙色是组织者和工作人员，白色是与会者，绿色是讲者）；左侧是 T 恤和更多工作人员及朋友的区域。到达时，我一自我介绍就受到了热情友好的欢迎。Henning Brauer 递给我们胸卡——看到那个绿色的胸卡，我心里充满了骄傲。和他在一起的还有 Katie McMillan，我之前从未与她交流过——即使在网上也没有，但我曾看到过她在 BSDCan 上的演讲视频，所以见到她非常高兴。另一桌旁坐着几位人士，包括 Mischa Peters（他给了我一件 [OpenBSD.Amsterdam](https://openbsd.amsterdam) 的 T 恤），我的“同事”Jeroen Janssen（aka h3artbl33d，[exquisite.social](https://exquisite.social/) Mastodon 实例的管理员），Peter Hansteen、Paul de Weerd、René Ladan、Janne Johansson、Guido Van Rooij、Michael Reim、Benny Siegert 等。我多年来一直敬佩的人们都聚集在那里。我们聊了一个多小时，随着时间推移，有些人加入，也有些人离开。我早就知道 BSD 社区是积极的，但我惊讶于各个小组之间的凝聚力。我立刻感到非常自在，那种身处熟悉的地方、在亲密朋友中间的感觉。

其间，Franco Fichtner 到了，准备做相同的技术检查。[OPNSense](https://opnsense.org/) 是我为客户选择路由器/防火墙解决方案时的首选之一，和他直接交流真的很愉快。

当教程继续进行时，参与者们回到各自的课程。我们回到酒店稍作休息，再次复习了我的演讲内容，还写了我回家后要发布的“[我解决了问题](https://it-notes.dragas.net/2024/10/03/i-solve-problems-eurobsdcon/http://)”博客文章。

## 星期六：会议正式开始

星期六早上 9:30：注册和参与者陆续到达。我既紧张又兴奋地看着每个人的胸卡，试着读取名字，然后尽量记住！就在入口大厅，我开始遇到人们：Vanja、Toni 和 Natalino 走过来，我们互相介绍了一下，聊了聊各自的角色。在热闹的氛围中，我很高兴能够说几句意大利语。Alfonso Siciliano 也到了，我们聊了不少。Alfonso 很亲切友好，且极为能干和严谨，我在 Leonardo Taccari（NetBSD 团队成员）身上也看到过同样的品质。

![EuroBSDCon 2024 开幕式](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-368b0d3842d6b214d92bde2e04c4f92e3834dc41%2Fhui-3.jpg?alt=media)

我四处走了走，看看一切是如何安排的。[这里有三个会议室](https://2024.eurobsdcon.org/venue.html)（Foyer A、Foyer B 和 Stage End），以及极好的明亮宽敞的大厅，俯瞰着 UCD 湖（UCD Lake）。这个大厅提供茶水、咖啡和水。还有一张桌子，人们把贴纸和其他赠品留在那里——我带了许多 BSD Cafe 的贴纸和杯垫，亲自分发并放在了桌子上，似乎大家都很喜欢。还有赞助商展台，中央则是 [FreeBSD 基金会](https://freebsdfoundation.org/) 的专属展台。我非常高兴亲自见到了 Deb Goodkin 和 Kim McMahon，与她们交谈真的很愉快。直接听她们谈基金会的想法和项目，真是太棒了。

与此同时，技术团队正在调整最后的细节——包括 Decimator 设备的古怪行为——这些都由 Michael Dexter 迅速处理，他连上了笔记本并更换了固件。能够与这么多高水平的专业人士在一起，真是太神奇了！

10:30，大家都前往 Stage End 会议室参加开幕式，与会者互相问候，介绍了会议日程并提供了一些有用的信息。11:00，Tom Smyth 做了主题演讲：《在欧盟制定政策时，我们正在呈现什么证据？》

Tom 情绪激动地演讲，充满激情、能力和自豪感。他的演讲内容全面而有价值，我相信每个人都非常欣赏他的发言。演讲结束后，经过实至名归的掌声，为讲者们发放了礼物：一条精美的绿色美利奴羊毛围巾，上面绣有 [Ogham](https://ogh.am/?text=EuroBSDCon) 字样。选择要参加的演讲是最困难的部分。每场演讲都很有趣，但三条并行的讲座轨道让我不得不做出艰难的选择。幸运的是，我知道演讲视频会在事后提供。

我参加了 Franco Fichtner 的演讲：《在 FreeBSD 上玩转工具——定制防火墙发行版的脚本编写》。对于我这种经常使用 OPNSense 的人来说，这个演讲非常有趣。理解他们的一些设计决策很有启发，也帮助我更好地理解了项目的方向。

## 与人见面并分享经验

我有机会见到的人中——我通过网上交流和他在 [BSD Now 播客](https://www.bsdnow.tv/) 中的角色已经认识了他——就有 Jason Tubnor。Jason 友好，充满活力，他问我是否愿意接受 [简短的采访](https://www.bsdnow.tv/579)，内容涉及这次活动、我、BSD Cafe 以及我的演讲。我很高兴接受了采访，我们在午餐休息期间完成了采访。楼上有设备齐全的房间，完美地布置好用于这个目的。BSD 风格的组织——总是高效的。

午餐休息时，我还与 Benedict Reuschling 会面并聊了聊。我非常感谢 Benedict（以及 Jason 和其他演讲者）为 [BSD Now 播客](https://www.bsdnow.tv/) 所做的事，还要感谢他一年前让我认识了 Jim Maurer，使我得以为 [FreeBSD 期刊](https://freebsdfoundation.org/our-work/journal/browser-based-edition/networking-10th-anniversary/make-your-own-vpn-freebsd-wireguard-ipv6-and-ad-blocking-included/) 撰写我的第一篇文章。还在基金会相关的话题中，我与 Ed Maste 进行了愉快的交谈。说实话，我是比较腼腆的人，感觉自己仅仅是接近别人可能会“打扰”他们。比如，我没能和 Colin Percival、Allan Jude、Dan Langille 等人——包括伟大的 Jon“Maddog”Hall 交流。我下次一定会补上！

午餐在大厅供应。供应着各种小份菜肴，还有甜点桌，甜点都非常好吃。在休息期间，能够结识并与许多人交流——太多了，无法一一列举。我担心自己会忘记某些人，那样会很遗憾，因为参加这次活动的每个人，都以某种方式与 IT、开源和 BSD 世界有所关联——这些人我很想与他们聊上好几个小时，而不仅仅是几分钟。

午餐后，我选择参加 Nicola Mingotti 的演讲：《树莓派 3B+ 和 NetBSD 中 GPIO 介绍，构建风速记录器应用》。在活动前，我已经与 Nicola 交流过，我也经常使用 NetBSD 和树莓派的组合。Nicola 展示了他的一套配置、他遇到的问题，以及这个解决方案是如何有效地解决这些问题的。

![UCD 建筑内休息区沙发](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-be1aff41f1db07a310e753da073e559f3b29ffcf%2Fhui-4.jpg?alt=media)

接下来的三场演讲都是我非常感兴趣的话题。由于难以选择，我利用这个时段来复习我的演讲内容，安静地坐在 UCD 建筑内非常舒适的休息区。柔软的沙发让我能够集中精力。我最大的担忧是跳过某部分或忘记一些重要的内容。在那里的时候，我遇到了 Dave Cottlehuber，我们聊了聊各种事情，包括邮件系统管理。见到 Dave 让我确认了我在线上对他的印象：既愉快又友好的人，而且知识非常渊博。

接下来我参加了 Kim McMahon 的演讲：《你如何为 FreeBSD 做宣传——我们如何帮助你》。我对此非常感兴趣，Kim 在 Stage End 会议室精彩地进行了演讲。我尽量只为自己使用且信任的解决方案做宣传，且不设任何障碍。如今，FreeBSD 可以高效且有效地解决我和我的客户面临的大多数挑战。但我并不是训练有素的沟通者，所以从像 Kim 这样的专业人士那里获得建议非常有帮助。

接下来的演讲选择很简单：Foyer A，Walter Belgers：《黑客——30 年前》。我选择这场演讲既是因为我对这个话题感兴趣（我喜欢真实的经历），也因为我的演讲将在同一会议室接着进行。此外，我知道这场演讲不会被录制，所以这是难得的现场机会。Walter 以诙谐而引人入胜的方式分享了许多有趣的轶事和故事。那是不同的时代，不同类型的计算，与今天的安全观念完全不同。然而，有些事情永远不会改变，这给人跨越时间的延续感。

## 我的演讲

时刻终于到来了，我站起来走向演讲者的讲台。突然，肾上腺素激增——然后又平静下来。有人离开了房间，也有人进来了。我忙着连接我的笔记本电脑，几乎没有注意到周围发生了什么，除了——令我非常高兴和荣幸的是——Marshall Kirk McKusick 教授留下来听我的演讲。他是另一个这次我没有“勇气”接近的人，但下次我一定会的。

Patrick McEvoy，一如既往地高效和专业，帮我戴上了麦克风，站到了自己的位置，点了点头。一切都完美无缺。

人们坐下，Henning 介绍了我。[舞台属于我](https://www.youtube.com/watch?t=19285\&v=u_bdSqqHm58)。是时候讲述我的故事了：大约 30 年前，一套 Linux 发行版的 CD-ROM 光盘套件，以及 22 年前遇到老师（[Özalp Babaoğlu](https://en.wikipedia.org/wiki/%C3%96zalp_Babao%C4%9Flu)——初始 BSD 之父），买了激光打印机（说服我的父母它是为了“大学用途”），并打印出 FreeBSD 手册，这一切让我走到了今天。多亏了老师、打印机和热情，我现在站在这里，在朋友们中间，而这些朋友们都在听我的故事。瞬间，我感到平静。我有些羞涩地开始了，但我的害羞只持续了几分钟。

“我是 Stefano Marinelli。我解决问题。”我看到人们露出了微笑。大家都理解了这个梗，毫无疑问。

随着我继续讲述，我看到观众的注意力越来越集中。多年来，BSD 的关注度有所下降。许多大公司在多年忽视开源解决方案后，开始拥抱 Linux 及其生态系统。虽然这给整个开源界带来了重大推动，但也间接减少了其他操作系统如 FreeBSD 的采用。有时原因是公司政治、技术储备（“更容易找到有 Linux 经验的人”）或者纯粹的意识形态或商业动机（“大家都知道 Linux 是什么，所以它更容易销售”）。但我做的恰恰相反。我不鄙视 Linux，可我更喜欢 BSD。而人们也想知道这是怎么做到的。

有一次，当我提到我关于 [一台 NetBSD 服务器运行超过 10 年无人工干预](https://it-notes.dragas.net/2023/08/27/that-old-netbsd-server-running-since-2010/) 的博客文章时，我看到前排的男士睁大了眼睛：“我不敢相信！你就是那个家伙！”这是美妙的时刻：他早就关注我的博客了，却没意识到我就是演讲者。在演讲结束后，我们聊了一会儿，他说了一些非常友善的话，我非常感激。谢谢你，Raymundo Soto！

![演讲结束后的致谢场景](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-142be55e363a1d431490d2b1f37dffd77570a7c8%2Fhui-5.jpg?alt=media)

演讲结束后（我略微超时），Henning 给了我演讲者的围巾，并建议将问题推迟到社交活动时再回答。尽管如此，还是有一些人立刻走过来，我很高兴能与他们聊天并回答问题。如果这些人愿意花一小时的时间来听我讲述，至少我也应该倾听他们的想法、经验和意见。 当我准备离开时，我遇到了 Max Stucchi 和 Salvatore Cuzzilla。握完手后，Max 确认了 GUFI（Gruppo Utenti FreeBSD Italia，FreeBSD 意大利用户组）仍然在运作，并邀请我加入，我非常高兴地接受了邀请。

## 社交活动

接着，我们前往社交活动的公交车站，活动地点是 BrewDog，位于都柏林 Docklands 区非常有特色的建筑，俯瞰着利菲河。公交车明显迟到了，我们在外面等了一会儿。尽管如此，我依然对自己的演讲反响感到松了口气和开心，所以等待也变得很愉快。我们和其他人聊了聊，最后公交车终于到了。我们跳上了上层座位，花了大约 25 分钟到达目的地。

我们穿过特色的 Dock 区，抵达了酒吧。扫描了我们的徽章后，我们得到了三张饮品券。在演讲后的紧张氛围过后，能够放松一下，与同事聊天，享受美食和美酒，体验一场真正的都柏林周六夜晚，感觉非常棒。Masanobu Saitoh 从日本专程来参加会议，他来到我的桌旁表示感谢。这对我意义重大，他拍的一些照片是我在此次活动中最喜欢的。

大约 22:30，我们决定回酒店，打了辆出租车。下楼时，我们遇到了同样要回酒店的 Robert Clausecker，于是决定一起回去。

我的心情如同火箭般高涨。我看到妻子看着我的眼神，她很高兴看到我这么平静和积极，已经开始想着第二天的事情。

## 星期天：活动收尾

星期天早上，活动开始得晚了半个小时。醒来时遇到一个意外：我几乎没声音了。过去几天说了太多话，加上都柏林的气候——和意大利的完全不同——这两者可能都有很大影响。这进一步限制了我与许多我想见的人互动。

星期天的主题演讲由 Kent Inge Fagerland Simonsen 主讲：《我们的软件可持续吗？》由于我对 IT 中的可持续性概念相当敏感，这个话题让我非常感兴趣。我坚信优化（无论是硬件还是软件）至关重要，特别是在中长期来看。如果我们让硬件变得更节能、更强大，但软件却膨胀到会抵消甚至加剧整体情况，那就没有意义了。

不幸的是，我没能参加，因为系统管理员没有固定的工作时间。那晚，两台物理服务器决定同时发生故障。由于这些服务器非常重要，而星期天又是会议的第二天，我决定在离开酒店之前先修复它们，稍晚些时候再到会议现场。好在 zfs-send、zfs-receive 和 DNS 更新处理起来不成问题。我等待数据传输，确保一切正常。用户没有察觉到任何问题，这让努力变得值得。FreeBSD、bhyve 和 jails 再次帮助最小化了问题和停机时间。

## 更多讲座和家庭合影

接下来的讲座选择很难，因为每个都非常有趣。我最终选择了 Alexander Bluhm 的演讲：《数据包在 OpenBSD 网络堆栈中的旅程》。非常有趣，Alexander 逐步展示了数据包的路径，并解释了沿途做出的决策。期间有许多问题和解答，使得讲座更具吸引力。在出口处，我遇到了刚刚做完演讲的 Sven Ruediger，我们交换了几句话。很遗憾没能听他的讲座，因为我听到许多人对他的讲座给予了非常正面的评价。

午餐后，大家集合拍摄了 [传统的家庭合影](https://2024.eurobsdcon.org/images/dublin-family-34636.jpg)。

仅仅几分钟，我们便在 UCD 大楼外集合好了——摄影师背后是湖泊——拍下了这张照片。200 多人很快排成了整齐的队伍。即便在这里，BSD 社区的高效也得到了体现。那一刻充满了欢乐与正能量。“家庭”合影这一词语比“团体”合影更能完美传达当时的氛围。 接下来是另一个艰难的选择：Kirk McKusick、David Brooks 还是 Jason Tubnor。我选择了 Jason 的讲座：《为澳大利亚健康行业的 NFP/NGO 建立 SD-WAN 设备》。Jason 的方法和我类似，详细说明了他在构建基础设施时遇到的有趣问题和挑战。我也喜欢用技术解决问题，而不仅仅是使用“盒子”，因此我非常享受他的演讲。

接着，我选择了 Michael Dexter 的讲座：《FreeBSD 和 Windows 环境》。[Michael 一直在从事 OpenZFS、jails 和 bhyve](https://callfortesting.org/) 的工作——这三者是我工作中不可或缺的工具——他的演讲一如既往地精彩、信息丰富且极具启发性。他让我又有了将一些 Windows 服务器从 Linux/KVM 迁移到 bhyve 的理由，而迁移后的结果非常好。在 Michael 演讲结束时，Patrick McEvoy 来到我面前，告诉我他非常喜欢我的演讲。这对我意义重大，Patrick 是我非常尊敬的人，他的评价对我非常重要。

不幸的是，祸不单行。当我准备去听 Dan Langille 的讲座，《用 FreeBSD jail 做傻事》时，我收到了大量的服务器警报，不得不赶紧出去解决问题。幸运的是，WiFi 信号非常好，我成功处理了问题，但还是错过了这场讲座。下一场讲座中途我才处理完，觉得不太合适中途闯入。所以，我利用这个时间与 Deb 和 Kim 讨论我的推广想法。人们往往不知道 FreeBSD 能为他们做什么，这也是我通过分享我的故事和博客文章，尽力向大家展示 BSD 并非“无法驯服的怪物”，而是我们的朋友。

## 闭幕式

最后，大家聚集在 Stage End 参加闭幕式。我了解了一些传统（例如 FreeBSD 基金会的抽奖——Vanja 是怎么把那个乐高吉他带回家的？它那么大！）以及“失物招领”拍卖。所得款项捐赠给了 <https://www.womensaid.ie/>。 闭幕式既有趣又富有信息量。感谢了所有赞助商和活动朋友，并提供了下载家庭合影的链接，我们还听到了关于下一个活动的信息，如 AsiaBSDCon 和 BSDCan。接下来是备受期待的时刻：宣布下届 EuroBSDCon 的举办地点。我希望这个地方还算方便，因为现在已经明确，EuroBSDCon 将成为我必须参加的活动。然后……扎格雷布（**译者注：扎格雷布是克罗地亚的首都**）！离家只有几小时的车程，所以我必须去参加。Mischa 让我承诺，由于我可以开车去，他希望我带更多的 BSD Cafe 周边。我会的。 最后，大家互道再见。那是发自内心的告别，大家都表示很快会再见。我感谢并祝贺了我能遇到的每一位，称赞了活动的技术、组织和内容质量。我也收到了来自大家同样温暖和亲切的回应。但……

……我口袋里还有最后一张贴纸。只有一张，因为一位 BSD Cafe 用户（Kaveman）告诉我他会参加会议，但我们还没见过面。就在最后时刻，就在我要离开时，我们终于见面了。把这张我保存了两天专门给他的贴纸送给他，真让我非常高兴。

## 最后的想法

即使是 Liam Proven（我在活动中没有与他交谈）也参加了这次活动，几周后，他在《The Register》上写了一篇 [关于此次活动和我的演讲的文章](https://www.theregister.com/2024/10/08/switching_from_linux_to_bsd/)，我非常感谢。

在见到所有这些人并参加讲座之后，我意识到 BSD 系统比以往任何时候都更加活跃，开发工作正在持续推进，FreeBSD 基金会和开发人员有着非常明确的计划，知道如何前进、需要什么，以及如何推动这些计划的实现。这次会议极大地丰富了我以及其他与会者，因为它由那些每天与正在讨论的系统打交道的人主办。几乎没有商业炒作，只有大量真正的技术内容。丰富的内涵。巨大的人员和技术价值。

EuroBSDCon 对我来说是一次难忘的活动。我怀疑自己能否完全用语言表达它带给我的所有情感和正能量。BSD 社区是包容、开放且合作的，而这次活动展示了每位参与者身上的这种精神。合作，而非竞争。BSD 社区有时保持低调，因为它专注于创造，而不是“销售”。在我看来，这是巨大的优势和强大的亮点。

Janne Johansson 在 IRC 上完美总结了我的感受：

“如果你看到一个矮个子一直微笑，那就是 Stefano Marinelli。每次我见到他时，他似乎都非常高兴能在那儿（这也是参加 EuroBSDCon 时正确的心态；））”

***

**Stefano Marinelli** 是一位 IT 顾问，拥有超过二十年的 IT 咨询、培训、研究和出版经验。他的专业知识涵盖操作系统，特别侧重于 **BSD 系统**——FreeBSD、NetBSD、OpenBSD、DragonFlyBSD——以及 Linux。Stefano 还是 BSD Cafe 的咖啡师，那是一个充满活力的 **BSD 爱好者** 社区中心，并且他领导了博洛尼亚大学的 FreeOsZoo 项目，使得开源操作系统镜像能够用于虚拟机。


# 读者来信

* 原文链接：[We Get Letters](https://freebsdfoundation.org/our-work/journal/browser-based-edition/virtualization-2/we-get-letters/)
* 作者：Michael W Lucas

![mailbox graphic](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-814b64e2758639a8e530c8d80cec175df2203491%2Fwe-get-letters-1.png?alt=media)

亲爱的 Letters 专栏主：

每天都有人发明新类型的虚拟化，或者把它搞得更复杂。全虚拟化、轻量虚拟化、容器、ABI 兼容性，没完没了。这什么时候是个头？我怎样才能跑到它前头？

——每天都在虚拟化赛跑中领先

附：本 *期刊* 的这专栏有新人了吗，还是说我会收到一篇毫无用处的吐槽？

亲爱的 RAVED：

认为我吐槽无用的人，准是 IT 新手。等你受的创伤让你形成条件反射，你就会懂。再等一周吧。

商学院教授 “跑到趋势前头” 的重要性。这话对系统管理员完全不适用。我们不仅本身就是趋势，还把业余时间用来设想怎么让趋势更陡峭、更时髦。

我们不断自欺欺人地以为能让事情变得更好，其实只能让它换一种方式变糟。

服务器只有一块硬盘？那就做镜像，或者上 RAID-5，或者用光纤通道甩到 NAS 上，让它变成别人的麻烦。臃肿软件跑得慢？那就裁剪程序，或者加内存，或者装一块快速缓存盘。宇宙射线导致文件系统损坏？用 copies=99 的 zraid3 阵列就能搞定！每一项 “改进” 都会引入新的故障模式。即便是资深系统管理员——他们骨子里深知《系统管理员守则第 16 条》（*改进的冲动是导致故障的首要原因*）——也躲不过这个陷阱。

虚拟化不过是这一点的又一表现罢了。

这并非新鲜事。还记得 chroot 吗？互联网从一个私有教育网络变成私营企业盘根错节的丛林时，许多核心软件被发现存在严重的安全问题。私有互联网时代没人想毁掉这张私有网络，可一旦公开，世上就有一小撮人喜欢把好端端的东西付之一炬。（看看今天的互联网，我不禁怀疑当年某些早期攻击者其实是穿越回来的时间旅行者，回去把我们从自己手中救出来。）如果某个程序存在 shell 逃逸漏洞，你可以从只包含该程序所需文件的目录里运行它。要是根本没有可逃逸的 shell，何谈 shell 逃逸！这既是个巧妙的 hack，也是 “解决错误问题” 的绝佳范例。

FreeBSD 的 jail 最初被构想为加强版 chroot。如果把那个被锁定的目录给它一个 IP 地址和它自己的进程空间，会怎样？它看起来就像完整的系统，却完全容纳在另一个系统之内！我们甚至可以让这个超级 chroot 拥有运行自己的子超级 chroot 的能力！它优雅、漂亮，又足够复杂，不仅会引发故障，还会引发你从未体验过的、刺激的全新故障模式。

不过 jail 爱好者们有好消息！你用 jail 多半是为了某个特定任务。它只需要基本系统的一小部分，而非完整的用户态。OccamBSD 就很实用，让你只安装必要的系统组件。jail 当年是 chroot 的进步，如今 chroot 反过来成了对 jail 的改进！

改进的冲动。

再说 Linuxulator。你可以拿一台 FreeBSD 系统让它跑 Linux 程序。Linux 毕竟只是个内核。把最不让你反感的 Linux 用户态装进某个目录，把你的程序 chroot 进去，当成 Linux 系统来跑。这样省得多装一台服务器专门跑 Linux——毕竟（实话实说）谁愿意跑一个没有 ZFS 和 PF 的 Unix？许多开发者把 Linuxulator 当作把 Linux 软件移植到 FreeBSD 的中间步骤。轻松的胜利，对吧？

当然。Linux 模式与 Linux “几乎完全兼容”。“几乎完全兼容” 就像 “几乎没麻风病”——对你身体好，但我可不碰。我去舔犰狳还更安全些。

不过这还不够。我们想进一步优化虚拟化，于是加入了 unionfs 和 base jail。优化！全然不顾如今磁盘空间如汪洋大海。是，你的大数据应用要存储阵列，但一块 NVMe 盘能塞下数千个操作系统安装，还能用一个简单脚本全升级。或者用布局最优的 base jail 把 “唯一真 jail” 用一个简单脚本升级，再用另一个简单脚本遍历所有 OS 安装并各自打补丁。没人愿意直面真正的问题——设计出不需要升级的系统——因为这会让整个计算机行业关门大吉，到时候谁来毒害地球？

重度虚拟化？bhyve、qemu、libvirt，全都是？不过是加了 CPU、进程和文件系统隔离的超级 jail。更安全？安全专家常说 “今天又是一种 hypervisor 逃逸”，这话可不只是说说。

虚拟化给无穷无尽的 “非寻常失败”，乃至 “惨败” 提供了机会。复杂互联系统纠缠不清，交互呈指数级攀升，攀升到那个由你颅内那团微微通电的布丁所无法处理的数字。你是系统管理员。你会坐下来冥想这些系统可能如何失效。有些失效模式显而易见：火灾、洪水、饥荒。有些不那么明显：万一 System V IPC 通信在两个特定 jail 之间泄漏，把公司机密泄到世上呢？再想想，这个借口先留着——等你决定举报你不道德的雇主时，会派上用场。怎么，你说他们道德？那他们赚的钱怎么够付你工资？别担心，等到捂不住的时候，你就准备好泄密了。

显而易见的 “问题” 当然是：现代硬件对大多数任务来说性能过剩到离谱。我租了一台小型专用服务器。它跑 jail 和 bhyve 虚拟机。我跑的最吃资源的应用是我自己的邮件服务器。邮件本身耗资源不多，但 rspamd 会把你给它的一切都吞掉。硬件大部分时间闲着，于是我不停往上堆功能。因为它就在那儿。何不试着让事情好一点，物尽其用？

计算能力过剩，从任何合理意义上看都不是问题。可我还是干了。

那么：跑到趋势前头？你是说跑到这一团噩梦般的优化纠缠前头？

是的，确实有办法 “跑到趋势前头”。

更快地失败。

祝好运。我对你有信心。

想向 Michael 提问？ 请发送至 <letters@freebsdjournal.org>

Michael W Lucas 的最新著作包括《Dear Abyss》（本专栏合集）、《Run Your Own Mail Server》和《Apocalypse Moi》。更多见 <https://mwl.io>。


# 基金会来信

* 原文链接：[Letter from the Foundation](https://freebsdfoundation.org/our-work/journal/browser-based-edition/kernel-development/letter-from-the-foundation/)
* 作者：Anne Dickison、John Baldwin

欢迎阅读 9/10 月刊的《FreeBSD 期刊》。本期是内核开发专题，收录的文章涵盖把 VPP 移植到 FreeBSD、FreeBSD 上的 Valgrind、字符设备驱动程序三部分教程的第一部分等话题，更多精彩还在后头！

在你沉浸在精彩文章之前，我们先带来一则关于 2025 年的更新。从 1 月起，《期刊》将改为季刊，继续为你带来同样顶尖的教程、文章和专栏。新的出版节奏让我们能专注于交付最优质的 FreeBSD 内容，同时让《期刊》继续人人可读。

《期刊》其他你喜爱的部分都将保持不变，期待未来一年继续为你带来有价值的 FreeBSD 内容！

谨代表 FreeBSD 基金会与《FreeBSD 期刊》编辑委员会，

**Anne Dickison** 副执行董事 FreeBSD 基金会

**John Baldwin** 《FreeBSD 期刊》编辑委员会主席


# 活动日历

* 原文链接：[2024 Events Calendar](https://freebsdfoundation.org/our-work/journal/browser-based-edition/kernel-development/events-calendar/)
* 作者：**Anne Dickison**

截至 2025 年 3 月的 BSD 活动。如发现此处未列出的 FreeBSD 相关活动或对 FreeBSD 用户有益的活动，请将详情发送至 <freebsd-doc@FreeBSD.org>。

![FOSDEM](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-b69b7170ee82d764ae45ef4fc00d1b28f15f4fad%2Fevents-calendar-1.png?alt=media)

## FOSDEM 2025

2025 年 2 月 1-2 日

比利时布鲁塞尔

FOSDEM 是由志愿者组织的为期两天的活动，旨在推动自由和开源软件的广泛应用。FOSDEM 于 2025 年 2 月 1-2 日举办，为开源和自由软件开发者提供一个相聚、交流想法、协作的平台。

![SCALE 22X](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-31932e0b1927af550af01bec6a0559c81090e059%2Fevents-calendar-2.png?alt=media)

## SCALE 22X

2025 年 3 月 6-9 日

美国加州帕萨迪纳

SCALE 22X——第 22 届年度南加州 Linux 大会——将于 2025 年 3 月 6-9 日在加州帕萨迪纳举行。

SCALE 是北美规模最大的社区主办的开源和自由软件大会。

![FreeBSD](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-70ac00e134ce943e58055ef20271aa6f13a6fb6b%2Fevents-calendar-3.png?alt=media)

## AsiaBSDCon 2025

2025 年 3 月 20-23 日

日本东京

AsiaBSDCon 是面向基于 BSD 的系统用户和开发者的大会。下一届大会将于 2025 年 3 月 20-23 日在日本东京举行。大会面向所有开发、部署和使用基于 FreeBSD、NetBSD、OpenBSD、DragonFlyBSD、Darwin 和 macOS X 系统的人。AsiaBSDCon 是一场技术大会，旨在汇集最优秀的技术论文和演讲，确保开源社区中的最新进展能被最广泛的受众所了解。


# 字符设备驱动程序教程

* 原文地址：[Character Device Driver Tutorial](https://freebsdfoundation.org/our-work/journal/browser-based-edition/kernel-development/character-device-driver-tutorial/)
* 作者：John Baldwin

![字符设备驱动教程配图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-aad50e2391b929dae171209ffe5d192f3863d6bf%2FCharacter-1.png?alt=media)

字符设备提供由设备文件系统（[**devfs(5)**](https://man.freebsd.org/devfs/5)）暴露给用户空间应用程序的伪文件。与标准文件系统不同，标准文件系统中所有文件的读取和写入等操作的语义是相同的，而每个字符设备为每个文件操作定义自己的语义。字符设备驱动程序声明一个字符设备 switch（character device switch）（`struct cdevsw`），其中包含每个文件操作的函数指针。

字符设备 switch 通常作为硬件设备驱动程序的一部分实现。FreeBSD 的内核提供了几种包装器 API，在一组更简单的操作之上实现字符设备。例如，[**disk(9)**](https://man.freebsd.org/disk/9) API 在 `struct disk` 的方法之上实现了一个内部字符设备 switch。某些设备驱动程序提供字符设备，向用户空间暴露未映射到现有内核子系统的设备行为。

其他字符设备 switch 完全以软件构造实现。例如，字符设备 **/dev/null** 和 **/dev/zero** 并未与任何硬件设备关联。

本系列由三篇文章组成，本文是第一篇。我们将构建一款简单的字符设备驱动程序，逐步添加新功能，以探索字符设备 switch 和它的驱动程序能实现的多项操作。每个版本的设备驱动程序的完整源代码可以在 <https://github.com/bsdjhb/cdev_tutorial> 找到。我们将从一款创建单个字符设备的基本驱动程序开始。

## 生命周期管理

字符设备驱动程序负责显式创建和销毁字符设备。活动的字符设备通过 `struct cdev` 的实例表示。字符设备通过函数 [**make\_dev\_s(9)**](https://man.freebsd.org/make_dev_s/9) 创建。此函数接受一个指向参数结构体的指针、一个指向字符设备对象指针的指针，以及一个 printf 风格的格式字符串及后续参数。格式字符串和后续参数用于构建字符设备的名称。

参数结构体包含几个必填字段和若干可选字段。在设置字段之前，必须通过调用 `make_dev_args_init()` 对结构体进行初始化。`mda_devsw` 成员必须指向字符设备 switch。`mda_uid`、`mda_gid` 和 `mda_mode` 字段应设置为设备节点的初始用户 ID、组 ID 和权限。大多数字符设备由 `root:wheel` 拥有，可以使用常量 `UID_ROOT` 和 `GID_WHEEL`。`mda_flags` 字段还应设置为 `MAKEDEV_NOWAIT` 或 `MAKEDEV_WAITOK`。如果需要，还可以通过 C 语言的按位或操作符（`|`）包含其他标志。对于我们的示例驱动程序，我们设置了 `MAKEDEV_CHECKNAME`，以便在 echo 设备已经存在时优雅地失败并返回错误，而不是使系统崩溃。

字符设备通过将字符设备的指针传递给 `destroy_dev()` 来销毁。此函数会阻塞，直到所有对该字符设备的引用都被移除，包括等待当前在该字符设备 switch 方法中执行的线程从这些方法返回。待 `destroy_dev()` 返回，就可以安全地释放字符设备使用的任何资源。或者，字符设备可以通过 `destroy_dev_sched()` 或 `destroy_dev_sched_cb()` 异步销毁。这些函数将字符设备销毁任务调度到内部内核线程。对于 `destroy_dev_sched_cb()`，在字符设备销毁后，提供的回调将与提供的参数一起调用。此回调可用于释放字符设备使用的资源。请记住，字符设备使用的资源之一是字符设备 switch 方法。这意味着，例如，模块卸载必须等待所有使用该模块中定义的函数的字符设备被销毁。

对于我们的初始驱动程序（清单 1），我们使用一个模块事件处理程序，在模块加载时创建一个 **/dev/echo** 设备，在模块卸载时销毁它。构建并加载该模块后，设备存在，但如示例 1 所示，它无法执行太多操作。该驱动程序的字符设备 switch（`echo_cdevsw`）仅初始化了两个必需字段：`d_version` 必须始终设置为常量 `D_VERSION`，`d_name` 应设置为驱动程序名称。

**清单 1：基础驱动程序**

```c
#include <sys/param.h>
#include <sys/conf.h>
#include <sys/kernel.h>
#include <sys/module.h>

static struct cdev *echodev;

static struct cdevsw echo_cdevsw = {
      .d_version =      D_VERSION,
      .d_name =         "echo"
};

static int
echodev_load(void)
{
      struct make_dev_args args;
      int error;

      make_dev_args_init(&args);
      args.mda_flags = MAKEDEV_WAITOK | MAKEDEV_CHECKNAME;
      args.mda_devsw = &echo_cdevsw;
      args.mda_uid = UID_ROOT;
      args.mda_gid = GID_WHEEL;
      args.mda_mode = 0600;
      error = make_dev_s(&args, &echodev, "echo");
      return (error);
}

static int
echodev_unload(void)
{
      if (echodev != NULL)
            destroy_dev(echodev);
      return (0);
}

static int
echodev_modevent(module_t mod, int type, void *data)
{
      switch (type) {
      case MOD_LOAD:
            return (echodev_load());
      case MOD_UNLOAD:
            return (echodev_unload());
      default:
            return (EOPNOTSUPP);
      }
}

DEV_MODULE(echodev, echodev_modevent, NULL);
```

**示例 1：使用基础驱动程序**

```sh
# ls -l /dev/echo
crw-------  1 root wheel 0x39 Oct 25 13:06 /dev/echo
# cat /dev/echo
cat: /dev/echo: Operation not supported by device
```

## 读取和写入

现在我们有了一个字符设备，接下来为它添加一些行为。正如“echo”（回显）这个名字所暗示的，这个设备应该通过写入设备来接受输入，并通过从设备读取来回显这些输入。为此，我们将在字符设备 switch 中添加读取和写入方法。

字符设备的读取和写入请求通过 `struct uio` 对象描述。这个结构中的两个字段对于字符设备驱动程序非常有用：`uio_offset` 是请求开始的逻辑文件偏移量（例如来自 [**lseek(2)**](https://man.freebsd.org/lseek/2)），`uio_resid` 是要传输的字节数。数据通过 [**uiomove(9)**](https://man.freebsd.org/uiomove/9) 函数在应用程序缓冲区和内核缓冲区之间传输。该函数会更新 `uio` 对象的成员，包括 `uio_offset` 和 `uio_resid`，并且可以多次调用。通过将一部分字节从应用程序缓冲区传输到内核缓冲区或反向传输，请求可以作为短操作完成。

第二版的 echo 驱动程序添加了一个全局静态缓冲区，用作读取和写入请求的后备存储。逻辑文件偏移量被视为全局缓冲区的偏移量。请求会被截断为缓冲区的大小，因此读取超出缓冲区末尾的部分会触发零字节读取，表示文件结束（EOF）。超出缓冲区末尾的写入会因错误 `EFBIG` 而失败。为了防止并发访问，使用全局 [**sx(9)**](https://man.freebsd.org/sx/9) 锁来保护缓冲区。由于 `uiomove()` 在访问应用程序缓冲区的页面时可能会休眠，因此使用 **sx(9)** 锁而不是常规的互斥锁。清单 2 显示了使用全局缓冲区的读取和写入字符设备方法。

**清单 2：使用全局缓冲区进行读取和写入**

```c
static int
echo_read(struct cdev *dev, struct uio *uio, int ioflag)
{
      size_t todo;
      int error;

      if (uio->uio_offset >= sizeof(echobuf))
            return (0);

      sx_slock(&echolock);
      todo = MIN(uio->uio_resid, sizeof(echobuf) - uio->uio_offset);
      error = uiomove(echobuf + uio->uio_offset, todo, uio);
      sx_sunlock(&echolock);
      return (error);
}

static int
echo_write(struct cdev *dev, struct uio *uio, int ioflag)
{
      size_t todo;
      int error;

      if (uio->uio_offset >= sizeof(echobuf))
            return (EFBIG);

      sx_xlock(&echolock);
      todo = MIN(uio->uio_resid, sizeof(echobuf) - uio->uio_offset);
      error = uiomove(echobuf + uio->uio_offset, todo, uio);
      sx_xunlock(&echolock);
      return (error);
}
```

这些方法的主体基本相同。原因之一是，`uiomove()` 的参数对于读取和写入操作是相同的。这是因为 `uio` 对象将数据传输的方向作为其状态的一部分编码。

如果我们加载此版本的驱动程序，现在可以通过读取和写入设备与其进行交互。示例 2 展示了几次交互，演示了回显行为。请注意，`jot` 的输出超出了驱动程序 64 字节缓冲区的大小，因此随后的设备读取被截断。

**示例 2：使用全局缓冲区回显数据**

```sh
# cat /dev/echo
# echo foo > /dev/echo
# cat /dev/echo
foo
# jot -c -s "" 70 48 > /dev/echo
# cat /dev/echo
0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmno#
```

## 通过 ioctl() 配置设备

全局缓冲区的固定大小是该设备的一个奇特之处。我们可以通过为该设备添加一个自定义的 [**ioctl(2)**](https://man.freebsd.org/ioctl/2) 命令来允许更改缓冲区的大小。I/O 控制命令通过命令常量命名，并接受一个可选的参数。

命令常量是通过 `<sys/ioccom.h>` 头文件中的宏 `_IO`、`_IOR`、`_IOW` 或 `_IOWR` 定义的。这些宏都接受一个组和一个数字作为前两个参数。两个值都是 8 位的。通常，组使用 ASCII 字母字符，而给定驱动程序的所有命令使用相同的组。FreeBSD 的内核定义了几个现有的 I/O 控制命令集。一个可以与任何文件描述符一起使用的通用命令集在 `<sys/filio.h>` 中定义，使用组 ‘f’。其他命令集则用于特定类型的文件描述符，例如在 `<sys/sockio.h>` 中为套接字定义的命令。对于字符设备驱动程序的自定义命令，不要使用 ‘f’ 组，以避免与 `<sys/filio.h>` 中的通用命令发生冲突。每个命令应使用不同的数字参数值。如果命令接受可选参数，则必须将参数类型作为第三个参数传递给宏 `_IOR`、`_IOW` 或 `_IOWR`。宏 `_IOR` 定义一个从驱动程序返回值到用户空间应用程序的命令（该命令“读取”来自驱动程序的参数）。宏 `_IOW` 定义一个向驱动程序传递值的命令（该命令“写入”参数到驱动程序）。宏 `_IOWR` 定义一个既由驱动程序读取也写入的命令。参数的大小被编码在命令常量中。这意味着具有相同组和编号但参数大小不同的命令将具有不同的命令常量。在实现对替代用户空间 ABI（例如，支持 64 位内核上的 32 位用户空间应用程序）的支持时，这一点非常有用，因为替代 ABI 将使用不同的命令常量。

如 FreeBSD 等 BSD 内核在通用系统调用层管理 I/O 控制命令参数的复制。这与 Linux 不同，后者将原始用户空间指针传递给设备驱动程序，要求设备驱动程序在用户空间和驱动程序之间复制数据。相反，BSD 内核使用命令常量中编码的大小参数来分配请求大小的内核缓冲区。如果命令使用 `_IOW` 或 `_IOWR` 定义，则通过从用户空间应用程序复制参数值来初始化缓冲区。如果命令使用 `_IOR` 定义，则将缓冲区清零。设备驱动程序的 ioctl 例程完成后，如果命令使用 `_IOR` 或 `_IOWR` 定义，则缓冲区的内容将被复制到用户空间应用程序中。

对于 echo 驱动程序，我们定义三个新的控制命令。第一个命令返回全局缓冲区的当前大小。第二个命令允许设置全局缓冲区的新大小。第三个命令通过将所有字节重置为零来清除缓冲区的内容。

这些命令在清单 3 所示的新 `echodev.h` 头文件中定义。使用头文件是为了让常量可以在用户空间应用程序和驱动程序之间共享。请注意，第一个命令将缓冲区大小读取到用户空间的参数 `size_t` 中，第二个命令将新的缓冲区大小写入用户空间的参数 `size_t` 中，第三个命令不接受参数。所有三个命令都使用 ‘E’ 组，并分配了唯一的命令号。

**清单 3：I/O 控制命令常量**

```c
#define     ECHODEV_GBUFSIZE   _IOR('E', 100, size_t)   /* 获取缓冲区大小 */
#define     ECHODEV_SBUFSIZE   _IOW('E', 101, size_t)   /* 设置缓冲区大小 */
#define     ECHODEV_CLEAR      _IO('E', 102)            /* 清除缓冲区 */
```

支持动态大小缓冲区需要对驱动程序进行一些更改。全局缓冲区被替换为指向动态分配缓冲区的全局指针，并且一个新的全局变量包含缓冲区的当前大小。指针和长度在模块加载时初始化，并在模块卸载时释放当前的缓冲区。由于缓冲区的大小不再是常量，因此现在必须在持有锁的情况下进行越界读取和写入的检查。

FreeBSD 内核中的 [**malloc(9)**](https://man.freebsd.org/malloc/9) 分配器要求在分配和释放例程中都提供额外的 malloc 类型参数。Malloc 类型跟踪分配请求，并提供细粒度的统计信息。这些统计信息可以通过 [**vmstat(8)**](https://man.freebsd.org/vmstat/8) 命令的标志 `-m` 查看，命令会为每种类型显示一行。内核确实包括一个通用的设备缓冲区 malloc 类型（`M_DEVBUF`），驱动程序可以使用它。然而，最佳实践是让驱动程序定义一个专用的 malloc 类型。对于内核模块中的驱动程序尤其如此。当模块卸载时，内核模块中定义的 malloc 类型会被销毁。如果仍然有分配引用这些 malloc 类型，内核将发出关于泄漏分配的警告。更细粒度的统计信息对于调试和性能分析也很有用。新的 malloc 类型通过宏 `MALLOC_DEFINE` 定义。第一个参数提供新类型的变量名。按惯例，类型名使用全大写，并以 **M\_** 作为前缀。对于此驱动程序，我们将使用名称 `M_ECHODEV`。第二个参数是一个短字符串名称，工具（如 **vmstat(8)**）将显示该名称。最佳实践是避免在短名称中使用空格字符。第三个参数是对该类型的字符串描述。

驱动程序对自定义控制命令的支持在清单 4 的新函数中实现。参数 `cmd` 包含请求的命令常量，参数 `data` 指向包含可选命令参数的内核缓冲区。函数的整体结构是一个基于参数 `cmd` 的 switch 语句。对于未知命令，默认的错误值是 `ENOTTY`，即使对于非 TTY 设备也是如此。接受大小参数的两个命令在解引用之前将 `data` 强制转换为正确的指针类型。`ECHODEV_GBUFSIZE` 命令将当前大小写入 `*data`，而 `ECHODEV_SBUFSIZE` 命令从 `*data` 中读取所需的新大小。

对于改变设备状态的命令，驱动程序要求文件描述符具有写权限（即使用 `O_RDWR` 或 `O_WRONLY` 打开的文件描述符）。为此，`ECHODEV_SBUFSIZE` 和 `ECHODEV_CLEAR` 命令要求 `fflag` 中设置标志 `FWRITE`。参数 `fflag` 包含在 `<sys/fcntl.h>` 中定义的文件描述符状态标志。这些标志将 `O_RDONLY`、`O_WRONLY` 和 `O_RDWR` 映射为标志 `FREAD` 和 `FWRITE` 的组合。**open(2)** 中的所有其他标志直接包含在文件描述符状态标志中。请注意，可以通过 [**fcntl(2)**](https://man.freebsd.org/fcntl/2) 在已打开的文件描述符上更改这些标志的子集。

**清单 4：I/O 控制处理程序**

```c
static int
echo_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
    struct thread *td)
{
      int error;

      switch (cmd) {
      case ECHODEV_GBUFSIZE:
            sx_slock(&echolock);
           *(size_t *)data = echolen;
            sx_sunlock(&echolock);
            error = 0;
            break;
      case ECHODEV_SBUFSIZE:
      {
            size_t new_len;

            if ((fflag & FWRITE) == 0) {
                  error = EPERM;
                  break;
            }

            new_len = *(size_t *)data;
            sx_xlock(&echolock);
            if (new_len == echolen) {
                  /* 无需操作 */
            } else if (new_len < echolen) {
                  echolen = new_len;
            } else {
                  echobuf = reallocf(echobuf, new_len, M_ECHODEV,
                      M_WAITOK | M_ZERO);
                  echolen = new_len;
            }
            sx_xunlock(&echolock);
            error = 0;
            break;
      }
      case ECHODEV_CLEAR:
            if ((fflag & FWRITE) == 0) {
                  error = EPERM;
                  break;
            }

            sx_xlock(&echolock);
            memset(echobuf, 0, echolen);
            sx_xunlock(&echolock);
            error = 0;
            break;
      default:
            error = ENOTTY;
            break;
      }
      return (error);
```

为了从用户空间调用这些命令，我们需要一个新的用户应用程序。该代码库包含示例 3 中使用的 `echoctl` 程序。`size` 命令输出当前缓冲区的大小，`resize` 命令设置新的缓冲区大小，`clear` 命令清除缓冲区内容。请注意，在这个示例中，`jot` 的输出不再被截断。该示例中的最后一条命令显示了使用 `M_ECHODEV` 分配的驱动程序的动态分配统计信息。

**示例 3：调整全局缓冲区大小**

```sh
# echoctl size
64
# echo foo > /dev/echo
# echoctl clear
# cat /dev/echo
# echoctl resize 80
# jot -c -s "" 70 48 > /dev/echo
# cat /dev/echo
0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstu
# vmstat -m | egrep 'Type|echo'
           Type Use Memory Req Size(s)
        echodev    1   128    2 64,128
```

## 每实例数据

到目前为止，我们的设备驱动程序使用全局变量来保存其状态。对于简单的演示驱动程序，且只有一个设备实例的情况，这样是可以的。然而，大多数字符设备都是硬件设备驱动程序的一部分，并且需要在单个系统中支持多个设备实例。为了支持这一点，驱动程序定义一个包含单个设备实例软件上下文的结构体。在 BSD 内核中，这个软件上下文被称为“softc”。驱动程序通常定义一个结构类型，其名称以 **\_softc** 作为后缀，而指向 softc 结构体的变量通常命名为“sc”。

字符设备提供了对每实例数据的直接支持。`struct cdev` 包含三个成员，可以用来存储驱动程序特定的数据。`si_drv0` 存储一个整数值，而 `si_drv1` 和 `si_drv2` 存储任意指针。设备驱动程序可以在创建字符设备时，通过 `struct make_dev_args` 结构体中的 `mda_unit`、`mda_si_drv1` 和 `mda_si_drv2` 字段来设置这些变量。然后，这些值可以作为 `struct cdev` 参数中的成员，供字符设备 switch 方法访问。历史上，设备驱动程序使用单元号来跟踪每实例数据。现代 FreeBSD 设备驱动程序将一个 softc 指针存储在 `si_drv1` 字段中，并且很少使用其他两个字段。

对于我们的 echo 设备驱动程序，我们定义了一个 `struct echodev_softc` 类型，包含 echo 设备实例所需的所有状态。设备驱动程序仍然存储一个全局变量，用于在模块加载和卸载时保存单个实例的 softc，但驱动程序的其余部分通过 softc 指针访问状态。这些更改不会改变驱动程序的任何功能，但确实需要重构驱动程序的各个部分。清单 5 显示了新的 softc 结构体类型。清单 6 通过展示更新后的读取方法，演示了每个字符设备 switch 方法所需的重构方式。最后，清单 7 显示了模块加载和卸载时使用的更新例程。

**清单 5：softc 结构体**

```c
struct echodev_softc {
      struct cdev *dev;
      char *buf;
      size_t len;
      struct sx lock;
};
```

**清单 6：使用 softc 结构体的驱动程序方法**

```c
static int
echo_read(struct cdev *dev, struct uio *uio, int ioflag)
{
      struct echodev_softc *sc = dev->si_drv1;
      size_t todo;
      int error;

      sx_slock(&sc->lock);
      if (uio->uio_offset >= sc->len) {
            error = 0;
      } else {
            todo = MIN(uio->uio_resid, sc->len - uio->uio_offset);
error = uiomove(sc->buf + uio->uio_offset, todo, uio);
      }
      sx_sunlock(&sc->lock);
      return (error);
}
```

**清单 7：使用 softc 结构体的模块加载和卸载**

```c
static int
echodev_create(struct echodev_softc **scp, size_t len)
{
      struct make_dev_args args;
      struct echodev_softc *sc;
      int error;

      sc = malloc(sizeof(*sc), M_ECHODEV, M_WAITOK | M_ZERO);
      sx_init(&sc->lock, "echo");
      sc->buf = malloc(len, M_ECHODEV, M_WAITOK | M_ZERO);
      sc->len = len;
      make_dev_args_init(&args);
      args.mda_flags = MAKEDEV_WAITOK | MAKEDEV_CHECKNAME;
      args.mda_devsw = &echo_cdevsw;
      args.mda_uid = UID_ROOT;
      args.mda_gid = GID_WHEEL;
      args.mda_mode = 0600;
      args.mda_si_drv1 = sc;
      error = make_dev_s(&args, &sc->dev, "echo");
      if (error != 0) {
            free(sc->buf, M_ECHODEV);
            sx_destroy(&sc->lock);
            free(sc, M_ECHODEV);
      }
      return (error);
}

static void
echodev_destroy(struct echodev_softc *sc)
{
      if (sc->dev != NULL)
            destroy_dev(sc->dev);
      free(sc->buf, M_ECHODEV);
      sx_destroy(&sc->lock);
      free(sc, M_ECHODEV);
}

static int
echodev_modevent(module_t mod, int type, void *data)
{
      static struct echodev_softc *echo_softc;

      switch (type) {
      case MOD_LOAD:
            return (echodev_create(&echo_softc, 64));
      case MOD_UNLOAD:
            if (echo_softc != NULL)
                   echodev_destroy(echo_softc);
            return (0);
      default:
            return (EOPNOTSUPP);
      }
}
```

## 结论

感谢你阅读到这里。本系列的下一篇文章将扩展这个驱动程序，实现一个 FIFO 缓冲区，包括对非阻塞 I/O 和通过 [**poll(2)**](https://man.freebsd.org/poll/2) 和 [**kevent(2)**](https://man.freebsd.org/kevent/2) 的 I/O 事件报告的支持。

***

**John Baldwin** 是一位系统软件开发人员。二十多年来，他一直直接向 FreeBSD 操作系统提交更改，涉及内核的多个部分（包括 x86 平台支持、SMP、各种设备驱动程序和虚拟内存子系统）以及用户空间程序。除了编写代码，John 还曾在 FreeBSD 核心团队和发布工程团队中担任职务。他还为 GDB 调试器做出了贡献。John 和妻子 Kimberly 及三个孩子（Janelle、Evan 和 Bella）一起住在弗吉尼亚州的 Ashland。


# VPP 移植到了 FreeBSD：基础用法

* 原文链接：[Porting VPP to FreeBSD: Basic Usage](https://freebsdfoundation.org/our-work/journal/browser-based-edition/kernel-development/porting-vpp-to-freebsd-basic-usage/)
* 作者：Tom Jones

**Vector Packet Process (VPP)** 是高性能框架，用于在用户空间处理数据包。得益于 FreeBSD 基金会和 RGNets 的项目，我获得资助将 VPP 移植到 FreeBSD，非常高兴与《FreeBSD 期刊》的读者分享一些基本用法。

VPP 使得转发和路由应用程序可以在用户空间编写，并提供 API 可控的接口。通过 DPDK（在 Linux 上）和 DPDK 及 netmap（在 FreeBSD 上），VPP 实现了高性能的网络处理。这些 API 允许直接零拷贝访问数据，并可用于创建可以显著超过主机转发性能的转发应用程序。

VPP 是完整的网络路由器替代方案，因此需要一些主机配置才能使用。本文展示了如何在 FreeBSD 上使用 VPP 的完整示例，大多数用户应该能够通过自己的虚拟机来跟随这些步骤。VPP 在 FreeBSD 上也可以运行在真实硬件上。

本文将介绍如何在 FreeBSD 上使用 VPP，并给出设置示例，展示如何在 FreeBSD 上完成各种任务。VPP 的资源可能较难找到，项目在 [https://fd.io](https://fd.io/) 的文档质量很高。

## 构建一个路由器

![FreeBSD 上 VPP 基本用法示意图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-47c33c7212ae4912f3a06af7172749fb3c752546%2Fvpp-1.png?alt=media)

VPP 可以用于许多用途，其中最常见且最容易配置的是作为某种形式的路由器或桥接器。以将 VPP 用作路由器为例，我们需要构建包含三个节点的小型网络——客户端、服务器和路由器。

为了展示如何在 FreeBSD 上使用 VPP，我将构建最小开销的示例网络。你只需要 VPP 和 FreeBSD 系统。我还将安装 iperf3，以便我们能够生成并观察通过我们的路由器传输的一些流量。

在具有最新 Ports 的 FreeBSD 系统中，可以使用 pkg 命令安装我们所需的两个工具，如下所示：

```sh
host # pkg install vpp iperf3
```

要为我们的网络创建三个节点，我们将利用 FreeBSD 最强大的功能之一——VNET jail。VNET jail 为我们提供了完全隔离的网络堆栈实例，它们的操作方式类似于 Linux 的网络命名空间（Network Namespaces）。要创建 VNET，我们需要在创建 jail 时添加选项 `vnet`，并传递它将使用的接口。

最后，我们将使用 `epair` 接口连接我们的节点。`epair` 接口提供了以太网电缆两端的功能——如果你熟悉 Linux 上的 `veth` 接口，它们提供类似功能。

我们可以通过以下 5 个命令构建我们的测试网络：

```sh
host # ifconfig epair create
epair0a
host # ifconfig epair create
epair1a
# jail -c name=router persist vnet vnet.interface=epair0a vnet.interface=epair1a
# jail -c name=client persist vnet vnet.interface=epair0b
# jail -c name=server persist vnet vnet.interface=epair1b
```

在这些 jail 命令中需要注意的标志是 `persist`，如果没有这个选项，jail 将会因为没有运行进程而自动删除；`vnet` 使这个 jail 成为 VNET jail；和 `vnet.interface=`，它将指定的接口分配给 jail。

当接口被移到新的 VNET 时，它的所有配置都会被清除——这是需要注意的，因为如果你配置了接口然后将其移到 jail，可能会困惑于为什么无法工作。

## 设置对等端

在转到 VPP 之前，我们先设置网络的客户端和服务器端。每一方都需要分配 IP 地址，并将接口设置为启用状态。我们还需要为客户端和服务器 jail 配置默认路由。

```sh
host # jexec client
# ifconfig
lo0: flags=8008<LOOPBACK,MULTICAST> metric 0 mtu 16384
options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
        groups: lo
        nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
epair0b: flags=1008842<BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP> metric 0
mtu 1500
        options=8<VLAN_MTU>
        ether 02:90:ed:bd:8b:0b
        groups: epair
        media: Ethernet 10Gbase-T (10Gbase-T <full-duplex>)
        status: active
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
# ifconfig epair0b inet 10.1.0.2/24 up
# route add default 10.1.0.1
add net default: gateway 10.1.0.1
```

```sh
host # jexec server
# ifconfig epair1b inet 10.2.0.2/24 up
# route add default 10.2.0.1
add net default: gateway 10.2.0.1
```

我们的客户端和服务器 jail 现在有了 IP 地址，并且配置了通向 VPP 路由器的路由。

## Netmap 要求

在我们的示例中，我们将使用 VPP 与 Netmap，Netmap 是高性能的用户空间网络框架，作为 FreeBSD 的默认组件提供。使用 Netmap 之前，接口需要一些配置——接口需要处于启用状态，并且需要配置选项 `promisc`。

```sh
host # jexec router
# ifconfig epair0a promisc up
# ifconfig epair1a promisc up
```

现在我们可以开始使用 VPP 了！

## VPP 的基本命令

VPP 非常灵活，支持通过配置文件、命令行接口和具备成熟 Python 绑定的 API 来配置。VPP 需要基础配置，告诉它从哪里获取命令，和它使用的控制文件的名称（如果这些文件不是默认的）。我们可以在命令行中将最小配置文件作为 VPP 的参数。对于这个示例，我们让 VPP 进入交互模式——提供给我们 CLI，并告诉 VPP 只加载我们将使用的插件（netmap），这是合理的默认设置。

如果我们不禁用所有插件，我们将需要配置机器使用 DPDK，或者单独禁用该插件。禁用插件的语法与启用 netmap 插件的语法相同。

```sh
host # vpp "unix { interactive} plugins { plugin default { disable } plugin
netmap_plugin.so { enable } plugin ping_plugin.so { enable } }"
      _______     _       _   _____  ___
    __/ __/ _ \  (_)__   | | / / _ \/ _ \
    _/ _// // / / / _ \  | |/ / ___/ ___/
    /_/ /____(_)_/\___/  |___/_/  /_/

vpp# show int
         Name            Idx        State  MTU (L3/IP4/IP6/MPLS)
Counter      Count
local0                    0         down          0/0/0/0
```

如果一切设置正确，你将看到 VPP 的横幅和默认的 CLI 提示符（`vpp#`）。

VPP 命令行界面提供了很多选项，用于创建和管理接口、像桥接一样的组、添加路由和用于检查 VPP 实例性能的工具。

接口配置命令的语法类似于 Linux 的 iproute2 命令——对于来自 FreeBSD 的用户来说，这些命令可能稍显陌生，但一旦开始使用，它们还是相当清晰的。

我们的 VPP 服务器还没有配置任何主机接口，`show int` 只列出了默认的 `local0` 接口。

要在 VPP 中使用我们的 netmap 接口，我们需要先创建它们，然后再配置。

创建命令允许我们创建新的接口，我们使用 `netmap` 子命令和主机接口。

```sh
vpp# create netmap name epair0a
netmap_create_if:164: mem 0x882800000
netmap-epair0a
vpp# create netmap name epair1a
netmap-epair1a
```

每个 netmap 接口都以 `netmap-` 为前缀创建。接口创建完成后，我们可以配置它们以供使用，并开始将 VPP 用作路由器。

```sh
vpp# set int ip addr netmap-epair0a 10.1.0.1/24
vpp# set int ip addr netmap-epair1a 10.2.0.1/24
vpp# show int addr
local0 (dn):
netmap-epair0a (dn):
  L3 10.1.0.1/24
netmap-epair1a (dn):
  L3 10.2.0.1/24
```

命令 `show int addr`（`show interface address` 的简写）确认了我们的 IP 地址分配成功。然后，我们可以将接口启用：

```sh
vpp# set int state netmap-epair0a up
vpp# set int state netmap-epair1a up
vpp# show int
          Name              Idx State  MTU (L3/IP4/IP6/MPLS)
Counter   Count
local0                         0  down         0/0/0/0
netmap-epair0a                 1   up          9000/0/0/0
netmap-epair1a                 2   up          9000/0/0/0
```

配置好接口后，我们可以使用 ping 命令测试 VPP 的功能：

```sh
vpp# ping 10.1.0.2
116 bytes from 10.1.0.2: icmp_seq=2 ttl=64 time=7.9886 ms
116 bytes from 10.1.0.2: icmp_seq=3 ttl=64 time=10.9956 ms
116 bytes from 10.1.0.2: icmp_seq=4 ttl=64 time=2.6855 ms
116 bytes from 10.1.0.2: icmp_seq=5 ttl=64 time=7.6332 ms

Statistics: 5 sent, 4 received, 20% packet loss
vpp# ping 10.2.0.2
116 bytes from 10.2.0.2: icmp_seq=2 ttl=64 time=5.3665 ms
116 bytes from 10.2.0.2: icmp_seq=3 ttl=64 time=8.6759 ms
116 bytes from 10.2.0.2: icmp_seq=4 ttl=64 time=11.3806 ms
116 bytes from 10.2.0.2: icmp_seq=5 ttl=64 time=1.5466 ms

Statistics: 5 sent, 4 received, 20% packet loss
```

如果我们跳到客户端 jail，我们可以验证 VPP 正在充当路由器：

```sh
client # ping 10.2.0.2
PING 10.2.0.2 (10.2.0.2): 56 data bytes
64 bytes from 10.2.0.2: icmp_seq=0 ttl=63 time=0.445 ms
64 bytes from 10.2.0.2: icmp_seq=1 ttl=63 time=0.457 ms
64 bytes from 10.2.0.2: icmp_seq=2 ttl=63 time=0.905 ms
^C
--- 10.2.0.2 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.445/0.602/0.905/0.214 ms
```

作为初始设置的最后一步，我们将在服务器 jail 中启动 iperf3 服务器，并使用客户端做 TCP 吞吐量测试。

```sh
server # iperf3 -s

client # iperf3 -c 10.2.0.2
Connecting to host 10.2.0.2, port 5201
[ 5] local 10.1.0.2 port 63847 connected to 10.2.0.2 port 5201
[ ID]   Interval         Transfer    Bitrate         Retr  Cwnd
[  5]   0.00-1.01   sec   341 MBytes  2.84 Gbits/sec  0     1001 KBytes
[  5]   1.01-2.01   sec   488 MBytes  4.07 Gbits/sec  0     1.02 MBytes
[  5]   2.01-3.01   sec   466 MBytes  3.94 Gbits/sec  144   612 KBytes
[  5]   3.01-4.07   sec   475 MBytes  3.76 Gbits/sec  0     829 KBytes
[  5]   4.07-5.06   sec   452 MBytes  3.81 Gbits/sec  0     911 KBytes
[  5]   5.06-6.03   sec   456 MBytes  3.96 Gbits/sec  0     911 KBytes
[  5]   6.03-7.01   sec   415 MBytes  3.54 Gbits/sec  0     911 KBytes
[  5]   7.01-8.07   sec   239 MBytes  1.89 Gbits/sec  201   259 KBytes
[  5]   8.07-9.07   sec   326 MBytes  2.75 Gbits/sec  0     462 KBytes
[  5]   9.07-10.06  sec   417 MBytes  3.51 Gbits/sec  0     667 KBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID]   Interval         Transfer     Bitrate         Retr
[  5]   0.00-10.06  sec  3.98 GBytes  3.40 Gbits/sec  345          sender
[  5]   0.00-10.06  sec  3.98 GBytes  3.40 Gbits/sec               receiver

iperf Done.
```

## VPP 分析

现在通过 VPP 发送了一些流量后，`show int` 命令的输出包含了更多信息：

```sh
vpp# show int
     Name               Idx   State  MTU (L3/IP4/IP6/MPLS)             Counter           Count
local0                   0    down        0/0/0/0
netmap-epair0a           1    up          9000/0/0/0       rx packets            4006606
                                                           rx bytes           6065742126
                                                           tx packets            2004365
                                                           tx bytes            132304811
                                                           drops                       2
                                                           ip4                   4006605
netmap-epair1a           2    up          9000/0/0/0       rx packets            2004365
                                                           rx bytes            132304811
                                                           tx packets            4006606
                                                           tx bytes           6065742126
                                                           drops                       2
                                                           ip4                   2004364
```

接口命令现在给出了通过 VPP 接口传输的字节和数据包的摘要。这在调试流量如何流动时非常有用，尤其是当数据包丢失时。

VPP 中的 V 代表向量（vector），这在项目中有两层含义。VPP 旨在使用向量化指令加速数据包处理，同时它还将一组数据包打包成向量，以优化处理。其理论是将一组数据包一起通过处理图，从而减少缓存争用并提供最佳性能。

VPP 提供了许多工具来查询数据包处理过程中的情况。深度调优超出了本文的讨论范围，但了解 VPP 内部发生情况的初步工具是运行时命令。

运行时数据会在每个向量通过 VPP 处理图时收集，记录每个节点的遍历时间和处理的向量数量。

要使用运行时工具，最好有一些流量。可以像这样启动长时间运行的 iperf3 吞吐量测试：

```sh
client # iperf3 -c 10.2.0.2 -t 1000
```

现在在 VPP jail 中，我们可以清除迄今为止收集到的运行时统计信息，稍等片刻，然后查看我们的运行情况：

```sh
vpp# clear runtime
... wait ~5 seconds ...
vpp# show runtime
Time 5.1, 10 sec internal node vector rate 124.30 loops/sec 108211.07
  vector rates in 4.4385e5, out 4.4385e5, drop 0.0000e0, punt 0.0000e0
          Name                  State         Calls       Vectors     Suspends   Clocks    Vectors/Call
ethernet-input                  active        18478       2265684     0          3.03e1    122.62
fib-walk                        any wait          0       0           3          1.14e4    0.00
ip4-full-reassembly-expire-wal  any wait          0       0           102        7.63e3    0.00
ip4-input                       active        18478       2265684     0          3.07e1    122.62
ip4-lookup                      active        18478       2265 684    0          3.22e1    122.62
ip4-rewrite                     active        18478       2265684     0          3.05e1    122.62
ip6-full-reassembly-expire-wal  any wait          0       0           102        5.79e3    0.00
ip6-mld-process                 any wait          0       0           5          6.12e3    0.00
ip6-ra-process                  any wait          0       0           5          1.18e4    0.00
netmap-epair0a-output           active         8383       755477      0          1.12e1    90.12
netmap-epair0a-tx               active         8383       755477      0          1.17e3    90.12
netmap-epair1a-output           active        12473       1510207     0          1.04e1    121.08
netmap-epair1a-tx               active        12473       1510207     0          2.11e3    121.08
netmap-input                    interrupt wa  16698       2265684     0          4.75e2    135.69
unix-cli-process-0              active            0       0           13         7.34e4    0.00
unix-epoll-input                polling      478752       0           0          2.98e4    0.00
```

`show runtime` 输出中的列为我们提供了关于 VPP 内部运行的很好的了解。它们告诉我们自从运行时计数器被清除以来，哪些节点被激活，它们的当前状态，每个节点被调用的次数，所使用的时间，和每次调用处理的向量数量。默认情况下，VPP 的最大向量大小是 255。

最终的调试任务是使用 `show vlib graph` 命令检查整个数据包处理图。此命令显示每个节点和可能导向它的潜在父节点、子节点。

## 下一步

VPP 是一款令人印象深刻的软件——解决了兼容性问题后，VPP 的核心部分就相对容易移植。即使仅做最小调优，VPP 在 FreeBSD 上与 netmap 配合使用时也能够达到相当出色的性能，而如果配置了 DPDK，它的表现会更好。VPP 的文档正在逐步增加关于在 FreeBSD 上运行的信息，但开发者确实需要更多 FreeBSD 上 VPP 的使用案例。

从这个简单网络的例子开始，将其移植到拥有更快接口的大型网络上应该是相对简单的。

***

**Tom Jones** 是 FreeBSD 提交者，致力于保持网络堆栈的高效运行。

![VPP 文章配图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-58a31b6310fa9a9ac971e9c84b6d850a92c22923%2Fvpp-graphic.png?alt=media)


# 利用 Kyua 的 Jail 功能提升 FreeBSD 测试套件的并行效率

* 原文地址：[Enhancing FreeBSD Test Suite Parallelism with Kyua’s Jail Feature](https://freebsdfoundation.org/our-work/journal/browser-based-edition/kernel-development/enhancing-freebsd-test-suite-parallelism-with-kyuas-jail-feature/)
* 作者：Igor Ostapenko

**测试是一个相当广泛的概念** 如今，无论采用何种方法、学术观点或特定情境，很难抗拒这样一个简单愿望：不要让最终用户代替我们测试。即使是一个简单的错误，也可能在多个方面对业务造成重大损害：声誉、上市时间、转化率、业务扩展等。行业已经从依赖发布前的复杂手动检查，演变为尽可能拥抱自动化。自动化测试提供了许多优势：更短的周期、更频繁的反馈、对结果的额外信心、减少对变更的恐惧等。尽管自动化测试是开发流程中的重要组成部分，但它仅是改善软件交付的众多实践之一。

## 使用 Kyua 组织测试

FreeBSD 测试套件通常位于 **/usr/tests**，其基础设施基于 Julio Merino 创建的测试框架 Kyua。Kyua 提供了一种富有表现力的测试套件定义语言、安全的运行时引擎和强大的报告生成能力。测试可以使用任意工具编写，也可以不依赖特定的库。测试套件中的单元和集成测试通常使用诸如 [**atf-c(3)**](https://man.freebsd.org/cgi/man.cgi?query=atf-c\&sektion=3)、[**atf-c++(3)**](https://man.freebsd.org/cgi/man.cgi?query=atf-c%2B%2B\&sektion=3)、[**atf-sh(3)**](https://man.freebsd.org/cgi/man.cgi?query=atf-sh\&sektion=3) 或 [pytest](https://www.freshports.org/devel/py-pytest/) 等库。

Kyua 的核心概念层次结构如下：测试套件 > 测试程序 > 测试用例。

一个测试套件将多个二进制文件（测试程序）分组为一个具有单一名称的集合。测试套件通过 Lua 脚本描述，通常保存为特殊的 [**kyuafile(5)**](https://man.freebsd.org/cgi/man.cgi?query=kyuafile\&sektion=5)。来看以下已存在的文件示例：

```sh
# cat -n /usr/tests/sys/kern/Kyuafile
       1  -- Automatically generated by bsd.test.mk.
       2
       3  syntax(2)
       4
       5  test_suite("FreeBSD")
       6
       7  atf_test_program{name="basic_signal", }
[skipped]
       34  atf_test_program{name="sonewconn_overflow", required_programs="python", required_user="root", is_exclusive="true"}
       35  atf_test_program{name="subr_physmem_test", }
       36  plain_test_program{name="subr_unit_test", }
[skipped]
       45  atf_test_program{name="unix_seqpacket_test", timeout="15"}
       46  atf_test_program{name="unix_stream", }
       47  atf_test_program{name="waitpid_nohang", }
       48  include("acct/Kyuafile")
       49  include("execve/Kyuafile")
       50  include("pipe/Kyuafile")
```

第 3 行指定了所使用语法的必要版本。第 5 行为测试套件设置了名称。通常，所有位于 **/usr/tests/\*\*/Kyuafile** 的描述都会被收集到一个名为 FreeBSD 的测试套件中。如果某个二进制文件基于 ATF 库，它会通过 `atf_test_program` 注册，从而使 Kyua 能够利用 ATF 提供的功能和特性。如果测试程序不基于 Kyua 支持的库，而仅通过退出码传递结果，则会使用 `plain_test_program` 构造。此外，还有 `tap_test_program`，用于那些通过经典的 [Test Anything Protocol](https://en.wikipedia.org/wiki/Test_Anything_Protocol)（TAP）协议传递结果的测试程序。

每个 Kyuafile 只描述其所在目录中的测试二进制文件。然而，**/usr/tests** 的结构设计方式使得每个测试目录都会显式包含其子目录中的测试文件，如第 48、49 和 50 行所示。因此，在该目录运行测试时，将会执行 `sys/kern` 子树中的所有测试，包括 `sys/kern/acct`、`sys/kern/execve` 和 `sys/kern/pipe` 中的测试：

```sh
# kyua test -k /usr/tests/sys/kern/Kyuafile
```

第 1 行表明，FreeBSD 测试套件中的 Kyuafile 并不是手动创建的。相反，与 FreeBSD 构建系统的大多数组件一样，该过程通过 Makefile 自动处理，Makefile 构建测试程序并生成相应的 Kyuafile。要详细了解该过程，可以将生成的 **/usr/tests/sys/kern/Kyuafile** 与其源文件 **/usr/src/tests/sys/kern/Makefile** 比较——这是直观的方法。

未在 Kyuafile 中注册的测试程序将不会被 Kyua 识别，因此也不会被执行。

Kyuafile 中未显式提及测试用例（test cases），因为测试用例是在更低的层级（即测试程序内部）定义的，并需要所用库的支持。通常，将多个相似的测试（即测试用例）分组到单个测试程序的二进制文件中会比创建多个独立的测试程序更为方便。对于普通测试程序，通常仅提供一个测试用例，通常根据 `main()` 函数命名为“main”。相比之下，基于 ATF 库的测试可以报告多个测试用例。在 Kyua 中，我们通常称为“测试”的内容被称为“测试用例”，被视为执行的基本单元。因此，尽管 Kyuafile 中描述的测试套件可能看起来只引用了一个测试程序，但它可能包含数十个或更多的测试用例。`kyua list` 命令会以 `<测试程序>:<测试用例>` 的格式列出测试用例，这种格式也可以用于其他命令，例如单独运行某个测试用例：

```sh
# cd /usr/tests/sys/kern
# kyua test unix_dgram:basic
```

每个测试用例都可以包含可选的元数据属性，以键/值对的形式存在，用于修改 Kyua 针对该特定测试用例的行为。上面的 Kyuafile 示例展示了如何将相同的元数据应用于测试程序中的所有测试用例。示例中包含的属性如下：

* **`timeout`**：允许更改默认超时时间（默认值为 300 秒）。
* **`required_programs`**：如果所指定的二进制文件无论通过完整路径还是在 `PATH` 环境变量中都未找到，该测试用例将被标记为跳过，并显示相应的消息。
* **`required_user="root"`**：如果 Kyua 未以 root 权限运行，则跳过该测试；而 **`required_user="unprivileged"`** 则确保测试在无 root 访问权限的情况下运行。
* **`is_exclusive="true"`**：指定该测试不能与其他测试同时运行。

## 并行性和 Jail

Kyua 可以通过配置实现测试用例的并行运行。默认情况下，`parallelism` 设置为 1，这意味着测试是按顺序运行的。该设置可以在 [**kyua.conf(5)**](https://man.freebsd.org/cgi/man.cgi?query=kyua.conf\&sektion=5) 文件中调整，或作为选项指定：

```sh
# kyua -v parallelism=8 test
```

测试用例需要独占访问共享资源时，应标记为 `is_exclusive="true"`，以便 Kyua 知道不与其他测试并行运行。Kyua 的操作分为两个阶段。第一阶段运行所有非独占的测试用例，如果配置了并行性设置，这些测试可以并行执行。第二阶段顺序运行所有独占的测试用例。为了保持测试套件的高效性，最好避免添加新的独占测试，并尽量创建非独占的替代版本，否则测试套件可能会耗费过多时间来执行。

然而，有些测试利用 [**jail(8)**](https://man.freebsd.org/cgi/man.cgi?query=jail\&sektion=8) 功能来处理其他方式难以复现的场景。例如，网络模块测试通常通过创建临时 jail，利用主机通过 [**epair(4)**](https://man.freebsd.org/cgi/man.cgi?query=epair\&sektion=4) 验证模块行为。这类测试必须标记为独占有以下几个原因：为了方便通常会重用相同的 jail 名称（但系统中的每个 jail 必须有唯一名称），主机端使用为演示用途分配的相同 IP 地址配置接口，可能导致共享路由表的冲突，和其他相关问题。虽然这些问题可以由测试用例自身解决，但这样会显著增加测试编写者和维护者的复杂度，并且某些问题可能在没有外部干预的情况下无法解决。这时，最新版本的 Kyua 派上用场了。

## 执行环境概念

在 15-CURRENT 版本中，Kyua 引入了“执行环境”这一新概念。该功能将在 14.2-RELEASE 中提供。

默认情况下，测试仍然按照之前的方式运行，即通过生成子进程——这种方式称为主机执行环境。测试用例可以通过指定新的元数据属性 `execenv`，选择使用不同的执行环境。针对每个测试用例的一般步骤顺序已扩展，包括以下内容：

1. 初始化执行环境
2. 执行测试
3. （可选）测试清理
4. 清理执行环境

目前，Kyua 仅支持一种额外的执行环境——jail 环境。虽然可以为单个测试用例配置该环境，但以下示例展示了如何将 `execenv` 元数据属性应用于测试程序中的所有测试用例：

```ini
atf_test_program{name="test_program", execenv="jail"}
```

此配置使 Kyua 为 `test_program` 中的每个测试用例提供临时 jail 来执行。如果某个测试用例声明了清理例程，该例程也将在相同的 jail 中执行。Kyua 使用 [**jail(8)**](https://man.freebsd.org/cgi/man.cgi?query=jail\&sektion=8) 创建这些 jail，测试用例可以通过名为 `execenv_jail_params` 的新元数据属性传递额外的参数：

```ini
atf_test_program{name="test_program", execenv="jail", execenv_jail_params="vnet allow.raw_sockets"}
```

只要不同父 jail 中的子 jail 名称不冲突，并且每个 jail 都能拥有自己的 VNET 堆栈，我们就可以轻松将测试（例如前面提到的网络测试）隔离到独立的 jail 中运行，并通过移除标志 `is_exclusive` 实现并行运行。具体效果取决于环境和配置，但有报告显示，在相同环境下，`netpfil/pf` 测试套件的运行速度提高了 4 至 5 倍，仅需几分钟就能完成，而非原来的半小时。

## 隐式参数与分层 Jail

由于测试用例和它的可选清理例程分别在独立的子进程中运行，Kyua 会隐式添加参数 `persist` 以保持临时 jail 存在，确保两个子进程都在同一个 jail 中运行。Kyua 会在“执行环境清理”步骤中删除临时 jail。

网络测试中常见的做法是生成 jail。这就引出了一个问题：是否允许已经运行在 jail 内的测试用例创建子 jail。从原则上讲，只要不超过系统限制，这是允许的。每个 jail 都有一个关于可创建子 jail 数量的限制。在 15-CURRENT 中引入了以下新的只读 sysctl 变量，用于提供这些信息：

```sh
# sysctl security.jail.children
security.jail.children.cur: 0
security.jail.children.max: 999999
```

显然，上述内容指的是层级中最高的 jail，称为 `prison0`。根据当前值和最大值，系统最多可以创建近百万个 jail。当使用 [**jail(8)**](https://man.freebsd.org/cgi/man.cgi?query=jail\&sektion=8) 创建新 jail 时，会应用以下默认配置：

```sh
# jail -c command=sysctl security.jail.children
security.jail.children.cur: 0
security.jail.children.max: 0
```

这表示不允许创建子 jail。显然，在这种条件下，尝试创建新 jail 的测试用例将会失败。为了解决这个问题，Kyua 通过添加另一个隐式参数来提供帮助，该参数允许最大数量的子 jail，这个数量是父 jail 最大限制减去 1。虽然可以通过测试用例中的 `execenv_jail_params` 元数据属性来配置这一点，但这似乎是一项繁琐且重复的工作。

以下公式阐明了 Kyua 如何创建临时 jails 和如何通过元数据属性修改这一过程：

```sh
jail -qc name=<name> children.max=<parent_max-1> <test case defined params> persist
```

临时 jail 的名称来源于测试程序路径和测试用例名称。例如，测试用例 **/usr/tests/sys/kern/unix\_dgram:basic** 将使用名为 `kyua_usr_tests_sys_kern_unix_dgram_basic` 的临时 jail。

## kldload 问题

由于除了 `prison0` 外的所有 jail 都没有加载内核模块的权限，这会在测试用例依赖 jail 执行环境时带来不便。

Kyua 的初衷是供开发人员和用户使用。这意味着系统管理员应能够在操作系统升级后运行测试套件，以确保一切正常工作。显然，这样的主机不是测试实验室，开发人员不能随意实验、破坏系统或引发故障。因此，测试应设计为避免干扰主机的正常操作，除非明确指示。这也是为什么 FreeBSD 测试套件有类似 `allow_sysctl_side_effects` 的配置变量来遵循这一方法。尽管该套件主要被视为开发工具，但许多现有的测试仍然遵循这一原则，通过检查所需的模块是否已加载，而不是隐式加载它。例如，如果防火墙的测试未被主机使用，却意外影响了主机流量甚至使其无法访问，系统管理员会不满。

因此，推荐的策略是在测试用例中使用 `kldstat -q -m <module-name>` 来检查所需模块是否存在，如果模块未找到，则跳过测试。FreeBSD CI 的配置确保在运行测试套件之前，所有必要的模块已加载，所需的软件包已安装。

## execenvs 和 **WITHOUT\_JAIL**

提供了新的引擎配置变量——`execenvs`。默认情况下，它设置为包含所有支持的执行环境的列表：

```sh
# kyua config
architecture = aarch64
execenvs = host jail
parallelism = 1
platform = arm64
unprivileged_user = tests
```

这个变量可以通过 [**kyua.conf(5)**](https://man.freebsd.org/cgi/man.cgi?query=kyua.conf\&sektion=5) 操作，也可以作为 [**kyua(1)**](https://man.freebsd.org/cgi/man.cgi?query=kyua\&sektion=1) 命令行工具的选项来指定。例如，以下命令将仅执行基于主机的测试，并跳过所有其他测试：

```sh
# kyua -v execenvs=host test
```

如果系统在构建时没有启用 jail 支持，则仅会提供默认的主机执行环境。因此，任何需要 jail 执行环境的测试都将被跳过。

## 入门示例

以下示例基于 [**atf-sh(3)**](https://man.freebsd.org/cgi/man.cgi?query=atf-sh\&sektion=3)，演示了如何在测试用例级别配置 jail 环境。它还提醒了 root 用户权限的重要性。

```sh
# cat /usr/src/tests/sys/kern/test_program.sh
atf_test_case "case1" "cleanup"
case1_head()
{
atf_set descr 'Test that X does Y'
       atf_set require.user root
       atf_set execenv jail
       atf_set execenv.jail.params vnet allow.raw_sockets
}
case1_body()
{
       if ! kldstat -q -m tesseract; then
               atf_skip "This test requires tesseract"
       fi

       # 测试代码……
}
case1_cleanup()
{
       # 清理代码……
}

atf_init_test_cases()
{
       atf_add_test_case "case1"
}
```

在 Makefile 中添加一行即可完成该测试程序的配置：

```c
# grep test_program /usr/src/tests/sys/kern/Makefile
ATF_TESTS_SH+= test_program
```

构建系统将会在脚本前添加 `#!/usr/libexec/atf-sh` 的 shebang 行，将脚本安装到 **/usr/tests/sys/kern/test\_program** 并去掉 `.sh` 后缀，同时在 `Kyuafile` 中注册：

```c
# grep test_program /usr/tests/sys/kern/Kyuafile
atf_test_program{name="test_program", }
```

在单个测试程序中包含多个测试用例可能会导致“不要重复你自己”（Don’t Repeat Yourself, DRY）的问题。为了解决这个问题，可以将通用的元数据提升到 Kyuafile 中的测试套件级别，从而使其适用于整个测试程序，而无需为每个测试用例重复设置。然而，个别测试用例仍然可以在必要时覆盖这些属性：

```sh
# cat /usr/src/tests/sys/kern/test_program2.sh
atf_test_case "case2"
case2_head()
{
       atf_set descr 'Test that A does B'
}
case2_body()
{...}

atf_test_case "case3"
case3_head()
{
       atf_set descr 'Test that Foo does Bar'
       atf_set execenv.jail.params vnet allow.raw_sockets
}
case3_body()
{...}

atf_init_test_cases()
{
       atf_add_test_case "case2"
       atf_add_test_case "case3"
}
```

现在，主要的配置是在测试程序级别提供的：

```c
# grep test_program2 /usr/src/tests/sys/kern/Makefile
ATF_TESTS_SH+= test_program2
TEST_METADATA.test_program2+= execenv="jail",execenv_jail_params="vnet"
```

因此，Kyua 将在不同级别定义的元数据合并为以下内容：

```c
# kyua list -k /usr/tests/sys/kern/Kyuafile -v test_program2
test_program2:case2 (FreeBSD)
    description = Test that A does B
    execenv = jail
    execenv_jail_params = vnet
test_program2:case3 (FreeBSD)
    description = Test that Foo does Bar
    execenv = jail
    execenv_jail_params = vnet allow.raw_sockets
```

需要注意的是，ATF 和 Kyua 之间元数据属性命名约定的关键区别——点（`execenv.jail.params`）与下划线（`execenv_jail_params`）。此外，名称本身可能会略有不同，可以对比 [**kyuafile(5)**](https://man.freebsd.org/cgi/man.cgi?query=kyuafile\&sektion=5) 和 [**atf-test-case(4)**](https://man.freebsd.org/cgi/man.cgi?query=atf-test-case\&sektion=4) 的手册页面。

要将现有的测试切换到 jail 执行环境，应该取反或移除 `is_exclusive="true"` 元数据属性。否则，该测试将无法从并行执行中受益。

## 进一步阅读

FreeBSD 测试套件的入口点在 [**tests(7)**](https://man.freebsd.org/cgi/man.cgi?query=tests\&sektion=7) 中有描述。对于测试作者，以下 wiki 页面是有价值的起点：<https://wiki.freebsd.org/TestSuite/DeveloperHowTo>。

官方的 [Kyua wiki](https://github.com/freebsd/kyua/wiki) 是了解历史背景、设计理念和功能概述的极好资源。有关执行环境的详细信息可以在 [**kyua.conf(5)**](https://man.freebsd.org/cgi/man.cgi?query=kyua.conf\&sektion=5) 和 [**kyuafile(5)**](https://man.freebsd.org/cgi/man.cgi?query=kyuafile\&sektion=5) 的手册页面中找到。

此外，审查现有基于 jail 的测试是如何编写和组织的也至关重要，以避免重复造轮子。位于 **/usr/src/tests/sys/netpfil/pf** 的 PF 测试套件是了解既定实践的重要来源。

虽然在现有代码中回溯添加测试可能是一项庞大的工作，但结合 bug 修复来添加测试，是提升 FreeBSD 测试套件乃至整个项目的值得做的机会。

***

**Igor Ostapenko** 是一位 FreeBSD 贡献者，拥有广泛的软件开发经验，涉及的领域包括操作导航设备的系统、企业优化业务流程的解决方案、逆向工程、B2B/B2C 创业公司等多个领域。


# FreeBSD 上的 Valgrind

* 原文链接：[Valgrind on FreeBSD](https://freebsdfoundation.org/our-work/journal/browser-based-edition/kernel-development/valgrind-on-freebsd/)
* 作者：Paul Floyd

![Valgrind 标志](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-b512333f737f6a40d0f2587fac848efe1518caa5%2FValgrind-1.png?alt=media)

我第一次使用 Valgrind 是在 2000 年代初期。在此之前，我在 Solaris/SPARC 上使用 Purify（现在是 [Unicom PurifyPlus](https://www.unicomsi.com/products/purifyplus/)）有相当多的经验。老实说，我对 [Valgrind](https://valgrind.org/) 并没有太深的印象。虽然它不需要特殊的构建过程，但它缺少与调试器交互的能力。

稍微转到 FreeBSD，我第一次安装的是 1995 年底的 2.1 版本。像 Valgrind 一样，最初我对它也没有太深的印象。至少它是我家用 PC 上的“一个 Unix”，如果我需要的话。我继续在 FreeBSD 上小试牛刀，偶尔安装新版本。我的主要家用系统一直是 OS/2，直到 90 年代末；之后是长期使用的 Solaris，直到 11.4 版本进入了“停滞期”。我在 2007 年还买了一台 MacBook，主要用于“桌面”工作——我并不认为在 macOS 上开发是一种令人愉快的体验。

我一直对质量抱有信念。在大学时，我上过一门关于产品质量的短期课程，这让我坚定了这一信念。后来，我读了一些 [W. Edwards Deming](https://en.wikipedia.org/wiki/W._Edwards_Deming) 的著作。虽然他的文字有些粗糙，但他的思想非常明确有力。我学的是电子学，日本公司在 20 世纪后半期通过质量流程获得的好处是显而易见的。我最终在电子仿真领域作为软件开发人员工作。不出所料，我继续使用像 Valgrind 这样的工具。

大约五年前，我决定是时候开始为开源社区做出一些贡献了。既然我已经是 Valgrind 的专家，并且已经稍微接触过它的源代码，这对我来说是合乎逻辑的项目。我犹豫了一下，是在 macOS 和 FreeBSD 的 Valgrind 移植之间做选择。两件事让我对 macOS 产生了抵触——频繁的操作系统和用户空间的重大更新，导致一切都无法工作，以及从 Apple 内部获得帮助的难度。虽然有 XNU 源代码和一些书籍，但除此之外，你就只能独自摸索了。最后，我选择了 FreeBSD。这也很适合我，因为我正在考虑摆脱 Solaris 的使用。Illumos 和 FreeBSD 之间有很多交叉合作，我认为这会让我更容易过渡。与此同时，macOS 仍然存在于 Valgrind 的官方代码库中，但自 2016 年 10.12 版本以来，它基本上无法使用了。

## Valgrind 的历史

Valgrind 现在已有超过 [20 年的历史](https://nnethercote.github.io/2022/07/27/twenty-years-of-valgrind.html)。它最初在 i386 Linux 上启动。随着时间的推移，添加了多个其他 CPU 架构（amd64、MIPS、ARM、PPC 和 s390x）和其他操作系统（macOS、Solaris，最近是 FreeBSD）。

这些工具在这 20 年里不断发展。从 2002 年初始版本开始，添加的工具包括：

**2002 memcheck**\
**2002 helgrind**\
**2002 cachegrind**\
**2004 massif**\
**2006 callgrind**\
**2008 drd**\
**2009 exp-bbv**\
**2018 DHAT**

此外，还有一些工具在树外维护（或不再维护）。

Valgrind 的开发由少数几个人完成，大约有二十位做出了重要贡献。一些公司也伸出了援手。RedHat/IBM 可能是贡献最多的公司。Sun 在 Solaris 积极开发期间也有贡献。Apple 也曾贡献过，直到他们突然变得反感 GPL。

## Valgrind 在 FreeBSD 上的历史

Valgrind 在 FreeBSD 上的历史相当长且充满波折。我不会提及每一个做出贡献的人（而且我甚至不确定是否有完整的名单，因为一些源代码仓库已经无法访问）。Doug Robson 在 2004 年做了大量的初期工作。接下来的接力者是 Stan Sedov，他在 2009 到 2011 年期间维护了这个 Port。在那时，曾有一段时间推动将 FreeBSD 的源代码提交到上游，但最终未能成功。上游的维护者对质量标准要求非常严格，FreeBSD 的 Port 虽然接近合格，但始终未能达到要求。其次，需要有人维护 Port，最好是 Valgrind 团队的成员。我不知道为什么一直没有。从 2021 年 4 月开始，我开始维护 [FreeBSD Port](https://github.com/paulfloyd/freebsd_valgrind)，并且已经有 4 年多时间拥有 Valgrind 的提交权限。现在，我是 Valgrind 的主要贡献者。

最近的一次重大变更是添加了对 aarch64 的支持。我在 2024 年 4 月为此 CPU 添加了 Port，及时赶上了 Valgrind 3.23 版本的发布。

## Valgrind 工具

在深入 Valgrind 的内部之前，我将简要概述这些工具。

### Memcheck

这是大多数人在提到 Valgrind 时会想到的工具。它是默认工具。Memcheck 的主要功能是验证内存读取是否来自已初始化的内存，并确保读取和写入操作在分配的堆内存块的边界内。缺失的部分是检查栈内存的边界——这需要使用插桩技术。

### DRD 和 Helgrind

这两个工具都是线程危险检测工具。它们会检测不同线程访问内存时没有使用某种锁机制的情况。它们还会警告 pthread 函数使用中的错误。两者的区别在于，Helgrind 会尝试为所有涉及的线程提供错误上下文，而 DRD 仅为一个线程提供详细信息。

### Callgrind 和 Cachegrind

这两个工具用于 CPU 性能分析。Callgrind 用于分析函数调用。Cachegrind 历史上用于使用基本缓存和分支预测模型分析 CPU 指令。这些模型从来都不是很准确，现在甚至显得非常不现实。此外，Valgrind 并不执行任何预测执行。基于这些原因，当前版本的 Valgrind 默认不再使用 Cachegrind 进行缓存仿真。一些人喜欢指令计数的精确性，但就我个人而言，我通常更喜欢像谷歌的 [perftools](https://github.com/gperftools/gperftools)（[port devel/google-perftools](https://www.freshports.org/devel/google-perftools/)）、Linux 的 perf 和 [gprofng](https://www.sourceware.org/binutils/docs/gprofng.html) 这样的采样分析工具，尤其是在处理大型问题时（运行时间长达数小时甚至数天，内存使用量达到数百 GB）。

### Massif 和 DHAT

这两个工具是内存分析工具。Massif 用于对内存使用情况进行时间序列分析。就我个人而言，我觉得它有些过于复杂。还有其他工具通常可以在没有 Valgrind 开销的情况下，提供同样优秀的分析结果——例如谷歌的 perftools 和 [HeapTrack](https://github.com/KDE/heaptrack)（[port devel/heaptrack](https://www.freshports.org/devel/heaptrack/)）。不过，还是有例外。如果你的应用程序大量使用基于 mmap 的自定义分配器，或者静态链接了 malloc 库，那么这些替代工具就无法工作。Massif 不需要替换共享库中的分配函数，它还可以选择在 mmap 级别对内存进行分析。DHAT 是 Valgrind 工具套件中的隐藏宝石。这个工具分析堆内存的访问情况。它提供的信息可以帮助你了解哪些内存被频繁使用，哪些内存长时间保持分配状态，哪些内存从未被使用。对于那些不太大的内存块，它还会为这些块生成访问直方图。从中，你可以看到结构体或类中的空洞或未使用的成员。你还可以推断访问模式，这可能有助于重新排序成员，使它们位于同一缓存行上。

## Valgrind 基础

### 无依赖

为了让 Valgrind 能够执行所有客户端代码（不仅仅是 `main()` 中的代码，而是程序启动时 ELF 文件中的第一条指令），并避免与诸如 stdio 缓冲区等产生冲突，Valgrind 不与 libc 或任何外部库链接。我有时开玩笑说，这不太像 C++，更像 C- -。这意味着 Valgrind 有自己的 libc 子集实现。为了使函数名不冲突，它使用宏作为伪命名空间。Valgrind 版本的 `printf` 是 `VG_(printf)`（对于代码导航来说非常有趣！）。这也意味着我们不能直接添加第三方库并使用它。这个库需要移植，以便使用 Valgrind 的 libc 子集。例如，目前有一个 bugzilla 项，计划添加对 zstd 压缩的 DWARF 段的支持。

### 多疑的编程

Valgrind 非常谨慎，广泛使用了在发布版本中启用的断言。这使得它的速度略有降低，但可能出错的事情太多了，最好是诚实地直接崩溃，而不是假装没事勉强运行下去。

Valgrind 有大量的详细输出和调试输出。你可以通过重复使用 `-v` 和 `-d`，每个最多 4 次，来提高调试/详细程度。除此之外，还有一些更为针对性的跟踪选项，比如 `--trace-syscalls=yes`。在 Valgrind 中调试可能相当困难，这些输出在开发新功能时会是很大的帮助。它们也对支持工作非常有用，例如要求用户上传日志到 Valgrind 的 bugzilla。

### 代码复杂性

Valgrind 本身有点复杂。开发 Valgrind 最困难的部分之一就是它涉及的内容非常广泛。它需要虚拟化四种 CPU 架构（Intel/AMD、ARM、MIPS 和 PPC，并包含一些子变种）。每种架构都有数千页的手册。你通常需要了解所有操作码，甚至是它们可能变化的每一位。你需要对 C、C++ 和 POSIX 有深入了解。你还需要能判断哪些操作系统系统调用需要特殊处理。了解 ELF 标准也很重要——因为 lld 和 mold 的实现方式不同，我们曾遇到过相关问题。除了 ELF，还有用于调试信息的 DWARF。到目前为止，我仅覆盖了 Valgrind 的核心部分。

尽管 Valgrind 很复杂，但我认为它并不包含大量的代码。干净的 git 克隆，不包括回归测试，大约有 50 万行代码。加上回归测试，总行数大约为 75 万行——虽然回归测试的数量大约有 1000 个，但其中有一些非常庞大的，涵盖了大量的比特模式组合，使用脚本生成所有输入组合进行测试。

这些工具本身只占代码量的不到 10%。主要的代码量来自 CPU 仿真和“核心”部分。核心包括很多内容——libc 替代、系统调用包装器、内存管理、gdb 接口、DWARF 读取器、信号处理、内部数据结构和函数重定向。

开发 Valgrind 时的额外难题是，由于它完全静态化，你无法用 sanitizers 构建它。然而，你可以在 Valgrind 内部运行 Valgrind！这需要特殊的构建，最终你会得到外部 Valgrind 和内部 Valgrind，内部 Valgrind 是外部 Valgrind 的“客户”，而内部 Valgrind 的“客户”是可执行程序。显然，这样会使得一切变得更慢。我确实使用免费的 Coverity Scan 服务对 FreeBSD 上构建的 Valgrind 进行静态分析。它大多数时候发现的是常见的假阳性，但也找到了几处实际的 bug，包括一些是我自己添加的。我仍然需要做一些工作，为 Valgrind 的内部 libc 替代，特别是分配函数，提供代码模型。

## Valgrind 在运行时

### 客户执行

Valgrind 中的 CPU 仿真被称为 VEX（不要与 Intel 向量扩展混淆）。VEX 的起源不太清楚，可能是“Valgrind Emulation”的缩写。

当 Valgrind 运行时，只有一个进程——宿主进程。不会使用 `ptrace`（调试器如 lldb 和 gdb 使用的工具）。客户（有时称为客户端）可执行文件在宿主进程中运行，使用动态二进制插桩（DBI）。为了执行插桩，Valgrind 进行动态重编译，使用即时编译（JIT）。这一过程如下：

* 读取一段机器代码。
* 将这些机器码转换为 Valgrind 中间表示（IR）——这与编译器使用的表示方式相同，并非巧合，Julian Seward 也曾参与过格拉斯哥 Haskell 编译器的工作。
* 根据工具的需要，对 IR 进行插桩。
* 对 IR 进行优化和重写。
* 将 JIT 生成的操作码存储在缓存中并执行它们。

### 内存隔离

Valgrind 有自己的内存管理器。它严格区分宿主进程使用的内存和客户进程使用的内存。许多工具会替换 C 和 C++ 的分配与释放函数。对于这些工具，所有的内存管理都由 Valgrind 的内存管理器处理。像 cachegrind 和 callgrind 这类工具不会替换内存分配器（因此，它们会在性能分析中包括分配器）。

### Valgrind 启动

Valgrind 首先在其自己的 `_start` 例程中以汇编语言启动（记住没有 libc），它做的第一件事是为自己创建临时堆栈，设置日志记录，并设置堆分配器。我想要强调的一点是，出错的空间非常有限。如果发生了错误，幸运的话，你可能只是看不到文件名和行号。如果运气不好，堆栈跟踪中只有一堆十六进制地址。可以想象，如果没有堆栈，程序会很快失败。待 Valgrind 完成了所有内部设置，它就准备好在合成 CPU 上启动客户可执行文件。它为自己创建了另一个堆栈，大小可以配置，然后启动客户可执行文件。从客户可执行文件的角度来看，它就像是在本地运行一样。

### 处理系统调用、线程和信号

Valgrind 拦截所有系统调用。幸运的是，大多数系统调用要么什么也不做，要么只做几项检查（寄存器中是否包含已初始化的内存？），然后将其转发到内核。更复杂的系统调用则会根据某些操作码（例如 `umtx_op` 和 `ioctl`）有不同的行为。最后，还有一些系统调用不会转发到内核，需要 Valgrind 自行实现。例如，`getcontext` 系统调用，Valgrind 需要从其合成 CPU 填充上下文，而不是让内核从 Valgrind 宿主的上下文中填充。

有一件棘手的事情是，运行在虚拟 CPU 上的代码必须保持在虚拟 CPU 上。虽然 Valgrind 会在物理 CPU 上本地执行某些客户代码，但通常这种本地执行范围非常有限。如果客户的控制流逃回物理 CPU，事情就会变得非常糟糕。我将举两个例子，说明为了确保 Valgrind 保持控制所需的复杂操作。首先是线程创建。当调用 `pthread_create` 时，Valgrind 需要确保操作系统不会运行第三个参数中传递的函数。相反，它需要用 `run_thread_in_valgrind` 函数挂钩第三个参数。类似地，对于信号，Valgrind 需要确保客户信号处理程序在 Valgrind 下运行，然后信号处理程序返回后也继续在 Valgrind 下运行。这些事情需要一些非常 hacky 的代码。Valgrind 还需要频繁调整信号掩码。当客户程序运行时，信号会被屏蔽，宿主会轮询并处理信号。当发生系统调用时，信号会被解屏蔽，执行系统调用，然后再次屏蔽信号。没有这个小舞蹈，阻塞的系统调用就无法被中断。

## Valgrind 移植

当我开始查看 Valgrind 的移植时，它的状态很糟糕。如前所述，从 2009 年到 2011 年，曾有一段推动将其提交到上游的努力。从 2011 年到 2018 年，它退回到最低限度的维护。

由于在 `stat` 系列函数中添加对大文件支持的更改，Valgrind 在 amd64 上的版本损坏了。一些人找到了修复该问题的补丁。i386 版本在多个方面存在问题。没有 FreeBSD 特定的回归测试。Valgrind 包含许多在所有平台上运行的测试，然后是所有操作系统和 CPU 架构的组合（例如，amd64、freebsd 和 amd64-freebsd）。大约有 600 个这样的通用测试。Linux amd64 平台上，在这些通用测试的基础上还增加了大约 200 个测试。我记不清这些通用测试有多少通过，多少失败了，通过的也就一半多一点。幸运的是，有很多问题是低垂的果实。在解决了 i386 上的一些严重问题后，经过大约六个月的努力，我让大约 90% 的回归测试通过。听起来不错，但仍然存在一些严重的限制。费力完成剩下的 10% 几乎就像是“最后的 10% 需要 90% 的时间”。

## 战斗故事

### 信号导致断言失败

信号。天哪，我一开始真是很难理解这一切。当程序本地运行时，信号会引发以下操作：

* 内核合成 ucontext 块，其中包含信号发生的地址和堆栈（或备用堆栈）中的调用帧，并将调用帧的返回地址设置为 `retpoline`（用于从信号处理程序返回的小型汇编函数）
* 内核将正在运行的可执行文件转移到信号处理程序
* 信号处理程序执行相关操作并返回
* `retpoline` 调用 `sigreturn` 系统调用
* 内核从上下文中获取信号发生前的原始地址，并将执行转移到该地址

在 Linux 上，这一过程对非线程化和线程化应用程序都适用。对于 FreeBSD，一旦链接了 libthr，这一过程就会有所变化。`thr_sighandler` 替代了用户的信号处理程序。它做一些信号屏蔽之类的事情，调用用户的信号处理程序并自己调用 `sigreturn`。

Valgrind 不能让客户代码执行。所以，它处理所有可能的信号。它合成自己的上下文，加入了一些额外的信息。它用自己的 `run_signal_handler_in_valgrind` 函数替换了客户的信号处理程序。返回地址设置了自己的 `retpoline`，它将调用 `valgrind_sigreturn`，将客户程序的执行转移回原来的位置。那么可能会出什么问题呢？事实证明，几乎所有的东西都有问题。至少有两处在这个流程中出了问题，我都处理过了。

第一个问题是非常小的代码更改。当从客户信号处理程序返回时，Valgrind 在 i386 上崩溃了。经过大量调试，我将问题缩小到汇编语言中的 `retpoline` 函数 `VG_(x86_freebsd_SUBST_FOR_sigreturn)`。某个时刻，ucontext 结构的大小一定发生过变化。`VG_(x86_freebsd_SUBST_FOR_sigreturn)` 在错误的偏移位置寻找返回地址——0x14 而不是 0x1c。这意味着虚拟 CPU 在某个无效的地址处恢复执行。砰！很快就触发了断言。

我与信号的第二次大战是间歇性的。如果信号在 Valgrind 执行“普通”客户代码时到达，那是非常好的，因为它确切地知道应该从哪里恢复。但如果信号在系统调用时到达呢？情况就变得复杂多了，因为系统调用算是 Valgrind 让客户在物理 CPU 上运行的地方之一。Valgrind 不能在其全局锁中执行客户的系统调用。系统调用可能会阻塞，这会导致多线程进程挂起。相反，它释放锁，然后执行系统调用。现在，如果在锁被释放的窗口期间发生了中断，Valgrind 需要尝试弄清楚中断发生的位置，以便决定是否需要重新启动。为此，执行客户系统调用的机器代码函数 `ML(do_syscall_for_client_WRK)` 有与之相关的地址表，包含了设置、重启、完成、提交和结束的地址。这个方法很好用，但偶尔会因断言失败而出错。问题出在系统调用状态的设置上。在 Linux 上，它只保存在 RAX 寄存器中，并从小型汇编函数返回，所以无需做任何特殊处理。而在 FreeBSD（和 Darwin）上，它保存在进位标志中。这需要调用一个函数来在合成 CPU 中设置进位标志。如果信号在 Valgrind 调用 `LibVEX_GuestAMD64_put_rflag_c` 函数时到达呢？这个情况就没有处理——导致了断言。遗憾的是，在 C 中没有简单的方法来判断指令指针正在执行哪个函数。你可以轻松地获取函数开始的地址，但函数结束的位置在哪里呢？我曾考虑过使用 Valgrind 的 DWARF 调试信息（它应该始终存在，且 Valgrind 内置了 DWARF 读取代码）。最后，我采取了丑陋且不标准的方法。我获取了 `LibVEX_GuestAMD64_put_rflag_c` 之后的一个虚拟函数的地址。即使没有保证编译器和链接器会按照源文件中的顺序布局函数，这个方法碰巧在 i386 和 amd64 上都有效。然而，后来当我处理 [aarch64 移植](https://github.com/paulfloyd/freebsdarm64_valgrind) 时，这个方法就不行了，因为设置进位标志的函数使用了几个辅助函数，而这些辅助函数并没有按顺序布局。因此，我改为在执行客户系统调用的汇编例程中设置全局变量。

### GlusterFS swapcontext 崩溃

另一个战斗故事。这是我在发布重新启动的 FreeBSD Valgrind 后最早收到的几个错误报告之一。运行 GlusterFS 的用户在使用 Valgrind 时遇到了崩溃。经过一番反复询问日志文件和跟踪信息后，我将问题缩小到 `swapcontext` 系统调用。原来，在 Valgrind 保存的线程状态中，切换到的上下文有两个指向信号掩码的指针，而 Valgrind 只设置了第一个指针。这又是一次调试了几天才只改了一行代码的问题。

### FreeBSD 问题

我在 Valgrind 上做的工作还揭示了 FreeBSD 中的一些 bug。我在处理 i386 二进制文件在 amd64 上运行时，早期就调试过其中一个问题。我在 i386 与 i386、amd64 与 amd64 上没有问题，但 i386 在 amd64 上运行时，在客户启动的早期就崩溃了，发生在链接加载器（lib rtld）阶段。最终，我发现这是与页面大小检测有关的问题。正常的独立应用程序将这些信息存储在它们的辅助向量（auxv）中，包含 `AT_PAGESZ`（实际页面大小）和 `AT_PAGESIZES`（指向可能页面大小表的指针）。Valgrind 为客户合成了 auxv，但当时它忽略了 `AT_PAGESIZES`。没有问题，rtld 有回退机制，会使用 `HW_PAGESIZE` 的 `sysctl`。i386 有两种可能的页面大小，而 amd64 有三种可能的页面大小。不幸的是，发生的情况是，运行在 amd64 上的 rtld 期望 `PAGESIZES` 表有三项，但 i386 内核组件只提供了两项。结果，`sysctl` 返回了 [ENOMEM](https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=246215)。

## 房间里的大象——Sanitizers

既然我们有了 Sanitizers，为什么还要使用 Valgrind？我也反过来问，既然我们有 Valgrind，为什么还要使用 Sanitizers？大致来说，Address Sanitizer 和 Memory Sanitizer 相当于 Memcheck，而 Thread Sanitizer 相当于 DRD 和 Helgrind。UB Sanitizer 没有 Valgrind 的对应工具。

有一种情况使用 Valgrind 根本不可行，那就是在使用不受支持的 CPU 架构时。Valgrind 在 FreeBSD 上只支持 amd64、i386 和 aarch64。如果你使用的是其他架构，那么 Valgrind 就无法使用。接下来，Valgrind 滞后于 CPU 的开发。这意味着如果你的应用依赖于使用 [AVX512](https://bugs.kde.org/show_bug.cgi?id=383010)，你就无法使用 Valgrind。

如果 Sanitizers 和 Valgrind 都能在你的系统上工作，你该选择哪个呢？一如既往，这取决于情况。

|   特性   |      Valgrind      |         Sanitizer         |
| :----: | :----------------: | :-----------------------: |
|   速度   |    非常慢，有时几乎无法使用    |             慢             |
| 堆栈边界检查 |          否         |             是             |
| 是否需要插桩 |          否         |             是             |
| 可用性和支持 | amd64、i386、aarch64 | amd64、i386、aarch64、risc-v |

当我说 Valgrind 不需要插桩时，那是个善意的谎言。如果你使用自定义分配器，那么你需要为 Valgrind 或 Sanitizers 写一些注解，才能确保它们正常工作。同样，如果你使用了自定义线程锁定例程（如自旋锁），在这两种情况下你也需要进行注解。Thread Sanitizer 的优势在于，它对不依赖于 pthread 的标准库机制（如 std::atomic）具有内置注解。

FreeBSD 很幸运，工具链是基于 LLVM 的。这意味着 Memory Sanitizer 很容易获得。而 GCC 没有 Memory Sanitizer，这使得在 Linux 上使用起来更加困难。不要低估“需要插桩”这一要求有多大。为了获得最佳结果，这意味着你应该对所有依赖的库进行插桩。如果你是 KDE 应用程序的开发者，那至少需要插桩以下几组库：KDE、Qt、libc++。还有许多其他依赖（如 libfontconfig、libjpeg 等）。正如我们 Valgrind 开发者常说的：“祝你好运！”如果你在一家大公司工作，有专门的 devops 团队来设置一切，那就不那么糟糕了。我也很想听听任何有使用 poudriere 构建 Sanitizer 版本经验的人的意见。我还读过一些拥有大规模单元测试套件的人抱怨，在构建带有 Sanitizer 的版本时，构建时间和磁盘空间要求过高，特别是因为你不能做“一站式”Sanitizer 构建（Address 和 Memory Sanitizers 不兼容）。

我的结论是，你应该根据需求选择最适合的工具。

## 未来工作

不幸的是，Valgrind 是位腐烂得很快的工具。FreeBSD 的新版本不断发布，带来了新的和变化的系统调用。辅助向量中不断增加新的项。`_umtx_op` 命令也在增多。libc++ 不断找到使用 pthread 的更奇怪方式。编译器以看似不安全的方式优化代码。这意味着 Valgrind 的工作永远不会完成。

### CPU 架构

Valgrind 在 FreeBSD 上支持 amd64、i386 和 aarch64。我不认为自己会添加对 MIPS 或 PPC 的支持。RISC-V 尚未被加入到官方 Valgrind 源代码中——[RISC-V 移植](https://github.com/petrpavlu/valgrind-riscv64) 正在进行中，但目前因为关于矢量指令实现的讨论而被拖延。

### 错误列表

Valgrind 的 [Bugzilla](https://bugs.kde.org/buglist.cgi?bug_status=UNCONFIRMED\&bug_status=CONFIRMED\&bug_status=ASSIGNED\&bug_status=REOPENED\&list_id=2388402\&product=valgrind\&query_format=advanced) 中大约有 1000 个未解决的错误。虽然其中许多只影响 Linux/macOS/Solaris，但也有不少影响 FreeBSD。

* Helgrind 在大量线程创建/销毁时会产生虚假警报，特别是线程局部存储（TLS）。这是因为有包含 TLS 的 pthread 栈缓存。Valgrind 没有把回收的 TLS 看作是有不同内存地址的。Linux 通过 GNU libc 环境变量禁用 pthread 栈缓存来规避这个问题。但我还没有找到使用 FreeBSD libc 做同样事情的方法。
* 当客户程序生成核心转储时，实际上是 Valgrind 生成了核心文件。目前，核心文件的布局几乎与 Linux 的核心转储相同。这意味着 lldb 和 gdb 无法对核心文件做太多处理。我认为这不是大问题，因为现在很少有人使用核心文件了。
* 线程调度器。Valgrind 有非常基础的线程调度器。线程上下文切换发生在系统调用边界或每 100000 个基本块。默认调度器只是释放全局锁，哪个线程获得锁完全取决于运气。如果前一个线程在 CPU 缓存中很热，那么它就有可能获得锁。Linux 有基于 futex 的可选公平调度器。虽然这个调度器不能直接移植到 FreeBSD，但用 `_umtx_op` 来实现它应该不会太难。
* 在 aarch64 上，偶尔会出现与线程局部存储中的访问相关的 DRD 虚假警报。
* 验证 `ioctl` 的代码非常有限。几乎所有的 `ioctl` 都只对它们的参数做基本的大小检查。这个功能需要扩展，理想情况下还需要添加测试用例。

## 结论

在 Valgrind 上工作是巨大的挑战。调试可能非常困难——我常常发现自己一边调试客户程序，一边调试 Valgrind 运行客户程序，同时还用 `vgdb` 调试在 Valgrind 中运行的客户程序。我学到了很多关于 ELF、信号和系统调用的知识，当然，也学到了关于 Valgrind 本身的很多东西。总是有很多需要学习的地方——例如 aarch64 和 amd64 的操作码细节，以及动态重编译中使用的各种技巧。

***

**Paul Floyd** 自 2.1 版本以来间歇性使用 FreeBSD，并从 10.0 版本起开始认真使用。他已加入 Valgrind 开发团队四年，拥有电子学博士学位，现居住在法国阿尔卑斯山边缘的格勒诺布尔附近，供职于西门子 EDA，开发模拟电子电路仿真工具。


# 嵌入式 FreeBSD：探索 bhyve

* 原文链接：[Embedded FreeBSD: Digression into bhyve](https://freebsdfoundation.org/our-work/journal/browser-based-edition/kernel-development/embedded-freebsd-digression-into-bhyve/)
* 作者：Christopher R. Bowman

![嵌入式 FreeBSD](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4bb7c960a50272848ebdd22949d3c5bd8df36c5c%2FEmbedded-1.png?alt=media)

在之前的两篇文章中，我们讨论了我一直在试用 [Digilent Arty Z7-20](https://digilent.com/shop/arty-z7-zynq-7000-soc-development-board/) 开发板。我觉得这是一块很有趣的开发板，因为它不仅可以切换主要引脚来与外界接口，而且你还可以将自己的电路集成到开发板中，并与处理器接口。然而，要做到这一点，你需要使用 Xilinx/AMD 的软件来配置和编程芯片。Xilinx 将这个工具套件称为 [Vivado](https://www.xilinx.com/products/design-tools/vivado.html#editions)，你可以从他们的网站上 [免费下载](https://www.xilinx.com/products/design-tools/vivado/vivado-ml.html) 一个适用于 Zynq 芯片的版本。

那么，缺点是什么呢？肯定有什么潜在的问题，对吧？其实有两个问题。首先，Vivado 只有 Windows 和 Linux 版本。其次，下载的文件本身就有 110GB。多年来，我在 Mac 上通过 VMWare 运行 Linux 版本，效果还不错。但最终，我的虚拟机有好几个不同版本，每个版本的软件安装情况都不同。这些虚拟机非常大，早期的版本大约是 30GB，最近的一些安装版本甚至达到了 75GB。假设在几台机器上有几个版本，那么总空间开始变得非常庞大，而且我也很难追踪哪个版本是最新的。

于是，我决定简化并集中管理。我希望将所有下载的原始厂商工具存储在我的网络文件服务器上，这台服务器自然运行 FreeBSD，拥有数 TB 的 ZFS 存储空间。毕竟，我不能指望厂商继续提供这些旧版本的工具，尤其是在他们更新到新版本后。这是可以理解的，但作为一个爱好者，我的工作进度并不总是与厂商的进度同步，因此我希望确保能够保留原始工具下载文件。我希望我的主目录和工作文件与项目都存储在我的网络服务器上，这样我可以从所有机器上访问它们，并像任何其他数据一样备份。我希望所有解压并安装好的厂商软件都存储在我的文件服务器上，而不是存放在我的虚拟机里。这样，我可以利用 ZFS 的强大功能为厂商软件安装创建检查点。我的虚拟机只是一个轻量级的 Linux 安装，可以是任何我想要实验的 Linux 版本。如果我需要升级或创建新的虚拟机，我知道里面没有任何工作内容，所有东西都在我的文件服务器上，所以我只需要安装一个新的基本系统，其他所有内容仍然保存在文件服务器上。我无需复制或重新安装厂商工具。由于最近我家计算环境的升级包含了一台 16 核 128GB 内存的 FreeBSD 机器，我希望在这台机器上运行这些工具，以应对我的设计规模变大，可能需要数小时来合成的情况。作为额外的好处，这样的设置使得其他人如果想要复制我的工作，只需要一台 FreeBSD 机器即可。

目前似乎有两种基本的方法。我可以尝试让安装程序在 FreeBSD 的 Linux 仿真环境下运行，并在我的 FreeBSD 机器上本地运行这些工具，而不需要 Linux 虚拟机。如果能够实现，那将是非常棒的！但我不确定是否能做到，也不清楚可能会遇到哪些问题，或者需要多长时间才能弄明白，而我当时正忙于几个项目。这个方法看起来确实是最好的，我很想知道是否有人已经成功实现，或者有谁想尝试，但我选择了一个我认为会涉及较少工作量且更有可能成功的方法。我听说过 bhyve，决定研究一下。如果我能运行一个搭载 Vivado 所支持 Linux 版本的虚拟机，我认为那将是最简单的。只用了一个晚上阅读和另一个晚上实验，我就惊讶地成功运行了。

我从 FreeBSD 手册开始。它真的是一个很棒的资源，感谢所有帮助使其如此出色的人。[第 24.6 章：作为主机使用 FreeBSD 和 bhyve](https://docs.freebsd.org/en/books/handbook/virtualization/#virtualization-host-bhyve) 对如何使用 FreeBSD 作为主机操作系统做了很好的介绍。在大多数情况下，我从那里和互联网上的其他一些地方拼凑出了一个设置。为了进行设置，我为网络创建了基本的主机配置：

```sh
# kldload vmm

# ifconfig tap0 create up
# ifconfig bridge0 create
# ifconfig bridge0 addm igb0 addm tap0
# ifconfig bridge0 up
```

我正在使用 [24.6.5 节：为 bhyve 客户机配置图形 UEFI 帧缓冲](https://docs.freebsd.org/en/books/handbook/virtualization/#virtualization-host-bhyve) 中的说明，因为我不想费神设置 grub。使用 UEFI 帧缓冲还允许我通过 vnc 导出 Linux 显示。我可以从 FreeBSD 主机连接（如果正在使用的话），或者从网络上的任何其他机器连接。不过，也许我应该再考虑一下安全性问题。

我下载了一个 CentOS 的 ISO 版本，是在 RedHat 终止它之前的版本，然后我使用以下虚拟机配置来安装：

```sh
# bhyve -c 4 -m 32G -w -H \
        -s 0,hostbridge \
        -s 3,ahci-cd,/u1/ISOs/CentOS/CentOS-7-x86_64-DVD-2009.iso \
        -s 4,ahci-hd,/dev/zvol/zroot/vms/centos7 \
        -s 5,virtio-net,tap0 \
        -s 29,fbuf,tcp=0.0.0.0:5900,w=1920,h=1200,wait \
        -s 30,xhci,tablet \
        -s 31,lpc -l com1,stdio \
        -l bootrom,/usr/local/share/uefi-firmware/BHYVE_UEFI.fd \
        vm0
```

通过这种方式，我得到了一个 4 核心、32GB 的虚拟机，ISO 镜像 **/u1/ISOs/CentOS/CentOS-7-x86\_64-DVD-2009.iso** 被挂载为客户机中的 CDROM。这样，我可以运行一个标准的图形界面安装 Linux，过程非常直接。我是在裸 zvol 上运行的：**/dev/zvol/zroot/vms/centos7**。这样做是为了我可以使用 ZFS 快照随时为虚拟机创建快照（最初是在干净安装后），以便随时回滚。如果我将虚拟机虚拟磁盘放在 ZFS 文件系统上，快照会应用到该文件系统中的任何内容，而不仅仅是虚拟机虚拟磁盘映像。我还听说使用裸 zvol 可能更快。回想起来，我本可以为每个虚拟机虚拟磁盘映像使用一个单独的数据集，并在该数据集上使用文件作为虚拟磁盘映像，而不是使用 zvol。如果每个虚拟机有一个数据集，快照仍然只会应用于虚拟机。我不确定哪种方法更好，随后的阅读让我对 zvol 与文件后端的速度产生了疑问。我的工具运行时间还不长，因此我还不太关心榨取每一分性能。我还听说，对于支持的客户操作系统，NVMe 设备比 AHCI 硬盘设备更快，但我还没有实验过。如果这个问题变得严重，创建一个新的虚拟机就很容易了，因为我的数据和安装都不在虚拟机上。

安装了 CentOS 后，我配置它通过 NFS 挂载我的 FreeBSD 机器上的主目录，然后我安装了 Xilinx Vivado 工具。这是一个图形界面的安装，过程很简单。我只需要确保安装路径是 NFS 挂载的目录。考虑到文件操作的量，我原本预期这个过程会非常痛苦，但实际上它相当快，尤其是考虑到安装后工具的体积达到了 66GB。要知道，我有一个非常快速的本地网络。我不打算修改工具的安装，但是我创建了一个快照——这也是为了安心。我不想再重复安装一次。

当我申请 Vivado 许可时，我复制了 Linux 客户机报告的以太网 MAC 地址。这个地址似乎在重启时保持稳定，但我希望能弄清楚如何配置它，以确保我的虚拟机中的 MAC 地址始终与 Vivado 许可中的 MAC 地址匹配。

现在，我有了一个相当通用的 CentOS 虚拟机，安装了 Vivado，可以通过 VNC 从我本地网络上的任何机器访问。这时，我还安装了一个 15 年前的 Linux 版本的《文明：天赋神权》（**Civilization: Call to Power**），以便在我的 FPGA 构建编译时玩。我惊讶于它运行得如此流畅（也惊讶于自己有多么上瘾）。

尽管大部分功能都运行得很好，但与我之前在 Mac 上使用 VMWare 的设置相比，还是有一些不同之处。首先，VMWare 支持将文件系统传递到主机。NFS 挂载基本完成了相同的功能，我没有注意到太多的速度损失，但我必须在 Linux 中配置，而不是在 VMWare 的图形界面中配置。这不是一个大问题——我对此非常适应。真正让我希望能有 VMWare 解决方案的地方是复制和粘贴。如果我在 Linux 客户机的图形界面中选择了一些内容，我无法轻松地将其复制并粘贴到运行 VNC 查看器的机器上。这偶尔是个痛点，但由于我在 Linux 中使用的用户文件系统都是从 FreeBSD 挂载的 NFS，我可以从 FreeBSD（或任何其他挂载它们的机器）上同样轻松编辑这些文件。因此，我几乎不在 Linux 下编辑或工作。大部分时间，我只是在 Linux 的 VNC 会话窗口中运行 Vivado 编译，其他所有工作都在外面进行。总体来说，这个方法还是相当有效的。

下期文章中，我们将开始使用 Vivado 构建第一个电路。

如果你对这些内容有反馈、抱怨或批评，我很乐意听到你的意见。你可以通过 <articles@ChrisBowman.com> 联系我。

***

**Christopher R. Bowman** 于 1989 年在约翰霍普金斯大学应用物理实验室地下两层工作时，首次在 VAX 11/785 上使用了 BSD。后来，他在 90 年代中期在马里兰大学使用 FreeBSD 设计了自己的第一个 2 微米 CMOS 芯片。从那时起，他一直是 FreeBSD 的用户，并对硬件设计和驱动它的软件非常感兴趣。他在半导体设计自动化行业工作了 20 年。


# TCP/IP 历险记：FreeBSD TCP 协议栈中的 Pacing

* 原文地址：[Adventures in TCP/IP: Pacing in the FreeBSD TCP Stack](https://freebsdfoundation.org/our-work/journal/browser-based-edition/kernel-development/adventures-in-tcp-ip-pacing-in-the-freebsd-tcp-stack/)
* 作者：Randall Stewart、Michael Tüxen

![TCP/IP 历险记](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-840bbc79b25e3e5808ca7a5f401930516e98323b%2FTCP-1.png?alt=media)

TCP 的发送和接收行为已经经历了 40 多年发展。在这期间，许多进展帮助 TCP 能够以非常高的速度传输可靠的数据流。然而，一些增强功能（无论是在协议栈中还是在网络中）也带来了负面影响。最初，TCP 协议栈会响应接收到的 TCP 段（ACK 应答）、上层提供的新数据或定时器到期时发送一个 TCP 段。TCP 发送方还实现了拥塞控制，以防止过快地向网络发送数据。这些特性结合在一起，可能会使得 TCP 端点大多数时间都受到 “ACK 时钟” 的影响，如下图所示。

![image](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-eb663a3ef177007801b55dc6db12b071aefdbdda%2FTCP-2.png?alt=media)

**图 1：ACK 时钟的示例**

为简化说明，假设数据包已编号并 ACK，并且接收方每 ACK 一个数据包后，跳过一个包（以减少开销）。图中底部的 ACK “1” ACK 了之前发送的 0 和 1 号包（图中未显示），移除了两个未确认的包，并允许发送两个新的包，即包 8 和 9。此时，由于拥塞控制，发送方被阻塞，等待 ACK “3” 的到来，这会 ACK 包 2 和包 3，并再次发送下两个包。这种 ACK 时钟现象在早期互联网的许多数据流中普遍存在，今天在某些情况下仍能看到。ACK 时钟形成了一种自然的数据传输 Pacing，允许数据包通过瓶颈发送，而通常在发送方的下一个数据包到达瓶颈时，前一个数据包已经被传输了。

然而，随着时间的推移，网络和 TCP 的优化已改变了这种行为。一个例子可以在有线网络中看到。在这种网络中，下行带宽很大，但上行带宽较小（通常只有下行带宽的一部分）。

由于回传路径带宽的稀缺，有线调制解调器通常会仅保留最后一个已发送的 ACK（假设它看到的 ACK 是按顺序的），直到决定发送。因此，在上述示例中，有线调制解调器可能只会发送 ACK-5，而不是允许发送 ACK-1、ACK-3 和 ACK-5。这将导致发送方发送一个较大的数据包突发，而不是将三次两个数据包的突发分开。

另一个修改行为的例子可以在其他时隙技术中看到（这些技术会等到自己的时间片到达时才发送数据包，然后一次性发送所有排队的包），在这些技术中，ACK 会被排队，然后一次性以三个 ACK 包的突发形式发送。这种类型的技术与 TCP-LRO（在上期中提到）相互作用，从而可能会将 ACK 合并成一个单一的 ACK（如果使用的是旧的方式），或者在发送函数调用之前，将所有 ACK 排队一起处理。在任何情况下，又会发送一个大数据包突发，而不是一系列的小两包突发，这些小包之间有小的时间间隔（大致接近瓶颈带宽加上一些传播延迟）。

我们的示例显示了六个数据包，但实际上在一次 `tcp_output()` 调用中，可能会有数十个数据包突发。这对 CPU 优化有好处，但可能会导致网络中的数据包丢失，因为路由器的缓冲区有限，大量突发数据包更容易导致尾丢失。这种丢包会减少拥塞窗口，从而影响整体性能。

除了上述两个例子外，还有其他原因可能导致 TCP 变得突发（其中一些在 TCP 中始终存在），例如应用程序受限的时段。这种情况是指发送应用程序由于某种原因暂停，延迟几毫秒才发送数据。在此期间，ACK 会到达，但没有数据需要发送。在网络中的所有数据尚未传输完之前（这可能由于空闲而导致拥塞控制减少），应用程序会发送另一个较大的数据块。此时，拥塞窗口是打开的，因此可以生成较大的发送突发。

另一个突发性源可能来自对等 TCP 实现，它们可能决定每 ACK 第八个或第十六个包，而不是遵循 TCP 标准每 ACK 一个包。这种较大范围的 ACK 会再次导致对应的突发。TCP Pacing（在下一节中描述）是一种改进 TCP 段发送的方式，用以平滑这些突发。

## TCP Pacing

以下示例说明了 TCP Pacing。假设使用 TCP 连接以 12 兆比特每秒的速度向对等方传输数据，包括 IP 和 TCP 头。假设最大 IP 数据包大小为 1500 字节（即 12000 位），这将导致每秒发送 1000 个 TCP 段。在使用 Pacing 时，将每毫秒发送一个 TCP 段。可以使用一个每毫秒触发的定时器来实现这一点。以下图形展示了这种发送行为。

![TCP Pacing 控制发送时序图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-f9086bf0675536f8b51584f19535ea9815536ed0%2FTCP-3.png?alt=media)

**图 2：以 12Mbps 速率 Pacing 的 TCP 连接**

以这种方式进行 Pacing 是可行的，但由于需要较高的 CPU 成本，这并不理想。因此，为了提高效率，TCP 在进行 Pacing 时，通常会发送小的突发数据包，并且这些数据包之间会有一定的时间间隔。突发包的大小通常与协议栈希望的 Pacing 速度相关。如果以较高的速率进行 Pacing，则需要较大的突发数据包；如果以较低的速率进行 Pacing，则使用较小的突发数据包。

在设计 TCP 协议栈的 Pacing 方法时，可以采取多种方法。常见的做法是让 TCP 协议栈在较低层设置一个速率，然后将大批量数据的发送交给该层处理。较低层再通过合适的定时器来多路复用来自不同 TCP 连接的数据包，从而实现数据的间隔发送。然而，这不是 FreeBSD 协议栈采用的方法，特别是因为这种方法限制了 TCP 协议栈对发送过程的控制。如果连接需要发送重传数据包，重传数据包将排在所有待发送的队列包之后。

在 FreeBSD 中，采用了不同的方法，让 TCP 协议栈控制发送，并创建了一个专门的定时系统，当 Pacing 间隔结束时，该系统会调用 TCP 协议栈发送数据。这种方法将发送控制完全交给 TCP 协议栈，但可能会带来一些性能问题，需要进行相应的补偿。下一节将介绍为 FreeBSD 创建的这一新子系统。

## 高精度定时系统

### 概念概述

高精度定时系统（HPTS）是一个可加载的内核模块，提供了一个简单的接口，供任何希望使用它的 TCP 协议栈调用。基本上，TCP 协议栈会调用两个主要函数来使用 HPTS 提供的服务：

* `tcp_hpts_insert()`：将 TCP 连接插入到 HPTS 中，在指定的时间间隔内调用 `tcp_output()`，或者在某些情况下，调用 TCP 的入站数据包处理函数 `tfb_do_queued_segments()`。
* `tcp_hpts_remove()`：要求 HPTS 从 HPTS 中移除一个连接。通常在连接关闭或不再需要发送数据时使用。

HPTS 中还提供了一些其他辅助功能，帮助进行定时和其他管理任务，但上述两个函数是 TCP 协议栈用于实现 Pacing 的基本构建块。

### 详细信息

在内部，每个 CPU 都有一个 HPTS 轮盘，这是一个连接列表的数组，表示在不同时间点需要服务的连接。轮盘中的每个槽位代表 10 微秒。当一个 TCP 连接被插入时，它会被分配一个从现在开始的槽位数量（即 10 微秒的时间间隔），直到调用 `tcp_output()` 函数。这个轮盘由系统定时器（即 FreeBSD 的调用系统）和一个软定时器（如 \[1] 中提议的）共同管理。基本上，每次系统调用返回时，在返回用户空间之前，HPTS 可能会被调用，以查看是否需要服务某个 HPTS 轮盘。

HPTS 系统还会自动调优 FreeBSD 的系统定时器，首先设置最小值（默认为 250 微秒）和最大值，如果 HPTS 轮盘中有更多连接且调用频繁，则 FreeBSD 系统超时期间的小量处理将增加系统定时器的长度。如果连接数低于某个阈值，则仅使用基于系统定时器的方法。这有助于避免通过在轮盘上保持连接时间过长而导致连接饿死。HPTS 尝试提供定时器最小精度（即 250 微秒），但这并不保证。

使用 HPTS 进行 Pacing 的 TCP 协议栈有一些特定的责任，以便与 HPTS 协作，实现所需的 Pacing 速率，包括：

* 待启动了 Pacing 定时器，协议栈必须禁止任何发送或其他对 `tcp_output()` 的调用，直到 Pacing 定时器到期。协议栈可以查看 `t_flags2` 字段中的标志 `TF2_HPTS_CALLS`。当 HPTS 调用 `tcp_output()` 函数时，该标志将被设置，协议栈应在其 `tcp_output()` 函数内进行清除。
* 在 Pacing 定时器到期后，HPTS 的调用中，协议栈需要验证它的空闲时间。HPTS 可能会比预期稍晚调用协议栈，甚至可能提前调用协议栈（尽管这种情况非常罕见）。协议栈需要将晚到或早到的时间纳入下一个 Pacing 超时计算中，之后再发送数据。
* 如果协议栈决定使用 FreeBSD 定时器系统，它还必须阻止定时器调用发送数据。RACK 和 BBR 协议栈不会使用 FreeBSD 定时器系统进行超时，而是直接使用 HPTS。
* 如果协议栈从 LRO 队列中排队数据包，则 HPTS 可能会调用输入函数，而不是 `tcp_output()`。如果发生这种情况，则不会再调用 `tcp_output()`，因为假设协议栈会在需要时调用其输出函数。

HPTS 还提供了一些工具来协助 TCP 协议栈，包括：

* `tcp_in_hpts()`：告诉协议栈是否已在 HPTS 系统中。
* `tcp_set_hpts()`：设置连接将使用的 CPU，这是一个可选调用，如果协议栈未调用此函数，HPTS 会为该连接执行此操作。
* `tcp_tv_to_hptstick()`：将 `struct timeval` 转换为 HPTS 槽位数。
* `tcp_tv_to_usectick()`：将 `struct timeval` 转换为 32 位无符号整数。
* `tcp_tv_to_lusectick()`：将 `struct timeval` 转换为 64 位无符号整数。
* `tcp_tv_to_msectick()`：将 `struct timeval` 转换为 32 位无符号毫秒定时器。
* `get_hpts_min_sleep_time()`：返回 HPTS 强制执行的最小睡眠时间。
* `tcp_gethptstick()`：可选地填充一个 `struct timeval` 并返回当前的单体时间，作为 32 位无符号整数。
* `tcp_get_u64_usecs()`：可选地填充一个 `struct timeval` 并返回当前的单体时间，作为 64 位无符号整数。

### `sysctl` 变量

HPTS 系统可以通过 `sysctl` 变量进行配置，以改变其性能特征。默认情况下，这些值设置为一组 “合理” 的值，但根据应用的不同，可能需要进行更改。值可以在 `net.inet.tcp.hpts` 系统控制节点下进行设置。

以下是可用的调优参数：

|           名称           |  默认值  |                                                                                                        描述                                                                                                       |
| :--------------------: | :---: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| no\_wake\_over\_thresh |   1   |                                          当将连接插入 HPTS 时，如果此布尔值为真且连接数大于 `cnt_thresh`，则不允许调度 HPTS 运行。如果值为 0（假），则在将连接插入 HPTS 时，可能会导致 HPTS 系统运行连接，即调用 `tcp_output()` 以调度的连接。                                         |
|       less\_sleep      |  1000 |                                                                                当 HPTS 完成运行时，它会知道它运行了多少个槽。如果运行的槽数超过此值，则需要减少动态定时器。                                                                                |
|       more\_sleep      |  100  |                                                                                       当 HPTS 完成运行时，如果运行的槽数少于此值，则动态睡眠时间将增加。                                                                                      |
|       min\_sleep       |  250  |                                                             这是 HPTS 定时器可以降低到的绝对最小值。减小此值会导致 HPTS 运行更多，使用更多的 CPU。增大它会导致 HPTS 运行更少，使用更少的 CPU，但会负面影响精度。                                                             |
|       max\_sleep       | 51200 |                                                                  这是定时器可以达到的最大睡眠值（以 HPTS 槽位为单位）。通常仅在没有连接被服务时使用，即 HPTS 每 51200 x 10 微秒（大约半秒）唤醒一次。                                                                 |
|        loop\_max       |   10  | 此值表示 HPTS 在尝试为所有需要服务的连接提供服务时将循环的次数。当 HPTS 启动时，它会将需要服务的连接列表集合在一起，然后开始对每个连接调用 `tcp_output()`。如果花费的时间太长，可能还需要服务更多的连接，因此它将再次循环以提供服务。此值表示 HPTS 在被强制休眠之前最多可以执行多少次此循环。注意，从函数调用返回时不会导致任何循环发生；只有 FreeBSD 定时器调用会受到此参数的影响。 |
|      dyn\_maxsleep     |  5000 |                                                                                         当调整调用时间并看到更多睡眠需求时，动态定时器可以提高的最大值。                                                                                        |
|      dyn\_minsleep     |  250  |                                                                                          当调整调用时间并看到较少睡眠时，动态定时器可以降低的最小值。                                                                                         |
|       cnt\_thresh      |  100  |                                                               这是轮盘上需要的连接数，用于开始更多依赖系统调用返回的情况。超过此阈值时，系统调用返回和超时都会导致 HPTS 运行；低于此阈值时，我们更依赖调用系统来运行 HPTS。                                                              |

## 在 RACK 堆栈中的 Pacing 优化

使用 HPTS 系统进行 Pacing 时，与运行在 TCP 堆栈之下的 Pacing 系统相比，可能会有一些性能损失。这是因为每次调用 `tcp_output()` 时，会做出很多关于发送内容的决策。这些决策通常会引用多个缓存行，并涉及大量代码。例如，默认的 TCP 堆栈在 `tcp_output()` 路径中有超过 1500 行代码，其中不包括任何关于 Pacing 或突发流量缓解的代码。对于没有 Pacing 的默认堆栈，通过这么多行代码和大量缓存未命中的影响，仍然能够通过一次发送多个数据段来进行补偿。然而，当实现一个低层 Pacing 系统时，它可以通过追踪接下来需要发送的数据以及发送的数量，轻松地优化它必须发送的各种数据包的发送过程。这使得低层 Pacing 系统减少了许多缓存未命中。

为了在像 HPTS 这样的高级系统中获得类似的性能，TCP 堆栈必须找到优化发送路径（包括传输和重传）的方式。堆栈可以通过创建 “快速路径” 发送通道来实现。RACK 堆栈已经实现了这些快速路径，以使 Pacing 的成本降低。BBR 堆栈当前也进行 Pacing，符合已实现的 BBRv1 规范，但它尚未实现以下所述的快速路径。

### 快速路径传输

当 RACK 首次进行 Pacing 时，发送调用会通过它的 `tcp_output()` 路径，并计算出可以发送的字节数。然后将其降低到符合已建立的 Pacing 微突发大小，但在这一过程中，会设置一个 “快速发送块”，记录剩余要发送的字节数以及这些数据在套接字发送缓冲区中的位置。同时还设置一个标志，以便 RACK 在下次知道快速路径处于活动状态。请注意，如果发生超时，快速路径标志会被清除，以便做出正确的决策，决定哪个重传需要发送。

在 RACK 的 `tcp_output()` 例程入口处，在验证可以发送数据后，会检查快速路径标志。如果标志被设置，则会使用之前保存的信息发送新数据，而不进行通常的输出路径检查。这大大降低了 Pacing 的成本，因为大部分代码和缓存未命中都从这个快速输出路径中消除了。

### 快速路径重传

RACK 中的重传也有一个快速路径。这得益于 RACK 的发送映射（sendmap），它跟踪所有已发送的数据。当某个数据块需要重传时，发送映射条目会精确告诉快速路径需要发送的数据的位置和大小。这跳过了典型的套接字缓冲区查找和其他开销，即使在发送重传时也能提供一定的效率。

## 结论

HPTS 为 TCP 堆栈提供了一种新颖的服务，使其能够实现 Pacing。为了实现与竞争设计方法相当的效率，TCP 堆栈和 HPTS 需要合作，减少开销并实现高效的包突发发送。本文讨论了 Pacing 的必要性以及 FreeBSD 中为此提供的基础设施。未来的文章将探讨 TCP 堆栈进行 Pacing 时的另一个关键问题，即选择合适的 Pacing 速率。

## 参考文献

1. Mohit Aron, Peter Druschel: *Soft Timers: Efficient Microsecond Software Timer Support for Network Processing*. In: ACM Transactions on Computer Systems, Vol. 18, No. 3, August 2000, pp 197-228. <https://dl.acm.org/doi/pdf/10.1145/319344.319167>.

***

**RANDALL STEWART** (<rrs@freebsd.org>) 已从事操作系统开发四十余年，自 2006 年以来一直是 FreeBSD 的开发者。他专注于传输协议，包括 TCP 和 SCTP，但也曾涉及操作系统的其他领域。目前他是独立顾问。

**MICHAEL TÜXEN** (<tuexen@freebsd.org>) 是明斯特应用科技大学的教授，Netflix 的兼职承包商，自 2009 年以来是 FreeBSD 源代码的提交者。他专注于传输协议，如 SCTP 和 TCP，及其在 IETF 的标准化以及在 FreeBSD 中的实现。


# 实用软件：实现无纸化（Paperless）

* 原文链接：[Practical Ports: Go Paperless](https://freebsdfoundation.org/our-work/journal/browser-based-edition/kernel-development/practical-ports-go-paperless/)
* 作者：Benedict Reuschling

![实用软件](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-d1760111251b24a651c99620e7d61c5011267f6c%2FGo-1.png?alt=media)

在疫情最严重的时候，我一直待在家里，日子基本上和其他日子没什么区别。时间过得很慢，环顾四周，我痛苦地意识到我的房间变得相当凌乱。尤其是我的工作桌旁，堆积的书籍、笔记、信件和其他纸张已经开始争相堆到桌面上。我决定做点什么。我打开播客，开始整理、归类、扔掉，一直清理到桌面整洁。这种巨大的成就感在全球危机时期格外强烈。受到这一感觉的激励，我决定开始扫描那些纸张，减少堆积的纸张数量。

我前阵子买了一台移动扫描仪，它通过 USB 连接，并使用一些专有但高效的扫描软件。这个扫描仪的大小不亚于擀面杖，能够扫描一张纸。当时正好有时间，我继续一张一张地扫描每张纸，为每个生成的 PDF 添加描述和日期，然后继续扫描下一张。虽然这很繁琐，但最终我完成了。之后整理这些文件又是一个漫长的任务：税务、保险、合同、收据、账单（包括已发送和已收到的）、记录、证书等，都需要放入正确的目录中。

扫描软件只有 32 位，几年后整个扫描设备停止工作了——恰好在另一堆纸危险地接近倒塌的时候。是时候寻找替代方案了。几个月前，我发现了 `textproc/py-ocrmypdf`。这款软件本质上是重新扫描 PDF，将有时候是一张大图的文字转换为可以提取和搜索单个单词的文本。它通过模式识别来识别单词和语言，结果出奇地好。我将其应用于现有的扫描文件，现在我可以对文档进行全文搜索，例如找到我参加 BSDCan 2014 时的机票费用。

## 介绍 Paperless-ngx

然后我发现了 paperless-ngx，`ocrmypdf` 是它的一部分，但相对整体而言只是较小的一部分。它是文档管理系统，你可以将文档导入其中，它会创建完全可搜索的档案，自动检测内容（AI 无处不在）并根据你定义的规则将其归档。基本上，我可以把一封信交给它，它会识别出这是来自银行的文件，并根据检测到的日期将其归档，同时使用 `ocrmypdf` 使其变得可搜索，并添加有用的元数据。文件最终存储在目录中，目录可以是主题相关的（例如我从该银行收到的所有文件），或按年 - 月 - 日 - 描述分类，或完全由你决定。你还可以将整个目录导入到 paperless-ngx，它会自动识别哪些文档扫描过，并跳过那些文档，剩下的则通过处理管道处理。随着每个文档的处理，未来类似文档被正确分类的概率也会增加。此外，它还配备了漂亮的 Web 用户界面，可以轻松地将文件拖放到界面中进行扫描，并方便地找到已有文档。另一种将文档导入的方法是通过“incoming”文件夹，你可以在办公室与同事共享这个文件夹，或者将文件作为附件（还记得电子邮件吗？）发送给它。

该软件堆栈本身令人印象深刻，甚至可能太令人生畏，尽管 [paperless-ngx 网站](https://docs.paperless-ngx.com/) 提供了优秀的文档。为了获得顺畅的扫描体验，许多软件和服务需要协同工作。幸运的是，`deskutils/py-paperless-ngx` 下有相关的 Port。更棒的是，Port 维护者创建了一份安装后手册页，详细列出了如何启动正常工作的 paperless-ngx 堆栈的所有步骤。我是不是提到过，我真心喜欢 Port 维护者？有了这些说明，我迅速设置好了自己的 paperless-ngx。首先在树莓派 3 上设置，然后在 Pi 4 上也成功运行了。尽管 Pi 3 由于处理能力有限，可能需要更多的耐心来获得最终结果，但 Pi 4 体验很好，扫描时间也合适。你可以在办公室或家里运行它，几乎不会对电费账单产生影响，同时允许其他人扫描文档而不会看到他人的文件。如果你处理很多文档并希望将其数字化，看看我们正在进行的 paperless-ngx 设置吧，等你体验之后再感谢我…

## Paperless-ngx 设置

无论你使用的是树莓派、其他嵌入式设备，还是一台完整的服务器，其实并不重要。只要它运行 FreeBSD，你就可以跟着做。我不会花时间介绍基础安装或系统硬化，因为有很多其他好文章覆盖了这些内容。只要确保在将你的 paperless-ngx 服务连接到网络，供其他人使用时，做好相应的安全配置。

首先，安装 Port paperless-ngx：

```sh
# pkg install deskutils/py-paperless-ngx
```

安装完成后，你将看到 `pkg-message`，其中建议你查看手册页以获取更多说明。如果没有这些说明，你将只能使用基本服务，此时它的功能并不多。

大多数文件最终会存储在 **/var/db/paperless**，你可能希望将它放在单独的 ZFS 数据集上，但根据我的经验，压缩节省的空间并不值得。不过，你的使用情况可能不同，ZFS 通常是存储这些重要文档的好选择。

Paperless-ngx 需要访问 Redis 实例，接下来我们将安装它：

```sh
# pkg install redis
# service redis enable
# service redis start
```

非常简单，使用这三条命令，你可以安装并设置 Redis，使其开机时自动启动，并且当前会话也能运行。如果你的 Redis 实例运行在网络中的其他地方，你需要修改 **/usr/local/etc/paperless.conf** 并添加相应凭据。如果在本地主机上运行，默认情况下无需特殊权限，因为这样它不会被其他主机访问。

配置文件有很好的文档说明，包含了注释。像 `THREADS_PER_WORKER`（在我的 RPI 4 上设置为 1）、`PAPERLESS_URL`（IP 地址或 DNS 名称）和 `PAPERLESS_TIME_ZONE`（我使用的是 UTF）等项需要根据你的系统和网络修改。许多其他设置在初始扫描时默认配置就足够了。你可以稍后回头查看此文件并修改。

Paperless-ngx 使用数据库来存储各种信息。初始化数据库非常简单，可以使用以下命令：

```sh
# service paperless-migrate onestart
```

如果你希望每次系统启动时都运行这个服务，也可以执行这个 `service` 命令：

```sh
# service paperless-migrate enable
```

完成之后，我们将按顺序启动 Paperless 所使用的后台服务：

```sh
# service paperless-beat enable
# service paperless-consumer enable
# service paperless-webui enable
# service paperless-worker enable
```

你可以在 paperless-ngx 网站上找到这些服务的详细描述。我们希望在不重启系统的情况下使用 paperless-ngx，接下来启动所有这些服务：

```sh
# service paperless-beat start
# service paperless-consumer start
# service paperless-webui start
# service paperless-worker start
```

机器学习是人工智能炒作背后的热门话题。Paperless-ngx 也使用了机器学习，但主要用于辅助字符识别，以确定当前文档的语言。为此，它使用了自然语言工具包（NLTK）。要下载必要的文件，可以使用以下单行命令（如有需要，请替换 Python 版本）：

```sh
# su -l paperless -c '/usr/local/bin/python3.11 -m nltk.downloader \
stopwords snowball_data punkt -d /var/db/paperless/nltkdata'
```

文档以不同方式分类，这是 Celery 组件的职责。扫描时自动完成分类，但你也可以通过以下命令手动触发：

```sh
# su -l paperless -c '/usr/local/bin/paperless document_create_classifier'
```

Celery 还运行可选组件 Flower。它监控 Celery 控制的工作节点集群。这个组件是可选的，我的实例没有运行它。但对于那些想要更多功能的人，这里是启动它的方式：

```sh
# service paperless-flower enable
# service paperless-flower start
```

## 设置 Web UI

为了保护基于 Django 的 Web UI，其中存储了迄今为止所有扫描的文档，你可以像这样设置超级用户密码：

```sh
# su -l paperless -c '/usr/local/bin/paperless createsuperuser'
```

我已经在运行 nginx web 服务器（SSL 代理），所以我可以重用它来指向我的 paperless-ngx 网站。如果你还没有 web 服务器，Port 也提供了现成的配置文件，位于 **/usr/local/share/examples/paperless-ngx/nginx.conf**，你只需将其复制到 **/usr/local/etc/nginx/** 目录即可。这个配置文件还包括 SSL 配置，避免有人窃听流量，获取登录信息并做出其他恶意行为。要创建有效期为一年的密钥，可以运行以下较长的 `openssl` 命令（或者通过 `lets-encrypt` 获取密钥）：

```sh
# openssl req -x509 -nodes -days 365 -newkey rsa:4096 \
-keyout /usr/local/etc/nginx/selfsigned.key \
-out /usr/local/etc/nginx/selfsigned.crt
```

当然，你可以在必要时调整 `nginx.conf`。完成后，启用它开机自启，并在当前会话中启动：

```sh
# service nginx enable
# service nginx start
```

Voila! 现在，你可以在浏览器中访问 `paperless.conf` 中定义的 web URL 并登录应用程序。

## Web UI 中的基础配置

在扫描第一份文档之前，我建议首先在左侧的“管理”部分设置一些项目。首先，定义“通信方”——这些是向你发送纸质文档的人或组织。可以是银行、保险公司，也可以是个人。你可以给他们描述性的名称，并配置 paperless-ngx，以便它在检测到某些关键词或其他条件时，将文档归档到该通信方下。

接下来，定义文档类型。合同与情书不同，账单与证书不同，依此类推。这样，你可以让 paperless-ngx 区分某人是向你发送账单，还是合同。两者都有可能发生，尤其是政府机构（至少在我所在的地方）往往会在不同的上下文中与你通信，而你希望将这些文档分开保存。这正是 paperless-ngx 的优势所在：一旦定义了最活跃的通信方和典型文档，你就不需要再担心正确分类了。只需添加文档，让 paperless-ngx 自动完成分类。通过一些调整，你就可以扫描大量文档。那么，如何给它们排序呢？这就是存储路径的作用。

这些路径定义了文档应存放在文件系统中的位置和目录层次结构。我个人使用的路径是 `{created_year}/{correspondent}/{title}`，这意味着我有像 `2024/insuranceXZY/YearlyReport.pdf` 这样的目录。如果你希望将所有与税务相关的文档存放在单独的目录中，请在存储路径部分定义该规则，并设置条件以匹配符合此条件的文档。最棒的是，如果你改变了排序方式，修改存储路径将自动移动并重新命名你扫描过的文档，而无需你手动执行繁琐的 `mkdir`、`cp`、`mv`、`rm` 等操作。

## 准备、开始、扫描

暂时就这些了。将一份你手头的 PDF 文档拖放到 Web UI 中，看看 paperless-ngx 如何开始处理它。左侧的“日志”部分提供了 paperless-ngx 如何选择匹配通信方和其他详细信息，帮助你调整匹配规则。处理完成后，你可以在仪表板或文档文件夹中找到最终结果。继续扫描其他文档，它们将全部存储在 **/var/db/paperless/media/documents/archive** 目录中（如果你没有在 `paperless.conf` 中修改该路径），然后按照存储路径定义进行存放。希望你能像我一样发现 paperless-ngx 对文档的管理非常有用。我总是期待收到下一封信，以便用 paperless-ngx 扫描它。感谢创建 paperless-ngx 的团队，和使 FreeBSD Port 安装体验如此出色的所有人。

***

**BENEDICT REUSCHLING** 是 FreeBSD 项目的文档提交者，也是文档工程团队的成员。过去，他曾任两届 FreeBSD 核心团队成员。他在德国达姆施塔特应用科技大学管理一个大数据集群，还为本科生教授“Unix for Developers”课程。Benedict 也是每周 bsdnow\.tv 播客的主持人之一。


# 读者来信

* 原文链接：[We Get Letters](https://freebsdfoundation.org/our-work/journal/browser-based-edition/kernel-development/we-get-letters/)
* 作者：Michael W Lucas

![mailbox graphic](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-04a351e75fa8b36a378785e776af7c35bd0537cf%2Fwe-get-letters-1.png?alt=media)

**亲爱的科技界（乃至任何领域）最没用的建议专栏作家：**

**在整个开源界，内核开发者是精英。他们能实现真正酷炫的东西，发明巧妙的新功能，比如 ZFS、缓冲区缓存、内存保护。有什么建议能让我成为他们的一员吗？**

**我读过你的专栏好几次了，老实说我得告诉你：为了最大化成功几率，我会仔细听取你的所有建议，然后反其道而行。**

**——新手但不天真（Novice But Not Naïve）**

亲爱的 NBNN̈：

“做计算机工作还不够。我要让我的失败真正难以解释！”

很好。

许多人幻想内核开发者是编程精英。John Baldwin——多次入选 FreeBSD Core Team，也是本《期刊》编辑委员会主席——就是从写文档直接转做内核开发的。John 不幸认识我几十年了，所以我可以笃定地告诉你：他不仅不是什么精英，他身上那些引人注目、足以定罪、值得说道的事，统统与编程无关。内核开发者当然得达到最低限度的胜任水平，但除了几条规则之外，内核代码没什么特别的。把内核程序员当成精英，会在你起步前就毁掉你——所以我强烈鼓励你这么想。

不过，你若执意前行，执意要让自己织一个茧，蜕变成一位内核开发者，像一只动不动就 panic、会 dump 内存、会损坏文件的蝴蝶——那就立刻把梦想和目标分开。目标是可付诸行动、完全在你掌控之内的事情。实现梦想则需要别人替你出力。约那个让你心动的人吃晚饭？纯属梦想。约那个让你心动的人吃晚饭，被对方提醒你天生不值得爱、应该识相点别再像所谓 “浪漫爱情” 里那种令人毛骨悚然的主角一样纠缠人家？这才是完全可以实现的目标！

你掌控不了别人。为目标努力，别为梦想。

要想成为内核开发者，你可以设定哪些目标？

先读文档。比如《FreeBSD 操作系统设计与实现》《FreeBSD 设备驱动程序开发》《Designing BSD Rootkits》——后者为学习内核工作原理增添了有趣的转折。《FreeBSD Developers Handbook》免费可读。把脑子塞满，做完书里的练习。如果某个知识点你啃不动，没关系，早就有人写过文章和书讨论过它了。

注意，我没说 “问别人该怎么开始学内核”。如果你常逛邮件列表、论坛，或者互联网上更不堪入目的讨论板，偶尔会看到有人求教如何学内核编程。你也许以为这些人在找上面那份书单，但我这里给出的答案用最浅显的搜索就能找到，看起来像在求人牵手。毛毛虫会求助织茧吗？不会！它们又流汗又挣扎，才能挤进逼仄的茧，熬成超越自身的存在。你必须自己干。多数转变都死在这一步，因为人类钟爱舒适惬意、不可付诸行动的梦想，不喜欢丑陋艰难的目标。

像参与任何开源项目一样，你得找一小块下手。从 bug 开始。问题报告（problem reports）是准内核开发者的金矿。在可能的项目里翻找时，你又要再一次把目标和梦想分开。“解决几个 panic bug 并让我的修复被合入” 是梦想。它需要资深内核开发者注意到你的修复并选择采用。“这个月解决一个已报告的内核 panic” 严格说也不是目标，因为你不能保证一定能解决。“这个月花十个小时研究一个已报告的内核 panic，期间不每三分钟就停下来在社交媒体上吐槽、在工作群里抱怨、或者向我那只即使我天生不值得爱也不得不忍受我的宠物诉苦。”——喏，这才是目标！完成足够多这样的目标，你就能练就内核编程的本事。

不过，内核工作真正的难处在哪？

在于别人。

假设你写好了补丁，修复了已报告的问题，并附在 bug 上。你没法让项目成员注意到你的工作。即使他们注意到了，你也没法让他们原样采纳你的补丁。某个项目成员可能把你的补丁当作灵感或概念验证，然后基于你完全没想到的理由写了个完全不同的补丁。让人注意你是梦想。通过提交一整套高质量补丁让自己难以被忽视，才是目标。

毛毛虫怎么变成蝴蝶，这里有个有趣的事实。它们其实不变。我们看到毛毛虫爬进茧，蝴蝶飞出来，于是以为发生了转变——残酷的真相是：毛毛虫的茧是个棺材。毛毛虫爬进去，化成一团浆，只剩下一个基本相当于自组装蝴蝶套件的小块。蝴蝶的第一顿饭是 100% 纯正的毛毛虫浆糊。当你提交了第二十个补丁，仍然感觉没人在乎，请庆幸自己没有真的化成一滩肉泥。精神上的肉泥不那么显眼。

假设你的补丁被采纳了？然后呢？

还是人。

在那个神圣而至高无上的计算机科学研究组（Computer Science Research Group）分发原始 BSD 的光辉岁月里，一个人足以对 Unix 形成充分理解。一次完整安装只占几兆字节。是的，这包括编译器和源代码——“完整安装” 哪里不清楚？大学生都被要求读完并理解这些代码。

今天呢？等你读完基本系统的源代码，它已经变了，你得从头再来。成为 “内核开发者” 几乎不可能。你顶多成为负责内核某一个小切片的可信开发者。做维护会涉及内核的其他部分，这意味着要和负责这些部分的人讨论你的改动。在内核里干活和在用户态编程没什么两样，区别只在于别人相信你达到了某种最低限度的胜任水平。

如果梦想成真，你成了真正的内核开发者，你会发现人不是问题，人是 **问题本身**。你的每一处改动都会得罪某个人。用户和非内核程序员会有一种奇怪的想法，以为你是精英、知道自己在干什么、比他们少一些茫然。

既然你声明了不仅要无视我的想法还要反其道而行，那让我总结一下：成为内核程序员是通往幸福的唯一正道，祝你好运。继续做梦吧！

想向 Michael 提问？ 请发送至 <letters@freebsdjournal.org>

Michael W Lucas 与受人尊敬的 FreeBSD 期刊编辑委员会主席、精英内核开发者 John Baldwin 不同，他始终待在文档领域。他的最新著作是《Run Your Own Mail Server》，该书以 FreeBSD 作为参考平台。更多信息见 <https://mwl.io>。


# 基金会来信

* 原文链接：[Letter from the Foundation](https://freebsdfoundation.org/our-work/journal/browser-based-edition/storage-and-filesystems/letter-from-the-foundation/)
* 作者：John Baldwin

欢迎阅读 7/8 月刊的《FreeBSD 期刊》！写这封信时，夏天即将结束，我们正步入秋季。距上一封信以来，FreeBSD 迎来了新的核心团队，14.1 版本正式发布。发布工程师 Colin Percival 也公布了后续版本的发布计划，13.4 即将完成，14.2 预计年底发布。今年 5 月 BSDCan 期间，《期刊》编辑委员会聚首，规划来年各期内容。FreeBSD 社区活力四射，我们有幸在这些页面中为你带来精彩故事，倍感振奋。

本期主题是 FreeBSD 上的存储。Jason Tubnor 和我先介绍 FreeBSD 支持的两种网络块存储协议：iSCSI 和 NVMe over Fabrics。随后 Roller Angel 讲解 ZFS 原生加密，探讨它如何保障数据安全。本期专栏中，Christopher Bowman 续写他的系列文章，分享在嵌入式环境中使用 FreeBSD 的见解；Randall Stewart 和 Michael Tüxen 剖析 TCP LRO；Benedict Reuschling 介绍如何用 Samba 做 Time Machine 备份；Michael Lucas 照例带来他风趣的评论。

会议方面，Aymeric Wibo 分享了他在 BSDCan 2024 的经历。展望未来，EuroBSDCon 2024 距今仅剩几周，将在爱尔兰都柏林举行；FreeBSD 秋季峰会则于 11 月在加州圣何塞的 NetApp 园区召开。编辑委员会成员将出席这两场活动，我们随时欢迎与你交流。

我们乐于倾听读者的声音。如有反馈、文章建议，或有意投稿，请通过 <info@freebsdjournal.com> 联系我们。

**John Baldwin**

FreeBSD 期刊编辑委员会主席


# 活动日历

* 原文链接：[2024 Events Calendar](https://freebsdfoundation.org/our-work/journal/browser-based-edition/storage-and-filesystems/events-calendar/)
* 作者：**Anne Dickison**

截至 2024 年 11 月的 BSD 活动。如发现此处未列出的 FreeBSD 相关活动或对 FreeBSD 用户有益的活动，请将详情发送至 <freebsd-doc@FreeBSD.org>。

![FreeBSD](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-70ac00e134ce943e58055ef20271aa6f13a6fb6b%2Fevents-calendar-1.png?alt=media)

## 2024 年 9 月 FreeBSD 开发者峰会

2024 年 9 月 19-20 日

爱尔兰都柏林

2024 年 9 月的 FreeBSD 开发者峰会将与 EuroBSDCon 2024 同地举办，地点在爱尔兰都柏林。本次活动为邀请制。FreeBSD 提交者可使用此 wiki 自行注册；非提交者须由一名提交者推荐方可参加。与会者还须参加 EuroBSDCon 2024，才能参与所有开发者峰会活动。

![EuroBSDCon](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-8bedca572005e3d30e4658619e4145c758529d06%2Fevents-calendar-2.jpg?alt=media)

## EuroBSDCon 2024

2024 年 9 月 19-22 日

EuroBSDCon 是年度国际性技术大会，每年在欧洲不同国家举办。大会汇聚基于 4.4BSD（Berkeley Software Distribution）操作系统家族及相关项目的用户和开发者。FreeBSD 基金会再次荣任银牌赞助商。

![All Things Open](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-b824d07a15b8a6ebaf4558038b622da80d343fd2%2Fevents-calendar-3.png?alt=media)

## All Things Open

2024 年 10 月 27-29 日

美国北卡罗来纳州罗利

All Things Open 是美国东海岸规模最大的开源/开放技术/开放 Web 大会，也是全美规模最大的同类大会之一。大会常年邀请世界上最知名的专家，以及几乎所有主流科技公司。FreeBSD 荣任本年度 All Things Open 的非营利合作伙伴。

![OpenZFS](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-cf354019e0ffc83b9853bbad64957c13d3314a48%2Fevents-calendar-4.png?alt=media)

## 2024 年 OpenZFS 用户与开发者峰会

2024 年 10 月 26-29 日

美国俄勒冈州波特兰

第十二届年度 OpenZFS 开发者峰会与首届 OpenZFS 用户峰会将于 2024 年 10 月 26-29 日（周六至周二）在美国俄勒冈州波特兰举行。

用户峰会的议题包括：

* OpenZFS 的存储局域网与广域网网络
* 混合操作系统的 OpenZFS 环境
* 可机器读写的 OpenZFS，包括 JSON、SNMP、REST
* 安全、加密与 MPAA TPN 合规
* Channel Program 工作流
* 极限 OpenZFS，包括全闪存、DRAID、特殊分配类
* 候选议题：持续复制头脑风暴
* 候选议题：用户态 OpenZFS
* 候选议题：OpenZFS DMU 对象存储

基金会荣任铜牌赞助商。

![FreeBSD](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-70ac00e134ce943e58055ef20271aa6f13a6fb6b%2Fevents-calendar-5.png?alt=media)

## 2024 年秋季 FreeBSD 峰会

2024 年 11 月 7-8 日

美国加州圣何塞

FreeBSD 峰会是年度盛会，旨在汇聚社区，共同学习、交流，并推动 FreeBSD 的使用。每年大会都会邀请 FreeBSD 用户，包括决策者、软件工程师和个人贡献者及用户，分享使用 FreeBSD 的最佳实践和成功经验。FreeBSD 峰会还提供难得的机会，让你与开发者社区当面讨论问题。

请注意：2024 年秋季活动的名称由 “Vendor Summit” 更名为 “FreeBSD Summit”，以更准确地反映与会者群体，为未来几年的发展做好准备。

注册将于 9 月下旬开放。


# FreeBSD 中的 NVMe-oF

* 原文链接：[NVMe Over Fabrics in FreeBSD](https://freebsdfoundation.org/our-work/journal/browser-based-edition/storage-and-filesystems/nvme-over-fabrics-in-freebsd-2/)
* 作者：John Baldwin

**NVM Express (NVMe) 是一种新兴的非易失性存储设备（如固态硬盘）访问标准。**

NVMe 最初是为了通过 PCIe 访问非易失性内存设备而定义的。它包括用于 PCIe 控制器设备的寄存器定义、存储在主内存中的命令提交和完成队列的布局与结构，以及一组命令。

NVMe 基本规范定义了管理员命令集，用于与每个控制器关联的单个管理员提交和完成队列对进行操作。管理员命令不处理 I/O 请求，而是用于创建 I/O 队列、获取辅助数据（如错误日志）、格式化存储设备等。NVMe 中的存储设备称为命名空间，特定命名空间的命令包括命名空间 ID。基本规范还定义了 NVM 命令集，用于对面向块的命名空间执行 I/O 请求。该规范设计为可扩展，支持后续的扩展，包括额外的 I/O 命令集（例如，针对键值存储的 I/O 命令集）。NVMe 控制器及其附加的命名空间一起被称为 NVM 子系统。

**NVMe-oF（NVMe over Fabrics）** 扩展了原始规范，使其能够通过网络传输（而非 PCIe）访问 NVM 子系统，这类似于使用 iSCSI 访问远程块存储设备作为 SCSI LUN。NVMe-oF 支持多种传输层，包括 FibreChannel、RDMA（通过 iWARP 和 ROCE）以及 TCP。为了处理这些不同的传输，Fabrics 包括了对基本 NVMe 规范的传输无关扩展，以及传输特定的绑定。

Fabrics 定义了一种新的 capsule 抽象，用于支持 NVMe 命令和 completion。每个 capsule 包含一个 NVMe 命令或 completion。此外，capsule 可能与数据缓冲区相关联。为了支持数据传输，NVMe 命令中现有的 PRP 条目被单个 NVMe SGL 条目替代。Fabrics 还将用于 PCIe 控制器的共享内存队列替换为逻辑完成和提交队列。与 PCIe I/O 队列不同，Fabrics 队列总是显式配对，每个提交队列都与一个专用的完成队列相连。capsule 和数据缓冲区如何在队列对上传输和接收是传输特定的，但从抽象的角度来看，命令 capsule 在提交队列上传输，completion 则在完成队列上传输。

Fabrics 主机创建一个管理员队列对和一个或多个 I/O 队列对，连接到控制器。完整的队列对集称为关联。单个关联可以包含多个传输特定的连接。例如，TCP 传输为每个队列对使用专用连接，因此一个活动的 TCP 关联至少需要两个 TCP 连接。

除了提供对命名空间访问的 I/O 控制器外，Fabrics 还添加了一种发现控制器类型。发现控制器支持一个新的发现日志页面，描述 Fabrics NVM 子系统中可用的控制器集。日志页面可能包含一个或多个 I/O 控制器、指向其他子系统中发现控制器的引用。如果控制器可以通过多个传输访问，则日志页面可能包含该控制器的多个条目。每个日志页面条目包含控制器类型（I/O 或发现）以及传输类型和传输特定地址。对于 TCP 传输，地址包括 IP 地址和 TCP 端口号。

通过 NVMe 合格名称（NQN）标识 Fabrics 主机和控制器。NQN 是个 ASCII 字符串，应以“nqn.YYYY-MM.reverse-domain”开头，后跟可选后缀。名称的 reverse-domain 部分应该是一个有效的 DNS 名称的逆序，YYYY 和 MM 字段应该指定使用该前缀的组织拥有该 DNS 域名时的有效年份和月份。规范定义了一个固定的子系统 NQN 用于发现控制器，并且定义了从 UUID 构建 NQN 的方案。在建立关联时，必须指定主机和子系统（控制器）的 NQN。

FreeBSD 15 支持通过主机内核驱动程序访问远程命名空间，以及支持将本地存储设备导出为命名空间到远程主机。Fabrics 的内核实现包括一个传输抽象层（由 `nvmf_transport.ko` 提供），用于对主机和控制器模块隐藏大部分传输特定的细节。这个模块会根据需要自动加载。独立的内核模块提供对各个传输的支持。这些模块必须显式加载才能启用传输。目前，FreeBSD 能通过 `nvmf_tcp.ko` 提供 TCP 传输的支持。可以在 [**nvmf\_tcp(4)**](https://man.freebsd.org/nvmf_tcp/4) 中查看 TCP 相关的细节。

## 主机

FreeBSD 中的 Fabrics 主机内置了新的 [**nvmecontrol(8)**](https://man.freebsd.org/nvmecontrol/8) 命令和内核驱动程序 [**nvmf(4)**](https://man.freebsd.org/nvmf/4) 。内核驱动程序将远程控制器暴露为类似于 PCIe NVMe 控制器的新总线设备 nvmeX。远程命名空间通过磁盘设备 [**nda(4)**](https://man.freebsd.org/nda/4) 以 CAM 暴露。与 PCIe 的驱动程序 [**nvme(4)**](https://man.freebsd.org/nvme/4) 不同，Fabrics 主机驱动程序不支持磁盘驱动程序 [**nvd(4)**](https://man.freebsd.org/nvd/4)。所有新的 **nvmecontrol(8)** 命令都使用从主机 UUID 生成的主机 NQN，除非显式指定主机 NQN。

### 发现服务

**nvmecontrol(8)** 的命令 `discover` 查询来自发现控制器的发现日志页面，并显示其内容。示例 1 显示了来自在 Linux 系统上运行的 Fabrics 控制器的日志页面。对于 TCP 传输，由服务标识符字段标识远程控制器的 TCP 端口。

**示例 1：来自 Linux 控制器的发现日志页面**

```sh
# nvmecontrol discover ubuntu:4420
Discovery
=========
Entry 01
========
Transport type: TCP
 Address family:       AF_INET
Subsystem type:        NVMe
 SQ flow control:      optional
 Secure Channel:       Not specified
 Port ID:              1
 Controller ID:        Dynamic
 Max Admin SQ Size:    32
 Sub NQN:              nvme-test-target
 Transport address:    10.0.0.118
 Service identifier:   4420
 Security Type:        None
```

### 连接到 I/O 控制器

**nvmecontrol(8)** 的 `connect` 命令与远程控制器建立关联。建立关联后，关联会传递给驱动程序 **nvmf(4)**，后者会创建一个新的设备 `nvmeX`。`connect` 命令需要远程控制器的网络地址和子系统 NQN。**示例 2** 连接到 **示例 1** 中列出的 I/O 控制器。

**示例 2：连接到 I/O 控制器**

```sh
# kldload nvmf nvmf_tcp
# nvmecontrol connect ubuntu:4420 nvme-test-target
```

建立关联后，内核会将图 1 中的文本输出到系统控制台和系统信息缓冲区。设备 `nvmeX` 在设备描述中包括远程子系统 NQN，每个远程命名空间都枚举为外设 `ndaX`。

**图 1：连接时的控制台信息**

```sh
nvme0: <Fabrics: nvme-test-target>
nda0:  at nvme0 bus 0 scbus0 target 0 lun 1
nda0: <Linux 5.15.0-8 843bf4f791f9cdb03d8b>
nda0:   Serial Number 843bf4f791f9cdb03d8b
nda0:  nvme version 1.3
nda0:  1024MB (2097152 512 byte sectors)
```

来自图 1 的 `nvme0` 设备能与其他 **nvmecontrol(8)** 命令一道使用，如 `identify`，类似于 PCIe 控制器。**示例 3** 展示了 **nvmecontrol(8)** 输出的部分控制器标识数据。能像任何其他 NVMe 磁盘设备一样使用磁盘设备 `nda0`。

**示例 3：标识远程 I/O 控制器**

```sh
# nvmecontrol identify nvme0
Controller Capabilities/Features
================================
...
Model Number:                Linux
Firmware Version:            5.15.0-8
...

Fabrics Attributes
==================
I/O Command Capsule Size:    16448 bytes
I/O Response Capsule Size:   16 bytes
In Capsule Data Offset:      0 bytes
Controller Model:            Dynamic
Max SGL Descriptors:         1
Disconnect of I/O Queues:    Not Supported
```

### 通过发现连接

**nvmecontrol(8)** 的命令 `connect-all` 从指定的发现控制器获取发现日志页面，并为每个日志页面条目创建关联。**示例 2** 中的关联可以通过执行 `nvmecontrol connect-all ubuntu:4420` 来创建，而不需要先获取发现日志页面再使用 `connect` 命令。

### 断开连接

**nvmecontrol(8)** 的命令 `disconnect` 从远程控制器断开命名空间并销毁关联。**示例 4** 断开 **示例 2** 中创建的关联。`disconnect-all` 命令销毁与所有远程控制器的关联。

**示例 4：断开与远程 I/O 控制器的连接**

```sh
# nvmecontrol disconnect nvme0
```

### 重新连接

如果连接中断（例如，一个或多个 TCP 连接失败），活动关联会被拆除（所有队列都会断开连接），但 `nvmeX` 设备会保持静止状态。所有挂起的远程命名空间的 I/O 请求也会保持挂起。在这种状态下，可以使用 `reconnect` 命令重新建立关联，以恢复与远程控制器的操作。**示例 5** 重新连接到 **示例 2** 中的控制器。请注意，`reconnect` 命令与 `connect` 命令类似，需要显式指定网络地址。

**示例 5：重新连接到远程 I/O 控制器**

```sh
# nvmecontrol reconnect nvme0 ubuntu:4420
```

## 控制器

FreeBSD 上的 Fabrics 控制器将本地块设备作为 NVMe 命名空间暴露给远程主机。FreeBSD 上的控制器支持包括发现控制器的用户空间实现和内核中的 I/O 控制器。与 FreeBSD 中现有的 iSCSI 目标类似，内核中的 I/O 控制器使用 CAM 的目标层 ([**ctl(4)**](https://man.freebsd.org/ctl/4))。块设备是通过使用 [**ctladm(8)**](https://man.freebsd.org/ctladm/8) 添加 **ctl(4)** LUN 创建的。发现服务和 I/O 控制器连接的初步处理由守护进程 [**nvmfd(8)**](https://man.freebsd.org/nvmfd/8) 管理。内核中的 I/O 控制器由 [**nvmft(4)**](https://man.freebsd.org/nvmft/4) 模块提供。**示例 6** 将名为 `pool/lun0` 的 ZFS 卷作为 **ctl(4)** LUN 添加，再启动守护进程 **nvmfd(8)** 。远程主机可以将此 ZFS 卷作为 NVMe 命名空间进行访问。

**示例 6：导出本地 ZFS 卷**

```sh
# kldload nvmft nvmf_tcp
# ctladm create -b block -o file=/dev/zvol/pool/lun0
LUN created successfully
backend:        block
device type:   0
LUN size:      4294967296 bytes
blocksize      512 bytes
LUN ID:        0
Serial Number: MYSERIAL0000
Device ID:     MYDEVID0000
# nvmfd -F -p 4420 -n nqn.2001-03.com.chelsio:frodo0 -K
```

每当远程主机连接到 I/O 控制器时，内核都会记录一条日志，列出远程主机的 NQN（如图 2 所示）。

**图 2：新关联的日志消息**

```sh
nvmft0: associated with
nqn.2014-08.org.nvmexpress:uuid:00000000-0000-0000-0000-ffffffffffff
```

在 **nvmfd(8)** 运行时，可以通过 **ctladm(8)** 添加和删除 LUN。如果在添加和删除 LUN 时有远程主机连接，则会向远程主机报告异步事件。这使得远程主机能够在连接的同时注意到命名空间的添加和删除。

**ctladm(8)** 添加了两个新命令来管理 Fabrics 关联。`nvlist` 命令列出所有来自远程主机的活动关联。当一个主机连接到 **示例 6** 中的控制器时，**示例 7** 显示了 `nvlist` 命令的输出。

**示例 7：列出活动关联**

```sh
# ctladm nvlist
  ID Transport        HostNQN                              SubNQN
   0 TCP
nqn.2014-08.org.nvmexpress:uuid:00000000-0000-0000-0000-ffffffffffff
nqn.2001-03.com.chelsio:frodo0
```

`nvterminate` 命令关闭一个或多个关联。可以终止单个连接或 NQN 的关联，也可以终止所有活动关联。**示例 8** 终止了 **示例 7** 中的关联。关联终止后，内核记录图 3 中的日志消息。

**示例 8：终止关联**

```sh
# ctladm nvterminate -c 0
NVMeoF connections terminated
```

**图 3：终止后的日志消息**

```sh
nvmft0: disconnecting due to administrative request
nvmft0: association terminated
```

## 结论

NVMe-oF 支持（包括主机和控制器功能）将在 FreeBSD 15.0 中提供。Fabrics 功能的开发由 Chelsio Communications, Inc. 赞助。

***

**John Baldwin** 是一名系统软件开发人员，已在 FreeBSD 操作系统中直接提交了二十余年的修改，涉及内核的各个部分（包括 x86 平台支持、SMP、各种设备驱动程序和虚拟内存子系统）及用户空间程序。除了编写代码，John 还曾在 FreeBSD 核心和发布工程团队中工作，并为 GDB 调试器做出了贡献。John 住在加州的康科德，与妻子 Kimberly 和三个孩子 Janelle、Evan 和 Bella 一起生活。


# FreeBSD iSCSI 入门

* 原文链接：[FreeBSD iSCSI Primer](https://freebsdfoundation.org/our-work/journal/browser-based-edition/storage-and-filesystems/freebsd-iscsi-primer/)
* 作者：Jason Tubnor

我们经常听说网络附属存储（NAS）能够为网络上的设备提供额外的存储空间。然而，这种存储的协议可能并不适用于所有的使用场景。

欢迎来到存储区域网络（SAN）的世界。一般来说，这些存储系统较多地出现在企业环境中，而不是家庭和小型企业，但这并不意味着它们不应该在这种情况下使用。实际上，如果你进行了大量虚拟化，且需要将中央存储接入到多个计算设备，或者需要为用于工程和图形设计的 Windows 工作站提供块存储，这些工作站的存储需求超过了桌面 PC 的物理存储空间，那么你可能会发现 SAN 极为有用。

一般来说，在企业领域我们听到的 SAN 供应商是戴尔 EMC、IBM、日立和 NetApp 等。然而，我们在 FreeBSD 环境中可以得到一款内置于基本系统的高性能 iSCSI SAN 解决方案，实在是让我们倍感幸运。通过与强大的 ZFS 卷管理器和文件系统相结合，我们能得到灵活、可靠且快速的存储解决方案，并将其提供给网络客户端。iSCSI 子系统实现于 FreeBSD 10.0-RELEASE，并在 10.1-RELEASE 中有了诸多性能改进。

iSCSI（Internet Small Computer Systems Interface，互联网小型计算机系统接口）是一种基于 IP 的协议，用于通过 TCP/IP 以太网网络传输 SCSI 命令。它能将块设备存储呈现给分布在网络上的计算机。它足够灵活，还可以通过互联网路由（如需要），但安全问题和需要考虑的预防措施超出了本文的讨论范围。

在理想情况下，iSCSI 应该存在于一个独立的二层物理网络中，在这个网络中，计算主机通过专用存储接口与存储目标的通信是唯一的通信。这个网络段上不应有其他普通的网络流量，以避免存储与其他计算节点之间的带宽争用。在较简单的环境中，使用带 VLAN 的分段交换机是一种可选的替代方案，但需要明白，普通网络流量将和存储流量争用接口带宽。

iSCSI 的高层术语相当简单。它有发起方（客户端）和目标方（主机）。发起方是连接的主动端，而目标方是被动端——它永远不会主动连接到发起方。

在接下来的操作中，我们将准备一个简单的 iSCSI 配置，在 FreeBSD 主机上使用它作为目标方，并为 FreeBSD 发起方和 MS Windows 发起方提供 ZFS `zvol` 块设备。

我们的网络主机如下：

```sh
2001:db8:1::a/64 – FreeBSD ZFS 存储主机（目标方） 
2001:db8:1::1/64 – FreeBSD 客户端（发起方）
2001:db8:1::2/64 – Windows Server 2022（发起方）
```

首先，我们将在存储主机上配置 ZFS 卷作为 iSCSI 目标方，提供给每个发起方。这也可以是简单的 ZFS 数据集或 UFS 分区上的文件，但 ZFS 卷提供了更多对存储方面的控制，尤其是在数据需求变化以及在快照和复制要求方面的持续管理。

```sh
zfs create -o volmode=dev -V 50G tank/fblock0
zfs create -o volmode=dev -V 50G tank/wblock0
```

在创建卷时，可以根据工作负载的需求调整属性 `volblocksize`。自 14.1-RELEASE 以降，`volblocksize` 默认为 16K，这对大多数工作负载来说是一个合理的平衡。

接下来，创建一个仅允许 root 读/写的文件 `/etc/ctl.conf`。此文件包含密钥，因此必须确保其他用户和组没有读/写该文件的权限。下面是我们将添加的内容，用于提供发起方的存储位置：

```ini
auth-group ag0 {
    chap-mutual "inituser1" "secretpassw0rd" "targetuser1" "topspassw0rd"
initiator-portal [2001:db8:1::1]
}

auth-group ag1 {
    chap-mutual "inituser2" "hiddenpassw0rd" "targetuser2" "freepassw0rd"
    initiator-portal [2001:db8:1::2]
}

portal-group pg0        {
    discovery-auth-group no-authentication
    listen [2001:db8:1::a]
}

target iqn.2012-06.org.example.iscsi:target1 {
    alias "Target for FreeBSD"
    auth-group ag0
    portal-group pg0
    lun 0 {
        path /dev/zvol/tank/fblock0
        #blocksize 4096
        option naa 0x4ee0ebaf06a1acee
        option pblocksize 4096
        option ublocksize 4096
    }
}

target iqn.2012-06.org.example.iscsi:target2 {
    alias "Target for Windows"
    auth-group ag1
    portal-group pg0
    lun 1 {
        path /dev/zvol/tank/wblock0
        blocksize 4096
        option naa 0x4ee0ebaf06a1acbb
    }
}
```

我们来分解一下这个文件，理解每个组件的作用及原因：

```ini
auth-group ag0 {
    chap-mutual "inituser1" "secretpassw0rd" "targetuser1" "topspassw0rd"
initiator-portal [2001:db8:1::1]
}
```

这是一个授权组，可以跨多个目标使用。这个例子将 `auth-group` 绑定到一个具有地址 `2001:db8:1::1` 的唯一发起方，并要求相互身份验证。你可以简单地使用 CHAP 身份验证作为“单向”认证；但建议若支持，请使用相互身份验证，以确保发起方和目标方双方都能正确地相互认证。

这种身份验证可能足够用于发起方和目标方位于同一物理网络的情况，但不应将其作为唯一的安全控制手段。此类身份验证应被视为确保仅分配正确存储给发起方的方法。配置松散的 iSCSI 目标可能会使错误的存储可用，可能会造成数据、分区表或其他元数据被覆盖，因此，限制访问特定目标数据集至关重要。

```ini
portal-group pg0        {
    discovery-auth-group no-authentication
    listen [2001:db8:1::a]
}
```

门户组设置了提供给发起方的目标环境。在这里，我们定义了一个门户组，允许发起方通过 `2001:db8:1::a` 连接到目标方，这样它们可以在无需先身份验证的情况下发现包括此门户组的目标数据集。通常在受控环境中，可以这样做，以确保发起方能够找到它们需要连接的目标，但在更加敌对或不受信任的环境中，这种做法则是不理想的。

```ini
target iqn.2012-06.org.example.iscsi:target1 {
    alias "Target for FreeBSD"
    auth-group ag0
    portal-group pg0
    lun 0 {
        path /dev/zvol/tank/fblock0
        #blocksize 4096
        option naa 0x4ee0ebaf06a1acee
        option pblocksize 4096
        option ublocksize 4096
    }
}
```

配置的核心部分是目标。这将包括 `auth-group` 和 `portal-group`，用来构建之前介绍的各个组件，并呈现给发起方。它们可按每个目标覆盖，即使没有适用的 `group` 定义也可定义。

iSCSI 合格名称（IQN）的格式为 `iqn.yyyy-mm.namingauthority:uniquename`。每个目标的定义都需要这个。

别名定义只是一个用于目标的可读简介。

每个目标可以有多个 LUN，但这里每个目标只有一个 LUN。

LUN 上下文允许你定义 LUN 的特征。对于在发起方内的呈现存储上使用 ZFS 的情况，这一点非常重要。尽管目标上的 ZFS 卷的块大小为 16KB，但当在发起方上创建 ZFS 池时，它会报错存储的块大小不是 4KB 或更小。它不会阻止你使用它，但当你在发起方执行 `zpool status` 时，一条消息会不断提醒你此问题。调整块大小属性为 4096 并不足以解决此问题。需要专门为 ZFS 使用案例添加选项 `pblocksize` 和 `ublocksize` 。

选项 `naa` 应为 LUN 明确定义。这是一个 64 位或 128 位的唯一十六进制标识符。在将 VMware 计算备份到 iSCSI 目标时，确保没有混淆 LUN 分配，这是非常重要的。

现在，你已经具备了启动并展示目标存储的基本配置。接下来，启用 `ctld`，再启动守护进程：

```sh
service ctld enable
service ctld start
```

为了验证存储是否已出现，你可以检查守护进程是否在监听：

```sh
# netstat -na | grep 3260
tcp6    0      0 2001:db8:1::a.3260     *.*     LISTEN
```

然后，使用 CAM 目标层控制实用程序验证 LUN 是否已出现：

```sh
# ctladm lunlist
(7:1:0/0):<FREEBSD CTLDISK 0001> Fixed Direct Access SPC-5 SCSI device
(7:1:1/1):<FREEBSD CTLDISK 0001> Fixed Direct Access SPC-5 SCSI device
# ctladm devlist
LUN Backend     Size (Blocks)   BS Serial Number     Device ID
  0 block           104857600   512 MYSERIAL0000   MYDEVID0000
  1 block            13107200  4096 MYSERIAL0001   MYDEVID0001
```

接下来，配置你的 FreeBSD 发起方。你需要创建配置文件 `/etc/iscsi.conf`。由于此文件包含机密信息，因此也需要显式设置为仅 root 可读写：

```ini
fblock0         {
targetaddress   = [2001:db8:1::a];
targetname      = iqn.2012-06.org.example.iscsi:target1;
initiatorname   = iqn.2012-06.org.example.freebsd:nobody;
authmethod      = CHAP;
chapiname       = "inituser1";
chapsecret      = "secretpassw0rd";
tgtChapName     = "targetuser1";
tgtChapSecret   = "topspassw0rd";
}
```

每个属性的含义如下：

* `fblock0`：这是一个人类可读的标识符，与任何内容无关，仅用于将以下配置项分组。
* `targetaddress`：存储目标的网络地址。也可以是一个完全合格的域名。
* `targetname`：这将与在 `ctl.conf` 文件中定义的相应目标名称对齐。
* `initiatorname`：定义发起方的 IQN。
* `authmethod`：在 FreeBSD 中可以简单地定义为 CHAP。如果 `ChapName` 和 `ChapSecret` 以 `tgt` 为前缀，则会假定使用相互身份验证。
* `chapiname/chapsecret`：如前所述，在 `ctl.conf` 文件中定义的身份验证。
* `tgtChap[Name,Secret]`：目标完成与发起方身份验证握手所需的身份验证。

要启用，只需执行以下命令：

```sh
service iscsid enable
service iscsictl enable
service iscsid start
service iscsictl start
```

这会将目标的存储呈现附加到发起方：

```sh
# iscsictl -L
Target name                           Target portal    State
iqn.2012-06.org.example.iscsi:target1 [2001:db8:1::a]  Connected: da0
```

在此，我们来看一下 `iscsictl` 命令中最常用的简单选项：

* `L` 列出已挂载到发起方的目标及其连接状态。
* `Aa` 附加在 `iscsi.conf` 文件中定义的所有目标。
* `Ra` 移除所有已连接到发起方的目标。

接下来，执行以下操作：

```sh
# gpart create -s GPT da0
da0 created
# gpart add -t freebsd-zfs -a 1M da0
da0p1 added
# zpool create tank da0p1
# zpool list tank
NAME   SIZE  ALLOC   FREE  CKPOINT  EXPANDSZ   FRAG    CAP  DEDUP    HEALTH  ALTROOT
tank  49.5G   360K  49.5G        -         -     0%     0%  1.00x    ONLINE        -
# zpool status tank
  pool: tank
 state: ONLINE
config:

        NAME        STATE     READ WRITE CKSUM
        tank        ONLINE       0     0     0
          da0p1     ONLINE       0     0     0

errors: No known data errors
```

配置、守护进程和控制工具的手册页写得非常详细，可以参考这些手册来更好地了解可用的其他功能。

这仅触及了 FreeBSD 中 iSCSI 实现的全部功能的一部分，但它为你提供了一个概念和实际示例，展示了如何利用它为你的计算基础设施提供灵活的远程存储选择。

***

**Jason Tubnor** 拥有逾 28 年的 IT 行业经验，广泛涉猎，目前是 Latrobe Community Health Service（澳大利亚维多利亚州）的 ICT 高级安全主管。在 1990 年代中期，Jason 接触到 Linux 和开源技术，2000 年开始使用 OpenBSD，他利用这些工具在各个行业的组织中解决了各种问题。Jason 还是 BSDNow 播客的联合主持人。


# 使用 ZFS 原生加密保护数据

* 原文链接：[Protecting Data with ZFS Native Encryption](https://freebsdfoundation.org/our-work/journal/browser-based-edition/storage-and-filesystems/protecting-data-with-zfs-native-encryption/)
* 作者：Roller Angel

![ZFS 加密文章配图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-f7e0ca2e91e8b9190ee4ebabe8e242f68b85619a%2Fzfs-1.jpg?alt=media)

ZFS 原生支持加密数据集，能让你轻松地使用行业标准的密码套件来保护数据。与磁盘的全盘加密相比，将数据集加密的主要优势在于，当未使用数据集时，可以将其卸载，而全盘加密要求磁盘关闭，以便在静止状态下加密数据。请记住，ZFS 原生加密有加载和卸载密钥的概念。仅仅卸载加密的数据集是不够的，你还必须卸载与该数据集关联的密钥。如果密钥仍然处于加载状态，数据集可以被挂载并且数据将可用。卸载密钥会使挂载操作失败。加载密钥是挂载数据集的前提。嵌套的子数据集会继承其父数据集的加密密钥，但这并非必须。即使父数据集使用不同的加密设置，也可以使用不同的加密密钥和密码套件。最后，更改密钥就像在数据集上执行 `zfs change-key` 命令一样简单。

这些是开始使用的基础概念。

为新创建的数据集启用加密参数，再设置密钥格式就足够了。如果未指定加密密码套件，则默认使用 aes-256-gcm。默认值可能会随着未来新增密码套件而变化。现有数据集的加密属性是只读的，无法修改未加密的数据集的属性来启用加密。要指定加密属性，你需要了解有哪些选项可用。我建议阅读 `zfsprops` 手册页，你可以输入命令 `man zfsprops` 来查看。我还建议阅读 `zfs-load-key` 的手册页。对于我们的第一个加密数据集，我们将使用默认的密码套件、口令密钥格式，来创建一个名为 `secrets` 的数据集。我使用的是我实验室中为本文创建的 FreeBSD jail 机器，名为 alice。实验室中的所有 jail 都位于名为 `lab` 的 zpool 上。我已将名为 `zroot` 的 zpool 分配给 jail。在 jail 中，我必须使用完整路径 `lab/alice/zroot` 作为 zpool 名称，以便在其中创建数据集。作为对比，我的笔记本电脑上，我可以直接使用我的 zpool 名称并在那里创建数据集。以下是创建加密数据集的命令，适用于 alice jail 和我的笔记本电脑。同其他 ZFS 数据集一样，设置挂载点是一个好主意，但请记住 ZFS 是个分层文件系统，因此不要使用现有路径作为新数据集的挂载点。

alice jail:

```sh
zfs create -o encryption=on -o keyformat=passphrase -o mountpoint=/secrets
lab/alice/zroot/secrets
```

我的笔记本：

```sh
zfs create -o encryption=on -o keyformat=passphrase -o mountpoint=/secrets zroot/secrets
```

在运行 `zfs create` 命令后，提示我输入一个足够长的口令。现在，我有了一个已挂载的加密数据集，可以在其中存储需要保护的数据。当数据集处于挂载状态时，我可以像使用其他未加密的数据集一样使用它。当我完成机密数据的添加后，我可以通过输入命令 `zfs unmount -u lab/alice/zroot/secrets` 一次性卸载数据集和密钥。要解密、重新挂载数据，只需运行命令 `zfs mount -l lab/alice/zroot/secrets`。这会提示我输入口令，加载密钥，然后挂载数据集。如果在卸载命令中省略选项 `-u`，只会卸载数据集，密钥仍然会保持加载状态。数据集仍然可以通过 `zfs mount lab/alice/zroot/secrets` 挂载，而无需输入口令。要在数据集已卸载后卸载密钥，我运行 `zfs unload-key lab/alice/zroot/secrets`。现在，之前的挂载命令将失败，因为密钥没有加载，并且我未提供选项 `-l` 来让 ZFS 在挂载之前加载密钥。要加载密钥且允许挂载数据集，我会运行 `zfs load-key lab/alice/zroot/secrets`。系统会提示我输入口令，之前的挂载命令现在会成功，因为密钥已经加载。要检查密钥是否已加载，可以查看数据集的属性。当我运行命令 `zfs list -o name,mountpoint,encryption,keylocation,keyformat,keystatus,encryptionroot lab/alice/zroot/secrets` 时，会显示一些有用的属性。`KEYSTATUS` 列显示为 `available` 时，表示密钥已加载。要查看所有数据集属性，可以使用 `zfs get all lab/alice/zroot/secrets`。

接下来，我在 `secrets` 数据集下创建一个嵌套的数据集，并使用不同的密码套件和密钥格式。这次，我将使用密钥文件而非口令。要创建使用密钥文件的数据集，我首先需要生成密钥并将其存储在文件中。我通过输入命令 `dd if=/dev/urandom bs=32 count=1 of=/media/more-secrets.key` 来完成。由于密钥文件要求长度为 32 字节，因此我使用了 `bs=32`。输出路径选择 `/media`，因为我在此路径下挂载了一个便携式 USB 驱动器，还使用 `dd` 命令生成了密钥文件，再将其直接存储到驱动器上。这样，当我卸载密钥并卸载和移除 USB 驱动器时，密钥文件就不会留在我的机器上。我建议将密钥文件存储在多个 USB 驱动器上，以防某个 USB 驱动器损坏。现在，密钥文件已经生成，我可以通过运行命令 `zfs create -o encryption=aes-256-ccm -o keyformat=raw -o keylocation=file:///media/more-secrets.key -o mountpoint=/secrets/more-secrets lab/alice/zroot/secrets/more-secrets` 来创建使用 AES-256-CCM 密码套件的嵌套数据集。查看这个新数据集的属性时，我可以看到 `ENCROOT` 列设置为 `lab/alice/zroot/secrets/more-secrets`。我可以使用与 `secrets` 数据集相同的方法来卸载数据集和卸载密钥。当我拥有更多数据集和密钥时，我可能想考虑使用 `zfs unload-key -a` 卸载所有密钥，或使用 `zfs unload-key -r lab/alice/zroot/secrets` 来仅卸载 `secrets` 数据集及其所有后代数据集的密钥。相反，如果我想加载 `secrets` 数据集及其所有后代数据集的密钥子集，可以运行 `zfs load-key -r lab/alice/zroot/secrets`。

如果我后来决定 `more-secrets` 数据集中的数据不需要单独的密钥文件，而是希望它继承父数据集 `secrets` 的设置（即从自定义生成的密钥文件切换到之前配置的口令），我只需要运行命令 `zfs change-key -i lab/alice/zroot/secrets/more-secrets`。再次查看属性，注意到 `ENCROOT`、`KEYLOCATION` 和 `KEYFORMAT` 都已经发生了变化。然而，密码套件不会改变，因为密码套件只能在数据集创建时设置。由于 `more-secrets` 是包含在 `secrets` 中的，它将作为卸载 `secrets` 数据集的一部分被卸载。虽然挂载 `secrets` 数据集不会自动挂载 `more-secrets`，但后者需要单独挂载。不过，由于它们共享相同的密钥，所以密钥只需要加载一次。要切换回使用密钥文件，我运行命令 `zfs change-key -o keyformat=raw -o keylocation=file:///media/more-secrets.key lab/alice/zroot/secrets/more-secrets`。如果我想永久销毁 `more-secrets` 中的数据，我只需卸载数据集、卸载密钥，并销毁密钥文件及我所做的任何备份副本。现在，数据将无法恢复。然后，我可以运行命令 `zfs destroy lab/alice/zroot/secrets/more-secrets` 来移除该数据集。

最后，想谈谈加密数据的备份。如你所见，ZFS 原生加密能轻松地使用加密来保护数据。加密数据集的快照可以在不受信任的备份服务器上以加密形式接收。没有密钥，远程备份服务器就无法挂载该数据集。可以使用 `zfs send` 命令的选项 `--raw` 来实现这一点。有关更多细节，我建议阅读 `zfs-send` 的手册页，以了解其工作原理，然后获取《ZFS Mastery: Advanced ZFS》一书，深入研究具体细节，并学习一系列技巧，以进一步提升你的 ZFS 技能。

希望你喜欢这篇操作指南，并且开始使用出色的 ZFS 文件系统提供的原生加密来保护你的敏感数据。

***

**Roller Angel** 大部分时间都在帮助人们学习如何使用技术实现他们的目标。他是一位热心的 FreeBSD 系统管理员和 Python 爱好者，喜欢学习开源技术（尤其是 FreeBSD 和 Python）解决问题的神奇方法。他坚信人们可以学会他们下决心想学的任何事情。Roller 总是在寻找创造性的解决方案，享受解决问题的挑战。他有很强的学习动机，喜欢探索新想法，并保持技能的敏锐。他喜欢参与研究社区，并分享自己的想法。


# 嵌入式 FreeBSD：打造自己的镜像

* 原文链接：[Rolling Your Own Images](https://freebsdfoundation.org/our-work/journal/browser-based-edition/storage-and-filesystems/embedded-freebsd-rolling-your-own-images/)
* 作者：Christopher R. Bowman

![嵌入式 FreeBSD](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-4bb7c960a50272848ebdd22949d3c5bd8df36c5c%2Ffreebsd-1.png?alt=media)

在上一卷中，我粗略提及了我用的开发板，Digilent 的 [ARTYZ7](https://digilent.com/shop/zedboard-zynq-7000-arm-fpga-soc-development-board/)。在本卷，我将讨论如何制作自己的镜像。到某个时候，你可能会需要一个与现有镜像略有差异的镜像，因此你可能想从源代码开始构建、创建镜像来写入 SD 卡。

首先，让我们了解一下 [ARTYZ7](https://digilent.com/shop/zedboard-zynq-7000-arm-fpga-soc-development-board/) 如何启动。 [Zynq-7000 SoC 技术参考手册](https://docs.xilinx.com/v/u/en-US/ug585-Zynq-7000-TRM) 是一本宝贵的技术信息来源，第 6 章讲述了芯片及所有 Zynq 主板的启动过程。这里有很多技术细节，但根据默认配置的跳线设置，ARTYZ7 是从 SD 卡启动的。你可能已经下载查看过我在上一卷中提供的镜像。如果查看过，你会发现它是以 MBR 模式格式化的，首先是 FAT 分区，第二个 MBR 分区上是一个带有 UFS 的 FreeBSD 切片。处理器将查找 MBR，并寻找 FAT16/FAT32 分区。它会在 FAT 分区中查找文件 `boot.bin`。若找到，它会将其加载到内存中，开始执行。如果你想要直接编程到裸机，可以将你的应用程序写入，命名为 `boot.bin`。对我来说这有些复杂。因此，当启动 FreeBSD 时，我们使用了一个第一阶段引导加载程序，像许多嵌入式开发板一样，我们使用 [Das U-Boot](https://en.wikipedia.org/wiki/Das_U-Boot)。U-Boot 是一款由社区维护的开源引导加载程序。在我们的用例中，使用了两次 U-Boot。首先，U-Boot 被编译为最小的第一阶段引导加载程序（FSBL），它设置硬件，然后查找第二阶段引导加载程序，第二阶段引导加载程序也是 U-Boot，但功能更加丰富。在我们的使用中，第二阶段的 U-Boot 位于 FAT 分区中名为 `U-boot.img` 的文件中。这个 U-Boot 第二阶段加载程序会从 FAT 分区中的 `EFI/BOOT/bootarm.efi` 文件加载 lua 加载器。然后，lua 加载器会将内核等文件加载到内存中再执行。

所以，如果我们要构建镜像，我们需要创建分区，再把 U-Boot 和 FreeBSD 安装到 SD 卡上。

构建 U-Boot 相对直接。虽然有许多 U-Boot 的移植版本用于各种板子，但 ARTYZ7 并没有现成的移植版本。我已经创建了一个移植版本，你能在 [这里](http://www.chrisbowman.com/crb/ArtyZ7/u-boot_ports/patches.html) 找到。虽然我还没有把它纳入 FreeBSD 的 Ports 中，但你可以直接将其放入最新的 Ports 目录 `/usr/ports/sysutils` 下。 [FreeBSD 手册第 4.5 章](https://docs.freebsd.org/en/books/handbook/ports/#ports-using) 提供了非常详细的安装和构建 Ports 的说明。你将该 Port 添加到你的 Ports 中后，在 `sysinstall/u-boot-artyz7` 目录下简单地运行 `make`，应该就能自动下载构建 U-Boot。运行 `make install` 后，文件 `boot.bin` 和 `U-boot.img` 应该会出现在目录 `/usr/local/share/U-boot/U-boot-artyz7` 下。注意：我以前可以使用较大的“`-j`”值来让 `make` 使用多个核心进行构建，但在最新的 Ports（2024Q3）中，似乎无法正常工作。

从源代码构建也在 FreeBSD 手册中有很好的文档。在 [第 26.6 章：从源代码更新 FreeBSD](https://docs.freebsd.org/en/books/handbook/cutting-edge/#makeworld) 中，你将找到有关下载 FreeBSD 源代码和从中构建的详细信息。如果你已经在 `/usr/src` 安装了 FreeBSD 源代码，你可以直接进入该目录并运行以下命令：

```sh
# make buildworld
# make buildkernel KERNCONF=ARTYZ7
```

虽然这些命令应该能在 ARTYZ7 板上正常工作，毕竟它有完整的 FreeBSD 安装，但你应该做好准备，它会花费很长时间。由于 PC 硬件变得非常强大且价格低廉，我使用一台 AMD64 系统来托管所有文件、开发环境，并进行所有构建。FreeBSD 对交叉编译和构建提供了内置支持。在我的 PC 上，我使用以下命令让我的 PC 为基于 ARM 的 ARTYZ7 板卡进行源代码构建：

```sh
# make buildworld TARGET=arm TARGET_ARCH=armv7 -j32
# make buildkernel KERNCONF=ARTYZ7 TARGET=arm \ TARGET_ARCH=armv7 -j32
```

这将从 AMD64 构建一个交叉编译器到 ARMv7，并使用这个编译器来构建所有内容。

对于内核配置文件，我已将 `src/sys/arm/conf/ZEDBOARD` 中的 ZEDBOARD 配置文件复制到 `src/sys/arm/conf/ARTYZ7` 并在其中修改了名称以匹配。选项 `-j32` 使得编译过程可以使用最多 32 个进程来进行构建。在一台搭载高速固态硬盘的 AMD 5950x PC 上，构建世界大约需要 10 分钟，构建内核大约需要 60 秒。我无法想象在 ARTYZ7 上需要多少天来完成这个过程。

若你从源代码构建了所有内容，过程可能会有所不同，具体有以下几种方式：

1. 你可以将 SD 卡挂载到主机开发系统，并直接从主机安装到 SD 卡上。
2. 你可以使用 [`mdconfig`](https://man.freebsd.org/cgi/man.cgi?query=mdconfig\&apropos=0\&sektion=0\&manpath=FreeBSD+13.2-RELEASE+and+Ports\&arch=default\&format=html) 命令创建一个基于文件的内存设备。这使你可以将一个文件当作块设备来处理。你可以使用所有标准的 FreeBSD 工具来分区设备，并将分区挂载到文件系统中。从那里，你可以像操作物理设备一样进行安装，最终得到适合使用 [`dd`](https://man.freebsd.org/cgi/man.cgi?query=dd\&apropos=0\&sektion=0\&manpath=FreeBSD+13.2-RELEASE+and+Ports\&arch=default\&format=html) 复制到 SD 卡或提供给其他人的文件。
3. 最后一种方法，也是我正在使用并将在这里讨论的方法，是先在主机 PC 上的某个目录中进行安装，然后使用 [`mkimg`](https://man.freebsd.org/cgi/man.cgi?query=mkimg\&apropos=0\&sektion=0\&manpath=FreeBSD+13.2-RELEASE+and+Ports\&arch=default\&format=html) 和 [`makefs`](https://man.freebsd.org/cgi/man.cgi?query=makefs\&apropos=0\&sektion=0\&manpath=FreeBSD+13.2-RELEASE+and+Ports\&arch=default\&format=html) 从主机目录构建一个镜像，该镜像同样适合使用 [`dd`](https://man.freebsd.org/cgi/man.cgi?query=dd\&apropos=0\&sektion=0\&manpath=FreeBSD+13.2-RELEASE+and+Ports\&arch=default\&format=html) 复制到 SD 卡上。

接下来，我们更详细地考察方法 3。我通常会创建 `msdos` 目录和 `ufs` 目录，来表示我需要在 SD 卡上的两个分区：

```sh
# mkdir msdos ufs
```

接下来，我在 `ufs` 目录上进行安装：

```sh
# make installworld installkernel TARGET=arm \
TARGET_ARCH=armv7 -j32 DESTDIR=ufs
```

你还需要运行分发目标，该目标会在 `/etc` 中创建所有默认的配置文件。当你对一个工作系统进行源代码升级时，通常不会运行这个命令，因为它会覆盖你的配置文件，但在从零开始构建系统时，你确实需要默认的配置文件：

```sh
# make distribution TARGET=arm \
TARGET_ARCH=armv7 DESTDIR=ufs -j32
```

此时，我已经在 `ufs` 目录下完成了完整的安装，并可以对镜像进行一些自定义。例如，我可以定制 `ufs/etc/rc.conf`，以便自动启动以太网接口 `cgem0` 上的 DHCP 获取 IP 地址。我可以将 `ssh` 密钥安装到 `ufs/etc/ssh` 中，这样系统每次启动时都会使用相同的 `ssh` 密钥，而非在首次启动时生成新的密钥。由于我使用主机系统来进行所有构建并托管我的文件，我还喜欢配置 `/etc/fstab` 以通过 NFS 挂载我的主目录。

我发现创建一个用户账户非常方便，这样我可以立即登录：

```sh
# echo 'xxxx' | pw -R ${ufs} useradd -n crb -m -u 1001 \
-d /homes/crb -g crb -G 1001,wheel,operator\
-c "Christopher R. Bowman" -s /bin/tcsh -H 0
```

`pw` 命令的选项 `-R` 使得 `pw` 会编辑 `ufs/etc` 中的密码文件，而不是我主机系统中的文件。选项 `-H0` 能让我使用 `echo` 将密码通过管道传输给 `pw`，而无需交互式输入（你需要使用你主机系统中的编码密码替代 `xxxx`）。你也可能觉得修改 root 账户的密码更加方便，避免没有密码的情况。

现在，既然我已经按照希望的方式定制了 `ufs` 目录，我们就来关注一下 FAT 分区。

我需要安装 `boot.bin` 和 `U-boot.img`：

```sh
# cp /usr/local/share/U-boot/U-boot-artyz7/boot.bin msdos
# cp /usr/local/share/U-boot/U-boot-artyz7/U-boot.img msdos
```

`boot.bin` 会加载 `U-boot.img`，而 `U-boot.img` 模拟了 FreeBSD 加载器的 EFI 固件。EFI 系统会在 FAT 分区上查找一个名为 `EFI/BOOT/bootarm.efi` 的文件，因此我们需要将 FreeBSD 的 lua 加载器复制到该位置：

```sh
# mkdir -p msdos/ EFI/BOOT
# cp ufs/boot/loader_lua.efi msdos/EFI/BOOT/bootarm.efi
```

注意，我复制的是我们构建的 ARM 版本，而不是主机版本（后者是 AMD64 代码）。

现在，我们已经有了两个目录 `msdos` 和 `ufs`，它们包含了我们想要写入 SD 卡的文件。接下来我们只需要创建镜像文件。这是一个 4 步的过程：

```sh
makefs -t msdos \
  -o fat_type=16 \
  -o sectors_per_cluster=1 \
  -o volume_label=EFISYS \
  -s 32m \
  efi.part msdos

makefs -B little \
  -o label=rootfs \
  -o version=2 \
  -o softupdates=1 \
  -s 3g \
  rootfs.ufs ufs

mkimg -s bsd \
-p freebsd-ufs:=rootfs.ufs \
  -p freebsd-swap::1G \
  -o freebsd.part

mkimg -s mbr -f raw -a 1\
-p fat16b:=efi.part \
  -p freebsd:=freebsd.part \
  -o selfbuilt.img
```

第一个命令从 `msdos` 目录构建文件系统镜像文件 (`efi.part`)。第二个命令从 `ufs` 目录构建文件系统镜像文件 (`rootfs.ufs`)。第三个命令将 `rootfs.ufs` 文件组合成一个包含 1GB 交换分区的 FreeBSD 切片。最后一个命令将我们的 `efi.part` 和 `freebsd.part` 文件打包成一个单一的镜像文件 (`selfbuilt.img`)。

如果我将 SD 卡插入主机，我会看到一个 `/dev/da0` 设备，并使用简单的 [`dd`](https://man.freebsd.org/cgi/man.cgi?query=dd\&apropos=0\&sektion=0\&manpath=FreeBSD+13.2-RELEASE+and+Ports\&arch=default\&format=html) 命令将镜像复制到 SD 卡：

```sh
# dd if=selfbuilt.img of=/dev/da0 bs=1m status=progress
```

到此为止，剩下的工作就是将 SD 卡插入 ARTYZ7 板卡，按下重置（reset）按钮，观察精彩的启动过程。由于我在 `ufs` 分区中的配置，我能够在启动完成后立即通过 `ssh` 登录到我的板卡，并且已经通过 NFS 挂载我的主目录。现在，要么征服世界，要么喝杯啤酒——反正啤酒什么时候都合适。

***

**Christopher R. Bowman** 自 1989 年开始使用 BSD，当时他在约翰斯·霍普金斯大学应用物理实验室的地下二层，使用 VAX 11/785 工作。后来，在 90 年代中期，他在马里兰大学使用 FreeBSD 设计了自己的第一款 2 微米 CMOS 芯片。从那时起，他一直是 FreeBSD 用户，并对硬件设计及其驱动软件非常感兴趣。他在过去 20 年里一直从事半导体设计自动化行业工作。


# TCP LRO 简介

* 原文链接：[Introduction to TCP Large Receive Offload](https://freebsdfoundation.org/our-work/journal/browser-based-edition/storage-and-filesystems/introduction-to-tcp-large-receive-offload/)
* 作者：Randall Stewart、Michael Tüxen

![TCP/IP 历险记](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-840bbc79b25e3e5808ca7a5f401930516e98323b%2Ftcp-1.png?alt=media)

TCP 大型接收卸载（TCP Large Receive Offload，TCP LRO）是一种特定于协议的方法，用于降低接收 TCP 段（TCP segment）时所需的 CPU 资源。它也是实现特定的，本篇文章介绍了它在 FreeBSD 内核中的实现。在任何给定时刻，TCP 通常用于单向通信，尽管 TCP 提供了双向通道。例如，当使用 TCP 作为传输协议的应用协议是请求/响应类型（如 HTTP）时，即是这种情况。

TCP LRO 可采用多种方式降低所需的 CPU 资源，包括：

* 合并确认（acknowledgment，ACK），向 TCP 栈发送单个大的扩展 ACK，而非多个较小的 ACK。适用于 TCP 端点主要发送用户数据的情况。
* 将多个传入的数据段合并成一个较大的数据块。这对于 TCP 端点主要接收用户数据时非常有用。
* 绕过部分 IP 栈处理。因此，TCP LRO 在网络接口层截取数据包是有用的。

所有这些方法都专注于减少调用 TCP 栈的次数，最小化 CPU 所需的缓存未命中次数，通过将所有处理压缩到一个或一系列一起处理的数据包中。对于大多数 FreeBSD 驱动程序，使用的是单一的软件 TCP LRO 过程，尽管某些特定的硬件及其驱动程序确实支持硬件 TCP LRO。本篇文章仅讨论 FreeBSD 中的软件 TCP LRO。

## TCP LRO 的发展

TCP LRO 的初步实现由 Andrew Gallatin 完成于 2006 年，特定于 **mxge(4)** 驱动程序。然后，在 2008 年，Jack Vogel 将其扩展到所有驱动程序。它只有两个主要目标：

1. 收集和合并小的传入数据段，向 TCP 提供一个更大的单一传入数据段，或者
2. 收集多个 ACK，并向 TCP 栈呈现一个更大的单个 ACK。

这两种方法的实现目的是减少 TCP 接收路径被调用的次数，从而节省 CPU 资源。它的实现非常谨慎，只处理连续的段和没有 TCP 选项的段（唯一允许的 TCP 选项是时间戳选项）。这个初步实现在 FreeBSD 中保留了近十年，几乎从未改动，唯一的变化是 2012 年 Bjoern A. Zeeb 增加了对 IPv6 的支持。

### 排序的加入

到了 2016 年，TCP LRO 代码开始显现老态，随着在客户端和服务器上部署的网卡越来越快，每次驱动程序中断时接收的数据包越来越多。最初的实现仅允许收集和压缩来自八个不同连接的数据。这在连接数较少的工作负载下效果很好，但对于连接数很多的工作负载来说效果较差，因为每次中断时驱动程序会从不同的连接发送多得多的数据包。由于每次中断时来自多个连接的数据包数量激增，单个连接的数据包间隔足够小以适应八连接限制的可能性越来越小，以至于 TCP LRO 基本上不起作用，尤其对于服务器端来说。

这时，Hans Petter Selasky 提出了一个绝妙的主意，他为驱动程序添加了一个可选的调用路径，在提交给 TCP LRO 之前对传入的数据包进行排序。这意味着来自每个连接的所有数据包都可以一起处理。这也就意味着，你可以在每次中断时最大化 TCP LRO 的效果。这一改变大大改善了 TCP LRO 性能，同时仍允许旧驱动程序保持不变。

### 数据包排队

随着 TCP LRO 效果的提高，这种更高效路径的其他问题也开始显现，包括：

1. TCP 的拥塞控制更喜欢看到每一个 ACK，因为 ACK 会推动其拥塞窗口。压缩 ACK 可能会妨碍拥塞控制算法。
2. 现代 TCP 栈通常希望获取精确的往返时间（RTT）信息，压缩多个 ACK 会隐藏这一信息。
3. TCP ECN（显式拥塞通知）的实现需要查看 IP 头部的位，以便监控和响应来自网络的 ECN 信号，压缩数据或 ACK 实际上隐藏了这些信息。
4. 如果 TCP 栈正在进行数据包限速（我们将在后续的文章中讨论数据包限速），那么当栈被禁止发送数据包时处理一系列 ACK 会增加开销。这是因为 ACK 不能发送，但在处理过程中会导致多个缓存未命中，之后在栈可以发送数据包时必须重新处理。

这些问题促使了一项新的优化，其中 TCP 栈允许 TCP LRO 代码直接将数据包排队，以便在下一次唤醒时处理。这样，当栈可以发送数据时，所有 IP 和 TCP 头部的数据就能在同一时间进行处理，并且揭示了所有 TCP 想要看到的信息（包括由于接收时间戳的添加，无论是在硬件上由网卡完成，还是在软件中由 TCP LRO 代码完成）。

### 压缩 ACK

这种新的排队机制运行良好，但在一系列 ACK 到达时，也引起了一组额外的缓存未命中。这是因为每个排队等待处理的数据包都会在处理时导致缓存未命中。在旧的压缩方案中，虽然会丢失一些信息，但因为对于若干个到达的 ACK，只会发生一次缓存未命中，因此优化效果更佳。

这促使了另一项 TCP LRO 优化。当连续的 ACK 到达时，TCP LRO 代码现在可以将它们压缩成一个特殊的数据包，该数据包包含到达的数据包信息的数组。这种压缩技术允许将所有以前丢失的数据（包括到达时间）以数组结构的形式呈现给 TCP 栈，从而只需要一次缓存未命中来访问这个特殊的数据包。需要注意的是，TCP 栈必须通知 TCP LRO 代码，表明它支持这种特殊类型的处理。

### 内外层头部

最后一组 TCP LRO 的优化与传入 IP 数据包的检查方式有关。最初，只支持包含使用 IPv4 或 IPv6 协议的 TCP 段的以太网帧。为了支持其他封装的 TCP 段，例如 VXLAN，它使得将以太网帧封装到 UDP 数据包中成为可能，数据包解析已经被扩展，以支持内层和外层头部。通过这种方式，具有 UDP 作为外层头部和 TCP 作为内层头部的数据包可以通过 TCP LRO 进行处理。假设网卡能够为这两种协议执行校验和卸载。

## TCP LRO 的管理

如果网卡驱动程序支持 TCP LRO，它可以通过 `ifconfig` 命令的参数 `lro` 和 `-lro` 启用、禁用之。

网卡驱动程序必须包含结构体 `struct lro_ctrl`，除了其他字段外，还包含指向以下内容的指针：

* 由指向 `struct mbuf` 的指针和序列号组成的对的数组。这些对的数量由 `lro_mbuf_max` 指定。
* 一组 `struct lro_entry`。这些条目的数量由 `lro_cnt` 指定。

`struct lro_entry` 用于存储一个聚合的已接收 TCP 段集的信息。如果该条目未使用，它会包含在 `lro_free` 列表中。当它被使用时，它会包含在 `lro_active` 列表中，并且也可以通过哈希表 `lro_hash` 访问。

这两个列表和哈希表也包含在 `struct lro_ctrl` 中。

网卡驱动程序初始化 TCP LRO 特定数据的方式有两种。传统的方式是调用函数 `tcp_lro_init()`。应分配的 `struct lro_entry` 数量由加载器可调参数 `net.inet.tcp.lro.entries` 指定。在使用传统初始化方式时，数组中的对没有条目。现代方式是使用函数 `tcp_lro_init_args()`，它允许调用者指定 `lro_cnt` 和 `lro_mbuf_max`。这意味着对数组也可能被分配。

无论使用哪种方式初始化 `struct lro_ctrl`，调用函数 `tcp_lro_free()` 都将释放所有已分配的资源。

## 将 TCP 段传递给 TCP LRO

网卡驱动程序有传统和现代两种方式将 TCP 段传递给 TCP LRO。如果将 TCP 段传递给 TCP LRO 失败，网卡驱动程序必须继续正常处理该 TCP 段。TCP LRO 失败的一个原因是网卡无法验证接收到的 IP 数据包的校验和。

使用传统方式将 TCP 段传递给 TCP LRO 时，网卡驱动程序会调用 `tcp_lro_rx()`。基本上，这会启动由 `tcp_lro_rx_common()` 完成的处理，后者将在下一小节中介绍。将 TCP 段传给 TCP LRO 的现代方式（也要求使用现代初始化方式）是调用 `tcp_lro_queue_mbuf()`。这个函数仅为 TCP 段计算一个序列号，并将其与 TCP 段一起存储在数组中的下一个空闲条目中。如果通过此操作数组已满，则会调用 `tcp_lro_flush_all()`，这将在下一小节中说明。

无论使用传统方式还是现代方式将 TCP 段传递给 TCP LRO，若网卡未提供硬件接收时间，传递给 TCP LRO 的时间将被保存。

## 在 TCP LRO 中处理 TCP 段

当使用现代方式将 TCP 段传递给 TCP LRO 时，会执行一个额外的初始步骤。`tcp_lro_flush_all()` 会根据序列号字段对数组中的所有条目进行排序。这使得同一 TCP 连接的所有 TCP 段很可能会在数组中彼此相邻，并按接收顺序排列。然后，调用 `tcp_lro_rx_common()` 处理数组中的所有条目。从此时起，无论是使用传统方式还是现代方式传递 TCP 段到 TCP LRO，TCP 段的处理方式都是一样的。

`tcp_lro_rx_common()` 会解析 TCP 段，并利用该信息查找哈希表中相应的 `struct lro_entry` 条目。如果找到了该条目，TCP 段将被添加到 TCP 段的数据包链中。如未找到条目，则会创建一个新的条目，并将 TCP 段添加到该条目中。需要注意的是，当 TCP LRO 代码没有空闲条目时，会 flush 一个较旧的条目，这样就释放了该结构，供新分配使用。

网卡驱动程序和 TCP LRO 代码本身都可以触发 flush 操作，这将导致处理 `struct lro_entry` 条目中的信息，使其适合由 TCP 栈处理，如下一小节所述。

## 从 TCP LRO 传递信息到 TCP 栈

如果使用类似 TCP RACK 和 TCP BBR 的替代 TCP 栈，则会使用高精度定时器系统（HPTS）。如果仅使用 FreeBSD 基础 TCP 栈，则不会使用此系统。

如果 FreeBSD 内核中没有加载 HPTS，当触发 flush 操作时，发生的情况是：TCP LRO 将合并一个 `struct lro_entry` 条目的数据包链，将所有单个 TCP 段的用户数据连接成一个大的 TCP 段。当然，这只有在没有间隙和重叠的情况下才有效。如果发生间隙和重叠，TCP LRO 可能只能合并较小的部分。ACK 数据的信息也会被合并，生成的这个大 TCP 段将被注入到接口层。这将减少需要处理的数据包数量，但也会导致丧失关于每个 TCP 段接收时间的信息，以及任何 IP 层 ECN 位。根据拥塞控制和丢包恢复的情况，这可能会带来负面影响。

如果加载了 HPTS 系统，flush 操作会触发查找 TCP 端点。这些信息用于确定 TCP 端点所使用的 TCP 栈是否支持 mbuf 队列。若不支持，则会执行与 FreeBSD 基础栈相同的处理。如果 TCP 栈支持 mbuf 队列，但不支持压缩 ACK，则条目的数据包链会被复制到 TCP 端点，并且可能会触发 TCP 端点处理该数据包链。这就是 TCP BBR 栈使用时的处理方式，它支持 mbuf 队列但不支持压缩 ACK。如果使用的是 TCP RACK 栈，它也支持压缩 ACK，可以将多个已连续接收的 ACK 存储在一个特殊的数据结构中，从而以更节省内存的方式传递它们。请注意，当使用 mbuf 队列和压缩 ACK 时，单个数据包接收时的信息会被保留并传递到 TCP 端点。

## 未来发展

TCP 的精确 ECN（显式拥塞通知）目前是由互联网工程任务组（IETF）规范的一项 TCP 特性，FreeBSD 正在开发对此的支持。除了使用两个新的 TCP 选项外，它还改变了两个现有 TCP 标志的使用，并使用了一个额外的标志。这需要对 TCP LRO 代码进行修改，以便仍然能够对支持精确 ECN 的 TCP 连接聚合传入的 TCP 段。

可以改进对 VXLAN 的支持，使其能够利用 mbuf 排队机制。

***

**RANDALL STEWART**（<rrs@freebsd.org>）是一位拥有超过 40 年操作系统开发经验的开发者，自 2006 年起成为 FreeBSD 开发者。他专注于传输协议，包括 TCP 和 SCTP，但也涉猎操作系统的其他领域。目前，他是一名独立顾问。

**MICHAEL TÜXEN**（<tuexen@freebsd.org>）是明斯特应用技术大学的教授，也是 Netflix 的兼职承包商，自 2009 年起成为 FreeBSD 源代码提交者。他的研究重点是传输协议，如 SCTP 和 TCP，它们在 IETF 的标准化过程以及在 FreeBSD 中的实现。


# 基于 Samba 的时间机器备份

* 原文链接：[Samba-based Time Machine Backups](https://freebsdfoundation.org/our-work/journal/browser-based-edition/storage-and-filesystems/samba-based-time-machine-backups/)
* 作者：Benedict Reuschling

![实用软件](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-d1760111251b24a651c99620e7d61c5011267f6c%2Fsamba-1.png?alt=media)

“我真希望我能把做备份的带宽节省下来做点有用的事情，因为我永远不会需要它们”——从未有人这么说过。在发生灾难时——而灾难总是不期而至——备份是 IT 弹性的重要组成部分。硬盘故障、笔记本电脑被盗、固件驱动程序损坏导致数据不可读等等，都是备份所要应对的情况。定期备份并确保其可用性对于持续的业务运营至关重要，而测试备份的可恢复性同样重要。但如果备份解决方案不再受支持，并且无法在较新的系统上工作该怎么办？新的系统如何备份，又如何与现有的解决方案集成？

我在《FreeBSD 期刊》2022 年 3/4 月号中写了一篇关于如何设置 FreeBSD 苹果时间机器的文章。这个设置我已经运行了很久，没有遇到问题——无论是在备份方面，还是在我需要恢复数据时。随着时间的推移，苹果改变了底层的 Apple 文件协议（AFP）。在更新的 macOS 版本中（我一直定期升级以获得安全和功能增强），该协议经历了一些变化，使得我原来的设置不再适用了。如上所述，当前的设置仍然有效，但对于新的时间机器系统，我再也无法使用它了。从 macOS 10.9（Mavericks）开始，Apple 将 SMB（更广为人知的名称是 Samba）集成到协议中。此次迁移在 macOS 11（Big Sur）完成，该版本移除了 AFP 服务器部分，改为将 SMB 作为新的标准，时间机器也开始内部使用 SMB。

当我设置新的时间机器备份系统时，我发现了这一点。许多年前，我将我在 2022 年的文章中的设置写成了一个 Ansible playbook 以便更轻松地部署。这个 playbook 仍然有效，但它导致新版本的 macOS 无法将导出的驱动器挂载为时间机器的存储位置。幸运的是，我发现其他人碰到了同样的问题，并且已经找到了相应的解决方案。哪怕这些说明并不像我希望的那样连贯，但它们已经解决了问题。以下的设置结合了不同的来源——Reddit、个人博客、FreeBSD 论坛以及 Samba 文档。我已经在两台不同的机器上进行了测试，补充了一些说明，并添加了缺失的命令以确保它按预期工作。我保留了原来的“基于 ZFS”，因为那是我用来存储重要数据的系统。你可以选择不使用 ZFS，去用别的文件系统，但如果没成功，也别怪我！

## 系统要求

为了设置此备份系统，你需要一台机器（在我的例子中是 FreeBSD）来存储备份。它应该有良好的网络连接和快速的存储。存储还需要以某种方式实现冗余，比如 RAID1 及更高等级的 RAID。存储容量取决于两个因素：备份数据的人数和他们的数据量。时间机器配置对话框允许你设置磁盘配额和加密，这两者都是不错的选择。ZFS 支持配额和保留空间，因此我在文件系统层面上设置了相同的值。当可用存储空间不足以容纳新的备份数据时，时间机器会自动删除较旧的备份。存储越多，你可以恢复的备份历史也就越长。

加密功能也很有用，特别是当我们将数据通过可能未加密或者经 VPN 网络传输时。在接收系统上，可以为静态数据添加加密的 ZFS 数据集。但是，值得注意的是，当备份目标需要重新启动时，除非有人输入用于挂载数据集的密码，否则加密的数据集将不会被挂载。你可以配置 ZFS 从某个文件获取密码，但我将其作为一个安全访问的练习，来保障存储密码的文件不被泄露。

在发送端，所有 macOS 系统都能够挂载、配置该驱动器，并通过单独用户凭证进行保护。这能让多人向同一时间机器备份。考虑到可能同时进行多个备份，因此有足够带宽的服务器变得尤为重要。当在 Mac 上配置多个时间机器备份位置时，系统会智能地避免同时备份到两个位置，从而防止系统 I/O 被长时间的备份任务所阻塞。

## 配置备份服务器

首先安装你选择的 FreeBSD 版本（理想情况下仍在支持范围内），并确保安装了最新的安全补丁。我们在这里不选择使用 jail，但我们用 jail 也没问题。首先安装 Samba 包：

```sh
# pkg install samba419
```

接下来，我们为 ZFS 配置备份存储。在我的例子中，我有一个专门的池，命名为 `backup`，并挂载在目录 `/backup`。我为时间机器创建了一个单独的数据集，并设置了配额和保留空间，因为我还在其中存储其他数据，并且希望为这些文件保留一定的空间。

```sh
# zfs create -o quota=1.5T -o reservation=1.5T backup/timemachine
```

我知道会有两个用户（Tammy 和 Tim；Alice 和 Bob 在度假）将他们的 Mac 备份到该位置。我平等地对待他们，因此我为两者设置了相同的空间保留和配额。请记住，数据集上的配额和保留空间也会应用于其下的所有数据集。1.5 TB 也将应用于他们的数据集，已对其进行了限制。但每人 500GB 是足够的，因此我为每个数据集分别设置了 `refquota` 和 `refreservation`。

```sh
# zfs create -o refquota=500g -o refreservation=500g backup/timemachine/tammy
# zfs create -o refquota=500g -o refreservation=500g backup/timemachine/tim
```

他们两人都永远无法登录到我的备份服务器（他们也不在乎），但他们仍然需要在系统上拥有一个用户来挂载时间机器的存储。我为他们两人都运行了 `adduser`，不给他们家目录（`/var/empty`），也不给他们 shell 访问权限（`/usr/sbin/nologin`）。

运行命令 `chmod` 和 `chown` 来保护他们的数据集挂载点，防止外部窥探。

```sh
chmod -R 0700 /backup/timemachine/tammy
chmod -R 0700 /backup/timemachine/tim
chown -R tim /backup/timemachine/tim
chown -R tammy /backup/timemachine/tammy
```

最后，设置 Samba 端的密码，供这两个用户使用。这就是在 macOS 中挂载时间机器备份时的密码提示。

```sh
# smbpasswd -a tim
# smbpasswd -a tammy
```

### Avahi 配置

到此为止，所需的软件已经安装完毕——简单易行。接下来，我们需要创建两个配置文件，一个是时间机器服务的配置，另一个是 Samba 配置。时间机器服务通过 Avahi 运行——不是来自《马达加斯加》的狐猴，而是这个软件。Avahi 是一款 Zeroconf 网络实现，允许程序在本地网络中发布和发现服务（比如我们的时间机器）。配置文件是基于 XML 格式的，位于 `/usr/local/etc/avahi/services/timemachine.service`（如果该文件不存在，需要创建），文件内容如下：

```xml
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">%h</name>
<service>
<type>_smb._tcp</type>
<port>445</port>
</service>
<service>
<type>_device-info._tcp</type>
<port>0</port>
<txt-record>model=RackMac</txt-record>
</service>
<service>
<type>_adisk._tcp</type>
<txt-record>sys=waMa=0,adVF=0x100</txt-record>
<txt-record>dk0=adVN=FreeBSD TimeMachine,adVF=0x82</txt-record>
</service>
</service-group>
```

该文件定义了监听 Samba 服务端口（445），挂载驱动器的图标样式（`RackMac`）和显示名称（`FreeBSD TimeMachine`）。你可以更改显示名称，以便为其指定一个更具描述性的名称。我未更改文件中的其他部分。

### Samba 配置

Samba 是 SMB 协议的开源实现，已经有 32 年历史。最初，它的主要目的是实现 Unix 系统与 Windows 之间的兼容性。随着 Windows 添加了更多附加功能，Samba 也加入了 Active Directory（AD，活动目录）集成、域控制器等功能。由于此设置中不涉及任何 Windows 系统（你能听到松了一口气的声音），我们在这里使用 Samba 的文件共享功能来代替 AFP。

Samba 的配置文件位于 `/usr/local/etc/smb4.conf`，内容如下：

```ini
[global]
workgroup = WORKGROUP
security = user
passdb backend = tdbsam
fruit:aapl = yes
fruit:model = MacSamba
fruit:advertise_fullsync = true
fruit:metadata = stream
fruit:veto_appledouble = no
fruit:nfs_aces = no
fruit:wipe_intentionally_left_blank_rfork = yes
fruit:delete_empty_adfiles = yes

[TimeMachine]
path = /backup/timemachine/%U
valid users = %U
browseable = yes
writeable = yes
vfs objects = catia fruit streams_xattr zfsacl
fruit:time machine = yes
create mask = 0600
directory mask = 0700
```

两个部分（`global` 和 `TimeMachine`）定义了备份目标所需的选项。以 `fruit:` 为前缀的行用于与 macOS 的兼容性。有关这些行的详细文档，请参见 Samba 文档（见文章末尾的参考文献）。在 `[TimeMachine]` 部分中，将路径行更改为之前在 FreeBSD 上创建的路径。`%U` 部分是一个占位符，表示个别用户名（在我们的例子中是 tammy 和 tim）进行文件备份。这样，当以后添加另一个用户时，我们就不需要更改此行了。`create mask` 和 `directory mask` 确保正确的权限，避免文件被混合，且用户无法看到和更改其他用户的备份。

## 启动

剩下的步骤就是启用并启动 dbus（avahi）和 samba 服务。

```sh
# service dbus enable
# service dbus start
# service samba_server enable
# service samba_server start
```

在 macOS 端（备份客户端），打开 Finder 并按下 CMD-K（“连接到服务器”的快捷键）。输入 `smb://server.ip.or.dns`。若一切顺利，输入用户名和密码。这就是我们之前在 `smbpasswd` 对话框中为 tim 和 tammy 设置的密码。如果成功，共享目录将挂载到系统中。接下来，进入时间机器配置对话框，再添加新的 Time Machine 卷。记得点击其中的选项按钮，再勾选加密备份框。此设置只能在初次备份之前设置，之后无法更改。你还可以限制备份占用的磁盘空间，但这不是强制的，因为我们已经在 ZFS 层面上设置了。然后，漫长的首次备份将开始进行。完成后，当网络可达时，时间机器会自动挂载和卸载该共享，将 Mac 备份到该位置，进行定期的备份。

## 总结

就这样，Samba 配置相当简单，用户应该能够根据自己的需求进行调整。我发现这个新解决方案和旧的解决方案一样可靠。我已经调整了我的 Ansible playbook，以便使用新的基于 Samba 的设置。我依然喜欢那种“一键备份”的方式，知道在需要时，我能恢复单个文件或整个系统，恢复的文件都是我最近使用的最新文件。

## 参考文献和来源

* [Samba 文档](https://www.samba.org/samba/docs/current/man-html/vfs_fruit.8.html)
* [Reddit 帖子](https://www.reddit.com/r/homelab/comments/83vkaz/howto_make_time_machine_backups_on_a_samba/)
* [FreeBSD 论坛帖子](https://forums.freebsd.org/threads/samba-functions-but-unable-to-use-it-as-a-macos-time-machine-destination.79896/#post-655905)
* [Dan Langille 的博客](https://dan.langille.org/2023/09/28/creating-a-time-capsule-instance-using-samba-freebsd-and-zfs/)

***

**BENEDICT REUSCHLING** 是 FreeBSD 项目的文档提交者，也是文档工程团队的成员。过去，他曾任两届 FreeBSD 核心团队成员。他在德国达姆施塔特应用科技大学管理一个大数据集群，还为本科生教授“Unix for Developers”课程。Benedict 也是每周 bsdnow\.tv 播客的主持人之一。


# 会议报告：BSDCan 2024

* 原文链接：[Conference Report: BSDCan 2024](https://freebsdfoundation.org/our-work/journal/browser-based-edition/storage-and-filesystems/conference-report-bsdcan-2024/)
* 作者：Aymeric Wibo

![会议报告](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-7817b5e58d90183792cf9b9feafe58e260be6966%2Fconference-report-bsdcan-2024-1.jpg?alt=media)

几个月前，我有幸参加今年的 BSDCan 并发表演讲。BSDCan 是三大年度 BSD 大会之一（另外两个是 AsiaBSDCon 和 EuroBSDCon），今年在渥太华举办。这是我第一次踏足北美——我来自比利时，那是华夫饼、啤酒以及组阁耗时 652 天的国度——所以我在从波士顿飞往渥太华参加 BSDCan 之前，先在东北走廊沿线转了两周。

我预定演讲的内容是我在谷歌 Summer of Code（GSoC）期间的工作：把 Linux 内核中的 BATMAN 实现（`batman-adv`）移植到 FreeBSD，更广泛地说，是它在 Freifunk 这类项目中的应用，以及 FreeBSD 上用于移植 Linux 内核驱动的 LinuxKPI 系统。

抵达古朴的渥太华国际机场后，我坐巴士和渥太华轻轨到了会议举办地渥太华大学。我入住讲者住宿——90U 宿舍，条件相当不错。这里的学生显然过得挺舒服！我和 Kirk McKusick 同住一间，他还没到，于是我先去了 BSDCan 与会者的固定聚会点 Father & Sons 酒馆，想认识一些人。我到时恰逢 Goat BoF（Birds of a Feather）尾声，那里人头攒动。我喝了几品脱，吃了丰盛的肉汁奶酪薯条，然后回宿舍见 Kirk。自上次在葡萄牙科英布拉的 EuroBSDCon 之后，我们还没见过面。

BSDCan 前两天并行举办教程和 FreeBSD DevSummit。我参加了 DevSummit——FreeBSD 开发者和其他来宾聚在一起讨论项目进展和未来。这里也有几场有意思的演讲，比如 Mitchell Horne 介绍 RISC-V 硬件以及他在 FreeBSD 上对 RISC-V 的支持工作；Alex Pshenichin 关于 Antithesis 确定性 hypervisor 的有趣演讲（基于 FreeBSD 的 bhyve 的 “Determinator”），用于软件测试，并讨论了运行确定性虚拟机时那些并非显而易见的种种考量。

DevSummit 期间还专门留出 “下一版本规划” 环节。大家各抒己见，讨论要进入（或砍掉）下一版 FreeBSD（本次为 15.0）的特性。这些特性被归类为：已在树中完成的特性、已完成但尚未上游的特性、正在开发的特性、亟待开发的特性，以及有则更好但非优先级的特性。我对这个清单的小小贡献是 S0ix 空闲支持——较新 CPU（包括 AMD Framework 笔记本）进入睡眠状态所必需，几年前曾启动开发但随后搁置。我把聚焦消费级硬件支持视为让更多人用上 FreeBSD 的重要途径，免得他们因为某个 X 或 Y 功能在新笔记本上不工作而却步。很高兴看到项目里其他人也有同感。

接下来两天是主会期。我先听了 Kirk 关于 FreeBSD 成功秘诀的演讲，再听 Shawn 的 State of the Hardened Union（会议期间我和他多次聊起 BATMAN 和开放无线网络），然后发表了自己的演讲。这是我第一次正式做技术演讲，不知道该期待什么，也不确定在观众面前讲技术会不会自在。总的来说我相当满意，不过语速确实快了些，会后收到的反馈也印证了我讲得太快。下次得改 😉 不过我想核心要点是讲清楚了，对提问环节也挺满意。

最后一天我听了 Warner Losh 关于通过 GitHub 向 FreeBSD 贡献的演讲。我认为这对新贡献者很重要，相比学习 Phabricator 的运作方式，门槛低了不少——尤其是那些只在 GitHub 这类工具上学过本事的初出茅庐的开发者。事实上，我第一次尝试提交 FreeBSD 贡献时也是走 GitHub，后来才明白这不是首选方式。我听的最后一场是 Sheng-Yi Hong 关于用 LLDB 调试内核的工作——我个人很期待这个功能。我做 BATMAN 那年 Sheng-Yi Hong 也是 GSoC 学生，能当面见到他很开心。他是个开朗乐观的小伙子，期待未来大会再聚！

闭幕式和惯例拍卖之后，是 Sens House 的社交活动——这是这类大会里我最享受的环节。我觉得从大会的社交层面获得的收获几乎比演讲本身还多：这是与那些你只在 handle 或补丁说明里见过的人见面的机会。通过这些交流，我结识了许多朋友，接触到许多原本不会遇到的 FreeBSD 及其他软件的用例和视角。

第二天，在 Father & Sons 吃过丰盛得离谱的早餐后，Kirk 和我收拾行李赶往机场，飞芝加哥，在奥黑尔机场与 Eric Allman 会合（可惜他今年没能来 BSDCan）。

不久之后，我们得知一则非常不幸的消息：Michael Karels 在大会最后一天之后辞世。Mike 是 4.4BSD-Lite 版本开发中举足轻重且备受爱戴的人物，所有现代 BSD 都可追溯到这个版本。我很庆幸能在 BSDCan 上认识他，并向他的家人和朋友致以最诚挚的慰问。

在风之城游览一天后，我们坐了三天加州和风号列车到伯克利，我在那里又待了几周，然后飞回老家——我的 BSDCan 之旅就此结束。

总体而言，大会及整体组织都相当出色，从餐饮到 AV 到周边活动无一不精。我想向那些在幕后累断了腰、把这一切办得如此顺畅专业的人们致以诚挚谢意。BSDCan 报销了我的差旅费，并代办了住宿事宜，这极大地缓解了离家这么远的旅行中几个主要的压力点。演讲引人入胜，我结识了一群出色而有趣的人，Father & Sons 的啤酒冰凉透心。

如果你有想讲的话题，我真心建议你投稿。期待明年再会！

Aymeric Wibo 是比利时 UCLouvain 的计算机科学学生，从高中起就在使用并基于 FreeBSD 开发项目。他的主要兴趣在图形和网络。


# 读者来信

* 原文链接：[We Get Letters](https://freebsdfoundation.org/our-work/journal/browser-based-edition/storage-and-filesystems/we-get-letters/)
* 作者：Michael W Lucas

![mailbox graphic](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-04a351e75fa8b36a378785e776af7c35bd0537cf%2Fwe-get-letters-1.png?alt=media)

**亲爱的最没用的科技专栏作家：**

**AI 无处不在。软件公司都往自家产品里塞 AI。我该不该为职业生涯担忧？**

**——忧心忡忡**

亲爱的忧心忡忡：

要正经回答你的问题，就得先把所有回避、不实之言和彻头彻尾的欺骗统统剔除。市场部把任何算法都叫 AI。问他们，Unix 也是一种 AI。微软 Excel 的 SUM 函数？那可是 AI 可靠性的巅峰。每一种 AI 都会让人失业——毕竟很久以前 calculator 本身就是个职位，改个电子表格还得花上好几个人时。不过我这里假设你说的是 “生成式 AI”，部分原因是如果你的工作 SUM 函数就能搞定，你根本不会知道这个专栏的存在；另一部分原因是这样我就能威胁一下那几家身价数百万美元的公司。

你已经面对过生成式 AI 带来的威胁了。虽然你永远战胜不了它，但正是这种威胁保证了你未来的饭碗。

从前有个人把我气得够呛，我不得不写一整本书来吐槽他。（这不是我写那本书的原因，也不是我在书里抱怨他的原因。）我的猛烈抨击铺天盖地，他不仅改了名，还改了性别，指望从身败名裂的废墟里重建点什么。（这完全不是他改名的原因，也不是他改性别的原因。）接下来我要讲讲和 Delta 共事的事。（我们其实从未真正共事过。他只是个示例人物。诽谤法禁止我点名 Gabriel，不过希望他会认出自己。Gabriel，你那种从未体验过的新情绪？那叫羞耻。）

你大概有个偏爱的技术讨论公开平台，比如 Reddit、Fediverse，或者某个用犯人管犯人的网络论坛。Delta 就是那种人：看到有人提问，他就去谷歌搜一下，把搜出来的第一个链接甩过去，哪怕那链接上明晃晃写着 “赞助商”。底特律到遥远国的那条光纤断了，Delta 自告奋勇去重做 RJ45 接头。有人问 “你试过给客户他们想要的东西吗？” 或者 “机器太烫了我就拔了电源”，那准是 Delta。

这个堕落世上的 Delta 们，给了我们宝贵的机会，让我们学会绕开故障前行。

生成式 AI，按定义来说，就是个能力更差的 Delta。

这些 “生成式人工智能” 在互联网上四处搜刮文本字符串，记录哪些字符常常以什么顺序出现。程序员们听过 “群体的智慧”（the wisdom of crowds）这句话，居然没听出这是反讽。你往系统里输入一段字符串，它就输出一段看起来像你输入的字符串之后该出现的字符串。换句话说，如果你输入的看起来像一个 StackExchange 问题，它就给出一个看起来像 StackExchange 上会得到的答案的答案。任何公开技术论坛上的平均答案，都是一剂毒害灵魂的毒药，毒到让我写的 Perl 都显得光鲜。不是好莱坞式的光鲜。更像是诡异的妖精女王的那种 glamour，最终你被锁在键盘前，被判写下去取悦妖精王庭，直到你成为世上最伟大的作家——这倒能让你练上不少，可你再也收不到来自人间的书，于是无法完成那场本可结束交易的比较。所以，别那么干。妖精女王记仇得很，尤其如果你偷偷把开锁工具带进了她那儿。

是的，互联网上能找到好信息。但它绝不会出现在搜索引擎的第一个结果里。多半也不在你的第一次查询里。从互联网里榨出有用信息是一项技能，需要多年的负反馈训练才能养成，而这些生成式引擎恰恰不具备这种技能。互联网里只有零星的智慧碎片，被掩埋在堆积如山的腐朽平庸之中，再撒上一点愚蠢调味。生成式 AI 动用几栋塞满 GPU 的楼、几兆瓦电力、以及够灌溉一个小国的清洁水，来模拟一个刚毕业的大学生，他真心希望潜在雇主别注意到他通过挑战性的通识学学士学位获得的 2.0001 的 “同情及格” GPA，以及他因为 “极端偏执” 被骑车能到的所有图书馆都禁止入内——这种 “大学生” 有什么不招人喜欢的？除了像我这样的作者正加入针对这些 AI 公司的无数起版权侵权诉讼之一？

所以，不，你不必害怕生成式 AI。

你必须提升绕开故障的本领。

你是怎么对付你那位 Delta 的？也许你给了他一份光鲜的绩效评价，好让他被无害地发配到人力资源部。或者你说服他接受 “六角头螺丝审计员” 这个重要任务。也许你把他派进了 “备份磁带大厅”，给他一团线绳好让他能找到回来的路，又因为担心他这段凶险旅途的安全，你测试了那线绳的易燃性，“惊愕” 地发现它像闪燃纸一样轰地燃起来，把他永久困在了纸带卷之间。我不会评判，除非线绳烧出的灰烬留下了一条痕迹。你有个问题，你用最不违法的方式解决了它。

你公司里总会有人染上 AI 热病，想把公司那笔没价值的工资支出转成给 AI 公司的宝贵付款。最简单的规避办法就是记住：你已经在用 AI 了。你总有张电子表格吧？让它替你加数字——砰——AI！这不是谎言。毕竟市场部说它是 AI，他们总不会撒谎。grep 和 awk 的恰当组合所能提供的 “智能”，比这些公司加起来还多。

如果有人非要用生成式 AI 不可，那就拿一个免费可用的模型，用你公司的文档库训练它。你有几十年写得稀烂的邮件、提案和白皮书，足以与 StackExchange 上那些痴人说梦一较高下。这能让你证明：无论普通员工有多糟，生成式 AI 只会更糟。

假设最坏的情况发生了。管理层下令部署生成式 AI。那你就一箭双雕——让 Delta 来部署它。

想向 Michael 提问？

请发送至 <letters@freebsdjournal.org>

Michael W Lucas 是《Absolute FreeBSD》《SSH Mastery》以及新书《Run Your Own Mail Server》的作者。他的专栏合集《Dear Abyss》即将在 <https://mwl.io/ks> 上线。


# 基金会来信

亲爱的读者：

欢迎阅读 2024 年 5/6 月刊！写下这篇文字时，我正在收听社区成员的 FreeBSD Day 直播。听大家讲述各自与 FreeBSD 结缘的故事、如何使用或贡献 FreeBSD，以及 FreeBSD 世界里有什么让他们兴奋，都很有意思。最打动人的是他们对 FreeBSD 的热爱与激情。本期作者在项目中的资历各异，有刚参加完 GSoC 的年轻人，也有在 FreeBSD 浸淫数十年的老手。虽然经验深浅不同，他们都在自己熟悉的领域分享知识与热忱。

本期并非以 FreeBSD Day 为主题，但如果你错过了最近的庆祝活动，可以访问我们的 [FreeBSD Day](https://freebsdfoundation.org/freebsd-day/) 页面，观看全部直播和相关内容。

我不想偏袒任何作者或主题，但我很期待阅读 Warner 撰写的《向 FreeBSD 提交 GitHub Pull Requests》——因为我本人也想开始为 FreeBSD 文档贡献一份力。说不定我会写一篇文章记录这次经历！

还有一件事想提。你们很多人已经知道 Mike Karels 突然离世的消息。这对社区是巨大的损失。在我眼里，他一向温和，既与 FreeBSD 的历史紧密相连，又参与当下的开发与支持。最近，Mike 出任副发布工程师，协助新任首席发布工程师 Colin 履新。为纪念 Mike，我想请大家以善意对待社区中的其他人。让我们伸出援手，互相扶持、填补空缺——就像 Mike 那样——共同守护这个出色的项目。虽然我们永远无法取代 Mike，但可以延续他对 FreeBSD 的热爱，让他的精神长存。

希望你享受本期！

**Deb Goodkin**

FreeBSD 基金会执行董事


# 活动日历

* 作者：**Anne Dickison**

截至 2024 年 10 月的 BSD 活动。如需提交此处未列出的 FreeBSD 相关活动，或 FreeBSD 用户感兴趣的活动信息，请发送至 <freebsd-doc@FreeBSD.org>。

![活动日历](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-cc52f9065db0f16752308c0a9ad2d36c135abe40%2Fevents-calendar-1.png?alt=media)

## EuroBSDCon 2024

9 月 19-22 日

爱尔兰都柏林

EuroBSDCon 是年度国际技术大会，每年在欧洲不同国家举办，汇聚基于 4.4BSD（伯克利软件发行版）操作系统家族及相关项目的用户和开发者。FreeBSD 基金会很高兴再次作为银牌赞助商参与。

![FreeBSD](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-ad952c0e894c6d9830c08d0a835422ec34035453%2Fevents-calendar-2.png?alt=media)

## EuroBSDCon FreeBSD 开发者峰会

9 月 19-20 日

第一天为演讲和讨论组，第二天为黑客马拉松。

![活动日历](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-def11b0c3de35d67166fda4ffa7db9a02669947b%2Fevents-calendar-3.png?alt=media)

## All Things Open

10 月 27-29 日

北卡罗来纳州罗利

All Things Open 是美国东海岸规模最大的开源/开放技术/开放 Web 大会，也是全美最大的同类大会之一。大会常邀全球知名专家，几乎所有主流科技公司也到场。FreeBSD 很荣幸成为今年 All Things Open 的非营利合作伙伴。


# 基本系统中的 mfsBSD

* 作者：SOOBIN RHO
* 原文链接 <https://freebsdfoundation.org/our-work/journal/browser-based-edition/configuration-management-2/mfsbsd-in-base/>

![mfsBSD 文章配图](https://430908880-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FAl0aZXzJcYVyK5yrIo4q%2Fuploads%2Fgit-blob-ce4d63f10af22ec96d5dc9bfc04064f1c04aca6c%2Fji-ben-xi-tong-zhong-de-mfsbsd-1.png?alt=media)

mfsBSD 是一款基于 FreeBSD 的内存操作系统。

mfsBSD 的不同之处在于，它是完全运行于内存的 FreeBSD 实例——因此叫做 mfs（memory file system，内存文件系统）。当然，这意味着当我们在使用 mfsBSD 时，不会对现有的磁盘设备造成任何干扰。例如，我们可以将其用于云服务器和本地服务器的故障排除 ①。在邮件列表中搜索 mfsBSD，看看人们是如何解决各种问题的，很是有趣。我个人最喜欢的应用场景是在仅有单个磁盘的设备中安装 FreeBSD，如果因某种原因无法使用 FreeBSD 安装介质，我就会：先制作 mfsBSD 镜像、将这个镜像安装到磁盘设备上、启动 mfsBSD、最后运行 bsdinstall。示例如下：

首先，构建 mfsBSD：

```sh
# 正在审查将 mfsBSD 集成到基本系统的补丁集，可在以下网址查看：
# https://reviews.freebsd.org/D41705
cd /usr/src/releasemake mfsbsd-se.img
# 此处 se 指 mfsBSD 特别版本（special edition），它包含
# dist 文件（即 base.txz 和 kernel.txz），bsdinstall 需要这些文件
cd /usr/obj/usr/src/${ARCH}/release/ls -lh
```

然后，将 mfsBSD 写入磁盘设备：

```sh
# 把 mfsBSD 安装到你的目标磁盘设备上
# 把 ada0 换成你自己的目标磁盘设备
dd if=./mfsbsd-se.img of=/dev/ada0 bs=1Mreboot
```

启动到 mfsBSD，然后执行 bsdinstall：

```sh
# 复制特别版本（special edition）的 dist 文件，以便 bsdinstall 用于安装。
mkdir /mnt/distmount /dev/ada0p3 /mnt/dist
mkdir /usr/freebsd-distcp /mnt/dist/<version>/*.txz /usr/freebsd-dist/bsdinstall
```

mfsBSD 能够把整个 FreeBSD 系统都加载到内存。加载完成后，就可以对原始磁盘任意修改，因为现有的 mfsBSD 文件都运行在内存中。正如 Matuška 在其白皮书（2009 年）中所述，“mfsBSD 是个工具集，能创建基于 mfsroot 的小巧但功能完备的 FreeBSD 发行版，将所有文件存储在内存中。”②

## mfsBSD 简史

mfsBSD 的作者是 Martin Matuška（`mm@FreeBSD.org`）。回顾 mfsBSD 存储库的提交日志，他首次提交于 2007 年 11 月 11 日，大约是在 FreeBSD 7.0 BETA 的发布同期。“这个项目 \[mfsBSD] 基于 Depenguinator 项目的想法”，Depenguinator 是 Colin Percival 在 2003 年创建的项目，旨在为仅提供 Linux 发行版的专用服务器远程安装 FreeBSD③。Matuška 想为 FreeBSD 6.x 提供 Depenguinator 的功能，这就是 mfsBSD 的起源。

自那时起，随着 mfsBSD 知名度不断增长，Matuška 维护着 <https://mfsbsd.vx.sk/> 用于分发 mfsBSD 的镜像。他已经在 GitHub④ 上维护了十七年的 mfsBSD 源代码，一路修复了无数 bug，并添加了对 zfsinstall、**/usr** tar 压缩包的支持等。

2023 年 5 月（即此篇文章写作的前一年），谷歌 Summer of Code（GSoC）项目开始了将 mfsBSD 集成到基本系统的尝试。

## 谷歌编程之夏

这一切是如何开始的呢？当时，我正在阅读黑客新闻（可能我正在拖延大学作业）。这是我第一次知道谷歌编程之夏（GSoC）。那里的某条热门评论说 FreeBSD 是参与组织之一。有趣的是，评论中只字未提其他事情，好像其他事情都是不言自明的。

我立刻被吸引住了。关于 FreeBSD 最令我惊讶的是：macOS 衍生于 FreeBSD，而且 Netflix 也在其 CDN 中使用了 FreeBSD。谷歌编程之夏的申请流程包括提交项目提案。强烈建议申请者从各组织的项目创意列表中寻找项目主题（除非你有自己的打算）。我看了看列表，对我来说，mfsBSD 项目最有趣，因为其他项目创意似乎更接近内核开发，超出了我能接受的范围。

在给我的导师们发了封电子邮件后，我收到了 Joseph Mingrone（`jrm@FreeBSD.org`）和 Juraj Lutter（`otis@FreeBSD.org`）的回复，并进行了简短的 Zoom 通话。几周后，我收到了谷歌编程之夏的录取通知。然后，我们进入了所谓的社区粘合期（community bonding period）。在这期间，所有的贡献者和导师相聚于虚拟会议，总共开了半个小时，所有人都进行了自我介绍。那天是 2023 年 5 月 12 日（星期五）。

## 基本系统中的 mfsBSD

三个月又二十二天后，将 mfsBSD 集成到基本系统中的项目终于完成了，历经反复调试（很多 Bug 是通过谷歌搜索和翻阅所有过去的 GitHub 问题来解决的），测试（用我的两台笔记本电脑 Thinkpad T440 和 P17 运行 shell 脚本），并且我向导师们问了太多问题。一整套三个补丁提交到了 Phabricator⑤。

简单地说，第一个提交“mfsBSD: Vendor import mfsBSD（mfsBSD: 引入 mfsBSD）”将 mfsBSD 集成为 `contrib/mfsbsd`。主要提交“release: Integrate mfsBSD image build targets into the release tool set（发行：将 mfsBSD 镜像构建目标集成到发行工具集）”：在 `release/Makefile` 中添加了目标 `mfsbsd-se.img` 和 `mfsbsd-se.iso`（作为 `release/Makefile.mfsbsd`）。最后一次提交“**release(7)**: Add entries for the new mfsBSD build targets（**release(7)**: 为新的 mfsBSD 构建目标添加条目）”在 `share/man/man7/release.7` 中添加了相应条目。这意味着，现在我们可以在构建所有 FreeBSD 安装介质（如 cdrom、dvdrom、memstick 和 mini-memstick）的同时，在同一份发行版 Makefile 中使用 `make release WITH_MFSBSD=1` 构建 mfsBSD。

现在正在审查补丁集。mfsBSD 之前位于 FreeBSD 发布工具链以外，仅生成了 release 版本。我的设想是将这个补丁集作为基本系统的一部分，提供 mfsBSD 镜像，并通过调用 `cd /usr/src/release && make release WITH_MFSBSD=1` 构建定制 mfsBSD 镜像，然后在 **/usr/obj/usr/src/${ARCH}/release/** 创建 `mfsbsd-se.img` 和 `mfsbsd-se.iso`。

### 参考文献

* ①. Matuška, Martin. (2022). FreeBSD 在 Hetzner 专用服务器上——VX Weblog。\[在线] 参考：<https://blog.vx.sk/archives/353>
* ②. Matuška, Martin（2009）。mfsBSD 工具集，用于创建基于内存文件系统的 FreeBSD 发行版。\[在线] 可在以下网址找到：<https://people.freebsd.org/~mm/mfsbsd/mfsbsd.pdf>
* ③. Colin Percival（2003）。Depenguinator — FreeBSD 远程安装。\[在线] 可在以下网址找到：<http://www.daemonology.net/depenguinator/>
* ④. Matuška, Martin（2024）。mmatuska/mfsbsd。\[在线] GitHub。可在以下网址找到：<https://github.com/mmatuska/mfsbsd>
* ⑤. <https://reviews.freebsd.org/D41705>

***

**SOOBIN RHO** 是南达科他州奥古斯塔纳大学的大四学生。他出生于韩国，但在迪拜长大，最终选择在美国上大学。他现在是 The Bancorp 银行网络安全部门的兼职员工。毕业后，他将成为一名信息安全分析师。自 2023 年谷歌编程之夏起，他一直是 FreeBSD 的贡献者。




---

[Next Page](https://freebsd-journal-cn.bsdcn.org/llms-full.txt/1)

