一、文件系统基础概念

inode和block关系示意图


文件系统的基本组成结构是inode和data以及superblock。

inode:代表了文件的元信息,包括:inode号、文件大小、权限、所属用户和组等信息。

data:是数据部分,存储了实际的文件数据,data的基本存储单位是块(block),不同文件系统下块的大小各不相同,但一般都是4KB,相当于8个连续扇区的大小。

如上图,上面是inode部分,下面是data部分,inode中存储了文件所在各个块的指针,文件块在磁盘上并不一定连续分布,可能分散在不同的各个块中。当然这个只是基础的原理示意,实际的存储过程显然要复杂的多,不同的文件系统有不同的实现方式,比如ext3/4是通过多级索引和Extent组织索引,而xfs则是b+ tree的方式组织索引。

superblock:代表了文件系统的元信息,inode和data是相对于文件来说的,而superblock则是相对于文件系统来说的,通过位图存储了已使用和未使用的inode和data信息,以及block的大小和块组、文件系统类型、文件系统挂载等信息。


二、虚拟文件系统VFS

我们通常说的文件系统是指磁盘上的实际文件系统,如ext3/4、xfs等,但文件系统在内存中也有自己的表示结构,这个结构就是VFS。

VFS是一个在具体的文件系统之上抽象的一层,用来处理与文件系统相关的所有调用,通过给各种文件系统提供一个通用的接口,从而屏蔽了底层不同文件系统之间的差异,使上层的应用程序能够使用一个通用的接口访问不同文件系统。

VFS示意图


VFS主要由dentry、inode、super_block和file结构体4部分组成。

1:dentry(目录项)

dentry结构体如下:

struct dentry {
    /* RCU lookup touched fields */
    unsigned int d_flags;                       /* 目录项状态标志 */
    seqcount_spinlock_t d_seq;                  /* 实现顺序一致性访问的锁 */
    struct hlist_bl_node d_hash;                /* 目录项查找函数 */
    struct dentry *d_parent;                    /* 父目录 */
    struct qstr d_name;                         /* 目录项名称 */
    struct inode *d_inode;                      /* 该目录项对应的inode */
    unsigned char d_iname[DNAME_INLINE_LEN];    /* 短名称(一般为32个字符以内) */
    const struct dentry_operations *d_op;       /* 目录项操作方法集 */
    struct super_block *d_sb;                   /* 对应的超级块结构 */
    unsigned long d_time;                       /* 重新变为有效的时间 */  
    void *d_fsdata;                             /* 私有数据 */
    struct lockref d_lockref;                   /* 自旋锁 */
    union {
        struct list_head d_lru;                 /* 最近未使用的目录项的链表 */ 
        wait_queue_head_t *d_wait;              /* 等待队列头 */
    };
    struct hlist_node d_sib;                    /* 哈希列表节点 */
    struct hlist_head d_children;               /* 目录项通过这个加入到父目录的d_subdirs中 */ 
    union {
        struct hlist_node d_alias;              /* 目录项别名 */
        struct hlist_bl_node d_in_lookup_hash;  /* 哈希列表节点,只有在目录项正在被查找时才使用*/
        struct rcu_head d_rcu;
    } d_u;
};

dentry也叫目录项,可以理解为文件路径在VFS中的表示结构,同一个文件系统中每个文件路径对应唯一的一个dentry结构,所有的dentry结构组成了dentry结构树,即:文件系统目录树。

dentry示意图


Linux可以把一个文件系统挂载到某个目录上,dentry是相对于文件系统来说的,每个dentry在其文件系统内dentry结构树上的位置是固定的,如上图,文件系统B挂载到文件系统A的/var/lib/docker目录,虽然containers可以通过/var/lib/docker/container路径访问,但containers的dentry结构在文件系统B对应的dentry结构树上的位置是固定的,不会随着文件系统B挂载到不同的位置而改变,也就是说dentry是相对于其所在的文件系统来说的。



1. dentryinode是多对一的(硬链接的情况),可以通过inode->i_dentry找到指向inode的所有dentry.
2. sys_open() 打开一个文件,如果文件不存在,dentry仍然会被创建且被加入到dcache中,但是dentry指向的inode为空(此时dentry为负状态),path_walk()完毕后,dentry被加入到super_blockLRU链表中,等待销毁.
3. dentry有自己所属的文件系统,因此dentry建立的树状层次结构只在dentry所属的文件系统中生效.
4. 引用计数为0dentry仍然会保留在dcache中,但是同时也会被加入到super_blockLRU未使用链表中,当需要释放内存时,压缩 dentryslab回调函数shrink_dcache_memory()被调用,将LRU链表中最久未被使用的dentrydentry缓存中删除后,释放给slab分配器.
5. dentry被创建时其父dentry必然存在,且会增加父dentry的引用计数,dentry被销毁时会减少其父dentry的引用计数,如果其父dentry的引用计数被递减后变为0,那么其父dentry会被销毁,依次向上,直到文件系统的root dentry. Q:文件系统中有一个文件foo,假设文件系统同时挂载到/mnt1/和/mnt2/,那么/mnt1/foo和/mnt2/foo对应的dentry是同一个dentry吗?
A:是的,一个块设备可以被挂载到多处,每次挂载都会创建vfsmount,但是VFSsuper_block只有一个,dentry是属于文件系统的 而不是属于某个挂载点,唯一代表文件系统的是VFSsuper_block,因此,dentry结构体中有到super_block的指针,但是没有到 vfsmount的指针,/mnt1/foo和/mnt2/foo是同一个文件系统下的同一个文件,他们对应的dentry也必然是相同的,理解了dentry是属于文件系统的,就很容易理解为什么struct path的成员只有vfsmountdentry了.
Q:得到dentry后,可以沿着dentry->d_parent往上一直到系统的根目录吗?
A:如果dentry是属于根文件系统的,则可以.否则,如果dentry属于新挂载的文件系统,则不能,因为通过dentry是无法得知文件系统的挂载点的(如果文件系统只挂载了一次,通过dentry拿到文件系统的根dentry,找到vfsmount,获得挂载点也是一种办法).
Q:引用计数为0dentry会同时存在于super_block的未使用链表和dcache中,当通过do_lookup()函数从dcache中找到dentry时只是增加dentry的引用计数,为什么不把它从super_block的链表中删除?
AdentryLRU实现使用了lazy LRU,因为dentry的使用时间可能非常短(如stat一个文件),如果从dcahce中找到后,立即将dentryLRU中移除,使用完毕后,马上又要插入到LRU链表中,增加了链表操作的开销,由于LRU链表是受全局dcache_lock保护的,加剧对dcache_lock的争用.但是,这样一来又有一个问题,dentry的引用计数从0->1,到1->0的过程中,dentryLRU链表中的位置没有变化,且dput()(减少dentry的引用计数)时,dentry已经在LRU链表中,DCACHE_REFERENCED标志不会置位,shrink_dcache_memory()被调用时,可能被当作最久未使用dentry给释放掉,但是实际情况是,dentry刚刚才被使用,super_blockLRU链表明显没有达到效果?哥们的这个疑问还真是一个问题,这个问题已由Nick Piggin解决.

Nick Piggin 的解决方案:Nick Piggin 通过锁分离和延迟 LRU 更新优化了dentry的LRU管理逻辑,具体实现如下: 1. 引入 dcache_lru_lock 分离锁职责
  • 新增 dcache_lru_lock 专门保护 LRU 链表操作(如添加/删除节点)。
  • 原全局锁 dcache_lock 仅用于保护哈希表和目录项树结构。
  • 效果:减少全局锁的争用,提升并发性能 。
2. 延迟 LRU 链表更新 Lazy 更新策略:
  • 访问 dentry 时不再立即调整 LRU 链表,而是通过 状态标记(如 DCACHE_REFERENCED)记录其活跃性。
  • 仅在内存回收阶段(如 shrink_dcache_memory())根据标记动态调整 LRU 位置。
  • 效果:减少高频访问场景下的链表操作次数 。
  • 修复前(2.6.36 及更早)—— dput() 中的代码: if (list_empty(&dentry->d_lru)) { // 只有第一次进入 LRU 才执行 dentry->d_flags |= DCACHE_REFERENCED; // ← 只在这里设置 dentry_lru_add(dentry); } → 只有在 dput() 且 dentry 第一次加入 LRU(list_empty 为真)时,才设置 DCACHE_REFERENCED。 修复后(2.6.37 及之后,该补丁引入的版本): dentry->d_flags |= DCACHE_REFERENCED; // ← 关键改动:无条件设置 if (list_empty(&dentry->d_lru)) dentry_lru_add(dentry); → 只要 dentry 要保留在 LRU 上(即走到 dput 的 retain 路径),就一定设置 DCACHE_REFERENCED,无论是否是第一次进入 LRU。
Q: 调用unlink()删除文件时,经过sys_unlink()->do_unlinkat()->vfs_unlink()进入到 d_delete(),如果dentry还被其他进程使用(dentry->d_count>1),为什么d_delete()需要将 dentrydcache中移除?
A:如果dentry被其他进程引用,在d_delete()中还不能将dentry转化为负状态(dentry指向的inode为空),但是必须将dentry(记为dentry1,dentry1对应的inode记为inode1)从dcache中移除,否则,后续在同一个目录下创建相同文件名的文件时会从dcache中找到dentry1,导致引用inode1,出现新创建的文件就有数据的情况.
注:记为dentry1,dentry1对应的inode记为inode1 这句话的目的仅仅是为了区分新建的dentry和当前移除dentry 哈希表的这个dentry,所以标为dentry1,避免混淆2者。


dentry函数集如下:


struct dentry_operations {
	int (*d_revalidate)(struct dentry *, unsigned int);      // 验证目录项(dentry)是否仍然有效。
	int (*d_weak_revalidate)(struct dentry *, unsigned int); // 轻量级验证(RCU 路径下用)
	int (*d_hash)(const struct dentry *, struct qstr *);     // 哈希算法,用于 dcache 哈希表查找
	int (*d_compare)(const struct dentry *,                  // 比较两个文件名是否相等。
			unsigned int, const char *, const struct qstr *);
	int (*d_delete)(const struct dentry *);        // 删除dentry,count=1设为负状态,否则移除哈希表
	int (*d_init)(struct dentry *);                         // 初始化 dentry 的私有数据。
	void (*d_release)(struct dentry *);                     // 释放 dentry 占用的资源。
	void (*d_prune)(struct dentry *);                       // dentry将被从缓存中回收时通知文件系统。
	void (*d_iput)(struct dentry *, struct inode *);        // 释放dentry与其关联的inode之间的引用。
	char *(*d_dname)(struct dentry *, char *, int);         // 动态生成路径名。
	struct vfsmount *(*d_automount)(struct path *);         // 处理自动挂载(automount)点。
	int (*d_manage)(const struct path *, bool);             // 管理对目录的访问控制。
	struct dentry *(*d_real)(struct dentry *, const struct inode *);// 查找底层的“真实” dentry。
} ____cacheline_aligned;

dput     // dentry引用计数减1
__d_drop // 把dentry从dcache移除

//注:d_delete删除时 如果count=1 则设为负状态(d_inode设为null),不会直接删除,避免查找不存在的时候多次磁盘查找。如果count>1,则从哈希表移除,防止被后续在同一个目录下创建相同文件名的文件时会从dcache中找到该dentry,导致新创建的文件就有数据的情况。


2:inode(索引节点)

inode结构体如下:

struct inode {
        struct hlist_node       i_hash;              /* 哈希表 */
        struct list_head        i_list;              /* 索引节点链表 */
        struct list_head        i_dentry;            /* 目录项链表 */
        unsigned long           i_ino;               /* 节点号 */
        atomic_t                i_count;             /* 引用记数 */
        umode_t                 i_mode;              /* 访问权限控制 */
        unsigned int            i_nlink;             /* 硬链接数 */
        uid_t                   i_uid;               /* 使用者id */
        gid_t                   i_gid;               /* 使用者id组 */
        kdev_t                  i_rdev;              /* 实设备标识符 */
        loff_t                  i_size;              /* 以字节为单位的文件大小 */
        struct timespec         i_atime;             /* 最后访问时间 */
        struct timespec         i_mtime;             /* 最后修改(modify)时间 */
        struct timespec         i_ctime;             /* 最后改变(change)时间 */
        unsigned int            i_blkbits;           /* 以位为单位的块大小 */
        unsigned long           i_blksize;           /* 以字节为单位的块大小 */
        unsigned long           i_version;           /* 版本号 */
        unsigned long           i_blocks;            /* 文件的块数 */
        unsigned short          i_bytes;             /* 使用的字节数 */
        spinlock_t              i_lock;              /* 自旋锁 */
        struct rw_semaphore     i_alloc_sem;         /* 索引节点信号量 */
        struct inode_operations *i_op;               /* 索引节点操作表 */
        struct file_operations  *i_fop;              /* 默认的索引节点操作 */
        struct super_block      *i_sb;               /* 相关的超级块 */
        struct file_lock        *i_flock;            /* 文件锁链表,包含所有类型的锁 */
        struct address_space    *i_mapping;          /* 指向inode关联的address_space */
        struct address_space    i_data;              /* Radix Tree实例 */
        struct dquot            *i_dquot[MAXQUOTAS]; /* 节点的磁盘限额 */
        struct list_head        i_devices;           /* 块设备链表 */
        struct pipe_inode_info  *i_pipe;             /* 管道信息 */
        struct block_device     *i_bdev;             /* 块设备驱动 */
        unsigned long           i_dnotify_mask;      /* 目录通知掩码 */
        struct dnotify_struct   *i_dnotify;          /* 目录通知 */
        unsigned long           i_state;             /* 状态标志 */
        unsigned long           dirtied_when;        /* 首次修改时间 */
        unsigned int            i_flags;             /* 文件系统标志 */
        unsigned char           i_sock;              /* 可能是个套接字吧 */
        atomic_t                i_writecount;        /* 写者记数 */
        void                    *i_security;         /* 安全模块 */
        __u32                   i_generation;        /* 索引节点版本号 */
        union {
                void            *generic_ip;         /* 文件特殊信息 */
        } u;
};

// 对于普通文件、目录以及符号链接,i_mapping 指向自己的i_data, 对于块设备(如:/dev/sda),内核中可能存在多个inode指向这个设备。为了保证缓存一致性,所有这些inode的i_mapping 指针都会指向同一个“主 inode”(bdev_inode,代表块设备的那个 inode)的i_data。

inode 函数集如下:


struct inode_operations {
  // 在目录中查找一个文件或子目录,并填充 dentry 对应的 inode 信息。
	struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int); 

  // 获取符号链接的目标路径。
	const char * (*get_link) (struct dentry *, struct inode *, struct delayed_call *);
	int (*permission) (struct inode *, int);

  // 检查对 inode 的访问权限(读、写、执行等)。                   
	struct posix_acl * (*get_acl)(struct inode *, int);

  // 获取 inode 的 POSIX 访问控制列表(ACL)。

  // 读取符号链接的内容(即链接指向的路径)。
	int (*readlink) (struct dentry *, char __user *,int);

  // 创建一个普通文件(非目录、非设备)。
	int (*create) (struct inode *,struct dentry *, umode_t, bool);

  // 创建硬链接。
	int (*link) (struct dentry *,struct inode *,struct dentry *);

  // 删除一个文件(即删除目录项,减少硬链接计数)。
	int (*unlink) (struct inode *,struct dentry *);

  // 创建一个符号链接。
	int (*symlink) (struct inode *,struct dentry *,const char *);

  // 创建一个目录。
	int (*mkdir) (struct inode *,struct dentry *,umode_t);

  // 删除一个空目录。
	int (*rmdir) (struct inode *,struct dentry *);

  // 创建设备文件、FIFO 或套接字等特殊文件。
	int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);

  // 移动或重命名文件/目录。
	int (*rename) (struct inode *, struct dentry *,
			struct inode *, struct dentry *, unsigned int);

  // 设置 inode 的属性(如权限、所有者、大小、时间戳等)。
	int (*setattr) (struct dentry *, struct iattr *);

  // 获取 inode 的属性(如大小、权限、时间等),填充到 struct kstat 结构中。
	int (*getattr) (const struct path *, struct kstat *, u32, unsigned int);

  // 列出文件的扩展属性(xattr)名称列表。
	ssize_t (*listxattr) (struct dentry *, char *, size_t);

  // 获取文件的物理盘区(extent)映射信息。
	int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
		      u64 len);

  // 更新 inode 的时间戳(atime、mtime、ctime)。
	int (*update_time)(struct inode *, struct timespec64 *, int);

  // 原子地执行打开和创建操作。
	int (*atomic_open)(struct inode *, struct dentry *,
			   struct file *, unsigned open_flag,
			   umode_t create_mode);

  // 创建一个临时文件(类似于 O_TMPFILE)。
	int (*tmpfile) (struct inode *, struct dentry *, umode_t);

  // 设置 inode 的 POSIX ACL。
	int (*set_acl)(struct inode *, struct posix_acl *, int);
} ____cacheline_aligned;

inode是文件的元信息,如上,inode存储了文件的各种属性、权限信息、以及文件各个数据块的存储位置。

inode和dentry是一对多的关系,一个inode可以对应多个dentry,比如文件和硬链接虽然对应不同的dentry结构,但其对应的inode是同一个,inode通过i_dentry链表链接起了对应的所有dentry的d_alias成员,从而实现了inode和dentry之间1对多的映射关系。但反过来一个dentry只能对应一个inode,是通过d_inode成员指向对应的inode结构体。


3:super_block(超级块)

代表了文件系统的元信息,inode和data是相对于文件来说的,而superblock则是相对于文件系统来说的,存储了已使用和未使用的inode和data信息,以及block的大小和块组、文件系统类型、文件系统挂载等信息。

超级块的结构体如下:

struct super_block
{ 
/************描述具体文件系统的整体信息的域*****************/
   kdev_t s_dev;                      /* 块设备标识符。例如,对于 /dev/hda1,其设备标识符为 0x301 */
   unsigned long s_blocksize;         /* 该具体文件系统中数据块的大小,以字节为单位 */                                                                       
   unsigned char s_blocksize_bits;    /* 块大小的值占用的位数,例如,如果块大小为1024字节,则该值为10 */  
   unsigned long long s_maxbytes;     /* 文件的最大长度 */
   unsigned long s_flags;             /* 安装标志*/
   unsigned long s_magic;             /* 魔数,即该具体文件系统区别于其它文系统的一个标志 */ 
   unsigned long s_dquot;             /* 磁盘限额相关选项 */ 
   unsigned long s_instances;         /* 连接这个双链表的连接点,同一类型的文件系统通过这个子墩将所有的super_block连接起来 */ 
   struct  block_device *s_bdev;      /* 指向文件系统被安装的块设备 */ 
   struct  backing_dev_info *s_bdi;   /* 底层存储设备信息 */ 

/**************用于管理超级块的域******************/
    struct list_head   s_list;        /* 指向超级块链表的指针 */  
    struct semaphore   s_lock         /* 锁标志位,若置该位,则其它进程不能对该超级块操作 */
    struct rw_semaphore  s_umount     /* 对超级块读写时进行同步,用于保护整个超级块 */
    unsigned char s_dirt;             /* 脏位,若置该位,表明该超级块已被修改 */
    struct dentry  *s_root;           /* 指向该具体文件系统安装目录的目录项。*/
    int          s_count;   /* 对超级块的引用计数, 引用(指向)超级块前+1, 防止超级块内存被释放 */
    atomic_t     s_active   /* 对超级块的操作计数, 操作(查询/更新)超级块前+1, 防止文件系统被卸载 */                
    struct list_head  s_dirty;        /* 已修改的索引节点形成的链表,新版本合入s_bdi中 */
    struct list_head  s_locked_inodes;/* 要进行同步(fsync,sync,fdatasync)的索引节点形成的链表 */
    struct list_head  s_inodes;       /* 管理所有inode的哈希表 */
    struct list_head  s_files         /* 用于管理与该超级块关联的所有打开文件,新版本内核已移除 */

/***********和具体文件系统相联系的域*************************/
   struct file_system_type *s_type;</span>   /* 指向文件系统的file_system_type 数据结构的指针 */
   struct super_operations *s_op;     /* 指向某个特定的具体文件系统的用于超级块操作的函数集合 */
   struct dquot_operations *dq_op;    /* 指向某个特定的具体文件系统用于限额操作的函数集合 */
   u;     /*一个共用体,其成员是各种文件系统的 fsname_sb_info数据结构 */
};

如下图:super_block存在于两个链表中,一个是系统所有super_block的链表, 一个是对于特定的文件系统的super_block链表,所有的super_block都存在于 super_blocks(VFS管理层) 链表中。

superblock全局链表

对于特定的文件系统(文件系统层的具体文件系统), 该文件系统的所有的super_block 都存在于file_sytem_type中的fs_supers链表中.

而所有的文件系统都存在于file_systems链表中,这是通过调用register_filesystem接口来注册文件系统的。

int register_filesystem(struct file_system_type * fs)

superblock特定文件系统链表


4:file结构体

每个打开的文件在内核中都由file结构体表示,里面记录了打开文件的各种信息,它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。

file结构体如下:

struct file {
    ...
    spinlock_t              f_lock;       /* 自旋锁 */
    fmode_t                 f_mode;       /* 文件模式 */
    atomic_long_t           f_count;      /* 文件引用计数 */
    struct mutex            f_pos_lock;   /* 互斥锁 */
    loff_t                  f_pos;        /* 当前文件读写位置 */
    unsigned int            f_flags;      /* 文件标志 */
    struct fown_struct      f_owner;      /* 文件所有者 */
    struct path             f_path;       /* 文件路径,包含dentry结构体和vfsmount结构 */
    struct inode           *f_inode;      /* 指向inode结构体的指针 */
    const struct file_operations *f_op;   /* 文件操作方法集 */
    u64                     f_version;    /* 版本号 */
#ifdef CONFIG_SECURITY
    void                   *f_security;   /* 存储与文件安全相关的数据 */
#endif
    void                   *private_data; /* 私有数据 */
    struct address_space   *f_mapping;    /* 指向address_space结构体 */
 
} __randomize_layout
  __attribute__((aligned(4)));    /* lest something weird decides that 2 is OK */

