注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

linux中VFS文件页面cache  

2012-06-24 22:18:13|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、cache思想
cache是一个计算机中提速的一个重要概念和方法。从cpu的寄存器、L1 cache,L2cache,内存、磁盘、网络各种级别的设备都会参与到这个大的cache之中,并且组成一个金字塔式的存储体系。这个概念其实在日常生活中也是存在的,我们随身携带的“伸手要钱”四件设备就是我们平时使用率最高的东西,所以离我们的具体最近,随身携带,出门的时候先检查一下有没有带齐。其它一些不常用的可能放在背包里,更不常用的可能就压箱底,甚至有的放垃圾堆了。所以说这个思想本身是大家都明白的,就像狗遇到骨头明白走直线最快一样,所以说叫做公理而不是定理。
在操作系统的,我们也可以想象一下,辛辛苦苦从外围设备(通常是块设备,例如硬盘)中读取的内容,它应该在内存中逗留尽可能长的时间,从而避免重复的、冗余的外设读取操作,这个原理我想大家也是明白的,但是理论毕竟是理论,没有结合例子,看起来就好像有人告诉你要“做一个XXX的人,做一个YYY的人,做一个ZZZ的人一样“一样,让人听了之后进行了一次”镜面反射“。但是有人告诉你有老太太过马路的时候你应该怎样、或者在马路边捡到一分钱的时候应该怎样,最好精确到每句话,每个表情,那就比较受用了。这也是 毛德操老师 在情景分析中说的一个结合情景看代码的原因。
现在想一个这样的场景,一个比较常用的可执行程序,例如ls(更常用的命令cd并不对应一个可执行文件,它是bash的内置命令,因为当前工作目录是一个进程特有的概念,你派生一个进程执行这个命令对bash本身没有任何影响),当我们执行的时候,操作系统需要把这个可执行程序的内容从磁盘上读到内存中,然后执行命令,在程序执行结束之后,读入的程序内容(代码段及常量数据段)不应该随着程序的结束而被立即释放掉。如果释放之后,再次执行ls的时候还是要启动硬盘操作,这个时间同样是比较长的一个操作。
但是这里就有一些问题,你怎么知道这个ls是一个常用的程序呢?有些同学说,我在bash里经常敲这个命令,敲的次数是最多的,所以它是一个常用的命令。这里的确就是一个经验问题,我们是根据历史的经验和数据知道这个命令使用的频率很高,但是操作系统相对于人脑来说是比较笨的,它的最大优势是计算速度。再进一步说,当你最早使用linux系统的时候,你是否知道这个ls命令将会是你今后最为常用的一个命令呢?或者说,给你一个新的操作系统,列举文件的命令叫做dir,那么你怎么就知道这个dir将会是一个比较常用的命令呢?
计算机科班出身的同学这时候就应该知道常用的LRU算法了Least Recently Used,但是它并不是一个算法,而是一思想、甚至只能说是一种思路。从名字上看,就是最近最少使用的内容被替换,它是一个基于历史信息来判断未来的方法。虽然不精确,但是没有更好的替换方法,就像高考一样,大家都觉得不合理,但是又没有更合理的方法。在实际应用中,这个算法被验证是的确可以完成这个功能的,能够消耗合理少的资源来完成需要完成的功能,例如对于ls命令的识别。
上面其实已经隐含说明了另一个问题,那就是如果进程已经退出,而进程使用的页面还没有释放,那么这些页面在什么时候释放,所谓”请神容易送神难“,当年的董卓叛乱以及郭子仪的借兵都应该是一些例子,或者说几万亿救市,可能的确是来了个回光返照,但是如果不合理控制,后患无穷。一个系统无论多少的内存都是不够用的,一个不常用的页面不能占用大量的内存空间,因为毕竟系统还要做其它的动作,例如编程序、看电影、听音乐、写文档等,这些都需要内存。
说了这么多,主要的意思是引出页面何时被释放的问题,因为进程已经退出,它的页面只能在系统中逗留一定常的时间,这个时间如何控制。比如说一个比较冷僻的进程,格盘命令fdisk,可能一般只是使用一次,使用之后操作系统本着平等的精神也让它留在系统中,但是毕竟它很少使用,所以它应该需要比较快的释放掉页面。此时操作系统也应该通过某种方法来完成。
二、文件关闭时释放了什么
当一个可执行程序执行结束之后,它通过sys_close来关闭文件
sys_close--->>>filp_close--->>>fput--->>>__fput--->>>dput
/*
     * AV: ->d_delete() is _NOT_ allowed to block now.
     */
    if (dentry->d_op && dentry->d_op->d_delete) {这个条件对于通常文件系统并不满足,例如vfat文件系统,大家可能奇怪为什么不用ext2来说明,因为我使用qemu调试内核的时候busybox没有自带创建ext2文件系统的命令,所以用vfat来代替了,但是ext2应该是一样的
        if (dentry->d_op->d_delete(dentry))
            goto unhash_it;
    }
    /* Unreachable? Get rid of it */
     if (d_unhashed(dentry))这一点一般也不满足,它表示文件已经被删除的情况。
        goto kill_it;
      if (list_empty(&dentry->d_lru)) {对于一个第一次被加载的程序,它满足该路径,因为在dentry分配的d_alloc中,该内容是清空了的,实现代码为//INIT_LIST_HEAD(&dentry->d_lru);
          dentry->d_flags |= DCACHE_REFERENCED;
          list_add(&dentry->d_lru, &dentry_unused);这里在文件关闭的时候,它并没有被立即释放掉,而是加入了lru队列,表示要对这个结构进行观察,开始接收应用的考验,如果过一段时间系统内存吃紧,而这个东西又没有被再次使用,那么它就应该真的下岗了,它占用的资源就应该释放。这里一个dentry占有的资源并不多,但是在它之上寄存着inode结构,而一个文件的所有页面又在一个inode结构中的address_space中
          dentry_stat.nr_unused++;
      }
     spin_unlock(&dentry->d_lock);
    spin_unlock(&dcache_lock);
    return;
