第2章:基本命令与目录层次

本章是本书中你将遇到的Unix命令和实用工具的指南。这是基础材料,你可能已经掌握了其中相当一部分。即使你认为自己已经跟上了进度,也请花几分钟快速浏览本章,特别是第2.19节的目录层次材料,以确保万无一失。

为什么是Unix命令?这难道不是一本关于Linux如何工作的书吗?当然是,但Linux本质上是一种Unix变体。你在本章中会看到Unix这个词比Linux更多,因为你可以将所学直接应用到BSD和其他Unix变体系统。我试图避免过多涉及Linux特有的用户界面扩展,这不仅是为了让你更好地了解其他操作系统,也是因为这些扩展往往不稳定。如果你掌握了核心命令,就能更快地适应新的Linux发行版。此外,了解这些命令可以提升你对内核的理解,因为许多命令直接对应系统调用。

NOTE

关于Unix入门更详细的内容,建议阅读《The Linux Command Line》第2版(No Starch Press, 2019)、《UNIX for the Impatient》第2版(Addison-Wesley Professional, 1995)和《Learning the UNIX Operating System》第5版(O’Reilly, 2001)。

2.1 Bourne Shell:/bin/sh

shell是Unix系统最重要的组成部分之一。shell是一个运行命令的程序,例如用户输入到终端窗口中的命令。这些命令可以是其他程序,也可以是shell的内置功能。shell还充当一个小型编程环境。Unix程序员经常将常见任务分解为更小的组件,并使用shell来管理任务和组合它们。

系统的许多重要部分实际上是shell脚本——包含一系列shell命令的文本文件。如果你以前用过MS-DOS,可以把shell脚本视为功能强大的.BAT文件。由于它们很重要,第11章将专门介绍shell脚本。

随着你阅读本书并积累经验,你将逐渐掌握使用shell操作命令的知识。关于shell,最棒的一点是,如果你犯了错误,可以轻松地看到你输入了什么来找出问题所在,然后重试。

有许多不同的Unix shell,但它们都源自Bourne shell(/bin/sh),这是贝尔实验室为早期Unix版本开发的标准shell。每个Unix系统都需要一个Bourne shell版本才能正常运行,正如你将在本书中看到的那样。

Linux使用增强版的Bourne shell,称为bash或“Bourne-again” shell。bash shell是大多数Linux发行版的默认shell,在Linux系统上,/bin/sh通常是指向bash的链接。你应该在运行本书中的示例时使用bash shell。

NOTE

如果你正在使用本章作为组织中Unix账户的指南,而你不是系统管理员,那么你的默认shell可能不是bash。你可以使用chsh更改自己的shell,或向系统管理员寻求帮助。

2.2 使用Shell

当你安装Linux时,应该至少创建一个普通用户作为你的个人账户。在本章中,你应该以普通用户身份登录。

2.2.1 Shell窗口