5:文件描述符表

5.1 文件描述符表结构:

  
 struct fdtable {
	unsigned int max_fds;         /* 表示当前文件描述符表的最大容量 */
	struct file __rcu **fd;       /* 指向动态分配的文件指针数组 */
	unsigned long *close_on_exec; /* 指向一个位图,用于记录哪些文件描述符在执行exec时需要关闭 */
	unsigned long *open_fds;      /* 指向一个位图,用于记录哪些文件描述符当前被打开 */
	unsigned long *full_fds_bits; /* 指向一个位图,用于记录哪些文件描述符当前被使用 */
	struct rcu_head rcu;          /* RCU 头,用于在 RCU 保护下更新文件描述符表 */
};
  

文件描述符表在files_struct中的位置:

  
  struct files_struct {
  /*
   * read mostly part
   */
	atomic_t count;                        /* 引用计数 */
	bool resize_in_progress;               /* 标志位,用于判断文件描述符表是否正在进行扩容 */
	wait_queue_head_t resize_wait;         /* 等待队列头,用于等待文件描述符表扩容完成 */

	struct fdtable __rcu *fdt;             /* 指向文件描述符表的指针 */
	struct fdtable fdtab;                  /* 文件描述符表 */
  /*
   * written part on a separate cache line in SMP
   */
	spinlock_t file_lock ____cacheline_aligned_in_smp; /* 文件描述符表的自旋锁 */
	unsigned int next_fd;
	unsigned long close_on_exec_init[1];           
	unsigned long open_fds_init[1];                
	unsigned long full_fds_bits_init[1];           
	struct file __rcu * fd_array[NR_OPEN_DEFAULT];    /* 指向文件指针的数组 */
};
  

5.2 fd_array:

上图是struct file __rcu * fd_array[NR_OPEN_DEFAULT]示意图, 它是 task_struct-> files_struct结构体中默认的内置静态数组,NR_OPEN_DEFAULT值在64位系统是64,32位系统是32,数组的key是文件描述符,value是文件描述符对应的file结构体(内核中每个被打开的文件用该结构体表示),主要用于小规模文件描述符存储,避免动态内存分配。


5.3 open_fds和full_fds_bits:

open_fds和full_fds_bits示意图

full_fds_bits是一个位图,主要用于记录哪些文件描述符当前被使用,0代表未使用,1代表已经使用。但不代表具体的一个文件描述符而是它指向的一组文件描述符,如上图所示的0-31,如果0-31的描述符都被使用,那么full_fds_bits对应的位才会被置为1,否则为0。


open_fds也是一个位图,主要用于记录full_fds_bits指向的一组位图中详细的文件描述符使用情况,使用就置1未使用则置0。


full_fds_bits 和 open_fds结合使用主要为了分配新文件描述符的时候提升查询效率,遍历full_fds_bits的时候如果该位为1则直接跳过查找下一组,这样可以极大提升查找效率。


5.4 文件描述符表的扩容流程:

当分配文件描述符的时候无空闲文件描述符时会触发扩容,扩容过程是新建一个更大的表,然后把旧表的数据拷贝到新表,然后通过*fdt指针原子替换新旧表访问地址。会同时扩容full_fds_bits,open_fds,fd_array,close_on_exec和其他相关成员。

1:当超过64时,会直接扩容到1024;2:超过1024后后续进行翻倍扩容策略,1024 -> 2048 -> 4096依此类推。


6:dentry_hashtable

dentry_hashtable是全局表,是所有file结构体全局共享的

注:文件系统的根dentry不在dentry_hashtable中,根dentry的特殊性源于其父指针指向自身,导致哈希值计算异常,同时作为目录树的起点,其子目录数量通常较少,无需哈希表加速查找。


dentry_hashtable: 是以(父目录的dentry指针和文件名的哈希值)为hash key


7:inode_hashtable

inode_hashtable是全局表,是所有file结构体和dentry全局共享的


inode_hashtable: 是以(super_block指针 *sb, inode编号i_ino) 组合为hash key


8:VFS各结构关系图

vfs关系图


9:文件系统分类

常规文件系统:(ext4, xfs, btrfs, …)

网络文件系统:(NFS, CIFS, …)

伪文件系统:(procfs, sysfs, …)

特殊文件系统:(tmpfs, devtmpfs, …)


三、path_walk过程

path_walk是通过文件路径查找其在vfs中dentry和inode的过程,查找过程如下图:

path_walk详细过程

path_walk详细过程

1:打开一个文件的调用链

open->sys_open->do_sys_open->do_filp_open->path_openat

path_openat会依次执行 alloc_empty_filp->path_init->link_path_walk->do_last->terminate_walk 从而完成路径的查找。

path_walk详细过程

2:do_sys_open

do_sys_open()函数首先调用build_open_flags()将传递进来的flags进行解析并存在op中,并调用getname把用户态的文件路径字符串拷到内核态,接着调用get_unused_fd_flags()获取一个可用的文件描述符fd,接着调用do_filp_open()创建文件结构f,并通过fd_install()将f其和文件描述符fd关联起来。这里的文件结构f即上文所述的结构体file。

get_unused_fd_flags()函数实际调用__alloc_fd()函数,这里传参files_struct来源于当前运行的task_struct中的files指针,该结构体最关键的是携带了文件描述符表fdtable。

对于任何一个任务,默认情况下文件描述符 0 表示 stdin 标准输入,文件描述符 1 表示 stdout 标准输出,文件描述符 2 表示 stderr 标准错误输出,除此之外打开的文件都会从这个列表中找一个空闲位置分配给它。文件描述符列表的每一项都是一个指向 struct file 的指针,也就是说每打开一个文件都会有一个 struct file 对应。

传入文件描述符表后,首先将fd赋值为files->next_fd,然后通过find_next_fd()去检查是否可用,如果不可用则会继续自增直至找到可用的文件描述符。找到之后,会将files->next_fd赋值为fd + 1以备下次使用,最后调用__set_open_fd()将fd位表的修改赋给fdt并保存。

// 文件描述符表的扩充:新申请一张更大的表,然后把旧表的数据拷贝到里面。


3:alloc_empty_filp

调用alloc_empty_filp() 生成一个 struct file 结构体,实际最终调用kmem_cache_alloc()分配,即采用前文所述的slab分配器分配;


4:path_init简述

1:nd(nameidata)初始化

2:确定查找的起点,起点的path(vfs_mount和dentry)和inode。

nameidata起到了很重要的作用:

1. 向查找函数传递参数;

2. 保存查找结果

nameidata是个临时结构,主要用来临时保存查询过程中的数据

struct nameidata {
    struct path path;                 //存储文件挂载点和dentry地址
    struct qstr last;                 //路径名最后分量
    struct path root;                 //存在文件所在文件系统根的信息
    struct inode    *inode;           //path.dentry.d_inode 
    unsigned int    flags;            //标志
    unsigned        seq, m_seq;       //seq 是相关目录项的顺序锁序号; m_seq 是相关文件系统(其实是mount)的顺序锁序号
    int             last_type;        //路径最后的文件类型
    unsigned        depth;            //解析符号链接过程中的递归深度
    char *saved_names[MAX_NESTED_LINKS + 1];    //相应递归深度的符号链接的路径
};

//其中type的取值是一个枚举类型,如下:
enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT, LAST_BIND};
//分别代表了普通文件,根文件,.文件,..文件和符号链接文件


5:path_init详细过程:

1:初始设置nd->last_type= LAST_ROOT,搜索路径名的过程中,值会根据情况改变,如:enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT, LAST_BIND} ,如果最后停留在点上,用LAST_DOT记录,如果成功找到了这一级的目标文件,就用LAST_NORM表示正常。


2:然后查看条件(flags & LOOKUP_ROOT)是否满足,当root目录的数据结构struct path不为空时,flag的这个bit才会拉高,此时从根目录开始搜索,一般不会满足。


3:调用read_seqbegin(&mount_lock),获取一个顺序锁,它给写操作赋予了更高的优先级,在使用顺序锁时即便正在进行读操作,写动作也可以进行。其优点在于写者永远不会由于有读者正在进行读而等待,其缺点在于读者可能需要尝试读好多次才能读到合法的数据。


4:判断路径名nd->name->name是否是根目录开始搜索,如果是,在不考虑RCU的情况下(为方便分析,不考虑RCU的链表搜索方式),调用set_root->get_fs_root首先设置nd->root=current->fs->root,然后调用path_get->mntget->mnt_add_count,将nd->root->mnt->mnt_count加1,由于每当将一个设备mount到某个节点时,内核都要为其建立一个vfsmnt结构,该结构包含设备信息和节点信息。因此此处加1表示在整个VFS下,搜索到当前挂载在此目录下的数目加1。同时调用path_get-> dget->lockref_get将nd->root->dentry->d_lockref->count加1,表示当前dentry结构的用户增加了一个。


5:接步骤4的判断,如果不是根目录的情况下,(nd->dfd== AT_FDCWD)条件满足,表示使用相对目录,则调用get_fs_pwd(current->fs,&nd->path)。代码如图所示
首先将当前进程的当前目录的struct path pwd作为入参传给path_get。之前介绍过pwd的作用,此处正是利用了pwd的dentry和mnt结构中存储的信息来做相对路径搜索的初始化。path_get函数在上一步已经分析过。


6:如果某目录已经被当前进程打开,则根据文件描述符fd.file->f_path.dentry找到dentry,且nd->path= f.file->f_path,最后调用path_get(&nd->path)做类似步骤4,5的操作。


7:最后设置nd->inode= nd->path.dentry->d_inode。 

tips:

last_type 一共有五种类型:
enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT, LAST_BIND};
LAST_NORM 就是普通的路径名;LAST_ROOT 是 “/”;LAST_DOT 和 LAST_DOTDOT 分别代表了 “.” 和 “..”;LAST_BIND 就是符号链接


查找起点路径的判断逻辑:
/*如果flags设置了LOOKUP_ROOT标志,则表示该函数被open_by_handle_at函数调用,该函数将指定一个路径作为根;这属于特殊情况,这里暂不分析;*/

1. 文件名包含绝对路径,因此我们优先使用文件系统的根目录作为查找起始点
2. 路径不是绝对路径,我们指定从当前目录开始开始查找
3. 函数第一个参数@dfd是一个目录文件描述符,我们就从这个目录开始查/* LOOKUP_PARENT:

LOOKUP_PARENT:目标是找到最终文件的父目录
LOOKUP_JUMPED:用于检查'jumped'的dentry,即那些不是通过lookup获取的dentry,如'', '.'或者'..'。这种场景只需要检查dentry对应inode是否OK即可。该函数不会在rcu-walk模式下调用,所以可以放心的使用inode。*/


6:link_path_walk过程简示

6.1 查找流程:

| | |-->may_lookup # 查询文件权限是否允许访问。
| | |-->hash = init_name_hash(); # 算出该文件名的哈希值,和文件名长度。
| | |-->## 判断文件名是否使用了"."或者"..",来标明文件类型type ##
| | |-->d_hash # 重新计算hash值
| | |-->walk_component # 依据刚刚识别的类型,做单次搜索。
| | | |-->handle_dots # "." 和 ".." 文件名处理。
| | | |-->do_lookup # 其他文件的搜索。
| | | | |-->__d_lookup_rcu # 带rcu搜寻。
| | | | |-->__d_lookup # 不带rcu,可能引起阻塞搜寻。
| | | | |-->d_alloc_and_lookup # 上两步搜不到,就要通过硬盘文件系统搜寻。
| | | |-->should_follow_link # 查看是否可以继续链接文件,前面提到过,对链接次数有限制。
| | |-->nested_symlink # 限制递归调用不能超过8次,符号链接不能超过40次。
| | |-->can_lookup # 判断是否可以继续查找,可以则继续。
| | |-->terminate_walk(nd); # 查找完成操作,包括解RCU锁。

/* 1:enum { MAX_NESTED_LINKS = 8 };符号链接的嵌套(递归)层数不能超过8(< 8)示例: a->b->c->d->e->f->g->h(都在路径里,且层层嵌套,如左侧示意,a指向b,b指向c依此类推,且自身都是符号链接)

2:#define MAXSYMLINKS 40在一个路径中符号链接的总数——不能超过 40 个(出现在路径里的符号链接总数不能超过40个(<40)) */


6.2 执行结果:

执行完该接口后,若查找成功,其返回打开文件的父目录的dentry、inode等变量值

RCU机制简述:

RCU机制总结

RCU机制的实现细节:
RCU机制通过复制被保护数据结构的副本来实现读写并发。当写者需要修改数据时,它会先复制数据的一个副本,并在副本上进行修改,而不会直接修改原始数据。
修改完成后,写者会在合适的时机(如所有读者都完成了对原始数据的访问后)将修改后的副本替换回原数据。这个替换过程通常是通过原子操作完成的,以确保数据的一致性和完整性。

RCU机制的适用场景:读多写少的场景,像文件目录这种极少修改的内容,是RCU的极佳使用场景
除了读多写少的场景外,RCU机制还适用于那些对读取性能要求很高,但对数据一致性要求不是非常严格的场景。例如,文件系统的目录结构、网络配置信息等。在这些场景下,即使读者读取到的是旧的数据版本,也不会对系统的整体性能和稳定性造成太大的影响。

RCU机制优势::
与传统的锁机制相比,RCU机制在读写并发性能方面具有明显的优势。它允许读者和写者并发地访问和修改数据,而无需等待锁释放或进行上下文切换。

RCU机制缺点:如果读取的数据正在被修改,可能需要多次读取才能读取到最新数据,
在数据一致性要求非常高的场景下,RCU机制可能并不是一个很好的选择此外,RCU机制的实现相对复杂,需要仔细处理各种同步和一致性问题。

综上所述,RCU机制是一种高效的并发控制机制,适用于读多写少、对读取性能要求高但对数据一致性要求不是非常严格的场景


7:link_path_walk完整过程

1:首先判断第一个路径名的第一个字符是否是“/”,如果是则name指针自加直到暴露出非“/”的字符,因为“//home”是等于“/home”的。


2:调用may_lookup(nd)查看存放的inode的运行线程的特权是否是MAY_EXEC|MAY_NOT_BLOCK,对于中间节点所需权限为MAY_EXEC。如果权限检查未通过(返回-ECHILD),则调用may_lookup->unlazy_walk清除flags中的LOOKUP_RCU属性,通过非RCU模式执行查找。


3:调用hash_name,如果输入路径为“/home/usr”,经过前面处理,此时变为“home/usr“,调用hash_name的目的在于计算”home“中每个字符的hash值和字符总长度,通过条件while(c && c != '/'),将home的长度提取出来。


4:在开始搜索之前假定type= LAST_NORM,然后判断name[0]== '.',再判断name[1]== '.',如果都满足,则标记type= LAST_DOTDOT,后文将向上级目录搜索。如果第二条不满足,则标记type= LAST_DOT,表示搜索到一个隐藏文件或者表示当前目录。


5:如果经过步骤4,type ==LAST_NORM仍然成立,说明不需要往上搜索,首先清除nd->flags&= ~LOOKUP_JUMPED,然后确定nd->path.dentry->d_flags的DCACHE_OP_HASH(DCACHE_OP_HASH 标志用于指示dentry对象是否需要使用自定义的哈希函数进行哈希处理,而不是使用默认的哈希逻辑)bit是否拉高,拉高则需要调用nd->path.dentry->d_op->d_hash来重新计算hash值,有些情况下搜索过程可能会跳到另一个文件系统中去,所以会有重新计算的需求。

最后对nd->last赋值。在path_init已经分析过,nd->path存放当前进程已经完成的搜索路径,进程可能之前正在解析被打断了,或者进程在递归解析都需要用nd->path做临时存储。如果等待解析的目录以仍然以“/“开头,说明是第一次从根目录解析,则其保存的是nd->root,否则其保存的是current->fs->pwd。nd->last存放待解析的下级分量信息。此时nd->last.name=”home/usr”


6:此时还需进一步判断name指针偏移hash_len之后是否为“/0“,表示路径到头了,如果初始需要搜索的路径为”/home“就是这种情况,此时直接跳到”OK“标签处的代码,如果不为空,则自加,防止home///usr的情况,此时则自加三次,name指针指向”usr“,为下次搜索做好准备。


7:接着步骤6来看下OK标签处的代码,首先判断!nd->depth,表示符号递归深度为0,解析完成了,直接退出;再查看nd->stack[nd->depth- 1].name,表示相应递归深度对应的符号链接,如果递归深度不为0,但对应的符号链接为0,也可以直接退出了。

//说明已经到文件路径的末尾了,无法再往后遍历了


8:如果没有退出则调用walk_component进行搜索,这个API非常重要。


8.1:首先检查nd->last_type!= LAST_NORM,步骤4中分析过type的值遇到“.“时会做一些标记,如果不是LAST_NORM就要调用handle_dots->follow_dotdot处理点的问题,由于一个点只是表示隐藏文件或者当前目录,隐藏文件按正常搜索处理即可,当前目录可以不做任何处理。两个点就表示上一级了。

8.1.1:首先判断(nd->path.dentry== nd->root.dentry && nd->path.mnt == nd->root.mnt)表示当前已经解析的目录(nd->path存储当前进程已经解析过的路径,如果是根目录,表示对类似“/home/usr“的解析才开始)就是根目录,再往上无意义了,比如“/../“就是这种情况。所以不修改nd->path的值,继续处理下一个要解析的路径。

8.1.2:其次判断(nd->path.dentry != nd->path.mnt->mnt_root)说明当前节点与父节点在同一设备(文件系统)上,无需处理挂载点,调用nd->path.dentry= dget_parent(nd->path.dentry)即可获取上一级的dentry。

8.1.3:接着,如果上述条件满足(即:nd->path.dentry == nd->path.mnt->mnt_root)了说明到当前文件系统的根了,就需要处理挂载点。首先获取path->mnt->mnt_parent,即获取父设备的vfsmount结构,同时更新mountpoint= dget(path->mnt->mnt_mountpoint),获取安装点的dentry结构,最后更新给path->mnt和path->dentry。

8.1.4:最后处理特殊挂载点的情况,如果一个挂载点挂载了多个文件系统就需要进行特殊处理。特殊处理方法会层层穿越,直到获取最初(第一个)那个挂载点,然后根据挂载点信息更新相关参数。

8.1.5:如果rcu-work(follow_dotdot_rcu)失败,就会转入ref-work搜索(follow_dotdot)函数进行处理。


8.2:处理完带点的情况,检查if(flags & WALK_PUT),如果为高则调用put_link(nd)释放nd中的链接数据。步骤7中展示过,递归深度为0或者无链接符号时便是这种情况。


8.3:调用lookup_fast。lookup_fast在之前的内核中叫cached_lookup,意思就是在内存中寻找已经建立起来的dentry结构。内核中有一个hash表—dentry_hashtable,他是一个list_head指针数组,一旦在内存中建立起一个dentry数据结构,就根据其节点(比如“/home/usr”中,/home和/home/usr都是一个节点)的hash值将其加入dentry_hashtable。所以使用link_path_walk查找dentry的过程中,当前节点的上游节点都已有dentry位于内存中。

使用lookup_fast查找内存中路径的过程又分RCU模式__d_lookup_rcu和非RCU模式__d_lookup两种场景,RCU模式为无锁操作,性能比非RCU模式高,但是RCU模式不能保证读到的数据是最新的,所以存在lookup失败的可能,如果失败的话,会通过unlazy_wlak清除flags中的LOOKUP_RCU属性,通过非RCU模式__d_lookup再执行一次查找。如果内存中找不到当前节点的dentry结构,则进一步通过lookup_slow去硬盘上上寻找到inode信息,并组装dentry结构,然后挂入哈希表。

内核中还有一个队列dentry_unused来记录用户数目为0 (步骤4中分析过nd->root->dentry->d_lockref->count表示该dentry用户数)的dentry结构。这是一个LRU队列(Leastrecently used,最近最少使用),当内核做内存回收时,将从LRU中回收最先放入其中的dentry。调用dput()API将回收dentry的计数器清0。Dentry中总共有6个list_head结构,list_head既可以用作一个队列的头部,也可以挂入到其他队列中,例如list_headd_hash可以挂入内核中的dentry_hashtable。

有了以上基础认识再来看lookup_fast,由于步骤2中已经调用may_lookup->unlazy_walk清除flags中的LOOKUP_RCU属性,因此此处直接调用__d_lookup,首先调用d_hash(parent,name-> hash)重新计算name的hash值,计算时带上了parent节点的dentry(此处为nd->path.dentry,即已经处理过的完整路径),这样做的原因是例如在计算机机房里很多学生都在/home/xxx(各自的姓名)下面创建了project,那么在该dentry的list_headd_hash中,将有很多个成员,这个d_hash挂入内核中的dentry_hashtable中后,搜索这个节点将有很多线性搜索,加上parent dentry重新hash可以减少哈希碰撞接下来就根据hash值在内存中寻找dentry,如果dentry->d_name.hash== name->hash,则表示找到了想要的dentry,然后检查dentry的d_parent,d_flags,d_flags等成员是否符合预期,如果都正确,则dentry->d_lockref.count++,将dentry用户再加1。然后返回lookup_fast,如果dentry为空,则拉高need_lookup,表示需要lookup_slow。同时还要调用d_revalidate(dentry,nd->flags),防止在搜索过程中,内存保存的dentry又失效了,如果不幸失效,调用d_invalidate(dentry)处理。
最后还要调用d_is_negative(dentry)。

如果以上一切都通过,则执行path->mnt= mnt; path->dentry = dentry;表示找到待处理的路径名对应的dentry和vfsmount结构。接下来调用follow_managed处理dentry中mount相关的flag,比如是否是automount,是否需要手动mount。最后调用*inode= d_backing_inode(path->dentry)获取dentry对应的inode。


8.4:如果lookup_fast搜索失败,那就只有lookup_slow再来一次了。

8.4.1:首先调用lookup_dcache->d_lookup->__d_lookup再来一次fast搜索,万一这间隙,其他线程创建了dentry呢?

8.4.2:然后才调用lookup_real中的API:nd->path.dentry->d_inode ->i_op->lookup(nd->path.dentry->d_inode, dentry,flags),即已搜索路径的dentry对应的inode中的操作函数lookup,入参是nd->path.dentry->d_inode。对于EXT4来说,这个API是ext4_lookup。
其中最重要的两个调用是ext4_find_entry和ext4_iget_normal,首先看ext4_find_entry,其目的是找到目录名的dentry,并返回目录所在的page cache对应的bufferhead。

8.4.2.1:首先从调用sb= dir->i_sb,即从nd->path.dentry->d_inode获取super block,dentry中的super block都是从父节点继承过来的。
8.4.2.2:如果节点名字太长,需要调用ext4_fname_setup_filename(dir,d_name, 1, &fname)使用fname额外存储名字。
8.4.2.3:如果有内联数据,调用ext4_has_inline_data处理。内联数据的特性,可以有效的减少磁盘次数,对于小文件的处理可以提高很大的性能。原始的ext4文件所有数据采用的都是blocks的map方式在逻辑块和物理块之间的转换,小文件为包括字节数一般为几十个字节,会带来很多碎片。采用inline data的方式,会将文件的数据直接放在inode的后面,此时的inode为扩大的inode,需要进行扩大处理。
8.4.2.4:如果路径名中出现了"."or "..",则设置block= start = 0;nblocks = 1然后直接跳到restart处。
8.4.2.5:再来看下restart标记的while循环,其功能是执行硬盘读数据操作。首先调用cond_resched主动让出cpu,为接下来从硬盘拷贝数据做准备。由于硬盘的物理特性,读一个记录块很消耗时间,而且大部分消耗在准备工作上,因此读一个记录块与读几个记录块时间起始差不多,所以读硬盘最好的办法就是预读一些记录块,EXT4最多预读八个记录块,读回来的数据会用buffer来管理。因此代码中使用bh_use[8]来存储预读回来的8个记录块的bufferhead指针。读数据的API是ext4_getblk,其返回bufferhead指针。
8.4.2.6:由于CPU读数据是异步,因此调用wait_on_buffer等待记录块到位。然后就带着bh返回上一级。

8.4.3:ext4_find_entry返回后,获得了路径名在磁盘上数据对应的bufferhead指针。有了bh即有了raw_inode,然后就调用ext4_iget_normal->ext4_iget(sb,ino)在内存中组织起inode结构,和dentry一样,构建好inode后,计算一个hash值并加入列表。

8.4.4:上一步建立了inode,然后就需要调用d_splice_alias中的security_d_instantiate->call_void_hook将inode和dentry绑定。最好还要调用d_rehash(dentry)将dentry计算hash值并加入内核中的dentryhash列表。
到这一步,dentry终于在内存中建好了,返回walk_component中进行步骤8.5。


8.5:调用should_follow_link(nd,&path, flags & WALK_GET, inode, seq),如果当前节点是一个链接,那么should_follow_link将会从具体的EXT4文件系统回到VFS上去,因为不知道链接的目标是不是另外一个文件系统。然后接下来的动作就和前面的path_init和link_path_walk类似。


8.6:调用path_to_nameidata(&path,nd)将dentry信息传入nameidatand中。


至此,对路径名中的一个节点搜索就完成了,回到link_path_walk中执行循环for(;;),直到所有节点都搜索完成。

在Kernel 中任何一个常用操作都会有两套以上的策略,其中一个是高效率的(lookup_fast),另一个效率低但是搜索的成功率高(lookup_slow)。

Kernel 会在rcu-walk 模式下会首先进入 lookup_fast 进行尝试,如果失败了那么就尝试就地转入 ref-walk,如果还是不行就回到 do_filp_open 从头开始。

Kernel在 ref-walk 模式下会首先在内存缓冲区查找相应的目标(lookup_fast),如果找不到就启动具体文件系统自己的 lookup 进行查找(ext4_lookup)。因此,在 rcu-walk 模式下不会进入lookup_slow 。如果有权限问题或者不适合ref-walk mode,将中止搜索,前者原因好理解,后者意思是如果rcu-walk mode找不到(这种情况概率挺大的),又无法使用ref-walk mode,将报找不到文件的错误。

LOOKUP_JUMPED:用于检查'jumped'的dentry,即那些不是通过lookup获取的dentry,如'', '.'或者'..'。这种场景只需要检查dentry对应inode是否OK即可。该函数不会在rcu-walk模式下调用,所以可以放心的使用inode。


8:do_last

执行流程示意:

| -->do_last
| | -->walk_component
| | -->mnt_want_write # 获取对文件系统的写访问权限
| | -->lookup_open
| | |-->d_lookup # 在内存的目录项缓存中查找
| | |-->如果没有找到,调用dir_inode_i_op->lookup()进行查找
| | |-->如果两次都没有找到
| | | | |-->may_o_create # 检查是否有创建文件的权限
| | | | |-->dir_inode_i_op->create() #创建文件
| | | | |-->fsnotify_create # 通告创建文件事件
| | -->may_open # 检查访问权限
| | -->vfs_open
| | |-->do_dentry_open
| | | |-->调用文件操作集合的open方法| | -->mnt_drop_write # 放弃对文件系统的写访问权限


执行结果:
1:获取文件对应的 inode 对象,并且初始化 file 对象

2:若打开的文件为一个链接文件则do_last直接返回,由path_openat中的接下来的函数调用follow_link接口,对链接文件对应的target文件路径进行查找,并返回查找文件的父目录对应的dentry、inode,接着调用do_last进行文件的打开操作


详细过程:

完成了最后一部分的解析和处理工作。首先调用lookup_fast()查找文件路径最后一部分对应的dentry,接着使用lookup_open()判断是否需要创建新的dentry,最终将dentry赋值给path。最后调用vfs_open()真正的打开文件

vfs_open:

| 设置 f->f_inode = inode;
| 设置 f->f_op的值为inode->i_fop;
| 设置 f->f_mapping 为inode->i_mapping;
| 执行f->f_op->open

将dentry赋值给path处理细节延伸:

do_last() 的最后一步是调用 vfs_open() 真正打开文件,实际调用 f_op->open,也就是调用实际文件系统的open方法,如:ext4_file_open()。另外一件重要的事情是将打开文件的所有信息填写到 struct file 这个结构里面,从而完成了整个打开的过程。

| path->mnt= mnt; path->dentry = dentry; 表示找到待处理的路径名对应的dentry和vfsmount结构。

| 接下来调用follow_managed处理dentry中mount相关的flag,比如是否是automount,是否需要手动mount。

| 最后调用*inode= d_backing_inode(path->dentry)获取dentry对应的inode

vfs_open()处理细节延伸:

vfs_open()函数内核最终都会调用到do_dentry_open()函数,来完成文件打开的操作。而do_dentry_open()函数里面会找到inode的i_fop成员变量,该成员变量也是一个指向文件操作集的指针,其中就包括 open() 函数,即最终是调用inode->i_fop->open,而后面的操作就和具体的文件系统相关了。

那inode->i_fop的值又是在哪里设置的呢?
对于具体的文件系统:是在挂载文件系统的时候设置的,设置文件系统根inode的i_fop,后面的都是从这个inode里面直接继承。
对于裸设备:是初次打开设备创建inode的时候在init_special_inode方法里设置的。

// fs/inode.c

具体文件系统设置i_fop:

调用链:do_mount() -> vfs_kern_mount -> mount_fs() -> 根据*type(*type是get_fs_type找到的具体文件系统结构体)获取其mount方法 -> type->mount() -> 实际就是调用ext4_mount -> ext4_mount -> mount_bdev -> ext4_fill_super -> ext4_iget

ext4_iget函数详解:

1. 给根目录inode赋值:

inode->i_op = &ext4_dir_inode_operations;

inode->i_fop = &ext4_dir_operations; //这里便是VFS重点所在

  
  const struct inode_operations ext4_dir_inode_operations = {
	 .create		= ext4_create,
	 .lookup		= ext4_lookup,
	 .link		= ext4_link,
	 .unlink		= ext4_unlink,
	 .symlink	= ext4_symlink,
	 .mkdir		= ext4_mkdir,
	 .rmdir		= ext4_rmdir,
	 .mknod		= ext4_mknod,
 	 .tmpfile	= ext4_tmpfile,
	 .rename		= ext4_rename2,
	 .setattr	= ext4_setattr,
	 .getattr	= ext4_getattr,
	 .listxattr	= ext4_listxattr,
	 .get_acl	= ext4_get_acl,
	 .set_acl	= ext4_set_acl,
	 .fiemap         = ext4_fiemap,
 };
   

2. 打开文件的赋值方式:

2.1 调用inode->i_op->ext4_create去创建文件

ext4_create函数会先创建文件对应的inode

inode = ext4_new_inode(handle, dir, mode, &dentry->d_name, 0, NULL);

2.2 然后对inode的i_op赋值成ext4类型

inode->i_op = &ext4_file_inode_operations;

inode->i_fop = &ext4_file_operations;


static int ext4_create(struct inode *dir, struct dentry *dentry, umode_t mode,
		       bool excl)
{
	handle_t *handle;
	struct inode *inode;
	int err, credits, retries = 0;

	err = dquot_initialize(dir);
	if (err)
		return err;

	credits = (EXT4_DATA_TRANS_BLOCKS(dir->i_sb) +
		   EXT4_INDEX_EXTRA_TRANS_BLOCKS + 3);
retry:
	inode = ext4_new_inode_start_handle(dir, mode, &dentry->d_name, 0,
					    NULL, EXT4_HT_DIR, credits);
	handle = ext4_journal_current_handle();
	err = PTR_ERR(inode);
	if (!IS_ERR(inode)) {
		inode->i_op = &ext4_file_inode_operations;
		inode->i_fop = &ext4_file_operations;
		ext4_set_aops(inode);
		err = ext4_add_nondir(handle, dentry, inode);
		if (!err && IS_DIRSYNC(dir))
			ext4_handle_sync(handle);
	}
	if (handle)
		ext4_journal_stop(handle);
	if (err == -ENOSPC && ext4_should_retry_alloc(dir->i_sb, &retries))
		goto retry;
	return err;
}

直接打开设备时设置i_fop:

直接打开设备创建inode都是调用 init_special_inode方法

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
	inode->i_mode = mode;
	if (S_ISCHR(mode)) {
		inode->i_fop = &def_chr_fops;    // 字符设备方法
		inode->i_rdev = rdev;
	} else if (S_ISBLK(mode)) {
		inode->i_fop = &def_blk_fops;   // 块设备方法
		inode->i_rdev = rdev;
	} else if (S_ISFIFO(mode))
		inode->i_fop = &pipefifo_fops;  // FIFO文件方法
	else if (S_ISSOCK(mode))
		;	/* leave it no_open_fops */
	else
		printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
				  " inode %s:%lu\n", mode, inode->i_sb->s_id,
				  inode->i_ino);
}
EXPORT_SYMBOL(init_special_inode);

1:从代码 inode->i_fop = &def_chr_fops;看,inode->i_fop 实际调用的就是 def_chr_fops方法 ,这个是通用字符设备的fops, 块设备的是def_blk_fops。


2:

const struct file_operations def_chr_fops = {
    .open = chrdev_open,.llseek = noop_llseek,};
const struct file_operations def_blk_fops = {
    .open = blkdev_open,.llseek = noop_llseek,};

从上述代码看
对于字符设备,def_chr_fops.open 实际就是 chrdev_open,故inode->i_fop->open最终是调用 chrdev_open 这个函数。
对于块设备,def_blk_fops 实际就是 blkdev_open,
inode->i_fop->open最终是调用 blkdev_open 这个函数。


3:chrdev_open/blkdev_open函数会将注册在驱动里的file_operations赋给 inode->i_fop,

详细过程:

| 该方法先调用kobj_lookup方法,在cdev_map中找对应的cdev;对于块设备在bdev_map中找到gendisk,然后利用container_of宏返回bdev结构。

| 找到之后把结果赋值给p。之后获取p->ops的值,赋值给fops,

| 再之后替换 inode->i_fop的值为fops,

| 最后检查 inode->i_fop的值中是否包含open方法,如果有,则调用该方法继续执行open逻辑


9: terminate_walk

执行结果:
主要做一些最终的资源释放工作,会将对路径的引用释放,同时将查找过程中跨越的符号链接引用释放掉,解RCU锁,并将nameidata的深度置为0。

通过dentry获取inode方法:

d_backing_inode(path.dentry);

dentry和inode绑定过程:

d_instantiate将inode和dentry绑定
d_alloc_root调用d_instantiate填充dentry的inode信息
d_instantiate调用__d_instantiate将inode指针设置到dentry的d_inode中


四、VFS和内存页的交互

VFS和内存页的交互示意图


五、文件锁详解

文件系统的锁包括flock文件锁和fnctl范围锁,都存储在inode的i_flock链表中,由struct file_lock通用锁结构表示。


通用锁结构:

struct file_lock
所有文件锁flock和fnctl锁均通过此结构表示,定义在 include/linux/fs.h 中257:

   
  
struct file_lock {
    struct file_lock *fl_next;           // 锁链表的下一节点(同一文件的锁列表)
    struct hlist_node fl_link;           // 全局锁哈希表节点
    struct list_head fl_list;            // 持有者进程的锁链表
    struct file *fl_file;                // 关联的文件对象(struct file)
    unsigned int fl_pid;                 // 对于 POSIX 锁,持有锁的进程 PID
    struct pid *fl_owner;                // 锁所有者(进程或文件)
    unsigned int fl_flags;               // 锁标志(FL_FLOCK, FL_POSIX 等)
    unsigned char fl_type;               // 锁类型:F_RDLCK(读)、F_WRLCK(写)、F_UNLCK(解锁)
    loff_t fl_start;                     // 锁定区域起始偏移
    loff_t fl_end;                       // 锁定区域结束偏移(0 表示 EOF)
    const struct file_lock_operations *fl_ops; // 锁操作函数集
    union {
        struct nfs_lock_info nfs_fl;    // NFS 特定信息
    } fl_u;
};

关键字段说明:
fl_flags:
  FL_FLOCK:标识由 flock() 创建的锁;
  FL_POSIX:标识由 fcntl() 或 lockf() 创建的锁;

fl_start 与 fl_end:
  flock 锁:固定为 0 和 OFFSET_MAX(锁定整个文件);
  fcntl 锁:由用户指定偏移区间(字节级粒度);

fl_file:关联的 struct file 对象(文件打开实例);

fl_owner:
  flock 锁:指向 struct file(与文件打开实例绑定);
  fcntl 锁:指向 struct pid(与进程绑定);
  

1:flock锁

如上图所示,flock锁的持有者是file结构体的指针,对于同一个进程,如果多次open同一个文件由于对应多个file结构体,所以这些结构体持有的flock的独占锁之间是互斥的,即使它们都属于同一个进程,所以flock关联的不是进程而是file结构体。


如上图所示,通过dup/dup2后,由于只是拷贝了文件描述符,而对应的file结构体没有改变是同一个,所以它们的flock锁是共享的。 比如进程A的文件描述符4对应的file结构体持有flock独占锁,那么dup该文件描述符为5后,其对应的file结构体和4文件描述符对应的是同一个,所以4和5文件描述符共享flock独占锁。


如上图所示,通过fork后,子进程继承父进程的文件描述符,而文件描述符对应的file结构体是相同的所以它们共享同一文件的flock锁。比如进程A的文件描述符4对应的file结构体持有flock独占锁,那么fork后子进程文件描述符为4对应的file结构体和父进程4文件描述符对应的是同一个,所以父子进程的4文件描述符共享同一文件的flock独占锁。


2:fnctl锁

如上图所示,fnctl范围锁的持有者是进程pid,所以fnctl锁和进程关联,所以pid10的4和5共享同一文件fnctl锁,pid11和pid10不共享同一文件的fnctl锁。同一个进程不管是哪个文件描述符,对于打开的同一个文件,它们的fnctl锁都是共享的,同一进程的多个线程之间同一打开文件的fnctl锁也是共享的。


由于fnctl和进程相关,所以fork后由于进程pid改变,所以不继承fnctl锁,父子进程不共享fnctl锁。


六、内存页和磁盘的交互

内存和磁盘交互流程图

1:sys_read调用链

  
  sys_read
    ->vfs_read 
      ->file->f_op->read
          ->ext4_file_read_iter
            ->generic_file_read_iter
              ->mapping->a_ops->direct_IO
                ->generic_file_buffered_read
                  ->mpage_bio_sumbit
                    ->sumbit_bio
                      ->generic_make_request
                        ->make_request_fn
                          ->request_fn do_hd_request
                            ->scsi_request_fn
  

2:文件系统整体层次分布

文件系统整体层次图


3:buffer和page映射关系

/proc/meminfo 里显示的 Buffers是 原始磁盘块数据,如使用dd命令直接写入磁盘分区。Cached是文件系统的缓存。

buffer和page关系图


4:buffer_head和b_state

buffer_head:

b_next:指向具有相同hash值的下一个缓冲头,用于链接到块缓冲区的hash表 
b_blocknr:本block的块号
b_size:block的大小
b_list:表示当前的这个buffer在那个链表中
b_dev:虚拟设备标识
b_count:引用计数(几个人在使用这个buffer)
b_rdev:真实设备标识
b_state:状态位图,如下:
b_flushtime:脏buffer需要被写入的时间
b_next_free:指向lru链表中next元素
b_prev_free:指向链表上一个元素
b_this_page:连接到同一个page中的那个链表
b_reqnext:请求队列
b_pprev:hash队列双向链表
b_data:指向数据块的指针
b_page:这个buffer映射的页面
b_end_io:IO结束时候执行函数
b_private:保留
b_rsector:缓冲区在磁盘上的实际位置  //在submit_bh方法中会转换成真实的扇区号
b_inode_buffers:inode脏缓冲区循环链表

b_state:

 BH_Uptodate,    /* 如果缓冲区包含有效数据则置1 */
 BH_Dirty,       /* 如果buffer脏(存在数据被修改情况),那么置1 */
 BH_Lock,        /* 如果缓冲区被锁定,那么就置1 */
 BH_Req,         /* 如果缓冲区无效就置0 */
 BH_Mapped,      /* 如果缓冲区有一个磁盘映射就置1 */
 BH_New,         /* 如果缓冲区是新的,而且没有被写出去,那么置1 */
 BH_Async,       /* 如果缓冲区是进行end_buffer_io_async I/O 同步则置1 */
 BH_Wait_IO,     /* 如果要将这个buffer写回,那么置1 */
 BH_Launder,     /* 如果需要重置这个buffer,那么置1 */
 BH_Attached,    /* 1 if b_inode_buffers is linked into a list */
 BH_JBD,         /* 如果和 journal_head 关联置1 */
 BH_Sync,        /* 如果buffer是同步读取置1 */
 BH_Delay,       /* 如果buffer空间是延迟分配置1 */
 BH_PrivateStart,/* not a state bit, but the first bit available
 * for private allocation by other entities
 * 当数据被写入缓冲块但没有写入设备时b_dirt=1,b_uptodate=0。特殊情况:在新申请的一个设备缓冲块时b_dirt与b_uptodate都为1,表示缓冲块中数据虽然与块设备上的不同,但是数据有效。

5:buffer缓冲区:

buffer缓冲区


6:数据下发到磁盘流程:

磁盘下发流程


7:bio结构

bio结构


8:文件、逻辑、物理块号

文件、逻辑、物理块号


9:磁盘相关参数

磁盘参数


10:调度算法

调度算法


deadline调度算法


cfq调度算法


11:完整调用链

上图流程详解:

mpage_readpage()的主要工作是:判断页的缓存块在磁盘上的块是否连续,
如果连续,则此页可以只提交一个bio请求,然后返回。
如果不连续,则调用block_read_full_page对页的每个缓存块提交一个bio请求。

(1) 这段if逻辑处理调用这个函数之前, 对应page的内容已经被部分map的情况。 对于这种情况,把对应部分的内容记录来。赋值给blocks[page_block]

(2) 接来下处理page中剩余的部分。调用get_block 映射block_in_file对应的文件sector, 结果存在map_bh中。

(3) 如果没有被映射, 说明对应的sector是一个文件洞。 这种情况下, 用first_hole 记录当前的位置, 然后继续循环。
(4)某些文件系统会在 get block 函数进行映射时复制数据到页面中,这就没必要再次从磁盘读取了。调用map_buffer_to_page复制我们收集的数据到页面缓冲区中,然后跳转到标号 confused 处继续执行,这样readpage 不需要重复调用 get block。
(5) 程序继续运行,那一定是缓冲头己经有映射的情况。如果这时 first hole 已经设置,说明这个页面在经过空洞后又重新被映射,没有办法用bio 方式来提交,跳转到 confused 标号处。
(6)如果这个逻辑块不是页面的第一个,判断它是否和前一个逻辑块在磁盘上也是连续的。如果不是,也没有办法用bio 方式来提交,跳转到 confused 标号处
(7) 根据缓冲头的映射信息,记录页面每个逻辑块在磁盘上对应的块编号。如果当前map_bh所映射的所有块都已经被处理完毕,清除缓冲头的 mapped 标志,退出循环;如果这个页面的所有sector都己经处理完,也退出循环,如果此时还有未使用完的map_bh,则留给下一个页面。
(8) 页面的所有逻辑块都经过上面的处理后,可以采用 bio 方式提交的情况就清楚了。只可能会是三种情况。页面的逻辑块被映射到磁盘上连续的逻辑块,这时设置页面的映射标志;页面只有前面一些逻辑块被映射到磁盘,这时清零没有被映射部分的数据:整个页面都没有被映射,清除整个页面,并设置最新标志。
(9) 如果传入了一个bio, 需要判断本页面第一个逻辑块和传入bio 的最后一个逻辑块在磁盘上是否连续,也就是看是否可以将本页面作为一个请求段加入传入的 bio 中。如果不连续,那么mpage_bio_submit提交前面的bio,并且不返回任何新的bio供后续使用
(10) 如果传入的bio 为 NULL, 或者传入的bio 已提交,那么,我们需要调用 mpage_alloc 重新分配一个bio。
(11) 设置该bio的参数, 并将该bio返回
(12) 对于无法用bio处理的情况, 先将已有的bio提交, 然后调用block_read_full_page 通过buffer_head的方式进行读取


do_mpage_readpage的整体逻辑就是, 尽量通过bio的方式去读取连续的sector, 如果不行, 就转而通过buffer_head的方式一个sector一个sector去读。 一个页面的逻辑块被映射到磁盘可能有以下几种情况(以下内容主要参考):
(1) 页面所有逻辑块映射到了磁盘上连续的逻辑块, 这种情况下会以bio方式提交。如果传入了bio,并和本页面在磁盘上连续, 那个尽可能合成一个bio
(2) 页面的逻辑块映射到了磁盘 上不连续的逻辑块。如果要作为bio的方式提交的话, 将不只一个请求, 会增加复杂性。所以采用buffer_head的方式处理, 如果传入了bio, 那么先将bio提交。
(3)页面的前面部分逻辑块未被映射到磁盘上 (标记为纯灰色的逻辑块)。尽管只有一个请求段,但它的起始位置不是从0开始,支持这种情况并非不可能,但代码会更复杂。所以也采用buffer_head的方式处理。如果调用时传入了一个bio,那么先把提交执行。
(4)页面的中间部分逻辑块未被映射到磁盘上,也就是说,中间部分为“空洞”。同第二种情况一样,因为有多个请求段,也只能采用缓冲页面的方式来处理。如果调用时传入了一个bio,那么先把提交执行。
(5)页面的后面部分逻辑块末被映射到磁盘上,它会以bio 方式来提交。如果函数调用时传入了个bio, 并且和本页面在磁盘上连续,那么会将这个页面添加到传入的 bio 中,如果可行的话。(6) 页面的所有逻辑块都未被映射到磁盘上,这时会跳过这个页面的处理。如果调用时传入了一个bio,则依旧将它返回,以期可以继续合并后面的页面。

磁盘驱动处理流程

磁盘驱动器读写流程

上图详解:

硬盘请求项的操作函数,request_fn 对于硬盘就是do_hd_request(do_hd_request是早期内核版本使用的方法,现在大多为scsi_request_fn或其他接口类型的xxx_request_fn方法,下面主要介绍do_hd_request的流程

1. 请求项中的内容转换为具体硬盘信息(起始扇区、读/写扇区总数、柱面号、低驱动器号+磁头号等

2. 复位控制器和硬盘、校正硬盘(当磁盘I/O功能调用出现错误时,需要调用此功能。向硬盘控制器发送重新校正命令,该命令会检查驱动器中磁盘状态,并执行寻道操作,让处于任何地方的磁头移动到0柱面,此功能调用不影响硬盘上的数据

3. 执行写/读命令(开始执行实际的读写操作


do_hd_request 函数执行流程:
1:检测参数合法性。

:检测请求项的合法性,若请求队列中没有请求项则退出

:取设备号中的子设备号(分区对应设备号)以及设备当前请求项中的起始扇区号

:判断子设备号是否存在以及起始扇区是否大于分区扇区数-2(因为一次要求读写一块数据,一块数据包含2个扇区,所以请求的扇区号不能大于分区中最后倒数第二个扇区号)。


2:求出绝对扇区号和硬盘号。绝对扇区号 = 加上子设备号对应分区的起始扇区号,子设备号除以5得到对应的硬盘号(比如3除以5得0,取整,硬盘+4分区总共5个所以除5)。


3:求解扇区号、柱面号和磁头号。
:sector(顺序扇区号)/track_secs(每磁道扇区数) = 整数是tracks(当前总磁道数),余数是sec(当前磁道上扇区号 )
:tracks(当前磁道总数)/dev_heads(硬盘磁头总数) = 整数是cyl(柱面号), 余数是head(当前磁头号)

在当前磁道上扇区号从1算起,于是算出sec后,都需要把sec增1

:sector = (cyl * dev_heads + head) * track_secs + sec - 1
// 指定的硬盘顺序扇区号 = (对应的柱面号*硬盘磁头总数 +
当前磁头号)*每磁道扇区数 + 在当前磁道上的扇区号 - 1

(硬盘磁头总数:dev_heads,指定的硬盘顺序扇区号:sector,对应当前磁道总数:tracks,对应的柱面号:cyl,在当前磁道上的扇区号:sec,磁头号:head)


4:检测硬盘控制器和硬盘复位情况。首先复位控制器状态,重新校正标志,然后置位重新校正标志,重新校正硬盘,让磁头移动到0柱面。


5:向硬盘控制器发送I/O操作信息。如果是写扇区命令,向硬盘控制器发送写命令,然后循环读取状态寄存器,判断请求服务标志(DRQ_STAT)是否置位,若没有置位,则跳转执行出错处理,若可以写入数据,则调用port_write()函数向硬盘控制器数据寄存器端口HD_DATA写入1个扇区的数据。如果是读命令向硬盘控制器发送读扇区命令。


12:缓存数据刷入磁盘间隔

dirty_writeback_centisecs,这个参数控制内核的脏数据刷新进程pdflush(writeback线程)的运行间隔。单位是 1/100 秒。缺省数值是500,也就是 5 秒。如果你的系统是持续地写入动作,那么实际上还是降低这个数值比较好,这样可以把尖峰的写操作削平成多次写操作;

dirty_expire_centisecs,脏页在内存中允许停留的最长时间。

dirty_expire_centisecs + dirty_writeback_centisecs 确定数据从内存缓存刷入磁盘的时间。


七、设备和驱动

</p


1:块设备注册流程

1:在sysfs中建立磁盘的拓扑关系

2:添加设备到总线,并探测设备,加载驱动并初始化设备

3:uevent通知系统设备添加事件

4:将设备添加到对应的类中, 建立设备类与设备之间的关联

块设备的注册流程主要在执行块设备驱动的probe函数中实现。

块设备添加后会生成对应的设备模型device结构,比如ata_device和sas_device等,它们都包含scsi_device。添加后总线会匹配对应的驱动,匹配到驱动后会分别执行总线和驱动的probe函数初始化设备并完成注册。

在Linux设备模型中,bus_probe_device(dev)函数扮演着至关重要的角色,它负责触发设备的探测过程。这个过程是设备驱动加载和初始化的关键步骤,确保了设备能够被系统正确地识别和使用。

bus_probe_device函数的作用:

触发探测: bus_probe_device函数通过特定的机制(如遍历总线上的设备列表、发送探测请求等)来触发对指定设备(dev)的探测。这个探测过程通常涉及到查找与该设备相匹配的驱动程序。

与设备模型交互: 在探测过程中,bus_probe_device会与设备模型的其他部分进行交互。它可能会查询设备的属性、状态以及与其关联的总线和类信息。这些信息对于确定设备的类型和所需的驱动程序至关重要。

加载和初始化驱动: 一旦找到了与设备相匹配的驱动程序,bus_probe_device(或它调用的其他函数)将负责加载该驱动程序,并调用其初始化函数来配置和设置设备。这个过程中,驱动程序可能会注册中断处理程序、分配必要的资源,并设置设备的初始状态。

错误处理: 如果在探测或驱动加载过程中遇到错误(如找不到匹配的驱动程序、资源分配失败等),bus_probe_device将负责进行错误处理。这可能包括回滚已经进行的操作、记录错误日志,并可能向系统报告设备的不可用状态。

在设备驱动加载和初始化过程中的角色 bus_probe_device函数是设备驱动加载和初始化流程中的关键环节。它确保了设备能够被发现、识别,并为其找到并加载正确的驱动程序。没有这个过程,设备将无法被系统使用,因为系统无法知道如何与设备进行通信或控制。

重要性 bus_probe_device函数对于设备的正确初始化和驱动加载至关重要。它是设备模型与驱动程序之间的桥梁,确保了设备能够按照预期工作。如果这个函数无法正常工作,可能会导致设备无法被识别、驱动加载失败或设备性能下降等问题。


1.1 申请主设备号

使用register_blkdev函数向内核注册块设备,并获取一个主设备号。如果传递的主设备号为0,内核会自动分配一个新的主设备号给设备。

/**
* 注册块设备
* @major: 主设备号(0表示由系统自动分配设备号,1~255表示自定义主设备号)
* @name:  块设备名称
*
* 返回:
* 成功返回主设备号,失败返回负值
*/
int register_blkdev(unsigned int major, const char *name);

1.2 申请gendisk

初始化gendisk结构体:使用alloc_disk函数动态分配一个gendisk结构体,该结构体用于表示一个独立的磁盘设备或分区。设置磁盘名称、主设备号、次设备号、文件操作集合以及请求队列等必要信息。

/**
* 申请gendisk
* @minors 申请的分区数(相当于在告诉内核块设备有多少个分区)
*
* 返回:
* 成功返回gendisk的地址,失败返回NULL
*/
struct gendisk *alloc_disk(int minors);

上述步骤后会分配gendisk结构,在/sys/block下创建名为disk->name的类,分配device结构并初始化kobj


1.3 申请并初始化请求队列

/**
* 初始化请求队列
* @rfn    请求处理函数
* 函数指针:void (request_fn_proc) (struct request_queue *q)
* @lock  自旋锁 
*  
* 返回:
* 成功返回请求队列的地址,失败返回NULL
*/
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);

1.4 初始化gendisk

/**
* 设置块设备大小
* @disk   gendisk 指针
* @size   块设备大小,单位: 扇区
*/
void set_capacity(struct gendisk *disk, sector_t size);

 
/**
* 块设备操作函数
* @disk   gendisk 指针
* @size   块设备大小,单位: 扇区
*/
static struct block_device_operations blkdev_fops = {
    .owner = THIS_MODULE,
};

blkdev.gendisk->major = blkdev.major;                      // 主设备号
blkdev.gendisk->first_minor = 0;                           // 起始次设备号
blkdev.gendisk->fops = &blkdev_fops;                       // 块设备操作函数
blkdev.gendisk->queue = blkdev.queue;                      // 块设备请求队列
strcpy(blkdev.gendisk->disk_name, blkdev_NAME);            // 块设备名称
set_capacity(blkdev.gendisk, DISK_SIZE/512);               // 告诉内核块设备大小,单位:扇区

1.5 向内核注册块设备(磁盘)

/**
* 将gendisk添加到内核
* @disk   gendisk 指针
*/
void add_disk(struct gendisk *disk);

add_disk是块设备注册的内核接口,是块设备驱动的最后一步,也是最关键的一步,将磁盘gendisk及其及分区添加到devices树及sysfs中。

调用链:add_disk -> device_add_disk -> __device_add_disk


__device_add_disk调用过程详解:

1:blk_alloc_devt #为主分区disk->part0分配设备号

//disk_to_dev(disk)->devt = devt;

*devt = MKDEV(disk->major, disk->first_minor + part->partno)

// 根据gendisk 结构中的major(主设备号),first_minor(第一个次设备号),partno(磁盘对应分区的分区号,因为磁盘对应分区是0号分区,所以这里是0)共同算出设备号


2:bdi_register_owner

#将disk->queue->backing_dev_info添加到全局链表bdi_list

backing_dev_info主要用于管理自己队列的回写线程;

// 所有backing_dev_info 都被链接到全局链表bdi_list中;


3:blk_register_region

#将磁盘添加到bdev_map中

将 dev_t 和 gendisk 关联起来,保存在 bdev_map 中

//data保存gendisk,range保存次设备数(minors),dev保存设备号


4:register_disk #注册磁盘,核心步骤


4.1 device_add(ddev) #创建磁盘设备(device结构),并在sysfs中建立磁盘的拓扑关系

// 与alloc_disk的device_initialize(创建并初步初始化device结构)对应

// 由device_create_file完成在dev目录下创建uevent文件(/dev/xxxxx)

将代表磁盘的0号分区的device注册到sysfs完成了磁盘的注册,从而加入到linux设备模型


4.2 bdget_disk #分配磁盘对应的bdev结构

为gendisk分配block_device结构体,并返回其指针, 即:获取0号分区对应的块设备描述符

4.2.1 disk_get_part(disk, partno) #获取磁盘对应分区(这里是0号分区)

4.2.2 bdget(part_devt(part)) #通过设备号获取0号分区对应的block_device结构

part_devt(part) 是获取分区对应的device(分区关联的设备)的设备号

//每个分区都关联一个device结构,表示一个设备,里面有该设备的设备号

4.2.2.1 iget5_locked // 通过设备号获取或新建inode

// 注:设备号是通过bdget参数传递进来的

ilookup5(sb, hashval, test, data);
// 先根据设备号hash值查找inode是否已经存在(是否在vfs的inode_hashtable中),查到则直接返回该inode
 
alloc_inode(sb);
inode_insert5(new, hashval, test, set, data);

// 如果没查找inode,则新建inode并执行初始化,最后插入inode哈希表中(通过bdev文件系统的superblock结构和inode号哈希值共同计算出hash key)

该步骤会初始化inode内嵌的address_space->a_ops为def_blk_aops,它会在读写块设备文件时被调用

4.2.2.2 bdev = &BDEV_I(inode)->bdev

// 通过4.2.2.1获取的inode再获取bdev结构

根据iget5_locked中获取的inode,然后利用container_of宏返回bdev结构

container_of(inode, struct bdev_inode, vfs_inode);
struct bdev_inode {
      struct block_device bdev;
      struct inode vfs_inode;
};

通过调用 iget5_locked 来获取或创建一个包含 bdev_inode 所需 inode 的结构体,间接地为 bdev_inode 分配了内存,所以这里通过inode利用container_of方法直接返回bdev_inode的地址


4.3 blkdev_get // 完善bdev结构并打开设备

该函数负责

1:从gendisk中获取信息;

2:建立相关数据结构之间(主要是bdev和gendisk之间)的联系(期间可能会扫描分区表并创建各个hd_struct);

3:完成块设备的打开动作;

4.3.1. 独占访问检查

WARN_ON_ONCE((mode & FMODE_EXCL) && !holder);:

// 如果请求独占访问但没有提供持有者(holder),则发出警告。

// FMODE_EXCL表示以独占模式打开文件

如果请求独占访问且提供了持有者,则调用bd_start_claiming来开始申请对设备的所有权,并检查返回值以确定是否成功。

4.3.2. __blkdev_get

// res = __blkdev_get(bdev, mode, 0);:调用__blkdev_get函数来实际获取对块设备的访问权,并检查返回值以确定是否成功

4.3.2.1 权限检查(如果for_part为0):
调用
devcgroup_inode_permission函数检查当前进程是否有权访问该设备。

如果没有权限,则释放对块设备的引用并返回错误代码。

4.3.2.2 bdev_get_gendisk // 通过 bdev_get_gendisk 获取gendisk结构体,并将分区号保存在partno,bdev_get_gendisk是通过设备号在bdev_map中找到对应的gendisk结构体

:处理的第一种情况是:通过设备号在bdev_map能找到gendisk
// 先通过设备号在bdev_map中查询即可得到对应的gendisk信息,如果能直接查到,直接获取查到的gendisk信息即可

1:在bdev_map中通过设备号找到probe结构后,先通过找到probe中的get函数获取kobj结构(kobj在device结构中)

  struct probe *p;
  get也就是exact_match函数
  static struct kobject *exact_match(dev_t devt, int *partno, void *data)
  {
        struct gendisk *p = data;

        return &disk_to_dev(p)->kobj;
  }
  probe = p->get
  kobj = probe(dev, index, data)

2:再通过container_of宏获取gendisk结构

container_of((device), struct gendisk, part0.__dev)

// 这里在bdev_map中实际上可以直接获取gendisk结构体,然而,函数的设计者之所以选择不直接返回p->data(指向gendisk),而是返回一个kobject指针,这样做主要是为了后续的扩展性和代码的通用性。

:处理的第二种情况是:通过设备号在bdev_map找不到gendisk

// 说明block_device 是指向某个分区的,而且是个扩展分区
1:通过设备号在bdev_map中查询不到对应的gendisk信息,说明block_device 对应的是个扩展分区,这个时候我们要到IDR树中通过次设备号去查询

2:找到hd_struct后,再通过hd_struct找到对应的整个设备的gendisk

// hd_struct里有__dev成员,找到他,然后获取其父节点的device(磁盘对应的device),最后通过 container_of((device), struct gendisk, part0.__dev) 方法获取对应的gendisk

IDR树查询过程:

在ext_devt_idr这个IDR树中,查找由devt的次设备号经过blk_mangle_minor函数处理后的值(IDR的ID)所对应的设备结构
(hd_struct)指针,并将这个指针赋值给part变量

part = idr_find(&ext_devt_idr, blk_mangle_minor(MINOR(devt)));

void *idr_find(const struct idr *idr, unsigned long id)
{
    return radix_tree_lookup(&idr->idr_rt, id - idr->idr_base);
}
EXPORT_SYMBOL_GPL(idr_find);

// radix_tree_lookup(&idr->idr_rt, id - idr->idr_base), idr_base是指定 ID 分配的起始地址,这里即:次设备的起始值(first-minor),次设备号 - 次设备起始值就找到对应的分区了

// IDR树中只存次设备号,主设备号都是取得BLOCK_EXT_MAJOR的值,是相同的。IDR的ID是次设备号经过blk_mangle_minor函数处理后的值,是唯一的。

4.3.2.3 打开设备

第一次打开块设备的处理:

bdev->bd_openers 为 0

// 建立bdev结构体与gendisk的关联
bdev->bd_disk = disk;
bdev->bd_queue = disk->queue;
bdev->bd_contains = bdev;
bdev->bd_partno = partno; *分区号,查找bdev_map过程中,通过设备号差值(dev - p->dev)算出

对磁盘(partno为0)的bdev进行初始化

{
  // 设置分区,通过part_tbl和partno获取
  bdev->bd_part = disk_get_part(disk, partno);     
 
  // 执行磁盘描述符的open回调
  ret = disk->fops->open(bdev, mode);
 
  // 设备磁盘容量
  bd_set_size(bdev,(loff_t)get_capacity(disk)<<9);  
 
  // 设置block_size的大小(这个块设备自身的块大小,文件系统的块大小一般是块设备自身的整数倍)
  set_init_blocksize(bdev);                                      

  // 如果bdev->bd_invalidated =1(该设备上分区是否无效) ,将重新扫描分区
  rescan_partitions(disk, bdev);
}

// 如果open回调的返回值是ERESTARTSYS,重新进行调用__blkdev_get方法(获取gendisk信息并打开设备)

if (ret == -ERESTARTSYS) {
        /* Lost a race with 'disk' being
         * deleted, try again.
         * See md.c
         */
        disk_put_part(bdev->bd_part);
        bdev->bd_part = NULL;
        bdev->bd_disk = NULL;
         bdev->bd_queue = NULL;
         mutex_unlock(&bdev->bd_mutex);
         disk_unblock_events(disk);
         put_disk_and_module(disk);
         goto restart;
}

// -ERESTARTSYS 表示信号函数处理完毕后重新执行信号函数前的某个系统调用

对分区(partno不为0)的bdev进行初始化

{

 // 获取整个设备(这里分区号参数是0)的bdev,赋值给变量whole
 whole = bdget_disk(disk, 0);       

 //递归调用__blkdev_get                            
 ret = __blkdev_get(whole, mode, 1);                       
 bdev->bd_contains = whole;

 // 设置分区,通过part_tbl和partno获取
 bdev->bd_part = disk_get_part(disk, partno);   

 // 设备磁盘容量    
 bd_set_size(bdev,(loff_t)get_capacity(disk)<<9);    

 // 设置block_size的大小
 set_init_blocksize(bdev);        
}                                 

bdev->bd_openers 不为 0

非第一次打开块设备的处理:

* 不同于第一次打开,相关对象的关系已经建立

// 执行磁盘描述符的open回调

ret = bdev->bd_disk->fops->open(bdev, mode);

// 如果bdev->bd_invalidated =1(该设备上分区是否无效) ,将重新扫描分区
rescan_partitions(disk, bdev);

// rescan_partitions(disk, bdev) 将重新扫描分区表并解析,更新到磁盘的分区表中

add_partition函数会根据分区信息创建一个分区hd_struct对象,并进行初始化,同时会调用device_initialize初始化partno号分区的device设备,并执行device_add,将磁盘分区设备的device加入到linux设备模型;

4.3.2.4 更新打开次数并解锁设备

// 该块设备打开的次数+1

bdev->bd_openers++;

// 递增bdev->bd_part_count,块设备分区被打开的次数+1

bdev->bd_part_count++;

// 释放该块设备的互斥锁

mutex_unlock(&bdev->bd_mutex);

// 解除对磁盘事件的阻塞
disk_unblock_events(disk);
// 如果这不是首次打开该块设备(first_open为假),则调用put_disk_and_module函数来释放对磁盘和模块的引用
if (!first_open)
put_disk_and_module(disk);

put_disk_and_module(disk) 主要用来减少kobj(device中的kobj)和module(设备对应的模块,是个module结构体)的引用计数,如果引用计数为0就释放对应的结构

这里已经完成设备的权限检查,以及gendisk和bdev结构的关联,设备的打开工作, kobj和module结构已经不再需要,释放掉以节省系统资源

