Linux DevOps 手册(一)

原文:annas-archive.org/md5/55f0ee1b5d0f6f58bdd7da1ffd9f7954

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

DevOps 已经成为现代软件开发和交付的关键组成部分。它彻底改变了我们构建、测试、部署和运营软件系统的方式。DevOps 不仅是一套工具和实践,它更是一种文化和心态,强调协作、沟通和自动化。

本书旨在成为一本全面的 DevOps 指南,涵盖从选择合适的 Linux 发行版到避免 DevOps 中的陷阱的所有内容。本书的每一章都提供了详细的信息和实际的示例,帮助你理解这些概念并将其应用于实际场景中。

本书适合对象

本书面向那些已经在软件开发和 IT 运维领域积累了一定知识和经验的人,旨在进一步扩展他们对 DevOps 和 Linux 系统的理解。

如果你对 Linux 系统不太熟悉,本书将为你提供必要的指导和工具,帮助你快速学习并掌握 Linux 基础设施的管理。你将了解 Linux 操作系统、其架构及基本概念。

此外,本书还强调学习公共云技术,重点介绍 AWS。如果你有兴趣了解如何使用 AWS 构建和管理可扩展、可靠的系统,本书将为你提供必要的知识和工具,帮助你入门。

无论你是 DevOps 新手还是已经有一定经验,本书都为学习更复杂的概念提供了坚实的基础。它涵盖了从 Linux 系统基础到更高级的 DevOps 实践(如配置与基础设施即代码、CI/CD 等)的一系列主题。

本书涵盖的内容

第一章选择合适的 Linux 发行版,讨论了 GNU/Linux 的历史以及流行发行版之间的差异。

第二章命令行基础,引导你了解命令行的使用以及我们在全书中将使用的常用工具。

第三章进阶 Linux,描述了 GNU/Linux 中的一些高级功能,这些功能将对你有用。

第四章使用 Shell 脚本自动化,解释了如何使用 Bash shell 编写自己的脚本。

第五章Linux 中的服务管理,讨论了管理 Linux 中服务的不同方式,并向你展示如何使用 systemd 定义自己的服务。

第六章Linux 中的网络,描述了网络是如何工作的,如何控制网络配置的不同方面,以及如何使用命令行工具。

第七章Git,通往 DevOps 的门户,讨论了 Git 是什么,以及如何使用 Git 的版本控制系统,包括一些不太为人知的 Git 特性。

第八章Docker 基础,探讨了如何将你的服务或工具容器化,以及如何运行和管理容器。

第九章深入探索 Docker,讨论了 Docker 的更多高级功能,包括 Docker Compose 和 Docker Swarm。

第十章监控、追踪和分布式日志记录,讨论了如何监控你的服务、可以在云中使用的工具以及如何进行基础设置。

第十一章使用 Ansible 进行配置即代码(Configuration as Code),介绍了如何使用 Ansible 实现配置即代码;它将引导你完成 Ansible 的基本设置及更多高级功能。

第十二章利用基础设施即代码(Infrastructure as Code),讨论了基础设施即代码IaC)的概念、流行工具以及如何使用 Terraform 管理基础设施。

第十三章使用 Terraform、GitHub 和 Atlantis 实现 CI/CD,通过使用 Terraform 和 Atlantis 对基础设施进行持续集成CI)和持续部署(**CD),将 IaC 推向更高水平。

第十四章避免 DevOps 中的陷阱,讨论了你在 DevOps 工作中可能遇到的挑战。

为了最大限度地从本书中受益

你需要在虚拟机或计算机的主操作系统中安装 Debian Linux 或 Ubuntu Linux。我们使用的其他软件要么已作为默认工具集预装,要么我们会告诉你如何获取并安装它。

假设你具备一些基本的 Linux 知识及其命令行界面(CLI)操作经验。熟悉 shell 脚本和基本的编程概念也将对你有帮助。此外,建议你具备一定的 IT 基础设施管理知识,并对软件开发实践有一定的了解。

本书面向 DevOps 新手,假设你渴望学习这个领域中常用的工具和概念。在阅读完本书后,你将能深入理解如何使用 IaC 工具(如 Terraform 和 Atlantis)来管理基础设施,并掌握如何使用 Ansible 和 Bash 脚本自动化重复任务。你还将学习如何设置日志记录和监控解决方案,帮助你维护和排查基础设施问题。

本书涉及的软件/硬件操作系统要求
BashLinux 操作系统预装
AnsiblePython 3 或更新版本
TerraformLinux 操作系统
AWS CLIPython 3 或更新版本
DockerLinux 操作系统

如果您正在使用本书的数字版,我们建议您自己输入代码或通过本书的 GitHub 仓库获取代码(下节会提供链接)。这样可以帮助您避免与复制粘贴代码相关的潜在错误。

下载示例代码文件

您可以从 GitHub 上下载本书的示例代码文件,链接为 github.com/PacktPublishing/The-Linux-DevOps-Handbook。如果代码有更新,将会在 GitHub 仓库中进行更新。

我们还提供了丰富的书籍和视频代码包,您可以访问 github.com/PacktPublishing/ 查看。

使用的约定

本书中使用了多种文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。示例如下:“当以 root 用户登录时,您的提示符号会以 # 结尾。当以普通用户登录时,提示符号会显示 $。”

代码块如下所示:

docker build [OPTIONS] PATH | URL | -

当我们希望引起您注意某一代码块的特定部分时,相关行或项目将以粗体显示:

docker build [OPTIONS] PATH | URL | -

任何命令行输入或输出将按如下方式书写:

chmod ug=rx testfile

粗体:表示新术语、重要单词或屏幕上显示的单词。例如,菜单或对话框中的单词会以粗体显示。示例如下:“Ansible Galaxy 是一个由社区驱动的平台,提供了大量的 Ansible 角色和剧本。”

提示或重要说明

这样显示。

联系我们

我们始终欢迎读者的反馈。

常规反馈:如果您对本书的任何方面有疑问,请通过电子邮件联系 customercare@packtpub.com,并在邮件主题中注明书名。

勘误:尽管我们已尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将非常感激您能将其报告给我们。请访问 www.packtpub.com/support/errata 并填写表格。

盗版:如果您在互联网上遇到我们作品的任何非法副本,请向我们提供该位置地址或网站名称。请通过 copyright@packt.com 联系我们,并附上该材料的链接。

如果您有兴趣成为作者:如果您在某个主题上有专长并且有兴趣写作或为书籍贡献内容,请访问 authors.packtpub.com

分享您的想法

一旦您阅读完 《Linux DevOps 手册》,我们很想听听您的想法!请点击这里直接进入本书的亚马逊评论页面并分享您的反馈

您的评论对我们以及技术社区非常重要,能够帮助我们确保提供优质内容。

下载本书的免费 PDF 版本

感谢您购买本书!

您喜欢随时阅读,但无法随身携带纸质书籍吗?

您购买的电子书与您选择的设备不兼容吗?

不用担心,现在每本 Packt 书籍都可以免费获得该书的无 DRM 版本 PDF。

随时随地,在任何设备上阅读。您可以直接从您最喜欢的技术书籍中搜索、复制并粘贴代码到您的应用程序中。

好处不仅仅是这些,您还可以获得独家的折扣、新闻通讯,并且每天会在您的邮箱中收到精彩的免费内容

按照以下简单步骤获得福利:

  1. 扫描二维码或访问下面的链接

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/linux-dop-hb/img/B18197_QR_Free_PDF.jpg

packt.link/free-ebook/9781803245669

  1. 提交您的购买证明

  2. 就是这样!我们会直接将您的免费 PDF 和其他福利发送到您的邮箱

第一部分:Linux 基础

在本书的开篇部分,我们将重点介绍 Linux 发行版及您需要的基本技能,以便高效使用 Linux 操作系统。您还将学习如何编写基本的 Shell 脚本来自动化日常任务。

本部分包括以下章节:

  • 第一章选择合适的 Linux 发行版

  • 第二章命令行基础

  • 第三章Linux 进阶

  • 第四章使用 Shell 脚本自动化

第一章:选择合适的 Linux 发行版

在本章中,我们将从 Linux 的最初开始深入探讨 Linux 世界。我们将简要介绍 Linux 的历史,解释什么是发行版,并解释在选择发行版用于生产环境时需要考虑的因素。你不需要了解 Linux、其管理或云计算。如果你不理解我们使用的某些词汇,不用担心。本章中不会有太多让人困惑的术语,如果有,我们将在后续章节中解释它们。当你读完本章后,你应该能够理解为什么有这么多不同的 Linux 版本,它的费用大概是多少,以及如何为自己选择合适的 Linux 版本。

本章将涵盖以下主要内容:

  • 什么是 Linux,什么是 Linux 发行版?

  • 你可以使用什么来帮助你做出正确的决策?

  • 目前有几个非常流行的主要 Linux 发行版。

技术要求

本章没有任何技术要求。我们暂时不会运行任何命令或安装任何软件。这些内容会在后续章节中讲解。

我们在书中展示的代码可以在公开的 GitHub 仓库中找到,您可以通过以下地址进行查看:github.com/PacktPublishing/The-Linux-DevOps-Handbook

什么是 Linux 发行版?

Linux 是云工作负载的标准操作系统。然而,并没有一个统一的 Linux 操作系统叫这个名字。它的生态系统相当复杂,这源于其最初的形成过程。

在 Linux 的创始人 Linus Torvalds 设想出 Linux 之前,Unix 就已经存在。由于法律原因,Unix 的源代码是可以授权给任何购买它的人使用的,这使得它在许多机构中非常流行,包括大学。然而,这些代码并非完全免费。这让许多人不满,他们认为软件应该是免费的——就像言论自由或啤酒自由一样——包括源代码。到了 1980 年代,一个完全免费和开放的 Unix 实现诞生了,这个项目被称为 GNU 项目。其目标是开发一个可以让用户完全控制计算机的操作系统。该项目成功地开发出了运行操作系统所需的所有软件,除了一个——内核。

操作系统的内核简而言之,就是操作硬件并为用户管理硬件和程序的核心。

1991 年,芬兰学生 Linus Torvalds 著名地宣布了他的爱好内核——Linux。当时他称其为“只是一个爱好——不会像 GNU 那样变得庞大和专业。”它本不打算变得庞大和流行。接下来发生的就是历史。Linux 内核在开发者和管理员中变得流行,并成为了 GNU 项目的缺失部分。Linux 是内核,GNU 工具是所谓的用户空间,它们一起组成了 GNU/Linux 操作系统。

前面的短故事对我们有两个重要的意义:

  • 虽然 GNU 用户空间和 Linux 内核是最流行的组合,但你会看到它并不是唯一的。

  • Linux 提供内核,GNU 项目提供用户空间工具,但它们必须以某种方式准备好以供安装。许多人和团队对如何最好地做这件事有不同的想法。我将继续扩展这个思路。

一个团队或公司将 GNU/Linux 操作系统交付给最终用户的方式被称为 发行版。它便于操作系统的安装,后续管理软件的手段,以及如何管理操作系统和正在运行的进程的一般概念。

是什么使得各个发行版不同?

Linux 和 GNU 项目的开放性质使几乎任何人都可以创建自己的发行版。让新用户感到困惑的其中一个原因是他们可以使用的 操作系统OS)版本之多。让 Linux 用户之间爆发圣战的可靠方法就是问哪个发行版最好。

我们可以通过发行软件的格式(软件包)和用于安装和移除这些软件的附加软件(软件包管理器)来对 Linux 发行版进行分组。有很多种方式,但最常见的两种是 RPMRPM 软件包管理器)和 DEB 软件包。软件包不仅仅是包含二进制文件的档案。它们还包含设置软件以便使用的脚本——创建目录、用户、权限、日志规则,以及其他一些我们将在后续章节中解释的内容。

RPM 发行版家族从 Red Hat Enterprise LinuxRHEL)开始,由 Red Hat 公司创建和维护。与之紧密相关的是 Fedora(由 Red Hat 赞助的自由社区发行版)。它还包括 CentOS Linux(RHEL 的免费版本)和 Rocky Linux(RHEL 的另一个免费版本)。

DEB 发行版包括 Debian(DEB 软件包的来源)——一个技术官僚社区项目。从 Debian 发行版中衍生出了一些基于它的发行版,使用了大部分核心组件。最著名的是 Ubuntu,由 Canonical 赞助的服务器和桌面发行版。

也有一些发行版使用最少自动化的软件包,最著名的是 Slackware,这是现存最古老的 Linux 发行版之一。

有些发行版为用户提供了一套脚本,用于在实际使用的硬件上编译软件——最著名的就是 Gentoo

最后,有一种发行版实际上是一本包含一套指令的书籍,用户可以按照这些指令手动构建整个操作系统——Linux From Scratch 项目。

另一种对发行版进行分组的方式是根据它们对封闭软件的接受程度——封闭软件限制了源代码、二进制文件或两者的分发。这可以是硬件驱动程序,如 NVIDIA 显卡驱动,也可以是用户软件,如允许播放流媒体和 DVD、蓝光光盘的电影解码器。一些发行版让安装和使用这些软件变得简单,而另一些则让它更困难,主张我们应该努力实现所有软件都为开源且免费的(无论是自由言论还是免费饮料的含义)。

另一个区分它们的方式是某个发行版使用的安全框架。两个最著名的框架是 AppArmor,主要由 Ubuntu 使用,以及 SELinux(来自美国国家安全局),用于包括 Red Hat Enterprise Linux(及其衍生版)和 Fedora Linux 在内的发行版。

还值得注意的是,虽然大多数 Linux 发行版使用 GNU 项目作为用户空间,流行的云端 Alpine Linux 发行版使用的是自己的一套软件,特别是以最小化大小为目标编写的。

看看发行版的开发方式,它可以是由社区驱动的(没有任何商业实体拥有过程或软件的任何部分——以 Debian 为例),也可以是商业性质的(完全由公司拥有——以 RHEL 为例,SuSE 另一个例子),以及介于两者之间的各种混合形式(Ubuntu 和 Fedora 是典型的商业拥有的发行版,拥有大量独立贡献者)。

最后,我们可以通过发行版如何促进云工作负载来对它们进行分组。在这里,我们可以从不同的方面来看:

  • 服务器端:某个发行版作为我们基础设施、虚拟机和容器的底层操作系统的表现如何。

  • 服务端:某个发行版在作为容器或虚拟机运行我们的软件时的表现如何。

为了让新用户更加困惑和有趣,每个发行版可以有许多变体(根据不同发行版的术语,叫做 flavorsvariantsspins),它们提供不同的软件集合或默认配置。

最后,亲爱的读者,为了让你更加困惑,对于桌面或笔记本电脑的使用,Linux 提供了它能给你的最佳选择——选择权。Linux 操作系统的图形界面种类繁多,甚至最有经验的用户也会头晕——KDE Plasma、GNOME、Cinnamon Desktop、MATE、Unity Desktop(与 Unity 3D 游戏引擎无关)和 Xfce。这个列表并不详尽,带有主观性,且非常有限。它们在易用性、可配置性、内存和 CPU 使用量以及许多其他方面都有所不同。

发行版的数量令人震惊——跟踪 Linux 发行版的主流网站 (distrowatch.com/dwres.php?resource=popularity) 在撰写本文时列出了 265 个不同的 Linux 发行版。如此庞大的数量使得本书只能选择其中的三个进行讲解。在大多数情况下,选择哪一个发行版对你自己并无太大区别,除非你选择的是商业版本,可能在许可和订阅上有所不同。每当选择的发行版产生影响,特别是技术方面的影响时,我们都会指出来。

选择发行版不仅仅是一个实用的选择。Linux 社区深受理想驱动。对于一些人来说,这些理想是他们构建生活的基石。关于哪款文本编辑器更好,基于其用户界面、发布的许可证或源代码质量,已经发生过无数次激烈的争论。对于选择运行 WWW 服务器的软件,或者如何接受新贡献,同样也会表现出同样的情绪。这不可避免地会影响 Linux 发行版的安装方式、配置和维护工具的选择,以及开箱即用的安装软件的种类。

尽管如此,我们必须提到,尽管他们有着强烈的信念,开源社区,包括 Linux 社区,实际上是非常友好的。在大多数情况下,你能够在在线论坛上找到帮助或建议,并且你很有可能会亲自遇到他们。

在选择发行版时,你需要注意以下几个因素:

  • 你希望运行的软件是否支持该发行版?一些商业软件限制了发布软件包的发行版数量。尽管有可能在不受支持的 Linux 版本上运行这些软件,但这可能会比较棘手,并容易出现中断。

  • 你打算运行的软件版本是否可用?有时,所选的发行版更新你所需的软件包的频率可能不够高。在云计算的世界里,几个月前的软件可能已经过时,缺乏重要的功能或安全更新。

  • 这个发行版的许可是怎样的?它是免费使用的,还是需要订阅计划?

  • 你的支持选项是什么?对于由社区驱动的免费发行版,你的选择仅限于在线和本地的友好 Linux 专家。对于商业发行版,你可以支付费用获得各种支持服务。根据你的需求和预算,你可以找到一种支持选项组合,既能满足你的需求,也能符合你的财务状况。

  • 你对编辑配置文件和运行长而复杂的命令的舒适度如何?一些发行版提供工具(包括命令行和图形界面工具),使配置任务更容易且减少出错。然而,这些工具大多是特定于发行版的,你在其他地方是找不到的。

  • 云相关工具在某个发行版上的支持情况如何?这可能包括安装的简便性、软件本身的更新程度,或者配置使用所需的步骤数。

  • 这个发行版在你选择的云服务上支持得怎么样?这意味着有多少云服务提供商提供带有这个发行版的虚拟机。要获得这个发行版的容器镜像来运行你的软件有多容易?我们怀疑,构建这个发行版并在其上部署的难易程度如何?

  • 它在互联网上的文档情况如何?这不仅包括发行版维护者编写的文档,还包括用户编写的各种博客文章和文档(主要是教程和所谓的操作指南)。

到目前为止,你已经了解了什么是 Linux 发行版,它们如何区分,以及你可以使用哪些标准来选择作为你将管理的系统核心的发行版。

在接下来的章节中,我们将深入探讨每个发行版,以便更好地了解最流行的几个,给你一个初步的了解,展示每个发行版是如何运作的,以及你可以期待什么。

介绍各个发行版

在这一段略显冗长但精炼的 Linux 操作系统历史之后,终于可以开始探索我们在本书中选定的几个发行版了。在这一节中,我们将涵盖我们刚刚列出的因素,因为我们认为它们在做决定时非常重要。不过,请记住,尽管我们力求为你呈现客观的事实和评估,但我们无法避免自己的主观看法。在你做出选择之前,务必自己进行评估,因为很有可能你将长期使用这个发行版。

需要注意的是,我们不会全面覆盖所有发行版。我们将尽力为你提供一个基础,接下来你需要通过研究来进行构建。

此外,在学习过程中,不要害怕从一个发行版跳到另一个发行版。只有通过实际经验,你才能真正了解哪个发行版最适合你的需求。

Debian

Debian (www.debian.org/) 是最古老的活跃 Linux 发行版之一。它的开发由社区支持的 Debian 项目主导。它以两件事著称——该发行版提供的包数量庞大以及稳定版发布的缓慢。后者在近年来有所改善,稳定版现在每两年发布一次。软件通过名为包的归档进行交付。Debian 包的文件名扩展名为 .deb,通常被称为deb。这些包保存在在线仓库中,仓库又被细分为多个池。仓库提供了大约 60,000 个包,包含最新稳定版中的软件。

Debian 始终提供三个版本(所谓的分支)——稳定、测试和不稳定。每个发布版本的名称都来源于《玩具总动员》电影系列中的角色。最新的稳定版——版本 11,名为 Bullseye。

不稳定分支是面向开发者、喜欢挑战极限的人或那些对最新软件的需求高于稳定性需求的用户的滚动分支。软件会在经过最小测试后进入不稳定分支。

测试分支是进行测试的地方,顾名思义,这里进行大量的测试,感谢最终用户的贡献。软件包从不稳定分支流入这里。这里的软件比稳定分支中的软件更新,但不如不稳定分支中的软件新。在新稳定版发布前的几个月,测试分支会被冻结。冻结意味着不再接受新的软件,仅允许已经接受的包的新版本,前提是它们修复了错误。

几个月后,测试分支会变成稳定分支。此时,软件仅会更新安全修复。

这个发行版适用于许多硬件平台——Intel、ARM、PowerPC 等等。除了非官方的移植版本,还有许多硬件平台可以安装它。

Debian 被视为最稳定的发行版,通常作为各种计算集群的基础平台使用,因此它通常被安装在数据中心机架中的裸金属服务器上,旨在长期持续使用。

根据 W3Techs (w3techs.com/technologies/details/os-linux) 的数据,Debian 占据了互联网上所有服务器的 16%。它的衍生版 Ubuntu 运行了其中的 33%。两者合计占所有服务器的 49%。这使得与 Debian 相关的管理技能非常具有市场需求。

Ubuntu Linux

Ubuntu Linux 发行版(ubuntu.com/)因使 Linux 在个人计算机上变得流行而广受赞誉,的确如此。由 Canonical 资助,其使命是使 Linux 对大多数人来说更易使用。它是第一个(如果不是第一个的话)通过分发非自由和非开源的二进制驱动程序和库,简化桌面使用并使其更加舒适的 Linux 版本之一。

著名的是,Mark Shuttleworth(Canonical 和 Ubuntu 的创始人)为 Ubuntu 发行版开设的第一个错误报告是:“微软拥有市场份额”。

该发行版本身基于 Debian Linux,最初的主要目标之一是实现完全的二进制兼容性。随着开发的进展,这一目标的相对重要性有所下降。

该发行版由社区和 Canonical 开发。公司的主要收入来源是与 Ubuntu Linux 相关的高级服务——支持、培训和咨询。

由于 Debian Linux 和 Ubuntu Linux 之间非常紧密的关系,许多开发人员和维护人员在一个发行版中的角色也在另一个发行版中担任相同职务。这导致了大量的软件同时为两个发行版打包。

Ubuntu 有三种主要版本——桌面版、服务器版和核心版(用于物联网)。桌面版和服务器版在默认配置的服务设置上可能略有不同,而核心版则差异较大。

软件以 .deb 包的形式分发,与 Debian 一样,源代码实际上是从 Debian 不稳定分支导入的。然而,这并不意味着你可以在 Ubuntu 上安装 Debian 包,反之亦然,因为它们不一定是二进制兼容的。应该可以重新构建并安装你自己的版本。

每个版本有四个软件包仓库——由 Canonical 官方支持的自由和非自由软件分别称为 mainrestricted。由社区提供和维护的自由和非自由软件分别称为 universemultiverse

重要提示

一条建议——在主要版本之间进行系统升级的普遍做法是等待第一个子版本发布。因此,如果当前安装的版本是 2.5,而新版本 3.0 已发布,建议等到 3.1 或甚至 3.2 发布后再进行升级。这适用于我们在这里列出的所有发行版。

长期支持LTS)版本的支持期限为五年。每两年发布一个新的 LTS 版本。也可以协商延长支持期。这为计划重大升级提供了一个非常好的时间表。每六个月发布一个新的 Ubuntu 版本。

Ubuntu Linux 在教育和政府项目中得到了广泛采用。著名的是慕尼黑市,在 2004 到 2013 年间,将超过 14,000 台市政桌面电脑迁移到了一个带有 KDE 桌面环境的 Ubuntu 版本。虽然这次迁移在政治上遭遇了干扰——其他操作系统供应商强烈反对这一迁移——但从技术角度来看,它被视为一次成功。

Ubuntu 是个人计算机的首选 Linux。Canonical 与硬件厂商,特别是联想和戴尔,紧密合作,最近也与 HP 合作,确保发行版与计算机之间的完全兼容性。戴尔销售的旗舰笔记本电脑预装了 Ubuntu。

有多个来源将 Ubuntu Linux 列为安装在服务器和个人计算机上的最多的 Linux 发行版。实际数字只能估算,因为 Ubuntu 不要求任何订阅或注册。

作为 Ubuntu Linux 流行的副产品,软件供应商通常会提供其软件的.deb包版本,尤其是对于桌面软件而言,这一点尤为明显。

基于 Ubuntu 的非官方版本、克隆或修改版的发行版数量惊人。

Ubuntu 拥有一个非常活跃的社区,既有组织化的也有非组织化的。你很容易就能找到你所在城市的用户群体。这也直接影响了互联网上教程和文档的数量。

Ubuntu Linux,尤其是在支持计划下,作为许多云计算基础设施部署的基础。许多电信、银行和保险公司已选择 Ubuntu Server 作为他们的基础平台。

Red Hat 企业 Linux(RHEL)

RHEL (www.redhat.com/en/technologies/linux-platforms/enterprise-linux) 是 Red Hat Linux 的精神继承者,由 Red Hat Inc. (www.redhat.com/)开发和维护。其主要目标是商业实体市场。对于开发或最多支持 16 台服务器的生产环境(截至写作时),可以免费使用 RHEL。然而,这个发行版的主要优势在于大量的文章资源,有助于解决问题,并且可以获得支持工程师的帮助,尽管后者只能通过付费支持计划获得。

RHEL 被认为是一个非常稳定和可靠的发行版。它是银行、保险公司和金融市场的主要选择之一。虽然缺乏许多流行的桌面软件包,但在服务器方面,尤其是在运行其他商业应用程序的操作系统方面,它是一等公民。

该软件以在线仓库包的形式分发,文件以.rpm结尾,因此得名RPMs。管理这些包的主要工具是 RPM,还有更复杂的工具——yum,最近的继任者dnf也可用。

作为一家基于开源的公司,Red Hat 提供了其发行版的源代码。这促成了一个著名的免费的开源 RHEL 克隆版本——CentOS的诞生。直到不久前,它一直是那些想要使用 RHEL,但又不愿意或无法支付订阅费用的人的热门选择。在 2014 年,CentOS 加入了 Red Hat 公司,而在 2020 年,Red Hat 宣布 CentOS 的版本发布将不再提供;将只会有所谓的滚动更新版本,它会不断更新软件包,并且不再与 RHEL 的发布版本一致。这一决定引发了 CentOS 用户的强烈反应。CentOS 的原始创始人 Gregory Kurtzer 创建了另一个 RHEL 克隆版本,名为Rocky Linux。它的主要目标与原始 CentOS 相同——提供一个免费的、开源的、由社区驱动的发行版,并且完全与 RHEL 二进制兼容。

RHEL 发行版每隔几年发布一个稳定版本,并且为这些版本提供 10 年的支持,从第 5 版开始。然而,完整的支持只会提供几年。其余时间,Red Hat 仅为系统提供安全修复和关键更新,不会引入新的软件包版本。然而,这种生命周期是大规模安装或关键任务系统用户所青睐的。

与 Ubuntu 类似,可以协商延长支持时间。

Red Hat 公司与开源社区的关系较为复杂。虽然该公司大部分时间是公平竞争,但也曾做出一些社区不满的决定。最近,Red Hat 做出了将 CentOS 发布模型更改为滚动发布的决定,这一决定引发了广泛争议 (lists.centos.org/pipermail/centos-announce/2020-December/048208.xhtml)。

与 Ubuntu 一样,RHEL 是商业支持云基础设施部署的首选基础。

Fedora Linux

Fedora (fedoraproject.org/wiki/Fedora_Project_Wiki)是与 Red Hat 公司相关的一个发行版。虽然超过 50%的开发者和维护者是与 Red Hat 无关的社区成员,但该公司在开发过程中拥有完全的管理权。它是 RHEL 的上游,这意味着它是实际 RHEL 的真正开发前端。这并不意味着 Fedora 中的所有内容都会被包含在 RHEL 的发布版本中。然而,紧跟 Fedora 的步伐将能够洞察 RHEL 发行版的当前方向。

与 RHEL 不同,Fedora 是 RHEL 的基础,Fedora 每六个月发布一次新版本。它使用与 RHEL 相同的软件包类型,RPM。

Fedora 被认为是一个节奏较快的发行版。它迅速采用最新和最前沿的包版本。

CentOS

CentOS (centos.org) 曾是 RHEL 的首选免费版本。该名称是 社区企业操作系统(Community Enterprise Operating System)的缩写。其主要目标是与 RHEL 完全二进制兼容,并遵循相同的版本和编号方案。2014 年,CentOS 加入了 Red Hat,但承诺该发行版将保持独立于该公司,同时享受开发和测试资源的支持。不幸的是,2020 年,Red Hat 宣布 CentOS 8 将是最后一个有编号的版本,从那时起,CentOS Stream 将成为唯一的变种。CentOS Stream 是一个中间版本,这意味着它位于前沿且快速发展的 Fedora 和稳定且生产就绪的 RHEL 之间。CentOS Stream 与 CentOS 的区别在于,Stream 是一个开发版,而 CentOS 只是实际最终产品 RHEL 的重建和打包镜像。

在使用 RHEL 时获得的所有知识、技能和经验,100% 可以应用于 CentOS。根据 W3Techs 的数据(w3techs.com/technologies/details/os-linux),由于 CentOS 是服务器上第三大最常部署的 Linux 发行版,这些技能在市场上非常有价值。

Rocky Linux

为了应对 CentOS 发行版的现状,其创始人宣布创建了 Rocky Linux (rockylinux.org/)。其目标与原始 CentOS 相同。发行方案和编号遵循 RHEL。宣布之后不久,Rocky Linux 的 GitHub 仓库成为了热门趋势(web.archive.org/web/20201212220049/https://github.com/trending)。Rocky Linux 与 CentOS 100% 二进制兼容。该项目已发布一套工具,可以轻松地将系统从 CentOS 迁移到 Rocky Linux,而无需重新安装系统。

该发行版相当年轻,成立于 2020 年,其受欢迎程度尚待观察。它在社区内引起了许多关注,似乎有一批 CentOS 用户将 Rocky Linux 作为他们的首选。

Rocky Linux 项目对开源世界的一个非常重要的贡献是其构建系统。它确保即使 Rocky Linux 停止运营,社区也能够轻松启动一个新的 RHEL 克隆版本。

所有适用于 RHEL 和 CentOS 的技能、知识和文章,100% 可以应用于 Rocky Linux。在 Rocky Linux 上,所有在 RHEL 和 CentOS 上运行的软件也应无需任何修改地运行。

Alpine

Alpine Linux (alpinelinux.org/) 是一个有趣的发行版。其主要的编程库和大部分基本命令行工具并非来自 GNU 项目。另外,目前在大多数发行版中使用的 systemd 服务管理系统在 Alpine 中并不常见。这使得其他主流发行版的一些管理技能在 Alpine 中不适用。Alpine 的优势在于其体积(相对较小)、注重安全的理念,以及在现有 Linux 发行版中最快的启动时间。正是这些特点,尤其是启动时间(毫无疑问是最重要的),使得它成为容器的最受欢迎选择。如果你运行容器化的软件或构建自己的容器镜像,很可能它就是基于 Alpine Linux。

Alpine 起源于 LEAF (Linux 嵌入式设备框架;参见:bering-uclibc.zetam.org/wiki/Main_Page) 项目——一个可以装入单张软盘的 Linux 发行版。LEAF 目前是嵌入式市场、路由器和防火墙的热门选择。Alpine 是一个更大的发行版,但必须做出这样的牺牲,因为开发者希望包含一些有用但相对较大的软件包。

包管理器叫做 apk。构建系统借鉴了另一个名为 Gentoo Linux 的发行版。由于 Gentoo 在安装软件时会进行构建,portage 显然包含了大量关于构建软件的逻辑,这些逻辑作为操作系统的一部分使用。

Alpine 可以完全从 RAM 中运行。它甚至有一个特殊的机制,可以让你最初只从启动设备加载一些必要的包,并且这可以通过 Alpine 的 本地备份 工具 (LBU) 实现。

如前所述,这是容器镜像首选的发行版。你不太可能看到它在大型服务器安装上运行,或者根本没有。当我们进入云计算世界时,你很可能会看到很多 Alpine Linux。

话虽如此,这些发行版中的每一个都有一个适用于云计算的变体,作为容器基础镜像——这是一种以真正的云计算方式运行你的软件的方法。

在这一章中,你了解了流行的 Linux 发行版的基础知识,以及它们之间的区别。你现在应该对你可以选择的发行版以及你将需要面对的后果(好与坏)有了一些了解。为了更好地让你了解如何与一些精选的 Linux 发行版互动,我们将研究如何在第二章中通过键盘与系统进行交互。

概述

本章中的简短清单只是现有 Linux 发行版的一小部分。这个清单主要基于技能的流行度和市场性,以及我们多年来积累的经验和知识。它们绝不是你唯一的选择,也不是最好的选择。

我们尝试指出主要优点所在,并阐明用户与各个发行版之间的关系。

我们可能无法回答你所有的问题。我们列表中的每个 Linux 发行版都有自己的书籍,而且在博客、维基和 YouTube 教程中还有更多的知识。

在下一章,我们将深入探讨命令行的神奇世界。

第二章:命令行基础知识

在本章中,我们将直接深入学习 Linux 命令行。我们将解释它的强大之处,以及如何对每个系统管理员和 DevOps 人员都至关重要。更重要的是,我们将开始教授你最有用的命令及其有效使用方式。在此过程中,我们还将添加其他核心 Linux 概念,因为这些概念对理解本章是必需的。

在本章中,我们将涵盖以下主要内容:

  • 什么是命令行及其工作原理

  • 为什么熟练掌握命令行如此重要

  • Linux 系统管理的基本命令

不可能在单一章节中介绍所有命令和工具。接下来我们选择了您需要了解的最基本工具。管理 Linux 系统是一个独立的书籍主题。恰好 Packt 出版社确实有几本相关出版物。

技术要求

强烈建议准备好并安装 Linux 系统以供使用。我们建议使用虚拟机或可以安全地从头开始重新安装的笔记本电脑。这将使您能够按照书中的示例并执行我们给出的任何练习。

我们不会涵盖安装过程。每个发行版可能都有自己的安装程序,可以是图形化的或文本的(取决于发行版及其选择的变体)。您需要记下或记住您的用户名(方便地称为用户名或登录名)和密码。如果您有物理访问权限但不知道登录名或密码,或两者都不知道,有方法可以进入系统,但这些超出了本书的范围。

我们书中的主要发行版是 Debian。然而,只要不是 Alpine,你在上一章中涵盖的任何主要发行版都应该可以使用。

Linux 命令行 - shell

Linux 系统管理员的自然环境是命令行。但是,你永远不会听到有人这样称呼它。正确的名称是shell,从现在开始我们在书中就这样称呼它。

Shell 是一个接受用户输入(主要是键盘输入,但也有其他方式,甚至可以使用鼠标指针)的程序,解释这些输入,如果是有效的命令则执行它,并向用户提供结果或错误信息,如果命令不能正确执行。

有几种访问 shell 的方法:

  • 登录到终端(图 2.1* 中的截图)

注意

你还会看到术语 console。终端和控制台是有区别的。控制台 是一个物理设备,让用户与计算机交互。它是物理输入(现在通常是键盘)和输出(现在通常是显示器,但最初输出是打印出来的)。终端 是一个控制台模拟器,一个程序,让用户执行相同的任务。

  • 在图形界面中打开一个终端窗口(如果你有的话)

  • 通过安全连接远程登录到另一台设备(手机、平板或你的电脑)

Shell 是一个非常强大的环境。一开始通过输入命令做所有事情可能看起来有些繁琐,但很快你会发现,任务越复杂,通过 shell 完成起来比图形界面更容易。每个有经验的 Linux 系统管理员都知道如何在 shell 中完成任务,以及如何通过它来管理系统,我敢打赌他们更倾向于使用 shell 而非任何 GUI。

了解你的 shell

Shell 是一个程序,因此并不是只有一个 shell。相反,有许多流行程度不同的 shell,它们各自呈现出不同的处理方式。

到目前为止,大多数 Linux 发行版中最流行且默认的 shell 是 BashBourne Again Shell)。还有其他一些你可能需要了解的 shell:

  • sh:原始的 Steve Bourne shell。它是最初的 shell,也是我们所知道的第一个编写出来的 shell。虽然它缺少许多用户从其他更现代的 shell 中获得的交互功能,sh 以其快速的脚本执行速度和小巧的体积而闻名。

  • ksh:作为 Bourne shell 的演变版本开发而来,它是其前身的超集。因此,所有为 sh 编写的脚本都能在 ksh 中运行。

  • csh:C shell。这个名字来自于它的语法,它与 C 编程语言的语法非常相似。

  • zsh:Z shell。对于 macOS 用户来说,它应该非常熟悉,因为它是该操作系统的默认 shell。它是一个完全现代化的 shell,提供了许多你所期待的功能:命令历史、算术运算、命令补全等。

我们不会用太多的 shell 变种和历史来困扰你。如果你对 Bash 的起源感兴趣,可以参考这篇维基百科文章:en.wikipedia.org/wiki/Bash_(Unix_shell)

在本书中,我们使用的是 Bash。如前所述,它是大多数 Linux 发行版的默认 shell,提供 sh 兼容模式,拥有现代 shell 所有的特性,而且关于它的书籍、文章、教程等资料极其丰富。

我们将要执行的第一个任务是登录到 Shell 中。根据你选择的安装方式,你可能需要启动本地虚拟机、物理机器或基于云的虚拟专用服务器VPS)。如果你选择了没有图形界面的服务器安装,你应该看到类似以下的屏幕截图:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/linux-dop-hb/img/B18197_02_1.jpg

图 2.1 – 登录界面

你会看到一个登录界面,你可以在此输入用户名和密码。成功登录后,你会看到一个命令提示符,表示你已经成功启动了 Shell。

提示符的外观是可配置的,并且可能根据你选择的发行版而有所不同。不过,有一件事会保持不变,我们建议你不要更改它。在 Linux 世界中,有两种类型的用户:普通用户超级用户。普通用户的登录名可以是任何符合 Linux 用户命名规范的名称,而超级用户的登录名是root,下面是其密码。

