创建文件是文件系统的基本操作,之前在介绍Linux文件系统VFS时,说过创建文件会调用inode操作表中的create()函数,那create函数具体应该如何实现呢?
文件系统在创建文件时,VFS会调用父目录目录的create()函数,在这个函数中要完成具体的文件创建,以ext2文件系统为例,ext2注册的创建文件的函数指针是ext2_create,在ext2文件系统创建文件,一般包括以下几个步骤:
- 新建一个inode对象
- 设置inode的文件操作表
- 标记inode为脏,等待写入到磁盘
- 在父目录的页缓存中,写入目录项数据,指向新建的inode
- 建立缓存dentry和inode关系
// 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的,详细流程如下:
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);
三、写入文件目录项
创建文件还有一项重要工作,就是写入文件目录项到数据块,也就是要建立与父目录关系。写入目录项时,要寻找到相应位置,由于目录项是按顺序写入的,并且每个长度不一样,所以寻找到准确位置非常重要,并且涉及到页缓存操作,代码逻辑复杂度较高,尤其是寻找合适的位置,这里分析一下主要流程:
- ① 获取父目录所有页缓存
- ② 从页缓存开始,寻找合适的位置:是否有同名目录项(直接退出)、是否有空目录项(可以复用)、是否有空间过大的目录项(分配的空间>需要的空间+待写入目录项,这样插入到空档)
- ③ 如果以上没寻找到,就按顺序在末尾写入
四、建立目录项和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
《05 ext2文件系统IO流程:创建文件create》有3个想法