当我们在 Linux 或 Unix 系统上使用 ls -l
命令列出目录的内容时,第一行通常会显示一个 total
后跟一个数字。这个数字表示当前目录下所有文件和子目录使用的总块数。
majn@tiger:~/C_Project$ ll -lh
total 100K
drwxrwxr-x 18 majn majn 4.0K 9月 28 11:58 ./
drwxr-x--- 29 majn csser 4.0K 9月 28 13:59 ../
-rwxrwxr-x 1 majn majn 1.5K 8月 10 12:26 a.out*
......
具体来说:
-
这里的“块”是文件系统存储的基本单位。不同的文件系统和不同的系统配置可能会有不同的块大小,但在很多文件系统上,块的大小是 512 字节、4096 字节或其他大小。
-
total
后面显示的数字是所有列出的文件和目录占用的块数的总和。它考虑了文件系统中的各种因素,如间接块和元数据。 -
ls
命令显示的这个值实际上是st_blocks
字段的值,它是stat
结构体中的一个字段。这个字段表示的是文件大小按照块大小(通常是 512 字节)进行四舍五入后的结果。
要注意的是,这个数字不仅仅是简单地将目录下所有文件的大小加起来,因为它还包括了其他文件系统的开销。
例如,如果有两个大小为 100 字节的文件,它们可能各自占用一个 4096 字节的块(取决于文件系统和配置)。因此,ls
显示的 total
可能远大于这两个文件的实际总大小。
stat 结构体
在 UNIX-like 系统中,stat
结构体提供了关于文件的信息。这些信息大多是文件的元数据,如文件大小、权限、所有者、时间戳等。可以使用 stat
系统调用来获取文件的这些信息,并将结果存储在 stat
结构体中。
以下是 stat
结构体的一般形式(取决于特定的平台和版本,字段可能会有所不同):
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for filesystem I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};
下面是这些字段的简要描述:
- st_dev: 文件所在设备的 ID。
- st_ino: 文件的 inode 号。每个文件在其所在的文件系统中都有一个唯一的 inode 号。
- st_mode: 文件的权限和类型。
- st_nlink: 文件的硬链接数量。
- st_uid: 文件所有者的用户 ID。
- st_gid: 文件所属组的组 ID。
- st_rdev: 如果文件是一个设备文件,则表示设备的 ID。
- st_size: 文件的大小(以字节为单位)。
- st_blksize: 为了优化文件系统 I/O 而设置的块大小。
- st_blocks: 文件所占用的 512 字节大小的块的数量。
- st_atime: 文件最后被访问的时间。
- st_mtime: 文件内容最后被修改的时间。
- st_ctime: 文件状态(如权限或链接数)最后被更改的时间。
在某些系统上,时间戳可能会有更高的精度,例如 st_atim
, st_mtim
, st_ctim
,它们可能是 timespec
结构体类型,提供了纳秒级的精度。
要获取文件的 stat
信息,可以使用 stat()
函数或其变体(如 fstat()
或 lstat()
)。例如:
struct stat sb;
if (stat("/path/to/file", &sb) == -1) {
perror("stat");
exit(EXIT_FAILURE);
}
上述代码将 “/path/to/file” 的 stat
信息存储在 sb
结构体中。
stat结构体与inode的关系
stat
结构体和 inode 之间的关系非常紧密。简而言之,stat
结构体为程序提供了对 inode 中存储的信息的访问。先来看看 inode 的定义以便更好地理解它们之间的关系。
inode
在 UNIX 和 UNIX-like 系统中,每个文件都有一个与之关联的 inode(索引节点)。inode 包含关于文件的元数据,例如:
- 文件类型(普通文件、目录、符号链接等)
- 权限(读、写、执行等)
- 所有者和组
- 文件大小
- 时间戳(访问时间、修改时间、状态更改时间)
- 文件数据的物理位置信息(指向文件数据块的指针)
- 硬链接数
- 其他属性
每个 inode 在文件系统中都有一个唯一的编号,称为 inode 号。
stat 结构体
当使用 stat()
, fstat()
, 或 lstat()
系统调用时,它们会填充一个 stat
结构体,该结构体包含与指定文件相关的信息。这些信息实际上是从文件的 inode 中取得的。也就是说,stat
结构体提供了一个方便的方式来在程序中访问 inode 的信息。
关系
所以,可以说 stat
结构体是 inode 信息的一个映射或表示,它使得应用程序能够轻松访问 inode 中的数据,而不必直接与底层文件系统交互。每个 stat
结构体字段大多都对应于 inode 中的一个或多个属性。
例如,stat
结构体中的 st_mode
字段表示文件的权限和类型,这些都存储在 inode 中。同样,st_ino
字段表示文件的 inode 号,st_uid
和 st_gid
字段分别表示文件所有者的用户 ID 和组 ID,这些都是直接从 inode 中取得的。
总之,stat
结构体为程序提供了一个接口,使其能够访问和操作 inode 中的信息。
在大多数常见的文件系统中,一个块(或扇区)通常只能由一个文件占用。也就是说,如果一个文件只占用块的一部分,那么这个块的剩余部分通常是浪费的,并不能被另一个文件使用。
举个例子,假设文件系统的块大小为4KB:
- 如果我们有一个只有1KB大小的文件,它会占用整个4KB的块,剩下的3KB是浪费的。
- 如果我们有另一个2KB大小的文件,它不会使用前一个文件未使用的那3KB,而是会占用另一个新的4KB的块,其中2KB未使用。
这样的设计简化了文件系统的设计和数据的读取,因为每个块的地址指向一个确定的文件。
然而,需要注意的是有一种称为"尾部合并"或"块共享"的优化技术,可以允许两个文件共享一个块,但这通常在特定的情境下,如某些复制写入 (Copy-On-Write, COW) 文件系统中可能会看到这种情况。
总体而言,尽管技术上可能有办法让多个文件共享一个块,但在大多数传统的文件系统中,这是不允许的,因为它会增加管理复杂性并可能导致性能下降。
Tail Merging & Copy-On-Write
尾部合并或块共享是某些文件系统中使用的技术,尤其是在那些支持复制写入 (Copy-On-Write, COW) 机制的文件系统中。这种技术旨在优化存储使用和提高空间效率。
尾部合并 (Tail Merging)
在许多文件系统中,由于块的大小固定,小文件和文件的尾部数据往往不会完全填充一个完整的块。在不使用尾部合并的传统文件系统中,这会导致存储浪费。
尾部合并的思想是将多个文件的这些"尾部"数据合并到单个存储块中,从而减少因未完全利用的块而造成的空间浪费。
复制写入 (Copy-On-Write, COW)
COW 是一种优化技术,在写入数据之前,不是直接修改原始数据,而是复制原始数据,并在副本上执行修改。这种方法的优点是它提供了一种原子性的写入方式,可以在系统崩溃时保护数据的完整性,并为高效的快照和其他功能提供基础。
尾部合并与 COW 的关系
在某些使用 COW 机制的文件系统中,尾部合并可以很自然地实现。当文件进行修改时,而不是修改原始块,系统可以简单地将新数据和原始块的尾部数据合并到新块中。由于不直接修改原始数据,这种合并操作变得相对简单和高效。
例如,在 Btrfs 这种 COW 文件系统中,当多个小文件或文件尾部数据被写入到同一个数据块中时,可以实现尾部合并。这不仅优化了存储,还减少了因多次小写入操作而导致的性能开销。
总结
尽管尾部合并和 COW 可以提供存储和性能上的优势,但它们也带来了额外的复杂性。例如,当执行文件删除或修改操作时,文件系统必须跟踪和管理块中的哪一部分属于哪个文件,并确保数据的完整性和一致性。但是,在权衡后,这些技术在很多现代文件系统中仍被认为是有价值的,并为提高存储效率和性能提供了有效的手段。
示例
下面,我们通过一个简单的例子来解释尾部合并和 COW 如何工作。
假设我们有一个文件系统,其块大小为4KB。
场景 1: 不使用尾部合并
- 用户创建了三个小文件,分别为 A.txt (1KB),B.txt (2KB) 和 C.txt (1KB)。
- 每个文件都会占用一个4KB的块。尽管 A.txt 和 C.txt 只用了块中的1KB空间,但剩余的3KB是不能用于其他文件的。
- 结果是,我们用了12KB的空间来存储4KB的数据,有8KB的空间被浪费。
场景 2: 使用尾部合并
- 用户再次创建了 A.txt, B.txt 和 C.txt。
- 文件系统看到这些文件都很小,所以它把它们的数据都放入一个单独的4KB块中。
- A.txt 的1KB数据,B.txt 的2KB数据和 C.txt 的1KB数据都存储在同一个块中,完美地填满了它。
- 结果是,我们只用了4KB的空间来存储4KB的数据,没有空间浪费。
场景 3: 使用 COW
- 现在假设用户想修改 B.txt,添加1KB的数据。
- 在一个常规文件系统中,通常直接在B.txt的块中添加数据。但在使用COW的文件系统中,B.txt的原始数据被复制到一个新块,然后再在新块中进行修改。
- 原始的 B.txt 块保持不变,直到操作成功完成并且新的块准备好替换它为止。这确保了在出现问题(例如电源故障)时数据的一致性和完整性。
- 一旦修改完成,文件系统的元数据会更新为指向新块,并可能在某个时刻释放原始块以供将来使用。
结合尾部合并和 COW,一个文件系统可以在添加、删除或修改小文件时,有效地利用和管理存储空间,同时确保数据的安全性和完整性。