4.3.3. 处理独占访问的完成:
如果whole为真,说明之前进行了独占访问的声明,且当前设备持有者以独占模式打开的,则现在需要完成该过程

    if (whole) {
         struct gendisk *disk = whole->bd_disk
 
        // 锁定相关的互斥锁和自旋锁,以确保线程安全。
        mutex_lock(&bdev->bd_m utex);
        spin_lock(&bdev_lock);
 
        // 如果获取设备访问权成功(__blkdev_get返回0),则更新设备的持有者信息,并设置bd_write_holder标志(如果适用)
        if (!res) {
            BUG_ON(!bd_may_claim(bdev, whole, holder));
            whole->bd_holders++;
            whole->bd_holder = bd_may_claim;    // 这里bd_may_claim是个系统添加临时占用标识,目的是阻止此标识期间被申请独占持有权限,但并不影响分区的独占申请(实际磁盘并未被其他人独占持有)
            bdev->bd_holders++;                          // 如果whole和bdev相同,意味这bdev代表整个磁盘,bd_holders需要递增2次,因为前面执行了whole->bd_holder = bd_may_claim;
            bdev->bd_holder = holder;
    }
 
    // 通知其他等待者独占访问已经完成。
    BUG_ON(whole->bd_claiming != holder);
    whole->bd_claiming = NULL;
    wake_up_bit(&whole->bd_claiming, 0);

4.3.4. 解锁并返回:

释放该块设备的自旋锁, spin_unlock(&bdev_lock);
如果请求了写操作,并且之前没有写持有者,且磁盘标志要求阻塞事件,则设置bd_write_holder为true并阻塞磁盘事件。

这段代码是Linux内核中处理块设备写操作的一部分。让我们逐步分析它的含义和作用:

  {
   ...{
      ...
      if (!res && (mode & FMODE_WRITE) && !bdev->bd_write_holder && (disk->flags & GENHD_FL_BLOCK_EVENTS_ON_EXCL_WRITE)):
          /* !res:检查之前的操作(可能是打开设备或检查权限等)是否成功,res为0表示成功
           * (mode & FMODE_WRITE):检查文件打开模式是否包含写模式(FMODE_WRITE)
           * !bdev->bd_write_holder:检查当前块设备是否没有被其他进程持有用于写操作
           * (disk->flags & GENHD_FL_BLOCK_EVENTS_ON_EXCL_WRITE):检查磁盘标志是否包含GENHD_FL_BLOCK_EVENTS_ON_EXCL_WRITE,这意味着在独占写操作期间应该阻塞事件
           */

          // 如果上述条件都满足,将块设备的bd_write_holder标志设置为true,表示当前进程持有该设备用于写操作
          bdev->bd_write_holder = true;:

      
          // 调用disk_block_events函数来阻塞磁盘事件。这通常是为了防止在独占写操作期间发生并发访问或事件通知,从而保护数据的一致性和完整性
          disk_block_events(disk);:
      }
      /* 综上所述,这段代码的目的是在特定条件下(即之前的操作成功、文件以写模式打开、块设备当前没有被其他进程持有用于写操作、且磁盘标志要求阻塞事件),将块设备标记为正在被当前进程持有          * 用    于写操作,并阻塞磁盘事件。这是Linux内核中用于管理块设备写操作和并发控制的一种机制。
        */

      // 释放该块设备的互斥锁
      mutex_unlock(&bdev->bd_mutex);
 
      // 释放对whole设备的引用(whole指针的使命已经完成不再需要)
      bdput(whole);
  }
 
  // 返回获取设备的结果
  return res;
}

2:设备和驱动结构

2.1 device结构

  
struct device {
    struct device    *parent; //设备的parent节点,通常是bus或controller。 如果此parent为NULL,则此设备就是顶层设备。
    struct device_private    *p;             //device的私有数据
    struct kobject           kobj;           //该数据结构对应的struct kobject,用于层级关系
    const char               *init_name;     //设备对象的名称,出现在sys目录下。
    struct device_type       *type;          // 指向device_type结构,代表了设备的特殊的信息
    struct semaphore         sem;
    struct bus_type          *bus;            //该device属于哪个总线
    struct device_driver     *driver;         //该device对应的驱动的数据
    void                     *platform_data;  //一个指针,用于保存具体的平台相关的数据
    void                     *driver_data;    //驱动层可以通过 dev_set/get_drvdata 函数来获取该成员变量。
    struct device_node       *of_node;        //存放设备树中匹配的设备节点。当内核使能设备树,总线负责将驱动的 of_match_table 以及设备树的 compatible 属性进行比较之后,将匹配的节点保存到该变量。
    struct dev_pm_info       power;           //电源管理相关的逻辑
    #ifdef CONFIG_ NUMA
    int                      numa_node;
    #endif
    u64                      *dma_mask;
    u64                      coherent_dma_mask;
    struct device_dma_parameters     *dma_parms;
    struct list_head                 dma_pools;
    struct dma_coherent_mem          *dma_mem;
    /* arch specific additions */
    struct dev_archdata              archdata;
    dev_t                            devt;          //一个32位的整数,它由两个部分(Major和Minor)组成
    spinlock_t                       devres_lock;
    struct list_head                 devres_head;
    struct klist_node                knode_class;
    struct class                     *class;       //该设备属于哪个class
    const struct attribute_group *   *groups;      //该设备的默认attribute集合
    void(*release)(struct device *dev);
}
  

2.2 driver结构

  
struct device_driver
{
  const char         *name;    //指定驱动名称,总线进行匹配时,利用该成员与设备名进行比较。
  struct bus_type    *bus;      //归属与哪个总线。内核需要保证在驱动执行之前,对应的总线能够正常工作。

  struct module      *owner;    //表示该驱动的拥有者,一般设置为 THIS_MODULE。
  const char         *mod_name; //内置模块名称,用于模块别名生成(MODULE_ALIAS)

  bool suppress_bind_attrs;     //布尔量,用于指定是否通过 sysfs 导出 bind 与 unbind文件,bind 与 unbind 文件是驱动用于绑定/解绑关联的设备。

  const struct of_device_id    *of_match_table;    //指定该驱动支持的设备类型。当内核使能设备树时,会利用该成员与设备树中的 compatible 属性进行比较。
  const struct acpi_device_id  *acpi_match_table;

  int (*probe) (struct device *dev);  //当驱动以及设备匹配后,会执行该回调函数,对设备进行初始化。
  int (*remove) (struct device *dev); //当设备从操作系统中拔出或者是系统重启时,会调用该回调函数。

  const struct attribute_group **groups; //指定该驱动的属性。
  struct driver_private              *p;
};
  

2.3 gendisk

gendisk表示一个实际磁盘设备的抽象,它是直接被设备驱动程序分配和操作。一个磁盘可以创建多个分区,其中磁盘本身内嵌0号分区,它通过part_tbl指向分区表;通过queue指向了块设备驱动中分配的request_queue。另外gendisk通过全局kobj_map进行管理,每个probe管理一个磁盘设备,利用设备号可以通过bdev_map查找到probe进而通过data找到gendisk

  
major:        磁盘的主设备号
first_minor:  磁盘的第一个次设备号,一般设置为0;
minors:       磁盘的最大次设备号数目,如果为1,则磁盘不能分区
disk_name:    磁盘名字,磁盘设备注册到内核后,可以在/dev下看到名为disk_name的块设备;
part_tbl:      一个数组,指向磁盘对应的分区表,包含了该磁盘的所有分区
fops:         该磁盘的操作函数集
queue:        磁盘对应的请求队列,针对该磁盘设备的请求都放到此队列中,可通过blk_mq_init_sq_queue函数分配;
flags:        磁盘的标志,表示磁盘的状态和磁盘类型的标志  (1:磁盘是移动磁盘需要设置为:GENHD_FL_REMOVABLE  2:磁盘初始化并处于可用状态需要设置为:GENHD_FL_UP)

part0:        该磁盘的第0个分区。实际上它不代表真正的分区,它代表整个磁盘(或许可以称为主设备),分区数组的0号元素指向了它,当磁盘包含分区时,分区在分区数组中的下标从1开始。
devnode:      函数指针,用于生成磁盘节点的路径名
events:       支持的事件
private_data: 磁盘的私有数据
driverfs_dev: 指向设备的指针,用于 driverfs(一种用于设备驱动程序的虚拟文件系统)
slave_dir:    指向从设备目录的指针
ev:           用于检测磁盘的事件
node_id:      该数据结构所使用的NUMA节点ID
  

2.4 hd_struct

内核使用该结构表示设备上的一个分区,是特殊的逻辑设备,每个分区覆盖了磁盘的一部分连续块,磁盘也可以理解为一个大的分区,分区编号为0,称为0号分区,其它分区从1开始编号。在对分区进行操作时,其偏移值会转换为对磁盘的偏移值,交付底层块设备执行。启动过程中,检测到磁盘会扫描分区,内存中构建磁盘和分区的关系

  
start_sect:  起始扇区号
nr_sects:    该分区扇区数目。分区0的该域保存的是整个磁盘的扇区数目,也就是磁盘的容量。
__dev:       Linux 设备结构体,表示与分区关联的设备。
holder_dir:  指向分区所在的父设备的kobject
ref:         分区的引用计数
partno:      分区号
dkstats:     盘统计信息,可能是每 CPU 的结构体或单个结构体

nr_sects_seq:     扇区数的序列计数器。
alignment_offset: 对齐偏移量,用于指示分区的对齐位置。
discard_alignment:丢弃对齐的大小。
holder_dir:  指向持有者目录的指针。
policy:      分区策略。
info:        指向分区元信息的指针。
make_it_fail:用于配置失败的请求。
stamp:       时间戳。
in_flight:   于跟踪处理中的请求的原子计数器数组。
rcu_head:    用于 RCU(Read-Copy Update)机制的头部。
  

2.5 block_device

每个分区(包括0号分区)都有一个块设备描述符,通过它可以联系上层文件系统(通过次inode,它的设备号与主inode相等)和底层的IO子系统(块设备)。每个块设备描述符通过bd_disk指向通用磁盘,通过bd_container指向了父块设备描述符,如分区的块设备描述符通过bd_container指向了磁盘的块设备描述符,所有的block_device形成一个链表;磁盘和分区都可以作为块设备独立使用,它们分别对应一个块设备描述符;

  
bd_dev:         设备编号,由主设备编号和次设备编号组成;
bd_openers:     一个引用计数,记录了该块设备打开的次数;
bd_inode:       指向该设备文件的inode;
bd_super:       指向该设备所在文件系统的超级块,设备如果有挂载文件系统bd_super指向文件系统的超级块
bd_mutex:        互斥锁
bd_inodes:      链表头,该链表包含了表示该块设备的所有设备文件的inode
bd_claiming:    申请获取设备者
bd_holder:      当前持有设备者,块设备打开的时候指向file
bd_holders:     设备有多少个持有者
bd_write_holder:是否是写持有
bd_contains:    该设备所属的块设备
bd_block_size:  该设备的块大小,单位为字节
bd_part:        指向该块设备的hd_struct,如果该block_device描述的是一个分区,则该变量指向分区的信息,对于gendisk,指向内置的分区0;
bd_part_count:  该块设备上的分区被引用的次数,如果不为0,则不能重新扫描分区,因为分区正被使用
bd_invalidated: 该设备上分区是否有效,1表示无效,如果分区无效,则下次打开时会重新扫描分区表
bd_disk:        指向该设备所对应的gendisk
bd_queue:       该设备对应的请求队列
bd_list:        用于将所有的块设备添加到all_bdevs中
bd_private:     给设备的当前持有者使用的私有数据结构
  

2.6 request_queue

request_queue是 磁盘对应的请求队列

注:block_device 和 gendisk 都包含 request_queue 类型的成员, 实际上他们指向的是同一个请求队列,因此,在初始化时,初始化其中一个即可,一般初始化gendisk中的请求队列。

当文件系统需要与块设备进行数据传输或者控制时,内核需要向设备的request_queue发送请求对象。

  
queue_head:      队列请求链表,链表上的每个元素都是一个struct request类型的结构,代表一个读写请求。
elevator:        指向该队列使用的调度算法。该调度算法用于对请求队列上的请求进行重排、优化以得到最好的性能。
rq:              struct request的缓存,分配和释放struct request时都通过它进行
request_fn:      请求处理函数。当内核期望驱动程序执行某些动作时,比如写数据到设备或者从设备读取数据时,内核会自动调用该函数。因此驱动程序必须提供该函数,它是块驱动框架和设备的接口。
make_request_fn: 创建新请求。内核提供有该函数的默认版本,在默认版本中,内核会向请求队列添加请求,如果队列中有足够多的请求,则就调用request_fn来处理请求。如果不想使用内核提供的默认实现,驱动开发者就要自己实现(这是可能的,因为驱动开发者更了解自己的硬件是如何工作的)。blk_queue_make_request用于设置队列的创建新请求函数。
prep_rq_fn:      请求预备函数。大多数驱动不适用该功能,而是将它设置为NULL。如果实现了该函数,则它的功能应该是在发出请求之前预先准备好一个请求。blk_queue_prep_rq用于设置队列的请求预备函数。
unprep_rq_fn:    取消请求的准备,在请求被处理完成时可能会被调用。如果在请求预备函数中分配了一些资源,这是一个释放的好地方。blk_queue_unprep_rq用于设置请求队列的该函数。
merge_bvec_fn:   用于确定一个现存的请求是否允许添加更多的数据。由于请求队列的长度是有限的,因而提供该检测可以在队列已满时用于检测是否可以往已存请求添加数据,如果可以,则就可以添加新的数据到已存请求中。blk_queue_merge_bvec用于设置请求队列的该函数
softirq_done_fn:当使用软中断异步完成请求时用于通知驱动程序请求已经完成
rq_timed_out_fn:当请求超时时执行的函数
dma_drain_needed:判断dma是否被耗光,如果是,则返回非0(fn which returns non-zero if drain is necessary)
lld_busy_fn:    当设备忙时调用该函数
queue_flags:    队列的状态标志
nr_requests:    请求队列上可以添加的最大请求数目
queue_limits:   包含了请求队列的各种限制,比如硬件的扇区大小
kobj:           请求队列的kobject,用于将请求队列放到kobject框架中管理
  

2.7 disk_part_tbl

disk_part_tbl结构体内主要包含指向磁盘分区结构实例的指针数组,描述了各个分区的属性,包括起止扇区,分区名等,其中分区0描述的就是代表磁盘的分区。

  
part[]:分区结构体指针数组,每一个成员都是 struct hd_struct*元素

主分区特点小结:
  1)是系统中必须要存在的分区,系统盘一般会选择主分区安装系统。
  2)分区数字编号只能是1-4,例如:SCSCI分区名为sda1、sda2、sda3、sda4。
  3)主分区最多四个,最少一个。

扩展分区(Extend)小结:
  1.不能直接存放数据,必须要再分逻辑分区
  2.一块磁盘最多1个扩展分区
  3.主分区+扩展分区总的数量不能超过4个
  

2.8 bdev_inode

bdev_inode是块设备inode

  
block_device:block_device结构
inode:       vfs中的inode,因为和devtmpsf的inode的设备号相同,所以通过设备号将其和主inode联系起来
  

2.9 主要的几个变量

  
bus_kset是一个kset类型的全局变量
  :内核系统中在bus模块初始化接口中,创建了一个kset类型的全局变量bus_kset。
  :该全局变量会将系统中所有已注册的总线类型对应的kobject链接在一起,通过该系统变量可以查看系统中所有已注册的总线类型。

bus->p->driver_kset
  :该变量依附于具体的总线类型,仅在具体的总线变量创建时,方会创建该变量,该变量会将所有注册进其依附总线的驱动对应的kobject链接在一起。

bus->p->device_kset
  :该变量主要是总线变量的成员变量,系统中主要使用该kset对应的kobject以及与具体设备变量对应的kobject,这两个kobject相互创建链接子目录。(针对bus->p->device_kset与device类型变量对应的kobject的链接关系,没有在上面图中画出)

devices_kset是一个kset类型的全局变量
  :该kset类型的全局变量,通过list成员集合了所有系统中已创建的device类型对应的kobject变量
  :系统中依附在所有类型总线上的device,均会通过其对应的kobject变量,链接至devices_kset链表上,即通过devices_kset可找到系统中所有已创建的device

bus->p->klist_drivers
  :该变量主要用于将所有注册至该总线上的device_driver类型变量链接在一起,通过该变量可以查看该总线上当前已注册的驱动。
  :总线类型通过klist_devices、klist_drivers,可以找到该总线上所有已注册的设备与驱动                    
  

2.10 注册设备主要函数

  
device_create()
    device_create_vargs();//新建结构体dev
        kobject_set_name_vargs();
        device_register();
            device_initialize();
            device_add();


device_register(struct device *dev)
    device_initialize(); /* 初始化通用数据结构 */
    device_add();        /* 加入到该dev所属bus上 */


device_initialize(struct device *dev)
    kobject_init();
    INIT_LIST_HEAD(&dev->dma_pools);
    INIT_LIST_HEAD(&dev->devres_head);
    device_pm_init();
    set_dev_node();


device_add()//把设备加到对应的bus上
   get_device();
   device_private_init();
   dev_set_name();           //用dev的init_name初始化dev-kobject->name即目录名
   !dev_name(dev) && dev->bus && dev->bus->dev_name;
   get_device(dev->parent);  //父节点的引用计数加1
   get_device_parent();      //找到其父类
   kobject_add();            //把内嵌的kobject注册到设备模型中,在sys下建目录kobj->name
   platform_notify();        //如果platform_notify定义的话,通知平台设备,一般不用
   device_create_file();     //创建sys目录下设备的uevent属性文件
   device_create_file();     //创建sys目录下设备的设备号属性,即major和minor 
   device_create_sys_dev_entry();//在/sys/dev/char/或者/sys/dev/block/创建devt的属性的连接文件
   device_add_class_symlinks();  //在class下创建符号链接,实际创建的kobject都是在device下面
   device_add_attrs();     //创建sys目录下设备其他属性文件(添加设备属性文件)
   bus_add_device();       //添加设备的总线属性 将设备加入到管理它的bus总线的设备连表上
   dpm_sysfs_add();        //把设备增加到sysfs电源管理power目录(组)下,如果该设备设置电源管理相关的内容.
   device_pm_add();        //设备添加到电源管理相关的设备列表中
   blocking_notifier_call_chain();//通知客户端,有新设备加入
   kobject_uevent();      //产生一个内核uevent事件(这里是有设备加入),可以是helper,也可是通过netlink机制
   bus_probe_device();在bus上匹配dev对应的drv。
   klist_add_tail();      //将设备添加到其父设备的子列表中
   klist_add_tail()       //将dev添加到class的klist_device链表(对driver有klist_driver链表)
   list_for_each_entry();
   add_dev(dev, class_intf);//通知有新设备加入, udev会生成/dev/***节点文件以及部分初始化工作.


device_private_init(struct device *dev)
    kzalloc(dev->p);
    klist_init();
    INIT_LIST_HEAD();


bus_add_device(struct device *dev)
    bus_get();//对总线的引用计数加1
    device_add_attrs();  //创建相应的属性文件
    device_add_groups(); //增加到组中,就是再加一层目录封装
    sysfs_create_link(); //在sys/bus/总线类型/devices/目录下,创建指向相同设备名字的符号链接
    sysfs_create_link(); //在sys/devices/分类/设备名字/目录下,创建指向在sys/bus/总线类型的符号链接
    klist_add_tail();    //把设备加入到总线的设备链中


bus_probe_device(struct device *dev)
  device_attach();//if (bus->p->drivers_autoprobe),drivers_autoprobe是一个bit变量,为l则允许本条总线上的device注册时自动匹配driver。drivers_autoprobe默认总是为1,除非用户空间修改 */
   list_for_each_entry();
   add_dev();


