注:本文分析基于linux-4.18.0-193.14.2.el8_2内核版本,即CentOS 8.2
1 inode
内存中,每个文件都有一个inode,一切皆文件,其实应该是说一切皆inode。inode保存了文件系统中一个文件的属性描述,比如文件类型,文件权限,属主,文件创建、读取或修改的时间等等。除了内存中的inode,磁盘中也有对应的inode结构,比如ext4文件系统中,内存中的inode是struct ext4_inode_info,而磁盘中的inode是struct ext4_inode_info,今天我们主要分析下内存中的inode结构。
2 inode主要成员变量
struct inode {umode_t i_mode; //文件类型和访问权限unsigned short i_opflags;kuid_t i_uid; //inode所属文件的所有者idkgid_t i_gid; //inode所属文件所在组idunsigned int i_flags;...const struct inode_operations *i_op; //该inode操作函数集合struct super_block *i_sb; //指向所属超级块对象struct address_space *i_mapping; //指向inode在内存中的pagecache...unsigned long i_ino; //inode节点号 ls -i 可以查看文件inode号...struct timespec64 i_atime; //文件最后访问时间struct timespec64 i_mtime; //文件最后修改时间struct timespec64 i_ctime; //文件创建时间spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */unsigned short i_bytes; //文件在最后一块中的字节数unsigned int i_blkbits; //block size(字节位数),一般为9enum rw_hint i_write_hint;blkcnt_t i_blocks; //文件的总块数,这里一个block为512 byte,也就是扇区大小...unsigned long dirtied_when; //inode变脏的时间unsigned long dirtied_time_when;//通过该变量链接到全局inode哈希表inode_hashtable,用于inode的快速查找//哈希值通过超级块和inode number计算struct hlist_node i_hash; //通过该变量挂载到bdi_writeback的b_io链表,等待BDI回写,也就是这是脏inodestruct list_head i_io_list; /* backing dev IO list *///该inode所在设备对应的bdi_writeback结构,用于inode的回写struct bdi_writeback *i_wb; ...struct list_head i_lru; //通过该变量链接到超级块的s_inode_lru链表struct list_head i_sb_list; //通过该变量链接到超级块的s_inodes上struct list_head i_wb_list; //通过该变量链接到超级块的s_inodes_wb上,回写统计union {//所有引用该inode的目录项将形成一个链表//比如文件被链接到其他的文件,就会有多个dentrystruct hlist_head i_dentry; struct rcu_head i_rcu;};...const struct file_operations *i_fop; //该inode对应的文件的操作函数集合struct file_lock_context *i_flctx;struct address_space i_data; //内嵌在inode的pagecache对象...void *i_private; /* fs or device private pointer */RH_KABI_RESERVE(1)RH_KABI_RESERVE(2)
};
3 创建一个inode
我们以ext4的创建目录——mkdir为例,看下inode的创建过程,
const struct inode_operations ext4_dir_inode_operations = {.create = ext4_create,....mkdir = ext4_mkdir,...
};const struct file_operations ext4_dir_operations = {.llseek = ext4_dir_llseek,.read = generic_read_dir,....open = ext4_dir_open,.release = ext4_release_dir,
};static int ext4_mkdir(struct inode *dir, struct dentry *dentry, umode_t mode)
{...//调用ext4文件系统的alloc_inode函数,创建新的inodeinode = ext4_new_inode_start_handle(dir, S_IFDIR | mode,&dentry->d_name,0, NULL, EXT4_HT_DIR, credits);...//对于目录文件,i_op和i_fop指向的也是对应的目录操作函数//如果是普通文件,i_op和i_fop指向的会是file操作函数inode->i_op = &ext4_dir_inode_operations;inode->i_fop = &ext4_dir_operations;...
}
ext4_new_inode_start_handle通过以下调用路径,
ext4_new_inode_start_handle ->__ext4_new_inode ->new_inode
最终在new_inode中将分配成功的inode挂到对应超级块的s_inodes链表上,这个链表就是这超级块所有的inode对象。
struct inode *new_inode(struct super_block *sb)
{struct inode *inode;spin_lock_prefetch(&sb->s_inode_list_lock);//调用对应文件系统的alloc_inode方法创建一个inodeinode = new_inode_pseudo(sb);if (inode)//将该inode挂载到对应超级块的s_inodes链表上inode_sb_list_add(inode);return inode;
}
4 添加inode到inode cache链表
超级块上除了s_inodes链表,还有一个LRU链表s_inode_lru,这放的是未使用,或者干净的inode,比如回写完毕的inode就会挂载到这个链表上,这个链表也称为inode cache,在系统需要回收内存时,就会对这个链表下手,回收最近最少使用的inode。
添加到s_inode_lru链表的路径主要有两个:
- 回写完毕的inode,也就是干净的inode
- inode没有其他进程引用
不过最终都是通过inode_lru_list_add将inode挂载到s_inode_lru链表,
static void inode_lru_list_add(struct inode *inode)
{//将inode挂载到超级块的s_inode_lru链表if (list_lru_add(&inode->i_sb->s_inode_lru, &inode->i_lru))this_cpu_inc(nr_unused);elseinode->i_state |= I_REFERENCED;
}
先看回写完毕的inode,
inode_sync_complete ->inode_add_lru ->inode_lru_list_add
然后是无其他进程引用时,
iput ->iput_final ->inode_add_lru ->inode_lru_list_add
5 从inode cache中删除inode(回收inode cache)
上面我们提到系统回收内存时,会对操作s_inode_lru链表,我们也大概看下,
long prune_icache_sb(struct super_block *sb, struct shrink_control *sc)
{LIST_HEAD(freeable);long freed;//遍历超级块的s_inode_lru链表,按照回收控制结构sc指定的回收数量,//将可回收的inode隔离到freeable链表中集中回收freed = list_lru_shrink_walk(&sb->s_inode_lru, sc,inode_lru_isolate, &freeable);//将隔离出来的inode进行回收,这样隔离后可以避免锁竞争dispose_list(&freeable);return freed;
}
回收inode主要是要从几个链表中抽离,和脏数据回写
- 超级块的s_inode_lru链表
- bdi_writeback的b_io链表
- 超级块的s_inodes链表
- 回写pagecache
- 全局inode哈希表
static void dispose_list(struct list_head *head)
{while (!list_empty(head)) {struct inode *inode;inode = list_first_entry(head, struct inode, i_lru);//将inode从超级块的s_inode_lru链表摘除list_del_init(&inode->i_lru);//回收inodeevict(inode);cond_resched();}
}static void evict(struct inode *inode)
{const struct super_operations *op = inode->i_sb->s_op;BUG_ON(!(inode->i_state & I_FREEING));BUG_ON(!list_empty(&inode->i_lru));//从bdi_writeback的b_io链表摘除if (!list_empty(&inode->i_io_list))inode_io_list_del(inode);//将inode从超级块的s_inodes链表摘除inode_sb_list_del(inode);//等待该inode回写完毕inode_wait_for_writeback(inode);//调用对应文件系统的evict_inode方法,回写pagecacheif (op->evict_inode) {op->evict_inode(inode);} else {truncate_inode_pages_final(&inode->i_data);clear_inode(inode);}//如果是块设备inodeif (S_ISBLK(inode->i_mode) && inode->i_bdev)bd_forget(inode);//如果是字符型设备if (S_ISCHR(inode->i_mode) && inode->i_cdev)cd_forget(inode);//从全局inode哈希表中摘除remove_inode_hash(inode);...//回收inodedestroy_inode(inode);
}
处理完这些引用后,就可以调用destroy_inode回收到slab缓存,对于ext4,调用的是ext4_destroy_inode,
static void destroy_inode(struct inode *inode)
{BUG_ON(!list_empty(&inode->i_lru));__destroy_inode(inode);//调用对应文件系统的destroy_inode方法,将inode回收到slab缓存//对于ext4,调用的是ext4_destroy_inodeif (inode->i_sb->s_op->destroy_inode)inode->i_sb->s_op->destroy_inode(inode);elsecall_rcu(&inode->i_rcu, i_callback);
}static void ext4_destroy_inode(struct inode *inode)
{ if (!list_empty(&(EXT4_I(inode)->i_orphan))) {...}//调用ext4_i_callback将inode释放会slab缓存call_rcu(&inode->i_rcu, ext4_i_callback);
}static void ext4_i_callback(struct rcu_head *head)
{struct inode *inode = container_of(head, struct inode, i_rcu);//释放回slab缓存kmem_cache_free(ext4_inode_cachep, EXT4_I(inode));
}
6 结构关系
- 一块磁盘,三个分区,sda1和sda2是ext4文件系统,sda3是xfs文件系统
- 全局超级块链表super_blocks将三个超级块串联在一起
- sda1上的ext4_inode_info结构中内嵌inode结构,其中i_sb指向对应的超级块
- A、B、C三个inode挂载到超级块s_nodes链表,A,B两个未使用的inode还会挂载到s_inode_lru链表
- sda2上的ext4文件系统同sda1一样
- 而sda3上的xfs除了对应的xfs_inode结构不同,大体结构也是一样的,这其实就是VFS的作用,对所有文件系统抽象了一层