注:本文分析基于linux-4.18.0-193.14.2.el8_2内核版本,即CentOS 8.2
内存中,每个文件都有一个inode,一切皆文件,其实应该是说一切皆inode。inode保存了文件系统中一个文件的属性描述,比如文件类型,文件权限,属主,文件创建、读取或修改的时间等等。除了内存中的inode,磁盘中也有对应的inode结构,比如ext4文件系统中,内存中的inode是struct ext4_inode_info,而磁盘中的inode是struct ext4_inode_info,今天我们主要分析下内存中的inode结构。
struct inode {
umode_t i_mode; //文件类型和访问权限
unsigned short i_opflags;
kuid_t i_uid; //inode所属文件的所有者id
kgid_t i_gid; //inode所属文件所在组id
unsigned 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(字节位数),一般为9
enum 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回写,也就是这是脏inode
struct 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的目录项将形成一个链表
//比如文件被链接到其他的文件,就会有多个dentry
struct 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)
};
我们以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函数,创建新的inode
inode = 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方法创建一个inode
inode = new_inode_pseudo(sb);
if (inode)
//将该inode挂载到对应超级块的s_inodes链表上
inode_sb_list_add(inode);
return inode;
}
超级块上除了s_inodes链表,还有一个LRU链表s_inode_lru,这放的是未使用,或者干净的inode,比如回写完毕的inode就会挂载到这个链表上,这个链表也称为inode cache,在系统需要回收内存时,就会对这个链表下手,回收最近最少使用的inode。
添加到s_inode_lru链表的路径主要有两个:
不过最终都是通过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);
else
inode->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
上面我们提到系统回收内存时,会对操作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主要是要从几个链表中抽离,和脏数据回写
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);
//回收inode
evict(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方法,回写pagecache
if (op->evict_inode) {
op->evict_inode(inode);
} else {
truncate_inode_pages_final(&inode->i_data);
clear_inode(inode);
}
//如果是块设备inode
if (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);
...
//回收inode
destroy_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_inode
if (inode->i_sb->s_op->destroy_inode)
inode->i_sb->s_op->destroy_inode(inode);
else
call_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));
}