这里也就是说,文件关闭的时候,无论是文件代表文件的目录项、inode以及页面都没有释放,所以它是在内存中进行了缓存。这也就是说,最近执行的程序在系统中一定是有缓存的。这一点也很合理,比如我们程序猿在编版本的时候,经常调用gcc/g++是必须的,更大的工程可能还要经常使用make,并且对于调试器啊,vim啊神马的在开发的时候一定是频繁使用的,所以这个假设是合理的,也就是”时间局部性“的一个例子。
三、dentry什么时候释放
dentry、inode、task_struct、file、vma之类的内核常用的结构通常都是在内核的slab机制管理之下存在的,而slab的内容一般是不主动释放的,而是在系统感觉内存吃紧的时候到这个地方来进行回收。但是dentry的回收并不会直接导致它对应的inode被回收,因为这里要考虑到符号链接的问题,不同的dentry可能使用相同的inode结构。另一方面说,当dentry马上被再次使用的时候,它就会算是被命中,从而再次回到活跃队列,这意味着它可以在内存中逗留的时间会增加。
那么当这个结构被再次命中的时候它如何激活呢?我调试了一下,发现它没有被激活,所有的dentry在释放的时候(dput)通过
      if (list_empty(&dentry->d_lru)) {
          dentry->d_flags |= DCACHE_REFERENCED;
          list_add(&dentry->d_lru, &dentry_unused);
          dentry_stat.nr_unused++;
      }
将它添加到由dentry_unused引导的系统全局dentry链表中,当它再次被使用的时候,例如再次执行ls命令,它走到dput的时候,这里的list_empty将会一直为不满足状况,所以这个lru的内容将会一直不再变化。而唯一变化的则是dentry中表示该目录项被引用次数的d_count成员。当dentry被再次使用时,它在__d_lookup函数中增加引用计数
        if (!d_unhashed(dentry)) {
            atomic_inc(&dentry->d_count);
            found = dentry;
        }
从而保证该结构不被slab结构回收。
四、inode及页面释放
当inode释放的时候,它包含的address_space中的内容也将会被释放。这一点比较重要,因为看新的内核中对于文件页面的hash是通过address_space作为重要键值来hash的,并且是address_space的地址作为hash,所以如果该结构释放,其中的所有内容也需要被释放。但是反过来说,即使address_space没有被释放,它包含的页面也可以释放,因为页面也有自己的lru队列来进行页面的老化。即使在同一个程序的内部,一些代码段是热点代码,有些则是非热点代码,热点代码所在的页面就应该在内存中保留较长时间,而非热点则应该在需要的时候被释放出去,要么释放页面,需要时再次启动硬盘操作读入;对于脏页面则需要写回之后再释放。
当slab分配器释放页面的时候,它执行下面流程
shrink_icache_memory--->>>prune_icache--->>invalidate_mapping_pages--->>invalidate_complete_page--->>remove_mapping---->>>__remove_from_page_cache---->>>radix_tree_delete
该操作之后,address_space中将无法看到该页面。
五、page页面cache
我们看页面cache的hash及查找方法
do_generic_mapping_read--->>find_get_page---->>>radix_tree_lookup
在这个过程中,address_space是一个基础的容器,所以如果该结构被释放,那么它所有的页面同样会从系统中消失,至少通过cache查找是无法查到该文件的。所以如果说一个inode被释放的话,相当于该文件中所有页面被释放,也就是不再被cache。而inode的cache是由slab来保存的,所以只要inode能够在slab中保存足够长的时间,那么页面同样可以再次被cache命中。反过来说,即使inode没有被释放,一个inode的address_space中的页面也有可能被回收,看一下页面回收的流程
free_more_memory----->>>try_to_free_pages
for (priority = DEF_PRIORITY; priority >= 0; priority--) {
……
        nr_reclaimed += shrink_zones(priority, zones, &sc); 首先回收zone页面
        shrink_slab(sc.nr_scanned, gfp_mask, lru_pages);然后从slab中回收。
……
}

shrink_zones--->>>shrink_zone
while (nr_active || nr_inactive) {
        if (nr_active) {
            nr_to_scan = min(nr_active,
                    (unsigned long)sc->swap_cluster_max);
            nr_active -= nr_to_scan;
            shrink_active_list(nr_to_scan, zone, sc, priority);//该函数中通过list_move(&page->lru, &zone->inactive_list);将页面从活跃列表转移到不活跃链表中
        }

        if (nr_inactive) {
            nr_to_scan = min(nr_inactive,
                    (unsigned long)sc->swap_cluster_max);
            nr_inactive -= nr_to_scan;
            nr_reclaimed += shrink_inactive_list(nr_to_scan, zone,
                                sc);
        }
    }
六、页面再次激活
当一个页面被再次使用时,该页面就被激活。大家搜索一下mark_page_accessed和lru_cache_add_active两个函数的调用。例如,当发生文件映射缺页的时候,cache页面应该从非活跃转义到活跃队列,从而避免页面回收时被选中。
对于一个文件内的页面,当新进程创建之后,文件的vma中还没有数据,虽然这些数据在内存中,inode也在,但是进程的地址空间是一个私有的概念,所以新进程访问的时候依然会出现缺页异常。此时异常处理程序就需要把这个内存中cache的页面和进程的内存地址空间建立起联系,在这个时候,系统就有机会知道文件中一个特定页面被访问了,此时可以将存在于非活跃链表上的页面放到活跃页面中。
do_generic_mapping_read
page = find_get_page(mapping, index);
……
        /*
         * When (part of) the same page is read multiple times
         * in succession, only mark it as accessed the first time.
         */
        if (prev_index != index)
            mark_page_accessed(page);

void fastcall mark_page_accessed(struct page *page)
{
    if (!PageActive(page) && PageReferenced(page) && PageLRU(page)) {
        activate_page(page);
        ClearPageReferenced(page);
    } else if (!PageReferenced(page)) {
        SetPageReferenced(page);
    }
}
七、一个推论
一个被频繁使用的程序操作系统具有跨进程的lru机制来保证避免对相同内容做重复的磁盘操作,所以如果说一个服务器中有一个程序被进场使用,或者一个文件被多次编辑,那么对这个文件的操作将会比较快。大家可以感受一些大型的程序,在第一次启动的时候比较慢,关掉之后再次启动就会快很多;或者说用在source insiht工程中搜索一个关键字,第一次搜索比较慢,第二次搜索(即使不同的关键字)就会比较快。
  评论这张
 
阅读(1353)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017