登录后,打开一个shell窗口(通常称为终端)。在GNOME或KDE等图形界面中,最简单的方法是打开一个终端应用程序,它会在一个新窗口中启动一个shell。打开shell后,它应该会在顶部显示一个提示符,通常以美元符号(,而在Fedora上,则是[name@host path]$,其中name是你的用户名,host是你的机器名,path`是你当前的工作目录(参见第2.4.1节)。如果你熟悉Windows,shell窗口类似于DOS命令提示符;在macOS中,终端应用程序本质上与Linux shell窗口相同。

本书包含许多你需要在shell提示符下键入的命令。它们都以一个单独的$符号表示shell提示符。例如,键入以下命令(只键入加粗部分,不包括$)并按回车:

$ echo Hello there.

NOTE

本书中的许多shell命令以#开头。这些命令需要以超级用户(root)身份运行,因此需要格外小心。运行它们的最佳实践是使用sudo,以提供一定的保护并留下一个日志,方便日后查找可能的错误。你将在第2.20节看到如何做到这一点。

现在输入以下命令:

$ cat /etc/passwd

此命令显示/etc/passwd系统信息文件的内容,然后返回shell提示符。暂时不用担心这个文件的作用;你将在第7章全面了解它。

命令通常以要运行的程序开头,后面可以跟参数,这些参数告诉程序对什么进行操作以及如何操作。这里,程序是cat,有一个参数/etc/passwd。许多参数是选项,用于修改程序的默认行为,通常以短划线(-)开头。稍后你将在讨论ls时看到这一点。然而,也有一些例外不遵循这种正常的命令结构,例如shell内置命令和临时使用环境变量。

2.2.2 cat

cat程序是Unix中最容易理解的程序之一;它只是输出一个或多个文件或其他输入源的内容。cat命令的一般语法如下:

$ cat file1 file2 ...

当您运行此命令时,cat会打印file1file2以及您指定为参数的其他文件(在上面的示例中用...表示)的内容,然后退出。该程序之所以叫作cat,是因为当它打印多个文件的内容时,它执行的是连接(concatenation)操作。有很多方法可以运行cat;让我们用它来探索Unix的I/O。

2.2.3 标准输入与标准输出

Unix进程使用I/O流来读写数据。进程从输入流读取数据,并向输出流写入数据。流非常灵活。例如,输入流的来源可以是文件、设备、终端窗口,甚至是另一个进程的输出流。

为了看看输入流是如何工作的,输入cat(不带任何参数)并按回车。这次你不会立即得到输出,也不会返回shell提示符,因为cat仍在运行。现在输入任何内容,并在每行末尾按回车。以这种方式使用时,cat命令会重复你输入的每一行。当你足够厌倦时,在空行上按CTRL-D来终止cat并返回shell提示符。

cat在这里采用交互式行为的原因与流有关。当你不指定输入文件名时,cat从Linux内核提供的标准输入流读取,而不是从连接到文件的流读取。在这种情况下,标准输入连接到运行cat的终端。

NOTE

在空行上按CTRL-D会发送EOF(文件结束)消息,停止从终端的当前标准输入输入(并且通常会终止程序)。不要将其与CTRL-C混淆,后者通常会无条件终止程序,无论其输入或输出如何。

标准输出类似。内核为每个进程提供一个标准输出流,进程可以向其写入输出。cat命令总是将其输出写入标准输出。当你在终端中运行cat时,标准输出连接到该终端,所以你就在那里看到了输出。

标准输入和输出通常缩写为stdin和stdout。许多命令的操作方式与cat类似;如果你没有指定输入文件,命令就从stdin读取。输出则有点不同。有些程序(如cat)只将输出发送到stdout,但其他程序可以选择直接将输出发送到文件。

还有第三个标准I/O流,称为标准错误(stderr)。你将在第2.14.1节中看到它。

标准流的最佳特性之一是,你可以轻松地操纵它们,以便从终端以外的地方读写,就像你在第2.14节中将学到的那样。特别是,你将学习如何将流连接到文件和其他进程。

2.3 基本命令

现在让我们看看更多的Unix命令。以下大多数程序可以接受多个参数,有些程序拥有极其丰富的选项和格式,以至于完整列出毫无意义。以下是一个简化的基本命令列表;你现在不需要所有的细节。

2.3.1 ls

ls命令列出目录的内容。默认情况下列出当前目录,但你可以添加任何目录或文件作为参数,并且有很多有用的选项。例如,使用ls -l进行详细(长格式)列表,使用ls -F显示文件类型信息。下面是一个长格式列表示例;它包括文件的所有者(第3列)、组(第4列)、文件大小(第5列)以及修改日期/时间(第5列和文件名之间):

$ ls -l
total 3616
-rw-r--r-- 1 juser users 3804    May 28 10:40  abusive.c
-rw-r--r-- 1 juser users 4165    Aug 13 10:01  battery.zip
-rw-r--r-- 1 juser users 131219  Aug 13 10:33  beav_1.40-13.tar.gz
-rw-r--r-- 1 juser users 6255    May 20 14:34  country.c
drwxr-xr-x 2 juser users 4096    Jul 17 20:00  cs335
-rwxr-xr-x 1 juser users 7108    Jun 16 13:05  dhry
-rw-r--r-- 1 juser users 11309   Aug 13 10:26  dhry.c
-rw-r--r-- 1 juser users 56      Jul  9 15:30  doit
drwxr-xr-x 6 juser users 4096    Feb 20 13:51  dw
drwxr-xr-x 3 juser users 4096    Jul  1 16:05  hough-stuff

关于此输出的第1列,你将在第2.17节了解更多。现在可以忽略第2列;它是文件的硬链接数,将在第4.6节中解释。

2.3.2 cp

最简单的形式是,cp复制文件。例如,要将file1复制到file2,输入:

$ cp file1 file2

你还可以将文件复制到另一个目录,并保持该目录中的文件名不变:

$ cp file dir

要将多个文件复制到一个名为dir的目录(文件夹),可以尝试类似下面的示例,它复制三个文件:

$ cp file1 file2 file3 dir

2.3.3 mv

mv(移动)命令的工作方式与cp非常相似。最简单的形式是,它重命名文件。例如,要将file1重命名为file2,输入:

$ mv file1 file2

你还可以使用mv将文件移动到其他目录,方式与cp相同。

2.3.4 touch

touch命令可以创建一个文件。如果目标文件已存在,touch不会改变文件,但会更新文件的修改时间戳。例如,要创建一个空文件,输入:

$ touch file

然后对该文件运行ls -l。你应该会看到类似下面的输出,其中日期和时间指示你运行touch的时刻:

$ ls -l file
-rw-r--r-- 1 juser users 0  May 21 18:32  file

要查看时间戳更新,请至少等待一分钟,然后再次运行相同的touch命令。ls -l返回的时间戳将会更新。

2.3.5 rm

rm命令删除(移除)一个文件。删除文件后,它通常从系统中消失,并且通常无法恢复,除非从备份中还原。

$ rm file

2.3.6 echo

echo命令将其参数打印到标准输出:

$ echo Hello again.
Hello again.

echo命令对于查找shell通配符(如*)和变量(如$HOME)的展开非常有用,你将在本章后面遇到它们。

2.4 导航目录

Unix 目录层次结构始于 /,也称为根目录。目录分隔符是斜杠 (/),而不是反斜杠 (\)。根目录下有几个标准子目录,例如 /usr,你将在 2.19 节中学习。

当你引用一个文件或目录时,你需要指定一个路径(path)或路径名(pathname)。当路径以 / 开头(例如 /usr/lib)时,它是一个完整路径或绝对路径。

由两个点 (..) 标识的路径组件指定目录的父目录。例如,如果你在 /usr/lib 目录中工作,路径 .. 将指向 /usr。类似地,../bin 将指向 /usr/bin

一个点 (.) 表示当前目录;例如,如果你在 /usr/lib,路径 . 仍然是 /usr/lib,而 ./X11/usr/lib/X11。你不必经常使用 .,因为大多数命令在路径不以 / 开头时会默认使用当前目录(所以在前面的例子中,你可以直接使用 X11 而不是 ./X11)。

不以 / 开头的路径称为相对路径。大多数时候,你会使用相对路径名,因为你已经位于或接近你需要的目录。

现在你已经了解了基本的目录机制,下面是一些基本的目录命令。

2.4.1 cd

当前工作目录是进程(如 shell)当前所在的目录。除了大多数 Linux 发行版的默认 shell 提示符外,你还可以使用 pwd 命令查看当前目录,详见 2.5.3 节。

每个进程可以独立设置自己的当前工作目录。cd 命令更改 shell 的当前工作目录:

$ cd dir

如果省略 dir,shell 将返回到你的主目录,即你最初登录时所在的目录。某些程序用 ~ 符号(波浪号)表示你的主目录。

NOTE

cd 命令是 shell 内置命令。它不能作为独立程序工作,因为如果它作为子进程运行,它(通常)无法更改其父进程的当前工作目录。目前这看起来可能不是一个特别重要的区别,但在某些时候,了解这一事实可以消除困惑。

2.4.2 mkdir

mkdir 命令创建一个新目录 dir

$ mkdir dir

2.4.3 rmdir

rmdir 命令删除目录 dir

$ rmdir dir

如果 dir 不为空,则该命令失败。但是,如果你不耐烦,可能不想先费力删除 dir 内的所有文件和子目录。你可以使用 rm -r dir 删除目录及其内容,但请小心!这是少数几个可能造成严重损害的命令之一,尤其是当你以超级用户身份运行它时。-r 选项指定递归删除,即反复删除 dir 内的所有内容。不要将 -r 标志与通配符(如星号 *)一起使用。最重要的是,在运行之前务必仔细检查你的命令。

2.4.4 Shell 通配符(“Wildcards”)

Shell 可以将简单模式与文件名和目录名匹配,这个过程称为通配符匹配(globbing)。这类似于其他系统中的通配符概念。最简单的通配符字符是 *,它告诉 shell 匹配任意数量的任意字符。例如,以下命令打印当前目录中的文件列表:

$ echo *

Shell 将包含通配符的参数与文件名进行匹配,用文件名替换这些参数,然后运行修改后的命令行。这种替换称为展开(expansion),因为 shell 将所有匹配的文件名替换为简化表达式。以下是使用 * 展开文件名的一些方式:

  • at* 展开为所有以 at 开头的文件名。
  • *at 展开为所有以 at 结尾的文件名。
  • *at* 展开为所有包含 at 的文件名。

如果没有文件匹配通配符,bash shell 将不执行任何展开,命令将使用文字字符(如 *)运行。例如,尝试一个命令,如 echo *dfkdsafh

NOTE

如果你习惯使用 Windows 命令提示符,你可能会本能地输入 *.* 来匹配所有文件。现在打破这个习惯。在 Linux 和其他 Unix 版本中,你必须使用 * 来匹配所有文件。在 Unix shell 中,*.* 只匹配其名称中包含点 (.) 字符的文件和目录。Unix 文件名不需要扩展名,而且通常不带扩展名。

另一个 shell 通配符字符,问号 (?),指示 shell 匹配恰好一个任意字符。例如,b?at 匹配 boatbrat

如果你不希望 shell 展开命令中的通配符,请将通配符括在单引号 ('') 中。例如,命令 echo '*' 打印一个星号。你会发现这对于下一节描述的一些命令(如 grepfind)非常方便。(你将在 11.2 节中了解更多关于引用的内容。)

NOTE

重要的是要记住,shell 在运行命令之前执行展开,并且仅在那时。因此,如果一个 * 在没有展开的情况下到达命令,shell 不会再对它做任何事情;由该命令自行决定如何处理它。

Shell 的模式匹配能力还有更多内容,但你现在需要知道的是 *?。2.7 节将描述通配符在以点开头的那些奇特文件上的行为。

2.5 中级命令

本节描述最基本的中级 Unix 命令。

2.5.1 grep

grep 命令从文件或输入流中打印与表达式匹配的行。例如,要打印 /etc/passwd 文件中包含文本 root 的行,请输入:

$ grep root /etc/passwd

当同时对多个文件进行操作时,grep 命令非常方便,因为它会打印文件名以及匹配的行。例如,如果你想检查 /etc 中每个包含单词 root 的文件,你可以使用以下命令:

$ grep root /etc/*

grep 最重要的两个选项是 -i(用于不区分大小写的匹配)和 -v(反转搜索——即打印所有不匹配的行)。还有一个更强大的变体叫 egrep(它只是 grep -E 的同义词)。

grep 理解正则表达式,这些模式源于计算机科学理论,在 Unix 实用程序中非常常见。正则表达式比通配符模式更强大,并且具有不同的语法。关于正则表达式,有三件重要的事情要记住:

  • .* 匹配任意数量(包括零个)的字符(类似于通配符中的 *)。
  • .+ 匹配一个或多个字符。
  • . 匹配恰好一个任意字符。

NOTE

grep(1) 手册页包含正则表达式的详细描述,但可能有点难以阅读。要了解更多,你可以阅读 Jeffrey E. F. Friedl 所著的《精通正则表达式》第 3 版(O’Reilly,2006),或参见 Tom Christensen 等人所著的《Perl 编程》第 4 版(O’Reilly,2012)中关于正则表达式的章节。如果你喜欢数学,并且对正则表达式的来源感兴趣,请查阅 Jeffrey Ullman 和 John Hopcroft 所著的《自动机理论、语言和计算导论》第 3 版(Prentice Hall,2006)。

2.5.2 less

当文件非常大,或者命令的输出很长并滚动到屏幕顶部之外时,less 命令就派上用场了。

要逐页浏览像 /usr/share/dict/words 这样的大文件,可以使用命令 less /usr/share/dict/words。运行 less 时,你将看到文件内容一次一屏。按空格键向前翻页,按 b(小写)向后跳一屏。要退出,按 q

NOTE

less 命令是较老程序 more 的增强版本。Linux 桌面和服务器拥有 less,但在许多嵌入式系统和其他 Unix 系统上,它并非标准配置。如果你遇到无法使用 less 的情况,可以尝试 more

你还可以在 less 内部搜索文本。例如,要向前搜索一个单词,可以输入 /word;要向后搜索,可以使用 ?word。找到匹配项后,按 n 继续搜索。

正如你将在 2.14 节中学到的,你可以将几乎任何程序的标准输出直接发送到另一个程序的标准输入。当你有一个输出很多、需要筛选的命令,并且希望使用 less 之类的程序查看输出时,这非常有用。以下是将 grep 命令的输出发送到 less 的示例:

$ grep ie /usr/share/dict/words | less

自己尝试一下这个命令。你可能会发现 less 还有许多类似的用途。

2.5.3 pwd

pwd(打印工作目录)程序仅输出当前工作目录的名称。你可能会想,既然大多数 Linux 发行版在用户帐户的提示符中包含了当前工作目录,为什么还需要它?有两个原因。

首先,并非所有提示符都包含当前工作目录,尤其是因为你可能希望在自己的提示符中将其去除,因为它占用太多空间。如果这样做,你就需要 pwd

其次,你将在 2.17.2 节中学到的符号链接有时可能会掩盖当前工作目录的真实完整路径。使用 pwd -P 可以消除这种混淆。

2.5.4 diff

要查看两个文本文件之间的差异,请使用 diff

$ diff file1 file2

有几个选项可以控制输出格式,默认输出格式通常对人类来说最易理解。但是,大多数程序员在需要将输出发送给其他人时更喜欢 diff -u 的输出,因为自动化工具更容易处理这种格式。

2.5.5 file

如果你看到一个文件但不确定其格式,请尝试使用 file 命令查看系统是否能猜测出来:

$ file file

你可能会惊讶于这个看似无害的命令能做多少事情。

2.5.6 findlocate

当你确定某个文件位于某个目录树中的某处,但就是不知道具体位置时,会非常令人沮丧。运行 finddir 中查找 file,方法如下:

$ find dir -name file -print

像本节中的大多数程序一样,find 能够做一些高级的事情。不过,在你熟练掌握这里显示的形式并理解为什么需要 -name-print 选项之前,不要尝试诸如 -exec 之类的选项。find 命令接受特殊的模式匹配字符,如 *,但你必须将它们括在单引号 ('*') 中,以保护特殊字符免受 shell 自身通配符功能的影响。(回想一下 2.4.4 节,shell 在运行命令之前会展开通配符。)

大多数系统还有一个 locate 命令用于查找文件。locate 不是实时搜索文件,而是搜索系统定期建立的索引。使用 locate 搜索比 find 快得多,但如果你要查找的文件比索引新,locate 将找不到它。

2.5.7 headtail

headtail 命令允许你快速查看文件或数据流的一部分。例如,head /etc/passwd 显示密码文件的前 10 行,tail /etc/passwd 显示最后 10 行。

要更改显示的行数,请使用 -n 选项,其中 n 是你希望看到的行数(例如,head -5 /etc/passwd)。要从第 n 行开始打印,使用 tail +n

2.5.8 sort

sort 命令可以快速将文本文件的行按字母数字顺序排列。如果文件的行以数字开头,并且你希望按数字顺序排序,请使用 -n 选项。-r 选项反转排序顺序。

2.6 更改密码和 Shell

使用 passwd 命令更改密码。系统会先询问旧密码,然后两次提示输入新密码。

最佳密码通常是容易记忆的长“无意义”句子。密码越长(字符长度),安全性越好;尽量使用 16 个字符或更多。(在很早以前,可用字符数有限,因此建议添加特殊字符等。)

可以使用 chsh 命令更改 shell(例如改为 zsh、ksh 或 tcsh),但请记住,本书假设你正在运行 bash,因此如果更改了 shell,部分示例可能无法正常工作。


2.7 点文件

如果你尚未进入主目录,请先切换,输入 ls 查看内容,然后运行 ls -a。你看到输出中的差异了吗?当运行不带 -als 时,你不会看到被称为 点文件 的配置文件。这些是名称以点(.)开头的文件和目录。常见的点文件有 .bashrc.login,还有像 .ssh 这样的点目录。

点文件或目录本身并无特殊之处。默认情况下,某些程序不会显示它们,这样在列出主目录内容时就不会看到一片混乱。例如,ls 除非使用 -a 选项,否则不会列出点文件。此外,shell 的通配符(globs)也不会匹配点文件,除非你明确使用像 .* 这样的模式。

NOTE

使用通配符 .* 可能会遇到问题,因为它会匹配 ...(当前目录和父目录)。你可能希望使用类似 .[^.]*.??* 的模式来获取除当前和父目录之外的所有点文件。


2.8 环境变量与 Shell 变量

Shell 可以存储临时变量,称为 shell 变量,其中包含文本字符串的值。Shell 变量在脚本中用于跟踪值非常有用,并且一些 shell 变量控制 shell 的行为方式。(例如,bash shell 在显示提示符之前会读取 PS1 变量。)

要给 shell 变量赋值,使用等号(=)。下面是一个简单示例:

$ STUFF=blah

上述示例将变量名 STUFF 的值设置为 blah。要访问此变量,请使用 $STUFF(例如,尝试运行 echo $STUFF)。你将在第 11 章中了解 shell 变量的许多用途。

NOTE

赋值时不要在 = 两侧加空格。

环境变量 类似于 shell 变量,但它不特定于 shell。Unix 系统上的所有进程都有环境变量存储。环境变量与 shell 变量的主要区别在于,操作系统会将所有 shell 的环境变量传递给 shell 运行的子进程,而 shell 变量则无法在你运行的命令中被访问。

使用 shell 的 export 命令来分配环境变量。例如,如果你想把 $STUFF 这个 shell 变量变成环境变量,可以这样做:

$ STUFF=blah
$ export STUFF

因为子进程会从父进程继承环境变量,所以许多程序会读取它们来进行配置和选项设置。例如,你可以将常用的 less 命令行选项放入 LESS 环境变量中,这样运行 less 时它就会自动使用这些选项。(许多手册页中都有一个名为 ENVIRONMENT 的部分,描述这些变量。)


2.9 命令路径

PATH 是一个特殊的环境变量,包含 命令路径(或简称为 路径),即 shell 在查找命令时要搜索的系统目录列表。例如,当运行 ls 时,shell 会在 PATH 列出的目录中搜索 ls 程序。如果路径中多个目录包含同名的程序,shell 会运行第一个匹配的程序。

运行 echo $PATH,你会看到路径组件由冒号(:)分隔。例如:

$ echo $PATH
/usr/local/bin:/usr/bin:/bin

要告诉 shell 在更多位置查找程序,请修改 PATH 环境变量。例如,使用以下命令可以将目录 dir 添加到路径的开头,使 shell 在查找其他 PATH 目录之前先查找 dir

$ PATH=dir:$PATH

或者,你可以将目录名追加到 PATH 变量的末尾,使 shell 最后查找 dir

$ PATH=$PATH:dir

WARNING

如果在修改路径时误输入 $PATH,你可能会意外清除整个路径。如果发生这种情况,不要惊慌!损坏并非永久性的;只需启动一个新 shell 即可。(如果想要永久生效,需要在编辑某个配置文件时误输入,即使那样也不难纠正。)恢复正常的最简单方法是关闭当前终端窗口并打开一个新窗口。


2.10 特殊字符

在与人讨论 Linux 时,你应该了解一些特殊字符的名称。如果你对此感兴趣,可以参阅“Jargon File”(http://www.catb.org/jargon/html/)或其印刷版《The New Hacker’s Dictionary》(第 3 版,Eric S. Raymond 著,MIT Press,1996)。

表 2-1 描述了一些选定的特殊字符,其中许多你已经在本章中见过。某些实用工具(如 Perl 编程语言)几乎使用了所有这些特殊字符!(请注意,这些是美式英语的名称。)

表 2-1:特殊字符

字符名称用途
*星号正则表达式,通配符
.当前目录,文件/主机名分隔符
!叹号否定,命令历史
``竖线
/斜杠目录分隔符,搜索命令
\反斜杠字面量,宏(从不用于目录)
$美元符号变量,行尾
'单引号字面字符串
`反引号命令替换
"双引号半字面字符串
^脱字符否定,行首
~波浪号否定,目录快捷方式
#井号注释,预处理器,替换
[ ]方括号范围
{ }花括号语句块,范围
_下划线当空格不被需要或不允许时,或自动补全算法混淆时,作为空格的廉价替代品

NOTE

你经常会看到控制字符用脱字符标记;例如,^C 表示 CTRL-C。


2.11 命令行编辑

在操作 shell 时,你会注意到可以使用左右箭头键编辑命令行,并使用上下箭头翻阅之前的命令。这在大多数 Linux 系统上是标准功能。

然而,建议你忘记箭头键,改用控制键组合。如果你学会了表 2-2 中列出的按键,你会发现自己在许多使用这些标准按键的 Unix 程序中能更好地输入文本。

表 2-2:命令行按键

按键动作
CTRL-B光标左移
CTRL-F光标右移
CTRL-P查看上一条命令(或光标上移)
CTRL-N查看下一条命令(或光标下移)
CTRL-A光标移到行首
CTRL-E光标移到行尾
CTRL-W删除前一个单词
CTRL-U从光标删除到行首
CTRL-K从光标删除到行尾
CTRL-Y粘贴已删除的文本(例如,从 CTRL-U 删除的)

2.12 文本编辑器

说到编辑,是时候学习一个编辑器了。要真正掌握 Unix,你必须能够在不破坏文件的情况下编辑文本文件。系统的大部分部分使用纯文本配置文件(例如 /etc 中的文件)。编辑文件并不困难,但你会经常这样做,因此需要一个强大的工具。

你应该尝试学习两个事实上的标准 Unix 文本编辑器之一:viEmacs。大多数 Unix 高手对其编辑器的选择都有宗教般的信仰,但不要听他们的。只需为自己选择。如果你选择一个符合你工作方式的编辑器,你会发现学习起来更容易。基本上,选择归结为以下两点:

  • 如果你想要一个几乎无所不能、拥有广泛在线帮助,并且不介意为此多打一些字的编辑器,可以尝试 Emacs。
  • 如果速度至上,试试 vi;它用起来有点像电子游戏。

《Learning the vi and Vim Editors: Unix Text Processing》(第 7 版,Arnold Robbins、Elbert Hannah 和 Linda Lamb 著,O’Reilly,2008)可以告诉你关于 vi 你需要知道的一切。对于 Emacs,请使用在线教程:启动 Emacs,按 CTRL-H,然后按 T。或者阅读《GNU Emacs Manual》(第 18 版,Richard M. Stallman 著,Free Software Foundation,2018)。

你可能会在刚开始时被诱惑去尝试更友好的编辑器,比如 nano、Pico 或众多 GUI 编辑器之一,但如果你倾向于对自己使用的第一个工具形成习惯,那么不应该走这条路。

NOTE

编辑文本是你第一次开始看到终端和 GUI 之间区别的地方。像 vi 这样的编辑器在终端窗口内运行,使用标准终端 I/O 接口。GUI 编辑器则启动自己的窗口并呈现自己的界面,独立于终端。Emacs 默认在 GUI 中运行,但也可以在终端窗口中运行。


2.13 获取在线帮助

Linux 系统拥有丰富的文档。对于基本命令,手册页(或 man 页)会告诉你需要知道的信息。例如,要查看 ls 命令的手册页,运行 man 如下:

$ man ls

大多数手册页主要侧重于参考信息,可能附带一些示例和交叉引用,但仅此而已。不要期望它是教程,也不要期待引人入胜的文学风格。

当程序有很多选项时,手册页通常会按某种系统方式列出选项(例如按字母顺序),但不会告诉你哪些是重要的。如果你有耐心,通常可以在手册页中找到你需要的信息。如果你没耐心,就问朋友——或者花钱雇一个人做你的朋友,然后问他。

要按关键字搜索手册页,使用 -k 选项:

$ man -k 关键字

如果你不太清楚所需命令的名称,这很有帮助。例如,如果你在寻找排序命令,可以运行:

$ man -k sort
--snip--
comm (1)          - 逐行比较两个已排序的文件
qsort (3)         - 对数组排序
sort (1)          - 对文本文件的行排序
sortm (1)         - 对消息排序
tsort (1)         - 执行拓扑排序
--snip--

输出包括手册页名称、手册节号(见下文)以及手册页内容的简短描述。

NOTE

如果你对前面几节中描述的命令有任何疑问,也许可以通过 man 命令找到答案。

手册页通过编号的节(section)引用。当某人提到一个手册页时,他们通常会将节号放在名称后面的括号中,如 ping(8)。表 2-3 列出了这些节及其编号。

表 2-3:在线手册节

节号描述
1用户命令
2内核系统调用
3高级 Unix 编程库文档
4设备接口和驱动程序信息
5文件描述(系统配置文件)
6游戏
7文件格式、约定和编码(ASCII、后缀等)
8系统命令和服务器

节 1、5、7 和 8 应该是对本书的良好补充。节 4 可能只有边际用途,而节 6 如果内容再多一些就更好了。如果你不是程序员,可能无法使用节 3,但或许你能理解其中的部分材料。

第2章:基本命令与目录层次

Section 2 once you’ve read more about system calls in this book.

一些常见术语在多个章节中都有匹配的手册页面。默认情况下,man 显示它找到的第一个页面。你可以通过章节来选择手册页面。例如,要阅读 /etc/passwd 文件描述(而不是 passwd 命令),你可以在页面名称前插入章节号,如下所示:

$ man 5 passwd

28 第2章

手册页面涵盖了基本内容,但还有更多获取在线帮助的方式(除了搜索互联网)。如果你只是查找某个命令的特定选项,可以尝试输入命令名后跟 --help-h(选项因命令而异)。你可能会收到大量输出(如 ls --help),也可能正好找到你要找的内容。

不久前,GNU 项目决定不再喜欢手册页面,转而采用另一种称为 info(或 texinfo)的格式。这些文档通常比典型的手册页面更深入,但也可能更复杂。要访问 info 手册,使用 info 加命令名:

$ info command

如果你不喜欢 info 阅读器,可以将输出发送到 less(只需添加 | less)。

一些软件包将其可用文档直接放入 /usr/share/doc,而忽略 maninfo 等在线手册系统。如果你在查找文档,请在你的系统上查看此目录——当然,也可以搜索互联网。

2.14 Shell 输入与输出

既然你已经熟悉了基本的 Unix 命令、文件和目录,现在可以学习如何重定向标准输入和输出了。让我们从标准输出开始。

要将命令的输出发送到文件而不是终端,请使用 > 重定向字符:

$ command > file

如果文件尚不存在,shell 会创建它。如果文件已存在,shell 会先擦除(覆盖)原文件。(某些 shell 有防止覆盖的参数。例如,你可以在 bash 中输入 set -C 来避免覆盖。)

你可以使用 >> 重定向语法将输出追加到文件,而不是覆盖:

$ command >> file

这是在执行一系列相关命令时将输出收集到同一位置的好方法。

要将一个命令的标准输出发送到另一个命令的标准输入,请使用管道符(|)。要了解其工作原理,请尝试以下两个命令:

$ head /proc/cpuinfo
$ head /proc/cpuinfo | tr a-z A-Z

Basic Commands and Directory Hierarchy 29

你可以将输出发送到任意多个管道命令;只需在每个附加命令前添加管道符。

2.14.1 标准错误

有时,你重定向了标准输出,但发现程序仍然在终端上打印了一些内容。这就是标准错误(stderr);它是用于诊断和调试的额外输出流。例如,以下命令会产生一个错误:

$ ls /fffffffff > f

完成后,f 应该是空的,但你仍然会在终端上看到以下错误消息(作为标准错误):

ls: cannot access /fffffffff: No such file or directory

如果需要,你也可以重定向标准错误。例如,要将标准输出发送到 f,标准错误发送到 e,请使用 2> 语法,如下所示:

$ ls /fffffffff > f 2> e

数字 2 指定 shell 要修改的流 ID。流 ID 1 是标准输出(默认),2 是标准错误。

你也可以使用 >& 表示法将标准错误发送到与 stdout 相同的位置。例如,要将标准输出和标准错误都发送到名为 f 的文件,请尝试:

$ ls /fffffffff > f 2>&1

2.14.2 标准输入重定向

要将文件导入程序的标准输入,请使用 < 运算符:

$ head < /proc/cpuinfo

偶尔你会遇到需要这种重定向的程序,但由于大多数 Unix 命令都接受文件名作为参数,这种情况并不常见。例如,上面的命令也可以写成 head /proc/cpuinfo

2.15 理解错误消息

当你在 Linux 等类 Unix 系统上遇到问题时,必须阅读错误消息。与其他操作系统的消息不同,Unix 错误通常能准确告诉你出了什么问题。

30 第2章

2.15.1 Unix 错误消息的结构

大多数 Unix 程序生成并报告相同的基本错误消息,但任何两个程序的输出之间都可能存在细微差异。这是一个你肯定会以某种形式遇到的例子:

$ ls /dsafsda
ls: cannot access /dsafsda: No such file or directory

这条消息包含三个部分:

  • 程序名称 ls。某些程序会省略此标识信息,这在编写 shell 脚本时可能令人烦恼,但也不是什么大问题。
  • 文件名 /dsafsda,这是一个更具体的信息。此路径存在问题。
  • 错误 No such file or directory 指明了文件名的问题。

综合起来,你可以得到类似“ls 试图打开 /dsafsda,但因它不存在而失败”的信息。这似乎很明显,但当你运行一个包含错误命令且名称不同的 shell 脚本时,这些消息可能会有点令人困惑。

在排查错误时,始终先处理第一个错误。有些程序在报告一系列其他问题之前会报告它们无法做任何事。例如,假设你运行一个虚构的程序 scumd,并看到此错误消息:

scumd: cannot access /etc/scumd/config: No such file or directory

紧接着是一大堆其他错误消息,看起来像一场彻底的灾难。不要被这些其他错误分散注意力。你可能只需创建 /etc/scumd/config

NOTE

不要将错误消息与警告消息混淆。警告通常看起来像错误,但包含单词 warning。警告通常意味着某处有问题,但程序会尝试继续运行。要修复警告消息中指出的问题,你可能需要先找到进程并将其终止,然后再做其他事情。(你将在第 2.16 节学习列出和终止进程。)

2.15.2 常见错误

你在 Unix 程序中遇到的大多数错误都源于文件和进程可能出现的问题。其中不少错误直接来自于内核系统调用遇到的条件,因此通过查看它们,你可以了解内核如何将问题传回进程。

Basic Commands and Directory Hierarchy 31

No such file or directory

这是头号错误。你尝试访问一个不存在的文件。由于 Unix 文件 I/O 系统对文件和目录没有太多区分,因此此错误消息涵盖两种情况。当你尝试读取一个不存在的文件、切换到一个不存在的目录、或尝试写入一个不存在的目录中的文件时,都会遇到此错误。此错误也称为 ENOENT,是“Error NO ENtity”的缩写。

NOTE

如果你对系统调用感兴趣,这通常是 open() 返回 ENOENT 的结果。有关它可能遇到的错误的更多信息,请参见 open(2) 手册页面。

File exists

在这种情况下,你可能尝试创建一个已存在的文件。这在你尝试用与文件同名的名称创建目录时很常见。

Not a directory, Is a directory

这些消息在你试图将文件当作目录,或将目录当作文件使用时出现。例如:

$ touch a
$ touch a/b
touch: a/b: Not a directory

注意,错误消息仅适用于 a/b 中的 a 部分。遇到此问题时,你可能需要仔细查找被当作目录处理的路径组件。

No space left on device

你的磁盘空间已满。

Permission denied

当你尝试读取或写入没有权限访问的文件或目录(权限不足)时,会遇到此错误。当你尝试执行一个未设置执行位的文件(即使你可以读取它)时,也会出现此错误。你将在第 2.17 节了解更多关于权限的内容。

Operation not permitted

这通常发生在你试图终止不属于你的进程时。

Segmentation fault, Bus error

段错误本质上意味着你刚刚运行的程序的作者在某处出了错。程序试图访问它不允许接触的内存部分,操作系统将其终止。类似地,总线错误意味着程序试图以不应采用的方式访问某些内存。当你遇到这些错误之一时,你可能给程序提供了它未预期的输入。在极少数情况下,可能是内存硬件故障。

32 第2章

2.16 列出和操作进程

回顾第1章,进程是一个正在运行的程序。系统上的每个进程都有一个数字进程 ID(PID)。要快速列出正在运行的进程,只需在命令行上运行 ps。你应该会得到如下列表:

$ ps
  PID TTY STAT TIME COMMAND
  520 p0  S    0:00 -bash
  545  ?  S    3:59 /usr/X11R6/bin/ctwm -W
  548  ?  S    0:10 xclock -geometry -0-0
 2159 pd  SW   0:00 /usr/bin/vi lib/addresses
31956 p3  R    0:00 ps

字段含义如下:

  • PID   进程 ID。
  • TTY   进程所在的终端设备。稍后会详细介绍。
  • STAT   进程状态——即进程正在做什么以及其内存位于何处。例如,S 表示睡眠,R 表示运行。(有关所有符号的描述,请参见 ps(1) 手册页面。)
  • TIME   进程到目前为止已使用的 CPU 时间(分钟和秒)。换句话说,进程在处理器上运行指令所花费的总时间。请记住,由于进程不会持续运行,这不同于进程启动以来的时间(即“挂钟时间”)。
  • COMMAND   这一项似乎很明显是用于运行程序的命令,但请注意,进程可以将其字段从原始值更改为其他值。此外,shell 可以执行通配符扩展,该字段将显示扩展后的命令,而不是你在提示符下输入的内容。

NOTE

PIDs 对于系统上运行的每个进程都是唯一的。但是,进程终止后,内核最终可以重用该 PID 用于新进程。

2.16.1 ps 命令选项

ps 命令有许多选项。更令人困惑的是,你可以用三种不同的风格指定选项——Unix、BSD 和 GNU。许多人发现 BSD 风格最顺手(可能是因为它涉及更少的输入),因此本书将使用该风格。以下是一些最有用的选项组合:

  • ps x   显示所有你拥有的运行进程。
  • ps ax   显示系统上的所有进程,不仅仅是你拥有的。
  • ps u   包含更详细的进程信息。
  • ps w   显示完整的命令名称,而不仅仅是一行能容纳的内容。

与其他程序一样,你可以组合选项,例如 ps auxps auxw

要检查特定进程,将其 PID 添加到 ps 命令的参数列表中。例如,要检查当前 shell 进程,可以使用 ps u $$$$ 是一个 shell 变量,其值为当前 shell 的 PID)。你将在第 8 章找到关于管理命令 toplsof 的信息。即使你在做系统维护以外的事情,这些命令对于定位进程也很有用。

