归档与备份
1、压缩文件
在计算机领域的发展历史中,人们一直努力将最多的数据塞入最小的空间,无论空间是指内存、存储设备,还是指网络带宽。许多如今已认为理所当然的数据服务,例如手机服务、高清电视或者宽带网络,其存在都要归功于高效的数据压缩技术。
数据压缩是指移除冗余数据的过程。考虑一个例子,假设有一张100像素×100像素的纯黑图像文件。就数据存储而言(假设每个像素点占用24bit,也就是3B),该图像则需要30 000B的存储空间:100×100×3=30 000。
只有一种颜色的图像包含的全都是冗余数据。要是聪明的话,在编码图像数据的时候,只简单地描述有10000个黑色像素点就够了。因此,用不着存储包含30000个0值字节(黑色在图像文件中通常用0表示1)的数据块,我们可以将这些数据用数字10000,其后再跟一个0来表示。这种数据压缩方案称为游程编码(run-lenghencoding),属于基本的压缩技术。虽然如今的压缩技术要先进、复杂得多,但基本目标仍是消除冗余数据。压缩算法(用于实现压缩的数学方法)一般分为两类。
- 无损压缩。无损压缩保留原文件中的所有数据。这意味着恢复后的文件和之前未压缩的文件一模一样。
- 有损压缩。在压缩时为了实现更高的压缩比例,会删除部分数据。更大程度的压缩删除了某些数据信息,有损压缩文件还原后,与原文件并不完全一致,但差别并不大。JPEG(图像压缩技术)和MP3(音频压缩技术)就是典型的有损压缩。
在下面的讨论中,我们只涉及无损压缩,因为计算机中的大部分数据无法容忍任何损失。
1.1、gzip
gzip程序可用于压缩单个或多个文件。该程序在执行时,会使用原文件的压缩版替换原文件。对应的gunzip程序用于解压缩。来看一个例子:
bash
[me@linuxbox ~]$ ls -l /etc > foo.txt
[me@linuxbox ~]$ ls -l foo.*
-rw-r--r-- 1 me me 15738 2018-10-14 07:15 foo.txt
[me@linuxbox ~]$ gzip foo.txt
[me@linuxbox ~]$ ls -l foo.*
-rw-r--r-- 1 me me 3230 2018-10-14 07:15 foo.txt.gz
[me@linuxbox ~]$ gunzip foo.txt
[me@linuxbox ~]$ ls -l foo.*
-rw-r--r-- 1 me me 15738 2018-10-14 07:15 foo.txt
在这个例子中,我们首先创建了一个名为foo.txt的文本文件,其内容为当前工作目录的文件列表。然后执行gzip,将原文件替换为压缩文件foo.txt.gz。在通配符foo.*产生的文件大小列表中,可以看到原文件已被其压缩版本取代,压缩后的文件大小差不多是原文件大小的1/5。此外,还可以看出,压缩文件的权限和时间戳与原文件相同。
接下来,执行gunzip解压缩文件。在这之后,我们可以看到压缩文件被原文件替代,文件权限和时间戳也一并保留了下来。
gzip选项众多,如下表所示。

