05 ext2文件系统IO流程:创建文件create

创建文件是文件系统的基本操作,之前在介绍Linux文件系统VFS时,说过创建文件会调用inode操作表中的create()函数,那create函数具体应该如何实现呢?

文件系统在创建文件时,VFS会调用父目录目录的create()函数,在这个函数中要完成具体的文件创建,以ext2文件系统为例,ext2注册的创建文件的函数指针是ext2_create,在ext2文件系统创建文件,一般包括以下几个步骤:

  • 新建一个inode对象
  • 设置inode的文件操作表
  • 标记inode为脏,等待写入到磁盘
  • 在父目录的页缓存中,写入目录项数据,指向新建的inode
  • 建立缓存dentry和inode关系
ext2创建文件
ext2创建文件
// namei.c
const struct inode_operations ext2_dir_inode_operations = {
    .create	= ext2_create,
    ......
};

// 注意:函数参数dentry就是当前文件,在内存中的目录项,也就是说在调用ext2_create函数之前,内存中的目录项已经创建完成
static int ext2_create (struct user_namespace * mnt_userns,
			struct inode * dir, struct dentry * dentry,
			umode_t mode, bool excl)
{
	struct inode *inode;
	int err;

	......

	inode = ext2_new_inode(dir, mode, &dentry->d_name); // ① 新建inode
	if (IS_ERR(inode))
		return PTR_ERR(inode);

	ext2_set_file_ops(inode); // ②设置操作表
	mark_inode_dirty(inode);  // ③ 标记脏inode,待写入磁盘
	return ext2_add_nondir(dentry, inode); // ④ 写入目录项
}

一、新建inode对象

新建inode对象时,除了创建inode内存结构,还要写入inode的磁盘结构,所以那就要涉及查找inode位图和inode表。所以,在新建inode之前,要找到新的inode所在的块组,找到对应块组描述符,这样就能获取到inode位图和inode表的位置,为后面的写入做好准备。新建inode的,详细流程如下:

ext2创建inode
ext2创建inode

1.寻找合适的块组

注意ext2_new_inode()是新建Inode对象函数,创建目录时,也会调用此函数,所以在寻找合适块组时,要区分是目录还是文件,对于目录类型将会在创建目录章节讲解,此处只介绍创建如何寻找所属块组。创建文件寻找块组时,一共涉及三个算法:

  • ① 优先使用父目录所在块组
  • ② 如果父目录所在块组已满,那么使用二次hash(Quadratic Hash)方式寻找
  • ③ 如果还未找到,就采用线性查找,从父目录所在组开始循环,循环所有块组,直到找到为止

这里说一下二次Hash,首先用父母所在组+父目录ino,然后除以总组数取余,作为循环的起点,然后每次循环跳动不长是i<<1,也就是下标乘以2,这样好处是,可以保证同一个目录下的文件,可以放到同一个新的组,并且散列冲突较小。

// ialloc.c
static int find_group_other(struct super_block *sb, struct inode *parent)
{
    int parent_group = EXT2_I(parent)->i_block_group;
    int ngroups = EXT2_SB(sb)->s_groups_count;
    struct ext2_group_desc *desc;
    int group, i;

    ......

    // Quadratic Hash
    group = (group + parent->i_ino) % ngroups;
    for (i = 1; i < ngroups; i <<= 1) {
        group += i;
        if (group >= ngroups)
            group -= ngroups;
        desc = ext2_get_group_desc (sb, group, NULL);
        if (desc && le16_to_cpu(desc->bg_free_inodes_count) &&
            le16_to_cpu(desc->bg_free_blocks_count))
            goto found;
    }

    ......

    return -1;

found:
    return group;
}

2.从块位图获取0位置

第1步获取到块组后,就可以找到找到块组描述符(sbi->s_group_desc),找到描述符后,就能获取inode位图所在的块号(desc->bg_inode_bitmap),这样通过buffer的sb_bread()函数,就能从设备上读取inode位图信息:

// read_inode_bitmap()
bh = sb_bread(sb, le32_to_cpu(desc->bg_inode_bitmap));

获取到位图之后,接下来就是寻找0位置,调用的是内核函数find_next_zero_bit_le(),也就是位图处理函数,找到位置之后,将此位置记录下来,同时设置成1,表示已占用。

