0%

一篇讲透 Linux 归档与压缩的设计哲学

tar 不是压缩工具:一篇讲透 Linux 归档与压缩的设计哲学

你敲了十年 tar -czf,可能从没意识到一个事实:tar 本身一个字节都不压缩

tar 不是压缩工具——它只负责把一堆文件”归档”成一条字节流,压缩只是顺手套上的外衣。本文以”一条流从生成到还原”为主线,从查看、打包、解压三件日常事,讲到流式传输、增量备份、可复现归档等工程用法,带你重新理解这个最熟悉却最被误解的 Linux 命令。

一、先看懂 tar 在干什么

在敲任何命令之前,先建立一个能贯穿全文的认知。

1. tar 是”归档器”,压缩只是外挂

tar 的全称是 tape archive(磁带归档)。它只干一件事——把一堆文件和目录”拼接”成一条连续的字节流(一个 .tar 文件)。真正的压缩,是 -z(gzip)、-J(xz)、--zstd(zstd)这些参数顺手调用的外部压缩程序干的。

把这句话刻进脑子,后面一切都顺了:

tar 干的事,就是把一组文件变成”一条流”,再把这条流安全地还原回去。压缩,只是给这条流套的一件”外衣”——可有可无,可以随时换。

2. 一张地图:用 tar 无非在回答五个问题

既然 tar 的全部工作都围绕”一条流”,那么你这辈子敲的每一条 tar 命令,本质都在回答下面五个问题之一。这五个问题,就是本文的主线:

  1. 往流里装什么?(选哪些文件、要不要权限属性、排除什么)
  2. 怎么把流造出来?(打包)
  3. 怎么不拆开就读这条流?(查看)
  4. 怎么让流动起来、长期存活?(传输、备份)
  5. 怎么把流拆开、并且信得过它?(解压、校验、安全)

而”压缩“这件外衣,横跨上面每一站。下面我们就从最日常的操作,一路走到工程级用法。

3. 先拿到钥匙:读懂所有 tar 命令的骨架

动手之前先记住这个骨架,之后任何 tar 命令你都能一眼拆解、也不会写错:

动作 + 参数 + 包名 + 源/目的地路径

  • 动作-c(Create 造包)/-x(eXtract 拆包)/-t(lisT 查看),三选一,互斥。
  • 参数:要不要压缩(-z/-J/--zstd)、详细输出(-v)等。
  • 包名:永远跟在 -f 后面。
  • 源/目的地:打包时是源目录,解包时是目标目录-C 指定)。

这里有一个最容易踩、却最实用的顺序规则:包名永远紧跟 -f,并且排在源/目标路径之前——不能调换。 因为 -f 的”参数”就是包名,所以无论造包还是拆包,都是先写包名、再写源目录或目标目录:

  • 造包:tar -czf 包名 源目录 ✅ / tar -czf 源目录 包名 ❌(tar 会把源目录名错当成包名)
  • 拆包:tar -xzf 包名 -C 目标目录

还有个配套的经典翻车点:组合参数里 f 必须放在最后tar -czf a.tar.gz dir 正确;写成 tar -cfz a.tar.gz dir 就错了——f 后面紧跟的是 z,tar 会把 z 当成包名。

如果把参数分开写成 -c -z -f a.tar.gz,则不强求 f 在末尾,但 -f 后面仍必须紧跟包名。说到底,规则只有一条:**-f 和包名形影不离。**

二、日常三件事:读、造、拆

这三件事覆盖了 90% 的场景,先把它们做对、做安全。

1. 读包:先查后解,别拆”盲盒”

拿到一个陌生的包,第一件事永远是看一眼里面装了什么,而不是直接解压。用 -t(list):

1
2
# 显示详细信息(推荐)
tar -tvf archive.tar | head

文件成百上千时终端会刷屏,所以配合 head判断依据:看所有条目是否都有相同的前缀路径(如 myproject/)。

1
2
3
4
5
6
7
tar -tvf project.tar | head
# 返回信息
myproject/
myproject/README.md
myproject/src/
myproject/src/main.c
myproject/docs/

