《独辟蹊径品Linux内核源代码导读》VFS一章内容笔记

    技术2022-06-26  41

    磁盘以扇区作为基本的存储单位 ,目前每个扇区有512个字节。硬盘的第一个扇区保存了MBR,分区表和结束标志55AA:前446 个字节为MBR代码,后64个字节为分区表,每个分区表占用16个字节,最多有4个分区表 ,最后4个字节为结束标志55AA.

    磁盘是机械结构的,在进行读写是,需要先控制磁头移动到指定磁道,成为寻道时间,然后等相应扇区旋转到磁头下面,成为旋转延时,最后才通过DMA把指定扇区读入内存。因此如果文件是连续的,那么只需要一次寻道延时间和一次旋转延时,就可以把文件内容全部读入内存,否则就需要多次寻道时间和旋转延时。虽然可以通过磁盘碎片整理来改善,但我们仍希望在一个文件末尾保留几个连续的扇区,这样当文件内容增加时,可以减少碎片的产生。为此Linux提出了块(block) 的概念,块的大小是扇区的2n ,通常为1024B,2048B和4096B。一个分区上的每个块都有块号,块是文件系统分配的基本单位。

    文件的内容可能散落在不连续的块中,这样每个文件都需要一个统一的结构来记录该文件的数据块的位置。在Linux中,这个结构被称为索引节点(inode)。 每个文件都有一个inode且只有一个 ,即使文件中没有数据,其索引结点也是存在的。每个文件用一个单独的Ext2 inode结构来描述,而且每一个inode都有唯一的标志号。Ext2 inode对应的是Ext2_inode结构。

    struct ext2_inode {   

        __u16  i_mode;     /* 文件类型和访问权限 */

       __u16   i_uid;      /* 文件拥有者标识号*/

       __u32   i_size;     /* 以字节计的文件大小 */

       __u32   i_atime;    /* 文件的最后一次访问时间 */

       __u32   i_ctime;    /* 该节点最后被修改时间 */

       __u32   i_mtime;    /* 文件内容的最后修改时间 */

       __u32   i_dtime;    /* 文件删除时间 */

       __u16   i_gid;      /* 文件的用户组标志 */

       __u16   i_links_count;   /* 文件的硬链接计数 */

       __u32   i_blocks;        /* 文件所占块数(每块以512 字节计)*/

       __u32   i_flags;         /* 打开文件的方式 */

       union   ...              /* 特定操作系统的信息*/

       __u32 i_block[Ext2_N_BLOCKS];      /* 指向数据块的指针数组 */

       __u32   i_version;                 /* 文件的版本号(用于 NFS */

       __u32   i_file_acl;                 /* 文件访问控制表(已不再使用) */

       __u32   i_dir_acl;                 /* 目录 访问控制表(已不再使用)*/

       __u8   l_i_frag;                   /* 每块中的片数 */

            __u32    i_faddr;                   /* 片的地址 */

         union {...}                       /* 特定操作系统信息*/

    }

     

    数组iblock存储数据块的地址,总共15项,12 个为直接块指针, 后三项iblock[12..14]分别为一、二、三次间接指针。 所谓“直接块”,是指该块直接 用来存储文件的数据,而“一次间接块”是指该块不存储数据,而是存储直接块的地址,同样,“二次间接块”存储的是“一次间接块”的地址。这里的所说的块,指的都是物理块。 Ext2 默认的物理块大小为1K ,块地址占4 个字节(32 位),所以每个物理块可以存储256 个地址。这样,文件大小最大可达12K+256K+64M+16G 。但实际上,Linux32 位的操作系统,故文件 大小最大只能为4G

    系统是以逻辑块号为 索引查找物理块的。 例如,要找到第100 个逻辑块对应的物理块,因为256+12>100>12 ,所以要用到一次间接块,在一次间接块中查找第88 项,此项内容就是对应的物理块的地址。而如果要找第1000 个逻辑块对应的物理块,由于1000>256+12 ,所以要用到二次间接块了。

     

     

     

    每个文件对应一个inode结构,通过iblock数组就可以访问到给文件的数据 ,然而inode中却没有文件名,文件名保存在目录中,目录也是一个文件,它也占用一个inode结构,它的数据块存储的是该目录下的所有文件的文件名以及文件对应的inode号。

      /*

      * 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 */

           };

     

    Ext2目录在磁盘上的结构如下图所示:

    以打开文件/bin/bash为例: 1 根目录的inode编号总是固定为2,读取根目录的inode,根据file_type说明这是个目录,根据这个inode的iblock数组,找到它的数据块,从数据块中读取该目录下的文件列表,找到文件bin的inode号,如上图为64。 2 根据64找到bin文件对应的inode,在根据file_type和iblock读取数据块中的文件列表,找到bash文件的inode号。 3 根据bash的inode号,找到对应的inode结构,根据iblock读取bash文件的数据块。

     

    硬链接和软连接 :硬链接与源文件的inode号相同,软连接与源文件inode号不同,软连接的inode结构的数据块中存储的是源文件的路径。硬链接的缺点是不能跨越文件系统,即使两个ext2的分区,也不能在他们之间建立硬链接。,硬链接只能指向文件:目录硬链接不可以被创建是为了阻止在同一个目录树中出现死循环。

     

    inode结构建立了目录,文件以及数据之间的联系 ,但还不够,在一个分区上还需要记录inode和数据块分别从哪里开始到哪里结束,哪些是空闲的。这样才能正确的分配文件。随着磁盘容量的不断增加,分区的大小也在不断的增加。为使文件的数据块尽可能的放在连续的磁盘空间上,Linux又提出了组的概念,组是一个逻辑概念,把一个分区划分为许多组,组用来限制文件的数据块不至于过于散落。 Ext2尽量保证一个文件的数据块在同一个组。 任何Ext2分区中的第一个块从不受Ext2文件系统的管理,因为这一块是为分区的引导扇区所保留的。 Ext2分区的其余部分分成块组(block group) ,每个块组的分布图如图所示。正如你从图中所看到的,一些数据结构正好可以放在一块中,而另一些可能需要更多的块。在Ext2文件系统中的所有块组大小相同并被顺序存放,因此,内核可以从块组的整数索引很容易地得到磁盘中一个块组的位置:

    由于内核尽可能地把属于同一个文件的数据块存放在同一块组中,所以块组减少了文件碎片。块组中的每个块包含下列信息之一:

    - 文件系统的超级块的一个拷贝

    - 一组块组描述符的拷贝

    - 一个数据块位图

    - 一个索引节点位图

    - 一个索引节表

    - 属干文件的一大块数据,即数据块

    如果一个块中不包含任何有意义的信息,就说这个块是空闲的。

    从上图中可以看出,超级块与组描述符被复制到每个块组中。

    其实呢,只有块组0中所包含超级块和组描述符才由内核使用,而其余的超级块和组描述符都保持不变; 事实上,内核甚至不考虑它们。当e2fsck程序对Ext2文件系统的状态执行一致性检查时,就引用存放在块组0中的超级块和组描述符,然后把它们拷贝到其他所有的块组中。如果出现数据损坏,并且块组0 中的主超级块和主描述符变为无效,那么,系统管理员就可以命令e2fsck引用存放在某个块组(除了第一个块组)中的超级块和组描述符的旧拷贝。通常情况下,这些多余的拷贝所存放的信息足以让e2fsck把Ext2分区带回到一个一致的状态。

     

    1 磁盘超级块

    Ext2在磁盘上的超级块存放在一个ext2_super_block结构中 ,它的字段在下面列出(为了确保Ext2和Ext3文件系统之间的兼容性,ext2_super_block数据结构包含了一些Ext3特有的字段,我们省略号省了):struct ext2_super_block {     __le32     s_inodes_count;            /* 索引节点的总数 */     __le32     s_blocks_count;             /* 块总数(所有的块) */     __le32     s_r_blocks_count;          /* 保留的块数 */     __le32     s_free_blocks_count;     /* 空闲块数 */     __le32     s_free_inodes_count;     /* 空闲索引节点数 */     __le32     s_first_data_block;         /* 第一个使用的块号(总为1) */     __le32     s_log_block_size;           /* 块的大小 */     __le32     s_log_frag_size;             /* 片的大小 */     __le32     s_blocks_per_group;      /* 每组中的块数 */     __le32     s_frags_per_group;        /* 每组中的片数 */     __le32     s_inodes_per_group;     /* 每组中的索引节点数 */     __le32     s_mtime;                        /* 最后一次安装操作的时间 */     __le32     s_wtime;                        /* 最后一次写操作的时间 */     __le16     s_mnt_count;                 /* 被执行安装操作的次数 */     __le16     s_max_mnt_count;        /* 检查之前安装操作的次数 */     __le16     s_magic;                   /* 魔术签名 */     __le16     s_state;                    /* 文件系统状态标志 */     __le16     s_errors;                   /* 当检测到错误时的行为 */     __le16     s_minor_rev_level;        /* 次版本号 */     __le32     s_lastcheck;                  /* 最后一次检查的时间 */     __le32     s_checkinterval;            /* 两次检查之间的时间间隔 */     __le32     s_creator_os;                /* 创建文件系统的操作系统 */     __le32     s_rev_level;             /* 主版本号 */     __le16     s_def_resuid;          /* 保留块的缺省UID */     __le16     s_def_resgid;          /* 保留块的缺省用户组ID */     /*      * These fields are for EXT2_DYNAMIC_REV superblocks only.      *      * Note: the difference between the compatible feature set and      * the incompatible feature set is that if there is a bit set      * in the incompatible feature set that the kernel doesn't      * know about, it should refuse to mount the filesystem.      *       * e2fsck's requirements are more strict; if it doesn't know      * about a feature in either the compatible or incompatible      * feature set, it must abort and not try to meddle with      * things it doesn't understand...      */     __le32     s_first_ino;           /* 第一个非保留的索引节点号 */     __le16   s_inode_size;           /* 磁盘上索引节点结构的大小 */     __le16     s_block_group_nr;      /* 这个超级块的块组号 */     __le32     s_feature_compat;      /* 具有兼容特点的位图 */     __le32     s_feature_incompat;      /* 具有非兼容特点的位图 */     __le32     s_feature_ro_compat;      /* 只读兼容特点的位图 */     __u8     s_uuid[16];          /* 128位文件系统标识符 */     char     s_volume_name[16];      /* 卷名 */     char     s_last_mounted[64];      /* 最后一个安装点的路径名 */     __le32     s_algorithm_usage_bitmap; /* 用于压缩 */     /*      * Performance hints.  Directory preallocation should only      * happen if the EXT2_COMPAT_PREALLOC flag is on.      */     __u8     s_prealloc_blocks;     /* 预分配的块数 */     __u8     s_prealloc_dir_blocks;     /* 为目录预分配的块数 */     __u16     s_padding1;             /* 按字对齐 */     ……     __u32     s_reserved[190];     /* 用null填充1024字节 */};

    __u8、__u16及__u32数据类型分别表示长度为8、16及32位的无符号数,而__s8、__s16及__s32数据类型表示长度为8、16及32位的有符号数。为清晰地表示磁盘上字或双字中字节的存放顺序,内核又使用了__le16、__le32、__be16和__be32数据类型,前两种类型分别表示字或双字的“小尾(little-endian )”排序方式(低阶字节在高位地址),而后两种类型分别表示字或双字的“大尾(big-endian )”排序方式(高阶字节在高位地址):

    typedef __signed__ char __s8;typedef unsigned char __u8;

    typedef __signed__ short __s16;typedef unsigned short __u16;

    typedef __signed__ int __s32;typedef unsigned int __u32;

    typedef __signed__ long long __s64;typedef unsigned long long __u64;

    typedef __u16 __bitwise __le16;typedef __u16 __bitwise __be16;typedef __u32 __bitwise __le32;typedef __u32 __bitwise __be32;typedef __u64 __bitwise __le64;typedef __u64 __bitwise __be64;

    有细心的朋友可以数一数这些u或者le后面的数字,然后加起来后除以8,可以发现刚刚小于512。不错,512B正是最小的块大小,再小一点一个块就装不下ext2_super_block了。所有我推断,为什么这里要用这些u、s、或者le然后后面加数字的形式,就是方便开发者避免超过一个块大小而看起来方便一点的原因。

    s_inodes_count字段存放索引节点的个数,而s_blocks_count字段存放Ext2文件系统的块的个数。

    s_log_block_size字段以2的幂次方表示块的大小,用1024字节作为单位。因此,0表示1024字节的块,1表示2048字节的块,如此等等。目前s_log_frag_size字段与slog_block_size字段相等,因为块片还没有实现。

    s_blocks_per_group、s_frags_per_group与s_inodes_per_group字段分别存放每个块组中的块数、片数及索引节点数。

    一些磁盘块保留给超级用户(或由s_def_resuid和s_def_resgid字段挑选给某一其他用户或用户组)。即使当普通用户没有空闲块可用时,系统管理员也可以用这些块继续使用Ext2文件系统。

    s_mnt_count、s_max_mnt_count、s_lastcheck及s_checkinterval字段使系统启动时自动地检查 Ext2文件系统。在预定义的安装操作数完成之后,或自最后一次一致性检查以来预定义的时间已经用完,这些字段就导致e2fsck执行(两种检查可以一起进行)。如果Ext2文件系统还没有被全部卸载(例如系统崩溃以后),或内核在其中发现一些错误,则一致性检查在启动时要强制进行。如果Ext2文件系统被安装或未被全部卸载,则s_state字段存放的值为0;如果被正常卸载,则这个字段的值为1;如果包含错误,则值为2。

    2 组描述符和位图

    每个块组都有自己的组描述符,它是一个ext2_group_desc结构:

    struct ext2_group_desc{     __le32     bg_block_bitmap;             /* 块位图的块号 */     __le32     bg_inode_bitmap;            /* 索引节点位图的块号 */     __le32     bg_inode_table;               /* 第一个索引节点表块的块号 */     __le16     bg_free_blocks_count;     /* 组中空闲块的个数 */     __le16     bg_free_inodes_count;     /* 组中空闲索引节点的个数 */     __le16     bg_used_dirs_count;        /* 组中目录的个数 */     __le16     bg_pad;                            /* 按字对齐 */     __le32     bg_reserved[3];                /* 用null填充24个字节 */};

    注意,每个块组都有n个块组描述符的拷贝,n等于块的个数,也就是刚才例子中的256个块组。没看懂的再仔细回忆回忆前面的那个块图。

    当分配新索引节点和数据块时,会用到bg_free_blocks_count、bg_free_inodes_count、和 bg_used_dirs_count字段。这些字段确定在最合适的块中给每个数据结构进行分配位图中位的序列,其中值0表示对应的索引节点块或数据块是空闲的,1表示占用。因为每个位图必须存放在一个单独的块中,又因为块的大小可以是1024、2048或4096字节,因此,一个单独的位图描述 8192、16384或32768个块的状态。

    3 磁盘索引节点表

    索引节点表由一连串连续的块组成,其中每一块包含索引节点的一个预定义号。索引点表第一个块的块号存放在组描述符的bg_inode_table字段中。共有n个块,n等于块组的数量(256)。

    所有索引节点的大小相同,即128字节。一个1024字节的块可以包含8个索引节点,一个4096字节的块可以包含32个索引节点。为了计算出索引节点表占用了多少块,用一个组中的索引节点总数(存放在超级块的s_inodes_per_group字段中)除以每块的索引节点数。

    每个Ext2索引节点为ext2_inode结构:struct ext2_inode {     __le16     i_mode;          /* 文件类型和访问权限 */     __le16     i_uid;          /* 拥有者标识符 */     __le32     i_size;          /* 以字节为单位的文件长度 */     __le32     i_atime;     /* 最后一次访问文件的时间 */     __le32     i_ctime;     /* 索引节点最后改变的时间 */     __le32     i_mtime;     /* 文件内容最后改变的时间 */     __le32     i_dtime;     /* 文件删除的时间 */     __le16     i_gid;          /* 用户组标识符低16位 */     __le16     i_links_count;     /* 硬链接计数器 */     __le32     i_blocks;     /* 文件的数据块数 */     __le32     i_flags;     /* 文件标志 */     union {          struct {               __le32  l_i_reserved1;          } linux1;          struct {               __le32  h_i_translator;          } hurd1;          struct {               __le32  m_i_reserved1;          } masix1;     } osd1;                    /* 特定的操作系统信息 1 */     __le32     i_block[EXT2_N_BLOCKS]; /* 指向数据块的指针 */     __le32     i_generation;     /* 文件版本(当网络文件系统访问文件时) */     __le32     i_file_acl;     /* 文件访问控制列表 */     __le32     i_dir_acl;     /* 目录访问控制列表 */     __le32     i_faddr;     /* 片的地址 */     union {          struct {               __u8     l_i_frag;     /* Fragment number */               __u8     l_i_fsize;     /* Fragment size */               __u16     i_pad1;               __le16     l_i_uid_high;     /* these 2 fields    */               __le16     l_i_gid_high;     /* were reserved2[0] */               __u32     l_i_reserved2;          } linux2;          struct {               __u8     h_i_frag;     /* Fragment number */               __u8     h_i_fsize;     /* Fragment size */               __le16     h_i_mode_high;               __le16     h_i_uid_high;               __le16     h_i_gid_high;               __le32     h_i_author;          } hurd2;          struct {               __u8     m_i_frag;     /* Fragment number */               __u8     m_i_fsize;     /* Fragment size */               __u16     m_pad1;               __u32     m_i_reserved2[2];          } masix2;     } osd2;                    /* 特定的操作系统信息 2 */};

     

     

     


    最新回复(0)