文件删除与sillyrename还有孤儿节点

NFS文件系统的unlink操作,当目标节点的目录下引用计数大于0时,则采用sillyrename策略,最后一个打开文件的进程关闭文件时候,会检测sillyrename的标志位,若置位,则将其删除。

×××××××××××××××××××××××××××××××××××

上文是我11年7月刚进入实验室看NFSv3代码时候,做的笔记,现在回头看,理解的连皮毛都算不上,着实汗颜。

最近看FUSE lib库的文件,又发现了FUSE也使用了silly rename策略就很好奇,为什么VFS层没有做一个统一的控制,而是交由具体的文件系统来处理,还有,为什么本地文件系统没有使用silly rename策略,现在略微懂了一些,把一些结论记录在此。

0.关于d_count,i_count,nlink

d_count描述有多少file结构指向这个dentry;

i_count描述有多少个dentry指向这个inode

nlink描述磁盘的硬链接数目。

filp_open filp_close会增加或减少d_count;

d_count减为0会递减i_count;

unlink会减少nlink;


1.nfs为什么要使用silly rename策略?

nfs v3是无状态的协议,在server端,就连基本的open- close的状态也没有,即客户的RPC中,是没有open和close 的操作的。

任何操作都是使用fh直接发请求,而fh的获取是最开始的lookup操作完成的。lookup操作会在server端建立起inode和dentry的数据结构,而一旦server端内存中有了dentry和inode结构,重复的lookup操作并不会增加引用计数d_count,这也就意味着,server端并不知道client端有多少个文件打开了同一个文件。

而在本地文件系统ext3中,d_count会随着open的调用而增加。

在nfs中,同一个客户端在执行unlink系统调用时候,若发现本机还有其他进程同时打开此文件时候,就会采用silly rename策略,否则server端就会简单粗暴的执行删除操作,而不去管是否还有其他进程也在使用同一个文件。


2.本地文件系统为何不必采用sillyrename 策略

当执行文件删除系统调用时,会调用vfs_unlink操作,此操作会将--nlink,然后尝试将对应dentry从哈希表中删掉,并递减i_count。由于本地文件系统,open时候会增加d_count,因此,不会导致dentry的错误删除。

而inode可能会成为孤儿节点。ext3和ext4都有专门的机制来处理。


orphan在英文中是孤儿的意思,在这里取被遗弃、被删除之意。
orphan inode是什么样的inode呢?这种inode是怎样产生的呢?
先介绍一个概念,文件的引用计数,准确地说应该是inode的引用计数,因为一般来说一个文件会对应一个inode。文件的引用计数,简单地说是表示有多少个文件指向该文件,准确地说是文件的硬链接的个数。
情况1:设想一个进程,open一个文件,然后unlink该文件,然后进行文件读写。这是允许的,并且在进程退出时,内核会自动将引用计数为0的文件删除。
但是如果该进程尚未退出之前,系统崩溃了,那么,内核就没有机会将已被unlink、并且引用计数为0的inode从磁盘上删除了。
情况2:设想我们正在截断一个大文件(系统调用truncate),但是操作尚未完成,系统就崩溃了。同样,内核也没有办法将该文件的所有数据块全部删除了。
ext3、ext4的orphan inode机制就是处理上述两种情况的。基本思想是这样的:如果要删除或截断一个inode,要先把这个inode记录到磁盘上的一个特殊的orphan inode链表上。如果删除或截断操作能够正常完成,那么,就从磁盘上的orphan inode链表上删除该inode;否则,如果删除或截断操作未完成之前,系统就发生崩溃了,那么,系统重启后,文件系统会遍历磁盘上的orphan inode链表,对链表上的每一个inode都重新进行一遍删除或截断操作,以此来保证这些inode真正在磁盘上被删除,维护文件系统的一致性。
内核版本:2.6.35

二、相关数据结构及之间的关系

先总体说一下orphan inode的组织。
orphan inode需要在两个地方组织,分别是在内存中和在磁盘上。不论在哪里,从抽象角度来看,orphan inode都被组织成一个单向链表。

1、ext4_inode

struct ext4_inode {

    __le32 i_dtime; /* Deletion Time */