2.16.2 进程终止

要终止一个进程,你可以使用 kill 命令向它发送一个信号——内核发送给进程的消息。大多数情况下,你只需要这样做:

$ kill pid

信号有多种类型。上面使用的默认信号是 TERM(终止)。你可以通过给 kill 添加额外选项来发送不同的信号。例如,要冻结一个进程而不是终止它,可以使用 STOP 信号:

$ kill -STOP pid

已停止的进程仍保留在内存中,随时准备从停止的地方继续运行。使用 CONT 信号可以让进程继续运行:

$ kill -CONT pid

NOTE

使用 CTRL-C 终止当前终端中运行的进程,与使用 killINT(中断)信号结束该进程的效果相同。

内核在收到信号时,会给大多数进程一个机会进行清理(通过信号处理机制)。然而,有些进程可能选择对信号做出非终止的响应、在处理信号时卡住,或者干脆忽略它,因此你可能会发现尝试终止后进程仍在运行。如果发生这种情况,并且你真的需要杀死一个进程,最粗暴的方式是使用 KILL 信号。与其他信号不同,KILL 无法被忽略;实际上,操作系统甚至不给进程任何机会。内核直接终止该进程并强制将其从内存中移除。这种方法只能作为最后的手段使用。

你不应该不加选择地杀死进程,尤其是当你不知道它们在做什么时。这可能会搬起石头砸自己的脚。

