日常开发中经常需要打包项目文件进行备份或传输,tar 命令是 Linux 系统中最经典的归档工具。很多人只知道 tar -czvf 这一套组合拳,但 tar 的实现原理其实很有意思。
tar 的核心概念:归档 vs 压缩
首先需要明确一个概念:tar 本身是归档工具,不是压缩工具。
归档(Archive)只是把多个文件打包成一个文件,相当于把一堆散落的文件装进一个箱子,箱子体积等于所有物品体积之和。
压缩(Compression)才是通过算法减少文件体积,比如把箱子里的衣服抽真空。
所以 tar 命令的典型用法是:
bash
tar -cvf archive.tar files/ # 只打包,不压缩
tar -czvf archive.tar.gz files/ # 打包 + gzip 压缩
tar -cjvf archive.tar.bz2 files/ # 打包 + bzip2 压缩
tar -cJvf archive.tar.xz files/ # 打包 + xz 压缩
-z、-j、-J 参数告诉 tar 在打包后调用对应的压缩工具。
tar 文件格式解析
tar 文件格式非常简单,每个文件前面加一个 512 字节的 header,header 记录文件名、大小、权限、时间戳等元数据,然后是文件内容(补齐到 512 字节的倍数),最后用两个 512 字节的空块结束。
c
struct tar_header {
char name[100]; // 文件名
char mode[8]; // 权限
char uid[8]; // 用户ID
char gid[8]; // 组ID
char size[12]; // 文件大小
char mtime[12]; // 修改时间
char checksum[8]; // 校验和
char typeflag; // 文件类型
char linkname[100]; // 链接目标
char magic[6]; // "ustar"
char version[2]; // 版本
char uname[32]; // 用户名
char gname[32]; // 组名
char devmajor[8]; // 主设备号
char devminor[8]; // 次设备号
char prefix[155]; // 路径前缀
char padding[12]; // 填充
};
这个格式有几个有趣的设计:
- 所有字段都是 ASCII 字本 :为了保证跨平台兼容性,数字用八进制字符串存储,比如权限 755 存储为
0000755\0 - 固定长度 header:每个 header 正好 512 字节,方便随机访问
- 校验和计算:header 的 checksum 字段初始为空格,计算所有字节的累加和
可以自己写一个简单的 tar 打包器:
python
import os
def create_tar(files, output):
with open(output, 'wb') as f:
for file_path in files:
# 创建 header
header = bytearray(512)
name = os.path.basename(file_path).encode('utf-8')[:100]
header[0:len(name)] = name
stat = os.stat(file_path)
# 权限(八进制)
header[100:107] = f'{stat.st_mode:07o}\0'.encode()
# 文件大小(八进制)
header[124:135] = f'{stat.st_size:011o}\0'.encode()
# 计算校验和
checksum = sum(header)
header[148:155] = f'{checksum:06o}\0 '.encode()
# 写入 header
f.write(header)
# 写入文件内容
with open(file_path, 'rb') as infile:
content = infile.read()
f.write(content)
# 补齐到 512 字节
padding = (512 - len(content) % 512) % 512
f.write(b'\0' * padding)
# 结束标记(两个空块)
f.write(b'\0' * 1024)
create_tar(['file1.txt', 'file2.txt'], 'archive.tar')
tar 选项的字母含义
tar 命令的选项设计比较独特,可以不带 - 前缀:
bash
tar cvf archive.tar files/ # 不带连字符
tar -cvf archive.tar files/ # 带连字符
常用选项的含义:
c(create): 创建归档x(extract): 解压归档t(list): 列出归档内容v(verbose): 显示详细过程f(file): 指定归档文件名(必须放在最后,因为后面跟文件名)z(gzip): 使用 gzip 压缩/解压j(bzip2): 使用 bzip2 压缩/解压J(xz): 使用 xz 压缩/解压C: 解压到指定目录
记住一个技巧:f 必须放在最后,因为后面要紧跟文件名,其他选项顺序随意。
实战技巧:增量备份与排除文件
排除特定文件
bash
# 排除 node_modules 和 .git 目录
tar -czvf project.tar.gz \
--exclude='node_modules' \
--exclude='.git' \
project/
# 从文件读取排除列表
tar -czvf project.tar.gz -X exclude.txt project/
exclude.txt 文件内容:
node_modules
.git
*.log
.DS_Store
增量备份
tar 支持基于时间的增量备份:
bash
# 完整备份
tar -czvf full-backup.tar.gz /data
# 增量备份(只备份今天修改的文件)
tar -czvf incremental-$(date +%Y%m%d).tar.gz \
-N "today" \
/data
# 备份最近 7 天修改的文件
tar -czvf week-changes.tar.gz \
-N "$(date -d '7 days ago' +%Y-%m-%d)" \
/data
三种压缩算法对比
| 算法 | 选项 | 压缩率 | 速度 | 适用场景 |
|---|---|---|---|---|
| gzip | -z | 中等 | 快 | 日常使用,网络传输 |
| bzip2 | -j | 高 | 慢 | 长期归档,节省空间 |
| xz | -J | 最高 | 最慢 | 大文件归档,发布包 |
实际测试(打包 100MB 项目代码):
bash
time tar -czvf test.tar.gz project/ # 2.3s, 18MB
time tar -cjvf test.tar.bz2 project/ # 8.1s, 14MB
time tar -cJvf test.tar.xz project/ # 25s, 11MB
xz 压缩率最高但速度最慢,适合一次性压缩长期存储的归档。
解压技巧与常见问题
解压到指定目录
bash
# 解压到 /opt 目录
tar -xzvf archive.tar.gz -C /opt
# 解压单个文件
tar -xzvf archive.tar.gz path/to/file.txt
查看归档内容(不解压)
bash
tar -tzvf archive.tar.gz
输出格式:
-rw-r--r-- user/group 1234 2026-05-08 10:30 file1.txt
drwxr-xr-x user/group 0 2026-05-08 10:30 directory/
绝对路径陷阱
tar 默认会移除路径前的 /,防止解压时覆盖系统文件:
bash
tar -czvf backup.tar.gz /home/user/project/
# 归档内部路径是 home/user/project/,不是 /home/user/project/
如果需要保留绝对路径(危险操作):
bash
tar -czvf backup.tar.gz -P /home/user/project/
文件名乱码问题
跨平台传输时可能遇到文件名编码问题:
bash
# 强制使用 UTF-8
tar -xzvf archive.tar.gz --force-local
性能优化:大文件处理
打包超大文件时,可以通过管道避免临时文件:
bash
# 直接通过 SSH 传输
tar -czvf - /large/directory | ssh user@server "cat > backup.tar.gz"
# 配合 split 分卷压缩
tar -czvf - /large/directory | split -b 1G - backup.tar.gz.part
解压分卷文件:
bash
cat backup.tar.gz.part* | tar -xzvf -
Web 实现:浏览器端 tar 解析
借助 JavaScript 可以在浏览器中解析 tar 文件:
javascript
async function parseTar(buffer) {
const view = new DataView(buffer)
let offset = 0
const files = []
while (offset < buffer.byteLength - 1024) {
// 读取文件名
const name = new TextDecoder().decode(
new Uint8Array(buffer, offset, 100)
).replace(/\0/g, '')
if (!name) break // 遇到空块,结束
// 读取文件大小(八进制)
const sizeStr = new TextDecoder().decode(
new Uint8Array(buffer, offset + 124, 11)
).trim()
const size = parseInt(sizeStr, 8)
// 提取文件内容
const content = new Uint8Array(buffer, offset + 512, size)
files.push({ name, size, content })
// 移动到下一个文件(512字节对齐)
offset += 512 + Math.ceil(size / 512) * 512
}
return files
}
// 使用示例
const response = await fetch('archive.tar')
const buffer = await response.arrayBuffer()
const files = await parseTar(buffer)
console.log(files.map(f => f.name))
实用脚本:项目快速备份
bash
#!/bin/bash
# project-backup.sh
PROJECT_NAME="my-project"
BACKUP_DIR="/backup"
DATE=$(date +%Y%m%d-%H%M%S)
tar -czvf "${BACKUP_DIR}/${PROJECT_NAME}-${DATE}.tar.gz" \
--exclude='node_modules' \
--exclude='.next' \
--exclude='dist' \
--exclude='*.log' \
-C /home/user/projects ${PROJECT_NAME}
# 保留最近 7 天备份
find ${BACKUP_DIR} -name "${PROJECT_NAME}-*.tar.gz" -mtime +7 -delete
echo "Backup created: ${PROJECT_NAME}-${DATE}.tar.gz"
总结
tar 命令的设计体现了 Unix 哲学:做好一件事,与其他工具配合。它专注于归档,把压缩交给 gzip/bzip2/xz,把传输交给 ssh,把分割交给 split。
掌握 tar 的核心要点:
- 理解归档与压缩的区别
- 记住
f选项必须放最后 - 善用
--exclude排除文件 - 根据场景选择压缩算法
想了解更多 tar 参数?直接查看帮助:Linux tar 命令详解