Linux ext2, ext3, ext4 文件系统解读[2]

时间:2021-10-16 19:35:43


目录与文件:

从前面Inode的结构中我们可以看到,其中并没有定义文件名称,文件名称实际是存放在目录中的,目录也是一类特殊的文件,在ext2文件系统中,目录是由ext2_dir_entry结构组成的列表,目录是变长的结构,这样可以避免磁盘空间的浪费。对于单个文件名,长度不能超过255字符,且文件名的长度会自动使用'\0'字符填充为4个整数倍。目录中有文件和子目录。

ext2_dir_entry结构:

 /*

 * Structure of a directory entry

 */

 #define EXT2_NAME_LEN 255

 

struct ext2_dir_entry {

       __u32   inode;                  /* Inode number */

       __u16   rec_len;                /* Directory entry length */

       __u16   name_len;               /* Name length */

       char    name[EXT2_NAME_LEN];    /* File name */

};

上面是老版本的定义,新版本的结构定义:

/*

 * The new version of the directory entry.  Since EXT2 structures are

 * stored in intel byte order, and the name_len field could never be

 * bigger than 255 chars, it's safe to reclaim the extra byte for the

 * file_type field.

 */

 

struct ext2_dir_entry_2 {

        __u32   inode;                  /* Inode number */

        __u16   rec_len;                /* Directory entry length */

        __u8    name_len;               /* Name length */

        __u8    file_type;

        char    name[EXT2_NAME_LEN];    /* File name */

};

可以看到,新版本中,将文件名的长度的属性从16比特修改为8比特,将剩余的8比特用于记录文件类型。

文件类型定义:

/ *

 * Ext2 directory file types.  Only the low 3 bits are used. The

 * other bits are reserved for now.

 */

 

enum {

        EXT2_FT_UNKNOWN,        /*未知*/

        EXT2_FT_REG_FILE,       /*普通文件*/

        EXT2_FT_DIR,           /*目录文件*/

        EXT2_FT_CHRDEV,         /*字符设备*/

        EXT2_FT_BLKDEV,         /*块设备*/

        EXT2_FT_FIFO,          /*管道*/

        EXT2_FT_SOCK,          /*套接字*/

        EXT2_FT_SYMLINK,            /*符号链接*/

        EXT2_FT_MAX             /*文件类型枚举的结尾标识*/

};

无论何种类型的文件,都对应一个InodeInode号在结构中inode属性记录。不同类型的文件使用Data Block的方式并不相同。有的文件不用来存放数据,不使用Data Block。下面是不同类型的文件使用Data Block的说明:

普通文件:

普通文件在创建时是空的,再向其中写入数据的时候,会为其分配Data Block

目录文件:

目录文件的Data Block中存放的就是上面的ext2_dir_entry结构(或者是新的结构),name长度可变,最大不会超过255rec_len记录了目录的大小,将它与这个目录的起始地址相加,就得到了下一个有效目录的起始地址,所以rec_len也可以看做是下一个有效目录的指针。要删除当前这个目录,只需要将inode置为0,并增加前一个目录的rec_len的值,以“跳过”当前这个删除的目录。

符号链接(软连接):

如果符号链接的路径名称小于60个字符,则使用Inodei_block数组直接存放其值(15*4字节=60字节),如果超出60了字符,则需要为其分配一个Block,并将i_block中的指针指向对应的Block

设备文件,管道,套接字:

这些文件类型不需要Block,所有信息都存放在Inode中。

 

下面是一个小例子,说明了ext2文件系统中,是如何利用目录来读出一个文件内容的,顺便也熟悉一下debugfs调试工具:

我们将1023GB的磁盘(分区:/dev/sdd1)使用ext2格式化,然后挂载到/mnt/sdd下,在下面创建了几个目录和文件,结构如下:

/mnt/sdd

/mnt/sdd/root_inode_debugfs

/mnt/sdd/test/new1.txt

/mnt/sdd/test/testlnk.txt -> test.txt

/mnt/sdd/test/test.txt

/mnt/sdd/test/test1/2.txt

/mnt/sdd/test/test2/2.txt

首先,输入debugfs,进入调试:

[root@DanCentOS65 daniel]# debugfs

debugfs 1.41.12 (17-May-2010)

debugfs:

接着使用open打开要调试的设备:

debugfs: open /dev/sdd1

