• Linux删除文件过程解析


    Linux删除文件过程解析

    1. 概述


    当我们执行rm命令删除一个文件的时候,在操作系统底层究竟会发生些什么事情呢,带着这个疑问,我们在Linux-3.10.104内核下对ext4文件系统下的rm操作进行分析。rm命令本身比较简单,但其在内核底层涉及到VFS操作、ext4块管理以及日志管理等诸多细节。

    2. 源码分析


    rm命令是GNU coreutils里的一个命令,在对一个文件进行删除时,它实际上调用了Linux的unlink系统调用,unlink系统调用在内核中的定义如下:

    SYSCALL_DEFINE1(unlink, const char __user *, pathname) 
    {
      return do_unlinkat(AT_FDCWD, pathname); 
    }
    

    do_unlinkat的第一个参数 fd值为AT_FDCWD时,pathname就是相对路径,用于删除当前工作目录下的文件,若pathname为绝对路径,则fd直接被忽略。接下来我们将会对do_unlinkat中一些重点函数做以详细分析。

    static long do_unlinkat(int dfd, const char __user *pathname)
    {
        int error;
        struct filename *name;
        struct dentry *dentry;
        struct nameidata nd;
        struct inode *inode = NULL;
        unsigned int lookup_flags = 0;
    retry:
        name = user_path_parent(dfd, pathname, &nd, lookup_flags);
        if (IS_ERR(name))
            return PTR_ERR(name);
    
        error = -EISDIR;
        if (nd.last_type != LAST_NORM)
            goto exit1;
    
        nd.flags &= ~LOOKUP_PARENT;
        error = mnt_want_write(nd.path.mnt);
        if (error)
            goto exit1;
    
        mutex_lock_nested(&nd.path.dentry->d_inode->i_mutex, I_MUTEX_PARENT);
        dentry = lookup_hash(&nd);
        error = PTR_ERR(dentry); 
         if (!IS_ERR(dentry)) {
             /* Why not before? Because we want correct error value */ 
             if (nd.last.name[nd.last.len])
                 goto slashes;
             inode = dentry->d_inode;
             if (!inode)
                 goto slashes;
             ihold(inode);
             error = security_path_unlink(&nd.path, dentry);
             if (error)
                 goto exit2;
             error = vfs_unlink(nd.path.dentry->d_inode, dentry);
    exit2:
             dput(dentry); 
         }
         mutex_unlock(&nd.path.dentry->d_inode->i_mutex);
         if (inode)
             iput(inode);    /* truncate the inode here */        
         ... 
    }
    

    为了便于理解,这里简要介绍一下索引节点inode、目录项dentry以及目录项缓存dcache这几个重要概念,更具体的内容可参考Linux内核分析的相关书籍,如Robert Love的《Linux内核设计与实现》一书。

    • inode包含了与文件本身相关的信息,如文件大小、访问权限、MAC time等。
    • dentry(directory entry)包含了文件管理与组织的的信息,一方面它建立了文件名到inode的映射关系(有硬链接时为多对一关系),另一方面依靠其d_parent和d_child成员形成文件系统的目录树结构。
    • dcache是为了加快VFS查找文件或目录建立的,如果内核每次查找文件都要逐层遍历目录,那么将会浪费很多时间,这时候如果在访问dentry时将其缓存起来,那么此后访问就会快很多。

    回到正题,首先在lookup_hash函数中,我们根据文件路径从目录项缓存dcache中查找对应的目录项dentry,然后根据dentry->d_inode找到对应的inode。

    接下来调用vfs_unlink函数,这个函数实际干了两件事,一是调用inode->i_op->unlink,i_op是inode数据结构中定义的inode_operations类型的成员,它描述了VFS操作inode的所有方法,在这个结构体中定义了一组函数指针,所以在ext4文件系统中,inode->i_op->unlink实际上调用了ext4_unlink这一函数。vfs_unlink干的另一件事是调用d_delete,这一函数的作用是当目录项的引用计数变为0即没有进程在使用该目录项时,将目录项从dcache中删除。 再往下走,dput函数将dentry->d_count引用计数减1,如果不为0,则直接返回;否则接着判断dentry是否从dcache的哈希链上删除,如果是,则可以释放dentry对应的inode;如果不是,则表明dentry对应的inode没有被释放,此时可以将该dentry加入到detry_unused这一LRU队列中。(注:dput函数以及vfs_unlink这两个函数涉及到的操作较为繁杂,本文没有详细展开,具体内容可参考dentry inode引用计数)。

    接下来的iput函数作用就是就是释放inode,其调用路径为:

    iput()-->iput_final()-->generic_drop_inode()                     
                        |-->inode_lru_list_del()
                        |-->evict()
    

    generic_drop_inode函数中,通过inode->i_nlink硬链接计数的值来判断inode是否可以被删除。inode_lru_list_del将inode从LRU链表中删除,而evict则是真正地释放inode的操作,其调用路径为:

    evict()-->ext4_evict_inode()-->ext4_truncate()-->ext4_ext_truncate()-->ext4_ext_remove_space()-->ext4_ext_rm_leaf()-->ext4_free_blocks()
    

    (注:本文对一些函数的调用路径没有全部展开,只对一些关键路径加以描述,要想获得内核调用链上的全部信息,推荐使用Brendangregg开源的perf-tool中的funcgraph工具)

    我们可以看到ext4_ext_truncate、ext4_ext4_remove_space以及ext4_ext_rm_leaf这三个函数名中间都有一个ext,其实就是extent,也就是说这三个函数都是操作extent的函数,而真正释放块是在ext4_free_blocks中。

    EXT4文件系统相比于EXT2、EXT3等文件系统的一个最大的区别就是,EXT4采用extent而非间接块指针(indirect block pointer)来管理磁盘块。EXT4的inode大小为256字节,40-99这60个字节在EXT2、EXT3文件系统中用来保存间接块指针(12个直接指针和3个间接指针),而现在用来保存extent信息,其中40-51字节为extent头部信息,保存了魔数、extent个数以及深度等信息:

    struct ext4_extent_header
    {
        __le16 eh_magic; /* probably will support different formats */
        __le16 eh_entries; /* number of valid entries */
        __le16 eh_max; /* capacity of store in entries */
        __le16 eh_depth; /* has tree real underlying blocks? */
        __le32 eh_generation; /* generation of the tree */ 
    };
    

    52~99这48个字节用来保存extent或者extent index信息,extent和extent index结构均为12个字节:

    struct ext4_extent
    {
        __le32 ee_block;
        __le16 ee_len;
        __le16 ee_start_hi;
        __le32 ee_start_lo; 
    };
    struct ext4_extent_idx
    {
        __le32 ei_block;
        __le32 ei_leaf_lo;
        __le16 ei_leaf_hi;
        __u16 ei_unused;
    };
    

    ext4_extent代表一组连续的块,ee_block为逻辑块号,ee_len代表extent中有多少个块,其最高位与ext4的预分配策略特性有关,所以一个extent最多能存2^15个块,由于物理块大小为4k,即可以存储128M的数据,ee_start_hi和ee_start_lo组成了物理块地址。

    当文件较小时(<512M,即4个extent能存储的数据大小),完全可以用extent来保存块信息,但当文件较大时,就不得不借助ext4_extent_idx 这个中间结构来索引下一级结点,此时ext4_extent_header中保存的eh_depth就不为0了。ei_leaf_lo与ei_leaf_hi组合起来构成下一级节点的物理块号。所以对于大文件来说,通过inode找到index结点,进而找到叶子结点,最终通过叶子结点中存储的extent来找到实际的磁盘物理块。整个extent tree的结构如下图所示:

    img

    回到evict函数的调用路径上,ext4_ext_rm_leaf用来释放叶子结点中extent及其相关的物理块,start和end参数用来指定起始和终止的逻辑块号,例如start=1, end=4代表第一个到第四个extent。

    static int
    ext4_ext_rm_leaf(handle_t *handle, struct inode *inode,
                     struct ext4_ext_path *path,
                     ext4_fsblk_t *partial_cluster, 
                     ext4_lblk_t start, ext4_lblk_t end)
    

    实际的释放块操作是在ext4_free_blocks中,block参数指定了要释放的起始物理块号,count指定要释放的块数目。

    void ext4_free_blocks(handle_t *handle,
                          struct inode *inode,
                          struct buffer_head *bh,
                          ext4_fsblk_t block,
                          unsigned long count,
                          int flags)
    

    值得注意的是,释放块并不是真正地从介质中擦除数据,而是将这些块对应的位从块位图(block bitmap)中清除掉。另外,ext4_free_blocks需要更新quota(磁盘配额)信息。其调用路径为:

    ext4_free_blocks()-->mb_clear_bits()
                     |-->dquot_free_block()-->dquot_free_space_nodirty()-->__dquot_free_space()-->inode_sub_bytes()
                                          |-->mark_inode_dirty_sync()-->__mark_inode_dirty()
    

    dquot_free_block里做的两件事情分别是:

    1. 调用dquot_free_space_nodirty,该函数内联展开为__dquot_free_space并最终调用inode_sub_bytes更新inode的两个成员i_blocks和i_bytes。
    2. 调用mark_inode_dirty_sync将inode标记为脏(因为第一步对inode做了修改),在ext4中执行该函数将会对日志进行更新。该函数内联展开为__mark_inode_dirty(inode, I_DIRTY_SYNC),I_DIRTY_SYNC表示要进行同步操作。
    void __mark_inode_dirty(struct inode *inode, int flags)
    {
        struct super_block *sb = inode->i_sb; 
        struct backing_dev_info *bdi = NULL;    
        if (flags & (I_DIRTY_SYNC | I_DIRTY_DATASYNC)) {     
            trace_writeback_dirty_inode_start(inode, flags);      
            if (sb->s_op->dirty_inode)       
                sb->s_op->dirty_inode(inode, flags);      
            trace_writeback_dirty_inode(inode, flags);   
        }   
        ...
    

    如果设置了I_DIRTY_SYNC标志,则在__mark_inode_dirty函数中会通过函数指针dirty_inode调用文件系统特有的操作,在EXT4文件系统下,相应的函数为ext4_dirty_inode,该函数会启动一个日志原子操作将应该同步的inode元数据向jdb2日志模块提交。

    void ext4_dirty_inode(struct inode *inode, int flags) 
    {   
        handle_t *handle;    
        handle = ext4_journal_start(inode, EXT4_HT_INODE, 2);   
        if (IS_ERR(handle))     
            goto out;  
        ext4_mark_inode_dirty(handle, inode);   
        ext4_journal_stop(handle); 
    out:
        return;  
    }
    

    ext4_dirty_inode函数中做了三件事:

    1. ext4_journal_start判断日志执行状态并调用jbd2__journal_start来启用日志handle;
    2. ext4_mark_inode_dirty会调用ext4_get_inode_loc函数来根据指定的inode获取inode在磁盘和内存中的位置,然后调用jbd2_journal_get_write_access获取写日志的权限,接着对inode在磁盘上对应的raw_inode进行更新,最后调用jbd2_journal_dirty_metadata设置元数据为脏并添加到日志transaction的对应链表中;
    3. ext4_journal_stop用来结束此日志handle,这样的话日志的commit进程在被唤醒时将会对这个日志进行提交。

    回到__mark_inode_dirty函数,该函数接下来会对inode的状态i_state添加flag标记,如上文所述,此处的flag为I_DIRTY_SYNC。

    当inode尚未dirty时,还会进行如下操作:

         ...
         if (!was_dirty) {
             bool wakeup_bdi = false;          
             bdi = inode_to_bdi(inode);            
             if (bdi_cap_writeback_dirty(bdi)) {              
                 WARN(!test_bit(BDI_registered, &bdi->state),                   
                      "bdi-%s not registered
    ", bdi->name);                
                 if (!wb_has_dirty_io(&bdi->wb))                  
                     wakeup_bdi = true;          
             }            
             spin_unlock(&inode->i_lock);          
             spin_lock(&bdi->wb.list_lock);          
             inode->dirtied_when = jiffies;          
             list_move(&inode->i_wb_list, &bdi->wb.b_dirty);          
             spin_unlock(&bdi->wb.list_lock);            
             if (wakeup_bdi)              
                 bdi_wakeup_thread_delayed(bdi);          
             return;     
         }
    

    当没有对回写进行限制(bdi_cap_writeback_dirty),且通过wb_has_dirty_io判断出inode对应的bdi没有正在处理的dirty io时(即dirty list, io list, more io list均为空),我们将wakeup_bdi设置为true。接下来设置inode的dirty时间,并将inode的i_wb_list移到bdi_writeback的dirty链表(wb.b_dirty)中。如果wakeup_bdi为真,则调用bdi_wakeup_thread_delayed将bdi添加到后台的回写队列中,回写队列中的dirty inode会被回写线程定期刷到磁盘,时间间隔由dirty_writeback_interval参数决定,默认为5s。

    3. rm对I/O影响

    实际上,evict调用链上有诸多地方都包含设置元数据为脏并更新日志这个操作(ext4_handle_dirty_metadata),例如在ext4_free_blocks中还会对存放块位图(block bitmap)的block以及存放块组描述信息(group descriptor)的block进行元数据的dirty操作。由此可知,要删除的文件越大,涉及到的日志更新操作就越频繁,所以直接rm一个大文件时,大量的日志更新操作将会影响到其他进程的I/O性能。如果其他进程是I/O密集型的程序,以MySQL为例,rm大文件与之同时运行将会使得其QPS降低,响应时间也会增加。 为了验证这点,本文用Sysbench对MySQL进行压测,使用的设备为NVMe接口的SSD,实验分两组:

    (1) 只运行Sysbench,测得的平均QPS为205500;

    (2) 运行Sysbench的同时对一个400GB的大文件进行rm操作,测得的平均QPS为40485。

    由此可见,在对大文件进行删除时,为了避免对其他I/O密集型应用的影响,不应该直接用rm对其删除,而应该采用其他方法。例如,每次将大文件truncate一部分并sleep一段时间,这样的话就可以将删除的I/O负载分散到每次truncate操作,不会出现I/O负载在一段时间内突然增高的现象。

    参考文献

    [1] https://www.ibm.com/developerworks/cn/linux/l-cn-usagecounter/

    [2] https://digital-forensics.sans.org/blog/2010/12/20/digital-forensics-understanding-ext4-part-1-extents

    [3] http://blog.csdn.net/luckyapple1028/article/details/61413724

  • 相关阅读:
    Windows下快速搭建安卓开发环境android-studio
    使用Android Studio搭建Android集成开发环境
    手动安装配置Android Studio
    android studio 各种问题 应该能帮助到你们
    如何清除XP的网络共享密码
    一个语言的“入流”,而是和这种语言进入某一子行业的契机有关
    必须冷静、必须听话,赶紧走
    QWidget继承自QPaintDevice,这样就可以直接把QWidget传入QPainter的构造函数,比如QPainter(mylabel),然后设置QWidget的长宽后直接进行作画了
    ActiveMQ
    开源word操作组件DocX的记录
  • 原文地址:https://www.cnblogs.com/lwyeric/p/13603959.html
Copyright © 2020-2023  润新知