    .........

}
这个是磁盘上的inode的结构,i_dtime本来表示该inode被删除的时间,在orphan inode机制中,因为此时该域的值并不重要,故借用一下,用于记录下一个被unlink/truncate的inode号。

2、ext4_super_block

struct ext4_super_block {

    __le32 s_last_orphan; /* start of list of inodes to delete */

    .........

}
这个是磁盘上的superblock结构。其中,s_last_orphan记录的是最近一个被unlink/truncate的inode号,从抽象角度来看,它就代表磁盘上orphan inode单链表的头。
新的inode插入orphan inode链表时采用“头插法”,也就是说,最近被unlink/truncate的inode号会放在s_last_orphan中。
这样,磁盘上的orphan inode单链表如下图1所示。



3、ext4_inode_info

struct ext4_inode_info {

    __u32 i_dtime;

    struct list_head i_orphan; /* unlinked but open inodes */

    .......

}

这个是内存中的inode结构,即 磁盘上的ext4_inode在内存中的表现。
其中,i_dtime与ext4_inode中的i_dtime相对应,i_orphan是个链表节点,用于在内存中组成orphan
inode链表。

4、ext4_sb_info

struct ext4_sb_info {

    struct list_head s_orphan;

    struct mutex s_orphan_lock;

    .........

}
这个是内存中的superblock结构,即磁盘上ext4_super_block在内存中的表现。其中,s_orphan代表链表头,s_orphan_lock是用于保护链表的互斥锁。在内存中的orphan inode链表结构与图1很类似,在此从略。

三、辅助函数

1、NEXT_ORPHAN(inode)
fs/ext4/ext4.h
1209 #define NEXT_ORPHAN(inode) EXT4_I(inode)->i_dtime
这个宏的作用是在磁盘orphan inode链表上取得下一个orphan inode号。

2、ext4_orphan_get()

1074 struct inode *ext4_orphan_get(struct super_block *sb, unsigned long ino)
{
       .......

1104 inode = ext4_iget(sb, ino);
       .......
}
这个函数的主要作用是根据一个inode号ino,从磁盘上将该inode的信息读入内存,保存在ext4_inode_info结构中。

3、ext4_orphan_add()
1984 int ext4_orphan_add(handle_t *handle, struct inode *inode)
{
       .......
       // 将“下一个”最近的inode号保存在本inode的ext4_inode_info->i_dtime中
       // 这样,当本inode写回磁盘时,会将“下一个”orphan inode号写回磁盘。
2030 NEXT_ORPHAN(inode) = le32_to_cpu(EXT4_SB(sb)->s_es->s_last_orphan);
       // 将最近的inode号保存在ext4_super_block->s_last_orphan中,
       // 这样,当本超级块写回磁盘时,会将最近的orphan inode号写回磁盘。
2031 EXT4_SB(sb)->s_es->s_last_orphan = cpu_to_le32(inode->i_ino);
       .......
       // 将内存中的ext4_inode_info结构链到ext4_sb_info->s_orphan链表的第一个位置。
2046 if (!err)
2047     list_add(&EXT4_I(inode)->i_orphan, &EXT4_SB(sb)->s_orphan);
       .......
}
总之,ext4_orphan_add()的作用是在内存中和磁盘上的orphan inode链表中分别添加一个inode节点。

4、ext4_orphan_del()
2062 int ext4_orphan_del(handle_t *handle, struct inode *inode)
{
       .......
       // 取得磁盘上“下一个”orphan inode号
2079 ino_next = NEXT_ORPHAN(inode);
       // 取得内存中“上一个”orphan inode的节点
2080 prev = ei->i_orphan.prev;
       .......
       // 在内存中的orphan inode链表上将该inode删除
2085 list_del_init(&ei->i_orphan);
       .......
       // 下面的代码为什么会有分支?
       // 主要是因为要区分要删除的inode是不是链表头
2098 if (prev == &sbi->s_orphan) {
            // 如果要删除的inode在链表头
           // ext4_super_block->s_last_orphan中记录该inode的下一个inode号即可
2104     sbi->s_es->s_last_orphan = cpu_to_le32(ino_next);
2106  } else {
             // 否则,要删除的inode不在链表头
             // 用上一个inode的i_dtime记录本inode下一个 inode号
2116       NEXT_ORPHAN(i_prev) = ino_next;
2118 }
        .......
       // 本inode已从orphan链表中删除了,故这里i_dtime设为0
2121 NEXT_ORPHAN(inode) = 0;
       .......
}