查看根目录的详细信息:

debugfs: ls -l

     2   40755 (2)      0     0    4096 21-Apr-2017 03:21 .

     2   40755 (2)      0     0    4096 21-Apr-2017 03:21 ..

    11   40700 (2)      0     0   16384 20-Apr-2017 14:16 lost+found

    12  100644 (1)      0     0    4096 21-Apr-2017 02:49 root_inode_debugfs

 855681  40755 (2)      0      0   4096 21-Apr-2017 07:30 test

每一列的说明:

  1. 第一列为Inode
  2. 第二列表示文件权限(前三位是suid/sgid/ticky,后三位的User/Group/Others的权限)
  3. 第三列是文件类型,(2)对应目录,(1)对应普通文件,具体可以参考一下前面ext2文件类型的枚举
  4. 第四列表示文件/目录的ownerid
  5. 第五列表示文件/目录的groupid
  6. 第六列表示文件占用的大小,可以除以Block大小得到文件占用的Block数量
  7. 第七列表示创建时间
  8. 最后一列是目录的名称

注意到上面输出结果中有两个特殊的目录“.”和“..”,这两个目录不能删除,用于相对路径的查找,“.”表示当前目录,“..”表示父目录。

言归正传,我们以/mnt/sdd/test/test2/2.txt来说明文件的读取过程:

我们当前查看的目录其实就是/dev/sdd1的挂载点的目录,即/mnt/sdd,所以在结果中看到的“.”目录对应的Inode号为2,也就是/mnt/sdd的根Inode号是2,进一步查看Inode 2的内容,首先通过命令将Inode 2的内容输出到文件中:

debugfs: dump_inode <2> dump1

接着在另一个终端中使用vi -b打开dump1文件(注意,上面dump1文件的存放目录是进入debugfs之前的那个目录,而不是我们调试的那个目录):

[root@DanCentOS65 daniel]# vi -b dump1

 

^B^@^@^@^L^@^A^B.^@^@^@^B^@^@^@^L^@^B^B..^@^@^K^@^@^@$^@

^Blost+found^@^@^@^@^@^@^P^@^E^A1.txt^@^@^@^L^@^@^@D^@^R^Aroot_inode_debugfs^F^A^@^@^@^@