device_attach(struct device *dev)
    klist_node_attached();
    device_bind_driver();
        driver_sysfs_add(dev);              //把dev和driver链接起来
            blocking_notifier_call_chain(); //通知其它总线将要绑定driver 到device
            sysfs_create_link(&dev->driver->p->kobj,&dev->kobj,kobject_name(&dev->kobj);在driver目录下创建device目录的符号链接,名字为设备的名字
            sysfs_create_link(&dev->kobj, &dev->driver->p->kobj,"driver");//在device目录下创建driver的目录,名字为driver
       driver_bound(dev);     //device里面私有的driver节点挂接到driver的设备链表
            klist_add_tail(); //把device私有的p里的knode_driver,绑定到driver里面的klist_device链表
            blocking_notifier_call_chain(); //通知其它子模块以及绑定成功
    bus_for_each_drv();
       __device_attach();
          driver_match_device();    //drv和dev匹配成功的话,才会往下执行的probe函数
            drv->bus->match(dev, drv) ;//drv对应的bus存在的话,call此bus的match函数
          driver_probe_device();
          device_is_registered();   //确定设备已注册
          pm_runtime_barrier();     //电源管理
          really_probe();//调用驱动的probe函数
              dev->driver = drv;    //匹配好后的驱动信息记录到设备内部
              driver_sysfs_add();   //driver加入sysfs,就是创建各种符号链接
              dev->bus->probe();    //若bus的probe函数存在,则执行bus的probe drv->probe();// 否则,指向device的probe函数
              driver_bound();       //将设备加入到驱动支持的设备链表中
          pm_request_idle();
    pm_request_idle(dev);                   
  

2.11 device和driver关系图

device和driver和bus总线的关系


sys目录下设备的层级关系


device和子目录对应结构


2.12 驱动和设备的关联过程

. 驱动通常是通过insmod载入内核中,成功载入后可以在内核模块列表中看到。

. 而设备注册是单独的过程,通常在驱动成功加载之后。设备注册过程中在device_add函数会通bus_probe_device方法探测设备并关联设备和其对应的驱动然后初始化设备。

. 驱动信息存储在device结构体的device_driver成员中,这样设备和驱动之间就联系起来了。


3:块设备各结构关系


八、ext4文件系统

1: 磁盘布局

ext4文件系统磁盘布局


引导块/MBR:

MBR结构图示意


2:档案系统信息

  • 档案系统 volume 名称 (Filesystem volume name) - 即是档案系统标签 (Filesystem label),用作简述该档案系统的用途或其储存数据。现时 GNU/Linux 都会用 USB 手指/IEEE1394 硬盘等可移除储存装置的档案系统标签作为其挂载目录的名称,方便使用者识别。而个别 GNU/Linux distribution 如 Fedora、RHEL 和 CentOS 等亦在 fstab 取代传统装置档案名称 (即 /dev/sda1 和 /dev/hdc5 等) 的指定开机时要挂载的档案系统,避免偶然因为 BIOS 设定或插入次序的改变而引起的混乱。可以使用命令 e2label 或 tune2fs -L 改变。
  • 上一次挂载于 (Last mounted on) - 上一次挂载档案系统的挂载点路径,此栏一般为空,很少使用。可以使用命令 tune2fs -M 设定。
  • 档案系统 UUID (Filesystem UUID) - 一个一般由乱数产生的识别码,可以用来识别档案系统。个别 GNU/Linux distribution 如 Ubuntu] 等亦在 fstab 取代传统装置档案名称 (即 /dev/sda1 和 /dev/hdc5 等) 的指定开机时要挂载的档案系统,避免偶然因为 BIOS 设定或插入次序的改变而引起的混乱。可以使用命令 tune2fs -U 改变。
  • (Filesystem magic number) - 用来识别此档案系统为 Ext2/Ext3/Ext4 的签名,位置在档案系统的 0x0438 - 0x0439 (Superblock 的 0x38-0x39),现时必定是 0xEF53。
  • 档案系统版本编号 (Filesystem revision #) - 档案系统微版本编号,只可以在格式化时使用 mke2fs -r 设定。现在只支援[1]:0 - 原始格式,Linux 1.2 或以前只支援此格式[2]1 (dymanic) - V2 格式支援动态 inode 大小 (现时一般都使用此版本)
  • 档案系统功能 (Filesystem features) - 开启了的档案系统功能,可以使用合令 tune2fs -O 改变。现在可以有以下功能:
  • has_journal - 有日志 (journal),亦代表此档案系统必为 Ext3 或 Ext4
  • ext_attr - 支援 extended attribute
  • resize_inode - resize2fs 可以加大档案系统大小
  • dir_index - 支援目录索引,可以加快在大目录中搜索档案。(ext3 ,ext4 支持,ext2 不支持)
  • filetype - 目录项目为否记录档案类型
  • needs_recovery - e2fsck 检查 Ext3/Ext4 档案系统时用来决定是否需要完成日志纪录中未完成的工作,快速自动修复档案系统
  • extent - 支援 Ext4 extent 功能,可以加快档案系系效能和减少 external fragmentation,使用bigalloc 的则必须enable
  • extentflex_bg - allows the per-block group metadata (allocation bitmaps and inode tables) to be placed anywhere on the storage ,In addition, mke2fs will place the per-block group metadata together starting at the first block group of each "flex_bg group" ,The size of the flex_bg group can be specified using the -G option.
  • sparse_super - 只有少数 superblock 备份,而不是每个区块组都有 superblock 备份,节省空间。
  • large_file - 支援大于 2GiB 的档案
  • huge_file - allows files to be larger than 2 terabytes in size
  • uninit_bg - ext4 file system feature indicates that the block group descriptors will be protected using checksums,making it safe for mke2fs(8) to create a file system without initializing all of the block groups. The kernel will keep a high watermark of unused inodes, and initialize inode tables and block lazily. This feature speeds up the time to check the file system using e2fsck(8), and it also speeds up the time required for mke2fs(8) to create the file system.
  • dir_nlink - 每个目录支持65000以上的目录数量
  • extra_isize - reserves a specific amount of space in each inode for extended metadata such as nanosecond timestamps and file creation time,inode size must be 256 bytes in size or larger
    下面的则不是dumpe2fs -h 输出的,而是我在man ext4里面看到的
    bigalloc - enables clustered block allocation, so that the unit of allocation is a power of two number of blocks. That is, each bit in the what had traditionally been known as the block allocation bitmap now indicates whether a cluster is in use or not, where a cluster is by default composed of 16 blocks. This feature can decrease the time spent on doing block allocation and brings smaller fragmentation,especially for large files. The size can be specified using the mke2fs -C option.
  • encrypt - This ext4 feature provides file-system level encryption of data blocks and file names. The inode metadata (timestamps, file size, user/group ownership, etc.) is not encrypted.This feature is most useful on file systems with multiple users, or where not all files should be encrypted. In many use cases, especially on single-user systems, encryption at the block device layer using dm-crypt may provide much better security.
  • journal_dev - This feature is enabled on the superblock found on an external journal device. The block size for the external journal must be the same as the file system which uses it.
    The external journal device can be used by a file system by specifying the -J device= option to mke2fs(8) or tune2fs(8).
  • mmp - This ext4 feature provides multiple mount protection (MMP). MMP helps to protect the filesystem from being multiply mounted and is useful in shared storage environments
  • project - This ext4 feature provides project quota support. With this feature, the project ID of inode will be managed when the filesystem is mounted.
  • meta_bg - This ext4 feature allows file systems to be resized on-line without explicitly needing to reserve space for growth in the size of the block group descriptors. This scheme is also used to resize file systems which are larger than 2^32 blocks. It is not recommended that this fea ture be set when a file system is created, since this alternate method of storing the block group descriptors will slow down the time needed to mount the file system, and newer kernels can automatically set this feature as necessary when doing an online resize and no more reserved space is available in the resize inode
  • sparse_super2 - This feature indicates that there will only be at most two backup superblocks and block group descriptors. The block groups used to store the backup superblock(s) and blockgroup descriptor(s) are stored in the superblock, but typically, one will be located at the beginning of block group #1, and one in the last block group in the file system. This feature is essentially a more extreme version of sparse_super and is designed to allow a much larger percentage of the disk to have contiguous blocks available for data files.
  • inline_data - Allow data to be stored in the inode and extended attribute area
  • 档案系统旗号 (Filesystem flags) - signed_directory_hash
  • 缺省挂载选项 (Default mount options) - 挂载此档案系统缺省会使用的选项
  • 档案系统状态 (Filesystem state) - 可以为 clean (档案系统已成功地被卸载)、not-clean (表示档案系统挂载成读写模式后,仍未被卸载) 或 erroneous (档案系统被发现有问题)
  • 错误处理方案 (Errors behavior) - 档案系统发生问题时的处理方案,可以为 continue (继续正常运作) 、remount-ro (重新挂载成只读模式) 或 panic (即时当掉系统)。可以使用 tune2fs -e 改变。
  • 作业系统类型 (Filesystem OS type) - 建立档案系统的作业系统,可以为 Linux/Hurd/MASIX/FreeBSD/Lites[1]
  • Inode 数目 (Inode count) - 档案系统的总 inode 数目,亦是整个档案系统所可能拥有档案数目的上限
  • 区块数目 (Block count) - 档案系统的总区块数目
  • 保留区块数目 (Reserved block count) - 保留给系统管理员工作之用的区块数目
  • 未使用区块数目 (Free blocks) - 未使用区块数目
  • 未使用 inode 数目 (Free inodes) - 未使用 inode 数目
  • 第一个区块编数 (First block) - Superblock 或第一个区块组开始的区块编数。此值在 1 KiB 区块大小的档案系统为 1,大于1 KiB 区块大小的档案系统为 0。(Superblock/第一个区块组一般都在档案系统 0x0400 (1024) 开始)[1]
  • 区块大小 (Block size) - 区块大小,可以为 1024, 2048 或 4096 字节 (Compaq Alpha 系统可以使用 8192 字节的区块)
  • Fragment 大小 (Fragment size) - 实际上 Ext2/Ext3/Ext4 未有支援 Fragment,所以此值一般和区块大小一样
  • 保留 GDT 区块数目 (Reserved GDT blocks) - 保留作在线 (online) 改变档案系统大小的区块数目。若此值为 0,只可以先卸载才可脱机改变档案系统大小[3]
  • 区块/组 (Blocks per group) - 每个区块组的区块数目
  • Fragments/组 (Fragments per group) - 每个区块组的 fragment 数目,亦用来计算每个区块组中 block bitmap 的大小
  • Inodes/组 (Inodes per group) - 每个区块组的 inode 数目
  • Inode 区块/组 (Inode blocks per group) - 每个区块组的 inode 区块数目
  • (Flex block group size) - 16
  • 档案系统建立时间 (Filesystem created) - 格式化此档案系统的时间
  • 最后挂载时间 (Last mount time) - 上一次挂载此档案系统的时间
  • 最后改动时间 (Last write time) - 上一次改变此档案系统内容的时间
  • 挂载次数 (Mount count) - 距上一次作完整档案系统检查后档案系统被挂载的次数,让 fsck 决定是否应进行另一次完整档案系统检查
  • 最大挂载次数 (Maximum mount count) - 档案系统进行另一次完整检查可以被挂载的次数,若挂载次数 (Mount count) 大于此值,fsck 会进行另一次完整档案系统检查
  • 最后检查时间 (Last checked) - 上一次档案系统作完整检查的时间
  • 检查间距 (Check interval) - 档案系统应该进行另一次完整检查的最大时间距
  • 下次检查时间 (Next check after) - 下一次档案系统应该进行另一次完整检查的时间
  • 保留区块使用者识别码 (Reserved blocks uid) - 0 (user root)
  • 保留区块群组识别码 (Reserved blocks gid) - 0 (group root)
  • 第一个 inode (First inode) - 第一个可以用作存放正常档案属性的 inode 编号,在原格式此值一定为 11, V2 格式亦可以改变此值[1]
  • Inode 大小 (Inode size) - Inode 大小,传统为 128 字节,新系统会使用 256 字节的 inode 令扩充功能更方便
  • (Required extra isize) - 28
  • (Desired extra isize) - 28
  • 日志 inode (Journal inode) - 日志档案的 inode 编号
  • 缺省目录 hash 算法 (Default directory hash) - half_md4
  • 目录 hash 种子 (Directory Hash Seed) - 17e9c71d-5a16-47ad-b478-7c6bc3178f1d
  • 日志备份 (Journal backup) - inode blocks
  • 日志大小 (Journal size) - 日志档案的大小

3:Ext4预留inode

Inode号

用途

0

不存在0号inode

1

损坏数据块链表

2

根目录

3

ACL索引

4

ACL数据

5

Boot loader

6

未删除的目录

7

预留的块组描述符inode

8

日志inode

11

第一个非预留的inode,通常是lost+found目录

4:Extents

在extent tree中,每一个节点都是一个block,对于存放extent tree节点信息的block,我们可以称之为extent block。当然其根节点是存放在inode table中,所以根节点所在的block就不这么称呼了。每个节点在其offset为0的地方都有一个结构体ext4_extent_header用于描述该节点。非叶子节点用ext4_extent_idx存放下一个节点信息,叶子节点用ext4_extent存放数据块信息。

  
struct ext4_extent_idx {
	__le32	ei_block;   /* 索引覆盖的逻辑块起始号 */
	__le32	ei_leaf_lo; /* 指向下一级物理块指针的低32位 */
                      /* 指向下一级物理块指针的高16位 */
	__le16	ei_leaf_hi; /* 未使用空间(保留字段) */
	__u16	ei_unused;
};
  
  
struct ext4_extent {
	__le32	ee_block;      /* 该 extent 覆盖的第一个逻辑块号 */
	__le16	ee_len;        /* extent 覆盖的块数量 */
	__le16	ee_start_hi;   /* 起始物理块号的高 16 位 */
	__le32	ee_start_lo;   /* 起始物理块号的低 32 位 */
};
  
  
struct ext4_extent_header {
	__le16	eh_magic;       /* 魔数,用于校验 */
	__le16	eh_entries;     /* 当前节点中有效条目数量 */
	__le16	eh_max;         /* 节点可容纳的最大条目数量 */
	__le16	eh_depth;       /* 节点在树中的深度 */
	__le32	eh_generation;  /* 树的生成号(用于一致性校验) */
};
  

5:dir_index功能

在目录下查找目录下的文件,正常是一个线性扫描的过程。在目录下文件数目比较少的情况下,这种方法还是不错的,但是如果一个目录下有几万几十万个条目,这个方法就比较慢了, 原因在于线性扫描,而且1个block(4096字节)基本只能放下几十~200个条目,一旦需要几十几百个block,那么为了获取子文件的inode,这个DISK IO的消耗是不能忍受的,因此开发了dir_index的功能。

dir_index是采用hash tree的方式来存放entry,而不是线性往后追加。注意,并不说打开了dir_index功能,所有的目录都一律使用hash tree的方式存储。当目录下的条目并不多的时候,并不采用hash tree,还是采用线性目录。

下图是dir_indexs实现原理图:

可以看出具体实现是:取文件名的哈希值的几位作为目录名,例如取哈希值的最后两位或四位,这样可以创建256或1024个子目录,文件根据哈希值被分配到这些子目录中。这样减少了在单个目录下进行文件查找时的遍历时间,因为文件系统在查找文件时,需要遍历目录中的每个条目,文件越多,这个过程就越慢。


6:ext4 inode分配过程


6.1 find_group_orlov - 目录文件分配

  
/*
 * Orlov's 目录分配器。
 * 1. 尝试分散第一级目录(根目录下的目录)。
 * 2. 如果某个块组(Block Group)的空闲 inode 和空闲簇(Cluster)不低于平均水平,则选择目录数最少的那个。
 * 3. 否则,随机返回一个块组。
 * 4. 对于非一级目录,优先放入父目录所在的组,除非该组:
 *    - 目录太多 (max_dirs)
 *    - 空闲 inode 太少 (min_inodes)
 *    - 空闲簇太少 (min_clusters)
 */

static int find_group_orlov(struct super_block *sb, struct inode *parent,
                ext4_group_t *group, umode_t mode,
                const struct qstr *qstr)
{
    // 获取父目录所在的块组编号
    ext4_group_t parent_group = EXT4_I(parent)->i_block_group;
    struct ext4_sb_info *sbi = EXT4_SB(sb);
    // 获取整个文件系统的总块组数
    ext4_group_t real_ngroups = ext4_get_groups_count(sb);
    int inodes_per_group = EXT4_INODES_PER_GROUP(sb);
    unsigned int freei, avefreei, grp_free;
    ext4_fsblk_t freec, avefreec;
    unsigned int ndirs;
    int max_dirs, min_inodes;
    ext4_grpblk_t min_clusters;
    ext4_group_t i, grp, g, ngroups;
    struct ext4_group_desc *desc;
    struct orlov_stats stats;
    // flex_size 是 Ext4 的特性,将多个块组逻辑合并在一起管理(减少碎片)
    int flex_size = ext4_flex_bg_size(sbi);
    struct dx_hash_info hinfo;

    ngroups = real_ngroups; //块组数量
    // 如果启用了 flex_bg,则按 flexgroup 为单位进行计算,而不是单个块组
    if (flex_size > 1) {
        ngroups = (real_ngroups + flex_size - 1) >>
            sbi->s_log_groups_per_flex;
        parent_group >>= sbi->s_log_groups_per_flex; // 获取父目录所在的 flexgroup 索引
    }

    // 从 CPU 计数器读取全局状态:空闲 inode 总数
    freei = percpu_counter_read_positive(&sbi->s_freeinodes_counter);
    // 计算每个组(或 flexgroup)的平均空闲 inode 数
    avefreei = freei / ngroups;
    // 读取全局空闲簇总数并计算平均值
    /* 非 Bigalloc 模式:1 个簇 = 1 个物理块(Block,通常 4KB)。此时 min_clusters 就等同于最小剩余块数。
     * Bigalloc 模式:为了支持超大文件系统,ext4 可以将多个块(如 16 个)组合成一个“簇”进行统一管理。此时 min_clusters 指的是这种逻辑分配单位的数量。
     */
    freec = percpu_counter_read_positive(&sbi->s_freeclusters_counter);
    avefreec = freec;
    do_div(avefreec, ngroups); // 64位除法:avefreec /= ngroups
    // 读取全局已有的目录总数
    ndirs = percpu_counter_read_positive(&sbi->s_dirs_counter);

    // 第一部分:处理根目录下的子目录或标记为 TOPDIR 的目录(旨在打散分布)
    if (S_ISDIR(mode) &&
        ((parent == d_inode(sb->s_root)) ||
         (ext4_test_inode_flag(parent, EXT4_INODE_TOPDIR)))) {
        int best_ndir = inodes_per_group;
        int ret = -1;

        // 如果有目录名(qstr),则根据目录名 Hash 来决定初始搜索位置,实现离散化
        if (qstr) {
            hinfo.hash_version = DX_HASH_HALF_MD4;
            hinfo.seed = sbi->s_hash_seed;
            ext4fs_dirhash(parent, qstr->name, qstr->len, &hinfo);
            parent_group = hinfo.hash % ngroups;
        } else
            // 否则随机选择一个组开始搜索
            parent_group = get_random_u32_below(ngroups);
        
        // 循环遍历所有组,寻找最适合放置新目录的组
        for (i = 0; i < ngroups; i++) {
            g = (parent_group + i) % ngroups;
            get_orlov_stats(sb, g, flex_size, &stats); // 获取该组的统计信息(inode, dirs, blocks)
            if (!stats.free_inodes) // 无 inode 直接跳过
                continue;
            if (stats.used_dirs >= best_ndir) // 找目录数最少的组
                continue;
            if (stats.free_inodes < avefreei) // 必须优于平均水平
                continue;
            if (stats.free_clusters < avefreec) // 空间也必须优于平均水平
                continue;
            grp = g;
            ret = 0;
            best_ndir = stats.used_dirs; // 更新找到的最佳目录数
        }
        if (ret) // 如果没找到符合条件的,跳转到 fallback
            goto fallback;
        
    found_flex_bg:
        // 如果找到了合适的 flexgroup,现在需要定位到具体的某个物理块组
        if (flex_size == 1) {
            *group = grp;
            return 0;
        }

        /*
         * 我们倾向于将 inode 集中在 flexgroup 的起始块组中。
         * 块分配会做类似的决策,从而保证数据和 inode 在物理上接近。
         */
        grp *= flex_size;
        for (i = 0; i < flex_size; i++) {
            if (grp+i >= real_ngroups)
                break;
            desc = ext4_get_group_desc(sb, grp+i, NULL);
            if (desc && ext4_free_inodes_count(sb, desc)) {
                *group = grp+i; // 找到第一个有空闲 inode 的物理组
                return 0;
            }
        }
        goto fallback;
    }

    // 第二部分:处理普通文件或子目录(旨在靠近父目录)
    // 计算软阈值:当前组允许的最大目录数、最小空闲 inode 数、最小空闲簇大小
    max_dirs = ndirs / ngroups + inodes_per_group*flex_size / 16; 
    // ndirs / ngroups 是文件系统的平均目录密度。inodes_per_group / 16 增加了一个宽容度,即允许某个组内的目录数量超出平均值,但不能超过该组 Inode 总容量的 1/16。
    min_inodes = avefreei - inodes_per_group*flex_size / 4;
    // 减去 ... / 4 意味着:只要该块组的资源不比系统平均值低太多(即差额不超过该组总容量的 1/4),就认为它是可用的。
    if (min_inodes < 1)
        min_inodes = 1;
    min_clusters = avefreec - EXT4_CLUSTERS_PER_GROUP(sb)*flex_size / 4;
    if (min_clusters < 0)
        min_clusters = 0;

    /*
     * 如果该父目录之前刚分配过 inode,优先从上次分配的那个组开始找
     */
    if (EXT4_I(parent)->i_last_alloc_group != ~0) {
        parent_group = EXT4_I(parent)->i_last_alloc_group;
        if (flex_size > 1)
            parent_group >>= sbi->s_log_groups_per_flex;
    }

    // 尝试寻找满足性能软阈值的组(既靠近父目录,又不会负载过重)
    for (i = 0; i < ngroups; i++) {
        grp = (parent_group + i) % ngroups;
        get_orlov_stats(sb, grp, flex_size, &stats);
        if (stats.used_dirs >= max_dirs)
            continue;
        if (stats.free_inodes < min_inodes)
            continue;
        if (stats.free_clusters < min_clusters)
            continue;
        goto found_flex_bg; // 找到了符合“局部性”要求的理想组
    }

fallback:
    // 第三部分:备选方案。如果前面的精细化选择都失败了
    ngroups = real_ngroups;
    avefreei = freei / ngroups; // 重新计算单个物理组的平均 inode 数
/* 
 * 标签:fallback_retry
 * 当第一轮严格筛选(考虑负载均衡、空间对齐等)失败后,
 * 代码会跳转到这里,放宽条件进行“保底”搜索。
 */
fallback_retry:

    /* 
     * 获取父目录所在的物理块组索引。
     * 目的是为了尽量让新目录/文件靠近父目录存放,以保持数据的局部性(Locality)。
     */
    parent_group = EXT4_I(parent)->i_block_group;

    /* 
     * 开始遍历所有可用的组(ngroups 代表总组数)。
     * 这是一个环形遍历,确保能检查到文件系统中的每一个角落。
     */
    for (i = 0; i < ngroups; i++) {

        /* 
         * 环形索引计算:从父目录所在的组开始,依次向后搜索。
         * (parent_group + 0), (parent_group + 1) ... 直到回到 parent_group - 1。
         */
        grp = (parent_group + i) % ngroups;

        /* 
         * 获取当前块组的描述符 (Group Descriptor)。
         * 描述符包含了该组的元数据状态,比如剩余 Inode 数、剩余块数等。
         */
        desc = ext4_get_group_desc(sb, grp, NULL);

        /* 如果描述符读取正常(即该组元数据未损坏) */
        if (desc) {
            /* 获取该组内当前可用的空闲 Inode 数量 */
            grp_free = ext4_free_inodes_count(sb, desc);

            /* 
             * 核心判定条件(保底模式):
             * 1. grp_free: 组内必须至少有一个空闲 Inode。
             * 2. grp_free >= avefreei: 
             *    - 在第二次重试时,avefreei 通常会被设置为 0。
             *    - 此时条件的实际含义是:“只要这个组里有空闲 Inode,我就要把新文件塞进去”。
             */
            if (grp_free && grp_free >= avefreei) {
                *group = grp;   /* 将选中的组编号通过指针传回给调用者 */
                return 0;        /* 成功找到目标组,返回 0 */
            }
        }
    }

    if (avefreei) {
        /*
         * 由于空闲计数器是近似值,如果上述循环没找到,
         * 将平均值降为 0(即:只要有空闲 inode 的组就行),重新搜一遍。
         */
        avefreei = 0;
        goto fallback_retry;
    }

    return -1; // 彻底找不到空闲 inode
}                 
  

6.2 find_group_other - 非目录文件分配

  
static int find_group_other(struct super_block *sb, struct inode *parent,
                ext4_group_t *group, umode_t mode)
{
    // 1. 初始化变量
    ext4_group_t parent_group = EXT4_I(parent)->i_block_group; // 父目录所在块组
    ext4_group_t i, last, ngroups = ext4_get_groups_count(sb); // 循环变量和组总数
    struct ext4_group_desc *desc;                             // 块组描述符指针
    int flex_size = ext4_flex_bg_size(EXT4_SB(sb));           // flex_bg大小

    /*
     * 2. flex_bg处理策略:
     * 尝试将inode分配在与父目录相同的flex group中。
     * 如果找不到空间,则使用Orlov算法查找另一个flex group,
     * 并将该信息存储在父目录的inode中,以便将来分配使用。
     */
    if (flex_size > 1) {                                      // 启用了flex_bg
        int retry = 0;                                        // 重试标志

    try_again:
        // 2.1 对齐到flex group边界
        parent_group &= ~(flex_size-1);                       // 清除低位,得到flex group起始组
        last = parent_group + flex_size;                      // 计算flex group结束组
        if (last > ngroups)                                   // 确保不超出总组数
            last = ngroups;
        
        // 2.2 在flex group内搜索空闲inode
        for (i = parent_group; i < last; i++) {
            desc = ext4_get_group_desc(sb, i, NULL);          // 获取组描述符
            if (desc && ext4_free_inodes_count(sb, desc)) {   // 有空闲inode
                *group = i;                                   // 返回组号
                return 0;                                     // 分配成功
            }
        }
        
        // 2.3 首次失败后重试(使用父目录的最后分配组)
        if (!retry && EXT4_I(parent)->i_last_alloc_group != ~0) {
            retry = 1;                                        // 设置重试标志
            parent_group = EXT4_I(parent)->i_last_alloc_group; // 使用最后分配的组
            goto try_again;                                   // 重新尝试
        }
        
        /*
         * 2.4 回退到Orlov搜索算法
         * 如果上述方法失败,使用Orlov算法查找新flex group
         * 传入mode参数避免使用顶层目录算法
         */
        *group = parent_group + flex_size;                   // 移动到下一个flex group
        if (*group > ngroups)                                // 超出总组数
            *group = 0;                                      // 回绕到0组
        return find_group_orlov(sb, parent, group, mode, NULL); // 调用Orlov算法
    }

    /*
     * 3. 非flex_bg策略(标准处理)
     * 尝试将inode分配在父目录所在的块组
     */
    *group = parent_group;                                   // 初始设为父目录组
    desc = ext4_get_group_desc(sb, *group, NULL);            // 获取组描述符
    // 检查是否有空闲inode和簇
    if (desc && ext4_free_inodes_count(sb, desc) &&
        ext4_free_group_clusters(sb, desc))
        return 0;                                            // 满足条件则成功返回

    /*
     * 4. 二次哈希搜索策略
     * 我们需要将inode分配到与父目录不同的块组。
     * 我们希望相同目录的文件位于相同块组,
     * 但文件目录所在父目录不同的文件应分配到不同块组。比如:/opt/test/1.txt和/opt/test/2.txt应该分配在同一块组。 /opt/test/1.txt和/www/test/2.txt应该分配在不同块组
     *
     * 解决方案:将父目录的i_ino加入哈希起点
     */
    *group = (*group + parent->i_ino) % ngroups;             // 基于inode号的哈希

    /*
     * 5. 二次探测法(Quadratic probing)
     * 使用二次哈希查找具有空闲inode和空闲簇的块组
     */
    for (i = 1; i < ngroups; i <<= 1) {                      // i按1,2,4,8...递增
        *group += i;                                         // 增加步长
        if (*group >= ngroups)                               // 处理回绕
            *group -= ngroups;
        desc = ext4_get_group_desc(sb, *group, NULL);
        // 检查空闲inode和簇
        if (desc && ext4_free_inodes_count(sb, desc) &&
            ext4_free_group_clusters(sb, desc))
            return 0;                                        // 找到合适组
    }

    /*
     * 6. 线性搜索保底策略
     * 二次哈希失败:线性搜索空闲inode(即使没有空闲簇)
     */
    *group = parent_group;                                   // 重置为父目录组
    for (i = 0; i < ngroups; i++) {
        if (++*group >= ngroups)                             // 移动到下一组
            *group = 0;                                      // 回绕处理
        desc = ext4_get_group_desc(sb, *group, NULL);
        if (desc && ext4_free_inodes_count(sb, desc))        // 只需空闲inode
            return 0;                                        // 找到合适组
    }

    // 7. 完全失败
    return -1;                                               // 未找到合适块组
}       
  

6.3 __ext4_new_inode

  
/*
 * There are two policies for allocating an inode.  If the new inode is
 * a directory, then a forward search is made for a block group with both
 * free space and a low directory-to-inode ratio; if that fails, then of
 * the groups with above-average free space, that group with the fewest
 * directories already is chosen.
 *
 * For other inodes, search forward from the parent directory's block
 * group to find a free inode.
 */
struct inode *__ext4_new_inode(handle_t *handle, struct inode *dir,
                   umode_t mode, const struct qstr *qstr,
                   __u32 goal, uid_t *owner, __u32 i_flags,
                   int handle_type, unsigned int line_no,
                   int nblocks)
{
    struct super_block *sb;                                              // 文件系统超级块
    struct buffer_head *inode_bitmap_bh = NULL;                          // inode 位图的缓冲头(稍后读取)
    struct buffer_head *group_desc_bh;                                   // group descriptor 的缓冲头
    ext4_group_t ngroups, group = 0;                                     // AG 总数、目标分配组(初始化为 0)
    unsigned long ino = 0;                                               // 组内相对 inode 号(从 0 开始)
    struct inode *inode;                                                 // 新分配的 inode 结构
    struct ext4_group_desc *gdp = NULL;                                  // 当前 AG 的 group descriptor
    struct ext4_inode_info *ei;                                          // ext4 私有 inode 信息
    struct ext4_sb_info *sbi;                                            // ext4 超级块扩展信息
    int ret2, err;                                                       // 临时返回值、错误码
    struct inode *ret;                                                   // 最终返回的 inode(成功时为 inode,失败为 ERR_PTR)
    ext4_group_t i;                                                      // 循环变量(扫描 AG 用)
    ext4_group_t flex_group;                                             // flex_bg 组编号(用于更新 flex 统计)
    struct ext4_group_info *grp;                                         // 当前 AG 的 group info(内存缓存结构)
    int encrypt = 0;                                                     // 是否需要加密上下文继承

    /* Cannot create files in a deleted directory */                    // 父目录已被删除(nlink=0),禁止创建子文件
    if (!dir || !dir->i_nlink)
        return ERR_PTR(-EPERM);

    sb = dir->i_sb;                                                      // 从父目录获取超级块
    sbi = EXT4_SB(sb);                                                   // 获取 ext4 私有超级块信息

    if (unlikely(ext4_forced_shutdown(sbi)))                             // 文件系统强制 shutdown(umount -f 或错误),返回 IO 错误
        return ERR_PTR(-EIO);

    // 检查是否需要加密(父目录加密 或 文件系统全局 dummy 加密,且创建的是 reg/dir/symlink,且不是纯 EA inode)
    if ((ext4_encrypted_inode(dir) || DUMMY_ENCRYPTION_ENABLED(sbi)) &&
        (S_ISREG(mode) || S_ISDIR(mode) || S_ISLNK(mode)) &&
        !(i_flags & EXT4_EA_INODE_FL)) {
        err = fscrypt_get_encryption_info(dir);                      // 获取父目录的加密信息(密钥等)
        if (err)
            return ERR_PTR(err);
        if (!fscrypt_has_encryption_key(dir))                        // 父目录无加密密钥,无法继承
            return ERR_PTR(-ENOKEY);
        encrypt = 1;                                                 // 标记需要为新 inode 继承加密上下文
    }

    // 如果没有传入事务句柄,且有日志,且不是纯 EA inode → 需要预估创建所需的日志块数
    if (!handle && sbi->s_journal && !(i_flags & EXT4_EA_INODE_FL)) {
#ifdef CONFIG_EXT4_FS_POSIX_ACL
        struct posix_acl *p = get_acl(dir, ACL_TYPE_DEFAULT);        // 获取父目录的默认 ACL

        if (IS_ERR(p))
            return ERR_CAST(p);
        if (p) {
            int acl_size = p->a_count * sizeof(ext4_acl_entry);  // 计算 ACL 大小

            // 预估设置 ACL 所需的额外日志块(目录多 1 倍)
            nblocks += (S_ISDIR(mode) ? 2 : 1) *
                __ext4_xattr_set_credits(sb, NULL, NULL, acl_size, true);
            posix_acl_release(p);
        }
#endif

#ifdef CONFIG_SECURITY
        {
            int num_security_xattrs = 1;                             // 至少有一个 security.*

#ifdef CONFIG_INTEGRITY
            num_security_xattrs++;                                   // integrity.* 额外一个
#endif
            // 保守假设 security xattr ≤ 1KB,预估日志信用
            nblocks += num_security_xattrs *
                __ext4_xattr_set_credits(sb, NULL, NULL, 1024, true);
        }
#endif
        if (encrypt)
            // 加密上下文 xattr 最大大小,预估日志信用
            nblocks += __ext4_xattr_set_credits(sb, NULL, NULL,
                    FSCRYPT_SET_CONTEXT_MAX_SIZE, true);
    }

    ngroups = ext4_get_groups_count(sb);                                 // 获取文件系统 AG 总数
    trace_ext4_request_inode(dir, mode);                                 // 跟踪:请求分配 inode

    inode = new_inode(sb);                                               // 分配通用 inode 结构(内核通用层)
    if (!inode)
        return ERR_PTR(-ENOMEM);
    ei = EXT4_I(inode);                                                  // 获取 ext4 私有 inode 信息

    // 提前初始化 owner 和 quota(避免后续事务中 quota 初始化占用过多块)
    if (owner) {                                                         // 调用者显式指定 uid/gid
        inode->i_mode = mode;
        i_uid_write(inode, owner[0]);
        i_gid_write(inode, owner[1]);
    } else if (test_opt(sb, GRPID)) {                                    // grpid 挂载选项:继承父 gid
        inode->i_mode = mode;
        inode->i_uid = current_fsuid();
        inode->i_gid = dir->i_gid;
    } else
        inode_init_owner(inode, dir, mode);                          // 默认规则:uid=current,gid=父或默认

    // 项目 ID 继承(project quota)
    if (ext4_has_feature_project(sb) &&
        ext4_test_inode_flag(dir, EXT4_INODE_PROJINHERIT))
        ei->i_projid = EXT4_I(dir)->i_projid;
    else
        ei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);

    err = dquot_initialize(inode);                                       // 初始化 quota 结构(提前做,避免事务中失败)
    if (err)
        goto out;

    if (!goal)                                                           // 如果没有指定目标 inode 号,使用全局默认目标
        goal = sbi->s_inode_goal;

    // 如果指定了 goal(目标 inode 号),直接计算对应的 group 和 ino
    if (goal && goal <= le32_to_cpu(sbi->s_es->s_inodes_count)) {
        group = (goal - 1) / EXT4_INODES_PER_GROUP(sb);              // 计算目标 AG
        ino = (goal - 1) % EXT4_INODES_PER_GROUP(sb);                // 组内偏移
        ret2 = 0;                                                    // 标记成功找到 group
        goto got_group;
    }

    // 根据类型选择分配策略(核心分发点)
    if (S_ISDIR(mode))
        ret2 = find_group_orlov(sb, dir, &group, mode, qstr);        // 目录 → Orlov 算法(分散 + 密度控制)
    else
        ret2 = find_group_other(sb, dir, &group, mode);              // 普通文件 → 亲和父组 + hash 二次方探测

got_group:
    EXT4_I(dir)->i_last_alloc_group = group;                             // 更新父目录的“上次分配组”,供后续子文件参考
    err = -ENOSPC;
    if (ret2 == -1)                                                      // 分配组失败(所有 AG 都不行)
        goto out;

    // 循环扫描 AG(通常只走一次,除非竞争导致选中的组 inode 被抢)
    for (i = 0; i < ngroups; i++, ino = 0) {                             // i 是扫描轮数,ino 重置为 0
        err = -EIO;

        gdp = ext4_get_group_desc(sb, group, &group_desc_bh);        // 获取当前 AG 的 group descriptor
        if (!gdp)
            goto out;

        // 先检查空闲 inode 计数(快速过滤)
        if (ext4_free_inodes_count(sb, gdp) == 0)
            goto next_group;

        grp = ext4_get_group_info(sb, group);                        // 获取内存中的 AG 信息
        // 如果 inode 位图已知损坏,跳过
        if (EXT4_MB_GRP_IBITMAP_CORRUPT(grp))
            goto next_group;

        brelse(inode_bitmap_bh);                                     // 释放旧位图(如果有)
        inode_bitmap_bh = ext4_read_inode_bitmap(sb, group);         // 读取 inode 位图
        if (EXT4_MB_GRP_IBITMAP_CORRUPT(grp) ||
            IS_ERR(inode_bitmap_bh)) {                               // 位图损坏或读取失败
            inode_bitmap_bh = NULL;
            goto next_group;
        }

repeat_in_this_group:
        // 在位图中找第一个空闲位(返回 1 表示找到,ino 被更新)
        ret2 = find_inode_bit(sb, group, inode_bitmap_bh, &ino);
        if (!ret2)
            goto next_group;

        // AG 0 的前几个 inode 是保留的(1~10 通常为特殊用途)
        if (group == 0 && (ino + 1) < EXT4_FIRST_INO(sb)) {
            ext4_error(sb, "reserved inode found cleared - inode=%lu", ino + 1);
            ext4_mark_group_bitmap_corrupted(sb, group, EXT4_GROUP_INFO_IBITMAP_CORRUPT);
            goto next_group;
        }

        // 如果没有事务句柄,立即启动一个(nblocks 是预估的日志块数)
        if (!handle) {
            BUG_ON(nblocks <= 0);
            handle = __ext4_journal_start_sb(dir->i_sb, line_no,
                             handle_type, nblocks, 0);
            if (IS_ERR(handle)) {
                err = PTR_ERR(handle);
                ext4_std_error(sb, err);
                goto out;
            }
        }

        BUFFER_TRACE(inode_bitmap_bh, "get_write_access");
        err = ext4_journal_get_write_access(handle, inode_bitmap_bh);// 获取位图写权限
        if (err) {
            ext4_std_error(sb, err);
            goto out;
        }

        ext4_lock_group(sb, group);                                  // 锁定当前 AG(防止并发分配)
        ret2 = ext4_test_and_set_bit(ino, inode_bitmap_bh->b_data);  // 测试并设置位(原子操作)
        if (ret2) {                                                  // 已经被别人抢了
            // 重新在锁保护下找位(避免重试时又被抢)
            ret2 = find_inode_bit(sb, group, inode_bitmap_bh, &ino);
            if (ret2) {
                ext4_set_bit(ino, inode_bitmap_bh->b_data);  // 强制设置(已确认安全)
                ret2 = 0;
            } else {
                ret2 = 1;                                        // 没找到,标记失败
            }
        }
        ext4_unlock_group(sb, group);

        ino++;                                                       // 位图从 0 开始,实际 inode 号 +1
        if (!ret2)
            goto got;                                                // 成功抢到 inode!

        if (ino < EXT4_INODES_PER_GROUP(sb))                         // 组内还有剩余位,继续在本组找
            goto repeat_in_this_group;

next_group:
        if (++group == ngroups)                                      // 下一个 AG,环绕
            group = 0;
    }

    err = -ENOSPC;                                                       // 扫描所有 AG 都没找到
    goto out;

got:
    BUFFER_TRACE(inode_bitmap_bh, "call ext4_handle_dirty_metadata");
    err = ext4_handle_dirty_metadata(handle, NULL, inode_bitmap_bh); // 标记位图脏(需写回)
    if (err) {
        ext4_std_error(sb, err);
        goto out;
    }

    BUFFER_TRACE(group_desc_bh, "get_write_access");
    err = ext4_journal_get_write_access(handle, group_desc_bh);      // group desc 也需要写权限
    if (err) {
        ext4_std_error(sb, err);
        goto out;
    }

    // 如果 block bitmap 未初始化(lazy init),在这里初始化它
    if (ext4_has_group_desc_csum(sb) &&
        gdp->bg_flags & cpu_to_le16(EXT4_BG_BLOCK_UNINIT)) {
        struct buffer_head *block_bitmap_bh;

        block_bitmap_bh = ext4_read_block_bitmap(sb, group);
        if (IS_ERR(block_bitmap_bh)) {
            err = PTR_ERR(block_bitmap_bh);
            goto out;
        }
        BUFFER_TRACE(block_bitmap_bh, "get block bitmap access");
        err = ext4_journal_get_write_access(handle, block_bitmap_bh);
        if (err) {
            brelse(block_bitmap_bh);
            ext4_std_error(sb, err);
            goto out;
        }

        BUFFER_TRACE(block_bitmap_bh, "dirty block bitmap");
        err = ext4_handle_dirty_metadata(handle, NULL, block_bitmap_bh);

        // 在锁保护下清除 UNINIT 标志并更新空闲计数
        ext4_lock_group(sb, group);
        if (ext4_has_group_desc_csum(sb) &&
            (gdp->bg_flags & cpu_to_le16(EXT4_BG_BLOCK_UNINIT))) {
            gdp->bg_flags &= cpu_to_le16(~EXT4_BG_BLOCK_UNINIT);
            ext4_free_group_clusters_set(sb, gdp,
                ext4_free_clusters_after_init(sb, group, gdp));
            ext4_block_bitmap_csum_set(sb, group, gdp, block_bitmap_bh);
            ext4_group_desc_csum_set(sb, group, gdp);
        }
        ext4_unlock_group(sb, group);
        brelse(block_bitmap_bh);

        if (err) {
            ext4_std_error(sb, err);
            goto out;
        }
    }

    // 更新 group descriptor 中的空闲 inode 计数等字段
    if (ext4_has_group_desc_csum(sb)) {
        int free;
        struct ext4_group_info *grp = ext4_get_group_info(sb, group);

        down_read(&grp->alloc_sem);                                  // 保护 vs lazy itable init
        ext4_lock_group(sb, group);
        free = EXT4_INODES_PER_GROUP(sb) - ext4_itable_unused_count(sb, gdp);
        if (gdp->bg_flags & cpu_to_le16(EXT4_BG_INODE_UNINIT)) {     // 如果 inode table 未初始化
            gdp->bg_flags &= cpu_to_le16(~EXT4_BG_INODE_UNINIT);
            free = 0;
        }
        // 如果新 inode 号超过了之前的 unused 范围,更新 bg_itable_unused
        if (ino > free)
            ext4_itable_unused_set(sb, gdp, (EXT4_INODES_PER_GROUP(sb) - ino));
        up_read(&grp->alloc_sem);
    } else {
        ext4_lock_group(sb, group);
    }

    // 减少 AG 空闲 inode 计数
    ext4_free_inodes_set(sb, gdp, ext4_free_inodes_count(sb, gdp) - 1);

    if (S_ISDIR(mode)) {                                                 // 如果是目录
        ext4_used_dirs_set(sb, gdp, ext4_used_dirs_count(sb, gdp) + 1); // 增加已用目录计数
        if (sbi->s_log_groups_per_flex) {                               // flex_bg 模式下更新 flex 统计
            ext4_group_t f = ext4_flex_group(sbi, group);
            atomic_inc(&sbi->s_flex_groups[f].used_dirs);
        }
    }

    if (ext4_has_group_desc_csum(sb)) {                                  // 更新校验和
        ext4_inode_bitmap_csum_set(sb, group, gdp, inode_bitmap_bh,
                       EXT4_INODES_PER_GROUP(sb) / 8);
        ext4_group_desc_csum_set(sb, group, gdp);
    }
    ext4_unlock_group(sb, group);

    BUFFER_TRACE(group_desc_bh, "call ext4_handle_dirty_metadata");
    err = ext4_handle_dirty_metadata(handle, NULL, group_desc_bh);       // 标记 group desc 脏
    if (err) {
        ext4_std_error(sb, err);
        goto out;
    }

    percpu_counter_dec(&sbi->s_freeinodes_counter);                      // 全局空闲 inode 计数 -1
    if (S_ISDIR(mode))
        percpu_counter_inc(&sbi->s_dirs_counter);                    // 全局目录计数 +1

    if (sbi->s_log_groups_per_flex) {                                   // flex_bg 模式更新空闲 inode
        flex_group = ext4_flex_group(sbi, group);
        atomic_dec(&sbi->s_flex_groups[flex_group].free_inodes);
    }

    // 计算全局 inode 号
    inode->i_ino = ino + group * EXT4_INODES_PER_GROUP(sb);

    inode->i_blocks = 0;                                                 // 初始块数为 0
    inode->i_mtime = inode->i_atime = inode->i_ctime = current_time(inode); // 时间戳
    ei->i_crtime = inode->i_mtime;                                       // 创建时间 = 修改时间

    memset(ei->i_data, 0, sizeof(ei->i_data));                           // 清空 extent/inline 数据区
    ei->i_dir_start_lookup = 0;                                          // 目录查找缓存清零
    ei->i_disksize = 0;                                                  // 磁盘大小 0

    // 继承标志,但只保留可继承的部分,并加上调用者传入的 i_flags
    ei->i_flags = ext4_mask_flags(mode, EXT4_I(dir)->i_flags & EXT4_FL_INHERITED);
    ei->i_flags |= i_flags;
    ei->i_file_acl = 0;                                                  // 无 ACL(后续初始化)
    ei->i_dtime = 0;                                                     // 删除时间 0
    ei->i_block_group = group;                                           // 记录所在 AG
    ei->i_last_alloc_group = ~0;                                         // 上次分配组重置

    ext4_set_inode_flags(inode);                                         // 根据标志设置 VFS inode 标志

    if (IS_DIRSYNC(inode))                                               // dirsync 模式,同步事务
        ext4_handle_sync(handle);

    // 插入 inode cache(哈希表),如果冲突说明双重分配(位图损坏)
    if (insert_inode_locked(inode) < 0) {
        err = -EIO;
        ext4_error(sb, "failed to insert inode %lu: doubly allocated?", inode->i_ino);
        ext4_mark_group_bitmap_corrupted(sb, group, EXT4_GROUP_INFO_IBITMAP_CORRUPT);
        goto out;
    }

    inode->i_generation = prandom_u32();                                 // 随机生成代数(NFS 等用)

    // 如果支持元数据校验和,预计算 inode csum seed
    if (ext4_has_metadata_csum(sb)) {
        __u32 csum;
        __le32 inum = cpu_to_le32(inode->i_ino);
        __le32 gen = cpu_to_le32(inode->i_generation);
        csum = ext4_chksum(sbi, sbi->s_csum_seed, (__u8 *)&inum, sizeof(inum));
        ei->i_csum_seed = ext4_chksum(sbi, csum, (__u8 *)&gen, sizeof(gen));
    }

    ext4_clear_state_flags(ei);                                          // 清理状态标志(32 位架构相关)
    ext4_set_inode_state(inode, EXT4_STATE_NEW);                         // 标记为新 inode

    ei->i_extra_isize = sbi->s_want_extra_isize;                         // 设置 extra isize(预留空间)
    ei->i_inline_off = 0;
    if (ext4_has_feature_inline_data(sb))                                // 支持 inline data
        ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);

    ret = inode;                                                         // 准备返回
    err = dquot_alloc_inode(inode);                                      // 分配 quota(可能失败)
    if (err)
        goto fail_drop;

    // 加密上下文继承(放在最前面,减少重复)
    if (encrypt) {
        err = fscrypt_inherit_context(dir, inode, handle, true);
        if (err)
            goto fail_free_drop;
    }

    // 非纯 EA inode,需要初始化 ACL 和 security xattr
    if (!(ei->i_flags & EXT4_EA_INODE_FL)) {
        err = ext4_init_acl(handle, inode, dir);
        if (err)
            goto fail_free_drop;

        err = ext4_init_security(handle, inode, dir, qstr);
        if (err)
            goto fail_free_drop;
    }

    // 如果支持 extent,设置标志并初始化 extent 树
    if (ext4_has_feature_extents(sb)) {
        if (S_ISDIR(mode) || S_ISREG(mode) || S_ISLNK(mode)) {
            ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
            ext4_ext_tree_init(handle, inode);
        }
    }

    if (ext4_handle_valid(handle)) {                                     // 记录事务 tid(用于 datasync 等)
        ei->i_sync_tid = handle->h_transaction->t_tid;
        ei->i_datasync_tid = handle->h_transaction->t_tid;
    }

    err = ext4_mark_inode_dirty(handle, inode);                          // 标记 inode 脏(写回)
    if (err) {
        ext4_std_error(sb, err);
        goto fail_free_drop;
    }

    ext4_debug("allocating inode %lu\n", inode->i_ino);
    trace_ext4_allocate_inode(inode, dir, mode);

    brelse(inode_bitmap_bh);                                             // 释放位图缓冲
    return ret;

fail_free_drop:
    dquot_free_inode(inode);                                             // 释放 quota
fail_drop:
    clear_nlink(inode);                                                  // 清空 nlink
    unlock_new_inode(inode);                                             // 解锁(失败路径)
out:
    dquot_drop(inode);                                                   // 丢弃 quota
    inode->i_flags |= S_NOQUOTA;                                         // 标记不再 quota
    iput(inode);                                                         // 释放 inode
    brelse(inode_bitmap_bh);                                             // 清理
    return ERR_PTR(err);
}   
  


九、xfs文件系统

1: 磁盘布局

xfs文件系统磁盘布局


AG:XFS 将磁盘划分为多个 AG(Allocation Group),每一个AG的磁盘布局结构完全一致,独立管理空间和 inode,实现并行操作。但是第0块AG被称为主AG(primary AG),有一些全局统计信息的使用主要是从该AG内获取,而其它AG保存的信息仅做备份。
一块AG的布局结构如下图所示:


超级块(xfs_sb):每个AG的第0块,记录文件系统全局元数据

空闲块头(xfs_agf):每个AG的第1块,AG内空闲空间管理与B+树指向

  
struct xfs_agf {
    __be32 agf_magicnum;     // 魔数标识
    __be32 agf_versionnum;   // 版本号
    __be32 agf_seqno;        // AG 序号
    __be32 agf_length;       // AG 长度(块数)
    __be32 agf_bno_root;     // BNO B+树根节点指针
    __be32 agf_cnt_root;     // CNT B+树根节点指针
    __be32 agf_bno_level;    // BNO B+树高度
    __be32 agf_cnt_level;    // CNT B+树高度
    __be32 agf_flfirst;      // 空闲链表首项
    __be32 agf_fllast;       // 空闲链表末项
    __be32 agf_flcount;      // 空闲链表项数
    __be32 agf_freeblks;     // AG 内空闲块总数
    // ... 其他元数据
};
  

inode头(xfs_agi):每个AG的第2块,AG内inode分配管理

  
struct xfs_agi {
    __be32 agi_magicnum;     // 魔数标识
    __be32 agi_versionnum;   // 版本号
    __be32 agi_seqno;        // AG 序号
    __be32 agi_length;       // AG 长度(块数)
    __be32 agi_count;        // 分配的 inode 数
    __be32 agi_root;         // inode B+树根节点
    __be32 agi_level;        // B+树高度
    __be32 agi_freecount;    // 空闲 inode 数
    __be32 agi_newino;       // 新分配的 inode
    __be32 agi_dirino;       // 目录 inode
    // ... inode 块指针等元数据
};
  

空闲块列表(xfs_agfl):每个AG的第3块,空闲块数组,用于快速分配块

ABTB B+树(xfs_btree):以起始块号为键管理空闲空间,优化随机分配

ABTC B+树(xfs_btree):以连续块数为键管理空闲空间,优化大文件连续分配

IABT B+树(xfs_btree):通过 B+Tree 管理64个inode组成的chunk(块组), 同时管理所有inode的分配状态,IABT 叶节点记录 对应一个 inode chunk

inode chunk:在 XFS 文件系统中,将 64 个 inode(默认大小是 256 字节)打包为一个块(chunk),而该块作为 B + 树的一个叶子节点。XFS 文件系统中的 inode 通过 B + 管理,位置并不确定。因此 XFS 文件系统无法像 ExtX 文件系统那样根据 inode 的偏移来确定编号。

DATA:存储文件内容、目录项、扩展属性等用户数据


2: 档案系统信息

  
magicnum = 0x58465342        // 文件系统标识符("XFSB")
blocksize = 4096             // 文件系统块大小(4KB)
dblocks = 2621440            // 文件系统总数据块数(≈10GB)
rblocks = 0                  // 实时设备块数(XFS实时功能未启用)
rextents = 0                 // 实时扩展区数量(实时功能未启用)
uuid = fd6f2b24-a4a2-4c28-a653-63a06fea15c2  // 文件系统唯一标识符
logstart = 2097158           // 日志区域起始块号(第2097158块)
rootino = 128                // 根目录inode号(文件系统根目录)
rbmino = 129                 // 实时位掩码inode号(实时功能未启用)
rsumino = 130                // 校验和inode号(用于RAID校验)
rextsize = 1                 // 实时扩展区大小(未启用)
agblocks = 655360            // 每个分配组(AG)包含的块数(≈2.5GB)
agcount = 4                  // 分配组总数(共4个AG)
rbmblocks = 0                // 实时位掩码块数(实时功能未启用)
logblocks = 16384            // 日志区域总块数(≈64MB)
versionnum = 0xb4a5          // 文件系统版本标志(XFS v5)
sectsize = 512               // 底层设备扇区大小(512字节)
inodesize = 512              // 单个inode大小(512字节)
inopblock = 8                // 每块包含的inode数(每4KB块含8个inode)
fname = "\000\000\000\000\000\000\000\000\000\000\000\000"  // 文件系统名称(未设置)
blocklog = 12                // 块大小的对数(2^12 = 4096字节)
sectlog = 9                  // 扇区大小的对数(2^9 = 512字节)
inodelog = 9                 // inode大小的对数(2^9 = 512字节)
inopblog = 3                 // 每块inode数的对数(2^3 = 8)
agblklog = 20                // AG块数的对数(2^20 = 1,048,576块)
rextslog = 0                 // 实时扩展区大小的对数(未启用)
inprogress = 0               // 正在进行的操作(无)
imax_pct = 25                // inode最多可占空间百分比(25%)
icount = 64                  // 已分配inode总数
ifree = 60                   // 空闲inode数
fdblocks = 2605023           // 空闲数据块数(≈10GB - 64MB)
frextents = 0                // 空闲实时扩展区(未启用)
uquotino = null              // 用户配额inode(未启用)
gquotino = null              // 组配额inode(未启用)
qflags = 0                   // 配额标志(未启用)
flags = 0                    // 全局标志(无特殊标志)
shared_vn = 0                // 共享卷标识(未启用)
inoalignmt = 8               // inode块对齐倍数(按8块对齐)
unit = 0                     // 条带单元大小(未设置)
width = 0                    // 条带宽度(未设置)
dirblklog = 0                // 目录块大小的对数(默认)
logsectlog = 0               // 日志扇区大小的对数(默认)
logsectsize = 0              // 日志扇区大小(默认)
logsunit = 1                 // 日志条带单元大小(1块)
features2 = 0x18a            // 高级特性标志(包含以下特性)
  - 0x002: 扩展属性(extended attributes)
  - 0x080: 大文件(large file support)
  - 0x100: 稀疏inode(sparse inodes)
bad_features2 = 0x18a        // 不支持的特性标志(与启用特性相同,可能为校验)
features_compat = 0          // 兼容特性(无)
features_ro_compat = 0xd     // 只读兼容特性(包含以下特性)
  - 0x001: 目录索引(directory indexing)
  - 0x008: 大inode(large inodes)
features_incompat = 0xb      // 不兼容特性(包含以下特性)
  - 0x001: 已分配inode(allocated inodes)
  - 0x002: 元数据校验(metadata checksumming)
  - 0x008: 目录类型(directory type)
features_log_incompat = 0   // 日志不兼容特性(无)
crc = 0x6af51d8 (correct)   // 超级块CRC校验值(校验正确)
spino_align = 4             // 特殊inode对齐倍数(4块)
pquotino = null              // 项目配额inode(未启用)
lsn = 0x1000000b0           // 日志序列号(最后写入的日志位置)
meta_uuid = 00000000-0000-0000-0000-000000000000  // 元数据UUID(未设置)
  

3: xfs_info详解

  
meta-data=/dev/vdb               # 设备路径
isize=512                        # inode大小=512字节(支持扩展属性)
agcount=4, agsize=655360 blks    # 4个分配组,每组655360个块(每个AG 2.5GB)
sectsz=512                       # 物理扇区大小=512字节(传统硬盘)
attr=2                           # 扩展属性版本=2(支持更高效xattr)
projid32bit=1                    # 启用32位项目ID(用于配额管理)
crc=1                            # 启用元数据CRC校验(数据完整性保护)
finobt=1                         # 启用空闲inode B+树(加速inode分配)
sparse=1                         # 启用稀疏inode(高效管理小文件)
rmapbt=0                         # 禁用反向映射B+树(记录块所有者)
reflink=1                        # 启用写时复制(高效文件克隆/快照)
bigtime=1                        # 启用64位时间戳(解决2038年问题)
inobtcount=1                     # inode B+树记录计数(优化树遍历)
nrext64=0                        # 禁用64位扩展区(限制文件最大16TB)

bsize=4096                       # 文件系统块大小=4KB
blocks=2621440                   # 总数据块数=2,621,440(容量=10GB)
imaxpct=25                       # inode空间占比上限=25%(最大支持549万inode)
sunit=0                          # 条带单元大小=0(未配置RAID)
swidth=0 blks                    # 条带宽度=0(未配置RAID)
version=2                        # 目录版本2(支持数十亿文件)
ascii-ci=0                       # 禁用ASCII大小写敏感(区分大小写)
ftype=1                          # 目录项存储文件类型(加速文件类型识别)
internal log                     # 日志类型=内部日志(非独立设备)
lazy-count=1                     # 启用懒计数器(减少超级块更新)
extsz=4096                       # 实时扩展区大小=4KB(默认值)
rtextents=0                      # 实时扩展区数=0
  

4: xfs inode分配过程

1:ag组分配策略

- 目录轮询分配,有个全局的轮询计数器,记录上一个分配的ag号,从该ag号开始分配

- 普通文件和目录分在一个ag中, 如果该ag满了,依次遍历循环剩余所有ag组进行搜索。//比如当前是3, 总共8个组,则从4 -> 5 -> 6 -> 7 -> 8 -> 0 -> 1 -> 2 -> 3 依次查找


2:成功条件

1.如果当前循环的ag中,空闲inode列表有多余的inode直接返回一个inode

2.如果当前循环的ag中,空闲inode列表没有多余的inode,则新建inode

a.剩余空间 >= 1个block + 一个inode chunk所需空间

b.剩余最大连续空间 >= 一个inode chunk所需空间 // 第一轮需要对齐,第二轮不需要对齐,主要是针对raid 条带化场景。另外如果剩余最大连续空间记录找不到,则按一个文件系统块来计算


3.成功直接返回该ag,不成功继续查找下一个ag


4.1 xfs_ialloc_ag_select

  
/*
 * 根据父目录 Inode 和文件模式(mode),选择一个分配组(AG)来查找或分配空闲 Inode。
 * 返回选中的分配组编号 (AGN)。
 */
STATIC xfs_agnumber_t
xfs_ialloc_ag_select(
    xfs_trans_t *tp,                /* 事务指针 */
    xfs_ino_t   parent,             /* 父目录的 Inode 编号 */
    umode_t     mode)               /* 文件类型标志(目录、文件、链接等) */
{
    xfs_agnumber_t  agcount;        /* 文件系统总的 AG 数量 */
    xfs_agnumber_t  agno;           /* 当前遍历到的 AG 编号 */
    int     flags;                  /* 分配缓冲区锁标志(如 TRYLOCK) */
    xfs_extlen_t    ineed;          /* 如果需要新建 Inode 块,所需的块数量 */
    xfs_extlen_t    longest = 0;    /* 当前 AG 中最长的连续空闲空间 */
    xfs_mount_t *mp;                /* 挂载点结构体指针 */
    int     needspace;              /* 标记该文件类型是否需要分配额外空间 */
    xfs_perag_t *pag;               /* 每个 AG 的内存缓存数据结构 */
    xfs_agnumber_t  pagno;          /* 起始搜索的 AG 编号 */
    int     error;

    /*
     * 目录、普通文件和软链接在长度大于 0 时通常需要至少一个数据块。
     * 虽然有些极小文件可以驻留在 Inode 内(Inline),但此处做通用判断。
     */
    needspace = S_ISDIR(mode) || S_ISREG(mode) || S_ISLNK(mode);
    mp = tp->t_mountp;
    agcount = mp->m_maxagi; // 获取最大 AG 数量

    /* 
     * 策略核心:
     * 1. 如果创建的是目录:调用 xfs_ialloc_next_ag 轮询选择下一个 AG。
     *    这能将不同目录均匀打散到各个 AG,从而极大提高多线程创建目录的并发性能。
     * 2. 如果创建的是文件:尝试放在与父目录相同的 AG 中,以保证局部性(Locality)。
     */
    if (S_ISDIR(mode))
        pagno = xfs_ialloc_next_ag(mp);
    else {
        pagno = XFS_INO_TO_AGNO(mp, parent); // 提取父目录所在的 AG 编号
        if (pagno >= agcount)
            pagno = 0;
    }

    ASSERT(pagno < agcount);

    /*
     * 遍历分配组,寻找有可用空间的组。
     * 注意:XFS 不仅仅寻找现有的空闲 Inode,它还会检查是否有足够空间来分配新的 Inode 簇(Chunk)。
     */
    agno = pagno;
    flags = XFS_ALLOC_FLAG_TRYLOCK;    // 第一遍遍历尝试非阻塞锁,提高效率
    for (;;) {
        pag = xfs_perag_get(mp, agno); // 获取该 AG 的运行时统计信息
        if (!pag->pagi_inodeok) {      // 如果该 AG 标记为禁止分配 Inode(如空间耗尽或损坏)
            xfs_ialloc_next_ag(mp);
            goto nextag;
        }

        // 如果 AG 的 Inode 信息未加载到内存,则进行初始化读取
        if (!pag->pagi_init) {
            error = xfs_ialloc_pagi_init(mp, tp, agno);
            if (error)
                goto nextag;
        }

        /* 最优选:如果该 AG 中已经有现成的空闲 Inode,直接返回此 AG */
        if (pag->pagi_freecount) {
            xfs_perag_put(pag);
            return agno;
        }

        // 如果没有空闲 Inode,则需要检查是否有空间新建 Inode 块,需初始化 AG 空间信息
        /* 
         * 检查该分配组(AG)的空闲空间元数据(PAGF)是否尚未初始化到内存中。
         * !pag->pagf_init 的意思是:“如果这个 AG 的空闲空间统计信息还没加载”。
         */
        if (!pag->pagf_init) {
          /* 
           * 调用初始化函数,从磁盘读取该 AG 的 AGF (Allocation Group Free-space) 扇区。
           * mp: 挂载结构体, tp: 事务指针, agno: AG编号, flags: 分配标志。
           */
            error = xfs_alloc_pagf_init(mp, tp, agno, flags);
            if (error)
                goto nextag;
        }

        /*
         * 检查是否有足够空间容纳文件本身以及一组新的 Inode 块。
         * 第一遍循环 (flags != 0) 会加上对齐补偿(alignment),
         * 这可以防止因对齐限制导致的分配失败。
         */
        ineed = mp->m_ialloc_min_blks; // 分配一个 Inode 簇所需的最小块数
        if (flags && ineed > 1)        // 如果是第一遍尝试,加入对齐预留空间
            /* 这里的对齐是2方面:
             * 1:申请的连续块数必须一个条带的整数倍
             * 2:起始块号必须是条带数的整数倍(起始块号 % sb_inoalignmt == 0,mp->m_sb.sb_inoalignmt是inode块对齐倍数(按8块对齐),启用条带就是条带占用的块数)。
             */
            ineed += xfs_ialloc_cluster_alignment(mp);
        
        longest = pag->pagf_longest;   // 获取该 AG 中最长的一块连续空闲空间
        if (!longest)                  // 如果没有记录,看空闲块计数是否大于 0
            longest = pag->pagf_flcount > 0;

        /* 
         * 判定条件:
         * 1. 总空闲块 >= 文件所需空间 + 新 Inode 簇所需空间
         * 2. 且最长连续空间足以放下新的 Inode 簇(保证物理连续性)
         */
        if (pag->pagf_freeblks >= needspace + ineed &&
            longest >= ineed) {
            xfs_perag_put(pag);
            return agno;
        }
nextag:
        xfs_perag_put(pag);
        /* 如果文件系统已经强制关闭(故障),则停止遍历 */
        if (XFS_FORCED_SHUTDOWN(mp))
            return NULLAGNUMBER;

        agno++;                // 移动到下一个 AG
        if (agno >= agcount)
            agno = 0;          // 环形搜索
        
        /* 
         * 如果搜索完了一圈回到原点:
         * 第一遍 (flags = TRYLOCK) 失败后,将 flags 设为 0,
         * 进行第二遍“硬碰硬”的阻塞锁搜索,不再考虑对齐宽容度。
         */
        if (agno == pagno) {
            if (flags == 0)
                return NULLAGNUMBER; // 两遍搜索都失败,返回无效号
            flags = 0;               // 降低条件,进行第二轮搜索
        }
    }
}  
  

4.2 xfs_ialloc_next_ag

  
/* 
 * STATIC: 静态函数,作用域仅限于当前源文件。
 * xfs_agnumber_t: 返回值类型,是 XFS 中表示 AG 索引的无符号整型。
 */
STATIC xfs_agnumber_t
xfs_ialloc_next_ag(
    xfs_mount_t *mp)            /* 输入参数:指向 XFS 挂载结构体 (mount structure) 的指针 */
{
    xfs_agnumber_t  agno;       /* 声明一个局部变量,用于存储当前选中的 AG 编号 */

    /* 
     * 获取挂载结构体中的自旋锁 (m_agirotor_lock)。
     * 目的是保护 m_agirotor 变量,防止多个 CPU 同时修改导致竞争。
     */
    spin_lock(&mp->m_agirotor_lock);

    /* 将当前的轮询计数器(rotor)的值赋给局部变量 agno */
    agno = mp->m_agirotor;

    /* 
     * 轮询核心逻辑:
     * 1. ++mp->m_agirotor: 将计数器自增 1。
     * 2. 如果自增后的值大于或等于当前文件系统的最大 AG 数量 (m_maxagi)。
     * 3. 则将计数器重置为 0,实现循环往复。
     */
    if (++mp->m_agirotor >= mp->m_maxagi)
        mp->m_agirotor = 0;

    /* 释放自旋锁,允许其他线程访问计数器 */
    spin_unlock(&mp->m_agirotor_lock);

    /* 返回本次分配所选择的 AG 编号 */
    return agno;
}
  

4.3 xfs_ialloc_pagi_init

  
/*
 * 读取 AGI 扇区,用于初始化挂载结构体中每个 AG 的特定数据。
 */
int
xfs_ialloc_pagi_init(
    xfs_mount_t    *mp,        /* 输入:指向文件系统挂载结构体的指针 */
    xfs_trans_t    *tp,        /* 输入:当前事务指针(XFS 是日志文件系统,操作通常在事务中) */
    xfs_agnumber_t    agno)        /* 输入:需要初始化的分配组(AG)编号 */
{
    xfs_buf_t    *bp = NULL;    /* 声明一个指向 XFS 缓存页(Buffer)的指针,初始化为空 */
    int        error;            /* 声明错误码变量 */

    /* 
     * 调用底层函数从磁盘读取指定 AG 的 AGI 数据块。
     * 该函数会将读取到的磁盘数据加载到 bp 指向的缓存中。
     */
    error = xfs_ialloc_read_agi(mp, tp, agno, &bp);
    
    /* 如果读取过程中出错(例如磁盘 IO 错误),直接返回错误码 */
    if (error)
        return error;

    /* 
     * 如果成功读取到了缓冲区 (bp 不为空):
     * 调用 xfs_trans_brelse 释放该缓冲区在当前事务中的引用。
     * 注意:读取动作本身已经完成了将磁盘数据“预热”到内存缓存并初始化相关内存结构的目的。
     */
    if (bp)
        xfs_trans_brelse(tp, bp);

    /* 执行成功,返回 0 */
    return 0;
}