回到先前的例子:
bash
[me@linuxbox ~]$ gzip foo.txt
[me@linuxbox ~]$ gzip -tv foo.txt.gz
foo.txt.gz: OK
[me@linuxbox ~]$ gzip -d foo.txt.gz
在这里,我们使用压缩文件foo.txt.gz替代了foo.txt。然后使用-t和-v选项测试该压缩文件的完整性。最后,解压缩文件。
借助标准输入和标准输出,gzip还有一些有趣的用法:
bash
[me@linuxbox ~]$ ls -l /etc | gzip > foo.txt.gz
该命令将目录列表制作成了压缩文件。
用于解压gzip文件的gunzip程序假定文件扩展名为.gz,所以没有必要明确写出扩展名,只要指定的文件名不会与现有的非压缩文件冲突即可:
bash
[me@linuxbox ~]$ gunzip foo.txt
如果只是想查看压缩的文本文件内容,可以这样做:
bash
[me@linuxbox ~]$ gunzip -c foo.txt | less
gzip附带了一个叫作zcat的程序,其功能等同于带有-c选项的gunzip。可以像cat命令那样查看经过gzip压缩过的文件内容:
bash
[me@linuxbox ~]$ zcat foo.txt.gz | less
1.2、bzip2
朱利安·苏厄德(Julian Seward)编写的bzip2程序类似于gzip,但使用了不同的压缩算法,在牺牲压缩速度的情况下实现了更高的压缩率。其用法和gzip基本上差不多。扩展名为.bz2代表经过bzip2压缩的文件:
bash
[me@linuxbox ~]$ ls -l /etc > foo.txt
[me@linuxbox ~]$ ls -l foo.txt
-rw-r--r-- 1 me me 15738 2018-10-17 13:51 foo.txt
[me@linuxbox ~]$ bzip2 foo.txt
[me@linuxbox ~]$ ls -l foo.txt.bz2
-rw-r--r-- 1 me me 2792 2018-10-17 13:51 foo.txt.bz2
[me@linuxbox ~]$ bunzip2 foo.txt.bz2
你应该已经看到了,gzip怎么用,bzip2就怎么用。先前讨论过的所有gzip选项(除了-r)也都适用于bzip2。但是要注意,压缩级别选项(-number)的含义对bzip2有些不一样。bzip2还带有用于解压缩的bunzip2和bzcat。
bzip2另配有用于恢复受损.bz2文件的bzip2recover程序。
2、文件归档
与压缩分不开的另一项常见的文件管理任务是归档。收集多个文件,将其组合成一个大文件,这个过程就是归档。归档通常作为系统备份的一部分,也用于将旧数据从系统转移到某些长期存储设备中。
2.1、tar
tar(磁带归档,tape archive)程序是类UNIX系统的软件世界里一款经典的文件归档工具,由此可见,它最初的用途就是备份磁带。虽然tar依然能够从事"老本行",但也同样可用于其他种类的存储设备。我们经常会看到扩展名为.tar和.tgz的文件,它们分别代表普通的tar归档文件和经过gzip压缩的归档(简称归档)。tar归档的组成可以是多个独立的文件、一个或多个目录层次或者两者兼而有之。tar用法如下:
bash
tar mode[options] pathname...
其中,mode是下表(只包含部分模式,完整模式参见tar的手册页)列出的tar模式之一。

tar选项的用法有点儿奇怪,我们通过几个例子来展示到底该怎么用。首先,重新创建之前文章中的playground目录:
bash
[me@linuxbox ~]$ mkdir -p playground/dir-{001..100}
[me@linuxbox ~]$ touch playground/dir-{001..100}/file-{A..Z}
然后,创建整个playground目录的tar归档:
bash
[me@linuxbox ~]$ tar cf playground.tar playground
该命令创建了一个名为playground.tar的归档,其中包含了整个playground目录的层次结构。从命令中可以看到,模式和用于指定tar归档名称的f选项可以合并书写,前面也不需要添加连字符。但是要注意,模式必须先于其他选项出现。
要想列出归档内容,可以这样:
bash
[me@linuxbox ~]$ tar tf playground.tar
要想获取更详细的清单,可以添加v(verbose)选项:
bash
[me@linuxbox ~]$ tar tvf playground.tar
现在,将归档内容提取到新的位置。先创建一个新目录foo,切换到该目录,提取tar归档:
bash
[me@linuxbox ~]$ mkdir foo
[me@linuxbox ~]$ cd foo
[me@linuxbox foo]$ tar xf ../playground.tar
[me@linuxbox foo]$ ls
playground
如果我们检查~/foo/playground的内容,可以看到已经成功提取了tar归档,生成了和原文件一模一样的副本。但是存在一个问题。除非是以超级用户身份操作,否则,从归档中提取出来的文件和目录的所有权属于执行提取操作的用户,而不再属于原先的属主。
tar另一个值得注意的行为就是其处理归档中路径名的方式,默认采用的是相对路径,而不是绝对路径。在创建归档时,tar会直接去掉路径名中最前面的正斜线2。作为演示,我们重新创建归档,这次指定绝对路径:
bash
[me@linuxbox foo]$ cd
[me@linuxbox ~]$ tar cf playground2.tar ~/playground
记住,执行命令时,~/playground会被扩展成绝对路径/home/me/playground。接下来,先提取归档,看一看会出现什么情况:
bash
[me@linuxbox ~]$ cd foo
[me@linuxbox foo]$ tar xf ../playground2.tar
[me@linuxbox foo]$ ls
home playground
[me@linuxbox foo]$ ls home
me
[me@linuxbox foo]$ ls home/me
playground
我们可以看到,在提取第二个归档的时候,tar重新创建了目录home/me/playground,该目录是相对于当前工作目录~/foo的,而不是相对于根目录的。这种方式看似奇怪,其实很实用,因为这允许用户将归档提取到任何位置,不用一定是原先的位置。再练习一遍,同时加入v选项(输出详细信息),这样能够清晰地看出整个操作的来龙去脉。
考虑一个虚构的,但却实用的tar应用示例。假设我们打算用一块大容量的USB硬盘把系统的主目录及其内容复制到另一个系统。在现代Linux系统中,该硬盘会自动挂载到/media目录下。再假设硬盘接入系统后的卷名为BigDisk。要想生成tar归档,可以执行下列操作:
bash
[me@linuxbox ~]$ sudo tar cf /media/BigDisk/home.tar /home
生成tar归档之后,卸载硬盘,将其接入另一台计算机。和先前一样,硬盘还是被挂载到/media/BigDisk。执行下列操作,提取归档:
bash
[me@linuxbox2 ~]$ cd /
[me@linuxbox2 /]$ sudo tar xf /media/BigDisk/home.tar
这里的重点是我们必须先切换到/目录,以便在提取归档时是相对于根目录的,因为归档中的所有路径全都是相对路径。
从归档中提取文件时,可以限制提取某些文件。例如,如果只想从归档中提取单个文件,可以这样:
bash
tar xf archive.tar pathname
在命令结尾加上pathname,tar就会只提取指定的文件,也可以指定多个路径名。注意,路径名必须和归档中保存的相对路径名一模一样。指定路径名时,一般不支持通配符;但是,GNU版本的tar(Linux发行版中大多是这个版本)通过--wildcards选项加入了对通配符的支持。来看一个例子:
bash
[me@linuxbox ~]$ cd foo
[me@linuxbox foo]$ tar xf ../playground2.tar --wildcards 'home/me/playground /
dir-*/file-A'
该命令只提取路径名中包含通配符dir-*的文件。
tar经常配合使用find来创建归档。在下面的例子中,我们使用find生成了一组要进行归档的文件:
bash
[me@linuxbox ~]$ find playground -name 'file-A' -exec tar rf playground.tar '{}' '+'
在这里,我们使用find命令查找playground目录中所有名为file-A的文件,然后使用-exec操作,以追加模式(r)调用tar,将查找到的文件添加到归档文件playground.tar之中。
tar结合find的方式很适合创建目录树或整个系统的增量备份。通过find查找比时间戳文件更新的(newer)文件,可以创建在上一次归档之后出现的那些文件的归档,前提是每次创建归档之后立刻更新时间戳文件。
tar也能利用标准输入和标准输出。下面是一个综合示例:
bash
[me@linuxbox foo]$ cd
[me@linuxbox ~]$ find playground -name 'file-A' | tar cf - --files-from=- | gzip
> playground.tgz
在本示例中,先用find得到匹配的文件列表,然后通过管道将其传给tar。如果将-指定为文件名,则根据需要将其表示为标准输入或标准输出。(顺便说一句,这种使用-代表标准输入或标准输出的惯例,也被其他许多程序采用)。--files-from选项(也可以写为-T)使tar从文件中而不是从命令行中读取文件列表。最后,tar生成的归档通过管道传给gzip进行压缩,由此得到压缩归档文件playground.tgz。作为惯例,经过gzip压缩的tar归档采用.tgz作为扩展名,不过有时候也会用.tar.gz。
虽然可以将gzip作为外部程序来生成压缩归档文件,但现代的GNU版本tar通过z选项和j选项,直接支持gzip和bzip2压缩。因此,之前的例子简化如下:
bash
[me@linuxbox ~]$ find playground -name 'file-A' | tar czf playground.tgz -T -
如果想创建bzip2的压缩归档文件,可以这样:
bash
[me@linuxbox ~]$ find playground -name 'file-A' | tar cjf playground.tbz -T -
只需简单地将压缩选项从z改成j(同时相应地将输出文件的扩展名改成.tbz,以作为bzip2压缩的文件),就可以改用bzip2压缩。
tar结合标准输入和标准输出的另一种值得注意的用法是在网络系统之间传输文件。假设有两台执行着类UNIX系统的计算机,各自安装了tar和ssh。在这种情况下,我们可以将远程主机(名为remote-sys)的目录传输至本地主机:
bash
[me@linuxbox ~]$ mkdir remote-stuff
[me@linuxbox ~]$ cd remote-stuff
[me@linuxbox remote-stuff]$ ssh remote-sys 'tar cf - Documents' | tar xf -
me@remote-sys's password:
[me@linuxbox remote-stuff]$ ls
Documents
这里,我们可以将远程主机remote-sys的Documents目录复制到本地主机的remote-stuff目录中。怎么做到的?首先,使用ssh在远程主机上执行tar。回想一下,ssh能够在联网的远程主机上执行程序并在本地主机上"看到"结果---远程主机产生的标准输出会被送至本地主机以供浏览。为了利用这一点,我们让tar生成归档(利用c模式),然后将其发送到标准输出,而不是生成文件(利用f选项加上-参数),这样就将归档通过ssh提供的加密隧道传输到了本地主机。在本地主机中,我们执行tar,提取(利用x模式)由标准输入提供的归档。
2.2、zip
zip既能压缩也能归档。该程序可以读写.zip文件,Windows用户应该很熟悉这种文件格式。但在Linux中,gzip才是主流压缩程序,bzip2紧随其后。
zip的基本用法如下:
bash
zip options zipfile file...
例如,要想生成playground目录的zip归档,可以这样:
bash
[me@linuxbox ~]$ zip -r playground.zip playground
除非加入用于递归操作的-r选项,否则只会保留playground目录(但不包含其内容)。尽管zip会自动添加扩展名.zip,但为了清晰起见,我们还是明确地写出扩展名。
在创建zip归档的过程中,zip程序会显示一系列消息:
bash
adding: playground/dir-020/file-Z (stored 0%)
adding: playground/dir-020/file-Y (stored 0%)
adding: playground/dir-020/file-X (stored 0%)
adding: playground/dir-087/ (stored 0%)
adding: playground/dir-087/file-S (stored 0%)
这些消息显示了加入归档的各个文件的状态。zip使用两种存储方式向归档中添加文件:一种是只存储,不压缩(就像上例中那样);另一种是压缩存储。每行结尾显示的数值表示压缩率。因为playground目录中包含的都是空文件,所以空文件均未进行压缩。
unzip程序可以方便地提取.zip文件内容:
bash
[me@linuxbox ~]$ cd foo
[me@linuxbox foo]$ unzip ../playground.zip
关于zip(与tar相反),有一件事要注意,如果指定的归档已存在,则将对其进行更新而不是替换。这意味着在保留现有归档的同时会向其中添加新文件,并替换同名文件。
在使用unzip时指定文件名,就可以有选择地列出和提取归档中的文件:
bash
[me@linuxbox ~]$ unzip -l playground.zip playground/dir-087/file-Z
Archive: ../playground.zip
Length Date Time Name
-------- ---- ---- ----
0 10-05-18 09:25 playground/dir-087/file-Z
-------- -------
0 1 file
[me@linuxbox ~]$ cd foo
[me@linuxbox foo]$ unzip ../playground.zip playground/dir-087/file-Z
Archive: ../playground.zip
replace playground/dir-087/file-Z? [y]es, [n]o, [A]ll, [N]one, [r]ename: y
extracting: playground/dir-087/file-Z
-l选项使unzip只列出归档内容,但不从中提取文件。如果没有指定任何文件,unzip会列出归档中的所有文件。可以加入-v选项,显示更详细的信息。注意,如果提取的文件与现有文件冲突,unzip会提示用户是否替换现有文件。
和tar一样,zip也能利用标准输入和标准输出,不过并不实用。可以使用-@选项,将文件名列表通过管道传给zip:
bash
[me@linuxbox foo]$ cd
[me@linuxbox ~]$ find playground -name "file-A" | zip -@ file-A.zip
这里,我们使用find生成符合测试条件-name "file-A"的文件列表,然后通过管道将其传给zip,由后者创建包含所选文件的归档file-A.zip。
zip也支持向标准输出写入输出结果,但因为很少有程序能够利用这种输出结果,所以该用法的作用有限。遗憾的是,unzip不接受标准输入。这就使zip和unzip不能像tar那样配合进行网络文件复制。
不过,zip可以接受标准输入,因此可用于压缩其他程序的输出结果:
bash
[me@linuxbox ~]$ ls -l /etc/ | zip ls-etc.zip -
adding: - (deflated 80%)
在这个例子中,我们将ls的输出结果通过管道传给zip。和tar一样,命令结尾的-被zip解释为"使用标准输入作为输入文件"。
如果指定了-p(pipe,管道)选项,unzip会将其输出结果发送到标准输出:
bash
[me@linuxbox ~]$ unzip -p ls-etc.zip | less
到目前为止,我们见识了zip/unzip的一些基本用法。两者都拥有大量选项,增强了其灵活性,不过部分选项只适用于特定的系统。zip和unzip的手册页编写得颇为不错,其中包含了一些有用的例子。zip和unzip的主要用途还是与Windows系统交换文件,而不是在Linux中进行压缩和归档,tar和gzip则是这些任务的首选。
3、同步文件与目录
维护系统备份的常用策略包括保持一个或多个目录与本地主机(通常是某种可移动存储设备)或远程主机上的其他目录同步。例如,有一个尚处于开发阶段的网站备份,我们会不时地将其与远程Web服务器上的实时副本进行同步。
在类UNIX系统中,执行该任务的首选工具是rsync。它利用rsync远程更新协议(rsync remote-updateprotocol)同步本地目录和远程目录,该协议允许rsync快速检测两处目录之间的差异,执行使其达成同步所需的最少复制操作。因此,相较于其他复制程序,rsync速度飞快、经济实用。
rsync的用法如下:
bash
rsync options source destination
其中,source和destination如下。
- 本地文件或目录。
- 采用user@host:path形式指定的远程文件或目录。
- 采用rsync://user@host:port/path形式的URI指定的远程rsync服务器。
注意,source或destination必须有一个是本地的,rsync不支持远程对远程(remote-to-remote)复制。
让我们对本地文件试一试rsync。先清空foo目录:
bash
[me@linuxbox ~]$ rm -rf foo /*
接下来,将playground目录与其在foo目录中的副本进行同步:
bash
[me@linuxbox ~]$ rsync -av playground foo
我们加入了-a选项(用于归档,执行递归操作并保留文件属性)和-v选项(详细输出),在foo目录中制作playground目录的镜像(mirror)文件。在命令执行时,我们会看到被复制的文件和目录列表。最后,显示类似于下面的汇总信息,说明复制了多少数据:
bash
sent 135759 bytes received 57870 bytes 387258.00 bytes/sec
total size is 3230 speedup is 0.02
如果再次执行命令,会看到不同的结果:
bash
[me@linuxbox ~]$ rsync -av playground foo
building file list ... done
sent 22635 bytes received 20 bytes 45310.00 bytes/sec
total size is 3230 speedup is 0.14
注意并没有出现文件列表,这是因为rsync检测到/playground和/foo/playground之间并没有什么差异,无须复制任何文件。如果我们修改playground中的某个文件,再次执行rsync:
bash
[me@linuxbox ~]$ touch playground/dir-099/file-Z
[me@linuxbox ~]$ rsync -av playground foo
building file list ... done
playground/dir-099/file-Z
sent 22685 bytes received 42 bytes 45454.00 bytes/sec
total size is 3230 speedup is 0.14
可以看到,rsync检测到了变化,只复制有更新的文件。
在指定rsync的同步源的时候,有一个细微但却实用的特性,我们可以善加利用。考虑下面两个目录:
bash
[me@linuxbox ~]$ ls
source destination
source目录中包含file1文件,destination目录为空。如果执行下列操作:
bash
[me@linuxbox ~]$ rsync source destination
rsync会将source复制到destination:
bash
[me@linuxbox ~]$ ls destination
source
但如果我们在作为同步源的目录名称后加上/,rsync只会复制目录内容,而非目录本身:
bash
[me@linuxbox ~]$ rsync source/ destination
[me@linuxbox ~]$ ls destination
file1
如果你只想复制目录内容,不想在同步目标处再创建另一个目录,这种用法就很方便了。从最终结果来看,我们可以将其看作source/*,但该方法只复制包括隐藏文件在内的源目录的内容。
回想一下之前讲解tar命令时的那个USB硬盘的例子。如果将硬盘接入系统,它会被挂载在/media/BigDisk目录下,我们先在硬盘上创建目录/backup,然后使用rsync命令将系统中最重要的内容复制到其中,以此完成系统备份:
bash
[me@linuxbox ~]$ mkdir /media/BigDisk/backup
[me@linuxbox ~]$ sudo rsync -av --delete /etc /home /usr/local /media/BigDisk/backup
在本例中,系统中的/etc、/home、/usr/local目录被复制到硬盘中。我们添加了--delete选项,用于删除存在于备份设备,但源设备中已经不存在的文件(这一步在首次备份时并不重要,但在后续的复制操作中就会发挥作用了)。每次接入硬盘,然后执行rsync命令。对于小型系统来说,这可以作为一个持续备份的实用方法(尽管并不完美)。当然,如果使用别名的话会更方便。我们定义下列别名并将其添加到.bashrc文件中,以提供备份功能:
bash
alias backup='sudo rsync -av --delete /etc /home /usr/local /media/BigDisk/backup'
现在要做的就是接入硬盘,执行backup命令进行备份。
在网络上使用rsync
rsync的真正美妙之处在于能够在网络上复制文件。rsync中的r就代表"remote"(远程)。远程复制有两种方法。第一种针对安装了rsync和远程Shell程序(例如ssh)的系统。假设在局域网中还有另一个系统,其中尚有大量可用的硬盘空间,我们想使用该远程系统代替外置存储设备来执行备份操作。如果用来存放备份的/backup目录已经建好,我们可以这么做:
bash
[me@linuxbox ~]$ sudo rsync -av --delete --rsh=ssh /etc /home /usr/local
remote-sys:/backup
我们对命令做了两处改动,以便于网络复制。首先,加入了--rsh=ssh选项,用于指示rsync使用ssh程序作为其远程Shell。这样就能使用加密隧道将本地系统数据安全地传输到远程系统。其次,在目标路径名之前加上了远程主机名(在本例中,远程主机名为remote-sys)。
第二种方法是使用rsync服务器。可以配置rsync,使其作为守护进程执行,监听传入的同步请求。这种做法常用于实现远程系统监控。例如,Red Hat Software为其Fedora发行版的软件开发维护着一个大型仓库。对于软件测试人员来说,在系统发行版发布周期的测试阶段创建仓库的镜像文件非常有用。由于该仓库中的文件改动频繁(经常一天好几次),因此通过定期同步来维持本地镜像文件要比大批量复制仓库更可取。其中一个仓库位于杜克大学,我们可以像下面这样使用本地系统的rsync和远程的rsync服务器来创建镜像文件:
bash
[me@linuxbox ~]$ mkdir fedora-devel
[me@linuxbox ~]$ rsync -av --delete rsync://archive.linux.duke.edu/fedora /
linux/development/rawhide/Everything/x86_64/os/ fedora-devel
本例中用到了远程rsync服务器的URI,其组成包含协议(rsync://)、远程主机名(archive.linux.duke.edu)以及仓库的路径名。