文件在磁盘中的开销

新建一个空文件占用多少磁盘空间

linux下touch一个空文件:

1
touch empyt_file

进行该操作,是否要消耗磁盘空间?需要的话,大概能消耗多少?

ls这个命令可以查看文件大小:

1
2
3
4
$ touch empty_file
$ ls -l
total 0
-rw-r--r-- 1 dongshifu dongshifu 0 Aug 17 17:49 empty_file

ls命令表示这个空文件占用的是0。文件的大小确实是0,因为还没有为该文件写入任何内容。但是现在要思考的是,一个空文件是否占用磁盘空间。所以直觉告诉这绝对不可能,磁盘上多出来一个文件,怎么可能一点空间开销都没有!

为了解开这个谜底,还需要借助df命令。输入df –i

1
2
3
4
# df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
......
/dev/sdb1 2147361984 12785019 2134576965 1% /search

该命令输出展示了文件系统中inode的使用情况。注意IUsed是12785019。继续新建一个空文件:

1
2
3
4
5
# touch empty_file2
df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
......
/dev/sdb1 2147361984 12785020 2134576964 1% /search

这下注意IUsed变成了12785020。

得出结论:新建一个空文件会占用一个Inode。

Inode

那么inode里都存了哪些和文件相关的信息呢?稍微看一下内核的源代码。以ext2文件系统为例,在linux-2.6里的文件fs/ext2/ext2.h中,可以找到内核对于inode结构体的定义。该结构体较为复杂,主要存储除了文件内容以外的一些其他数据,选一些比较关键的截取出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ext2_inode {
__le16 i_mode; // 文件权限
__le16 i_uid; // 文件所有者ID
__le32 i_size; // 文件字节数大小
__le32 i_atime; // 文件上次被访问的时间
__le32 i_ctime; // 文件创建时间
__le32 i_mtime; // 文件被修改的时间
__le32 i_dtime; // 文件被删除的时间
__le16 i_gid; // 文件所属组ID
__le16 i_links_count; // 此文件的inode被连接的次数
__le32 i_blocks; // 文件的block数量
...
__le32 i_block[EXT2_N_BLOCKS]; // 指向存储文件数据的块的数组
...

可以看到和文件相关的所属用户、访问时间等都是存在inode中的。使用stat命令就可以直接看到文件inode中数据。

每个inode到底是多大呢?dumpe2fs可以查看(XFS的话使用xfs_info)。

1
2
3
4
# dumpe2fs -h /dev/mapper/vgroot-lvroot
dumpe2fs 1.41.12 (17-May-2010)
......
Inode size: 256

Inode size表示每个Inode的大小。一般,每个inode都是256字节。两个inode的大小正好对齐到磁盘扇区的512字节。

文件名保存地址

Inode结构体都看完了,搞了半天不知道有没有发现一个问题,inode里并没有存储文件名!!那么,文件名到底跑哪儿去了?

fs/ext2/ext2.h中,可以找到如下文件夹相关的结构体

1
2
3
4
5
6
struct ext2_dir_entry {
__le32 inode; /* Inode number */
__le16 rec_len; /* Directory entry length */
__le16 name_len; /* Name length */
char name[]; /* File name, up to EXT2_NAME_LEN */
};

该结构体就是文件夹所使用的数据结构。没错,文件名是存在其所属的文件夹中的,就是其中的char name[]字段。和文件名一起,文件夹里还记录了该文件的inode号等信息。

一个字节的文件实际占用多少磁盘空间

假如给文件里只写入1个字节,那么这个文件实际的磁盘占用多大,难道真的是1个字节吗?

实际操作:

1
2
3
4
5
6
7
# mkdir tempDir
# cd tempDir
# du -h
0 .
# touch test
# du -h
0

在一个目录中创建了一个空的文件以后,通过du命令看到的该文件夹的占用空间并没有发生变化。这符合之前的认识,因为空文件只占用inode。接着修改文件,添加一个字母:

1
2
3
echo "a" > test
# du -h
4.0K

保存后再次查看该目录的空间占用。发现由原来的0增加到了4K。 所以说,文件里的内容不论多小,哪怕是一个字节,其实操作系统也会分配4K的。哦,当然了还得再算前文中说到的inode和文件夹数据结构中存储的文件名等所用的空间。 所以,不要在系统里维护一大堆的碎文件。文件再小,占用磁盘其实一点都不少!

4K占用的底层原理

再把linux源代码文件fs/ext2/ext2.h里关于inode的定义翻出来,找到结构体中定义的指向数据节点用的block数组:

1
2
3
4
struct ext2_inode {
......
__le32 i_block[EXT2_N_BLOCKS]; // 指向存储文件数据的块的数组
......

当文件没有数据需要存储的时候,这个数组都是空值。而当写入了1个字节以后,文件系统就需要申请block去存储了,申请完后,指针放在这个数组里。哪怕文件内容只有一个字节,仍然会分配一个整的Block,因为这是文件系统的最小工作单位。那么这个block大小是多大呢,ext下可以通过dumpe2fs查看。

1
2
3
#dumpe2fs -h /dev/mapper/vgroot-lvroot
......
Block size: 4096

一般情况,一个Block是4KB。

大文件如何存储

inode中定义的block数组大小呢,只有EXT2_N_BLOCKS个。再查看一下这个常量的定义,发现它是15,相关内核中定义如下:

1
2
3
4
5
#define EXT2_NDIR_BLOCKS        12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)

就按4K的block size来看,15个block只够存的下154=60K的文件。 这个文件大小肯定不是那么简单,存一个mp4大片都得上G了。那*Linux是怎么实现大文件存储的呢?其实上面宏的定义过程已经告知了一切,就是只有12个数组直接存block指针,其余的用来做间接索引(EXT2_IND_BLOCK),二级间接索引(EXT2_DIND_BLOCK)和三级索引(EXT2_TIND_BLOCK)。

inode中的直接与间接索引

这样,一个文件可以使用的空间就指数倍的扩展了。 文件小的时候,都用直接索引,磁盘IO少,性能好。文件大的时候,访问一个block可能得先进行三次的IO,性能略慢,不过有OS层面的页缓存、目录项缓存的加持,也还好

小结

文件系统是按照inode+block来组织的,所以不管文件多小,哪怕只有一个字节,在数据上都会消耗掉整整一个块(当然还得算上inode等开销)。这个块大小可以通过dumpe2fs等命令来查看。如果想改变这个块大小怎么办?对不起,只能重新格式化。

再扯的远一点,所有的文件系统理念都是按照块来分配的,包括分布式文件系统,例如HDFS。由于HDFS应用场景是各种GB、TB甚至是PB级别的数据处理。所以为了降低block的管理成本,它的block size设置的非常大。在比较新的版本里,一个block直接就是128M,没看错,单位是M。

参考

https://mp.weixin.qq.com/s/WE6BodR_q0GSKks_TgYL1w