如果第一行不是一个统一的顶层目录,而是直接一堆散文件——小心,这是一颗”tar 炸弹”,解压会炸得满地都是(第 3 节细说)。

小提示:归档是 .tar.gz 时,严格写法应带 -ztar -tzf project.tar.gz)。但现代 GNU tar 能自动识别压缩格式,所以 tar -tvf project.tar.gz 同样能用——这就是为什么很多人查看 gz 包时省略了 -z

2. 造包:把一个目录干净地装进流里

打包只有一个”正确姿势”:-C 切到父目录,再打包,让包里始终带一个干净的顶层目录。

1
2
# 最佳实践:用 -C
tar -czf app-v1.tar.gz -C /path/to/parentdir appdir

逐个参数拆解(用上面那把”骨架钥匙”):

  • -cCreate,造包的核心动作
  • -z:套上 gzip 这件外衣。
  • -f app-v1.tar.gz:包名。
  • -C /path/to/parentdir:**关键!大写 C 是 “change directory”**——“开始打包前,先切到这个目录”。
  • appdir:要打包的目录。因为已经 -C 切到了它的父目录,这里直接写名字。

这样解压时,当前目录下只会干净地出现一个 appdir 文件夹,没有冗长的绝对路径前缀

为什么不先 cd 再打包? 在脚本和 CI/CD 里,先 cd 进去打包会带来两个隐患:

  • 污染源目录:打包产物被扔进了源代码 / 原始数据目录。
  • 递归套娃:下次再跑、又没排除 *.tar.gz,tar 会把上次的包再打进去,越滚越大甚至死循环。

-C无副作用、不污染源目录、还支持后面要讲的多源打包。所以记住:生产环境一律 -C,绝不用绝对路径。

那如果硬用绝对路径,会发生什么?——tar 会”剥掉”你的路径开头

很多人不信邪,直接拿绝对路径打包:

1
tar -czf backup.tar.gz /home/ubuntu/project

命令能跑,但 tar 会甩给你一条告警:

1
tar: Removing leading '/' from member names

这不是报错,而是 tar 在自我保护。 它把每个成员名开头的 / 剥掉了——包里存的不再是绝对路径 /home/ubuntu/project/...,而是相对路径 home/ubuntu/project/...

tar 为什么非这么做不可?因为如果归档里存的是绝对路径,那别人解压时,这些文件就会被强行写回 /home/ubuntu/... 这种写死的系统位置,甚至可能盖掉 /etc/bin 里的关键文件。剥掉开头的 /、强制相对化,解压才会乖乖落在”当前目录之下”,而不是满世界乱写。(这层保护的安全意义,后面”坑三”还会细说。)

代价是:你的包里现在套着一长串又深又没用的目录。tar -tzf 看一眼就知道:

1
2
3
4
tar -tzf backup.tar.gz
# home/ubuntu/project/
# home/ubuntu/project/src/
# home/ubuntu/project/src/main.c

于是解压时,当前目录下会凭空长出 home/ubuntu/project/ 这一整串嵌套空壳,而不是你想要的、干净的一个 project/。这正是前面那句”没有冗长的绝对路径前缀“想避开的麻烦。

结论很简单:别和这条告警较劲,用 -C 从根上绕开它。

1
2
3
4
5
# 不要这样(触发告警 + 解压套娃)
tar -czf backup.tar.gz /home/ubuntu/project

# 要这样(-C 切到父目录,包里干净地只有 project/)
tar -czf backup.tar.gz -C /home/ubuntu project

-C /home/ubuntu project 等于告诉 tar:”先进到 /home/ubuntu,再打包 project“——成员名天然就是相对的 project/...,既不报告警,解压也直接还原成一个清清爽爽的 project/

3. 拆包:三个能毁掉现场的坑

解压看似最简单,却最容易出事。永远不要在当前目录裸解压,养成”先建目录、再 -C“的习惯:

1
2
mkdir -p /target/dir
tar -xzf app-v1.tar.gz -C /target/dir

-C 指定目标目录,但目录必须已存在,否则报错(tar 只 cd 进去,不替你 mkdir)。除此之外,还有三个坑:

坑一:外面套了一层带版本号的目录 → --strip-components

从 GitHub 下的源码包常常套着一层 project-v1.2.3/,这层目录名里带着版本号、每次发版都在变:

1
2
# 剥掉路径开头的 1 层,内容直接进目标目录
tar -xzf project-v1.2.3.tar.gz --strip-components=1 -C /opt/myapp

它真正解决的不是”嫌目录深”,而是把带版本号的那一层抹掉,让目标路径永远固定在 /opt/myapp——和版本号解耦。手动解压时,多一层目录无所谓,cd 进去就行;但在部署脚本里你没法写死 cd project-v1.2.3(下个版本一升级脚本就崩),这个参数因此成了部署/打包脚本里的常客,却很少在入门教程里被点出来。

坑二:往非空目录解包,会默默覆盖同名文件 → 覆盖控制

覆盖只在目标目录里已经存在同名文件时才发生。解到一个空目录,无论解多少次都不会有这个问题。所以这不是”解压”的通病,而是”往已有内容的目录里解压“才会咬人的坑。

真正危险的地方在于它完全无声——没有提示、没有确认,磁盘上的同名文件被包里的版本直接替换。最容易踩到的几种情况:

  • 把工具包、配置解到一个本来就有东西的公共目录,比如 /usr/local/etc,结果撞掉了无关的已有文件;
  • 多个 tar 包解到同一目录,后解的悄悄盖掉先解的同名文件;
  • 大包解压中途中断,重来一遍时又把已解出的部分整个覆写一轮;
  • 解一个来源不完全可信的包,不希望它动到目录里已有的任何文件。

这几种的共同点是:目标目录里已经有你想护住的内容,而这次解压并不是在一块干净空地上铺开。这时就该显式控制覆盖行为,而不是放任默认:

参数 行为
-k / --keep-old-files 遇到同名文件报错Cannot open: File exists)并以退出码 2 退出,旧文件原样保留
--skip-old-files 同名文件静默跳过,不报错,退出码 0,继续解其余文件
--overwrite 显式声明覆盖意图(注意它是”原地覆写”,与默认的”先删旧文件、再写新文件”并不完全等价,处理符号链接时行为不同)
-w / --interactive 每解一个动作都交互确认一次

这里最容易选错的是 -k--skip-old-files——名字像,行为却相反:

1
tar -xzkf app.tar.gz -C /target/dir   # 撞到同名文件就报错中断、绝不覆盖

-k 是”撞到同名就报错退出(码 2)”。如果部署脚本带了 set -e,它会在第一个冲突文件处直接中断——这有时正是你要的”宁可停下也别乱动已有文件”;但如果你的意图是”已存在的跳过、剩下的照常解、脚本别失败”,就必须换成 --skip-old-files,否则脚本会莫名其妙地挂在这一步。

坑三:不可信的包想搞你 → “tar 炸弹”与”路径穿越”

  • tar 炸弹(散弹包):没有统一顶层目录,几十上百个文件解压时直接吐进当前目录。防御:先 tar -tvf 看结构,再统一 -C 到一个全新空目录
  • 路径穿越(tar slip):恶意包藏着 ../../etc/cron.d/x 或绝对路径,试图覆盖系统文件。

好消息是 GNU tar 默认帮你挡住了大半:创建时剥离开头的 /,解压时拒绝带 .. 的成员名——这正是”为什么 tar 要剥离绝对路径”的安全意义。但务必注意:

  • **对不可信包绝不要加 -P / --absolute-names**,那会关掉这层保护;
  • 老版本或非 GNU 的 tar 不一定有这层防护;
  • 软链接攻击仍可能绕过。所以:不可信的包一律先 -tvf 审查,再解到隔离的空目录。

三、决定”往流里装什么”

