总体描述
从EXT2文件系统的角度来看磁盘——在BIOS下,不应该认为是磁盘,而是通过Block IO
表示的一个磁盘分区——的构成是这样的:
启动块 | 块组0 | 块组1 | … | 块组N |
---|
关于块的有两种不同的含义:
- 硬件上表示的是磁盘本身的块,它的大小可通过
BlockIo->Media->BlockSize
得到; - EXT2文件系统中有一个软件上的块的概念:EXT2文件系统是基于块的文件系统,它将硬盘划分为若干块,每个块的长度都相同,按块管理元数据和文件系统。
EXT2的(软件)块会受到底层磁盘的块大小的影响,可以的取值有1024字节、2048字节和4096字节,这跟磁盘的大小并不对应,如果磁盘块大小是512字节,那么EXT2的块大小通常会取值1024字节,当然这并不是强制的,构建EXT2文件系统时可以根据实际的情况修改软件块大小,毕竟,跟磁盘块大小这种硬件相关的值不同,EXT2的块大小是软件的概念,后续如果没有特别说明,指的都是EXT2软件块。EXT2的块大小还会影响到文件系统中的最大文件长度:
块大小 | 最大文件长度 |
---|---|
1024 | 16GiB |
2048 | 256GiB |
4096 | 2TiB |
另外,一个块(后续实现中都按1024个字节(1KB)为准)最多只会存放一个文件的数据,如果一个文件的数据大于一个块,最终文件数据占据的块也会向上取整。
启动块属于磁盘分区结构的一部分,而块组0~块组N占据分区的其它部分,它们是这里研究的重点,块组的大小也是可以设定,一般是8092个块构成一个块组。块组的分布如下所示:
超级块 | 组描述符 | 数据块位图 | Inode位图 | Inode表 | 数据块 |
---|
总体上来看一个Block IO
表示的磁盘分区的结构如下图所示:
块组中各个结构的作用如下:
- 超级块:用于存储文件系统自身元数据的核心结构,超级块理论上在所有的块组中都存在,不过只使用第一个块组中的超级块即可,之所以这样冗余设计,是为了一个超级块被破坏时文件系统不至于穗槐。超级块通常位于分区的第二个块(因为第一块是启动块)。
- 组描述符:每个块组都有一个组描述符的集合,紧随超级块之后,其中保存的信息反映了文件系统中每个块组的内容,因此不仅关系到当前块组的数据块,还与其它块组的数据块和Inode块相关。占据的块跟块大小,块组个数和组描述符长度有关。
- 数据块位图和Inode位图:用于保存一长串的比特位,每个比特位都对应于一个数据块或者Inode,用于表示对应的数据块或Inode是否空闲。分别占据1个块。
- Inode表:Inode用于保存文件系统中与各个文件和目录相关的元数据,每个文件或目录都有且只有一个对应的Inode,其中包含元数据(如访问权限、上次修改的日志,等等,但不包含文件名)和指向文件数据的指针;占据若干个块。
- 数据块:包含了文件系统中的文件的有用数据,占据剩余的块。
创建测试磁盘
通过上述的数据结构信息,可以编写代码来读取EXT2文件系统的信息,不过首先需要创建一个EXT2的文件系统,可以使用DiskGenius来完成:
通过上图中文件系统的详细信息,结合BIOS的基础,可以确定以下信息:
- BIOS启动后会对磁盘安装
Block IO
,也会对磁盘上的分区安装Block IO
,编写代码时需要关注的是分区对应的Block IO
,它相对于磁盘的起始位置通过“起始扇区号”可以确定是2048,再加上”扇区大小“是512字节,所以分区所在的Block IO
描述的存储空间对于硬盘来说偏移是2048 * 512 = 1048576 = 0x100000字节,从DiskGenius可以查看该位置的信息:
- 分区存储空间的开始就是第一个块是启动块,如上图所示暂时还是0,对于UEFI BIOS来说,并不需要这个内容。
- 启动分区占据一个块,而之后就是块组,块组中的第一个块就是超级块,启动块的大小是一个快(1024字节),所以超级块的起始位置是0x100400:
关于这两个地址可以通过宏来表示:
#define BBSIZE 1024
#define SBSIZE 1024
#define BBOFF ((UINTN)(0))
#define SBOFF ((UINTN)(BBOFF + BBSIZE))
于是为了判断某个Block IO
中是否存在EXT2文件系统,第一步需要做的就是从上述位置获取1024个字节的数据,并判断它是否是一个超级块。
不过DiskGenius不能写文件,所以如果要往里面存放文件,则需要使用Linux创建EXT2版本的disk.img文件,具体操作如下:
# dd if=/dev/zero of=disk.img count=200 bs=1M
200+0 records in
200+0 records out
209715200 bytes (210 MB, 200 MiB) copied, 0.113229 s, 1.9 GB/s
# mkfs.ext4 disk.img
mke2fs 1.44.1 (24-Mar-2018)
Discarding device blocks: done
Creating filesystem with 204800 1k blocks and 51200 inodes
Filesystem UUID: 513c5206-7726-4dba-8bb1-fbf7b68c449c
Superblock backups stored on blocks:
8193, 24577, 40961, 57345, 73729
Allocating group tables: done
Writing inode tables: done
Creating journal (4096 blocks): done
Writing superblocks and filesystem accounting information: done
# mkdir loop
# mount -o loop disk.img loop/
# cd loop
# ls
lost+found
# mkdir tmp1
# mkdir tmp2
# echo hello1 >> tmp1.log
# echo hello2 >> tmp2.log
# cd ..
# umount loop
这里创建了一个大小是200MB的虚拟磁盘disk.img,格式化成EXT4文件系统,并增加了tmp1和tmp2两个目录,tmp1.log和tmp2.log两个文件。
超级块
一个正常超级块包含的数据如下,首先是EXT2需要支持的:
起始 | 结束 | 大小 | 描述 |
---|---|---|---|
0 | 3 | 4 | Total number of inodes in file system. |
4 | 7 | 4 | Total number of blocks in file system. |
8 | 11 | 4 | Number of reserved blocks. |
12 | 15 | 4 | Total number of unallocated blocks. |
16 | 19 | 4 | Total number of unallocated inodes. |
20 | 23 | 4 | Block number of the block containing the superblock. This is 1 on 1024 byte block size filesystems, and 0 for all others. |
24 | 27 | 4 | log2 (block size) - 10 (In other words, the number to shift 1,024 to the left by to obtain the block size). |
28 | 31 | 4 | log2 (fragment size) - 10 (In other words, the number to shift 1,024 to the left by to obtain the fragment size). |
32 | 35 | 4 | Number of blocks in each block group. |
36 | 39 | 4 | Number of fragments in each block group. |
40 | 43 | 4 | Number of inodes in each block group. |
44 | 47 | 4 | Last mount time (in POSIX time). |
48 | 51 | 4 | Last written time (in POSIX time). |
52 | 53 | 2 | Number of times the volume has been mounted since its last consistency check (fsck). |
54 | 55 | 2 | Number of mounts allowed before a consistency check (fsck) must be done. |
56 | 57 | 2 | Magic signature (0xef53), used to help confirm the presence of Ext4 on a volume. |
58 | 59 | 2 | File system state. |
60 | 61 | 2 | What to do when an error is detected. |
62 | 63 | 2 | Minor portion of version (combine with Major portion below to construct full version field). |
64 | 67 | 4 | POSIX time of last consistency check (fsck). |
68 | 71 | 4 | Interval (in POSIX time) between forced consistency checks (fsck). |
72 | 75 | 4 | Operating system ID from which the filesystem on this volume was created (see below). |
76 | 79 | 4 | Major portion of version (combine with Minor portion above to construct full version field). |
80 | 81 | 2 | User ID that can use reserved blocks. |
82 | 83 | 2 | Group ID that can use reserved blocks. |
其次是如果要支持EXT4动态超级块的话,则还有额外的内容:
起始 | 结束 | 大小 | 描述 |
---|---|---|---|
84 | 87 | 4 | First non-reserved inode in file system. |
88 | 89 | 2 | Size of each inode structure in bytes. |
90 | 91 | 2 | Block group that this superblock is part of for backup copies. |
92 | 95 | 4 | Optional features present. |
96 | 99 | 4 | Required features present. |
100 | 103 | 4 | Features that if not supported the volume must be mounted read-only. |
104 | 119 | 16 | File system UUID. |
120 | 135 | 16 | Volume name. |
136 | 199 | 64 | Path Volume was last mounted to. |
200 | 203 | 4 | Compression algorithm used. |
204 | 204 | 1 | Amount of blocks to preallocate for files |
205 | 205 | 1 | Amount of blocks to preallocate for directories. |
206 | 207 | 2 | Amount of reserved GDT entries for filesystem expansion. |
208 | 223 | 16 | Journal UUID. |
224 | 227 | 4 | Journal Inode. |
228 | 231 | 4 | Journal Device number. |
232 | 235 | 4 | Head of orphan inode list. |
236 | 251 | 16 | HTREE hash seed in an array of 32 bit integers. |
252 | 252 | 1 | Hash algorithm to use for directories. |
253 | 253 | 1 | Journal blocks field contains a copy of the inode’s block array and size. |
254 | 255 | 2 | Size of group descriptors in bytes, for 64 bit mode. |
256 | 259 | 4 | Mount options. |
260 | 263 | 4 | First metablock block group, if enabled. |
264 | 267 | 4 | Filesystem Creation Time. |
268 | 335 | 68 | Journal Inode Backup in an array of 32 bit integers. |
此外如果开启了64位支持,则超级块的内容还要更多,结构体数据最大占据1024个字节,一般代码中直接设定超级块的大小是1024个字节,后面的数据:
起始 | 结束 | 大小 | 描述 |
---|---|---|---|
336 | 339 | 4 | High 32-bits of the total number of blocks. |
340 | 343 | 4 | High 32-bits of the total number of reserved blocks. |
344 | 347 | 4 | High 32-bits of the total number of unallocated blocks. |
348 | 349 | 2 | Minimum inode size. |
350 | 351 | 2 | Minimum inode reservation size. |
352 | 355 | 4 | Misc flags, such as sign of directory hash or development status. |
356 | 357 | 2 | Amount logical blocks read or written per disk in a RAID array. |
358 | 359 | 2 | Amount of seconds to wait in Multi-mount prevention checking. |
360 | 367 | 8 | Block to multi-mount prevent. |
368 | 371 | 4 | Amount of blocks to read or write before returning to the current disk in a RAID array. Amount of disks * stride. |
372 | 372 | 1 | log2 (groups per flex) - 10. (In other words, the number to shift 1,024 to the left by to obtain the groups per flex block group) |
373 | 373 | 1 | Metadata checksum algorithm used. Linux only supports crc32. |
374 | 374 | 1 | Encryption version level. |
375 | 375 | 1 | Reserved padding. |
376 | 383 | 8 | Amount of kilobytes written over the filesystem’s lifetime. |
384 | 387 | 4 | Inode number of the active snapshot. |
388 | 391 | 4 | Sequential ID of active snapshot. |
392 | 399 | 8 | Number of blocks reserved for active snapshot. |
400 | 403 | 4 | Inode number of the head of the disk snapshot list. |
404 | 407 | 4 | Amount of errors detected. |
408 | 411 | 4 | First time an error occurred in POSIX time. |
412 | 415 | 4 | Inode number in the first error. |
416 | 423 | 8 | Block number in the first error. |
424 | 455 | 32 | Function where the first error occurred. |
456 | 459 | 4 | Line number where the first error occurred. |
460 | 463 | 4 | Most recent time an error occurred in POSIX time. |
464 | 467 | 4 | Inode number in the last error. |
468 | 475 | 8 | Block number in the last error. |
476 | 507 | 32 | Function where the most recent error occurred. |
508 | 511 | 4 | Line number where the most recent error occurred. |
512 | 575 | 64 | Mount options. (C-style string: characters terminated by a 0 byte) |
576 | 579 | 4 | Inode number for user quota file. |
580 | 583 | 4 | Inode number for group quota file. |
584 | 587 | 4 | Overhead blocks/clusters in filesystem. Zero means the kernel calculates it at runtime. |
588 | 595 | 8 | Block groups with backup Superblocks, if the sparse superblock flag is set. |
596 | 599 | 4 | Encryption algorithms used, as a array of unsigned char. |
600 | 615 | 16 | Salt for the string2key algorithm. |
616 | 619 | 4 | Inode number of the lost+found directory. |
620 | 623 | 4 | Inode number of the project quota tracker. |
624 | 627 | 4 | Checksum of the UUID, used for the checksum seed. (crc32c(~0, UUID)) |
628 | 628 | 1 | High 8-bits of the last written time field. |
629 | 629 | 1 | High 8-bits of the last mount time field. |
630 | 630 | 1 | High 8-bits of the Filesystem creation time field. |
631 | 631 | 1 | High 8-bits of the last consistency check time field. |
632 | 632 | 1 | High 8-bits of the first time an error occurred time field. |
633 | 633 | 1 | High 8-bits of the latest time an error occurred time field. |
634 | 634 | 1 | Error code of the first error. |
635 | 635 | 1 | Error code of the latest error. |
636 | 637 | 2 | Filename charset encoding. |
638 | 639 | 2 | Filename charset encoding flags. |
640 | 1019 | 380 | Padding. |
1020 | 1023 | 4 | Checksum of the superblock. |
不过这里暂时不管64位特性,所以对应的超级块结构体代码如下:
//
// EXT2文件系统中的超级块,大小是1024个字节
//
typedef struct {
UINT32 Ext2FsInodeCount; // 0: Inode数据
UINT32 Ext2FsBlockCount; // 4: 块数据
UINT32 Ext2FsRsvdBlockCount; // 8: 已分配块的数据
UINT32 Ext2FsFreeBlockCount; // 12: 空闲块数目
UINT32 Ext2FsFreeInodeCount; // 16: 空闲Inode数据
UINT32 Ext2FsFirstDataBlock; // 20: 第一个数据块
UINT32 Ext2FsLogBlockSize; // 24: 块长度
UINT32 Ext2FsFragmentSize; // 28: 碎片长度
UINT32 Ext2FsBlocksPerGroup; // 32: 每个块组包含的块数
UINT32 Ext2FsFragsPerGroup; // 36: 每个块组包含的碎片
UINT32 Ext2FsInodesPerGroup; // 40: 每个块组的Inode数据
UINT32 Ext2FsMountTime; // 44: 装载时间
UINT32 Ext2FsWriteTime; // 48: 写入时间
UINT16 Ext2FsMountCount; // 52: 装载计数
UINT16 Ext2FsMaxMountCount; // 54: 最大装载计数
UINT16 Ext2FsMagic; // 56: 魔数,标记文件系统类型
UINT16 Ext2FsState; // 58: 文件系统状态
UINT16 Ext2FsBehavior; // 60: 检测到错误时的行为
UINT16 Ext2FsMinorRev; // 62: 副修订号
UINT32 Ext2FsLastFsck; // 64: 上一次检查的时间
UINT32 Ext2FsFsckInterval; // 68: 两次检查允许间隔的最长时间
UINT32 Ext2FsCreator; // 72: 创建文件系统的操作系统
UINT32 Ext2FsRev; // 76: 修订号
UINT16 Ext2FsRsvdUid; // 80: 能够使用保留块的默认UID
UINT16 Ext2FsRsvdGid; // 82: 能够使用保留块的默认GID
//
// 修订号大于等于1的版本才有下述的数据
//
UINT32 Ext2FsFirstInode; // 84: 第一个非保留的Inode
UINT16 Ext2FsInodeSize; // 88: Inode结构的长度
UINT16 Ext2FsBlockGrpNum; // 90: 当前超级块所在的块组编号
UINT32 Ext2FsFeaturesCompat; // 92: 兼容特性集
UINT32 Ext2FsFeaturesIncompat; // 96: 不兼容特性集
UINT32 Ext2FsFeaturesROCompat; // 100: 只读兼容特性集
UINT8 Ext2FsUuid[16]; // 104: 卷的128位UID
CHAR8 Ext2FsVolumeName[16]; // 120: 卷名
CHAR8 Ext2FsFSMnt[64]; // 136: 上一次装载的目录
UINT32 Ext2FsAlgorithm; // 200: 用于压缩
UINT8 Ext2FsPreAlloc; // 204: 试图预分配的块数
UINT8 Ext2FsDirPreAlloc; // 205: 试图为目录预分配的块数
UINT16 Ext2FsRsvdGDBlock; // 206: 为块描述符保留的块
UINT32 Rsvd2[11]; // 208: 保留
UINT16 Rsvd3; // 252: 保留
UINT16 Ext2FsGDSize; // 254: 开启64位模式时组描述符的大小(字节为单位)
UINT32 Rsvd4[192]; // 256: 保留
} EXT2FS;
BIOS下获取到的Block IO
可能是磁盘本身的,也可能是分区的,对于超级块,一般是在分区上的,占据1个物理块或者2个物理块(因为磁盘的物理块可能只有512个字节,而超级块最大可以是1024个字节),所以获取超级块的代码大致如下:
//
// 从磁盘读取超级块,读取方式需要根据硬盘的物理块来确定
// 如果物理块大小是512字节,则指定Buffer大小是1024,即1个超级块的大小,然后读第2个物理块开始的2个物理块
// 如果物理块大小是4096字节,则指定Buffer大小是4096,直接读第1个物理块即可
//
BufferSize = (BlockSize > SBSIZE) ? BlockSize : SBSIZE;
Buffer = AllocatePool (BufferSize);
if (NULL == Buffer) {
Status = EFI_OUT_OF_RESOURCES;
goto DONE;
}
Status = MediaReadBlocks (
Volume->BlockIo,
SBOFF / BlockSize,
BufferSize,
Buffer
);
if (EFI_ERROR (Status)) {
DEBUG ((EFI_D_ERROR, "%a MediaReadBlocks failed. - %r\n", __FUNCTION__, Status));
goto DONE;
}
//
// 读取数据之后需要根据读的Buffer来确定超级块的位置
// 如果读取2个物理块,则Offset是0,读到的数据就是超级块
// 如果读取1个物理块,则这个物理块的offset为1024字节的偏移位置就是超级块的开始
//
SbOffset = (SBOFF < BlockSize) ? SBOFF : 0;
Ext2Fs = (EXT2FS *)(&Buffer[SbOffset]);
获取到超级块数据之后就需要判断它是不是有效的超级块,判断代码:
//
// 超级块首先需要判断魔术字是否正确
//
if (E2FS_MAGIC != Ext2Fs->Ext2FsMagic) {
Status = EFI_NOT_FOUND;
goto DONE;
}
//
// 其次需要判断版本信息以及相关的配套数据是否正确
// 1. 如果EXT2的版本是1,则第一个可用Inode是从Inode 11开始的(注意Inode的Index从1开始计数,而不是0)
// 2. 文件系统中的Inode大小必须是固定的128个字节或者256个字节
// 3. 兼容性判断
//
if ((Ext2Fs->Ext2FsRev > E2FS_REV1) ||
((E2FS_REV1 == Ext2Fs->Ext2FsRev) &&
((EXT2_FIRSTINO != Ext2Fs->Ext2FsFirstInode) ||
((128 != Ext2Fs->Ext2FsInodeSize) && (256 != Ext2Fs->Ext2FsInodeSize)) ||
(Ext2Fs->Ext2FsFeaturesIncompat & ~EXT2F_INCOMPAT_SUPP)))) {
Status = EFI_NOT_FOUND;
goto DONE;
}
组描述符
组描述符紧接在超级块之后,它的结构体说明如下:
起始 | 结束 | 大小 | 描述 |
---|---|---|---|
0 | 3 | 4 | Low 32bits of block address of block usage bitmap. |
4 | 7 | 4 | Low 32bits of block address of inode usage bitmap. |
8 | 11 | 4 | Low 32bits of starting block address of inode table. |
12 | 13 | 2 | Low 16bits of number of unallocated blocks in group. |
14 | 15 | 2 | Low 16bits of number of unallocated inodes in group. |
16 | 17 | 2 | Low 16bits of number of directories in group. |
18 | 19 | 2 | Block group features present. |
20 | 23 | 4 | Low 32-bits of block address of snapshot exclude bitmap. |
24 | 25 | 2 | Low 16-bits of Checksum of the block usage bitmap. |
26 | 27 | 2 | Low 16-bits of Checksum of the inode usage bitmap. |
28 | 29 | 2 | Low 16-bits of amount of free inodes. This allows us to optimize inode searching. |
30 | 31 | 2 | Checksum of the block group, CRC16(UUID+group+desc). |
如果没有开启64位支持,则不用管上述写的Low 32bits
,因为也没有更高位的了;如果开启了64位支持,则还有额外的内容:
起始 | 结束 | 大小 | 描述 |
---|---|---|---|
32 | 35 | 4 | High 32-bits of block address of block usage bitmap. |
36 | 39 | 4 | High 32-bits of block address of inode usage bitmap. |
40 | 43 | 4 | High 32-bits of starting block address of inode table. |
44 | 45 | 2 | High 16-bits of number of unallocated blocks in group. |
46 | 47 | 2 | High 16-bits of number of unallocated inodes in group. |
48 | 49 | 2 | High 16-bits of number of directories in group. |
50 | 51 | 2 | High 16-bits of amount of free inodes. |
52 | 55 | 4 | High 32-bits of block address of snapshot exclude bitmap. |
56 | 57 | 2 | High 16-bits of checksum of the block usage bitmap. |
58 | 59 | 2 | High 16-bits of checksum of the inode usage bitmap. |
60 | 63 | 4 | Reserved as of Linux 5.9rc3. |
不关注64位支持的话对应的代码如下:
//
// EXT2文件系统块描述符
//
typedef struct {
UINT32 Ext2BGDBlockBitmap; // 块位图块所在位置
UINT32 Ext2BGDInodeBitmap; // Inode位图块所在位置
UINT32 Ext2BGDInodeTables; // Inode表块所在位置
UINT16 Ext2BGDFreeBlocks; // 空闲块数据
UINT16 Ext2BGDFreeInodes; // 空闲Inode数据
UINT16 Ext2BGDNumDir; // 目录数据
UINT16 Rsvd; // 保留
UINT32 Rsvd2[5]; // 保留
UINT32 Ext2BGDInodeTablesHi; // 如果支持64位模式,则表示Inode表块所在位置的高32位
UINT32 Rsvd3[5]; // 保留
} EXT2GD;
需要注意块组描述符并不是只有一个,而是一个集合,所以上述的结构体有多个。事实上每个块组中的包含了文件系统中所有块组的组描述符。因此从每个块组都可以确定系统中所有其它块组的下列信息:
- 块和Inode位图的位置
- Inode表的位置
- 空闲块和Inode的数据
组描述符的个数通过如下的函数计算(磁盘大小是200M):
#define HOWMANY(x, y) (((x)+((y)-1))/(y))
FileSystem->Ext2FsNumCylinder =
HOWMANY ((FileSystem->Ext2Fs.Ext2FsBlockCount - FileSystem->Ext2Fs.Ext2FsFirstDataBlock),
FileSystem->Ext2Fs.Ext2FsBlocksPerGroup);
Ext2FsBlockCount
表示的是块个数,因为一个块是1024字节(1KB),所以200M的磁盘对应的就是204800
;Ext2FsFirstDataBlock
表示的是EXT2文件系统的数据,所以最开始就是超级块,它在第一个块,所以这里的值是1
;Ext2FsBlocksPerGroup
表示的是一个块组的块个数,这个是在创建EXT2文件系统的时候就固定下来的,默认是8192
。最终计算的结果是(((x)+((y)-1))/(y)) = (((204800 - 1)+((8192)-1))/(8192)) = 25。从计算方式来看,组描述符的个数主要跟磁盘大小有关。最后还需要注意的是组描述符个数的名称是Ext2FsNumCylinder
,这里涉及到了Cylinder这个普通硬盘才会出现的概念,大概是对于那种通过磁头来寻址的硬盘来说,组描述符的设计有利于更快速的寻址,上述的计算方式也是为了服务于此。
计算一个Inode所在的块组和在该块组中的位置可以用如下的宏:
//
// 计算Inode位置所在块组
// 计算Inode在块组中的Index
//
#define INOTOCG(fs, x) (((x) - 1) / (fs)->Ext2Fs.Ext2FsInodesPerGroup)
#define INODETOFSBO(fs, x) ((((x) - 1) % (fs)->Ext2Fs.Ext2FsInodesPerGroup) % (fs)->Ext2FsInodesPerBlock)
Inode
Inode表的位置在组描述符中指定,从这里可以找到所有的Inode。其结构体描述如下:
起始 | 结束 | 大小 | 描述 |
---|---|---|---|
0 | 1 | 2 | Type and Permissions (see below) |
2 | 3 | 2 | User ID |
4 | 7 | 4 | Lower 32 bits of size in bytes |
8 | 11 | 4 | Last Access Time (in POSIX time) |
12 | 15 | 4 | Creation Time (in POSIX time) |
16 | 19 | 4 | Last Modification time (in POSIX time) |
20 | 23 | 4 | Deletion time (in POSIX time) |
24 | 25 | 2 | Group ID |
26 | 27 | 2 | Count of hard links (directory entries) to this inode. When this reaches 0, the data blocks are marked as unallocated. |
28 | 31 | 4 | Count of disk sectors (not Ext2 blocks) in use by this inode, not counting the actual inode structure nor directory entries linking to the inode. |
32 | 35 | 4 | Flags (see below) |
36 | 39 | 4 | Operating System Specific value #1 |
40 | 43 | 4 | Direct Block Pointer 0 |
44 | 47 | 4 | Direct Block Pointer 1 |
48 | 51 | 4 | Direct Block Pointer 2 |
52 | 55 | 4 | Direct Block Pointer 3 |
56 | 59 | 4 | Direct Block Pointer 4 |
60 | 63 | 4 | Direct Block Pointer 5 |
64 | 67 | 4 | Direct Block Pointer 6 |
68 | 71 | 4 | Direct Block Pointer 7 |
72 | 75 | 4 | Direct Block Pointer 8 |
76 | 79 | 4 | Direct Block Pointer 9 |
80 | 83 | 4 | Direct Block Pointer 10 |
84 | 87 | 4 | Direct Block Pointer 11 |
88 | 91 | 4 | Singly Indirect Block Pointer (Points to a block that is a list of block pointers to data) |
92 | 95 | 4 | Doubly Indirect Block Pointer (Points to a block that is a list of block pointers to Singly Indirect Blocks) |
96 | 99 | 4 | Triply Indirect Block Pointer (Points to a block that is a list of block pointers to Doubly Indirect Blocks) |
100 | 103 | 4 | Generation number (Primarily used for NFS) |
104 | 107 | 4 | In Ext2 version 0, this field is reserved. In version >= 1, Extended attribute block (File ACL). |
108 | 111 | 4 | In Ext2 version 0, this field is reserved. In version >= 1, Upper 32 bits of file size (if feature bit set) if it’s a file, Directory ACL if it’s a directory |
112 | 115 | 4 | Block address of fragment |
116 | 127 | 12 | Operating System Specific Value #2 |
可以看到有一部分成员是根据不同的操作系统存在差异的,对应的代码:
typedef struct {
UINT16 Ext2DInodeMode; // 0: 文件模式,保存了访问权限和文件类型(目录,设备文件等)
UINT16 Ext2DInodeUid; // 2: 所有者UID的低16位
UINT32 Ext2DInodeSize; // 4: 文件长度,以字节为单位
UINT32 Ext2DInodeAcessTime; // 8: 上一次访问文件的时间戳
UINT32 Ext2DInodeCreatTime; // 12: 上一次修改Inode的时间戳
UINT32 Ext2DInodeModificationTime; // 16: 上一次修改文件的时间戳
UINT32 Ext2DInodeDeletionTime; // 20: 文件删除的时间戳
UINT16 Ext2DInodeGid; // 24: 组ID的低16位
UINT16 Ext2DInodeLinkCount; // 26: 指向Inode的硬链接的数目
UINT32 Ext2DInodeBlockCount; // 28: 文件长度,以块为单位
UINT32 Ext2DInodeStatusFlags; // 32: 文件标志
UINT32 Ext2DInodeLinuxRsvd1; // 36: 保留
UINT32 Ext2DInodeBlocks[NDADDR + NIADDR]; // 40: 块指针(块号),指向文件数据块的指针保存在这里,
// 默认情况下数组元素个数是12+3,前12个寻址直接块,
// 后3个用于实现简单、二次和三次间接
UINT32 Ext2DInodeGen; // 100: 文件版本,用户NFS
UINT32 Ext2DInodeFileAcl; // 104: 文件ACL(访问控制表)
UINT32 Ext2DInodeDirAcl; // 108: 目录ACL(访问控制表)
UINT32 Ext2DInodeFragmentAddr; // 112: 碎片地址(未使用)
UINT8 Ext2DInodeFragmentNum; // 116: 碎片编号(未使用)
UINT8 Ext2DInodeFragmentSize; // 117: 锁片长度(未使用)
UINT16 Ext2DInodeLinuxRsvd2; // 118: 保留
UINT16 Ext2DInodeUidHigh; // 120: 所有者UID的高16位
UINT16 Ext2DInodeGidHigh; // 122: 组ID的高16位
UINT32 Ext2DInodeLinuxRsvd3; // 124: 保留
} EXTFS_DINODE;
文件类型的可能值(BIT12 ~ BIT15):
Type value in hex | Type Description |
---|---|
0x1000 | FIFO |
0x2000 | Character device |
0x4000 | Directory |
0x6000 | Block device |
0x8000 | Regular file |
0xA000 | Symbolic link |
0xC000 | Unix socket |
访问权限的可能值(BIT0 ~ BIT11):
Permissionvalue in hex | Permissionvalue in octal | Permission Description |
---|---|---|
0x001 | 00001 | Other—execute permission |
0x002 | 00002 | Other—write permission |
0x004 | 00004 | Other—read permission |
0x008 | 00010 | Group—execute permission |
0x010 | 00020 | Group—write permission |
0x020 | 00040 | Group—read permission |
0x040 | 00100 | User—execute permission |
0x080 | 00200 | User—write permission |
0x100 | 00400 | User—read permission |
0x200 | 01000 | Sticky Bit |
0x400 | 02000 | Set group ID |
0x800 | 04000 | Set user ID |
Inode的大小根据EXT2的版本不同,值也不同,具体的计算方法如下:
//
// 计算INODE结构体的大小,跟EXT2版本有关:
// EXT2版本0中Inode的大小是128个字节;
// EXT2版本1开始,Inode的大小通过超级块中的成员指定;
//
#define EXT2_REV0_DINODE_SIZE sizeof(EXTFS_DINODE)
#define EXT2_DINODE_SIZE(fs) ((fs)->Ext2Fs.Ext2FsRev > E2FS_REV0 ? \
(fs)->Ext2Fs.Ext2FsInodeSize : \
EXT2_REV0_DINODE_SIZE)
读取Inode的流程大致如下:
存储在Inode结构中的文件元数据能够关联到位于磁盘数据块部分的文件内容,二者之间的关联是通过将数据块的地址存储在Inode中建立的。需要注意,数据块并不一定是连续的。
数据块的地址存放在Ext2DInodeBlocks
,不过Ext2DInodeBlocks
只有总计15个32位的数据,而一个数据块的大小可能只有1024个字节,所以如果直接去指定的话,会发现数据显然是不够的。这里使用了直接和间接(间接还有好几级)来寻址数据块的方式,大致如下图所示:
直接寻址只用于小文件;如果文件较大,则会使用间接,此时文件系统在磁盘上分配一个数据块,不存储文件,专门用于存储块号,这样的话,假设一个块大小是1024字节,而32比特表示一个数据块地址,则可以存放256个指针,对应256个数据库块,能够存储的数据大小是256x1024=256K,如果是双重间接则能够存储的数据大小是256x256x1024=65536K,如果是三重间接则能够存储的数据大小是256x256x256x1024=16777216KB=16G,即单个文件最大支持的就是16GB。
直接寻址没有什么介绍的,间接寻址有两种方式,一种是EXT2本身的,一种是EXT4扩展的,这里主要介绍EXT4扩展的。
EXT4扩展版本使用了新的结构体来表示,称为EXT4扩展索引树,此时Inode结构体中的Ext2DInodeBlocks
(60个字节)就不再是一个数组,而表示的是索引树结构体(大小也是60个字节):
#define EXT4_MAX_HEADER_EXTENT_ENTRIES 4
#define EXT4_EXTENT_HEADER_MAGIC 0xF30A
//
// 一个间接块可以表示的数据块,因为数据块寻址地址的大小是UINT32,就是块大小除以UINT32的大小
//
#define NINDIR(fs) ((fs)->Ext2FsBlockSize / sizeof(UINT32))
//
// EXT4扩展版的间接寻址
//
typedef struct {
UINT16 EhMagic; // 魔数0xF30A
UINT16 EhEntries; // 当前节点中有效entry的数目
UINT16 EhMax; // 当前节点中entry的最大数目
UINT16 EhDepth; // 当前节点在树中的深度
UINT32 EhGen; // 索引树版本
} EXT4_EXTENT_HEADER;
typedef struct {
UINT32 EiBlk; // 当前节点块的索引
UINT32 EiLeafLo; // 物理块指针低位
UINT16 EiLeafHi; // 物理块指针高位
UINT16 EiUnused;
} EXT4_EXTENT_INDEX;
typedef struct {
UINT32 Eblk; // 当前节点的第一个块位置
UINT16 Elen; // 块数目
UINT16 EstartHi; // 物理块指针高位
UINT32 EstartLo; // 物理块指针低位
} EXT4_EXTENT;
//
// 跟Inode中的Ext2DInodeBlocks大小一样,都是64个字节
//
typedef struct {
EXT4_EXTENT_HEADER Eheader;
union {
EXT4_EXTENT_INDEX Eindex[EXT4_MAX_HEADER_EXTENT_ENTRIES];
EXT4_EXTENT Extent[EXT4_MAX_HEADER_EXTENT_ENTRIES];
} Enodes;
} EXT4_EXTENT_TABLE;
整个索引的过程如下图所示(图片来自《Addition of Ext4 Extent and Ext3 HTree DIR Read-Only Support in NetBSD》):
索引树的循环代码大致如下:
while (Etable->Eheader.EhDepth > 0) {
ExtIndex = NULL;
for (Index=1; Index < Etable->Eheader.EhEntries; Index++) {
ExtIndex = &(Etable->Enodes.Eindex[Index]);
if (((UINT32) FileBlock) < ExtIndex->EiBlk) {
ExtIndex = &(Etable->Enodes.Eindex[Index-1]);
break;
}
ExtIndex = NULL;
}
if (NULL != ExtIndex) {
//
// 目前还不支持48位
//
ASSERT (ExtIndex->EiLeafHi == 0);
NextLevelNode = ExtIndex->EiLeafLo;
//
// 继续深入读取下一个间接块
//
Status = MediaReadBlocks (
File->BlockIo,
FSBTODB (Fp->FsPtr, (DADDRESS) NextLevelNode),
FileSystem->Ext2FsBlockSize,
Buf
);
if (EFI_ERROR (Status)) {
DEBUG ((EFI_D_ERROR, "%a MediaReadBlocks failed. - %r\n", __FUNCTION__, Status));
return Status;
}
Etable = (EXT4_EXTENT_TABLE*) Buf;
if (Etable->Eheader.EhMagic != EXT4_EXTENT_HEADER_MAGIC) {
DEBUG ((DEBUG_ERROR, "EXT4 extent header magic mismatch 0x%X!\n", Etable->Eheader.EhMagic));
return EFI_DEVICE_ERROR;
}
} else {
DEBUG ((DEBUG_ERROR, "Could not find FileBlock #%d in the index extent data!\n", FileBlock));
return EFI_NO_MAPPING;
}
}
文件读取
一个文件对应一个Inode,Inode会对应到数据,但是Inode跟数据的对应根据前面的介绍可以看出有直接和间接两种方式,所以为了读取整个文件,需要先将文件切成块的大小,然后每个块对应一个文件索引,通过这个文件块索引结合Inode里面的寻址数据,最终找到对应的数据块,然后读取整个数据块。如此循环,直到将整个文件都读取出来。
代码示例
对应的代码实现已经上传https://gitee.com/jiangwei0512/edk2-beni,命令执行结果: