Inode

概述

参考:

Index node(索引节点,简称 inode) 是 Unix 风格的文件系统中的一种数据结构。每个索引节点保存了文件系统中的一个文件系统对象(i.e.文件、目录等)的元信息数据,但不包括数据内容或者文件名。

注:数据的内容存放在硬盘的一个 block(区域块) 中,通过 inode(索引节点) 来访问 block,每个索引节点都会命名一个文件名。文件的索引节点通过 ls -i 命令查看

~]# ls -i /
     12 bin         1 dev  6029313 home       14 lib32       16 libx32      2097153 media  1572865 opt   1048577 root       17 sbin  1966081 srv             1 sys  4325377 usr
      2 boot  1835009 etc       13 lib        15 lib64       11 lost+found  4063233 mnt          1 proc        1 run   4980737 snap       18 swap.img  3801089 tmp  1179649 var

所以,linux 里的所有文件,都相当于一个 Hard link(硬连接),链接到 inode 号上,展现在屏幕上的只是该文件内容的文件名。就算几个文件名字不一样的文件只要节点号相同,那么这几个文件的内容是就是相同的。想查看文件内容,就要找到该文件名对应的 inode 然后通过 inode 找到 block,找到 block 就能看到其中的内容了。

实际例子:我家里的内容有什么就是 block,我家的门牌号就是 inode,我家叫什么名字,比如叫“DesistDaydream 的家“这几个字就是文件名,通过文件名找到门牌号,找到了门牌号才能开门看到我家中的内容。(能不能开门就是权限决定的了)

Inode 的计算

不同文件系统,有不同的计算方式,详情可以参考各类文件系统中的章节

总得来说,所以我们可以通过降低 BytesPerInode 的值以提高 Inode 数量

索引区和数据区

文件系统怎样从文件名索引到存放文件内容的 Block?

解答这个问题,先了解下 inode。

linux 文件系统在磁盘分区格式化后可以简单认为会划分两个区域

  • 一个是索引区(inode 区),用来存放文件的 inode(inode 包含文件的各种属性,统称为元数据),索引区存放的 inode 个数是有上限的,在格式化的时候会分配好
  • 另一个是数据区,存放文件数据,即文件里面的真实内容。

如果文件系统太大,将所有的 inode 与 block 放在一起很难管理,因此 Ext2 文件系统在格式化的时候基本上是区分为多个 block group(块组),每个块组都有独立的 inode/block/super block 系统。

inode 的大小和 block 的大小可以使用命令来看,inode 的大小一般为 256 Bytes,block 的大小一般为 4096 Bytes。

export DEVICE="/dev/vdb"
dumpe2fs ${DEVICE} | egrep -i "Block size|Inode size"

inode 会有一个总量,如果 inode 数耗尽了(小文件很多),磁盘就使用不了了。inode 的总量和使用量可以用 df -i 命令查看

image.png

/dev/sdb 中可以看到 Inodes 的具体总数,使用情况和可用个数

inode 总数有一个简单的计算公式,如下所示。假设 bytes-per-inode 为 1024,一个 1GB 的磁盘,inode 的总数可能有 1024 * 1024 个,如果 inode 的大小为 256 字节,那么索引区的大小会达到 256M。

inode_count = 磁盘大小 / bytes-per-inode

其中 bytes-per-inode 在格式化的时候可以指定,例如

mkfs.ext4 -i bytes-per-inode /dev/sda2

不管是什么文件类型(包括目录),都会给它分配一个 inode,每个 inode 都有一个唯一的编号,文件系统就用 inode 号码来识别不同的文件。inode 结构体包含文件的元信息,简单来说有以下内容:文件大小、所属组、读写执行权限、时间戳、链接数(硬链接),文件数据 block 的地址

inode 里面存有文件数据 block 的地址,所以要想获取文件的数据,必须要得到对应 inode。但是 inode 里面并没有保存文件名,文件名和 inode 号是存在于上一级目录的内容里面,所以得获取到目录的内容。目录的内容也至少占用一个 block,可以做一个实验来证明。分配一个 100M 的磁盘,然后格式化成 ext4 文件系统,block size 设置为 4K,并挂载。

mkfs.ext4 -b 4096 /dev/sdk mount /dev/sdk /mnt/test4

在往 /mnt/test3 目录下创建 20480 个目录前后,分别统计下 df 命令输出的结果如下

image.png

创建 20480 个目录前后,blocks 已用的个数对比

可以看到已用的 blocks 个数 82444/4 = 20611 个,这里面有 20480 个是存的是 20480 个目录的内容。

源码解析

inode 中判断是文件还是目录

在 linux 中一切皆文件,目录也是文件。虽然都是文件,但类型不同,可以通过 inode 结构体中的 umode*t i_mode 成员来判断是目录文件,还是普通文件。

typedef unsigned short umode_t

umode_t 是 16 位的 2 进制数,保存的就是文件类型及用户权限信息

  • 第 0-3 位—文件类型位
  • 第 4 位 – suid 位
  • 第 5 位 – sgid 位
  • 第 6 位 – sticky 位
  • 第 7-9 位 – 文件所属主权限位
  • 第 10-12 位 – 文件所属组权限位
  • 第 13-15 位 – 其他用户权限位

1,文件类型位

用来判断文件类型

#define         S_IFMT  0170000 /* type of file ,文件类型掩码*/
#define         S_IFREG 0100000 /* regular 普通文件*/
#define         S_IFBLK 0060000 /* block special 块设备文件*/
#define         S_IFDIR 0040000 /* directory 目录文件*/
#define         S_IFCHR 0020000 /* character special 字符设备文件*/
......

S_IFMT 是文件类型掩码,其值是 0170000,转换成二进制就是 1111 0000 0000 0000,

S_IFMT 就是用来取 mode 的 0–3 位

所以判断文件还是目录的方法就出来了,如下。ceph 文件系统的用户态客户端中 struc Inode 结构体中 uint32_t mode 成员来判断是目录还是文件:is_dir()函数判断是否是目录

#define         S_ISDIR(m)      (((m) & S_IFMT) == S_IFDIR)    // 判断是否是目录文件
#define         S_ISREG(m)      (((m) & S_IFMT) == S_IFREG)    // 判断是否是普通文件

2,SUID,SGID,sticky 位

SUID 是 Set User ID, SGID 是 Set Group ID 的意思。具体怎么用,暂时不研究

3,文件权限

文件权限分为属主权限、属主组权限和其他用户权限,即我们所知的 rwxrwxrwx(777)之类

目录项:ext4_dir_entry

普通文件的 inode 指向的数据块存的是它自己的数据的,而目录的 inode 指向的数据块存的是位于该目录下的目录项的,即目录下的子目录和文件的目录项这里注意此目录项并不是 dentry,dentry 是 vfs 层的,只存在于内存中,而各个实际的文件系统都有自己的目录项,这都是存在磁盘上的,比如 ext4 文件系统的目录项是ext4_dir_entry,里面有 inode 号,文件名。

struct ext4_dir_entry {
 __le32 inode;   /* Inode number */
 __le16 rec_len;  /* Directory entry length */
 __le16 name_len;  /* Name length */
 char name[EXT4_NAME_LEN]; /* File name */
};

根据 inode 号,再找到存于索引区的 inode 数据。

inode 里面不会存在文件名,文件名存在上一级目录的 block 里面,删除文件实际上只删除了上一级目录中该文件名的记录,即对应的 ext4*dir_entry 目录项。如果文件在使用的情况下(被服务占用),删除文件,仅仅是删除了文件名,空间是没有释放掉(之前遇到过,ceph-mon 产生了很大的日志文件,将日志文件删掉后,发现空间仍然没有释放),可以通过命令

lsof | grep deleted

可以看到被占用的文件.

这里注意:vfs 和各个实际文件系统都有 inode,vfs 的是 struct inode,这部分数据只存在于内存中,而具体存于磁盘上的是具体文件系统的 inode,比如 ext4 文件系统中是 struct ext4*inode,ceph 文件系统中是 struct ceph_inode_info

根据文件名索引到文件内容

表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:首先,系统找到这个文件名对应的 inode 号码;其次,通过 inode 号码,获取 inode 信息;最后,根据 inode 信息,找到文件数据所在的 block,读出数据。

现在举一个具体的例子,来说明文件是怎么读取到的,比如读取/home/bzw/test 里的内容,目录结构如下图

image.png image.png image.png

