现代-Linux-学习指南-全-
现代 Linux 学习指南(全)
原文:
zh.annas-archive.org/md5/533721df49cfec6ada4695a4c0ac812d译者:飞龙
序言
学习现代 Linux 热烈欢迎你!我很高兴我们将一同走过这段旅程。如果你已经在使用 Linux,并希望深入了解结构化、实践性的方法,或者已经有经验,想要在使用 Linux 时获得一些技巧和窍门,例如在专业设置(如开发或运维)中,那么这本书适合你。
我们将专注于使用 Linux 来满足你日常需求,从开发到办公任务,而不是系统管理方面的事务。同时,我们将专注于命令行,而不是视觉用户界面。因此,尽管 2022 年可能终将成为 Linux 桌面年,我们将使用终端作为与 Linux 交互的主要方式。这样做的另一个优势是,你可以在多种不同的设置中同样应用你的知识,从树莓派到你选择的云提供商的虚拟机。
在我们开始之前,我想通过分享我的个人经历为你提供一些背景:我的第一次操作系统实际体验并不是 Linux。我使用的第一个操作系统是 AmigaOS(在 80 年代末),之后在技术高中期间,我主要使用 Microsoft DOS 和当时新出现的 Microsoft Windows,具体涉及事件系统和用户界面相关的开发。然后,在 20 世纪 90 年代中期至晚期,在大学实验室中,我主要使用基于 Unix 的 Solaris 和 Silicon Graphics 机器。我真正开始接触 Linux 是在 2000 年代中期,与大数据相关,然后在 2015 年开始使用容器,首先是在 Apache Mesos 的背景下(在 Mesosphere 工作),然后是在 Kubernetes 上(最初在 Red Hat 的 OpenShift 团队,然后在 AWS 的容器服务团队)。那时我意识到,在这个领域有效工作需要精通 Linux。Linux 与众不同,其背景、全球用户社区以及多样性和灵活性使其独特。
Linux 是一个有趣的、不断增长的开源个体和供应商生态系统。它几乎可以在任何设备上运行,从价格仅为 50 美元的树莓派到你最喜爱的云提供商的虚拟机,甚至到火星车辆。经过 30 年的发展,Linux 很可能会继续存在一段时间,所以现在是深入学习 Linux 的好时机。
让我们首先设定一些基本规则和期望。在序言中,我将分享如何从本书中获得最大收益,以及一些行政事务,比如在哪里以及如何尝试我们将一同探讨的主题。
你的经历
本书适合那些需要或想要在专业环境中使用 Linux 的人士,如软件开发人员、软件架构师、QA 测试工程师、DevOps 和 SRE 角色以及类似角色。我假设如果您是一位业余爱好者,在进行诸如 3D 打印或家居改善等活动时遇到 Linux,您对操作系统一般或特别是 Linux/UNIX 的了解可能非常少或几乎没有。如果您从头到尾地阅读本书,您将会从中受益匪浅,因为各章节往往是相互构建的;但是,如果您已经熟悉 Linux,您也可以将其用作参考。
如何使用本书
本书的重点是让您使用 Linux,而不是管理它。关于 Linux 管理的优秀书籍有很多。
在本书结束时,您将了解什么是 Linux(第一章)及其关键组成部分(第 2 和 3 章)。您将能够列举和使用基本的访问控制机制(第四章)。您还将理解文件系统(第五章)在 Linux 中作为基础构建块的角色以及了解应用程序(第六章)是什么。
接下来,您将通过实践操作了解 Linux 网络堆栈和工具(第七章)。此外,您还将了解现代操作系统的可观测性(第八章)以及如何应用它来管理您的工作负载。
您将了解如何通过使用容器以及像 Bottlerocket 这样的不可变发行版的现代方法来运行 Linux 应用程序,以及如何通过安全外壳(SSH)和像点对点和云同步机制这样的高级工具安全通信(下载文件等)和共享数据(第九章)。
以下是您可以尝试的方式和跟随的建议(我强烈建议您这样做;学习 Linux 就像学习一门语言——您需要大量实践):
-
获取一台 Linux 桌面或笔记本电脑。例如,我有一台名为Star Labs的 StarBook 很不错的机器。或者,您可以使用一台不再运行最新 Windows 版本的桌面电脑或笔记本电脑,并在其上安装 Linux。
-
如果您想在不同的(主机)操作系统上尝试实验,比如您的 MacBook 或 iMac,您可以使用虚拟机(VM)。例如,在 macOS 上您可以使用优秀的Linux-on-Mac。
-
使用您选择的云提供商启动基于 Linux 的虚拟机。
-
如果您喜欢折腾并且想尝试 ARM 等非英特尔处理器架构,您可以购买像树莓派这样的单板计算机。
无论如何,您都应该准备好环境并进行大量实践。不要只是阅读:尝试执行命令并进行实验。例如,通过提供荒谬或故意奇怪的输入来“破坏”事物。在执行命令之前,先对结果形成假设。
另一个提示:始终问为什么。当你看到一个命令或特定输出时,尝试找出它的来源以及负责它的基础组件。
约定
本书使用以下排版约定:
斜体
指示新术语、网址、电子邮件地址、文件名和文件扩展名。
等宽字体
用于程序列表,以及在段落中引用程序元素,如变量或函数名、数据库、数据类型、环境变量、语句和关键字。
等宽斜体
显示应替换为用户提供的值或由上下文确定的值的文本。
提示
此元素表示提示或建议。
注意
此元素表示一般说明。
警告
此元素表示警告或注意。
使用代码示例
补充材料(代码示例、练习等)可在https://oreil.ly/learning-modern-linux-code下载。
如果您有技术问题或使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com。
本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您复制了代码的大部分内容,否则无需联系我们请求权限。例如,编写一个使用本书多个代码块的程序不需要权限。出售或分发 O’Reilly 书籍中的示例需要权限。引用本书并引用示例代码回答问题不需要权限。将本书中大量示例代码合并到产品文档中需要权限。
我们感谢,但通常不要求归属。归属通常包括标题、作者、出版社和 ISBN。例如:“学习现代 Linux,作者 Michael Hausenblas(O’Reilly)。版权所有 2022 Michael Hausenblas,978-1-098-10894-6。”
如果您认为您对代码示例的使用超出了合理使用或上述授权,请随时通过permissions@oreilly.com联系我们。
O’Reilly 在线学习
注意
40 多年来,O’Reilly Media提供技术和业务培训、知识和洞察,帮助公司取得成功。
我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专长。O'Reilly 的在线学习平台提供按需访问的实时培训课程、深入的学习路径、交互式编程环境,以及来自 O'Reilly 和其他 200 多家出版商的大量文本和视频。更多信息请访问http://oreilly.com。
如何联系我们
请将有关此书的评论和问题发送至出版商:
-
O'Reilly Media, Inc.
-
1005 Gravenstein Highway North
-
Sebastopol, CA 95472
-
800-998-9938(美国或加拿大)
-
707-829-0515(国际或本地)
-
707-829-0104(传真)
我们为这本书设立了一个网页,列出勘误、示例和任何额外信息。您可以访问https://oreil.ly/learning-modern-linux。
发送电子邮件至bookquestions@oreilly.com以就此书发表评论或提出技术问题。
有关我们的书籍和课程的新闻和信息,请访问http://oreilly.com。
在 LinkedIn 上找到我们:https://linkedin.com/company/oreilly-media
在 Twitter 上关注我们:http://twitter.com/oreillymedia
在 YouTube 上观看我们:http://youtube.com/oreillymedia
致谢
首先,我要感谢这本书的出色审阅人员:克里斯·尼古斯、约翰·博内西奥和帕维尔·克鲁帕。没有他们的反馈,这本书将不会那么出色和有用。
我要感谢我的父母,他们支持我的教育,并为我今天的成就打下了基础。非常感谢我的姐姐莫妮卡,她是我进入科技领域的灵感来源。
我要向我那非常棒且支持我的家人表达最深的感激之情:我的孩子们,萨菲拉、兰娅和伊安尼斯;我聪明又有趣的妻子安妮莉丝;我们最好的狗狗史诺比;以及我们最新的家庭成员查理,一只公猫。
在我 Unix 和 Linux 的旅程中,有太多人影响了我的思维,并从他们那里学到了很多。我有幸与许多人共事或互动,其中包括但不限于杰罗姆·佩塔佐尼、杰西·弗雷泽尔、布伦丹·格雷格、贾斯汀·加里森、迈克尔·克里斯克和道格拉斯·麦克尔罗伊。
最后但同样重要的是,我要感谢 O'Reilly 团队,特别是我的开发编辑杰夫·布莱尔,他在书写过程中给予了我极大的帮助。
第一章:介绍 Linux
Linux 是目前被广泛使用的操作系统,应用范围从移动设备到云端。
你可能对操作系统的概念并不熟悉。或者你可能在使用像微软 Windows 这样的操作系统,而并未深究其背后。或者你可能是 Linux 的新手。为了让你有正确的认知和心态,本章将从鸟瞰视角来看操作系统和 Linux。
我们将首先讨论本书背景下现代的含义。然后,我们将回顾 Linux 在过去 30 年中的重要事件和阶段。此外,在本章中,你将了解操作系统的角色及 Linux 如何充当这一角色。我们还会快速浏览 Linux 发行版以及资源可见性的概念。
如果你对操作系统和 Linux 还不太了解,建议你阅读整章。如果你已经对 Linux 有经验,可以直接跳到“Linux 的一万英尺视角”,这部分提供了视觉概述并与本书章节进行了对应。
但在深入技术细节之前,让我们先稍微退后一步,专注于我们说“现代 Linux”时的含义。这其实是一个非常复杂的问题。
现代环境是什么?
书名明确指定为现代,但这到底意味着什么呢?在本书的背景下,这可以涵盖从云计算到树莓派等任何内容。此外,Docker 及其相关基础设施的最新发展,显著改变了开发人员和基础设施运营者的情景。
让我们更详细地了解一些现代环境及 Linux 在其中扮演的重要角色:
移动设备
当我对孩子们说“手机”时,他们会说,“与什么相比?”老实说,在今天,许多手机——根据不同人的说法,高达 80%或更多——以及平板电脑都运行着 Android,这是一个Linux 变体。这些环境在功耗和稳定性方面有着极高的要求,因为我们每天都依赖它们。如果你对开发 Android 应用感兴趣,请考虑访问Android 开发者网站获取更多信息。
云计算
在云端,我们看到类似于移动和微空间的规模模式。有新的、强大的、安全的和节能的 CPU 架构,例如成功的基于 ARM 的AWS Graviton产品,以及在开源软件环境中特别是在云提供商领域的成熟的大型外包服务。
物联网(智能设备)
我相信你已经看到了许多与物联网(IoT)相关的项目和产品,从传感器到无人机。我们许多人已经接触过智能家电和智能汽车。这些环境对功耗有更高的要求,甚至可能不是一直运行,比如每天只唤醒一次以传输一些数据。这些环境的另一个重要方面是实时能力。如果您有兴趣在 IoT 环境中开始使用 Linux,请考虑AWS IoT EduKit。
处理器架构的多样性
在过去大约 30 年中,Intel 一直是领先的 CPU 制造商,主导微型计算机和个人计算机领域。Intel 的 x86 架构被视为金标准。IBM 采取的开放方式(发布规格并使其他人能够提供兼容设备)前景广阔,导致了使用 Intel 芯片的 x86 克隆机,至少在最初是这样的。
尽管 Intel 在台式机和笔记本系统中仍然广泛使用,但随着移动设备的兴起,我们看到了ARM 架构和最近的RISC-V的日益普及。同时,像 Go 或 Rust 这样的多架构编程语言和工具也越来越普及,形成了一场完美的风暴。
所有这些环境都是我认为的现代环境的例子。几乎所有这些环境都以某种形式运行或使用 Linux。
现在我们了解了现代(硬件)系统,您可能想知道我们是如何到达这里的,以及 Linux 是如何产生的。
Linux 的故事(到目前为止)
Linux 在 2021 年庆祝了30 岁的生日。拥有数十亿用户和数千名开发人员,Linux 项目毫无疑问是一个全球(开源)成功故事。但是这一切是如何开始的,我们又是如何走到今天的呢?
1990 年代
我们可以将 Linus Torvalds 于 1991 年 8 月 25 日在comp.os.minix新闻组中发布的电子邮件视为 Linux 项目诞生的公开记录。这个业余项目很快就起飞了,无论是在代码行数(LOC)还是采纳率方面。例如,不到三年时间,Linux 1.0.0 发布,代码行数超过 176,000 行。到那时,实现运行大多数 Unix/GNU 软件的最初目标已经实现。此外,上世纪 90 年代出现了第一个商业版本:Red Hat Linux。
2000 年至 2010 年
作为一个“青少年”,Linux 不仅在功能和支持的硬件方面日益成熟,而且超越了 UNIX 的能力。在这个时期,我们还见证了 Linux 被 Google、Amazon、IBM 等大公司广泛采纳,并且采纳程度不断增加。这也是发行版之战的高峰期,导致企业改变了他们的方向。
2010 年至今
Linux 在数据中心和云端以及各种物联网设备和手机中确立了自己的地位。从某种意义上说,可以认为发行版之战已经结束(现在,大多数商业系统都基于 Red Hat 或 Debian),而容器的兴起(从 2014/15 年开始)促成了这一发展。
通过这个超快的历史回顾,有必要来设定背景并理解本书的范围的动机,我们继续看一个看似无辜的问题:为什么任何人都需要 Linux,或者说需要操作系统?
为什么需要操作系统?
假设你没有可用的操作系统(OS),或者由于任何原因无法使用它。那么你最终会自己做几乎所有的事情:内存管理,中断处理,与 I/O 设备通信,文件管理,配置和管理网络堆栈——列举的列表还在继续。
注意
从技术上讲,操作系统并非绝对必要。有一些系统并没有操作系统。这些通常是具有微小足迹的嵌入式系统:比如物联网信标。它们简单地没有足够的资源来保留除一个应用程序之外的任何其他东西。例如,使用 Rust,你可以使用其核心和标准库在裸机上运行任何应用程序。
操作系统承担所有这些未区分的重活,抽象掉不同的硬件组件,并为您提供(通常是)干净和设计良好的应用程序编程接口(API),例如 Linux 内核的情况,我们将在第二章中更详细地查看。我们通常称这些操作系统暴露的 API 为系统调用,或简称为syscalls。高级编程语言如 Go、Rust、Python 或 Java 建立在这些 syscalls 的基础上,可能将它们包装在库中。
所有这些使您能够专注于业务逻辑,而不必自己管理资源,同时也能处理您希望在其上运行应用程序的不同硬件。
让我们来看一个系统调用的具体例子。假设我们想要识别(并打印)当前用户的 ID。
首先,我们看看 Linux 系统调用getuid(2):
...
getuid() returns the real user ID of the calling process.
...
好的,所以这个getuid系统调用是我们可以从库中编程使用的。我们将在“系统调用”中更详细地讨论 Linux 系统调用。
注意
你可能会想知道getuid(2)中的(2)代表什么。这是man实用程序(类似于内置帮助页面)用来指示分配给man命令的部分的术语,类似于邮政或国家代码。这是 Unix 遗产显现出来的一个例子;你可以在Unix Programmer’s Manual,第七版,卷 1中找到其起源,1979 年。
在命令行(shell)中,我们将使用等效的id命令,该命令进而使用getuid系统调用:
$ id --user
638114
现在,你对为什么在大多数情况下使用操作系统有了基本的理解,让我们继续讨论 Linux 发行版的主题。
Linux 发行版
当我们说“Linux”时,可能不会立即明白我们的意思。在本书中,当我们说“Linux 内核”或者只说“内核”时,我们指的是系统调用和设备驱动程序的集合。此外,当我们提到Linux 发行版(简称distros)时,我们指的是一种具体的内核及相关组件捆绑,包括包管理器、文件系统布局、init 系统和一个为您预先选择的 Shell。
当然,你也可以自己动手:你可以下载并编译内核,选择一个包管理器等等,并创建(或者说roll出)自己的发行版。许多人在早期就是这样做的。多年来,人们逐渐意识到,把这些打包(以及安全补丁)交给专家(无论是私人还是商业)处理,并简单使用生成的 Linux 发行版,是更好的时间利用方式。
提示
如果你有意打造自己的发行版,也许是因为你喜欢动手或因为出于某些业务限制,我建议你仔细研究Arch Linux,它能让你掌控一切,并且通过一点努力,你可以创建一个非常定制化的 Linux 发行版。
要感受发行版空间的广阔,包括传统的发行版(如 Ubuntu、Red Hat Enterprise Linux [RHEL]、CentOS 等,详见第六章)和现代的发行版(如 Bottlerocket 和 Flatcar;详见第九章),请访问DistroWatch。
关于发行版的话题讨论完毕,让我们转移到一个完全不同的话题:资源及其可见性与隔离。
资源可见性
Linux 一直以来,在 UNIX 的传统下,默认具有全局视图的资源观念。这带来了一个问题:全局视图意味着什么(与之相对的是什么?),以及这些资源是什么?
注意
首先,为什么我们在这里讨论资源可见性?主要原因是提高对这个主题的认识,并帮助您进入正确的思维状态,以便理解现代 Linux 背景下的重要主题之一:容器。如果现在您还没有完全理解所有细节,不要担心;我们将在整本书中以及特别是第六章中多次回到这个主题,详细讨论容器及其构建块。
你可能听说过在 Unix 中,以及扩展到 Linux,一切皆为文件的说法。在本书的背景下,我们认为资源是任何可以用来辅助软件执行的东西。这包括硬件及其抽象(如 CPU 和 RAM,文件),文件系统,硬盘驱动器,固态硬盘(SSD),进程,与网络相关的设备或路由表,以及代表用户的凭据。
警告
在 Linux 中,并非所有资源都是文件或通过文件接口表示。但是,有一些系统(如 Plan 9)推动这一点更进一步。
让我们看看一些 Linux 资源的具体示例。首先,我们想查询一个全局属性(Linux 版本),然后查询正在使用的 CPU 的特定硬件信息(输出已编辑以适应空间):
$ cat /proc/version 
Linux version 5.4.0-81-generic (buildd@lgw01-amd64-051)
(gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04))
#91~18.04.1-Ubuntu SMP Fri Jul 23 13:36:29 UTC 2021
$ cat /proc/cpuinfo | grep "model name" 
model name : Intel Core Processor (Haswell, no TSX, IBRS)
model name : Intel Core Processor (Haswell, no TSX, IBRS)
model name : Intel Core Processor (Haswell, no TSX, IBRS)
model name : Intel Core Processor (Haswell, no TSX, IBRS)
打印 Linux 版本。
打印与模型相关的 CPU 信息。
通过前述命令,我们了解到该系统有四个英特尔 i7 核心可供使用。当您使用不同用户登录时,您是否期望看到相同数量的 CPU?
让我们考虑一种不同类型的资源:文件。例如,如果用户 troy 在 /tmp/myfile 下创建一个有权限进行此操作的文件(“权限”),另一个用户 worf 是否能看到这个文件或者能够对其进行写入?
或者,以进程为例,即存储在内存中具有运行所需所有必要资源的程序,如 CPU 和内存。Linux 使用其 进程 ID 或简称 PID 来标识进程(详见 “进程管理”):
$ cat /proc/$$/status | head -n6 
Name: bash
Umask: 0002
State: S (sleeping)
Tgid: 2056
Ngid: 0
Pid: 2056
打印进程状态,即当前进程的详细信息,并将输出限制为仅显示前六行。
在 Linux 中可以有多个具有相同 PID 的进程吗?这可能听起来像是一个愚蠢或无用的问题,但事实证明这是容器的基础(详见 “容器”)。答案是可以的,可以在不同的上下文中称为 命名空间(详见 “Linux 命名空间”)中有多个具有相同 PID 的进程,例如在 Docker 或 Kubernetes 中运行应用时。
每个单独的进程可能会认为自己很特殊,具有 PID 1,在更传统的设置中,这是为用户空间进程树的根保留的位置(详见 “Linux 启动过程”)。
从这些观察中我们可以得出的结论是,对于给定的资源可以有全局视图(两个用户在同一位置看到一个文件)以及本地或虚拟化视图,例如进程示例。这引出了一个问题:在 Linux 中,默认情况下是否所有内容都是全局的?剧透:并不是。让我们仔细看看。
并行运行多个用户或进程的错觉的一部分是(受限)对资源的可见性。在 Linux 中,提供对(某些支持的)资源的本地视图的方法是通过命名空间(详见 “Linux 命名空间”)。
另一个独立的维度是隔离。当我在这里使用“隔离”一词时,并不一定对其进行限定——也就是说,我不假设事物的隔离程度如何。例如,考虑进程隔离的一种方式是限制内存消耗,以使一个进程不能使其他进程饥饿。例如,我给你的应用分配了 1 GB 的 RAM 来使用。如果它使用更多,就会被 内存不足 杀掉。这提供了一定程度的保护。在 Linux 中,我们使用一个称为 cgroups 的内核特性来提供这种隔离,你将在 “Linux cgroups” 中进一步了解它。
另一方面,一个完全隔离的环境给人的感觉是应用程序完全独立运行。例如,虚拟机(VM;见 “虚拟机”)可以用来提供完全的隔离。
Linux 的一万英尺视图
哇,我们已经深入到了复杂的细节中。是时候深呼吸并重新聚焦了。在 图 1-1 中,我试图为您提供 Linux 操作系统的高级概述,将其映射到书的各章节。

图 1-1. 将 Linux 操作系统映射到书的章节
从其核心来看,任何 Linux 发行版都有内核,提供一切构建的 API。文件、网络和可观察性这三个核心主题随处可见,你可以将它们视为内核之上最基本的构建模块。从纯使用角度来看,你很快会发现,你最常处理的是 shell(这个应用的输出文件在哪里?)和与访问控制相关的事物(为什么这个应用程序崩溃了?啊,这个目录是只读的,笨!)。
顺便说一下:我在 第九章 中收集了一些有趣的话题,从虚拟机到现代发行版,我称这些话题为“高级”主要是因为我认为它们是可选的。也就是说,你可以不学习它们也能应付过去。但是,如果你真的、真的、真的想要充分利用现代 Linux 所能提供的全部功能,我强烈建议你阅读 第九章。我想这不言而喻,按设计,书中的其他部分——从 第二章 到 第八章——是你绝对应该学习和应用其内容的必要章节。
结论
当我们在本书的背景下称某些东西为“现代”时,我们指的是在现代环境中使用 Linux,包括手机、数据中心(公共云提供商)和嵌入式系统,比如树莓派。
在本章中,我分享了 Linux 背景的高层次版本。我们讨论了操作系统的一般角色——抽象化底层硬件并为应用程序提供一组基本功能,如进程、内存、文件和网络管理——以及 Linux 如何在这一任务中进行操作,特别是关于资源可见性的方面。
以下资源将帮助您继续加快学习速度,并深入探讨本章讨论的概念:
O’Reilly 的其他书籍
-
Linux Cookbook 由卡拉·施罗德编著
-
深入理解 Linux 内核 由丹尼尔·P·博韦特和马可·切萨蒂编著
-
高效 Linux 命令行 由丹尼尔·J·巴雷特编著
-
Linux 系统编程 由罗伯特·洛夫编著
其他资源
-
UNIX 环境高级编程 是一门完整的课程,提供入门材料和实践练习。
-
“UNIX 的诞生” 与布莱恩·科尼汉是了解 Linux 传承的重要资源,为原始 UNIX 概念提供背景。
现在,毫不拖泥带水地:让我们开始探索现代 Linux 的核心——嗯,内核,事情的要点!
第二章:Linux 内核
在"为什么需要操作系统?"中,我们了解到操作系统的主要功能是在不同的硬件之上提供抽象,并为我们提供 API。通过编程调用这个 API,我们可以编写应用程序,而不必担心它们在何处以及如何执行。简而言之,内核为程序提供了这样的 API。
在本章中,我们将讨论 Linux 内核是什么,以及您应该如何整体思考它及其组成部分。您将了解 Linux 整体架构以及 Linux 内核的基本作用。本章的一个主要要点是,虽然内核提供了所有核心功能,但它本身并不是操作系统,而只是其中非常核心的一部分。
首先,我们俯瞰全局,看看内核如何适应并与底层硬件交互。然后,我们回顾计算核心,讨论不同的 CPU 架构及其与内核的关系。接下来,我们放大内核的各个组件,并讨论内核为您可以运行的程序提供的 API。最后,我们看看如何定制和扩展 Linux 内核。
本章的目的是为您提供必要的术语,让您了解程序与内核之间的接口,并让您基本了解功能是什么。本章的目标不是让您成为内核开发人员,甚至不是系统管理员配置和编译内核。但是,如果您有兴趣深入研究,我在章节末尾提供了一些指引。
现在,让我们深入探讨:Linux 架构及内核在这一背景下的中心作用。
Linux 架构
在高层次上,Linux 架构看起来如图 2-1 所示。您可以将事物分为三个不同的层次:
硬件
从 CPU 和主存储器到磁盘驱动器、网络接口和诸如键盘和显示器等外围设备。
内核
本章剩余部分的重点。请注意,还有一些组件位于内核和用户空间之间,如初始化系统和系统服务(网络等),但严格来说,它们不是内核的一部分。
用户空间
大多数应用程序运行的地方,包括操作系统组件如 shell(在第三章中讨论)、诸如ps或ssh之类的实用程序,以及基于 X Window 系统的图形用户界面桌面。
本书重点讨论图 2-1 中的上两层,即内核和用户空间。在这本书的本章和其他几章中,我们只会涉及硬件层面,涉及相关内容。
不同层次之间的接口已经明确定义,并且是 Linux 操作系统包的一部分。在内核和用户空间之间的接口称为系统调用(简称syscalls)。我们将在“系统调用”中详细探讨这一点。
与系统调用不同,硬件与内核之间的接口不是单一的。它由多个个体接口组成,通常按硬件分组:
-
CPU 接口(见“CPU 架构”)
-
与主存储器的接口,详见“内存管理”
-
网络接口和驱动程序(有线和无线;见“网络”)
-
文件系统和块设备驱动程序接口(见“文件系统”)
-
字符设备、硬件中断和设备驱动程序,适用于键盘、终端和其他 I/O 输入设备(见“设备驱动程序”)

图 2-1. Linux 架构的高级视图
正如您所见,许多我们通常认为属于 Linux 操作系统一部分的东西,比如 shell 或诸如 grep、find 和 ping 等实用程序,实际上并不是内核的一部分,而更像是您下载的应用程序,属于用户空间。
在用户空间的讨论中,您经常会读到或听到用户与内核模式的区别。这实际上指的是对硬件访问的特权以及可用的抽象受到的限制程度。
一般而言,内核模式意味着快速执行但抽象化程度有限,而用户模式则意味着相对较慢但更安全和更便捷的抽象化。除非您是内核开发者,否则几乎可以忽略内核模式,因为所有应用程序都将在用户空间运行。然而,了解如何与内核交互(见“系统调用”)对我们的考虑至关重要。
现在 Linux 架构概述已经介绍完毕,让我们从硬件开始逐步深入。
CPU 架构
在讨论内核组件之前,让我们回顾一个基本概念:计算机体系结构或 CPU 家族,我们将这两个术语视为同义词使用。Linux 可以运行在众多不同的 CPU 架构上,这无疑是其如此受欢迎的原因之一。
除了通用代码和驱动程序,Linux 内核还包含特定于体系结构的代码。这种分离使得能够迅速将 Linux 移植并应用于新硬件。
有多种方法可以确定您的 Linux 运行在哪种 CPU 上。让我们依次看看几种。
一种方法是使用称为 dmidecode 的专用工具与 BIOS 进行交互。如果这不起作用,您可以尝试以下方法(输出已缩短):
$ lscpu
Architecture: x86_64 
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 40 bits physical, 48 bits virtual
CPU(s): 4 
On-line CPU(s) list: 0-3
Thread(s) per core: 1
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 60
Model name: Intel Core Processor (Haswell, no TSX, IBRS) 
Stepping: 1
CPU MHz: 2592.094
...
我们所关注的架构是 x86_64。
看起来有四个可用的 CPU。
CPU 型号名称是英特尔酷睿处理器(Haswell)。
在前面的命令中,我们看到 CPU 架构被报告为 x86_64,型号报告为“英特尔酷睿处理器(Haswell)”。我们稍后将更多了解如何解码这些信息。
另一种获取类似架构信息的方式是使用 cat /proc/cpuinfo,或者,如果你只关心架构,可以简单地调用 uname -m。
现在我们掌握了如何在 Linux 上查询架构信息,让我们看看如何解码它。
x86 架构
x86 是最初由英特尔开发并后来许可给了 AMD 的指令集系列。在内核中,x64 指的是英特尔的 64 位处理器,x86 表示英特尔的 32 位处理器。此外,amd64 指的是 AMD 的 64 位处理器。
如今,在桌面和笔记本电脑中主要使用 x86 CPU 家族,但它也广泛用于服务器。具体来说,x86 架构构成了公共云的基础。它是一种强大且广泛可用的架构,但能效不高。部分原因是它严重依赖乱序执行,最近因安全问题(如Meltdown)而受到广泛关注。
欲了解更多详细信息,例如 Linux/x86 引导协议或英特尔和 AMD 特定的背景,请参阅x86 特定的内核文档。
ARM 架构
超过 30 年历史的ARM 是一系列精简指令集计算(RISC)架构。RISC 通常包括许多通用 CPU 寄存器以及可以更快执行的一小组指令。
由于 Acorn 公司——ARM 背后的原始公司——从一开始就专注于最小功耗,你可以在许多便携设备中找到基于 ARM 的芯片,如 iPhone。它们也广泛用于大多数基于 Android 的手机和嵌入式系统中的物联网设备,如树莓派。
考虑到它们快速、廉价且产热量比 x86 芯片少,你不应对在数据中心中越来越多地发现基于 ARM 的 CPU(例如AWS Graviton)感到意外。虽然比 x86 简单,但 ARM 并非不受漏洞影响,例如Spectre。有关详细信息,请参阅ARM 特定的内核文档。
RISC-V 架构
一个新兴的玩家,RISC-V(发音为 risk five) 是一个开放的 RISC 标准,最初由加州大学伯克利分校开发。截至 2021 年,已存在多种实现,从阿里巴巴集团和英伟达到初创公司如 SiFive。尽管令人兴奋,这是一个相对新的且尚未广泛使用的 CPU 家族,想要了解其外观和感觉,你可能需要进行一些研究——一个好的开始是 Shae Erisson 的文章 “Linux on RISC-V”。
要获取更多详细信息,请参阅RISC-V 内核文档。
内核组件
现在您已经了解了 CPU 架构的基础知识,是时候深入研究内核了。尽管 Linux 内核是一个单体内核,也就是说,所有讨论的组件都是单个二进制文件的一部分,但我们可以在代码库中识别并分配专门的功能区域。
正如我们在“Linux 架构”中讨论过的那样,内核位于硬件和您想要运行的应用程序之间。内核代码库中的主要功能模块如下:
-
进程管理,例如基于可执行文件启动进程
-
内存管理,例如为进程分配内存或将文件映射到内存中
-
网络,如管理网络接口或提供网络栈
-
提供文件管理并支持文件的创建和删除的文件系统
-
字符设备和设备驱动程序的管理
这些功能组件通常伴随着相互依赖关系,并确保“内核永不破坏用户空间”的内核开发者格言是一项真正具有挑战性的任务。
有了这些了解之后,让我们更仔细地看看内核组件。
进程管理
内核中有许多与进程管理相关的部分。其中一些部分处理与 CPU 架构特定的事务,例如中断,而其他部分专注于程序的启动和调度。
在深入了解 Linux 具体细节之前,让我们注意到,通常情况下,一个进程是用户界面单元,基于可执行程序(或二进制文件)。另一方面,线程是在进程上下文中的执行单元。您可能听说过 多线程 这个术语,它意味着一个进程有多个并行执行,可能在不同的 CPU 上运行。
有了这个总体视图之后,让我们看看 Linux 是如何实现的。从最精细到最小单元,Linux 有以下几个方面:
会话
包含一个或多个进程组,并代表一个高级用户界面单元,可以选择连接 tty。内核通过称为 会话 ID(SID)的数字标识会话。
进程组
包含一个或多个进程,会话中至多有一个作为前台进程组的进程组。内核通过称为 进程组 ID(PGID)的数字标识进程组。
进程
抽象出多个资源(地址空间、一个或多个线程、套接字等),内核通过 /proc/self 为当前进程向您公开。内核通过称为 进程 ID(PID)的数字标识进程。
线程
由内核实现为进程。也就是说,没有专门表示线程的数据结构。相反,线程是与其他进程共享某些资源(如内存或信号处理程序)的进程。内核通过线程 ID(TID)和线程组 ID(TGID)来识别线程,共享 TGID 值意味着一个多线程的进程(在用户空间;还有内核线程,但这超出了我们的范围)。
任务
在内核中有一个称为task_struct的数据结构——在sched.h中定义——它是实现进程和线程的基础。该数据结构包含调度相关信息、标识符(如 PID 和 TGID)、信号处理程序,以及与性能和安全性相关的其他信息。简而言之,所有前述的单元都源自和/或锚定在任务中;然而,在内核外部并没有直接暴露任务。
我们将在第六章中看到会话、进程组和进程的实际运作方式,并学习如何管理它们,并且它们将再次出现在第九章中,关于容器的上下文中。
让我们看看这些概念如何实际运作:
$ ps -j
PID PGID SID TTY TIME CMD
6756 6756 6756 pts/0 00:00:00 bash 
6790 6790 6756 pts/0 00:00:00 ps 
bash shell 进程的 PID、PGID 和 SID 是 6756。从ls -al /proc/6756/task/6756/可以获取任务级别的信息。
ps 进程的 PID/PGID 是 6790,与 shell 相同的 SID。
我们之前提到在 Linux 中任务数据结构中随时可获得一些与调度相关的信息。这意味着在任何给定时间,进程处于特定状态,如图 2-2 所示。

图 2-2. Linux 进程状态
注
严格来说,进程状态稍微复杂一些;例如,Linux 区分可中断和不可中断的睡眠状态,还有僵尸状态(失去父进程)。如果你对细节感兴趣,请查阅文章“Linux 中的进程状态”。
不同的事件会导致状态转换。例如,运行中的进程在执行一些 I/O 操作(如从文件中读取)时可能会转换到等待状态,无法继续执行(离开 CPU)。
简单浏览了一下进程管理之后,让我们来看一个相关的话题:内存。
内存管理
虚拟内存使得您的系统看起来比实际拥有更多的内存。事实上,每个进程都获得了大量(虚拟)内存。工作原理如下:物理内存和虚拟内存都被划分为我们称为页的固定长度块。
图 2-3 展示了两个进程的虚拟地址空间,每个进程都有自己的页表。这些页表将进程的虚拟页面映射到主内存(即 RAM)中的物理页面。

图 2-3. 虚拟内存管理概述
多个虚拟页面可以通过各自的进程级页表指向同一个物理页面。从某种意义上说,这是内存管理的核心:如何在有效地为每个进程提供其页面实际存在于 RAM 中的幻觉的同时,最优地使用现有空间。
每当 CPU 访问一个进程的虚拟页时,CPU 原则上需要将进程使用的虚拟地址转换为相应的物理地址。为了加速这一过程——这可能是多级的,因此很慢——现代 CPU 架构支持芯片上的查找,称为翻译后备缓冲区(TLB)。TLB 实际上是一个小缓存,如果未命中,则导致 CPU 通过进程的页表计算页面的物理地址,并将其更新到 TLB 中。
传统上,Linux 的默认页面大小为 4 KB,但自内核 v2.6.3 以来,它支持大页面,以更好地支持现代架构和工作负载。例如,64 位 Linux 允许每个进程使用高达 128 TB 的虚拟地址空间(虚拟是指理论上可寻址的内存地址数),总共约 64 TB 的物理内存(物理是指机器上实际的 RAM 量)。
好的,这是大量的理论信息。让我们从更实际的角度来看待它。一个非常有用的工具,用于查找诸如可用 RAM 量等与内存相关的信息,是/proc/meminfo接口:
$ grep MemTotal /proc/meminfo 
MemTotal: 4014636 kB
$ grep VmallocTotal /proc/meminfo 
VmallocTotal: 34359738367 kB
$ grep Huge /proc/meminfo 
AnonHugePages: 0 kB
ShmemHugePages: 0 kB
FileHugePages: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
列出物理内存(RAM)的详细信息;那里有 4 GB。
列出虚拟内存的详细信息;那里有超过 34 TB。
列出大页面的信息;显然这里的页面大小为 2 MB。
接下来,我们进入下一个内核函数:网络。
网络
内核的一个重要功能是提供网络功能。无论您想浏览网页还是将数据复制到远程系统,您都依赖网络。
Linux 网络堆栈遵循分层架构:
套接字
用于抽象通信
传输控制协议(TCP)和用户数据报协议(UDP)
分别用于面向连接的通信和无连接的通信
互联网协议(IP)
用于处理机器地址
内核负责这三个动作。应用层协议(例如 HTTP 或 SSH)通常是在用户空间中实现的。
您可以使用以下命令获取您的网络接口概述(输出已编辑):
$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode
DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00
brd 00:00:00:00:00:00
2: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state
UP mode DEFAULT group default qlen 1000 link/ether 52:54:00:12:34:56
brd ff:ff:ff:ff:ff:ff
此外,ip route提供了路由信息。因为我们有一个专门的网络章节(第七章),我们将深入研究网络堆栈、支持的协议和典型操作,我们将在此保持这样的状态,并转向下一个内核组件,即块设备和文件系统。
文件系统
Linux 使用文件系统来组织存储设备(如硬盘驱动器(HDD)和固态硬盘(SSD)或闪存内存)上的文件和目录。有许多类型的文件系统,如ext4和btrfs或 NTFS,并且您可以同时使用多个相同的文件系统实例。
虚拟文件系统(VFS)最初是引入以支持多种文件系统类型和实例。在 VFS 的最高层提供了诸如打开、关闭、读取和写入等函数的通用 API 抽象。在 VFS 的底部是称为给定文件系统的插件的文件系统抽象。
我们将在第五章中详细讨论文件系统和文件操作。
设备驱动程序
驱动程序是在内核中运行的一小段代码。它的工作是管理设备,可以是实际的硬件,如键盘、鼠标或硬盘驱动器,也可以是伪设备,例如/dev/pts/下的伪终端(它不是物理设备,但可以像一个物理设备一样对待)。
另一个有趣的硬件类别是图形处理单元(GPU),传统上用于加速图形输出并减轻 CPU 负担。近年来,GPU 在机器学习领域找到了新的用例,因此它们不仅在桌面环境中具有相关性。
驱动程序可以静态构建到内核中,也可以作为内核模块构建(参见“模块”),以便在需要时动态加载。
提示
如果你对交互式探索设备驱动程序及内核组件如何交互感兴趣,请查看Linux 内核地图。
内核驱动程序模型复杂且超出了本书的范围。但是,以下是一些与之交互的提示,仅足够让您知道在哪里找到需要的内容。
要获取 Linux 系统上设备的概述,您可以使用以下方法:
$ ls -al /sys/devices/
total 0
drwxr-xr-x 15 root root 0 Aug 17 15:53 .
dr-xr-xr-x 13 root root 0 Aug 17 15:53 ..
drwxr-xr-x 6 root root 0 Aug 17 15:53 LNXSYSTM:00
drwxr-xr-x 3 root root 0 Aug 17 15:53 breakpoint
drwxr-xr-x 3 root root 0 Aug 17 17:41 isa
drwxr-xr-x 4 root root 0 Aug 17 15:53 kprobe
drwxr-xr-x 5 root root 0 Aug 17 15:53 msr
drwxr-xr-x 15 root root 0 Aug 17 15:53 pci0000:00
drwxr-xr-x 14 root root 0 Aug 17 15:53 platform
drwxr-xr-x 8 root root 0 Aug 17 15:53 pnp0
drwxr-xr-x 3 root root 0 Aug 17 15:53 software
drwxr-xr-x 10 root root 0 Aug 17 15:53 system
drwxr-xr-x 3 root root 0 Aug 17 15:53 tracepoint
drwxr-xr-x 4 root root 0 Aug 17 15:53 uprobe
drwxr-xr-x 18 root root 0 Aug 17 15:53 virtual
此外,您可以使用以下方法列出已挂载的设备:
$ mount
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620, \
ptmxmode=000)
...
tmpfs on /run/snapd/ns type tmpfs (rw,nosuid,nodev,noexec,relatime,\
size=401464k,mode=755,inode64)
nsfs on /run/snapd/ns/lxd.mnt type nsfs (rw)
通过这一部分,我们已经涵盖了 Linux 内核组件,并转向内核与用户空间之间的接口。
系统调用
无论您是坐在终端前输入touch test.txt,还是您的应用程序想要从远程系统下载文件的内容,归根结底,您都要求 Linux 将高级指令(例如“创建文件”或“从某处地址读取所有字节”)转换为一组具体的、与体系结构相关的步骤。换句话说,内核公开的服务接口及用户空间实体调用的即是系统调用集合,或简称为syscalls。
Linux 有数百种系统调用:大约三百个或更多,具体取决于 CPU 系列。然而,您和您的程序通常不会直接调用这些系统调用,而是通过我们称之为C 标准库来调用。标准库提供了包装函数,并以各种实现形式提供,如glibc或musl。
这些包装库执行了一个重要的任务。它们处理了系统调用执行的重复低级别处理。系统调用实现为软件中断,导致传输控制到异常处理程序。每次调用系统调用时都需要处理一些步骤,如图 2-4 所示:

图 2-4. Linux 中的系统调用执行步骤
-
定义在syscall.h和与体系结构相关的文件中,内核使用所谓的syscall 表,实际上是内存中的函数指针数组(存储在名为
sys_call_table的变量中),用于跟踪系统调用及其对应的处理程序。 -
system_call()函数充当系统调用复用器,它首先将硬件上下文保存在堆栈上,然后执行检查(例如是否执行跟踪),然后跳转到sys_call_table中相应系统调用编号索引指向的函数。 -
在使用
sysexit完成系统调用后,包装库将恢复硬件上下文,程序执行将在用户空间继续。
值得注意的是前面步骤中内核模式和用户空间模式之间的切换,这是一个耗时的操作。
好的,刚才那些都有点枯燥和理论化,为了更好地理解系统调用在实践中的外观和感觉,让我们看一个具体的例子。我们将使用strace来窥探一下幕后,这是一个用于故障排除的工具,例如,如果您没有应用程序的源代码但想了解它的工作原理。
假设您想知道在执行看似无害的ls命令时涉及哪些系统调用。以下是您可以使用strace找出答案的方式:
$ strace ls 
execve("/usr/bin/ls", ["ls"], 0x7ffe29254910 /* 24 vars */) = 0 
brk(NULL) = 0x5596e5a3c000 
...
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) 
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 
...
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 p\0\0\0\0\0\0"..., \ 832) = 832 
...
通过 strace ls,我们要求 strace 捕获 ls 使用的系统调用。请注意,我编辑了输出,因为在我的系统上 strace 生成大约 162 行(这个数字因不同发行版、架构和其他因素而异)。此外,你看到的输出是通过 stderr,如果要重定向它,你需要在此处使用 2>。在第三章你将更多地了解这些。
系统调用 execve 执行 /usr/bin/ls,导致 shell 进程被替换。
brk 系统调用是分配内存的过时方式;更安全和更可移植的做法是使用 malloc。请注意,malloc 并非系统调用,而是一个函数,它又使用 mallocopt 来决定是使用 brk 系统调用还是基于内存访问量使用 mmap 系统调用。
access 系统调用检查进程是否被允许访问某个特定文件。
系统调用 openat 打开文件 /etc/ld.so.cache 相对于目录文件描述符(这里第一个参数 AT_FDCWD,表示当前目录),并使用标志 O_RDONLY|O_CLOEXEC(最后一个参数)。
read 系统调用从文件描述符(第一个参数 3)读取 832 字节(最后一个参数)到一个缓冲区(第二个参数)。
strace 很有用,可以准确查看已调用的系统调用——包括顺序和参数——有效地连接到用户空间和内核之间的实时事件流。它也适用于性能诊断。让我们看看 curl 命令大部分时间花在哪里(输出已缩短):
$ strace -c \ 
curl -s https://mhausenblas.info > /dev/null 
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
26.75 0.031965 148 215 mmap
17.52 0.020935 136 153 3 read
10.15 0.012124 175 69 rt_sigaction
8.00 0.009561 147 65 1 openat
7.61 0.009098 126 72 close
...
0.00 0.000000 0 1 prlimit64
------ ----------- ----------- --------- --------- ----------------
100.00 0.119476 141 843 11 total
使用 -c 选项生成使用的系统调用的概览统计数据。
丢弃 curl 的所有输出。
有趣的是,curl 命令在这里几乎一半的时间用于 mmap 和 read 系统调用,而 connect 系统调用耗时 0.3 毫秒——还不错。
为了帮助你对覆盖范围有所感觉,我已经整理了表 2-1,其中列出了内核组件和系统级别广泛使用的系统调用示例。你可以通过man 手册第二章节查找系统调用的详细信息,包括它们的参数和返回值。
表 2-1. 示例系统调用
| 类别 | 示例系统调用 |
|---|---|
| 进程管理 | clone, fork, execve, wait, exit, getpid, setuid, setns, getrusage, capset, ptrace |
| 内存管理 | brk, mmap, munmap, mremap, mlock, mincore |
| 网络 | socket, setsockopt, getsockopt, bind, listen, accept, connect, shutdown, recvfrom, recvmsg, sendto, sethostname, bpf |
| 文件系统 | open, openat, close, mknod, rename, truncate, mkdir, rmdir, getcwd, chdir, chroot, getdents, link, symlink, unlink, umask, stat, chmod, utime, access, ioctl, flock, read, write, lseek, sync, select, poll, mount, |
| 时间 | time, clock_settime, timer_create, alarm, nanosleep |
| 信号 | kill, pause, signalfd, eventfd, |
| 全局 | uname, sysinfo, syslog, acct, _sysctl, iopl, reboot |
提示
有一个很好的互动系统调用表在线提供源代码参考。
现在你对 Linux 内核、其主要组件和接口有了基本的了解,让我们继续探讨如何扩展它的问题。
内核扩展
在本节中,我们将重点讨论如何扩展内核。在某种意义上,这里的内容是高级和可选的。一般情况下,你不需要它来进行日常工作。
注意
配置和编译自己的 Linux 内核超出了本书的范围。关于如何做到这一点的信息,我推荐Linux Kernel in a Nutshell(O’Reilly)由主要的 Linux 维护者和项目负责人 Greg Kroah-Hartman 撰写。他涵盖了从下载源代码到配置和安装步骤,再到运行时内核选项的整个任务范围。
让我们从一些简单的东西开始:你如何知道你正在使用哪个内核版本?你可以使用以下命令来确定这一点:
$ uname -srm
Linux 5.11.0-25-generic x86_64 
从这里的uname输出可以看出,我在写作时正在使用5.11 内核在一个x86_64机器上(另见“x86 架构”)。
现在我们知道内核版本,我们可以解决如何扩展内核的问题,即如何在不必添加功能到内核源代码然后构建它的情况下扩展内核。对于这种扩展,我们可以使用模块,让我们来看看。
模块
简而言之,模块是一个可以按需加载到内核中的程序。也就是说,你不一定需要重新编译内核和/或重新启动机器。如今,Linux 自动检测大多数硬件,并且 Linux 自动加载其模块。但也有一些情况下你想手动加载一个模块。考虑以下情况:内核检测到一个视频卡并加载一个通用模块。然而,视频卡制造商提供了一个更好的第三方模块(不在 Linux 内核中可用),你可能选择使用它。
要列出可用的模块,请运行以下命令(输出已经被编辑,因为在我的系统上有一千多行):
$ find /lib/modules/$(uname -r) -type f -name '*.ko*'
/lib/modules/5.11.0-25-generic/kernel/ubuntu/ubuntu-host/ubuntu-host.ko
/lib/modules/5.11.0-25-generic/kernel/fs/nls/nls_iso8859-1.ko
/lib/modules/5.11.0-25-generic/kernel/fs/ceph/ceph.ko
/lib/modules/5.11.0-25-generic/kernel/fs/nfsd/nfsd.ko
...
/lib/modules/5.11.0-25-generic/kernel/net/ipv6/esp6.ko
/lib/modules/5.11.0-25-generic/kernel/net/ipv6/ip6_vti.ko
/lib/modules/5.11.0-25-generic/kernel/net/sctp/sctp_diag.ko
/lib/modules/5.11.0-25-generic/kernel/net/sctp/sctp.ko
/lib/modules/5.11.0-25-generic/kernel/net/netrom/netrom.ko
太棒了!但内核实际加载了哪些模块?让我们来看一下(输出已经缩短):
$ lsmod
Module Size Used by
...
linear 20480 0
crct10dif_pclmul 16384 1
crc32_pclmul 16384 0
ghash_clmulni_intel 16384 0
virtio_net 57344 0
net_failover 20480 1 virtio_net
ahci 40960 0
aesni_intel 372736 0
crypto_simd 16384 1 aesni_intel
cryptd 24576 2 crypto_simd,ghash_clmulni_intel
glue_helper 16384 1 aesni_intel
注意,前述信息可以通过 /proc/modules 获得。这要归功于内核通过伪文件系统接口公开这些信息;更多关于这个主题的信息见 第六章。
想了解某个模块或有一种方便的方式来操作内核模块?那么 modprobe 就是你的好朋友。例如,列出依赖项:
$ modprobe --show-depends async_memcpy
insmod /lib/modules/5.11.0-25-generic/kernel/crypto/async_tx/async_tx.ko
insmod /lib/modules/5.11.0-25-generic/kernel/crypto/async_tx/async_memcpy.ko
接下来:一种替代的、现代化的扩展内核方式。
现代化扩展内核的一种方式:eBPF
扩展内核功能的一种越来越流行的方式是 eBPF。最初被称为 Berkeley Packet Filter (BPF),如今这个内核项目和技术通常被称为 eBPF(这个术语并不代表任何东西)。
从技术角度来说,eBPF 是 Linux 内核的一个特性,您需要 Linux 内核版本为 3.15 或更高版本才能从中受益。它通过使用 bpf 系统调用安全有效地扩展 Linux 内核功能。eBPF 作为一个内核虚拟机实现,使用自定义的 64 位 RISC 指令集。
小贴士
如果您想了解更多关于 eBPF 在哪个内核版本中启用的信息,可以查阅 iovisor/bcc GitHub 文档。
在 BPF Performance Tools: Linux System and Application Observability (Addison Wesley) 书中,从 Brendan Gregg 的高层概述中看到的图 2-5 概述。

图 2-5. Linux 内核中的 eBPF 概述
eBPF 已经在许多地方和用例中使用,例如以下情况:
作为 Kubernetes 中启用 Pod 网络的 CNI 插件
例如,在 Cilium 和 Project Calico 中。此外,用于服务可伸缩性。
用于可观测性
用于 Linux 内核跟踪,例如与 iovisor/bpftrace 以及在集群设置中的应用 Hubble (见 第八章)。
作为安全控制
例如,用于与项目如 CNCF Falco 一起执行容器运行时扫描。
用于网络负载均衡
例如,在 Facebook 的 L4 katran 库中。
2021 年中期,Linux 基金会宣布 Facebook、Google、Isovalent、Microsoft 和 Netflix 联合成立 eBPF Foundation,为 eBPF 项目提供了一个供应商中立的家园。敬请关注!
如果你想保持最新,可以看看ebpf.io。
结论
Linux 内核是 Linux 操作系统的核心,无论您在哪个发行版或环境中使用 Linux——无论是在桌面还是云端——您都应该对其组件和功能有一个基本的了解。
在本章中,我们回顾了整体 Linux 架构、内核的角色及其接口。最重要的是,内核抽象化了硬件的差异——CPU 架构和外设,并使 Linux 非常可移植。最重要的接口是系统调用接口,通过这个接口,内核暴露其功能,无论是打开文件、分配内存还是列出网络接口。
我们还稍微了解了内核的内部工作原理,包括模块和 eBPF。如果您想扩展内核功能或在内核中实现高性能任务(从用户空间控制),那么 eBPF 绝对值得更深入地研究。
如果您想了解内核的某些方面,请参考以下资源,这些资源应该为您提供一些起点:
通用
-
Linux 编程接口 由 Michael Kerrisk(No Starch Press)撰写。
-
Linux 内核教学 提供了一个全面深入的介绍。
-
“Linux 内核解剖” 提供了一个快速的高层介绍。
-
“操作系统内核” 提供了内核设计方法的概述和比较。
-
KernelNewbies 是一个很好的资源,如果您想深入了解实践性话题。
-
kernelstats 显示了随时间变化的一些有趣的分布。
-
Linux 内核地图 是内核组件和依赖关系的视觉表示。
内存管理
设备驱动程序
-
Linux 设备驱动程序 由 Jonathan Corbet 撰写
系统调用
eBPF
掌握了这些知识后,我们现在准备在抽象梯级中上升一步,并移至本书中考虑的主要用户界面:shell,无论是手动使用还是通过脚本自动化。
第三章:终端和脚本
在本章中,我们将专注于通过终端与 Linux 交互,即通过提供命令行界面(CLI)的 shell。能够有效地使用 shell 来完成日常任务非常重要,因此我们在这里着重介绍了其可用性。
首先,我们将回顾一些术语,并对 shell 基础进行简要而清晰的介绍。然后我们会看一些现代、易用的 shell,比如 Fish shell。我们还会探讨 shell 中的配置和常见任务。接下来,我们会讨论如何使用终端复用器有效地在 CLI 上工作,从而使你能够处理多个会话,无论是本地还是远程。在本章的最后部分,我们会转变视角,专注于如何使用脚本自动化 shell 中的任务,包括安全、可靠和可移植的脚本编写最佳实践,以及如何对脚本进行 lint 和测试。
从 CLI 的角度来看,与 Linux 交互有两种主要方式。第一种方式是手动操作,即人类用户坐在终端前,交互地输入命令并消耗输出。这种即兴的交互对于你在日常基础上想要在 shell 中完成的大部分事情都有效,包括以下内容:
-
列出目录,查找文件,或在文件中查找内容
-
在不同目录之间复制文件,或将文件复制到远程机器
-
从终端阅读电子邮件或新闻,或者从终端发送推文
此外,我们还将学习如何在同一时间方便高效地处理多个 shell 会话。
另一种操作模式是一系列命令的自动处理,这些命令包含在一种特殊的文件中,shell 会解释并执行这些命令。这种模式通常称为shell 脚本或简称脚本。通常情况下,你应该使用脚本而不是手动重复某些任务。此外,脚本是许多配置和安装系统的基础。脚本确实非常方便。但是,如果不加预防地使用,它们也可能带来危险。因此,每当你考虑编写脚本时,请记住XKCD 网络漫画第 3-1 图。

图 3-1. XKCD 关于自动化。授权:Randall Munroe(根据 CC BY-NC 2.5 许可共享)
我强烈建议你准备好一个 Linux 环境,并立即尝试这里展示的示例。有了这些,你准备好进行一些(互动)操作了吗?如果是这样,那么让我们从一些术语和基本 shell 使用开始。
基础
在我们深入研究不同选项和配置之前,让我们专注于一些基本术语,比如终端和shell。在本节中,我将定义这些术语,并展示如何在 shell 中完成日常任务。我们还将回顾现代命令并看到它们的实际应用。
终端
我们从终端开始,或者终端仿真器,或软终端,它们都指的是同一物事:终端 是提供文本用户界面的程序。也就是说,终端支持从键盘读取字符并在屏幕上显示它们。许多年前,这些是集成设备(键盘和屏幕在一起),但现在终端只是应用程序。
除了基本的面向字符的输入和输出之外,终端还支持所谓的 转义序列 或 转义代码,用于光标和屏幕处理以及可能的颜色支持。例如,按下 Ctrl+H 会导致退格,删除光标左侧的字符。
环境变量 TERM 指定正在使用的终端仿真器,并且其配置通过 infocmp 可以如下查看(注意输出已经被缩短):
$ infocmp 
# Reconstructed via infocmp from file: /lib/terminfo/s/screen-256color screen-256color|GNU Screen with 256 colors,
am, km, mir, msgr, xenl,
colors#0x100, cols#80, it#8, lines#24, pairs#0x10000,
acsc=++\,\,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
bel=^G, blink=\E5m, bold=\E[1m, cbt=\E[Z, civis=\E[?25l,
clear=\E[H\E[J, cnorm=\E[34h\E[?25h, cr=\r,
...
[
infocmp 的输出不容易理解。如果你想详细了解能力,请查阅 terminfo 数据库。例如,在我的具体输出中,终端支持 80 列 (cols#80) 和 24 行 (lines#24) 的输出以及 256 色 (colors#0x100,十六进制表示法)。
终端的例子不仅包括 xterm,rxvt 和 Gnome 终止器,还包括利用 GPU 的新一代终端,例如 Alacritty,kitty 和 warp。
在 “终端复用器” 中,我们将再次回到终端这个主题。
Shells
接下来是 shell,它是一个在终端内运行并作为命令解释器的程序。shell 通过流处理输入和输出,支持变量,具有一些内置命令可供使用,处理命令执行和状态,并通常支持交互式使用以及脚本化使用(“脚本化”)。
shell 的正式定义在sh中已明确定义,并且我们经常遇到术语POSIX shell,在脚本和可移植性的背景下,这将变得更为重要。
最初,我们有 Bourne shell sh,以作者命名,但现在通常用 bash shell 取而代之——这是对原始版本的一个双关语,缩写为 “Bourne Again Shell”,被广泛用作默认。
如果你对自己使用的内容感兴趣,请使用 file -h /bin/sh 命令查找,或者如果失败了,请尝试 echo $0 或 echo $SHELL。
注意
在本节中,我们默认使用 bash shell (bash),除非我们明确指出。
还有许多 sh 的实现以及其他变种,例如 Korn shell ksh 和 C shell csh,但今天并不广泛使用。然而,我们将在 “用户友好的 shell” 中审查现代的 bash 替代方案。
让我们从两个基本特性开始我们的 Shell 基础:流和变量。
流
让我们从输入(流)和输出(流)或简称 I/O 的主题开始。如何为程序提供输入?如何控制程序的输出去向,比如在终端或文件中?
首先,Shell 为每个进程配备了三个默认的文件描述符(FD),用于输入和输出:
-
stdin(FD 0) -
stdout(FD 1) -
stderr(FD 2)
如图 Figure 3-2 所示,这些 FD 默认连接到您的屏幕和键盘。换句话说,除非指定其他方式,否则在 Shell 中输入的命令将从键盘输入(stdin),并将输出(stdout)发送到屏幕。
下面的 Shell 交互演示了这种默认行为:
$ cat
This is some input I type on the keyboard and read on the screen^C
在前面使用cat的示例中,您可以看到默认行为。请注意,我使用 Ctrl+C(显示为^C)来终止命令。

图 3-2. Shell I/O 默认流
如果不想使用 Shell 默认设置——例如,不希望stderr输出到屏幕而想将其保存到文件中——可以将流重定向。
使用$FD>和<$FD重定向进程的输出流,其中$FD是文件描述符——例如,2>表示重定向stderr流。请注意,1>和>是一样的,因为stdout是默认的。如果想要同时重定向stdout和stderr,可以使用&>;如果想要丢弃某个流,可以使用/dev/null。
让我们通过具体例子来看看它是如何运行的,通过curl下载一些 HTML 内容:
$ curl https://example.com &> /dev/null 
$ curl https://example.com > /tmp/content.txt 2> /tmp/curl-status 
$ head -3 /tmp/content.txt
<!doctype html>
<html>
<head>
$ cat /tmp/curl-status
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1256 100 1256 0 0 3187 0 --:--:-- --:--:-- --:--:-- 3195
$ cat > /tmp/interactive-input.txt 
$ tr < /tmp/curl-status [A-Z] [a-z] 
% total % received % xferd average speed time time time current
dload upload total spent left speed
100 1256 100 1256 0 0 3187 0 --:--:-- --:--:-- --:--:-- 3195
通过将stdout和stderr都重定向到/dev/null来丢弃所有输出。
将输出和状态重定向到不同的文件。
交互地输入并保存到文件;使用 Ctrl+D 停止捕获并存储内容。
使用从stdin读取的tr命令将所有单词转换为小写。
Shell 通常理解一些特殊字符,例如:
和号(&)
放置在命令末尾,以后台方式执行命令(参见“作业控制”)
反斜杠(\)
用于将命令继续写在下一行,提高长命令的可读性
管道(|)
将一个进程的stdout连接到下一个进程的stdin,允许您传递数据而无需将其存储在临时文件中
再次,让我们看看理论内容如何实际操作。让我们尝试通过使用curl下载 HTML 文件并将内容通过wc工具进行管道传输,来计算 HTML 文件包含多少行:
$ curl https://example.com 2> /dev/null | \ 
wc -l 
46
使用curl下载 URL 的内容,并丢弃它在stderr上输出的状态。(注意:实际上,你会使用curl的-s选项,但我们想要学习如何应用我们辛苦获得的知识,对吧?)
curl的stdout被馈送到wc的stdin,使用-l选项计算行数。
现在你已经基本了解了命令、流和重定向,让我们继续学习另一个核心 shell 特性,即变量的处理。
变量
在 shell 的上下文中,你会经常遇到变量这个术语。每当你不想或不能硬编码一个值时,可以使用变量来存储和更改值。使用案例包括以下内容:
-
当你想要处理 Linux 公开的配置项时——例如,shell 寻找可执行文件所在的位置,捕获在
$PATH变量中。这是一种接口,其中变量可能是读/写的。 -
当你想要交互地查询用户的一个值,比如在脚本的上下文中。
-
当你想通过一次定义长值来缩短输入,比如 HTTP API 的 URL。这个用例大致对应于程序语言中的
const值,因为定义变量后不再改变值。
我们区分两种变量类型:
环境变量
Shell-wide 设置;用env列出它们。
Shell 变量
在当前执行上下文中有效;在 bash 中使用set列出。shell 变量不会被子进程继承。
在 bash 中,你可以使用export创建一个环境变量。当你想要访问一个变量的值时,在它前面加上$,当你想要摆脱它时,使用unset。
好的,刚才我们学到了很多信息。让我们看看在实践中的样子(在 bash 中):
$ set MY_VAR=42 
$ set | grep MY_VAR 
_=MY_VAR=42
$ export MY_GLOBAL_VAR="fun with vars" 
$ set | grep 'MY_*' 
MY_GLOBAL_VAR='fun with vars'
_=MY_VAR=42
$ env | grep 'MY_*' 
MY_GLOBAL_VAR=fun with vars
$ bash 
$ echo $MY_GLOBAL_VAR 
fun with vars
$ set | grep 'MY_*' 
MY_GLOBAL_VAR='fun with vars'
$ exit 
$ unset $MY_VAR
$ set | grep 'MY_*'
MY_GLOBAL_VAR='fun with vars'
创建一个名为MY_VAR的 shell 变量,并赋值为 42。
列出 shell 变量并过滤掉MY_VAR。注意_=,表示它未导出。
创建一个名为MY_GLOBAL_VAR的新环境变量。
列出 shell 变量并过滤掉所有以MY_开头的变量。正如预期的那样,我们看到了前面步骤中创建的两个变量。
列出环境变量。我们看到MY_GLOBAL_VAR,正如我们希望的那样。
创建一个新的 shell 会话——也就是当前 shell 会话的子进程,它不会继承MY_VAR。
访问环境变量MY_GLOBAL_VAR。
列出 shell 变量,这里我们只得到了MY_GLOBAL_VAR,因为我们在子进程中。
退出子进程,移除MY_VAR shell 变量,并列出我们的 shell 变量。正如预期的那样,MY_VAR 已经不存在。
在表 3-1 中,我整理了常见的 shell 和环境变量。你几乎在任何地方都会找到这些变量,它们非常重要,需要理解和使用。对于任何变量,你可以通过echo $XXX来查看相应的值,其中XXX是变量名。
表 3-1. 常见的 shell 和环境变量
| 变量 | 类型 | 语义 |
|---|---|---|
EDITOR |
环境 | 默认用于编辑文件的程序路径 |
HOME |
POSIX | 当前用户的主目录路径 |
HOSTNAME |
bash shell | 当前主机名 |
IFS |
POSIX | 用于分隔字段的字符列表;当 shell 扩展时用到 |
PATH |
POSIX | 包含一组目录,shell 在其中寻找可执行程序(二进制文件或脚本) |
PS1 |
环境 | 当前使用的主提示字符串 |
PWD |
环境 | 当前工作目录的完整路径 |
OLDPWD |
bash shell | 上一次 cd 命令前的完整路径 |
RANDOM |
bash shell | 0 到 32767 之间的随机整数 |
SHELL |
环境 | 当前使用的 shell |
TERM |
环境 | 使用的终端模拟器 |
UID |
环境 | 当前用户唯一 ID(整数值) |
USER |
环境 | 当前用户名 |
_ |
bash shell | 在前台执行的上一个命令的最后一个参数 |
? |
bash shell | 退出状态;参见“Exit status” |
| ` | 变量 | 类型 |
| --- | --- | --- |
EDITOR |
环境 | 默认用于编辑文件的程序路径 |
HOME |
POSIX | 当前用户的主目录路径 |
HOSTNAME |
bash shell | 当前主机名 |
IFS |
POSIX | 用于分隔字段的字符列表;当 shell 扩展时用到 |
PATH |
POSIX | 包含一组目录,shell 在其中寻找可执行程序(二进制文件或脚本) |
PS1 |
环境 | 当前使用的主提示字符串 |
PWD |
环境 | 当前工作目录的完整路径 |
OLDPWD |
bash shell | 上一次 cd 命令前的完整路径 |
RANDOM |
bash shell | 0 到 32767 之间的随机整数 |
SHELL |
环境 | 当前使用的 shell |
TERM |
环境 | 使用的终端模拟器 |
UID |
环境 | 当前用户唯一 ID(整数值) |
USER |
环境 | 当前用户名 |
_ |
bash shell | 在前台执行的上一个命令的最后一个参数 |
? |
bash shell | 退出状态;参见“Exit status” |
| bash shell | 当前进程的 ID(整数值) | |
0 |
bash shell | 当前进程的名称 |
此外,请查看完整的bash 特定变量列表,同时注意表 3-1 中的变量在“Scripting”环境下再次会派上用场。
退出状态
shell 使用所谓的退出状态向调用者通知命令执行的完成情况。一般来说,Linux 命令在终止时会返回一个状态。这可以是正常终止(成功路径)或异常终止(出现错误)。0 退出状态表示命令成功运行且无任何错误,而在 1 到 255 之间的非零值则表示失败。要查询退出状态,请使用echo $?。
在管道中要小心处理退出状态,因为某些 shell 只会将最后一个状态可用。你可以通过使用$PIPESTATUS来解决这个限制(https://oreil.ly/mMz9k)。
内建命令
Shell 提供了许多内置命令。一些有用的例子包括yes、echo、cat或read(取决于 Linux 发行版,其中一些命令可能不是内置的,而是位于/usr/bin)。您可以使用help命令列出内置命令。但请记住,其他所有程序都是 Shell 外部程序,通常可以在/usr/bin(用户命令)或/usr/sbin(管理命令)找到。
您如何知道在哪里找到可执行文件?以下是一些方法:
$ which ls
/usr/bin/ls
$ type ls
ls is aliased to `ls --color=auto'
注意
本书的一位技术审阅者正确指出,which是一个非 POSIX 的外部程序,可能并不总是可用。此外,他们建议使用*command* -v而不是which来获取程序路径或 Shell 别名/函数。另请参阅shellcheck 文档获取更多详细信息。
作业控制
大多数 Shell 支持的一个特性称为作业控制。默认情况下,当您输入一个命令时,它会接管屏幕和键盘,我们通常称之为前台运行。但是如果您不想交互式地运行某些内容,或者在服务器上,如果stdin没有输入怎么办呢?这时候就要用到作业控制和后台作业了:要在后台启动一个进程,可以在命令末尾加上&,或者将前台进程发送到后台,按 Ctrl+Z。
下面的示例展示了这一操作,让您对此有一个大致的了解:
$ watch -n 5 "ls" & 
$ jobs 
Job Group CPU State Command
1 3021 0% stopped watch -n 5 "ls" &
$ fg 
Every 5.0s: ls Sat Aug 28 11:34:32 2021
Dockerfile
app.yaml
example.json
main.go
script.sh
test
通过在命令末尾加上&,我们将命令在后台启动。
列出所有作业。
使用fg命令,我们可以将一个进程带回前台。如果您想退出watch命令,请使用 Ctrl+C。
如果您希望保持后台进程在关闭 Shell 后继续运行,可以在命令前加上nohup。此外,对于已经运行且未使用nohup前置的进程,您可以事后使用disown来实现相同效果。最后,如果您想终止运行中的进程,可以使用带有不同强度级别的kill命令(详见“信号”获取更多详细信息)。
与其使用作业控制,我建议使用终端复用器,如“终端复用器”中讨论的那样。这些程序处理了大多数常见用例(Shell 关闭、多个进程运行需要协调等),同时还支持与远程系统的工作。
让我们继续讨论替换历史悠久的核心命令的现代替代品。
现代命令
每天你会发现自己反复使用几个命令。这些包括导航目录的命令(cd)、列出目录内容的命令(ls)、查找文件的命令(find)和显示文件内容的命令(cat、less)。考虑到你经常使用这些命令,你希望尽可能高效——每次按键都很重要。
现代版本的某些常用命令存在变体。其中一些是可替换的,而其他一些则扩展了功能。所有这些命令都提供了一些合理的默认值用于常见操作,并且提供了易于理解的丰富输出,通常能够让你在完成相同任务时输入更少的内容。这减少了在使用 Shell 时的摩擦,使工作更加愉快并改善了流程。如果你想了解更多关于现代工具的信息,请查看附录 B。在这个背景下,特别是如果你在企业环境中应用这些知识,需要注意:我对这些工具没有任何利益关系,纯粹是因为我自己发现它们有用而推荐它们。安装和使用任何这些工具的一个好方法是使用经你选择的 Linux 发行版验证过的工具版本。
使用 exa 列出目录内容
每当你想知道一个目录包含什么,你会使用 ls 或其带参数的变体之一。例如,在 bash 中我曾经将 l 别名为 ls -GAhltr。但是有更好的方法:exa,一个用 Rust 编写的现代替代品,具有内置的 Git 支持和树形渲染。在这种情况下,你认为在列出目录内容后最常用的命令是什么?在我的经验中,是清屏,人们经常使用 clear。这是输入五个字符然后按 ENTER。你可以更快地达到同样的效果——简单地使用 Ctrl+L。
使用 bat 查看文件内容
假设你列出了一个目录的内容并找到了一个你想检查的文件。你会使用 cat,也许?我建议你看看更好的东西:bat。bat 命令在 图 3-3 中显示了语法高亮,显示不可打印字符,支持 Git,并且有一个集成的分页器(用于分页查看比屏幕显示更长的文件)。
使用 rg 在文件中查找内容
传统上,你会使用 grep 在文件中查找内容。然而,有一个现代命令 rg,它速度快且功能强大。
在这个例子中,我们将 rg 与 find 和 grep 的组合进行比较,我们希望找到包含字符串“sample”的 YAML 文件:
$ find . -type f -name "*.yaml" -exec grep "sample" '{}' \; -print 
app: sample
app: sample
./app.yaml
$ rg -t "yaml" sample 
app.yaml
9: app: sample
14: app: sample
使用 find 和 grep 结合在 YAML 文件中查找字符串。
使用 rg 完成同样的任务。
如果你比较前面示例中的命令和结果,你会发现 rg 不仅更容易使用,而且结果更具信息量(在本例中提供上下文,即行号)。

图 3-3. 通过 bat 渲染的 Go 文件(上)和 YAML 文件(下)的显示
使用 jq 进行 JSON 数据处理
现在来看一个额外的命令。这个命令 jq 并不是一个实际的替代品,而更像是 JSON 的专用工具,这种流行的文本数据格式。你会在 HTTP API 和配置文件中找到 JSON。
因此,使用 jq 而不是 awk 或 sed 来提取特定的值。例如,通过使用 JSON 生成器 来生成一些随机数据,我得到了一个 2.4 kB 的 JSON 文件 example.json,大致如下(这里只显示第一条记录):
[
{
"_id": "612297a64a057a3fa3a56fcf",
"latitude": -25.750679,
"longitude": 130.044327,
"friends": [
{
"id": 0,
"name": "Tara Holland"
},
{
"id": 1,
"name": "Giles Glover"
},
{
"id": 2,
"name": "Pennington Shannon"
}
],
"favoriteFruit": "strawberry"
},
...
假设我们对所有“第一个”朋友感兴趣——也就是说,在 friends 数组中的第 0 个条目——这些朋友最喜欢的水果是“草莓”。使用 jq,你会执行以下操作:
$ jq 'select(.[].favoriteFruit=="strawberry") | .[].friends[0].name' example.json
"Tara Holland"
"Christy Mullins"
"Snider Thornton"
"Jana Clay"
"Wilma King"
CLI 玩得开心吧?如果你对现代命令主题和替代选择感兴趣,请查看 modern-unix 仓库,其中列出了一些建议。现在我们把注意力转移到超出目录导航和文件内容查看的一些常见任务及其处理方法。
常见任务
经常你可能发现自己在做一些事情,并且有些技巧可以帮助你加快在 shell 中的任务。让我们回顾一下这些常见任务,并看看如何更高效地完成它们。
缩短经常使用的命令
在界面上的一个基本洞察是,你经常使用的命令应该耗费最少的精力——它们应该快速输入。现在把这个想法应用到 shell 中:而不是 git diff --color-moved,我只需输入 d(一个字符),因为我每天在我的仓库中查看变更几百次。根据不同的 shell,有不同的方法可以实现这一点:在 bash 中这称为 alias,而在 Fish (“Fish Shell”) 中则可以使用 缩写。
导航
当你在 shell 提示符中输入命令时,有几件事情你可能想做,比如导航行(例如,将光标移动到开头)或操作行(比如删除光标左边的所有内容)。表 3-2 列出了常见的 shell 快捷键。
表 3-2. Shell 导航和编辑快捷键
| 动作 | 命令 | 注释 |
|---|---|---|
| 移动光标到行首 | Ctrl+a | - |
| 移动光标到行尾 | Ctrl+e | - |
| 向前移动一个字符 | Ctrl+f | - |
| 向后移动一个字符 | Ctrl+b | - |
| 向前移动一个单词 | Alt+f | 只适用于左 Alt 键 |
| 向后移动一个单词 | Alt+b | - |
| 删除当前字符 | Ctrl+d | - |
| 删除光标左侧字符 | Ctrl+h | - |
| 删除光标左侧单词 | Ctrl+w | - |
| 删除光标右侧所有内容 | Ctrl+k | - |
| 删除光标左侧所有内容 | Ctrl+u | - |
| 清屏 | Ctrl+l | - |
| 取消命令 | Ctrl+c | - |
| 撤销 | Ctrl+_ | 仅限 bash |
| 搜索历史记录 | Ctrl+r | 一些 shell |
| 取消搜索 | Ctrl+g | 一些 shell |
请注意,并非所有快捷方式都在所有 shell 中受支持,并且某些操作(例如历史记录管理)可能在某些 shell 中实现方式有所不同。此外,您可能希望了解这些快捷键基于 Emacs 编辑按键。如果您喜欢vi,您可以在.bashrc文件中使用set -o vi,例如,以执行基于vi按键的命令行编辑。最后,从表 3-2 开始,尝试您的 shell 支持的功能,并查看如何配置以满足您的需求。
文件内容管理
您并非总是想启动编辑器(如vi)来添加一行文本。有时候您无法这样做——例如,在编写 shell 脚本的上下文中(参见“脚本编写”)。
那么,如何操作文本内容呢?让我们看几个例子:
$ echo "First line" > /tmp/something 
$ cat /tmp/something 
First line
$ echo "Second line" >> /tmp/something && \ 
cat /tmp/something
First line
Second line
$ sed 's/line/LINE/' /tmp/something 
First LINE
Second LINE
$ cat << 'EOF' > /tmp/another  First line
Second line
Third line
EOF
$ diff -y /tmp/something /tmp/another 
First line First line
Second line Second line
> Third line
通过重定向echo输出来创建文件。
查看文件内容。
使用>>运算符向文件追加一行,然后查看内容。
使用sed替换文件内容并输出到stdout。
使用此处文档创建文件。
显示我们创建的文件之间的差异。
现在您了解了基本的文件内容操作技巧,让我们来看一下高级文件内容查看。
查看长文件
对于长文件——即文件的行数超过 shell 可以在屏幕上显示的行数——您可以使用分页程序如less或bat(bat带有内置分页程序)。使用分页,程序将输出分为适合屏幕显示的页面,并提供一些命令来导航页面(查看下一页、上一页等)。
另一种处理长文件的方式是仅显示文件的选择区域,比如前几行。这有两个方便的命令:head和tail。
例如,要显示文件的开头:
$ for i in {1..100} ; do echo $i >> /tmp/longfile ; done 
$ head -5 /tmp/longfile 
1
2
3
4
5
创建一个长文件(这里有 100 行)。
显示长文件的前五行。
或者,要获取正在不断增长的文件的实时更新,我们可以使用:
$ sudo tail -f /var/log/Xorg.0.log 
[ 36065.898] (II) event14 - ALPS01:00 0911:5288 Mouse: device is a pointer
[ 36065.900] (II) event15 - ALPS01:00 0911:5288 Touchpad: device is a touchpad
[ 36065.901] (II) event4 - Intel HID events: is tagged by udev as: Keyboard
[ 36065.901] (II) event4 - Intel HID events: device is a keyboard
...
使用 tail 显示日志文件的末尾,使用 -f 选项表示跟随或自动更新。
最后,在这一节中我们将看看如何处理日期和时间。
日期和时间处理
date 命令是生成唯一文件名的有用方法。它允许您生成各种格式的日期,包括 Unix 时间戳,并且可以在不同的日期和时间格式之间转换。
$ date +%s 
1629582883
$ date -d @1629742883 '+%m/%d/%Y:%H:%M:%S' 
08/21/2021:21:54:43
创建一个 UNIX 时间戳。
将 UNIX 时间戳转换为人类可读的日期。
通过这些基础知识,您现在应该对终端和 shell 是什么,以及如何使用它们来执行基本任务,如导航文件系统、查找文件等有了很好的理解。现在我们继续讨论人性化的 shell 主题。
人性化的 Shell
虽然 bash shell 可能仍然是最广泛使用的 shell,但不一定是最人性化的。自上世纪 80 年代末问世以来,它的年龄有时体现出来。我强烈建议您评估并使用一些现代、用户友好的 shell 替代 bash。
我们将首先详细研究一个名为 Fish shell 的现代、用户友好的具体例子,然后简要讨论其他选择,以确保您了解各种选择。我们在 “Which Shell Should I Use?” 中快速给出推荐和结论。
Fish Shell
Fish shell 自称为智能且用户友好的命令行 shell。让我们先看一些基本用法,然后再讨论配置主题。
基本用法
对于许多日常任务,您在输入方面可能不会注意到与 bash 的显著区别;Table 3-2 中提供的大多数命令是有效的。但是,fish 与 bash 不同并且更加方便的地方有两个:
没有明确的历史管理。
您只需输入命令即可看到之前执行的命令历史。您可以使用上下键选择其中一个(参见 Figure 3-4)。
对许多命令都可以使用自动建议。
这在 Figure 3-5 中显示。此外,当您按 Tab 键时,Fish shell 将尝试完成命令、参数或路径,并为您提供视觉提示,例如如果不认识命令,会将您的输入着色为红色。

图 3-4. Fish 历史记录处理示例

图 3-5. Fish 自动建议的示例
Table 3-3 列出了一些常见的 fish 命令。在这个背景下,特别注意环境变量的处理方式。
表 3-3. Fish shell 参考
| 任务 | 命令 |
|---|---|
导出环境变量 KEY 的值为 VAL |
set -x KEY VAL |
删除环境变量KEY |
set -e KEY |
内联环境变量KEY用于命令cmd |
env KEY=VAL cmd |
| 将路径长度更改为 1 | set -g fish_prompt_pwd_dir_length 1 |
| 管理简写 | abbr |
| 管理函数 | functions和funcd |
与其他 Shell 不同,fish将上一个命令的退出状态存储在名为$status的变量中,而不是存储在$?中。
如果你从 bash 转来,你可能也想参考Fish 常见问题解答,这里解决了大部分常见问题。
配置
要配置 Fish Shell,你只需输入fish_config命令(根据你的发行版,可能需要添加browse子命令),Fish 将通过http://localhost:8000启动一个服务器,并自动用一个漂亮的 UI 在你默认的浏览器中打开,如图 3-6 所示,允许你查看和更改设置。

图 3-6. 通过浏览器配置 Fish Shell
小贴士
要在命令行导航中切换vi和 Emacs(默认)键绑定,请使用fish_vi_key_bindings启动vi模式,并使用fish_default_key_bindings将其重置为 Emacs。请注意,更改将立即在所有活动的 Shell 会话中生效。
现在让我们看看我如何配置我的环境。实际上,我的配置相当简短;在config.fish中我有以下内容:
set -x FZF_DEFAULT_OPTS "-m --bind='ctrl-o:execute(nvim {})+abort'"
set -x FZF_DEFAULT_COMMAND 'rg --files'
set -x EDITOR nvim
set -x KUBE_EDITOR nvim
set -ga fish_user_paths /usr/local/bin
我的提示符,在fish_prompt.fish中定义,看起来是这样的:
function fish_prompt
set -l retc red
test $status = 0; and set retc blue
set -q __fish_git_prompt_showupstream
or set -g __fish_git_prompt_showupstream auto
function _nim_prompt_wrapper
set retc $argv[1]
set field_name $argv[2]
set field_value $argv[3]
set_color normal
set_color $retc
echo -n '─'
set_color -o blue
echo -n '['
set_color normal
test -n $field_name
and echo -n $field_name:
set_color $retc
echo -n $field_value
set_color -o blue
echo -n ']'
end
set_color $retc
echo -n '┬─'
set_color -o blue
echo -n [
set_color normal
set_color c07933
echo -n (prompt_pwd)
set_color -o blue
echo -n ']'
# Virtual Environment
set -q VIRTUAL_ENV_DISABLE_PROMPT
or set -g VIRTUAL_ENV_DISABLE_PROMPT true
set -q VIRTUAL_ENV
and _nim_prompt_wrapper $retc V (basename "$VIRTUAL_ENV")
# git
set prompt_git (fish_git_prompt | string trim -c ' ()')
test -n "$prompt_git"
and _nim_prompt_wrapper $retc G $prompt_git
# New line
echo
# Background jobs
set_color normal
for job in (jobs)
set_color $retc
echo -n '│ '
set_color brown
echo $job
end
set_color blue
echo -n '╰─> '
set_color -o blue
echo -n '$ '
set_color normal
end
前面的提示符定义产生了图 3-7 中显示的提示符;请注意包含 Git 存储库和不包含 Git 存储库的目录之间的区别,这是加速你工作流的内置视觉提示。同时,注意右侧显示的当前时间。

图 3-7. Fish Shell 提示符
我的简写——把它们当作在其他 Shell 中找到的alias替代品——看起来是这样的:
$ abbr
abbr -a -U -- :q exit
abbr -a -U -- cat bat
abbr -a -U -- d 'git diff --color-moved'
abbr -a -U -- g git
abbr -a -U -- grep 'grep --color=auto'
abbr -a -U -- k kubectl
abbr -a -U -- l 'exa --long --all --git'
abbr -a -U -- ll 'ls -GAhltr'
abbr -a -U -- m make
abbr -a -U -- p 'git push'
abbr -a -U -- pu 'git pull'
abbr -a -U -- s 'git status'
abbr -a -U -- stat 'stat -x'
abbr -a -U -- vi nvim
abbr -a -U -- wget 'wget -c'
要添加新的简写,请使用abbr --add。简写对于不带参数的简单命令非常方便。如果你有更复杂的构造想要缩短怎么办?比如你想缩短一个涉及git并且带有参数的序列。那就来认识一下 Fish 中的函数吧。
现在让我们看一个示例函数,这个函数定义在名为c.fish的文件中。我们可以使用functions命令列出所有定义的函数,使用function命令创建一个新函数,而在这种情况下使用function c来编辑它如下:
function c
git add --all
git commit -m "$argv"
end
到此为止我们已经完成了 Fish 部分,我们通过一个使用教程和配置技巧来进行了详细讨论。现在让我们快速看看其他现代 Shell。
Z-shell
Z-shell,或zsh,是一种类似 Bourne Shell 的 Shell,具有强大的补全系统和丰富的主题支持。借助Oh My Zsh,你可以在保留广泛的 bash 向后兼容性的同时,配置和使用zsh,就像之前在fish上看到的那样。
zsh使用五个启动文件,如下例所示(注意,如果未设置$ZDOTDIR,zsh将使用$HOME代替):
$ZDOTDIR/.zshenv 
$ZDOTDIR/.zprofile 
$ZDOTDIR/.zshrc 
$ZDOTDIR/.zlogin 
$ZDOTDIR/.zlogout 
在 Shell 的所有调用中被引用。它应该包含设置搜索路径以及其他重要环境变量的命令。但不应包含产生输出或假设 Shell 连接到tty的命令。
作为ksh爱好者的.zlogin替代品(这两者不应同时使用);与.zlogin类似,但在.zshrc之前被引用。
在交互式 Shell 中被引用。它应该包含设置别名、函数、选项、键绑定等命令。
在登录 Shell 中被引用。它应该包含仅在登录 Shell 中执行的命令。请注意,.zlogin不适合别名定义、选项、环境变量设置等。
在登录 Shell 退出时被引用。
欲了解更多zsh插件,请参阅 GitHub 上的awesome-zsh-plugins repo。如果你想学习zsh,可以阅读 Paul Falstad 和 Bas de Bakker 的“An Introduction to the Z Shell”。
其他现代 Shell
除了fish和zsh外,还有许多其他有趣的 Shell 可供选择,但不一定始终与 bash 兼容。当你查看这些时,请考虑各自 Shell 的重点(交互使用 vs. 脚本编写)以及社区活跃程度。
我发现的一些适用于 Linux 的现代 Shell 示例,并推荐你查看:
面向 Python 和 JavaScript 用户。换句话说,更专注于脚本编写而非交互使用。
一个 POSIX Shell,具有集成的测试框架、类型化管道和事件驱动编程等有趣特性。
一种实验性的新 Shell 范例,展示了带有强大查询语言的表格输出。通过详细的Nu Book了解更多信息。
一个跨平台的 Shell,最初作为 Windows PowerShell 的分支,并提供与 POSIX Shell 不同的语义和交互方式。
还有很多其他选择。继续寻找并找到最适合你的方法。试着超越 bash 并优化你的使用场景。
我应该使用哪个 Shell?
此时,除了 bash 之外的现代 Shell 似乎都是一个不错的选择,从人性化的角度来看。流畅的自动完成,简单的配置和智能环境在 2022 年并不奢侈,在你通常花在命令行上的时间里,你应该尝试不同的 Shell,并选择你最喜欢的那个。我个人使用 Fish Shell,但我的许多同行都对 Z Shell 非常满意。
你可能会有一些问题,让你不愿意远离 bash,例如以下问题:
-
你在远程系统工作和/或无法安装自己的 Shell。
-
由于兼容性或肌肉记忆,你一直在使用 bash。摆脱某些习惯可能会很难。
-
几乎所有的指令(隐含地)假设 bash。例如,你会看到像
export FOO=BAR这样的指令,这是特定于 bash 的。
结果表明,这些问题在大多数用户那里基本上不成问题。虽然你可能不得不在远程系统中暂时使用 bash,但大多数情况下你将在你控制的环境中工作。学习曲线是存在的,但长期投资是值得的。
接下来,让我们集中精力讨论另一种提升终端生产力的方式:多路复用器。
终端多路复用器
我们在本章开头遇到了终端,在“终端”。现在让我们深入探讨如何改善你的终端使用,基于一个简单而强大的概念:多路复用。
这样想:通常你会在不同的事物上工作,可以将它们分组在一起。例如,你可能会在一个开源项目上工作,撰写博客文章或文档,远程访问服务器,与 HTTP API 交互进行测试等等。这些任务可能每个都需要一个或多个终端窗口,并且通常你希望或需要在两个窗口中同时进行可能相互依赖的任务。例如:
-
使用
watch命令定期执行目录列表,并同时编辑文件。 -
你启动一个服务器进程(Web 服务器或应用服务器),希望它在前台运行(也见“作业控制”)以便查看日志。
-
你想使用
vi编辑文件,同时使用git查询状态并提交更改。 -
你有一个在公共云中运行的虚拟机,想要通过
ssh登录它,并有可能在本地管理文件。
把所有这些例子看作是逻辑上属于一起的事情,时间跨度从短期(几分钟)到长期(几天或几周)。这些任务的分组通常称为会话。
现在,如果你想要实现这种分组,会有一些挑战:
-
您需要多个窗口,因此一种解决方案是启动多个终端,或者如果 UI 支持,多个实例(选项卡)。
-
即使您关闭终端或远程端关闭,也希望保留所有窗口和路径。
-
您希望扩展或缩放以专注于特定任务,同时保持所有会话的概览,并能够在它们之间导航。
为了实现这些任务,人们提出了在终端上叠加多个窗口(和会话,以分组窗口)的想法,换句话说,复用终端 I/O。
让我们简要了解终端复用的原始实现,称为 screen。然后我们将深入研究广泛使用的 tmux 实现,并总结此领域的其他选项。
screen
screen 是最初的终端复用工具,目前仍在使用。除非您在没有其他选择的远程环境中,或者无法安装其他复用工具,否则建议不要使用 screen。原因之一是它不再处于活跃维护状态,另一个原因是它不太灵活且缺乏许多现代终端复用工具的功能。
tmux
tmux 是一个灵活且功能丰富的终端复用工具,可以根据您的需求进行定制。正如您在图 3-8 中所见,tmux 有三个核心元素,您可以与之交互,从粗粒度到细粒度单元:

图 3-8. tmux 的元素:会话、窗口和 pane
会话
一个逻辑单元,您可以将其视为专用于特定任务(如“在项目 X 上工作”或“撰写博客文章 Y”)的工作环境。它是所有其他单元的容器。
窗口
您可以将窗口视为浏览器中的标签,属于一个会话。使用它是可选的,通常每个会话只有一个窗口。
Panes
这些是您的工作马,实际上是运行的单个 shell 实例。一个 pane 是一个窗口的一部分,您可以轻松地垂直或水平分割它,以及展开/折叠它(类似于缩放),并根据需要关闭 panes。
与 screen 类似,在 tmux 中您可以附加和分离会话。假设我们从头开始,让我们使用一个名为 test 的会话来启动它:
$ tmux new -s test
使用上述命令,tmux 作为服务器运行,您会进入一个您在 tmux 中配置的 shell,作为客户端运行。这种客户端/服务器模型允许您创建、进入、离开和销毁会话,并使用其中运行的 shell,而无需考虑它们运行(或失败)的进程。
tmux 使用 Ctrl+b 作为默认键盘快捷键,也称为前缀或触发器。例如,要列出所有窗口,您可以按 Ctrl+b 然后 w,或者要扩展当前(活动)窗格,您可以使用 Ctrl+b 然后 z。
小贴士
在 tmux 中,默认的触发键是 Ctrl+b。为了改善流程,我将触发键映射到一个未使用的键上,因此只需按一次按键即可。我是通过首先将触发键映射到 tmux 中的 Home 键,然后通过更改 /usr/share/X11/xkb/symbols/pc 中的映射将 Home 键映射到 Caps Lock 键来实现这一点的。
这种双映射是我需要做的一种变通方法。根据您的目标键或终端,您可能不必这样做,但我建议您将 Ctrl+b 键映射到一个易于到达的未使用键,因为您每天会频繁按下它。
您现在可以使用 表格 3-4 中列出的任何命令来管理进一步的会话、窗口和窗格。此外,按下 Ctrl+b+d 键,您可以分离会话。这有效地将 tmux 放入后台。
当您启动新的终端实例或者从远程位置通过 ssh 到您的机器时,您可以附加到一个已有的会话,所以让我们使用之前创建的 test 会话来做这件事:
$ tmux attach -t test 
附加到名为 test 的现有会话。请注意,如果您想要从之前的终端分离会话,还需要提供 -d 参数。
表格 3-4 列出了按单元分组的常见 tmux 命令,从最广泛的范围(会话)到最窄的范围(窗格)。
表格 3-4. tmux 参考
| 目标 | 任务 | 命令 |
|---|---|---|
| 会话 | 创建新的 | :new -s NAME |
| 会话 | 重命名 | 触发键 + ` |
| --- | --- | --- |
| 会话 | 创建新的 | :new -s NAME |
| 会话 | 列出全部 | 触发键 + s |
| 会话 | 关闭 | 触发键 |
| 窗口 | 创建新的 | 触发键 + c |
| 窗口 | 重命名 | 触发键 + , |
| 窗口 | 切换到 | 触发键 + 1 … 9 |
| 窗口 | 列出全部 | 触发键 + w |
| 窗口 | 关闭 | 触发键 + & |
| 窗格 | 水平分割 | 触发键 + " |
| 窗格 | 垂直分割 | 触发键 + % |
| 窗格 | 切换 | 触发键 + z |
| 窗格 | 关闭 | 触发键 + x |
现在您已经基本了解如何使用 tmux,让我们转向配置和自定义。我的 .tmux.conf 如下所示:
unbind C-b 
set -g prefix Home
bind Home send-prefix
bind r source-file ~/.tmux.conf \; display "tmux config reloaded :)" 
bind \\ split-window -h -c "#{pane_current_path}" 
bind - split-window -v -c "#{pane_current_path}"
bind X confirm-before kill-session 
set -s escape-time 1 
set-option -g mouse on 
set -g default-terminal "screen-256color" 
set-option -g status-position top 
set -g status-bg colour103
set -g status-fg colour215
set -g status-right-length 120
set -g status-left-length 50
set -g window-status-style fg=colour215
set -g pane-active-border-style fg=colour215
set -g @plugin 'tmux-plugins/tmux-resurrect' 
set -g @plugin 'tmux-plugins/tmux-continuum'
set -g @continuum-restore 'on'
run '~/.tmux/plugins/tpm/tpm'
此行及接下来两行将触发键更改为 Home。
通过触发键 + r 重新加载配置。
此行及以下重新定义了窗格分割;保留现有窗格的当前目录。
为新会话和终止会话添加快捷方式。
无延迟。
启用鼠标选择。
将默认终端模式设置为 256 色模式。
主题设置(下面六行)。
从这里到末尾:插件管理。
首先安装 tpm,tmux 插件管理器,然后按下触发键 + I 进行插件管理。这里使用的插件如下:
允许您使用 Ctrl+s(安全)和 Ctrl+r(恢复)来恢复会话。
自动保存/恢复会话(每 15 分钟间隔)。
图 3-9 显示了我的 Alacritty 终端正在运行 tmux。您可以看到位于左上角的快捷方式 0 到 9 的会话。

图 3-9. tmux 实例示例,显示可用会话。
虽然 tmux 确实是一个极好的选择,但确实还有其他选项,所以让我们来看看吧。
其他复用器
您可以查看并尝试的其他终端复用器包括以下几种:
元工具,允许您管理 tmux 会话。
是 screen 或 tmux 的包装器;如果您使用基于 Ubuntu 或 Debian 的 Linux 发行版,它尤其有趣。
自称为终端工作区,用 Rust 编写,并且超越了 tmux 的功能,包括布局引擎和强大的插件系统。
将平铺窗口管理概念引入终端;功能强大但像 tmux 一样有学习曲线。
一个简单的用 Go 编写的终端复用器;易于使用但不像 tmux 那样强大。
通过对复用器选项进行快速回顾后,让我们来谈谈如何选择一个。
我应该使用哪个复用器?
不像面向人类用户的 shell,我在终端复用器的背景下确实有一个明确的偏好:使用 tmux。理由多种多样:它成熟稳定、功能丰富(有许多可用插件)、灵活。很多人都在使用它,所以有很多相关资料和可用的帮助。其他复用器也很令人兴奋,但相对较新或者像 screen 一样已经不再是首选了。
希望能说服您考虑使用终端复用器来改善终端和 shell 的体验,加快任务速度,使整体流程更加顺畅。
现在,让我们转向本章的最后一个主题,使用 shell 脚本自动化任务。
脚本编写
在本章的前几节中,我们专注于 shell 的手动、交互式使用。一旦您在提示符上反复执行某项任务,很可能是时候自动化该任务了。这就是脚本的用武之地。
这里我们关注在 bash 中编写脚本。这是由于两个原因:
-
大多数脚本都是用 bash 编写的,因此您会找到很多 bash 脚本的示例和帮助。
-
在目标系统上找到 bash 的可能性很高,这使得你的潜在用户群比使用其他(可能更强大但是又神秘或不常用)的 bash 替代品要大。
在我们开始之前,我想提供一些背景信息,有些 shell 脚本的代码量可以达到几千行。并不是鼓励你去追求这样的量级 —— 相反,如果你发现自己在编写长脚本,可以考虑是否应该选择像 Python 或 Ruby 这样的适当脚本语言。
现在让我们退后一步,开发一个简短但有用的示例,一路上应用良好的实践。我们假设我们想要自动化显示一个单一语句在屏幕上的任务,给定一个用户的 GitHub 句柄,显示用户加入的时间,使用他们的全名,类似以下的内容:
XXXX XXXXX joined GitHub in YYYY
我们如何用脚本自动化这个任务呢?让我们从基础开始,然后审视可移植性,并逐步提升到脚本的“业务逻辑”。
脚本基础
好消息是,通过交互式使用 shell,你已经掌握了大部分相关术语和技术。除了变量、流和重定向以及常见命令之外,在脚本的上下文中还有一些特定的事物,让我们来复习一下。
高级数据类型
虽然 shell 通常将一切视为字符串(如果你想执行一些更复杂的数值任务,可能不应该使用 shell 脚本),但它们支持一些高级数据类型如数组。
现在让我们看看数组的实际运用:
os=('Linux' 'macOS' 'Windows') 
echo "${os[0]}" 
numberofos="${#os[@]}" 
定义一个包含三个元素的数组。
访问第一个元素;这将打印 Linux。
获取数组长度,结果为 numberofos 为 3。
流程控制
流程控制允许你在脚本中进行分支(if)或重复(for 和 while),使执行依赖于特定条件。
流程控制的一些用法示例:
for afile in /tmp/* ; do 
echo "$afile"
done
for i in {1..10}; do 
echo "$i"
done
while true; do
...
done 
基本循环遍历目录,打印每个文件名
范围循环
无限循环;使用 Ctrl+C 退出。
函数
函数允许你编写更模块化和可重复使用的脚本。在使用之前必须先定义函数,因为 shell 会逐行解析脚本。
简单函数示例:
sayhi() { 
echo "Hi $1 hope you are well!"
}
sayhi "Michael" 
函数定义;参数通过 $n 隐式传递。
调用函数;输出为 “Hi Michael hope you are well!”
高级 I/O
使用read可以从stdin中读取用户输入,你可以用它来获取运行时输入,例如使用选项菜单。此外,与使用echo相比,考虑使用printf,它允许你对输出进行精细的控制,包括颜色。与echo相比,printf更具可移植性。
以下是高级 I/O 使用示例:
read name 
printf "Hello %s" "$name" 
从用户输入读取值。
在上一步中读取输出值。
对于你来说还有其他更高级的概念,比如信号和陷阱。鉴于我们只想在这里提供脚本主题的概述和介绍,我会推荐你参考优秀的bash 脚本速查表,以获取所有相关结构的全面参考。如果你真的想要写 shell 脚本,我建议你阅读卡尔·阿尔宾、JP·沃森和卡梅伦·纽汉的bash Cookbook,其中包含了很多优秀的片段,可以作为起点使用。
编写可移植的 bash 脚本
现在我们来看看在 bash 中编写可移植脚本意味着什么。但是等等。可移植意味着什么,为什么你应该关心呢?
在“Shells”的开头,我们定义了POSIX的含义,所以让我们在此基础上继续。当我说“可移植”时,我的意思是我们对脚本将被执行的环境不会有太多假设——无论是隐含地还是明确地。如果一个脚本是可移植的,它可以在许多不同的系统(shell、Linux 发行版等)上运行。
但请记住,即使你将 shell 的类型固定在 bash 上,不是所有功能在不同版本的 shell 中都能以相同的方式工作。归根结底,这取决于你可以在多少不同的环境中测试你的脚本。
执行可移植脚本
脚本是如何执行的?首先,我们要说明脚本实际上只是文本文件;扩展名并不重要,尽管通常你会发现使用.sh作为约定。但有两件事可以将一个文本文件变成一个可执行的脚本,并能够由 shell 运行:
-
文本文件需要在第一行声明解释器,使用所谓的shebang(或hashbang),写为
#!(另请参见下面模板的第一行)。 -
然后,你需要使用例如
chmod +x使脚本可执行,这允许任何人运行它,或者更好的是chmod 750,这更符合最小权限原则,因为它只允许与脚本关联的用户和组来运行它。我们将在第四章深入探讨这个主题。
现在你已经了解了基础知识,让我们来看一个具体的模板,作为起点使用。
一个骨架模板
一个可移植 bash shell 脚本的骨架模板如下所示:
#!/usr/bin/env bash 
set -o errexit 
set -o nounset 
set -o pipefail 
firstargument="${1:-somedefaultvalue}" 
echo "$firstargument"
哈希注释告诉程序加载器我们要使用bash来解释此脚本。
定义我们希望在发生错误时停止脚本执行。
将未设置的变量视为错误(以便脚本不会悄无声息地失败)。
定义管道的一部分失败时整个管道应被视为失败。这有助于避免悄无声息的失败。
带有默认值的命令行参数示例。
我们将在本节后面使用此模板来实现我们的 GitHub 信息脚本。
良好的实践
我使用良好的实践而不是最佳实践,因为你应该根据情况和你想要达到的目标来做决定。一个你为自己编写的脚本和一个你提供给成千上万用户的脚本之间有区别,但总体来说,编写脚本的高级良好实践如下:
快速失败和大声失败
避免悄无声息的失败,并快速失败;像errexit和pipefail可以帮助你做到这一点。由于 bash 默认倾向于悄无声息地失败,快速失败几乎总是一个好主意。
敏感信息
不要在脚本中硬编码任何敏感信息,如密码。此类信息应在运行时通过用户输入或调用 API 提供。同时,考虑到 ps 命令可以泄漏程序参数和更多信息,这也是敏感信息泄漏的另一种方式。
输入的净化
在可能的情况下设置和提供合理的默认变量,并清理用户或其他来源接收到的输入。例如,通过 read 命令提供的启动参数或交互式摄取,避免因未设置变量而导致类似于 rm -rf "$PROJECTHOME/"* 这样看似无害的命令删除你的驱动器。
检查依赖项
不要假设某个特定工具或命令是可用的,除非它是内置的或你了解你的目标环境。仅仅因为你的机器安装了 curl 并不意味着目标机器也有。如果可能的话,提供备用方案——例如,如果没有 curl 可用,使用 wget。
错误处理
当你的脚本失败时(这不是一个“如果”而是“何时”和“何处”的问题),为用户提供可操作的指导。例如,而不是错误 123,说清楚发生了什么问题以及用户如何解决,比如试图写入 /project/xyz/ 但似乎对我来说这是只读的。
文档
内联文档你的脚本(使用 # 这里是一些文档)主要块,并尽量保持 80 列宽以提高可读性和差异化。
版本控制
考虑使用 Git 对你的脚本进行版本控制。
测试
对脚本进行检查和测试。由于这是如此重要的实践,我们将在下一节详细讨论它。
现在让我们在开发和测试时通过检查脚本的语法来使脚本更加安全,然后再分发它们。
语法检查和测试脚本
在开发过程中,您希望检查和检查您的脚本,确保正确使用命令和指令。有一个很好的方式来做到这一点,在图 3-10 中有所描绘,使用ShellCheck程序;您可以在本地下载和安装它,或者也可以通过shellcheck.net的在线版本来使用它。此外,考虑使用shfmt格式化您的脚本。它可以自动修复稍后由shellcheck报告的问题。

图 3-10。在线 ShellCheck 工具的屏幕截图
此外,在将脚本提交到存储库之前,请考虑使用bats进行测试。bats,即 Bash 自动化测试系统,允许您定义测试文件作为具有特殊语法的 bash 脚本,每个测试用例都是一个带有描述的 bash 函数。通常将这些脚本作为 GitHub 操作的一部分调用。
现在我们将运用脚本编写、语法检查和测试的良好实践。让我们实现我们在本节开头指定的示例脚本。
端到端示例:GitHub 用户信息脚本
在此端到端示例中,我们将前面的所有提示和工具汇总在一起,实现我们的示例脚本,该脚本应该接受 GitHub 用户句柄并打印出包含用户加入年份及其全名的消息。
这是一个实现的示例,考虑了良好的实践。将以下内容存储在名为gh-user-info.sh的文件中,并使其可执行:
#!/usr/bin/env bash
set -o errexit
set -o errtrace
set -o nounset
set -o pipefail
### Command line parameter: targetuser="${1:-mhausenblas}" 
### Check if our dependencies are met: if ! [ -x "$(command -v jq)" ]
then
echo "jq is not installed" >&2
exit 1
fi
### Main: githubapi="https://api.github.com/users/"
tmpuserdump="/tmp/ghuserdump_$targetuser.json"
result=$(curl -s $githubapi$targetuser) 
echo $result > $tmpuserdump
name=$(jq .name $tmpuserdump -r) 
created_at=$(jq .created_at $tmpuserdump -r)
joinyear=$(echo $created_at | cut -f1 -d"-") 
echo $name joined GitHub in $joinyear 
如果用户未提供值,请提供一个默认值。
使用curl访问GitHub API以下载用户信息的 JSON 文件,并将其存储在临时文件中(下一行)。
使用jq提取我们需要的字段。请注意,created_at字段的值看起来类似于"2009-02-07T16:07:32Z"。
使用cut从 JSON 文件的created_at字段中提取年份。
组装输出消息并打印到屏幕。
现在让我们使用默认设置运行它:
$ ./gh-user-info.sh
Michael Hausenblas joined GitHub in 2009
恭喜,现在您可以随时使用 shell 进行交互和脚本编写。在我们结束之前,花点时间考虑我们的gh-user-info.sh脚本:
-
如果 GitHub API 返回的 JSON 数据块不合法怎么办?如果我们遇到 500 HTTP 错误怎么办?也许添加类似于“稍后再试”的消息会更有用,如果用户自己无法做任何事情的话。
-
要使脚本工作,你需要网络访问,否则
curl调用将失败。关于缺乏网络访问,你可以做些什么?向用户通知并建议他们检查网络可能是一个选择。 -
思考如何改进依赖检查——例如,我们在这里暗示
curl已安装。你可以添加一个检查,使二进制变量并回退到wget吗? -
如何添加一些使用帮助?如果脚本带有
-h或--help参数调用,或许显示一个具体的使用示例和用户可以使用的选项来影响执行(最好包括定义使用的默认值)。
现在你可以看到,尽管这个脚本看起来不错,在大多数情况下可以工作,但总是有一些地方可以改进,比如使脚本更加健壮,并提供可操作的错误消息。在这种情况下,考虑使用诸如 bashing,rerun,或者 rr 等框架来提升模块化。
结论
在本章中,我们专注于在终端中使用 Linux,这是一个文本用户界面。我们讨论了 shell 的术语,提供了使用 shell 基础的实际介绍,并审查了常见任务及如何使用某些命令的现代变体来提高 shell 生产力(例如使用 exa 而不是 ls)。
接着,我们看了现代、用户友好的 shell,特别是 fish,以及如何配置和使用它们。此外,我们通过使用 tmux 作为实际示例来介绍了终端复用器,使你能够同时处理多个本地或远程会话。使用现代 shell 和复用器可以极大提高你在命令行上的效率,我强烈建议你考虑采用它们。
最后,我们讨论了通过编写安全和可移植的 shell 脚本自动化任务,包括对这些脚本进行检查和测试。请记住,shell 实际上是命令解释器,和任何语言一样,你需要实践才能流利。说到这一点,现在你已经掌握了使用 Linux 命令行的基础,你已经可以在大多数基于 Linux 的系统上工作,无论是嵌入式系统还是云虚拟机。无论如何,你都会找到方法获取一个终端,并通过交互或执行脚本发出命令。
如果你想深入研究本章讨论的主题,这里有一些额外的资源:
终端
Shell
终端复用器
Shell 脚本
有了 Shell 基础,我们现在转向 Linux 中的访问控制和执行。
第四章:访问控制
在前一章关于所有与 shell 和脚本相关的广泛范围之后,我们现在专注于 Linux 中一个特定且至关重要的安全方面。在本章中,我们讨论了用户及其对资源(特别是文件)的访问控制的主题。
在这样一个多用户设置中,一个立即想到的问题是所有权。例如,用户可能拥有一个文件。他们被允许从文件中读取、向文件中写入,还可以删除它。考虑到系统上还有其他用户,那么这些用户被允许做什么,这是如何定义和强制执行的呢?也有一些活动可能并不是您首先会与文件相关联的。例如,用户可能(或者可能不会)被允许更改与网络相关的设置。
为了掌握这个主题,我们首先将从访问的角度看一下用户、进程和文件之间的基本关系。我们还将回顾沙盒和访问控制类型。接下来,我们将专注于 Linux 用户的定义,用户可以做什么,以及如何在本地或者从中心位置管理用户。
接下来,我们将进入权限的主题,我们将看看如何控制对文件的访问以及这种限制如何影响进程。
我们将结束本章,涵盖访问控制领域一系列高级 Linux 功能,包括能力、seccomp 配置文件和 ACL。为了完善内容,我们将提供一些关于权限和访问控制的安全良好实践。
有了这些基础,让我们直接进入用户和资源所有权的主题,为本章的其余部分打下基础。
基础知识
在我们深入访问控制机制之前,让我们退后一步,从鸟瞰视角来看一下这个主题。这将帮助我们建立一些术语,并澄清主要概念之间的关系。
资源和所有权
Linux 是一个多用户操作系统,因此从 UNIX 继承了用户(见“Users”)的概念。每个用户帐户都与一个用户 ID 相关联,可以访问可执行文件、文件、设备和其他 Linux 资产。人类用户可以使用用户帐户登录,进程可以作为用户帐户运行。然后,有资源(我们简单地称之为文件),这些资源可以是用户可用的任何硬件或软件组件。在一般情况下,我们将资源称为文件,除非我们明确讨论访问其他类型资源的情况,比如系统调用。在图 4-1 和随后的段落中,您可以看到 Linux 中用户、进程和文件之间的高级关系。

图 4-1. Linux 中的用户、进程和文件
用户
启动进程并拥有文件。进程是内核加载到主存储器并运行的程序(可执行文件)。
文件
有所有者;默认情况下,创建文件的用户拥有它。
进程
使用文件进行通信和持久性。当然,用户间接地也会使用文件,但他们需要通过进程来进行。
这种用户、进程和文件之间关系的描述当然是一个非常简化的视角,但它使我们能够理解这些参与者及其关系,并且在我们详细讨论这些不同角色之间的互动时将会派上用场。
首先让我们看一下一个进程的执行上下文,解答进程的受限程度问题。在谈论资源访问时,我们经常会遇到一个术语,即沙盒化。
沙盒化
沙盒化是一个模糊定义的术语,可以指一系列不同的方法,从监狱到容器再到虚拟机,这些方法可以在内核或用户空间中管理。通常在沙盒中会运行一些应用程序,监管机制会在沙盒化的进程和托管环境之间实施一定程度的隔离。如果这些听起来相当理论化,我请求您稍微耐心等待。我们将在本章后面看到沙盒化的实际操作,在“seccomp Profiles”中,以及在第九章中再次讨论虚拟机和容器时在 ch09.xhtml#advanced 中。
在您的脑海中对资源、所有权和对资源的访问有了基本理解后,让我们简要讨论一些概念上的访问控制方法。
访问控制类型
访问控制的一个方面是访问本身的性质。用户或进程是否直接访问资源,可能是无限制的方式?或者可能有一组明确的规则,规定进程在什么情况下可以访问什么样的资源(文件或系统调用)。或者访问本身甚至被记录。
在概念上,有不同的访问控制类型。在 Linux 环境中,最重要和相关的两种是自主访问控制和强制访问控制:
自主访问控制
使用自主访问控制(DAC),其思想是基于用户身份限制对资源的访问。在某种程度上,这是自主的,即某些权限的用户可以将其传递给其他用户。
强制访问控制
强制访问控制基于表示安全级别的层次模型。用户被分配一个许可级别,资源被分配一个安全标签。用户只能访问其自身许可级别等于或低于的资源。在强制访问控制模型中,管理员严格和独占地控制访问,设置所有权限。换句话说,即使拥有资源的用户也不能自行设置权限。
此外,Linux 传统上采取全有或全无的态度,即您要么是超级用户具有更改所有内容的权限,要么是普通用户权限受限。最初,并没有一种简单灵活的方法来为用户或进程分配某些特权。例如,在一般情况下,要使“进程 X 被允许更改网络设置”,您必须给予其root访问权限。这自然会对遭受攻击的系统产生具体影响:攻击者可以轻易滥用这些广泛的特权。
注意
要稍微解释一下 Linux 中的“全有或全无态度”:在大多数 Linux 系统中,默认情况下允许“其他人”(即系统上的所有用户)对几乎所有文件和可执行文件进行读取访问。例如,启用 SELinux 后,强制访问控制仅限于明确授予权限的资产。因此,例如,Web 服务器只能使用 80 和 443 端口,只能共享特定目录中的文件和脚本,只能将日志写入特定位置,依此类推。
我们将在“高级权限管理”中重新讨论这个主题,并看看现代 Linux 功能如何帮助克服这种二元世界观,从而实现更精细的权限管理。
Linux 中可能最著名的强制访问控制实现是SELinux。它是为了满足政府机构的高安全要求而开发的,并且通常用于这些环境,因为其严格的规则使得可用性受到影响。另一个 Linux 内核自版本 2.6.36 起包含的强制访问控制选项,在 Ubuntu 系列 Linux 发行版中相当流行,是AppArmor。
现在让我们转移到 Linux 中用户及其管理的话题上。
用户
在 Linux 中,我们通常从用途或预期使用的角度区分两种类型的用户账户:
所谓的系统用户或系统账户
典型地,程序(有时被称为守护进程)使用这些类型的账户来运行后台进程。这些程序提供的服务可以是操作系统的一部分,比如网络服务(例如,sshd),或者是应用层的服务(例如,流行的关系型数据库 mysql)。
普通用户
例如,通过 shell 与 Linux 交互使用的人类用户。
系统用户和普通用户之间的区别在技术上不那么明显,更多地是一种组织结构。要理解这一点,我们首先要介绍 Linux 中的用户 ID(UID)的概念,这是由 Linux 管理的一个 32 位数值。
Linux 通过 UID 标识用户,用户属于一个或多个组,由组 ID(GID)标识。有一种特殊的用户,UID 为 0,通常称为root。这位“超级用户”可以做任何事情,没有任何限制。通常情况下,你应该避免以root用户身份工作,因为拥有太大的权限可能会轻易毁坏系统(相信我,我曾经这么做过)。我们稍后会在本章节中再次讨论这个问题。
不同的 Linux 发行版有各自的方式来决定如何管理 UID 范围。例如,由systemd支持的发行版(参见systemd)在这里简化描述有以下约定:
UID 0
是root
UID 1 到 999
保留给系统用户使用
UID 65534
用户nobody是用来映射远程用户到一些已知 ID 的例子,比如“网络文件系统”
UID 1000 到 65533 和 65536 到 4294967294
普通用户
要查找自己的 UID,可以使用(惊喜!)id命令,如下所示:
$ id -u
2016796723
现在你已经了解了 Linux 用户的基础知识,让我们看看如何管理用户。
本地用户管理
第一个选项,也是传统上唯一可用的选项,是本地管理用户。也就是说,仅使用机器本地的信息,用户相关信息不会在多台机器之间共享。
对于本地用户管理,Linux 使用一个简单的基于文件的接口,其中命名方案有些混乱,这是一个历史遗留问题,我们不得不接受。表 4-1 列出了四个文件,共同实现用户管理。
表 4-1. 本地用户管理文件参考
| 目的 | 文件 |
|---|---|
| 用户数据库 | /etc/passwd |
| 组数据库 | /etc/group |
| 用户密码 | /etc/shadow |
| 组密码 | /etc/gshadow |
把 /etc/passwd 看作是一种迷你用户数据库,用来跟踪用户名称、UID、组成员资格和其他数据,例如常规用户使用的主目录和登录 shell。我们来看一个具体的例子:
$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash 
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin 
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
mh9:x:1000:1001::/home/mh9:/usr/bin/fish 
root 用户的 UID 为 0。
系统账户(nologin表明其用途;后文会详细讨论)。
我的用户账户。
让我们仔细看一下 /etc/passwd 中的一行,以详细了解用户条目的结构:
root:x:0:0:root:/root:/bin/bash
^ ^ ^ ^ ^ ^ ^
| | | | | | └── 
| | | | | └── 
| | | | └── 
| | | └── 
| | └── 
| └── 
└── 
要使用的登录 shell。为了阻止交互式登录,可以使用 /sbin/nologin。
用户的主目录,默认为 /。
用户信息,如全名或联系电话等。通常也被称为GECOS字段。请注意,不使用 GECOS 格式化,而是通常用于与帐户关联的人的全名。
用户的主要组(GID);另请参阅/etc/group。
UID。请注意,Linux 为系统使用保留了 1000 以下的 UID。
用户的密码,使用x字符意味着(加密的)密码存储在/etc/shadow中,默认情况下是这样的。
用户名,必须是 32 个字符或更少。
我们注意到/etc/passwd中缺少的一件事情是我们基于其名称期望在那里找到的事情:密码。出于历史原因,密码存储在一个称为/etc/shadow的文件中。虽然每个用户都可以读取/etc/passwd,但通常需要root权限才能读取/etc/shadow。
要添加用户,您可以使用adduser命令如下:
$ sudo adduser mh9
Adding user `mh9' ...
Adding new group `mh9' (1001) ...
Adding new user `mh9' (1000) with group `mh9' ...
Creating home directory `/home/mh9' ...  Copying files from `/etc/skel' ... 
New password: 
Retype new password:
passwd: password updated successfully
Changing the user information for mh9
Enter the new value, or press ENTER for the default 
Full Name []: Michael Hausenblas
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y/n] Y
adduser命令会创建一个主目录。
它还会复制一堆默认的配置文件到主目录。
需要定义一个密码。
提供可选的 GECOS 信息。
如果您想创建一个系统账户,请使用-r选项。这将禁用使用登录 shell,并且避免创建家目录。有关配置详细信息,还可以参考/etc/adduser.conf,包括用于 UID/GID 范围的选项。
除了用户外,Linux 还有组的概念,从某种意义上说,它只是一个或多个用户的集合。任何常规用户都属于一个默认组,但可以成为其他组的成员。您可以通过/etc/group文件了解有关组和映射的信息:
$ cat /etc/group 
root:x:0:
daemon:x:1:
bin:x:2:
sys:x:3:
adm:x:4:syslog
...
ssh:x:114:
landscape:x:115:
admin:x:116:
netdev:x:117:
lxd:x:118:
systemd-coredump:x:999:
mh9:x:1001: 
显示组映射文件的内容。
一个我的用户组示例,其 GID 为 1001。请注意,您可以在最后一个冒号后添加逗号分隔的用户名列表,以允许多个用户具有该组权限。
现在我们已经掌握了这个基本的用户概念和管理方法,我们可以进一步探讨一种在专业设置中更好地管理用户的可能方式,从而支持扩展。
集中化用户管理
如果您有多台或多个服务器需要管理用户——比如在专业设置中——那么快速在本地管理用户很快就会变得老旧。您希望有一种集中式的方式来管理用户,可以将其应用到特定的机器上。根据您的需求和(时间)预算,有几种可供选择的方法:
基于目录
轻量目录访问协议 (LDAP),一套几十年历史的协议套件,现在由 IETF 正式规范化,定义了如何通过 Internet Protocol (IP)访问和维护分布式目录。您可以自行运行 LDAP 服务器,例如使用像Keycloak这样的项目,或者将其外包给云提供商,如 Azure Active Directory。
通过网络
用户可以使用 Kerberos 以此方式进行身份验证。我们将在“Kerberos”中详细讨论 Kerberos。
使用配置管理系统
这些系统,包括 Ansible、Chef、Puppet 或 SaltStack,可用于在多台机器上一致地创建用户。
实际实现通常受环境限制。也就是说,公司可能已经在使用 LDAP,因此选择可能有限。然而,不同方法的细节及其利弊超出了本书的范围。
权限
在本节中,我们首先详细介绍 Linux 文件权限,这对访问控制的工作方式至关重要,然后再看看关于进程权限的权限。也就是说,我们将审查运行时权限以及它们是如何从文件权限中派生的。
文件权限
文件权限对于 Linux 中资源访问的概念至关重要,因为在 Linux 中,几乎所有东西都可以视为文件。让我们首先回顾一些术语,然后详细讨论围绕文件访问和权限的元数据表示。
有三种类型或范围的权限,从狭窄到宽:
用户
文件的所有者
组
具有一个或多个成员
其他
用于其他所有人的类别
此外,有三种类型的访问方式:
读取(r)
对于普通文件,这允许用户查看文件内容。对于目录,这允许用户查看目录中文件的名称。
写入(w)
对于普通文件,这允许用户修改和删除文件。对于目录,这允许用户在目录中创建、重命名和删除文件。
执行(x)
对于普通文件,这允许用户在具有读取权限的情况下执行文件。对于目录,这允许用户访问目录中的文件信息,有效地允许他们切换到它(cd)或列出其内容(ls)。
让我们看看文件权限的实际操作(请注意,这里ls命令输出中的空格已经展开以提高可读性):
$ ls -al
total 0
-rw-r--r-- 1 mh9 devs 9 Apr 12 11:42 test
^ ^ ^ ^ ^ ^ ^
| | | | | | └── 
| | | | | └── 
| | | | └── 
| | | └── 
| | └── 
| └── 
└── 
文件名
最后修改时间戳
文件大小(以字节为单位)
文件所属的组
文件所有者
硬链接的数量
文件模式
当深入查看文件模式时——即文件类型和权限,如前面片段中所称的,我们可以了解到各字段的具体含义:
. rwx rwx rwx
^ ^ ^ ^
| | | └── 
| | └── 
| └── 
└── 
其他用户的权限
组的权限
文件所有者的权限
文件类型(参见表格 4-2)
文件模式中的第一个字段表示文件类型;详细信息请参见表格 4-2。文件模式的其余部分编码了针对各种目标(从所有者到每个人)设置的权限,如表格 4-3 中所列。
表格 4-2. 文件模式中使用的文件类型
| 符号 | 语义 |
|---|---|
- |
普通文件(例如执行touch abc时) |
b |
块特殊文件 |
c |
字符特殊文件 |
C |
高性能(连续数据)文件 |
d |
目录 |
l |
符号链接 |
p |
命名管道(使用mkfifo创建) |
s |
套接字 |
? |
其他(未知)文件类型 |
一些其他(旧的或过时的)字符,如M或P,在位置0上使用,您大部分可以忽略。如果您对它们的含义感兴趣,请运行info ls -n "What information is listed"。
这些文件模式中的权限结合起来,定义了对目标集合的每个元素(用户、组、其他所有人)所允许的操作,如表格 4-3 中所示,通过access进行检查和执行。
表格 4-3. 文件权限
| 模式 | 生效权限 | 十进制表示 |
|---|---|---|
--- |
无 | 0 |
--x |
执行 | 1 |
-w- |
写 | 2 |
-wx |
写和执行 | 3 |
r-- |
只读 | 4 |
r-x |
读和执行 | 5 |
rw- |
读和写 | 6 |
rwx |
读、写、执行 | 7 |
让我们看几个示例:
755
对其所有者完全访问;其他所有人仅读和执行
700
对其所有者完全访问;其他所有人无权限
664
对所有者和组读写访问;其他人只读
644
对所有者读写;其他人只读
400
只有所有者可读
664在我的系统上具有特殊含义。当我创建文件时,它被分配为默认权限。您可以通过umask命令来检查,我的情况下是0002。
setuid权限用于告诉系统以所有者身份运行可执行文件,并使用所有者的权限。如果文件由root所有,可能会引起问题。
您可以使用chmod更改文件的权限。您可以显式指定所需的权限设置(如644),也可以使用快捷方式(例如,+x使其可执行)。但实际上是什么样子呢?
让我们使用chmod使文件可执行:
$ ls -al /tmp/masktest
-rw-r--r-- 1 mh9 dev 0 Aug 28 13:07 /tmp/masktest 
$ chmod +x /tmp/masktest 
$ ls -al /tmp/masktest
-rwxr-xr-x 1 mh9 dev 0 Aug 28 13:07 /tmp/masktest 
最初,文件权限对于所有者是r/w,对于其他人只读,即644。
使文件可执行。
现在,文件权限对于所有者是r/w/x,对于其他人是r/x,即755。
在图 4-2 中,您可以看到底层发生了什么。请注意,您可能不希望每个人都有执行文件的权利,因此在这里使用chmod 744可能更好,仅为所有者提供正确的权限,而不会更改其余部分。我们将在“良好实践”中进一步讨论这个话题。

图 4-2。使文件可执行以及随之更改的文件权限
你也可以使用chown(和chgrp用于组)更改所有权:
$ touch myfile
$ ls -al myfile
-rw-rw-r-- 1 mh9 mh9 0 Sep 4 09:26 myfile 
$ sudo chown root myfile 
-rw-rw-r-- 1 root mh9 0 Sep 4 09:26 myfile
我创建并拥有的文件myfile。
经过chown后,root拥有该文件。
在讨论了基本的权限管理之后,让我们看看在这个领域中的一些更高级的技术。
进程权限
到目前为止,我们关注的是人类用户如何访问文件以及所涉及的权限。现在我们将重点转向进程。在“资源和所有权”中,我们谈到了用户如何拥有文件以及进程如何使用文件。这引发了一个问题:从进程角度来看,相关的权限是什么?
如credentials(7)手册页中所述,在运行时权限的上下文中存在不同的用户 ID。
实际 UID
实际UID 是启动进程的用户的 UID。它表示人类用户在进程所有权方面的所有权。进程本身可以通过getuid(2)获取其实际 UID,并且您可以通过 shell 使用stat -c "%u %g" /proc/$pid/查询它。
有效 UID
Linux 内核使用有效UID 来确定进程在访问共享资源(如消息队列)时的权限。在传统的 UNIX 系统中,它们也用于文件访问。但是 Linux 以前使用专用的文件系统 UID(参见下面的讨论)来控制文件访问权限,出于兼容性考虑仍然支持。进程可以通过geteuid(2)获取其有效 UID。
保存的设置用户 ID
在suid情况下,保存的设置用户 ID 用于允许进程通过在实际 UID 和保存的设置用户 ID 之间切换其有效 UID 来获取特权。例如,为了允许进程使用某些网络端口(请参阅“端口”),它需要以root身份运行。进程可以通过getresuid(2)获取其保存的设置用户 ID。
文件系统 UID
这些特定于 Linux 的 ID 用于确定文件访问的权限。最初引入这一 UID 是为了支持文件服务器代表普通用户执行操作,同时将该进程与该用户发出的信号隔离。程序通常不直接操作该 UID。内核跟踪有效 UID 何时更改,并自动将文件系统 UID 与之同步。这意味着通常文件系统 UID 与有效 UID 相同,但可以通过setfsuid(2)更改。请注意,从技术上讲,自内核 v2.0 起不再需要此 UID,但出于兼容性考虑仍受支持。
最初,通过fork(2)创建子进程时,它会继承父进程的 UID 副本;而在execve(2)系统调用期间,进程的真实 UID 会保留,但有效 UID 和已保存的设置用户 ID 可能会发生变化。
例如,当您运行passwd命令时,您的有效 UID 是您的 UID,假设为 1000。现在,passwd启用了suid设置,这意味着当您运行它时,您的有效 UID 是 0(即root)。还有其他影响有效 UID 的方式,例如使用chroot和其他沙箱技术。
注意
POSIX 线程要求所有线程共享凭证。但在内核级别,Linux 为每个线程维护单独的用户和组凭证。
除了文件访问权限外,内核还使用进程 UID 来执行其他任务,包括但不限于以下内容:
-
建立信号发送权限——例如确定当您对某个进程 ID 执行
kill -9时会发生什么。我们将在第六章回到这个问题。 -
用于调度和优先级的权限处理(例如
nice)。 -
在容器环境下,我们将详细讨论资源限制检查,参见第九章。
在处理suid时,理解有效 UID 可能会很直接,但一旦涉及到能力(capabilities),情况可能会更复杂。
高级权限管理
尽管我们迄今专注于广泛使用的机制,但本节的主题在某种意义上属于高级内容,并不一定适合休闲或爱好设置。对于专业使用——即部署业务关键工作负载的生产用例——您至少应了解以下高级权限管理方法。
能力(Capabilities)
在 Linux 中,与传统的 UNIX 系统一样,当root用户运行进程时没有任何限制。换句话说,内核只区分两种情况:
-
特权进程,绕过内核权限检查,其有效 UID 为 0(即
root) -
对于非特权进程,其有效 UID 非零,在内核执行权限检查时,详见“进程权限”
自内核 v2.2 引入 capabilities syscall 以来,这种二元世界观已经改变:传统上与 root 相关联的特权现在被分解为可以独立分配到每个线程级别的不同单元。
在实际应用中,基本思想是普通进程没有任何权限,受到前面讨论的权限控制。你可以为可执行文件(二进制文件和 shell 脚本)以及进程分配权限,逐步增加执行任务所需的特权(详见“良好实践”中的讨论)。
现在需要提醒一下:特权通常只与系统级任务相关。换句话说,大多数情况下你不一定需要依赖它们。
在 Table 4-4 中,你可以看到一些更广泛使用的特权。
Table 4-4. 有用特权的示例
| 特权 | 语义 |
|---|---|
CAP_CHOWN |
允许用户对文件的 UID/GID 进行任意更改 |
CAP_KILL |
允许向属于其他用户的进程发送信号 |
CAP_SETUID |
允许更改 UID |
CAP_SETPCAP |
允许设置正在运行进程的特权 |
CAP_NET_ADMIN |
允许执行各种网络相关操作,例如接口配置 |
CAP_NET_RAW |
允许使用 RAW 和 PACKET sockets |
CAP_SYS_CHROOT |
允许调用 chroot |
CAP_SYS_ADMIN |
允许系统管理员操作,包括挂载文件系统 |
CAP_SYS_PTRACE |
允许使用 strace 调试进程 |
CAP_SYS_MODULE |
允许加载内核模块 |
现在让我们看看特权如何发挥作用。首先,要查看特权,可以使用如下命令(输出已编辑以适应):
$ capsh --print 
Current: =
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,
cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,
...
$ grep Cap /proc/$$/status 
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
系统上所有特权的概述
当前进程(shell)的特权
你可以通过 getcap 和 setcap 在文件级别上细粒度管理权限(本章节不涉及详细内容和良好实践)。
权限能帮助实现从全有或全无的方式向基于文件更精细的特权过渡。现在让我们转向另一个高级访问控制主题:seccomp 的沙箱技术。
seccomp 配置文件
安全计算模式(seccomp) 是自 2005 年以来可用的 Linux 内核特性。这种沙箱技术的基本思想是,通过一个专用的系统调用 seccomp(2),可以限制进程可以使用的系统调用。
虽然你可能觉得直接管理 seccomp 有些不便,但是有方法可以在不太费力的情况下使用它。例如,在容器环境中(参见“容器”),Docker 和 Kubernetes 都支持 seccomp。
现在让我们来看看传统的、精细的文件权限的扩展。
访问控制列表
使用访问控制列表(ACL),我们在 Linux 上有一个灵活的权限机制,可以在传统权限之上或作为补充使用,这些传统权限在“文件权限”中已经讨论过。ACL 解决了传统权限的一个缺点,即允许您为不在用户组列表中的用户或组授予权限。
要检查您的发行版是否支持 ACL,可以使用grep -i acl /boot/config*,希望在输出中的某处找到POSIX_ACL=Y以确认。为了在文件系统上使用 ACL,必须在挂载时启用acl选项。关于acl的文档参考有很多有用的细节。
我们不会在这里详细讨论 ACL,因为它们略微超出了本书的范围;然而,了解它们并知道从哪里开始可能会有好处,如果你在实际中遇到它们的话。
有了这些,让我们回顾一些访问控制的良好实践。
良好实践
在访问控制的更广泛背景中,这里有一些安全的“良好实践”。尽管其中一些可能更适用于专业环境,但每个人至少应该意识到它们。
最小权限
最小权限原则简言之,一个人或进程只应具有完成特定任务所需的必要权限。例如,如果一个应用程序不写入文件,则它只需要读取访问权限。在访问控制的上下文中,您可以通过两种方式实践最小权限:
-
在“文件权限”中,我们看到使用
chmod +x时会发生什么。除了您打算的权限外,它还为其他用户分配了一些额外的权限。使用数字模式的显式权限比符号模式更好。换句话说:虽然后者更方便,但不够严格。 -
尽量避免以 root 身份运行。例如,当您需要安装某些东西时,应该使用
sudo而不是以root登录。
注意,如果您正在编写应用程序,可以使用 SELinux 策略限制对仅选定的文件、目录和其他功能的访问。相比之下,默认的 Linux 模型可能会使应用程序能够访问系统上留下的任何打开文件。
避免使用 setuid
利用能力而不是依赖于setuid,后者像是一把大锤,为攻击者提供了接管系统的绝佳途径。
审计
审计是记录行为(以及谁执行了它们)的概念,以一种不可篡改的方式记录。然后,您可以使用这个只读日志来验证谁在什么时候做了什么。我们将在第八章中深入探讨这个主题。
结论
现在你了解了 Linux 如何管理用户、文件和资源访问,你拥有了一切必要的工具来安全、可靠地执行日常任务。
对于在 Linux 上进行任何实际工作,请记住用户、进程和文件之间的关系。这在 Linux 作为多用户操作系统的背景下至关重要,对于安全运行和避免损害都是关键的。
我们回顾了访问控制类型,定义了 Linux 中的用户及其能力,并讨论了如何在本地和中心化管理它们。文件权限及其管理可能有些棘手,但掌握它主要是一个实践问题。
在容器环境中,高级权限技术,包括能力和 seccomp 配置文件,非常相关。
在最后一节中,我们讨论了关于访问控制相关安全的良好实践,特别是应用最少特权。
如果你想深入了解本章讨论的主题,这里有一些资源:
概述
-
“访问控制策略调查” 由阿曼达·克劳尔编写
-
Lynis,一款审计和合规性测试工具
能力
seccomp
访问控制列表
-
“访问控制列表” 通过 ArchLinux
-
“Linux 访问控制列表(ACL)简介” 由 Red Hat 提供
请记住安全是一个持续的过程,因此您需要密切关注用户和文件,我们将在第八章和第九章中详细讨论这一点,但现在让我们转向文件系统的话题。
第五章:文件系统
在本章中,我们专注于文件和文件系统。UNIX 的“一切皆文件”的概念在 Linux 中继续存在,尽管这并非始终如此,但 Linux 中的大多数资源确实是文件。文件可以是您写给学校的信件内容,也可以是您从显然安全和受信任的网站下载的有趣的 GIF 图像。
在 Linux 中,还有其他一些东西被公开为文件,例如设备和伪设备,比如在echo "Hello modern Linux users" > /dev/pts/0中,它会将“Hello modern Linux users”打印到屏幕上。虽然您可能不会将这些资源与文件关联起来,但您可以使用与常规文件相同的方法和工具访问它们。例如,内核公开了有关进程的某些运行时信息(如在“进程管理”中讨论的那样),如其 PID 或用于运行进程的二进制文件。
所有这些事物的共同之处在于标准化的统一接口:打开文件、收集有关文件的信息、向文件写入等等。在 Linux 中,文件系统提供了这种统一接口。这种接口与 Linux 将文件视为一系列字节流而无需关注结构的事实一起,使我们能够构建可以处理各种不同文件类型的工具。
此外,文件系统提供的统一接口减少了您的认知负荷,使您能够更快地学习如何使用 Linux。
在这一章中,我们首先定义了一些相关术语。然后,我们看看 Linux 如何实现“一切皆文件”的抽象概念。接下来,我们回顾了内核用于公开有关进程或设备信息的特定用途文件系统。然后,我们转向普通文件和文件系统,这些通常与文档、数据和程序相关联。我们比较文件系统选项并讨论常见操作。
基础知识
在我们深入讨论文件系统术语之前,让我们先明确一些关于文件系统的隐含假设和期望:
-
尽管有例外情况,但今天大多数广泛使用的文件系统都是分层的。也就是说,它们为用户提供了一个以根目录(
/)开始的单一文件系统树。 -
在文件系统树中,您会找到两种不同类型的对象:目录和文件。将目录视为一个组织单元,允许您对文件进行分组。如果您想应用树的类比,目录就是树中的节点,而叶子可以是文件或目录。
-
您可以通过列出目录内容(
ls)、切换到该目录(cd)以及打印当前工作目录(pwd)来导航文件系统。 -
权限是内建的:正如在“权限”中讨论的那样,文件系统捕获的属性之一是所有权。因此,所有权通过分配的权限强制访问文件和目录。
-
一般来说,文件系统是在内核中实现的。
注意
虽然出于性能原因,文件系统通常是在内核空间中实现的,但也有在用户空间中实现它们的选项。请参阅用户空间文件系统(FUSE)文档和libfuse 项目网站。
在这个非正式的高层解释之后,我们现在专注于一些更清晰的术语定义,你需要理解:
驱动器
一个(物理)块设备,如硬盘驱动器(HDD)或固态硬盘(SSD)。在虚拟机的上下文中,驱动器也可以被模拟,例如/dev/sda(SCSI 设备)或/dev/sdb(SATA 设备)或/dev/hda(IDE 设备)。
分区
您可以将驱动器逻辑地分割成分区,一组存储扇区。例如,您可以决定在您的 HDD 上创建两个分区,然后它们将显示为/dev/sdb1和/dev/sdb2。
卷
卷与分区有些相似,但更灵活,也为特定文件系统格式化。我们将在“逻辑卷管理器”中详细讨论卷。
超级块
格式化后,文件系统在开头有一个特殊的部分,用于捕获文件系统的元数据。这包括文件系统类型、块、状态以及每个块中的索引节点数。
索引节点
在文件系统中,索引节点存储关于文件的元数据,如大小、所有者、位置、日期和权限。然而,索引节点不存储文件名和实际数据。这些信息保存在目录中,实际上目录只是一种特殊类型的常规文件,将索引节点映射到文件名。
那是很多理论,让我们看看这些概念是如何应用的。首先,这里是如何查看系统中存在的驱动器、分区和卷:
$ lsblk --exclude 7 
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 0 223.6G 0 disk 
├─sda1 8:1 0 512M 0 part /boot/efi 
└─sda2 8:2 0 223.1G 0 part 
├─elementary--vg-root 253:0 0 222.1G 0 lvm /
└─elementary--vg-swap_1 253:1 0 976M 0 lvm [SWAP]
列出所有块设备,但排除伪(循环)设备。
我们有一个名为sda的磁盘驱动器,总共有 223 GB。
这里有两个分区,其中sda1是引导分区。
第二个分区,名为sda2,包含两个卷(详细信息请参阅“逻辑卷管理器”)。
现在我们已经对物理和逻辑设置有了一个整体的概念,让我们更仔细地看看正在使用的文件系统:
$ findmnt -D -t nosquashfs 
SOURCE FSTYPE SIZE USED AVAIL USE% TARGET
udev devtmpfs 3.8G 0 3.8G 0% /dev
tmpfs tmpfs 778.9M 1.6M 777.3M 0% /run
/dev/mapper/elementary--vg-root ext4 217.6G 13.8G 192.7G 6% /
tmpfs tmpfs 3.8G 19.2M 3.8G 0% /dev/shm
tmpfs tmpfs 5M 4K 5M 0% /run/lock
tmpfs tmpfs 3.8G 0 3.8G 0% /sys/fs/cgroup
/dev/sda1 vfat 511M 6M 504.9M 1% /boot/efi
tmpfs tmpfs 778.9M 76K 778.8M 0% /run/user/1000
列出文件系统,但排除squashfs 类型(专门用于 CD 的只读压缩文件系统,现在也用于快照)。
我们可以进一步查看单个文件系统对象,如目录或文件:
$ stat myfile
File: myfile
Size: 0 Blocks: 0 IO Block: 4096 regular empty file 
Device: fc01h/64513d Inode: 555036 Links: 1 
Access: (0664/-rw-rw-r--) Uid: ( 1000/ mh9) Gid: ( 1001/ mh9)
Access: 2021-08-29 09:26:36.638447261 +0000
Modify: 2021-08-29 09:26:36.638447261 +0000
Change: 2021-08-29 09:26:36.638447261 +0000
Birth: 2021-08-29 09:26:36.638447261 +0000
文件类型信息
设备和索引节点的信息
在前面的命令中,如果我们使用了stat .(注意点号),我们会得到相应目录文件的信息,包括其 inode、使用的块数等。
表 5-1 列出了一些基本的文件系统命令,可以帮助你探索我们之前介绍的概念。
表 5-1. 选择低级文件系统和块设备命令
| Command | Use case |
|---|---|
lsblk |
列出所有块设备 |
fdisk, parted |
管理磁盘分区 |
blkid |
显示块设备属性,如 UUID |
hwinfo |
显示硬件信息 |
file -s |
显示文件系统和分区信息 |
stat, df -i, ls -i |
显示和列出与 inode 相关的信息 |
在文件系统的上下文中,你会经常遇到链接这个术语。有时你想用不同的名称引用文件或提供快捷方式。在 Linux 中有两种类型的链接:
硬链接
引用 inode,并且不能引用目录。它们也不跨文件系统工作。
符号链接,或symlinks
特殊文件,其内容是表示另一个文件路径的字符串。
现在让我们看看链接的实际应用(一些输出被缩短):
$ ln myfile somealias 
$ ln -s myfile somesoftalias 
$ ls -al *alias 
-rw-rw-r-- 2 mh9 mh9 0 Sep 5 12:15 somealias
lrwxrwxrwx 1 mh9 mh9 6 Sep 5 12:45 somesoftalias -> myfile
$ stat somealias 
File: somealias
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: fd00h/64768d Inode: 6302071 Links: 2
...
$ stat somesoftalias 
File: somesoftalias -> myfile
Size: 6 Blocks: 0 IO Block: 4096 symbolic link
Device: fd00h/64768d Inode: 6303540 Links: 1
...
创建一个指向myfile的硬链接。
创建一个指向同一文件的软链接(注意使用了-s选项)。
列出文件。注意不同的文件类型和名称的渲染。我们也可以使用ls -ali *alias,这将显示关联到硬链接上的两个名称具有相同的 inode。
显示硬链接的文件详细信息。
显示软链接的文件详细信息。
现在你对文件系统术语已经很熟悉了,让我们探索 Linux 如何使任何类型的资源都可以作为文件对待。
虚拟文件系统
Linux 通过一种称为virtual file system (VFS)的抽象来管理许多资源(内存中的、本地附加的或网络存储中的),实现了类似文件的访问。其基本思想是在客户端(系统调用)和实现具体设备或其他资源操作的各个文件系统之间引入一层间接层。这意味着 VFS 将通用操作(打开、读取、寻找)与实际实现细节分离开来。
VFS 是内核中的一个抽象层,为客户端提供了一种基于文件范例的通用访问资源的方式。在 Linux 中,文件没有任何规定的结构;它只是一串字节流。客户端决定这些字节的含义。正如图 5-1 所示,VFS 抽象了对不同类型文件系统的访问:
本地文件系统,如ext3、XFS、FAT 和 NTFS
这些文件系统使用驱动程序访问本地块设备,如硬盘驱动器或固态驱动器。
内存中的文件系统,如tmpfs,不依赖于长期存储设备,而是存在于主存储器(RAM)中
我们将在“普通文件”中讨论这些以及之前的类别。
伪文件系统如procfs,如“伪文件系统”所述
这些文件系统也是内存中的。它们用于内核接口和设备抽象。
诸如 NFS、Samba、Netware(前身为 Novell)等网络文件系统
这些文件系统也使用驱动程序;然而,实际数据所在的存储设备不是本地附加的而是远程的。这意味着驱动程序涉及网络操作。因此,我们将在第七章中进行介绍。

图 5-1. Linux VFS 概述
描述 VFS 的构成并不容易。与文件相关的系统调用超过 100 个;然而,其核心操作可以分为几类,如表 5-2 中所列。
表 5-2. 构成 VFS 接口的选择性系统调用
| 类别 | 示例系统调用 |
|---|---|
| Inodes | chmod, chown, stat |
| 文件 | open, close, seek, truncate, read, write |
| 目录 | chdir, getcwd, link, unlink, rename, symlink |
| 文件系统 | mount, flush, chroot |
| 其他 | mmap, poll, sync, flock |
许多 VFS 系统调用会分派到特定于文件系统的实现。对于其他系统调用,存在 VFS 的默认实现。此外,Linux 内核定义了相关的 VFS 数据结构—参见include/linux/fs.h—如下:
inode
核心文件系统对象,包括类型、所有权、权限、链接、指向包含文件数据的块的指针、创建和访问统计信息等
file
表示一个打开的文件(包括路径、当前位置和 inode)
dentry(目录条目)
存储其父项和子项
super_block
表示包括挂载信息的文件系统
其他
包括vfsmount和file_system_type
VFS 概述完成后,让我们更详细地查看细节,包括卷管理、文件系统操作和常见文件系统布局。
逻辑卷管理器
我们之前讨论过如何使用分区划分驱动器。尽管这是可能的,但特别是在需要调整大小(更改存储空间量)时,分区使用起来很困难。
逻辑卷管理器(LVM)在物理实体(如驱动器或分区)与文件系统之间使用一层间接,这样可以通过资源池化实现零风险、零停机时间的扩展和自动存储扩展。LVM 的工作方式如图 5-2 所示,关键概念在接下来的段落中解释。

图 5-2. Linux LVM 概述
物理卷(PV)
可能是磁盘分区、整个磁盘驱动器或其他设备。
逻辑卷(LV)
是从 VG 创建的块设备。在概念上类似于分区。在使用中可以轻松调整 LV 的大小。
卷组(VG)
是一组 PV 和 LV 之间的中介。将 VG 视为共同提供资源的 PV 池。
要使用 LVM 来管理卷,需要一些工具;然而,它们的名称一致且相对易于使用:
PV 管理工具
-
lvmdiskscan -
pvdisplay -
pvcreate -
pvscan
VG 管理工具
-
vgs -
vgdisplay -
vgcreate -
vgextend
LV 管理工具
-
lvs -
lvscan -
lvcreate
让我们看看一些 LVM 命令的实际应用,使用一个具体的设置:
$ sudo lvscan 
ACTIVE '/dev/elementary-vg/root' [<222.10 GiB] inherit
ACTIVE '/dev/elementary-vg/swap_1' [976.00 MiB] inherit
$ sudo vgs 
VG #PV #LV #SN Attr VSize VFree
elementary-vg 1 2 0 wz--n- <223.07g 16.00m
$ sudo pvdisplay 
--- Physical volume ---
PV Name /dev/sda2
VG Name elementary-vg
PV Size <223.07 GiB / not usable 3.00 MiB
Allocatable yes
PE Size 4.00 MiB
Total PE 57105
Free PE 4
Allocated PE 57101
PV UUID 2OrEfB-77zU-jun3-a0XC-QiJH-erDP-1ujfAM
列出逻辑卷;我们有两个(root 和 swap_1),使用卷组 elementary-vg。
显示卷组;我们有一个名为 elementary-vg。
显示物理卷;我们有一个(/dev/sda2),分配给卷组 elementary-vg。
无论您使用分区还是 LV,接下来需要进行两个步骤才能使用文件系统。
文件系统操作
在下一节中,我们将讨论如何在给定分区或使用 LVM 创建的逻辑卷上创建文件系统。涉及两个步骤:创建文件系统——在其他非 Linux 操作系统中,此步骤有时称为格式化——然后将其挂载或插入到文件系统树中。
创建文件系统
要使用文件系统,第一步是创建一个。这意味着您正在设置组成文件系统的管理部件,以分区或卷作为输入。如果您不确定如何收集有关输入的必要信息,请参考表 5-1,一旦准备就绪,使用mkfs创建文件系统。
mkfs有两个主要输入:您想要创建的文件系统类型(查看我们在“常见文件系统”讨论的选项之一)和要在其上创建文件系统的设备(例如,逻辑卷):
mkfs -t ext4 \ 
/dev/some_vg/some_lv 
创建类型为ext4的文件系统。
在逻辑卷/dev/some_vg/some_lv上创建文件系统。
如前所述,要创建文件系统并没有太多工作,因此您主要的工作是弄清楚要使用的文件系统类型。
使用mkfs创建了文件系统后,您可以将其在文件系统树中可用。
挂载文件系统
挂载文件系统意味着将其附加到文件系统树(从根目录开始)。使用mount命令来附加文件系统。mount有两个主要输入:你想要附加的设备和文件系统树中的位置。此外,你还可以提供其他输入,包括挂载选项(通过-o),如只读,以及通过--bind进行绑定挂载,用于将目录挂载到文件系统树中。我们将在容器的背景下重新讨论后者。
你也可以单独使用mount。以下是如何列出现有挂载点的方法:
$ mount -t ext4,tmpfs 
tmpfs on /run type tmpfs (rw,nosuid,noexec,relatime,size=797596k,mode=755)
/dev/mapper/elementary--vg-root on / type ext4 (rw,relatime,errors=remount-ro) 
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
列出挂载点,但只显示特定的文件系统类型(这里是ext4和tmpfs)。
一个挂载的示例:LVM VG /dev/mapper/elementary--vg-root 类型为ext4,挂载在根目录。
你必须确保使用与其创建时相同的类型挂载文件系统。例如,如果你试图使用mount -t vfat /dev/sdX2 /media挂载 SD 卡,你必须知道 SD 卡是用vfat格式化的。你可以使用-a选项让mount尝试所有文件系统,直到找到一个可用的。
此外,挂载只在系统运行时有效,因此要使其永久,你需要使用fstab 文件(/etc/fstab)。例如,这是我的(输出略作编辑以适应):
$ cat /etc/fstab
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point> <type> <options> <dump> <pass>
/dev/mapper/elementary--vg-root / ext4 errors=remount-ro 0 1
# /boot/efi was on /dev/sda1 during installation
UUID=2A11-27C0 /boot/efi vfat umask=0077 0 1
/dev/mapper/elementary--vg-swap_1 none swap sw 0 0
现在你知道如何管理分区,卷和文件系统了。接下来,我们将审查组织文件系统常见方法。
常见的文件系统布局
一旦你有了一个文件系统,显而易见的挑战就是想出一种组织其内容的方法。你可能想要组织诸如存储程序的位置,配置数据,系统数据和用户数据的东西。我们将把这些目录及其内容的组织称为文件系统布局。正式地说,布局被称为文件系统层次结构标准(FHS)。它定义目录,包括它们的结构和推荐内容。Linux 基金会维护 FHS,这是 Linux 发行版遵循的良好起点。
FHS 的理念是值得赞扬的。然而,在实际应用中,你会发现文件系统布局在很大程度上取决于你使用的 Linux 发行版。因此,我强烈建议你使用man hier命令来了解你具体的设置。
为了给你提供在看到特定顶级目录时可以期待什么的高级概念,我编制了一个常见顶级目录的列表,详见 Table 5-3。
Table 5-3. 常见的顶级目录
| Directory | Semantics |
|---|---|
| bin, sbin | 系统程序和命令(通常是到/usr/bin和/usr/sbin的链接) |
| boot | 内核映像和相关组件 |
| dev | 设备(终端,驱动器等) |
| etc | 系统配置文件 |
| home | 用户主目录 |
| lib | 共享系统库 |
| mnt, media | 可移动媒体的挂载点(例如 USB 存储设备) |
| opt | 特定于发行版;可以存放包管理器文件 |
| proc, sys | 内核接口;参见 “伪文件系统” |
| tmp | 用于临时文件 |
| usr | 用户程序(通常为只读) |
| var | 用户程序(日志、备份、网络缓存等) |
接下来,让我们来看看一些特殊类型的文件系统。
伪文件系统
文件系统是组织和访问信息的一个好方法。到现在为止,你可能已经内化了 Linux 的口号“一切皆文件”。我们已经看过 Linux 如何通过 VFS 提供统一的接口,在 “虚拟文件系统” 中。现在,让我们更仔细地看看在 VFS 实现不是块设备(如 SD 卡或 SSD 驱动器)的情况下提供接口的方式。
认识伪文件系统:它们只是假装是文件系统,以便我们可以像平常一样与它们交互(ls、cd、cat),但实际上它们是在封装一些内核接口。这些接口可以是多种形式,包括以下内容:
-
关于进程的信息
-
与键盘等设备的交互
-
实用工具,如可以用作数据源或接收器的特殊设备
让我们更仔细地看一下 Linux 拥有的三个主要伪文件系统,从最古老的开始。
procfs
Linux 从 UNIX 继承了 /proc 文件系统(procfs)。最初的目的是从内核发布与进程相关的信息,以便系统命令如 ps 或 free 可以使用。它的结构几乎没有规则,允许读写访问,随着时间的推移,许多内容都被加入其中。一般来说,你会在这里找到两种类型的信息:
-
/proc/PID/ 中的每个进程信息。这是内核通过目录以 PID 作为目录名公开的与进程相关的信息。有关那里可用信息的详细内容列在 表 5-4 中。
-
其他信息,如挂载信息、与网络相关的信息、TTY 驱动程序、内存信息、系统版本和运行时间。
你可以通过像 cat 这样的命令轻松获取 表 5-4 中列出的每个进程信息。请注意,大多数是只读的;写入语义取决于底层资源。
表 5-4. procfs 中的每个进程信息(最显著的)
| 条目 | 类型 | 信息 |
|---|---|---|
attr |
目录 | 安全属性 |
cgroup |
文件 | 控制组 |
cmdline |
文件 | 命令行 |
cwd |
链接 | 当前工作目录 |
environ |
文件 | 环境变量 |
exe |
链接 | 进程的可执行文件 |
fd |
目录 | 文件描述符 |
io |
文件 | 存储 I/O(读取和写入的字节/字符) |
limits |
文件 | 资源限制 |
mem |
文件 | 已使用内存 |
mounts |
文件 | 已使用的挂载点 |
net |
目录 | 网络统计信息 |
stat |
文件 | 进程状态 |
系统调用 |
文件 | 系统调用使用情况 |
任务 |
目录 | 每个任务(线程)的信息 |
定时器 |
文件 | 定时器信息 |
要查看这个过程,请检查进程状态。我们在这里使用status而不是stat,后者没有人类可读的标签:
$ cat /proc/self/status | head -10 
Name: cat
Umask: 0002
State: R (running) 
Tgid: 12011
Ngid: 0
Pid: 12011 
PPid: 3421 
TracerPid: 0
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
获取关于当前运行命令的进程状态,仅显示前 10 行。
当前状态(运行中,在 CPU 上)。
当前进程的 PID。
命令的父进程的进程 ID;在本例中,它是我运行cat命令的 Shell。
这里是使用procfs从网络空间获取信息的另一个示例:
$ cat /proc/self/net/arp
IP address HW type Flags HW address Mask Device
192.168.178.1 0x1 0x2 3c:a6:2f:8e:66:b3 * wlp1s0
192.168.178.37 0x1 0x2 dc:54:d7:ef:90:9e * wlp1s0
如前所示的命令,我们可以从此特殊的/proc/self/net/arp获取当前进程的 ARP 信息。
如果您正在进行低级调试或开发系统工具,则procfs非常有用。它相对混乱,因此您需要内核文档,甚至更好的是内核源代码,以了解每个文件代表什么以及如何解释其中的信息。
让我们来看看内核以更近期、更有条理的方式暴露信息。
sysfs
在procfs非常混乱的地方,/sys文件系统(sysfs)是 Linux 特有的一种结构化方式,内核使用标准化布局选择性地暴露信息(如关于设备的信息)。
这里是sysfs中的目录:
块/
这个目录是已发现的块设备的符号链接。
总线/
在这个目录中,您会找到内核支持的每种物理总线类型的一个子目录。
类别/
这个目录包含设备类。
设备/
此目录包含两个子目录:块/ 用于系统上的块设备和 字符/ 用于字符设备,使用major-ID:minor-ID结构。
设备/
在此目录中,内核提供了设备树的表示。
固件/
通过这些目录,您可以管理特定于固件的属性。
文件系统/
此目录包含一些文件系统的子目录。
模块/
在这些目录中,您会找到每个加载到内核中的模块的子目录。
在sysfs中还有更多子目录,但有些是较新的和/或需要更好的文档。您会发现sysfs中的某些信息重复出现在procfs中,但其他信息(如内存信息)只能在procfs中找到。
让我们看看sysfs的工作方式(输出已编辑以适合)。
$ ls -al /sys/block/sda/ | head -7 
total 0
drwxr-xr-x 11 root root 0 Sep 7 11:49 .
drwxr-xr-x 3 root root 0 Sep 7 11:49 ..
-r--r--r-- 1 root root 4096 Sep 8 16:22 alignment_offset
lrwxrwxrwx 1 root root 0 Sep 7 11:51 bdi -> ../../../virtual/bdi/8:0 
-r--r--r-- 1 root root 4096 Sep 8 16:22 capability 
-r--r--r-- 1 root root 4096 Sep 7 11:49 dev 
列出关于块设备sda的信息,仅显示前七行。
使用MAJOR:MINOR格式的backing_dev_info链接。
捕获设备的功能,例如它是否可移动。
包含设备主要和次要编号(8:0);另请参阅块设备驱动程序参考以了解这些数字的含义。
接下来在我们的伪文件系统评论中是设备。
devfs
/dev(https://oreil.ly/EkO8V)文件系统(devfs)托管设备特殊文件,代表从物理设备到像随机数生成器或仅写数据接收器等内容的设备。
通过devfs可用和管理的设备包括:
块设备
按块处理数据,例如存储设备(驱动器)
字符设备
按字符处理,比如终端、键盘或鼠标
特殊设备
生成数据或允许您操作数据,包括著名的/dev/null或/dev/random
现在让我们看看devfs的实际运行。例如,假设您想要获取一个随机字符串。您可以做类似以下的操作:
tr -dc A-Za-z0-9 < /dev/urandom | head -c 42
前述命令生成一个包含大写字母、小写字母和数字字符的 42 字符随机序列。而/dev/urandom看起来像一个文件,也可以像一个文件一样使用,但实际上它是一个特殊文件,利用多种来源生成(或多或少)随机的输出。
您认为以下命令怎么样:
echo "something" > /dev/tty
是的!字符串“something”出现在您的显示器上,这是有意设计的。/dev/tty代表终端,使用这个命令我们确实向它发送了(非常字面的)内容。
了解了文件系统及其特性后,现在让我们关注管理诸如文档和数据文件之类的常规文件所需使用的文件系统。
常规文件
在这一节中,我们专注于常规文件以及适用于这些文件类型的文件系统。在工作中,我们处理的大多数日常文件都属于这个类别:办公文档、YAML 和 JSON 配置文件、图像(PNG、JPEG 等)、源代码、纯文本文件等等。
Linux 提供了丰富的选择。我们将重点关注本地文件系统,包括 Linux 原生支持的以及 Linux 允许使用的其他操作系统(如 Windows/DOS)的文件系统。首先,让我们看一些常见的文件系统。
常见文件系统
“常见文件系统”这个术语没有正式定义。它只是一个文件系统的总称,可以是 Linux 发行版中的默认文件系统,也可以是存储设备(如可移动设备如 USB 闪存和 SD 卡)或只读设备(如 CD 和 DVD)中广泛使用的文件系统。
在表 5-5 中,我提供了一些常见文件系统的快速概述和比较,这些文件系统都享有内核级支持。在本节的后面,我们将更详细地审视一些流行的文件系统。
表 5-5. 常见的常规文件系统
| 文件系统 | Linux 支持自 | 文件大小 | 卷大小 | 文件数 | 文件名长度 |
|---|---|---|---|---|---|
ext2 |
1993 | 2 TB | 32 TB | 10¹⁸ | 255 字符 |
ext3 |
2001 | 2 TB | 32 TB | 可变 | 255 字符 |
ext4 |
2008 | 16 TB | 1 EB | 40 亿 | 255 字符 |
btrfs |
2009 | 16 EB | 16 EB | 2¹⁸ | 255 字符 |
| XFS | 2001 | 8 EB | 8 EB | 2⁶⁴ | 255 字符 |
| ZFS | 2006 | 16 EB | 2¹²⁸ 字节 | 10¹⁴ 每目录的文件 | 255 字符 |
| NTFS | 1997 | 16 TB | 256 TB | 2³² | 255 字符 |
vfat |
1995 | 2 GB | 不适用 | 每目录 2¹⁶ | 255 字符 |
注意
表格 5-5 中提供的信息旨在让您对文件系统有一个大致的了解。有时很难确定文件系统何时会被正式视为 Linux 的一部分;有时仅在应用相关上下文时,数字才有意义。例如,理论极限与实现之间存在差异。
现在让我们更仔细地看一些常用于普通文件的文件系统:
一种广泛使用的文件系统,现在许多发行版默认使用它。它是ext3的向后兼容进化版本。像ext3一样,它提供了日志功能,即更改记录在日志中,以便在最坏的情况下(例如:停电)快速恢复。它是一个非常好的通用选择。详细使用方法请参见ext4手册。
一种日志文件系统,最初由 Silicon Graphics(SGI)在 1990 年代初为其工作站设计。支持大文件和高速 IO,例如在 Red Hat 发行版系列中使用。
最初由 Sun Microsystems 于 2001 年开发,ZFS 结合了文件系统和卷管理器功能。现在有OpenZFS 项目,在开源环境中提供了一种前进的路径,但是关于ZFS 与 Linux 集成还存在一些问题。
这实际上是 Linux 的一系列 FAT 文件系统,其中最常用的是vfat。其主要用途是与 Windows 系统的互操作性以及使用 FAT 的可移动媒体。许多关于卷的本机考虑不适用。
驱动器不是唯一存储数据的地方,所以让我们看看内存选项。
内存文件系统
有许多内存文件系统可用;有些是通用目的,而其他一些则有非常具体的用例。以下我们列出一些广泛使用的内存文件系统(按字母顺序排列):
用于调试的专用文件系统;通常使用mount -t debugfs none /sys/kernel/debug挂载。
允许将文件系统映射到块而不是设备。请参阅关于背景的 邮件线程。
pipefs
一个安装在 pipe: 上的特殊(伪)文件系统,使管道可用。
sockfs
另一个特殊的(伪)文件系统,使网络套接字看起来像文件,位于系统调用和 sockets 之间。
用于实现交换(不可挂载)。
一个通用的文件系统,将文件数据保留在内核缓存中。它速度快但不持久(断电意味着数据丢失)。
让我们继续讨论一类特殊的文件系统,特别是在 “容器” 的背景下相关的文件系统。
Copy-on-Write 文件系统
Copy-on-write(CoW)是一个巧妙的概念,可以提高 I/O 速度,同时使用更少的空间。它的工作方式如 图 5-3 所示,并在接下来的段落中进一步解释。

图 5-3. CoW 原理的实际应用
-
原始文件 File 1,由块 A、B 和 C 组成,被复制到一个名为 File 2 的文件中。与其复制实际块不同,只复制了元数据(指向块的指针)。这样做快速且不占用太多空间,因为只创建了元数据。
-
当文件 2 被修改(比如说块 C 中的某些内容被改变)时,只有块 C 被复制:创建了一个新块称为 C′,虽然文件 2 仍然指向(使用)未修改的块 A 和 B,但现在它使用一个新块(C′)来捕获新数据。
在我们讨论实现之前,我们需要了解在这种情况下相关的第二个概念:联合挂载。这是一个概念,您可以将(挂载)多个目录合并到一个位置,以便对于最终目录的用户来说,该目录看起来包含所有参与目录的组合内容(或者说:联合)。使用联合挂载时,您经常会遇到“上层文件系统”和“下层文件系统”这样的术语,暗示了挂载顺序的分层。您可以在文章 “使用联合挂载统一文件系统” 中找到更多详细信息。
使用联合挂载时,细节决定成败。您必须制定规则,规定当文件存在于多个位置时发生什么,或者写入或删除文件意味着什么。
让我们在 Linux 文件系统的上下文中快速看一下 CoW 的实现。在我们讨论它们作为容器映像构建块的用途时,我们将更详细地研究其中一些内容,见 第六章。
最初由斯托尼布鲁克大学开发,Unionfs 实现了 CoW 文件系统的联合挂载。它允许您在挂载时通过优先级透明地叠加来自不同文件系统的文件和目录。它曾在 CD-ROM 和 DVD 的背景下广泛使用。
2009 年引入的 Linux 联合挂载文件系统实现,并于 2014 年添加到内核中。使用 OverlayFS,一旦打开文件,所有操作都由底层或上层文件系统直接处理。
另一种尝试实现内核内联挂载的方法,AUFS(高级多层统一文件系统;最初称为 AnotherUnionFS),尚未合并到内核中。它曾经是 Docker 的默认选项(参见 “Docker”;现在 Docker 默认使用带有存储驱动程序 overlay2 的 OverlayFS)。
简称为 B 树文件系统(并发音为 butterFS 或 betterFS),btrfs 是最初由 Oracle 公司设计的 CoW 文件系统。今天,许多公司参与了 btrfs 的开发,包括 Facebook、Intel、SUSE 和 Red Hat。
它具有多种功能,如快照(用于软件 RAID)和静默数据损坏的自动检测。这使得 btrfs 非常适合专业环境,例如在服务器上。
结论
在本章中,我们讨论了 Linux 中的文件和文件系统。文件系统是以分层方式组织信息访问的一种出色且灵活的方式。Linux 在文件系统周围有许多技术和项目。一些是基于开源的,但也有一系列商业产品。
我们讨论了从驱动器到分区和卷的基本构建模块。Linux 使用 VFS 实现“一切皆文件”的抽象,支持几乎任何类型的文件系统,无论是本地的还是远程的。
内核使用伪文件系统,如 /proc 和 /sys,来公开有关进程或设备的信息。您可以与这些(内存中的)代表内核 API 的文件系统进行交互,就像与 ext4(用于存储文件的文件系统)一样。
然后我们转向常规文件和文件系统,比较了常见的本地文件系统选项以及内存中和 CoW 文件系统的基础知识。Linux 的文件系统支持非常全面,允许您使用(至少读取)一系列文件系统,包括源自其他操作系统(如 Windows)的文件系统。
您可以通过以下资源深入了解本章涵盖的主题:
基础知识
VFS
常规文件
装备了关于文件系统的知识,我们现在可以把一切整合起来,专注于如何管理和启动应用程序。
第六章:应用程序、软件包管理和容器
在本章中,我们讨论 Linux 中的应用程序。有时,“应用程序”(或简称“应用”)这个术语与“程序”、“二进制文件”或“可执行文件”是可以互换使用的。我们将解释这些术语之间的区别,并且最初将集中讨论术语,包括应用程序和软件包的定义。
我们讨论 Linux 如何启动并启动我们所依赖的所有服务。这也被称为引导过程。我们将重点介绍初始化系统,特别是事实上的标准——systemd 生态系统。
然后,我们转向软件包管理,首先在一般术语中回顾应用程序供应链,看看不同的运动部件是如何运作的。然后,为了让您了解现有机制和挑战,我们重点讨论了传统 Linux 发行版中的软件包管理,从红帽到基于 Debian 的系统,还瞥见了特定编程语言的软件包管理器,例如 Python 或 Rust。
在本章的下一部分,我们将关注容器:它们是什么以及它们如何工作。我们将回顾容器的构建模块,您可以使用的工具,以及使用容器的良好实践。
为了结束本章,我们将看看在桌面环境中管理 Linux 应用程序的现代方法。大多数现代软件包管理解决方案也以某种形式利用容器。
现在,让我们看看应用程序是什么,以及还有哪些相关术语,不再拖泥带水。
基础知识
在我们深入应用程序管理、初始化系统和容器的细节之前,让我们先从本章及更深远的定义开始。我们现在才详细讨论应用程序的原因是,有一些先决条件(例如 Linux 内核、Shell、文件系统和安全方面)您需要充分理解应用程序,现在我们可以建立在迄今所学的基础上,进一步深入了解应用程序:
程序
这通常是 Linux 可以加载到内存并执行的二进制文件或 Shell 脚本。另一种称呼这个实体的方式是可执行文件。可执行文件的类型决定了如何运行它——例如,一个 shell(参见“Shell”)会解释和执行一个 Shell 脚本。
进程
基于程序的运行实体,加载到主存储器中,并在不休眠时使用 CPU 或 I/O。详见“进程管理”和第三章。
守护进程
简称为守护进程,有时也称为服务,这是一个后台进程,为其他进程提供某种功能。例如,打印守护进程允许您打印。还有用于 Web 服务、日志记录、时间等等您日常所依赖的许多其他实用程序的守护进程。
应用程序
包括其依赖关系的程序。通常是一个重要的程序,包括用户界面。我们通常将术语 应用程序 与程序的整个生命周期、其配置和数据相关联:从查找和安装到升级再到删除。
软件包
包含程序和配置的文件;用于分发软件应用程序。
包管理器
一个程序,以软件包作为输入,并根据其内容和用户指令安装、升级或从 Linux 环境中删除它。
供应链
一个由软件生产商和分发商组成的集合,使您能够基于软件包找到并使用应用程序;详细信息请参阅 “Linux 应用程序供应链”。
引导
Linux 的启动序列涉及硬件和操作系统初始化步骤,包括加载内核和启动服务(或守护程序),目的是将 Linux 系统带入可用状态;详细内容请参阅 “Linux 启动过程”。
借助这些高级定义,我们从字面上来看确实是从头开始:让我们看看 Linux 是如何启动的,以及如何启动所有守护进程,以便我们可以使用 Linux 来完成工作。
Linux 启动过程
Linux 的 引导过程 通常是一个多阶段的工作,涉及硬件和内核的协同工作。
在 图 6-1 中,您可以看到完整的引导过程,包括以下五个步骤:

图 6-1. Linux 启动过程
-
在现代环境中,统一可扩展固件接口(UEFI)规范定义了存储在 NVRAM 中的引导配置和引导加载程序。在旧系统中,在完成自检(POST)之后,基本输入输出系统(BIOS;参见 “BIOS 和 UEFI”)会初始化硬件(管理 I/O 端口和中断),并将控制权交给引导加载程序。
-
引导加载程序的一个目标是引导内核。根据引导介质的不同,细节可能略有不同。现代有多种引导加载程序选择(例如,GRUB 2、systemd-boot、SYSLINUX、rEFInd),还有一些传统的选择(例如,LILO、GRUB 1)。
-
内核通常位于 /boot 目录中以压缩形式存在。这意味着第一步是将内核提取并加载到主内存中。在初始化其子系统、文件系统和驱动程序之后(如第二章讨论的和 “挂载文件系统”),内核将控制权交给 init 系统,引导过程正式结束。
-
初始化系统负责在整个系统范围内启动守护程序(服务进程)。这个 init 进程是进程层次结构的根,并且具有进程 ID(PID)1。换句话说,PID 1 的进程会一直运行,直到系统关机。除了负责启动其他守护程序外,PID 1 进程传统上还负责处理孤立进程(即没有父进程的进程)。
-
通常,这之后会发生一些其他的用户空间初始化,具体取决于环境:
-
通常会有终端、环境和 shell 初始化,如 第三章 中讨论的那样。
-
启动显示管理器、图形服务器等桌面环境的 GUI 组件,考虑用户的偏好和配置。
-
通过这个对 Linux 启动过程的高层概述,我们结束了我们的介绍性部分,并关注一个重要的、面向用户的组件:init 系统。在这本书的上下文中,这部分(前面的第 4 和第 5 步)对您来说最为重要,可以帮助您定制和扩展 Linux 安装。
有一个关于 init 系统的比较 可以在 Gentoo wiki 上找到。我们将限制我们的讨论在 systemd 上,几乎所有当前的 Linux 发行版都在使用它。
systemd
systemd 最初是一个 init 系统,用来替代 initd,但今天它是一个功能强大的管理程序,包括日志记录、网络配置和网络时间同步等功能。它提供了一种灵活、便携的方式来定义守护程序及其依赖关系,并统一接口来控制配置。
几乎所有当前的 Linux 发行版都在使用 systemd,包括自 2011 年 5 月以来的 Fedora,自 2012 年 9 月以来的 openSUSE,自 2014 年 4 月以来的 CentOS,自 2014 年 6 月以来的 RHEL,自 2014 年 10 月以来的 SUSE Linux,自 2015 年 4 月以来的 Debian 和 Ubuntu。
特别是,systemd 通过以下方式解决了以前 init 系统的缺点:
-
提供一种跨发行版管理启动的统一方式。
-
实施更快、更易理解的服务配置。
-
提供一个现代管理套件,包括监控、资源使用控制(通过 cgroups)和内置审计。
此外,init 在初始化时按顺序(即按字母数字顺序)启动服务,而 systemd 则可以启动任何已满足其依赖关系的服务,可能加快启动时间。
告诉 systemd 何时运行、如何运行的方法就是通过单元。
单元
在 systemd 中,单元是具有不同语义的逻辑分组,具体取决于其功能和/或其所针对的资源。systemd 区分了许多类型的单元,具体取决于目标资源:
service 单元
描述如何管理服务或应用程序。
target 单元
捕获依赖关系
mount 单元
定义一个挂载点。
timer 单元
为 cron 作业等定义定时器
其他不太重要的单元类型包括以下内容:
套接字
描述网络或 IPC 套接字
设备
对于 udev 或 sysfs 文件系统
自动挂载
配置自动挂载点
交换
描述交换空间
路径
用于基于路径的激活
快照
允许在更改后重建系统的当前状态
切片
与 cgroups 相关(参见 “Linux cgroups”)
范围
管理通过外部创建的系统进程集合
要使 systemd 知道单元,必须将其序列化为文件。 systemd 在多个位置查找单元文件。最重要的三个文件路径如下:
/lib/systemd/system
包安装的单元
/etc/systemd/system
系统管理员配置的单元
/run/systemd/system
非持久运行时修改
有了 systemd 定义的基本工作单位(别有用心)之后,让我们继续学习如何通过命令行控制它。
使用 systemctl 进行管理
用于与 systemd 交互以管理服务的工具是 systemctl。
在 表 6-1 中,我列出了常用的 systemctl 命令列表。
表 6-1. 有用的 systemd 命令
| 命令 | 用途 |
|---|---|
systemctl enable XXXXX.service |
启用服务;准备启动 |
systemctl daemon-reload |
重新加载所有单元文件并重新创建整个依赖树 |
systemctl start XXXXX.service |
启动服务 |
systemctl stop XXXXX.service |
停止服务 |
systemctl restart XXXXX.service |
停止然后启动服务 |
systemctl reload XXXXX.service |
向服务发出 reload 命令;如果失败,则退回到 restart |
systemctl kill XXXXX.service |
停止服务执行 |
systemctl status XXXXX.service |
获取服务状态的简短摘要,包括一些日志行 |
请注意,systemctl 还提供了许多其他命令,从依赖管理和查询到控制整个系统(例如 reboot)。
systemd 生态系统还有许多其他命令行工具,你可能会觉得方便,至少应该了解一下。这包括但不限于以下内容:
允许您检查引导加载程序状态并管理可用的引导加载程序。
timedatectl
允许您设置和查看与时间和日期相关的信息。
coredumpctl
允许您处理保存的核心转储。在故障排除时,请考虑使用此工具。
使用 journalctl 进行监控
日志是 systemd 的一个组件;从技术上讲,它是由 systemd-journald 守护程序管理的二进制文件,提供了由 systemd 组件记录的所有消息的集中位置。我们将在 “journalctl” 中详细介绍它。现在你只需要知道的是,这是一个允许查看 systemd 管理的日志的工具。
示例:调度问候者
经过所有这些理论,让我们看看systemd的实际运行情况。作为一个简单的用例示例,假设我们想每小时启动我们的登录应用(参见“Running Example: greeter”)。
首先,我们定义一个systemd服务单元文件。这告诉systemd如何启动登录应用程序;将以下内容存储在名为greeter.service的文件中(可以是任何目录,可以是临时目录):
[Unit]
Description=My Greeting Service 
[Service]
Type=oneshot
ExecStart=/home/mh9/greeter.sh 
我们服务的描述,在使用systemctl status时显示
我们应用程序的位置
接下来,我们定义一个timer unit,每小时启动登录服务。将以下内容存储在名为greeter.timer的文件中:
[Unit]
Description=Runs Greeting service at the top of the hour
[Timer]
OnCalendar=hourly 
使用systemd 时间和日期格式定义时间表
现在我们将两个单元文件复制到/run/systemd/system,这样systemd就能识别它们:
$ sudo ls -al /run/systemd/system/
total 8
drwxr-xr-x 2 root root 80 Sep 12 13:08 .
drwxr-xr-x 21 root root 500 Sep 12 13:09 ..
-rw-r--r-- 1 root root 117 Sep 12 13:08 greeter.service
-rw-r--r-- 1 root root 107 Sep 12 13:08 greeter.timer
当我们将它复制到相应目录时,systemd自动接收了我们的登录计时器。
注意
基于 Debian 的系统如 Ubuntu 默认启用并启动服务单元。红帽家族的系统不会在没有显式systemctl start greeter.timer的情况下启动服务。这也适用于在启动时启用服务,其中 Debian-based 发行版默认启用服务,而 Red Hat 发行版则需要通过systemctl enable来确认。
让我们检查我们的登录程序计时器的状态:
$ sudo systemctl status greeter.timer
● greeter.timer - Runs Greeting service at the top of the hour
Loaded: loaded (/run/systemd/system/greeter.timer; static; \
vendor preset: enabled)
Active: active (waiting) since Sun 2021-09-12 13:10:35 IST; 2s ago
Trigger: Sun 2021-09-12 14:00:00 IST; 49min left
Sep 12 13:10:35 starlite systemd[1]: \
Started Runs Greeting service at the top of the hour.
systemd确认已知道我们的登录程序,并已计划运行。但是如何知道它是否成功?让我们检查日志(请注意输出已经编辑,stdout输出直接进入日志):
$ journalctl -f -u greeter.service 
-- Logs begin at Sun 2021-01-24 14:36:30 GMT. --
Sep 12 14:00:01 starlite systemd[1]: Starting My Greeting Service...
Sep 12 14:00:01 starlite greeter.sh[21071]: You are awesome!
...
使用journalctl查看和跟踪(-f)greeter.service单元的日志(用-u选择)
通过这个高级systemd概述,让我们继续以传统方式管理应用程序,即使用通用的软件包管理器。但在深入讨论包的技术细节之前,让我们稍微退后一步,讨论应用程序、包和包管理器的概念,这些概念属于更广泛的供应链范畴。
Linux 应用程序供应链
让我们从我们所说的供应链开始:一个向消费者供应产品的组织和个人系统。虽然您可能不经常考虑供应链,但您每天都在处理它们——例如,当您购买食品或为汽车加油时。在我们的讨论中,产品是由软件成果组成的应用程序,您可以将消费者视为使用应用程序的自己,或者作为管理应用程序的工具。
在概念层面上,图 6-2 显示了典型 Linux 应用程序供应链的主要参与者和阶段。

图 6-2. Linux 应用程序供应链
Linux 应用程序供应链中的三个明显领域如下:
软件维护者
这些包括个人开发者、开源项目以及像独立软件供应商(ISV)这样的公司,他们生产软件成果并将其发布为包到存储库(repo),例如。
存储库
这列出了包含应用程序全部或部分内容以及元数据的包。包通常捕获应用程序的依赖关系。依赖关系是应用程序需要的其他包,以便其正常运行。这可以是库、某种类型的导入或导出程序或其他服务程序。保持这些依赖关系的最新状态很困难。
工具(一个包管理器)
在目标系统方面,可以在存储库中查找包并按照人类用户的指示安装、更新和删除应用程序。请注意,一个或多个包可以代表应用程序及其依赖关系。
虽然细节可能因发行版而异并依赖于环境(服务器、桌面等),但所有应用程序供应链都具有图 6-2 中显示的元素。
有许多选项可用于包和依赖项管理,例如传统的包管理器、基于容器的解决方案以及更近期的方法。
在图 6-3 中,我试图为您提供一个高层次的概述,不过并不宣称这是一个完整的画面。

图 6-3. Linux 包管理和应用程序依赖管理的宇宙
关于包和依赖项管理的三个主要选项类别的几点说明:
传统包管理器
在这个类别中,我们通常区分低级和高级工具。如果一个包管理器可以解决依赖关系并提供高级接口(安装、更新、删除),我们称其为高级包管理器。
基于容器的解决方案
这些最初来自服务器和云计算领域。鉴于它们的功能,一个用例是应用程序管理,但并非其主要用途。换句话说,作为开发人员,您会喜欢容器,因为它们使您能够轻松测试和直接部署您的生产就绪应用程序。另请参阅 “容器”。
现代软件包管理器
这些软件包源自桌面环境,主要目标是尽可能地使最终用户轻松使用应用程序。另请参阅 “现代软件包管理器”。
软件包和软件包管理器
在本节中,我们讨论了长期以来一直在使用的软件包格式和软件包管理器,有些甚至已有几十年历史。这些通常源自两大 Linux 发行版系列:红帽(RHEL、Fedora、CentOS 等)和基于 Debian 的系统(Debian、Ubuntu 等)。
这里讨论的两个相关概念如下:
这些软件包本身
从技术上讲,通常是一个压缩文件,可能包含元数据。
工具(称为 软件包管理器)
处理目标系统上的这些软件包,以安装和维护应用程序。软件包管理器通常代表您与仓库进行交互,并维护本地软件包缓存。
目标系统可能是您笔记本电脑上的桌面环境,也可能是云中的服务器虚拟机实例,例如。根据环境不同,软件包可能更或少适用——例如,在服务器上的 GUI 应用程序不一定是有意义的。
RPM 软件包管理器
RPM 软件包管理器(使用递归缩写 RPM)最初由红帽创建,但现在广泛用于各种发行版。.rpm 文件格式用于 Linux 标准基础,并可以包含二进制或源文件。这些软件包可以通过补丁文件进行密码验证和支持增量更新。
使用 RPM 的软件包管理器包括以下内容:
在 Amazon Linux、CentOS、Fedora 和 RHEL 中
在 CentOS、Fedora 和 RHEL 中
在 openSUSE 和 SUSE Linux Enterprise
让我们看看 RPM 的实际运行情况:假设我们有一个新的开发者环境,并希望使用 yum 安装 Go 编程语言工具链。
请注意,以下 shell 会话的输出已被编辑和缩短,以适应空间(输出中有许多与理解使用方式无关的行)。
首先,我们需要找到 Go 的软件包:
# yum search golang 
Loaded plugins: ovl, priorities
================= N/S matched: golang =================
golang-bin.x86_64 : Golang core compiler tools
golang-docs.noarch : Golang compiler docs
...
golang-googlecode-net-devel.noarch : Supplementary Go networking libraries
golang-googlecode-sqlite-devel.x86_64 : Trivial sqlite3 binding for Go
搜索 Go 软件包。请注意 # 提示,表明我们以 root 用户登录。也许更好的方式是使用 sudo yum。
有了关于软件包的这些信息,我们现在可以使用以下方法安装它:
# yum install golang 
Loaded plugins: ovl, priorities
Resolving Dependencies 
--> Running transaction check
---> Package golang.x86_64 0:1.15.14-1.amzn2.0.1 will be installed
--> Processing Dependency: golang-src = 1.15.14-1.amzn2.0.1 for package:
golang-1.15.14-1.amzn2.0.1.x86_64
...
Transaction Summary
===============================================================================
Install 1 Package (+101 Dependent packages)
Total download size: 183 M
Installed size: 624 M
Is this ok [y/d/N]: y 
Dependencies Resolved
===============================================================================
Package Arch Version Repository Size
===============================================================================
Installing:
golang x86_64 1.15.14-1.amzn2.0.1 amzn2-core 705 k
Installing for dependencies:
acl x86_64 2.2.51-14.amzn2 amzn2-core 82 k
apr x86_64 1.6.3-5.amzn2.0.2 amzn2-core 118 k
...
Verifying : groff-base-1.22.2-8.amzn2.0.2.x86_64 101/102
Verifying : perl-Text-ParseWords-3.29-4.amzn2.noarch 102/102
Installed: 
golang.x86_64 0:1.15.14-1.amzn2.0.1
Dependency Installed:
acl.x86_64 0:2.2.51-14.amzn2 apr.x86_64 0:1.6.3-5.amzn2.0.2
...
Complete!
安装 Go 包。
yum 的第一步是确定 Go 的依赖项。
这里 yum 提供了关于依赖项的摘要,并告诉我们它计划执行什么操作。我需要在这里通过输入 y 进行交互确认。然而,在脚本中,我会使用 yum install golang -y 形式的命令来自动接受这个操作。
在确认所有依赖项和主要包都安装好之后,yum 报告安装成功。
最后但同样重要的是,我们想要验证包,检查我们确切安装了什么及其位置:
# yum info golang
Loaded plugins: ovl, priorities
Installed Packages
Name : golang
Arch : x86_64
Version : 1.15.14
Release : 1.amzn2.0.1
Size : 7.8 M
Repo : installed
From repo : amzn2-core
Summary : The Go Programming Language
URL : http://golang.org/
License : BSD and Public Domain
Description : The Go Programming Language.
接下来,让我们看看另一个广泛使用的包管理器,使用 Debian 包。
Debian deb
deb 包和 .deb 文件格式源自 Debian 发行版。deb 包也可以包含二进制或源文件。多个包管理器使用 deb,包括低级别、无依赖管理的 dpkg,以及高级别的 apt-get、apt 和 aptitude。鉴于 Ubuntu 是基于 Debian 的发行版,deb 包在桌面和服务器上都被广泛使用。
要看看 deb 包如何工作,假设我们想要使用 apt 安装 curl 实用程序。这是一个用于与 HTTP API 交互和从多个位置下载文件的实用工具。请注意,我们再次编辑了输出以使其适应。
首先,我们搜索 curl 包:
# apt search curl 
Sorting... Done
Full Text Search... Done
curl/focal-updates,focal-security 7.68.0-1ubuntu2.6 amd64
command line tool for transferring data with URL syntax
curlftpfs/focal 0.9.2-9build1 amd64
filesystem to access FTP hosts based on FUSE and cURL
flickcurl-doc/focal 1.26-5 all
utilities to call the Flickr API from command line - documentation
flickcurl-utils/focal 1.26-5 amd64
utilities to call the Flickr API from command line
gambas3-gb-net-curl/focal 3.14.3-2ubuntu3.1 amd64
Gambas advanced networking component
...
使用 apt 搜索 curl 包。请注意,总共显示了几十个搜索结果,其中大多数是库和特定语言的绑定(Python、Ruby、Go、Rust 等)。
接下来,我们安装 curl 包如下:
# apt install curl 
Reading package lists... Done
Building dependency tree 
Reading state information... Done
The following additional packages will be installed:
ca-certificates krb5-locales libasn1-8-heimdal libbrotli1 ...
Suggested packages:
krb5-doc krb5-user libsasl2-modules-gssapi-mit ...
The following NEW packages will be installed:
ca-certificates curl krb5-locales libasn1-8-heimdal ...
0 upgraded, 32 newly installed, 0 to remove and 2 not upgraded.
Need to get 5447 kB of archives.
After this operation, 16.7 MB of additional disk space will be used.
Do you want to continue? [Y/n] 
Get:1 http://archive.ubuntu.com/ubuntu focal-updates/main amd64
libssl1.1 amd64 1.1.1f-1ubuntu2.8 [1320 kB]
Get:2 http://archive.ubuntu.com/ubuntu focal-updates/main amd64
openssl amd64 1.1.1f-1ubuntu2.8 [620 kB]
...
Fetched 5447 kB in 1s (3882 kB/s)
Selecting previously unselected package libssl1.1:amd64.
(Reading database ... 4127 files and directories currently installed.)
Preparing to unpack .../00-libssl1.1_1.1.1f-1ubuntu2.8_amd64.deb ...
Unpacking libssl1.1:amd64 (1.1.1f-1ubuntu2.8) ...
...
Setting up libkeyutils1:amd64 (1.6-6ubuntu1) ...
...
Processing triggers for ca-certificates (20210119~20.04.1) ...
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done. 
Running hooks in /etc/ca-certificates/update.d...
Done.
安装 curl 包。
apt 的第一步是确定依赖关系。
这里 apt 提供了一个依赖项摘要,并告诉我们它将安装什么。这里需要交互确认;在脚本中,我会使用 apt install curl -y 来自动接受。
在确认所有依赖项和主要包已安装后,apt 报告成功。
最后,我们验证 curl 包:
# apt show curl
Package: curl
Version: 7.68.0-1ubuntu2.6
Priority: optional
Section: web
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Alessandro Ghedini <ghedo@debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 411 kB
Depends: libc6 (>= 2.17), libcurl4 (= 7.68.0-1ubuntu2.6), zlib1g (>= 1:1.1.4)
Homepage: http://curl.haxx.se
Task: server, cloud-image, ubuntu-budgie-desktop
Download-Size: 161 kB
APT-Manual-Installed: yes
APT-Sources: http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages
Description: command line tool for transferring data with URL syntax
N: There is 1 additional record. Please use the '-a' switch to see it
现在让我们转向特定于编程语言的包管理器。
特定语言的包管理器
还有特定于编程语言的包管理器,比如以下几种:
C/C++
有 许多不同的包管理器,包括 Conan 和 vcpkg
Go
内置包管理(go get、go mod)
Node.js
有 npm 和其他的
Java
有 maven 和 nuts 等其他的
Python
有 pip 和 PyPM
Ruby
有 rubygems 和 Rails
Rust
有 cargo
接下来,让我们看看容器以及如何通过它们来管理应用程序。
容器
在本书的背景下,我们将容器理解为使用 Linux 命名空间、cgroups 和可选的 CoW 文件系统来提供应用程序级别的依赖管理的 Linux 进程组。容器的用途从本地的测试和开发到与分布式系统一起工作——例如,在 Kubernetes 中使用容器化的微服务。
虽然容器对开发人员和系统管理员非常有用,但作为最终用户,您更可能倾向于使用更高级别的工具来管理应用程序——例如,在“现代包管理器”中讨论的工具。
容器在 Linux 中并不新鲜。然而,它们直到 Docker 开始在大约 2014 年左右得到主流采用才如此。在此之前,我们有多次尝试引入容器的努力,通常是面向系统管理员而不是开发人员,包括以下几种:
所有这些方法的共同之处在于它们使用 Linux 内核提供的基本构建模块,比如命名空间或 cgroups,允许用户运行应用程序。
Docker 在概念上进行了创新,并引入了两个开创性的元素:通过容器镜像定义包装的标准化方式,以及人性化的用户界面(例如 docker run)。容器镜像的定义和分发方式,以及容器的执行方式,形成了现在被称为开放容器倡议(OCI)核心规范的基础。在这里讨论容器时,我们关注符合 OCI 的实现。
OCI 容器规范的三个核心是:
定义了运行时需要支持的内容,包括操作和生命周期阶段
定义了如何构建容器镜像,基于元数据和层次
定义容器镜像的发布方式,实际上是在容器仓库工作的方式
与容器相关的另一个概念是不可变性。这意味着一旦配置完成,您在使用过程中不能更改它。换句话说,变更需要创建一个新的(静态)配置和一个新的资源(如进程)。我们将在容器镜像的上下文中重新讨论这一点。
现在您已经对容器在概念上的了解有所了解,让我们更详细地看一下符合 OCI 标准的容器的构建块。
Linux 命名空间
正如我们在第一章中讨论的,Linux 最初对资源有全局视图。为了让进程能够对资源(如文件系统、网络甚至用户)有局部视图,Linux 引入了命名空间。
换句话说,Linux 命名空间关注的是资源的可见性,并可用于隔离操作系统资源的不同方面。在这个上下文中,隔离主要是指进程看到的内容,而不一定是严格的边界(从安全角度来看)。
要创建命名空间,您可以利用三个相关的系统调用:
用于创建能够与父进程共享部分执行上下文的子进程
用于从现有进程中移除共享的执行上下文
用于将现有进程加入到现有命名空间中
上述系统调用采用一系列标志作为参数,使您能够对要创建、加入或离开的命名空间进行精细控制。
CLONE_NEWNS
用于文件系统挂载点。可通过/proc/$PID/mounts可见。自 Linux 2.4.19 开始支持。
CLONE_NEWUTS
用于创建主机名和(NIS)域名隔离。通过uname -n和hostname -f可见。自 Linux 2.6.19 开始支持。
CLONE_NEWIPC
用于执行进程间通信(IPC)资源隔离,如 System V IPC 对象或 POSIX 消息队列。可通过/proc/sys/fs/mqueue、/proc/sys/kernel和/proc/sysvipc可见。自 Linux 2.6.19 开始支持。
CLONE_NEWPID
用于PID 号空间隔离(命名空间内部/PID 号空间外部)。您可以通过/proc/$PID/status收集相关信息。自 Linux 2.6.24 开始支持。
CLONE_NEWNET
用于控制网络系统资源,例如网络设备、IP 地址、IP 路由表和端口号。您可以通过ip netns list、/proc/net和/sys/class/net查看。自 Linux 2.6.29 开始支持。
CLONE_NEWUSER
用于在命名空间内外映射UID+GID。您可以通过id命令和/proc/\(PID/uid_map*和*/proc/\)PID/gid_map查询 UID 和 GID 及其映射。自 Linux 3.8 开始支持。
CLONE_NEWCGROUP
使用以在命名空间中管理cgroups。您可以通过/sys/fs/cgroup、/proc/cgroups和/proc/$PID/cgroup来查看。自 Linux 4.6 起支持。
查看系统中正在使用的命名空间的一种方法如下(输出经过编辑以适应):
$ sudo lsns
NS TYPE NPROCS PID USER COMMAND
4026531835 cgroup 251 1 root /sbin/init splash
4026531836 pid 245 1 root /sbin/init splash
4026531837 user 245 1 root /sbin/init splash
4026531838 uts 251 1 root /sbin/init splash
4026531839 ipc 251 1 root /sbin/init splash
4026531840 mnt 241 1 root /sbin/init splash
4026531860 mnt 1 33 root kdevtmpfs
4026531992 net 244 1 root /sbin/init splash
4026532233 mnt 1 432 root /lib/systemd/systemd-udevd
4026532250 user 1 5319 mh9 /opt/google/chrome/nacl_helper
4026532316 mnt 1 684 systemd-timesync /lib/systemd/systemd-timesyncd
4026532491 mnt 1 688 systemd-resolve /lib/systemd/systemd-resolved
...
下一个容器构建块专注于资源消耗限制和资源使用报告。
Linux cgroups
如果说命名空间关注可见性,cgroups则提供了一种不同的功能:它们是组织进程组的机制。除了层次化组织外,您可以使用 cgroups 来控制系统资源的使用。此外,cgroups 还提供资源使用跟踪;例如,它们显示进程(组)使用了多少 RAM 或 CPU 秒数。将 cgroups 视为声明单元,控制器作为内核代码的一部分,强制执行特定的资源限制或报告其使用情况。
在撰写本文时,内核中有两个版本的 cgroups 可用:cgroups v1 和 v2。cgroup v1 仍然广泛使用,但 v2 最终将取代 v1,因此您应专注于 v2。
cgroup v1
使用cgroup v1,社区采用了即兴的方法,根据需要添加新的 cgroups 和控制器。存在以下 v1 cgroups 和控制器(按年龄排序;请注意文档分散且不一致):
通过cpu cgroup 使用。自 Linux 2.6.24 起支持。
通过cpuacct cgroup 使用。自 Linux 2.6.24 起支持。
允许您为任务分配 CPU 和内存。自 Linux 2.6.24 起支持。
允许您隔离任务的内存行为。自 Linux 2.6.25 起支持。
允许您控制设备文件的使用。自 Linux 2.6.26 起支持。
用于批处理作业管理。自 Linux 2.6.28 起支持。
用于为数据包分配不同的优先级。自 Linux 2.6.29 起支持。
允许您限制块 I/D 的速度。自 Linux 2.6.33 起支持。
允许您收集性能数据。自 Linux 2.6.39 起支持。
允许您动态设置网络流量的优先级。自 Linux 3.3 起支持。
允许您限制 HugeTLB 的使用。自 Linux 3.5 起支持。
用于在达到某一限制后允许 cgroup 层次结构创建新进程。自 Linux 4.3 起支持。
cgroup v2
cgroup v2 是 cgroups 的全新版本,吸取了从 v1 中学到的经验教训。无论是在一致的配置还是在 cgroups 的使用上,以及(集中和统一的)文档使用上都是如此。与 v1 的每进程 cgroup 设计不同,v2 只有单一层次结构,并且所有控制器都以相同的方式管理。以下是 v2 的控制器:
CPU 控制器
调节 CPU 周期的分配,支持不同的模型(权重、最大),并包括使用报告
内存控制器
调节内存分配,支持用户空间内存,内核数据结构如 dentries 和 inodes,以及 TCP socket 缓冲区
I/O 控制器
调节 I/O 资源的分配,包括基于权重和绝对带宽或每秒 I/O 操作(IOPS)限制,报告字节和读/写的 IOPS
进程编号(PID)控制器
与 v1 版本类似
cpuset 控制器
与 v1 版本类似
device 控制器
管理设备文件的访问,基于 eBPF 实现
rdma 控制器
调节 远程直接内存访问(RDMA)资源 的分配和核算
HugeTLB 控制器
与 v1 版本类似
cgroups v2 中还有其他杂项 cgroups,允许对标量资源进行资源限制和跟踪机制(无法像其他 cgroup 资源那样抽象化)。
您可以通过 systemctl 命令以漂亮的树形图查看 Linux 系统中所有 v2 cgroups,如下例所示(输出已简化和编辑以适应):
$ systemctl status 
starlite
State: degraded
Jobs: 0 queued
Failed: 1 units
Since: Tue 2021-09-07 11:49:08 IST; 1 weeks 1 days ago
CGroup: /
├─22160 bpfilter_umh
├─user.slice
│ └─user-1000.slice 
│ ├─user@1000.service
│ │ ├─gvfs-goa-volume-monitor.service
│ │ │ └─14497 /usr/lib/gvfs/gvfs-goa-volume-monitor
...
使用 systemctl 工具渲染 cgroups
systemd 管理的一个具体 cgroup 的示例
cgroups 的另一个有用视角是交互式资源使用,如下所示(输出已编辑以适应):
$ systemd-cgtop
Control Group Tasks %CPU Memory Input/s Output/s
/ 623 15.7 5.8G - -
/docker - - 48.3M - -
/system.slice 122 6.2 1.6G - -
/system.slice/ModemManager.service 3 - 748.0K - -
...
/system.slice/rsyslog.service 4 - 420.0K - -
/system.slice/snapd.service 17 - 5.1M - -
展望未来,随着现代内核版本的广泛应用,cgroups v2 将成为标准。确实有某些发行版,如 Arch,Fedora 31+ 和 Ubuntu 21.10,默认已经支持 v2。
Copy-on-Write 文件系统
容器的第三个构建块是 Copy-on-Write 文件系统,详细讨论见 “Copy-on-Write Filesystems”。这些在构建时使用,将应用程序及其所有依赖项打包成单个、自包含的文件,可以分发。通常与 bind mounts 结合使用,以高效方式将不同依赖项的内容层叠在一起。
Docker
Docker 是由 Docker Inc. 在 2014 年开发并普及的人性化容器实现。借助 Docker,轻松地打包程序及其依赖项,并在从桌面到云端的各种环境中启动它们。Docker 的独特之处不在于构建模块(命名空间、cgroups、CoW 文件系统和绑定挂载)。在 Docker 诞生之前,这些构建模块就已存在。特殊之处在于 Docker 将这些构建模块组合起来,以一种易于使用的方式,通过隐藏管理命名空间和 cgroups 等底层细节的复杂性。
如 图 6-4 所示,并在随后的段落中描述,Docker 中有两个主要概念:镜像和运行中的容器。

图 6-4. Docker 高级架构
容器镜像
包含 JSON 文件中的元数据和有效目录(即层)的压缩归档文件。Docker 守护程序会根据需要从容器注册表拉取容器镜像。
作为运行时产物的容器(例如,应用程序 A/B/C)
您可以启动、停止、杀死和删除它。您可以使用客户端 CLI 工具 (docker) 与 Docker 守护进程进行交互。此 CLI 工具发送命令给守护程序,后者执行相应的操作,如构建或运行容器。
表格 6-2 提供了常用 Docker CLI 命令的简要参考,涵盖了构建时和运行时阶段。要获取完整的参考资料,包括使用案例,请参阅 Docker 文档。
表格 6-2. 常用 Docker 命令
| 命令 | 描述 | 示例 |
|---|---|---|
run |
启动容器 | 作为守护进程运行 NGINX 并在退出时删除容器:docker run -d --rm nginx:1.21 |
ps |
列出容器 | 列出所有容器(包括非运行中的):docker ps -a |
inspect |
显示低级信息 | 查询容器 IP:docker inspect -f '{{.NetworkSettings.IPAddress}}' |
build |
本地生成容器镜像 | 基于当前目录构建镜像并打标签:docker build -t some:tag . |
push |
将容器镜像上传到注册表 | 推送到 AWS 注册表:docker push public.ecr.aws/some:tag |
pull |
从注册表下载容器镜像 | 从 AWS 注册表拉取:docker pull public.ecr.aws/some:tag |
images |
列出本地容器镜像 | 列出特定注册表的镜像:docker images ubuntu |
image |
管理容器镜像 | 删除所有未使用的镜像:docker image prune -all |
现在让我们仔细看一下构建时的产物:Docker 使用的容器镜像。
容器镜像
要定义如何构建容器镜像的指令,您可以使用名为 Dockerfile 的纯文本文件格式。
在 Dockerfile 中可以有不同的指令:
基础镜像
FROM;可以在构建/运行阶段中有多个
元数据
LABEL 用于血统
参数和环境变量
ARGS、ENV
构建时的规格
COPY、RUN 等命令,它们定义了镜像的构建方式,一层一层地构建
运行时的规格
CMD 和 ENTRYPOINT,它们定义了容器的运行方式。
使用 docker build 命令,您可以将代表应用程序的一组文件(无论是源码还是二进制格式)以及 Dockerfile 转换为容器镜像。这个容器镜像是您可以运行或推送到注册表的工件,以便其他人拉取并最终运行。
运行容器
您可以以交互式输入(附加终端)或作为守护进程(后台运行)方式运行容器。docker run 命令接受一个容器镜像和一组运行时输入,例如环境变量、要暴露的端口和要挂载的卷。有了这些信息,Docker 就会创建必要的命名空间和 cgroups,并启动容器镜像中定义的应用程序(CMD 或 ENTRYPOINT)。
接下来,让我们看看 Docker 理论如何应用。
示例:容器化的 greeter 应用
现在让我们把我们的 greeter 应用程序(参见 “运行示例:greeter”)放入容器并运行它。
首先,我们需要定义 Dockerfile,其中包含构建容器镜像的指令:
FROM ubuntu:20.04 
LABEL org.opencontainers.image.authors="Michael Hausenblas" 
COPY greeter.sh /app/ 
WORKDIR /app 
RUN chown -R 1001:1 /app 
USER 1001
ENTRYPOINT ["/app/greeter.sh"] 
使用显式标签(20.04)定义基础镜像。
通过 标签 分配一些元数据。
复制 shell 脚本。这可以是二进制文件、JAR 文件或 Python 脚本。
设置工作目录。
这行及其下一行定义运行应用程序的用户。如果不这样做,它将不必要地以 root 身份运行。
定义要运行的内容,在我们的情况下是 shell 脚本。通过使用 ENTRYPOINT 定义方式,可以通过运行 docker run greeter:1 _SOME_PARAMETER_ 传递参数。
接下来,我们构建容器镜像:
$ sudo docker build -t greeter:1 . 
Sending build context to Docker daemon 3.072kB
Step 1/7 : FROM ubuntu:20.04 
20.04: Pulling from library/ubuntu
35807b77a593: Pull complete
Digest: sha256:9d6a8699fb5c9c39cf08a0871bd6219f0400981c570894cd8cbea30d3424a31f
Status: Downloaded newer image for ubuntu:20.04
---> fb52e22af1b0
Step 2/7 : LABEL org.opencontainers.image.authors="Michael Hausenblas"
---> Running in 6aa921276c3b
Removing intermediate container 6aa921276c3b
---> def717e3352b
Step 3/7 : COPY greeter.sh /app/
---> 5f3eb160fea3
Step 4/7 : WORKDIR /app
---> Running in 698c29938a96
Removing intermediate container 698c29938a96
---> d73572886c13
Step 5/7 : RUN chown -R 1001:1 /app
---> Running in 5b5eb5d1935a
Removing intermediate container 5b5eb5d1935a
---> 42c35a6db6e2
Step 6/7 : USER 1001
---> Running in bec92deaac6e
Removing intermediate container bec92deaac6e
---> b6e0e27f253b
Step 7/7 : CMD ["/app/greeter.sh"]
---> Running in 6d3b439f7e50
Removing intermediate container 6d3b439f7e50
---> 433a5f10d84e
Successfully built 433a5f10d84e
Successfully tagged greeter:1
构建容器镜像并加标签(使用 -t greeter:1)。. 表示它使用当前目录,并假定那里有一个 Dockerfile。
这些以及接下来的行,逐层拉取基础镜像并构建它。
让我们检查一下容器镜像是否存在:
$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
greeter 1 433a5f10d84e 35 seconds ago 72.8MB
ubuntu 20.04 fb52e22af1b0 2 weeks ago 72.8MB
现在我们可以基于greeter:1镜像运行一个容器,如下所示:
$ sudo docker run greeter:1
You are awesome!
这样我们就结束了 Docker 101。现在让我们快速查看相关的工具。
其他容器工具
您不必使用 Docker 来处理 OCI 容器;作为替代方案,您可以使用由 Red Hat 主导和赞助的组合:podman和buildah。这些无守护进程的工具允许您构建 OCI 容器镜像(buildah)并运行它们(podman)。
此外,还有许多工具可以更轻松地处理 OCI 容器、命名空间和 cgroups,包括但不限于以下内容:
一个管理 OCI 容器生命周期的守护程序,从镜像传输和存储到容器运行时的监督。
用于容器镜像操作(复制、检查清单等)
一种支持 cgroups 的top变种,可以交互式地显示资源使用情况。
允许您在指定的现有命名空间中执行程序。
允许您使用特定命名空间运行程序(通过标志进行选择)。
列出有关 Linux 命名空间的信息。
列出与进程 ID 相关联的 Linux 命名空间和 cgroups 的信息。
这样我们结束了容器之旅。现在让我们看看现代包管理器以及它们如何利用容器来隔离应用程序。
现代包管理器
除了传统的通常特定于发行版的包管理器外,还有一种新型的包管理器。这些现代解决方案通常利用容器,并旨在跨发行版或针对特定环境。例如,它们可以让 Linux 桌面用户轻松安装 GUI 应用程序。
由 Canonical Ltd.设计和推广的软件打包和部署系统。它配备了一个精细的sandboxing设置,并可在桌面、云和物联网环境中使用。
针对 Linux 桌面环境进行优化,使用 cgroups、命名空间、绑定挂载和 seccomp 作为其构建基础。尽管最初来自 Linux 发行版宇宙的 Red Hat 部分,现在已经适用于数十个发行版,包括 Fedora、Mint、Ubuntu、Arch、Debian、openSUSE 和 Chrome OS。
已经存在多年,并推广了一个应用程序等于一个文件的理念;也就是说,除了目标 Linux 系统中包含的内容外,它不需要任何依赖关系。随着时间的推移,AppImage 逐渐引入了许多有趣的功能,从高效更新到桌面集成再到软件目录。
起源于 macOS 世界,但可在 Linux 上使用并越来越受欢迎。它是用 Ruby 编写的,具有强大而直观的用户界面。
结论
在本章中,我们涵盖了与如何在 Linux 上安装、维护和使用应用程序相关的广泛主题。
我们首先定义了基本的应用程序术语,然后讨论了 Linux 启动过程,探讨了 systemd,这是管理启动和组件的现行标准方式。
为了分发应用程序,Linux 使用包和包管理器。我们在这个背景下讨论了各种管理器,以及如何在开发和测试中使用容器以及依赖管理。Docker 容器利用 Linux 原语(cgroups,命名空间,CoW 文件系统)提供应用级依赖管理(通过容器镜像)。
最后,我们探讨了应用程序管理的定制解决方案,包括 Snap 和其他方案。
如果您对本章中的主题感兴趣,并希望进一步阅读,请查看以下资源:
启动过程和初始化系统
包管理
容器
现在您已经了解了关于应用程序的所有基础知识,让我们从单个 Linux 系统的范围转移到互联设置及其必要的前提条件:网络。
第七章:网络
在本章中,我们详细介绍了 Linux 网络。在现代环境中,Linux 提供的网络堆栈是一个重要的组成部分。没有它,几乎无法完成任何事情。无论您是想访问云提供商中的实例、浏览网页还是安装新应用程序,您都需要连接性,并且需要一种与之交互的方式。
我们首先来看看常见的网络术语,从硬件级别一直到用户界面组件,例如 HTTP 和 SSH。我们还将讨论网络堆栈、协议和接口。具体来说,我们将花时间讨论网络和更广泛的互联网的命名核心组成部分,即所谓的域名系统(DNS)。有趣的是,这个系统不仅在广域部署中发挥作用,而且还是诸如 Kubernetes 等容器环境中用于服务发现的中心组件。
接下来,我们将看看应用层网络协议和工具。这包括文件共享、网络文件系统以及其他通过网络共享数据的方法。
在本章的最后部分,我们将回顾一些高级网络主题,从地图制图到网络时间管理。
要设定本章内容的期望:您可以花费大量时间研究 Linux 网络;事实上,有整本书专门讲述这个主题。在这里,我们将采取一种务实的立场,从最终用户的角度进入实际操作的主题。关于网络的管理员主题,例如网络设备的配置和设置,大部分都超出了本章的范围。
现在,让我们把注意力转向网络基础知识。
Basics
让我们首先讨论为何网络对多个用例至关重要,并定义一些常见的网络术语。
在现代环境中,网络在其中起着核心作用。这涵盖了诸如安装应用程序、浏览网页、查看邮件或社交媒体,以及与远程机器工作(从您连接到的嵌入式系统到云提供商数据中心中运行的服务器)。鉴于网络有许多移动部件和层次,要确定问题是硬件相关还是软件堆栈中的原因可能会很困难。
Linux 网络面临的另一个挑战来自于抽象化:本章涵盖的许多内容提供了一个高级用户界面,使得实际上在远程机器上运行的文件或应用程序似乎可以在本地机器上访问或操作。虽然提供使远程资源看起来像本地可访问的抽象是一个有用的特性,但我们不应忘记,归根结底,所有这些都归结为通过电线和空气传输的比特流。在故障排除或测试时请记住这一点。
图 7-1 展示了在高层次上,Linux 中网络是如何工作的。有各种网络硬件,如以太网或无线网卡;然后是一些内核级组件,如 TCP/IP 协议栈;最后,在用户空间中,有一系列工具用于配置、查询和使用网络。

图 7-1. Linux 网络概述
现在让我们深入了解 TCP/IP 协议栈,这是 Linux 网络的核心。
提示
与 Linux 的其他领域不同,在网络空间,你需要查阅源代码或者希望接口和协议背后有正确的文档化设计假设。几乎每个协议和接口都是基于公开的规范。互联网工程任务组(IETF)通过 datatracker.ietf.org 免费提供所有这些请求评论(RFC)。
养成在深入实现细节之前简单阅读这些 RFC 的习惯。这些 RFC 是由实践者为实践者撰写的,并记录了良好的实践方法以及如何实现相关内容。不要害怕逐步阅读它们;你会对动机、用例以及事物为何成为现状有更好的理解。
TCP/IP 协议栈
TCP/IP 协议栈,如 图 7-2 所示,是一个由多个协议和工具组成的分层网络模型,主要由 IETF 规范定义。每一层必须意识到并能够与其上下相邻的层通信。数据封装在数据包中,每一层通常在数据上添加包含其功能相关信息的头部。因此,如果应用程序想要发送数据,它会直接与最高层交互,逐层添加头部(发送路径)。相反,如果应用程序想要接收数据,数据会到达最低层,然后每一层依次处理并根据找到的头部信息将有效载荷传递到上一层(接收路径)。

图 7-2. TCP/IP 层共同工作以实现通信
从协议栈的底部开始,TCP/IP 协议栈的四层如下:
链路层
在协议栈的最低层,该层涵盖硬件(以太网、WiFi)和内核驱动程序,重点是如何在物理设备之间发送数据包。详见 “链路层”。
互联网层
使用互联网协议(IP),该层侧重于路由;即它支持在网络间的机器之间发送数据包。我们将在 “互联网层” 中讨论它。
传输层
这一层控制(虚拟或物理)主机之间的端到端通信,使用传输控制协议(TCP)进行基于会话的可靠通信和用户数据报协议(UDP)进行无连接通信。它主要处理数据包如何传输,包括通过端口寻址机器上的各个服务以及数据完整性。此外,Linux 支持套接字作为通信端点。请参阅“传输层”。
应用层
该层处理用户面向的工具和应用程序,如 Web、SSH 和邮件。我们将在“DNS”和“应用层网络”中讨论它。
分层意味着每一层的头部和有效负载构成下一层的有效负载。例如,看一下图 7-2,互联网层中的有效负载是传输层头部 H[T]及其有效负载。换句话说,互联网层接收来自传输层的数据包,将其视为不透明的字节块,并可以专注于其功能,即将数据包路由到目标机器。
现在让我们从 TCP/IP 协议栈的最低层开始工作,即链路层。
链路层
在 TCP/IP 协议栈的链路层中,所有内容都涉及硬件或接近硬件的内容,如字节、电线、电磁波、设备驱动程序和网络接口。在这个上下文中,您会遇到以下术语:
以太网
一系列使用电线连接机器的网络技术;通常用于局域网(LAN)中。
无线
也被称为 WiFi,一类通信协议和方法,不使用电线,而是使用电磁波来传输数据。
MAC 地址
缩写为媒体访问控制,MAC 是硬件的唯一 48 位标识符,用于识别您的机器(确切地说是网络接口;请参阅以下术语)。MAC 地址通过组织唯一标识符(OUI)编码制造商(接口的制造商),通常占据前 24 位。
接口
网络连接。它可以是物理接口(有关详细信息,请参阅“网络接口控制器”)或虚拟(软件)接口,如环回接口lo。
了解了这些基础知识,我们来更深入地了解链路层。
网络接口控制器
一个必要的硬件设备是网络接口控制器 (NIC),有时也称为网络接口卡。NIC 通过有线标准(例如,以太网的 IEEE 802.3-2018 标准)或来自IEEE 802.11 家族的众多无线标准,提供与网络的物理连接。一旦连接到网络,NIC 将要发送的字节的数字表示转换为电气或电磁信号。对于接收路径,NIC 将接收到的物理信号转换为软件可以处理的位和字节。
让我们看看 NIC 的操作。传统上,人们会使用(现在广泛被视为已废弃的)ifconfig命令查询系统上可用的 NIC 的信息(我们在此首先展示出来是为了教育目的;实际上,最好使用ip,如下一个示例所示):
$ ifconfig
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 7218 bytes 677714 (677.7 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 7218 bytes 677714 (677.7 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
wlp1s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 
inet 192.168.178.40 netmask 255.255.255.0 broadcast 192.168.178.255
inet6 fe80::be87:e600:7de7:e08f prefixlen 64 scopeid 0x20<link>
ether 38:de:ad:37:32:0f txqueuelen 1000 (Ethernet)
RX packets 2398756 bytes 3003287387 (3.0 GB)
RX errors 0 dropped 7 overruns 0 frame 0
TX packets 504087 bytes 85467550 (85.4 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
这里的第一个接口是lo,具有 IP 地址127.0.0.1(参见“IPv4”)。最大传输单元(MTU)是数据包大小,这里为 65,536 字节(较大的尺寸意味着更高的吞吐量);出于历史原因,以太网的默认值为 1,500 字节,但您可以使用巨型帧,大小为 9,000 字节。
报告的第二个接口是wlp1s0,分配了 IPv4 地址192.168.178.40。此接口是一个 NIC,并且具有 MAC 地址(ether为38:de:ad:37:32:0f)。查看标志(<UP,BROADCAST,RUNNING,MULTICAST>),看起来它是可操作的。
对于以相同方式(查询接口并检查其状态)更现代的方法,请使用ip命令。在本章中,我们将经常使用这种方法(已编辑输出以适应):
$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue 
state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: wlp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue 
state UP mode DORMANT group default qlen 1000
link/ether 38:de:ad:37:32:0f brd ff:ff:ff:ff:ff:ff
环回接口。
我的 NIC,MAC 地址为38:de:ad:37:32:0f。请注意,此处的名称(wlp1s0)告诉您有关接口的一些信息:它是 PCI 总线 1 中的无线接口(wl),插槽 0(s0)。这种命名使接口名称更加可预测。换句话说,如果您有两个旧式接口(比如eth0和eth1),重启或添加新卡可能会导致 Linux 重新命名这些接口。
对于ifconfig和ip link,您可能对标志(如LOWER_UP或MULTICAST)的含义感兴趣;这些内容在netdevice 手册页中有详细记录。
地址解析协议
地址解析协议(ARP)将 MAC 地址映射到 IP 地址。在某种意义上,它将链路层与其上层,即互联网层,桥接起来。
让我们看看它的实际操作:
$ arp 
Address HWtype HWaddress Flags Mask Iface
mh9-imac.fritz.box ether 00:25:4b:9b:64:49 C wlp1s0
fritz.box ether 3c:a6:2f:8e:66:b3 C wlp1s0
使用arp命令显示将 MAC 地址映射到主机名或 IP 地址的缓存。请注意,您可以使用arp -n来阻止主机名解析,并显示 IP 地址而不是 DNS 名称。
或者,使用更现代的方法,使用ip:
$ ip neigh 
192.168.178.34 dev wlp1s0 lladdr 00:25:4b:9b:64:49 STALE
192.168.178.1 dev wlp1s0 lladdr 3c:a6:2f:8e:66:b3 REACHABLE
使用ip命令显示将 MAC 地址映射到 IP 地址的缓存。
要显示、配置和排除无线设备问题,您可以使用iw命令。例如,我知道我的无线网卡叫做wlp1s0,因此我可以查询它:
$ iw dev wlp1s0 info 
Interface wlp1s0
ifindex 2
wdev 0x1
addr 38:de:ad:37:32:0f
ssid FRITZ!Box 7530 QJ 
type managed
wiphy 0
channel 5 (2432 MHz), width: 20 MHz, center1: 2432 MHz 
txpower 20.00 dBm
显示有关无线接口wlp1s0的基本信息。
该接口连接的路由器(也请参阅下一个示例)。
该接口使用的 WiFi 频段。
此外,我可以这样收集与路由器和流量相关的信息:
$ iw dev wlp1s0 link 
Connected to 74:42:7f:67:ca:b5 (on wlp1s0)
SSID: FRITZ!Box 7530 QJ
freq: 2432
RX: 28003606 bytes (45821 packets) 
TX: 4993401 bytes (15605 packets)
signal: -67 dBm
tx bitrate: 65.0 MBit/s MCS 6 short GI
bss flags: short-preamble short-slot-time
dtim period: 1
beacon int: 100
显示有关无线接口wlp1s0的连接信息。
这和下一行发送(TX表示“发送”)和接收(RX)的统计信息——即通过此接口发送和接收的字节和数据包。
现在我们对 TCP/IP 堆栈的最低层,即数据链路层,有了很好的掌握,让我们向上移动堆栈。
互联网层
TCP/IP 堆栈的第二低层,即互联网层,负责从网络上的一台机器路由数据包到另一台机器。互联网层的设计假定可用的网络基础设施是不可靠的,并且参与者(例如网络中的节点或它们之间的连接)频繁变化。
互联网层提供尽力而为的传输(即,不保证性能),并将每个数据包视为独立的。因此,更高层次的传输层通常负责处理地址和可靠性问题,包括数据包顺序、重试或传输保证。
在这一层中,用于在全球范围内唯一逻辑标识机器的主导协议是互联网协议(IP),它有两种版本,即 IP 版本 4(IPv4)和 IP 版本 6(IPv6)。
IPv4
IPv4 定义了唯一的 32 位数字,用于标识在 TCP/IP 通信中作为端点的主机或进程。
写 IPv4 地址的一种方式是将 32 位数分为四个由句点分隔的 8 位段,每个段在 0 到 255 范围内,称为八位字节(提示该段覆盖了 8 位)。让我们看一个具体的例子:
63.32.106.149
\_/\_/ \_/ \_/
| | | └─ 
| | └───── 
| └───────── 
└──────────── 
第一个八位字节的二进制形式:00111111
第二个八位字节的二进制形式:00100000
第三个八位字节的二进制形式:01101010
第四个八位字节的二进制形式:10010101
IP 头部(图 7-3)如 RFC 791 和相关的 IETF 规范所定义,有若干字段,但以下是你应该了解的最重要的几个:
源地址(32 位)
发送方的 IP 地址
目的地地址(32 位)
接收方的 IP 地址
协议(8 位)
负载类型(下一高层类型),根据 RFC 790,例如 TCP、UDP 或 ICMP
存活时间,又称 TTL(8 位)
数据包允许存在的最大时间
服务类型(8 位)
可用于服务质量(QoS)目的

图 7-3. 根据 RFC 791 的 IP 头部格式
鉴于互联网是一个网络的网络,将网络和网络中的单个机器(主机)区分开来似乎是很自然的。IP 地址范围分配给网络,而在这些网络中又分配给个别主机。
如今,无类域间路由(CIDR) 是分配 IP 地址的唯一相关方法。CIDR 格式由两部分组成:
-
第一部分表示网络地址。这看起来像一个普通的 IP 地址,例如
10.0.0.0。 -
第二部分定义了地址范围内有多少位(及相应的 IP 地址)—例如
/24。
因此,一个完整的 CIDR 范围示例如下所示:
10.0.0.0/24
在上面的示例中,前 24 位(或三个八位字节)表示网络,剩余的 8 位(总共 32 位减去用于网络的 24 位)是为 256 个主机(2⁸)提供的 IP 地址。此 CIDR 范围内的第一个 IP 地址是 10.0.0.0,最后一个 IP 地址是 10.0.0.255。严格来说,只有地址 10.0.0.1 到 10.0.0.254 可以分配给主机,因为 .0 和 .255 地址被保留用于特殊目的。此外,我们可以说子网掩码是 255.255.255.0,因为这是代表网络的前 24 位。
实际上,你不需要记住这里的所有数学。如果你每天都处理 CIDR 范围,那么你只需了解,如果你是个偶尔使用者,你可能希望使用一些工具。如果你想进行 CIDR 范围计算,比如确定一个范围内有多少个 IP 地址,下面是可以使用的:
-
在 https://cidr.xyz 和 https://ipaddressguide.com/cidr 等在线工具中
-
像 mapcidr 和 cidrchk(由本人开发) 这样的命令行工具
还有一些显著的 保留的 IPv4 地址 你应该知道:
127.0.0.0
此子网保留用于本地地址,最突出的是环回地址 127.0.0.1。
169.254.0.0/16(169.254.0.0 到 169.254.255.255)
这些是本地链路地址,意味着发送到这里的数据包不应该被转发到网络的其他部分。一些云服务提供商如亚马逊网络服务使用这种地址来提供特殊服务(元数据)。
224.0.0.0/24 (224.0.0.0 到 239.255.255.255)
这个范围是保留用于多播。
RFC 1918 定义了私有 IP 范围。私有 IP 范围意味着其中的 IP 地址在公共互联网上不可路由,因此在内部分配这些地址是安全的(例如,在公司的上下文中)。
-
10.0.0.0到10.255.255.255(10/8前缀) -
172.16.0.0到172.31.255.255(172.16/12前缀) -
192.168.0.0到192.168.255.255(192.168/16前缀)
另一个有趣的 IPv4 地址是0.0.0.0。这是一个非路由地址,根据上下文有不同的用例和意义,但从服务器的角度来看,最重要的是0.0.0.0指代机器上所有 IPv4 地址的意思。这是一个很好的方式来表达“在所有可用 IP 地址上监听”。
那是很多干巴巴的理论;让我们看看它如何运行。我们将从查询与 IP 相关的机器开始(输出已编辑):
$ ip addr show 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo 
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: wlp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc
noqueue state UP group default qlen 1000
link/ether 38:de:ad:37:32:0f brd ff:ff:ff:ff:ff:ff
inet 192.168.178.40/24 brd 192.168.178.255 scope global dynamic 
noprefixroute wlp1s0
valid_lft 863625sec preferred_lft 863625sec
inet6 fe80::be87:e600:7de7:e08f/64 scope link noprefixroute
valid_lft forever preferred_lft forever
列出所有接口的地址。
回环接口的 IP 地址 (127.0.0.1,如预期所示)。
无线网卡的(私有)IP 地址。请注意,这是机器的局域网本地 IP 地址,不可公开路由,因为它属于192.168/16范围。
IPv4 地址空间已经枯竭,考虑到今天的端点比互联网设计者预想的多得多(例如移动设备和物联网设备),我们需要一个可持续的解决方案。
IPv6 解决了地址耗尽问题,这是一大利好。遗憾的是,截至目前,由于基础设施问题以及缺乏支持 IPv6 的工具,整体生态系统仍未全面过渡到 IPv6。这意味着在当前时期,你仍需处理 IPv4 及其限制和解决方案。
让我们来看看(希望不会太遥远的)未来:IPv6。
IPv6
IPv6 互联网协议版本 6 是一种 128 位数,用于标识 TCP/IP 通信中的端点。这意味着使用 IPv6,我们可以分配大约 10³⁸个独立的机器(设备)。与 IPv4 不同,IPv6 使用十六进制表示,每组 16 位,用冒号(:)分隔。
IPv6 地址有几条缩写规则,例如去掉前导零或者用两个冒号(::)替换连续的零段。例如,IPv6 的回环地址可以简写为::1(IPv4 的变体是127.0.0.1)。
就像 IPv4 一样,IPv6 有许多特殊和保留地址;请参见 APNIC 的IPv6 地址类型列表以获取示例。
需要注意的是 IPv4 和 IPv6 不兼容。这意味着 IPv6 支持需要内置到每个网络参与者中,从边缘设备(如您的手机)到路由器到服务器软件。至少在 Linux 的背景下,IPv6 支持已经非常广泛。例如,我们在“IPv4”部分看到的ip addr命令已经默认显示了 IPv6 地址。
Internet Control Message Protocol
RFC 792 定义了 Internet Control Message Protocol(ICMP),用于低级组件发送错误消息和操作信息,例如可用性。
让我们通过使用ping测试网站的可达性来看 ICMP 如何工作:
$ ping mhausenblas.info
PING mhausenblas.info (185.199.109.153): 56 data bytes
64 bytes from 185.199.109.153: icmp_seq=0 ttl=38 time=23.140 ms
64 bytes from 185.199.109.153: icmp_seq=1 ttl=38 time=23.237 ms
64 bytes from 185.199.109.153: icmp_seq=2 ttl=38 time=23.989 ms
64 bytes from 185.199.109.153: icmp_seq=3 ttl=38 time=24.028 ms
64 bytes from 185.199.109.153: icmp_seq=4 ttl=38 time=24.826 ms
64 bytes from 185.199.109.153: icmp_seq=5 ttl=38 time=23.579 ms
64 bytes from 185.199.109.153: icmp_seq=6 ttl=38 time=22.984 ms
^C
--- mhausenblas.info ping statistics ---
7 packets transmitted, 7 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 22.984/23.683/24.826/0.599 ms
或者,您可以使用gping,它可以同时对多个目标进行 ping 测试,并在命令行上绘制图表(参见图 7-4)。

图 7-4. 使用gping对两个网站进行 ping 测试
请注意,IPv6 也有一个等效的工具:恰如其名的ping6。
路由
Linux 的一部分网络堆栈涉及路由,即决定每个数据包发送到哪里。目的地可以是同一台机器上的一个进程,也可以是不同机器上的 IP 地址。
尽管路由的确切实现细节超出了本章的范围,我们将提供一个高层次的概述:iptables,这是一个广泛使用的工具,允许您操纵路由表,例如根据条件重新路由数据包或实施防火墙,它使用netfilter来拦截和操纵数据包。
您应该知道如何查询和显示路由信息,如下所示:
$ sudo route -n 
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.178.1 0.0.0.0 UG 600 0 0 wlp1s0
169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 wlp1s0
192.168.178.0 0.0.0.0 255.255.255.0 U 600 0 0 wlp1s0
使用route命令并使用-n,强制使用数值 IP 地址。
前一个route命令中表格输出的详细含义如下:
目的地
目的地的 IP 地址;0.0.0.0表示未指定或未知,可能会将其发送到网关。
网关
对于不在同一网络上的数据包,网关地址。
Genmask
使用的子网掩码。
标志
UG 意味着网络已连接并且是一个网关。
接口
数据包要使用的网络接口。
一个现代的方法是使用ip,像这样:
$ sudo ip route
default via 192.168.178.1 dev wlp1s0 proto dhcp metric 600
169.254.0.0/16 dev wlp1s0 scope link metric 1000
192.168.178.0/24 dev wlp1s0 proto kernel scope link src 192.168.178.40 metric 600
它是下线了吗?我们可以检查连接性如下:
$ traceroute mhausenblas.info
traceroute to mhausenblas.info (185.199.108.153), 30 hops max, 60 byte packets
1 _gateway (192.168.5.2) 1.350 ms 1.306 ms 1.293 ms
请注意,我们将在“监控”中讨论许多与 TCP/IP 相关的故障排除和性能工具。
最后,我也会简要提到边界网关协议(BGP),如RFC 4271所定义以及其他 IETF 规范。虽然您不太可能直接与 BGP 交互(除非您在网络提供商工作或管理网络),但了解其存在并高层次理解其作用至关重要。
我们之前说过,互联网实际上是一个网络的网络。在 BGP 术语中,网络称为自治系统(AS)。为了使 IP 路由工作,这些 AS 需要共享它们的路由和可达性数据,宣布路由以跨互联网传递数据包。
现在您已经了解了互联网层的基本工作原理——地址和路由如何工作——让我们上升到堆栈的更高层。
传输层
在这一层,重点在于端点之间通信的性质。有连接导向的协议和无连接的协议。可靠性、QoS 和顺序传递可能是一个问题。
注意
在现代协议设计中,有一些尝试——例如,HTTP/3——结合功能,例如将 TCP 的某些部分移到更高级别的协议中。
端口
这一层的一个核心概念是端口。无论在这一层使用哪种协议,每个协议都需要端口。端口是一个唯一的 16 位数,用于标识 IP 地址上可用的服务。可以这样理解:单个(虚拟)机器可能运行多个服务(参见“应用层网络”),您需要能够在机器的 IP 上识别每个服务。
我们区分以下内容:
熟知端口(从 0 到 1023)
这些是用于守护进程(如 SSH 服务器或 Web 服务器)的端口。使用(绑定到)其中一个端口需要提升的权限(root 或 CAP_NET_BIND_SERVICE 能力,如在“能力”讨论)。
注册端口(从 1024 到 49151)
这些由互联网分配号码管理局(IANA)通过公开文档化的过程管理。
临时端口(从 49152 到 65535)
这些无法注册。它们可以用于自动分配临时端口(例如,如果您的应用连接到 Web 服务器,它本身需要一个端口,作为通信的另一端点),以及用于私有(例如,公司内部)服务。
您可以在 /etc/services 中看到端口和映射,并且还有一个详尽的TCP 和 UDP 端口号列表供您参考,如果您不确定的话。
如果您想查看本地机器上正在使用的内容(请勿在他人的机器上/对非本地 IP 执行此操作):
$ nmap -A localhost 
Starting Nmap 7.60 ( https://nmap.org ) at 2021-09-19 14:53 IST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.00025s latency).
Not shown: 999 closed ports
PORT STATE SERVICE VERSION
631/tcp open ipp CUPS 2.2 
| http-methods:
|_ Potentially risky methods: PUT
| http-robots.txt: 1 disallowed entry
|_/
|_http-server-header: CUPS/2.2 IPP/2.1
|_http-title: Home - CUPS 2.2.7
Service detection performed. Please report any incorrect results
at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 6.93 seconds
扫描本地机器上的端口。
发现一个开放的端口,631,即互联网打印协议(IPP)。
介绍了端口的一般概念后,让我们看看这些端口在不同的传输层协议中是如何使用的。
传输控制协议
传输控制协议(TCP)是一个面向连接的传输层协议,被许多高层协议使用,包括 HTTP 和 SSH(见“应用层网络”)。它是一个基于会话的协议,保证数据包的有序传输并支持错误重传。
TCP 头部(如图 7-5 所示),定义在RFC 793和相关的 IETF 规范中,其最重要的字段包括:
源端口(16 位)
发送方使用的端口。
目标端口(16 位)
接收方使用的端口。
序列号(32 位)
用于管理有序传输。
确认号(32 位)
此号码和SYN和ACK标志是所谓的TCP/IP 三次握手的核心。
标志位(9 位)
最重要的是SYN(同步)和ACK(确认)位。
窗口(16 位)
接收窗口大小
校验和(16 位)
TCP 头部的校验和,用于错误检查。
数据
待传输的有效负载。

图 7-5。根据 RFC 793 定义的 TCP 头部格式
TCP 从建立到终止跟踪连接的状态,发送方和接收方都必须协商某些事项,从发送多少数据(TCP 窗口大小)到 QoS。
从安全的角度来看,TCP 没有任何防御机制。换句话说,负载以明文形式发送,发送方和接收方之间的任何人(设计上有很多跳)都可以检查数据包;详见“Wireshark 和 tshark”,了解如何使用 Wireshark 和tshark检查数据包的详细信息。要启用消息的加密,您需要使用传输层安全性(TLS)协议,理想情况下是第 1.3 版,根据RFC 8446。
接下来,让我们转向最重要的无状态传输层协议:UDP。
用户数据报协议
用户数据报协议(UDP)是一种无连接的传输层协议,允许您发送称为数据报的消息,无需像 TCP 那样进行通信设置(如握手)。然而,它支持数据报的校验和以确保完整性。有许多应用层协议,如 NTP 和 DHCP(见“应用层网络”),以及 DNS(见“DNS”),使用 UDP。
RFC 768定义了 UDP 头部的格式,如图 7-6 所示。其最重要的字段如下:
源端口(16 位)
发送方使用的端口;可选,如果不使用,则使用 0
目标端口(16 位)
接收方使用的端口
长度(16 位)
UDP 头部和数据的总长度
校验和(16 位)
可以选择用于错误检查
数据
数据包的有效载荷

图 7-6。根据 RFC 768 的 UDP 头格式
UDP 是一个非常简单的协议,并且需要在其之上运行的更高级别协议来处理许多 TCP 本身会处理的事情。另一方面,UDP 的开销非常小,可以实现很高的吞吐量。使用起来非常简单;另请参阅UDP 手册页面。
套接字
Linux 提供的高级通信接口是套接字。将它们视为通信中的端点,具有它们自己的独特身份:由 TCP 或 UDP 端口和 IP 地址组成的元组。
如果您想要开发与网络相关的工具或应用程序,很可能只会使用套接字,但至少应该知道如何查询它们。例如,在Docker 守护程序的上下文中,您至少需要了解套接字所需的权限。
让我们看看如何使用ss命令显示与套接字相关的信息。
假设我们想要系统上正在使用的 TCP 套接字的概述:
$ ss -s 
Total: 913 (kernel 0)
TCP: 10 (estab 4, closed 1, orphaned 0, synrecv 0, timewait 1/0), ports 0 
Transport Total IP IPv6 
* 0 - -
RAW 1 0 1
UDP 10 8 2
TCP 9 8 1
INET 20 16 4
FRAG 0 0 0
使用ss命令查询端口(使用-s,我们请求一个摘要)。
TCP 的总结;总共使用了 10 个套接字。
更详细的概述,按类型和 IP 版本分解。
现在,UDP 怎么样?我们可以获取这些信息吗,也许还有一些更详细的信息,例如端点 IP 地址?事实证明,这也可以通过ss实现(输出已编辑):
$ ss -ulp 
State Recv-Q Send-Q Local Address:Port Peer Address:Port
UNCONN 0 0 0.0.0.0:60360 0.0.0.0:*
UNCONN 0 0 127.0.0.53%lo:domain 0.0.0.0:*
UNCONN 0 0 0.0.0.0:bootpc 0.0.0.0:*
UNCONN 0 0 0.0.0.0:ipp 0.0.0.0:*
UNCONN 0 0 0.0.0.0:mdns 0.0.0.0:*
UNCONN 0 0 [::]:mdns [::]:*
UNCONN 0 0 [::]:38359 [::]:*
使用ss:-u参数限制为 UDP 套接字,-l用于选择监听套接字,-p还显示进程信息(在我们的情况下没有)。
在这种情况下,您可能会发现另一个有用的工具(套接字和进程)是lsof。例如,让我们看看我的机器上 Chrome 使用了哪些 UDP 套接字(输出已编辑):
$ lsof -c chrome -i udp | head -5 
COMMAND PID USER FD TYPE DEVICE NODE NAME
chrome 3131 mh9 cwd DIR 0,5 265463 /proc/5321/fdinfo
chrome 3131 mh9 rtd DIR 0,5 265463 /proc/5321/fdinfo
chrome 3131 mh9 txt REG 253,0 3673554 /opt/google/chrome/chrome
chrome 3131 mh9 mem REG 253,0 3673563 /opt/google/chrome/icudtl.dat
chrome 3131 mh9 mem REG 253,0 12986737 /usr/lib/locale/locale-archive
使用lsof与-c来具体选择一个进程的名称,同时限制为使用-i的 UDP。请注意,总体输出可能有几十行;这就是为什么我用管道中的head -5命令将其减少到五行。
通过这样,我们已经涵盖了 TCP/IP 协议栈的三个较低层次。由于应用层有很多工作要做,我专门划分了两个部分来处理它:首先,我们将研究全球范围的命名系统,然后我们将研究一些应用层(或第 7 层)协议和应用程序,比如 Web。
DNS
我们了解到 TCP/IP 协议栈的 Internet 层定义了所谓的 IP 地址,其主要功能是标识机器,无论是虚拟的还是物理的。在"容器"的背景下,我们甚至为每个容器分配 IP 地址。无论是 IPv4 还是 IPv6,数字 IP 地址都面临两个挑战:
-
作为人类,我们通常比起(长)数字更容易记住名称。例如,如果你想与朋友分享一个网站,你只需说这是ietf.org,而不是
4.31.198.44。 -
由于互联网及其应用程序的构建方式,IP 地址经常会发生变化。在传统设置中,您可能会得到一个新的服务器和新的 IP 地址。或者在容器的上下文中,您可能会被重新调度到不同的主机,此时容器将自动分配一个新的 IP 地址。
简而言之,IP 地址难以记住且可能会改变,而名称(用于服务器或服务)保持不变。这个挑战自互联网初期以来就存在,并且自 UNIX 支持 TCP/IP 协议栈以来也是如此。
解决这个问题的方式是在本地(在单台机器的上下文中)通过/etc/hosts维护名称和 IP 地址之间的映射。网络信息中心(NIC)通过 FTP 与所有参与的主机共享一个称为HOSTS.TXT的单一文件。
很快就清楚,这种集中式方法无法跟上不断增长的互联网发展步伐,在 20 世纪 80 年代初设计了分布式系统。保罗·莫卡佩特里斯是首席架构师。
DNS 是互联网上主机和服务的全球分层命名系统。虽然有许多相关的 RFC 文档,但最初的RFC 1034及其通过RFC 1035的实施指南仍然有效,如果您想了解更多动机和设计,请务必阅读它们。
DNS 使用许多术语,但以下是主要概念:
域名空间
一棵以.为根的树状结构,每个树节点和叶子都包含关于特定空间的信息。从叶子到根的路径上的标签(最大长度为 63 个字节)称为完全限定域名(FQDN)。例如,demo.mhausenblas.info. 是一个带有所谓顶级域名.info的 FQDN。请注意,右边的点,即根,通常被省略。
资源记录
域名空间的节点或叶子中的有效负载(请参见"DNS 记录")。
名称服务器
保存有关域树结构信息的服务器程序。如果一个名字服务器拥有完整的空间信息,它被称为权威名字服务器。权威信息被组织成区域。
解析器
响应客户端请求从名称服务器中提取信息的程序。它们是机器本地的,没有为解析器和客户端之间的交互定义明确的协议。通常支持用于解析 DNS 的库调用。
图 7-7 显示了 DNS 系统的完整设置,包括用户程序、解析器和名称服务器,如 RFC 1035 所述。在查询过程中,解析器将从根开始迭代查询权威名称服务器(NS),或者如果支持的话,使用递归查询,其中一个 NS 代表解析器查询其他 NS。

图 7-7。一个完整的 DNS 示例设置
注意
尽管它们仍然存在,但我们通常不在现代系统中使用 DNS 解析器配置 /etc/resolv.conf,特别是在部署 DHCP(见“动态主机配置协议”)时。
DNS 是一个层次命名系统,在其根部署着管理顶级域记录的 13 个根服务器。根之下直接是顶级域名(TLD):
基础设施顶级域
由 IANA 代表 IETF 管理,包括例如example和localhost
通用顶级域名(gTLD)
三个或更多字符的通用域名,例如.org或.com
国家代码顶级域(ccTLD)
适用于分配了两字母 ISO 国家代码的国家或地区。
赞助的顶级域名(sTLD)
为私人机构或组织建立和执行限制使用 TLD 资格的规则,例如.aero和.gov
让我们更仔细地看一些 DNS 的移动部件及其在实践中的使用方法。
DNS 记录
名称服务器管理记录,捕获类型、有效载荷和其他字段,包括生存时间(TTL)、记录应该被丢弃的时间段。您可以将 FQDN 视为节点的地址,将资源记录(RR)视为有效载荷,节点中的数据。
DNS 具有许多记录类型,包括以下最重要的(按字母顺序):
A记录(RFC 1035)和AAAA记录(RFC 3596)
分别是 IPv4 和 IPv6 地址记录,通常用于将主机名映射到主机的 IP 地址。
CNAME记录(RFC 1035)
提供一个名称的规范名称记录到另一个名称的别名。
NS记录(RFC 1035)
将 DNS 区域委托给权威名称服务器使用的名称服务器记录。
PTR记录(RFC 1035)
用于执行反向 DNS 查找的指针记录;与A记录相反。
SRV记录(RFC 2782)
服务定位器记录。它们是通用的发现机制,而不是像传统上硬编码的(例如邮件交换的MX记录类型)。
TXT记录(RFC 1035)
文本记录。这些最初用于任意人类可读文本,但随着时间的推移发现了新的用途。今天,在安全相关 DNS 扩展的背景下,这些记录通常包含机器可读的数据。
也有以星号标签(*)开头的通配符记录—例如,**.mhausenblas.info*—用作匹配不存在名称的请求的通配符。
现在让我们看看这些记录在实践中是什么样子的。DNS 记录以文本形式在区域文件中表示,这些文件由像bind这样的名字服务器读取并作为其数据库的一部分。
$ORIGIN example.com. 
$TTL 3600 
@ SOA nse.example.com. nsmaster.example.com. (
1234567890 ; serial number
21600 ; refresh after 6 hours
3600 ; retry after 1 hour
604800 ; expire after 1 week
3600 ) ; minimum TTL of 1 hour
example.com. IN NS nse 
example.com. IN MX 10 mail.example.com. 
example.com. IN A 1.2.3.4 
nse IN A 5.6.7.8 
www IN CNAME example.com. 
mail IN A 9.0.0.9 
在命名空间中这个区域文件的开始。
所有未定义自己 TTL 的资源记录的默认过期时间(以秒为单位)。
此域的名称服务器。
此域的邮件服务器。
此域名的 IPv4 地址。
名字服务器的 IPv4 地址。
将www.example.com设置为该域的别名,即example.com。
邮件服务器的 IPv4 地址。
将所有讨论的概念整合在一起,我们现在可以理解图示中显示的例子,见 Figure 7-8。这显示了全球域名空间的一部分和一个具体的 FQDN 示例,demo.mhausenblas.info:
.info
由名为Afilias的公司管理的通用顶级域。
mhausenblas.info
我购买的一个域名。在此区域内,我可以任意分配子域。
demo.mhausenblas.info
我为演示目的分配的子域。

图 7-8. 域名空间及示例路径(FQDN)
考虑到在前面的示例中,每个实体(Afilias 或我)只需照看自己的部分,无需协调。例如,要创建demo子域,我只需更改我的区域的 DNS 设置,无需请求 Afilias 的支持或权限。这看似简单的事实正是 DNS 去中心化性质的核心,也是其可扩展性的原因。
现在我们知道域名空间的结构及其节点中的信息是如何表示的,让我们看看如何查询它们。
DNS 查找
有了所有基础设施的支持,主要是名字服务器和解析器,我们现在看看如何执行 DNS 查询。在解析(主要涵盖在 RFC 1034 和 1035 中)的评估和构建中有很多逻辑,但这超出了本书的范围。让我们看看如何在不必理解内部工作的情况下进行查询。
您可以使用host命令查询本地(和全球)名称以将其解析为 IP 地址,反之亦然:
$ host -a localhost 
Trying "localhost.fritz.box"
Trying "localhost"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 49150
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;localhost. IN ANY
;; ANSWER SECTION:
localhost. 0 IN A 127.0.0.1
localhost. 0 IN AAAA ::1
Received 71 bytes from 127.0.0.53#53 in 0 ms
$ host mhausenblas.info 
mhausenblas.info has address 185.199.110.153
mhausenblas.info has address 185.199.109.153
mhausenblas.info has address 185.199.111.153
mhausenblas.info has address 185.199.108.153
$ host 185.199.110.153 
153.110.199.185.in-addr.arpa domain name pointer cdn-185-199-110-153.github.com.
查找本地 IP 地址。
查找 FQDN。
反向查找 IP 地址以找到 FQDN;看起来像是 GitHub CDN。
使用dig命令查找 DNS 记录的更强大方法:
$ dig mhausenblas.info 
; <<>> DiG 9.10.6 <<>> mhausenblas.info
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 43159
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 2, ADDITIONAL: 5
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;mhausenblas.info. IN A
;; ANSWER SECTION: 
mhausenblas.info. 1799 IN A 185.199.111.153
mhausenblas.info. 1799 IN A 185.199.108.153
mhausenblas.info. 1799 IN A 185.199.109.153
mhausenblas.info. 1799 IN A 185.199.110.153
;; AUTHORITY SECTION: 
mhausenblas.info. 1800 IN NS dns1.registrar-servers.com.
mhausenblas.info. 1800 IN NS dns2.registrar-servers.com.
;; ADDITIONAL SECTION:
dns1.registrar-servers.com. 47950 IN A 156.154.132.200
dns2.registrar-servers.com. 47950 IN A 156.154.133.200
dns1.registrar-servers.com. 28066 IN AAAA 2610:a1:1024::200
dns2.registrar-servers.com. 28066 IN AAAA 2610:a1:1025::200
;; Query time: 58 msec
;; SERVER: 172.16.173.64#53(172.16.173.64)
;; WHEN: Wed Sep 15 19:22:26 IST 2021
;; MSG SIZE rcvd: 256
使用dig,查找 FQDN mhausenblas.info 的 DNS 记录。
DNS A 记录。
权威名称服务器。
dig命令有可用的替代方法,特别是dog和nslookup;参见附录 B。
提示
你经常会听到一句话:“问题总在 DNS 上。” 但这是什么意思呢?它涉及故障排除和理解 DNS 是一个具有许多移动部分的分布式数据库。在调试与 DNS 相关的问题时,请考虑记录的 TTL 和在你的应用程序到解析器之间的所有缓存。
在“DNS 记录”中,我们提到了SRV记录类型及其作为通用发现机制的功能。因此,与其在 RFC 中定义一个新的服务记录类型,社区想出了一种通用方法来应对任何即将到来的服务类型。本机制在RFC 2782中描述,解释了如何通过 DNS 传递SRV记录以通信服务的 IP 地址和端口。
让我们来看看实际操作。比如说,我们想知道有哪些聊天服务——更具体地说,可扩展消息传递与状态协议(XMPP)服务——是可用的:
$ dig +short _xmpp-client._tcp.gmail.com. SRV 
20 0 5222 alt3.xmpp.l.google.com.
5 0 5222 xmpp.l.google.com. 
20 0 5222 alt4.xmpp.l.google.com.
20 0 5222 alt2.xmpp.l.google.com.
20 0 5222 alt1.xmpp.l.google.com.
使用dig命令和+short选项仅显示相关的回答部分。_xmpp-client._tcp部分是 RFC 2782 规定的格式,而命令末尾的SRV指定了我们感兴趣的记录类型。
总共有五个回答。例如,一个服务实例可在xmpp.l.google.com:5222找到,TTL 为 5 秒。如果你有像 Jabber 这样的 XMPP 服务,可以使用这个地址进行配置输入。
到这里,我们已经结束了 DNS 部分。现在让我们来看看其他应用层协议和工具。
应用层网络
在本节中,我们专注于用户空间或应用层网络协议、工具和应用程序。作为最终用户,你可能会在这里花费大部分时间,使用诸如网页浏览器或邮件客户端之类的工具完成日常任务。
网络
Web 最初由 Tim Berners-Lee 爵士在 1990 年代初开发,有三个核心组件:
统一资源定位符(URL)
根据RFC 1738以及一些更新和相关的 RFC,URL 定义了 Web 上资源的标识和位置。资源可以是静态页面或动态生成内容的进程。
超文本传输协议(HTTP)
HTTP 定义了应用层协议以及如何通过 URL 与可用内容交互。根据RFC 2616的 v1.1 版本,但也有更现代的版本,例如在本文撰写时正在制定的 HTTP/2,由RFC 7540定义,以及HTTP/3 草案。核心 HTTP 概念包括:
包括GET用于读操作,以及POST等其他操作,这些定义了类似 CRUD 的接口。
这决定了如何形成 URL。
成功的2xx范围,重定向的3xx,客户端错误的4xx,服务器错误的5xx。
超文本标记语言(HTML)
最初是 W3C 规范,HTML 现在是通过WHATWG提供的一个活动标准。超文本标记语言允许您定义页面元素,如标题或输入。
让我们更仔细地看看如何构建 URI(URL 的通用版本)(根据 RFC 3986),以及它如何映射到 HTTP URL:
michaelh:12345678@http://example.com:4242/this/is/the/way?orisit=really#another
\______/ \______/ \__/ \_____________/\______________/ \___________/ \_____/
| | | | | | |
v v v v v v v
user password scheme authority path query fragment
这些组件如下:
user和password(两者都是可选的)
最初用于基本身份验证,这些组件不应再使用。相反,对于 HTTP,您应该使用适当的身份验证机制,以及用于传输中的HTTPS进行加密。
scheme
参考URL 方案,这是一个 IETF 规范,定义了其含义。对于 HTTP 来说,该方案称为http,实际上是一组 HTTP 规范,例如 RFC 2616。
authority
分层命名部分。对于 HTTP 来说,这是:
主机名
可作为 DNS FQDN 或 IP 地址。
端口
默认为 80(因此example.com:80和example.com是相同的)。
path
用于进一步资源细节的方案特定部分。
query和fragment(两者都是可选的)
在非层次数据后出现(例如,用于表示标签或表单数据),后者在#后出现(在 HTML 的上下文中,这可能是一个部分)。
今天,Web 已远远超出其谦逊的 1990 年代根基,许多技术,如JavaScript/ECMAScript和Cascading Style Sheets (CSS),被视为核心。这些新增内容,JavaScript 用于动态客户端内容,CSS 用于样式设计,最终导致了单页 Web 应用程序的诞生。虽然这个话题超出了本书的范围,但重要的是要记住,熟练掌握基础知识(URL、HTTP 和 HTML)对于理解工作原理和解决可能出现的问题至关重要。
现在让我们通过模拟端到端流程来看 Web 规范的实际运作,从 HTTP 服务器端开始。
您可以通过两种方式相当容易地运行一个简单的 HTTP 服务器,它只提供目录内容:使用Python或者使用netcat (nc)。
使用 Python,要提供目录内容,您需要执行以下操作:
$ python3 -m http.server 
Serving HTTP on :: port 8000 (http://[::]:8000/) ... 
::ffff:127.0.0.1 - - [21/Sep/2021 08:53:53] "GET / HTTP/1.1" 200 - 
使用内置的 Python 模块http.server来提供当前目录的内容(即您启动此命令的目录)。
它确认准备通过端口 8000 提供服务。这意味着您可以在浏览器中输入http://localhost:8000,然后在那里看到您目录的内容。
这表明根路径(/)发出了 HTTP 请求并成功提供服务(200 HTTP 响应代码)。
提示
如果您希望执行更高级的操作,超出了提供静态目录内容的范围,请考虑使用像NGINX这样的合适的 Web 服务器。例如,您可以使用以下命令在 Docker 中运行 NGINX(参见“Docker”):
$ docker run --name mywebserver \ 
--rm -d \ 
–v "$PWD":/usr/share/nginx/html:ro \ 
-p 8042:80 \ 
nginx:1.21 
将运行的容器称为mywebserver;当您发出docker ps命令列出运行的容器时,您应该看到它。
--rm参数会在退出时删除容器,而-d会将其变成守护进程(从终端分离,后台运行)。
将当前目录($PWD)挂载到容器中作为 NGINX 源内容目录。请注意,$PWD是 bash 中指代当前目录的方法。在 Fish 中,您应该使用(pwd)。
使容器内部端口 80 通过 8042 在主机上可用。这意味着您可以在您的机器上通过http://localhost:8042访问 Web 服务器。
要使用的容器映像是nginx:1.21,隐含地使用了 Docker Hub,因为我们没有指定注册表部分。
现在让我们看看如何使用curl,这是一个强大且流行的工具,可与任何类型的 URL 交互,以获取我们在前面示例中启动的 Web 服务器的内容(确保它仍在运行,或者如果您已经终止了它,则在单独的会话中再次启动它):
$ curl localhost:8000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href="app.yaml">app.yaml</a></li>
<li><a href="Dockerfile">Dockerfile</a></li>
<li><a href="example.json">example.json</a></li>
<li><a href="gh-user-info.sh">gh-user-info.sh</a></li>
<li><a href="main.go">main.go</a></li>
<li><a href="script.sh">script.sh</a></li>
<li><a href="test">test</a></li>
</ul>
<hr>
</body>
在表格 7-1 中,您可以看到一些对curl有用的常见选项。选择基于我在开发和系统管理任务中的使用历史。
表格 7-1. curl的有用选项
| 选项 | 长选项 | 描述和使用案例 |
|---|---|---|
-v |
--verbose |
用于调试的详细输出。 |
-s |
--silent |
静默模式:不显示进度表或错误消息。 |
-L |
--location |
跟随页面重定向(3XX HTTP 响应代码)。 |
-o |
--output |
默认情况下,内容输出到stdout;如果要直接将其存储在文件中,请通过此选项指定。 |
-m |
--max-time |
最大允许等待时间(以秒为单位)。 |
-I |
--head |
仅获取头部信息(注意:并非所有 HTTP 服务器都支持路径的HEAD方法)。 |
-k |
--insecure |
默认情况下,HTTPS 调用是经过验证的。使用此选项可忽略无法进行验证的错误情况。 |
如果curl不可用,您可以退回到wget,它的功能更为有限,但对于简单的 HTTP 相关交互已经足够。
安全外壳
安全外壳(SSH)是一种用于在不安全网络上安全提供网络服务的加密网络协议。例如,作为telnet的替代,您可以使用ssh登录远程机器,并且还可以在(虚拟)机器之间安全传输数据。
让我们看看 SSH 如何运作。我在云中预置了一个虚拟机,其 IP 地址为63.32.106.149,默认提供的用户名是ec2-user。要登录到这台机器,我可以执行以下操作(请注意,输出已编辑,假定您或其他人已经在~/.ssh/lml.pem中创建了凭据):
$ ssh \ 
-i ~/.ssh/lml.pem \ 
ec2-user@63.32.106.149 
...
https://aws.amazon.com/amazon-linux-2/
11 package(s) needed for security, out of 35 available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-172-26-8-138 ~]$ 
使用ssh命令登录远程机器。
使用身份文件/.ssh/lml.pem*而不是密码。在我们的情况下,明确提供该文件是一种良好的做法,但严格来说并不是必需的,因为它位于默认位置*/.ssh中。
SSH 目标机器的格式为username@host。
登录过程完成后,我可以从提示符中看出我已经进入了目标机器,并且可以像在本地一样使用它。
一些通用的 SSH 使用技巧:
-
如果运行 SSH 服务器,即允许其他人
ssh到您的机器,则绝对应禁用密码认证。这将强制用户创建密钥对,并与您共享公钥,然后您将其添加到~/.ssh/authorized_keys以允许通过此机制登录。 -
使用
ssh -tt强制伪终端分配。 -
当您通过
ssh登录到机器时,执行export TERM=xterm,以防出现显示问题。 -
在客户端为
ssh会话配置超时。通常情况下,可以通过~/.ssh/config在每个用户的基础上设置ServerAliveInterval和ServerAliveCountMax选项以保持连接活动。 -
如果您遇到问题,并且您已经排除了与密钥相关的本地权限问题,则可以尝试使用
ssh -v选项启动ssh,以获取关于底层操作的详细信息(还可以尝试多个v实例,例如-vvv以获取更精细的调试信息)。
SSH 不仅直接由人类使用,而且还作为底层构建模块使用——例如,在文件传输工具中。
文件传输
网络中非常常见的一个任务是文件传输。您可以从本地机器到云中的服务器进行传输,或者从本地网络中的另一台机器进行传输。
要从远程系统复制和复制,可以使用一个基本工具。scp(简称“安全复制”)建立在 SSH 之上。由于scp默认使用ssh,因此我们需要确保已经设置了密码(或者更好的是基于密钥的认证)。
假设我们有一个具有 IPv4 地址63.32.106.149的远程机器,并且我们想从本地机器将文件复制到那里:
$ scp copyme \ 
ec2-user@63.32.106.149:/home/ec2-user/ 
copyme 100% 0 0.0KB/s 00:00
源文件是当前目录中的文件copyme。
目标是位于机器63.32.106.149上的/home/ec2-user/目录。
使用rsync同步文件比scp更方便且更快。在底层,默认情况下rsync使用 SSH。
现在让我们看看如何使用rsync从本地机器的~/data/目录传输文件到主机63.32.106.149:
$ rsync -avz \ 
~/data/ \ 
mh9@:63.32.106.149: 
building file list ... done
./
example.txt
sent 155 bytes received 48 bytes 135.33 bytes/sec
total size is 10 speedup is 0.05
$ ssh ec2-user@63.32.106.149 -- ls 
example.txt
选项意味着-a用于存档(增量,保留),-v用于详细显示以便看到某些内容,-z用于使用压缩。
源目录(因为-a包含递归的-r)。
目标使用user@host格式。
通过在远程机器上执行ls来验证数据是否已到达。下一行显示确实成功了——数据按顺序到达。
如果您不确定rsync会执行什么操作,请在其他选项之外使用--dry-run选项。它基本上会告诉您它会执行的操作,而不实际执行操作,因此很安全。
rsync也是一个非常好的工具,用于执行目录备份,因为它可以设置为仅复制已添加或更改的文件。
警告
不要忘记主机后面的:!如果没有这个符号,rsync将会愉快地将源或目标解释为本地目录。也就是说,命令会正常工作,但是文件不会复制到远程机器上,而是会放在您的本地机器上。例如,user@example.com作为目标将会成为当前目录的一个子目录,名为user@example.com/。
最后但并非最不重要的是,您经常遇到的一个用例是某人在 Amazon S3 存储桶中提供文件。要下载这些文件,您可以使用AWS CLI的s3子命令如下所示。我们正在使用来自开放数据注册表中的公共 S3 存储桶的数据集(输出编辑以适应):
$ aws s3 sync \ 
s3://commoncrawl/contrib/c4corpus/CC-MAIN-2016-07/ \ 
.\ 
--no-sign-request 
download: s3://commoncrawl/contrib/c4corpus/CC-MAIN-2016-07/
Lic_by-nc-nd_Lang_af_NoBoilerplate_true_MinHtml_true-r-00009.seg-00000.warc.gz to
./Lic_by-nc-nd_Lang_af_NoBoilerplate_true_MinHtml_true-r-00009.seg-00000.warc.gz
download: s3://commoncrawl/contrib/c4corpus/CC-MAIN-2016-07/
Lic_by-nc-nd_Lang_bn_NoBoilerplate_true_MinHtml_true-r-00017.seg-00000.warc.gz to
./Lic_by-nc-nd_Lang_bn_NoBoilerplate_true_MinHtml_true-r-00017.seg-00000.warc.gz
download: s3://commoncrawl/contrib/c4corpus/CC-MAIN-2016-07/
Lic_by-nc-nd_Lang_da_NoBoilerplate_true_MinHtml_true-r-00004.seg-00000.warc.gz to
./Lic_by-nc-nd_Lang_da_NoBoilerplate_true_MinHtml_true-r-00004.seg-00000.warc.gz
...
使用 AWS S3 命令从公共存储桶同步文件。
这是源存储桶,s3://commoncrawl,以及我们想要同步的源的确切路径。警告:该目录中有超过 8GB 的数据,所以只有在您不介意带宽的情况下才尝试这样做。
目标是当前目录,由单个句点(.)表示。
忽略/跳过身份验证,因为这是一个公开可用的存储桶(因此其中的数据)。
文件传输协议(FTP),根据RFC 959,仍然在使用中,但我们不建议再使用它。这些不仅不安全,而且还有许多更好的替代方案,例如我们在本节中讨论的那些。因此,实际上不再需要 FTP。
网络文件系统
通过网络文件系统(NFS),一种在网络上从中央位置共享文件的广泛支持和使用的方式,最早由 Sun Microsystems 在 1980 年代初开发。根据RFC 7530和其他相关的 IETF 规范,它经历了多次迭代,并且非常稳定。
在专业设置中,通常会有一个由云提供商或中央 IT 维护的 NFS 服务器。您只需要安装客户端(通常通过一个名为nfs-common的软件包)。然后,您可以按如下方式挂载 NFS 服务器上的源目录:
$ sudo mount nfs.example.com:/source_dir /opt/target_mount_dir
许多云提供商,如 AWS 和 Azure,现在提供 NFS 作为一种服务。这是为您的存储密集型应用程序提供大量空间的一种不错方式,几乎像本地附加存储一样使用和感觉。但是,对于媒体应用程序来说,网络附加存储(NAS)设置可能是更好的选择。
与 Windows 共享
如果您的本地网络中有 Windows 计算机并希望进行共享,您可以使用Server Message Block(SMB),这是 20 世纪 80 年代 IBM 最初开发的协议,或其 Microsoft 拥有的继任者公共 Internet 文件系统(CIFS)。
通常,您会使用Samba,这是 Linux 的标准 Windows 互操作套件,用于实现文件共享。
高级网络主题
在本节中,我们讨论了一些跨 TCP/IP 协议栈的高级网络协议和工具。它们的使用通常超出了普通用户的范围。但是,如果您是开发人员或系统管理员,您可能至少需要了解它们。
whois
whois是用于查找注册和用户信息的 whois 目录服务的客户端。例如,如果我想找出谁在ietf.org域名背后(请注意,您可以向域名注册商支付费用以保护这些信息的私密性),我会执行以下操作:
$ whois ietf.org 
% IANA WHOIS server
% for more information on IANA, visit http://www.iana.org
% This query returned 1 object
refer: whois.pir.org
domain: ORG
organisation: Public Interest Registry (PIR)
address: 11911 Freedom Drive 10th Floor,
address: Suite 1000
address: Reston, VA 20190
address: United States
contact: administrative
name: Director of Operations, Compliance and Customer Support
organisation: Public Interest Registry (PIR)
address: 11911 Freedom Drive 10th Floor,
address: Suite 1000
address: Reston, VA 20190
address: United States
phone: +1 703 889 5778
fax-no: +1 703 889 5779
e-mail: ops@pir.org
...
使用whois查找域的注册信息。
动态主机配置协议
动态主机配置协议(DHCP)是一种网络协议,用于自动为主机分配 IP 地址。这是一种客户端/服务器设置,可消除手动配置网络设备的需求。
设置和管理 DHCP 服务器超出了我们的范围,但您可以使用dhcpdump扫描 DHCP 数据包。为此,您本地网络中的设备需要加入,试图获取 IP 地址,因此您可能需要耐心等待看到某些内容(输出已缩短):
$ sudo dhcpdump -i wlp1s0 
TIME: 2021-09-19 17:26:24.115
IP: 0.0.0.0 (88:cb:87:c9:19:92) > 255.255.255.255 (ff:ff:ff:ff:ff:ff)
OP: 1 (BOOTPREQUEST)
HTYPE: 1 (Ethernet)
HLEN: 6
HOPS: 0
XID: 7533fb70
...
OPTION: 57 ( 2) Maximum DHCP message size 1500
OPTION: 61 ( 7) Client-identifier 01:88:cb:87:c9:19:92
OPTION: 50 ( 4) Request IP address 192.168.178.42
OPTION: 51 ( 4) IP address leasetime 7776000 (12w6d)
OPTION: 12 ( 15) Host name MichaelminiiPad
...
使用dhcpdump,嗅探接口wlp1s0上的 DHCP 数据包。
网络时间协议
网络时间协议(NTP)用于在网络上同步计算机的时钟。例如,使用ntpq命令,一个标准的 NTP 查询程序,您可以像这样进行明确的时间服务器查询:
$ ntpq -p 
remote refid st t when poll reach delay offset jitter
==============================================================================
0.ubuntu.pool.n .POOL. 16 p - 64 0 0.000 0.000 0.000
1.ubuntu.pool.n .POOL. 16 p - 64 0 0.000 0.000 0.000
2.ubuntu.pool.n .POOL. 16 p - 64 0 0.000 0.000 0.000
3.ubuntu.pool.n .POOL. 16 p - 64 0 0.000 0.000 0.000
ntp.ubuntu.com .POOL. 16 p - 64 0 0.000 0.000 0.000
...
ntp17.kashra-se 90.187.148.77 2 u 7 64 1 27.482 -3.451 2.285
golem.canonical 17.253.34.123 2 u 13 64 1 20.338 0.057 0.000
chilipepper.can 17.253.34.123 2 u 12 64 1 19.117 -0.439 0.000
alphyn.canonica 140.203.204.77 2 u 14 64 1 91.462 -0.356 0.000
pugot.canonical 145.238.203.14 2 u 13 64 1 20.788 0.226 0.000
使用-p选项显示已知于该机器的对等体列表,包括其状态。
通常情况下,NTP 在后台运行,由systemd和其他守护程序管理,因此您不太可能需要手动查询它。
Wireshark 和 tshark
如果您想进行低级网络流量分析——也就是说,您想精确查看整个协议栈上的数据包——您可以使用命令行工具tshark或其基于 GUI 的版本wireshark。
例如,通过ip link命令发现我有一个名为wlp1s0的网络接口后,我在那里捕获流量(编辑输出以适应):
$ sudo tshark -i wlp1s0 tcp 
Running as user "root" and group "root". This could be dangerous.
Capturing on 'wlp1s0'
1 0.000000000 192.168.178.40 → 34.196.251.55 TCP 66 47618 → 443
[ACK] Seq=1 Ack=1 Win=501 Len=0 TSval=3796364053 TSecr=153122458
2 0.111215098 34.196.251.55 → 192.168.178.40 TCP 66
[TCP ACKed unseen segment] 443 → 47618 [ACK] Seq=1 Ack=2 Win=283
Len=0 TSval=153167579 TSecr=3796227866
...
8 7.712741925 192.168.178.40 → 185.199.109.153 HTTP 146 GET / HTTP/1.1 
9 7.776535946 185.199.109.153 → 192.168.178.40 TCP 66 80 → 42000 [ACK]
Seq=1 Ack=81 Win=144896 Len=0 TSval=2759410860 TSecr=4258870662
10 7.878721682 185.199.109.153 → 192.168.178.40 TCP 2946 HTTP/1.1 200 OK
[TCP segment of a reassembled PDU]
11 7.878722366 185.199.109.153 → 192.168.178.40 TCP 2946 80 → 42000
[PSH, ACK] Seq=2881 Ack=81 Win=144896 Len=2880 TSval=2759410966 \
TSecr=4258870662
[TCP segment of a reassembled PDU]
...
使用tshark在网络接口wlp1s0上捕获网络流量,并仅查看 TCP 流量。
在另一个会话中,我发出了一个curl命令来触发一个 HTTP 会话,在这个应用层交互开始。您还可以使用功能较弱但更广泛可用的tcpdump来完成此任务。
其他高级工具
有许多高级网络相关工具可供选择,您可能会发现它们很有用,包括但不限于以下内容:
建立了两个双向字节流,并启用了端点之间数据传输。
允许您将 IP 映射到地理区域。
隧道
VPN 和其他站点到站点网络解决方案的易于使用的替代方案。通过inlets等工具启用。
BitTorrent
一个点对点系统,将文件分组成一个称为torrent的包。查看一些客户端,看看这是否适合你的工具箱。
结论
在本章中,我们定义了从硬件层面(如 NIC)到 TCP/IP 协议栈再到应用层用户界面组件(如 HTTP)的常见网络术语。
Linux 提供了一个强大的、基于标准的 TCP/IP 协议栈实现,您可以在编程中使用(例如套接字)以及在设置和查询(通常使用ip命令)的上下文中使用。
我们进一步讨论了组成大多数日常(与网络相关的)流量的应用层协议和接口。在这里,您的命令行朋友包括用于传输的curl和用于 DNS 查找的dig。
如果您想深入研究网络主题,请查看以下资源:
TCP/IP 协议栈
-
理解 Linux 网络内部 作者:克里斯蒂安·贝文努蒂(O'Reilly)
-
伊尔吉奇·范·贝伊纳姆的BGP 专家网站
DNS
应用层和高级网络
有了这些,我们准备好进入书中的下一个主题:利用可观测性避免盲目行动。
第八章:可观测性
您需要了解整个堆栈的运行情况——从内核到面向用户的部分。通常,您可以通过知道任务的正确工具来获得这种可见性。
本章重点讨论收集和利用 Linux 及其应用程序生成的不同信号,以便您能够做出明智的决策。例如,您将看到如何执行以下操作:
-
弄清楚进程消耗了多少内存
-
理解磁盘空间将会在多快的时间内耗尽
-
获取安全事件的自定义事件警报
为了建立共同的词汇表,我们首先将回顾您可能遇到的不同信号类型,例如系统或应用程序日志、指标和进程跟踪。我们还将探讨如何进行故障排除和性能测量。接下来,我们将重点介绍日志,回顾不同的选项和语义。然后,我们将涵盖不同资源类型的监控,例如 CPU 周期、内存或 I/O 流量。我们将审视您可以使用的不同工具,并展示您可能希望采用的特定端到端设置。
您将了解到可观测性通常是一种反应性的。也就是说,某些情况下会出现崩溃或运行缓慢,然后您开始查看进程及其 CPU 或内存使用情况,或者深入查看日志。但有时可观测性更具调查性质——例如,当您想要弄清楚某些算法需要多长时间时。最后但同样重要的是,您可以使用预测性(而不是反应性)的可观测性。例如,您可以在将来的某种条件下收到警报,推测当前行为(对于可预测负载的磁盘使用情况就是一个很好的例子)。
关于可观测性的最佳视觉概述可能来自性能专家 Brendan Gregg。从他的Linux Performance 站点获取的图 8-1 让您感受到可用的各种移动部件和工具的丰富性。

图 8-1. Linux 可观测性概述。来源:Brendan Gregg(在 CC BY-SA 4.0 许可下共享)
可观测性是一个涵盖许多用例和大量(开源)工具的令人兴奋的主题,因此让我们首先建立一个策略,并查看一些常用术语。
基础知识
在我们深入讨论可观测性术语之前,让我们退后一步,看看如何将提供的信息转化为可操作的洞察力,并以结构化方式修复问题或优化应用程序。
可观测性策略
在可观测性背景下广泛使用的一种策略是OODA 循环(观察-定向-决策-行动)。它提供了一种基于观察数据进行假设测试并采取行动的结构化方法——即从信号中获取可操作的洞察力。
例如,假设一个应用程序运行缓慢。我们进一步假设有多种可能的原因(内存不足、CPU 周期太少、网络 I/O 不足等)。首先,您需要能够测量每个资源的消耗情况。然后,您将分别更改每个资源的分配(保持其他资源不变)并测量结果。
如果您为应用程序提供更多的 RAM 后性能有所改善吗?如果是这样,您可能已经找到了原因。如果没有,您将继续尝试其他资源,始终测量消耗并尝试将其与观察到的情况影响联系起来。
术语
在可观测性领域存在一系列术语,^(1)并非所有术语都有正式的定义。此外,如果您观察单个机器或处于网络化(分布式)设置中,其含义可能会略有不同:
可观察性
通过测量外部信息来评估系统(例如 Linux)的内部状态,通常的目的是对其进行响应。例如,如果您注意到系统反应迟钝,并且测量可用主存储器的数量,您可能会发现某个特定的应用程序占用了所有的内存,于是您可能决定终止它以解决问题。
信号类型
表示和发出关于系统状态的信息的不同方式,可以通过符号方式(负载为文本,例如日志的情况)或数值方式(如指标的情况),或者它们的组合。另请参见“信号类型”。
源
生成信号,可能是不同类型的信号。源可以是 Linux 操作系统或应用程序。
目的地
消费、存储和进一步处理信号的地方称为目的地。我们将暴露用户界面(GUI、TUI 或 CLI)的目的地称为前端。例如,日志查看器或绘制时间序列的仪表板就是前端,而 S3 存储桶则不是(但仍可作为日志等信息的目的地)。
遥测
从源中提取信号并将信号传输(或路由、发送)到目的地的过程,通常使用代理收集和/或预处理信号(例如过滤或下采样)。
信号类型
信号是我们用来传达系统状态以供进一步处理或解释的方式。总体而言,我们区分文本负载(最适合人类搜索和解释)和数值负载(对于机器和处理后的人类都很好)。在本章讨论中,与我们讨论相关的三种基本且常见的信号类型是:日志、指标和跟踪。
日志
日志是每个系统在某种程度上都会生成的基本信号类型。日志是离散事件,具有文本载荷,供人类使用。通常,这些事件都有时间戳。理想情况下,日志应该结构化,以便每个日志消息的每个部分都有清晰的含义。这种含义可能通过形式化模式来表达,以便可以自动进行验证。
有趣的是,虽然每个日志都有一些结构(即使它不是明确定义的,解析起来可能很困难,可能是由于分隔符或边缘情况),你经常会听到术语结构化日志。当人们这么说时,他们实际上是指使用 JSON 结构化的日志。
尽管自动化日志内容很难(因为它是文本性质的),但日志对人类仍然非常有用,因此它们可能会在一段时间内继续是主要的信号类型。我们将在“记录”中深入探讨处理日志的内容。日志是最重要的信号类型(对我们的考虑而言),这就是为什么我们在本章中大部分时间都将处理它们。
指标
指标通常是定期采样的数值数据点,形成时间序列。这些单独的数据点可以通过维度或标识元数据提供额外的上下文。通常情况下,你不会直接使用原始指标数据;而是使用某种聚合或图形表示,或者在满足特定条件时得到通知。指标既可用于操作任务,也可用于故障排除,以回答诸如应用完成多少交易或某个操作耗时多长时间(过去 X 分钟)等问题。
我们区分不同类型的指标:
计数器
计数器的值只能不断增加(除非将计数器重置为零)。计数器指标的一个示例是服务处理的总请求数或某段时间内通过接口发送的字节数。
测量值
测量值可以增加或减少。例如,您可以测量当前可用的总内存或运行的进程数。
直方图
构建值分布的复杂方式。使用桶,直方图允许你评估数据的整体结构。它们还使你能够进行灵活的陈述(例如,50% 或 90% 的值落入某个范围内)。
在“监控”中,我们将看到一系列可用于简单用例的工具,在“Prometheus 和 Grafana”中,您将看到一种高级示例设置用于指标。
跟踪
跟踪是运行时信息的动态集合(例如,有关进程使用的系统调用的信息,或者给定原因的内核事件序列)。跟踪通常不仅用于调试,还用于性能评估。我们将在“跟踪和性能分析”中详细研究这个高级主题。
记录
如前所述,日志是(一组)文本有效负载的离散事件,优化供人类消费。让我们分解这个陈述以更好地理解它:
离散事件
在代码库的上下文中,想象一个离散事件。您希望使用(原子性)日志项共享关于代码中正在进行的情况的信息。例如,您发出一个日志行,指示数据库连接已成功建立。另一个日志项可能是因为文件丢失而标记错误。保持日志消息的范围小而具体,这样消费消息的人更容易找到代码中相应的位置。
文本有效负载
日志消息的有效负载是文本性质的。默认的消费者是人类。换句话说,无论您是在命令行上使用日志查看器还是在具有可视化用户界面的高级日志处理系统中,人类读取和解释日志消息的内容,并根据此内容决定采取行动。
从结构上看,总体而言,日志包括以下内容:
一组日志项、消息或行
捕获关于离散事件的信息。
元数据或上下文
可以以每条消息的方式出现,也可以在全局范围(例如整个日志文件)上出现。
一个用于解释单个日志消息的格式
定义日志的部分和含义。例如,面向行的、空格分隔的消息或 JSON 模式。
在表 8-1 中,您可以看到一些常见的日志格式。例如,用于数据库或编程语言的(更具体、范围更窄的)格式和框架有许多。
表 8-1. 常见日志格式
| 格式 | 注意 |
|---|---|
| 常见事件格式 | 由 ArcSight 开发;用于设备、安全用例 |
| 常见日志格式 | 用于 Web 服务器;另见扩展日志格式 |
| Graylog 扩展日志格式 | 由 Graylog 开发;改进 Syslog |
| Syslog | 用于操作系统、应用程序、设备;参见“Syslog” |
| 嵌入式度量格式 | 由亚马逊开发(既有日志又有度量) |
作为良好的实践,您希望避免日志的开销(启用快速查找和小的占地面积——即不占用太多磁盘空间)。在这种情况下,例如通过logrotate进行日志轮换是常用的。一个称为数据温度的高级概念也可能很有用,将较旧的日志文件移动到更便宜和更慢的存储(附加磁盘、S3 存储桶、Glacier)中。
警告
有一个情况需要特别注意日志信息,特别是在生产环境中。每当您决定在应用程序中发出一条日志行时,请问自己是否可能泄露敏感信息。这些敏感信息可能是密码、API 密钥,甚至只是用户识别信息(电子邮件、帐户 ID)。
问题在于日志通常以持久形式存储(例如在本地磁盘上或甚至在 S3 存储桶中)。这意味着即使进程终止后很长时间,某人仍然可以访问敏感信息并用于攻击。
为了标志一个日志项的重要性或预期的目标消费者,日志通常定义级别(例如 DEBUG 用于开发,INFO 用于正常状态,或 ERROR 用于意外情况可能需要人工干预)。
现在是动手的时候了:让我们从一些简单的东西开始,并作为一个概述,看看 Linux 的中央日志目录(为了易读性而缩短输出):
$ ls -al /var/log
drwxrwxr-x 8 root syslog 4096 Jul 13 06:16 .
drwxr-xr-x 13 root root 4096 Jun 3 07:52 ..
drwxr-xr-x 2 root root 4096 Jul 12 11:38 apt/ 
-rw-r----- 1 syslog adm 7319 Jul 13 07:17 auth.log 
-rw-rw---- 1 root utmp 1536 Sep 21 14:07 btmp 
drwxr-xr-x 2 root root 4096 Sep 26 08:35 cups/ 
-rw-r--r-- 1 root root 28896 Sep 21 16:59 dpkg.log 
-rw-r----- 1 root adm 51166 Jul 13 06:16 dmesg 
drwxrwxr-x 2 root root 4096 Jan 24 2021 installer/ 
drwxr-sr-x+ 3 root systemd-journal 4096 Jan 24 2021 journal/ 
-rw-r----- 1 syslog adm 4437 Sep 26 13:30 kern.log 
-rw-rw-r-- 1 root utmp 292584 Sep 21 15:01 lastlog 
drwxr-xr-x 2 ntp ntp 4096 Aug 18 2020 ntpstats/ 
-rw-r----- 1 syslog adm 549081 Jul 13 07:57 syslog 
apt 包管理器的日志
所有登录尝试(成功和失败)和认证过程的日志
登录尝试失败
打印相关日志
dpkg 包管理器的日志
设备驱动程序日志;使用 dmesg 进行检查
系统安装日志(Linux 发行版最初安装时)
journalctl 的位置;详见journalctl了解详情
内核日志
所有用户的最后登录;使用 lastlog 进行检查
NTP 相关的日志(也参见“Network Time Protocol”)
syslogd 的位置;详见“Syslog”了解详情
一个常见的实时消费日志的模式(即在其发生时)是跟随日志;也就是说,你可以在日志的末尾观看新添加的日志行(编辑以适应):
$ tail -f /var/log/syslog 
Sep 26 15:06:41 starlite nm-applet[31555]: ... 'GTK_IS_WIDGET (widget)' failed
Sep 26 15:06:41 starlite nm-dispatcher: ... new request (3 scripts)
Sep 26 15:06:41 starlite systemd[1]: Starting PackageKit Daemon...
Sep 26 15:06:41 starlite nm-dispatcher: ... start running ordered scripts...
Sep 26 15:06:42 starlite PackageKit: daemon start 
^C
使用 -f 选项跟随 syslogd 进程的日志。
一个示例日志行;详见“Syslog”了解格式。
提示
如果你想看一个进程的日志输出,并同时将其存储在文件中,你可以使用tee 命令:
$ someprocess | tee -a some.log
现在你会在终端看到 someprocess 的输出,并且同时输出会存储在 some.log 中。请注意,我们使用 -a 选项来追加到日志文件,否则它会被截断。
现在让我们看看两个最常用的 Linux 日志系统。
Syslog
Syslog 是一种用于从内核到守护程序再到用户空间的各种来源的日志记录标准。它起源于网络环境,今天该协议包括在RFC 5424中定义的文本格式,以及部署场景和安全注意事项。Figure 8-2 显示了 Syslog 的高级格式,但请注意,还有许多很少使用的可选字段。

图 8-2. 根据 RFC 5424 定义的 Syslog 格式
如 RFC 5424 中定义的 Syslog 格式具有以下头字段(其中TS和HN最常使用):
PRI
消息设施/严重性
VER
Syslog 协议号(通常被省略,因为它只能是 1)
TS
包含使用 ISO 8601 格式生成消息的时间
HN
标识发送消息的机器
APP
识别发送消息的应用程序(或设备)
PID
标识发送消息的进程
MID
可选的消息 ID
格式还包括结构化数据,这是以结构化(键/值为基础)列表形式的有效负载,其中每个元素由[ ]界定。
通常,人们会使用syslogd二进制文件来管理日志。随着时间的推移,其他选择也已经出现,你应该知道:
一种增强的日志守护程序,可以作为syslogd的替代品,并且支持 TLS、基于内容的过滤以及将日志记录到诸如 PostgreSQL 和 MongoDB 等数据库中。自 1990 年末推出以来就可用。
扩展了 Syslog 协议,也可以与systemd一起使用。自 2004 年起可用。
尽管其年龄较大,Syslog 系列协议和工具仍然广泛存在并可用。随着systemd成为所有主要 Linux 发行版中的事实标准,用于所有主要 Linux 发行版的 init 系统,然而,关于日志记录还有一种新的方式:来看看systemd日志。
journalctl
在systemd中,我们简要介绍了systemd生态系统的一个组件,负责日志管理:journalctl。与迄今为止使用的 Syslog 和其他系统不同,journalctl使用二进制格式存储日志项。这样可以实现更快的访问和更好的存储效果。
当引入二进制存储格式时,确实引起了一些批评,因为人们无法继续使用熟悉的tail、cat和grep命令查看和搜索日志。尽管如此,使用journalctl时,人们需要学习与日志交互的新方式,学习曲线并不陡峭。
让我们来看看一些常见任务。如果你不带任何参数启动journalctl,它将呈现为一个交互式分页器(你可以使用箭头键或空格键滚动并用q退出),显示所有日志。
要限制时间范围,例如可以使用以下命令:
$ journalctl --since "3 hours ago" 
$ journalctl --since "2021-09-26 15:30:00" --until "2021-09-26 18:30:00" 
限制时间范围到过去三小时发生的事情。
另一种限制时间范围的方式,带有明确的开始和结束时间。
您可以限制输出到特定的systemd单元,例如(假设有一个名为abc.service的服务):
$ journalctl -u abc.service
提示
journalctl工具具有强大的日志项输出格式化方式。使用--output(或简写为-o)参数,您可以优化输出以适应特定的用例。重要的值如下:
cat
简短形式,没有时间戳或来源
short
默认情况下,模拟 Syslog 输出
json
每行一个 JSON 格式的条目(用于自动化)
您可以使用以下方式跟随日志,就像使用tail -f一样:
$ journalctl -f
让我们将前面的所有信息放在一起,形成一个具体的示例。假设您希望重新启动由systemd管理的 Linux 发行版的安全组件:AppArmor。也就是说,在一个终端中我们使用systemctl restart apparmor重新启动服务,在另一个终端中我们执行以下命令(输出经过编辑;实际输出是每行一个日志项):
$ journalctl -f -u apparmor.service 
-- Logs begin at Sun 2021-01-24 14:36:30 GMT. --
Sep 26 17:10:02 starlite apparmor[13883]: All profile caches have been cleared,
but no profiles have been unloaded.
Sep 26 17:10:02 starlite apparmor[13883]: Unloading profiles will leave already
running processes permanently
...
Sep 26 17:10:02 starlite systemd[1]: Stopped AppArmor initialization.
Sep 26 17:10:02 starlite systemd[1]: Starting AppArmor initialization... 
Sep 26 17:10:02 starlite apparmor[13904]: * Starting AppArmor profiles
Sep 26 17:10:03 starlite apparmor[13904]: Skipping profile in
/etc/apparmor.d/disable: usr.sbin.rsyslogd
Sep 26 17:10:09 starlite apparmor[13904]: ...done.
Sep 26 17:10:09 starlite systemd[1]: Started AppArmor initialization.
跟随 AppArmor 服务的日志。
在systemd停止服务后,它会再次启动。
这样我们就到了日志部分的结尾,然后转向数值值和更广泛的监控主题。
监控
监控是为了各种原因捕获系统和应用程序指标。例如,您可能对某个操作花费的时间或进程消耗的资源(性能监控)感兴趣,或者您可能正在排除一个不健康的系统。在监控的上下文中,您最常执行的两种活动如下:
-
跟踪一个或多个指标(随时间变化)
-
在条件上发出警报
在本节中,我们首先关注您应该了解的一些基础知识和工具,随着我们深入到更多只在特定情况下相关的高级技术。
让我们看一个简单的例子,显示一些基本的指标,比如系统运行时间,内存使用情况等,使用uptime命令:
$ uptime 
08:48:29 up 21 days, 20:59, 1 user, load average: 0.76, 0.20, 0.09 
使用uptime命令显示一些基本的系统指标。
以逗号分隔,输出告诉我们系统运行的时间、登录的用户数,然后是 load average 部分的三个指标:1 分钟、5 分钟和 15 分钟的平均值。这些平均值是运行队列中作业数或等待磁盘 I/O 的作业数;这些数字已经归一化,并且表示 CPU 使用率。例如,这里过去 5 分钟的负载平均值为 0.2(单独看并不能告诉您太多,因此您必须与其他值进行比较并跟踪它随时间的变化)。
接下来,让我们监视一些基本的内存利用情况,使用 free 命令(输出压缩以适应):
$ free -h 
total used free shared buff/cache available
Mem: 7.6G 1.3G 355M 395M 6.0G 5.6G 
Swap: 975M 1.2M 974M 
使用人类友好的输出显示内存使用情况。
内存统计信息:总使用/空闲/共享内存,用于缓冲区和缓存的内存(如果不想得到合并值,请使用 -w),以及可用内存。
总交换空间的总量/已使用/空闲空间,即移动到交换磁盘空间的物理内存。
查看内存使用情况的更复杂方法是使用 vmstat(虚拟内存统计)命令。以下示例以自更新方式使用 vmstat(输出编辑以适应):
$ vmstat 1 
procs -----------memory--------- ---swap-- ----io---- -system- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st 
4 0 1184 482116 682388 5447048 0 0 12 105 28 191 6 3 91 0 0
0 0 1184 483444 682388 5446600 0 0 0 0 369 522 1 0 99 0 0
0 0 1184 483696 682392 5446600 0 0 0 104 278 374 1 1 99 0 0
^C
显示内存统计信息。参数 1 表示每秒打印一个新的摘要行。
一些重要的列标题:r 表示运行或等待 CPU 的进程数(应小于或等于您拥有的 CPU 数量),free 是以 KB 为单位的空闲主内存,in 是每秒中断数,cs 是每秒上下文切换数,us 到 st 是总 CPU 时间的百分比,涵盖用户空间、内核、空闲等。
要查看某个操作花费了多长时间,您可以使用 time 命令:
$ time (ls -R /etc 2&> /dev/null) 
real 0m0.022s 
user 0m0.012s 
sys 0m0.007s 
测量递归列出所有 /etc 子目录所需的时间(我们使用 2&> /dev/null 丢弃所有输出,包括错误)。
总共(挂钟)所需的时间(除了性能外并不真正有用)。
ls 本身在 CPU 上花费的时间(用户空间)。
ls 等待 Linux 执行某些操作的时间(内核空间)。
在前面的例子中,如果您对操作花费了多长时间感兴趣,将 user 和 sys 的总和作为一个很好的近似值,并且两者的比率可以很好地告诉您执行时间大部分花费在哪里。
现在我们专注于一些更具体的主题:网络接口和块设备。
设备 I/O 和网络接口
使用 iostat 命令 可以监视 I/O 设备(输出编辑):
$ iostat -z --human 
Linux 5.4.0-81-generic (starlite) 09/26/21 _x86_64_ (4 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
5.8% 0.0% 2.7% 0.1% 0.0% 91.4%
Device tps kB_read/s kB_wrtn/s kB_read kB_wrtn
loop0 0.00 0.0k 0.0k 343.0k 0.0k
loop1 0.00 0.0k 0.0k 2.8M 0.0k
...
sda 0.38 1.4k 12.4k 2.5G 22.5G 
dm-0 0.72 1.3k 12.5k 2.4G 22.7G
...
loop12 0.00 0.0k 0.0k 1.5M 0.0k
使用 iostat 显示 I/O 设备指标。使用 -z,我们告诉它仅显示有活动的设备,而 --human 则使输出更友好(单位以人类可读的形式显示)。
示例行:tps 表示每秒传输(I/O 请求)的次数,read 表示数据量,wrtn 表示写入的数据量。
接下来是:使用 ss 命令 查看可以转储套接字统计信息的网络接口(参见 “套接字”)。以下命令列出了 TCP 和 UDP 套接字以及进程 ID(输出已编辑以适应):
$ ss -atup 
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
udp UNCONN 0 0 0.0.0.0:60360 0.0.0.0:*
...
udp UNCONN 0 0 0.0.0.0:ipp 0.0.0.0:*
udp UNCONN 0 0 0.0.0.0:789 0.0.0.0:*
udp UNCONN 0 0 224.0.0.251:mdns 0.0.0.0:*
udp UNCONN 0 0 0.0.0.0:mdns 0.0.0.0:*
udp ESTAB 0 0 192.168.178.40:51008 74.125.193.113:443
...
tcp LISTEN 0 128 0.0.0.0:sunrpc 0.0.0.0:*
tcp LISTEN 0 128 127.0.0.53%lo:domain 0.0.0.0:*
tcp LISTEN 0 5 127.0.0.1:ipp 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:45313 0.0.0.0:*
tcp ESTAB 0 0 192.168.178.40:57628 74.125.193.188:5228 
tcp LISTEN 0 128 [::]:sunrpc [::]:*
tcp LISTEN 0 5 [::1]:ipp [::]:*
使用 ss 和以下选项:用 -a 选择所有(即监听和非监听的套接字);-t 和 -u 分别选择 TCP 和 UDP;-p 显示正在使用套接字的进程。
正在使用的一个套接字示例。这是一个本地 IPv4 地址 192.168.178.40 和远程 74.125.193.188 之间建立的已建立的 TCP 连接,看起来处于空闲状态:接收队列 (Recv-Q) 和发送队列 (Send-Q) 均报告为零。
注意
一个过时的收集和显示接口统计信息的方法是使用 netstat。例如,如果你想要对 TCP 和 UDP 有一个持续更新的视图,包括进程 ID 并使用 IP 地址而不是 FQDN,你可以使用 netstat -ctulpn。
lsof 的意思是“列出打开的文件”,是一个功能强大的工具,有许多用途。以下示例展示了 lsof 在网络连接上的应用(输出已编辑以适应)。
$ sudo lsof -i TCP:1-1024 
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
...
rpcbind 26901 root 8u IPv4 615970 0t0 TCP *:sunrpc (LISTEN)
rpcbind 26901 root 11u IPv6 615973 0t0 TCP *:sunrpc (LISTEN)
列出特权 TCP 端口(需要 root 权限)。
另一个 lsof 的用法示例是进程为中心的视图:如果你知道一个进程的 PID(这里是 Chrome),你可以使用 lsof 来跟踪文件描述符、I/O 等(输出已编辑以适应):
$ lsof -p 5299
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
chrome 5299 mh9 cwd DIR 253,0 4096 6291458 /home/mh9
chrome 5299 mh9 rtd DIR 253,0 4096 2 /
chrome 5299 mh9 txt REG 253,0 179093936 3673554 /opt/google/chrome/chrome
...
还有许多更多用于(性能)监控的工具可用,例如 sar(涵盖各种计数器,非常适合脚本)和 [perf](https://oreil.ly/TJ4gP)—我们将在 “高级可观察性” 中讨论其中一些。
现在你已经掌握了各个工具,让我们转向允许你交互式监控 Linux 的集成工具。
集成性能监视器
使用我们在前一节讨论的工具,比如 lsof 或 vmstat,是一个很好的起点,也适用于脚本。为了更方便的监控,你可能更喜欢集成解决方案。这些通常带有文本用户界面(TUI),有时还带有颜色,并提供以下功能:
-
支持多种资源类型(CPU、RAM、I/O)
-
交互式排序和过滤(按进程、用户、资源)
-
实时更新和深入查看详细信息,如进程组甚至 cgroups 和命名空间
例如,广泛可用的 top 在标题中提供了一个概述——类似于我们在 uptime 输出中看到的内容——然后是 CPU 和内存详细信息的表格渲染,接着是您可以跟踪的进程列表(输出已编辑):
top - 12:52:54 up 22 days, 1:04, 1 user, load average: 0.23, 0.26, 0.23 
Tasks: 263 total, 1 running, 205 sleeping, 0 stopped, 0 zombie 
%Cpu(s): 0.2 us, 0.4 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.0 si, \
0.0 st% 
KiB Mem : 7975928 total, 363608 free, 1360348 used, 6251972 buff/cache
KiB Swap: 999420 total, 998236 free, 1184 used. 5914992 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 
1 root 20 0 225776 9580 6712 S 0.0 0.1 0:25.84 systemd
...
433 root 20 0 105908 1928 1700 S 0.0 0.0 0:00.05 `- lvmetad
...
775 root 20 0 36552 4240 3880 S 0.0 0.1 0:00.16 `- bluetoothd
789 syslog 20 0 263040 4384 3616 S 0.0 0.1 0:01.98 `- rsyslogd
系统概要(与 uptime 输出比较)
任务统计
CPU 使用统计(用户、内核等;类似于 vmstat 输出)
动态进程列表,包括每个进程级别的详细信息;类似于 ps aux 输出
提示
以下是在 top 中记住的最重要的按键:
?
列出帮助(包括键映射)
V
切换到和从进程树视图
m
按内存使用率排序
P
按 CPU 消耗排序
k
发送信号(例如 kill)
q
退出
虽然 top 在几乎任何环境中都可用,但还有许多其他可用的替代品,包括以下内容:
htop(图 8-3)
一个增强型 top,比 top 更快且具有更好的用户界面。
atop(图 8-4)
一个强大的替代品,比 top 更强大。除了 CPU 和内存外,还详细涵盖了 I/O 和网络统计信息。
一个相对较新的工具,特别值得注意,因为它支持 cgroups v2(参见“Linux cgroups”)。其他工具不理解 cgroups,因此仅提供全局资源视图。

图 8-3. htop 工具的截图

图 8-4. atop 工具的截图
还有许多其他集成的监控工具可用,超越基本来源或专门用途。包括但不限于以下内容:
一个强大的混合体,除了常规资源外还涵盖设备
一个集成的性能分析器,允许您显示和绘制各种指标范围
用于网络流量监控;ss 的替代品,提供了良好的 TUI
用于网络流量监控;比 traceroute 更强大的替代品(参见“路由”以获取有关 traceroute 的详细信息)
现在您已经对从自己的代码公开系统指标的工具有了广泛的了解,让我们看看如何执行此操作。
仪表化
到目前为止,我们关注的信号来自内核或现有应用程序(即您不拥有的代码)。现在我们转向如何像日志一样,为您的代码配备发出指标的功能。
将代码插入以发出信号(特别是指标)的过程,如果您是开发软件,这个过程通常是相关的。这个过程通常称为仪器化,并且有两种常见的仪器化策略:自动仪器化(作为开发者,您无需额外的工作)和自定义仪器化,在其中您手动插入代码片段,例如在代码库的某个点发出一个指标。
您可以使用StatsD,针对多种编程语言提供了客户端库,例如 Ruby, Node.js, Python 和 Go。StatsD 很好用,但在 Kubernetes 或 IoT 等动态环境中存在一些限制。在这些环境中,通常更好的选择是一种称为拉取式或抓取的不同方法。使用抓取,应用程序公开指标(通常通过 HTTP 端点),然后代理调用此端点以检索指标,而不是配置应用程序发送指标的位置。我们将在“Prometheus 和 Grafana”中返回该主题。
高级可观测性
现在您已经了解了 Linux 可观测性的基础知识,让我们来看看这个领域中的一些更高级的主题。
跟踪和分析
跟踪这个术语有多重含义:在 Linux 的上下文中,在单台机器上,跟踪意味着随时间捕获进程执行(用户空间的函数调用,系统调用等)。
注意
在分布式设置中,例如在 Kubernetes 中容器化的微服务或一堆 Lambda 函数作为无服务器应用的一部分,我们有时会将分布式跟踪(例如使用 OpenTelemetry 和 Jaeger)缩写为跟踪。这种类型的跟踪不在本书的讨论范围之内。
在单台 Linux 机器的上下文中,有许多数据源。您可以使用以下内容作为跟踪的数据源:
Linux 内核
跟踪可以来自内核中的函数或者由系统调用触发。例如包括内核探测器 (kprobes) 或者内核跟踪点。
用户空间
应用程序功能调用,例如通过用户空间探测器 (uprobes),可以作为跟踪的数据源。
跟踪的用例包括以下内容:
警告
您可能会倾向于在所有地方使用strace;然而,您应该意识到它所带来的开销。这在生产环境中尤为重要。阅读 Brendan Gregg 的“strace Wow Much Syscall”以了解其背景。
参见图 8-5 作为sudo perf top的示例输出,它生成一个按进程汇总的摘要。

图 8-5. perf追踪工具的屏幕截图
未来看来,eBPF(参见“扩展内核的现代方法:eBPF”)将成为实现追踪的事实标准,特别是对于定制案例。它拥有丰富的生态系统和不断增长的供应商支持,因此如果您正在寻找一种具有未来保障的追踪方法,请确保使用 eBPF。
追踪的一个特定用例是性能分析,即识别频繁调用的代码部分。一些相关的低级分析工具包括pprof,Valgrind,以及火焰图可视化。
有许多选项可以交互地消耗perf的输出并可视化追踪结果;例如,请参阅 Mark Hansen 的博客文章“Linux perf Profiler UIs”。
持续性分析是分析的高级变体,它随时间捕获(内核和用户空间)追踪数据。一旦收集到这些时间戳追踪数据,您可以绘制并比较它们,深入研究感兴趣的段落。一个非常有前途的例子是基于 eBPF 的开源项目parca,如图 8-6 所示。

图 8-6. parca 的屏幕截图,一个持续性分析工具
Prometheus 和 Grafana
如果您正在处理随时间变化的指标数据(时间序列数据),使用Prometheus和Grafana组合是您可能希望考虑的高级可观察性工具。
我将向您展示一个简单的单机设置,您可以在 Linux 机器上的仪表板上查看并甚至对发生的事件设置警报。
我们将使用node exporter来公开一系列系统指标,从 CPU 到内存和网络。然后我们将使用 Prometheus 来抓取 node exporter 的数据。抓取意味着 Prometheus 调用 node exporter 提供的 HTTP 端点,通过 URL 路径/metrics返回OpenMetrics 格式的指标数据。为此,我们需要配置 Prometheus,使其使用 node exporter 的 HTTP 端点 URL。我们设置的最后一步是在 Grafana 中使用 Prometheus 作为数据源,您可以在仪表板中查看时间序列数据(随时间变化的指标),甚至可以根据某些条件(如低磁盘空间或 CPU 超载)设置警报。
因此,作为第一步,下载并解压 node exporter,并在后台运行二进制文件./node_exporter &。您可以使用以下命令检查它是否正常运行(输出已编辑):
$ curl localhost:9100/metrics
...
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 7.2575e-05
go_gc_duration_seconds{quantile="0.25"} 0.00011246
go_gc_duration_seconds{quantile="0.5"} 0.000227351
go_gc_duration_seconds{quantile="0.75"} 0.000336613
go_gc_duration_seconds{quantile="1"} 0.002659194
go_gc_duration_seconds_sum 0.126529838
go_gc_duration_seconds_count 390
...
现在我们已经设置好信号数据源,我们将 Prometheus 和 Grafana 都作为容器运行。为了继续操作,您需要安装和配置 Docker(参见“Docker”)。
创建一个名为prometheus.yml的 Prometheus 配置文件,内容如下:
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
monitor: 'mymachine'
scrape_configs:
- job_name: 'prometheus' 
static_configs:
- targets: ['localhost:9090']
- job_name: 'machine' 
static_configs:
- targets: ['172.17.0.1:9100']
Prometheus 本身会暴露指标数据,所以我们将其包含在内(自我监控)。
这是我们的节点导出器。由于我们在 Docker 中运行 Prometheus,不能使用localhost,而是要使用 Docker 默认使用的 IP 地址。
我们使用之前创建的 Prometheus 配置文件,并通过卷将其挂载到容器中,操作如下:
$ docker run --name prometheus \
--rm -d -p 9090:9090 \ 
-v /home/mh9/lml/o11y/prometheus.yml:/etc/prometheus/prometheus.yml \ 
prom/prometheus:main
这里的参数使 Docker 在退出时删除容器(--rm),作为守护进程运行(-d),并公开端口 9090(-p),这样我们可以从本地使用它。
将我们的配置文件作为卷映射到容器中。请注意,在此处,你需要用存储路径替换/home/mh9/lml/o11y/。此外,这必须是绝对路径。因此,如果你希望保持灵活性,可以在 bash 中使用$PWD,或者在 Fish 中使用(pwd),而不是硬编码的路径。
执行了上述命令后,在浏览器中打开localhost:9000,然后在顶部的状态下拉菜单中点击 Targets。几秒钟后,你应该会看到类似图 8-7 中显示的屏幕,证明 Prometheus 已成功从自身和节点导出器中抓取了指标。

图 8-7. Prometheus Web UI 中的目标截图
接下来,我们启动 Grafana:
$ docker run --name grafana \
--rm -d -p 3000:3000 \
grafana/grafana:8.0.3
执行了上述命令后,在浏览器中打开localhost:3000,并使用admin作为用户名和密码。接下来,我们需要做两件事:
-
在 Grafana 中添加Prometheus 作为数据源,使用
172.17.0.1:9100作为 URL
完成此操作后,你应该会看到类似图 8-8 的内容。

图 8-8. Node Exporter Full 仪表板的 Grafana UI 截图
这是关于 Linux 的一些令人兴奋的高级可观察性,使用现代工具。考虑到 Prometheus/Grafana 的设置更为复杂,涉及多个组件,你可能不会用它来完成一个简单的任务。换句话说,在本节讨论的 Linux 本地工具中,应该能够解决大部分问题;然而,在更高级的用例中,例如家庭自动化或媒体服务器,你可能需要更完整的解决方案,这时 Prometheus/Grafana 就显得非常合适。
结论
在本章中,我们确保在遇到 Linux 系统问题时不会盲目操作。你通常用于诊断的主要信号类型是日志(文本)和指标(数值)。对于高级情况,你可以应用分析技术,显示进程的资源使用情况以及执行上下文(正在执行的源文件和行)。
如果你想进一步了解并深入探讨这个主题,请查看以下资源:
基础知识
-
系统性能:企业与云 第二版,作者 Brendan Gregg(Addison-Wesley)
日志
监控
高级
通过完成本章和之前的章节,您现在了解了从内核到 Shell 再到文件系统和网络的 Linux 基础知识。本书的最后一章包含了一些高级主题,这些主题在其他章节中并不适合。根据您的目标,您可能会发现它们有趣且有用,但对于大多数日常任务而言,您现在已经掌握了所需的一切。
^(1) 可观测性有时也被称为数字术语 o11y,因为在 o 和 y 之间有 11 个字母。
第九章:高级主题
这章节是一个综合性的章节。我们涵盖了从虚拟机到安全性再到新的 Linux 使用方式等多个主题。这章节的共同点是,如果你有特定的用例,或者在专业设置中需要它们,这些主题对你来说大多是相关的。
我们从单台机器上的进程如何通信和共享数据开始这一章节。有丰富的进程间通信(IPC)机制可用,在这里我们专注于成熟和广泛使用的特性:信号,命名管道和 Unix 域套接字。
接下来我们看看虚拟机(VM)。与我们在“容器”中讨论过的容器相比(适用于应用程序级别的依赖管理),VM 为您的工作负载提供了强大的隔离。您在公共云环境和数据中心中经常会遇到 VM,使用 VM 在本地也非常有用,例如用于测试或模拟分布式系统。
本章的下一节重点介绍现代 Linux 发行版,这些发行版通常以容器为中心,并假定为不可变状态。您经常会在像 Kubernetes 这样的分布式系统环境中找到这些发行版。
接着我们转向选定的安全主题,涵盖了 Kerberos,一个广泛使用的身份验证套件,以及可插入认证模块(PAM),这是 Linux 提供的用于身份验证的扩展机制。
在本章的最后部分,我们回顾了 Linux 的解决方案和使用案例,这些在撰写时尚未成为主流,但它们可能对您有用,值得探索。
进程间通信
在 Linux 中,有许多进程间通信(IPC)选项可供选择,从管道到套接字再到共享内存。IPC 使进程能够通信,同步活动并共享数据。例如,Docker 守护程序使用可配置的套接字来管理容器。在本节中,我们回顾了一些流行的 IPC 选项及其使用案例。
信号
信号最初开发用作内核向用户空间进程通知某些事件的一种方式。将信号视为发送给进程的异步通知。有许多可用的信号(使用man 7 signal命令了解更多),其中大多数都有默认动作,例如停止或终止进程。
大多数信号都可以定义自定义处理程序,而不是让 Linux 继续执行默认动作。当您想要做一些清理工作或简单地忽略某些信号时,这非常有用。表格 9-1 展示了您应该熟悉的最常见信号。
表 9-1. 常见信号
| 信号 | 含义 | 默认动作 | 处理选项 | 快捷键组合 |
|---|---|---|---|---|
SIGHUP |
告诉守护进程重新读取其配置文件 | 终止进程 | nohup或自定义处理器 |
N/A |
SIGINT |
用户通过键盘中断 | 终止进程 | 自定义处理器 | Ctrl+C |
SIGQUIT |
用户通过键盘退出 | 核心转储并终止进程 | 自定义处理器 | Ctrl+\ |
SIGKILL |
强制终止信号 | 终止进程 | 无法处理 | N/A |
SIGSTOP |
停止进程 | 停止进程 | 无法处理 | N/A |
SIGTSTP |
用户通过键盘停止 | 停止进程 | 自定义处理器 | Ctrl+Z |
SIGTERM |
优雅终止 | 终止进程 | 自定义处理器 | N/A |
也有一些没有定义含义的信号(SIGUSR1和SIGUSR2),进程可以使用它们来相互通信,发送异步通知,前提是双方同意信号的语义。
发送信号给进程的一个典型方式是有些奇怪命名的kill命令(由于其默认行为是导致进程终止):
$ while true ; do sleep 1 ; done & 
[1] 17030 
$ ps 
PID TTY TIME CMD
16939 pts/2 00:00:00 bash
17030 pts/2 00:00:00 bash 
17041 pts/2 00:00:00 sleep
17045 pts/2 00:00:00 ps
$ kill 17030 
[1]+ Terminated while true; do
sleep 1;
done
我们在这里设置了一个非常简单的程序,只是简单地休眠。用&将其放到后台运行。
shell 作业控制确认我们的程序作为后台作业运行,并报告其 PID(17030)。
使用ps,我们检查程序是否仍在运行。
这是我们的程序(比较 PID)。
默认情况下,kill发送SIGTERM信号给进程,而默认动作是优雅地终止进程。我们提供kill命令与我们进程的 PID(17030),由于我们没有注册自定义处理程序,它被终止。
现在我们来看看如何使用trap处理信号。这允许我们在 shell 环境(命令行或脚本)中定义一个自定义处理器:
$ trap "echo kthxbye" SIGINT ; while true ; do sleep 1 ; done 
^Ckthxbye 
使用trap "echo kthxbye" SIGINT,我们注册一个处理程序,告诉 Linux 当用户按下 Ctrl+C 时(导致SIGINT信号发送到我们的进程),Linux 应该在默认动作(终止)之前执行echo kthxbye。
我们看到用户中断(^C等同于 Ctrl+C),然后我们的自定义处理程序执行,如预期地打印kthxbye。
信号是一种简单而强大的 IPC 机制,现在您已经了解了如何在 Linux 中发送和处理信号的基础知识。接下来,我们讨论两种更为复杂和强大的 IPC 机制——命名管道和 UNIX 域套接字。
命名管道
在“流”中,我们讨论了通过使用管道符(|)可以将数据从一个进程传递到另一个进程,方法是将一个进程的stdout与另一个进程的stdin连接起来。我们称这些管道为未命名管道。进一步延伸这个想法,命名管道是可以分配自定义名称的管道。
就像未命名管道一样,命名管道与正常的文件 I/O(open、write等)一起工作,并提供先进先出(FIFO)传递。与未命名管道不同,命名管道的生命周期不限于使用它的进程。从技术上讲,命名管道是管道的一种包装,使用pipefs伪文件系统(参见“伪文件系统”)。
让我们看看命名管道的实际应用,以更好地理解它们能做什么。我们在下文中创建了一个名为examplepipe的命名管道,并创建了一个发布者和一个消费者进程:
$ mkfifo examplepipe 
$ ls -l examplepipe
prw-rw-r-- 1 mh9 mh9 0 Oct 2 14:04 examplepipe 
$ while true ; do echo "x" > examplepipe ; sleep 5 ; done & 
[1] 19628
$ while true ; do cat < examplepipe ; sleep 5 ; done & 
[2] 19636
x 
x
...
我们创建了一个名为examplepipe的命名管道。
通过使用ls查看管道的类型:第一个字母是p,表示我们正在查看一个命名管道。
使用循环,我们将字符x发布到我们的管道中。请注意,除非有其他进程从examplepipe读取,否则管道将被阻塞。不能再向其写入数据。
我们启动了第二个进程,它在循环中从管道中读取。
由于我们的设置,我们看到每隔大约五秒钟终端上出现x。换句话说,每次进程 PID 为19636的进程能够从带有cat的命名管道中读取时,它就会出现。
命名管道易于使用。由于其设计,它们看起来和感觉像普通文件。但它们也有局限性,因为它们仅支持单向和单个消费者。我们接下来看看的 IPC 机制解决了这些限制。
UNIX 域套接字
我们已经在网络环境中讨论了套接字。还有其他种类的套接字专门在单台机器的上下文中使用,其中一种称为UNIX 域套接字:这些是双向的、多路通信端点。这意味着您可以有多个消费者。
域套接字有三种类型:面向流(SOCK_STREAM)、面向数据报(SOCK_DGRAM)和顺序数据包(SOCK_SEQPACKET)。寻址基于文件系统路径名。与 IP 地址和端口不同,一个简单的文件路径就足够了。
通常,您会通过编程的方式使用域套接字(链接)。然而,您可能会发现自己处于需要排除系统故障并希望使用例如socat工具从命令行手动与套接字交互的情况。
虚拟机
本节讲述了一种成熟的技术,使我们能够使用像您的笔记本电脑或数据中心中的服务器这样的物理机器模拟多个虚拟机。这提供了一种更灵活、更强大的方式来运行不同的工作负载,可能是来自不同租户的,具有强隔离的方式。我们重点关注 x86 架构的硬件辅助虚拟化。
在 在 [图 9-1 中,您可以看到概念层面的虚拟化架构,包含以下内容(从底部开始):
CPU
必须支持硬件虚拟化。
基于内核的虚拟机
位于 Linux 内核中;在“基于内核的虚拟机”中讨论。
用户空间中的组件
用户空间中的组件包括以下内容:
虚拟机监控器(VMM)
管理虚拟机并模拟虚拟设备,如 QEMU 和 Firecracker(见“Firecracker”)。还有 libvirt,一个暴露通用 API 的库,旨在标准化 VMM,您可以通过编程使用(在图中未明确显示;视为 VMM 块的一部分)。
来宾内核
通常也是 Linux 内核,但也可以是 Windows。
来宾进程
运行在来宾内核上。

图 9-1. 虚拟化架构
运行在主机内核上的进程(在 图 9-1 中,进程 1 和进程 2)与来宾进程隔离。这意味着一般来说,主机的物理 CPU 和内存不会受到来宾活动的影响。例如,如果虚拟机中有攻击,主机内核和进程不会受到影响(只要虚拟机没有特殊权限访问主机系统)。请注意,实际情况中可能会有例外,例如 rowhammer 或 Meltdown 和 Spectre。
基于内核的虚拟机
基于内核的虚拟机(KVM)是一个针对支持虚拟化扩展的 x86 硬件的 Linux 本地虚拟化解决方案,如 AMD-V 或 Intel VT。
KVM 内核模块有两个部分:核心模块(kvm.ko)和特定于 CPU 架构的模块(kvm-intel.ko/kvm-amd.ko)。通过 KVM,Linux 内核作为超视图管理程序,负责大部分的重负担。此外,还有诸如集成的 Virtio 这样的驱动程序,允许进行 I/O 虚拟化。
如今,硬件通常支持虚拟化,KVM 已经可用,但为了检查您的系统是否能够使用 KVM,您可以执行以下检查(输出已编辑):
$ grep 'svm\|vmx' /proc/cpuinfo 
flags : 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 ht tm pbe syscall nx pdpe1gb
rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology
tsc_reliable nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64
ds_cpl vmx tm2 ssse3 sdbg cx16 xtpr pdcm sse4_1 sse4_2 x2apic movbe popcnt 
tsc_deadline_timer aes xsave rdrand lahf_lm 3dnowprefetch cpuid_fault cat_l2
ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust
smep erms mpx rdt_a rdseed smap clflushopt intel_pt sha_ni xsaveopt xsavec
xgetbv1 xsaves dtherm ida arat pln pts md_clear arch_capabilities
...
$ lsmod | grep kvm 
kvm_intel 253952 0 
kvm 659456 1 kvm_intel
在 CPU 信息中搜索 svm 或 vmx(请注意,它是按 CPU 计算的,因此如果您有八个核心,您将看到这个 flags 块重复出现八次)。
我们看到列出了 vmx,所以在硬件辅助虚拟化方面,一切都很好。
在这里我们检查 KVM 内核模块是否可用。
这告诉我们已加载了 kvm_intel 内核模块,因此在使用 KVM 方面一切都设置好了。
管理 KVM 的一种现代方式是使用 Firecracker。
Firecracker
Firecracker 是一个可以管理 KVM 实例的 VMM。它用 Rust 编写,主要在亚马逊网络服务(AWS)上开发,用于无服务器提供,如 AWS Lambda 和 AWS Fargate。
Firecracker 被 设计 以安全地在同一物理机上运行多租户工作负载。Firecracker VMM 管理所谓的 microVMs,向主机公开 HTTP API,允许您启动、查询和停止 microVMs。它通过在主机上使用 TUN/TAP 设备 模拟网络接口,并且块设备由主机上的文件支持,支持 Virtio 设备。
从安全的角度来看,除了迄今讨论的虚拟化外,Firecracker 默认使用 seccomp 过滤器(参见“seccomp Profiles”)来限制它可以使用的主机系统调用。还可以使用 cgroups。从可观察性的角度来看,您可以通过命名管道从 Firecracker 收集日志和指标。
接下来我们转向专注于不变性并利用容器的现代 Linux 发行版。
现代 Linux 发行版
最著名的传统 Linux 发行版包括以下几种:
-
红帽家族(RHEL、Fedora 和 CentOS/Rocky)
-
基于 Debian 的家族(Ubuntu、Mint、Kali、Parrot OS、elementary OS 等)
-
SUSE 家族(openSUSE 和 Enterprise)
-
Gentoo
-
Arch Linux
这些都是非常好的发行版。根据您的需求和偏好,您可以选择从完全控制和自己处理一切(从安装到打补丁)到完全托管的方案,在这种情况下,发行版将处理大部分任务。
随着容器的兴起,如在 “容器” 中讨论的那样,主机操作系统的角色发生了变化。在容器的上下文中,传统的包管理器(参见 “包和包管理器”)扮演了不同的角色:大多数基础容器镜像通常是从特定的 Linux 发行版构建的,并且容器内部通过 .deb 或 .rpm 包来满足所有应用程序级别的依赖关系。
此外,对系统进行增量更改证明是一个巨大的挑战。特别是当您需要在规模化环境中进行管理时,比如需要管理一群机器时。因此,对于现代发行版来说,焦点越来越多地放在不变性上。其思想是,对配置或代码(比如修复安全问题或新增功能的补丁)的任何更改都会导致新构件的创建,例如一个容器映像,而不是更改运行中的系统。
当我说“现代 Linux 发行版”时,我指的是以容器为中心,以不变性和自动升级(由 Chrome 开创)为核心的发行版。让我们看一些现代发行版的例子。
Red Hat Enterprise Linux CoreOS
2013 年,一家名为 CoreOS 的年轻初创公司发布了CoreOS Linux(后来改名为 Container Linux)。其主要特点包括系统更新的双分区方案和缺少包管理器。换句话说,所有应用程序都将作为容器在本地运行。在这个生态系统中,开发了许多仍在使用的工具(如etcd;想象一个用于配置任务的分布式版本的/etc目录)。
在 Red Hat 收购了 CoreOS(公司)之后,它宣布了将 CoreOS Linux 与 Red Hat 自己的 Project Atomic(具有类似目标的项目)合并的意图。这一合并导致了Red Hat Enterprise Linux CoreOS (RHCOS),这并不意味着它可以单独使用,而是在 Red Hat 称为 OpenShift Container Platform 的 Kubernetes 发行版的上下文中使用。
Flatcar Container Linux
稍后,Red Hat 宣布其在 Container Linux 周围的计划后,一家名为 Kinvolk GmbH(现为 Microsoft 的一部分)的德国初创公司宣布将继续在新品牌Flatcar Container Linux下分叉并继续开发 Container Linux。
Flatcar 自称是一个容器本地化、轻量级操作系统,适用于容器编排器如 Kubernetes 和 IoT/边缘计算。它延续了 CoreOS 的传统自动升级(可选与其自己的更新管理器Nebraska)并且有一个称为Ignition的强大而简单易用的引导设备配置实用程序,使您能够对启动设备进行精细控制(RHCOS 也用于此目的)。此外,它没有包管理器;一切都在容器中运行。您可以使用systemctl在单台机器上管理容器化应用的生命周期,或者更常见地使用 Kubernetes。
瓶子火箭
瓶子火箭是由 AWS 开发的基于 Linux 的操作系统,专为托管容器而设计。用 Rust 编写,用于多种服务,如 Amazon EKS 和 Amazon ECS。
与 Flatcar 和 CoreOS 类似,Bottlerocket 不使用包管理器,而是使用基于 OCI 镜像的模型进行应用程序升级和回滚。 Bottlerocket 使用(大体上)只读、完整性检查的文件系统,基于dm-verity。为了通过 SSH(虽然不推荐)访问和控制 Bottlerocket,它运行一个所谓的控制容器,在一个独立的containerd实例中。
RancherOS
RancherOS是一个 Linux 发行版,其中一切都是由 Docker 管理的容器。由 Rancher(现为 SUSE)赞助,它针对容器工作负载进行了优化,如同其 Kubernetes 发行版。它运行两个 Docker 实例:系统 Docker 作为第一个进程运行,用户 Docker 用于创建应用程序容器。RancherOS 具有很小的占用空间,非常适合嵌入式系统和边缘计算环境中使用。
选定的安全主题
在第四章中,我们讨论了许多访问控制机制。我们讨论了认证(简称为authn),用于验证用户的身份,并且是任何授权(简称为authz)的先决条件。在本节中,我们简要讨论了两个广泛使用的认证工具,您应该注意它们。
Kerberos
Kerberos是麻省理工学院在 1980 年代开发的认证套件。今天,它在RFC 4120和相关的 IETF 文件中有正式规范。Kerberos 的核心思想是,我们通常处理不安全的网络,但我们希望客户端和服务之间能够安全地证明其身份。
在概念上,Kerberos 认证过程如图 9-2 所示,工作原理如下:

图 9-2. Kerberos 协议概念
-
一个客户端(例如,您笔记本上的程序)向称为密钥分发中心(KDC)的 Kerberos 组件发送请求,请求为特定服务(如打印或目录)获取凭据。
-
KDC 响应所请求的凭据——即服务的票证和临时加密密钥(会话密钥)。
-
客户端将票证(其中包含客户端的身份和会话密钥的副本)传输给服务。
-
会话密钥由客户端和服务共享,用于验证客户端,并且可选择用于验证服务。
Kerberos 也存在一些挑战,例如 KDC 扮演的中心角色(单点故障)以及其严格的时间要求(需要通过 NTP 在客户端和服务器之间进行时钟同步)。总体而言,尽管操作和管理不简单,Kerberos 在企业和云服务提供商中得到了广泛使用和支持。
可插拔认证模块
在历史上,程序会自行管理用户认证过程。使用可插拔认证模块(PAM),Linux 引入了一种灵活的开发程序的方式,独立于具体的认证方案(PAM 自 1990 年代末起在更广泛的 UNIX 生态系统中存在)。PAM 采用模块化架构,为开发人员提供了一个强大的库来与之交互。它还允许系统管理员插入不同的模块,例如以下内容:
要求用户在/etc/passwd中列出
用于会话密钥环
用于 Kerberos 5 基于密码的检查
到此,我们已经完成了高级安全主题的讨论,现在转向更具抱负性的主题。
其他现代和未来的提供
在本节中,我们将探讨一些令人兴奋的 Linux 提供,包括设置 Linux 的新方法以及在新环境中使用 Linux 的方式。在服务器世界中(无论是本地数据中心还是公共云),Linux 已经是事实上的标准,在许多移动设备中,Linux 在幕后起着关键作用。
在本节中的主题共同点是,目前这些主题尚未进入主流。然而,如果你对未来的发展可能是什么样子或 Linux 仍存在高增长潜力的领域感兴趣,请继续阅读。
NixOS
NixOS是一种基于源的 Linux 发行版,采用功能化的包管理和系统配置方式以及用于升级回滚的机制。我将其称为“功能化的方法”,因为这些制品基于不变性。
Nix 软件包管理器构建整个操作系统,从内核到系统包和应用程序。
与大多数其他 Linux 发行版不同,NixOS 不遵循 Linux 标准基础文件系统布局的讨论(位于/usr/bin、/usr/lib等位置的系统程序以及通常位于/etc的配置)。
NixOS 及其生态系统中有许多有趣的想法,使其特别适合 CI 流水线。即使您不想完全投入,也可以单独使用 Nix 软件包管理器(在 NixOS 之外)。
Linux 桌面化
尽管Linux 桌面化的可行性仍在讨论中,但毫无疑问,关于桌面友好的发行版以及与之相关的窗口管理器有很多选择。
符合 UNIX 传统,图形用户界面(GUI)部分与操作系统的其余部分分离。通常,X 窗口管理器负责 GUI 的职责(从窗口管理到样式和渲染),并借助显示管理器的帮助。
在窗口管理器的基础上,实现桌面体验(例如图标、小部件和工具栏)的是桌面环境,比如 KDE 或 MATE。
如今有许多适合初学者的桌面 Linux 发行版,从而使从 Windows 或 macOS 切换变得容易。同样适用于各种开源应用程序,从办公应用程序(撰写文档或处理电子表格,比如 LibreOffice)到绘图和图像编辑(Gimp),再到所有主要的 Web 浏览器、游戏、媒体播放器和实用工具,以及开发环境。
事实上,Linux 桌面的催化剂可能来自一个非常意想不到的方向:随着Windows 11 允许你直接运行图形化 Linux 应用程序,这可能改变相关的激励和采用情况。时机将会证明一切。
嵌入式系统上的 Linux
嵌入式系统上的 Linux是一个广泛的领域,涵盖从汽车到网络设备(如路由器),到智能家居设备(例如冰箱)和媒体设备/智能电视等多种实现。
你可以以极少的费用获取一个特别有趣的通用平台,即Raspberry Pi(RPI)。它配备了自己的 Linux 发行版称为 Raspberry Pi OS(基于 Debian 的系统),通过微型 SD 卡可以简单地安装这个及其他 Linux 发行版。RPI 具有多个通用输入/输出(GPIO),使得通过面包板使用外部传感器和电路变得简单。你可以通过 Python 等语言实验、学习电子技术并编程硬件。
云 IDE 中的 Linux
近年来,基于云的开发环境的可行性取得了巨大进展,现在已经存在(商业)产品,将集成开发环境(通常是 Visual Studio Code)、Git 和多种编程语言结合到 Linux 环境中。作为开发者,你只需一个 Web 浏览器和网络访问,就可以在“云端”编辑、测试和运行代码。
在撰写时,两个著名的云 IDE 的例子分别是Gitpod,它可以作为托管服务提供,也可以作为开源项目自行托管;以及Codespaces,它与 GitHub 深度集成。
结论
本章涵盖了高级主题,并且深化了您对基本技术和工具的了解。如果您想要启用 IPC,可以使用信号和命名管道。要隔离工作负载,可以使用虚拟机,特别是现代变体如 Firecracker。我们还讨论了现代 Linux 发行版:如果您计划运行容器(Docker),可能需要考虑这些强制不变性的容器中心发行版。然后我们转向了选定的安全主题,特别是 Kerberos 和 PAM 用于灵活和/或大规模认证。最后,我们回顾了尚未主流的 Linux 解决方案,例如桌面 Linux 及如何在树莓派等嵌入式系统上开始使用 Linux 进行本地实验或开发。
本章的进一步阅读材料:
IPC
虚拟机
现代发行版
选定的安全
其他现代和未来的选择
我们已经完成了本书的阅读。希望这是您自己 Linux 旅程的起点。感谢您陪伴我到最后,如果您有反馈意见,我非常乐意听取,可以通过 Twitter 或者传统的电子邮件联系:modern-linux@pm.me。
附录 A. 有用的配方
在本附录中,我汇编了一些常见任务的配方列表。这只是我随时间收集的一些配方的选择,这些任务我经常执行,并且喜欢作为参考保留。这些配方并不是对 Linux 使用和管理员任务的全面或深入覆盖。对于全面的配方集合,我强烈推荐您查看 Carla Schroder 的Linux Cookbook(O'Reilly),详细介绍了各种配方。
收集系统信息
要了解 Linux 版本、内核和其他相关信息,请使用以下任一命令:
cat /etc/*-release
cat /proc/version
uname -a
要了解基本硬件设备(CPU、RAM、磁盘),请执行以下操作:
cat /proc/cpuinfo
cat /proc/meminfo
cat /proc/diskstats
要了解系统的硬件详细信息,如 BIOS 等,请使用:
sudo dmidecode -t bios
关于前一个命令的说明:-t的其他有趣选项包括system和memory。
查询整体主存储器和交换使用情况,执行以下命令:
free -ht
要查询进程可以拥有多少文件描述符,请使用:
ulimit -n
处理用户和进程
您可以使用who或w列出已登录用户(更详细的输出)。
要显示特定用户SOMEUSER的每个进程的系统指标(CPU、内存等),请使用以下命令:
top -U SOMEUSER
使用以下命令以树形式详细列出所有用户的所有进程:
ps faux
查找特定进程(此处为python):
ps -e | grep python
要终止进程,如果知道其 PID,请使用它(如果进程忽略此信号,请添加-9作为参数):
kill *`PID`*
或者,您可以使用killall按名称终止进程。
收集文件信息
要查询文件详细信息(包括文件系统信息如 inode),请使用:
stat *`somefile`*
要了解命令的工作方式、shell 如何解释它以及可执行文件的位置,请使用:
type *`somecommand`*
which *`somebinary`*
处理文件和目录
要显示名为afile的文本文件的内容:
cat afile
要列出目录的内容,请使用ls,并可能进一步使用输出。例如,要计算目录中文件的数量,请使用:
ls -l /etc | wc -l
查找文件和文件内容:
find /etc -name "*.conf" 
find . -type f -exec grep -H FINDME {} \; 
在目录/etc中查找以.conf结尾的文件。
通过执行grep在当前目录中查找“FINDME”。
要显示文件差异,请使用:
diff -u *`somefile`* *`anotherfile`*
要替换字符,请使用tr如下所示:
echo 'Com_Acme_Library' | tr '_A-Z' '.a-z'
替换字符串的另一种方法是使用sed(请注意分隔符不一定是/,这对于在路径或 URL 中替换内容的情况非常方便):
cat 'foo bar baz' | sed -e 's/foo/quux/'
要创建指定大小的文件(用于测试),可以使用dd命令,如下所示:
dd if=/dev/zero of=output.dat bs=1024 count=1000 
创建名为output.dat的 1 MB 文件(由 1000 个 1 KB 块组成),其中填充了零。
使用重定向和管道
在“流”中,我们讨论了文件描述符和流。以下是围绕这个主题的几个配方。
文件 I/O 重定向:
*command* 1> *file* 
*command* 2> *file* 
*command* &> *file* 
*command* >*file* 2>&1 
*command* > /dev/null 
*command* < *file* 
将*command*的stdout重定向到*file*。
将 *command* 的 stderr 重定向到 *file*。
将 *command* 的 stdout 和 stderr 都重定向到 *file*。
将 *command* 的 stdout 和 stderr 重定向到 *file* 的替代方法。
丢弃 *command* 的输出(通过将其重定向到 /dev/null)。
将 *file* 的输入重定向到 *command* 的 stdin。
要将一个进程的 stdout 连接到另一个进程的 stdin,请使用管道 (|):
*`cmd1`* | *`cmd2`* | *`cmd3`*
要显示管道中每个命令的退出代码:
echo ${PIPESTATUS[@]}
处理时间和日期
要查询与时间相关的信息,例如本地时间、UTC 时间以及同步状态,请使用:
timedatectl status
处理日期时,通常需要获取当前时间的日期或时间戳,或将现有时间戳从一种格式转换为另一种格式。
要以 YYYY-MM-DD 格式获取日期(例如 2021-10-09),请使用以下命令:
date +"%Y-%m-%d"
要生成 Unix 时间戳(例如 1633787676),请执行:
date +%s
要为 UTC 创建 ISO 8601 时间戳(例如 2021-10-09T13:55:47Z),可以使用以下方法:
date -u +"%Y-%m-%dT%H:%M:%SZ"
同样的 ISO 8601 时间戳格式,但用于本地时间:
date +%FT%TZ
使用 Git
要克隆一个 Git 仓库(即在您的 Linux 系统上创建一个本地副本),使用以下命令:
git clone https://github.com/*`exampleorg`*/*`examplerepo`*.git
在上述 git clone 命令完成后,Git 仓库将位于 examplerepo 目录中,您应该在此目录中执行接下来的所有命令。
要以颜色显示本地更改并显示已添加和已删除行,请使用:
git diff --color-moved
要查看本地发生的更改(编辑的文件、新文件、已删除的文件),请执行:
git status
添加所有本地更改并提交它们:
git add --all && git commit -m "adds a super cool feature"
要找出当前提交的提交 ID,请使用:
git rev-parse HEAD
要用标签 ATAG 标记 ID 为 HASH 的提交,请执行:
git tag ATAG HASH
将本地更改推送到远程(上游)仓库,并附上标签 ATAG:
git push origin ATAG
要查看提交历史记录,请使用 git log;具体来说,要获取摘要,请执行:
git log (git describe --tags --abbrev=0)..HEAD --oneline
系统性能
有时您需要查看设备的速度或者 Linux 系统在负载下的性能。以下是生成系统负载的一些方法。
模拟内存负载(同时消耗一些 CPU 循环)使用以下命令:
yes | tr \\n x | head -c 450m | grep z
在上述管道中,yes 生成无限数量的 y 字符,每个字符位于自己的一行上,然后 tr 命令将其转换为一个连续的 yx 流,head 命令将其截断为大约 450 百万字节(约 450 MB)。最后,我们让 grep 消耗结果的 yx 块以寻找不存在的内容(z),因此我们看不到输出,但它仍在生成负载。
更详细的目录磁盘使用情况:
du -h /home
列出空闲磁盘空间(全局,本例中):
df -h
使用以下命令对磁盘进行负载测试并测量 I/O 吞吐量:
dd if=/dev/zero of=/home/some/file bs=1G count=1 oflag=direct
附录 B. 现代 Linux 工具
本附录聚焦于现代 Linux 工具和命令。其中一些命令是现有命令的即插即用替代品,而另一些是全新的。这些工具大多数都改善了用户体验(UX),包括简化用法和使用彩色输出,从而提高了效率流程。
我已经整理了一个相关工具列表,详见表 B-1,展示了功能和潜在的替代方案。
表 B-1. 现代 Linux 工具和命令
| 命令 | 许可证 | 特性 | 可替换或增强: |
|---|---|---|---|
bat |
MIT 许可证 和 Apache 许可证 2.0 | 显示、分页、语法高亮 | cat |
envsubst |
MIT 许可证 | 基于模板的环境变量 | 不适用 |
exa |
MIT 许可证 | 有意义的彩色输出,理智的默认设置 | ls |
dog |
欧洲联盟公共许可证 v1.2 | 简单强大的 DNS 查询 | dig |
fx |
MIT 许可证 | JSON 处理工具 | jq |
fzf |
MIT 许可证 | 命令行模糊查找器 | ls + find + grep |
gping |
MIT 许可证 | 多目标,图形化 | ping |
httpie |
BSD 3-Clause “New” 或 “Revised” 许可证 | 简单的用户体验 | curl(还有 curlie) |
jo |
GPL 许可证 | 生成 JSON | 不适用 |
jq |
MIT 许可证 | 本地 JSON 处理器 | sed,awk |
rg |
MIT 许可证 | 快速,理智的默认设置 | find,grep |
sysz |
The Unlicense | fzf 的 systemctl 用户界面 |
systemctl |
tldr |
CC-BY(内容)和 MIT 许可证(脚本) | 重点放在命令的使用示例上 | man |
zoxide |
MIT 许可证 | 快速更改目录 | cd |
要了解本附录中列出的许多工具的背景和用法,请利用以下资源:
-
欢迎收听来自 The Changelog: Software Development, Open Source 的 现代 UNIX 工具的播客集。
-
现在可以通过 GitHub 仓库Modern UNIX查看现代工具的活跃列表。


浙公网安备 33010602011771号