文件系统orphan inode机制分析
之前做技术研究的时候搞文件系统元数据镜像时处理过orphan inode的问题,而现在恰好有同事在做lsof时发现了一些的特殊的文件,lsof可以看到进程在使用,同时ls具体文件时却又看不到:
suse:~ # lsof /var | grep deleted nscd 2831 root 8u REG 8,5 217016 42883 /var/run/nscd/db4Dqbpq (deleted) nscd 2831 root 9r REG 8,5 217016 42883 /var/run/nscd/db4Dqbpq (deleted) suse:~ # ll /var/run/nscd/db4Dqbpq ls: 无法访问 /var/run/nscd/db4Dqbpq: 没有那个文件或目录
/var/run/nscd/db4Dqbpq文件就成为orphan文件,42883是文件的inode,称之为orphan inode
一、神马是orphan inode
Orphan inode,顾名思义就是孤儿节点,是Linux ext系列文件系统结构中节点的一种,orphan意指的是被删除,无主的节点。通常inode数据结构字段中有i_links_count的参数,指的就是有多少文件硬链接到这个inode上。而orphan inode的i_links_count则为0,即为无主,这个时候ls上就看不到了。
二、为什么存在orphan inode
正常情况下一个inode被占用,则i_links_count>0,如果i_links_count=0则说明这个文件已经被删除了,因为删除一个文件并不是原子操作,我们把删除这个过程无限放大,如果一个文件先将i_links_count--到0,然后再删除具体的数据块,现实却是在“然后”之前系统断电了,没有电池呀。这个样子orphan inode形态就产生了,然后系统起来之后通过fsck比较有些块直接被bitmap标记了,却没有inode占用,这个样子就视为fs error,而orphan inode机制则是及时处理orphan inode,避免文件系统不一致的情况。
当然还有另外一种情况,也就是文件开头的示例所说,一个文件已经被一个进程open了,立即unlink掉这个文件,进程并不结束,一直占用open的文件句柄,此时文件已经被删除了,i_links_count=0,如果具体的数据块也被释放了,那进程读写被释放的数据块(更有可能被另外的文件占用),文件系统就彻底悲剧了,所以此时文件的具体数据块并没有释放,仍然保留,进程仍可以读写,直到句柄关闭时,orphan inode机制才保证删除具体的数据块
三、orphan inode 结构
首先说主要有两个结构体是存有orphan inode的,其一是ext3_inode,另外一个就是ext3_super_block,这个两个都是磁盘数据的存储形式,内存上的对应形式是ext3_inode_info和ext3_sb_info,但内存上的具体内容就不在赘述。
先看ext3_super_block:
struct ext3_super_block { __le32 s_journal_dev; /* device number of journal file */ __le32 s_last_orphan; /* start of list of inodes to delete */ __le32 s_hash_seed[4]; /* HTREE hash seed */
s_last_orphan就是超级块中的链接到orphan inode的索引,或者说就是orphan inode链表的头结点。
通过tune2fs工具查看var挂着设备sda5的超级块数据
suse:~ # df /var 文件系统 1K-块 已用 可用 已用% 挂载点 /dev/sda5 505604 78872 400628 17% /var suse:~ # tune2fs -l /dev/sda5 | grep orphan First orphan inode: 42883
超级块中的orphan inode 是42283,和前面lsof看到的inode是对应的。
但头结点只是一个无符号的整型数,描述的是一个indoe号,但磁盘分区有N个orphan inode,它们之间的链接就一块inode中的i_dtime参数
ext3_inode:
struct ext3_inode { __le16 i_mode; /* File mode */ __le16 i_uid; /* Low 16 bits of Owner Uid */ __le32 i_size; /* Size in bytes */ __le32 i_atime; /* Access time */ __le32 i_ctime; /* Creation time */ __le32 i_mtime; /* Modification time */ __le32 i_dtime; /* Deletion Time */ __le16 i_gid; /* Low 16 bits of Group Id */ __le16 i_links_count; /* Links count */ __le32 i_blocks; /* Blocks count */ __le32 i_flags; /* File flags */
i_links_count代表了inode的被链接数,i_dtime则代表这inode被删除的参数,但事实上dtime参数应该一直没什么用(正常情况下一直是空的,为0),除了被orphan inode利用,用来链接下一个orphan inode。
我们通过debugfs工具看一下orphan inode具体形式:
suse:~ # debugfs /dev/sda5 -R "stat <42883>" Inode: 42883 Type: regular Mode: 0600 Flags: 0x0 Generation: 574603438 Version: 0x00000000 User: 0 Group: 0 Size: 217016 File ACL: 0 Directory ACL: 0 Links: 0 Blockcount: 426 Fragment: Address: 0 Number: 0 Size: 0 ctime: 0x515ed461 -- Fri Apr 5 21:40:49 2013 atime: 0x515f009f -- Sat Apr 6 00:49:35 2013 mtime: 0x515ed461 -- Fri Apr 5 21:40:49 2013 BLOCKS: (0-11):180209-180220, (IND):180221, (12-14):180222-180224, (15-211):180482-180678 TOTAL: 213
从上面可以看出文件具体数据BLOCKS仍然存在,只是Links为0,同时没有删除时间。
如此它们的链接关系如是:
四、orphan inode 机制
通过删除一个文件可以清晰看到orphan inode的机制,下面描述删除文件的过程。
系统调用进入内核是通过SYSCALL_DEFINE1实现的,具体调用流程如下
SYSCALL_DEFINE1-->do_unlinkat-->vfs_unlink-->ext3_unlink
系统最后调用的是ext3_unlink,在函数中,它首先通过ext3_delete_entry函数删除了inode父目录中的普通数据条目,然后用ext3_orphan_add把准备删除的inode加入到orphan inode链表中,具体函数如下:
static int ext3_unlink(struct inode * dir, struct dentry *dentry) { if (!inode->i_nlink) { ext3_warning (inode->i_sb, "ext3_unlink", "Deleting nonexistent file (%lu), %d", inode->i_ino, inode->i_nlink); inode->i_nlink = 1; } retval = ext3_delete_entry(handle, dir, de, bh); if (retval) goto end_unlink; dir->i_ctime = dir->i_mtime = CURRENT_TIME_SEC; ext3_update_dx_flag(dir); ext3_mark_inode_dirty(handle, dir); inode->i_nlink--; if (!inode->i_nlink) ext3_orphan_add(handle, inode); inode->i_ctime = dir->i_ctime; ext3_mark_inode_dirty(handle, inode); retval = 0; }
需要注意的是orphan inode链表采用的是头插法,删除也是自头结点开始删除。
ext3_unlink之后数据事实上还没有删除,只是删除一个硬链接,如果硬链接此时为0,则将inode的相关节点挂着orphan inode上,真正删除普通数据的过程函数如下:
do_unlinkat-->iput-->generic_drop_inode-->generic_delete_inode-->ext3_delete_inode
在generic_drop_inode中判断硬链接如果为0,则准备删除数据,最终通过ext3_delete_inode来实现的,需要注意的是ext3_delete_inode是在ext3_sops初始化是赋值的,generic_delete_inode调用ext3_sops中对象函数指针实现的。
void ext3_delete_inode (struct inode * inode) { if (IS_SYNC(inode)) handle->h_sync = 1; inode->i_size = 0; if (inode->i_blocks) ext3_truncate(inode); /* * Kill off the orphan record which ext3_truncate created. * AKPM: I think this can be inside the above `if'. * Note that ext3_orphan_del() has to be able to cope with the * deletion of a non-existent orphan - this is because we don't * know if ext3_truncate() actually created an orphan record. * (Well, we could do this if we need to, but heck - it works) */ext3_orphan_del(handle, inode); EXT3_I(inode)->i_dtime = get_seconds();
函数通过调用ext3_truncate层层删除具体的文件数据,完成后用ext3_orphan_del将orphan inode删掉,此时inode的被删除时间才被赋值。
我们讨论的是在ext3_orphan_del完成之前,系统挂掉了,代表这文件删除失败了,但文件系统被重新挂着时,系统会加载文件系统上的超级块数据,读取s_last_orphan,将所有的orphan inode删除掉,保证系统是一致的。
ext3_get_sb-->ext3_fill_super-->ext3_orphan_cleanup
static int ext3_fill_super (struct super_block *sb, void *data, int silent) { EXT3_SB(sb)->s_mount_state |= EXT3_ORPHAN_FS; ext3_orphan_cleanup(sb, es); EXT3_SB(sb)->s_mount_state &= ~EXT3_ORPHAN_FS;
文件系统orphan inode机制分析来自于OenHan
链接为:https://oenhan.com/fs-orphan-inode-analysis