包含常见命令行使用,Bash 基础、Shell 进阶编程,以及实用范例!

初识 Linux Shell

本书学习的第一步,就是要找到 Linux 终端的所在位置。目前较常见的图形化终端有 Konsole、Gnome terminal、xterm 等几种。一般安装后在各个发行版的菜单中搜索即可找到。Gnome terminal 和 Konsole 基本是当前各大流行 Linux 发行版预装最多的终端应用,功能也非常强大好用。而 xterm 是世界上第一款图形化终端软件,xterm 软件包在 X Window 出现之前就有了。其对于老式哑终端的模仿非常到位,可以仿真各类老式哑终端的色彩等行为。所谓的哑终端就是传统的利用通信电缆(一般是一条多线束的串行电缆)连接到 Unix 系统上的一台显示器和一个键盘。关于各个图形化终端的各类配置,同学们可以自行慢慢摸索。

除了上述的图形化终端,在 Linux 出现的早期,没有图形化界面可供操作,当时通常的方式是使用一个简单的哑终端操作 Unix 系统。当今 2021 年,这种连接方式基本早已不存在了,但是你仍然可以模拟此种连接方式。Linux 系统启动后,它会自动创建出一些虚拟控制台。虚拟控制台是运行在 Linux 系统内存中的终端会话。通过按键组合ctrl + alt + (F1~F7),可以分别切换到多个不同的虚拟控制台。这种模式称作 Linux 控制台,它仿真了早期的硬接线控制台终端,而且是一种同 Linux 系统交互的直接接口。虚拟控制台一般也被称为 tty。tty 代表电传打字机(teletypewriter)。这是一个古老的名词,指的是一台用于发送消息的机器。

在开始使用 tty 后,你可以看到首先会让你输入要登录的用户名,输入用户名回车后会提示输入密码,需要注意的是,密码的输入是不会显示在屏幕上的,盲输完回车即可完成登录,屏幕不显示密码是正常的,不要以为自己的键盘坏了。

起步

在启动图形化终端后,你首先会看到一个待输入的命令提示符,类似 windows 系统中的 cmd。一般如下所示,显示了当前用户 ID 名 testuser,系统名为 archlinux。~这个波浪线代表你正处于 testuser 这个用户的个人目录下,路径为/home/testuser$代表当前是已一个普通用户登陆,若为#则表示是以 root 帐户登陆。

testuser@archlinux:~$

你可能听说过 Linux 中有非常多的命令,都记住各个命令的用法是不现实的,所以第一步我们来学习的命令就是 man 命令:一个用来查询各个命令如何使用的命令。

在 ArchLinux 上,你首先需要安装man-db man-pages两个包,如果是第一次安装,最好执行一下如下命令,以便 man-db.service 建立-k 搜索参数需要使用的 db 索引。

$ sudo systemctl enable --now man-db #第一次执行时间会较长,可能会有几分钟 耐心等待

sudo 命令代表可以让当前用户暂时使用 root 权限执行此条命令。

比如,现在想查询 ls 命令的使用方式,那么输入执行如下命令即可

$ man ls

执行后你可以看到标题,描述,以及一大堆的信息。首先要知道,man 命令是用来速查某种命令使用方式的,不是用来各个详细阅读的(当然你有时间愿意读也没问题)。 在这个界面你可以执行一些搜索操作,方式和在 vim 中类似,比如输入/斜杠加上你想搜索的单词进行搜索,n键跳到下一个匹配的位置等等。一般常规的查询一个命令的流程就是先看一下 Description 知道这个命令是干什么的,然后输入斜杠搜索一下你想查询的参数的含义,最后看完按q键退出。

在 man 页面的左上角,可以看到LS(1)的字样。这个(1)代表的是手册类型,下面列举一下各个数字所代表的类型意义。

  • 1:可执行程序或 shell 命令
  • 2:系统调用
  • 3:库调用
  • 4:特殊文件
  • 5:文件格式与约定
  • 6:游戏
  • 7:概览、约定及杂项
  • 8:超级用户和系统管理员命令
  • 9:内核例程

上面有的部分现在看不懂也没关系,该懂的时候你自然就懂了。

一个命令偶尔会在多个内容区域都有对应的手册页。比如说,有个叫作 hostname(在 archlinux 中需要安装包inetutils) 的命令。手册页中既包括该命令的相关信息,也包括对系统主机名的概述。要想查看所需要的页面,对手册页中的第 1 部分而言,可以输入 man 1 hostname。对于手册页中的第 7 部分,就是输入 man 7 hostname。

除了 man 命令可以查询 Linux 命令的使用方式,info 命令也可以进行查询(在 archlinux 中需要安装包texinfo)。这里不再详细讲述 info 命令,因为 man 命令已经可以覆盖绝大多数内容。

另外,大多数命令都可以接受-help 或–help 选项。例如你可以输入 hostname –help 来查看帮助。

如果不记得命令名怎么办?可以使用关键字搜索手册页。语法是:man -k 关键字。例如,要查找与终端相关的命令,可以输入 man -k terminal。

除了万能的 man 命令,近几年还出现了一个非常流行的命令行帮助项目:tldr。其含义为太长不看的缩写。像它的名字一样,这个命令只输出某个命令的最简要的使用方式,对喜欢太长不看的人来说非常好用。在 archlinux 上可以直接通过 pacman -S tldr 进行安装。

$ tldr tar   #查看tar命令的简明使用方式

若你出现如下错误,说明你的网络环境下,tldr 命令查询的 github raw 网址已经被 GFW 墙掉了。请使用全局代理proxychains 前缀,或添加 https_proxy 环境变量。

Error fetching from tldr: <urlopen error [Errno 111] Connection refused>

cd 漫游文件系统

当登录系统并获得 shell 命令提示符后,你通常位于自己的主目录中。一般情况下,你首先会想去逛逛主目录之外的其他地方。本节将告诉你如何使用 shell 命令来实现这个目标。

稍微了解 Linux 文件系统的话就会知道,一般来说一个完整的 Linux 文件路径可能是这样的:

/home/testuser/Documents/test.cpp

这种完整的路径被称为绝对路径,即从根路径/到目标文件的完整路径结构,含义为 testuser 这个用户家路径下的 Documents 目录下,有一个名为 test.cpp 的文件。想查看当前所处的位置的绝对路径,可以使用命令pwd

$ pwd
/home/testuser/Documents/

与此对应的,另一个概念为相对路径。其代表当前路径为基准起点,对应的一个相对位置。比如当前你所处的路径为/home/testuser/Documents,此时想要去到 testuser 用户的桌面 /home/testuser/Desktop 路径下,用相对路径即可表示为

../Desktop

其中..为双点符,表示当前目录的父目录。另外一个常用标识符为.单点符,标识当前目录自身。

在文件系统中变更目录位置的命令为cd,可以接受绝对路径或相对路径作为参数

#绝对路径的例子
$ cd /home/testuser/Documents/  #使用绝对路径的方式切换到/home/testuser/Documents/路径下
#相对路径的例子
$ cd ../Desktop               #从/home/testuser/Documents/目录,使用相对路径的方式切换到/home/testuser/Desktop/路径下

一般来说,要视情况来使用相对路径或者绝对路径。在上述例子中,使用相对路径可以少输入很多内容。若此时想要切换到/etc 路径下,则明显使用绝对路径较为方便

$ cd /etc         #如果使用绝对路径
$ cd ../../../etc #如果使用相对路径

cd 命令可以直接使用,不加任何参数,此时会默认切换到当前用户的家目录下,如/home/testuser。在终端中,如果你能看到提示符最后有一个~波浪线,它代表的就是当前的路径是当前用户的家目录。

此外,另一个常用的用法是cd -。此命令可以切换到你上次所处的文件系统路径位置下。在需要返回上次路径的时候,此命令非常高效实用。

ls 查看文件信息

本节首先来学习一下查看文件与目录信息的相关命令。ls 命令最基本的形式会显示当前目录下的文件和目录。

注意,ls 命令输出的列表是按字母排序的(按列排序而不是按行排序)。可用带 -F 参数的 ls 命令轻松区分文件和目录。

$ ls -F
Documents/                 Videos/
Music/                     my_script.sh*
Desktop/                   Pictures/
testfile

F 参数在目录名后加了正斜线(/),以方便用户在输出中分辨它们。类似地,它会在可执行文件(比如上面的 my_script 文件)的后面加个星号,以便用户找出可在系统上运行的文件。

Linux 经常采用隐藏文件来保存配置信息。在 Linux 上,隐藏文件通常是文件名以点号开始的文件。这些文件并没有在默认的 ls 命令输出中显示出来,因此我们称其为隐藏文件。要把隐藏文件和普通文件及目录一起显示出来,就得用到 -a 参数。,使用ls -a,所有以点号开头的隐藏文件现在都显示出来了。

-R 参数是 ls 命令可用的另一个参数,叫作递归选项。它列出了当前目录下包含的子目录中的文件。如果目录很多,这个输出就会很长。注意,首先 -R 参数显示了当前目录下的内容,也就是之前例子中用户 home 目录下的那些文件。另外,它还显示出了用户 home 目录下所有子目录及其内容。如果当前路径下的目录有更多的子目录,-R 参数会继续进行遍历。正如你尝试的,如果目录结构很庞大,输出内容会变得很长。

命令行的多个选项一般并不一定要分开输入:ls -F -R -a。它们可以进行如下合并:ls -FRa

在基本的输出列表中,ls 命令并未输出太多每个文件的相关信息。要显示附加信息,另一个常用的参数是 -l。-l 参数会产生长列表格式的输出,包含了目录中每个文件的更多相关信息。这种长列表格式的输出在每一行中列出了单个文件或目录。除了文件名,输出中还有其他有用信息。输出的第一行显示了在目录中包含的总块数(block)。在此之后,每一行都包含了关于文件(或目录)的下述信息:

$ ls -l
总用量 44
drwxr-xr-x  3 testuser testuser 4096 Dec 13 21:09 Android
drwx--x---+ 2 testuser testuser 4096 Dec 17 13:02 Desktop #多出的加号代表ACL
drwxr-xr-x  5 testuser testuser 4096 Dec 10 15:08 Documents
drwxr-xr-x  4 testuser testuser 4096 Dec 17 11:06 Downloads
drwxr-xr-x  3 testuser testuser 4096 Dec 10 22:50 Games
drwxr-xr-x  3 testuser testuser 4096 Dec 11 18:54 Music
drwxr-xr-x  4 testuser testuser 4096 Dec 14 22:50 Pictures
drwxr-xr-x  2 testuser testuser 4096 Dec 13 11:28 Videos
  • 文件类型,比如目录(d)、文件(-)、字符型文件(c)或块设备(b);
  • 文件的权限;
  • 文件的硬链接总数;
  • 文件属主的用户名;
  • 文件属组的组名;
  • 文件的大小(以字节为单位);
  • 文件的上次修改时间;
  • 文件名或目录名。

-l 参数是一个强大的工具。有了它,你几乎可以看到系统上任何文件或目录的大部分信息。在进行文件管理时,ls 命令的很多参数都能派上用场。如果在 shell 提示符中输入 man ls,就能看到可用来修改 ls 命令输出的参数有好几页。别忘了可以将多个参数结合起来使用。你不时地会发现一些参数组合不仅能够显示出所需的内容,而且还容易记忆,例如 ls -alF。

由前面的例子可知,默认情况下,ls 命令会输出目录下的所有非隐藏文件。有时这个输出会显得过多,当你只需要查看单个或者少数文件信息时更是如此。幸而 ls 命令还支持在命令行中定义过滤器(filter)。它会用过滤器来决定应该在输出中显示哪些文件或目录。这个过滤器就是一个进行简单文本匹配的字符串。可以在要用的命令行参数之后添加这个过滤器:当用户指定特定文件的名称作为过滤器时,ls 命令只会显示该文件的信息。有时你可能不知道要找的那个文件的确切名称。ls 命令能够识别标准通配符,并在过滤器中用它们进行模式匹配:

  • 问号(?)代表一个字符;
  • 星号(*)代表零个或多个字符。

在过滤器中使用星号和问号被称为文件扩展匹配(file globbing),指的是使用通配符进行模式匹配的过程。通配符正式的名称叫作元字符通配符(metacharacter wildcards)。除了星号和问号之外,还有更多的元字符通配符可用于文件扩展匹配,例如可以使用中括号:

$ ls -l my_scr[ai]pt
-rw-rw-r-- 1 christine christine  0 May 21 13:25 my_scrapt
-rwxrw-r-- 1 christine christine 54 May 21 11:26 my_script

在这个例子中,我们使用了中括号以及在特定位置上可能出现的两种字符:a 或 i。中括号表示一个字符位置并给出多个可能的选择。可以像上面的例子那样将待选的字符列出来,也可以指定字符范围,例如字母范围[a - i]:

$ ls -l f[a-i]ll
-rw-rw-r-- 1 christine christine 0 May 21 13:44 fall
-rw-rw-r-- 1 christine christine 0 May 21 13:44 fell
-rw-rw-r-- 1 christine christine 0 May 21 13:44 fill

另外,可以使用感叹号(!)将不需要的内容排除在外:

$ ls -l f[!a]ll
-rw-rw-r-- 1 christine christine 0 May 21 13:44 fell
-rw-rw-r-- 1 christine christine 0 May 21 13:44 fill
-rw-rw-r-- 1 christine christine 0 May 21 13:44 full

在进行文件搜索时,文件扩展匹配是一个功能强大的特性。它也可以用于 ls 以外的其他 shell 命令。随后的部分会有更多相关的例子。

文件的处理

touch 创建文件

touch 命令用于创建空白文件。如果作用于一个已有文件,可以更改其修改时间。
如果只想改变访问时间,可用-a参数:

$ touch -a test_one
ls -l --time=atime test_one

如果只使用 ls -l 命令,并不会显示访问时间。这是因为默认显示的是修改时间。要想查看文件的访问时间,需要加入另外一个参数:–time=atime。有了这个参数,就能够显示出已经更改过的文件的访问时间。

cp 复制文件

复制文件的格式为:

$ cp source destination

当 source 和 destination 参数都是文件名时,cp 命令将源文件复制成一个新文件,并且以 destination 命名。新文件就像全新的文件一样,有新的修改时间。

如果目标文件已经存在,cp 命令并不会提醒这一点。最好是加上-i选项,强制 shell 询问是否需要覆盖已有文件。

也可以将文件复制到现有目录中。

$ cp -i test_one /home/christine/Documents/

新文件就出现在目录 Documents 中了,和源文件同名。

上面的例子在目标目录名尾部加上了一个正斜线(/),这表明 Documents 是目录而非文件。这有助于明确目的,而且在复制单个文件时非常重要。如果没有使用正斜线,子目录/home/christine/Documents 又不存在,就会有麻烦。在这种情况下,试图将一个文件复制到 Documents 子目录反而会创建一个名为 Documents 的文件,连错误消息都不会显示!

上一个例子采用了绝对路径,不过也可以使用相对路径。

本章在前面介绍了特殊符号可以用在相对文件路径中。其中的单点符(.)就很适合用于 cp 命令。记住,单点符表示当前工作目录。如果需要将一个带有很长的源对象名的文件复制到当前工作目录中时,单点符能够简化该任务。如果你的源对象名很长,使用单点符要比输入完整的目标对象名省事得多。

$ cp -i /etc/NetworkManager/NetworkManager.conf .

cp 命令的 -R 参数威力强大。可以用它在一条命令中递归地复制整个目录的内容:

$ ls -Fd *Scripts #-d选项只列出目录本身的信息,不列出其中的内容。一般可与 l 选项搭配显示目录自身详情
$ cp -R Scripts/  Mod_Scripts

在执行 cp -R 命令之前,目录 Mod_Scripts 并不存在。它是随着 cp -R 命令被创建的,整个 Scripts 目录中的内容都被复制到其中。注意,在新的 Mod_Scripts 目录中,所有的文件都有对应的新日期。Mod_Scripts 目录现在已经成为了 Scripts 目录的完整副本。

也可以在 cp 命令中使用通配符:

$ cp *script  Mod_Scripts/

该命令将所有以 script 结尾的文件复制到 Mod_Scripts 目录中。

[Tab]键 自动补全

如果你需要操作的文件/文件夹的名字很长,这正是制表键(Tab 键)自动补全挺身而出的时候。制表键自动补全允许你在输入文件名或目录名时按一下制表键,让 shell 帮忙将内容补充完整。

$ cp really_ridiculously_long_file_name  Mod_Scripts/

在上面的例子中,我们输入了命令 cp really,然后按制表键,shell 就将剩下的文件名自动补充完整了。

使用制表键自动补全的的技巧在于要给 shell 足够的文件名信息,使其能够将需要文件同其他文件区分开。假如有另一个文件名也是以 really 开头,那么就算按了制表键,也无法完成文件名的自动补全。如果你的电脑有蜂鸣器,这时候你会听到蜂鸣器嘟的一声。要是再连按一下制表键,shell 就会列出所有以 really 开头的文件名。这个特性可以让你观察究竟应该输入哪些内容才能完成自动补全。

以上是对文件/文件夹的自动补全处理。如果希望对于 linux 命令也可以使用自动补全,在 Archlinux 上则需要额外安装包bash-completion

mv 移动/重命名文件

在 Linux 中,重命名文件称为移动(moving)。mv 命令可以将文件和目录移动到另一个位置或重新命名。

$ mv fall  fzll #重命名
$ mv fzll  Pictures/   #把文件fzll从/home/testuser移动到了/home/testuser/Pirctures

注意,mv 将文件名从 fall 更改为 fzll,但 inode 编号和时间戳保持不变。这是因为 mv 只影响文件名。 和 cp 命令类似,也可以在 mv 命令中使用-i参数。这样在命令试图覆盖已有的文件时,你就会得到提示。

也可以使用 mv 命令移动文件位置并修改文件名称,这些操作只需一步就能完成:

$ mv /home/testuser/Pictures/fzll  /home/testuser/fall

也可以使用 mv 命令移动整个目录及其内容:

$ mv Mod_Scripts  Old_Scripts

rm 删除文件

bash shell 中删除文件的命令是 rm。rm 命令的基本格式非常简单。

$ rm -i fall

注意,-i 命令参数提示你是不是要真的删除该文件。bash shell 中没有回收站或垃圾箱,文件一旦删除,就无法再找回。因此,在使用 rm 命令时,要养成总是加入-i 参数的好习惯。也可以使用通配符删除成组的文件。别忘了使用-i 选项保护好自己的文件。

$ rm -i f?ll
rm: remove regular empty file 'fell'? y
rm: remove regular empty file 'fill'? y
rm: remove regular empty file 'full'? y

rm 命令的另外一个特性是,如果要删除很多文件且不受提示符的打扰,可以用-f 参数强制删除。小心为妙!

ln 链接文件

链接文件是 Linux 文件系统的一个优势。如需要在系统上维护同一文件的两份或多份副本,除了保存多份单独的物理文件副本之外,还可以采用保存一份物理文件副本和多个虚拟副本的方法。这种虚拟的副本就称为“链接”。链接是目录中指向文件真实位置的占位符。在 Linux 中有两种不同类型的文件链接:

  • 符号链接(symbolic link)
  • 硬链接(hard link)

“符号链接”就是一个实实在在的文件,它指向存放在虚拟目录结构中某个地方的另一个文件。这两个通过符号链接在一起的文件,彼此的内容并不相同。

要为一个文件创建符号链接,原始文件必须事先存在。然后可以使用 ln 命令以及-s选项来创建符号链接:

$ ln -s data_file  sl_data_file
-rw-rw-r-- 1 christine christine 1092 May 21 17:27 data_file
lrwxrwxrwx 1 christine christine    9 May 21 17:29 sl_data_file -> data_file

在上面的例子中,注意符号链接的名字 sl_data_file 位于 ln 命令中的第二个参数位置上。显示在长列表中符号文件名后的->符号表明该文件是链接到文件 data_file 上的一个符号链接。

另外,还要注意的是符号链接的文件大小与数据文件的文件大小。符号链接 sl_data_file 只有 9 个字节,而 data_file 有 1092 个字节。这是因为 sl_data_file 仅仅只是指向 data_file 而已。它们的内容并不相同,是两个完全不同的文件。

另一种证明链接文件是独立文件的方法是查看 inode 编号。文件或目录的 inode 编号是一个用于标识的唯一数字,这个数字由内核分配给文件系统中的每一个对象。要查看文件或目录的 inode 编号,可以给 ls 命令加入-i 参数。

$ ls -i *data_file
296890 data_file
296891 sl_data_file

从这个例子中可以看出数据文件的 inode 编号是 296890,而 sl_data_file 的 inode 编号则是 296891,所以说它们是不同的文件。

当含有一连串符号链接的链接串时,不必一个一个用 ls 查看其链接关系,可以直接使用 readlink -f filename 指令查到当前符号链接串的原始文件是什么。


“硬链接”会创建独立的虚拟文件,其中包含了原始文件的信息及位置。但是它们从根本上而言是同一个文件。引用硬链接文件等同于引用了源文件。要创建硬链接,原始文件也必须事先存在,只不过这次使用 ln 命令时不再需要加入额外的参数了。

$ ln code_file  hl_code_file
$ ls -li *code_file
296892 -rw-rw-r-- 2 christine christine 189 May 21 17:56  code_file
296892 -rw-rw-r-- 2 christine christine 189 May 21 17:56  hl_code_file

在上面的例子中,我们使用 ls -li 命令显示了*code_files 的 inode 编号以及长列表。注意,带有硬链接的文件共享 inode 编号。这是因为它们终归是同一个文件。还要注意的是,链接计数(列表中第三项)显示这两个文件都有两个链接。另外,它们的文件大小也一模一样。

只能对处于同一存储媒体的文件创建硬链接。要想在不同存储媒体的文件之间创建链接,只能使用符号链接。

复制链接文件的时候一定要小心。如果使用 cp 命令复制一个文件,而该文件又已经被链接(不论是符号链接还是硬链接)到了另一个源文件上,那么你得到的其实是源文件的一个副本。这很容易让人犯晕。其实用不着复制链接文件,可以创建原始文件的另一个链接。同一个文件拥有多个链接,这完全没有问题。但是,尽可能不要创建符号链接文件的符号链接。这会形成混乱的链接链,不仅容易断裂,还会造成各种麻烦。

文件夹的处理

在 Linux 中创建目录很简单,用 mkdir 命令即可:

$ mkdir New_Dir

要想同时创建父目录和其下的子目录,需要加入-p 参数:

$ mkdir -p New_Dir/Sub_Dir/Under_Dir

mkdir 命令的-p 参数可以根据需要创建缺失的父目录。父目录是包含目录树中下一级目录的目录。

删除目录的基本命令是 rmdir。默认情况下,rmdir 命令只删除空目录。如果在一个目录下创建了内容, rmdir 命令会拒绝删除目录。要解决这一问题,得先把目录中的文件删掉,然后才能在空目录上使用 rmdir 命令。rmdir 并没有-i 选项来询问是否要删除目录。这也是为什么说 rmdir 只能删除空目录还是有好处的原因。

也可以在整个非空目录上使用 rm 命令。使用-r 选项使得命令可以向下进入目录,删除其中的文件,然后再删除目录本身。

$ rm -ri My_Dir #i用来确认是否删除每个文件/目录

对 rm 命令而言,-r 参数和-R 参数的效果是一样的。-R 参数同样可以递归地删除目录中的文件。shell 命令很少会就相同的功能采用不同大小写的参数。

一口气删除目录及其所有内容的终极大法就是使用带有-r 参数和-f 参数的 rm 命令。rm -rf 命令既没有警告信息,也没有声音提示。这肯定是一个危险的工具,尤其是在拥有超级用户权限的时候。务必谨慎使用,请再三检查你所要进行的操作是否符合预期。

$ tree Small_Dir
$ rm -rf Small_Dir

在上面的例子中,我们使用了 tree 工具。它能够以一种美观的方式展示目录、子目录及其中的文件。如果需要了解目录结构,尤其是在删除目录之前,这款工具正好能派上用场。不过它可能并没有默认安装在你所使用的 Linux 发行版中。ArchLinux 用户需要安装tree包来使用。

文件的查看

可用 file 命令确定文件的文件类型。

$ file 1.txt
1.txt: ASCII text  #file命令不仅能确定文件中包含的文本信息,还能确定该文本文件的字符编码,ASCII

常见的文件类型还有很多类型,比如下面几种

  • directory 目录
  • symbolic link to ‘data_file’ 符号链接
  • Bourne-Again shell script,ASCII text executable 脚本文件
  • /usr/bin/ls: ELF 64-bit LSB executable,x86-64,version 1 (SYSV),dynamically linked (uses shared libs),for GNU/Linux 2.6.24
    二进制可执行程序。file 命令能够确定该程序编译时所面向的平台以及需要何种类型的库。如果你有从未知源处获得的二进制文件,这会是个非常有用的特性
  • JSON data

知道如何查看文件类型后,接下来学习如何查看文件内容。几个常见的命令是 cat,more 与 less。more 命令目前已经和 less 一样支持上下翻页,基本没有区别了。对于查看单个完整文件,群主更偏向直接使用 vim 查看。

有时存在一些巨型文件,如有些日志文件可以达到几十 GB 之大,这时候如果还整体查看文件,可能直接把 vim 等程序卡死了。此时需要的就是查看部分文件。常用的命令为 tail 和 head。

更常见的场景是查看文件的末尾,如日志的末尾,查看最新产生的内容。默认情况是查看此文件最后十行的内容。

$ tail log_file

可以向 tail 命令中加入-n 参数来修改所显示的行数。在下面的例子中,通过加入-n 2 使 tail 命令只显示文件的最后两行:

$ tail -n 2 log_file

-f 参数是 tail 命令的一个突出特性。它允许你在其他进程使用该文件时查看文件的内容。tail 命令会保持活动状态,并不断显示添加到文件中的内容。这是实时监测系统日志的绝妙方式。

head 命令,顾名思义,会显示文件开头那些行的内容。默认情况下,它会显示文件前 10 行的文本。类似于 tail 命令,它也支持-n 参数,这样就可以指定想要显示的内容了。

这两个命令都允许你在破折号后面直接输入想要显示的行数:

$ head log_file
$ head -5 log_file

文件的编辑

在命令行中编辑文件最常用的三个程序分别为 emacs,vim,以及 nano。其中 emacs 以及 vim 的功能较为丰富,nano 较为简单。个人的意见是掌握 vim 一种即可。原因是 vim 应用广泛,且是很多 Linux 发行版的预装程序。对于 vim 的学习,也只是适度即可,有很多 linux 爱好者把 vim 玩的炉火纯青,花费很多时间精力将 vim 打造成 IDE 级别的程序,个人认为是没有必要的。

对于 vim 的学习,有一个命令是vimtutor。这是一个非常好的边操作边学的教程,大家学习几遍,即可很好的掌握 vim 的大多数基础功能了。如果想看中文的版本,可以执行vimtutor -g zh

更多实用命令

进程相关

当程序运行在系统上时,我们称之为进程(process)。想监测这些进程,需要熟悉 ps/top 等命令的用法。ps 命令好比工具中的瑞士军刀,它能输出运行在系统上的所有程序的许多信息。而 top 可以监控当前各个进程的运行状态,以及占用 cpu,内存等系统资源的情况。

ps 查看进程

默认情况下,ps 命令只会显示运行在当前控制台下的属于当前用户的进程。直接执行 ps 命令,可以发现我们只运行了 bash shell(注意,shell 也只是运行在系统上的另一个程序而已)以及 ps 命令本身。可以看到基本输出显示了程序的进程 ID(Process ID,PID)、它们运行在哪个终端(TTY)以及进程已用的 CPU 时间。

Linux 系统中使用的 GNU ps 命令支持 3 种不同类型的命令行参数:

  • Unix 风格的参数,前面加单破折线;Unix 风格的参数是从贝尔实验室开发的 AT&T Unix 系统上原有的 ps 命令继承下来的。
  • BSD 风格的参数,前面不加破折线;伯克利软件发行版(Berkeley software distribution,BSD)是加州大学伯克利分校开发的一个 Unix 版本。它和 AT & T Unix 系统有许多细小的不同
  • GNU 风格的长参数,前面加双破折线。

Unix 风格一些常用的参数组合:

$ ps -ef #查看系统上运行的所有进程  -e参数指定显示所有运行在系统上的进程;-f参数则扩展了输出,这些扩展的列包含了有用的信息。
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 11:29 ?        00:00:01 /sbin/init
...
  • UID:启动这些进程的用户。
  • PID:进程的进程 ID。
  • PPID:父进程的进程号(如果该进程是由另一个进程启动的)。
  • C:进程生命周期中的 CPU 利用率。
  • STIME:进程启动时的系统时间。
  • TTY:进程启动时的终端设备。
  • TIME:运行进程需要的累计 CPU 时间。
  • CMD:启动的程序名称。

如果想要获得更多的信息,可采用-l 参数,它会产生一个长格式输出。

$ ps -l
F S  UID PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY      TIME   CMD
0 S  500 3081  3080  0  80   0 -  1173 wait pts/0   00:00:00 bash
0 R  500 4463  3081  1  80   0 -  1116 -    pts/0   00:00:00 ps

注意使用了-l 参数之后多出的那些列。

  • F:内核分配给进程的系统标记。1 代表 进程被 fork 但没有被执行。4 代表使用了超级管理员的权限。5 代表 1 和 4 都做了。0 没有任何特殊含义,含义为进程被 fork 了,也确实执行了,并且没有超级用户权限。
  • S:进程的状态(S 代表在休眠;R 代表正在运行,或正等待运行;Z 代表僵化,进程已结束但父进程已不存在;T 代表停止,I 代表 idle 进程)。
  • PRI:进程的优先级(越大的数字代表越低的优先级)。
  • NI:谦让度值用来参与决定优先级。越大优先级越低。
  • ADDR:进程的内存地址。正在运行的任务将在此列中显示一个破折号(’-‘)
  • SZ:所需物理内存页面的大致大小。详情看 vsz 与 rss
  • WCHAN:进程休眠的内核函数的地址。

在使用 BSD 参数时,ps 命令会自动改变输出以模仿 BSD 格式。大部分的输出列跟使用 Unix 风格参数时的输出是一样的,只有一小部分不同。

$ ps l
F  UID  PID PPID PRI  NI  VSZ  RSS WCHAN  STAT TTY      TIME COMMAND
0  500 3081 3080  20   0 4692 1432 wait   Ss   pts/0    0:00 -bash
0  500 5104 3081  20   0 4468  844 -      R+   pts/0    0:00 ps l
  • VSZ:进程的虚拟内存大小,以千字节(KB)为单位。
  • RSS:常驻集大小,进程在未换出时占用的物理内存。
  • STAT:代表当前进程状态的双字符状态码。

许多系统管理员都喜欢 BSD 风格的 l 参数。它能输出更详细的进程状态码(STAT 列)。双字符状态码能比 Unix 风格输出的单字符状态码更清楚地表示进程的当前状态。第一个字符采用了和 Unix 风格 S 列相同的值,表明进程是在休眠、运行还是等待。第二个参数进一步说明进程的状态。

  • <:该进程运行在高优先级上。
  • N:该进程运行在低优先级上。
  • L:该进程有页面锁定在内存中。
  • s:该进程是控制进程。
  • l:该进程是多线程的。
  • +:该进程运行在前台。

从前面的例子可以看出,bash 命令处于休眠状态,但同时它也是一个控制进程(在我的会话中,它是主要进程),而 ps 命令则运行在系统的前台。


最后,GNU 开发人员在这个新改进过的 ps 命令中加入了另外一些参数。其中一些 GNU 长参数复制了现有的 Unix 或 BSD 类型的参数,而另一些则提供了新功能。

可以将 GNU 长参数和 Unix 或 BSD 风格的参数混用来定制输出。GNU 长参数中一个着实让人喜爱的功能就是–forest 参数。它会显示进程的层级信息,并用 ASCII 字符绘出可爱的图表。这种格式让跟踪子进程和父进程变得十分容易。

top 监控进程

ps 命令虽然在收集运行在系统上的进程信息时非常有用,但也有不足之处:它只能显示某个特定时间点的信息。如果想观察那些频繁换进换出的内存的进程趋势,用 ps 命令就不方便了。而 top 命令刚好适用这种情况。top 命令跟 ps 命令相似,能够显示进程信息,但它是实时显示的。

xbren@archlinux:~$ top
top - 00:01:04 up 38 min,  0 users,  load average: 0.52, 0.58, 0.59
Tasks:   4 total,   1 running,   3 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.5 us,  0.8 sy,  0.0 ni, 98.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :  16042.5 total,   8472.3 free,   7346.2 used,    224.0 buff/cache
MiB Swap:  49152.0 total,  49099.5 free,     52.5 used.   8565.7 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    1 root      20   0    9216    660    320 S   0.0   0.0   0:00.18 init
 5115 root      20   0    9308    240    176 S   0.0   0.0   0:00.00 init
 5116 xbren     20   0   19280   4808   4704 S   0.0   0.0   0:00.81 bash
 5606 xbren     20   0   18920   2148   1528 R   0.0   0.0   0:00.04 top

输出的分上下两部分。第一部分显示的是系统的概况。第二部分显示了进程的实时概要信息。

第一部分的第一行显示了当前时间、系统的运行时间、登录的用户数以及系统的平均负载。平均负载有 3 个值:最近 1 分钟的、最近 5 分钟的和最近 15 分钟的平均负载。值越大说明系统的负载越高。由于进程短期的突发性活动,出现最近 1 分钟的高负载值也很常见,但如果近 15 分钟内的平均负载都很高,就说明系统可能有问题。

Linux 系统管理的要点在于定义究竟到什么程度才算是高负载。这个值取决于系统的硬件配置以及系统上通常运行的程序。对某个系统来说是高负载的值可能对另一系统来说就是正常值。在单核机器上,负载的意义是这样的,比如最近十五分钟的负载值为 5.09,含义为计算机的平均过载为 409%。平均而言,有 4.09 个进程在等待 CPU。通常对于单核机器来说,如果系统的负载值超过了 2,就说明系统比较繁忙了。但是对于多核机器来说,就不是这样计算的了。例如,如果在单 CPU 系统上的平均负载为 2,则意味着系统过载了 100%,在整个时间段内,一个进程正在使用 CPU,而另一个进程正在等待。在具有两个 CPU 的系统上,含义为两个不同的进程始终使用两个不同的 CPU。在具有四个 CPU 的系统上,这将代表只有 50%的使用率-两个进程使用两个 CPU,而两个 CPU 处于空闲状态。

第二行显示了进程概要信息——top 命令的输出中将进程叫作任务(task):有多少进程处在运行、休眠、停止或是僵化状态(僵化状态是指进程完成了,但父进程没有响应)。

下一行显示了 CPU 使用的概要信息。由前到后分别为:用户态使用率,内核态使用率,用做 nice 加权的进程分配的用户态 cpu 使用率,空闲的 cpu 使用率,等待磁盘写入完成时间比,硬件中断消耗时间,软件中断消耗时间,为处理其他进程而从虚拟机中偷走的 cpu 时间(仅虚拟机)。

紧跟其后的两行说明了系统内存的状态。第一行说的是系统的物理内存:总共有多少内存,还有多少空闲,当前用了多少,缓存占用了多少。后一行说的是同样的信息,不过是针对系统交换空间(如果分配了的话)的状态而言的。


第二部分显示了当前运行中的进程的详细列表,有些列跟 ps 命令的输出类似。给出一些未出现过的列的解释

  • USER:进程属主的名字。
  • PR:进程的优先级。
  • NI:进程的谦让度值。
  • VIRT:进程占用的虚拟内存总量。
  • RES:进程占用的物理内存总量。
  • SHR:进程和其他进程共享的内存总量。
  • S:进程的状态(D 代表可中断的休眠状态,R 代表在运行状态,S 代表休眠状态,T 被任务控制信号终止的停止状态,t 代表被 debugger 在跟踪时终止的停止状态,Z 代表僵化状态,I 代表 idle 空闲)。
  • %CPU:进程使用的 CPU 时间比例。
  • %MEM:进程使用的内存占可用内存的比例。
  • TIME+:自进程启动到目前为止的 CPU 时间总量。
  • COMMAND:进程所对应的命令行名称,也就是启动的程序名

默认情况下,top 命令在启动时会按照 %CPU 值对进程排序。可以在 top 运行时使用多种交互命令重新排序。每个交互式命令都是单字符,在 top 命令运行时键入可改变 top 的行为。键入 f 允许你选择对输出进行排序的字段,键入 d 允许你修改轮询间隔。键入 q 可以退出 top。用户在 top 命令的输出上有很大的控制权。用这个工具就能经常找出占用系统大部分资源的罪魁祸首。当然了,一旦找到,下一步就是结束这些进程。这也正是接下来的话题。

kill 发送信号

作为系统管理员,很重要的一个技能就是知道何时以及如何结束一个进程。有时进程挂起了,只需要动动手让进程重新运行或结束就行了。但有时,有的进程会耗尽 CPU 且不释放资源。在这两种情景下,你就需要能控制进程的命令。Linux 沿用了 Unix 进行进程间通信的方法。

在 Linux 中,进程之间通过信号来通信。进程的信号就是预定义好的一个消息,进程能识别它并决定忽略还是作出反应。进程如何处理信号是由开发人员通过编程来决定的。大多数编写完善的程序都能接收和处理标准 Unix 进程信号。这些信号列举如下。

信号 名称 描述
1 HUP 挂起
2 INT 中断
3 QUIT 结束运行
9 KILL 无条件终止
11 SEGV 段错误
15 TERM 尽可能终止
17 STOP 无条件停止运行,但不终止
18 TSTP 停止或暂停,但继续在后台运行
19 CONT 在 STOP 或 TSTP 之后恢复执行

在 Linux 上有两个命令可以向运行中的进程发出进程信号。

kill 命令可通过进程 ID(PID)给进程发信号。默认情况下,kill 命令会向命令行中列出的全部 PID 发送一个 TERM 信号。遗憾的是,你只能用进程的 PID 而不能用命令名,所以 kill 命令有时并不好用。要发送进程信号,你必须是进程的属主或登录为 root 用户。否则会提示Operation not permitted

TERM 信号告诉进程可能的话就停止运行。不过,如果有不服管教的进程,那它通常会忽略这个请求。如果需要强制终止,可以指定广为人知的-9 参数,即 KILL 信号。

同时,-s 参数支持指定其他信号(用信号名或信号值)。

$ kill -s HUP 3940

要检查 kill 命令是否有效,可再运行 ps 或 top 命令,看看问题进程是否已停止。

第二个是 killall 命令,它非常强大,它支持通过进程名而不是 PID 来结束进程。killall 命令也支持通配符,这在系统因负载过大而变得很慢时很有用。

$ killall http*

上例中的命令结束了所有以 http 开头的进程,比如 Apache Web 服务器的 httpd 服务。 以 root 用户身份登录系统时,使用 killall 命令要特别小心,因为很容易就会误用通配符而结束了重要的系统进程。这可能会破坏文件系统。