// ialloc.c

ino = ext2_find_next_zero_bit((unsigned long *)bitmap_bh->b_data,
        EXT2_INODES_PER_GROUP(sb), ino);

static inline unsigned long find_next_zero_bit_le(const void *addr,
        unsigned long size, unsigned long offset)
{
    return find_next_zero_bit(addr, size, offset);
}

static inline int test_and_set_bit_le(int nr, void *addr)
{
	return test_and_set_bit(nr ^ BITOP_LE_SWIZZLE, addr);
}

缓冲区操作之后,要标记脏块,便于后面自动同步到设备

mark_buffer_dirty(bitmap_bh); // 标记脏的inode位图块
mark_buffer_dirty(bh2); // 标记脏的块组描述符

3.填充inode,然后写入

填充vfs的inode和内存中ext2_inode_info两个对象,填充完毕后,标记脏inode,总体逻辑比较清晰,这里不做赘述。

// ialloc.c

inode->i_ino = ino;
inode->i_blocks = 0;
inode->i_mtime = inode->i_atime = inode->i_ctime = current_time(inode);
memset(ei->i_data, 0, sizeof(ei->i_data));
.....
ei->i_block_alloc_info = NULL;
ei->i_block_group = group;
ei->i_dir_start_lookup = 0;
ei->i_state = EXT2_STATE_NEW;
.....
err = dquot_initialize(inode);
err = dquot_alloc_inode(inode);
err = ext2_init_acl(inode, dir);
err = ext2_init_security(inode, dir, qstr);


mark_inode_dirty(inode);

二、设置inode操作表,然后标记脏inode

普通文件的inode的操作表一共有三项:

  • ① i_op:inode操作相关(不重要,主要使用父目录的inode操作)
  • ② i_fop:文件对象操作相关(不重要,主要使用父目录的文件操作)
  • ③ i_mapping->a_ops:文件数据对应的地址空间映射操作表,也就是文件数据页缓存操作函数(重要)
// ialloc.c
void ext2_set_file_ops(struct inode *inode)
{
    inode->i_op = &ext2_file_inode_operations;
    inode->i_fop = &ext2_file_operations;
    if (IS_DAX(inode))
        inode->i_mapping->a_ops = &ext2_dax_aops;
    else if (test_opt(inode->i_sb, NOBH))
        inode->i_mapping->a_ops = &ext2_nobh_aops;
    else
        inode->i_mapping->a_ops = &ext2_aops;
}

mark_inode_dirty(inode);

三、写入文件目录项

创建文件还有一项重要工作,就是写入文件目录项到数据块,也就是要建立与父目录关系。写入目录项时,要寻找到相应位置,由于目录项是按顺序写入的,并且每个长度不一样,所以寻找到准确位置非常重要,并且涉及到页缓存操作,代码逻辑复杂度较高,尤其是寻找合适的位置,这里分析一下主要流程:

  • ① 获取父目录所有页缓存
  • ② 从页缓存开始,寻找合适的位置:是否有同名目录项(直接退出)、是否有空目录项(可以复用)、是否有空间过大的目录项(分配的空间>需要的空间+待写入目录项,这样插入到空档)
  • ③ 如果以上没寻找到,就按顺序在末尾写入
ext2写入目录项
ext2写入目录项

四、建立目录项和inode关系

经过前面三个步骤,inode已经创建,目录项也创建完成,并且持久化了到磁盘,最后一步就是建立内存的dentry和inode关系,调用的函数是d_instantiate_new(),主要动作就是设置dentry->d_inode域,指向新创建的inode。

// namei.c
static inline int ext2_add_nondir(struct dentry *dentry, struct inode *inode)
{
    int err = ext2_add_link(dentry, inode);
    if (!err) {
        d_instantiate_new(dentry, inode);
        return 0;
    }
    inode_dec_link_count(inode);
    discard_new_inode(inode);
    return err;
}

至此,创建文件IO流程全部结束。

前置博客:03 ext2文件系统物理结构剖析

内核版本:5.16.7

内核文档:https://docs.kernel.org/filesystems/index.html

《05 ext2文件系统IO流程:创建文件create》有3个想法

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注