你可能会看到其他用户使用数字而不是名称来配合 kill——例如,kill -9 代替 kill -KILL。这是因为内核使用数字来表示不同的信号;如果你知道要发送的信号编号,就可以这样使用 kill。运行 kill -l 可以获取信号编号到名称的映射。

2.16.3 作业控制

Shell 支持作业控制,这是一种通过多种按键和命令向程序发送 TSTP(类似于 STOP)和 CONT 信号的方式。这允许你挂起正在使用的程序并在它们之间切换。例如,你可以用 CTRL-Z 发送 TSTP 信号,然后通过输入 fg(移到前台)或 bg(移到后台;见下一节)重新启动该进程。但是,尽管它很有用,而且许多有经验的用户都习惯使用它,但作业控制并不是必需的,而且可能会让初学者感到困惑:用户常常会按 CTRL-Z 而不是 CTRL-C,然后忘记自己运行了什么,最终导致大量挂起的进程。

NOTE

要查看你是否在当前终端中意外挂起了任何进程,请运行 jobs 命令。

如果你想运行多个程序,请在单独的终端窗口中运行每个程序,将非交互式进程放入后台(如下一节所述),并学习使用 screentmux 工具。

2.16.4 后台进程

通常,当你在 shell 中运行一条 Unix 命令时,在程序完成执行之前你无法获得 shell 提示符。但是,你可以使用与号(&)将进程从 shell 中分离并放入“后台”;这样会立即返回提示符。例如,如果你有一个需要解压缩的大文件(你将在第 2.18 节中看到 gunzip),并且希望在它运行时做其他事情,可以运行如下命令:

$ gunzip file.gz &

Shell 应该会打印出新后台进程的 PID,然后提示符会立即返回,以便你可以继续工作。如果进程需要很长时间才能完成,它甚至可以在你退出登录后继续运行,这在你必须运行大量数值计算程序时尤其方便。如果进程在你退出登录或关闭终端窗口之前完成,shell 通常会根据你的设置通知你。

NOTE

如果你远程访问一台机器,并希望在退出登录时保持程序运行,你可能需要使用 nohup 命令;详情请参见其手册页。

运行后台进程的弊端在于,它们可能期望与标准输入交互(或者更糟,直接从终端读取)。如果程序在后台时试图从标准输入读取某些内容,它可能会冻结(尝试用 fg 将其调回前台)或终止。此外,如果程序向标准输出或标准错误写入,输出可能会出现在终端窗口中,而不考虑其中正在运行的任何其他内容,这意味着你在做其他工作时可能会收到意外的输出。

确保后台进程不会打扰你的最好方法是重定向其输出(以及可能的输入),如第 2.14 节所述。

如果来自后台进程的杂乱输出干扰了你,请学习如何重绘终端窗口的内容。bash shell 和大多数全屏交互式程序都支持 CTRL-L 来重绘整个屏幕。如果程序正在从标准输入读取,CTRL-R 通常会重绘当前行,但在错误的时间按下错误的序列可能会让你陷入比以前更糟的境地。例如,在 bash 提示符下输入 CTRL-R 会让你进入反向搜索模式(按 ESC 退出)。