磁盘相关

在 Linux 系统上有几个命令行命令可以用来帮助管理存储媒体。本节将介绍在日常系统管理中经常用到的核心命令。

mount 挂载磁盘

Linux 文件系统将所有的磁盘都并入一个虚拟目录下。在使用新的存储媒体之前,需要把它放到虚拟目录下。这项工作称为挂载(mounting)。在今天的图形化桌面环境里,大多数 Linux 发行版都能自动挂载特定类型的可移动存储媒体。可移动存储媒体指的是可从 PC 上轻易移除的媒体,比如 CD-ROM、软盘和 U 盘。如果用的发行版不支持自动挂载和卸载可移动存储媒体,就必须手动完成。本节将介绍一些可以帮你管理可移动存储设备的 Linux 命令行命令。

Linux 上用来挂载媒体的命令叫作 mount。默认情况下,mount 命令会输出当前系统上挂载的设备列表。

$ mount
/dev/nvme0n1p1 on / type ext4 (rw,relatime)
/dev/nvme0n1p2 on /home type ext4 (rw,relatime)
/dev/nvme0n1p3 on /boot/EFI type vfat (rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro)
/dev/sda1 on /run/media/testuser/My Ultra type fuseblk (rw,nosuid,nodev,relatime,user_id=0,group_id=0,default_permissions,allow_other,blksize=4096,uhelper=udisks2)

mount 命令提供如下四部分信息:

  • 媒体的设备文件名
  • 媒体挂载到虚拟目录的挂载点
  • 文件系统类型
  • 已挂载媒体的访问状态

上面例子的最后一行输出中,移动硬盘被 KDE 桌面自动挂载到了挂载点/run/media 下。这个移动硬盘本身是 NTFS 格式,但是显示为 fuseblk。fuse 意为 file system in user space,在 archlinux 下,需要使用 ntfs-3g 来识别 NTFS 硬盘。ntfs-3g 并不是内核模块,而是调用 fuse 来挂载的,所以 df -hT 以及 mount 的结果会认为是 fuseblk(blk=block)。

要手动在虚拟目录中挂载设备,需要以 root 用户身份登录,或是以 root 用户身份运行 sudo 命令。下面是手动挂载媒体设备的基本命令:

$ mount -t type device directory

type 参数指定了磁盘被格式化的文件系统类型。Linux 可以识别非常多的文件系统类型。如果是和 Windows PC 共用这些存储设备,通常得使用下列文件系统类型。

  • vfat:Windows 长文件系统。缺点是单文件 4GB 的限制。
  • ntfs:Windows NT、XP、Vista、Win 7 以及 Win10 中广泛使用的高级文件系统。
  • iso9660:标准 CD-ROM 文件系统。
  • exFAT:vfat 升级版,突破了 4GB 的限制。

大多数 U 盘和软盘会被格式化成 vfat/NTFS/exFAT 文件系统。而数据 CD 则必须使用 iso9660 文件系统类型。

后面两个参数定义了该存储设备的设备文件的位置以及挂载点在虚拟目录中的位置。比如说,手动将 U 盘/dev/sdb1 挂载到/media/disk,可用下面的命令:

$ sudo mount -t vfat /dev/sdb1 /media/disk

媒体设备挂载到了虚拟目录后,root 用户就有了对该设备的所有访问权限,而其他用户的访问则会被限制。你可以通过目录权限(后文将介绍权限)指定用户对设备的访问权限。

-o 参数允许在挂载文件系统时添加一些以逗号分隔的额外选项。以下为常用的选项。

  • ro:以只读形式挂载。
  • rw:以读写形式挂载。
  • user:允许普通用户挂载文件系统。
  • check=none:挂载文件系统时不进行完整性校验。
  • loop:挂载一个文件。

从 Linux 系统上移除一个可移动设备时,不能直接从系统上移除,而应该先卸载。Linux 上不能直接弹出已挂载的 CD。如果你在从光驱中移除 CD 时遇到麻烦,通常是因为该 CD 还挂载在虚拟目录里。先卸载它,然后再去尝试弹出。

卸载设备的命令是 umount(是的,你没看错,命令名中并没有字母 n,这一点有时候很让人困惑)。umount 命令的格式非常简单:

umount [directory | device ]

umount 命令支持通过设备文件或者是挂载点来指定要卸载的设备。如果有任何程序正在使用设备上的文件,系统就不会允许你卸载它:

$ sudo umount: /home/rich/mnt
umount: /home/rich/mnt: device is busy
$ cd /home/rich
$ sudo umount /home/rich/mnt
$ ls -l mnt
total 0

上例中,命令行提示符仍然在挂载设备的文件系统目录中,所以 umount 命令无法卸载该镜像文件。一旦命令提示符移出该镜像文件的文件系统,umount 命令就能卸载该镜像文件。

如果在卸载设备时,系统提示设备繁忙,无法卸载设备,通常是有进程还在访问该设备或使用该设备上的文件。这时可用 lsof 命令获得使用它的进程信息,然后在应用中停止使用该设备或停止该进程。lsof 命令的用法很简单:lsof /path/to/device/node,或者 lsof /path/to/mount/point

df 查看磁盘空间

有时你需要知道在某个设备上还有多少磁盘空间。df 命令可以让你很方便地查看所有已挂载磁盘的使用情况

$ df
Filesystem           1K-blocks      Used Available Use% Mounted on
/dev/sda2             18251068   7703964   9605024  45% /
/dev/sda1               101086     18680     77187  20% /boot
tmpfs                   119536         0    119536   0% /dev/shm
/dev/sdb1               127462    113892     13570  90% /media/disk

df 命令会显示每个有数据的已挂载文件系统。如你在前例中看到的,有些已挂载设备仅限系统内部使用。可以注意到,默认大小均为 1024 字节,不利于直观查看,可附加-h 参数进行更直观的查看。它会把输出中的磁盘空间按照用户易读的形式显示,通常用 M 来替代兆字节,用 G 替代吉字节。

$ df -h
Filesystem            Size  Used Avail Use% Mounted on
/dev/sdb2              18G  7.4G  9.2G  45% /
/dev/sda1              99M   19M   76M  20% /boot
tmpfs                 117M     0  117M   0% /dev/shm
/dev/sdb1             125M  112M   14M  90% /media/disk

du 查看目录空间

通过 df 命令很容易发现哪个磁盘的存储空间快没了。系统管理员面临的下一个问题是,发生这种情况时要怎么办。另一个有用的命令是 du 命令。du 命令可以显示某个特定目录(默认情况下是当前目录)的磁盘使用情况。这一方法可用来快速判断系统上某个目录下是不是有超大文件。默认情况下,du 命令会显示当前目录下所有的文件、目录和子目录的磁盘使用情况,它会以磁盘块为单位来表明每个文件或目录占用了多大存储空间。对标准大小的目录来说,这个输出会是一个比较长的列表。

每行输出左边的数值是每个文件或目录占用的磁盘块数。注意,这个列表是从目录层级的最底部开始,然后按文件、子目录、目录逐级向上。 这么用 du 命令(不加参数,用默认参数)作用并不大。我们更想知道每个文件和目录占用了多大的磁盘空间,但如果还得逐页查找的话就没什么意义了。下面是能让 du 命令用起来更方便的几个命令行参数。

  • -s:同时查询多目录时,依次只显示每个输出参数(目录)的总大小。
  • -c:同时查询多目录时,显示所有已列出文件总的大小。
  • -h:按用户易读的格式输出大小,即用 K 替代千字节,用 M 替代兆字节,用 G 替代吉字节。
$ du -sh ./Documents/ ./Desktop/
3.4G    ./Documents/
58G     ./Desktop/
$ du -shc ./Documents/ ./Desktop/
3.4G    ./Documents/
58G     ./Desktop/
62G     总用量

文件数据相关

sort 排序

处理大量数据时的一个常用命令是 sort 命令。顾名思义,sort 命令是对数据进行排序的。默认情况下,sort 命令按照会话指定的默认语言的排序规则对文本文件中的数据行排序。

对数字排序时,如果你本期望这些数字能按值排序,就要失望了。默认情况下,sort 命令会把数字当做字符来执行标准的字符排序,产生的输出可能根本就不是你要的。解决这个问题可用-n 参数,它会告诉 sort 命令把数字识别成数字而不是字符,并且按值排序。

另一个常用的参数是-M,按月排序。Linux 的日志文件经常会在每行的起始位置有一个时间戳,用来表明事件是什么时候发生的,下面是一个例子。

Sep 13 07:10:09 testbox smartd[2718]: Device: /dev/sda, opened

如果将含有时间戳日期的文件按默认的排序方法来排序,并不会得到想要的结果。如果用-M 参数,sort 命令就能识别三字符的月份名,并相应地排序。

-k 和-t 参数在对按字段分隔的数据进行排序时非常有用,例如/etc/passwd 文件。可以用-t 参数来指定字段分隔符,然后用-k 参数来指定排序的字段。举个例子,对密码文件/etc/passwd 根据用户 ID 进行数值排序,可以这么做:

$ sort -t ':' -k 3 -n /etc/passwd
root:x:0:0::/root:/bin/bash
bin:x:1:1::/:/usr/bin/nologin
daemon:x:2:2::/:/usr/bin/nologin
mail:x:8:12::/var/spool/mail:/usr/bin/nologin
ftp:x:14:11::/srv/ftp:/usr/bin/nologin
http:x:33:33::/srv/http:/usr/bin/nologin
uuidd:x:68:68::/:/usr/bin/nologin

现在数据已经按第三个字段——用户 ID 的数值排序。

最后给出一个综合的例子:

$ du -s * | sort -nr
4649672 Android
2726928 Desktop
2224812 Documents
1139980 Games
47172   Downloads
29072   Pictures
560     ThunderNetwork
8       Music
4       下载
4       Videos
注意,-r 参数将结果按降序输出,这样就更容易看到目录下的哪些文件占用空间最多。本例中用到的管道命令( )将 du 命令的输出重定向到 sort 命令。我们将在本书后面进一步讨论。

grep 搜索

你会经常需要在大文件中找一行数据,而这行数据又埋藏在文件的中间。这时并不需要手动翻看整个文件,用 grep 命令来帮助查找就行了。grep 命令的命令行格式如下:

grep [options] pattern [file]

grep 命令会在输入或指定的文件中查找包含匹配指定模式的字符的行。grep 的输出就是包含了匹配模式的行。

$ grep three file1
three
$ grep t file1
two
three

第一个例子在文件 file1 中搜索能匹配模式 three 的文本。grep 命令输出了匹配了该模式的行。第二个例子在文件 file1 中搜索能匹配模式 t 的文本。这个例子里,file1 中有两行匹配了指定的模式,两行都输出了。由于 grep 命令非常流行,它经历了大量的更新。有很多功能被加进了 grep 命令。如果查看一下它的手册页面,你会发现它是多么的无所不能。

如果要进行反向搜索(输出不匹配该模式的行),可加-v 参数。

$ grep -v t file1
one
four
five

如果要显示匹配模式的行所在的行号,可加-n 参数

$ grep -n t file1
2:two
3:three

如果只要知道有多少行含有匹配的模式,可用-c 参数。

$ grep -c t file1
2

如果要指定多个匹配模式,可用-e 参数来指定每个模式。这个例子输出了含有字符 t 或字符 f 的所有行。

$ grep -e t -e f file1
two
three
four
five

默认情况下,grep 命令用基本的 Unix 风格正则表达式来匹配模式。Unix 风格正则表达式采用特殊字符来定义怎样查找匹配的模式。 以下是在 grep 搜索中使用正则表达式的简单例子。

$ grep [tf] file1
two
three
four
five

正则表达式中的方括号表明 grep 应该搜索包含 t 或者 f 字符的匹配。如果不用正则表达式,grep 就会搜索匹配字符串 tf 的文本。

egrep 命令是 grep 的一个衍生,支持 POSIX 扩展正则表达式。POSIX 扩展正则表达式含有更多的可以用来指定匹配模式的字符(后文会讲)。fgrep 则是另外一个版本,支持将匹配模式指定为用换行符分隔的一列固定长度的字符串。这样就可以把这列字符串放到一个文件中,然后在 fgrep 命令中用其在一个大型文件中搜索字符串了。egrep 与 grep -E 相同。 fgrep 与 grep -F 相同,所以掌握好 grep 就好。

tar 压缩与归档

gzip 是非常流行的压缩工具软件包,使用方式也很简单,这个软件包含有下面的工具。

  • gzip:用来压缩文件。
  • gzcat:用来查看压缩过的文本文件的内容。
  • gunzip:用来解压文件
$ gzip myprog #压缩文件
ls -l my*
-rwxrwxr-x 1 rich rich 2197 2007-09-13 11:29 myprog.gz

gzip 命令会压缩你在命令行指定的文件。也可以在命令行指定多个文件名甚至用通配符来一次性批量压缩文件。

$ gzip my*

虽然 gzip 命令能够很好地将数据压缩和归档进单个文件,但它不是 Unix 和 Linux 中的标准归档工具。目前,Unix 和 Linux 上最广泛使用的归档工具是 tar 命令。 tar 命令最开始是用来将文件写到磁带设备上归档的,然而它也能把输出写到文件里,这种用法在 Linux 上已经普遍用来归档数据了。关于 tar 的用法由于历史原因也有三种使用方式,较为复杂,记住常用方式即可。此时使用tldr tar命令,即可很方便的查看常用用法。

系统信息相关

dmidecode 信息大全

dmidecode 命令 可以让你在 Linux 系统下获取有关硬件方面的信息。dmidecode 的作用是将 DMI 数据库中的信息解码,以可读的文本方式显示。由于 DMI 信息可以人为修改,因此里面的信息不一定是系统准确的信息。dmidecode 遵循 SMBIOS/DMI 标准,其输出的信息包括 BIOS、系统、主板、处理器、内存、缓存等等。

DMI(Desktop Management Interface,DMI)就是帮助收集电脑系统信息的管理系统,DMI 信息的收集必须在严格遵照 SMBIOS 规范的前提下进行。SMBIOS(System Management BIOS)是主板或系统制造者以标准格式显示产品管理信息所需遵循的统一规范。SMBIOS 和 DMI 是由行业指导机构 Desktop Management Task Force(DMTF)起草的开放性的技术标准,其中 DMI 设计适用于任何的平台和操作系统。

DMI 充当了管理工具和系统层之间接口的角色。它建立了标准的可管理系统更加方便了电脑厂商和用户对系统的了解。DMI 的主要组成部分是 Management Information Format(MIF)数据库。这个数据库包括了所有有关电脑系统和配件的信息。通过 DMI,用户可以获取序列号、电脑厂商、串口信息以及其它系统配件信息。

一个小技巧是,在笔记本售后网站一般需要输入序列号查询,以下命令可以直接显示出。不用把笔记本翻过来到背面去记序列号再查询了。

$ sudo dmidecode -s system-serial-number

同时,也可以很方便的查看 BIOS 版本、内存频率(也许是唯一查看内存频率的方法)等信息。

upower 电池信息

upower 可以查看电池相关信息,使用如下命令获取详情。可以看到电池损耗,充电比率等实用信息:

$ upower -i `upower -e | grep 'BAT'`

理解 Shell

shell 的父子关系

用于登录的某个虚拟控制器终端,或在 GUI 中运行终端仿真器时所启动的默认的交互 shell,是一个父 shell。本书到目前为止都是父 shell 提供 CLI 提示符,然后等待命令输入。

查看父子结构

在 CLI 提示符后输入/bin/bash 命令或其他等效的 bash 命令时,会创建一个新的 shell 程序。这个 shell 程序被称为子 shell(child shell)。子 shell 也拥有 CLI 提示符,同样会等待命令输入。当输入 bash、生成子 shell 的时候,你是看不到任何相关的信息的,因此需要另一条命令帮助我们理清这一切。在之前讲过的 ps 命令能够派上用场,在生成子 shell 的前后配合选项-f 来观察不同。

$ ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
501       1841  1840  0 11:50 pts/0    00:00:00 -bash
501       2429  1841  4 13:44 pts/0    00:00:00 ps -f
$
$ bash
$ ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
501       1841  1840  0 11:50 pts/0    00:00:00 -bash
501       2430  1841  0 13:44 pts/0    00:00:00 bash
501       2444  2430  1 13:44 pts/0    00:00:00 ps -f
$

第一次使用 ps -f 的时候,显示出了两个进程。其中一个进程的进程 ID 是 1841(第二列),运行的是 bash shell 程序(最后一列)。另一个进程(进程 ID 为 2429)对应的是命令 ps -f。

输入命令 bash 之后,一个子 shell 就出现了。第二个 ps -f 是在子 shell 中执行的。可以从显示结果中看到有两个 bash shell 程序在运行。第一个 bash shell 程序,也就是父 shell 进程,其原始进程 ID 是 1814。第二个 bash shell 程序,即子 shell 进程,其 PID 是 2430。注意,子 shell 的父进程 ID(PPID)是 1841,指明了这个父 shell 进程就是该子 shell 的父进程。

进程就是正在运行的程序。bash shell 是一个程序,当它运行的时候,就成为了一个进程。一个运行着的 shell 就是某种进程而已。因此,在说到运行一个 bash shell 的时候,你经常会看到“shell”和“进程”这两个词交换使用。

在生成子 shell 进程时,只有部分父进程的环境被复制到子 shell 环境中。这会对包括变量在内的一些东西造成影响,我们会在后续谈及相关的内容。

同样的,你也可以在子 shell 中不停的继续创建子 shell,它们最终会形成一个嵌套结构,可以用 ps –forest 命令展示了这些子 shell 间的嵌套结构。可以利用 exit 命令有条不紊地退出各个子 shell。

另一个创建子 shell 的方式是使用进程列表。命令列表要想成为进程列表,这些命令必须包含在括号里。

$ (pwd ; ls ; cd /etc ; pwd ; cd ; pwd ; ls)

括号的加入使一串命令变成了进程列表,生成了一个子 shell 来执行对应的命令。

进程列表是一种命令分组(command grouping)。另一种命令分组是将命令放入花括号中,并在命令列表尾部加上分号(;),前后的空格均不可省略。语法为{ command; }。使用花括号进行命令分组并不会像进程列表那样创建出子 shell。

要想知道是否生成了子 shell,得借助一个使用了环境变量的命令。(环境变量会在后续详述。)这个命令就是 echo $BASH_SUBSHELL。如果该命令返回 0,就表明没有子 shell。如果返回 1 或者其他更大的数字,就表明存在一个或多个子 shell。

下面的例子中使用了一串命令列表,列表尾部是 echo $BASH_SUBSHELL

$ pwd ; ls ; cd /etc ; pwd ; cd ; pwd ; ls ; echo $BASH_SUBSHELL
...
0

在命令输出的最后,显示的是数字 0。这就表明这些命令不是在子 shell 中运行的。要是使用进程列表的话,结果就不一样了。在列表最后加入 echo $BASH_SUBSHELL。

$ (pwd ; ls ; cd /etc ; pwd ; cd ; pwd ; ls ; echo $BASH_SUBSHELL)
...
1

这次在命令输入的最后显示出了数字 1。这表明的确创建了子 shell,并用于执行这些命令。所以说,进程列表就是使用括号包围起来的一组命令,它能够创建出子 shell 来执行这些命令。你甚至可以在进程列表中嵌套括号来创建子 shell 的子 shell。

$ ( pwd ; echo $BASH_SUBSHELL)
/home/Christine
1
$ ( pwd ; (echo $BASH_SUBSHELL))
/home/Christine
2

