Tar 文件

目录

tar 文件是一个非常常见的文件格式,随便打开一个 Github 仓库,看看它的 Releases,大概率就能发现提供了两种格式的源代码压缩包: .zip.tar.gz

这里细心的读者想必已经发现,相比 .zip.tar.gz 有两个后缀名。没错,tar 文件并不是一个压缩格式,而是将多个文件合并成单个文件的打包归档的格式。用 tar 来制作压缩包,就是先将多个文件打包成一个 tar 文件 foo.tar ,再用其他压缩算法把这个 tar 文件进行压缩,得到最终的压缩包 foo.tar.** 。根据最后使用的不同压缩方法,得到的后缀名不同。以 GNU 的 tar 程序为例,其支持 gzip、xz、bzip2 等压缩方法,得到的后缀名分别是 foo.tar.gzfoo.tar.xzfoo.tar.bzip2

Tar 的历史背景

可现在的压缩程序都是同时完成打包和压缩,为什么还需要 tar 的文件格式来单独打包?这是因为 tar 的历史原因。tar 格式诞生于 47 年前的 1979 年。它有多古老?这么说吧,在 GNU 的 tar 程序,需要使用 --file 参数来显式指定对 tar 文件进行操作。这对于现代的 CLI 程序来说几乎是默认的行为,而在 GNU 的 tar 程序中还需要选项指定。

如果没有指定这个选项,会发生什么?根据 GNU tar 的文档,它会先看环境变量 TAPE 的值。 TAPE 指的正是磁带。没错,在 tar 诞生的时代,Unix 的主流备份介质仍是磁带。磁带是一种顺序 I/O 设备,不具备文件系统,操作系统会将其看作看作一个可以持续写入的字节流设备。

如果要把多个文件备份到磁带中该怎么办?不能先写入 file1 的数据,再写 file2 的数据,因为这样第二次写入会覆盖 file1 的数据。那如果想现代硬盘一样,在写入 file1 后移动磁带的写入头,再写入 file2 呢?因为磁带的磁头只能向前或向后卷动,无法像磁盘那样随机跳转到任意位置,因此这样的操作会造成大量的浪费。那怎么办?答案就显而易见了——用 tar 把多个文件打包成一个文件再后写入磁带。

Tar 文件格式

在我最近心血来潮,决定研究 tar 文件的格式解析后,我发现 tar 文件的格式出乎意料地简单。整个 tar 文件并没有在开头设置魔数,而是单纯由文件对象组成。文件对象包含其文件的元数据,如文件名、所有者、时间戳、权限和目录结构等。

具体来说,tar 文件由多个 Record 构成,每个 Record 包含多个 512 字节的“块”。也就是说,默认情况下 tar 文件的大小会是 512 字节的整数倍。以 GNU 的 tar,在其默认参数下1,每个 Record 包含 20 个块,大小为 10KiB。因此 GNU tar 产生的 tar 文件大小最小为 10KiB,大小也为 10KiB 的倍数。在不满足的情况下,会在文件末尾用全零字节的空块填充到满足为止。Record 的概念和大小取值看起来有点奇怪,但这应该是磁带时代的历史遗留。

虽然我之前说 tar 文件是由文件对象组成,现在又说 tar 文件由 Record 组成。其实两者并不冲突,关系更像是文件系统中块和文件的关系。

文件对象和 Record 一样也是由多个 512 字节的块组成,最开头是一个 Header 块,存储了文件的元数据。如果是普通文件,会在 Header 块之后会用多个块来存储文件内容。文件大小不满足 512 字节的整数倍,也就是无法恰好划分为多个块时,用零字节填充到满足为止。

原始 Unix tar 格式

原始 Unix tar 格式中 Header 的内容定义如下2

起始字节偏移量字段内容
0100文件路径
1008POSIX 风格的文件权限, 八进制数字表示
1088文件所有者的数字 ID, 八进制表示
1168文件所属组的数字 ID, 八进制表示
12412文件大小, 按字节数量计算, 八进制表示
13612上次修改时间的 UNIX 时间戳, 八进制表示
1488checksum
1561文件类型标识符
157100链接的文件名

原始 Unix tar 格式的文件类型标识符只有以下三种,部分 tar 实现会用文件路径最后是否有 / 来标识文件是否为目录。

含义
‘0’ 或零字节普通文件
‘1’硬链接
‘2’符号链接

只使用了 257 个字节,剩余的 512-257=255 个字节用零字节填充。而没有使用的字段也用零字节填充,例如普通文件的“链接的文件名”就是全零字节。

出于历史原因,存储数值的字段都是用 ASCII 数字字符,并用八进制表示3。前导填充'0’,而且要在末尾填充零字节或空格,也就是实际有意义的字节数量比实际偏移数量少一。例如文件大小虽然预留了 12 个字节,但实际只能存储 11 个八进制数字,所以最大的文件大小为 8 GB 左右。

UStar 格式

后续的 POSIX 标准引入了新的 UStar (Unix Standard TAR) 格式。兼容旧的 tar 格式,引入了额外的字段,并扩充了文件类型标识符的取值空间。具体定义如下4

起始字节偏移量字段内容
0257和旧的 tar 格式相同
2576UStar 标识符, “ustar\x00”
2632UStar 版本标识符, “00” 或 “\x00\x00”
26532文件所有者的名字
29732文件所属组的名字
3298主设备号
3378次设备号
345155文件路径前缀

UStar 格式通过将超过 100 字节的文件路径拆分到文件路径前缀中,从而使得最大文件名长度拓展为 255 个字符。

UStar 的文件类型标识符支持以下取值:

含义
‘0’ 或零字节普通文件
‘1’硬链接
‘2’符号链接
‘3’字符设备文件
‘4’块设备文件
‘5’目录
‘6’命名管道
‘7’连续文件, 已废弃, 现代文件系统不使用, 视为普通文件
‘g’全局文件元数据扩展头
‘x’下一个文件的元数据拓展头
‘A’-‘Z’可自定义

全局文件元数据扩展头指示当前 Header 块包含适用于后续所有文件条目的元数据,直到遇到下一个全局扩展头覆盖它。

下一个文件的元数据拓展头指示在当前块之后,包含仅适用于下一个文件条目的额外元数据,用于突破 UStar 格式的字段长度限制。

其他 tar 格式

后来,为了突破 UStar 格式的限制,有不少其他的 tar 格式被提出。例如后续 POSIX 标准引入的 pax 格式,GNU 的 tar 格式等。对这些格式我没有进行细致的研究,因此本文也不再讲解。

结尾

说实话,tar 文件最令我意外的地方是它的简洁,甚至就是一个单纯的文本文件。只需要按照特定的规定,就能蕴含丰富的结构化信息。真的很有意思。


  1. 详见文档 https://www.gnu.org/software/tar/manual/ 中关于 --blocking-factor, --record-size 等参数的说明。 ↩︎

  2. https://en.wikipedia.org/wiki/Tar_(computing)#Header ↩︎

  3. 根据 Wikipedia 的说法,这是因为 Unix 最初是为 PDP-7 设计的,该系统使用 18 位 CPU 和 6 位字符编码,因此三位八进制表示法更具优势 ↩︎

  4. https://en.wikipedia.org/wiki/Tar_(computing)#UStar_format ↩︎