假设文件系统的的简单分区如下

image.png

  • 获取 home 对应的 inode 号:先找根目录’/‘的 inode(不考虑缓存):根目录的 inode 号可以从 super_block 中获取,ext4 文件系统的根目录 inode 号为 2(xfs 文件系统根目录 inode 号是 64,ceph 文件系统根目录 inode 号是 1),所以在索引区读取 inode 号为 2 存的 inode 内容。假如 inode 中存的 block 地址是_1000_,那么去数据区读取地址为_1000_的 block 存的内容,内容如下图所示。

image.png

地址为 1000 的 block 存的内容

地址为 1000 的 block 里面存了 20 个目录项(struct ext4*dir_entry),可以找到***目录 home 对应的 inode 号为 100_**。

  • 获取 bzw 对应的 inode 号:上一步获取到了目录 home 的 inode 号,在索引区读取 inode 号为_100_存的 inode 内容。假如 inode 中存的 block 地址为_2000_,那么去读地址为_2000_的 block 存的内容,内容如下图所示。

image.png

地址为 2000 的 block 存的内容

地址为 2000 的 block 里面存了 3 个目录项(struct ext4*dir_entry),可以找到***目录 bzw 对应的 inode 号为 200_**。

  • 获取 test 对应的 inode 号:上一步获取到了目录 bzw 的 inode 号,在索引区读取 inode 号为_200_存的 inode 内容。假如 inode 中存的 block 地址为_3000_,那么去读地址为_3000_的 block 存的内容,内容如下图所示。

image.png

地址为 3000 的 block 存的内容

地址为 3000 的 block 里面存了 2 个目录项(struct ext4*dir_entry),可以找到***文件 test 对应的 inode 号为 300_**。

  • 获取 test 对应的内容:上一步获取到了文件 test 的 inode 号,在索引区读取 inode 号为_300_存的 inode 内容。假如 inode 中存的 block 地址为_4000_,那么去读地址为_4000_的 block 存的内容。这个时候就完成了操作。

这里注意如果 test 内容很大,那么在 inode 里面存的 block 地址就不止一个了。

可以以 ext4 中的 struct ext4_inode 为例

struct ext4_inode {
 __le16 i_mode;  /* File mode */
 __le16 i_uid;  /* Low 16 bits of Owner Uid */
 __le32 i_size_lo; /* Size in bytes */
 __le32 i_atime; /* Access time */
 __le32 i_ctime; /* Inode Change time */
 __le32 i_mtime; /* Modification time */
 __le32 i_dtime; /* Deletion Time */
 __le16 i_gid;  /* Low 16 bits of Group Id */
 __le16 i_links_count; /* Links count */
 __le32 i_blocks_lo; /* Blocks count */
 __le32 i_flags; /* File flags */
        ......
 __le32 i_block[EXT4_N_BLOCKS];/* Pointers to blocks */ 这里面存的就是block的地址
 __le32 i_generation; /* File version (for NFS) */
 __le32 i_file_acl_lo; /* File ACL */
 __le32 i_size_high;
 __le32 i_obso_faddr; /* Obsoleted fragment address */
 ......
 __le16 i_extra_isize;
 __le16 i_checksum_hi; /* crc32c(uuid+inum+inode) BE */
 __le32  i_ctime_extra;  /* extra Change time      (nsec << 2 | epoch) */
 __le32  i_mtime_extra;  /* extra Modification time(nsec << 2 | epoch) */
 __le32  i_atime_extra;  /* extra Access time      (nsec << 2 | epoch) */
 __le32  i_crtime;       /* File Creation time */
 __le32  i_crtime_extra; /* extra FileCreationtime (nsec << 2 | epoch) */
 __le32  i_version_hi; /* high 32 bits for 64-bit version */
 __le32 i_projid; /* Project ID */
};

inode_hashtable

文件系统中的位于内存中的所有 inode 存放在一个名为 inode_hashtable 的全局哈希表中(如果 inode 还在磁盘,尚未读入内存中,则不会加入到全局哈希表中)。另一方面,所有的 inode 还存放在超级块中的 s_inode 链表中。

static struct hlist_head *inode_hashtable __read_mostly;

在上面进行文件索引时,并没有讲根目录 inode 号为 2 是怎么获取的,这将下一章中讲解。