前面都是”整个目录原样打包”。但真实场景里,”装什么进去”本身就是一门学问:从哪儿抓、按什么挑、排除什么、带不带元数据。

1. 多源打包:把散落各处的东西收进一个包

先说清楚”多源”是什么意思。 这里的”源”指的是文件的来源目录。日常打包通常只有一个源——就打包 /srv/app 这一个目录。但发布一个应用时,你要的东西常常分散在好几个互不相关的目录:代码在 /project/src,配置在 /etc/myapp,启动脚本在 /opt/scripts。所谓”多源打包”,就是在一条命令里,从这些毫不相关的目录各抓一部分,合并进同一个包

这件事 cd 根本做不到——你不可能同时 cd 到三个目录。而 -C 可以,因为 -C 在 tar 里是”位置相关”的:它可以在一条命令里出现多次,每次切到一个新目录,再打包紧跟其后的东西。

1
2
3
4
tar -czf release.tar.gz \
-C /project/src code_folder \
-C /etc/myapp config_file \
-C /opt/scripts start.sh

执行时 tar 会:先切到 /project/src 打包 code_folder,再切到 /etc/myapp 打包 config_file,最后切到 /opt/scripts 打包 start.sh,三者合成一个 release.tar.gz。这就是 -Ccd 强大的地方。

注意:多个 -C接力式 chdir——每个 -C 都在上一个的落点基础上再跳,而非都从初始目录起跳。上例三个路径都是绝对路径,才表现为互不相干的独立跳转;若用相对路径,第二个 -C 会相对第一个的结果来解析,需要自己用 ../ 抵消。实践中多源打包一律用绝对路径,就能彻底回避这个累加陷阱。

2. 按清单打包:让 find 决定装什么

当”要打包哪些文件”本身是一次查询的结果时,让 find 负责筛选、tar 负责打包——这是 Unix”组合小工具”哲学的经典体现。

1
2
# 把所有 .log 文件打包,文件名含特殊字符也安全
find . -name '*.log' -print0 | tar --null -czf logs.tar.gz -T -
  • -T -:从标准输入读取文件清单-T file 则从文件读)。
  • --null:清单用 \0 分隔,与 find -print0 配对。
  • 为什么必须 -print0 + --null 文件名里可能有空格甚至换行,用普通换行分隔会被拆错。这对组合是处理任意文件名的安全写法,和 find 强调的 -print0 | xargs -0 是同一个道理。

3. 排除:让包只装该装的

打包源码或日志时,排除 .gitnode_modules、过期日志等。为与”相对路径”原则一致,仍用 -C 切到父目录:

1
tar -czf backup.tar.gz --exclude='.git' --exclude='node_modules' -C /path/to project

--exclude 要写在被打包路径之前,否则可能不生效。

4. 元数据:为什么你的备份”权限丢了”

标准 tar 默认就记录权限位、owner/group、mtime。

但 ACL、SELinux 上下文、扩展属性这三类默认不存,系统备份必须显式加 --acls --selinux --xattrs(且打包和解压两端都要加)才能完整保留。

这里有一个初学者必栽的坑——必须分清”创建”和”恢复”两个阶段

-p--preserve-permissions)本质是”解压时”的参数,不是”打包时”的。

  • tar 创建归档时默认就会记录权限和 owner,所以创建命令里单独加 -p 几乎没用。
  • 真正决定”权限能否原样恢复”的是解压那一步:普通用户解压时,tar 会用当前 umask 过滤权限(实际权限 = 存储权限 & ~umask)。只有解压时加 -p,才严格按归档记录恢复。

所以备份要分两段记:

① 创建时--acls --selinux --xattrs 捕获扩展属性,这几个在创建阶段是真有用的):

1
tar -czf backup.tar.gz --acls --selinux --xattrs -C /path/to systemdir

真正能把权限和 owner 都原样恢复的前提,是以 root 解压——此时 -p(严格权限)和 --same-owner(保留属主)都是默认行为。普通用户即便加了 -p,也只能恢复权限位,恢复不了属主。