超级用户账户之所以这么命名,是有原因的。在大多数 Linux 发行版中,它是无所不能的用户。登录为root后,你可以做任何事情,甚至删除所有文件,包括操作系统本身。

为了帮助你区分普通用户和root用户,提示符会给你提供一个线索。当以root用户登录时,提示符会以#符号结尾。当以普通用户登录时,提示符则会以$符号结尾。

顺便提一下,#(井号)符号也是所谓的注释符号。如果你从互联网复制命令或输入命令时,若它以#$开头,这就是提示你该命令应该由哪种类型的用户来执行。你应该省略这个符号,尤其是#符号会阻止命令的执行。

除了结束提示符的符号,许多发行版会在其前面加上用户名,确保你知道自己是哪位用户。如果在一个名为myhome的 Linux 系统上有一个管理员用户,Debian 11 的默认提示符看起来像这样:

$admin@myhome:~$

对于root用户,它看起来像这样:

root@myhome:~#

需要说明的是,检查身份的方式有很多种,但我们将留待在第四章中讨论。

我呼唤你

到目前为止,一切都很好。你已经登录,知道自己是谁,可以输入命令并进行阅读。但实际操作程序是如何进行的呢?

在 Linux 术语中,运行一个程序就是执行它或调用它。实际上,调用这个词通常用于指代系统命令或 Shell 内建命令,而执行则用于谈论那些不属于发行版的一部分的程序——即所谓的第三方程序或二进制文件。

在我告诉你如何执行程序或调用命令之前,我得先解释一下文件系统结构和一个非常重要的系统变量,叫做 PATH

文件系统结构

由于这可能是你第一次接触,我们将稍微退后一步,解释一下文件系统的结构(换句话说,就是典型 Linux 系统中文件夹是如何组织的)。

Linux 遵循 Unix 的哲学思想,即一切皆文件。(虽然有例外,但不多。)其结果是操作系统几乎每个方面都以文件或目录的形式体现。内存状态、进程(正在运行的程序)的状态、日志、二进制文件和设备驱动程序都在这个结构中。也就是说,几乎你 Linux 系统的每个方面都可以通过普通的文本编辑工具进行编辑或检查。

在目录树中,它的结构总是从一个 / 文件夹开始,这个文件夹被称为 根目录。每个驱动器、网络共享和系统目录都在一个从根目录开始的层次结构中。

将网络共享或本地驱动器提供给系统或用户的过程被称为 /home/ 目录。你在浏览文件系统结构时是无法看出它的。唯一能看出来的方法是通过以下命令检查已挂载的驱动器和分区:

$ mount

或者

$ df

我们将在第三章中详细讲解它们,所以现在你只需要知道它们的存在即可。

最上层目录的名称是 /。我们已经讲过了。嵌套在另一个文件夹中的文件夹之间的分隔符也是 /。所以 /usr/bin 表示在 usr 目录中存在一个 bin 目录,而 usr 目录位于 / 目录中。很简单。

有一个非常方便的命令,可以让我们查看目录结构,叫做 tree。这个命令可能在你的系统中没有安装。如果没有,别担心;它并不是那么重要,你可以通过我们的讲解来了解。在第三章中,当我们介绍如何安装软件包时,你可以回过头来再试试这个命令。默认情况下,tree 命令会把整个目录结构显示出来,可能会让屏幕信息满得让人难以阅读和跟随。不过,有一个选项可以让我们限制查看的深度:

admin@myhome:~$ tree -L 1 /
/
|-- bin -> usr/bin
|-- boot
|-- dev
|-- etc
|-- home
|-- lib -> usr/lib
|-- lib32 -> usr/lib32
|-- lib64 -> usr/lib64
|-- libx32 -> usr/libx32
|-- lost+found
|-- media
|-- mnt
|-- opt
|-- proc
|-- root
|-- run
|-- sbin -> usr/sbin
|-- srv
|-- sys
|-- tmp
|-- usr
`-- var
22 directories, 0 files

这里有几个重要的概念需要讲解;不过我们暂时不会讲解所有目录。每当某个文件夹变得重要时,从第三章开始,我们会简要地提及它。

首先,调用 tree 命令。你看到了我的提示符,它告诉我当前以 admin 用户身份在名为 myhome 的系统上运行,并且我不是 root 用户(提示符末尾的美元符号)。如果你想运行 tree 命令,你可以跳过提示符。接下来是实际的命令调用:tree 加上 -L 选项和数字 1;这会指示程序只打印一个深度级别。换句话说,它不会进一步进入目录。最后,/ 符号告诉程序从文件系统的最开始——root 文件夹开始打印。

接下来,你会注意到某些行中有一个神秘的箭头,指向一个名字。这个箭头表示一个快捷方式。有两种类型的快捷方式,硬链接和符号链接。目录只能拥有符号链接。在前面的输出中,/bin 目录是指向 /usr/bin 目录的链接。从实际使用的角度来看,它们可以当作一个目录来对待。存在这个链接有技术上的原因和历史背景。过去,位于 /bin/sbin 目录中的工具用于挂载 /usr 分区,然后允许访问 /usr/bin/usr/sbin。如今,这一任务由启动过程中的其他工具更早处理,因此不再需要此链接。该结构的保留是为了与可能需要同时存在 /bin/sbin 目录的工具的向后兼容性。更多详情请参考 refspecs.linuxfoundation.org/FHS_3.0/fhs/index.xhtmlwww.linuxfoundation.org/blog/blog/classic-sysadmin-the-linux-filesystem-explained

既然我们已经提到了 /bin/sbin 目录,那么我们来解释一下它们之间的区别。/usr/bin 目录包含 /usr/sbin 目录,后者包含所谓的 root 用户。它还包含系统进程的二进制文件(称为 守护进程)——这些程序在后台运行并为正在运行的系统执行重要工作。

/root 目录是超级用户的主目录。所有与超级用户相关的配置文件都存放在这里。

有趣的是 /home 目录。这里存放着所有用户的主目录。当我为我的家用机器创建 admin 用户时,它被放置在 /home/admin 文件夹中。

对我们来说,目前重要的是 /etc/ 目录。它包含整个系统的所有配置文件:在线软件包仓库的源,默认的 shell 配置,系统名称,启动时会启动的进程,系统用户及其密码,以及基于时间的命令。在一个全新安装的 Debian 11 系统中,/etc/ 目录包含大约 150 个子目录和文件,每个子目录可能还包含更多的文件夹。

/tmp文件夹包含临时文件。它们只在系统启动时存在,一旦关闭或重启系统,它们会被删除。这些文件的性质通常非常不稳定;它们可能会迅速生成并消失,或者频繁被修改。这个目录有时仅存在于计算机的内存中。它是你系统中最快的存储设备,并且在重启或关闭电源时会自动清除。

如前所述,附加驱动器会在该结构下挂载。我们可能会为主目录分配一个单独的硬盘。整个/home文件夹可能会存放在该硬盘上,甚至存放在一个网络硬盘阵列中。如前所述,/tmp目录通常挂载在内存中。有时,/var目录(包含系统中经常变化的内容但不应被清除的文件,如日志)会挂载在单独的硬盘上。这样做的原因之一是,/var的内容,尤其是/var/log(系统日志所在的目录),可能会增长得非常快,占用所有可用空间,导致无法访问系统,或者变得非常困难。

最后,有两个重要且特殊的目录,遍布你访问的每个地方:

  • .:一个点表示文件夹,你将停留在当前位置。不过,它很有用,正如你将在第三章中看到的那样。

  • ..:两个点表示上级目录——..文件夹,你将进入比起始位置高一级的目录。请注意,对于/目录来说,...表示相同的目录:/。你无法再向上移动超出根目录。

运行程序。

现在我们对文件夹层次结构有了一些了解,接下来简要讲一下执行程序的过程。

在 Shell 中执行程序有三种基本方法:

  • PATH变量(在第三章中解释)。

  • /。当使用绝对路径时,必须列出所有指向程序的目录,包括前导的/。执行示例可能如下所示:

/opt/somecompany/importantdir/bin/program
  • ./。这个快捷方式表示./myprogram./bin/myprogram。后者意味着:在当前目录中的bin目录里启动一个名为myprogram的程序。

  • 要在系统中的其他目录调用程序时,我们需要使用两个点,表示父文件夹。假设你已经登录到你的主目录/home/admin,并想执行位于/opt/some/program/bin中的程序;你需要输入../../opt/some/program/bin/myprogram。这两个点和斜杠表示向上移动。

如果这看起来很神秘,那是因为它确实有点复杂。幸运的是,随着本书的推进,一切都会逐渐变得清晰。

用来教你所有命令的命令。

你应该养成遇到问题或有疑问时上网查找的习惯。大多数问题已经在网上得到了解决或解释。然而,有一个命令能拯救你的生命——或者至少能节省很多时间。你应该养成经常使用它的习惯——即使你确信自己知道正确的语法,你可能还是会发现一种更好的完成任务的方法。这个命令如下:

$ man

man 命令是 manual(手册)的简写,它正是它所说的:它是你想了解的任何命令的手册。要了解更多关于 man 命令的内容,只需调用以下命令:

$ man man

你看到的输出应该类似如下:

MAN(1)                                                                 
                                   Manual pager utils                                     
                                                               MAN(1)
NAME
       man - an interface to the system reference manuals
SYNOPSIS
       man [man options] [[section] page ...] ...
       man -k [apropos options] regexp ...
       man -K [man options] [section] term ...
       man -f [whatis options] page ...
       man -l [man options] file ...
       man -w|-W [man options] page ...
DESCRIPTION
       man is the system's manual pager.  Each page argument given to man is normally the name of a program, utility or function.  The manual page associated with each of these arguments is then found and displayed.  A section, if
       provided, will direct man to look only in that section of the manual.  The default action is to search in all of the available sections following a pre-defined order (see DEFAULTS), and to show only the  first  page  found,
       even if page exists in several sections.

我已简化内容以便简洁。编写良好的 man 页面将包含多个部分:

  • name:这里列出了命令的名称。如果命令有多个名称,它们都会列出。

  • synopsis:这将列出调用命令的可能方式。

  • description:这是命令的目的。

  • examples:这将展示几个命令调用的示例,以使语法更加清晰并提供一些思路。

  • options:这将显示所有可用选项及其含义。

  • getting help:这是获取简化版命令摘要的方式。

  • files:如果命令有配置文件或使用文件,并且这些文件在文件系统中已知存在,它们将被列在此处(对于 man,我列出了 /etc/manpath.config/usr/share/man)。

  • bugs:这是查找 bug 和报告新 bug 的地方。

  • history:这将显示程序的当前及所有历史作者。

  • see also:这些是与命令有某种关联的程序(对于 manapropos(1)groff(1)less(1)manpath(1)nroff(1)troff(1)whatis(1)zsoelim(1)manpath(5)man(7)catman(8)mandb(8))。

许多命令会包含大量额外的部分,这些部分是特定于该程序的。

man 页面包含大量知识,有时名称可能会重复。这时,括号中的神秘数字就派上用场了。man 页面分为多个部分。引用 man 页面中的 man

  1. 可执行程序或 shell 命令

  2. 系统调用(内核提供的函数)

  3. 库调用(程序库中的函数)

  4. 特殊文件(通常位于 /dev

  5. 文件格式和约定,例如,/etc/passwd

  6. 游戏

  7. 其他(包括宏包和约定),例如,man(7)groff(7)

  8. 系统管理命令(通常仅限root使用)

  9. 内核例程 [非标准]

举个例子,printf。有几种东西被称为 printf,其中之一是 C 语言的库函数。它的 man 页面位于第 3 节。

要阅读这个库函数,你必须告诉 man 去查找第 3 节:

admin@myhome:~$ man 3 printf
PRINTF(3)                                                              
                                Linux Programmer's Manual                                
                                                             PRINTF(3)
NAME
       printf, fprintf, dprintf, sprintf, snprintf, vprintf, vfprintf, vdprintf, vsprintf, vsnprintf - formatted output conversion
If you don't, what you'll get is a shell function for printing—totally useless in C programming:
admin@myhome:~$ man printf
PRINTF(1)                                                              
                                      User Commands                                        
                                                           PRINTF(1)
NAME
       printf - format and print data
SYNOPSIS
       printf FORMAT [ARGUMENT]...
       printf OPTION

大多数命令和 shell 程序都有一个简短的概要,叫做help。通常,可以通过运行带有-h或–help选项的二进制文件来调用它:

admin@myhome:/$ man --help
Usage: man [OPTION...] [SECTION] PAGE...
  -C, --config-file=FILE     use this user configuration file
  -d, --debug                emit debugging messages
  -D, --default              reset all options to their default values
      --warnings[=WARNINGS]  enable warnings from groff

我已经简化了输出,但你应该明白我的意思。

注意

短选项前面加一个短横线,而长选项前面加两个。–help不是一个长横线,而是两个标准的短横线。

man–help命令应该成为你的朋友,即使在你搜索在线资料之前。很多问题可以通过查看help输出快速得到答案。即使你是一个经验丰富的管理员,也可以忘记命令语法。网络上有一个无尽的 Linux 指南来源,叫做The Linux Documentation Projecttldp.org。将它加入书签吧。

了解你的环境

系统的行为由几个因素控制。其中之一就是一组被称为环境变量的变量。它们设置了系统与用户交互时使用的语言,列出文件时的排序方式,shell 寻找可执行文件的路径等许多其他内容。具体的环境变量集取决于所使用的发行版。

可以使用env命令打印出 shell 设置的所有环境变量的完整列表:

admin@myhome:/$ env
SHELL=/bin/Bash
PWD=/
LOGNAME=admin
XDG_SESSION_TYPE=tty
MOTD_SHOWN=pam
HOME=/home/admin
LANG=C.UTF-8
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

如果你知道要检查的变量,可以使用echo命令:

admin@myhome:/$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

请注意,当你使用一个变量时,你必须在变量名前加上美元符号,因此是$PATH

我当前在 PATH 中的哪个位置?

既然我们提到过PATH,那么我们简要地谈一下它。Path可以有两种含义:

  • 系统中的一个指向某个对象的地方:一个二进制文件,一个文件,或者一个设备

  • 一个列出 shell 在执行程序时会查找的地方的环境变量

你已经对第一种路径有了些了解。我们解释了绝对路径和相对路径。有一个命令可以让你在目录间移动,它叫做cd(如果不带参数,cd会将你带到家目录。如果你带上参数,它会将你移动到指定的文件夹,前提是文件夹存在,你指定的路径正确,并且你有权限访问该文件夹。我们来看看几个例子:

  • 检查当前所在的目录:
admin@myhome:~# pwd
/home/admin
  • 切换目录到/home/admin/documents/current
admin@myhome:~# cd documents/current
admin@myhome:~/documents/current#
  • 从当前目录上级跳转:
admin@myhome:~/documents/current# cd ..
admin@myhome:~/documents#
  • 切换到用户的家目录:
admin@myhome:~/documents# cd
admin@myhome:~# pwd
/home/admin

了解你的权限

Linux 中最基本的安全机制是基于为一组实体定义权限组合。权限如下:

  • read

  • write

  • execute(在谈到目录时,表示读取内容)

实体如下:

  • 文件或目录的拥有者

  • 拥有文件或目录的组

  • 所有其他用户和组

这是一个粗略的安全系统。它足以用于小型服务器和桌面使用,但对于更复杂的设置,有时它的限制太大。还有其他附加的系统,例如访问控制列表ACLs)、AppArmor、SELinux 等。本书不打算涵盖这些内容。

使用之前提到的系统,我们仍然可以在系统安全性方面取得相当大的进展。

这些权限和所有权是如何工作的?我们使用ls命令(列出文件和目录):

admin@myhome:/$ ls -ahl
total 36K
drwxr-xr-x 3 admin admin 4.0K Aug 20 20:21 .
drwxr-xr-x 3 root  root  4.0K Aug 20 14:35 ..
-rw------- 1 admin admin  650 Aug 20 20:21 .Bash_history
-rw-r--r-- 1 admin admin  220 Aug  4  2021 .Bash_logout
-rw-r--r-- 1 admin admin 3.5K Aug 20 14:47 .Bashrc
-rw-r--r-- 1 admin admin    0 Aug 20 14:40 .cloud-locale-test.skip
-rw------- 1 admin admin   28 Aug 20 16:39 .lesshst
-rw-r--r-- 1 admin admin  807 Aug  4  2021 .profile
drwx------ 2 admin admin 4.0K Aug 20 14:35 .ssh
-rw------- 1 admin admin 1.5K Aug 20 14:47 .viminfo

输出呈现为九列。

第一列简洁地呈现条目的类型和权限,但让我们跳到第三列和第四列。这些列告诉我们谁是文件的所有者,以及该文件属于哪个用户组。所有文件和目录必须属于一个用户和一个组。在前面的输出中,大多数文件属于用户 admin 和组 admin。例外是..目录,它属于root用户和root组。通常用user:group的形式表示这对。

接下来的列描述了文件的大小(对于目录,它描述的是条目的大小,而不是目录内容占用的空间),最后修改的日期、最后修改的时间以及条目的名称。

现在,让我们回到第一列。它告诉我们,文件或目录的所有者、组和系统中所有其他用户被允许对该条目执行什么操作:

  • 字母d表示我们正在处理的是一个目录。一个破折号()表示它是一个文件。

  • 接下来是一组九个单字符符号,表示谁可以对给定的条目执行什么操作:

前三个字母表示文件或目录的所有者可以对其执行的操作。r表示他们可以读取,w表示他们可以写入,x表示他们可以将其作为程序执行。如果一个文本文件设置了x,shell 将尝试将其作为脚本运行。需要注意的是,当我们处理目录时,x表示我们可以将当前工作目录切换到该目录。有可能进入一个目录(x设置)但无法查看其中的内容(r未设置)。

相同的三个字母在第二组中解释了组的权限。

相同的一组符号在第三组中解释了所有其他用户的权限。

在前面的输出中,.Bash_history是一个文件(它在第一列有一个破折号);文件的所有者(用户 admin)可以读取并写入该文件。可能会有这样的情况:可以写入一个文件(例如日志文件),但无法读取它。该文件不能作为脚本执行(破折号)。接下来的六个破折号告诉我们,分配给 admin 组的用户以及系统中任何其他用户或组都不能对该文件执行任何操作。

有一个例外,就是root用户。除非通过 ACL 和诸如 SELinux 等工具提升权限,否则无法限制root在系统中的全能性。即使是没有权限分配的文件或目录(全是破折号),root也能完全访问。

所有权和权限的管理是通过两条命令来完成的:

  • chown:此命令允许你更改文件或目录的所有权。该命令的名称是更改所有者的缩写。语法非常简单。让我们借此机会练习一下 Linux 帮助文档中的表示法:
chown [OPTION]... [OWNER][:[GROUP]] FILE...

有一个不成文的约定,几乎所有命令的帮助文档都会遵循:

  • 没有括号的文本必须按显示方式输入。

  • 任何位于[ ]括号中的内容是可选的。在chown命令中,用户和组是可选的,但你必须至少提供一个。

  • 尖括号< >中的文本是强制性的,但它是一个占位符,代表你需要提供的内容。

  • 花括号{ }表示一组选项,你需要选择其中一个。它们可以通过竖线|分隔。

  • 元素后面跟三个点表示该元素可以多次提供。在chown的例子中,它是文件或目录的名称。

以下是我对文件应用的一组所有权变更:

  • 我将所有权从admin更改为testuser,但不改变组。请注意,进行更改实际上需要使用root账户(通过sudo命令,详见第三章)。

  • 我将所有权更改回admin,但将组更改为testuser

admin@myhome:~$ ls -ahl testfile
-rw-r--r-- 1 admin admin 0 Aug 22 18:32 testfile
admin@myhome:~$ sudo chown testuser testfile
admin@myhome:~$ ls -ahl testfile
-rw-r--r-- 1 testuser admin 0 Aug 22 18:32 testfile
admin@myhome:~$ sudo chown admin:testuser testfile
admin@myhome:~$ ls -ahl testfile
-rw-r--r-- 1 admin testuser 0 Aug 22 18:32 testfile

在前面的输出中,我们可以看到,成功调用一个命令时不会产生任何输出(chown),除非输出本身就是命令的目的(如ls)。这是 Linux 遵循的基本规则之一。在接下来的输出中,我们可以看到当命令因错误而终止时的情况——没有足够的权限来更改组:

admin@myhome:~$ chown :testuser testfile
chown: changing group of 'testfile': Operation not permitted

运行chown命令的另一种方式是指定一个参考文件,如下面的示例所示:

admin@myhome:~$ sudo chown —reference=.Bash_history testfile
admin@myhome:~$ ls -ahl testfile
-rw-r—r—1 admin admin 0 Aug 22 18:32 testfile
admin@myhome:~$ ls -ahl .Bash_history
-rw------- 1 admin admin 1.1K Aug 22 18:33 .Bash_history

使用–reference选项,我们可以指定一个文件作为我们更改的基准。这在我们进入下一章节时会变得更加有趣。

  • chmod:与chown命令类似,chmod更改模式的缩写)是你用来更改分配给用户和组的权限的命令:
admin@myhome:~$ chmod --help
Usage: chmod [OPTION]... MODE[,MODE]... FILE...
  or:  chmod [OPTION]... OCTAL-MODE FILE...
  or:  chmod [OPTION]... --reference=RFILE FILE...

chmod命令将接受选项、强制模式、可选的更多模式以及需要应用更改的文件列表。与chown命令一样,我们可以指定一个参考文件,其模式将被复制过来。

在第一种形式中,你将指定一个用户、一个组、其他人或所有这些的权限模式,语法如下:

chmod [ugoa…] [-+=] [perms…] FILE...

这里适用以下含义:

  • u:用户,也就是文件的拥有者

  • g:拥有文件的组

  • o:其他人——所有其他人

  • a:所有,意味着每个人

  • -:移除指定的权限

  • +:添加指定的权限

  • =:将权限设置为完全符合指定的内容

让我们来看一些例子。

这为文件所有者添加了testfile文件的读写权限:

chmod u+rw testfile

这会移除对testfile文件的执行权限,适用于所有不是文件所有者且不在文件所有组中的用户:

chmod o-x testfile

这为用户和组授予了testfile文件的读取和执行权限:

chmod ug=rx testfile

在我们粘贴的语法总结中,中间一行很有趣。八进制模式意味着我们可以通过数字指定模式。这在脚本中尤其有用,因为处理数字更简单。一旦你记住了模式数字,你可能会发现使用八进制chmod更加方便。

设置文件模式的数字公式很简单:

  • 0:无权限 (---)

  • 1:执行模式 (--x)

  • 2:写入模式 (-w-)

  • 4:读取模式 (r–)

要设置文件或目录的模式,你将使用你想要应用的权限的总和,前面加上一个0,这将告诉chmod你正在设置八进制模式。语法如下:

chmod [OPTION]... OCTAL-MODE FILE...

这种形式和字母形式之间有一个非常重要的区别——你必须为所有三种实体(用户、组和其他)指定模式。你不能省略其中任何一个。这意味着你也不会使用-+=符号。使用八进制语法时,权限将始终严格按指定方式设置。

要组合多个模式,你需要将它们的数字相加,并使用这个总和作为最终的指定。你会发现这是一个非常优雅的解决方案。没有两个相同的数字组合(权限组合)。试试吧:

  • 执行和读取是 1 和 2,合起来是 3。没有其他组合可以得到 3。

  • 读和写是 2 和 4,合起来是 6。再次强调,没有其他组合能得到 6。

现在,让我们尝试一些例子。

文件的所有者将拥有文件的读写权限,而组用户和其他用户将只有读取权限:

chmod 0644 testfile

文件的所有者拥有所有权限(读、写和执行),而组用户和其他用户只能读取和执行:

chmod 0755 testfile

模式前面的0并非必需。

我们已经讲解了文件系统、目录结构以及与用户和组相关的基本文件权限。在下一部分,我们将介绍基本的 Linux 命令。

与系统交互

存储在硬盘上的程序和脚本只是文件。当它们被映射到内存并开始执行时,它们就变成了进程。在这一阶段,你可以安全地假设系统中运行的任何东西都是某种进程。

处理这个

Linux 中的进程有几个特性是你需要了解的:

  • 进程 ID (PID):一个系统范围内唯一的数字标识符。

  • 父进程 IDPPID):Linux 系统中的每个进程(除进程号为 1 的进程外)都有一个父进程。进程号为 1 的进程是 init 进程。它是负责启动所有系统服务的程序。启动另一个程序的程序被称为 父进程。由另一个程序启动的程序被称为 子进程。当你登录到系统时,你的 shell 也是一个进程,并且它有自己的 PID。当你在该 shell 中启动一个程序时,你的命令行 PID 将成为该程序的父进程 ID。如果一个进程失去了它的父进程(即父进程在未终止其子进程的情况下终止),那么该子进程将被分配一个新的父进程:进程号 1。

  • S 状态,进程进入休眠状态。然而,它不会接受任何中断和信号。只有当请求的资源可用时,它才会醒来。

  • T:我们可以指示程序停止执行并等待。这被称为 停止状态。这样的进程可以通过使用特殊信号重新启动执行。

  • Z:僵尸进程。当一个进程结束其执行时,它会通知其父进程。进程终止时也会发生同样的事情。父进程负责将其从进程表中移除。在此之前,进程会保持在僵尸状态,也叫做 defunc

  • 用户:进程的所有者,或者更准确地说,是使用该用户权限执行进程的用户。如果该用户无法做某事,进程也无法做到。CPU:进程所使用的 CPU 时间的百分比,以 0.0 到 1.0 之间的浮动数字表示。MEM:内存使用量,同样在 0.0 到 1.0 之间,其中 1.0 表示系统内存的 100%。

每个进程都有比我们刚才提到的更多特征,但这些是绝对必要的。管理进程是一个独立章节的主题,如果不是整本书的内容。

检查进程的工具叫做 ps。乍一看,它似乎是一个非常简单的命令,但实际上,man 页面展示了大量的选项。需要注意的是,ps 本身只会打印出系统的快照。它不会监控并更新其输出。你可以将 pswatchrun tophtop 命令结合使用,以持续获取关于进程和系统负载的信息。

在最简单的形式下,ps 将打印出与调用它的用户 ID 相同的所有进程:

admin@myhome:~$ ps
    PID TTY          TIME CMD
  24133 pts/1    00:00:00 Bash
  25616 pts/1    00:00:00 ps

在我的 Linux 机器上,只有两个进程在为我的用户运行:Bash shell 和 ps 程序本身。

ps 命令有几个有趣的选项,可以帮助查询系统中的运行进程。现在,为 ps 指定选项是有点棘手的,因为它接受两种语法,一种带有连字符,另一种不带,有些选项根据连字符的不同有不同的含义。让我引用一下 man 页面:

       1   UNIX options, which may be grouped and must be preceded by a dash.
       2   BSD options, which may be grouped and must not be used with a dash.
       3   GNU long options, which are preceded by two dashes.
Note that ps -aux is distinct from ps aux.  The POSIX and UNIX standards require that ps -aux print all processes owned by a user named x, as well as printing all processes that would be selected by the -a option.  If the
       user named x does not exist, this ps may interpret the command as ps aux instead and print a warning.  This behavior is intended to aid in transitioning old scripts and habits.  It is fragile, subject to change, and thus
       should not be relied upon.

为了简洁起见,我已经省略了一些输出内容。一旦你开始使用 shell 脚本,这个区别可能变得很重要,因为它们可能会采用三种语法中的任何一种。每当你有疑问时,请使用破折号表示法:

  • -f:所谓的 PIDPPIDCSTIMETTYTIME 以及启动进程的命令:
admin@myhome:~$ ps -f
UID          PID    PPID  C STIME TTY          TIME CMD
admin      24133   24132  0 16:05 pts/1    00:00:00 -Bash
admin      25628   24133  0 17:35 pts/1    00:00:00 ps -f
  • -e:所有用户的所有进程:
admin@myhome:~$ ps -e
    PID TTY          TIME CMD
      1 ?        00:00:04 systemd
      2 ?        00:00:00 kthreadd
[...]
  25633 ?        00:00:00 kworker/u30:0-events_unbound
  25656 pts/1    00:00:00 ps
  • -ef:以长格式查看所有进程:
admin@myhome:~$ ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 Aug20 ?        00:00:04 /sbin/init
root           2       0  0 Aug20 ?        00:00:00 [kthreadd]
  • -ejH:一个漂亮的进程树。输出中缩进更多的 CMD(最后一列)是较少缩进的 CMD 的子进程:
admin@myhome:~$ ps -ejH
    PID    PGID     SID TTY          TIME CMD
      2       0       0 ?        00:00:00 kthreadd
      3       0       0 ?        00:00:00   rcu_gp
      4       0       0 ?        00:00:00   rcu_par_gp
      6       0       0 ?        00:00:00   kworker/0:0H-events_highpri
      9       0       0 ?        00:00:00   mm_percpu_wq

还有很多其他选项可用,特别是用于控制哪些字段是感兴趣的。我们将在后续章节中继续讨论这些。

有一个名字可能会让人误解的命令,叫做 kill 命令。它用于向正在运行的进程发送所谓的信号。信号是一种通知进程执行某种操作的方式。其中一个信号确实会终止程序,立即将其结束,但这只是其中的一种。

要列出现有信号,请使用 kill -l 命令:

admin@myhome:~$ admin@myhome:~$ kill -l
 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

好吧,这个列表很可爱,但它什么都没告诉我们。我们怎么知道这些信号分别有什么作用呢?下面来做点侦探工作。首先,由于我们在 kill -l 的输出中看到了这些内容,我们可以运行 man kill 命令,看看是否有什么能解释这些信号:

EXAMPLES
       kill -9 -1
              Kill all processes you can kill.
       kill -l 11
              Translate number 11 into a signal name.
       kill -L
              List the available signal choices in a nice table.
       kill 123 543 2341 3453
              Send the default signal, SIGTERM, to all those processes.
SEE ALSO
       kill(2), killall(1), nice(1), pkill(1), renice(1), signal(7), sigqueue(3), skill(1)

例子部分显示并描述了一个信号,但在 SEE ALSO 部分,我们看到有一个指向 man 页面中第七部分的信号的引用。我们来检查一下:

admin@myhome:~$ man 7 signal
SIGNAL(7)                                                              
                                 Linux Programmer's Manual                                     
                                                         SIGNAL(7)
NAME
       signal - overview of signals

现在,有一个漂亮的页面,其中有一个表格列出了你在 Linux 中可以使用的所有信号。

那么,如何使用这个 kill 命令呢?你需要学习所有这些信号吗?答案是否定的。你会使用一些常见的信号。如果你忘记了某个信号,别犹豫,直接查阅 man 或在网上搜索:

  • kill -9 PID:臭名昭著的 SIGKILL。这会强制终止我们指定 PID 的进程,且会突然中断,忽略任何它可能进行的清理工作。如果该进程已打开文件句柄,它不会释放;如果需要写入文件或与其他程序同步,它也不会执行。这个命令应该谨慎使用,仅在我们确定必须停止程序时才使用。

  • kill PID:如果没有指定信号,默认会发送 SIGTERM。这会告诉程序停止运行,但要优雅地停止——执行所有必要的退出例程并清理资源。如果你不得不使用 kill 命令,这是首选的使用方式。

  • kill -1:所谓的 SIGHUP 信号。它最初用于检测用户连接丢失——电话线路挂断。现在,它常常用于通知进程重新读取其配置。

这里是一个调用 kill 来终止进程的例子。我启动了一个 shell 脚本,做的事情只是等待键盘输入。我将它命名为 sleep.sh

admin@myhome:~$ ps aux | grep sleep
admin      24184  0.0  0.2   5648  2768 pts/1    S+   16:09   0:00 /bin/Bash ./sleep.sh
admin@myhome:~$ pgrep sleep.sh
24184
admin@myhome:~$ kill -9 24184
admin@myhome:~$ pgrep sleep.sh
admin@myhome:~$ ps aux | grep sleep
admin      24189  0.0  0.0   4732   732 pts/0    S+   16:09   0:00 grep sleep

首先,我使用ps aux命令并在输出中查找sleep.sh进程,目的是向你展示它确实存在。接着,我使用pgrep命令快速找到正在运行的脚本的 PID,并将该 PID 传递给kill -9命令。实际上,sleep.sh进程已经被终止。你可以在另一个终端中确认这一点,那时我正运行着sleep.sh

admin@myhome:~$ ./sleep.sh
Killed

如果我只是简单地使用kill命令,输出会有所不同:

admin@myhome:~$ ./sleep.sh
Terminated

还有一种方式可以将所有信号传递给正在运行的程序,但前提是当前程序正在我们登录的 shell 中运行,并且处于前台状态;这意味着它控制了屏幕和键盘:

  • 按下Ctrl + C键会向程序发送SIGINT信号。SIGINT告诉程序用户已经按下了该组合键,程序应该停止。程序如何终止还取决于它自身的实现。

  • 按下Ctrl + D键会发送SIGQUIT信号——它类似于SIGINT,但还会生成所谓的核心转储(core dump),也就是一个可以在调试过程中使用的文件。

在文本中表示这些组合键的常见方式是^c^d^代表Ctrl键),ctrl+cctrl+dctrlCtrl键的快捷方式),以及C-cC-dC代表Ctrl键)。

在找东西吗?

有时你需要在文件系统中查找一个目录或文件。Linux 提供了一些命令,可以让你执行这一操作。在这些命令中,find是最强大的。要详细介绍它的所有功能需要更多的篇幅,而我们目前没有足够的空间。你可以查找与指定名称完全匹配的文件或目录,查找名称中包含你指定的部分字符的文件或目录,查找在特定时间修改过的文件,查找属于某个用户或组的文件,以及更多其他情况。此外,对于每一个找到的文件,还可以执行一些操作,比如重命名、压缩或搜索某个单词。

在下面的例子中,我们正在查找一个文件signals.h,它位于/``usr目录下:

admin@myhome:/$ find / -name os-release
find: '/lost+found': Permission denied
find: '/etc/sudoers.d': Permission denied
/etc/os-release
find: '/etc/ssl/private': Permission denied
/usr/lib/os-release
[...]

首先,我们调用find命令,然后告诉它从文件系统的根目录(/)开始查找,接着告诉它查找一个名为os-release的文件(-``name os-release)。

你会注意到,在输出中(为了简洁起见我省略了一部分),有一些错误信息,表示find没有权限访问某些文件。

如果你不确定名称的大小写,即它是否包含小写字母或大写字母(记住,Linux 是区分大小写的,OS-Releaseos-release不是同一个文件),你可以使用-iname选项。

如果你确定你要找的是一个文件,那么可以使用-type f选项:

admin@myhome:/$ find / -type f -name os-release

对于目录选项,使用-``type d

如果要查找与某个模式匹配的文件,比如以.sh结尾的文件,可以使用以下模式:

admin@myhome:/$ find / -type f -name "*.sh"

星号表示任意数量的任意字符。我们将它放在引号中,以避免在 find 有机会执行之前,shell 先解释星号。我们将在下一章 中级 Linux 中解释所有特殊符号(称为 globs)和正则表达式。

要删除所有找到的文件,你可以使用 -delete 选项:

admin@myhome:~$ find . -type f -name test -delete

要对找到的文件执行操作,你可以使用 -exec 选项:

admin@myhome:/$ find / -type f -name  os-release -exec grep -i debian {} \;
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
NAME="Debian GNU/Linux"
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

在前面的示例中,我们使用了名为 grep 的命令来查找包含 debian 这个单词的所有行,无论是大写还是小写。

这引出了 grep 命令。

grep 命令用于在文件中查找某个模式的出现。找到匹配的行后,它会打印该行。grep 命令与 find 命令类似,不同之处在于它的目的是在文件内部进行搜索,而 find 只关心文件和目录本身的特性。

假设有一个名为 red_fox.txt 的文本文件,内容如下:

admin@myhome:~$ cat red_fox.txt
The red fox
jumps over
the lazy
brown dog.

顺便说一下,cat 是一个将指定文件内容打印到终端的命令。

假设我们想查找所有包含单词 the 的行:

admin@myhome:~$ grep -i The red_fox.txt
The red fox
the lazy

你可能已经猜到,-i 选项表示我们不在乎大小写。

等等,我们可以搜索替代项。假设我们关心的行包含 foxdog。你可以使用 -e 选项为每个要搜索的单词添加一次,或者使用 -E 选项将所有单词放在单引号内,并用 | 字符分隔:

admin@myhome:~$ grep -e fox -e dog red_fox.txt
The red fox
brown dog.
admin@myhome:~$ grep -E 'fox|dog' red_fox.txt
The red fox
brown dog.

通过添加 -n 选项,你将获得匹配项所在行的行号:

admin@myhome:~$ grep -n -E 'fox|dog' red_fox.txt
1:The red fox
4:brown dog.

你可以查找以指定单词开头的行或在行尾具有特定模式的行。

你甚至可以在一个目录层次结构中的所有文件中执行 grep 搜索。语法稍有不同:模式放在前面,目录放在后面。同时,你还会得到匹配所在的文件名:

admin@myhome:~$ grep -r "fox" .
./red_fox.txt:The red fox

findgrep 的最大威力来自于一个叫做 正则表达式regexregexp 的简称)的概念,它们有一本专门的书,且对于新用户可能会感到困惑。我们将在 第三章第四章 中详细解释它们。不过,我们会仅介绍最常用的用法。

如果你正在寻找一个程序并且想知道它的完整路径,有一个命令可以做到,那就是 whereis。这里有一个示例:

admin@myhome:~$ whereis ping
ping: /usr/bin/ping /usr/share/man/man8/ping.8.gz

whereis 命令不仅会打印二进制文件的完整路径,如果安装了相应的 man 手册页,它还会打印该手册页。

让我们进行一些操作

对文件和目录可以执行四种基本操作:

  • 创建

  • 重命名或移动

  • 删除

  • 复制

每一个操作都有一个特殊的工具:

  • mkdir:这个命令的语法非常简单且有限。基本上,你只是告诉它创建一个指定名称的目录。如果你要创建嵌套目录,也就是一个文件夹包含另一个文件夹,路径中的所有目录必须存在。如果它们不存在,你可以使用特殊的-p选项来创建它们:
admin@myhome:~$ mkdir test
admin@myhome:~$ ls -l
total 4
drwxr-xr-x 2 admin admin 4096 Aug 24 15:17 test
admin@myhome:~$ mkdir something/new
mkdir: cannot create directory 'something/new': No such file or directory
admin@myhome:~$ mkdir -p something/new
admin@myhome:~$ ls -l
total 8
drwxr-xr-x 3 admin admin 4096 Aug 24 15:18 something
drwxr-xr-x 2 admin admin 4096 Aug 24 15:17 test

在前面的示例中,你可以看到我直接在我的主文件夹中创建了一个名为 test 的目录。接下来,我尝试在something文件夹中创建一个new文件夹。然而,后者并不存在,mkdir告诉我这一点并拒绝创建新的目录。我使用了特殊的-p选项来创建一个完整的路径以便创建新的目录。

  • mv:这是一个用于移动和重命名文件及目录的命令。同样,语法相当简单,尽管这个命令提供了一些额外的功能,比如创建移动文件的备份。

    要重命名一个文件或目录,我们将其从当前名称移动到新名称:

admin@myhome:~$ mv test no-test
admin@myhome:~$ ls -l
total 8
drwxr-xr-x 2 admin admin 4096 Aug 24 15:17 no-test

查看其man页面或help信息以了解更多。

  • rm:这个命令比较有趣,主要因为它提供了安全功能。通过特殊的-i选项,你可以指示它在删除文件或目录之前始终询问你。通常,rm会对目录进行退出,如下例所示:
admin@myhome:~$ admin@myhome:~$ ls -l no-test/
total 0
-rw-r--r-- 1 admin admin 0 Aug 24 15:26 test
admin@myhome:~$ rm no-test/
rm: cannot remove 'no-test/': Is a directory
admin@myhome:~$ rm -d no-test/
rm: cannot remove 'no-test/': Directory not empty
admin@myhome:~$ rm no-test/test
admin@myhome:~$ rm -d no-test/

我在no-test目录中创建了一个名为 test 的文件。rm拒绝删除该文件夹。我使用了-d选项,指示命令删除空目录。然而,该目录仍然包含一个文件。接着,我删除了文件,然后使用rm -d成功删除了no-test文件夹。我本可以使用-r选项,这将使命令删除所有目录,即使它们不是空的。

  • cp:这个命令用于复制文件和目录。请注意,与rm类似,cp会拒绝复制目录,除非使用-r选项。cp可以说是所有命令中最复杂且功能最丰富的命令之一,包括备份文件、创建链接(快捷方式)代替真正的复制等功能。查看它的man页面。在以下示例中,我将something目录复制到new目录。显然,我必须使用-r选项。接着,我创建了一个名为file的空文件并将其复制到newfile。对于这些,我不需要任何选项:
admin@myhome:~$ ls -l
total 4
drwxr-xr-x 3 admin admin 4096 Aug 24 15:18 something
admin@myhome:~$ cp something/ new
cp: -r not specified; omitting directory 'something/'
admin@myhome:~$ cp -r something new
admin@myhome:~$ ls -l
total 8
drwxr-xr-x 3 admin admin 4096 Aug 24 15:33 new
drwxr-xr-x 3 admin admin 4096 Aug 24 15:18 something
admin@myhome:~$ touch file
admin@myhome:~$ cp file newfile
admin@myhome:~$ ls -l
total 8
-rw-r--r-- 1 admin admin    0 Aug 24 15:33 file
drwxr-xr-x 3 admin admin 4096 Aug 24 15:33 new
-rw-r--r-- 1 admin admin    0 Aug 24 15:34 newfile
drwxr-xr-x 3 admin admin 4096 Aug 24 15:18 something

你现在应该理解并能够使用 Linux 或类似系统中的基本命令行命令,比如创建、复制和删除文件;你还可以查找文本文件中的内容,或者按名称查找文件或目录。你对你工作系统中的进程也有所了解。在下一章中,我们将深化这些知识。

总结

我们展示的只是 Linux 管理员在工作中可能使用的几百个命令中的一小部分。正如本章开头所提到的,完整的参考超出了本书的范围。然而,我们学到的内容足以进行基本的系统操作,并为下一章的内容打下基础:更高级的 Linux 管理话题。

练习

  • 了解如何递归地应用chown——这意味着我们的chown调用应该进入目录并将更改应用于其中的所有项目。

  • 了解watch命令的功能。将其与ps命令一起使用。

  • 了解如何删除一个目录。

  • 对于你在这里学到的所有命令,阅读它们的–help输出。打开man页面并浏览其中的内容,特别是示例部分。

资源

第三章:中级 Linux

在本章中,我们将继续介绍 Linux shell。本话题非常广泛,足以写成一本独立的书。我们将回顾上一章的内容并介绍新的主题。

在本章中,我们将涵盖以下内容:

  • 通配符

  • 自动化重复任务

  • 软件安装

  • 用户管理

  • 安全外壳SSH)协议

技术要求

强烈建议你安装并准备好使用 Linux 系统。我们建议使用虚拟机或笔记本电脑,这样如果出了什么大问题,可以安全地重新安装。这将让你能够跟随本书的示例并完成我们给出的任何练习。

我们不会讲解安装过程。每个发行版可能会使用不同的安装程序,无论是图形界面还是文本界面(具体取决于发行版及所选变种)。你需要记下或记住你的用户名称(方便地称为用户名登录名)和密码。如果你有物理访问权限,但不知道登录名或密码,甚至两者都不知道,还是有方法可以进入系统,但这些方法远超本书的范围。

本书的主要发行版是Debian。不过,你应该可以使用上一章中介绍的任何主要发行版,只要它不是 Alpine。

通配符

Shell 能为你做很多事情,帮助你简化工作。其中之一就是允许在输入命令参数时存在一定的不确定性。为此,shell 定义了几个特殊字符,将它们作为符号使用,而不是字面输入。这些符号称为全局模式,或称为通配符。在通配符中使用的字符有时被称为通配符(wildcards)。

不要将通配符与正则表达式regexps)混淆。虽然通配符本身非常强大,但它们无法与正则表达式相比。另一方面,bash 在执行模式匹配时并不会对正则表达式进行求值。

下表描述了 Shell 通配符及其含义。我们将通过几个示例来解释它们的确切含义:

通配符含义
*匹配任意数量的字符(包括零个字符)
?精确匹配一个字符
[...]匹配括号内集合中的任意一个字符

表 3.1 – Shell 通配符

上表可能对你来说不够清晰,下面我们将通过一些示例来进一步说明:

示例含义
*这将匹配任意长度的任何字符串。
*``test*这将匹配任何包含test的字符串:test.txtgood_test.txttest_run,甚至是简单的test(记住,它也可以匹配空字符串)。
test*txt这将匹配任何名称以test开头,txt结尾的文件,例如 test.txttesttxttest_file.txt 等。
test?这将匹配任何一个包含test并加上一个字符的情况:test1test2testatest,等等。
test.[ch]这将匹配两种情况中的一种:test.ctest.h,其他情况不匹配。
*.[``ab]这将匹配任何以点号结束并且后面跟着 ab 的字符串。
?[``tf]这将匹配任何一种字符,后跟 tf

表 3.2 – Shell 通配符 – 示例

通配符的真正威力在于你开始编写一些更复杂的命令字符串(所谓的单行命令)或脚本时才能显现。

一些简单命令在与通配符结合使用时会达到全新的层次,像是 findgreprm。在以下示例中,我使用通配符删除所有以任意字符开始、接着是 test 和一个点,然后是 log 和任意字符结尾的文件。因此,weirdtest.loganothertest.log1test.log.3 文件会被匹配,但 testlogimportant_test.out 则不会。首先,让我们列出所有包含 test 字样的文件:

admin@myhome:~$ ls -ahl *test*
-rw-r--r-- 1 admin admin 0 Sep 17 20:36 importat_test.out
-rw-r--r-- 1 admin admin 0 Sep 17 20:36 runner_test.lo
-rw-r--r-- 1 admin admin 0 Sep 17 20:36 runner_test.log
-rw-r--r-- 1 admin admin 0 Sep 17 20:36 test.log
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.1
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.2
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.3
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.4
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.5
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.6
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.7

你会注意到,我使用了通配符(*)来实现我的目标。现在,是时候进行实际的删除操作了:

admin@myhome:~$ rm *test.log*
admin@myhome:~$ ls -ahl *test*
-rw-r--r-- 1 admin admin 0 Sep 17 20:36 importat_test.out
-rw-r--r-- 1 admin admin 0 Sep 17 20:36 runner_test.lo

正如这里演示的那样,它已经成功执行了。你还会注意到,一个正确执行的命令不会打印任何消息。

在本节中,我们解释了如何使用通配符(globs)——这些特殊字符允许我们以一定的不确定性匹配系统中的名称。在下一节中,我们将介绍自动化重复任务的机制。

自动化重复任务

有时你可能希望使某些任务变得重复。你可能会编写一个脚本来备份数据库、检查用户的主目录权限,或者将当前操作系统的性能指标转储到文件中。现代的 Linux 发行版提供了两种设置这些任务的方法。还有第三种方法允许你在延迟的时间一次性运行任务(at 命令),但在这里我们关注的是重复性任务。

Cron 作业

Cron 是一种传统的方式,用来定期执行需要在特定时间间隔内运行的任务。通常,它们应该被 systemd 定时器 取代,但许多软件仍通过 cron 作业提供可重复性,而 Alpine Linux 在最小化发行版的情况下不会包含这一特性。

Cron 作业本质上是定期执行的命令。这些命令及其触发定时器被定义在位于 /etc/ 目录中的配置文件中。不同的发行版会有不同数量的文件和目录。所有的发行版都会包含一个 /etc/crontab 文件。这个文件通常包含对其中字段的解释以及几个实际命令,可以作为模板使用。在以下的代码块中,我粘贴了来自默认 /etc/crontab 文件的解释:

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly

通常,有两种方法来设置 cron 任务。其一是将一个运行命令的脚本放置在四个目录中的一个:/etc/cron.hourly/etc/cron.daily/etc/cron.weekly/etc/cron.monthly。这些目录应该足以满足日常操作。/etc/crontab 文件指定了这些任务何时运行。

第二个选项是使用 crontab 命令。crontab 命令允许给定的用户在其 crontab 文件中创建一个条目。然而,系统范围的 crontab 文件(位于 /etc/ 目录中)和每个用户的 crontab 文件之间是有区别的。用户的 cron 文件不指定用户字段。所有条目都以用户的权限运行。我们来看看这些区别:

  • crontab -l 列出用户在其 crontab 文件中定义的所有 cron 任务。

  • crontab -e 让用户编辑 crontab 文件,添加、删除或修改任务。

Systemd 定时器

我们在这里不会详细介绍 systemd 定时器,只会简要提到它们为何可能是更好的选择。

Systemd 定时器 单元是 cron 守护进程 任务的更新版本。它们可以完成 cron 的所有功能。然而,它们提供了一些额外的能力:

  • 您可以指定任务在系统启动后某个时间运行。

  • 您可以指定任务必须在其他任务运行后的某个间隔内执行。

  • 对定时器单元的依赖甚至可以是服务单元——即普通的系统服务任务。

  • 精度更高。Cron 任务的精度通常只能到每分钟一次,而 Systemd 定时器的触发精度可以达到秒级。

在本节中,我们介绍了 cron 任务,并提到 systemd 定时器是自动化定期任务的两个最重要的方式。下一节中,我们将介绍软件管理。

软件安装

根据您选择的发行版和安装方式,您的系统可能缺少一些日常工作所需的软件。也可能有一天,您需要安装一款默认未安装的软件。

Linux 发行版在一些方面领先于其他操作系统,后来其他操作系统才开始效仿。Linux 操作系统上安装软件的常见方式是运行一个合适的命令,这个命令会下载一个二进制文件,正确地将其放置到系统中,必要时添加一些配置,并使其对用户可用。今天,这可能听起来并不算革命性。毕竟,我们生活在一个拥有 Apple App Store、Google Play 和 Microsoft Apps 的世界中。但是,在过去,当 Windows 和 macOS 用户需要在互联网上找到合适的软件安装程序时,Linux 用户只需一个命令就能安装大多数软件。

这一点在 DevOps 力图建立的自动化环境中至关重要,原因有多个:

  • 可安装的软件(以软件包形式分发)存储在发行版团队维护的仓库中。这意味着你不需要知道软件在互联网上的位置;你只需要知道它的包名,并确保它在仓库中。

  • 我们将在这里介绍的包管理标准(rpmdeb了解依赖关系。这意味着,如果你尝试安装的软件依赖于另一个尚未安装的软件,它会自动拉取并安装。

  • 我们将在这里介绍的发行版都有安全团队。它们与包维护者合作,确保修复已知的漏洞。然而,这并不意味着他们会主动研究这些包中的漏洞。

  • 仓库会在互联网上进行镜像。这意味着,即使其中一个仓库出现故障(下线、被 DDoS 攻击或其他原因),你仍然可以从全球各地访问它的镜像副本。这对于商业仓库未必适用。

  • 如果你愿意,可以在本地局域网中创建一个本地仓库镜像。这将为你提供最快的下载速度,但代价是大量的硬盘空间。软件包仓库可能非常庞大。

软件的数量和版本在许多方面取决于发行版:

  • 政策分发必须分发具有不同许可证类型的软件:有些发行版会禁止任何不完全开放和免费的软件(根据开源倡议定义)。其他发行版则会让用户选择是否添加包含更严格许可的软件的仓库。

  • 维护者数量和维护模式:显而易见,发行版能做的工作量取决于他们拥有多少人力资源。团队越小,能够打包和维护的软件就越少。部分工作是自动化的,但很多工作始终需要人工完成。由于 Debian 是一个非商业发行版,它完全依赖志愿者的工作。而 Ubuntu 和 Fedora 则有商业支持,部分团队成员甚至是由其中的公司雇佣的:Canonical 和 Red Hat。红帽企业 LinuxRHEL)完全由红帽员工构建和维护。

  • 你决定使用的仓库类型:一些软件厂商将它们的软件包分发在独立的仓库中,你可以将其添加到你的配置中,像使用普通的发行版仓库一样使用它们。不过,如果你这样做了,有些事情需要注意:

    • 第三方仓库中的软件不属于发行版的质量管理工作:这完全取决于仓库维护者——在这种情况下,就是软件供应商。这也包括安全修复。

    • 第三方仓库中的软件可能不会与核心发行版仓库同时更新:这意味着有时软件所需的包版本与发行版提供的包版本之间会存在冲突。而且,随着你向服务器添加更多第三方仓库,发生冲突的概率也会增加。

Debian 和 Ubuntu

Debian 发行版及其衍生版 Ubuntu 使用 DEB 包格式。它是专为 Debian 创建的。我们这里不会深入探讨其历史,只会根据需要触及一些技术细节。

直接操作包文件的命令是 dpkg。它用于安装、移除、配置,并且重要的是,构建 .deb 包。它只能安装存在于文件系统上的包,并且不理解远程仓库。让我们看看 dpkg 的一些可能操作:

  • dpkg -i package_file.deb:安装包文件。该命令会经过多个阶段,之后安装该软件。

  • dpkg –unpack package_file.deb:解包意味着它会将所有重要文件放入相应的位置,但不会配置包。

  • dpkg –configure package:请注意,这里需要的是包名,而不是包文件名。如果因为某些原因包已解包但未配置,你可以使用 -a–pending 标志来处理它们。

  • dpkg -r package:此操作会移除软件,但不会删除配置文件及其可能包含的数据。如果你打算将来重新安装该软件,这可能会很有用。

  • dpkg -p package:此操作会清除该包并移除一切:软件、数据、配置文件和缓存。一切。

在以下示例中,我们正在从一个物理下载到系统的包中安装 nano 编辑器,这个包可能是通过点击网页上的下载按钮获得的。请注意,这并不是一种常见的做法:

root@myhome:~# dpkg -i nano_5.4-2+deb11u1_amd64.deb
(Reading database ... 35904 files and directories currently installed.)
Preparing to unpack nano_5.4-2+deb11u1_amd64.deb ...
Unpacking nano (5.4-2+deb11u1) over (5.4-2+deb11u1) ...
Setting up nano (5.4-2+deb11u1) ...
Processing triggers for man-db (2.9.4-2) ...

通常,你会需要使用 apt 工具集来安装和移除软件:

  • apt-cache search NAME 将搜索包含给定字符串的软件包。在以下示例中,我正在寻找一个包含 vim 字符串的软件包(vim 是几个流行的命令行文本编辑器之一)。为了简洁,我已将输出进行了缩短:
root@myhome:~# apt-cache search vim
acr - autoconf like tool
alot - Text mode MUA using notmuch mail
[...]
vim - Vi IMproved - enhanced vi editor
[...]
  • apt-get install NAME 将安装你指定名称的包。你可以在一行中安装多个软件包。在以下示例中,我正在安装一个 C 编译器、一个 C++编译器和一个 Go 语言套件。请注意,输出中还包含了一些为了确保我的软件正常工作而需要的依赖包,并且它们将会被安装以提供该功能:
root@myhome:~# apt-get install gcc g++ golang
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
gcc is already the newest version (4:10.2.1-1).
The following additional packages will be installed:
  bzip2 g++-10 golang-1.15 golang-1.15-doc golang-1.15-go golang-1.15-src golang-doc golang-go golang-src libdpkg-perl libfile-fcntllock-perl libgdbm-compat4 liblocale-gettext-perl libperl5.32 libstdc++-10-dev perl perl-modules-5.32
  pkg-config
Suggested packages:
  bzip2-doc g++-multilib g++-10-multilib gcc-10-doc bzr | brz git mercurial subversion debian-keyring gnupg patch bzr libstdc++-10-doc perl-doc libterm-readline-gnu-perl | libterm-readline-perl-perl libtap-harness-archive-perl dpkg-dev
The following NEW packages will be installed:
  bzip2 g++ g++-10 golang golang-1.15 golang-1.15-doc golang-1.15-go golang-1.15-src golang-doc golang-go golang-src libdpkg-perl libfile-fcntllock-perl libgdbm-compat4 liblocale-gettext-perl libperl5.32 libstdc++-10-dev perl
  perl-modules-5.32 pkg-config
0 upgraded, 20 newly installed, 0 to remove and 13 not upgraded.
Need to get 83.9 MB of archives.
After this operation, 460 MB of additional disk space will be used.
Do you want to continue? [Y/n]

安装程序在这里停止并等待我们的输入。默认操作是接受所有附加包并继续安装。通过输入 nN 并按下 Enter,我们可以停止这个过程。安装操作的 -y 开关将跳过这个问题,并自动继续到下一步:

  • apt-get update 将刷新包数据库,获取新的可用包和新版本。

  • apt-get upgrade 将所有已安装的包升级到数据库中列出的最新版本。

  • apt-get remove NAME 将删除给定名称的包。在以下示例中,我们正在卸载 C++ 编译器:

root@myhome:~# apt-get remove g++
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following packages were automatically installed and are no longer required:
  g++-10 libstdc++-10-dev
Use 'apt autoremove' to remove them.
The following packages will be REMOVED:
  g++
0 upgraded, 0 newly installed, 1 to remove and 13 not upgraded.
After this operation, 15.4 kB disk space will be freed.
Do you want to continue? [Y/n]
(Reading database ... 50861 files and directories currently installed.)
Removing g++ (4:10.2.1-1) ...

CentOS、RHEL 和 Fedora

另一类流行的发行版使用 RPM 包格式。与包交互的基本工具是 rpm。使用这种格式的主要发行版是由 Red Hat 公司制作的 RHEL。包的文件扩展名始终是 .rpm

它们使用 dnf 命令来管理包。还有 yum 命令,它是 RHEL 发行版的原始包管理器(并且,扩展到 Fedora 和 CentOS 发行版),但已被移除。dnfyum 的下一代重写,具有许多改进,使其更强大、更现代:

  • dnf install package_name 将安装给定名称的包及其依赖项。

  • dnf remove package_name 将删除包。

  • dnf update 将所有包更新到包数据库中的最新版本。你可以指定包名,之后 yum 将更新该包。

  • dnf search NAME 将搜索包含 NAME 字符串的包名。

  • dnf check-update 将刷新包数据库。

让我们来看看另一款广泛使用的 Linux 发行版,特别是作为 Docker 镜像的基础——Alpine Linux。

Alpine Linux

Alpine Linux 尤其受到主要使用 Docker 和 Kubernetes 的工程师喜爱。正如该发行版的主页所宣称,小巧、简单、安全。它没有像基于 Debian 或 Red Hat 的发行版中那样的花里胡哨,但输出的 Docker 镜像非常小,而且由于它专注于安全,你可以假设如果你已经将所有包更新到最新版本,那么没有重大安全漏洞。

Alpine Linux 的主要缺点(也可以说是优点,视角不同而定)是它使用 musl 库而非广泛使用的 libc 库进行编译,尽管它确实使用了 libc,因此在安装任何 Python 依赖之前,你需要执行额外的步骤,确保已安装编译时依赖。

与包交互的命令是 3.16)和 edge,这是一个滚动更新版本(它始终拥有最新版本的包)。

此外,还有三个仓库可以用来安装包:maincommunitytesting

你将会在主仓库中找到官方支持的包;所有经过测试的包都放在社区仓库中,而测试版则用于测试,这意味着可能存在一些损坏、过时或带有安全漏洞的包。

搜索包

在搜索或安装任何包之前,建议先下载最新的包缓存。你可以通过调用apkupdate命令来实现:

root@myhome:~# apk update
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
v3.16.2-376-g3ff8974e73 [https://dl-cdn.alpinelinux.org/alpine/v3.16/main]
v3.16.2-379-g3c25b38306 [https://dl-cdn.alpinelinux.org/alpine/v3.16/community]
OK: 17037 distinct packages available

如果你在构建一个用于以后使用的 Docker 镜像,建议在构建过程的最后一步使用apk cacheclean命令来删除此缓存。

有时,在我们创建新的 Docker 镜像时,不知道包的确切名称。此时,最简单的方式是使用 Web 界面:pkgs.alpinelinux.org/packages

使用命令行界面(CLI),你将能够搜索部分库名和二进制文件名,尽管你可以使用so:前缀来指定你正在搜索的是库。其他有用的前缀包括cmd:用于命令,pc:用于pkg-config文件:

root@myhome:~# apk search libproc
libproc-3.3.17-r1
libksysguard-5.24.5-r0
process-cpp-3.0.1-r3
samba-dc-libs-4.15.7-r0
procps-dev-3.3.17-r1
root@myhome:~# apk search vim
neovim-doc-0.7.0-r0
gvim-8.2.5000-r0
vim-tutor-8.2.5000-r0
faenza-icon-theme-vim-1.3.1-r6
notmuch-vim-0.36-r0
kmymoney-5.1.2-r3
faenza-icon-theme-gvim-1.3.1-r6
meson-vim-0.62.1-r0
runvimtests-1.30-r1
graphviz-3.0.0-r0
neovim-0.7.0-r0
py3-pynvim-0.4.3-r3
nftables-vim-0_git20200629-r1
vim-doc-8.2.5000-r0
vim-editorconfig-0.8.0-r0
apparmor-vim-3.0.4-r0
geany-plugins-vimode-1.38-r1
vimdiff-8.2.5000-r0
vimb-3.6.0-r0
neovim-lang-0.7.0-r0
u-boot-tools-2022.04-r1
fzf-neovim-0.30.0-r7
nginx-vim-1.22.1-r0
msmtp-vim-1.8.20-r0
protobuf-vim-3.18.1-r3
vimb-doc-3.6.0-r0
icinga2-vim-2.13.3-r1
fzf-vim-0.30.0-r7
vim-sleuth-1.2-r0
gst-plugins-base-1.20.3-r0
mercurial-vim-6.1.1-r0
skim-vim-plugin-0.9.4-r5
root@myhome:~# apk search -e vim
gvim-8.2.5000-r0
root@myhome:~# apk search -e so:libproc*
libproc-3.3.17-r1
libksysguard-5.24.5-r0
process-cpp-3.0.1-r3
samba-dc-libs-4.15.7-r0

安装、升级和卸载包

你可以使用add(安装)、del(卸载)和upgrade命令对包执行基本操作。在安装过程中,你也可以使用在搜索操作中可用的特殊前缀,但建议使用包的准确名称。请注意,当向系统添加新包时,apk将选择该包的最新版本:

root@myhome:~# apk search -e postgresql14
postgresql14-14.5-r0
root@myhome:~# apk add postgresql14
(1/17) Installing postgresql-common (1.1-r2)
Executing postgresql-common-1.1-r2.pre-install
(2/17) Installing libpq (14.5-r0)
(3/17) Installing ncurses-terminfo-base (6.3_p20220521-r0)
(4/17) Installing ncurses-libs (6.3_p20220521-r0)
(5/17) Installing readline (8.1.2-r0)
(6/17) Installing postgresql14-client (14.5-r0)
(7/17) Installing tzdata (2022c-r0)
(8/17) Installing icu-data-en (71.1-r2)
Executing icu-data-en-71.1-r2.post-install
*
* If you need ICU with non-English locales and legacy charset support, install
* package icu-data-full.
*
(9/17) Installing libgcc (11.2.1_git20220219-r2)
(10/17) Installing libstdc++ (11.2.1_git20220219-r2)
(11/17) Installing icu-libs (71.1-r2)
(12/17) Installing gdbm (1.23-r0)
(13/17) Installing libsasl (2.1.28-r0)
(14/17) Installing libldap (2.6.3-r1)
(15/17) Installing xz-libs (5.2.5-r1)
(16/17) Installing libxml2 (2.9.14-r2)
(17/17) Installing postgresql14 (14.5-r0)
Executing postgresql14-14.5-r0.post-install
*
* If you want to use JIT in PostgreSQL, install postgresql14-jit or
* postgresql-jit (if you didn't install specific major version of postgresql).
*
Executing busybox-1.35.0-r17.trigger
Executing postgresql-common-1.1-r2.trigger
* Setting postgresql14 as the default version
OK: 38 MiB in 31 packages

你还可以选择安装特定版本的包,而不是最新版本。不幸的是,无法从同一仓库安装旧版本的包,因为每当部署新版本时,旧版本会被移除。然而,你可以从其他仓库安装包的旧版本:

root@myhome:~# apk add bash=5.1.16-r0 --repository=http://dl-cdn.alpinelinux.org/alpine/v3.15/main
(1/4) Installing ncurses-terminfo-base (6.3_p20220521-r0)
(2/4) Installing ncurses-libs (6.3_p20220521-r0)
(3/4) Installing readline (8.1.2-r0)
(4/4) Installing bash (5.1.16-r0)
Executing bash-5.1.16-r0.post-install
Executing busybox-1.35.0-r17.trigger
OK: 8 MiB in 18 packages

你还可以使用以下命令安装你事先准备好的自定义包:

root@myhome:~# apk add --allow-untrusted your-package.apk

要升级系统中所有可用的包,你可以简单地调用apk upgrade命令。但是,如果你只想升级某个特定的包,你需要在升级选项后添加该包的名称。记得在此之前刷新包缓存:

root@myhome:~# apk update
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
v3.16.2-376-g3ff8974e73 [https://dl-cdn.alpinelinux.org/alpine/v3.16/main]
v3.16.2-383-gcca4d0a396 [https://dl-cdn.alpinelinux.org/alpine/v3.16/community]
OK: 17037 distinct packages available
root@myhome:~# apk upgrade
(1/2) Upgrading alpine-baselayout-data (3.2.0-r22 -> 3.2.0-r23)
(2/2) Upgrading alpine-baselayout (3.2.0-r22 -> 3.2.0-r23)
Executing alpine-baselayout-3.2.0-r23.pre-upgrade
Executing alpine-baselayout-3.2.0-r23.post-upgrade
Executing busybox-1.35.0-r17.trigger
OK: 8 MiB in 18 packages

你可以通过不带任何其他选项地调用apk来查找所有可能的操作。最有用的操作之一是apk info命令。它会打印出关于包或仓库的信息(以下输出已被缩略):

root@myhome:~# apk info --all bash
bash-5.1.16-r2 description:
The GNU Bourne Again shell
bash-5.1.16-r2 webpage:
https://www.gnu.org/software/bash/bash.xhtml
bash-5.1.16-r2 installed size:
1308 KiB
bash-5.1.16-r2 depends on:
/bin/sh
so:libc.musl-x86_64.so.1
so:libreadline.so.8
bash-5.1.16-r2 provides:
cmd:bash=5.1.16-r2.

在这一部分,我们介绍了包管理工具——在 Linux 发行版中管理软件的标准方式。在接下来的部分中,我们将介绍用户账户管理。

用户管理

在 Linux 系统中,用户由三组文件定义:

  • /etc/passwd:此文件包含关于用户的信息 —— 即用户的名称、唯一的数字 IDUID)、用户所属的主要组的 GID、主目录的路径以及用户登录时加载的 shell。一个典型的条目如下所示:
   admin:x:1000:1000:Debian:/home/admin:/bin/bash

每一行描述一个用户。字段通过冒号分隔。第二个字段在非常特殊的情况下才会包含除了 x 以外的其他内容。在这里,x 表示密码存储在 /etc/shadow 文件中。原因是,/etc/passwd 文件的权限必须稍微宽松一些,以便登录过程能够正常工作。/etc/shadow 只能被 root 用户和 root 组读取,并且只能由 root 用户写入:

    root@myhome:~# ls -ahl /etc/passwd
    -rw-r--r-- 1 root root 1.6K Aug 22 18:38 /etc/passwd
    root@myhome:~# ls -ahl /etc/shadow
    -rw-r----- 1 root shadow 839 Aug 22 18:38 /etc/shadow
  • /etc/shadow -:此文件包含加密的密码。正如前面提到的,为了安全起见,只有 root 用户可以读取和写入此文件。

  • /etc/group -:此文件包含有关用户所属组的信息。组就是那些已被归类在一起的账户,以便可以管理它们的权限。

你永远不应该手动修改这些文件,特别是 /etc/shadow 文件。正确的方法是使用 passwd 命令来更改它的内容。我们建议你阅读手册页获取更多信息。

有三个命令参与用户修改,另外有三个命令用于管理组:useradduserdelusermodgroupaddgroupdelgroupmod

添加用户

useradd 向系统添加一个用户账户。不同的选项修改该命令的行为。调用 useradd 命令的最常见版本会添加一个用户,创建其主目录,并指定默认的 shell:

root@myhome:~# useradd -md /home/auser -s /bin/bash auser

-m 告诉命令创建主目录,-d(这里它和 -m 一起传递了一个减号)告诉命令主目录应该是什么(注意是绝对路径),-s 指定默认的 shell。可以指定更多的参数,再次建议你查看手册页获取更多详细信息。

修改用户

usermod 修改现有的用户账户。它可以用来更改用户的组成员、主目录、锁定账户等。这里有一个有趣的选项是 -p 标志,它允许你非交互式地应用新密码。这在自动化中非常有用,当我们可能希望通过脚本或工具更新用户密码,而不是通过命令行。然而,这也存在安全风险:在命令执行期间,系统中的任何人都可以列出正在运行的进程及其参数,并查看密码条目。虽然这不会自动危及密码,因为密码必须通过 crypt (3) 函数加密后提供,但如果攻击者获得了加密的密码版本,他们可以运行密码破解程序进行攻击,最终通过暴力破解得到明文密码:

root@myhome:~# usermod -a -G admins auser

上述命令将把 auser 用户添加到名为 admins 的组中。-a 选项表示该用户将被添加到附加组(不会从其他已有的组中删除)。

删除用户

userdel 命令用于从系统中删除用户。它只能从系统文件中删除用户条目,并保持主目录不变,或者删除带有主目录的用户:

root@myhome:~# userdel -r auser

上述命令将删除用户及其主目录和所有文件。注意,如果用户仍然登录,它将不会被删除。

管理组

与管理用户类似,你可以在 Linux 系统中添加、删除和修改组。为此有等效的命令:

  • groupadd:在系统中创建一个组。组可以用于将用户归类,并指定其执行、目录或文件访问权限。

  • groupdel:从系统中删除该组。

  • groupmod:更改组定义。

你还可以使用 who 命令查看当前登录系统的用户:

admin@myhome:~$ who
ubuntu   tty1         2023-09-21 11:58
ubuntu   pts/0        2023-09-22 07:54 (1.2.3.4)
ubuntu   pts/1        2023-09-22 09:25 (5.6.7.8)
trochej  pts/2        2023-09-22 11:21 (10.11.12.13)

你还可以使用 id 命令查看当前登录用户的 UID 和 GID:

admin@myhome:~$ id
uid=1000(ubuntu) gid=1000(ubuntu) 
groups=1000(ubuntu),4(adm),20(dialout), 24(cdrom),25(floppy),27(sudo),29(audio), 30(dip),44(video),46(plugdev),119(netdev),120(lxd),123(docker)

执行此命令时不带任何选项,它将显示你的用户 ID 和用户所在的所有组。或者,你可以提供一个名称,查看该用户的 UID 或 GID:

admin@myhome:~$ id dnsmasq
uid=116(dnsmasq) gid=65534(nogroup) groups=65534(nogroup)

要查看主组的 ID 和 UID,你可以分别使用 -u-g 选项:

admin@myhome:~$ id -u
1000
admin@myhome:~$ id -g
1000

在本节中,我们介绍了用于管理 Linux 系统中用户帐户和组的命令。下一节将解释如何使用 SSH 安全地连接到远程系统。

安全外壳(SSH)协议

在 DevOps 世界中,几乎没有什么东西在你的笔记本电脑或 PC 上本地运行。要连接到远程系统,SSH 协议是公认的黄金标准。SSH 于 1995 年开发,作为一种安全的加密远程 shell 访问工具,用来取代像 telnetrsh 这样的明文工具。其主要原因是,在分布式网络中,监听通信过于容易,任何明文传输的内容都容易被截获。这包括诸如登录信息等重要数据。

Linux 世界中最常用的 SSH 服务器(和客户端)是 OpenSSH (www.openssh.com/)。截至撰写时,其他仍在维护的开源服务器包括 lsh (www.lysator.liu.se/~nisse/lsh/)、wolfSSH (www.wolfssl.com/products/wolfssh/) 和 Dropbear (matt.ucc.asn.au/dropbear/dropbear.xhtml)。

SSH 主要用于登录远程机器执行命令。但它也能够传输文件(22)。

配置 OpenSSH

安装 OpenSSH 服务器后,你的发行版将在 /etc/ssh/sshd_config 文件中放置一个基本配置。最基本的配置如下所示:

AuthorizedKeysFile .ssh/authorized_keys
AllowTcpForwarding no
GatewayPorts no
X11Forwarding no
Subsystem sftp /usr/lib/ssh/sftp-server

在继续选项之前,让我们先研究一下它们:

  • AuthorizedKeysFIle 告诉我们的服务器在用户目录中查找存储所有可以用来作为指定用户连接到该机器的公钥的文件。因此,如果你将公钥放在 AlphaOne 的主目录中,即 /home/AlphaOne/.ssh/authorized_keys,你将能够使用对应的私钥作为该用户进行连接(关于密钥的更多内容将在 创建和管理 SSH 密钥 小节中讨论)。

  • AllowTCPForwarding 将启用或禁用所有用户转发 TCP 端口的能力。端口转发用于访问那些无法直接在互联网上访问的远程机器,但你可以访问另一个可以连接的机器。这意味着你正在使用 SSH 主机作为所谓的跳跃主机来连接到私有网络,类似于使用 VPN。

  • GatewayPorts 是另一个直接与端口转发功能相关的选项。通过允许 GatewayPorts,你不仅可以将转发的端口暴露给你的机器,还可以暴露给你连接的网络中的其他主机。由于安全原因,不建议将此选项设置为 yes;你可能会不小心将私有网络暴露给你所在的网络,例如在咖啡馆中。

  • X11Forwarding 有一个非常特定的使用场景。通常情况下,你不希望在服务器上运行完整的图形界面,但如果你有这个需求,启用此选项后,你将能够登录到远程机器并启动远程图形应用程序,这些应用程序将像在本地主机上运行一样显示。

  • Subsystem 使你能够通过附加功能扩展 OpenSSH 服务器,例如在此案例中使用 SFTP。

在前面的命令块中没有指定的一个非常重要的选项是 PermitRootLogin,默认情况下设置为 prohibit-password。这意味着你将能够以 root 用户身份登录,但只有在需要使用公钥和私钥对进行身份验证时才可以登录。我们建议将此选项设置为 no,并且仅通过 sudo 命令允许 root 用户访问。

就是这些。当然,你可以添加更多高级配置,例如使用 man sshd_config 命令。

以同样的方式,你可以了解如何配置 SSH 客户端——也就是说,运行 man ssh_config

以下是一些非常有用的客户端选项:

# Show keys ascii graphics
VisualHostKey yes
# Offer only one Identity at a time
Host *
  ForwardAgent yes
  IdentitiesOnly yes
  IdentityFile ~/.ssh/mydefaultkey
# Automatically add all used keys to agent
AddKeysToAgent yes

VisualHostKey 设置为 yes 时,将显示服务器公钥的 ASCII 图形。你将连接的每个服务器都有一个唯一的密钥,因此会有独特的图形。它很有用,因为作为人类,我们非常擅长识别模式,因此如果你连接到 1.2.35.2 服务器,但打算进入不同的系统,你很可能通过看到与预期不同的图形来察觉到不对劲。

这里有一个示例:

root@myhome:~# ssh user@hosts
Host key fingerprint is SHA256:EppY0d4YBvXQhCk0f98n1tyM7fBoyRMQl2o3ife1pY
+--[ED25519 256]--+
|    .oB++o       |
|     +o*o ..     |
|    +..o*.+ .    |
|    .o +.= . +   |
|    ..o.S.= = .  |
|      .ooooE. .  |
|        .o.o     |
|                 |
|                 |
+----[SHA256]-----+

主机选项允许你为一个或多个服务器设置特定的选项。在这个示例中,我们启用了 SSH 代理转发,并禁用了所有服务器的密码登录。此外,我们还设置了一个默认的私钥,用于连接任何服务器。这将引出我们对 SSH 密钥和加密算法的讨论。

最后一个选项,AddKeysToAgent,意味着每当你使用(并解锁)一个密钥时,它也会被添加到 SSH 代理中,以供将来使用。这样,你就不需要在连接时指定密钥,也不必在每次连接尝试时解锁密钥。

创建和管理 SSH 密钥

SSH 由三个组件组成:传输层(传输控制协议 (TCP)/互联网协议 (IP)),用户身份验证层,以及一个连接层,可以有效地是多个独立传输数据的连接。

就不同的身份验证选项而言,你有一种基本的密码身份验证方式,证明它不够安全。还有公钥身份验证,我们将在这里讨论。剩下的两个是 keyboard-interactive通用安全服务应用程序接口 (GSSAPI)。

公钥身份验证要求我们生成一个密钥,它将有两个对应的部分:私钥和公钥。你将把公钥放在服务器的 authorized_keys 文件中;私钥则用于身份验证。

在编写本书时,RSA 密钥是与 SSH 一起使用的标准。它是安全的,但建议使用更大的密钥,长度为 4,096 位,但 3,072 位(默认)也被认为足够。更大的密钥意味着更慢的连接速度。

当前,更好的选择是使用 ed25519 类型的密钥,它具有固定长度。

此外,所有密钥都可以用密码进行保护。

以下代码展示了如何生成这两种密钥类型:

root@myhome:~# ssh-keygen -b 4096 -o -a 500 -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Created directory '/root/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa
Your public key has been saved in /root/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:eMu9AMUjpbQQ7DJIl4MB2UnpfiUqwZJi+/e9dsKWXZg root@614bbd02e559
The key's randomart image is:
+---[RSA 4096]----+
|o=+++.. .        |
|.o++.o =         |
|o+... + +        |
|*o+ o .+ .       |
|+o.+ oo S   o    |
|..o .  + o E .   |
| ...    = + .    |
|   . .  .B +     |
|    . ..oo=      |
+----[SHA256]-----+
root@myhome:~# ssh-keygen -o -a 100 -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/root/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_ed25519
Your public key has been saved in /root/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:EOCjeyRRIz+hZy6Pbtcnjan2lucJ2Mdih/rFc/ZnthI
root@614bbd02e559
The key's randomart image is:
+--[ED25519 256]--+
|  . =..          |
|   * o .         |
|  o B .          |
|   * o .         |
|  + o   S        |
|   B o +    E    |
|  o +.*=B o  .   |
| ...ooB*+= .. +  |
| ..oo=o=o   .=.. |
+----[SHA256]-----+

现在,要将这个新创建的密钥放到服务器上,你需要手动将其复制到 authorized_keys 文件中,或者使用 ssh-copy-id 命令,如果你已经有其他访问手段(如密码身份验证),它会为你完成此操作:

root@myhome:~# ssh-copy-id -i ~/.ssh/id_ed25519 user@remote-host

你只能通过使用密钥执行下次登录到此服务器的操作。

到目前为止,你应该已经很好地理解了 SSH 的工作原理以及如何使用它的最常用功能。你现在知道如何创建密钥以及如何在远程系统上保存它们。

总结

本章结束了我们对日常工作中所需的基本 Linux 操作的介绍。虽然这并没有全面解释你管理 Linux 系统所需了解的所有内容,但足以帮助你入门,并帮助你管理系统。在下一章中,我们将从头开始讲解如何编写 shell 脚本,并指导你学习基本以及更高级的主题。

练习

通过以下练习来测试你对本章内容的掌握:

  1. 在 Debian/Ubuntu 中,安装vim软件包。

  2. 创建一个定时任务,每周六上午 10:00 生成一个名为/tmp/cronfile的文件。

  3. 创建一个名为admins的组,并将一个现有用户添加到该组中。

第四章:使用 Shell 脚本自动化

在本章中,我们将通过 shell 脚本演示系统管理任务的自动化。我们将展示使用 Bash shell 处理脚本的几种方法。计划是创建一个脚本来自动化数据库转储的创建。这个任务虽然简单,但将演示如何处理可能出现的问题,并且如何应对这些情况。

在本章中,我们将涵盖以下主题:

  • 备份数据库

  • 理解脚本编写

  • 理解 Bash 内建命令和语法

  • 理解备份脚本——第一步

  • 处理错误和调试

技术要求

对于本章内容,你将需要一个可以安装软件包的 Linux 系统,并且不害怕在过程中出错。为此,虚拟机是最理想的,通常是在一台可以从头开始重新安装的旧计算机上运行。我们不指望会出错,但在学习过程中,可能会发生这种情况。

备份数据库

对于最常见的数据库,如 MySQL 和 PostgreSQL,至少有两种不同的方式来备份数据库:

  • 通过提取当前的所有数据和数据库架构来进行数据库转储

  • 复制复制日志

在云环境中,你也可以对存储备份的磁盘数据库进行快照。

数据库转储也可以作为完整备份使用。复制日志并不是独立的数据库转储,因此你需要将它们与完整备份结合使用。这叫做增量备份。

进行完整备份可能会花费很长时间,尤其是对于大型数据库。在备份过程中,数据库会锁定其数据文件,因此不会将新数据保存到磁盘上;相反,所有数据都会存储在复制日志中,直到数据库锁被释放。对于大型数据库,这个过程可能需要数小时。鉴于此,我们将每周进行一次完整备份,并每小时复制一次所有复制日志。此外,我们还会每天创建一次 AWS 数据库实例的磁盘快照。

有了这些知识,我们可以开始创建最基本版本的脚本了。

理解脚本编写

Shell 脚本是一个简单的文本文件,里面包含了命令。与编译程序不同,shell 脚本在执行之前并不会被解析,而是在执行时解析。这使得开发过程非常快速——完全没有编译过程。但同时,执行速度稍慢。而且,编译器能够捕获的错误会在执行时显现,通常会导致脚本退出。

优点是,编写脚本时需要学习的东西并不多——远远少于用 C 或 Python 编写程序时。与系统命令的交互就像是直接输入命令的名称一样简单。

Bash 编程语言缺乏许多复杂性:它没有很多数据类型和结构,作用域控制非常基础,内存实现也不是为了在大规模下高效。

没有一个固定的经验法则来选择何时编写脚本,何时开发程序。然而,有一些要点需要考虑。适合用 Shell 脚本的一个好候选条件如下:

  • 它不会很长。我们不能给你一个具体的规则,但当你的代码行数达到数百行时,考虑使用 Python 或将其拆分成多个脚本可能是个好主意。

  • 它与系统命令进行交互,有时会进行很多交互。你可以将其视为一种自动化运行这些命令的方式。

  • 它不做大量的数据处理。只有少数的数组、字符串和数字,仅此而已。

  • 它不执行任何系统调用。Shell 脚本不是用来做系统调用的,也没有直接的方法来做到这一点。

Shell 脚本的基本结构如下:

#!/bin/bash
echo "Hello world"

第一行以所谓的 she-bang 开头。它用于告诉系统使用哪个解释器(在本例中是 Bash)来运行这个脚本。在很多网上的脚本中,she-bang 看起来像这样:

#!/usr/bin/env bash

使用 env 命令既有一个很大的优势,也有一个劣势。使用它的优点是它会使用当前用户 PATH 环境变量中排在最前面的 Bash 可执行文件。具体取决于目的。env 命令还不允许你传递任何参数给你选择的解释器。

上面脚本的第二行简单地显示了Hello world。它使用了一个内置命令 echo,它的作用就是显示你输入的任何文本。

现在,要执行这个脚本,我们需要将其保存到一个文件中。最好以 .sh.bash 后缀来结束这个文件。执行这个新脚本有两种方式——通过调用解释器并传入脚本名,或者直接通过脚本名执行:

admin@myhome:~$ bash ./myscript.sh
Hello world
admin@myhome:~$

要直接执行脚本,我们需要更改其权限,使其可以被执行;否则,系统将无法识别它为可执行文件:

admin@myhome:~$ chmod +x myscript.sh
admin@myhome:~$ ./myscript.sh
Hello world
admin@myhome:~$

类似地,我们可以轻松地将 Python 或任何其他 Shell 设置为我们脚本的解释器。

现在,让我们聚焦于 Bash,并看一些我们将要使用的 Bash 内置命令。

理解 Bash 内置命令和语法

在我们开始创建脚本之前,让我们回归基础。首先,我们将探讨 Bash 脚本语言的语法及其局限性。

内置命令是与 Bash 紧密相关的命令,是我们将要使用的主要脚本语法。Bash 会尝试执行它运行所在系统中的任何其他命令。

就像其他任何解释型语言(例如 Python 或 Ruby)一样,Bash 有其独特的语法和语法规则。让我们来看看。

Bash,类似于其他编程语言,从上到下、从左到右解释文件。每行通常包含一个或多个命令。你可以使用管道符号(|)或双管道符号(||)、分号(;)或双和符号(&&)将多个命令连接在同一行中。记住,双管道的作用与逻辑OR相同,而双和符号的作用与逻辑AND相同。这种方式可以让你按顺序运行命令,并根据前一个命令的结果执行下一个命令,而不需要使用更复杂的条件语句。这被称为命令列表或命令链:

commandA && commandB

在前面的命令中,你可以看到使用双和符号的示例。在这里,commandB 只有在 commandA 成功执行后才会执行。我们可以通过在命令链的末尾添加||来继续连接更多的命令。

commandA || commandB

另一方面,这个示例展示了如何使用双管道。在这里,commandB 只有在 commandA 执行失败时才会执行。

Bash(或 Linux 中的任何其他 shell)通过使用返回码来判断一个命令是否执行失败或成功退出。每个命令需要以一个正整数退出——零(0)是成功的代码,任何其他代码都是失败的。如果你使用AND(&&)OR(||)将多个命令连接在一起,则整行的返回状态将由先前执行的命令的返回状态决定。

那么命令后面的单个和符号(&)呢?它有一个完全不同的功能——它会在后台执行命令,并且脚本会继续运行,而无需等待命令完成。这对于不需要其他部分程序完成的任务,或者同时运行多个相同命令的实例(例如同时进行多个数据库的完整备份以缩短执行时间)非常有用。

现在我们知道如何连接命令,接下来我们可以了解任何编程语言的另一个核心特性——变量。

变量

在 Bash 中,有两种类型的变量:全局变量和局部变量。全局变量在脚本运行期间可以访问,除非该变量被取消设置。局部变量仅在脚本的某个块中可访问(例如,定义的函数)。

每当脚本执行时,它会从当前运行的 shell 中获取一组现有的变量;这被称为环境。你可以使用export命令将新变量添加到环境中,使用use unset命令移除变量。你也可以使用declare -x命令将函数添加到环境中。

所有的参数,无论是局部的还是全局的,都以美元符号($)为前缀。所以,如果你有一个名为 CARS(区分大小写)的变量,引用它时,需要在脚本中写成$CARS

对于变量,单引号或双引号(或没有引号)是重要的。如果你将变量放在单引号中,它将不会被展开,而引号中的变量名将被当作字面字符串处理。双引号中的变量会被展开,并且这被认为是一种安全的引用变量的方式(以及连接它们,或者将它们拼接在一起),因为如果字符串中有空格或其他特殊字符,它们对脚本不会有实际意义 —— 也就是说,它们不会被执行。

有时,你需要将多个变量连接起来。你可以使用大括号({})来完成。例如,"${VAR1}${VAR2}" 将扩展为你设置的 VAR1VAR2 的值。在这种情况下,你也可以使用大括号来截取或替换字符串的部分。这里有一个例子:

name="John"
welcome_str="Hello ${name}"
echo "${welcome_str%John}Jack"

上述代码将显示 % 操作符只会移除字符串末尾的字符。如果你想从字符串开头截取变量,你可以使用 # 操作符,方法相同。

如果你在没有引号的情况下引用变量,变量值中的空格可能会破坏脚本的流程并妨碍调试,因此我们强烈建议使用单引号或双引号。

参数

我们可以使用两种类型的参数,但它们都是特殊类型的变量,因此每个变量前都有一个美元符号($)。

你需要注意的第一类参数是 位置参数 —— 这些是传递给脚本或脚本内函数的参数。所有参数从 1 开始索引,一直到 n,其中 n 是最后一个参数。你可以用 $1$n 引用每一个参数。你可能会好奇,如果使用 $0 会发生什么。

$0 包含脚本的名称,因此它对于在脚本中生成文档等操作非常有用。

要引用从 1 开始的所有可用参数,你可以使用 $@(美元符号,at 符号)。

以下是一些其他常用的特殊参数:

  • #:位置参数的数量

  • ?:最近执行的前台命令的退出代码

  • $:Shell 的进程 ID

循环

你可能熟悉其他编程语言中的不同类型的循环。在 Bash 中也有这些循环,但语法稍有不同。

最基本的 for 循环 看起来像这样:

for variable_name in other_variable; do some_commands; done

这个变种的 for 循环将 other_variable 中的每个元素设置为 variable_name 的值,并为每个找到的元素执行 some_commands。执行完成后,它将以最后执行的命令的状态退出循环。in other_variable 部分是可以省略的 —— 在这种情况下,for 循环会为每个位置参数执行一次 some_commands。使用该参数的示例如下:

for variable_name; do some_commands; done

上述 for 循环将根据你为函数(或此情况下的脚本)添加的输入变量执行多次。你可以使用 $@ 引用所有位置参数。

以下是一个 C/C++风格的for循环:

for (( var1 ; var2 ; var3 )); do some_commands; done

下面是此语法的一个示例:

for ((i=1; i<=5; i++)); do echo $i; done

第一个表达式将i变量设置为1,第二个表达式是循环继续运行的条件,最后一个表达式将i变量的值增加 1。每次循环运行时,会显示分配给i变量的下一个值。

此命令的输出将如下所示:

1
2
3
4
5

另一个有用的循环是while循环,它会根据需要运行多次,直到满足条件(传递给它的命令成功退出——返回零)。与之对应的是until循环,它会一直运行,直到传递给它的命令返回非零状态:

while some_command; do some_command; done
until some_command; do some_command; done

你可以通过使用始终满足条件的命令来创建一个无限循环,对于while循环,条件可以简单地设为true

最常用的命令块是条件语句,它们与if语句一起使用。让我们仔细看看。

条件执行——if语句

if语句的语法如下:

if test_command
then
  executed_if_test_was_true
fi

test_command 可以是你能想到的任何命令,但通常,测试是用双方括号或单方括号包围的。这两者之间的区别是前者是一个系统命令,叫做test(你可以通过执行man test来查看它的语法),而后者是 Bash 的内建命令,功能更强大。

放置变量在方括号中的经验法则是使用双引号,这样即使变量包含空格,也不会改变我们的测试意图:

if [[ -z "$SOME_VAR" ]]; then
    echo "Variable SOME_VAR is empty"
fi

-z 测试检查$SOME_VAR变量是否为空。如果变量为空,评估为true,否则为false

以下是其他常用的测试:

  • -a:逻辑“与”

  • -o:逻辑“或”

  • -eq:等于

  • -ne:不等于

  • -gt 或 >:大于

  • -ge 或 >=:大于或等于

  • -lt 或 <:小于

  • -le 或 <=:小于或等于

  • = 或 ==:等于

  • !=:不等于

  • -z:字符串为空(其长度为零字符)

  • -n:字符串不为空

  • -e:文件存在(目录、符号链接、设备文件或文件系统中的任何其他文件)

  • -f:文件是常规文件(不是目录或设备文件)

  • -d:文件是目录

  • -h-L:文件是符号链接

  • -r:文件具有读取权限(对于执行测试的用户)

  • -w:文件具有写权限(对于执行测试的用户)

  • -x:文件可以被执行脚本的用户执行

注意,使用系统测试(单方括号,[...])时,测试可能与内建测试(双方括号,[[...]])行为不同。

双等号比较运算符,在使用通配符匹配字符串时,将根据你是否对模式加引号来匹配模式或字面字符串。

以下是一个模式匹配的例子,假设字符串以w开头:

if [[ $variable == w* ]];
    echo "Starts with w"
fi

当使用系统测试(单个方括号)而不是内置测试时,测试将尝试查找$变量是否与本地目录中的任何文件名匹配(包括带有空格的文件)。这可能会导致一些不可预测的结果。

以下是模式匹配的示例,如果字符串为w*

if [[ $variable == "w*" ]];
    echo "String is literally 'w*'"
fi

拥有这些知识后,我们已经准备好开始创建和运行脚本了。让我们直接开始吧!

理解备份脚本 – 第一步

既然我们知道脚本的样子了,就可以开始编写脚本了。你可以使用你喜欢的控制台编辑器或 IDE 来做这件事。让我们创建一个名为run_backups.sh的空文件,并更改其权限,使其可执行:

admin@myhome:~$ touch run_backups.sh && chmod +x run_backups.sh
admin@myhome:~$ ls -l run_backups.sh
-rwxr-xr-x  1 admin  admin  0 Dec  1 15:56 run_backups.sh

这是一个空文件,因此我们需要添加一个基本的数据库备份命令,并从那里继续。我们不会讨论如何授予该脚本访问数据库的权限。我们将备份一个 PostgreSQL 数据库,并使用pg_dump工具来实现。

让我们在基本脚本中输入一个 shebang 行并调用pg_dump命令:

#!/usr/bin/env bash
pg_dump mydatabase > mydatabase.sql

要执行此脚本,我们需要启动以下命令:

admin@myhome:~$ ./run_backups.sh

点号和斜杠表示我们要执行当前目录中的某个文件,文件名为run_backups.sh。如果没有最初的点斜杠对,当前运行的 shell(这里是bash)将查找PATH环境变量,并尝试在其中列出的目录中查找我们的脚本:

admin@myhome:~$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

如你所见,这是一个由冒号分隔的目录列表。

现在,让我们看看执行时我们的 Bash 脚本做了什么:

admin@myhome:~$ ./run_backups.sh
./run_backups.sh: line 3: pg_dump: command not found

除非你系统中已安装pg_dump,否则你将看到此错误。这意味着 Bash 没有找到我们打算运行的命令。它还会显示错误发生的行。此外,一个空的mydatabase.sql文件被创建了。

通常,我们会创建一个包含所有所需工具的 Docker 镜像,另一个镜像运行 PostgreSQL 数据库。但由于这一部分将在第八章中讲解,我们就直接继续安装所有所需的软件包到本地机器上吧。假设你使用的是 Ubuntu 或 Debian Linux 系统,你可以运行以下命令:

admin@myhome:~$ sudo apt-get update
Get:1 http://archive.ubuntu.com/ubuntu jammy InRelease [270 kB]
[Condensed for brevity]
Get:18 http://archive.ubuntu.com/ubuntu jammy-backports/main amd64 Packages [3520 B]
Fetched 24.9 MB in 6s (4016 kB/s)
Reading package lists... Done
admin@myhome:~$ sudo apt-get install postgresql
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  cron libbsd0 libcommon-sense-perl libedit2 libgdbm-compat4 libgdbm6 libicu70 libjson-perl libjson-xs-perl libldap-2.5-0 libldap-common libllvm14 libmd0 libperl5.34 libpopt0 libpq5 libreadline8
[Condensed for brevity]
Suggested packages:
  anacron checksecurity default-mta | mail-transport-agent gdbm-l10n libsasl2-modules-gssapi-mit | libsasl2-modules-gssapi-heimdal libsasl2-modules-ldap libsasl2-modules-otp libsasl2-modules-sql
[Condensed for brevity]
0 upgraded, 42 newly installed, 0 to remove and 2 not upgraded.
Need to get 68.8 MB of archives.
After this operation, 274 MB of additional disk space will be used.
Do you want to continue? [Y/n] y

经用户确认后,数据库将被安装、配置,并在后台启动。为了可读性,我们已经截断了后续输出。

安装完成后,可能还需要对数据库做一些额外的配置更改,以便你能够使用另一个名为psql的工具连接到数据库。psql是一个用于连接 PostgreSQL 的控制台命令。在/etc/postgresql/14/main/pg_hba.conf文件中,我们定义了信任关系以及谁可以使用多种机制连接到数据库。

查找以下行:

local   all  postgres peer

将其更改为以下内容:

local   all  all  trust

做出此修改后,你可以使用以下命令重启数据库:

admin@myhome:~$ sudo systemctl restart postgresql
* Restarting PostgreSQL 14 database server

现在,你应该能够登录到数据库并列出所有可用的数据库:

admin@myhome:~$ psql -U postgres postgres
psql (14.5 (Ubuntu 14.5-0ubuntu0.22.04.1))
Type "help" for help.
postgres=# \l
                              List of databases
   Name    |  Owner   | Encoding | Collate |  Ctype  |   Access privileges
-----------+----------+----------+---------+---------+-----------------------
 postgres  | postgres | UTF8     | C.UTF-8 | C.UTF-8 |
 template0 | postgres | UTF8     | C.UTF-8 | C.UTF-8 | =c/postgres          +
           |          |          |         |         | postgres=CTc/postgres
 template1 | postgres | UTF8     | C.UTF-8 | C.UTF-8 | =c/postgres          +
           |          |          |         |         |
postgres=CTc/postgres
(3 rows)
postgres=# \q
admin@myhome:~$

登录后,我们使用 \l(反斜杠,小写 L)列出所有可用的数据库,并使用 \q(反斜杠,小写 Q)退出 psql shell。一旦设置完成,我们可以回到脚本并再次尝试运行它:

admin@myhome:~$ ./run_backups.sh
pg_dump: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL:  role "root" does not exist

PostgreSQL 中没有 root 角色,这是此时预期的错误。我们需要使用不同的角色连接到数据库。默认角色是 postgres,传递给 pg_dump 的选项是 -U,这与我们在 psql 中使用的相同。更新后,我们的脚本将如下所示:

#!/usr/bin/env bash
pg_dump -U postgres mydatabase > mydatabase.sql

最后一步是创建一个数据库并生成一些实际数据,以确保输出的 sql 文件不会为空。以下脚本将创建一个名为 mydatabase 的数据库,并创建两个包含随机数据的表:

CREATE DATABASE mydatabase;
\c mydatabase
CREATE TABLE random_data AS SELECT data_series, md5(random()::text) from generate_series(1,100000) data_series;
CREATE TABLE another_random AS SELECT data_series, md5(random()::text) from generate_series(1,100000) data_series;

CREATE DATABASE 这一行正在创建一个名为 mydatabase 的数据库。第二行表示我们正在连接到这个新数据库。接下来的两行以 CREATE TABLE 开头,分别创建表并使用内置的 PostgreSQL 函数填充数据。让我们将其分解为两个独立的查询——SELECTCREATE

SELECT data_series, md5(random()::text) from generate_series(1,100000) data_series;

这里发生了几件事:

  • generate_series() 函数正在创建一个从 1 到 100,000 的整数序列——这将生成表中的所有记录

  • data_series 关键字位于分号之前,命名了来自 generate_series() 函数的输出,因此它是我们打算创建的表中的实际字段名。

  • random() 函数生成一个介于 0 和 1 之间的值——即大于或等于 0 小于 1

  • random() 函数后的 ::text 将此函数的输出转换为文本格式

  • md5() 函数将 random()::text 的输出进行哈希处理,使用 md5 算法,确保我们获得一个唯一的字符串,并运行的次数与 generate_series() 函数的输出数量一致(这里是从 1 到 100,000)

  • 最后,SELECT data_series, md5() 正在生成一个包含两列(data_seriesmd5)的表,这两列的数据由两个函数生成

现在,回到 CREATE TABLE,有一部分叫做 another_random AS —— 这将从 SELECT 获取输出,并为我们创建一个表。

有了这些知识,我们可以创建一个 sql 脚本并使用 psql 执行它:

admin@myhome:~$ psql -U postgres < create_db.sql
CREATE DATABASE
You are now connected to database "mydatabase" as user "postgres".

要检查我们是否创建了某些内容并查看我们创建的数据,我们再次需要使用 psql 和对新数据库的 SELECT 查询:

admin@myhome:~$ psql (14.1)
Type "help" for help.
postgres=# \c mydatabase
You are now connected to database "mydatabase" as user "postgres".
mydatabase=# \dt
             List of relations
 Schema |      Name      | Type  |  Owner
--------+----------------+-------+----------
 public | another_random | table | postgres
 public | random_data    | table | postgres
(2 rows)
mydatabase=# select * from random_data ;
 data_series |               md5
-------------+----------------------------------
           1 | 4c250205e8f6d5396167ec69e3436d21
           2 | a5d562ccd600b3c4c70149361d3ab307
           3 | 7d363fac3c83d35733566672c765317f
           4 | 2fd7d594e6d972698038f88d790e9a35
--More--

前面的输出末尾的 --More-- 表示还有更多记录要显示。你可以按空格键查看更多数据,或按 Q 键退出。

一旦你创建了一个数据库并填充了一些数据,你可以尝试再次运行我们的备份脚本:

admin@myhome:~$ ./run_backup.sh
admin@myhome:~$

没有错误,所以我们可能已经成功创建了完整的数据库转储:

admin@myhome:~$ ls -l mydatabase.sql
-rw-r--r--    1 root     root      39978060 Dec 15 10:30 mydatabase.sql

输出文件不是空的;让我们看看里面有什么:

admin@myhome:~$ head mydatabase.sql
--
-- PostgreSQL database dump
--
-- Dumped from database version 14.1
-- Dumped by pg_dump version 14.1
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;

在一些 SET 语句之后,你还应该找到 CREATE TABLEINSERT 语句。由于输出内容较多,我没有提供完整的输出。

在本节中,我们学习了如何为脚本设置测试环境,并使得脚本能够创建数据库转储。在下一节中,我们将更多地关注错误处理,检查备份是否成功。

错误处理和调试

在运行备份脚本时,我们可能会遇到几种错误:数据库访问可能被阻止,pg_dump 进程可能被杀死,磁盘空间可能不足,或者任何其他错误导致我们无法完成完整的数据库转储。

在任何这些情况下,我们需要捕获错误并优雅地处理它。

此外,我们可能需要重构脚本,使其具有配置功能,使用函数,并进行调试。调试将非常有用,特别是在处理较大的脚本时。

让我们深入了解并开始添加一个函数:

#!/usr/bin/env bash
function run_dump() {
  database_name=$1
  pg_dump -U postgres $database_name > $database_name.sql
}
run_dump mydatabase

我们添加了一个 run_dump 函数,它接受一个参数,并将该参数的内容设置为一个名为 database_name 的局部变量。然后它使用这个局部变量将选项传递给 pg_dump 命令。

这将立即允许我们通过使用 for 循环来备份多个数据库,代码如下:

for dbname in mydatabase mydatabase2 mydatabase3; do
    run_dump $dbname
done

这个循环将创建数据库 mydatabasemydatabase2mydatabase3 的完整转储。备份将逐个完成,通过此函数。我们现在可以将数据库列表放入变量中,以使其更具配置性。当前脚本将如下所示:

#!/usr/bin/env bash
databases="mydatabase"
function run_dump() {
  database_name=$1
  pg_dump -U postgres $database_name > $database_name.sql
}
for database in $databases; do
  run_dump "$database"
done

现在,这个备份脚本变得更加复杂。我们需要注意接下来会发生的一些事情:

  • 如果任何备份失败,脚本将继续运行

  • 如果备份由于 pg_dump 无法访问数据库而失败,我们将覆盖之前的数据库转储

  • 我们将在每次运行时覆盖转储文件

在脚本中,几项默认设置被认为是良好的实践需要覆盖。我们提到的第一个问题可以通过在任何命令返回一个与零(或 true)不同的值时终止运行来减轻。这意味着该命令执行时出现了错误。此选项名为 errexit,我们可以通过 set 命令覆盖它,set 是 Bash 内建命令。我们可以通过两种方式来实现这一点:

set -o errexit
set -e

这里是我们推荐使用的一些其他选项:

  • set -u:这将把我们在脚本中尝试使用的任何未设置的变量视为错误

  • set -o pipefail:当使用管道链式命令时,这个管道的退出状态将是最后一个命令的状态,如果该命令以非零状态结束,或者如果所有命令都成功执行(即退出状态为零),则为零(成功)。

  • set -Cset -o noclobber:如果启用,Bash 将不会使用重定向命令(例如我们在脚本中使用的 >)覆盖任何现有文件

一个非常有用的附加选项是 set -xset -o xtrace,它使得 Bash 在执行每个命令之前打印该命令。

让我们看看一个简单 Bash 脚本的工作原理:

#!/usr/bin/env bash
set -x
echo "Hello!"

这是执行该脚本后的输出:

admin@myhome:~$ ./simple_script.sh
+ echo 'Hello!'
Hello!

让我们用推荐的 Bash 设置更新我们的备份脚本:

#!/usr/bin/env bash
set -u
set -o pipefail
set -C
databases="mydatabase"
function run_dump() {
  database_name=$1
  pg_dump -U postgres $database_name > $database_name.sql
}
for database in $databases; do
  run_dump "$database"
done

现在,让我们回到控制台测试它是否仍然按预期工作:

admin@myhome:~$ ./run_backups.sh
./run_backups.sh: line 12: mydatabase.sql: cannot overwrite existing file

我们已启用 noclobber 选项,它已防止我们覆盖先前创建的备份。我们需要重命名或删除旧文件才能继续。现在,我们还将启用 xtrace 选项,以查看正在执行的命令脚本:

admin@myhome:~$ rm mydatabase.sql
admin@myhome:~$ bash -x ./run_backups.sh
+ set -u
+ set -o pipefail
+ set -C
+ databases=mydatabase
+ for database in $databases
+ run_dump mydatabase
+ database_name=mydatabase
+ pg_dump -U postgres mydatabase

为了避免覆盖现有文件错误,我们可以采取以下三种方法之一:

  • 在尝试运行备份之前删除旧文件,这将销毁以前的备份。

  • 重命名上一个备份文件并添加当前日期后缀。

  • 确保每次运行脚本时,转储文件都有一个不同的名称,例如当前日期。这将确保我们保留以前的备份,以防需要恢复到比上次完整备份更早的版本。

在这种情况下,最常见的解决方案是我们提出的最后一个——每次备份运行时生成不同的备份文件名。首先,让我们尝试获取一个带有本地日期和时间的时间戳,格式为 YYYYMMDD_HHMM,我们有以下选项:

  • YYYY:当前年份的四位数字格式

  • MM:当前月份的两位数字格式

  • DD:两位数格式的日期

  • HH:当前小时

  • MM:当前分钟

我们可以通过使用date命令来实现这一点。默认情况下,它将返回当前的日期、星期几和时区:

admin@myhome:~$ date
Fri Dec 16 14:51:34 UTC 2022

为了更改该命令的默认输出,我们需要使用格式字符传递一个日期格式字符串。

date 命令的最常见格式字符如下:

  • %Y:年份(例如,2022)

  • %m:月份(01-12)

  • %B:长月份名称(例如,January)

  • %b:短月份名称(例如,Jan)

  • %d:日期(例如,01-31,取决于某个月的天数)

  • %j:年份中的天数(例如,001-366)

  • %u:星期几(1-7)

  • %A:完整的星期几名称(例如,Friday)

  • %a:短星期几名称(例如,Fri)

  • %H:小时(00-23)

  • %I:小时(01-12)

  • %M:分钟(00-59)

  • %S:秒(00-59)

  • %D:以 mm/dd/yy 格式显示日期

要按照我们希望的格式格式化日期,我们需要使用格式字符 %Y%m%d_%H%M,并将其传递给 date 命令进行解释:

admin@myhome:~$ date +"%Y%m%d_%H%M"
20221216_1504

为了将输出字符串传递给脚本中的变量,我们需要在子 shell 中运行 date(由我们的 Bash 脚本执行的 Bash 进程):

timestamp=$(date +"%Y%m%d_%H%M")

让我们将其放入脚本中,并使用timestamp变量来生成输出文件名:

#!/usr/bin/env bash
set -u
set -o pipefail
set -C
timestamp=$(date +"%Yum'd_%H%M")
databases="mydatabase"
function run_dump() {
  database_name="$1"
  pg_dump -U postgres "$database_name" > "${database_name}_${timestamp}".sql
}
for database in $databases; do
  run_dump "$database"
done

如果你在pg_dump命令行中看到变量之间有大括号,可能会想知道为什么我们需要它们。我们使用大括号确保变量名称在扩展为字符串时是正确的。在我们的案例中,我们防止了 Bash 尝试搜索一个不存在的变量名$database_name_

现在,每次运行备份脚本时,它都会尝试创建一个带有当前日期和备份开始时间的文件。如果我们每天运行这个脚本,文件数量会随时间增加,最终填满我们的磁盘空间。所以,我们还需要让脚本删除旧备份——比如,删除 14 天及以上的备份。

我们可以通过使用find命令来实现这一点。让我们找到所有以数据库名开头,后跟下划线,且以.sql结尾的文件,这些文件的修改时间超过 14 天:

admin@myhome:~$ find . -name "mydatabase_*.sql" -type f -mtime +14
./mydatabase_20221107.sql

find命令有一种独特的语法,与其他命令行工具略有不同,所以我们来描述一下每个选项的含义:

  • .(一个点):这是我们希望搜索文件的目录。点代表当前目录

  • -name:此选项可以接受完整字符串或通配符,如*?,用于查找文件名。它是区分大小写的。如果我们不确定正在查找的文件或目录是大写还是小写,可以使用-iname选项代替。

  • -type f:这表示我们在寻找一个常规文件。其他选项如下:

    • d:目录

    • l:符号链接

    • s:套接字文件

    • p:FIFO 文件

    • b:块设备

    • c:字符设备

  • -mtime +14:此文件的修改时间应早于 14 天。此选项还可以接受其他单位(秒,s;周,w;小时,h;天,d——如果未提供单位,则默认为天)。

要删除找到的文件,我们至少有两种选择:使用-delete选项或通过-exec find选项使用rm命令。让我们看看在这两种情况下的表现:

admin@myhome:~$ find . -name "mydatabase_*.sql" -type f -mtime +14 -delete
admin@myhome:~$ find . -name "mydatabase_*.sql" -type f -mtime +14 -exec rm -- {} \;
  • 在这种情况下,更安全的选择是使用-execdir而不是-exec。它们的区别微妙但重要:-exec不会在找到的文件所在的同一目录中执行,而-execdir会,这使得它在边缘情况下更安全。

让我们解析一下-exec选项之后的内容:

  • rm:这是一个 CLI 工具,用于删除文件或目录。

  • --(双破折号):这表示将从stdin获取参数,或find命令的输出。

  • {}:这将替代我们找到的文件名。

  • \;(反斜杠,分号):这将允许多个命令由-exec执行。反斜杠是一个转义字符,可以防止该分号被解释为下一个命令的分隔符。find工具使用;+来终止 Shell 命令,因此我们可以将其标记为";"\++(不带引号)。

  • -delete选项用于删除文件,但它总是返回true,因此如果例如我们的脚本没有删除文件的权限,它会悄无声息地失败。在我们的脚本中使用它相对安全,所以我们会继续使用它。

现在,让我们将这个嵌入到我们的脚本中,看看它的最终版本:

#!/usr/bin/env bash
set -u
set -o pipefail
set -C
timestamp=$(date +"%Y%m%d_%H%M")
databases="mydatabase"
function cleanup_old_backups() {
  database_name="$1"
  find . -type f -name "${database_name}_*.sql" -mtime +14 -delete
}
function run_dump() {
  database_name="$1"
  pg_dump -U postgres "$database_name" > "${database_name}_${timestamp}".sql
}
for database in $databases; do
  cleanup_old_backups "$database"
  run_dump "$database"
done

在这里,我们添加了一个名为cleanup_old_backups的函数,它会在创建新转储之前运行,以释放一些空间来存放新的文件。我们在run_dump之前的 for 循环中调用了这个函数。这个脚本可以通过cron 守护进程systemd cron服务自动运行;我们将在第五章在 Linux 中管理服务中更详细地讨论这一点。

在本节中,我们了解了在 Shell 脚本中建议启用的选项以及如何启用调试选项。我们现在知道如何创建函数和循环。此外,我们也部分了解了 PostgreSQL 以及如何创建测试数据库。

下一章将带领我们深入了解各种 Linux 服务,以及如何使用initsystemd来管理它们。

总结

Shell 脚本是一种在 Linux 系统中自动化定期执行任务的常见方式。有时,它会演变成一个更大的系统,通过多个 Bash 脚本和 Python 程序连接起来,完成复杂任务,同时利用多个小任务以非常可靠的方式同时做一件事。

在现代系统中,你可能会看到和 Bash 一样多的 Python 脚本。

在这一章中,我们学习了如何创建可执行脚本,并且如何创建一个简单的备份脚本,处理错误并在每次运行时生成一个新的文件名。我们还添加了一个函数来删除旧的备份,以避免填满磁盘空间。此外,作为附带效果,我们学习了如何创建一个新的 PostgreSQL 数据库,并允许本地系统访问它。

在下一章,我们将学习如何创建 Linux 服务以及如何管理它们。

练习

尝试以下练习,以测试你对本章内容的理解:

  1. 编写一个函数,列出所有数据库,并将该列表传递给我们创建的 for 循环。

  2. 将日期时间戳转换为您选择的另一种格式。

  3. 捕捉find函数可能返回的任何错误(例如,它无法删除文件)。

第二部分:日常 DevOps 工具

在第二部分中,我们将学习 Linux 内部机制。从管理服务和网络开始,我们将继续了解最常见的工具,如 Git 和 Docker。

本部分包含以下章节:

  • 第五章在 Linux 中管理服务

  • 第六章Linux 中的网络

  • 第七章Git,通向 DevOps 的大门

  • 第八章Docker 基础

  • 第九章深入探讨 Docker

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值