深入剖析Linux文件系统之ext2路径名查找(超详细~)
补给站Linux内核
2022年06月30日 16:43

本文以ext2文件系统为例来剖析一个真实的文件系统如何查找文件,这对于深入理解文件系统至关重要。

1.准备文件系统镜像

所用工具:dd、mkfs.ext2、hexdump、dumpe2fs、mount等工具

1)制作100k大小镜像文件

代码块
JavaScript
自动换行
复制代码
$ dd if=/dev/zero of=ext2.img bs=1k count=100
记录了100+0 的读入
记录了100+0 的写出
102400 bytes (102 kB, 100 KiB) copied, 0.00125457 s, 81.6 MB/s
复制成功

2)格式化为ext2文件系统格式

代码块
JavaScript
自动换行
复制代码
$ mkfs.ext2 ext2.img
mke2fs 1.44.1 (24-Mar-2018)
丢弃设备块: 完成                            
创建含有 100 个块(每块 1k)和 16 个inode的文件系统

正在分配组表: 完成                            
正在写入inode表: 完成                            
写入超级块和文件系统账户统计信息: 已完成
复制成功

3)查看文件系统信息

代码块
JavaScript
自动换行
复制代码
$ dumpe2fs ext2.img 
dumpe2fs 1.44.1 (24-Mar-2018)
Filesystem volume name:   <none>
Last mounted on:          <not available>
Filesystem UUID:          3680e1d5-7f58-4324-9cbd-c7d382f0c3df
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:              16
Block count:              100
Reserved block count:     5
Free blocks:              79
Free inodes:              5
First block:              1
Block size:               1024
Fragment size:            1024
Blocks per group:         8192
Fragments per group:      8192
Inodes per group:         16
Inode blocks per group:   2
Filesystem created:       Wed May 26 15:23:33 2021
Last mount time:          n/a
Last write time:          Wed May 26 15:23:33 2021
Mount count:              0
Maximum mount count:      -1
Last checked:             Wed May 26 15:23:33 2021
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:      5b0daa29-c2a0-4ab1-b09e-50992d3b070d


组 0:(块 1-99)
  主 超级块位于 1,组描述符位于 2-2
  块位图位于 3 (+2)
  Inode 位图位于 4 (+3)
  Inode表位于 5-6 (+4)
  79 个可用 块,5 个可用inode,2 个目录 
  可用块数: 21-99
  可用inode数: 12-16
复制成功
  • 这实际是是读取文件系统的超级块和块组描述符信息。我们可以看的创建的文件系统的总体信息:

  • Filesystem magic number:0xEF53 表示为ext2文件系统Inode count: 16 表示文件系统inode个数为16Block count: 100 表示文件系统块个数为100 Free blocks: 79 表示文件系统空闲块个数为79Free inodes: 5 表示文件系统空闲inode个数为5 First block: 1 第一个数据块编号为1(编号0保留为引导块) Block size: 1024 文件系统块大小为1kBlocks per group: 8192 每个块组8192个块Inodes per group: 16 每个块组个inode Inode blocks per group: 2 每个块组2个inode块 First inode: 11 分配的第一个inode号为11(除根inode外,根inode号为2)Inode size: 128 inode大小为128字节块组的信息(这里只有一个块组) 1 - 99号 超级块块编号为 1 块组描述符块编号为 2 块位图块编号为 3 inode位图块编号为 4

  • inode表位于5和6块

  • 79 个可用 块,5 个可用inode,2 个目录 (一个为根目录一个为lost+found,存放坏块) 可用块数:21-99 可用inode数:12-16

【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!(含视频教程、电子书、实战项目及代码)     

4)挂载文件系统并创建文件

代码块
JavaScript
自动换行
复制代码
创建一个挂载点目录:
$  mkdir root_dir

挂载:
$  sudo mount -t ext2 ext2.img  root_dir

查看文件:
$ ls -la
总用量 17
drwxr-xr-x 3 root  root   1024 5月  26 15:23 .
drwxrwxr-x 3 hanch hanch  4096 5月  26 15:28 ..
drwx------ 2 root  root  12288 5月  26 15:23 lost+found
复制成功

可以发现有三个目录:

代码块
JavaScript
自动换行
复制代码
.
 ..
 lost+found
复制成功
  • 实际上是根目录的数据块的内容(包含各个目录项)。

  • 下面我们来创建一个目录,目录下创建文件:

代码块
JavaScript
自动换行
复制代码
$ sudo mkdir dir

$ cd dir/
$ su
# echo hello > test.txt
复制成功

现在目录树是这样的:

代码块
JavaScript
自动换行
复制代码
$ tree
.
├── dir
│   └── test.txt
└── lost+found [error opening dir]

2 directories, 1 file
复制成功
  • 后面我们会通过解析文件系统镜像来观察如何查找 /dir/test.txt 文件的

  • 现在关注一下相关的索引节点:

代码块
JavaScript
自动换行
复制代码
$ cd  dir

$ ls -lai
总用量 3
12 drwxr-xr-x 2 root root 1024 5月  26 15:57 .
 2 drwxr-xr-x 4 root root 1024 5月  26 15:56 ..
13 -rw-r--r-- 1 root root    6 5月  26 15:57 test.txt
复制成功
  • 可以发现 /dir目录下:当前工作目录下索引节点为12(dir目录的),上一级目录的索引节点为2(根目录),test.txt文件的所有节点为13。记住这几个索引节点后面我们会通过解析文件系统镜像来获得。

2.解析文件系统镜像

1)dump文件系统镜像

代码块
JavaScript
自动换行
复制代码
$ hexdump -C ext2.img

00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000400  10 00 00 00 64 00 00 00  05 00 00 00 4f 00 00 00  |....d.......O...|
00000410  05 00 00 00 01 00 00 00  00 00 00 00 00 00 00 00  |................|
00000420  00 20 00 00 00 20 00 00  10 00 00 00 ae f8 ad 60  |. ... .........`|
00000430  ae f8 ad 60 01 00 ff ff  53 ef 00 00 01 00 00 00  |...`....S.......|
00000440  75 f7 ad 60 00 00 00 00  00 00 00 00 01 00 00 00  |u..`............|
...
*
00018c00  0c 00 00 00 0c 00 01 02  2e 00 00 00 02 00 00 00  |................|
00018c10  0c 00 02 02 2e 2e 00 00  0d 00 00 00 e8 03 08 01  |................|
00018c20  74 65 73 74 2e 74 78 74  00 00 00 00 00 00 00 00  |test.txt........|
00018c30  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00019000
复制成功
  • 根据之前dumpe2fs的信息我们知道:

镜像文件中(均为16进制显示) 00000000 开始的1k大小 保留的引导块 块1 00000400 开始的1k大小 保存磁盘的超级块 (dumpe2fs的部分信息从这里获得) 块2 00000800 开始的1k大小 保存块组描述符 (dumpe2fs的部分信息从这里获得) 块3 00000c00 开始的1k大小 保存块位图 块4 00001000 开始的1k大小 保存 Inode 位图 块5 块6 00001400 开始的2k大小 保存 Inode表 剩下的为数据块

  • 磁盘中的文件系统对象结构在内核如下文件定义:

代码块
JavaScript
自动换行
复制代码
fs/ext2/ext2.h

磁盘超级块:
 struct ext2_super_block {                                                 
         __le32  s_inodes_count;         /* Inodes count */                
         __le32  s_blocks_count;         /* Blocks count */
         __le32  s_r_blocks_count;       /* Reserved blocks count */
         __le32  s_free_blocks_count;    /* Free blocks count */
         __le32  s_free_inodes_count;    /* Free inodes count */
         __le32  s_first_data_block;     /* First Data Block */
         __le32  s_log_block_size;       /* Block size */
         __le32  s_log_frag_size;        /* Fragment size */
         __le32  s_blocks_per_group;     /* # Blocks per group */
         __le32  s_frags_per_group;      /* # Fragments per group */
         __le32  s_inodes_per_group;     /* # Inodes per group */
  ...
}

磁盘块组描述符:
struct ext2_group_desc                                                    
{                                                                         
        __le32  bg_block_bitmap;                /* Blocks bitmap block */ 
        __le32  bg_inode_bitmap;                /* Inodes bitmap block */ 
        __le32  bg_inode_table;         /* Inodes table block */          
        __le16  bg_free_blocks_count;   /* Free blocks count */           
        __le16  bg_free_inodes_count;   /* Free inodes count */           
        __le16  bg_used_dirs_count;     /* Directories count */           
        __le16  bg_pad;                                                   
        __le32  bg_reserved[3];                                           
};                                                                        


磁盘inode:
 struct ext2_inode {                                                 
         __le16  i_mode;         /* File mode */                     
         __le16  i_uid;          /* Low 16 bits of Owner Uid */      
         __le32  i_size;         /* Size in bytes */                 
         __le32  i_atime;        /* Access time */                   
         __le32  i_ctime;        /* Creation 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;       /* Blocks count */  
     ...
  __le32  i_block[EXT2_N_BLOCKS];/* Pointers to blocks */                
 ...
}; 


磁盘目录项:
struct ext2_dir_entry_2 {                                                    
        __le32  inode;                  /* Inode number */                   
        __le16  rec_len;                /* Directory entry length */         
        __u8    name_len;               /* Name length */                    
        __u8    file_type;                                                   
        char    name[];                 /* File name, up to EXT2_NAME_LEN */ 
};                     
复制成功
  • 大家可以对照磁盘镜像文件和磁盘数据结构定义来解析出文件系统的超级块和块组描述符信息(可以发现和dumpe2fs工具显示的是一致的,例如镜像文件00000400 处四字节为10 00 00 00 是小端存储,所以为0x00000010=16);

3.路径名查找

  • 下面开始我们的重头戏:查找文件系统中的 /dir/test.txt 文件。

  • 我们知道,使用文件系统给我最直观也是最大的好处是:用户可以通过一个路径名来访问文件,那么一个文件系统究竟如何来找到我们所需要的文件呢?下面我们详细来看ext2文件系统如何查找指定的文件的?(实际的内核中路径名查找比较复杂,考虑很多情况,如dentry cache查找、解析软链接文件、上级目录、挂载点等,当然如果目录分量是挂载点就会步进到相应文件系统的根目录,后面文件系统挂载专题会讲解,这里以简单的路径解析来让大家有个深刻的认识)。

1)查找根目录

  • 万事开头难,对于访问一个目录上挂载的文件系统,内核路径名查找会判断并找到挂载的文件系统的根目录,这个过程在文件系统挂载的时候,会从磁盘上读取并在内存构建超级块实例,然后进行的最重要的一步是读取文件系统的根inode:

代码块
JavaScript
自动换行
复制代码
fs/ext2/super.c
ext2_fill_super
->root = ext2_iget(sb, EXT2_ROOT_INO)   //EXT2_ROOT_INO为2,系统定义好的
 ->raw_inode = ext2_get_inode(inode->i_sb, ino, &bh);  //根据inode号查找磁盘inode
   核心算法如下:
     ->block_group = (ino - 1) / EXT2_INODES_PER_GROUP(sb)  //获得块组 编号
      gdp = ext2_get_group_desc(sb, block_group, NULL); //获得块组描述符
      offset = ((ino - 1) % EXT2_INODES_PER_GROUP(sb)) * EXT2_INODE_SIZE(sb); //计算出在 块组的  inode表中的inode偏移
      block = le32_to_cpu(gdp->bg_inode_table) + (offset >> EXT2_BLOCK_SIZE_BITS(sb)); //计算出在文件系统中的块号
      bh = sb_bread(sb, block))  //组合成submit_bh 读取这个块到bh
      *p = bh;  //赋值bh  用于返回
      offset &= (EXT2_BLOCK_SIZE(sb) - 1);  //计算出块中偏移
      return (struct ext2_inode *) (bh->b_data + offset);  //返回inode中位置 
复制成功
  • 简述ext2通过inode号找到并读取磁盘inode核心算法:

  1. 根据inode号计算出所在的块组block_group

  2. 根据inode号计算出块组中的inode表中的字节偏移offset

  3. 根据inode号计算出磁盘inode在文件系统中的块号block

  4. 根据块号block 通过sb_bread读取缓冲区块到内存

  5. 根据inode表中的字节偏移offset 计算出 磁盘inode在块中偏移

  6. 通过读取的缓冲区和磁盘inode在块中偏移 最终返回磁盘inode结构

  • 我们已知:

每个块组inode个数:EXT2_INODES_PER_GROUP(sb) = 16 磁盘inode大小:EXT2_INODE_SIZE(sb) = 128 块大小的bit表示:EXT2_BLOCK_SIZE_BITS(sb) = 10

  • 所以计算根inode块号:

代码块
JavaScript
自动换行
复制代码
 ino=2 跟inode时:

 块组编号:   block_group = (ino - 1) / EXT2_INODES_PER_GROUP(sb)  = (2 - 1) / 16 = 0
 inode表中的根inode偏移 : offset = ((ino - 1) % EXT2_INODES_PER_GROUP(sb)) * EXT2_INODE_SIZE(sb
          = ( 1 % 16 ) * 128 = 128 =0x80  (第2个inode  也就是0x1480处)
 文件系统中的根inode所在块号 : block = le32_to_cpu(gdp->bg_inode_table) + (offset >> EXT2_BLOCK_SIZE_BITS(sb))
       = 5 + (128 >> 10) = 5   
 根inode所在块中偏移:offset &= (EXT2_BLOCK_SIZE(sb) - 1) = 128 = 0x80
    inode中位置 = bh->b_data + offset = 所在块 + 0x80
复制成功

所以:根inode所在的镜像文件中偏移为:5 * 0x400 + 0x80 = 0x1400 + 0x80 = 0x1480

代码块
JavaScript
自动换行
复制代码
查看0x1480偏移处内容如下(即是根目录的磁盘inode内容):
*
00001480  ed 41 00 00 00 04 00 00  54 ff ad 60 44 ff ad 60  |.A......T..`D..`|
00001490  44 ff ad 60 00 00 00 00  00 00 04 00 02 00 00 00  |D..`............|
000014a0  00 00 00 00 01 00 00 00  07 00 00 00 00 00 00 00  |................|
000014b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
复制成功
  • 对照ext2文件系统磁盘inode结构,可知i_block为磁盘inode结构的偏移40B处,内容即为0x07(ext2通过i_block来查找文件在磁盘中的位置)。

  • 于是我们知道,根目录数据块的块号 为0x7(镜像中字节偏移为 0x400 * 7= 1c00),这个数据块中保存的是根目录中包含的所有目录和文件的目录项(我们知道这里为".&#​34;、"..&#​34;、"dir&#​34;、"lost+found&#​34;四个目录项)。

代码块
JavaScript
自动换行
复制代码
根目录数据块的内容:
*
00001c00  02 00 00 00 0c 00 01 02  2e 00 00 00 02 00 00 00  |................|
00001c10  0c 00 02 02 2e 2e 00 00  0b 00 00 00 14 00 0a 02  |................|
00001c20  6c 6f 73 74 2b 66 6f 75  6e 64 00 00 0c 00 00 00  |lost+found......|
00001c30  d4 03 03 02 64 69 72 00  00 00 00 00 00 00 00 00  |....dir.........|
00001c40  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
复制成功
  • 根据目录项ext2_dir_entry_2 结构我们可以查询到文件名为dir的目录项,从而获取dir目录的inode号,为0x0c(和我们之前通ls -lai显示的dir目录inode号12是一致)。

2)查找dir目录

  • 和上面查询根inode一样的原理,计算过程如下:

代码块
JavaScript
自动换行
复制代码
dir目录所在块组编号: block_group = (ino - 1) / EXT2_INODES_PER_GROUP(sb)  = (12 - 1) / 16 = 0
dir目录所在inode表中的inode偏移 : offset = ((ino - 1) % EXT2_INODES_PER_GROUP(sb)) * EXT2_INODE_SIZE(sb
          = ( 11 % 16 ) * 128 = 1408(0x580)  
文件系统中的dir目录的inode所在块号 : block = le32_to_cpu(gdp->bg_inode_table) + (offset >> EXT2_BLOCK_SIZE_BITS(sb))
       = 5 + (1408 >> 10) = 5 +1 =6 
dir目录的inode所在块号中偏移:offset &= (EXT2_BLOCK_SIZE(sb) - 1) = 1408(0x580)& (0x400 -1) =   0x180
    inode中位置 = bh->b_data + offset = 所在块 +  0x180  
复制成功

所以:dir目录inode所在的镜像文件中字节偏移为:6 * 0x400 + 0x180 = 0x1800 + 0x180 = 0x1980

代码块
JavaScript
自动换行
复制代码
查看0x1980偏移处内容如下(即是dir目录的磁盘inode内容):

*
00001980  ed 41 00 00 00 04 00 00  84 ff ad 60 66 ff ad 60  |.A.........`f..`|
00001990  66 ff ad 60 00 00 00 00  00 00 02 00 02 00 00 00  |f..`............|
000019a0  00 00 00 00 02 00 00 00  63 00 00 00 00 00 00 00  |........c.......|
000019b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
复制成功
  • 对照ext2文件系统磁盘inode结构,可知i_block为磁盘inode结构的偏移40B处,内容即为0x63。

  • 于是我们知道,dir目录数据块的块号 为0x63(偏移为 0x400 * 0x63= 0x18c00),这个数据块中保存的是dir目录中包含的所有目录和文件的目录项(我们知道这里为".&#​34;、"..&#​34;、"test.txt&#​34;三个目录项)。

代码块
JavaScript
自动换行
复制代码
dir目录数据块的内容:

*
00018c00  0c 00 00 00 0c 00 01 02  2e 00 00 00 02 00 00 00  |................|
00018c10  0c 00 02 02 2e 2e 00 00  0d 00 00 00 e8 03 08 01  |................|
00018c20  74 65 73 74 2e 74 78 74  00 00 00 00 00 00 00 00  |test.txt........|
00018c30  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
复制成功
  • 对照目录项ext2_dir_entry_2 结构,查找文件名为test.txt的inode号,即为0x0d(和我们之前通ls -lai显示的dir目录inode号13是一致)。

  • 于是我们知道,test.txt文件的inode号为0x0d(13)。

3)查找test.txt文件

  • 和上面查询根inode一样的原理,计算过程如下:

代码块
JavaScript
自动换行
复制代码
test.txt文件inode所在块组 编号: block_group = (ino - 1) / EXT2_INODES_PER_GROUP(sb)  = (13 - 1) / 16 = 0
test.txt文件inode在 inode表中的inode偏移 : offset = ((ino - 1) % EXT2_INODES_PER_GROUP(sb)) * EXT2_INODE_SIZE(sb
          = ( 12 % 16 ) * 128 = 1536(0x600)  
文件系统中的test.txt文件inode所在块号 : block = le32_to_cpu(gdp->bg_inode_table) + (offset >> EXT2_BLOCK_SIZE_BITS(sb))
       = 5 + (1536 >> 10) = 5 +1 =6 
test.txt文件inode所在块号中偏移:offset &= (EXT2_BLOCK_SIZE(sb) - 1) = 1408(0x600)& (0x400 -1) =   0x200
    inode中位置 = bh->b_data + offset = 所在块 +  0x200   
复制成功

所以:test.txt文件inode所在的镜像文件中偏移为:= 6 * 0x400 + 0x200 = 0x1800 + 0x200 = 0x1a00

代码块
JavaScript
自动换行
复制代码
查看 0x1a00偏移处内容如下(即是test.txt文件的磁盘inode内容):

00001a00  a4 81 00 00 06 00 00 00  85 ff ad 60 66 ff ad 60  |...........`f..`|
00001a10  66 ff ad 60 00 00 00 00  00 00 01 00 02 00 00 00  |f..`............|
00001a20  00 00 00 00 01 00 00 00  15 00 00 00 00 00 00 00  |................|
00001a30  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
复制成功
  • 对照ext2文件系统磁盘inode结构,可知i_block为磁盘inode结构的偏移40B处,内容即为0x15。

  • 于是我们知道,test.txt文件数据块的块号 为0x15(偏移为0x15 * 0x400 = 0x5400)。

代码块
JavaScript
自动换行
复制代码
查看  0x5400偏移处内容如下(test.txt文件数据块的内容):
*
00005400  68 65 6c 6c 6f 0a 00 00  00 00 00 00 00 00 00 00  |hello...........|
00005410  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
复制成功

4)查找过程图解

  • 以下为 dir/test.txt查找过程:

  • 已知根目录inode号(ext2为2) -> 查找根目录磁盘inode(文件系统挂载时查找) -> 查找根目录的数据块 -> 查找dir目录的目录项找到其inode号 (为12) -> 查找dir目录的磁盘inode -> 查找dir目录的数据块 -> 查找test.txt文件的inode号(为13) -> 查找test.txt文件的磁盘inode -> 查找test.txt文件的数据块

  • 下面为查找图解:

4.总结

  • 对于ext2文件系统,路径名查找中,实际上是解析路径名的各个分量,查找每个分量的目录项,然后通过目录项找到inode号,通过inode号找到对应的磁盘inode,然后通过磁盘inode获得目录/文件的数据块, 最终查找到对应目录/文件的磁盘inode,而磁盘inode的i_block中保存着文件的逻辑块号和磁盘的逻辑块号映射关系,读写文件时就可以访问到整个文件。