② 解压 / 恢复时(这里的 -p 才是关键,通常还要 root 来恢复 owner):

1
sudo tar -xzpf backup.tar.gz --acls --selinux --xattrs -C /restore/target

恢复时漏了 -p,权限会被 umask 悄悄改掉——备份看着”完整”,实则权限已失真。

四、让流动起来:tar 的流式杀手锏

还记得开头那句话吗——tar 的产物本质是”一条流”。既然是流,它就能用 -(代表标准输入/输出)直接接进管道,和 ssh、split 组合,全程不落地。这是 tar 区别于 zip 最强大的能力。

跨机器迁移整个目录(不产生任何中间文件):

1
2
# 本地打包成流 → 通过 ssh → 远端直接解开
tar -cf - -C /src appdir | ssh user@host 'tar -xf - -C /dest'

带宽紧张时本地压缩、远端解压:

1
tar -czf - -C /src appdir | ssh user@host 'tar -xzf - -C /dest'

超大包分卷(便于传输或受单文件大小限制):

1
2
tar -czf - -C /src bigdir | split -b 1G - backup.tar.gz.part-   # 切成每块 1G
cat backup.tar.gz.part-* | tar -xzf - -C /dest # 还原:先拼后解

-cf - = 把归档写到 stdout;-xf - = 从 stdin 读归档。- 就是”流”的占位符。

五、压缩这件”外衣”

回到开头那句话——压缩是横跨始终的外挂。前面所有命令里的 -z,都只是顺手套了 gzip。现在单独聊聊:这件外衣怎么选、怎么穿得更快。

1. 选哪件外衣:gzip、zstd 还是 xz

默认选 tar.gz(gzip)——速度、兼容性、成熟度都稳。需要”又快又小”时,现代首选 zstd(.tar.zst;只在”体积极度敏感 + 可控环境”才上 xzbz2 基本不推荐。

格式 压缩程序 tar 参数 典型命令示例
.tar.gz gzip -z tar -czf app.tar.gz -C /parent dir
.tar.zst zstd --zstd tar --zstd -cf app.tar.zst -C /parent dir
.tar.xz xz (LZMA) -J tar -cJf app.tar.xz -C /parent dir
.tar.bz2 bzip2 -j tar -cjf app.tar.bz2 -C /parent dir

**zstd 需要 GNU tar 1.31+**,速度和压缩比几乎全面优于 gzip,恰好填补了 gzip 与 xz 之间”又快又小”的空档。

2. 并行压缩:别让单核拖垮大包

默认的 gzip 是单线程的,大包压缩能卡很久。换成多线程压缩程序,几分钟能缩到几秒。用 -I--use-compress-program)指定外部程序:

1
2
3
4
5
6
# pigz = 多线程版 gzip,产物仍是标准 .gz,完全兼容
tar -I pigz -cf app.tar.gz -C /parent dir

# zstd / xz 用 -T0 自动吃满所有核心
tar -I 'zstd -T0' -cf app.tar.zst -C /parent dir
tar -I 'xz -T0' -cf app.tar.xz -C /parent dir

六、tar 的边界:什么时候换工具

tar 强在”完整保留 Unix 语义 + 流式传输”,但它不是万能的:

工具 强项 典型场景
tar + gzip/zstd 完整保留权限/owner/链接/扩展属性,流式管道 Linux 内部打包、备份、迁移
zip 跨平台(Windows 原生支持),可在包内随机取单文件 发给 Windows 用户
7z 压缩比高,支持加密 极致体积、需要密码保护
zstd(独立用) 单文件超快压缩,适合实时/流 日志、数据管道的即时压缩
rsync 只传差异、可断点续传 反复同步大目录、远程增量同步
borg / restic 专业增量、去重、加密备份 长期、海量、需去重加密的备份体系

一句话决策:Linux 内部流转用 tar;给 Windows 用 zip;要省空间加密用 7z;反复同步用 rsync;正经的企业备份体系,上 borg/restic。