2.17 文件模式与权限

每个 Unix 文件都有一组权限,决定你是否可以读取、写入或运行该文件。运行 ls -l 会显示这些权限。以下是这种显示的一个例子:

-rw-r--r-- 1 juser somegroup 7041 Mar 26 19:34 endnotes.html

文件的模式① 表示文件的权限和一些额外信息。模式有四个部分,如图 2-1 所示。

图 2-1:文件模式的各个部分

类型 用户权限 组权限 其他权限
- rw- r-- r--

NOTE

① 译注:原文为 “mode”,这里译为“模式”。

模式的第一个字符是文件类型。此位置的一个短横线(-)表示一个普通文件,意味着该文件没什么特别的;它只是二进制或文本数据。这是迄今为止最常见的文件类型。目录也很常见,由文件类型位置上的 d 表示。(第 3.1 节列出了其余的文件类型。)

文件模式的其余部分包含权限,这些权限分为三组:用户其他,按顺序排列。例如,例子中的 rw- 字符是用户权限,其后的 r-- 字符是组权限,最后的 r-- 字符是其他权限。

每组权限可以包含四种基本表示:

  • r 表示文件可读。
  • w 表示文件可写。
  • x 表示文件可执行(你可以将其作为程序运行)。
  • - 表示“无”(更具体地说,该组中该位置的权限未被授予)。

用户权限(第一组)与拥有该文件的用户相关。在前面的例子中,那是 juser。第二组,组权限,针对文件的组(例子中的 somegroup)。该组中的任何用户都可以利用这些权限。(使用 groups 命令查看你所在的组,更多信息请参见第 7.3.5 节。)

系统上的其他所有人根据第三组,即其他权限(有时称为世界权限)来获得访问权限。

NOTE

每个读、写和执行权限位置有时被称为权限位,因为底层操作系统中表示它们的方式是一系列位。因此,你可能会听到人们将权限的某些部分称为“读位”。

有些可执行文件在用户权限列表中有一个 s 而不是 x。这表示该可执行文件是 setuid 的,意味着当你执行该程序时,它运行时如同文件所有者是用户而不是你。许多程序使用这个 setuid 位来以 root 身份运行,以便获得更改系统文件所需的特权。其中一个例子是 passwd 程序,它需要更改 /etc/passwd 文件。

2.17.1 修改权限

要更改文件或目录的权限,请使用 chmod 命令。首先,选择你要更改的权限集,然后选择要更改的位。例如,要向文件添加组(g)和世界(o,表示“其他”)的读(r)权限,你可以运行以下两个命令:

$ chmod g+r file
$ chmod o+r file

或者一次性完成:

$ chmod go+r file

要移除这些权限,请使用 go-r 而不是 go+r

NOTE

显然,你不应该使文件对世界可写,因为这样做会使你系统上的任何人都能更改它们。但这也会允许任何连接到互联网的人更改它们吗?可能不会,除非你的系统存在网络安全漏洞。在这种情况下,文件权限也帮不了你。

你有时可能会看到人们用数字更改权限,例如:

$ chmod 644 file

这称为绝对更改,因为它一次性设置了所有权限位。要理解这是如何工作的,你需要知道如何以八进制形式表示权限位(每个数字代表一个基数为 8 的数,范围 0 到 7,并对应一个权限集)。参见 chmod(1) 手册页或信息手册以了解更多。

如果你喜欢使用绝对模式,其实并不需要知道如何构造它们;只需记住你最常用的模式即可。表 2-4 列出了最常见的几种。

表 2-4:绝对权限模式

模式含义用于
644user: 读/写;group, other: 读文件
600user: 读/写;group, other: 无文件
755user: 读/写/执行;group, other: 读/执行目录、程序
700user: 读/写/执行;group, other: 无目录、程序
711user: 读/写/执行;group, other: 执行目录

目录也有权限。如果目录可读,你可以列出其内容;但只有目录可执行时,你才能访问其中的文件。在大多数情况下,你需要两者兼有;人们在设置目录权限时常犯的一个错误是,在使用绝对模式时意外移除了执行权限。

最后,你可以使用 shell 命令 umask 指定一组默认权限,它会将预定义的权限集应用于你创建的任何新文件。一般来说,如果你希望所有人都能看到你创建的所有文件和目录,请使用 umask 022;如果你不希望这样,请使用 umask 077。如果你希望所需的权限掩码应用于新的窗口和以后的会话,则需要在你的一个启动文件中放置带有所需模式的 umask 命令,如第 13 章所述。

2.17.2 使用符号链接

符号链接是一种指向另一个文件或目录的文件,实际上创建了一个别名(类似于 Windows 中的快捷方式)。符号链接提供了快速访问深层目录路径的途径。

在长格式目录列表中,符号链接看起来像这样(注意文件模式中的文件类型 l):

lrwxrwxrwx 1 ruser users  11 Feb 27 13:52  somedir -> /home/origdir

如果你试图访问此目录中的 somedir,系统会将其替换为 /home/origdir。符号链接只是指向其他名称的文件名。它们的名称以及它们指向的路径不必有任何实际含义。在上面的例子中,/home/origdir 甚至不需要存在。

事实上,如果 /home/origdir 不存在,任何访问 somedir 的程序都会返回一个错误,报告 somedir 不存在(除了 ls somedir 这个命令——它愚蠢地告诉你 somedir 就是 somedir)。这可能会令人困惑,因为你明明可以看到名为 somedir 的东西就在眼前。