注意,在第一个进程列表中,数字 1 表明了一个子 shell,这个结果和预期的一样。但是在第二个进程列表中,在命令 echo $BASH_SUBSHELL 外面又多出了一对括号。这对括号在子 shell 中产生了另一个子 shell 来执行命令。因此数字 2 表明的就是这个子 shell。

后台模式

在 shell 脚本中,经常使用子 shell 进行多进程处理。但是采用子 shell 的成本不菲,会明显拖慢处理速度。在交互式的 CLI shell 会话中,子 shell 同样存在问题。它并非真正的多进程处理,因为终端控制着子 shell 的 I/O。

在交互式的 shell CLI 中,还有很多更富有成效的子 shell 用法。进程列表、协程和管道(后续会讲到)都利用了子 shell。它们都可以有效地在交互式 shell 中使用。在交互式 shell 中,一个高效的子 shell 用法就是使用后台模式。在讨论如何将后台模式与子 shell 搭配使用之前,你得先搞明白什么是后台模式。

在后台模式中运行命令可以在处理命令的同时让出 CLI,以供他用。演示后台模式的一个经典命令就是 sleep。sleep 命令接受一个参数,该参数是你希望进程等待(睡眠)的秒数。这个命令在脚本中常用于引入一段时间的暂停。命令 sleep 10 会将会话暂停 10 秒钟,然后返回 shell CLI 提示符。

$ sleep 10

要想将命令置入后台模式,可以在命令末尾加上字符&。把 sleep 命令置入后台模式可以让我们利用 ps 命令来小窥一番。

$ sleep 3000 &
[1] 2396
$ ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
christi+  2338  2337  0 10:13 pts/9    00:00:00 -bash
christi+  2396  2338  0 10:17 pts/9    00:00:00 sleep 3000
christi+  2397  2338  0 10:17 pts/9    00:00:00 ps -f
$

sleep 命令会在后台(&)睡眠 3000 秒(50 分钟)。当它被置入后台,在 shell CLI 提示符返回之前,会出现两条信息。第一条信息是显示在方括号中的后台作业(background job)号(1)。第二条是后台作业的进程 ID(2396)。

ps 命令用来显示各种进程。我们可以注意到命令 sleep 3000 已经被列出来了。在第二列显示的进程 ID(PID)和命令进入后台时所显示的 PID 是一样的,都是 2396。

除了 ps 命令,你也可以使用 jobs 命令来显示后台作业信息。jobs 命令可以显示出当前运行在后台模式中的所有用户的进程(作业)。

$ jobs
[1]+  Running                 sleep 3000 &
$

jobs 命令在方括号中显示出作业号(1)。它还显示了作业的当前状态(running)以及对应的命令(sleep 3000 &)

利用 jobs 命令的-l 选项,你还能够看到更多的相关信息。除了默认信息之外,-l 选项还能够显示出命令的 PID。 一旦后台作业完成,就会显示出结束状态。

$ jobs -l
[1]+ 28331 Done                 sleep 3000 &
$

需要提醒的是:后台作业的结束状态可未必会一直等待到合适的时候才现身。当作业结束状态突然出现在屏幕上的时候,你可别吃惊啊。

之前说过,进程列表是运行在子 shell 中的一条或多条命令。使用包含了 sleep 命令的进程列表,并显示出变量 BASH_SUBSHELL,结果和期望的一样。

$ (sleep 2 ; echo $BASH_SUBSHELL ; sleep 2)
1

在上面的例子中,有一个 2 秒钟的暂停,接着显示出的数字 1 表明只有一个子 shell,在返回提示符之前又经历了另一个 2 秒钟的暂停,没什么特别的。

将相同的进程列表置入后台模式会在命令输出上表现出些许不同。

$ (sleep 2 ; echo $BASH_SUBSHELL ; sleep 2)&
[2] 2401
$ 1
[2]+  Done                  ( sleep 2; echo $BASH_SUBSHELL; sleep 2 )

把进程列表置入后台会产生一个作业号和进程 ID,然后返回到提示符。不过奇怪的是表明单一级子 shell 的数字 1 显示在了提示符的旁边!不要不知所措,只需要按一下回车键,就会得到另一个提示符。

在 CLI 中运用子 shell 的创造性方法之一就是将进程列表置入后台模式。你既可以在子 shell 中进行繁重的处理工作,同时也不会让子 shell 的 I/O 受制于终端。

当然了,sleep 和 echo 命令的进程列表只是作为一个示例而已。使用 tar 创建备份文件是有效利用后台进程列表的一个更实用的例子。

$ (tar -cf Rich.tar /home/rich ; tar -cf My.tar /home/christine)&
[3] 2423

协程的使用

将进程列表置入后台模式并不是子 shell 在 CLI 中仅有的创造性用法。协程就是另一种方法。协程可以同时做两件事。它在后台生成一个子 shell,并在这个子 shell 中执行命令。要进行协程处理,得使用 coproc 命令,还有要在子 shell 中执行的命令。

$ coproc sleep 10
[1] 2544
$

除了会创建子 shell 之外,协程基本上就是将命令置入后台模式。当输入 coproc 命令及其参数之后,你会发现启用了一个后台作业。屏幕上会显示出后台作业号(1)以及进程 ID(2544)。jobs 命令能够显示出协程的处理状态

$ jobs
[1]+  Running                 coproc COPROC sleep 10 &

在上面的例子中可以看到在子 shell 中执行的后台命令是 coproc COPROC sleep 10。COPROC 是 coproc 命令给进程起的名字。你可以使用命令的扩展语法自己设置这个名字。

$ coproc My_Job { sleep 10; }
[1] 2570
$
$ jobs [1]+  Running                 coproc My_Job { sleep 10; } &
$

通过使用扩展语法,协程的名字被设置成 My_Job。这里要注意的是,扩展语法写起来有点麻烦。必须确保在第一个花括号({)和命令名之间有一个空格。还必须保证命令以分号(;)结尾。另外,分号和闭花括号(})之间也得有一个空格。

协程能够让你尽情发挥想象力,发送或接收来自子 shell 中进程的信息。只有在拥有多个协程的时候才需要对协程进行命名,因为你得和它们进行通信。否则的话,让 coproc 命令将其设置成默认的名字 COPROC 就行了。

你可以发挥才智,将协程与进程列表结合起来产生嵌套的子 shell。只需要输入进程列表,然后把命令 coproc 放在前面就行了。

$ coproc ( sleep 10; sleep 2 )
[1] 143311
$ jobs
[1]+  Running     coproc COPROC ( sleep 10; sleep 2 ) &
$ ps -f --forest
testuser    142848  142839  0 18:00 pts/1    00:00:00 /bin/bash
testuser    143311  142848  0 18:01 pts/1    00:00:00  \_ /bin/bash
testuser    143312  143311  0 18:01 pts/1    00:00:00  |   \_ sleep 10
testuser    143776  142848  0 18:02 pts/1    00:00:00  \_ ps -f --forest

理解 shell 的内建命令

在学习 GNU bash shell 期间,你可能听到过“内建命令”这个术语。搞明白 shell 的内建命令和非内建(外部)命令非常重要。内建命令和非内建命令的操作方式大不相同。

外部命令,有时候也被称为文件系统命令,是存在于 bash shell 之外的程序。它们并不是 shell 程序的一部分。外部命令程序通常位于/bin、/usr/bin、/sbin 或/usr/sbin 中。

ps 就是一个外部命令。你可以使用 which 和 type 命令找到它。

$ which ps
/bin/ps
$
$ type -a ps
ps is /bin/ps
$
$ ls -l /bin/ps
-rwxr-xr-x 1 root root 93232 Jan  6 18:32 /bin/ps
$

当外部命令执行时,会创建出一个子进程。这种操作被称为衍生(forking)。外部命令 ps 很方便显示出它的父进程以及自己所对应的衍生子进程。

$ ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
christi+  2743  2742  0 17:09 pts/9    00:00:00 -bash
christi+  2801  2743  0 17:16 pts/9    00:00:00 ps -f
$

作为外部命令,ps 命令执行时会创建出一个子进程。在这里,ps 命令的 PID 是 2801,父 PID 是 2743。作为父进程的 bash shell 的 PID 是 2743。

当进程必须执行衍生操作时,它需要花费时间和精力来设置新子进程的环境。所以说,外部命令多少还是有代价的。

就算衍生出子进程或是创建了子 shell,你仍然可以通过发送信号与其沟通,这一点无论是在命令行还是在脚本编写中都是极其有用的。发送信号(signaling)使得进程间可以通过信号进行通信。信号及其发送会在后续章节中讲到。


内建命令和外部命令的区别在于前者不需要使用子进程来执行。它们已经和 shell 编译成了一体,作为 shell 工具的组成部分存在。不需要借助外部程序文件来运行

cd 和 exit 命令都内建于 bash shell。可以利用 type 命令来了解某个命令是否是内建的。

$ type cd
cd is a shell builtin
$
$ type exit
exit is a shell builtin
$

因为既不需要通过衍生出子进程来执行,也不需要打开程序文件,内建命令的执行速度要更快,效率也更高。

要注意,有些命令有多种实现。例如 echo 和 pwd 既有内建命令也有外部命令。两种实现略有不同。要查看命令的不同实现,使用 type 命令的-a 选项。

$ type -a echo
echo is a shell builtin
echo is /bin/echo
$
$ which echo
/bin/echo
$
$ type -a pwd
pwd is a shell builtin
pwd is /bin/pwd
$
$ which pwd
/bin/pwd
$

命令 type -a 显示出了每个命令的两种实现。注意,which 命令只显示出了外部命令文件。

对于有多种实现的命令,如果想要使用其外部命令实现,直接指明对应的文件就可以了。例如,要使用外部命令 pwd,可以输入/bin/pwd。

history 历史记录

一个有用的内建命令是 history 命令。bash shell 会跟踪你用过的命令。你可以唤回这些命令并重新使用。要查看最近用过的命令列表,可以输入不带选项的 history 命令。通常历史记录中会保存最近的 500/1000 条命令。这个数量可是不少的!你可以设置保存在 bash 历史记录中的命令数。要想实现这一点,你需要修改名为 HISTSIZE 的环境变量。

你可以唤回并重用历史列表中最近的命令。这样能够节省时间和击键量。输入!!,然后按回车键就能够唤出刚刚用过的那条命令来使用。或者点击方向键中的向上键,也能使用刚刚用过的那条命令,连续点击还能向上翻阅执行历史命令。

你可以唤回历史列表中任意一条命令。只需输入惊叹号和命令在历史列表中的编号即可。如执行!20

命令历史记录被保存在隐藏文件.bash_history 中,它位于用户的主目录中。这里要注意的是,bash 命令的历史记录是先存放在内存中,当 shell 退出时才被写入到历史文件中。

可以在退出 shell 会话之前强制将命令历史记录写入.bash_history 文件。要实现强制写入,需要使用 history 命令的-a 选项。

如果你打开了多个终端会话,仍然可以使用 history -a 命令在打开的会话中向.bash_history 文件中添加记录。但是对于其他打开的终端会话,历史记录并不会自动更新。这是因为.bash_history 文件只有在打开首个终端会话时才会被读取。要想强制重新读取.bash_history 文件,更新终端会话的历史记录,可以使用 history -n 命令。

使用 bash shell 命令历史记录能够大大地节省时间。利用内建的 history 命令能够做到的事情远不止这里所描述的。可以通过输入 man history 来查看 history 命令的 bash 手册页面。

alias 命令别名

alias 命令是另一个 shell 的内建命令。命令别名允许你为常用的命令(及其参数)创建另一个名称,从而将输入量减少到最低。你所使用的 Linux 发行版很有可能已经为你设置好了一些常用命令的别名。要查看当前可用的别名,使用 alias 命令以及选项-p。

$ alias -p
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l='ls -CF'
alias la='ls -A'
alias ll='ls -alF'
alias ls='ls --color=auto' #表明终端支持彩色模式的列表

可以使用 alias 命令创建属于自己的别名。

alias li='ls -li'

在定义好别名之后,你随时都可以在 shell 中使用它,就算在 shell 脚本中也没问题。要注意,因为命令别名属于内部命令,一个别名仅在它所被定义的 shell 进程中才有效。

$ alias li='ls -li'
$ bash
$ li
bash: li: command not found

不过好在有办法能够让别名在不同的子 shell 中都奏效。下一章中就会讲到具体的做法。shell、子 shell、进程和衍生进程都会受到环境变量的影响。下一章,我们会探究环境变量的影响方式以及如何在不同的上下文中使用环境变量。

Linux 环境变量

Linux 环境变量能帮你提升 Linux shell 体验。很多程序和脚本都通过环境变量来获取系统信息、存储临时数据和配置信息。在 Linux 系统上有很多地方可以设置环境变量,了解去哪里设置相应的环境变量很重要。

认识环境变量

bash shell 用环境变量(environment variable)的特性来存储有关 shell 会话和工作环境的信息(这也是它们被称作环境变量的原因)。这项特性允许你在内存中存储数据,以便程序或 shell 中运行的脚本能够轻松访问到它们。这也是存储持久数据的一种简便方法。 在 bash shell 中,环境变量分为两类:

  • 全局变量
  • 局部变量

全局环境变量对于 shell 会话和所有生成的子 shell 都是可见的。局部变量则只对创建它们的 shell 可见。这让全局环境变量对那些所创建的子 shell 需要获取父 shell 信息的程序来说非常有用。Linux 系统在你开始 bash 会话时就设置了一些全局环境变量。系统环境变量基本上都是使用全大写字母,以区别于普通用户的环境变量。要查看全局变量,可以使用 env 或 printenv 命令。

系统为 bash shell 设置的全局环境变量数目众多,我们不得不在展示的时候进行删减。其中有很多是在登录过程中设置的,另外,你的登录方式也会影响到所设置的环境变量。

要显示个别环境变量的值,可以使用 printenv 命令,但是不要用 env 命令。

$ printenv HOME
/home/Christine
$
$ env HOME
env: HOME: No such file or directory
$

也可以使用 echo 显示变量的值。在这种情况下引用某个环境变量的时候,必须在变量前面加上一个美元符($)。

$ echo $HOME
/home/Christine
$

在变量名前加上$也可在其他命令中作为参数使用

$ ls $HOME ## 等价与ls ~

正如前面提到的,全局环境变量可用于进程的所有子 shell。

$ bash
$
$ ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
501       2017  2016  0 16:00 pts/0    00:00:00 -bash
501       2082  2017  0 16:08 pts/0    00:00:00 bash
501       2095  2082  0 16:08 pts/0    00:00:00 ps -f
$
$ echo $HOME
/home/Christine
$
$ exit
$

在这个例子中,用 bash 命令生成一个子 shell 后,显示了 HOME 环境变量的当前值,这个值和父 shell 中的一模一样,都是/home/Chrisine。


局部环境变量,顾名思义只能在定义它们的进程中可见。尽管它们是局部的,但是和全局环境变量一样重要。事实上,Linux 系统也默认定义了标准的局部环境变量。不过你也可以定义自己的局部变量,如你所想,这些变量被称为用户定义局部变量。

查看局部环境变量的列表有点复杂。遗憾的是,在 Linux 系统并没有一个只显示局部环境变量的命令。set 命令会显示为某个特定进程设置的所有环境变量,包括局部变量、全局变量以及用户定义变量。

所有通过 printenv 命令能看到的全局环境变量都出现在了 set 命令的输出中。但在 set 命令的输出中还有其他一些环境变量,即局部环境变量和用户定义变量。

命令 env、printenv 和 set 之间的差异很细微。set 命令会显示出全局变量、局部变量以及用户定义变量。它还会按照字母顺序对结果进行排序。env 和 printenv 命令同 set 命令的区别在于前两个命令不会对变量排序,也不会输出局部变量和用户定义变量。在这种情况下,env 和 printenv 的输出是重复的。不过 env 命令除了查看环境变量外还有一些其他功能。

操作环境变量

一旦启动了 bash shell(或者执行一个 shell 脚本),就能创建在这个 shell 进程内可见的局部变量了。可以通过等号给环境变量赋值,值可以是数值或字符串

$ echo $my_variable
$ my_variable=Hello
$
$ echo $my_variable
Hello

非常简单!现在每次引用 my_variable 环境变量的值,只要通过$my_variable 引用即可。如果要给变量赋一个含有空格的字符串值,必须用引号来界定字符串的首和尾。没有单引号的话,bash shell 会以为下一个词是另一个要执行的命令。注意,你定义的局部环境变量用的是小写字母,而到目前为止你所看到的系统环境变量都是大写字母。

所有的环境变量名均使用大写字母,这是 bash shell 的标准惯例。如果是你自己创建的局部变量或是 shell 脚本,请使用小写字母。变量名区分大小写。在涉及用户定义的局部变量时坚持使用小写字母,这能够避免重新定义系统环境变量可能带来的灾难。

设置了局部环境变量后,就能在 shell 进程的任何地方使用它了。但是,如果生成了另外一个 子 shell,它在子 shell 中就不可用。当你退出子 shell 并回到原来的 shell 时,这个局部环境变量依然可用。

类似地,如果你在子进程中设置了一个局部变量,那么一旦你退出了子进程,那个局部环境变量就不可用。

这种时候可以设置全局环境变量。在设定全局环境变量的进程所创建的子进程中,该变量都是可见的。创建全局环境变量的方法是先创建一个局部环境变量,然后再把它导出到全局环境中。这个过程通过 export 命令来完成。

$ my_variable="I am Global now"
$ export my_variable

在定义并导出局部环境变量 my_variable 后,可通过 bash 命令启动一个子 shell。在这个子 shell 中能够正确的显示出变量 my_variable 的值。该变量能够保留住它的值是因为 export 命令使其变成了全局环境变量。

修改子 shell 中全局环境变量并不会影响到父 shell 中该变量的值。除此之外,子 shell 甚至无法使用 export 命令改变父 shell 中全局环境变量的值。

当然,既然可以创建新的环境变量,自然也能删除已经存在的环境变量。可以用 unset 命令完成这个操作。在 unset 命令中引用环境变量时,记住不要使用$。

$ echo $my_variable
I am Global now
$
$ unset my_variable
$
$ echo $my_variable
$

在涉及环境变量名时,什么时候该使用$,什么时候不该使用$,实在让人摸不着头脑。记住一点就行了:如果要用到变量,使用$;如果要操作变量,不使用$。这条规则的一个例外就是使用 printenv 显示某个变量的值

在处理全局环境变量时,事情就有点棘手了。如果你是在子进程中删除了一个全局环境变量,这只对子进程有效。该全局环境变量在父进程中依然可用。和修改变量一样,在子 shell 中删除全局变量后,你无法将效果反映到父 shell 中。

默认的环境变量

默认情况下,bash shell 会用一些特定的环境变量来定义系统环境。这些变量在你的 Linux 系统上都已经设置好了,只管放心使用。bash shell 源自当初的 Unix Bourne shell,因此也保留了 Unix Bourne shell 里定义的那些环境变量。除了默认的 Bourne 的环境变量,bash shell 还提供一些自有的变量。你可能已经注意到,不是所有的默认环境变量都会在运行 set 命令时列出。尽管这些都是默认环境变量,但并不是每一个都必须有一个值。

其中最重要的一个环境变量为 PATH。当你在 shell 命令行界面中输入一个外部命令时,shell 必须搜索系统来找到对应的程序。PATH 环境变量定义了用于进行命令和程序查找的目录。在本书所用的 Arch Linux 系统中,PATH 环境变量的内容是这样的:

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl

输出中显示了多个可供 shell 用来查找命令和程序的目录。PATH 中的目录使用冒号分隔。如果命令或者程序的位置没有包括在 PATH 变量中,那么如果不使用绝对路径的话,shell 是没法找到的。如果 shell 找不到指定的命令或程序,它会产生一个经典的错误信息:command not found

一般来说,默认环境变量有很多,在需要用到时查阅用法即可,不必全部记忆。

可以把新的搜索目录添加到现有的 PATH 环境变量中,无需从头定义。PATH 中各个目录之间是用冒号分隔的。你只需引用原来的 PATH 值,然后再给这个字符串添加新目录就行了。

$ PATH=$PATH:/home/christine/Scripts

一些程序员习惯将单点符也加入 PATH 环境变量。该单点符代表当前目录

$ PATH=$PATH:.

如此对 PATH 变量的修改只能持续到终端退出或重启系统。这种效果并不能一直持续。在下一节中,你会学到如何永久保持环境变量的修改效果。

定位系统环境变量

在你登入 Linux 系统启动一个 bash shell 时,默认情况下 bash 会在几个文件中查找命令。这些文件叫作启动文件或环境文件。bash 检查的启动文件取决于你启动 bash shell 的方式。启动 bash shell 有 3 种方式:

  • 登录时作为默认登录 shell
  • 作为非登录 shell 的交互式 shell
  • 作为运行脚本的非交互 shell

下面几节介绍了 bash shell 在不同的方式下启动文件。

登录 shell

当你登录 Linux 系统时,bash shell 会作为登录 shell 启动。登录 shell 会从 5 个不同的启动文件里读取命令:

  • /etc/profile
  • $HOME/.bash_profile
  • $HOME/.bashrc
  • $HOME/.bash_login
  • $HOME/.profile

/etc/profile 文件是系统上默认的 bash shell 的主启动文件。系统上的每个用户登录时都会执行这个启动文件

要留意的是有些 Linux 发行版使用了可拆卸式认证模块(Pluggable Authentication Modules ,PAM)。在这种情况下,PAM 文件会在 bash shell 启动之前处理,这些文件中可能会包含环境变量。PAM 文件包括/etc/environment 文件和$HOME/.pam_environment 文件。PAM 更多的相关信息可以在 http://linux-pam.org 中找到。

另外 4 个启动文件是针对用户的,可根据个人需求定制。注意,这四个文件都以点号开头,这说明它们是隐藏文件(不会在通常的 ls 命令输出列表中出现)。它们位于用户的 HOME 目录下,所以每个用户都可以编辑这些文件并添加自己的环境变量,这些环境变量会在每次启动 bash shell 会话时生效。

Linux 发行版在环境文件方面存在的差异非常大。本节中所列出的$HOME下的那些文件并非每个用户都有。例如有些用户可能只有一个$HOME/.bash_profile 文件。这很正常。

  1. /etc/profile 文件

/etc/profile 文件是 bash shell 默认的的主启动文件。只要你登录了 Linux 系统,bash 就会执行/etc/profile 启动文件中的命令。不同的 Linux 发行版在这个文件里放了不同的命令。每个发行版的/etc/profile 文件都有不同的设置和命令。例如,在 Arch Linux 的/etc/profile 文件中,涉及了一个叫作/etc/bash.bashrc 的文件。这个文件包含了系统环境变量等其他内容。但是,/etc/profile 各个发行版有所不同,在 CentOS 发行版的/etc/profile 文件中,并没有出现这个文件。

许多发行版的/etc/profile 文件都用到了同一个特性:for 语句。它用来迭代/etc/profile.d 目录下的所有文件(该语句会在后续章节详述)。这为 Linux 系统提供了一个放置特定应用程序启动文件的地方,当用户登录时,shell 会执行这些文件。在本书所用的 Arch Linux 系统中,/etc/profile.d 目录下包含以下文件:

$ ls -l /etc/profile.d
总用量 36
-rw-r--r-- 1 root root  545 Oct 20 17:10 freetype2.sh
-rw-r--r-- 1 root root 1107 Apr 15  2020 gawk.csh
-rw-r--r-- 1 root root  757 Apr 15  2020 gawk.sh
-rwxr-xr-x 1 root root  105 Sep 25 21:52 gpm.sh
-rw-r--r-- 1 root root  766 Sep  3 06:30 locale.sh
-rw-r--r-- 1 root root  468 Sep 18 05:12 perlbin.csh
-rw-r--r-- 1 root root  464 Sep 18 05:12 perlbin.sh

不难发现,有些文件与系统中的特定应用有关。大部分应用都会创建两个启动文件:一个供 bash shell 使用(使用.sh 扩展名),一个供 c shell 使用(使用.csh 扩展名)。locale.sh 文件会尝试去判定系统上所采用的默认语言字符集,然后设置对应的 LANG 环境变量。

  1. $HOME 目录下的启动文件

这些启动文件都起着同一个作用:提供一个用户专属的启动文件来定义该用户所用到的环境变量。大多数 Linux 发行版只用这四个启动文件中的一到两个:

  • $HOME/.bash_profile
  • $HOME/.bashrc
  • $HOME/.bash_login
  • $HOME/.profile

shell 会按照按照下列顺序,运行第一个被找到的文件,余下的则被忽略:

  • $HOME/.bash_profile
  • $HOME/.bash_login
  • $HOME/.profile

注意,这个列表中并没有$HOME/.bashrc 文件。这是因为该文件通常通过其他文件运行的。

记住,$HOME 表示的是某个用户的主目录。它和波浪号(~)的作用一样。

Arch Linux 系统中的.bash_profile 文件的内容如下

$ cat $HOME/.bash_profile
#
## ~/.bash_profile
#

[[ -f ~/.bashrc ]] && . ~/.bashrc

.bash_profile 启动文件会先去检查 HOME 目录中是不是还有一个叫.bashrc 的启动文件。如果有的话,会先执行启动文件里面的命令。

交互式 shell 进程

如果你的 bash shell 不是登录系统时启动的(比如是在命令行提示符下敲入 bash 时启动),那么你启动的 shell 叫作交互式 shell。交互式 shell 不会像登录 shell 一样运行,但它依然提供了命令行提示符来输入命令。

如果 bash 是作为交互式 shell 启动的,它就不会访问/etc/profile 文件,只会检查用户 HOME 目录中的.bashrc 文件。

在本书所用的 Arch Linux 系统上,这个文件看起来如下:

$ cat .bashrc
#
## ~/.bashrc
#

## If not running interactively, don't do anything
[[ $- != *i* ]] && return

alias ls='ls --color=auto'
PS1='[\u@\h \W]\$'

.bashrc 文件有两个作用:一是查看并执行/etc 目录下通用的 bashrc 文件(/etc/bashrc),在 Arch Linux 上无此表现,但是在 Centos 上是存在的。二是为用户提供一个定制自己的命令别名和私有脚本函数(将在后文讲到)的地方。

上面的 PS1 值就是终端下提示符的格式,如[testuser@archlinux ~]$

非交互式 shell

最后一种 shell 是非交互式 shell。系统执行 shell 脚本时用的就是这种 shell。不同的地方在于它没有命令行提示符。但是当你在系统上运行脚本时,也许希望能够运行一些特定启动的命令。

脚本能以不同的方式执行。只有其中的某一些方式能够启动子 shell。你会在后续学习到 shell 不同的执行方式。

为了处理这种情况,bash shell 提供了 BASH_ENV 环境变量。当 shell 启动一个非交互式 shell 进程时,它会检查这个环境变量来查看要执行的启动文件。如果有指定的文件,shell 会执行该文件里的命令,这通常包括 shell 脚本变量设置。

在本书所用的 Arch Linux 发行版中,变量 BASH_ENV 没有被设置。记住,如果变量未设置,echo 命令会显示一个空行,然后返回 CLI 提示符:

$ printenv BASH_ENV

$

那如果 BASH_ENV 变量没有设置,shell 脚本到哪里去获得它们的环境变量呢?别忘了有些 shell 脚本是通过启动一个子 shell 来执行的。子 shell 可以继承父 shell 导出过的变量。