总之,ext4_orphan_del()的作用是在内存中和磁盘上的orphan inode链表中分别删除一个inode节点。

四、删除一个inode的正常过程
我们结合unlink系统调用的逻辑,看看删除一个inode的正常的过程。
注意:我们只看未unlink前,该inode的引用计数为1,unlink后,该inode的引用计数为0的情况。因为只有这种情况才会要求真正从磁盘上删除该inode。
SYSCALL_DEFINE1(unlink, const char __user *, pathname)
    -->do_unlinkat
          -->vfs_unlink
                 -->ext4_unlink
                         -->ext4_delete_entry从该文件所在目录中删除该文件
                         -->ext4_orphan_add
          -->iput
                  -->iput_final
                          -->generic_drop_inode
                                  -->generic_delete_inode(inode);
                                          -->ext4_delete_inode
                                                  -->ext4_truncate清除磁盘上的索引信息
                                                  -->ext4_orphan_del
                                                  -->ext4_free_inode从内存中和磁盘上分别删除该inode

由此过程可知,在删除一个inode时,ext4_orphan_add和ext4_orphan_del是成对使用的,
这样才能保证一致性。

五、恢复时处理orphan inode的逻辑
一般而言,上述unlink过程在ext4_delete_inode会执行较长的时间,因为里面有ext4_truncate函数,它负责把inode的全部索引信息清除,如果一个文件比较大,索引信息就会很多,删除过程就会很长。
现在有两个假设:
假设1、假设orphan inode的信息已经写回磁盘,这样磁盘上的inode和superblock就包含了orphan inode的信息。
假设2、假设在ext4_delete_inode函数的执行过程中,系统崩溃了。
此时orphan inode机制就可以发挥作用了。
挂载文件系统会调用下列函数:
ext4_get_sb
       -->get_sb_bdev
               -->ext4_fill_super
                       -->ext4_orphan_cleanup
1970 static void ext4_orphan_cleanup(struct super_block *sb,
1971 struct ext4_super_block *es)
1972 {
             .......
2017      while (es->s_last_orphan) {
                   // 有点小技巧的地方在这里!
                   // es->s_last_orphan明显是个整数,这里怎么弄成个循环呢?
                   // 原因是每次循环,会调用ext4_orphan_del()在orphan链表上删除一个inode,
                   // 注意看ext4_orphan_del()函数的2104行,会将es->s_last_orphan设置成下一个被orphan的inode号。
                   // 于是,本循环的作用是从最近被orphan的inode开始,依次处理次近的、第三近的......最后一个inode。
2018           struct inode *inode;
2019
2020           inode = ext4_orphan_get(sb, le32_to_cpu(es->s_last_orphan));
                    .......
  
                  // 将该inode加入内存中的orphan inode链表
2026           list_add(&EXT4_I(inode)->i_orphan, &EXT4_SB(sb)->s_orphan);
2028           if (inode->i_nlink) {
                      // 如果该inode的引用计数为0,则重新执行一遍 ext4_truncate,
                      // 清除该文件在磁盘上的索引信息
2034               ext4_truncate(inode);
2035               nr_truncates++;
2036           } else {
2042                nr_orphans++;
2043           }

                   // 正如注释中说的那样,真正神奇的地方在这里
                   // 参考第四章“删除一个inode的正常过程”,你就会直到iput最终会调用ext4_orphan_del()在orphan链表上删除一个inode,
                  // 注意看ext4_orphan_del()函数的2104行,
                  // 会将es->s_last_orphan设置成下一个被orphan的inode号,这样循环才能继续。
2044         iput(inode);  /* The delete magic happens here! */
            }
             .......
2045 }



3.fuse的lib库为何要使用使用sillyrename策略?

还需要研究...


你可能感兴趣的:(文件删除与sillyrename还有孤儿节点)