之前做技术研究的时候搞文件系统元数据镜像时处理过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_link_list

四、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

链接为:http://oenhan.com/fs-orphan-inode-analysis

发表回复