符号链接造成困扰的方式不止这一种。另一个问题是,你不能仅通过查看链接的名称就判断出链接目标的特点;你必须跟随链接才能看到它指向的是文件还是目录。你的系统中可能还有指向其他链接的链接,这称为链式符号链接,在追踪它们时可能会很麻烦。

要创建从 targetlinkname 的符号链接,使用 ln -s 命令,如下所示:

$ ln -s target linkname

linkname 参数是符号链接的名称,target 参数是链接指向的文件或目录的路径,-s 标志指定这是一个符号链接(请参阅下面的警告)。

创建符号链接时,请务必在运行命令前仔细检查两遍,因为可能会出错。例如,如果你意外颠倒了参数的顺序(ln -s linkname target),并且 linkname 是一个已存在的目录,那就会遇到麻烦。如果是这种情况(而且很常见),ln 会在 linkname 内部创建一个名为 target 的链接,并且除非 linkname 是完整路径,否则该链接将指向自身。如果创建指向目录的符号链接时出现问题,请检查该目录中是否有错误的符号链接并将其删除。

当你不知道符号链接存在时,它们也可能带来麻烦。例如,你很容易编辑你认为是一个文件的副本,但实际上它是指向原始文件的符号链接。

WARNING

创建符号链接时不要忘记 -s 选项。没有它,ln 将创建硬链接,为单个文件提供一个额外的真实文件名。新文件名具有与旧文件名相同的状态;它直接指向(链接到)文件数据,而不是像符号链接那样指向另一个文件名。硬链接可能比符号链接更令人困惑。除非你理解了第 4.6 节的内容,否则请避免使用它们。

尽管有这么多关于符号链接的警告,你可能想知道为什么还有人要使用它们。事实证明,它们为组织文件提供的强大功能以及轻松修补小问题的能力,远远超过了它们的缺陷。一个常见的用例是:某个程序期望找到一个特定的文件或目录,而该文件或目录已经存在于你系统的其他位置。你不想复制一份,如果你无法更改程序,只需创建一个从该位置到实际文件或目录的符号链接即可。

2.18 归档与压缩文件

现在你已经了解了文件、权限和可能的错误,你需要掌握 gziptar 这两个用于压缩和打包文件与目录的常用工具。

2.18.1 gzip

程序 gzip(GNU Zip)是当前标准的 Unix 压缩程序之一。以 .gz 结尾的文件是 GNU Zip 归档文件。使用 gunzip file.gz 解压缩 file.gz 并移除后缀;若要再次压缩该文件,请使用 gzip file

2.18.2 tar

与其他操作系统中的 ZIP 程序不同,gzip 不会创建文件归档;也就是说,它不会将多个文件和目录打包成一个文件。要创建归档,请改用 tar

$ tar cvf archive.tar file1 file2 ...

tar 创建的归档通常带有 .tar 后缀(这是惯例,并非必需)。例如,在上一条命令中,file1file2 等是你要归档到 archive.tar 中的文件和目录的名称。c 标志激活创建模式。vf 标志有更具体的角色。

v 标志激活详细诊断输出,使 tar 在遇到归档中的文件和目录时打印它们的名称。添加另一个 v 会使 tar 打印详细信息,如文件大小和权限。如果你不希望 tar 报告其操作,可以省略 v 标志。

f 标志表示文件选项。f 标志后的下一个命令行参数必须是 tar 要创建的归档文件(在上面的例子中是 archive.tar)。除了磁带驱动器外,你始终必须使用此选项并后跟一个文件名。要使用标准输入或输出,请将文件名设置为短划线(-)。

解包 .tar 文件

要使用 tar 解包 .tar 文件,请使用 x 标志:

$ tar xvf archive.tar

在此命令中,x 标志将 tar 置于提取(解包)模式。你可以通过在命令行末尾输入特定部分的名称来提取归档中的个别部分,但你必须知道它们的确切名称。(要准确了解,请使用下面描述的目录列表模式。)

NOTE

使用提取模式时,请记住 tar 在提取内容后不会删除已归档的 .tar 文件。

使用目录列表模式

在解包之前,通常最好使用 t 标志(而不是 x 标志)通过目录列表模式检查 .tar 文件的内容。此模式验证归档的基本完整性并打印其中所有文件的名称。如果你在解包前不测试归档,最终可能会将大量文件倾倒到当前目录中,清理起来非常困难。

当你使用 t 模式检查归档时,请验证所有内容是否都位于合理的目录结构中;即归档中的所有文件路径名应以同一目录开头。如果不确定,请创建一个临时目录,切换到该目录,然后提取。(如果归档没有制造混乱,你总是可以用 mv * .. 将文件移回。)

解包时,考虑使用 p 选项保留权限。在提取模式下使用它以覆盖你的 umask 并获取归档中指定的确切权限。当以超级用户身份操作时,p 选项是默认的。如果你在以超级用户解包归档时遇到权限和所有权问题,请确保你一直等到命令终止并重新获得 shell 提示符。尽管你可能只想提取归档的一小部分,但 tar 必须运行整个归档,并且你不能中断该过程,因为它只在检查完整个归档后才会设置权限。

请将本节中的所有 tar 选项和模式牢记在心。如果记不住,可以制作一些闪卡。这听起来像是小学生的做法,但避免在使用此命令时出现粗心错误非常重要。

2.18.3 压缩归档(.tar.gz)

许多初学者发现归档通常以压缩形式出现,文件名以 .tar.gz 结尾,这令人困惑。要解包压缩归档,请从右向左操作;先处理 .gz,再处理 .tar。例如,以下两条命令解压并解包 file.tar.gz

$ gunzip file.tar.gz
$ tar xvf file.tar

在刚开始时,一步一步来是可以的,先运行 gunzip 解压缩,然后运行 tar 验证和解包。要创建压缩归档,则反过来:先运行 tar,再运行 gzip。如果经常这样做,你很快就会记住归档和压缩过程是如何运作的。但即使不经常操作,你也会发现所有的输入变得多么繁琐,并开始寻找捷径。现在让我们来看看这些捷径。

2.18.4 zcat

刚才展示的方法并非在压缩归档上调用 tar 的最快或最高效的方式,而且它浪费磁盘空间和内核 I/O 时间。更好的方法是将归档和压缩功能与管道结合起来。例如,以下命令管道解包 file.tar.gz

$ zcat file.tar.gz | tar xvf -

zcat 命令等同于 gunzip -dc-d 选项解压缩,-c 选项将结果发送到标准输出(在本例中,发送给 tar 命令)。

由于使用 zcat 非常常见,Linux 自带的 tar 版本提供了一个快捷方式。你可以使用 z 选项自动对归档调用 gzip;这既适用于提取归档(使用 tarxt 模式),也适用于创建归档(使用 c)。例如,使用以下命令验证压缩归档:

$ tar ztvf file.tar.gz

但是,请记住使用快捷方式时实际上是在执行两个步骤。

NOTE

.tgz 文件等同于 .tar.gz 文件。这个后缀是为了适应 FAT(基于 MS-DOS 的)文件系统而设计的。

2.18.5 其他压缩工具

另外两个压缩程序是 xzbzip2,它们压缩后的文件分别以 .xz.bz2 结尾。虽然速度比 gzip 稍慢,但它们通常能更有效地压缩文本文件。对应的解压缩程序是 unxzbunzip2,两者的选项与它们的 gzip 对应版本非常接近,你无需学习新内容。

大多数 Linux 发行版都附带 zipunzip 程序,它们与 Windows 系统上的 ZIP 归档兼容。它们可以处理常见的 .zip 文件,以及以 .exe 结尾的自解压归档。但如果你遇到以 .Z 结尾的文件,那么你找到了一个由 compress 程序生成的遗物,它曾是 Unix 的标准。gunzip 程序可以解压这些文件,但 gzip 不会创建它们。

2.19 Linux 目录层次结构要点

