- 如果要格式化一个分区来研究文件系统格式则必须有一个空闲的磁盘分区,为了方便实验,我们把一个文件当作分区来格式化,然后分析这个文件中的数据来印证上面所讲的要点。首先创建一个1MB的文件并清零:
$ dd if=/dev/zero of=disk count=256 bs=4k
我们知道 cp 命令可以把一个文件拷贝成另一个文件,而 dd 命令可以把一个文件的一部分拷贝成另一个文件。这个命令的作用是把 /dev/zero 文件开头的 1M(256×4K) 字节拷贝成文件名为 disk 的文件。上一讲中我们看到 /dev/zero 是一个特殊的设备文件,它没有磁盘数据块,对它进行读操作传给设备号为 **1, 5** 的驱动程序。/dev/zero 这个文件可以看作是无穷大的,不管从哪里开始读,读出来的都是字节0x00。因此这个命令拷贝了 1M 个 0x00 到 disk 文件。if 和 of 参数表示输入文件和输出文件, count 和 bs 参数表示拷贝多少次,每次拷多少字节。
- 做好之后对文件 disk 进行格式化,也就是把这个文件的数据块合起来看成一个1MB的磁盘分区,在这个分区上再划分出块组。
$ mke2fs disk
mke2fs 1.42.13 (17-May-2015)
Discarding device blocks: done
Creating filesystem with 1024 1k blocks and 128 inodes
Allocating group tables: done
Writing inode tables: done
Writing superblocks and filesystem accounting information: done
- 格式化一个真正的分区应该指定块设备文件名,例如 /dev/sda1,而这个 disk 是常规文件而不是块设备文件。
- 现在 disk 的大小仍然是1MB,但不再是全 0 了,其中已经有了块组和描述信息。用 dumpe2fs 工具可以查看这个分区的超级块和块组描述符表中的信息:
$ dumpe2fs disk
dumpe2fs 1.42.13 (17-May-2015)
Filesystem volume name: <none>
Last mounted on: <not available>
Filesystem UUID: f70563b5-ad96-4034-a65e-a0cfb4e878dd
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: ext_attr resize_inode dir_index filetype sparse_super large_file
Filesystem flags: signed_directory_hash
Default mount options: user_xattr acl
Filesystem state: clean
Errors behavior: Continue
Filesystem OS type: Linux
Inode count: 128
Block count: 1024
Reserved block count: 51
Free blocks: 986
Free inodes: 117
First block: 1
Block size: 1024
Fragment size: 1024
Reserved GDT blocks: 3
Blocks per group: 8192
Fragments per group: 8192
Inodes per group: 128
Inode blocks per group: 16
Filesystem created: Thu May 3 16:43:58 2018
Last mount time: n/a
Last write time: Thu May 3 16:43:58 2018
Mount count: 0
Maximum mount count: -1
Last checked: Thu May 3 16:43:58 2018
Check interval: 0 (<none>)
Reserved blocks uid: 0 (user root)
Reserved blocks gid: 0 (group root)
First inode: 11
Inode size: 128
Default directory hash: half_md4
Directory Hash Seed: aab73ef7-9993-4fd3-88e2-0aad174dd58c
Group 0: (Blocks 1-1023)
Primary superblock at 1, Group descriptors at 2-2
Reserved GDT blocks at 3-5
Block bitmap at 6 (+5), Inode bitmap at 7 (+6)
Inode table at 8-23 (+7)
986 free blocks, 117 free inodes, 2 directories
Free blocks: 38-1023
Free inodes: 12-128
根据上面讲过的知识简单计算一下:
· 块大小是 1024 字节, 1MB 的分区共有 1024 个块。
· 第 0 个块是启动块,启动块之后才算 ext2 文件系统的开始,因此 Group 0 占据第 1 个到第 1023 个块,共 1023 个块。
· 块位图占一个块,共有 1024×8=8192 个 bit,足够表示这 1023 个块了,因此只要一个块组就够了。
· 默认是每 8KB 分配一个 inode ,因此 1MB 的分区对应 128 个 inode。
· 这些数据都和 dumpe2fs 的输出吻合。
- 用常规文件制作而成的文件系统也可以像磁盘分区一样mount到某个目录,例如:
$ sudo mount -o loop disk /mnt
$ cd /mnt
$ ls -la
total 17
drwxr-xr-x 3 root root 1024 5月 3 16:43 .
drwxr-xr-x 28 root root 4096 4月 24 07:58 ..
drwx------ 2 root root 12288 5月 3 16:43 lost+found
- -o loop 选项告诉 mount 这是一个常规文件而不是一个块设备文件。mount 会把它的数据块中的数据当作分区格式来解释。文件系统格式化之后在根目录下自动生成三个子目录: ., ..和 lost+found。其它子目录下的 . 表示当前目录, ..表示上一级目录,而根目录的 .和 ..都表示根目录本身。 lost+found 目录由 e2fsck 工具使用,如果在检查磁盘时发现错误,就把有错误的块挂在这个目录下,因为这些块不知道是谁的,找不到主,就放在这里“失物招领”了。
现在可以在 /mnt 目录下添加删除文件,这些操作会自动保存到文件 disk 中。然后把这个分区 umount 下来,以确保所有的改动都保存到文件中了。
$ sudo umount /mnt
- 现在我们用二进制查看工具查看这个文件系统的所有字节,并且同 dumpe2fs 工具的输出信息相比较,就可以很好地理解文件系统的存储布局了。
$ od -tx1 -Ax disk
000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
000400 80 00 00 00 00 04 00 00 33 00 00 00 da 03 00 00
000410 75 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00
......
以*开头的行表示这一段数据全是零因此省略了。
- 从 000000 开始的 1KB 是启动块,由于这不是一个真正的磁盘分区,启动块的内容全部为零。从 000400 到 0007ff 的 1KB 是超级块,对照着 dumpe2fs 的输出信息,详细分析如下:
* 超级块中从 0004d0 到末尾的 204 个字节是填充字节,保留未用,上图未画出。注意, ext2 文件系统中各字段都是按小端存储的,如果把字节在文件中的位置看作地址,那么靠近文件开头的是低地址,存低字节。各字段的位置、长度和含义自行搜索。
- 从 000800 开始是块组描述符表,这个文件系统较小,只有一个块组描述符,对照着 dumpe2fs 的输出信息分析如下:
...
Group 0: (Blocks 1-1023)
Primary superblock at 1, Group descriptors at 2-2
Reserved GDT blocks at 3-5
Block bitmap at 6 (+5), Inode bitmap at 7 (+6)
Inode table at 8-23 (+7)
986 free blocks, 117 free inodes, 2 directories
Free blocks: 38-1023
Free inodes: 12-128
...
- 整个文件系统是 1MB,每个块是 1KB,应该有 1024 个块,除去启动块还有 1023 个块,分别编号为 1-1023,它们全都属于 Group 0。其中, Block 1 是超级块,接下来的块组描述符指出,块位图是 Block 6,因此中间的 Block 2-5 是块组描述符表,其中Block 3-5 保留未用。块组描述符还指出,inode 位图是Block 7, inode 表是从Block 8 开始的,那么 inode 表到哪个块结束呢?由于超级块中指出每个块组有 128 个 inode,每个 inode 的大小是 128 字节,因此共占 128 X 128 / 1024 = 16 个块, inode 表的范围是 Block 8-23。
- 从 Block 24 开始就是数据块了。块组描述符中指出,空闲的数据块有 986 个,由于文件系统是新创建的,空闲块是连续的 Block 38-1023,用掉了前面的 Block 24-37。从块位图中可以看出,前37位(前4个字节加最后一个字节的低5位)都是 1,就表示Block 1-37 已用:
001800 ff ff ff ff 1f 00 00 00 00 00 00 00 00 00 00 00
001810 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
001870 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 80
001880 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
*
- 在块位图中,Block 38-1023 对应的位都是 0 (一直到 001870 那一行最后一个字节的低 7 位),接下来的位已经超出了文件系统的空间,不管是 0 还是 1 都没有意义。可见,块位图每个字节中的位应该按从低位到高位的顺序来看。以后随着文件系统的使用和添加删除文件,块位图中的 1 就变得不连续了。
- 块组描述符指出,空闲的 inode 有 117 个,由于文件系统是新创建的,空闲的 inode 也是连续的,inode编号从 1 到 128,空闲的 inode 编号从 12 到 128。从inode位图可以看出,前11位都是 1,表示前11个inode已用:
001c00 ff 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00
001c10 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
*
001c00 这一行的 128 位就表示了所有 inode,因此下面的行不管是 0 还是 1 都没有意义。已用的 11 个 inode 中,前 10 个 inod e是被 ext2 文件系统保留的,其中第 2 个 inode 是根目录,第 11 个 inode 是 lost+found 目录,块组描述符也指出该组有两个目录,就是根目录和 lost+found。
探索文件系统还有一个很有用的工具 debugfs,它提供一个命令行界面,可以对文件系统做各种操作,例如查看信息、恢复数据、修正文件系统中的错误。下面用 debugfs 打开 disk 文件,然后在提示符下输入 stat / ,这时在新的一屏中显示 根目录 的 inode 信息:
$ debugfs disk
debugfs 1.40.2 (12-Jul-2007)
debugfs: stat/
Inode: 2 Type: directory Mode: 0755 Flags: 0x0 Generation: 0
User: 1000 Group: 1000 Size: 1024
File ACL: 0 Directory ACL: 0
Links: 3 Blockcount: 2
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x4764cc3b -- Sun Dec 16 14:56:59 2007
atime: 0x4764cc3b -- Sun Dec 16 14:56:59 2007
mtime: 0x4764cc3b -- Sun Dec 16 14:56:59 2007
BLOCKS:
(0):24
TOTAL: 1
输入 q 可退出调试。
把以上信息和 od 命令的输出对照起来分析:
上图中的 st_mode 以八进制表示,包含了文件类型和文件权限,最高位的 4 表示文件类型为目录(各种文件类型的编码详见下表),低位的 755 表示权限。Size 是 1024,说明根目录现在只有一个数据块。 Links 为 3 表示根目录有三个硬链接,分别是根目录下的.和..,以及 lost+found 子目录下的 ..。
宏定义 | 编码 | 编码说明 |
---|---|---|
S_IFMT | 0170000 | 文件类型的位遮罩 |
S_IFSOCK | 0140000 | socket |
S_IFLNK | 0120000 | 符号链接(symbolic link) |
S_IFREG | 0100000 | 一般文件 |
S_IFBLK | 0060000 | 区块装置(block device) |
S_IFDIR | 0040000 | 目录 |
S_IFCHR | 0020000 | 字符装置(character device) |
S_IFIFO | 0010000 | 先进先出(fifo) |
S_ISUID | 0004000 | 文件的(set user-id on execution)位 |
S_ISGID | 0002000 | 文件的(set group-id on execution)位 |
S_ISVTX | 0001000 | 文件的sticky位 |
S_IRWXU | 00700 | 文件所有者的遮罩值(即所有权限值) |
S_IRUSR | 00400 | 文件所有者具可读取权限 |
S_IWUSR | 00200 | 文件所有者具可写入权限 |
S_IXUSR | 00100 | 文件所有者具可执行权限 |
S_IRWXG | 00070 | 用户组的遮罩值(即所有权限值) |
S_IRGRP | 00040 | 用户组具可读取权限 |
S_IWGRP | 00020 | 用户组具可写入权限 |
S_IXGRP | 00010 | 用户组具可执行权限 |
S_IRWXO | 00007 | 其他用户的遮罩值(即所有权限值) |
S_IROTH | 00004 | 其他用户具可读取权限 |
S_IWOTH | 00002 | 其他用户具可写入权限 |
S_IXOTH | 00001 | 其他用户具可执行权限 |
注意,虽然我们通常用 / 表示根目录,但是并没有名为 / 的硬链接,事实上, / 是路径分隔符,不能在文件名中出现。
- 这里的Blockcount是以 512 字节为一个块来数的,并非格式化文件系统时所指定的块大小,磁盘的最小读写单位称为扇区(Sector),通常是 512 字节,所以 Blockcount 是磁盘的物理块数量,而非分区的逻辑块数量。根目录数据块的位置由上图中的 Blocks[0] 指出,也就是第 24个 块,它在文件系统中的位置是 24×0x400=0x6000 ,从 od 命令的输出中找到 006000 地址,它的格式是这样:
- 目录的数据块由许多不定长的记录组成,每条记录描述该目录下的一个文件,在上图中用框表示。第一条记录描述 inode 号为 2 的文件,也就是根目录本身,该记录的总长度为 12 字节,其中文件名的长度为 1 字节,文件类型为 2 (见下表,注意此处的文件类型编码和 st_mode 不一致),文件名是 . 。
- 第二条记录也是描述 inode 号为 2 的文件(根目录),该记录总长度为 12 字节,其中文件名的长度为 2 字节,文件类型为 2,文件名字符串是 ..。
- 第三条记录一直延续到该数据块的末尾,描述 inode 号为11的文件(lost+found目录),该记录的总长度为 1000 字节(和前面两条记录加起来是1024字节),文件类型为 2,文件名字符串是 lost+found,后面全是 0 字节。
- 如果要在根目录下创建新的文件,可以把第三条记录截短,在原来的 0 字节处创建新的记录。如果该目录下的文件名太多,一个数据块不够用,则会分配新的数据块,块编号会填充到 inode 的 Blocks[1] 字段。
- debugfs 也提供了 cd、ls 等命令,不需要mount 就可以查看这个文件系统中的目录,例如用 ls 查看根目录:
2 (12) . 2 (12) .. 11 (1000) lost+found
列出了 inode 号、记录长度和文件名,这些信息都是从根目录的数据块中读出来的。