FreeBSD-绝对指南-全-

FreeBSD 绝对指南(全)

原文:zh.annas-archive.org/md5/0b4f906c290a43ade7a2d251af09c6e0

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

image

欢迎来到Absolute FreeBSD!本书是系统管理员的综合指南,适合那些希望构建、配置和管理 FreeBSD 服务器的人。对于那些希望在桌面、嵌入式设备、服务器农场等设备上运行 FreeBSD 的人来说,本书也同样有用。完成本书后,您应该能够使用 FreeBSD 提供网络服务。您还应该了解如何管理、修补和维护您的 FreeBSD 系统,并对网络、系统安全和软件管理有基本的了解。我们将讨论 FreeBSD 11 和 12 版本,这些是本书发布时最新的版本;然而,本书的大部分内容同样适用于早期和更高版本。

什么是 FreeBSD?

FreeBSD 是一种自由可用的类 Unix 操作系统,在互联网服务提供商、家电和嵌入式系统中非常流行,适用于任何对普通硬件可靠性要求极高的场合。就在上周的某一天,FreeBSD 奇迹般地出现在互联网上,完全形成,直接从其英雄创造者的高远智慧中挤压出来。开玩笑的——事实上,真相更为令人印象深刻。FreeBSD 是近四十年持续开发、研究和改进的结果。FreeBSD 的故事始于 1979 年,与 BSD 有关。

BSD:FreeBSD 的祖先

多年前,AT&T 需要大量定制的软件来支持其业务运行。然而,由于不能在计算机行业中竞争,AT&T 不能出售其软件。因此,AT&T 以低廉的价格将各种软件和其源代码授权给大学。大学通过使用这些软件代替价格高昂的商业软件节省了开支,而有机会接触这些技术的大学生可以查看源代码,了解其工作原理。作为回报,AT&T 获得了曝光、一些零花钱,以及一代计算机科学家,他们从 AT&T 的技术中汲取了经验。每个人都从中受益。根据这种授权计划分发的最著名软件是 Unix。

与现代操作系统相比,最初的 Unix 存在许多问题。然而,成千上万的学生有机会接触其源代码,而且成百上千的教师需要为学生提供有趣的项目。如果某个程序表现异常,或者操作系统本身出现问题,那么那些每天使用系统的人具备修复它的工具和动机。他们的努力迅速改善了 Unix,并创造了许多我们现在认为理所当然的功能。学生们增加了控制正在运行的进程的能力,也就是 作业控制。Unix S51K 文件系统让系统管理员像精疲力竭的幼儿一样哭泣,因此他们用快速文件系统(FFS)替换了它,而 FFS 的特性已经扩展到每一个现代文件系统。多年来,许多小型的、有用的程序被编写出来,逐渐取代了 Unix 中的整块功能。

加利福尼亚大学伯克利分校的计算机系统研究小组(CSRG)参与了这些改进,并且还充当了 Unix 代码改进的中央清算中心。CSRG 收集了其他大学的修改,评估它们,打包后免费分发给任何持有有效 AT&T UNIX 许可证的人。CSRG 还与国防高级研究计划局(DARPA)签订合同,以实现 Unix 中的各种功能,比如 TCP/IP。最终,所有的软件集合被称为 伯克利软件分发版,或 BSD

BSD 用户拿到软件后,进一步改进了它,然后将他们的改进反馈到 BSD 中。今天,我们认为这是一个开放源代码项目运行的相当标准的方式,但在 1979 年,这种方式是革命性的。BSD 也相当成功;如果你查看旧的 BSD 系统上的版权声明,你会看到这个:

Copyright 1979, 1980, 1983, 1986, 1988, 1989, 1991, 1992, 1993, 1994
        The Regents of the University of California. All rights reserved.

没错,15 年的工作——在软件开发中,这是一生的时间。还有哪些软件不仅依然在使用,而且在工作开始 15 年后,仍在积极开发?事实上,BSD 经历了如此多的增强和改进,以至于 CSRG 发现,随着时间的推移,它几乎用 CSRG 及其贡献者创建的代码替代了原始的 Unix 代码。你需要仔细找,才能发现任何原始的 AT&T 代码。

最终,CSRG 的资金逐渐减少,显然 BSD 项目将会结束。经过加利福尼亚大学内部的一些政治斗争后,1992 年,BSD 代码在所谓的 BSD 许可证 下公开发布给大众。

BSD 许可证

BSD 代码可以供任何人使用,依据的是可能是软件开发历史上最宽松的许可证。该许可证可以总结为以下几点:

  • 不要声称你写了这个。

  • 如果它崩溃了,不要怪我们。

  • 不要利用我们的名字来推广你的产品。

这意味着你几乎可以用 BSD 代码做任何你想做的事情。(原 BSD 许可证确实要求如果软件产品包含 BSD 授权的代码,用户必须被通知,但这个要求后来被取消了。)甚至没有要求你与原作者共享你的修改!人们可以自由地将 BSD 纳入专有产品、开源产品或免费产品——甚至可以将它打印在穿孔卡片上,铺满整个草坪。你想制作 10,000 份 BSD 光盘并分发给朋友?随便。BSD 许可证有时被称为复制中心许可证,而不是版权,意思是把这个拿到复印中心,自己复印几份。毫不奇怪,像 Sun Microsystems 这样的公司立即抓住了这个机会:它是免费的、有效的,而且很多新毕业生都有使用这项技术的经验——其中包括 Sun 的创始人之一比尔·乔伊。有一家公司 BSDi,就是专门为了利用 BSD Unix 而成立的。

AT&T/CSRG/BSDi 铁笼大战

在 AT&T,UNIX 的工作持续进行,即便 CSRG 也在其自得其乐地进行着。AT&T 将 BSD Unix 发行版的部分内容与其 UNIX 系统集成,并将修改后的结果重新授权给提供这些改进的大学。这对 AT&T 来说运作良好,直到公司被拆分,拆分后的公司被允许在计算机软件行业竞争。AT&T 拥有一项特别有价值的资产:一款经过成千上万的人调试过的高端操作系统。这个操作系统具有许多有用的功能,如多种小而强大的命令、现代文件系统、作业控制和 TCP/IP。AT&T 成立了一家子公司——Unix Systems Laboratories(USL),并开始愉快地向企业销售 Unix,并收取非常高的费用,同时保持与大学的关系,这也是最初赋予它如此先进操作系统的原因。

伯克利大学在 1992 年公开发布 BSD 代码时,遭到了 USL 的强烈反对。几乎立即,USL 将大学和那些利用该软件的软件公司起诉了,特别是 BSDi。加利福尼亚大学声称,CSRG 是从与 AT&T 无关的成千上万的第三方贡献者那里编译了 BSD,因此这属于 CSRG 的知识产权,可以自行处置。

这场诉讼促使许多人拿起一份 BSD,看看究竟是什么引发了如此多的争议,而另一些人则开始在其基础上构建产品。其中之一就是 386BSD,最终成为 FreeBSD 1.0 的核心。

1994 年,在经历了两年的法律争斗后,加利福尼亚大学的律师证明,AT&T UNIX 的大部分内容实际上完全来自 BSD,而不是相反。更糟的是,AT&T 实际上违反了 BSD 许可证,通过去除 CSRG 版权信息的方式剥夺了它所吸收的文件的版权。(只有非常特别的公司才能违反世界上最慷慨的软件许可证!)最终,争议的焦点仅在于六个文件,为了解决这些悬而未决的问题,USL 将其中的一些文件捐赠给了 BSD,同时将一些保留为专有信息。

一旦尘埃落定,新的 BSD Unix 版本作为 BSD 4.4-Lite 发布到了世界上。随后的更新版 BSD 4.4-Lite2 是当前 FreeBSD 的“祖父”,也是今天所有 BSD 变体的先祖。

FreeBSD 的诞生

BSD 的一个早期成果是 386BSD,这是一个旨在运行在廉价的 386 处理器上的 BSD 版本。^(1) 386BSD 项目成功地将 BSD 移植到了英特尔的 386 处理器上,但项目停滞了。经过一段时间的忽视,一群 386BSD 用户决定独立发展,创建 FreeBSD,以便保持操作系统的更新。(大约在同一时期,其他几个小组也开始从 386BSD 分支出来,其中只有 NetBSD 至今仍然存在。)

386BSD 和 FreeBSD 1 源自 1992 年发布的 BSD 版本,这是 AT&T 愤怒的源头。由于这场诉讼,所有使用原始 BSD 的用户都被要求基于 BSD 4.4-Lite2 进行后续工作。BSD 4.4-Lite2 并不是一个完整的操作系统——尤其是那些 AT&T 保留为专有的文件对系统的正常运行至关重要。(毕竟,如果这些文件不重要,AT&T 根本就不会去管它们!)FreeBSD 开发团队紧急工作,替换了这些丢失的文件,很快就发布了 FreeBSD 2.0。从那时起,开发工作一直在持续进行。

如今,FreeBSD 被一些最重要、最具影响力的互联网公司广泛应用。Netflix 的内容传输系统完全运行在 FreeBSD 上。IBM、Dell/EMC、Juniper、NetApp、Sony 等众多硬件公司在嵌入式系统中使用 FreeBSD,除非有人告诉你,否则你根本不会察觉。事实上,如果一个公司需要处理大量的互联网带宽,那么它很可能在使用 FreeBSD 或其 BSD 系列的某个变种。

FreeBSD 还广泛应用于各种嵌入式和专用设备中。你有 PlayStation 4 吗?恭喜,你正在运行 FreeBSD。不过,我听说在这些设备上获取 root shell 比较困难。

就像雾霾、蜘蛛和玉米糖浆一样,FreeBSD 无处不在;你之所以看不见它,是因为 FreeBSD 就是这么好用。FreeBSD 稳定性的关键在于其开发团队和用户社区——它们实际上是同一个东西。

FreeBSD 开发

有句老话说,管理程序员就像是在赶猫。尽管 FreeBSD 开发团队分散在全球各地,且讲着数十种语言,但大多数情况下,成员们作为 FreeBSD 社区的一部分,能够很好地协作。他们更像是一群狮子,而不是一群家猫。与其他一些项目不同,FreeBSD 的所有开发工作都在公开进行。负责 FreeBSD 进展的有三个群体:提交者、贡献者和用户。

提交者

FreeBSD 大约有 500 名开发者,或称提交者。提交者拥有 FreeBSD 主源代码库的读写权限,可以开发、调试或增强系统的任何部分。(“提交者”一词来源于他们能够提交代码更改。)由于这些提交可能会以微妙或明显的方式破坏操作系统,提交者肩负着沉重的责任。提交者负责保持 FreeBSD 的正常运行,或者至少在加入新功能和评估贡献者的补丁时,不会破坏系统。大多数开发者是志愿者;只有少数人是被支付报酬来做这项艰苦工作的,而且这些人通常与其他工作有关。例如,英特尔雇佣提交者来确保 FreeBSD 正确支持其网络卡。由于 FreeBSD 在互联网的重负载群体中有着很高的知名度,因此英特尔需要确保其卡片在 FreeBSD 上正常工作。

要将自己融入 FreeBSD 开发的“蜂巢”中,可以考虑订阅邮件列表* FreeBSD-hackers@FreeBSD.org,其中包含了大部分的技术讨论。有些技术讨论被分拆到更具体的邮件列表中——例如,网络实现的细节会在 FreeBSD-net@FreeBSD.org*中讨论。

每隔几年,提交者团队会选举少数成员组成核心团队,或者称为Core。Core 的工作既至关重要,又常常被低估和误解。理论上,Core 负责 FreeBSD 的整体管理,但实际上,它几乎不涉及其他事务,主要职责是解决提交者之间的个性冲突和程序冲突。Core 还负责批准新的提交者,并将 FreeBSD 的许多部分的责任委托给个人或团队。例如,它将端口和软件包系统的管理权限委托给端口管理团队。Core 不会为 FreeBSD 设定架构方向,也不会规定流程或程序;这些都由提交者们决定,他们必须集体达成一致。然而,Core 会提出建议、劝导、调解并激励大家。

Core 团队也经历了管理中的最艰难部分。公司管理的几个关键职能包括监督、激励和处理人员之间的问题。监督由成千上万的用户提供,他们在任何东西出故障或表现异常时都会大声抱怨,而 FreeBSD 的提交者则是自我激励的。管理中的棘手部分是解决两人之间的争执,这正是 Core 团队需要处理的部分。说“我是 Core 成员”所获得的地位,并不足以弥补需要管理两位才华横溢的开发人员之间偶尔的争吵,尤其是当他们已经互相惹恼的时候。幸运的是,这种争议很少,并且通常能很快解决。

贡献者

除了提交者团队,FreeBSD 还有成千上万的贡献者。贡献者不需要担心破坏主操作系统源代码库;他们提交补丁供提交者考虑。提交者会评估贡献者的提交内容,决定接受什么,拒绝什么。提交许多高质量补丁的贡献者通常会被邀请成为提交者。

举个例子,我曾花了好几年时间在 FreeBSD 项目中贡献代码,随心所欲地提交补丁。每当我觉得自己浪费了生命时,我会去看看 FreeBSD 的网站,看到我的工作被提交者接受并分发给成千上万的人。在我将这本书的第一版提交给出版社后,我在空闲时间向 FreeBSD FAQ 提交补丁。最终,FreeBSD 文档项目的成员联系了我,并邀请我成为提交者。作为奖励,我得到了一个电子邮件地址,并有机会再次在成千上万的人面前出丑,证明了“好事不常有”的道理。

如果我从未做出任何贡献,我可能仍然是个用户。其实,这也没什么不对。

用户

用户是那些运行 FreeBSD 系统的人。实际估算 FreeBSD 用户的数量几乎是不可能的。尽管像 BSDstats 项目(*www.bsdstats.org/)这样的组织付出了努力,但这些项目是自愿参与的。它们只衡量那些已安装 FreeBSD 并安装了将其系统添加到统计中的软件的人。大多数用户免费下载安装了完整的 FreeBSD,并且从未注册、升级或加入邮件列表。我们无法知道全球有多少 FreeBSD 用户。

由于 FreeBSD 目前是最受欢迎的开源 BSD 系统,这意味着使用 FreeBSD 的机器数量相当庞大。而且,由于一台 FreeBSD 服务器能够处理成千上万个互联网域名,很多网站将 FreeBSD 作为其支持的操作系统。这意味着今天世界上有成千上万,甚至可能是百万计的 FreeBSD 系统管理员。

其他 BSD 系统

FreeBSD 可能是最受欢迎的 BSD,但它并不是唯一的。BSD 4.4-Lite2 衍生出了几个不同的项目,每个项目都有自己的焦点和目标。这些项目又衍生出一些后代,其中有几个至今仍在蓬勃发展。

NetBSD

NetBSD 在许多方面与 FreeBSD 相似,NetBSD 和 FreeBSD 共享开发人员和代码。NetBSD 的主要目标是提供一个安全可靠的操作系统,可以以最小的努力移植到任何硬件平台。因此,NetBSD 能够在 Vixen、PocketPC 设备以及高端 SPARC 和 Alpha 服务器上运行。我曾在我的 HP Jornada 手持电脑上运行 NetBSD。^(2)

OpenBSD

OpenBSD 于 1996 年从 NetBSD 分支出来,目标是成为最安全的 BSD。OpenBSD 是第一个支持硬件加速加密的操作系统,且其开发者为他们的默认安装在数年内几乎免受远程攻击而感到自豪。OpenBSD 团队为世界贡献了几项宝贵的软件,包括 LibreSSL TLS 库和 OpenSSH 套件,几乎每个从 Linux 到 Microsoft 的系统都在使用这些工具。

DragonFly BSD

DragonFly BSD 于 2003 年从 FreeBSD 4 分支出来。它的发展方向与 FreeBSD 不同,采用了一个新的内核消息系统。DragonFly BSD 拥有非常高的性能,其 HAMMER 文件系统支持快照和精细化历史管理。欲了解更多信息,请访问* www.dragonflybsd.org/*。

macOS

Apple 的 macOS?没错,Apple 持续将大量 FreeBSD 的代码块集成到其 macOS 中。如果你在寻找一个稳定的操作系统,既有友好的界面又有强大的内核,macOS 无疑适合你。虽然 FreeBSD 为计算机专业人士提供了一个极好的桌面环境,但我不会把它推荐给普通用户。然而,我毫不犹豫地会把 macOS 推荐给那些随机的用户,而且我会觉得自己做了正确的选择。但 macOS 包含了很多对互联网服务器完全没有必要的东西,并且只在 Apple 硬件上运行,因此我不推荐它作为一个经济实用的通用服务器。

FreeBSD 的后代

一些项目基于 FreeBSD 开发了其他项目或产品。屡获殊荣的 FreeNAS 将一台普通系统转变为网络文件服务器。pfSense 项目将你的系统转变为带有漂亮网页管理界面的防火墙。TrueOS 在支持资源密集型高级功能(如 ZFS)的同时,还为 FreeBSD 增添了一个友好的界面,而 GhostBSD 则在硬件计算能力较弱的设备上提供了一个友好的界面。类似的其他项目不时出现,虽然并非所有都成功,但我敢肯定,在这本书出版时,我们会看到一两个更为成熟的项目加入这一群体。

其他 Unix 系统

其他一些操作系统或多或少地从原始 Unix 派生或模仿。这个列表并不全面,但我会简要提到其中的一些亮点。

Solaris

最著名的 Unix 可能是 Oracle Solaris。Solaris 运行在支持数十个处理器和大量磁盘的高端硬件上。(是的,gobs是一个技术术语,意思是比你可能需要的任何数量都多,而且我非常清楚你需要的磁盘比我认为你需要的要多。)Solaris,尤其是早期版本的 Solaris,具有强大的 BSD 根基。许多企业级应用程序都在 Solaris 上运行。Solaris 主要运行在 Sun 制造的 SPARC 硬件平台上,这使得 Sun 能够支持一些有趣的功能,如热插拔内存和主板。

Oracle 公司在 2009 年收购 Sun Microsystems 时获得了 Solaris。Oracle 于 2016 年停止了 Solaris 的开发。尽管 Solaris 系统仍然有广泛的安装基础,并且你仍然可以从 Oracle 获得 Solaris,但截至今天,Oracle Solaris 已经没有未来了。

illumos

在甲骨文(Oracle)收购 Sun Microsystems 之前,Sun 公司将 Solaris 的大部分代码开源,并且赞助了 OpenSolaris 项目以改进该代码库。OpenSolaris 成功运行了多年,直到 Oracle 关闭了源代码访问并收回了所有 OpenSolaris 资源。

不过,OpenSolaris 的代码仍然是可以访问的。OpenSolaris 社区将 OpenSolaris 分叉成了 illumos (illumos.org/)。如果你怀念 Solaris,你仍然可以使用一个免费的、现代的、类似 Solaris 的操作系统。FreeBSD 包括了 OpenSolaris 中的两个重要特性:Zetabyte 文件系统(ZFS)和 DTrace,一个全系统追踪系统。

AIX

另一个 Unix 的竞争者是 IBM 的 AIX。AIX 的主要特点是其日志文件系统,它会记录所有磁盘事务并在发生时进行处理,从而允许在崩溃后快速恢复。多年来,它也是 IBM 的标准 Unix 系统,而任何由 IBM 支持的系统都会随处可见。AIX 最初是基于 BSD 的,但 AT&T 几乎对一切都进行了修改,因此今天你几乎找不到 BSD 的痕迹了。

Linux

Linux 是 Unix 的一个近亲,从头开始编写。Linux 在很多方面类似于 FreeBSD,尽管 FreeBSD 有着更悠久的历史,并且比 Linux 更适合商业使用。Linux 要求任何分发 Linux 的用户必须将自己所做的更改提供给最终用户,而 BSD 则没有这样的限制。当然,一位 Linux 爱好者会说,“FreeBSD 更容易被商业化开发者利用,而 Linux 则不是。”Linux 开发者相信共享精神,而 BSD 开发者则提供无条件赠送的礼物给大家。这一切都取决于你认为什么更重要。

许多新的 Unix 用户对 BSD 和 Linux 阵营之间的冲突有一定的看法。然而,如果你深入了解,你会发现这些操作系统的大多数开发者都以友好和开放的方式进行交流与合作。只是少数极端的用户和开发者制造了摩擦,类似于不同足球队的球迷暴徒或不同星际迷航系列的粉丝。^(3)

其他 Unix 系统

许多 Unix 系统已经消失,其他的一些则继续挣扎。过去的竞争者包括 Silicon Graphics 的 IRIX、Hewlett-Packard 的 HP/UX、Tru64 Unix 和自杀性的 SCO Group 的 UnixWare。进一步挖掘,你会发现一些较老的遗弃产品,包括 Apple 的 A/UX 和 Microsoft 的 Xenix。(没错,微软曾是一个获得许可的 Unix 供应商,那时恐龙还在紧张地看着天空,而我爸还在为部落仪式狩猎猛犸象。)许多高端应用程序被设计成最好运行在某个特定的 Unix 版本上。所有现代 Unix 系统都从这些老旧操作系统中吸取了经验教训,今天的 Unix 系统和类 Unix 系统非常相似。

为什么是类 Unix?

需要注意的一点是,FreeBSD、Linux 等操作系统被称为类 Unix,而不是UnixUnix一词是 The Open Group 的商标。要获得“Unix”这一称号,操作系统的供应商必须证明其操作系统符合当前版本的单一 Unix 规范。虽然 FreeBSD 一般符合该标准,但持续的测试和再认证需要费用,而 FreeBSD 项目没有多余的资金。被认证为 Unix 还需要有人签署文件,声明他或她不仅对 FreeBSD 符合单一 Unix 规范负责,而且承诺将来会修复任何发现的标准偏差。FreeBSD 的开发模式使得这一点更加困难——尽管会发现漏洞并修复偏差,但没有人能签署一份保证 100%符合标准的文件。

FreeBSD 的优势

经过这些介绍,是什么让 FreeBSD 与众不同?

可移植性

FreeBSD 项目的目标是提供一个可以自由分发、稳定且安全的操作系统,运行在最常见的计算机硬件上。人们还将 FreeBSD 移植到了多种不太流行的平台上。

最受支持的 FreeBSD 平台是由 AMD 开发的常见 64 位硬件,几乎所有人都在使用,甚至 Intel 也在模仿。FreeBSD 还完全支持较旧的 32 位计算机,如 486 和各种型号的奔腾。 本书使用 64 位商用硬件,或称amd64,作为参考平台。

FreeBSD 在其他几种硬件架构上也能运行良好,但这些架构目前还不完全支持,包括 32 位 ARM 处理器和 PowerPC。虽然这些平台并非被忽视,但它们并没有像 x86 和 amd64 那样得到同等的关注。然而,64 位 ARM 平台预计将在本书发布后不久成为一级支持平台。

你还可以将 FreeBSD 安装到某些较旧的架构上,如 64 位 SPARC。这些平台曾经得到了很好的支持,但现在正逐渐退出市场。

性能

由于 FreeBSD 在 486 处理器上运行良好,因此在现代计算机上表现极为出色。拥有一个不需要 8 个核心和 12 GB 内存来运行用户界面的操作系统是相当不错的。因此,你可以真正将硬件资源用于完成实际工作,而不是那些你不关心的任务。如果你选择运行一个漂亮的图形界面,带有各种旋转的小饰品和花哨的铃声,FreeBSD 会支持你,而且如果你选择其他方式,它也不会给你带来惩罚。FreeBSD 还会在最新的 n -CPU 硬件上为你提供支持。

简化的软件管理

FreeBSD 还通过软件包系统和 Ports 集合简化了软件管理。传统上,在类似 Unix 的系统上运行软件需要大量的专业知识。软件包和 Ports 大大简化了这一过程,通过自动化和记录数千个软件包的安装、卸载和配置过程。

我们将在第十五章讨论软件包,在第十六章讨论 Ports。

可定制的构建

FreeBSD 提供了一个无痛的升级程序,但它也允许你根据硬件精确定制操作系统。像苹果这样的公司也在做这件事,但他们控制着硬件和软件;FreeBSD 在普通硬件上完成了同样的操作。

高级文件系统

文件系统是信息如何存储在物理磁盘上的方式——它将文件 我的简历 映射为硬盘上的一系列零和一。FreeBSD 包含两种广泛支持的文件系统,UFS(第十一章)和 ZFS(第十二章)。UFS 已经存在了几十年,并且非常耐用。ZFS 虽然较年轻,但包括了如网络复制和自我修复等功能。

谁应该使用 FreeBSD?

虽然 FreeBSD 可以用作强大的桌面或开发机器,但它的历史显示出明显的网络服务偏向:网页、邮件、文件和辅助应用程序。FreeBSD 因其在互联网服务器方面的优势而最为人知,作为任何网络服务的底层平台,它是一个非常出色的选择。如果像 Netflix 这样的主要公司依赖 FreeBSD 提供可靠的服务,它同样会为你提供稳定的表现。

如果你打算在桌面上运行 FreeBSD(或任何 Unix),你需要了解你的计算机如何工作。如果你需要简单的点击操作,FreeBSD 可能不是最佳选择。如果这是你的目标,建议你购买一台 Mac,这样你可以在需要时使用 Unix 的强大功能,而其余时间不必为此担心。不过,如果你想学习 FreeBSD,那么在桌面上运行它是最好的方法——我们稍后会讨论这一点。

谁应该运行另一个 BSD 系统?

NetBSD 和 OpenBSD 是 FreeBSD 最接近的竞争者。与商业世界的竞争者不同,这种竞争大多是友好的。FreeBSD、NetBSD 和 OpenBSD 自由地共享代码和开发人员;有些人甚至在多个操作系统中维护相同的子系统。

如果你想使用旧的或不常见的硬件,NetBSD 是一个不错的选择。几年间,我曾在一台古老的 SGI 工作站上运行 NetBSD,将它作为域名系统(DNS)和文件服务器使用。它运行得非常好,直到硬件最终冒出一股烟雾并停止工作。

OpenBSD 实现了令人印象深刻的多种安全功能。部分工具最终会被整合到 FreeBSD 中,但这需要几个月甚至几年时间。然而,有些工具永远无法在 FreeBSD 中复制。如果你有真实的安全担忧,并且可以在没有 FreeBSD 提供的功能集的情况下使用类 Unix 系统,可以考虑 OpenBSD。你可以阅读我的书籍《Absolute OpenBSD》(No Starch Press,2013),了解更多介绍。

如果你只是想尝试看看有哪些 BSD 系统,任何一个 BSD 都可以!

谁应该运行专有操作系统?

尽管开源操作系统在不断蚕食市场份额,像 macOS、Windows、AIX 等操作系统仍然非常流行。高端企业通常紧紧依赖于商业操作系统。虽然这种情况正在慢慢变化,但在这些环境中,你可能还是不得不依赖商业操作系统。不过,偶尔加入一台 FreeBSD 机器来处理基本服务,比如监控和部门文件服务,可以大大简化工作,并且成本更低。像 Dell/EMC/Isilon 这样的公司就通过使用 FreeBSD 而非商业操作系统建立了整个业务。

当然,如果你需要的某个软件仅能在专有操作系统上运行,那么你的选择就非常明确了。尽管如此,始终向供应商询问是否有 FreeBSD 版本;你可能会感到惊喜。

如何阅读本书

许多计算机书籍厚重得足以让一头牛晕倒,前提是你有足够的力量将它们抬得足够高。而且,它们的内容要么是百科全书式的广泛覆盖,要么是详细到让人难以真正读完的程度。当你被告知点击“确定”或接受许可协议时,真的需要参考截图吗?你上次真的坐下来阅读百科全书是什么时候?

Absolute FreeBSD 有些不同。它是设计为一次性从头到尾阅读的。你可以跳着读,但每一章都建立在前面的内容基础上。虽然这本书不小,但比许多流行的计算机书籍要小。读完一遍之后,它可以作为一个不错的参考书。

如果你是计算机书籍的常客,欢迎随意插入一些通常的建议,比如“每次阅读一章以达到最佳学习效果”等等。我不会宠爱你——如果你拿起这本书,说明你要么有两颗脑细胞能互相碰撞,要么你正在拜访一个有这两颗脑细胞的人。(如果是后者,希望你的主人足够聪明,在你学到足够多的知识,变得有点危险之前,就把这本书拿走。)

你必须知道什么?

本书面向的是新的 Unix 管理员。三十年前,普通的 Unix 管理员有内核编程经验,并且正在攻读计算机科学硕士学位。即使是十年前,他们已经是熟练的 Unix 用户,拥有一定的编程技能,并且大多拥有计算机科学学士学位。如今,类 Unix 操作系统已经可以免费获取,计算机比食物还便宜,甚至 12 岁的孩子也能运行 Unix,阅读源代码,并学到足以让年长者感到自愧不如的知识。因此,我并不指望你在启动系统之前就掌握大量 Unix 知识。

为了充分发挥本书的潜力,你需要熟悉一些基本任务,比如如何更改目录、列出目录中的文件,以及如何使用用户名和密码登录。如果你不熟悉基本命令和 Unix shell,建议你从 Evi Nemeth 等人所著的《UNIX 系统管理手册》(Prentice Hall PTR,2017)这本书开始。为了方便新的系统管理员,我提供了产生所需结果的确切命令。如果你更喜欢通过示例学习,你应该在这里找到所有需要的内容。

你还需要了解一些计算机硬件知识——不需要太多,适量即可。了解如何识别 SATA 数据线是有帮助的。你对这些知识的需求取决于你所使用的硬件,但如果你有足够兴趣拿起这本书并读到这里,你大概已经了解得足够多了。

对于新手系统管理员

如果你是 Unix 新手,学习的最好方式就是亲自实践。不是的,我不是建议你和罗威尔一起吃饭。如果你经营一家狗粮公司,你肯定希望生产一种你自己的狗会开心吃的产品。如果你的狗对你最新的配方嗤之以鼻,那你就有问题了。这里的重点是,如果你使用某个工具或创造某样东西,你应该亲自去使用它。同样的道理也适用于任何类 Unix 操作系统,包括 FreeBSD。

Desktop FreeBSD

如果你真心想学习 FreeBSD,我建议你把主计算机上的操作系统抹掉,改为运行 FreeBSD。不是像 TrueOS 或 GhostBSD 这样的桌面导向的 FreeBSD 衍生版本:直接运行原版 FreeBSD。是的,我知道,现在这种“狗粮”听起来不那么糟糕了。但学习操作系统就像学习一门语言;完全沉浸式学习是最迅速、最有效的学习方式。这就是我当时做的,今天我可以让一个类似 Unix 的系统做我想做的任何事。我已经在一台 FreeBSD 笔记本上写了整本书,使用开源文本编辑器 XEmacs 和 LibreOffice.org 办公套件。我还使用 FreeBSD 看电影,刻录并听 MP3,平衡我的银行账户,处理我的电子邮件,浏览网页。实验室里的桌面上运行着十二个动画的 BSD 守护进程,它们在窗口管理器周围跑来跑去,我偶尔会停下来,用鼠标点击它们。如果这不算一个“愚蠢桌面技巧”,那我就不知道什么算了。^(4)

许多 Unix 系统管理员现在都来自 Windows 背景。他们在自己的小世界里忙碌着,这时他们的经理突然出现,说:“你能再处理一个系统吗?我很高兴听到这个消息!顺便告诉你,它是一个 Unix 系统。”然后消失在管理层的空气中。一旦这位新 Unix 管理员决定不辞职,去开始一份新鲜而激动人心的鲸鱼尸检技术员的工作,她就小心翼翼地尝试操作系统。她了解到,ls 类似于 dircd 在两个平台上都是相同的。她可以通过死记硬背、阅读和经验来学习命令。她无法学到的是,来自这种背景的她,如何理解 Unix 机器是如何思考的。Unix 不会根据你来调整,你必须适应它。Windows 和 macOS 也需要类似的调整,但它们将这些调整隐藏在华丽的外表之下。考虑到这一点,让我们花些时间来学习如何思考 Unix。

如何思考 Unix

现在,大多数 Unix 系统自带相当不错的图形用户界面(GUI),但它们仅仅是装饰。无论桌面看起来多么华丽,真正的工作还是发生在命令行上。Unix 的命令行实际上是 Unix 的优势之一,正是它赋予了系统无与伦比的灵活性。

Unix 的基本哲学是 许多小工具,每个工具都能很好地完成一项任务。我的邮件服务器的本地程序目录(/usr/local/bin)里有 262 个程序。我安装了其中的每一个,要么是直接安装,要么是间接安装。大多数程序都是小巧简单的,只执行一项任务。这种小工具的集合使得 Unix 极其灵活和可扩展。许多商业软件包尝试做所有事情;它们拥有各种各样的功能,但核心功能的表现通常平庸。记住,曾经你需要是一个程序员才能使用 Unix 系统,更不用说运行它了。程序员不介意自己构建工具。Unix 的管道概念鼓励了这一点。

管道

习惯了图形界面环境(如 Windows 和 macOS)的人可能不太熟悉 Unix 如何处理输入和输出。他们习惯了点击某个东西并看到一个确认消息、错误、什么都没有,或者(更常见的是)一个漂亮的蓝色屏幕,上面用一种叫做 Geek 的语言解释为什么系统崩溃了。Unix 做事情有些不同。

Unix 程序有三种通信渠道,或称 管道:标准输入、标准输出和标准错误。一旦你理解了这些管道是如何工作的,你就离理解整个系统更进一步了。

标准输入 是信息的来源。当你在控制台输入命令时,标准输入就是来自键盘的数据。如果一个程序正在监听网络,那么标准输入就是网络。许多程序可以重新安排标准输入,以接受来自网络、文件、另一个程序、键盘或任何其他来源的数据。

标准输出 是程序的输出显示的地方。通常这是控制台(屏幕)。网络程序通常将它们的输出返回到网络。程序可能将输出发送到文件、另一个程序、网络或计算机可以访问的任何其他地方。

最后,标准错误 是程序发送错误信息的地方。通常,控制台程序将错误信息返回给控制台;其他程序则将错误记录在文件中。如果你错误地设置了程序,它可能会丢弃所有错误信息。

这三种管道可以任意排列,这个概念可能是新 Unix 用户和管理员面临的最大障碍。例如,如果你不喜欢在终端上显示的错误信息,你可以将它们重定向到文件。如果你不想重复输入很多信息,你可以把信息放入文件中(以便重用),然后将文件内容导入命令的标准输入。或者,更好的是,你可以运行一个命令生成这些信息并将其放入文件中,或者直接将第一个命令的输出通过管道发送到第二个命令,而根本不需要使用文件。

小程序、管道和命令行

如果将这些输入/输出管道及各种工具推向其逻辑极限,它们看起来可能令人不知所措。当我在最初的 Unix 培训课程中看到一个系统管理员输入类似以下的内容时,我曾认真考虑过要不要换个职业。

$ tail -f /var/log/messages | grep -v popper | grep -v named &

一行行难以理解的文本开始在屏幕上滚动,而且源源不断。而更糟糕的是,我的导师还在输入,胡言乱语不停地冒出来!如果你来自一个点选式计算环境,那么像这样的长串命令绝对会让你感到害怕。那些奇怪的词是什么意思?还有一个和符号?你让我学什么

把学习使用命令行当作学习一门语言来理解。当学习一门语言时,我们从简单的单词开始。随着词汇量的增加,我们也学会了如何将这些单词连在一起。我们了解到,按特定顺序排列单词是有意义的,而不同的顺序则毫无意义。你三岁时说话肯定不那么流利——给自己一点时间,你会做到的。

小巧简单的程序和管道提供了几乎无限的灵活性。你有没有曾经希望能将一个程序中的某个功能应用到另一个程序中?通过使用各种小程序并按照自己的需求安排输入和输出,你可以让 Unix 系统以任何你想要的方式运行。最终,如果你不能像这样通过 | sort -rnk 6 | less 来运行一个命令的输出,你会感觉自己被束缚住了。^(5)

万物皆文件

你在 Unix 环境中呆得久了,肯定会听到“万物皆文件”。程序、账户信息和系统配置都存储在文件中。Unix 没有 Windows 风格的注册表;如果你备份了这些文件,你就备份了整个系统。

更重要的是,系统将硬件识别为文件!你的 CD-ROM 驱动器是一个文件,/dev/cd0。串口以像 /dev/cuaa0 这样的文件形式出现。甚至虚拟设备,如数据包嗅探器和硬盘分区,也是文件。

当你遇到问题时,请记住这一点:万物皆文件,或者至少某处的文件里有它。你要做的就是找到它!

第三版说明

Absolute BSD(No Starch Press,2002 年)是我的第一本技术书,它写作时,各种 BSD 操作系统有更多的相似之处,尽管它们并不愿意承认。第二版 Absolute FreeBSD(No Starch Press,2007 年)在 BSD 系统分歧之后出版,详细介绍了过去五年 FreeBSD 的进展。随着又一个十年的发展,FreeBSD 已经进化到能够与最好的商业操作系统竞争的程度。你将会发现多种顶级的文件系统。磁盘管理也发生了变化,以适应新的分区方法。虚拟化如今已经成为现实,FreeBSD 既可以作为客户端也可以作为主机来支持虚拟化。

这种发展推动了本书的变化。

我们不会讨论如何配置邮件、DNS 或 Web 服务器。这些任务现在有比以往更多的软件选择。已经有整本书专门讲述这些选择及其使用方法。我也写过其中一些书。为了腾出空间介绍 FreeBSD 特定的内容,如 ZFS 和 Jail,这些话题被删减了。

其中一些新功能非常复杂。对 ZFS 的完整覆盖将填满整本书——我知道,因为我也写过那些书。FreeBSD 支持许多专用文件系统,每一个对需要它们的人都极为有用,而对不需要的人则完全无关紧要。与其写一本没人会读的巨著,我选择覆盖每个 FreeBSD 系统管理员必须知道的内容。如果你对某个特定话题的更深入内容感兴趣,它也可以提供。

一些子系统正在进行大规模的修订。我本可以等到每个 FreeBSD 子系统都有稳定接口后再写这本书,但那时书可能永远也出版不出来。写这本书时,bhyve 开发者正在积极地重构整个配置系统。在选择是简单略过一个话题还是提供完全错误的材料时,我选择跳过对 bhyve 的详细介绍。我希望在这本书出版之前能够删除这一段内容。

我已无情地删除了本版中的过时信息。例如,现代硬盘驱动器通常不需要担心写入缓存。如果你发现你记得使用的一些建议在本书中没有出现,请查阅 FreeBSD 的信息资源,看看这些建议是否仍然适用。

本书内容

Absolute FreeBSD 第 3 版包含以下章节。

第一章:获取更多帮助

本章讨论了 FreeBSD 项目及其热心支持者为用户提供的信息资源。没有一本书可以涵盖所有内容,但知道如何使用互联网上的 FreeBSD 资源可以帮助填补你在这里找到的任何空白。

第二章:安装前须知

安装 FreeBSD 并不难。然而,如果在安装过程中做出错误选择,你将得到一个不适合你需求的系统。避免重新安装的最佳方法是事先考虑你的需求,并做出所有决策,这样实际安装时就不需要再动脑筋。

第三章:安装

本章概述了使用不同的分区方案和文件系统安装 FreeBSD。

第四章:启动我吧!启动过程

本章介绍了 FreeBSD 启动过程以及如何使你的系统在不同配置下启动、停止和重启。

第五章:在你弄坏其他东西之前,先读这本书!(备份与恢复)

在这里,我们讨论如何在系统级别和按文件级别备份数据,以及如何进行更改以便能够轻松撤销。

第六章:内核游戏

本章描述了配置 FreeBSD 内核。与其他一些操作系统不同,FreeBSD 要求你根据自己的需求来调整内核。这给了你巨大的灵活性,并且让你能够优化硬件的潜力。

第七章:网络

在这里,我们讨论了支撑现代互联网的 TCP/IP 协议,包括第 4 版和第 6 版。

第八章:配置网络

FreeBSD 不仅能疯狂快速地处理数据包,它还支持虚拟局域网、链路聚合等功能。我们将在这里配置这些功能。

第九章:保护你的系统

本章教你如何让你的计算机抵抗攻击者和入侵者。

第十章:磁盘、分区与 GEOM

本章介绍了在 FreeBSD 中使用硬盘的一些细节。使用现代硬件意味着需要理解多个分区方案、磁盘对齐以及 FreeBSD 的磁盘管理基础设施。

第十一章:Unix 文件系统

UFS 已经是 FreeBSD 的标准文件系统数十年了,UFS 的概念贯穿整个操作系统。无论你是否打算使用 UFS,都必须了解其基本要素。

第十二章:Z 文件系统

ZFS 是一种较新的文件系统,在大型系统中非常流行。如果你需要管理大量数据,你会需要 ZFS。

第十三章:外部文件系统

每个系统管理员都需要通过网络挂载磁盘或使用 ISO 文件,而无需将其烧录到 CD 上。本章将带你完成这些任务,并介绍 FreeBSD 特有的文件系统,如 devfs。

第十四章:探索 /etc

本章介绍了 FreeBSD 中的多个配置文件以及它们的工作方式。

第十五章:让你的系统更有用

在这里,我描述了 FreeBSD 用来管理附加软件的包管理系统。

第十六章:使用端口定制软件

有时预构建的软件包无法满足你所有的需求。你可以利用 FreeBSD 的包构建系统来创建你自己的软件包,定制以满足你的特定需求。

第十七章:高级软件管理

本章讨论了在 FreeBSD 系统上运行软件的一些细节。

第十八章:升级 FreeBSD

本章教你如何使用 FreeBSD 的升级过程。升级系统是所有操作系统中最卓越且最流畅的之一。

第十九章:高级安全功能

在这里,我们讨论了一些 FreeBSD 中更有趣的安全功能。

第二十章:小型系统服务

在这里,我们讨论了一些你需要管理的小程序,以便正确使用 FreeBSD。

第二十一章:系统性能与监控

本章介绍了 FreeBSD 的一些性能测试和故障排除工具,并展示了如何解读结果。我们还讨论了日志记录和 FreeBSD 的 SNMP 实现。

第二十二章:监狱

FreeBSD 拥有一个类似于 Linux 和 Solaris 容器的进程隔离子系统,叫做 jails。我们将介绍 jail 系统,以及如何利用它提高系统安全性。

第二十三章:FreeBSD 的边缘

本章将教你一些关于 FreeBSD 的有趣技巧,例如如何在没有磁盘或使用小磁盘的情况下运行系统,以及一些适合云环境的功能,比如 libxo。

第二十四章:问题报告与崩溃

本章将教你如何处理 FreeBSD 系统出现故障的罕见情况,如何调试问题,以及如何创建一个有用的问题报告。

你还会找到一份注释书目、后记以及一份非常棒的专业编制索引。

好的,前言就到这里。接下来继续!

第一章:获取更多帮助**

image

这本书虽然厚重,但仍然无法覆盖你必须了解的关于 FreeBSD 的所有内容。毕竟,Unix 已经存在将近 50 年,BSD 接近 40 年,而 FreeBSD 足够老,足以获得博士学位。即便你背下这本书,它也无法涵盖你可能遇到的每种情况。FreeBSD 项目支持大量的信息资源,包括众多的邮件列表和 FreeBSD 网站,更不用提官方手册和指南了。它的用户在更多网站上维护着更多文档。信息的洪流本身就可能让人不知所措,甚至让你想要直接给全世界发邮件请求帮助。但是在你向邮件列表或论坛发送问题之前,先确认一下你所需要的信息是否已经存在。

为什么不请求帮助?

FreeBSD 提供了两种流行的资源来获取帮助:邮件列表和论坛。两者的许多参与者都非常有经验,能够非常迅速地回答问题。但是,当你向这些社区支持资源发送问题时,你是在请求全世界成千上万的人花一点时间阅读你的信息。你还在请求其中一人或多人花时间帮助你,而不是看自己喜欢的电影、与家人共进晚餐或补觉。当这些专家重复回答同一个问题 10 次、50 次,甚至几百次时,问题就来了。他们变得不耐烦,有些人甚至会变得相当暴躁。

更糟糕的是,这些人中的许多人已经花费了大量的时间和精力,把大部分问题的答案提供到其他地方。如果你表明你已经搜索过这些资源,而答案确实没有出现在其中,你很可能会收到一份礼貌而有帮助的答复。然而,如果你提出一个已经被问过几百次的问题,那位专家可能会忍不住大发雷霆。做足功课的话,你很可能会比单纯发起求助请求更快得到答案。

FreeBSD 的态度

“家庭作业?你是什么意思?我是不是回到学校了?你想要什么,跪地上的祭品吗?” 是的,你是在上学。信息技术行业不过是一个终身的、自我引导的学习过程。要么适应,要么退出。另一方面,跪地的祭品很难通过电子邮件传递,而且今天也不太有用。

大多数商业软件都隐藏了其内部工作原理。你对它们的唯一接触途径是供应商提供的选项。即使你想了解某个东西是如何工作的,你可能也无法得知。当某些东西坏了,你唯一的选择就是打电话给供应商并请求帮助。更糟糕的是,那些被付钱帮助你的人通常知道的也不过比你多一点。

如果你从未与开源软件供应商合作过,FreeBSD 的支持机制可能会让你感到惊讶。这里没有免费电话可以拨打,也没有供应商可以升 escal。你不能与经理交谈,这也是有充分理由的:你就是经理。恭喜你升职了!

支持选项

话虽如此,你并不完全孤立。FreeBSD 社区包括众多开发者、贡献者和用户,他们非常关心 FreeBSD 的质量,乐于与愿意承担责任的用户合作。FreeBSD 提供了你所需的一切:完整的源代码访问,用于将源代码转换为程序的工具,以及开发人员使用的调试工具。没有任何东西被隐藏;你可以看到内部的所有内容,尽管可能会有一些瑕疵。你可以查看 FreeBSD 自诞生以来的开发历史,包括每一次修改和原因。这些工具可能超出了你的能力范围,但那不是项目的问题。许多社区成员甚至乐意在你发展技能的过程中提供指导,帮助你自己使用这些工具。你会在履行责任的过程中获得大量的帮助。

一般来说,人们更愿意帮助与自己相似的人。如果你想使用 FreeBSD,你必须从依赖供应商提供的现成工具,转向学习如何自己动手“做饭”。每一位 FreeBSD 用户都学习过如何使用它,他们热烈欢迎感兴趣的新用户。如果你只是想知道输入什么,而不真正理解幕后发生了什么,那么你最好阅读文档;因为一般的 FreeBSD 支持社区并没有动力去帮助那些不愿自助或者无法遵循指令的人。

如果你想使用 FreeBSD,但既没有时间也没有兴趣深入了解,考虑投资商业支持合同。它或许无法让你直接联系到 FreeBSD 的拥有者,但至少你会有一个可以抱怨的人。你可以在 FreeBSD 网站上找到几家商业支持提供商。

另一个重要的点是要记住,FreeBSD 项目只维护 FreeBSD。如果你遇到其他软件的问题,FreeBSD 邮件列表并不是寻求帮助的地方。FreeBSD 的开发者通常精通各种软件,但这并不意味着他们愿意帮助你,例如配置 KDE。

那么,你的第一项任务是了解本书以外的资源。这些包括集成手册、FreeBSD 网站、邮件列表归档和其他网站。

手册页

手册页manual pages 的简称)是展示 Unix 文档的最初方式。虽然手册页因晦涩、难懂甚至让人无法理解而声名狼藉,但它们其实对特定的用户来说非常友好。当手册页最初创建时,普通的系统管理员是 C 程序员,因此这些页面是由程序员为程序员编写的。如果你能像程序员一样思考,手册页对你来说是完美的。我曾尝试过像程序员一样思考,但只有在连续熬夜两天之后才真正取得成功。(大量咖啡因和高烧帮助我成功了。)

在过去的几年中,系统管理员所需的技能水平降低了;你不再必须是程序员。同样,手册页也变得越来越易读。然而,手册页并不是教程;它们解释的是某个特定程序的行为,而不是如何实现某个目标。虽然它们既不友好也不安慰人,但它们应该是你的第一道防线。如果你在没有查看手册的情况下向邮件列表提问,你可能会收到一个简短的man whatever作为回复。

手册章节

FreeBSD 手册分为九个部分。粗略地说,这些部分是:

  1. 一般用户命令

  2. 系统调用和错误号

  3. C 编程库

  4. 设备和设备驱动程序

  5. 文件格式

  6. 游戏说明

  7. 杂项信息

  8. 系统维护命令

  9. 内核接口

每个手册页都以它所记录的命令名称开头,后跟括号中的章节号,例如:reboot(8)。当你在其他文档中看到这种格式时,它表示你需要在手册的该章节中阅读该手册页。几乎每个主题都有相应的手册页。例如,要查看编辑器 vi 的手册页,输入以下命令:

$ man vi

作为回应,你应该看到以下内容:

VI(1)      FreeBSD General Commands Manual           VI(1)

NAME
     ex, vi, view - text editors

SYNOPSIS
     ex [-FRrSsv] [-c cmd] [-t tag] [-w size] [file ...]
     vi  [-eFRrS] [-c cmd] [-t tag] [-w size] [file ...]
     view [-eFrS] [-c cmd] [-t tag] [-w size] [file ...]

DESCRIPTION
     vi is a screen-oriented text editor.  ex is a line-oriented text editor.
     ex and vi are different interfaces to the same program, and it is
     possible to switch back and forth during an edit session.  view is the
     equivalent of using the -R (read-only) option of vi.
:

手册页开始时会显示手册页的标题(vi)和章节号(1),然后给出该页面的名称。这个特定页面有三个名称:ex、vi 和 view。输入 man exman view 会带你进入这个相同的页面。

浏览手册页

一旦进入手册页,按空格键或 PGDN 键可以向前翻一整页。如果你不想翻得那么远,按回车键或向下箭头键可以向下滚动一行。按 b 键或 PGUP 键可以向后翻一页。要在手册页中搜索,输入 / 后跟你要查找的词。你将跳转到该词的第一次出现,并且该词会被高亮显示。之后按 n 键可以跳转到该词的下一个出现位置。

这假设你正在使用默认的 BSD 分页工具 more(1)。如果你使用的是其他分页工具,请使用该分页工具的语法。当然,如果你对 Unix 了解得足够多,已经设置了你偏好的默认分页工具,那么你可能已经跳过了本书的这一部分。

查找手册页

新用户常说,如果他们能找到正确的页面,他们会很乐意阅读 man 页面。你可以通过 apropos(1) 和 whatis(1) 在 man 页面上执行基本的关键词搜索。要搜索包含你指定词汇的任何 man 页面名称或描述,请使用 apropos(1)。要仅匹配完整的单词,请使用 whatis(1)。例如,如果你对 vi 命令感兴趣,你可以尝试以下操作:

$ apropos vi
unvis(1) - revert a visual representation of data back to original form
vidcontrol(1) - system console control and configuration utility
vis(1) - display non-printable characters in a visual format
madvise, posix_madvise(2) - give advice about use of memory
posix_fadvise(2) - give advice about use of file data
--snip--

这将继续显示 581 个条目,可能比你希望查看的还要多。不过,这些条目大多数与 vi(1) 无关;字母 vi 只是出现在名称或描述中。设备驱动程序 是手册中一个相当常见的术语,所以这并不令人惊讶。另一方面,在这种情况下,whatis(1) 提供了更有用的结果。

$ whatis vi
vi, ex, view, nex, nvi, nview(1) - text editors
$

我们只得到了一个结果,明显与 vi(1) 相关。在其他搜索中,apropos(1) 比 whatis(1) 提供了更好的结果。试着两个都使用,你会很快学会它们如何适应你的风格。

man -k 命令模拟了 apropos(1),而 man -f 则模拟了 whatis(1)。

章节号与 Man

你可能会发现有些命令在手册的多个部分中都出现。例如,每个 man 部分都有一个介绍性的 man 页面,解释该部分的内容。要指定一个部分来查找 man 页面,只需在 man 命令后面直接输入数字。

$ man 3 intro

这会调出手册第三部分的介绍。我建议你阅读手册中每个部分的介绍页面,哪怕只是帮助你理解可用信息的广度和深度。

Man 页面内容

Man 页面分为几个部分。虽然作者可以在 man 页面中放入任何他或她喜欢的标题,但有一些是标准的。有关这些标题以及其他 man 页面标准的部分列表,请参阅 mdoc(7):

  • NAME 给出了程序或工具的名称。有些程序有多个名称——例如,vi(1) 文本编辑器也可以作为 ex(1) 和 view(1) 提供。

  • SYNOPSIS 列出了可能的命令行选项及其参数,或者如何访问一个库调用。如果我已经熟悉一个程序,但就是记不住我要找的选项,我发现这个部分足够提醒我需要的内容。

  • DESCRIPTION 包含程序、库或功能的简要描述。本节的内容根据主题的不同而变化,因为程序、文件和库有着非常不同的文档要求。

  • OPTIONS 给出程序的命令行选项及其效果。

  • BUGS 描述已知的代码问题,通常可以避免很多麻烦。你有多少次与计算机问题斗争,最后才发现它在那些情况下并不像你预期的那样工作?BUGS 部分的目标是通过描述已知的错误和其他异常情况来节省你的时间。^(1)

  • EXAMPLES 提供了程序的使用示例。许多程序非常复杂,几个使用示例比任何选项列表更能帮助理解。

  • HISTORY 显示命令或代码何时被添加到系统中,如果它不是 FreeBSD 原生的,它的来源在哪里。

  • SEE ALSO 通常是 man 页面中的最后一节。记住,Unix 就像一种语言,系统是一个相互关联的整体。就像胶带一样,SEE ALSO 链接把一切连接在一起。

如果你此刻无法访问手册页面,许多网站提供了相关内容,其中包括 FreeBSD 的官方网站。

FreeBSD.org

FreeBSD 网站 (www.freebsd.org/) 包含了关于 FreeBSD 一般管理、安装和管理的各种信息。最有用的部分是《手册》、FAQ 和邮件列表档案,但你还会找到涵盖众多主题的大量文章。除了关于 FreeBSD 的文档外,网站还包含了大量关于 FreeBSD 项目内部管理和项目各部分状态的信息。

Web 文档

FreeBSD 文档分为文章和书籍。两者的区别非常任意:一般而言,书籍比文章长,覆盖的主题更广,而文章较短,专注于单一主题。最应该吸引新用户的两本书是《手册》和《常见问题解答》(FAQ)。

《手册》是 FreeBSD 项目的教程式手册。它持续更新,描述了如何执行基本系统任务,是你启动项目时的一个极好参考。我故意选择不在本书中包含某些主题,因为它们在《手册》中已有足够的覆盖。

FAQ 旨在为 FreeBSD 邮件列表中最常见的问题提供快速回答。有些答案不适合包含在《手册》中,而另一些则直接指向适当的《手册》章节或文章。

其他几本书涵盖了各种主题,如《FreeBSD 开发者手册》、《Porter 手册》和《FreeBSD 架构手册》。

在大约 50 篇可用的文章中,有些仅因历史原因保留(例如原始的 BSD 4.4 文档),而其他则讨论系统特定部分的细节,如串口或构建过滤桥接。

另一方面,官方文档也经过了删减。《手册》和《常见问题解答》涵盖了当前的 FreeBSD 版本,文档团队严厉删除了过时的信息。如果你想知道什么与当前的 FreeBSD 兼容,请查看《手册》。

这些文档非常正式,需要准备。因此,它们总是稍微滞后于现实世界。当一个新特性首次推出时,相应的手册条目可能要过几周或几个月才会出现。如果网页文档看起来过时了,那么最及时的答案来源就是邮件列表归档。

邮件列表归档

除非你真的是站在技术的最前沿,否则可能有人在以前遇到过你所遇到的问题,并将相关问题发布到邮件列表中。毕竟,邮件归档可以追溯到 1994 年,包含了数百万封邮件。唯一的问题是,这些邮件中有数百万条信息,每一条可能都包含你正在寻找的答案。虽然 FreeBSD.org 网站有自己的搜索引擎,但你也可以使用任何一个能索引 lists.FreeBSD.org/ 的搜索引擎。

在查看邮件列表的归档时,一定要检查日期。邮件列表是永久存在的。1995 年关于硬件问题的讨论可能会让你觉得自己是经历过糟糕主板问题的系统管理员历史的一部分,^(2),但它可能帮不了你解决新服务器的问题。这些古老的信息基本上是“不死的文档”,从坟墓中复生,给你虚假的希望。不过,它们是项目历史的一部分,不会被删除。

论坛

和许多其他开源项目一样,FreeBSD 也有一个在线论坛,* forums.FreeBSD.org/ *。论坛就像是专为网页设计的邮件列表,只是我们这些老家伙并不怎么喜欢它们。不过,你确实可以在论坛上找到很多很好的讨论和教程,它们是宝贵的信息来源。

许多人也在论坛上发布了冗长的教程。基于论坛的教程应该正式地进入手册或官方文章,但目前还没有人将它们移过来。在跟随这些教程之前,先阅读关于这些教程的讨论;人们通常会指出错误或例外,或者评论说,整个教程在新版 FreeBSD 下已经过时。如果你想参与 FreeBSD 项目,将这些教程转换为官方文档将是一个很好的起点。

论坛对于非常老的信息问题不太严重,但仅仅因为它们在 2009 年才变成了官方平台。当论坛进入四分之一世纪时,它们也会积累相同数量的不死文档。不过,到时候,可能会有更先进的讨论系统出现——或者,也许我们会有更好的方法来索引和提取在线讨论中的有用信息。

其他网站

FreeBSD 的用户创建了大量的网站,你可以在这些网站上寻找答案、帮助、教育资源、产品和一般的交流。几乎每个聚合网站,如 lobste.rs 和 Reddit,都有一个 FreeBSD 部分,你可以在这里获取新的帖子和文章的链接。通过这些链接,你会进入一个充满博客的世界。此外,许多托管公司也提供大量的 FreeBSD 教程。虽然这些教程是面向公司的客户的,但它们通常对所有人都有很大的帮助。

最受欢迎的 FreeBSD 网站之一是 FreshPorts,* www.FreshPorts.org/*。FreshPorts 跟踪 FreeBSD 的变化。最初,它跟踪通过 Ports 系统提供的附加软件的变化(我将在第十六章中讨论),但它很快扩展到了跟踪基础系统、文档、网站等的变化。如果你想看看 FreeBSD 的变化,从 FreshPorts 开始。

FreeBSD Journal (www.freebsdfoundation.org/journal/) 是 FreeBSD 基金会的一个项目。它是一个商业项目,但你的订阅费用直接用于基金会。期刊文章由一些经验最丰富的 FreeBSD 开发者和用户审阅,因此这些文章可以被认为是权威的。虽然作为编辑委员会成员,我有些偏见。

FreeBSD 基金会 (www.freebsdfoundation.org/) 支持 FreeBSD 的开发,我鼓励大家捐助一些资金。像项目列表这样的页面对我来说很有用。这个列表列出了 FreeBSD 基金会资助的所有开发项目以及它们的当前状态。在写这段话时,我发现 FreeBSD 新增了无线网状网络支持和多路径 TCP。虽然我现在不需要这两个功能,但谁知道下周会发生什么呢?

四处看看,你会找到你自己喜欢的资源。

使用 FreeBSD 问题解决资源

好的,让我们用 FreeBSD 的资源来调查一个常见问题。几十年来,人们一直在询问 FreeBSD 对密码学的支持情况,因此我们来找出它支持和不支持的密码学功能的确切答案。

密码学是一个复杂的主题,搜索相关信息时会遇到很多复杂性,尤其是不同的术语。有时它会被称为“cryptography”(密码学)、“cryptographic”(加密的)、非正式的“crypto”(加密)或相关的词汇,如“encrypt”(加密)。我们会尝试这些所有的搜索方式。

检查手册和 FAQ

浏览手册的目录,可以看到有关“加密磁盘分区”和“加密交换分区”的条目,这些似乎是相关的。FAQ 中也指向了这些主题。这是一个开始。那些条目会引导你查阅相关的手册页,进而发现更多的手册页。

检查手册页

让我们通过aproposwhatis命令查询手册页中的密码学相关信息。

$ apropos cryptography
krb5_allow_weak_crypto, krb5_cksumtype_to_enctype...
crypto, cryptodev(4) - user-mode access to hardware-accelerated cryptography

这只是两个条目,但第一个条目非常长。任何以krb5开头的条目都与 Kerberos 认证相关,这对大规模网络来说是一个关键功能,并且涉及加密技术,因此它是相关的。第二个条目有点有趣:有一个关于 crypto(4)和 cryptodev(4)的手册页面。(这两个词指向同一个 4 节的手册页面,因此数字只出现在一个搜索结果中。)我们来看看这个手册页面。

$ man crypto
crypto(3)                  OpenSSL                 crypto(3)

NAME
       crypto - OpenSSL cryptographic library

SYNOPSIS
DESCRIPTION
       The OpenSSL crypto library implements a wide range of cryptographic
       algorithms used in various Internet standards. The services provided by
       this library are used by the OpenSSL implementations of SSL, TLS and
       S/MIME, and they have also been used to implement SSH, OpenPGP, and
       other cryptographic standards.

OVERVIEW
       libcrypto consists of a number of sub-libraries that implement the
       individual algorithms.
--snip--

等一下——OpenSSL 并不是一个加密设备。显然有什么不对劲。仔细看看这个手册页面,它来自手册的第三部分,即 C 库部分。你需要在手册中搜索其他包含crypto的条目。我们来尝试更具体的whatis(1)搜索。

$ whatis crypto
crypto, cryptodev(4) - user-mode access to hardware-accelerated cryptography
crypto(7) - OpenCrypto algorithms
crypto, crypto_dispatch, crypto_done, crypto_freereq, crypto_freesession, crypto_get_driverid, crypto_getreq, crypto_kdispatch, crypto_kdone, crypto_kregister, crypto_newsession, crypto_register, crypto_unblock, crypto_unregister, crypto_unregister_all, crypto_find_driver(9) - API for cryptographic services in the kernel

成功了!我们找到了三个 crypto 手册页面:一个在第四部分,一个在第七部分,还有一个在第九部分。这些页面为我们提供了关于程序访问硬件加密功能的接口、支持的算法列表以及内核加密服务的描述。阅读这些将帮助你很好地了解 FreeBSD 的加密支持。每个页面中的 SEE ALSO 链接将引导你获取更多信息。现在,你可以将加密知识填满你的大脑。

邮件列表归档和论坛

虽然邮件列表和论坛是不同的平台,但你可以以类似的方式搜索它们。你可以使用 FreeBSD 网站的搜索引擎搜索邮件列表的归档,但我更喜欢使用 Google 或 DuckDuckGo。搜索crypto site:lists.FreeBSD.org会返回一堆结果,crypto site:forums.FreeBSD.org也是如此。

使用这类讨论来获取某个话题的通用指导存在一个问题,那就是人们会深入探讨细节。你不会得到关于加密学的概览,而是会发现关于这个算法在那个硬件加速上应用的细节,或者在那个版本的 FreeBSD 中的细节。如果你进行非常详细的搜索,你可以得到一个非常详细的答案。

使用您的答案

你从问题中得到的任何答案都会做出一定的假设。如果你在讨论加密学,讨论假设你知道加密为什么重要、明文与密文的区别以及什么是密钥。这是解决基本问题所需的典型知识水平。如果你得到的答案超出了你的理解范围,你需要做研究来理解它。虽然经验丰富的开发者或系统管理员可能不愿意解释公钥加密,但如果你礼貌地请求,他们可能愿意为你指引一个解释这些概念的网页。始终记住,自 1994 年以来,关于 FreeBSD 的问题就有人在提,而关于 Unix 的问题已经有近半个世纪的历史了。

寻求帮助

当你最终决定请求帮助时,请以一种让人们能够实际提供所需帮助的方式提问。无论你偏好电子邮件还是论坛,你都必须提供你所有能获取到的信息。虽然有很多建议的信息需要包含,你可能认为可以跳过其中一些或全部。然而,如果你马虎,没有提供所有必要的信息,以下其中一件事将会发生:

  • 你的问题将被忽略。

  • 你将收到一堆邮件,要求你收集这些信息。

另一方面,如果你确实希望得到帮助解决问题,请在消息中包含以下信息:

  • 完整的问题描述。像我如何让我的电缆调制解调器工作?这样的消息只会引发一系列问题:你希望调制解调器做什么?它是什么类型的调制解调器?症状是什么?你在尝试使用时发生了什么?你是怎么尝试使用它的?

  • uname -a的输出。这会显示操作系统版本和平台。

  • 任何错误输出。尽可能完整地提供,包含控制台或日志中的任何消息,尤其是/var/log/messages和任何特定应用的日志。关于硬件问题的消息应包括/var/run/dmesg.boot的副本。

最好以类似这样的消息开始:“我的电缆调制解调器无法连接到我的 ISP。该调制解调器是 BastardCorp v.90 型号 BOFH667。我使用的是版本 12.2 的操作系统,运行在四核 Opteron 上。这里是我尝试连接时的/var/log/messages/var/run/dmesg.boot的内容。当我手动运行 dhclient 时,出现了这些消息。” 通过这样的消息,你可以跳过一整个讨论阶段,并且能够更快地得到更好的结果。

撰写你的消息

首先,要礼貌。人们在网上说的话,通常是面对面时根本不敢说的。这些邮件列表是由志愿者组成的,他们出于善意回答你的消息。在你点击发送或提交按钮之前,问问自己,我会迟到去和梦中情人见面而去回答这条消息吗? 在处理公司电话支持时,偶尔需要的强硬态度,只会让这些知识渊博的人直接删除你的邮件或在论坛上直接封锁你的账户。他们的世界不需要脾气暴躁的混蛋。大声叫喊直到有人帮助你,虽然在处理商业软件支持时是一项宝贵技能,但在任何开源项目中寻求支持时,反而会伤害你获得帮助的机会。

无论你选择使用论坛还是电子邮件,都要保持话题的相关性。如果你在遇到X.org的问题,去查看X.org网站。如果你的窗口管理器不工作,去问负责窗口管理器的人。让 FreeBSD 的人员帮你配置 Java 应用服务器就像是向工业机械销售人员抱怨快餐午餐。他们也许会提供额外的番茄酱包,但这不是他们的事。另一方面,如果你希望 FreeBSD 系统在启动时不再启动邮件系统,那就是 FreeBSD 的问题。^(3)

发送电子邮件

FreeBSD 开发者倾向于使用邮件列表,而不是论坛。这意味着邮件列表能让你获得更多了解系统的人关注,但也意味着你需要遵循该环境的礼仪。

发送纯文本邮件,而不是 HTML 邮件。许多 FreeBSD 开发者用仅文本的邮件程序来阅读邮件,比如 Mutt。这类程序是处理大量邮件的非常强大的工具,但它们不显示 HTML 邮件,除非经过复杂的操作。想亲自体验一下这种情况,可以安装/usr/ports/mail/mutt并用它读取一些 HTML 邮件。如果你使用的是图形邮件客户端,比如 Microsoft Outlook,要么发送纯文本邮件,要么确保你的邮件包含纯文本和 HTML 两种版本。所有的邮件客户端都能做到这一点;只是你需要找到你的图形界面隐藏按钮的位置。而且,务必将你的文本换行到 72 个字符。发送仅 HTML 邮件或没有适当换行的邮件会导致邮件被丢弃,甚至没有被阅读。

严格吗?一点也不,一旦你理解了你写信的对象。大多数电子邮件客户端并不适合处理每天成千上万的邮件,这些邮件分布在几十个邮件列表中,每个列表都包含多个同时进行的对话。最受欢迎的电子邮件客户端使得阅读邮件变得简单,但它们并没有提高效率;当你收到这么多邮件时,效率比简单更为重要。由于大多数在这些邮件列表中的人处于类似的情况,纯文本邮件对于他们来说是非常标准的。

不鼓励在电子邮件中进行顶端回复。请将评论嵌入到讨论中,以保持上下文的连贯性。

类似的,许多电子邮件附件是没有必要的。你不需要在发送到公共邮件列表的邮件上使用 OpenPGP,而那些名片附件只是证明你不是系统管理员。不要使用过长的电子邮件签名。电子邮件签名的标准是四行。就这样——四行,每行不超过 72 个字符。^(4) 长的 ASCII 艺术签名绝对不合适。

当你编写了详细且礼貌的问题后,发送到 FreeBSD-questions@FreeBSD.org。是的,还有其他 FreeBSD 邮件列表,其中一些可能专门讨论你遇到的问题。然而,作为新用户,你的问题几乎肯定更适合发送到一般问题邮件列表。我已经在许多其他邮件列表上潜水了十多年,直到现在,还没有看到新用户在任何一个列表上提问是更适合其他列表的。通常,提问者最终会被推荐回 FreeBSD-questions。如果你的问题需要在其他地方提问,别人会告诉你。

这回到第一点关于礼貌的问题。向架构邮件列表发送询问 FreeBSD 支持哪些架构的消息,只会惹恼那些正在处理架构问题的人。你可能会得到一个答案,但你不会交到朋友。相反,FreeBSD-questions 上的人在那儿是因为他们志愿帮助像你一样的人。他们希望听到你聪明、充分研究过且有文档支持的问题。相当一部分人是 FreeBSD 开发者,其中一些甚至是核心成员。其他人则是稍微有些经验的用户,他们已经超越了你现在的困境,并且积极想要帮助你。

论坛发帖

论坛的用户群体与邮件列表不同。一些开发者会在论坛上活跃,但不像在邮件列表中那么多。论坛上的人更积极地帮助你解决问题。

论坛比邮件列表要简单一些。你只能以网站支持的格式发帖。没有关于顶部发帖与内联发帖或无法读取的 HTML 之类的担忧。正因如此,它们才这么受欢迎。不过,如果你深入了解 FreeBSD,最终还是会希望加入邮件列表。

但无论你选择哪个平台,礼貌都是至关重要的。

回应邮件

你的回答可能是一个简短的备注,附带一个 URL,甚至只是两个词:man xxx。如果你收到这样的回答,那就是你需要去的地方。在你实际学习了该资源之前,不要要求更多的细节。如果你对参考资料的内容有疑问,或者被参考资料搞得困惑,视其为另一个问题。缩小困惑的来源,具体并清楚地提问。手册页和教程并不完美,有些部分在你理解之前看起来是自相矛盾或互相排斥的。

最后,要跟进。如果有人要求你提供更多信息,提供它。如果你不知道如何提供,学习如何提供。如果你积累了坏名声,没有人会愿意帮助你。

互联网是永恒的

那些在 80 年代就上网的人还记得,当时我们把互联网当成一个私人游乐场。我们可以随心所欲地说话,跟任何人说。毕竟,那些东西是短暂的。没有人会保存这些内容;就像 CB 无线电,你可以做个完全的傻瓜而不受惩罚。

那些早期的 Usenet 讨论?是的,谷歌已经将它们恢复并上线了。我们的信仰与事实正好相反。

潜在的雇主、潜在的约会对象,甚至是家人,可能会扫描互联网,查看你在邮件列表或留言板上的帖子,试图了解你是怎样的人。我曾经因为某些人发在邮件列表和讨论板上的帖子而拒绝了他们的工作申请。我希望与那些在支持论坛上发送礼貌、专业信息的系统管理员共事,而不是那些发泄情绪、语无伦次、缺乏足够细节以提供任何指导的愤怒言辞。如果我偶然看到某个亲戚在某个留言板上像个傻子一样发言,我对他们的看法也会大打折扣。FreeBSD 讨论内容被广泛归档;好好选择你的词语,因为它们会伴随你几十年。

现在你知道如何在问题出现时获得更多帮助,让我们来安装 FreeBSD 吧。

第二章:安装前

image

仅仅让 FreeBSD 在您的计算机上运行是不够的,无论第一次安装时多么让您满足。确保您的安装是成功的同样重要。一个成功的安装应该是能够达到预期目的的。服务器与桌面计算机有着非常不同的需求,服务器的预期功能可能会完全改变安装需求。在安装 FreeBSD 之前做好充分的规划,可以使安装过程轻松很多。另一方面,由于您每次安装仅进行一次,您将得到的重复安装经验会少得多。如果您的主要目标是通过反复练习来掌握安装程序,请跳过这段“提前规划”的内容,直接阅读下一章。

我假设您希望在实际环境中运行 FreeBSD,进行实际工作。这种环境可能是您的笔记本电脑—虽然您可能会争辩说您的笔记本电脑不是生产系统,但我挑战您在没有备份的情况下清除所有数据,然后再告诉我它不是生产系统。如果您是在一个打算进行破坏性测试的系统上安装,并且您对其命运完全无所谓,我仍然建议您遵循最佳实践,以便养成良好的习惯。

请先考虑您需要的硬件或您已有的硬件。然后决定如何最佳使用这些硬件,应该使用哪种文件系统,以及如何安排磁盘。只有在此之后,您才应该继续下载和安装 FreeBSD。

不过,在您开始安装之前,让我们先了解一下在您整个 FreeBSD 使用过程中会遇到的一些概念:默认文件和通用配置语言(UCL)。

但是,首先,您必须了解 FreeBSD 的默认配置文件系统。

默认文件

FreeBSD 将配置文件分为默认文件和自定义文件。默认文件包含变量赋值,并不打算进行编辑;相反,它们设计为可以被同名的其他文件覆盖。

默认配置文件保存在名为 default 的目录中。例如,启动加载器的配置文件是 /boot/loader.conf,默认配置文件是 /boot/defaults/loader.conf。如果您想查看完整的加载器变量列表,请查看默认配置文件。

在升级过程中,安装程序会替换默认配置文件,但不会修改您的本地配置文件。这种分离方式确保了您的本地更改保持不变,同时允许将新值添加到系统中。FreeBSD 每次发布都会增加新功能,其开发者会尽力确保对这些文件的更改是向后兼容的。这意味着您不必逐个检查升级后的配置并手动合并更改;最多,您只需查看新的默认文件,以便发现新的配置机会和系统功能。

loader 配置文件是这些文件的一个很好的例子。/boot/defaults/loader.conf 文件包含多个条目,类似于这样:

verbose_loading="NO"            # Set to YES for verbose loader output

变量 verbose_loading 的默认值为 NO。要更改此设置,请不要编辑 /boot/defaults/loader.conf,而是将该行添加到 /boot/loader.conf 并在那里修改。你的 /boot/loader.conf 条目会覆盖默认设置,并且你的本地配置只包含你的本地更改。系统管理员可以轻松查看做了哪些更改,以及该系统与原始配置有何不同。

我鼓励你将配置文件保存在版本控制系统中。如果你有像 Ansible 这样的全球配置管理系统,那就更好了。如果没有这样的系统,使用 svn(1) 或广受喜爱或讨厌的 git(1) 作为集中式存储库也可以。即使是像 rcs(1) 这样的本地版本控制系统,也能在某一天救你一命。

不要复制默认配置!

一个常见的错误是将默认配置复制到覆盖文件中,然后直接在那里进行修改。这样的复制会导致系统某些部分出现重大问题。你可能在一两个地方勉强能过得去,但最终它会给你带来麻烦。例如,将 /etc/defaults/rc.conf 复制到 /etc/rc.conf 会导致系统无法启动。你已经被警告过了。

默认配置机制在整个 FreeBSD 中都会出现,尤其是在核心系统配置中。

使用 UCL 配置

通用配置语言,或 UCL,是用于管理 Unix 风格配置文件的通用库。FreeBSD 使用 UCL 来处理核心功能,例如打包系统。

任何位于 UCL 中的文件可以采用几种格式之一,例如大多数 Unix 程序使用的传统变量 = 设置格式、YAML 或 JSON。如果你之前配置过任何 Unix 软件,UCL 不会是问题。

本书中将展示 UCL 风格配置的例子。此时你不需要了解 UCL 的详细信息,只需知道 UCL 是 FreeBSD 中的一种配置方式。

FreeBSD 硬件

FreeBSD 支持大量硬件,包括为每种架构设计的不同架构和设备。该项目的目标之一是支持最广泛可用的硬件,硬件清单包含的设备远不止“个人电脑”。今天完全支持的Tier 1硬件包括 32 位和 64 位版本的英特尔风格处理器。

大多数现代硬件使用英特尔经典 32 位架构的 64 位扩展。这些扩展由 AMD 创建,因此该平台被称为amd64。过去十年内大多数硬件都采用了 amd64 标准。虽然 amd64 硬件可以启动 FreeBSD 的 32 位和 64 位版本,但 32 位版本包含了一些绕过措施来支持硬件特性和扩展的地址空间。在 64 位硬件上运行 64 位 FreeBSD。

传统的 32 位 IBM 兼容 PC 主导了计算领域数十年。FreeBSD 通过i386平台支持该硬件。^(1) 仅在纯 32 位硬件上使用 FreeBSD 的 i386 版本。FreeBSD 对一些其他硬件平台提供有限支持,称其为Tier 2 架构。其中一些架构逐渐变得越来越流行,例如 ARM。FreeBSD 支持 32 位和 64 位 ARM CPU,分别对应armarm64平台。对 64 位 ARM 硬件的支持正在迅速提升,预计 ARM64 很快会成为 Tier 1 平台。其他硬件平台正在逐渐退出,并且在被从源代码树中移除之前已经降级为 Tier 2 平台。此外,你还可以在 PowerPC(ppc)和 64 位 Sparc(sparc64)硬件上运行 FreeBSD,虽然这些硬件从未进入 Tier 1 平台。Tier 2 平台可以接受 FreeBSD 的前沿版本出现临时故障。Tier 2 平台可能会有,也可能没有可用的软件包。

你还会发现Tier 3平台,它们是高度实验性的。RISCV 硬件属于 Tier 3。

Tier 4包括几乎没有支持的平台。其中一些平台已经过时,正处于退出阶段。虽然代码仍然存在,并且理论上可以被复兴,但没有人愿意做这项工作。其他平台可能正在开发中,但还没有完全成熟。每个达到更高 Tier 的平台都会经过 Tier 4 这个阶段。

FreeBSD 支持为每种架构设计的许多网络卡、硬盘控制器和其他外设。由于许多架构使用相似的接口和硬件,因此这并不像你想象的那么具有挑战性:SATA 在任何地方都是 SATA,Intel 以太网卡也不会因为你把它插入 arm64 机器就变魔术一样。

虽然 FreeBSD 在古老的硬件上运行得很好,但该硬件必须处于可接受的状态。如果你的 Pentium IV 因为内存有问题而崩溃,安装 FreeBSD 也无法阻止崩溃。

FreeBSD 支持大多数 RAID 控制器,并包括管理大多数 RAID 控制器的软件。然而,我建议使用 UFS 文件系统的用户选择 FreeBSD 的 RAID 选项,而不是硬件 RAID 控制器。RAID 控制器是在存储冗余管理如此占用计算资源的时代诞生的,那时管理冗余几乎完全占用了主机的处理器。如今的计算硬件能够轻松管理 RAID。此外,RAID 控制器在硬盘上使用自定义格式。通常,只有相同型号的 RAID 控制器才能读取这些磁盘。RAID 控制器的意外故障可能会让你在可疑的网络拍卖中搜索旧控制器。而且,如果你觉得这些控制器新买时价格高,等到它们使用五年后再买,那时愿意买的人只有那些急需该型号的人!FreeBSD 提供几种不同的软件 RAID 选项,这些磁盘可以用任何类似硬件读取。

如果你使用的是 ZFS,关于 RAID 控制器的警告就变成了“直接不要用”。ZFS 期望能够直接访问磁盘。使用 RAID 控制器会禁用 ZFS 的自我修复和错误校正功能。如果你必须使用 RAID 控制器,请禁用 RAID 并让它作为存储控制器使用。虽然许多 RAID 卡声称可以作为 RAID 控制器使用,但大多数实际上只是提供一堆单盘 RAID 容器。在部署 ZFS 之前,确保你的 RAID 控制器可以切换到仅磁盘(JBOD)模式或主机总线适配器(HBA)模式。

本书使用 amd64 作为参考平台。所有内容应该也可以在 32 位 i386 主机上运行,但目前 amd64 是全球标准,因此我们将使用它。测试系统包括几台 iXsystems 存储服务器和各种虚拟机。^(2)

专有硬件

一些硬件厂商认为,保持其硬件接口的保密能够防止竞争对手复制他们的设计并打入他们的市场。然而,这种策略已经一再被证明是糟糕的,特别是在大量通用零件涌入市场后,这些保密的硬件厂商基本上被淹没了。不过,仍有一些厂商固守他们的保密策略。我们称这类设备为 专有硬件

为一款硬件开发设备驱动程序而没有其接口规范是相当困难的。有些硬件在没有完整文档的情况下也能得到很好的支持,并且足够常见,以至于值得在缺少文档的情况下进行努力。

如果一个 FreeBSD 开发者拥有一款硬件、该硬件的文档并且对这款硬件感兴趣,他可能会为其实现支持。如果没有,那款硬件就无法在 FreeBSD 上工作。在大多数情况下,不受支持的专有硬件可以轻松地被更便宜、更开放的选项所替代。

一些厂商为其硬件提供封闭源的二进制驱动程序,以内核模块的形式提供(参见 第六章)。记住,虽然 FreeBSD 将内核称为模块化的,但这意味着你可以选择加载哪些部分,哪些部分不加载。一旦加载了内核模块,该模块便可以完全访问整个内核。视频驱动程序的内核模块完全有可能破坏你的文件系统。我强烈建议你尽可能避免使用二进制驱动程序,并避免使用需要此类驱动程序的硬件。

我的硬件是否受支持?

确定硬件是否受支持的最简单方法是启动 FreeBSD。如果你还没有物理访问该硬件,请查看 www.FreeBSD.org/ 上你选择的版本的发行说明。

硬件要求

曾几何时,主机的最低硬件要求是一个大问题。FreeBSD 1.0 只支持非常特定的硬盘控制器和以太网适配器,并且需要几兆字节的内存。在那个时候,无法运行 FreeBSD 的硬件依然在广泛使用。

大多数硬件要求已经成为过去。任何生产过的 amd64 系统都能运行 FreeBSD。任何在本千年内制造的服务器级 i386 系统都能运行 FreeBSD。是的,一台配备 18GB SCSI-2 硬盘和 128MB 内存的 Pentium 提供的性能平平,但如果你想要更好的性能,尽量不要使用那样的硬件。

仅仅因为某个硬件应该能够工作,并不意味着它一定能工作。“便宜”并不等同于“廉价”。被支持的劣质硬件依然是劣质的。在购买硬件之前,最好先做些研究。

FreeBSD 可以在虚拟机管理程序(如 VMware、VirtualBox、Xen 和 KVM)上运行良好。合法的云服务提供商提供 FreeBSD 镜像和 ISO 文件。FreeBSD 也可以顺利运行在集成的 bhyve(8)虚拟机管理程序和 OpenBSD 的 vmm(8)上。你可以在 128MB RAM 和 1GB 磁盘的情况下进行基础安装,尽管为了更深入的实验,你可能需要更多的资源。

BIOS 与 EFI

上世纪 80 年代,IBM 发明了基本输入输出系统(BIOS),用于处理低级硬件任务,比如寻找操作系统。然而,BIOS 有内建的限制,使其在现代硬件上表现不佳。现代的 BIOS 替代品是可扩展固件接口(EFI),它比 BIOS 更灵活、更强大。FreeBSD 可以顺利从 EFI 启动,使用 EFI 还能让 FreeBSD 做一些有趣的事情,比如全盘加密。

如果你的硬件支持 EFI,就使用 EFI。只有在 FreeBSD 暴露出你硬件的 EFI 实现中的错误时,才回退到 BIOS 模式,在这种情况下我建议你提交一个 bug 报告(参见第二十四章)。需要注意的是,硬件设置工具可能会把 BIOS 模式称作“传统启动”或“古老垃圾”之类的名称。

磁盘和文件系统

安装系统时,最关键的部分可能就是如何分配磁盘空间以及使用哪个文件系统。FreeBSD 的基本安装大约需要半个 GB 的磁盘空间,但文件系统的选择在很大程度上决定了系统的行为。

FreeBSD 文件系统

FreeBSD 支持两种主要的文件系统,UFS 和 ZFS。你应该使用哪种呢?这完全取决于你打算如何使用你的系统。在启动安装媒体之前做出决定,你需要了解每种文件系统的基本知识。

FreeBSD 的 Unix 文件系统(UFS) 是直接继承自 4.4 BSD 随附的文件系统,已经持续开发了几十年。UFS 的其中一位原创作者至今仍活跃在 FreeBSD 社区,持续改进文件系统,并为新一代开发者提供支持和指导。UFS 作为原始的 FreeBSD 文件系统,已经将其影响扩展到操作系统的各个方面。许多其他 FreeBSD 文件系统通过为 UFS 创建的基础设施,连接到内核的虚拟内存系统。UFS 设计用于有效处理最常见的情况,同时可靠地支持不常见的配置。FreeBSD 配备了针对现代硬件广泛优化的 UFS,但如果你愿意,也可以选择优化一个分区,用于处理万亿个小文件或几个 1TB 的大文件。

ZFS(不是缩写)由 Solaris 于 2005 年推出,并在 2007 年集成到 FreeBSD 中。它的年轻似乎是一个劣势,但它结合了许多已经使用了很长时间的技术和概念。ZFS 计算每个数据块或元数据的校验和,并可以用它进行错误修正。存储是池化的,这意味着你可以在不重新创建文件系统的情况下,动态地向现有的 ZFS 文件系统添加更多磁盘。ZFS 有许多很酷的功能,比如高效的内建复制功能,以及能够实时创建和删除数据集(分区)。

虽然 ZFS 是在十多年前编写的,但它是为未来的硬件而设计的。所有这些酷功能都带来了性能成本,ZFS 可能会占用大量内存。虽然 32 位系统可以使用 ZFS,但不推荐这样做。我不建议在内存少于 4GB 的主机上运行 ZFS,并且拒绝在少于 2GB 内存的主机上运行它。UFS 更适合小型和嵌入式系统,而不是 ZFS。

ZFS 是虚拟化服务器的一个优秀存储系统,但对于使用磁盘映像的虚拟机来说,未必是最佳选择。许多虚拟机没有足够的内存来有效地运行 ZFS。此外,我也曾看到过多个基于 KVM 的虚拟化系统无法迁移基于 ZFS 的虚拟机。如果你打算在虚拟化客户端上使用 ZFS,务必确保你的虚拟化系统支持恢复和迁移 ZFS 磁盘映像,再安装一大堆主机。

有些人坚持认为 ZFS 需要 ECC RAM。ECC RAM 是非常好的,如果可以的话,你应该选择它。然而,没有 ECC 的 ZFS 并不比带 ECC 的 UFS 更差。ECC 提供了一层完整性检查,类似于 ZFS。如果主机的非 ECC 内存被宇宙射线击中,ZFS 会将损坏的数据写入磁盘——就像你使用 UFS 一样。

最后,ZFS 假设你是在以 ZFS 的方式进行操作。ZFS 是一个结合了文件系统和卷管理的系统,它要求对原始磁盘的访问。切记,绝不要在 ZFS 中使用 RAID 控制器;使用 RAID 卷作为磁盘会干扰 ZFS 的自我修复功能。许多 RAID 控制器声称提供原始磁盘,但实际上它们提供的是单磁盘 RAID 容器。^(3)

UFS 也并不完美。断电或系统崩溃可能会损坏 UFS 文件系统。修复该文件系统需要时间和系统内存。大致来说,修复每一个 terabyte 的 UFS 文件系统需要 700MB 的内存。如果你在一个拥有 6GB 内存的系统上创建一个 7TB 的文件系统,FreeBSD 将无法自动修复它。

总结一下,在一台现代的 amd64 笔记本电脑或服务器上,我推荐使用 ZFS。测试 ZFS 是否能与您的虚拟化系统兼容。如果可以使用,请将 ZFS 用于具有 4GB 或更大内存的 64 位虚拟机。对于 i386 硬件或内存少于 4GB 的 64 位主机,使用 UFS。

如果你正在运行一个高负载、高流量的应用程序和数据库,在继续之前,建议在生产硬件上同时测试 UFS 和 ZFS,以查看哪种在你的应用程序中表现更好。可以尝试不同的磁盘配置、ZFS 池类型以及 GEOM RAID 方法。有些应用程序在 UFS 上运行得比在 ZFS 上更好。例如,Netflix 就是通过 FreeBSD 主机提供其所有内容,这些主机上拥有大量存储,格式化为 UFS。在安装大容量存储服务器之前,请查看第十二章以获取更多 ZFS 部署的相关考虑。

所有这些建议都服从一个铁律:选择最适合你环境的文件系统。

文件系统加密

磁盘加密已成为许多环境中的重要功能。一个丢失笔记本电脑的用户并不想丢失他的数据。某些组织要求对静态或非活动磁盘上的关键数据进行加密。你不能在已安装的系统上事后对磁盘进行加密。

FreeBSD 支持两种磁盘加密系统:基于 GEOM 的磁盘加密(GBDE)GELI。gbde(8) 加密系统专为那些加密数据的存在可能威胁到用户生命的情况设计。它是为了保护一个被枪指着头的用户而设计的。幸运的是,这种使用场景比较少见;本书不涉及此内容。

geli(8) 加密系统则针对更常见的风险提供保护。如果你的笔记本电脑被盗,GELI 会防止小偷读取硬盘。^(4) 如果你将公司的财务记录存储在 GELI 加密的分区中,服务技术员在服务过程中是无法读取这些数据的。第二十三章将更详细地介绍 GELI。

许多组织要求包含财务数据或知识产权的磁盘在退役时必须无法读取。你可以将这样的磁盘送去粉碎,但在安装时对磁盘进行加密同样有效。当你销毁加密密钥时,磁盘将变得不可读取。

我建议要么加密整个系统,要么不加密任何部分。部分加密的磁盘会给技术高超的入侵者留下机会,破坏你的系统并绕过加密。

在继续之前,决定是否需要加密。

磁盘分区方法

磁盘分区让你可以将一个磁盘或磁盘阵列划分为多个逻辑单元。即使是普通消费者级别的操作系统(比如你在本地大型商店里看到的 Windows 笔记本)也通常会在硬盘上设置多个分区。分区方案是组织磁盘上分区的系统。

计算技术总是在不断变化中,现在我们正处在一个特别令人头疼的磁盘分区变革中。旧的、较小的硬件使用主引导记录(MBR)分区,且始终限制在 2TB 或更小的磁盘上。新的、更大的硬件则使用更灵活、通常更好的 GUID 分区表(GPT)方案。FreeBSD 使用 gpart(8) 管理这两种类型的分区。

在安装时应该使用哪个分区方案?在任何支持 GPT 的系统上都使用 GPT,不管磁盘的大小。只有在系统无法支持 GPT 的情况下,才使用 MBR。(你可以使用 gptboot(8) 和 gptzfsboot(8) 将 GPT 支持强加到仅支持 MBR 的磁盘上,但这应该留到第二次或第三次安装时使用。)

我遇到过不止一个支持 GPT 的系统,但它们有硬件限制,无法使用大于 2TB 的磁盘。虽然在这样的系统上使用 MBR 似乎很合理,但请记住,GPT 要灵活得多。即使你是一位拥有几十年 MBR 经验的系统管理员,也请学习并使用 GPT。

使用 UFS 进行分区

如果你决定为主机使用 UFS,你需要考虑文件系统分区。由于 FreeBSD 支持多种磁盘大小,安装程序不会尝试预测你希望如何分区系统。请在安装之前决定如何分区磁盘。

至少,要将操作系统与数据分开。如果此主机用于用户帐户,创建一个单独的 /home 分区。如果你在运行数据库,创建一个数据库的分区。Web 服务器应该有一个用于 Web 数据的分区,可能还需要一个用于日志的分区。

作为一名老派的 Unix 用户,我通常会创建独立的 /usr/usr/local/var/var/log/home 分区,以及一个根分区(/)和一个交换空间分区,再加上一个用于服务器应用数据的独立分区。不过,我被告知我是个死板的人,认为对抗恶意进程和用户填满硬盘的担忧已经过时了。^5

现代 FreeBSD 的基础安装大约需要半个 GB 的空间。与当今的硬盘相比,这是微不足道的。在一块运行在真实硬件上的现代硬盘上,分配 20GB 给操作系统和相关程序应该是足够的。

然而,如果你在现代硬件上运行 FreeBSD,你可能更倾向于使用 ZFS 而不是 UFS。

多个操作系统

在石器时代(大约 2001 年),能够在单个 6GB 的硬盘上安装四个操作系统让我感到非常兴奋。这是唯一一种在桌面上运行多个操作系统而不需要更换硬盘的方法。

仍然可以进行多重引导安装,但虚拟化要好得多。你不必关闭主操作系统就能访问其他操作系统。bhyve(8) 虚拟机管理程序允许你在 FreeBSD 上运行其他操作系统,包括微软的 Windows。其他操作系统也有虚拟机管理程序,允许你在它们上面运行 FreeBSD。

多个硬盘

如果主机中有多个硬盘,你几乎肯定应该使用它们来创建某种存储冗余。如果你使用 ZFS,可以使用镜像或某种 RAID-Z(见 第十二章)。如果使用 UFS,FreeBSD 支持软件 RAID。不过,当你有一堆硬盘时,事情就变得有点复杂。

经验法则仍然是将操作系统与应用程序数据分开。如果你有 30 块硬盘,可以将其中两块镜像用于操作系统安装,其余硬盘用于存储数据。像所有经验法则一样,这个观点是有争议的。但没有哪个系统管理员会告诉你,这绝对是个糟糕的主意。

如果你有多个硬盘,考虑一下哪些数据通过哪个硬盘控制器传输。如果硬盘控制器故障,系统会发生什么?如果你的两个操作系统硬盘都连接在同一个控制器上,而该控制器出现故障,你的主机将会宕机。将每个硬盘连接到不同的控制器上可以提供冗余。理想情况下,应该将镜像操作系统硬盘连接到不同的硬盘控制器上。

此外,请记住,SATA 硬盘控制器会将其所有的数据传输量分配给连接到它们的所有硬盘。如果你有两个硬盘连接在一个 SATA 控制器上,每个硬盘的工作速度大约是单独连接在同一通道上时的一半。端口倍增器虽然增加了硬盘数量,但会大幅降低每个硬盘的性能。

交换空间

当 FreeBSD(以及其他任何现代操作系统)用尽所有物理 RAM 时,它可以将长时间闲置的内存信息转移到交换空间中。如今,即使是笔记本电脑也配备了 32GB 的 RAM,已经很难想象主机会出现内存不足的情况,但永远不要低估一个程序吞噬 RAM 的能力。虚拟系统可能会分配非常少的 RAM。

那么,你需要多少交换空间呢?这是一个系统管理员之间长期争论的问题。简短的答案是:“这取决于。”它取决于什么?一切。长期以来的经验法则认为,主机的交换空间应该是物理内存的两倍,但今天这不仅过时,而且是危险的。当一个进程开始灾难性地分配内存时——比如由于无限循环导致的 bug——一旦系统用尽虚拟内存,内核就会杀死该进程。一个拥有 32GB RAM 和 64GB 交换空间的系统,拥有 96GB 的虚拟内存。i386 平台将每个进程的内存使用限制为 512MB,这意味着内核会很快终止这种失控的进程。像 amd64 这样的 64 位系统拥有广阔的虚拟内存空间。一个在磁盘和内存之间不停交换几 GB 内存的系统将非常缓慢。现代主机应该只有足够的交换空间来完成其任务。

多个硬盘可以通过将交换空间分配到不同磁盘控制器的磁盘之间,提高交换空间的效率。不过,请记住,崩溃转储必须完全适配到单个交换分区。FreeBSD 会压缩崩溃转储,以便它们占用更少的空间,但即便如此,多个小交换分区也可能适得其反。如果你有大量硬盘,最好不要将应用程序磁盘用于交换空间;将交换空间限制在操作系统磁盘上。

现代系统上交换空间的主要用途是为内存转储提供一个存储位置,以防系统发生崩溃和 panic。FreeBSD 使用内核小型转储,因此它们只转储内核内存。小型转储比完全转储小得多:一个 8GB 内存的主机的平均小型转储大小大约为 250MB。每 10GB 内存配置 1GB 交换空间对于大多数情况应该足够了。

然而,如果你遇到了一个真正难以解决的问题,你可能需要将整个内存内容转储到交换空间中。如果我在设置一个重要的生产系统,我总是会创建一个比主机最大可能虚拟内存空间更大的未使用分区,并告诉主机将内核转储到该分区。如果我的笔记本电脑遇到类似问题,我只需插入一个闪存驱动器,并配置系统将转储到它上面。

获取 FreeBSD

现在你已经做出所有决定,你需要一个 FreeBSD 的副本。如果这是你第一次安装 FreeBSD,请访问 www.FreeBSD.org/,并查找页面顶部的“获取 FreeBSD”部分。在那里,你将看到支持的发布版本列表,其中通常包括两个推荐用于生产的版本。有时是一个,有时是三个,但通常是两个。

FreeBSD 版本

两个生产版本?这是什么疯狂的事情?

FreeBSD 开发在多个轨道上进行,正如我将在第十八章中讨论的那样。一些轨道并行存在,处于不同的支持状态。每个轨道都会接收 bug 修复和增量改进。较新的轨道会获得新特性。

当我写这篇文章时,FreeBSD.org 列出了两个生产版本,分别是 11.0 和 10.4。版本 11.0 是最新发布的版本,但它也是一个 .0 版本。它是这个分支的第一次发布,包含最新功能,但也最有可能包含未知的错误。版本 10.4 稍微旧一些,缺少 11.0 中的一些功能,但它是该分支的第四个版本。虽然不能保证没有错误,但很多人在生产环境中已经运行了几个月或几年,所有明显的问题都已修复。

每个 FreeBSD 版本最终都会达到生命周期终止(EoL),并停止支持。安全团队会停止发布补丁,新的软件包也将不再提供。较旧的版本会先于较新的版本达到生命周期终止。如果你今天安装 FreeBSD 10.4,你将来需要升级到 11,但届时你将升级到的版本就不是 .0 版本了。

FreeBSD 平均同时发布两个生产版本。这不是一个不可违背的规则,而是观察到的现象。大约在 12.0 版本发布时,10.x 分支将达到生命周期终止(EoL)。

我个人会运行 FreeBSD .0 版本,但由于之前曾在其他操作系统上遭遇过问题,我对那些坚决拒绝 .0 版本的用户表示理解。如果你以前没有使用过 FreeBSD,我建议安装最新的生产版。它包含了最新的设备驱动程序和功能。

跟随下载链接,选择并下载你需要的版本。

选择安装镜像

你可以选择几种不同格式的 FreeBSD 安装介质。所有安装介质都有压缩版(使用 xz(1))和未压缩版。如果你能够方便地解压 .xz 文件,建议下载压缩版本。这既节省了带宽,也减少了下载时间。任何现代操作系统都可以原生支持 .xz 文件,或者可以通过附加软件来处理此任务。

FreeBSD 提供两种类型的安装介质。第一种只包含足够启动 FreeBSD 安装程序并连接网络的内容。安装程序随后会从 FreeBSD 镜像站点下载操作系统文件。然而,如果你打算对同一版本的 FreeBSD 进行多次安装,那么下载包含操作系统文件的安装程序会更合适。

安装程序提供光盘格式(.iso)和闪存格式(.img)。选择适合你系统的格式。如果你是在安装虚拟机,ISO 格式可能是最简单的选择。

FREEBSD 镜像

旧文档常常强调选择一个好的镜像站点对安装的重要性。忽略这些内容。FreeBSD 软件分发站点 ftp.freebsd.org/ 是一个全球性的镜像服务器集合。当您获取安装介质、软件包或任何其他 FreeBSD 材料时,系统会自动将您指向最近的镜像站点。如果您想使用特定的镜像站点而不是 GeoDNS 自动选择的站点,可以从 FreeBSD 手册的附录 A 中按名称选择(该内容在第一章中有讨论)。

每个安装镜像的文件名都以 FreeBSD、版本号和平台名称开始。如果您正在为 amd64 硬件下载 FreeBSD 12.0,安装镜像的文件名将以 FreeBSD-12.0-RELEASE-amd64 开头。紧接着,文件名会标明安装类型。

文件扩展名是一个帮助您轻松找到所需文件的工具:

  • bootonly.iso 结尾的文件是启动 FreeBSD 安装程序的 ISO 镜像。使用这些文件意味着通过网络下载 FreeBSD。

  • disc1.iso 结尾的文件是包含完整 FreeBSD 安装程序的 ISO 镜像。此镜像包含操作系统文件。

  • mini-memstick.img 结尾的文件是用于闪存驱动器的。它们启动 FreeBSD 安装程序,但通过网络下载操作系统文件。

  • memstick.img 结尾的文件是包含完整 FreeBSD 安装的闪存驱动器镜像。

FreeBSD 还提供更大的 DVD 镜像。这些镜像包含了整个 FreeBSD 系统和大量的软件包。它们适用于那些没有互联网连接的人。请记住,所有 FreeBSD 项目的带宽都是由社区捐赠的;除非您确实需要,否则请不要下载巨大的 DVD 镜像。

一旦您有了安装镜像,您需要将其放入实际的启动介质中。使用操作系统内置的工具将镜像刻录到物理磁盘上。虽然 Windows 现在包括 CD 刻录作为内置功能,但不支持闪存磁盘镜像。FreeBSD 项目推荐使用适用于 Windows 的 Image Writer (sourceforge.net/projects/win32diskimager/),这是一个非常不错的选择。打开程序,选择您的闪存驱动器和镜像文件,然后点击 开始

网络安装

如果您的安装介质仅能启动安装程序,并且需要通过网络获取 FreeBSD 分发文件,则在安装程序运行时需要配置网络。如果您的网络使用 DHCP,安装程序应该会自动获取网络配置。如果不是,您的 FreeBSD 主机需要一个有效的网络配置。在启动安装程序之前,请收集以下信息:

  • 有效的 IP 地址和子网掩码

  • 您网络的默认网关

  • 名称服务器的 IP 地址

如果您必须使用代理服务器才能访问互联网,您还需要其配置。

拥有这些信息后,您就可以开始安装 FreeBSD。

第三章:安装**

image

你已经考虑过你希望你的 FreeBSD 安装做些什么。你已经选择了硬件。你已下载启动介质,并将其刻录到闪存驱动器或光盘上。你找到了一台可用的 USB 键盘,并设置你的测试机器从该介质启动。现在,让我们开始 FreeBSD 的安装过程。启动你的安装介质并跟随操作。

在整个安装过程中,我会提到各种键盘映射、怪癖和安装程序提供的快捷方式。有一个让人烦恼的问题是,安装程序没有“返回”按钮:如果你在基本设置上犯了错误,比如磁盘分区,就得重新开始。

当然,我的桌面系统已经安装并运行了好几年。不过,我还是被迫为 Bert 设置一个系统,^(1)。如果他不喜欢我的安装方法,他可以阅读本章并自己安装他的机器。

核心设置

启动安装介质后,我看到启动加载器屏幕,并且有一个 10 秒的倒计时,如图 3-1 所示。

image

图 3-1:启动加载器

按 ENTER 跳过 10 秒倒计时。

接着我看到如图 3-2 所示的选择菜单。

image

图 3-2:选择安装

在第五章,我们会讨论如何使用 live CD 选项来修复损坏的系统。现在,选择默认的安装(Install)选项,按 ENTER。

你可能会注意到,每个选项的第一个字母是红色的,而大部分文本是灰色的。你可以直接输入那个字母来选择,而不必用箭头键。这里,输入 S 会进入 shell,而输入 L 会启动 live CD 镜像。

现在你进入了 bsdinstall(8),FreeBSD 的传统安装器。其他操作系统有漂亮的图形化安装器,带有鼠标驱动的菜单和多色饼图,而 FreeBSD 的看起来像是一个老旧的 DOS 程序。你将通过选择一个键盘映射开始安装,如图 3-3 所示。

image

图 3-3:键盘映射选择

Bert 的打字习惯糟糕透了,他真的需要一个更好的键盘布局。你可以用上下箭头浏览这个列表,但这很慢。PAGEUP 和 PAGEDOWN 可以一次性上下翻页,而 HOME 和 END 则分别让你跳到顶部和底部。当我找到一个我喜欢的键盘映射时,我按下 ENTER。然后我可以测试这个键盘映射,如图 3-4 所示。

这个键盘映射看起来很熟悉,但许多键盘映射的名字都很相似。按下 ENTER 会弹出一个输入框,让我可以猛敲键盘测试这个键盘映射是否符合我所选择的。如果看起来没问题,按 ENTER 返回这个界面,然后我可以按上箭头再按 ENTER 继续。

安装程序接着会要求我输入主机名,如图 3-5 所示。

image

图 3-4:测试或不测试?

image

图 3-5:输入主机名

我是自己的网络管理员,所以可以使用任何我想要的名称。你的组织可能有不同的规则。按回车键继续。

分发选择

设置键盘映射和主机名很重要,但当我们选择要安装的分发版本时,才是真正特有于 FreeBSD 的内容。在 FreeBSD 中,分发版本是 FreeBSD 组件的一个特定子集。当你安装 FreeBSD 时,需要选择你想要的分发版本。安装程序没有列出任何强制选择项:你必须安装内核和基本用户空间。一些部分是可选的(见图 3-6)。

image

图 3-6:分发版本

你不一定需要这些选项,但有些在特定情况下会非常有用。

base-dbg 基础系统的调试符号,对程序员有用

doc FreeBSD 的官方文档,例如手册

kernel-dbg 内核的调试符号,对程序员有用

lib32-dbg 32 位库的调试符号(仅限 64 位系统)

lib32 32 位兼容库(仅限 64 位系统)

src 已安装操作系统的源代码

tests FreeBSD 的自测工具

如果你在 FreeBSD 上进行编程或开发,或者正在开发 FreeBSD 本身,可以上下箭头选择调试库。新用户可能会发现文档有帮助。使用空格键选择或取消选择某个选项,按回车键继续进行磁盘分区。

我建议始终安装操作系统源代码。它占用的空间非常小,但可以成为一个宝贵的资源。

对我来说,我希望 Bert 尽量少打扰我。我会给他所有的调试库和系统源代码,如果他抱怨,我可以告诉他去读/usr/src

磁盘分区

FreeBSD 支持两种主要文件系统:UFS 和 ZFS(见图 3-7)。第二章讨论了如何在它们之间做选择,因此我不会再次讨论这个问题。现在我需要停止拖延,做出选择。

image

图 3-7:选择文件系统

经验丰富的用户可以选择手动安装,或者对于硬核用户,选择 Shell。不过我会让你跟着操作,因此我会选择自动安装(UFS)或自动安装(ZFS)。我会使用 UFS 来演示磁盘分区,然后再介绍 ZFS。

UFS 安装

由于默认的 UFS 安装非常简单,许多人使用默认选项也能顺利安装,因此我选择了一些不太常见的选项来演示如何使用 bsdinstall。首先,它会问我想使用多少磁盘空间,如图 3-8 所示。

如果 Bert 想使用多个操作系统,他可以启动一个虚拟化管理程序。我按回车键使用整个磁盘。弹出窗口会警告我即将擦除磁盘。没错,我就是要这么做。选择。接下来我需要选择一个分区方案,如图 3-9 所示。

image

图 3-8:磁盘使用

image

图 3-9:分区方案

Bsdinstall 默认使用保守的 MBR 分区方案。几乎所有设备都支持 MBR 分区,就像几乎所有设备都支持 BIOS 而不是 EFI,但 GPT 会在后续过程中减少我很多麻烦。我按箭头键向上移动,选择 GPT,弹出默认的 GPT 分区方案(见图 3-10)。

image

图 3-10:默认 GPT 分区方案

你现在可以按 ENTER 完成 UFS 分区,但我确定 Bert 需要特别处理。我们为他创建特殊的分区方案。

每个 GPT 系统都需要一个 freebsd-boot 分区,因此不要更改 ada0p1。按箭头键下移至 ada0p2,按 D 键或按箭头选择“删除”按钮将其删除。同样操作 ada0p3,这样你就会剩下一个分区和一堆空闲空间,如图 3-11 所示。

image

图 3-11:仅包含引导加载程序

现在决定如何对这个磁盘进行分区。磁盘有 16GB 的空间,我将其分配如下:

  • 512KB freebsd-boot EFI 启动分区

  • 1GB 交换区

  • 4GB 紧急转储空间(见第二十四章)

  • 1GB 根分区 (/)

  • 512MB /tmp

  • 2GB /var

  • 其他所有内容在 /usr

启动分区已经存在,因此我按箭头键选择“创建”或直接按 C 来添加第一个分区,弹出对话框如图 3-12 所示。

image

图 3-12:添加新分区

箭头键将帮助你在屏幕底部的选项之间移动,但你需要使用 TAB 键才能跳到顶部的文本区。一旦进入文本区,箭头键可以让你在各个字段之间移动,或者在每行内部前后移动。我们的第一个分区将是交换空间,因此使用 DELETE 键清除类型字段的内容并输入 freebsd-swap。将大小设置为 1GB。每个分区都应该有一个标签,因此我将其标记为 swap0。我们将在第十章中讨论标签。

现在按 TAB 键离开文本框并选择 OK

我敢肯定 Bert 会让这台机器崩溃,并且以非常糟糕的方式进行操作,以至于我必须将所有主机内存转储到磁盘。主机有 4GB 的 RAM,所以我创建了一个 4GB 的转储分区。它看起来和交换空间完全一样,包括 freebsd-swap 类型,但我将大小设置为 4GB,并将其标记为 dump0

根分区有点不同,如图 3-13 所示。根分区需要一个文件系统,因此将类型设置为 freebsd-ufs。我决定将其分配 1GB。根分区的挂载点总是 /,我将其标记为 root

image

图 3-13:添加根分区

剩下的分区用于 /tmp/var/usr,它们看起来都很相似。当你用完所有磁盘空间时,你将得到一个类似于图 3-14 中的分区表。

image

图 3-14:完整的自定义 GPT/UFS 分区表

安装程序询问我是否确定。这个布局应该能防止 Bert 抱怨日志文件溢出他的系统,所以我很满意。选择完成来分区磁盘并继续安装。

ZFS 安装

如果我选择 ZFS,我将看到图 3-15 所示的 ZFS 配置屏幕。

image

图 3-15:ZFS 配置

默认选项是安装,这会因为你还没有选择 ZFS 虚拟设备类型而报错。你需要先选择池类型/磁盘。虽然在我们进入之前,让我们看看其他选项。

FreeBSD 根 ZFS 池的默认名称是zroot。除非你希望系统看起来与其他 ZFS 系统不同,或者你的组织有命名池的标准,否则没有必要更改此名称。

强制 4K 扇区选项非常重要,原因我们将在第十章中讨论。除非你完全确定你的磁盘有 512 字节扇区,否则保持此选项为“是”。

如果你选择加密磁盘,你将被提示输入用于全磁盘加密的密码短语。FreeBSD 使用 GELI 进行 ZFS 加密(见第二十三章),尽管当 ZFS 支持原生加密时,这可能会有所改变。

对于分区方案,选择 GPT。如果你的主机可以合理地运行 ZFS,它支持 GPT。

你需要多少交换空间?根据需要调整交换空间大小。我希望这个主机有足够的空间来存储完整的内核内存转储,因为 Bert,所以我将交换空间大小调整为 4GB。

拥有多个硬盘的主机可以在多个硬盘上使用交换分区。当包含交换分区的硬盘故障时,主机会丧失该交换内存块中的所有内容并崩溃。选择镜像交换会为你的交换空间提供冗余,但会占用更多的磁盘空间。

你是否应该选择加密交换区?它的性能成本非常低,而且如果你的硬盘被盗,这可能带来一些潜在的优势。

现在上移并选择池类型/磁盘以选择 ZFS 虚拟设备类型,如图 3-16 所示。

image

图 3-16:虚拟设备选择

第十二章详细讨论了 ZFS 虚拟设备。选择虚拟设备类型是你为 ZFS 系统做出的最重要决策。然而,对于单磁盘主机,唯一可行的选项是条带。选择它后,你将有机会选择 ZFS 池中的硬盘(见图 3-17)。

image

图 3-17:ZFS 磁盘选择

使用空格键选择你想要包含在此池中的磁盘。由于此主机只有一块磁盘,我选择它,然后选择“确定”继续。

安装程序将我带回主 ZFS 配置屏幕。我再次检查我的选择(GPT 分区和 4GB 交换分区),然后向上箭头选择“安装”。安装程序给了我最后一个“你真的确定吗?”的警告。我确定。

网络和服务配置

一旦你批准了磁盘布局,bsdinstall 会将新的分区表写入磁盘,创建文件系统,并提取你选择的发行版,无需进一步干预。安装程序继续设置网络、服务和用户。

首先,系统会提示你设置新的 root 密码。root 用户可以对系统执行任何操作,所以请设置一个强密码。你需要输入两次密码以便接受。

使用上下箭头选择一个网络接口。此主机只有一个接口,所以我按 ENTER 来配置它(见图 3-18)。

image

图 3-18:选择网络接口

接下来,我们被询问是否要为此接口配置 IPv4。如果你不确定 IPv4 是什么,但你需要互联网连接,选择。我当然选择是。接着,我们被询问是否希望使用 DHCP 自动配置网络。如果这是一个一次性的系统,那可能会选择,但这将是 Bert 的个人服务器。它需要特殊的网络配置。我选择否,并进入网络配置屏幕,如图 3-19 所示。

光标已经位于文本区域。使用箭头键向下移动,而不是 TAB 或 ENTER。看到“确定”被高亮了吗?一旦按下 ENTER,安装程序会继续到下一个屏幕,无论你是否已设置好网络。填写 IP 地址、子网掩码和默认网关的适当值。如果你不知道这些是什么,你应该使用 DHCP 或者阅读第七章。不用担心这里出错;如果你搞砸了,安装程序的最后一个屏幕会提供更改网络配置的机会。完成后按 ENTER。

image

图 3-19:网络配置

一旦配置了 IPv4,安装程序会继续配置 IPv6。你们现在都在使用现代网络,所以可以继续配置 IPv6。IP 地址、子网掩码和默认路由器设置与 IPv4 相似。安装程序还支持 SLAAC,也就是IPv6 的 DHCP。不过,如果你还在使用老旧的仅 IPv4 网络,就跳过 IPv6 配置。

接下来,你可以选择配置 DNS。在这里,我输入我的网络的搜索域和名称服务器(参见图 3-20)。

image

图 3-20:解析器配置

如果你有网络的 IP 地址信息,但不知道搜索域和名称服务器的 IP 地址,可以从另一台机器上复制这些值。

安装程序现在会请求主机的时区。与其把所有时区列在一个巨大的列表中,不如通过一系列层次化菜单来选择,如图 3-21 所示。

image

图 3-21:时区选择器

选择你的大洲。然后,你将被要求选择一个地区。我选择了美国——Bert 确实在欧洲,但我希望他清楚地意识到,如果他在早上请求帮助,他是得不到的。记住,END 和 HOME 键可以让你快速跳转到这些长列表的顶部和底部;通过按 END 键然后向上移动几个位置,比一页一页地浏览整个西半球的每个国家(包括那些小岛屿)要快得多。然后我可以选择美国的任何时区。美国公民将再次被提醒,许多州的时区设置非常混乱。^(2) 即使是我来自的密歇根州也不例外。但我选择了密歇根州,并有机会确认我的选择(见图 3-22)。

我认出 EDT,即东部夏令时。如果我不认得,我会选择“不”,然后重新尝试。

接下来的几个界面提供了设置系统时钟的选项。奇怪的是,默认设置为跳过。虽然你可以在这里输入时间和日期,但更容易的方法是从网络上设置时间,正如我们稍后将做的那样。

现在我们可以在系统启动时启用一些服务,如图 3-23 所示。

image

图 3-22:美国时区

image

图 3-23:启动服务

大多数主机都需要 SSH,并且你总是应该启用内核崩溃转储。然而,其他服务可能不适合你的网络。我总是启用ntpd(见第二十章)和local_unbound(见第八章),以便主机的时钟可以与公共 NTP 服务器同步,并保持本地 DNS 缓存,但如果你的主机无法访问公共互联网,它们就没有那么有用了。笔记本电脑用户可以研究 moused(8)和 powerd(8)。

然后我们在图 3-24 中看到系统加固选项。

image

图 3-24:加固选项

我们在第十九章中详细讨论了加固选项。如果这是你第一次安装,而且你想要一个轻松的学习体验,保持默认设置不启用任何选项。如果你想学习如何在一个更加安全的系统上工作,可以选择启用所有选项。我在所有主机上启用每一个加固选项,学习如何在更安全的环境中工作对 Bert 来说会很有帮助。

现在我们可以向系统添加一个用户(参见图 3-25)。我建议为每个系统至少添加一个非特权用户,这样你就可以登录到新安装的主机,而不是直接进入root。如果你有一个需要用户账户的配置系统,如 Ansible,可以在这里创建该账户。这个主机是为 Bert 准备的,所以我给他创建了一个账户。

image

图 3-25:添加用户

第九章详细讨论了如何创建用户账户,但在这里我会给出一些合理的设置。Bert 首选的账户名是xistence,我会满足他的要求。我填写了他的名字,并直接按回车键接受默认的Uid登录组。他是这个系统的主要用户,所以我将他添加到 wheel 组中,允许他使用 root 密码。由于 tcsh 是我最喜欢的 shell,所以他使用的是 tcsh。

如果你有关于用户主目录位置的策略,请遵循它。否则,使用默认设置。类似地,虽然你可以根据默认设置调整密码设置,但通常情况下,输入用户的密码是最简单的。很多人推荐使用像ChangeMe这样的密码,但我更喜欢选择那些能积极促使用户尽快更改的密码—也许类似于BertIsTheWorstIMeanTheWorstHumanBeingEver。^(3) 如果我在创建账户后将其锁定,只有在他需要使用这台机器时我才会解锁它。

在添加一个用户后,系统会询问我是否想要添加另一个。如果我为自己添加一个账户,我将对该主机承担部分责任。我选择了“不”。

完成安装

核心配置,如图 3-26 所示,已经完成!接下来,我可以回去调整一些设置。

image

图 3-26:最终配置

这些选项大部分来自安装过程中早期的设置。你想返回更改网络配置吗?选择网络。你是否想添加另一个用户或启用更多服务?是不是输入了错误的密码?这是你纠正这些错误的机会。

当你觉得准备好了,选择退出,你会发现你并不需要完成所有操作。

安装程序涵盖了基础内容,但每个环境都是独特的。手动配置提供了一个命令行提示符,允许你进入系统,并进行最后的更改(参见图 3-27)。选择“不”,系统会提示你移除启动介质并重启。我通常发现,在主机首次启动前进行调整可以简化我的操作,所以我选择了“是”。

image

图 3-27:手动配置

我已经通过 root shell 进入安装的主机。你在这里执行的任务完全取决于你的网络。第九章讨论了 chflags(8) 和 schg。现在我输入exit,如图 3-28 所示。

image

图 3-28:最终的 shell 配置

然后我重新启动,拔出安装介质,并启动进入完整的 FreeBSD 安装!

第四章:开始启动!启动过程**

image

虽然 FreeBSD 在开机时可以轻松且自动地启动,但了解每个阶段到底发生了什么,将使你成为一个更好的系统管理员。在启动过程中通常不需要干预,但有一天你会很高兴自己知道如何操作。一旦你熟悉了调整启动过程,你会发现自己能够解决那些之前你只能接受并忍受的问题。

我们将首先讨论系统加载器是如何启动的,并从加载器获取信息。你可以使用加载器来改变早期的启动过程,包括启动替代内核和进入单用户模式。我们还将讨论串行控制台,这是一种标准的系统管理工具。FreeBSD 的多用户启动过程负责启动所有各种使你的计算机有用的服务,我们也会关注这一点。此外,我们还将讨论 FreeBSD 记录的关于启动过程的信息,以及 FreeBSD 如何在不破坏数据的情况下关闭系统。

递归警告

本章的一些主题引用了后续章节中的内容。这些后续章节反过来要求你先理解本章内容。学习没有固定的起点。如果你对本章的某部分不完全理解,跳过它继续阅读;随着你继续深入,你会发现这些内容会在你的脑海中逐渐清晰。

启动过程本身可以分为三个主要部分:加载器、单用户模式启动和多用户模式启动。

开机

计算机需要足够的智能来找到并加载其操作系统。多年来,这个功能来自基本输入输出系统(BIOS)。较新的系统使用统一可扩展固件接口(UEFI)代替 BIOS。新的安装应该使用 UEFI。其他硬件平台有控制台固件或启动 ROM,执行相同的功能,但我们关注的是常见硬件,因此我们将讨论 UEFI 和 BIOS。

统一可扩展固件接口

UEFI 是替代三十多年历史的 BIOS 的新技术。任何新系统都将启用 UEFI,并期望使用它。

UEFI 在启动盘上搜索标记为 UEFI 启动分区的分区。尽管这个特殊标记可能暗示一些事情,但该分区仅包含一个 FAT 文件系统,以及特定的目录和文件布局。UEFI 执行文件 /EFI/BOOT/BOOTX64.EFI。这个文件可能是一个复杂的多操作系统启动加载器,或者直接将你带入操作系统。在 FreeBSD 中,UEFI 启动会启动加载器,/boot/loader.efi

UEFI 相对较新。如果你的新系统在启动 FreeBSD 时遇到问题,你可以尝试启用 BIOS 或“传统”模式。如果系统在 BIOS 模式下能够启动 FreeBSD,但在 UEFI 模式下无法启动,请按照 第二十四章中的讨论提交 bug。^(1)

基本输入输出系统

最初的 Intel PC 使用了一种基本输入/输出系统(BIOS),它具备足够的能力来在磁盘上查找操作系统。BIOS 会搜索一个被标记为活动的磁盘分区,然后执行该分区的第一部分。对于 FreeBSD 而言,这部分数据被称为加载器。每个 FreeBSD 系统都有一个加载器的引用副本,路径为/boot/loader

BIOS 有各种限制。启动加载器必须位于磁盘的一个特定区域。BIOS 无法从大于 2.2TB 的磁盘启动。目标启动加载器必须小于 512KB——按 1980 年的标准来说,这已经很大了,但今天显得微不足道。已安装的加载器是二进制文件,而不是文件系统,因此即使是微小的更改也需要重新编译。UEFI 没有这些限制,并且提供现代功能,如鼠标支持。

然而,最终来说,BIOS 和 UEFI 的目标都是将系统引导到 FreeBSD 加载器。

加载器

加载器,或称启动块,加载 FreeBSD 内核,并在启动内核之前为你呈现一个菜单。加载器(8)程序在左侧提供七个选项。新的 FreeBSD 系统展示了这些选项:

  1. 启动多用户模式 [Enter]

  2. 启动单用户模式

  3. 跳转到加载器提示

  4. 重启

  5. 内核:default/kernel(1 of 2)

  6. 配置启动选项…

  7. 选择启动环境…

每个选项都突出显示某些单词或字符,比如“启动单用户模式”中的S和“跳转到加载器提示”中的 ESC。选择一个选项可以通过按下高亮字符或数字来实现。

菜单顶部的选项控制 FreeBSD 的启动方式。我们将逐一介绍每个选项。如果你等待 10 秒,加载器将默认自动启动 FreeBSD。

菜单下半部分的选项让你可以微调启动过程。你可以根据需要调整系统的启动方式,稍后我们会讨论这些内容,然后选择前面提到的启动选项之一。

启动多用户模式 [Enter]

这是正常的启动。按 ENTER 立即启动,跳过 10 秒的延迟。

在单用户模式下启动 FreeBSD

单用户模式是一种最小化启动模式,对于损坏的系统非常有用,尤其是当损坏是人为造成的时。它是 FreeBSD 提供命令提示符的最早时刻,这个模式足够重要,因此本章稍后会有专门的章节讲解。

跳转到加载器提示

加载器包括一个命令行解释器,你可以在其中输入命令来调整系统启动方式,确保它完全符合你的需求。我们将在“加载器提示”中详细介绍此内容,见第 55 页。

重启

再来一次,这次要有感觉!

在这些选项中,最重要的是单用户模式和加载器提示。

单用户模式

FreeBSD 可以执行一个最小启动,称为 单用户模式,它加载内核并查找设备,但不会自动设置文件系统、启动网络、启用安全性或运行任何标准的 Unix 服务。单用户模式是系统可能提供命令提示符的最早阶段。

为什么要使用单用户模式?如果一个配置错误的守护进程挂起了启动,你可以进入单用户模式以防止其启动。如果你忘记了根密码,可以通过单用户模式启动并进行更改。如果你需要调整关键的文件系统,单用户模式也是执行此操作的地方。

当你选择单用户模式启动时,你会看到常规的系统启动信息流过。但在任何程序启动之前,内核会给你一个选择 shell 的机会。你可以进入根分区上的任何 shell;我通常选择默认的 /bin/sh,但如果你偏好,可以使用 /bin/tcsh

单用户模式中的磁盘

在单用户模式下,根分区是以只读方式挂载的,且没有其他磁盘被挂载。(我们将在第十章讨论挂载文件系统,但现在请暂时跟随进行。)你想使用的许多程序都位于根分区以外的分区,因此你需要将它们全部挂载为可读写并且可用。执行此操作的方式取决于你使用的是 UFS 还是 ZFS。

单用户模式下的 UFS

为了使 /etc/fstab 文件系统表中列出的所有文件系统可用,请运行以下命令:

# fsck -p
# mount -o rw /
# mount -a

fsck(8) 程序“清理”文件系统,并确认它们在内部的一致性,以及磁盘认为它拥有的所有文件实际上都存在并已记录。

根文件系统是以只读方式挂载的。无论是什么原因导致我们进入单用户模式,可能需要更改根文件系统。重新挂载根文件系统为可读写模式。

最后,mount(8) 命令的 -a 标志会激活 /etc/fstab 中列出的每一个文件系统(参见第十章)。如果这些文件系统中的某一个正在给你带来问题,你可以通过在命令行中指定它们来单独挂载所需的文件系统(例如,mount /usr)。如果你是一个高级用户并且配置了 NFS 文件系统(参见第十三章),你将在此时看到这些文件系统的错误信息,因为网络尚未启动。如果主机在 /etc/fstab 中有网络文件系统,应该只挂载 UFS 文件系统,如下所示。

如果你在按名称挂载分区时遇到困难,可以尝试使用设备名称。根分区的设备名称可能是 /dev/ad0s1a。你还需要为此分区指定一个挂载点。例如,要将你的第一个 IDE 磁盘分区挂载为根分区,请输入以下命令:

# mount /dev/ad0s1a /

如果你的服务器上有网络文件系统,但网络尚未启动,你可以通过指定文件系统类型来挂载所有本地分区。在这里,我们挂载所有类型为 UFS 的本地文件系统,这是 FreeBSD 的默认文件系统类型:

# mount -a -t ufs

现在你可以访问你的 UFS 文件系统了。

单用户模式下的 ZFS

要使所有的 ZFS 数据集可用,使用zfs mount。你可以按名称挂载单独的数据集,也可以使用-a挂载所有标记为可挂载的数据集。

# zfs mount -a

ZFS 将在挂载数据集之前执行常规的完整性检查。

大多数数据集在单用户模式下与多用户模式下一样可访问,但作为根的挂载数据集仍然是只读的。关闭它。我在这里将根数据集设置为可读写,适用于默认的 FreeBSD 安装。

# zfs set readonly=off zroot/ROOT/default

现在你可以更改文件系统。

单用户模式下可用的程序

可用的命令取决于哪些分区已挂载。一些基本命令可以在根分区的/bin/sbin中找到,即使根分区是只读的,它们也可以使用。其他命令存放在/usr中,直到挂载该分区之前无法访问。(看看系统中的/bin/sbin,你可以了解在出现问题时可用的命令。)

如果你破坏了共享库系统(参见第十七章),这些程序将无法使用。如果你真的那么不幸运,FreeBSD 在/rescue目录中提供了许多核心实用程序的静态链接版本。

单用户模式下的网络

如果你希望在单用户模式下保持网络连接,使用脚本/etc/netstart。该脚本会调用适当的脚本启动网络,给接口分配 IP 地址,并启用数据包过滤和路由。如果你只需要其中的一部分服务,你需要查看该脚本并手动执行适当的命令。

单用户模式的用途

在单用户模式下,你对系统的访问仅受限于你对 FreeBSD 和 Unix 的了解。

例如,如果你忘记了根密码,你可以从单用户模式重置它:

# passwd
Changing local password for root
New Password:
Retype New Password:
#

注意

你会注意到系统并没有要求输入旧的根密码。在单用户模式下,你会自动成为 root,而 passwd(8)不会要求 root 输入任何密码。

或者,如果你发现/etc/fstab中有一个错误,导致系统无法启动,你可以通过设备名称挂载根分区,然后编辑/etc/fstab来解决问题。

如果你有一个在启动时导致系统崩溃的程序,且你需要阻止该程序再次启动,你可以编辑/etc/rc.conf来禁用该程序,或者设置启动脚本的权限,使其无法执行。

# chmod a-x /usr/local/etc/rc.d/program.sh

我们将在第十五章讨论第三方程序(端口和包)。

你需要理解单用户模式才能成为一个成功的系统管理员,我们将在本书中多次提到它。不过现在,让我们先来看一下加载器提示。

系统故障与人为错误

所有这些示例都涉及从人为错误中恢复的原因是有原因的。硬件故障并不常见,FreeBSD 故障则更少。如果不是因为人为错误,我们的计算机几乎永远不会让我们失望。随着您对 FreeBSD 的了解不断深入,您将在单用户模式下变得越来越有能力。

加载器提示符

加载器提示符允许您对计算机的启动环境和启动过程中必须配置的变量进行基本更改。它不是类 Unix 环境;它非常简陋,仅支持最小的功能集。当您进入加载器提示符(启动菜单中的第三个选项)时,您将看到如下内容:

Type '?' for a list of commands, 'help' for more detailed help.
OK

这是加载器提示符。虽然OK这个词可能显得友好且令人放心,但它是加载器环境中为数不多的友好特性之一。这不是一个功能齐全的操作系统;它是一个配置系统启动的工具,不适合不了解或胆小的人。您在加载器提示符下所做的任何更改只会影响当前的启动。要撤销更改,请重新启动。(我们将在下一节中看到如何使加载器更改永久生效。)

要查看所有可用的命令,请输入问号。

OK ?
Available commands:
  heap             show heap usage
  reboot           reboot the system
  lszfs            list child datasets of a zfs dataset
--snip--

许多加载器命令除了开发者外对其他人没有什么用处,因此我们将重点介绍对系统管理员有用的命令。

查看磁盘

要查看加载器已识别的磁盘,请使用lsdev

   OK lsdev
➊ cd devices:
   disk devices:
➋ disk0:    BIOS drive C (33554432 X 512):
      ➌ disk0p1: FreeBSD boot
         disk0p2: FreeBSD swap
       disk0p3: FreeBSD ZFS
➍ zfs devices:
       zfs:zroot

加载器检查 CD 驱动器➊,但没有找到任何。 (只有在从 CD 启动时加载器才会找到 CD 驱动器,所以不要因此而惊慌。)它找到了一个硬盘,BIOS 识别为 C 盘➋。然后它描述了该硬盘上的分区。如我们将在第十章中看到的,GPT 分区使用字母p和一个数字来标识分区。分区 disk0p1 ➌是一个 FreeBSD 启动分区,用于从 BIOS 启动 FreeBSD。在一个不熟悉的系统上遇到启动问题时,这些知识可能会很有帮助。加载器还可以识别主机上的 ZFS 池 ➍。

加载器变量

加载器在内核中和配置文件中设置了变量。使用show命令查看这些变量及其设置,并使用空格键翻到下一页。

OK show
LINES=24
acpi.oem=VBOX
acpi.revision=2
acpi.rsdp=0x000e0000
--snip--

这些值包括低级内核调优参数以及从硬件 BIOS 或 UEFI 中获取的信息。我们将在《加载器配置》一章的第 57 页中看到加载器变量的部分列表,书中的相关章节也会提到更多的值。

您可以按名称显示特定的变量。不幸的是,您不能显示一个关键字的所有子变量。像show acpi.oem这样的命令可以使用,但show acpishow acpi.*则无法使用。

使用set命令可以更改单次启动的值。例如,要将console设置更改为comconsole,可以输入:

OK set console=comconsole

加载器允许您更改那些本不应该更改的变量。将 acpi.revision 设置为 4 不会突然将您的系统升级到 ACPI 版本 4,您也不能通过软件设置更换硬盘。

重启

您不是故意进入加载器的吗?重新开始吧。

从加载器引导

既然您已经调整了系统的低级设置,您可能想要启动系统。使用 boot(8)命令。您可以通过 man 页面中讨论的启动标志进一步调整启动。

一旦您的系统启动到您需要的状态,您可能希望将这些设置永久保存。FreeBSD 允许您通过加载器配置文件实现这一点。

加载器配置

通过配置文件/boot/loader.conf使加载器设置永久生效。此文件中的设置会在系统启动时直接传递给引导加载器。当然,如果你喜欢每次系统启动时都呆在控制台前,那就不用管这个了!

加载器有一个默认的配置文件,/boot/defaults/loader.conf。我们在这里覆盖了许多值。

如果您查看默认的加载器配置,您会看到许多与加载器中列出的变量类似的选项。例如,在这里我们可以设置控制台设备的名称:

console="vidconsole"

在整个 FreeBSD 文档中,您会看到提到启动时可调节项加载器设置。这些设置都在loader.conf中配置,其中包括许多 sysctl 值,这些值在系统启动后变为只读。(有关可调节项和 sysctl 的更多信息,请参见第六章)。在这里,我们将内核变量kern.maxusers强制设置为32

kern.maxusers="32"

有些变量在loader.conf中没有设置特定值,而是以空引号出现。这意味着加载器通常让内核设置此值,但如果您想覆盖内核的设置,您可以这样做。

kern.nbuf=""

内核对kern.nbuf的值有一定的认知,但如果需要,您可以让加载器指定一个不同的值。

我们将在适当的部分讨论通过引导加载器进行系统调优——例如,内核值将在第六章中讨论,届时它们会变得有点道理——但在这里,有一些常用的加载器值,影响加载器本身的外观和操作以及基本的启动功能。随着 FreeBSD 的成熟,开发人员会引入新的加载器值并修改旧的功能,所以一定要检查您的安装中的/boot/defaults/loader.conf,以获取当前的列表。

boot_verbose="NO"

这个值切换你可以通过启动菜单访问的详细启动模式。在标准启动中,内核会打印出一些关于每个设备的基本信息。当你以详细模式启动时,内核会要求每个设备驱动程序打印出它能获取的关于设备的所有信息,并显示各种内核相关的设置细节。详细模式对于调试和开发很有用,但通常不适合日常使用。

autoboot_delay="10"

这个值表示启动菜单显示与自动启动之间的秒数。我经常将其调低到 2 或 3 秒,因为我希望我的机器尽可能快地启动。

beastie_disable="NO"

这个值控制启动菜单的显示方式(最初,BSD “Beastie” 吉祥物的 ASCII 艺术图片装饰了启动菜单)。如果设置为 YES,启动菜单将不会显示。

loader_logo="fbsdbw"

这个值允许你选择启动菜单右侧显示的 logo。fbsdbw 选项给你默认的 FreeBSD ASCII 艺术 logo。其他选项包括 beastiebw(原始 logo)、beastie(彩色 logo)和 none(无 logo)。

启动选项

启动菜单还提供了三个选项:选择内核、设置启动选项和选择启动环境。我们将在适当的章节中讨论这些内容,但这里有一些简要的介绍,帮助你入门。

一台主机可以在其 /boot 目录下拥有多个内核。选择 Kernel 选项会让加载器在可用的选项之间循环切换。要让某个内核作为选项出现,需要在 loader.conf 中的 kernels 变量中列出它。

KERNELS="kernel kernel.old kernel.GENERIC"

菜单只识别以 /boot/kernel 开头的目录中的内核。如果你有一个内核在 /boot/gerbil 中,你将需要从加载器提示符加载它。

FreeBSD 支持多种启动选项。选择 Configure Boot Options 项目会弹出最常用的选项。

加载系统默认值

你调整了设置并希望撤销所有这些操作?选择这个。你至少可以将系统启动到单用户模式,并修复你的 loader.conf

ACPI 支持

ACPI 是高级配置和电源接口(Advanced Configuration and Power Interface),是英特尔/东芝/微软的硬件配置标准。它取代并包含了一堆晦涩的标准。ACPI 已经成为标准多年,但如果某个硬件在运行 FreeBSD 时遇到问题,你可以关闭它看看会发生什么。如果你甚至考虑尝试这个选项,务必阅读第二十四章并提交一个 bug 报告。

安全模式

FreeBSD 的 安全模式 启用了操作系统中几乎所有保守的选项。它关闭了硬盘的 DMA 和写入缓存,限制了速度但提高了可靠性。它还关闭了 ACPI。32 位系统会禁用 SMP。在安全模式下,USB 键盘将不再工作。这个选项对于调试旧硬件非常有用。

详细模式

FreeBSD 内核在启动时会探测每一块硬件。大多数发现的信息与日常使用无关,因此引导加载程序不会显示它们。当您以详细模式启动时,FreeBSD 会打印出它能获取的每个系统设置和附加设备的所有细节。这些信息稍后将在/var/run/dmesg.boot中可用,正如下一节所讨论的那样。我鼓励您在新机器上尝试详细模式,看看系统的复杂性。

最后,选择启动环境选项允许您在 ZFS 启动环境之间进行选择,如第十二章所讨论的。

启动消息

启动中的 FreeBSD 系统会显示指示系统附加硬件、操作系统版本以及各种程序和服务启动状态的消息。这些消息在您首次安装系统和进行故障排除时非常重要。启动消息总是以相同的方式开始,首先列出 FreeBSD 项目和加利福尼亚大学董事会的版权声明:

Copyright (c) 1992-2018 The FreeBSD Project.
Copyright (c) 1979, 1980, 1983, 1986, 1988, 1989, 1991, 1992, 1993, 1994
    The Regents of the University of California. All rights reserved.
FreeBSD is a registered trademark of The FreeBSD Foundation.
FreeBSD 12.0-CURRENT #3 r320502: Fri Jun 30 13:48:50 EDT 2017
    root@storm:/usr/obj/usr/src/sys/GENERIC amd64
FreeBSD clang version 4.0.0 (tags/RELEASE_400/final 297347) (based on LLVM 4.0.0)

此外,您还会看到正在启动的 FreeBSD 版本的通知,以及它的编译日期和时间以及使用的编译器。您还可以看到是谁编译了这个内核,在哪台机器上构建的,甚至可以看到这个内核是在文件系统中的哪个位置构建的。如果您经常构建内核,这些信息在确定系统特性时可能非常有价值。

WARNING: WITNESS option enabled, expect reduced performance.

内核将在整个启动过程中打印诊断消息。前面的消息意味着我在这个特定的内核中启用了调试和故障识别代码,因此我的性能将受到影响。在这种情况下,我不关心性能影响,原因稍后会变得清晰。

Timecounter "i8254" frequency 1193182 Hz quality 100

此消息标识了特定的硬件。时间计数器,或硬件时钟,是一种特殊的硬件,虽然您的计算机需要它,但它是如此低级的设备,最终用户实际上无法直接使用它。偶尔,您会看到类似的消息,涉及一些对用户不可见但对系统至关重要的硬件。启动消息在显示过多细节和隐藏可能至关重要的细节之间摇摆。例如,它还会显示系统中 CPU 的所有信息:

CPU: Intel(R) Xeon(R) CPU E5-1620 v2 @ ➊3.70GHz (3700.08-MHz K8-class CPU)
  Origin="GenuineIntel"  Id=0x306e4  Family=0x6  Model=0x3e  Stepping=4
              ➋ Features=0xbfebfbff<FPU,VME,DE,PSE,TSC,MSR,PAE,MCE,CX8,APIC,SEP,MTRR,PGE,MCA,
CMOV,PAT,PSE36,CLFLUSH,DTS,ACPI,MMX,FXSR,SSE,SSE2,SS,HTT,TM,PBE>
  Features2=0x7fbee3ff<SSE3,PCLMULQDQ,DTES64,MON,DS_CPL,VMX,SMX,EST,TM2,SSSE3,CX16,
xTPR,PDCM,PCID,DCA,SSE4.1,SSE4.2,x2APIC,POPCNT,TSCDLT,AESNI,XSAVE,OSXSAVE,AVX,F16C,RDRAND>
  AMD Features=0x2c100800<SYSCALL,NX,Page1GB,RDTSCP,LM>
  AMD Features2=0x1<LAHF>
  Structured Extended Features=0x281<FSGSBASE,SMEP,ERMS>
  XSAVE Features=0x1<XSAVEOPT>
  VT-x: PAT,HLT,MTF,PAUSE,EPT,UG,VPID,VID,PostIntr
  TSC: P-state invariant, performance statistics

您可能不知道,简单的 CPU 竟然有如此多的细节和特性,对吧?但是当您提交故障报告时,指出某些高级特性无法正常工作,开发人员可能会问您的 CPU 是否具有某个特定的特性。

这是我不担心之前提到的WITNESS选项所导致的性能损失的原因:这台机器相当快➊,并且支持许多对现代 CPU 来说至关重要的功能➋。虽然我当然希望能够充分利用我所支付的性能,但我也希望在问题发生时能够及时捕捉到。我希望能够针对这些问题提交有效的 bug 报告,这样开发者才能听到我的问题报告。毕竟,这就是为什么我会运行带有 WITNESS 启用的 FreeBSD 开发版本!

FreeBSD/SMP: Multiprocessor System Detected: 8 CPUs

在这里,内核宣布它已经找到了所有八个 CPU 核心,并准备好管理它们。我有充足的 CPU 和相当多的内存。

➊ real memory  = 34359738368 (32768 MB)
➋ avail memory = 33207656448 ➌(31669 MB)

实际内存➊是计算机中实际安装的 RAM 数量,而可用内存➋是内核加载后剩余的内存。我的系统有 31,669MB 的 RAM ➌ 可供实际使用,这足以应对当前系统的负载。

➊ioapic0 <Version 2.0> ➋irqs 0-23 on motherboard
➌ioapic1 <Version 2.0> irqs 24-47 on motherboard

这是一个相当典型的设备条目。这个设备被称为 ioapic,内核已经发现这个硬件是 2.0 版本,并且有额外的信息与之关联➋。更重要的是,我们发现了两个这种类型的设备,编号为 0 ➊ 和 1 ➌。(所有设备的编号从零开始。)你可以通过阅读设备驱动程序的手册页来了解更多关于这个设备的信息。几乎所有—但并非全部—设备驱动程序都有手册页。

usbus0: EHCI version 1.0
usbus0 on ehci0
usbus0: 480Mbps High Speed USB v2.0

并非所有设备驱动程序都将它们的所有信息打印在一行上。在这里,我们有一个单一设备,usbus,它用三行来表示仅有的一个设备实例。唯一知道这是单一 USB 总线而非三个独立总线的方法是检查设备的编号。所有这些都属于设备编号零,所以它是一个单一设备。

➊ pci0: <simple comms> at device 22.0 (no driver attached)
➋ pcib8: <ACPI PCI-PCI bridge> irq 17 at device 28.0 on ➌pci0
➍ pci8: <ACPI PCI bus> on ➎pcib8

启动信息的一个有趣之处是,它们展示了计算机各个组件是如何连接在一起的。在这里,我们有 pci0 ➊,一个直接在主板上的 PCI 接口。然后是 pcib8 ➋,第八号 PCI 桥接器,连接到 pci0 ➌。我们还发现了连接到该 PCI 桥接器的 PCI 总线 pci8 ➍。继续阅读,你会发现连接到该总线的各个设备。现在你可能无法对这些信息做太多事情,但你会发现,当你需要排除故障时,拥有这些信息会非常有价值。

➊em0: <Intel(R) PRO/➋1000 Network Connection> port 0xd000-0xd01f mem 
0xfba00000-0xfba1ffff,0xfba20000-0xfba23fff irq 18 at device 0.0 on pci9

这个条目显示了 em0,一个 em(4) 类型的网络卡➊,并指出该卡支持千兆以太网➋。我们还看到关于它的内存地址、IRQ 和 PCI 总线连接等各种信息。

你计算机上的每个设备都有一个或多个类似这样的条目。整体来看,它们合理详细地描述了你计算机的硬件。如果你以详细模式启动,你会看到更多的细节—可能比你想要的还要多。

启动信息文件

尽管启动信息很有用,但它很可能在你需要时已经从屏幕上消失。为了将来参考,FreeBSD 会将启动消息存储在文件 /var/run/dmesg.boot 中。这意味着即使系统已经运行了几个月,你仍然可以查看内核的硬件消息。

内核在启动消息中显示的一个关键内容是每个硬件设备的设备名称。这对于管理系统至关重要。每个硬件设备都有一个设备节点名称,要配置它,你需要知道这个名称。例如,之前我们看到了一张名为 em0 的以太网卡。该卡使用 em(4) 驱动程序,第一个由此驱动程序控制的设备编号为零。第二个同类型的设备将是 em1,接着是 em2,以此类推。

大多数可以配置或管理的设备都会在 /dev 目录下的某个位置有一个设备节点条目。例如,第一个光驱由文件 /dev/cd0 表示。这些文件被称为 设备节点,它们是指向特定硬件的便捷方式。大多数设备节点不能像常规文件那样直接访问;你不能对设备节点使用 cat(1) 命令,也不能将另一个文件复制到它上面。然而,设备节点会作为参数传递给专门的程序。例如,启动时出现的硬盘 ada4 与设备节点 /dev/ada4 是相同的。当你想挂载这块硬盘时,可以使用设备节点名称,确保你正处理的是那块特定的硬件。

多用户启动

超越单用户模式,你将找到多用户模式。这是类似 Unix 的操作系统的标准操作模式。如果你在做实际工作,你的系统处于多用户模式。

当 FreeBSD 完成硬件检测并适当地加载所有设备驱动程序后,它会运行 shell 脚本 /etc/rc。该脚本挂载所有文件系统,启动网络接口,配置设备节点,识别可用的共享库,并完成其他使系统准备好进行正常工作的任务。大多数系统有不同的启动要求;虽然几乎所有服务器都需要挂载硬盘,但 Web 服务器的操作要求与数据库服务器的操作要求非常不同,即使它们运行在完全相同的硬件上。这意味着 /etc/rc 必须非常灵活。它通过将所有任务委派给其他负责系统特定方面的 shell 脚本来实现灵活性。

/etc/rc 脚本由文件 /etc/defaults/rc.conf/etc/rc.conf 控制。

/etc/rc.conf、/etc/rc.conf.d 和 /etc/defaults/rc.conf

与加载器配置文件类似,/etc/rc 的配置分为两个文件:默认设置文件 /etc/defaults/rc.conf 和本地设置文件 /etc/rc.conf/etc/rc.conf 中的设置会覆盖 /etc/defaults/rc.conf 中的任何值,正如加载器的配置方式一样。

/etc/defaults/rc.conf 文件非常庞大,包含许多变量,通常被称为 knobstunables。我们不会讨论所有这些变量,不仅因为 knobs 会不断增加,列出它们会很快过时,还因为很多 knobs 在服务器上并不常用。几乎 FreeBSD 系统中的每个东西都有一个或多个 rc.conf knobs,从键盘映射到 TCP/IP 行为。要查看完整且最新的列表,请阅读 rc.conf(5)。要更改 rc.conf 设置,可以使用文本编辑器或 sysrc(8)。

sysrc(8)

虽然手动编辑 rc.conf 完全可行,但在云计算时代,这种方式在大量机器上不可持续。如果必须修改数十台服务器,你需要一种可靠的方法来修改系统,而不是手动编辑每台服务器的配置或依赖 sed/awk 的“黑客”方法。^(2)

FreeBSD 包括 sysrc(8),这是一个命令行程序,可以一致且安全地从命令行修改 /etc/rc.conf 及其相关文件。此外,sysrc(8) 还可以显示有关系统非默认设置的信息。

使用 -a 来询问 sysrc(8) 你主机的相关信息。

# sysrc -a
clear_tmp_enable: YES
defaultrouter: 203.0.113.1
dumpdev: AUTO
keymap: us.dvorak.kbd
--snip--

你将获得所有非默认 /etc/rc.conf 设置的列表。

要让 sysrc(8) 启用某个服务,提供变量名、等号和新值。

# sysrc rc_startmsgs=NO
rc_startmsgs: YES -> NO

变量 rc_startmsgs 现在被设置为 no

请记住,sysrc(8) 是一个用于更改 rc.conf 的工具,而不是用来配置 FreeBSD 的工具。它不会进行有效性检查。我一个非常初级的系统管理员不希望 Bert 登录,他根据一些错误的建议进行了设置。

# sysrc bert=no

虽然这段代码在 /etc/rc.conf 中设置了 bert="no",但这个变量并没有任何作用。可以通过 -x 参数删除它。

# sysrc -x bert

许多 FreeBSD 配置文件与 rc.conf 类似。你可以通过添加 -f 参数和文件名,使用 sysrc(8) 来管理它们。

# sysrc -af /boot/loader.conf

是否应该编辑 rc.conf 或使用 sysrc(8)?如果你是在进行手动修改,那么使用你偏好的方式。如果是自动化操作,应该倾向于使用 sysrc(8)。本书中混合了两者的示例。

/etc/rc.conf.d/

如果你使用像 Puppet 或 Ansible 这样的服务器配置系统,你可能更信任复制整个文件而不是编辑它们。通过这些工具,使用 /etc/rc.conf.d/ 文件来启用服务。

要在 /etc/rc.conf.d/ 中管理一个服务,创建一个以服务名称命名的文件。也就是说,要管理 bsnmpd(8),你需要创建 /etc/rc.conf.d/bsnmpd。在该文件中启用或禁用该服务。

bsnmpd_enable=YES

我通常使用 Ansible 的服务启用功能,直接修改 /etc/rc.conf 而不是 /etc/rc.conf.d,但可以根据个人喜好选择使用。

接下来的几节展示了你可以在 /etc/rc.conf 中启用和禁用的各种内容。每个设置都会出现在 /etc/defaults/rc.conf 中,并可以通过 /etc/rc.conf 条目进行覆盖。每个变量都带有默认设置。

启动选项

以下rc.conf选项控制 FreeBSD 如何配置自身并启动其他程序。这些深远的设置会影响所有其他系统程序和服务的运行。

如果您遇到启动脚本本身的问题,您可以在/etc/rc及其下属脚本上启用调试功能。这可以提供有关脚本为何未能启动的额外信息。

rc_debug="NO"

如果您不需要完整的调试输出,但希望获得有关/etc/rc 过程的额外信息,可以使用rc_info启用信息性消息:

rc_info="NO"

当启动过程进入多用户启动阶段时,它会打印出每个启动的守护进程的消息。使用rc_startmsgs选项可以移除这些消息。

rc_startmsgs="NO"
文件系统选项

FreeBSD 可以使用内存作为文件系统,正如我们在第十三章中讨论的那样。此功能的一个常见用途是通过使用内存而不是硬盘来加速/tmp。在阅读完第十三章后,您可能会考虑实现此功能。rc.conf中的变量允许您启用内存支持的/tmp并透明、轻松地设置其大小。您还可以选择 FreeBSD 将使用哪些选项来完成文件系统的配置。(那些急切的朋友们可能在想-S标志意味着什么。它的意思是禁用软更新。如果你不知道这是什么意思,也请等到第十一章再来看。)如果您想使用内存文件系统作为/tmp,请将tmpmfs设置为YES,并将tmpsize设置为您希望的/tmp大小。

tmpmfs="AUTO"
tmpsize="20m"
tmpmfs_flags="-S"

另一个流行的 FreeBSD 文件系统功能是其集成的加密分区。FreeBSD 开箱即用支持两种不同的文件系统加密系统:GBDE 和 GELI。基于 GEOM 的磁盘加密(GBDE)是 FreeBSD 设计的第一个加密文件系统,旨在用于军事级别的安全。GELI 则更加友好,并且符合与 GBDE 不同的标准。(在启用这些功能之前,您一定要阅读第二十三章!)

geli_devices=""
geli_tries=""
geli_default_flags=""
geli_autodetach="YES"

默认情况下,FreeBSD 在进入多用户模式后会以可读写的方式挂载根分区。如果您希望改为以只读模式运行,可以将以下变量设置为NO。许多人认为这种方式更安全,但只读根分区可能会干扰某些软件的运行,并且肯定会阻止您编辑根分区上的任何文件!

root_rw_mount="YES"

当启动中的 FreeBSD 尝试挂载其文件系统时,它会检查文件系统的内部一致性。如果内核发现文件系统存在重大问题,它可以尝试使用fsck -y自动修复这些问题。虽然在某些情况下这是必要的,但并非完全安全。(在启用此功能之前,请务必仔细阅读第十一章!)

fsck_y_enable="NO"

内核可能还会发现一些小的文件系统问题,它会在系统以多用户模式运行时,通过使用后台 fsck 自动解决这些问题,正如在第十一章中讨论的那样。在某些情况下,使用此功能的安全性存在合理的担忧。你可以控制后台 fsck 的使用,并设置系统在开始后台 fsck 之前等待多长时间。

background_fsck="YES"
background_fsck_delay="60"
杂项网络守护进程

FreeBSD 包含许多较小的程序或守护进程,它们在后台运行以提供特定的服务。我们将在全书中介绍一些集成服务,但这里有几个对有经验的系统管理员来说特别有用的守护进程。一个流行的守护进程是 syslogd(8)。日志是好东西。日志是如此非常好,以至于第二十一章的大部分内容都专门讨论了在 FreeBSD 上如何使用、为、通过和在日志记录。

syslogd_enable="YES"

一旦决定运行日志守护进程,你可以通过设置命令行标志来精确控制它的运行方式。FreeBSD 在启动守护进程时会使用这些标志。所有可以接受命令行标志的 rc.conf 中的程序,都采用以下格式设置标志:

syslogd_flags="-s"

另一个流行的守护进程是 inetd(8),它是用于小型网络服务的服务器。(我们在第二十章中介绍 inetd。)

inetd_enable="NO"

大多数系统使用安全外壳(SSH)守护进程进行远程登录。如果你希望通过网络远程连接到你的系统,几乎可以肯定需要 SSH 服务。

sshd_enable="NO"

虽然可以通过命令行配置 SSH 守护进程,但通常更好的方法是使用 /etc/ssh/ 中的配置文件。有关详细信息,请参见第二十章。

sshd_flags=""

FreeBSD 还集成了广泛的时间同步软件,确保系统时钟与外部世界同步。你需要配置这些设置才能使其有效;我们将在第二十章中详细讨论。

ntpd_enable="NO"
ntpd_flags="-p /var/run/ntpd.pid -f /var/db/ntpd.drift"

此外,FreeBSD 还包括一个小型的 SNMP 守护进程,用于与基于 SNMP 的管理工具配合使用。我们将在第二十一章中讨论如何配置 SNMP。

bsnmpd_enable="NO"
网络选项

这些控制项决定了 FreeBSD 在启动时如何配置其网络设施。我们将在第七章中讨论网络配置。

互联网上的每台机器都需要一个主机名。主机名是系统的完全合格域名,例如 www.absolutefreebsd.org。没有这个主机名,许多程序将无法正常运行。

hostname=""

FreeBSD 包括一些不同的集成防火墙软件包。我们将在第十九章中简要介绍包过滤器(PF)。在 rc.conf 中启用和禁用 PF。

pf_enable="NO"

你可能对网络上尝试连接到你的系统的失败尝试感兴趣。这有助于检测端口扫描和网络入侵尝试,但也会收集大量垃圾数据。短时间内设置它查看网络上真正发生了什么会很有趣。(不过,知道什么是真正发生的往往会引起焦虑。)设置为 1 以记录失败的连接尝试。

log_in_vain="0"

路由器使用 ICMP 重定向来通知客户端计算机特定路由的正确网络网关。虽然这是完全合法的,但在一些网络中,入侵者可以利用这一点来捕获数据。如果你在网络中不需要 ICMP 重定向,可以为此选项设置一个极其小的安全措施。如果你不确定是否正在使用它们,可以询问你的网络管理员。

icmp_drop_redirect="NO"

如果你是网络管理员,并且不确定你的网络是否使用 ICMP 重定向,有一种简单的方法可以找出——只需将系统接收到的所有重定向记录到 /var/log/messages 中。^(3) 请注意,如果你的服务器正在遭受攻击,这可能会很快填满你的硬盘。

icmp_log_redirect="NO"

要连接到网络,你需要为每个接口分配一个 IP 地址。我们将在第八章中详细讨论这一点。你可以使用 ifconfig(8) 命令获取网络接口的列表。每个网络接口列在其自己的行上,配置的网络信息放在引号中。例如,要为你的 em0 网卡分配一个 IP 地址 172.18.11.3 和子网掩码 255.255.254.0,你可以使用:

ifconfig_em0="inet 172.18.11.3 netmask 255.255.254.0"

如果你的网络使用 DHCP,请使用 dhcp 作为 IP 地址。

ifconfig_em0="dhcp"

类似地,你可以为网络卡分配别名。别名不是卡的实际 IP 地址,但卡会响应该 IP 地址,如第八章中所述。FreeBSD 支持在单个网卡上配置数百个别名,形式如下:rc.conf 中的条目:

ifconfig_em0_aliasnumber="address netmask 255.255.255.255"

别名编号必须是连续的,从 0 开始。如果编号中断,编号中断之后的别名将在启动时不会被安装。(这是一个常见问题,当你遇到此问题时,请检查你的别名列表。)例如,192.168.3.4 的别名将列为:

ifconfig_em0_alias0="192.168.3.4 netmask 255.255.255.255"
网络路由选项

FreeBSD 的网络栈包括许多用于路由互联网流量的功能。这些从最基础的配置开始,例如为默认网关配置 IP 地址。虽然为网络接口分配有效的 IP 地址让你能够进入本地网络,但默认路由器将使你可以访问局域网以外的一切。

defaultrouter=""

网络控制设备,如防火墙,必须在不同的接口之间传递流量。虽然 FreeBSD 默认不会执行此操作,但启用它非常简单。只需告诉系统它是一个网关,它就会为你连接多个网络。

gateway_enable="NO"
控制台选项

控制台选项控制着显示器和键盘的行为。你可以更改键盘的语言、显示器的字体大小,或者几乎任何你想调整的内容。例如,键盘映射默认使用标准的美国键盘,通常称为 QWERTY。你可以在目录 /usr/share/syscons/keymaps 中找到各种键盘映射。我个人偏好 Dvorak 键盘布局,它在该目录下的条目为 us.dvorak。将 keymap 设置为 us.dvorak 后,我的系统将在启动至多用户模式时使用 Dvorak 键盘布局。

keymap="NO"

FreeBSD 会在键盘长时间空闲后将显示器调暗,空闲时间由 blanktime 设置决定。如果将其设置为 NO,FreeBSD 将不会调暗屏幕。不过,新硬件也会在一段时间后调暗显示器,以节省电力。如果你已经将 blanktime 设置为 NO,但屏幕仍然变暗,请检查你的 BIOS 设置和显示器手册。

blanktime="300"

FreeBSD 还可以在控制台上使用多种字体。虽然默认字体对于服务器来说足够了,但你可能希望在桌面或笔记本电脑上使用不同的字体。我的笔记本电脑有一个 17 英寸的屏幕,适合观看电影,而默认字体在这个尺寸下看起来有些不合适。你可以从目录 /usr/share/syscons/fonts 中选择新字体,试试不同的字体,看看它们在系统上的效果。字体名称包括字体大小,因此你可以设置相应的变量。例如,字体 swiss-8x8.fnt 是瑞士字体,大小为 8 像素 x 8 像素。要使用它,你需要设置 font8x8 变量。

font8x16="NO"
font8x14="NO"
font8x8="YES"

即使没有图形用户界面,你也可以在控制台上使用鼠标。默认情况下,FreeBSD 会尝试自动检测你的鼠标类型。如果你使用的是 PS/2 或 USB 鼠标,启用鼠标守护进程后,它很可能会自动工作,无需特别配置。一些较旧或不常见的鼠标类型需要手动配置,具体内容请参见 moused(8)。

moused_enable="NO"
moused_type="AUTO"

你还可以根据需要调整显示器上的显示内容。如果你的显示器尺寸不寻常,你可以调整文本的行数和长度,使其适配;还可以更改文本颜色、光标及其行为,进行其他各种小调整。你可以在 man vidcontrol(1) 中查看不同选项的完整列表。

allscreens_flags=""

同样地,你几乎可以任意调整键盘的行为。从按键重复速度到功能键的效果,都可以根据需要进行配置,具体内容请参见 kbdcontrol(1)。

allscreens_kbdflags=""
其他选项

这些最终的选项可能在特定环境下有用,也可能没有,但由于它们经常需要使用,因此值得提及。例如,并非所有系统都有打印机可用,但有打印机的系统会希望运行打印守护进程 lpd(8)。我们会在第二十章中提到打印机配置。

lpd_enable="NO"

sendmail(8) 守护进程管理系统间邮件的发送和接收。虽然几乎所有的系统都需要发送邮件,但大多数 FreeBSD 机器不需要接收邮件。sendmail_enable 控制接收邮件,而 sendmail_outbound_enable 则允许机器发送邮件。有关更多详细信息,请参见第二十章。

sendmail_enable="NO"
sendmail_submit_enable="YES"

FreeBSD 的一个更有趣的特点是它能够运行为 Linux 构建的软件。我们在第十七章中讨论了这个功能。运行 Linux 软件并不像打开一个开关那么简单,所以在启用 Linux 兼容模式之前,先阅读那一章!

linux_enable="NO"

任何类 Unix 操作系统的一个重要组成部分是共享库。你可以控制 FreeBSD 查找共享库的位置。尽管默认设置通常足够用,但如果你发现自己经常为用户设置 LD_LIBRARY_PATH 环境变量,应该考虑调整库路径。有关库路径的更多建议,请参见第十七章。

ldconfig_paths="/usr/lib /usr/local/lib"

FreeBSD 有一个安全配置文件系统,允许管理员控制基本的系统功能。你可以全局禁止挂载硬盘、访问特定的 TCP/IP 端口,甚至更改文件。有关如何使用这些功能的详细信息,请参见第九章。

kern_securelevel_enable="NO"
kern_securelevel="-1"

现在你已经了解了 FreeBSD 默认支持的一些配置选项,让我们来看一下它们是如何使用的。

rc.d 启动系统

FreeBSD 通过 shell 脚本 /etc/rc 弥补了单用户模式和多用户模式之间的空白。这个脚本读取配置文件 /etc/defaults/rc.conf/etc/rc.conf,并根据这些文件中找到的内容运行一系列其他脚本。例如,如果你启用了网络时间守护进程,/etc/rc 会运行一个专门用来启动该守护进程的脚本。FreeBSD 包含了用于启动服务、挂载磁盘、配置网络和设置安全参数的脚本。

这些脚本位于/etc/rc.d/usr/local/etc/rc.d。我建议你阅读其中的一些脚本,即使只是为了了解 rc.d 系统是如何工作的。

使用 service(8) 来控制这些脚本。

service(8) 命令

所有 rc.d 脚本都是可读的,它们如何组合在一起是相当直观的。当你遇到问题时,你可以阅读这些脚本,看看它们是如何工作的,做了什么。不过这有点像工作,大多数系统管理员有更有趣的工作要做。service(8) 命令提供了一个友好的前端来管理 rc.d 脚本。你可以使用 service(8) 查看哪些脚本会自动运行;停止、启动和重启服务;检查服务状态;以及更多操作。

列出和识别已启用的服务

使用 -e 标志调用 service(8),可以查看系统启动时将按顺序运行的所有脚本的完整路径。

# service -e
/etc/rc.d/hostid
/etc/rc.d/zvol
/etc/rc.d/hostid_save
/etc/rc.d/zfsbe
--snip--
/etc/rc.d/sshd
/etc/rc.d/sendmail
--snip--

这个小型主机在启动时运行 23 个脚本。

这里的一个重要细节是脚本名称。你将会在其他命令中使用脚本名称,比如启动、停止和重启服务。

管理服务

虽然完全可以在命令行中重新启动,例如,sshd(8),但生产主机需要保持所有服务的一致运行。最佳实践是使用 service(8) 来管理进程。你需要像前面展示的那样使用脚本名称,但不包含目录路径。

# service name command

例如,假设我想重启 sshd(8) 服务。根据之前显示的 service -e 输出,有一个脚本 /etc/rc.d/sshd。我强烈怀疑这个脚本管理着 sshd(8),但我想确定自己不会不小心重启 Stupidly Similarly named Harassment Daemon。此时就需要用到 describe 命令。让我们让 service(8) 来描述 sshd 脚本。

# service sshd describe
Secure Shell Daemon

它是正确的守护进程。让我们重新启动它。

   # service sshd restart
➊ Performing sanity check on sshd configuration.
➋ Stopping sshd.
➌ Performing sanity check on sshd configuration.
➍ Starting sshd.

重启服务实际上是“停止服务”和“启动服务”的组合。不过这个特定的服务不仅仅做这些。它首先通过验证配置文件 ➊ 然后停止守护进程 ➋,接着重新验证配置 ➌ 并启动守护进程 ➍。为什么?

SSH 处理此主机的远程访问。如果 SSH 服务出现故障,任何人都无法登录主机来修复 SSH 服务。是的,你可以使用远程 KVM 或 IPMI,或者开车去托管中心,但这些都会延长停机时间。最好在关闭之前验证 sshd(8) 是否可以重新启动。许多服务脚本包括这种安全检查。如果某个服务抱怨无法停止,仔细查看输出,找出原因。

每个服务支持的命令各不相同。获取特定服务支持的完整命令列表的最简单方法是给服务一个无效参数。像“bert”这样的参数就相当无效。

# service sshd bert
/etc/rc.d/sshd: unknown directive 'bert'.
Usage: /etc/rc.d/sshd fast|force|one|quiet

你将获得此服务支持的完整命令列表,分为两组。

第一组(用方括号括起来)包含命令的选项。这里是标准选项。将它们作为第二组命令的前缀来使用。

fast 不进行检查(用于启动时)。

force 更加努力地尝试。

one 即使在 rc.conf 中未启用,也启动该服务。

quiet 只打印服务名称(用于启动时)。

第二组(用圆括号括起来)包含以下命令:

start 启动服务。

stop 停止服务。

restart 停止并重启服务。

rcvar 打印此服务的rc.conf变量。

enabled 如果启用则在 shell 中返回 true(用于脚本)。

describe 打印服务描述。

extracommands 显示服务特定的命令。

extracommands 命令非常特定于服务,并且仅列出该服务接受的附加命令。默认情况下,附加命令会出现在默认命令之后。这里是一些常见的附加命令:

configtest 解析服务的配置文件,并在出现错误时停止。

reload 执行软重载(通常通过 SIGHUP),而不是重启。

status 确定服务是否正在运行。

要确定服务的额外命令到底做了什么,你需要阅读服务脚本。

我们将在第十七章中详细讨论 rc.d,那时我们会讨论如何定制和编写你自己的 rc.d 脚本。

系统关闭

FreeBSD 使得 rc.d 启动系统发挥双重作用;它不仅必须处理系统启动,还必须在关机时关闭所有这些程序。必须有某些操作来卸载所有硬盘,关闭守护进程,并在完成所有工作后进行清理。一些程序不在乎它们是否在系统关闭时被无情地终止——毕竟,系统关闭后,通过 SSH 连接的客户端会被踢掉,任何未完成的网页也会停留在中间。然而,数据库软件非常关心它是如何关闭的,无情地杀死进程会损坏你的数据。许多管理实际数据的其他程序也非常挑剔,如果你不让它们清理自己,你会后悔的。

当你使用 shutdown(8)、halt(8) 或 reboot(8) 命令关闭 FreeBSD 时,系统会调用 shell 脚本 /etc/rc.shutdown。该脚本依次调用每个 rc.d 脚本,并传递 stop 选项,顺序与启动时相反,从而允许服务器程序优雅地终止,并在断电前清理磁盘。

串行控制台

所有这些控制台的东西都很好,但当你的 FreeBSD 系统位于全国另一端或另一个大洲的共置设施中时,你无法走到键盘前开始输入。许多数据中心没有足够的空间放置键盘或显示器。那么,当机器无法响应网络时,如何远程重启它呢?使用串行控制台将计算机的键盘和视频信号重定向到串行端口,而不是键盘和显示器,可以解决这些问题。

串行控制台可以是物理的,例如计算机背面的串行端口。通过将标准的空接线调制解调器电缆连接到串行端口,并将另一端连接到另一台计算机的串行端口,你可以通过第二台计算机访问第一台系统的启动信息。

它们也可以是虚拟的,例如由 IPMI 的 Serial-over-LAN (SOL) 协议提供的虚拟控制台。你需要设置 IPMI 接口并使用特殊软件配置和访问虚拟串行端口,而不是使用空接线调制解调器电缆。

不过,在我们设置端口之前,先来了解一下串行端口协议。

串行协议

一些最早的计算机控制台是通过串行端口连接到电传打字机的。串行通信已经存在很长时间,并在几十年中不断发展。与现代协议不同,串行线路不会自动协商。你必须将串行连接的两端配置为完全相同的设置。如果配置不匹配,屏幕可能会变成空白,或者显示乱码。

原始的串行线路工作在低速下。许多串行电缆基本保持不变,但我们已经开发出了更好的软件和硬件,能够在每端连接,使我们能够更快地传输数据。旧的串行连接速度为 300 比特每秒(波特率),而一大批现代硬件可以运行在 115,200 波特。尽管如此,在不同的硬件平台之间,常见的标准波特率是 9600 波特,这是 FreeBSD 的默认控制台速度。9600 波特率足以以舒适的速度传输整屏的文本。

对于物理连接,最好坚持使用 9600 波特率,除非无法做到。某些现代硬件不支持 9600 波特率。也有一些硬件宣称支持 9600 波特率,但实际上并不支持。我曾经遇到过硬件被硬编码为 115,200 波特的设备。任何无法或坚决拒绝使用 9600 波特率的设备在设计上就是有问题的,但我们通常无法控制硬件的选择。除非是硬件限制,否则更改串口速度会让你的连接变得更加脆弱,而如果你正在使用控制台,你是没有心情面对脆弱性的。当我提到更改端口速度时,那仅仅是当你必须这样做时才使用的建议。

SOL 连接不是物理线缆,因此你不需要担心线路噪声。你可以安全地在更高的速度下运行它们。

串行协议还包括许多除了速度之外的设置。你可以修改它们,但 8 个数据位、无奇偶校验和 1 个停止位的标准设置是最广泛使用的。你无法在 FreeBSD 中更改这些设置,除非重新编译内核,因此不要乱动这些设置。

记住这些,我们来设置一个控制台。

物理串行控制台设置

无论你使用的是何种串行控制台,你都需要正确地插入连接才能使其工作。你需要一根空调调制解调器电缆,可以在任何计算机商店或在线供应商处购买。虽然镀金串行电缆不值得花钱,但也不要购买你能找到的最便宜的电缆;如果你遇到紧急情况并需要串行控制台,你可能不会心情去忍受线路噪声!^(4)

将空调调制解调器电缆的一端插入你的 FreeBSD 服务器上的串行控制台端口——默认情况下是第一个串行端口(COM1 或 uart0,具体取决于你习惯使用的操作系统)。你可以通过服务器来更改此设置。

将空调调制解调器电缆的另一端插入另一个系统的空闲串口。我推荐使用另一个 FreeBSD(或其他 Unix 系统)或终端服务器,但如果你只有 Windows 系统,也可以使用。

如果你在远程位置有两台 FreeBSD 机器,确保它们每台都有两个串口。准备两根空中调制解调器电缆,并将每台机器的第一个串口连接到另一台机器的第二个串口。这样,你可以将每台机器作为另一台机器的控制台客户端。如果你有三台机器,可以将它们串联成一个环形。通过将两台和三台机器组合在一起,你可以在任何数量的系统上使用串行控制台。我曾在数据中心工作,那里有 30 或 40 台 FreeBSD 机器,安装显示器根本不现实,我们用串行控制台取得了很好的效果。然而,一旦你有了一两个机架的服务器,投资一个终端服务器是一个非常好的主意。你可以在 eBay 上找到它们,价格便宜。

另一种选择是使用两台 DB9 转 RJ45 转换器,一台标准转换器和一台交叉转换器。这样,你可以通过标准的 CAT5 电缆连接控制台。如果你有一个禁止人类进入的灯塔数据中心,你可以将串行控制台的连接引到你的桌子旁、温暖的房间或任何其他标准以太网式跳线板可达的地方。大多数现代数据设施更适合处理以太网,而不是串行电缆。

IPMI 串行控制台设置

智能平台管理接口 (IPMI) 是一种在硬件层面上管理计算机系统的标准。IPMI 独立于操作系统运行,使用一个叫做基板管理控制器 (BMC) 的小型设备。基本上,BMC 就像是你的远程双手和双眼,用来控制服务器。要使用 IPMI 控制台,你需要同时配置 BMC 和主机的 BIOS 或 UEFI。

我会在这里尝试给你一些方向,但配置 BMC 或 UEFI 的最佳资源是你的硬件手册。^(5)

BMC 设置

服务器的 BMC 有自己的 IP 地址,通常会获得主板上的专用以太网端口。每个厂商根据自己的偏好设计自己的 BMC。这意味着,配置 BMC 的内容远远超出了本书的范围,但这里有一些提示。

大多数 BMC 配置通过网页界面进行。然而,在你访问网页界面之前,BMC 需要一个 IP 地址。大多数 BMC 的 IP 信息可以在 BIOS 或 UEFI 固件的设置菜单中进行设置。进入管理界面后,配置用户名和密码。记住它们。

一般的 BMC 还包括诸如主系统的电源循环、通过某种下载应用程序(通常是 Java)进行远程控制台访问、虚拟媒体等功能。

永远不要忘记,BMC 是一个运行 Web 服务器的小型嵌入式计算机,它是由某个超负荷工作的公司员工编写的,目的是建立最小可行产品。BMC 并没有经过长时间运行后的性能测试。如果它给你带来任何问题,就重启它。你不需要重启整个计算机;通常在网页界面中会有一个“BMC 重置”或“单元重启”的选项。

如果 BMC 支持基于小程序的控制台,为什么还要使用串口控制台?因为 BMC 控制台是基于小程序的,而 BMC 固件很少更新。我有很多 BMC 控制台,只能使用过时、不安全^(6)的 Java 版本。使用它们需要覆盖安全警告并反复点击“是的,我知道我很傻,还是继续做”框。我不得不专门保留一个虚拟机,用这个不安全的 Java 版本来访问这些控制台。基于小程序的控制台不支持复制粘贴,并且通常延迟很高。

在较慢的连接上,IPMI 比 Java 控制台小程序更好用。我可以复制和粘贴。而且,我可以在任何现代操作系统的命令行中使用 IPMI 控制台。

当你进入 BMC 设置时,找到启动 SOL 的选项。它会启动一个小程序,连接到主机的 SOL 接口,帮助你测试串口控制台配置。

UEFI/BIOS 串口控制台配置

一旦 BMC 准备好,你必须配置服务器硬件,将串口指向 BMC。进入硬件的设置菜单,在那里配置 UEFI 或 BIOS。在那个复杂的小选项迷宫中,你会找到类似“串口控制台重定向”这样的选项。

这里的一个重要问题是,你的主机有多少个串口?可能没有,也可能有好几个。你可以选择重定向其中一个串口,或者添加一个额外的虚拟端口。我建议你保持现有的串口不动,添加一个专门用于 SOL 的虚拟端口。它可能被称为“SOL 控制台重定向”。启用它,然后进入该端口的设置。

这里有一些我认为对 FreeBSD 和 SOL 有帮助的设置:

终端类型 vt100

数据位 8

奇偶校验

停止位 1

流控制

关键部分是波特率、速度或每秒比特数设置。保持默认速度,但记下它。你将需要这个速度来进行连接。

现在你有了串口控制台,设置 FreeBSD。

配置 FreeBSD 的串口控制台

当 FreeBSD 启动时,加载程序决定在哪里打印控制台消息以及从哪里接受输入。虽然默认是从显示器和键盘,但通过一些调整,你可以将控制台重定向到串口。串口控制台不会提供 BIOS 访问权限,但你几乎可以以任何方式调整 FreeBSD 启动过程。你可以在第一阶段或第二阶段引导加载器中配置串口控制台。

第一级引导加载程序能让你在最早的时候访问控制台,但它要求你使用第一个串口作为控制台。更改端口需要重新编译内核。第一级引导加载程序允许你执行像选择从哪个磁盘加载第二级引导加载程序的任务——本质上是从与 BIOS 或 UEFI 选择的磁盘不同的磁盘引导。这无疑很有用,但很少有用户需要这样做。

第二阶段的引导加载器可以使用任何串口作为控制台,但你首先看到的输出将是《引导提示》中讨论的引导菜单,位于第 55 页。对我们大多数人来说,这完全可以接受。

控制台选项

FreeBSD 的默认配置使用显示器和键盘作为控制台。你可以选择切换到仅串口控制台,或者使用双控制台。通过/boot/loader.conf选项console来选择其中之一。

仅使用串口的控制台可以防止一些随机的共同工作者对你的设备进行断电重启、插入显示器并修改菜单。是的,他们仍然可以通过第一阶段加载器制造混乱,或者从 USB 启动,但这需要更高的技能。将console变量设置为comconsole,以仅使用串口作为控制台。

console="comconsole"

对于大多数部署,我推荐使用双控制台。双控制台会同时在串口和显示器上显示控制台活动。你可以根据需要使用标准控制台或串口控制台。通过列出comconsolevidconsole来指定双控制台配置。

console="comconsole vidconsole"

如果你处在服务器机房环境中,可能会需要在标准控制台和串口控制台之间切换。我通常通过串口控制台管理大量的 FreeBSD 系统,但如果发生问题,我会保留视频控制台以备不时之需。

控制台在重新启动后才会生效。你可以通过查看启动消息来确认 FreeBSD 是否将其控制台放置在串口上。

uart0: <16550 or compatible> port 0x3f8-0x3ff irq 4 flags 0x10 on acpi0
uart0: console (9600,n,8,1)

第二行显示串口 uart0 被配置为控制台,使用默认设置。我们将在《使用串口控制台》中查看这些设置,位于第 79 页。

高级控制台选项

除了启用控制台外,你还可以调整控制台的端口和速度。

也许我需要使用第二个串口作为控制台。也许第一个串口已经插入了某些设备,或者第二个端口是虚拟 SOL 端口。串口使用 uart(4)设备驱动程序。记住,FreeBSD 设备的编号从零开始,而 COM 端口的编号从 1 开始。COM1 是 uart0,COM2 是 uart1,以此类推。你需要知道端口的基础 I/O 端口,这可以从系统启动消息中获得。

# grep uart /var/run/dmesg.boot
uart0: <16550 or compatible> port ➊0x3f8-0x3ff irq 4 flags 0x10 on acpi0
uart1: <16550 or compatible> port ➋0x2f8-0x2ff irq 3 on acpi0

单词 port 后面的第一个数字是基础 I/O 端口➊。COM2(或 uart1)的基础地址是 0x2f8。将comconsole_port设置为这个值➋。

comconsole_port="0x2f8"

你的控制台现在位于串口 COM2。

如果我的串行连接无法达到 9600 波特率,我可以通过comconsole_speed选项来更改端口速度。

comconsole_speed="115200"

在物理端口上,不要仅仅因为可以就增加端口速度。

使用串口控制台

现在你已经完成了物理和软件的设置,接下来配置你的客户端来访问串口控制台。使用串口控制台的关键是记住以下设置:

  • 速度(9600 波特,或者你硬件设置的其他值)

  • 8 位

  • 无奇偶校验

  • 1 停止位

访问串行线路的方式取决于它是物理线路还是 IPMI SOL 连接。

物理串行线路

将你的客户端连接到串行线路的另一端。你可以在 Microsoft 平台(PuTTY 是最著名的)、macOS 以及几乎任何其他操作系统上找到终端仿真器。曾几何时,我使用过一台 Palm 掌中宝手持设备,通过串行电缆访问串行控制台。输入正确的设置值到终端仿真器中,串行控制台就会“正常工作”。

FreeBSD 通过 tip(1)访问串行线路,这是一款让你像 telnet 一样连接到远程系统的程序。要运行 tip,作为 root 用户执行以下操作:

# tip portname

端口名称是指定要在串行端口上使用的串行端口号和速度的简写。文件/etc/remote包含了端口名称的列表。该文件中的大多数条目都是 UUCP(Unix-to-Unix 复制协议)曾是主要数据传输协议、串行线路曾是常见连接方式的时代遗留下来的遗物。^(7) 在文件的末尾,你会看到几个类似的条目:

# Finger friendly shortcuts
uart0|com1:dv=/dev/cuau0:br#9600:pa=none:
uart1|com2:dv=/dev/cuau1:br#9600:pa=none:
--snip--

uart条目是标准的 Unix 类型设备名称,而com名称是为了方便那些使用过 x86 硬件的人而添加的。

假设你有两台 FreeBSD 机器,背靠背连接,每台机器的串行端口 1 通过 null-modem 电缆连接到串行端口 2。两台机器都配置为使用串行控制台。你将需要连接到本地的串行端口 2,以与另一台系统的串行控制台通信:

# tip uart1
connected

成功连接!

要断开串行控制台,随时按 ENTER 键,然后输入断开序列“波浪符点”。

~.

你将被优雅地断开连接。(这也适用于 OpenSSH 客户端。)

tip(1)程序使用波浪符号(~)作为控制字符。请阅读 man 页面,查看你可以用它做的所有事情。

IPMI SOL 连接

你需要一个 SOL 客户端来连接到你的 IPMI 串行端口。测试你的配置的最快方法可能是使用 BMC 中包含的 SOL 客户端 Applet。尽管该客户端与控制台 Applet 有许多相同的缺点,但它是一个很好的测试场所。如果 BMC 的 SOL 客户端无法工作,请检查你的 SOL 设置和 FreeBSD 配置。验证 SOL 客户端是否设置为使用与你在硬件和 FreeBSD 中设置的相同速度。如果它不起作用,但所有设置看起来匹配,请重启 BMC。一旦它工作,你就可以从另一个主机使用 SOL 了。

标准的 IPMI SOL 客户端是 IPMItool(sourceforge.net/projects/ipmitool/),作为 ipmitool 软件包提供。(第十五章讨论了软件包。)IPMItool 可以通过网络与 BMC 交互,提供所有 BMC 功能,无需登录到笨重的 Web 界面。你可以重新启动主机、检查硬件报警和传感器等,所有这些都可以通过命令行完成。但目前,我们将继续使用 SOL 控制台。使用 BMC 的主机名或 IP 地址、用户名和密码登录到 SOL。

# ipmitool -H bmc -U username -I lanplus sol activate

在这里,我以用户名“bert”登录到我的 Web 服务器的 BMC,主机名为 www-bmc。

# ipmitool -H www-kvm -U bert -I lanplus sol activate

在提示符下输入密码,SOL 会确认你的登录。

[SOL Session operational.  Use ~? for help]

我们有控制台。应该有吧。让我们做最后的测试。

在控制台工作

串行控制台的真正考验在于你是否能够通过它传输数据。一旦你连接了控制台,按下 ENTER 键。

FreeBSD/amd64 (www) (ttyu2)

login:

FreeBSD 默认允许通过串行控制台登录。登录主机并重启,它会显示通常的控制台信息。

Jul 13 11:48:24 Stopping cron.
Stopping sshd.
Stopping devd.
Writing entropy file:.
Writing early boot entropy file:.
Terminated
.
Jul 13 11:48:24 zfs1 syslogd: exiting on signal 15
Waiting (max 60 seconds) for system process `vnlru' to stop... done
Waiting (max 60 seconds) for system process `bufdaemon' to stop... done
Waiting (max 60 seconds) for system process `syncer' to stop...
Syncing disks, vnodes remaining... 0 0 0 done
All buffers synced.

系统运行 BIOS 程序并将控制权交给串行控制台时,会有一段较长的暂停。就在你决定机器再也不会启动时,你会看到引导菜单。恭喜!你正在使用串行控制台。按空格键中断启动,就像你在键盘前一样。

系统距离有多远并不重要;你可以更改启动内核,获得详细的启动信息,在单用户模式下启动,或者手动运行 fsck 检查硬盘——都可以。软件串行控制台可能无法显示 BIOS,但很有可能它已经正确设置。使用串行控制台一段时间后,机器是否在地球的另一端还是房间的另一边都不再重要;仅仅是为了访问控制台而离开座椅会显得太麻烦。

如果远程位置的系统完全锁死,你可以连接到串行控制台,并让机房的“远程操作人员”重启系统。这对你的计算机可能不好,但系统被锁死也不好。通过串行控制台,你可以进入单用户模式,并通过查看日志和进行其他故障排除来修复问题。我们将在第二十四章讨论这种问题的故障排除。

现在你已经理解了 FreeBSD 是如何启动和关闭的,让我们来看一些基本工具,确保即使你在实验过程中,系统仍然能继续运行。

第五章:在您打破其他东西之前,请阅读此内容!(备份和恢复)**

image

系统故障最常见的原因是那些烦人的人类,但硬件和操作系统也会出现故障。黑客不断学习新方法来破坏网络和渗透应用程序,您不可避免地需要定期升级和修补您的系统。(您是否升级和修补是另一个完全不同的问题。)每次操作系统时,您都有可能犯错、配置错误至关重要的服务,或彻底毁坏系统。想想看,每次修补任何操作系统的计算机后,总会发现某些地方异常!即使是小的系统变动也可能损坏数据。因此,您应该始终假设最坏的情况即将发生。在我们的情况下,这意味着如果硬件或人为因素破坏了硬盘上的数据,您必须能够恢复这些数据。

我们将从使用 tar(1) 进行系统备份和管理磁带驱动器开始,然后回顾如何使用 script(1) 记录系统行为。最后,如果您遭遇部分或几乎完全的灾难,我们将考虑如何通过单用户模式和安装介质恢复和重建系统。

系统备份

只有在您关心数据时,才需要进行系统备份。这并不像听起来那么无意义。真正的问题是,“如果我的数据丢失,替代它的成本是多少?”低端磁带备份系统可能只需几百美元。您的时间值多少钱,恢复系统所需的时间有多久?如果硬盘上最重要的数据是您浏览器的书签文件,那么备份系统可能不值得投资。但如果您的服务器是公司骨干,您就需要非常认真地对待这一投资。

在线备份很容易被破坏或摧毁,就像破坏正在运行的服务器一样。正确的备份应该安全地离线存储。像 rsync(1) 这样的工具,甚至是 ZFS 复制,都不创建实际的备份;它们只是创建方便的在线副本。

完整的备份和恢复操作需要磁带驱动器和介质。您还可以将数据备份到文件、通过网络或到可移动介质(如 CD 或 DVD)。许多人使用通过 USB 3 连接的可移动多 terabyte 硬盘进行备份。尽管我们尽力而为,但磁带仍然是许多环境中的重要媒介。

备份磁带

FreeBSD 支持 SCSI 和 USB 磁带驱动器。SCSI 驱动器是最快且最可靠的。USB 磁带驱动器并不总是符合标准,因此并非总是与 FreeBSD 兼容。务必检查发布说明或 FreeBSD 邮件列表存档,以确认您的磁带驱动器是否与 FreeBSD 兼容。

一旦你物理安装了磁带驱动器,需要确认 FreeBSD 是否识别它。最简单的方法是检查 /var/run/dmesg.boot 文件中是否有 sa 设备(参见 第四章)。例如,以下三行来自 dmesg.boot,描述了此机器中的 SCSI 磁带设备:

➊sa0 at mps0 bus 0 ➋scbus0 ➌target 3 lun 0
sa0: ➍<QUANTUM ULTRIUM 5 3210> Removable Sequential Access SPC-4 SCSI device
sa0: Serial Number HU1313V6JA
sa0: ➎600.000MB/s transfers
sa0: Command Queueing enabled

在所有关于这个磁带驱动器的信息中,最重要的是你的 FreeBSD 系统将这个设备识别为 sa0 ➊。我们还看到它连接到 SCSI 卡 mps0 ➋,SCSI ID 为 3 ➌,并且我们看到驱动器的型号 ➍,以及它能够以每秒 600MB 的速度运行 ➎。

磁带驱动器设备节点、倒带和弹出

磁带是一种线性存储介质。每一段磁带存储一部分数据。如果你将多个数据块备份到磁带,避免在每次备份操作后倒带。假设你将一个系统的备份写入磁带,倒带磁带后再备份另一个系统,第二次备份会覆盖第一次备份,因为它使用了相同的磁带段。当你在单个磁带上进行多个备份时,使用合适的设备节点以确保任务之间不会倒带。

与许多有着数十年历史的 Unix 设备一样,访问磁带驱动器的方式决定了它的行为。磁带驱动器有多个不同的设备节点,每个节点让磁带驱动器表现不同。最基本的磁带控制机制是用来访问它的设备节点。普通磁带驱动器有三个节点:/dev/esa0/dev/nsa0/dev/sa0

磁带是顺序访问设备,要访问磁带上某一特定段的数据,必须倒带磁带以暴露该段。是否倒带是一个重要的问题。

注意

不同磁带设备节点的行为在操作系统之间有所不同。不同版本的 Unix,以及不同的磁带管理软件,会以不同的方式处理磁带。不要对你的备份磁带做假设!

如果你使用与设备名称匹配的节点名称,当命令完成时,磁带驱动器将自动倒带。我们的示例 SCSI 磁带驱动器的设备名称为 sa0,因此如果你使用 /dev/sa0 作为设备节点运行命令,命令完成后磁带将倒带。

如果你不希望命令完成时磁带自动倒带,可以使用以 n 开头的节点名称来阻止倒带。也许你需要将来自另一台机器的第二个备份附加到磁带上,或者你想在倒带和弹出之前对磁带进行编目。在我们的示例中,使用 /dev/nsa0 来运行命令而不进行倒带。

要在命令完成时自动弹出磁带,可以使用以 e 开头的节点。例如,如果你正在运行完整系统备份,可能希望命令完成时自动弹出磁带,这样操作员就可以将磁带放入盒子中送往外部存储或存放起来。我们的示例使用 /dev/esa0 设备名称,在命令完成时弹出磁带。一些磁带驱动器可能不支持自动弹出;它们需要你按下物理按钮,操作杠杆将磁带从驱动器中拉出。识别这种驱动器的最简单方法是尝试通过设备节点弹出它,然后看看发生了什么。

$TAPE 变量

许多程序假设你的磁带驱动器是 /dev/sa0,但这并不总是正确的。即使你只有一个磁带驱动器,你也可能希望在命令完成时自动弹出磁带(/dev/esa0),或者在完成时不自动倒带(/dev/nsa0)。

许多(但不是所有)与备份相关的程序使用环境变量 $TAPE 来控制它们默认使用的设备节点。你始终可以在命令行上覆盖 $TAPE,但将其设置为你最常用的选项可以节省你以后的一些麻烦。

使用 mt(1) 检查磁带状态

现在你知道如何找到你的磁带驱动器后,你可以使用 mt(1) 执行基本操作——如倒带、重张、擦除等等。mt(1) 做的一个基本操作是检查磁带驱动器的状态,具体如下:

# mt status
Mode      Density              Blocksize      bpi      Compression
Current: ➊0x25:DDS-3          variable       97000   ➋DCLZ
---------available modes---------
0:        0x25:DDS-3           variable       97000    DCLZ
1:        0x25:DDS-3           variable       97000    DCLZ
2:        0x25:DDS-3           variable       97000    DCLZ
3:        0x25:DDS-3           variable       97000    DCLZ
---------------------------------
             ➌ Current Driver State: at rest.
---------------------------------
File Number: 0  Record Number: 0          Residual Count 0

你不必担心这里的大多数信息,但如果你想逐行查看,mt(1) 手册页面包含了所有功能的良好描述。至少,如果命令返回了任何有用的信息,意味着 mt(1) 能找到你的磁带驱动器。

我们首先看到的是驱动器密度 ➊。旧的驱动器可以有不同密度的磁带,用于不同的目的,但现代磁带驱动器尽可能紧密地压缩数据。这个特定的磁带驱动器是 DDS-3 型号;虽然你可以选择使用其他密度,但它提供的所有选择都是 DDS-3。我们还看到这个磁带驱动器提供了 DCLZ 算法的硬件压缩 ➋。在底部附近,我们看到磁带驱动器现在正在做什么 ➌。

status 命令可能会给你不同种类的信息。最有问题的就是那个告诉你磁带驱动器未配置的消息:

#mt status
mt: /dev/nsa0: Device not configured

这意味着你在 $TAPE 变量指向的设备节点上实际上没有磁带。你可以通过使用 -f 标志来指定设备节点(例如,mt -f /dev/nsa1 status)来实验设备节点和 mt(1),尽管你应该从 dmesg.boot 获取正确的信息。如果你确定你的设备节点是正确的,也许是因为驱动器中没有插入磁带,或者磁带驱动器需要清洁。

mt status返回的另一个响应是mt: /dev/nsa0: 设备忙碌。你请求了磁带的状态,磁带回应:“我现在忙,不能接听。”稍后再试,或者使用ps -ax查看哪些命令正在使用磁带驱动器。当你在操作实际的磁带时,只有一个程序实例能够同时访问它。你无法在从磁带提取文件时列出磁带内容。

其他磁带驱动器命令

你可以用磁带驱动器做的不仅仅是检查它是否正常工作。我最常用的 mt(1)子命令是retensioneraserewindoffline

磁带有伸展的倾向,尤其是在第一次使用后。(我非常清楚,现代的磁带供应商都声称他们的磁带经过预拉伸,或者说他们的磁带无法被拉伸,但这个说法和两片面包就能做成一个博洛尼亚三明治。)重新张紧磁带只是将磁带完全前后地通过一次,命令是mt retension。重新张紧会去掉磁带中的松弛部分,使备份更加可靠。

擦除会从磁带上移除所有数据。这不是一个完全可靠的擦除方法,如果你需要隐藏数据以防止数据恢复公司或税务局查看,mt erase只是简单地在磁带上滚动并覆盖所有内容一次。这可能需要很长时间。如果你想快速擦除磁带,可以使用mt erase 0将磁带标记为空白。

mt rewind命令将磁带倒带到起始位置,和通过默认设备节点访问设备一样。

当你脱机磁带时,你需要倒带并弹出磁带,这样才能插入新的磁带。奇怪的是,命令是mt offline

现在,让我们获取一些关于这张磁带的数据。

磁带驱动器的特性

并非所有的磁带驱动器都支持所有功能。特别是老旧的磁带驱动器非常挑剔,甚至有些脾气不好,要求非常特定的设置才能正常工作。如果你遇到特定驱动器的问题,可以查看FreeBSD-questions邮件列表归档,看看是否有其他人遇到过相同的问题。你很可能在那里找到答案。

BSD tar(1)

用于将系统备份到磁带的最流行工具是 tar(1)。Tar是“磁带归档程序”的缩写——它是专门为备份而编写的。FreeBSD 也包含了 dump(8),但它仅适用于不使用软更新日志的 UFS 文件系统。你还会遇到其他备份工具,如 pax 和 cpio,以及基于网络的备份工具,如 Amanda、Bacula 和 Tarsnap。这些工具适合某些特定环境,但不如 tar 那样通用。Tar 是几乎所有操作系统供应商都认可的常见标准;你可以在 Windows、Linux、Unix、BSD、macOS、AS/400、VMS、Atari、Commodore 64、QNX 等几乎所有其他平台上找到 tar。

你可以使用 tar(1)将文件备份到磁带或文件中。包含 tar 文件的备份文件称为tarball。从 tarball 中恢复单个文件或一部分文件非常快速且简单。从磁带中恢复备份的一部分也很容易,但速度远不如直接恢复。

FreeBSD 使用的 tar 版本叫做bsdtar。bsdtar 可以完全一致地与 GNU tar 配合使用,也可以严格遵循 POSIX tar 的行为。如果你对 GNU tar、POSIX tar 和 bsdtar 之间的差异有任何疑虑,阅读 tar(1)以了解所有详细信息。bsdtar 基于 libarchive(3)构建,这是一个专门用于创建和提取备份归档的库。得益于 libarchive,bsdtar 可以从传统的磁带备份到 ISO 镜像提取文件,所有操作都有相同的界面。如果你需要打开 RPM、zip 文件或几乎任何其他归档文件,bsdtar 都是你的朋友。

像其他任何 tar(1)一样,bsdtar 也可以很“傻”。如果你的文件系统以任何方式损坏,bsdtar 会备份它认为你请求的内容。然后,它会愉快地恢复在原始备份过程中损坏的文件,将正在工作但不正确的文件覆盖为无法正常工作的错误版本。虽然这种问题很少发生,但一旦发生,往往令人难以忘怀。

文件系统一致性

无论你使用什么备份软件,在你尝试备份文件时,文件可能会发生变化。日志文件会不断地在末尾添加内容,而数据库可能在文件的任何地方发生变化。文件系统快照始终是一致的,UFS(第十一章)和 ZFS(第十二章)都支持这种功能。绝不要备份正在运行的数据库;应该将数据库转储到归档文件中,然后备份该归档文件。

tar 模式

tar 可以执行几种不同的操作,这些操作由命令行标志控制。这些不同的操作被称为模式。你需要阅读 man 页面,了解所有 tar 模式的完整描述,但以下列表列出了最常用的几种。

创建归档

使用创建模式-c)来创建一个新的归档。除非另有指定,否则此标志将把所有内容备份到你的磁带驱动器($TAPE,如果没有设置$TAPE,则为/dev/sa0)。要备份整个系统,你需要告诉 tar 从根目录开始归档所有内容:

# tar -c /

作为回应,你的磁带驱动器应该亮起,如果你的磁带足够大,最终会呈现出一个完整的系统备份。然而,许多现代硬盘比磁带驱动器能够容纳的容量还要大,因此备份系统中仅重要的部分是更为合理的。例如,如果你电脑上唯一需要的文件都在/home/var目录下,你可以在命令行中指定这些目录:

# tar -c /home /var
列出归档内容

列出模式-t)列出归档中的所有文件。创建归档后,你可以使用此模式列出磁带的内容。

# tar -t
.
.snap
dev
tmp
--snip--

这个列表包括你备份中的所有文件,运行时可能需要一些时间。注意,文件名中的初始斜杠缺失;例如,/tmp 会显示为 tmp。这在恢复时变得很重要。

从备份中提取文件

提取模式下,tar 从归档中恢复文件并将其复制到磁盘上。(这也叫做解压。)tar 在你当前的位置提取文件;如果你想用备份中的文件覆盖现有的 /etc 目录,首先进入根目录。另一方面,如果我要在我的主目录中恢复一个 /etc 的副本,我会首先进入我的主目录。

# cd /home/mwlucas
# tar -x etc

记得我说过,缺少的初始斜杠很重要吗?这就是原因。如果备份中包含了那个初始斜杠,tar 将始终相对于根目录提取文件。恢复的 /etc/rc.conf 将始终写入到 /etc/rc.conf。没有前导的 /,你可以将文件恢复到你想要的任何位置;恢复后的 /etc/rc.conf 可以被恢复为 /home/mwlucas/etc/rc.conf。如果我正在从一台已退役的机器恢复文件,我不希望它们覆盖当前机器上的文件;我希望它们被放置在其他地方,以免干扰我的系统。

验证备份

一旦你有了备份,你可能想确认它是否与你的系统匹配。Diff 模式(-d)将磁带上的文件与磁盘上的文件进行比较。如果磁带上的一切与系统匹配,tar -d 将默默运行。然而,磁带和系统之间的完美匹配并是正常的情况。日志文件通常在备份过程中增长,因此磁带上的日志文件不应该与磁盘上的文件匹配。类似地,如果你有一个正在运行的数据库服务器,数据库文件可能不匹配。如果你真的想要一个完美的备份(也叫做冷备份),你需要在备份之前将系统切换到单用户模式。你必须决定你可以接受哪些错误,哪些错误需要修正。

其他 tar 特性

Tar 还有一些其他功能,可以使它更友好或更有用。这些功能包括详细模式、不同类型的压缩、权限恢复,以及最流行的选项——备用存储。

使用非默认存储

默认情况下,tar 会将所有内容传输到你的磁带驱动器,但 -f 标志允许你指定另一个设备或文件作为目标。在之前的所有示例中,我要么使用默认的磁带驱动器 /dev/sa0,要么设置了 $TAPE。如果我没有这两者中的任何一个,我需要使用 -f 来指定磁带驱动器:

# tar -c -f /dev/east0 /

你还可以将备份保存到文件(或 tar 包)中,而不是使用磁带。通过互联网分发的源代码通常以 tar 包的形式分发。使用相同的 -f 标志来指定文件名。例如,为了备份这本书写作时的各个章节,我每隔一段时间运行以下命令来创建 tar 包 bookbackup.tar

#tar -cf bookbackup.tar /home/mwlucas/af3e/

这个文件可以轻松地在其他地方的机器上进行备份——即使我的房子着火了,书籍也会安全。我可以接通电话和电力线到邻居家,借一台笔记本电脑,找到一个开放的无线接入点,运行 tar -xf bookbackup.tar,在焦黑的木材中工作,等待保险公司处理。 (反正那时我也做不了别的事。)

详细模式

除非遇到错误,否则 tar 通常静默运行。大多数时候这是好的(谁想在每次备份时查看服务器上完整的文件列表?),但有时你会想看到程序执行的过程,获得一种温暖的满足感。添加 -v 标志会使 tar 打印它处理的每个文件的名称。你可以使用详细模式标志来创建所有正在备份或恢复的文件的完整列表。在常规的备份或恢复中,这种冗余信息会使错误变得难以察觉。

压缩

Bsdtar 继承了 libarchive(3) 所理解的所有压缩算法的支持。我们将介绍一些你可能用来创建归档的压缩算法,按从最理想到最不推荐的顺序排列。Bsdtar 支持更多的压缩算法,但通常情况下你不会使用它们来创建归档。

XZ 压缩

XZ 压缩算法是当前的新热点。使用-J启用它。非 FreeBSD 主机可能需要通过 xz(1) 管道恢复数据以进行读取。使用 XZ 压缩的 Tarball 通常以 .txz 结尾。

bzip 压缩

FreeBSD 的 tar 支持 bzip 压缩,它比 gzip 压缩得更紧凑,使用 -j 标志。Bzip 使用的 CPU 时间比 gzip 多,但如今,CPU 时间不像 gzip 出现时那样有限。并不是所有版本的 tar 都支持 bzip 压缩。如果你只会在 FreeBSD 机器上读取文件,或者你愿意在其他平台上安装 bzip,可以使用 -j 标志。大多数使用 bzip(1) 压缩的 tarball 以 .tbz 结尾。

gzip 压缩

gzip 标志(-z)会在归档的过程中将文件通过 gzip(1) 压缩程序进行处理。压缩后的 tarball 通常以 .tar.gz.tgz 或在少数情况下以 .taz 结尾。压缩可以大大减小归档的大小;许多备份在压缩后减小了 50% 或更多。虽然所有现代版本的 tar 都支持 gzip,但旧版本不支持,因此,如果你希望每个人都能读取你的备份,最好不要使用 -z

原始 Unix 压缩

相比之下,所有 Unix 版本的 tar 都可以使用 -Z 标志通过 compress(1) 压缩文件。compress 程序的效率不如 gzip,但它确实能够减小文件大小。你可能会遇到的每个 tar 实现都支持 compress(1)。使用 -Z 压缩的 tarball 后缀为 .tar.Z

权限恢复

-p标志会恢复提取文件的原始权限。默认情况下,tar 会将提取文件的所有者设置为提取文件的用户名。对于源代码来说这没问题,但在进行系统恢复时,你真的希望恢复文件的原始权限。(有时间手动恢复这些权限试试看;你会学到不少关于为什么第一次就应该做对的事情。)

压缩与 FreeBSD TAR

FreeBSD 的 libarchive 会自动检测备份中使用的压缩类型。在创建档案时,你必须指定所需的压缩方式,但提取时不需要提供压缩算法。让 tar(1)来决定压缩类型,它会自动执行正确的操作,即使档案是使用你从未见过的算法进行压缩的。

还有更多,更多,更多……

Tar 有许多更多的功能,以适应备份、文件、文件系统和磁盘几十年来的变化。欲了解完整的功能列表,请阅读 man tar(1)。

记录发生的事情

现在你可以备份整个系统并跟踪单个文件中的变化。剩下的就是跟踪屏幕前发生的事情。一个很少被提及但非常有用的工具是每个系统管理员都应该知道的script(1)。它记录你输入的所有内容以及屏幕上显示的所有内容。你可以记录错误和日志输出,供以后分析和剖析。例如,如果你正在运行一个每次都会在同一位置失败的程序,你可以使用script来复制你的按键和程序的响应。这在升级系统或从源代码构建软件时特别有用;日志文件的最后 30 行左右是请求帮助时的好材料。

启动script(1)只需输入script。你将会得到命令提示符,可以继续正常工作。当你想停止记录时,只需输入exit或按 CTRL-D。你的活动将会出现在名为typescript的文件中。如果你希望文件有特定的名称或位于特定位置,只需将该名称作为参数传递给script

# script /home/mwlucas/debug.txt

这个工具对于精确记录你输入的内容以及系统如何响应非常有用。每当你需要寻求帮助时,考虑使用script(1)

修复损坏的系统

学习操作系统的最佳方式是与它玩耍,你玩得越深入,学到的越多。如果你玩得足够狠,你肯定会弄坏某些东西,而这恰恰是好事——修复一个严重损坏的系统可以说是学习的最快方式。如果你刚刚让你的系统无法启动,或者计划快速学习到足够的程度以冒险这样做,那么这一节适合你。如果你的系统已经严重损坏,你会学到很多并且会很快掌握。

单用户模式(在第四章中讨论)为你提供了访问许多不同命令和工具的权限。然而,如果你破坏了这些工具怎么办?也许你甚至破坏了/rescue中的静态链接程序。这时,安装介质就派上用场了。

FreeBSD 安装镜像提供了一个激活 Live 系统的选项。这个 Live 系统包括了 FreeBSD 默认提供的所有程序。当你从安装介质启动时,你可以选择进入 Live CD,而不是进行安装。

使用 Live CD 你必须对系统管理有所了解。基本上,Live CD 为你提供了一个命令行提示符和各种 Unix 工具。你可以使用启动时的错误信息以及你脑海中的那部分“平衡物”来修复问题。这是你与计算机的较量。在我第一次使用 Live CD 或其前身时,计算机赢了三次。不过之后,我的成功率大大提高了。阅读本书以及其他 Unix 管理手册将提高你的成功几率。

为通用问题情境概括一个逐步的过程几乎是不可能的;你必须遵循的确切步骤取决于你对可怜的、无辜的操作系统造成的具体损害。不过,如果你真的很绝望,Live CD 可以让你在不重新安装的情况下有机会恢复。我曾经不小心破坏了/etc 目录,或者损坏了显示登录提示符的 getty(1) 程序。小心使用 Live CD 可以在重新安装所需的时间的一小部分内修复这些问题。如果什么都做不到,也许你可以备份任何幸存的数据,然后重新安装。

始终使用与你当前运行的 FreeBSD 版本大致相当的安装介质。你可能可以使用 12.2 安装 CD 来修复 12.1 系统,但 14-current 安装光盘可能会引发一系列新的问题。

现在你已经可以从几乎所有可能犯的错误中恢复过来了,我们可以深入 FreeBSD 的核心部分:内核。

第六章:KERNEL GAMES**

image

如果你是 Unix 管理的新手,内核这个词可能会让你感到害怕。毕竟,内核是计算机中的一些神秘部分,普通人不应该轻易触碰。在某些版本的 Unix 中,篡改内核是不可想象的。微软甚至不宣传它的操作系统有内核,这就像忽略了人类有大脑这一事实一样。^(1) 虽然高级用户可以通过各种方法访问内核,但这并不被广泛承认或鼓励。然而,在许多开源类 Unix 的世界中,干预内核是改变系统行为的一种非常可行且预期的方式。如果你被允许这样做,这可能也是调整其他操作系统的一个极好方式。

FreeBSD 的内核可以动态调节或随时更改,大多数系统性能的方面可以根据需要进行调整。我们将讨论内核的 sysctl 接口,以及如何使用它来更改正在运行的内核。

同时,内核的某些部分只能在系统启动的早期阶段进行修改。引导加载程序允许你在主机甚至没有找到其文件系统之前调整内核。

一些内核特性需要大量的重新配置。你可以为非常小的系统定制内核,或者为你正在运行的硬件精确调整一个内核。做到这一点的最佳方法是自己构建内核。

FreeBSD 有一个模块化的内核,这意味着可以将整个内核部分加载或卸载,从操作系统中打开或关闭整个子系统。这在如今可拆卸硬件时代非常有用,例如 PC 卡和 USB 设备。可加载的内核模块会影响性能、系统行为和硬件支持。

最后,我们将讨论如何调试你的内核,包括一些看起来可怕的错误信息,以及何时以及如何启动备用内核。

什么是内核?

你会听到很多关于内核的不同定义。许多定义完全令人困惑,有些技术上是正确的,但让初学者感到困惑,而其他的则是错误的。以下的定义并不完整,但对于大多数人来说,它足够简单易懂:内核是硬件和软件之间的接口

内核让软件可以将数据写入磁盘驱动器和网络。当一个程序需要内存时,内核负责处理访问物理内存芯片和为该任务分配资源的所有底层细节。一旦你的 MP3 文件通过编解码器软件,内核将编解码器输出转换为你的特定声卡能够理解的零和一的流。当一个程序请求 CPU 时间时,内核会为其安排一个时间槽。简而言之,内核提供了程序访问硬件资源所需的所有软件接口。

虽然内核的工作定义起来很简单(至少在这种简化的方式下),但实际上它是一项复杂的任务。不同的程序期望内核提供不同的硬件接口,而不同类型的硬件以不同的方式提供接口。例如,FreeBSD 支持几打种类的以太网卡,每种卡都有自己的要求,内核必须处理这些要求。如果内核无法与网卡通信,系统就无法联网。不同的程序请求以不同的方式安排内存,如果你有一个请求内核不支持的内存安排方式的程序,那你就倒霉了。内核在启动序列中如何检查某些硬件,决定了硬件的行为方式,所以你必须控制这一点。有些设备以友好的方式自我标识,而有些则在你询问它们的功能时会锁死。

内核和 FreeBSD 附带的任何模块都是 /boot/kernel 目录中的文件。第三方内核模块放在 /boot/modules 中。系统中的其他文件不是内核的一部分。非内核文件统称为 userland,意味着它们是为用户准备的,即使它们使用内核的功能。

由于内核只是文件集合,你可以为特殊情况准备备用内核。在你自己构建内核的系统中,你会找到 /boot/kernel.old,这是一个目录,包含了当前内核之前安装的内核。我习惯性地将与系统一起安装的内核复制到 /boot/kernel.install。你也可以创建自己的特殊内核。FreeBSD 团队使得配置和安装内核尽可能简单。改变内核最简单且最有支持的方式是通过 sysctl 接口。

内核状态:sysctl

sysctl(8) 程序允许你查看内核使用的值,并在某些情况下设置它们。更复杂的是,这些值有时也被称为 sysctl。sysctl 接口是一个强大的功能,因为在许多情况下,它可以让你解决性能问题,而无需重建内核或重新配置应用程序。不幸的是,这种能力也让你有可能将正在运行的程序“踢倒”,并让你的用户非常不高兴。

sysctl(8) 程序处理所有的 sysctl 操作。在本书中,我将指出特定的 sysctl 如何改变系统行为,但首先,你需要一般性地了解 sysctl。开始时,先抓取你系统中所有对人类可见的 sysctl,并将它们保存到一个文件中,以便你可以轻松地进行研究。

# sysctl -o -a > sysctl.out

文件 sysctl.out 现在包含了数百个 sysctl 变量及其值,其中大部分看起来毫无意义。然而,你可以理解其中一些,而无需了解太多内容:

kern.hostname: storm

这个特定的 sysctl,叫做kern.hostname,其值为storm。奇怪的是,我运行该命令的系统的主机名也是storm,而且该 sysctl 提示这是内核为其运行的系统指定的名称。使用-a标志查看这些 sysctl。大多数 sysctl 都应该以这种方式读取,但有一些被称为不透明 sysctl,只能由用户态程序解读。使用-o标志显示不透明的 sysctl。

net.local.stream.pcblist: Format:S,xunpcb Length:5488 Dump:0x20000000000000001
1000000dec0adde...

我可以猜测变量net.local.stream.pcblist代表了网络栈中的某些内容,但我甚至无法猜测其值的含义。像netstat(1)这样的用户态程序从这些不透明的 sysctl 中获取信息。

sysctl MIBs

这些 sysctl 以树状格式组织,称为管理信息库(MIB),包含多个大类,如 net(网络)、kern(内核)和 vm(虚拟内存)。表 6-1 列出了在运行 GENERIC 内核的系统上 sysctl MIB 树的根节点。

表 6-1: sysctl MIB 树的根节点

sysctl 功能
kern 核心内核功能和特性
vm 虚拟内存系统
vfs 文件系统
net 网络
debug 调试
hw 硬件
machdep 机器相关设置
user 用户态接口信息
p1003_1b POSIX 行为
kstat 内核统计信息
dev 设备特定信息
security 特定于安全的内核功能

这些类别每个还会进一步细分。例如,net类别,涵盖所有与网络相关的 sysctl,被分为 IP、ICMP、TCP 和 UDP 等子类别。管理信息库的概念在系统管理的其他部分也有应用,我们将在第二十一章看到,未来的职业生涯中你也会接触到。术语sysctl MIBsysctl通常可以互换使用。每个类别的命名是通过将父类别和所有子类别串联在一起,形成一个唯一的变量名,例如:

--snip--
kern.maxfilesperproc: 11095
kern.maxprocperuid: 5547
kern.ipc.maxsockbuf: 262144
kern.ipc.sockbuf_waste_factor: 8
kern.ipc.max_linkhdr: 16
--snip--

这里列出了五个 sysctl,来自kern类别的中间部分。前两个直接位于kern标签下,除了它们与内核相关这一点外,并没有与其他值有明显的分组关系。剩余的三个都以kern.ipc开头,它们属于内核 sysctl 的 IPC(进程间通信)部分。如果你继续阅读你保存的 sysctl,你会发现一些 sysctl 变量有多个类别层级。

sysctl 值和定义

每个 MIB 都有一个值,表示内核使用的缓冲区、设置或特性。更改该值会改变内核的操作方式。例如,内核负责传输和接收数据包,但默认情况下不会将数据包从一个接口发送到另一个接口。你可以更改一个 sysctl 以允许这种转发,从而将主机变成一个路由器。

每个 sysctl 值可以是字符串、整数、二进制值或不透明类型。字符串 是任意长度的自由格式文本;整数 是普通的整数;二进制 值可以是 0(关闭)或 1(开启);不透明类型 是机器代码片段,只有专门的程序才能解释。

许多 sysctl 值文档不全;没有一个单一的文档列出了所有可用的 sysctl MIB 及其功能。MIB 的文档通常出现在相应功能的 man 页面中,或者有时仅出现在源代码中。例如,kern.securelevel 的原始文档(在第九章讨论)在 security(7) 中。尽管近年来 sysctl 文档有所扩展,许多 MIB 仍然没有文档。

幸运的是,某些 MIB 有明显的含义。例如,正如我们在本章后面讨论的那样,如果你经常启动不同的内核,这是一个重要的 MIB:

kern.bootfile: /boot/kernel/kernel

如果你在调试一个问题,并且需要依次重启多个不同的内核,你可能会很容易忘记已经启动了哪个内核(不过,实际上这从未发生过在我身上)。因此,一个提醒可能会很有帮助。

了解一个 sysctl 的作用的一个简单方法是使用 -d 开关与完整的 MIB 结合使用。这会打印出该 sysctl 的简短描述:

# sysctl -d kern.maxfilesperproc
kern.maxfilesperproc: Maximum files allowed open per process

这个简短的定义告诉你,这个 sysctl 控制的正是你可能想象的内容。不幸的是,并不是所有的 sysctl 都提供 -d 参数的定义。虽然这个例子相对简单,但其他 MIB 可能更难以猜测。

查看 sysctl 设置

要查看 MIB 树中特定子树下所有可用的 MIB,可以使用 sysctl 命令并输入你想查看的树的名称。例如,要查看 kern 下的所有内容,输入以下命令:

# sysctl kern
kern.ostype: FreeBSD
kern.osrelease: 12.0-CURRENT
kern.osrevision: 199506
kern.version: FreeBSD 12.0-CURRENT #0 r322672: Fri Aug 18 16:31:34 EDT 2018
    root@storm:/usr/obj/usr/src/sys/GENERIC
--snip--

这个列表会持续很长时间。如果你刚开始熟悉 sysctl,你可能会用这个来查看可用项。要获取特定 sysctl 的确切值,请将完整的 MIB 名称作为参数传入:

# sysctl kern.securelevel
kern.securelevel: -1

MIB kern.securelevel 的整数值为 -1。我们将在第九章中讨论这个 sysctl 及其值的含义。

更改 sysctl 设置

有些 sysctl 是只读的。例如,看看硬件 MIB:

hw.model: Intel(R) Xeon(R) CPU E5-1620 v2 @ 3.70GHz

FreeBSD 项目尚未开发出通过软件设置将 Intel 硬件转换为 ARM64 硬件的技术,因此这个 sysctl 是只读的。如果你能够修改它,所有你会做的就是崩溃系统。FreeBSD 通过不允许你更改此值来保护你。尝试更改它不会造成任何损害,但你会收到警告。另一方面,考虑以下 MIB:

vfs.usermount: 0

这个 MIB 决定用户是否可以挂载可移动媒体,如 CDROM 和软盘驱动器,详见第十三章。更改此 MIB 不需要对内核进行大量调整或修改硬件;它只是一个内核内的权限设置。要更改此值,可以使用sysctl(8)命令、sysctl MIB、等号和期望的值:

# sysctl vfs.usermount=1
vfs.usermount: 0 -> 1

sysctl(8)程序会响应并显示 sysctl 名称、旧值和新值。这个 sysctl 现在已经更改。像这样可以动态调整的 sysctl 称为运行时可调 sysctl

自动设置 sysctl

一旦你调整了内核设置以满足个人需求,你可能希望这些设置在重启后仍然生效。可以使用文件/etc/sysctl.conf来实现这一点。在这个文件中列出你希望设置的每个 sysctl 及其期望值。例如,要在启动时设置vfs.usermount sysctl,可以在/etc/sysctl.conf中添加如下内容:

vfs.usermount=1

内核环境

内核是由引导加载程序启动的一个程序。引导加载程序可以将环境变量传递给内核,从而创建内核环境。内核环境也是一个 MIB 树,类似于 sysctl 树。很多(但并非所有)环境变量最终会被映射为只读的 sysctl。

查看内核环境

使用 kenv(8)查看内核环境。为它提供一个内核环境变量的名称,可以只查看该变量,或者不带参数运行它以查看整个树状结构。

# kenv
LINES="24"
acpi.oem="SUPERM"
acpi.revision="2"
acpi.rsdp="0x000f04a0"
acpi.rsdt="0x7dff3028"
--snip--

这些变量看起来很像加载器变量,因为它们就是加载器变量。它们通常与初始硬件探测相关。如果你的串口使用了不寻常的内存地址,内核需要在尝试探测之前知道这个信息。

这些环境设置也被称为启动时可调的 sysctl,或可调项,通常与低级硬件设置相关。例如,当内核首次探测硬盘时,它必须决定是否使用基于标识的标签或基于 GPT ID 的标签。这个决定必须在内核访问硬盘之前做出,而且在不重启机器的情况下无法改变。

内核环境变量只能通过加载程序设置。你可以在启动时手动进行更改,或在/boot/loader.conf中设置它们,以便在下次启动时生效(见第四章)。

就像sysctl.conf一样,在loader.conf中设置可调值也可能会导致系统崩溃。好消息是,这些值可以轻松恢复。

有太多可调项吗?

不要将只能在启动时设置的 sysctl 值、可以动态调整的 sysctl 值和已配置为在启动时自动调整但可以动态设置的 sysctl 值混淆。记住,启动时可调的 sysctl 涉及低级内核功能,而运行时可调的 sysctl 涉及更高级的功能。让 sysctl 在启动时自行调整仅仅是保存你工作的一个例子——它并不改变该 sysctl 所属的类别。

向设备驱动程序提供提示

你可以使用环境变量来告知设备驱动程序所需的设置。你将通过阅读驱动程序的手册页和其他文档来了解这些设置。此外,许多古老的硬件要求内核以非常特定的 IRQ 和内存值来访问它们。如果你足够老,记得插卡即“上天祈祷”的“硬件配置”软盘和专门的总线主卡插槽,你知道我在说什么,甚至今天你可能还会在你的硬件柜里找到这样的系统。(如果你太年轻了,给我们这些老人买一杯酒,听听我们的噩梦故事。^(2)) 你可以告诉 FreeBSD 探测你指定的任何 IRQ 或内存地址上的硬件,这在你有一张已知配置的卡,而那张改变配置的软盘已经腐烂了多年前时非常有用。

如果你真的很不幸运,可能会拥有一台内置软盘驱动器的机器。查看 /boot/device.hints,可以找到配置此硬件的条目:

hint.fdc.0.at="isa"
hint.➊fdc.➋0.➌port=➍"0x3F0"
hint.fdc.0.irq=➎"6"
hint.fdc.0.drq=➏"2"

这些条目都是给 fdc(4)设备驱动程序的提示 ➊。该条目用于 fdc 设备编号零 ➋。如果启用此设备,启动时内核将探测位于内存地址(或端口 ➌)0x3F0 ➍、IRQ 6 ➎ 和 DRQ 2 ➏的卡。如果它发现具有这些特征的设备,就会将其分配给 fdc(4)驱动程序。如果该设备不是软盘驱动器,你将看到有趣的崩溃。^(3)

测试启动时可调参数

所有这些提示和启动时可调的 sysctl 都可以在启动加载器中使用,并可以在 OK 提示符下交互设置,如第四章中所讨论。你可以在不编辑 loader.conf 的情况下测试设置,找到合适的值后,再将其永久修改到文件中。

启动时可调参数和 sysctl 让你调整内核的行为,但内核模块允许你向正在运行的内核添加功能。

内核模块

内核模块是内核的一部分,可以在需要时启动或加载,在不使用时卸载。内核模块可以在插入硬件时加载,并随硬件一起移除。这极大地扩展了系统的灵活性。而且,若将所有可能的功能都编译到内核中,内核的体积会非常庞大。使用模块后,你可以拥有一个更小、更高效的内核,只有在需要时才加载那些不常用的功能。

就像默认的内核文件保存在/boot/kernel/kernel一样,内核模块也保存在/boot/kernel/目录下。查看该目录,您会看到数百个内核模块文件。每个内核模块的名称以.ko结尾。通常情况下,文件的名称与模块所包含的功能相对应。例如,文件/boot/kernel/wlan.ko处理 wlan(4)无线层。FreeBSD 需要这个模块来支持无线网络。

查看已加载的模块

kldstat(8)命令显示已加载到内核中的模块。

   # kldstat
   Id Refs Address           Size     Name
➊ 1   36 0xffffffff80200000 204c3e0  kernel
➋ 2    1 0xffffffff8224e000 3c14f0   zfs.ko
➌ 3    2 0xffffffff82610000 d5f8     opensolaris.ko
➍ 5    1 0xffffffff82821000 ac15     linprocfs.ko
   --snip--

这个桌面系统上已经加载了三个内核模块。第一个是内核本身➊;接着是支持 ZFS 的模块➋,以及 ZFS 所需的 OpenSolaris 内核功能模块➌。由于我在这个主机上试验 Linux 软件(参见第十七章),所以看到加载了 linprocfs(5)模块➍并不意外。

每个模块都包含一个或多个子模块,您可以通过使用kldstat -v来查看它们,但内核本身有数百个子模块——因此准备好面对大量输出。

加载和卸载模块

加载和卸载内核模块可以使用 kldload(8)和 kldunload(8)命令。例如,假设我在测试主机上实验 IPMI 功能。这需要 ipmi(4)内核模块。虽然我通常会使用loader.conf在启动时自动加载该模块,但我现在在实验室中。我使用kldload命令和包含该功能的内核模块或文件的名称:

# kldload /boot/kernel/ipmi.ko

如果我恰好记得模块的名称,我可以直接使用该名称。模块名称不需要后缀.ko。我恰好记得 IPMI 模块的名称。

# kldload ipmi

大多数情况下,我那颗脆弱的脑袋依赖于 shell 中的 tab 补全功能来提醒我模块的完整和正确名称。

实验完成后,我会卸载该模块。^(4)指定在 kldstat(8)中显示的内核模块名称。

# kldunload ipmi

任何正在使用中的模块,如每当使用 ZFS 时加载的opensolaris.ko模块,都不允许卸载。尝试卸载一个正在使用中的模块时,您将遇到类似下面的错误:

# kldunload opensolaris
kldunload: can't unload file: Device busy

系统管理员加载模块的频率远远高于卸载模块。卸载模块通常能正常工作,并且大多数情况下都能成功,但它也是导致系统崩溃最常见的方式之一。如果卸载模块时触发了崩溃,请按照第二十四章的指引提交错误报告。

启动时加载模块

使用/boot/loader.conf文件在启动时加载模块。默认的loader.conf包含了许多加载内核模块的示例,但语法始终是相同的。获取内核模块的名称,去掉后缀.ko,然后添加字符串_load="YES"。例如,要在启动时自动加载模块/boot/kernel/procfs.ko,可以在loader.conf中添加以下内容:

procfs_load="YES"

当然,最难的部分是知道加载哪个模块。简单的模块是设备驱动程序;如果你安装了一个新的网络卡或 SCSI 卡,而你的内核不支持它,你可以加载驱动程序模块,而不必重新配置内核。在这种情况下,你需要找出哪个驱动程序支持你的卡;man 页和 Google 会帮助你解决这个问题。在本书中,我会为解决特定问题提供具体的内核模块指引。

等一下——为什么 FreeBSD 需要你加载一个设备驱动来识别硬件,而它在启动时几乎可以识别所有硬件?这是个很好的问题!答案是,你可能已经构建了自己的定制内核,并移除了对你不使用的硬件的支持。你不知道如何构建内核?好吧,让我们现在就解决这个问题。

构建你自己的内核

最终,你会发现,仅仅使用 sysctl(8)和模块,你无法像希望的那样调整内核,你唯一的解决方法就是构建一个定制内核。这听起来比实际操作要难;我们说的并不是写代码——只是编辑一个文本文件并运行几个命令。如果你按照流程操作,它是完全安全的。如果你按照流程操作,那么,就像在错误的车道上开车一样。(市中心。高峰时段。)不过,从一个坏的内核恢复过来也并不像想象中那么糟糕。

默认安装中附带的内核称为GENERIC。GENERIC 被配置为在各种硬件上运行,尽管不一定是最优的。GENERIC 可以在过去 15 年左右的大多数硬件上顺利启动,我在生产环境中经常使用它。当你定制内核时,可以为特定硬件添加支持,移除不需要的硬件支持,或者启用 GENERIC 中未包含的功能。

不要重建内核

曾几何时,构建内核被视为一种成长的仪式。但现在已经不是这种情况了。大多数系统管理员只在玩实验性特性或专用硬件时才需要重建内核。

准备工作

在构建内核之前,必须拥有内核源代码。如果你按照我在第三章中的建议操作,那么你已经准备好了。如果没有,你可以重新进入安装程序并加载内核源代码,或者从 FreeBSD 镜像站下载源代码,或者跳到第十八章使用 svnlite(1)。如果你不记得是否安装了源代码,可以查看你的/usr/src目录。如果其中包含一堆文件和目录,那么你已经有了内核源代码。

在构建新内核之前,你必须了解系统的硬件配置。这可能是一个棘手的问题;组件上的品牌名称不一定能描述设备的身份或功能。许多公司使用重新品牌化的通用组件——我记得有一家厂商发布了四款不同的网卡,但都使用了相同的型号名称,前三款甚至没有标注版本号。唯一区分它们的方法就是不断尝试不同的设备驱动程序,直到找到一个有效的。这个问题已经存在了几十年——许多不同的公司都生产 NE2000 兼容的网卡。盒子外面有厂商的名字,但卡片上的电路却标注了 NE2000。幸运的是,一些厂商为其驱动程序和硬件使用标准架构;你可以相当确定英特尔的网卡会被英特尔的设备驱动程序识别。

查看 FreeBSD 系统上硬件信息的最佳位置是文件 /var/run/dmesg.boot,在第四章中有讨论。每一项条目代表内核中的硬件或软件特性。在为系统构建新内核时,随时保留该系统的 dmesg.boot 非常重要。

总线和连接

计算机中的每个设备都连接到其他某个设备。如果你仔细阅读 dmesg.boot,你可以看到这些连接链条。以下是编辑后的启动消息集,来做个演示:

➊ acpi0: <SUPERM SMCI--MB> on motherboard
➋ acpi0: Power Button (fixed)
➌ cpu0: <ACPI CPU> on acpi0
   cpu1: <ACPI CPU> on acpi0
➍ attimer0: <AT timer> port 0x40-0x43 irq 0 on acpi0
➎ pcib0: <ACPI Host-PCI bridge> port 0xcf8-0xcff on acpi0
➏ pci0: <ACPI PCI bus> on pcib0

这个系统上的第一个设备是 acpi0 ➊。你可能不知道它是什么,但你可以随时阅读 man acpi 来了解。(或者,如果你非得这么做,可以读完整章。) 在 acpi0 设备上有一个电源按钮 ➋。CPU ➌ 也连接到 acpi0 设备,还有一个计时设备 ➍。最终,我们找到了第一个 PCI 桥,pcib0 ➎,它连接到 acpi0 设备。第一个 PCI 总线 ➏ 也连接到 PCI 桥。

所以,你常见的 PCI 设备连接到一系列总线,这些总线再通过 PCI 桥与计算机的其他部分通信。你可以查看 dmesg.boot 并绘制系统上所有设备的树状图;虽然这不是必须的,但了解各设备的连接位置会让配置内核的成功几率大大提高。

如果你有疑问,可以使用 pciconf(8) 来查看系统上实际存在的设备。pciconf -lv 将列出系统上每一个 PCI 设备,无论当前内核是否为其找到驱动程序。

备份你的工作内核

一个坏的内核可能会使你的系统无法启动,因此你必须时刻保留一个好的内核。内核安装过程会将你之前的内核保留下来作为备份,存放在目录 /boot/kernel.old 中。这对于能够回滚是很有用的,但我建议你更进一步。有关启动备用内核的详细信息,请参见第四章。

如果你没有保留一个已知的良好备份,可能会发生以下情况。如果你构建了一个新的内核,发现自己犯了一个小错误,必须重新构建它,而系统生成的备份内核实际上是你第一次制作的内核——那个包含小错误的内核。你的工作内核已经被删除。当你发现新的自定义内核也有相同的问题,或者甚至是更严重的错误时,你会深深后悔失去那个工作内核。

一个常见的保留已知良好内核的位置是/boot/kernel.good。像这样备份你的工作、可靠的内核:

# cp -a /boot/kernel /boot/kernel.good

如果你正在使用 ZFS,那么启动环境可能比复制更有意义(请参阅第十二章)。

不要害怕手头保留各种内核。磁盘空间比时间便宜。我知道一些人会将内核保存在以日期命名的目录中,以便在必要时回退到早期版本。许多人还会将当前版本的 GENERIC 内核保存在/boot/kernel.GENERIC中,用于测试和调试目的。拥有太多内核的唯一方法是把硬盘填满。

配置文件格式

FreeBSD 的内核通过文本文件进行配置。没有图形化工具或菜单驱动的系统来配置内核;这与 4.4 BSD 时的情况基本相同。如果你对文本配置文件不熟悉,构建内核就不适合你。

每个内核配置条目都位于单独的一行。你会看到一个标签来表示这是什么类型的条目,然后是条目的术语。许多条目还会有以井号标记的注释,类似于 FreeBSD 文件系统 FFS 的这个条目。

options         FFS                     # Berkeley Fast Filesystem

每个完整的内核配置文件由五种类型的条目组成:cpuidentmakeoptionsoptionsdevices。这些条目的存在与否决定了内核如何支持相关的功能或硬件:

cpu 这个标签表示该内核支持哪种类型的处理器。针对那些普通 PC 硬件的内核配置文件包括几个 CPU 条目,以涵盖如 486(I486_CPU)、奔腾(I586_CPU)和奔腾 Pro 到现代奔腾 4 CPU(I686_CPU)等处理器。而 amd64/EM64T 硬件的内核配置仅包括一个 CPU 类型——HAMMER,因为该架构只有一个 CPU 系列。虽然一个内核配置可以包括多个 CPU 类型,但它们必须是相似架构的;一个内核可以在 486 和奔腾 CPU 上运行,但你不能让一个内核同时在 Intel 兼容的处理器和 ARM 处理器上运行。

ident 每个内核都有一行ident,为内核提供一个名称。这就是 GENERIC 内核获得名称的方式;它是一个任意的文本字符串。

makeoptions 这个字符串为内核构建软件提供指令。最常见的选项是DEBUG=-g,它告诉编译器构建一个调试内核。调试内核帮助开发人员排查系统问题。

选项 这些是内核功能,不需要特定硬件。包括文件系统、网络协议和内核内调试器。

设备 也称为设备驱动程序,这些为内核提供了如何与特定设备通信的指令。如果你希望系统支持某个硬件,内核必须包括该硬件的设备驱动程序。有些设备条目,称为伪设备,并不与特定硬件绑定,而是支持整个硬件类别——例如以太网、随机数生成器或内存磁盘。你可能会问,伪设备与选项有什么区别。答案是,伪设备在至少某些方面表现得像设备,而选项没有类似设备的特征。例如,回环伪设备是一个仅连接到本地机器的网络接口。尽管它没有硬件支持,但软件可以连接到回环接口,并将网络流量发送到同一机器上的其他软件。

这是配置文件的另一个片段——涵盖 ATA 控制器的部分:

# ATA controllers
device          ahci     # AHCI-compatible SATA controllers
device          ata      # Legacy ATA/SATA controllers
device          mvs      # Marvell 88SX50XX/88SX60XX/88SX70XX/SoC SATA
device          siis     # SiliconImage SiI3124/SiI3132/SiI3531 SATA

这些设备是不同类型的 ATA 控制器。将这些条目与我们在/var/run/dmesg.boot中查看的几个 ATA 条目进行比较:

atapci0: <Intel PIIX4 UDMA33 controller> port 0x1f0-0x1f7,0x3f6,0x170
-0x177,0x376,0xc160-0xc16f at device 1.1 on pci0
ata0: <ATA channel> at channel 0 on atapci0
ata1: <ATA channel> at channel 1 on atapci0
ada0 at ata0 bus 0 scbus0 target 0 lun 0
cd0 at ata1 bus 0 scbus1 target 0 lun 0

内核配置中有一个 ATA 总线,device ata。它是一个“传统”ATA 总线,不管“传统”这个词今天是什么意思。这里的 dmesg 片段以 atapci 设备开始,这是 ATA 与 PCI 连接的控制器。接下来有两个 ATA 总线,ata0 和 ata1。磁盘 ada0 在 ata0 上,而 CD 驱动器 cd0 在 ata1 上。

如果内核配置中没有device ata,内核将无法识别 ATA 总线。即使系统发现系统有一个 DVD 驱动器,内核也不知道如何与它进行信息交换。你的内核配置必须包含所有依赖于它们的驱动程序的中介设备。另一方面,如果你的系统没有 ATA RAID 驱动器、软盘驱动器或磁带驱动器,你可以从内核中移除这些设备驱动程序。

如果该主机有 AHCI、MVS 或 SIIS 控制器,那么这些设备名称将在 dmesg 中显示,而不是 ata。

配置文件

幸运的是,你通常不需要从头开始创建内核配置文件;相反,你是基于现有的配置文件进行构建的。从适合你硬件架构的 GENERIC 内核开始。它可以在/sys//conf中找到——例如,i386 内核配置文件位于/sys/i386/conf,amd64 内核配置文件位于/sys/amd64/conf,依此类推。该目录包含多个文件,其中最重要的是DEFAULTSGENERICGENERIC.hintsMINIMALNOTES

默认设置 这是为特定架构启用的选项和设备列表。这并不意味着你可以编译和运行默认设置,但如果你想通过添加设备来构建内核,它是一个起点。不过,使用GENERIC更为简单。

GENERIC 这是标准内核的配置。它包含了启动并运行该架构的标准硬件所需的所有设置;这是安装程序使用的内核配置。

GENERIC.hints 这是稍后会安装到/boot/device.hints的提示文件。此文件提供了旧硬件的配置信息。

MINIMAL 此配置排除了任何可以从模块加载的内容。

NOTES 这是该硬件平台的全面内核配置。每个平台特定的功能都包含在NOTES中。平台无关的内核功能请参见/usr/src/sys/conf/NOTES

许多架构还有仅针对特定硬件的架构特定配置。i386 架构包括 PAE 内核配置,使你能够在 32 位系统上使用超过 4GB 的 RAM。arm 架构包括数十种配置,每种都对应 FreeBSD 支持的不同平台。

有时,你会找到一个完全符合你需求的内核配置。我想要最小的内核。MINIMAL内核看起来是一个不错的起点。让我们来构建它。

构建内核

一个基础的 FreeBSD 安装,加上操作系统源代码,包含了构建内核所需的所有基础设施。你所需要做的就是通过 KERNCONF 变量告诉系统要构建哪个内核配置。你可以在/etc/src.conf(或/etc/make.conf,如果你真的是老派做法)中设置 KERNCONF。

KERNCONF=MINIMAL

如果你正在尝试构建和运行不同的内核,最好在构建内核时在命令行中设置配置文件。使用make buildkernel命令来构建内核。

# cd /usr/src
# make KERNCONF=MINIMAL buildkernel

构建过程首先运行 config(8)来查找语法配置错误。如果 config(8)检测到问题,它会报告错误并停止。有些错误是非常明显的——例如,你可能不小心删除了对 Unix 文件系统(UFS)的支持,但却包含了从 UFS 启动的支持。一个依赖于另一个,config(8)会告诉你具体出了什么问题。其他一些错误消息则比较陌生和难以理解;那些最难以解决的错误可能像这样:

MINIMAL: unknown option "NET6"

NET6 是 IPv6 选项,不是吗?不,那是I NET6。显然有个傻瓜在文本编辑器中查看配置文件时不小心删除了一个字母。这个错误一目了然——只要你熟悉所有支持的内核选项。仔细阅读这些错误信息!

一旦 config(8)验证了配置,内核构建过程将在现代计算机上完成几分钟。构建成功后会显示类似这样的消息。

--------------------------------------------------------------
>>> Kernel build for MINIMAL completed on Tue Sep 12 14:27:08 EDT 2017
--------------------------------------------------------------

构建完内核后,进行安装。运行make installkernel会将当前内核移到/boot/kernel.old,并将新内核安装到/boot/kernel。安装内核的速度比构建内核要快得多。

信任内核

最终,你会到达一个阶段,信任自己的内核配置,并希望用一个命令构建并安装它。make kernel命令会构建并安装内核。真正的系统管理员会运行make kernel && reboot

安装完成后,重启你的服务器并观察启动信息。如果一切正常,你会看到类似以下内容,显示正在运行的内核以及它的构建时间。

Copyright (c) 1992-2018 The FreeBSD Project.
Copyright (c) 1979, 1980, 1983, 1986, 1988, 1989, 1991, 1992, 1993, 1994
The Regents of the University of California. All rights reserved.
FreeBSD storm 12.0-CURRENT FreeBSD 12.0-CURRENT #0 r323136: Sat Sep  2 
21:46:53 EDT 2018     root@storm:/usr/obj/usr/src/sys/MINIMAL  amd64
--snip--

关键是,MINIMAL 内核无法启动所有硬件。它不能启动大多数硬件。而对于 MINIMAL 能够启动的硬件,它也无法启动该硬件上的大部分 FreeBSD 安装。

MINIMAL 将所有可以作为模块的东西都放在模块中。磁盘分区方法,无论是 GPT 还是 MBR,都可以作为模块。你必须通过loader.conf加载geom_part_gpt.kogeom_part_mbr.ko才能启动 MINIMAL。文件系统也是模块,因此你必须加载它们。简而言之,你需要加载硬件和安装选择所需的每个模块。MINIMAL 是所有内核所需模块的一个很好的参考,也是设计自己内核的一个不错的起点,但不足以用于生产环境。

启动备用内核

那么,如果你的新内核无法工作,或者工作得很差怎么办?也许你忘记了某个设备驱动程序,或者不小心去掉了INET选项,导致无法访问互联网。有时它会在启动过程的早期就卡住,唯一能做的就是重启主机。别慌!你还保留着旧的内核,对吧?下面是该怎么做。

首先记录下错误信息。你需要研究这些信息,以找出你的新内核哪里出错了。^(5)不过,要修复这个错误,你需要启动一个工作内核,才能构建一个改进的内核。

在第四章中,我们讨论了启动备用内核的机制。我们将在这里讲解具体的操作步骤,但要查看一些有关加载器管理的详细内容,你可以回去查看前面的部分。现在,我们将专注于为什么需要启动备用内核,以及如何正确操作。

首先决定你想启动哪个内核。你的旧内核应该在/boot目录下;在本节中,我们假设你想启动/boot/kernel.good中的内核。重启并中断启动过程,进入启动菜单。第五个选项允许你选择其他内核。菜单会显示loader.confkernels选项列出的每个内核目录。默认列出的是kernelkernel.old,我会添加kernel.good

一旦你安装了另一个新内核,记住:现有的/boot/kernel会被复制到/boot/kernel.old,这样你的新内核就可以放在/boot/kernel中。如果那个内核无法启动,而你的新内核也无法启动,你将没有可用的内核。这种情况很糟糕。一定要确保手头有一个已知的工作内核。

自定义内核配置

也许提供的内核配置都不适合你,你需要的是其他的东西。FreeBSD 让你可以创建任何你想要的配置。但最简单的方式是修改现有的配置。你可以复制一个现有的文件或使用 include 选项。我们将从修改现有文件开始。确保你使用正确的架构目录,可能是 /sys/amd64/conf/sys/i386/conf

不要直接编辑配置目录中的任何文件。相反,复制 GENERIC 文件,并以你的机器或内核功能命名,然后编辑副本。在这个例子中,我正在构建一个支持 VirtualBox 系统的最小内核。我将 GENERIC 文件复制为名为 VBOX 的文件,并用我偏好的文本编辑器打开 VBOX

裁剪内核

曾几何时,内存比今天贵得多,而且只有较小的容量。当系统只有 128MB 的内存时,你希望这些内存能用于工作,而不是用来存放无用的设备驱动程序。今天,当一台廉价的笔记本电脑以可怜的 64GB 内存艰难度日时,内核大小几乎变得无关紧要。

对大多数人来说,剥除内核中不必要的驱动和功能以减小其大小是浪费时间和精力,但我鼓励你至少做一次。这会教你如何构建内核,这样当你需要测试内核补丁或其他操作时,就不必在处理重新构建的问题时再学习内核构建的知识。当你开始在像 BeagleBone 或 Raspberry Pi 这样的小型主机上实验 FreeBSD 时,这也会有所帮助。

我想构建一个支持 VirtualBox 内核的内核。我在 VirtualBox 上启动一个正常工作的 FreeBSD 安装,以便查看 dmesg.boot。我将在 dmesg 和配置文件之间来回切换,注释掉不需要的条目。

CPU 类型

在大多数架构上,FreeBSD 只支持一种或两种类型的 CPU。amd64 平台只支持一种,HAMMER。i386 平台支持三种,但其中两种——486 和最初的奔腾——在嵌入式市场之外已经极为过时。

cpu             I486_CPU
cpu             I586_CPU
cpu             I686_CPU

你只需要包含你拥有的 CPU。如果你不确定硬件中的 CPU,检查 dmesg.boot。我有一台古老的笔记本,显示如下:

CPU: AMD Athlon(tm) 64 X2 Dual Core Processor 4200+ (2200.10-MHz 686-class CPU)
  Origin = "AuthenticAMD"  Id = 0x20fb1  Stepping = 1
  Features=0x178bfbff<FPU,VME,DE,PSE,TSC,MSR,PAE,MCE,CX8,APIC,SEP,MTRR,PGE,MCA,
CMOV,PAT,PSE36,CLFLUSH,MMX,FXSR,SSE,SSE2,HTT>
--snip--

如粗体所示,这是一个 686 类 CPU,这意味着我可以移除 I486_CPU 和 I586_CPU 语句,从而让我的内核更小。

核心选项

在 CPU 类型配置条目之后,我们有一长串用于基本 FreeBSD 服务的选项,如 TCP/IP 和文件系统。普通系统不需要所有这些,但它们的存在提供了极大的灵活性。你还会遇到一些在你的环境中很少使用的选项,以及那些可以从自定义内核配置中移除的选项。我们不会讨论所有可能的内核选项,而是会具体介绍不同类型选项的示例。我会特别提到那些可以从互联网服务器中修剪掉的选项。LINT 文件、手册页以及你最喜欢的互联网搜索引擎可以帮助你了解其他选项。如果你对某个选项有疑问,保留它。或者禁用它,看看会有什么问题。

考虑以下与网络相关的选项:

options         INET                 # InterNETworking
options         INET6                # IPv6 communications protocols
options         IPSEC                # IP (v4/v6) security
options         IPSEC_SUPPORT        # Allow kldload of ipsec and tcpmd5
options         TCP_OFFLOAD          # TCP offload
options         TCP_HHOOK            # hhook(9) framework for TCP
options         SCTP                 # Stream Control Transmission Protocol

这些选项支持网络。INET 是传统的 TCP/IP,INET6 支持 IPv6。许多类似 Unix 的软件依赖于 TCP/IP,因此你肯定需要这两者。IPSEC 和 IPSEC_SUPPORT 让你使用 IPSec VPN 协议。我肯定不会在我的虚拟机上使用这些,所以我会将它们注释掉。

TCP_OFFLOAD 选项允许网络堆栈将 TCP/IP 计算卸载到网卡上。听起来不错,除了虚拟机上的 vnet(4) 网络接口并不执行此功能。砍掉它!

TCP_HHOOK 选项为你提供了一个方便的手册页供阅读。我会使用这个选项吗?或许吧。更重要的是,我不知道我正在运行的软件是否需要它。我会保留它。

SCTP 传输协议很不错,但对于在我笔记本电脑上运行的虚拟机来说完全没用。再见。

options         FFS             # Berkeley Fast Filesystem
options         SOFTUPDATES     # Enable FFS soft updates support
options         UFS_ACL         # Support for access control lists
options         UFS_DIRHASH     # Improve performance on big directories
options         UFS_GJOURNAL    # Enable gjournal-based UFS journaling

FFS 选项提供了标准的 FreeBSD 文件系统 UFS。即使是 ZFS 主机也需要 UFS 支持。保留它。其他选项都与 FFS 相关。我们在 第十一章 中详细讨论了 FFS 及其选项,比你想象的还要详细,但现在,先相信我,照做就行。

软更新确保即使系统不正确关闭时也能保证磁盘完整性。如在 acl(9) 中讨论的,UFS 访问控制列表允许你为文件授予非常详细的权限,而这些我在我的虚拟主机上不需要。砍掉它!

UFS_DIRHASH 启用目录哈希,使得包含成千上万个文件的目录更加高效。保留它。我将使用软更新日志,而不是 gjournaling,所以 UFS_GJOURNAL 可以去掉。

options         MD_ROOT                 # MD is a potential root device

这个选项——以及所有其他 _ROOT 选项——允许系统使用标准 UFS 或 ZFS 文件系统以外的其他磁盘设备作为根分区。安装程序使用内存设备(MD)作为根分区。如果你使用的是无盘系统(参见 第二十三章),你将需要一个 NFS 根分区。如果你在一台标准计算机系统上运行 FreeBSD,且配有硬盘、键盘等,你的内核不需要这些功能。

options         NFSCL                   # Network Filesystem Client
options         NFSD                    # Network Filesystem Server
options         NFSLOCKD                # Network Lock Manager

这两个选项支持网络文件系统(参见 第十三章)。这里的关键问题是,你需要 NFS 吗?如果需要,你是需要作为服务器还是客户端?我将保留这些选项。

options         MSDOSFS              # MSDOS filesystem
options         CD9660               # ISO 9660 filesystem
options         PROCFS               # Process filesystem (requires PSEUDOFS)
options         PSEUDOFS             # Pseudo-filesystem framework

这些选项支持间歇性使用的文件系统,如 FAT、CD、进程文件系统和伪文件系统框架。我们在第十三章中讨论了许多这些文件系统,但它们都可以作为内核模块使用。删除它们。

options         COMPAT_FREEBSD32        # Compatible with i386 binaries
options         COMPAT_FREEBSD4         # Compatible with FreeBSD4
options         COMPAT_FREEBSD5         # Compatible with FreeBSD5
options         COMPAT_FREEBSD6         # Compatible with FreeBSD6
--snip--

这些兼容性选项允许你的系统运行为较旧版本的 FreeBSD 或者基于较旧版本的 FreeBSD 内核假设构建的软件。如果你是从头开始安装系统,可能不需要兼容 FreeBSD 4、5 或 6,但有相当一部分软件需要兼容 32 位的 FreeBSD。保留 COMPAT_FREEBSD32 选项,否则你的系统 崩溃。

options         SCSI_DELAY=5000         # Delay (in ms) before probing SCSI

SCSI_DELAY 选项指定 FreeBSD 在找到你的 SCSI 控制器后等待多少毫秒再进行探测,给它们一点时间来启动并自我识别到 SCSI 总线上。如果你没有 SCSI 硬件,可以删除这一行。

options         SYSVSHM                 # SYSV-style shared memory
options         SYSVMSG                 # SYSV-style message queues
options         SYSVSEM                 # SYSV-style semaphores

这些选项启用 System-V 风格的共享内存和进程间通信。许多数据库程序使用此功能。

多处理器

以下条目启用 i386 内核中的对称多处理(SMP):

options         SMP                     # Symmetric MultiProcessor Kernel
options         DEVICE_NUMA             # I/O Device Affinity
options         EARLY_AP_STARTUP

这些选项可能不会有害,但如果你知道你的系统运行在单核板卡上,可能是非常旧的系统或使用嵌入式硬件,你可以删除这些选项。

设备驱动

在所有选项之后,你会找到设备驱动条目,这些条目被分组得相当合理。为了缩小内核的大小,你需要删除所有主机不使用的部分——但究竟是什么是主机不使用的呢?可以在 dmesg.boot 中搜索每个设备驱动。

第一个设备条目是总线,例如 device pcidevice acpi。除非你系统中确实没有这种类型的总线,否则请保留这些条目。

接下来,我们进入大多数人认为的真正的设备驱动部分——软盘驱动器、SCSI 控制器、RAID 控制器等的条目。如果你的目标是减少内核的大小,这是一个很好的修剪位置;删除所有不具备的硬件设备驱动。你还会找到一些设备驱动条目,如键盘、显卡、USB 端口等的条目。你几乎肯定不想删除这些。

网络卡设备驱动部分相当长,看起来很像 SCSI 和 IDE 部分。如果你近期不打算更换网络卡,可以删除所有不使用的网络卡驱动。

我们不会在这里列出所有设备驱动,因为从这样一个列表中学到的东西很少,除了展示 FreeBSD 在我编写本节时支持的硬件。请查看你当前运行的 FreeBSD 版本的发布说明,以了解它支持的硬件。

你还会看到一大部分虚拟化驱动程序。最常用的虚拟接口基于 VirtIO,但你也会看到专门为 Xen、Hyper-V 和 VMware 设计的驱动程序。内核只需要为其运行的平台提供虚拟化驱动程序。真实硬件的内核不需要任何虚拟化驱动程序,即使主机上会运行虚拟机。

伪设备

在 GENERIC 内核配置的底部,你会找到一组选项伪设备。顾名思义,这些完全是由软件创建的。以下是一些常用的伪设备。

device          loop            # Network loopback

回环设备允许系统通过网络套接字和网络协议与自身进行通信。我们将在下一章详细讨论网络连接。你可能会惊讶于有多少程序使用回环设备,因此不要删除它。

device          random                  # Entropy device
device          padlock_rng             # VIA Padlock RNG
device          rdrand_rng              # Intel Bull Mountain RNGdevice          

这些设备提供伪随机数,供加密操作和一些关键任务应用程序(如游戏)使用。有些设备需要底层芯片组的支持。FreeBSD 支持多种随机源,透明地将它们聚合到随机设备/dev/random/dev/urandom中。

device          ether           # Ethernet support

以太网具有许多设备特性,对于 FreeBSD 来说,将其视为一个设备是最简单的。除非你想学习,否则无需更改此设置。

device          vlan                    # 802.1Q VLAN support
device          tun                     # Packet tunnel
device          gif                     # IPv6 and IPv4 tunneling

这些设备支持像 VLAN 和各种隧道这样的网络功能。

device          md              # Memory "disks"

内存磁盘允许你将文件存储在内存中。这对于非常快速的临时数据存储非常有用,正如我们将在第十三章中学习的那样。对于大多数(但不是所有)互联网服务器,内存磁盘是浪费内存的。你也可以使用内存磁盘来挂载和访问磁盘映像。如果你不使用内存磁盘,可以将其从内核中移除。

可移动硬件

GENERIC 内核支持几种不同类型的可移动硬件。如果你的笔记本电脑是由包含两个连续数字 9 或 0 的年份生产的,它可能有 Cardbus 甚至 PCMCIA 卡。否则,你不需要在内核中支持这些。FreeBSD 支持热插拔的 PCI 卡,但如果你没有这些卡,就将这些驱动程序丢弃。

包含配置文件

你的内核二进制文件可能与其构建所在的机器分开。我建议使用 INCLUDE_CONFIG_FILE 选项,将内核配置文件复制到已编译的内核中。这样你会失去任何注释,但至少你会保留此内核中的选项和设备,并且如果需要,可以复制它。sysctl kern.conftxt包含内核配置。

一旦你有了修剪过的内核,尝试构建它。你的第一个内核配置肯定会出错。

内核构建故障排除

如果你的内核构建失败,第一步是查看输出的最后几行。这些错误中的一些可能非常难以理解,但其他的会很直白。需要记住的重要一点是,错误信息中说“在 某目录 停止”的并没有用;有用的错误信息会出现在它们之前。我们在《寻求帮助》中讨论了如何解决这些问题,见第 11 页:拿着错误信息去搜索引擎搜一搜。编译错误通常是由于配置错误引起的。

幸运的是,FreeBSD 强制要求在安装任何东西之前编译完整的内核。构建失败不会损坏已安装的系统。然而,它将给你一个机会来测试我们在第一章中提到的故障排除技巧。

最常见的错误类型是 make buildkernel 阶段失败。它可能看起来像这样:

--snip--
linking kernel.full
vesa.o: In function `vesa_unload':
/usr/src/sys/dev/fb/vesa.c:1952: undefined reference to ➊ `vesa_unload_ioctl'
vesa.o: In function `vesa_configure':
/usr/src/sys/dev/fb/vesa.c:1169: undefined reference to ➋ `vesa_load_ioctl'
*** Error code 1
--snip--

你会看到几页的错误代码 1 消息,但实际的错误出现在它们之前。

我们的内核中的某行代码需要vesa_unload_ioctl ➊ 和 vesa_load_ioctl ➋ 这两个函数,但提供这些功能的设备或选项并没有包含在内核中。尝试通过互联网搜索这些错误,看看是否有这些函数的手册页。如果都不行,再去查找源代码。

# cd /usr/src/sys
# grep -R vesa_unload_ioctl *
dev/fb/vesa.h:int vesa_unload_ioctl(void);
dev/fb/vesa.c:  if ((error = vesa_unload_ioctl()) == 0) {
dev/syscons/scvesactl.c:vesa_unload_ioctl(void)

等等——在 GENERIC 配置文件中不是有提到“syscons”驱动程序吗?

# syscons is the default console driver, resembling an SCO console
#device          sc
#options         SC_PIXEL_MODE           # add support for the raster text mode

我已经注释掉了 sc(4) 驱动程序。把它重新添加回去,再试一次。

还有更多“正确”的方法来弄清楚哪些内核设备需要哪些设备。最终的方法归结为“阅读并理解源代码”。对于我们大多数人来说,试错、研究,再继续试错,反而是更快速的方式。

包含、排除与扩展内核

现在你可以编译内核了,接下来让我们来点花样,看看如何使用包含、各种 no 配置和 NOTES 文件。

备注

FreeBSD 的内核包含了许多在 GENERIC 中没有的功能。这些特殊功能通常是为特定系统或某些特殊网络的边缘案例设计的。你可以在每个平台的内核配置目录下找到包含硬件特定功能的完整列表——例如,/sys/amd64/conf/NOTES。而那些硬件无关的内核功能——即适用于 FreeBSD 支持的所有平台的功能——可以在 /sys/conf/NOTES 中找到。如果你的硬件在 GENERIC 内核中似乎没有完全得到支持,看看 NOTES 文件。虽然其中一些功能较为晦涩,但如果你有这些硬件,你会感激它们的。让我们来看看 NOTES 中的一个典型条目:

# Direct Rendering modules for 3D acceleration.
device          drm             # DRM core module required by DRM drivers
device          mach64drm       # ATI Rage Pro, Rage Mobility P/M, Rage XL
device          mgadrm          # AGP Matrox G200, G400, G450, G550
device          r128drm         # ATI Rage 128
device          savagedrm       # S3 Savage3D, Savage4
device          sisdrm          # SiS 300/305, 540, 630
device          tdfxdrm         # 3dfx Voodoo 3/4/5 and Banshee
device          viadrm          # VIA
options         DRM_DEBUG       # Include debug printfs (slow)

你在桌面上使用这些显卡吗?也许你想要一个包含适当设备驱动程序的自定义内核。

如果 NOTES 文件列出了每个设备的所有功能,为什么不直接将其作为内核的基础呢?首先,这样的内核会使用比 GENERIC 内核更多的内存。虽然即使是小型现代计算机也有足够的内存来运行 GENERIC,而如果内核变得比 GENERIC 大十倍,却没有相应的功能增加,人们就会感到烦恼。此外,许多选项是互斥的。你会发现有些选项允许你决定内核如何调度进程。例如,内核一次只能使用一个调度程序,而每个调度程序的影响遍及整个内核。将所有调度程序同时添加到内核中,会增加代码复杂性并降低稳定性。

我每发布一个或两个版本,就会重点查看 NOTES,以便寻找一些有趣的新特性。

包含和排除

FreeBSD 的内核配置有两个有趣的功能,可以使维护内核变得更容易:no 选项和 include 功能。

include 功能让你将一个独立的文件引入到内核配置中。例如,如果你有一个可以描述为“GENERIC 加上几个额外功能”的内核配置,你可以使用 include 语句将 GENERIC 内核配置包括进来:

include GENERIC

所以,如果你想构建一个既具备 GENERIC 所有功能,又支持 VIA 3d 芯片的 DRM 功能的内核,你可以创建一个完全由以下内容组成的有效内核配置:

ident        VIADRM
include      GENERIC
options      drm
options      viadrm

你可能会认为这实际上比将 GENERIC 复制到一个新文件并进行编辑更麻烦,而你是对的。那么,为什么还要做这个呢?最大的原因是,随着 FreeBSD 的升级,GENERIC 配置可能会发生变化。FreeBSD 12.1 中的 GENERIC 与 12.0 中的略有不同。你的新配置对于这两个版本都是有效的,并且在这两种情况下都可以正当描述为“GENERIC 加上我的选项”。

这种方法很适合包括内容,但对于从内核中删除内容并不太有效。与其为每个新的 FreeBSD 版本手动重建内核,你可以使用 include 语句,但通过 nodevicenooptions 关键字排除不需要的条目。使用 nodevice 移除不需要的设备驱动,而 nooptions 则禁用不需要的选项。

查看 -current 机器上的 GENERIC-NODEBUG 内核配置。它与 GENERIC 配置相同,只是禁用了所有调试功能。

include GENERIC

ident   GENERIC-NODEBUG

nooptions       INVARIANTS
nooptions       INVARIANT_SUPPORT
nooptions       WITNESS
nooptions       WITNESS_SKIPSPIN
nooptions       BUF_TRACKING
nooptions       DEADLKRES
nooptions       FULL_BUF_TRACKING

我们首先包括了 GENERIC 内核配置。不过,这个内核自我标识为 GENERIC-NODEBUG。接下来的七个 nooptions 语句关闭了 FreeBSD-current 的标准调试选项。开发人员使用 GENERIC-NODEBUG 内核来查看内核调试器是否引起了问题。如果带调试的内核在运行时出现崩溃,而没有调试的内核则不会崩溃,那么调试代码看起来就会显得可疑。

跳过模块

如果你已经费心构建了一个自定义内核,你可能清楚地知道主机需要哪些内核模块。如果你从不打算使用它们,为什么还要构建这么多内核模块呢?你可以使用MODULES_OVERRIDE选项关闭模块的构建。将MODULES_OVERRIDE设置为你想要构建和安装的模块列表。

# make MODULES_OVERRIDE='' kernel

也许你想构建大多数模块,但你有理由讨厌某个特定的模块。使用WITHOUT_MODULES将其从构建中排除。在这里,我排除了 vmm 模块,因为我甚至不想有在 VirtualBox 上运行 bhyve(8)的诱惑。从这里开始,虚拟化层的数量会迅速增多,最终我可能会感叹为什么我的笔记本变慢了。

# make KERNCONF=VBOX WITHOUT_MODULES=vmm kernel

选择性构建模块,结合自定义内核,可以让你将系统锁定到非常小的框架中。当你发现缺少某个你从未想过会需要的功能时,你就会明白这些框架有多小。如果你必须构建一个内核,保守地保留一些功能是明智的。

现在,你的本地机器已经调优到你想要的精确状态,接下来我们来考虑互联网的其余部分。

第七章:网络

image

FreeBSD 以其网络性能著称。TCP/IP 网络协议套件最初是在 BSD 上开发的,而 BSD 则包括了第一个重大实现的 TCP/IP。尽管在 1980 年代,竞争性网络协议被认为更具吸引力,但 BSD TCP/IP 栈的广泛可用性、灵活性以及宽松的许可政策使其成为事实上的标准。这不仅仅是历史的好奇,今天,Facebook 正积极寻找能够让 Linux 的网络性能与 FreeBSD 匹配的工程师。这个项目预计将持续数年。

许多系统管理员今天对网络基础知识有些模糊的了解,但并不真正理解这些知识是如何相互关联的。然而,优秀的系统管理员理解网络。了解 IP 地址的真实含义、子网掩码的工作原理以及端口号与协议号的区别是掌握你职业技能的必要步骤。本章将涵盖其中的一些问题。首先,你必须理解网络层次。

本章提供了关于 TCP/IP 的一个不错概览,但并未涵盖众多细节、陷阱和注意事项。如果你需要深入学习 TCP/IP,建议阅读相关书籍。作为概览,你可以参考我的书《系统管理员的网络基础》(Tilted Windmill Press,2015)。最终,你将需要深入了解网络,直接阅读 Charles M. Kozierok 的《TCP/IP 指南》(No Starch Press,2005)。

主流的互联网协议是 TCP/IP(基于互联网协议的传输控制协议)。TCP 是传输协议,而 IP 是网络协议,但它们紧密相连,通常被视为一个整体。我们将首先探讨网络如何工作,然后讨论 IP 版本 4 和 6,再继续讲解 TCP 和 UDP。

网络层

网络的每一层都处理网络过程中特定的任务,并且只与其上下相邻的层进行交互。学习 TCP/IP 的人常常会笑谈这些层简化了网络过程,但这确实是真的。此时需要记住的重要一点是,每一层仅与直接位于其上方和下方的层进行通信。

经典的开放系统互联(OSI)网络协议栈有七层,完整无缺,几乎涵盖了所有网络协议和应用的各种情况。然而,互联网只是其中的一种情况,而本书并不是关于网络或网络应用的书籍。我们将讨论的内容仅限于 TCP/IP 网络,例如互联网和几乎所有的企业网络,因此我们只需要考虑网络栈的四个层次。

物理层

在最底层,我们有物理层:网络卡以及从网络卡连接到的电缆、光纤或无线电波。这个层包括物理交换机、集线器或基站、连接这些设备到路由器的电缆以及从你办公室到电话公司的光纤。电话公司交换机是物理层的一部分,横跨大陆的光纤电缆也是如此。如果它能被踩到、掉落或被电锯锯断,那它就是物理层的一部分。从现在开始,我们将把物理层称为电缆,尽管这个层可以是任何类型的媒介。

这是最容易理解的层——它就像是硬件完好无损一样简单。如果你的电缆符合物理协议的要求,那就成功了。如果不符合,那就没戏了。没有物理层,其它网络层根本无法工作——就是这么简单。互联网路由器的一个功能是将一种物理层连接到另一种物理层——例如,将本地以太网转换为光纤。物理层没有决策能力,也没有智能;它上面运行的一切都由数据链路层决定。

数据链路层:物理协议

数据链路层,或称物理协议层,是网络中最有趣的地方。这个层将信息转化为实际的二进制数据(0 和 1),并以适合该物理协议的编码方式通过物理层发送。例如,IP 第四版(IPv4)通过以太网使用媒体访问控制(MAC)地址和地址解析协议(ARP);而 IP 第六版(IPv6)通过以太网使用邻居发现协议(NDP,有时也简称 ND)。除了流行的以太网数据链路层,FreeBSD 还支持其他协议,包括点对点协议(PPP)和高级数据链路控制协议(HDLC),以及一些组合协议,如某些家庭宽带供应商使用的 PPPoE(以太网上的 PPP)。尽管 FreeBSD 支持所有这些数据链路协议,但它并不支持所有曾经使用过的数据链路协议。如果你有特殊的网络需求,可以查阅你所使用的 FreeBSD 版本的文档,看看是否支持该协议。

一些物理协议已经在许多不同的物理层上实现。例如,以太网就曾通过双轴电缆、同轴电缆、CAT3、CAT5、CAT6、CAT7、光纤、HDMI 和无线电波等方式进行传输。通过对设备驱动程序进行些许调整,数据链路层可以适应任何类型的物理层。这就是层次化结构简化网络的一种方式。我们将在本章最后的第 140 页中的“理解以太网”一节详细讨论以太网,因为它是 FreeBSD 系统最常使用的网络类型。通过了解 FreeBSD 上的以太网,你也将能够管理其他协议——当然,前提是你已经理解了这些协议!

除了与物理层交换信息,数据链路层还与网络层进行通信。

网络层

网络层?难道整个东西不是网络吗?

是的,但网络层更为具体。它映射网络节点之间的连接,回答诸如“其他主机在哪里?”和“你能访问这个特定的主机吗?”这样的问题。这个逻辑协议为在网络上运行的程序提供了一个一致的接口,无论你使用什么样的物理层。互联网上使用的网络层是互联网协议(IP)。IP 为每个主机提供一个独一无二的^(1)地址,称为IP 地址,以便网络上的任何其他主机都可以找到它。你需要了解 IP 的第 4 版和第 6 版。

网络层是我们真正抽象出底层物理媒介的地方。IP 是通过以太网运行的吗?ATM?信鸽?谁在乎?它有一个 IP 地址,所以我们可以与之通信。继续吧。

网络层与下面的数据链路层和上面的传输层进行通信。

繁重工作:传输层

传输层处理真实应用程序的实际数据,甚至可能是实际的人类。三种常见的传输层协议是 ICMP、TCP 和 UDP。

互联网控制消息协议(ICMP)管理具有 IP 地址的主机之间的基本连接消息。如果 IP 提供了一条道路和地址,那么 ICMP 提供了交通信号灯和高速公路出口标志。大多数时候,ICMP 在后台运行,你无需关注它。

其他著名的传输协议有用户数据报协议(UDP)传输控制协议(TCP)。这些有多常见?

好的,互联网协议套件通常称为 TCP/IP。这些协议提供了如通过端口号复用和传输用户数据等服务。UDP 是一种基本的传输协议,提供了传输数据所需的最基本服务。TCP 则提供了更复杂的功能,如拥塞控制和完整性检查。

除了这三种协议,许多其他协议也运行在 IP 之上。文件/etc/protocols包含了一个相当全面的传输协议列表,这些协议使用 IP 作为底层机制。你不会在这里找到非 IP 协议,比如 Digital 的 LAT,但它包含了许多你在现实中永远不会见到的协议。例如,下面是 IP 和 ICMP 的条目,它们是互联网上常用的网络层协议:

➊ip   ➋0   ➌IP    ➍# Internet protocol, pseudo protocol number
  icmp  1    ICMP    # Internet control message protocol

/etc/protocols中的每个条目有三个关键字段:一个非正式的名称➊,一个协议编号➋,以及任何别名➌。协议编号在网络请求中用于识别流量。如果你曾经启动过数据包嗅探器或出于某种原因深入研究过网络,你就会看到它。如你所见,IP 是协议 0,ICMP 是协议 1——如果这不是所有其他协议的基础,那么很难想象还有什么能成为基础!TCP 是协议 6,UDP 是协议 17。你还会看到注释➍,提供关于每个协议的更多细节。

传输层与下面的网络层和上面的应用程序层进行通信。

应用程序

应用程序肯定是网络的一部分。应用程序发起网络连接请求,通过网络发送数据,从网络接收数据,并处理这些数据。网页浏览器、电子邮件客户端、JSP 服务器等都是网络感知应用程序。应用程序只需要与网络协议和用户进行通信。用户层的问题超出了本书的范围。^(2)

实践中的网络

所以,你现在明白所有的连接是如何搭建在一起的,并且准备继续前进,对吧?不一定。让我们看看在现实世界中它是如何工作的。部分解释涉及到我们在本章后面会讨论的内容,但如果你正在读这本书,你应该足够了解网络知识,能够跟上。如果你有困难,读完本章的其他部分后可以重新阅读这一节。(可以再买一本这本书,把这几页剪下来,粘贴在本章结尾处。)

假设一个通过你的网络连接到互联网的用户想要查看 Yahoo!用户打开他的浏览器并输入网址。

浏览器应用程序知道如何与网络中的下一层进行通信,即传输层。在将用户的请求处理成合适的格式后,浏览器向传输层请求一个到特定 IP 地址、端口 80 的 TCP 连接。(纯粹主义者会注意到我们跳过了 DNS 请求部分,但这与所描述的过程非常相似,不会对我们的示例产生影响。)

传输层检查浏览器的请求。由于应用程序请求了 TCP 连接,传输层为该类型的连接分配适当的系统资源。请求被分解成易于处理的块,并交给网络层。

网络层不关心实际的请求。它接收到的是一堆需要通过互联网传输的数据。就像邮递员在投递信件时不关心信件内容一样,网络层只是将 TCP 数据与适当的地址信息捆绑在一起。最终的数据块被称为数据包。网络层将这些数据包交给数据链路层。

数据链路层不关心数据包的内容。它当然不关心 IP 地址或路由。它收到了一堆零和一,任务就是将这些零和一通过网络传输。它只知道如何执行这个传输。数据链路层可能会根据所使用的物理介质为数据包添加适当的头部和/或尾部信息,从而创建一个。最后,它将帧交给物理层,在本地电缆、无线或其他介质上进行传输。

每个都在另一个里面?

是的,你的原始网页请求已经被 TCP 协议封装。这个请求在传输层又被 IP 协议封装一次,然后又被数据链路协议封装一次。所有这些头部信息都被加在原始请求的前后。你见过那张小鱼被稍大一点的鱼吞掉,而稍大一点的鱼又被更大的鱼吃掉的图片吗?它正是这样。或者,如果你愿意,数据帧就像是最外面的套娃。解开一个协议,你会发现另一个协议。

物理层完全没有智能。数据链路层将一堆零和一传递给物理层,物理层将它们传输到另一个物理设备。它不知道正在使用什么协议,也不知道这些数字是如何通过交换机、集线器或中继器回响的,但这个网络中的某个主机大概是网络的路由器。

当路由器接收到零和一时,它会将它们交给数据链路层。数据链路层去除其封装信息,并将得到的数据包交给路由器中的网络层。路由器的网络层检查数据包并根据其路由表决定如何处理它。然后,它会将数据包交给适当的数据链路层。这可能是另一个以太网接口,或者是一个来自 T1 的 PPP 接口。

在数据传输过程中,你的电缆可能会经历许多物理变化。你的有线互联网连接可能会聚合成光纤 DS3,然后再转化为 OC192 的跨国链路。感谢分层和抽象的奇妙,既你的计算机也你的用户都不需要了解这些内容。

当请求到达目的地时,另一端的计算机接收该帧并将其一路传回协议栈。物理层接收零和一的信号并将它们传送到数据链路层。数据链路层去除以太网头部,并将处理后的数据包交给网络层。网络层去除数据包头部,并将剩余的数据段传递到传输层。传输层将这些数据段重新组装成数据流,然后交给应用程序——在这个例子中是一个 web 服务器。应用程序处理请求并返回答案,然后数据沿协议栈向下传递,必要时穿越各种数据链路层。这一系列的工作是为了让机器完成,只为让你看到那个“404 页面未找到”的错误。

这个例子展示了为什么分层如此重要。每一层仅了解关于上下层必须知道的内容,这使得如果需要,可以轻松地替换层内部的实现。当创建新的数据链路协议时,其他层不必更改;网络协议只需向数据链路层发送格式正确的请求,让该层完成它的工作。当你拥有一个新的网卡时,你只需要一个与数据链路层和物理层接口的驱动程序;你不需要更改网络栈中更高层的任何内容,包括你的应用程序。想象一下,如果设备驱动程序必须安装在你的 web 浏览器、电子邮件客户端和你电脑上所有其他应用程序中,包括自定义构建的应用程序,你会很快放弃计算机,去做一些更理智、更合理的事情,比如与铁砧一起跳伞。

获取比特和十六进制

作为一名系统管理员,你会经常遇到像48 位地址18 位子网掩码这样的术语。我见过不少系统管理员在听到这些时只是点点头,微笑着,心里想着:“好吧,反正告诉我做工作需要知道什么就行。”不幸的是,数学是这项工作的一部分,你必须理解比特。虽然这些数学计算并不直观,但理解它是区分业余者和专业人士的关键之一。如果你想保持业余水平,就不会读这样的书。

或许你在嘀咕,“但我已经知道这些了!”那就跳过吧。但如果你不了解,就不要骗自己。

你可能已经知道,计算机把所有数据都当作 0 和 1 来处理,一个零或一就是一个比特。当协议指定了比特数时,它指的是计算机看到的数字。一 个 32 位数字有 32 个数字,每个数字是 0 或 1。你可能在小学时接触过二进制数学,或者说基数 2,并且记得刚好足够长的时间来通过测试。现在是时候重新拾起那些记忆了。二进制数学实际上只是另一种处理我们每天看到的数字的方式。

我们每天使用十进制数学,或者说基数 10,来支付披萨费和核对账单。数字从 0 到 9。当你想超过当前最高数字时,你会在左边添加一个数字,并将当前数字设为零。这就是你多年前学到的“进位”规则,现在可能不加思考地自动做出来了。在二进制数学中,数字从 0 到 1,当你想超过当前最高数字时,你会在左边添加一个数字,并将当前数字设为 0。这和十进制数学一模一样,只是少了八根手指。举个例子,表 7-1 展示了将前几个十进制数字转换为二进制的过程。

表 7-1: 十进制和二进制数字

十进制 二进制
0 0
1 1
2 10
3 11
4 100
5 101
6 110
7 111
8 1000

当你有一个 32 位数字时,例如一个 IP 地址,你会得到一串 32 个 1 和 0。以太网 MAC 地址是 48 位数字,有 48 个 1 和 0。

仅供娱乐,Unix 在某些情况下也使用十六进制数字,例如 MAC 地址和子网掩码。十六进制数字是 4 位长。二进制数 1111,完整的 4 位,等同于 15;这意味着十六进制数学中的数字范围是从 0 到 15。此时,可能有一些人看到应该是单一数字的 2 位数 15,心想我在吸什么烟,以及你们去哪儿能买到同样的东西。十六进制数学使用字母 A 到 F 表示数字 10 到 15。当你数到最后一个数字并想加 1 时,你会将当前数字设为零,并在数字的左边添加一个新数字。例如,想要在十六进制中数到 17,你会说:“1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11。”脱掉一只鞋子,数一数直到你明白为止。

十六进制数字通常会在前面加上0x标记。数字 0x12 是十六进制的 18,相当于十进制的 18,而数字 18 则是普通的 18。如果一个十六进制数字没有前导的 0x 标记,那它出现在一个始终以十六进制输出的地方,比如 MAC 地址。字母 A 到 F 也是一种明显的标识,但并不完全可靠;许多十六进制数字根本没有字母,就像许多十进制数字没有奇数位一样。

当你在处理十六进制、十进制和二进制数字时,最简单的做法是拿出科学计算器。如今的中端或更高端计算器都具备在三种系统之间转换的功能,大多数软件计算器也有这种功能。

比特与字节

计算机系统通常以字节为单位工作,其中一个 8 位数字由单个字符表示。唯一的例外是网络栈,其中一切都以比特为单位。因此,我们在一台具有 10 兆比特网络连接的机器上有一个 5 兆字节的文件。不要把这两者混淆!

网络栈

网络栈是让主机通过网络与其他主机通信的软件。主机可以使用仅支持 IPv4 的网络栈、仅支持 IPv6 的网络栈,或者使用双栈配置。FreeBSD 默认启用这两者。

你可能对仅支持 IPv4 的网络栈很熟悉。在过去的 30 年中,大多数主机都使用 IPv4。仅支持 IPv4 的网络栈只能通过 IPv4 进行通信。今天,IPv4-only 的网络栈能够访问大部分互联网,尽管有一些故意的例外。但在几年后,这种情况将不再成立。

同样,只有 IPv6 的网络栈只能与 IPv6 主机通信。大多数大型互联网网站支持 IPv6,但你会发现少数令人烦恼的例外。^(3) 仅使用 IPv6 会使你无法访问一些流行的互联网网站。

当前最常见的服务器配置是双栈设置。客户端主机尝试同时使用 IPv4 和 IPv6,通常会偏向其中一个。微软 Windows 的最近几个版本更倾向于使用 IPv6。

我们首先将讨论更为熟悉的 IPv4,然后以 IPv4 为参考来讨论 IPv6。

IPv4 地址和子网掩码

IP 地址是分配给网络上特定节点的唯一 32 位数字。有些 IP 地址几乎是永久性的,比如分配给重要服务器的地址。其他地址则会根据网络需求变化,比如拨号客户端使用的地址。共享网络上的单独机器会获取相邻的 IP 地址。

不是将 32 位数字表示为一个单一的数字,而是将 IP 地址分为四个 8 位数字,通常以十进制数字表示。虽然 203.0.113.1 与 11001011.00000000.01110001.00000001 或单一数字 11001011000000000111000100000001 是相同的,但四个十进制数字是我们思维最容易处理的方式。

IP 地址是由互联网服务提供商以块的形式分配的。这些块通常非常小——比如说 16 个或 32 个 IP 地址。如果你的系统位于一个服务器农场中,你可能只会得到一个地址块中的几个 IP 地址。

网络掩码,也可以称为 前缀长度斜杠,是一个标签,表示分配给本地网络的 IP 地址块的大小。你的 IP 块的大小决定了你的网络掩码——或者说,你的网络掩码决定了你有多少个 IP 地址。如果你做过一段时间的网络工作,你一定见过网络掩码 255.255.255.0,并知道它与 256 个 IP 地址的块相关。你甚至可能知道,错误的网络掩码会导致你的系统无法正常工作。然而,在今天,这种简单的网络掩码变得越来越不常见。由 255 和 0 组成的网络掩码虽然易于观察,但会浪费 IP 地址。^(4) 而 IPv4 地址是一种极其稀缺的资源。

当你为服务器获取一块 IP 地址时,它可能看起来像 203.0.113.128/25\。这不是一个二进制数学课程,所以我不会让你画出来并做转换,但可以把 IP 地址看作是一个二进制数的字符串。在你的网络上,你可以改变最右边的位,但不能改变最左边的位。唯一的问题是,“分隔左右的界线在哪里?”这个边界不一定要在那些方便的 8 位线处,这些线把地址的十进制版本分开。前缀长度就是你网络上固定的位数。/25 意味着你有 25 个固定的位。你可以操作 7 个位。通过将固定位设为 1,将网络位设为 0,你可以得到一个十进制的网络掩码,如以下 /25 网络掩码示例:

11111111.11111111.11111111.10000000

二进制 11111111 是十进制 255,而 1000000 是 128\。你的网络掩码是 255.255.255.128\。如果你从二进制角度思考,这非常简单。你不需要每天都处理这个问题,但如果你不理解底层的二进制概念,十进制转换看起来可能会让人困惑。通过练习,你将学会识别某些十进制数字串作为合法的二进制转换。

所有这些在实践中意味着什么呢?首先,IP 地址块是以 2 的倍数发放的。如果你有 4 位可以操作,你将拥有 16 个 IP 地址(2 × 2 × 2 × 2 = 16)。如果你有 8 位可以操作,你将拥有 256 个地址(2⁸ = 256)。如果有人说你正好有 19 个 IP 地址,那你要么是和别人共享以太网,要么他们错了。

经常会看到主机的 IP 地址后附带其网络掩码——例如,198.51.100.4/26\。这给你提供了将主机接入本地网络所需的所有信息。(寻找默认网关是另一个问题,但根据惯例,它通常是该块中最高或最低的地址。)

十进制计算网络掩码

你可能不想反复进行十进制和二进制之间的转换。这样不仅不舒服,还增加了出错的机会。这里有一个技巧,可以让你在保持十进制思维的同时计算网络掩码。

你需要找到你网络中有多少个 IP 地址。这个数字几乎肯定是 2 的倍数,并且小于 256。将你拥有的 IP 地址数量从 256 中减去,这就是你的子网掩码的最后一个数字。你还需要识别合法的网络大小。如果你的 IP 地址是 203.0.113.100/26,你需要知道,/26 表示 26 个固定位,或者说 64 个 IP 地址。看一下你 IP 地址的最后一个数字,100。它显然不在 0 到 63 之间,但它在 64 到 127 之间。你 IP 块中的其他主机的 IP 地址范围从 203.0.113.64 到 203.0.113.127,而你的子网掩码是 255.255.255.192(256 – 64 = 192)。

在这一点上,我应该提到,子网掩码经常以十六进制数出现。你可能会觉得这整个事情无望,但为了简化你的生活,表 7-2 展示了子网掩码、IP 信息以及针对/24 及更小网络的相关信息。

表 7-2: 子网掩码和 IP 地址转换

前缀 二进制掩码 十进制掩码 十六进制掩码 可用 IP 地址
/24 00000000 255.255.255.0 0xffffff00 256
/25 10000000 255.255.255.128 0xffffff80 128
/26 11000000 255.255.255.192 0xffffffc0 64
/27 11100000 255.255.255.224 0xffffffe0 32
/28 11110000 255.255.255.240 0xfffffff0 16
/29 11111000 255.255.255.248 0xfffffff8 8
/30 11111100 255.255.255.252 0xfffffffc 4
/31 11111110 255.255.255.254 0xfffffffe 2
/32 11111111 255.255.255.255 0xffffffff 1

不可用的 IP 地址

你现在已经理解了斜杠、子网掩码和 IP 地址分配是如何协同工作的,举例来说,/28 有 16 个 IP 地址。不幸的是,你不能使用一个块中的所有 IP 地址。块中的第一个 IP 地址是网络号,它用于内部记账。

传统上,任何 IP 地址块中的最后一个数字被称为广播地址。根据最初的 IP 规格,网络中的每台机器都应该响应该地址的请求。这使得你可以通过 ping 广播地址,快速确定哪些 IP 地址正在使用。例如,在典型的/24 网络中,广播地址是x .y .z .255。 然而,在 1990 年代末,这一功能被转变为一种攻击技术,并且几乎所有操作系统和大多数网络设备默认禁用了它。^(5) 如果你需要此功能,可以将 sysctl net.inet.icmp.bmcastecho 设置为1。在大多数环境中,广播地址是一个浪费的 IP 地址。无论如何,你不能将网络中的第一个或最后一个 IP 地址分配给设备,否则会导致网络问题。(是的,这使得/31 网络变得无用。)某些系统优雅地失败;其他系统则失败得不那么优雅。不妨试试看——最好在下班后进行,除非你想有个好故事分享给下一个工作。

分配 IPv4 地址

你可能认为每台网络上的计算机都有一个 IP 地址,但这并不完全正确。每个网络接口都有一个 IP 地址。大多数计算机只有一个网络接口,所以对它们来说,二者没有区别。然而,如果你有多个网络卡,每个卡都有一个单独的 IP 地址。你还可以通过别名配置,在一张卡上拥有多个 IP 地址。另一方面,通过特殊配置,你可以将多张卡合并为一个网络接口,即使有多张卡,计算机仍然只有一个虚拟接口。虽然这些区别很小,但在故障排除时要记住它们。

IP 地址 127.0.0.1 始终附加在每个主机的回环接口上。它只能从本地机器访问。

IPv6 地址和子网

IPv4 的原始工程师认为 42.9 亿个 IP 地址足够整个世界使用。毕竟,计算机是昂贵的,只有军事和教育系统才会连接到互联网。并不是每个人都能拥有多个联网设备。

哎呀。

未使用的 IPv4 地址已经不再可用。IPv4 地址的价格正在上涨。最终,IPv4 地址的价格将高得大多数人无法承受。全世界不得不迈向 IPv4 的替代者——IP 版本 6。

电信网络和北美以外地区的部分地区已经广泛使用 IPv6。即使你的网络今天没有使用 IPv6,总有一天你会突然发现,上一周你就应该使用它了。

IPv6 基础

像 IPv4 一样,IPv6 是一种网络层协议。TCP、UDP、ICMP 和其他协议都运行在它之上。回想一下,IPv4 使用 32 位地址,通常表示为四组十进制数字,每组从 0 到 255——例如,203.0.113.13。IPv6 使用 128 位地址,表示为八组四个十六进制字符,每组之间用冒号分隔——例如,2001:db8:5c00:0:90ff:bad:c0de:cafe。

128 位的地址空间是难以想象的巨大,但让我们试着想象一下。数一数每一个曾经活过的人的数量。然后计算他们每个人体内的细胞数量——不仅仅是身体中的细胞,还有身体里的所有细菌细胞。IPv6 足够宽敞,可以为这些细胞中的每一个分配一个比整个 IPv4 地址空间还要大的地址空间。

好消息是,你不需要重新学习网络的基础知识。主机需要一个 IP 地址、一个子网掩码和一个默认网关。你几乎可以——几乎——将 IPv6 地址替换为 IPv4 地址,并观察一切正常工作。一个 Web 服务器并不关心它是绑定在 203.0.113.13 的 80 端口,还是绑定在 2001:db8:5c00:0:90ff:bad:c0de:cafe 的 80 端口。服务器接受收到的请求并作出相应的响应。话虽如此,软件的确需要做一些轻微的更改,因为我们的 Web 服务器必须能够记录来自 IPv4 和 IPv6 地址的连接。这些更改有广泛的影响,我们将在几十年内发现新的边缘案例。但总体而言,一旦你理解了 IPv6 的新规则,你所有的网络知识都是适用的。

理解 IPv6 地址

如前所述,IPv6 地址是 128 位,表示为八组用冒号分隔的每组四个十六进制字符。与十进制的 IP 地址类似,每组中的前导零不需要显示。地址 2001:db8:5c00:0:90ff:bad:c0de:cafe 可以写成 2001:0db8:5c00:0000:90ff:0bad:c0de:cafe,但就像我们不会写 203.000.113.013 一样,我们会去掉 IPv6 地址中的前导零。

IPv6 地址通常包含长串的零,这是由于 IPv6 子网的方式所致。当我写这篇文章时,Sprint 网站的 IPv6 地址是 2600:0:0:0:0:0:0:0。连续的零组会被两个冒号替代。你可以将这个 IP 地址显示为 2600::。但是,你只能在每个地址中做一次双冒号替换。像 2001:🅰️:1 这样的地址将是模棱两可的。2001:🅰️:1 代表 2001:0:0:0:0🅰️0:1,2001:0:0:0🅰️0:0:1,2001:0:0🅰️0:0:0:1,还是 2001:0🅰️0:0:0:0:0:1?没有办法确定。

你可能已经见过 IPv4 地址后面加上端口号,例如 203.0.113.13:80。将这种术语与 IPv6 地址结合使用会使它们更加丑陋,并且让所有人感到困惑。像 2001:db8:5c00:0:90ff:bad:c0de:cafe:80 这样的 IP 地址和端口组合并不模糊,但除非你非常仔细阅读,否则你可能会以为它是一个以 80 结尾的 IP 地址。如果你在表示 IP 和端口组合时,应该将地址用方括号括起来,如 [2001:db8:5c00:0:90ff:bad:c0de:cafe]:80。

IPv6 子网

IPv6 地址每 16 位用冒号分隔,因此划分网络的自然方式通常是/16、/32、/48、/64、/80、/96 和/112。原始的 IPv6 标准建议仅在这些边界进行子网划分(重复了 IPv4 的一个重大错误),但这一做法正被越来越多的人拒绝,转而支持类似 IPv4 风格的任意位置子网划分。IPv6 子网总是用斜线表示,也称为前缀长度,所以你不会看到类似 IPv4 的网掩码 ffff:ffff:ffff:ffff::。

ISP 通常会分配一个/32 或/48 地址块,期望为终端用户网络(例如典型的客户端)分配一个/64 的网络。一个/64 有 2⁶⁴个子网,即 18,446,744,073,709,551,616 个地址。如果你家里或办公室的 IP 地址用完了,你需要停止为每个蓝莓设备单独联网。

当你在 16 位边界进行子网划分时,每个网络会有 65,536 个下一级子网。一个/32 包含 65,536 个/48 网络,一个/48 包含 65,536 个/64 网络。

这是一个冗长的解释,说明了为什么我不提供 IPv6 子网和网络大小的便捷图表。你可以在网上搜索“IPv6 子网计算器”来使用互联网上的计算工具。

链路本地地址

fe8x:开头的地址(其中x是任何十六进制字符)是本地接口的地址。每个接口都有这样的链路本地地址,这些地址仅在特定的本地网络上有效。即使一个 IPv6 网络没有路由器,直接附加到本地网络的主机仍然可以使用这些本地地址相互发现并进行通信。链路本地网络始终是/64 子网。你会在其他接口以及完全与本地网络断开的网络上看到相同的 IPv6 子网。这是可以的,这些地址只对链路本地有效。例如,这是一个来自测试机器的链路本地地址。

fe80::bad:c0de:cafe%vtnet0

这个接口的链路本地地址是fe80::bad:c0de:cafe。结尾的%vtnet0表示这个地址只在接口vtnet0本地有效,无法在机器上的任何其他接口上使用。如果你的机器有一个vtnet1接口,而该网络上的主机试图访问地址fe80::bad:c0de:cafe,这台机器不会响应。这个地址仅对直接附加到vtnet0接口的网络段上的主机有效。

你可能会注意到,这个链路本地地址和该接口的公共 IPv6 地址有一部分是相同的。这是因为自动配置的 IPv6 地址通常是根据接口的物理地址计算出来的;无论该自动配置的地址是公共的还是链路本地的,都没有关系。

分配 IPv6 地址

在/64 或更大的网络上,IPv6 客户端通常可以通过路由器发现自动配置自己的网络。路由器发现类似于简化版的 DHCP 服务。路由器广播网关和子网信息,主机自我配置以使用这些信息。

现代路由器发现版本包括非常基础的 DHCP 风格选项,如 DNS 服务器。然而,并非所有的 IPv6 提供商在其路由器发现配置中都包括这些选项。如果你想为手机或无盘主机提供复杂的自动配置,或者如果你的提供商没有在其配置中提供 DNS 信息,你将需要设置一个 IPv6 DHCP 服务器。

服务器不应使用 IPv6 自动配置。服务器通常需要一个静态 IP,即使是在 IPv6 中也是如此。

网络上小于 /64 的主机必须手动配置。

地址 ::1 始终表示本地主机,并被分配给回环地址。

TCP/IP 基础

现在你已经对 IP 系统的工作原理有了简单的概述,让我们更深入地考虑一下最常见的网络协议。互联网上占主导地位的传输协议是传输控制协议/因特网协议,简称 TCP/IP。尽管 TCP 是一种传输协议,而 IP 是一种网络协议,但它们紧密交织在一起,通常被视为一个整体。

我们将从最简单的 ICMP 开始,然后讨论 UDP 和 TCP。这些协议都可以在 IPv4 和 IPv6 上运行。虽然每种协议的版本根据底层 IP 协议有所不同,但它们的行为基本相同。

ICMP

因特网控制消息协议(ICMP)是用于传输路由和可用性消息的标准协议。像 ping(8) 和 traceroute(8) 这样的工具使用 ICMP 来收集它们的结果。IPv4 和 IPv6 有稍微不同版本的 ICMP,有时被称为 ICMPv4ICMPv6

尽管有人声称出于安全原因必须阻止 ICMP,但 ICMP 和我们更熟悉的协议 TCP 和 UDP 一样具有多样性。正确的 IPv4 网络性能需要大量的 ICMPv4。如果你觉得必须阻止 ICMP,应该选择性地进行。例如,阻止源抑制消息会破坏路径最大传输单元(pMTU)发现,就像跌进一箱破碎的玻璃和生锈的钉子里。如果你不明白这句话的意思,那么不要阻止 ICMP。

IPv6 在没有 ICMPv6 的情况下无法正常工作,因为 IPv6 不支持数据包分片。如果你使用 IPv6,绝对不要完全阻止 ICMPv6。阻止 ICMPv6 的某些部分而不破坏你的网络需要仔细研究和测试。

UDP

用户数据报协议(UDP)是最基础的数据传输协议,运行在 IP 之上。它没有错误处理,最小的完整性验证,并且完全没有防止数据丢失的机制。尽管有这些缺点,UDP 仍然是某些类型数据传输的一个不错选择,许多重要的互联网服务依赖于它。

当主机通过 UDP 传输数据时,发送方无法知道数据是否到达了目的地。接收 UDP 数据的程序只是监听网络,并接受到达的任何数据。当程序通过 UDP 接收数据时,它无法验证数据的来源——尽管 UDP 数据包包含源地址,但这个地址很容易被伪造。这就是为什么 UDP 被称为无连接无状态的原因。

既然有这么多缺点,为什么还要使用 UDP 呢?使用 UDP 的应用程序通常会有自己的错误校正处理方法,这些方法与 TCP 等协议提供的默认机制不太兼容。例如,简单的客户端 DNS 查询必须在几秒钟内超时,否则用户会打电话到帮助台抱怨。TCP 连接在两分钟后才会超时。由于计算机希望更快地处理失败的 DNS 请求,简单的 DNS 查询使用 UDP。在需要传输大量数据的 DNS 情况下(例如,区域传送),它会智能地切换到 TCP。实时流数据,如视频会议,也使用 UDP。如果在实时视频会议中错过了一些像素,重新传输这些数据只会增加拥堵。毕竟,你无法回到过去来填补那些丢失的图片部分!几乎所有其他使用 UDP 的网络应用程序背后都可以找到类似的理由。

由于 UDP 协议本身在连接到端口时不会返回任何信息,因此没有可靠的方法可以远程测试 UDP 端口是否可达(尽管像 nmap 这样的工具会尝试执行此操作)。

UDP 也是一种数据报协议,这意味着每个网络传输都是完整的、自包含的,并作为一个单独的整体单元接收。虽然应用程序可能不会将单个 UDP 数据包视为一个完整的请求,但网络会这样做。TCP 则完全不同。

TCP

传输控制协议(TCP)包括如错误修正和恢复等巧妙功能。接收方必须确认每个收到的数据包;否则,发送方会重新传输任何未被确认的数据包。使用 TCP 的应用程序可以期待可靠的数据传输。这使得 TCP 成为连接型有状态协议,与 UDP 截然不同。

TCP 也是一种流式协议,这意味着一个请求可能会被拆分成多个网络数据包。虽然发送方可能会依次传输多个数据块,但接收方可能会收到乱序或分片的数据。接收方必须跟踪这些数据块,并将其正确组装,以完成网络事务。

为了让两台主机交换 TCP 数据,它们必须建立一个通道来传输数据。一台主机请求连接,另一台主机响应请求,然后第一台主机开始传输。这个设置过程被称为三次握手。具体细节现在不重要,但你应该知道这个过程的存在。类似地,一旦传输完成,系统必须执行一定的操作来拆除连接。

TCP 常常被应用程序使用,例如邮件程序、FTP 客户端和网页浏览器,因为它有一个相对通用的超时和传输功能集。

协议如何协同工作

你可以把网络协议栈比作和家人一起坐在节日聚餐桌前。数据链路层(在 IPv4 通过以太网时是 ARP)让你看到桌上的每个人。IP 给每个人分配一个独特的座位,除了那三个用钢琴凳 NAT 的年轻侄子。ICMP 提供基本的路由信息,例如,“到豌豆的最快方式是让克里斯叔叔递给你。”TCP 是你递给别人一道菜,另一个人必须说“谢谢”,你才能放手。最后,UDP 就像是把一个面包滚向贝蒂阿姨;她可能会接住,可能会从额头弹开,或者可能会被在饭桌旁等机会的狗从空中抓住。

传输协议端口

你有没有注意到计算机有太多的端口?我们要在这道汤中加入 TCP 和 UDP 端口。传输协议端口允许一个服务器通过一个传输协议提供许多不同的服务,实现机器之间连接的复用。

当一个网络服务器程序启动时,它会附加或绑定到一个或多个逻辑端口。逻辑端口只是一个从 1 到 65535 的任意数字。例如,互联网邮件服务器绑定到 TCP 端口 25。每个到达系统的 TCP 或 UDP 数据包都有一个字段,指示它希望的目标端口。每个传入请求都会标记一个目标端口号。如果传入请求请求端口 25,它会连接到在该端口上监听的邮件服务器。这意味着其他程序可以在不同的端口上运行,客户端可以与这些不同的端口通信,除非是系统管理员,否则没有人会感到困惑。

/etc/services 文件包含了一份端口号及其常用服务的列表。几乎可以在任何端口上运行任何服务,但这样做会让其他试图连接到你系统的互联网主机感到困惑。如果有人尝试向你发送电子邮件,他们的邮件程序会自动连接到你系统的端口 25。如果你把邮件服务运行在端口 77,而你在端口 25 上有一个 Web 服务器,你将永远收不到邮件,且你的 Web 服务器将开始收到垃圾邮件。/etc/services 文件有一个非常简单的五列格式。

➊qotd  ➋17/➌tcp  ➍quote        ➎#Quote of the Day

这是qotd服务的条目 ➊,它在 TCP 协议中运行在 17 端口 ➋。它也被称为quote服务 ➍。最后,我们有一个评论 ➎,提供了更多的细节;显然,qotd代表每日一句。服务在 TCP 和 UDP 协议中都分配相同的端口号,尽管它们通常只在其中一个协议上运行而不是另一个——例如,qotd 有 17/tcp 和 17/udp 端口。

许多服务器程序会读取/etc/services文件,以了解启动时应该绑定到哪个端口,而客户端程序会读取/etc/services文件,以了解它们应该尝试连接到哪个端口。如果你在非标准端口上运行服务器,可能需要编辑这个文件来告知服务器应该连接到哪个端口。

正如所有标准一样,常常有充分的理由去打破规则。SSH 守护进程 sshd 通常监听 22/tcp 端口,但我曾出于各种原因将其运行在 23(telnet)、80(HTTP)和 443(HTTPS)端口上。配置这一点取决于你所使用的服务器程序。

保留端口

TCP 和 UDP 协议中低于 1024 的端口被称为保留端口。这些端口只分配给核心互联网基础设施和重要服务,如 DNS、SSH、HTTP、LDAP 等——这些服务应该仅由系统或网络管理员合法提供。只有具有根用户权限的程序才能绑定到这些低编号的端口。用户可以在系统策略允许的情况下,在高编号端口上提供比如游戏服务器等服务——但这与设置一个公开可见并声明机器主要用途是游戏服务器的正式网页有些不同!这些核心协议的端口分配通常是固定的。

你可以使用 sysctl 命令net.inet.ip.portrange.reservedhighnet.inet.ip.portrange.reservedlow查看和更改保留端口。

有时,某些人认为他们可以禁用这个“仅根用户绑定”功能,从而提高系统的安全性——毕竟,如果你的应用程序可以作为普通用户而非根用户运行,这是否会增加系统的安全性呢?大多数在保留端口上运行的程序实际上是以根用户身份启动,绑定端口后,再将权限降级为一个权限比普通用户更少的特殊限制用户。这些程序设计上就是以根用户启动,并且在以普通用户身份运行时通常会表现得不同。一些程序,如 Apache Web 服务器,编写时已考虑到可以由非根用户安全启动,但其他程序则没有。

理解以太网

以太网在企业和家庭网络中极为流行,也是 FreeBSD 系统最常见的连接介质。以太网是一个共享网络;许多不同的机器可以连接到同一个以太网,并可以直接相互通信。这使得以太网相较于其他网络协议具有很大优势,但以太网也有物理距离限制,这使得它仅适用于办公室、共置设施和其他相对较小的网络。^(6)

多种不同的物理介质在过去多年里都支持以太网。曾几何时,大多数以太网电缆都是厚重的同轴电缆。如今,大多数则是相对较薄的 CAT6 电缆,内部有八根非常细的导线。你还可能遇到光纤或无线电上的以太网。为了便于讨论,我们假设你使用的是 CAT6 或更高规格的电缆,这是目前最流行的选择。无论你使用什么物理介质,以太网的理论并不会改变——记住,物理层已经被抽象化。

协议和硬件

以太网是一种广播协议,这意味着你在网络上发送的每个数据包都可以发送到网络上的每个工作站。(注意,我说的是可以发送;一些以太网硬件限制了这些广播的接收者。)你的网卡或其设备驱动程序将目标是你计算机的数据与目标是其他计算机的数据分开。以太网广播特性的一种副作用是,你可以窃听其他计算机的网络流量。虽然这在诊断问题时非常有用,但它也是一个安全隐患。在传统的以太网上捕获明文密码是很简单的。所有主机可以直接与所有其他主机通信而不涉及路由器的以太网部分被称为广播域

以太网段通过集线器或交换机连接。以太网集线器是一个中央硬件设备,用来物理连接多个其他以太网设备。集线器只是简单地将收到的所有以太网帧转发给连接到网络的其他设备。集线器将收到的所有以太网流量广播到每个附加的主机和其他连接的集线器。每个主机负责过滤掉它不需要的流量。集线器是老式的以太网设备,现在很少见了。

交换机已经大多取代了集线器。交换机就像集线器,但它会过滤掉发送到每个主机的流量。它识别附加设备的物理地址,并在大多数情况下,仅将帧转发到它们所指向的设备。由于每个以太网主机的带宽是有限的,交换通过减少每个主机必须处理的流量,来减轻单个系统的负担。

交换机故障

交换机会发生故障,尽管思科公司可能不希望你这样认为。一些故障很明显,例如当神秘的黑烟从设备后面泄漏出来时。当交换机失去它的“魔法烟雾”时,它就停止工作了。还有一些故障较为微妙,看起来交换机依然在工作。

每个交换机制造商必须决定如何处理细微的错误。交换机可以选择关闭,直到有人处理,或者尝试向管理员发出警告并继续尽力转发数据包。如果你是供应商,选择显而易见——你尽最大努力让你的客户不认为你的交换机是垃圾。这意味着你的交换机可能会开始像集线器一样工作,而你可能并不知道。坏消息是,如果你依赖交换机来防止机密信息泄漏,你将注定失望。已经有不止一台交换机以这种方式失败过,所以当它发生在你身上时,别太惊讶。

安装一个 syslog 服务器(参见第二十一章)并让你的交换机将日志发送到它,可以降低这个风险。虽然日志记录无法防止交换机故障,但它将简化当交换机开始出现问题时,倾听其抱怨的过程。

以太网速度

以太网最初只支持每秒几兆比特的速度,但现在已经扩展到能够处理数十吉比特的速度。大多数以太网卡的速度是千兆,意味着它们每秒可以处理一个千兆比特的数据,但在高速应用中你会找到一些 10Gbs 或 100Gbs 的卡。如果一张卡被标记为千兆,并不意味着它实际上能够处理这么大的流量——我见过千兆卡在只有十分之一带宽的情况下就卡住了。当你需要推送带宽时,卡的质量很重要,而当你推送大量带宽时,整个计算机的质量也非常重要。

让交换机和网卡通过自动协商自行协商它们的设置。虽然一些经验丰富的人可能记得在旧版以太网卡上禁用自动协商,但千兆及更高速率的以太网要求自动协商才能正常工作。

MAC 地址

每个以太网卡都有一个唯一的标识符,即媒体访问控制(MAC)地址。这个 48 位的数字有时被称为以太网地址物理地址。当一个系统向另一个主机发送数据时,它首先广播一个以太网请求,询问:“哪个 MAC 地址负责这个 IP 地址?”如果主机用它的 MAC 地址作出回应,进一步的数据就会传输到那个 MAC 地址。

IPv4 使用地址解析协议(ARP)将 IP 地址映射到主机。使用 arp(8)命令查看你的 FreeBSD 系统对 ARP 表的了解。最常见的用法是arp -a命令,它显示计算机已知的所有 MAC 地址和主机名。

# arp -a
gw.blackhelicopters.org (198.51.100.1) at 00:00:93:34:4e:78 on igb0 [ethernet]
sipura.blackhelicopters.org (198.51.100.5) at 00:00:93:c2:0f:8c on igb0 [ethernet]

这个完整的 ARP 信息列表被称为ARP 表,或MAC 表。(术语 MAC 和 ARP 常常可以互换使用,所以不用太担心这个问题。)在这里,我们看到主机gw.blackhelicopters.org的 IP 地址是 198.51.100.1,MAC 地址是 00:00:93:34:4e:78,并且你可以通过本地系统的接口igb0访问这些主机。

如果 MAC 地址显示为incomplete,则表示无法在本地以太网中联系到该主机。在这种情况下,检查你的物理层(电缆)、远程系统以及两者的配置。

IPv6 使用邻居发现协议(NDP)将 IPv6 地址映射到 MAC 地址。这是一个与 ARP 不同的独立协议,用于包括路由器发现。使用 ndp(8)查看主机的 MAC 表和对应的 IPv6 地址。输出故意与 arp(1)类似。

# ndp -a
Neighbor                          Linklayer Address  Netif  Expire    S Flags
fe80::fc25:90ff:fee8:1270%vtnet0  fe:25:90:e8:12:70  vtnet0 4s        R R
www.michaelwlucas.com             00:25:90:e8:12:70  vtnet0 permanent R
fe80::225:90ff:fee8:1270%vtnet0   00:25:90:e8:12:70  vtnet0 permanent R

输出故意与 arp(8)类似,但略显表格化。Neighbor列显示每个邻居的 IPv6 地址、主机名或链路本地地址。Linklayer Address列显示邻居的 MAC 地址。Netif列显示该主机所连接的网络接口,而Expire列显示缓存条目将过期的时间。S(状态)列提供有关条目的进一步信息。R状态表示主机是可达的,而I(incomplete)表示主机不可达。你可能看到的唯一Flags条目是R,表示该主机正在将自己广告为路由器。有关更多状态和标志,请参见 ndp(8)。

为什么有两个独立的命令?arp(8)和 ndp(8)都用于将 IP 地址映射到 MAC 地址。有些主机可能仅通过其中一个协议或另一个协议可用。仅支持 IPv6 的主机不会出现在你的 ARP 表中,而仅支持 IPv4 的主机则不会出现在 NDP 表中。

对于 arp(8)和 ndp(8)命令,-n标志会关闭主机名查找。当你在调试网络问题并且无法获取 DNS 解析时,这个功能非常有用。

现在你了解了网络的工作原理,配置互联网连接就非常简单了。

第八章:配置网络**

image

现在您已经掌握了足够的网络知识,可以配置网络连接了。虽然 FreeBSD 支持许多不同的协议,但我们将专注于几乎无处不在的以太网连接,通常通过 CAT5 或 CAT6 电缆提供。^(1)

我们将从确保主机能够连接到网络并访问其他互联网主机的基本步骤开始。然而,原始的 TCP/IP 连接不足够;您还需要能够将主机名解析为 IP 地址的功能,因此接下来我们将介绍这一点。然后,我们将讨论网络活动的测量、性能、VLAN 以及链路聚合。

然而,在您做任何事情之前,您需要一些信息。

网络先决条件

如果您的网络提供动态主机配置协议(DHCP),您可以作为客户端连接到网络,而无需了解网络的任何信息。然而,在服务器上,静态 IP 地址更加合适。虽然安装程序会为您配置网络,但最终每台服务器都需要进行更改。IPv4 和 IPv6 都需要以下信息:

  • 一个 IP 地址

  • 该 IP 地址和协议的子网掩码

  • 默认网关的 IP 地址

拥有这些信息后,使用 ifconfig(8)和 route(8)将系统连接到网络,并将配置保存到/etc/rc.conf中。

使用 ifconfig(8)配置更改

ifconfig(8)程序显示您计算机上的接口,并允许您配置它们。首先,通过不带任何参数运行 ifconfig(8)来列出系统上现有的接口:

# ifconfig
➊em0: flags=8843<➋UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        options=85259b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM,TSO4,
LRO,WOL_MAGIC,VLAN_HWFILTER,VLAN_HWTSO>
        inet ➌203.0.113.43 netmask 0xfffffff0 broadcast 198.51.100.47
        inet6 ➍fe80::225:90ff:fee8:1270%em0 prefixlen 64 scopeid 0x1
        inet6 ➎2001:db8::bad:c0de:cafe prefixlen 64
        ether ➏00:25:90:db:d5:94
        media: ➐Ethernet autoselect (1000baseTX <full-duplex>)
        status: ➑active

rl0: flags=8802<BROADCAST,SIMPLEX,MULTICAST> mtu 1500
        options=8<VLAN_MTU>
        ether 00:20:ed:72:3b:5f
        media: Ethernet autoselect (10baseT/UTP)
        status: ➒no carrier
➓lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
        options=600003<RXCSUM,TXCSUM,RXCSUM_IPV6,TXCSUM_IPV6>
        inet6 ::1 prefixlen 128
        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x2
        inet 127.0.0.1 netmask 0xff000000
        nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>

我们的第一个网络接口是 em0 ➊,即使用 em(4)驱动程序的第一张网卡。em(4)的手册页显示,这是 Intel PRO/1000 卡。接下来,您将看到有关此卡的基本信息 ➋,包括它处于 UP 状态,意味着它正在工作或正在尝试工作。它被分配了 IPv4 地址 203.0.113.43 ➌和子网掩码 0xfffffff0(或 255.255.255.240,参见表 7-2)。这张卡有两个 IPv6 地址,链路本地地址(以 fe80 开头) ➍和全局 IPv6 地址 ➎。您还会看到 MAC 地址 ➏和连接速度 ➐。最后,status条目显示该卡是活动的 ➑:电缆已插入,我们有连接指示灯。

第二张卡片,rl0,几乎没有任何相关信息。一个关键事实是no carrier信号 ➒:它没有插入,并且没有连接指示灯。这张卡片没有被使用。

最后,我们有 lo0 接口➓,即回环接口。该接口在每台机器上都有 IPv4 地址 127.0.0.1 和 IPv6 地址::1。这个回环地址用于机器与自己通信。这是一个标准的软件接口,没有关联的物理硬件。不要尝试删除回环接口,也不要更改其 IP 地址——如果这么做,系统会以一种有趣的方式出错。FreeBSD 还支持其他软件接口,如 disc(4)、tap(4)、gif(4)等。

向接口添加 IP 地址

安装过程将会配置所有在安装时可用的网络卡。如果在安装过程中没有配置所有网卡的网络,或者在安装完成后你添加或移除网络卡,你可以通过 ifconfig(8)为网卡分配 IP 地址。你需要网卡的分配 IP 地址和子网掩码。

# ifconfig interface-name inet IP-address netmask

例如,如果你的网卡是 em0,IP 地址是 203.0.113.250,子网掩码是 255.255.255.0,你可以输入:

# ifconfig em0 inet 203.0.113.250 255.255.255.0

如上所示,可以以点分十进制表示法或十六进制格式(0xffffff00)指定子网掩码。最简单的方式可能是使用斜杠表示法,如下所示:

# ifconfig em0 inet 203.0.113.250/24

要配置 IPv6 地址,在接口名称和地址之间添加inet6关键字。

# ifconfig em0 inet6 2001:db8::bad:c0de:cafe/64

ifconfig(8)程序还可以执行其他网络卡所需的配置,帮助你绕过硬件问题,比如各种类型的校验和卸载功能,设置子千兆网卡的媒体类型和双工模式等。你可以在驱动程序和 ifconfig(8)的手册页中找到支持的选项。这里,我在设置 IP 地址的同时,禁用了 em0 接口的校验和卸载和 TCP 分段卸载。

# ifconfig em0 inet 203.0.113.250/24 -tso -rxcsum

为了在重启后保持该配置,向/etc/rc.conf中添加一行,告诉系统在启动时配置该网卡。IPv4 的配置项形式是ifconfig_接口名称="ifconfig 参数"。例如,配置空闲的 rl0 网卡时需要像下面这样添加配置项:

ifconfig_rl0 ="inet 203.0.113.250/24"

IPv6 的配置项形式是ifconfig_接口名称_ipv6="ifconfig 参数"

ifconfig_rl0_ipv6="2001:db8::bad:c0de:cafe/64"

一旦你有了一个正常工作的接口配置,将你的 ifconfig(8)参数复制到/etc/rc.conf配置文件中。

测试你的接口

现在你的接口已分配了 IP 地址,尝试 ping 一下默认网关的 IPv4 地址。如果你得到了响应,如下例所示,那么你就连接到了本地网络。用 CTRL-C 中断 ping 命令。

# ping 203.0.113.1
PING 203.0.113.1 (203.0.113.1): 56 data bytes
64 bytes from 203.0.113.1: icmp_seq=0 ttl=64 time=1.701 ms
64 bytes from 203.0.113.1: icmp_seq=1 ttl=64 time=1.436 ms
^C
--- 203.0.113.1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 1.436/1.569/1.701/0.133 ms

对于 IPv6,使用 ping6(8)而不是 ping(8)。如果你使用路由器发现功能,默认路由几乎总是一个链路本地地址。

# ping6 2001:db8::1
PING6(56=40+8+8 bytes) 2001:db8::bad:c0de:cafe --> 2001:db8::1
16 bytes from 2001:db8::1, icmp_seq=0 hlim=64 time=0.191 ms
16 bytes from 2001:db8::1, icmp_seq=1 hlim=64 time=0.186 ms
16 bytes from 2001:db8::1, icmp_seq=2 hlim=64 time=0.197 ms
--snip--

如果没有得到任何回应,说明你的网络连接没有正常工作。要么是连接有问题(检查你的电缆和连接指示灯),要么是网卡配置错误。

设置默认路由

默认路由是系统将所有非本地网络流量发送到的地址。如果你能 ping 通默认路由的 IPv4 地址,可以通过 route(8)设置它。

# route add default 203.0.113.1

就是这样!现在您应该能够 ping 任何公共 IPv4 地址。

添加默认的 IPv6 路由非常相似,但您需要添加 -6 命令行标志来更改 IPv6 路由表。

# route -6 add default 2001:db8::1

如果在系统安装时没有选择名称服务器,则必须使用 IP 地址而不是主机名。

一旦您有了有效的默认路由器,可以通过在 /etc/rc.conf 中添加适当的 defaultrouteripv6_defaultrouter 条目,使其在重启后仍然有效:

defaultrouter="203.0.113.1"
ipv6_defaultrouter="2001:db8::1"

一个接口上的多个 IP 地址

一个 FreeBSD 系统可以在一个接口上响应多个 IP 地址。这对 Jail(参见 第二十二章)尤其有用。使用 ifconfig(8) 和 inetalias 关键字为一个接口指定额外的 IPv4 地址。IPv4 别名的子网掩码总是 /32,无论主地址使用的网络地址块大小如何。

# ifconfig em0 inet alias 203.0.113.225/32

IPv6 别名使用它们所在子网的实际前缀长度(斜杠)。请确保使用 inet6 关键字。

# ifconfig em0 inet6 alias 2001:db8::bad:c0de:caff/64

一旦为接口添加了别名,额外的 IP 地址将在 ifconfig(8) 输出中显示。主 IP 地址总是首先出现,别名随后列出。

   # ifconfig fxp0
   fxp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
           options=b<RXCSUM,TXCSUM,VLAN_MTU>
           inet6 fe80::225:90ff:fee8:1270%vtnet0 prefixlen 64 scopeid 0x1
➊          inet6 2001:db8::bad:c0de:cafe prefixlen 64
           inet6 2001:db8::bad:c0de:caff prefixlen 64
           inet 203.0.113.250 netmask 0xffffff00 broadcast 203.0.113.255
➋          inet 203.0.113.225 netmask 0xffffffff broadcast 203.0.113.255
           ether 00:02:b3:63:e4:1d
   --snip--

在这里,我们看到了全新的 IPv4 ➊ 和 IPv6 ➋ 别名。对您的别名地址进行 ping 的主机会从此服务器获得响应。

一旦您将别名设置按预期工作,可以通过在 /etc/rc.conf 中添加额外的 ifconfig 语句,使其在重启后仍然有效:

ifconfig_em0_alias0="inet 203.0.113.225/32"
ifconfig_em0_alias1="inet6 2001:db8::bad:c0de:caff/64"

这个条目和标准 rc.conf 中的“这是我的 IP 地址”条目之间唯一的真正区别是 alias0alias1 部分。alias 关键字告诉 FreeBSD 这是一个别名 IP,01 是分配给每个别名的唯一编号。每个在 /etc/rc.conf 中设置的别名必须具有唯一的编号,并且该编号必须是连续的。如果跳过了某个编号,则跳过的后面的别名在启动时不会被安装。这是我见过的最常见的接口配置错误。

许多守护进程,如 inetd(8) 和 sshd(8),可以绑定到一个单一的地址(参见 第二十章),因此您可以使用多个地址在同一服务器上运行多个相同程序的实例。

别名和外部连接

所有来自您的 FreeBSD 系统的连接都使用系统的真实 IP 地址。您可能将 2,000 个地址绑定到一张网卡上,但当您从该机器 ssh 时,连接将来自主 IP 地址。在编写防火墙规则和其他访问控制过滤器时,请记住这一点。Jail 从 Jail 的 IP 地址发起所有连接,但我们直到 第二十二章 才会讨论 Jail。

重命名接口

FreeBSD 根据网络卡所使用的设备驱动程序为其网络接口命名。这是 Unix 世界中的一项悠久传统,也是大多数工业操作系统的常见行为。一些操作系统通过接口类型来命名其网络接口——例如,Linux 将其以太网接口命名为eth0eth1,以此类推。有时,重新命名接口是有意义的,要么是为了遵循内部标准,要么是为了使其功能更加明显。例如,我有一台设备,拥有 12 个网络接口,每个接口都连接到不同的网络。每个网络都有一个名称,如testQA等。将这些网络接口重新命名为与附加网络相匹配是合理的。

虽然 FreeBSD 在接口名称方面比较灵活,但一些软件则不然——它假设网络接口名称是一个短单词,后面跟着一个数字。预计在不久的将来这种情况不会改变,因此最佳实践是使用以数字结尾的短接口名称。使用 ifconfig(8) 的 name 关键字来重命名接口。例如,要将 em1 重命名为 test1,可以运行:

# ifconfig em1 name test1

运行 ifconfig(8) 不带参数将显示你已重命名该接口。

--snip--
test1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        options=b<RXCSUM,TXCSUM,VLAN_MTU>
--snip--

使用 ifconfig_interface_name 选项在 /etc/rc.conf 中使此更改永久生效。

ifconfig_em1_name="test1"

FreeBSD 在启动过程中早期就重命名接口,在设置 IP 地址或其他值之前。这意味着任何进一步的接口配置必须引用新接口名称,而不是旧接口名称。对重命名接口进行完整配置,包括 IP 地址和别名,应该如下所示:

ifconfig_em1_name="dmz2"
ifconfig_dmz2="inet 203.0.113.2 netmask 255.255.255.0"
ifconfig_dmz2_alias0="inet 203.0.113.3"

DHCP

很少有网络使用 DHCP 来处理所有设备,包括服务器。DHCP 服务器会为你设置服务器的 IP 地址、子网掩码、域名服务器和默认网关。如果你的网络管理员通过 DHCP 配置服务器,你可以告诉网络卡通过 DHCP 获取配置,方法如下:

ifconfig_em0="DHCP"

重启!

现在,你已经完全配置了网络接口,务必重启以测试你对/etc/rc.conf所做的任何更改。如果 FreeBSD 在 /etc/rc.conf 中发现错误,特别是在网络配置方面,你将遇到远程访问系统的问题。在受控条件下发现打字错误总比在你睡觉的时候发现要好得多。

如果你觉得挑战刺激,你可以运行 service netif restart 并指定接口名称来仅重新配置单个接口。

# service netif restart em0

如果省略接口名称,则会重启所有接口。这不是一个完美的测试,但它可以捕捉到一些愚蠢的错误。重启始终是最好的测试。

域名服务

域名服务,或DNS,是那些安静的幕后服务之一,通常得不到应有的关注。尽管大多数用户从未听说过它,DNS 却是我们所知的互联网能够正常工作的原因。DNS 也叫做名称服务,它提供了主机名与 IP 地址之间的映射。它还提供了反向映射,即 IP 地址到主机名的映射。没有 DNS,网页浏览器和电子邮件程序无法使用像* www.michaelwlucas.com www.nostarch.com 这样的简便主机名;相反,你将不得不输入一些令人震惊的东西,如https://2001:19f0:5c00:9041:225:90ff:fee8:1270*来浏览网页。这将极大地减少互联网的普及率。^(2) 对大多数终端用户来说,DNS 故障就是互联网故障,故事到此为止。虽然我们不会讨论如何构建自己的权威型名称服务器,但我们必须讲解如何配置你的服务器以使用 DNS。

一个用于在互联网上搜寻 DNS 映射的主机被称为名称服务器,或DNS 服务器。DNS 服务器并不难以运行,但大多数个人不需要一个。只有那些运行自己服务器的组织(以及像我一样在地下室有数十台主机的疯子)才需要 DNS 服务器。名称服务器有两种类型,权威型和递归型。

权威型名称服务器提供 DNS 映射,使公众能够找到组织的名称服务器。作为* michaelwlucas.com *的运营者,我必须为该域提供权威型名称服务器并让公众查询它们。这些权威型名称服务器只回答关于我管理的域的查询。配置权威型名称服务器超出了本书的范围。

递归型名称服务器处理客户端请求。当你尝试浏览* www.michaelwlucas.com *时,你的本地递归型名称服务器会在互联网上查找我的权威型名称服务器。一旦递归型名称服务器找到了主机名到 IP 地址的映射,它会将该响应返回给客户端。本书将向你展示如何使用递归型名称服务器,以及如何启用你自己的递归型名称服务器。

系统的解析器负责配置主机如何执行 DNS 查询,并将响应转发给程序。配置解析器是系统管理中的一项重要工作。即使是 DNS 服务器,也需要配置解析器,因为除非你告诉它,否则主机不会知道自己是名称服务器。配置解析器需要回答一些问题:

  • 服务器在哪里查找 DNS 信息?

  • 你想要哪些本地覆盖?

  • 什么是本地域名?

  • 应该查询哪些名称服务器?

这些问题的答案在/etc/nsswitch.conf/etc/resolv.conf中进行配置。

主机/IP 信息来源

这应该很简单。服务器通过名称服务器获取主机信息,对吧?我刚刚花了几段话告诉你这个,不是吗?

然而,现实情况并没有那么简单。也许你有一个只有三台机器的小型家庭网络,你希望每台机器都能通过主机名互相找到,但你不想运行一个本地的权威名称服务器。或者,也许你在一个大型公司网络中,完成 DNS 更改需要几周时间,而你有几台需要相互通信的测试系统。像所有类 Unix 操作系统一样,FreeBSD 可以从 DNS 和明文 hosts 文件 /etc/hosts 中获取信息。

当 FreeBSD 需要知道一个主机的地址(或者一个地址的主机名)时,默认情况下查询首先会访问 hosts 文件,然后是配置的名称服务器。这意味着你可以本地覆盖名称服务器的结果,这对于位于 NAT 后的主机或在大型公司网络中具有特殊需求的主机非常有用。在某些情况下,你可能需要反转这个顺序,首先查询 DNS,然后再查询 hosts 文件。可以在 /etc/nsswitch.conf 中设置此顺序。

名称服务切换

文件 /etc/nsswitch.conf 不仅由解析器使用,还由所有其他名称服务使用。一个网络操作系统包括许多不同的名称服务。/etc/services 中的 TCP/IP 端口既是名称服务,也是网络协议的名称和编号。确定用户的 UID 和 GID 需要不同类型的名称查找(参见 第九章)。/etc/nsswitch.conf 确定所有这些查询的顺序及更多内容。我们这里只讨论主机名查找,更多关于名称服务切换的内容请参考 第二十章。

/etc/nsswitch.conf 中的每个条目都是一行,包含名称服务的名称、冒号和信息源列表。以下是主机名服务查找的配置:

hosts: files dns

解析器按照列出的顺序查询信息源。如果你有额外的信息源,例如 nscd(8),请在此列出。关于这些附加组件的文档来源应包括服务的名称。

使用 /etc/hosts 设置本地名称

/etc/hosts 文件将互联网地址与主机名匹配。曾几何时,在域名服务(DNS)之前,互联网有一个单一的 hosts 文件,提供互联网中每个节点的主机名和 IP 地址。系统管理员将他们的主机更改提交给一个中央维护者,后者每隔几个月发布一次更新的 hosts 文件。然后,系统管理员会下载该文件并将其安装到他们的所有机器上。当整个互联网只有四个系统时,这种方式运行得很好,当主机数量只有几百个时也能接受。然而,一旦互联网开始呈指数增长,这种方案就变得完全无法维护。

虽然 hosts 文件非常有效,但它只在安装它的机器上工作,并且必须由系统管理员维护。公共 DNS 在很大程度上取代了 /etc/hosts,但它在你不想运行本地权威 DNS^(3) 或者你处于 IPv4 NAT 设备后面时仍然很有用。如果你在家有一两台服务器,或者由别人管理你的权威名称服务器,使用 hosts 文件是完全合理的。一旦你有足够多的主机,以至于更新 hosts 文件的想法让你感到恶心,就该学习构建权威名称服务器了。

每行 /etc/hosts 中的条目代表一个主机。每行的第一个条目是 IP 地址,第二个是主机的完全限定域名,例如 mail.michaelwlucas.com。在这两个条目之后,你可以列出该主机的任意数量的别名。

例如,一个小公司可能只有一台服务器处理电子邮件、提供 FTP、网页和 DNS 服务,并执行其他各种功能。该网络上的桌面计算机可能会有类似这样的 hosts 文件条目:

203.0.113.3     mail.mycompany.com    mail ftp www dns

使用这个 /etc/hosts 条目,桌面计算机可以通过完整的域名或列出的任何简短别名来找到服务器。然而,这不会让你访问 Facebook。为此,你需要名称服务。

配置名称服务

通过文件 /etc/resolv.conf 告诉你的主机如何查询名称服务器。你可能希望提供一个本地域名或一个域名搜索列表,然后列出名称服务器。

本地域名和搜索列表

如果你的组织有许多机器,逐个输入完整的主机名会很快让人感到厌烦。如果你正在进行维护并需要登录每一台 Web 服务器,当你输入到 www87.BertJWRegeerHasTooManyBlastedComputers.com 时,你可能已经需要治疗即将到来的腕管综合症了。你可以在 /etc/resolv.conf 的第一行提供一个本地域名或一个要搜索的域名列表。

domain 关键字告诉解析器默认检查哪个本地域名,用于所有主机名。我的所有测试主机都在域名 michaelwlucas.com 下,所以我可以将其设置为默认域。

domain  michaelwlucas.com

一旦你指定了一个本地域名,解析器将自动将该域名附加到任何简短的主机名上。如果我输入 ping www,解析器会附加本地域名并将 ping(8) 发送到 www.michaelwlucas.com。但如果我提供一个完整的主机名,比如 www.bertjwregeer.com,解析器就不会添加默认域名。

也许我有多个域名想要搜索。使用 search 关键字来提供一个按顺序尝试的域名列表。像 domain 一样,search 必须是 resolv.conf 的第一行。

search  michaelwlucas.com bertjwregeer.com mwl.io

当你使用简短的主机名,如www时,解析器会将第一个域名添加到搜索列表中。如果没有找到结果,它会使用第二个域名重复查询,然后是第三个。如果我运行ping petulance,解析器会依次搜索petulance.michaelwlucas.competulance.bertwjregeer.competulance.mwl.io。如果在这些域名中都没有该主机,查询将失败。

如果在/etc/resolv.conf中既没有域名(domain)也没有搜索项(search),但机器的主机名中包含域名,解析器将使用本地机器的域名。

名称服务器列表

现在你的解析器已经知道了尝试哪些域名,接下来告诉它查询哪些名称服务器。在/etc/resolv.conf中为每个名称服务器列出一行,按优先顺序排列。使用关键字nameserver和 DNS 服务器的 IP 地址。解析器会按顺序查询列出的名称服务器。一个完整的resolv.conf可能如下所示:

domain mwl.io
nameserver 127.0.0.1
nameserver 203.0.113.8
nameserver 192.0.2.8

这个解析器已经准备好工作了。

注意第一个名称服务器条目。地址 127.0.0.1 总是与本地主机相关联。此机器正在运行一个本地递归名称服务器。你也可以这样做!

缓存名称服务器

每次主机必须联系一个主机时,都需要执行 DNS 查找。一个忙碌的服务器会发出大量查询,而解析器本身并不会缓存这些响应。如果一个主机一分钟内需要连接 Google 500 次,那就意味着有 500 次 DNS 查找。虽然设置一个权威 DNS 服务器需要特定的技能,但配置一个本地递归服务器只需要在/etc/rc.conf中添加一行。这让你的 FreeBSD 主机能够缓存 DNS 响应,同时减少网络拥堵并提高性能。

使用rc.conf变量local_unbound_enable启用本地名称服务器。

# sysrc local_unbound_enable=YES
local_unbound_enable: NO -> YES

现在可以启动本地名称服务器。

# service local_unbound start

当你第一次启动该服务时,unbound会自行配置。它会从/etc/resolv.conf中提取系统的名称服务器,并配置自己将所有查询转发到这些名称服务器。然后,设置过程会编辑/etc/resolv.conf,使所有查询指向运行在 IP 地址 127.0.0.1 上的本地名称服务器。

当你的主机发起 DNS 查询时,解析器会查询unbound。本地名称服务器会检查其缓存,看看是否有有效且未过期的查询结果。如果没有缓存的响应,unbound会查询你偏好的名称服务器。

我建议在每台不是 DNS 服务器的服务器上启用local_unbound

网络活动

现在你已经接入网络,怎么查看发生了什么呢?有几种方法可以查看网络,我们将逐一考虑。与许多商业操作系统不同,FreeBSD 中的命令如 netstat(8) 和 sockstat(1) 能为你提供比健康所需更多的网络信息。

当前网络活动

通用网络管理程序 netstat(8) 根据给定的标志显示不同的信息。人们常问的一个问题是:“我的系统现在正在处理多少流量?” netstat(8) 的 -w(即 等待)选项显示系统正在处理多少数据包和字节。-w 标志需要一个参数,即更新之间的秒数。添加 -d(即 丢弃)标志会告诉 netstat(8) 包含有关未到达系统的数据包的信息。在这里,我们要求 netstat(8) 每五秒更新一次显示:

# netstat -w 5 -d
            input        (Total)           output
   packets  errs idrops      bytes    packets  errs      bytes colls drops
       ➊34   ➋0     ➌0    ➍44068        ➎23   ➏0      ➐1518    ➑0   ➒0 
         33    0       0     42610          23    0        1518      0    0
--snip--

当你输入此命令时,似乎什么也没有发生,但几秒钟后,显示屏会打印出一行信息。前三列描述了入站流量,接下来的三列描述了出站流量。我们可以看到自上次更新以来接收的数据包数 ➊,入站流量的接口错误数 ➋,以及丢失的入站数据包数 ➌。输入信息的最后是自上次更新以来接收的字节数 ➍。接下来的三列显示了机器自上次更新以来传输的数据包数 ➎,传输中的错误数 ➏,以及我们发送的字节数 ➐。然后,我们会看到自上次更新以来发生的网络冲突数 ➑,以及丢失的数据包数 ➒。例如,在此显示中,系统自 netstat -w 5 -d 开始运行以来接收了 34 个数据包 ➊。

五秒钟后,netstat(8) 打印出第二行,描述自第一行打印以来的活动。

你可以根据需要使输出尽可能详细,并随时运行它。如果你想每秒获取更新,只需运行 netstat -w 1 -d。如果每分钟一次足够,你可以使用 netstat -w 60 -d。我发现当我在积极监控网络时,五秒的间隔最合适,但你会很快学到什么最适合你的网络和问题。

按 CTRL-C 停止报告,直到你看够为止。

哪个端口在监听什么?

另一个常见问题是:“哪些端口是开放的,哪些程序在监听这些端口?” FreeBSD 包含了 sockstat(1),这是一个友好的工具来回答这个问题。它显示了活跃的连接和可供客户端使用的端口。

sockstat(1) 程序不仅列出了监听网络的端口,还包括系统上的其他任何端口(或 套接字)。使用 -4 标志可以查看 IPv4 套接字,使用 -6 可以查看 IPv6。以下是来自一个 非常 小型服务器的截断 sockstat(1) 输出:

image

第一列给出了运行附加到相关端口的程序的用户名。第二列是命令的名称。接下来是程序的进程 ID 以及附加到套接字的文件描述符号。接下来的列显示了套接字使用的传输协议——如果是 TCP/IP 版本 4 上的 TCP,则为tcp4,如果是 TCP/IP 版本 4 上的 UDP,则为udp4。接着列出了本地 IP 地址和端口号,最后是每个现有连接的远程 IP 地址和端口号。

看看我们第一个条目。我正在运行程序 sshd ➊。一个手册页搜索会带你到 sshd(8),即 SSH 守护进程。主 sshd(8)守护进程为我分叉了一个子进程来处理我的连接,因此我们看到多个不同进程 ID 的 sshd(8)实例。我连接到本地 IP 地址 203.0.113.43 ➋上的 TCP 端口 22。这个连接的远端 IP 地址是 24.192.127.92 ➌,端口为 62937。 这是来自远程系统到本地计算机的 SSH 连接。

其他可用的连接包括运行在端口 25 上的 Sendmail ➍邮件服务器。请注意,此项没有列出作为外部地址的 IP 地址。这个套接字正在监听传入的连接。我们的 httpd 进程 ➎ 正在监听端口 80 上的传入连接。

机智的你可能会注意到,这台服务器有两个 SSH 守护进程可以接受传入连接,一个在端口 23 ➏上,另一个在端口 22 ➐上。正如/etc/services所示,SSH 通常运行在端口 22 上,而端口 23 则预留给 telnet。任何通过 telnet 连接到此机器的人都会连接到一个 SSH 守护进程,而这并不会按他们预期的那样工作。怀疑的你可能会猜测,这个 SSH 服务器是为了绕过只基于源和目标端口而非实际协议过滤流量的防火墙而设置的。(对于这样的指控,我不做评论。)

最后两项是一个命名服务器,名为 named,正在等待端口 53 上的传入连接。这个命名服务器正在监听 UDP ➑和 TCP ➒连接,并附加到单个 IP 地址 203.0.113.8。

端口监听器详细信息

虽然 sockstat(1)提供了一个网络服务可用性的高层视图,但你可以通过 netstat(8)获得有关单个连接的更详细信息。要查看开放的网络连接,请使用 netstat(8)的-a标志。-n标志告诉 netstat(8)不要翻译 IP 地址为主机名;这种翻译不仅可能减慢输出速度,还可能导致模糊的输出。最后,-f inet选项告诉 netstat(8)只关注 IPv4 网络连接,而-f inet6则关注 IPv6。以下是与我们刚刚运行 sockstat(1)相同机器的 netstat 输出:

# netstat -na -f inet
Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0     48  203.0.113.43.22        24.192.127.92.62937    ESTABLISHED
tcp4       0      0  *.25                   *.*                    LISTEN
tcp4       0      0  *.23                   *.*                    LISTEN
tcp4       0      0  *.80                   *.*                    LISTEN
tcp4       0      0  *.22                   *.*                    LISTEN
tcp4       0      0  203.0.113.8.53         *.*                    LISTEN
udp4       0      0  203.0.113.8.53         *.*  

在这里,我们无法得知任何端口上附加的程序是什么。每一列的第一项是套接字使用的传输协议——通常是 TCP,但最后一行显示的是 UDP。

Recv-QSend-Q 列显示了等待被该连接处理的字节数。如果你看到某些连接的 Recv-Q 数字大部分时间都不为零,说明监听该端口的程序处理传入数据的速度不够,无法跟上网络堆栈的速度。同样,如果 Send-Q 列持续出现非零值,说明网络或远程系统无法以你发送的速度接收数据。偶尔的队列数据包是正常的,但如果它们一直存在,你可能需要调查为什么处理速度慢。你需要观察自己的系统,了解什么是正常的。

Local Address 是你猜的那样,表示本地系统上网络连接监听的 IP 地址和端口号。网络端口出现在条目的末尾,并且与 IP 地址之间由一个点分隔。例如,203.0.113.43.22 就是 IP 地址 203.0.113.43,端口 22。如果条目是一个星号后跟一个点和端口号,意味着该系统在所有可用的 IP 地址上都在监听该端口,系统已准备好接受该端口上的连接。

Foreign Address 列显示任何连接的远程地址和端口号。

最后,(state) 列显示 TCP 握手的状态。你不需要了解所有可能的 TCP 连接状态,只要了解什么是正常的就行。ESTABLISHED 表示连接已建立,数据可能正在流动。LAST_ACKFIN_WAIT_1FIN_WAIT_2 表示连接正在关闭。SYN_RCVDACKSYN+ACK 是连接创建的一部分(来自 第七章 的三次握手)。LISTEN 表示端口已准备好接收传入连接。在前面的示例中,一个 TCP 连接正在运行,四个连接准备接受客户端。由于 UDP 是无状态的,这些连接没有列出状态信息。

通过读取这些输出并结合 sockstat(1) 提供的信息,你可以准确了解哪些程序表现良好,哪些程序存在瓶颈。

如果你对监听套接字不感兴趣,只关心那些有活动连接的套接字,可以使用 netstat(8) 的 -b 选项,而不是 -a。运行 netstat -nb -f inet 只会显示与远程系统的连接。

你还可以使用 netstat -T 来显示单个连接的 TCP 重传和乱序数据包。重传和乱序数据包是丢包的症状。

内核中的网络容量

FreeBSD 内核通过使用 mbufs 来处理网络内存。mbuf 是用于网络的内核内存块。你将在 FreeBSD 网络堆栈文档中不断遇到关于 mbuf 的提及,因此了解它们至少有一个模糊的概念是很重要的。

FreeBSD 在启动时会自动根据系统中物理内存的大小分配网络带宽。我们假设,如果你有一个 64GB 内存的系统,你希望在网络上使用更多的内存,而不是一个只有 1GB 内存的小设备。通过 netstat -snetstat -m 查看 FreeBSD 如何使用其资源。我们先来看最简洁的一个。

要获取关于用于网络的内核内存的概览,运行 netstat -m。输出可以分为两个大类:使用了多少内存和有多少请求失败。以下是经过剪裁的输出,只包括这些项中的几个例子,但它们都遵循相同的格式:

# netstat -m
--snip--
➊32/372/404/➋25600 mbuf clusters in use (current/cache/total/max)
--snip--
0/0/0 requests for mbufs ➌denied (mbufs/clusters/mbuf+clusters)
--snip--

在这里我们看到使用了多少个 mbuf 集群 ➊。你可能会猜测这些与 mbuf 有关,答案是对的。你不需要完全知道 mbuf 集群是什么;重要的是你知道可以分配多少个,并且能够看到自己没有超过这个限制 ➋。

同样,我们可以看到内核拒绝了多少次对 mbuf 的请求 ➌。这个系统没有拒绝任何 mbuf 请求,这意味着我们没有因为内存不足而造成的性能问题。如果你的系统开始因为内存不足而拒绝 mbuf 请求,那就麻烦了。接下来请查看“优化网络性能”部分。

虽然 netstat -m 会生成十几行输出,netstat -s 则会产生多页输出。它提供了每种协议的性能统计数据。与 netstat -m 类似,你可以将这些统计数据分为两类:做了多少事情以及遇到了多少问题。偶尔在你的系统上运行这两个命令,并查看结果,这样你就知道服务器上什么是正常的,并且在遇到问题时能够识别出异常的数据。

优化网络性能

既然你已经了解了发生了什么,那么如何提高 FreeBSD 的网络性能呢?在考虑优化时有一个简单的经验法则:不要优化。网络性能通常只受硬件的限制。许多应用程序处理数据的速度跟不上网络的提供速度。如果你认为需要优化性能,可能是你在错误的地方寻求解决方案。查看第二十一章,那里有关于调查性能瓶颈的提示。

一般来说,只有在遇到网络问题时,才需要调整网络性能。这意味着你应该从 netstat -mnetstat -s 中获得输出,显示内核正在面临资源问题。如果内核开始拒绝资源请求或丢弃连接请求,请查看本节中的提示。如果你遇到问题,或者认为应该获得更好的性能,首先检查硬件。

优化网络硬件

不是所有的网络硬件都是一样的。虽然在 IT 行业中这句话被频繁提到,但 FreeBSD 的开放性让这一点显而易见。例如,以下是 rl(4) 网卡驱动源代码中的一条注释:

The RealTek 8139 PCI NIC redefines the meaning of 'low end.' This is
probably the worst PCI ethernet controller ever made, with the possible
exception of the FEAST chip made by SMC. The 8139 supports bus-master
DMA, but it has a terrible interface that nullifies any performance
gains that bus-master DMA usually offers.

这可以总结为:“这张卡同时又烂又差。买一张新卡吧。”虽然这是我在 FreeBSD 源代码中看到的最尖刻的评论,而且这款硬件今天很难找到,但某些其他卡的驱动程序以更礼貌的方式表达了相同的观点。用低端硬件优化网络性能就像将高性能赛车变速器装进 1974 年的 Gremlin 车里。更换你的廉价网卡可能会解决你的问题。一般来说,英特尔生产的网卡不错;他们为有线网卡维护 FreeBSD 驱动程序,并提供支持,以便 FreeBSD 社区帮助维护这些驱动程序。(无线网卡则是另一回事。)类似地,许多制造服务器级机器的公司特别注重使用服务器级网卡。有些公司提供 FreeBSD 驱动程序,但不提供硬件文档。这意味着驱动程序可能正常工作,但你完全依赖于厂商未来对 FreeBSD 的喜好来获取更新。专注于低成本消费级网络设备的公司并不是高性能卡的最佳选择——毕竟,普通家庭用户通常不知道如何选择网卡,所以他们只看价格。如果不确定,可以查阅 FreeBSD-questions 邮件列表的档案,寻找近期的网卡推荐。

同样,交换机的质量差异也非常大。声称交换机支持千兆连接协议并不意味着你能够通过每个端口真正达到千兆速度!我有一台 100Mb 的交换机,在 15Mbps 时会形成瓶颈,还有一台“千兆”交换机似乎在大约 50Mbps 时就开始卡顿。我建议你将交换机的速度视为一种协议或语言:我可以声称我会说俄语,但在我停止学习 30 年后,我的口语速度每分钟大约只能说三个词。我的俄语接口质量非常差。同样,专为家庭使用设计的交换机并不是生产环境中的最佳选择。

如果获得合适的硬件没有解决你的问题,请继续阅读。

内存使用

FreeBSD 使用系统中安装的内存量来决定为 mbuf 保留多少内存空间。除非 netstat -m 告诉你 mbuf 空间不足,否则不要调整你创建的 mbuf 数量。如果你遇到 mbuf 问题,真正的解决方法是向你的机器添加内存。这将使 FreeBSD 在启动时重新计算创建的 mbuf 数量,并解决你的问题。否则,你只会将问题转移到系统的其他部分或其他应用程序上。你可能为网络连接配置了大量内存,却发现你的数据库服务器被压垮了。不过,如果你确定要继续操作,下面是如何做的。

有两个 sysctl 值控制 mbuf 的分配,kern.maxuserskern.ipc.nmbclusters。第一个,kern.maxusers,是一个启动时可调的参数。你的系统会在启动时自动根据系统硬件确定合适的 kern.maxusers 值。调整这个值可能是扩展系统整体规模的最佳方法。在旧版本的 FreeBSD 中,kern.maxusers 会预分配网络内存,并拒绝将其释放给其他任务,因此增加 kern.maxusers 可能会严重影响系统的其他部分。然而,现代 FreeBSD 并不会预分配网络内存,因此这仅仅是网络内存的上限。如果 kern.maxusers 设置得过小,你将在 /var/log/messages 中收到警告(请参阅 第二十一章)。

sysctl kern.ipc.nmbclusters 专门控制系统为数据(存储在套接字缓冲区中,等待被应用程序发送或读取)分配的 mbufs 数量。尽管这是一个可以在运行时调整的参数,但最好在启动时就通过在 /etc/sysctl.conf 中定义它来进行设置(请参阅 第六章)。然而,如果设置得过高,你实际上可能会导致内核无法为其他任务分配内存,从而使机器发生崩溃。

# sysctl kern.ipc.nmbclusters
kern.ipc.nmbclusters: 25600

Mbufs 是以称为 nmbclusters(有时也称为 mbuf 集群)的单位进行分配的。虽然一个 mbuf 的大小有所不同,但一个集群大约是 2KB。你可以使用简单的数学计算来确定当前 nmbcluster 设置需要多少内存,然后为你的系统和应用程序计算出合适的值。这个示例机器有 25,600 个 nmbclusters,这意味着内核可以为网络通信用途使用大约 50MB 的内存。对于我这台具有 1GB 内存的测试笔记本来说,这几乎可以忽略不计,但在嵌入式系统上可能不适用。

要计算适当数量的 mbuf 集群,当服务器非常繁忙时运行 netstat -m。输出的第二行将显示正在使用的 mbuf 数量和可用的总数量。如果你的服务器在最繁忙时使用的 nmbclusters 数量远远少于可用数量,那说明你可能走错了方向——停止调整 mbufs,尽早更换硬件吧。^(4) 例如:

➊32/➋372/➌404/➍25600 mbuf clusters in use (current/cache/total/max)

目前此系统在此机器上使用了 32 个 nmbclusters ➊,并且缓存了 372 个之前使用过的 nmbclusters ➋。此时内存中共有 404 个集群 ➌,而我们的 25,600 个集群 ➍ 仅使用了 1.5%。如果这就是你实际的系统负载,实际上减少 nmbclusters 的数量可能是有意义的。

我的个人经验是,服务器应该有足够的 mbufs 来处理其标准高负载的两倍。如果你的服务器在高峰时段使用了 25,000 个 nmbclusters,那么它至少应该有 50,000 个可用来应对这些短暂的异常高峰。

一生一次的负载与标准负载

始终将一次性事件的规划与常规负载的规划区分开。当美国政府的《平价医疗法案》健康保险注册网站上线时,数百万用户立即尝试注册。在最初几天,该网站极其缓慢。经过一周,硬件处理负载时没有问题。这无疑是正确的容量规划。

最大并发连接数

FreeBSD 内核提供了处理一定数量新 TCP 连接的能力。这并不是指服务器之前接收到并正在处理的连接,而是指那些尝试同时发起连接的客户端。例如,目前正在传递给客户端的网页不算在内,但已经到达内核但还未到达 Web 服务器进程的请求则会计算在内。这是一个非常狭窄的窗口,但有些网站会超出这个限制。

sysctl kern.ipc.somaxconn 决定了系统将尝试处理的同时连接请求的最大数量。默认值为 128,这对于高负载的 Web 服务器可能不够。如果你正在运行一个高容量的服务器,并且预计会有超过 128 个新请求同时到达,你可能需要增加这个 sysctl 的值。如果用户开始抱怨无法连接,这可能就是问题所在。当然,很少有应用程序会接受这么多同时的新连接;在遇到这个问题之前,你可能需要对应用程序进行优化。

轮询

一些千兆网卡可以通过轮询来提高性能。轮询采用了传统的中断和 IRQ 的概念,并将其抛出窗外,取而代之的是定期检查网络活动。在经典的中断驱动模式中,每当数据包到达网卡时,网卡会通过生成中断来请求 CPU 的注意。CPU 会停止当前的任务,处理这些数据。当网卡处理的数据量不大的时候,这样做是很棒的,甚至是必要的。然而,一旦系统开始处理大量数据,网卡会持续生成中断。与其不断中断,系统更高效的做法是内核定期从网卡抓取网络数据。这个定期检查的过程叫做轮询。一般来说,轮询只有在推送大量流量时才有用。

目前,轮询(Polling)并未作为内核模块提供,因为它需要修改设备驱动程序。这也意味着并非所有网络卡都支持轮询,因此请务必查阅 polling(4) 获取完整的支持列表。通过在内核配置中添加 DEVICE_POLLING 来启用轮询。重启后,可以通过 ifconfig(8) 逐个接口启用轮询。

# ifconfig em0 polling

同样地,使用参数-polling禁用轮询。ifconfig(8)命令还会显示接口上是否启用了轮询。由于可以动态启用和禁用轮询,当系统负载较重时,启用轮询,看看性能是否有所改善。

轮询仅在旧卡上使用。10GB 卡及更快的卡无法进行轮询。

其他优化

FreeBSD 有大约 200 个与网络相关的 sysctl。你拥有所有的工具,可以优化系统到一个极限,以至于它完全不再通过任何流量。玩网络优化时要非常小心。许多看似能解决问题的设置,实际上只修复了某一类问题,却引入了新的问题。

一些软件供应商(例如 Samba)推荐特定的网络 sysctl 更改。谨慎尝试这些更改,并观察它们是否对其他程序产生意外的副作用,然后再将其作为新默认设置。TCP/IP 是一个极其复杂的协议,而 FreeBSD 的默认设置和自动调优反映了多年的经验、测试和系统管理员的痛苦。

还要记住,FreeBSD 已经有二十多年历史了。几年前的邮件列表和论坛帖子可能对网络调优不再有用。

网络适配器团队

随着网络服务器在业务中的重要性不断增加,冗余变得愈发重要。我们在服务器中有冗余硬盘,在数据中心有冗余带宽,但服务器的冗余带宽如何解决呢?在小范围内,当你在办公室内移动时,可能会在有线和无线连接之间切换。能够在拔掉电缆后仍然保持现有的 SSH 会话不掉线,真的是一件非常棒的事情。

FreeBSD 可以将两个网卡视为一个实体,允许你与一个交换机建立多个连接。这通常被称为网络适配器团队绑定链路聚合。FreeBSD 通过 lagg(4)实现适配器团队,即链路聚合接口。

TRUNKING、TEAMING 和 VLANs

一些供应商使用trunk这个词来描述链路聚合。其他供应商则使用trunk来描述一根带有多个网络(VLAN)的电缆。FreeBSD 通过不使用trunk这个词来避免这种争议。如果有人在你面前讨论 trunk,问问他们指的是哪一种。

内核模块 lagg(4)提供了一个 lagg0 虚拟接口。你将物理接口分配给 lagg0 接口,使它们成为聚合链路的一部分。虽然你可以只使用一个物理接口来使用 lagg(4),但只有在你有两个或更多物理接口可分配到聚合链路时,聚合链路才有意义。lagg(4)模块允许你在有线和无线网络之间实现无缝漫游、故障转移,以及几种不同的聚合协议。

聚合协议

不是所有的网络交换机都支持所有的链路聚合协议。FreeBSD 对一些复杂的高端协议提供了基本实现,同时也包括了非常基础的故障转移设置。我推荐的三种协议是 Fast EtherChannel、LACP 和故障转移。(还有更多的方案,可以在 lagg(4)中查看。)

Cisco 的Fast EtherChannel (FEC)是一个可靠的链路聚合协议,但仅适用于运行特定版本 Cisco 操作系统的高端到中端 Cisco 交换机。如果你使用的是非托管交换机,Fast EtherChannel 就不是一个可行的选择。Fast EtherChannel 的配置比较复杂(在交换机上),因此我只建议在它已经成为你公司链路聚合标准的情况下使用 FEC。

链路聚合控制协议(LACP)是链路聚合的行业标准。物理接口被合并为一个虚拟接口,其带宽大约等于所有单独链接的总和。LACP 提供了出色的容错能力,几乎所有交换机都支持它。除非你有特定要求使用 Fast EtherChannel,或者在使用 LACP 时交换机会出现问题,否则我推荐使用 LACP。

如果你的交换机在使用 LACP 时出现问题,使用failover。故障转移方法一次通过一个物理接口传输流量。如果该接口出现故障,连接会自动切换到池中的下一个连接。虽然你无法获得聚合带宽,但你可以将服务器连接到多个交换机上以实现容错。使用故障转移可以让你的笔记本电脑在有线和无线连接之间切换。

配置 lagg(4)

lagg 接口是虚拟的,意味着没有物理部分可以指认为“这是 lagg0 接口”。在你配置该接口之前,必须先创建它。FreeBSD 允许你使用ifconfig命令通过ifconfig interfacename create创建接口,但你也可以在/etc/rc.conf中使用cloned_interfaces语句来创建接口。

rc.conf中配置 lagg(4)接口有三个步骤:创建接口、启动物理接口、以及聚合它们。在这里,我们通过两个 Intel 千兆以太网卡 em0 和 em1 创建一个单独的 lagg0 接口。

cloned_interfaces="lagg0"
ifconfig_em0="up"
ifconfig_em1="up"
ifconfig_lagg0="laggproto lacp laggport em0 laggport em1 inet 203.0.113.1/24"

首先,你将 lagg0 列为一个克隆接口,这样 FreeBSD 在启动时就会创建这个接口。然后,启动 em0 和 em1 接口,但不要配置它们。最后,告诉 lagg0 接口使用哪种聚合协议(LACP)、哪些物理接口属于它,以及它的网络信息。这几行配置将为你提供一个高可用性的以太网连接。

虚拟局域网(VLAN)

虚拟局域网,或VLAN,允许你在一根线上的多个以太网段。你有时会看到 VLAN 被称为802.1q标记,或这几个术语的组合。你可以通过配置附加到物理接口的额外逻辑接口来使用这些多个网络。然而,物理线路仍然只能承载有限的数据量,因此所有 VLAN 和共享线路的常规网络(或原生 VLAN)都使用一个共同的带宽池。如果你需要在多个以太网段上同时使用 FreeBSD 主机,这就是一种不需要再拉更多电缆的方式。

到达网络卡的 VLAN 帧就像常规以太网帧,附带一个额外的头部,表示“这是 VLAN 编号的部分。”每个 VLAN 通过 1 到 4096 之间的标签来识别。原生 VLAN 则没有任何标签。网络通常(但不总是)内部称其为VLAN 1

在你的 FreeBSD 主机上配置 VLAN 并不会自动将主机连接到 VLAN。网络必须配置为将这些 VLAN 发送到你的主机。你需要与网络团队合作以获得 VLAN 的访问权限。

配置 VLAN 设备

使用 ifconfig(8)来创建 VLAN 接口。你必须知道物理接口和 VLAN 标签。

# ifconfig interface.tag create vlan tag vlandev interface

在这里,我为 VLAN 2 创建一个接口,并将其附加到接口 em0。

# ifconfig em0.2 create vlan 2 vlandev em0

现在我可以像配置物理接口一样配置接口 em0.2。

# ifconfig em0.2 inet 192.0.2.236/28

实际上,我可能会在一个命令中完成所有这些操作。

# ifconfig em0.2 create vlan 2 vlandev em0 inet 192.0.2.236/28

一切完成了。现在使用 ifconfig(8)来显示你的新接口。

# ifconfig em0.2
em0.2: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        options=503<RXCSUM,TXCSUM,TSO4,LRO>
        ether 00:25:90:db:d5:94
        inet 198.22.65.236 netmask 0xffffff00 broadcast 255.255.255.240
        inet6 fe80::225:90ff:fedb:d594%em0.2 prefixlen 64 scopeid 0x6
        nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
     ➊ media: Ethernet autoselect (100baseTX <full-duplex>)
        status: active
     ➋ vlan: 2 vlanpcp: 0 parent interface: em0
     ➌ groups: vlan

这看起来几乎和任何其他物理接口一样。媒体信息➊直接来自底层接口。你会看到一个带有 VLAN 信息的标签➋,以及一个说明这是与其他 VLAN 接口分组的注释➌。

启动时配置 VLAN

使用rc.conf变量在启动时配置 VLAN。首先,使用带有接口名称的vlan_变量列出附加到该接口的 VLAN。在这里,我告诉 FreeBSD 在接口 em0 上启用 VLAN 2 和 3,并为每个 VLAN 分配 IP 配置。

vlans_em0="2 3"
ifconfig_em0_2="inet 192.0.2.236/28"
ifconfig_em0_3="inet 198.51.100.50/24"

如果底层接口没有配置,你至少需要将其启用。除非物理接口开启,否则 VLAN 接口无法工作。

ifconfig_em0="up"

现在你在启动时已经有了虚拟局域网。恭喜!

现在你已经有了一个正常工作的网络,让我们稍微本地化一点,来看一下基本的系统安全。

第九章:保护您的系统

image

保护您的系统意味着确保计算机的资源仅供授权人员用于授权的目的。即使您的系统上没有重要数据,您仍然拥有宝贵的 CPU 时间、内存和带宽。许多曾认为自己的系统不重要,不会有人去破解的人,最终发现自己成了一个无意中的中继站,成为了攻击的工具,导致某个大型公司瘫痪。你不希望有一天早上醒来,听到执法人员破门而入的声音,因为您的不安全计算机被用来破解了某个银行。

当然,有比某个小屁孩接管您的服务器更糟糕的事情——比如邻里的高利贷者把你的两条腿打断。发现您组织的网页上现在写着“哈哈,你被 r00t 了!”是个相当强劲的竞争者,可能排得上第二。即便是更容易理解的入侵也会带来巨大的麻烦。我曾参与过的多数实际入侵事件(我不是攻击者,而是作为受害者的顾问)都来自那些有政府审查制度的国家,流量分析显示攻击者其实只是想获取不受限制的新闻网站访问权限。虽然我完全同情这些人,但当我依赖服务器的稳定运行来经营我的业务时,他们的入侵是不可接受的。

在过去几年里,接管远程计算机变得容易多了。可以通过搜索引擎找到一些点选即用的程序,用来颠覆计算机。当一个聪明的攻击者编写了一个漏洞时,数千个无聊的青少年可以下载它,并让我们其他人生活得更困难。即使系统上的数据毫无价值,您仍然必须保护系统的资源。

一般来说,操作系统本身并不会被攻破;而是运行在操作系统上的程序会被攻破。即便是世界上最为偏执、默认安全的操作系统,也无法保护写得不好的程序免受其自身问题的影响。偶尔,某个程序可能与操作系统以某种方式交互,从而实际上危及操作系统的安全。其中最著名的就是缓冲区溢出,在这种情况下,攻击者的程序会直接被转储到 CPU 的执行空间,操作系统随后执行它。FreeBSD 进行了广泛的审计,消除了缓冲区溢出以及其他众多众所周知的安全问题,但这并不意味着它们已经被彻底根除。新功能和程序不断出现,它们可能以意想不到的方式与旧功能或彼此之间发生交互。

FreeBSD 提供了许多工具来帮助你保护系统免受内外部攻击者的侵害。虽然这些工具中的任何一个单独使用都不足够,但它们都是可取的。将你关于系统安全的所有知识视为工具箱中的工具,而不是解决所有问题的答案。例如,虽然仅仅提高系统的安全级别不会让你的系统变得安全,但结合合理的权限、文件标志、定期修补、密码控制以及所有构成良好安全策略的其他措施时,它会起到作用。我们将在第十九章中介绍更多的高级安全工具,但如果没有在这里讨论的基本保护措施,这些工具无法帮助你保护系统。

敌人是谁?

我们将潜在的攻击者随便分为四组:脚本小子、不满的用户、僵尸网络和有技术的攻击者。你会在专门讨论安全的书籍中找到更详细的分类,但这不是你要关注的内容。这些类别容易解释,容易理解,并涵盖了你可能遇到的 99%的攻击者。

脚本小子

最多的人类攻击者,脚本小子,并不是系统管理员。他们没有技能。他们下载基于点击操作的攻击程序,寻找易受攻击的系统。他们相当于抢包的小偷,专门欺负那些手提包稍微松懈的老太太。幸运的是,脚本小子很容易防范:只需保持软件更新,并遵循良好的计算机操作规范。像蝗虫一样,脚本小子容易被打败,但他们实在是太多了!

不满的用户

第二类群体,你的用户,是导致大多数安全问题的根源。你组织的员工最有可能知道安全漏洞的位置,认为规则不适用于他们,并且有时间去破坏你的安全。如果你告诉某个员工公司政策禁止他访问某个计算机资源,而该员工认为他应该有权访问,那么他很可能会寻找绕过限制的方法。任何认为自己特别到规则不适用的人都是安全风险。更糟糕的是,当一个知道所有丑事的员工生气时,坏事可能会发生。你可能已经修补了所有服务器,并安装了极为反人类的防火墙,但如果任何知道密码的人是Current93,能够拨打后端调制解调器,你就麻烦了。

阻止这些人的最佳方式就是简单地不要疏忽大意。不要把项目半途而废或文档不全地留着。当有人离开公司时,禁用他的账户,修改所有的管理员密码,通知所有员工该人的离职,并提醒他们不要与该人分享机密信息。制定一套具有实际违规惩罚的计算机安全政策,并由人力资源部门执行。并且,清除那些不安全的调制解调器、运行在奇怪端口上的未记录的 telnet 服务器,或者你当初匆忙设置的、以为没人会发现的黑客后门。

僵尸网络

僵尸网络的数量超过了上述任何一类,但它们不是人类。它们是被恶意软件感染并从一个中心点控制的机器。僵尸网络可以包括数百万台机器。恶意软件的作者控制着这些僵尸网络,并利用它们做任何事情,从寻找更多易受攻击的主机到发送垃圾邮件或突破安全网站。大多数僵尸网络由 Windows 和 Linux 机器组成,但并不排除 FreeBSD 操作系统也可能被纳入僵尸网络中。

幸运的是,僵尸网络防御和脚本小子防御非常相似;保持软件更新并遵循良好的计算实践会带来很大帮助。

有动机的熟练攻击者

最危险的群体——熟练攻击者——是那些有能力的系统管理员、安全研究员、渗透专家和犯罪分子,他们想要访问你的特定资源。如今,计算机渗透是一个有利可图的犯罪领域,尤其是当受害者拥有可以用于分布式拒绝服务(DDoS)攻击或大规模垃圾邮件传输的资源时。妥协一个网站农场并将其转化为恶意用途是有利可图的。如果你有有价值的公司机密,可能会成为这些入侵者的目标。如果这些人真的想要突破你的网络,他很可能会成功。

尽管如此,能够阻止前三类人群的安全措施会改变熟练攻击者的战术。他将不再通过网络突破你的计算机,而是得以伪装成一名电信维修工,背着一个数据包嗅探器出现在你家门口,或是翻垃圾桶找寻写满密码的旧便签。这大大增加了他的风险,可能会使得入侵变得不值得冒险。如果你能让入侵者的突破计划看起来像一部好莱坞剧本,不管他对你的网络了解多少,那么你的安全措施可能已经相当不错了。

黑客、入侵者及相关渣滓

你会经常听到“黑客”这个词用来描述那些闯入计算机的人。这个词根据说话者的不同有不同的含义。在技术领域,黑客是指那些对技术内部运作感兴趣的人。有些黑客对所有内容都有兴趣,而其他人则专注于某一特定领域。在开源社区中,黑客是一个尊重的称呼。FreeBSD 的主要技术邮件列表是* FreeBSD-hackers@FreeBSD.org*。然而,在大众媒体中,黑客通常是指那些闯入计算机系统的人,故事就到此为止。

为了减少混淆,我建议完全避免使用“黑客”这个词。在本书中,我称那些闯入计算机的人为入侵者。* 技术高手可以被称为各种名字,但他们通常不会反对“哦,伟大而强大的存在”。

  • 两个版本之后,我的编辑仍然不允许我当面说出我对他们的看法。

FreeBSD 安全公告

防范任何攻击者的最佳方法是保持系统的最新状态。这意味着你必须知道何时为系统打补丁,打什么补丁,以及如何打。一个过时的系统是脚本小子的最佳伙伴。

FreeBSD 项目包含一些志愿者,他们专门负责审计源代码,并监控基础操作系统及附加软件的安全问题。这些开发人员维护着一个非常低流量的邮件列表,* FreeBSD-security-notifications@FreeBSD.org ,订阅该列表是一个好主意。虽然你可以通过其他邮件列表来获取一般公告,但安全通知列表是获取 FreeBSD 特定信息的唯一来源。要订阅安全通知邮件列表,请参见 lists.freebsd.org/*。FreeBSD 安全团队会在该邮件列表中一有新的安全公告就发布。

仔细阅读安全公告,并迅速采取措施应对影响到你的那些问题,因为你可以确定脚本小子正在寻找易受攻击的机器。正如第十八章所讨论的,FreeBSD 使得应用安全补丁变得非常简单。

用户安全

记得我说过,你自己的用户是你最大的安全隐患吗?现在你将学习如何管好这些“小家伙”。FreeBSD 提供了多种方式,让用户能够完成工作,而不需要让他们在系统上为所欲为。我们将在这里讨论一些最重要的工具,从添加用户开始。

创建用户账户

FreeBSD 使用标准的 Unix 用户管理程序,如 passwd(1)、pw(8)和 vipw(8)。FreeBSD 还包含了一个友好的交互式用户添加程序 adduser(8)。当然,只有root用户可以添加其他用户。只需在命令行中输入adduser即可进入交互式命令行。

当你第一次运行 adduser(8) 时,它会提示你为所有新用户设置适当的默认值。使用以下示例会话来帮助你为系统确定合适的默认值。

   # adduser
➊ Username: xistence
➋ Full name: Bert Reger
➌ Uid (Leave empty for default):

用户名 ➊ 是账户的名称。我系统上的用户会获得一个由他们的名字首字母、中间名首字母和姓氏组成的用户名。你可以根据自己想出的方案来分配用户名。在这里,我让用户自己选择用户名,这种宽容通常会让我后悔。全名 ➋ 是用户的真实姓名。FreeBSD 接着让你选择一个数字化的用户 ID (UID) ➌。FreeBSD 从 1000 开始编号 UID;虽然你可以更改此设置,但所有低于 1000 的 UID 都保留给系统使用。我建议直接按 ENTER 键选择下一个可用的 UID。

➊ Login group [xistence]: 
➋ Login group is xistence. Invite xistence into other groups? []: www
➌ Login class [default]: 
➍ Shell (sh csh tcsh nologin) [sh]: tcsh
➎ Home directory [/home/xistence]: 
➏ Home directory permissions (Leave empty for default):

用户的默认组 ➊ 非常重要——记住,Unix 权限是根据所有者和组设置的。FreeBSD 默认将每个用户分配到自己的组,通常是大多数设置中最合理的方式。关于系统管理的大部头书籍提供了几种分组方案——随时使用任何符合你需求的方案。如果适当,你现在可以将该用户添加到其他组 ➋,除了主组之外。

登录类 ➌ 指定了用户可以访问的资源级别。我们稍后会在本节中讨论登录类。

Shell ➍ 是命令行环境。虽然系统默认是 /bin/sh,我更喜欢 tcsh。^(1) 如果你对其他 shell 深有依赖,随时可以使用它。精通的用户可以更改自己的 shell。

家目录 ➎ 是用户文件在磁盘上的存放位置。该用户及其主组拥有此目录。如果需要,你可以在目录上设置自定义权限 ➏,通常是为了防止其他用户查看该用户的目录。

➊ Use password-based authentication? [yes]: 
➋ Use an empty password? (yes/no) [no]: 
➌ Use a random password? (yes/no) [no]: y
➍ Lock out the account after creation? [no]: n

密码选项提供了一定程度的灵活性。如果所有用户都熟悉基于密钥的 SSH 身份验证,也许你可以不用密码。与此同时,其余的人都得依赖密码 ➊。

如果你希望用户通过控制台设置自己的密码,可以使用空密码 ➋。第一次连接到该账户的人将设定密码。将空密码作为一种设置,与在氢气飞艇内吸烟一样不太明智。

另一方面,随机密码 ➌ 对于新账户来说是一个好主意。FreeBSD 提供的随机密码生成器足够日常使用。随机密码通常很难记住,这会促使用户尽快更改密码。

当账户被锁定 ➍ 时,没人可以用它登录。这通常是适得其反的。

输入所有信息后,adduser 会将所有内容反馈给您进行审查和确认或拒绝。一旦您确认,adduser 会验证账户设置并为您提供随机生成的密码。然后,它会创建用户的主目录,从/etc/skel复制 shell 配置文件,并询问您是否要设置另一个用户。

配置 Adduser: /etc/adduser.conf

在某些 Unix 系统上创建新用户需要手动编辑/etc/passwd,重建密码数据库,编辑/etc/group,创建主目录,设置该主目录的权限,安装 dotfiles,等等。这使得处理本地自定义变得例行化——如果您手动设置所有内容,就可以轻松管理本地账户设置。adduser(8)程序提供了一组合理的默认值。对于有不同需求的网站,/etc/adduser.conf允许您将这些需求设置为默认值,同时保持高度的自动化。要创建adduser.conf 文件,请运行adduser -C并回答相关问题。

➊ Uid (Leave empty for default):
➋ Login group []: 
➌ Enter additional groups []: staff
➍ Login class [default]: staff
➎ Shell (sh csh tcsh nologin) [sh]: tcsh
➏ Home directory [/home/]: /nfs/u1/home 
➐ Use password-based authentication? [yes]: 
➑ Use an empty password? (yes/no) [no]: 
➒ Use a random password? (yes/no) [no]: yes
➓ Lock out the account after creation? [no]: no

您可能希望从 1,000 以外的地方开始编号 UID。如果您想要更高的初始 UID,请在Uid字段中输入 ➊。不要从 1,000 以下开始。

登录组 ➋ 是默认的用户组。空的登录组意味着用户账户默认会有一个独立的唯一用户组(FreeBSD 默认设置)。

您可以指定新账户默认属于的任何其他用户组 ➌,以及登录类 ➍。我将这两个设置为staff,这样所有新用户都会被添加到该组并分配该类。

为您的用户选择一个默认的 shell ➎。

您的主目录位置 ➏ 可能与独立的 FreeBSD 标准有所不同。在这个例子中,我指定了当多个用户在多台机器上有账户时,常用的 NFS 挂载的主目录样式。

选择新用户的默认密码行为。您可以指定用户是否应该使用密码 ➐,以及初始密码是应该为空 ➑ 还是随机生成 ➒。

最后,决定新账户是否默认锁定 ➓。

您可以在 adduser.conf(5)中找到更多配置设置。虽然您可以在这里设置账户特征,但该文件的格式被认为是 adduser(8)的内部格式。设置名称可能会随着任何 FreeBSD 版本的发布而变化。要更改adduser.conf,请重新运行adduser -C

编辑用户

管理用户不仅仅是创建和删除账户。您需要不时地更改这些账户。虽然 FreeBSD 提供了多个工具用于编辑账户,但最简单的工具是 passwd(1)、chpass(1)、vipw(8)和 pw(8)。这些工具作用于紧密关联的文件/etc/master.passwd/etc/passwd/etc/spwd.db/etc/pwd.db。我们将从这些文件开始,然后回顾编辑这些文件的常用工具。

文件 /etc/master.passwd/etc/passwd/etc/spwd.db/etc/pwd.db 都包含用户账户信息。每个文件的格式和用途略有不同。文件 /etc/master.passwd 是权威的用户账户信息来源,并包含加密形式的用户密码。普通用户没有权限查看 /etc/master.passwd 的内容。然而,普通用户需要访问基本的账户信息;否则,未经授权的系统程序怎么识别用户呢?文件 /etc/passwd 列出了用户账户,移除了所有特权信息(如加密密码)。任何人都可以查看 /etc/passwd 的内容以获取基本的账户信息。

许多程序需要账户信息,而解析文本文件非常缓慢。在如今笔记本超级计算机的时代,缓慢 这个词不再那么有意义,但在迪斯科风靡一时的年代,这确实是一个非常实际的问题。正因如此,BSD 衍生的系统将 /etc/master.passwd/etc/passwd 构建为一个数据库文件。(其他类 Unix 系统在不同的文件中有类似的功能。)文件 /etc/spwd.db 直接来自 /etc/master.passwd,并包含敏感的用户信息,但只有 root 可以读取。文件 /etc/pwd.db 则可以被任何人读取,但它只包含 /etc/passwd 中的有限信息。

每当任何标准的用户管理程序更改 /etc/master.passwd 中的账户信息时,FreeBSD 会运行 pwd_mkdb(8) 来更新其他三个文件。例如,三个程序 passwd(1)、chpass(1) 和 vipw(8) 都允许你修改主密码文件,并且这三个程序都会触发 pwd_mkdb 来更新相关文件。

更改密码

使用 passwd(1) 来更改密码。用户可以更改自己的密码,而 root 用户可以更改任何人的密码。要更改自己的密码,只需在命令提示符下输入 passwd

# passwd
Changing local password for mwlucas
Old Password:
New Password:
Retype New Password:

在更改自己的密码时,passwd(1) 会首先要求输入当前密码。这是为了确保没有其他人可以在你不知情的情况下更改你的密码。虽然走开时注销总是个好习惯,但当你没有注销时,passwd(1) 中的这个简单检查可以防止恶作剧者真正让你烦恼。然后输入两次新密码,操作就完成了。当你是超级用户并且想更改其他用户的密码时,只需在 passwd 后给出用户名作为参数。

# passwd mwlucas
Changing local password for mwlucas
New Password:
Retype New Password:

请注意,root 用户不需要知道用户的旧密码;root 用户可以以任何方式更改系统中的任何用户账户。

用户管理与 $EDITOR

像 chpass 和 vipw 这样的用户管理工具(以及许多其他系统管理工具)会弹出一个文本编辑器窗口,让你进行修改。这些工具通常会检查环境变量$EDITOR,以查看你偏好的文本编辑器。$EDITOR允许你默认使用 vi、Emacs 或其他已安装的编辑器。我推荐 Vigor,它是 vi(1)的一个克隆,具有动画回形针帮助系统,这可能会让习惯使用老版 Microsoft Office 的用户感到更舒适。

使用 chpass(1)更改账户

账户除了密码外,还有更多关联的信息。chpass(1)工具允许用户编辑他们能接触到的账户信息。例如,如果我以普通用户身份运行 chpass(1),我会看到一个编辑器,里面包含以下内容:

#Changing user information for mwlucas.
Shell: /bin/tcsh
Full Name: Michael W Lucas
Office Location:
Office Phone:
Home Phone:
Other information:

我可以编辑账户中的六个信息字段。第一个是我的 shell,可以设置为/etc/shells中列出的任何 shell(参见 “Shells 和 /etc/shells” 第 178 页)。我可以更改我的全名;也许我想列出我的全名中的中间名,或者可能我希望其他系统用户称我为斯卡比斯先生。我还可以更新我的办公位置和办公电话,以便同事们能轻松找到我。这是 BSD 在大学校园中发展时非常有用的功能,那时系统用户很少知道彼此的物理位置。现在我们有了广泛的在线目录和更多的计算机,这个功能就不那么有用了。我通常将我的家庭电话号码设置为 911(在英国为 999),并在其他字段中填写一些个人信息。

还要注意,作为普通用户,我无法更改的内容。系统管理员设置了我的主目录,即使系统有一块新硬盘,空间足够存放我的 MP3 收藏,我也不能更改它。我的 UID 和 GID 号也类似,是由系统或系统管理员分配的。

另一方面,如果我运行chpass xistence,其提升的权限给了我一个完全不同的视角。

   #Changing user information for xistence.
   Login: xistence
➊ Password: $6$D9b4FFD0kHK2sPSP$bXUFTQqV/QposXw2KTlswzpvoz4HBo8...
   Uid [#]: 1001
   Gid [# or name]: 1001
   Change [month day year]:
   Expire [month day year]:
   Class:
   Home directory: /home/xistence
   Shell: /bin/tcsh
   Full Name: Bert Regeer
   Office Location:
   Office Phone:
   Home Phone:
   Other information:

作为 root,你可以对那个可怜的用户做任何你想做的事情。将他的登录名改为megaloser只是你能制造的混乱的开始。你甚至可以访问该用户的哈希密码 ➊。除非你擅长计算密码哈希,否则不要修改这个字段。使用 passwd(1)来更安全、更可靠地更改用户的密码。你也可以更改用户的主目录,尽管 chpass(1)不会移动用户的文件;你必须手动复制它们。

你还可以设置密码更改和账户过期的日期。如果你刚刚更改了一个用户的密码,并希望他在第一次登录时更改密码,密码过期功能就非常有用。账户过期功能在某人请求账户时尤其有用,尤其是当他们坚持只需要账户使用有限时间时。你可能会忘记删除该账户,但 FreeBSD 永远不会忘记。这两个字段都采用 月 日 年 的日期格式,但只需要输入月份的前三个字母。例如,要让一个用户的密码在 2028 年 6 月 8 日到期,我会在 Change 空间输入 Jun 8 2028。一旦用户更改了密码,密码过期字段会被清空,但只有系统管理员才能延长账户的过期日期。

大锤:vipw(8)

虽然 chpass(1) 适用于编辑单个账户,但当你必须编辑多个账户时该怎么办?假设你的系统有数百个用户,并且有一个全新的硬盘用于主目录分区。你真想执行 chpass(1) 数百次吗?这时,vipw(8) 就派上用场了。

直接使用 vipw(8) 编辑 /etc/master.passwd。编辑完成后,vipw(8) 会检查密码文件的语法,确保你没有破坏任何内容。然后,它会保存新的密码文件并运行 pwd_mkdb(8)。尽管 vipw(8) 可以保护你的密码文件免受许多基本错误,但如果你足够聪明,仍然可能会弄糟。你必须了解密码文件的格式,才能正确使用 vipw(8)。

如果 /etc/master.passwd 中的信息与其他文件中的信息冲突,则以 /etc/master.passwd 中的信息为准。例如,/etc/master.passwd 中显示的主要组是正确的,即使 /etc/group 中没有将该用户列为成员。这种“master.passwd 永远是正确的”逻辑深深植根于用户管理中。

/etc/master.passwd 中的每一行都是一个账户记录,包含 10 个由冒号分隔的字段。这些字段如下:

用户名

该字段是系统管理员创建的账户名,或者是安装时为提供某些系统服务而创建的用户名。FreeBSD 包括用于系统管理的用户,如 root、daemon、games 等。每个用户拥有基础系统的一部分。FreeBSD 还提供了用于常见服务的账户,如为 web 服务器保留的 www 用户。附加软件可能还会添加自己的系统账户。

加密密码

第二个字段是加密密码。系统用户没有密码,因此你不能以他们的身份登录。用户账户在此处有一串看起来随机的字符。

用户 ID

第三个字段是用户 ID 数字,或UID。每个用户都有一个唯一的 UID。

组 ID

同样,第四个字段是组 ID 数字,或GID。这是用户的主要组。通常,这与 UID 相同,且该组的名称与用户名相同。

用户类别

下一个字段是用户在 /etc/login.conf 中定义的用户类别(参见 “限制系统使用” 第 188 页(page 188))。

密码过期

这个字段与通过 chpass(1) 设置的密码过期日期相同,不过这里的时间以自纪元以来的秒数存储。使用 date -j+%s 输出格式可以将真实日期转换为纪元秒数。要将 2018 年 6 月 1 日午夜转换为纪元秒数,可以运行 date -j 201806010000 '+%s'

账户过期

这个字段允许你在某一天使账户自动关闭。只需像设置密码过期一样设置账户过期日期。

个人数据

这个字段也被称为 gecos 字段,原因有些历史上的晦涩。它包含用户的真实姓名、办公室号码、工作电话和家庭电话,所有内容以逗号分隔。不要在此字段中使用冒号;/etc/master.passwd 保留冒号作为字段分隔符。

用户的主目录

第九个字段是用户的主目录。默认情况下为 /home/,但你可以将其移动到任何适当的位置。更改此字段时,你还需要移动实际的主目录及其文件。没有主目录的用户默认无法登录,不过可以通过 login.conf 中的 requirehome 设置进行更改。

用户的 Shell

最后的字段是用户的 shell。如果这个字段为空,系统会分配给用户老式的 /bin/sh

虽然 chpass(1) 让你可以修改单个用户账户,但 vipw(8) 让你可以操作整个用户库。使用时要小心!

删除用户

rmuser(8) 程序用于删除用户账户。你会被提示输入要删除的用户名,并询问是否删除该用户的主目录。其实你只需要做这些,毕竟销毁比创建要简单得多。

使用 pw(8) 脚本

pw(8) 命令提供了一个强大的命令行界面来管理用户账户。虽然 useradd(8) 会以友好的方式引导你设置账户,但 pw(8) 让你可以在一行命令中指定所有内容。我发现 pw(8) 对日常使用来说有些笨重,但如果你管理很多用户账户,它是无价的。

我确实使用 pw(8) 来锁定账户。虽然被锁定的账户仍然存在,但没人能登录。我曾在客户拖欠账单时有效使用这个方法;当用户无法登录时,他们通常会很快联系,但他们的网站依然在线,邮件也继续积累。

# pw lock xistence

当 Bert 道歉时,我将解锁他的账户。

# pw unlock xistence

如果你需要脚本来管理用户,强烈建议阅读 pw(8) 的手册页。

Shell 和 /etc/shells

Shell 是提供用户命令提示符的程序。不同的 shell 行为各异,提供不同的快捷方式和功能。许多人对特定的 shell 非常依赖,如果他们的 shell 在某个系统上不可用,常常会抱怨。软件包集合中包含了许多不同的 shell。

文件 /etc/shells 包含所有合法用户 shell 的列表。当你从端口或软件包安装一个 shell 时,它会在 /etc/shells 中添加一个相应的条目。如果你从源代码编译自己的 shell,而没有使用 FreeBSD 端口,你必须在 /etc/shells 中列出该 shell 的完整路径。

如果用户的 shell 没有列在 /etc/shells 中,FTP 守护进程将不允许用户通过 FTP 登录。如果你使用 /sbin/nologin 作为仅限 FTP 使用的用户 shell,你必须将其添加到该文件中,尽管处理此类用户的更好方法是使用登录类,正如本章后面讨论的那样。

root、用户组与管理

Unix 安全性被认为有些粗糙,因为一个超级用户,root,可以做任何事。其他用户只是低级的仆人,忍受着 root 对他们的束缚。问题在于,root 并没有很多不同种类的束缚,且无法很好地个性化这些束缚。虽然这话有一定道理,但一位称职的管理员可以通过组合用户组和权限,安全地解决几乎任何问题。

根密码

某些操作需要对系统的绝对控制,包括操作核心系统文件,如内核、设备驱动程序和身份验证系统。这些活动是为 root 用户设计的。

要使用根密码,你可以在控制台登录提示符下以 root 身份登录,或者,如果你是 wheel 组的成员,可以以自己的身份登录并使用切换用户命令 su(1)。(我们稍后会在本节中讨论用户组。)我建议使用 su;它会记录谁使用了它,并且可以在远程系统上使用。这个命令非常简单:

# su
Password:
#

接下来,使用 id(1) 命令检查你当前的用户 ID:

# id
uid=0(root) gid=0(wheel) groups=0(wheel), 5(operator)
#

现在你拥有了系统——我确实是指拥有它。考虑每一个按键;粗心大意可能会让你的硬盘恢复到未经格式化的原始状态,变成空旷的废土。而且,根密码要谨慎共享,如果需要分享,尽量少分享,因为任何拥有根密码的人都可以对系统造成无限的损害。

记住,只有在 wheel 组中的用户才能通过 su(1) 命令使用根密码成为 root。任何人都可以在系统控制台使用根密码,这也是为什么系统的物理保护至关重要。如果你把根密码交给一个没有物理访问控制台权限的普通用户,他们可以输入 su 命令,并多次输入根密码,但仍然无法成功。

这自然引出了一个问题:“谁需要 root 访问权限?”本书中讨论的许多配置都需要使用根密码。一旦系统正常运行,你可以大大减少或停止使用根密码。对于那些绝对需要 root 权限的任务,我推荐使用 sudo 包,可能还需要我的书《Sudo Mastery》(Tilted Windmill Press, 2013)。减少对 root 访问权限需求的最简单方法之一是通过正确使用用户组。

用户组

类 Unix 操作系统将用户分类为 ,每个组由执行相似管理职能的人员组成。系统管理员可以定义一个名为 webmasters 的组,将编辑网页的人员账户添加到该组,并设置与网页相关文件的权限,以便该组的成员可以编辑这些文件。她还可以创建一个名为 email 的组,将电子邮件管理员添加到该组,并相应地设置邮件相关文件的权限。以这种方式使用组是系统管理中一个强大且常被忽视的工具。

任何用户都可以通过 id(1) 查明自己所属的组。前面的示例显示了用户 root 属于 wheeloperator 组。然而,root 是一个特殊用户,几乎可以做任何事。这是我的账户,更加贴近普通用户的实际情况:

# id
uid=1001(mwlucas) gid=1001(mwlucas) groups=1001(mwlucas),0(wheel),68(dialer),1
0001(webmaster)

我的 UID 是 1001,用户名是 mwlucas。我的 GID,即主组 ID,也是 1001,且我的主组名为 mwlucas。这是系统中第一个用户的标准配置,即使是在后续的用户中,唯一变化的是分配给账户和主组的数字。更有趣的是我被分配到的其他组:除了我的主组,我还属于 wheeldialerwebmaster 组。wheel 组的成员可以使用 root 密码成为 root,dialer 组的成员可以使用 tip(1) 而无需成为 root,webmaster 组的成员可以编辑本地系统上的网页文件。这些组在我的系统上具有特殊权限,作为这些组的成员,我继承了这些权限。

组信息在 /etc/group 中定义。

/etc/group

文件 /etc/group 包含所有组信息,除了用户的主组(该主组在 /etc/master.passwd 中与用户账户一起定义)。/etc/group 中的每一行包含四个由冒号分隔的字段:组名、组密码、组 ID 号码和成员列表。

这是一个示例条目:

wheel:*:0:root,mwlucas,xistence

组名是一个对人类友好的名称。这个组名为 wheel。组名是任意的;如果你愿意,可以把一组用户称为 lackeys。选择能够让你了解组别用途的组名;尽管你可能会记得你的 lackeys 可能会编辑公司网页,但你的同事能理解这一点吗?

第二个字段,组密码,是一个好主意,但结果变成了安全噩梦。现代类 Unix 系统不再对组密码做任何事情,但该字段仍然保留,因为旧程序期望在这个位置找到内容。星号是一个占位符,用来应付这些程序。

第三个字段给出了组的唯一数字组 ID(GID)。许多程序使用 GID 而不是名称来识别组。wheel 组的 GID 为 0,最大 GID 为 65535。

最后是一个由逗号分隔的所有组内用户的列表。用户 rootmwlucasxistencewheel 组的成员。

更改组成员资格

如果你想将一个用户添加到某个组,只需要将该用户的用户名添加到该组的行末。例如,wheel 组是允许使用 root 密码的用户列表。这里,我将 rwatson 添加到 wheel 组:

wheel:*:0:root,mwlucas,xistence,rwatson

说实话,我说服 rwatson(领先的安全研究员和前 FreeBSD 基金会主席)承担我任何系统的 sysadmin 职责的可能性几乎为零,但试试看还是值得的。

创建组

创建新组时,只需要一个组名和一个组 ID 号码。从技术上讲,你甚至不需要为该组指定成员;某些程序以组成员身份运行,FreeBSD 使用组权限来控制这些程序,就像控制用户一样。

传统上,GID 是按列表顺序分配的下一个数字。GID 是一个介于 0 和 65535 之间的任意数字。一般来说,低于 1000 的 GID 保留给操作系统使用。需要专用组 ID 的程序通常会使用这个范围内的号码。用户账户从 1001 开始分配 GID,并逐渐递增。一些特殊组可能从 65535 开始编号,并逐渐递减。

使用组来避免 root 权限

除了安全隐患外,root 密码的分发政策还可能导致组织内部的分歧。许多 sysadmins 拒绝与负责维护系统一部分的人共享 root 密码,但他们又不提供替代方案,从而阻止这些人完成工作。其他 sysadmins 则几乎把 root 密码分发给任何想要的人,然后当系统变得不稳定时抱怨。两种态度从长远来看都是无法维持的。就我个人而言,我不希望你在我的系统上拥有 root 权限。虽然拥有 root 权限很方便,但当系统崩溃时缺乏责任感更方便。

一个常见的情况是,初级 sysadmin 负责系统的某一部分。我曾有很多 DNS 管理员在我下面工作;^(2) 这些人从不安装软件、重新编译内核或执行其他 sysadmin 任务。他们只处理邮件、更新区域文件并重新加载 named 守护进程。新手 sysadmins 通常认为他们需要 root 访问权限来做这些工作。其实不需要,你可以使用组权限。

创建你自己的组,由执行相似管理职能的人组成,可以避免分发 root 密码,同时仍然允许人们完成他们的工作。在本节中,我们将实现基于组的名称服务器文件访问控制。相同的原则也适用于你选择保护的任何文件。邮件和 Web 配置文件是其他常见的组管理对象。

系统账户

FreeBSD 为集成程序保留了一些用户账户名。我们将在第十九章讨论这些无特权账户。例如,nameserver 在用户账户 bind 和组 bind 下运行。如果入侵者攻破了 nameserver,她只能以 bind 用户的权限访问系统。

不要让用户以这些用户身份登录。它们设计上并不是作为交互账户的。而且,不要允许系统账户用户的组拥有为该功能创建的文件。创建一个单独的用户和组来拥有程序文件。这样,我们假设的入侵者甚至无法编辑 DNS 服务器使用的文件,从而进一步减少潜在的损害。如果程序定期更新这些文件(例如,数据库的后端存储),你必须给予程序访问权限,但人类通常不需要编辑这些文件。类似地,数据库也不应该能够编辑其自己的配置文件。

管理员组创建

创建拥有文件的组的最简单方法是使用 adduser(8)创建一个拥有文件的用户,然后将该用户的主组作为文件的组。因为我们已经有了一个叫bind的用户,所以我们将创建一个名为dns的管理员用户。用户名并不重要,但你应该选择一个大家都能识别的名字。

给你的管理员用户设置一个nologin的 shell,这会将 shell 设置为/sbin/nologin。这样可以防止任何人实际以管理员用户身份登录。

如果你愿意,可以为这些类型的用户指定特定的 UID 和 GID。我有时会选择类似于其关联服务账户所使用的 UID 和 GID 号码。例如,用户 bind 的 UID 和 GID 都是 53。我可以为用户 dns 指定 UID 为 10053,这样就容易辨识了。其他时候,我会从 65535 开始为我的管理员组编号,然后逐渐向下。这并不重要,只要在一个组织内保持完全一致。

不要将这个管理员用户添加到任何其他组中。在任何情况下,都不要将这个用户添加到特权组中,例如 wheel!

每个用户都需要一个主目录。对于管理员用户,/nonexistent 的主目录非常合适。毕竟,这个用户的文件存储在系统的其他地方。

最后,让 adduser(8)禁用该账户。虽然 shell 已经阻止了登录,但额外的防御层总是有益的。

现在你已经有了一个管理员用户和一个组,你可以将文件的所有权分配给该用户。每个文件都有一个用户和一个组。你可以使用ls -l查看现有的文件所有权和权限。(如果你忘记了 Unix 权限如何工作,可以阅读 ls(1)和 chmod(1)。)许多系统管理员非常关注文件所有者,对全局权限关注较少,只是匆匆看一下组权限。

# ls -l
total 3166
-rw-r-----  1 mwlucas  mwlucas    79552 Nov 11 17:58 rndc.key
-rw-rw-r--  1 mwlucas  mwlucas  3131606 Nov 11 17:58 mwl.io.db

在这里,我创建了两个文件。第一个文件,rndc.key,可以由用户mwlucas读取和写入。任何在mwlucas组中的人都可以读取它,但其他人无法操作它。文件mwl.io.db可以由用户mwlucas或任何在mwlucas组中的人读取或写入,但其他人只能读取该文件。如果你在mwlucas组中,你可以编辑文件mwl.io.db而不需要成为 root 用户。

使用 chown(1)更改文件的所有者和组。你必须知道你想更改所有权的用户和组的名称。在这种情况下,我们要将两个文件的所有权都更改为用户 dns 和组 dns。

# chown dns:dns rndc.key
# chown dns:dns mwl.io.db 
# ls -l
total 3166
-rw-r-----  1 dns  dns    79552 Nov 11 17:58 rndc.key
-rw-rw-r--  1 dns  dns  3131606 Nov 11 17:58 mwl.io.db

这些文件现在由用户 dns 和组 dns 拥有。任何在 dns 组中的人都可以编辑mwl.io.db而不需要使用 root 密码。最后,这个文件可以被运行 nameserver 的用户 bind 读取。将你的 DNS 管理员添加到/etc/group中的 dns 组,然后他们就可以执行他们的工作了。

DNS 管理员可能认为他们需要 root 密码来重新启动 nameserver 程序。然而,这可以通过 rndc(8)轻松管理。其他任务可以通过 cron 作业或附加程序 sudo(8)来管理。

如果你只需要一个组而不是一个管理员用户,可以使用 vigr(8)编辑/etc/group

有趣的默认组

FreeBSD 随附了几个默认组。大多数由系统使用,对系统管理员来说并不是特别重要——你应该知道它们的存在,但这与每天与它们打交道是两回事。在表 9-1 中,我为你的娱乐和启发展示了最有用、有趣和令人好奇的默认组。添加你自己的组可以简化系统管理,但这里列出的组在每个 FreeBSD 系统上都可用。

表 9-1: FreeBSD 系统组

组名 用途
audit 可以访问 audit(8)信息的用户
authpf 可以通过 PF 数据包过滤器进行身份验证的用户
bin 一般系统程序的组所有者
bind BIND DNS 服务器软件的组
daemon 用于各种系统服务,如打印系统
_dhcp DHCP 客户端操作
dialer 可以访问串口的用户;对于调制解调器和 tip(1)非常有用
games 游戏文件的所有者
guest 系统访客(几乎不使用)
hast 被 hastd(8)使用的文件
kmem 可以访问内核内存的程序,如 fstat(1)、netstat(1)等
mail 邮件系统的所有者
mailnull sendmail(8)或其他邮件服务器的默认组
man 拥有未压缩 man 页面的用户
network 拥有像 ppp(8)这样的网络程序的用户
news 拥有 Usenet 新闻软件的用户(可能未安装)
nobody 为无特权用户 nobody 设置的主组,供 NFS 使用
nogroup 没有特权的组,供 NFS 使用
operator 可以访问驱动器的用户,通常用于备份目的
_pflogd PF 日志记录组
proxy FTP 代理组,位于 PF 数据包过滤器中
smmsp Sendmail 提交的组
sshd SSH 服务器的所有者(参见 第二十章)
staff 系统管理员(来自 BSD 的大学根源,当时用户是职员、教职工或学生)
sys 另一个系统组
tty 可以写入终端的程序,如 wall(1)
unbound 与 unbound(8) DNS 服务器相关的文件和程序
uucp 与 Unix 到 Unix 复制协议相关的程序组
video 可以访问 DRM 和 DRI 视频设备的组
wheel 可以使用 root 密码的用户
www 网站服务器程序(不是文件)
_ypldap LDAP 支持的 YP 服务器 ypldap(8) 所需的文件

我知道很少有人使用互联网新闻或 UUCP,你可能认为可以将这些组重用于其他目的。然而,实际上创建一个新的组会比以后引起混淆更好。组 ID 号码并不短缺。

调整用户安全性

通过设置账户的限制,你可以防止任何单个用户使用过多的内存、处理器时间或其他系统资源。现在即使是小型计算机也有非常快的处理器和大量的内存,这些限制不再那么重要,但在有数十或数百个用户的系统中,这仍然非常有用。你还可以控制用户可以从哪里登录。

限制登录能力

FreeBSD 每次用户尝试登录时都会检查 /etc/login.access。如果 login.access 中包含禁止该用户登录的规则,登录尝试会立即失败。默认情况下,该文件没有规则,这意味着提供有效用户名和密码的任何人都没有任何限制。

/etc/login.access 文件有三个由冒号分隔的字段。第一个字段表示授予(+)或拒绝(-)登录权限;第二个字段是用户或用户组的列表;第三个字段是连接源的列表。你可以使用 ALLALL EXCEPT 语法,这让你可以创建简单但表达力强的规则。规则按先到先得的方式检查。当 login(1) 找到一个用户和连接源匹配的规则时,连接会立即被接受或拒绝,因此规则的顺序非常重要。默认情况下允许登录。

例如,要仅允许 wheel 组的成员从系统控制台登录,你可以尝试以下规则:

+:wheel:console

然而,这条规则的问题在于它实际上并没有拒绝用户的登录权限。由于默认情况下接受登录,而这条规则只是明确地授予 wheel 组中的用户登录权限,所以没有任何变化。Bert 肯定不在 wheel 组中,但如果他尝试登录,没有规则会拒绝他访问。

你可以尝试两个像这样的规则:

+:wheel: console
-:ALL:console

这组规则可以实现预期的效果,但比你需要的要长。改用 ALL EXCEPT 会更简洁。

-:ALL EXCEPT wheel: console

这条规则可以最快地拒绝不需要的登录,并且管理员出错的风险较小。作为一种规则,最好通过拒绝登录而不是允许它们来构建 login.access 列表。FreeBSD 在遇到此规则时会立即拒绝非 wheel 用户在控制台上的登录。

通过添加最后一条规则,将默认设置从“允许访问”更改为“拒绝访问”。

-:ALL:ALL

任何不匹配之前允许规则的登录请求都会被拒绝。

login.access 中的最后一个字段——连接源,可以使用主机名、主机地址、网络号、域名或特殊值 LOCALALL。让我们来看看它们是如何工作的。

主机名

主机名依赖于 DNS 或 hosts 文件。如果你怀疑你的域名服务器可能遭受入侵或攻击,避免使用主机名;入侵者可以将任何 IP 地址绑定到主机名,从而欺骗你的系统接受连接,且域名服务器故障可能会完全锁定你。尽管如此,仍然可以使用像这样的规则:

-:ALL EXCEPT wheel:fileserver.mycompany.com

属于 wheel 组的用户可以从文件服务器登录,但其他人不能。

主机地址和网络

主机地址的工作方式与主机名类似,但它们不受 DNS 故障或欺骗的影响。

-:ALL EXCEPT wheel:203.0.113.5

网络号是一个截断的 IP 地址,像这样:

-:ALL EXCEPT wheel:203.0.113.

这个网络号允许任何在 wheel 组中的用户从 IP 地址以 203.0.113 开头的机器上登录,并拒绝其他所有来自这些 IP 地址的访问。

LOCAL

最复杂的匹配是 LOCAL,它匹配任何没有点号的主机名(通常只有本地域的主机)。例如,* www.mwl.io * 认为任何位于 mwl.io 域中的机器都匹配 LOCAL。DNS 欺骗很容易绕过这个过滤器。尽管我的桌面声称它的主机名是 storm.mwl.io,但它的 IP 地址有反向 DNS 记录,声称它位于我的有线调制解调器提供商的网络中。主机 www.mwl.io 认为我的桌面不在同一个域,因此不是本地的。因此,我不能使用 LOCAL 验证方法。

类似地,任何拥有 IP 地址块的人都可以给他们的地址设置任意所需的反向 DNS。最好避免使用 LOCAL 限制。

ALL 和 ALL EXCEPT

ALL 匹配所有内容,ALL EXCEPT 匹配除了你指定的内容以外的所有内容。根据我的观点,这些是最有用的连接源。例如,如果你有一台仅可从几个管理工作站访问的高安全性机器,你可以有这样一条规则:

-:ALL EXCEPT wheel:ALL EXCEPT 203.0.113.128 203.0.113.44
将一切联系在一起

这些规则的目的是构建一个符合你现实世界策略的登录政策。如果你提供通用服务,但只允许系统管理员远程登录,那么一行 login.access 就可以防止其他任何用户登录:

-:ALL EXCEPT wheel:ALL

如果你能忍受这样严格的限制,这很好。另一方面,我曾在几家使用 FreeBSD 提供客户端服务的互联网服务提供商工作。普通客户如果没有 Shell 账户,就不能登录到服务器。系统管理员可以远程登录,DNS 和 Web 团队成员(属于 dns 和 webmasters 组)也可以。只有系统管理员才能登录到控制台。

-:ALL EXCEPT wheel:console
-:ALL EXCEPT wheel dns webmasters:ALL

login.access中设置一次,然后让组成员身份控制所有远程登录。

限制系统使用

你可以通过登录类提供更具体的控制。通过/etc/login.conf管理的登录类,定义了为用户提供的资源和信息。每个用户都会被分配一个类,每个类对可用的系统资源有限制。当你更改类的限制时,所有用户在下次登录时都会获得新的限制。在创建用户账户时设置用户的类,或者通过 chpass(1)稍后更改。

类定义

默认的login.conf从默认类开始,这是没有其他类的账户所使用的类。这个类基本上给用户提供了对系统资源的无限访问权限,适用于用户数量有限的应用服务器。如果这符合你的需求,则无需调整文件。

每个类定义由一系列变量赋值组成,定义用户的资源限制、计费和环境。类定义中的每个变量赋值以冒号开始和结束。反斜杠字符是一个续行字符,用来表示类在下一行继续,从而使文件更易读。以下是一个类开头的示例:

➊default:\
      ➋:passwd_format=➌sha512:\
        :copyright=/etc/COPYRIGHT:\
        :welcome=/etc/motd:\
--snip--

这个类被称为default ➊。这里展示了这个类中的几个变量中的三个。例如,变量passwd_format ➋被设置为sha512 ➌。通过这些变量赋值和类名,描述了该类,你可以通过将用户分配到另一个类来改变用户在系统上的体验。

login.conf中的某些变量没有值,而是通过存在来改变账户行为。例如,requirehome变量仅通过在类中出现就生效。如果该值存在,用户必须有一个有效的主目录。

       :requirehome:\

编辑login.conf后,必须更新登录数据库以使更改生效。

# cap_mkdb /etc/login.conf

这会重建用于快速查找的数据库文件/etc/login.conf.db,类似于/etc/spwd.db

默认的/etc/login.conf包括几个用户的示例类。如果你想知道在各种情况下应该对用户施加什么样的限制,可以查看这些示例。接下来的部分提供了关于可以在登录类中设置内容的想法。要查看你使用的 FreeBSD 版本中支持的完整设置列表,请阅读 man login.conf(5)。

资源限制

资源限制允许你控制任何一个用户在任何时刻可以独占系统的多少资源。如果你有几百个用户登录到一台机器,而其中一个用户决定编译 LibreOffice,那么这个用户将占用远超过他应得的处理器时间、内存和 I/O 资源。通过限制一个用户可以独占的资源,你可以让系统对所有用户更具响应性。

表 9-2 定义了资源限制的 login.conf 变量。

表 9-2: 一些 login.conf 变量用于限制资源使用

变量 描述
cputime 单个进程可以使用的最大 CPU 时间
filesize 任意一个文件的最大大小
datasize 一个进程可以消耗的最大数据内存大小
stacksize 进程可使用的最大栈内存量
coredumpsize 核心转储的最大大小
memoryuse 一个进程可以锁定的最大内存量
maxproc 用户可以运行的最大进程数
openfiles 每个进程可以打开的最大文件数
Sbsize 用户应用程序可以设置的最大套接字缓冲区大小

请注意,资源限制通常是按进程设置的。如果你为每个进程允许 200MB 的内存,并允许每个用户有 40 个进程,那么你实际上给每个用户提供了大约 8GB 的内存。也许你的系统有很多内存,但它真的有这么多吗?

当前和最大资源限制

除了前面列出的限制外,你还可以指定当前和最大资源限制。当前限制是建议性的,用户可以根据需要覆盖它们。这在一个合作系统中效果良好,在这个系统中,多个用户自愿共享资源,但你希望通知那些超过标准资源分配的用户。许多用户希望做一个好公民,当被告知他们接近资源限制时,他们会愿意配合。^(3) 用户不能超过最大限制。

如果你没有指定资源限制为当前或最大值,FreeBSD 将其视为最大限制。

要指定当前限制,可以在变量名后加上 -cur。要设置最大限制,可以加上 -max。例如,要设置用户可以拥有的进程数量的当前和最大限制,使用以下输入:

        --snip--
        :maxproc-cur: 30:\
        :maxproc-max: 60:\
        --snip--

资源限制的一个对应概念是资源核算。如今,核算的重要性已经不如过去,当时今天这种便宜的计算机价格可能高达几百万美元,因此我们在本书中不会讨论它。现在,限制单个用户占用系统资源比为每个 CPU 周期计费更为重要。然而,你应该知道这种功能是存在的。

如果你需要更复杂的资源限制,调查 rctl(8)。

类别环境

你也可以在 /etc/login.conf 中定义环境设置。与在默认的 .cshrc.profile 中设置它们相比,这种方法通常更有效,因为 login.conf 设置会立即影响所有用户账户,下次登录时生效。某些 shell,如 zsh(1),不会读取这些配置文件,因此使用类环境可以为这些用户设置正确的环境变量。

所有环境字段都识别两个特殊字符。波浪线(~)表示用户的主目录,而美元符号($)表示用户名。以下是一些来自默认类的示例,说明了这一点:

        :setenv=MAIL=➊/var/mail/$,BLOCKSIZE=K,FTP_PASSIVE_MODE=YES:\
        :path=/sbin /bin /usr/sbin /usr/bin /usr/games /usr/local/sbin /usr/
local/bin /usr/X11R6/bin ➋~/bin:\

通过使用 $ 字符,环境变量 MAIL 被设置为 /var/mail/ ➊。类似地,PATH 变量中的最后一个目录是用户主目录中的 bin 子目录 ➋。

表 9-3 列出了常见的 login.conf 环境设置。

表 9-3: 常见的 login.conf 环境设置

变量 描述
hushlogin 如果存在,则在登录时不会显示系统信息。
ignorenologin 如果存在,即使 /var/run/nologin 存在,用户仍然可以登录。
manpath $MANPATH 环境变量的目录列表。
nologin 如果存在,用户无法登录。
path $PATH 环境变量的目录列表。
priority 用户进程的优先级(nice)(参见第二十一章)。
requirehome 用户必须拥有有效的主目录才能登录。
setenv 用逗号分隔的环境变量及其值列表。
shell 登录时要执行的 shell 的完整路径。此设置会覆盖 /etc/master.passwd 中的 shell。用户的 $SHELL 包含密码文件中的 shell,从而导致环境不一致。对这个设置进行操作是惹恼用户的好方法。
term 默认的终端类型。几乎任何试图设置终端类型的操作都会覆盖此设置。
timezone $TZ 环境变量的默认值。
umask 初始的 umask 设置;应该始终以 0 开头,参见 builtin(1)。
welcome 登录欢迎信息的路径,通常是 /etc/motd

请记住,类的更改会影响该类中的所有用户。如果用户需要不同于类设置的更改,则需要更改其所属的类。

密码和登录控制

与环境设置不同,很多设置可以在登录类以外的地方配置,大多数登录和认证选项只能在登录类中控制。以下是一些常见的认证选项:

passwd_format

此选项设置用于存储密码的加密哈希算法,存储在/etc/master.passwd中。默认值是sha512,用于 SHA512 哈希。其他允许的选项有des(DES)、blf(Blowfish)、md5sha256(SHA256)。DES 和 Blowfish 在需要在不同的类 Unix 操作系统之间共享密码文件时非常有用,但它们的安全性较弱。SHA256 则是为了与较旧的密码文件兼容,这些文件在 SHA512 成为默认值之前使用。

mixpasswordcase

如果设置了此选项,FreeBSD 会警告用户如果他将密码更改为全小写的单词。尽管名称如此,但全大写的密码也符合此选项的要求。

host.allow, host.deny

这些值允许该类用户使用 rlogin 和 rsh。像你那次被怪异的室友强行喂的模糊绿色肉一样,尽量避免使用它们。

times.allow

此选项允许您安排用户何时可以登录,使用逗号分隔的日期和时间列表。日期使用星期几的前两个字母表示(Su、Mo、Tu、We、Th、Fr 和 Sa)。时间采用标准 24 小时制。例如,如果用户只能在周三的上午 8 点到下午 5 点之间登录,则使用以下条目:

        :times.allow=We8-17:\

times.deny

此选项允许您指定用户无法登录的时间窗口。请注意,这不会强制已登录的用户退出。格式与times.allow相同。如果times.allowtimes.deny时间重叠,则times.deny优先。

你无法让那个过度工作的开发者回家,但你可以防止他再打开一个终端窗口。

文件标志

所有类 Unix 操作系统都具有相同的文件系统权限,为文件的所有者、其所属组以及所有其他用户分配读取、写入和执行权限。FreeBSD 通过文件标志扩展了权限机制,这些标志与权限一起工作,以增强系统的安全性。

许多标志的效果会根据系统的 securelevel 级别不同而有所不同,我们将在下一节中讨论。理解 securelevel 需要对文件标志的了解,而文件标志又依赖于 securelevel。现在,只需在遇到 securelevel 时点点头微笑,接下来的几页将让一切变得清晰。

一些文件标志仅在特殊情况下有用。我们将只讨论最常用的标志。请参阅 chflags(1)获取完整列表。

许多标志有多个名称;虽然在 ls(1)的输出中只会显示一个名称,但你可以在命令行中使用任何名称。出现这些替代名称是因为人们在键入sappend时,不希望出现错误,而他们本来是想键入sappnd。在这里,我首先展示标志的主要名称,然后是更易于理解的别名。

sappnd, sappend

这个系统级的追加-only 标志只能由 root 设置。带有此标志的文件可以被追加,但不能被删除或以其他方式编辑。这个标志对于日志文件尤其有用。如果一个用户的 .history 文件设置了 sappnd,并且该帐户被入侵,情况会变得很有趣。由于一个常见的入侵者策略是删除 .history 文件或将其符号链接到 /dev/null,以便管理员无法看到发生了什么,sappnd 确保了脚本小子无法通过这种方式抹去痕迹。回顾一个尝试删除 sappnd 文件的记录几乎是有趣的;你几乎可以看到攻击者的挫败感,随着他尝试各种方法而增加。^(4) 当系统运行在安全级别 1 或更高时,这个标志不能被移除。

schg

只有 root 可以设置系统级不可变标志。带有此标志的文件无法以任何方式更改。它们不能被编辑、移动、替换或覆盖。基本上,文件系统本身会阻止所有更改文件的尝试。当系统运行在安全级别 1 或更高时,这个标志不能被移除。

sunlnk

只有 root 可以在文件上设置系统级不可删除标志。文件可以被编辑或修改,但不能被删除。这不如前两个标志安全,因为如果文件可以编辑,就可以被清空。然而,它在某些情况下还是有用的。我曾在程序崩溃时,程序坚持删除自己的日志文件时使用过这个标志。然而,通常不建议在任何标准系统文件上设置这个标志。当系统运行在安全级别 1 或更高时,这个标志不能被移除。

uappnd

这个用户级的追加-only 标志只能由文件所有者或 root 设置。像系统级的追加-only 标志 sappnd 一样,设置了此标志的文件可以被追加,但不能以其他方式编辑或删除。这个标志最适用于个人程序等的日志;它主要是一种防止用户意外删除自己文件的手段。所有者或 root 可以移除此标志。

uchg

这个用户级的不可变标志只能由所有者或 root 设置。像 schg 标志一样,这个不可变标志可以防止任何人更改文件。再次强调,root 可以覆盖这个设置,并且用户可以在任何安全级别下禁用它。这个标志有助于防止错误,但并不是一种保护系统的方式。

uunlnk

这个用户级的不可删除标志只能由所有者或 root 设置。设置了此标志的文件无法被所有者删除。Root 可以覆盖这一点,用户也可以随时关闭此标志,这使得这个标志大多没什么用处。

设置和查看文件标志

使用 chflags(1) 设置标志。例如,为了确保没有人替换服务器的内核,你可以这样做:

# chflags schg /boot/kernel/kernel

你需要移除此标志才能执行系统更新。

你可以使用 -R 标志递归地更改整个目录树的标志。例如,要使 /bin 目录不可变,可以运行以下命令:

# chflags -R schg /bin

然后,砰!你的基本系统二进制文件无法更改。

要查看文件上设置了哪些标志,请使用 ls -lo

# ls -lo log
-rw-r--r--  1 mwlucas  mwlucas  sappnd 0 Nov 12 12:37 log

sappnd 条目告诉我们,该日志文件设置了系统的追加-only 标志。做个对比,如果一个文件没有设置标志,它看起来会是这样的:

# ls -lo log
-rw-r--r--  1 mwlucas  mwlucas  - 0 Nov 12 12:37 log

标志名的短横线告诉我们,该文件没有设置任何标志。

一台全新安装的 FreeBSD 系统,默认情况下没有很多文件被标记为带有标志,但你可以标记任何你想要的文件。在一台我完全预期会被黑客攻击的系统上,我通过在各种系统目录中使用 chflags -R schg 来阻止任何人用木马版本替换系统二进制文件。虽然这可能无法阻止攻击者进入,但想象他们的挫败感让我心情好多了。

要移除文件标志,请使用 chflags 并在标志名前加上 no。例如,要取消设置内核上的 schg 标志,输入以下命令:

# chflags noschg /boot/kernel/kernel

也就是说,你必须运行在 securelevel –1 时才能取消设置许多标志。那么,废话不多说,让我们讨论一下 securelevel 及其对你的意义。

Securelevels

Securelevel 是内核设置,它通过改变系统的基本行为来禁止某些操作。随着 securelevel 的提高,内核的行为会发生些许不同。例如,在低 securelevel 下,文件标志可以被移除。一个文件可能被标记为不可更改(immutable)——但你可以移除这个标志,编辑文件,然后重新设置该标志。当你提高 securelevel 时,文件标志无法再被移除。系统的其他部分也会发生类似的变化。总体而言,增加 securelevel 所带来的行为变化要么会让入侵者感到沮丧,要么会完全阻止他们。你可以在启动时通过 rc.conf 选项 kern_securelevel_enable="YES" 启用 securelevel。

Securelevel 通过对你的行为施加限制,增加了系统维护的复杂性。毕竟,许多系统管理任务也是入侵者可能用来掩盖痕迹的行为。例如,在某些 securelevel 下,你无法在系统运行时格式化或挂载新的硬盘。另一方面,securelevel 对入侵者的阻碍要远远大于对你的影响。

Securelevel 定义

Securelevel 有 5 个等级:–1、0、1、2 和 3,其中 –1 为最低级别,3 为最高级别。启用 securelevels 后,你可以通过 rc.conf 选项 kern_securelevel_enable 在启动时设置 securelevel。你可以随时提高 securelevel,而不仅仅是在启动时,但你不能在不重启并进入单用户模式的情况下降低 securelevel。毕竟,如果你可以随时降低 securelevel,入侵者也可以!

每个 securelevel 的效果根据你的 FreeBSD 版本而有所不同。要获取最新的信息,请阅读 security(7)。

Securelevel –1

默认设置下,内核并没有提供额外的安全保护。如果你正在学习 FreeBSD,并且频繁更改配置,保持在 securelevel –1,并使用内建的文件权限和其他 Unix 安全机制来保护系统。像 sappndschg 这样的标志会生效,但 chflags(1) 可以轻松移除这些标志。

Securelevel 0

Securelevel 0 仅在启动时使用,除了 securelevel –1 外没有特殊功能。然而,当系统进入多用户模式时,securelevel 会自动提高到 1。将 kern_securelevel=0 设置在 /etc/rc.conf 中,实际上与将 kern_securelevel=1 设置是一样的。如果你有启动脚本执行 securelevel 1 禁止的操作,securelevel 0 会非常有用。

Securelevel 1

在 securelevel 1,即基本安全模式下,事情变得有趣:

  • 系统级文件标志不能被关闭。

  • 你不能加载或卸载内核模块(参见 第六章)。

  • 程序不能通过 /dev/mem/dev/kmem 直接写入系统内存。

  • 无法访问 /dev/io

  • 你不能通过 debug.kdb.enter sysctl 进入内核调试器。

  • 你不能通过 debug.kdb.panic sysctl 让系统崩溃。

  • 挂载的磁盘无法直接写入。(你可以将文件写入磁盘;你只是不能直接访问原始磁盘设备。)

对于普通用户来说,securelevel 1 最明显的效果是无法修改 BSD 特定的文件系统标志。如果一个文件被标记为系统级不可变,并且你想替换它,那就太糟糕了。

Securelevel 2

Securelevel 2 具有 securelevel 1 的所有行为,并有两个附加功能:

  • 磁盘无法进行写入操作,无论是否已挂载。

  • 你不能将系统时间改动超过一秒。

这两项看起来对新手系统管理员无关紧要,但它们提供了重要的安全保护。尽管 Unix 提供了方便的工具,如文本编辑器来写文件,但也可以绕过这些工具和实际的文件系统,直接访问硬盘上的底层数据。直接操作硬盘使你可以更改任何文件,而不考虑文件权限。通常这种情况发生在你安装新硬盘并必须在其上创建文件系统时。通常,只有 root 用户可以以这种方式直接写入磁盘。在 securelevel 2 下,即使是 root 也无法使用 newfs(8)、zpool(8) 等命令。

类似地,另一个老黑客技巧是改变系统时间,编辑文件,再将时间改回。这样,当管理员查找可能导致问题的文件时,被篡改的文件看起来似乎已经几个月或几年没有更改,因此看起来不太可能是一个显而易见的关注点。

Securelevel 3

Securelevel 3 是 网络安全模式。除了 securelevel 1 和 2 的设置外,你无法调整数据包过滤规则。你主机上的防火墙是不可变的。如果你有一个启用了数据包过滤或带宽管理的系统,且这些规则已经经过良好的调优且不太可能改变,那么你可以使用 securelevel 3。

你需要哪种 Securelevel?

适合你环境的 securelevel 完全取决于你的具体情况。如果你刚刚将 FreeBSD 机器投入生产并且还在微调它,那么保持 securelevel 为 -1。系统调整完毕后,你可以提高 securelevel。大多数生产系统在 securelevel 2 下运行良好。

如果你使用的是 FreeBSD 的数据包过滤或防火墙软件,securelevel 3 可能看起来很诱人。不过,在启用这个选项之前,一定要非常确认你的防火墙规则!securelevel 3 会使你无法更改防火墙,而不影响连接。你百分百确信你的客户永远不会打电话来说:“这是支票,快给我更多服务器!”吗?

安全级别和文件标志无法实现什么?

考虑一种情况,假设某人侵入了你的网站服务器上的 CGI 脚本,利用它启动一个 shell,再通过 shell 获得 root 权限。

如果你已经相应地设置了 securelevel,也许攻击者会感到沮丧,因为她不仅无法将你的内核替换为她特别编译的版本,还无法加载内核模块。没问题——她仍然可以用木马版本替换各种系统程序,这样下次你登录时,你的新版本的 login(1) 就会将你的密码发送到一个匿名的基于网页的邮箱或互联网新闻组。

所以,为了保护关键文件,你四处执行 chflags schg -R /bin/*chflags schg -R /usr/lib 等命令。好吧。如果你忘记了某个文件——比如像 /etc/rc.bsdextended 这样不常见的文件——入侵者可以编辑该文件,加入 chflags -R noschg /。她可以在深夜时重启系统,可能你并没有注意到。你有多频繁坐下来彻底审计你的 /etc/rc 文件?

你认为系统是安全的,所有文件都得到完全保护。但如何看待 /usr/local/etc/rc.d,即本地程序启动目录呢?系统启动过程会尝试执行该目录下包含 #PROVIDE: 行的任何可执行文件(关于这一点,详见第十七章)。因此,入侵者可以通过将一个简单的 shell 脚本放置在此目录中,造成很大的损害。毕竟,/etc/rc 在启动过程的最后会提升 securelevel。假设她创建一个 shell 脚本,在 securelevel 提升之前杀死正在运行的 /etc/rc,然后又运行自己放在 /var/.hidden/rc.rootkit 中的脚本继续启动系统,情况会如何?

当然,这些只是几种可能性。还有其他的,唯一的限制是入侵者的创造力。记住,系统安全是一个棘手的问题,没有简单的解决方案。一旦入侵者获得了命令提示符,剩下的就是你和他们的较量。如果他们足够厉害,你甚至在入侵发生时都不会察觉。通过遵循良好的计算机实践并保持系统更新,你可以防止他们第一次入侵就成功。不要让 securelevel 使你变得懒惰!

与安全级别共存

如果你对 schg 标志使用得很宽松,你很快就会发现,自己无法方便地升级或修补系统。事实上,使入侵者生活困难的条件,如果你不知道如何与之合作,也可能使你的生活变得一团糟。

如果你已使用 schg 锁定了 /etc/rc.conf,你必须降低安全级别才能更改系统上运行的程序。当然,安全级别设置就在该文件中,因此为了编辑它,你必须在 /etc/rc 运行之前控制系统。这意味着你必须启动单用户模式(如 第四章 中讨论的那样),挂载你的文件系统,对相关文件运行 chflags noschg,然后继续启动。你甚至可以完全禁用 /etc/rc.conf 中的安全级别设置,在系统运行时正常工作,或者在 /etc/rc.local 中添加命令,使其在设置安全级别之前生效。这样你可以更快地恢复服务,但会失去文件标志的保护。

完成维护后,你可以通过将 kern.securelevel sysctl 设置为你期望的安全级别来提高(但不能降低)安全级别。

# sysctl kern.securelevel=3

现在你可以控制文件更改了,让我们考虑控制网络访问到你的系统。

网络目标

入侵者通常是通过破坏监听网络的应用程序而非操作系统本身来入侵的。一个操作系统可能会或可能不会帮助防御软件免受网络攻击,但入侵本身通常是从应用程序开始的。减少可以对你的服务器发起的攻击数量的一种方法是,识别所有监听网络的程序,并禁用任何不严格必要的程序。FreeBSD 提供了 sockstat(1) 工具,作为一种简单的方法来识别监听网络的程序。

我们在 第八章 中详细介绍了 sockstat;运行 sockstat -4 可以显示所有打开的 IPv4 TCP/IP 端口。每个你打开的网络端口都是一个潜在的弱点和潜在的攻击目标。关闭不必要的网络服务并保护那些必须提供的服务。

定期审查你系统上开放的端口是一个好主意,因为你可能会发现一些令人惊讶的东西。你可能会发现某个你安装的软件有一个你没有意识到的网络组件,而且它一直在安静地监听网络。

一旦你知道了正在运行的程序,如何关闭不需要的程序呢?关闭这些端口的最佳方法是不启动运行它们的程序。网络守护进程通常会在两个地方启动:/etc/rc.conf/etc/rc.d 下的启动脚本。与主 FreeBSD 系统集成的程序,如 sendmail(8)、sshd(8) 和 rpcbind(8),在 rc.conf 中有标志位用来启用或禁用它们,许多附加程序也如此。有关在启动时启用和禁用程序的详细信息,请参见 第四章。

工作站与服务器安全

我见过的许多公司都有严格安全的服务器,但很少关注工作站的安全。然而,潜在的入侵者并不关心一个系统是服务器还是工作站。许多服务器和防火墙都有针对系统管理员工作站的特殊规则。入侵者会乐意渗透一个工作站,并试图利用这一点访问服务器。虽然服务器安全至关重要,但不要忽视工作站——尤其是的工作站!

网络探测非常奇怪,因为你真的不知道什么时候有人在试探你的主机。为了了解这种情况有多严重,可以在你的一个公共服务器上的 /etc/rc.conf 文件中将 log_in_vain 设置为 1。这会告诉内核记录所有连接尝试到已关闭的端口。当有人检查你的主机是否存在不存在的 Telnet、Squid 或数据库监听器时,内核会将该尝试记录到 /var/log/messages 文件中。仅需观看日志,足够让你深刻意识到,整个互联网真的想要攻破你——然后关闭 log_in_vain

汇总

一旦你只开放了必要的网络端口,并且知道哪些程序正在使用这些端口,你就知道哪些程序是你最需要关注安全性的。假如 FreeBSD 安全团队发布了一个关于你没有运行的服务的问题公告,你可以安全地推迟修复,直到下一个维护窗口。如果安全团队发布了你正在使用的程序存在漏洞的公告,你就知道必须尽快实施修复。如果他们宣布你正在使用的网络软件存在严重的安全问题,你就知道必须迅速采取行动。能够智能、快速地应对实际风险有助于保护你免受大多数入侵者的攻击。像文件标志和安全级别这样的工具可以最大程度地减少成功入侵者造成的损害。最后,使用组来限制你的系统管理员访问特定的系统部分,可以保护你的计算机免受意外和故意的损害。

现在让我们换个话题,谈谈存储。

第十章:磁盘、分区和几何**

image

系统管理员不能过分强调管理磁盘和文件系统的重要性。(去吧,试着过分强调。我会等着的。)你的磁盘存储着你的数据,这使得操作系统对其可靠性和灵活性要求极高。FreeBSD 支持多种文件系统,并有许多不同的方法来处理它们。在本章中,我们将讨论每个系统管理员都会执行的最常见的磁盘任务。

首先,让我们讨论存储设备最重要的一点。

磁盘会撒谎

曾几何时,系统管理员可以根据磁盘提供的信息做出决策。你可以插入硬盘,查询其盘片数、气缸数、扇区数等更多信息。那时候已经是很久很久以前的事了。是的,你现在可以执行相同的查询并得到答案,但这些答案并不反映任何现实。今天,磁盘就像是一个魔法盒子,根据需求吐出数据。这个魔法盒子有的包含旋转的盘片,另一些则没有活动部件。这个魔法盒子提供带编号的扇区来存储比特和字节。那些数字和盒子内容之间的关系?那就是魔法:不可理解且无法知晓。

在之前的书籍中,包括本书的早期版本,我讨论过数据在磁盘上正确放置的重要性,但这些知识如今已经完全过时。如果你仍然保留这些知识,请抛弃它,转而学习更有用的东西,比如所有出演经典神秘博士角色的演员的完整传记。

就磁盘设计而言,你需要知道的唯一事情就是逻辑块寻址(LBA)。磁盘上的每个扇区都会分配一个编号。文件系统通过编号来调用磁盘扇区。就是这样。LBA 以下的任何内容,都只是你个人的猜测而已。

不幸的是,磁盘现在有了一类新的谎言:扇区大小。

到了 1990 年代,磁盘扇区大小从 128 字节到 2KB 不等。即使是最早的 IBM PC 也能理解软盘上不同的扇区大小。

然而,在 2000 年代初期,制造商决定使用 512 字节的扇区。今天的硬盘容量大得多,文件也同样更大。在过去的几年里,512 字节的扇区大多被 4,096 字节的扇区取代,这种硬盘被称为4K 硬盘。这种扇区大小对于我们今天存储的数据类型来说更为合理。

问题是,像 Windows XP 这样的操作系统知道磁盘扇区一直是,而且永远会是,512 字节。这些操作系统无法容忍报告拥有 4KB 扇区的硬盘,因为大家都知道根本没有这种东西。如果你制造了 4K 硬盘,你该怎么办?

就是你一直在做的事。

你教硬盘撒谎。

最重要的是,不同的 4K 驱动方式不同。如果操作系统询问驱动其扇区大小,大多数驱动会声明它们有 512 字节的扇区。那些宣称有 512 字节和 4KB 扇区的驱动可能是 4K 驱动,正在努力说出真相。很少有驱动承认只使用 4KB 扇区。更复杂的是,一些固态硬盘的扇区甚至达到 8KB 或 16KB,或者它们支持多种扇区大小。

FreeBSD 的两个主要文件系统都必须知道底层磁盘的扇区大小以及该扇区的逻辑块地址。如果在磁盘上使用了错误的扇区大小,性能会受到影响。我可以详细讨论这种情况发生的原因,但为了简化,始终将分区对齐到偶数 MB 边界。这可能会浪费一些字节,但与磁盘和文件系统不对齐所带来的严重性能问题相比,这点浪费是微不足道的。

设备节点

我们在第四章中简要讨论过设备节点,但在这里我们将更详细地探讨它们。设备节点是表示硬件设备或操作系统功能的特殊文件。它们作为逻辑接口提供功能给用户程序。通过对设备节点使用命令、发送信息或读取数据,你实际上是在告诉内核执行某个动作。如果设备节点代表物理设备,那你正在对该设备进行操作。这些操作在不同设备间可能大不相同——写数据到磁盘与写数据到声卡是完全不同的操作。虽然你可以在任何地方暴露设备节点,但标准的设备节点存在于/dev目录中。

在你可以使用磁盘或磁盘分区之前,你必须知道它的设备名称。FreeBSD 的磁盘设备节点来自该类型硬件的设备驱动程序名称。设备驱动程序名称反过来通常来源于设备的类型,而不是设备的角色或功能。

表 10-1 展示了最常见的磁盘设备节点。

表 10-1: 存储设备节点和类型

设备节点 手册页 描述
/dev/ada* ada(4) ATA 风格直接访问磁盘(SATA, IDE 等)
/dev/cd* cd(4) 光学媒体驱动(CD,Blu-Ray 等)
/dev/da* da(4) SCSI 风格直接访问磁盘(USB 存储、SAS 等)
/dev/md* md(4) 内存磁盘
/dev/mmcsd* mmcsd(4) MMC 和 SD 存储卡
/dev/nvd* nvd(4) NVM express 驱动
/dev/vtbd* virtio_blk(4) 基于 Virtio 的虚拟机磁盘
/dev/xbd* xen(4) Xen 虚拟磁盘

许多 RAID 控制器将它们的 RAID 容器呈现为 SCSI 设备,因此它们显示为 /dev/da 设备节点。其他一些则将它们的磁盘呈现为“带有特殊厂商标识的 SCSI”,因此它们会有特别的设备节点名称,如 /dev/raid(ATA RAID)、/dev/mfid(某些 LSI MegaRAID 卡)等。查看你 RAID 控制器的 man 页面,了解它所呈现的设备节点。

通用访问方法

通用访问方法(CAM) 是一种标准化的设备驱动架构,最初是为了支持 20 世纪的 SCSI-2 磁盘的复杂命令集而编写的。其理念是,基于这种架构的标准化将简化设备驱动程序的编写。然而,实际上只有 FreeBSD 和 DEC OSF/1 配备了 CAM,并且每个操作系统都以不同的方式填补了规范中的空白。

FreeBSD 9 及之后的版本将支持 CAM 的所有物理磁盘的管理整合到了 CAM 接口中。使用 camcontrol(8) 从磁盘收集信息并向其发出命令。camcontrol(8) 命令有多种子命令,可以向硬盘发出指令。

你有什么磁盘?

要识别主机的存储设备,你可以扫描 /var/run/dmesg.boot 查找磁盘设备节点,或者查看挂载的文件系统并从中倒推。但识别存储设备的最简单方法是让 camcontrol(8) 查询 CAM 系统,看看它识别出了哪些磁盘。让我们看一下我的一台测试系统:

# camcontrol devlist
<ATA WDC WD1003FBYZ-0 1V03>        at scbus0 target 0 lun 0 (pass0,da0)
<ATA WDC WD1003FBYZ-0 1V03>        at scbus0 target 1 lun 0 (pass1,da1)
<ATA WDC WD1003FBYZ-0 1V03>        at scbus0 target 2 lun 0 (pass2,da2)
<ATA WDC WD1003FBYZ-0 1V03>        at scbus0 target 3 lun 0 (pass3,da3)

该输出分为三个字段。第一个字段给出了设备的名称,由设备本身报告。通常是厂商和厂商的型号。

第二部分给出了 SCSI 连接信息。这些驱动器实际上并不是 SCSI 驱动器——它们是通过 CAM 管理的 SATA 连接。但现在你知道哪些磁盘设备插入了 SATA 控制器的哪个端口。

最后,在括号中,我们有 SCSI 设备和我们可能需要的存储设备节点。这个主机有四个磁盘,分别命名为 da0、da1、da2 和 da3。

非 CAM 设备

一般来说,除了专有的 RAID 控制器和虚拟磁盘外,其他都支持 CAM。

RAID 控制器通常已经拥抱并扩展了 CAM 协议,出于制造商当时认为合理的原因。一个在 1990 年代初期编写的协议对于 2010 年的 RAID 控制器来说已经不够用了。这些控制器通常有自己的控制程序。RAID 容器会出现在 devlist 和其他 camcontrol(8) 子命令中。

同样,虚拟磁盘不会响应 CAM 命令。那里没有磁盘可供操作——你只是在文件中写入数据块。你可以通过 camcontrol devlist 查看磁盘,但就仅此而已。

对于大多数应用,我推荐使用 FreeBSD 的 RAIDZ 或 GEOM RAID,而不是硬件 RAID 控制器。

GEOM 存储架构

FreeBSD 拥有一个极其灵活的存储基础设施系统,称为 GEOM(即“磁盘几何”)。GEOM 位于设备驱动程序节点与底层硬件之间,处理它们之间交换的数据。从这个位置,GEOM 可以任意转换输入/输出请求。

设备控制程序

一些存储设备具有特殊功能,这些功能在通用 CAM 框架中并未涵盖。RAID 控制器通常具有厂商特定的功能,FreeBSD 包含许多小程序来单独管理这些控制器。你会发现像 mfiutil(8) 和 mptutil(8) 用于较旧的 LSI 控制器,mpsutil(8) 用于较新的 LSI 控制器,等等。如果你有非易失性存储器高速缓存(NVMe)驱动器,可以查看 nvmecontrol(8)。

GEOM 是由内核模块构建的,称为 GEOM 类,它们让你执行特定类型的转换或管理。磁盘有一个 GEOM 类,允许内核将数据写入磁盘。但如果你想加密你的磁盘,那也是一个 GEOM 类。基于软件的 RAID?一个 GEOM 类。FreeBSD 将所有存储修改实现为 GEOM 类。

GEOM 类是 可堆叠 的。它们使用一个类的输出作为另一个类的输入。如果你想加密硬盘,然后将其镜像到另一个硬盘?当然可以!在硬盘上堆叠一个加密模块,然后再堆叠一个驱动器镜像模块。你想将那个驱动器跨网络镜像?把那个 GEOM 类加到堆栈中。这种灵活的模块化使得 GEOM 成为 FreeBSD 最强大的特性之一。

GEOM 自动配置

当 FreeBSD 发现新存储设备时,无论是在启动时还是插入新硬盘时,GEOM 子系统会检查该设备是否包含已知的格式,比如主引导记录、BSD 磁盘标签或其他元数据。GEOM 还会检查物理标识符,如磁盘的序列号。这被称为 品尝

当 GEOM 发现标识信息时,它会根据该元数据的指示配置设备。如果磁盘的元数据说,“我是名为 garbage 的镜像的一部分,和另外两块磁盘一起,”GEOM 会寻找其他磁盘并组装镜像。如果 GEOM 能通过格式、标签或其他信息识别存储设备,它会启动该设备,启动一个 GEOM 类实例,创建适当的设备节点,并执行任何它理解的其他配置。

如果 GEOM 无法识别磁盘上的其他元数据,例如在未格式化且未分区的磁盘上,GEOM 会为该存储设备创建设备节点并保持原样。

GEOM 类的实例称为 geom。gmirror(8) 类让磁盘彼此镜像,但名为 garbage 的特定镜像磁盘对就是一个 geom。该镜像中的每个磁盘也是一个 geom。

GEOM 与卷管理器

传统的卷管理器期望你按它们的方式做事,无论这对你的环境和硬件是否合理。如果卷管理器说你通过加密单个驱动器并在其上进行镜像来创建加密的磁盘镜像,那么你就必须这么做。或许在你的环境中,先镜像驱动器然后加密它们更合适,但如果这不是卷管理器的做法,那也没办法。更糟糕的是,一些卷管理器做出糟糕的选择,然后通过旁路方式修复这些选择,以最小化这些决策的后果。

GEOM 与卷管理器的不同之处在于,它假设你知道自己在做什么。它给你灵活性,让你以最适合硬件并能为你的用例带来好处的方式安排存储。GEOM 类让你可以轻松地将新的数据转换功能插入到存储中。而你不能在商业卷管理器中加入加密层之类的功能。

卷管理器涵盖了它们设计时硬件中最常见的情况。然而,随着时间的推移,这些常见的情况变得越来越不常见。人们继续使用卷管理器,即使它们最初为已经过时的硬件设计。GEOM 让你随着硬件、环境和应用程序的变化,演化你的设计。

FreeBSD 包含了两个看起来很像卷管理器的软件套件:gvinum(8) 和 ZFS。Vinum 是 1990 年代的 FreeBSD 卷管理器,尽管 gvinum(8) 重新实现了它作为 GEOM 类,但强烈不建议使用它。ZFS 非常强大,正如我们在第五章中看到的,但它确实有卷管理器的“按我们的方式做事”的理念。

虽然理论上你可以永远堆叠 GEOM 模块,但你必须考虑硬件资源。通过网络镜像一个繁忙的磁盘可能需要专用的网络接口和空闲的交叉连接电缆。加密和解密数据会消耗处理器时间和内存。GEOM 不会阻止你对磁盘进行过度操作;它只是为你提供了进行这些操作的新机会和有趣的可能性。

提供者、消费者和分割器

各个 geom 要么是消费者,要么是提供者,或者两者兼具。

一个 提供者 为另一个 geom 提供服务。如果你正在镜像两个硬盘,硬盘的 geom 提供磁盘给镜像使用。一个提供者通常具有一个设备节点,比如 /dev/ada1p1

一个 消费者 使用提供者的服务。磁盘镜像 geom 消耗底层磁盘驱动器。一个 geom 的消费者部分不需要设备节点。

一个 geom 可以既是提供者又是消费者——实际上,堆栈中间的每个 geom 必须既是提供者又是消费者。磁盘镜像 geom 消耗底层的物理存储介质,但它为文件系统提供了一个镜像磁盘。

FreeBSD 对待所有提供者和消费者的方式是相同的。一个物理硬盘只是另一个提供者,就像镜像、加密层或网络导入一样。这一特点使得你可以任意堆叠 GEOM 类。

一个将一个类细分为多个子类的 GEOM 类称为 切片器,通常负责管理分区。处理主引导记录(MBR)分区的 GEOM 类是一个切片器,GUID 分区表(GPT)类也是如此。我们在第二章中讨论了这两种分区方法,在本章中我们将进一步探讨这两种方法。切片器必须确保磁盘分区不重叠,并且分区符合分区方案的规则。

GEOM 控制程序

许多 GEOM 类都有一个控制程序,让你管理模块或查询设备。一些广泛使用的类使用 geom(8),而其他类使用如 gmirror(8) 或 geli(8) 之类的程序。disk GEOM 类与物理存储介质进行通信,并为上层提供消费者。这是一个非常常用的类。在这里,我查询主机以查看它具有的 disk 类型的 geoms,并打印出磁盘提供给操作系统的信息。

   $ geom disk list
➊ Geom name: da0
   Providers:
   1\. Name: da0
    ➋ Mediasize: 1000204886016 (932G)
    ➌ Sectorsize: 512
    ➍ Mode: r2w2e3
    ➎ descr: ATA WDC WD1003FBYZ-0
    ➏ lunname: ATA   WDC WD1003FBYZ-010FB0            WD-WCAW36478143
    ➐ lunid: 50014ee25e60dab5
    ➑ ident: WD-WCAW36478143
    ➒ rotationrate: 7200
    ➓ fwsectors: 63
       fwheads: 255

该硬盘提供一个名为 da0 ➊ 的磁盘设备。mediasize 字段提供其大小(以字节为单位),并将其转换为更便捷的 932GB ➋。

该磁盘声称其 扇区大小 为 512 字节 ➌。许多磁盘会虚报其扇区大小。请查看硬盘制造商的文档,以确定实际的扇区大小。驱动器可能会提供 Stripesize 值 4,096,表示它们实际上是 4K 驱动器。

一个 GEOM 类的 mode 看起来非常像文件权限 ➍,但它实际上是读取(r2)和写入(w2)设备的 GEOM 类的数量,以及请求独占访问设备的设备的数量(e3)。

descr 字段 ➎ 提供驱动器的型号。

lunname 字段 ➏ 提供型号和序列号。是的,它是 descrident 字段的组合。硬盘确实很希望你相信这就是它的名称和标识符。

lunid ➐ 提供逻辑单元号(LUN)标识符,描述此驱动器如何连接到此主机。

磁盘的 ident ➑ 是驱动器的序列号。

rotationrate ➒ 告诉我们该驱动器的旋转速度。它是一个 7,200 RPM 磁盘。非旋转磁盘,如 SSD,其 rotationrate 为 0。

fwsectorsfwheads 字段 ➓ 提供磁盘几何信息。这些是本章开头提到的谎言的例子。即使是 SSD 也会提供这些值。

一些驱动器提供较少的信息。虚拟磁盘几乎不提供任何信息,而且它们提供的任何信息都不可信。(尽管虚拟机系统可能会说该磁盘提供 32,212,254,720 个 512 字节扇区,但谁知道虚拟磁盘下方的实际磁盘是什么样子呢?)

GEOM 设备节点和堆栈

许多系统管理员工具期望在磁盘或磁盘分区上运行。类 Unix 系统提供磁盘和分区作为设备节点。GEOM 提供设备节点,以确保这些工具保持兼容性。

大多数活跃的 GEOM 模块在/dev/下都有自己的目录。该目录中的设备节点表示该模块当前的提供者。该目录通常(但不总是)以使用该模块的 GEOM 模块命名。例如,gmirror(8)类使用/dev/mirror

目录名称可能会更改,以避免歧义或重叠。glabel(GEOM 标签)类使用/dev/label/dev/gpt目录包含存储在 GPT 分区上的标签,而/dev/gptid目录包含与 GPT 分区相关的数字标识符。

一些类不会创建目录,而是附加到现有设备上。gnop(8)类会在附加的节点旁边创建一个新的节点,但会在设备名称末尾附加.nop

硬盘、分区和方案

虽然我们在第九章中讨论了分区,但请从磁盘驱动器的角度考虑分区。我们第一个 SATA 控制器上的第一个可能的 SATA 磁盘叫做/dev/ada0。后续的磁盘分别是/dev/ada1/dev/ada2,依此类推。如果你还拥有 SAS 磁盘,它们的编号将从 0 开始。

磁盘被进一步划分为分区。即使是运行 Microsoft 操作系统的普通消费级系统,其硬盘上也通常会有多个分区。系统管理员将巨大的磁盘阵列划分为更小、更易于管理的单元,具有特定用途——或者他们也可能采取相反的做法,将多个磁盘合并为一个庞大的分区。

分区方案是用于组织磁盘分区的系统。传统的主引导记录(MBR)是一种分区方案。旧的苹果和 SPARC 硬件有它们自己的分区方案。今天,大多数硬件和操作系统使用的方案是GUID 分区表(GPT)。每种方案都有其对引导块、硬件架构和分区的要求。本书讨论了 MBR 和 GPT 方案,但你应该意识到其他方案也存在。

每个磁盘分区都会获得自己的设备节点,这些设备节点是通过在底层设备节点名称末尾添加内容来创建的。在这里,我查看了一个默认的 FreeBSD 安装,使用 UFS 格式在虚拟磁盘上进行的设备节点:

# ls /dev/vtbd0*
/dev/vtbd0      /dev/vtbd0p1    /dev/vtbd0p2    /dev/vtbd0p3

我们有一个磁盘本身的设备节点,然后还有三个其他设备节点,分别以 p1、p2 和 p3 结尾。那些子分区是什么?p表示它们是 GPT 分区。在默认安装中,p1 是引导分区,p2 是交换空间,p3 是主文件系统。

每个分区方案都有自己独特的设备节点扩展。我们将在本章后面阅读到这些内容。

文件系统表:/etc/fstab

FreeBSD 像大多数类 Unix 操作系统一样,使用文件系统表/etc/fstab将磁盘上的分区映射到文件系统和交换空间。虽然 ZFS 不使用/etc/fstab,但其他所有 FreeBSD 文件系统都可以出现在其中。每个使用中的分区都会出现在单独的一行上,并附有挂载和管理指令。

/dev/gpt/rootfs  /       ufs     rw  2  1
/dev/gpt/swapfs  none    swap    sw  0  0
proc             /proc   procfs  rw  0  0

第一个字段给出了 GEOM 提供者的名称。这可能是一个物理磁盘分区,例如 /dev/ada0p1,或者可能是一个 GEOM 设备节点的分区。这里的前两行提供了 /dev/gpt 下的设备节点。这是 GPT 标签,我们将在本章稍后看到。我们的第三项列出了 proc,而不是设备节点:它是 procfs(5) 虚拟文件系统,我们将在 第十三章 中详细讨论。

第二个字段给出了文件系统可用的目录,称为 挂载点。每个可以读写文件的分区都会附加到一个挂载点,例如 /usr/var 等等。一些特殊的分区,例如 swap 空间(这里是第二行),其挂载点为 none。因为它们没有连接到目录树,且系统在交换时会覆盖这些文件,所以你不能在 swap 空间上读写文件。

接下来,我们来看文件系统类型。第一行显示的是 ufs 类型,或者说是 Unix 文件系统。第二行定义为 swap 空间,而第三行是 procfs 类型。其他类型包括 cd9660(CD 光盘或映像)、nfs(网络文件系统挂载)和 ext4fs(Linux 文件系统)。文件系统表告诉 FreeBSD 如何挂载这个分区。第十三章讨论了其他文件系统。

第四个字段显示了用于此特定分区的 mount(8) 选项。每个文件系统都有其自己的挂载选项,但这里列出了一些多个文件系统都使用且经常出现在 /etc/fstab 中的选项:

ro 文件系统以只读方式挂载。连 root 用户也不能对其进行写操作。

rw 文件系统以读写方式挂载。

noauto FreeBSD 不会自动挂载文件系统,无论是在启动时还是在使用 mount -a 时。这个选项对于可能在启动时没有介质的可移动媒体驱动器非常有用。

第五个字段用于告诉 dump(8) 需要哪个备份级别来备份这个文件系统。现在,dump 已经基本过时;人们使用 tar(1) 进行文件级备份,或者使用更先进的备份软件,如 Bacula (www.bacula.org/) 或 Tarsnap (www.tarsnap.com/)。

最后一个字段告诉 FreeBSD 启动过程在何时检查文件系统的完整性。所有具有相同编号的分区会与 fsck(8) 一起并行检查。根文件系统标记为 1,意味着它是第一个被检查的。只有根文件系统应该标记为 1,任何其他分区应该标记为 2 或更高,意味着它们稍后会被检查。交换分区、只读介质和逻辑文件系统不需要完整性检查,因此它们会设置为 0。

FreeBSD 在启动时配置所有在 /etc/fstab 中找到的文件系统。然而,随着系统运行,系统管理员可以挂载其他文件系统,并且她也可以卸载那些列在其中的文件系统。这就引出了我们的下一个问题……

现在挂载的是什么?

如果不是所有的文件系统在启动时都被自动挂载,并且如果系统管理员可以添加和移除挂载的文件系统,如何确定当前已挂载的文件系统呢?使用 mount(8)命令,不带任何选项,可以查看所有挂载的文件系统。

# mount
/dev/gpt/rootfs on / (ufs, local, journaled soft-updates)
devfs on /dev (devfs, local, multilabel)

这是一个小型的基于 UFS 的主机。它只有一个磁盘分区和一个 devfs(5)实例(参见第十三章)。local一词意味着该分区位于连接到此计算机的硬盘上。日志化软更新选项是 UFS 的一个特性,我们将在第十一章中讨论。如果你正在使用 NFS 或 SMB 来挂载分区,它们会出现在这里。

更复杂的主机会产生更大的结果:

# mount
base/ROOT/default on / (zfs, local, noatime, nfsv4acls)
base/tmp on /tmp (zfs, local, noatime, nosuid, nfsv4acls)
base/usr/home on /usr/home (zfs, local, noatime, nfsv4acls)
base/usr/ports on /usr/ports (zfs, local, noatime, nosuid, nfsv4acls)
procfs on /proc (procfs, local)
devfs on /dev (devfs, local, multilabel)
--snip--

该主机使用多个 ZFS 数据集,每个数据集都有自己的挂载点。mount(8)命令的输出显示了选定的 ZFS 选项,例如 noatime 和 nfsv4acls。

在输出的最后,我们有一个 procfs(5)条目和一个 devfs(5)挂载条目。一个正常工作的 FreeBSD 系统需要在/dev挂载 devfs,否则它的运行将不太顺利,或者根本无法长时间工作。

磁盘标签

在最低层次上,操作系统通过硬件与系统的物理连接来识别磁盘。传统上,文件系统表可能会写道:“将连接在 ATA 端口 3 的磁盘用作/var/log文件系统。”在硬件灵活性较差时,这种方法工作得很好,但随着硬件技术的发展,这种连接变得更加灵活。如果你根据物理连接来分配磁盘角色,有时这种连接会发生变化。我曾经有过多次主板在不合时宜的时刻爆炸,迫使我进行紧急替换。在这种情况下,追踪哪个电缆连接到哪个接口通常都会失败。在旧版本的 FreeBSD 中,你需要“固定”设备,使得特定的磁盘总是作为特定的设备节点出现。现在不再需要这样做。

今天,系统管理员使用磁盘上的标签来通过其他方式引用磁盘,而不是依赖于物理连接。标签标识了一个 geom 实例。与其告诉 FreeBSD/var/www在连接到 SATA 端口 2 的磁盘上,不如声明/var/www在标签为website的磁盘上。前者容易出错,而后者则大多能避免因为硬件技术人员的疏忽。一个磁盘可以同时有多个标签,只要它们是不同类型的标签。FreeBSD 会自动根据磁盘的固有特征推导出许多标签;系统管理员也可以定义其他标签。

大多数标签类型都有一个专门的设备节点目录。每个 GPT 分区都有一个全局唯一标识符(GUID),这些分区的自动创建标签存储在/dev/gptid中。磁盘根据其序列号获得唯一的磁盘 ID,这些 ID 会出现在/dev/diskid中。手动创建的 GPT 标签出现在/dev/gpt中。

像使用其他设备名称一样使用这些标签。如果你将磁盘 ada5 标记为stuff1,你可以将磁盘 stuff 分区为stuff1p1stuff1p2,并在配置文件中使用这些分区,等等。

不是所有标签都来自 GEOM。ZFS 使用自己内部的标签方法来标识文件系统和池。你还可以向 UFS 文件系统添加标签。

不要让交换的 SATA 电缆毁了你的周末。给所有东西加上标签。

查看标签

使用 glabel(8),即geom label的快捷方式来查看标签。以下是来自一个小型虚拟机的标签部分。实际硬件上的标签可能会变得非常复杂。

   $ glabel list
➊ Geom name: ada0p1
   Providers:
➋ 1\. Name: gptid/b9c0c7c5-5b66-11e7-8aec-080027739ff6
      Mediasize: 524288 (512K)
      Sectorsize: 512
   --snip--
➌ Consumers:
   1\. Name: ada0p1
      Mediasize: 524288 (512K)
      Sectorsize: 512
      Stripesize: 0
      Stripeoffset: 20480
      Mode: r0w0e0

这个主机在磁盘分区/dev/ada0p1上有一个单一的 GEOM ➊。它提供了一个基于 GPT 分区 ID ➋的非常长的标签。我们将在底层磁盘上看到一些信息,例如磁盘上的扇区数量、扇区大小以及你可能在geom disk list输出中看到的其他信息。然而,这些信息来自于分区。物理驱动器信息是从底层磁盘传递上来的。^(1)

这个驱动器有一个单一的消费者 ➌,即实际的底层分区。我们处于这个简单 GEOM 堆栈的最底层,紧贴磁盘,因此它正在自我消费。如果你添加了加密层或软件 RAID,你将看到该 GEOM 消费的其他设备。

示例标签

以下是你在大多数 FreeBSD 系统上看到的标签类型的示例。

磁盘 ID 标签

物理机器提供虚拟机无法获得的标签。

Geom name: ada3
Providers:
1\. Name: diskid/DISK-WD-WCAW36477141
--snip--

驱动器 ada3 提供了一个名为diskid/DISK-WD-WCAW36477141的 GEOM。diskid GEOM 以硬盘的序列号命名,基于硬盘提供的信息。你可以将磁盘从这台机器上取下,并将其连接到完全不同的 FreeBSD 主机上,新的主机将生成完全相同的磁盘 ID 标签。在配置中使用 diskid 标签可以确保 FreeBSD 使用你打算使用的确切磁盘。以下是在/etc/fstab中列出该磁盘分区 3 的方式:

/dev/diskid/DISK-WD-WCAW36477141p3 /usr/local ufs rw 2 2

这个磁盘可以作为/dev/ada3/dev/ada300连接到主机,FreeBSD 仍然会将该分区挂载为/usr/local

磁盘 ID 标签的问题在于它们难以阅读,也更难输入。我之所以提到它们,是因为它们可能是默认出现的,但我鼓励你选择其他标签。通过在/boot/loader.conf中将可调项kern.geom.label.disk_ident.enable设置为 0,来从你的主机中移除这些标签。

GPT GUID 标签

每个 GPT 分区都包括一个 GUID。FreeBSD 可以将 GUID 视为标签。在这里,我们看到了附加为 ada0 的磁盘上分区 1 的 GPT ID 标签:

   Geom name: ada0p1
   Providers:
➊ 1\. Name: gptid/075e7b89-30ed-11e7-a386-002590dbd594
   --snip--

这个磁盘分区可以方便地表示为/dev/gptid/075e7b89-30ed-11e7-a386-002590dbd594 ➊。类似于磁盘序列号,GUID 是分区的一部分。你可以将磁盘移动到另一台主机,仍然能够获得相同的 GPT ID。通过在像/etc/fstab这样的配置文件中使用 GPT ID 标签,可以确保 FreeBSD 在系统启动时使用这个特定的分区,而不是分区 1,无论哪个设备被分配到 ada0。

使用 GPT ID 标签在你有多个自动配置磁盘时很有意义,例如大型存储阵列。然而,在较小的系统中,128 位的 GUID 过长,令人烦恼。如果你决定不使用这些标签,可以通过将可调参数 kern.geom.label.gptid.enable 设置为 0,在 /boot/loader.conf 中将它们从系统中移除。

对于大多数主机,我建议分配 GPT 标签。

GPT 标签

GPT 分区允许你在分区表内手动分配标签名称。我强烈建议在可能的情况下这么做。这里是我分配了名称的一个分区:

   Geom name: ada2p1
   Providers:
➊ 1\. Name: gpt/swap2
   --snip--

我已经为磁盘 ada2 上的分区 1 分配了标签 swap2 ➊。这个标签物理存储在磁盘分区上。我可以像使用其他设备名称一样在配置中使用这个标签。对于小型系统来说,使用手动分配的标签更加可管理,如这个 /etc/fstab 所示:

/dev/gpt/swap2  none  swap  sw  0  0

一个分配的标签比长串的序列号或 GUID 更加人性化。如果可以选择,我建议你为 GPT 分区分配标签。我们在分区时会分配标签。

GEOM 标签

除了在系统上显示标准标签外,glabel(8) 命令还允许你配置 GEOM 标签。GEOM 标签是特定于 FreeBSD GEOM 基础架构的,并出现在 /dev/label 中。使用 glabel label 命令来管理 GEOM 标签。这里,我将 GEOM 标签 root 应用到 GPT 分区 da0p1 上:

# glabel label da0p1 root

还有一个 glabel create 命令,但这些标签在系统重启时会消失。

GEOM 枯萎

一个提供者可以有多个标签。一个分区可能有基于底层存储设备的磁盘 ID(/dev/diskid/somethinglong)、GPT ID(/dev/gptid/somethingevenlonger)、手动分配的标签(/dev/gpt/swap0)以及基于底层设备附件点的设备节点(/dev/ada0p1)。虽然多个进程可以同时查看磁盘设备,但许多磁盘操作——例如挂载分区——需要独占的设备控制。

为了防止通过多个名称访问 geoms,当你通过一个标签访问设备时,内核会移除未使用的标签。这被称为 枯萎。例如,如果我通过 GPT 标签 /dev/gpt/swap0 挂载一个交换分区,那么该分区的所有其他标签都会从 /dev 中消失。任何尝试访问相应 /dev/gptid 分区的人会发现该设备节点丢失。

一旦设备上的所有独占锁定被移除,内核会恢复其他设备标签的显示。如果我停用了交换空间,GPT ID 和原始设备名称会重新出现。

gpart(8) 命令

像许多操作系统一样,FreeBSD 曾经为每种分区方案提供专门的分区工具。今天,所有的磁盘分区功能,无论是 MBR 还是 GPT,都包括在 gpart(8) 程序中。对于嵌入式设备,可能偶尔需要像 fdisk(8) 和 bsdlabel(8) 这样的旧工具,但 gpart(8) 对服务器和桌面系统完全适用。

这个常用工具意味着无论你使用的是哪种分区方案,都能以相同的方式执行许多操作。例如,无论你使用 MBR 还是 GPT 方案,都需要一种方法来指示特定的分区。两种方案都允许你通过 -i 和分区号来指示一个分区。

查看和删除分区是常见功能的很好示例。

查看分区

使用 gpart show 可以查看 geom 上所有 GPT 和 MBR 分区的简要概览。通过提供 geom 的名称作为参数,可以仅查看该 geom 上的分区。gpart show 的输出与 fdisk(8) 和其他传统磁盘管理工具的输出没有太大区别。在这里,我通过传统的设备节点查看存储设备,但我也可以使用 diskid、gptid 或任何其他标签:

   $ gpart show ada0
➊ =>        40  1953525088  ada0  GPT  (932G)
➋           40        1024     1  freebsd-boot  (512K)
➌         1064         984        - free -  (492K)
➍         2048     4194304     2  freebsd-swap  (2.0G)
➎      4196352  1949327360     3  freebsd-zfs  (930G)
➏   1953523712        1416        - free -  (708K)

第一列给出了分区的第一个块;第二列是分区的大小(以块为单位)。第三列是分区号,第四列是分区类型。(我们将在本章后面讨论分区类型:目前,先按流程进行。)最后,我们给出了磁盘大小。

我们的第一个分区从磁盘的第 40 个扇区开始,填充了将近 20 亿个扇区 ➊。第三个字段显示这不是磁盘上的一个分区,而是整个磁盘的条目。第四个字段给出了使用的分区方案。这是一个 GPT 磁盘。整个磁盘大约为 932GB。

第二个条目也从第 40 扇区开始,填充了 1,024 个扇区 ➋。这是分区 1,类型是 freebsd-boot。如果我们想从这个磁盘启动,则需要在此分区上安装引导加载程序。

第三个条目从第 1,064 扇区开始,填充了 984 个扇区 ➌。为什么是 1,064?第一个分区从第 40 扇区开始,填充了 1,024 个扇区,因此前 (1,024 + 40) 1,064 个扇区已被其他分区占用。但这个分区没有分区号,其类型是 - free -。这个分区为 4K 扇区的磁盘进行了对齐。

第四个条目是交换空间,根据分区类型 ➍。它从第 2,048 扇区开始,长度为 4,194,304 个扇区,是分区 2。你通常会看到交换空间位于磁盘的前面,这是过去分区放置对性能有影响的遗留物。然而,如果你使用的是虚拟机,将交换空间放在磁盘的前面可以为你在磁盘末尾扩展分区留下空间。

第五个条目是 FreeBSD ZFS 文件系统,从第 4,196,352 扇区开始,占用大约 19 亿个扇区 ➎。这个 freebsd-zfs 分区包含我们的数据。

磁盘的最后部分有 1,416 个空闲扇区 ➏。空间不足以在对齐到 1MB 边界的同时增加分区的空间。

MBR 磁盘看起来与 GPT 磁盘非常相似。

其他视图

添加命令行标志来修改 gpart show 的输出。

你可以通过底层设备名称和分区编号来组装每个分区的设备节点。如果你想看到设备节点而不是分区编号,可以添加-p标志。

要用分区标签替代分区类型,请使用-l

在这里,我展示了这个磁盘的设备节点和标签:

$ gpart show -pl ada0
=>        40  1953525088    ada0  GPT  (932G)
          40        1024  ada0p1  gptboot0  (512K)
        1064         984          - free -  (492K)
        2048     4194304  ada0p2  swap0  (2.0G)
     4196352  1949327360  ada0p3  zfs0  (930G)
  1953523712        1416          - free -  (708K)

现在,分区编号包含完整的设备名称,比如ada0p3。你不再看到 GPT 分区类型,而是看到应用于 GPT 分区的标签,例如swap0zfs0

要查看人类不友好的 GPT 分区类型,而不是 FreeBSD 显示的名称,请使用-r。我通常在检查其他操作系统的磁盘时使用它。FreeBSD 可能会将多个分区类型标记为ntfs;虽然这对大多数用途来说足够,但如果我在做数字取证,精确的分区方案可能非常重要。

要查看 GPT 分区的更详细描述,请使用gpart list。这会产生类似于glabel list或其他 GEOM 类命令的输出。

删除分区

也许你在创建分区时出错,需要删除其中一个。不是的,你还没有在 MBR 或 GPT 中创建分区,但你遵循的过程无论哪种方式都是一样的。通过编号删除分区。

查看上一节中的分区表。我们有用于启动、交换和 ZFS 的分区。也许你不希望在启动磁盘上有交换空间。使用 gpart delete 命令删除该分区。使用-i标志和你想删除的分区编号。gpart show 命令显示交换空间是分区 2。让我们删除它。

# gpart delete -i 2 ada0
ada0p2 deleted

现在你可以调整 ZFS 分区的大小以使用这些空间。调整分区大小的方式取决于分区方案。

设计磁盘

不,这不是那种故意欺骗你的磁盘。我们谈论的是磁盘的分区方案。在现实世界和存储中,摧毁比创建更容易。在你能够分区磁盘之前,你需要为它分配一个分区方案。

删除磁盘分区方案

你可以逐个删除磁盘上的每个分区,然后摧毁分区方案。然而,这样做工作量很大。更简单的方法是直接丢弃整个磁盘分区表。

你不能擦除带有已挂载分区的磁盘。首先卸载这些分区,并从任何 ZFS 池中删除它们。一旦磁盘真正未使用,使用gpart destroy擦除任何现有的分区表。

# gpart destroy da3
da3 destroyed

如果命令立即返回,说明磁盘没有分区。它可能有一个分区方案,但没有分区。如果你遇到“设备繁忙”错误,要么磁盘仍在使用中,要么磁盘有分区。你可以通过gpart delete逐个删除所有现有分区,然后销毁分区方案,但通过添加-F更容易彻底删除现有方案。

# gpart destroy -F da3

这会强制擦除所有分区和分区方案。运行 gpart show da3 将显示没有分区表。现在可以创建新的磁盘分区。

分配分区方案

在创建磁盘分区之前,您需要标记磁盘所使用的分区方案类型。使用 gpart create 命令并加上 -s 标志和方案类型,如 gptmbr。在这里,我将磁盘标记为使用 GPT 方案:

# gpart create -s gpt da3

使用 gpart show 验证磁盘现在是否有 GPT 分区表。现在可以添加 GPT 分区,或重新创建分区表并使用 MBR 添加这些分区。但我们将从深入了解 GPT 开始。

GPT 分区方案

GUID 分区表(GPT)是现代硬盘分区的标准。这是新安装的推荐标准。除非有非常强烈的理由不使用,比如硬件不支持,否则总是使用 GPT 分区方案。

GPT 支持最大 9.4ZB 的磁盘。1 ZB 等于 10 亿 TB。尽管我们的技术最终会超越 9.4ZB,但我预计 GPT 将持续到我职业生涯的最后。

FreeBSD 的 GPT 实现目前支持 128 个分区。每个分区都会分配一个 GUID,它是一个 128 位的数字,以 32 个十六进制字符显示。虽然 GUID 并不保证在整个文明范围内真正唯一,但它们在你的组织内肯定是唯一的。

大多数现代操作系统支持 GPT 及其前身——主引导记录(MBR)。基于 MBR 的系统将分区记录存储在硬盘的第一个扇区。如果主机仅支持 MBR,但磁盘的第一个扇区包含不是 MBR 的内容,系统会感到困惑,可能会拒绝启动。GPT 方案将 保护性主引导记录(PMBR) 放置在每个磁盘的第一个扇区。PMBR 表示磁盘包含一个类型为 GPT 的 MBR 分区。第二个扇区包含实际的 GUID 分区表。GPT 还在磁盘的最后一个扇区放置了分区表的备份副本,以便更轻松地恢复损坏的数据。

GPT 需要为引导代码分配一个分区。PMBR 启动代码会搜索磁盘上的 FreeBSD 启动分区。这个启动分区必须大于引导代码,小于 545KB,并且保留给 FreeBSD 启动加载程序。FreeBSD 有两个 GPT 启动加载程序,gptboot(8) 和 gptzfsboot(8)。必须在启动分区上安装其中一个。

使用 gptboot(8) 启动基于 UFS 的系统。在系统启动时,gptboot 会搜索标记为 bootmebootonce 属性的 FreeBSD 分区。

在运行 ZFS 的系统上使用 gptzfsboot(8)。

使用 gpart(8) 及其众多子命令来查看、创建、编辑和销毁 GPT 分区。

GPT 设备节点

每个磁盘分区都有一个设备节点。GPT 分区的设备节点是它们所在 geom 的扩展,由字母 p 和分区编号表示。如果你直接在磁盘 ada0 上创建 GPT 分区,第一个分区将是 /dev/ada0p1,第二个是 /dev/ada0p2,依此类推。

许多系统将它们的分区放在上层的 geom 上。我有一个系统使用 SATA RAID,并将磁盘呈现为 /dev/raid/r0。这个磁盘上的分区分别是 /dev/raid/r0p1/dev/raid/r0p2 等等。你也可能通过 GUID 或磁盘 ID 将分区放到一个设备上,从而获得像 /dev/diskid/DISK-WD-WCAW36477062p1 这样的分区。

GPT 分区类型

当你创建 GPT 分区时,必须为其标记一个 分区类型。该类型表示分区的预定用途。FreeBSD 会根据分区类型做出决策,因此要正确分配它们。

严格来说,分区类型是另一个 128 位的 GUID。FreeBSD 使用感叹号标记作为分区类型的 GUID,例如 !516e7cb5-6ecf-11d6-8ff8-00022d09712b。这些分区类型在所有操作系统中都很常见,但大多数操作系统会为这些不适合人类阅读的 GUID 提供易于理解的名称。本书使用的是易于理解的名称;如需查看不适合人类的名称,请查阅 gpart(8)。

你在 FreeBSD 系统中最常见的分区类型包括以下几种:

freebsd-boot FreeBSD 引导加载程序

freebsd-ufs FreeBSD UFS 文件系统

freebsd-zfs FreeBSD ZFS 文件系统

freebsd-swap FreeBSD 交换分区

efi EFI 系统分区,用于从 EFI 启动

你可能还会看到这些 GPT 分区类型。在现代 FreeBSD 中不要使用它们,但知道它们的存在可能有助于你识别出这个奇怪的磁盘是什么,以及如何将其拆解。

freebsd 一个被分为 bsdlabel(8) 分区的 GPT 分区

freebsd-vinum 一个由 gvinum(8) 控制的分区

mbr 一个被细分为 MBR 分区的分区

ntfs 一个包含微软 NTFS 文件系统的分区

fat16, fat32 包含 FAT 文件系统的分区

要查看完整的已识别分区类型列表,请参见 gpart(8)。

创建 GPT 分区

分区磁盘很简单:弄清楚你需要哪些分区,创建它们,然后就可以了。棘手的部分是如何使用你的分区。在创建分区之前,决定你将如何使用这个磁盘。你有多少空间?你希望如何划分?在开始创建分区之前,准确写下你想要实现的目标。

这里,我正在为 UFS FreeBSD 安装手动分区一个 1TB 的磁盘。它将需要一个 512KB 的引导分区(类型 freebsd-boot)和 8GB 的交换分区(类型 freebsd-swap)。其他分区将是 freebsd-ufs 类型:5GB 用于根分区,5GB 用于 /tmp,100GB 用于 /var,其余的用于 /usr。我会为每个分区标明其预定的角色。

使用 gpart(8)创建分区。使用-t标志指定分区类型,-s指定大小,-l为新分区分配 GPT 标签。我将从 boot 分区开始。

# gpart add -t freebsd-boot -l boot -s 512K da3
da3p1 added

使用gpart show检查您的工作。加上-l标志查看 GPT 标签。

# gpart show -l da3
=>        40  1953525088  da3  GPT  (932G)
          40        1024    1  boot  (512K)
        1064  1953524064       - free -  (932G)

这个磁盘有一个分区,大小为 512K,标记为boot。命令执行成功。现在添加交换空间。

# gpart add -a 1m -t freebsd-swap -s 8g -l swap da3
da3p2 added

这个命令与添加 boot 分区的命令非常相似:我们指定分区类型、大小和标签。

等一下——这-a 1m是什么?-a标志允许您设置分区对齐方式,使您可以设置分区相对于磁盘起始位置的开始和结束位置。还记得在本章开始时我提到过,如果文件系统与 4K 磁盘的物理扇区不对齐可能会导致问题吗?-a 1m告诉 gpart 从磁盘的开始位置按 1MB 的偶数倍创建分区。正如我们在本章的第 215 页中“查看分区”部分看到的那样,分区 1 和分区 2 之间会有一些空白空间,但这没问题。这给您留出了空间,必要时可以更改该分区以支持 UEFI(请参阅本章后面部分的“统一可扩展固件接口和 GPT”)。

在创建 5GB 的根分区、/tmp分区和 100GB 的/var分区时,保持 1MB 对齐。

# gpart add -a 1m -t freebsd-ufs -s 5g -l root da3
da3p3 added
# gpart add -a 1m -t freebsd-ufs -s 5g -l tmp da3
da3p4 added
# gpart add -a 1m -t freebsd-ufs -s 100g -l var da3
da3p5 added

创建最后一个分区时,不要指定大小。这告诉 gpart 将该分区做得尽可能大。

# gpart add -a 1m -t freebsd-ufs -l usr da3
da3p6 added

您已分区,并且它已准备好进行安装。

调整 GPT 分区大小

再想一想,也许创建一个巨大的/usr分区并不明智。一个大约 100GB 的/usr分区足以为操作系统文件提供足够的空间,同时为隔离的/home分区留出几百 GB 的空间。我信任大多数用户,但有些用户(2)可能会把/dev/random扔进文件,直到占满所有可用空间。在这里,我将调整/usr的大小,为/home创建空间。

使用gpart resize来更改分区的大小。您必须知道目标分区的分区编号。运行gpart show da3会告诉我们/usr是分区 6。使用-i标志和分区编号来调整分区大小。

# gpart resize -i 6 -s 100g -a 1m da3
da3p6 resized

运行gpart show以查看新的磁盘大小。

# gpart show da3
--snip--
   247465984   209715200    6  freebsd-ufs  (100G)
   457181184  1496343944       - free -  (714G)

这个磁盘在末尾有 714GB 的空闲空间。现在我们可以为所有麻烦的用户创建一个宽敞的/home分区。

每个分区都会分配特定的扇区。若分区两侧没有空闲空间,您无法增加分区的大小。尽管此示例磁盘在分区 6 之后有大量空闲空间,但您无法利用这些空间来增大分区 1 到分区 5 的大小。您必须删除并重新创建这些分区。

更改分区的大小不会更改该分区上文件系统的大小。缩小带有文件系统的分区会切断部分文件系统。增大分区大小不会扩展文件系统。UFS 和 ZFS 都有处理增大分区大小的工具,但你必须将此作为一个独立的过程来处理。

更改标签和类型

你可以使用 gpart modify 命令修改 GPT 分区的类型或 GPT 标签。使用 -i 给出分区号,使用 -l 给出新的标签。在这里,我修改了磁盘 vtbd0 的分区 2 的 GPT 标签:

# gpart modify -i 2 -l rootfs vtbd0

类似地,使用 -t 更改分区的类型:

# gpart modify -i 2 -t freebsd-zfs vtbd0

磁盘的 GPT 表现在声明分区 2 的标签为 rootfs,并且类型为 freebsd-zfs

在传统硬件上启动

较旧的硬件期望在磁盘的开始处看到一个主引导记录(MBR),并且不会识别 GPT 分区表。FreeBSD 使用保护性 MBR(PMBR)为传统硬件提供一个可识别的分区表,并帮助该硬件启动 GPT 分区的磁盘。一个格式化为 GPT 的可启动磁盘需要同时拥有保护性 MBR 和 GPT 引导加载程序。

使用 gpart bootcode 命令和 -b 标志安装一个 PMBR。FreeBSD 提供了一个 PMBR,路径为 /boot/pmbr

# gpart bootcode -b /boot/pmbr da3
bootcode written to da3

这个磁盘将不再干扰那些寻找 MBR 的主机。

你还需要一个引导加载程序。UFS 主机需要 gptboot 引导加载程序,而 ZFS 主机需要 gptzfsboot。为了方便,FreeBSD 在 /boot 目录中提供了每个引导加载程序的副本。这些副本不是磁盘上的引导加载程序,而是该版本 FreeBSD 所需的引导加载程序版本。使用 -p 标志将选定的引导加载程序安装到 gpart bootcode。使用 -i 选项告诉 gpart(8) 要将引导加载程序复制到哪个分区。我们在上一节使用的示例磁盘中,分区 1 的类型是 freebsd-boot,所以我们将使用该分区。

# gpart bootcode -p /boot/gptboot -i 1 da3
partcode written to da3p1

你可以将 -p-b 合并为一个命令。

统一可扩展固件接口和 GPT

统一可扩展固件接口 (UEFI) 是一种较新的标准,用于在不使用 BIOS 仿真的情况下启动 amd64 硬件。FreeBSD 10 及之后的版本支持早期的 UEFI 启动到 UFS,而 FreeBSD 11 能够从 UEFI 启动 ZFS。

UEFI 使用类型为 efi 的分区,该分区必须为 800KB 或更大。使用 gpart create 在新磁盘上创建一个 efi 分区。

# gpart create -s gpt da0
# gpart add -t efi -s 800K da0

FreeBSD 提供了一个 efi 分区,路径为 /boot/boot1.efifat。使用 dd(1) 将其复制到新的引导分区。

# dd if=/boot/boot1.efifat of=/dev/da0p1

根据需要分区剩余的磁盘空间。

一个 efi 分区实际上是一个 FAT 文件系统,具有非常特定的目录层次结构。你可以挂载文件 boot1.efifat 并进行探索。

扩展 GPT 磁盘

我们已经了解了如何扩展分区,但磁盘呢?扩展磁盘通常发生在虚拟主机上。扩展虚拟磁盘时,gpart(8)会抱怨磁盘的 GPT 无效。GPT 和 GEOM 在磁盘的第一个和最后一个扇区存储信息。扩展虚拟磁盘意味着添加扇区。新的最后一个扇区将是空的。使用gpart recover为最后一个扇区创建新的元数据块。

# gpart recover vtbd0

你现在可以在扩展后的虚拟磁盘上创建或扩展分区。

既然你已经掌握了 GPT 分区,我们来看看 MBR,看看为什么 GPT 似乎是一个如此重大的改进。

MBR 分区方案

旧硬件或新但小型的硬件可能需要在其磁盘上使用主引导记录分区。Intel 风格的硬件已经使用 MBR 分区数十年,数百万个设备运行着各种操作系统都在使用它。MBR 方案仅适用于 2TB 或更小的磁盘。更大的磁盘必须使用 GPT 分区。

什么是主引导记录(MBR)?

主引导记录(MBR)是一个文件,占据传统磁盘的前 512 字节,也称为扇区 0。MBR 包含分区信息和一个引导加载程序,允许 BIOS 找到操作系统。术语MBR可能指代磁盘上的实际第一个扇区或该格式使用的分区方案。

主引导记录描述了四个主分区,在 BSD 社区中称为切片。每个切片描述包括该分区包含的磁盘扇区和预计在该切片上使用的文件系统类型。如果一个磁盘上只有一个切片,MBR 仍然列出四个切片,但其中三个切片没有分配任何扇区。虽然 MBR 格式支持最多 20 个扩展分区的链表,但由于 BSD 标签的存在,FreeBSD 不需要这些扩展分区。

四个主切片中的一个被认为是活动的。当系统开机时,引导程序代码会查找活动切片并尝试引导它。

MBR 扇区还包含引导程序代码。你不需要专门为引导加载程序分配空间。在 FreeBSD 中,引导程序代码会查找并执行内核。FreeBSD 包括两个不同的引导加载程序,mbrboot0。mbr 加载程序用于单操作系统主机。如果你在硬件上安装了多个操作系统,使用 boot0 加载程序——或者,最好是将主机专用于 FreeBSD,并将其他操作系统虚拟化。

切片的主要功能是包含一个 bsdlabel(8)分区。

BSD 标签

BSD 出现的时间早于 MBR 或 IBM PC。BSD 使用自己独特的磁盘分区格式,称为 disklabel。现在磁盘标签变得更加常见,disklabel 也被称为 BSD labelsbsdlabels。(如果你想激起一场激烈的讨论,可以问一群 FreeBSD 开发者,哪种叫法更正确。)BSD 系统有多个分区,至少包括 /(根目录),/usr/var/tmp 和交换空间,还有独立的分区用于系统执行的实际工作。

当 BSD 移植到 i386 平台时,它们本可以将磁盘改为使用 MBR 分区。通过扩展 MBR 分区,一个磁盘最多可以拥有 24 个分区。然而,disklabel 分区深嵌在内核中,通常位于没人敢碰的棘手地方。移植小组决定将 MBR 切片视为 BSD 磁盘,并使用 BSD disklabel 对每个切片进行分区。系统管理员需要创建 MBR 分区,然后将 disklabel 分区嵌套在这些 MBR 分区内。^(3)

这种方式有效,但也使得 partition 这个词变得模糊。partition 是指 MBR 分区还是 disklabel 分区呢?FreeBSD 重新使用了 slices 这个词来表示 MBR 分区。每个 MBR 切片都会有自己的 disklabel,列出包含在切片中的 BSD 分区。如果你来自 Linux 或 Microsoft Windows 背景,你所熟悉的 MBR 分区在这里被称为 slices

你不能为切片或 disklabel 分区添加标签。这些格式没有空间容纳标签。相反,应为分区上的 ZFS 或 UFS 文件系统添加标签。

也可以跳过切片,直接在硬盘上安装 disklabel。有些硬件拒绝从这样的磁盘启动,因此它们被称为 dangerously dedicated(危险专用)。随着 GPT 的出现,危险专用磁盘现在不再被广泛使用。

MBR 设备节点

每个磁盘、切片和分区都有一个设备节点。切片设备节点是基础磁盘的扩展,而分区设备节点是设备节点的扩展。以下是在基于 MBR 的系统中,磁盘 ada0 上的设备节点:

/dev/ada0       /dev/ada0s1a    /dev/ada0s1d
/dev/ada0s1     /dev/ada0s1b    /dev/ada0s1e

磁盘的第一个细分是切片。设备节点使用字母 s 和 1 到 4 的数字表示切片。第一个切片是 s1,第二个是 s2,以此类推。未使用的 MBR 分区没有设备节点。在这里,/dev/ada0s1 是磁盘上的第 1 个切片。

第二层细分是切片内的 disklabel 分区。每个分区都有一个唯一的设备节点名称,通过在切片的设备节点后添加字母来创建。在这里,我们有四个 disklabel 分区,从 /dev/ada0s1a/dev/ada0s1e。传统上,结尾为 a/dev/ada0s1a)的节点是根分区,而结尾为 b/dev/ada0s1b)的节点是交换空间。

请注意,设备节点列表中没有使用字母 c。c 分区表示整个切片。如今,你在切片条目上运行磁盘分区工具,而不是在切片的 disklabel 上。

你可以随意分配 d 到 h 的分区。一个默认的磁盘标签最多可以有七个可用分区。每个磁盘最多可以有四个分区,因此你可以在一个磁盘上拥有最多 28 个分区。一个磁盘标签最多支持 20 个分区,但在第一次创建标签时,必须明确表示要使用额外的分区。

MBR 和磁盘标签对齐

分区有各自的磁盘扇区和文件系统块对齐问题。传统上,MBR 分区会在磁道边界结束。虽然在现代硬件上,磁道边界并没有实际意义,但即使是较新的硬盘也会为旧的或能力较弱的硬件提供这样的“舒适的谎言”。如果你创建的 MBR 分区没有在磁道边界结束,并且将该磁盘放入需要遵守磁道边界的机器中,这台机器可能会发生某种“崩溃”。今天你所创建的分区,理论上可能会进入一台较老的系统中。因此,FreeBSD 会安排分区,使它们结束在磁道边界上。磁道边界不仅可以,而且可能与 4K 磁盘扇区大小冲突。至少,MBR 本身就占用了第一个磁道,或者 63 个 512 字节的扇区!

幸运的是,你很少需要写入分区表,并且写入分区表的性能问题也很少。只要你将磁盘标签分区对齐到 1MB 的边界,你会在分区表和磁盘标签分区之间丢失几个扇区,但性能将会是正常的。

所以:对齐磁盘标签分区。不要对齐分区。

创建分区

使用 gpart(8)来管理 MBR 分区。要创建一个分区,你需要指定一个分区类型和大小。FreeBSD 的分区使用类型freebsd。如果不指定大小,gpart(8)将使用所有可用空间。在一个空的磁盘上,这会将整个磁盘分配给一个单一的分区。

在这里,我删除现有的分区布局,告诉磁盘使用 MBR 方案,并创建一个单一的 FreeBSD 分区:

# gpart destroy -F ada3
# gpart create -s mbr ada3
# gpart add -t freebsd ada3
ada3s1 added

运行gpart show,你会看到该磁盘现在有一个单独的分区。添加-p标志以查看分区的设备节点。

# gpart show -p ada3
=>        63  1953525105    ada3  MBR  (932G)
          63  1953525105  ada3s1  freebsd  (932G)

我们的分区 ada3s1 现在已经准备好用于磁盘标签分区。

要创建多个分区,使用-s指定大小。小型嵌入式系统的常见配置是在一个磁盘上放置三个分区。两个较小的分区包含操作系统的不同版本,而第三个则包含任何数据。在这里,我将这个 1TB 的磁盘分为两个 150GB 的分区,并将其余的分配给第三个分区:

# gpart add -s 150g -t freebsd ada3
ada3s1 added
# gpart add -s 150g -t freebsd ada3
ada3s2 added
# gpart add -t freebsd ada3
ada3s3 added

删除分区

使用gpart delete来删除不需要的分区。通过-i指定分区编号。在这里,我删除了我们上一节中创建的多分区磁盘中的第三个较大分区:

# gpart delete -i 3 ada3
ada3s3 deleted

激活分区

激活的分区是 BIOS 尝试启动的分区。使用-a激活标志设置激活分区。使用-i指定激活分区的编号。

# gpart set -a active -i 1 ada3

通过设置不同的激活分区,改变启动的分区。

引导磁盘还需要一个引导加载程序。虽然 MBR 引导加载程序与 GPT 或 UEFI 引导加载程序不同,但它使用相同的 gpart(8) -b标志。FreeBSD 提供了一个 MBR 引导加载程序副本,位于/boot/mbr

# gpart bootcode -b /boot/mbr ada3

磁盘 ada3 上的切片 1 现在是可引导的。现在你已经对磁盘进行了分区,可以在这些切片内部创建 BSD 标签了。

BSD 标签

在切片内部创建 BSD 标签(或磁盘标签)分区,就像创建切片或 GPT 分区一样。你必须告诉存储设备使用的方案,创建并删除分区,直到你满意为止,然后安装引导加载程序。

创建 BSD 标签

在 GPT 和 MBR 特定提供分区表空间的情况下,你必须创建一个 BSD 标签并将其写入切片的开头。和任何方案一样,使用-s和方案名称。将该方案安装在切片上,而不是磁盘上。

假设你想在切片 ada3s1 上创建一个 BSD 标签。使用 BSD 方案。

# gpart create -s bsd ada3s1
ada3s1 created

这是一个默认的磁盘标签,最多可以容纳 8 个磁盘标签分区。你可以通过使用-n标志增加分区数目,最多可达 20 个。在这里,我在 ada3s3 这个大分区上创建了一堆分区。

# gpart create -n 20 -s bsd ada3s3
ada3s3 created

这个切片上没有实际的磁盘标签分区;只有一个可以包含磁盘标签分区的标签。标签一旦存在,你就可以创建这些分区了。

创建 BSD 标签分区

在盲目输入分区命令之前,先计划好如何对磁盘进行分区。提前在纸上搞清楚比在命令行上解决要容易得多。我打算将磁盘上第一个 150GB 的切片分配给 UFS 文件系统。这个切片将为/(根)、交换分区和/tmp分配 5GB 的分区。其余的空间将分配给/usr。为什么不为/var分区?我会把大的切片 ada3s3 分配给/var。我不需要添加引导分区,因为 MBR 磁盘不需要引导分区。

要创建磁盘标签分区,你必须使用-t指定类型,使用-s指定大小——就像你为 GPT 分区做的那样。FreeBSD UFS 文件系统的类型是freebsd-ufs。我们从根分区开始。

# gpart add -t freebsd-ufs -s 5g -a 1m ada3s1
ada3s1 added

要查看这个分区,你必须给gpart show提供切片设备,而不是磁盘设备。使用磁盘设备会显示切片。

# gpart show ada3s1
=>        0  314572800  ada3s1  BSD  (150G)
          0       1985          - free -  (993K)
       1985   10485760       1  freebsd-ufs  (5.0G)
   10487745  304085055          - free -  (145G)

输出的第三行显示了我们的 5GB 分区。

在这个切片的最开始,我们有 1985 个空闲块,约 993KB。我要求分区对齐到 1MB 边界,所以 gpart 浪费了一些空间来满足这个请求。我乐意失去这 993KB,而不是降低系统的性能。

现在创建类型为freebsd-swap的交换分区。

# gpart add -t freebsd-swap -s 5g -a1m ada3s1
ada3s1b added

接下来是 5GB 的/tmp。然后,我将剩余的空间分配给一个/usr分区,通过省略大小来完成。

# gpart add -t freebsd-ufs -s 5g -a1m ada3s1
ada3s1d added
# gpart add -t freebsd-ufs -a1m ada3s1
ada3s1e added

一个gpart show命令显示我们的磁盘标签分区在磁盘末尾浪费了 63 个块,约 32KB。看我对此毫不在乎。

这些分区现在已经准备好接收文件系统了。我们在第十一章中讨论 UFS。

分配特定的分区字母

在传统的 BSD 标签上,a分区用于根文件系统,而b用于交换。c分区表示整个切片。这不是强制性的,但我建议不要将这些字母用于任何其他用途。

为什么这很重要?我曾经在一台服务器上添加了一个硬盘,以便为数据库提供更多空间。我们将数据库软件迁移到了分区a,而实际数据则迁移到了分区b。^(4) 几个月后,当我去度假时,系统虚拟内存不足。我接到了系统管理员的电话,他找到了并激活了新硬盘上未配置的交换空间——但现在数据库数据丢失了。是的,公司失去了几位客户,并且损失了成千上万美元的收入,这很可悲——但更重要的是,这毁了一天的假期,并且对剩余的假期蒙上了阴影。这是不可接受的。

不要试图反抗这些传统,尤其是在越来越不常见的磁盘格式上。除非是由伯克利的长老们规定,否则不要将字母abc用于其他分区。

gpart 程序设计时是为了处理分区编号,而不是字母。然而,在创建磁盘标签时,gpart add会将索引号映射到字母上。分区 1 是a,分区 2 是b,依此类推。通过在创建分区时指定分区索引,你将字母分配给该分区。

如果你没有指定分区号,gpart add会从分区字母a开始分配分区字母。你可能将第一个分区编号为 18,但如果没有为下一个分区指定编号,它将默认使用分区a。为了避免使用abc,为你创建的每个分区使用数字。你只能使用字母,直到分区有的磁盘标签槽位的数量。标准磁盘标签只能使用字母ah,而一个支持 20 个分区的标签可以使用at

在我的三分区系统中,我想将/var放在 ada3s3 上。我想使用一个不同于abc的字母,所以我随机选择了索引 18。它几乎与/usr的分区完全相同,只不过我们将它添加到了不同的切片上。

# gpart add -t freebsd-ufs -a 1m -i 18 ada3s3
ada3s3r added

要查看该磁盘标签分区,你需要运行gpart show ada3s3。加上-p可以查看设备名称。

# gpart show -p ada3s3
=>         0  1324379505   ada3s3  BSD  (632G)
           0        1985           - free -  (993K)
        1985  1324376064  ada3s3r  freebsd-ufs  (632G)
  1324378049        1456           - free -  (728K)

你知道吗?我们字母表中的第 18 个字母是R

有了分区,我们可以开始查看文件系统了。

第十一章:UNIX 文件系统

image

FreeBSD 的文件系统——Unix 文件系统(UFS)是 BSD 4.4 自带文件系统的直接后代。原 UFS 的作者之一仍然在开发 FreeBSD 的文件系统,并在近年来增加了许多有用的功能。FreeBSD 并不是唯一一个仍在使用 4.4 BSD 文件系统或其后代的操作系统。如果一个 Unix 供应商没有特别宣传其“改进和高级”的文件系统,那么它很可能正在使用 UFS 衍生系统。

UFS 作为最初的文件系统,使其有机会在 FreeBSD 中扩展。许多 UFS 的概念为 FreeBSD 支持其他文件系统提供了基础,从 ZFS 到光盘。即使你不打算使用 UFS,了解 UFS 的基本知识也是理解 FreeBSD 如何管理文件系统的关键。

像 Unix 的其他部分一样,UFS 旨在有效地处理最常见的情况,同时可靠地支持不常见的配置。FreeBSD 自带的 UFS 配置旨在尽可能在现代硬件上广泛使用,但如果需要,你可以选择为数万亿个小文件或几块 1TB 的大文件优化特定的文件系统。

我们今天所称的 UFS 实际上是 UFS 版本 2,或称UFS2。原始 UFS 无法处理现代的磁盘容量。

UFS 最适合较小的系统或无法处理 ZFS 开销的应用程序。许多人更喜欢在虚拟机中使用 UFS。我在第二章中讨论了选择文件系统的问题。

UFS 组件

UFS 由两层构成,一层叫做Unix 文件系统,另一层是快速文件系统(FFS)。UFS 处理文件名、将文件附加到目录、权限以及用户关心的所有琐碎细节。FFS 则负责将文件写入磁盘并对其进行排列,以便快速访问。这两者协同工作提供数据存储。

快速文件系统

FFS 由超级块、块、碎片和索引节点组成。

超级块记录了文件系统的特征。它包含一个魔术数字,用于识别文件系统为 UFS,以及内核用于优化文件读写的文件系统几何信息。UFS 文件系统会保留多个超级块的备份,以防主块损坏。

是存储数据的磁盘片段。FreeBSD 默认使用 32KB 的块。FFS 将块映射到底层磁盘或 GEOM 提供者的特定扇区。每个存储的文件都会被分割成 32KB 的块,每个块都会存储在自己的块中。

不是所有文件都是 32KB 的整数倍,因此 FFS 将剩余部分存储在碎片中。标准为块大小的八分之一,即 4KB。例如,一个 39KB 的文件将填满一个块和两个碎片。一个碎片只有 3KB,因此碎片确实浪费了磁盘空间——但它们浪费的空间比到处使用完整块要少得多。

UFS 如何使用 FFS

UFS 将某些 FFS 块分配为 inode,或 索引节点,用来将块和碎片映射到文件。一个 inode 包含每个文件的大小、权限以及包含该文件的块和碎片列表。总的来说,inode 中的数据被称为 元数据,即关于数据的数据。

每个文件系统有一定数量的 inode,与文件系统的大小成比例。现代磁盘每个分区可能有数十万个 inode,足以支持数十万个文件。然而,如果你有大量非常小的文件,你可能需要重建文件系统来支持更多的 inode。使用 df -i 查看文件系统中剩余的 inode 数量。

理论上,UFS 可以在除 FFS 以外的存储层上运行。这也是许多基于日志或基于区块的文件系统的工作原理。然而,经过数十年的发展,UFS 和 FFS 的特性,如日志和软更新,已经深度交织在一起,以至于分离这两者变得不再现实,甚至连稍微可行的可能性都没有。

Vnodes

如果你只使用 UFS 文件系统,并且所有硬盘都永久附加,那么 inode 和块的工作是完美的。然而,现如今我们通常会在不同的机器之间甚至不同的操作系统之间交换磁盘。你可能需要在桌面上读取光盘和闪存磁盘,服务器甚至可能需要接受为不同操作系统格式化的硬盘。

FreeBSD 使用存储抽象层——虚拟节点,或 vnode——在文件系统和内核之间进行调解。你永远不会直接操作 vnode,但 FreeBSD 文档经常提到它们。Vnode 是内核与已挂载文件系统之间的翻译层。如果你是面向对象编程的开发者,可以把 vnode 想象成所有存储类继承的基类。当你写文件到 UFS 文件系统时,内核将数据定向到一个 vnode,这个 vnode 又映射到一个 UFS inode 和 FFS 块。当你写入 FAT32 文件系统时,内核将数据定向到一个 vnode,该 vnode 映射到 FAT32 文件系统的特定部分。只有在处理 UFS 文件系统时才使用 inode,但在处理任何文件系统时都使用 vnode。

挂载和卸载文件系统

mount(8) 程序的主要功能是将文件系统附加到主机的文件系统树上。虽然 FreeBSD 在启动时会挂载 /etc/fstab 中列出的每个文件系统,但你必须理解 mount(8) 的工作原理。如果你之前从未操作过挂载,可以将你的 FreeBSD 测试机启动到单用户模式(参见 第四章)并跟随操作。

在单用户模式下,FreeBSD 会将根分区挂载为只读。在传统的类 Unix 系统中,根分区只包含足够的系统文件来执行基本的设置、启动核心服务并找到其余的文件系统。其他文件系统没有被挂载,因此它们的内容无法访问。当前的 FreeBSD 安装程序将所有内容放在根分区中,因此你可以得到基本的操作系统,但任何特殊的文件系统、网络挂载等都会是空的。你可能需要挂载其他文件系统来进行系统维护。

挂载标准文件系统

要手动挂载在 /etc/fstab 中列出的文件系统,例如 /var/usr,可以给 mount(8) 命令指定你想要挂载的文件系统名称。

# mount /media

这会按照 /etc/fstab 中列出的方式挂载分区,并带有该文件中指定的所有选项。如果你想挂载 /etc/fstab 中列出的所有分区,除了那些标记为 noauto 的分区,可以使用 mount 命令的 -a 标志。

# mount -a

当你挂载所有文件系统时,已经挂载的文件系统不会被重新挂载。

特殊挂载

你可能需要将文件系统挂载到不寻常的位置或临时挂载某个东西。我通常在安装新磁盘时手动挂载磁盘。使用设备节点和期望的挂载点。如果我的 /var/db 分区是 /dev/gpt/db,并且我想将其挂载到 /mnt,我会运行:

# mount /dev/gpt/db /mnt

卸载分区

当你想要从系统中断开文件系统时,可以使用 umount(8) 命令告知系统卸载该分区。(注意命令是 umount,而不是 unmount。)

# umount /usr

你不能卸载正在被任何程序使用的文件系统。如果你无法卸载某个分区,可能是你正在以某种方式访问它。即使是命令行提示符在挂载目录中,也会阻止你卸载底层分区。运行 fstat | grep /usr(或其他分区)可以显示阻止操作的程序。

UFS 挂载选项

FreeBSD 支持几种挂载选项来改变文件系统的行为。当你手动挂载分区时,可以使用 -o 参数来指定任何挂载选项。

# mount -o ro /dev/gpt/home /home

你还可以在 /etc/fstab 中指定挂载选项(见 第十章)。在这里,我对 /home 文件系统使用 ro 选项,就像在前面的命令行中一样。

/dev/gpt/home /home ufs ro 2 2

mount(8) 手册页列出了所有 UFS 挂载选项,但这里是最常用的一些。

只读挂载

如果你想查看磁盘的内容,但不允许更改它们,可以将分区挂载为 只读。你不能修改磁盘上的数据或写入任何新数据。在大多数情况下,这种方式是最安全但最没有用的挂载方式。

许多系统管理员希望将根分区,甚至 /usr,挂载为只读,以最小化来自入侵者或恶意软件对系统的潜在损害。这最大化了系统的稳定性,但极大地增加了维护的复杂性。如果你使用自动部署系统,如 Ansible 或 Puppet,并且习惯性地从零开始重新部署服务器,而不是进行升级,那么只读挂载可能适合你。

只读挂载在损坏的计算机上尤其有价值。当 FreeBSD 检测到文件系统损坏或脏文件系统时,它不会允许执行标准的读写挂载,但如果文件系统没有严重损坏,它会执行只读挂载。这为从濒临崩溃的磁盘中恢复数据提供了机会。

要以只读方式挂载文件系统,请使用 rdonlyro 选项。两者效果相同。

同步挂载

同步(或 sync挂载是挂载文件系统的老式方式。当你向同步挂载的磁盘写入数据时,内核会等待写入是否实际完成,然后才告知程序。如果写入没有成功完成,程序可以选择相应地采取措施。

同步挂载在崩溃时提供最大的数据信用度,但它也比较慢。诚然,如今“慢”是相对的,因为即使是便宜的磁盘也能超越几年前的高端产品。如果你希望在数据完整性方面表现得极为谨慎,可以考虑使用同步挂载,但在几乎所有情况下,它都是过度的。

要同步挂载分区,请使用 sync 选项。

异步挂载

尽管 异步挂载 很大程度上被软更新取代(参见软更新章节,第 237 页),你仍然会听到关于它们的讨论。为了更快的数据访问但伴随更高的风险,可以选择异步挂载分区。当磁盘以异步方式挂载时,内核会将数据写入磁盘,并在不等待磁盘确认数据已实际写入的情况下告诉写入程序写入成功。

异步挂载适用于一次性文件系统,如在关机时消失的内存文件系统,但不要用于重要数据。异步挂载和结合软更新的 noasync 之间的性能差异微乎其微。(下一节将讲解 noasync。)

要异步挂载分区,请使用 async 选项。

同步和异步结合

FreeBSD 的默认 UFS 挂载选项将同步和异步挂载组合为 noasync。使用 noasync 时,影响 inode 的数据会同步写入磁盘,而实际数据则异步处理。结合软更新(本章后面会介绍),noasync 挂载创建了一个非常稳健的文件系统。

由于 noasync 挂载是默认选项,挂载时无需指定它,但当别人使用时,不要让它困扰你。

禁用 Atime

UFS 中的每个文件都包含一个访问时间戳,称为atime,记录文件最后一次被访问的时间。如果您有大量文件且不需要这些数据,可以将磁盘挂载为noatime,以便 UFS 不更新此时间戳。这对于闪存介质或负载较重的磁盘(如 Usenet 新闻缓冲驱动器)最为有用。不过,某些软件会使用 atime,因此不要盲目禁用它。

禁用执行

您的策略可能要求某些文件系统不应有可执行程序。noexec挂载选项防止系统在文件系统上执行任何程序。挂载/home为 noexec 可以帮助防止用户运行自己的程序,但为了有效性,还应将/tmp/var/tmp以及用户可以写入自己文件的任何地方挂载为 noexec。

noexec 挂载并不会阻止用户运行 shell 脚本或解释型脚本(如 Perl 或 Python 等)。虽然脚本可能位于 noexec 文件系统上,但解释器通常不在该文件系统上。

noexec 挂载的另一个常见用途是,当您有一个包含不同操作系统或硬件架构的二进制文件的文件系统时,您不希望任何人执行它们。

禁用 SUID

Setuid 程序允许用户以另一个用户的身份运行程序。例如,像 login(1)这样的程序必须作为 root 执行某些操作,但必须由普通用户运行。显然,setuid 程序必须小心编写,以防止入侵者利用它们获取未经授权的系统访问权限。许多系统管理员习惯性地禁用所有不必要的 setuid 程序。

nosuid选项禁用文件系统中所有程序的 setuid 访问。与 noexec 一样,脚本包装器可以轻松绕过 nosuid 限制。

禁用聚集

FFS 通过聚集优化物理介质上的读写操作。它不是将文件分散到硬盘的各个位置,而是将整个文件以大块的形式写入。同样,读取文件时以较大块的形式读取是有意义的。您可以使用挂载选项noclusterr(用于读取聚集)和noclusterw(用于写入聚集)来禁用此功能。

禁用符号链接

nosymfollow选项禁用符号链接,或文件的别名。符号链接主要用于创建指向位于其他分区的文件的别名。要创建指向同一分区上另一个文件的别名,请使用常规链接。有关链接的讨论,请参见 ln(1)。

指向目录的别名始终是符号链接;对于这些,您不能使用硬链接。

UFS 的弹性

UFS 起源于当时断电意味着数据丢失的时代。经过数十年的使用和调试,UFS 几乎永远不会丢失数据,尤其是与其他开源文件系统相比。UFS 通过仔细的完整性检查来实现这一弹性,特别是在像断电这样的意外关机后。

弹性恢复的重点不是验证磁盘上的数据——UFS 在这方面表现相当好。它的目的是加速在意外关机后完整性验证和文件系统恢复。现代硬盘的容量意味着没有额外的弹性机制,验证可能需要很长时间。对一个 100MB 文件系统的完整性检查要比对一个多 TB 的文件系统进行相同的检查要快得多!增加弹性机制可以提高恢复时间。

UFS 提供了几种方法来提高 UFS 文件系统的可靠性,如软更新和日志记录。在创建文件系统之前,选择一个适合您需求的文件系统。

软更新

软更新 是一种技术,用于组织和安排磁盘写入操作,以确保文件系统的元数据始终保持一致,从而提供几乎与异步挂载相当的性能,并具备同步挂载的可靠性。这并不意味着所有数据都会安全地写入磁盘——在错误时刻发生的电源故障仍然可能导致数据丢失。无论操作系统如何处理,在电源断开的那一刻,正在写入磁盘的文件都无法到达磁盘。但磁盘上的实际内容将是内部一致的。软更新使得 UFS 能够在故障后快速恢复。

你可以在挂载或创建文件系统时启用或禁用软更新。

随着文件系统的增长,软更新会显示出其局限性。多达数 TB 的文件系统在非计划性关机后仍需要相当长的时间来恢复。原始的软更新日志论文 (www.mckusick.com/softdep/suj.pdf) 提到,一个 92%满的 14 盘阵列,在故意损坏文件系统后,完成完整性检查需要 10 小时。你需要在此之前就拥有日志记录。

软更新日志

一个日志文件系统记录所有实际文件系统之外的更改。这些更改会迅速写入存储设备,然后以更为从容的速度插入文件系统中。如果系统意外死机,文件系统会自动从日志中恢复任何更改。这大大减少了启动时重建文件系统完整性的需求。当你安装 FreeBSD 时,默认会创建带有软更新日志的 UFS 分区。

软更新日志并不是记录所有事务,而是记录所有元数据更新,以确保文件系统始终能够恢复到一个内部一致的状态。基准测试显示,日志记录对软更新的负载影响极小。然而,它确实增加了 I/O 开销,因为系统必须将所有更改转储到日志中,然后再将它们重新播放到文件系统中。然而,它极大地减少了恢复时间。那个需要 10 小时才能完成完整性检查的 14 盘阵列?使用日志后,它在相同的损坏情况下恢复的时间不到一分钟。

带日志的软更新非常强大。为什么不总是使用日志呢?软更新日志会禁用 UFS 快照。如果你需要 UFS 快照,就不能使用日志。但如果你需要快照,实际上使用 ZFS 可能会更好。FreeBSD 版的 dump(8) 使用 UFS 快照来备份实时文件系统。现在只有我们这些老派 Unix 用户还在使用 dump,主要是因为我们已经熟悉它,但如果你的组织要求使用 dump(8),你需要另一种容错选项。

GEOM 日志

FreeBSD 也可以在 GEOM 层级使用 gjournal(8) 进行日志记录。像任何其他文件系统日志一样,gjournal 记录文件系统事务。在启动时,FreeBSD 检查日志文件中是否有尚未写入文件系统的更改,并执行这些更改,以确保文件系统的一致性。Gjournal 在软更新日志之前就已经存在。

虽然软更新日志只记录元数据,但 gjournal 记录所有文件系统事务。在系统故障时,你丢失数据的可能性较小,但一切都会被写入两次,从而影响性能。不过,如果你使用 gjournal,不要使用任何类型的软更新。你还应该以异步方式挂载文件系统。你可以在 gjournaled 文件系统上使用快照。

Gjournal 每个文件系统使用 1GB 的磁盘空间。你不能随便开关它——必须为日志提供空间。你可以为日志使用单独的分区,或者如果留出空间,可以将这个一 GB 包含在分区内。如果决定将 gjournal 添加到现有的分区,你需要找到空间。

你应该使用 gjournal 还是软更新日志?如果可能的话,我建议使用软更新日志。如果这不是一个选项,就使用普通的软更新。如果你需要 UFS 快照,包括在快照上的 dump(8),那么使用 GEOM 日志。就我个人而言,我已经不再使用 gjournal 了。

创建和调优 UFS 文件系统

在上一章中,我们对你的磁盘进行了分区和标签操作。现在让我们在这些分区上创建文件系统。使用 newfs(8) 创建 UFS 文件系统,设备节点作为最后一个参数。在这里,我在设备 /dev/gpt/var 上创建了一个文件系统:

# newfs /dev/gpt/var
/dev/gpt/var: ➊51200.0MB (104857600 sectors) ➋block size 32768, ➌fragment size 4096
        using ➍82 cylinder groups of 626.09MB, 20035 blks, 80256 inodes.
super-block backups (for fsck_ffs -b #) at:
➎192, 1282432, 2564672, 3846912, 5129152, 6411392, 7693632, 8975872,
--snip--

第一行重复设备节点并打印分区的大小 ➊,以及块 ➋ 和碎片大小 ➌。你将获得文件系统几何信息 ➍,这是一种遗留信息,源自硬盘几何曾与硬件有某种关系的时代。最后,newfs(8) 打印出超级块备份列表 ➎。你的文件系统越大,备份超级块就越多。

如果你想使用软更新日志,请添加 -j 标志。要使用没有日志的软更新,请添加 -U 标志。在创建文件系统后,你可以通过 tunefs(8) 启用或禁用软更新日志,或使用普通的软更新。

UFS 标签

设备节点可以改变,但标签保持不变。最佳实践是为 GPT 分区添加标签,但不能为 MBR 分区添加标签。MBR 上的 UFS 文件系统可以使用 -L 标志添加 UFS 标签。

# newfs -L var /dev/ada3s1d

这些标签会出现在 /dev/ufs 中。在 /etc/fstab 和其他配置文件中使用它们,以避免磁盘重命名的混乱。你不能将 UFS 标签应用到非 UFS 文件系统。

如果你在 GPT 分区上使用 UFS,请选择 GPT 或 UFS 标签。由于衰退,你每次只能看到一个标签,这可能会让你感到困惑。

块大小和碎片大小

UFS 的效率与读取或写入的块和碎片数量成正比。通常,FreeBSD 读取一个 10 块的文件的时间是读取一个 20 块文件的一半。FreeBSD 开发人员选择默认的块和碎片大小是为了适应最广泛种类的文件。

如果你有一个专用文件系统,主要包含大文件或小文件,你可以考虑在创建文件系统时更改块大小。虽然可以更改现有文件系统的块大小,但这是一个糟糕的主意。块大小必须是 2 的幂。碎片是块大小的八分之一这一假设在许多地方是硬编码的,因此让 newfs(8) 根据块大小来计算碎片大小。

假设我有一个专门用于大文件的文件系统,我想增加块大小。默认的块大小是 32KB,那么下一个较大的块大小就是 64KB。使用 -b 指定新的块大小。

# newfs -b 64K -L home /dev/da0s1d

如果你将有很多小文件,可能需要考虑使用更小的块大小。需要注意的一点是,碎片大小不应小于底层磁盘的物理扇区大小。FreeBSD 默认使用 4KB 的碎片。如果你的磁盘有 4KB 的扇区,千万不要使用更小的碎片大小。如果你完全确定你的磁盘有 512 字节的物理扇区,可以考虑创建一个 16KB(甚至 8KB)块大小的文件系统,并使用相应的 2KB 或 1KB 碎片大小。

在我的系统管理员生涯中,我只需要^(1)定制块大小两次。在遇到性能问题之前不要使用定制块大小。

使用 GEOM 日志

在使用 gjournal(8) 之前,决定将 1GB 日志放在哪里。如果可能,我建议将这一千兆字节包含在文件系统分区中。这意味着如果你想要一个 50GB 的文件系统,就将它放入一个 51GB 的分区。否则,使用一个单独的分区。

在执行任何 gjournal 操作之前,通过 gjournal load 或在 /boot/loader.conf 中加载 geom_journal 内核模块。

要创建一个 gjournal 提供者,同时将分区包含在日志中,请使用 gjournal label 命令。

# gjournal label da3p5

如果你想让一个独立的提供者作为日志,请将该提供者作为第二个参数添加。

# gjournal label da3p5 da3p7

如果成功,这些命令会静默执行。它们会创建一个新设备节点,名称与您的日志设备相同,只是末尾加上 .journal。运行 gjournal label da3p5 会创建 /dev/da3p5.journal。从此以后,所有工作都将在日志设备节点上进行。

在日志设备上创建新的 UFS 文件系统。使用 -J 标志告诉 UFS 它是在 gjournal 上运行的。不要启用任何形式的软更新,包括软更新日志功能。这似乎有效一段时间……然后它就不行了。

将 gjournal 文件系统挂载为异步模式。适用于异步挂载的常规警告并不适用于 gjournal。然而,gjournal GEOM 模块会处理通常由文件系统管理的验证和完整性检查。

/dev/da3p5.journal /var/log ufs rw,async 2 2

文档中说,你可以将现有分区转换为使用 gjournal,只要你为日志设置了一个单独的分区,且现有文件系统的最后一个扇区为空。实际上,我发现现有文件系统的最后一个扇区总是已满,但如果你愿意,试着阅读 gjournal(8) 来获取详细信息。

调整 UFS 设置

你可以使用 tunefs(8) 查看和更改每个 UFS 文件系统的设置。这可以让你启用和禁用特性;此外,你还可以调整 UFS 如何写入文件、管理空闲空间以及使用文件系统标签。

查看当前设置

使用 -p 标志和分区的当前挂载点或底层提供者查看文件系统的当前设置。

   # tunefs -p /dev/gpt/var
   tunefs: POSIX.1e ACLs: (-a)                                disabled
➊ tunefs: NFSv4 ACLs: (-N)                                   disabled
➋ tunefs: MAC multilabel: (-l)                               disabled
➌ tunefs: soft updates: (-n)                                 enabled
➍ tunefs: soft update journaling: (-j)                       enabled
➎ tunefs: gjournal: (-J)                                     disabled
   tunefs: trim: (-t)                                         disabled
   tunefs: maximum blocks per file in a cylinder group: (-e)  4096
   tunefs: average file size: (-f)                            16384
   tunefs: average number of files in a directory: (-s)       64
➏ tunefs: minimum percentage of free space: (-m)              8%
   tunefs: space to hold for metadata blocks: (-k)            6408
   tunefs: optimization preference: (-o)                      time
➐ tunefs: volume label: (-L)

许多可用的设置与特定的安全功能相关,而这些我们并未涉及。像 MAC 限制 ➋ 和各种 ACL 类型 ➊ 的话题充满了整本书。但我们可以看到,这个文件系统使用了软更新 ➌ 和软更新日志功能 ➍,但并未使用 gjournal ➎。我们获得了最小的空闲空间 ➏。最后,我们有一个不存在的 UFS 标签 ➐。我们得到了关于文件系统几何结构和块大小的一堆信息。

使用 tunefs(8) 在未挂载的文件系统上更改任何这些设置。方便的是,tunefs(8) 会显示每个设置对应的命令行标志。我通常会在更改文件系统设置之前启动单用户模式。

你可能会注意到,你可以调整各种文件系统内部设置,如块排列和文件系统几何结构。但请不要这么做。在超过二十年的 FreeBSD 使用过程中,我从未见过有人通过调整这些设置来改善他们的情况。反而,我见过很多人调整这些设置并最终让自己陷入困境。

但让我们来看看你实际上可能需要启用和禁用的设置。

软更新和日志功能

使用 -j 标志启用或禁用文件系统上的软更新日志功能。启用该功能会自动启用软更新。

# tunefs -j enable /dev/gpt/var
Using inode 5 in cg 0 for 33554432 byte journal
tunefs: soft updates journaling set

若要禁用软更新日志功能,请使用 disable 关键字。

# tunefs -j disable /dev/gpt/var
Clearing journal flags from inode 5
tunefs: soft updates journaling cleared but soft updates still set.
tunefs: remove .sujournal to reclaim space

在一个未启用日志的文件系统上启用软更新日志只会让问题变得更加复杂。挂载该文件系统并删除文件系统根目录中的 .sujournal 文件。请注意,关闭日志功能并不会移除软更新功能。使用 -n enable-n disable 可以开启和关闭软更新(没有日志功能)。

最小空闲空间

UFS 会保留每个分区的 8% 空间,以便它有空间重新排列文件以提高性能。我在 “UFS 空间保留” 章节中进一步讨论了这一点,见 第 249 页。如果你想更改此百分比,可以使用 -m 标志。在这里,我告诉文件系统仅保留 5% 的磁盘空间。

# tunefs -m 5 /dev/gpt/var
tunefs: minimum percentage of free space changes from 8% to 5%
tunefs: should optimize for space with minfree < 8%

你现在应该有更多可用的磁盘空间了。同时,由于 UFS 总是尽可能紧密地打包文件系统,它会运行得更慢。

SSD TRIM

固态硬盘使用磨损平衡来延长其使用寿命。磨损平衡在文件系统通知 SSD 每个块不再使用时效果最佳。TRIM 协议负责此通知。通过 -t 标志在你的 SSD 支持的文件系统上启用 TRIM。

# tunefs -t enable /dev/gpt/var
tunefs: issue TRIM to the disk set

为了获得最佳效果,请为固态硬盘上的每个分区启用 TRIM。使用 newfs -E 在文件系统创建时启用 TRIM。

为 UFS 文件系统添加标签

你可以通过 -L 标志为现有的文件系统添加 UFS 标签。

# tunefs -L scratch /dev/ada3s1e

不要混合使用 UFS 和 GPT 标签——这样只会让自己更混淆。

扩展 UFS 文件系统

你的虚拟机磁盘空间不足?扩大磁盘,并扩展最后一个分区来覆盖那个空间,正如在第十章中讨论的那样。那么,那个分区上的文件系统怎么办?这就是 growfs(8) 发挥作用的地方。

growfs(8) 命令将现有的 UFS 文件系统扩展到填充其所在的分区。给 growfs 提供一个参数,即文件系统的设备节点。你也可以使用标签。

# growfs /dev/gpt/var
It's strongly recommended to make a backup before growing the file system.
OK to grow filesystem on /dev/gpt/var from 50.0GB to 100GB? [Yes/No] ➊yes
super-block backups (for fsck_ffs -b #) at:
 19233792, 20516032, 21798272, 23080512, 24362752,
--snip--

当 growfs(8) 请求确认 ➊ 时,你必须输入完整的 yes。任何其他回答,包括许多程序接受的简单 y,都会取消操作。确认操作后,growfs(8) 将根据需要添加额外的块、超级块和 inode 来填充分区。

如果你不想让文件系统填满整个分区,你可以使用 -s 指定一个大小。在这里,我将这个分区扩展到 80GB。

# growfs -s 80g /dev/gpt/var

我强烈建议你将文件系统的大小设置为与底层分区相同,除非你想让你的同事打你一巴掌。^(2)

UFS 快照

你可以在某一时刻对 UFS 文件系统进行快照;这被称为 快照。你可以对文件系统进行快照,删除并更改一些文件,然后从快照中复制未更改的文件。像 dump(8) 这样的工具使用快照来确保备份的一致性。UFS 快照不如 ZFS 快照那样强大或灵活,但在其限制范围内,它们是一个稳定、可靠的工具。

UFS 快照需要软更新,但与软更新日志不兼容。每个文件系统最多可以有 20 个快照。

快照让你访问编辑或删除的文件的旧版本。通过将文件作为内存设备挂载,你可以访问快照的内容。我将在第十三章中讨论内存设备。

拍摄和销毁快照

使用 mksnap_ffs(8) 创建快照。该程序假设你想创建当前工作目录所在文件系统的快照。将快照位置作为参数传入。传统上,快照会存放在文件系统根目录下的 .snap 目录中。如果你使用的是自动创建和删除快照的工具,比如 dump(8),可以在这里找到你的快照文件。不过,如果你不喜欢这个位置,你可以将它们放在任何你想要的位置。这里,我对 /home 文件系统创建了一个快照:

# cd /home
# mksnap_ffs .snap/beforeupgrade

快照会占用磁盘空间。你不能对一个已满的文件系统进行快照。

快照其实就是文件。删除文件就意味着删除快照。

查找快照

快照就是文件,你可以将它们放在文件系统的任何位置。这意味着很容易丢失它们。使用 find(1) 命令和 -flags snapshot 选项,可以在文件系统中查找所有快照。

# find /usr -flags snapshot
/usr/.snap/beforeupgrade
/usr/.snap/afterupgrade
/usr/local/testsnap

这是我的遗留快照!

快照磁盘使用情况

快照记录了当前文件系统与快照创建时文件系统之间的差异。每次在创建快照之后对文件系统进行的更改都会增加快照的大小。如果你删除了一个文件,快照会保留该文件的副本,以便你以后可以恢复它。

这意味着从包含快照的文件系统中删除数据并不会真正释放空间。如果你有一个 /home 分区的快照,并删除了一个文件,那么删除的文件会被添加到快照中。

确保使用快照的文件系统始终有足够的空闲空间。如果你尝试创建快照并且 mksnap_ffs(8) 报告因为没有足够空间而无法创建,你可能已经有了 20 个该文件系统的快照。

UFS 恢复与修复

从硬件故障到系统管理不当^(3)都可能损坏你的文件系统。UFS 的所有弹性技术旨在快速恢复数据完整性,但没有任何技术能完全保证完整性。

让我们来讨论 FreeBSD 如何保持每个 UFS 文件系统的整洁。

系统关机:同步器

当你关闭 FreeBSD 系统时,内核会将所有数据同步到硬盘,标记磁盘为干净状态,然后关机。这个过程是由一个名为 syncer 的内核进程完成的。在系统关机过程中,同步器会报告它在同步硬盘过程中的进度。

在关机过程中,你会看到同步器做出一些奇怪的操作。同步器会遍历需要同步到磁盘的 vnode 列表,使其能够支持所有文件系统,而不仅仅是 UFS。由于软更新的存在,写入一个 vnode 到磁盘可能会生成另一个脏 vnode,需进行更新。你会看到写入磁盘的缓冲区数量迅速从一个较高的值下降到一个较低的值,并且在系统真正同步硬盘时,可能会在零和一个较低的数字之间反复波动一两次。

如果同步器没有机会完成工作,或者由于你的笨拙操作同步器根本没有运行,那么你就会得到一个脏的文件系统。

脏文件系统

不,磁盘在使用时并不会变脏(尽管盘片上的灰尘会迅速损坏它,水也无济于事)。一个脏的 UFS 分区处于某种“中间状态”;操作系统已经请求写入数据到磁盘,但数据尚未完全写入物理介质。部分数据块可能已经写入,inode 可能已经编辑,但数据没有写入,或者两者的任意组合。实时文件系统几乎总是脏的。

如果主机的文件系统出现问题——例如由于系统崩溃或 Bert 绊倒电源线,系统再次启动时文件系统仍然是脏的。内核会拒绝挂载脏的文件系统。

清理文件系统可以恢复数据完整性,但并不一定意味着你所有的数据都已经写入磁盘。如果文件在系统崩溃时只写了一半,那么这部分文件就丢失了。没有什么可以恢复文件丢失的那一半,而磁盘上剩下的那一半基本上也没有用处。

日志化文件系统应该在 FreeBSD 尝试挂载它时自动恢复。如果文件系统无法恢复,或者你没有日志文件,你将需要使用传奇的 fsck(8)。

文件系统检查:fsck(8)

fsck(8)程序检查 UFS 文件系统,并尝试验证每个文件是否附加到正确的 inode 并且位于正确的目录。这就像验证数据库的引用完整性。如果文件系统只遭受了轻微损坏,fsck(8)可以自动恢复完整性并将文件系统重新投入使用。

修复一个损坏的文件系统需要时间和内存。一个 fsck(8)操作需要大约 700MB 的内存来分析一个 1TB 的文件系统。大多数计算机系统都有相对比例的内存和存储系统:很少有主机拥有 512MB 内存和 PB 级别的磁盘。但是你应该知道,有可能创建一个 UFS 文件系统大到系统没有足够的内存来修复它。

手动 fsck 操作

有时,这个自动的重启时 fsck 检查会失败。当你检查控制台时,你会看到一个单用户模式提示符,并且系统要求你手动运行 fsck(8)。

先用fsck -p修整文件系统。这个命令会自动修复一些较轻微的错误,不会询问你的确认。修整操作通常不会造成数据丢失,虽然偶尔会发生。这通常会成功,但如果不行,系统会要求你运行“完全 fsck”。

如果你在命令行中输入fsck,fsck(8)会验证磁盘上的每一个块和 inode。它会找到任何与其 inode 脱节的块,并猜测它们如何拼接以及应该如何附加。然而,fsck(8)可能无法识别这些文件属于哪个目录。

然后,fsck(8)会询问你是否要执行这些重新附加操作。如果你回答n,它会删除损坏的文件。如果你回答y,它会将丢失的文件添加到分区根目录下的lost+found目录中,文件名为一个数字。例如,你的/usr分区上的lost+found目录是/usr/lost+found。如果只有几个文件,你可以手动识别它们;如果有很多文件并且你在寻找特定的文件,像 file(1)和 grep(1)这样的工具可以帮助你按内容识别它们。

如果你回答n,那些未知数据将从文件系统中脱离。文件系统会保持脏状态,直到你通过其他方式修复它们。

信任 fsck(8)

如果 fsck(8)无法弄清楚文件该去哪儿……你能吗?如果不能,你别无选择,只能信任 fsck(8)来恢复你的系统或从备份中恢复。

完整的 fsck(8)运行会检查每一个块、inode 和超级块,识别出每一个不一致之处。它会要求你输入yn来批准或拒绝每一个修正。任何你拒绝的更改,你必须通过其他方式自行修复。你可能会花上几个小时在控制台上不断输入yyy

所以我再问一遍:如果 fsck(8)无法修复问题,你能吗?

如果你不能,考虑使用fsck -y-y标志告诉 fsck(8)尽可能自动重组这些文件,而无需你确认。它假设你会对所有问题回答“是”,即使是那些非常危险的问题。使用-y会自动触发-R,这告诉 fsck(8)重新尝试清理每个文件系统,直到成功或连续 10 次失败为止。这是治愈还是毁灭。你备份吧?

危险!

运行fsck -y并不能保证安全。有时,在运行-current 或做其他愚蠢的事情时,我曾让fsck -y将整个文件系统的内容迁移到lost+found。那时恢复就变得很困难。话虽如此,在运行 FreeBSD-stable 和标准 UFS 文件系统的生产系统中,我从未遇到过问题。

你可以设置系统在启动时自动尝试fsck -y。然而,我不推荐这样做,因为如果我的文件系统有一丝机会进入数字涅槃,我希望知道。我要亲自输入这条命令,并感受听到磁盘转动时的忐忑不安。此外,发现你的系统被搞砸了却完全不知道是怎么回事,总是让人不愉快。如果你比我勇敢,可以在rc.conf中设置fsck_y_enable="YES"

避免使用 fsck -y

如果你不想使用fsck -y,你还有哪些选择?好吧,fsdb(8)和 clri(8)允许你调试文件系统,并将文件重定向到它们的正确位置。你可以将文件恢复到正确的目录和名称。然而,这很困难,^(4),因此只推荐给秘密忍者文件系统大师使用。

后台 fsck

后台 fsck 为 UFS 提供了一些类似日志文件系统的好处,而无需实际使用日志记录。你必须在没有日志记录的情况下使用软更新才能使用后台 fsck。(有日志记录的软更新远远优于后台 fsck。)当 FreeBSD 看到重启后后台 fsck 正在进行时,它会将脏磁盘挂载为读写模式。当服务器运行时,fsck(8) 会在后台运行,识别文件的松散部分并在幕后整理它们。

后台 fsck 实际上有两个主要阶段。当 FreeBSD 在初始启动过程中发现脏磁盘时,它会对磁盘进行初步的 fsck(8) 评估。fsck(8) 程序会决定是否可以在系统运行时修复损坏,或者是否需要进行完整的单用户模式 fsck 运行。通常情况下,fsck 认为可以继续,并允许系统启动。系统进入单用户模式后,后台 fsck 会以低优先级运行,逐个检查分区。fsck 过程的结果会出现在 /var/log/messages 中。

你可以预期在后台 fsck 期间,任何需要磁盘活动的应用程序的性能都会很差。fsck(8) 程序占用了磁盘大部分的活动空间。虽然你的系统可能会变慢,但至少它会保持运行。

必须 在后台 fsck 后检查 /var/log/messages 以查找错误。初步的 fsck 评估可能会出错,也许确实需要对分区进行完整的单用户模式 fsck。如果你发现这样的消息,请在几小时内安排停机时间来修复问题。尽管不方便,但安排定期停机比由于停电导致的非计划停机以及由此产生的单用户模式 fsck -y 要好。

强制读写挂载脏磁盘

如果你真的想强制 FreeBSD 在不使用后台 fsck 的情况下将脏磁盘挂载为读写模式,你是可以这么做的。你不会喜欢结果的,真的不会。正如在 mount(8) 中所描述的那样,某些读者会认为这是个好主意,除非他们知道为什么。使用 -w(读写)和 -f(强制)标志来挂载(8)。

挂载一个脏分区进行读写会损坏数据。注意这句话中没有出现像 可能也许 这样的词。还要注意我没有使用 可恢复。挂载脏文件系统可能会让你的计算机崩溃。它可能会摧毁分区上剩余的所有数据,甚至可能彻底破坏底层文件系统。强制挂载脏文件系统进行读写是非常危险的。不要这么做。

后台 fsck,fsck -y,前台 fsck,哎呀!

所有这些不同的 fsck(8) 问题和情况可能会发生,但 FreeBSD 何时使用每个命令呢?FreeBSD 使用以下条件来决定何时以及如何对文件系统执行 fsck(8):

  • 如果文件系统是干净的,它会在没有 fsck(8) 的情况下挂载。

  • 如果一个日志文件系统在启动时是脏的,FreeBSD 会从日志中恢复数据并继续启动。日志文件系统很少需要 fsck(8)。

  • 如果一个没有软更新的文件系统在启动时处于脏状态,FreeBSD 会对其运行 fsck(8)。如果文件系统损坏严重,FreeBSD 会停止检查并要求你介入。你可以选择运行 fsck -y 或手动批准每个修复。

  • 如果一个启用了软更新的文件系统在启动时处于脏状态,FreeBSD 会执行一个非常基础的 fsck(8) 检查。如果损坏较轻,FreeBSD 可以在多用户模式下使用后台 fsck(8)。

  • 如果损坏严重,或者你不希望后台运行 fsck(8),FreeBSD 会中断启动并请求手动运行 fsck(8)。

在配置你的 UFS 文件系统时,考虑恢复路径。

UFS 空间保留

一个 UFS 文件系统的大小永远不会如你预期的那样大。UFS 会预留 8% 的文件系统空间用于即时优化。只有 root 用户可以写入超过这个限制的空间。这就是为什么一个文件系统似乎使用了超过 100% 可用空间的原因。为什么是 8%?这个数字来源于多年的经验和实际测试。在大多数文件系统上,8% 的预留空间不会造成太大问题,但随着文件系统的增大,这个数值会变得相当可观。在一个 1PB 的磁盘阵列上,UFS 会预留 80TB 空间。

UFS 的表现取决于文件系统的填充程度。在空文件系统上,它优化速度。一旦文件系统达到 92% 的容量(包括 8% 保留空间,占总大小的 85%),它会切换到优化空间利用率。大多数人也会做同样的事——一旦洗衣篮快满了,你还可以再塞些脏衣服进去,不过需要更多的时间和力气。UFS 会将文件碎片化,以更有效地利用空间。碎片会降低磁盘性能。随着空闲空间的减少,UFS 会越来越努力地提高空间利用率。一个满载的 UFS 文件系统运行速度大约是正常速度的三分之一。

你可能想使用 tunefs(8) 来减少 FreeBSD 保留的磁盘空间量。它的效果可能没有你想象的那么大。将保留空间减少到 5% 或更低,告诉 UFS 始终使用空间优化,并尽可能紧密地压缩文件系统。

增加保留空间百分比并不会提高性能。如果你增加保留空间百分比,导致文件系统显示已满,普通用户将无法写入文件。^(5)

保留空间可能会干扰像 NFS 这样的工具。某些可以通过 NFS 挂载 UFS 的操作系统会发现文件系统已满,并告知用户无法写入文件,尽管本地客户端可以写入文件。在故障排除时要记住这一点。

最好的做法是避免你的分区被填满。

分区有多满?

要查看每个 UFS 分区剩余的空间,使用 df(1) 命令。它会列出你系统中的分区、每个分区使用的空间以及挂载点。(不要在 ZFS 上使用 df(1);我们将在下一章讨论原因。)

$BLOCKSIZE

关于 FreeBSD 的磁盘工具(包括 df(1))有一个让人烦恼的地方,那就是它们默认以 512 字节块的形式提供信息。块在使用 512 字节物理块的微小磁盘上是没问题的,但今天它并不是一个有用的度量单位。环境变量 $BLOCKSIZE 控制 df(1) 输出的单位。默认的 .cshrc.profile 文件将 $BLOCKSIZE 设置为 1KB,这使得 df(1) 显示千字节而不是块。

-h-H 标志告诉 df(1) 生成人类可读的输出,而不是使用块。小的 -h 使用基 2 来生成 1,024 字节的兆字节,而大的 -H 使用基 10 来生成 1,000 字节的兆字节。通常,网络管理员和磁盘制造商使用基 10,而系统管理员使用基 2。只要你知道你选择的是哪一个,任意一种都可以。我是网络管理员,所以你得忍受我在这些示例中的偏见,尽管我的技术编辑不这么认为。

   # df -H
➊ Filesystem      Size    Used   Avail Capacity  Mounted on
➋ /dev/gpt/root   1.0G    171M    785M      18%  /
   devfs           1.0k    1.0k      0B     100%  /dev
   /dev/gpt/var    1.0G     64M    892M       7%  /var
   /dev/gpt/tmp    1.0G    8.5M    948M       1%  /tmp
➌ /dev/gpt/usr     14G   13.8G    203M      98%  /usr

第一行向我们展示了列标题➊,包括提供者名称、分区大小、已用空间、可用空间、已用空间的百分比以及挂载点。我们可以看到,标记为 /dev/gpt/root ➋ 的分区只有 1GB 大,但仅使用了 171MB,剩下 785MB 可用。它已满 18%,并挂载在 / 上。

如果你的系统像我的一样,磁盘使用情况似乎总是在没有明显原因的情况下不断增加。看看这里的 /usr 分区 ➌。它已满 98%。你可以使用 ls -l 来识别单个大文件,但在系统的每个目录中递归地执行这个操作是不可行的。

du(1) 程序在单一目录中显示磁盘使用情况。它的初始输出令人畏惧,可能会吓跑没有经验的用户。在这里,我们使用 du(1) 来找出占用我主目录所有空间的原因:

# cd $HOME
# du
1       ./bin/RCS
21459   ./bin/wp/shbin10
53202   ./bin/wp
53336   ./bin
5       ./.kde/share/applnk/staroffice_52
6       ./.kde/share/applnk
--snip--

这会一直显示下去,列出每个子目录并给出其以块为单位的大小。每个子目录的总和都会显示——例如,$HOME/bin 的内容总共占用了 53,336 块,约为 53MB。我可以坐着让 du(1) 列出每个目录和子目录,但那样我将不得不浏览比我真正需要的更多的信息。而且,块这种单位并不是特别方便,尤其是当它们被左对齐打印时。

让我们整理一下这个输出。首先,du(1) 支持类似于 df 的 -h 标志。此外,我不需要查看每个子目录的递归内容。我们可以通过 du 的 -d 标志来控制显示的目录数量。这个标志接受一个参数,即你想要明确列出的目录数量。例如,-d0 只深入一层目录,并给出该目录中文件的简单小计。

# du -h -d0 $HOME
 14G    /home/mwlucas

我的主目录里有 14GB 的数据?让我们深入一层,找出最大的子目录。

# du -h -d1
 38K    ./bin
 56M    ./mibs
--snip--
 13G    ./startrekgifs
--snip--

显然,我必须去别处寻找存储空间,因为我主目录中的数据太重要,不能删除。也许我应该在这个主机下扩展虚拟磁盘。

如果你不太依赖于-h标志,可以使用 sort(1)来查找最大目录,命令如下:du -kxd 1 | sort -n

添加新的 UFS 存储

无论你做多少规划,最终你的硬盘空间都会填满。你将需要添加新磁盘。在你能使用新硬盘之前,必须对其进行分区、创建文件系统、挂载这些文件系统,并将数据迁移到它们上面。

和初次安装时一样,给你新的磁盘分区和文件系统设计足够的思考。安装时正确分区要比安装后再去重新分区(带有数据的)容易得多。

备份,备份,再备份!

在进行任何磁盘操作之前,务必确保你有完整的备份。一个粗心的错误就可能摧毁你的系统!虽然你不常计划重格式化根文件系统,但如果发生这种情况,你希望能够快速恢复。

磁盘分区

虽然你可以随意分区磁盘,但我建议新磁盘使用与主机其他磁盘相同的分区方案。一个磁盘使用 MBR 而另一个使用 GPT 会很麻烦。这个例子中我将使用 GPT。

决定如何划分磁盘。这是一个 1TB 的磁盘。100GB 将分配给扩展后的/tmp。我会为我的新数据库分区分配 500GB。剩余的空间将划分并标记为emergency。我不会在这个空间上创建文件系统;它是为了防止需要做完全内存转储或存放某些文件时用的。我将它放在数据库分区旁边,以便需要时可以扩展数据库分区。我可以选择不对紧急空间进行分区,但我希望它有一个 GPT 标签,这样我的同事系统管理员就知道这个空闲空间不是意外的。

从销毁磁盘上的任何分区方案开始,并创建一个 GPT 方案。

# gpart destroy -F da3
da3 destroyed
# gpart create -s gpt da3
da3 created

现在创建你的 100GB /tmp和 500GB 数据分区,并将剩余空间放入紧急分区。

# gpart add -t freebsd-ufs -l tmp -s 100g da3
da3p1 added
# gpart add -t freebsd-ufs -l postgres -s 500g da3
da3p2 added
# gpart add -t freebsd-ufs -l emergency da3
da3p3 added

使用gpart show检查你的工作。

# gpart show -lp da3
=>        40  1953525088    da3  GPT  (932G)
          40   209715200  da3p1  tmp  (100G)
   209715240  1048576000  da3p2  postgres  (500G)
  1258291240   695233888  da3p3  emergency  (332G)

在每个分区上创建文件系统。

# newfs -j /dev/gpt/tmp
# newfs -j /dev/gpt/postgres

由于/tmp在每次启动时都会被清空,我更倾向于不在/tmp上使用软更新日志。相反,我会将/tmp挂载为异步,并在启动时运行newfs /dev/gpt/tmp。很多时候,newfs(8)比 rm(1)更快。

配置/etc/fstab

现在,告诉/etc/fstab关于你的文件系统。我们在第十章中讨论了/etc/fstab的格式。

/dev/gpt/postgres  /usr/local/etc/postgres ufs  rw  0  2
/dev/gpt/tmp       /tmp                    ufs  rw  0  2

FreeBSD 将在启动时识别这些文件系统,或者你可以在命令行上挂载这些新分区。不过,暂时不要重启或挂载分区。首先,你需要将文件迁移到这些文件系统中。

将现有文件安装到新磁盘

很可能你打算让新的磁盘替换或细分现有分区。你需要将新分区挂载到临时挂载点,将文件移动到新磁盘,然后在所需位置重新挂载分区。虽然/tmp没有任何文件,但如果我们正在安装新的数据库文件系统,我们大概有数据库文件需要放在那里。

在移动文件之前,关闭任何正在使用它们的进程。你不能成功地复制正在被更改的文件。如果你要移动数据库文件,请关闭数据库。如果你要移动邮件队列,请关闭所有邮件程序。这也是我推荐在单用户模式下进行所有新磁盘安装的主要原因之一。

现在将你的新分区挂载到一个临时挂载点。这正是/mnt的用途所在。

# mount /dev/gpt/postgres /mnt

现在你必须将文件从当前位置移动到新磁盘,而不更改它们的权限。这用 tar(1)非常简单。你可以将现有数据打包到磁带或文件中,并在新位置解包,但这有点笨重。通过将一个tar管道传递给另一个来避免中间步骤。

# tar cfC - /old/directory . | tar xpfC - /tempmount

如果你在派对上不讲 Unix,这看起来相当惊人。让我们拆解一下。首先,我们进入旧目录并将所有内容打包(tar)。然后,将输出通过管道传输到第二个命令,该命令在新目录中提取备份。当此命令完成时,你的文件将安装到新的磁盘上。例如,要将/usr/local/etc/postgres移动到临时挂载在/mnt的新分区,你可以执行以下操作:

# tar cfC - /usr/local/etc/postgres . | tar xpfC - /mnt

检查临时挂载点,确保你的文件确实在那里。一旦你确认文件已正确移动,删除旧目录中的文件,并将磁盘挂载到新位置。例如,在将文件从/usr/local/etc/postgres复制后,你将运行:

# rm -rf /usr/local/etc/postgres
# umount /mnt
# mount /usr/local/etc/postgres

现在你可以恢复正常操作了。我建议重启以验证一切是否按照你的预期恢复。

可堆叠挂载

也许你不关心你的旧数据;你只想分割现有的文件系统以获得更多空间,并打算从备份中恢复数据。那也没问题。所有 FreeBSD 文件系统都是可堆叠的。这是一个高级概念,日常系统管理中并不特别有用,但当你尝试将一个分区分割成两个时,它可能会让你措手不及。

假设,例如,你的数据在/usr/src中。查看磁盘上已使用的空间,然后在/usr/src上挂载一个新的空分区。如果你之后查看该目录,你会发现它是空的。

问题在于:旧的文件系统仍然保留着原始数据。新的文件系统被挂载在“上方”,因此你只看到新的文件系统。旧的文件系统在你移动数据之前没有更多的可用空间。如果你卸载新的文件系统并再次检查目录,你会惊讶地发现数据奇迹般恢复!新的文件系统遮盖了下层的文件系统。

虽然你看不见数据,但旧文件系统上的数据仍然占用空间。如果你添加了一个文件系统来获取空间,并且将一个新文件系统挂载到旧文件系统的某一部分上,你将无法释放原始文件系统上的空间。这个教训是:即使你正在从备份中恢复数据,也要确保将原始磁盘上的数据移除,以便回收磁盘空间。

既然你已经掌握了 UFS,让我们来探索一下 ZFS。

第十二章:Z 文件系统**

image

大多数文件系统在计算机术语中都是古老的。我们因为硬件速度慢而丢弃 5 年前的设备,但我们却用一个 40 年历史的文件系统来格式化新硬盘。虽然我们已改进了这些文件系统并使其更加稳健,但它们仍然使用相同的基本架构。每当文件系统出问题时,我们都会咒骂并急于修复,同时迫切希望有更好的选择。

ZFS 是更好的选择。

并不是说 ZFS 使用了革命性的技术。ZFS 的所有个别组件都已经被很好地理解。哈希、数据树和索引都没有什么神秘之处。但 ZFS 将所有这些经过验证的原理结合成一个统一、良好工程化的整体。它的设计考虑了未来。今天的哈希算法可能无法满足 15 年后的需求,但 ZFS 的设计使得可以在不失去向后兼容性的前提下,为新版本添加新的算法和技术。

本章不会涵盖关于 ZFS 的所有知识。ZFS 几乎是一个独立的操作系统,或者说是一个专用数据库。已经有整本书专门讲解如何使用和管理 ZFS。不过,你会学到足够的 ZFS 工作原理,以便在服务器上使用它,并理解它最重要的特性。

虽然 ZFS 预计是直接安装在磁盘分区上的,但你也可以使用其他 GEOM 提供者作为 ZFS 存储。最常见的例子是,当你进行加密磁盘的安装时,FreeBSD 会在磁盘上放置一个 geli(8) geom 并在该 geom 上安装 ZFS。本章中,将任何存储提供者都称作“磁盘”,即使它可能是一个文件、加密提供者或其他任何东西。

如果你以前从未使用过 ZFS,可以在虚拟机上安装一个基于 ZFS 的 FreeBSD 系统并跟着操作。安装程序会自动处理一些前提条件,比如在 loader.conf 中设置 zfs_load=YES,并在 rc.local 中设置 zfs_enable=YES;你只需要关注文件系统即可。

ZFS 代表什么?

Z 文件系统。没错,真的是这样。曾几何时,它代表着泽塔字节文件系统,但这个缩写已经被重新定义了。

ZFS 将一大堆已被理解的技术融合成一个组合式卷管理器和文件系统。它期望处理从文件权限到跟踪哪些存储提供者上的哪些块存储哪些信息的一切。作为系统管理员,你只需告诉 ZFS 你拥有的硬件以及如何配置它,然后 ZFS 会继续处理其余的部分。

ZFS 有三个主要组件:数据集、池和虚拟设备。

数据集

数据集被定义为一块有名称的 ZFS 数据。最常见的数据集类似于分区文件系统,但 ZFS 也支持其他类型的数据集,用于其他用途。快照(参见“快照”章节,见第 271 页)就是一种数据集。ZFS 还包括用于虚拟化和 iSCSI 目标、克隆等的块设备;所有这些都是数据集。本书主要集中在文件系统数据集上。传统文件系统,如 UFS,拥有多种小程序来管理文件系统,但你可以使用 zfs(8)管理所有 ZFS 数据集。

使用zfs list查看现有的数据集。输出看起来很像 mount(8)的结果。

   # zfs list
   NAME                     USED  AVAIL  REFER  MOUNTPOINT
➊ zroot                   4.71G   894G    88K  none
➋ zroot/ROOT              2.40G   894G    88K  none
➌ zroot/ROOT/2018-11-17      8K   894G  1.51G  /
➍ zroot/ROOT/default      2.40G   894G  1.57G  /
➎ zroot/usr               1.95G   894G    88K  /usr
➏ zroot/usr/home           520K   894G   520K  /usr/home
   --snip--

每一行以数据集名称开始,后面跟着该数据集所在的存储池或zpool。第一个条目是zroot ➊。这个条目代表池的根数据集。数据集树的其余部分悬挂在此数据集下。

接下来的两列显示了已使用空间和可用空间的数量。zroot池已使用 4.71GB,并且有 894GB 的可用空间。虽然可用空间的数值显然是正确的,但 4.71GB 的情况比看起来更复杂。一个数据集在“USED”列下显示的空间包含了该数据集以及所有子数据集的空间。一个根数据集的子数据集包括该 zpool 中的所有其他数据集。

REFER列是 ZFS 特有的。该列显示此特定数据集上可以访问的数据量,这不一定等同于已使用的空间量。一些 ZFS 功能,如快照,会在它们之间共享数据。这个数据集已经使用了 4.71GB 的数据,但只引用了 88KB。没有它的子数据集,这个数据集只有 88KB 的数据。

最后,我们看到了数据集的挂载点。这个根数据集没有挂载点;它没有被挂载。

看下一个数据集,zroot/ROOT ➋。这是为根目录及其相关文件创建的数据集。听起来很合理,但如果查看REFER列,你会发现它只包含 88KB 的数据,并且没有挂载点。根目录难道不应该存在吗?

接下来的两行解释了为什么...有点。数据集zroot/ROOT/2018-11-17 ➌的挂载点是/,所以它是一个真实的根目录。下一个数据集,zroot/ROOT/default ➍,也有一个挂载点/。不,ZFS 不允许你将多个数据集挂载到同一个挂载点。一个 ZFS 数据集记录了其设置的许多内容,而挂载点只是其中之一。

稍微考虑一下这四个数据集。zroot/ROOT数据集是zroot数据集的子集。zroot/ROOT/2018-11-17zroot/ROOT/default数据集是zroot/ROOT的子集。每个数据集的空间使用都算在它的子数据集上。

为什么要这么做?当你启动一个 FreeBSD ZFS 主机时,你可以轻松选择多个根目录。每个可启动的根目录称为 启动环境。假设你应用了一个补丁并重启系统,但新系统无法启动。通过启动到一个备用的启动环境,你可以轻松访问故障的根目录并尝试找出问题所在。

下一个数据集,zroot/usr ➎,是 zroot 的一个完全不同的子集。它有自己的子集,zroot/usr/home ➏。zroot/usr/home 中使用的空间会计入 zroot/usr,而两者都会计入它的父集,但它们的分配不会影响 zroot/ROOT

数据集属性

除了一些会计技巧外,数据集目前看起来与分区非常相似。但分区是磁盘的逻辑划分,填充存储设备上的非常特定的 LBA(逻辑块地址)。分区对分区上的数据没有感知。改变分区意味着摧毁其上的文件系统。

ZFS 紧密集成了文件系统和底层存储层。它可以根据需要动态地在各个文件系统之间划分存储空间。当分区通过控制可用块数来约束磁盘使用时,数据集可以使用配额来实现相同的效果。不过,如果没有这些配额,只要池中有空间,就可以使用它。

数据集可以使用的空间是一个 ZFS 属性。ZFS 支持数十种属性,从控制数据集增长大小的 quotas 属性,到显示数据集是否挂载的 mounted 属性。

查看和更改数据集属性

使用 zfs set 来更改属性。

# zfs set quota=2G zroot/usr/home

使用 zfs get 查看属性。你可以指定某个特定的属性,或者使用 all 查看所有属性。你可以通过逗号分隔列出多个属性。如果指定了数据集名称,则只影响该数据集。

# zfs get mounted zroot/ROOT
NAME       PROPERTY  VALUE    SOURCE
zroot/ROOT  mounted     no    -

在这里,我们有数据集的名称、属性、属性值以及一个叫做 source 的东西。(我们将在 “属性继承” 中讨论那个东西,位于 第 261 页。)

我的真正问题是,哪个数据集被挂载为根目录?我可以检查两个挂载点为 / 的数据集,但当我有几十个启动环境时,这会让我抓狂。通过添加 -r 标志,可以检查数据集及其所有子数据集的属性。

# zfs get -r mounted zroot/ROOT
NAME                                   PROPERTY  VALUE    SOURCE
zroot/ROOT                              mounted     no    -
zroot/ROOT/2018-11-17                   mounted     no    -
zroot/ROOT/default                      mounted   ➊yes    -

在这三个数据集中,只有 zroot/ROOT/default ➊ 被挂载。那是我们的活动启动环境。

属性继承

许多属性是可继承的。你可以在父数据集上设置它们,它们会传递到子数据集。在像挂载点这样的属性上,继承是没有意义的,但对于某些更高级的功能来说,继承是合适的。虽然我们将在 “压缩” 中讨论 compression 属性的作用,位于 第 273 页,但我们将在此处以它作为继承的示例。

# zfs get compression
NAME                     PROPERTY     VALUE     SOURCE
zroot                    compression  lz4       local
zroot/ROOT               compression  lz4       inherited from zroot
zroot/ROOT/2018-11-17    compression  lz4       inherited from zroot
zroot/ROOT/default       compression  lz4       inherited from zroot
zroot/tmp                compression  lz4       inherited from zroot
--snip--

根数据集 zroot 的 compression 属性设置为 lz4。源是本地的,这意味着该属性是设置在此数据集上的。现在看一下 zroot/ROOTcompression 属性也是 lz4,但源是从 zroot 继承的。这个数据集继承了父数据集的这个属性设置。

管理数据集

ZFS 使用数据集的方式与传统文件系统使用分区的方式相似。使用 zfs(8) 管理数据集。你将需要创建、删除和重命名数据集。

创建数据集

使用 zfs create 创建数据集。通过指定池和数据集名称来创建一个文件系统数据集。在这里,我为我的包创建了一个新数据集。(请注意,这会破坏启动环境,正如我们将在本章稍后看到的那样。)

# zfs create zroot/usr/local

每个数据集必须有一个父数据集。默认的 FreeBSD 安装包含一个 zroot/usr 数据集,因此我可以创建一个 zroot/usr/local。我想为 /var/db/pkg 创建一个数据集,但虽然 FreeBSD 包含一个 zroot/var 数据集,却没有 zroot/var/db。我需要创建 zroot/var/db,然后创建 zroot/var/db/pkg

请注意,数据集是可以叠加的,就像 UFS 一样。如果我在 /usr/local 目录中有文件,并且我在该目录上创建了一个数据集,ZFS 将会将该数据集挂载到目录上。我将无法访问这些文件。你必须移动文件以复制现有的目录。

销毁和重命名数据集

我创建的那个新的 zroot/usr/local 数据集?它隐藏了我 /usr/local 目录中的内容。使用 zfs destroy 删除它,然后再试一次。

# zfs destroy zroot/usr/local

/usr/local 的内容重新出现了。或者,我可以选择重命名这个数据集,使用 zfs rename

# zfs rename zroot/usr/local zroot/usr/new-local

我喜欢启动环境,因此我打算保持 /usr/local 不变。不过,有时候你真的需要一个 /usr/local 数据集……

未挂载的父数据集

作为一个 Postgres 用户,我希望为我的 Postgres 数据创建一个单独的数据集。FreeBSD 的 Postgres 9.6 包使用 /var/db/pgsql/data96。没有 zroot/var 数据集,我无法创建这个数据集,而没有 zroot/var 又会破坏包的启动环境支持。该怎么办呢?

解决方案是为 /var/db 创建一个数据集,但不使用它,通过设置 canmount 数据集属性。这个属性控制是否可以挂载数据集。FreeBSD 正是因为这个原因使用了一个未挂载的数据集作为 /var。新数据集默认将 canmount 设置为 on,因此通常无需担心它。使用 -o 标志在创建数据集时设置属性。

# zfs create -o canmount=off zroot/var/db

/var/db 数据集存在,但无法挂载。检查你 /var/db 目录的内容,确认一切仍然存在。现在你可以为 /var/db/postgres 甚至 /var/db/pgsql/data96 创建数据集。

# zfs create zroot/var/db/postgres
# zfs create zroot/var/db/postgres/data96
# chown -R postgres:postgres /var/db/postgres

你有一个用于数据库的数据集,且仍然有 /var/db 目录中的文件,作为根数据集的一部分。现在初始化你新的 Postgres 数据库并开始使用吧!

在你探索 ZFS 的过程中,你会发现许多情况可能需要在数据集创建时设置属性,或者使用未挂载的父数据集。

将文件移动到新数据集

如果你需要为现有目录创建新的数据集,你需要将文件复制过去。我建议你创建一个稍有不同名称的数据集,将文件复制到该数据集,然后重命名目录,最后重命名数据集。在这里,我需要为/usr/local创建一个数据集,因此我使用了不同的名称创建它。

# zfs create zroot/usr/local/pgsql-new

使用 tar(1)命令复制文件,方式与为新的 UFS 分区复制文件时完全相同(参见第十一章)。

# tar cfC - /usr/local/pgsql . | tar xpfC - /usr/local/pgsql-new

完成后,将旧目录移开并重命名数据集。

# mv /usr/local/pgsql /usr/local/pgsql-old
# zfs rename zroot/usr/local/pgsql-new zroot/usr/local/pgsql

我的 Postgres 数据现在存放在自己的数据集中。

ZFS 池

ZFS 按池而不是按磁盘组织其底层存储。ZFS 存储池,或称zpool,是对底层存储设备的抽象,使你能够将物理介质与其上方的用户可见文件系统分离。

使用 zpool(8)查看和管理主机的 ZFS 池。在这里,我使用zpool list查看我一台主机的池。

# zpool list
NAME      SIZE  ALLOC   FREE  EXPANDSZ   FRAG    CAP  DEDUP  HEALTH  ALTROOT
zroot     928G  4.72G   923G         -     0%     0%  1.00x  ONLINE  -
jail      928G  2.70G   925G         -     0%     0%  1.00x  ONLINE  -
scratch   928G  5.94G   922G         -     0%     0%  1.00x  ONLINE  -

这个主机有三个池:zrootjailscratch。每个池都有自己的一行。

SIZE列显示了池的总容量。所有这些池的容量为 928GB。ALLOC列显示每个池已使用的空间,而FREE列则显示剩余空间。这些磁盘几乎是空的,这很合理,因为我大约三小时前才安装了这个主机。

EXPANDSZ列显示底层存储提供商是否有空闲空间。当一个池具有虚拟设备冗余(我们将在下一节讨论)时,你可以替换池中的单个存储设备,并使池变大。就像把 RAID 阵列中的 5TB 硬盘换成 10TB 硬盘,以扩展其容量。

FRAG列显示了此池的碎片化程度。你可能听说过,碎片化会降低性能。不过,ZFS 最小化了碎片化的影响。

CAP列显示可用空间的使用百分比。

DEDUP列显示此池是否使用去重。尽管许多人将去重作为 ZFS 的一大特色,但它的实际效用可能并不像你期望的那么大。

HEALTH列显示池是否运行正常,或者底层磁盘是否有问题。

池的详细信息

你可以通过运行zpool status获取更多关于池的信息,或者查看单个池的信息。如果省略池名称,你将看到所有池的信息。在这里,我检查了我的jail池的状态。

# zpool status jail
  pool: jail
 state: ONLINE
  scan: none requested
config:

        NAME               STATE     READ WRITE CKSUM
        jail               ONLINE       0     0     0
          mirror-0         ONLINE       0     0     0
            gpt/da2-jail   ONLINE       0     0     0
            gpt/ada2-jail  ONLINE       0     0     0

errors: No known data errors

我们从池名称开始。状态与HEALTH列类似;它显示池的任何问题。扫描字段展示了清理过程的信息(参见“池的完整性与修复”,见第 273 页)。

然后是池的配置。配置展示了池中虚拟设备的布局。当我们创建池时,我们会深入讨论这一点。

池属性

类似于数据集,zpool 也有属性来控制和显示池的设置。有些属性是纯粹的信息性属性,例如free属性,表示池中剩余的空间。你可以更改其他属性。

查看池属性

要查看池的所有属性,请使用zpool get。添加属性all可以查看每个属性。你也可以添加池名称,仅查看该池的属性。

# zpool get all zroot
NAME   PROPERTY       VALUE                          SOURCE
zroot  size           928G                           -
zroot  capacity       0%                             -
zroot  health         ONLINE                         -
zroot  guid           7955546176707282768            default
--snip--

一些信息会被提取到诸如zpool statuszpool list这样的命令中。你也可以通过使用属性名称查询所有池的单个属性。

# zpool get readonly
NAME     PROPERTY  VALUE   SOURCE
zroot    readonly  off     -
jail     readonly  off     -
scratch  readonly  off     -

与数据集属性不同,大多数池属性在创建或导入池时设置。

虚拟设备

虚拟设备(VDEV)是存储设备的集合。你可以将 VDEV 视为 RAID 容器:一个大的 RAID-5 向操作系统呈现为一个巨大的设备,尽管系统管理员知道它实际上是由多个较小的磁盘组成的。虚拟设备是 ZFS 魔法发生的地方。你可以为不同的冗余级别安排池,或者放弃冗余以最大化空间。

ZFS 的自动错误修正发生在 VDEV 级别。ZFS 中的一切,从 znodes(索引节点)到数据块,都会进行校验和以验证完整性。如果你的池具有足够的冗余,ZFS 会注意到数据已损坏,并从一个良好的副本中恢复。如果池缺乏冗余,ZFS 会通知你数据已损坏,你可以从备份中恢复。

一个 zpool 由一个或多个相同的 VDEV 组成。池将数据条带化到所有 VDEV 中,没有冗余。丢失一个 VDEV 意味着池丢失。如果你有一个包含大量磁盘的池,确保使用冗余 VDEV。

VDEV 类型和冗余

ZFS 支持几种不同类型的 VDEV,每种类型的冗余程度和样式有所不同。常见的镜像磁盘类型,其中每个磁盘复制另一个磁盘上的内容,是一种 VDEV 类型。没有冗余的磁盘堆是另一种 VDEV 类型。ZFS 还包括三种不同种类的基于奇偶校验的高级冗余,称为RAID-Z

在池中使用多个 VDEV 会创建类似于高级 RAID 阵列的系统。RAID-Z2 阵列看起来非常像 RAID-6,但一个具有两个 RAID-Z2 VDEV 的 ZFS 池类似于 RAID-60。镜像 VDEV 的工作方式类似于 RAID-1,但池中多个镜像的行为像 RAID-10。在这两种情况下,ZFS 将数据条带化到 VDEV 中,没有冗余。各个 VDEV 提供冗余。

小心选择你的 VDEV 类型。

条带化 VDEV

由单个磁盘组成的 VDEV 称为条带,没有冗余。丢失该磁盘意味着数据丢失。虽然一个池可以包含多个条带化 VDEV,但每个磁盘都是独立的 VDEV。类似于 RAID-0,丢失一个磁盘意味着整个池丢失。

镜像 VDEV

镜像 VDEV 在每个磁盘上存储 VDEV 数据的完整副本。你可以丢失 VDEV 中的所有磁盘,除了一个,仍然可以访问数据。一个镜像可以包含任意数量的磁盘。

ZFS 可以同时从所有镜像磁盘中读取数据,因此读取数据非常快速。然而,当你写入数据时,ZFS 必须同时将数据写入所有磁盘。直到最慢的磁盘完成写入,写入才算完成。写入性能会受到影响。

RAID-Z

RAID-Z 将数据和奇偶校验信息分布到所有磁盘中,类似于传统的 RAID。如果 RAID-Z 中的某个磁盘出现故障或开始提供损坏的数据,RAID-Z 会使用奇偶校验信息重新计算丢失的数据。RAID-Z VDEV 必须包含至少三个磁盘,并且能够承受任何单个磁盘的损坏。RAID-Z 有时也被称为RAID-Z1

你不能在 RAID-Z 中添加或移除磁盘。如果你创建了一个五盘的 RAID-Z,它将永远是一个五盘的 RAID-Z。不要以为你可以向 RAID-Z 中添加额外的磁盘来增加存储空间,你不能这样做。

如果你使用的是超过 2TB 的磁盘,在修复第一个磁盘时,第二个磁盘出现故障的可能性是非同小可的。对于大容量磁盘,你应该考虑使用 RAID-Z2。

RAID-Z2

RAID-Z2 将奇偶校验和数据分条存储在 VDEV 中的每个磁盘上,类似于 RAID-Z1,但将奇偶校验信息的数量加倍。这意味着 RAID-Z2 可以承受最多两个磁盘的损失。你不能在 RAID-Z2 中添加或移除磁盘。它的速度稍慢于 RAID-Z。

RAID-Z2 必须包含四个或更多的磁盘。

RAID-Z3

三重奇偶校验适用于最重要的数据,或者是那些有大量磁盘且没有时间浪费的系统管理员。你可以在 RAID-Z3 中丢失最多三个磁盘而不丢失数据。和其他 RAID-Z 一样,你不能在 RAID-Z3 中添加或移除磁盘。

RAID-Z3 必须包含五个或更多的磁盘。

日志和缓存 VDEVs

池可以通过专用的 VDEV 来提升性能。只有在性能问题需要时才调整或实现这些 VDEV;不要主动添加它们。^(1) 大多数人不需要它们,因此我不会详细讲解,但你应该知道它们的存在,以防万一你不走运。

分离意图日志(SLOGZIL) 是 ZFS 的文件系统日志。待写入的数据会被转存到 SLOG,然后以更合适的方式安排到主池中。每个池都会为 SLOG 分配一块磁盘空间,但你也可以使用一个独立的设备来作为 SLOG。如果你需要更快的写入速度,可以安装一个非常快速的磁盘并将其专门用作 SLOG。池会将所有初始写入操作转存到快速磁盘设备上,然后随着时间推移将这些写入操作迁移到较慢的介质中。一个专用的快速 SLOG 还会平滑突发的 I/O。

二级自适应替换缓存(L2ARC)类似于 SLOG,但用于读取。ZFS 将最近访问和最常访问的数据保存在内存中。通过添加一个非常快的设备作为 L2ARC,你可以扩大 ZFS 从缓存提供的数据量,而不是从慢磁盘读取。L2ARC 比内存慢,但比慢磁盘快。

RAID-Z 和池

你可以向池中添加 VDEV,但不能向 RAID-Z VDEV 添加磁盘。创建池之前,请考虑好你的存储需求和硬件配置。

假设你有一台可以容纳 20 个硬盘的服务器,但你只有 12 个硬盘。你用这 12 个硬盘创建了一个 RAID-Z2 VDEV,想着如果以后需要,可以再往池中添加更多硬盘。但你甚至还没安装完服务器,就已经失败了。

你可以向池中添加多个相同的 VDEV。如果你创建了一个 12 磁盘的 VDEV,而主机只能再容纳 8 个磁盘,那就无法创建第二个相同的 VDEV。一个 12 磁盘的 RAID-Z2 和一个 8 磁盘的 RAID-Z2 并不相同。你可以强制 ZFS 接受不同的 VDEV,但性能会受到影响。向池中添加 VDEV 是不可逆的操作。

提前规划。看看你的物理设备。决定如何扩展存储。这个 20 驱动的服务器可以使用两个 10 磁盘的 RAID-Z2 VDEV,或者一个 12 磁盘的池和一个独立的 8 磁盘池。不要让自己受阻。

一旦你知道了想要使用哪种 VDEV,就可以创建池了。

管理池

既然你已经理解了不同的 VDEV 类型并开始规划你的存储,我们来创建一些不同类型的 zpool。首先设置你的磁盘块大小。

ZFS 和磁盘块大小

第十章讲解了现代磁盘有两种不同的扇区大小,512 字节和 4KB。如果文件系统假设磁盘具有 4KB 的扇区,而实际上磁盘有 512 字节的扇区,你的性能将会急剧下降。ZFS 当然假设磁盘具有 512 字节的扇区。如果你的磁盘确实有 512 字节的扇区,那就没问题了。但如果你不确定物理扇区的大小,最好还是谨慎些,告诉 ZFS 使用 4KB 的扇区。通过ashift属性控制 ZFS 的磁盘扇区假设。ashift 为 9 时,ZFS 使用 512 字节的扇区,而 ashift 为 12 时表示 4KB 扇区。通过 sysctl vfs.zfs.min_auto_ashift来控制 ashift。

# sysctl vfs.zfs.min_auto_ashift=12

通过在/etc/sysctl.conf中设置它,使其永久生效。

在创建池之前,你必须设置 ashift。池创建后再设置 ashift 没有效果。

如果你不确定磁盘的扇区大小,可以使用 ashift 为 12。这也是 FreeBSD 安装程序所做的。你会损失一点性能,但如果在 4KB 磁盘上使用 ashift 为 9,会大幅度降低系统性能。

现在创建你的池。

创建和查看池

使用 zpool create命令创建一个池。

# zpool create poolname vdevtype disks...

如果命令成功执行,你不会收到任何输出。

在这里,我创建了一个名为 db 的池,使用镜像 VDEV 和两个带有 GPT 标签的分区:

# zpool create db mirror gpt/zfs3 gpt/zfs4

我们分配的结构会在池状态中反映出来。

# zpool status db
--snip--
config:

        NAME          STATE     READ WRITE CKSUM
        db            ONLINE       0     0     0
        ➊ mirror-0    ONLINE       0     0     0
          ➋ gpt/zfs3  ONLINE       0     0     0
          ➌ gpt/zfs4  ONLINE       0     0     0
--snip--

池 db 包含一个名为 mirror-0 ➊ 的 VDEV。它包括两个带有 GPT 标签的分区,/dev/gpt/zfs3 ➋ 和 /dev/gpt/zfs ➌。所有这些分区都在线。

如果你没有包括 VDEV 名称,zpool(8) 会创建一个没有冗余的条带池。在这里,我创建了一个名为 scratch 的条带池:

# zpool create scratch gpt/zfs3 gpt/zfs4

池状态显示每个 VDEV,名字来自底层磁盘。

--snip--
        NAME        STATE     READ WRITE CKSUM
        garbage     ONLINE       0     0     0
          gpt/zfs3  ONLINE       0     0     0
          gpt/zfs4  ONLINE       0     0     0
--snip--

创建任何类型的 RAID-Z 看起来都很像创建镜像。只需要使用正确的 VDEV 类型。

# zpool create db raidz gpt/zfs3 gpt/zfs4 gpt/zfs5

池状态与镜像的状态非常相似,但 VDEV 中有更多的磁盘。

多 VDEV 池

当你创建池时,关键字 mirrorraidzraidz2raidz3 都会告诉 zpool(8) 创建一个新的 VDEV。列在这些关键字后的任何磁盘都会用于创建新的 VDEV。要创建一个包含多个 VDEV 的池,你可以像这样操作:

# zpool create poolname vdevtype disks... vdevtype disks...

在这里,我创建了一个包含两个 RAID-Z VDEV 的池,每个 VDEV 包含三个磁盘:

# zpool create db raidz gpt/zfs3 gpt/zfs4 gpt/zfs5 raidz gpt/zfs6 gpt/zfs7 gpt/zfs8

对这个新池执行 zpool status 命令时,显示的信息会稍有不同。

--snip--
        NAME          STATE     READ WRITE CKSUM
        db            ONLINE       0     0     0
        ➊ raidz1-0    ONLINE       0     0     0
            gpt/zfs3  ONLINE       0     0     0
            gpt/zfs4  ONLINE       0     0     0
            gpt/zfs5  ONLINE       0     0     0
        ➋ raidz1-1    ONLINE       0     0     0
            gpt/zfs6  ONLINE       0     0     0
            gpt/zfs7  ONLINE       0     0     0
            gpt/zfs8  ONLINE       0     0     0
--snip--

这个池包含一个名为 raidz1-0 ➊ 的 VDEV,其中有三个磁盘。还有一个名为 raidz1-1 ➋ 的第二个 VDEV,里面也有三个磁盘。很明显,这些池是相同的。数据在两个 VDEV 之间进行条带化存储。

销毁池

要销毁一个池,使用 zpool destroy 和池的名称。

# zpool destroy db

注意,zpool 不会在销毁池之前询问你是否真的确定。是否确定要销毁池是你的问题,而不是 zpool(8) 的问题。

错误和 -f

如果你输入一个没有意义的命令,zpool(8) 会报错。

# zpool create db raidz gpt/zfs3 gpt/zfs4 gpt/zfs5 raidz gpt/zfs6 gpt/zfs7
invalid vdev specification
use '-f' to override the following errors:
mismatched replication level: both 3-way and 2-way raidz vdevs are present

阅读错误信息时你首先看到的是“使用 -f 来覆盖此错误。”许多系统管理员会将其理解为“-f 使这个问题消失。”然而,ZFS 实际上是在说:“你的命令行是个大错误。添加 -f 会做一些无法修复、对系统稳定性有害的事,而且你会后悔直到这个系统不再运行。”

大多数 zfs(8)zpool(8) 错误信息都有意义,但你需要仔细阅读。如果你不理解信息,可以查看 第一章 的故障排除说明。通常,重新检查你输入的内容会暴露问题所在。

在这个例子中,我让 zpool(8) 创建一个包含三个磁盘的 RAID-Z VDEV 和一个只包含两个磁盘的第二个 RAID-Z VDEV。我搞错了这个命令行。添加 -f 并继续将我的数据库安装到新的错误格式的 db 池中,只会确保我以后不得不重新创建这个池并重新安装数据库。^(2) 如果你发现自己处于这种情况,调查 zfs sendzfs recv

写时复制

在普通文件系统和 ZFS 中,文件以块的形式存在于磁盘上。当你在传统文件系统中编辑文件时,文件系统会取出块,修改它,并将其放回磁盘上的同一位置。写入过程中的系统故障可能会导致损坏的写入:文件的 50%是旧版本,50%是新版本,可能完全无法使用。

ZFS 永远不会覆盖文件中现有的块。当文件发生更改时,ZFS 会识别必须更改的块,并将其写入新的磁盘空间。旧版本会保持不变。这被称为写时复制(COW)。通过写时复制,短时间的写入可能会丢失文件的最新更改,但文件的先前版本会保持完好。

不会破坏文件是写时复制(COW)的一个很大优点,但 COW 也带来了其他可能性。元数据块也是写时复制,一直到形成 ZFS 池数据树根的uberblocks。ZFS 通过跟踪包含文件旧版本的块来创建快照。虽然这听起来很简单,但细节会让你迷失方向。

快照

快照是数据集在某一特定时刻的副本。快照是只读的,永远不会改变。你可以访问快照的内容来访问文件的旧版本或甚至已删除的文件。虽然快照是只读的,你可以将数据集恢复到快照状态。在升级系统之前先创建一个快照,如果升级出了严重问题,你可以回滚到快照。ZFS 使用快照提供许多功能,例如启动环境(见 启动环境 在 第 276 页)。最棒的是,根据你的数据,快照可能只占用极少的空间。

每个数据集都有一堆元数据,所有这些元数据都是从一个顶级块构建成树形结构的。当你创建快照时,ZFS 会复制这个顶级块。一个元数据块与数据集一起,而另一个与快照一起。数据集和快照共享数据集中的数据块。

删除、修改或覆盖活动数据集中的文件意味着为新数据分配新的块,并断开包含旧数据的块。然而,快照需要一些旧数据块。在丢弃旧块之前,ZFS 会检查是否有快照仍然需要该块。如果快照需要一个块,但数据集不再需要,ZFS 会保留该块。

因此,快照仅仅是记录在创建快照时数据集使用的那些块的列表。创建快照会告诉 ZFS 保留这些块,即使数据集不再需要这些块。

创建快照

使用 zfs snapshot 命令来创建快照。通过其完整路径指定数据集,然后添加 @ 和快照名称。我习惯性地根据创建快照的日期和时间命名快照,原因将在本章末尾变得清晰。

我即将对用户的主目录进行维护,删除一些旧文件以释放空间。我很确定有人会抱怨我删除了他们的文件^(3),所以在清理之前,我想先创建一个快照。

# zfs snapshot zroot/usr/home@2018-07-21-13:09:00

我没有收到任何反馈。发生了什么事吗?通过给 zfs list 添加 -t snapshot 参数来查看所有快照。

# zfs list -t snapshot
NAME                                    USED  AVAIL    REFER     MOUNTPOINT
zroot/usr/home@2018-07-21-13:09:00       ➊0     ➋-   ➌4.68G   ➍-

快照已经存在。USED 列显示它使用了零磁盘空间 ➊:它与它所在的数据集完全相同。由于快照是只读的,AVAIL 列显示的可用空间 ➋ 对它来说并不相关。REFER 列显示该快照占用了 4.68GB 的磁盘空间 ➌。如果你检查一下,你会发现这是 zroot/usr/home 的大小。最后,MOUNTPOINT 列显示该快照没有被挂载 ➍。

这是一个活跃的系统,其他人也已经登录。我等了一会儿,然后再次检查我的快照。

# zfs list -t snapshot
NAME                                    USED  AVAIL  REFER  MOUNTPOINT
zroot/usr/home@2018-07-21-13:09:00      ➊96K      -  4.68G  -

该快照现在使用了 96KB ➊。某个用户修改了数据集,快照因此占用了维持差异所需的空间。

现在我开始清理,删除我认为是垃圾的文件。

# zfs list -t snapshot
NAME                                    USED  AVAIL  REFER  MOUNTPOINT
zroot/usr/home@2018-07-21-13:09:00     1.62G      -  4.68G  -

现在这个快照使用了 1.62GB 的空间。这些是我已删除的文件,但它们仍然可以从快照中获取。我会保留这个快照一段时间,给用户一个投诉的机会。

访问快照

每个 ZFS 数据集的根目录中都有一个隐藏的 .zfs 目录。它不会出现在 ls(1) 中;你需要知道它的存在。该目录包含一个快照目录,其中包含一个以每个快照命名的子目录。快照的内容就在该目录中。

对于我们的快照 zroot/usr/home@2018-07-21-13:09:00,我们需要进入 /usr/home/.zfs/snapshot/2018-07-21-13:09:00。虽然 .zfs 目录在 ls(1) 中不会显示,但一旦进入该目录,ls(1) 就能正常工作。该目录包含了创建快照时文件的所有内容,即使我在创建该快照后删除或更改了这些文件。

从快照中恢复文件只需要将文件从快照复制到一个可读写的位置。

销毁快照

快照是一个数据集,就像一个文件系统风格的数据集一样。使用 zfs destroy 命令来删除它。

# zfs destroy zroot/usr/home@2017-07-21-13:09:00

现在,快照占用的空间可以用来存储更多的垃圾文件。

压缩

快照并不是 ZFS 节省空间的唯一方式。ZFS 使用实时压缩,透明地检查每个文件的内容,并在可能的情况下压缩其大小。使用 ZFS 时,你的程序不需要自己压缩日志文件:文件系统会实时为你完成压缩。虽然 FreeBSD 在安装时默认启用压缩,但如果你了解它是如何工作的,你会更有效地使用它。

压缩会改变系统性能,但可能不是你想象的方式。你需要 CPU 时间来压缩和解压缩数据,因为数据在磁盘之间进出。尽管如此,大多数磁盘请求通常比平常要小。你基本上是用处理器时间换取磁盘 I/O。我管理的每一台服务器,无论是裸机还是虚拟机,都拥有远远超过磁盘 I/O 的处理器能力,所以这是我愿意做出的交换。最终结果是,使用 ZFS 压缩通常会提高性能。

压缩在不同的数据集上表现不同。二进制文件本身已经压缩得非常紧凑;压缩/usr/bin几乎不会节省空间。然而,压缩/var/log通常会将文件大小减少六到七倍。检查属性compressratio以查看压缩效果如何缩小数据大小。我的主机写日志的频率远远高于写二进制文件。我会欣然接受这种常见任务的六倍性能提升。

ZFS 支持多种压缩算法,但默认使用lz4。lz4 算法的特别之处在于它能迅速识别无法压缩的文件。当你将二进制文件写入磁盘时,lz4 会检查它并说:“不,我帮不上忙,”然后立即停止尝试。这消除了无谓的 CPU 负载。然而,它会有效地压缩那些可以压缩的文件。

池完整性与修复

ZFS 池中的每一块数据都有一个与之关联的加密哈希,存储在其元数据中以验证数据的完整性。每次访问数据时,ZFS 会重新计算该数据中每个块的哈希值。当 ZFS 在具有冗余的池中发现数据损坏时,它会透明地修正这些数据并继续。如果 ZFS 在没有冗余的池中发现数据损坏,它会发出警告并拒绝提供该数据。如果你的池中已识别任何数据错误,它们会显示在zpool status中。

完整性验证

除了即时验证,ZFS 还可以显式地遍历整个文件系统树并验证池中的每个数据块。这叫做扫描。与 UFS 的 fsck(8)不同,扫描在池在线并且正在使用时进行。如果你之前运行过扫描,结果也会显示在池状态中。

  scan: scrub repaired 0 in 8h3m with 0 errors on Fri Jul 21 14:17:29 2017

要扫描池,运行zpool scrub并指定池名称。

# zpool scrub zroot

你可以使用zpool status查看扫描进度。

扫描池会降低其性能。如果你的系统已经接近极限,请仅在非高峰时段扫描池。你可以使用-s选项取消扫描^(4)。

# zpool scrub -s zroot

当负载下降时,再次运行扫描。

修复池

磁盘会发生故障。这就是它们的用途。冗余的意义在于你可以用正常工作的磁盘替换故障或完全坏掉的磁盘,并恢复冗余。

镜像和 RAID-Z 虚拟设备专门设计用来重建磁盘故障时丢失的数据。从这个角度看,它们与 RAID 非常相似。如果 ZFS 镜像中的一块磁盘故障,你只需更换故障磁盘,ZFS 会将存活的镜像数据复制到新磁盘上。如果 RAID-Z VDEV 中的一块磁盘故障,你只需更换损坏的磁盘,ZFS 会从奇偶校验数据中重建该磁盘上的数据。

在 ZFS 中,这种重建操作被称为resilvering。与其他 ZFS 完整性操作类似,resilvering 只会在活动文件系统上进行。Resilvering 不像从奇偶校验重建 RAID 磁盘那样,ZFS 利用其对文件系统的了解来优化重新填充替换设备的过程。当你更换故障设备时,resilvering 会自动开始。ZFS 会以低优先级进行 resilvering,以免干扰正常操作。

池状态

zpool status命令在STATE字段中显示底层存储硬件的健康状态。我们已经看到一些健康的池,接下来让我们看看一个不健康的池。

# zpool status db
  pool: db
 state: ➊DEGRADED
status: One or more devices could not be opened.  Sufficient replicas exist for
        the pool to continue functioning in a degraded state.
action: Attach the missing device and online it using 'zpool online'.
   see: http://illumos.org/msg/ZFS-8000-2Q
  scan: none requested
config:

        NAME                        STATE     READ WRITE CKSUM
        db                          DEGRADED     0     0     0
        ➋mirror-0                   DEGRADED     0     0     0
            gpt/zfs1                ONLINE       0     0     0
          ➌14398195156659397932   ➍UNAVAIL      0     0     0   ➎was /dev/gpt/zfs3

errors: No known data errors

池的状态是DEGRADED ➊。如果你进一步查看输出内容,会看到更多DEGRADED条目和一个UNAVAIL ➍。这究竟是什么意思?

池中的错误会向上蔓延。池状态是对池整体健康状况的总结。当池的虚拟设备mirror-0 ➋处于DEGRADED状态时,整个池会显示为DEGRADED。这个错误源于底层磁盘处于UNAVAIL状态。我们可以获取该磁盘的 ZFS GUID ➌,以及用于创建池的标签 ➎。

当底层设备出现故障时,ZFS 池会显示错误。当池的状态不是ONLINE时,需深入查看 VDEV 和磁盘列表,直到找到真正的问题。

池、VDEV 和磁盘可以有六种状态:

ONLINE 该设备正常运行。

DEGRADED 该池或 VDEV 至少有一个提供者丢失、离线或产生的错误超过了 ZFS 的容忍范围。冗余机制正在处理该错误,但你需要立即解决这个问题。

FAULTED 故障磁盘已损坏或产生的错误超过了 ZFS 的容忍范围。故障 VDEV 会采用最后一个已知的良好数据副本。一个由两块故障磁盘组成的两盘镜像会处于故障状态。

UNAVAIL ZFS 无法打开该磁盘。也许它已经被移除、关闭,或者那个不稳定的电缆终于坏了。磁盘不在,所以 ZFS 无法使用它。

OFFLINE 该设备已被故意关闭。

REMOVED 一些硬件可以检测到在系统运行时物理移除的驱动器,从而让 ZFS 设置 REMOVED 标志。当你将驱动器重新插入时,ZFS 会尝试重新激活该磁盘。

我们丢失的磁盘处于 UNAVAIL 状态。由于某种原因,ZFS 无法访问/dev/gpt/zfs3,但磁盘镜像仍然可以提供数据,因为它有一块工作中的磁盘。现在你需要跑来跑去,搞清楚那个磁盘去哪儿了。你如何管理 ZFS 取决于你发现了什么问题。

重新连接和断开驱动器

不可用的磁盘可能并没有坏掉,它们可能只是断开了连接。如果你晃动磁盘托盘并突然看到绿色指示灯,那说明磁盘本身没问题,但连接出现故障。你应该解决这个硬件问题,是的,但在此期间,你可以重新激活该磁盘。你也可以重新激活故意移除的磁盘。使用zpool online命令,输入池名称和缺失磁盘的 GUID 作为参数。如果我示例中的池中的磁盘只是断开了连接,我可以这样重新激活它:

# zpool online db 14398195156659397932

ZFS 会重新同步磁盘并恢复正常功能。

如果你想移除一个磁盘,可以告诉 ZFS 通过zpool offline将其下线。作为参数提供池名和磁盘名。

# zpool offline db gpt/zfs6

将磁盘下线、物理移动它们、重新上线并允许池进行重新同步,能够让你在不产生停机时间的情况下将大型存储阵列从一个 SAS 机架迁移到另一个。

更换磁盘

如果磁盘不仅仅是松动,而是完全坏掉了,你需要用新磁盘替换它。ZFS 允许以多种方式更换磁盘,但最常见的方式是使用zpool replace。作为参数,提供池名、故障磁盘和新磁盘。在这里,我将 db 池中的/dev/gpt/zfs3磁盘替换为/dev/gpt/zfs6

# zpool replace db gpt/zfs3 gpt/zfs6

该池将重新同步并恢复正常操作。

在大型存储阵列中,你还可以使用连续的zpool replace操作来清空一个磁盘架。只有当你所在组织的操作需求不允许将磁盘下线和重新上线时,才做这件事。

启动环境

ZFS 帮助我们应对系统管理员所做的最危险的事情之一。不是我们的饮食习惯,也不是缺乏运动。我指的是系统升级。当升级顺利时,大家都很高兴。当升级失败时,可能会毁掉你的工作日、周末甚至工作。没有人喜欢在关键任务软件因共享库的新版本而崩溃时进行备份恢复。没有人喜欢恢复备份。

通过启动环境的魔力,ZFS 利用快照功能,让你在系统升级后只需要重启就能回退。启动环境是根数据集的克隆。它包括内核、基础系统的用户空间、附加包以及核心系统数据库。在执行升级之前,创建一个启动环境。如果升级顺利进行,那么一切正常。如果升级出了问题,你可以重启进入启动环境。这将恢复服务,让你调查升级失败的原因并修复这些问题。

启动环境在主机需要独立启动池时无法使用。安装程序会为你处理启动池。它们在结合 UEFI 和 GELI 时出现,或者在使用 ZFS 时出现在 MBR 分区的磁盘上。

使用启动环境需要一个启动环境管理器。我推荐使用 beadm(8),它可以作为一个包安装。

# pkg install beadm

现在你可以使用启动环境了。

查看启动环境

每个引导环境都是 zroot/ROOT 下的一个数据集。刚安装 beadm 的系统应该只有一个引导环境。使用 beadm list 可以查看所有引导环境。

  # beadm list
  BE        Active    Mountpoint  Space   Created
 ➊default  ➋NR      ➌/          ➍2.4G   ➎2018-05-04 13:13

这台主机有一个引导环境,名为 default ➊,对应的数据集是 zroot/ROOT/default

Active 列 ➋ 显示这个引导环境是否正在使用中。N 表示该环境现在正在使用中。R 表示该环境将在重启后生效。当默认环境正在运行时,它们会一起出现。

Mountpoint 列 ➌ 显示了此引导环境的挂载点位置。大多数引导环境在未使用时不会挂载,但你可以使用 beadm(8) 挂载未使用的引导环境。

Space 列 ➍ 显示此引导环境使用的磁盘空间。它是基于一个快照构建的,因此该数据集中可能有比这个数量更多的数据。

Created 列 ➎ 显示了此引导环境的创建日期。在本例中,它是机器安装的日期。

在更改系统之前,创建一个新的引导环境。

创建和访问引导环境

每个引导环境都需要一个名称。我建议使用基于当前操作系统版本和补丁级别或日期的名称。像“beforeupgrade”和“dangitall”这样的名称,虽然当时有意义,但以后会让你感到困惑。

使用 beadm create 创建新的引导环境。在这里,我检查当前的 FreeBSD 版本,并用它来创建引导环境的名称:

# freebsd-version
11.0-RELEASE-p11
# beadm create 11.0-p11
Created successfully

现在我有两个相同的引导环境。

# beadm list
BE  a       Active Mountpoint  Space Created
default    NR     /           12.3G 2015-04-28 11:53
11.0-p11   -      -          236.0K 2018-07-21 14:57

你可能注意到,新创建的引导环境已经占用了 236KB。这是一个实时系统。在我创建引导环境和列出这些环境之间,文件系统或其元数据发生了变化。

Active 列显示我们当前正在使用默认的引导环境,并且下次启动时将使用该环境。如果我更改了已安装的软件包或升级了基础系统,这些更改将影响默认环境。

每个引导环境都可以作为 zroot/ROOT 下的一个快照使用。如果你想访问一个引导环境并进行读写操作,可以使用 beadm mount 将其临时挂载到 /tmp 下。使用 beadm umount 卸载这些环境。

激活引导环境

假设你升级了软件包,系统崩溃了。通过激活一个引导环境并重启,你可以恢复到早期的操作系统安装状态。使用 beadm activate 激活引导环境。

# beadm activate 11.0-p11
Activated successfully
# beadm list
BE         Active Mountpoint  Space Created
default    N      /           12.4G 2015-04-28 11:53
11.0-p11   R      -          161.8M 2018-07-21 14:57

默认的引导环境的 Active 标志被设置为 N,表示当前正在运行。11.0-p11 环境有 R 标志,因此重启后它将生效。

重启系统后,你会突然回到先前的操作系统安装状态,系统中的不稳定更改也不会被保留。与其从备份中恢复,这样更简单。

删除引导环境

在几次升级之后,你会发现你再也无法回到某些现有的启动环境。一旦我将这台主机升级到例如 12.2-RELEASE-p29,可能再也无法重启进入 11.0-p11。使用beadm destroy删除过时的启动环境并释放它们的磁盘空间。

# beadm destroy 11.0-p11
Are you sure you want to destroy '11.0-p11'?
This action cannot be undone (y/[n]): y
Destroyed successfully

当系统提示时,回答y,beadm 将删除该启动环境。

启动环境在启动时

所以,你真的搞砸了操作系统。忘了进入多用户模式吧,你甚至无法进入单用户模式,除非生成一堆奇怪的错误信息。你可以在加载器提示符下选择一个启动环境。这需要控制台访问,但任何其他自救方法也都需要控制台访问。

启动加载器菜单包括一个选项,可以选择启动环境。选择该选项后,你会看到一个新的菜单,列出了主机上所有启动环境的名称。选择你想要的启动环境并按下 ENTER 键。系统将进入该环境,给你一个机会来查明为什么一切都出错了。

启动环境与应用程序

升级失败并不足够糟糕,它可能还会带走你的应用程序数据。

大多数应用程序将它们的数据存储在根数据集的某个位置。MySQL 使用/var/db/mysql,而 Apache 使用/usr/local/www。这意味着回退到早期的启动环境时,可能会将你的应用程序数据一并回滚。根据你的应用程序,你可能希望或不希望发生这种回滚。

如果某个应用程序使用的数据不应该包含在启动环境中,你需要为该数据创建一个新的数据集。我在本章的 “未挂载的父数据集” 以及 第 262 页 中提供了一个示例。考虑你的应用程序的需求,并根据需要将数据分离。

虽然 ZFS 具有更多功能,但这篇文章涵盖了每个系统管理员必须知道的主题。你们中的许多人可能会发现克隆、委派或复制功能很有用。你也许会觉得 Allan Jude 和我自己所著的《FreeBSD Mastery: ZFS》(Tilted Windmill Press, 2015)和《FreeBSD Mastery: Advanced ZFS》(Tilted Windmill Press, 2016)这两本书很有帮助。你还会在互联网上找到很多关于这些主题的资源。

现在,让我们考虑一些 FreeBSD 管理员认为有用的其他文件系统。

第十三章:外部文件系统**

image

FreeBSD 支持多种除 ZFS 和 UFS 外的文件系统。你需要通过使用光盘、闪存驱动器等与其他主机互操作。此外,FreeBSD 使用特殊用途的文件系统 devfs(5) 来管理设备节点。Jail 用户可能需要进程文件系统 procfs(5)。对于不需要在重启后存活的超快速存储,你可以将系统内存用作文件系统。你可以通过网络挂载文件系统,使用 Unix 风格的网络文件系统(NFS)或 Microsoft 的常用互联网文件系统(CIFS)。不管你如何努力避免,有时候你还是不得不挂载 ISO 镜像。

使用这些命令需要对挂载文件系统有更深入的理解。

FreeBSD 挂载命令

我们在讨论 UFS 文件系统时曾提到过 mount(8),但你也将使用它来将其他文件系统附加到目录树上。mount(8) 命令假设任何本地分区都使用 UFS。如果你尝试挂载一个非 UFS 文件系统,将会遇到错误。

# mount /dev/cd0 /media
mount: /dev/cd0: Invalid argument

设备节点 /dev/cd0 代表一个光驱。我为了这个测试将光盘放入驱动器,所以应该能正常工作。然而,尝试挂载时却出现错误。要挂载 UFS 文件系统,你需要设备节点和挂载点。挂载外部文件系统时,必须使用 -t 参数指定文件系统类型。光盘使用 ISO 9660 文件系统,FreeBSD 称之为 cd9660。这里,我指定了挂载该光盘的文件系统,并将其挂载到 /cdrom 目录下:

# mount -t cd9660 /dev/cd0 /media

现在我可以进入 /media 目录查看内容。很简单,是吧?

许多文件系统都有其自定义的 mount(8) 命令变体。通过运行 apropos mount_ 可以获取完整的列表。是的,你需要在命令末尾加下划线;所有的 mount(8) 变体都使用下划线作为分隔符。你会找到 mount_cd9660(8)、mount_msdosfs(8)、mount_nfs(8) 等等。对于没有这种命令的文件系统,始终使用 mount -t

使用 -o 标志应用挂载选项。你需要查看每个挂载命令的 man 页面,了解文件系统支持哪些挂载选项。多个挂载选项用逗号分隔。这里,我将一个 FAT32 驱动器挂载到设备节点 /dev/da1,并以只读方式挂载,同时将所有者和组分配给用户 bert

# mount -t msdosfs -o ro,-gbert,-ubert /dev/da1 /media

你可以使用 umount(8) 卸载任何已挂载的文件系统:

# umount /media

umount(8) 命令不关心文件系统类型。它只是尝试将磁盘分区从文件系统中断开。然而,它确实关心是否有进程正在使用该文件系统,如果有进程在使用,umount(8) 会拒绝卸载该文件系统。如果你在文件系统中有一个空闲的终端并且显示 shell 提示符,umount(8) 会拒绝卸载该文件系统。

如果你经常连接和断开文件系统,建议研究一下 autofs(5) 来自动处理这些挂载。

支持的外部文件系统

下面是一些最常用的外部文件系统,以及每个文件系统的简要描述和适用的挂载命令。

FAT (MS-DOS)

FreeBSD 包含了广泛的 FAT 支持,FAT 是 DOS/Windows 9x 文件分配表文件系统,通常用于可移动媒体和一些双启动系统。此支持涵盖了 FAT12、FAT16 和 FAT32 类型。不过,你可以使用非 FAT 文件系统格式化 U 盘,因此不要盲目认为所有的 U 盘都使用 FAT。由于现在 U 盘最常见的用途是机器之间传输文件,因此大多数 U 盘都是 FAT32 格式。挂载类型为 msdosfsmount -t msdosfs)。

如果你处理大量的 FAT32 磁盘,建议调查一下 mtools 包,它是一组用于处理 FAT 文件系统的程序,提供比默认的 FreeBSD 工具更高的灵活性。

ISO 9660

ISO 9660 是 CD 的标准文件系统,偶尔也用于 DVD。FreeBSD 支持读取和写入 CD,如果你有 CD 刻录机的话。几乎所有遇到的 CD 都使用 ISO 9660 格式。挂载命令是 mount -t cd9660

cdrtools 包位于 /usr/ports/sysutils/cdrtools,包含了许多有用的工具,用于处理 CD 镜像,包括可以从磁盘上的文件生成 ISO 镜像的工具。

UDF

UDF(通用磁盘格式)是 ISO 9660 的替代品。你会在一些 DVD 和 Blu-Ray 磁盘上找到 UDF,此外一些大于 FAT32 支持的 32GB 的 U 盘也使用 UDF。随着可移动媒体容量的增加,UDF 文件系统会越来越常见。挂载命令是 mount -t udf

EXT

标准的 Linux 文件系统——EXT2、EXT3 和 EXT4——支持许多与 UFS 相同的功能。FreeBSD 可以安全地读取和写入 EXT2 和 EXT3 文件系统,但只能将 EXT4 文件系统挂载为只读模式。

挂载 Linux 文件系统最常用于灾难恢复、双启动系统或系统迁移。尽管名字如此,mount -t ext2fs 支持挂载所有版本的 EXT。

Linux 文件系统用户可能会发现 /usr/ports/sysutils/e2fsprogs 中的工具很有用,它们可以让你 fsck(8) 并检查 Linux 文件系统,除此之外还有其他功能。

权限和外部文件系统

文件系统的权限取决于文件系统的特性和挂载它的人。FreeBSD 会尝试支持那些与 UFS 或 ZFS 中的特性差异不大的功能。

考虑一下 Linux 文件系统 EXT。EXT 在文件系统中存储权限,并让内核将其映射到 UID。由于 EXT 权限与 UFS 权限非常相似,并且所有必要的权限信息都可以在文件系统内找到,因此 FreeBSD 会尊重这些文件系统上的权限。然而,EXT 不支持 BSD 文件标志,因此你不能在 EXT 上为文件分配这些标志。

FAT 没有权限系统。即使你将 FAT32 U 盘挂载到 FreeBSD 主机上,也无法对文件应用权限。

默认情况下,只有 root 用户可以挂载文件系统,并且 root 拥有所有非 Unix 文件系统。如果这不是你想要的配置,你可以使用-u-g标志来设置挂载 FAT32、ISO 9660 或 UDF 文件系统时的用户 ID 和组 ID。例如,如果你要为用户xistence挂载一个 FAT32 USB 设备,并希望他能够编辑其内容,可以使用以下命令:

# mount -t msdosfs -u xistence -g xistence /dev/da5 /mnt

现在,用户xistence拥有设备上的文件。

你可能会厌倦为用户挂载介质,尤其是在拥有数十台机器的设施中。要允许用户挂载文件系统,可以将 sysctl vfs.usermount设置为1。用户就可以在他们有权限访问的任何挂载点上挂载任何设备。虽然xistence不能将可移动设备挂载到/media,但他可以将其挂载到/home/xistence/media

使用可移动介质

你必须能够管理任何可能进入数据中心的可移动介质。在这里,我们将讨论光盘和闪存驱动器。

我建议不要随便把可移动介质插入你的生产服务器——至少出于安全原因,应该避免这样做。谁知道那个供应商的 USB 设备上到底有什么?更糟糕的是,你甚至可以订购“USB 杀手”设备,它们故意损坏硬件。将可疑设备挂载在可丢弃的工作站上,检查其内容,然后将所需的数据复制到 FreeBSD 机器上。这并不能保证安全,因为许多 USB 接口可以在操作系统层下面注入数据,但这是你能做到的最安全方式。然而,某些应用程序仍然觉得可移动介质太方便,当然,当是我的个人 USB 设备时,规则会有所不同。

使用设备需要一个文件系统类型、一个设备节点和一个挂载点。

确定可移动驱动器的文件系统可能需要一些试验和错误。CD 使用 ISO 9660 文件系统,而 DVD 和蓝光光盘则使用 UDF 或 ISO 9660 和 UDF 的组合。若不确定,首先尝试 CD9660。USB 设备和软盘通常使用 FAT32。虽然曾经预计大容量 USB 设备会使用 UDF,但大多数仍然使用 FAT32。运行 fstyp(8)命令来帮助识别设备节点上的文件系统,或者可以在磁盘的设备节点上尝试gpart show

可移动设备每次插入时可能会有不同的设备节点。光驱稍微容易识别,因为大多数主机的光驱数量非常少。如果你有一个光驱,它的设备节点是/dev/cd0。USB 设备会显示为/dev/da的下一个可用单元。当你插入 USB 设备时,控制台和/var/log/messages会显示一个包含设备节点和类型的消息,或者你也可以通过camcontrol devlist查看新设备。

FreeBSD 提供了一个/media挂载点用于一般的可移动媒体挂载。你可以根据需要创建其他挂载点——它们只是目录。对于一些杂项的短期挂载,FreeBSD 提供了/mnt

所以,要将你的 FAT32 USB 设备/dev/da0挂载到/media,运行:

# mount -t msdosfs /dev/da0 /media

偶尔,你会发现一个带有分区表的 U 盘。这些设备会要求你挂载/dev/da0s1/dev/da0p1,而不是/dev/da0。这是设备的格式化方式所决定的,而不是 FreeBSD 的设置。gpart show命令可以帮助你弄清楚设备上的分区以及每个分区上的文件系统。

弹出可移动媒体

要从 FreeBSD 系统中断开可移动媒体,首先卸载文件系统。直到卸载磁盘之前,光驱将无法打开。你可以从端口拔出 USB 闪存驱动器,但在文件系统挂载的情况下拔出可能会损坏设备上的数据。像对待任何其他文件系统一样,使用 umount(8):

# umount /media

在许多光驱上,camcontrol eject可以打开驱动器托盘。

可移动媒体和/etc/fstab

你可以通过更新/etc/fstab来为可移动媒体添加条目,以便简化系统维护。如果一个可移动文件系统在/etc/fstab中有条目,你可以在挂载时省略文件系统和设备名称。这意味着你不必记住精确的设备名称或文件系统来挂载设备。

/etc/fstab中列出可移动媒体时,务必包含noauto标志。否则,每当你没有插入可移动媒体时,启动过程会停在单用户模式,因为缺少文件系统。

这是光驱的/etc/fstab条目:

/dev/cd0   /cdrom   cd9660  ro,noauto       0       0

虽然我确信你已经记住了/etc/fstab中每一列的含义,但我们仍然提醒你,这个条目的意思是,“将/dev/cd0挂载到/cdrom,使用 ISO 9660 文件系统。将其设置为只读,并且在启动时不要自动挂载。”

这里是一个类似的 U 盘条目。我使用large选项来支持大于 128GB 的文件系统,如 mount_msdosfs(8)中所述。

➊/dev/da0    /media    msdosfs  rw,noauto,large     0       0

FreeBSD 默认不提供这些设置,但在我经常使用可移动媒体的系统中,拥有这些设置会更加方便。确认你下一个可用的 da 设备是/dev/da0 ➊,因为尝试挂载一个已经挂载的硬盘是无法成功的。

格式化 FAT32 媒体

U 盘使用 FAT32 文件系统,但总是已经预格式化。由于 U 盘的读写次数与其价格成反比,切勿轻易重新格式化它们。^(1) 只有在文件系统损坏时才重新格式化 U 盘。使用 newfs_msdos(8)来创建 FAT32 文件系统。

# newfs_msdos /dev/da0

你会看到几行输出,并且你会有一个新的文件系统。

创建光盘介质

FreeBSD 允许你将多个文件打包成适合刻录到 CD、DVD 或 Blu-Ray 上的镜像,使用 CD 9660 或 UDF 格式。你可以将任一镜像刻录到磁盘上。FreeBSD 原生支持创建 ISO 文件,但你需要从cdrtools包中获取程序来创建 UDF。

无论哪种方式,首先将你想要刻录的所有文件和目录放入一个单一的目录。镜像将包含这些文件和目录,按照你安排的方式保存。记住,光盘镜像是只读的。你不能更新一个镜像;你只能创建一个新的镜像,所以一定要确保所有内容都正确无误。稍后本章你将学到如何使用 mdconfig(8)挂载这些镜像。

在这两个示例中,我们都是从/home/xistence/cdfiles中的文件创建一个镜像。

创建 ISO

使用 makefs(8)来创建 ISO。

# makefs ➊-t cd9660 ➋-o allow-deep-trees,rockridge ➌image.iso ➍source-files

首先使用-t ➊指定要创建的文件系统类型——在本例中是 CD 9660。-o标志➋允许你指定特定于文件系统的选项。你可以从 makefs(8)的手册页中获得完整的选项列表,但这里显示的选项对于大多数镜像已经足够。接下来,我们需要指定创建的镜像文件名 ➌ 和源目录 ➍,该目录中包含要复制的文件。

要创建一个包含/home/xistence/cdfiles中所有文件的镜像,并命名为bert.iso,运行:

# makefs -t cd9660 -o allow-deep-trees,rockridge bert.iso /home/xistence/cdfiles

Bert 现在可以浪费地将 ISO 刻录到物理介质上。

创建 UDF

创建 UDF 需要使用来自cdrtools包的 mkisofs(1)命令。使用-o指定目标镜像文件。使用-J-R分别启用 Joliet 和 Rock Ridge 扩展。(我不会详细说明这些选项的作用,但如果你希望 ISO 像本世纪的光盘一样工作,你需要它们。)添加-udf-iso-level 3标志。^(2)

# mkisofs -R -J -udf -iso-level 3 -o bert.udf /home/xistence/cdfiles

现在你已经有了一个基于/home/xistence/cdfiles内容的 UDF 镜像。

无论你创建哪种格式,我建议你在刻录物理光盘之前先挂载它,并仔细检查你的工作。如果你幸运的话,你会记得在镜像中忘记包含的东西。

刻录 ISO 到光学介质

使用来自cdrtools包的 cdrecord(1)命令将 ISO 镜像刻录到光盘。将镜像文件作为参数提供。

# cdrecord bert.iso

根据驱动器速度和映像大小,这可能需要一些时间。

cdrecord(1)程序默认使用/dev/cd0。如果你有其他光驱,可以使用-dev标志指定另一个设备名称。

# cdrecord -dev=cd9 bert.iso

现在你有了一张脆弱的塑料光盘,你将在使用两次后把它丢进垃圾填埋场。恭喜!

将 UDF 刻录到光学介质

虽然你可以使用 cdrecord(1)将 UDF 镜像刻录到介质上,但通常推荐使用dvd+rw-tools包中的 growisofs(1)命令。你需要使用-dvd-compat-Z标志。然后,指定设备和镜像文件。

# growisofs -dvd-compat -Z /dev/burner=image.udf

假设我想将bert.udf刻录到/dev/cd0的蓝光光盘中。

# growisofs -dvd-compat -Z /dev/cd0=bert.udf

UDF 文件可能非常大。去泡杯茶吧。最终,你会得到一张刻录好的光盘。

将映像写入闪存驱动器

USB 闪存驱动器由于其可重复使用性,逐渐取代了光盘。FreeBSD 支持使用 dd(1)命令将磁盘映像写入闪存驱动器。

一定要非常确定哪个设备节点是你的 U 盘,哪个是你的系统硬盘。U 盘显示为 /dev/da 设备,就像许多硬盘一样。覆盖错误的硬盘是非常尴尬的。^(3)

dd(1) 命令乍一看可能会让人感到困惑。

# dd ➊if=inputfile ➋of=outputdevice ➌bs=1M ➍conv=sync

if= 参数 ➊ 指定你要复制的文件。of= 参数 ➋ 指定你要复制到的设备节点。bs= 标志 ➌ 指定一次复制的数据量。如果没有这个,dd(1) 会按 512 字节递增进行复制。conv= 参数 ➍ 给 dd(1) 提供如何转换输入文件的指令。在这种情况下,sync 告诉 dd(1) 同步输入和输出缓冲区的大小。为了将 bertimage.udf 刻录到 U 盘 /dev/da9,我会运行:

# dd if=bert.udf of=/dev/da9 bs=1m conv=sync

等一下,你就会得到一个镜像的 U 盘。dd(1) 的其他用法可能不需要 conv= 标志,但始终使用 bs

现在让我们看看一些你可能会发现有用的其他文件系统。

内存文件系统

除了将文件系统放在磁盘或分区上,FreeBSD 还允许你从文件、纯 RAM 或两者的组合中创建分区。这个功能最常见的用途之一是 内存文件系统内存磁盘。将文件读写到内存中的速度比访问磁盘上的文件要快得多,这使得内存支持的文件系统在某些应用中是一项巨大的优化。然而,和其他所有内存操作一样,你会在系统关闭时丢失内存磁盘中的内容。

FreeBSD 支持两种不同的内存支持磁盘:tmpfs(发音为“temp f s”)和内存磁盘。虽然它们背后有相似的概念,但底层代码完全不同,而且它们的作用也不同。在长期运行的系统上使用 tmpfs(5) 作为内存支持文件系统。内存磁盘更加灵活,但更适合短期使用或挂载磁盘镜像。

tmpfs

tmpfs(5) 中的 tmp 并不意味着“临时”。它字面上意味着 tmp,即 /tmp。使用 tmpfs 来加速内存支持的 /tmp 和类似的文件系统。不过,不要在你看到路径中有 tmp 时就到处部署 tmpfs。虽然 /tmp 应该在每次启动时清空,但 /var/tmp 应该在重启后依然存在。你可能会使用 tmpfs 来处理应用程序锁文件和其他短暂数据,这些数据的速度大幅提升会改善应用程序性能。尽管 tmpfs 曾有过一段困难的历史,但从 FreeBSD 10 开始,它已广泛部署并被认为可以用于生产环境。

通过挂载来创建一个 tmpfs。

# mount -t tmpfs tmpfs /tmp

如果你的系统将 sysctl vfs.usermount 设置为 1,用户就可以创建和挂载 tmpfs 文件系统。

tmpfs 选项

默认情况下,tmpfs 的大小为系统可用的 RAM 加上可用的交换空间。反复将文件复制到 /tmp 可能会耗尽系统内存。这将是非常糟糕的。通过 size 选项为你的 tmpfs 设置一个最大大小。

# mount -o size=1g -t tmpfs tmpfs /tmp

使用 uidgidmode 选项来控制 tmpfs 的所有权和权限。实际的 /tmp 目录需要具有世界可写权限,并设置粘滞位,因此请确保使用选项 mode=1777

如果 tmpfs 是为特定用户而设置的,即使是只运行一个应用程序的无特权用户,也应将该用户的所有权分配给 tmpfs。

Boot 时的 tmpfs

现在你可以设置最大大小和适当的权限,因此可以使用 /etc/fstab 来自动在启动时创建 tmpfs。

tmpfs  /tmp  tmpfs  rw,mode=1777,size=1G  0  0

对于更复杂的内存后备磁盘,可以考虑使用传统的内存磁盘。

内存磁盘

内存磁盘是一个短暂的存储设备。尽管名称是内存磁盘,内存磁盘并不总是将一块内存当作磁盘来使用。它也可以是这样一个设备,但它可能使用文件、交换空间或其他后备存储。无论如何,内存磁盘在系统关闭时会消失。

内存磁盘类型

内存磁盘有四种类型:malloc-backed、swap-backed、vnode-backed 和 null。

Malloc-backed 内存磁盘是纯内存磁盘。即使系统内存不足,FreeBSD 也不会将 malloc-backed 磁盘交换出去。与 tmpfs(5) 类似,使用大容量的 malloc-backed 磁盘是耗尽系统内存的一种好方法。Malloc-backed 磁盘对于没有交换空间的嵌入式设备特别有用。

Swap-backed 内存磁盘大多数是内存,但它们也会访问系统的交换分区。如果系统内存不足,它会将最近最少使用的内存部分移动到交换区,正如在第二十一章中讨论的那样。Swap-backed 磁盘通常是速度和性能之间最好的折衷。

Vnode-backed 内存磁盘是磁盘上的文件。虽然你可以使用文件作为内存磁盘的后备存储,但这主要用于挂载磁盘映像和测试。

Null 内存磁盘会丢弃发送到它的所有内容。所有写入操作都会成功,而所有读取操作都返回零。如果我没有提到 null 内存磁盘,可能有人会写信抱怨,但我不会再给一个保证丢失所有数据的磁盘更多的介绍。

一旦你知道想要做什么,就使用 mdmfs(8) 执行操作。

创建和挂载内存磁盘

mdmfs(8) 工具是多个程序的便捷前端,例如 mdconfig(8) 和 newfs(8)。它处理设备配置和文件系统创建的繁琐工作,使创建内存磁盘尽可能简单。你只需知道你想使用的磁盘大小、内存磁盘类型和挂载点。

Swap-backed 内存磁盘是默认设置。只需告诉 mdmfs(8) 磁盘的大小和挂载点。这里,我们在 /home/mwlucas/test 上创建了一个 48MB 的 swap-backed 内存磁盘:

# mdmfs -t -s 48m md /home/mwlucas/test

-s 标志指定磁盘大小。如果你运行 mount(8) 而不带任何参数,你会看到你现在已经在该目录上挂载了内存磁盘设备 /dev/md0

-t 标志启用 TRIM,我们将在接下来的“内存磁盘问题”一节中讨论。

要创建并挂载一个 malloc 支持的磁盘,添加-M标志。

要挂载一个 vnode 支持的内存磁盘,使用-F标志并指定映像文件的路径。

# mdmfs -F diskimage.file md /mnt

我们一直使用的md条目意味着,“我不在乎我得到什么设备名称,给我下一个空闲的就行。”你也可以指定一个特定的设备名称,如果你愿意的话。在这里,我声明我想要磁盘设备/dev/md9

# mdmfs -F diskimage.file md9 /mnt
内存磁盘头痛

传统的交换支持的内存磁盘从未将已使用的内存返回给系统。一旦你写入内存磁盘,那块内存就被消耗掉了。如果你需要一个更大的内存磁盘,你必须为其永久分配内存。这是 FreeBSD 包括 tmpfs(5)的原因之一。

然而,如果内存磁盘上的文件系统支持TRIM,FreeBSD 现在会将未使用的内存返回给系统。TRIM 不是一个缩写,而是一种告诉磁盘哪些扇区不再使用的协议。UFS,默认的内存磁盘格式,支持 TRIM。在 mdmfs 中通过-t标志启用 TRIM。如果你在内存磁盘上使用的是其他文件系统,请确保它是严格临时的。

要释放内存磁盘的内存,关闭内存磁盘。

内存磁盘关闭

要移除内存磁盘,必须卸载分区并销毁磁盘设备。销毁磁盘设备会释放该设备占用的内存,这在系统负载较重时非常有用。要找到磁盘设备,运行 mount(8)并查找你的内存磁盘分区。在输出的某个位置,你会找到类似这样的行:

/dev/md41 on /mnt (ufs, local, soft-updates)

在这里,我们看到内存磁盘/dev/md41挂载在/mnt上。让我们卸载并销毁它。

# ➊umount /mnt
# mdconfig ➋-d ➌-u 41

使用umount ➊卸载就像处理其他文件系统一样。mdconfig(8)调用是一个新的命令。使用 mdconfig(8)直接管理内存设备。-d标志 ➋表示销毁-u标志 ➌提供一个设备号。上述操作销毁了设备/dev/md41,即md设备号 41。该设备占用的内存现在被释放,可以供其他用途。

内存磁盘与/etc/fstab

如果你在/etc/fstab中列出了内存磁盘,FreeBSD 会在启动时自动创建它们。这些条目看起来比其他条目复杂,但如果你理解我们到目前为止使用的 mdmfs(8)命令,其实也不难。

我们可以使用md作为设备名称来表示内存磁盘。选择挂载点,就像任何其他设备一样,并使用文件系统类型mfs。在Options下,列出rw(表示可读写)以及用于创建此设备的命令行选项。如果这是一个长期挂载,添加-t以启用 TRIM。要创建一个 48MB 的文件系统并将其挂载到/home/mwlucas/test,使用以下/etc/fstab条目:

md      /home/mwlucas/test      mfs      rw,-s48m,-t      0   0

看起来很简单,是不是?唯一的问题是,长长的一行会弄乱你漂亮整齐的/etc/fstab条目的外观。好吧,它们并不是唯一会让这个文件变丑的东西,正如我们很快将看到的。

挂载磁盘映像

你可以使用 mdmfs(8)查看 UFS 磁盘镜像,但通常你希望查看 ISO 或 UDF 文件的内容,而不需要将其刻录到磁盘上。(FreeBSD 的 tar(1)可以访问 ISO 的内容,但无法访问 UDF。)只需使用 mdconfig(8)命令的-a标志将内存磁盘附加到文件。这里,我将 Bert 的 ISO 附加到一个内存设备:

# mdconfig ➊-a -t ➋vnode -f ➌/home/mwlucas/bert.iso
➍md0

我们告诉 mdconfig(8)将一个基于 vnode 的内存设备➊附加到指定的文件➌。mdconfig(8)命令响应并告诉我们它附加到的设备➍。现在,我们只需使用适当的挂载命令将该设备挂载到文件系统:

# mount -t cd9660 /dev/md0 /mnt

我现在可以验证 ISO 中包含了 Bert 的文件,这样他就不会抱怨 ISO 文件坏掉了。

在这一步,常见的错误是没有指定文件系统类型就挂载镜像。你可能会遇到错误,或者可能会成功挂载但没有数据——默认情况下,mount(8)会假设文件系统是 UFS!

完成数据访问后,请确保卸载镜像并销毁内存磁盘设备,就像你对待任何其他内存设备一样。虽然基于 vnode 的内存磁盘不会消耗系统内存,但如果你留下未使用的内存设备,几个月后你可能会困惑为什么它们出现在/dev中。如果你不确定系统上有哪些内存设备,可以使用mdconfig -l查看所有已配置的 md(5)设备。

# mdconfig -l
md0 md1

我有两个内存设备?添加-u标志和设备编号,看看它是什么类型的内存设备。让我们来看一下内存设备 1(/dev/md1)是什么:

# mdconfig -l -u 1
md1     vnode     456M  /slice1/usr/home/mwlucas/iso/omsa-51-live.iso

我在这个系统上挂载了一个 ISO 镜像?哇。我可能应该在某个月重启一下。算了,太麻烦了,我直接卸载文件系统并销毁内存设备就好了。

文件系统中的文件

嵌入式系统中使用的一个技巧是将完整的文件系统镜像构建在本地文件中。在前一部分中,我们看到如何使用内存磁盘挂载和访问 CD 磁盘镜像。你可以使用相同的技术来创建、更新和访问 UFS 磁盘镜像。

要在文件中使用文件系统,必须创建一个合适大小的文件,将该文件附加到内存设备上,在设备上放置文件系统,然后挂载该设备。

创建空的文件系统文件

使用 truncate(1)创建一个空的文件系统文件。这些文件是稀疏文件:它们标记为具有某个大小,但在你往里面放东西之前并不会占用任何空间。一个空的稀疏文件只占用一个文件系统块,放入数据后它才会增长。这意味着你可以为任意大小的磁盘创建一个镜像文件,但实际占用的空间只等于你放入镜像中的数据量。

使用-s选项和文件大小来创建镜像文件。这里,我创建了一个 1GB 的文件:

# truncate -s 1G filesystem.file

结果文件显示它的大小相当大。

# ls -l filesystem.file
-rw-r--r--  1 mwlucas  mwlucas  1073741824 Aug 11 11:31 filesystem.file

但是,如果你检查磁盘使用情况,你会看到不同的结果。

# du filesystem.file
1       filesystem.file

这个 1GB 的文件在磁盘上使用一个块。

稀疏文件永远不会缩小。它们只能增长。如果你从磁盘镜像中删除了大量文件,镜像文件仍然需要这些空间。

此外,并不是所有文件系统都支持稀疏文件。UFS 和 ZFS 支持。如果你正在尝试在 FAT32 文件系统上创建稀疏文件,那可能是你在解决错误的问题。

在文件上创建文件系统

要在文件上获取文件系统,首先通过 vnode 支持的内存磁盘将文件与设备关联。我们在上一节中做的就是这个操作:

# mdconfig -a -t vnode -f filesystem.file
md0

现在,让我们在这个设备上创建一个文件系统。这很像在一个闪存盘上使用 newfs(8) 命令创建 UFS 文件系统。软更新日志与文件支持文件系统一样对磁盘支持的文件系统非常有用,因此通过 -j 启用它们。

# newfs -j /dev/md0 
/dev/md0: 1024.0MB (2097152 sectors) block size 32768, fragment size 4096
        using 4 cylinder groups of 256.03MB, 8193 blks, 32896 inodes.
        with soft updates
super-block backups (for fsck_ffs -b #) at:
 192, 524544, 1048896, 1573248
Using inode 4 in cg 0 for 8388608 byte journal
newfs: soft updates journaling set

newfs(8) 程序会输出磁盘的基本信息,例如其大小、块和碎片大小,以及 inode 数量。

现在你已经有了一个文件系统,挂载它:

# mount /dev/md0 /mnt

恭喜!你现在拥有了一个 1GB 的文件支持文件系统。你可以将文件复制到其中,将其转储到磁带上,或以任何你使用其他文件系统的方式使用它。但除此之外,你还可以像对待其他文件一样移动它。

文件支持文件系统和 /etc/fstab

你可以通过在 /etc/fstab 中添加正确的条目,自动在启动时挂载文件支持文件系统,就像自动挂载其他内存磁盘一样。你只需使用 -F 指定文件名,并使用 -P 告诉系统不要在这个文件上创建新文件系统,而是使用已经存在的那个文件系统。在这里,我们将创建的文件支持文件系统自动挂载到 /mnt,并在启动时挂载。

md    /mnt     mfs     rw,-P,-F/home/mwlucas/filesystem.file    0    0

我告诉过你,我们会看到比通用内存磁盘更丑的 /etc/fstab 条目,是吧?

devfs

devfs(5) 是一个用于管理设备节点的动态文件系统。记住,在类 Unix 操作系统中,一切都是文件。这包括物理硬件。几乎所有系统上的设备都有一个 /dev/ 下的设备节点。你已经见过一些磁盘的设备节点,但你也会看到键盘(/dev/ukbd0/dev/kbd0)、控制台(/dev/console)、音频混音器(/dev/mixer0)等等。你还会找到逻辑设备的设备节点,例如随机数生成器(/dev/random)、终端会话(/dev/ttyv0)等等。

曾几何时,系统管理员负责创建这些设备节点文件。幸运的系统管理员管理着一个带有用于处理设备节点创建和权限的 shell 脚本的操作系统。如果操作系统的开发者没有提供这样的脚本,或者如果服务器有不包含在脚本中的特殊硬件,那么系统管理员就得通过动物祭祀和 mknod(8) 创建节点。如果出现任何小问题,设备就无法正常工作。另一种选择是将操作系统与每种硬件设备的设备节点一起发布。系统管理员可以放心——嗯,大致放心——所需的设备节点在某个地方,埋藏在 /dev 下的成千上万的文件中。

当然,内核确切知道每个设备节点应该具有什么特性。使用 devfs(5),FreeBSD 仅询问内核系统应具备哪些设备节点,并提供这些设备节点——没有更多。对于大多数人来说,这样的方式非常有效。不过,你我并非“多数人”。我们对计算机有着不同寻常的需求。或许我们需要使用不同的名称来访问设备节点、改变设备节点的所有权,或者独特地配置硬件。FreeBSD 将设备节点管理的问题分解为三部分:启动时配置设备、全局可用性与权限以及配置启动后通过 devd(8) 动态出现的设备。

/dev 启动时

当设备节点是磁盘上的永久文件时,系统管理员可以创建指向这些节点的符号链接或更改它们的权限,而不必担心这些更改会消失。随着自动化的动态设备文件系统,这种保障消失了。(当然,你也不再需要担心神秘的 mknod(8) 命令了,所以从长远来看,你会受益。)设备节点的更改可能包括例如:

  • 使设备节点以不同名称可用

  • 更改设备节点的所有权

  • 隐藏设备节点不让用户访问

设备管理与服务器

在大多数情况下,服务器上的设备节点管理工作无需任何调整或干预。我最常需要调整设备节点的地方是在笔记本电脑和偶尔的工作站上。FreeBSD 的设备节点管理工具非常强大且灵活,支持一些我不曾预料到的使用方式。我们只讨论基础部分。不要认为你必须精通 devfs(5) 才能让你的服务器运行得很好!

在启动时,devfs(8) 根据 /etc/devfs.conf 中的规则创建设备节点。

devfs.conf

/etc/devfs.conf 文件允许您为启动时可用的设备创建链接、改变所有权和设置权限。每条规则的格式如下:

action      realdevice      desiredvalue

有效的操作包括 link(创建链接)、perm(设置权限)和 own(设置所有者)。realdevice 条目是一个预存在的设备节点,而最后一个设置是您所需的值。例如,在这里我们为设备节点创建一个新名称:

➊link      ➋cd0           ➌cdrom

我们希望为设备节点 /dev/cd0 ➋(光驱)创建一个符号链接 ➊,并希望该链接命名为 /dev/cdrom ➌。只要我们在 /etc/devfs.conf 文件中添加这条规则并重启,光驱 /dev/cd0 就会以 /dev/cdrom 的形式出现,正如许多桌面多媒体程序所期望的那样。

要更改设备节点的权限,请以八进制形式提供所需的权限值:

perm        cd0            666

在这里,我们设置 /dev/cd0(我们的 CD 设备)的权限,以便任何系统用户都可以读取或写入该设备。记住,更改 /dev/cdrom 链接的权限不会更改设备节点的权限,只会更改符号链接的权限。

最后,我们也可以更改设备的所有权。更改设备节点的所有者通常表示你正在以错误的方式解决问题,可能需要停下来思考。FreeBSD 虽然允许你如果坚持的话,随意搞砸系统,但还是能让你做这些操作。在这里,我们让一个特定的用户对磁盘设备/dev/da20拥有绝对控制权限:

own         da20            xistence:xistence

然而,这可能不会产生预期的效果,因为有些程序仍然认为你必须是 root 用户才能执行设备上的操作。我见过不止一个软件如果不是由 root 用户运行,甚至在未尝试访问其设备节点之前就自行关闭。更改设备节点权限不会阻止这些程序在普通用户运行时的抱怨。

使用 devfs.conf(5)进行配置解决了许多问题,但并非所有问题。如果你想让一个设备节点完全不可见且不可访问,你必须使用 devfs 规则。

全局 devfs 规则

每个 devfs(5)实例的行为都遵循devfs.rules中定义的规则。devfs 规则适用于启动时存在的设备和动态出现或消失的设备。规则允许你设置设备节点的所有权和权限,并使设备节点可见或不可见。你不能使用 devfs 规则创建指向设备节点的符号链接。

类似于/etc/rc.conf/etc/defaults/rc.conf,FreeBSD 使用/etc/devfs.rules/etc/defaults/devfs.rules。为你的自定义规则创建一个/etc/devfs.rules,并保持默认文件中的条目不变。

devfs 规则集格式

每一组 devfs 规则以一个名称和一个方括号中的规则集编号开始。例如,这是来自默认配置的一个 devfs 规则:

[➊devfsrules_hide_all=➋1]
➌add hide

devfs.rules中的第一条规则被称为devfs_hide_all ➊,并且是规则集编号 1 ➋。这个规则集只包含一条规则 ➌。

一旦你有了一组喜欢的 devfs 规则,就可以在启动时在/etc/rc.conf中启用它们。这里,我们激活了名为laptoprules的 devfs 规则集:

devfs_system_rulesets="laptoprules"

请记住,devfs 规则适用于启动时系统中的设备,以及启动后动态配置的设备。

规则集内容

所有 devfs 规则(在文件中)都以单词add开始,以将规则添加到规则集中。然后,你可以选择使用path关键字和设备名称的正则表达式,或者使用type关键字和设备类型。在规则的末尾,你有一个动作,或者是执行的命令。以下是一个 devfs 规则的示例:

add path da* user mwlucas

该规则将用户mwlucas的所有权分配给所有名称以da开头的设备节点。这可能不是一个好主意。

通过路径指定的设备使用标准的 Shell 正则表达式。如果你想匹配多种设备,可以使用星号作为通配符。例如,path ada1s1仅匹配设备/dev/ada1s1,而path ada*s*匹配所有以ada开头,后跟一个字符,字母s,并且可能有更多字符的设备节点。你可以通过在命令行上使用通配符来精确了解匹配了哪些设备。

# ls /dev/ada*s*

这将列出你所有 SATA 硬盘上的所有 MBR 切片和分区,但不包括整个硬盘的设备。

type 关键字表示你希望规则应用于给定类型的所有设备。有效的关键字包括 disk(磁盘设备)、mem(内存设备)、tape(磁带设备)和 tty(终端设备,包括伪终端)。type 关键字很少使用,因为它的作用范围太广。

如果你既没有包含 path 也没有包含 type,devfs 会将规则末尾的动作应用到所有设备节点。在几乎所有情况下,这都是不希望的。

规则集动作可以是 groupusermodehideunhide 中的任何一种。group 动作让你设置设备的组所有者,作为附加参数。类似地,user 动作分配设备所有者。在这里,我们将 da 磁盘的所有权设置为用户名 desktop 和组 usb

add path da* user desktop
add path da* group usb

mode 动作允许你以标准的八进制形式分配设备的权限。

add path da* mode 664

hide 关键字可以让你让设备节点消失,unhide 让它们重新出现。由于没有程序可以使用不可见的设备节点,这在除了系统使用 jail(8) 时的用途有限。隐藏和重新显示最有意义的场景是包含规则中的规则。

在规则中包含规则

和许多系统管理方面一样,使 devfs 规则模块化以便重用是减少问题的好方法。默认的监狱规则正好展示了 FreeBSD 的 devfs 如何通过 include 关键字支持重用。

这是默认配置的开始:

➊ [devfsrules_hide_all=1]
➋ add hide

➌ [devfsrules_unhide_basic=2]
➍ add path log unhide
➎ add path null unhide
➏ add path zero unhide
➐ add path crypto unhide
   --snip--

规则一,devfsrules_hide_all ➊,隐藏所有设备节点 ➋。

规则二,devfsrules_unhide_basic ➌,仅包含一系列 unhide 声明。此规则只会做一件事:重新显示关键的 Unix 设备节点,如 /dev/log ➍、/dev/null ➎、/dev/zero ➏、/dev/crypto ➐ 等。大多数进程没有这些设备就无法运行。这些设备节点在标准系统中已经暴露,那么为什么还需要一个规则来重新显示它们呢?类似地,规则集三,devfsrules_unhide_login,仅仅是重新显示已登录用户的设备节点。

最后一个规则集利用了所有这些。

[devfsrules_jail=4]
add include $devfsrules_hide_all
add include $devfsrules_unhide_basic
add include $devfsrules_unhide_login
add path zfs unhide

这个规则集 devfsrules_jail 使用 include 语句通过引用引入之前的规则集。最后一个语句还重新显示了 /dev/zfs,使得 ZFS 工具可以在监狱中工作。

如果你希望在所有监狱中都能使用额外的设备节点,你可以将该设备节点添加到监狱的规则集中。或者,你可以定义一个新的规则集,并在所有监狱中使用它。更好的做法是,你可以为仅需要该设备的监狱定义一个规则集,并将该规则集分配给那些监狱。

最后,让我们看一下动态设备。

使用 devd(8) 进行动态设备管理

热插拔硬件现在已经是常规操作。FreeBSD 的 devfs 在插入硬件时动态创建新的设备节点,并在硬件移除时删除这些节点,这使得使用这些动态设备变得更加简单。devd(8)守护进程更进一步,允许你在硬件出现和消失时运行用户空间程序。

FreeBSD 的默认配置,/etc/devd.conf,能够很好地处理大多数现代硬件。如果你需要定制 devd(8),可以将配置文件放在/usr/local/etc/devd/下,以简化升级。你还可以为不同类型的设备添加不同的规则文件,如果你发现 devd(8)配置变得非常复杂。

devd 配置

你会发现四种类型的 devd(8)规则:attachdetachnomatchnotify

attach规则在匹配的硬件附加到系统时被触发。当你插入一个网络卡时,attach规则会为卡配置一个 IP 地址并启动网络。

detach规则在匹配的硬件从系统中移除时被触发。detach规则不常见,因为当底层硬件消失时,内核会自动标记资源为不可用,但你可能会在某些情况下用到它们。

nomatch规则在新硬件安装但未附加到设备驱动时被触发。这些设备在当前内核中没有设备驱动。

devd(8)在内核向用户空间发送匹配的事件通知时应用notify规则。例如,网络接口已启动的控制台消息就是一个notify事件。通知通常会出现在控制台或/var/log/messages中。

规则也有优先级,0 是最低的。只有最高匹配的规则会被处理,而优先级较低的匹配规则会被跳过。以下是一个 devd(8)规则的示例:

➊notify ➋0 {
          match "system"          ➌"IFNET";
          match "subsystem"       ➍"!usbus[0-9]";
          match "type"            ➎"ATTACH";
          action ➏"/etc/pccard_ether $subsystem start";
};

这是一个notify规则➊,意味着当内核向用户空间发送消息时,它会被触发。作为一个优先级为 0 的规则➋,只有在没有更高优先级的规则匹配我们指定的条件时,这个规则才会被触发。这个规则只有在通知发生在网络系统IFNET ➌(网络)且子系统 ➍不匹配表达式usbus[0-9]时才会被触发。它排除了 USB 网卡。当通知类型为ATTACH ➎时,也就是说,只有在有人插入网络接口时才会匹配。如果这三个条件都满足,devd(8)会运行一个命令来配置网络接口➏。

阅读 devd(8)的手册页,了解你可以在规则中使用的所有选项。如果你想在特定的挂载点上自动挂载某个 USB 闪存盘,可以通过检查每个插入的 USB 设备的序列号来实现。如果你希望对 Intel 网卡和 Atheros 网卡进行不同的配置,可以通过检查供应商来做到这一点。无论你需要编写什么规则,它可能都能在这里找到相关信息。

其他文件系统

FreeBSD 支持一些鲜为人知的文件系统。它们大多数只有在特殊情况下才有用,但系统管理中每天都会出现这种特殊情况。

进程文件系统 procfs(5) 包含了大量关于进程的信息。它被视为一个安全风险,并且在现代 FreeBSD 版本中已正式弃用。然而,通过挂载进程文件系统,你可以了解很多关于进程的信息。少数旧应用程序仍然需要挂载在 /proc 上的进程文件系统;如果一个服务器应用程序需要 procfs,试着找到一个类似的应用程序,它可以完成相同的任务而不需要它。

如果你使用的是 Linux 模式(参见 第十七章),你可能需要 Linux 进程文件系统 linprocfs(5)。许多 Linux 软件都需要进程文件系统,FreeBSD 建议在安装 Linux 模式时,在 /compat/linux/proc 安装 linprocfs。我建议只有当某个软件提示找不到它时再安装 linprocfs。

文件描述符文件系统 fdesc(5) 提供了每个进程的文件描述符文件系统视图。一些软件,特别是 Java 和流行的 Bash shell,需要 fdescfs(5)。它比 procfs 风险小,但仍然不太理想。当你安装需要它的软件包时,会提供如何挂载 fdescfs(5) 的说明。

现在我们已经讨论了本地文件系统,让我们来看看网络。

网络文件系统

网络文件系统允许通过网络访问另一台计算机上的文件。最常用的两种网络文件系统是 Unix 中实现的原始网络文件系统(NFS)和由 Microsoft Windows 推广的 CIFS(也称为 SMB)文件系统。我们会简单提及这两者,但先从老旧的 Unix 标准 NFS 开始。

在 Unix 类系统之间共享目录和分区,或许是你能找到的最简单的网络文件系统。FreeBSD 默认支持 Unix 标准的网络文件系统。配置 NFS 让许多初级系统管理员感到畏惧,但在设置了几个文件共享后,你会发现它并不像想象中那么难。

NFS 并不是作为安全协议设计的。不要在没有包过滤器或防火墙的情况下将 NFS 服务器暴露到互联网上。仅仅在 NFS 层面限制访问是完全不够的——你必须防止随机主机探测主机的远程过程调用(RPC)服务。需要通过 IP 地址和端口号限制对主机的访问。

此外,标准的 NFS 并未加密。任何拥有数据包嗅探器并能够访问你的网络线缆的人都能看到所有文件系统活动。一旦你部署了 Kerberos,你就可以加密 NFS,但 Kerberos 需要单独的书籍来讲解。

每个 NFS 连接使用客户端-服务器模型。一台计算机是服务器;它向其他计算机提供文件系统。这被称为 NFS 导出,提供的文件系统称为 导出。客户端可以以几乎与挂载本地文件系统相同的方式挂载服务器的导出。

关于 NFS,有一个有趣的特点是它的无状态性。NFS 不会跟踪连接的状态。你可以重启 NFS 服务器,客户端不会崩溃。虽然在服务器停机时客户端无法访问服务器的导出文件,但一旦服务器恢复,客户端会接着之前的状态继续操作。其他网络文件共享系统并不总是如此健壮。当然,无状态性也会引发一些问题;例如,客户端无法知道它当前打开的文件是否被另一个客户端修改过。

NFS 互操作性

每个 NFS 实现都有些微差异。你会发现 Solaris、Linux、BSD 和其他类 Unix 系统之间存在一些 NFS 的变种。NFS 应该能在它们之间正常工作,但可能需要偶尔调整。如果你在与其他类 Unix 操作系统进行兼容时遇到问题,可以查看FreeBSD-net邮件列表的归档;该问题几乎肯定已经在其中讨论过。

NFS 服务器和客户端都需要内核选项,但各种 NFS 命令会动态加载相应的内核模块。FreeBSD 的 GENERIC 内核支持 NFS,因此对于没有自定义内核的用户来说,这不成问题。

NFS 是一个有完整书籍专门讨论的主题。我们不会深入讨论 NFS 的细节,而是专注于使基本的 NFS 操作能够正常工作。如果你正在部署复杂的 NFS 设置,建议进行进一步的研究。即使是这个基础设置,也能让你完成许多复杂的任务。

NFS 版本

现代的 NFS 有三个版本:NFSv2、NFSv3 和 NFSv4。FreeBSD 可以透明地自动检测并与版本 2 和 3 进行互操作。

NFSv2 相对简陋,源自于人们当时仅仅满足于能够实现文件共享的时期。

NFSv3 相较于 NFSv2 包含许多增量改进,并且性能大大提升。大多数这些改进甚至无需特别配置。

NFSv4 是一个完全不同且高度复杂的协议,它打破了许多 NFS 的长期规则。它被刻意设计成类似微软的文件共享协议。理解 NFSv4 需要了解文件系统扩展 ACL、跨网络同步用户 ID 等问题。

当人们说“NFS”时,他们几乎总是指 NFSv2 或 NFSv3。有些人将这些协议称为“传统 NFS”。通常如果指 NFSv4,人们会说“NFSv4”。

本书坚持使用常见的 NFSv2 和 NFSv3。我在《FreeBSD Mastery: Specialty Filesystems》(Tilted Windmill Press,2016 年)一书中专门花了几章讲解 NFSv4 及相关主题。

配置 NFS 服务器

使用以下rc.conf选项开启 NFS 服务器支持。虽然并非所有选项在所有环境中都是严格必要的,但开启这些选项可以提供最广泛的 NFS 兼容性,并提供良好的开箱即用性能。

➊ nfs_server_enable="YES"
➋ rpcbind_enable="YES"
➌ mountd_enable="YES"
➍ rpc_lockd_enable="YES"
➎ rpc_statd_enable="YES"

首先,告诉 FreeBSD 加载 nfsserver.ko ➊ 内核模块。如果内核不支持 NFS,所有操作都会失败。rpcbind(8) ➋ 守护进程将远程过程调用(RPC)映射到本地网络地址。每个 NFS 客户端都会向服务器的 rpcbind(8) 守护进程询问它可以找到哪个 mountd(8) 守护进程来连接。mountd(8) ➌ 守护进程监听来自客户端的挂载请求,使用高端口号。启用 NFS 服务器还会启动 nfsd(8),它处理实际的文件请求。NFS 通过 rpc.lockd(8) ➍ 确保文件锁定的顺利进行,rpc.statd(8) ➎ 监控 NFS 客户端,以便在主机消失时释放资源。

虽然你可以在命令行启动所有这些服务,但如果你刚刚学习 NFS,最好在启用 NFS 服务器后重启系统。一旦 NFS 正常运行,sockstat(1) 的输出将显示 rpc.lockdrpc.statdnfsdmountdrpcbind 在监听。如果你没有看到所有这些守护进程在监听网络,请检查 /var/log/messages 中的错误信息。

NFS 服务器旨在无缝地与各种 NFS 实现进行互操作。虽然它应该能自动协商连接,但你可能需要调整 NFS 服务器的 nfsd(8) 配置,以最佳适配你的客户端。通过 rc.conf 中的选项 nfs_server_flags 调整 nfsd(8) 的启动参数。

NFS 可以通过 TCP 或 UDP 运行。UDP 是传统的 NFS 传输协议。TCP 在丢包网络环境下表现更好,且能更好地应对不规则的网络速度。FreeBSD 提供了这两种协议,但默认使用 TCP 挂载。一些客户端在某个协议下表现更好。你可以通过 -t 明确启用 TCP,或通过 -u 启用 UDP。

NFS 服务器默认会监听机器上的所有 IP 地址。当服务器有多个 IP 地址时,回复 UDP 请求的地址可以是这些地址中的任意一个。这可能会让 NFS 客户端感到困惑。如果你的 NFS 服务器有多个 IP 地址,并且客户端偏好 UDP 协议,可以告诉 NFS 服务器仅使用一个 IP 地址,通过 -h 和服务器的 IP 来指定。

虽然 nfsd(8) 工作良好,但负载较高的服务器可能需要额外的 nfsd(8) 进程。虽然 FreeBSD 默认启动四个 nfsd(8) 进程,但你可以通过 -n 标志和所需的进程数量启动额外的进程。

这个 rc.conf 条目告诉 NFS 仅使用 UDP,绑定到 IP 地址 198.51.100.71,并运行六个 nfsd(8) 实例。

nfs_server_flags="-uh 198.51.100.71 -n 6"

不过,在你开始调整服务器行为之前,最好先配置一些共享目录。

配置 NFS 导出

现在,告诉你的服务器它可以共享哪些内容,或者说 导出。你可以导出整个服务器上的所有目录和文件系统,但任何称职的安全管理员都会对此感到不满(并且是有正当理由的)。与所有服务器配置一样,在保证服务器履行其职能的同时,应尽量限制访问权限。例如,在大多数环境中,客户端并不需要远程挂载 NFS 服务器的根文件系统。

FreeBSD 让你通过两种不同的路径配置导出。传统方法是通过文件/etc/exports。基于 ZFS 的服务器可以通过每个数据集的sharenfs属性来配置导出。服务器将根据这些属性创建 ZFS 导出文件/etc/zfs/exports。这两种导出文件格式相同。

选择一种管理 NFS 导出的方式。要么编辑/etc/exports,要么使用 zfs(8)。同时使用这两种方法可能会让你感到困惑,甚至可能导致所有功能失效。如果使用 ZFS 方法,切勿手动编辑/etc/zfs/exports。坚持使用一种方法。

无论选择哪种方法,/etc/exports必须存在。如果你通过 zfs(8)管理 NFS,我建议创建一个仅包含注释的单行/etc/exports,告诉大家使用 zfs(8)。

Exports 条目

那么如何配置一个导出呢?我将从 exports 文件/etc/exports开始,但大多数内容也适用于使用 ZFS。我将在第 308 页的“使用 zfs(8)管理 NFS”中讨论这些差异,但理解这些限制需要先理解/etc/exports

每个 exports 条目最多有三部分:

  • 要导出的目录或分区(必需)

  • 该导出的选项

  • 可以连接的客户端

每个客户端和磁盘设备的组合在 exports 文件中只能有一行。这意味着,如果/usr/ports/usr/home在同一分区上,并且你想将它们都导出到某个特定客户端,它们必须出现在同一行中。你不能将/usr/ports/usr/home导出到一个客户端,且给它们不同的权限。请注意,你不必导出整个磁盘设备;你可以仅导出分区中的一个目录。这个目录不能包含符号链接或点。

NFS 挂载不能跨越分区。如果主机为/usr/usr/src配置了独立的 UFS 分区,导出/usr并不会自动导出/usr/src

/etc/exports条目的三部分中,只有目录是必需的。exports 行不能包含符号链接或点。为了将我的主目录导出到互联网上的所有主机,我可以使用完全由以下内容组成的/etc/exports行:

/home/mwlucas

这没有任何选项和主机限制。当然,这种导出是愚蠢的,但我还是可以这么做。^(4)

编辑完exports文件后,告诉 mountd(8)重新读取该文件:

# service mountd reload

与 mountd(8)相关的问题会出现在/var/log/messages中。日志消息通常比较难以理解:虽然 mountd(8)会告诉你某行有问题,但通常不会说明原因。我遇到的最常见错误涉及符号链接。在目录中使用 pwd(1)可以获取目录的实际路径。

NFS 和用户

NFSv2 和 NFSv3 通过 UID 来识别用户。(NFSv4 使用用户名,因为它假设你已在整个网络中同步了用户名。)例如,在我的笔记本上,用户 mwlucas 的 UID 是 1001。在 NFS 服务器上,mwlucas 也有 UID 1001。这使得我的工作变得简单,因为我不必太担心文件所有权;我在服务器上的权限与在笔记本上的权限相同。

这在大规模网络中可能成为一个问题,尤其是当用户在自己的机器上拥有 root 权限时。解决这个问题的最佳方法是通过 Kerberos 创建一个授权用户的中央存储库。在小型网络或用户数量有限的 NFS 网络中,通常不会遇到这个问题;你可以同步系统上的/etc/master.passwd,或者在每个系统上为每个用户分配相同的 UID。

然而,root 用户的处理方式略有不同。NFS 服务器并不信任其他机器上的 root 用户执行服务器上的 root 命令。毕竟,如果入侵者突破了 NFS 客户端,你不希望服务器自动跟着崩溃。NFS 默认将来自客户端 root 账户的请求映射到服务器上的 UID 和 GID 为 –2 的账户。这就是 highly unprivileged nobody 账户的由来。

许多其他服务器程序的作者认为 nobody 账户是个好主意,因此他们将 nobody 用于自己的目的。多个安全实体同时以 nobody 身份运行会带来安全问题。FreeBSD 的软件包为所有需要的应用程序创建了无特权用户。我认为 nobody 用户已被污染,建议不要允许使用它。

你可以将 root 的请求映射到其他用户名。例如,你可以指定所有来自客户端 root 的请求将作为服务器上的 nfsroot 用户运行。通过小心使用组,你可以允许该 nfsroot 用户拥有有限的文件访问权限。使用 maproot 选项将 root 映射到其他用户。这里,我们将客户端上的 UID 0(root)映射到服务器上的 UID 5000:

/usr/home/mwlucas -maproot=5000

如果你真的希望客户端的 root 拥有服务器上的 root 权限,可以使用 -maproot 将 root 映射到 UID 0。这可能适用于你的家庭网络或测试系统。

你不能随意将用户账户映射到彼此。在复杂环境中,确保你同步网络中所有机器上的用户账户和 UID。

NFS 用户最多可以属于 16 个组。一些操作系统能够突破这个限制,但这样做违反了 NFS 协议。如果用户无法通过基于组的访问控制访问文件,请检查他们所属的组数。

编辑 exports 文件后,请记得重新启动 mountd(8)。

导出多个目录

标准的 FreeBSD UFS 安装将所有文件放在一个分区上。你可能希望导出该分区上的多个目录。在 /etc/exports 文件中,列出同一分区上的所有目录,并将它们与第一个导出的目录放在同一行上,之间用空格隔开。以下是一个具有多个导出的 /etc/exports 示例:

/usr/home/mwlucas /usr/src /var/log /usr/ports/distfiles -maproot=nfsroot

客户端可以挂载这些目录中的任何一个,来自 root 的请求会映射到 nfsroot。

行的各个部分之间没有标识符、分隔符或定界符。是的,如果我们能将每个共享目录放在自己的一行上,阅读会更容易,但我们做不到——它们都在同一个分区上。FreeBSD 团队可以重写它,使其结构更清晰,但那样的话,FreeBSD 的/etc/exports就与其他 Unix 系统不兼容了。

也许你希望客户端能够挂载分区上的任何目录。可以使用-alldirs选项允许此操作。我不会在只有一个分区的主机上这样做。

/home -alldirs

你只能使用-alldirs指定一个分区的挂载点。

长行

与许多其他配置文件一样,你可以使用反斜杠将一行配置分成多行。你可能会发现前面的配置在如下所示的情况下更易于阅读:

/usr/home/mwlucas \
    /usr/src \
    /usr/obj \
    /usr/ports/distfiles \
    -maproot = 5000

一旦你的导出行变得足够长,这种样式突然比其他方式更具可读性。

限制客户端

要仅允许特定的客户端访问 NFS 导出,请将它们列在/etc/exports条目的末尾。在这里,我们将前面的共享限制为一个 IP 地址:

/usr/home/mwlucas /usr/src /usr/obj /usr/ports/distfiles \
    -maproot=5000 203.0.113.200

你还可以通过使用-network-mask限定符,将文件共享限制为某个特定网络上的客户端:

/usr/home/mwlucas /usr/src /usr/obj /usr/ports/distfiles \
    -maproot=5000 -network 203.0.113 -mask 255.255.255.0

这样,任何 IP 地址以 203.0.113 开头的客户端都可以访问你的 NFS 服务器。我使用类似的设置来快速升级客户端。我在 NFS 服务器上构建一个新的世界和内核,然后让客户端挂载这些分区,并通过 NFS 安装二进制文件。

要将导出到 IPv6 网络,请在地址中包含斜杠。

/usr/home/mwlucas -network 2001:db8:bad:c0de::/64

你也可以列出主机名而不是 IP 地址,但这会依赖于名称解析。如果丢失了 DNS,你将无法进行文件共享。此外,NFS 服务器在启动 mountd 时会查找每个主机的 IP 地址。如果客户端的 IP 发生变化,你需要重新加载 DNS 和 mountd(8)。如果必须列出主机名,请将它们放在行的末尾。

/usr/home/mwlucas www1 www2 www3

按每主机分配 NFS 会增加工作量。尽量在不妥协安全性的前提下,广泛地分配 NFS 权限。

客户端和导出组合

/etc/exports中的每一行都指定了从一个分区到一个网络、地址或主机集合的导出。不同的主机需要完全不同的导出声明。如果需要,你可以为每个主机更改选项。

/usr/home/mwlucas /usr/src /usr/obj /usr/ports/distfiles \
    -maproot=5000 203.0.113.200
/usr -maproot=0 203.0.113.201

在这里,我将/usr的几个子目录导出给 IP 为 203.0.113.200 的 NFS 客户端。IP 为 203.0.113.201 的 NFS 客户端可以挂载整个/usr,甚至可以作为 root 进行挂载。

NFS 和防火墙

NFS 因不喜欢防火墙而著名。像 mountd(8)、rpc.lockd(8)和 rpc.statd(8)这样的服务动态端口分配,使得数据包过滤几乎不可能。你可以使用-p标志为这些服务分配特定的 TCP 端口。在这里,我使用rc.conf条目将 mountd(8)绑定到 4046 端口,将 rpc.lockd(8)绑定到 4045 端口,将 rpc.statd(8)绑定到 4047 端口:

mountd_flags="-r -p 4046"
rpc_lockd_flags="-p 4045"
rpc_statd_flags="-p 4047"

我可以在我的数据包过滤规则中使用这些端口,为我的 NFS 服务器提供一些保护。

使用 zfs(8)管理 NFS

使用 zfs(8)管理 NFS 有优点也有缺点。你可以按数据集配置 NFS,而且每次更改后不需要手动重启 mountd(8)。命令行配置更容易自动化,很多人发现它也更容易输入。

使用sharenfs属性来启用、禁用和配置 NFS 导出。将该属性设置为on,可以全局共享一个数据集及其所有后代。这相当于在/etc/exports中单独列出该数据集。世界上的任何人都可以挂载这个数据集或它的任何子目录,除非你有其他访问控制,如防火墙。

# zfs set sharenfs=on zroot/home

类似地,设置为off可以取消共享该数据集。

你可能希望在导出上设置一些 NFS 选项。将sharenfs设置为所需的选项。这个例子设置了一个 maproot 用户,并将客户端限制在我的本地网络内。将选项放在引号中。

# zfs set sharenfs="-network 203.0.113.0/24 -maproot=nfsroot" zroot/home

使用 ZFS 管理 NFS 导出的一个问题是,所有允许的主机都会获得相同的选项。也就是说,如果大多数主机需要使用-maproot=nfsroot挂载/home,但你有一台主机需要以 root 身份挂载该数据集,你就不能使用 ZFS 属性。同样,ZFS 属性只允许定义一个被允许的网络。

启用 NFS 客户端

配置 NFS 客户端要简单得多。在/etc/rc.conf中,添加以下内容:

nfs_client_enable="YES"

你可以重启或运行service nfsclient start。这两种方法都会启动 NFS 客户端功能。

显示可用挂载点

对于 NFS 客户端来说,显而易见的问题是:“我可以从这个服务器挂载什么?”showmount(8)命令列出了客户端可以访问的所有导出。给出-e标志和 NFS 服务器的名称。在这里,我询问 storm 服务器提供了哪些导出:

# showmount -e storm
Exports list on storm:
/usr/home                          203.0.113.0

该客户端被允许根据允许网络 203.0.113.0 的规则挂载/usr/home

运行 showmount(8)不会提供任何服务器端选项,如-maproot。这些细节对客户端来说并不容易获取,尽管 touch(1)可以让你轻松测试只读导出。

挂载导出

现在,你可以挂载 NFS 服务器导出的目录或文件系统。与其使用设备名称,不如使用 NFS 服务器的主机名和你想要挂载的目录。例如,要将我的 storm 服务器上的/home/mwlucas目录挂载到/mnt目录,我会运行:

# mount storm:/usr/home/mwlucas /mnt

之后,用 df(1)命令测试你的挂载。

# df -h
Filesystem            Size    Used   Avail Capacity  Mounted on
--snip--
storm:/usr/home       891G    2.7G    888G     0%    /mnt

NFS 挂载的目录显示为一个普通分区,我可以随意读取和写入文件。

NFS 挂载选项

FreeBSD 使用保守的 NFS 默认设置,以便与任何其他类 Unix 操作系统互操作。你可以使用挂载选项来调整 FreeBSD 挂载 NFS 导出的方式。你可以在命令行使用-o选项,或者将它们添加到/etc/fstab条目中。

如果你需要访问一个仅支持 UDP 的 NFS 服务器,可以使用挂载选项udp来使用 UDP,而不是默认的 TCP。

程序期望文件系统不会消失,但在使用 NFS 时,可能会发生服务器从网络中消失的情况。这会导致客户端上试图访问 NFS 文件系统的程序永久挂起。通过将 NFS 挂载设置为可中断,你将能够通过 CTRL-C 中断挂起的进程。使用 intr 设置可中断性。

通过使用软挂载,FreeBSD 会通知程序它们正在处理的文件不再可用。程序如何处理这些信息取决于程序本身,但它们将不再无限期挂起。使用soft选项启用软挂载。

如果你想要只读挂载,请使用ro挂载选项。

综合来看,我可能会将我的主目录挂载为可中断的软挂载。^(5)

# mount -o soft,intr storm:/usr/home/mwlucas /mnt

我可以像这样将其添加到/etc/fstab文件中:

storm:/usr/home/mwlucas  /mnt  nfs  rw,soft,intr  0  0

虽然 NFS 对于简单使用来说非常直接,但你可能会花费很多时间进行调整、优化和增强。如果你希望建立一个复杂的 NFS 环境,不要完全依赖这个简短的介绍,而是花时间阅读一本关于这个主题的好书。

现在,让我们来看看读取 Windows 共享。

通用互联网文件系统

如果你在一个典型的办公网络中,标准的网络文件共享协议是微软的通用互联网文件系统(CIFS)。你可能知道 CIFS 作为服务器消息块(SMB)、"网络邻居"或"为什么我无法挂载那个驱动器?"最初仅由 Microsoft Windows 系统提供,但这个协议已经变成了某种伪标准。

FreeBSD 包含 smbutil(8) 程序,用于查找、挂载和作为 CIFS 客户端使用 CIFS 共享。FreeBSD 在基础系统中不包括 CIFS 服务器,但开源 CIFS 服务器 Samba (www.samba.org/)在 FreeBSD 上运行良好。

使用 FreeBSD 的 CIFS 支持与现有的 Microsoft 基础设施进行互操作。不要部署 CIFS 来支持类似 Unix 的系统。

先决条件

在开始使用 Microsoft 文件共享之前,请收集以下有关 Windows 网络的信息:

  • 工作组或 Windows 域名

  • 有效的 Windows 用户名和密码

  • Windows DNS 服务器的 IP 地址

内核支持

FreeBSD 使用多个内核模块来支持 CIFS。smbfs.ko 模块支持基本的 CIFS 操作。libmchain.kolibiconv.ko 模块提供支持函数,并在加载 smbfs.ko 时自动加载。你可以将这些静态编译到内核中:

options    NETSMB
options    LIBMCHAIN
options    LIBICONV
options    SMBFS

你可以通过在boot/loader.conf文件中添加条目来在启动时自动加载这些设置。

smbfs_load=YES

你现在可以配置 CIFS。

配置 CIFS

CIFS 依赖于一个配置文件,通常是$HOME/.nsmbrc/etc/nsmb.conf。所有在/etc/nsmb.conf中的设置都会覆盖用户主目录中的设置。配置文件通过方括号中的标签分为多个部分。例如,适用于每个 CIFS 连接的设置位于[default]部分。创建自己的部分以指定服务器、用户和共享,格式如下:

[servername]
[servername:username]
[servername:username:sharename]

适用于整个服务器的信息放在以服务器名命名的部分中。适用于特定用户的信息保存在用户名部分中,适用于单个共享的信息保存在包含共享名称的部分中。如果你没有更具体的每用户或每共享信息,可以将所有共享的信息放在一个普通的[servername]条目下。

配置条目使用来自 CIFS 系统的值——例如,Bert 的 Windows 用户名是bertjw,但他的 FreeBSD 用户名是xistence,所以我在nsmb.conf中使用bertjw

nsmb.conf 关键字

在适当的部分下,指定一个带有关键字和值的nsmb.conf配置。例如,服务器有 IP 地址而用户没有,所以在服务器部分仅使用 IP 地址分配。使用关键字时,用等号赋值,如keyword=value。以下是常用的关键字;有关完整列表,请参见 nsmb.conf(5)。

workgroup=string

workgroup 关键字指定要访问的 Windows 域或工作组的名称。这通常是所有服务器的默认设置。

workgroup=MegaCorp
addr=a.b.c.d

addr 关键字设置 CIFS 服务器的 IP 地址。此关键字只能出现在简单的 [servername] 标签下。如果你已经有工作中的 CIFS 名称解析,则不需要此项,但现实中有时并非如此。

nbns=a.b.c.d

nbns 关键字设置 NetBIOS(WINS)名称服务器的 IP 地址。你可以将此行放在默认部分或特定服务器下。如果你使用 Active Directory(基于 DNS),你可以使用 DNS 主机名。然而,添加一个 WINS 服务器不会影响你的配置,并有助于测试基本的 CIFS 设置。

password=string

password 关键字为用户或共享设置明文密码。如果必须将密码存储在/etc/nsmb.conf中,必须确保只有 root 用户可以读取该文件。在多用户系统中,在$HOME/.nsmbrc中存储密码是个坏主意。

你可以使用smbutil crypt对 Windows 密码进行加密,生成一个字符串,用于此关键字。加密后的字符串前面有双美元符号($$)。虽然这有助于防止有人意外发现密码,但恶意用户可以轻松地解密它。

# smbutil crypt superSecretPassword
$$1624a53302a6d

如果服务器需要访问 CIFS 共享以执行其常规任务,请不要使用你的账户。请向 Windows 团队申请为你的服务器提供一个账户,以避免你的账户问题干扰服务器功能。

示例配置

在这里,我创建了一个 nsmb.conf,允许 Bert 访问他在公司 CIFS 文件服务器上的文件。

[default]
nbns=203.0.113.12
workgroup=BigCorp
[FILESERVER:bertjw]
password=$$1624a53302a6d

在这种配置下,Bert 应该能够访问那些专制的 Windows 管理员允许的 CIFS 共享。

CIFS 名称解析

在 FreeBSD 挂载 CIFS 共享之前,它需要识别共享所在的主机。虽然微软已经使用 DNS 数十年,但典型的 Windows 环境通常支持多种旧版协议。通过 smbutil lookup 验证 smbutil(1) 是否能够找到 CIFS 服务器。

# smbutil lookup fileserver1
Got response from 203.0.113.12
IP address of ntserv1: 203.0.113.4

如果这有效,那么你已经具备了基本的 CIFS 功能。

其他 smbutil(1) 功能

你可以在命令行查看主机上的共享。首先登录到你的主机。

# smbutil login //unix@fileserver1
Password:

所以,我们的配置是正确的。让我们使用 smbutil 的 view 命令查看此服务器提供了哪些资源。

# smbutil view //unix@fileserver1
Password:
Share        Type       Comment
-------------------------------
IPC$         pipe       Remote IPC
ADMIN$       disk       Remote Admin
C$           disk       Default share
unix         disk
4 shares listed from 4 available

你将得到一个 CIFS 服务器上每个共享资源的列表。现在,假设你已经完成,退出服务器。

# smbutil logout //unix@fileserver

挂载共享

在你完成调查后,使用 mount_smbfs(8) 挂载共享。语法如下:

# mount_smbfs //username@servername/share /mount/point

我在这台 Windows 机器上有一个叫 MP3 的共享,我想从我的 FreeBSD 系统访问它。为了将它挂载到 /home/mwlucas/smbmount,我会这样做:

# mount_smbfs //unix@fileserver1/MP3 /home/mwlucas/smbmount

mount(8) 和 df(1) 程序会显示此共享已附加到你的系统,你可以像访问任何其他文件系统一样访问服务器上的文档。使用 umount(8) 来断开与服务器的连接。

其他 mount_smbfs 选项

mount_smbfs 包括多个选项来调整挂载 CIFS 文件系统的行为。使用 -f 选项可以选择不同的文件权限模式,使用 -d 选项可以选择不同的目录权限模式。例如,若要设置一个挂载,使得只有我可以访问该目录的内容,我会使用 mount_smbfs -d 700。这将使 FreeBSD 权限比 Windows 权限更严格,但我完全没问题。我可以通过 -u 选项更改文件的所有者,使用 -g 选项更改组。

微软的文件系统对大小写不敏感,但类 Unix 操作系统对大小写敏感。CIFS 默认保持原样,但这可能并不总是理想的。-c 标志会让 mount_smbfs(8) 修改文件系统中的大小写:-c l 会将所有字母改为小写,而 -c u 则会将所有字母改为大写。

nsmb.conf 选项

下面是不同情况下 nsmb.conf 配置项的示例。它们都假设你已经在配置中定义了工作组、NetBIOS 名称服务器,并且拥有访问 CIFS 共享的权限的用户名。

独立系统上的唯一密码

如果你有一台名为 desktop 的机器,并且有一个受密码保护的共享,你可以使用类似以下的命令。许多独立的 Windows 系统都有这种密码保护功能。

[desktop:shareusername]
password=$$1789324874ea87
访问第二个域

在这个例子中,我们访问的是一个名为 development 的第二个域。该域的用户名和密码与我们默认域的不同。

[development]
workgroup=development
username=support

CIFS 文件所有权

Unix-like 系统和 Windows 系统之间的文件所有权可能会出现问题。首先,你的 FreeBSD 用户名可能无法映射到 Windows 用户名,而且 Unix 的权限机制与 Windows 有很大不同。

由于你使用的是单一的 Windows 用户名来访问共享资源,因此你只能获得该帐户对 Windows 资源的访问权限,但必须为挂载的共享分配正确的 FreeBSD 权限。默认情况下,mount_smbfs(8)将为新的共享分配与挂载点相同的权限。在我们之前的示例中,目录/home/mwlucas/smbmount由用户mwlucas拥有,并且权限为 755。这些权限表示 mwlucas 可以编辑该目录中的内容,但其他人不能。尽管 FreeBSD 表示该用户可以编辑这些文件,但 Windows 仍可能不允许该特定用户编辑它所共享的文件。

提供 CIFS 共享

正如 FreeBSD 可以访问 CIFS 共享一样,它也可以通过 Samba 将共享提供给 CIFS 客户端。你可以在包集合中找到几个版本较新的 Samba。Samba 官网 www.samba.org/ 提供了许多有用的教程。从 FreeBSD 提供 CIFS 共享比访问它们要复杂得多,因此我们将在此结束讨论,以免本书变得更厚。

我们现在已经完成了对 FreeBSD 文件系统的介绍。虽然我已经花了几章来讲解这个主题,但 FreeBSD 还有几个额外的文件系统选项、一个自动挂载工具,甚至支持 Filesystem in Userspace(FUSE),用于访问 NTFS、Linux 的 extfs 等。它还具有特殊的 iSCSI 支持和像 nullfs(5)这样的特殊文件系统,使得在大规模管理监狱(jails)时非常强大。不过,如果我再花更多时间讲解文件系统,你们可能会追踪到我并用钝器表达你们的不满,所以让我们继续讨论 FreeBSD 的一些高级安全特性。

第十四章:探索 /etc**

image

/etc目录包含了启动类 Unix 系统所需的基本配置信息。每当我接触到一个陌生的系统时,我做的第一件事之一就是查看/etc。从初级系统管理员成长为中级系统管理员的最快途径之一就是阅读/etc和相关的手册页面。是的,全部都要读。是的,这是一大堆阅读材料。理解/etc意味着你理解了系统的整体结构。随着你作为系统管理员的进步,你将会零散地获得这些信息,所以最好一开始就通过这个更轻松的路径,掌握这部分工具。

我在各个章节中讨论了许多/etc文件,通常是在它们最重要的地方,比如在第七章中讨论/etc/services,以及在第十章中讨论/etc/fstab。此外,一些文件仅有历史意义或正在逐渐被移除。本章涵盖了其他章节没有涵盖的重要/etc文件。

/etc 在类 Unix 系统中的表现

不同的类 Unix 系统使用不同的/etc文件。在许多情况下,这些文件只是最初的 BSD 系统中的文件重新命名或重构的结果。例如,当我第一次遇到 IBM AIX 系统时,我去寻找一个 BSD 风格的/etc/fstab。结果并没有找到。稍作查找,我发现了/etc/filesystems,它是 IBM 特定的/etc/fstab。显然,IBM 认为“文件系统表”(filesystem table)缩写的文件名让人困惑,所以他们重新命名了该文件。知道这个信息存在于/etc的某个地方,并且知道哪些文件显然不包含这些信息,极大地缩短了我的搜索时间。

即使是完全不同的 FreeBSD 系统,它们的/etc目录也几乎相同。尽管一些附加程序会在这里插入自己的文件,但你可以期望每个你遇到的 FreeBSD 系统中都会有某些特定的文件。

记住,/etc是 FreeBSD 的核心,修改这些文件可能会破坏或摧毁你的系统。虽然手动恢复一个混乱的文件系统能够将一个普通的系统管理员培养成一个相当不错的管理员,但这绝对是最不愉快的成长方式之一。

/etc/adduser.conf

该文件允许你配置新用户的默认设置。详情请参见第九章。

/etc/aliases

该文件允许你配置系统范围的电子邮件转发。我们将在第二十章中讨论它。

/etc/amd.map

FreeBSD 具备在需求时通过自动挂载守护进程 amd(8)自动挂载和卸载 NFS 文件系统的能力。然而,自动挂载守护进程非常老旧,已经在很大程度上被 autofs(5)和 automountd(8)替代。自动挂载主要对工作站有用,因此我们不再深入探讨。

/etc/auto_master

auto_master文件配置了 FreeBSD 的现代自动挂载服务。它允许你配置挂载选项,确定自动挂载的文件系统应保持挂载多长时间,等等。详情请参见 auto_master(5)、autofs(5)和 automountd(8)。

/etc/blacklistd.conf

FreeBSD 包含一个自动化黑名单守护进程 blacklistd(8),类似于 fail2ban 等程序。链接到 libblacklist(3) 的程序可以指示 blacklistd(8) 在防火墙上阻止有害主机。我们在第十九章中配置 blacklistd(8)。

/etc/bluetooth、/etc/bluetooth.device.conf 和 /etc/defaults/bluetooth.device.conf

FreeBSD 支持蓝牙,它是一个短距离无线通信标准。与 802.11 不同,蓝牙设计用于提供短距离但高水平的服务,如语音通信。本书主要讲解服务器内容,因此我们不会讨论蓝牙,但你应该知道,如果你愿意,你的 FreeBSD 笔记本可以连接到蓝牙手机并接入互联网。

/etc/casper

Capsicum 安全系统允许程序员为软件添加沙箱和安全功能。/etc/casper 目录包含示例 capsicum(4) 配置。

/etc/crontab 和 /etc/cron.d

cron(8) 守护进程让用户可以调度任务。请参阅第二十章以获取示例和详细信息。

/etc/csh.*

/etc/csh.* 文件包含 csh 和 tcsh 的系统默认设置。当用户使用其中一个 shell 登录时,shell 会执行 /etc/csh.login 中的任何命令。类似地,当用户注销时,/etc/csh.logout 会被执行。你可以将一般的 shell 配置信息放入 /etc/csh.cshrc 中。

每个 shell 配置容易出错,并且你必须为其他 shell 保持一致的设置。我建议将必要的环境变量放入登录类中(请参阅 “限制系统使用” 在第 188 页)。

/etc/ddb.conf

内核调试器配置工具 ddb(8) 读取 ddb.conf 文件中的指令。我们将在第二十四章中使用它,为小型系统准备内核崩溃转储。

/etc/devd.conf

设备守护进程 devd(8) 主要用于管理可拆卸硬件,如 USB、PCCard 和 Cardbus 设备。当你将 USB 网卡插入笔记本电脑时,devd(8) 会检测到设备到达,并启动相应的系统进程,根据 /etc/rc.conf 配置网卡。更一般来说,它是一个状态变化守护进程,可以对链路上下事件做出反应,通知你 CPU 过热、进程挂起/恢复等事件。我们在第十三章简要讨论了 devd(8),但如果你认为需要在服务器上编辑 /etc/devd.conf,你可能是在做错事。

/etc/devfs.conf、/etc/devfs.rules 和 /etc/defaults/devfs.rules

FreeBSD 通过 devfs(5) 管理设备节点,这是一个虚拟文件系统,能够动态提供设备节点,随着硬件启动、出现和消失。请参阅第十三章了解更多信息。

/etc/dhclient.conf

许多操作系统提供非常基础的 DHCP 客户端配置,无法进行精细调节或自定义;你只能选择使用或不使用。在大多数情况下,一个空的 /etc/dhclient.conf 文件可以提供完整的 DHCP 客户端功能,但在所有情况下都无法正常工作。也许你的网络出现了问题,或者你正在参加一个会议,某个脚本小子觉得在他的机器上设置第二个 DHCP 服务器并通过它路由所有流量很有趣,这样他就可以捕获密码。

你的服务器最好不要通过 DHCP 配置(除非它是无盘系统),因此我们不会深入探讨此内容。不过你应该知道,你可以配置 FreeBSD 的 DHCP 客户端功能。

/etc/disktab

曾几何时,硬盘是稀有而神秘的存在,只存在几种不同类型。在 /etc/disktab 文件中,你会找到各种磁盘的低级描述,从 360KB 软盘到 60MB 的松下笔记本硬盘。(是的,笔记本电脑曾配有 60MB 的硬盘,而我们当时可高兴坏了。)

今天,这个文件主要用于可移动媒体,如 1.44MB 软盘和 Zip 磁盘。虽然我在第十三章中描述了如何格式化闪存驱动器,但该文件包含了格式化其他可移动媒体所需的描述。如果你想在 LS 120 磁盘或 Zip 驱动器上创建文件系统,你会在条目开头找到必要的标签。

编辑 /etc/disktab 只有在你有多个相同的硬盘,并希望它们按完全相同的方式进行分区和格式化时才有用。如果你需要添加自己的条目,请阅读 disktab(5)。

/etc/dma/

Dragonfly 邮件代理(DMA)将其配置存储在 /etc/dma/ 中。我们在第二十章中讨论了 DMA。

/etc/freebsd-update.conf

该文件在通过 freebsd-update(8) 获取服务器的二进制更新时使用。有关详细信息,请参阅第十八章。

/etc/fstab

有关文件系统表的讨论,请参阅第十章,以及 /etc/fstab

/etc/ftp.*

FTP 守护进程 ftpd(8) 使用这些文件来确定谁可以通过 FTP 访问系统,以及在成功连接后他们可以进行何种访问。除非你运行的是一个大型 FTP 站点,否则你应该使用 sftp(1)。

/etc/group

将用户分配到不同的组在第九章中有详细的说明。

/etc/hostid

某些软件要求每台主机都有一个全球唯一的 ID,或 UUID。如果你在实际硬件上运行,该 UUID 会被烧录到主板中,并可以通过 kenv(8) 访问。虚拟主机可以通过软件生成 UUID。/etc/hostid 文件包含了该 UUID。

/etc/hosts

该文件包含主机到 IP 的映射,详见第八章。

/etc/hosts.allow

/etc/hosts.allow 文件控制哪些人可以访问支持 TCP Wrappers 的守护进程。请在第十九章中了解相关内容。

/etc/hosts.equiv

/etc/hosts.equiv 文件由 r 服务(如 rlogin、rsh 等)使用,允许受信任的远程系统在不提供密码或甚至不登录的情况下登录或在本地系统上运行命令。列在此文件中的主机被假定已经在受信任的系统上进行了用户身份验证,因此本地系统无需再次验证用户。

这种明显的信任在友好的网络中非常方便,就像把你曼哈顿市区联排别墅的门锁开着一样,每次回家时都省去了寻找门钥匙的麻烦。然而,所谓的“友好网络”并不存在。^(1) 一个不满的员工可以通过此服务几乎摧毁一个企业网络,任何使用 r 服务的机器对于第一个路过的脚本小子来说几乎就是“狗肉”。事实上,/etc/hosts.equiv 及其相关服务曾经让即使是顶级安全专家也吃了亏,他们曾认为可以安全地使用它。我建议将此文件保持为空,甚至可以将其设置为不可变(参见 第九章)。

/etc/hosts.lpd

/etc/hosts.lpd 文件是 /etc 目录中最简单的文件之一。这里列出的每个主机(每行一个)可能会打印到由此计算机控制的打印机上。虽然你可以使用主机名,但这可能会导致 DNS 问题影响打印,因此建议使用 IP 地址。

与大多数其他配置文件不同,/etc/hosts.lpd 不接受网络号或子网掩码;你必须列出单独的主机名或 IP 地址。

我们在 第二十章 中配置 FreeBSD 作为打印机客户端。

/etc/inetd.conf

inetd(8) 处理较小的守护进程的传入网络连接,这些守护进程不会频繁运行。请参阅 第二十章 中关于 inetd 的部分。

/etc/libmap.conf

FreeBSD 的链接器允许你用一个共享库替换另一个共享库。我们在 第十七章 中讨论了这一点。

/etc/localtime

该文件包含由 tzsetup(8) 配置的本地时区数据。它是一个二进制文件,无法使用普通工具编辑。tzsetup(8) 实际上是从 /usr/share/zoneinfo 的一个子目录中复制了此文件。如果你的时区发生变化,你需要升级 FreeBSD 以获取新的时区文件,然后重新运行 tzsetup(8) 来正确配置时间。

我们在 第二十章 中讨论时间。

/etc/locate.rc

locate(1) 查找给定名称的所有文件。例如,要查找 locate.rc,请输入以下内容:

# locate locate.rc
/etc/locate.rc
/usr/share/examples/etc/locate.rc
/usr/src/usr.bin/locate/locate/locate.rc
/var/db/etcupdate/current/etc/locate.rc

你会发现名为 locate.rc 的文件可以在四个地方找到:主 /etc 目录、系统示例目录、系统源代码以及由 etcupdate(8) 保留的副本。

作为 periodic(8) 每周运行的一部分(参见 第二十一章),你的 FreeBSD 系统会扫描磁盘,构建它找到的所有内容的列表,并将该列表存储在数据库中。构建列表的程序 locate.updatedb(8) 会从 /etc/locate.rc 获取其设置。此文件中的以下变量都会改变你的 locate.updatedb(8) 如何构建 locate 数据库:

  • TMPDIR 包含 locate.updatedb(8) 使用的临时目录,默认为 /tmp。如果 /tmp 空间不足,请将此路径更改为一个有更多空间的地方。

  • 虽然你可以通过 FCODES 变量更改数据库的位置,但这会影响 FreeBSD 其他期望在默认位置找到该数据库的部分。做好准备面对一些奇怪的结果,尤其是在你将旧的 locate 数据库保留在默认位置 /var/db/locate.database 时。

  • SEARCHPATHS 变量指定了你希望开始构建数据库的目录。默认情况下,它是 /,即整个磁盘。要只索引磁盘的一部分,请在此设置该值。

  • PRUNEPATHS 列出了你不希望索引的目录。默认情况下,排除了传统上仅包含短期文件的临时目录。

  • FILESYSTEMS 变量列出了你希望索引的文件系统类型。默认情况下,locate.updatedb(8) 只索引 UFS(FreeBSD)和 ext2fs(Linux)文件系统。列出 NFS(参见 第十三章)文件系统并不是一个好主意:所有服务器同时索引文件服务器会导致网络或文件服务器的瓶颈。

/etc/login.*

你可以通过使用 /etc/login.access/etc/login.conf 控制谁可以登录到你的系统,以及这些用户可以访问哪些资源。请参见 第九章 的说明。

/etc/mail

/etc/mail 中的大部分内容用于 Sendmail。两个例外是 aliases(5) 文件和 mailer.conf(5)。我们在 第二十章 中讨论了这两个文件。

/etc/mail.rc

虽然 FreeBSD 使用大多数 .rc 文件进行系统启动,/etc/mail.rc 文件则用于配置 mail(1)。

/etc/mail/mailer.conf

FreeBSD 允许你通过 /etc/mail/mailer.conf 选择任何你喜欢的邮件服务器程序,具体内容请参见 第二十章。

/etc/make.conf

编译一个程序是指将其从源代码构建成机器语言。我们将在 第十七章 中详细讨论构建软件。/etc/make.conf 包含控制构建过程的设置,让你可以设置直接影响软件构建的选项。记住,添加到 make.conf 的任何内容都会影响系统上构建的所有软件,包括系统升级。这可能会导致升级失败。^(2) 许多 make.conf 中的选项对开发者才有用。

如果你想设置只影响系统升级的选项,请改用 /etc/src.conf

下面是一些在make.conf中设置的常见功能。这里设置的任何值都需要使用 make(1)所使用的相同语法。如果你坚持尝试优化软件构建,按照 make.conf(5)中的示例或软件文档中的示例操作。然而,最好的做法是完全不要修改 make。

CFLAGS

这个选项指定了构建非内核程序时的优化设置。许多其他类 Unix 操作系统建议使用特定的编译器标志CFLAGS来编译软件。在 FreeBSD 中,强烈不推荐这种做法。需要编译器标志的系统组件已经在软件配置中指定了该标志,附加软件的配置也为其单独设置了该标志。虽然有人可能会推荐其他CFLAGS设置,但 FreeBSD 项目不支持自定义选项。

一般来说,FreeBSD 代码期望在开箱即用时就能正确编译。唯一的作用是添加编译器选项可能会损害你的性能。如果你使用非标准标志构建 FreeBSD 或端口并遇到问题,移除这些标志并重新构建。

COPTFLAGS

COPTFLAGS优化仅用于构建内核。同样,非默认设置可能会导致无法正常工作的内核。

CXXFLAGS

CXXFLAGS告诉编译器在构建 C++代码时使用哪些优化。使用CXXFLAGS时,请务必使用+=语法,这样你就能将你的指令添加到软件中指定的指令上。我之前提到的关于CFLAGS的所有内容同样适用于CXXFLAGS

/etc/master.passwd

该文件包含所有用户账户的机密核心信息,详情请参见第九章。请妥善保护它。

/etc/motd

每日信息(motd)文件在用户登录时显示。你可以在这个文件中放置系统通知或其他你希望 Shell 用户看到的信息。/etc/login.conf中的welcome选项(参见第九章)可以指向不同的 motd 文件,因此你可以为每个登录类设置不同的消息。

/etc/mtree

mtree(1)构建具有权限设置的目录层次结构,这些权限根据预定义的标准设置。/etc/mtree目录存储 FreeBSD 基础系统的该标准。FreeBSD 的升级过程使用 mtree 记录来正确安装系统。如果你破坏了基础系统中文件或目录的权限,可以使用 mtree(1)将其恢复到默认值。虽然你通常不需要编辑这些文件,但如果你过度修改系统,这些文件可能会很有用。无盘系统使用这些文件来构建基于内存的/var文件系统。我们将在第十九章中使用这些信息检查系统安全性。

/etc/netconfig

如果你习惯了基于 SVR4 的操作系统,你可能会期望在/etc/netconfig中配置各种网络部分。但 FreeBSD 仅将此文件用于 RPC 代码。我提到它只是为了防止老一辈的 Solaris 用户误以为在这里进行更改能帮助他们。

/etc/netstart

这个 Shell 脚本专门用于在单用户模式下启动网络。在单用户模式下使用网络对许多原因都非常有用,从挂载 NFS 共享到连接远程机器以验证配置。只需运行/etc/netstart。在完全多用户模式下,这个脚本没有任何效果。

/etc/network.subr

这个 Shell 脚本不是为了人类使用的;相反,其他网络配置脚本使用这里定义的子例程来支持常见功能。

/etc/newsyslog.conf

这个文件配置日志文件的旋转和删除。有关更多信息,请参阅第二十一章。

/etc/nscd.conf

nscd(8)服务缓存名称服务查找的结果,以优化系统性能。如果你运行 LDAP,它很有用,但对于主机名查找,你最好使用本地缓存解析器。

/etc/nsmb.conf

FreeBSD 的 Windows 文件共享挂载系统使用/etc/nsmb.conf来定义对 Windows 系统的访问,正如第十三章中所述。

/etc/nsswitch.conf

名称服务切换在第八章和第二十章中有所讨论。

/etc/ntp/, /etc/ntp.conf

保持主机时间的准确性简化了……嗯,一切。时间守护进程 ntpd(8)使用这些文件,正如第二十章所示。

/etc/opie*

一次性密码系统 (OPIE) 是一个源自 S/Key 的一次性密码系统。虽然它仍然在一些地方使用,但已经不再非常流行。如果你感兴趣,可以阅读 opie(4)。大部分情况下,OPIE 已经被像 Kerberos、Google Authenticator 和其他 PAM 插件这样的系统所替代。

/etc/pam.d/*

可插拔认证模块 (PAM) 允许系统管理员使用不同的认证、授权和访问控制系统。如果你正在使用 Kerberos、LDAP 或其他某种集中认证系统,你需要配置 PAM。遗憾的是,PAM 本身就可以写成一本书。如果你不得不涉及到 PAM,允许我推荐我的书《PAM 精通》(Tilted Windmill Press, 2016)。

/etc/passwd

这个文件包含用户可见的账户信息。我们在第九章中讨论了密码文件。

/etc/pccard_ether

这个脚本用于启动和停止可移动网络卡,比如 Cardbus 卡和 USB 以太网卡。它的名字只是历史的遗留物,那时只有 PC 卡可用。在大多数情况下,devd(8)会根据需要运行这个脚本,正如第十三章所讨论的。

/etc/periodic.conf 和 /etc/defaults/periodic.conf

系统的常规维护任务会生成那些烦人的根用户邮件,这些任务由 periodic(8) 运行,它只会运行存储在 /etc/periodic/usr/local/etc/periodic 中的 shell 脚本。每个脚本都可以在 /etc/periodic.conf 中启用或禁用。

periodic(8) 每天、每周或每月运行程序。每组程序有其自己的设置——例如,日常程序的配置与每月程序的配置是分开的。这些设置通过 /etc/periodic.conf 中的条目来控制。虽然我们只展示了日常脚本的示例,但你会发现每周和每月脚本有非常类似的设置。

daily_output=”root”

如果你希望将状态电子邮件发送到 root 以外的用户,请在此列出该用户的用户名。除非你有一个专门负责阅读周期性电子邮件的用户,否则最好保持默认设置,并将 root 的电子邮件转发到你阅读的帐户。你也可以提供一个文件的完整路径,如果你愿意的话,甚至可以使用 newsyslog(8) 来轮转周期性日志(见 第十九章)。

daily_show_success=”YES”

将此设置为 YES 时,每日信息中会包括所有成功检查的信息。

daily_show_info=”YES”

设置为 YES 时,每日信息中会包括它运行的命令的常规信息。

daily_show_badconfig=”NO”

设置为 YES 时,每日信息中会包括周期性命令的运行信息,这些命令尝试执行但未能成功。这些信息通常是无害的,涉及到你系统不支持或不包含的子系统。

daily_local=”/etc/daily.local”

你可以定义自己的脚本,作为每日、每周和每月的 periodic(8) 任务的一部分。默认情况下,这些脚本位于 /etc/daily.local/etc/weekly.local/etc/monthly.local,但你可以将它们放在任何你喜欢的位置。

每个位于 /etc/periodic 下的 daily、weekly 和 monthly 子目录中的脚本文件,在文件顶部都有简短的描述,大多数脚本在 /etc/defaults/periodic.conf 中有配置选项。快速浏览这些文件,寻找对你有用的内容。启用的默认设置对于大多数情况来说是合理的,但你可以通过在 /etc/periodic.conf 中简单的设置来启用额外的功能。例如,如果你使用基于 GEOM 的磁盘功能,你会发现每日的 GEOM 状态信息很有用。由于我列举的任何内容在我交付这份手稿之前就可能已经过时,更不用说在书籍送到你手里之前了,因此我不会详细介绍各个脚本。

/etc/pf.conf, /etc/pf.os

我们在 第十九章 中介绍了 PF 包过滤器的基础知识。

PF 的一个较少为人知的特性是它能够通过发送的数据包识别操作系统。/etc/pf.os文件包含不同操作系统的 TCP 指纹,允许您编写防火墙规则,例如“显示 FreeBSD 用户我的真实主页,但向 Windows 用户建议他们获取一个真正的操作系统”。更多信息,请参阅 pf.os(5)。我建议您阅读此文件,即使只是为了强调所有这些网络堆栈的行为是如此不同。

/etc/phones

模拟调制解调器用户可以在/etc/phones中存储远程调制解调器的电话号码,并为它们创建别名,以便他们只需输入home而不是完整的电话号码。然而,只有 tip(1)和 cu(1)使用此文件,因此它并不像您想象的那样有用。

/etc/portsnap.conf

Portsnap 提供端口树的更新,如第十八章所述。

/etc/ppp/

FreeBSD 支持使用 ppp(8)进行出站调制解调器。阅读手册页以获取更多信息。

/etc/printcap

此文件包含打印机配置信息。在类 Unix 系统上打印可能非常复杂,尤其是您可以使用各种各样的打印机。然而,使您的 FreeBSD 机器发送打印作业到打印服务器并不难。我们在第二十章中涵盖了这个主题。

/etc/profile

/etc/profile文件包含/bin/sh shell 的默认帐户配置信息,类似于 csh 和 tcsh 用户的/etc/csh.**。每当/bin/sh用户登录时,他都会继承此文件中的内容。用户可以用自己的.profile覆盖/etc/profile*。Bash 和其他 sh 衍生版本也使用此文件。

虽然 tcsh 是标准的 FreeBSD shell,但 sh 及其衍生物(尤其是 bash)非常流行。保持/etc/profile/etc/csh.login中的设置同步,以便将来进行故障排除,或者更好地在登录类中设置必要的环境变量(见第九章),以便影响用户需要的任何 shell。

/etc/protocols

在第七章中,我们讨论了网络协议。/etc/protocols文件列出了您可能遇到的各种网络协议。请记住,TCP 或 UDP 端口号与协议号不同。

/etc/pwd.db

这是/etc/passwd 文件的数据库版本。它包含有关用户帐户的公共信息,如第九章所讨论。

/etc/rc*

每当系统引导到可以执行用户命令的阶段时,它都会运行 shell 脚本/etc/rc。此脚本挂载所有文件系统,启动网络接口,配置 devfs(5),查找和编录共享库,并执行设置系统所需的所有其他任务。我们在第四章讨论了 FreeBSD 启动系统。

不同系统的启动任务差异很大。带有三张 48 端口串行卡的终端服务器与网页服务器的工作方式完全不同。FreeBSD 并不像单一的 monolithic /etc/rc 脚本那样处理所有任务,而是将每个启动过程拆分成一个独立的 shell 脚本来处理特定的需求。

此外,你还会在/etc目录下找到一些脚本,如/etc/rc.firewall/etc/rc.initdiskless。这些脚本在当前启动系统出现之前的几年就已经被分开,仍然保留在它们的历史位置,因为移动它们并没有带来任何好处。

/et/regdomain.xml

无线卡根据使用地区的不同,受到不同的监管规则限制。这些卡读取regdomain.xml来了解它们可以使用的频率以及允许的发射强度。编辑此文件时请自行承担风险。

/etc/remote

本文件包含用于通过串行线连接远程系统的机器可读配置。如今,只有当你将系统用作串行客户端时,这个文件才有意义——例如,如果你想连接到串行控制台。我们在第四章中讨论了串行控制台。

/etc/resolv.conf

本文件允许你设置名称服务器、域名搜索顺序和更多的 DNS 客户端设置。详情请见第八章。

/etc/rpc

远程过程调用 (RPC) 是在远程计算机上执行命令的一种方法。类似于 TCP/IP,RPC 也有服务和端口号,而/etc/rpc包含这些服务及其端口号的列表。最常见的 RPC 消费者是 NFS,我们在第十三章中进行了讨论。

/etc/security/

该目录包含审计(8)安全工具的配置信息。

/etc/services

本文件包含网络服务及其相关的 TCP/IP 端口列表。我们在第七章中讨论了/etc/services

/etc/shells

本文件包含所有合法用户 shell 的列表,详见第九章。

/etc/skel/

/etc/skel/中,你会找到一些 shell 配置文件,这些文件会复制到新的用户账户中。

/etc/snmpd.config

FreeBSD 包含了一个基础的 SNMP 实现,我们在第二十章中进行了讨论。

/etc/spwd.db

本文件包含用户密码文件/etc/master.passwd的机密数据库。详见第九章中的所有细节。

/etc/src.conf

本文件包含用于从源代码构建 FreeBSD 的机器指令。这是仅针对源代码树的 make.conf。不过,设置在/etc/make.conf中的值也会影响从源代码构建 FreeBSD;区别在于,/etc/src.conf 只影响 FreeBSD 的构建,而不会影响端口和包的构建。详情请见第十八章。

/etc/ssh/

配置安全外壳软件套件(SSH)在/etc/ssh中。这包括客户端 ssh(1)和服务器端 sshd(8)。 第二十章介绍了 sshd(8)。

/etc/ssl/

FreeBSD 包含了 OpenSSL 加密软件。第十九章讨论了一些基本的使用方法和配置。/etc/ssl目录包含了大部分 OpenSSL 信息。

/etc/sysctl.conf

这个文件包含了在启动过程中设置的内核 sysctl 信息。请参见第六章。

/etc/syslog.conf, /etc/syslog.conf.d/

这个文件控制哪些数据进入你的系统日志,并且这些日志存储的位置。请参见第二十一章。

/etc/termcap, /etc/termcap.small

这个文件包含了不同终端类型的设置和功能。在终端有数十种不同类型,且厂商几乎每天都会发布新终端的时代,理解这个文件至关重要。然而,现如今世界大多数已经统一采用 vt100 作为标准,默认配置适合几乎所有人。

termcap 文件是指向/usr/share/misc/termcap的符号链接。在单用户模式下,这个文件可能不可用。FreeBSD 提供了/etc/termcap.small文件,以在单用户模式下提供终端信息。

/etc/ttys

这个文件包含了所有系统终端设备(包含命令提示符的窗口)。这个名字源自于终端曾经是物理电传打字机的时代,但今天大多数用户使用的是通过 telnet 或 SSH 生成的虚拟终端。

我们将在第二十三章中使用此文件设置串行登录。

/etc/unbound/

FreeBSD 随附 Unbound DNS 客户端。配置文件存放在/etc/unbound/。 第二十章讲解了如何将 Unbound 设置为本地 DNS 解析器。

/etc/wall_cmos_clock

这不是一个重要的文件,但由于我花时间挖掘它的作用,你也可以了解一下它。如果这个文件存在,FreeBSD 的时间保持程序已经确定硬件的 CMOS 时钟所保持的时间不是协调世界时(UTC)。如果文件缺失,CMOS 时钟将被设置为其他时间。它在 adjkerntz(8)中有说明。

/etc/zfs/

FreeBSD 的 ZFS 使用这个目录存储 NFS 集成信息。我们在第十三章中讨论 NFS。

如果你能深入了解整个/etc目录,你将比大多数系统管理员准备得更充分。现在让我们向你的服务器添加一些软件。

第十五章:使你的系统更有用

image

一个基本的 FreeBSD 安装包括正好足以让系统运行的内容,再加上一些 Unix 系统传统上包含的额外组件。你可以决定是否安装额外的程序或源代码。尽管 FreeBSD 随着时间的推移不断发展,但一个完整的基本安装大约占用 1GB 的空间——比 Windows 或商业 Linux 安装所需的磁盘空间要少得多。

这种简洁性的优点在于它仅包含必要的系统组件。当你知道没有任何你从未听说过、且永远不会使用的共享库能够导致你的问题时,调试变得简单得多。缺点是你必须决定需要哪些功能,并选择提供这些功能的软件。FreeBSD 通过端口和包简化了附加软件的安装。

端口和软件包

FreeBSD 支持两种安装附加软件的方式。一切都从端口集合开始,但大多数用户会更喜欢预配置的软件包。

FreeBSD 有一个构建附加软件的系统,叫做端口集合,或简称端口。端口让你从程序供应商提供的原始源代码开始,并按照你需要的方式构建软件,按需启用或禁用功能。对于经验丰富的用户来说,端口既快速又简单,但需要一定的 Unix 专业知识,对于新用户来说可能会感到有些吓人。

软件包是通过构建端口而生成的,使用端口维护者认为对最广泛的用户群体最有用的选项,并将它们打包成一个整体,方便安装。FreeBSD 项目有一整套系统,仅用于构建所有端口,打包并供用户下载和安装。软件包让你能够快速安装、卸载和升级附加软件。

互联网建议永远不会消失

论坛和邮件列表存档中有许多建议跳过软件包,直接使用端口。这已经不再正确;从技术角度和个人心态上看,软件包更为优选。旧的软件包系统 pkg_tools 有严重的限制。

忽略任何提到像 pkg_add(8)、pkg_delete(8)、pkg_create(8)等工具的推荐意见。它们已经过时了。

FreeBSD 高度灵活的软件包系统叫做package,pkg(8),或简单称为 pkg。软件包信息存储在 SQLite 数据库中,这使得你可以对软件包数据执行任意查询。在开发过程中,pkg 曾被称为pkgNG。这个名称已经消失多年,但仍然出现在一些旧文档和第三方软件中。不要让这个名字混淆你。^(1)

我们将首先讨论如何使用 pkg(8)管理系统,然后继续讨论如何通过端口定制软件。

软件包

软件包是来自 Ports Collection 的预编译软件,针对特定版本的 FreeBSD 进行打包。FreeBSD 项目提供了多个软件包集,并在公共仓库中发布,每几天更新一次。软件包是安装附加软件的最简单方式。任何没有法律分发限制的软件可能都可以作为软件包提供。

法律限制?软件可能具有任何许可证条款,包括一些非常奇怪的条款。有些软件的许可证禁止以任何非源代码形式进行分发。FreeBSD 在法律上无法将其打包。其他软件则只能以预编译形式合法分发。FreeBSD 通常会打包这类软件,将其作为预编译的二进制文件和 FreeBSD 特定的打包信息一起分发。

软件包是基于每个主要 FreeBSD 版本的最旧支持版本构建的。所有 FreeBSD 12 版本的软件包都是基于 FreeBSD 12 的最旧支持版本构建的,FreeBSD 13 的软件包是基于 FreeBSD 13 的最旧支持版本构建的,依此类推。这有助于减少、识别和解决 ABI 不兼容问题。

如果你需要建立自己的软件包仓库,查阅 Ports Collection(参见第十六章)以及附加软件包poudriere

软件包文件

最终,软件包包含文件。这些文件可能是二进制程序、文档、配置文件或软件可能需要的任何其他内容。这些文件被视为操作系统的一部分。不要手动编辑它们。

一个特殊的情况是,当软件包包含一个示例配置文件时。如果程序需要配置文件,软件包应该包括一个示例。你需要编辑该配置文件以适应你的需求——这就是配置文件的用途

FreeBSD 通过安装带有后缀.sample的配置文件来解决这个问题。我们的 Web 服务器配置文件通常会以httpd.conf.sample的形式出现。

如果没有生产配置文件,软件包安装时也会将示例文件复制到适当位置。这个文件可以由你来编辑。

如果你升级了一个软件包,pkg(8)会将当前的生产文件与旧的示例文件进行比较。如果示例文件与生产版本相同,升级会替换生产文件。如果文件有任何差异,pkg 只会更新示例文件。合并任何需要的更改到生产配置是你的工作。请注意,软件包升级总是会替换示例配置文件,因此如果旧的示例文件很重要,你需要特别保存它。

介绍 pkg(8)

与旧的打包系统不同,pkg(8)是一个单一程序,带有一大堆子命令。你将使用相同的程序来安装、卸载和调查软件包。所有对已安装软件包的更改必须以 root 身份运行。以下是如何安装所有有远见的系统管理员都需要的一个重要程序:

# pkg install emacs

那些对更优文本处理器抱有不合理偏见的人,可能想要将其删除。

# pkg delete emacs

所有包操作都使用 pkg(8) 命令。

虽然 pkg(8) 手册页记录了基本的 pkg 功能,但每个子命令都有自己的手册页,命名格式为 pkg- 加上子命令名。示例包括 pkg-install(8) 和 pkg-delete(8)。你也可以使用 pkg help 命令和子命令的名称获取帮助,例如,pkg help install

FreeBSD 并没有预装 pkg(8)。你需要安装它……作为一个包。等等,别尖叫——它比听起来要好得多

安装 pkg(8)

FreeBSD 随附了一个非常简单的包管理器 /usr/sbin/pkg,即 pkg(7)。它几乎没有足够的智能来找到 FreeBSD 当前的包管理器。它安装那个新的包管理器,并将所有包管理责任交给它。这使得 FreeBSD 在更新包管理器时具有灵活性。

当你第一次尝试安装一个包时,pkg(8) 会提示你安装包管理器。我发现在新服务器上需要安装 dmidecode 包,这样我才能从厂家获得坏电源的 RMA。(不用担心我如何找到 dmidecode 包——暂时跟着我走就好。)

# pkg install dmidecode

FreeBSD 运行 pkg(8) 并发现尚未安装包管理。

The package management tool is not yet installed on your system.
Do you want to fetch and install it now? [y/N]: y

默认的答案以大写字母显示。如果我按 n 和回车,pkg 会终止。如果我按 y 和回车,FreeBSD 将启动系统。

➊ Bootstrapping pkg from pkg+http://pkg.FreeBSD.org/FreeBSD:12:amd64/quarterly, please wait...
➋ Verifying signature with trusted certificate pkg.freebsd.org.2013102301... done
➌ Installing pkg-1.10.0_2...
   Extracting pkg-1.10.0_2: 100%
➍ Updating FreeBSD repository catalogue...
   meta.txz                                 : 100%  944 B     0.9kB/s    00:01
   packagesite.txz                          : 100%    6 MiB   2.0MB/s    00:03
   Processing entries: 100%
   FreeBSD repository update completed. 26059 packages processed.
   All repositories are up to date.
   Updating database digests format: 100%

安装开始时会从 FreeBSD 镜像站点下载当前的 pkg 工具 ➊。然后它会检查下载文件的数字签名 ➋。工具被解压并安装 ➌。然后 pkg 下载并安装可用包的目录 ➍。

包管理系统现在已安装。为了顺利运行,FreeBSD 随附的简单 pkg(8) 告诉新安装的包管理系统去安装你真正想要的程序。在这种情况下,我们的新 pkg(8) 为你安装了 dmidecode。

你可以单独安装包管理系统,而不添加其他包,通过运行 pkg bootstrap ——但说实话,没人会在命令行中这么做。当包管理系统已经安装时,运行 pkg bootstrap 不会做任何事情,因此它主要用于安装脚本中。

常见的 pkg 选项

虽然每个 pkg 子命令都有独特的功能,但有一些命令选项在几乎所有子命令中都能通用。

在默认配置下,pkg 在执行任何操作之前都会提示你确认。你可以通过 -y 标志告诉 pkg 执行操作而不提示你。

另一方面,也许你希望 pkg 显示如果你运行命令会做什么,但不实际执行任何操作。可以使用 -n 标志执行演练。例如,使用 -n 安装包时,会显示每个将要安装的包的名称,包括依赖包。然而,系统不会安装任何包,演练可以帮助你在维护窗口期间准备好变更。

许多 pkg 操作会生成大量输出。使用 -q 可以减少输出量。

-a 标志通常将命令应用于所有已安装的软件包。

最后,pkg 通常拒绝做无意义的事情或破坏系统的操作。-f 标志强制 pkg 执行你所说的操作。强制执行软件包活动通常是个坏主意,尽管不是总是如此。例如,你可能需要 -f 来强制重新安装损坏的软件包。

配置 pkg(8)

pkg(8) 程序设计得非常灵活。虽然每个子命令有很多选项,但你可以通过配置文件 /usr/local/etc/pkg.conf 为大多数程序建立定制但一致的行为。

pkg.conf 文件包含了为 pkg(8) 注释掉的默认值。它是一个很好的地方,可以查看系统在你完全没有修改它的情况下是如何运行的。配置采用 UCL 编写(见 第二章)。变量可以设置为整数、字符串(如文件路径)或布尔值,像 YESNOYESONTRUE 是同义词,NOOFFFALSE 也是同义词。所有这些都不区分大小写。

#PKG_DBDIR = "/var/db/pkg";
#PKG_CACHEDIR = "/var/cache/pkg";
#PORTSDIR = "/usr/ports";
#INDEXDIR = "";
--snip--

FreeBSD 在 pkg.conf 为空的情况下也能正常运行。默认配置包含许多被注释掉的条目和相当多的别名。你可以考虑在继续时参考这些示例设置。

大多数 pkg 操作提供一个是/否对话框,默认值显示为大写字母。为了保持保守,pkg 通常默认设置为 NO。可以通过 DEFAULT_ALWAYS_YES 选项将默认值更改为 YES

你可以通过添加 -y 标志使 pkg 命令假设你会对所有问题回答“是”。如果你不想每次都输入 -y,可以通过将 ASSUME_ALWAYS_YES 标志设置为 YES,让 pkg 始终假设你会回答“是”。

因为我懒但不鲁莽,所以我更喜欢这些 pkg.conf 设置:

DEFAULT_ALWAYS_YES = true;
ASSUME_ALWAYS_YES = false;

如果安装软件包时出现问题,可能需要调试输出。设置 DEBUG_LEVEL 可以开启调试输出。此变量接受从 0(不调试)到 4(完全调试)的整数值。

许多软件包在安装过程中包含脚本。通过将 DEBUG_SCRIPTS 设置为 YES,可以为每个脚本启用调试。

任何 pkg.conf 设置也可以用作环境变量。环境变量会覆盖配置文件中的任何设置。你可以像这样安装带有调试的包:

   # env DEBUG_LEVEL=4 pkg upgrade

所有选项都可以在 pkg.conf(5) 中找到文档。并不是所有的选项都有注释掉的条目。如果某个选项的示例不存在但你需要它,可以自行添加。我们将在接下来的章节中详细研究其中的许多选项。

查找软件包

现在你已经安装了包管理器,可以安装软件包。熟悉各种类 Unix 操作系统的系统管理员知道,不同的操作系统会为同一软件的打包版本赋予不同的名称。在 FreeBSD 上,Apache Web 服务器的软件包名称与 illumos 或其他 Linux 发行版上的 Apache 软件包名称完全不同。在你安装任何东西之前,你需要弄清楚它的名称。

假设客户想在 Apache 上运行 WordPress。你的工作不是质疑客户选择的 Web 服务器;你的工作是构建和支持 Web 服务器。首先,使用 pkg search 命令查找 Apache。你需要提供一个文本字符串,供 pkg 执行不区分大小写的搜索。

# pkg search apache
apache-ant-1.9.7    Java- and XML-based build tool
apache-forrest-0.9  Tool for rapid development of small sites
apache-mode.el-2.0  Emacs major mode for editing Apache configuration files
--snip--

我故意选了一个麻烦的例子;FreeBSD 有大约 50 个与 Apache Web 服务器相关的软件包。幸运的是,每个搜索结果都会列出一行包描述。翻阅这些结果直到找到实际的 Web 服务器是非常简单的。

--snip--
apache22-2.2.31_1              Version 2.2.x of Apache web server with prefork MPM
apache22-event-mpm-2.2.31_1    Version 2.2.x of Apache web server with event MPM
apache22-itk-mpm-2.2.31_1      Version 2.2.x of Apache web server with itk MPM
apache22-peruser-mpm-2.2.31_1  Version 2.2.x of Apache web server with peruser MPM
apache22-worker-mpm-2.2.31_1   Version 2.2.x of Apache web server with worker MPM
apache24-2.4.25_1              Version 2.4.x of Apache web server
--snip--

六个不同版本的 Apache。首先,看一下软件包名称。当一款软件有多个版本时,主要版本号会被集成到软件包名称中。Apache 2.2 与 Apache 2.4 有很大不同,因此这些软件包被命名为 apache22apache24。实际的版本号紧随其后。我们第一个 Apache 2.2 软件包实际上是 Apache 2.2.31。后缀 _1 是软件包的版本号,这意味着这是一个更新版的软件包。包含的软件没有变化,但软件包本身有所修改。软件包版本号的提升有两个原因。当源代码端口发生变化,且对软件包有实际影响时,版本号会增加。当所需的共享库发生 ABI 更改,需要重新编译软件包时,也会提升版本号。

Apache 2.2 有五个不同的软件包。熟悉 Apache 的人可能记得,Apache 的这个版本可以使用不同的多处理模块(MPM),但必须在编译时选择 MPM。我已经完全忘记了关于 MPM 的所有知识,因此我选择安装 Apache 2.4 包,apache24

软件包搜索选项

有些搜索会生成数百个结果。试着搜索 Perl,你会得到大约 150 个软件包。Perl 模块的名称都以字符串 p5- 开头;FreeBSD 拥有超过 5200 个 Perl 模块的软件包!可以使用命令行选项来修剪或调整搜索结果。虽然 pkg-search(8) 列出了许多选项,这里是一些最常用的。

  • 使用 -C 使搜索区分大小写。

  • 如果你确切知道自己需要哪个软件包,并且只想查看它是否适用于你的系统,可以使用 -e 标志来搜索精确匹配。你的搜索词必须包含软件包版本号。

  • 如果你需要高度自定义你的搜索和搜索结果,请查看 -L-S-Q 标志,参见 pkg-search(8)。

检查已安装的包

或许你不确定某个包是否是你真正想要的。你可能会去第三方网站查找该包的详细信息,比如 FreshPorts (www.freshports.org/),但这需要离开你的终端,而我不建议这么做。使用 -R 标志来查看包的仓库目录的元数据。这些元数据是每个包中内置的 包清单 的一个子集。

# pkg search -R apache24
name: "apache24"
origin: "www/apache24"
version: "2.4.25_1"
comment: "Version 2.4.x of Apache web server"
maintainer: "apache@FreeBSD.org"
www: "http://httpd.apache.org/"
--snip--

包清单包括包名、构建包的端口、软件版本、包仓库、依赖关系等字段。它很少被使用并且可能会发生变化,所以我们不会详细讨论,但浏览这些信息能提供关于包内软件的更多细节。

这里有一个重要的细节是 www 字段,它给出了原始软件来自的网站。这是 Apache Web 服务器,而不是某个使用该名称的分支或其他项目。

这个原始清单的默认格式是 YAML,即 “YAML Ain’t Markup Language”。它是另一种格式化配置文件的语法,但相当易于人类阅读。使用 --raw-format 标志选择备用格式。其他支持的格式包括 json 和 json-compact。

# pkg search -R --raw-format json-compact apache24

如果你想自动解析包信息,这就是获取原始数据的方式。

安装软件

使用 pkg 的 install 子命令和包名来安装一个包。你不需要给出完整的包名。

# pkg install apache24

第一件事是 pkg 检查它本地的包数据库是否与包服务器上的数据库一致。你将看到类似“正在更新 FreeBSD 仓库目录”或“FreeBSD 仓库是最新的”这样的消息。

系统会检查你选择的包所需的任何其他包。阅读依赖列表。列表中有你不希望安装的内容吗?这个列表是否给出了 安装该包的理由?

The following 8 package(s) will be affected (of 0 checked):
New packages to be INSTALLED:
        apache24: 2.4.25_1
        libxml2: 2.9.4
--snip--
Number of packages to be installed: 8

最后的警告是,pkg 会告诉你安装所需的磁盘空间和带宽。然后你会被提示改变主意。

The process will require 139 MiB more space.
33 MiB to be downloaded.

Proceed with this action? [y/N]:

输入 y,pkg 会从仓库中获取该包并将其安装到你的系统上。

install 子命令假定你要么给出包的完整名称,要么给出没有版本号的包名。你可以请求 apache24 包,pkg 会自动识别当前包为 apache24-2.4.25_1。你也可以使用构建该包的端口名称,例如 pkg install www/apache24

在上一节中,我们的软件包搜索显示了五个不同的 Apache 2.2 软件包,每个软件包是稍微不同的变体。如果你要求pkg install安装 apache22 软件包,它会安装名为 apache22 的软件包以及一个版本号。如果你想要某个变体,比如 apache22-event-mpm,可以在install命令中指定完整的软件包名称。

一些软件包包含安装消息。这些消息可能是有用的说明、警告、注意事项或其他相关内容。如果软件包创建者认为某条信息足够重要,值得花费宝贵的时间编写说明,那你至少应该阅读它。你可以使用 script(1)记录这些信息,或者运行pkg info --pkg-message并指定软件包名称再次查看这些信息。

获取软件包

FreeBSD 通过从互联网上下载软件包进行安装。你可能想在一个位置下载软件包,然后在其他地方或以后安装它们。使用 pkg fetch 命令可以下载但不安装软件包。结合-d选项使用pkg fetch命令最为合适,这样它会抓取所有依赖包以及指定的软件包。

# pkg fetch -d apache24

你会看到正常的仓库更新信息,之后会有 pkg 将要下载的内容通知。

New packages to be FETCHED:
        apache24-2.4.25_1 (5 MiB: 14.25% of the 33 MiB to download)
        libxml2-2.9.4 (821 KiB: 2.43% of the 33 MiB to download)
--snip--

验证 pkg 计划下载的内容是否与预期一致,然后按 y 继续。软件包会下载到软件包文件缓存中。

要安装已下载的软件包,正常运行 pkg install。安装过程会使用缓存的文件,而不是已下载的文件。

阅读 man 页面的用户可能会注意到-a标志,它会下载整个软件包仓库。不要使用这个选项。-a选项是为公共仓库镜像设计的。普通的系统管理员镜像整个仓库会浪费带宽,且会减慢系统速度。慷慨的人捐赠了 FreeBSD 的软件包服务器带宽,千万不要浪费它。你可能需要下载一大堆软件包,带有依赖关系时,可能需要数百个甚至数千个软件包,但你不需要下载成千上万的软件包。只下载你需要的内容。

下载时机

任何访问互联网的工具都需要设置最大下载时间。你可以通过两个pkg.conf设置来定制 pkg 的下载行为。

如果下载失败,pkg 会重试。FETCH_RETRY选项控制 pkg 重试下载的次数。默认情况下是三次,这意味着它会尝试下载一次,并最多重试三次。

在大多数现代互联网连接下,下载过程相当迅速。如果你的上行带宽不是那么现代,你可能需要增加 pkg 在单次下载尝试中花费的时间。FETCH_TIMEOUT设置控制 pkg 等待某个文件下载的时间。默认值为 30,限制下载时间为 30 秒。如果你正在通过 33.6 的调制解调器下载 LibreOffice,你可能需要增加这个设置,并考虑通过“马车快递”将文件送到可移动驱动器上。

包缓存

下载包并稍后安装的功能意味着 pkg(8)会将这些包存储在磁盘上的某个地方以供以后使用。包缓存目录/var/cache/pkg包含从互联网下载的原始包文件。尽管你可以多年管理 FreeBSD 主机而无需处理缓存,但这里有一些你需要知道的事情。

清理缓存

由于升级、新包、删除的包以及系统管理的随机性,缓存目录可能会填满。我的 Web 服务器只有几个包,但不知怎的已经积累了 1.7GB 的旧包文件。pkg clean命令会删除任何已被新版本替换的缓存包,以及任何不再在仓库中的包文件。你将看到所有将被删除的文件列表,并有机会继续或退出。

# pkg clean
The following package files will be deleted:
        /var/cache/pkg/php56-mbstring-5.6.26.txz
        /var/cache/pkg/mod_php56-5.6.21-c80f5ce183.txz
--snip--

如果你从未清理过长期运行系统上的包缓存,列表可能会非常长。在提示符下,按 y 继续。

如果你想删除所有缓存的包,可以使用-a标志。

记住,pkg clean会删除那些不再在包仓库中的包文件。如果你依赖于某个已从仓库中删除的包,在清理之前请将该文件备份到缓存之外,否则它会被无意删除。你也可以尝试 pkg-create(8)来从已安装的组件重新构建一个包。

如果你希望在每次安装或升级包后自动清理包缓存,请将pkg.conf中的选项AUTOCLEAN设置为 true。我觉得自动清理过于激进,因为有时升级后的新包中的错误迫使我恢复到旧版本。我们将在本章末尾讨论包的升级问题。

移动缓存

你可能希望将包缓存放在文件系统的其他位置。使用pkg.conf中的选项PKG_CACHEDIR来设置一个新的包缓存目录。

为什么要移动缓存目录?许多服务器集群会在多台机器之间共享一个包缓存。你可以安全地在运行相同 FreeBSD 主版本和硬件架构的主机之间共享包缓存。确保你的 NFS 配置使用了锁定,并设置pkg.conf中的选项NFS_WITH_PROPER_LOCKING

包信息与自动安装

一段时间后,你会忘记你在系统上安装了哪些包。使用pkg info获取已安装软件的完整列表。

# pkg info
gettext-runtime-0.19.8.1_1  GNU gettext runtime libraries and programs
indexinfo-0.2.6             Utility to regenerate the GNU info page index
--snip--

如果你想了解更多关于已安装包的信息,使用pkg info和包名。这样会显示一个易于理解的报告,列出包清单和安装细节。

# pkg info apache24
apache24-2.4.25_1
Name           : apache24
Version        : 2.4.25_1
Installed on   : Tue Mar 14 16:56:14 2017 EDT
Origin         : www/apache24
Architecture   : freebsd:12:x86:64
--snip--

这个包是什么时候安装的?这个包是在这台机器上从 Ports Tree 构建并启用了某些选项吗?它的许可证是什么?该包中的每个程序需要哪些共享库?使用pkg info和包名,你可以回答这些问题以及更多问题。

pkg info子命令有许多其他功能。我们将在本章稍后看到其中的一些,例如锁定状态。pkg-info(8)手册页有完整的细节。

自动安装包

回顾一下示例的pkg info输出。我故意在这个系统上安装了几个不同的程序,但我可以肯定我从未故意安装任何关于 GNU 信息页面或 gettext 的内容。

我确实安装了那些程序。我只是没有太关注它们是什么,因为我更关心的是安装那个需要它们的包。它们是依赖项。

FreeBSD 会记录你是请求安装了某个包,还是它作为依赖项被带入。作为依赖项安装的包被称为自动包。你请求的包只是包,尽管有时也称它们为非自动包。

你可能想知道哪些包是你要求安装的,哪些是作为依赖项被拖入的。到这时,事情就变得复杂了。

查询包数据库

pkg 工具无法覆盖系统管理员可能遇到的所有情况。获取一些信息的最简单方法是查询已安装的包数据库。虽然你可以使用原始的 SQLite,但那意味着你需要深入了解数据库的内部结构。大多数系统管理员没有那么多时间,特别是当数据库可能随时发生变化时。FreeBSD 通过pkg query子命令隔离了这一点。完整的包查询概述会占据一章的篇幅,但这里提供一个快速的概览。

远程查询

使用 pkg-query(8)来搜索已安装包的数据库。不过,如果你仓库中可用的包数据库是最新的,你也可以使用 pkg-rquery(8)进行搜索。然而,可用包的数据库并不包含已安装包的所有元数据,因此并非所有模式都可以使用。

任何你可能想从包数据库中提取的信息,都可以通过pkg query轻松表示。关键是,任何人可能想从包数据库中提取的所有信息,都在pkg query中,正如快速浏览 pkg-query(8)所展示的那样。查询和命令结构是专门为脚本使用设计的,但我们有时会互动式使用它。

通过使用 模式 来运行查询。模式是一个有指定含义的变量,用百分号和字母表示。例如,%n 包含包名,%o 包含构建该包的端口,%t 包含表示包安装时间戳的时间戳。

运行 pkg query 并给定一个模式,会为每个安装的包输出该值。由于 %n 代表包名,以下是你如何获取系统上所有内容的列表:

# pkg query %n
apache24
apr
--snip--

我们没有得到 pkg info 显示的额外信息——但也许这正是你想要的。

你可以在一个查询中请求多个项。%v 模式代表包的版本,而 %c 代表评论。在这里,我用破折号分隔包名和版本,但在版本和评论之间放置一个制表符。使用 shell 制表符字符 \t 意味着我必须为 pkg query 参数加上引号。

# pkg query "%n-%v\t%c"
apache24-2.4.25_1       Version 2.4.x of Apache web server
apr-1.5.2.1.5.4_2       Apache Portability Library
--snip--

你知道,这看起来非常像 pkg info 的输出。当 pkg 命令查询或操作包数据库时,它使用的是这些完全相同的模式。你对包装系统的可视化程度与其他工具相同。

如果你想获取特定包的模式,将包名作为最后一个参数。在这里,我获取 apache24 包来源的端口:

# pkg query %o apache24
www/apache24

当然,我们在查询所有包和查询特定包之间有一个中间选择。

查询中的评估

这里有一个最后的巧妙包查询功能。许多——但不是所有的——模式都可以作为变量使用。一个命令可以评估这些变量,并根据结果采取行动。使用 -e 命令行选项可以评估变量,并使用逻辑运算符。逻辑运算符的完整列表可以在 pkg-query(8) 中找到。

评估可以分解成“如果这个为真,就做那个”。测试放在引号内。这里有一个例子:

# pkg query -e '%a = 0' %n

这个查询遍历了所有安装的包。-e 表示我们正在评估每个包的一个变量。引号内的语句 %a = 0 意味着我们在测试该包的 %a 值。如果 %a 等于 0,查询就会为真,pkg query 会打印出 %n 的内容。如果 %a 等于 0 以外的任何值,语句为假,pkg query 会跳过该包并继续处理下一个包。

我们已经知道 %n 包含包名。变量 %a 包含 pkg 记录的包是否是自动安装的。如果你请求了这个特定的包,它被设置为 0。如果一个包最初是作为依赖安装的,它被设置为 1。所以:如果一个包不是依赖包,就打印出包名。这个查询打印的是非自动安装的包。

# pkg query -e '%a = 0' %n
apache24
dmidecode
pkg
youtube_dl

这里有几点需要注意。首先,我并没有故意请求 pkg 安装 pkg(8)。我请求了 dmidecode,而 pkg 自我引导安装了。pkg 套件本身始终被视为非自动安装的包。

第二个:谁在这个机器上安装了 youtube_dl?

要找出哪些软件包是作为依赖安装的,请检查%a是否设置为 1。

= 或 ==?

您将看到使用双等号的示例,就像pkg query是编程语言一样。我的示例使用单等号。两者之间肯定有一些细微的差别,以及在特殊条件下应该使用每个符号的情况?

不!

您可以根据自己的肌肉记忆偏好使用单等号或双等号。

但实际上,我不会费心记住如何在所有主机上运行这个查询。我需要一种简单的方式让 pkg(8)为我记住它。

Pkg 命令别名

您可以在pkg.conf中定义 pkg 子命令的别名。这允许您创建别名来显示自动和非自动命令。尽管我也可以在我的 shell 中做类似的事情,但它不会显示为 pkg(8)子命令,而且我很容易困惑。

pkg.conf的底部,您会找到一个名为ALIAS的部分。

ALIAS              : {
  all-depends: query %dn-%dv,
  annotations: info -A,
--snip--
  }

别名是别名名称的单个单词,后跟冒号或等号,然后是要运行的 pkg 命令。如果您运行pkg all-depends,pkg(8)会在pkg.conf中查找并运行pkg query %dn-%dv。每个别名以冒号结尾,表示别名列表将在下一行继续。

默认配置中的许多别名代表了从 pkg_add aeon 遗留下来的东西,这是为我们这些老家伙创建的。尽管如此,现有的别名是查找示例查询和搜索的好地方。而且,通过别名搜索,您会找到以下这个很好的条目:

noauto = "query -e '%a == 0' '%n-%v'",

这个别名,noauto,运行一个pkg query命令来评估%a,并在其值为 0 时打印软件包的名称和版本号。它会打印那些未自动安装的软件包。我添加了一个非常相似的别名,用于打印自动安装的软件包。

auto = "query -e '%a == 1' '%n-%v'",

当您发现自己反复运行复杂命令时,可以添加别名。

卸载软件包

我们都曾安装软件,最后却因厌恶而将其卸载。唯一的区别是,究竟是什么让我们感到厌恶。使用pkg delete子命令卸载软件包。它也可以通过pkg remove来实现。那个多余的 youtube_dl 软件包?让我们把它从系统中移除。

# pkg delete youtube_dl
Checking integrity... done (0 conflicting)

删除过程会确保软件包没有出现严重问题,没有其他软件包需要它,并且它的删除不会导致软件包系统无法预测的严重问题。^(2)

然后,您将看到要删除的软件包列表以及它们释放的空间量。最后会有一个最终的机会让您选择“不”。

Proceed with deinstalling packages? [Y/n]: y

[1/1] Deinstalling youtube_dl-2017.02.11...
[1/1] Deleting files for youtube_dl-2017.02.11: 100%

该软件包已从您的系统中删除。

移除依赖

如果您删除一个其他软件包依赖的包,pkg 也会删除依赖的软件包。

# pkg delete trousers
--snip--
Installed packages to be REMOVED:
        trousers-0.3.14_1
        gnutls-3.5.9
        emacs-nox11-25.1,3
--snip--

gnutls 软件包需要 trousers,而 emacs-nox11 需要 gnutls。删除 trousers 会破坏它们两个,因此 pkg 认为您显然也不想在系统中保留它们。

如果您真的想删除一个其他软件包依赖的包,请添加-f标志。

非常小心地阅读pkg delete的警告信息!

自动移除

在主机上保留不必要的软件会增加安全风险并增加系统管理员的工作量。在长期运行的系统上,你并不总是知道该删除哪些软件。删除你选择安装的软件很容易,但这些软件可能带来了你从未关注过的依赖项。或者,某个软件包的新版本可能比旧版本具有更少或不同的依赖项。

我从我的测试系统中删除了 youtube_dl 软件包。这样就只剩下我故意安装的其他软件包及其依赖项。它也保留了 youtube_dl 依赖的但没有其他软件包需要的那些软件包。pkg autoremove子命令识别出作为依赖项安装但不再被其他任何软件包需要的软件包,并提供删除这些不再需要的软件包。我强烈建议在删除不需要的依赖项之前执行一次模拟运行,给你脆弱的人类大脑一次检查列表的机会。

# pkg autoremove

Pkg 运行数据库查询以识别不需要的依赖项,并提议删除它们。

Installed packages to be REMOVED:
        python27-2.7.13_1
        readline-6.3.8
        rtmpdump-2.4.20151223
        librtmp-2.4.20151223
--snip--

仔细研究这个列表。非软件包软件需要通过其他途径引入的软件包并不罕见。你可能不需要没有 youtube_dl 的 rtmpdump 和 librtmp 视频处理工具,但很多软件都需要 Python 解释器。你真的想删除它吗?

如果你真的可以删除这些软件包,回答y并继续。如果其中某个依赖项变得至关重要,那么改变你的数据库来告诉它。

更改软件包数据库

想要在 pkg(8)之外更改软件包数据库吗?不要。你只会给自己带来麻烦,寻求帮助时要么会遭遇嘲笑,要么会被建议删除所有软件包并重新开始。

有几个情况 pkg(8)支持更改软件包数据库。那时你可以使用pkg set。pkg-set(8)子命令允许你在不破坏数据的情况下正确调整数据库中的一些合理值。最常见的情况是当你想让一个自动软件包不再是自动的。

pkg set-A标志允许你更改软件包的自动设置。将此标志设置为 1 表示该软件包是自动安装的,作为依赖项,而设置为 0 则表示该软件包是用户专门请求的。

在上一节中,pkg autoremove要删除的四个软件包中包括了 Python。我想保留 Python——不仅是这次,而且是今后每次执行 autoremove 时。我保持 Python 的方法很简单,就是把它从自动软件包改为非自动软件包。

# pkg set -A 0 python27
Mark python27-2.7.13_1 as not automatically installed? [Y/n]: y

现在,Python 是一个非自动软件包。pkg autoremove的结果现在看起来不同了。

# pkg autoremove -n
--snip--
Installed packages to be REMOVED:
        rtmpdump-2.4.20151223
        librtmp-2.4.20151223
--snip--

现在只剩两个软件包,而不是四个?显然 Python 需要 readline。我很高兴 pkg 帮我找出了这一点,因为我实在不想记住这些。

我们会根据需要进一步讲解pkg set

锁定软件包

有些软件就像地铁的电气化轨道。触摸它会导致痛苦或死亡。

我最喜欢的例子是远程文件同步程序 rsync(8)。rsync 已经存在几十年了,其内部协议随着时间的推移发生了变化。许多嵌入式和遗留系统使用 rsync,但它从未被升级。我曾花费许多痛苦的小时调试为什么当前的 rsync 无法与 20 世纪的嵌入式电话交换机控制器进行通信。结果发现,某个 rsync 版本的发布去掉了对电话交换机支持的非常旧协议的支持。由于无法升级电话交换机,因此我需要确保主机上的 rsync 软件包永远不会升级。永远

这就是锁定软件包的作用。

当你锁定一个软件包时,pkg 将不会对其进行升级、降级、卸载或重新安装。它将对软件包的依赖项和它所依赖的程序应用相同的规则。负责获取电话交换机文件的主机需要锁定其 rsync 软件包。使用pkg lock来锁定软件包。

# pkg lock rsync
rsync-3.1.2_6: lock this package? [Y/n]: y
Locking rsync-3.1.2_6

这个软件包现在已经固定在原位。

要显示系统上所有被锁定的软件包,请使用-l标志。这只会显示你有意锁定的软件包,而不会显示依赖项或依赖关系。

# pkg lock -l
Currently locked packages:
rsync-3.1.2_6

使用pkg unlock命令来移除锁定。

# pkg unlock rsync
rsync-3.1.2_6: unlock this package? [Y/n]: y
Unlocking rsync-3.1.2_6

要锁定或解锁系统上的所有软件包,请使用-a标志。每个软件包都会弹出确认提示,因此,如果你真的想影响所有软件包,请添加-y标志。

# pkg unlock -a
apache24-2.4.25_1: already unlocked
apr-1.5.2.1.5.4_2: already unlocked
--snip--
rsync-3.1.2_6: unlock this package? [Y/n]: y
Unlocking rsync-3.1.2_6
--snip--

软件包锁定并不能阻止具有 root 权限的人修改软件包中包含的文件。

相关的内容,第二十二章讲解了如何使用监狱来限制非常旧的软件。

软件包文件

由软件包安装的文件被视为系统文件,你不应该手动编辑它们。在你可以编辑这些文件之前,必须知道哪些文件是由软件包带来的。使用pkg info -l和软件包名称查看完整的文件列表。(通过pkg.conf别名,这个信息也可以通过pkg list获得。)

# pkg info -l rsync
rsync-3.1.2_6:
        /usr/local/bin/rsync
        /usr/local/etc/rc.d/rsyncd
        /usr/local/etc/rsync/rsyncd.conf.sample
--snip--

另一种可能性是你想知道某个文件来自哪个软件包。使用pkg which命令。当我发现一个奇怪的库时,我通常会用这个命令来看看它是从哪里来的。

# pkg which libp11-kit.so
/usr/local/lib/libp11-kit.so was installed by package p11-kit-0.23.5

我的疑问现在是:“什么是 p11-kit?”但这算是进步。

软件包完整性

尽管你不应该修改软件包文件,但最终,还是会有人这么做。你可以使用 pkg 来发现这些更改并撤销损害。

pkg-check(8)工具包括用于识别软件包和软件包依赖关系损坏的功能。开发者也可以使用 pkg-check(8)检查从端口构建并分发给最终用户的捆绑软件包,但那是一个完全不同的问题。

文件损坏

使用pkg check -s和软件包名称来验证软件包文件是否未被修改。当我锁定的 rsync 软件包停止从挑剔的远程服务器同步文件时,我会验证其中一个事项就是软件包完整性。

# pkg check -s rsync
Checking rsync:   0%
rsync-3.1.2_6: checksum mismatch for /usr/local/bin/rsync
Checking rsync: 100%

要么是磁盘出现故障,要么是有人动过我的 rsync(1)二进制文件。由于这个系统使用自修复的 ZFS,所以会有一番“修补”。

你可以卸载并重新安装这个包,但这可能会触发其他依赖该包的包发生变化。另外,正如前面讨论的,这个特定的包很特殊。我不想让 pkg 将这个包升级到最新版本,而是希望强制 pkg 从包缓存中重新安装当前版本。使用-f标志执行pkg install。虽然它会更新仓库数据库,但会重新安装缓存中的包。如果该包被锁定,你必须先解锁它。

# pkg unlock -y rsync
Unlocking rsync-3.1.2_6
# pkg install -fy rsync
--snip--
[1/1] Reinstalling rsync-3.1.2_6...
[1/1] Extracting rsync-3.1.2_6: 100%
# pkg lock -y rsync

我珍贵的 rsync 已经恢复了。

通过运行pkg check -saq检查所有包的完整性。除非有变动,否则不会产生任何输出,因此你可以通过 cron 定时运行它(见第二十章)。

依赖性问题

如果有人真的想尝试,他们可以删除其他包依赖的包。使用pkg check-d标志来识别并修复缺失的依赖。

# pkg check -d emacs-nox11
Checking emacs-nox11: 100%
emacs-nox11 has a missing dependency: gnutls
emacs-nox11 is missing a required shared library: ➊libgnutls.so.30
--snip--
>>> Try to fix the missing dependencies? [Y/n]: y

首先要注意的是,当pkg check识别到缺失的依赖时,它会尝试修复它。对提示回答 y 以重新安装该依赖。

注意,这个pkg check运行显示我们缺少一个库,libgnutls.so.30 ➊。依赖性检查实际上并不会搜索所有包中的所有文件。它之所以知道这个库缺失,是因为包含它的包已经不存在了。如果你手动删除了这个库,依赖性检查是无法找到它的。你需要像之前一样检查包文件的完整性。

如果你想用pkg check -d检查所有包的依赖关系,不要提供包名。你可以添加-a来显式检查所有包,但这并不是必需的。如果你添加-q标志,这个命令只有在发现问题时才会输出结果。添加-q还告诉pkg check尝试自动解决它发现的任何依赖性问题,而无需用户干预。

这种组合意味着,虽然我可以将这个检查作为定期任务运行,但我对于我的主机重新安装缺失的依赖并不太放心。在自动修复依赖问题之前,考虑一下系统在没有你关注的情况下安装包的情况。

pkg check子命令包括一些其他有用的选项,例如-B用于重建共享库依赖,-r用于手动重新计算已安装包的校验和。详情请参阅 pkg-check(8)。

包维护

包管理系统包括一些维护脚本,旨在通过 periodic(8)运行。从/etc/periodic.conf启用这些脚本,如第二十章所讨论的那样。每个脚本都会添加到每日、每周或安全状态的电子邮件中。

要进行每日维护检查包的校验和并替换损坏的包,像pkg check -saq一样,将daily_status_security_pkg_checksum_enable设置为 YES。

为了检测已安装的软件包是否存在 FreeBSD 软件包安全系统中发布的安全漏洞,如 第十九章所讨论的,可以将 daily_status_security_pkgaudit_enable 设置为 YES。

如果你希望 FreeBSD 每天备份已安装的软件包和软件包数据库,请将 daily_backup_pkg_enable 设置为 YES。

为了接收已安装软件包的变更通知,将 daily_status_pkg_changes_enable 设置为 YES。

最后,你可以通过将 weekly_status_pkg_enable 设置为 YES,每周检查过时的软件包。

软件包网络配置和环境

FreeBSD 的软件包系统是为了适应普通的互联网连接网络而设计的。这有点像一个残酷的笑话,因为没有网络是“正常”的。你可以调整 pkg 的行为以适应你的网络。

最常见的变更是需要使用代理服务器。Pkg 使用 fetch(3) 下载软件包文件,任何特殊的网络配置都可以通过环境变量进行设置。在 pkg.conf 文件的 PKG_ENV 部分设置环境变量。每个变量需要包括变量名、冒号和对应的值。在这里,我将 HTTP_PROXY 环境变量设置为我的网络代理:

pkg_env : {
  HTTP_PROXY: "http://proxy.mwl.io/"
}

查看 fetch(3) 以获取完整的代理环境设置列表。

有些网络为不同的网络协议栈分配了独立的带宽。我曾经使用过多个网络,其中某些网络在 IPv6 上的连接速度比 IPv4 更好,或者反之。你可以通过在 pkg.conf 文件中设置 IP_VERSION 来让 pkg 使用某个特定的网络协议。你可以将此设置为 4、6,或者让主机默认自动选择(默认为 0)。

最后,pkg.conf 文件中的 NAMESERVER 设置允许你覆盖 /etc/resolv.conf 中的 DNS 服务器。在此处可以输入 IPv4 或 IPv6 地址。你也可以使用主机名,pkg 会通过系统默认的 DNS 服务器来查找该主机名。

软件包仓库

你可能希望使用 FreeBSD 项目提供的以外的软件包。也许你自己构建了软件包,正如 第十六章 所讨论的那样。或者你可能有权访问一个实验性的软件包仓库。又或者你想要切换使用的官方软件包集合。

Pkg 支持软件包 仓库,或 repo,它们是命名的软件包集合。你可以添加、删除、启用和禁用这些仓库。

正常的仓库配置非常简单,但在少数情况下,它们可能会变得相当复杂。我们不会深入讨论配置自定义仓库的极端情况,但基础知识会让你走得很远。

仓库配置

在各自的文件中配置每个仓库。官方的 FreeBSD 仓库应放置在 /etc/pkg 目录下。仓库配置使用 UCL 格式(参见 第二章)。FreeBSD 默认启用了 FreeBSD 仓库。你可以在 /etc/pkg/FreeBSD.conf 文件中找到该配置文件。

   FreeBSD: {
➊ url: "pkg+http://pkg.FreeBSD.org/${ABI}/quarterly",
➋ mirror_type: "srv",
➌ signature_type: "fingerprints",
➍ fingerprints: "/usr/share/keys/pkg",
➎ enabled: yes
   }

这个名为 FreeBSD 的仓库支持 FreeBSD 的软件包仓库。当你决定设置自己的仓库时,给它起个有意义的名字。

mirror_type 条目 ➋ 告诉 pkg 该仓库是否托管在一个普通的网站上。将 mirror_type 设置为 NONE 会告诉 pkg 使用 fetch 通过标准网络方法(如 HTTP、FTP,甚至是文件路径)来获取软件包。

数百万台机器运行 FreeBSD 并需要访问软件包仓库。单个 web 服务器无法满足需求。通过将 mirror_type 设置为 srv,你告诉 pkg 检查 DNS 是否有 SRV 记录。SRV 记录用于引导高可用性服务,如 VoIP 和 Active Directory。

url 条目 ➊ 显示了该仓库所在的互联网网站。我相信你之前见过 http URLs,那这个 pkg+http 是怎么回事呢?它将请求与用于引导 pkg 请求的 SRV 记录绑定在一起,正如 mirror_type 所设置的那样。

包管理系统可以通过公钥或加密哈希指纹 ➌ 来验证下载的软件包。但你需要告诉 pkg 在哪里找到这些密钥或哈希 ➍。

最后,你必须显式地启用或禁用 ➎ 每个仓库。

仓库定制

你可以根据需要添加和移除仓库。由于 /etc/pkg 保留用于官方 FreeBSD 仓库,你需要另一个目录。传统的位置是 /usr/local/etc/pkg/repos。如果你想使用不同的目录,你需要在 pkg.conf 中通过 REPO_DIRS 选项设置一个位置。你将看到注释掉的默认示例。

#REPOS_DIR [
#    "/etc/pkg/",
#    "/usr/local/etc/pkg/repos/",
#]

本地仓库目录默认不存在,因此你需要创建它。

# mkdir -p /usr/local/pkg/repos

将你自己的仓库配置放在那个目录里。

FreeBSD 按照目录顺序搜索软件包,检查 REPOS_DIR 中给定顺序的目录。显而易见的含义是,默认的 FreeBSD 仓库不能被禁用或覆盖。虽然这不完全正确,但原因有些复杂。

仓库继承

你可以将一个仓库的配置分割到多个文件中。后续文件中的条目会覆盖前面文件中的条目。

要查看这如何工作,可以考虑默认的仓库,名为 FreeBSD。如果你在 /usr/local/etc/pkg/repos 中配置了所有自定义仓库,pkg 会首先找到 FreeBSD 仓库。

但现在创建一个 /usr/local/etc/pkg/repos/FreeBSD.conf 文件。在里面定义 FreeBSD 仓库,但只包括一个配置声明。

FreeBSD: { enabled: no }

Pkg 在 /etc/pkg/FreeBSD.conf 中首先找到名为 FreeBSD 的仓库。此配置定义了该仓库的默认设置。它稍后会找到第二个配置。第二个配置只覆盖一个选项,但该选项会禁用仓库。

虽然禁用 FreeBSD 仓库是一个极端的例子,适用于那些不运行自己仓库的人,但对仓库进行小幅修改是有充分理由的,正如我们接下来将看到的。

软件包分支

FreeBSD 的软件包是从 Ports 集合中构建的(参见第十六章)。Ports 集合试图将成千上万种不同的软件套件带到 FreeBSD 上。这些不同的程序都有自己的发布计划,而 Ports 集合也在不断演进,努力跟上它们的步伐。正如你能想象的那样,Ports 集合充满了变化。我们大多数运行服务器的人都更看重稳定性。当大多数系统管理员谈到“稳定性”时,变化这个词并不是他们脑海中的第一印象。

我们大多数人并不需要在服务器上安装最新的软件。大多数时候,如果我的数据库服务器只是稍微落后一个或两个小版本,我是没问题的;我只关心它是否能继续正常工作。我当然不会因为 MySQL、nginx 或 PHP 发布了新版本就升级我的服务器。那样做会陷入不断升级的疯狂之中。

我确实希望获得安全性和稳定性的更新。然而,数据库服务器稍微老旧一点我并不在乎。数据库服务器偶尔失去“理智”并将我的所有数据发送到垃圾桶,或者把所有数据提供给底特律的黑客团队,这让我非常不安。

FreeBSD 的软件包系统的季度分支试图在世界上不断变化的软件和系统管理员的心理平衡之间找到一个中间点。每年的一月、四月、七月和十月,Ports 团队会将 Ports 集合分叉成一个季度分支。季度分支只会接收安全性和稳定性更新,而主 Ports 集合则继续快速发展。

FreeBSD 项目为每个版本构建了两套软件包。季度软件包是从季度 Ports 集合中构建的。最新软件包则是从前沿的软件包中构建的。

你们中的一些人虽然不喜欢软件包频繁更新,但还是偏爱使用最新的软件包。没关系,切换很简单。你只需要覆盖 FreeBSD 仓库中的一个条目。像上一节那样创建一个新的仓库文件,/usr/local/etc/pkg/repos/FreeBSD.conf。不过,这次我们不会禁用默认仓库,而是要覆盖软件包来源。将 URL 末尾的“quarterly”改为“latest”。

FreeBSD: { url: "pkg+http://pkg.FreeBSD.org/${ABI}/latest" }

欢迎来到频繁变动的世界!

强烈建议在更改仓库后运行pkg update -f,以强制 pkg 下载最新的仓库目录。

切换软件包集合并不意味着你需要重新安装所有内容。如果你旧的软件包能正常工作,就继续使用它们。不过,如果出现奇怪的问题,可以使用类似 pkg upgrade -fa 的命令重新安装所有软件包。即使是版本相同的其他软件包,它们可能也会有所不同。

升级软件包

尽管我们可能希望它是另一种情况,但你不能忽视一个新系统的建立。要么会出现稳定性错误,要么某个聪明的家伙发现了一个安全漏洞。(第十九章讨论了包安全的审计。) 有时你必须升级第三方软件。在 FreeBSD 的原始包管理系统 pkg_add 中,包升级存在一定程度的麻烦风险。而使用 pkg(8)时,你仍然会遇到麻烦——但这来自于软件的新版本,而不是包管理系统本身。

在升级包之前,先备份系统。然后,使用upgrade子命令让 pkg(8)升级所有包。我建议首先运行干运行,使用-n选项。

# pkg upgrade -n
--snip--
Checking for upgrades (2 candidates): 100%
Processing candidates (2 candidates): 100%
The following 1 package(s) will be affected (of 0 checked):

Installed packages to be UPGRADED:
        ca_root_nss: 3.29.1 -> 3.29.3

Number of packages to be upgraded: 1

335 KiB to be downloaded.

仔细查看待升级的包列表。是否有大的跳跃?是否需要查看任何发行说明?这次升级可能会造成多大影响?升级是否会移除你需要的某些包,比如X.org或你的文本编辑器?你是否应该等到周日凌晨 3 点,再让你的下属来处理?^(3) 研究升级内容并考虑每个包升级的风险,可能不会减少你需要做的工作量,但它会减少停机时间和被人责骂的时间。

一旦你对将要发生的变化感到满意,就可以运行升级了。

# pkg upgrade -y

你将看到关于待升级包的非常相似的信息,然后是下载和安装过程的通知。最后,pkg 会显示每个升级包的安装信息。

即使是世界上最灵活的包管理系统也并不总能满足你的需求。FreeBSD 通过 Ports Collection 使得定制附加软件变得非常容易,我们将进入下一个部分。

第十六章:使用端口定制软件

image

包含了最常见配置的最受欢迎的程序。如果你要构建一个通用的 web 服务器,官方的 FreeBSD 包,如 nginx 或 lighttpd,或是你偏好的任何 web 服务器应该就足够了。如果你有一个特殊的环境或更少见的需求,那么 Ports Collection(端口集合)就派上用场了。Ports Collection 是一个可以轻松构建许多软件包定制版本的工具。它将依赖关系、许可证、维护者和所有其他软件信息结合成一个既适合机器处理也适合人类阅读的标准格式。通过端口,你可以设置一些系统选项,比如“禁止第三方 GPL 许可证代码”(对嵌入式供应商有用)、“为所有软件添加 LDAP”或者“禁用 X11”。

从长远来看,端口最好通过 poudriere 包构建系统来管理。然而,在你能够使用 poudriere 之前,必须了解端口是如何工作的。我鼓励你在测试系统上探索端口。但与其将端口部署到每台单独的服务器上,不如使用 poudriere 来构建你自己的包存储库。完全通过包来管理你的服务器。除非是你的包构建器,否则永远不要在生产服务器上使用端口树。

在我们深入讨论端口之前,让我们先聊聊软件构建的基本概念。

制作软件

传统的软件构建非常复杂,因为源代码必须被非常具体地处理,才能创建一个可运行的程序——更不用说一个运行良好的程序了!这与使用,比如说,JavaScript 编译器的过程完全不同。尽管程序员可以为每个程序提供安装说明,里面充满了像Now type ar cru .libs/lib20_zlib_``plugin.a istream-zlib.o zlib-plugin.o这样的指令,但这简直是出于极端恶意的设计。虽然 Unix 管理员似乎会批准这种恶作剧,但他们对对自己施加的残酷行为是坚决反对的;如果可以自动化,那就一定会自动化。

构建软件的主要工具是 make(1)。当运行 make 时,它会在当前目录中查找一个名为Makefile的文件,里面充满了类似于前面段落中那个可怕示例的指令。它读取这些指令并执行,自动化安装过程,无论过程多么复杂。你其实不需要了解Makefile的内部细节,因此我们不会对它进行详细分析。

每个Makefile都包含一个或多个targets,或者说是一组需要执行的指令。例如,输入make install指示 make(1)查找名为install的目标,并在找到时执行它。目标的名称通常与要执行的过程相关,因此你可以放心地认为make install会安装软件。你会发现有许多目标用于安装、配置和卸载大多数软件。make(1)处理了各种各样的功能,其中一些功能远远超出了创建者的初衷。但这正是 Unix 的乐趣所在!

源代码和软件

源代码是用于构建实际机器代码的可读指令,这些机器代码构成了一个可运行的程序。你可能已经以某种形式接触过源代码。如果你从未见过它,可以查看 /usr/src 下的几个文件或* svnweb.freebsd.org/*。即便是新手系统管理员也需要能在三次中识别源代码两次。

一旦你拥有一个程序的源代码,你就在你希望运行它的系统类型上构建(或编译)程序。(通过交叉编译来为外部平台构建软件要复杂得多。)如果程序是为一个与你正在构建的系统足够相似的操作系统编写的,它就能运行。如果你的平台与原始平台差异过大,它就会失败。一旦你在你的系统上成功构建了软件,你可以将生成的程序(或二进制文件)复制到其他相同的系统上,并且它应该能正常运行。

一些程序写得足够好,以至于它们可以在许多不同的平台上进行编译。有一些程序特别支持广泛不同的平台;例如,Apache web 服务器可以在 Windows 和类 Unix 系统上编译。这代表了软件作者的巨大努力,尽管如此,在 Windows 上进行构建之前,您仍然必须运行一些脚本并严格按照指示配置环境。

一般来说,如果你能够从源代码构建程序,它可能会运行。它可能不会正确运行,可能不会做你期望的事情,但它会运行。经验丰富的系统管理员可以利用源代码和错误信息,了解为什么一个程序无法构建或运行。在许多情况下,问题是简单的,并且可以通过最小的努力解决。这也是为什么访问源代码非常重要的原因之一。

在每个系统管理员都是程序员的时代,调试软件占用了管理员大量的时间。每个类 Unix 系统都有些许不同,因此每个系统管理员都必须了解自己的平台、软件设计所针对的平台以及两者之间的差异,才能希望使一段代码正常运行。这种重复性的工作真是令人吃惊。

多年来,程序员开发了像 autoconf 这样的工具来帮助解决这些跨平台问题。并非每个程序都使用这些工具,而当它们出错时,系统管理员就不得不重新开始。系统管理员必须编辑源代码和Makefiles,才能有机会使程序运行。而且,运行并不等同于运行良好,更别提正确运行了。

FreeBSD Ports 集合旨在简化这一过程,为 FreeBSD 用户提供便利。

Ports 集合

Ports 集合,也叫做ports 树或简称ports,包含了一个在 FreeBSD 上编译软件的自动化系统。

端口系统的基本理念是,如果源代码需要修改才能在 FreeBSD 上运行,那么这些修改应该是自动化的。如果你需要其他软件来从源代码构建这个程序或运行该软件,那么这些依赖关系应该被记录和跟踪。如果你要自动化这些更改,那么不妨记录下程序所包含的内容,这样你就可以轻松地安装和卸载它。而且,由于你有一个生成每次都能产生相同结果的软件构建过程,并且你已经记录了这个过程创建的所有内容,你可以复制二进制文件并将其安装到任何类似的系统上。

除了创建软件包所需的信息外,端口集还包含有关构建软件的法律限制、安全信息、许可详情等。

端口与软件包互操作。端口集用于创建软件包。你可以根据需要从端口安装一些软件,从软件包安装其他软件,灵活地混合安装来源。你需要使用与构建软件包时相同版本的端口集,无论是季度分支还是最新版本。大多数端口用户希望获得最新的软件,因此我们将重点关注这一点。

端口

端口是一组说明如何修复或补丁一组源代码文件,并构建和安装这些文件的指令。一个端口包含创建完成软件所需的所有信息。这使得系统管理员不再为安装程序而苦恼,而是可以将精力集中在配置它们上。

端口树安装

如果你按照第三章中的安装说明进行操作,你应该已将端口树安装在/usr/ports目录中。在该目录下,你应该能找到几个文件和几十个目录。如果你在/usr/ports中没有任何内容,显然你没有按照说明操作。没关系——我也没做到——但你必须安装端口树才能继续。

FreeBSD 支持几种不同的方法来获取端口树。你可以使用 svn(1)查看端口树,或者从网上下载一份。对于系统管理员来说,推荐的方法是使用 portsnap(8)下载最新的(非季度版)端口树。

# portsnap auto
Looking up portsnap.FreeBSD.org mirrors... 6 mirrors found.
Fetching snapshot tag from your-org.portsnap.freebsd.org... done.
Fetching snapshot metadata... done.
Updating from Mon Oct 17 15:59:41 EDT 2018 to Mon Mar 20 14:13:53 EDT 2019.
Fetching 5 metadata patches... done.
Applying metadata patches... done.
Fetching 5 metadata files... done.
Fetching 10202 patches.
(700/10202) 6.86% .........

在这里,portsnap 会搜索 portsnap 文件的镜像,进行加密验证以确认这些文件在 portsnap 服务器上的完整性,下载文件,并验证下载本身的完整性。

现在,你已经拥有了所有 FreeBSD 端口的最新版本。要将现有的端口树更新到最新版本,只需再次运行portsnap auto

如果你希望通过 cron(1)定期调度 portsnap 更新运行,使用portsnap cron update命令,而不是portsnap auto。这会告诉 portsnap 在命令运行后 60 分钟内的某个随机时间更新端口树。这有助于分散 FreeBSD portsnap 服务器的负载。在 root 的 crontab 中安排一个在上午 5 点到 5:59:59 之间的 portsnap 运行,可以使用如下条目:

0    5    *    *    *    /usr/sbin/portsnap cron update

这会在早上 5 点到 6 点之间的随机时间启动实际更新,这比 24 个 portsnap 用户中 1 个在早上 5 点同时访问下载服务器要有效得多

端口树内容

你在这里看到的大多数目录都是软件类别。每个类别包含进一步的目录层次,而这些目录中的每一个都是一个软件包。截止到我写这篇文章时,FreeBSD 有超过 28,000 个端口,因此使用目录树并正确分类软件至关重要。在这个类别中,不是软件类别的文件和目录中的主要部分在此进行描述。

CHANGES文件列出了对 FreeBSD 端口基础设施所做的更改。它主要供 FreeBSD 端口开发者和那些对端口集合内部结构感兴趣的人使用。

CONTRIBUTING.md文件存在于 GitHub 上的 FreeBSD 源代码镜像中。所有 FreeBSD 源代码都镜像在 GitHub 上,方便用户使用,但 FreeBSD 内部并不使用 Git。GitHub 用户通常会查阅CONTRIBUTING.md,了解如何贡献——而在 FreeBSD 的情况下,回答是“访问 FreeBSD 官网。”(截至我写这篇文章时,将 GitHub 的拉取请求自动提交到 FreeBSD PR 系统的工作仍在进行中。)

COPYRIGHT包含了整个端口集合的许可信息。虽然端口集合中每个单独的软件都有自己的版权和许可信息,但端口集合本身是根据两条条款的 BSD 许可证进行许可的。

GIDs文件包含了端口集合中所有软件使用的组 ID 列表。集合中的许多软件期望以无特权用户身份运行。如果每个端口都创建一个随机用户,用户名、用户 ID 和组 ID 就会重叠。相反,需要无特权 GID 的端口在这个文件中预留一个。这个文件记录了分配给端口集合的 GID。直到使用时,GID 才会在/etc/passwd中分配。

你的/usr/ports中有一个以你运行的 FreeBSD 版本命名后缀的INDEX文件。这个 FreeBSD 12 系统中有/usr/ports/INDEX-12。端口系统的搜索和描述功能使用这个索引。索引是本地生成的,不存储在 Subversion 中。

Keywords目录包含了通用配置语言系统的信息,详见第二十三章。

LEGAL描述了端口集合中任何软件的法律限制。有些软件有特定的限制——比如不允许商业使用、不允许再分发、不允许盈利等。每个单独的端口也列出了这些限制;这只是从所有端口中汇总的主列表。

MOVED 列出了所有已重命名、从一个类别移动到另一个类别或被移除的端口,并附上了原因。自动化管理工具如 portmaster(8) 会使用此列表来查找已移动端口的新位置。为什么要移动端口?当我开始使用 FreeBSD 时,它有一个用于 X Windows 软件的类别。这个类别变得极其庞大,因此端口团队将其拆分,然后再次拆分,直到我们到了 2017 年的九个类别。

Makefile 包含了整个 Ports Collection 的高级指令。只有当你想构建整个 Ports Collection 中的每一个端口时才会使用它。你最好使用像 “Private Package Repositories” 在 381 页 讨论的 poudriere,而不是在这里直接运行 make

Mk 子目录包含了驱动 make(1) 从互联网获取源文件、打补丁、构建和安装的逻辑。许多类型的程序期望能够集成在一起,这些文件确保同一工具的不同部分能够以兼容的方式构建和安装。一些功能,如 LDAP 和 Emacs,可能会影响许多端口。这个目录包含了像 bsd.ldap.mkbsd.emacs.mk 这样的 Makefiles,专门用于这些功能。

Mk 子目录下,你会找到 Uses 目录。这个目录包含了广泛使用的 Makefiles,用于其他广泛使用的功能或软件套件。例如,KDE 和 GNOME 桌面套件包括数十个或数百个小程序,每个程序都必须正确构建才能相互协作。如果你查看 Uses,你会看到专门配置这些程序的 gnome.mkkde.mk 文件,以及 GSSAPI、Lua、Varnish 等许多其他软件系列的文件。

如果你真的想了解 Ports Collection 是如何工作的,可以阅读 /usr/ports/Mk/usr/ports/Mk/Uses 中的所有内容。这些内容非常具有教育意义,尽管支持这些不同程序的本质意味着 Makefiles 像一篮被疯狂小猫攻击过的线团一样纠结。

README 文件包含了 Ports Collection 的高层次介绍。

Templates 目录包含其他部分的 Ports Collection 使用的框架文件。

Tools 目录包含了程序、脚本和其他自动化工具,主要供端口开发者使用。

UIDs 文件包含系统中端口使用的非特权用户 ID。与 GIDs 文件类似,这有助于端口开发者避免端口软件所需的非特权用户之间的冲突。

UPDATING 包含了升级软件时的使用说明。需要特别干预的更新会按日期逆序出现在这里。在更新软件之前,请查看此文件,以获取对你有影响的重要说明。

distfiles 目录包含了已移植软件的原始源代码。当一个端口下载一段源代码时,该源代码会保存在 /usr/ports/distfiles 下。

所有其他目录都是端口的分类。以下是ports/arabic目录的内容,其中包含了专门针对阿拉伯语言的软件。虽然端口集合中的其他软件也支持阿拉伯语,但此类别是针对阿拉伯语的软件——例如字体、某些类型文档的翻译等等。这个类别对大多数人来说并不有用,但它的优势在于它足够小,可以放入这本书中。有些端口类别有数百个条目。^(1)

Makefile        ae_fonts_ttf    kacst_fonts     libitl
Makefile.inc    arabtex         kde4-l10n       libreoffice
ae_fonts_mono   aspell          khotot

这个Makefile包含了该目录中所有端口的指令。它们比/usr/ports中的全局Makefile更为具体,但没有单个端口的Makefile那么具体。文件Makefile.inc包含了该目录中端口的元指令。所有其他目录则是独立的软件包。我们将在第 371 页的“安装端口”中剖析其中一个目录。

独立端口通常根据它们在端口集合中的目录来命名。gnuplot 图形程序可能被称为 math/gnuplot,因为它的端口可以在/usr/ports/math/gnuplot找到。

端口索引

端口索引文件包含了所有基于特定 FreeBSD 版本构建的端口列表。在 FreeBSD 13 中,这是/usr/ports/INDEX-13。端口集合使用该索引有多个用途,包括搜索整个端口树。

索引文件描述了每个端口的单行信息,字段之间用管道符号(|)分隔。虽然这种方式对于系统工具很方便,但对人类并不特别友好。在/usr/ports中运行make print-index可以得到一个更长且更易理解的索引。这个索引包含了如下条目:

Port:   p5-Archive-Extract-0.80
Path:   /usr/ports/archivers/p5-Archive-Extract
Info:   Generic archive extracting mechanism
Maint:  perl@FreeBSD.org
Index:  archivers perl5
B-deps: perl5-5.24.1
R-deps: perl5-5.24.1
E-deps:
P-deps:
F-deps:
WWW:    http://search.cpan.org/dist/Archive-Extract/

索引从端口的名称和端口目录的完整路径开始。Info提供了端口的简短描述。Maint标题列出了端口的维护者,即负责将该软件集成到端口集合中的个人或团队。Index部分列出了该端口可能归类的所有类别。列出的第一个类别是该端口在端口集合中的所在目录。在本例中,端口出现在archivers目录中。

接下来是依赖项。B-deps列出了构建依赖项——即构建此端口时必须安装的其他软件。R-deps列出了运行时依赖项,即实际运行时所需的软件。这是一个 Perl 模块,因此它需要一个 Perl 解释器。某些软件必须由特定的工具提取或解压缩,这在E-deps中指定。P-deps字段列出了用于补丁的软件依赖项——一些稀有的软件必须使用特定工具进行修补。F-deps字段类似,指定fetch 依赖项——即必须使用的特殊软件来下载软件。

最后,WWW部分给出了该软件的主页。

搜索索引

Ports Collection 包括用于搜索索引的工具。如果你需要一个特定程序,可能更好通过pkg search或者甚至locate -i来找到端口目录。将搜索 Ports Collection 留给回答类似“哪些端口使用 SNMP?”的问题。

如果你知道某个软件的名称,可以使用make searchINDEX文件中搜索它。这里,我查找包含net-snmp名称的端口:

   # cd /usr/ports
   # make search name=net-snmp
➊ Port:   net-snmp-5.7.3_12
   Path:   /usr/ports/net-mgmt/net-snmp
   Info:   Extendable SNMP implementation
   Maint:  zi@FreeBSD.org
   B-deps: perl5-5.24.1
   R-deps: perl5-5.24.1
   WWW:    http://net-snmp.sourceforge.net/

   Port:   p5-Net-SNMP-6.0.1_1rt:   p5-Net-SNMP-365-3.65
   --snip--

截至目前,FreeBSD 有几个端口名称中包含net-snmp。第一个是当前标准的 net-snmp 软件集合➊。其他的包括使用 SNMP 通过网络传输的 Perl 库,但与 net-snmp 套件无关,已经不再支持的旧版本 net-snmp,以及 Tcl/Tk 接口到 net-snmp。描述中的字段直接来自INDEX文件。

如果你不需要这么详细的信息,可以尝试make quicksearch,这样只会显示端口、路径、信息和(如果适用)关于为什么它不再存在的备注。

关键字搜索

你也可以使用端口描述中的任意字段作为关键字进行搜索。请去除关键字名称中的任何连字符。你想要查找所有依赖于 Perl 的运行时端口吗?

# make quicksearch rdeps=perl5

你可以将多个搜索词组合在一个查询中。例如,假设你想要查找所有名称中包含 Apache 且依赖于 Python 运行时的程序。

# make quicksearch name=apache rdeps=python

通过在关键字前加上* x 来排除某个单词的搜索结果。在这里,我们查找所有依赖于 Python 运行时但不*包含 Apache 名称的端口:

# make quicksearch xname=apache rdeps=python

然而,这些按字段搜索并不适用于所有软件。例如,如果你在寻找 Midnight Commander 文件管理器,你可能按名称进行搜索。

# make search name=midnight
#

好吧,这个搜索结果并不太有帮助。搜索所有字段,以匹配key这个词。

这会扫描更多字段并返回更多结果。不过,如果你正在搜索一个常见词汇,key搜索可能会提供过多的信息。使用quicksearch来精简输出结果。

# make quicksearch key=midnight

这将返回所有描述、名称或依赖中包含midnight字符串的端口。我们很快就能发现 Midnight Commander 可以在/usr/ports/misc/mc找到。

浏览 Ports Collection 的其他方式

如果你更喜欢使用网页浏览器,可以构建一个 HTML 索引。只需进入/usr/ports目录,并以 root 用户身份输入make readmes,生成一个包含端口树索引的README.html文件,以及每个端口的 HTML 文件。你可以点击浏览不同的类别,甚至查看每个端口的详细描述。

如果这些选项都不适用,可以尝试 FreeBSD Ports Tree 搜索,访问 www.freebsd.org/cgi/ports.cgi。此外,FreshPorts 搜索引擎在 www.freshports.org/ 提供了一个独立的但非常不错的搜索功能。

在网页浏览器和搜索引擎之间,你应该能够找到满足需求的软件。找到所需的端口可能是使用端口时最困难的部分。

法律限制

尽管大多数 Ports Collection 中的软件可以用于任何用途,但其中一些软件有更为严格的许可证。/usr/ports/LEGAL 文件列出了 Ports Collection 中的法律限制。最常见的限制是禁止重新分发;FreeBSD 项目不会将此类软件包含在其 FTP 站点或 CD 镜像中,而是提供如何构建它的说明。

法律限制出现在你可能没有预料到的地方。你不能下载已编译好的 Oracle Java 软件包,FreeBSD 项目也不能重新分发 Java 源代码。FreeBSD 可以并且确实会分发关于如何在 FreeBSD 上构建 Oracle Java 源代码的说明,但用户必须自行前往 Oracle 网站下载代码。幸运的是,OpenJDK 已经取代了 Oracle Java,成为大多数软件的首选,而 FreeBSD 为它提供了高质量的软件包。

类似地,一些软件禁止商业使用或嵌入到商业产品中。一些软件由于美国商务部的加密技术出口限制,无法从美国出口。^(2) 如果你正在为重新分发、出口或商业用途构建 FreeBSD 系统,你需要查看这个文件。

幸运的是,大多数 Ports Collection 中的软件可以免费用于商业或非商业用途。这些有限制的软件包是例外,而非常规。

端口包含了什么?

从端口安装软件比使用包管理工具要花更长时间,而且 Ports Collection 需要一个活跃的互联网连接。作为交换,Ports Collection 可以比包管理工具生成更优化的结果。让我们来看一个端口。以下是 dns/bind911 的内部结构,这是 ISC BIND 域名服务器的 9.11 版本:

Makefile        files           pkg-help
distinfo        pkg-descr       pkg-plist

Makefile 包含了构建端口的基本指令。如果你阅读这个文件,你会发现它只有几百行。对于如此复杂的软件来说,这并不是很多指令,而且大多数 Makefile 都要短得多。这个文件的主要内容是一些很少用到的定制设置。这里几乎没有关于 BIND 本身的信息,也没有多少关于如何在 FreeBSD 上构建软件的内容。大多数 FreeBSD 端口系统的 Makefile 都位于 /usr/ports/Mk 目录中。

distinfo 文件包含端口下载的各种文件的校验和,确保你的系统能够确认文件在传输过程中没有出现错误,并且在你获取文件之前没有人篡改它。

files 目录包含了在 FreeBSD 上构建此端口所需的所有附加文件和补丁。BIND 9.11 需要十几个补丁。大多数补丁并不是构建所必需的,因为 ISC 在 FreeBSD 上支持他们的 DNS 服务器。它们仅提供与 FreeBSD 包管理系统的集成。

pkg-descr 文件包含了软件的详细描述。

一些端口包括 pkg-help 文件,提供如何使用该端口的额外细节。

一些端口(不是这个端口)有一个pkg-message文件,包含一个模板,用于创建软件包的安装信息。

最后,pkg-plist文件是一个列出所有已安装文件的清单(“包装清单”)。端口只会安装包装清单中列出的文件。一些端口(如与 Python 相关的端口)使用自动生成的包装清单,因此如果包装清单缺失,不要感到惊讶。

这些文件结合在一起,包含了构建软件所需的工具和指令。

安装端口

如果你熟悉源代码,你可能已经注意到一个端口实际上包含的源代码非常少。没错,确实有一些补丁需要应用到源代码上,并且有一些脚本需要在源代码上运行,但没有软件的源代码!你可能会问,既然没有源代码,怎么从源代码构建软件呢?

端口和生产环境

我强烈建议你使用 poudriere 构建自己的软件包仓库,并通过该仓库管理服务器的端口。从主机上直接升级已安装的端口既麻烦又困难。像 portmaster 和 portupgrade 这样的工具目前已经过时,尽管它们可能会被更新或重写,但 poudriere 是永恒的方法。你已经被提醒了。请在可丢弃的测试系统上探索端口。

当你激活一个端口时,FreeBSD 会自动从包含的站点列表中下载相应的源代码。然后,端口会检查下载的代码是否有完整性错误,将代码提取到临时工作目录,打上补丁,构建它,安装所有内容,并将安装记录到包数据库中。如果该端口有依赖项,并且这些依赖项没有安装,它会中断当前端口的构建,开始从源代码构建依赖项。要触发所有这些操作,你只需要进入端口目录并输入:

# make install

当端口执行其工作时,你会看到大量文本滚动通过终端,完成后你会重新获得命令提示符。

然而,随着你在从源代码构建方面经验的增加,你会发现这种一体化的方法并不适用于所有场合。别担心;端口集合提供了按照你希望的方式进行端口构建的能力,因为make install实际上会运行一系列子命令。如果你指定其中一个子命令,make(1)会运行所有前面的命令以及你指定的命令。例如,make extract会运行make configmake fetchmake checksummake dependsmake extract。这些子命令的顺序如下:

make config

许多端口有可选组件。运行make config可以让你选择希望在该端口中支持的选项。你选择的选项会保存在/var/db/ports中,以便将来构建该端口时使用。这些选项会影响端口的构建方式——例如,如果你选择使用 net-snmp 支持来构建一个程序,那么你就在添加对 net-snmp 的依赖。我们在本章后面的“端口自定义选项”一节中更详细地讨论了make config,第 373 页也有相关内容。

make fetch

配置好端口后,系统会搜索一个预先配置的互联网站点列表以查找程序源代码。端口的Makefile可能会列出该文件的权威下载站点,或者可能使用 Ports Collection 提供的多个权威列表之一。当端口找到源代码时,它会将其下载。原始下载的源代码称为distfile,并存储在/usr/ports/distfiles中。

如果端口需要特定的程序来获取 distfile,端口会在make fetch过程中安装该程序。

make checksum

make checksum步骤计算 distfile 的加密哈希值,并将其与端口的distinfo文件中记录的哈希值进行比较。文件可能会以多种方式损坏:在下载过程中、在下载站点上被恶意入侵者篡改,或由于某种随机的不可预见情况。校验和验证可以检测任何原因导致的文件损坏,并在文件损坏时停止构建。

该步骤不会尝试确定文件为何或如何损坏。对于端口而言,源代码在下载过程中是否损坏,或是某个恶意入侵者在你下载之前将后门代码加入 distfile,都无关紧要。无论如何,都不要浪费时间构建它,当然也不要安装它!

足射法 #839:忽略校验和

软件作者,特别是自由软件作者,有时会对代码做出小的更改,但不会更改软件版本或 distfile 的文件名。FreeBSD 端口会正确地发现这个问题,并在发生此类更改后无法正常工作。如果你完全确定 distfile 没有被篡改或损坏,你可以通过make NO CHECKSUM=yes install来覆盖此检查。

我强烈建议在这样做之前,咨询软件的原作者——而不是端口维护者。与作者确认能确保你没有安装被篡改的软件,并有助于教育软件作者理解版本号和发布工程的重要性。

make depends

许多软件是建立在其他软件之上的。虽然 FreeBSD 包括了 make(1) 和编译器,但有些软件只能用特定的编译器编译,或要求特定版本的 make。也许源代码文件使用了一种罕见的压缩算法分发。可能它需要一个 FreeBSD 没有提供的第三方库。在 make depends 阶段,端口会检查缺失的依赖项,并尝试通过构建端口来解决这些问题。

依赖项可能也有它们自己的依赖项。make depends 会递归地处理依赖关系,直到端口拥有构建、安装和运行所需的一切。

make extract

一旦 FreeBSD 拥有了端口的源代码文件,它必须解压并提取它们。大多数源代码会使用类似 gzip(1)、bzip(1) 或 xz(1) 这样的工具压缩,并使用 tar(1) 归档。此命令会在端口中创建一个 work 子目录,并在那里解压 tar 包。如果端口需要特定的程序来提取源代码文件,它会在此时安装该程序。

make patch

此命令将端口中的任何补丁应用到 work 子目录中提取的源代码。如果端口需要特定的补丁程序,而不是基础系统中的 patch(1),它将在此时安装该程序。

make configure

接下来,FreeBSD 会检查软件是否有配置脚本。这与端口执行的 make config 步骤不同。如果软件自带了配置脚本,端口会运行它。有些端口在这个阶段会中断构建,提示用户输入信息,但大多数会默默运行。

make build

这一步编译已检查、解压、打补丁并配置好的软件。那些不需要编译的端口在这里可能没有任何操作。有些端口仅用于方便地打包其他端口。

make install

最后,make install 安装软件并告诉包管理系统记录其存在。

端口自定义选项

许多软件包有大量的自定义构建功能。虽然为任何个别软件启用这些功能并不难,但没有通用的方法来定义它们。对于某些软件,你可能需要编辑原始软件的 Makefile;对于另一些,你可能需要向配置脚本提供标志。学习如何进行这些更改需要时间,而且可能会让人烦恼。FreeBSD Ports Collection 提供了两种方法来一致地配置系统上的这些选项。

较新且更美观的方法由 make config 支持。这会弹出一个对话框,类似于你首次安装 FreeBSD 时看到的界面。例如,流行的访问控制系统 sudo (www.sudo.ws/) 支持 LDAP、审计,并且最重要的是,当用户输入错误密码时会侮辱用户。如果你进入 /usr/ports/security/sudo 目录并输入 make config,你会看到一个类似于 图 16-1 所示的菜单。

使用空格键选择你喜欢的选项,使用箭头键和 TAB 键移动。按 ENTER 键选择 OK 或 Cancel 以完成。端口会将你选择的选项记录在 /var/db/ports/_/options 中。当你需要重建或升级端口时,端口会重复使用这些选项,除非你运行 make config 来更改它们,或者运行 make rmconfig 来清除它们。

image

图 16-1:端口配置

在命令行进行自定义

有时候,你不想要一个漂亮的箭头选择菜单,而是想要一个适合系统管理员使用的命令行界面。Ports Collection 让你跳过菜单,直接在 make(1) 命令中提供所有配置选项。在你进行此操作之前,你需要关闭漂亮的菜单。在命令行上设置环境变量 BATCH=1 来关闭菜单。在这里,我们使用默认配置构建端口,正如 FreeBSD 包集群所做的那样:

# make BATCH=1 install

现在你已经摆脱了烦人的菜单,看看端口支持的配置选项。make pretty-print-config 命令会以易于阅读的格式显示当前设置。让我们来看看 security/sudo

# make pretty-print-config
+AUDIT -DISABLE_AUTH -DISABLE_ROOT_SUDO +DOCS -INSULTS -LDAP +NLS -NOARGS_
SHELL -OPIE -SSSD

每一个选项都代表一个配置选项。标记为加号的选项已启用,而标记为减号的选项已禁用。这些选项意味着什么?运行 make showconfig 会显示所有端口的选项及其作用。

# make showconfig
===> The following configuration options are available for sudo-1.8.19p2:
     AUDIT=on: Enable BSM audit support
     DISABLE_AUTH=off: Do not require authentication by default
     DISABLE_ROOT_SUDO=off: Do not allow root to run sudo
     DOCS=on: Build and/or install documentation
     INSULTS=off: Enable insults on failures
--snip--

尽管 sudo 支持 LDAP、SSD 和各种复杂的信息源,但我真正需要的是让 sudo 在用户输入错误密码时侮辱用户。我想要 INSULTS 选项。在命令行上使用 WITH 环境变量来设置该选项。选项名称区分大小写。在这里,我设置了该选项并再次检查配置:

# make WITH=INSULTS pretty-print-config
+AUDIT -DISABLE_AUTH -DISABLE_ROOT_SUDO +DOCS +INSULTS -LDAP +NLS -NOARGS_
SHELL -OPIE -SSSD

INSULTS 选项现在已设置。

使用引号来启用多个选项。

# make WITH="INSULTS LDAP" pretty-print-config

同样,使用 WITHOUT 来禁用一个选项。

# make WITH=INSULTS WITHOUT="AUDIT NLS" pretty-print-config

如果在构建端口时保留了菜单启用状态,make config 图形菜单将会出现,但会使用你选择的选项。记住,通过设置 BATCH 变量来关闭菜单。

全局使用自定义设置

你构建端口是为了在软件中获取特定功能。通常,你希望在所有支持该功能的端口中都包含这个功能。以 LDAP 为例。如果你的企业使用 LDAP,你可能希望所有软件都能使用它。你会希望 LDAP 成为默认选项。

FreeBSD 将每次运行 make 时使用的设置存储在 /etc/make.conf 中。你可以在这里启用 LDAP 或 LibreSSL 或其他应在系统中全局出现的自定义设置。将任何希望全局应用的选项放入 make.conf 中。与命令行不同,make.conf 使用变量 OPTIONS_SETOPTIONS_UNSET

在这里,我希望在每个端口上启用 LDAP 和 INSULTS 选项:

OPTIONS_SET=INSULTS LDAP

make.conf设置对不支持该选项的端口无效。许多端口根本不认识 LDAP。我不知道除了 sudo 之外是否有其他端口提供侮辱用户的可选功能,但如果有这个功能,我需要它。

为什么要在make.conf中使用单独的选项而不是命令行?优先级。使用WITH设置的选项会覆盖使用OPTIONS_SET设置的选项。在这个例子中,我已经全局启用了侮辱功能。如果出于某种难以理解的原因,我需要某个端口不侮辱用户,^(3) 我可以在构建该端口时在命令行使用WITHOUT=INSULTS来覆盖全局默认设置。

/etc/make.conf 和单个端口

也许你想用特定选项构建某个端口,但又不想在命令行中指定它。可以在/etc/make.conf中使用端口类别、下划线、端口名称、另一个下划线和SET变量。

security_sudo_SET=INSULTS

虽然端口应该缓存配置,但这将提供额外的保护,防止因手误导致的错误。

设置默认版本

FreeBSD 支持多种端口自定义选项。然而,并非所有选项都是合理的端口选项。有些选项必须在整个端口集合中使用才能生效。最常见的例子是 SSL 库。你可以使用基础系统的 SSL 库构建所有端口,一切正常。你也可以使用外部 SSL 库构建所有端口,同样软件可以正常工作。然而,如果部分端口使用基础系统的 SSL,另一些使用第三方 SSL,则会导致灾难。类似的情况也适用于例如 PostgreSQL 数据库服务器和 Python 解释器的不同版本。不同的 SSL 库与不同版本的数据库服务器结合,常常会引发我非常喜欢交给初级系统管理员的那种灾难,他们急需一次无法忘记的关于共享库如何工作的教训。

端口集合使用DEFAULT_VERSIONS变量列出应作为默认使用的关键软件。这替代了像DEFAULT_MYSQL_VERWITH_BDB_VER这样的旧变量。获取完整的变量列表唯一的方法是通过/usr/ports/Mk/进行查找。bsd.default-versions.mkbsd.apache.mkMk/Uses下的文件特别有用。

在这里,我告诉端口集合始终使用 LibreSSL 构建端口,而不是使用基础系统的 OpenSSL 库,并使用 Python 3.7。

DEFAULT_VERSIONS += ssl=libressl
DEFAULT_VERSIONS += python=python3.7

我将每个默认版本列在单独的一行,并使用+=语法告诉端口系统将其添加到列表中。

我建议在构建第一个端口之前设置默认版本。否则,你最终会重建端口,以便它们链接到你偏好的库。

不要将预构建的包与使用替代 DEFAULT_``VERSIONS 构建的端口混合使用。通过包构建的程序将使用默认的库,而你的端口将使用你首选的库。如果你的系统在之后能够正常工作,那也只是偶然的结果。

预加载递归

有时候,构建一个端口时的交互性并不是问题,递归才是问题。

假设你正在构建一个大型端口,例如 LibreOffice 或 GNOME。这些端口有数十个甚至数百个依赖项。许多这些端口需要交互式配置。也许你决定在睡觉前启动一个 KDE 构建,想着醒来后会看到最新的窗口管理器,或者至少是一个有趣的错误信息。相反,你会发现依赖项的 make config 菜单自你离开后 30 秒起就耐心地等待着你的关注。

从端口构建软件的重点是你可以定制它。然而,对于这些大型构建,你确实希望在一开始就做所有的定制。这就是 make config-recursive 发挥作用的地方。

make config-recursive 会遍历所需端口的树,并对每一个端口运行 make config。你会花几分钟时间在每个端口中选择选项,或者仅在你不关心的端口上点击 OK。一旦完成递归配置,你就可以安全地在你实际需要的端口上运行 make install,然后去做其他事情。等你回来时,你将看到一个已安装的端口或一个构建失败的结果。

改变一个端口的构建选项可以增加或删除依赖项。如果你决定启用 LibreOffice 中的 SNMP 支持,^(4) 该端口将需要适当的 SNMP 库。该库的端口需要配置。重新运行 make config-recursive,直到你的所有决定不再变化。

端口系统会缓存你所有的配置选择。要删除一个端口及其所有依赖项的缓存,可以运行 make rmconfig-recursive

如果带宽时延是问题,你可以通过 make fetch-recursive 下载所有依赖项所需的所有 distfiles。如果你身处像南极这样地方,那里构建时间和服务器冷却无限制,但你每天只有几个小时的互联网连接,这个方法会非常有用。

打包依赖

有些软件有数百个依赖项,你可能并不想构建它们所有的部分。虽然我可能想要一个定制的 Emacs 构建,但我可能不想从源代码构建 gmake 和最新的 GNU C 编译器。make missing 命令会显示缺失的依赖项。你可以使用该命令挑选你想要构建的部分。

如果你不想从源代码构建任何依赖项,而是希望从包中安装它们,你可以将 make missing 传递给 pkg 命令。

# pkg install -Ay $(make -DBATCH missing)

如果有可用的包,它会被安装。你只需要从端口安装那些只能通过端口获取的东西。

端口风味

一些端口有复杂的依赖关系。虽然你可以使用 Python 2 或 Python 3 来构建 Ansible,但一个支持 Python 2 的 Ansible 软件包与支持 Python 3 的完全不同。Flavors是一种在单个端口中表达这些可能性的机制,最近才引入到 Ports 集合中。Flavors 目前还未在整个端口系统中普及,但在我写这篇文章时,它们已经在 Python、Perl、Qt 和 Emacs 中实现。你可以预期它们会越来越频繁地出现在其他地方。

要查看一个端口是否支持任何不同的版本,可以进入端口目录并运行make -V FLAVORS。在这里,我查看了流行的 Python 打包工具 Setuptools 支持的版本。

# cd /usr/ports/devel/py-setuptools
# make -V FLAVORS
py27 py36 py35 py34

我当前的端口树支持 Python 2.7、3.6、3.5 和 3.4。

要为特定版本的 Python 构建 Setuptools,请在命令行中指定版本。

# make FLAVOR=py34 install clean

如果没有指定版本,端口将使用当前默认的 Python 进行构建。要为系统设置默认的 Python,请在make.conf中设置DEFAULT_VERSIONS

构建软件包

你可以从已安装的端口创建一个软件包。然后,你可以将自定义的端口复制到其他机器并进行安装。

在创建软件包之前,创建目录/usr/ports/packages。Ports 系统将构建好的软件包放入该目录。如果没有packages目录,软件包将会被放到端口目录中,导致软件包文件散布在文件系统各处。

使用make package创建软件包。如果你想打包当前端口以及所有依赖项,可以运行make package-recursive

需要大量定制端口的人应该考虑使用 poudriere 设置自己的仓库(本章稍后会讨论),但如果你有特殊情况或者希望省事,偶尔构建单独的软件包也是可以的。

卸载和重新安装端口

虽然你可以使用pkg remove来卸载一个端口,但也可以从端口目录中卸载一个端口。在端口目录中运行make deinstall将程序从系统中删除,但会保留已编译的端口,随时可以重新安装。

卸载端口后,已编译的程序和源文件仍然保存在端口的work子目录下。运行make reinstall将重新安装已编译的程序。你可以任意多次卸载和重新安装。

跟踪端口构建状态

Ports 集合如何跟踪已完成的操作?如果你能运行make extract然后运行make install,那么 FreeBSD 如何知道已经完成了哪些步骤?Ports 集合使用隐藏文件(以点开头的文件),也就是cookie,来跟踪已完成的步骤。可以通过列出端口的work目录中的所有文件来查看这些文件:

# cd /usr/ports/security/sudo/work
# ls -a
ls -a
--snip--
.PLIST.flattened
.PLIST.mktmp
.PLIST.objdump
.PLIST.setuid
.PLIST.writable
.build_done.sudo._usr_local
.configure_done.sudo._usr_local
.extract_done.sudo._usr_local
.install_done.sudo._usr_local
.license-catalog.mk
--snip--

文件.configure_done.sudo._usr_local表示make configure步骤已经完成。

在多次执行make install/deinstall循环之后,我曾遇到过端口拒绝重新安装的情况。通常这是由隐藏文件造成的,指示安装已完成。删除该文件后,重新安装即可进行。

清理端口

端口可能占用大量磁盘空间。像 GNOME、KDE 和 LibreOffice 这样的程序,由于其许多依赖,可能会占用数十 GB 的空间!大部分内容都存储在端口的work目录中,那里存放着源代码文件以及所有中间生成的二进制部分。然而,一旦端口安装完成,你就不再需要这些文件。

使用make clean删除端口的工作文件。这将删除当前端口及所有依赖的work目录,因此在执行此操作之前,请确保你对新程序感到满意。你还可以在安装后立即清理端口,方法是运行make install clean

你可能还想删除存储在/usr/ports/distfiles中的原始 distfiles。make distclean命令会删除当前端口及所有依赖的 distfiles。

要清理整个端口树,在/usr/ports目录下直接运行make clean -DNOCLEANDEPENDS-DNOCLEANDEPENDS是可选的,但它防止了默认的递归清理。没有它,你将清理一些热门端口几十次或几百次。虽然有更快速的方法可以删除端口树中的每个work目录,但这个方法是 FreeBSD 项目直接支持的。

只读端口树

很多人不喜欢在/usr/ports中保留临时文件甚至包文件。你可以将各种工作目录移动到文件系统的其他位置,从而保持/usr/ports只读,除了更新之外。

使用make.conf中的WRKDIRPREFIX选项在单独的目录中构建端口。许多人会将此设置为类似/usr/obj的位置。

PACKAGES选项设置一个新的包目录,而不是/usr/ports/packages

最后,DISTDIR设置一个存储 distfiles 的路径,除了/usr/ports/distfiles以外。

相关地,尽管必须具有相应目录的写权限才能构建端口和包,但仍然可以在不是 root 用户的情况下进行构建。然而,只有 root 用户才能安装软件。

更改安装路径

许多环境都有关于附加软件安装的标准。我曾在一些组织中看到,/usr/local专用于该机器的特定文件,禁止将软件安装到该目录。相反,软件安装必须放入/opt或其他指定位置。

使用LOCALBASEPREFIX变量设置备用安装位置。你可以在命令行中执行此操作,但如果你遵循组织标准,建议使用make.conf。无论哪种方式,都从构建 pkg(8)本身开始。

# cd /usr/ports/ports-mgmt/pkg
# make LOCALBASE=/opt PREFIX=/opt install

该端口将所有文件安装到该目录下。例如,通常会放入/usr/local/bin的程序将会放入/opt/bin

不是每个端口都能处理从/usr/local更改LOCALBASEPREFIX。一些软件对/usr/local有硬编码依赖,而其他软件则可能存在尚未发现的 bug。如果某个端口在更改安装路径时出现问题,可以提交一个 PR(参见第二十四章)。考虑检查该端口,找出问题的原因。像这样提交修复是参与 FreeBSD 项目最简单的方式之一。

私有软件包仓库

软件包很方便,直到你需要定制版本;这时你就需要端口。同样,端口也很有用,直到你有几十台机器都需要定制的端口。在一台主机上容易构建的东西,在多台主机上就很难维护,而在大规模服务器集群中几乎不可能。当你超越端口时,你需要软件包,定制的软件包。

FreeBSD 项目使用 poudriere(发音为 poo-DRE-er)来构建软件包。为什么使用 poudriere?它是法语中的 powderkeg(火药桶)的意思。作为“tinderbox”工具的继任者,^(5) poudriere 是一组 shell 脚本,利用现有的 FreeBSD 基础设施,如监狱(jails)、tmpfs 和端口集合(Ports Collection)。

构建跨多个系统可用的软件包与在本地主机上构建软件是不同的。任何由人类管理的东西都会积累杂物。当我的桌面使用超过几个月时,我相当自信某些我所做的小改动会让它与任何新安装的系统有所不同。也许我在升级后保存了一个共享库。也许我手动安装了一些东西并忘记了它。也许 Gremlins(小怪物)篡改了链接器,我也不知道。重要的是,我的主机不再与每个运行相同操作系统的主机保持完美一致。一个在这台主机上构建和打包的端口可能包括依赖关系、库或其他什么,导致它无法在其他主机上运行。

Poudriere 通过在它自己管理的监狱(jails)中构建所有内容,避免了这个问题。一个 poudriere 可以为任何支持的 FreeBSD 版本构建软件包,即使该版本比其运行的主机版本要旧。比如,你不能在 12.4-RELEASE 主机上为 13.0-RELEASE 构建软件包,因为内核缺少必要的接口。

使用 poudriere,你可以在一台主机上构建软件包,并在所有服务器之间分发它们。虽然 poudriere 包含许多高级功能,但启动一个基本的仓库并不困难。

Poudriere 资源

构建软件包需要系统资源。你可以限制 poudriere 在构建期间使用的处理器数量,这有助于减少其内存使用。然而,虽然 poudriere 本身只有几兆字节,但监狱(jails)和构建环境可能会占用大量磁盘空间。官方的 poudriere 文档建议为每个监狱分配至少 4GB 的磁盘空间,为端口树(ports tree)分配 3GB 的磁盘空间。我通常在使用 ZFS 时为每个监狱分配大约 1GB,但我建议你还是按照推荐配置来分配空间。

Poudriere 利用 ZFS 克隆和快照来构建 jail,大大减少了所需的磁盘空间,并且越来越提高了性能。你可以在 UFS 上运行 poudriere,但它会占用更多空间并且运行得更慢。

更重要的是构建端口所需的空间。我的 Web 服务器只运行几十个软件包,其中许多非常小。Poudriere 只需要几 GB 的磁盘来构建它们。如果你在构建成百上千个软件包,你需要大量的磁盘空间。需要多少?嗯,你是在构建 GnuPG 还是构建 LibreOffice?为了估算,先构建但不要清理你所有的端口软件包,然后查看 /usr/ports 的大小。

每个主机应该只使用一个软件包仓库。是的,从技术上讲,你可以构建本地软件包并将它们与官方 FreeBSD 仓库中的软件包一起安装。问题是软件包之间是相互依赖的。你可以让主机首先检查你的仓库,然后再回退到官方仓库。然而,官方仓库每隔几天就会更新。更新之间的时间取决于构建集群中可用的硬件,但几天是一个不错的估计。你的 poudriere 更新是否与官方仓库的稍微不规律的更新完全同步?你的端口树是否与端口集群使用的完全一致?软件包应该作为一个集成集合来工作,而不是来自两个不同集合的一堆东西。问问任何 Linux 管理员,他们关于从多个仓库安装软件包的恐怖故事,然后决定自己构建所有的软件包。根据需要合理规划磁盘使用。

最后,首先在你打算安装软件包的相同架构的主机上构建软件包。如果你在为 arm64 系统构建软件包,请使用 arm64 主机来运行 poudriere。你可以在 amd64 上构建 i386 软件包,但 amd64 硬件设计本身就可以运行 i386 代码。一旦你对 poudriere 熟悉了,你可以使用 qemu-user-static 包来为较慢的平台交叉构建软件包。

你可以将 poudriere 添加到现有的生产主机吗?也许可以。在测试系统上运行几次 poudriere 可以帮助你了解你的环境所需的资源。

安装和配置 Poudriere

Poudriere 没有构建选项,因此从软件包中安装它。

# pkg install poudriere

/usr/local/etc 中配置 poudriere。你会发现一个用于配置特定软件包构建的目录,poudriere.d,但我们将从通用配置文件 poudriere.conf 开始。在这里,你将告诉 poudriere 如何运行。虽然你可以自定义目录和路径,但我们将使用默认设置。

你必须通过设置 FREEBSD_HOST 变量来告诉 poudriere 从哪里下载 FreeBSD 安装文件。如果没有本地安装镜像,请使用默认的 download.freebsd.org

FREEBSD_HOST=https://download.FreeBSD.org

Poudriere 包含 ZFS 支持功能。当然,ZFS 对 poudriere 并非必需,但如果在 ZFS 上运行,它会根据需要创建、克隆和销毁数据集。在 UFS 上运行不会妨碍 poudriere,但复制文件的速度比克隆要慢。如果你使用 UFS,取消注释NO_ZFS=yes配置选项。就这样。

ZFS 用户需要指定 poudriere 将使用的 ZFS 池。我的主操作安装可能在zroot池上,但该池位于一对我不想过度使用的闪存 SATADOM 上。我有一个专门用于处理数据的scratch池。在poudriere.conf中设置ZPOOL

ZPOOL=scratch

在你第一次运行 poudriere 之前,创建一个/usr/local/poudriere数据集。这样你会更高兴。

所有 poudriere 的工作文件都会放在/usr/local/poudriere下。如果你使用的是单独的 ZFS 池,该池上数据集的挂载点将被设置为/usr/local/poudriere下的不同位置。在 UFS 上,它是一个像其他目录一样的目录。

我的示例运行在 ZFS 上,因为我可以。poudriere 的输出在 UFS 系统上可能看起来稍有不同,但无论底层文件系统是什么,你运行的命令都是相同的。

稍后我们将看看一些 poudriere 的自定义设置,但这些足以让你入门。现在,创建用于你的包的 jails。

Poudriere Jail 创建

Poudriere 可以从各种不同来源创建 jails。你可以从不同的来源下载,或者从源树中构建,等等。请查阅 poudriere(8)以获取完整的列表。在这里,我将通过我最喜欢的三种方法安装三个不同的 jails:从互联网下载,从安装镜像安装,以及从我自定义构建的/usr/src/usr/obj安装。所有安装命令都使用相同的通用语法。某些安装方法会添加新选项,但一切都从这些命令开始。

# poudriere jail -c -j jailname -v version

jail子命令告诉 poudriere 处理某个 jail。-c标志表示创建,-j允许你为 jail 分配一个名称。一个 jail 可以有任何不包含句点的名称。我将我的 poudriere jails 命名为架构和版本号,用连字符替代任何句点。这让我得到了像amd64-12-0amd64-11-4这样的 jails。-v标志需要一个参数,即uname -r中的 FreeBSD 版本,但不包括任何补丁级别信息。如果你的主机当前运行的是 12.3-RELEASE-p20,只需使用 12.3-RELEASE 即可。补丁级别会在随后的 poudriere 运行中发生变化——是的,poudriere 会将安全补丁应用到 jails 中。

从网络安装 Jail

默认的 jail 安装从poudriere.conf中指定的下载站点获取 FreeBSD 软件。FreeBSD 的主下载站点是地理负载均衡的,因此除非你有自己的镜像站点,否则不需要使用其他站点。在这里,我创建了一个名为amd64-11-1的 jail,用于构建 11.1 包:

# poudriere jail -c -j amd64-11-1 -v 11.1-RELEASE
[00:00:00] ====>> Creating amd64-11-1 fs... done
[00:00:01] ====>> Using pre-distributed MANIFEST for FreeBSD 11.1-RELEASE amd64
[00:00:01] ====>> Fetching base.txz for FreeBSD 11.1-RELEASE amd64
--snip--

Poudriere 会访问网站并开始下载发行文件。一旦它将所有文件下载到本地,它会将/etc/resolv.conf复制到监狱中,并运行freebsd-update以获取所有最新的安全补丁。poudriere 的运行以以下内容结束:

[00:04:21] ====>> Recording filesystem state for clean... done
[00:04:21] ====>> Jail amd64-11-1 11.1-RELEASE-p1 amd64 is ready to be used

你现在可以配置这个监狱了。

从媒体安装监狱

从互联网上下载没问题,但如果你本地有安装介质怎么办?为什么要重新下载你已经保存在 ISO 或内存棒镜像中的内容呢?将这些发行文件提取到本地硬盘上,你可以用它们来创建你需要的任意数量的监狱。对于 ISO 文件,使用 tar(1)。

# tar -xf ../FreeBSD-11.0-RELEASE-amd64-disc1.iso usr/freebsd-dist

内存棒镜像稍微复杂一些;遗憾的是,libarchive 目前无法打开磁盘镜像。你必须将镜像附加到内存设备并挂载它。

# mdconfig -at vnode -f FreeBSD-11.0-RELEASE-amd64-memstick.img
md0

如果你尝试挂载/dev/md0,会遇到错误。这不是一个文件系统;它是一个分区磁盘镜像。识别磁盘上的分区。

# gpart show md0
=>      3  1433741  md0  GPT  (700M)
        3     1600    1  efi  (800K)
     1603      125    2  freebsd-boot  (63K)
     1728  1429968    3  freebsd-ufs  (698M)
  1431696     2048    4  freebsd-swap  (1.0M)

分区 3 是一个 UFS 文件系统。这看起来很有前景。^(6) 挂载它。

# mount /dev/md0p3 /mnt

发行文件现在可在/mnt/usr/freebsd-dist中找到。我可以将它们复制出来,或者直接从当前位置安装。

在这里,我创建了一个用于构建 FreeBSD 11.0 软件包的监狱。它将被命名为amd64-11-0,并使用从挂载的内存棒获取的文件。使用-m标志来指定 poudriere 应从何处获取文件。

# poudriere jail -c -j amd64-11-0 -v 11.0-RELEASE -m url=file:///mnt/usr/freebsd-dist/

请注意,-m的参数是一个 URL。我可以在这里指定一个网站,但file://是完全有效的 URL 类型。在 Unix 主机上,file:// URL 通过第三个斜杠来表示文件系统根目录。

从本地构建安装监狱

我运行的是-current 版本,并定期从源代码构建。我想为我的自定义构建创建软件包,因此监狱需要一个与我的主机匹配的 FreeBSD 版本。获取它的简单方法是从构建主机时使用的相同/usr/src安装。(你也可以使用 Subversion 下载你用来构建该系统的源代码的新副本,但这需要理解 Subversion。)使用-m 来指定源目录的位置。

# poudriere jail -c -j amd64-current -v 12.0-CURRENT -m src=/usr/src

[00:00:00] ====>> Copying /usr/src to /usr/local/poudriere/jails/amd64-current/usr/src...
--snip--

Poudriere 在/usr/obj中预构建的世界上运行make installworld来创建你的监狱。它不会运行freebsd-update,因为-current 版本不支持它。

我们将在所有未来的示例中使用amd64-current监狱。

查看监狱

要查看 poudriere 设置的所有监狱,请运行poudriere jail -l。输出非常宽,因此我无法在本书中复现,但你会看到监狱的名称、安装的 FreeBSD 版本、硬件架构、安装方法、安装时间戳以及监狱的路径。

安装 Poudriere 端口树

Poudriere 可以为不同的构建使用不同的端口树。你可能会为一台主机使用季度端口分支,为另一台主机使用当前端口树,为第三台主机使用去年的端口树。(你需要使用 Subversion 从 FreeBSD 镜像中提取特定的端口树,所以我们不会详细介绍这部分内容。)支持多个端口树意味着你必须为每个安装的端口树分配一个名称。多个 jail 可以共享一个端口树。默认情况下使用的是当前的端口树。

使用poudriere ports子命令进行所有与端口相关的操作。-c标志告诉 poudriere 创建一个端口树,-p标志让你分配名称。

# poudriere ports -cp head
[00:00:00] ====>> Creating head fs... done
[00:00:00] ====>> Extracting portstree "head"...
Looking up portsnap.FreeBSD.org mirrors... 6 mirrors found.
--snip--

Poudriere 利用了我们在本章之前讨论过的 portsnap(8)。

如果你安装了多个端口树,可以使用poudriere ports -l查看它们。

配置 Poudriere 端口

构建端口的重点在于定制它。你不需要将整个端口树都构建为包——除非你正在运行 FreeBSD 包构建集群或类似的东西!你必须告诉 poudriere 要构建哪些端口。一旦你有了这个列表,你可能需要为某些端口指定特定的选项,但你也可能需要全局选项。通常你会使用/etc/make.conf来设置这些选项,但你不希望 poudriere 使用系统的设置。Poudriere 需要一个隔离的make.conf。同样,你可能会使用make config来设置一个端口,但你如何在 poudriere 中做到这一点呢?

包列表

首先定义你希望 poudriere 构建的包列表。这个列表通常放在/usr/local/etc/poudriere.d/pkglist文件中,尽管你可以将它放在任何你想要的位置。通过类别和目录指定每个端口。要构建 poudriere 本身,可以使用类似这样的条目:

ports-mgmt/poudriere

这里的难点在于建立一个基础的包集。你必须构建主机所需的所有包。一台主机可能需要几十个或上百个包。你真的需要所有这些包吗?这些包是怎么进入系统的?

记住,你可能并没有选择安装所有这些包。你安装了像 Emacs、Apache 或 LibreOffice 这样的应用程序,而这些应用程序引入了所有这些依赖项。你关心的只是那些依赖项如何影响你需要的软件。如果 LibreOffice 失去了一个依赖项,你不希望 poudriere 再构建这个依赖项。Poudriere 会自动构建和打包依赖项。你只需要指定你需要的应用程序,让 poudriere 处理剩下的部分。

使用pkg-query(8)获取生产系统中所有非自动安装软件的列表。

# pkg query -e '%a=0' %o
www/apache24
shells/bash
sysutils/beadm
--snip--

使用这个作为包列表的基础。审查它是否有不需要的内容。从你的其他生产主机那里获取类似的列表。使用它们来汇总你的仓库包列表。

Poudriere make.conf

Poudriere 为每个监狱环境从 /usr/local/etc/poudriere.d/ 目录中的文件中组装一个独特的 make.conf 文件。文件 /usr/local/etc/poudriere.d/make.conf 包含了您希望为所有监狱环境设置的全局 make.conf 选项。其他 make.conf 文件可以覆盖这些设置,正如 poudriere(8) 中所述,但我们将专注于每个监狱环境的 make.conf 文件。

假设我希望在我的整个企业中都使用 LDAP。Poudriere 的 /usr/local/etc/poudriere.d/make.conf 文件将包含:

OPTIONS_SET=LDAP

但是,我所有运行自定义 FreeBSD 构建的主机都使用 LibreSSL。我会为该监狱环境创建一个单独的 make.conf 文件,命名为 amd64-current-make.conf。该文件将包含 LibreSSL 配置。

DEFAULT_VERSIONS += ssl=libressl

更具体的文件会覆盖一般文件。每个监狱环境文件中的设置会覆盖 poudriere 的全局 make.conf。即使启用了 LibreSSL,我也可以关闭这个监狱环境中的 LDAP。

运行 make config

使用 poudriere options 来为您的监狱环境运行 make config。每个监狱环境和端口树的组合可以有自己独特的端口选项,因此您需要在命令行中指定它们。您必须使用 -j 指定监狱环境,使用 -p 指定端口树名称,使用 -f 指定软件包文件。

# poudriere options -j amd64-current -p head -f pkglist

Poudriere 会计算出哪些端口实际被构建以及它们的所有依赖关系。它会为每个端口运行 make config

记下您选择的选项;如果其中的一些应该放入全局或每个监狱环境的 make.conf 中呢?将它们设置为默认值可以在未来的 poudriere 运行中为您节省麻烦。

现在,您可以构建您的软件包仓库。

运行 Poudriere

poudriere bulk 子命令批量构建软件包。使用 -j 指定监狱环境,-p 给出端口树名称,-f 指定软件包列表文件。(是的,这些和配置端口时使用的标志相同;好像 poudriere 设计者想保持一致性一样。)

# poudriere bulk -j amd64-current -p head -f pkglist

Poudriere 启动监狱环境,挂载所有端口,将各种配置文件复制到监狱环境中,决定构建的顺序,并开始构建。您将看到每个端口开始构建时的名称。

一些端口构建可能需要很长时间。按 CTRL-T 获取当前状态,或检查日志以查看当前状态。

构建结束时,您将看到已构建的端口列表和未能构建的端口列表。以下是来自一个小型 pkglist 的结果:

[00:04:56] ====>> Built ports: ports-mgmt/pkg devel/pkgconf security/libressl
[00:04:56] ====>> Failed ports: www/obhttpd:build

pkgpkgconflibressl 端口构建正常。它们可能无法运行,但端口集合能够构建并打包它们。然而,obhttpd 包未能构建。如果该包至关重要,我需要在允许我的客户使用这个仓库之前解决这个问题。

让我们先看看问题,然后再检查仓库。

问题端口

在构建的端口列表之后,您将看到一条消息,指示在哪里可以找到日志。

[00:04:56] ====>> Logs: /usr/local/poudriere/data/logs/bulk/amd64-current-head/2018-10-10_15h05m43s

日志文件会放在一个以监狱和端口树命名的目录中,并按日期存放子目录。如果你不想手动输入日期,可以使用方便的 latest 目录,它会直接带你到最近的日志目录。

# cd /usr/local/poudriere/data/logs/bulk/amd64-current-head/latest

这里不仅有日志文件;还有一个网站。如果你将你的 Web 服务器配置为提供 /usr/local/poudriere/data 目录,你可以使用 Web 浏览器检查 poudriere 构建(以及将仓库提供给客户端)。此处的 logs 子目录包含每个端口的 poudriere 构建日志。如果你不想一一查看这些日志,可以在 logs/errors 子目录中找到仅包含构建失败日志的文件。

现在你需要做一些非常激进的事情:阅读错误日志。也许 poudriere 无法获取 distfile,可能主机磁盘空间不足,或者发生了一些非常奇怪的事情。或者,也许端口本身与所选的构建选项不兼容。并不是所有端口都始终用所有选项构建;端口维护者很容易忽略一些少用的功能可能会出错。不过要记住,poudriere 是 FreeBSD 的官方端口构建机制。如果某个端口在 poudriere 下构建失败,那它就是坏的,你应该考虑提交一个 bug(见 第二十四章)。

软件包仓库

/usr/local/poudriere/data/packages 目录下查找已完成的包。每个监狱和端口树的组合都有自己的子目录。我在 amd64-current 监狱上使用端口树的主干构建这组包,因此我的新仓库位于 /usr/local/poudriere/data/packages/amd64-current-head。你会找到各种 .txz 文件和 Latest 子目录,其中存放最新的包。

恭喜你。你已经拥有了一个私有软件包仓库。接下来,应该让你的客户端使用它。

使用私有仓库

使用私有仓库的最简单方法是在 poudriere 主机上。pkg(8) 的本地仓库配置文件位于 /usr/local/etc/pkg/repos。该目录默认不存在,因此需要创建它。

# mkdir -p /usr/local/etc/pkg/repos

在其中创建一个 FreeBSD.conf 文件。本地仓库配置会增强或覆盖系统默认设置——这是 UCL 内置的功能。我们需要在默认仓库配置文件 /etc/pkg/FreeBSD.conf 中添加一个设置。

FreeBSD: {
        enabled: no
}

这样会保留 /etc/pkg/FreeBSD.conf 文件不变,但会将名为 FreeBSD 的仓库的 enabled 设置为 no。默认仓库不再使用。

现在为我们的自定义仓库创建一个单独的配置文件。我将这个仓库命名为 amd64-current,与监狱的名称一致。

amd64-current: {
        url: "file:///usr/local/poudriere/data/packages/amd64-current-head",
        enabled: yes,
}

你的主机现在已经准备好使用这些包了。你需要强制重新安装所有当前的包,以停止使用 FreeBSD 仓库的版本,并使用你本地的版本。

# pkg install -fy

pkg(8) 程序将下载仓库目录,但下载的过程会与平时有所不同。

--snip--
Updating amd64-current repository catalogue...
Fetching meta.txz: 100%    260 B   0.3kB/s    00:01
Fetching packagesite.txz: 100%   17 KiB  17.4kB/s    00:01
Processing entries: 100%
amd64-current repository update completed. 62 packages processed.
--snip--

与官方仓库目录相比,这个目录相当小。它提取目录和元数据只需要一秒钟。最后一行显示该仓库只有 62 个软件包。你正在使用新的仓库。安装你的自定义软件包!

远程自定义仓库

包仓库的核心理念是你只需构建软件包一次,然后将其部署到各地。你可以使用只读 NFS 导出将软件包提供给本地机器,但互联网常常会滥用公开可访问的 NFS 服务器。pkg.conf 文件通过 URL 定义仓库位置。虽然我使用了文件作为 URL,但没有理由这个仓库不能改为使用网站。你可以在包构建器上安装一个 web 服务器,让它提供 /usr/local/poudriere/data/packages 中的内容给其他服务器。然后,将应该使用该仓库的其他主机配置其自己的仓库配置。

amd64-current: {
        url: "https://pkg.mwl.io/amd64-current-head",
        enabled: yes,
}

我们所有的机器现在都得到了一套相同的定制端口。这一改变让我的手下 Bert 不再需要在十几台机器上构建端口,而是开始为我擦车。

所有 Poudriere,无论大小

Poudriere 默认运行得相当好,但有几个选项可以在小型和大型系统上提供帮助。

小型系统

如果你有一台资源受限的主机,你不希望让 poudriere 随意运行。这里有一些 poudriere.conf 选项来限制它。

一般来说,如果你能在主机上构建一个端口,poudriere 就能构建该端口。你不希望的是多个同时运行的 poudriere 进程淹没主机。Poudriere 通常会运行与主机处理器数量相同的并发进程。使用 PARALLEL_JOBS 选项来限制并行构建的数量。

PARALLEL_JOBS=1

其他限制,比如减少 poudriere 构建可以使用的内存,比你想象的要少用。软件构建所需的内存与它所需的相同。仅用 1GB 内存构建 LibreOffice 结果不会好。

记住,你也可以通过 nice(1) 全局降低 poudriere 进程的优先级,正如 第二十一章 所讨论的那样。

大型系统

Poudriere 可以利用强大的系统加速构建。你不能加速磁盘,但可以利用内存使用 tmpfs(5) 来处理构建中的关键部分。设置 USE_TMPFS 选项以使用内存作为工作目录。

USE_TMPFS=yes

你可以使用 tmpfs(5) 来存放工作目录之外的部分构建内容,但我们中的少数人拥有那样多的内存。有关详情,请阅读 poudriere.conf.sample

如果你构建许多软件包仓库,可以研究 poudriere 对缓存的支持(*ccache.samba.org/)。你每个 jail 大约需要 5GB 的磁盘空间,但能节省大量重建软件包的时间。

更新 Poudriere

新的端口不断被添加,并且带有新的选项。其他软件项目也会持续发布新版本,而 FreeBSD 端口会相应地更新。你肯定希望这些新版本能够在你的服务器上使用。如果你使用 poudriere 构建端口,更新过程会相当简单。你需要更新你的监狱和端口树。然而,在更新之前,确保 poudriere.conf 已经设置好以处理更新。

/USR/PORTS/UPDATING

在更新端口之前,检查 /usr/ports/UPDATING,看看是否有任何可能影响你环境的特别说明。Python 或 Perl 默认版本的意外变化可能会让你的一整天都泡汤。

Poudriere 提供了两种处理依赖关系变化的选项。你需要启用这两者。CHECK_CHANGED_DEPS 告诉 poudriere 不要信任之前的依赖关系计算,而是重新执行这些检查。这可以捕捉到底层 Perl、Python 等的变化。类似地,CHECK_CHANGED_OPTIONS 告诉 poudriere 验证每个端口的选项。将其设置为 verbose 会告诉 poudriere 显示任何变化。

CHECK_CHANGED_OPTIONS=verbose
CHECK_CHANGED_DEPS=yes

现在你可以更新你的监狱和端口树。使用 -u 标志来更新监狱。通过 -j 指定监狱的名称。在这里,我更新了 poudriere 的 amd-11-1 监狱。

# poudriere jail -j amd64-11-1 -u

对于从官方媒体安装的监狱,poudriere 会运行 freebsd-update(8) 并应用任何缺失的安全补丁。如果你是从源代码安装的,poudriere 会重复安装过程。

类似地,使用 -u 更新端口树。通过 -p 指定端口树的名称。

# poudriere ports -p head -u

你会看到 poudriere 使用 portsnap(8) 获取最新的更新。现在你可以像第一次那样构建新版本的软件包仓库。

# poudriere bulk -j amd64-current -p head -f pkglist

Poudriere 会确定哪些需要更新,哪些必须重新构建,并相应地执行操作。构建完成后,客户可以从仓库中升级他们的软件包。

更多 Poudriere

Poudriere 拥有比我在这里介绍的更多功能。你可以通过 PKG_REPO_SIGNING_KEY 变量对你的软件包进行加密签名。软件包集合允许你为不同的仓库定义不同的构建选项。想要构建一个实验性的包并运行最新的 Python 吗?看看软件包集合。你还可以将某些端口列入黑名单,确保它们永远不会被构建,即使它们作为依赖被调用。查看 poudriere(8) 了解更多实用功能。

在端口和 poudriere 之间,你现在可以根据需要定制软件。如果你真的想深入了解 Ports Collection,查看 FreeBSD Porter’s Handbook 在 www.freebsd.org/ 上的内容。其他人则可以继续了解 FreeBSD 的一些高级软件功能。

第十七章:高级软件管理**

image

FreeBSD 提供了独特的功能,帮助系统管理员更好地满足用户需求。了解系统的真正工作原理有助于您做出更好的决策。例如,虽然多个处理器、多核处理器和硬件线程都能提高系统性能,但它们并不总是像您想象的那样有效。了解不同类型的多处理如何影响不同类型的工作负载,能告诉您在哪些地方可以提高性能,哪些地方不能。

为了让您的程序在启动时自动启动,并在关机时干净地停止,您必须能够创建和编辑正确的启动和关机脚本。虽然有些程序在操作系统关闭时能够优雅地停止,但其他程序(例如数据库)则要求更温和的关机。干净地启动和停止系统服务是一个很好的习惯,所以我们将进一步了解 FreeBSD 的启动和关机脚本。

在正常情况下,您永远不需要知道 FreeBSD 的链接和共享库支持是如何工作的,但我们还是会讨论这些内容。为什么?因为正常情况在计算机行业中,奇怪地说,是相当罕见的。

最后,FreeBSD 可以通过 Linux 兼容层运行 Linux 软件,也可以运行为其他硬件架构编写的软件。

使用多个处理器:SMP

如果您的桌面或服务器是在过去 10 年内构建的,它几乎可以肯定配备了多个处理器。其中一些处理器用于专用目的,例如您显卡中的图形处理器。现代操作系统使用 对称多处理(SMP),即多个相同的通用处理器。现代硬件包括许多不同的专用处理器,例如显卡和服务器远程管理等,但呈现给操作系统的硬件是相同的处理器。

SMP 系统相比单个处理器有许多优势,而且不仅仅是显而易见的“更多的力量!”如果从微观层面考虑,在非常短的时间内,CPU 一次只能做一件事。计算机上的每个进程都在争夺处理器时间。如果 CPU 正在执行数据库查询,它就无法接受以太网卡试图传送的包。每一秒的一小部分时间,内核会指示 CPU 执行 上下文切换,并处理另一个请求。这种情况发生得如此频繁和快速,以至于计算机看起来像是在同时做很多事情——就像电视画面看起来在移动,其实它只是在以非常快的速度依次显示单个画面一样。

我的桌面上有 cwm 提供窗口管理,Firefox 打开了成千上万的标签页,LibreOffice 正在接受我的输入。还有一堆终端窗口连接着 SSH 会话。网络中断不断到达;屏幕上显示着文本;MP3 播放器正在播放 Nurse With Wound 的 Stereohenge 专辑。计算机的“无缝多任务处理”看起来对我那脆弱的大脑而言确实很无缝。实际上,计算机只是非常快速地从一个任务切换到另一个任务。一个毫秒,它在将另一个声音片段传输到我的耳机,接下来的一个毫秒,它更新屏幕上的文本。

如果有多个处理器,你的计算机的确可以同时执行多个操作。这非常有用——但系统复杂性也急剧上升。

内核假设

要理解 SMP 以及它相关的问题,我们必须深入研究内核。所有操作系统在支持 SMP 时面临相同的挑战,这里的理论适用于各种平台。接下来是一个粗略的简化。内核设计是一个复杂的话题,几乎不可能有任何描述能够完全阐述它。然而,下面是一个大致的尝试。

FreeBSD 将 CPU 使用率划分为时间片。时间片是 CPU 执行一个任务的时间长度。一个进程可以使用 CPU,或者使用整个时间片,或者直到没有更多工作要做为止,此时下一个任务可能会运行。内核使用基于优先级的系统来分配时间片并决定哪些程序可以在哪些时间片中运行。如果一个进程正在运行,但一个更高优先级的进程出现,内核允许第一个进程被中断,或抢占。这通常被称为抢占式多任务处理

尽管内核正在运行,但它并不是一个进程。任何进程都有由内核设置的某些数据结构,内核根据需要操作这些数据结构。你可以把内核看作一种特殊类型的进程,它的行为与其他进程大不相同。它无法被其他程序中断——你不能输入 pkill kernel 来重启系统。早在以前,内核可能被称为控制进程监控程序

内核面临着系统其他部分没有的特殊问题。假设你有一个程序在通过网络发送数据。内核接受来自程序的数据并将其放入一块内存中,准备交给网络卡。如果计算机一次只能做一件事,那么在内核回到该任务之前,这块内存或网络卡不会发生任何变化。然而,如果你有多个处理器,计算机可以同时执行多个任务。那么,如果两个不同的 CPU,都在处理内核任务,同时指示网络卡执行不同的操作,会发生什么呢?网络卡的表现就像你在一边听老板吼叫,另一边听配偶抱怨;你做的任何事都无法让他们满意。如果一个 CPU 为网络任务分配了内存,而另一个 CPU 为文件系统任务分配了相同的内存,会怎样呢?内核会变得混乱,结果不会让你满意。

为单个处理器设计的类 Unix 内核声明内核是非抢占式的,不能被中断。这简化了内核管理,因为一切都变得完全可预测:当内核的一部分分配内存时,它可以指望在执行下一条指令时该内存不会改变。内核的其他部分不会改变那块内存。当计算机一次只能做一件事时,这是一种安全的假设。然而,一旦开始同时做多件事,这个假设就会崩溃。

SMP:第一次尝试

FreeBSD 中 SMP 支持的第一次实现非常简单。进程在 CPU 之间分散,达到粗略的负载平衡,并且内核上有一个锁。在一个 CPU 尝试运行内核之前,它会检查锁是否可用。如果锁是空闲的,CPU 会获得锁并运行内核。如果锁不可用,CPU 就知道内核正在其他地方运行,然后继续处理其他任务。这个锁被称为大锁(BGL),或者后来简称为Giant。在这种系统下,内核可以确保数据不会被其下方的进程改变。本质上,Giant 保证了内核只会在一个 CPU 上运行,就像它一直以来的工作方式一样。

这种策略对两个 CPU 的情况勉强有效。你可以在一台双 CPU 机器上运行一个中型数据库和一个 Web 服务器,并且有信心 CPU 不会成为瓶颈。如果一个 CPU 忙于提供网页服务,另一个 CPU 就可以空闲来处理数据库查询。但如果你有一台八 CPU 的机器,你就麻烦了;系统会花费大量时间等待 Giant 变得可用!

这种简化的 SMP 技术既不高效也不可扩展。标准的 SMP 教科书很少提到这种方法,因为它非常笨重。然而,其他一些 SMP 处理方法更糟。例如,微软早期的几个服务器操作系统版本将一个处理器专用于用户界面,另一个处理器用于其他所有任务。这种技术也很少出现在教科书中,尽管它确实能使鼠标更加灵敏。

今天的 SMP

然而,一旦你拥有了内核的锁,你就可以将这个锁分割开。FreeBSD 已经将 Giant 锁分解成许多更小的锁,现在内核的每个部分都使用最小的锁来执行任务。最初,这些锁被实现于内核的核心基础设施中,如调度器(内核中负责分配任务时间片的部分)、网络栈、磁盘 I/O 栈等。这立即提高了性能,因为当一个 CPU 在调度任务时,另一个可以处理网络流量。随后,锁被推向了更低层次的各个内核组件。网络栈的每个部分开发了自己的锁,I/O 子系统的每个部分也都如此——使得内核能够利用多个处理器同时执行多个任务。这些独立的内核子进程被称为线程

每种类型的锁定都有其独特的要求。你将会看到许多不同的锁,例如互斥锁、sx 锁、读写锁、自旋互斥锁、信号量、读多写少锁等等。每种锁都有其优缺点,且每种锁必须在内核中小心使用。

精细化锁定比听起来要难得多。锁得太细,内核花费更多时间处理锁,而不是推送数据;锁得太粗,系统浪费时间等待锁可用。适用于 2 处理器系统的锁定会使 32 处理器系统停滞不前,适用于 32 核心主机的锁定对新的 192 核心系统来说完全不够。锁定调整和调优已经花费了数年时间,仍在进行中,并将永远持续下去。

尽管内核的每个部分都使用当前可能的最小锁,但有时那个锁就是 Giant 锁。拔掉 USB 设备意味着在短暂的一瞬间抢占 Giant 锁,内核会说:“暂停一切!我正在重新配置硬件!”一些设备驱动程序仍然使用 Giant 锁,一些虚拟内存栈中的棘手部分、sysctl 处理程序等也使用它。

SMP 问题:死锁和锁顺序反转

所有这些内核锁都有复杂的使用规则,它们之间以各种方式相互作用。这些规则是为了防止锁之间发生意外的相互作用。假设内核线程 A 需要资源 Y 和 Z,内核线程 B 也需要 Y 和 Z,但 B 需要 Z 后才需要 Y。如果 A 锁定了 Y,而 B 锁定了 Z,那么 A 就会等待 Z,而 B 则会等待 Y。两个线程都无法继续,直到丢失的资源被释放。这种死锁(也叫致命的拥抱)会使系统不稳定,可能导致系统崩溃。正确的锁定方式可以避免这个问题,和其他一些问题。

您可能会看到控制台上出现关于锁定顺序反转的警告,意味着锁定顺序错误地应用了。虽然这个内核通知并不总是预示着灾难的来临,但关注它仍然很重要。

WITNESS 内核选项专门用于监控锁定顺序和锁定顺序违规情况。该选项在 FreeBSD-current 上默认启用(参见第十八章),如果您报告系统出现问题,开发团队可能会要求您启用它。WITNESS 选项使内核检查每个操作是否存在锁定顺序违规,这会降低系统性能。不过,运行 WITNESS、查看消息并根据消息采取行动,是帮助改进 FreeBSD 的一个极好的方式。

处理锁定顺序反转

当您收到一个锁定顺序反转(LOR)消息时,请完整复制该 LOR 消息。除了显示在控制台上外,这些消息还会记录到 /var/log/messages 中,方便您查看。一旦获得锁定顺序消息,请在 FreeBSD-current 邮件列表中搜索 LOR 消息的前几行,看看是否有人已经提交了该问题。如果在邮件列表中找到了您的 LOR,请阅读消息并采取推荐的行动。如果开发者最近并特意要求通知进一步的此类 LOR,您才需要在邮件列表中发布“我也遇到这个问题”的消息。

如果您遇到一个新的锁定顺序反转(LOR),恭喜您!发现一个新的 LOR 并不像发现一个新的昆虫物种那样令人满足——首先,您不能给您的 LOR 命名——但它确实有助于 FreeBSD 项目。请将您的报告发送至 FreeBSD-current 邮件列表。提供有关您的系统的详细信息,特别是在 LOR 出现时所执行的工作。您可能会被要求提交一个 bug 报告,正如在第二十四章中讨论的那样。

处理器和 SMP

您会看到三种不同类型的多处理器系统:多核、多包装和硬件线程。您需要理解它们之间的区别,因为不同的处理器类型会直接影响系统和应用程序的行为。

处理器中的基本单位是CPU 核心。每个 CPU 核心由一组资源组成,如执行单元、寄存器、缓存等。曾几何时,一个核心就等同于一个处理器。

CPU 封装是插入或焊接到主板上的芯片。它就是许多人所称的“CPU”或“处理器”。那个你可能不小心踩碎的昂贵部件?那就是封装。每个封装包含一个或多个核心。在对称多处理(SMP)之前,一个封装只有一个核心。如今,大多数封装至少包含两个核心,且核心数不断增加。同一封装中的 CPU 核心可以相互快速通信。

一些主机有多个封装。多个封装为你提供多个包含多个核心的组,从而提供更多的并行性。封装之间的通信比同一封装内核心之间的通信要慢。此外,每个封装通常都有自己的内存控制器。一个封装中的 CPU 核心从另一个封装获取内存数据会花费更长的时间。是的,这意味着一个 16 核封装比两个 8 核封装的性能更好。然而,实际上,很少有软件会被多线程到足以充分利用这种差异。

最后,某些 CPU 核心通过能够同时运行多个线程来更有效地利用其执行资源。这被称为硬件线程同时多线程(SMT),或(如果你是 Intel 的话)超线程(HyperThreading)。这些额外的线程有时被称为虚拟处理器或虚拟核心。然而,虚拟处理器并不是一个完整的 CPU;例如,只有当第一个 CPU 在等待某些操作时,虚拟处理器才会被激活。FreeBSD 的默认调度程序 sched_ule(4)能够区分哪些核心是实际核心,哪些是虚拟核心,并适当地调度任务。

硬件线程引发了多种潜在的安全问题。一个虚拟处理器上运行的任务可以通过各种微妙的时间攻击,从另一个虚拟处理器上运行的任务中捕获数据,例如加密密钥。这不是一个适合脚本小子攻击的手段,但如果你不信任用户,可以通过将启动时可调参数machdep.hyperthreading_allowed设置为0来禁用硬件线程。

使用 SMP

请记住,多个处理器并不一定让系统更快。一个处理器每秒钟可以处理一定数量的操作。第二个处理器仅意味着计算机每秒钟可以处理两倍的操作量,但这些操作不一定更快。

想象一下 CPU 的数量就像道路上的车道。如果你有一条车道,你只能一次让一辆车经过某个地方。如果你有四条车道,你就可以同时让四辆车经过那个地方。虽然四车道的道路不会让这些车更快到达目的地,但它们会有更多的车同时到达。如果你认为这没有区别,可以想想如果有人把你本地的高速公路换成单车道会发生什么。CPU 的带宽很重要。

虽然一个 CPU 一次只能做一件事,但一个进程一次只能在一个 CPU 上运行。许多程序无法在多个处理器上同时执行工作。线程程序是一个例外,正如我们在本章后面将看到的那样。一些程序通过同时运行多个进程,并让操作系统根据需要将它们分配到不同的处理器来绕过这个限制。流行的 Apache Web 服务器已经这样做了很多年。线程程序是专门设计来与多个处理器一起工作的,而不需要生成多个进程。许多线程程序简单地创建大量线程来处理数据,并将这些线程分散到 CPU 上,这是一种简单的(虽然并非总是有效的)并行处理方式。其他程序根本不处理多个 CPU。

如果你发现其中一个 CPU 100% 忙碌,而其他 CPU 大部分时间处于空闲状态,那说明你正在运行一个无法处理多个 CPU 的程序。第二十一章 将深入探讨性能问题,但对于这种程序,能做的帮助不多。

SMP 和 make(1)

用于构建软件的 make(1) 程序可以启动多个进程。如果你的程序写得很干净,你可以使用多个进程来构建它。这对小程序没有帮助,但当你构建一个大程序时,比如 FreeBSD 本身(参见 第十八章)或 LibreOffice,使用多个处理器确实可以加速工作。使用 make(1) 的 -j 标志告诉系统同时启动多少个进程。一个好的选择是系统中处理器或核心的数量加一。例如,在一台双处理器系统中,每个处理器有两个核心,我会运行五个进程来构建一个程序。

# make -j5 all install clean

一些程序员没有正确设计他们的Makefile,因此他们的程序无法处理 -j 标志。如果构建出现问题,停止使用 -j 再试一次——或者,更好的是,找出问题并向作者提交 bug 报告。

线程、线程、还有更多线程

你会在各种上下文中听到一个词——线程。一些 CPU 支持超线程(HyperThreading)。一些进程有线程。内核的某些部分作为线程运行。我的裤子上有很多很多的线(虽然有些比我妻子认为那些裤子应该在公共场合穿着所需的线少)。这些线程都是什么,它们意味着什么呢?

在大多数情况下,线程是一个轻量级进程。记住,进程是系统中的一个任务,一个正在运行的程序。进程有自己在系统中的进程 ID,并可以由用户启动、停止及管理。线程是进程的一部分,但它们由进程管理,不能直接由用户访问。一个进程一次只能做一件事,但个别线程可以独立执行。如果你有一个多处理器系统,一个进程可以在多个处理器上同时运行线程。

任何多线程程序都需要使用一个 线程库,该库通过与内核交互,告诉应用程序如何在该操作系统上使用线程。线程库以不同的方式实现线程,因此使用特定的库可能会影响应用程序的性能。

类似地,内核线程是内核中的一个子进程。FreeBSD 有内核线程来处理 I/O、网络等操作。每个线程都有自己的功能、任务和锁定。内核中的线程不使用任何用户空间的库。

硬件线程是虚拟的 CPU 核心,正如在“使用多个处理器:SMP”的 第 396 页中所讨论的那样。虽然你需要了解硬件是什么以及它如何影响你的系统,但硬件线程实际上并不属于线程的一部分。

启动和关闭脚本

service(8) 命令是系统启动和关闭脚本的前端。这些脚本被称为 rc 脚本,源自 /etc/rc,即管理多用户启动和关闭过程的脚本。虽然主要的 rc 脚本位于 /etc/rc.d,但其他位置的脚本用于管理附加软件。Ports 和 packages 安装启动脚本,但如果你安装了自己的软件,你需要创建自己的 rc 脚本。如果你之前没有使用过 shell 脚本,请仔细阅读。Shell 脚本并不难,学习的最好方法是阅读示例并在这些示例的基础上进行变动。此外,修改现有软件包的启动或关闭过程需要理解启动脚本的功能。

在启动和关闭过程中,FreeBSD 会检查 /usr/local/etc/rc.d 目录,寻找额外的 shell 脚本并将其集成到启动/关闭过程中。(你可以使用 local_startup rc.conf 变量定义额外的目录,但目前我们假设你只有默认目录。)启动过程会特别查找可执行的 shell 脚本,并假定它找到的任何脚本都是启动脚本。它以 start 参数执行该脚本。在关闭过程中,FreeBSD 以 stop 参数运行这些相同的命令。预期这些脚本会读取这些参数并采取适当的行动。

rc 脚本排序

几十年来,类 Unix 操作系统在启动脚本中编码了服务启动顺序。这变得非常烦人,非常快。许多(但不是所有)Unix 系统已经摒弃了这种方式。类似地,FreeBSD 的 rc 脚本按照顺序排列。每个 rc 脚本会标识出在启动之前需要哪些资源。rc 系统利用这些信息对脚本进行排序。这个过程在启动和关闭时由 rcorder(8) 执行,但你随时可以手动进行,以查看它是如何工作的。只需将 rcorder(8) 路径传递给你的启动脚本作为参数。

# rcorder /etc/rc.d/* /usr/local/etc/rc.d/*
/etc/rc.d/growfs
/etc/rc.d/sysctl
/etc/rc.d/hostid
/etc/rc.d/zvol
/etc/rc.d/dumpon
/etc/rc.d/ddb
/etc/rc.d/geli
/etc/rc.d/gbde
--snip--

rcorder(8) 程序将 /etc/rc.d/usr/local/etc/rc.d 目录中的所有脚本按系统启动时的顺序排序,使用脚本本身内的标记。如果你的 rc 脚本有任何排序错误,例如死锁脚本,这些错误会出现在 rcorder(8) 输出的开头。

一个典型的 rc 脚本

rc 脚本系统非常简单——虽然脚本本身可以变得复杂,但复杂性来自于脚本所运行的程序,而不是 rc 系统。启动 NFS 服务器的脚本有一堆依赖关系和需求。而像 timed(8) 这样的简单守护进程的脚本则体现了 rc 系统的简单性。

   #!/bin/sh

➊ # PROVIDE: timed
➋ # REQUIRE: DAEMON
➌ # BEFORE:  LOGIN
➍ # KEYWORD: nojail shutdown

➎ . /etc/rc.subr

➏ name="timed"
➐ desc="Time server daemon"
➑ rcvar="timed_enable"
➒ command="/usr/sbin/${name}"

➓ load_rc_config $name
   run_rc_command "$1"

PROVIDE 标签 ➊ 告诉 rcorder(8) 这个脚本的官方名称。这个脚本叫做 timed,以 timed(8) 命名。

REQUIRE 标签 ➋ 列出了在此脚本运行之前必须运行的其他脚本。需要在启动前运行 timed 的脚本会在 REQUIRE 中列出 timed。此脚本可以在 DAEMON 脚本运行后任何时间运行。

BEFORE 标签 ➌ 允许你指定该脚本后应运行的其他脚本。此脚本应在 LOGIN 脚本之前运行。/etc/rc.d/LOGIN/etc/rc.d/timed 都指定它们必须在 DAEMON 之后运行,但 BEFORE 标签允许你设置额外的排序要求。

KEYWORD 命令 ➍ 让启动系统只选择某些特定的启动脚本。timed(8) 脚本包括 nojailshutdown。即使启用,监狱环境也不会运行这个脚本。此脚本在系统关闭时运行。

/etc/rc.subr 文件 ➎ 包含了 rc 脚本基础结构。每个 rc 脚本都必须包含它。

虽然脚本有一个名字,但脚本运行的程序可能有一个独立的名字 ➏。通常情况下,名为 timed 的 rc 脚本会运行名为 timed 的程序。

description 字段 ➐ 提供了该脚本所提供服务的简要描述,正如你所预期的那样。

rcvar 语句 ➑ 列出了切换此脚本的 rc.conf 变量。

command ➒ 明确指定了该脚本应运行的命令——毕竟,系统中可能有多个相同名称的命令,只是位于不同的目录中。

脚本执行的最后两个操作是从 /etc/rc.conf 加载该服务的配置 ➓,然后实际运行命令。

虽然这看起来可能有点让人畏惧,但实际上并没有那么难。开始编写你的自定义 rc 脚本时,可以先复制一个现有的脚本。将命令名称设置为你的命令,并适当地更改路径。确定在运行脚本之前必须完成哪些操作:你需要网络正常运行吗?你需要某些守护进程已经启动,还是需要在某些守护进程之前运行你的程序?如果你实在不确定,可以通过使用REQUIRE语句并指定系统上最后运行的脚本的名称,将脚本安排在最后运行。通过查看其他提供类似功能的 rc 脚本,你将学会如何在启动脚本中做几乎任何事。

使用这个简单的脚本,你可以通过将信息添加到/etc/rc.conf来启用、禁用和配置你的程序。例如,如果你的自定义守护进程名为tracker,启动脚本会在/etc/rc.conf中查找变量tracker_enabletracker_flags,并在每次运行启动脚本时使用它们。

特殊的 rc 脚本提供程序

你可能注意到我们示例中有名为DAEMON的服务,并且可能会想:“这很奇怪,我不知道有什么叫DAEMON的系统进程。”其实那并不是一个进程。rc 系统有一些特殊的提供程序,用于定义启动过程中的重要节点。使用这些提供程序可以简化编写 rc 脚本的过程。

FILESYSTEMS 提供程序确保所有本地文件系统都已按/etc/fstab中的定义挂载。

网络提供程序在所有网络功能配置完成后出现。这包括在网络接口上设置 IP 地址、PF 配置等。

SERVERS 提供程序表示系统已经具备了支持基本服务器所需的基本功能,如 named(8)和 NFS 支持程序。但此时远程文件系统尚未挂载。

守护进程提供程序确保所有本地和远程文件系统都已挂载,包括 NFS 和 CIFS,并确保更高级的网络功能,如 DNS,已正常运行。

在登录时,所有网络系统服务已启动,FreeBSD 开始启动支持通过控制台、FTP 守护进程、SSH 等进行登录的服务。

通过在自定义 rc 脚本中的REQUIRE语句中使用这些提供程序,你可以大致指定何时希望你的自定义程序运行,而无需深入细节。

供应商启动/关机脚本

也许你正在安装一个复杂的软件包,而供应商并不支持 FreeBSD 的 rc 系统。这不是问题。大多数供应商提供的脚本预计会接收一个参数,如startstop。记住,在启动时,FreeBSD 会以start作为参数运行每个 rc 脚本,而在系统关机时,它会以stop作为参数运行脚本。通过将PROVIDEREQUIRE语句作为注释添加到此供应商脚本中,并确认它接受这些参数,你可以确保脚本在启动和关机过程中在正确的时机运行。

在管理脚本中使用 rc 系统功能不是强制性的。在启动过程的最后,FreeBSD 会运行 /etc/rc.local。你可以在这里添加本地命令。然而,你不能使用 service(8) 来管理 rc.local 中的任何内容。

调试自定义 rc 脚本

本地脚本,例如通过 Ports Collection 安装的脚本,是由 /etc/rc.d/localpkg 运行的。如果你的自定义脚本引发了问题,你可以尝试通过调试运行 localpkg 脚本,查看你的脚本如何与 rc 系统交互。最好的做法是使用调试功能。

# /bin/sh -x /etc/rc.d/localpkg start

这会尝试重新启动服务器上的每个本地守护进程,这在生产系统中可能并不合适。建议先在测试系统上尝试。另外,记住 -x 调试标志不会传递给子脚本;你正在调试的是系统启动脚本 /etc/rc.d/localpkg 本身,而不是本地脚本。运行脚本时请使用 -x 标志进行调试。

管理共享库

共享库是提供公共功能给其他已编译代码的已编译代码块。共享库的设计目的是尽可能被多个不同的程序重复使用。例如,许多程序必须为数据生成 哈希 或加密校验和。如果每个程序都需要包含自己的哈希代码,程序将更难编写,并且更不容易维护。而且,如果程序实现的哈希略有不同,它们之间将会出现互操作性问题,程序作者也需要学会大量关于哈希的知识才能使用它们。通过使用共享库(在这个例子中是 libcrypt),程序可以访问哈希生成函数,而没有兼容性和维护问题。这减小了程序的平均大小,无论是在磁盘上还是在内存中,代价是复杂性增加。

共享库版本和文件

共享库有一个人类友好的名称、一个版本号和一个相关联的文件。人类友好的名称通常(但不总是)与相关文件相似。例如,版本 1 的共享库 libjail 存放在文件 /lib/libjail.so.1 中。另一方面,主 Kerberos 库的版本 11 存放在文件 /usr/lib/libkrb5.so.11 中。版本编号从 0 开始。

历史上,当库的更改使其与早期版本的库不兼容时,版本号会递增。例如,libjail.so.0 变成了 libjail.so.1。FreeBSD 团队只有在发布周期开始时才会增加这些版本号(参见 第十八章)。每个库也有一个不带版本号的库名称的符号链接,指向该库的最新版本。例如,你会发现 /usr/lib/libwres.so 实际上是一个符号链接,指向 /usr/lib/libwres.so.10。这使得编译软件变得更容易,因为软件只需要查找一般的库文件,而不是该库的特定版本。

FreeBSD 的主要库支持 符号版本控制,这使得共享库可以支持多个编程接口。通过符号版本控制,共享库会为每个程序提供该程序所需的库版本。如果你有一个程序需要版本 2 的库,版本 3 也能支持这些功能。

仅仅因为 FreeBSD 支持符号版本控制,并不意味着 Ports Collection 中的所有软件都支持它。你必须留意库版本问题。

将共享库附加到程序中

那么,程序是如何获取所需的共享库的呢?FreeBSD 使用 ldconfig(8) 和 rtld(1) 根据需要提供共享库,但也提供了一些人性化的工具,供你调整和管理共享库的处理。

rtld(1) 也许是最容易理解的程序,至少从系统管理员的角度来看。每当程序启动时,rtld(8) 会检查程序需要哪些共享库。rtld(8) 程序会搜索库目录,查看这些库是否可用,然后将库与程序链接在一起,确保一切正常工作。你不能直接用 rtld(1) 做太多事情,但它提供了将共享库连接在一起的关键“粘合剂”。

库目录列表:ldconfig(8)

系统不会在每次运行动态链接的程序时都在整个硬盘上搜索任何看起来像共享库的文件,而是通过 ldconfig(8) 维护一个共享库目录列表。(FreeBSD 的旧版本会构建一个系统中实际库的缓存,但现代版本只保持一个检查共享库的目录列表。) 如果程序找不到你知道系统中有的共享库,这意味着 ldconfig(8) 并不知道这些共享库所在的目录。^(1) 若要查看当前 ldconfig(8) 能找到的库,可以运行 ldconfig -r

# ldconfig -r
/var/run/ld-elf.so.hints:
        search directories: /lib:/usr/lib:/usr/lib/compat:/usr/local/lib:/usr/
local/lib/perl5/5.24/mach/CORE
        0:-lcxxrt.1 => /lib/libcxxrt.so.1
        1:-lalias.7 => /lib/libalias.so.7
        2:-lrss.1 => /lib/librss.so.1
        3:-lkiconv.4 => /lib/libkiconv.so.4
        4:-lpjdlog.0 => /lib/libpjdlog.so.0
--snip--

使用 -r 标志,ldconfig(8) 会列出所有共享库目录中的共享库。我们首先看到被搜索的目录列表,然后是这些目录中的各个库。我的主邮件服务器有 170 个共享库;我的主网页服务器有 244 个;我的桌面有 531 个。

如果程序在启动时因找不到共享库而崩溃,那么该库就不会出现在这个列表中。你的问题就是需要将所需的库安装到共享库目录中,或者将库目录添加到被搜索的目录列表中。你可以将每个需要的共享库复制到 /usr/lib 中,但这会使得系统管理变得非常困难——就像是文件柜中所有东西都归到 P 类别下(即 纸张)。在中长期内,添加目录到共享库列表中会是更好的选择。

将库目录添加到搜索列表中

如果你添加了一个新的共享库目录,必须将它添加到 ldconfig(8)搜索的列表中。检查/etc/defaults/rc.conf中的这些 ldconfig(8)条目:

ldconfig_paths="/usr/lib/compat /usr/local/lib /usr/local/lib/compat/pkg"
ldconfig_local_dirs="/usr/local/libdata/ldconfig"

ldconfig_paths变量列出了常见的库位置。虽然开箱即用的 FreeBSD 没有/usr/local/lib目录,但大多数系统在安装后不久会创建一个类似目录。类似地,兼容旧版本 FreeBSD 的库会放入/usr/lib/compat。由软件包安装的旧版本库会存放在/usr/local/lib/compat/pkg目录。/lib/usr/lib目录是默认被搜索的,但这个变量中的路径是共享库的常见位置。

端口和软件包使用ldconfig_local_dirs变量,将它们的共享库添加到搜索列表中,而不只是把所有东西都丢进/usr/local/lib。软件包可以在这个目录中安装一个文件。这个文件以软件包的名字命名,并包含一个列出软件包安装的库目录的列表。ldconfig 程序会检查这些目录中的文件,读取文件中的路径,并将这些路径视为额外的库路径。例如,Perl 5 软件包将共享库安装在/usr/local/lib/perl5/5.24/mach/CORE中。这个端口还会安装一个名为/usr/local/libdata/ldconfig/perl5的文件,里面只有一行内容,列出了这个路径。ldconfig 启动脚本会将这些文件中的目录添加到它检查共享库的目录列表中。

/USR/LOCAL/LIB 与每个端口的库目录

/usr/local/lib不是专门用于由端口和软件包安装的库吗?为什么不把所有共享库都放进那个目录呢?大多数端口正是这么做的,但有时使用单独的目录可以简化维护。例如,我在我的笔记本上安装了 Python 2.7,而/usr/local/lib/python27中包含了 647 个文件!如果把所有这些文件都放到/usr/local/lib中,就会压倒我非 Python 库的文件,并且让我更难找到由端口安装的只有一两个共享库的文件。

要将你的共享库目录添加到搜索列表中,可以将它添加到/etc/rc.conf中的 ldconfig_paths,或者在/usr/local/libdata/ldconfig中创建一个列出该目录的文件。两种方法都可以。一旦添加了该目录,该目录中的库将立即可用。

ldconfig(8)与奇怪的库

共享库有一些边缘情况你应该理解,还有许多你实际上不必担心的情况。这些情况包括针对不同二进制类型的库和针对其他架构的库。

FreeBSD 支持两种不同格式的二进制文件,a.out 和 ELF。系统管理员不需要了解这些二进制类型的详细信息,但你应该知道,ELF 二进制文件是现代标准,并且在 1998 年的版本 3.0 中成为了 FreeBSD 的标准。早期版本的 FreeBSD 使用的是 a.out。以某种类型编译的程序无法使用另一种类型的共享库。虽然 a.out 二进制文件已经基本消失,但由于支持它们的成本非常低,因此这个支持从未被移除。ldconfig(8) 为 a.out 和 ELF 二进制文件维护了独立的目录列表,正如你从 /etc/rc.d/ldconfig 的输出中所看到的那样。你会在 rc.conf 中找到单独配置 ldconfig(8) 使用 a.out 库的选项。几乎可以说,你不会需要使用 a.out 程序。

另一个特殊情况是当你在 64 位 FreeBSD 安装上运行 32 位二进制文件时。最常见的情况是你在运行 amd64 安装并且希望使用来自旧版本 FreeBSD 的程序时。64 位二进制文件无法使用 32 位库,因此 ldconfig(8) 为它们保持了一个单独的目录列表。你也可以在 rc.conf 中找到配置这些目录的选项。不要混合使用 32 位和 64 位库!

一些硬件平台,如 ARM,拥有针对软浮点运算的特殊版本库。你也可以在 rc.conf 中找到这些选项,指向一组新的目录。

简而言之,不要将不常用的库与标准库混合使用。这样做会让 FreeBSD 混淆,从而让你感到困扰。

LD_LIBRARY_PATH 和 LD_PRELOAD

尽管 FreeBSD 内建的共享库配置系统在你是系统管理员时工作得很好,但如果你只是一个没有 root 权限的普通用户,它就无法工作了。^(2) 此外,如果你有自己的个人共享库,你可能不希望它们被全局访问。系统管理员当然也不希望在生产程序中链接随机的用户拥有的库!这时 LD_LIBRARY_PATH 就派上用场了。

每次运行 rtld(1) 时,它会检查环境变量 LD_LIBRARY_PATH。如果该变量包含目录,它会检查这些目录中的共享库。这些目录中的任何库都会作为程序的选项被包括进去。你可以在 LD_LIBRARY_PATH 中指定任意数量的目录。例如,如果我想做一些测试并且在接下来的程序运行中使用 /home/mwlucas/lib/tmp/testlibs 里的库,我只需这样设置该变量:

# setenv LD_LIBRARY_PATH /home/mwlucas/lib:/tmp/testlibs

你可以通过在 .cshrc.login 中输入正确的命令,在登录时自动设置这个环境变量。

类似地,LD_PRELOAD 环境变量允许你先加载特定的库。你可以通过在 LD_PRELOAD 中指定完整路径来测试自定义的 libc。当 rtld(1) 运行时,它会从 LD_PRELOAD 中获取库,并忽略后续提供相同符号的库。

LD_ 环境变量与安全性

使用 LD_LIBRARY_PATHLD_PRELOAD 并不安全。如果你将此变量指向一个过于开放的目录,你的程序可能会链接到任何人放入该目录的内容。LD_LIBRARY_PATH 变量会覆盖共享库目录列表,因此,如果有人能够在你的库目录中放入任意文件,他们就能接管你的程序。出于这个原因,setuidsetgid 程序会忽略这些变量。

程序需要什么

最后,关于程序正确运行所需的库的问题。通过 ldd(1) 获取这些信息。例如,要了解 Emacs 需要哪些库,可以输入以下命令:

# ldd /usr/local/bin/emacs
/usr/local/bin/emacs:
        libtiff.so.5 => /usr/local/lib/libtiff.so.5 (0x800a78000)
        libjpeg.so.8 => /usr/local/lib/libjpeg.so.8 (0x800cf1000)
        libpng16.so.16 => /usr/local/lib/libpng16.so.16 (0x800f63000)
        libgif.so.7 => /usr/local/lib/libgif.so.7 (0x80119d000)
        libXpm.so.4 => /usr/local/lib/libXpm.so.4 (0x8013a6000)
        libgtk-3.so.0 => /usr/local/lib/libgtk-3.so.0 (0x801600000)
--snip--

这个输出告诉我们 Emacs 所需的共享库的名称以及包含这些库的文件的位置。如果你的程序找不到所需的库,ldd(1) 会告诉你。程序本身在你尝试运行时会宣布第一个缺失的共享库的名称,但 ldd(1) 会给你完整的列表,这样你可以使用搜索引擎查找所有缺失的库。

ldconfig(8)ldd(1) 之间,你应该充分准备好在你的 FreeBSD 系统上管理共享库。

重新映射共享库

偶尔,你会发现有一款软件需要使用特定的共享库,而这些库并不被系统的其他部分使用。例如,FreeBSD 的标准 C 库是 libc。你可以有一个带有特定功能的 libc 副本,只为某个特定程序提供,然后你可以只让那个程序使用这个特殊的 libc,同时其他程序使用标准的 libc。FreeBSD 允许你更改任何应用程序使用的共享库。这听起来有些奇怪,但在各种边缘情况下非常有用。开发者使用这个功能在将代码推送到整个系统之前进行小规模的测试。使用 /etc/libmap.conf/usr/local/etc/libmap.d/ 中的文件来告诉 rtld(1) 对客户端程序撒谎。

虽然 libmap.conf 条目对于软件开发非常有用,但你也可以用它们来全局替换库。一些通过包安装的视频卡驱动程序需要你使用它们的驱动程序,而不是某些系统库。少数 Nvidia 驱动程序希望提供 libGL 图形功能。不要覆盖所有程序依赖的 libGL 包:相反,重新映射这个库。你可以为整个系统、特定程序名称或特定完整路径下的程序配置库替换。

一个 libmap 文件(可以是 libmap.conf/usr/local/etc/libmap.d/ 中的文件)有两列。第一列是程序请求的共享库的名称;第二列是提供的共享库。所有更改将在下次执行程序时生效;不需要重新启动或重启守护进程。例如,在这里,我们告诉系统,每当任何程序请求 libGL 时,应该提供 Nvidia 版本的库。这些全局覆盖必须首先出现在 libmap.conf 中:

libGL.so         libGL-NVIDIA.so
libGL.so.1       libGL-NVIDIA.so.1

“我可以获得 libGL.so.1 吗?”

“当然,这是 libGL-NVIDIA.so.1。”

全局重新映射库是一个相当大胆的步骤,可能会引起其他系统管理员的讨论,但逐个程序地重新映射库则不那么雄心勃勃,更有可能解决更多问题,而不是制造问题。只需在重新映射语句之前指定所需的程序名并放入方括号。如果你通过完整路径指定程序,映射仅在通过完整路径调用程序时有效。如果你只提供名称,当你运行任何该名称的程序时,映射都会生效。例如,在这里我们重新映射 emacs(1),使其在通过完整路径调用时使用 Nvidia 的库,而不是系统库:

[/usr/local/bin/emacs]
libGL.so         libGL-NVIDIA.so
libGL.so.1       libGL-NVIDIA.so.1

如何证明这成功了呢?好吧,检查一下 ldd(1):

# ldd /usr/local/bin/emacs | grep libGL
        libGL.so.1 => /usr/local/lib/libGL-NVIDIA.so.1 (0x80ad60000)

你可以看到,当/usr/local/bin/emacs请求 libGL.so.1 时,rtld(1)会将其附加到 libGL-NVIDIA.so.1 上。我们指定了 Emacs 二进制文件的完整路径,因此我们需要通过完整路径调用该程序。尝试在没有完整路径调用的情况下对 Emacs 使用 ldd(1):

# cd /usr/local/bin
# ldd emacs | grep libGL
        libGL.so.1 => /usr/local/lib/libGL.so.1 (0x0x8056fa000)

通过进入/usr/local/bin并直接对 Emacs 运行 ldd(1),而无需指定完整路径,rtld 无法看到 emacs(1)二进制文件的完整路径。/etc/libmap.conf表示只在/usr/local/bin/emacs的完整路径下使用 Nvidia 的库。当裸露的emacs请求 libGL.so.1 时,它得到了它请求的内容。

如果你希望程序无论是通过完整路径还是基本名称调用,都使用替代库,只需在方括号中给出程序名称,而不是完整名称:

[emacs]
libGL.so         libGL-NVIDIA.so
libGL.so.1       libGL-NVIDIA.so.1

同样,你也可以通过列出目录名并在后面加上斜杠,选择一个替代库来应用于目录中的所有程序。在这个/usr/local/etc/libmap.d/oracle文件中,我们强制目录中的所有程序使用替代库:

[/opt/oracle/bin/]
libc.so.7       libc-special.so.2

使用libmap.conf允许你任意重新映射共享库。开发人员使用此功能来测试代码。端口使用它来为某些程序覆盖库。你也会找到它的用处。

在错误的操作系统上运行软件

传统的软件是为特定操作系统编写的,只能在该操作系统上运行。许多人通过改变软件使其能够在另一个系统上运行,从而建立了成功的商业,这一过程称为移植。作为管理员,你有几种不同的方式使用为其他平台(非 FreeBSD)编写的软件。最有效的方法是重新编译源代码,使其能够在 FreeBSD 上本地运行。如果这不可行,你可以通过模拟器(如 Wine)运行非本地软件,或者通过重新实现软件本地平台的应用程序二进制接口(ABI)来运行。

重新编译

许多 FreeBSD 包实际上是其他平台上原本设计的软件的移植版。(这就是为什么它被称为Ports集合的原因。)为 Linux、Solaris 或其他类 Unix 操作系统编写的软件,通常可以通过少量或不做修改直接从源代码重新编译并在 FreeBSD 上无缝运行。只需将源代码取出并在 FreeBSD 机器上构建,就可以原生运行外部软件。

重新编译在平台相似时效果最佳。类 Unix 平台应该是相当相似的,不是吗?例如,FreeBSD 和 Linux 提供了许多相似的系统功能;它们都建立在标准 C 函数之上,都使用类似的工具,都使用 GCC 编译器,等等。然而,多年来,各种类 Unix 操作系统已经发生了分歧。每个版本的 Unix 都实现了新特性、新库和新函数,如果某个软件依赖这些特性,它就无法在其他平台上构建。POSIX 标准部分是为了解决这个问题而提出的。POSIX 定义了最基本的类 Unix 操作系统标准。仅使用符合 POSIX 的系统调用和库编写的软件,应该可以立即移植到任何其他符合 POSIX 的操作系统上,而且大多数类 Unix 操作系统厂商都遵循 POSIX。问题在于如何确保开发者遵守 POSIX。许多开源开发者只关心他们的软件是否能在自己偏好的平台上运行。很多特定于 Linux 的软件不仅不符合 POSIX 标准,还包含一堆独特的功能,通常被称为Linuxisms。而仅遵循 POSIX 的代码则无法利用操作系统提供的任何特殊功能。

公平地说,FreeBSD 也有一些 FreeBSD 特有的特性,比如超高效的数据读取系统调用 kqueue(2)。其他类 Unix 操作系统则使用 select(2) 和 poll(2),或者实现自己的系统调用。应用程序开发者会问自己,是否应该使用 kqueue(2),这样他们的软件在 FreeBSD 上会非常快速,但在其他地方则毫无用处;还是应该使用 select(2) 和 poll(2),让他们的软件能够在所有地方运行,尽管速度较慢。开发者可以投入更多时间,支持 kqueue(2)、select(2)、poll(2) 和其他操作系统特定的变种,但尽管这样会让用户高兴,从开发者的角度来看却相当糟糕。

FreeBSD 采取了一种折中的方式。如果某个软件可以重新编译以在 FreeBSD 上正常运行,端口团队通常会让它实现。如果该软件需要小的补丁,端口团队会将这些补丁与端口一起包含,并将它们发送给软件开发者。大多数软件开发者都会愉快地接受补丁,从而使他们的软件支持另一个操作系统。即使他们可能没有该操作系统可用于测试,或者他们可能对该操作系统不熟悉,只要补丁来自可靠的来源,并且看起来不错,通常都会被接受。

仿真

如果软件需要大量重新设计才能在 FreeBSD 上运行,或者如果源代码根本不可用,我们可以尝试仿真。一个 仿真器 会将一个操作系统的系统调用和库调用转换为本地操作系统提供的等效调用,从而使在仿真器下运行的程序认为它们正在本地系统上运行。然而,转换所有这些调用会增加额外的系统开销,影响程序的速度和性能。

FreeBSD 支持多种仿真器,其中大多数在 Ports 集合中位于 /usr/ports/emulators 下。在大多数情况下,仿真器用于教育或娱乐。如果你有一款老旧的 Commodore 64 游戏,想再玩一次,可以安装 /usr/ports/emulators/frodo。(警告:在现代 FreeBSD 系统上挂载 C64 磁盘会让你比人类本应知道的更多了解磁盘。)在 /usr/ports/emulators/dolphin-emu 下有一个 Nintendo GameCube 仿真器,在 /usr/ports/emulators/simh 下有一个 PDP-11 仿真器,等等。

虽然仿真器非常酷,但对于服务器来说并没有太大用处,因此我们不会深入讲解它们。

ABI 重新实现

除了重新编译和仿真,运行外部程序的最后一种选择是 FreeBSD 最为人熟知的:应用程序二进制接口(ABI)重新实现。ABI 是内核提供给程序的服务部分,包括从管理声卡到读取文件、打印到屏幕、启动其他程序的所有操作。就程序而言,ABI 就是操作系统。通过在本地操作系统上完全实现另一个操作系统的 ABI 并提供该操作系统所使用的用户空间库,你可以像在本地平台上一样运行非本地程序。

虽然 ABI 重新实现通常被称为仿真,但它并不是仿真。当实现 ABI 时,FreeBSD 并不是在仿真系统调用,而是为应用程序提供本地实现。没有程序会将系统调用转换为 FreeBSD 等价的调用,也没有努力将用户空间库转换为 FreeBSD 库。同样,称“FreeBSD 实现了 Linux”也是不正确的。创建这个技术时,没有一个词可以完全描述它,直到今天也没有一个特别合适的描述方式。你可以说 FreeBSD 实现了 Linux 系统调用接口,并且支持将二进制文件指向适当的系统调用接口,但这实在是有点冗长。你通常会听到它被称为 模式,比如说“Linux 模式”。

ABI 重实现的问题是重叠。许多操作系统都包含具有通用名称的系统调用,例如 read、write 等。FreeBSD 的 read(2) 系统调用与 Microsoft 的 read() 系统调用行为非常不同。当程序使用 read() 调用时,FreeBSD 如何知道它需要哪个版本呢?你可以给你的系统调用不同的名称,但那样会违反 POSIX 并且混淆程序。FreeBSD 通过提供多个 ABI 并通过 品牌化 来控制程序使用哪个 ABI,从而解决了这个问题。

二进制品牌化

操作系统通常具有一个执行程序的系统功能。当内核将程序发送到这个执行引擎时,它会运行该程序。

几十年前,BSD(当时的 Unix)程序执行系统调用被更改为包括对以 #!/bin/sh 开头的程序进行特殊检查,并用系统 shell 运行它们,而不是执行引擎。BSD 将这个想法推向了极限:其执行引擎包括一个不同二进制类型的列表。每个程序的二进制类型指向正确的 ABI。因此,FreeBSD 系统可以实现多个 ABI,保持它们的独立性,并支持来自不同操作系统的程序。

这个系统的妙处在于它的开销极小。因为 FreeBSD 无论如何都必须决定如何运行程序,那为什么不让它决定使用哪个 ABI 呢?毕竟,不同操作系统的二进制文件都有略微不同的特征,FreeBSD 可以利用这些特征来识别它们。FreeBSD 只是让这一过程对最终用户透明。二进制文件的识别称为 品牌化。FreeBSD 的二进制文件标记为 FreeBSD,而其他操作系统的二进制文件则适当品牌化。

支持的 ABI

由于这种 ABI 重定向,FreeBSD 可以像本地编译一样运行 Linux 二进制文件。旧版本的 FreeBSD 也可以运行 OSF/1、SCO 和 SVR4 二进制文件,但这些平台的需求已经显著减少。^(3) 如果你需要其中的某个,你可以尝试在虚拟机上运行旧版本的 FreeBSD。

Linux 模式,也叫 Linuxulator,非常全面,因为 Linux 的源代码是开放的,且其 ABI 有良好的文档。事实上,Linux 模式工作得如此顺利,以至于 Ports Collection 中的许多程序都依赖于它。

安装和配置 Linuxulator

虽然 ABI 重实现解决了一个主要问题,但程序不仅仅需要 ABI。没有共享库、支持程序和其余的用户空间,大多数程序无法正常运行。无论你使用哪个 ABI,你必须能够访问该平台的用户空间。

如果你想使用 Ports Collection 中可用的 Linux 软件,安装该端口。这将自动安装任何用户空间的依赖项。

如果你想运行某个任意的 Linux 软件,你必须先安装一个 Linux 用户空间。FreeBSD 通常会提供几种不同的 Linux 用户空间作为包。要查看有哪些可用的包,可以在包数据库中搜索 linux_base。

# pkg search linux_base
linux_base-c6-6.9_2            Base set of packages needed in Linux mode (Linux CentOS 6.9)
linux_base-c7-7.3.1611_6       Base set of packages needed in Linux mode (Linux CentOS 7.3.1611)

这个版本的 FreeBSD 有两个 Linux 用户空间:一个基于 CentOS 6.9,另一个基于 CentOS 7.3。Linux 发行版可能会随着 Linux 方向的变化而发生变化。

检查你的软件运行在哪些版本的 Linux 上。为你的应用安装最合适的用户空间。FreeBSD 将 Linux 用户空间安装在 /usr/compat/linux 下。

该端口还会加载 Linux 模式内核模块。要在启动时自动加载该模块,可以使用以下 rc.conf 条目:

linux_enable="YES"

就是这样!Linux 模式不是一个正式的服务,因为你无法重启它或获取状态,因此不能用 service(8) 来配置它。运行 /etc/rc.d/abi start 可以在不重启的情况下激活 Linux 模式。

在我们深入了解如何运行 Linux 程序之前,先来探索一下用户空间。

Linux 用户空间

就像 linux.ko 内核模块提供 Linux ABI 一样,Linuxulator 需要一个非常精简的 Linux 用户空间。看看 /usr/compat/linux 目录,你会看到类似下面的内容:

# ls
bin     etc     lib64   proc    selinux sys     var
dev     lib     opt     sbin    srv     usr

看起来很像 FreeBSD 的 / 目录的内容,不是吗?如果你多探查一下,你会发现,通常情况下,/usr/compat/linux 中的内容与核心 FreeBSD 安装中的内容相当。你会在两者中找到许多相同的程序。

Linux 爱好者立刻会注意到任何 linux_base 端口的一个特点,那就是它的内容比典型的 Linux 安装要精简。这是因为每个基于 Linux 的包只安装它运行所需的内容。FreeBSD 的 Linux 包将极简的 BSD 哲学强加到 Linux 软件中。

每当可能时,Linux 模式下的程序会尽量保持在 /usr/compat/linux 目录下,这有点像一个弱监狱(参见 第二十二章)。当你执行一个调用其他程序的 Linux 二进制程序时,Linux ABI 会首先在 /usr/compat/linux 下查找该程序。如果程序在那里不存在,Linux 模式会在主系统中查找。例如,假设你有一个调用 ping(8) 的 Linux 二进制程序。ABI 首先会在 /usr/compat/linux/ 下查找 ping 程序;截至本文撰写时,它不会找到任何内容。然后,ABI 会检查主 FreeBSD 系统,找到 /sbin/ping,并使用它。Linuxulator 在很大程度上利用了这种回退机制来减少 Linux 模式用户空间的大小。

另外,假设一个 Linux 二进制程序想调用 sh(1)。Linux ABI 会在 /usr/compat/linux 下查找,找到 /usr/compat/linux/bin/sh,并执行该程序,而不是 FreeBSD 原生的 /bin/sh

linprocfs 和 tmpfs

Linux 使用一个进程文件系统,或procfs。FreeBSD 几十年前就将 procfs 作为默认设置移除,因为它存在安全风险,但一些 Linux 程序仍然需要它。使用需要 procfs 的 Linux 软件意味着接受固有的风险。FreeBSD 提供了一个作为 linprocfs(5)的 Linux procfs。

要启用 linprocfs(5),在安装 Linuxulator 后,将以下内容添加到/etc/fstab中:

linproc    /compat/linux/proc    linprocfs  rw     0       0

FreeBSD 根据需要加载文件系统内核模块,因此请输入mount /compat``/li``nux/proc来激活 linprocfs(5)而无需重启。

许多 Linux 程序还期望/dev/shm用于共享内存。FreeBSD 可以通过 tmpfs(5)模拟这一点。

tmpfs    /compat/linux/dev/shm  tmpfs   rw,mode=1777    0       0

输入mount /compat/linux/dev/shm,共享内存设备就绪。

测试 Linux 模式

现在您对 Linux 模式下安装的内容有了大致了解,测试 Linux 功能变得容易了。运行 Linux shell 并询问它正在运行哪个操作系统:

# /usr/compat/linux/bin/sh 
sh-4.1# uname -a
Linux storm 2.6.32 FreeBSD 12.0-CURRENT #0 r322672: Fri Aug 17 16:31:34 EDT
2018 x86_64 x86_64 x86_64 GNU/Linux
sh-4.1#

当我们询问这个命令提示符运行的是什么类型的系统时,这个 shell 回答它是一个在 Linux 2.6.32 内核之上运行的 Linux 系统,叫做FreeBSD。很酷吧?

但是请记住,Linux 模式并不是一个完整的 Linux 用户空间。在默认的 Linuxulator 安装中,您无法交叉编译软件。您只能执行非常基础的任务。

识别和设置品牌

为软件二进制文件打上品牌要比给牲畜打上品牌容易,但没有那么冒险。大多数现代类 Unix 二进制文件都采用 ELF 格式,其中包括一个注释空间。品牌就存在于这里。FreeBSD 通过该二进制文件上的品牌为每个程序分配一个 ABI。如果二进制文件没有品牌,则假定它是 FreeBSD 二进制文件。

使用 brandelf(1)查看和更改品牌:

# brandelf /bin/sh 
File '/bin/sh' is of brand 'FreeBSD' (9).

这并不令人惊讶。这是一个 FreeBSD 二进制文件,因此它将在 FreeBSD ABI 下执行。让我们尝试一个 Linux 二进制文件:

# brandelf /usr/compat/linux/bin/sh
File '/usr/compat/linux/bin/sh' is of brand 'Linux' (3).

使用-l标志查看 FreeBSD 支持的品牌。

# brandelf -l
known ELF types are: FreeBSD(9) Linux(3) Solaris(6) SVR4(0)

如果您有一个无法运行的外部程序,请检查其品牌。如果没有品牌或品牌错误,您可能已经发现了问题所在:FreeBSD 试图在原生 FreeBSD ABI 下运行该程序。通过brandelf -t手动设置品牌来解决此问题。例如,要将程序标记为 Linux,可以执行以下操作:

# brandelf -t Linux /usr/local/bin/program

下次尝试运行该程序时,FreeBSD 将在 Linux ABI 和 Linux 用户空间下运行它,程序应该按预期工作。

您还可以使用 sysctl 设置回退品牌。所有 FreeBSD 二进制文件都会被正确地标记,但您复制到主机的随机程序可能不会。未标记品牌的二进制文件将被视为所选回退品牌。sysctl kern.elf32.fallback_brand为 32 位主机设置回退品牌,而kern.elf64.fallback_brand则为 64 位主机设置回退品牌。此 sysctl 采用品牌的数字标识符,对于 Linux 来说是 3。

# sysctl kern.elf64.fallback_brand=3

现在,您应该能够运行 Linux 程序,而无需进一步配置。剩下的只是 Linux 模式中的一些小烦恼和瑕疵。可惜的是,正如我们接下来所示的那样,这些问题确实存在。

使用 Linux 模式

许多 Linux 程序仅作为端口提供。Ports Collection 足够智能,可以识别出某个软件需要 Linux 模式,并选择合适的 Linux 组件进行安装。一个受欢迎的选择是 Skype。安装这个端口时,会触发正确的 Linux 用户空间安装。

拥有一个最小化的 Linux 用户空间的缺点是,任何端口都会有一堆依赖项。其中一些是 FreeBSD 二进制文件,其他是 Linux 文件。我建议使用端口的 make missing 命令来显示缺少的依赖项,甚至像 第十六章 讨论的那样,自动从软件包中安装依赖项。一旦安装了所有所需的软件包,安装了 linprocfs 和 Linux 共享内存设备,并加载了所有内核模块,安装 Skype 就像执行 make install clean 一样简单。

调试 Linux 模式

Linux 模式并不是 Linux,尤其在程序崩溃时,这一点尤为明显。许多程序都有晦涩的错误信息,而 Linux 模式可能会进一步掩盖这些信息。你需要一些工具,能够深入挖掘错误信息,看到真正的问题所在。

Linux 模式与 truss(1)

我发现用于调试 Linux 模式的最佳工具是 truss(1),即 FreeBSD 系统调用追踪器。有些人告诉我,用 truss(1) 来做这件事就像把一台 Mack 卡车的 12 缸发动机装进一辆大众 Beetle 汽车里,但经过深思熟虑,我决定我不在乎。它有效。一旦你了解了 truss(1),你会想知道你是怎么在没有它的情况下生活的。^(4)

truss(1) 程序可以准确识别程序进行的系统调用以及每个调用的结果。记住,系统调用是程序与内核的接口。当程序试图访问网络、打开文件,甚至分配内存时,它会发出系统调用。这使得 truss(1) 成为查看程序为何失败的一个极好的工具。程序会发出大量的系统调用,这意味着 truss(1) 会生成大量数据,因此用 truss(1) 调试非常适合使用 script(1) 来记录。

所以让我们启动 Skype。

$ skype
Segmentation fault (core dumped)

好消息是:程序可以运行!坏消息是,它在某个地方卡住了。我最常遇到的错误是缺少库、文件和目录,但具体是哪一个呢?truss(1) 的输出可以告诉我。启动一个 script(1) 会话,在 truss(1) 下运行程序,并结束 script。

你的脚本文件可能会有几百行或几千行;你怎么可能找到问题呢?搜索相关的错误信息或字符串 ERR。在这个例子中,我搜索了字符串 directory,并在输出的末尾找到了如下内容:

$ truss skype
--snip--
linux_open("/usr/local/Trolltech/Qt-4.4.3-static/lib/tls/i686/sse2/libasound.so.2",0x0,00)
ERR#-2 'No such file or directory'
linux_stat64("/usr/local/Trolltech/Qt-4.4.3-static/lib/tls/i686/sse2",0xffffb248) ERR#-2 'No
such file or directory'
linux_open("/usr/local/Trolltech/Qt-4.4.3-static/lib/tls/i686/libasound.so.2",0x0,00) ERR#-2 'No such file or directory'
--snip--

啊哈!Skype 找不到所需的库。软件包维护者可能遗漏了这些库,或者可能是我某个地方搞错了。检查一下你的主机上是否有这些库。如果没有,你需要安装它们。也许有可用的端口,或者我可能需要安装 Linux 软件包。

安装 Linux 软件包

如果你需要的 Linux 库或软件没有端口,你有几种选择。一种是为该软件创建一个端口。端口是参与 FreeBSD 社区的好方法。然而,如果你的目标是让软件运行起来并继续完成你的工作,那么你需要从源 RPM 安装适当的 Linux 软件。不过要注意,一旦你安装了 Ports 集合以外的软件,你就需要手动维护它。

查找你需要的软件的 RPM。确保包的版本与安装在 linux_base 中的版本匹配。如果你的 FreeBSD 主机使用的是 CentOS 7.3.1611,而你找到了适用于 CentOS 8 的缺失库包,那就没用。下载 RPM 文件。

商业 Linux 软件和 Linux 模式

请记住,商业软件供应商不会在 FreeBSD 的 Linux 模式下支持其 Linux 软件。如果你身处有服务级别协议的工业环境,并且存在可能需要支付罚款的风险,在使用 Linux 模式之前请三思。商业软件的主要优势是当软件出现故障时,可以找人负责,但 FreeBSD 的 Linux 模式消除了这一优势。

假设我的生活发生了可怕的转折^(5),我需要在 Linux 模式下运行 Supermin。我找到并下载了包文件,然后使用 tar(1) 安装它。FreeBSD 基于 libarchive 的 tar 可以像处理其他任何文件一样打开 RPM 文件。

# cd /compat/linux
# tar -xf /home/mwl/supermin-5.1.16-4.el7.x86_64.rpm

现在,我得去找下一个缺失的依赖项。一旦我拥有了完整的依赖项列表,我将写一个端口,以节省其他人这份繁琐的工作。

运行来自错误架构或版本的软件

当你运行 FreeBSD 的 amd64 平台时,最终会遇到一些只提供给 i386 平台的软件。如果你的内核启用了 COMPAT_FREEBSD32 选项(GENERIC 中已包含),FreeBSD/amd64 可以运行所有 FreeBSD/i386 软件。不过,你不能使用 FreeBSD/amd64 的共享库来运行 FreeBSD/i386 软件。如果你想在 64 位计算机上运行一个复杂的 32 位程序,你必须提供必要库的 32 位版本。这是完全支持的;如果你查看 rc.conf,会看到 ldconfig(8) 的选项 ldconfig32_pathsldconfig_local32_dirs。这些选项专门用于告诉你的 amd64 系统在哪里可以找到 32 位库。FreeBSD 在安装介质中包含了 32 位库。

此外,FreeBSD 可以运行来自旧版本 FreeBSD 的软件。GENERIC 内核包含所有的系统调用,但你仍然需要基础系统库。这些库以包的形式提供,每个主要的 FreeBSD 版本都有一个相应的包。每个包的名称以compat开头,后跟版本号,并以x结尾。如果你必须运行 FreeBSD 8 的二进制文件,安装 compat8x 包。compat 包包含 64 位和 32 位库。

如果你需要运行非 i386 或 amd64 的二进制文件,你甚至可以使用 binmiscctl(8)在每次运行非 x86 二进制文件时自动启动合适的模拟器。

尽管关于软件管理的知识总是有更多可以学习的内容,但你现在已经足够掌握基本知识来应付了。接下来,让我们继续学习如何升级 FreeBSD。

第十八章:升级 FreeBSD**

image

升级服务器可能是系统管理员日常工作中最让人头痛的任务。我可以应对桌面在升级后出现的不可解释的行为,但当我的整个组织或数百个客户依赖一台服务器时,甚至想动一下系统都会让我感到不安。任何操作系统的升级都可能让你增加越来越多的白发。即使是非常有经验的系统管理员,在面对升级一个关键系统的选择时,也经常需要坐下来仔细考虑自己的选择,这比用红热的针刺自己眼睛还要让人犹豫不决。虚拟化和编排的云系统可能看起来不那么麻烦,但即使如此,准备升级也会让人彻夜难眠。记住,尽管自动化带来了许多好处,但它在大规模实施时也可能是一条通往错误的捷径。

FreeBSD 的最大优势之一就是它的升级程序。FreeBSD 设计为一个整体操作系统,而不是一组软件包。(即使 FreeBSD 转向将基础系统提供为软件包,它仍然会保持整体设计和构建的方式。)我曾经让多个主机通过五个不同的主要版本和无数的修补级别,而无需重新安装系统。只有当 FreeBSD 系统老旧到硬件故障的风险让我晚上无法安睡时,我才会退役它们。^(1) 虽然我可能会担心操作系统上运行的应用程序,但即使是跨越主要版本的升级,现在也不再让我担心了。

FreeBSD 版本

为什么升级 FreeBSD 是一件相对简单的事情?关键在于 FreeBSD 的开发方法。FreeBSD 是一个不断发展的操作系统。如果你在下午下载当前版本的 FreeBSD,它会和早上版本略有不同。来自世界各地的开发者不断添加更改和改进,这使得传统的严格发布版本号(常见于封闭软件)不再适用。在任何给定的时刻,你可以获得几种不同版本的 FreeBSD:发布版本、-stable、-current 和快照版本。

发布版本

FreeBSD 发布主要版本和次要版本。主要版本的版本号像 11.0、12.0、13.0 等等。每个主要版本都包含在早期版本中没有的大特性。大规模的更改仅出现在主要版本中。

次要版本是主要版本的更新版本。你会看到像 12.1-RELEASE、12.2-RELEASE、12.3-RELEASE 等次要版本。(大多数人会省略“release”这个词。)这些次要版本为主要版本添加了一些小功能和修复错误。你可能会得到新的功能或程序,但只有在这些新功能不会干扰现有功能的情况下。避免出现意外的、破坏性的更改。

你还会看到 补丁级别。多亏了 freebsd-update(8),更新发布变得又快又容易。补丁号通常作为版本后的数字显示。这意味着你会看到类似 12.1-RELEASE-p20、11.4-RELEASE-p9、13.0-RELEASE-p31 等 FreeBSD 版本。

用户应通过逐步升级各个次版本来密切跟踪其主要版本,就像其他操作系统一样。

FreeBSD-current

FreeBSD-current,也叫做 -currentHEAD,是 FreeBSD 的最前沿版本,包含了首次公开发布的代码。尽管开发者有测试服务器,并在应用之前发布补丁供审核,但这仍然比 FreeBSD-current 的广泛用户群体的曝光要少得多。FreeBSD-current 是初步同行评审进行的地方;有时,current 会经历激烈的变化,给有经验的系统管理员带来头痛。

FreeBSD-current 为开发者、测试人员和感兴趣的各方提供,但它并不适用于一般用途。由于开发者根本没有时间在成千上万的紧急问题面前帮助用户修复他的网页浏览器,因此对 -current 用户问题的支持非常有限。用户需要帮助修复这些问题,或者耐心忍受,直到其他人修复它们。

更糟糕的是,-current 的默认设置包括各种调试代码、特殊警告和相关的开发者功能。这些都使得 -current 比任何其他版本的 FreeBSD 运行得更慢。你可以禁用所有这些调试功能,但如果这么做,你在遇到问题时将无法提交适当的故障报告。这意味着你将更加孤立无援。你可以查看 -current 系统中的文件 /usr/src/UPDATING 以获取调试细节。

如果你不懂 C 语言和 shell 代码,不想调试操作系统,不喜欢计算机功能随机失败,或者只是不喜欢等到你的问题烦到能修复它的人的时候再被解决,那么 -current 可能不适合你。勇敢的人当然欢迎尝试 -current,就像任何愿意投入大量时间学习和调试 FreeBSD 的人,或者需要一课谦逊的人一样。你并不被禁止使用 -current;只是你要自己应对。FreeBSD-current 并不总是最前沿的,但有时它可能是那种“为什么我的手指突然变成了小小的晃动残肢?”的前沿。你已经被警告过了。

要运行 -current,你真的 必须 阅读 FreeBSD-current@FreeBSD.orgsvn-src-head@FreeBSD.org 邮件列表。这些都是高流量的列表,每天有数百条警告、提醒和评论。如果你正在读这本书,你可能不应该在这些列表上发帖;只需阅读并学习。如果有人发现最新的文件系统补丁将硬盘变成了克苏鲁的僵尸爪牙,信息将会在这里发布。

-current 代码冻结

每 12 到 18 个月,FreeBSD-current 会经历一个月的代码冻结,在此期间不允许进行非关键性变更,所有已知的关键问题都在修复中。目标是稳定 FreeBSD 的最新版本,并打磨其粗糙的边角。在代码冻结结束(或稍后),-current 版本将成为新的 FreeBSD 主要版本的.0版本。例如,FreeBSD 12.0 曾经是-current 版本,FreeBSD 13.0 也是如此。当新的主要版本发布时,-current 版本号将递增。FreeBSD 17.0 的发布意味着-current 将被称为FreeBSD 18

一旦.0 主要版本发布到公共环境中,开发工作就会分为两条路线:FreeBSD-current 和 FreeBSD-stable。

FreeBSD-stable

FreeBSD-stable(或简称* -stable*)是“普通用户的前沿技术”,包含了一些最新的同行评审代码。熟悉 Linux 的系统管理员知道-stable 是一个“滚动发布”。你会发现每个主要版本都有一个 FreeBSD-stable 版本。

一旦一段代码在-current 中经过彻底测试,它可能会被合并回-stable。-stable 版本是几乎任何时候都可以安全升级的版本;你可以把它当作 FreeBSD 的 beta 版本。

每年三到四次,发布工程团队会要求开发人员集中精力解决与-stable 版本相关的未解决问题,而不是进行重大变更。发布工程团队从这段代码中剪切出几个候选版本,并提供给公众进行测试。当 FreeBSD 团队对自己以及社区的测试结果满意时,最终的版本会被赋予一个发布编号。开发团队随后将注意力转回到他们的常规项目上。^(2)

这个过程如何在实践中运作呢?考虑 FreeBSD 13。当 13.0 发布时,开发人员将开始将 bug 修复和功能添加到 13-stable 版本中。希望帮助测试新 FreeBSD 版本的用户可以运行 13-stable。在合并功能并进行一些测试几个月后,13-stable 将变为 13.1。当 13.1 发布后,该开发路径会恢复为 13-stable。FreeBSD 13.1、13.2 和 13.3 只是 FreeBSD 13-stable 的连续版本。

FreeBSD-stable 预计是平稳且可靠的,几乎不需要用户的注意。

随着-stable 版本的老化,-stable 和-current 之间的差异会越来越大,直到必须从-current 分支出一个新的-stable 版本。旧的-stable 会在几个月内继续维护,而新的-stable 会进行完善。一些用户会立即升级到新的-stable 版本,而其他用户则会更为谨慎。在新-stable 发布一到两个版本后,旧的-stable 会被淘汰,开发人员会鼓励用户迁移到新版本。经过一段时间后,旧的 stable 将只接收关键的 bug 修复,最终会完全被废弃。你可以在图 18-1 中看到这一过程是如何运作的。

image

图 18-1:FreeBSD 开发分支

每个版本实际上应该有一个稍微拖曳到一侧的补丁级别尾部,但那样会使图表变得非常混乱。

FreeBSD-stable 的用户必须阅读* FreeBSD-stable@FreeBSD.org 邮件列表。虽然该邮件列表的流量适中,并且有一些本应在-questions@*上进行的问答交换,但来自开发人员的重要信息通常会以“HEADS UP”开头。请留意这些邮件,它们通常意味着,如果你不知道某些更改,系统可能会让你的一天变得糟糕透顶。

-STABLE 的稳定性

稳定这个词描述的是代码库,而不是 FreeBSD 本身。从稳定分支上的某个随机点运行代码并不能保证你的系统会稳定,只能保证底层代码不会发生剧烈变化。API 和 ABI 预计保持不变。尽管开发人员会尽力确保-stable 保持稳定,但错误是有可能发生的。如果这种风险让你担忧,最好还是使用打过补丁的发布版本。

从-当前合并

从-当前(MFC)合并这个短语意味着某个功能或子系统已经从 FreeBSD-current 回溯到 FreeBSD-stable(或者,在少数情况下,回溯到 errata 分支)。然而,并不是所有功能都会被 MFC。FreeBSD-current 的重点在于,它是进行重大变更的地方,而这些变更通常需要几个月的测试和调试。这些大的变更不能被回溯,因为它们会严重影响期望稳定代码库的-stable 用户。

有时,看似“显而易见的 MFC 候选项”的功能无法被合并。有时内核基础设施需要变更以支持新的驱动程序和功能,而这些基础设施无法安全地合并。需要这些基础设施的新驱动程序无法进行 MFC。这种情况通常发生在视频和无线网络驱动程序中。

选择性的新驱动程序、bug 修复和小的增强功能可以进行 MFC——但仅限于此。FreeBSD 项目特别强调不进行 MFC 大规模更改,以免破坏用户应用程序。

快照

每个月左右,FreeBSD 发布工程团队会发布-当前和-稳定版本的快照,并将其放到 FTP 站点上供大家下载。快照只是开发分支中的某些时间点,它们不会经过特殊的打包或测试。快照没有像正式发布版本那样的质量控制,它们的目的仅是作为那些有兴趣运行-当前或-稳定版本的用户的一个良好起点。质量控制相对有限,很多开发人员直到快照出现在 FTP 服务器上时才知道它的发布。你会发现 bugs,你会发现错误,你会遇到一些问题,可能会让你母亲的头发都白了,假设你还没有把她弄成那样。

FreeBSD 支持模型

随着 FreeBSD 11.0 的发布,项目的支持模式发生了变化,更加接近其他商业和非商业操作系统的使用模式。

每个主要版本将在首次发布后提供五年的安全性和稳定性补丁。如果 FreeBSD 13 于 2021 年 1 月 1 日发布,支持将于 2026 年 1 月 1 日结束。

每个小版本在下一个小版本发布后会得到三个月的支持。FreeBSD 12.3 的支持将在 FreeBSD 12.4 发布后三个月结束。这为你提供了三个月的时间来安排升级。

官方支持的失效并不意味着你不能继续运行不受支持的版本。然而,你需要检查每个安全公告,确定它是否影响你的环境,并创建自己的补丁。你最好还是进行升级。

FreeBSD 小版本发布的核心意义在于它们对系统的干扰最小。从 FreeBSD 12.3 升级到 12.4 应该类似于应用 Windows 更新或从 Centos 8.1 升级到 8.2。应用程序应继续正常运行。

一个版本的最后一个 FreeBSD 小版本将得到支持,并在五年内修复漏洞。如果 FreeBSD 12.5 是 FreeBSD 12 的最后一个版本,它将在发布 FreeBSD 12.0 后五年内提供安全补丁。

测试 FreeBSD

每个版本和发布的 FreeBSD 都经过各种方式的测试。个别开发者在自己的硬件上检查他们的工作,并相互请求对方进行复核。如果工作较为复杂,他们可能会使用官方的 FreeBSD Phabricator 系统(*reviews.FreeBSD.org/)或甚至私有的源代码仓库,在提交到 -current 之前将他们的工作提供给更广泛的社区进行检查。分析公司向 FreeBSD 团队捐赠了分析软件,使得源代码可以在持续的基础上自动进行审核、测试和调试,捕获许多错误,防止它们影响到实际用户。像 Sentex、EMC、Netflix 和 iX Systems 等公司为项目提供了测试支持。几位备受尊敬的 FreeBSD 开发者将测试工作作为项目的重点问题,他们目前已经拥有了自动化的 Kyua 测试框架。

然而,最终,一个只有几百个开发者的志愿者项目无法购买所有曾经生产过的计算机硬件,也无法在所有可能的负载下运行这些硬件。FreeBSD 项目作为一个整体,依赖于使用 FreeBSD 的公司和个人来进行软件测试。

最有价值的测试来自于那些拥有实际设备、实际测试平台和实际工作负载的用户。遗憾的是,大多数用户在进行测试时,只是在将发布 CD 插入计算机,运行安装程序并启动系统时进行测试。到那个时候,已无法对发布版本产生实际的帮助。你发现的任何 bug 可能对下一个版本有帮助,但在此期间,可能通过补丁解决你的问题。

解决方案显而易见——在发布之前,在你的实际工作负载上测试 FreeBSD。对新 -stable 版本的测试请求可以发送至 FreeBSD-stable@FreeBSD.org。通过测试 -stable 或 -current,你将从 FreeBSD 中获得更大的价值。

你应该使用哪个版本?

-current、-stable、release、snapshot——令人头晕。是的,这看起来很复杂,但它确保了特定的质量水平。用户可以放心,发布版经过了广泛的测试和同行评审,是尽可能可靠的。同样的用户知道,在 -stable 和 -current 中的炫酷新特性是可以使用的——如果他们愿意承担每个版本所固有的风险。那么,你应该使用哪个版本呢?以下是我的建议:

生产环境

如果你在生产环境中使用 FreeBSD,请安装最新的次版本发布。当下一个次版本发布出来时,进行升级。

预发布

如果你需要了解下一个 FreeBSD 次版本发布的内容以及它将如何影响你的生产环境,可以在你的预发布环境中跟踪 -stable。

测试

这里的问题是你希望测试什么。项目方希望在 -current 和 -stable 上都进行测试。如果你不确定,从运行 -stable 开始。

开发

操作系统开发者、闲得无聊且缺乏刺激的人,以及彻底的傻瓜应该运行 -current。当 -current 摧毁了你的 MP3 收藏时,调试问题并提交修复补丁。

爱好

如果你是爱好者,可以运行任何版本!只是要记住你选择的分支的局限性。如果你刚开始学习 Unix,我建议使用 -release。等你熟悉了,再升级到 -stable。虽然 -current 比 20 年前更加稳定,但如果你觉得系统崩溃带来的肾上腺素激增能让你的一天更有趣,那就选择它吧。那些运行 -current 的高风险玩家欢迎志同道合的伙伴。

升级方法

FreeBSD 提供两种升级方式:二进制更新和从源代码构建。

FreeBSD 支持通过 freebsd-update(8) 进行 二进制更新。这与 Windows、Firefox 和其他商业软件提供的二进制更新服务非常相似。(软件作者表示,freebsd-update(8) 是以 Windows Update 命名的。)你可以使用 FreeBSD Update 来升级主要版本、次版本和补丁级别。

从源代码升级意味着下载 FreeBSD 源代码,构建组成 FreeBSD 的程序,并将它们安装到你的硬盘上。例如,如果你有 FreeBSD 13.1 的源代码,你可以升级到该版本。这需要更多的设置和使用工作,但它为你提供了更大的灵活性。当跟踪 -stable 或 -current 时,使用从源代码升级。

保护你的数据!

第五章被称为“在破坏其他东西之前请阅读!”是有充分理由的。升级可能会破坏你的数据。在尝试任何形式的升级之前,请备份你的系统!我每周都会升级我的桌面,纯粹是为了娱乐(参见我之前关于肾上腺素瘾君子跑 -current 的评论)。但在升级之前,我会确保所有重要数据已安全地缓存到另一台机器上。将数据备份到磁带、文件或其他地方,但不要在没有新备份的情况下进行升级。已经警告过你了。

二进制更新

许多操作系统提供二进制更新,用户可以下载操作系统的新二进制文件。FreeBSD 通过 freebsd-update(8)提供类似的程序,允许你轻松地升级系统。^(3) 你不能使用 freebsd-update(8)跟踪-stable 或-current,只能跟踪发布版本。例如,如果你安装了 FreeBSD 12.0,freebsd-update(8)可以将你升级到 12.0-p9、12.1 或 13.0,但不能升级到 12-stable 或 14-current。

如果你使用的是自定义内核,你必须从源代码构建内核更新,而不是依赖于更新服务。同样,如果你从源代码升级了主机(本章稍后讨论),运行 freebsd-update(8)将会用默认的二进制文件覆盖你自定义的二进制文件。

/etc/freebsd-update.conf中配置更新。

/etc/freebsd-update.conf

使用 freebsd-update(8)进行更新旨在为普通用户提供无缝体验,因此很少建议更改其配置。然而,你可能会遇到特殊情况,下面是你在此文件中会找到的最有用选项:

KeyPrint 800...

KeyPrint列出了更新服务的加密签名。如果 FreeBSD Update 服务遭到安全漏洞,FreeBSD 项目需要修复漏洞并发布新的加密密钥。在这种情况下,漏洞将通过安全公告邮件列表发布(并且在 IT 界也会成为重大新闻)。换句话说,正常使用中没有理由更改此项。(构建自定义的 FreeBSD 并通过 freebsd-update(8)分发,尽管可能且实际可行,但被认为是异常使用。)

ServerName update.freebsd.org

ServerName告知 freebsd-update(8)从哪里获取更新。虽然 FreeBSD 项目确实提供了构建自己更新的工具,但实际上没有必要这样做。如果你有足够多的服务器来考虑建立自己的更新服务器,那么你可能也有一个代理服务器,可以缓存官方更新。

组件 src world kernel

默认情况下,FreeBSD Update 提供 /usr/src 中的源代码、用户空间(world)和 GENERIC 内核的最新补丁。然而,你可能不需要所有这些组件。虽然用户空间是必须的,但你可能有一个自定义内核。删除内核声明可以让 freebsd-update(8) 忽略内核。自定义内核用户还可以将 GENERIC 内核复制到 /boot/GENERIC。更新将更新 GENERIC 内核,但不会更改你的自定义内核。或者,你可以删除 kernel 条目,从而避免警告。如果你没有在机器上安装源代码,freebsd-update 会意识到这一点,并且不会尝试打补丁,但你也可以删除 src 条目,省去软件的麻烦。你还可以选择只接收源代码更新的部分内容,正如 freebsd-update.conf(5) 中所述。

UpdateIfUnmodified /etc/ /var/ /root/ /.cshrc /.profile

更新包括对 /etc 中配置文件的更改。然而,如果你修改了这些文件,你可能不希望 freebsd-update(8) 覆盖它们。同样,/var 是非常动态的,设计上便于系统管理员定制;你不希望 FreeBSD Update 改动你的设置。FreeBSD Update 只会在文件自默认值未更改的情况下,将补丁应用于 UpdateIfUnmodified 中列出的目录。

MergeChanges /etc/ /boot/device.hints

更新到新版本可能会更改配置文件。更新过程将为你提供机会将更改合并到出现在 MergeChanges 位置的文件中。

MailTo root

如果你计划运行 FreeBSD Update(如本章后面所述),freebsd-update(8) 会将结果通过邮件发送到 MailTo 中列出的账户。

KeepModifiedMetadata yes

也许你已经修改了系统文件或命令的权限或所有者。你可能不希望 freebsd-update(8) 将这些权限恢复为默认。设置 KeepModifiedMetadatayes 后,freebsd-update(8) 将保持你的自定义权限和所有权不变。

查看 freebsd-update.conf(5) 以了解更多选项。

运行 freebsd-update(8)

使用二进制更新更新系统分为两个阶段:下载更新和应用更新。如果你应用的是补丁,或者跨越大版本,过程会略有不同。

如果你使用 ZFS,升级或打补丁之前一定要创建一个新的启动环境!

更新到最新的补丁级别

要下载最新的补丁到你选择的版本,运行 freebsd-update fetch。在这里,我正在将一台 FreeBSD 11.0 主机更新到最新的补丁级别。

# freebsd-update fetch

你将看到程序查找下载源的补丁,比较这些下载源的加密密钥,最终将补丁下载到 /var/db/freebsd-update 中。检查系统可能需要几分钟,具体取决于存储设备的速度。

偶尔,你会看到类似如下的消息:

   The following files will be removed as part of updating to 11.0-RELEASE-p12:
➊ /boot/kernel/hv_ata_pci_disengage.ko
➋ /usr/share/zoneinfo/America/Santa_Isabel
➌ /usr/share/zoneinfo/Asia/Rangoon

我们正在更新一个 .0 版本,即一个主要 FreeBSD 版本的第一个版本,直接更新到 11.0-RELEASE-p12,因此有一些累计的补丁。为什么这样的补丁集会先删除文件呢?

时区文件相对简单。在 FreeBSD 11.0 发布到现在之间,Santa Isabel ➋ 和 Rangoon ➌ 的时区发生了变化。可惜的是,各国并没有与 FreeBSD 发布同步调整时区。从系统中删除这些时区能简化这些国家系统管理员的工作,而不会影响我们其他人。

但他们也在移除一个内核模块 ➊。为什么会发生这种情况?稍作研究 FreeBSD 邮件列表可以发现,这个模块本不应该随着 11.0 发布,而且真的不应该使用它。这类变更很少见,但在一个主要发布后的不久有时会发生。

接着你会看到作为该补丁集一部分的新增文件(如果有的话)。

The following files will be added as part of updating to 11.0-RELEASE-p12:
/usr/share/zoneinfo/Asia/Barnaul
/usr/share/zoneinfo/Asia/Famagusta
/usr/share/zoneinfo/Asia/Tomsk
--snip--

看来仰光的系统管理员这个夏天非常忙。

几乎所有的补丁都会修改系统上的现有文件。接下来你会看到这些。

The following files will be updated as part of updating to 11.0-RELEASE-p12:
/bin/freebsd-version
/boot/gptboot
/boot/gptzfsboot
/boot/kernel/cam.ko
/boot/kernel/hv_storvsc.ko
--snip--

如果你的版本接近生命周期结束,你会看到这样的通知:

WARNING: FreeBSD 11.0-RELEASE-p1 is approaching its End-of-Life date.
It is strongly recommended that you upgrade to a newer
release within the next 1 month.

如果该版本已经过了生命周期结束,通知会变得更加... 强调。

要安装下载的文件,运行 freebsd-update install

# freebsd-update install
Installing updates... done.

如果更新还需要其他步骤,你将在这里看到它们。重启系统后,你将看到自己运行的是最新的补丁版本。

更新发布版本

这台 FreeBSD 11.0-p12 机器已接近生命周期结束?让我们使用 freebsd-update upgrade 更新它。用 -r 标志指定目标版本。

# freebsd-update -r 11.1-RELEASE upgrade

最难的部分是记住 -RELEASE 是版本名称的一部分。

升级会检查你的系统并呈现其结论。

The following components of FreeBSD seem to be installed:
kernel/generic world/base world/lib32

The following components of FreeBSD do not seem to be installed:
kernel/generic-dbg world/base-dbg world/doc world/lib32-dbg

Does this look reasonable (y/n)? y

还记得安装过程中,你选择了哪些 FreeBSD 组件来设置新主机吗?这就是 freebsd-update 在检查的内容。不过,你可能添加或移除了某些组件,所以请查看一下列表。如果看起来没问题,按 y 继续。

更新会更仔细地审查系统,将现有文件与新版本进行比较,然后开始下载。

Fetching 10697 patches.....10....20....30....40....50....60....70....80....90

去泡杯茶吧。根据你主机的带宽,应该能在返回后看到:

Applying patches...

你的茶可能太烫了,让它冷却一下吧。

Fetching 236 files...

还要下载更多内容?没问题,享受你的茶,看看程序怎么运行吧。

Attempting to automatically merge changes in files... done.

The following files will be removed as part of updating to 11.1-RELEASE-p1:
/usr/include/c++/v1/__undef___deallocate
/usr/include/c++/v1/tr1/__undef___deallocate
/usr/include/netinet/ip_ipsec.h
--snip--

你可以查阅邮件列表存档和 FreeBSD 源代码树,了解每个文件被删除的原因。

小版本将包含从 -current 回移植的新特性。那些可能涉及将文件添加到系统中。

The following files will be added as part of updating to 11.1-RELEASE-p1:
/boot/kernel/amd_ecc_inject.ko
/boot/kernel/bytgpio.ko
/boot/kernel/cfiscsi.ko
/boot/kernel/cfumass.ko
--snip--

这些新功能不应干扰现有功能,但浏览一下列表可能会让你发现一些有趣的内容。

升级应该会改变系统中的几乎所有文件,接下来我们会看到这一点。

The following files will be updated as part of updating to 11.1-RELEASE-p1:
/.cshrc
/.profile
/COPYRIGHT
/bin/
/bin/cat
--snip--

最终你会看到:

To install the downloaded upgrades, run "/usr/sbin/freebsd-update install".

你是谁,居然敢忽视指令?

到目前为止,更新过程只下载了文件并在临时暂存区进行了比较,操作系统并没有受到影响。如果你觉得可以继续操作,运行安装程序。

# freebsd-update install
src component not installed, skipped
Installing updates...
Kernel updates have been installed.  Please reboot and run
"/usr/sbin/freebsd-update install" again to finish installing updates.

为什么在更新的不同阶段之间需要重启?新的用户空间程序可能需要新的内核功能。安装一个不可用的reboot命令会导致你需要断电重启主机,这会让你的技术卡片丢脸。

# reboot

一旦机器重新启动,完成用户空间升级。

# freebsd-update install
src component not installed, skipped
Installing updates...
Completing this upgrade requires removing old shared object files.
Please rebuild all installed 3rd party software (e.g., programs
installed from the ports tree) and then run "/usr/sbin/freebsd-update 
install" again to finish installing updates.

这是什么疯狂的操作?

更新过程会尽力避免留下损坏的系统或功能失常的软件。如果freebsd-update移除了旧版本的共享库,这些库是你附加软件所需的,那么这些软件将无法运行。此时更新会暂停,给你机会升级软件。我们将在本章稍后讨论如何升级软件包和端口。沿着-stable 分支的升级通常不需要删除旧的无用文件。

这次freebsd-update的操作移除了旧的共享库等。

# freebsd-update install

升级现在已经完成。就像每次进行大规模系统维护时一样,最后再重启一次,确保一切能正常启动。

回滚更新

你以为升级会很顺利,之前每次都很顺利。但这次,你错了。新补丁和你软件之间的某些微妙交互让你遇到了麻烦。如果你正在使用启动环境,现在是时候恢复到之前的安装了。如果没有使用启动环境,请使用freebsd-updaterollback命令移除最近安装的升级。

# freebsd-update rollback
Uninstalling updates... done.

回滚比安装补丁要快得多。无需检查系统;freebsd-update只需读取之前操作的日志并撤销所有更改。

安排二进制更新

最佳实践是定期在固定的时间下载和应用更新,比如每月的维护日。freebsd-update程序提供了专门的支持,避免每小时定时向下载服务器发送请求。freebsd-update cron命令会告诉系统在接下来的一个小时内随机时间下载更新。将此命令放在/etc/crontab文件中,就可以在这个一小时的窗口内下载更新。这有助于减少下载服务器的负载。系统有更新时,你会收到一封电子邮件,这样你就可以在方便的时候安排重启。

优化和自定义 FreeBSD 更新

关于 FreeBSD 更新,有两个常见的问题:自定义构建的 FreeBSD 版本和本地分发更新。

许多人为内部使用构建自己的 FreeBSD 版本。通常,这只是删除了一些部分的 FreeBSD 版本,但一些公司会进行广泛的修改。如果你从 FreeBSD 安装中删除了文件,freebsd-update(8)将不会尝试修补这些文件。

类似地,许多公司喜欢为补丁管理设置内部更新服务器。FreeBSD Update 系统专门设计用于与缓存 Web 代理配合工作。尽管所有文件都经过加密签名并验证,但它们通过普通 HTTP 传输,以便你的代理可以对其进行缓存。

通过源代码升级

另一种更新系统的方式是从源代码构建它。FreeBSD 是自托管的,这意味着它包含了构建 FreeBSD 所需的所有工具。你不需要安装任何编译器或开发工具包。构建一个新的 FreeBSD 系统所需要的唯一条件是更新的源代码。

当开发者发布 FreeBSD 的改进时,这些更改会在几分钟内全球范围内可用。FreeBSD 主源代码服务器跟踪源代码、所有对该代码所做的更改以及这些更改的作者。开发者可以提交新的代码,用户可以通过Subversion (SVN)检出最新版本。FreeBSD 有一个简单的 SVN 客户端 svnlite(1),足以处理所有源代码操作。这是一个没有 svn(1)支持的复杂选项的标准 Subversion 客户端。许多人发现 svnlite(1)足够满足他们所有非 FreeBSD 的 Subversion 需求。

CSUP、CVSUP、CVS、SUP 和 CTM?

不幸的是,互联网上的文档通常会长时间存在,并在最糟糕的时刻引发混乱。复生的 FreeBSD 文档和第三方教程可能会提到使用名为CVSCVSup的工具进行源代码更新。这些工具在 2013 年被取代。任何提到这些程序的地方都表明你在阅读旧文档。如果你看到提到 CTM,那么你正在阅读比 CVS 更早的文档。^([4)

升级源代码需要控制台访问。你并不总是需要它,但从一个坏的构建恢复可能需要在键盘上进行干预。在安装自建操作系统版本之前,请测试你的串口控制台、Java 应用程序或 IPMI 控制台。根据我的经验,唯一需要控制台访问的升级是那些我没有控制台访问的情况。

哪个源代码?

每个 FreeBSD 版本都附带用于构建你正在安装的系统的源代码。如果你在安装系统时没有选择安装源代码,你会在安装介质的/usr/freebsd-dist/src.txz找到它。如果你安装了源代码,你会在/usr/src找到它。

不幸的是,这个版本的源代码缺少构建 FreeBSD 所需的版本控制标签。它仅供参考。你需要使用 svnlite(1)下载一个带有这些标签的代码版本。

你在/usr/src的源代码副本是否适合构建 FreeBSD?请询问 svnlite(1)。

# svnlite info /usr/src
svn: E155007: '/usr/src' is not a working copy

“不是工作副本”错误意味着这里的任何源代码都不能与 Subversion 一起使用。

获取源代码的 svnlite(1)命令如下。

# svnlite checkout ➊svn.freebsd.org/➋repository/➌branch ➍localdir

镜像 ➊ 是一个 FreeBSD Subversion 镜像。镜像 svn.FreeBSD.org 是一个地理路由别名,指向最近的 Subversion 镜像。

仓库 ➋ 是你正在使用的代码组。你可以在 svnweb.FreeBSD.org/ 上获取当前仓库的完整列表。主要的项目仓库包括 base(操作系统),doc(文档)和 ports(Ports 集合)。

分支 ➌ 是你想要的 FreeBSD 版本。对于最新的短指头 -current,使用 head。要获取稳定版本,使用分支 stable/ 后跟主版本号。FreeBSD 12-stable 就是 stable/12。要获取一个版本及其所有当前补丁,使用 releng/ 和发布号。完全打补丁的 FreeBSD 12.2 位于 releng/12.2

如果你不确定需要哪个分支,可以浏览 svnweb.freebsd.org/。分支实际上告诉 svnlite(1) 从这个站点获取哪个子目录。

最后,localdir ➍ 是我希望放置源代码的本地目录。

这台主机运行的是 FreeBSD 11.1。我想要尝试升级到 FreeBSD 11-stable。以下是我将怎么做:

# svnlite checkout https://svn0.us-east.FreeBSD.org/base/stable/11 /usr/src/
Error validating server certificate for 'https://svn0.us-east.freebsd.org:443':
 - The certificate is not issued by a trusted authority. Use the
   fingerprint to validate the certificate manually!
Certificate information:
 - Hostname: svnmir.ysv.FreeBSD.org
 - Valid: from Jul 29 22:01:21 2013 GMT until Dec 13 22:01:21 2040 GMT
 - Issuer: svnmir.ysv.FreeBSD.org, clusteradm, FreeBSD.org, CA, US(clusteradm@FreeBSD.org)
 - Fingerprint: 1C:BD:85:95:11:9F:EB:75:A5:4B:C8:A3:FE:08:E4:02:73:06:1E:61
(R)eject, accept (t)emporarily or accept (p)ermanently? p

这是什么新鲜的疯狂事?我们尝试获取 FreeBSD 源代码,结果却遇到了证书错误?

将显示的证书指纹与 FreeBSD.org 网站上的服务器指纹进行比较。如果匹配,输入 p 永久接受证书。源代码文件的文件名将在你的终端上滚动显示。

一旦 svnlite(1) 完成,查看 /usr/src

# ls /usr/src/
COPYRIGHT               cddl                    sbin
LOCKS                   contrib                 secure
MAINTAINERS             crypto                  share
Makefile                etc                     sys
Makefile.inc1           gnu                     targets
Makefile.libcompat      include                 tests
ObsoleteFiles.inc       kerberos5               tools
README                  lib                     usr.bin
README.md               libexec                 usr.sbin
UPDATING                release
bin                     rescue

这是 FreeBSD 源代码树的顶级目录,包含构建你选择的 FreeBSD 版本所需的所有代码。

更新源代码

所以你昨天构建了 FreeBSD?很酷。但现在你想要构建今天的版本。

好消息是,Subversion 只需要更新你已经下载的代码,而不是重新下载整个源代码树。更好的消息是,源代码会记录你从哪里获取它,以及它应该属于哪个分支,这些信息存储在 .svn 目录中。这使得更新比初始下载要简单得多。

FreeBSD 已经将 Subversion 命令集成到源代码中。你需要做的就是告诉系统,它可以通过设置 SVN_UPDATE 在 /etc/src.conf 中调用 svnlite(1) 来更新。

# echo 'SVN_UPDATE=yes' >> /etc/src.conf

现在运行 make update 来获取最新的源代码。

# cd /usr/src
# make update

你会看到相同类型的更新流过。尽管这些更新会比最初的下载速度快得多。

从源代码构建 FreeBSD

一旦你拥有最新的源代码,查看/usr/src/UPDATING文件。该文件的开头按时间倒序列出了与 FreeBSD 变更相关的任何警告和特别通知,这些内容对从源代码构建的人尤为重要。这些说明会告诉你在重建系统之前是否需要采取任何特别的操作,或者系统的主要功能是否发生了变化。如果你希望在升级后系统能够正常工作,请严格按照这些说明操作。

UPDATING文件的末尾给出了从源代码构建 FreeBSD 的官方说明。本书中描述的过程自 FreeBSD 6-current 以来一直在使用,虽然与 5-current 相比仅有些微变化,但我仍然建议你将本书中的说明与UPDATING中的说明进行核对。

如果你使用自定义内核,还需要检查新的 GENERIC 或 NOTES 内核配置文件,查看是否有新的选项或有趣的内核变更。

自定义你的 FreeBSD 构建

还记得在第十六章中我们讨论过/etc/make.conf吗?FreeBSD 使用一个单独的文件来处理构建 FreeBSD 自身的自定义设置。虽然/etc/make.conf中的设置会影响系统上构建的所有软件,但/etc/src.conf中的任何内容只会影响从源代码构建 FreeBSD。

如果你在 FreeBSD 社区呆一段时间,你会听到各种关于人们用来构建 FreeBSD 的特殊方法的故事。你会听到一些非正式的证据,声称某种方法比标准方法更快、更高效,或者在某种神秘的方式上“更好”。虽然你当然可以自由选择任何你喜欢的构建方法,但 FreeBSD 项目唯一支持的方法是/usr/src/UPDATING文件末尾所记录的方法。如果你采用其他程序并遇到问题,你将被引导回文档中记录的程序。

构建世界

首先,构建新的用户环境:

# cd /usr/src
# make buildworld

make buildworld命令从源代码构建构建系统编译器所需的基本工具,然后构建编译器及其相关库。最后,它使用新工具、编译器和库来构建 FreeBSD 核心安装中包含的所有软件。(这就像在开始构建一辆车时,指令是“从地下挖出铁矿石。”)buildworld将其输出放在/usr/obj目录下。根据你的硬件,它可能需要从一小时到几个小时的时间。如果你的硬件足够强大,buildworld运行时你可以继续正常工作;尽管构建过程中会消耗系统资源,但它不会占用你的注意力。

buildworld完成后,确认它没有错误地完成。如果构建结束时显示了类似内核编译失败时的错误信息,不要继续进行升级。如果你无法弄清楚为什么构建失败,去第一章查看如何获得帮助。绝不要尝试安装损坏或不完整的升级版本。

并行构建世界

经验丰富的系统管理员可能已经使用过 make(1) 的 -j 标志来提高构建速度。这会启动多个构建进程,并允许系统利用多个 CPU。如果你有一个多 CPU 系统,或者你的 CPU 有多个核心,使用 -j 在构建 FreeBSD 时是可行的。一个合理的启动构建进程数是比你的 CPU 数量多一个。例如,如果你有一个八核处理器,你可以通过运行 make -j9 buildworld && make -j9 kernel 来合理地使用九个构建进程。

FreeBSD 项目官方并不支持 -j 用于升级,尽管许多开发者使用它。如果在使用 -j 时构建失败,在抱怨之前请尝试去掉 -j

构建、安装并测试内核

测试升级的最佳方法是构建一个新的 GENERIC 内核。这可以将自定义内核中的问题与 FreeBSD 的通用问题分开。冲动的用户当然可以直接升级到他们的自定义内核配置,但如果你的内核失败了,你需要尝试使用 GENERIC 内核。请确保比较你的自定义内核与新的 GENERIC 配置,以便捕捉到任何自定义设置所需的修改。你可以使用 Subversion 历史记录在 svnweb.FreeBSD.org/ 上对比不同版本的内核配置。

默认情况下,内核升级过程会构建一个 GENERIC 内核。如果你想直接升级到自定义内核,可以使用变量 KERNCONF 告诉 make(1) 内核的名称。你可以在命令行、/etc/make.conf/etc/src.conf 中设置 KERNCONF

你可以通过两种方式构建一个新的内核。make buildkernel 命令会构建一个新的内核,但不会安装它。你可以在运行 make buildkernel 后,接着运行 make installkernel 来安装内核。make kernel 命令会在执行时同时运行这两个命令。选择最适合你计划的方式。例如,如果我在工作中进行系统升级,并且是在周日的维护窗口进行,我可能会在前一周运行 make buildworldmake buildkernel 来节省几个小时的宝贵周末时间。但我不想在维护日之前安装该内核——如果机器在周五出现问题并需要重启,我想启动旧的生产内核,而不是新的升级内核。周日早上,当我准备好进行升级时,我会运行 make installkernel。另一方面,当升级我的桌面系统时,使用 make kernel 是更有意义的。因此,要使用我的自定义内核进行升级,我会运行:

# make KERNCONF=THUD kernel

同样,请不要尝试安装未成功编译的内核。如果你的 make buildkernel 出现错误并中断,请在继续之前先解决该问题。

安装了新内核后,重新启动计算机进入单用户模式。为什么要单用户模式?用户空间可能期望与新内核提供的不同的内核接口。虽然/usr/src/UPDATING应列出所有这些内容,但没有文档可以涵盖所有可能的更改及其对第三方软件的影响。这些更改在-stable 上很少发生,但在-current 上却是不可预测的。如果每周更新主机,则您的用户空间可能在新内核上出现问题。如果您一年没有更新主机,那么所有变更将同时被推送给您。虽然许多人可以在完全多用户模式下安装升级,但单用户模式是最安全的选择。

如果您的系统在新内核的单用户模式下运行正常,请继续。否则,请完全记录问题并启动旧内核以恢复服务,同时解决问题。

准备安装新世界

当心,小虫子!这是无法回头的时刻。您可以轻松地回退到一个糟糕的内核——只需启动较旧的、已知良好的内核即可。一旦安装了新构建的世界,您将无法撤销,除非从备份中恢复或使用 ZFS 引导环境。在继续之前,请确认您有一个良好的备份,或者至少认识到第一个不可逆转的步骤正在发生。

如果新内核运行正常,请继续安装您刚刚构建的用户空间。首先确认您的系统能够安装新的二进制文件。每个新版本的 FreeBSD 都希望旧系统支持新版本所需的所有必要用户、组和权限。如果一个程序必须由特定用户拥有而该用户在系统上不存在,则升级将失败。这就是 mergemaster(8)的用武之地。

mergemaster(8)

mergemaster 程序将现有的配置文件在/etc下与/usr/src/etc中的新文件进行比较,突出显示它们之间的差异,并为您安装它们,将它们设置为评估的一部分,甚至允许您将两个不同的配置文件合并为一个。在升级过程中,这非常有用。您在安装新世界之前运行一次 mergemaster 以确保系统能够安装新的二进制文件,并在安装新世界后再运行一次以使/etc的其余部分与新世界同步。

以 mergemaster(8)的 prebuildworld 模式开始,使用-Fp标志。-F标志自动安装仅通过版本控制信息不同的文件。-p标志比较/etc/master.passwd/etc/group,并突出显示任何必须存在以使installworld成功的帐户或组。

  # mergemaster -Fp

➊  *** Creating the temporary root environment in /var/tmp/temproot
   *** /var/tmp/temproot ready for use
➋  *** Creating and populating directory structure in /var/tmp/temproot

   *** Beginning comparison

这些初始消息,均以三个星号开头,是 mergemaster 在解释其操作。我们从临时根目录 ➊ /var/tmp/temproot 开始,这样 mergemaster 就可以安装一组原始的配置文件 ➋ 以便与已安装的文件进行比较。之后,mergemaster 展示了第一次比较。

➊   *** Displaying differences between ./etc/group and installed version:

  --- /etc/group  2017-09-01 11:12:49.693484000 -0400
  +++ ./etc/group 2017-09-01 13:22:15.849816000 -0400
  @@ -1,6 +1,6 @@
➋  -# $FreeBSD: releng/11.1/etc/group 294896 2016-01-27 06:28:56Z araujo $
➌  +# $FreeBSD: stable/11/etc/group 294896 2016-01-27 06:28:56Z araujo $
    #
➍  -wheel:*:0:root,mwlucas
➎  +wheel:*:0:root
    daemon:*:1:
    kmem:*:2:
    sys:*:3:
  @@ -33,4 +33,3 @@
    hast:*:845:
    nogroup:*:65533:
    nobody:*:65534:
➏  -mwlucas:*:1001:

一个重要的信息是正在对比的文件,mergemaster 会在前面显示文件名 ➊。我们正在检查已安装系统中的/etc/group,并与新的/etc/group进行对比。

然后,我们看到两个不同版本的文件进行对比,首先是已安装的文件 ➋,其次是升级后的文件 ➌。注意这些行前的减号和加号。减号表示该行来自当前已安装的文件,而加号表示该行来自/usr/src中的版本。

这一点在 mergemaster 显示的接下来的两行中得到了很好的说明。第一行带有减号的组是当前的 wheel 组 ➍。第二行是出厂升级的密码条目 ➎。这个主机的 wheel 组包含了一个不在默认安装中的用户。我想保留我的账户在其中。

在列表的最后,有一个前面带有减号的组 ➏。这个组存在于本地系统中,但不在源代码中。这是完全正常的。

这些更改与本次无关。

一旦 mergemaster 显示完所有这些文件中的更改,它会显示我的选项。

  Use 'd' to delete the temporary ./etc/group
  Use 'i' to install the temporary ./etc/group
  Use 'm' to merge the temporary and installed versions
  Use 'v' to view the diff results again

  Default is to leave the temporary file to deal with by hand

How should I deal with this? [Leave it for later] d

我有四个选择。我可以使用d删除临时的/etc/group。如果我想抛弃当前配置并直接从源代码安装一个新配置,我可以使用i安装。如果我需要旧版和新版的一部分内容,我可以使用m。如果我没注意到变化,我可以使用v再次查看对比。

新的/etc/group没有我需要的更改。我删除它,让 mergemaster 继续处理下一个文件,/etc/passwd

mergemaster 显示的密码文件开始时与组文件类似。是的,root 密码已经更改——我希望是这样!我的账户有一个额外的条目。但在显示的中间,有一个这样的条目:

   _pflogd:*:64:64::0:0:pflogd privsep user:/var/empty:/usr/sbin/nologin
➊ +_dhcp:*:65:65::0:0:dhcp programs:/var/empty:/usr/sbin/nologin
   uucp:*:66:66::0:0:UUCP pseudo-user

用户 _dhcp ➊的行前面带有加号,并且没有对应的带减号的 _dhcp 条目。用户 _dhcp 存在于下载的源代码中,但不在当前运行的系统中。如果新用户出现在默认的 FreeBSD 配置中,那是因为新系统中的程序或文件期望由该用户拥有。

如果这个用户不存在,安装新版本将失败。

我不能替换当前的/etc/passwd,因为它包含了活动用户账户。我也不能丢弃新的/etc/passwd,因为它包含了我需要的用户。我想我必须将这两个文件合并在一起。

How should I deal with this? [Leave it for later] m

合并文件时,mergemaster 会将你的命令窗口一分为二,显示 sdiff(1)。左侧显示当前已安装文件的开头,而右侧显示新版文件。只有不同的部分会显示。选择你希望保留在新master.passwd文件中的部分。

# $FreeBSD: releng/11.1/etc/master.passwd 299 | # $FreeBSD: stable/11/etc/master.passwd 29936

这一行显示了 /etc/passwd 文件两份副本的版本控制信息。左侧是 releng/11.1 分支中的文件版本,即 11.1;右侧是 stable/11 分支中的文件版本,即 11-stable。未来的 mergemaster 运行将使用版本信息(以及其他工具)来确定文件是否需要更新,因此我们的合并文件需要正确的版本号。请选择左侧(l)或右侧(r)列。我想要右侧的条目,所以我输入 r

Mergemaster 显示下一个差异。

root:$6$fD7a5caQtkZbG93E$wGfw5G2zNORLq8qxlT8z | root::0:0::0:0:Charlie &:/root:/bin/csh

我已经更改了我的 root 密码,并且希望保留它。输入 l 以保留左侧版本。

                                              > _dhcp:*:65:65::0:0:dhcp programs:/var/empty:/

在下一个示例中,当前文件中没有 _dhcp 用户,而新文件中有。我们需要 _dhcp 用户来完成 installworld,因此输入 r 选择右侧条目,并获取下一个差异。

mwlucas:$1$zxU7ddkN$9GUEEVJH0r.owyAwU0NFX1:10 <

这是我的账户。如果我想在升级后以自己身份登录,我最好输入 l。

一旦我们浏览完文件中的每个差异,mergemaster 将展示我们的下一个选择:

  Use 'i' to install merged file
  Use 'r' to re-do the merge
  Use 'v' to view the merged file
  Default is to leave the temporary file to deal with by hand

    *** How should I deal with the merged file? [Leave it for later]

查看合并后的文件总是个好主意,除非你已经知道自己搞砸了,并且想重新做一遍。用 v 查看合并后的文件,如果看起来正确,就用 i 安装它。

*** You installed a new master.passwd file, so make sure that you run
    '/usr/sbin/pwd_mkdb -p /etc/master.passwd'
     to rebuild your password files

    Would you like to run it now? y or n [n]y

只有当你希望新的用户账户生效时,才需要重建密码数据库。输入 y。

你现在可以安装你的新用户空间。

安装世界

仍在单用户模式下,你可以使用 make installworld 安装全新的 FreeBSD。你会看到大量消息滚动屏幕,主要包括 install 这个词。

# cd /usr/src
# make installworld

你现在拥有了新的用户空间,与全新的内核一起。恭喜!

过时的文件

然而,仅仅安装所有新程序还不够。更新可能会从基础系统中删除程序和文件。要查看哪些文件已经过时,可以运行 make check-old

# make check-old
>>> Checking for old files
/usr/include/sys/ksyms.h
/usr/lib/clang/4.0.0/include/sanitizer/allocator_interface.h
/usr/lib/clang/4.0.0/include/sanitizer/asan_interface.h
/usr/lib/clang/4.0.0/include/sanitizer/common_interface_defs.h
--snip--

这将列出系统中曾经安装但现在不再需要的每一部分。确认你不再使用这些程序;如果仍在使用,可以保留现有的不受支持的程序或寻找替代方案。

稍后,你会看到现在已经过时的共享库:

>>> Checking for old libraries
/lib/libzfs.so.2
/usr/lib/debug/lib/libzfs.so.2.debug
/usr/lib/libarchive.so.6
/usr/lib/debug/usr/lib/libarchive.so.6.debug
/usr/lib/libmilter.so.5
--snip--

最后,你可能会看到一个不再需要的目录列表。与删除文件相比,删除目录比较少见。

如果你没有特别使用任何旧的程序或目录,可以用 make delete-old 删除它们。make(1) 会提示每个文件的名称,并询问你是否确认删除该文件。

# make delete-old
>>> Removing old files (only deletes safe to delete libs)
remove /usr/include/sys/ksyms.h? y
remove /usr/lib/clang/4.0.0/include/sanitizer/allocator_interface.h? y
remove /usr/lib/clang/4.0.0/include/sanitizer/asan_interface.h?

这真是愚蠢。这些文件有好几十个。我打算删除每一个。幸运的是,每个真实的 Unix 系统都包括一些自动化愚蠢操作的工具。

# yes | make delete-old

所有这些文件都会被删除。或者,如果你想使用 FreeBSD 的内建选项,可以使用 BATCH_DELETE_OLD_FILES 标志。

# make -DBATCH_DELETE_OLD_FILES delete-old

不过,我觉得使用 yes(1) 更简单。

过时的共享库

过时的共享库需要更加小心。许多第三方程序依赖共享库。如果你删除了共享库,该程序将无法运行。如果你删除了关键任务应用程序所需的库文件,这会非常麻烦。恢复服务的唯一方法是重新编译程序或替换共享库。我们在第十七章中讨论了共享库。如果没有程序需要该库,你可以删除它。然而,识别每个需要该库的程序是件麻烦事。

例如,检查上面列出的过时共享库。条目之一是 libzfs.so.2。查看/lib,我发现现在有 libzfs.so.3。也许我不需要两个不同版本的 ZFS 库。虽然这台主机使用 ZFS,并且我安装了很多 ZFS 工具。如果我删除旧版本的 libzfs,可能会导致其中某些程序无法再工作。过时的库版本短期内并不会造成问题;你可以在系统上同时保留旧库和新库,并以更从容的方式重新安装附加软件。稍后我们将在本章更新你的端口。

如果你认为列出的旧库都不重要,可以安全删除它们,那么在删除之前先备份每个库文件。只需将库文件复制到某个old-libs目录中,当你发现关键任务软件不再工作时,恢复起来会简单得多。

你还可以将旧库文件复制到/usr/lib/compat目录中,这样你的程序将继续运行,但旧的库文件会被安全地放在一旁。这里的问题是,我们都知道你永远不会去清理这些库文件。

我更喜欢先备份库文件,然后再将它们从实时系统中移除。当我发现某个程序无法工作时,我会暂时从备份中将缺失的库文件复制到兼容目录中。这种额外的麻烦足以让我去解决真正的问题,这样我就可以删除兼容库。

# make check-old-libs | grep '^/' | tar zcv -T - -f /root/old-libs.tgz

一旦你备份了库文件,删除它们。你可以在这里使用BATCH_DELETE_OLD_FILES选项,但我发现 yes(1)更容易输入。

# yes | make delete-old-libs

如果由于某种原因删除这些库文件导致 pkg(8)出现问题,可以运行pkg-static install -f pkg来修复 pkg(8)本身,或者运行pkg-static upgrade -f来重新安装所有包。

另一个选择是使用 libchk 包来识别链接到旧库的程序。

mergemaster 重访

我们快完成了!虽然我们已经更新了/etc中的密码和组信息,但我们还必须更新其余的文件。mergemaster 有很多特殊功能,所有这些都在其手册页中有详细记录。我将特别推荐两个我认为特别有用的功能。

当一个文件被添加到基础 FreeBSD 安装中时,无需与任何内容进行比较。-i选项会使 mergemaster 自动安装/etc中的新文件。在 mergemaster 运行结束时,我会得到一个自动安装文件的列表。

另一些我不太关心的文件是那些我没有编辑过的文件。例如,FreeBSD 在/etc/rc.d中有数十个启动脚本。如果我没有编辑过某个启动脚本,我只想安装该脚本的最新版本。-U标志告诉 mergemaster 自动更新任何我没有编辑过的基础系统文件。

# mergemaster -iU

mergemaster 程序会检查/etc中的每个文件,并与 FreeBSD 基础发行版中的文件进行比较。这与你在安装前运行 mergemaster 时的方式完全相同,因此我们这里不再详细讲解。你应该熟悉你对系统所做的自定义,因此不应该有什么意外。如果有任何看起来不熟悉的内容,请参考第十四章。

重启后,基础系统就完全升级了!

自定义 Mergemaster

一旦你运行 mergemaster 几次,你会发现某些文件总是让你烦恼。mergemaster 总是抱怨你定制的/etc/motd/etc/printcap。你可能会每次都输入-F-U。你可以将你偏好的选项设置在$HOME/.mergemasterrc中,具体见 mergemaster(8)文档。虽然你应该阅读手册页面来查看完整的选项列表,以下是我最常使用的选项。

忽略文件

某些文件你不希望 mergemaster 甚至去检查。你组织的/etc/motd文件永远不会与 FreeBSD 发行版中的文件匹配。你的自定义打印机配置、snmpd.conf或定制的sshd_config也不会匹配。要让 mergemaster 跳过这些文件,可以将它们列在IGNORE_FILES中。

IGNORE_FILES='/etc/motd /etc/printcap'

我在这里没有列出密码或组文件,因为有时 FreeBSD 会引入新的用户。

自动安装新文件

如果你希望 mergemaster 自动安装 FreeBSD 新版本中但尚未出现在系统上的文件,可以设置AUTO_INSTALL

AUTO_INSTALL=yes

这相当于-i标志。

自动更新未更改的文件

不同版本的 FreeBSD 有类似的文件。有些文件几乎是相同的,只在包含版本控制信息的那一行上有所不同。你可以告诉 mergemaster 通过使用FREEBSD_ID选项自动更新那些只在版本控制信息上有所不同的文件。

FREEBSD_ID=yes

这与-F标志相同。

自动更新未编辑的文件

你可以告诉 mergemaster 更新那些自安装以来未被编辑过的文件。FreeBSD 团队会在希望改变系统行为时修改/etc/中的文件。虽然这些变化中的许多可能与你无关,但其中的一些可能会给你带来麻烦。如果你想盲目更新所有未更改过的文件,可以设置AUTO_UPGRADE

AUTO_UPGRADE=yes

这相当于-U选项。

更新数据库

FreeBSD 从/etc/master.passwd/etc/services等文件构建数据库。如果你更新了这些文件,也需要更新相应的数据库。Mergemaster 会在每次运行结束时询问你是否要运行这些更新。通过设置RUN_UPDATES,告诉 mergemaster 始终运行更新。

RUN_UPDATES=yes

你可以在 mergemaster(8)中找到其他选项。

升级与单用户模式

根据说明,升级的几个部分必须在单用户模式下完成。许多用户认为这是烦人的,甚至是一个障碍。FreeBSD 程序不就是磁盘上的文件吗?常识告诉我们,你可以将它们复制到磁盘上,重新启动,然后就完成了。

这是另一个常识试图毁了你一个月的例子。偶尔,FreeBSD 团队需要做一些系统的底层更改,这些更改要求在单用户模式下运行安装程序。你可能会遇到冲突,导致一些重要程序在多用户模式下无法运行。这种情况很少发生,但如果它发生在/bin/sh上,你会陷入麻烦。如果发生这种情况,你有一个非常简单的恢复路线:将硬盘从服务器中取出,挂载到另一台机器上,启动该机器,并在格式化并重新安装之前将数据从损坏的系统中复制出来。或者,你可以从安装介质启动,展示你惊人的系统管理员技能。^(5)

在多用户模式下运行可能会导致其他升级问题,比如微妙的竞争条件、符号问题和无数其他头痛问题。你可以选择在多用户模式下升级,但如果系统出现问题,不要抱怨。

在多用户模式下构建新世界是完全安全的。你甚至可以在多用户模式下构建并安装你的新内核。然而,一旦你开始安装用户空间,你必须进入单用户模式,并在升级后的内核上运行。

NFS 和升级

有很多机器需要更新吗?请查看我们在第十三章中讨论过的 NFS。你可以在一台中央的快速机器上构建世界和所有内核,然后将/usr/src/usr/obj从该系统导出到其他客户端。从这些 NFS 导出中运行make installkernelmake installworld可以节省其他机器的构建时间,并确保所有 FreeBSD 系统上的二进制文件一致。

缩减 FreeBSD

如果你无法自定义操作系统,拥有这些源代码有什么意义呢?FreeBSD 不仅提供源代码,它还提供了现成的旋钮,轻松自定义你的 FreeBSD 构建。

这些选项可以在/etc/make.conf(参见第十六章)或/etc/src.conf中设置。src.conf中的设置仅适用于构建 FreeBSD 源代码,而make.conf的设置适用于所有源代码构建。src.conf选项的完整列表在 src.conf(5)中有文档说明,但它们都遵循一个标准的模式。

这些选项中的每一个都以WITHOUT_开头,少数情况下是WITH_,然后是特定子系统的名称。例如,WITHOUT_BHYVE选项会关闭 bhyve(8)虚拟机监控器的构建或安装。WITHOUT_INETD选项会关闭 inetd(8)守护进程的构建(参见第二十章)。WITHOUT_INET6选项会关闭 IPv6。如果你想缩小你的 FreeBSD 安装,剔除所有不需要的部分。

构建系统会检查这些变量是否已定义为某个值。这意味着即使你将其中一个设置为NO,仅仅是选项的存在就会激活该选项。不要把这些全部复制到src.conf并将它们都设置为NO,因为这样你会禁用系统中大量的构建内容。

在大多数情况下,添加这些WITHOUT_选项会将移除的系统包含在make delete-old检查中。例如,如果你决定系统不需要 bhyve(8),升级时不仅不会构建新的 bhyve 二进制文件,还会提议从已安装的系统中移除现有的 bhyve。如果你不打算构建某个软件,最好完全移除它,而不是让旧版本继续存在于系统中。

软件包和系统升级

操作系统升级很棒,除了没人关心的那部分。^(6) 基础操作系统更新是必要的,但大多数人并不关心使用基础系统。他们关心的是在基础系统上运行的软件。而这些软件和其他程序一样,容易出现缺陷。你需要升级它。第十五章讨论了软件包的一般升级,但我们来谈谈当你升级操作系统时对软件包的影响。

软件包和系统升级中常见的问题通常归结为共享库。如果你正在升级 FreeBSD 的主要版本——比如从 FreeBSD 12 升级到 FreeBSD 13——你需要重新安装所有的软件包。

首先,升级 pkg(8)本身,使用-f标志进行pkg upgrade。如果升级破坏了 pkg(8)本身,你需要使用 pkg-static(8)。这个工具包含了 pkg(8)的核心功能,但它是一个静态二进制文件。

# pkg-static upgrade -f pkg

这将把你引导进入当前的软件包工具。现在你可以强制重新下载并重新安装所有软件包。

# pkg upgrade -f

一旦你升级了所有通过软件包构建的内容,就需要重新构建任何通过端口构建的内容。我真心希望你是通过 poudriere 安装的端口。

更新已安装的端口

如果你使用 portsnap 来更新端口树,那么从现在开始,你安装的任何内容都会是最新版本。但之前安装的应用程序怎么办?FreeBSD 跟踪各种附加包之间的依赖信息,通常更新一个程序会影响到其他数十个程序。这是一个非常麻烦的管理问题。难道不希望能简单地说,“更新我的 Apache 安装”,然后让 FreeBSD 为你管理依赖关系吗?有几种方法可以解决这个问题。

最好的方法是在生产主机上不使用端口树。改为使用 poudriere 构建一个私有仓库(参见第十六章),然后让所有主机从中拉取。这与传统的 FreeBSD 系统管理员做法有所不同。

也许你已经在本地安装了端口树,并在一堆包上方使用一两个自定义端口。如果你只安装了一个端口,可以重建它,卸载包,然后安装新端口。对于依赖关系复杂的端口来说,这很糟糕,但对于只有一两个端口的主机来说,效果很好。

我们中的一些人已经存在很长时间了,感觉自己生活在这些解决方案之间。我们的主机感觉太小,无法运行 poudriere,但我们需要自定义端口。FreeBSD 没有包含用于更新主要由端口管理的系统的官方工具,但有人编写了附加工具,如 portupgrade 和 portmaster。这些工具的问题在于它们是在 FreeBSD 之外维护的。如果它们无法升级端口,但正常的构建过程有效,工具的用户需要负责修复问题。这适用于 FreeBSD 的所有部分,但基础系统的用户群比任何附加工具都要广泛。

然而,随着我写这篇文章时,FreeBSD 的端口基础设施正在发生变化,以支持单个包的多个版本。这些工具尚未更新以适应新的基础设施。我预计它们中的一个会更新,但我不能说具体会是哪一个。第十六章建议在生产环境中仅使用包。这就是原因。

现在你可以更新系统和已安装的软件,让我们来看一下 FreeBSD 一些更有趣的安全功能。

第十九章:高级安全特性

image

FreeBSD 提供了多种工具来保护网络流量和用户。一些工具对系统管理员是不可见的,但它们在后台工作以增加安全性,例如沙箱 API capsicum(4)。数据包过滤使你可以控制谁可以访问你的系统。你还可以使用黑名单来阻止那些不断攻击你主机的网络地址。此外,FreeBSD 还拥有一整套可选的安全特性,你可以在安装过程中或稍后启用。在本章中,我们将研究这些工具和技术,了解如何监控系统的安全性,并讨论如果遭遇入侵该如何应对。

我们从一个核心安全话题开始:非特权用户。

非特权用户

非特权用户是为特定任务创建的特定用户。他只有执行该有限任务所需的权限。许多程序作为非特权用户运行,或使用非特权用户执行特定职责。

“只有执行职责所需的权限”听起来像是每个用户账户的要求,不是吗?确实如此,但即便是最低权限的人类用户所使用的账户,仍然比许多程序所需的权限要多。任何拥有 shell 访问权限的用户都有一个家目录。普通用户可以在其家目录中创建文件、运行文本编辑器或处理电子邮件。普通 shell 用户需要这些最小权限,但程序不需要。通过让程序,特别是网络守护进程,以一个非常受限的用户身份运行,你可以控制入侵者对程序或用户造成的损害。

FreeBSD 包括几个非特权用户。查看 /etc/passwd,你会看到像 auditbinduucpwww 这样的账户。这些都是为特定服务器守护进程使用的非特权账户。看看它们有什么共同点。

非特权用户没有正常的家目录。许多用户的家目录是 /nonexistent,而其他一些用户,如 sshd,则有一个特殊的家目录,如 /var/empty。拥有一个无法写入或读取文件的家目录虽然使账户的灵活性降低,但对于服务器守护进程来说已经足够。虽然这些用户确实拥有系统上的文件,但通常无法修改这些文件。

同样,任何人都不应该登录这些账户。如果账户bind是为 DNS 系统保留的,那么实际上也不应该以该用户身份登录系统!这样的账户必须具有专门拒绝登录的用户 shell,例如 /usr/sbin/nologin。这一切是如何增强系统安全性的呢?让我们看一个例子。

无论你使用的是哪个 Web 服务器,它通常都会在未授权账户 www 下运行。假设入侵者发现了你正在使用的 Web 服务器程序版本中的安全漏洞,并且能够让 Web 服务器执行任意代码。这是最糟糕的安全问题之一,因为入侵者可以让服务器程序做它有能力做的任何事。那么,这个程序的能力究竟是什么呢?

入侵者可能想要在系统上获得一个命令提示符。毕竟,在类 Unix 系统上,命令提示符是进入更多访问权限的门槛。未授权用户拥有一个专门不允许登录的 shell。这会让入侵者非常恼火,迫使他们付出更多努力才能达到命令提示符。

不过,如果她真的足够聪明,nologin shell 并不会阻止入侵者。假设通过巧妙的手段,她让 Web 服务器执行一个简单的 shell,比如 /bin/sh,并向她提供命令提示符。她进来了,可以造成无法估量的破坏……或者她能做到吗?

她没有主目录,也没有权限创建一个。这意味着她想存储的任何文件都必须放在一个全局可访问的目录中,比如 /tmp/var/tmp,这增加了她的可见性。Apache 配置文件由 root 或您的 Web 服务器管理组拥有,而 www 用户不属于该组。入侵者可能有途径进入 Web 服务器,但她无法重新配置它。她无法更改网站文件,因为 www 用户并不拥有它们。www 用户对系统上的任何内容都没有访问权限,除了 Web 服务器本身。一个足够有技能的入侵者可以让 Web 服务器展示不同的页面或重定向到另一个站点,至少在重启之前是这样。突破服务器上运行的应用程序或主机本身,则需要一整套额外的安全漏洞。

不过,未授权用户并不能解决所有的安全问题。我们已经被攻破的 www 用户可以查看 Web 应用的源代码文件。如果你的应用编写不当,或者将数据库密码硬编码在隐藏的文件中,你仍然会陷入很大的麻烦。不过,如果你已经保持系统更新并且所有软件包都保持最新,入侵者将很难渗透到 FreeBSD 系统中。

nobody 账户

多年来,系统管理员将账户nobody作为通用的非特权用户。他们会以nobody身份运行网页服务器、代理服务器以及其他程序。这样做比以 root 身份运行这些程序更安全,但还不如为每个守护进程创建独立的用户。如果入侵者成功渗透了这些程序中的一个,他将可以访问所有这些程序。我们的假设性网页服务器入侵者不仅能访问网页服务器,还能访问以相同用户身份运行的其他程序!如果您正在使用 NFS,请记住,NFS 默认将远程 root 账户映射为nobody。使用非特权用户的整个目的是最小化成功入侵后可能造成的损害。

虽然您可能会使用nobody账户进行测试,但绝不要使用它来部署生产服务。要广泛使用独立的非特权账户。

一个示例非特权用户

以下是适用于通用非特权用户的参数:

用户名 为用户的功能分配一个相关的用户名。例如,网页服务器的默认用户是www

主目录 非特权用户应该故意没有主目录,因此使用/nonexistent。也不要创建/nonexistent目录;重点是它根本不存在!

Shell 非特权用户必须没有可以执行命令的 shell,因此使用/usr/sbin/nologin

UID/GID 为非特权用户选择一个特殊范围的用户和组 ID。

全名 为用户的功能分配一个描述性的名称。

密码 使用 chpass(1)将用户的加密密码设置为一个星号,这将禁用账户密码。(请注意,chpass(1)代表更改密码文件,而不是更改密码!)

这些设置使得您的非特权用户确实变得非常不特权。您可以使用 adduser(8)轻松设置这些,给账户设置无密码、正确的主目录和适当的 shell。

许多端口和包已经分配了非特权用户和组,列在/usr/ports/UIDs/usr/ports/GIDs中。不要害怕添加更多。使用 1000 以上的 UID,以避免与包和 FreeBSD 核心分配的 UID 冲突。

网络流量控制

系统管理员必须具备控制进出其系统的流量的能力。必须阻止不受欢迎的访问者,同时允许合法用户访问。FreeBSD 提供了多种工具,允许您控制外部访问您的系统,包括 TCP 包装器、数据包过滤和黑名单功能。

TCP 包装器,或简称包装器,控制网络守护进程的访问权限。虽然程序必须支持 TCP 包装器,但大多数现代软件多年来已经支持包装器。包装器配置相对简单,不需要太多网络知识。然而,作为访问控制手段,包装器的功能是有限的。尽管如此,包装器允许您对连接和提供连接的守护进程执行一些有趣的操作,这就是为什么我们要讨论它的原因。

数据包过滤 控制系统允许哪些流量通过,以及哪些流量被拒绝。大多数防火墙实际上是数据包过滤器,只是上面加了一个漂亮的图形用户界面,但你也可以使用 FreeBSD 的数据包过滤和代理软件来构建一个坚固的防火墙。被拒绝的连接请求永远不会到达任何用户空间程序;它会在网络栈中被阻止。数据包过滤可以控制对任何程序、服务或网络端口的访问,但需要更多的网络知识。

黑名单 在你希望程序能够决定停止监听远程主机时非常有用。最常见的黑名单工具是 fail2ban (www.fail2ban.org/),它非常灵活,但需要大量的特殊配置。FreeBSD 包含 blacklistd,一个更易配置的黑名单工具,但需要与使用它的程序集成。

你应该使用哪种策略?对于基本的 TCP/IP 访问控制,我建议始终使用数据包过滤器。只有在需要 TCP 包装器的特定功能时,才使用它们。我仅将 TCP 包装器的连接阻止与允许作为实现这些高级功能的前提进行讨论。如果你希望服务在多次连接失败后阻止客户端,可以考虑使用 blacklistd。

使用包装器或数据包过滤时,你必须决定是选择默认接受还是默认拒绝的流量控制策略。

默认接受与默认拒绝

在任何安全策略中,一个重要的决策就是选择默认接受还是默认拒绝。默认接受 安全策略意味着你允许任何类型的连接,除了你明确禁止的连接。默认拒绝 策略意味着你只允许来自特定部分互联网和/或特定服务的连接,拒绝所有其他连接。除非你制定了特定规则,否则默认设置会被使用。一旦选择了默认安全策略,你就可以以某种方式创建例外,以提供或阻止服务。选择的关键实际上是在于你是否愿意向全世界提供服务(默认接受),还是只向少数人提供服务(默认拒绝)。

例如,公司的政策可能要求公司内部网服务器只能从公司内部访问。如果是这样,可以采用默认拒绝策略,并明确列出可以访问该服务器的人员。或者,如果你有一个公共网站,但希望阻止某些互联网部分出于某种原因访问它,则可以采用默认接受策略。

我总是推荐默认拒绝策略。然而,如果你没有做出选择,那么你就选择了默认接受。

选择一个默认策略并不意味着这个默认策略必须毫无例外地执行。我的公共 web 服务器采取默认拒绝安全策略,但我特意允许全球访问这些网站。除非连接请求来自几个指定的 IP 地址之一,否则机器会拒绝连接其他程序的尝试。这是一个完全可以接受的默认拒绝策略。

不同的安全工具以不同的方式实现这些策略。例如,使用 TCP 包装器时,应用的是第一个匹配规则。如果你的最后一条规则拒绝所有内容,那么你就建立了一个策略,表示:“除非我在之前的规则中明确允许此流量,否则阻止它。”另一方面,使用 PF 包过滤器时,应用的是最后一个匹配规则。如果你的第一条规则说:“阻止所有流量”,那么你就实施了一个策略,表示:“除非我在后面的规则中特别允许此流量,否则阻止它。”

默认接受和默认拒绝都会让系统管理员感到烦恼。如果你采取默认接受策略,你将不断花时间修补漏洞。如果选择默认拒绝策略,你将花时间为别人打开访问权限。你会对任何选择不断道歉。如果选择默认拒绝,你可能会说:“我刚刚为你激活了服务,非常抱歉给你带来不便。”如果选择默认接受,你可能会说:“……这就是入侵者能够访问我们的内部财务数据库并导致我们损失数百万美元的原因。”在后一种情况下,“我为不便向你道歉”真的是不够的。

TCP 包装器

记住,在第七章中我们提到过,网络连接会建立到各种监听连接请求的程序上。当一个程序内置 TCP 包装器支持时,程序会根据包装器配置检查传入的请求。如果包装器配置指示拒绝连接,程序会立即丢弃该请求。尽管名称中有 TCP,TCP 包装器也可以与 UDP 连接一起使用。包装器是一个长期存在的 Unix 标准,已经被纳入 FreeBSD。个别程序可能支持包装器,也可能不支持;虽然 FreeBSD 基础系统中的几乎所有程序都支持,某些第三方软件则不支持。

TCP 包装器实现为一个共享库,称为libwrap。正如在第十七章中所看到的,共享库是可以在程序之间共享的小块代码。任何与 libwrap 链接的程序都可以使用 TCP 包装器功能。

包装器最常见的保护对象是 inetd(8),它是一个超级服务器,处理较小程序的网络请求。我们将在第二十章中讨论 inetd。虽然我们的示例涉及 inetd(8),但你可以用完全相同的方式保护任何其他支持包装器的程序。虽然包装器有助于保护 inetd(8),但要确保 inetd(8) 不提供任何不必要的服务,正如你对待主系统一样。

配置包装器

封装器按顺序检查每个传入连接请求与/etc/hosts.allow中的规则进行匹配。第一个匹配的规则会被应用,并且立即停止处理。这使得规则的顺序变得非常重要。每条规则都在单独的一行上,由三部分组成,三部分通过冒号分隔:守护进程名称、客户端列表和选项列表。下面是一个示例规则:

ftpd : all : deny

守护进程名称是ftpd;客户端列表是all,意味着所有主机;选项是deny,指示封装器拒绝所有连接。除非先前的规则明确授权访问,否则没有人能够连接到该主机上的 FTP 服务器。

在早期的示例中,我只提到两个选项:acceptdeny。它们分别允许和拒绝连接。稍后我们将讨论其他选项。

守护进程名称

守护进程名称是程序在命令行中出现的名称。例如,inetd(8)在接收到 FTP 请求时启动 ftpd(8)程序。Apache web 服务器启动一个名为httpd的程序,所以如果你的 Apache 版本支持封装器,请将守护进程名称指定为httpd。(注意,Apache 并不是从 inetd 启动的,但它仍然可以支持封装器。)一个特殊的守护进程名称ALL匹配所有支持封装器的守护进程。

如果你的系统有多个 IP 地址,你可以在守护进程名称中指定每个守护进程监听的 IP 地址的不同封装器规则:

ftpd@203.0.113.1 : ALL : deny
ftpd@203.0.113.2 : ALL : accept

在这个示例中,我们有两个守护进程名称,ftpd@203.0.113.1ftpd@203.0.113.2。每个都有一个单独的 TCP 封装器规则。

客户端列表

客户端列表是一个由特定 IP 地址、网络地址块、主机名、域名和关键字组成的列表,用空格分隔。主机名和 IP 地址很简单:只需要列出它们。

ALL : netmanager.absolutefreebsd.com 203.0.113.5 : allow

使用此规则放在/etc/hosts.allow文件顶部,封装器允许我的网络管理机和任何 IP 地址为 203.0.113.5 的主机连接到该主机上的任何服务。(请注意,我可以通过其他方式阻止此访问。)

在客户端列表中指定网络号时,IP 地址和子网掩码之间用斜杠分隔,如第七章中所述。例如,如果脚本小子从一堆以 192.0.2 开头的地址攻击你的服务器,你可以通过以下方式来阻止它们:

ALL : 192.0.2.0/255.255.255.0 : deny

你也可以通过在客户端列表中使用域名,并在前面加一个点来实现。通过反向 DNS 实现此功能,这意味着任何控制一个地址块的 DNS 服务器的人都可以绕过此限制。

ALL : .mycompany.com : allow

如果你有一个长的客户端列表,你甚至可以将它们列在一个文件中,并将文件的完整路径放入/etc/hosts.allow文件中的客户端空间。我曾经在一个拥有大量分布广泛主机的网络上工作,比如一个 ISP 或公司网络环境,在全球各地有网络管理工作站。每个工作站共享与其他工作站相同的封装器规则,并在hosts.allow中出现多行。通过维护一个包含工作站列表的文件,我可以集中管理所有的变更。

除了专门列出客户端地址和名称外,包装器还提供了几个特殊的客户端关键字,用于将一组客户端添加到你的列表中。表 19-1 显示了这些关键字及其用法。

表 19-1 中列出的多数客户端关键字需要一个正常工作的 DNS 服务器。如果你使用这些关键字,你必须拥有一个非常可靠的 DNS 服务,并且必须记住 DNS 和其他程序之间的重要联系。如果你的 DNS 服务器失败,使用包装器和这些关键字的守护进程将无法识别任何主机。这意味着所有的连接都会匹配 UNKNOWN 规则,通常会拒绝连接。此外,客户端端的 DNS 故障可能会拒绝远程用户访问你的服务器,因为你的 DNS 服务器无法从客户端的服务器获取正确的信息。最后,如果你广泛使用基于 DNS 的包装,入侵者只需要过载你的名称服务器或以其他方式干扰你的名称服务器,就能对你的网络发起非常有效的拒绝服务攻击。

表 19-1: TCP 包装器关键字

关键字 用法
ALL 该规则匹配所有可能的主机。
LOCAL 该规则匹配每个主机名中不包含点的机器。通常,这意味着局域网内的机器。位于世界另一端、恰好共享你域名的机器也会被视为“本地”主机。
UNKNOWN 该规则匹配无法识别主机名或用户名的机器。一般来说,任何建立 IP 连接的主机都有已知的 IP 地址。然而,追踪主机名需要 DNS,追踪用户名需要 identd(8)。使用此选项时要非常小心,因为临时的 DNS 问题可能导致即使是本地主机名也无法解析,而且大多数主机默认不运行 identd(8)。你不希望因为你的名称服务器配置错误而导致服务无法使用——尤其是当那台机器就是你的名称服务器时!
KNOWN 该规则匹配任何具有可确定主机名和 IP 地址的主机。使用此规则时要非常小心,因为 DNS 中断可能会影响服务。
PARANOID 该规则匹配任何主机,其名称与其 IP 地址不匹配。你可能会收到来自 IP 地址为 192.168.84.3 的主机的连接请求,该主机声称自己叫做mail.michaelwlucas.com。包装器会反过来检查mail.michaelwlucas.com的 IP 地址。如果包装器得到不同的 IP 地址,该主机就符合此规则。没有时间维护 DNS 的系统管理员最有可能拥有未修补的、不安全的系统。

TCP 包装器提供了其他一些关键字,但它们没有这些关键字那么有用或安全。例如,可以基于远程机器上的用户名来允许连接。然而,你不应该依赖远程机器上的客户端用户名。例如,如果我设置包装器,只允许用户名为mwlucas的人连接到我的本地系统,某人可以轻松地在他们的 FreeBSD 系统中添加一个该名字的帐户,并直接访问。此外,这还依赖于之前提到的罕用的 identd(1)协议。你可以在 hosts_access(5)中找到一些其他类似有用的模糊关键字。

ALL 和 ALL EXCEPT 关键字

守护进程名称和客户端列表都可以使用ALLALL EXCEPT关键字。ALL关键字匹配所有内容。例如,默认的hosts.allow文件以一条允许所有连接的规则开始,这条规则允许从任何位置到任何守护进程的连接:

ALL : ALL : accept

这适用于所有程序和所有客户端。你可以通过为客户端列表或守护进程列表指定特定名称来限制这一点。

ALL : 203.0.113.87 : deny

在这个例子中,我们拒绝来自主机 203.0.113.87 的所有连接。

完全阻止对所有主机的访问并不是一个好主意,但请记住,TCP 包装器会按顺序执行规则,并在找到第一个匹配规则时停止检查。ALL关键字使你可以轻松设置一个默认立场。考虑以下规则集:

ALL : 192.168.8.3 192.168.8.4 : accept
ftpd : ALL : accept
ALL : ALL : deny

我们的工作站 192.168.8.3 和 192.168.8.4(可能是系统管理员的工作站)可以访问任何他们想访问的内容。世界上的任何人都可以访问 FTP 服务器。最后,我们拒绝所有其他连接。这是一个有用的默认拒绝立场。

使用ALL EXCEPT关键字来压缩规则。ALL EXCEPT允许你通过排除的方式列出主机;没有列出的主机将匹配。在这里,我们用ALL EXCEPT写出相同的规则:

ALL : 192.168.8.3 192.168.8.4 : accept
ALL EXCEPT ftpd : ALL : deny

当然,这条规则依赖于拥有一个默认的接受策略,该策略允许之后的 FTP 连接。

有些人认为用ALL编写规则更清晰,而另一些人则更喜欢ALL EXCEPT。需要记住的重要一点是,第一次匹配的规则会结束检查,因此在使用ALL时要小心。

允许来自本地主机的任何连接是一个好主意;你可能会发现有些程序在无法与本机通信时会出错。在你的hosts.allow文件中尽早放入这样的规则:

ALL : localhost : allow
选项

我们已经看到了两种选项:allowdenyallow允许连接,而deny则阻止连接。默认的hosts.allow文件中的第一条规则适用于所有守护进程和客户端,它匹配并允许所有可能的连接。如果你想保护你的服务,这条规则不能排在hosts.allow中的第一位,但它是默认接受安全立场下的一个好最终规则。类似地,ALL:ALL:deny规则是在默认拒绝安全立场下的一个好最终规则。然而,TCP 包装器除了简单的allowdeny之外,还支持其他选项,给你提供了很大的灵活性。

长规则

如果你使用了很多选项,包装规则可能会变得非常长。为了帮助保持规则的可读性,hosts.allow 文件可以使用反斜杠 (\) 后跟回车符作为行续字符。

记录

一旦你决定接受或拒绝连接尝试,你还可以记录该连接。假设你想允许但特别记录来自竞争对手的所有传入请求。同样,你可能想知道因为 DNS 问题而被拒绝的服务器连接数量,当使用 PARANOID 客户端关键字时。记录很有用,更多的记录更好。磁盘空间比你的时间便宜。

severity 选项将消息发送到系统日志,即 syslogd(8)。你可以配置 syslogd 将这些消息定向到一个任意文件,具体取决于你选择的 syslogd 设施和级别(见 第二十一章)。

sshd : ALL : severity local0.info : allow

这个示例允许所有 SSH 连接,但也通过 local0 设施记录它们。

扭转

twist 选项允许你在有人尝试连接到包装的 TCP 守护进程时执行任意的 shell 命令和脚本,并将输出返回给远程用户。twist 选项仅适用于 TCP 连接。(记住,UDP 是无连接的;没有连接可以返回响应,因此你必须通过非常复杂和麻烦的步骤才能使 twist 在 UDP 上工作。此外,许多通过 UDP 传输的协议通常不期望这样的响应,也不具备接收或解释它的能力。在 UDP 上使用 twist 并不值得麻烦。)twist 选项将 shell 命令作为参数,并充当拒绝加执行此操作的规则。你必须了解基本的 shell 脚本才能使用 twisttwist 的复杂用法是可能的,但我们将坚持使用简单的用法。

twist 选项对于默认拒绝策略中的最后一条规则很有用。使用 twist 向尝试连接的人返回如下答案:

ALL : ALL : twist /bin/echo "You cannot use this service."

如果你只想拒绝某个特定主机的特定服务,可以使用更具体的守护进程和客户端列表与 twist 配合使用:

sendmail : .spammer.com : twist /bin/echo \
    "You cannot use this service, spam-boy."

这对抗垃圾邮件并不有效,但可能会让你感觉更好。不过,遇到粗鲁信息的合法客户可能会引发会议。

如果你感觉友好,可以告诉人们你拒绝他们连接的原因。以下 twist 会拒绝所有主机名与其 IP 地址不匹配的连接,并告诉他们原因:

ALL : PARANOID : twist /bin/echo \
    "Your DNS is broken. When you fix it, try again."

使用 twist 会保持网络连接开启,直到 shell 命令完成。如果你的命令执行时间较长,你可能会发现你保持了比预期更多的连接,这可能会影响系统性能。脚本小白可以利用 twist 使你的系统过载,进行一个非常简单的拒绝服务攻击。确保 twist 简单且能快速完成。

生成

twist 类似,spawn 选项会拒绝连接并运行指定的 shell 命令。与 twist 不同,spawn 不会将结果返回给客户端。当您希望 FreeBSD 系统在连接请求时采取某种行动,但又不希望客户端知道时,使用 spawn。生成的命令在后台运行。以下示例允许连接,但将客户端的 IP 地址记录到文件中:

ALL : PARANOID : spawn (/bin/echo %a >> /var/log/misconfigured ) \
    : allow

等一下——%a 是从哪里来的?TCP wrapper 支持多个变量,用于 twistspawn 命令中,因此您可以轻松自定义响应。这个特定的变量 %a 代表 客户端地址。在命令运行之前,它会展开为客户端的 IP 地址,出现在 shell 命令中。表 19-2 列出了其他变量。

表 19-2: twistspawn 脚本的变量

变量 描述
%a 客户端地址。
%A 服务器的 IP 地址。
%c 所有可用的客户端信息。
%d 连接的守护进程名称。
%h 客户端的主机名,如果没有主机名,则是 IP 地址。
%H 服务器主机名,如果没有主机名,则为 IP 地址。
%n 客户端的主机名,如果找不到主机名,则返回 UNKNOWN。如果主机名和 IP 地址不匹配,则返回 PARANOID
%N 服务器的主机名,如果找不到主机名,则返回 UNKNOWNPARANOID
%p 守护进程的进程 ID。
%s 所有可用的服务器信息。
%u 客户端的用户名。
%% 单个 % 字符。

在 shell 脚本中,您可以在任何需要表示它们所代表信息的地方使用这些变量。例如,当有人连接到包装程序时,您可以使用以下命令将所有可用的客户端信息记录到文件中:

ALL : ALL : spawn (/bin/echo %c >> /var/log/clients) : allow

空格和反斜杠是 shell 命令中的非法字符,可能会导致问题。虽然在正常情况下,主机名中不会出现这两个字符,但互联网几乎可以说是与常规不同的。TCP wrapper 会将任何可能干扰命令 shell 的字符替换为下划线 (_)。请检查您的日志中的下划线;它们可能表示潜在的入侵尝试,或者只是某些不懂的人在操作。

包装封装器总结

让我们将本节中给出的所有示例结合起来,构建一个完整的 /etc/hosts.allow 文件来保护一个假设的网络系统。我们首先必须盘点这个系统提供的网络资源、我们网络中的 IP 地址,以及我们希望允许连接的远程系统。

尽管这些要求相当复杂,但它们归结为一个非常简单的规则集:

#reject all connections from hosts with invalid DNS and from our competitor
ALL : PARANOID 198.51.100.0/24 : deny
#localhost can talk to itself
ALL : localhost : allow
#our local network may access portmap, but no others
portmap : ALL EXCEPT 203.0.113.0/24 : allow
#allow SSH, pop3, and ftp, deny everything else
sshd, POP3, ftpd : ALL : allow
ALL : ALL : deny

您可以在 /etc/hosts.allow 或 hosts_allow(5) 和 hosts_access(5) 中找到更多注释掉的示例。

数据包过滤

如果你需要控制不支持 TCP 包装器的网络程序的访问,或者当你的需求超出了包装器所能提供的功能时,可以使用 FreeBSD 的内核级包过滤工具。如果你需要包过滤器,最好完全用包过滤替代 TCP 包装器的实现。在同一台机器上同时使用这两种工具只会让你更加困惑。

包过滤器会将每个进入系统的网络包与规则列表进行比较。当某个规则与包匹配时,内核会根据该规则进行操作。规则可以指示系统允许、丢弃或修改包。然而,你不能使用 TCP 包装器提供的那些巧妙选项;连接会在网络层被切断,客户端甚至没有机会到达应用层,就被拒绝了,而不是返回一个相对友好的拒绝信息。

尽管包过滤的概念足够简单,但你第一次实现时将面临一场彻底的噩梦——呃,我是说,一场“宝贵的学习经验”。准备好花几个小时进行实验,并且不要因为失败而灰心丧气。根据我的经验,困扰包过滤的往往是对基本 TCP/IP 知识的无知,而不是包过滤器本身。试图在不了解网络的情况下进行网络流量过滤是令人沮丧且毫无意义的。要真正理解 TCP/IP,唯一的方法是进行实际的工作。再去学习第七章。如果那还不够,就深入阅读那里的推荐书籍。

FreeBSD 面临着大量的包过滤器:IPFW、IP Filter 和 PF。

IPFW 是 FreeBSD 最早的包过滤软件。它与 FreeBSD 紧密集成;事实上,名为 /etc/rc.firewall/etc/rc.firewall6 的文件完全是为 IPFW 准备的。虽然非常强大,并且在经验丰富的 FreeBSD 管理员中非常流行,但对于初学者来说,它有点难度。

第二个包过滤器,IP Filter,并不是 FreeBSD 特有的防火墙程序,而是在多个类 Unix 操作系统上都有支持。它主要是由一个人——达伦·里德(Darren Reed)——的巨大努力完成的,他开发了绝大部分代码并将其移植到所有这些操作系统上。如果你希望在多个操作系统之间共享一个防火墙配置,IP Filter 最为有用。

我们将重点讨论富有创意命名的PF,即包过滤器。PF 起源于 OpenBSD,旨在提供丰富的功能、灵活性并且易于使用。普通的 FreeBSD 管理员可以使用 PF 实现几乎所有使用其他两个包过滤器可以达到的效果。

注意

如果你想深入讨论 PF,可以参考彼得·N·M·汉斯廷(Peter N. M. Hansteen)的《PF 之书》(The Book of PF)(No Starch Press, 2014),或者我自己的书《绝对 OpenBSD》(Absolute OpenBSD)(No Starch Press, 2013),其中有几章专门讲解 PF。你还可以查看在线的 PF FAQ,但那里的俳句要少一些。

启用 PF

PF 包括数据包过滤内核模块 pf.ko 和用户空间程序 pfctl(8)。在使用 PF 之前,你必须加载内核模块。最简单的方法是通过 rc.conf 启用 PF:

pf_enable="YES"

PF 默认采用接受所有策略,这意味着仅仅启用防火墙并不会让你把自己锁定在服务器之外。

默认接受和默认拒绝在数据包过滤中的应用

安全策略(默认接受和默认拒绝)在数据包过滤中至关重要。如果你使用默认接受策略并希望保护你的系统或网络,你需要许多规则来阻止每一种可能的攻击。如果你使用默认拒绝策略,你必须为你提供的每个小服务显式地开放端口。在几乎所有情况下,默认拒绝是更可取的;虽然它可能更难管理,但它带来的更高安全性完全弥补了这种困难。

使用默认拒绝策略时,很容易将自己锁定在无法远程访问计算机的状态。当你通过 SSH 连接到远程计算机并不小心打破允许 SSH 访问的规则时,你就会遇到麻烦。每个人至少都会发生一次这种情况,所以当它发生在你身上时,不要太尴尬。关键是,最好不要在远程计算机上学习数据包过滤;从你可以控制台访问的计算机开始,这样你可以轻松恢复。我自己多次切断了自己的访问,通常是因为在解决与数据包过滤无关的问题时没有理清思路。如果没有远程控制台或 IPMI,唯一的解决办法就是在我爬上车、开车去远程地点时踢自己一脚,并在修复问题时向我给他人带来不便的人道歉。幸运的是,随着我年龄的增长,这种情况越来越少发生。^(1)

尽管如此,在几乎所有情况下,默认拒绝策略是正确的。作为一个新管理员,你唯一合理的学习数据包过滤的方式是确保你可以方便地访问系统控制台。如果你对自己的配置不完全有信心,除非你有远程控制台和电源访问、一个合格的本地管理员或者一个串行控制台,否则不要在全国范围内设置数据包过滤系统。

基础数据包过滤与有状态检查

回想一下第七章,TCP 连接可以处于多种状态,如打开、开启、关闭等。例如,每个连接在客户端发送 SYN 数据包请求连接同步时都会开启。如果服务器在请求的端口上监听,它会回复 SYN-ACK,表示:“我已收到你的请求,这是我们的连接的基本信息。”客户端用 ACK 数据包确认收到信息,表示:“我已确认收到连接信息。”三次握手的每个部分都必须完成,才能建立连接。你的数据包过滤规则集必须允许三次握手的所有部分,以及实际的数据传输。如果你的数据包过滤器规则不允许传输 SYN-ACK,允许服务器接收传入连接请求就毫无意义。

在 1990 年代初,数据包过滤器会单独检查每个数据包。如果数据包匹配规则,它将被允许通过。系统不会记录之前已经通过的内容,也不知道某个数据包是否属于合法的事务。例如,如果一个标记为 SYN-ACK 的数据包到达并且其目标地址在数据包过滤器内,数据包过滤器通常会认为该数据包必须是对之前已批准的数据包的响应。这样的数据包必须被批准,以完成三次握手。因此,入侵者伪造 SYN-ACK 数据包并利用它们绕过看似安全的设备。由于数据包过滤器不知道谁曾发送过 SYN 数据包,它无法拒绝不合法的 SYN-ACK 数据包。一旦入侵者将数据包带入网络,他通常可以从一个随机设备触发响应,开始渗透。

现代数据包过滤器使用有状态检查来应对这个问题。有状态检查意味着跟踪每个连接及其当前状态。如果一个传入的 SYN-ACK 数据包似乎是正在进行的连接的一部分,但没有人发送相应的 SYN 请求,则该数据包会被拒绝。虽然这使内核变得更加复杂,但编写有状态检查数据包过滤规则比编写传统的规则要容易。数据包过滤器必须跟踪许多可能的状态,因此这比看起来要难编程,尤其是当你考虑到数据包碎片、反欺骗等问题时。

PF 默认执行有状态检查。你无需在规则中指定它。

如果您已经开始想,“嘿,数据包过滤听起来像是防火墙”,您是对的,有一定道理。防火墙这个词被应用于各种网络保护设备。 其中一些设备非常复杂;一些设备甚至在智力竞赛中败给了水泥块。如今,防火墙这个词不过是一个营销术语,几乎没有实际意义。防火墙这个词就像汽车这个词:你是指一辆生锈的 1972 年款 Gremlin,配有 6 马力的发动机,排气系统的废气足以突破《京都议定书》,还是一辆闪亮的特斯拉 Roadster,配有 500 马力的发动机、华丽的三色涂装以及“末日音响系统”?两者各有其用途,但显然其中一款是为性能而设计的。虽然“Gremlin”式的防火墙可能也有其用武之地,但最好还是选择您能负担得起的最好的设备。

话虽如此,FreeBSD 可以打造一个坚固的防火墙,完全满足您的需求。数据包过滤只是开始。软件包集合包含各种应用代理,可以让您的 FreeBSD 系统与 Checkpoint 或 PIX 抗衡,并在顶尖表现中脱颖而出——且费用远低于数万美元。

配置 PF

配置 PF 在/etc/pf.conf文件中。这个文件包含了不同格式的声明和规则,具体格式取决于它们配置的功能。不仅规则的顺序至关重要,功能配置的顺序也同样关键。例如,如果您在重新组装分片数据包之前尝试进行状态检查,连接将无法正常工作。

默认的/etc/pf.conf包含了正确顺序的示例规则,但如果您有任何混淆的危险,建议您在各个部分之间加上大号注释标记,必要时使用大写字母。 (使用井号来注释pf.conf。)功能必须按此准确顺序输入:

  1. 表格

  2. 选项

  3. 数据包规范化

  4. 带宽管理

  5. 翻译

  6. 重定向

  7. 数据包过滤

是的,PF 不仅仅是用来过滤数据包。它是一个通用的 TCP/IP 处理工具。我们不会在这里介绍它的所有功能;去阅读 Peter 的书吧。

宏允许您定义变量,以便更轻松地编写和阅读规则。例如,下面是定义您的网络接口和 IP 地址的宏:

interface="em0"
serveraddr="203.0.113.2"

在您的规则中,您可能会将网络接口描述为$interface,将服务器的 IP 地址描述为$serveraddr。这意味着,如果您重新编号服务器或更换网络卡,只需在pf.conf中进行一次更改,您的规则就会完全更新。

有时您希望某个规则引用“当前在此接口上的所有 IP 地址”。您不关心流量到达的是哪个地址,只希望接受或拒绝到该接口的流量。PF 为此提供了简写方式。将接口名称括在括号中,稍后我们将看到。 (您也可以不使用括号直接使用接口名称,但这样 PF 就无法察觉到自上次重新加载或重启以来 IP 的变化。)

表格和选项

PF 可以通过表格存储大量地址。这比我们将要使用的 PF 更加复杂,但你应该知道这一功能的存在。

类似地,PF 有多种选项来控制网络连接的时间、表格大小和其他内部设置。默认设置通常足够满足正常(以及大多数不正常)使用。

数据包规范化

TCP/IP 数据包在传输过程中可能被拆分,处理这些数据碎片会增加系统负载,以及服务器必须做的工作量,无论是为了服务请求,还是过滤数据包。系统必须在将数据包交给客户端软件之前重新组装这些碎片,同时决定如何处理任何其他随机的垃圾数据。PF 将这种重新组装称为 清理。例如,要重新组装所有通过网络接口进入的碎片,丢弃所有可能不合法的过小碎片,并合理地清理传入的数据流,使用以下规则:

scrub in

这会影响进入计算机的所有数据包。

虽然清理看起来像是“锦上添花”,但它实际上相当重要,因为 PF 的过滤是基于整个数据包的。碎片要过滤起来要复杂得多,并且需要特殊处理,除非它们被重新组装。没有清理流量会导致连接问题。

带宽、转换和重定向

PF 包括其他对于防火墙至关重要的功能,并执行通常与网络设备相关的其他功能。通过排队,PF 可以控制主机在每个 IP 或甚至每个端口的基础上传输的流量。PF 包含一大堆功能来支持网络地址转换(NAT)和端口重定向,这两个是防火墙的关键功能。它的支持超出了许多商业产品的范畴。

所有这些内容足以填满另一本书。字面意思。Peter Hansteen 写了 PF 手册。去读一下,然后搭建一个防火墙。每个系统管理员至少应该用裸操作系统搭建一次防火墙。即使最终选择使用商业产品、嵌入式设备,或像 pfSense 或 OPNsense 这样的产品,你也会学到很多东西。^(2)

小型服务器 PF 规则示例

这里有一组用于保护小型互联网服务器的 PF 规则样本。从这里开始,编辑它以匹配你服务器的需求。

➊ ext_if="em1"
➋ set skip on lo0
➌ scrub in

➍ block in
➎ pass out

➏ pass in on $ext_if proto tcp from any to ($ext_if) port {22, 53, 80, 443}
➐ pass in on $ext_if proto udp to ($ext_if) port 53
➑ pass in on $ext_if inet proto icmp to ($ext_if) icmp-type { unreach, redir,
       timex, echoreq }

我们首先定义一个宏来指定我们的接口名称 ➊,这样如果更换网络卡时,我们就不需要重写所有规则。

第二行指示 PF 不对 lo0 接口 ➋ 进行过滤。回环接口是本地机器的,只有本地机器能够通过它进行通信。

然后,我们清理传入流量 ➌,将数据包重新组装成一个连贯的整体,并丢弃那些无法重新组装的数据包。

现在我们有了一个合理的传入数据流,我们可以对其进行过滤。此策略首先通过阻止所有传入流量 ➍,设置默认拒绝策略。没有明确允许的任何东西都被禁止。

出站流量有默认允许策略 ➎。

本策略中的最后三条规则处理 TCP、UDP 和 ICMP。它们有类似的格式,我们稍后会详细分析。

首先,我们允许 TCP 流量访问端口 22、53、80 和 443 ➏。

接下来,我们允许 UDP 流量访问端口 53 ➐。如果该主机提供的服务多于 DNS,那么我们会有更长的端口列表。

最后一条规则允许至关重要的 ICMP 流量访问我们的主机,并允许该主机响应 ➑。

让我们更仔细地看看 TCP 规则。

➊pass in➋on $ext_if➌proto tcp➍from any➎to ($ext_if)➏port {22, 53, 80, 443}

该主机对入站流量有默认拒绝策略,因此通过pass in语句 ➊,我们对该策略做出了一个例外。

规则的下一部分指定了该规则适用的接口 ➋。该规则适用于由宏$ext_if定义的接口,或者是 em1。

然后,我们指定一个协议 ➌。这个规则适用于 TCP 连接。

你可以编写仅适用于特定源或目标地址的 PF 规则。这个规则适用于来自任何主机的流量 ➍。如果你允许任何源地址,可以省略规则中的这一部分。

然后,我们指定一个目标地址 ➎。目标是括号中的接口名称,这意味着“该接口上的任何 IP 地址”。

最后,定义该规则适用的端口 ➏。大括号允许你将多个实体组合在一起。该过滤器允许连接到端口 22(ssh)、53(DNS)、80(HTTP)和 443(HTTPS)。你也可以通过端口名指定端口(来自/etc/services),但我发现数字更可靠。编辑/etc/services不应该破坏你的防火墙!在该主机上部署新的 TCP 服务只需要将端口添加到列表中并重新加载防火墙规则。

UDP 规则略有不同。

pass in on $ext_if➊proto udp to ($ext_if)➋port 53

最明显的变化是定义 UDP 协议而不是 TCP ➊。一个不那么明显的变化是该规则省略了源地址。它适用于来自任何地址的数据包。这个数据包过滤器只允许一个端口,53 ➋。带有单一端口的规则不需要大括号。

ICMP 规则看起来有点复杂,但其实它和其他规则是一样的。

pass in on $ext_if inet➊proto icmp to ($ext_if)➋icmp-type➌{ unreach, redir, timex echoreq }

指定此规则适用于 ICMP 是直接的 ➊。这个规则也没有列出源地址,因此适用于来自任何地方的流量。

在 TCP 和 UDP 规则中指定目标端口的地方,ICMP 规则列出了icmp-type ➋。ICMP 没有端口,但它有不同类型的流量。对于我们的目的,ICMP 类型类似于端口。类型有数字代码,但名称更容易记。

该规则指定了四种不同类型的 ICMP 流量 ➌。

总的来说,这条规则允许通常对互联网正常运行至关重要的 ICMP 流量。你的环境可能需要其他类型的 ICMP。你组织的安全政策可能会指定可以和不能传递哪些 ICMP。但是这四种类型对于面对互联网的服务器来说是一个合理的组合。

这个简单的策略定义了与我们服务器通信的基本规则。虽然它并不完美,但它可以为入侵者设置障碍。那个闯入你的网站服务器并在 10000 端口启动命令提示符的家伙?如果你的防火墙规则不允许该端口的传入连接,那么他们所有的努力都会白费。真是个悲剧。

管理 PF

使用 pfctl(8)管理 PF。如果你的规则没有错误,pfctl(8)将静默运行;只有在出现错误时才会输出结果。你需要测试、启用、查看和删除规则。

测试规则

由于防火墙错误可能会给你带来很大麻烦,最好在启用规则之前先检查它们。虽然规则检查只会解析文件,检查规则本身的语法错误,但如果启用包含语法错误的规则,可能会导致系统无法保护、锁定或两者兼而有之。使用-n标志检查文件中的问题,使用-f指定 PF 规则文件。

# pfctl -nf /etc/pf.conf

如果出现错误,请修复它们并重新尝试。

启用规则

一旦语法检查静默运行,移除-n标志以启用新规则。

# pfctl -f /etc/pf.conf

更改 PF 配置非常迅速。这意味着你可以为不同的时间或情况准备多个 PF 配置。也许你只想在一天中的某些时段允许访问特定服务;你可以安排一个 pfctl(8)运行,在这些时段安装适当的规则。或者,也许你有针对灾难情况的独立规则,并希望在失去互联网连接时安装特殊的规则集。使用 pfctl(8)可以使所有这些配置变得简单。

查看规则

如果你想查看当前正在防火墙上运行的规则,请使用pfctl -sr

# pfctl -sr
scrub in all fragment reassemble
block drop in all
pass in on em1 proto tcp from any to (em1) port = ssh flags S/SA keep state
pass in on em1 proto tcp from any to (em1) port = domain flags S/SA keep state
pass in on em1 proto tcp from any to (em1) port = http flags S/SA keep state
pass in on em1 proto tcp from any to (em1) port = https flags S/SA keep state
pass in on em1 proto udp from any to (em1) port = domain keep state
pass in on em1 inet proto icmp from any to (em1) icmp-type unreach keep state
pass in on em1 inet proto icmp from any to (em1) icmp-type redir keep state
pass in on em1 inet proto icmp from any to (em1) icmp-type timex keep state
pass in on em1 inet proto icmp from any to (em1) icmp-type echoreq keep state
pass out all flags S/SA keep state

你可以按照这里展示的格式编写 PF 规则。

注意,虽然我们在配置文件中指定了多个 TCP 端口,但在数据包过滤器中,每个 TCP 和 UDP 端口都有自己的规则。同样,每种 ICMP 类型也有自己的规则。

删除规则

最后,使用-Fa(刷新所有)标志从运行配置中删除所有规则。(你可以使用除a以外的其他标志来删除防火墙配置的部分内容,但这样可能会使系统处于不一致的状态。)

# pfctl -Fa

你会看到 PF 系统地删除所有规则、NAT 配置以及配置中的其他内容。在加载新配置之前,不要手动清除配置;只需加载新规则文件即可删除旧规则。

PF 非常强大,灵活性很高,可以几乎以任何你喜欢的方式(甚至一些你不喜欢的方式)滥用 TCP/IP。我们才刚刚触及表面。查看在第 462 页中提到的“数据包过滤”中的一些资源,以深入探索 PF。

Blacklistd(8)

有时你可能需要比简单的允许或拒绝更深思熟虑的数据包过滤。我经常将 SSH 服务器开放给公网,这样我就可以从任何地方登录。不过,我确实有点不满那些认为我会允许没有密码登录的僵尸网络。这个时候,Blacklistd(8)就派上用场了。

Blacklistd 允许一个守护进程报告:“嘿,这个 IP 地址在骚扰我。”一旦 Blacklistd 收到足够多关于某个地址的投诉,它就会通知防火墙阻止该地址。那些不断攻击你 SSH 服务器的机器人?它们已经历史。

这种黑名单策略对于像 Hail Mary Cloud 这样的分布式僵尸网络只有有限的作用,但即便如此,你仍然可以配置敏感度,屏蔽掉最烦人的客户端。一切取决于每个僵尸网络成员的侵扰程度。

要使用 Blacklistd,你必须设置数据包过滤器以接受来自 Blacklistd 的输入,设置每个服务的容忍级别,并配置服务以使用 Blacklistd。

PF 与 Blacklistd

PF 通过锚点处理动态规则。你可以使用 pfctl(8)编辑活动锚点,让你在策略中的特定位置插入规则。在你的blockpass语句之前添加 Blacklistd 锚点。按照上一节的策略,你的规则将如下所示:

--snip--
anchor "blacklistd/*" in on $ext_if
block in
pass out
--snip--

你必须在锚点名称周围包含引号,并且必须指定接口。

数据包过滤器现在已经准备好进行动态黑名单处理。

配置 Blacklistd

Blacklistd 从/etc/blacklistd.conf获取其配置。虽然它的大部分配置都放在这个文件中,但你也可以通过命令行选项修改服务的行为。

从启用/etc/rc.conf中的 Blacklistd 开始。

# sysrc blacklistd_enable=YES
blacklistd_enable: NO -> YES

守护进程不会启动,直到你重新启动或手动启动它,所以你现在可以进行配置。

/etc/blacklistd.conf

Blacklistd 规则每个只支持单个服务、端口或地址组。将你的规则放入/etc/blacklistd.conf,每行一个规则。Blacklistd 规则分为两组,分别是本地和远程。

本地的 Blacklistd 规则适用于运行 Blacklistd 的机器上的项目。这里是设置本地 SSH 服务、端口 99 或任何本地内容的地方。本地规则部分以[local]开头。

远程的 Blacklistd 规则适用于不在本地机器上的项目。这里,你可以定义规则,比如“这个块减少容忍度”或“禁用这些地址较短时间”或“永远不阻止这些地址”。远程规则部分以[remote]开头。我们将首先讨论本地规则,然后再讨论远程规则支持的附加内容。

下面是一个blacklistd.conf条目的示例:

[local]
ssh             stream  *       *               *       3       24h

第一行是一个[local]语句。此后出现的每个规则都适用于本地机器,直到遇到[remote]条目为止。

每个规则有七个字段。前四个字段识别要列入黑名单的流量,而后三个字段定义黑名单行为。星号(*)是通配符,表示任何内容都匹配这个字段。

第一个字段是位置。对于本地规则,它提供了适用的网络端口。像sshftp这样的条目可能会让人产生误解。它们并不适用于名为sshdftpd的程序,而是适用于/etc/services中列出的网络端口。虽然在本地规则中你可以列出特定的 IP 地址和端口,但 blacklistd 会忽略该地址。只有端口才会生效。示例规则阻止了ssh,即端口 22。

第二个字段提供了套接字类型。TCP 套接字使用stream类型,而 UDP 套接字需要dgram。目前,所有支持 blacklistd 的服务都使用 TCP。你可以放心地在这里使用星号,表示“任何套接字类型”。我们的示例规则使用的是 stream,因此它适用于 TCP 连接。

第三个字段定义了协议。支持的选项包括tcpudptcp6udp6numeric,或者你可以使用通配符表示“任何协议”。不使用通配符的唯一原因是,如果你只想匹配某一版本的 IP,例如为 TCP over IPv4 与 TCP over IPv6 使用不同的黑名单设置。

第四个字段给出了抱怨流量的守护进程的所有者。这可以是一个通配符、一个用户名或一个 UID。这里通配符是最常见的条目。对于黑名单目的来说,我不关心哪个用户运行在端口 22 上运行的服务器;我关心的是它能被保护不受随机访问。

第五个字段,数据包过滤规则名称,是决定如何阻止的第一个条目。Blacklistd 的默认行为是将所有阻止操作放在名为blacklistd的锚点下,这个锚点我们在上一节的pf.conf中添加过。如果你希望不同的黑名单使用不同的锚点,可以在这个字段中定义锚点名称;否则,使用通配符作为默认值。

如果你在名称前加上一个连字符(-),意味着“使用默认名称前缀的锚点”。

ssh             stream  *       *               -ssh       3       24h

该条目将任何新的黑名单规则添加到名为blacklistd-ssh的锚点。

在名称字段中使用斜杠(/)和网络掩码的长度告诉 blacklistd 使用前缀表示法来阻止整个子网。

22              stream  tcp       *               */24    3       24h

当网络中的一台主机行为异常时,我们会阻止邻接的整个/24 子网。/24 在 IPv4 和 IPv6 中有非常不同的含义。务必指定该规则适用的协议!

第六列,nfail,设置需要多少次登录失败才能将远程 IP 列入黑名单。这里,通配符表示从不。我们的示例规则设置为 3,这是 OpenSSH 在一次连接中给你的登录尝试次数。

最后一列,禁用,表示要将主机列入黑名单多长时间。默认单位是秒,但你也可以使用mhd分别表示分钟、小时和天。我们的示例规则设置为 24 小时。

所以,设置了这条规则后,SSH 认证失败三次将导致客户端被封锁 24 小时。

一旦你设置了本地规则,就可以配置远程规则。

blacklistd.conf 远程规则

使用远程规则来指定 blacklistd 如何根据远程主机改变其行为。远程规则中的每个字段与本地规则相同,但 blacklistd 使用它们的方式会有所不同。这里是一个示例远程规则:

[remote]
203.0.113.128/25 *      *       *               =/25    =       48h

地址 列是一个 IP 地址(IPv4 或 IPv6),一个端口,或者两者。这允许你为特定的远程地址范围设置特殊规则。我们的示例规则适用于地址范围 203.0.113.128/25。

类型协议所有者列的解释与本地规则相同。

名称 列非常有趣。在远程规则中的等号意味着“使用你匹配的本地规则中的值”。这条规则表示从防火墙规则中获取规则名称条目,并在其后添加网络前缀 /25(一个 255.255.255.128 的子网掩码)。如果来自这个地址范围的连接被列入黑名单,将会影响整个子网。如果你在此处放入一个 PF 锚点名称,blacklistd 会将该地址块的规则添加到命名锚点中。通配符将恢复为默认表。

nfail 列让你为这个地址设置自定义的失败次数。也许你想给那个总是输入错误密码的客户提供额外的尝试机会,允许他失败前 30 次。将此列设置为星号将禁用阻止功能。

禁用 列允许你为这个地址块设置自定义的封锁时间。使用通配符将禁用封锁功能。

远程规则让你对你不喜欢的人施加更严格的限制,同时告诉 blacklistd(8) 永远不要黑名单你的办公室。

你现在可以启动 blacklistd 了。但它不会做任何事情,因为程序不知道它应该向 blacklistd 报告错误。不过,一旦你配置了它们,它就准备好了。

配置 Blacklistd 客户端

FreeBSD 包含了一些支持 blacklistd 的客户端。你最可能使用的两个是 ftpd(8) 和 sshd(8)。

要在你的 SSH 服务器中启用 blacklistd,请将以下行添加到 /etc/ssh/sshd_config 中。

UseBlacklist yes

重启 sshd。

在 ftpd(8) 中启用黑名单功能,可以使用 -B 命令行选项,配置在 /etc/inetd.conf 中或独立进程的 /etc/rc.conf 标志中。

ftpd_flags="-B"

现在这些程序会在有人登录失败时向 blacklistd(8) 报告。

管理 Blacklistd

将那些没有权限访问你服务的烦人客户端加入黑名单,能够减少你需要进行的日志分析量,但你可能会想看到 blacklistd 正在阻止的具体内容。你需要使用 blacklistctl(8)。

blacklistctl(8) 程序只有一个功能:显示 blacklistd 阻止的地址和网络。你总是需要使用 blacklistctl dump 命令。

默认情况下,blacklistctl dump 显示的是在黑名单候选列表中的主机,但尚未被封锁。添加 -b 标志可以查看所有已封锁的主机。

# blacklistctl dump -b
        address/ma:port id      nfail   last access
  203.0.113.128/25:22   OK      6/3     2018/08/28 16:30:09

在这里,我们看到地址范围 203.0.113.128/25 尝试了 3 次登录,但允许的登录尝试次数为 6 次。它是如何做到的?SSH 允许客户端在一个 TCP/IP 连接上尝试多次登录。黑名单不会阻止一个活动连接。上次有问题的主机尝试访问此服务是在last access显示的日期。

你可能会发现剩余时间比最后访问时间更有用。加上-r标志。

# blacklistctl dump -br
        address/ma:port id      nfail   remaining time
  203.0.113.128/25:22   OK      4/3     36s

很快,这个子网将被释放,可以继续骚扰和困扰我的无辜 SSH 服务器。也许我需要增加黑名单的时效。

去黑名单

尽管你尽了最大努力,总有一天你需要在地址自然过期之前从黑名单中移除一个地址。blacklistctl(8)程序并不提供这样的功能:你必须手动从 PF 表中删除该地址。这样做需要理解 blacklistd 如何在 PF 中管理地址。

每个被阻止的端口都有一个子锚点,在 blacklistd 锚点内。这个锚点是以端口命名的。阻止端口 22 的子锚点将被命名为blacklistd/22。在那个子锚点内,你会找到一个包含被阻止地址的表格。该表格的名称是port,后面跟着端口号。无法再连接到端口 22 的主机将出现在一个名为port22的表格中。

在这里,我使用包过滤控制程序 pfctl(8)来检查子锚点 blacklistd/22\中的 port22 表的内容。我不会解释所有内容;只需替换你的表和子锚点名称。(阅读 Hansteen 的《PF 之书》,让锚点将你拖得更深。非常深,非常深。)

# pfctl -a blacklistd/22 -t port22 -T show
--snip--
   203.0.113.128/25
--snip--

是的,我们的问题地址就在其中。移除它需要一个相当深奥的 pfctl(8)命令。

# pfctl -a blacklistd/22 -t port22 -T delete 203.0.113.128/25

然而,黑名单是保存在 PF 之外的一个数据库中,因此被黑名单中的地址仍会出现在 blacklistctl(8)中。该数据库条目最终会无害地过期。如果主机再次不当行为,它将再次被阻止。

公钥加密

许多服务器守护进程依赖公钥加密来确保通信的机密性、完整性和真实性。许多不同的互联网服务也使用公钥加密。你需要对公钥加密有基本的了解,才能运行像安全网站(https)和安全 POP3 邮件(pop3ssl)这样的服务。如果你已经熟悉公钥加密,你大概可以跳过这一部分。如果不熟悉,请为一个高度压缩的入门介绍做好准备。

加密系统使用密钥将消息在可读(明文)和编码(密文)版本之间转换。尽管明文密文这两个词包含了文本一词,但它们并不限于文本;它们还可以包括图形文件、二进制文件以及你可能想要发送的任何其他数据。

所有加密系统有三个主要目的:完整性、保密性和不可否认性。完整性意味着消息没有被篡改。保密性意味着只有预定的受众能够读取消息。不可否认性意味着作者不能在事后声称自己没有写过这条消息。

早期的密码系统依赖于单一密钥,任何拥有密钥的人都可以加密和解密消息。你可能需要做大量工作来转换消息,比如二战期间让盟军头痛的恩尼格玛机,但密钥使得这种转换成为可能。一个典型的例子就是需要密钥或密码的任何代码。流行于间谍小说中的一次性信息密码本就是最终的单密钥密码系统,除非你拥有那把准确的密钥,否则是无法破解的。

与单密钥密码系统不同,公钥(或非对称)加密系统使用两把密钥:一把私钥和一把公钥。消息使用其中一把密钥加密,使用另一把密钥解密,数字签名确保消息在传输过程中未被篡改。解释这一点的数学确实相当复杂,但它确实有效——只需要接受这样一个事实:非常非常大的数字表现得非常非常奇怪。通常,密钥拥有者会将私钥保密,但将公钥发给全世界供任何人使用。密钥拥有者使用私钥,而其他人则使用公钥。密钥拥有者可以加密任何人都能阅读的消息,而世界上任何人都可以发送只有密钥拥有者能够读取的消息。

公钥加密技术满足了我们对完整性、保密性和不可否认性的需求。如果作者希望任何人都能够读取他的消息,并确保消息未被篡改,他可以用自己的私钥加密消息。任何拥有公钥的人(也就是全世界的人)都可以读取这条消息,但一旦篡改,消息将变得无法辨认。(根据实际用途,他可能选择代替签署数字签名来签署消息。)

以这种方式加密消息还确保了消息的作者拥有私钥。如果某人想发送一条只有特定人员才能读取的消息,他可以用该特定受众的公钥来加密消息。只有拥有匹配私钥的人才能读取这条消息。

只要私钥保持私密,这种方法是有效的。一旦私钥被盗、丢失或公开,安全性就会丧失。一个粗心的人如果丢失了自己的私钥,甚至可能会发现别人代替他签署文件。如果你不想发现有人用你的私钥订购了价值五十万美元的高端图形工作站并将其通过快递送到底特律市中心一个废弃房屋的邮件投递点,就要小心保管好你的密钥。^(3)

所有这些操作的标准工具包是 OpenSSL。

为什么选择 OpenSSL?

多年来,OpenSSL 一直是唯一的加密库选择。今天的更新替代方案,尽管可能更加可靠,但并不符合 FreeBSD 的长期支持模型。最明显的替代品,LibreSSL,只对每个版本提供一年的支持。在加密工具包既可靠又能在 FreeBSD 发布周期内持续升级之前,OpenSSL 将不会被替代。

OpenSSL

FreeBSD 包含了用于处理公钥加密的 OpenSSL 工具包。OpenSSL 让你能够执行全方位的加密操作。尽管许多程序使用 OpenSSL 的功能,系统管理员通常不需要直接使用 OpenSSL。

虽然 OpenSSL 开箱即用,但我发现设置一些默认值可以让我的工作更轻松。使用文件 /etc/ssl/openssl.cnf 配置 OpenSSL。该文件中的几乎所有设置默认是正确的,除非你是密码学家,否则不应该更改这些设置。可以更改的少数有用的设置是生成加密签名的默认值。每个默认值都以字符串 _default 标记。你最可能关心的是以下设置,我已经根据自己的需求进行了调整:

➊ countryName_default            = US
➋ stateOrProvinceName_default    = Michigan
➌ 0.organizationName_default     = Burke and Hare Word Mine, LLC

countryName_default ➊ 是你所在国家的两字母代码——在我这里是 USstateOrProvinceName_default ➋ 是你所在州的名称,可以是任何长度。我会设置为 Michigan0.organizationName_default 字段 ➌ 是你的公司名称。如果我要购买签名证书,我会在这里填入希望出现在证书上的内容。如果只是测试程序如何与 SSL 一起工作,并且没有真正的公司名称,我可能会使用我工作的公司名称或我自己编造的名称。

以下值不会出现在 openssl.cnf 中,但如果你设置了它们,它们会作为默认值出现在 OpenSSL 命令提示符中。我发现这些很有用,尽管它们比之前的默认值更新得更频繁——它们至少能提醒我正确的答案格式。

➊ localityName_default            = Detroit
➋ organizationalUnitName_default  = Pen-Monkey Division
➌ commonName_default              = www.michaelwlucas.com
➍ emailAddress_default            = mwlucas@michaelwlucas.com

localityName_default ➊ 是你所在城市的名称。organizationalUnitName_default ➋ 是你公司中该证书所适用的部门。OpenSSL 中最常被误解的值之一是 commonName_default ➌,它是该证书所属机器的主机名,如同反向 DNS 中所显示的那样。记住,反向 DNS 不一定与主机名相同!你的 Web 服务器可能有一个友好的名称,但托管公司可能会在反向 DNS 中为它分配一个完全不同的名称。最后,emailAddress_default ➍ 是站点管理员的电子邮件地址。

这些值会作为默认选择出现在 OpenSSL 命令提示符中。将它们设置在配置文件中可以省去后续的麻烦。

证书

关于公钥加密,有一个有趣的地方是,作者和受众不必是人类。他们可以是程序。安全外壳(SSH)和安全套接字层(SSL)是程序可以在不担心入侵者窃听的情况下通信的两种不同方式。公钥密码学是数字证书的一个重要组成部分,这些数字证书用于安全网站和安全邮件服务。当你打开 Firefox 购买东西时,可能没意识到浏览器正在疯狂地加密和解密网页。这就是为什么你的计算机会抱怨“无效证书”的原因;某人的公钥可能已经过期,或者证书是自签的。今天的协议使用传输层安全(TLS)进行加密和解密,并使用TLS 证书

SSL 与 TLS

你常常听到关于 SSL 的说法,但它通常是不正确的。今天,传输层安全(TLS)大多取代了 SSL。大多数关于SSL的使用是遗留下来的残余。一般来说,面向互联网的网站应该使用 TLS 1.1 或更高版本。TLS 1.0 的保护仅仅是弱保护。任何版本的 SSL 协议所加密的流量都不安全。

许多公司,如 VeriSign,提供公钥签名服务。这些公司被称为证书颁发机构(CAs),因为它们提供TLS 证书。其他需要签署证书的公司提供他们的身份验证证明,如公司文件和商业记录,然后这些公钥签名公司用它们的 CA 证书签署申请人的证书。通过签署证书,CA 表示:“我已检查过此人的证件,他/她/它已证明自己的身份,且我对此表示满意。”然而,它们并不保证其他任何事情。TLS 证书的拥有者可以使用证书运行一个销售欺诈或危险产品的网站,或者用它来加密勒索信。签名的 TLS 证书保证的是某些类型的技术安全,而非个人诚信,甚至也不保证单方面的技术安全。证书并不会神奇地为你应用安全补丁。

网络浏览器和其他使用证书的软件包含主要 CA 的证书。当浏览器收到一个由 CA 签署的证书时,它会将该证书视为合法。基本上,网络浏览器会说:“我信任这个证书颁发机构,而证书颁发机构信任这家公司,因此我也会信任这家公司。”只要你信任该 CA,一切都能正常工作。

ca_root_nss包包含 Mozilla 项目认可的 CA 证书。如果某个软件在验证证书时失败,请确保你已安装该包。

大多数 CA 都是大型商业公司。不过,无论你的组织规模如何,我建议你调查 Let’s Encrypt(www.letsencrypt.org/)。Let’s Encrypt 是一个提供免费、全球有效的 TLS 证书的 CA。

使用未被任何 CA 签名的证书在测试中是完全可以的。它也可能适用于公司内部的应用程序,你可以将证书安装到客户端的网页浏览器中,或者告诉用户信任该证书。我们将讨论这两种方法。

这两种证书使用都需要一个主机密钥。

TLS 主机密钥

无论是签名证书还是自签名证书,都需要主机的私钥。主机密钥只是一个精心生成的随机数。以下命令创建一个 2,048 位的主机密钥并将其放置在 host.key 文件中:

# openssl genrsa 2048 > host.key

你将看到一条声明,表示 OpenSSL 正在创建主机密钥,并且屏幕上会出现点点移动,表示密钥生成过程正在进行。只需几秒钟,你就会得到一个包含密钥的文件。该文件是一个纯文本文件,包含 BEGIN RSA PRIVATE KEY 和一堆随机字符。

保护你的主机密钥!确保它由 root 拥有并且仅 root 可读。一旦将证书投入生产环境,任何拥有该密钥的人都可以用它来窃听你的私人通信。

# chown root host.key
# chmod 400 host.key

将这个主机密钥放置在与我们为密钥文件本身设置的权限相同的目录中。

创建证书请求

你需要一个证书请求,无论是签名证书还是自签名证书。我们不常使用 OpenSSL,所以我们不会深入分析这个命令。进入包含主机密钥的目录,并按原样输入以下命令:

# openssl req -new -key host.key -out csr.pem

在响应中,你将看到一些说明,然后是一系列问题。按下 ENTER 键,你将接受默认答案。如果你已配置 OpenSSL,则默认答案是正确的。

➊ Country Name (2 letter code) [US]:
➋ State or Province Name (full name) [Michigan]:
➌ Locality Name (eg, city) [Detroit]:
➍ Organization Name (eg, company) [Burke and Hare Word Mine, LLC]:
➎ Organizational Unit Name (eg, section) [Pen-Monkey Division]:
➏ Common Name (eg, YOUR name) [www.michaelwlucas.com]:
➐ Email Address [mwlucas@michaelwlucas.com]:

国家代码 ➊ 是根据 ISO 3166 标准定义的,你可以通过快速的网页搜索找到它。如果你不知道你所在的州 ➋ 和城市 ➌,可以问问偶尔离开服务器房间的人。组织名称 ➍ 很可能是你的公司,并且你也需要列出部门或分部名称 ➎。如果你没有公司,可以列出你的姓氏或其他任何能唯一标识你的方式,对于自签名证书,你可以列出任何你想要的内容。不同的 CA 对非公司实体有不同的标准,所以请检查 CA 的说明。

公共名称 ➏ 经常被误解。它不是你的名字,而是反向 DNS 中显示的服务器名称。你必须在这里填写服务器名称,否则请求将毫无用处。

我建议使用一个通用的电子邮件地址 ➐,而不是个人的电子邮件地址。在这种情况下,我 michaelwlucas.com,所以我可以使用我的地址。你不希望你的组织的证书与一个可能因任何原因离开公司的个人挂钩。

   Please enter the following 'extra' attributes
   to be sent with your certificate request
➊ A challenge password []:
➋ An optional company name []:

挑战密码➊也被称为密码短语。再次提醒,保密这个密码短语,因为任何知道它的人都可以使用你的证书。然而,使用证书密码短语是可选的。如果使用密码短语,服务器启动时必须输入它。这意味着,如果你的 Web 服务器崩溃,网站将无法正常工作,直到有人输入密码短语。虽然使用密码短语非常有价值,但这可能是不可接受的。按 ENTER 键以使用空密码短语。

你已经输入了不少公司名称,所以第三个➋可能没有必要了。

一旦你返回命令提示符,你会看到当前目录下有一个名为csr.pem的文件。它看起来与您的主机密钥非常相似,不同之处在于顶行显示的是BEGIN CERTIFICATE REQUEST,而不是BEGIN RSA PRIVATE KEY

csr.pem提交给你的证书授权中心,证书授权中心将返回实际的证书。我建议将证书保存在一个以主机命名的文件中,比如www.mwl.io.crt。这个签名证书适用于任何 TLS 服务,包括网页、pop3ssl 或任何其他支持 TLS 的守护进程。

一些 CA 要求你使用中间证书与您的证书一起使用。虽然大多数守护进程有配置选项来指定中间证书,但如果没有,你可以将签名证书附加到中间证书的末尾。

自行签署证书

自签名证书在技术上与签名证书相同,但它没有提交给证书授权中心。相反,你自己提供签名。大多数客户不会在生产服务中接受自签名证书,但它非常适合用于测试。要签署你自己的 CSR,请运行以下命令:

# openssl x509 -req -days➊365 -in csr.pem -signkey host.key \
-out➋selfsigned.crt
Signature ok
subject=/C=US/ST=Michigan/L=Detroit/O=Burke and Hare Word Mine, LLC/OU=Pen-
Monkey Division/CN=michaelwlucas.com/emailAddress=mwlucas@michaelwlucas.com
Getting Private key
#

就这样!你现在拥有一个有效期为 365 天➊的自签名证书,保存在文件selfsigned.crt➋中。你可以像使用签名证书一样使用这个密钥,只要你愿意忽略应用程序显示的警告。

如果你自行签署证书,客户端软件会生成“证书签名者未知”的警告。这是正常的——毕竟,除了我的办公室的人,没有人知道 Michael W. Lucas 是谁,或者他为什么要签署网站证书。出于某种原因,人们信任 Symantec 和其他大型公司的 CA。我被认识我的人信任^(4),但在全球范围内并不被信任。因此,不要在公众能够看到的地方使用自签名证书,因为警告会让他们困惑、恼火,甚至把他们吓跑。

在你花钱购买 CA 证书之前,务必先查看 Let’s Encrypt。它真的会改变你的系统管理实践。

TLS 技巧:连接到 TLS 保护的端口

我说过我们不会做太多 OpenSSL 的操作,没错。但这款软件提供了一个太有用的功能,不能错过,一旦你学会了,你每月至少会使用这个技巧一次,并且很高兴你掌握了它。

在本书中,我们通过使用 telnet(1)连接到运行在该端口上的守护进程并发出命令来测试网络服务。这对于 SMTP、POP3 和 HTTP 等明文服务效果很好,但对加密服务如 HTTPS 则不起作用。当你连接到这些服务时,你需要一个程序来为你管理加密。OpenSSL 包含openssl s_client命令,专门用于这种类型的客户端调试。虽然你会看到很多加密信息,但你还可以发出明文命令给守护进程,并查看它的响应。使用命令openssl s_client -connect,并提供主机名和端口号,中间用冒号隔开。这里,我们连接到安全网页服务器 www.absolutefreebsd.com

# openssl s_client -connect www.michaelwlucas.com:443
CONNECTED(00000003)
depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
--snip--

你将看到许多关于信任链和责任限制的内容,以及看起来随机的数字证书。然而,在所有这些之后,你会看到一个空白行,没有命令提示符。你正在直接与服务器守护进程对话。由于这是一个网页服务器,让我们尝试一个 HTTP 命令:

GET /

系统响应如下:

HTTP/1.1 400 Bad Request

HTTP 协议自上次我尝试这个以来发生了变化,我猜是这样。但我肯定已经连接到网页服务器了。网络工作正常。

你们中的一些人可能会想,如果与加密服务的通信如此简单,为什么还要加密这个服务呢?加密并不是保护守护进程,而是保护客户端和服务器之间的数据流。TLS 加密可以防止有人在传输过程中窃听你的网络对话——它并不保护服务器或客户端。如果有人闯入了你的桌面,TLS 也无法拯救你。

从此时起,我假设你已经理解了这个 OpenSSL 命令以及我们使用它时发生的事情。

硬件加密支持

大多数现代硬件都内置了加密加速功能。不幸的是,FreeBSD 默认配置中没有包含这一功能。硬件加密加速可以减少 CPU 的负载,并且可能加速加密过程。aesni(4)内核模块激活了对 Intel 硬件加密加速器的访问。新的 AMD 加速器驱动正在开发中。内核驱动仅影响内核中发生的加密过程,例如加密磁盘和 IPSec。

全局安全设置

FreeBSD 支持许多可选的安全设置。这些设置改变了基本的 FreeBSD 行为,使其与常见的 Unix 体验不同。然而,一些其他操作系统默认提供这些设置,因此它们并不唯一于 FreeBSD。

你是否应该为了提高安全性而开启所有这些功能?这里没有一个普遍正确的答案。如果限制某部分系统的访问权限到 root 账户意味着你需要给予更多的人 root 权限,也许你不应该强制施加这种限制。不过,其中有几项应该在所有系统上启用。

安装时选项

FreeBSD 安装程序提供了一个选项,可以在首次启动时启用这些设置。你可以稍后使用给定的 sysctl 设置启用或禁用它们。

这些功能在用户不多的服务器上特别有用。如果你的应用服务器除了应用程序使用的用户外没有其他非特权用户,那么你应该启用限制非特权用户的功能。不过,如果有非特权用户,请更仔细地考虑这种情况。我的大多数非特权用户^(5)不应该查看服务器进程或其他用户,因此我会对他们进行限制。

隐藏其他 UID 的进程

通常,像 ps -ax 这样的命令会显示系统上所有正在运行的进程。当你将 sysctl security.bsd.see_other_uids 设置为 0 时,用户只能看到自己的进程。无论如何设置,root 用户可以看到所有进程。

隐藏其他 GID 的进程

同样,用户通常可以看到其他组拥有的进程。通过将 sysctl security.bsd.see_other_gids 设置为 0,可以禁用该功能。同样,root 用户可以看到所有进程,无论该设置如何。

隐藏监禁进程

主机上的用户通常可以看到所有在 jail 中运行的进程。通过将 security.bsd.see_jail_proc 设置为 0,非特权的非监禁用户将无法看到监禁进程。这个功能出现在 FreeBSD 12 中。

隐藏消息缓冲区

非特权用户通常可以看到系统消息缓冲区,该缓冲区可以通过 dmesg(8) 访问。通过将 sysctl security.bsd.unprivileged_read_msgbuf 设置为 0,可以禁用该访问。

禁用进程调试

调试器可以告诉用户大量有用的信息。将 security.bsd.unprivileged_proc_debug 设置为 0,禁止非特权用户使用调试器调试进程。

随机化进程 ID

传统的 Unix 系统按顺序创建进程 ID,这让攻击者有机会猜测下一个 PID 会是什么。通过将 sysctl kern.randompid 设置为一个随机的大整数,可以随机化进程 ID。如果你将其设置为 1,内核在每次启动时会从 100 到 1,123 之间选择一个新的随机数。

清理 /tmp

所有合理的类 Unix 系统在启动时会清理 /tmp 以清除临时文件。在过去的几年里,FreeBSD 默认关闭了这个行为。你可以使用 tmpfs(5) 来处理 /tmp,它会在每次关机时销毁。如果你的 /tmp 存储在磁盘上,那么……作为一个理性和合格的系统管理员,请始终在 /etc/rc.conf 中将 clear_tmp_enable 设置为 YES

禁用 Syslogd 网络功能

默认情况下,syslogd(8) 会在 UDP 端口 514 上创建一个半开放的套接字。没有人可以连接到这个套接字;它仅作为占位符使用,以防止其他东西绑定到该端口。一些人认为这个半开放的套接字是一个问题。但我认为这是一个特性;你不希望其他东西绑定到端口 514,声称自己是 syslogd,并向你的日志主机发送令人担忧或虚假安抚的消息。要禁用这个半开放的套接字,请在 /etc/rc.conf 文件中将 syslogd_flags 设置为 -ss

禁用 Sendmail

默认的 FreeBSD 安装不会接受来自网络的电子邮件,但它会运行一个 sendmail(8) 守护进程来发送外发邮件。要完全禁用从此主机发送邮件,请在 /etc/rc.conf 文件中将 sendmail_enable 设置为 NONE

禁用外发邮件不会阻止每日、每周和每月的维护任务运行。然而,除非你直接登录到主机,否则它会阻止你接收这些消息的输出。对于有多个主机的人来说,禁用外发邮件是不明智的。如果你使用其他邮件代理,例如 dma(8)(请参见 第二十章),禁用 Sendmail 是合理的。

安全控制台

大多数 Unix 系统认为物理控制台是安全的。任何拥有物理机器访问权限的人都可以对主机做任何事情,包括更改 root 密码。通过将所有 /etc/ttys 文件中标记为 secure 的条目更改为 insecure,你告诉 FreeBSD 即使在单用户模式下也要求输入 root 密码。^(6) 这不会阻止某人通过物理访问来入侵你的操作系统,但意味着他们需要做更多的工作来破坏你的机器。非常稍微多一点的工作。

不可执行堆栈与堆栈保护

一种基本的漏洞缓解技术是不可执行堆栈。程序加载到内存后,分配给该程序的每个内存页面应该是可写的或可执行的,但不能两者兼具。

一种常见的漏洞利用技术是欺骗程序将信息写入内存,然后执行该内存。攻击者可能会说服程序写入一块内存,但在不可执行堆栈的情况下,内核不会执行它。

在现代版本的 FreeBSD 中,堆栈默认是不可执行的。禁用此功能的唯一理由是,如果你有一个编写不当的程序,依赖于执行和写入同一块内存。大多数此类有缺陷的软件在过去 15 年中已被正当地清除出开源生态系统。如果你非常不幸且无法避免运行无法处理不可执行堆栈的程序,你可以通过将 kern.elf32.nxstack(对于 32 位程序)或 kern.elf64.nxstack(对于 64 位程序)设置为 0 来禁用此功能。

与不可执行堆栈相关,堆栈保护页在程序内存分配的各部分之间添加了一个大小随机的额外内存片段。这使得攻击者更难猜测内存地址。FreeBSD 默认分配堆栈保护页,但你可以通过将 sysctl security.bsd.stack_guard_page 设置为 0 来关闭它。

其他安全设置

FreeBSD 其他大多数内核级安全设置可以在 security.bsd sysctl 树中找到。每隔几个月会增加一些新的设置。运行 sysctl -d security.bsd 来显示主机可用的选项。我在本节中已经描述了其中的许多内容,但你可能会发现其他一些设置也有用。选项包括禁用 root 账户的特权(security.bsd.suser_enabled)、允许非 root 用户设置空闲优先级(security.bsd.unprivileged_idprio)以及阻止非特权用户使用 mlock(2)(security.bsd.unprivileged_mlock)。看看当前的选项,看看哪些可能有用。

使用 mtree(1) 为入侵做准备

对系统管理员来说,最糟糕的事情之一就是让他觉得系统可能被攻破了。如果你在 /tmp 中发现神秘文件,或在 /usr/local/sbin 中发现多余的命令,或者感觉“总有些不对劲”,你就会怀疑是否有人入侵了你的系统。最糟糕的是,这种感觉无法得到证实。一个熟练的攻击者可以用她自己定制的版本替换系统二进制文件,这样她的行为就永远不会被记录,你对她的追踪也会失败。当犯罪分子提供了放大镜并包含了特殊的犯罪遮掩功能时,给你的服务器做检查就像是让福尔摩斯用放大镜来检查你的服务器一样毫无意义!有人甚至劫持了系统编译器,以便新构建的二进制文件包含劫持者的后门。^(7) 更糟糕的是,计算机经常做出奇怪的事情。操作系统极其复杂,应用程序更糟。也许 /tmp 中那个奇怪的文件是你的文本编辑器在你敲键盘太快时吐出的,或者它是某个粗心入侵者留下的遗物。

恢复被入侵系统的唯一方法是从头重新安装系统,从备份中恢复数据,并希望导致入侵的安全漏洞已经被修复。这是一个微弱的希望,怀疑感是如此容易产生,以至于许多系统管理员最终停止在乎,或者欺骗自己,而不是忍受不断的担忧。

大多数入侵者会修改系统上已经存在的文件。FreeBSD 的 mtree(1) 可以记录系统上文件的权限、大小、日期和加密校验和。(虽然 freebsd-update(8) 也包含类似的功能,并且你无需提前收集数据,但它仅涵盖基础系统。)如果你在系统刚安装时记录这些特征,你就有了文件完整无损的记录。当入侵者更改这些文件时,比较将突出显示差异。当你感觉自己可能被黑客攻击时,你可以检查现有文件上的相同信息,看看是否有文件发生变化。

运行 mtree(1)

以下命令会在你的根分区上运行 mtree(1),并存储 SHA512 和 SHA256 加密校验和,将它们放入一个文件中以供后续分析:

# mtree➊-x➋-ic➌-K sha512➍-K sha256➎-p /➏-X /home/mwlucas/mtree-exclude >➐/tmp/mtree.out

虽然你可以在整个服务器上使用 mtree(1),大多数人使用 -x ➊ 每次对每个分区运行它。你不希望在频繁更改的文件上记录校验和,例如数据库服务器上的数据库分区。在 NFS 挂载点上收集校验和具有双重特性:非常慢并且增加网络拥塞。-ic 标志 ➋ 告诉 mtree 将其结果打印到屏幕上,每个后续的文件系统层级都会缩进。这个格式与系统中的 mtree 文件(位于 /etc/mtree)匹配。-K 标志接受几个可选的关键字;在这个例子中,我们希望生成 SHA512 校验和 ➌ 和 SHA256 校验和 ➍。-p 标志 ➎ 告诉 mtree 要检查哪个分区。几乎每个分区都有定期更改的文件或目录,因此你不希望为这些文件记录校验和。使用 -X ➏ 指定一个 排除文件,该文件包含不匹配的路径列表。最后,将此命令的输出重定向到文件 /tmp/mtree.out ➐。

mtree(1) 输出:规范文件

mtree(1) 的输出被称为规范(spec),或 规范文件。虽然这个规范最初是为了安装软件时使用,但我们现在用它来验证软件安装。你的规范从注释开始,显示了运行该命令的用户、命令运行的机器、分析的文件系统以及日期。规范中的第一个真实条目设置了该主机的默认值,并以 /set 开头。

/set type=file uid=0 gid=0 mode=0755 nlink=1 flags=uarch

mtree(1) 程序根据对分区中文件的分析选择了这些默认设置。默认的文件系统对象是一个文件,属于 UID 0 和 GID 0,权限为 0755,具有一个硬链接和用户归档标志。之后,系统上的每个文件和目录都有一个单独的条目。以下是根目录的条目:

➊.        ➋type=dir➌nlink=19➍time=1504101311.033742000

这个文件是点(.) ➊,或 我们现在所在的目录。它是一个目录 ➋,并且它有 19 个硬链接 ➌。这个目录在 Unix 纪元时间 1,504,101,311.033742000 秒时被修改 ➍。Unix 纪元始于 1970 年 1 月 1 日。

纪元秒与真实日期

不想计算自纪元开始以来的秒数吗?要将纪元秒转换为普通日期,可以运行date -r秒数。不过,需要去掉 mtree 时间末尾的小数部分;因为 date(1)只接受整数秒。

从某些方面来说,目录的条目相当无聊。毕竟,入侵者无法实际替换目录本身!以下是根目录中一个实际文件的条目:

.cshrc      mode=0644 nlink=2➊size=950 time=1499096179.000000000 \
➋ sha256digest=20d2a78c9773c159bac1df5585227c7b64b6aab6b77bccadbe4c65f1be474e8c \
➌ sha512=24d4330e327f75f10101cd7c0d6a5e59163336ade5b9eb04b0d96ea43d221c5eea4c71a89dfe85a...

我们可以看到文件名和与根目录相同的模式、链接和时间信息,但还可以看到文件大小➊。此外,还有从文件计算得出的 SHA256➋和 SHA512➌加密哈希值。

虽然理论上入侵者可以制作一个与特定加密哈希值匹配的文件,且尽管密码学家们不断尝试找到实际方法来制作匹配任意 SHA256 和 SHA512 校验和的文件,但入侵者能够制作一个匹配这两个校验和、包含其后门且仍能正常运行的伪造文件的可能性极低。如果真的发生这种情况,我们将会有其他抗此类方法的校验和算法,并会切换到这些算法。

排除文件

排除文件(通过-X提供)列出了你不希望 mtree(1)分析的文件系统。许多文件系统会在没有恶意干扰的情况下发生变化。日志文件和用户主目录应该会发生变化。像/tmp/var/db/entropy这样的目录在功能系统中最好也会发生变化。在排除文件中每一行列出你不想检查的目录,前面加上一个点。

./tmp
./var/db/entropy
./var/log
./usr/home

等待一天左右,然后再次运行 mtree(1)生成新的规格文件。两个 mtree 文件之间的差异将帮助你改进排除文件。当你怀疑系统被入侵时,你也可以做完全相同的操作。

保存规格文件

规格文件包含了在怀疑发生入侵后验证系统完整性所需的信息。如果你将规格文件保存在你想要验证的服务器上,入侵者就可以编辑该文件并掩盖其恶行。你绝对不能将文件保存在系统本身上!时不时会有人建议你对 mtree 规格文件进行校验和,但将其保留在服务器上。这样没有用;如果有人篡改了 mtree 文件和校验和,你怎么知道呢?更糟的是——如果有人篡改了规格文件,而你发现了,你也无法知道更改了什么!请将规格文件复制到一个安全的位置,最好是离线介质,如闪存驱动器或光盘。

查找系统差异

当一些事情引起你的怀疑,并开始认为你可能遭遇入侵时,创建一个新的 mtree 规格文件,并与你离线存储的“已知良好”规格文件进行比较。使用 mtree(1)检查规格文件之间的差异。

# mtree -f mtree.suspect -f mtree.good > mtree.differences

文件中的每一项条目都代表一个已发生更改的内容。我的排除文件经过精细调校,去除了我预期会发生更改的文件。这次运行生成了两行输出。

                bin/sh file➊size=161672
➋ sha256digest=a4a85ca3563d8f3bda449711c6b591b37093e668fc136f8829eb188b955f56ab
➌ sha512=011793e3e6cacd99b4261e0a0f3a0b9bd6a6842f3ccd55da1ce2070b568e3c49ae7b0e51d33bb59eff...
                bin/sh file size=➍10489808
➎ sha256digest=45856525d4251b43d68df1429cf1fe0f4adb6640f06d7f995aace5b7ca0c03c2
➏ sha512=a4f0e83e5fb12d615721fd7d57cb6a120068d1aa71fc305b7b86927391f33bec822cf14ce8a8a9db14...

文件/bin/sh ➊的大小在 mtree 运行之间发生了变化➍。这不好。同时,注意到两个不同的 SHA256 哈希值➋ ➎和两个不同的 SHA512 哈希值➌ ➏。现在不要慌张,但开始向你的同事们提出尖锐、直接的问题。如果你不能得到一个合理的答案,解释为什么这个二进制文件发生了变化,可能需要查看你的安装介质。

或者,也许你需要更新你的排除文件。但如果/bin/sh发生了变化,可能就不是了。

监控系统安全

所以,你认为你的服务器是安全的。也许它是……暂时是。

不幸的是,存在一类入侵者,他们无事可做,只能跟进最新的安全漏洞,并尝试在他们认为可能脆弱的系统上进行攻击。即使你虔诚地阅读FreeBSD-security并应用每一个补丁,你仍然有可能在某一天被黑客入侵。虽然没有办法绝对确认你没有被黑客入侵,但以下线索将帮助你在发生异常时发现问题:

  • 熟悉你的服务器。定期运行ps -axx,并了解它们通常运行的进程。如果看到不认识的进程,进行调查。

  • 使用netstat -nasockstat检查你的开放网络端口。你的服务器应该监听哪些 TCP 和 UDP 端口?如果你不认识某个开放端口,进行调查。也许它是无害的,但它也可能是入侵者的后门。

  • 无法解释的系统问题是线索。许多入侵者都是笨手笨脚、缺乏系统管理员技能的傻瓜,他们使用点击和拖拽攻击。他们会让你的系统崩溃,并以为自己是塞缪尔·L·杰克逊的网络化身。

  • 真正熟练的入侵者不仅会在事后清理现场,还会确保系统没有任何可能引起你警觉的问题。因此,系统异常稳定的情况下也应当保持警惕。

  • 无法解释的重启可能表明有人非法安装了新的内核。它们也可能是硬件故障或配置不当的迹象,因此无论如何都应该进行调查。

  • FreeBSD 每天会通过邮件发送基本的系统状态信息。阅读它们。保存它们。如果发现任何可疑之处,进行调查。查看旧的消息,了解什么时候发生了变化。

我特别推荐使用lsof软件包来增加你对系统的熟悉度。lsof程序列出系统上所有打开的文件。阅读 lsof(8)的输出本身就是一种教育;你可能从未意识到你的 Web 服务器会打开这么多无用的文件。看到陌生的文件被打开,意味着你可能对你的系统不够熟悉,或者某人在做不当的事情。

软件包安全

FreeBSD 项目提供了一个包含端口和软件包系统安全漏洞的数据库。这个数据库以漏洞与曝光标记语言(VuXML)的形式提供。当有人自愿维护一个端口时,他们也自愿关注该端口的安全问题。

一台连接互联网并安装了 pkg(8) 的 FreeBSD 主机会在 periodic(8) 运行时下载最新的 VuXML 文件(参见 第二十一章),并将其存储在 /var/db/pkg/vuln.xml 中。然后,它会将已安装的软件包与该数据库进行比较。如果你的某个软件包存在漏洞,你将会在每日状态邮件中收到通知。(你有在阅读每日状态邮件吧?)

如果你的软件包不安全,按照 第十五章 的方法升级它们。

如果需要,你可以通过在 pkg.conf 中使用 VULNXML_SITE 选项来设置一个不同的地址来获取 vuln.xml 文件。如果你维护自己的软件包仓库和漏洞数据库,你可能会这么做。

如果你被黑了

在这一切之后,如果你的系统被黑了,你该怎么办?没有简单的答案。关于这个主题已经写了大量书籍。不过,这里有一些常规的建议。

首先也是最重要的:一个被黑客入侵的系统是无法信任的。如果有人获得了你互联网服务器的 root 权限,他可能已经替换了系统上的任何程序。即使你修补了他入侵的漏洞,他也可能已经安装了一个篡改过的 login(8) 程序,每次你登录时都会将你的用户名和密码发送到某个 IRC 频道。不要信任这个系统。升级无法清除它,因为即使是 freebsd-update(8) 和编译器也有可能受到怀疑。

虽然 rootkit 检测软件可能帮助你验证是否存在入侵者,但没有任何工具可以验证入侵者是否“已不在”系统中。你可以随时写信给 FreeBSD-security@FreeBSD.org 请求建议。描述你所看到的情况以及你认为自己被黑的原因。不过,准备好面对一个不太愉快的答案:彻底从已知安全的介质重新安装你的计算机,并从备份中恢复数据。你看过 第五章了吗?

良好的安全实践能减少被黑客攻击的几率,就像安全驾驶能减少发生车祸的几率一样。最终你无论如何都会撞坏你的车,然后会想为什么要费这个劲。祝你好运!

第二十章:小型系统服务**

image

即使是角色定义非常狭窄的服务器,例如专用的 web 服务器,也需要多种小型“辅助”服务来处理基本的管理问题。在本章中,我们将讨论其中一些服务,例如时间同步、发送邮件、DHCP 服务、任务调度等等。我们将从使用 SSH 加固远程连接到 FreeBSD 服务器开始。

安全外壳

Unix 的一个伟大优势是它的远程管理的便捷性。无论服务器是在你面前,还是位于一个远程的、被围栏隔离的实验室,或者在一个地下的、最高安全级别的设施中,周围环绕着由一个名叫 Ivan 的自大鼬鼠训练的凶猛守卫犬,只要你能访问网络,就可以控制它。

多年来,telnet(1) 一直是访问远程服务器的标准方式。然而,作为远程管理协议,telnet 有一个致命的缺陷:大多数版本的 telnet 发送的数据是未加密的。任何在你的连接路途中放置数据包嗅探器的人,都可以窃取你的用户名、密码和你在 telnet 会话中查看的任何信息。当你使用 telnet 时,世界上最好的密码选择方案也无法保护你的用户名和密码。入侵者可以将非法的数据包嗅探器放置在任何他们能接触到的地方;我见过它们出现在小型局域网、全球企业网络、处理敏感政府工作的律师事务所、家庭电脑以及互联网骨干网络中。对抗数据包嗅探器的唯一防御手段是以一种嗅探器无法理解的方式处理你的认证凭据和数据。这就是 SSH(安全外壳)的作用所在。

SSH 的行为与 telnet 非常相似,因为它提供了一个高度可配置的终端窗口用于远程主机。但是与 telnet 不同,SSH 会加密你通过网络发送的所有内容。SSH 不仅确保你的密码不会被嗅探,还确保你输入的命令及其输出被加密。虽然 telnet 在某些方面对比 SSH 具有一些小的优势,例如需要更少的 CPU 时间和配置更简单,但 SSH 在安全性上的优势完全压倒了这些小优点。SSH 还具有 telnet 所不具备的许多功能,例如通过加密会话隧道传输任意协议。SSH 可以在所有现代的 Unix 变种上运行,甚至在微软的 Windows 上也能运行。

SSH 通过公钥加密技术加密和认证远程连接。SSH 守护进程提供服务器的公钥给客户端,并将私钥保留给自己。客户端和服务器使用加密密钥协商一个加密安全通道。由于公钥和私钥都在此交易中起作用,因此你的数据是安全的;即使有人捕获了你的 SSH 流量,他们也只能看到加密的垃圾数据。

要使用 SSH,必须在 FreeBSD 机器上运行 SSH 服务器,并在工作站上运行 SSH 客户端。

SSH 服务器:sshd(8)

sshd(8) 守护进程监听来自网络的 SSH 请求,端口为 TCP 22。要在启动时启用 sshd,请将以下行添加到 /etc/rc.conf

sshd_enable="YES"

设置完成后,你可以使用 /etc/rc.d/sshd 脚本或 service sshd 子命令来启动和停止 SSH。停止 SSH 守护进程不会终止已经在使用的 SSH 会话;它只会阻止守护进程接受新的连接。

与我们查看的未加密协议不同,sshd 很难手动测试。你可以做的一件事是通过使用 nc(1) 连接到 SSH TCP 端口来确认 sshd 是否正在运行。

# nc localhost 22
SSH-2.0-OpenSSH_7.2 FreeBSD-20160310

我们连接到端口 22,并收到一个 SSH 横幅信息。我们可以看到监听该端口的守护进程将自己称为 SSH 版本 2,基于 OpenSSH 7.2 实现,运行在 FreeBSD 上,版本为 20160310。你可以通过一个简单的 nc(1) 连接获取这些信息,但这也是 sshd 提供的最后一条免费信息。除非你能够手动加密数据包,否则这就是你能做到的极限了。按 CTRL-C 离开 nc(1),返回命令提示符。

SSH 密钥和指纹

第一次启动 sshd(8) 时,程序会意识到它没有加密密钥,并自动创建它们。初始化的 sshd 进程会创建三对密钥:一个 RSA 密钥,一个 ECDSA 密钥和一个 ED25519 密钥。

.pub 结尾的密钥文件包含每种类型的公钥。这些是 sshd 提供给连接客户端的密钥。这使得连接的用户能够验证他连接的服务器是否真的是他认为的那台服务器。(入侵者曾通过诱使用户登录虚假的机器来窃取他们的用户名和密码。)看看其中一个公钥文件;它非常长。即使用户被提供确认服务器提供的密钥是否正确的机会,由于密钥长度过长,即使是最警觉的用户也不会逐一验证每个字符。

幸运的是,SSH 允许你生成一个 密钥指纹,这是密钥的一个简短表示。你不能用指纹加密流量或协商连接,但两个不相关的密钥具有相同指纹的机会可以忽略不计。要为公钥生成指纹,输入命令 ssh-keygen -lf keyfile.pub。

# ssh-keygen -lf /etc/ssh/ssh_host_rsa_key.pub 
2048 SHA256:tEcBfgXctTfaaEF9d5QK3oYUwr5Tb/cuIr3MNxV4wwE root@bert (RSA)

第一个数字2048表示密钥的位数。2048 是 2018 年 RSA 密钥的标准,但随着计算能力的提高,我预计这个数字会增加。以tEcB开头,以wwE结尾的字符串是公钥的指纹。虽然它很长,但比实际的密钥短得多,也更易读。将这个密钥指纹从原始服务器复制到你可以从客户端机器访问的地方。如果需要人工验证指纹,可以尝试使用网页或纸质列表。如果你的 SSH 客户端支持 SSHFP 记录,并且你的 DNS 区域支持 DNSSEC,可以改为使用 DNS。在第一次连接时,使用此密钥确认你的服务器身份,或者使用其他密钥分发方法之一。

配置 SSH 守护进程

尽管 sshd 自带的配置文件已经可以正常使用,但一旦你了解了 sshd(8)提供的所有功能,你可能会想调整设置。配置文件/etc/ssh/sshd_config列出了所有默认设置,并用井号(#)注释掉。如果你想更改某个设置的值,只需取消注释并修改其值。

我们不会讨论所有可用的 sshd 选项;那样会需要一本相当大的书。此外,OpenSSH 的更新速度非常快,以至于那本书在出版之前就会过时。相反,我们将重点讨论一些人们常做的、更常见的配置更改。

更改 SSH 守护进程的配置后,使用/etc/rc.d/sshd restartservice sshd restart重启守护进程。

VersionAddendum FreeBSD-20170902

VersionAddendum会出现在你连接到 sshd 的 TCP 端口时的服务器名称中。有人建议更改它以掩盖操作系统版本。然而,通过使用指纹识别技术分析与主机交换的数据包,识别计算机的操作系统是非常简单的,因此通常不值得花时间做这件事。(另一方面,如果将VersionAddendum更改为DrunkenBadgerSoftware让你觉得好玩,那就继续吧。)

Port 22

sshd(8)默认监听 TCP 端口 22。如果你愿意,可以将其更改为非标准端口。如果你希望 sshd 监听多个端口(例如,除了端口 22 外,还监听端口 443),可以在不同的行上包含多个Port条目:

Port 22
Port 443

更改端口并不作为一种安全措施有用。它可能有助于减少日志的杂音。我坦白承认,我有一个小型 SSH 服务器,监听多个流行的 TCP 端口,专门用来绕过无用的网络安全设备。但这并不会使 SSH 变得更加安全

ListenAddress 0.0.0.0

sshd 默认监听机器上的所有 IP 地址的传入请求。如果你需要限制监听的地址范围(例如,在一个监禁服务器上),可以在此处指定:

ListenAddress 203.0.113.8

如果你希望 sshd 监听多个地址,可以使用多个ListenAddress行。

SyslogFacility AUTH 和 LogLevel INFO

这两个设置控制 sshd(8) 如何记录连接信息。有关日志记录的更多信息,请参见 第二十一章。

LoginGraceTime 2m

该选项控制用户在连接后必须多长时间内完成登录。如果有用户连接但未在此时间窗口内成功登录,sshd 会断开连接。

PermitRootLogin no

不要让人们直接以 root 用户身份登录到你的服务器。相反,他们应该以普通用户身份通过 SSH 登录,并通过 su(1) 切换为 root 用户。允许直接的 root 登录会让你失去识别是谁错误配置了你的系统的机会,并且让入侵者更容易掩盖他们的痕迹。

MaxAuthTries 6

这是用户在单次连接过程中可以尝试输入密码的次数。超过这个次数后,用户将被断开连接。

AllowTcpForwarding yes

SSH 允许用户将任意的 TCP/IP 端口转发到远程系统。如果用户有 shell 访问权限,他们可以安装自己的端口转发器,因此没有理由禁用这个功能。

X11Forwarding yes

类 Unix 操作系统使用 X11(或 X)协议来显示图形化程序。在 X 中,显示与物理机器是分开的。你可以在一台机器上运行,比如说一个网页浏览器,并将结果显示在另一台机器上。

由于 X 在安全性方面有过波折,许多管理员下意识地禁用 X 转发。然而,禁用 SSH 的 X 转发并不意味着完全禁用 X 转发。大多数用户如果无法通过 SSH 转发 X,就会使用 X 自带的网络功能或第三方转发工具,在未加密的 TCP/IP 上转发 X,这通常比允许通过 SSH 转发 X 更糟。如果你的 sshd 服务器已安装 X 库和客户端程序,用户可以通过某种方式转发 X;最好让 SSH 来处理转发。如果没有安装 X 软件,那么 X11Forwarding 将无效。

横幅是认证发生前显示的消息。这个选项最常用的场景是显示法律警告。默认情况下不使用横幅。

Subsystem sftp /usr/libexec/sftp-server

SSH 允许你通过 scp(1) 安全地将文件从一台系统复制到另一台系统。尽管 scp 工作得很好,但它的用户友好性差。sftp 服务器提供了类似 FTP 的文件传输接口,减少了你在用户教育上的时间,同时仍保持了坚实的安全性。

管理 SSH 用户访问

默认情况下,任何拥有合法 shell 的用户都可以登录到服务器。通过使用配置变量 AllowGroupsDenyGroupsAllowUsersDenyUsers,sshd(8) 允许你定义哪些用户和组可以或不可以访问你的机器。

当你明确列出可以通过 SSH 登录机器的用户时,任何未列出的用户都无法通过 SSH 登录。

例如,AllowGroups 选项允许你限制只有在指定组中的用户才能访问 SSH,这些组在 /etc/group 中定义(见 第九章)。如果设置了此选项,并且用户不属于任何允许的组,他将无法登录。多个组之间用空格分隔:

AllowGroups wheel webmaster dnsadmin

如果你不想给整个组提供 SSH 访问权限,可以使用 AllowUsers 列出单个用户。通过使用 AllowUsers,你禁止了除了列出的用户之外的所有人访问 SSH。

DenyGroups 列表与 AllowGroups 相反。指定的系统组中的用户无法登录。列出的组必须是他们的主组,这意味着它必须列在 /etc/master.passwd 中,而不仅仅是 /etc/group 中。这个限制使得 DenyGroups 没有看起来那么有用;除非你将它设置为用户的主组,否则无法定义一个叫做 nossh 的通用组并将用户添加进去。显式列出允许的组是一个更有用的策略。

最后,DenyUsers 变量列出了不允许登录的用户。你可以使用它显式禁止那些虽然属于允许组但被列为禁止的特定用户。

这四个不同的设置使得一个用户可以同时属于多个组。例如,一个用户可能同时属于 AllowGroups 列出的组和 DenyGroups 列出的组。那么该怎么办呢?SSH 守护进程按以下顺序检查这些值:DenyUsersAllowUsersDenyGroupsAllowGroups。第一个匹配的规则生效。例如,假设 Bert 是 wheel 组的成员。这里是 sshd_config 的一部分:

DenyUsers: bert
AllowGroups: wheel

Bert 无法通过 SSH 登录这台机器,因为 DenyUsersAllowGroups 之前进行检查。

SSH 客户端

当然,FreeBSD 自带 SSH 客户端,像大多数类 Unix 操作系统一样。如果可能,使用包含的 SSH 客户端——它是 OpenSSH 的一部分,由 OpenBSD 团队的一个子集开发,不仅是最流行的实现,而且是最好的。如果你被迫使用微软的操作系统,我推荐 PuTTY,它对商业或非商业用途都是免费的,并且有出色的终端仿真。微软正在将 OpenSSH 的一个分支集成到 Windows 中,但在我写这篇文章时,它仍处于测试版。

这是一本 FreeBSD 的书,所以我们将重点介绍 FreeBSD 的 OpenSSH 客户端。你可以通过多种方式配置客户端,但最常见的配置选项仅仅是禁用服务器提供的功能。如果你真的有兴趣调整客户端的行为,阅读 ssh_config(5)。

要连接到另一台主机进行 SSH 登录,输入 ssh 主机名。你将看到类似这样的响应:

# ssh mwl.io
The authenticity of host 'mwl.io (203.0.113.221)' can't be established.
ECDSA key fingerprint is SHA256:ZxOWglg4oqcZKHOLv5tfqPlAwDW6UGVbiTvjfAjMc4E.
No matching host key fingerprint found in DNS. 
Are you sure you want to continue connecting (yes/no)? yes

您的客户端会立即从您连接的主机获取公钥,并检查其内部 SSH 密钥列表中是否存在与该主机匹配的密钥。如果服务器提供的密钥与客户端列表中的密钥匹配,客户端就会认为您连接到了正确的主机。如果客户端在其已知主机列表中没有该主机的密钥,它将呈现密钥指纹供您确认。

SSH 客户端提供的指纹应与您在服务器上生成的指纹完全相同。如果指纹不相同,则说明您连接到了错误的主机,需要立即断开连接。如果指纹匹配,则接受该密钥并继续。一旦您接受了指纹,密钥将保存在您的主目录下的.ssh/known_hosts中。

如果您正在为个人使用在本地网络上搭建新服务器,也许您不需要手动比较密钥指纹。不过,您仍然应该复制密钥指纹,因为您最终会希望从远程位置连接,并且需要验证密钥。如果许多人将连接到该服务器,通常可以将指纹放在网页上。您必须决定需要多少安全性。我强烈建议您在安全性上采取谨慎态度。

接受主机密钥,您将被允许登录服务器。虽然使用带有密码短语的私钥比使用密码更好,但在 SSH 中使用密码仍然比使用 telnet 更安全。

通过 SSH 复制文件

SSH 客户端适用于命令行访问,但如果要将文件从一个系统移动到另一个系统呢?SSH 包含两个用于通过网络移动文件的工具:scp(1) 和 sftp(1)。

scp(1) 是“安全复制”,非常适合移动单个文件。scp 需要两个参数:第一个是文件的当前所在位置;第二个是目标位置。目标位置的格式为 <用户名>@<主机名>:<文件名>。假设我想将本地系统上的文件 bookbackup.tgz 复制到远程服务器 mwl.io,并为远程副本指定不同的名称。我会运行:

# scp bookbackup.tgz mwlucas@mwl.io:bookbackup-january.tgz

如果您想让新复制的文件保持相同的名称,可以省略第二个参数中的文件名:

# scp bookbackup.tgz mwlucas@mwl.io:

scp(1) 还允许您将文件从远程系统复制到本地系统:

# scp mwlucas@mwl.io:bookbackup-january.tgz bookbackup.tgz

如果您不想更改本地系统上的文件名,您可以使用一个点作为目标名称:

# scp mwlucas@mwl.io:bookbackup.tgz .

最后,如果您在远程系统上的用户名与本地用户名相同,您可以删除用户名和 @ 符号。例如,要备份我的工作,我只需使用:

# scp bookbackup.tgz mwl.io:

尽管看起来有些复杂,但它对于快速在网络上移动单个文件非常有用。

如果您喜欢交互式系统,或者如果您不确定想从远程服务器抓取的文件的准确名称,那么 sftp(1) 是您的朋友。sftp(1) 需要一个参数,使用 scp 的语法来指定远程服务器的用户名和服务器名:

# sftp mwlucas@mwl.io
Connecting to bewilderbeast...
Password:
sftp> ls

sftp(1)客户端看起来非常像标准的命令行 FTP 客户端;它支持常见的 FTP 命令,如ls(列出)、cd(更改目录)、get(下载文件)和put(上传文件)。一个重要的区别是,sftp(1)不需要在 ASCII 和二进制传输之间做出选择;它只是按原样传输文件。

使用 SSH、scp 和 sftp,你可以完全消除网络中的明文密码。

OPENSSH 密码和密钥

要真正保护你的系统,请使用基于密钥的 SSH 认证。创建密钥并不困难,但以最适合你的环境的方式部署它们,比我在这里能讲的要复杂得多。消除 SSH 密码是你可以在网络中做出的最重要的安全改进。

虽然 SSH 是最常见的系统管理员工具,但我们只触及了它的表面。你花时间精通 SSH,将会得到多倍的回报。你可以在网上找到一些很好的教程和几本不错的书籍,包括我自己的SSH Mastery(Tilted Windmill Press,2018)。

电子邮件

近年来,运行邮件服务器变得异常复杂。应对垃圾邮件、病毒以及随机的无用信息需要专门的技能,并且这些垃圾信息的数量每年都在激增。在你部署邮件服务器之前请仔细考虑。然而,每个主机都需要某种形式的邮件客户端。FreeBSD 包含两个可以用于管理本地邮件并将邮件转发到邮件服务器的软件套件:Sendmail 和 Dragonfly Mail Agent。

Sendmail 是邮件程序的鼻祖。它可以是服务器、客户端、过滤器,也可以是任意的邮件处理程序。如果你想与那些隔绝在外、仅通过拨号线路的 UUCP 每天交换一次邮件的网站进行通信,同时也与最新的商业邮件服务器交换邮件,Sendmail 是一个可靠的选择。然而对于大多数人来说,这个被称为电子邮件的瑞士军刀过于复杂。

Dragonfly Mail Agent (DMA) 来自 Dragonfly BSD。它是一个非常简洁的邮件客户端,可以在本地主机上发送邮件,或将邮件转发到邮件服务器。它正是你需要的那种工具,用来将每日状态邮件转发给负责读取这些邮件的下属,将你的应用程序的报告发送给应用管理员,并转发所有那些让你 WordPress 安全插件希望你阅读的烦人报告。

我们将花一些时间了解 DMA。然而,在此之前,让我们先谈谈 FreeBSD 如何应对全球各种各样的邮件服务器。

mailwrapper(8)

几十年来,Sendmail 是类 Unix 系统中唯一可用的邮件服务器。因此,大量软件都期望每个服务器都有 /usr/sbin/sendmail,并且期望它的行为与 Sendmail 完全相同。更糟糕的是,当通过不同名称调用时,Sendmail 的行为会有所不同。程序 mailq(1) 是 sendmail(8) 的硬链接,但由于它的名称不同,因此行为也不同。newaliases(1)、send-mail(8)、hoststat(8) 和 purgestat(8) 也是如此。^(1)

由于客户端期望找到 Sendmail,任何替代的邮件服务器必须精确模拟 Sendmail,包括这种多重名称行为。使用不同的邮件服务器并不像删除 Sendmail 二进制文件并替换为其他东西那么简单。但人们总是试图这么做。

因此,探索不熟悉的 Unix 系统的系统管理员可能完全不知道 /usr/sbin/sendmail 真实是什么!如果有人曾经安装过多个不同的邮件服务器,试图找到比 Sendmail 更易用的替代品,那么你就得通过侦探工作和坚持不懈的努力来识别所谓的 sendmail(8)。

FreeBSD 通过使用一个独立的 mailwrapper(8) 程序绕过了所有这些混乱。邮件包装器将对 Sendmail 的请求引导到首选的邮件服务器,邮件服务器安装在其他地方。

/etc/mail/mailer.conf 中配置 mailwrapper(8)。这个文件包含了程序名称的列表,以及实际程序的路径。以下是默认的 mailer.conf,它将所有请求指向老旧的 sendmail(8):

sendmail        /usr/libexec/sendmail/sendmail
send-mail       /usr/libexec/sendmail/sendmail
mailq           /usr/libexec/sendmail/sendmail
newaliases      /usr/libexec/sendmail/sendmail
hoststat        /usr/libexec/sendmail/sendmail
purgestat       /usr/libexec/sendmail/sendmail

左栏中的这六个“程序”名称是其他程序可能用来指代 Sendmail 的名称。右栏给出了应该调用的程序路径。在这里,我们看到 Sendmail 安装在 /usr/libexec/sendmail/sendmail 路径下。如果你使用的是替代邮件服务器,必须编辑 mailer.conf 来指向正确的邮件程序路径。大多数替代邮件服务器为每个功能使用单独的程序,因为自 Sendmail 出现以来,磁盘空间的成本已经大幅下降。当你从软件包或端口安装替代邮件服务器时,安装后的消息通常会提供如何更新 mailer.conf 的具体说明。如果你希望新邮件服务器正常工作,请按照这些说明操作。如果你没有使用软件包而是安装了不同的邮件服务器,你需要自己编辑 mailer.conf

Dragonfly Mail Agent

Dragonfly Mail Agent (DMA) 可以本地投递邮件并将邮件发送到另一台服务器,但它无法通过网络接收邮件。大多数邮件服务器会绑定到本地主机的 TCP 端口 25,而 dma(8) 则不会。它只为那些可以调用 /usr/sbin/sendmail 或其对等程序的程序投递邮件。

在启用 DMA 之前,需在 /etc/dma/dma.conf 中进行配置。这个文件包含了一些变量,你可以取消注释并设置为特定值。虽然 DMA 有多个可配置的设置,但你应该保持大多数设置为默认值。

智能主机

智能主机是实际的邮件服务器,是客户端应该通过其转发邮件的主机。使用主机名或 IP 地址。

SMARTHOST=mail.mwl.io
TCP 端口

如果你的邮件管理员是个疯狂的人,给智能主机的邮件设置了非标准端口,或者你正在尝试绕过 ISP 封锁的 25 端口,可以在此设置 TCP 端口:

PORT 2025

如果你没有设置智能主机但设置了端口,邮件投递将会中断。

虚假主机名和用户名

你可能希望服务器在发送邮件时自称为另一个主机。也许你的云服务提供商给这个系统分配了一个由随机数字和字母组成的主机名,但你希望它以www.example.com发送邮件。使用 MAILNAME 来设置一个假主机名。

MAILNAME www.mwl.io

如果你为 MAILNAME 提供了文件的完整路径,dma(8)将使用该文件的第一行作为主机名。

一些邮件服务器会非常严格地检查转发的邮件,并拒绝不充分伪造的消息。对于这些主机,你需要使用 MASQUERADE 选项。伪装提供了几种更改邮件的方式。如果你使用完整的电子邮件地址,所有通过 dma(8)发送的邮件都会被重写,使其看起来来自该地址。如果你使用带有@符号的用户名,例如bert@,所有邮件看起来都来自该主机上的该用户。仅使用主机名将保留发送者的用户名不变,但会更改主机名。

MASQUERADE bert@mwl.io

从这个主机发送的任何邮件看起来都是 Bert 发送的,所有的回复也会发给他。一切都按预期进行。

禁用本地投递

一些主机不应该接收邮件。该主机上的任何账户都不应接收邮件,甚至是来自其他本地账户的邮件。通过取消注释 NULLCLIENT 选项完全禁用本地邮件投递。

安全传输

在过去的几十年里,电子邮件协议加入了许多不同的安全措施。你的邮件服务器可能会使用其中的某些或全部措施。请与邮件管理员沟通,了解你的智能主机需要和支持哪些安全措施。

通过取消注释 SECURETRANSFER 选项启用 TLS(如果你的邮件服务器特别糟糕,也可以使用 SSL)。你不需要设置该值,单纯的存在即可启用 TLS。如果你的邮件服务器需要 STARTTLS,也需要取消注释该选项。如果你希望即使 TLS 协商失败也能发送邮件,还需要取消注释 OPPORTUNISTIC_TLS。

这三个选项都需要前面的选项。你可以单独使用 SECURETRANSFER,或者同时使用 STARTTLS 和 SECURETRANSFER,或者全部使用。没有前导选项的 STARTTLS 和 SECURETRANSFER 无法正常工作。

如果你需要本地 TLS 证书,可以通过 CERTFILE 选项进行设置。

CERTFILE /etc/ssl/host.crt

这些选项应该让你能够连接到几乎任何智能主机。

用户名和密码

一些智能主机要求客户端使用用户名和密码进行身份验证。将身份验证凭据放入文件/etc/dma/auth.conf中。每个条目需要以下格式:

user|host:password

假设我的智能主机是mail.mwl.io。用户名是www1,密码是BatteryHorseStapleCorrect。我的auth.conf文件将包含:

www1|mail.mwl.io:BatteryHorseStapleCorrect

DMA 将使用这个来登录到你的主机。

如果你想通过未加密的连接使用用户名和密码,你必须设置 INSECURE 变量。通过网络发送未加密的身份验证信息是个不好的主意,但许多邮件服务器充满了不好的主意。

启用 DMA

使用 DMA 需要关闭任何现有的 Sendmail 进程,并在 mailer.conf 中启用 dma(8)。

即使 Sendmail 仅处理本地投递,它仍作为一个守护进程运行。使用 service(8) 或 /etc/rc.d/sendmail 脚本关闭 Sendmail。

# service sendmail stop

确保它永远不会再次启动。

# sysrc sendmail_enable=NONE

现在,前往 /etc/mail/mailer.conf 并将每个邮件程序指向 dma(8)。

sendmail        /usr/libexec/dma
send-mail       /usr/libexec/dma
mailq           /usr/libexec/dma
newaliases      /usr/libexec/dma
rmail           /usr/libexec/dma

DMA 没有持久的守护进程,因此不需要启动脚本。

恭喜,现在你拥有一个小型、简单且有效的客户端邮件代理。

别名文件和 DMA

/etc/mail/aliases 文件包含发送到特定账户或用户名的电子邮件的重定向。即使是像 DMA 这样的邮件客户端和邮件代理也使用别名文件。向别名文件添加条目是本地重定向邮件的好方法。

虽然别名文件具有许多功能,但 DMA 仅能使用其中的一部分。像将邮件重定向到任意文件这样的功能是不可用的。我们将讨论其基本功能。

打开别名文件并查看。每一行都以别名名称或地址开始,后面跟着冒号和一个真实用户列表,用于接收邮件。我们将通过示例来说明别名是如何工作的。

将邮件从一个用户转发到另一个用户

应该总有人读取发送到 root 账户的邮件。与其让某个人登录每一台服务器去读取这些信息,不如将所有 root 的邮件转发到另一个邮件地址。

root: bert@mwl.io

我已将 Bert 指派为读取所有机器上所有邮件的工作。^(2)

许多电子邮件地址没有与之关联的账户。例如,通常要求的 postmaster 地址并没有账户。你可以使用别名将其转发到一个真实账户。

postmaster: root

所以,postmaster 转发到 root,然后转发到 Bert。Bert 获取这两个地址的所有邮件。

默认的别名文件包含各种标准地址,用于互联网服务,以及所有默认 FreeBSD 服务账户的别名。默认情况下,它们都指向 root。通过为 root 邮件定义一个真实地址作为目标,你将自动接收到所有系统管理邮件。

别名邮件列表

你可以列出多个用户来创建小型邮件列表。对于动态列表来说,这并不适用,但对于快速和简便的列表来说足够了。

escalate: mwlucas@mwl.io, bert@mwl.io, helpdesk@mwl.io

一旦你发现自己正在创建一个别名邮件列表,那就是你需要开始考虑要部署哪个邮件列表解决方案的时候。你会比你想象的更早需要它。

网络时间

如果一个数据库开始录入比实际时间滞后三小时的日期,或者如果电子邮件的日期显示是明天,你很快就会听到相关反馈。时间是重要的。^(3) 你有两个工具来管理系统时间:tzsetup(8) 来控制时区,ntpd(8) 来调整时钟。首先手动设置你的时区,然后使用网络时间协议。

设置时区

使用 tzsetup(8) 管理时区非常简单,它是一个菜单驱动的程序,可以为每个时区在系统上做出相应的更改。全球性组织可能在他们的系统上使用 UTC(协调世界时,之前称为格林尼治标准时间,当前称为协调世界时,很快将改名为另一种名称),而其他组织则使用他们自己的本地时间。输入 tzsetup,按照地理提示操作,选择适合你所在位置的时区。如果你知道你时区的官方名称,可以直接在命令行中设置,而无需经过提示。

# tzsetup America/Detroit

tzsetup(8) 程序将相关的时区文件从 /usr/share/zoneinfo 复制到 /etc/localtime。这是一个二进制文件,你不能使用普通的文本编辑器进行编辑。如果你的时区特性发生变化——例如夏令时开始的日期改变——你必须升级 FreeBSD 来获取新的时区文件,然后重新运行 tzsetup(8) 来正确地重新配置时间。

用户可以使用 TZ 环境变量来设置个人时区。

网络时间协议

网络时间协议 (NTP) 是一种在网络中同步时间的方法。你可以让本地计算机的时钟与政府研究实验室的原子钟或主服务器的时间保持一致。提供时间同步的计算机被称为 时间服务器,大致分为两组:Tier 1 和 Tier 2。

Tier 1 NTP 服务器直接连接到高精度的时间保持设备。如果你真的需要这种精度,那么你真正需要的是你自己的原子钟。像在廉价 GPS 上找到的 USB 无线电钟看起来可能很不错,但 USB 证明不是传输时间数据的理想介质。去查看一个专用的非 USB GPS 接收器的价格,然后选择一个 Tier 1 NTP 服务器。

Tier 2 NTP 服务器从 Tier 1 NTP 服务器获取时间,提供作为公共服务的时间服务。它们的服务精度达到毫秒级,对于几乎所有非生命支持应用都足够了。进一步的调查甚至会让你找到 Tier 3 时间服务器,它们从 Tier 2 服务器获取时间。

最好的时间服务器来源是 www.pool.ntp.org/ 上的列表。这个组织将公共 NTP 服务器收集成循环 DNS 池,方便进行 NTP 配置。NTP 服务器首先按全球列表排列,然后按大陆排序,再按国家排列。例如,如果你在加拿大,简单搜索该站点会找到 0.ca.pool.ntp.org1.ca.pool.ntp.org2.ca.pool.ntp.org。我们在以下示例中将使用这些服务器,但在设置您自己的时间服务时,请查找适合您国家的服务器并使用它们。

配置 ntpd(8)

ntpd(8) 会将系统时钟与一组时间服务器进行对比。它取这些时间服务器提供的时间的合理平均值,舍弃任何与共识相差过远的服务器,并逐渐调整系统时间以匹配平均值。这提供了最精确的系统时间,而不会对任何一个服务器要求过多,并帮助保持错误硬件的检查。NTP 配置文件在 /etc/ntpd.conf 中。以下是一个使用加拿大时间服务器的示例:

server 1.ca.pool.ntp.org
server 2.ca.pool.ntp.org
server 3.ca.pool.ntp.org

该系统检查三个时间服务器的更新时间。如果只列出一个服务器,ntpd(8) 会将其时钟与该服务器同步,并共享该服务器遇到的任何时间问题。使用两个时间服务器会导致系统无法正确判断时间;请记住,NTP 会取时间服务器的平均值,但会丢弃任何与其他服务器的时间差异过大的值。当只有两个值时,NTP 如何判断一个服务器是否错误?使用三个时间服务器是最优的;如果其中一个服务器出现问题,ntpd 会发现该服务器提供的时间与另外两个服务器提供的时间不一致。(可以把它看作是“多数人的暴政”;唯一与其他人意见不同的人根本没有发言权。)

ntpd(8) 在启动时

要让 ntpd 在启动时执行一次性时钟同步,然后持续调整时钟,请在 /etc/rc.conf 中设置以下内容:

ntpd_enable="YES"
ntpd_sync_on_start="YES"

Ntpd 在启动时会立即强制校正时间,然后轻柔地保持时钟同步。

即时时间校正

ntpd(8) 非常擅长长期保持系统时钟的准确性,但它只会逐渐调整本地时钟。如果您的时间差错达到小时或天数(例如安装时或长时间断电后发生的情况),您可能希望在启动任何时间敏感的应用程序之前,先正确设置时钟。ntpd(8) 也包含此功能,可以使用 ntpd -q 来实现。

要对时钟执行一次强制校正,请使用ntpd -q。这会连接到您的 NTP 服务器,获取正确的时间,设置系统时钟,并退出。

# ntpd -q
ntpd: time set -76.976809s

系统的时间大约落后了 77 秒,但现在已与 NTP 服务器同步。

不要在生产系统中随意更改时钟。许多数据库驱动的应用程序等时间敏感型软件,如果时间突然向前或向后移动,会遇到问题。

如果你有非常好的硬件并且配备了优质的振荡器,使用ntpd -q在启动时可以解决所有时间问题。然而,实际上很少有人拥有这种硬件。我们大多数人都得忍受廉价硬件,后者通常有着极差的时钟。确保时间准确的最佳方式是运行 ntpd(8),以持续轻微调整时钟。

时间重新分配

虽然 ntpd 不会使用大量网络带宽,但让你网络中的每个服务器查询公共 NTP 服务器是浪费网络资源——既是你自己的资源,也是时间服务器捐赠者的资源。这还可能导致你自己网络中的时间出现非常微小(亚秒级)的波动。

可靠的时间服务器不是虚拟机。Tier 1 NTP 服务器都运行在真实硬件上,专门避免虚拟机可能遇到的时钟抖动问题。

我建议为你的网络设置三个权威的时间服务器。让这些服务器与全球 NTP 池同步它们的时钟。配置你网络中的每个服务器指向这些服务器进行 NTP 更新。这样,你网络中的每个时钟都会完美同步。你无需翻遍 NTP 日志,试图判断全球时间服务器池中的某个特定服务器是否以某种方式破坏了你的系统时钟。最好通过网络边界的防火墙规则强制执行这一策略;只允许你的时间服务器与外部 NTP 服务器通信,这样可以消除一个常见的时间混乱源。

名称服务切换

任何类 Unix 系统都会进行无数次对不同名称服务的检查。我们已经讨论过将主机名映射到 IP 地址的域名系统(请参见第七章),但还有密码条目查找服务、TCP/IP 端口号和名称查找服务、IP 协议名称和编号查找服务等。你可以使用/etc/nsswitch.conf来配置你的 FreeBSD 系统如何进行这些查询以及它通过 nsswitch(名称服务切换)使用哪些信息来源。

每个名称服务都有一个nsswitch.conf条目,包含该服务的类型以及它所使用的信息来源。我们之前在第八章中看到过一个名称服务切换的示例。记得这个主机查找的条目吗?

hosts: files dns

这意味着,“首先在本地文件中查找 IP 地址,然后查询 DNS。”其他信息来源也类似工作。像大多数其他类 Unix 操作系统一样,FreeBSD 支持表 20-1 中列出的信息来源的名称服务切换。

表 20-1: 支持名称服务切换的查找

查找 功能
groups 用户组成员资格检查(/etc/group
hosts 主机名和 IP 检查(DNS 和/etc/hosts
networks 网络条目(/etc/networks
passwd 密码条目(/etc/passwd
shells 有效 Shell 的检查(/etc/shells
services TCP 和 UDP 服务 (/etc/services)
rpc 远程过程调用 (/etc/rpc)
proto TCP/IP 网络协议 (/etc/protocols)

大多数情况下,你不希望去修改这些,除非你喜欢破坏系统功能。例如,如果你有一个 Kerberos 或 NIS 域,你可能希望将你的 FreeBSD 机器连接到它们,以获取用户和组信息——但如果没有,重新配置密码查找会让系统运行得很慢,甚至在最坏的情况下完全无法工作!

对于每个名称服务,你必须指定一个或多个信息来源。许多名称服务非常简单,默认只有一个权威信息来源——一个文件。其他服务,如主机的名称服务,则更为复杂,有多个来源。一些服务非常复杂,仅仅是因为可用的信息种类繁多,并且获取这些信息的方式多种多样。由于本书没有涉及 Kerberos、NIS 或其他企业级用户管理系统,因此我们不会讨论更改密码、组和 shell 信息来源。如果你在这样的环境中,阅读 nsswitch.conf(5) 获取详细信息。

最常见的服务有特定的有效信息来源。文件是包含服务信息的标准文本文件。例如,网络协议通常存储在/etc/protocols,网络服务存储在/etc/services,密码存储在/etc/passwd及其相关文件中。dns来源意味着信息可通过 DNS 服务器获取,这对于负责将主机名映射到 IP 地址的主机服务来说是典型的。密码服务通常使用compat,它与/etc/passwd和 NIS 兼容,但也可以使用files。你可能会将信息源添加到系统中——例如,启用 LDAP 身份验证会添加ldap信息源。

按照你希望它们被尝试的顺序列出每个所需的信息来源。我们的 hosts 条目告诉名称服务查找首先尝试本地文件,然后查询 DNS 服务器。

hosts: files dns

如果你部署了像 LDAP 这样的集中身份验证方案,你需要添加适当的条目,告诉主机从 LDAP 查找密码和组。关键问题是,主机应该先使用本地密码文件,然后再回退到 LDAP,还是从 LDAP 开始,再回退到密码文件?

passwd: ldap files

在这里,我们首先使用 LDAP,但如果 LDAP 不可用,则回退到密码文件。

inetd

inetd(8) 守护进程处理不常用网络服务的传入网络连接。大多数系统没有持续不断的 FTP 请求流量,那么为什么要让 FTP 守护进程一直运行呢?相反,inetd 监听网络上的传入 FTP 请求。当 FTP 请求到达时,inetd(8) 启动 FTP 服务器并交接请求。其他依赖 inetd 的常见程序有 telnet、tftp 和 POP3。

Inetd 还处理一些非常小且不常用的功能,这些功能实现起来比通过独立程序更容易。包括 discard(将接收到的任何数据丢弃到黑洞 /dev/null)、chargen(输出一串字符)以及其他功能。现在,这些服务大多不仅不再需要,反而被认为是有害的。例如,chargen 服务主要用于拒绝服务攻击。

INETD 安全性

一些系统管理员将 inetd 看作是一个具有单一安全配置文件的服务,另一些则认为 inetd 有不好的安全历史。两者都不完全正确。inetd 服务器本身是相当安全的,但它要为转发请求到的程序承担一定的责任。一些 inetd 支持的服务,比如 ftp、telnet 等,本身就不安全,而其他一些则有过不良的历史并因此出现问题(例如 popper)。像对待任何其他网络服务器程序一样对待 inetd:除非需要,否则不要运行 inetd,并确认它仅提供受信任且安全的程序!

/etc/inetd.conf

看一下 /etc/inetd.conf。大多数守护进程有独立的 IPv4 和 IPv6 配置,但如果你没有运行 IPv6,可以忽略 IPv6 条目。我们来看一个条目——FTP 服务器的配置。

➊ftp    ➋stream     ➌tcp     ➍nowait     ➎root      ➏/usr/libexec/ftpd     ➐ftpd -l

第一个字段是服务名称 ➊,必须与 /etc/services 中的名称匹配。inetd 会执行服务名称查找,以确定它应该监听哪个 TCP 端口。如果你想改变 FTP 服务器运行的 TCP/IP 端口,可以在 /etc/services 中更改 FTP 的端口。(你也可以将第一个字段改为匹配所需端口上运行的服务,但我发现这样会让条目稍微有些混乱。)

套接字类型 ➋ 决定了这是什么类型的连接。所有的 TCP 连接都是 stream 类型,而 UDP 连接是 dgram 类型。虽然你可能会发现其他可能的值,但如果你正在考虑使用它们,要么是你在阅读一个告诉你应该使用什么的文档,要么就是你理解错了。

协议 ➌ 是第 4 层网络协议,可以是 tcp(IPv4 TCP)、udp(IPv4 UDP)、tcp6(IPv6 TCP)或 udp6(IPv6 UDP)。如果你的服务器同时接受 IPv4 和 IPv6 连接,可以使用 tcp46udp46

下一个字段指示 inetd 是否应该等待服务器程序关闭连接,还是只启动程序然后退出 ➍。一般来说,TCP 守护进程使用nowait,而 UDP 守护进程需要wait。(虽然有一些例外,但它们很少见。)inetd(8)会为每个传入请求启动一个新的网络守护进程。如果服务使用nowait,你可以通过在nowait后面直接添加斜杠和数字(例如:nowait/5)来控制 inetd 每秒接受的最大连接数。一种入侵者(通常是脚本小子)尝试将服务器从互联网上击倒的方式是为某个服务打开比服务器能够处理的更多的请求。通过限制传入连接的速率,你可以阻止这种情况。另一方面,这意味着你的入侵者可以完全阻止其他人使用该服务。小心选择你的“毒药”!

接下来是服务器守护进程运行的用户 ➎。FTP 服务器 ftpd(8)以 root 身份运行,因为它必须为多个系统用户提供服务,但其他服务器则以专用用户身份运行。

第六个字段是连接请求到达时,inetd 运行的服务器程序的完整路径 ➏。与 inetd(8)集成的服务作为内部服务出现。

最后一个字段给出了启动外部程序的命令,包括任何所需的命令行参数 ➐。

配置 inetd 服务器

虽然/etc/inetd.conf似乎使用了很多信息,但添加一个程序实际上是相当简单的。了解 inetd(8)的最简单方法是实现一个简单的服务。例如,我们来实现一个每日名言(qotd)服务。当你连接到 qotd 端口时,服务器会返回一个随机名言并断开连接。FreeBSD 在其游戏集合中包含了一个随机名言生成器 fortune(1)。这个随机名言生成器就是我们实现基于 inetd 的网络程序所需要的全部内容。我们必须指定端口号、网络协议、用户、路径和命令行。

端口号

/etc/services文件将 qotd 列在 17 端口。

网络协议

qotd 服务要求你连接到网络端口并获取回应,因此它需要通过 TCP 运行。记住,UDP 是无连接的——不需要回复。我们必须在 inetd 配置中指定tcp,这意味着我们必须在第四个字段中指定nowait

用户

最佳实践建议创建一个没有特权的用户来运行 qotd 服务,如第十九章中讨论的那样。对于这个例子,我们将使用一般的无特权用户 nobody,但如果你在生产环境中实现这一点,你应该创建一个无特权用户 qotd。

路径

/usr/bin/fortune中找到 fortune。

运行命令

fortune(6) 不需要任何命令行参数,但如果你愿意,可以添加它们。^(4) 在 FreeBSD 11 上,信奉墨菲定律的人可以使用 fortune murphy,而 星际迷航 的粉丝可以通过 fortune startrek 获取引用。(后者正确地只包括唯一的 星际迷航,而不包括任何跟风的续集。)那些对教育感兴趣的人可以使用 fortune freebsd-tips。遗憾的是,FreeBSD 12 删除了许多 fortune 数据库。

inetd.conf 配置示例

将这些内容结合起来,qotd 在 /etc/inetd.conf 中的条目如下所示:

qotd    stream    tcp    nowait    nobody    /usr/bin/fortune    fortune

你可能觉得这个例子很简单,但通过 inetd(8) 提供其他服务并不更加困难。

启动 inetd(8)

首先,通过将以下条目添加到 /etc/rc.conf 中来在启动时启用 inetd(8):

inetd_enable=YES

设置完毕后,手动启动 inetd,命令为 /etc/rc.d/inetd start。现在 inetd 已经在运行,使用 telnet 连接到 17 端口来测试我们的新服务:

   # telnet localhost 17
➊ Trying 127.0.0.1...
   Connected to localhost.
   Escape character is '^]'.
➋ It is difficult to produce a television documentary that is both
   incisive and probing when every twelve minutes one is interrupted by
   twelve dancing rabbits singing about toilet paper.
                   -- Rod Serling
   Connection closed by foreign host.

它工作了!我们得到了常见的 TCP/IP 连接信息 ➊ 和我们的随机格言 ➋。(额外的奖励是,你也知道为什么我不为电视写作。)

更改 inetd 的行为

inetd 根据你设置的标志表现不同。默认标志启用 TCP 包装器,按照 /etc/hosts.allow 中的配置(见 第十九章)。表 20-2 列出了一些有用的标志。

表 20-2: inetd(8) 标志

标志 描述
-l 记录每个成功的连接。
-c 设置每秒钟最多可以连接到任何服务的次数。默认情况下没有限制。注意,“无限制”并不等同于“无限”——你的硬件只处理这么多连接。
-C 设置一个 IP 地址每分钟最多可以连接某个服务的次数。默认情况下,连接速率是无限制的,但使用此选项可以防止有人试图独占你的带宽或资源。
-R 设置任何服务每分钟最大可启动的次数。默认值是 256。如果使用 -R 0,则允许对任何服务进行无限次连接。
-a 设置 inetd(8) 绑定的 IP 地址。默认情况下,inetd 会监听系统上所有附加的 IP 地址。
-w 按照 hosts.allow 中的配置,使用 TCP 包装器来启动 inetd(8) 启动的程序(见 第十九章)。
-W 按照 hosts.allow 中的配置,使用 TCP 包装器为与 inetd(8) 集成的服务提供保护(见 第十九章)。

作为一个极端的例子,如果你想使用 TCP 包装器,每秒钟只允许单一主机连接两次,允许每分钟无限次调用服务,并且仅监听 IP 地址 203.0.113.2,那么你需要在 /etc/rc.conf 中设置如下内容:

inetd_flags="-Ww -c 2 -R 0 -a 203.0.113.2"

使用 inetd(8),几乎任何东西都可以是网络服务。

DHCP

动态主机配置协议(DHCP) 是为客户端计算机分配 IP 地址的标准方法。虽然 FreeBSD 默认没有集成 DHCP 服务,但它通常是实现如无盘工作站等服务所必需的。我们将在这里介绍 DHCP 配置的基本内容,帮助你设置自己的网络。

现在,几乎每个防火墙和嵌入式设备都有一个 DHCP 服务器。那么为什么还需要单独的 DHCP 服务器呢?大多数嵌入式 DHCP 服务器缺少运行无盘客户端所需的功能,如网络启动的服务器和 VoIP 电话。当它们支持这些功能时,往往很难管理。这些服务应该运行在真正的服务器上。我们将在此介绍足够的 DHCP 内容,帮助你配置自己的网络客户端,包括无盘主机。

FreeBSD 包含了几种 DHCP 服务器。我比较喜欢 OpenBSD 的 dhcpd 和 ISC 的 DHCP 服务器。ISC DHCP 服务器 是行业标准,支持你可能需要的每一个功能。对于小规模部署,我推荐使用 OpenBSD 的 dhcpd。OpenBSD 的开发者在 ISC DHCP 的基础上,去掉了所有那些不常用的功能,制作了一个更小、更简单的服务器。配置文件仍然是单向兼容的;你可以在 ISC 的 DHCP 服务器上运行 OpenBSD 的 dhcpd 配置文件,不会有任何问题。(反之亦然,如果你没有使用 OpenBSD 去掉的那些功能。)如果你想运行无盘的 FreeBSD 客户端,或者需要 LDAP 集成,切换到更复杂的 ISC 服务器也是相对简单的。你只能安装这两者中的一个服务器。

这两款服务器的包都包含 dhcpd(8),配置文件 /usr/local/etc/dhcpd.conf,以及详尽的 man 页。

流氓 DHCP 服务器

每个网络应当只有一组权威的 DHCP 信息。如果你在已经有 DHCP 服务器的网络上设置自己的 DHCP 服务器,比如在公司办公室,可能会导致大量客户端无法正常工作,并引发一堆电话打到网络团队那里。设置一个“流氓”DHCP 服务器是让网络团队从此忽视你所有求助请求的绝佳方式。

DHCP 工作原理

在一个需要在各个办公室之间转发 DHCP 请求的大型网络中,DHCP 可能会变得非常复杂,但在本地以太网上,它相对简单。每个 DHCP 客户端会向本地以太网广播请求,寻找任何人来提供网络配置信息。如果你的 DHCP 服务器在本地以太网上,它会直接做出响应。如果你的 DHCP 服务器在另一个网络段,那个网络段的路由器需要知道该将 DHCP 请求转发到哪个 IP 地址。DHCP 服务器会将配置文件借给客户端,并追踪哪些客户端被分配了哪些 IP 地址。分配给客户端的配置文件叫做 租约。就像你为房屋或汽车支付的租金一样,DHCP 租约会到期,并且需要定期续租。

客户端可以请求某些功能——例如,Microsoft 客户端请求 WINS 服务器的 IP 地址,而无盘系统请求查找内核的位置。你可以根据需要设置这些选项。

每个客户端通过用于连接网络的网卡的 MAC 地址唯一标识。ISC dhcpd 在文件/var/db/dhcpd.leases中跟踪 MAC 地址和 IP 地址,以及租约。在这个文件中,你可以识别出哪些主机拥有哪些 IP 地址。如果一个主机暂时离开网络并重新连接,dhcpd(8)会重新分配相同的 IP 地址给该客户端,如果该 IP 地址仍然可用。

配置 dhcpd(8)

文件/usr/local/etc/dhcpd.conf包含了所有关于 dhcpd 的配置。虽然 ISC dhcpd(8)本身就可以写成完整的书籍,我们将在这里重点关注适用于基本小型办公室的功能以及本书后续示例中使用的功能。默认的dhcpd.conf文件注释齐全,还包括更多的示例,而 dhcpd.conf(5)则详细到令人痛苦的程度。我们假设你在网络上运行一个单独的 DHCP 服务器,且该服务器应答所有 DHCP 服务请求。(当然,也可以为容错集群化 dhcpd,但这超出了我们的讨论范围。)

全局设置

从一些客户端配置的通用规则开始你的dhcpd.conf文件。这些规则适用于所有 DHCP 客户端,除非被明确覆盖。

➊ option domain-name "mwl.io";
➋ option domain-name-servers 198.51.100.2, 198.51.100.3;
➌ option subnet-mask 255.255.255.0;
➍ default-lease-time 600;
➎ max-lease-time 7200;

每个 DHCP 客户端会向 DHCP 服务器注册其主机名,但客户端必须从服务器学习本地域名。(也可以让 DHCP 服务器设置客户端的主机名。)使用domain-name选项 ➊来设置这个。你可以给你的 DHCP 客户端任何你喜欢的域名;它们不需要与服务器的域名相同。如果用空格分隔,你可以包含多个域名,但并非所有操作系统都会识别额外的域名。

每个 TCP/IP 客户端需要一个或多个 DNS 服务器。使用domain-name-servers选项 ➋来指定它们。多个 DNS 服务器用逗号分隔。

设置默认子网掩码 ➌是一个好主意。各个网络可以覆盖此设置,但全局默认值非常有用。

租约的正常持续时间由default-lease-time选项➍(以秒为单位)给出。租约时间到期后,客户端会向 DHCP 服务器请求新的 DHCP 租约。DHCP 服务器通常默认设置为几分钟,但如果你的网络相对稳定,你可以将其延长至几个小时或几天。如果客户端无法连接到 DHCP 服务器,它将继续使用旧的租约,直到达到租约的最大有效时间,该时间由max-lease-time ➎指定。你可以将最大租约时间理解为“如果我的 DHCP 服务器失败了,这是我在电话开始响起之前,必须更换它的时间。”给自己足够的时间来解决问题。^(5)

现在定义子网。

子网设置

网络上的每个子网都需要一个 subnet 声明来识别该子网上 DHCP 客户端的配置信息。例如,这是一个单个小型办公室网络的网络声明:

➊ subnet 198.51.100.0 netmask 255.255.255.0 {
  ➋ range 198.51.100.50 198.51.100.99;
  ➌ option routers 198.51.100.1;
  }

每个子网声明开始时会标识该子网的网络号和子网掩码 ➊。在这里,我们有一个使用 IP 网络号 198.51.100.0 和子网掩码 255.255.255.0 的子网,或 IP 地址 198.51.100.1 到 198.51.100.255。随后的信息都与该子网上的主机有关。

range 关键字 ➋ 用于标识 dhcpd(8) 可以分配给客户端的 IP 地址。在这个例子中,我们有 50 个可供客户端使用的 IP 地址。如果在任何租约到期之前有 51 个 DHCP 客户端连接,最后一个主机将无法获得地址。

使用 routers 选项 ➌ 定义默认路由。请注意,您不能通过 dhcpd(8) 定义额外的路由;相反,您的本地网络路由器需要有适当的路由来到达目标。如果您的本地网络上有多个网关,您的网关会向 DHCP 客户端发送 ICMP 重定向,告知它更新后的路由。(如果您不知道这是什么意思,没关系。当您需要时,您会突然理解我在说什么,如果您永远不需要它,那么您刚刚浪费了两秒钟的时间去读这句话。)

如果您有多个子网,创建多个子网声明。一些子网可能需要不同于全局默认值的设置,例如子网掩码或 DNS 服务器。如果是这样,请使用相同的关键字为该子网定义这些值。

Dhcpd 允许您设置从子网掩码、启动服务器到甚至为古老 Windows 客户端配置 WINS 服务器的所有内容。我们将在第二十三章中使用这些不太常见的设置来管理无磁盘客户端。有关详细信息,请参阅 dhcpd.conf(5)。

管理 dhcpd(8)

Dhcpd 默认监听所有网络接口以捕捉 DHCP 请求广播。然而,我曾运行过许多带有多个网络卡的 DHCP 服务器,通常我只希望 dhcpd 监听单个接口。将所需的接口作为命令行参数提供。

sysrc dhcpd_flags="em1"

现在启用 dhcpd(8) 本身。

sysrc dhcpd_enable=YES

现在,您可以通过 service dhcpd start 启动 dhcpd。

恭喜,您已经准备好了!

打印和打印服务器

在类 Unix 操作系统上,打印是一个让新手系统管理员哭泣的主题,而经验丰富的系统管理员则会喋喋不休地谈论过去打印机作为 TTY 设备的时代,以及年轻一代不懂得自己有多幸运。^(6) 最常见的打印情境是通过 USB 端口直接连接到计算机的打印机和连接到网络打印服务器的打印机。

如果你有一台直接连接到 FreeBSD 机器的打印机,比如通过 USB 电缆连接,我建议使用 Common Unix Printing System (CUPS)。这套软件管理许多流行的消费级和商用打印机,从普通的喷墨打印机到大规模的激光打印机。我不会详细讲解 CUPS,因为它很复杂,并且根据打印机型号有所不同。你可以在 www.cups.org/ 上了解更多关于 CUPS 的信息。许多品牌的打印机在 CUPS 中有专门的设置程序,比如 HP 的 hp-setup。不过,如果你的打印机支持网络连接,建议避免使用 CUPS,而改用网络打印。

通过 Line Printer Spooler Daemon (LPD) 访问远程打印服务器或网络打印机相比之下更为简单。LPD 接收 PostScript 文件并生成打印输出。大多数办公打印服务器都运行 LPD。lpd(8) 守护进程管理着 LPD。大多数现代网络打印机也支持 LPD,并且可以作为独立的打印服务器使用。

通过连接到 TCP 端口 515 来测试是否支持 LPD;如果能够连接,说明设备支持 LPD。

# nc -v color-printer 515
Connection to color-printer 515 port [tcp/printer] succeeded!

该设备支持 LPD。我们可以通过配置 /etc/printcap 将打印任务发送到它。

/etc/printcap

每个系统已知的打印机都需要在 /etc/printcap 中有一个条目,即打印机能力数据库。这个文件按照现代标准来看,格式相当晦涩,且对于没有接触过 termcap(5) 的人来说,可能显得非常陌生。幸运的是,要访问打印服务器,你并不需要理解 printcap(5),你只需要使用以下模板。

要连接到打印服务器上的打印机,你必须知道打印服务器的主机名或 IP 地址,以及你想访问的打印机名称。根据以下模板在 /etc/printcap 中创建条目。特别注意冒号和反斜杠——它们至关重要。

➊ lp|printername:\
       ➋ :sh=:\
       ➌ :rm=printservername:\
       ➍ :sd=/var/spool/output/lpd/printername:\
       ➎ :lf=/var/log/lpd-errs:\
       ➏ :rp=printername:

我们的第一行显示了打印机的名称 ➊。如果你从 LibreOffice 或图形化的网页浏览器中打印,这些名称会显示为打印机选项。每台打印机可以有多个名称,用管道符号(|)分隔。任何类 Unix 系统上的默认打印机都叫 lp,因此将其列为你首选打印机的名称之一。另一个名称应该是打印服务器为你的打印机设置的名称(例如,3rdFloorPrinter)。需要注意的是,微软的打印服务器经常将一台打印机共享为多个不同的名称,并且通过不同的名称处理打印任务。如果你在网络上发现这种情况,请确保选择 PostScript 名称。^(7)

默认情况下,lpd(8) 会在每个打印任务前打印一页,列出任务名称、编号、主机及其他信息。除非你处于一个只有一个共享打印机的环境,否则这可能是浪费纸张。:sh:\ 条目 ➋ 会抑制此页面的打印。

rm(远程机器)变量 ➌ 提供打印服务器的主机名。你必须能够通过你在这里指定的名称 ping 到该服务器。如果打印服务器是打印机的一部分,请在此处提供打印机的主机名。

每台打印机都需要一个唯一的打印队列目录 ➍,打印守护进程可以在此目录中存储传输到打印服务器的文档。此目录必须由 root 用户和 daemon 组拥有。

与必须不同的打印队列目录不同,打印机可以共享一个公共日志文件 ➎。

最后,指定远程打印机名称 ➏,因为打印服务器是通过该名称识别的。如果你直接连接到打印机,而不是连接到中央打印服务器,可以跳过此项—but 你必须删除前一行中的尾部斜杠。

确保你在 /etc/printcap 文件末尾添加新行;不要在打印机名称后立即终止文件。另外,请注意,和模板中的其他条目不同,最后一行不需要尾部的反斜杠。

打印机有许多选项,从每页的费用到手动设置字符串以进纸新的纸张。大多数这些选项今天已经过时。然而,如果你有一台较老的打印机或有特殊需求,可以查阅 printcap(5) 文档,获取足够的详细信息,甚至让人应接不暇。

启用 LPD

/etc/rc.conf 中将 lpd_enable 设置为 YES,以便在启动时启动 lpd(8)。每次编辑 /etc/printcap 时,必须重启 lpd(8)。使用 lpq(1) 查看打印队列,并在 /var/log/lpd-errs 中查看是否有任何问题。

TFTP

让我们结束关于小型网络服务的讨论,也许是仍然使用的最小网络服务——简单文件传输协议(TFTP)。TFTP 允许你在机器之间传输文件,而无需任何身份验证。它的灵活性也远不如文件复制协议,如 SCP 或 FTP。然而,TFTP 仍然被嵌入式设备制造商(如思科)用来加载系统配置和操作系统更新。我们在这里提到它,仅仅是因为无盘客户端使用 TFTP 下载操作系统内核并获取初始配置信息。在 TCP 端口 69 上通过 inetd(8) 运行 tftpd(8)。

TFTP 安全

TFTP 不适合在公共互联网中使用。任何人都可以在 TFTP 服务器上读取或写入文件!仅应在防火墙后使用 TFTP,或者至少使用 TCP 包裹器(参见 第十九章)保护它。

设置 tftpd(8) 服务器包括四个步骤:为服务器选择根目录、为服务器创建文件、选择文件的所有者,以及运行服务器进程。

根目录

tftpd(8) 守护进程默认使用目录 /tftpboot。如果您只有几个很少访问的文件,这可能是合适的选择,但最好将根分区保留给那些不常更改的文件。您不希望通过 TFTP 上传文件导致根分区填满,从而崩溃系统!如果您使用的是 ZFS,可以创建一个 tftp 数据集。在 UFS 中,我通常将 tftpd(8) 的根目录放在 /var/tftpboot,并添加一个指向 /tftpboot 的符号链接:

# mkdir /var/tftpboot
# ln -s /var/tftpboot /tftpboot

现在您可以创建文件并通过 TFTP 进行访问。

tftpd 和文件

用户可以通过 TFTP 进行文件的读取和写入。如果您希望 tftpd(8) 用户能够读取某个文件,该文件必须是全局可读的:

# chmod +r /var/tftproot/filename

同样,tftpd(8) 不允许任何人上传文件,除非该文件名已经存在且是全局可写的。请记住,程序和常规文件具有不同的权限。程序必须具备执行权限,除了读取和写入权限,因此您需要为程序和文件设置不同的权限。您可以使用 touch(1) 命令预先创建您希望通过 TFTP 上传的文件。

# chmod 666 /var/tftproot/filename
# chmod 777 /var/tftproot/programname

是的,这意味着任何知道文件名的人都可以覆盖该文件的内容。请将重要文件设置为只读。^(8) 这也意味着您无需担心有人上传大文件并占满您的硬盘。

文件所有权

TFTP 服务器中的文件应该由具有最低权限的用户拥有。如果您只偶尔运行 TFTP 服务器,可以使用 nobody 用户。例如,如果您只需要 TFTP 服务器执行偶尔的嵌入式设备升级,可以让 nobody 用户拥有您的文件,并在不需要时关闭 tftpd(8)。然而,如果您运行的是永久性的 TFTP 服务器,最好让专门的 tftp 无权限用户拥有文件。tftp 用户不需要拥有 tftproot 目录,实际上,应该有一个完全不同的主目录。他只需要拥有可供用户访问的文件的所有权。

tftpd(8) 配置

tftpd(8) 完全通过命令行参数进行配置,且参数不多。有关完整列表,请阅读 tftpd(8),以下是最常用的几个。

如果您仅为运行 tftpd(8) 创建用户,请使用 -u 参数指定该用户。如果不指定用户,tftpd(8) 将以 nobody 身份运行。请创建一个无权限用户。

我建议记录所有对 TFTP 守护进程的请求。-l 参数会启用日志记录功能。tftpd(8) 使用 FTP 功能,您需要在 syslog.conf 中启用该功能(请参见 第二十一章)。

tftpd 支持使用 -s 标志进行 chroot 限制。这可以让您将 tftpd(8) 限制在您选择的目录中。您可不希望用户通过 TFTP 上传诸如 /etc/passwd/boot/kernel/kernel 这样全局可读的文件,这是出于一般原则的考虑!始终对您的 tftpd(8) 安装进行 chroot 限制。

你可以通过 -c 参数按 IP 地址为 TFTP 客户端设置 chroot。在这种情况下,你必须为每个允许连接的客户端创建一个目录。例如,假设你只想允许你的路由器通过 IP 地址 192.168.1.1 访问 TFTP。你可以创建一个目录 /var/tftproot/192.168.1.1 并使用 -c。你还必须使用 -s 来定义 /var/tftproot 的基本目录。这是一种不错的折衷方法,当你只想为一两个主机提供 TFTP 服务时,但又不希望让全球用户都能访问你的 TFTP 服务器。

你可以选择允许客户端向 TFTP 服务器写入新文件。这是一个不好的主意,因为它允许远程用户用任意文件填满你的硬盘。如果你确实需要这个功能,请使用 -w 标志。

例如,假设你想记录所有对 tftpd 的请求,将根目录改为 /var/tftpboot,以 tftpd 用户身份运行服务器,并按 IP 地址为客户端设置 chroot。运行 tftpd 的命令可能是这样的:

tftpd -l -u tftpd -c -s /var/tftpboot

按照本章前面描述的方式,将此内容输入到inetd.conf中,重启 inetd(8),然后就可以开始使用了!

调度任务

FreeBSD 的作业调度器 cron(8) 允许管理员定期运行任何命令。结合系统维护调度系统 periodic(8),你几乎可以调度任何任务。

cron(8)

如果你需要每天夜间备份数据库,或者一天四次重新加载 DNS 服务器,cron 是你的好帮手。cron(8) 的配置文件称为 crontabs,并通过 crontab(1) 进行管理。每个用户都有一个单独的 crontab 文件,存储在 /var/cron/tabs 中,全局 crontab 文件是 /etc/crontab。全局 cron 条目也可以放在 /etc/cron.d 中,并将像 /etc/crontab 中的一部分那样运行。

用户 Crontab 与 /etc/crontab

/etc/crontab 的目的与各个用户的 crontab 不同。使用 /etc/crontab 时,root 用户可以指定某个命令由哪个用户执行。例如,在 /etc/crontab 中,系统管理员可以指定,“在每周二晚上 10 点以 root 用户身份运行此任务,在早上 7 点以 www 用户身份运行另一个任务。”其他用户只能以自己的身份运行任务。当然,root 用户也可以编辑其他用户的 crontab。

此外,任何系统用户都可以查看 /etc/crontab。如果你有一个不希望用户知道的定时任务,可以将其放在用户的 crontab 中。例如,如果你为数据库创建了一个非特权用户,可以使用该用户的 crontab 来运行数据库维护任务。

/etc/crontab 被认为是一个 FreeBSD 系统文件。在升级时不要覆盖它!简化升级/etc/crontab的一种方法是将自定义条目放在文件末尾,并用几行井号(#)标记。/etc/crontab 文件必须以新行结尾,否则最后一行将不会被解析并执行。如果最后一行是注释,这样没有问题,但如果它是命令,则可能会出现问题。

最后,当你使用文本编辑器编辑/etc/crontab时,使用crontab -e来编辑用户的 crontab。

cron 和环境

Crontab 在一个 shell 中运行,程序可能需要环境变量才能正确运行。你还可以在命令行中为每个从 cron 运行的命令指定环境变量。cron 不会从任何地方继承环境变量;程序需要的任何环境变量必须在 crontab 中指定。例如,以下是 FreeBSD 12 系统中/etc/crontab的环境:

SHELL=/bin/sh
PATH=/etc:/bin:/sbin:/usr/bin:/usr/sbin

是的,这确实是非常简洁!可以根据需要在用户 crontab 中添加环境变量,但在更改/etc/crontab时要谨慎。如果需要自定义环境变量,最好使用用户 crontab 而不是/etc/crontab,因为/etc/crontab中的许多命令是用于核心系统维护的。

Crontab 格式

在环境声明下面,用户的 crontab 分为六列。前五列表示命令应运行的时间,依次是分钟、小时、日期、月份和星期几。如果某列为星号(*),表示每一个,而数字则表示在此精确时刻。分钟、小时和星期几从 0 开始,日期和月份从 1 开始。此外,由于 AT&T 和 BSD 之间的历史分歧,星期天可以用 7 或 0 表示。在时间之后,列出在该时间运行的命令。

/etc/crontab文件和/etc/cron.d下的文件有一个额外的列:指定以哪个用户身份运行命令。这个用户列位于时间说明和命令之间。如果你愿意,可以查看/etc/crontab中的许多示例。

示例 Crontab

假设我们正在编辑一个无权限用户的 crontab,以安排程序的维护。由于/etc/crontab文件顶部有列标题,这里我们将展示用户 crontab。(如果要在/etc/crontab中使用这些示例,只需在命令前加上用户即可。)在这里,我们希望每小时的第 55 分钟运行程序/usr/local/bin/maintenance.sh

55    *    *    *    *    /usr/local/bin/maintenance.sh

星号告诉 cron 每小时、每个月的每一天以及每个工作日运行这个作业。55告诉 cron 仅在第 55 分钟运行这个作业。

要每天在下午 1:55 运行相同的作业,可以使用以下配置:

55    13    *    *    *    /usr/local/bin/maintenance.sh

这里,13表示 24 小时制下的下午 1:00,55表示该小时的第 55 分钟。

使用 cron 时,人们常犯的一个错误是指定了较大的时间单位,却漏掉了较小的单位。例如,假设你想每天上午 8 点运行作业:

*    8    *    *    *    /usr/local/bin/maintenance.sh

这是错误的。是的,作业将在上午 8:00 运行。它也会在 8:01、8:02、8:03 等时间运行,一直到上午 9:00。如果你的作业运行超过一分钟,你很快就会让系统崩溃。指定仅在上午 8:00 运行的方法是这样的:

0    8    *    *    *    /usr/local/bin/maintenance.sh

要指定时间范围,例如每小时、每天在上午 8 点到下午 6 点之间的某个时间运行程序,可以使用类似这样的语法:

55    8-18    *    *    1-5    /usr/local/bin/maintenance.sh

要指定多个精确的时间,使用逗号分隔:

55    8,10,12,14,16    *    *    *    /usr/local/bin/maintenance.sh

更有趣的是,你可以指定时间的分数或 步长。例如,要每隔 5 分钟运行一个程序,请使用:

*/5    *    *    *    *    /usr/local/bin/maintenance.sh

你可以将范围与步长结合使用。要每隔 5 分钟运行一次程序,但比上一个示例延迟 1 分钟,请使用以下命令:

1-56/5    *    *    *    *    /usr/local/bin/maintenance.sh

用两个字段控制任务运行的日期:月中的日期和星期几。如果你同时指定这两个条件,任务将在 任一 条件满足时运行。例如,告诉 cron 在每个月的 1 号和 15 号,以及每个星期一运行任务,命令如下:

55    13    *    1,15    1    /usr/local/bin/maintenance.sh

如果你的任务有非标准环境变量,请像在 shell 中一样在命令行上设置环境变量。例如,如果你的程序需要 LD_LIBRARY_PATH 环境变量,可以这样设置:

55    *    *    *    *    LD_LIBRARY_PATH=/usr/local/mylibs ; /usr/local/bin/maintenance.sh

cron 还支持特殊的调度,如 每年一次每天,通过 @ 符号。大多数这些术语最好不要使用,因为它们可能会产生歧义。虽然机器确切知道它们的含义,但人类往往会误解!一个有用的 crontab 条目是 每次系统启动时,其表示为 @reboot。这允许一个非特权用户在系统启动时运行任务。使用 @reboot 标签代替时间字段:

@reboot    /usr/local/bin/maintenance.sh

Crontabs 和 cron(8) 让你以任何你喜欢的方式调度工作,从而将人类从许多常规维护任务中解放出来。

periodic(8)

一些系统维护任务应该只在特定系统上运行,但它们的运行方式在所有主机上是相同的。这就是 periodic(8) 的作用。

periodic(8) 命令按 cron(8) 确定的计划运行系统功能。Periodic 会检查一个目录中的脚本集合,看看是否需要运行。FreeBSD 包括几个用于定期任务的目录:/etc/periodic/daily/etc/periodic/weekly/etc/periodic/monthly/etc/periodic/security。根据你安装的包,你可能会在 /usr/local/etc/periodic 中有相应的目录。当 cron 运行 periodic daily 时,periodic(8) 会检查每个 periodic/daily 目录中的每个脚本,看看是否应该执行。

当你有空时,我建议你浏览一下 periodic(8) 脚本。你可能会发现禁用的维护脚本对你的环境很有用。

应该运行哪些脚本?默认设置列在 /etc/defaults/periodic.conf 中,但你可以在 /etc/periodic.conf 中覆盖它们。

一旦 periodic(8) 运行,它会将脚本的结果通过邮件发送给本地机器上的 root 用户。你可以将 root 的邮件转发给那些会实际阅读它的人。

为什么使用 periodic(8)?它的作用是进行系统维护。/etc/crontab 用于配置你自己的系统管理任务。使用单独的脚本可以让系统升级过程中替换任务,并且包可以添加或移除它们。

不过,所有的 periodic(8) 任务都是以 root 用户身份运行的。如果你有需要由权限较低的用户执行的定时任务,请从该用户的 crontab 中运行它们。

现在你已经对 FreeBSD 提供的常见小型服务有了相当的了解,我们接下来讲讲性能。

第二十一章:系统性能与监控

image

即使“它很慢!”不是系统管理员最害怕听到的话,它也排在列表的前面。用户不知道系统为什么慢,可能连问题到底是如何造成的都无法进一步量化或描述。它就是感觉慢。通常没有测试用例,也没有一组可重现的步骤,问题似乎也没什么特别的地方。慢速投诉可能导致几个小时的工作时间,因为你需要在系统中挖掘,寻找可能根本不存在的问题。

有一句话更让人恐惧,尤其是在你已经投入了大量工作时间之后:“它仍然很慢。”

一个缺乏经验的系统管理员通过购买更快的硬件来加速慢速系统。这将“速度问题”换成了昂贵的部件和更加昂贵的时间。升级只是让你掩盖问题,而没有真正利用你已经拥有的硬件,有时它们甚至根本无法解决问题。

你可以通过调整导致问题的软件来解决性能问题。你的网站在 WordPress 下很慢?可以考虑在 memcached 或其他 PHP 加速器下运行 PHP。FreeBSD 只是你的应用堆栈中的一层,所以一定要对其他层也给予适当关注。

FreeBSD 包含了许多旨在帮助你检查系统性能并提供必要信息的工具,帮助你找出真正导致系统变慢的原因。它们中的一些,例如 dtrace(1),非常复杂,要求对系统、软件有深入了解,甚至需要一本专门的书籍。一旦你明白了问题所在,找到问题的解决方案就变得简单多了。你可能确实需要更快的硬件,但有时候调整系统负载或重新配置软件可能会以更低的成本解决问题。在任何情况下,第一步都是理解问题。

计算机资源

性能问题通常是由于运行的任务超过了计算机能够处理的能力。这个看起来很显而易见,但请想一想,这到底意味着什么?

一台计算机有四种基本资源:输入/输出、网络带宽、内存和 CPU。如果其中任何一项达到容量限制,其他资源就无法发挥最大效能。例如,你的 CPU 可能在等待磁盘交付数据或等待网络数据包到达。如果你升级 CPU 以提高系统速度,可能会失望。购买一台全新的服务器或许能解决问题,但只是扩大了现有瓶颈。新系统可能比旧系统有更多内存、更快的磁盘、更好的网络卡和更快的处理器,你只是将问题推迟到性能达到新的极限。然而,通过识别系统中的短板并针对特定需求进行优化,你可以大大提升现有硬件的性能。毕竟,为什么要购买一整套新系统,如果几 GB 价格相对便宜的内存就能解决问题呢?(当然,如果你的目标是将这台“慢”系统退役并转做新的桌面,那又是另当别论了。)

输入/输出是一个常见的瓶颈。系统总线有最大吞吐量,虽然你可能没有把磁盘或网络推到极限,但如果不断地同时对它们进行大量操作,你可能会饱和总线。

系统变慢的一个常见原因是同时运行多个大型程序。磁盘 I/O 不仅会被饱和,处理器可能会花费大部分时间等待在 CPU 缓存和内存之间交换数据。例如,我曾经不小心安排了一个庞大的数据库日志轮换任务,同时移动和压缩几 GB 的数据,并且与每日的 periodic(8) 任务重叠。由于这个任务需要关闭主数据库并导致应用程序停机,速度变得至关重要。数据库任务和 periodic(8) 任务都变得无法忍受地慢。重新调度其中一个任务,使得两个任务都能更快速地完成。

FreeBSD 有一些可以提高性能的特性。如果你需要进行大量的加密操作,可以使用 aesni(4) 内核模块。数据库受限于磁盘性能?考虑文件系统的块大小。ZFS 池速度慢?也许你需要一个附加缓存。然而,确定应该更改什么需要对系统进行仔细的检查。

我们将介绍几个 FreeBSD 工具,用于检查系统性能。掌握这些信息后,我们将考虑如何解决性能问题。每个潜在的瓶颈都可以通过合适的工具进行评估。FreeBSD 不断变化,因此较新的系统可能会有新的调优选项和性能特性。请查阅系统中的 tuning(7) 手册,了解当前的性能优化建议。

什么是正常的?

本章中你会不断碰到一个词:异常。作为系统管理员,你应该了解你的系统的正常状态。它有点像艺术;你可能无法定义正常,但你需要在看到异常时能够识别它。经常使用这些工具,当系统运行正常时,你就能对系统减速时的异常结果有一个良好的认识。注意你的硬件!

检查网络

如果你担心网络性能,测量它。查阅net``stat -mnetstat -s,查看是否有错误或内存或缓冲区不足的地方。这些是瞬时快照,但对于网络,你实际上需要评估几分钟、几小时甚至几天的拥塞和延迟。网络团队可能有像 Cacti、Zabbix 或 Graphite 这样的工具来观察长期性能。^(1)向他们索取信息。将这些工具提供的数据与瞬时快照结合起来。如果你 10 吉比特以太网的每分钟平均吞吐量只有 5 吉比特每秒,但你的瞬时测量显示频繁的峰值达到满载的 10 吉比特,你可能面临非常突发的连接问题。

一些网卡在轮询模式下可以更好地处理满负载网络。轮询指示网卡停止将帧发送到操作系统,而是让操作系统定期访问收集这些帧。查阅网卡的手册页,查看它是否支持轮询。使用 ifconfig(8)启用和禁用轮询。

网络负载较重时,可能需要使用不同的拥塞控制算法。FreeBSD 提供了几种 TCP 拥塞控制算法。查找以cc_开头的文件,它们位于/boot/kernel目录中;这些是拥塞控制模块,每个模块都有一个手册页。

使用 sysctl net.inet.tcp.cc.available查看当前加载的拥塞控制算法。

# sysctl net.inet.tcp.cc
net.inet.tcp.cc.available: newreno

新的 Reno 是传统的拥塞控制算法。该系统上的拥塞控制内核模块包括 CDG、CHD、CUBIC、DCTCP、HD、H-TCP 和 Vegas。H-TCP 算法专为长距离、高带宽应用设计。让我们启用它。

# kldload /boot/kernel/cc_htcp.ko
# sysctl net.inet.tcp.cc.available
net.inet.tcp.cc.available: newreno, htcp

我们现在在内核中可以使用 H-TCP。通过net.inet.tcp.cc.algorithm sysctl 来启用它。

# sysctl net.inet.tcp.cc.algorithm=htcp
net.inet.tcp.cc.algorithm: newreno -> htcp

最终,你无法将 10 磅带宽放入一个 5 磅的电路中。如果你的饱和以太网正在拖慢应用程序的速度,关闭不必要的网络服务或增加更多带宽。

其他系统条件则要复杂得多。首先通过 vmstat(8)检查问题所在。

使用 vmstat(8)进行一般瓶颈分析

FreeBSD 包含多个用于检查系统性能的程序。其中包括 vmstat(8)、iostat(8)和 systat(1)。我们将讨论 vmstat(8),因为我发现它最有用;iostat(8)与 vmstat(8)相似,而 systat(1)则以 ASCII 图形格式提供相同的信息。

使用 vmstat(8)查看系统当前的虚拟内存统计数据。虽然输出结果需要一些时间来适应,但 vmstat(8)非常适合在小空间内展示大量数据。你可以在命令行输入 vmstat 并跟随进行。

# vmstat
procs  memory      page                    disks        faults     cpu
r b w  avm   fre   flt  re  pi  po    fr   sr ad0 ad1   in    sy    cs us sy id
8 0 0 1.3G   26G   157   0   1   0   172    1   0   0   12   212   149  0  0 100

vmstat 将显示分为六个部分:进程(procs)、memory、分页(page)、磁盘(disks)、故障(faults)和 CPU(cpu)。我们将快速浏览一下这些部分,然后详细讨论对于调查性能问题最重要的部分。这一行代表系统运行期间的平均值。在接下来的部分,我们会获取更多实时数据。

进程

vmstat(8)在procs标题下有三列。严格来说,vmstat 统计的是线程而不是进程。没有线程的应用程序每个进程有一个线程,但你的多线程应用程序可能会有更多的线程。

r 正在等待 CPU 时间的可运行线程数,包括所有运行中的进程。每个 CPU 一个线程是正常的,这意味着你的硬件得到了充分利用。如果超过这个数量,说明 CPU 成了瓶颈。不过,有些程序会要求主机的处理器全部投入,甚至更多;检查一下是否有这样的“计算黑洞”程序在运行。

b 正在等待系统输入或输出的阻塞线程数——通常是等待磁盘访问的线程。这些线程会在获取到数据后运行。如果这个数字很高,说明磁盘是瓶颈。

w 可运行但完全交换出的线程数。如果你经常有进程被交换出去,说明系统的内存无法满足主机的工作负载。

这台主机自启动以来平均有 8 个可运行线程,但没有等待 I/O 或内存。如果你收到该主机运行缓慢的抱怨,首先需要检查的就是处理器的利用率。是否有某个人在编译 FreeBSD 源代码,以生成书中性能章节的有趣输出,而真实用户却在同一系统上做着工作?

内存

FreeBSD 将内存分成大小均匀的块,称为页面。当程序请求内存时,会分配若干页面。页面的大小依赖于硬件和操作系统,但可以在hw.pagesize的 sysctl 中查看。在 FreeBSD 的 i386 和 amd64 平台上,页面大小是 4KB。系统将每个页面视为一个整体——例如,如果 FreeBSD 必须将内存换出,它会按页面逐一进行。管理内存的内核线程被称为分页守护进程memory部分有两列。

avm 正在使用的虚拟内存页面的平均数。如果这个值异常高或正在增加,说明系统正在积极消耗交换空间。

fre 可供使用的内存页面数。如果这个值异常低,说明你的系统内存不足。

我们的示例输出使用了 1.3GB 的 RAM,剩余 26GB 可用。内存不是问题。

分页

page 部分显示了虚拟内存系统的工作负载。虚拟内存系统的内部工作原理是一门晦涩的学问,我在这里不会详细描述。^(2)

flt 页面故障的数量,其中需要的信息不在真实内存中,必须从交换空间或磁盘中获取。

re 从缓存中回收或重用的页面数量。

pi 缩写自 pages in;表示从真实内存移动到交换区的页面数量。

po 缩写自 pages out;表示从交换区移动到真实内存的页面数量。

fr 每秒释放的页面数量。

sr 每秒扫描的页面数量。

将内存移入交换区并不坏,但如果不断地恢复已分页的内存,则表明存在内存不足的情况。高 frflt 值可能意味着有大量短命的进程——例如,启动许多其他进程的脚本,或者调度过于频繁的 cron 作业。或者可能是有人在运行 make -j16 buildworld。高 sr 值可能意味着你的内存不足,因为分页守护进程不断尝试释放内存。分页守护进程通常每分钟运行一次,但高 sr 值表示你可能正在尝试做的工作超出了你的 RAM 能承载的范围。

磁盘

disks 部分显示每个磁盘的设备名称。显示的数字是每秒磁盘操作次数,这是判断磁盘负载情况的有价值线索。你应该尽可能将磁盘操作分配到不同的磁盘上,并在可能的情况下将它们安排到不同的总线上。如果某个磁盘显然比其他磁盘忙,并且系统有等待磁盘访问的操作,考虑将一些频繁访问的文件从一个磁盘移动到另一个磁盘。磁盘负载高的一个常见原因是会自我重启的核心转储程序。例如,每当有人点击链接时就会转储核心的故障 CGI 脚本会大大增加磁盘负载。

如果你有很多磁盘,可能会发现它们并不都出现在 vmstat 显示中。由于设计时考虑了 80 列显示,vmstat(8) 无法列出大型系统中的所有磁盘。不过,如果你有更宽的显示器,并且不介意超过 80 列的限制,可以使用 -n 标志来设置你希望显示的磁盘数量。

故障

故障本身并不坏;它们只是系统收到的陷阱和中断。当然,异常大量的故障是坏的——但在解决这个问题之前,你需要了解什么是你系统的正常情况。

vmstat 输出的第一行显示自系统启动以来的平均故障数每秒。

in 接收到的系统中断(IRQ 请求)的数量。

sy 系统调用的数量。

cs 是上秒的上下文切换次数,或者自上次更新以来的每秒平均值。(例如,如果你让 vmstat 每五秒更新一次显示,这一列就会显示过去五秒钟内每秒的上下文切换平均数。)

该主机自启动以来,每秒平均执行 12 次系统调用和 212 次上下文切换。与系统正常工作时看到的数据相比如何?

CPU

最后,cpu 部分显示了系统在执行用户任务(us)、系统任务(sy)以及空闲时间(id)的花费时间。top(1) 以更友好的格式呈现这些相同的信息,但仅显示当前时刻的数据,而 vmstat 允许你查看系统利用率的变化情况。

使用 vmstat

那么,如何利用这些信息呢?首先检查前三列,看看系统在等待什么。如果你正在等待 CPU 访问(r 列),那说明你的 CPU 性能不足。如果你在等待磁盘访问(b 列),那说明你的磁盘成为了瓶颈。如果你在交换(w 列),说明你的内存不足。使用其他列来更详细地探讨这三种资源短缺情况。

持续 vmstat

你可能更关心的是随时间变化的系统情况,而不是简短的系统性能快照。使用 -w 标志和数字来以持续更新的方式运行它,每隔一定秒数更新一次。FreeBSD 会显示自上次更新以来的平均值,持续更新计数器:

# vmstat -w 5
procs  memory       page                    disks     faults         cpu
r b w  avm   fre   flt  re  pi  po    fr   sr ad0 ad1   in    sy    cs us sy id
8 0 0 1.6G   25G   415   0   1   0   432    6   0   0   12   281   157  1  0 99
8 0 0 2.4G   24G 53089   0   7   0 11188  561  11   8   45  8789   994 96  4  0
8 0 0 2.5G   24G 44600   0   3   0 38703  741  10   9   49  8806  1032 96  3  1
8 0 0 2.2G   24G 42841   0  15   0 58044  717  11   9   52 10271  1103 96  4  0
--snip--

第一行仍然显示自启动以来的平均值。然而,每五秒钟,系统会在末尾更新一行。你可以坐在那里观看系统性能如何随着计划任务的启动或特定程序的运行而变化。完成后按 CTRL-C 停止。在这个例子中,进程总是在等待 CPU 时间(如 r 列中一堆 8 所示),并且我们经常会看到等待磁盘访问的情况。

偶尔等待系统资源并不意味着你必须升级硬件;如果性能是可以接受的,就不用担心。但如果不行,必须进一步检查。最常见的原因是存储系统。

磁盘 I/O

磁盘速度是常见的性能瓶颈,尤其是旋转磁盘,但即便是基于闪存的存储也可能变慢。必须反复等待磁盘活动完成的程序运行会变得更慢。这通常被称为磁盘阻塞,意味着磁盘在阻止程序的活动。解决这个问题的唯一方法是使用更快的磁盘、安装更多的磁盘,或者重新调度负载。

虽然 FreeBSD 提供了多种工具来检查磁盘活动,但我最喜欢的是 gstat(8),所以我们将使用这个工具。你可以运行 gstat 不带任何参数,以显示所有磁盘和分区的状态,更新频率大约每秒一次。如果你的磁盘较多,这可能会显示一大堆零。我总是使用 -a 标志,这样 gstat(8) 只会显示有活动的磁盘。-p 标志也很有用,可以查看整个磁盘,但我更喜欢按分区查看。

# gstat -a
dT: 1.002s  w: 1.000s
 L(q)  ops/s    r/s   kBps ➊ms/r    w/s   kBps ➋ms/w   %busy Name
   0    120      0      0    0.0    118    331    0.1   12.1| ada1
   0    120      0      0    0.0    118    331    0.1   12.1| ada1p1
   0     21      0      0    0.0     19    351    0.4    8.2| da1
   0     20      0      0    0.0     18    331    0.1   12.1| gpt/zfs4
   0     21      0      0    0.0     19    351    0.4    8.2| da1p1
   0     21      0      0    0.0     19    351    0.4    8.2| gpt/zfs7

我们为每个磁盘设备、切片和分区获取一行信息,并为每个设备显示各种信息。gstat(8)展示了各种有用的内容,如每秒读取次数(r/s)、每秒写入次数(w/s)、每秒读取和写入的千字节数,以及一个友好的%busy列。

忽略这些大部分内容。有些内容,比如忙碌百分比列,采用了不太准确的测量方法。FreeBSD 开发人员选择了磁盘性能,而不是统计测量的精确度。然而,真正重要的是 ms/r(每次读取的毫秒数)➊ 和 ms/w(每次写入的毫秒数)➋。这些数字是准确的。请测量并监控它们。如果一个磁盘的活动非常高,而另一个处于空闲状态,可以考虑将磁盘上的数据分散到多个磁盘上,或者使用条带化存储。或者,如果是你的笔记本电脑,可能需要接受这就是你存储系统的最大速度。

一旦确定了稀缺的系统资源,你需要找出哪个程序正在消耗这些资源。我们将需要其他工具来帮助诊断。

CPU、内存和 I/O 使用 top(1)

top(1) 工具提供了一个系统状态的良好概览,显示了有关 CPU、内存和磁盘使用的信息。只需输入 top,即可获得系统性能数据的全屏显示。该显示每两秒更新一次,因此你可以获得接近实时的系统视图。即使你将更新间隔设置为一秒,你也可能错过一些短暂的、高资源消耗的进程。

top(1) 的输出分为上下两部分。上半部分提供基本的系统信息,而下半部分提供每个进程的数据。

➊last pid: 84111;  ➋load averages:  0.09,  0.21,  0.20               ➌up 7+07:58:00  14:41:09
➍28 processes:  2 running, 26 sleeping
➎CPU:  0.0% user,  0.0% nice,  0.9% system,  0.0% interrupt, 99.1% idle
➏Mem: 80M Active, 642M Inact, 124M Laundry, 222M Wired, 17M Free
➐Swap: 1024M Total, 83M Used, 941M Free, 8% Inuse

  PID USERNAME       THR PRI NICE   SIZE    RES STATE    TIME    WCPU COMMAND
  479 bind             4  20    0 99444K 35956K kqread   6:55   0.00% named
  586 root             1  20    0   154M 33768K select   4:54   0.00% perl
  562 root             1  20    0 22036K 13948K select   1:27   0.00% ntpd
--snip--

非常紧凑,对吧?top(1) 工具将尽可能多的数据塞入一个标准的 80 × 25 终端窗口或 X 终端。我们来拆解它,学习如何阅读。我们从上半部分开始,这部分内容在使用 UFS 或 ZFS 时可能会有所不同。

UFS 和 top(1)

top(1) 的主机信息在 ZFS 和 UFS 主机之间稍有不同,但我们将从 UFS 开始,然后解释它们的差异。

PID 值

每个 Unix 系统上的进程都有一个唯一的进程 ID(PID)。每当一个新进程启动时,内核会为它分配一个比前一个进程 PID 大一的值。最后的 PID 值是系统分配的最后一个进程 ID。在前面的示例中,我们的最后一个 PID 是 84,111 ➊。下一个创建的进程将是 84,112,然后是 84,113,依此类推。观察这个数字可以了解系统变化的速度。如果系统以比平常更快的速度分配 PID,可能表示某个进程的分叉失控,或者有某个进程崩溃并重新启动。

负载平均值

负载平均值 ➋ 是一个相对模糊的数字,它提供了系统 CPU 负载的大致情况。负载平均值是等待 CPU 时间的线程的平均数量。(其他操作系统可能有不同的负载平均值计算方法。)一个可接受的负载平均值取决于你的系统。如果数字异常偏高,您需要调查一下。某些主机在负载平均值为 3 时可能会感到滞后,而某些现代系统即使负载平均值看起来非常高,仍然运行得很流畅。再说一次,这个主机的正常负载是多少?

你将看到三个负载平均值。第一个(这里是 0.09)表示过去一分钟的负载平均值,第二个(0.21)表示过去五分钟的负载,最后一个(0.20)表示过去十五分钟的负载。如果你的十五分钟负载平均值很高,但一分钟负载平均值很低,说明你有一次重要的活动峰值,之后已经平息。另一方面,如果十五分钟负载低,但一分钟负载很高,说明在过去的 60 秒内发生了某些事情,可能仍在继续。如果所有负载平均值都很高,说明这种情况已经持续了至少 15 分钟。

运行时间

第一行的最后一项是 运行时间 ➌,即系统已运行的时间。此系统已运行了 7 天、7 小时和 58 分钟,当前时间是 14:41:09。计算系统启动的时间就留给你了。

进程计数

在第二行,你将看到关于系统当前正在运行的进程的信息 ➍。正在运行的进程实际上在执行任务——它们在响应用户请求、处理邮件,或者执行你的系统所需的其他操作。处于休眠状态的进程则在等待某个来源的输入;它们没问题。你应该预期任何时候都会有相当数量的休眠进程。处于其他状态的进程通常是在等待某个资源的可用,或者以某种方式挂起。大量非休眠、非运行的进程可能表示系统出现了问题。ps(1) 命令可以显示所有进程的状态。

进程类型

CPU 状态 行 ➎ 显示了系统在处理不同类型的进程时所花费的 CPU 时间百分比。它显示了五种不同的进程类型:用户nice系统中断空闲

user进程是普通的日常程序——可能是由 root 运行的守护进程,或者是普通用户运行的命令,等等。如果它出现在ps -ax中,它就是一个用户进程。

nice进程是优先级被故意调整的用户进程。我们将在《使用 Niceness 重新调整优先级》的第 543 页详细讨论这一点。

system值显示 FreeBSD 用于运行内核进程和内核中的用户进程所花费的 CPU 时间百分比。这些包括虚拟内存处理、网络、写入磁盘、使用 INVARIANTS 和 WITNESS 进行调试等。

interrupt值显示系统处理中断请求(IRQ)所花费的时间。

最后,idle项显示系统处于空闲状态的时间。如果你的 CPU 经常出现非常低的空闲时间,可能需要考虑重新调度任务或更换更快的处理器。

TOP 和 SMP

在 SMP 系统中,top(1)显示所有处理器的平均使用情况。你可能有一个处理器完全占用在编译某个东西,但如果另一个处理器处于空闲状态,top(1)显示的 CPU 使用率可能只有 50%。使用-p标志查看每个 CPU 的统计信息。

内存

Mem行➏表示物理 RAM 的使用情况。FreeBSD 将内存使用分为多个不同的类别。

活动内存是用户进程正在使用的内存总量。当一个程序结束时,它所使用的内存会被放入非活动内存中。如果系统再次运行这个程序,它可以从内存中检索软件,而不是从磁盘加载。

空闲内存完全未被使用。它可能是从未被访问的内存,也可能是某个进程释放的内存。该系统有 17MB 的空闲 RAM。如果你的服务器已经运行了几个月,仍然有空闲内存,你可以考虑将一些 RAM 转移到内存不足的机器上。

洗衣中的内存排队等待与其他存储同步,如磁盘。

FreeBSD 11 根据需要在非活动、洗衣和空闲类别之间调度内存,以维持可用内存池。非活动内存最容易转移到空闲池。当缓存内存不足且 FreeBSD 仍然需要更多空闲内存时,它会从非活动池中选择页面,验证是否可以将其用作空闲内存,然后将其移入空闲池。FreeBSD 尽量保持空闲页面的总数高于 sysctl vm.v_free_target

FreeBSD 12 没有缓存,处理低内存情况的方式略有不同。当空闲内存不足时,页面守护进程会从非活动池中选择页面。如果该非活动页面需要同步到磁盘,它会被放入洗衣队列,页面守护进程则会尝试另一个非活动页面。测试主机是否需要更多 RAM 的一种方式是,如果页面守护进程因所有这些测试而积累了 CPU 时间。

在任一 FreeBSD 版本中,空闲内存并不意味着系统内存充足。如果 vmstat(8) 显示你正在进行交换操作,那么说明你使用的物理内存超过了系统的实际内存。你可能有一个定期释放内存的程序。此外,FreeBSD 会将一些从非活动状态的页面移到空闲状态,以保持一定的空闲内存。

FreeBSD 使用 wired 内存来存储内核数据结构,以及必须立即获取某块内存的系统调用。wired 内存永远不会被交换或分页。所有 ZFS 使用的内存都是 wired 内存。

交换区

Swap 行 ➐ 显示了系统上可用的交换空间总量以及当前使用量。交换区是使用磁盘驱动器作为额外内存。我们将在本章后面更详细地讨论交换区。

ZFS 与 top(1)

在 ZFS 系统上,top(1) 的输出看起来表面上有所不同,但每个主机对内存的处理有重要的差异。

  last pid: 53202;  load averages:  0.26,  0.28,  0.30      up 1+15:41:48
  13:50:54
  120 processes: 1 running, 119 sleeping
  CPU:  0.1% user,  0.0% nice,  0.0% system,  0.0% interrupt, 99.9% idle
➊ Mem: 288M Active, 205M Inact, 3299M Wired, 137M Free
➋ ARC: 2312M Total, 458M MFU, 1626M MRU, 420K Anon, 38M Header, 189M Other
➌      1918M Compressed, 8885M Uncompressed, 4.63:1 Ratio
  Swap: 2048M Total, 126M Used, 1922M Free, 6% Inuse

    PID USERNAME    THR PRI NICE   SIZE    RES STATE   C   TIME    WCPU COMMAND
  53202 mwlucas       1  20    0 20124K  3388K CPU0    0   0:00   0.08% top
    835 mysql        26  23    0   629M   219M select  1  62:42   0.03% mysqld
  53151 www           1  20    0   237M 12924K select  2   0:00   0.03% httpd
    863 nobody        7  20    0 34928K  4960K kqread  0   0:31   0.02% memcached
  53058 www           1  20    0   239M 13296K lockf   0   0:00   0.01% httpd
    852 root          1  20    0   166M 11716K kqread  2   0:05   0.01% php-fpm
  --snip--

Mem 部分 ➊ 列出了 ActiveInactiveLaundryWiredFree 内存,这些是从 UFS 输出中熟悉的字段。

ARC 行 ➋ 表示 ZFS 的 高级替换缓存Total 字段显示整个 ARC 使用的内存量。在缓存使用的 2,312MB 中,458MB 在最常用(MFU)缓存中,而 1,626MB 在最近使用(MRU)缓存中。你还会看到一些更小的条目,用于表示 ZFS 内部数据结构,如匿名缓冲区(Anon)、ZFS 头(Header)以及非常有用的 Other

ZFS 压缩了 ARC ➌,通过大量的 CPU 时间来换取稀缺的内存。你可以看到压缩和未压缩缓存数据所使用的空间量。

ZFS 对内存的需求很大,前提是没有其他进程需要它。ZFS 会积极缓存从磁盘读写的数据。该主机有 4,096MB 的内存,而 ZFS 已经占用了其中的 2,312MB。你会看到该主机只剩下 137MB 的空闲内存。如果某个程序请求内存,而系统没有足够的内存可用,ZFS 会将一些缓存释放回系统。如果你看到较高的“Wired”内存水平,记住所有由 ZFS 占用的内存都会进入“wired”类别。

这是一种啰嗦的方式在说:“不要被表面上看起来较高的 ZFS 内存使用量所困扰。” 只有在主机开始分页和交换时才需要担心。

更有趣的是使用这些内存的进程列表。

进程列表

最后,top(1) 列出了系统上的进程及其基本特征。表格格式旨在尽可能少的空间中展示尽可能多的信息。每个进程占用一行。

PID 首先,我们有进程 ID 号,或 PID。每个运行中的进程都有一个唯一的 PID。当你使用 kill(1) 时,通过 PID 来指定进程。(如果你不知道某个进程的 PID,可以使用 pkill(1) 通过进程名称来杀死该进程。)

用户名 接下来是运行该进程的用户的用户名。如果多个进程占用了大量 CPU 或内存,并且它们都由同一个用户拥有,那么你就知道该找谁谈话。

优先级和亲和度 PRI(优先级)和 NICE 列是相互关联的,表示系统给予每个进程的优先级。我们将在本章稍后讨论优先级和亲和度。

大小 SIZE 显示进程请求的内存量。

常驻内存 RES 列显示程序当前实际在内存中的部分。一个程序可能请求大量的内存,但在任何时候只使用其中的一小部分。内核足够智能,可以根据程序的实际需求分配内存,而不是按请求分配。

状态 STATE 列显示进程当前正在做什么。进程可能处于各种状态——等待输入、处于睡眠状态直到某个事件唤醒它、正在积极运行等等。你可以看到进程正在等待的事件名称,例如 selectpausettyin。在 SMP 系统上,当进程运行时,你会看到它所运行的 CPU。

时间 TIME 列显示该进程已消耗的总 CPU 时间。

加权 CPU 加权 CPU(WCPU)使用量显示该进程使用的 CPU 时间百分比,已根据进程的优先级和亲和度进行了调整。

命令 最后,我们有正在运行的程序的名称。

查看 top(1) 的输出可以让你了解系统花费时间的地方。

不是每个主机上的进程都在积极工作。你可能有几十个或上百个空闲的守护进程。按 i 键切换显示空闲进程,或者使用 -i 命令行标志。要显示单独的线程,可以切换 H 或添加 -H 标志。

默认情况下,top 按加权 CPU 使用量对输出进行排序。你也可以按优先级、大小和常驻内存对输出进行排序。在运行的 top 显示中,按 o 键,然后输入你想排序的列名。这有助于识别那些自命不凡或使用过多内存的程序。

top(1) 和 I/O

除了标准的 CPU 显示,top(1) 还有一个 I/O 模式,显示哪些进程正在最活跃地使用磁盘。运行 top(1) 时,按 m 进入 I/O 模式。显示的上半部分仍然显示内存、交换区和 CPU 状态,但下半部分会发生显著变化。

  PID USERNAME     VCSW  IVCSW   READ  WRITE  FAULT  TOTAL PERCENT COMMAND
 3064 root           89      0     89      0      0     89 100.00% tcsh
  767 root            0      0      0      0      0      0   0.00% nfsd
 1082 mwlucas         2      1      0      0      0      0   0.00% sshd
 1092 root            0      0      0      0      0      0   0.00% tcsh
  904 root            0      0      0      0      0      0   0.00% sendmail
--snip--

PID 是进程 ID,当然,USERNAME 列显示的是谁在运行这个进程。

VCSW 代表 自愿上下文切换;这是该进程已经将系统交给其他进程的次数。IVCSW 代表 非自愿上下文切换,显示内核告诉进程“你完成了,现在该让别人运行一会儿”的次数。

类似地,READWRITE显示系统从磁盘读取和写入的次数。FAULT列显示该进程需要从磁盘拉取内存页的次数,这也是另一种磁盘读取。这最后三列的汇总显示在TOTAL列中。

PERCENT列显示该进程使用的磁盘活动的百分比。与 gstat(8)不同,top(1)显示的是每个进程的实际磁盘活动占比,而不是可能的磁盘活动。如果只有一个进程在访问磁盘,top(1)会显示该进程使用了 100%的磁盘活动,即使它仅仅是发送了一点点数据。gstat(8)告诉你磁盘有多忙,而 top(1)则告诉你是什么产生了磁盘活动,并指出责任。这里,我们看到进程 ID 3064 正在产生所有磁盘活动。它是一个 tcsh(1)进程,也叫做“某个用户的 shell”。让我们追踪一下这个罪魁祸首。

更多顶级功能

top(1)工具可以以多种方式改变显示方式。你可以查看特定用户的进程,包含或排除内核线程,排除空闲进程等等。有关详细信息,请阅读手册页。

后续进程

在任何类 Unix 系统上,每个用户空间进程都有与其他进程的父子关系。当 FreeBSD 启动时,它通过启动 init(8)并将其分配 PID 1 来创建一个单一进程。这个进程会启动其他进程,比如/etc/rc启动脚本和处理你登录请求的 getty(8)程序。这些进程是进程 ID 1 的子进程。当你登录时,getty(8)会启动 login(8),然后启动一个新的 shell,使得你的 shell 成为 login(8)进程的子进程。你运行的命令要么是你 shell 进程的子进程,要么是你 shell 的一部分。你可以使用 ps(1)并通过-ajx标志(以及其他标志)查看这些父子关系。

# ps -ajx
USER      PID  PPID  PGID  SID JOBC STAT TT         TIME COMMAND
root        0     0     0    0    0 DLs   -      6:26.23 [kernel]
root        1     0     1    1    0 ILs   -      0:00.09 /sbin/init --
root        2     0     0    0    0 DL    -      0:00.00 [crypto]
--snip--
root      845     1   845  845    0 Is    -      0:00.00 /usr/sbin/sshd
root      849     1   849  849    0 Ss    -      0:01.05 /usr/sbin/cron -s
root     8632   845  8632 8632    0 Is    -      0:00.09 sshd: mwlucas [priv] (sshd)
mwlucas  8634  8632  8632 8632    0 S     -      0:00.53 sshd: mwlucas@pts/0,pts/1 (sshd)
mwlucas  8687     1  8687 8687    0 Ss    -      0:25.90 tmux: server (/tmp/tmux-1001/default) (tmux)
--snip--

在最左边,我们可以看到进程拥有者的用户名,然后是该进程的PID和父进程 PID(PPID)。这是我们在这里看到的最有用的信息,但我们将简要介绍其他字段。

PGID是进程组 ID 号,通常由其父进程继承。程序可以启动一个新的进程组,该新进程组将拥有与进程 ID 相等的 PGID。进程组用于信号处理和作业控制。会话 ID,或SID,是 PGID 的一个分组,通常由单一用户或守护进程启动。进程不能从一个 SID 迁移到另一个 SID。JOBC给出作业控制计数,指示进程是否在作业控制下运行(即在后台)。

STAT显示进程状态——即你运行 ps(1)时,进程当前正在做什么。进程状态非常有用,它告诉你进程是否处于空闲状态,正在等待什么等等。我强烈建议阅读 ps(1)中关于进程状态的章节。

TT列列出进程的控制终端。此列仅显示终端名称的最后部分,例如v0表示ttyv0p0表示ttyp0。没有控制终端的进程显示为??

TIME列显示了进程使用的处理器时间,包括用户态和内核态的时间。

最后,我们看到COMMAND名称,这是父进程调用的名称。方括号中的进程实际上是内核线程,而非真正的进程。FreeBSD 运行了大量的内核线程。

那么,如何利用这一点来追踪一个可疑进程呢?在我们 top(1)的 I/O 示例中,我们看到进程 3064 几乎产生了我们所有的磁盘活动。运行ps -ajx来查找这个进程:

   USER      PID  PPID  PGID  SID JOBC STAT TT         TIME COMMAND
   --snip--
   root     3035  3034  3035  2969    1 S+    p0    0:00.03 _su -m (tcsh)
➊ bert     2981  2980  2981  2981    0 Is    p1    0:00.03 -tcsh (tcsh)
➋ root     2989  2981  2989  2981    1 I     p1    0:00.01 su -m
➌ root     2990  2989  2990  2981    1 D     p1    0:00.05 _su -m (tcsh)
➍ root     3064  2990  3064  2981    1 DV+   p1    0:00.15 _su -m (tcsh)
   mwlucas  2996  2995  2996  2996    0 Is    p2    0:00.02 -tcsh (tcsh)
   --snip--

我们关注的进程由 root 拥有,是一个 tcsh(1)实例➍,正如 top 的 I/O 模式所显示的那样。然而,命令是以 su(1)身份运行的。检查这个进程的父进程 ID,通过 PPID 列,你会看到进程 3064 是进程 2990 ➌的子进程,进程 2990 又是进程 2989 ➋的子进程,后者由 root 拥有。然而,进程 2989 是由 2981 ➊这个 shell 进程创建的,2981 是由一个真实用户启动的。你可能还会注意到,这些进程都属于会话 2981,显示它们可能都在同一个登录会话中运行。TT列显示为p1,这意味着用户在该机器的第二个虚拟终端(/dev/ttyp1)上登录。调查这个会话 ID 可以揭示 Bert 认为他在做什么。

现在你已经了解了进程的父子操作是如何工作的,你可以进行一些作弊操作。添加-d标志,例如ps -ajxd,可以以树状结构显示进程及其父进程。你可能需要一个宽的终端。

系统经历短时间的完全占用是正常的。如果没有其他人使用系统,而且没有人抱怨性能问题,为什么不让这个用户运行他的任务呢?然而,如果这个进程影响了其他用户,我们可以选择降低其优先级,使用我们的根权限杀死这个任务,或者拿着棒球棒出现在用户的工位前。

分页与交换

使用交换空间本身并不算坏。交换空间比内存慢得多,但它确实能工作,许多程序不需要将所有内容都保存在 RAM 中才能运行。旧的经验法则说,一个典型的程序 80%的时间用于运行 20%的代码,其余的时间则用来启动和关闭、错误处理等。你可以放心地让这些部分从 RAM 中移出,几乎不会影响性能。

交换缓存了它已经处理过的数据。一旦一个进程使用了交换空间,直到该进程退出或从交换空间中调用内存回来之前,这块交换空间将一直在使用。

交换的使用通过分页交换实现。分页是没问题的;交换则不太好,但总比崩溃强。

分页

分页发生在 FreeBSD 将正在运行的程序的部分内容移到交换空间时。分页实际上可以提高系统负载很重时的性能,因为未使用的部分可以存储在磁盘上,直到需要时再取出——如果真的需要的话。FreeBSD 可以将实际的内存用来运行真正的代码。系统是否将数据库启动代码移到交换区,一旦数据库启动并运行,真的很重要吗?

交换

如果计算机没有足够的物理内存来存储在某一微秒内未运行的进程,系统可以将整个进程移动到交换空间。当调度器再次启动该进程时,FreeBSD 会从交换区取回整个进程并运行它,可能还会把其他进程送到交换区。

交换的主要问题是磁盘 I/O 活动暴增,性能急剧下降。由于请求处理时间更长,系统中任何时刻的请求也会增加。登录检查问题只会让情况变得更糟,因为你的登录只是又一个进程。一些系统能够处理一定量的交换,而在其他系统上,情况很快会恶化成死循环。

当 CPU 过载时,系统会变慢。当磁盘成为瓶颈时,系统也会变慢。内存不足实际上可能导致计算机崩溃。如果你在进行交换操作,你必须购买更多的内存,否则只能忍受糟糕的性能。如果你被困在这款硬件上无法购买更多内存,可以考虑购买一块非常快速的 SSD 用作交换空间。

vmstat(8) 的输出显示了在任何时刻被交换出去的进程数量。

性能调优

FreeBSD 会将最近访问的数据缓存到内存中,因为有很多信息会被反复从磁盘中读取。缓存到物理内存中的信息可以非常快速地访问。如果系统需要更多内存,它会丢弃最旧的缓存数据以腾出空间给新的数据。UFS 和 ZFS 使用不同的方法来决定缓存哪些数据,但这个原则通常适用。

今天早晨启动我的桌面时,我打开了 Firefox 以便查看我的 RSS 订阅。磁盘运行了一两秒钟来加载程序。然后我关闭了浏览器,以便集中精力工作,但 FreeBSD 将 Firefox 保留在缓存中。如果我重新启动 Firefox,FreeBSD 将直接从内存中提取它,而不是再去访问磁盘,从而大大减少了启动时间。如果我启动了一个需要大量内存的进程,FreeBSD 则会将网页浏览器从缓存中清除,以支持新进程。

如果你的系统运行正常,至少应该有几兆字节的空闲内存。sysctl 参数vm.v_free_targethw.pagesize告诉你 FreeBSD 认为系统中需要多少空闲内存。如果你总是拥有比这两个 sysctl 参数相乘的值更多的空闲内存,那么你的系统没有得到充分的利用。例如,在我的邮件服务器上,我有:

# sysctl vm.v_free_target
vm.v_free_target: 5350
# sysctl hw.pagesize
hw.pagesize: 4096

我的系统至少需要拥有 5,350 × 4,096 = 21,913,600 字节,约 22MB 的空闲内存。如果不是因为我对不足的 RAM 有深层次的情感创伤,我完全可以毫不犹豫地从我的桌面丢掉 1GB 的 RAM。^(3)

内存使用

如果主机的缓存或缓冲区中有大量内存,或者 ARC 已经占用了所有 RAM,那么它并不意味着内存不足。你可能会更好地利用更多的内存,但并不是严格必要的。如果你有较少的空闲内存,但大量活跃且非 ZFS 固定的内存,那么你的系统正在消耗 RAM。增加内存会让你更好地利用缓冲区缓存。

如果页面守护进程持续运行,并且在你的 vmstat 输出中递增 sr 字段,说明内核正在努力提供内存。主机可能确实存在内存不足的情况。然而,一旦主机开始使用交换空间,这种内存不足就不再是假设的情况。虽然这不一定很糟糕,但也不是理论上的问题。

交换空间使用

交换空间可以帮助暂时弥补 RAM 的不足。例如,如果你在解压一个巨大的文件,可能会轻易消耗掉所有的物理内存,并开始使用虚拟内存。对于这种偶尔的任务,购买更多的 RAM 并不值得,因为交换空间已经足够。如果一台内存不足的服务器运行着一个从未被调用的守护进程,那么这个守护进程最终会被完全或大部分交换掉,以优先考虑执行工作的进程。

只有在系统持续频繁地将数据从交换空间中交换进出时,才需要担心交换空间的使用。

简而言之,交换空间就像酒。偶尔喝一两杯不会伤害你,甚至可能是一个不错的选择。但如果你总是依赖交换空间,那就成问题了。如果你不得不频繁使用交换空间,可以考虑使用一个非常快速且耐用的 SSD。

CPU 使用

处理器每秒钟能处理的事情是有限的。如果你运行的任务超过了 CPU 的承载能力,请求会开始积压,你将出现处理器积压,系统也会变慢。这就是 CPU 使用的核心概念。如果性能不可接受,并且 top(1)显示你的 CPU 一直处于接近 100%的状态,那么 CPU 利用率可能就是你的问题所在。虽然新硬件确实是一个选择,但你还有其他的选择。例如,检查系统中运行的进程,看看它们是否都必要。是不是某个初级系统管理员安装了 SETI@Home 客户端来用你的空闲 CPU 周期寻找外星人?比特币挖矿程序呢?是否有一些曾经重要的程序现在已经不再需要了?找出并关闭这些不必要的进程,并确保它们在下次系统启动时不会再运行。

如果你有非常具体的需求,比如将某些处理器专门分配给特定任务,考虑使用 cpuset(1)。对于大多数用户来说,这是过度的,但高性能应用可能会很好地利用专用的处理器。

完成这些后,再次评估你的系统性能。如果问题仍然存在,尝试重新调度或调整优先级。

重新调度

重新调度比重新排序更简单;它是一种相对简单的方法,旨在平衡系统进程,以免它们垄断系统资源。如第二十章所讨论,你和你的用户可以使用 cron(8) 在特定时间安排程序运行。如果你的用户在特定时间运行大量任务,你可以考虑使用 cron(1) 在非高峰时段运行它们。通常像每月账单数据库查询这类任务可以在晚上 6 点到早上 6 点之间运行,没人会介意——财务部门只需要在每月第一天早上 8 点时拿到数据,以便完成上个月的账务结算。类似地,你可以在凌晨 1 点安排你的 make buildworld && make buildkernel

使用友好性重新排序

如果重新调度不可行,那么你就需要进行重新排序,这可能会稍微复杂一些。在重新排序时,你告诉 FreeBSD 改变给定进程的优先级。例如,你可以让一个程序在繁忙时段运行,但只有在没有其他程序需要运行时才运行。你刚刚告诉那个程序要友好,并让出位置给其他程序。

进程越“友好”,它所需的 CPU 时间就越少。默认的友好性是 0,但友好性范围从 20(非常友好)到 -20(一点也不友好)。这看起来可能有些反直觉;你可能会认为较高的数值应该意味着更高的优先级。然而,这会导致语言上的问题;将这个因素称为“自私”或“暴躁”而不是“友好性”当时似乎不是一个好主意。^(4)

top(1) 工具会显示一个 PRI 列,表示进程优先级。FreeBSD 会根据多种因素计算进程的优先级,包括友好性,并尽可能先运行高优先级的进程。友好性会影响优先级,但你不能直接编辑优先级。

如果你知道系统的负载接近或达到了容量上限,你可以选择使用 nice(1) 命令运行某个命令并为该进程指定友好性。通过 nice -n 和命令前的友好性值来指定友好性。例如,要以 nice 15 启动一个非常自私的 make buildworld,你可以运行:

# nice -n 15 make buildworld

只有 root 用户可以为程序分配负的友好性值,比如 nice -n -5。例如,如果你想滥用超级用户权限使编译尽可能快地完成,可以使用负的友好性:

# nice -n -20 make

NICE 与 TCSH

tcsh(1) shell 内置了一个 nice 命令。这个内置的 nice 使用的是 renice(8) 语法,与 nice(1) 不同。我相信这么做除了让 tcsh 用户不高兴之外是有原因的,但现在我想不起来了。要使用 nice(1),请使用完整路径 /usr/bin/nice

通常,你没有足够的时间在启动命令时就让它变得友好,而是必须在发现它占用了系统所有资源时改变其友好性。你可以使用 renice(8) 通过进程 ID 或所有者来重新排序正在运行的进程。要改变进程的友好性,运行 renice 并指定新的友好性值和 PID 作为参数。

在我的职业生涯中,我曾运行过几个日志主机。除了常规的 syslog 服务外,它们通常还会运行几个 flow-capture、Nagios 和其他关键的网络感知系统。我通常会使用一个 web 界面来管理所有这些,并允许其他人访问我的日志。如果我发现 web 服务器上的间歇性负载干扰了我的网络监控或 syslogd(8)服务器,我必须采取行动。将 web 服务器的优先级调整低会让客户端运行得更慢,但这比让监控变慢要好。使用 pgrep(1)找到 web 服务器的 PID:

# pgrep httpd
993
# renice 10 993
993: old priority 0, new priority 10

轰!FreeBSD 现在在其他进程之后处理 web 请求。这大大惹恼了该服务的用户,但既然是我的服务器,我已经很烦了,那也没关系。

要重新调整用户拥有的每个进程的优先级,可以使用-u标志。例如,若要使我的进程优先级高于其他任何人的,我可以这样做:

# renice -5 -u mwlucas
1001: old priority 0, new priority -5

1001是我在此系统上的用户 ID。同样,可以推测,我这么做有非常充分的理由,而不仅仅是为了个人的权力需求。^(5) 同样,如果那个吃掉我所有处理器时间的用户坚持要为难我,我可以将他的进程设置为非常、非常低的优先级,这可能会解决其他用户的抱怨。如果你有一个大的后台数据库任务,让运行该任务的用户优先级较低可以让前台工作正常进行。

Niceness 仅影响 CPU 使用率, 对磁盘或网络活动没有影响。

瓶颈交替

每个系统都有瓶颈。如果消除了一个瓶颈,性能会提升,直到遇到另一个瓶颈。系统的性能受限于计算机中最慢的组件。例如,web 服务器通常会受到网络限制,因为系统中最慢的部分是互联网连接。如果你将千兆上传带宽升级到 2.4Gb/s 的 OC-48,系统将以其他组件的速度提供站点。管理层经常要求的“消除瓶颈”实际上是“消除干扰你常规工作负载的瓶颈”。

现在你可以查看系统问题了,让我们来学习如何听懂系统试图告诉你的内容。

状态邮件

FreeBSD 每天、每周和每月通过 periodic(8)运行维护任务。这些任务进行基本的系统检查,并通知管理员有关变化、需要关注的项目和潜在的安全问题。每个计划任务的输出每天都会发送到本地系统上的 root 账户。了解系统正在做什么的最简单方法是阅读这些邮件;许多像你一样非常忙碌的系统管理员已经合作,使这些信息变得非常有用。虽然你可能会收到很多这些邮件,但通过一点经验,你会学会如何快速浏览报告,只关注关键或异常的变化。

每日、每周和每月报告的配置在periodic.conf中进行控制,如第二十章所述。

你可能不想每天都以 root 用户身份登录到所有服务器以读取邮件,因此将每台服务器的 root 邮件转发到一个集中式邮箱。在 /etc/mail/aliases 中进行此更改,如 第二十章 所讨论。

我建议只在嵌入式系统上禁用这些任务,因为嵌入式系统应该通过其他方式进行管理和监控,例如通过你的网络监控系统。在这样的系统上,禁用 /etc/crontab 中的 periodic(8) 检查。

虽然这些日常报告很有用,但它们并没有讲述全部情况。日志提供了更为完整的视图。

使用 syslogd 进行日志记录

FreeBSD 日志系统非常有用。任何类 Unix 操作系统几乎都可以记录几乎任何内容,并且可以设置详细的记录级别。虽然你会发现默认的系统日志挂钩用于最常见的系统资源,但你可以选择符合需求的日志配置。几乎所有程序都与日志守护进程 syslogd(8) 集成。

syslog 协议通过消息工作。程序发送单个消息,syslog 守护进程 syslogd(8) 捕获并处理这些消息。syslogd(8) 根据消息的设施和优先级处理每个消息,这些都是客户端程序为消息指定的。你必须理解设施和优先级才能管理系统日志。

设施

设施 是表示日志条目来源的标签。这是一个任意的标签,仅是用来区分不同程序的文本字符串。在大多数情况下,每个需要唯一日志的程序都会使用一个独特的设施。许多程序或协议都有专门的设施——例如,FTP 是一种常见协议,以至于 syslogd(8) 为它专门设置了一个设施。syslogd 还支持各种通用设施,你可以将它们分配给任何程序。

以下是标准设施及其用途的信息类型。

auth 关于用户授权的公共信息,例如人们登录或使用 su(1) 时的情况。

authpriv 关于用户授权的私人信息,仅 root 用户可访问。

console 正常打印到系统控制台的消息。

cron 来自系统进程调度器的消息。

daemon 用于所有没有其他明确处理程序的系统守护进程的捕获设施。

ftp 来自 FTP 和 TFTP 服务器的消息。

kern 来自内核的消息。

lpr 打印系统的消息。

mail 邮件系统消息。

mark 该设施每 20 分钟在日志中插入一条记录。与其他日志结合使用时非常有用。

news 来自 Usenet News 守护进程的消息。

ntp 网络时间协议消息。

security 来自安全程序的消息,例如 pfctl(8)。

syslog 来自日志系统本身的消息。注意,不要在日志时再记录日志,这样只会让你头晕。

user 捕获所有消息的设施。如果用户程序没有指定日志设施,则使用此设施。

uucp 来自 Unix 到 Unix 复制协议的消息。这是互联网前时代的 Unix 历史,你可能永远不会遇到。

local0 local7 这些是为系统管理员提供的。许多程序都有设置日志设施的选项;如果可能,选择这些之一。例如,你可能会告诉客户服务系统将日志记录到 local0

尽管大多数程序都有合理的默认设置,但作为系统管理员,你的职责是管理哪些程序记录到哪些设施。

级别

日志消息的级别代表其相对重要性。虽然程序将所有日志数据发送到 syslogd,但大多数系统仅记录 syslogd 接收到的重要内容,丢弃其余的内容。当然,一个人认为无关紧要的事物,可能对另一个人来说是至关重要的,这就是级别的作用所在。

syslog 协议提供了八个级别。使用这些级别来告诉 syslogd 记录什么内容,丢弃什么内容。这些级别按照重要性从高到低的顺序排列:

emerg 系统崩溃。消息会在每个终端上闪烁。计算机基本上已经无法使用。你甚至不需要重启——系统会自动为你重启。

crit 严重错误包括硬盘上的坏块或严重的软件问题。如果你足够勇敢,你可以继续运行,照常使用。

alert 这很糟糕,但不是紧急情况。系统仍然可以继续运行,但这个错误应该立即处理。

err 这些是需要某个时候关注的错误,但不会摧毁系统。

warning 这些是杂项警告,可能不会阻止发出警告的程序照常工作。

notice 包括一般信息,通常不需要你采取行动,比如守护进程的启动和关闭。

info 包括程序信息,如邮件服务器中的个别事务。

debug 该级别通常仅对程序员有用,偶尔也对试图弄清楚程序为何按某种方式运行的系统管理员有用。调试日志可能包含程序员认为调试代码所需的任何信息,其中可能包括违反用户隐私的信息。

none 这意味着,“不要从这个设施记录任何内容。”它最常用于排除通配符条目的信息,稍后我们会看到。

通过将级别与优先级结合使用,你可以非常精确地分类消息,并根据需要处理每条消息。

使用 syslogd(8) 处理消息

syslogd(8)守护进程从网络中捕获消息,并将它们与/etc/syslog.conf/etc/syslog.d/中的条目进行比较。/etc/syslog.d/中的文件用于你的自定义条目和附加程序,而/etc/syslog.conf用于集成的系统程序。syslogd 只读取以.conf结尾的/etc/syslog.d/文件。这两个文件格式相同,但为了清晰起见,我将参考/etc/syslog.conf。该文件有两列;第一列描述日志消息,可能是通过设施和级别,或者通过程序名称。第二列告诉 syslogd(8)当日志消息与描述匹配时应该做什么。例如,看看这个来自默认syslog.conf的条目:

mail.info                                       /var/log/maillog

这告诉 syslogd(8),当它接收到来自mail设施且级别为info或更高的消息时,该消息应该附加到/var/log/maillog中。

如果日志文件不存在,logger 将无法记录日志。在重新启动 syslogd(8)之前,请使用 touch(1)创建日志文件。

通配符

你还可以使用通配符作为信息来源。例如,这一行记录来自mail设施的每条消息:

mail.*                                       /var/log/maillog

要记录来自所有地方的所有内容,请取消注释all.log条目,并创建文件/var/log/all.log

*.*                                         /var/log/all.log

这能工作,但我发现它提供的信息过于冗杂,实际使用时并不方便。你会发现自己需要使用复杂的 grep(1)语句将多个命令串联起来才能找到即使是最简单的信息。此外,这还会包含各种私人数据。

排除信息

使用none级别可以从日志中排除信息。例如,在这里,我们将authpriv信息从我们的全包含日志中排除。分号允许你将多个条目合并到同一行:

*.*; authpriv.none        /var/log/most.log
比较

你也可以在syslog.conf规则中使用比较操作符<(小于)、=(等于)和>(大于)。虽然 syslogd 默认记录指定级别或以上的所有消息,但你可能只想包括一个级别范围。例如,你可以将所有info级别及以上的日志记录到主日志文件中,同时将其余的日志记录到调试文件中:

mail.info                /var/log/maillog
mail.=debug              /var/log/maillog.debug

mail.info条目匹配所有发送到mail设施且级别为info及以上的日志消息。第二行只匹配级别恰好为debug的消息。你不能简单地使用mail.debug,因为那样调试日志会重复前一个日志的内容。这样,你就不需要为基本的邮件日志筛选调试信息,也不需要为获取调试输出而筛选邮件传输信息。

本地设施

许多程序提供通过 syslog 记录日志的功能。大多数这些程序可以设置为你选择的设施。各种local设施是为这些程序保留的。例如,默认情况下,dhcpd(8)(参见第二十章)将日志记录到local7设施中。在这里,我们捕获这些消息并将它们发送到自己的文件:

local7.*                /var/log/dhcpd

如果local设施用完了,你可以使用系统未使用的其他设施。例如,我曾在一个没有uucp服务的繁忙日志服务器上使用过uucp设施。

按程序名称记录日志

如果设施已满,你可以使用程序的名称作为匹配条件。对于一个名称的条目,需要两行:第一行包含带有前导感叹号的程序名称,第二行设置日志记录。例如,FreeBSD 使用此方法记录 ppp(8)的信息:

!ppp
*.*                    /var/log/ppp.log

第一行指定程序名称,第二行使用通配符告诉 syslogd(8)将所有内容附加到一个文件中。

!programname 语法会影响其后的所有行,因此你必须将其放在syslogd.conf的最后。你可以放心地在/etc/syslog.d文件中使用它,而不必担心影响到其他条目。

记录到用户会话

当你将日志记录到用户时,任何到达的消息都会显示在该用户的屏幕上。要将消息记录到用户会话中,请列出以逗号分隔的用户名作为目标。要将消息写入所有用户的终端,使用星号(*)。例如,默认的syslog.conf文件包括这一行:

*.emerg                *

这表示任何紧急级别的消息都会出现在所有用户的终端上。由于这些消息通常会以某种方式说“再见”,因此这是合适的。

将日志消息发送到程序

要将日志消息定向到程序,使用管道符号(|):

mail.*                |/usr/local/bin/mailstats.pl
记录到日志主机

我的网络通常有一个单独的日志主机,处理的不仅是 FreeBSD 主机,还有 Cisco 路由器和交换机、其他 Unix 主机以及任何支持 syslog 的设备。这大大减少了系统维护并节省了磁盘空间。每条日志消息都包含主机名,因此你可以轻松地在之后进行分类。

使用at符号(@)将消息发送到另一台主机。例如,以下这一行将你本地 syslog 接收到的所有内容转发到我网络上的日志主机:

*.*                  @loghost.blackhelicopters.org

目标主机上的syslog.conf文件决定了这些消息的最终目的地。

在日志主机上,你可以通过日志消息的来源主机来分隔日志。使用加号(+)符号和主机名来表示接下来的规则适用于此主机:

+dhcpserver
local7.*            /var/log/dhcpd
+ns1
local7.*            /var/log/named

将你的通用规则放在syslog.conf的顶部。每个主机的规则应该放在底部或单独的syslog.d文件中。

日志重叠

日志守护进程不是基于首次匹配或最后匹配的方式进行日志记录;相反,它根据每个匹配规则进行记录。这意味着你可以轻松地将一条日志消息记录到多个不同的日志中。考虑以下日志配置片段。

*.notice;authpriv.none        /var/log/messages
local7.*                      /var/log/dhcp

几乎每条 notice 级别或更高的消息都会被记录到 /var/log/messages。然而,任何具有 authpriv 设施的消息都会故意从此日志中排除。我们让我们的 DHCP 服务器记录到 /var/log/dhcp。这意味着任何 notice 级别或更高的 DHCP 消息都会同时记录到 /var/log/messages/var/log/dhcpd。我不喜欢这样,我希望我的 DHCP 消息只出现在 /var/log/dhcpd 中。我可以通过遵循 authpriv 示例,使用 none 设施故意将 DHCP 消息从 /var/log/messages 中排除:

*.notice;authpriv.none;local7.none        /var/log/messages

我的 /var/log/messages syslog 配置经常随着我逐步排除每个本地设施而变得相当长,但这没关系。

空格与制表符

传统的类 Unix 操作系统要求在 syslog.conf 中的列之间使用制表符(tabs),但 FreeBSD 允许使用空格。如果你在不同的操作系统之间共享相同的 syslog.conf,请确保只使用制表符。

syslogd 自定义

FreeBSD 默认运行 syslogd,开箱即用,它可以作为日志主机使用。你可以通过使用命令行标志来定制它的工作方式。你可以在命令行或在 rc.conf 中指定标志,形式为 syslogd_flags

允许的日志发送者

你可以指定 syslogd(8) 接受来自哪些主机的日志消息。这可以防止你接受来自互联网上随机人的日志。虽然发送大量日志可以用来填满硬盘,作为攻击的前期准备,但更有可能是配置错误的结果。无论如何,你的日志服务器应该通过防火墙进行保护。使用 -a 标志来指定可以向你发送日志消息的主机的 IP 地址或网络,以下两个(互斥)示例展示了这一点:

syslogd_flags="-a 192.168.1.9"
syslogd_flags="-a 192.168.1.0/24"

虽然 syslogd(8) 也接受 DNS 主机名和域名作为这一限制,但 DNS 是一个完全不适合用作访问控制机制的工具。

你可以通过指定 -s 标志来完全禁用接受远程主机的消息,这是 FreeBSD 的默认设置。如果你改用 -ss,syslogd(8) 还会禁用向远程主机发送日志消息。使用 -ss 会将 syslogd(8) 从 sockstat(1) 和 netstat(1) 显示的网络感知进程列表中移除。尽管这个半开放的 UDP 套接字无害,但有些人更喜欢 syslogd(8) 完全不显示在网络上。

绑定到单一地址

syslogd(8) 默认绑定到系统所有 IP 地址上的 UDP 514 端口。你的监狱服务器需要 syslogd,但监狱机器只能运行绑定到单一地址的守护进程。使用 -b 标志强制 syslogd(8) 绑定到单一 IP 地址:

syslogd_flags="-b 192.168.1.1"
附加日志套接字

syslogd(8)可以通过 Unix 域套接字以及通过网络接受日志消息。标准位置是/var/run/log。然而,系统上没有任何 chroot 进程可以访问这个位置。如果你希望这些 chroot 进程能够运行,你必须配置它们通过网络进行日志记录,或者为它们提供一个额外的日志套接字。使用-l标志来实现这一点,并指定额外日志套接字的完整路径:

syslogd_flags="-l /var/named/var/run/log"

named(8)和 ntpd(8)程序是 FreeBSD 的一部分,通常会被 chroot。/etc/rc.d/syslogd足够智能,可以在你通过rc.conf配置这些程序的 chroot 时,自动添加适当的 syslogd 套接字。

详细日志记录

使用详细模式记录日志(-v)会打印本地日志中每条消息的数字设施和级别。使用双重详细日志记录会打印设施和级别的名称,而不是数字:

syslogd_flags="-vv"

这些是我认为最常用的标志。有关完整的选项列表,请参阅 syslogd(8)。

日志文件管理

日志文件会增长,你必须决定它们可以增长到什么大小后进行修剪。标准的方法是通过日志轮换。当日志被轮换时,最旧的日志会被删除,当前日志文件会关闭并重新命名,并为新的数据创建一个新的日志文件。FreeBSD 包含一个基本的日志文件处理器 newsyslog(8),它还会压缩文件、重启守护进程,并一般处理所有常规的日志文件操作。cron(1)每小时运行一次 newsyslog(8)。

当 newsyslog(8)运行时,它会读取/etc/newsyslog.conf/etc/newsyslog.conf.d/中的文件。/etc/newsyslog.conf文件用于核心系统功能,而/etc/newsyslog.conf.d/中的文件用于附加软件。newsyslog 程序尝试将/etc/newsyslog.conf.d/中的任何文件解析为 newsyslog 配置文件。两者使用相同的格式,因此为了清晰起见,我们将参考newsyslog.confnewsyslog.conf中的每一行给出了轮换一个日志文件的条件。如果满足轮换日志的条件,日志会被轮换,并根据需要执行其他操作。/etc/newsyslog.conf每个日志文件使用一行;每一行有七个字段,如下所示:

/var/log/ppp.log        root:network    640  3     100  *     JC

让我们逐个检查每个字段。

日志文件路径

每行的第一个条目(例如示例中的/var/log/ppp.log)是要处理的日志文件的完整路径。

所有者和组

第二个字段(在我们的示例中为root:network)列出了轮换文件的所有者和组,它们由冒号分隔。这个字段是可选的,并且在许多标准条目中并不存在。

newsyslog(8)可以更改旧日志文件的所有者和组。默认情况下,日志文件由 root 用户和 wheel 组拥有。虽然更改所有者并不常见,但在多用户机器上你可能需要此功能。

你还可以选择只更改所有者或仅更改组。在这种情况下,你可以在冒号的两侧只使用一个名称。例如,:www将组更改为 www,而mwlucas:则将文件的所有权转给我。

权限

第三个字段(在我们的示例中是640)给出了标准 Unix 三位数字表示法中的权限模式。

计数

该字段指定 newsyslog(8)应保留的最旧旋转日志文件。newsyslog(8)按从最新到最旧的顺序编号存档日志,从最新的日志为日志 0。例如,使用默认的count5,对于/var/log/messages,你将看到以下日志消息:

messages
messages.0.bz
messages.1.bz
messages.2.bz
messages.3.bz
messages.4.bz
messages.5.bz

会算数的人会意识到,这样会有六个归档文件,而不是五个,加上当前的日志文件,共有一周的日志。通常来说,日志文件太多总比太少好;但是,如果磁盘空间紧张,删除一个或两个额外的日志文件可能会为你争取一些时间。

大小

第五个字段(在我们的示例中是100)表示文件的大小,以千字节为单位。当 newsyslog(8)运行时,它会将此处列出的大小与文件的实际大小进行比较。如果文件大于此处给定的大小,newsyslog(8)会旋转该文件。如果你不希望文件大小影响文件何时旋转,可以在这里放置一个星号。

时间

到目前为止,这似乎很简单,对吧?第六个字段,旋转时间,改变了这一点。时间字段有四种不同的合法值类型:星号、数字和两种日期格式。

如果你希望基于日志大小而非年龄进行旋转,请在这里放置一个星号。

如果你在此字段中填写一个普通的数字,newsyslog(8)将在经过该小时数后旋转日志。例如,如果你希望日志每 24 小时旋转一次,但不关心具体的时间,可以在这里填写24

日期格式稍微复杂一些。

ISO 8601 时间格式

任何以@符号开头的条目都采用限制版 ISO 8601 时间格式。这是 newsyslog(8)在大多数类 Unix 系统上使用的标准格式;它也是 MIT 最初版本的 newsyslog(8)所采用的时间格式。限制版 ISO 8601 稍显晦涩,但每个类 Unix 操作系统都支持它。

限制版 ISO 8601 格式的完整日期是 14 位数字,中间有一个T。前四位是年份,接下来的两位是月份,再接下来的两位是日期。T作为一种十进制点插入在中间,将整天与部分天数分开。接下来的两位是小时,接下来的两位是分钟,最后两位是秒数。例如,2008 年 3 月 2 日晚上 9:15:08 的日期在限制版 ISO 8601 中表示为20080302T211508

完整的受限 ISO 8601 日期格式比较简单,但当你没有列出完整日期时,容易产生混淆。你可以选择仅指定靠近 T 的字段,其他字段留空。空字段是通配符。例如,1T 匹配每个月的第 1 天。4T00 匹配每个月第 4 天的午夜。T23 匹配每天的第 23 小时,即晚上 11 点。使用 newsyslog.conf 配置的 @T23,日志会在每天晚上 11 点旋转。

与 cron(1) 一样,你必须详细指定时间单位。例如,@7T,表示每月第七天,日志每小时旋转一次,持续整天。毕竟,它匹配整天!@7T01 会在每月第七天的凌晨 1 点旋转日志,这通常是更理想的设置。然而,你不需要更精确的细节,因为 newsyslog(8) 只每小时运行一次。

FreeBSD 特有时间

受限 ISO 8601 时间系统不允许你轻松指定每周任务,也无法指定每月的最后一天。因此,FreeBSD 提供了一种时间格式,让你能够轻松执行这些常见任务。任何以美元符号($)开头的条目,都是采用 FreeBSD 特有的 月周日 格式编写的。

该格式使用三个标识符:M(月份的日期),W(星期几),和 H(一天中的小时)。每个标识符后跟一个数字,表示特定的时间。小时数从 0 到 23,星期几从 0(星期天)到 6(星期六)。月份的日期从 1 开始,直到月末,L 表示每月的最后一天。例如,要在每个月的第 5 天中午进行日志旋转,我可以使用 $M5H12。要在每个月的最后一天晚上 10 点开始月末日志统计,可以使用 $MLH22

按大小和时间旋转

你可以在特定时间、达到某个大小时,或两者都满足时旋转日志。如果你同时指定大小和时间,日志将在满足任一条件时旋转。

标志

flags 字段指定日志旋转时要执行的任何特殊操作。这通常告诉 newsyslog(8) 如何压缩日志文件,但你也可以在日志被旋转时通知进程。

日志文件格式和压缩

日志可以是文本文件或二进制文件。

二进制文件只能以非常特定的方式写入。newsyslog(8) 每次创建新的日志时,都会添加“日志文件已翻转”的信息,但将此文本添加到二进制文件会损坏它。B 标志告诉 newsyslog(8),这是一个二进制文件,不需要此头信息。

其他日志文件以普通 ASCII 文本形式写入,newsyslog(8) 可以并且应该在文件顶部添加一个时间戳消息,指示日志何时被旋转。如果你使用 UFS,压缩旧日志文件可以节省大量空间。-J 标志告诉 newsyslog(8) 使用 bzip(1) 压缩归档文件;-Z 标志指定 gzip 压缩;-X 标志表示 xz(1) 压缩;-Y 标志表示新兴的 zstd(1) 压缩方式。

然而,如果你使用 ZFS,文本日志文件会在数据集层与其他可压缩文件一起被压缩。你仍然可以按传统方式压缩日志文件,但这样做没有任何优势。此外,在查看文件之前,你需要手动解压它们。让 ZFS 为你处理压缩。

特殊日志文件处理

当它创建并旋转日志文件时,newsyslog(8) 可以执行一些特殊任务。以下是最常见的任务;你可以在 newsyslog.conf(5) 中阅读关于其他任务的内容。

或许你有许多类似的日志文件,想要以相同方式处理。-G 标志告诉 newsyslog,行首的日志文件名实际上是一个 shell 通配符,并且所有匹配该表达式的日志文件都应按这种方式旋转。要了解 shell 表达式,请阅读 glob(3)。准备好带上止痛药。

你可能希望 newsyslog 在文件不存在时创建一个文件。为此,请使用 -C 标志。syslogd 程序不会记录到不存在的文件。

-N 标志明确告诉 newsyslog 在旋转此日志时不要发送信号。

最后,当你不需要使用这些标志时,使用短横线(-)作为占位符。它在此处创建一列,以便你可以放置比如 pidfile 路径。

Pidfile

下一个字段是一个 pidfile 路径(在我们的示例中未显示,但请查看 /etc/newsyslog.conf 以了解几个示例)。pidfile 记录程序的进程 ID,以便其他程序可以轻松查看它。如果你列出 pidfile 的完整路径,newsyslog(8) 会在旋转日志时向该程序发送 kill -HUP 信号。这会通知该进程关闭其日志文件并重新启动自己。并非所有进程都有 pidfile,也并非所有程序在旋转日志时都需要这种特殊处理。

信号

大多数程序在收到 SIGHUP 信号时会执行日志文件旋转,但一些程序在日志旋转时需要特定的信号。你可以在最后一个字段(pidfile 后)列出所需的确切信号。

示例 newsyslog.conf 条目

让我们将所有这些组合起来,形成一个最糟糕的、你绝对不会相信的示例。一个数据库日志文件需要在每个月的最后一天的晚上 11 点进行旋转。数据库文档说明,在旋转日志时,必须向服务器发送一个中断信号(SIGINT,即信号编号 2)。你希望归档日志文件的所有权属于用户 dbadmin,且仅该用户可以查看。你需要保留六个月的日志。更重要的是,这些日志是二进制文件。你的 newsyslog.conf 条目看起来将是这样的:

/var/log/database    dbadmin:   400   6   *   $MLH23   B   /var/run/db.pid   2

这是一个故意设计的恶劣示例;在大多数情况下,你只需要输入文件名和轮换条件,就能完成。

FreeBSD 与 SNMP

通过电子邮件发送的报告很好,但比较笼统,日志也很难分析长期趋势。网络、服务器和服务管理的行业标准是简单网络管理协议(SNMP)。许多不同的厂商支持 SNMP 作为一种协议,用于从网络上许多不同的设备收集信息。FreeBSD 包括一个 SNMP 代理 bsnmpd(8),不仅提供标准的 SNMP 功能,还能展示 FreeBSD 特有的功能。

FreeBSD 的bsnmpdBegemot SNMPD的缩写)是一个极简主义的 SNMP 代理,专为可扩展性设计。所有实际功能通过外部模块提供。FreeBSD 包括用于标准网络 SNMP 功能的 bsnmpd 模块以及用于 FreeBSD 特定功能的模块,如 PF 和 netgraph(4)。bsnmpd(8)并不是想做所有事情,而是提供一个基础,使每个人都可以构建只做他们所需的功能的 SNMP 实现,既不多也不少。

SNMP 101

SNMP 工作在经典的客户端-服务器模型上。SNMP 客户端,通常是某种管理工作站或监控服务器,向 SNMP 服务器发送请求。SNMP 服务器,也称为代理,从本地系统收集信息并将其返回给客户端。FreeBSD 的 SNMP 代理是 bsnmpd(8)。

SNMP 代理还可以发送请求以对 SNMP 服务器进行更改。如果系统正确(或不正确,取决于你的观点)配置,你可以通过 SNMP 发出命令。这个“写”配置通常用于路由器、交换机和其他嵌入式网络设备。大多数类 Unix 操作系统有命令行管理系统,通常不接受通过 SNMP 发出的指令。通过 SNMP 写入系统配置或发出命令需要仔细的设置,并会引发各种安全问题;这本身就是一本书的好话题。我认识的没有哪位系统管理员会通过 SNMP 管理他们的系统。考虑到这一点,我们将专注于只读 SNMP。

除了让 SNMP 服务器响应来自 SNMP 客户端的请求外,客户端还可以向网络上其他地方的陷阱接收器发送 SNMP陷阱。SNMP 代理会在服务器发生特定事件时生成这些陷阱。SNMP 陷阱很像 syslogd(8)消息,只是它们遵循 SNMP 所要求的非常具体的格式。FreeBSD 目前不包括 SNMP 陷阱接收器;如果需要,可以查看来自 net-snmp 的 snmptrapd(8)(net-mgmt/net-snmp)。

SNMP MIBs

SNMP 通过管理信息库(MIB)来管理信息,MIB 是一个树状结构,包含以 ASN.1 格式表示的层级信息。我们之前见过一个 MIB 树的例子:在第六章中讨论的 sysctl(8)接口。

每个 SNMP 服务器都有一个它可以从本地计算机中提取信息的列表。服务器将这些信息按层次结构排列成一棵树。每个 SNMP MIB 树都有一些非常通用的主要类别:网络、物理、程序等,每个类别下有更具体的子分类。可以把这棵树想象成一个组织良好的文件柜,其中每个抽屉存放着特定的信息,而抽屉中的文件则存放着具体的事实。类似地,最上层的 MIB 包含了它下属的 MIB 列表。

MIB 可以通过名称或数字来引用。例如,下面是从一个示例系统中提取的 MIB:

interfaces.ifTable.ifEntry.ifDescr.1 = STRING: "em0"

这个 MIB 中的第一个术语 interfaces 告诉我们,我们正在查看这台机器的网络接口。如果这台机器没有接口,第一类甚至不会存在。ifTable 是接口表,或者说是系统上所有接口的列表。ifEntry 显示某个特定接口,ifDescr 表示我们正在查看该接口的描述。这个 MIB 可以总结为:“这台机器上的接口 1 叫做 em0。”

MIB 可以用数字表示,大多数 SNMP 工具本地使用数字 MIB 进行工作。大多数人喜欢使用文字,但你的大脑必须能够处理任意一种方式。MIB 浏览器可以帮助你在数字形式和文字形式的 SNMP MIB 之间进行转换,或者你可以安装 net-mgmt/net-snmp 并使用 snmptranslate(1),但现在,暂时相信我。前面的示例可以转换为:

.1.3.6.1.2.1.2.2.1.2.1

用文字表示时,这个 MIB 由 5 个以点分隔的术语组成。用数字表示时,这个 MIB 有 11 个部分。如果它们应该是同一个东西,那么这看起来就不太对劲了。怎么回事?

数字表示的 MIB 更长,因为它包含了默认的 .1.3.6.1.2.1,它表示 iso.org.dod.internet.mgmt.mib-2。这是互联网上使用的标准 MIB 子集。绝大多数 SNMP MIB(但不是全部)都有这个前缀,因此大家通常不再写它。

如果你处于那种难以抑制的情绪中,你甚至可以混合使用文字和数字:

.1.org.6.1.mgmt.1.interfaces.ifTable.1.2.1

在这一点上,国际条约允许你的同事们用火把和叉子把你赶出大楼。选择一种表示 MIB 的方式并坚持使用它。

MIB 定义和 MIB 浏览器

MIB 是根据非常严格的语法定义的,并且记录在 MIB 文件 中。每个 SNMP 代理都有自己的 MIB 文件;bsnmpd 的 MIB 文件位于 /usr/share/snmp。这些文件是非常正式的纯文本格式。尽管你可以仅凭大脑阅读和理解它们,但我强烈建议将它们复制到工作站并安装一个 MIB 浏览器,以便更容易地理解它们。

MIB 浏览器解释 MIB 文件,并以完整的树形结构呈现它们,包括每个树的定义和每个 MIB 的描述。一般来说,MIB 浏览器让你输入一个特定的 MIB,并显示该 MIB 的数字和文字定义,同时查询 SNMP 代理以获取该 MIB 的状态。

如果你的工作站上运行 FreeBSD(或其他较小的 Unix 系统),可以使用 mbrowse(net-mgmt/mbrowse)进行 MIB 浏览。如果你不想使用图形界面来进行 SNMP 操作,可以查看 net-snmp(net-mgmt/net-snmp),它提供了一整套命令行 SNMP 客户端工具。

SNMP 安全性

许多安全专家表示,SNMP 实际上代表着“安全:不是我的问题!”这虽然有些刻薄,但确实很真实。SNMP 应该只在受信任的网络上的防火墙后使用。如果你必须在裸露的互联网中使用 SNMP,请使用数据包过滤来防止公众查询你的 SNMP 服务。SNMP 代理运行在 UDP 端口 161 上。

更常见的 SNMP 版本 1 和 2c 不提供加密。这意味着任何拥有数据包嗅探器的人都可以捕获你的 SNMP 社区名,所以一定要确保只在私有网络上使用 SNMP。在不受信任的网络上发起未加密的 SNMP 查询,是让陌生人窥探你系统管理的好方法。SNMP 版本 3 使用加密来保护数据传输。

SNMP 通过社区提供基本的安全性。如果你去查找相关内容,你会发现各种解释,说明为什么社区与密码不同,但社区就是密码。大多数 SNMP 代理默认有两个社区:public(只读访问)和 private(读写访问)。是的,默认有一个提供读写访问的社区。每当你为任何主机上的任何操作系统配置 SNMP 代理时,首先要做的就是禁用这些默认的社区名称,并用那些几十年来未被广泛记录的名称来替代。

FreeBSD 的 bsnmpd(8)默认使用 SNMPv2c,但也可以使用 SNMPv3。SNMPv3 是一个更复杂的协议,所以我们在这里不讨论它。如果你理解 SNMPv3 协议和配置 FreeBSD 的 bsnmpd 的基础知识,那么启用 SNMPv3 将不会有任何问题。

配置 bsnmpd

在你可以使用 SNMP 来监控系统之前,必须配置 SNMP 守护进程。配置 bsnmpd(8)文件位于/etc/snmpd.config。除了包含公共(public)和私有(private)的默认社区外,默认配置不会启用任何使得 bsnmpd(8)具有吸引力的 FreeBSD 特定功能。

bsnmpd 变量

bsnmpd 使用变量为配置语句赋值。大多数高可见性的变量在配置文件的顶部设置,正如你在这里看到的:

location := "Room 200"
contact := "sysmeister@example.com"
system := 1     # FreeBSD
traphost := localhost
trapport := 162

这些顶部的变量定义了应该在每个 SNMP 代理上设置的 MIB 值。位置描述了机器的物理位置。每个系统都需要一个合法的电子邮件联系方式。bsnmpd(8)也运行在 FreeBSD 以外的操作系统上,因此你可以选择在这里设置特定的操作系统。最后,如果你有陷阱主机,你可以在这里设置服务器名称和端口。

在文件的下方,你可以设置 SNMP 社区名称:

# Change this!
read := "public"
# Uncomment begemotSnmpdCommunityString.0.2 below that sets the community
# string to enable write access.
write := "geheim"
trap := "mytrap"

read字符串定义了该 SNMP 代理的只读社区。默认配置文件建议你更改它。听取这个建议。write字符串是读写社区名称,默认在配置文件的下方被禁用。你还可以设置该代理发送的 SNMP 陷阱的社区名称。

仅凭这一配置,bsnmpd(8)将启动、运行并为你的网络管理系统提供基本的 SNMP 数据。只需在/etc/rc.conf中设置bsnmpd``_enable="YES",以便在启动时启动 bsnmpd。然而,你不会获得任何特别的 FreeBSD 功能。让我们继续,看看如何管理这个。

详细的 bsnmpd 配置

bsnmpd(8)使用你在配置文件顶部设置的变量,稍后在配置中为不同的 MIB 分配值。例如,在文件的顶部你将变量read设置为public。稍后在配置文件中,你会看到如下语句:

begemotSnmpdCommunityString.0.1 = $(read)

这将 MIB begemotSnmpdCommunityString.0.1设置为read变量的值。

为什么不直接设置这些值呢?bsnmpd(8)被专门设计为可扩展和可配置的。在文件顶部设置几个变量比直接编辑文件下方的规则要容易得多。

让我们回到这里设置的begemotSnmpdCommunityString MIB。我们为什么要设置这个?在 MIB 浏览器中搜索这个字符串,你会看到这是定义 SNMP 社区名称的 MIB。你可能已经从read变量的赋值中猜到这一点,但确认一下也不错。

同样,你会找到类似这样的条目:

begemotSnmpdPortStatus.0.0.0.0.161 = 1

检查 MIB 浏览器显示,这决定了 bsnmpd(8)绑定的 IP 地址和 UDP 端口(在本例中,所有可用地址,端口为 161)。所有 MIB 配置都是以这种方式完成的。

加载 bsnmpd 模块

大多数有趣的 bsnmpd(8)功能是通过模块配置的。通过在配置文件中为begemotSnmpdModulePath MIB 指定模块处理的类和实现该功能的共享库的完整路径来启用模块。例如,在默认配置中,你会看到 PF bsnmpd(8)模块的注释掉的条目:

begemotSnmpdModulePath."pf"    = "/usr/lib/snmp_pf.so"

这启用了 PF MIB 的支持。当你启用此选项时,你的网络管理软件将能够直接查看 PF,允许你跟踪从丢失的包到状态表大小的所有信息。

截至本文撰写时,FreeBSD 的 bsnmpd(8) 包含了以下模块,默认禁用。有些模块是 FreeBSD 特有的,而另一些支持行业标准。通过取消注释它们的配置文件条目并重新启动 bsnmpd 来启用这些模块。

lm75 通过 SNMP 提供来自 lm75(4) 温度传感器的数据。

Netgraph 提供了所有基于 Netgraph 的网络功能的可视化,详细信息见 snmp_netgraph(3)。

PF 提供了 PF 包过滤器的可视化。

Hostres 实现了主机资源 SNMP MIB,见 snmp_hostres(3)。

bridge 提供了桥接功能的可视化,详细信息见 snmp_bridge(3)。

wlan 访问有关无线网络的信息。

启用这些模块后,重新启动 bsnmpd(8)。如果程序无法启动,请检查 /var/log/messages 中的错误信息。

使用 bsnmpd(8)、syslogd(8)、状态邮件和各种性能分析工具,你可以让你的 FreeBSD 系统成为网络中监控最强的设备。现在你已经可以看到系统的所有功能了,让我们拿起手电筒,探索一下 FreeBSD 的一些隐秘角落。

第二十二章:JAILS

image

虚拟化将操作系统实例与底层硬件分离。虚拟化使你可以通过复制文件将操作系统安装从一台硬件迁移到另一台硬件。虚拟化需要在硬件上安装操作系统,但该安装通常非常精简,没有面向公众的服务,并且可以在新硬件上轻松重建。这或许是几十年来系统管理中最大的变化。

虚拟化有点像客户端-服务器环境。硬件及其核心操作系统实例是 宿主,而 客户端 是所有虚拟化的操作系统实例。客户端依赖宿主提供基本服务,如存储、处理器性能和内存。对宿主的更改可以反映在虚拟化的客户端中,但客户端上的更改不会对宿主产生影响,除了消耗资源之外。

FreeBSD 支持两种类型的虚拟化:jails 和 bhyve。

Jail 是一种轻量级虚拟化方法,有时称为 操作系统级虚拟化。一个 jail 通常包含一个完整的操作系统用户空间,运行在现有 FreeBSD 系统之上。jail 依赖于宿主系统的文件系统,但只限于目录树的一个子集。它甚至可能在 ZFS 池中有一块专用空间。一个 jail 没有自己的内核,而是在宿主系统的内核限制区域内运行。宿主系统可以在不进入 jail 的情况下管理其中的进程,或者如果需要,也可以在 jail 内运行进程。Jail 不提供图形控制台。使用 jail 来虚拟化相同版本或更早版本的 FreeBSD 安装,或运行简单的虚拟 Linux 系统。

Bhyve 是一个更重的虚拟化系统。与使用宿主的内核和文件系统不同,bhyve 模拟硬件。宿主系统为虚拟机提供一块磁盘空间作为存储。一个 bhyve 虚拟机必须携带自己的文件系统、内核和支持基础设施。与 jails 相比,bhyve 虚拟机需要更多的资源,但它们还提供通过虚拟网络计算(VNC)的控制台,并且可以运行真正的外来操作系统,如 Microsoft Windows。由于 bhyve 的快速发展,它正在迅速变化,因此本书没有涵盖它。等到 bhyve 稳定后,我会再写关于它的内容。

在考虑 bhyve 之前,先看看 jail 是否能满足你的需求。

Jail 基础

Jail 是一种增强版的 chroot,不仅对文件系统适用,还对进程和网络栈进行限制。一个被限制的系统只能访问文件系统的一小部分,无法看到 jail 外的进程。传统上,每个 jail 都分配一个专用的 IP 地址,并且该 jail 只能查看该 IP 的流量。每个 jail 甚至有自己的用户账户。在 jail 中,root 账户完全控制该 jail,但无法访问 jail 之外的任何内容。

对于一个拥有 root 权限的监狱用户来说,监狱看起来像一个几乎完整的 FreeBSD 系统,只缺少一些设备节点。用户可以安装任何她喜欢的软件,而不会干扰主机或其他监狱。监狱中运行的所有进程只能影响监狱的文件和进程。被监禁的用户无法看到监狱之外的任何内容;她被限制在监狱中。如果监狱被黑客入侵,入侵者也会被限制在监狱中。

监狱可以使用基于 vnet(9) 的虚拟网络栈。这是一种高级用法,本文不涉及,但如果你需要为监狱提供自己的路由表,可以按照这种方式操作。

从 FreeBSD 9 开始,多个监狱可以共享一个 IP 地址,但系统管理员需要为每个监狱配置唯一的 TCP/IP 端口来提供每个网络服务。你不能在同一个 IP 的端口 22 上运行多个 SSH 实例!为了简便起见,以下示例为每个监狱使用一个 IP,但请记住,你还有其他选择。

许多人将所有服务都放在监狱中,即使主机是专门用于某个特定目的。监狱数据集的 ZFS 快照,或目录树的 tarball,都是监狱的完整备份。在软件升级失败后恢复变得非常简单,只需要提取 tarball 或回滚到快照。

监狱环境对于软件开发和测试也非常有用。部署一个新服务通常需要安装和测试多个软件包。在选择解决方案并进入生产阶段之前,在监狱环境中进行测试可以防止将不再使用的文件和不需要的软件污染主机。

根据你的硬件和系统负载,单个 FreeBSD 主机可以支持几十个甚至几百个监狱。然而,如果你打算真正运行这么多监狱,确保你的主机有两个独立的网络接口。一个用于监狱,另一个用于管理主机。

一切从配置你的监狱主机开始。

监狱主机服务器设置

作为监狱主机的服务器必须在一些令人烦恼的约束条件下运行。在建立第一个监狱之前,请正确配置你的主机。

监狱系统有自己的 sysctl 树,security.jail。你只能从主机系统更改这些 sysctl。一些 sysctl 会影响主机上所有运行的监狱。以 security.jail.param 开头的 sysctl 可以按监狱逐个设置。我们将在本章中逐步讲解这些内容。

监狱主机存储

我强烈建议你仅将监狱主机用于运行监狱,并将所有服务都放入监狱中。首先配置主机的存储,将监狱和主机操作系统分开。

许多用于虚拟化的主机在主板上包括用于操作系统的 SATA DOM 闪存驱动器。这些驱动器的大小通常小于 100GB,但 FreeBSD 的基础安装占用的空间远小于一个 GB。如果你有 SATA DOM 或类似设备,可以用它来安装主机操作系统。如果你有多个冗余硬盘集,可以用一对来镜像操作系统,其他的空间则专门用于监狱。

如果你没有这样的硬件,可以将空间分配给主机操作系统。可以使用 UFS 文件系统的分区,或者 ZFS 的数据集预留。无论哪种方式,10GB 的空间应该足够。如果你需要额外的紧急空间,可以从监狱空间借用一些。

虽然 ZFS 对监狱非常有用,但并不是必需的。我曾经在 UFS 上运行监狱多年。使用适合你并符合你环境的方案。

一旦你分区并安装了操作系统,接下来就可以查看网络配置了。

监狱网络配置

监狱网络配置有两个看似冲突的方面:首先,每个监狱期望完全控制分配给它的任何 IP 地址;其次,监狱可以与其他监狱甚至主机共享 IP 地址。你可以使用主机的任何 IP 地址来启动监狱,但该监狱不能与在该 IP 上运行的其他服务协调任何面向网络的服务。如果你的监狱与主机共享 IP 地址,而主机在端口 22 上运行 SSH,监狱就不能使用端口 22。如果你尝试在监狱中启动 sshd(8),程序会抱怨不能使用端口 22,并崩溃。监狱之间,或者监狱与主机之间共享 IP 地址,要求系统管理员协调各个端口属于哪个主机,并相应配置所有服务。

配置监狱的最简单方法是为每个监狱分配一个独立的 IP 地址,并为主机分配一个自己的 IP 地址。这样,每个监狱就可以完全控制自己的 IP 地址。一旦熟悉了这种配置,你可以开始在监狱之间共享 IP 地址。这样主机就不能在分配给监狱的 IP 地址上运行守护进程。如果主机的守护进程在监狱的 IP 地址上监听,虽然不会阻止监狱启动,但会阻止监狱在该端口上启动自己的服务。如果用户像 Bert 一样无法 SSH 进入他们的私有监狱,他们会抱怨的!

配置监狱主机的最简洁方法是决定主机仅提供监狱服务。任何在主机上运行的服务必须都在监狱中。如果你需要简单的服务,比如名称服务器或邮件交换机,可以在监狱中配置它们。这不仅比重新配置所有这些服务器并将它们仅附加到选定 IP 地址要容易,而且还为你的其他监狱提供了一层额外的安全性。如果主机被入侵,入侵者会自动获得对所有监狱的访问权限,而入侵单个监狱则会将入侵者限制在该监狱中。

使用 sockstat(1) 来识别网络上监听的程序,正如在 第九章 中讨论的那样。添加 -46 标志以仅显示 IPv4 和 IPv6 流量,使用 -l 标志以仅显示监听套接字。

# sockstat -46l
USER     COMMAND  PID   FD PROTO  LOCAL ADDRESS     FOREIGN ADDRESS
root     ntpd     19776 20 udp6   *:123             *:*
root     ntpd     19776 21 udp4   *:123             *:*
--snip--
root     sshd     2846  3  tcp6   *:22              *:*
root     sshd     2846  4  tcp4   *:22              *:*

这个相当默认的 FreeBSD 安装有两个程序在网络上监听:ntpd 和 sshd。两者都在监听所有 IP 地址。我们必须配置这些守护进程,使它们仅监听主机地址。

这里是一些在主机服务器上常见的会引发问题的守护进程。在所有这些例子中,我假设监狱主机的 IP 地址是 198.51.100.50。

syslogd

系统日志记录器 syslogd(8) 打开一个 UDP 套接字,以便将消息发送到其他主机。如果你不需要远程日志记录,或者使用其他日志记录解决方案,可以在 rc.conf 中使用 -ss 标志关闭网络组件。

syslogd_flags="-ss"

如果你需要发送 syslogd 消息,可以使用 -b 标志强制 syslogd 仅附加到一个 IP 地址。

syslogd_flags="-b 198.51.100.50"

这两种解决方案允许你的监狱管理员单独决定是否跨网络进行日志记录。有关 syslogd 的完整讨论,请参见 第二十一章。

inetd

如果你需要 inetd(参见 第二十章),你几乎可以肯定应该在监狱中运行它,而不是在主机上。如果你实在无法避免在主机上运行 inetd,使用 -a 标志将它限制为一个 IP 地址,就像以下 rc.conf 片段所示。

inetd_flags="-a 198.51.100.50 -wW -C 60"

如果我只指定了 -a 标志和 IP 地址,它会覆盖 inetd 在 /etc/defaults/rc.conf 中的默认标志。过去几十年的每个 FreeBSD 版本都使用默认标志 -wW -C 60;我在这些标志上添加了我的 -a 和 IP 地址。

sshd

/etc/ssh/sshd_config 中的 ListenAddress 选项告诉 sshd(8) 绑定哪些地址。将其限制为仅你的主机 IP 地址。

ListenAddress 198.51.100.50

如果你的监狱主机提供的唯一服务是 sshd(8),那么你已经做得很好了。

NFS

网络文件系统程序,如 rpcbind(8) 和 nfsd(8),会绑定主机上的所有 IP 地址,无论你做什么。不要在监狱内运行这些程序,也不要在监狱内运行 NFS。如果你的客户端需要 NFS 挂载,请让主机运行这些程序并提供 NFS 挂载。

网络时间协议

在监狱主机上最具问题的服务是时间同步。所有监狱都从主机获取系统时钟。FreeBSD 内建的时间守护进程 ntpd(8) 会监听主机上的所有 IP 地址,包括监狱内的 IP 地址。作为唯一的特殊例外,我建议你继续在主机上运行 ntpd。

监狱缺乏适当的权限来更改内核的时间。虽然你可以在监狱中运行 ntpd,但它实际上无法任何事情。可以在你的监狱主机上运行 ntpd,别担心它会监听所有 IP 地址。任何试图在 123 端口运行除 ntpd 以外的基于 UDP 的服务的人,可能是想绕过数据包过滤器。让他们更努力地工作吧。

如果你想避免即使是碰撞的可能性,安装 openntpd 包。与基础系统中的 ntpd(8) 不同,OpenNTPD 可以配置为只监听一个 IP 地址。

IP 地址

每个监狱可以有一个或多个 IP 地址。这些地址必须在启动监狱之前附加到主机。一个没有网络的监狱也可以运行,但无法在主机之外访问。将任何必要的 IP 地址作为别名添加到 /etc/rc.conf 中。

监狱在启动时

要让 FreeBSD 在启动时启动你的监狱,设置 jail_enablerc.conf 中。

jail_enable=YES

FreeBSD 默认会启动 /etc/jail.conf 中列出的所有监狱。如果你希望系统只在启动时启动其中一部分监狱,可以使用 jail_list rc.conf 选项。在这里,我有两个监狱,叫 mariadbhttpd。我希望它们按此顺序启动,以确保我的数据库监狱在调用它的 Web 服务器之前启动。

jail_list="mariadb httpd"

在系统关闭过程中,FreeBSD 会以启动时的顺序停止监狱。你的应用程序可能不喜欢这种顺序。以我的例子为例,我希望 Web 服务器在数据库后端之前关闭。我宁愿让网站完全无法访问,也不想让用户看到令人讨厌的“数据库服务器已崩溃”错误。

jail_reverse_stop=YES

如果监狱的启动顺序不重要,你可以同时启动和停止所有监狱。

jail_parallel_start="YES"

现在你可以配置监狱了。

监狱设置

现在我有了一台主机,可以安装一些监狱了。我将从一个叫 mariadb 的监狱开始,用来运行……等一下……MariaDB。

每个监狱需要一个专用的根目录。我的所有示例监狱都位于 /jail 下。我通常会将每个监狱放在一个以监狱名称命名的目录中——在这个例子中是 /jail/mariadb

每个监狱需要一个主 IP。它也可以有其他 IP,稍后我们将看到,但我们先从一个开始。监狱 mariadb 获取 203.0.113.51。

每个监狱都需要一个互联网主机名,就像它是一个真实主机一样。这个监狱将成为 mariadb.mwl.io

现在我们可以在监狱中放置用户空间。

监狱用户空间

虽然你可以在监狱中安装任何用户空间组件,但监狱所需的仅仅是基础系统。获取适用于你所需 FreeBSD 版本的 base.txz 发行集,并将其提取到监狱的根目录中。

# tar -xpf base.txz -C /jail/mariadb

这就是基础操作系统的完整安装。如果你想要额外的发行集,如调试符号,使用相同的方式提取它们。

如果你自己构建了 FreeBSD 基础系统,可以将它安装到监狱中。

# cd /usr/src
# make installworld DESTDIR=/jail/mariadb

监狱还需要安装过程中创建的支持目录和各种杂项文件,但这些文件不会通过 make installworld 创建。make distribution 命令会创建这些文件。如果你已经有了这些目录和文件,千万不要重新运行 make distribution:它会覆盖任何本地更改。并且不要忘记 DESTDIR 设置,除非你喜欢重置主机的配置!

# make distribution DESTDIR=/jail/mariadb

你还可以构建一个只包含运行单个程序所需的二进制文件的自定义用户空间,类似于为传统的 chroot 程序构建环境。对于我们大多数人来说,这太麻烦了,但如果你想解锁 ldd(1)并尽情玩耍,我不会阻止你。

一旦你有了监狱的用户空间,就需要在/etc/jail.conf中告诉 FreeBSD 关于你的监狱。

/etc/jail.conf

传统上,FreeBSD 在/etc/rc.conf中配置监狱。这种方式笨重且不方便。虽然 FreeBSD 仍然支持通过rc.conf配置监狱,但我建议使用更加灵活的/etc/jail.conf。这个文件不是 UCL 格式,尽管它看起来像是 UCL 可以支持的东西。通过名称定义每个监狱。在监狱名称后面用大括号给出监狱的参数定义。每个参数定义以分号结束。

许多监狱参数都有一个等号,我们为参数赋值。在这里,我将参数path设置为值/jail/mariadb

path="/jail/mariadb";

其他参数仅通过存在与否来启用或禁用某个功能。在这里,我告诉这个监狱开启mount.devfs功能:

mount.devfs;

监狱支持一大堆“挂载”参数,并有不同文件系统的子参数。这个特定参数专门处理挂载 devfs。

通过在特定参数前添加no,可以关闭监狱的切换功能。如果我不想启用 devfs,我不会关闭整个挂载参数;我会在devfs前加上no

mount.nodevfs;

这是我定义名为mariadb的监狱的方法:

mariadb {
 host.hostname="mariadb.mwl.io";
 ip4.addr="203.0.113.51";
 path="/jail/mariadb";
 mount.devfs;
 exec.clean;
 exec.start="sh /etc/rc";
 exec.stop="sh /etc/rc.shutdown";
}

参数host.hostname提供了监狱的主机名。虽然监狱名称是mariadb,但此主机通过互联网主机名mariadb.mwl.io来标识自己。

IP 地址位于ip4.addr中。我已将地址 203.0.113.51 分配给这个监狱。这个 IP 地址必须先在主机上配置。

监狱的根目录放在path变量中。在这里,它被设置为/jail/mariadb

几乎每个监狱都需要访问/dev中的特定设备节点,这需要在监狱中挂载 devfs(请参见第十三章)。通过mount.devfs设置启用 devfs。监狱默认仅获取少量非常特定的设备节点。由于不受信任的用户有时可能利用设备节点逃离监狱,因此在没有仔细研究的情况下,不要添加额外的设备。您可以使用自定义的 devfs 规则集允许其他设备节点。通过 devfs_ruleset jail.conf参数将自定义 devfs 规则集分配给监狱。我强烈建议使用默认的监狱 devfs 规则作为基础,解开该监狱需要的额外设备,而不是试图从头开始构建自定义 devfs 规则集。

一个被监禁的进程可以继承其父进程的部分环境。exec.clean选项告诉 jail(8)去除除\(TERM 之外的所有环境变量。环境变量\)HOME、\(USER 和\)SHELL 会被设置为目标环境,通常是监狱的根账户环境。你几乎总是需要exec.clean

exec.startexec.stop选项告诉 FreeBSD 如何启动和停止监狱。

监狱内启动

监狱可以模拟一个完整运行的 FreeBSD 用户空间,运行单个进程,或者介于两者之间。你必须使用exec.start jail.conf 参数来告诉 FreeBSD 在监狱中运行哪个进程,或者使用persist参数声明即使没有任何进程,监狱仍然存在。在这里,我启动了一个完整的 FreeBSD 用户空间,使用了正常的 FreeBSD 启动脚本:

exec.start="/bin/sh /etc/rc"

如果你只需要在监狱内运行一个命令,你可以编写自己的启动脚本,并在监狱启动时使用exec.start来运行它。你的全新监狱还没有rc.conf,因此不会启动任何额外的进程。

你也可以设置persist选项来代替exec.start。这告诉 FreeBSD,即使监狱内没有任何进程运行,监狱也可以存在。包括persistexec.start意味着 FreeBSD 会为监狱启动一个进程,但当进程停止运行时,监狱不会自行关闭。

你可以告诉监狱在启动后使用exec.poststart选项运行一个额外的命令。任何通过exec.poststart列出的命令或脚本,在正常的/etc/rc 启动过程(包括所有启用的包)完成后会在主机中运行。这让你可以编写脚本将监狱连接起来。

类似地,你可以使用exec.prestop选项在停止监狱之前在主机上运行一个命令。当系统管理员关闭监狱时,主机会首先运行此命令,然后监狱会执行正常的关闭命令。

exec.stop命令告诉 FreeBSD 在监狱中运行哪个命令来关闭监狱。如果你在模拟一个完整的监狱,你可能会像我们在上一节中的示例一样运行/``bin/sh /etc/rc.shutdown

监狱默认设置

你会发现许多监狱共享公共设置。你可以在配置的前面定义这些设置。除非你覆盖它们,否则所有监狱都会使用这些设置。当只使用一个监狱时,这似乎没有太多意义。

exec.start="/bin/sh /etc/rc";
exec.stop="/bin/sh /etc/rc.shutdown";
exec.clean;
mount.devfs;

mariadb {
        host.hostname="mariadb.mwl.io";
        ip4.addr="203.0.113.221";
        path="/jail/mariadb";
}

然而,考虑到这个配置,添加另一个监狱将需要五行代码,包括大括号。

httpd {
        host.hostname="httpd.mwl.io";
        ip4.addr="203.0.113.222";
        path="/jail/httpd";
}

在数十个监狱的情况下,这可以节省很多麻烦。

你可以在监狱的定义中覆盖默认设置。如果我不想在监狱中挂载 devfs(5),我会为该特定监狱设置mount.nodevfs

jail.conf 变量

你可以在监狱中使用变量替换。虽然你可以定义一些这些变量,但你也可以从监狱的设置中提取一些变量。变量可以在双引号和未加引号的字符串中展开,但不能在单引号字符串中展开。

在这里,我定义了一个变量来指向包含我所有监狱的目录,并在我的监狱定义中使用它:

$j="/jail";
mariadb {
        path="$j/mariadb";
--snip--
}

如果我必须将我的监狱迁移到一个新的文件系统或池中,我可以通过更改一个变量来更新jail.conf,而不是编辑每个定义。

作为变量的参数

一旦定义了监狱参数,你就可以将其用作变量。每个监狱至少有一个参数,name。你可以使用这些参数进一步扩展默认设置。

$j="/jail";
path="$j/$name";
host.hostname="$name.mwl.io";

mariadb {
        ip4.addr="203.0.113.221";
}

通过将全局默认path设置为$j/$name,我去除了为每个单独的监狱定义路径的需求。

你可以通过将参数括在大括号中来使用带有句点的多参数。虽然对于像mount.devfs这样的参数没有太大意义,但对于每个监狱(jail)参数来说,这是有用的,比如host.hostname

path = "/jail/${host.hostname}";

我更喜欢将监狱放在以较短名称命名的目录中,而不是以主机名命名,但可以随意根据自己的偏好调整。

将参数和变量与一致的目录布局结合,可以将每个监狱定义压缩成一个配置语句。

测试和配置监狱

一旦你有了监狱的文件,就将自己锁进监狱。运行 jail(8) 命令在监狱内运行单个命令。你需要四个参数:路径、监狱名称、主 IP 和要运行的命令。

# jail <path to jail> <jail name> <jail IP> <command>

在这里,我使用位于/jail/mariadb中的名为mariadb的监狱,IP 地址为 203.0.113.51,来运行命令/bin/sh

# jail /jail/mariadb/ mariadb 203.0.113.51 /bin/sh
#

运行 ls(1)。你现在在监狱文件系统的根目录中。这个监狱还不是完全的单用户模式,但除了/bin/sh之外没有其他程序在这里运行。你可以进行一些基本设置,但连 devfs(5) 都没有挂载。

# ps
ps: /dev/null: No such file or directory

是的,正常的监狱启动过程会挂载/dev,但是监狱没有用户账户,没有根密码,没有守护进程运行,绝对没有任何可选项。在启动之前配置监狱。

从宿主机窃取的东西

一些宿主机的设置信息在监狱内也很有用。你可以将这些信息从宿主机复制到监狱中,但必须从宿主机进行,而不是从监狱内进行。

每个监狱都执行自己的 DNS 解析。你可能可以将宿主机的/etc/resolv.conf复制到监狱中。

你的监狱可能与宿主机共享相同的时区。将宿主机的/etc/localtime复制到监狱中,或者在监狱内运行 tzsetup(8)来选择一个新的时区。

创建 /etc/fstab

许多程序和脚本,包括/etc/rc,期望找到/etc/fstab,如果找不到它们会发火。要求/etc/fstab在真实服务器中是非常合理的,但监狱中的机器不需要文件系统表。创建一个空的文件系统表。

# touch /etc/fstab

我不介意不高兴的程序。我只是不想听它们抱怨。

创建 /etc/rc.conf

你可以选择从宿主机管理所有监狱,或者通过 SSH 来管理监狱。你将需要在/etc/rc.conf中为 sshd 添加条目。

sshd_enable="YES"

在创建这个文件时,添加你想要的其他设置。如果你知道某些设置是包所需要的,在它们被需要之前进行设置也不会有坏处。

用户账户和根密码

你只能在监狱内添加用户账户和更改密码。使用 passwd(1)设置 root 密码,并运行 adduser(8)至少添加一个用户,供 SSH 使用。虽然 SSH 不是访问主机的唯一方式,但在大多数情况下,它更为简便。

监狱启动和关闭

主机将每个监狱视为一个独立的服务,就像 sshd(8)、Web 服务器或任何其他守护进程一样。是的,每个监狱可能会运行一堆需要独立管理的服务,但从主机的角度来看,每个监狱都是一个包含一组进程的单一实体。这是主机和监狱之间分离的一部分。

使用 service(8)命令启动、停止和重启监狱。你需要提供一个额外的参数,即监狱的名称。FreeBSD 会在启动时自动启动监狱,但在系统运行后,你可以单独停止、启动和重启它们。让我们关闭我的数据库监狱并重新启动它。

# service jail stop mariadb
# service jail start mariadb

我可以使用重启命令,但在页面上显示效果远没有这么震撼。

如果省略监狱名称,service(8)命令将影响 FreeBSD 在启动时启动的所有监狱。

# service jail restart
Stopping jails: httpd mariadb.
Starting jails: mariadb httpd

这让你能够有条理地重新初始化你的生产监狱基础设施。

FreeBSD 默认启动/etc/jail.conf中列出的所有监狱。如在“启动时的监狱”中讨论的那样,第 568 页,你可以在/etc/rc.conf中更改此设置。service(8)命令可以控制那些未自动启动的监狱,但必须通过名称指定它们。

监狱依赖关系

如果你有很多监狱,列出/etc/rc.conf中的启动顺序可能会变得繁琐。你通常需要设置启动顺序以保持服务依赖关系。然而,你可以告诉某个监狱它依赖于另一个监狱,使用depend选项,而不是在rc.conf中定义顺序。

httpd {
        ip4.addr="203.0.113.232";
        depend=mariadb;
}

监狱 httpd 不会启动,直到监狱 mariadb 运行。depend语句会覆盖rc.conf中的jail_list条目。

管理监狱

虚拟化并不会使系统管理任务消失;它只是为执行典型的系统管理员任务提供了更多的选择。以下是一些这些选择。

查看监狱和监狱 ID

使用 jls(8)命令查看当前系统上运行的所有监狱。

# jls
   JID  IP Address      Hostname        Path
    29  203.0.113.221   mariadb.mwl.io  /jail/mariadb
    30  203.0.113.222   httpd.mwl.io    /jail/httpd
    31  203.0.113.223   test.mwl.io     /jail/test

每个监狱都有一个唯一的监狱 ID,或 JID。JID 就像进程 ID;每个监狱都有一个,但每次启动监狱时,分配给监狱的确切 JID 会有所不同。我们将使用监狱 ID 或名称来执行各种监狱管理任务。

我们还会获取每个监狱的 IP 地址、主机名以及监狱文件的路径。你不会得到监狱名称,但那些使用基于监狱名称的主机名的人不会有问题弄清楚。

监狱进程

所有被限制在监狱中的进程都会获得一个进程 ID,就像其他 Unix 进程一样。进程 ID 并不是监狱专有的,它们在主机、监狱和所有其他监狱之间共享。你不会看到重复的进程 ID。

被限制在监狱中的进程会在 ps(1)中显示,使用-J标志。

# ps -ax
  PID TT  STAT        TIME COMMAND
    0  -  DLs      2:56.24 [kernel]
    1  -  ILs      0:00.07 /sbin/init –
--snip--
35002  -  SsJ      0:00.01 /usr/sbin/syslogd -s
35129  -  IsJ      0:00.00 /usr/sbin/sshd
--snip--

进程 ID 35002 和 35129 被限制在监狱中。

使用-J标志和监狱名称,通过 ps(1)查看特定监狱的进程。

# ps -ax -J test
  PID TT  STAT    TIME COMMAND
35561  -  IsJ  0:00.01 /usr/sbin/syslogd -s
35652  -  IsJ  0:00.00 /usr/sbin/sshd
35661  -  SsJ  0:00.03 sendmail: accepting connections (sendmail)
--snip--

使用-J 0会排除所有被监禁的进程,让你更容易调试主机。

像 pgrep(1)、pkill(1)和 killall(1)这样的命令都接受一个-j参数,让你可以指定一个监狱。如果你喜欢使用 pgrep(1)查看进程信息,可以使用pgrep -lfj以及监狱名称或 JID。

# pgrep -lf -j mariadb
  PID TT  STAT    TIME COMMAND
35002  -  SsJ  0:00.01 /usr/sbin/syslogd -s
35129  -  IsJ  0:00.00 /usr/sbin/sshd
35158  -  SsJ  0:00.02 sendmail: accepting connections (sendmail)
--snip--

为什么 Sendmail 在这个监狱内运行?让我们把它杀掉。

# pkill -9 -j mariadb sendmail

再次运行 pgrep 显示 Sendmail 已经死掉了。

如果你想获取监狱中运行的进程信息,这个方法效果很好,但有时候你只有进程 ID,必须确定它属于哪个监狱。此时,你需要 ps(1)的-O选项。这个选项支持一系列关键字,可以调整 ps(1)的输出方式,这是常规命令行标志无法做到的——具体来说,-O jail会增加一个列,显示进程所在的监狱名称。

# ps -ax -O jail | grep 39415
39415 mariadb  -  IsJ    0:00.00 /usr/local/libexec/mysqld

这个进程正在监狱 mariadb 内运行。

在监狱中运行命令

jexec(8)命令允许监狱主机管理员在不需要登录到监狱的情况下执行监狱内的命令。这有助于保持监狱所有者的隐私感。^(2) 当监狱所有者 Bert 打电话求助时,我不需要他的 root 密码,甚至不需要在他的系统上有一个账户。使用 jexec 需要知道监狱的名称或 JID。在这里,我使用主机的 root 账户在我的监狱 mariadb 内运行ps -ax

# jexec mariadb ps -ax
  PID TT  STAT    TIME COMMAND
35002  -  SsJ  0:00.00 /usr/sbin/syslogd -s
35129  -  IsJ  0:00.00 /usr/sbin/sshd
--snip--

这个命令作为 root 在监狱内运行。不过,我可能希望作为另一个被监禁的用户来运行这个命令。使用-U标志指定那个用户名。

# jexec -U xistence mariadb ps
jexec: xistence: no such user

嗯,这可不太好。我期望 Bert 来运行我的数据库。让我们给他创建一个用户账户。

# jexec mariadb adduser
Username: xistence
Full name: Bert JW Regeer
Uid (Leave empty for default):
Login group [bert]:
Login group is bert. Invite bert into other groups? []: wheel
--snip--

这个监狱现在有一个 Bert 的账户,使用他喜欢的用户名和所有设置。我已经将它添加到监狱内的 wheel 组中。记住,监狱内的 root 访问权限并不等同于主机上的 root 访问权限。这就是监狱的全部意义所在。

我现在可以作为那个用户在那个监狱中运行命令了。

# jexec -U xistence mariadb sh
$

我被锁在监狱里了!具体来说,是在 Bert 的监狱牢房里。

然而,这个被监禁的进程可能会表现得有点奇怪。一个进程会保留它的环境。在这种情况下,虽然我作为用户 xistence 在运行,但我保留了在非监禁进程中所有的环境设置。这包括$SSH_AUTH_SOCK、我的 IRC 服务器设置等。我不希望这些东西出现在我的监狱环境中。如果我以 Bert 的身份登录,我希望就是Bert。

要在进入监狱之前清空你的环境,请使用 jexec 的-l标志。这会模拟一个干净的登录。

# jexec -lU xistence mariadb sh

在监狱中运行命令时,是否总是需要清空环境?不,并不是总是如此。这完全取决于你在做什么。

许多命令支持在主机上运行,但可以指定目标为监狱。请始终检查手册页以了解是否有此类选项。一个好的例子是 sysrc(8),它允许你通过-j指定监狱。在这里,我在监狱 mariadb 上启用 MariaDB。MariaDB 选择继续使用 MySQL 命名惯例,因此通过rc.conf选项mysql_enable启用它。

# sysrc -j mariadb mysql_enable=YES
mysql_enable:  -> YES

这个监狱现在已准备好运行 MariaDB。

当然,除了 MariaDB 未安装的那部分。接下来我们来处理这个问题。

安装监狱软件包

FreeBSD 的包管理工具让你可以从监狱内或从主机上管理软件包。如果主机管理员为你分配了一个监狱来配置,你可能希望从监狱中管理软件包。监狱软件包的工作方式与任何其他 FreeBSD 主机上的软件包完全相同,如第十五章所述。如果你负责整个系统,包括主机和主机上的所有监狱,你可能希望从主机而不是登录到每个监狱来管理每个监狱的软件包。我们来花些时间讲解这个。

pkg(8)命令的-j标志允许你指定一个监狱进行管理。你需要一个参数,即监狱的名称或 JID。-j标志必须在 pkg(8)子命令之前给出。在这里,我在专用的监狱上安装 MariaDB 服务器:

# pkg -j mariadb install mariadb101-server
Updating FreeBSD repository catalogue...
FreeBSD repository is up to date.
All repositories are up to date.
The following 9 package(s) will be affected (of 0 checked):
--snip--

请注意,pkg(8)不会提示你正在监狱中安装软件包。它假设如果你使用-j,你就知道自己在监狱中工作。

当你从主机管理监狱的软件包时,包管理工具不会在监狱中安装。监狱有自己的软件包数据库,存储在监狱内,但监狱没有直接使用该数据库的方式。

不要在主机和监狱内部之间切换包管理方式。选择一种方法并坚持使用。

更新监狱

所以你在主机上有无数个监狱,每个监狱都专门执行自己的任务并与其他监狱完全隔离。那很棒,直到你需要为所有监狱应用安全补丁。如果你是从源代码构建 FreeBSD 的,你需要在每个监狱中安装新的世界。但如果你运行的是发行版,freebsd-update(8)(参见第十八章)可以处理监狱。

你不能在监狱内使用 freebsd-update(8)。隔离监狱以防止危及主机系统的同样机制也会禁止 freebsd-update(8)所需的部分功能。相反,你需要从主机更新监狱。

每当你需要更新系统时,先更新主机,再更新监狱。主机必须运行与任何监狱版本相同或更新的 FreeBSD 版本。

首先,将/etc/freebsd-update.conf复制到另一个文件中,例如/etc/jail-freebsd-update.conf。删除所有在监狱中未安装的组件。监狱没有内核,大多数监狱也没有源代码,所以你可能最终会得到如下条目:

Components world

当你运行 freebsd-update(8) 时,它会检查你正在运行的 FreeBSD 版本。它通过查询内核来实现。如果你在 FreeBSD 13.0 系统上有 FreeBSD 12.0 的监狱,更新程序就会混淆,卡住并产生神秘错误。你需要 freebsd-update 使用监狱中安装的版本,而不是主机上运行的版本。使用 --currently-running 选项告诉 freebsd-update(8) 监狱当前运行的版本。你必须使用监狱的发布版本,包括补丁级别。虽然你可以轻松地从监狱中提取这些信息,但我鼓励你让 freebsd-update 询问监狱当前运行的版本。通过使用 jexec(8) 查询监狱中运行的 FreeBSD 版本来实现。

你还可以使用 -b 标志告诉 freebsd-update(8) 监狱所在的目录。

在这里,我更新名为 test 的监狱。监狱的文件位于 /jail/test。我使用反引号中的 jexec(8) 命令检查当前的 FreeBSD 版本。

# freebsd-update -f /etc/jail-freebsd-update.conf -b /jail/test/ --currently-running `jexec -l test freebsd-version` fetch install

一旦 freebsd-update 运行完成,重新启动你的监狱。它已经升级。

很多人编写了脚本来遍历 /etc/jail.conf 并升级所有监狱。如果你有多个监狱,最好找到或编写这样的脚本。

更多监狱选项

你可以以各种方式自定义监狱。jail(8) 手册页包括了当前的监狱选项列表,但这里是我常用的一些功能。

与其让 jail(8) 分配监狱 ID,你可以使用 jid 选项为每个监狱分配一个永久的 ID。

jid=101;

securelevel 选项允许你在监狱中提高安全级别(参见 第九章)。监狱的安全级别永远不能低于主机的安全级别。

你可以通过手动运行 /etc/rc 和监狱命令来查看监狱的启动信息。不过,这对于日常故障排除来说不太方便。使用 exec.consolelog 选项将监狱的控制台消息定向到一个文件。

exec.consolelog="$j/logs/$name.log";

除了挂载 /dev,监狱还可以通过 mount.fdescfsmount.procfs 选项拥有自己的 fdescfs(5) 和 procfs(5)。

监狱中的古老 FreeBSD

根据我的经验,企业网络 这个词意味着“我们有很多没人敢动的古老设备”。监狱可以帮助你应对这些系统。2014 年,我曾在一家公司工作,运行着一个在 FreeBSD 4.10 上的关键自定义 PHP 和 MySQL 应用程序。我不知道这台服务器是什么时候安装的,但 FreeBSD 4.11 于 2005 年 1 月发布,所以应该是在那之前。这个应用程序使用了过时的 Perl、PHP、OpenSSL 版本等。

更糟糕的是,这个应用程序运行在一台重新利用的桌面机器上,配有一块标准的高质量桌面硬盘。我把一台同年代的备用桌面机器藏在我的桌子下,这样我才有希望将 FreeBSD 4.10 安装到它上面。最合适的解决方案是重写或替换这个应用程序。几个系统管理员曾面对这个任务——并失败了。我决定将其虚拟化。FreeBSD 4.10 在 VMWare 上运行不太好——是的,你可以找到 de(4) 和 fxp(4) 驱动程序,但它们只适用于十多年前版本的这些卡。以下是我如何将这个古老的 FreeBSD 系统放入监狱的方法。

切换到单用户模式。卸载 /proc——是的,FreeBSD 4 仍然使用 /proc。那是个好日子。将整个文件系统打包,包括临时目录,如 /usr/obj/usr/ports/var/tmp 等。按照现代标准,它们占用的空间几乎为零,而且你也无法知道哪些文件以后可能会用到。你或许能在网上找到一个旧的 PHP 5.0.whatever 的 tar 包,但那将需要一些工作。

将 tar 文件复制到你的监狱主机,并在你的监狱目录中解压它。

# tar -C /jail/oldserver -xvpf oldserver.tgz

一定要使用 -p 标志来保留权限。

现在查看 /etc/rc.conf。监狱主机将处理所有网络功能,所以关闭任何设置 IP 地址或设置路由的语句。移除提供监狱主机所提供服务的守护进程,如时间、数据包过滤器和 SSH。你的监狱主机只需要直接支持应用程序的功能。在这个例子中,我只需要 Apache 和 MySQL。

考虑监狱的 /etc/fstab。你需要 NFS 文件系统或其他特殊挂载吗?移除所有不需要的内容。如果这个应用程序需要 /proc,则为它提供监狱选项 mount.procfs

移除旧的 /dev。你不能在现代的 FreeBSD 上使用 FreeBSD 4 的设备节点。

配置主机以保护监狱。虽然人们可以在 Apache 和 PHP 中编写完全正常的应用程序,但即便是最忠实的 Apache 和 MySQL 粉丝也不会鼓励你将这些 15 年前版本的服务器暴露到互联网上。使用主机的包过滤器来保护监狱。甚至不要考虑使用迁移主机的 OpenSSH 服务器。

你将无法在监狱中使用某些 FreeBSD 4 命令,因为接口已发生了很大的变化。FreeBSD 4 的 ps(1) 无法成功查询现代 FreeBSD 内核。然而,你可以从主机的 /rescue 目录中复制大多数这些程序的静态链接版本,并将它们复制到监狱中。

这样就简单吗?不,实际上并不简单。源系统越旧,遇到的问题就越多。我在这次迁移中遇到的大多数问题都意味着需要更改一个配置文件,以适应新的底层文件系统。你需要进行通常的系统管理员调试。但这是将现代网络接口应用到没有设备驱动的系统上的一种方法,也是唯一一种在 FreeBSD 4 系统上安装 ZFS 的方式。

最后的监狱笔记

人们已经发展出许多使用监狱的方法。要完全覆盖所有这些功能几乎需要一本书,但这里有一些提示。

你可以使用 ZFS 特性将数据集完全委派给监狱管理员,以便监狱所有者可以自己创建快照并创建子数据集。通过 VIMAGE 内核选项,你可以为监狱分配独立的路由表。如果你够大胆,nullfs(5) 允许你回收操作系统安装并最小化磁盘利用率。你还可以使用 RCTL 内核选项为每个监狱设定资源限制。

如果你有很多监狱,可能会更倾向于使用监狱管理程序,如 iocage 或 ezjail。两者都可以在 Ports Collection 中找到。

成功使用监狱要求自动化你的维护工作。每个监狱都需要单独的安全补丁,包括用户空间和已安装的软件包。你能自动化这个过程的程度越高,你就越有可能真正进行这种维护。我推荐使用 Ansible 的监狱模块,或者至少编写你自己的 shell 脚本来应用补丁。

jail(8) 命令可以让你无需命令行即可修改、创建和销毁监狱。如果你进行大量监狱工作,务必阅读手册页。

现在让我们看看一些 FreeBSD 不太为人知的方面。

第二十三章:FREEBSD 的边缘领域

image

如果你在 FreeBSD 社区待上一段时间,你会听到各种可以通过了解技术来实现的事情。人们构建嵌入式 FreeBSD 设备,并将它们运送到全球客户手中,而这些客户甚至不知道他们的空调或无线电中继站里有一台类 Unix 服务器。人们还在没有硬盘的机器上运行 FreeBSD,从单台服务器支持数百或数千个无盘工作站。你会发现可以启动的 CD 和 USB 设备,里面包含完整的 FreeBSD 系统,包括你可能需要的所有已安装软件。一旦你掌握了技巧,这些事情并不难做。

在本章中,我们将进入 FreeBSD 的边缘领域——由 FreeBSD 用户完成的真正酷的事情,这些事情并不一定是 FreeBSD 主流项目所支持的。虽然你可以通过常规渠道获得支持和帮助,但你必须做好比平时更多的准备,来调试和排查本章中的所有内容。

终端

终端是人们可以登录的设备。键盘、显示器和鼠标组成了一个终端,也叫做控制台。当你通过 SSH 登录到主机时,它提供的是虚拟终端。终端配置通常是自动的,但你可能需要进行一些调整。

文件/etc/ttys控制着用户如何以及在哪里登录到你的 FreeBSD 系统。控制台登录是否有效?虚拟终端呢?通过串行线路登录怎么样?FreeBSD 系统提供了四种标准终端:控制台、虚拟终端、拨号终端和伪终端。

控制台是单用户模式下唯一可用的设备。在大多数 FreeBSD 系统中,这通常是一个视频控制台,包括显示器和键盘,或者是从另一台系统访问的串行控制台。一旦系统进入多用户模式,控制台通常会连接到一个虚拟终端。控制台设备是/dev/console

虚拟终端连接到物理显示器和键盘。你可以在一个物理终端上拥有多个虚拟终端。通过 ALT 和功能键在它们之间切换。下次你坐到键盘前,按下 ALT-F2。你会看到一个新的登录屏幕,主机名后面是ttyv1。这就是第二个虚拟终端。按 ALT-F1 则回到主虚拟终端。默认情况下,FreeBSD 有八个虚拟终端,并为 X Windows 保留了第九个。即使在 X 环境下,你也可以使用这八个虚拟文本终端,并且一些 X 桌面提供多个 X 虚拟终端。这些虚拟终端是/dev/ttyv设备。

拨号终端通过串行线连接。你可以将调制解调器直接连接到你的串口,并让用户拨号连接到你的服务器。虽然现在这种方式不太常见,但相同的功能也支持通过串行控制台登录。拨号终端就是/dev/ttyu设备。

最后,伪终端完全由软件实现。当你通过 SSH 登录到服务器时,你不需要任何实际的硬件,但软件仍然需要为你的会话创建一个设备节点。伪终端是/dev/pts/中的设备节点。你不需要配置伪终端;它们会在你登录时自动协商。

配置对控制台、虚拟终端和拨号终端的访问权限,配置文件为/etc/ttys。你可以启用串行访问、要求或禁用密码等设置。

/etc/ttys 格式

/etc/ttys中的典型条目如下所示:

ttyv0   "/usr/libexec/getty Pc"         xterm  on  secure

第一个字段是终端的设备节点。在这种情况下,ttyv0是系统中的第一个虚拟终端。

第二个字段是用于处理此终端登录请求的程序。FreeBSD 使用 getty(8),但如果你有偏好的终端管理程序,可以使用它代替。你可以在软件包中找到几个这样的程序。这个字段接受一个参数,即终端配置。文件/etc/gettytab包含了所有的终端配置。

第三个条目是终端类型。文件/etc/termcap描述了 FreeBSD 支持的所有终端类型。对于真正的小型系统,FreeBSD 提供了/etc/termcap.small,其中仅包含最重要的条目。几乎所有现代终端都可以与xtermvt100兼容。

第四个条目决定了终端是否可以接受登录。可以设置为on以接受登录,或off以禁止登录。onifconsole设置允许在内核将端口配置为控制台时在串行端口上进行登录。

最后,我们有一些选项。这个示例设置了secure选项,这告诉 getty(8),root 可以登录该控制台。

提供终端是一个低级的系统任务,直接由 init(8)处理。对/etc/ttys的更改不会生效,直到你告诉 init(8)重新读取其配置文件。Init 始终是 PID 1。

# kill -1 1

不安全的控制台

当你以单用户模式启动 FreeBSD 时,你会得到一个 root 命令提示符。这对于你的笔记本电脑来说没问题,并且在公司数据中心的服务器上也能很好地工作,但对于不可信的设施中的机器呢?例如,如果你有一台在合租数据中心的服务器,你可能不希望任何人都能够获得 root 级别的访问权限。你可以告诉 FreeBSD 物理控制台不安全,并要求进入单用户模式时需要 root 密码。然后系统会从开机启动到多用户模式而不需要密码,但在你明确选择以单用户模式启动时,会要求输入密码。

在单用户模式下要求密码并不能完全保护你的数据,但它确实大大提高了安全门槛。一个孤单的技术人员在深夜、没人注意时,可以将你的系统启动到单用户模式,并在大约 15 分钟内为自己添加一个账户。而拆卸你的机器,移除硬盘,将它们安装到另一台机器上,进行修改并将服务器重新上线,需要更多时间,侵入性更强,且更有可能被托管管理人员注意到。

/etc/ttys中找到控制台条目:

console none        unknown off secure

你会发现,控制台终端不像其他终端那样功能丰富;它不运行 getty(8),并使用通用的unknown终端类型。然而,控制台仅限在单用户模式下使用,并且当连接到物理终端时,这样是可以的。

要使控制台在启动到单用户模式时要求 root 登录,请将secure更改为insecure

console none        unknown off insecure

为控制台设置密码保护可以阻止随便的恶作剧行为,但它甚至无法减缓那些拥有物理访问权限的熟练入侵者。

管理云端 FreeBSD

数百台或上千台服务器的集群变得越来越常见。像 Ansible 和 Puppet 这样的自动化系统,某种程度上让我们能够保持这些系统的基本秩序。然而,Unix 并不是为了这样操作而设计的。最初的 UNIX 是为了由一个高度熟练的操作员来管理,他能轻松应对各种不同命令输出格式以及更多配置文件样式的怪癖。

FreeBSD 正在通过 libXo 和通用配置语言(UCL)解决云规模管理的问题。

LibXo

虽然自动化监控是必需的,可以提醒你出现问题,但当涉及到深入的故障排除时,没有什么能替代登录主机、运行命令并解释结果。我已经忘记了写了多少脚本来解析一些晦涩的 ps(1)标志组合的输出,这样我就能将数字传递给监控软件。我也已经忘记了我花了多少小时调试这些脚本,或者解释为什么我写的处理一个 netstat(1)标志的脚本与我们现在感兴趣的标志无关。^(1) 如果将这个问题扩展到数百台或上千台服务器,那么从软件中快速获取信息就变成了一个严重的问题。

FreeBSD 通过 libXo 减少了这个问题的复杂性。

LibXo 是一个库,帮助命令提供不仅是文本形式的输出,还能生成 XML、JSON,甚至 HTML。与其使用 grep(1)和 awk(1),或者你用过的任何令人不安的 Shell、Perl 或 Python 组合来查找所需的信息,不如让一个解析器从标记格式中提取数据。你还可以将命令输出直接导出到网页上。

不是所有程序都支持 libXo,但越来越多的程序正在添加支持。手册页会声明一个程序是否支持 libXo,如果你懒得阅读,可以尝试使用--libxo标志运行命令。所有支持 libXo 的命令都使用该命令行选项。你还必须指定输出格式,文本、XML、JSON 或 HTML。这里,我运行arp -an并将 JSON 设置为输出格式。

$ arp -an --libxo json
{"__version": "1", "arp": {"arp-cache": {"hostname":"?","ip-address":"
203.0.113.221","mac-address":"08:00:27:31:91:0d","interface":"em0",
"permanent":true,"type":"ethernet"} 
--snip--

如何使用这个?我们中的许多人可能不会使用。但如果你正在管理数十台或数百台服务器,你可能拥有足够的专业知识来轻松解析这个。数百种工具可以选择标记数据,而你的应用开发人员可能已经在主机上安装了他们首选的软件。虽然arp(8)的输出结果相对一致,但 libXo 也能处理 netstat(1)、vmstat(8)等命令的任何任意标志组合。学会一次性抓取标记数据,你就可以永远告别那些可怕的脚本编写。

通用配置语言

Unix 系统有一种相当标准的配置文件格式。井号(#)是注释。存在变量。也许变量在配置文件中的存在足以激活某个功能,或者你需要将变量设置为某个值。尽管如此,它们都有些不同。有些程序可以从主文件和目录中的文件中提取配置片段,就像 cron(8)从/etc/crontab/etc/cron.d/中提取一样。其他程序则不行。有些使用大括号来划分配置块,而有些则使用……无论是 30 年前程序员认为好的主意。结果是,没人看syslog.conf会觉得它看起来像pkg.conf,尽管它们共享相似的基础概念。

通用配置语言(UCL)旨在改变这一现状。如果这些程序的语法类似,为什么不为每个程序使用一个统一的解析库呢?如果你已经有了一个解析库,为什么不让它解析多种格式呢?UCL 允许你提供经典 Unix 风格、JSON 或 YAML 格式的配置文件,非常适合自动化管理。它可以从 shell 代码、UCL、JSON 或 YAML 中提取配置设置。

在我写这篇文章时,FreeBSD 为 pkg(8)使用了 UCL。对其他工具的支持,例如 bhyve(8),正在慢慢推进。如果你正在管理大量服务器,请查看你的发行版中 UCL 的支持状态。

无盘 FreeBSD

尽管 FreeBSD 并不难管理,但数十台或数百台几乎相同的系统可能会变得相当沉重。减少维护负担的一种方法是使用无盘系统。无盘系统并不是禁止使用硬盘,而是它们从网络上的 NFS 服务器加载内核和操作系统。

为什么要为你的服务器集群使用无盘系统?多个系统可以从单一的 NFS 服务器启动,集中管理所有的补丁和软件包。这对于终端机集合、计算集群以及其他有大量相同系统的环境非常适用。发布操作系统更新变得非常简单,只需在 NFS 服务器上替换文件。同样,当你发现更新有问题时,恢复它也只是简单地恢复 NFS 服务器上的文件。在这两种情况下,客户端所需做的唯一事情就是重启。由于客户端对服务器只有只读访问权限,未经授权的用户无法对操作系统做出任何更改。如果你只运行少量系统,无盘系统可能会显得过于复杂,但如果系统数量超过几个,无盘系统无疑是一个明确的赢家。

在你运行无盘系统之前,你必须拥有一个 NFS 服务器、一个 DHCP 服务器、一个 TFTP 服务器以及支持无盘启动的硬件。让我们逐一查看并了解如何进行设置。

测试,测试,测试!

你的第一次无盘设置将很像你第一次设置防火墙:容易出错、麻烦且令人恼火。我强烈建议你测试准备工作的每一步,这样你可以更容易地发现并解决问题。每个必需服务的测试说明都会提供。

无盘客户端

运行无盘的机器必须具备足够的智能,通过网络找到它们的引导加载程序和操作系统。实现这一目标的有两种标准方式:BOOTP 和 PXE。BOOTP(互联网引导协议)是一个较老的标准,早已被淘汰。PXE(英特尔预启动执行环境)已在几乎所有的新机器上得到支持多年,因此我们将专注于此。

启动你的无盘客户端机器并进入 BIOS 设置。在 BIOS 中,你会找到一个选项来设置启动设备顺序。如果机器支持 PXE,其中一个选项将是网络。启用该选项并让机器首先尝试网络启动。

你的无盘客户端已经准备好了。现在让我们准备服务器。

DHCP 服务器设置

虽然大多数人将 DHCP 看作是为客户端分配 IP 地址的方式,但它能提供的不止这些。你可以配置 DHCP 服务器来提供 TFTP 服务器、NFS 服务器和其他网络资源的位置。无盘系统广泛使用 DHCP,你会发现我们使用了一些你从未尝试过的 DHCP 选项。

OpenBSD 的 DHCP 服务器不支持 FreeBSD 无盘客户端;你必须使用 ISC 的 DHCP 服务器或其他功能更全的版本。一旦你获得了无盘工作站的 MAC 地址,配置 ISC DHCP 服务器来处理无盘系统就非常直接了。

MAC 地址

要为 DHCP 客户端分配配置信息,您需要该客户端网卡的 MAC 地址。某些 BIOS 实现提供集成网卡的 MAC 地址,某些服务器级硬件则有打印着 MAC 地址的标签。然而,这些选项过于简单,所以我们将尝试更复杂的方法。

当一台机器尝试从网络启动时,它会发出 DHCP 请求以获取其配置信息。虽然您还没有配置无盘配置,但任何 DHCP 服务器都会记录客户端的 MAC 地址。您可以从租约文件/var/db/dhcpd.leases获取客户端信息。

  --snip--
➊  lease 198.51.100.10 {
     starts 6 2017/09/16 06:57:23;
     ends 6 2017/09/16 07:07:23;
  --snip--
➋  hardware ethernet 08:00:27:d8:c1:1c;
     uid "\001\010\000'\330\301\034";
     set vendor-class-identifier = "PXEClient:Arch:00000:UNDI:002001";
  }
  --snip--

该客户端的 MAC 地址为 08:00:27:d8:c1:1c➋,并已被分配 IP 地址 198.51.100.10➊。根据这些信息,我们可以创建一个 DHCP 配置来为此主机分配静态 IP 地址,并提供其启动信息。

DHCP 配置:特定无盘主机

我们在[第二十章中配置了基本的 DHCP 服务。下面是一个适用于无盘客户端的 dhcpd(8)配置示例。这些配置不应该放在子网声明中,而是独立作为顶层声明,即使它与非无盘 DHCP 客户端共享同一子网。

➊ group diskless {
        ➋ next-server 198.51.100.1;
        ➌ filename "pxeboot";
        ➍ option root-path "198.51.100.1:/diskless/1/";
        ➎ host compute1.mwl.io {
                ➏ hardware ethernet 08:00:27:d8:c1:1c  ; 
                ➐ fixed-address 198.51.100.101 ;
          }
    }

我们定义了一个名为diskless的组➊。这个定义将允许我们为该组分配特定的参数,然后只需将主机添加到该组中。组中的每个主机都会得到相同的参数。

next-server设置➋告知 DHCP 客户端 TFTP 服务器的 IP 地址,filename选项➌告诉客户端从该 TFTP 服务器请求的引导加载程序文件的名称。记得在第四章中提到,引导加载程序是找到并加载内核的软件。最后,option root-path ➍告诉引导加载程序在哪里找到该机器的根目录。所有这些选项和设置都会提供给无盘组中的所有客户端。

然后,我们使用host语句和此系统的主机名➎将无盘客户端分配到无盘组中。我们的第一个客户端被称为compute1。此客户端通过其 MAC 地址➏进行标识,并被分配了一个静态 IP➐。它还接收此组的标准配置。

为网络上的每个无盘主机创建类似的主机条目。

重启 dhcpd(8)使此配置生效。现在重新启动您的无盘客户端。DHCP 日志应该会显示您已为该客户端分配了静态地址。然而,DHCP 客户端在没有引导加载程序的情况下无法继续启动,这意味着您需要一个 TFTP 服务器。

DHCP 配置:无盘集群

也许你有大量相同的无盘主机,比如终端机房中的瘦客户端。完全可以理解你不希望为每个瘦客户端都做一个静态的 DHCP 条目。让这些主机从 DHCP 服务器获取它们的引导信息,但不指定主机地址。它们将从 DHCP 池中获取一个地址。许多集群解决方案包括客户端服务,它们会将新主机注册到它们正在使用的“集群管理器”中,因此硬编码的地址就不那么重要了。

你还可以特别识别那些请求 DHCP 信息的 PXE 主机,并将这些主机分配给一个特定的地址组。使用 PXE 引导的主机会将自己标识为 PXEclient 类型的客户端,向 DHCP 服务器报告。你可以编写特定的规则来匹配这种类型的客户端,并进行适当配置。有关如何匹配 vendor-class-identifierdhcp-client-identifier,请查阅 DHCP 手册。

tftpd 和引导加载程序

我们在第二十章中介绍了如何配置 TFTP 服务器。TFTP 服务器必须为你的无盘客户端提供 pxeboot 文件。FreeBSD 将 pxeboot 提供在 /boot 目录下。

# cp /boot/pxeboot /tftpboot
# chmod +r /tftpboot/pxeboot

尝试通过 TFTP 从你的工作站下载pxeboot。如果成功,重启你的无盘客户端并观察它尝试启动。控制台应该会显示类似以下的信息:

Building the boot loader arguments
Relocating the loader and the BTX
Starting the BTX loader

你之前可能见过这个消息,当一个常规的 FreeBSD 从硬盘启动时。你的无盘客户端将识别 PXE 版本,打印内存,并声明它正在运行引导加载程序。此时,它将无限循环,尝试加载内核。它无法加载内核,因为我们还没有设置 NFS 服务器。

无盘安全性

无盘系统通过 NFS 运行,并且具有所有 NFS 的安全问题。即使你部署 Kerberos 来加密 NFS 流量,初始网络引导和根文件系统的挂载始终是未加密的。不要在开放的互联网中运行无盘节点。

你可以通过为 NFS 根账户分配一个不同的用户来在某种程度上保护你的 NFS 服务器。运行 find /diskless/1 -user 0 -exec chown nfsroot {} \; 会将所有 root 拥有的文件的所有者更改为 nfsroot 用户。然后你可以编辑 exports 文件,将 root 映射到 nfsroot 用户。然而,要运行 freebsd-update(8) 时,你需要还原它,并在应用补丁后恢复。但是在你刚开始学习时,不要太复杂。先让基本的用户空间工作起来。

NFS 服务器和无盘客户端用户空间

许多关于无盘操作的教程建议使用服务器的用户空间和根分区作为无盘客户端。这可能很容易做到,但这根本不安全。你的无盘服务器上可能有你不希望客户端访问的程序,而且它肯定包含了你不希望泄露给一大堆工作站的敏感安全信息。提供一个独立的用户空间是一个更明智的选择。

虽然你可以通过多种方式提供一个独立的用户空间,但我发现最简单的方式是稍微修改第二十二章中的 jail(8)构建过程。首先,为我们的无盘客户端创建一个数据集、UFS 文件系统或目录作为它们的根目录,然后在该目录中安装用户空间和内核。将base.txzkernel.txz分发文件提取到该目录中。

# tar -xpf base.txz -C /diskless/1/
# tar -xpf kernel.txz -C /diskless/1/

如果你构建了一个 FreeBSD 并希望运行它,这也是可以的。在这里,我们将一个本地构建的用户空间安装到/diskless/1

# cd /usr/src
# make installworld DESTDIR=/diskless/1
# make installkernel DESTDIR=/diskless/1
# make distribution DESTDIR=/diskless/1

现在将这个目录告诉你的 NFS 服务器。我打算在这个网络上安装几台无盘系统,所以我通过 NFS 将这个目录共享给整个子网。客户端不需要对 NFS 根目录有写权限,因此我将其设置为只读。下面的/etc/exports行实现了这一点:

/diskless/1 -ro -maproot=0 -alldirs -network 198.51.100.0 -mask 255.255.255.0

重新启动 mountd(8)以使这个共享可用,并尝试从工作站进行挂载。确认该目录包含一个从客户端可见的基本用户空间,并且客户端无法写入文件系统。

你的无盘主机需要设置一个根密码。使用 chroot(8)和 passwd(1)来设置它。

# chroot /diskless/1/ passwd

你需要告诉主机它的根文件系统是只读的。创建/diskless/1/etc/rc.conf并将root_rw_mount设置为 NO。在该目录下,还需要为你的客户端创建一个resolv.conf文件。

现在重启你的无盘客户端,看看会发生什么。它应该能够找到内核并启动到一个未配置的多用户模式。根据服务器、客户端和网络速度的不同,这可能需要一些时间来完成。

在这一点上,你可以配置你的用户空间,以特定匹配你的单个无盘客户端。你可以在/etc中进行修改,例如创建反映你需求的/etc/fstab文件,并将密码文件复制到相应位置。这对一个无盘客户端来说足够了,但 FreeBSD 有专门的基础设施来支持从同一个文件系统启动几十或几百台主机。我们来看看这是如何做到的。

无盘工作站农场配置

无盘系统的一个好处是多个机器可以共享同一个文件系统。然而,即使是大多数相同的机器,你也会发现必须让某些配置文件略有不同。FreeBSD 提供了一种机制,通过在 tmpfs(5)临时文件系统上重新挂载目录,并将自定义文件复制到这些分区上,从而在统一的用户空间上提供个性化的配置文件。

FreeBSD 的默认无盘设置允许你跨多个网络和子网配置无盘工作站——这是大型网络中的一个宝贵特性。然而,如果你只有少数几台无盘系统,刚开始你可能会觉得它有点繁琐。但是随着时间的推移,你会发现它会越来越有用。无盘系统是解决许多问题的便捷方案。

启动中的 FreeBSD 系统使用 vfs.nfs.diskless_valid 来判断它是否在无盘运行。如果 sysctl 等于 0,则说明它从硬盘运行;否则,它是在无盘模式下运行。在无盘系统上,FreeBSD 会运行 /etc/rc.initdiskless 脚本来解析并部署层次化的无盘配置。

配置层次结构

在无盘主机的 /conf 目录中配置你的无盘农场。/conf 目录中可能包含很多目录。两个关键的目录是 /conf/base/conf/default,但你可能也会有单独的子网和/或个别 IP 地址的目录。无盘系统使用这些目录中的内容在挂载的根分区上构建 tmpfs 文件系统,以便每台主机可以拥有独特的设置和读写文件系统。你可以将任何目录都作为 tmpfs 文件系统,并从这个层次结构中填充它,但每台主机都需要一个读写的 /etc 目录,所以我们将以此为例。

/conf/base 目录包含需要在无盘客户端上以读写方式挂载的基础系统文件。创建 /conf/base/etc 并将一组 /etc 文件填充其中,无盘主机就可以将它们作为其 tmpfs /etc 的基础。(如我们稍后所见,它也可以回收无盘根目录的 /etc)。

/conf/default 目录包含你环境的默认设置。也许你环境中的每一台主机都需要一个指向挂载共享数据存储的 /etc/fstab 文件。你可以创建 /conf/defaults/etc/fstab,然后无盘系统会将其复制到每台主机,覆盖基础系统中的 /conf/base/etc。我还会在默认目录中分发你环境的通用 rc.conf

你还可以创建每个子网的目录。将该目录命名为子网的广播地址,即网络中的最高地址。我的无盘农场运行在子网 198.51.100.0/24 上,广播地址是 198.51.100.255。如果我创建了 /conf/198.51.100.255/etc/rc.conf,该子网中的每台主机都会获得这个 rc.conf 文件。如果我为该子网中的无盘主机有一个特殊的 /etc/fstab,我可以将其放入 /conf/198.51.100.255/etc/fstab,它将覆盖默认设置。我还会在 /etc/rc.conf.d/ 中添加为仅在该子网上运行的特殊服务的文件。

最后,我可以为每个主机创建独立的目录。如果我创建了 /conf/198.51.100.101/etc/rc.conf.d/apache,那么主机 198.51.100.101—仅这台主机—会获得这个文件。如果这台主机需要一个真正独特的 /etc/fstab,我可以将其放入 /conf/198.51.100.101/etc/fstab,它会覆盖默认的以及子网的 /etc/fstab。^(2)

这种层次化配置通过一个叫做 无盘重挂载 的过程进行部署。

无盘重挂载 /etc

无磁盘系统检查文件/conf/base/etc/diskless_remount,以获取它应该作为内存文件系统挂载的目录列表。如果没有这个文件,就不会创建内存文件系统,并且你的无磁盘主机与所有其他无磁盘主机共享一个只读的用户空间。diskless_remount文件包含需要重新挂载的文件系统列表。

/etc

这告诉 FreeBSD 构建一个 MFS /etc并将无磁盘根目录现有的/etc复制到其中,为我们提供一个基础来工作。

你不一定希望无磁盘根目录中的所有文件都出现在无磁盘主机的/etc目录中。这是一个内存文件系统,所以为什么要浪费内存存放你不需要的东西?你也不想让初级系统管理员误以为主机支持它们并不具备的功能。无磁盘系统不应在本地保存日志,因此它们不需要newsyslog/etc/newsyslog.conf。你也不需要备份无磁盘客户端,因此/etc/dumpdates也是不必要的。浏览/etc将揭示出许多与无磁盘主机无关的文件。然而,如果你删除得太多,系统将无法启动,而且所需文件的列表并不直观。例如,如果你删除了/etc/mtree,机器将在单用户模式下挂起,因为它无法重新填充 MFS /var分区。

将你不需要的文件和目录的完整路径放入文件/conf/base/etc.remove中。例如,以下条目会删除/etc/gss/etc/bluetooth目录,以及之前提到的 syslog 和备份文件。你不需要复制/etc/resolv.conf。FreeBSD 的/etc/rc.d/resolv启动脚本会从启动主机时的原始 DHCP 响应中创建一个。

/etc/gss
/etc/bluetooth
/etc/dumpdates
/etc/resolv.conf
/etc/newsyslog.conf
/etc/syslog.conf

这不难,是吗?

现在让我们将一些内容添加回我们的配置中。

完成设置

既然你已经有了一个安装好的系统,让我们做一些微调。无磁盘客户端需要第三方软件包和各种配置文件。完成客户端设置最简单和最安全的方法是使用 chroot(8)程序,它将你锁定在文件系统的一个子目录中。通过在 NFS 服务器上使用 chroot(8),你几乎可以获得对文件系统的读写访问,就像它在无磁盘客户端上存在一样。

# chroot /diskless/1

是的,/etc仍然具有层次化的覆盖,但系统的其他部分存在的方式完全符合无磁盘客户端所看到的样子。你在 chroot 后所做的任何更改都将对客户端保持一致。

安装软件包

使用 pkg(8)在无磁盘客户端上安装软件。使用-c标志来指定无磁盘根目录,并让 pkg(8)切换到该目录。

# pkg -c /diskless/1/ install pkg

现在你已经在无磁盘客户端上有了软件包工具、数据库和仓库信息。

# pkg -c /diskless/1/ install sudo

以这种方式安装你需要的软件。

SSH 密钥

也许关于无盘客户端最令人烦恼的事情就是主机的 SSH 密钥。在正常操作中,每台主机都需要唯一的 SSH 密钥。如果你运行在一个私有网络中,你可能会决定让所有无盘客户端共享相同的 SSH 密钥。你也可能决定让每台主机在启动时自动生成新的 SSH 密钥。由于/etc位于 tmpfs 上,这些密钥将在关机时消失,但用户会很快习惯“主机密钥已更改”的消息。然而,你并不希望用户习惯这个。

然而,为每个无盘客户端建立持久的、唯一的主机密钥并不困难。为每台主机创建一个/conf目录。

# mkdir -p /diskless/1/conf/198.51.100.101/etc/ssh
# cd /diskless/1/conf/198.51.100.101/etc/ssh

在此目录中,为你的 SSH 版本使用的每种算法创建 SSH 密钥。虽然 ssh-keygen(1) 包含 -A 标志以自动生成缺失的密钥,但它会将这些密钥放在/etc/ssh中。这对你的无盘用户环境甚至在 chroot 中都不起作用。你需要通过传统的方式来创建这些密钥。

# ssh-keygen -N "" -qt algorithm -f ssh_host_algorithm_key

你需要将加密算法的名称替换为小写两次。例如,这里是如何创建一个 DSA SSH 密钥:

# ssh-keygen -N "" -qt dsa -f ssh_host_dsa_key

今天,OpenSSH 为 RSA、ECDSA 和 ED25519 创建密钥。为每种算法创建密钥。密钥创建非常容易脚本化。查看/etc/rc.d/sshd中的示例。

无盘客户端让你轻松地运行成千上万几乎相同的机器。现在我们来看看如何保护其中的一台。

存储加密

FreeBSD 支持两种不同的磁盘加密方法,GBDE 和 GELI。两种工具工作方式完全不同,支持不同的加密算法,并针对不同的威胁模型设计。人们常常谈论加密磁盘,但很少有人讨论磁盘加密到底是为了保护磁盘免受什么样的威胁。

GBDE(基于几何的磁盘加密)在高安全性环境中具有特定的功能,在这些环境中,保护用户与隐藏数据同样重要。除了用户提供的加密密钥外,GBDE 还使用存储在硬盘特定扇区中的密钥。如果任一密钥不可用,则无法解密分区。为什么这很重要?如果一个安全数据中心(例如在大使馆)遭到攻击,操作员可能会有一两分钟的时间来摧毁硬盘上的密钥并使数据无法恢复。如果坏人把枪指着我的头,告诉我“输入密码,否则怎样”,我希望磁盘系统能说:“密码正确,但密钥已被销毁”。我不想要一个泛泛的错误提示:“无法解密磁盘”。在第一种情况下,我仍然作为一个支离破碎的人质有一定价值;在后一种情况下,我要么死了,要么攻击者会变得非常有创意。^(3)

GELI 更加灵活,但它不像 GBDE 那样能保护我免受身体伤害。如果有人可能因为我笔记本上的机密文件而偷走我的电脑,或者如果一个不受信任的系统用户可能窥探我的交换空间来窃取秘密,GELI 足够用了。GELI 并不试图保护我的人身安全,只保护我的数据。由于我不会从事任何可能增加暴露于枪支的工作(考虑到我住在底特律),这对我来说完全没问题。GELI 还使用 FreeBSD 的加密设备驱动程序,这意味着如果你的服务器有硬件加速器,GELI 会透明地利用它。

我应该提到,人们因为加密配置错误或丢失密钥而损失的数据,远多于因为笔记本被盗。当我听到有人说“我已经加密了整个硬盘!”时,我几乎能预见未来那个同一个人会说:“我丢失了对硬盘上所有内容的访问!”通常我没错。仔细考虑一下你是否真的需要磁盘加密。如果你确实需要它,也要备份你的文件。那些政府特工不会破解你笔记本上的加密,他们会等你自己解密——然后他们才会入侵。

如果你想加密你的笔记本,使用 FreeBSD 安装程序来完成这项工作。你仍然应该阅读这一节,以便理解磁盘加密是如何工作的,但如果安装程序愿意为你做这项工作,就让它去做。我们将通过使用 GELI 加密/dev/da0上的磁盘分区,并将加密密钥存储在挂载于/media的 USB 存储设备上。你可能会觉得使用文件系统中的文件(见第十三章)作为加密分区更为合理。实际上很少有人需要加密整个硬盘,在某些情况下,加密整个硬盘可能会引起怀疑。我已经够麻烦地向机场安检解释我的电脑“看起来如此奇怪”了。在他们看来,一个显示插入加密密钥并输入加密密码的启动提示,离此人是 一个危险的疯子,可能需要进行非常彻底的身体检查`只有一步之遥。如果你真的需要加密某些文件,通常它们的总大小只有几兆字节。那正是使用文件系统中的文件或闪存驱动器的完美应用场景。

请注意,你必须在使用 GELI 之前加载geom_eli.ko内核模块。

生成和使用加密密钥

GELI 允许你使用密钥文件和/或密码短语作为加密设备的加密密钥。我们将同时使用这两者。要生成你的加密密钥文件,使用 dd(1)/dev/random 获取适量的数据并写入到文件中。记住,/media 是我们的 USB 设备挂载点。如果你真的想保护你的数据,最好直接在 USB 设备上创建密钥,而不是把它保存在你的文件系统中,以防假设的入侵者恢复它。(即使删除文件,依然会留下痕迹,熟练的攻击者有可能提取这些残留数据。)

# dd if=/dev/random of=/media/da0p1.key bs=64 count=1
1+0 records in
1+0 records out
64 bytes transferred in 0.000149 secs (429497 bytes/sec)

这 64 字节的数据构成了一个 512 位的密钥。如果你愿意,可以增加密钥的大小,但这会在访问加密文件系统时增加额外的处理器负载。别忘了,你的密码短语也会增加密钥的复杂性。

要为密钥分配密码短语,使用 geli init-s 标志告诉 geli(8) 加密文件系统所需的扇区大小;通常,4,096 字节或 4KB 是一个合适的扇区大小。-K 表示密钥文件。你还必须指定要加密的设备。

# geli init -s 4096 -K /media/da0p1.key /dev/da0p1
Enter new passphrase:
Reenter new passphrase:

密码短语与密码类似,只不过它可以包含空格并且长度没有限制。如果你真的想保护你的数据,我建议使用一个由多个单词组成、包含非字母数字字符的密码短语,并且不要使用你母语中的常见短语。

现在你已经有了一个密钥,将它附加到要加密的设备上。

# geli attach -k /media/da0p1.key /dev/da0p1
Enter passphrase:

GELI 现在知道 /dev/da0p1 是一个加密磁盘,并且文件 /media/da0p1.key 包含密钥文件。一旦你输入密码短语,你就可以通过新的设备节点 /dev/da0p1.eli 访问加密磁盘的解密内容。当然,你还需要一个文件系统来存储数据。

加密设备上的文件系统

在你为加密设备构建文件系统之前,先清除磁盘上任何残留的数据。像 newfs(8) 和 zpool(8) 这样的程序并不会真正覆盖新分区中的大部分数据位;它们仅仅添加了超块,标明 inode 的位置。如果你之前使用过这块磁盘,入侵者将能够看到磁盘上的旧文件残片。更糟的是,他还会看到 GELI 放置在其中的加密数据块。在为磁盘创建文件系统之前,最好覆盖磁盘一层伪装的随机数据,这样可以让入侵者更难识别哪些块包含数据,哪些不包含数据。再次使用 dd(1)

# dd if=/dev/random of=/dev/da0p1.eli bs=1m

FreeBSD 拥有无限的混乱——或者用技术术语说,/dev/random 是非阻塞的。用高质量的随机数覆盖整个磁盘所需的时间取决于你的存储系统。可能需要一天时间。

现在你的磁盘充满了垃圾数据,在它上面创建文件系统并将其附加到系统上。我通常会在这种加密设备上使用 UFS 文件系统。

# newfs /dev/da0p1.eli
# mount /dev/da0p1.eli /mnt

你的加密磁盘设备现在可以在 /mnt 中访问。将你的机密文件存储在那里。

加密磁盘有更多的可能性。要么阅读 geli(8),要么查看我的书籍《FreeBSD 精通:存储基础》(Tilted Windmill Press, 2014)。

这将引导你了解 FreeBSD 中一些较为晦涩的部分。现在让我们看看当事情真的出错时该怎么办。。。

第二十四章:问题报告和紧急情况

image

FreeBSD 是由人类生产的。人类会犯错。其中一些错误可能微不足道,而另一些则可能导致整个系统崩溃。FreeBSD 明确声明不提供任何保证,但社区对问题非常认真。然而,开发者无法在没有适当的 bug 报告的情况下修复这些问题,而你正是解决问题的关键所在。学习如何提交一个可用的 bug 报告将帮助你与不仅仅是 FreeBSD 项目,还包括所有其他生产软件实体进行互动。

Bug 报告需要展示证明 bug 的程序输出,但如果问题导致整个系统崩溃呢?系统停止的 panic 或许是最棘手的问题,但通过适当的准备,你可以像处理任何其他不那么入侵性的 bug 一样处理 panic。FreeBSD 的开发人员确实希望从你的 panic 报告中获得调试输出,并且你可以在问题报告中轻松地提供这些信息。

但首先是有用的 bug 报告。

Bug 报告

Bug 报告 是对导致系统表现出意外行为的问题的详细描述。这种描述可能有些模糊,是的。什么算是意外行为?什么算是问题?有效的问题报告范围很广,从“我期望这个东西有一个 man 手册页”到“当我挂载 SMB 文件系统时,操作系统崩溃”。像缺少 man 手册页引用这样的微不足道的问题可能看起来不值得你花时间报告,但每个 man 手册页中的每个引用之所以存在,是因为有人认为它值得包含进去。那么,什么是 bug 报告,什么又不是呢?

Bug 报告不仅仅是说你有问题。Bug 报告是证明 FreeBSD 存在问题的地方。是的,我说的是 证明。并非 FreeBSD 无罪推定,但要提交 bug 报告,你必须证实你的声明。未附带证据的 bug 报告将会被关闭,并附有简短的回复,比如“不是 bug”或“无用的报告”。寻求帮助的正确途径是首先使用搜索引擎,然后是邮件列表或 FreeBSD 论坛。

任何形式的“我不知道我在做什么”都不应包含在 bug 报告中。这包括“FreeBSD 不按我预期的方式工作”或“我做了一些愚蠢的事情时发生了一些不好的事情”。如果你以一名新手的身份在被评为高难度的悬崖上进行自由攀岩,摔倒并摔断了脖子,医院必须接待你。如果你在运行 newfs(8) 时半途抽掉新硬盘并抱怨文件系统损坏,FreeBSD 的人员会驳回你的 bug 报告。

Bug 可能涉及系统不一致性。每个网络接口、API 和系统调用都有 man 手册页。如果你尝试调用系统调用的 man 手册页却找不到匹配项,那就是一个 bug。如果你在阅读源代码时发现文档与代码不匹配的情况,那也是一个 bug。如果你能够使程序(或整个系统)可复现地崩溃,那就是一个 bug。

你也可以提交 Bug 来向 FreeBSD 项目提交改进。这里的关键词是 改进,而不是 愿望。一个改进需要附带实际的代码,说明你如何测试这段代码以及任何相关信息,如规范和标准。如果你提交足够多这样的 Bug,你可能会被邀请成为提交者。

Bug 是协作的。通过报告 Bug,你是在表示愿意与 FreeBSD 开发者一起解决你的问题。这可能意味着应用补丁、尝试不同的方法,或者运行调试命令并将输出发送给开发者。报告 Bug 并期待得到类似“已修复!去做这个”的回答是不现实的。在初始的 Bug 报告中包含所有信息可以更快地解决问题。尽量提供过多的数据。

跟我重复一遍:“自由软件。捐赠的支持时间。” 服务器的 RAID 卡让你的硬盘随着康歌的节奏旋转,对你来说可能至关重要,但报告 Bug 的人们正在放弃他们的私人时间来帮助你。记住这一点。

在报告 Bug 时,最好运行的是 FreeBSD 的最新版本。如果你在 FreeBSD 12.0-RELEASE 上报告 Bug,而当前版本是 12.4-p15,别人会要求你更新并重新尝试。没人会查看已经过生命周期的 FreeBSD 版本的 Bug 报告。

在报告 Bug 之前

理想情况下,你永远不需要报告 Bug。一个正确的严重 Bug 报告对你来说是很大的工作量,对 FreeBSD 开发者来说也是。FreeBSD 项目有一个内部邮件列表,专门用来评估 Bug 数据库并引导报告到最有可能的开发者那里。虽然向 FreeBSD 邮件列表发送邮件会让成千上万的人知道你的问题,但报告 Bug 会让成千上万的高技能人士知道你的问题,并要求他们为你处理虚拟文书工作。在报告 Bug 之前,确保你和 FreeBSD 项目都需要它。

问题是出现在所有主机上,还是仅在一台主机上?只在一台主机上出现的问题可能是硬件故障导致的。持续的、可重复的行为更可能是一个 Bug。

把你的问题当作一个普遍的问题来处理,先搜索一下 FreeBSD 的常见资源。查看 FAQ 和手册。检查 FreeBSD Bug 数据库,看看是否有现存的 Bug。bugs.FreeBSD.org/ 上查看。搜索邮件列表归档、论坛以及更广泛的互联网,看看是否有人已经遇到过这个问题。你可以在论坛或 FreeBSD-questions 邮件列表上询问是否有人遇到过这种行为。这是预期的吗,还是应该报告一个 Bug?人们提问的内容将对故障排除和 Bug 报告的创建非常有帮助。

在开始报告 Bug 之前,收集所有可能有帮助的信息。包括:

  • 详细启动输出

  • 系统版本

  • 自定义内核配置(如果有的话)

  • 程序调试输出

  • 你期望发生什么?

  • 实际发生了什么?

你能重现这个问题吗?开发人员在调查 bug 时需要一个可重现的测试用例。如果你的服务器在凌晨三点开始播放歌舞剧曲目,那确实是个问题。如果它只发生一次,且你无法重现,最好的做法是保持沉默,这样别人就不会觉得你疯了。不过,如果它每次在运行特定命令组合时发生,且发生在某些硬件上,那么这个问题就可以得到验证和调查,解决问题后,或者有人给你的服务器提供录音合约。

FreeBSD 使用 Bugzilla 跟踪 bug,网址是 bugs.FreeBSD.org/。在提交 bug 之前,先搜索现有的 bug 数据库,看看是否有类似或相关的 bug。你的问题是否与现有的某个 bug 相似?如果你的服务器播放迪士尼歌曲,而另一个 bug 显示某人的硬件播放百老汇音乐剧的歌曲,你可能应该在报告中提到这个 bug。那些 bug 是否有一些有启发性的评论?这些评论可能会告诉你如何应对或绕过这个问题,而不必再提交一个相同的 bug。如果你想要更新,可以把自己加入到 bug 的 cc 字段。每次 bug 更新时,你都会收到一封电子邮件。

在你搜索 Bugzilla 时,先在那里创建一个账户。即使你现在不需要提交这个 bug,总有一天你会需要…而你可能会因此感到相当烦恼。提前拥有 Bugzilla 账户会让你提交 bug 时稍微轻松一点。

如果你已经走到这一步,仍然遇到问题,那么你可能真的需要提交一个 bug 报告。让我们看看不应该在报告中写什么。

糟糕的 Bug 报告

理解一个好的 bug 报告最简单的方法就是阅读一些糟糕的报告,找出它们的不足之处。浏览已关闭的 bug 会揭示出许多糟糕的报告,下面是一个典型的例子:

当我启动 FreeBSD 12.1 ISO 镜像时,无法通过“欢迎使用 FreeBSD”选项屏幕。启动菜单卡住了,每次刷新屏幕时都停留在 10。无论我按什么键,系统都无法启动。如果我按下许多按键,最终会得到内核崩溃。相同的 ISO 镜像在 VirtualBox 中可以启动,并且我能够将其安装到磁盘上。

Bug 报告中包括了一块已损坏的标准 SuperMicro 主板、键盘和鼠标的型号。没有任何硬件是特殊的。报告者建议通过使用类似的硬件来重现这个问题。

首先,报告者显然在安装 FreeBSD 时遇到了问题。甚至可能是 FreeBSD 本身有问题。我毫不怀疑这个系统在启动时会如宣传所说地失败。但没有证据,也没有诊断信息。重现过程也没有什么用;如果每个 12.1 版本的安装镜像在常见硬件上都表现如此,发布工程师早就不会通过这次发布了。

提供硬件的品牌和型号可能没有你想象的那么有用。厂商偶尔会更换芯片组,但不更改型号。详细的启动信息以硬件可以识别的方式标识了机器的硬件,而型号永远无法做到这一点。不过,这个报告者无法提供 12.1 版本的详细启动信息。

如果我遇到这种情况,我首先会尝试使用第二张 CD。也许第一张烧录的磁盘有问题。如果问题仍然存在,我会下载一个稍微旧一点的 FreeBSD 版本,看看问题是否依然存在。如果 12.0 版本失败了,11.0 版本怎么样?我会在我的 bug 报告中加入早期版本的详细启动信息。如果旧版本也失败了,我会在提交 bug 之前向 FreeBSD-questions 邮件列表请求进一步的建议。

正如你可能猜到的那样,没有人会跟进这样的问题。

很多开发者喜欢修复 bug。他们喜欢深入代码,找出微妙的问题。可是他们并不喜欢整理人们那种不稳定的 bug 报告;他们希望自己能够得到报酬来处理那些难缠的人。你的目标是提交一个如此完整且令人信服的 bug 报告,以至于一个正在寻找 bug 的开发者会觉得你是一个容易合作的人——然后,你真的需要一个容易合作的人。

FreeBSD 的 FAQ 中有一个 Dag-Erling Smørgrav 的笑话:“需要多少个-current 用户来换一个灯泡?”答案是 1,169 个,其中包括“有三个用户提交了(bug),其中一个错误归档在文档中,内容仅是‘这里很黑’。”如果你的 bug 报告只是“这里很黑”,那就属于一个糟糕的报告。^(1)

推测与证据

每当你向任何人或组织提交 bug 报告时,务必将你的证据与你认为发生的事情分开。证据是可以采取行动的;而你的推测则不是。包括推测并不会有什么坏处,但它需要与证据清晰区分开来。你有多少次接到支持电话,用户声称他遇到了某个问题,但一旦你深入调查,发现他告诉你的一切都是假的,实际上是完全无关的事情在发生?是的,别做那种用户。将推测与证据分开。

修复方案

任何 bug 报告中最重要的部分就是修复方法。你是如何解决这个问题的?也许你只有一个临时解决方案。“程序在我这样做时崩溃了,但我可以运行它并将输出通过某某处理,效果也还不错。”这个评论对下一个遇到你 bug 的人很有帮助。

当你遇到 bug 时,先看看源代码。修正 man 页或者网站上的错别字并不难。如果你是程序员,浏览源代码几分钟可能就能发现问题。如果没有发现,那么弄清楚系统为什么以这种方式运行,将会让你成为一个更好的程序员和调试者。

也许你无法修复这个 bug。告诉大家这个 bug 的存在仍然很有帮助。但是,通过提供修复方案,你的 bug 报告就不仅仅是一个报告,而是对社区的贡献。

提交 Bug 报告

所有的 bug 都会在bugs.FreeBSD.org/提交和处理。FreeBSD 有三类 bug:端口、基础系统和文档。遇到附加软件的 bug,请使用端口类别;遇到随基本 FreeBSD 安装一起安装的 bug,请使用基础系统类别;遇到关于 man 页面、FAQ、手册和网站的问题,请使用文档类别。每个类别会带出稍微不同的网页表单。文档和端口 bug 所需的字段大部分是基础系统 bug 的子集,因此我们将通过提交一个基础系统 bug 来演示。

网页表单包含几个下拉字段,可以帮助你将 bug 引导到正确的人那里。你的 bug 可能在你提交后被重新分配,但这没关系;最初,你是希望找到一个理解你说的是什么的开发者。你不希望一个文档提交者处理系统调用问题,也不希望一个源代码提交者去搞清楚一个端口包装问题。

Component字段让你选择该 bug 影响的系统部分。组件列表会随时间变化,但选择一个组件后会显示其描述。虽然总有一个通用字段,比如Bin用于基础系统 bug,但做出正确选择会加速 bug 的处理。

Version字段中,选择这个 bug 所涉及的 FreeBSD 版本。

Severity字段有点误导,并且需要你从个人情感中抽离。选项有“仅影响我”、“影响部分人”和“影响多人”。一个糟糕到让你考虑咬掉自己脚趾来逃脱的 bug 可能只影响你。对你来说非常重要,但对 FreeBSD 项目不重要。要抵制宣称“影响所有人”的冲动。同样,网站上的一个拼写错误可能对所有人都可见,但如果到现在为止没人注意到,它可能不值得标为“影响多人”。保留更重要的严重级别给那些对特定设备驱动程序的所有用户或使用某个文件系统的所有人产生负面影响的 bug。如果你因为提交琐碎报告而被认为是关键问题,你会很快发现自己被忽视。FreeBSD 项目依靠的是荣誉系统,声誉比你想的更重要。

Hardware字段中,选择你发现该 bug 的平台。即使看似无关,它也可能是关键的。

OS字段是 Bugzilla 的遗留字段,可以忽略。

在这些下拉框下方,Bugzilla 提供了文本框。这些需要更多的思考。

Summary字段需要简要描述问题。一个好的总结应该提供独特的信息,使你的 bug 在其他 bug 中脱颖而出。“卸载 SMB 文件系统时出现恐慌”是一个不错的总结。“无法安装”、“系统崩溃”和“问题”则很糟糕。开发者在浏览 bug 数据库时,首先会看到你的总结。一个糟糕的总结会让他跳过这个 bug。

描述 区域是你描述问题的地方。不要发泄或抱怨一切有多糟糕。说明发生了什么以及你期望发生什么。如果调试输出足够简短,可以直接附上;否则,将调试输出作为附件上传。包括如何重现问题的建议。如果你有修复方案,请提供。将你关于该问题的任何发现添加进去。有时,最不寻常的细节提供了至关重要的线索。

在描述下方添加 附件。这里可以上传你的自定义内核配置、详细启动消息、内核崩溃消息以及冗长的讨论。

使用 预览 按钮来确认你包含了所有内容。确认无误后,点击 提交

提交后

很快你会收到一封电子邮件,通知你现在是某个 bug 编号的骄傲拥有者。你对该邮件的任何回应都会附加到该 bug 上,只要你不更改主题。

包含正确信息的 bug 报告有很高的几率会迅速关闭。复杂或难以捉摸的 bug 可能需要更长时间,但只要你提供了足够的细节,就会看到更新。

如果你的 bug 报告似乎被遗忘了,可以向相应的邮件列表发送一封邮件,提供 bug 编号、简要说明问题及为什么该问题很重要。FreeBSD 是一个志愿者项目,可能处理你 bug 报告的人发生了什么事情。尽管许多 FreeBSD 开发者是专业程序员,但对他们中的许多人来说,这仍然是一个爱好,必须让位于生病的孩子或紧迫的工作截止日期。如果其他方法都行不通,可以雇佣开发者按合同方式来解决你的特定问题。

如果你提交了一个特别棘手的 bug,FreeBSD 开发者可能会要求你提供更多信息。尽可能快速且全面地提供这些信息。如果你不理解他们的要求,可以花些时间研究后再询问。大多数开发者都很乐意为一个有意愿且基本称职的合作伙伴提供指点,尤其是当你能帮助他们改进代码时。

我已经数不清提交过多少个 FreeBSD 错误报告了。我倾向于提交些微不足道或严重的错误,比如文档错误和内核崩溃,但中间的错误很少。大多数问题已经被解决或提交并关闭。那些奇怪的问题大多是文档上的小错误,位于/usr/src/contrib目录下,这个区域是 FreeBSD 项目明确不负责修复小问题的地方。如果像我这样的人能成功关闭超过 90%的错误报告,任何人都可以。值得注意的是,如果你提交了足够多正确的补丁,你会发现与你合作的提交者开始在你背后谈论你。最终,他们会厌倦为你的高质量工作做秘书,并会给你提交权限。如果你拒绝,他们会更坚持地提出。如果你担心的话,不用担心;成为一个提交者并不那么痛苦。关于 FreeBSD 项目的入会仪式有传闻,说是有一群丹麦人拿着斧头在自行车棚后面等着你,但这些完全不是真的。大多数情况下。

不管怎样,还是要继续提交良好的错误报告;这是 FreeBSD 改进的唯一途径!

最难处理的错误类型是完全崩溃的系统。我们来谈谈如何从中获取信息。

系统崩溃

崩溃是指操作系统完全停止工作。所有系统,从网络堆栈到磁盘驱动器,都停止工作。当内核面临无法解决的冲突时,系统会选择崩溃,或完全停止工作。如果系统达到了它不知道如何处理的条件,或者它未能通过内部一致性检查,它就会举手表示:“我不知道该怎么办!”崩溃是内核的恶意顺从版本。^(2) FreeBSD 的生产版本越来越难以崩溃,但它仍然可能发生。让系统崩溃的最简单方法是做些愚蠢的事,比如在使用中拔掉一个不可热插拔的硬盘。运行-current 时,崩溃并不罕见;虽然它们不频繁,但也不是非常稀有的情况。

FreeBSD 非常复杂,它的贵族血统和开源开发过程都无法保护它免受所有错误的影响。幸运的是,这种遗产和开发过程确实为你提供了调试问题所需的信息工具。你可能会从一个晦涩的错误代码开始,但你很快会发现,这串乱码字符对某些人来说是有意义的。

发生 panic 的内核可以将关键的信息复制到 crash dump(崩溃转储)中。崩溃转储包含了足够的 panic 信息,开发人员希望通过它能够识别并修复底层问题。配置每个系统以捕获崩溃转储,然后再将它们投入生产。FreeBSD 可以通过安装时的设置捕获崩溃转储,但如果你重新配置了服务器或有独特的磁盘分区方式,你需要确认崩溃转储仍然有效。这个预防措施对大多数服务器来说可能没什么用,但当某些事情爆炸时,它将带来巨大的回报。

识别 Panic

当系统发生 panic 时,它会停止运行所有程序、停止写入磁盘并停止监听网络。在除 -current 外的任何版本的 FreeBSD 中,发生 panic 后系统会自动重启。并非所有无解释的重启都是 panic——坏硬件,比如故障的电源或劣质内存,可能会导致重启,而没有任何日志或控制台信息。然而,如果你运行的是 -current,panic 会在控制台中显示类似这样的信息:

panic: Assertion cp->co_locker == curthread failed at /usr/src/sys/modules/smbfs/../..
/netsmb/smb_conn.c:363
cpuid = 5
KDB: stack backtrace:
db_trace_self_wrapper() at db_trace_self_wrapper+0x2b/frame 0xfffffe085d0db630
vpanic() at vpanic+0x182/frame 0xfffffe085d0db6b0
kassert_panic() at kassert_panic+0x126/frame 0xfffffe085d0db720
smb_co_unlock() at smb_co_unlock+0x9c/frame 0xfffffe085d0db740
smb_co_put() at smb_co_put+0x68/frame 0xfffffe085d0db770
nsmb_dev_ioctl() at nsmb_dev_ioctl+0x484/frame 0xfffffe085d0db800
devfs_ioctl_f() at devfs_ioctl_f+0x15d/frame 0xfffffe085d0db860
kern_ioctl() at kern_ioctl+0x230/frame 0xfffffe085d0db8c0
sys_ioctl() at sys_ioctl+0x17e/frame 0xfffffe085d0db9a0
amd64_syscall() at amd64_syscall+0x2de/frame 0xfffffe085d0dbab0
Xfast_syscall() at Xfast_syscall+0xfb/frame 0xfffffe085d0dbab0
--- syscall (54, FreeBSD ELF64, sys_ioctl), rip = 0x800b8016a, rsp = 0x7fffffffe4f8, rbp = 0x7fffffffe530 ---
KDB: enter: panic
db>

对我来说,这条信息中唯一似乎有些道理的部分是第一行。我卸载了一个 SMB 文件系统,结果得到了这个 panic 信息。第一行提到了 smbfsnetsmbsmb_conn,这些看起来都很像 SMB 相关的内容。

底部的 db> 是调试器的命令提示符。按下 ENTER 几次,你会看到调试器作出回应;你可以输入命令。调试器指令不是 Unix 命令,但它们有助于你从系统中提取更多信息。

响应 Panic

如果遇到 panic,首先要做的就是获取 panic 信息的副本。由于 FreeBSD 已经不再运行,标准的数据复制方法将无法工作——你不能通过 SSH 连接,也不能使用 script(1)。控制台可能完全卡住,不再响应,而不是处于调试器中。不管怎样,你必须获得那个错误信息。

在 1990 年代的糟糕年代里,FreeBSD 在遇到 panic 后并不会自动重启;最初,它只是停在那里显示 panic 信息。我第一次看到 panic 时,慌忙寻找纸和笔。最终,我找到了一只旧信封和一只断了的铅笔,只有在以恰当角度握住时,它才会留下痕迹,然后我趴在服务器架和粗糙的砖墙之间。用一只手平衡住六英寸的黑白显示器,另一只手把信封贴在墙上。显然,在极度紧张的情况下,我长出了第三只手,因为我设法在信封上记录了 panic 信息。最后,身心疲惫,我从机架里滑出来,得意地把所有信息输入到邮件中。相信 FreeBSD 项目的 Panic 应急响应团队一定能够查看这些垃圾信息,告诉我到底发生了什么。

我很快意识到,FreeBSD 并没有一个精英的 PERT 团队随时准备接受我的问题报告。相反,我收到了一封孤单的电子邮件:“你能发送一个回溯信息吗?”当我询问如何操作时,系统将我指引到了一份手册页。(拖自己一路回到 第一章。)幸运的是,panic 很容易重现——要重现这个问题,唯一需要做的事情就是让一个客户登录系统。我花了一天的时间,艰难地掌握了串行控制台和核心转储。

我的信封上的 panic 信息的问题在于,它只提供了故事的一小部分。事实上,它模糊不清,像是把一辆被盗的车描述为“红色,车翼上有一道刮痕”。如果你不提供车的品牌、型号、VIN 号和车牌号,你不可能指望警察能够取得什么进展。同样,如果没有更多来自崩溃内核的信息,FreeBSD 开发人员也无法抓住那段有问题的代码。

好消息是,panic 处理在这些年里有了很大改进。FreeBSD 可以自动记录崩溃转储并捕捉有关 panic 的所有信息。安装程序中甚至有一个开关可以启用它。不过,我强烈建议在将机器投入生产之前先测试 panic 捕获功能。这样,如果发生 panic,你就准备好了,并且能够轻松地提交完整的问题报告。

准备工作

配置崩溃转储需要通过 dumpdev /etc/rc.conf 变量告诉 FreeBSD 将转储保存到哪个交换设备上。如果将 dumpdev 设置为 AUTO,内核会自动将转储保存到第一个交换设备。如果需要,你可以指定一个不同的交换设备,但整个转储必须能放入一个交换设备中。如果常规的交换空间不足以容纳转储,请添加一个磁盘以获得足够的交换空间,并将 dumpdev 设置为该分区。

崩溃转储的实际操作

当一个配置为捕捉 panic 的系统崩溃时,它会保存内核内存的副本。这个副本被称为 dump(转储)。系统无法将 dump 直接保存到文件中。崩溃的内核根本不知道文件系统的事情,其一,文件系统可能已经损坏,或者写入操作可能会破坏它。然而,崩溃的内核理解分区,因此它可以将 dump 写入到一个分区。大多数 FreeBSD 主机都有现成的临时空间——交换分区。FreeBSD 默认将 dump 存储在系统的第一个交换分区中,并将 dump 放置在分区的尽可能后面。完成 dump 后,计算机会重新启动。

在发生 panic 后,主机的文件系统几乎可以肯定会处于脏状态。它们可能是 ZFS 文件系统或使用软更新日志,但系统仍然必须从日志中恢复或回滚到最后一个成功的 ZFS 事务组。使用 fsck(8)清理文件系统可能会占用大量内存,因此 FreeBSD 必须在运行 fsck(8)之前启用交换空间。希望你有足够的内存,使得 fsck(8)不需要交换空间,如果需要交换空间,也希望你有足够的交换空间,以避免覆盖位于交换分区末尾的转储文件。最坏的情况下,你可以进入单用户模式,启用交换到没有转储的分区,清理一个文件系统来保存转储文件,然后手动运行 savecore(8)。

一旦 FreeBSD 有了一个可以保存核心转储的有效文件系统,它会检查交换分区是否有转储文件。如果找到核心转储,FreeBSD 会运行 savecore(8)将转储从交换分区复制到适当的文件系统文件中,运行 crashinfo(8)从转储中收集信息,清除交换分区中的转储数据,然后继续重启。现在,你将拥有一个可用于调试的内核核心文件。

savecore(8)会自动将内核转储放置在/var/crash目录中。每个崩溃都有一个名为vmcore的文件,后跟一个点和数字。第一次崩溃是vmcore.0,第二次是vmcore.1,以此类推。FreeBSD 默认保留最近的 10 个崩溃转储。文件vmcore.last始终指向最近的崩溃转储。

如果你的/var分区不够大以容纳转储,可以在rc.conf中通过dumpdir变量选择其他目录:

dumpdir="/usr/crash"

虽然 savecore(8)还支持一些其他选项,如压缩,但在现代系统中通常不需要使用这些选项。

FreeBSD 默认在每个恢复的崩溃转储上运行 crashinfo(8)。crashinfo(8)程序运行一系列调试器脚本,从 panic 中收集信息,并将其存储在一个方便的文本文件core.txt.0中。这些信息包括 panic 回溯、进程列表以及大量的虚拟内存统计信息。

串口控制台与 panic

虽然串口控制台在 panic 调试中不是严格必要的,但在处理卡住的机器时,它是非常有价值的。虽然提供远程访问的 Java 小程序总比没有好,但能够使用 script(1)捕获所有内容使得串口控制台非常值得使用。如果你真的想为 panic 做好准备,确保你的所有机器都有串口控制台,或者至少有双重控制台。如果可能,记录串口控制台的输出;这样,即使系统未配置崩溃转储,你也能获取到 panic 消息。如果你的笔记本电脑没有串口,可以拍下 panic 消息的照片并附加到故障报告中。

崩溃转储测试

你已经设置了savecore rc.conf选项,所以一切应该正常工作。每当你听到“应该正常工作”这个词时,立即问:“我怎么验证它是否正常工作?”通过将 sysctl debug.kdb.panic 设置为大于 0 的任意整数强制 FreeBSD 崩溃。虽然这是重启机器最丑陋的方式,但它确实让主机经历崩溃和核心保存过程。关闭任何可能在中断时损坏数据的活跃进程,例如数据库,并故意触发崩溃。

# sysctl debug.kdb.panic=1

你将看到控制台上闪现出崩溃信息,随后是主机将核心转储到交换空间的进度。如果你观察重启信息,应该会看到快速提到保存核心文件。当你再次登录到机器时,查看/var/crash目录。你会找到三个文件:info.0vmcore.0core.txt.0

info.0文本文件描述了转储恢复过程。它包含主机名、架构、崩溃信息等。然而,最重要的细节是最后一行。

  Dump Status: good

这个转储是可用的。你可以继续调试了。

文件vmcore.0包含二进制形式的内存转储。它的大小应该在几百兆字节到几 GB 之间,具体取决于当主机崩溃时它正在做什么。

文件core.txt.0包含来自 vmcore.0 的崩溃信息。当你提交问题报告时,请包含你的崩溃的core.txt文件。

恭喜——你有了一个核心转储!当主机发生真正的崩溃时,你可以从转储中获取信息。然而,有时崩溃可能会变得更加复杂。

崩溃转储类型

FreeBSD 支持三种不同类型的崩溃转储:迷你转储、完整转储和文本转储。所有转储都在发生崩溃时写入交换空间,并在启动时复制到文件。

迷你转储,当前默认的转储格式,包含了内核使用的内存。虽然内核本身并不大,但你还会得到 UFS 缓冲区缓存。所需的交换空间应该只占系统内存的一小部分,但也有可能几乎与系统内存一样大,具体取决于系统当时正在执行的任务。转储不包括内核未使用的内存以及 ZFS ARC。

完整转储包含系统拥有的所有内存。如果它在 RAM 中,它就会被转储。整个内核内存?没错。你的网页服务器的缓冲区?密码?都在里面。完整转储占用的交换空间与主机的内存大小相同。通过设置 sysctl debug.minidump 为 0 来启用完整转储。仅在 FreeBSD 开发人员要求你这样做,以帮助调试特别难以解决的崩溃时才启用完整转储。

文本转储是一种高级转储类型,仅包含由 ddb(8)调试器和相关脚本捕获的信息。它仅在内核中启用了 DDB 选项的主机上可用——即,并非任何版本的通用内核。但它包含在-current 通用内核中,因此勇敢的-you souls 使用-current 的用户可以利用文本转储。

文本转储

文本转储利用 ddb(8) 调试器在发生 panic 的内核上运行脚本。/etc/ddb.conf 中的默认脚本从内核中提取最常用的信息,并将这些信息转储到磁盘中。尽管 crashinfo(8) 是在捕获的内存镜像上运行的,但 ddb(8) 是在发生 panic 的内核上运行的。一位有经验的开发者可以利用这一点。你可能不是一个经验丰富的开发者,但如果你已经读到这本书的这一部分,你应该能够按照指示操作并编辑 /etc/ddb.conf,这就足够了。

使用 ddb_enable rc.conf 选项启用文本转储。

# sysrc ddb_enable=YES

在系统启动时,内核调试器 ddb(8) 会从 /etc/ddb.conf 读取调试脚本并将其加载到内核中。调试器会在发生 panic 时运行这些脚本。脚本会将内核切换到文本转储模式,调用多个命令收集有用信息,将这些数据写入交换空间,并重启主机。文本转储不像小转储那样有用,但它们占用的空间要小得多。

完整的文本转储将以 tar 文件的形式出现在 /var/crash 目录下,文件名为 textdump.tar.0。每次 panic 后,文本转储的编号会增加,文件 textdump.tar.last 始终指向最近的文本转储。textdump(4) 手册页描述了 tar 包内每个文件的内容,但作为用户,你需要知道的是,你应该将整个文件附加到你的 bug 报告中。

转储与安全

vmcore 文件包含 panic 时你内核内存中的所有内容,其中可能包括敏感的安全信息。有人可能会利用这些信息入侵你的系统。一位 FreeBSD 开发者可能会因为很多合法的原因请求获取 vmcore 文件和有问题的内核;这会使调试变得更加容易,并且可以节省大量的邮件来回交流。尽管如此,仍然需要谨慎考虑某人获得这些信息可能带来的后果。如果你不认识请求者,或者不信任他,不要发送该文件,并且不要对此感到内疚。花时间研究任何请求你 vmcore 的开发者。即使他们看起来值得信赖和有声望,如果你决定通过邮件来回沟通,而不是直接提供 vmcore,也是完全可以接受的。任何有资格处理你崩溃问题的开发者都明白你为何犹豫是否发送 vmcore,而任何试图让你感到羞愧并迫使你发送它的人,可能根本不应该获得它。

将你的 vmcore 链接发布到公共互联网,相当于将你服务器的核心暴露给全世界。不要这样做。

然而,如果 panic 是可重现的,你可以通过冷启动系统进入单用户模式并立即触发 panic。如果系统没有启动任何包含机密信息的程序,并且没有人向系统输入任何密码,那么转储中就不会包含这些信息。在单用户模式下重现 panic 会生成一个不包含安全信息的、已清理的转储。启动到单用户模式后,运行:

# mount -ar
# /etc/rc.d/dumpon start
# command_that_panics_the_system

第一个命令将文件系统挂载为只读,这样你就不必在恐慌之后再次运行 fsck(8)了。第二个命令告诉 FreeBSD 将转储保存在哪里。最后,运行触发恐慌的命令。触发恐慌可能需要多个命令,但在大多数情况下,这应该能为你提供一个干净的转储。

如果你的恐慌要求你将机密信息加载到内存中,那么这些信息将出现在转储中。如果你能提交一个有用的错误报告,你就是 FreeBSD 的精英。恭喜!

第二十五章:后记

image

如果你已经读到这里,你现在知道如何管理和使用 FreeBSD 作为几乎任何服务器任务的平台。你可能需要学习新的协议和如何配置新的程序,但底层操作系统基本上已经是一个解决好的问题。恭喜你!FreeBSD 是一个非常棒、灵活的平台,几乎可以承担你网络中任何角色。为了总结一下,我想讨论一下 FreeBSD 的其他一些方面。

在本书中,我们已经讨论了 FreeBSD 的特性:程序、内核、特性等。我们没有涉及的一个方面是创造这些内容的社区。

FreeBSD 社区

FreeBSD 社区包括计算机科学家、经验丰富的程序员、用户、系统管理员、文档编写者,以及几乎任何对该系统感兴趣的人。他们来自世界各地,教育水平从高中到博士后不等。我个人曾与来自每个大洲和地球上大多数大岛屿的 FreeBSD 用户打过交道。^(1) 国籍根本不重要——种族、肤色、性别、性取向或信仰也同样无关紧要。

有些人是计算机科学家。有些人工作于云服务提供商或制造公司。有些人是医生,还有一些人在那些在政府注意到之前就消失的小店里做职员。我曾经与一位才华横溢的开发者紧密合作,结果发现他年纪太小,连合法驾驶都不行。奇怪的是,时区很重要,但只是因为它们影响了开发者之间的沟通能力。由于大多数社区的互动都是在线进行的,唯一代表你的是你的言辞和工作。这些人改善了 FreeBSD 并推动它向前发展,使其不仅仅是一个由一和零组成的集合,也不仅仅是一个用来提供网站的工具。

FreeBSD 社区的一个有趣之处在于,它已经发展出了应对领导层变化的方法。许多开源项目有一个单一的领导者或一个小的静态领导团队。当这些人决定离开时,项目可能就此结束。其他人可能会分支或叉开该项目,但原始社区通常会分裂。创造 FreeBSD 的人们大多已经转向了其他的事情,但社区培养了其他的领导者。经过五代领导层更替,FreeBSD 作为一个项目展现出了对领导层变化的韧性,这在开源世界中几乎是独一无二的。今天的 FreeBSD 领导者非常积极地关注他们的接班人,指导和辅导那些看起来最有可能成为 2020 年代乃至 2030 年代领导者的年轻社区成员。

这种领导层的变动能力让 FreeBSD 作为一个社区保持活力,并帮助它适应这个世界。1994 年,没人会认为一个开源项目需要行为准则或讨论和管理架构变化的流程。如今,FreeBSD 既有行为准则,也有这些流程。从前,由核心团队处理所有核心决策,现在核心团队已将许多职责委托给更具体的团队。所有组织在成长和成熟过程中都会发生变化。变革摧毁了许多组织,但 FreeBSD 已经证明,它能够在重新定义自己的同时生存和繁荣。

更重要的是,他们总是欢迎那些曾经参与过的人,无论是在 Subversion 仓库中,还是在酒吧里。UFS 文件系统的原创作者仍然活跃在其中。FreeBSD 的一位创始人最近重新加入了项目。有人从创立之初就一直是提交者。而 somehow,他们让我从 90 年代中期就一直参与——可能是因为我太大,别人很难轻易把我赶走,但我接受这个事实。

我们为什么要这么做?

每个人之所以参与 FreeBSD 都有自己的原因。有一小部分人是由依赖 FreeBSD 的公司支付费用来改进代码,例如 Dell EMC 和 Netflix。FreeBSD 基金会聘请开发者按合同完成特定任务。大多数开发者作为爱好参与 FreeBSD,要么是为了做出比日常工作中更精确的编程,要么是为了做一些他们感兴趣的工作。你们中有多少人在工作中,因为外部因素导致没能达到预期的成果?又有多少人从事的工作虽然能支付账单,却并没有让你感到充实?开发 FreeBSD 使人们能够同时满足这两种需求。

许多贡献者并非软件开发人员,而是参与 FreeBSD 的其他部分工作。有些人写文档,有些人设计网站,还有些人则潜伏在论坛的阴暗角落,回答用户问题。那些无法做其他事的人则测试发布候选版本和快照,寻找只在他们的环境中出现的 bug。许多人花费数小时甚至更长时间从事与 FreeBSD 相关的工作。为什么?我可以向你保证,这本书的版税远不足以弥补我本可以与家人共度的时间。我现在是一名全职作家,但如果我要追求金钱,我会写关于 Windows、Linux 和最新的荒唐管理手段的书。相反,我在写一本关于 FreeBSD 的书。

更糟的是,我正在周六下午在我的办公室写这篇后记,而我的家人正楼下欢聚一堂,试图说服那些占领了烧烤架的加拿大红松鼠家庭搬回树上。楼下传来叫喊声和尖叫声,偶尔还有“哦,天啊,别这样!”的喊声——看起来大家都玩得很开心。你完全有理由问:“你到底怎么了?你为什么要这么做?”

我们做这件事是为了创造一些对人类有用的东西的满足感,并回馈我们所得到的一部分。

你完全可以只拿 FreeBSD 提供的东西,随意使用。我曾经也这样做过一段时间。在成为一个相对称职的系统管理员之后的几年,我发现自己想要回馈社区。这就是社区如何发展的方式,社区的发展意味着 FreeBSD 会繁荣。

如果你也想获得这种满足感,这里也有属于你的位置。

你能做什么?

如果你出于某种原因有兴趣支持 FreeBSD,这里有你的空间。自从我 1996 年开始接触 FreeBSD 以来,时不时会有人发帖说,“我想帮助,但我不会编程。”(我敢肯定,我也曾在 1998 年左右把这封邮件发到了questions@邮件列表。)这些帖子通常的标准回复就是沉默。如果你已经决定自己无法提供帮助,那你是对的——你做不到。但一旦你决定可以帮助,你就能做到。

没有人否认一些高知名度的程序员是 FreeBSD 的明星。许多这样的程序员拥有令人印象深刻的技能,而我们大多数人甚至无法梦想成为下一个罗伯特·沃森约翰·鲍德温。然而,即便你连从湿纸袋中脱身的编程能力都没有,你仍然可以提供帮助。

你只是问错了问题。

不要问 FreeBSD 需要什么。除非你有一大笔资金,迫切需要一个慈善项目来归属,否则你无法提供那个需求。(如果你确实有多余的钱,急需有人照顾,它们,FreeBSD 基金会非常愿意收养这些资金,给予它们关爱,让它们感受到温暖。)如果你不能自己做出某个“FreeBSD 应该做某某事情”之类的提议,那就不要说,“如果 FreeBSD 做某事会很酷吧?”很多人都能做到这一点。

相反,问问自己有什么技能。任何大型组织都需要不同的人员,而你今天所拥有的任何技能对 FreeBSD 都是有用的。你会写文档吗?深入官方文档,或者将论坛上的流行教程移植到官方文档库中。做得多了,文档团队会把你拉进 FreeBSD,并在你额头上印上提交者的标签。

你是一个网页设计师吗?独立网页设计师提供了有价值的第三方资源,比如freshports.org/daemonforums.org/。这个领域有着广阔的空间,你可以在其中发挥作用。

有没有你需要的第三方软件尚未移植到 FreeBSD?把它修补到能工作,然后将其变成一个官方的端口。FreeBSD 一直在寻找更多被维护的软件。一旦你做了几个这样的工作,你就可以接手那些没有维护者的端口的维护工作。保持下去,Ports 团队会来找你,让你成为一个提交者。^(2)

我写作大量且尚可。我为 FAQ 写了一些更新,然后写了第一版书籍。FAQ 更新让我成为了一名提交者,尽管在多年前我将注意力转向写更多书籍时,我已经让这个角色过期。即便是我写的代码,都会让小孩子绝望地哭泣,让温柔的老太太做出驱邪的手势,但 FreeBSD 的人们欢迎我作为自己的一员,并把我视作合作伙伴,仅仅因为我做了这项工作。

你做的是什么?即使你不常有机会去做,你最喜欢做的事情是什么?发挥你的那项技能吧。它会被人们感激的。

如果没有别的...

如果你确实没有什么有用的技能,且没有其他想法,那就重读这本书。阅读 FreeBSD 网站上的文档。订阅 FreeBSD-questions@FreeBSD.org,或者加入论坛,帮助其他用户。许多人就是通过这种方式开始为 FreeBSD 做贡献的。

我鼓励你在可能的情况下,将人们引导到现有的信息资源。当有人问到 FAQ 中已回答的问题时,引导他们去那里。如果问题曾经被问过,建议他们搜索邮件列表的归档。教会人们自助是你时间的最有效利用——不仅仅在 FreeBSD 中,在整个世界也是如此。正如那句老话所说,教会一个人钓鱼,你就能卖给他鱼钩。

做足够多的这些,你会想更新 FreeBSD FAQ,只是为了不再回答那个问题一遍又一遍。提交足够多的 FAQ 更新后,文档团队会再次向你提供提交权限。

最棒的是,在回答问题一段时间后,你会对 FreeBSD 的需求有更深的理解。那些需求中几乎肯定有与你的技能相匹配的部分。

完成任务

这是 FreeBSD 成功的最大秘诀:它包含的一切,都是因为某人看到了一个自己能够填补的需求并采取了行动。NetBSD 和 FreeBSD 的起源是在一群 386BSD 补丁用户厌倦了等待下一个官方发布时开始的。我在开始写这本书之前并没有请求许可。那些在 bugbusters@FreeBSD.org 的好心人可不是为了娱乐而翻阅 bug 数据库;他们之所以这么做,是因为他们认为这值得花费时间。(如果你是程序员,翻阅 bug 数据库并找到可以解决的问题,是你能做出的最佳贡献之一。)

一旦你有了想法,就在邮件列表上搜索有关讨论。许多项目会被提议和辩论,但从未实现。如果有人提到了你的想法,阅读一下存档讨论。如果这个想法在过去几年获得了普遍的认可,但没人去做,那么就开始行动吧!FreeBSD 的朋友们会很高兴第一次听到你的消息是在一个错误报告中,说:“嗨,这是我实现这个功能的补丁,正如在某某邮件列表线程中讨论的那样。”

无论你做什么,都不要在邮件列表或论坛上问,“为什么不让别人为 X 做这项工作?”这些建议大多可以归为三类:显而易见的(“嘿,如果 FreeBSD 能在特斯拉上运行,那不是很酷吗?”)、愚蠢的(“为什么没有一个内核选项叫做 BRINGMEACOLDBEER?”)、或者两者都有(“为什么不支持我的 Sinclair ZX80?”)。在任何一种情况下,提出问题的人既不具备自己做这项工作的能力,也声称无法支持那些做这项工作的人。所有这些建议只会浪费带宽并惹恼他人。带宽便宜,但人不是。

总之:闭嘴,去做。做你能做的,做好它,人们会感激你。程序员可以通过跳入错误数据库,挑选一个有前景的错误来处理。非程序员可以通过找到一个自己能填补的漏洞,并做工作来填补它。你可能会成为 FreeBSD 的领导者,或者你可能会被称为“那个总是出现在-questions@,帮助人们解决 EFI 引导加载器问题的了不起的女性。”这些都是至关重要的。你的帮助让 FreeBSD 蓬勃发展。坚持足够长时间,你开始时帮助人们解决 EFI 引导加载器问题,可能会发展成代表整个 FreeBSD 的工作。

我期待在邮件列表上见到你。

posted @ 2025-11-25 17:06  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报