既然你已经知道如何检查文件、切换目录和阅读手册页,就可以开始探索系统文件和目录了。Linux 目录结构的细节在 文件系统层次结构标准(FHS,https://refspecs.linuxfoundation.org/fhs.shtml)中有概述,但此处先进行简要说明即可。

图 2-2 提供了一个简化的层次结构概览,展示了 //usr/var 下的一些目录。注意 /usr 下的目录结构包含一些与 / 中相同的目录名。

/
├── bin/
├── dev/
├── etc/
├── usr/
├── home/
├── lib/
├── sbin/
├── tmp/
├── var/
│   ├── log/
│   └── tmp/
└── 以及 /usr 下的子目录:
    ├── bin/
    ├── lib/
    ├── man/
    ├── local/
    ├── sbin/
    └── share/

图 2-2:Linux 目录层次结构

以下是根目录中最重要的一些子目录:

  • /bin:包含准备好运行的程序(也称为可执行文件),包括大多数基本 Unix 命令,如 lscp/bin 中的程序大部分是二进制格式,由 C 编译器创建,但在现代系统上,有些是 shell 脚本。
  • /dev:包含设备文件。你将在第 3 章了解更多。
  • /etc:这个核心系统配置目录(发音为 EHT-see)包含用户密码、引导、设备、网络和其他设置文件。
  • /home:存放普通用户的主目录(个人目录)。大多数 Unix 安装符合此标准。
  • /lib:库(library)的缩写,存放可执行程序可以使用的代码(即库文件)。有两种类型的库:静态库和共享库。/lib 目录应当只包含共享库,但其他 lib 目录(如 /usr/lib)则包含两种类型以及其他辅助文件。(我们将在第 15 章更详细地讨论共享库。)
  • /proc:通过可浏览的目录和文件接口提供系统统计信息。Linux 上 /proc 子目录结构的大部分是独特的,但许多其他 Unix 变体也有类似功能。/proc 目录包含关于当前运行进程的信息以及一些内核参数。
  • /run:包含系统特定的运行时数据,包括某些进程 ID、套接字文件、状态记录,以及在许多情况下系统日志。这是根目录中相对较新的添加;在旧系统中,你可以在 /var/run 中找到它。在新系统中,/var/run 是指向 /run 的符号链接。
  • /sys:此目录与 /proc 类似,提供设备和系统接口。你将在第 3 章了解更多关于 /sys 的内容。
  • /sbin:系统可执行文件的位置。/sbin 目录中的程序与系统管理相关,因此普通用户通常不会在命令路径中包含 /sbin 组件。这里发现的许多实用程序如果不是以 root 身份运行,则无法正常工作。
  • /tmp:存储较小的临时文件的地方,这些文件不太重要。任何用户都可以读取和写入 /tmp,但用户可能没有权限访问其中其他用户的文件。许多程序将此目录用作工作区。如果某物极其重要,不要放在 /tmp 中,因为大多数发行版在机器启动时会清除 /tmp,有些甚至定期删除其旧文件。另外,不要让 /tmp 被垃圾填满,因为它的空间通常与某个关键位置(例如 / 的其余部分)共享。
  • /usr:虽然发音为 “user”,但这个子目录没有用户文件。相反,它包含一个大型目录层次结构,包括 Linux 系统的大部分内容。/usr 中的许多目录名与根目录中的相同(如 /usr/bin/usr/lib),并且它们保存相同类型的文件。(根目录不包含整个系统的原因主要是历史性的——过去是为了降低根目录的空间需求。)
  • /var:变量子目录,程序在此记录随时间变化的信息。系统日志、用户跟踪、缓存以及系统程序创建和管理的其他文件都在这里。(你会注意到这里有一个 /var/tmp 目录,但系统在启动时不会清除它。)

2.19.1 其他根子目录

根目录中还有其他一些有趣的子目录:

  • /boot:包含内核引导加载程序文件。这些文件仅涉及 Linux 启动过程的第一阶段,因此你不会在此目录中找到关于 Linux 如何启动其服务的信息。更多内容请参见第 5 章。
  • /media:可移动媒体(如闪存驱动器)的基本挂载点,在许多发行版中可找到。
  • /opt:可能包含额外的第三方软件。许多系统不使用 /opt

2.19.2 /usr 目录

/usr 目录乍一看可能相对干净,但快速查看 /usr/bin/usr/lib 会发现这里有很多内容;/usr 是大多数用户空间程序和数据所在的位置。除了 /usr/bin/usr/sbin/usr/lib/usr 还包含以下内容:

  • /include:存放 C 编译器使用的头文件。
  • /local:管理员可以安装自有软件的地方。其结构应当类似于 //usr 的结构。
  • /man:包含手册页。
  • /share:包含应在其他类型的 Unix 机器上同样工作且功能不变的文件。这些通常是程序和库按需读取的辅助数据文件。过去,机器网络会从文件服务器共享此目录,但现代系统上此类文件没有实际的空间限制,因此这种方式的共享目录已很少见。相反,在 Linux 发行版中,你会在这里找到 /man/info 以及许多其他子目录,因为这是一种易于理解的约定。

2.19.3 内核位置

在 Linux 系统上,内核通常是一个二进制文件 /vmlinuz/boot/vmlinuz。引导加载程序在系统启动时将此文件加载到内存并启动它。(引导加载程序的详细信息见第 5 章。)

一旦引导加载程序启动内核,主内核文件就不再被运行中的系统使用。但是,你会在 /lib/modules 下找到许多内核在正常系统操作期间按需加载和卸载的模块。这些称为可加载内核模块。


2.20 以超级用户身份运行命令

在进一步深入之前,你应该学习如何以超级用户身份运行命令。你可能会想启动一个 root shell,但这有许多缺点:

  • 没有系统更改命令的记录。
  • 没有执行系统更改命令的用户的记录。
  • 无法访问正常的 shell 环境。
  • 需要输入 root 密码(如果有的话)。

2.20.1 sudo

大多数发行版使用一个名为 sudo 的软件包,允许管理员以自身身份登录后以 root 身份运行命令。例如,在第 7 章中,你将学习使用 vipw 编辑 /etc/passwd 文件。你可以这样做:

$ sudo vipw

当你运行此命令时,sudo 会通过 local2 设施将此操作记录到 syslog 服务中。你将在第 7 章了解更多关于系统日志的信息。

2.20.2 /etc/sudoers

当然,系统不会让任何用户都以超级用户身份运行命令;你必须在 /etc/sudoers 文件中配置特权用户。sudo 软件包有许多选项(你可能永远不会用到),这使得 /etc/sudoers 的语法有些复杂。例如,以下文件授予用户 user1user2 以 root 身份运行任何命令的权限,且无需输入密码:

User_Alias ADMINS = user1, user2
ADMINS ALL = NOPASSWD: ALL
root ALL=(ALL) ALL

第一行定义了一个名为 ADMINS 的用户别名,包含两个用户;第二行授予权限。ALL = NOPASSWD: ALL 部分表示 ADMINS 别名中的用户可以使用 sudo 以 root 身份执行命令。第二个 ALL 表示“任何命令”。第一个 ALL 表示“任何主机”。(如果你有多台机器,可以为每台机器或机器组设置不同类型的访问权限,但我们不会介绍该功能。)

root ALL=(ALL) ALL 仅表示超级用户也可以在任何主机上使用 sudo 运行任何命令。额外的 (ALL) 表示超级用户还可以以任何其他用户的身份运行命令。你可以通过向第二行添加 (ALL) 来将此特权扩展到 ADMINS 用户,如下所示:

ADMINS ALL = (ALL) NOPASSWD: ALL

注意

使用 visudo 命令编辑 /etc/sudoers。此命令会在保存文件后检查语法错误。

2.20.3 sudo 日志

尽管我们稍后会在书中更详细地讨论日志,但你可以在大多数系统上使用以下命令找到 sudo 日志:

$ journalctl SYSLOG_IDENTIFIER=sudo

在较老的系统上,你需要在 /var/log 中查找日志文件,例如 /var/log/auth.log

关于 sudo 目前就介绍这些。如果你需要使用其更高级的功能,请参阅 sudoers(5)sudo(8) 手册页。(用户切换的实际机制在第 7 章中介绍。)


2.21 展望未来

现在你应该知道如何在命令行中执行以下操作:运行程序、重定向输出、与文件和目录交互、查看进程列表、查看手册页,以及大致掌握 Linux 系统的用户空间。你还应该能够以超级用户身份运行命令。你可能还对用户空间组件的内部细节或内核中发生的事情了解不多,但掌握了文件和进程的基础知识后,你已经在路上了。在接下来的几章中,你将使用刚刚学到的命令行工具,与内核和用户空间系统组件一起工作。