在本章中,您将了解设计良好的程序倾向于使用的配置层次结构。无论您是需要查看手册页以找到一个单一命令的命令行参数,还是想要设置一个环境变量,使其适用于您在 shell 中运行的所有命令,您都将看到如何做到这一点。
从这里开始,我们将向您展示几乎所有 Unix 软件使用的通用配置层次结构,以便当程序的行为不符合您基于所给配置的预期时,您总是知道在哪里进行检查。
最后,您将看到这些配置如何转化为通过 systemd 管理的程序,这是 Linux 上最受欢迎的服务管理工具。

总结来说,本章将涵盖以下主题:
配置层次结构命令行参数环境变量配置文件Docker 中的配置配置层次结构 在 Linux 上运行程序时,您将做的第一件事之一就是根据您的特定需求调整它们。实际上,您已经这样做了:通过向 ls、grep 等命令传递参数,您已经改变了这些程序的行为。
您可能对如何工作有一个直观的感觉,因为您一生都在使用软件。例如,您可能认为传递命令行参数会覆盖程序的默认设置:ls -l 给出的输出与 ls 的默认输出不同。
现在,让我们更严格地了解一下这种直觉,并看看我们是否可以为 Unix 环境中配置通常如何工作制定一些启发式规则。大多数标准的 Unix 命令行程序遵循特定的配置层次结构,其中先前的值会被后续值覆盖。如果您曾经编写过接受用户配置的软件,您可能以前已经创建过这样优先级层次结构:
将可配置值设置为内置默认值。检查通过配置文件传递的值,覆盖这些默认值。检查环境变量(人们可能称这些为 env vars),覆盖配置文件和早期的值。检查命令行界面参数(您可能听说这些被引用为 CLI 参数),并根据需要更新值,覆盖早期的值。每个连续的级别都更接近于特定时刻运行软件的用户,因此每个连续的级别优先于前一个级别。
例如,如果您的软件检测到配置文件和程序启动时传递给程序的 CLI 参数之间存在冲突值,它应该优先选择命令行参数中的值。换句话说,靠近程序调用的值“遮蔽”(在遮蔽或替换的意义上)远离执行的值。命令行参数值替换配置文件值,因为配置文件比程序启动时传递给程序的参数更远。这应该符合直觉:如果软件忽略您的命令行标志,而偏爱程序默认值,您就无法依赖软件。ls -l 不应该给出与 ls 相同的输出。
大多数 Linux 软件在有多种配置方式时遵循此层次结构。请记住,并非所有软件都使用我们将在这里展示的所有配置路径,也并非所有软件都完全遵守这种配置顺序。
让我们再次看看这个层次结构,但这次将其与 nginx Web 服务器程序的具体实际例子联系起来。您可能在职业生涯的某个时候会使用 nginx,因为它是世界上最流行的 Web 服务器之一,用于前端各种动态 Web 应用程序。让我们看看我们刚刚涵盖的优先级层次结构的每个部分如何映射到实际的 nginx 配置:
内置默认值:nginx 启动后执行的用户的默认值为 nobody。全局配置文件可以为所有 nginx 进程更改此值,因此通常在 /etc/nginx/nginx.conf 中找到全局 nginx 配置文件,值为“user www;”,指示 nginx 作为 www 用户运行。用户级配置文件通常是“点文件”(以点开头的文件名,这使它们不在常规 ls 列表中)。例如,/home/dave/.bashrc 是用户特定的 bash 配置的位置。nginx 是一个长期运行的进程,通常不作为常规 Linux 用户运行,但它确实有类似的东西:个别站点通常在它们自己的单独配置文件中配置,位于 /etc/nginx/conf.d/yourwebsite.conf。这些通常从上一级全局配置继承值。环境变量。nginx 从名为 TZ 的环境变量中获取时区信息。命令行参数在软件运行时指定,无论是手动还是自动方式(例如通过 cron 或单元文件)。确保在调试问题时查看这些可能的命令行参数的外部来源——当您看到程序行为和配置文件之间存在差异时,它们是常见的罪魁祸首。nginx 接受各种命令行参数来修改其行为:从覆盖它将使用的配置文件到完全阻止它作为 Web 服务器运行,而是向已经运行的 nginx 进程发出停止或重新加载的信号。现在您已经看到了这个配置层次结构与您在 Linux 上运行的所有程序以及您可能为其编写的所有程序的理论和实践方面,让我们逐步通过配置层次结构,仔细查看每个级别。我们将从最直接和强大的配置形式开始,它覆盖了所有其他内容:在调用程序的时刻传递命令行参数。
命令行参数 您已经熟悉配置程序的最常见方式:使用命令行参数。这些在作为 shell 命令调用时配置程序。
要查找程序的有效命令行参数,请从命令的手册(manual)页开始。除了最基础的系统外,Unix 软件都附带了手册页,记录了大多数程序、解释可用的标志,并——通常在末尾——列出了其他类型的配置方法,如配置文件。
让我们看看 find 命令的手册页内容的开头:
man findFIND(1) General Commands Manual FIND(1) NAME find – walk a file hierarchy SYNOPSIS find [-H | -L | -P] [-EXdsx] [-f path] path ... [expression] find [-H | -L | -P] [-EXdsx] -f path [path ...] [expression] DESCRIPTION The find utility recursively descends the directory tree for each path listed, evaluating an expression (composed of the "primaries" and "operands" listed below) in terms of each file in the tree. The options are as follows: -E Interpret regular expressions followed by -regex and -iregex primaries as extended (modern) regular expressions rather than basic regular expressions (BRE's). The re_format(7) manual page fully describes both formats. -H Cause the file information and file type (see stat(2)) returned for each symbolic link specified on the command line to be those of the file referenced by the link, not the link itself. If the referenced file does not exist, the file information and type will be for the link itself. File information of all symbolic links not on the command line is that of the link itself.
你可以看到,这个手册页面主要记录了运行find命令时可用的各种命令行参数。自第一章以来,你已经使用了很多命令行参数,所以这些应该都很熟悉。
让我们来看下一个,稍微远一点的配置类型:环境变量。
环境变量 尽管命令行参数很强大,但它只适用于它所参与的单个程序调用。当你输入ls -l时,只有那个ls命令会有长格式输出。但如果你想让一个配置值在多次调用命令时持续存在呢?这在写脚本时非常有用,例如,你正在编写一个脚本,在几个不同的点安装包,你想要一次性设置一个配置选项,而不是每次运行包安装命令时都要一遍又一遍地添加它作为命令行参数。这就是环境变量的用武之地。
作为一个开发人员,无论你编写什么类型的软件,你很可能都知道环境变量:与任何其他编程语言中的变量类似的shell值。这些与命令行参数不同,因为它们在更高的层次上操作。环境变量给你更多的杠杆作用:一旦你在shell中设置了配置变量,它就会应用于在该shell会话中启动的所有程序。设置一次,每次运行查找环境变量的程序时,它都会尊重它,直到变量改变或你结束shell会话。
注意
我们将在第12章,“使用Shell脚本自动化任务”中更深入地探讨环境变量,但本节涵盖了基础知识。
大多数标准的Unix环境使用环境变量作为指定对许多不同程序都相关的公共配置的一种方式,而不仅仅是一个程序。例如,环境变量跟踪用户家目录的位置($HOME)、当前工作目录是什么($PWD)、默认应使用哪个shell($SHELL)、在哪里查找与通过CLI接收的命令相对应的可执行文件($PATH)等。
现在就随意检查它们;你可以通过使用echo命令打印出特定环境变量的值来查看它:
$ echo $SHELL/bin/zsh
或者你可以使用env命令查看当前设置的所有环境变量:
$ env...# 你的每个环境变量都有多行输出 ...
要在当前shell中设置一个环境变量,只需使用=进行赋值(确保等号周围没有空格):
MYVAR=fruitloops
你已经为当前shell设置了它:
$ echo $MYVARfruitloops
要使这个变量对任何你启动的子shell持久化(例如,当你运行一个脚本时),请使用export内置命令:
export MYVAR=fruitloops
你将在第12章,“使用Shell脚本自动化任务”中了解更多,但上述命令是你将需要传递给大多数你交互的程序的环境变量配置的范围。
回到find的例子:如果你在上一节中我们查看的find手册页面上滚动得足够远,你会看到一个标题为“环境”的部分:
环境
LANG, LC_ALL, LC_COLLATE, LC_CTYPE, LC_MESSAGES 和 LC_TIME 环境变量影响 find 实用程序的执行,如 environ(7) 所述。
这是不同层次的配置——它们不是在运行时作为命令参数传递,而是可以从shell环境变量中读取的配置指令。
为什么程序应该将环境变量与参数区别对待?让我们仔细思考一下:命令行参数-H非常具体,因为它是在命令调用级别定义的。因此,它只适用于在那个瞬间运行的命令。
另一方面,环境变量不那么具体。它们在shell级别定义,因此可用于从该shell运行的所有命令。
让我们继续向上走配置层次结构:如果值没有在运行时作为命令行参数设置,或者作为程序启动的shell会话中的环境变量设置,那么配置来自哪里?
配置文件 程序查找配置的下一个位置是其配置文件。程序查找配置的位置可能会有很大差异,但有一些标准的地方可以查找。
系统级配置在/etc/ 首先,/etc/目录是一个很好的开始。在第5章,“介绍文件”中,你已经见过这个目录。/etc/programname——其中programname是你想配置的程序的名称——是软件保留系统范围配置的常见选择。对于许多程序来说,这就足够了。例如,nginx web服务器是一个系统级程序:不同的用户通常不会在单个机器上运行自己的web服务器实例,因此只需要系统范围的配置。
也就是说,对于大型或复杂的程序,配置仍然可以拆分在/etc/programname目录内。Nginx是一个很好的例子;它的主配置文件位于/etc/nginx.conf,附加的配置文件来自/etc/nginx/conf.d/目录中的附加文件。
用户级配置在~/.config 对于那些有重要每用户配置的程序——想想文本编辑器、开发工具、游戏等——用户的主目录内的~/.config目录被使用。回想一下第1章,“命令行如何工作”,~是“当前用户的主目录”的简写,并且目录名以句点字符(".")开头的目录在没有传递-a标志的情况下不会出现在ls输出中。~/.config目录是XDG基础目录标准的一部分,你可以在这里查看概述:https://wiki.archlinux.org/title/XDG_Base_Directory。
作为一个例子,我的neovim配置与其他开发人员的配置有显著的不同,但是一个单一的neovim二进制文件在系统上可以同时支持数百名开发人员工作,因为每个开发人员调用neovim时使用的是他们用户特定的配置文件,这些文件保存在~/.config/nvim/中。这很好!
你可以想象,如果只有/etc/中一个单一的系统范围的地方来配置这个程序,那将会引起怎样的混乱——每个开发人员在运行neovim编辑器之前都必须设置无数的环境变量,或者使用无数的命令行标志调用编辑器命令。
现在,你已经走过了Unix程序的经典配置来源,让我们看看一个Linux特有的复杂情况,你应该了解一下:通过环境文件和CLI参数管理通过systemd控制的程序的配置。
systemd单元 在大多数Linux发行版中——除了Docker容器——systemd是主导。我们已经在本书中介绍了systemd的基础知识(见第3章,“使用systemd管理服务”),在本节中,我们将快速看看systemd如何为程序管理配置。
首先,快速回顾一下,以防第3章看起来非常遥远:在systemd管理的Linux环境中,服务被打包成systemd单元文件,这些文件包装并控制实际的可执行二进制文件、它的参数、用于启动、重启和停止单元的命令等等。
我们已经介绍过许多systemd单元类型,但我们在这里对服务单元类型感兴趣。
我们已经介绍过单元文件可以存在于几个不同的目录中,这取决于它们的目的,但你自己的自定义systemd单元通常位于/etc/systemd/system。
为了理解一个systemd单元如何让你影响我们在本章中介绍的配置层次结构,让我们通过为我们想象中的程序yourprogram编写自己的systemd单元来创建一个systemd管理的服务。
创建自己的服务 作为一个开发者,您可能需要将您正在编写的程序封装成一个服务,这样可以比手动(交互式)调用程序更容易管理。这本身就非常有用,但在本章中,我们将深入探讨systemd单元如何为您提供额外的控制,以便您控制程序的配置方式和位置。让我们通过创建一个systemd单元文件来包装一个二进制文件,从而创建一个服务。
首先,确保您已经将一个可执行文件复制到默认的$PATH中的某个位置:/usr/local/bin/yourprogram。如果您想充分利用这一点,请使用像上一章“管理已安装的软件”中创建的htop二进制文件这样的手动编译程序,并将想象中的yourprogram替换为htop。
现在,在/etc/systemd/system/yourprogram.service创建以下systemd单元文件:
[Unit]Description=Your program description.After=network-online.target[Service]Type=execExecStart=/usr/local/bin/yourprogram -clioption=1 –clioption2EnvironmentFile=-/etc/yourprogram/prod_defaultsRestart=on-failure[Install]WantedBy=multi-user.target
您能找到这个单元文件中的两条与配置相关的行吗?
您可以看到,ExecStart行指定了当有人启动这个systemd服务时程序是如何被调用的。我们使用systemd单元文件来向程序传递命令行参数,以确保无论何时有人启动服务,程序都会以我们想要的确切选项运行。每当有人运行systemctl start yourprogram时,我们都确保yourprogram将使用-clioption=1和-clioption2被调用。
其次,EnvironmentFile行指定了systemd要检查的文件路径,它期望在这个路径中设置与此程序相关的环境变量。这个文件将由systemd使用的shell解析;它应该包含像这样的shell变量赋值:
# yourprogram environment variablesENV=productionDB_HOST=localhostDB_PORT=5432
让systemd重新读取其配置文件,以确保它看到了您定义的新服务单元:
$ sudo systemd daemon-reload
现在,您可以像管理任何其他systemd服务一样管理它:
systemctl start yourprogramsystemctl status yourprogramsystemctl stop yourprogramsystemctl enable yourprogramsystemctl disable yourprogram
您知道每次启动此服务时,将使用位于/etc/yourprogram/prod_defaults的环境文件来源环境变量,并且ExecStart行将传递您指定的CLI选项。
我们在这里向您展示了一个非常简单的服务,只是为了让您了解如何使用systemd来控制程序配置,但这里还可以传递许多其他配置指令。如果您手头有更复杂的服务,请花些时间阅读systemd单元文档(https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#%5BUnit%5D%20Section%20Options)。
快速说明:Docker中的配置 在本章前面,我们提到Docker在配置方面通常是特例。因为Docker容器是一个更小的环境,它们没有许多传统Unix系统中的额外二进制文件、服务和配置文件。但是,由于现在软件开发者创建的许多软件都在容器中运行,而不是在传统的、完整的操作系统环境中,我们想在这里介绍一些基础知识,以确保您对Docker容器中的配置有何不同有一个直观的认识。我们将在第15章“用Docker容器化应用程序”中更深入地探讨Docker容器。
在容器环境中——无论是Docker还是其他容器运行时——您所处理的是戏剧性更小的环境。安装的程序和实用程序非常少,systemd的替代品是一个戏剧性简化的init,并且文件系统更小,没有我们在这里提到的许多目录。
尽管如此,配置层次结构的原则仍然适用。大多数容器化应用程序期望从以下来源获取其配置:
容器文件系统的某个位置的配置文件,通常由容器调度程序在容器启动前不久动态创建 由容器调度程序或启动它的操作员传递的环境变量 命令行参数 尽管这是配置层次结构的简化版本,您会注意到它基本上与我们在完整的非容器Linux系统中探索的层次结构相同。
我们将在第15章“用Docker容器化应用程序”中更深入地探讨容器。
结论 本章为您提供了Linux配置层次结构的概述以及它如何适用于您将每天使用(和编写)的程序。您了解了命令行参数、环境变量以及适合程序从中获取配置的更大层次结构中的所有其他内容。
如果您跟随了本章的内容,您甚至创建了一个systemd服务,该服务包装了一个程序,并允许您以更统一的方式管理其配置。