(^@^L^Adir1_debugfs^@^@^@^@^T^@^K^Adev_debugfs^@<81>^N^M^@<80>^O^D^Btest^@^@^@^@^@^@^@^@

^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@

^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@

^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@

^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@

……

上面的结果中显示了目前Inode 2中存放的内容,其中有一些乱码先忽略掉,我们先查看可读的文件名。细心的读者可能会发现除了我们在目录中看到的lost+found目录,test目录以及root_inode_debugfs文件之外,还有1.txtdir1_debugfs两个文件,其实这两个文件之前确实是存在的,后来删除掉了,由于删除其实仅仅是修改对应的InodeBlock的标记位,在没有新的内容覆盖掉其内容之前,这些旧的条目会一直都在磁盘上,所以这里我们才能看到对应的内容,这也是一些磁盘数据恢复工具的原理所在。

当我们尝试打开/mnt/sdd/test/test2/2.txt时,文件系统首先会根据目录/mnt/sdd得到其Inode2,并且确认其权限有读权限,从中读出Block中的内容,也就是上面这些子目录和文件的内容,从中找到我们要访问的下一个目录test,并找到对应的Inode号码855681(从上面的结果中可以看到)。接着系统会读取Inode 855681的内容:

 

.                                                                                                                                                                                                                             ntest2.tx

<81>^N^M^@^L^@^A^B.^@^@^@^B^@^@^@^L^@^B^B..^@^@<85>^N^M^@^P^@^H^Anew1.txt<82>^N^M^@^P^@^E^Btest1t.s^A^

O^M^@^P^@^E^Btest2.tx<83>^N^M^@^X^@^K^Gtestlnk.txtwp^@^@^@<84>^N^M^@<a0>^O^H^Atest.txtt.swx^@^@^@^@^@^

@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^

@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^

@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^

@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^

@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@......

同理,文件系统会从中找到下一级目录test2,并找到他的Inode编号855809

debugfs: ls -l test

 855681  40755 (2)      0      0   4096 21-Apr-2017 08:09 .

     2   40755 (2)      0     0    4096 21-Apr-2017 09:14 ..

 855685 100644 (1)      0      0     15 21-Apr-2017 06:54 new1.txt

 855682  40755 (2)      0      0   4096 21-Apr-2017 05:04 test1

 855809  40755 (2)      0      0   4096 21-Apr-2017 05:05 test2

 855683 120777 (7)      0      0      8 21-Apr-2017 08:09 testlnk.txt

 855684 100644 (1)      0      0     58 21-Apr-2017 07:30 test.txt

继续读取其中内容:

^A^O^M^@^L^@^A^B.^@^@^@<81>^N^M^@^L^@^B^B..^@^@^D^O^M^@<e8>^O^E^A2.txt^@^@^@^@^@^@^@<d8>^O

^A.2.txt.swp^@^@^@^@^@^@<c4>^O^F^A2.txt~.swx^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^

@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^

@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@

 进一步得到2.txtInode号,最终从Data Block中读出文件数据。

调试完成后,可以使用close关闭打开的设备:

debugfs: close

需要注意的是,在调试过程中,如果我们在其他终端中对这个文件系统的文件或者目录进行调整,是不会反映到当前Debug的过程中的,想要重新载入修改的目录和文件,需要退出debugfs,重新mount一下分区,再次打开debugfs即可。

 

除了上面列出的命令之外,还有很多其他命令可以使用,例如可以使用features查看superblock中定义的功能特性,使用stat <inode number>查看对应的Inode的结构信息,使用stats可以查看superblock的结构等等,有兴趣的读者可以使用?查看全部命令(也可以使用man debugfs查看更详细的用户手册):

debugfs: ?

Available debugfs requests:

 

show_debugfs_params, params

                        Show debugfs parameters

open_filesys, open      Open a filesystem

close_filesys, close    Close the filesystem

feature, features       Set/print superblock features

dirty_filesys, dirty    Mark the filesystem as dirty

init_filesys            Initialize a filesystem (DESTROYS DATA)

show_super_stats, stats Show superblock statistics

ncheck                  Do inode->name translation

icheck                  Do block->inode translation

change_root_directory, chroot

                        Change root directory

change_working_directory, cd

                        Change working directory

list_directory, ls      List directory

show_inode_info, stat   Show inode information

dump_extents, extents, ex

                        Dump extents information

link, ln                Create directory link

unlink                  Delete a directory link

mkdir                   Create a directory

rmdir                   Remove a directory

rm                      Remove a file (unlink and kill_file, if appropriate)

kill_file               Deallocate an inode and its blocks

clri                    Clear an inode's contents

freei                   Clear an inode's in-use flag

seti                    Set an inode's in-use flag

testi                   Test an inode's in-use flag

freeb                   Clear a block's in-use flag

setb                    Set a block's in-use flag

testb                   Test a block's in-use flag

modify_inode, mi        Modify an inode by structure

find_free_block, ffb    Find free block(s)

find_free_inode, ffi    Find free inode(s)

print_working_directory, pwd

                        Print current working directory

expand_dir, expand      Expand directory

mknod                   Create a special file

list_deleted_inodes, lsdel

                        List deleted inodes

undelete, undel         Undelete file

write                   Copy a file from your native filesystem

dump_inode, dump        Dump an inode out to a file

cat                     Dump an inode out to stdout

lcd                     Change the current directory on your native filesystem

rdump                   Recursively dump a directory to the native filesystem

set_super_value, ssv    Set superblock value

set_inode_field, sif    Set inode field

set_block_group, set_bg Set block group descriptor field

logdump                 Dump the contents of the journal

htree_dump, htree       Dump a hash-indexed directory

dx_hash, hash           Calculate the directory hash of a filename

dirsearch               Search a directory for a particular filename

bmap                    Calculate the logical->physical block mapping for an inode

imap                    Calculate the location of an inode

dump_unused             Dump unused blocks

set_current_time        Set current time to use when setting filesystme fields

supported_features      Print features supported by this version of e2fsprogs

help                    Display info on command or topic.

list_requests, lr, ?    List available commands.

quit, q                 Leave the subsystem.

 

除了上面说的dumpe2fsdebugfs中的调试命令之外,下面也是一些常用的查看命令:

通过-i参数在输出结果中查看Inode编号:

[root@DanCentOS65 test]# ls -il

total 16

855685 -rw-r--r-- 1 root root  15 Apr 21 06:54 new1.txt

855682 drwxr-xr-x 2 root root 4096 Apr 21 05:04 test1

855809 drwxr-xr-x 2 root root 4096 Apr 21 05:05 test2

855683 lrwxrwxrwx 1 root root   8 Apr 21 08:09 testlnk.txt -> test.txt

855684 -rw-r--r-- 1 root root  58 Apr 21 07:30 test.txt

stat命令查看目录或文件对应的Inode信息:

[root@DanCentOS65 test]# stat test2

 File: `test2'

 Size: 4096              Blocks: 8         IO Block: 4096   directory

Device: 831h/2097d        Inode: 855809     Links: 2

Access: (0755/drwxr-xr-x) Uid: (    0/    root)  Gid: (    0/    root)

Access: 2017-04-21 05:35:58.000000000 +0000

Modify: 2017-04-21 05:05:08.000000000 +0000

Change: 2017-04-21 05:05:08.000000000 +0000

查看Inode使用情况:

[root@DanCentOS65 daniel]# df -ih

Filesystem    Inodes IUsed IFree IUse% Mounted on

/dev/sda1       1.9M   98K  1.8M   6% /

tmpfs           873K     1  873K   1% /dev/shm

/dev/sdb1        18M    12   18M   1% /mnt/resource

/dev/sdf1       1.9M   36K  1.9M   2% /mnt/ubuntu

/dev/sdd1      1023K    20 1023K    1% /mnt/sdd

 

顺便再补充一下软连接和硬链接的知识:

我们接着上面的例子,再创建一个硬链接:

[root@DanCentOS65 daniel]# ll /mnt/sdd/test

total 20

-rw-r--r-- 2 root root  15 Apr 21 06:54 new1HDlnk

-rw-r--r-- 2 root root  15 Apr 21 06:54 new1.txt

drwxr-xr-x 2 root root 4096 Apr 21 05:04 test1

drwxr-xr-x 2 root root 4096 Apr 21 05:05 test2

lrwxrwxrwx 1 root root   8 Apr 21 08:09 testlnk.txt -> test.txt

-rw-r--r-- 1 root root  58 Apr 21 07:30 test.txt

上面的目录中new1HDlnk文件是new1.txt的硬链接,testlnk.txttest.txt的软连接,我们还是用debugfs来看一下二者的区别:

debugfs: ls -l test

 855681  40755 (2)      0      0   4096 21-Apr-2017 09:47 .

     2   40755 (2)      0     0    4096 21-Apr-2017 09:14 ..

 855685 100644 (1)      0      0     15 21-Apr-2017 06:54 new1.txt

 855682  40755 (2)      0      0   4096 21-Apr-2017 05:04 test1

 855809  40755 (2)      0      0   4096 21-Apr-2017 05:05 test2

 855683 120777 (7)      0      0      8 21-Apr-2017 08:09 testlnk.txt

 855684 100644 (1)      0      0     58 21-Apr-2017 07:30 test.txt

 855685 100644 (1)      0      0     15 21-Apr-2017 06:54 new1HDlnk

可以明显看到,硬链接new1HDlnk实际上仅仅是源文件的一个别名,写到了上级目录的Data Block中,它是没有自己的Inode的,所以它的Inode855685其实也就是源文件new1.txtInode号。而软连接不同,软连接是一类特殊的文件,有自己的Inode和内容。上面例子中,源文件test.txt的文件名小于60字节,所以我们可以看到testlnk.txtInode中并没有分配Data Block

debugfs: stat <855683>

Inode: 855683  Type: symlink    Mode:  0777  Flags: 0x0

Generation: 2285856085   Version: 0x00000000

User:    0   Group:     0  Size: 8

File ACL: 0   Directory ACL: 0

Links: 1  Blockcount: 0

Fragment: Address: 0    Number: 0    Size: 0

ctime: 0x58f9be38 -- Fri Apr 21 08:09:28 2017

atime: 0x58f9be39 -- Fri Apr 21 08:09:29 2017

mtime: 0x58f9be38 -- Fri Apr 21 08:09:28 2017

Size of extra inode fields: 0

Fast_link_dest: test.txt

我们在做一个测试,创建一个超过60字节文件名的文件file0123456789012345678901234567890123456789012345678901234567890123456789.txt,并为其创建一个软连接largerlnk

[root@DanCentOS65 test]# ll

total 28

-rw-r--r-- 1 root root  71 Apr 22 08:35 file0123456789012345678901234567890123456789012345678901234567890123456789.txt

lrwxrwxrwx 1 root root  78 Apr 22 08:36 largerlnk -> file0123456789012345678901234567890123456789012345678901234567890123456789.txt

-rw-r--r-- 2 root root  15 Apr 21 06:54 new1HDlnk

-rw-r--r-- 2 root root  15 Apr 21 06:54 new1.txt

drwxr-xr-x 2 root root 4096 Apr 21 05:04 test1

drwxr-xr-x 2 root root 4096 Apr 21 05:05 test2

lrwxrwxrwx 1 root root   8 Apr 21 08:09 testlnk.txt -> test.txt

-rwxr-xr-x 1 root root  58 Apr 21 07:30 test.txt

再来看一下largerlnk对应的Inode

debugfs: ls -l

 855681  40755 (2)     0     0   4096 22-Apr-2017 08:36 .

     2   40755 (2)     0    0    4096 21-Apr-2017 09:14 ..

 855685 100644 (1)     0     0     15 21-Apr-2017 06:54 new1.txt

 855682  40755 (2)     0     0   4096 21-Apr-2017 05:04 test1

 855809  40755 (2)     0     0   4096 21-Apr-2017 05:05 test2

 855683 120777 (7)     0     0      8 21-Apr-2017 08:09 testlnk.txt

 855684 100755 (1)     0     0     58 21-Apr-2017 07:30 test.txt

 855685 100644 (1)     0     0     15 21-Apr-2017 06:54 new1HDlnk

 855687 120777 (7)     0     0     78 22-Apr-2017 08:36 largerlnk

 855688 100644 (1)     0     0     71 22-Apr-2017 08:35 file0123456789012345678901234567890123456789012345678901234567890123456789.txt

debugfs: stat <855687>

Inode: 855687  Type: symlink    Mode:  0777  Flags: 0x0

Generation: 2439924333   Version: 0x00000000

User:    0   Group:     0  Size: 78

File ACL: 0   Directory ACL: 0

Links: 1  Blockcount: 8

Fragment: Address: 0    Number: 0    Size: 0

ctime: 0x58fb1607 -- Sat Apr 22 08:36:23 2017

atime: 0x58fb1608 -- Sat Apr 22 08:36:24 2017

mtime: 0x58fb1607 -- Sat Apr 22 08:36:23 2017

Size of extra inode fields: 0

BLOCKS:

(0):219056128

TOTAL: 1

可以看到,当指向的文件名超过60字节时,就不能在Inodei_block中直接存放指向的文件名了,此时文件系统会分配一个Data Block,我们看到Data Block的编号为219056128,查看一下其中的内容(当然直接在debugfs中通过cat也可以查看,只不过这里演示一下如何使用dd来读取Data Block的数据):

[root@DanCentOS65 test]# dd if=/dev/sdc1 of=block.txt skip=219056128 ibs=4096 bs=4096 count=1

1+0 records in

1+0 records out

4096 bytes (4.1 kB) copied, 0.00018398 s, 22.3 MB/s

[root@DanCentOS65 test]# vi block.txt

file0123456789012345678901234567890123456789012345678901234567890123456789.txt^@^@^@^@^@^@^@^@^

@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^

@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^

@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^

@^@^@^@^@^@^@^@^@^@^@^@^@……

可以看到,Data Block中存放的就是指向的文件的名称。

注:dd命令参数简要说明:

if=file                                                   #输入文件名,缺省为标准输入

of=file                                                   #输出文件名,缺省为标准输出

ibs=bytes                                               #一次读入 bytes 个字节(即一个块大小为 bytes 个字节)

obs=bytes                                               #一次写 bytes 个字节(即一个块大小为 bytes 个字节)

bs=bytes                                                 #同时设置读写块的大小为 bytes ,可代替 ibs 和 obs 

cbs=bytes                                               #一次转换 bytes 个字节,即转换缓冲区大小

skip=blocks                                            #从输入文件开头跳过 blocks 个块后再开始复制

seek=blocks                                            #从输出文件开头跳过 blocks 个块后再开始复制

count=blocks                                          #仅拷贝 blocks 个块,块大小等于 ibs 指定的字节数

conv=conversion[,conversion...] #用指定的参数转换文件