Linux 系统管理的哲学(三)
原文:The Linux Philosophy for SysAdmins
协议:CC BY-NC-SA 4.0
十一、尽早测试,经常测试
你知道,我差点忘了包括这一章。忘记测试我编写的程序就像忽略测试程序本身一样容易。
这是为什么呢?
我希望我有一个明确的答案。在某些方面,它类似于文档。一旦程序看起来工作了,我们只想继续做最初让我们写这个程序的任务。
总会多一个 bug。
—卢巴尔斯基的控制论昆虫学定律
不管卢巴斯基是谁,他都是正确的。我们永远也找不到代码中的所有 bug。对于我发现的每一个,似乎总会有另一个突然出现,通常是在非常不合时宜的时候。
在第十章,“总是使用 Shell 脚本”,我们开始谈论测试和我用于测试的过程。本章更详细地介绍了测试。您将了解测试如何影响系统管理员所做的许多任务的最终结果。你还会学到测试是哲学的一个组成部分。
然而,测试不仅仅是关于程序。这也是为了验证我们应该已经解决的问题——无论是由硬件、软件还是用户似乎永无止境的破坏方式引起的——实际上已经解决了。这些问题可能与我们编写的应用或实用软件、系统软件、应用和硬件有关。同样重要的是,测试也要确保代码易于使用,界面对用户有意义。
程序
我之前的一份工作是在思科公司做基于 Linux 的设备测试员。我开发测试计划,编写 Tcl/Expect 代码来实现测试计划,并帮助跟踪失败的根本原因。我喜欢那份工作,从中学到了很多东西。
我在第十章中简单地提到了测试,但是这里有必要提供更多关于我的程序的细节。在编写和测试 shell 脚本时,遵循定义良好的过程有助于获得一致和高质量的结果。我的程序很简单。
-
创建测试计划,至少是简单的测试计划。
-
在开发之初就开始测试。
-
当代码完成时,执行最终测试。
-
转向生产并进行更多测试。
创建测试计划
测试是一项艰苦的工作,它需要一个基于需求陈述的设计良好的测试计划。不管情况如何,从测试计划开始。甚至一个非常基本的测试计划也提供了一些保证,测试将是一致的,并且覆盖代码的所需功能。
任何好的计划都包括测试来验证代码做了它应该做的一切。也就是说,如果你输入 X 并点击按钮 Y,你应该得到结果 Z。因此,您编写一个测试来创建这些条件,然后验证 Z 是结果。
最好的计划包括测试来确定代码失败的程度。当我在 1982 年得到我的第一台 IBM 个人电脑时,我艰难地发现了这一点。
PC 在 1981 年 8 月刚刚发布,员工购买直到 1982 年初才开始。没有太多的项目,尤其是针对孩子的。我想给我的小儿子介绍个人电脑,但是找不到合适的,所以我用 BASIC 写了一个小程序,我想他会喜欢的。坦白地说,我甚至不记得它应该做什么。
我用我能想到的所有方法测试了那个程序。它做了它应该做的一切。然后我把电脑交给儿子,走出房间。我还没走多远,他就喊道:“爸爸!它应该这样做吗?”不是的。我问他做了什么,他描述了一些非常奇怪的按键,我说,“你不应该这样做,”并立即意识到这对他来说是多么愚蠢。
我的问题是我没有测试过程序对意外输入的反应。这似乎是所有程序的一个共同问题。但是我永远不会忘记那个特殊的教训。因此,我总是试图包含测试意外输入的代码,然后我测试以确保程序检测到它并正常失败。
测试计划有许多不同的格式。我处理过所有的问题,从在脑子里记下所有的内容,到在一张纸上记下一些笔记,再到一组复杂的表格,这些表格需要对每个测试进行完整的描述,测试哪些功能代码,测试要完成什么,以及输入和结果应该是什么。
作为一个曾经是但现在不是测试人员的系统管理员,我试图采取中间立场。至少有一个简短的书面测试计划将确保从一个测试运行到下一个测试运行的一致性。您需要多少细节取决于您的开发和测试过程的正式程度。
测试计划内容
我使用 Google 找到的所有样本测试计划文档都很复杂,并且是为具有非常正式的开发和测试过程的大型组织设计的。虽然这些测试计划对那些职位名称中带有“测试”的人来说是好的,但是它们真的不适用于系统管理员和我们更加混乱和快速的依赖于时间的工作环境。正如我们工作的大多数其他方面一样,我们需要有创造力。因此,这里有一个简短的列表,列出了您想要考虑包含在您的测试计划中的事情。修改它以适合你的需要。
-
被测试软件的名称和简短描述。
-
待测试软件特性描述。
-
每次测试的开始条件。
-
每次测试应遵循的程序。
-
对每项测试的预期结果的描述。
-
包括为测试负面结果而设计的特定测试。
-
测试程序如何处理意外输入。
-
对每项测试的通过或失败的清晰描述。
-
模糊测试,将在下面描述。
这个简短的列表应该给你一些创建你自己的测试计划的想法。对于大多数系统管理员来说,这应该保持简单和相当不正式。
从头开始测试
我总是在完成可执行的第一部分后就开始测试我的 shell 脚本。无论我是在编写一个简短的命令行程序还是一个可执行文件的脚本,都是如此。
我通常用 shell 脚本模板开始创建新程序,您在实验 10-2 中已经有机会探索过了。我编写帮助过程的代码并测试它。这通常是流程中微不足道的一部分,但它帮助我开始,并确保模板中的东西在一开始就正常工作。此时,很容易修复脚本的模板部分的问题,或者修改它以满足标准模板不能满足的特定需求。
当模板和帮助过程工作时,我继续通过添加注释来创建程序体,以记录满足程序规范所需的编程步骤。现在我开始添加代码来满足每个注释中陈述的需求。这段代码可能需要添加在模板的那个部分中初始化的变量——这现在变成了我们的 shell 脚本。
在这里,测试不仅仅是输入数据和验证结果。这需要一点额外的工作。有时,我会添加一个命令,简单地打印我刚刚编写的代码的中间结果,并进行验证。其他时候,对于更复杂的脚本,我会为“测试模式”添加-t 选项在这种情况下,只有在命令行输入-t 选项时,才会执行内部测试代码。
最终测试
代码完成后,我使用已知的输入产生特定的输出,对所有特性和功能进行完整的测试。我还测试了一些随机输入,看看程序现在是否可以处理意外的输入。
最终测试的目的是验证程序现在已经完成,基本上按预期运行。最终测试的很大一部分是为了确保在开发周期早期工作的功能没有被在周期后期添加或更改的代码破坏。
如果您在向脚本中添加新代码时一直在测试脚本,那么在最后的测试中应该不会有什么意外。错了!最终测试中总会有惊喜。一直都是。期待那些惊喜,并准备好花些时间去修复它们。如果在最终测试中没有发现任何错误,那么就没有必要进行最终测试,不是吗?
生产中的测试
嗯——什么?
直到一个程序投入生产至少六个月后,最有害的错误才会被发现。
—Troutman 的编程假设
是的,生产中的测试现在被认为是正常的和可取的。作为一名测试人员,这看起来确实是合理的。“但是等等!这很危险,”你说。我的经验是,它并不比在专用测试环境中进行广泛而严格的测试更危险。在某些情况下,没有选择,因为没有测试环境,只有生产环境。
我的一份工作就是这样,我负责维护大量为网站生成动态页面的 Perl CGI 脚本。这个庞大组织的电子邮件管理界面的整个网站都运行在一个非常陈旧的戴尔台式机系统上。那是我们的关键服务器。我有一台更旧的戴尔台式机,我可以通过它登录服务器进行编程。这两台电脑都运行早期版本的 Red Hat Linux。
我们不得不做的唯一选择是在中午的时候进行许多关键的修改,然后在生产中进行测试。那是多么有趣啊!
最终,我们获得了几个额外的旧台式机作为开发和测试环境,但在我们做到这一点之前,这是一个棘手的挑战。缺乏运行这个大型电子邮件系统的设备的部分原因是,它最初只是一个部门的小规模试点测试。随着越来越多的部门一听说这件事就要求加入,这件事迅速失去了控制。试点测试从来没有资金支持,通常幸运的是得到了另一个部门的旧的和不需要的设备。
因此,系统管理员对在生产中测试新的或修改过的脚本并不陌生。任何时候一个脚本被转移到产品中,那都成为最终的测试。生产环境本身构成了该测试最关键的部分。测试人员在测试环境中想象出来的任何东西都不能完全复制真实的生产环境。
所谓的生产中测试的新实践只是对我们系统管理员一直以来所知道的事情的认识。最好的测试是生产——只要它不是唯一的测试。
最终测试之后,程序可以进入生产阶段。生产本身总是一个考验。在隔离的开发和测试环境中编写代码并不能代表真实生产环境中遇到的情况。
无论脚本写得多好,测试得多好,总会有新的 bug 出现在产品中。正如 Troutman 的假设所说,最有害的错误在程序投入生产后的相当长一段时间内不会被发现,并且每个人都认为结果总是正确的。最有害的错误不是那些导致程序崩溃的错误;他们是悄悄导致不正确结果的人。
继续检查脚本产生的结果,即使它已经投入生产。寻找下一个 bug,你最终会找到的。
模糊测试
这是我第一次听到时引起我翻白眼的又一个流行语。我了解到它的本质含义很简单——让某人敲击键盘,直到某件事情发生,然后看看程序处理得如何。但是实际上不止如此。
模糊测试有点像我儿子用他的随机输入在不到一分钟的时间里破坏了我的代码。大多数测试计划利用非常具体的输入来生成具体的结果或输出。不管测试是正面的还是负面的成功结果,它仍然是受控的,并且输入和结果是指定的和预期的,例如针对特定故障模式的特定错误消息。
模糊测试是关于处理测试所有方面的随机性,例如开始条件、非常随机和意外的输入、选择的选项的随机组合、低内存、与其他程序的高水平 CPU 争用、测试中程序的多个实例,以及您能想到的应用于测试的任何其他随机条件。
我试着从一开始就做一些模糊测试。如果 bash 脚本不能在早期阶段处理显著的随机性,那么随着我们添加更多的代码,它也不可能变得更好。这也是捕捉这些问题并在代码相对简单时修复它们的好时机。在完成的每个阶段进行一点模糊测试也有助于在问题被更多代码掩盖之前找到问题。
代码完成后,我喜欢做一些更广泛的模糊测试。总是做一些模糊测试。我确实对我遇到的一些结果感到惊讶。测试预期的事情很容易,但是用户通常不会用脚本做预期的事情。
自动化测试
测试可以是自动化的,但是我们作为系统管理员所做的大部分工作都有内在的时间压力,不允许花时间编写代码来测试我们的代码。这些压力是我们写的大多数代码又快又脏的原因。所以我们匆忙地编写代码并测试它。
使用像 Tcl/Expect 这样的工具为我们的 shell 脚本编写一个复杂的测试套件是可能的。作为一名系统管理员,我从来没有时间做那么正式的事情。作为一名系统管理员,我做过的最自动化的事情就是编写一个非常短的脚本,通过一组命令来验证测试脚本的一些关键方面。大多数时候,我在程序完成的每一步都进行手工测试。使用 bash 历史可以作为一种合理的替代,并且至少提供一些半自动测试。
我在思科担任测试人员时,使用 Tcl/Expect 编写了很多测试。我的任务是编写将被之前编写的测试床调用的模块。我编写的 Tcl/Expect 代码本来可以作为独立的测试运行,但是测试平台提供了一个框架,该框架聚合了来自各个测试的所有结果,并生成了一组很好的报告,使我们能够看到我们在将错误修复应用于代码方面取得了多大的进展。
有许多商业测试套件可用。许多都非常昂贵,并不特别适合系统管理员使用,因为学习它们需要付出努力,准备测试也需要时间。
编写 Tcl/Expect 程序非常耗时,但在开发大型代码库时,它会非常有用。我最喜欢的 Tcl/Expect 的书是 Exploring Expect 、1,里面包含了大量关于 Tcl 的信息。维基百科上有一篇关于软件测试的优秀文章,有很多更深入的链接。
尝试一下
在实验 10-2 中,你对 shell 脚本模板的副本做了一些修改。在该实验的每一步,您都测试了所做更改的结果,因此您已经熟悉了基本的 SysAdmin 开发过程。本章中的实验将使用该过程来开发和测试一个程序,该程序将列出一些关于您的 Linux 主机的有趣信息和统计数据。最后,我们将会有一个经过充分测试的相当长的脚本。该脚本的典型输出如图 11-1 所示。
图 11-1
由您将在本章的实验中创建的 shell 脚本生成的 MOTD 示例
我将这个脚本作为 cron 作业运行,每天生成一个报告,并存储为/etc/motd,这是当天消息文件。每当有人使用远程终端或虚拟控制台登录时,就会显示 MOTD。
在我们开始编码之前,我们需要首先创建一组需求,然后创建一个简单的测试计划。
MOTD 脚本的要求
一组简单的要求将有助于我们设计程序,并针对我们希望包含的特定功能提供帮助。这些要求应该工作得很好,但要为创造性留有余地。
-
所有的输出都发送到 STDOUT 和 STDERR,以便根据需要进行重定向。
-
提供打印脚本发布版本的选项。
-
以令人满意的格式打印下列数据。
-
带有当前日期的标题
-
主机名
-
机器类型–虚拟机或物理机
-
主机硬件架构 X86_64 或 i386
-
主板供应商和型号
-
CPU 型号和超线程状态
-
内存容量(GB)
-
交换空间的大小(GB)
-
Linux 的安装日期
-
Linux 发行版
-
内核版本
-
磁盘分区信息
-
LVM 物理卷信息
-
-
包括描述代码的注释。
-
应该不需要选项来产生期望的输出。
这似乎是一个很长的列表,但是与我看到的一些需求集相比,这是相当短的。我根据一组类似的需求创建了最初的 bash 脚本。我们在第十章“总是使用脚本”中创建的脚本模板已经有了可以帮助满足这些需求的代码。
有人可能对这个列表的唯一问题是“取悦”这个词。谁知道对于使用这个脚本的人来说,什么是令人愉快的,什么是令人不愉快的。所以对于这个实验来说,取悦将是我所说的。在其他环境中,可能需要许多页的需求来定义输出的显式格式。在 SysAdmin 环境中,取悦通常是对我们或任何要求编写程序的人有用的。
MOTD 脚本的测试计划
我们的测试计划简单明了。
-
验证 help (-h)选项显示了正确的帮助信息。
-
验证 GPL (-g)选项显示 GPL 许可证声明。
-
验证要求中规定的所有输出数据均已生成。
-
验证执行测试的系统的所有打印输出是否正确。
-
通过与其他来源进行比较,验证数字输出的值是否正确。由于任何正在运行的计算机的动态性质,这些数字中的一些可能会在运行之间以及与其他来源进行比较时发生变化,但是它们应该相当接近。
-
确保不正确的选项选择会产生相应的错误代码。
-
如果可能,在多个系统上进行测试,包括物理硬件和虚拟机,以验证不同条件下的正确结果。包括英特尔、AMD 以及 ARM 硬件。
-
如果可能的话,用多个 Linux 发行版进行测试。
这个简单的测试计划是我们在测试脚本时需要知道的一切。我们知道需要检查的输出,因为它们是在程序需求中定义的。
最后两项在学习环境中可能是不可能的,但它总是需要考虑的。不同的环境应该用这个脚本产生不同的结果。在我们实际的开发/测试环境之外进行测试有助于确保其他环境的逻辑和结果是准确的。生产中的测试可以帮助解决这个问题。
开发脚本
记住,对于系统管理员来说,开发也意味着测试。由于创建完整的脚本需要大量的工作,我将它分成了一系列的实验,在这些实验中,每个实验将开发和测试一段代码来满足部分或全部特定需求。
注意
如果您不清楚一些命令是如何工作的,尤其是管道的各个阶段是做什么的,您应该研究一下它们。首先查看管道中每个命令的手册页,了解它的作用。然后构建管道——一次一个命令阶段,以查看结果。当我刚开始做系统管理员的时候,这对于我理解看起来复杂的代码非常有帮助。我仍然依赖这种方法来帮助我理解一些代码是如何工作的。
基础知识
让我们从基础开始——从修改后的模板复制脚本,在内部更改脚本名称,添加简短描述,并更改帮助过程以匹配我们程序的功能。
实验 11-1
使用 mymotd 的新名称制作 test1.sh 的副本。您可以以 root 用户或学生用户的身份编辑这个新脚本 mymotd,但是在测试时它必须以 root 用户的身份运行。我建议以非 root 用户的身份编辑 shell 脚本。打开两个终端会话并 su–root 其中一个会话。在终端会话中,以用户 student 的身份在您最喜欢的编辑器中打开 mymotd 脚本。
在我们编辑脚本时,请务必经常保存您的工作。
首先,让我们更改标题注释中的脚本名称,并添加脚本的简短描述。结果应该是这样的。
#!/bin/bash
###########################################################################
# mymotd #
# #
# This bash shell extracts various interesting bits of information about #
# the Linux host and the operating system itself. It prints this data to #
# STDOUT in a nice looking format. The results can also be redirected to #
# the/etc/motd file to create an informational message of the day. #
# #
# Change History #
# 01/08/2018 David Both Original code. #
# #
# #
###########################################################################
请确保将更改历史记录中的第一行设置为当前日期和您的姓名。
接下来,让我们学习帮助程序。这里所需要的只是添加一个简短的描述、一行描述命令语法的文字和一个可能选项的列表。
########################################################################
# Help #
########################################################################
Help()
{
# Display Help
echo " mymotd"
echo "Generate an MOTD that contains information about the system"
echo " hardware and the installed version of Linux."
echo
echo "Syntax: mymotd [-g|h|v|V]"
echo "options:"
echo "g Print the GPL license notification."
echo "h Print this Help."
echo "v Verbose mode."
echo "V Print software version and exit."
echo
}
让我们进行第一次测试。在作为 root 的终端会话中,将/home/student 设为 PWD,这是新代码所在的位置。现在,每次运行时使用三个选项之一来运行程序;-h 测试帮助工具,-g 测试打印 GPL 语句,-x 测试无效选项。
[root@testvm1 student]# ./mymotd -h
mymotd
Generate an MOTD that contains information about the system
hardware and the installed version of Linux.
Syntax: mymotd [-g|h|v|V]
options:
g Print the GPL license notification.
h Print this Help.
v Verbose mode.
V Print software version and exit.
[root@testvm1 student]# ./mymotd -g
#############################################################################
# Copyright (C) 2007, 2016 David Both #
# Millennium Technology Consulting LLC #
# http://www.millennium-technology.com #
# #
# This program is free software; you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation; either version 2 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program; if not, write to the Free Software #
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #
##############################################################################
[root@testvm1 student]# ./mymotd -x
ERROR: Invalid option
mymotd
Generate an MOTD that contains information about the system
hardware and the installed version of Linux.
Syntax: Syntax: mymotd [-g|h|v|V]
options:
g Print the GPL license notification.
h Print this Help.
v Verbose mode.
V Print software version and exit.
Program terminated with error ID 10T
如果你还能想到任何需要执行的测试,比如模糊测试,现在就去做吧。如果您在测试时发现任何问题,现在就修复它们,然后再次测试。
添加健全性检查
在第十章中,我们注释掉了健全性检查,以确保脚本由 root 运行,因此我们需要恢复它。我们还将添加一项检查,以确保脚本在 Linux 主机上运行。作为一个 bash 脚本,它可以与各种 Unix 系统兼容,但是一些特定于 Linux 的功能会失败。
实验 11-2
首先从根检查中移除注释散列。现在看起来像这样。
#---------------------------------------------------------------------------
# Check for root.
if [ `id -u` != 0 ]
then
echo ""
echo "You must be root user to run this program"
echo ""
Quit 1
fi
现在做两个快速测试。以 root 用户身份运行程序,以确保 root 用户仍然可以使用该程序。
[root@testvm1 student]# ./mymotd
total used free shared buff/cache available
Mem: 4046060 254392 2947324 984 844344 3532200
Swap: 4182012 0 4182012
以学生用户的身份运行程序,验证非根用户是否收到错误。
[student@testvm1 ~]$ ./mymotd
You must be root user to run this program
如果您发现任何错误,请在我们继续之前修复它们。
让我们添加第二个健全性检查,以确保这个程序在 Linux 系统上运行。该测试使用uname命令返回操作系统名称。
在检查 root 用户的代码下面添加以下代码。
#---------------------------------------------------------------------------
# Check for Linux
if [[ "$(uname -s)" != "Linux" ]]
then
echo ""
echo "This script runs on Linux only -- OS detected: $(uname -s)."
echo ""
Quit 1
fi
#---------------------------------------------------------------------------
我们只能测试积极的结果——也就是说,我们在 Linux 上运行——而不能测试消极的结果,除非我们在非 Linux 主机上测试。但至少让我们做阳性测试。只需运行该命令并验证您没有得到任何错误。
版本号
所有程序都应该有一个版本号。这个脚本已经有一个指定版本号的变量,但是它是脚本模板的版本,而不是我们正在工作的新程序的版本。让我们设置我们的版本号。由于这是在开发过程的早期,它不会是一个完整的版本级别。
我喜欢用三个两位数的部分来表示我的版本号,以保证灵活性。所以让我们从版本 00.01.00 开始,因为它表示代码远未准备好。随着我们越来越接近释放到野外,这个数字将上升,第一次完全释放将是 01.00.00。
实验 11-3
首先在代码的变量部分设置版本号。让我们也删除这个部分中的 RC(返回代码)行。这就是我们目前在变量部分所拥有的,但是当我们在本章的其余实验中进行时,更多的变量将被添加到这里。
# Set initial variables
badoption=0
error=0
verbose=0
Version=00.01.00
现在我们甚至测试这一点点新代码,尽管它看起来简单无害。
[root@testvm1 student]# ./mymotd -V
Version = 00.01.00
但是您还没有真正完成测试。您应该对以前编码的函数做一些额外的测试,以确保它们没有被新代码破坏。
至此,基础工作已经完成。我们有一个显示帮助、GNU 许可证声明、执行一些健全性检查和显示版本号的部分脚本。我们不需要向 case 语句添加任何选项,因为我们需要的一切都已经存在了。
主体
现在我们可以添加代码的主体来收集我们想要的数据并显示出来。我们将按照所需的顺序收集数据,并在进行过程中将其打印到 STDOUT。这将使早期测试变得容易。
实验 11-4
首先,删除我们用来从脚本中提供一些输出的free命令。
现在让我们给程序添加一些代码。在程序主体中,在Quit函数调用之前,添加执行这些函数的代码。我们还添加了一些代码来打印我们收集的数据。这提供了一个简单的测试,同时也产生了预期的结果。
这段代码现在看起来像这样。
########################################################################
########################################################################
# The main body of your program goes here.
########################################################################
########################################################################
# Get the date
Date=`date`
# Get the hostname
host=`hostname`
########################################################################
# Start printing the data using printf to make it pretty #
########################################################################
printf "#############################################################\n"
printf "# MOTD for $Date\n"
printf "# HOST NAME: \t\t$host \n"
在 printf 语句中,\t 在输出中插入一个制表符,\n 是一个换行符。当我第一次编写这个脚本的时候,我花了一段时间来正确格式化整个输出。你有我的知识优势,这是我先做的。
将以下变量添加到初始化部分。
host=""
Date=""
现在让我们运行程序来测试这些结果。
[root@testvm1 student]# ./mymotd
########################################################################
# MOTD for Sat Jan 13 12:14:34 EST 2018
# HOST NAME: testvm1
这对于我的虚拟机主机来说看起来是正确的。因此,在五行活动代码中——不包括注释行——我们已经开始编写和测试我们的脚本。
到目前为止,代码非常适合这样的开发/测试周期。我们添加代码来获取一些数据,并添加一些代码来打印这些数据。事情会变得更加复杂。
实验 11-5
现在,我们想要添加一些代码来确定主机是物理机还是虚拟机,并且我们还想要一些关于主板的信息。我们需要这样做的 Linux 命令dmidecode并不总是被安装,所以我们需要确保它存在于我们的主机上。最简单的方法就是尝试安装它。如果不存在,它将被安装。以 root 用户身份执行此操作。
[root@testvm1 ~]# dnf install -y dmidecode
dmidecode 实用程序(其中 dmi 代表桌面管理界面)可以访问由系统管理 BIOS (SMBIOS)维护的硬件数据表。例如,使用以下命令检索有关主板的数据。“-t”表示“类型”,类型 2 是主板。dmidecode 手册页列出了所有可用的数据类型。
[root@david ~]# dmidecode -t 2
# dmidecode 3.1
Getting SMBIOS data from sysfs.
SMBIOS 3.0.0 present.
Handle 0x0002, DMI type 2, 15 bytes
Base Board Information
Manufacturer: ASUSTeK COMPUTER INC.
Product Name: TUF X299 MARK 2
Version: Rev 1.xx
Serial Number: 170807951700403
Asset Tag: Default string
Features:
Board is a hosting board
Board is replaceable
Location In Chassis: Default string
Chassis Handle: 0x0003
Type: Motherboard
Contained Object Handles: 0
上面 dmidecode 命令的结果来自我的主工作站,而不是测试虚拟机。
注意这个 mymotd 脚本是为 Intel 处理器和 Fedora Linux 编写的。它可以与其他芯片和发行版一起工作,但是某些代码部分的结果可能不正确。您可能想自己做一些实验,让这些部分在您的环境中工作。
现在我们已经安装了 dmidecode 工具,我们可以继续添加到我们的程序中。首先,我们想知道主机是虚拟机还是物理机。
实验 11-6
现在让我们添加一些代码来告诉我们主机是否是 VM 的物理机器。我们为此需要的信息是每次引导时启动的 dmesg 日志缓冲区的一部分。我们只需要搜索适当的文本字符串。将下面的代码添加到上一个实验的日期和主机名之后。
########################################################################
# Is this a VirtualBox, VMWare, or Physical Machine. #
########################################################################
if dmesg | grep -i "VBOX HARDDISK" > /dev/null
then
MachineType="VM running under VirtualBox."
elif dmesg | grep -i "vmware" > /dev/null
then
MachineType="VM running under VMWare."
else
MachineType="physical machine."
fi
printf "# Machine Type: \t$MachineType\n"
将 MachineType 变量添加到脚本的变量部分。然后测试这个新代码。
[root@testvm1 student]# ./mymotd
#######################################################################
# MOTD for Sun Jan 14 09:49:29 EST 2018
# HOST NAME: testvm1
# Machine Type: VM running under VirtualBox.
这对于我的测试虚拟机来说也是正确的。您的结果可能不同。
了解主机的架构,即它是 32 位还是 64 位的,通常会有所帮助。下一个实验添加了一些代码来确定这一点。
实验 11-7
添加以下三行代码,确定主机的架构是 32 位还是 64 位。
# Get the host physical architecture
HostArch=`echo $HOSTTYPE | tr [:lower:] [:upper:]`
printf "# Host architecture: \t$HostArch\n"
将 HostArch 添加到变量部分并进行测试。
[root@testvm1 student]# ./mymotd
#######################################################################
# MOTD for Sun Jan 14 10:43:05 EST 2018
# HOST NAME: testvm1
# Machine Type: VM running under VirtualBox.
# Host architecture: X86_64
这表明我们的虚拟机是 64 位的,这是正确的。如今大多数主机都是 64 位的,只有少数 32 位的还在,比如我的华硕 EeePC。然而,一些小型的单板计算机(SBC)仍然是 32 位的。
即使在虚拟机中,主板信息也很有趣,所以现在让我们来获取这些数据。不用拆开电脑就能获得这些信息的能力非常有用。
实验 11-8
以下代码添加在主机架构代码之后,并使用 dmidecode 提取有关主板的信息。
########################################################################
# Get the motherboard information #
########################################################################
MotherboardMfr=`dmidecode -t 2 | grep Manufacturer | awk -F: '{print $2}' | sed -e "s/^ //"`
MotherboardModel=`dmidecode -t 2 | grep Name | awk -F: '{print $2}' | sed -e "s/^ //"`
printf "# Motherboard Mfr: \t$MotherboardMfr\n"
printf "# Motherboard Model: \t$MotherboardModel\n"
printf "#------------------------------------------------------------\n"
注意,获得主板信息的代码行都包含在上面的清单中。请确保在一行中输入它们。我还添加了一行来打印分隔符,以便将其与下一部分数据分开。
将两个新变量添加到变量初始化部分。
MotherboardMfr=""
MotherboardModel=""
现在测试程序以验证结果。
[root@testvm1 student]# ./mymotd
#######################################################################
# MOTD for Sun Jan 14 10:57:05 EST 2018
# HOST NAME: testvm1
# Machine Type: VM running under VirtualBox.
# Host architecture: X86_64
# Motherboard Mfr: Oracle Corporation
# Motherboard Model: VirtualBox
#----------------------------------------------------------------------
结果的第一部分对虚拟机来说非常好,非常正确。它看起来很好,容易阅读。
至此,我们已经提取并打印了一些关于主机的一般信息。现在我们想添加一个部分,向我们展示一些关于 CPU 本身的信息。这些信息大部分位于/proc 文件系统中。很多时候它不能作为一个单一的、格式良好的数据点,所以我们需要使用我们的 Linux 工具来提取我们想要的东西并适当地格式化它。
实验 11-9
我们从/proc 文件系统中获取 CPU 型号信息开始获取 CPU 信息。这几行代码就是这样做的,所以将它们添加到程序的末尾,正好在Quit函数调用的上面。
########################################################################
# Get the CPU information #
########################################################################
# Starting with the specific hardware model
CPUModel=`grep "^model name" /proc/cpuinfo | head -n 1 | cut -d : -f 2 | sed -e "s/^ //"`CPUModel
printf "# CPU Model:\t\t$CPUModel\n"
给 CPUModel 变量赋值的那一行是换行的,所以一定要在程序中的一行中输入。将 CPUModel 变量添加到 variables 部分并进行测试。
[root@testvm1 student]# ./mymotd
#######################################################################
# MOTD for Sun Jan 14 15:54:24 EST 2018
# HOST NAME: testvm1
# Machine Type: VM running under VirtualBox.
# Host architecture: X86_64
# Motherboard Mfr: Oracle Corporation
# Motherboard Model: VirtualBox
#----------------------------------------------------------------------
# CPU Model: Intel(R) Core(TM) i9-7960X CPU @ 2.80GHz
我们代码的这个最新添加产生了关于我们系统中安装的 CPU 的良好信息。
让我们找到一些额外的 CPU 信息,例如 CPU 的数量和封装信息——每个芯片有多少个内核——以及我们是否有超线程技术。
实验 11-10
该实验收集了一些 CPU 数据,然后对 CPU 的封装方式以及 CPU 是否能够超线程进行了一些有根据的判断。
首先,让我们在初始化部分添加一些新变量。
PhysicalChips=0
Siblings=0
HyperThreading="No"
CPUSpeed=""
NumCores=0
Package=0
Arch=""
CPUdata=""
CPUArch=""
这里有很多代码,因为它们都是相关的,并且需要完整的测试才能成功。进入时要小心。特别要注意下面清单中的代码行。
#############################################################################
# Get some CPU details. #
#############################################################################
# Get number of actual physical chips
PhysicalChips=`grep "^physical id" /proc/cpuinfo | sort | uniq | wc | awk '{print $1}'`
if [ $PhysicalChips -eq 0 ]
then
let PhysicalChips=1
fi
# Get the total number of cores
CPUs=`cat /proc/cpuinfo | grep "cpu cores" | head -n 1 | cut -d : -f 2 | sed -e "s/^ //"`
# Do we have HyperThreading
Siblings=`grep "^siblings" /proc/cpuinfo | head -n 1 | cut -d : -f 2 | sed -e "s/^ //"`
if [ $Siblings -gt $CPUs ]
then
# Yes we have HyperThreading
HyperThreading="Yes"
fi
# Now Cores per CPU
# We are assuming each package has the same number of cores – the next line is wrapped
NumCores=`grep "^cpu cores" /proc/cpuinfo | sort | uniq | awk -F: '{print $2}' | sed -e "s/^ //"`
case "$NumCores" in
1) Package="Single Core";;
2) Package="Dual Core";;
4) Package="Quad Core";;
6) Package="Six Core";;
8) Package="Eight Core";;
12) Package="Twelve Core";;
16) Package="Sixteen Core";;
18) Package="Eighteen Core";;
20) Package="Twenty Core";;
24) Package="Twenty-four Core";;
26) Package="Twenty-six Core";;
28) Package="Twenty-eight Core";;
30) Package="Thirty Core";;
32) Package="Thirty-two Core";;
*) Package="Single Core"
NumCores=1;;
esac
# Get the CPU architecture which can be different from the host architecture
CPUArch=`arch`
# Now lets put some of this together to make printing easy
CPUdata="$PhysicalChips $Package $CPUArch"
# Get the CPU speed – The next line is wrapped
CPUSpeed=`grep "model name" /proc/cpuinfo | sed -e 's/.*\( [0-9]*.[0-9]*[GM]Hz\)/\1/' -e 's/^ *//g' | uniq`
# Let's print what we have
printf "# CPU Data:\t\t$CPUdata\n"
printf "# HyperThreading:\t$HyperThreading\n"
printf "#-----------------------------------------------------------------\n"
输入代码时,请务必仔细检查。然后我们再次测试。
[root@david development]# ./mymotd
#######################################################################
# MOTD for Mon Jan 15 15:35:09 EST 2018
# HOST NAME: david
# Machine Type: physical machine.
# Host architecture: X86_64
# Motherboard Mfr: ASUSTeK COMPUTER INC.
# Motherboard Model: TUF X299 MARK 2
#----------------------------------------------------------------------
# CPU Model: Intel(R) Core(TM) i9-7960X CPU @ 2.80GHz
# CPU Data: 1 Sixteen Core x86_64
# HyperThreading: Yes
#----------------------------------------------------------------------
我们的前两部分已经完成。下一节详细介绍系统内存要简单一些。
实验 11-11
这个实验添加代码来显示一些内存统计数据。这里我们再次使用现成的资源。
/proc/meminfo 文件有我们需要的数据,但它是以 KB 为单位的,为了清楚起见,我们希望它以 GB 为单位。为此,我们将以下过程添加到脚本的过程部分。Bash 没有像样的数学能力,所以这段代码使用 bc calculator 命令,它有自己独特的语法。
########################################################################
# Convert KB to GB #
########################################################################
kb2gb()
{
# Convert KBytes to Giga using 1024
# first convert the input to MB
echo "scale=3;$number/1024/1024" | bc
}
下面的代码应该添加在脚本末尾的最后一个退出过程调用之前,它从/proc/meminfo 文件中获取数据,然后将数字转换为 GB。然后打印结果。
############################################################################
# Memory and Swap data #
############################################################################
# Get memory size in KB.
number=`grep MemTotal /proc/meminfo | awk '{print $2}'`
# Convert to GB
mem=`kb2gb`
# Get swap size in KB
number=`grep SwapTotal /proc/meminfo | awk '{print $2}'`
# Convert to GB
swap=`kb2gb`
printf "# RAM:\t\t\t$mem GB\n"
printf "# SWAP:\t\t\t$swap GB\n"
printf "#-----------------------------------------------------------------\n"
我们需要在变量部分添加新的变量。
number=0
mem=0
swap=0
现在又到了考验的时候了。
[root@david development]# ./mymotd
#######################################################################
# MOTD for Mon Jan 15 21:56:40 EST 2018
# HOST NAME: david
# Machine Type: physical machine.
# Host architecture: X86_64
# Motherboard Mfr: ASUSTeK COMPUTER INC.
# Motherboard Model: TUF X299 MARK 2
#----------------------------------------------------------------------
# CPU Model: Intel(R) Core(TM) i9-7960X CPU @ 2.80GHz
# CPU Data: 1 Sixteen Core x86_64
# HyperThreading: Yes
#----------------------------------------------------------------------
# RAM: 62.586 GB
# SWAP: 14.902 GB
#----------------------------------------------------------------------
在 testvm1 主机上,测试结果如下所示。
[root@testvm1 student]# ./mymotd
#######################################################################
# MOTD for Mon Jan 15 21:57:32 EST 2018
# HOST NAME: testvm1
# Machine Type: VM running under VirtualBox.
# Host architecture: X86_64
# Motherboard Mfr: Oracle Corporation
# Motherboard Model: VirtualBox
#----------------------------------------------------------------------
# CPU Model: Intel(R) Core(TM) i9-7960X CPU @ 2.80GHz
# CPU Data: 1 Quad Core x86_64
# HyperThreading: No
#----------------------------------------------------------------------
# RAM: 3.854 GB
# SWAP: 7.999 GB
#----------------------------------------------------------------------
如果您有其他要测试的主机,您应该这样做来验证结果对于其他情况是否正确。
虽然根据我在本章开始时为它创建的需求集,这个脚本还没有完成,但我认为我们已经走得足够远了,你可以自己完成了。所以,我把这个脚本的完成作为一个练习留给你们,全世界的系统管理员。
如果您喜欢这段代码,但是需要一些额外的指导,或者只是不关心如何完成它,那么可以从以下网址下载这段 bash 脚本的完整代码:
https://github.com/Apress/linux-philo-sysadmins/tree/master/Ch11
无论您选择哪种方法,您都可以随意修改这些代码,以满足自己的需求。
修复脚本
很多时候我们需要修复现有的脚本。我最近写的一个剧本出了点问题。幸运的是,在造成任何损害之前,我就意识到了这个问题。正如您在 mymotd 脚本中看到的,我喜欢提供适量的注释来帮助我在以后解决问题。如果我不必在每次需要修改代码以修复它或添加新功能时都确定代码是如何工作的,这就容易多了。
正如我在本章的“生产测试”一节中提到的,有时候修改一个脚本需要从最基础的开始。在这种情况下,我添加了注释,使代码更容易阅读,并解决了我遇到的任何明显的错误。由于前面提到的情况,我们仅有的环境就是生产。每当我对代码进行哪怕是很小的修改时,我都必须进行测试,以确保修改按预期进行,并且没有任何可能依赖于修改后的代码的东西被破坏。
在最近的一个案例中,是我自己的注释相当好的代码工作不太正常。在这种情况下,在 u 盘上创建了一个 MP3 文件,其文件名包含 MMDDYYYY-X 格式的日期,其中 X 是序列号。我的程序将文件的名称改为包含直接取自文件名的日期,但重新排列为 YYYYMMDD-X,然后将文件复制到服务器。如果记录设备在一天内创建了两个文件,它们具有相同的日期,但是这些文件通过序列号来区分。我的脚本旨在保持它们的顺序,这样它们就可以按照创建的顺序进行排序。
当一个文件被创建并传输,然后同一天又创建了另一个文件时,问题就出现了。它将具有与第一个文件相同的文件名,并在服务器上覆盖它。
为了解决这个问题,我决定使用文件的时间戳以 YYYYMMDD-HHMMSS 的格式创建自己的时间戳,并在新文件名中消除序列号的使用。
这个特殊的修复是一个简单的问题,做了一个小的改动,修正了我以前没有考虑到的边缘情况。测试很容易;只需创建边界条件并运行程序,同时确保当边界条件不存在时程序仍能正确运行。我手头有几个样本 MP3 文件用于测试,只需将它们复制到 USB 记忆棒即可。
摘要
为系统管理员测试代码很像写代码——快速且不严谨。我希望最后一点听起来比“反复无常”更好快速编写代码通常意味着快速测试代码。这并不意味着测试 shell 脚本需要随机进行。“尽早测试,经常测试”是让测试成为编码的一部分的一个很好的口号。随着这一原则的应用,测试 shell 脚本的任务变成了第二天性,并且成为了编写脚本过程中不可或缺的一部分。
我发现本章中的 mymotd 脚本是重写我的原始版本的一个很好的借口。我在 2007 年写了这个脚本,我对硬件架构和 Linux 中的报告的更好理解帮助了我这个版本。我对 Linux 工具的了解,不管是新的还是新的,都有所提高,并给了我更多的灵活性来简化这个新脚本。
请注意,变量名都是对正在执行的任务有意义的名称。查看变量名并了解它应该包含的数据类型是可能的。这也有助于简化测试。
Footnotes 1唐·利贝斯,探索期望,奥赖利,2010 年,国际标准书号 978-1565920903
2
维基百科,软件测试, https://en.wikipedia.org/wiki/Software_testing
十二、使用常识命名
我在本书的几个地方提到过,打字不是我的强项,懒惰的系统管理员会尽一切可能减少打字。我对此很认真。这个原则对此进行了扩展,但是它不仅仅是减少我需要做的打字量。这也是关于脚本的可读性和命名的事情,以便他们更容易理解。
最初的 Unix 哲学原则之一——尽管是较次要的原则之一——是始终使用小写字母并保持名称简短。这是一个令人钦佩的目标,但在系统管理员的世界里却不那么容易实现。在许多方面,我自己的信条似乎是对原文的彻底驳斥。但是,最初的版本面向不同的受众,而这一版本面向具有不同需求的系统管理员。
我认为最终的目标是创建易读易懂的脚本,以便于维护。然后使用其他简单的脚本和 cron 作业来自动运行这些脚本。保持脚本名称的合理简短还可以减少从命令行执行这些脚本时的输入,但是当从另一个脚本或作为 cron 作业启动它们时,这几乎没有关系。
脚本和程序名称
dbu 这个程序名对你有什么意义吗?在你成为 Linux 极客之前,dd 这个程序名字对你来说有什么意义吗?这两个问题的答案可能都是否定的。虽然 dd 是一个常见的 GNU 实用程序“磁盘转储”,但 dbu 是我自己创建的一个 shell 脚本。这个名字对你来说毫无意义,但对我来说,它意味着大卫的备份。它很容易输入,一旦你知道它的意思,你就会记住它。
如果你研究了所有最初的 GNU 核心工具,你会发现它们的名字都很短——很多都是两三个字母。这很好,但是总共有成千上万的 Linux 命令,只有这么多有意义的简短组合。任何名称的一个属性应该是它与程序或脚本的目的有某种有意义的联系。
我们在第十一章中为脚本使用的名称 mymotd 稍长一些,但也比使用较短名称的情况更有意义。我们可以使用 davesmotd、dmotd、mmotd、davesMOTD、dMOTD 或任何其他相对有意义的名称。一些名字中的大写字母确实有助于更容易辨别脚本的功能,但是它们确实使在命令行上键入名字有点困难。
和我的“dbu”程序一样,仍然有一些空间用于非常短的名字。例如,有一个名为“mtr”的非常好的程序,它是旧的 traceroute 程序的交互式替代程序。mtr 程序维护一个活动的、连续的 traceroute,动态显示每一跳丢失的数据包数量,如果数据包由于某种原因被重新路由,还可以显示多条路由。非常有趣和有用。
mtr 计划最初被命名是因为一个叫 Matt Kimball 的人编写并维护了它。因此,这是“马特的跟踪路线。”马特停止支持后,罗杰·沃尔夫接手。它仍被命名为 mtr,但现在代表“我的追踪路线”
用非常短的名字命名脚本可能是一个挑战,因为许多现有的短字母组合已经被采用。在给一个脚本命名时,我总是试图做一些研究,以确保它不会对我电脑上已经安装的可执行文件造成问题。我通常用which命令做一个快速检查,如实验 12-1 所示。
实验 12-1
因为我们没有将 mymotd 脚本复制到任何标准的可执行路径位置,所以当我们使用下面的命令时,它应该不会出现。
[root@david ~]# which mymotd
/usr/bin/which: no mymotd in (/usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin)
which命令显示它在试图定位指定的可执行文件时搜索的路径。
不管它的完成状态如何,将 mymotd 脚本复制到/usr/local/bin,这是 Linux FHS 中存储本地创建的可执行文件的正确位置。然后再次运行which命令。
[root@testvm1 student]# cp mymotd /usr/local/bin
[root@testvm1 student]# which mymotd
/usr/local/bin/mymotd
我们已经确定当前没有安装可能导致我们为文件选择的名称出现问题的可执行文件,然后我们将可执行文件复制到适当的位置。
不是所有的冲突都会在你第一次写新剧本的时候出现。检查完安装的程序后,我还会进行谷歌搜索。我试图找到任何与我的程序有命名冲突的信息。较长的名字冲突的可能性要小得多,但是它们真的不需要太长。
我曾经遇到过这样一种情况,问题出现得很晚。我试图用yum,安装一个新程序,和它的继任者dnf一样,它是rpm命令的包装器。我收到一个错误,指出需要从新的 RPM 包中安装的一个文件与另一个包中同名的文件冲突。
这不是我的一个脚本,而是另一对冲突的 rpm。我能够通过删除已经安装的 RPM 来消除冲突,因为不再需要它了。
这种类型的冲突应该非常罕见。即使文件具有相同的名称(这首先是不太可能的),只要它们位于不同的目录中,它们在安装过程中就不会冲突。但是,如果它们都是可执行文件,并且位于PATH 中列出的第一个目录中的文件将由命令运行。要运行另一个,您需要使用完全限定的路径名来确保运行正确的程序。
像这样的潜在冲突,命名脚本可能有点棘手。我喜欢使用稍微长一点的脚本名,比如 4 到 8 个或 10 个字符,是为了防止命名冲突。我有时会在我的脚本名称中添加一个大写字母,以帮助它脱颖而出,并在一个长列表中帮助澄清名称。
这里的底线是名称应该容易记忆和有意义——对您和其他系统管理员来说,容易键入,并且容易在列表中找到。这些是我个人的标准。你可以有其他的,这是非常好的。请记住,其他系统管理员可能有一天需要与您的脚本所在的主机一起工作。
变量
当我在 1981 年购买我的第一台 IBM 个人电脑时,我订购了最大 64KB 的主板 1 ,而最便宜的型号只有 16K。这不是一个很大的工作空间。BASIC 包含在 PC 的一个板载 ROM 中,是当时许多学习编程的人的一个很好的选择。
因为空间有限,所以在用 BASIC 编写程序时节约内存是很重要的。我没有把它做得看起来很像缩进循环和子程序,因为每个制表符或空格占用了一个字节的内存,而这个内存可能是一些重要的东西所需要的。我尽可能保持变量名的简短。我通常使用单字母或双字符变量名,包括一个字母和一个数字,比如 A7。如果我们编写相当大的程序,注释是不存在的,因为没有空间容纳它们。我做这些都是为了节省内存。但是这使得我的程序很难阅读。
我希望我保存了一些我自己写的或者我不得不修改的由别人写的非常糟糕的代码。两者都有很多。
命名变量
我倾向于让我的脚本变量足够长,以表示它们的内容,就像你在第十一章的项目中看到的那样。这些变量名的长度从相当短到适中不等。所有这些都是为了让我自己和未来的脚本维护者更容易阅读和理解代码。
我的脚本中的变量名倾向于反映它们的内容。因此,您应该能够推断出名为$CPUArch 的变量可能包含有关 CPU 架构的信息。您可能不知道数据的确切类型,但是在查看该变量的内容时,您应该有一个大致的概念。对于该变量的值,您可能会看到类似“X86_64”或“64”的内容。至少在我的剧本里,是这样的。
需要记住的是,变量名只在编写和维护脚本时输入。该脚本可以根据需要运行多次,我永远不需要输入任何变量名。
让一切都成为变量
这是一个非常常见的最佳实践。即使您需要使用像圆周率、欧拉常数或与特定领域相关的常数这样的“常数”,也应该将它们声明为变量,然后在计算中使用该变量。当然,bash 本身只做整数运算,但是还有其他类型的变量。
我喜欢在脚本中使用变量作为路径和文件名。我还为将要打印的数据使用了变量,比如上一章中的 mymotd 脚本。正如我们在脚本中所做的那样,使用host、MotherboardModel 等变量可以更容易地阅读和理解功能。当我看到类似图 12-1 中的语句时,我立刻明白了代码应该完成什么——即使它与这里的上下文无关。我们期望找到的分配给变量的类型值是清楚的。
图 12-1
从变量名可以明显看出变量的期望内容
您应该能够从变量名中推断出一点代码是如何工作的。显然,dmidecode 实用程序用于获取有关主板的信息,而“type”2 是主板信息。它还告诉我们包含字符串“Name”的输出行包含我们正在寻找的特定信息。剩下的代码是提取包含模型信息的数据字符串,并清理它以供我们的脚本使用。
所以我们来试试这个小实验来说明。
实验 12-2
在一行中输入以下故意不正确的命令行程序。
[root@david ~]# MotherboardModel=`dmidecode -t 2 | grep Version | awk -F: '{print $2}' | sed -e "s/^ //"`;echo $MotherboardModel
Rev 1.xx
变量值显然是错误的,因为数据与变量名的预期值不匹配。这显然不是主板的型号,而是修订版号。
现在使用下面更正的代码。又是一行。
[root@david ~]# MotherboardModel=`dmidecode -t 2 | grep Name | awk -F: '{print $2}' | sed -e "s/^ //"`;echo $MotherboardModel
TUF X299 MARK 2
即使我们在运行代码之前不知道主板的确切型号,结果显然更适合变量。
即使变量在赋值后只使用一次,这样做也是有意义的。我多次发现,在后来的脚本维护中,我添加了更多也使用该变量的代码。这节省了我在脚本中第二次或第三次输入长路径名的时间。
例如,如果我的脚本中有客户发票的路径名,~/Documents/business/Customer/Invoices,那么很容易设置一个变量,比如$Invoices,并在我的脚本中使用它,而不是完整的路径。这使得在脚本中的其他地方引用该变量也很容易。通过不必再次键入长路径名,我还防止了路径名中可能的键入错误,这将导致执行期间的错误。
很多时候,我从多个变量构建路径名,因为我需要额外的灵活性,这也减少了输入。例如,我的 rsbu 备份程序每天为一组新的备份使用一个新的目录。这棵树的结构是这样的。
/-
|
\-path to backup media
|
\Backups
|
|--host1
| |--2018-01-01
| | \--data
| |--2018-01-02
| | \--data
| |--2018-01-03
| | \--data
| etc
--host2
| |--2018-01-01
| | \--data
| |--2018-01-02
| | \--data
| |--2018-01-03
| | \--data
etc etc
每天为每个主机添加一个新的日期子目录。因此,我们需要创建一系列变量,用在可以生成这个目录结构的代码中。实验 12-3 给出了实现这一点的一种方法。
实验 12-3
我们不需要为这个实验创建脚本。输入以下命令开始安装。这些变量一旦在命令行定义,就一直是环境的一部分,直到使用unset命令取消设置或设置为 null。
[student@testvm1 ~]$ BasePath="/media/Backup-Drive/Backups"
[student@testvm1 ~]$ BackupDate=`date +%Y-%m-%d`
[student@testvm1 ~]$ YesterdaysDate=`date -d "now-1days" "+%Y-%m-%d"`
现在验证我们刚刚设置的变量的值。
[student@testvm1 ~]$ echo $BasePath;echo $BackupDate;echo $YesterdaysDate;echo $HOSTNAME
/media/Backup-Drive/Backups
2018-01-22
2018-01-21
testvm1
注意$HOSTNAME 变量是一个 BASH 内置变量,所以我们不需要设置它。现在,为该主机设置主备份路径。我使用这个程序来备份多台主机,所以我将每台主机的备份保存在一个单独的目录中。这对远程主机不起作用,但在本实验中这是一个很好的快捷方式。
[student@testvm1 ~]$ BackupPath="$BasePath/$HOSTNAME/"
[student@testvm1 ~]$ echo $BackupPath
/media/Backup-Drive/Backups/testvm1/
要完成当前备份路径,只需添加今天的日期。
[student@testvm1 ~]$ TodaysBackupPath="$BackupPath$BackupDate"
[student@testvm1 ~]$ echo $TodaysBackupPath
/media/Backup-Drive/Backups/testvm1/2018-01-22
但是因为我使用 rsync 和它的一些最有趣的特性, 2 ,我还需要为昨天的备份生成路径。
[student@testvm1 ~]$ YesterdaysBackupPath="$BackupPath$YesterdaysDate"
[student@testvm1 ~]$ echo $YesterdaysBackupPath
/media/Backup-Drive/Backups/testvm1/2018-01-21
我已经为昨天的备份生成了路径,这样 rsync 就可以简单地创建从昨天的备份文件到今天的目录的硬链接,并且只对已经更改的文件执行备份。
我在脚本的不同部分使用了这一系列包含路径元素的变量,以便为多台主机生成多条路径。对$BasePath 变量的修改也可以用来挂载我用于备份的外部硬盘驱动器。
尽管变量名相当长,但在脚本中输入它们相当容易。这些名字便于理解每个变量的功能以及它们如何融入整体。毫无疑问,这些名字可以变得更短,仍然可以理解,但我喜欢这样。
当然,有一些边缘情况,这些代码没有直接处理,但是我在脚本中有更复杂的代码来处理这些情况。我不想用边缘情况来混淆基础知识,例如没有以前的备份时会发生什么,以及如何处理以前的备份存在但不是昨天备份的可能性。实验 12-3 中定义的许多变量也被用来帮助处理那些边缘情况。
程序
Bash 是一种命令行语言,支持过程的使用。脚本中的过程就像变量一样需要名字。我们在第十一章中创建的脚本包含几个过程,命名这些过程是为了提供对其功能的深入了解。
例如,Help()过程显然是为了打印帮助信息,而 GPL()过程打印 GPL 许可证声明。kb2gb()过程有点晦涩,但是只要稍微考虑一下,就应该清楚它是将千字节转换成千兆字节的。
主机
是的,主机——网络上的计算机——需要命名。大多数组织都有某种命名主机的惯例。我认识的大多数系统管理员都已经建立了某种约定,即使他们的组织并没有强制实施。
我工作过的一个地方使用主要的希腊和罗马神来命名他们的 Linux 服务器,而他们的 Unix 和 Linux 工作站接收次要神和神话人物的名字。其他地方用《星际迷航》或《??》或《星球大战》中的名字来称呼他们的东道主。
大多数系统管理员都有家庭网络,我们都有自己使用的某种命名约定。无论是基于游戏,神话中的神和人物,孩子和孙子,鸟,宠物,船,电影,国家,矿物,亚原子粒子,化学物质,科学家的名字,还是其他什么,我都见过许多不同的约定。本·科顿,我的技术评论员,使用了他追逐风暴的地方的城镇名称。
我用埃塞克斯级航空母舰的名字称呼我家庭网络中的大多数主机,以此向我的父亲致敬,二战期间他在太平洋上的邦克山号上。对于这样一个两个单词的名字,我只是把这些单词放在一起创造了“bunkerhill”按照惯例,主机名总是小写。稍加测试就会发现,互联网 DNS 系统在执行查找时会忽略大小写,但我确实喜欢按照惯例来处理这类事情。
组织命名
许多组织都有定义良好的命名约定,而其他组织则将这些细节留给系统管理员。
我工作过的一些组织有命名主机、其他网络节点、程序和脚本的惯例。我认为这有点矫枉过正,但是有一个广泛的、有良好文档记录的约定总比没有好。
我工作过的大多数地方都有网络主机和节点的命名约定,但是大多数低级别的命名(比如脚本)都留给了系统管理员。这是我喜欢的情况。大多数组织不需要如此详细地处理命名约定。
无论您在系统管理员级别有什么约定,都应该被很好地记录下来。
摘要
对于系统管理员来说,与 Linux 哲学的其他原则一样,在为文件、过程、脚本、变量和其他任何东西创建名称时,没有一种特定的“正确”方法。这真的是关于什么最适合你。除了对你来说有意义和有意义的东西之外,你不应该感到有任何压力。
命名的常识是这里真正的关键。我使用的主要标准是,“几年后当脚本需要维护时,这个名称对我或另一个系统管理员有意义吗?”
系统管理员在命名事物时使用常识有助于我们成为懒惰的系统管理员。易于阅读的代码比不易于阅读的代码需要更少的维护时间。在完全重写之前,必须维护写得很差的难以理解的代码,这耗费了大量的时间和精力,这些时间和精力本可以更好地用在其他地方。
Footnotes 1通过直接插入系统总线的附加板,可以为 PC 增加更多的内存。我后来添加了一个 256KB 的内存适配器,这是我用零件构建的。
2
下面是我写的一篇关于使用 rsync 进行备份的文章的链接: https://opensource.com/article/17/1/rsync-backup-linux
十三、以开放格式存储数据
我们使用计算机的原因是为了处理数据。它过去被称为“数据处理”是有原因的,这是一个准确的描述。尽管数据可能以视频和音频流、网络和无线流、文字处理数据、电子表格、图像等形式存在,但我们仍在处理数据。这仍然只是数据。
我们使用 Linux 中的工具来处理和操作文本数据流。这些数据通常需要存储,当需要存储数据时,以开放的文件格式存储总比以封闭的文件格式存储好。
尽管许多用户应用以 ASCII 格式存储数据,包括简单的平面 ASCII 和 XML,但本章是关于与 Linux 直接相关的配置数据和脚本。本章中我们要考虑的文件是关于系统配置的。
封闭是无法穿透的
早在 Windows 3.1 引入注册表 1 之前,大多数实用程序和应用都将其配置数据存储在注册表中。ini 文件。这些。ini 文件存储为 ASCII 文本,易于访问、阅读,甚至修改。只需要一个简单的文本编辑器就可以对这些进行修改。ini 配置文件。
注册表通过将配置数据存储在一个单一的、庞大的、难以理解的二进制数据文件中改变了这一切。虽然个别程序可以将配置数据存储在。ini 文件,注册表被吹捧为一种集中控制程序配置的方式,据说它的二进制格式比 ASCII 文本文件解析起来更快。
作为系统管理员,我们需要使用许多不同类型的数据。二进制格式本质上是晦涩难懂的,需要特殊的工具和知识来操作。有很多工具可以提供注册表查看和编辑功能。这些工具从所谓的免费软件到昂贵的商业程序都有。为了管理计算机,必须使用本身是封闭的特殊工具,这是向不可穿透性的进一步迈进。
所有这些问题的一部分是,这些工具的作者需要了解正在查看或编辑的注册表项的内容。没有来自专有软件供应商的内部知识,这些工具也是无用的。专有软件以二进制和专有格式存储配置数据的一个原因是为了对用户隐藏信息。
这一切都源于这些厂商所坚持的封闭和专有哲学。表面上看,这是为了保护用户不做“蠢事”,但这也是掩盖信息的好方法。
我试图在/etc 中找到一个二进制格式的 Linux 系统配置文件,但是没有找到。该目录中的数百个配置文件没有一个是二进制格式的。这确实是一件好事,但它让我没有一个二进制配置文件的示例,我可以用它来向您展示它是什么样子的。
二进制格式的一个问题是,没有理由创建我们在 Linux 中拥有的许多强大的工具。可以从二进制格式文件中生成的数据流没有一个可以用于 grep、awk、sed、cat、vim、emacs 等工具,或者我们在管理我们负责的系统时每天理所当然使用的数百种其他基于文本的工具。
开放是可知的
“开放源代码”是指代码,并使任何想要查看或修改它的人都可以使用源代码。“开放数据 2 ”讲的是数据本身的开放性。
术语“开放数据”不仅仅意味着可以访问数据本身,还意味着数据可以被查看、以某种方式使用以及与他人共享。实现这些目标的确切方式可能取决于某种归属和开放许可。与开放源码软件一样,这种许可旨在确保数据的持续开放可用性,而不是以任何方式对其进行限制。
开放数据是可知的。这意味着对它的访问是不受限制的。真正开放的数据可以自由阅读和理解,无需进一步解释或解密。在系统管理员的世界里,开放意味着我们用来配置、监控和管理我们的 Linux 主机的数据很容易被找到、读取和修改。它以易于访问的格式存储,如 ASCII 文本。当一个系统是开放的时,数据和软件都可以通过开放工具来管理——处理 ASCII 文本的工具。
平面 ASCII 文本
平面文本文件是开放和可知的。程序和系统管理员都很容易阅读它们,所以很容易看出什么时候工作,什么时候不工作。大多数 Linux 配置文件都是简单的平面 ASCII 文本文件,这使得它们易于使用我们已经掌握的简单 Linux 文本操作工具进行查看和修改。
所以我们可以使用cat和less来查看 Linux 配置文件,使用grep来提取和查看包含指定字符串的行。我们可以使用 vi、vim、emacs 或任何其他文本编辑器来修改 ASCII 文本格式的配置文件。
在我的一项工作中——我们使用 Perl CGI 脚本来管理电子邮件系统——我们使用纯文本文件来存储我们所有的数据。这些数据包括部门信息,例如谁被授权访问该部门的数据。它还包含每个部门的电子邮件用户的 ID 和登录信息。
我们编写了一些 Perl 程序来管理对这些数据的访问,这既适用于作为总体电子邮件系统管理员的我们,也适用于部门管理员。数据仍然是平面的 ASCII 文本文件,因此我们可以使用基本的 Linux 命令行工具来访问和修改数据,尤其是在对文件进行大规模更改时。同时,我们还能够使用基于 web 的 Perl CGI 脚本来处理个人和部门记录。
我们确实考虑过使用 MySQL 进行记录管理,但我们认为 ACII 文件更容易访问。我们的一个系统管理员在大约一周的时间里编写了一系列 Perl 脚本,允许我们在 Perl 脚本中使用类似 SQL 的函数调用,因此我们拥有了两个世界的优点。
系统配置文件
大多数系统范围的配置文件位于/etc 目录及其子目录中。/etc 中的文件提供了许多系统服务和服务器的配置数据,例如电子邮件(SMTP、POP、IMAP)、web (HTTP)、时间(NTP 或 chrony)、SSH、网络适配器和路由、GRUB 引导加载程序、显示屏和打印机配置等等。
您还可以找到提供影响所有用户的系统级配置的配置文件,例如/etc/bashrc。/etc/bashrc 文件在所有用户打开 bash shell 时为他们提供初始设置和配置。图 13-1 显示了我的 Fedora VM 上/etc/bashrc 文件的内容。
图 13-1
/etc/bashrc 文件为所有打开的 bash shell 会话提供配置
放松——我们不会检查图 13-1 中/etc/bashrc 文件的每一行。但是,在这个文件中我们应该注意一些事情。
首先,看看所有的评论。这个文件是供用户阅读的。我们系统管理员毕竟是高级用户。我喜欢基于 Red Hat 的发行版的一点是大多数配置文件和脚本都有很好的注释。
这个脚本的功能之一是设置 shell 命令提示符。该脚本确定 shell 是标准 xterm 还是 vte 终端会话,或者它是否在屏幕会话中。它根据不同的条件设置不同的提示字符串。它还使用诸如/etc/sys config/bash-prompt-xterm 之类的外部文件,这些文件将提示符配置包含在一个文件和位置中,很容易由系统管理员进行管理。
靠近文件顶部是一系列注释,简要描述了脚本的功能,并告诫不要更改这个特定的文件。注释也告诉你你自己的修改应该去哪里。我们将进一步研究这个问题。
注意缩进是如何使这个脚本片段的结构比所有东西都挤在左边更容易阅读。
我们经过的时候你看到了吗?这个配置文件是一个可执行程序。这是一个 bash 脚本,它包含的程序逻辑可以根据外部条件决定采用哪条执行路径。这个脚本本身并不完整;它实际上是一个片段,可以在必要时来源-导入-到其他脚本中。
Sourcing 是一种 bash shell 方法,用于将其他 bash 脚本或片段的内容包含到脚本中。这允许多个脚本使用所获取的片段内容。你可以把它想象成编译程序使用的函数库。源文件被加载到调用脚本中的源命令位置。然后立即执行。
可以使用 source 命令来完成源操作。句号(。)是源命令的别名。这如图 13-2 所示,是图 13-1 中的一段代码。
图 13-2
这段代码片段提供了*。sh 文件位于/etc/profile.d。该目录中的其他文件将被忽略
图 13-2 中突出显示的代码来自所有的*。将/etc/profile.d 中的 sh 文件复制到这个代码片段中。
那么图 13-1 中的程序片段是如何执行的呢?哪里是代码或触发器导入-源-这个代码到它,所以它可以被执行。好问题。图 13-3 中的/etc/profile 脚本来源于/etc/bashrc 文件。
图 13-3
/etc/profile 脚本在启动时为系统上的所有 shells 设置全局环境。它还在/etc/profile.d 和/etc/bashrc 中提供 bash 脚本片段。
/etc/profile 文件也是一个脚本片段。我们可以在这里花一些时间来定位/etc/profile 启动的方式,但是这将把我们引向错误的方向。可以说,当从 bash 本身调用时,它是作为登录 shell 被调用的,它首先读取/etc/profile(如果存在),然后读取~/。bash_profile,/。bash_login 和/。概要文件,按此顺序排列(如果存在的话 3 )。
全局 Bash 配置
现在,让我们对 bash 进行一些全局配置更改。
/etc/bashrc 文件提到了/etc/profile.d 目录。让我们看看实验 13-1 中的目录及其文件。同时,我们将添加一些我们自己的全局 bash 配置。
实验 13-1
这个实验应该以 root 用户身份进行。我们的目标是对 bash shell 的全局配置进行一些补充。
制作/etc/profile . d PWD 并列出内容。
[root@testvm1 ~]# cd /etc/profile.d/ ; ls -l
total 100
-rw-r--r--. 1 root root 664 Jul 25 2017 bash_completion.sh
-rw-r--r--. 1 root root 196 Aug 3 04:18 colorgrep.csh
-rw-r--r--. 1 root root 201 Aug 3 04:18 colorgrep.sh
-rw-r--r--. 1 root root 1741 Nov 10 12:53 colorls.csh
-rw-r--r--. 1 root root 1606 Nov 10 12:53 colorls.sh
-rw-r--r--. 1 root root 69 Aug 4 19:53 colorsysstat.csh
-rw-r--r--. 1 root root 56 Aug 4 19:53 colorsysstat.sh
-rw-r--r--. 1 root root 162 Aug 5 02:00 colorxzgrep.csh
-rw-r--r--. 1 root root 183 Aug 5 02:00 colorxzgrep.sh
-rw-r--r--. 1 root root 216 Aug 3 04:57 colorzgrep.csh
-rw-r--r--. 1 root root 220 Aug 3 04:57 colorzgrep.sh
-rwxr-xr-x. 1 root root 249 Sep 21 03:40 kde.csh
-rwxr-xr-x. 1 root root 288 Sep 21 03:40 kde.sh
-rw-r--r--. 1 root root 1706 Jan 2 10:36 lang.csh
-rw-r--r--. 1 root root 2703 Jan 2 10:36 lang.sh
-rw-r--r--. 1 root root 500 Aug 3 11:02 less.csh
-rw-r--r--. 1 root root 253 Aug 3 11:02 less.sh
-rwxr-xr-x. 1 root root 49 Aug 3 21:06 mc.csh
-rwxr-xr-x. 1 root root 153 Aug 3 21:06 mc.sh
-rw-r--r--. 1 root root 106 Jan 2 07:21 vim.csh
-rw-r--r--. 1 root root 248 Jan 2 07:21 vim.sh
-rw-r--r--. 1 root root 2092 Nov 2 10:21 vte.sh
-rw-r--r--. 1 root root 120 Aug 4 23:29 which2.csh
-rw-r--r--. 1 root root 157 Aug 4 23:29 which2.sh
所有带*的文件。sh 扩展由/etc/bashrc 或。etc/个人资料。不执行带有其他扩展名的文件。我们将通过在这个目录中创建一个新文件来添加 bash 配置。
使用您喜欢的编辑器在这个目录中创建一个名为“mybash.sh”的新文件。将以下内容添加到文件中。
################################################################
# The following are global changes to BASH configuration #
################################################################
alias lsn='ls --color=no'
alias vim='vim -c "colorscheme desert" '
TestVariable="Hello World"
set -o vi
在测试之前,让我们确保别名还没有出现,并且 TestVariable 为 null。
[root@testvm1 profile.d]# alias
alias cp='cp -i'
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l.='ls -d .* --color=auto'
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
alias mc='. /usr/libexec/mc/mc-wrapper.sh'
alias mv='mv -i'
alias rm='rm -i'
alias which='(alias; declare -f) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot'
alias xzegrep='xzegrep --color=auto'
alias xzfgrep='xzfgrep --color=auto'
alias xzgrep='xzgrep --color=auto'
alias zegrep='zegrep --color=auto'
alias zfgrep='zfgrep --color=auto'
alias zgrep='zgrep --color=auto'
[root@testvm1 profile.d]# echo $TestVariable
[root@testvm1 profile.d]#
现在测试结果。这一更改不会影响已经打开的 bash 会话。新的会话将反映这些更改。所以打开一个新的终端会话。作为学生用户,运行以下命令来验证结果。
[root@testvm1 profile.d]# echo $TestVariable
Hello World
[root@testvm1 profile.d]# alias
alias cp='cp -i'
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l.='ls -d .* --color=auto'
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
alias lsn='ls --color=no'
alias mc='. /usr/libexec/mc/mc-wrapper.sh'
alias mv='mv -i'
alias rm='rm -i'
alias vim='vim -c "colorscheme desert" '
alias which='(alias; declare -f) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot'
alias xzegrep='xzegrep --color=auto'
alias xzfgrep='xzfgrep --color=auto'
alias xzgrep='xzgrep --color=auto'
alias zegrep='zegrep --color=auto'
alias zfgrep='zfgrep --color=auto'
alias zgrep='zgrep --color=auto
正如这个实验所示,对 ASCII 文件进行修改是很容易的。请注意,不需要重启就可以使这些更改生效——它们会立即对新的 bash 终端会话生效。
用户配置文件
让我们看看您自己的主目录中所谓的隐藏文件——那些名称以句点(.).这些是用户特定的配置文件,您可以根据自己的需要和偏好进行更改。让我们看看。bashrc 文件,这是一个配置文件,单个用户可以在其中设置他们自己的 bash 配置,比如别名、函数和他们独有的环境变量。
实验 13-2
以学生用户的身份执行此实验。
那个。bashrc 文件很短,所以我们可以用cat查看它。让我们确保我们位于学生用户的主目录中,然后显示该文件。
[student@testvm1 ~]$ cd ; cat .bashrc
# .bashrc
# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
# Uncomment the following line if you don't like systemctl's auto-paging feature:
# export SYSTEMD_PAGER=
# User specific aliases and functions
这个文件也有很好的注释,甚至告诉我们在哪里添加我们自己的配置。因此,让我们添加一些无关紧要的东西来测试这个本地配置。使用您最喜欢的编辑器将下面一行添加到文件的末尾。
StudentVariable="This is a local variable."
查看变量。
[student@testvm1 ~]$ echo $StudentVariable
[student@testvm1 ~]$
变量尚未添加到环境中。从现在开始,它将成为 bash 终端会话环境的一部分。可以将它添加到现有的 bash 终端会话中。bashrc 文件是这样的。
[student@testvm1 ~]$ . .bashrc
[student@testvm1 ~]$ echo $StudentVariable
This is a local variable.
这些都是微不足道的例子,但是它们应该让您对开放格式配置文件的灵活性有所了解。遵循文件的逻辑很容易,并且在需要时修改它们也很容易。尽管每个发行版在如何向这些文件添加注释方面有所不同,但我使用的所有发行版都在注释中包含了足够的信息,使我能够找出适当的位置来修改配置。它们还包含了足够的信息,让我能够理解其中的逻辑。这并不意味着我不需要努力去理解它,但是如果我需要或者只是好奇的话,我可以做到。
请注意,本地用户 bash 配置会覆盖全局配置。因此,如果用户了解全局配置参数,并想为自己修改它,他们可以通过在~/中设置它来实现。bashrc 文件。
ASCII 岩石
现在我们可以看到,通过使用 ASCII 文本文件进行配置所创建的开放性是如何允许我们探索和理解我们的 Linux 操作系统的许多进程的。ASCII 是配置文件和 shell 脚本的首选格式。
许多系统级可执行文件也是设置配置和启动二进制文件的 bash 脚本。让我们检查一下/bin 目录来验证这一点。
实验十三-三
以 root 用户身份执行此实验。
制作/bin PWD 并计算文件的数量,看看总共有多少个可执行文件。
[root@testvm1 ~]# cd /bin/ ; ls | wc -l
2605
我们来算算有多少是 ASCII 文本文件。
[root@testvm1 bin]# for I in `ls`;do file $I;done | grep ASCII | wc -l
355
/bin 中超过 13%的可执行文件是 ASCII shell 脚本。现在查看 ASCII 脚本文件列表。你的主人给出的具体结果几乎肯定会和我的不同。
[root@testvm1 bin]# for I in `ls`;do file $I;done | grep ASCII | less
我不会在这里列出这些文件,但是您应该浏览一下,看看有什么。
现在让我们来看看其中的一个脚本。我选择了 ps2ascii 脚本,它被用作 ghostscript 程序的包装器。
注意如果您使用的主机没有安装 ps2ascii 程序,您可以安装它或者选择一个不同的 ascii 文件来完成本实验的其余部分。
[root@testvm1 bin]# cat ps2ascii
#!/bin/sh
# Extract ASCII text from a PostScript file. Usage:
# ps2ascii [infile.ps [outfile.txt]]
# If outfile is omitted, output goes to stdout.
# If both infile and outfile are omitted, ps2ascii acts as a filter,
# reading from stdin and writing on stdout.
# This definition is changed on install to match the
# executable name set in the makefile
GS_EXECUTABLE=gs
trap "rm -f _temp_.err _temp_.out" 0 1 2 15
OPTIONS="-q -dSAFER -sDEVICE=txtwrite"
if ( test $# -eq 0 ) then
$GS_EXECUTABLE $OPTIONS -o - -
elif ( test $# -eq 1 ) then
$GS_EXECUTABLE $OPTIONS -o - "$1"
else
$GS_EXECUTABLE $OPTIONS -o "$2" "$1"
fi
ghostscript 程序通过从原件中提取文本,将 Postscript 和 PDF 文件转换为 ASCII 文本文件。这个包装器有注释告诉我们程序做什么。它设置一些变量,然后用不同条件的选项运行程序。
像 ps2ascii 这样的脚本在启动程序时允许很大的灵活性。它们使用户的生活变得更容易,因为脚本可以管理设置传递给主程序的选项和参数的任务。
最后的想法
Linux 中的开放数据使我们作为系统管理员能够探索一切,以满足我们对 Linux 如何工作的好奇心。使用 ASCII 文本文件作为脚本和配置文件允许我们访问我们每天工作的环境的内部工作方式。
我们能够利用这种开放性来跟踪一些相关的 bash 配置程序和文件。我们发现了如何做出全球性和地方性的改变。我们添加了一些我们自己的配置,所以 bash 现在的配置更符合我们的喜好。
而且,如果我们想要或者需要,我们可以下载用于编译内核可执行代码的源代码,以及我们的 Linux 发行版中提供的所有开源程序和实用程序。我已经这样做了几次,因为我想知道更多。如果你的好奇心带你去那里,你也可以。
所有这一切只有在开放的操作系统中才有可能。
Footnotes 1维基百科,Windows 注册表, https://en.wikipedia.org/wiki/Windows_Registry
2
维基百科,打开数据, https://en.wikipedia.org/wiki/Open_data
3
请参见 bash 手册页了解这一点以及更多详细信息。
十四、为数据使用单独的文件系统
这个特别的原则有很多内容,它需要理解 Linux 文件系统和挂载点的本质。如果你跳过了第六章“使用 Linux FHS”,你现在应该回去读一读。
注意
本章中术语“文件系统”的主要含义是位于独立分区或逻辑卷上的目录树的一部分,该分区或逻辑卷必须安装在根文件系统的指定安装点上,以便能够对其进行访问。我们还使用这个术语来描述分区或卷上的元数据结构,如 EXT4、XFS 或其他结构。这些不同的用法应该从它们的上下文中弄清楚。
为什么我们需要独立的文件系统
在我们的 Linux 主机上维护独立的文件系统至少有三个很好的理由。首先,当硬盘崩溃时,我们可能会丢失损坏的文件系统上的部分或全部数据,但是,正如我们将看到的,崩溃的硬盘上的其他文件系统上的数据仍然可以挽救。
其次,尽管可以访问大量的硬盘空间,文件系统还是有可能被填满。发生这种情况时,单独的文件系统可以最大限度地减少直接影响,并使恢复更容易。
第三,当某些文件系统(如/home)位于不同的文件系统上时,升级会变得更容易。这使得升级变得容易,而不需要从备份中恢复数据。
在我的职业生涯中,我经常遇到这三种情况。在某些情况下,只有一个分区,因此恢复相当困难。当主机配置了单独的文件系统时,从这些情况中恢复总是更容易、更快。
保证所有类型数据的安全是系统管理员工作的一部分。使用单独的文件系统来存储数据可以帮助我们实现这一点。这种做法也可以帮助我们实现成为一个懒惰的管理员的目标。备份确实允许我们恢复在崩溃场景中可能丢失的大部分数据,但是使用单独的文件系统可能允许我们恢复到崩溃时刻的所有数据。从备份恢复需要更长的时间。
硬盘崩溃
你的电脑硬盘是否崩溃过,导致你的 Linux 电脑无法启动;崩溃的硬盘上的所有数据;没有最近的备份?我们大多数人都经历过。我们的朋友、同事或顾客也都经历过这种情况。
拥有独立的文件系统使得在某些(但不是全部)硬盘崩溃的情况下从潜在的未受影响的文件系统中恢复数据成为可能。当单个文件系统用于整个目录树时,任何类型的硬盘崩溃都极有可能导致所有数据丢失。
不幸的是,有些硬盘故障模式会导致硬盘无法正常工作,硬盘上的所有数据都会丢失。
完整的文件系统
尽管有大量的硬盘空间可供我们使用,文件系统还是会被填满。失控的程序会很快填满文件系统。如果只有一个文件系统,主机很可能会崩溃,许多有价值的数据将会丢失。
我见过文件系统瞬间填满。在只有一个文件系统的主机中,结果可能是灾难性的。具体症状可能各不相同,从用户无法创建新文件、保存修改过的文件或登录桌面,到主机完全无响应甚至无法通过 SSH 远程访问。在某些情况下,重新获得控制的唯一方法是关闭系统并引导至恢复模式。然后,就有可能找到并删除填充磁盘的文件,并尝试了解是什么导致了这种情况。我遇到的最糟糕的情况是为这本书测试一个虚拟机,这个虚拟机不会终止。
在配置了独立文件系统的主机中,填充一个文件系统的任何影响都将被最小化,并且症状的破坏性可能会更小。从这种状况中恢复通常会更快更容易。
笔记本哀叹
这发生在今天。
今天——是的,真的,就在我 2017 年 12 月 26 日写这篇文章的今天——我的一个朋友给我发了一条短信,告诉我我一直为她提供支持的笔记本电脑无法启动。
几年前,我的朋友辛迪,也是我的瑜伽教练,变得不开心,因为她的电脑由于恶意软件的侵扰而不断变慢。她请我帮忙,我同意了。我给她讲了 Linux,她决定试试。
这些年来,我帮助辛迪通过硬件维修、软件更新和升级到 Fedora 的新版本来保持她的计算机运行,这也是我让她起步的地方。当她遇到自己无法解决的电脑问题时,她总是先打电话给我。今天也不例外。
第一个文本是在我写这本书的第八章时出现的。它说她的电脑无法启动,并在屏幕上打印出一系列重复的信息。经过几次短信交流,我决定我应该看看笔记本电脑,她把它带到了我的家庭办公室。根据/dev/sda 上显示一长串 I/O 错误的消息,我确定硬盘出现了故障。我还告诉她,我不确定是否能从硬盘中恢复数据,但我会尽力而为。
此时,我检查了她的外部 USB 备份驱动器,并确定最近的备份是几个月前的。那部分是我的错,我已经修好了。我们讨论了情况,她告诉我尽我所能。在她离开后,我从笔记本电脑中取出 320GB 硬盘,并将其插入我的硬盘扩展坞的一个插槽中进行探索。
这是我已经知道的。当我在笔记本电脑上安装 Linux 时,我为/usr、/var、/tmp、/swap 和/home 使用了单独的文件系统,就像我安装 Linux 时经常做的那样。这意味着/home 文件系统与根(/)文件系统位于不同的逻辑卷上。因为问题发生在启动过程中——从技术上讲是在启动之后,在 systemd 启动各种服务的过程中——很有可能其他逻辑卷没有受到影响,包括我安装了她的主目录/home 的卷。
硬盘启动后,我使用lvscan工具定位我的主工作站上的所有逻辑卷。结果包括故障硬盘上的逻辑卷,如图 14-1 所示。
图 14-1
lvscan 显示逻辑卷,包括故障硬盘上的逻辑卷
lvscan命令的结果还列出了分配给笔记本电脑硬盘上逻辑卷的设备文件。这些设备文件是由 udev 1 在硬盘达到速度并可以读取时创建的。正如我在第五章“一切都是文件”中提到的,udev 负责检测新设备何时插入系统,并在/dev 中为它们创建设备文件。
在列出的逻辑卷中有/dev/fedora_mobilemantra/home,这是我朋友的硬盘的主目录。此时,我知道了在我的工作站上的/mnt 上挂载主目录所需要的全部内容。我这样做了,并开始探索主目录。一切看起来都很好,我可以毫无问题地阅读几个文件。
注意
我很幸运,硬盘没有发生灾难性的故障。在这种情况下,错误显然是/(根)分区上的坏扇区,这些坏扇区使其他分区保持完整,因此它们可以被恢复。
创建主目录的备份并发现其中的许多文件是否有错误的最简单方法是使用 tar 命令。在创建 tarball 的过程中没有发生错误,所以我能够从主目录中检索所有数据。
当我打电话告诉辛迪,她的数据将继续以数字形式存在时,她非常高兴。
我使用了dd命令,如图 14-2 所示,对整个硬盘进行了快速测试。在总共 320GB 中仅读取 115GB 后,出现 I/O 错误。我本来可以继续关注错误的位置,但是现在知道错误导致了启动问题就足够了,这表明它们位于/(根)文件系统中。
图 14-2
使用 dd 命令测试硬盘。这也显示了发生的 I/O 错误
将“outfile”数据发送到/dev/null 可以防止它在终端会话中显示为 STDOUT。向终端显示 STDOUT 也会显著降低整个过程的速度。但是,任何指示 I/O 错误的 STDERR 消息仍会显示在终端上。运行此命令时,硬盘或其任何分区都未安装。
我为她的笔记本电脑订购了一个新的硬盘并安装了它。然后,我安装了 Fedora 27,这是目前最新的版本,并将保存的数据恢复到她的主目录中。一切正常,她的所有数据都被证明是完整的。
这个故事很好地说明了为什么要为数据使用单独的文件系统。它还很好地展示了理解 Linux 文件系统的层次结构对我们这些系统管理员来说是很重要的。它显示了/mnt 挂载点和/dev/null 设备的正确用法。这也是一个很好的例子,说明了一切都是一个文件,而/dev 中的设备专用文件可以通过简单的工具来使用。
数据安全
这一原则与数据安全和其他任何东西一样重要。在这种情况下,我指的是维护数据的持续存在和完整性意义上的安全性。今天的硬盘是巨大的,有些达到了数 TB。硬盘是电脑中最常见的故障设备之一,还有其他带有机械运动部件的设备,如风扇。因此,硬盘越大,发生故障时丢失的数据就越多。
当然,保护数据安全的一部分就是备份。另一个非常重要的部分是确保数据(实际数据,如文档、项目文件、财务文件、图形、视频、音频、用户配置文件等)尽可能地安全,以免被破坏。因为备用系统也会失灵。
根据 Linux 文件系统分层标准,“破坏根文件系统上的数据的磁盘错误是比任何其他分区上的错误更大的问题。小的根文件系统不容易因为系统崩溃而损坏。 2 “这背后的原因是,在大多数系统中,最大数量的磁盘写入发生在根分区中,因此它最有可能被问题破坏。前一个例子似乎就是这种情况。
由此得出的推论是,作为根文件系统一部分的目录比不是根文件系统一部分的目录更容易受到这些系统崩溃的副作用的影响。这是显而易见的,因为我无法挂载根文件系统,所以无法恢复上面的任何文件。我能够挂载/home 文件系统。
这使我们得出结论,为包含用户数据的目录树维护单独的文件系统是一个好主意。它还强化了上面引用的语句,即根文件系统应该尽可能的小。我的主工作站的根文件系统中的已用空间量只有 444MB,这不是很多。尽管如此,考虑到当前硬盘驱动器的巨大容量,我还是建议为根文件系统分配大约 5GB 的磁盘空间,以备不时之需。
推荐
我建议将 Linux 目录树的一些特定部分放在单独的文件系统中。有时,我甚至建议将它们放在单独的硬盘上,以进一步确保它们的安全并便于恢复,因为如果需要替换包含根文件系统的驱动器,则在单独驱动器上维护的文件系统上的数据不需要从备份中恢复。
作为重新安装的目录树的一部分,挂载预先存在的文件系统所需要做的就是在/etc/fstab 文件中添加适当的条目。这样,在新驱动器中安装操作系统后,当系统重新启动时,就可以挂载它们。这种配置可以在 Linux 安装期间使用“定制”磁盘配置来完成。该过程的细节超出了本书的范围。
当前基于 Red Hat 的安装(如 Fedora 和 CentOS)使用的默认磁盘配置可能远非理想。CentOS 7 将除/boot 和用于交换的单独卷之外的所有内容都放在根(/)分区中。
Fedora 27 将 1GB 放在/boot 中,50GB 放在/root 中,几 GB 放在 swap 中——实际容量取决于系统中的 RAM 容量,其余的放在/home 中。在我的测试虚拟机上,这是 195GB。对于我使用的大多数系统来说,这实在是太多了。我通过大量的实验发现,实际数字会随着硬盘的大小而变化。但结果还是一样。
这些默认值可能会导致磁盘使用率达不到最佳水平,并在主机生命周期的后期导致问题。尽管在这些默认安装中,/home 是一个独立的文件系统,但它的大小对于许多环境来说都太大了。例如,我的主工作站上的主目录只有大约 30GB 的数据,包括这本书和许多照片,都可以追溯到 20 多年前。还有其他需要考虑的文件系统。
Linux 文件系统层次结构的三个主要分支专门设计为位于单独的分区或卷上,作为单独的文件系统。 3 这是可能的,因为符合 Linux FHS 使它如此。这三个分支是/usr、/opt 和/var。我还发现了其他一些分支,它们作为独立的文件系统/home、/tmp 和/opt 工作得很好。
让我们看看目录树的分支,我推荐它们是独立的文件系统。所有这些文件系统都可以放在一个或多个独立于包含根文件系统的硬盘上。这有助于确保不在故障驱动器上的目录分支的整体生存能力。
你可以回头参考表 6-1 对 Linux FHS 的这些分支进行简单的描述,也可以参考文件系统层次标准 V3.0 4 。
/boot
/boot 目录树很有趣,因为它不能是逻辑卷配置的一部分。它必须是一个独立的磁盘分区,具有 Linux EXT2、EXT3、EXT4、VFAT 或 XFS 文件系统。这些是当前版本的 Fedora、CentOS6 和 CentOS7 安装程序 Anaconda 支持的唯一文件系统。
因为大多数现代发行版使用逻辑卷管理,所以这个目录必须是一个单独的文件系统。如果是这样,你就没得选了。但是,如果您不使用逻辑卷管理,而是使用直接在原始分区上创建的文件系统,比如 EXT4,那么您可以将/boot 作为根(/)文件系统的一部分。
我建议/boot 始终是一个单独的文件系统,即使主机上的其他文件系统不使用逻辑卷。
/home
显然/home 应该是一个单独的文件系统。我上面讨论的经验,以及这些年来其他类似的经验,让我非常清楚/home 应该永远是一个独立于目录树其余部分的文件系统。我所熟悉的发行版的默认文件系统配置表明,除了任何大小问题之外,这是一个最佳实践。
例如,当从 Fedora 的一个版本升级到下一个版本时,将我的数据(尤其是/home)放在一个单独的文件系统上使得版本级升级变得很容易,即使我选择不使用提供的升级工具,而只是在旧版本上安装新版本的 Fedora。这需要与从根文件系统损坏中恢复相同的过程;有必要执行文件系统的定制配置,并选择保留现有的主目录而不进行格式化。
/home 也是我建议放在独立于操作系统的硬盘上的文件系统之一。这有助于确保主驱动器出现故障时/home 上的数据是安全的。它还通过将磁盘访问分散到多个硬盘来提高性能,这样操作系统就不必等待用户数据访问,反之亦然。
尽管 FHS 将/home 指定为“主”目录,但它也认识到主目录在目录树中的位置通常取决于组织的标准和管理系统的系统管理员的判断。我遇到过/var/home、/opt/home、/homedirs 等。我喜欢遵循这个标准。
将主目录作为一个单独的文件系统,可以在需要时将它移动到不同的挂载点。为了实现这一点,可能还需要更改其他内容,比如默认路径。
root 用户的主目录是/root,这个目录应该是根文件系统的一部分。这样做的原因是为了确保为了方便起见,我们存储在根主目录中的工具和文件在不挂载其他文件系统的恢复模式和运行级别中仍然可用。
许多与系统服务相关的非登录帐户在其他位置也有主目录。具体位置因服务而异,但通常是作为服务本身一部分的目录。
/usr
/usr 分支包含不重要的命令,即用户命令,而不是在引导和启动过程中或在恢复模式下运行时需要的系统管理命令。
虽然前面的陈述是在 FHS 文档中发现的,但在 Fedora 和 CentOS 7 上严格来说不再是真实的。为了简化文件系统层次结构,这些发行版在/bin、/sbin、/lib 和/lib64 中使用符号链接来挂载点。/bin 目录是指向/usr/bin 的符号链接,/sbin 是指向/usr/sbin 的符号链接,/lib 是指向/usr/lib 的符号链接,/lib64 是指向/usr/lib64 的符号链接。
引导所需的文件现在是 Linux 初始 RAM 文件系统 initramfs 的一部分。 5 所以现在是符号链接的目录在引导时不再需要可用。
我通常将/usr 作为一个单独的文件系统,以防止它作为根文件系统的一部分出现问题,同时确保这里数据的安全性。大多数用户级命令和库都位于这里。这被认为是一个包含静态文件的目录树,这些文件在主机运行过程中通常不会改变。
这种树被称为“二级层次”的原因之一是,在许多方面,它的结构类似于从根目录开始的主树。有一个用于本地文件的子目录树/usr/local。
/usr/local 目录树包含子目录 etc、bin、sbin、include、lib、lib64、share 等等。/usr/local 树的目的是存储本地程序和配置文件。
这是我放置所有本地编写的脚本和它们需要的任何配置文件的地方。脚本本身位于/usr/local/bin 中,配置文件位于/usr/local/etc 中。为这些脚本编写的文档(如手册页)位于/usr/local/share 中。
另一种选择是将/usr 作为根文件系统的一部分,但是将/usr/local 作为一个单独的文件系统。反正我只备份/usr/local,因为我从来不对整个系统进行裸机恢复,所以这很有意义。如果我的操作系统或安装它的硬盘有问题,那就需要完全重新安装,除了/usr/local 树之外的所有内容都会在安装过程中重新创建。
/usr 和/usr/local 也可以是单独的文件系统。/usr 文件系统将挂载在/usr 挂载点上,然后本地分支将挂载在/usr/local 挂载点上。
/user 树显然不适合大型程序,如商业软件或大型开源应用。它适用于小型到中等规模的本地编码程序,以满足系统管理员或本地普通用户的需求。
大型软件应用应安装在另一个位置。建议使用/opt 目录树。
/opt
大型程序应该安装在/opt 目录树中。这个目录应该创建为一个单独的文件系统,以便在必要时可以轻松地扩展它的大小。
/opt 分支支持多个供应商安装其软件的完整子目录层次结构,以及为本地系统管理员使用而保留的完整目录集/opt/bin、/opt/doc、/opt/include、/opt/info、/opt/lib 和/opt/man。 6
/var
Linux 文件系统层次结构的/var 分支是一个有趣的组合——嗯,东西。它旨在包含“可变”数据——可以改变的数据,但不是配置数据。因为在任何类型的恢复或维护模式下,或者在初始引导过程中,操作系统都不需要/var 中的数据,所以可以将它安全地创建为一个单独的文件系统。
位于/var 中的数据是用户数据和数据库。我们在/var 中找到许多不同类型的数据。例如,如果主机是 web 服务器,则/www 将包含网站所需的文件。事实上,我的 web 服务器上运行着多个站点,每个站点都有自己的目录,比如/var/wwwboth、/var/wwwlinuxdatabook 等等。这使得确定哪个目录分支包含每个网站的数据变得容易。完成这项工作所需的唯一配置是在网站配置文件中。我使用 Apache web 服务器,所以应该是/etc/httpd/conf/httpd.conf。
Maria db——MySQL 的一个分支——在/var/lib/mysql 中维护它的数据库。SendMail 将用户收件箱存储在/var/spool/mail 中。BIND 为位于/var/named 中的数据库提供名称服务。那里也存储了更多的数据。
/tmp
/tmp 目录是用户和服务可以临时存储文件的地方。把/tmp 想象成一个任何类型的数据都可以被任何用户或进程临时存储的地方。存储在/tmp 中的文件很有可能会被删除,通常是在下次引导时。
我使用/tmp 下载大文件,比如 ISO 映像,比如各种发行版的安装映像。这些文件可能非常大,加上各种系统进程创建的文件,可以填满一个小的/tmp 文件系统。因此,我喜欢让我的/tmp 文件系统非常大,通常是 10GB 或更多。对于我的主工作站,我目前有 30GB 分配给/tmp。在多 TB 硬盘的今天,30GB 是一个非常合理的大小。
如果/tmp 文件系统被填满,就会发生奇怪的事情。我在第六章“使用 Linux FHS”中提到了当我设法填满/tmp 文件系统时出现的问题。GUI 桌面登录失败,因为桌面无法在/tmp 中创建新文件,但是控制台和远程 SSH 登录继续工作。
然而,如果/tmp 是根文件系统的一部分,问题会更严重。在这种情况下,可能会出现许多其他症状,因为各种附加服务无法找到足够的磁盘空间来工作。
其他分支
Linux 文件系统层次结构的所有其他分支都必须是根文件系统的原子部分。它们不能作为单独的文件系统创建,也不能在适当的挂载点挂载到目录树中。存储在目录树的这些其他分支中的程序和数据需要在引导的早期阶段以及在低级恢复或维护模式下运行时可用。
从独立的文件系统开始
为 Linux 文件系统层次结构的一个或多个组件设置单独的文件系统的最佳时机是首次安装操作系统的时候。大多数 Linux 安装程序,比如 Red Hat 的 Anaconda,都提供了在安装过程中进行定制磁盘配置的能力。此时,您可以指定单独的逻辑卷来包含一个或多个我们在本章中讨论过的文件系统,这些文件系统可以单独挂载。
安装程序——至少是我熟悉的安装程序——还能够识别现有的分区、卷和文件系统,并显示关于它们的标识信息。这使得在包含操作系统的硬盘崩溃并需要更换后重新安装 Linux 变得容易。当需要完全重新安装时,它还支持轻松的版本升级,例如从 Fedora 27 升级到 Fedora 28。无需接触/home、/usr 和/var 文件系统,就可以安装操作系统并重新格式化根文件系统。例如,这将使我所有的个人数据、电子邮件收件箱和我的网站数据保持完整。
稍后添加单独的文件系统
在初始安装之后,将这里讨论的一个或多个目录转换成一个单独的文件系统并不特别困难。它只是需要一些远见和计划。
基本流程很简单。事实上,有多种方法可以做到这一点。下面是/home 目录的一个进程的样子。这个过程假设/home 当前不在与根文件系统不同的文件系统上。
-
如有必要,安装新的硬盘。
-
在驱动器上创建分区或逻辑卷。
-
向新分区或卷添加文件系统标签。这使得新文件系统在未挂载时很容易识别,并允许通过标签挂载。
-
备份当前主目录中的数据。如果有空间,将备份存储在/tmp 中。这是让/tmp 变大的一个很好的理由。
-
从当前/主目录中删除数据。这个步骤释放了在新文件系统被挂载到文件系统层次结构的主干上的/home 之后不可访问的空间。
-
在/etc/fstab 中添加一个条目,指定新文件系统在/home 上的挂载。
-
将新卷挂载到/home。
-
将数据恢复到/home。
-
测试并验证所有数据都已正确恢复。
让我们在实验 14-1 中做一个类似的实验。
实验 14-1
这个实验应该以 root 用户身份进行。在备份了现有的/home 目录后,我们将删除 USB 设备上的现有分区并创建一个 Linux 分区,将新分区格式化为 EXT4,挂载为/home,然后恢复备份的数据。
注意如果您没有注销所有学生用户会话,可能会出现意外结果。
如果您以学生用户身份登录,请注销所有学生登录会话。
**警告!**该实验可能会导致您的个人目录中的数据丢失。您应该仅在用于培训而非用于生产的虚拟机或主机上执行此实验。
我们将使用 USB 驱动器作为新家庭驱动器的位置。如果 USB 设备已经插入您的系统,请将其卸载并移除。
**警告!**该实验将销毁 USB 设备上的所有现有数据。在继续之前,请确保您使用的是为这些实验指定的设备。
将 USB 驱动器插入 USB 端口。不要安装它。使用dmesg确定分配给设备的设备专用文件。在我的虚拟机中,这是/dev/sdb。
我们将使用fdisk删除现有的分区并创建一个新的 Linus 分区。使用fdisk查看现有分区。
[root@testvm1 ~]# fdisk /dev/sdb
Welcome to fdisk (util-linux 2.30.2).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.
Command (m for help): p
Disk /dev/sdb: 62.5 MiB, 65536000 bytes, 128000 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x73696420
Device Boot Start End Sectors Size Id Type
/dev/sdb1 2048 127999 125952 61.5M c W95 FAT32 (LBA)
Command (m for help):
注意,现有的分区可能是 FAT32 (VFAT)分区。我们希望将 EXT4 用于我们的/home 文件系统。删除现有分区,然后打印结果以验证该分区已被删除。
Command (m for help): d
Selected partition 1
Partition 1 has been deleted.
Command (m for help): p
Disk /dev/sdb: 62.5 MiB, 65536000 bytes, 128000 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x73696420
使用设备上的所有空间创建新分区。采用分区类型和数量的默认值。当被问及是否要删除 VFAT 签名时,请回答是。然后采用起始和结束扇区的默认值。
Command (m for help): n
Partition type
p primary (0 primary, 0 extended, 4 free)
e extended (container for logical partitions)
Select (default p):
Using default response p.
Partition number (1-4, default 1):
First sector (2048-127999, default 2048):
Last sector, +sectors or +size{K,M,G,T,P} (2048-127999, default 127999):
Created a new partition 1 of type 'Linux' and of size 61.5 MiB.
Partition #1 contains a vfat signature.
Do you want to remove the signature? [Y]es/[N]o: y
The signature will be removed by a write command.
验证新分区是 Linux type 83 分区。
Command (m for help): p
Disk /dev/sdb: 62.5 MiB, 65536000 bytes, 128000 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x73696420
Device Boot Start End Sectors Size Id Type
/dev/sdb1 2048 127999 125952 61.5M 83 Linux
Filesystem/RAID signature on partition 1 will be wiped.
Command (m for help):
现在将新的分区表写入 USB 设备。
Command (m for help):
w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks
[root@testvm1 ~]#
创建 EXT4 文件系统。注意,我们将文件系统添加到 sdb1 分区,而不是磁盘本身 sdb。在小型设备上,这不会花费很长时间。
[root@testvm1 ~]# mkfs -t ext4 /dev/sdb1
mke2fs 1.43.5 (04-Aug-2017)
Creating filesystem with 62976 1k blocks and 15744 inodes
Filesystem UUID: 915c4857-cc81-4637-80ac-5e69d40329df
Superblock backups stored on blocks:
8193, 24577, 40961, 57345
Allocating group tables: done
Writing inode tables: done
Creating journal (4096 blocks): done
Writing superblocks and filesystem accounting information: done
为分区创建一个标签,然后验证它是否已创建。
[root@testvm1 ~]# e2label /dev/sdb1 home
[root@testvm1 ~]# e2label /dev/sdb1
home
[root@testvm1 ~]#
是时候备份当前的主目录了。由于您执行这些实验的主机是为培训而指定的,因此应该不会有太多备份,所以这不会花很长时间。我们将创建一个简单的 tarball 作为备份。
[root@testvm1 ~]# tar -cvf /tmp/home.tar /home
现在我们需要小心一点。对于这个实验,我们不会在/etc/fstab 中添加条目。并且我们不会删除/home 当前的任何内容。
提示在/home 挂载点上挂载位于 USB 设备上的主文件系统不会删除或损坏当前主目录中的现有数据。新文件系统安装在现有数据上,无法再访问这些数据。卸载 USB 设备上的主文件系统后,原始主目录及其数据将再次可访问。
现在让我们在/home 上挂载新创建的主文件系统。我们将使用设备专用文件来明确指定我们想要挂载的设备。这防止了与任何其他可能带有“home”标签的主文件系统的任何潜在冲突我们还快速查看了除 lost+found 目录之外应该为空的内容。
[root@testvm1 ~]# mount /dev/sdb1 /home ; ls -lR /home
/home:
total 12
drwx------. 2 root root 12288 Feb 2 14:49 lost+found
/home/lost+found:
total 0
[root@testvm1 ~]#
请记住,/home 中的原始数据仍然存在,只是被挂载在/home 挂载点上的空文件系统所掩盖。
现在,我们可以将备份数据恢复到主目录。当从 tarball 中提取数据时,它总是被恢复到当前目录中。因此,如果我们想要恢复/home,我们需要在执行提取之前将根目录(/)设置为 PWD,我们在下面的命令中这样做。
[root@testvm1 ~]# cd / ; tar -xf /tmp/home.tar
[root@testvm1 /]#
让我们验证提取是否正常工作。
[root@testvm1 /]# ls -l /home
total 16
drwx------. 2 root root 12288 Jan 15 10:04 lost+found
drwx------. 6 student student 1024 Jan 31 09:03 student
[root@testvm1 /]# ls -l /home/student/
total 37
-rw-rw-r--. 1 student student 84 Jan 27 15:28 error.txt
-rw-rw-r--. 1 student student 15 Jan 27 11:41 file0.txt
-rw-rw-r--. 1 student student 15 Jan 27 11:41 file1.txt
-rw-rw-r--. 1 student student 15 Jan 27 11:41 file2.txt
-rw-rw-r--. 1 student student 15 Jan 27 11:41 file3.txt
-rw-rw-r--. 1 student student 15 Jan 27 11:41 file4.txt
-rw-rw-r--. 1 student student 15 Jan 27 11:41 file5.txt
-rw-rw-r--. 1 student student 15 Jan 27 11:41 file6.txt
-rw-rw-r--. 1 student student 15 Jan 27 11:41 file7.txt
-rw-rw-r--. 1 student student 15 Jan 27 11:41 file8.txt
-rw-rw-r--. 1 student student 15 Jan 27 11:41 file9.txt
-rw-rw-r--. 1 student student 60 Jan 27 15:28 good.txt
-rwxr-xr--. 1 student student 9830 Jan 30 09:28 script.template.sh
-rw-rw-r--. 1 student student 42 Jan 27 15:16 test1.txt
你也可以这样做。
[root@testvm1 /]# df -h /home
Filesystem Size Used Avail Use% Mounted on
/dev/sdb1 56M 36M 16M 70% /home
请注意,由于我使用的 USB 设备非常小,所以只有少量的可用空间。
此时,用户 student 正在使用新的/home 文件系统。以学生用户的身份重新登录,验证一切正常。然后以学生用户的身份注销。
现在我们需要卸载/home 文件系统。
[root@testvm1 /]# umount /home
现在可以安全地从主机上移除 USB 设备了。原来的主目录现在被取消屏蔽,正在使用中。
最后的想法
独立的文件系统使我们作为系统管理员的工作更容易。在单独的文件系统中维护部分目录树,我们在驱动器崩溃的情况下提供了更大的灵活性,能够灵活地将文件系统移动到不同的挂载点,并在需要时更容易执行完整的操作系统重新安装。它还提高了目录结构的其他部分在一个硬盘崩溃时的生存能力。
我花了一段时间才明白这是一个好主意,但是从那时起,我一直为这里讨论的目录维护独立的文件系统。不止一次保存了我的数据。
Footnotes 1Unnikrishnan A,Linux, Udev:现代 Linux 系统设备管理入门, https://www.linux.com/news/udev-introduction-device-management-modern-linux-system
2
LSB 工作组 Linux 基金会,文件系统分层标准 V3.0 ,3, https://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.pdf
3
同上,第 3 页。
4
同上。
5
维基百科,初始 ramdisk , https://en.wikipedia.org/wiki/Initial_ramdisk
6
同上,第 13 页。
十五、使程序可移植
可移植程序让懒惰的系统管理员的生活变得更加轻松。可移植性是一个重要的考虑因素,因为它允许程序在广泛的操作系统和硬件平台上使用。使用可以在许多类型的系统上运行的解释性语言,如 bash 和 Perl,可以节省大量的工作。
当从一个平台移植到另一个平台时,用 C 等编译语言编写的程序至少必须重新编译。在许多情况下,必须在源代码中维护特定于平台的代码,以便支持二进制文件预期运行的不同硬件平台。这产生了许多额外的工作,包括编写和测试程序。
Perl、bash 和许多其他脚本语言在大多数环境中都可用。除了极少数例外,用 Perl、bash、Python、PHP 和其他语言编写的程序可以不加修改地在许多不同的平台上运行。
英特尔 PC 到大型机
在我工作的一个地方,我负责一台运行 Linux 的 Intel 主机和一个相当大的带有内置数据库引擎的 Apache 网站。我们编写了大量的 Perl 和 bash 程序,它们被用作 CGI,根据从数据库中检索到的数据生成网页。甚至数据库软件也是由我们的一个系统管理员用 Perl 编写的。
作为灾难恢复计划的一部分,我们运行的所有大型机和 Unix 程序(我们的整个软件库存)都应该迁移到位于费城的恢复服务中。该服务不提供基于英特尔的计算机,因此我们的网站不能直接得到相同硬件的支持。
但是,我们组织中的一部分硬件(也在灾难恢复站点复制)是 IBM Z 系列大型机,它可以支持大量的 Red Hat 实例。我们决定明智的做法是测试我们的软件,看看它是否可以移植到 IBM Z 系列机器上。我们希望不需要做太多的改动就能让它工作。我可以访问 Z 系列主机上的专用 Red Hat 实例,并被告知报告我的结果。
我从识别必须移动的软件和相关数据开始。这很容易,因为我们按照 Linux 文件系统分层标准的定义,使用文件和数据的标准目录位置。
创建我们需要传输的文件的 tarball 不到五分钟,将 tarball scp(安全拷贝)到大型机只需几秒钟。我从 tarball 中提取文件,使用启动 shell 程序启动各种服务器,“自动化一切”,并开始测试。一切都完美无缺。除了测试本身,从开始到结束再到传输、启动和运行的总时间为 12 分钟。
这部分是因为我们的数据库实际上是一个平面 ASCII 文本文件,符合第十三章中的原则:“以开放格式文件存储数据。”修改它,从一种二进制格式转换到另一种,或者从 ASCII 转换到 EBCDIC,或者从一个系统导出到另一个系统,都不需要神奇的咒语。刚刚成功了。
但是这种简单的移植也是因为我们使用了 Perl 和 bash,这有利于程序的移植。
架构
Linux 运行在许多架构上。其实硬件架构挺多的。 1 维基百科维护了一长串 Linux 支持的硬件架构列表,这里只是其中的几个。
当然 Linux 支持 Intel 和 AMD。
它还支持 32 位和 64 位 ARM 架构,这在地球上几乎所有的手机和设备中都可以找到,如 Raspberry Pi。 2 大多数手机使用的是一种叫做 Android 的 Linux 形式。
飞思卡尔(原摩托罗拉)68K 架构;德州仪器 320 系列,高通六角形;惠普的 PA-RISC;IBM 的 S390 和 Z 系列;MIPSIBM 的 Power、PowerPC、SPARC 等等。
这些架构中的每一个在硬件指令集 3 级别上都是不同的。每个架构需要不同的编译器,或者至少需要一个能够支持各自指令集的编译器。这反过来意味着,当从一个架构移植到另一个架构时,任何使用编译语言的程序都必须重新编译。这是一种可移植性,尽管程序需要重新编译。
我在这一章中的意思是,当程序从一种架构转移到另一种架构时,它应该能够正常工作。不需要重新编译或重写。只有 shell 和其他解释性脚本语言可以做到这一点。
便携性限制
当我第一次听到与软件有关的术语“可移植性”时,它指的是复制一个程序,可以从一台计算机转移到另一台具有相同架构和操作系统的计算机上,并在那里运行。在谷歌上搜索会找到大量的结果,这些结果都与使用各种技术将软件从一台 Windows 电脑转移到另一台电脑有关,包括从可以插入任何电脑的 USB 驱动器上运行程序。其他技术描述得不太清楚。
批准
其他结果指的是简单地在多个硬盘上安装程序。供应商可以出于各种原因试图阻止这种情况,在某些情况下,这是相当非法的。最终用户许可协议(EULA)可能明确声明您有权仅在一台计算机上安装和使用程序。更宽松的版本可能允许您在多台计算机上安装它——有一些特定的限制——但一次只能在一台计算机上使用它。
我不想卷入关于许可协议的讨论。但是真正的可移植性受到许可的影响,所以有必要考虑一下。有时候技术并不是便携性的限制因素。
技术
然而,有时技术是软件可移植性的限制因素。
编译器和代码
我们已经研究了与支持平台相关的可移植性。对于编译过的程序,这意味着编译器必须能够创建与支持的平台兼容的二进制文件。我们已经看到 Linux 在广泛的硬件平台上得到支持,所以显然有编译器支持这些平台。
我们可以说,这些平台有一定程度的兼容性,代码在它们之间是半可移植的。这基本上意味着,如果需要的话,代码可以放在一个单一的代码库中,但是为了支持目标平台,需要在代码中进行考虑。这些差异是由于每个平台的硬件指令集的固有差异。
好消息是,Linux 使用的 GNU 编译器集合 4 (GCC)包含 C、C++、Objective C、Fortran、Java 和 Ada 编程语言的编译器。GCC 可以运行在 60 多种操作系统平台上,包括 Linux、DOS、Windows、许多 Unix 变体、MIPS、NeXT 和一大堆我在找到脚注 4 中引用的 GCC 定义之前从未听说过的平台。我们还可以在该文档中看到,GCC 支持多种处理器,可以针对这些处理器编译二进制代码。
这意味着我们在编译的二进制世界中有了某种程度的可移植性。缺点是为一个硬件平台编译的代码不能在不同的硬件平台上运行,所以必须重新编译。有时,为了使代码能够编译,必须对其进行重大修改。这需要很大的努力,而且大多数开发人员都不愿意尝试让他们的代码在所有甚至大部分硬件平台上编译。他们通常会挑选一两家最有潜在客户的公司,不会超出这个范围。
如果源代码是开放源代码,那么一些需要或希望让这些代码在一个不太常见的硬件平台上运行的程序员可以这样做。如果他们真的这么做了,那肯定需要大量的工作和知识才能实现。
对于我们这些懒惰的管理员来说,这绝对不是一个合适的选择。让我们从一开始就使我们的代码更加可移植,并消除大部分额外的工作。在我看来,编译代码的可移植性很低,因为从一个平台转移到另一个平台需要大量的工作。可以做,但是我不想自己做。
书店老板
LibreOffice 5 是可移植的编译代码的一个很好的例子。我在各种项目中广泛使用 LibreOffice,包括写这本书。LibreOffice 可用于许多操作系统平台,包括 Linux、各种 Windows 版本、Mac OS 和 Android。甚至还有一个“便携”6版本采用 PortableApps.com 打包。 7 这种打包使应用可以从自己的 u 盘上使用,例如,在任何 Windows 计算机上。
所以 LibreOffice 在多种意义上都是可移植的。它也是开源的,因此您可以从 LibreOffice 网站下载源代码,并根据自己的需要进行修改。我们大多数人都不会这样做,但是代码是可用的,所以如果我们需要或想要的话,我们可以查看或更改它。LibreOffice 是在 Mozilla 公共许可证 2.0 版下分发的。 8
Shell 脚本
现在,我们回到 shell 脚本。为什么呢?因为绝大多数 shell 脚本可以在 Linux 下的任何硬件平台上运行。在大多数情况下,它们也可以在其他 Unix 和类似 Unix 的操作系统上工作。
shell 实际上是一种编程语言:它有变量、循环、决策等等。
Unix 编程环境 9
这句话适用于我用过的每一款 shell。在前面的章节中,你已经看到了如何直接在命令行编写简短的 shell 程序,以便于快速解决问题。我们还介绍了如何创建可执行文件来存储这些特别的程序,以便将来可以使用它们,并供其他可能需要相同解决方案的系统管理员使用。
我更喜欢使用 bash,因为它是所有 Linux 发行版的默认 shell,并且它也适用于 Unix。其他 shells 也很普遍,如 ksh、csh、tcsh 和 zsh,但它们可能需要安装,因为它们可能不是默认安装的。
bash shell 与可移植操作系统接口(POSIX 11 )标准几乎 100%兼容,这意味着在一个操作系统和硬件平台上运行的 bash shell 脚本也可以在支持 bash 的所有其他平台上运行。这并不意味着你不会遇到一些问题。例如,我们在第十一章中编写的 mymotd 脚本寻找一些特定的硬件数据,这些数据可能不可用,也可能可用,但方式与我们的脚本假设的不同。脚本将会运行,但是您可能会遇到一些异常的结果。
Windows 的可移植性
到目前为止,我们一直关注 Linux 和 Unix 操作系统的兼容性。但是 Windows 呢?尽管这本书和《系统管理员的 Linux 哲学》是关于 Linux 环境的,但是如果我们不看看 Windows,不管多么简短,这一章都是不完整的。
如上所述,可以创建可以在 Linux、各种版本的 Unix、Windows 和其他操作系统上编译的源代码。要做到这一点需要做很多工作,但这是可以做到的,而且已经做到了。真正的问题是我们如何在 Linux 和 Windows 上运行我们的 shell 脚本。
有几种方法可以在 Linux 和 Windows 之间提供脚本可移植性。
Cygwin
Cygwin 是一个免费的开源产品,可以下载并安装在您的 Windows 计算机上。Cygwin 支持 Windows Vista 和更高版本,并安装了一个非常灵活的 Linux 环境和一套从 Linux 和 GNU 实用程序移植过来的几乎完整的程序、实用程序和桌面环境。
可以使用 Cygwin 安装 bash、tcsh、其他 shells、KDE 和其他 Linux 桌面,以及我们系统管理员已经习惯的许多 Linux 实用程序。不仅可以在 Windows 上体验 Linux,而且我们的 bash 和其他脚本现在也可以移植到 Windows 上。Cygwin 环境甚至扩展到强加/dev 目录和我们期望在任何 Linux 主机上找到的通常的设备专用文件。
不过,这种便携性也有其局限性。例如,硬件和操作系统特定的功能可能无法正常工作。因此,可能有必要在 shell 脚本中添加一些代码来确定操作系统环境,并相应地考虑到这些差异。这并不是什么新鲜事,在不同的 Linux 发行版之间以及 Linux 和各种版本的 Unix 之间都有过。向脚本添加一点额外的代码以允许它在多个操作系统上运行,这是确保更高水平的可移植性的一种非常简单的方法。
在其他情况下,bash 脚本可能会在运行的意义上进行移植,但在一般情况下没有意义。例如,我编写的安装后脚本可以处理 Fedora Linux 安装无法运行的所有任务,但它会生成许多错误。
我花了一点时间安装并学习了一些关于 Cygwin 的知识,但是除了像这样的一些测试之外,我通常不使用 Windows。Cygwin bash shell 是大家熟悉的,它提供了一个使用不依赖于操作系统的 Linux 命令和脚本的好机会。
管理员
微软在 2006 年发布了第一版 PowerShell 12 。2018 年 1 月,他们在麻省理工学院的许可下推出了 PowerShell。代码本身现在可以在许多平台上使用,包括 Linux。PowerShell 是一种面向对象的脚本语言,它旨在提供 Windows 和 Linux 平台之间的脚本可移植性。
我没有用过 PowerShell,虽然我只是玩了一会儿,看看它是关于什么的。它与我使用过的任何 Linux 和 Unix shells 都非常不同。我怀疑如果我花一些时间使用它,我可以像使用 bash shell 一样使用它。由于我还需要学习 Linux,我自己可能不会使用 PowerShell。但是,如果您需要 Linux 和 Windows 操作系统之间的脚本可移植性,您绝对应该查看一下。
面向 Linux 的视窗子系统
用于 Linux 的 Windows 子系统 14 (WSL)允许 Linux ELF 二进制文件在 X64 版本的 Windows 10 主机上运行。这个兼容层使 Windows 用户能够从 Windows 商店安装和运行许多不同的 Linux 发行版。
WSL 有其局限性,但它为需要跨平台兼容性的用户提供了另一种选择。
互联网和便携性
我们一直在从命令行运行 shell 脚本。当我们使用其他程序运行我们的脚本时会发生什么?例如,使我们的脚本可移植的一种方法是在 web 服务器上将它们作为 CGI 程序运行,并将结果交付给发出请求的 web 浏览器。
这种可移植性方法的优点是用户不需要特殊的工具、虚拟机或兼容层。不需要在客户端,即用户的主机上下载和安装软件。脚本运行,工作在 web 服务器上完成。只有浏览器用来生成和显示包含 web 服务器完成的工作所产生的信息的页面的数据流被发送到请求客户端。
让我们快速地看一下为这种类型的环境创建脚本。
创建网页
回到互联网的石器时代,当我第一次创建我的第一个商业网站时,生活是美好的。我安装了 Apache HTTP 服务器,并创建了几个简单的 HTML 页面,这些页面陈述了一些关于我的业务的重要事项,并提供了一些信息,如我的产品概述和如何联系我。这是一个静态网站,因为内容很少改变。维护很简单,因为我的网站是不变的。
静态内容
静态内容很容易,而且仍然很常见。让我们快速地看一下两个示例静态网页。你不需要一个工作网站来执行这些小实验。只需将文件放在您的主目录中,然后用浏览器打开它们。如果文件通过网络服务器提供给你的浏览器,你会看到完全一样的效果。
web 服务器的唯一功能是将创建网页的文本数据从服务器发送到浏览器。在本章的实验中,我们将简单地在/home/~目录中创建文本数据流作为文件。
在静态网站上,我们首先需要的是 index.html 文件,它通常位于/var/www/html 目录中。这个文件可以简单到像“Hello world”这样的文本短语,根本不需要任何 HTML 标记。这将简单地显示没有任何格式的文本字符串。
实验 15-1
本章中的所有实验都可以作为学生用户进行。
在您的主目录中创建 index.html,并添加文本“Hello world ”,没有引号或任何 HTML 标记,因为它是唯一的内容。
使用以下 URL 在浏览器中打开 index.html。
file:///home/<yourhomedirectory>/index.html
结果相当平淡无奇。只是浏览器窗口上的一点文字。
所以 HTML 标记不是必需的,但是如果你有大量需要格式化的文本,没有 HTML 编码的网页的结果将是不可理解的,因为所有的东西都在一起运行。
所以下一步是通过使用一些 HTML 编码来提供一些格式,使内容更具可读性。
实验 15-2
下面的数据创建了一个页面,它具有 HTML 静态网页所需的绝对最小标记。将 H1 标记添加到 index.html 文件的文本中。
<h1>Hello World</h1>
现在看看 index.html,看看不同之处。
当然,您可以在实际的内容行周围放置许多额外的 HTML 来制作一个更加完整和标准的网页。如下所示的更完整的版本仍然会在浏览器中显示相同的结果。它还为更标准化的网站奠定了基础。继续将此内容用于您的 index.html 文件,并在您的浏览器中显示。
<!DOCTYPE HTML PUBLIC "-//w3c//DD HTML 4.0//EN">
<html>
<head>
<title>My Web Page</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
使用更复杂的表单的结果变化不大,但它构成了一个完整的 HTML 编码的网页。上面的 HTML 代码唯一改变的是我们现在有了一个标题“我的网页”,它出现在浏览器标签或标题栏中。
我用这些技术建立了几个静态网站,但是我的生活即将改变。
新工作的动态网页
我曾经接受了一份新工作,我的主要任务是为一个非常动态的网站创建和维护 CGI(公共网关接口)代码。在这种情况下,动态意味着在浏览器上生成网页所需的 HTML 是从每次访问页面时都可能不同的数据中生成的。这包括用户在用于在数据库中查找数据的 web 表单上的输入。结果数据被适当的 HTML 包围并显示在请求浏览器上。但是不需要那么复杂。
为网站使用 CGI 脚本允许您创建简单或复杂的交互式程序,运行这些程序可以提供动态网页,该网页可以根据输入、计算、服务器中的当前条件等进行更改。有许多语言可以用于 CGI 脚本。我们将研究其中的两个,Perl 和 BASH。其他流行的 CGI 语言是 PHP 和 Python。
本章不包括 Apache 或任何其他 web 服务器的安装和设置。如果您可以访问可以试验的 web 服务器,则可以直接查看浏览器中显示的结果。否则,您仍然可以从命令行运行程序,并查看将创建的 HTML。您还可以将 HTML 输出重定向到一个文件,然后在浏览器中显示结果文件。
使用 Perl
Perl 是一种非常流行的 CGI 脚本语言。它的优势在于它是一种非常强大的文本处理语言。
要执行 CGI 脚本,您需要在您正在使用的网站的 httpd.conf 中使用下面一行。
ScriptAlias /cgi-bin/ "/var/www/cgi-bin/"
这将告诉 web 服务器可执行 CGI 文件的位置。对于这个实验,我们不要担心服务器端的事情。没有网络服务器,我们仍然可以做任何我们需要的事情。
实验 15-3
创建一个新文件 index.cgi,并向其中添加以下 Perl 代码。对于这个实验,这个文件也应该位于您的主目录中。
#!/usr/bin/perl
print "Content-type: text/html\n\n";
print "<html><body>\n";
print "<h1>Hello World</h1>\n";
print "Using Perl<p>\n";
print "</body></html>\n";
将 index.cgi 的权限设置为 755,因为它必须是可执行的。
[student@testvm1 ~]$ chmod 755 index.cgi
从命令行运行该程序并查看结果。它应该显示它将生成的 HTML 代码。
[student@testvm1 ~]$ ./index.cgi
Content-type: text/html
<html><body>
<h1>Hello World</h1>
Using Perl<p>
</body></html>
[student@testvm1 ~]$
我们现在有了一个 Perl 程序,可以生成 HTML 以便在 web 浏览器中查看。
当使用 web 服务器时,可以将文件的所有权设置为 apache.apache。
现在在浏览器中查看 index.cgi。你从这里得到的只是文件的内容。浏览器真的需要将它作为 CGI 内容来发布。除非服务器告诉它程序所在的目录是按照上面 httpd.conf 中显示的那样指定的,否则它并不知道如何做。
要查看它在浏览器中的样子,请再次运行该程序,并将输出重定向到一个新文件 test1.html。
[student@testvm1 ~]$ ./index.cgi > test1.html
[student@testvm1 ~]$ cat test1.html
Content-type: text/html
<html><body>
<h1>Hello World</h1>
Using Perl<p>
</body></html>
[student@testvm1 ~]$
现在使用您的浏览器查看您刚刚创建的包含生成内容的文件。您应该会看到一个格式良好的网页。
实验 15-3 中的 CGI 程序仍然生成静态内容,因为它总是显示相同的输出。在实验 15-4 中,我们使用 Perl“system”命令在系统 Shell 中执行跟在它后面的 Linux 命令。结果被返回给程序。在这种情况下,我们简单地从 free 命令的结果中提取当前的 RAM 使用情况。
实验 15-4
将下面一行添加到 index.cgi 程序中。
system "free | grep Mem\n";
您的程序现在应该看起来像这样。
#!/usr/bin/perl
print "Content-type: text/html\n\n";
print "<html><body>\n";
print "<h1>Hello World</h1>\n";
print "Using Perl<p>\n";
system "free | grep Mem\n";
print "</body></html>\n";
从命令行运行该程序两三次,可以看到 free 命令几乎每次都返回不同的数字。
[student@testvm1 ~]$ ./index.cgi
Content-type: text/html
<html><body>
<h1>Hello World</h1>
Using Perl<p>
Mem: 4042112 300892 637628 1040 3103592 3396832
</body></html>
[student@testvm1 ~]$ ./index.cgi
Content-type: text/html
<html><body>
<h1>Hello World</h1>
Using Perl<p>
Mem: 4042112 300712 637784 1040 3103616 3396996
</body></html>
[student@testvm1 ~]$ ./index.cgi
Content-type: text/html
<html><body>
<h1>Hello World</h1>
Using Perl<p>
Mem: 4042112 300960 637528 1040 3103624 3396756
</body></html>
[student@testvm1 ~]$
再次运行程序,并将输出重定向到结果文件。
[student@testvm1 ~]$ ./index.cgi > test1.html
在浏览器中重新加载~/test1.html 文件。您应该会看到显示系统内存统计信息的附加行。运行程序,同时将输出重定向到该文件,并多次刷新浏览器,注意内存使用情况会偶尔发生变化。
使用 BASH
Bash 可能是 CGI 脚本中使用的最简单的语言。它在 CGI 编程方面的主要优势在于它可以直接访问所有标准的 GNU 实用程序和系统程序,并且系统管理员应该熟悉它。
实验 15-5
将现有的 index.cgi 重命名为 Perl.index.cgi,并使用以下内容创建一个新的 index.cgi。
#!/bin/bash
echo "Content-type: text/html"
echo ""
echo '<html>'
echo '<head>'
echo '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">'
echo '<title>Hello World</title>'
echo '</head>'
echo '<body>'
echo '<h1>Hello World</h1><p>'
echo 'Using BASH<p>'
free | grep Mem
echo '</body>'
echo '</html>'
exit 0
记得将权限设置为可执行。从命令行运行该程序并查看输出。
[student@testvm1 ~]$ chmod 755 index.cgi
[student@testvm1 ~]$ ./index.cgi
Content-type: text/html
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hello World</title>
</head>
<body>
Hello World</h1><p>
Using BASH<p>
Mem: 4042112 290076 647716 1040 3104320 3407516
</body>
</html>
再次运行该程序,并将输出重定向到您之前创建的临时结果文件。然后刷新浏览器,查看它显示为网页的样子。结果应该是相同的,除了一些内存数字会有一点不同。
CGI–开放和便携
从这些实验中可以看出,创建开放的、可移植的 CGI 程序是很容易的,这些程序可以用来生成各种各样的动态网页。这些都是微不足道的例子,但你现在应该看到一些可能性。
虽然我们认为脚本最常见的方式是从命令行运行它们,但是它们也可以与其他软件一起使用来执行一些非常有趣的任务。用通用语言编写的 CGI 脚本就是一个很好的例子。
因为用于创建我们的 CGI 程序的语言在许多操作系统上都受支持,所以这些程序是可移植的。您可能需要在 Windows web 服务器上安装 bash,但这也是可能的。例如,像 Python 和 PHP 这样的其他语言也可以用来生成动态网页,并且与 Perl 一起,在大多数平台上都很容易获得,包括操作系统和硬件。
博客
WordPress 15 是一个强大的开源程序,允许创建和管理网页。这是用脚本语言编写完整程序来生成和交付基于 web 的动态内容的一个很好的例子。WordPress 本身只是生成网页的代码;仍然需要像 Apache HTTP 服务器这样的 web 服务器将数据从服务器传送到客户机 web 浏览器。
WordPress 是用 PHP 编写的,所以很容易移植到任何运行 PHP 的平台上。PHP 是一种特别适合编写动态网页的编程语言。我有时会忘记安装 PHP,因为它并不总是默认安装的。我将 PHP 安装添加到我的安装后脚本中,因此它将一直存在。但是如果你在运行 WordPress 时遇到问题,请检查 PHP 是否已经安装。
WordPress 非常灵活,因为它使用主题来生成网站的外观和感觉。通过改变主题,只需点击几下鼠标,就可以在几秒钟内改变网站的外观。我在我所有的网站上都使用 WordPress,因为它非常简单和灵活。我甚至教过非技术人员如何使用类似文字处理的界面来创建新的网页和帖子。
虽然 WordPress 主题的许多方面可以通过它自己的基于网络的管理界面来改变,但是有些事情需要直接使用 CSS 样式表,以及特定于主题的 PHP 代码,来定义每个主题的外观和感觉。通过 WordPress 界面使用 CSS 是可能的,但是我发现在终端会话中使用 Vi 或 Vim 对我来说效果最好。
然而,在我修改任何东西之前,我总是会做一个新的副本,并保持原来的原样。我通常会将副本重新命名为“我的主题”,以区别于原始版本。然后我使用 WordPress 管理界面切换到我的新主题。现在,我可以修改新的主题,而不需要担心对原始主题的更新会消除我的更改。
当然,我可以修改 CSS 来改变颜色和字体。我还可以修改主题的 PHP 代码,以便稍微改变一下页面结构。当主题需要一点调整时,我已经这样做了几次。在安装 WordPress 可用的插件时,我还修改了主题的 PHP 代码。
所有这些成为可能的唯一原因——就可移植性和改变它的能力而言——是 WordPress 和它可用的主题都是开放和可访问的。组成这个应用的文件都存储为 ASCII 文本文件。而且是开源的,也就是说 WordPress 所发布的 GPLv2 17 许可证允许这一切。
最后的想法
我打算在这一章中尝试定义可移植性。然而,随着我写作的进展,我开始意识到可移植性是一系列的值,而不仅仅是二元的反应——是的,它是可移植的,或者不是,它不是可移植的。尽管它们是可移植的,但是 shell 脚本可能仍然需要调整,以便在不同的操作系统和硬件平台上运行时产生期望的结果。
便携性是减少我们工作量的一个关键因素。编写可移植的代码——或者至少是尽可能可移植的——是只需要做一次工作的极好方法。当可以用 shell 脚本在所有这些平台上运行一次时,为什么要为几个不同的平台编写代码呢?
作为系统管理员,命令行脚本是我们花费大部分时间的地方,让这些脚本可移植是很重要的。幸运的是,大多数 shell 脚本,尤其是那些用 bash 编写的脚本,都具有很高的可移植性。为我们管理的网站编写可移植的 CGI 代码是另一个很好的步骤,如果它适用的话。
使用开源代码可以节省更多的时间,这些代码是可移植的,并且已经为许多环境进行了测试和创建。我们把 WordPress 作为一个例子。仅仅因为我们可以编写自己令人惊叹的 CGI 脚本来驱动一个网站,并不意味着这样做是有效的。WordPress 已经写好了,它是开源的,而且做得很好。如果你不喜欢 WordPress,还有很多其他的选择。
便携性太棒了!
Footnotes 1维基百科,支持 Linux 的计算机架构列表, https://en.wikipedia.org/wiki/List_of_Linux-supported_computer_architectures
2
树莓派基金会, https://www.raspberrypi.org/
3
免费在线计算词典,指令集, http://foldoc.org/instruction+set
4
Linux 信息工程, GCC 定义,,http://www.linfo.org/gcc.html
5
书店,主页, https://www.libreoffice.org/
6
LibreOffice,便携版, https://www.libreoffice.org/download/portable-versions/
7
portableapps . com,主页,
8
https://www.libreoffice.org/about-us/licenses/
9
布赖恩·w·克尼根;派克·罗布(1984),《3。使用 Shell,"UNIX 编程环境, Prentice Hall,Inc .,ISBN 0-13-937699-2,94
10
Newham 和 Rosenblatt,学习 Bash Shell (O'Reilly 1998),ISBN 1-56592-347-2248。
11
维基百科,【POSIX】、 https://en.wikipedia.org/wiki/POSIX
12
Opensource.com 2018 年 2 月 6 日,权(壳)到人, https://opensource.com/article/18/2/powershell-people
13
Linux 基础, MIT License , https://spdx.org/licenses/MIT
14
微软,https://docs.microsoft.com/en-us/windows/wsl/about的 Windows 子系统
15
16
维基百科,PHP, https://en.wikipedia.org/wiki/PHP
17
自由软件基金会,自由软件授权资源, https://www.fsf.org/licensing/education