举例来说,如果父 shell 是登录 shell,在/etc/profile、/etc/profile.d/*.sh 和$HOME/.bashrc 文件中设置并导出了变量,用于执行脚本的子 shell 就能够继承这些变量。

要记住,由父 shell 设置但并未导出的变量都是局部变量。子 shell 无法继承局部变量。

对于那些不启动子 shell 的脚本,变量已经存在于当前 shell 中了。所以就算没有设置 BASH_ENV,也可以使用当前 shell 的局部变量和全局变量。

环境变量持久化

现在你已经了解了各种 shell 进程以及对应的环境文件,找出永久性环境变量就容易多了。也可以利用这些文件创建自己的永久性全局变量或局部变量。

对全局环境变量来说(Linux 系统中所有用户都需要使用的变量),可能更倾向于将新的或修改过的变量设置放在/etc/profile 文件中,但这可不是什么好主意。如果你升级了所用的发行版,这个文件也会跟着更新,那你所有定制过的变量设置可就都没有了。

最好是在/etc/profile.d 目录中创建一个以.sh 结尾的文件。把所有新的或修改过的全局环境变量设置放在这个文件中。

在大多数发行版中,存储个人用户永久性 bash shell 变量的地方是$HOME/.bashrc文件。这一点适用于所有类型的shell进程。但如果设置了BASH_ENV变量,那么记住,除非它指向的是$HOME/.bashrc,否则你应该将非交互式 shell 的用户变量放在别的地方。

图形化界面组成部分(如 GUI 客户端)的环境变量可能需要在另外一些配置文件中设置,这和设置 bash shell 环境变量的地方可能不一样。

想想之前讲过的 alias 命令设置就是不能持久的。你可以把自己的 alias 设置放在$HOME/.bashrc 启动文件中,使其效果永久化。

数组变量

环境变量有一个很酷的特性就是,它们可作为数组使用。数组是能够存储多个值的变量。这些值可以单独引用,也可以作为整个数组来引用。 要给某个环境变量设置多个值,可以把值放在括号里,值与值之间用空格分隔。

mytest=(one two three four five)

没什么特别的地方。如果你想把数组像普通的环境变量那样显示,你会失望的。

$ echo $mytest
one

只有数组的第一个值显示出来了。要引用一个单独的数组元素,就必须用代表它在数组中位置的数值索引值。索引值要用方括号括起来。

$ echo ${mytest[2]}
three

环境变量数组的索引值都是从零开始。这通常会带来一些困惑。

要显示整个数组变量,可用星号作为通配符放在索引值的位置。

$ echo ${mytest[*]}
one two three four five

甚至能用 unset 命令删除数组中的某个值,但是要小心,这可能会有点复杂。看下面的例子。

$ unset mytest[2]
$
$ echo ${mytest[*]}
one two four five
$
$ echo ${mytest[2]}

$ echo ${mytest[3]}
four
$

这个例子用 unset 命令删除在索引值为 2 的位置上的值。显示整个数组时,看起来像是索引里面已经没这个索引了。但当专门显示索引值为 2 的位置上的值时,就能看到这个位置是空的。

最后,可以在 unset 命令后跟上数组名来删除整个数组。

$ unset mytest
$
$ echo ${mytest[*]}

$

有时数组变量会让事情很麻烦,所以在 shell 脚本编程时并不常用。对其他 shell 而言,数组变量的可移植性并不好,如果需要在不同的 shell 环境下从事大量的脚本编写工作,这会带来很多不便。有些 bash 系统环境变量使用了数组(比如 BASH_VERSINFO),但总体上不会太频繁用到。

理解 Linux 文件权限

缺乏安全性的系统不是完整的系统。系统中必须有一套能够保护文件免遭非授权用户浏览或修改的机制。Linux 沿用了 Unix 文件权限的办法,即允许用户和组根据每个文件和目录的安全性设置来访问文件。本章将介绍如何在必要时利用 Linux 文件安全系统保护和共享数据。

Linux 的安全性

Linux 安全系统的核心是用户账户。每个能进入 Linux 系统的用户都会被分配唯一的用户账户。用户对系统中各种对象的访问权限取决于他们登录系统时用的账户。用户权限是通过创建用户时分配的用户 ID(User ID,通常缩写为 UID)来跟踪的。UID 是数值,每个用户都有唯一的 UID,但在登录系统时用的不是 UID,而是登录名。

Linux 系统使用特定的文件和工具来跟踪和管理系统上的用户账户。在我们讨论文件权限之前,先来看一下 Linux 是怎样处理用户账户的。本节会介绍管理用户账户需要的文件和工具,这样在处理文件权限问题时,你就知道如何使用它们了。

/etc/passwd 文件

Linux 系统使用一个专门的文件来将用户的登录名匹配到对应的 UID 值。这个文件就是/etc/passwd 文件,它包含了一些与用户有关的信息。下面是 Linux 系统上典型的/etc/passwd 文件的一个例子。

$ cat /etc/passwd
root:x:0:0::/root:/bin/bash
bin:x:1:1::/:/usr/bin/nologin
daemon:x:2:2::/:/usr/bin/nologin
http:x:33:33::/srv/http:/usr/bin/nologin
nobody:x:65534:65534:Nobody:/:/usr/bin/nologin
dbus:x:81:81:System Message Bus:/:/usr/bin/nologin
systemd-journal-remote:x:982:982:systemd Journal Remote:/:/usr/bin/nologin
systemd-network:x:981:981:systemd Network Management:/:/usr/bin/nologin
testuser:x:1000:985::/home/testuser:/bin/bash
cups:x:209:209:cups helper user:/:/usr/bin/nologin
dhcpcd:x:969:969:dhcpcd privilege separation:/var/lib/dhcpcd:/usr/bin/nologin

root 用户账户是 Linux 系统的管理员,固定分配给它的 UID 是 0。就像上例中显示的,Linux 系统会为各种各样的功能创建不同的用户账户,而这些账户并不是真的用户。这些账户叫作系统账户,是系统上运行的各种服务进程访问资源用的特殊账户。所有运行在后台的服务都需要用一个系统用户账户登录到 Linux 系统上。

在安全成为一个大问题之前,这些服务经常会用 root 账户登录。遗憾的是,如果有非授权的用户攻陷了这些服务中的一个,他立刻就能作为 root 用户进入系统。为了防止发生这种情况,现在运行在 Linux 服务器后台的几乎所有的服务都是用自己的账户登录。这样的话,即使有人攻入了某个服务,也无法访问整个系统。

Linux 为系统账户预留了 1000 以下的 UID 值。有些服务甚至要用特定的 UID 才能正常工作。为普通用户创建账户时,大多数 Linux 系统会从 1000 开始,将第一个可用 UID 分配给这个账户(并非所有的 Linux 发行版都是这样)。 你可能已经注意到/etc/passwd 文件中还有很多用户登录名和 UID 之外的信息。/etc/passwd 文件的字段包含了如下信息:

  • 登录用户名
  • 用户密码
  • 用户账户的 UID(数字形式)
  • 用户账户的组 ID(GID)(数字形式)
  • 用户账户的文本描述(称为备注字段)
  • 用户 HOME 目录的位置
  • 用户的默认 shell

/etc/passwd 文件中的密码字段都被设置成了 x,这并不是说所有的用户账户都用相同的密码。在早期的 Linux 上,/etc/passwd 文件里有加密后的用户密码。但鉴于很多程序都需要访问/etc/passwd 文件获取用户信息,这就成了一个安全隐患。随着用来破解加密密码的工具的不断演进,用心不良的人开始忙于破解存储在/etc/passwd 文件中的密码。Linux 开发人员需要重新考虑这个策略。

现在,绝大多数 Linux 系统都将用户密码保存在另一个单独的文件中(叫作 shadow 文件,位置在/etc/shadow)。只有特定的程序(比如登录程序)才能访问这个文件。

/etc/passwd 是一个标准的文本文件。你可以用任何文本编辑器在/etc/password 文件里直接手动进行用户管理(比如添加、修改或删除用户账户)。但这样做极其危险。如果/etc/passwd 文件出现损坏,系统就无法读取它的内容了,这样会导致用户无法正常登录(即便是 root 用户)。用标准的 Linux 用户管理工具去执行这些用户管理功能就会安全许多。

/etc/shadow 文件

/etc/shadow 文件对 Linux 系统密码管理提供了更多的控制。只有 root 用户才能访问/etc/shadow 文件,这让它比起/etc/passwd 安全许多。

/etc/shadow 文件为系统上的每个用户账户都保存了一条记录。记录就像下面这样:

testuser:$6$inJRhswsgTqYbpOp$Tjas9zGdk/lLovn4M1xxczsZF/5ZlJlqnjyDiaRuTTs.:18361:0:99999:7:::

在/etc/shadow 文件的每条记录中都有 9 个字段

  • 与/etc/passwd 文件中的登录名字段对应的登录名
  • 加密后的密码
  • 自上次修改密码后过去的天数(自 1970 年 1 月 1 日开始计算)
  • 多少天后才能更改密码
  • 多少天后必须更改密码
  • 密码过期前提前多少天提醒用户更改密码
  • 密码过期后多少天禁用用户账户
  • 用户账户被禁用的日期(用自 1970 年 1 月 1 日到当天的天数表示)
  • 预留字段给将来使用

使用 shadow 密码系统后,Linux 系统可以更好地控制用户密码。它可以控制用户多久更改一次密码,以及什么时候禁用该用户账户,如果密码未更新的话。

添加新用户

用来向 Linux 系统添加新用户的主要工具是 useradd。这个命令简单快捷,可以一次性创建新用户账户及设置用户 HOME 目录结构。useradd 命令使用系统的默认值以及命令行参数来设置用户账户。系统默认值被设置在/etc/default/useradd 文件中。

$ sudo cat /etc/default/useradd
## useradd defaults file for ArchLinux
## original changes by TomK
GROUP=users
HOME=/home
INACTIVE=-1
EXPIRE=
SHELL=/bin/bash
SKEL=/etc/skel
CREATE_MAIL_SPOOL=no

在创建新用户时,如果你不在命令行中指定具体的值,useradd 命令就会使用那些默认值。这个例子列出的默认值如下:

  • 新用户会被添加到 users 的公共组;
  • 新用户的 HOME 目录将会位于/home/loginname;
  • 新用户账户密码在过期后不会被禁用;
  • 新用户账户未被设置过期日期;
  • 新用户账户将 bash shell 作为默认 shell;
  • 系统会将/etc/skel 目录下的内容复制到用户的 HOME 目录下;
  • 系统不会为该用户账户在 mail 目录下创建一个用于接收邮件的文件。

倒数第二个值很有意思。useradd 命令允许管理员创建一份默认的 HOME 目录配置,然后把它作为创建新用户 HOME 目录的模板。这样就能自动在每个新用户的 HOME 目录里放置默认的系统文件。在 Arch Linux 系统上,/etc/skel 目录有下列文件:

$ ls -al /etc/skel
总用量 20
drwxr-xr-x  2 root root 4096 Aug 20 13:32 .
drwxr-xr-x 99 root root 4096 Nov 26 13:03 ..
-rw-r--r--  1 root root   21 Aug 10 00:27 .bash_logout
-rw-r--r--  1 root root   57 Aug 10 00:27 .bash_profile
-rw-r--r--  1 root root  141 Aug 10 00:27 .bashrc

它们是 bash shell 环境的标准启动文件。系统会自动将这些默认文件复制到你创建的每个用户的 HOME 目录。

可以用默认系统参数创建一个新用户账户,然后检查一下新用户的 HOME 目录。

$ sudo useradd -m test

默认情况下,useradd 命令不会创建 HOME 目录,但是-m 命令行选项会使其创建 HOME 目录。你能在此例中看到,useradd 命令创建了新 HOME 目录,并将/etc/skel 目录中的文件复制了过来。

运行本章中提到的用户账户管理命令,需要以 root 用户账户登录或者通过 sudo 命令以 root 用户账户身份运行这些命令。

要想在创建用户时改变默认值或默认行为,可以使用 useradd 的额外命令行参数,具体可参见 man。

你会发现,在创建新用户账户时使用命令行参数可以更改系统指定的默认值。但如果总需要修改某个值的话,最好还是修改一下系统的默认值。可以在-D 选项后跟上一个指定的值来修改系统默认的新用户设置。如下示例

$ sudo useradd -D -s /bin/tsch

现在,useradd 命令会将 tsch shell 作为所有新建用户的默认登录 shell。

删除用户

如果你想从系统中删除用户,userdel 可以满足这个需求。默认情况下,userdel 命令会只删除/etc/passwd 文件中的用户信息,而不会删除系统中属于该账户的任何文件。

如果加上-r 参数,userdel 会删除用户的 HOME 目录以及邮件目录。然而,系统上仍可能存有已删除用户的其他文件。这在有些环境中会造成问题。

下面是用 userdel 命令删除已有用户账户的一个例子。

$ sudo userdel -r test

在有大量用户的环境中使用-r 参数时要特别小心。你永远不知道用户是否在其 HOME 目录下存放了其他用户或其他程序要使用的重要文件。记住,在删除用户的 HOME 目录之前一定要检查清楚!

修改用户

Linux 提供了一些不同的工具来修改已有用户账户的信息。如下列出了这些工具。

  • usermod: 修改用户账户的字段,还可以指定主要组以及附加组的所属关系
  • passwd: 修改已有用户的密码
  • chpasswd: 从文件中读取登录名密码对,并更新密码
  • chage: 修改密码的过期日期
  • chfn: 修改用户账户的备注信息
  • chsh: 修改用户账户的默认登录 shell

每种工具都提供了特定的功能来修改用户账户信息。下面将具体介绍这些工具

usermod

usermod 命令是用户账户修改工具中最强大的一个。它能用来修改/etc/passwd 文件中的大部分字段,只需用与想修改的字段对应的命令行参数就可以了。参数大部分跟 useradd 命令的参数一样(比如,-c 修改备注字段,-e 修改过期日期,-g 修改默认的登录组)。除此之外,还有另外一些可能派上用场的选项。

  • -l 修改用户账户的登录名。
  • -L 锁定账户,使用户无法登录。
  • -p 修改账户的密码。
  • -U 解除锁定,使用户能够登录。

-L 选项尤其实用。它可以将账户锁定,使用户无法登录,同时无需删除账户和用户的数据。要让账户恢复正常,只要用-U 选项就行了。

passwd 和 chpasswd

改变用户密码的一个简便方法就是用 passwd 命令

$ sudo passwd test
新的密码:
重新输入新的密码:
passwd:已成功更新密码

如果只用 passwd 命令,它会改你自己的密码。系统上的任何用户都能改自己的密码,但只有 root 用户才有权限改别人的密码。

-e 选项能强制用户下次登录时修改密码。你可以先给用户设置一个简单的密码,之后再强制在下次登录时改成他们能记住的更复杂的密码。

如果需要为系统中的大量用户修改密码,chpasswd 命令可以事半功倍。chpasswd 命令能从标准输入自动读取登录名和密码对(由冒号分割)列表,给密码加密,然后为用户账户设置。你也可以用重定向命令来将含有 userid:passwd 对的文件重定向给该命令。

$ sudo chpasswd < users.txt
chsh、chfn 和 chage

chsh、chfn 和 chage 工具专门用来修改特定的账户信息。chsh 命令用来快速修改默认的用户登录 shell。使用时必须用 shell 的全路径名作为参数,不能只用 shell 名。

$ sudo chsh -s /bin/csh test
Changing shell for test.
Shell changed.

chfn 命令提供了在/etc/passwd 文件的备注字段中存储信息的标准方法。chfn 命令会将用于 Unix 的 finger 命令的信息存进备注字段,而不是简单地存入一些随机文本(比如名字或昵称之类的),或是将备注字段留空。finger 命令可以非常方便地查看 Linux 系统上的用户信息。

$ sudo finger testuser
Login: testuser                           Name: (null)
Directory: /home/testuser                 Shell: /bin/bash
On since Thu Nov 26 13:02 (CST) on tty1 from :0
    8 hours 54 minutes idle
On since Thu Nov 26 13:02 (CST) on pts/0 from :0
   8 hours 53 minutes idle
On since Thu Nov 26 21:17 (CST) on pts/1 from :0
   5 seconds idle
     (messages off)
No mail.
No Plan.

出于安全性考虑,很多 Linux 系统管理员会在系统上禁用 finger 命令,不少 Linux 发行版甚至都没有默认安装该命令。在 Arch Linux 上,需要通过 AUR 安装netkit-bsd-finger包来使用 finger 命令

如果在使用 chfn 命令时没有参数,它会向你询问要将哪些适合的内容加进备注字段。

$ sudo chfn test
Changing finger information for test.
Name []: Ima Test
Office []: Director of Technology Office
Phone []: (123)555-1234
Home Phone []: (123)555-9876
Finger information changed.

查看/etc/passwd 文件中的记录,你会看到下面这样的结果。

$ grep test /etc/passwd
test:x:504:504:Ima Test,Director of Technology,(123)555- 1234,(123)555-9876:/home/test:/bin/csh

所有的指纹信息现在都存在/etc/passwd 文件中了。

最后,chage 命令用来帮助管理用户账户的有效期。

  • -d: 设置上次修改密码到现在的天数
  • -E: 设置密码过期的日期
  • -I: 设置密码过期到锁定账户的天数
  • -m: 设置修改密码之间最少要多少天
  • -W: 设置密码过期前多久开始出现提醒信息

chage 命令的日期值可以用下面两种方式中的任意一种:

  • YYYY-MM-DD 格式的日期
  • 代表从 1970 年 1 月 1 日起到该日期天数的数值

chage 命令中有个好用的功能是设置账户的过期日期。有了它,你就能创建在特定日期自动过期的临时用户,再也不需要记住删除用户了!过期的账户跟锁定的账户很相似:账户仍然存在,但用户无法用它登录。

使用 Linux 组

用户账户在控制单个用户安全性方面很好用,但涉及在共享资源的一组用户时就捉襟见肘了。为了解决这个问题,Linux 系统采用了另外一个安全概念——(group)

组权限允许多个用户对系统中的对象(比如文件、目录或设备等)共享一组共用的权限。

Linux 发行版在处理默认组的成员关系时略有差异。有些 Linux 发行版会创建一个组,把所有用户都当作这个组的成员。遇到这种情况要特别小心,因为一个用户的文件很有可能对其他用户也是可读的。有些发行版会为每个用户创建单独的一个组,这样可以更安全一些。例如,在 KDE 中创建用户, 就会为每个用户创建一个单独的与用户账户同名的组。在添加用户前后可用 grep 命令或 tail 命令查看/etc/group 文件的内容进行比较。

每个组都有唯一的 GID——跟 UID 类似,在系统上这是个唯一的数值。除了 GID,每个组还有唯一的组名。Linux 系统上有一些组工具可以创建和管理你自己的组。本节将细述组信息是如何保存的,以及如何用组工具创建新组和修改已有的组。

/etc/group 文件

与用户账户类似,组信息也保存在系统的一个文件中。/etc/group 文件包含系统上用到的每个组的信息。下面是一些来自 Linux 系统上/etc/group 文件中的典型例子。

$ cat /etc/group
root:x:0:root
sys:x:3:bin
log:x:19:
proc:x:26:polkitd
wheel:x:998:testuser,test,test2
tty:x:5:
systemd-journal:x:984:
bin:x:1:daemon
daemon:x:2:bin
http:x:33:
nobody:x:65534:
dbus:x:81:
systemd-network:x:981:
systemd-timesync:x:979:
git:x:970:
cups:x:209:
dhcpcd:x:969:
sddm:x:968:
testuser:x:1000:

和 UID 一样,GID 在分配时也采用了特定的格式。系统账户用的组通常会分配低于 1000 的 GID 值,而用户组的 GID 则会从 1000 开始分配。/etc/group 文件有 4 个字段:

  • 组名
  • 组密码
  • GID
  • 属于该组的用户列表

组密码允许非组内成员通过它临时成为该组成员。这个功能并不很普遍,但确实存在。

千万不能通过直接修改/etc/group 文件来添加用户到一个组,要用 usermod 命令。在添加用户到不同的组之前,首先得创建组(创建用户时,默认生成的与用户名同名的组除外)。

用户账户列表某种意义上有些误导人。你会发现,在列表中,有些组并没有列出用户,如 testuser 组。这并不是说这些组没有成员。当一个用户在/etc/passwd 文件中指定某个组作为默认组时,用户账户不会作为该组成员再出现在/etc/group 文件中。多年以来,被这个问题难倒的系统管理员可不是一两个呢。如果想要严谨的查看一个组所有的全部用户,可以先取/etc/passwd 下以该组为默认组的用户,再加上/etc/group 下该组的用户,合并这两部分,就能得到该组全部的用户列表了。除此之外,以用户的维度,可以使用id命令查看一个用户所属的组的情况。

创建新组

groupadd 命令可在系统上创建新组。

$ sudo groupadd shared

在创建新组时,默认没有用户被分配到该组。groupadd 命令没有提供将用户添加到组中的选项,但可以用 usermod 命令来弥补这一点。

$ sudo usermod -G shared test

usermod 命令的-G 选项会用这个新组覆盖该用户的附加属组列表。

如果更改了已登录系统账户所属的用户组,该用户必须登出系统后再登录,组关系的更改才能生效。

为用户账户分配组时要格外小心。如果加了-g 选项,指定的组名会替换掉该账户的默认组。-G 选项的值将覆盖用户附加属组列表,不会影响默认组。如果想向附加属组列表中追加组,需要使用-aG 选项。

修改组

在/etc/group 文件中可以看到,需要修改的组信息并不多。groupmod 命令可以修改已有组的 GID(加-g 选项)或组名(加-n 选项)。

$ groupmod -n sharing shared

原 shared 组被更名为 sharing。

修改组名时,GID 和组成员不会变,只有组名改变。由于所有的安全权限都是基于 GID 的,你可以随意改变组名而不会影响文件的安全性。

理解文件权限

现在你已经了解了用户和组,是时候解读 ls 命令输出时所出现的谜一般的文件权限了。本节将会介绍如何对权限进行分析以及它们的来历。

使用文件权限符

使用 ls -l 可以查看到文件的权限情况

$ ls -l
总用量 600
-rw-r--r--  1 testuser users 540878 Nov 20 00:10 '2020-11-20 00-10-20.flv'
-rw-r--r--  1 testuser users      9 Oct 29 12:44  222
drwxr-xr-x  5 testuser users  12288 Nov 27 01:55  Desktop
drwxr-xr-x  7 testuser users   4096 Oct 30 02:03  Documents
drwxr-xr-x 21 testuser users  16384 Nov  7 17:25  Downloads
drwxr-xr-x  5 testuser users   4096 Nov 23 23:21  Games
drwxr-xr-x  3 testuser users   4096 Apr 10  2020  Music
drwxr-xr-x  4 testuser users   4096 Nov 18 17:32 'Nutstore Files'
drwxr-xr-x  3 testuser users   4096 Nov  2 14:11  Pictures
drwxr-xr-x  6 testuser users   4096 Sep  9 19:01  ThunderNetwork
drwxr-xr-x  3 testuser users   4096 Sep  5 22:33  Videos
drwxr-xr-x  7 testuser users   4096 Nov 18 18:18 'VirtualBox VMs'

输出结果的第一个字段就是描述文件和目录权限的编码。这个字段的第一个字符代表了对象的类型:

  • -代表文件
  • d 代表目录
  • l 代表链接
  • c 代表字符型设备
  • b 代表块设备
  • n 代表网络设备

之后有 3 组三字符的编码。每一组定义了 3 种访问权限

  • r 代表对象是可读的
  • w 代表对象是可写的
  • x 代表对象是可执行的

若没有某种权限,在该权限位会出现单破折线。这 3 组权限分别对应对象的 3 个安全级别。

  • 对象的属主
  • 对象的属组
  • 系统其他用户

讨论这个问题的最简单的办法就是找个例子,然后逐个分析文件权限。

-rwxrwxr-x 1 rich rich 4882 2010-09-18 13:58 myprog

文件 myprog 有下面 3 组权限。

  • rwx:文件的属主(设为登录名 rich)。
  • rwx:文件的属组(设为组名 rich)。
  • r-x:系统上其他人

这些权限说明登录名为 rich 的用户可以读取、写入以及执行这个文件(可以看作有全部权限)。类似地,rich 组的成员也可以读取、写入和执行这个文件。然而不属于 rich 组的其他用户只能读取和执行这个文件:w 被单破折线取代了,说明这个安全级别没有写入权限。

默认文件权限

你可能会问这些文件权限从何而来,答案是 umask。umask 命令用来设置所创建文件和目录的默认权限。

$ touch newfile
$ ls -al newfile
-rw-r--r--    1 rich     rich            0 Sep 20 19:16 newfile

touch 命令用分配给我的用户账户的默认权限创建了这个文件。umask 命令可以显示和设置这个默认权限。

$ umask
0022
$

遗憾的是,umask 命令设置没那么简单明了,想弄明白其工作原理就更混乱了。第一位仅代表 c 语言习惯的前导八进制 0,并不起实际作用。实际上,umask 会忽略任何一个前导 0(很多资料,如鸟哥私房菜、Linux bible 等,将第一位 0 解释为设置特殊权限位,即 SUID SGID 以及 SBIT,这是错误的。更多可参考 man 2 umask)。

后面的 3 位表示文件或目录对应的 umask 八进制值。要理解 umask 是怎么工作的,得先理解八进制模式的安全性设置。

八进制模式的安全性设置先分别获取文件的属主文件的属组系统上其他人 3 组 rwx 权限的二进制值,然后将其转换成 对于应的 3 个八进制值,连成一个长度为 3 的数值。举例来说,如果读权限是唯一置位的权限,权限值就是 r–,转换成二进制值就是 100,代表的八进制值是 4。如果拥有全部权限,权限值就是 rwx,转换成二进制值就是 111,代表的八进制值是 7。

八进制模式先取得权限的八进制值,然后再把这三组安全级别(属主、属组和其他用户)的八进制值顺序列出。因此,八进制模式的值 664 代表属主和属组成员都有读取和写入的权限,而其他用户都只有读取权限。

了解八进制模式权限是怎么工作的之后,umask 值反而更叫人困惑了。我的 Linux 系统上默认的八进制的 umask 值是 0022,而我所创建的文件的八进制权限却是 644,这是如何得来的呢?

umask 值只是个掩码。它会屏蔽掉不想授予该安全级别的权限。接下来我们还得再多进行一些八进制运算才能搞明白来龙去脉。

下一步要把 umask 值从对象的全权限值中减掉。对文件来说,全权限的值是 666(所有用户都有读和写的权限);而对目录来说,则是 777(所有用户都有读、写、执行权限)。

所以在上例中,文件一开始的权限是 666,减去 umask 值 022 之后,剩下的文件权限就成了 644。

在大多数 Linux 发行版中,umask 值通常会设置在/etc/profile 启动文件中,不过有一些是设置在/etc/login.defs 文件中的(如 Ubuntu)。可以用 umask 命令为默认 umask 设置指定一个新值。

$ umask 026
$ touch newfile2
$ ls -l newfile2
-rw-r-----    1 rich     rich            0 Sep 20 19:46 newfile2

在把 umask 值设成 026 后,默认的文件权限变成了 640,因此新文件现在对组成员来说是只读的,而系统里的其他成员则没有任何权限。

umask 值同样会作用在创建目录上。

$ mkdir newdir
$ ls -l
drwxr-x--x    2 rich     rich         4096 Sep 20 20:11 newdir/

由于目录的默认权限是 777,umask 作用后生成的目录权限不同于生成的文件权限。umask 值 026 会从 777 中减去,留下来 751 作为目录权限设置。

时刻记住,这里的减去描述的意思为按位去除 umask 的权限,并不是真正的减法。比如文件的全权限是 666,umask 为 003,如果直接进行真正的减法操作,结果为 663,也即为-rw-rw--wx,原本没有执行权限的文件反而有执行权限了,这明显是错误的。正确的结果为转换为对应的权限,进行去除权限的操作,正确的结果为 664。

隐藏文件权限属性

文件可以存在一些隐藏属性,对其权限进行额外的控制,较为常见的为 i 与 a 这两个属性。隐藏属性可通过如下方式查询和设置。

$ sudo chattr +i testfile
$ lsattr attrtest
----i----------- attrtest
$ chattr -i testfile

隐藏属性 i 设置后,可以让一个文件不能被删除、改名、设置链接,也无法写入或新增数据。对于系统安全性有相当大的助益。只有 root 能设置此属性。

当设置 a 之后,这个文件将只能增加数据,而不能删除也不能修改数据,只有 root 才能设置这属性。

在执行 lsattr 后,你可能还会看到一个 e 属性,查阅 man 手册可知,e 属性表示文件正在使用范围扩展数据块来映射磁盘上的块。不能使用 chattr 将其删除。范围扩展是文件系统中为文件保留的连续存储区域。当进程创建文件时,文件系统管理软件会分配整个范围。当再次写入文件时(可能在执行其他写入操作之后),数据将从上次写入停止的地方继续。这减少或消除了文件碎片以及可能的文件分散。

改变安全性设置

如果你已经创建了一个目录或文件,需要改变它的安全性设置,在 Linux 系统上有一些工具能够完成这项任务。本节将告诉你如何更改文件和目录的已有权限、默认文件属主以及默认属组。

改变权限

chmod 命令用来改变文件和目录的安全性设置。该命令的格式如下:

chmod options mode file

mode 参数可以使用八进制模式或符号模式进行安全性设置。八进制模式设置非常直观,直接用期望赋予文件的标准 3 位八进制权限码即可。

$ chmod 760 newfile
$ ls -l newfile
-rwxrw----    1 rich     rich            0 Sep 20 19:16 newfile

八进制文件权限会自动应用到指定的文件上。符号模式的权限就没这么简单了。 与通常用到的 3 组三字符权限字符不同,chmod 命令采用了另一种方法。下面是在符号模式下指定权限的格式。

[ugoa…][+-=][rwxxstugo…]

第一组字符定义了权限作用的对象:

  • u 代表用户
  • g 代表组
  • o 代表其他
  • a 代表上述所有

下一步,后面跟着的符号表示你是想在现有权限基础上增加权限(+),还是在现有权限基础上移除权限(-),或是将权限设置成后面的值(=)。

最后,第三组符号代表作用到设置上的权限。通常是 rwx 三种,你也会看到其余几种特殊的标志位,如 u,g,o。

  • u:将权限设置为跟属主一样。
  • g:将权限设置为跟属组一样。
  • o:将权限设置为跟其他用户一样。

像这样使用这些权限。

$ chmod o+r newfile
$ ls -lF newfile
-rwxrw-r--    1 rich     rich            0 Sep 20 19:16 newfile*

不管其他用户在这一安全级别之前都有什么权限,o+r 都给这一级别添加读取权限。

$ chmod u-x newfile
$ ls -lF newfile
-rw-rw-r--    1 rich     rich            0 Sep 20 19:16 newfile

u-x 移除了属主已有的执行权限。注意 ls 命令的-F 选项,它能够在具有执行权限的文件名后加一个星号。

options 为 chmod 命令提供了另外一些功能。-R 选项可以让权限的改变递归地作用到文件和子目录。你可以使用通配符指定多个文件,然后利用一条命令将权限更改应用到这些文件上。

特殊标志位 SUID SGID 以及 SBIT

除了常见的 rwx 权限,还有一些特殊的标志位,这让很多人迷惑,它们就是 SUID SGID 以及 SBIT。

Linux 为每个文件和目录存储了 3 个额外的信息位。

  • 设置用户 ID(SUID):当 s 这个标志出现在文件拥有者的 x 权限上时,此时就被称为 Set UID。当文件被用户使用时,程序会以文件属主的权限运行。举一个直观的例子,用户的密码存储在/etc/shadow 下,这个文件只有 root 用户才能进行写入,但是一个用户是可以通过 passwd 命令对自身的密码进行变更的,这里便用到了 SUID。通过 ls 查看,可以看到/usr/bin/passwd 这个文件被赋予了 SUID 标志位,其拥有者是 root,那么执行 passwd 的过程中,普通用户会“暂时”获得 root 的权限,以便对自身密码进行修改。注意,SUID 仅在 binary program 二进制文件上生效,SUID 对于目录也是不生效的。
  • 设置组 ID(SGID):当 s 标志在文件拥有者的 x 位置为 SUID,同理,s 在群组的 x 时则称为 Set GID。对文件来说,程序会以文件属组的权限运行,SGID 也仅在 binary program 二进制文件上生效;对目录来说,目录中创建的新文件会以目录的默认属组作为默认属组。
  • 粘着位(SBIT):受限删除位。作用于其他组权限的位置,标志为 t。当使用者在该目录下创建文件或目录时,仅有自己与 root 才有权力删除该文件,他人无法删除。SBIT 目前只针对目录有效,对于文件已经没有效果了。

SGID 位对文件共享非常重要。启用 SGID 位后,你可以强制在一个共享目录下创建的新文件都属于该目录的属组,这个组也就成为了每个用户的属组

特殊标志位们可通过 chmod 命令设置。它会加到标准 3 位八进制值之前(组成 4 位八进制值),或者在符号模式下用符号 s/t。

如果你用的是八进制模式,你需要知道这些位的位置,如下表所示。

二进制值 八进制值 描述
000 0 所有位都清零
001 1 粘着位置位
010 2 SGID 位置位
011 3 SGID 位和粘着位都置位
100 4 SUID 位置位
101 5 SUID 位和粘着位都置位
110 6 SUID 位和 SGID 位都置位
111 7 所有位都置位

举例来说,想要一个文件权限为-rwsr-xr-x,那么使用chmod 4755 filename进行设置。除了数字法之外,你也可以通过符号法来处理。其中 SUID 为 u+s ,而 SGID 为 g+s ,SBIT 则是 o+t。

此外,你有时还会看到大写的 X/S/T,它们的含义如下:

  • X:如果对象是目录或者文件已有执行权限,则赋予执行权限。在批量设置文件夹和文件的权限时,此项很有用,用于保证批量执行时可执行权限的正确赋予。它避免了必须区分文件和目录。比如需要清除全部可执行标志位时,可以进行a-x,a=rwX的权限操作,而不用刻意区分目录和文件。
  • S/T: 空的,没有执行权限的 UID/GID 和黏置位。小写的 s 与 t 都是取代 x 这个权限的,但是当下达 7666 权限时,也就是说,user,group 以及 others 都没有 x 这个可执行的标志时(因为是 666),特殊权限位也不可能有权限执行,7666 的结果为-rwSrwSrwT。所以,这个 S, T 代表的就是“空的”执行权限,不具有执行权限。换个说法, SUID +s 是表示“该文件在执行的时候,具有文件拥有者的权限”,但是文件拥有者都无法执行时,也就不存在权限给其他人使用了。

改变所属关系

有时你需要改变文件的属主,比如有人离职或开发人员创建了一个在产品环境中需要归属在系统账户下的应用。Linux 提供了两个命令来实现这个功能:chown 命令用来改变文件的属主,chgrp 命令用来改变文件的默认属组。

chown 命令的格式如下。

chown options owner[.group] file

可用登录名或 UID 来指定文件的新属主

$ sudo chown dan newfile
$ ls -l newfile
-rw-rw-r--    1 dan      rich            0 Sep 20 19:16 newfile

非常简单。chown 命令也支持同时改变文件的属主和属组。

$ sudo chown dan.shared newfile
$ ls -l newfile
-rw-rw-r--    1 dan      shared            0 Sep 20 19:16 newfile

如果你不嫌麻烦,可以只改变一个目录的默认属组。

$ sudo chown .rich newfile
$ ls -l newfile
-rw-rw-r--    1 dan      rich            0 Sep 20 19:16 newfile

最后,如果你的 Linux 系统采用和用户登录名匹配的组名,可以只用一个条目就改变二者。

$ sudo chown test. newfile
$ ls -l newfile
-rw-rw-r--    1 test      test            0 Sep 20 19:16 newfile

chown 命令采用一些不同的选项参数。-R 选项配合通配符可以递归地改变子目录和文件的所属关系。-h 选项可以改变该文件的所有符号链接文件的所属关系。

只有 root 用户能够改变文件的属主。任何属主都可以改变文件的属组,但前提是属主必须是原属组和目标属组的成员。

chgrp 命令可以更改文件或目录的默认属组。

$ chgrp shared newfile
$ ls -l newfile
-rw-rw-r--    1 rich     shared          0 Sep 20 19:16 newfile

用户账户必须是这个文件的属主,除了能够更换属组之外,还得是新组的成员。现在 shared 组的任意一个成员都可以写这个文件了。这是 Linux 系统共享文件的一个途径。然而,在系统中给一组用户共享文件也会变得很复杂。下一节会介绍如何实现。

共享文件

可能你已经猜到了,Linux 系统上共享文件的方法是创建组。但在一个完整的共享文件的环境中,事情会复杂得多。

你已经看到,创建新文件时,Linux 会用你默认的 UID 和 GID 给文件分配权限。想让其他人也能访问文件,要么改变其他用户所在安全组的访问权限,要么就给文件分配一个包含其他用户的新默认属组。

如果你想在大范围环境中创建文档并将文档与人共享,这会很烦琐。幸好有一种简单的方法可以解决这个问题。要创建一个共享目录,使目录里的新文件都能沿用目录的属组,只需将该目录的 SGID 位置位。

$ mkdir testdir
$ ls -l
drwxrwxr-x    2 rich     rich         4096 Sep 20 23:12 testdir/
$ chgrp shared testdir
$ chmod g+s testdir
$ ls -l
drwxrwsr-x    2 rich     shared       4096 Sep 20 23:12 testdir/
$ umask 002
$ cd testdir
$ touch testfile
$ ls -l
total 0
-rw-rw-r--    1 rich     shared          0 Sep 20 23:13 testfile

首先,用 mkdir 命令来创建希望共享的目录。然后通过 chgrp 命令将目录的默认属组改为包含所有需要共享文件的用户的组(你必须是该组的成员)。最后,将目录的 SGID 位置位,以保证目录中新建文件都用 shared 作为默认属组。

为了让这个环境能正常工作,所有组成员都需把他们的 umask 值设置成文件对属组成员可写。在前面的例子中,umask 改成了 002,所以文件对属组是可写的。

做完了这些,组成员就能到共享目录下创建新文件了。跟期望的一样,新文件会沿用目录的属组,而不是用户的默认属组。现在 shared 组的所有用户都能访问这个文件了。

管理文件系统

使用 Linux 系统时,需要作出的决策之一就是为存储设备选用什么文件系统。大多数 Linux 发行版在安装时会非常贴心地提供默认的文件系统,大多数入门级用户想都不想就用了默认的那个。

使用默认文件系统未必就不好,但了解一下可用的选择有时也会有所帮助。本章将探讨 Linux 世界里可选用的不同文件系统,并向你演示如何在命令行上进行创建和管理。

探索 Linux 文件系统

之前讨论了 Linux 如何通过文件系统来在存储设备上存储文件和目录。Linux 的文件系统为我们在硬盘中存储的 0 和 1 和应用中使用的文件与目录之间搭建起了一座桥梁。Linux 支持多种类型的文件系统管理文件和目录。每种文件系统都在存储设备上实现了虚拟目录结构,仅特性有所不同。本章将带你逐步了解 Linux 环境中较常用的文件系统的优点和缺陷。

基本的 Linux 文件系统

Linux 最初采用的是一种简单的文件系统,它模仿了 Unix 文件系统的功能。本节将讨论这种文件系统的演进过程。

  1. ext 文件系统

Linux 操作系统中引入的最早的文件系统叫作扩展文件系统(extended filesystem,简记为 ext)。它为 Linux 提供了一个基本的类 Unix 文件系统:使用虚拟目录来操作硬件设备,在物理设备上按定长的块来存储数据。

ext 文件系统采用名为索引节点的系统来存放虚拟目录中所存储文件的信息。索引节点系统在每个物理设备中创建一个单独的表(称为索引节点表)来存储这些文件的信息。存储在虚拟目录中的每一个文件在索引节点表中都有一个条目。ext 文件系统名称中的 extended 部分来自其跟踪的每个文件的额外数据,包括:

  • 文件名
  • 文件大小
  • 文件的属主
  • 文件的属组
  • 文件的访问权限

Linux 通过唯一的数值(称作索引节点号)来引用索引节点表中的每个索引节点,这个值是创建文件时由文件系统分配的。文件系统通过索引节点号而不是文件全名及路径来标识文件。

  1. ext2 文件系统

最早的 ext 文件系统有不少限制,比如文件大小不得超过 2 GB。在 Linux 出现后不久,ext 文件系统就升级到了第二代扩展文件系统,叫作 ext2。

如你所猜测的,ext2 文件系统是 ext 文件系统基本功能的一个扩展,但保持了同样的结构。ext2 文件系统扩展了索引节点表的格式来保存系统上每个文件的更多信息。

ext2 的索引节点表为文件添加了创建时间值、修改时间值和最后访问时间值来帮助系统管理员追踪文件的访问情况。ext2 文件系统还将允许的最大文件大小增加到了 2 TB(在 ext2 的后期版本中增加到了 32 TB),以容纳数据库服务器中常见的大文件。

除了扩展索引节点表外,ext2 文件系统还改变了文件在数据块中存储的方式。ext 文件系统常见的问题是在文件写入到物理设备时,存储数据用的块很容易分散在整个设备中(称作碎片化,fragmentation)。数据块的碎片化会降低文件系统的性能,因为需要更长的时间在存储设备中查找特定文件的所有块。

保存文件时,ext2 文件系统通过按组分配磁盘块来减轻碎片化。通过将数据块分组,文件系统在读取文件时不需要为了数据块查找整个物理设备。 多年来,ext 文件系统一直都是 Linux 发行版采用的默认文件系统。但它也有一些限制。索引节点表虽然支持文件系统保存有关文件的更多信息,但会对系统造成致命的问题。文件系统每次存储或更新文件,它都要用新信息来更新索引节点表。问题在于这种操作并非总是一气呵成的。

如果计算机系统在存储文件和更新索引节点表之间发生了什么,这二者的内容就不同步了。ext2 文件系统由于容易在系统崩溃或断电时损坏而臭名昭著。即使文件数据正常保存到了物理设备上,如果索引节点表记录没完成更新的话,ext2 文件系统甚至都不知道那个文件存在!很快开发人员就开始尝试开发不同的 Linux 文件系统了。

日志文件系统

日志文件系统为 Linux 系统增加了一层安全性。它不再使用之前先将数据直接写入存储设备再更新索引节点表的做法,而是先将文件的更改写入到临时文件(称作日志,journal)中。在数据成功写到存储设备和索引节点表之后,再删除对应的日志条目。

如果系统在数据被写入存储设备之前崩溃或断电了,日志文件系统下次会读取日志文件并处理上次留下的未写入的数据。

Linux 中有 3 种广泛使用的日志方法,每种的保护等级都不相同

  • 数据模式: 索引节点和文件都会被写入日志;丢失数据风险低,但性能差
  • 有序模式: 只有索引节点数据会被写入日志,在对应的元数据标记为提交前,强制写入文件内容。只有数据成功写入后才删除;在性能和安全性之间取得了良好的折中
  • 回写模式: 只有索引节点数据会被写入日志,但不控制文件数据何时写入;丢失数据风险高,但仍比不用日志好

数据模式日志方法是目前为止最安全的数据保护方法,但同时也是最慢的。所有写到存储设备上的数据都必须写两次:第一次写入日志,第二次写入真正的存储设备。这样会导致性能很差,尤其是对要做大量数据写入的系统而言。

这些年来,在 Linux 上还出现了一些其他日志文件系统。下面将会讲述常见的 Linux 日志文件系统。

  1. ext3 文件系统

2001 年,ext3 文件系统被引入 Linux 内核中。它采用和 ext2 文件系统相同的索引节点表结构,但给每个存储设备增加了一个日志文件,以将准备写入存储设备的数据先记入日志。

默认情况下,ext3 文件系统用有序模式的日志功能——只将索引节点信息写入日志文件,直到数据块都被成功写入存储设备才删除。你可以在创建文件系统时用简单的一个命令行选项将 ext3 文件系统的日志方法改成数据模式或回写模式。

虽然 ext3 文件系统为 Linux 文件系统添加了基本的日志功能,但它仍然缺少一些功能。例如 ext3 文件系统无法恢复误删的文件,它没有任何内建的数据压缩功能(虽然有个需单独安装的补丁支持这个功能),ext3 文件系统也不支持加密文件。鉴于这些原因,Linux 项目的开发人员选择再接再厉,继续改进 ext3 文件系统。

  1. ext4 文件系统

扩展 ext3 文件系统功能的结果是 ext4 文件系统(你可能也猜出来了)。ext4 文件系统在 2008 年受到 Linux 内核官方支持,现在已是大多数流行的 Linux 发行版采用的默认文件系统。

除了支持数据压缩和加密,ext4 文件系统还支持一个称作区段(extent)的特性。区段在存储设备上按块分配空间,但在索引节点表中只保存起始块的位置。由于无需列出所有用来存储文件中数据的数据块,它可以在索引节点表中节省一些空间。

ext4 还引入了块预分配技术(block preallocation)。如果你想在存储设备上给一个你知道要变大的文件预留空间,ext4 文件系统可以为文件分配所有需要用到的块,而不仅仅是那些现在已经用到的块。ext4 文件系统用 0 填满预留的数据块,不会将它们分配给其他文件。

  1. Reiser 文件系统

2001 年,Hans Reiser 为 Linux 创建了第一个称为 ReiserFS 的日志文件系统。ReiserFS 文件系统只支持回写日志模式——只把索引节点表数据写到日志文件。ReiserFS 文件系统也因此成为 Linux 上最快的日志文件系统之一。

有两个有意思的特性被引入了 ReiserFS 文件系统:一个是你可以在线调整已有文件系统的大小;另一个是被称作尾部压缩(tailpacking)的技术,该技术能将一个文件的数据填进另一个文件的数据块中的空白空间。如果你必须为已有文件系统扩容来容纳更多的数据,在线调整文件系统大小功能非常好用。

  1. JFS 文件系统

作为可能依然在用的最老的日志文件系统之一,JFS(Journaled File System)是 IBM 在 1990 年为其 Unix 衍生版 AIX 开发的。然而直到第 2 版,它才被移植到 Linux 环境中。

IBM 官方称 JFS 文件系统的第 2 版为 JFS2,但大多数 Linux 系统提到它时都只用 JFS。

JFS 文件系统采用的是有序日志方法,即只在日志中保存索引节点表数据,直到真正的文件数据被写进存储设备时才删除它。这个方法在 ReiserFS 的速度和数据模式日志方法的完整性之间的采取的一种折中。

JFS 文件系统采用基于区段的文件分配,即为每个写入存储设备的文件分配一组块。这样可以减少存储设备上的碎片。

除了用在 IBM Linux 上外,JFS 文件系统并没有流行起来,但你有可能在同 Linux 打交道的日子中碰到它。

  1. XFS 文件系统

XFS 日志文件系统是另一种最初用于商业 Unix 系统而如今走进 Linux 世界的文件系统。美国硅图公司(SGI)最初在 1994 年为其商业化的 IRIX Unix 系统开发了 XFS。2002 年,它被发布到了适用于 Linux 环境的版本。

XFS 文件系统采用回写模式的日志,在提供了高性能的同时也引入了一定的风险,因为实际数据并未存进日志文件。XFS 文件系统还允许在线调整文件系统的大小,这点类似于 ReiserFS 文件系统,除了 XFS 文件系统只能扩大不能缩小。

写时复制文件系统

采用了日志式技术,你就必须在安全性和性能之间做出选择。尽管数据模式日志提供了最高的安全性,但是会对性能带来影响,因为索引节点和数据都需要被日志化。如果是回写模式日志,性能倒是可以接受,但安全性就会受到损害。

就文件系统而言,日志式的另一种选择是一种叫作写时复制(copy-on-write,COW)的技术。COW 利用快照兼顾了安全性和性能。如果要修改数据,会使用克隆或可写快照。修改过的数据并不会直接覆盖当前数据,而是被放入文件系统中的另一个位置上。即便是数据修改已经完成,之前的旧数据也不会被重写。COW 文件系统已日渐流行,接下来会简要概览其中最流行的两种(Btrf 和 ZFS)。

  1. ZFS 文件系统

COW 文件系统 ZFS 是由 Sun 公司于 2005 年研发的,用于 OpenSolaris 操作系统,从 2008 年起开始向 Linux 移植,最终在 2012 年投入 Linux 产品的使用。

ZFS 是一个稳定的文件系统,与 Resier4、Btrfs 和 ext4 势均力敌。它最大的弱项就是没有使用 GPL 许可。自 2013 年发起的 OpenZFS 项目有可能改变这种局面。但是,在获得 GPL 许可之前,ZFS 有可能终无法成为 Linux 默认的文件系统。

  1. Btrfs 文件系统

Btrfs 文件系统是 COW 的新人,也被称为 B 树文件系统。它是由 Oracle 公司于 2007 年开始研发的。Btrfs 在 Reiser4 的诸多特性的基础上改进了可靠性。另一些开发人员最终也加入了开发过程,帮助 Btrfs 快速成为了最流行的文件系统。究其原因,则要归于它的稳定性、易用性以及能够动态调整已挂载文件系统的大小。OpenSUSE Linux 发行版将 Btrfs 作为其默认文件系统。除此之外,该文件系统也出现在了其他 Linux 发行版中(如 RHEL),Fedora 在 2020 年将 Btrfs 作为其默认文件系统。

操作文件系统

Linux 提供了一些不同的工具,我们可以利用它们轻松地在命令行中进行文件系统操作。可使用键盘随心所欲地创建新的文件系统或者修改已有的文件系统。本节将会带你逐步了解命令行下的文件系统交互的命令。

创建分区

一开始,你必须在存储设备上创建分区来容纳文件系统。分区可以是整个硬盘,也可以是部分硬盘,以容纳虚拟目录的一部分。此部分推荐使用的终端中好用的图形化 cfdisk 工具,除此之外,fdisk 也是一个使用广泛的传统工具,不过使用较为麻烦,不推荐使用。

要启动 cfdisk 命令,你必须指定要分区的存储设备的设备名,另外还得有超级用户权限。如果在没有对应权限的情况下使用该命令,你会得到类似于下面这种错误提示。

$ cfdisk /dev/sdb

Unable to open /dev/sdb

有时候,创建新磁盘分区最麻烦的事情就是找出安装在 Linux 系统中的物理磁盘。Linux 采用了一种标准格式来为硬盘分配设备名称,但是你得熟悉这种格式。对于老式的 IDE 驱动器,Linux 使用的是/dev/hdx。其中 x 表示一个字母,具体是什么要根据驱动器的检测顺序(第一个驱动器是 a,第二个驱动器是 b,以此类推)。对于较新的 SATA 驱动器和 SCSI 驱动器,Linux 使用/dev/sdx。其中的 x 具体是什么也要根据驱动器的检测顺序(和之前一样,第一个驱动器是 a,第二个驱动器是 b,以此类推)。最新的 SSD 固态硬盘一般以/dev/nvme0nx 来标识。在格式化分区之前,最好再检查一下是否正确指定了驱动器。

如果你拥有超级用户权限并指定了正确的驱动器,那就可以进入 cfdisk 工具的操作界面了。

cfdisk 可以看到目前磁盘的详情,在界面下方有各类操作,可用方向键和回车进行选择,非常方便。一般的步骤是新建分区,输入大小,选择类型,最后写入退出即可。

创建文件系统

在将数据存储到分区之前,你必须用某种文件系统对其进行格式化,这样 Linux 才能使用它。每种文件系统类型都用自己的命令行程序来格式化分区。如下列出了本章中讨论的不同文件系统所对应的工具。

  • mkefs 创建 ext 文件系统
  • mke2fs 创建 ext2 文件系统
  • mkfs.ext3 创建 ext3 文件系统
  • mkfs.ext4 创建 ext4 文件系统
  • mkreiserfs 创建 ReiserFS 文件系统
  • jfs_mkfs 创建 JFS 文件系统
  • mkfs.xfs 创建 XFS 文件系统
  • mkfs.zfs 创建 ZFS 文件系统
  • mkfs.btrfs 创建 Btrfs 文件系统

并非所有文件系统工具都已经默认安装了。要想知道某个文件系统工具是否可用,可以使用 type 命令。

$ type mkfs.ext4
mkfs.ext4 是 /usr/bin/mkfs.ext4
$ type mkfs.btrfs
-bash: type: mkfs.btrfs: not found

据上面这个取自 Ubuntu 系统的例子显示,mkfs.ext4 工具是可用的。而 Btrfs 工具则不可用。 不可用的工具请按发行版自行安装。

每个文件系统命令都有很多命令行选项,允许你定制如何在分区上创建文件系统。要查看所有可用的命令行选项,可用 man 命令来显示该文件系统命令的手册页面。所有的文件系统命令都允许通过不带选项的简单命令来创建一个默认的文件系统。

$ sudo mkfs.ext4 /dev/sdb1
mke2fs 1.45.6 (20-Mar-2020)
创建含有 3891192 个块(每块 4k)和 972944 个inode的文件系统
文件系统UUID:3f916991-2368-4d43-8b10-90b6a7c13445
超级块的备份存储于下列块:
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208

正在分配组表: 完成
正在写入inode表: 完成
*创建日志*(16384 个块)完成
写入超级块和文件系统账户统计信息: 已完成

这个新的文件系统采用 ext4 文件系统类型,这是 Linux 上的日志文件系统。注意,创建过程中有一步是创建新的日志。

为分区创建了文件系统之后,下一步是将它挂载到虚拟目录下的某个挂载点,这样就可以将数据存储在新文件系统中了。你可以将新文件系统挂载到虚拟目录中需要额外空间的任何位置。

$ ls /mnt
$ sudo mkdir /mnt/my_partition
$ ls -al /mnt/my_partition/
$ #啥也没有
$ sudo  mount -t ext4  /dev/sdb1  /mnt/my_partition
$ ls -al /mnt/my_partition/
total 24
drwxr-xr-x. 3 root root  4096 Jun 11 09:53 .
drwxr-xr-x. 3 root root  4096 Jun 11 09:58 ..
drwx------. 2 root root 16384 Jun 11 09:53 lost+found

mkdir 命令在虚拟目录中创建了挂载点,mount 命令将新的硬盘分区添加到挂载点。mount 命令的-t 选项指明了要挂载的文件系统类型(ext4)。现在你可以在新分区中保存新文件和目录了!

这种挂载文件系统的方法只能临时挂载文件系统。当重启 Linux 系统时,文件系统并不会自动挂载。要强制 Linux 在启动时自动挂载新的文件系统,可以将其添加到/etc/fstab 文件。

现在文件系统已经被挂载了到虚拟目录中,可以投入日常使用了。遗憾的是,在日常使用过程中有可能会出现一些严重的问题,例如文件系统损坏。下一节将演示如何应对这种问题。

文件系统的检查与修复

就算是现代文件系统,碰上突然断电或者某个不规矩的程序在访问文件时锁定了系统,也会出现错误。幸而有一些命令行工具可以帮你将文件系统恢复正常

每个文件系统都有各自可以和文件系统交互的恢复命令。这可能会让局面变得不太舒服,随着 Linux 环境中可用的文件系统变多,你也不得不去掌握大量对应的命令。好在有个通用的前端程序,可以决定存储设备上的文件系统并根据要恢复的文件系统调用适合的文件系统恢复命令。

fsck 命令能够检查和修复大部分类型的 Linux 文件系统,包括本章早些时候讨论过的 ext、ext2、ext3、ext4、ReiserFS、JFS、XFS、ZFS 以及 Btrfs。该命令的格式是:

fsck options /dev/sdX

你可以在命令行上列出多个要检查的文件系统。文件系统可以通过设备名、在虚拟目录中的挂载点以及分配给文件系统的唯一 UUID 值来引用。

fsck 命令使用/etc/fstab 文件来自动检查文件系统类型。如果存储设备从未被挂载(比如你刚刚在新的存储设备上创建了个文件系统,/etc/fstab 并无其信息),你需要用-t 命令行选项来指定文件系统类型。使用 man 或 tldr 查看其他可用的命令行选项。在使用 fsck 时,被检查的文件系统应该处在未挂载状态。

你可能注意到了,有些命令行选项是重复的。这是为多个命令实现通用的前端带来的部分问题。有些文件系统修复命令有一些额外的可用选项。如果要做更高级的错误检查,就需要查看这个文件系统修复工具的手册页面来确定是不是有该文件系统专用的扩展选项。

只能在未挂载的文件系统上运行 fsck 命令。对大多数文件系统来说,你只需卸载文件系统来进行检查,检查完成之后重新挂载就好了。但因为根文件系统含有所有核心的 Linux 命令和日志文件,所以你无法在处于运行状态的系统上卸载它。这正是亲手体验 Linux LiveCD 的好时机!只需用 LiveCD 启动系统即可,然后在根文件系统上运行 fsck 命令。

到目前为止,本章讲解了如何处理物理存储设备中的文件系统。Linux 还有另一些方法可以为文件系统创建逻辑存储设备。下一节将告诉你如何使用逻辑存储设备。

逻辑卷管理

如果用标准分区在硬盘上创建了文件系统,为已有文件系统添加额外的空间多少是一种痛苦的体验。你只能在同一个物理硬盘的可用空间范围内调整分区大小。如果硬盘上没有地方了,你就必须弄一个更大的硬盘,然后手动将已有的文件系统移动到新的硬盘上。

这时候可以通过将另外一个硬盘上的分区加入已有文件系统,动态地添加存储空间。Linux 逻辑卷管理器(logical volume manager,LVM)软件包正好可以用来做这个。它可以让你在无需重建整个文件系统的情况下,轻松地管理磁盘空间。

逻辑卷管理布局

逻辑卷管理的核心在于如何处理安装在系统上的硬盘分区。在逻辑卷管理的世界里,物理硬盘的各个分区称作物理卷(physical volume,PV)。每个物理卷都会映射到硬盘上特定的物理分区。

多个物理卷集中在一起可以形成一个卷组(volume group,VG)。逻辑卷管理系统将卷组视为一个物理硬盘,但事实上卷组可能是由分布在多个物理硬盘上的多个物理分区组成的。卷组提供了一个创建逻辑分区的平台,而这些逻辑分区则包含了文件系统。

整个结构中的最后一层是逻辑卷(logical volume,LV)。逻辑卷基于卷组之上,为 Linux 提供了创建文件系统的分区环境,作用类似于到目前为止我们一直在探讨的 Linux 中的物理硬盘分区。Linux 系统将逻辑卷视为物理分区。

可以使用任意一种标准 Linux 文件系统来格式化逻辑卷,然后再将它加入 Linux 虚拟目录中的某个挂载点

卷组横跨多个不同的物理硬盘,覆盖了多个独立的物理分区。在卷组上层可有多个独立的逻辑卷。Linux 系统将每个逻辑卷视为一个物理分区。每个逻辑卷可以被格式化成 ext4 文件系统,然后挂载到虚拟目录中某个特定位置。

某个物理硬盘也可以有一些未使用的分区。通过逻辑卷管理,你随后可以轻松地将这个未使用分区分配到已有卷组:要么用它创建一个新的逻辑卷,要么在需要更多空间时用它来扩展已有的逻辑卷。

类似地,如果你给系统添加了一块硬盘,逻辑卷管理系统允许你将它添加到已有卷组,为某个已有的卷组创建更多空间,或是创建一个可用来挂载的新逻辑卷。这种扩展文件系统的方法要好用得多!

Linux 中的 LVM

Linux LVM 是由 Heinz Mauelshagen 开发的,于 1998 年发布到了 Linux 社区。它允许你在 Linux 上用简单的命令行命令管理一个完整的逻辑卷管理环境。

Linux LVM 有两个可用的版本。

  • LVM1: 最初的 LVM 包于 1998 年发布,只能用于 Linux 内核 2.4 版本。它仅提供了基本的逻辑卷管理功能。
  • LVM2: LVM 的更新版本,可用于 Linux 内核 2.6 版本。它在标准的 LVM 1 功能外提供了额外的功能。

大部分采用 2.6 或更高内核版本的现代 Linux 发行版都提供对 LVM 2 的支持。除了标准的逻辑卷管理功能外,LVM 2 还提供了另外一些好用的功能。

  1. 快照

最初的 Linux LVM 允许你在逻辑卷在线的状态下将其复制到另一个设备。这个功能叫作快照。在备份由于高可靠性需求而无法锁定的重要数据时,快照功能非常给力。传统的备份方法在将文件复制到备份媒体上时通常要将文件锁定。快照允许你在复制的同时,保证运行关键任务的 Web 服务器或数据库服务器继续工作。遗憾的是,LVM 1 只允许你创建只读快照。一旦创建了快照,就不能再写入东西了。 LVM 2 允许你创建在线逻辑卷的可读写快照。有了可读写的快照,就可以删除原先的逻辑卷,然后将快照作为替代挂载上。这个功能对快速故障转移或涉及修改数据的程序试验(如果失败,需要恢复修改过的数据)非常有用。

  1. 条带化

LVM 2 提供的另一个引人注目的功能是条带化(striping)。有了条带化,可跨多个物理硬盘创建逻辑卷。当 Linux LVM 将文件写入逻辑卷时,文件中的数据块会被分散到多个硬盘上。每个后继数据块会被写到下一个硬盘上。

条带化有助于提高硬盘的性能,因为 Linux 可以将一个文件的多个数据块同时写入多个硬盘,而无需等待单个硬盘移动读写磁头到多个不同位置。这个改进同样适用于读取顺序访问的文件,因为 LVM 可同时从多个硬盘读取数据。

LVM 条带化不同于 RAID 条带化。LVM 条带化不提供用来创建容错环境的校验信息。事实上,LVM 条带化会增加文件因硬盘故障而丢失的概率。单个硬盘故障可能会造成多个逻辑卷无法访问。

  1. 镜像

通过 LVM 安装文件系统并不意味着文件系统就不会再出问题。和物理分区一样,LVM 逻辑卷也容易受到断电和磁盘故障的影响。一旦文件系统损坏,就有可能再也无法恢复。

LVM 快照功能提供了一些安慰,你可以随时创建逻辑卷的备份副本,但对有些环境来说可能还不够。对于涉及大量数据变动的系统,比如数据库服务器,自上次刚刚快照之后可能又要新增存储成百上千条记录。

这个问题的一个解决办法就是 LVM 镜像。镜像是一个实时更新的逻辑卷的完整副本。当你创建镜像逻辑卷时,LVM 会将原始逻辑卷同步到镜像副本中。根据原始逻辑卷的大小,这可能需要一些时间才能完成。

一旦原始同步完成,LVM 会为文件系统的每次写操作执行两次写入,一次写入到主逻辑卷,一次写入到镜像副本。可以想到,这个过程会降低系统的写入性能。就算原始逻辑卷因为某些原因损坏了,你手头也已经有了一个完整的最新副本

使用 Linux LVM

现在你已经知道 Linux LVM 可以做什么了,本节将讨论如何创建 LVM 来帮助组织系统上的硬盘空间。Linux LVM 包只提供了命令行程序来创建和管理逻辑卷管理系统中所有组件。有些 Linux 发行版则包含了命令行命令对应的图形化前端,但为了完全控制你的 LVM 环境,最好习惯直接使用这些命令。

定义物理卷

创建过程的第一步就是将硬盘上的物理分区转换成 Linux LVM 使用的物理卷区段。这里可以直接使用 cfdisk 进行操作。类型中选择 Linux LVM 即可。

下一步是用分区来创建实际的物理卷。这可以通过 pvcreate 命令来完成。pvcreate 定义了用于物理卷的物理分区。它只是简单地将分区标记成 Linux LVM 系统中的分区而已。

如果下一步中的 pvcreate 命令不能正常工作,很可能是因为 LVM 2 软件包没有默认安装。可以使用软件包名 lvm2 进行安装

$ sudo pvcreate /dev/sdb1
Physical volume "/dev/sdb1" successfully created

如果你想查看创建情况的话,可以使用 pvdisplay 命令来显示已创建的物理卷列表。

$ sudo pvdisplay /dev/sdb1
"/dev/sdb1" is a new physical volume of "2.01 GiB"
--- NEW Physical volume ---
PV Name               /dev/sdb1
VG Name
PV Size               2.01 GiB
Allocatable           NO
PE Size               0
Total PE              0
Free PE               0
Allocated PE          0
PV UUID               0FIuq2-LBod-IOWt-8VeN-tglm-Q2ik-rGU2w7

pvdisplay 命令显示出/dev/sdb1 现在已经被标记为物理卷。注意,输出中的 VG Name 内容为空,因为物理卷还不属于某个卷组。

PE Size(physical extent):物理区域是物理卷中可用于分配的最小存储单元,物理区域大小在建立卷组时指定,一旦确定不能更改,同一卷组所有物理卷的物理区域大小需一致,新的 pv 加入到 vg 后,pe 的大小自动更改为 vg 中定义的 pe 大小。

创建卷组

下一步是从物理卷中创建一个或多个卷组。究竟要为系统创建多少卷组并没有既定的规则,你可以将所有的可用物理卷加到一个卷组,也可以结合不同的物理卷创建多个卷组。

$ sudo vgcreate Vol1 /dev/sdb1
  Volume group "Vol1" successfully created
$

输出结果平淡无奇。如果你想看看新创建的卷组的细节,可用 vgdisplay 命令。

$ sudo vgdisplay Vol1
--- Volume group ---
VG Name               Vol1
System ID
Format                lvm2
Metadata Areas        1
Metadata Sequence No  1
VG Access             read/write
VG Status             resizable
MAX LV                0
Cur LV                0
Open LV               0
Max PV                0
Cur PV                1
Act PV                1
VG Size               2.00 GiB
PE Size               4.00 MiB
Total PE              513
Alloc PE / Size       0 / 0
Free  PE / Size       513 / 2.00 GiB
VG UUID               oe4I7e-5RA9-G9ti-ANoI-QKLz-qkX4-58Wj6e

这个例子使用/dev/sdb1 分区上创建的物理卷,创建了一个名为 Vol1 的卷组。创建一个或多个卷组后,就可以创建逻辑卷了。

创建逻辑卷

Linux 系统使用逻辑卷来模拟物理分区,并在其中保存文件系统。Linux 系统会像处理物理分区一样处理逻辑卷,允许你定义逻辑卷中的文件系统,然后将文件系统挂载到虚拟目录上。

要创建逻辑卷,使用 lvcreate 命令。虽然你通常不需要在其他 Linux LVM 命令中使用命令行选项,但 lvcreate 命令要求至少输入一些选项。具体选项详见 man,大多数情况下你用到的只是少数几个选项。

$ sudo lvcreate -l 100%FREE -n lvtest Vol1
Logical volume "lvtest" created

如果想查看你创建的逻辑卷的详细情况,可用 lvdisplay 命令

$ sudo lvdisplay Vol1
--- Logical volume ---
LV Path                /dev/Vol1/lvtest
LV Name                lvtest
VG Name                Vol1
LV UUID                4W2369-pLXy-jWmb-lIFN-SMNX-xZnN-3KN208
LV Write Access        read/write
LV Creation host, time archlinux, 2021-02-02 13:23:03 +0800
LV Status              available
## open                 0
LV Size                2.00 GiB
Current LE             513
Segments               1
Allocation             inherit
Read ahead sectors     auto
- currently set to     256
Block device           253:2

现在可以看到你刚刚创建的逻辑卷了!注意,卷组名(Vo l 1)用来标识创建新逻辑卷时要使用的卷组。 -l 选项定义了要为逻辑卷指定多少可用的卷组空间。注意,你可以按照卷组空闲空间的百分比来指定这个值。本例中为新逻辑卷使用了所有的空闲空间。 你可以用-l 选项来按可用空间的百分比来指定这个大小,或者用-L 选项以字节、千字节(KB)、兆字节(MB)或吉字节(GB)为单位来指定实际的大小。-n 选项允许你为逻辑卷指定一个名称(在本例中称作 lvtest)。

LE(logical extent):逻辑区域是逻辑卷中可用于分配的最小存储单元,逻辑区域的大小取决于逻辑卷所在卷组中的物理区域的大小。

创建文件系统

运行完 lvcreate 命令之后,逻辑卷就已经产生了,但它还没有文件系统。你必须使用相应的命令行程序来创建所需要的文件系统

$ sudo mkfs.ext4 /dev/Vol1/lvtest

在创建了新的文件系统之后,可以用标准 Linux mount 命令将这个卷挂载到虚拟目录中,就跟它是物理分区一样。唯一的不同是你需要用特殊的路径来标识逻辑卷。

$ sudo mount /dev/Vol1/lvtest /mnt/my_partition

注意,mkfs.ext4 和 mount 命令中用到的路径都有点奇怪。路径中使用了卷组名和逻辑卷名,而不是物理分区路径。文件系统被挂载之后,就可以访问虚拟目录中的这块新区域了。

修改 LVM

Linux LVM 的好处在于能够动态修改文件系统,在 Linux 有一些工具允许你修改现有的逻辑卷管理配置。

如果你无法通过一个很炫的图形化界面来管理你的 Linux LVM 环境,也不是什么都干不了。在本章中你已经看到了一些 Linux LVM 命令行程序的实际用法。还有一些其他的常见命令可以用来管理 LVM 的设置。

  • vgchange 激活和禁用卷组
  • vgremove 删除卷组
  • vgextend 将物理卷加到卷组中
  • vgreduce 从卷组中删除物理卷
  • lvextend 增加逻辑卷的大小
  • lvreduce 减小逻辑卷的大小

通过使用这些命令行程序,就能完全控制你的 Linux LVM 环境。更详细的用法可使用 man 或 tldr 查看。

在手动增加或减小逻辑卷的大小时,要特别小心。逻辑卷中的文件系统需要手动修整来处理大小上的改变。大多数文件系统都包含了能够重新格式化文件系统的命令行程序,比如用于 ext2、ext3 和 ext4 文件系统的 resize2fs 程序。