Linux内核工程导论——存储:缓存层

来源:互联网 发布:上海建模软件培训班 编辑:程序博客网 时间:2024/06/06 01:09

缓存层

bdi:缓存设备

         bdi是对块设备层的内存支持,相关代码页位于mm目录下。bdi的全称是backing device info,后备设备是非易失性存储器,但是这种存储器都比较慢,所以需要缓存。bdi对应的结构体是backing_dev_info,这个模块完成的工作就是对于每个bdi设备都对其写入操作进行缓存,然后再恰当的时间写入到bdi设备。注意的是,每个磁盘都对应bdi,而磁盘上的文件系统可以选择使用bdi,也可以不使用。如果选择使用了就意味着本质上有了双层的bdi。

         由于这里使用的是恰当的时间,这个时间点的选择就可以有多种情况。例如,周期性的,或者是内存使用到了一定的时候启动。这种方式启动的操作无疑是个内核线程(也可以是工作队列),但内核中采用了内核线程。在2.6.30以前,这个叫做pdflush线程。而之后的内核对性能进行了优化改进,就变成了bdi-default、flush-x:y等多个线程。

         这里考虑新版本的结构。bdi-default是flush-x:y的父线程。bdi-default根据情况产生或销毁一个或多个flush线程。flush-x:y中x表示设备的中了,y表示序号。这是内核设备中major和minor的编号思路。

         当然,对一个设备既有写也有读,但是读的主要机制是预读。

         bdi在实现时实现为链表。因为会有多个bdi设备(磁盘等),linux习惯把多个同类设备组织成链表。由于linux链表的名字一般用结构体的第一个域(链表域)来表示。而backing_dev_info的第一个域为bdi_list。代码中默认生成了两个全局的backing_dev_info结构体:default_backing_dev_info和noop_backing_dev_info。而4.02中已经只有一个noop_backing_dev_info了。

         每个功能模块都有两种意义上的初始化,一种是模块整体的初始化(包括初始化全局变量),另一种是模块所支持的实体的初始化。这个模块可能可以处理多个实体,每个实体在添加的时候也都需要初始化。backing-dev就是这样的模块。

 

backing_dev_info

 

struct backing_dev_info {

         struct list_head bdi_list; //bdi设备链表

         unsigned long ra_pages;   //预读的页数

         unsigned long state;        /* Always use atomic bitops on this */

         unsigned int capabilities; /* Device capabilities */

         congested_fn *congested_fn; /* Function pointer if device is md/dm */

         void *congested_data;    /* Pointer to aux data for congested func */

 

         char *name;

         struct percpu_counter bdi_stat[NR_BDI_STAT_ITEMS];

 

         unsigned long bw_time_stamp;      /* last time write bw is updated */

         unsigned long dirtied_stamp;

         unsigned long written_stamp;         /* pages written at bw_time_stamp */

         unsigned long write_bandwidth;     /* the estimated write bandwidth */

         unsigned long avg_write_bandwidth; /* further smoothed write bw */

 

         /*

          * The base dirty throttle rate, re-calculated on every 200ms.

          * All the bdi tasks' dirty rate will be curbed under it.

          * @dirty_ratelimit tracks the estimated @balanced_dirty_ratelimit

          * in small steps and is much more smooth/stable than the latter.

          */

         unsigned long dirty_ratelimit;

         unsigned long balanced_dirty_ratelimit;

 

         struct fprop_local_percpu completions;

         int dirty_exceeded;

 

         unsigned int min_ratio;

         unsigned int max_ratio, max_prop_frac;

 

         struct bdi_writeback wb;  /* default writeback info for this bdi */

         spinlock_t wb_lock;   /* protects work_list & wb.dwork scheduling */

 

         struct list_head work_list;

 

         struct device *dev;

 

         struct timer_list laptop_mode_wb_timer;

 

#ifdef CONFIG_DEBUG_FS

         struct dentry *debug_dir;

         struct dentry *debug_stats;

#endif

};

初始化

模块初始化

static int __init default_bdi_init(void)

{

         int err;

         bdi_wq = alloc_workqueue("writeback", WQ_MEM_RECLAIM | WQ_FREEZABLE |

                                                     WQ_UNBOUND | WQ_SYSFS, 0);

         if (!bdi_wq)

                   return -ENOMEM;

         err = bdi_init(&noop_backing_dev_info);

         return err;

}

         可以看出这个功能模块使用一个writeback的workqueue,然后初始化两个全局的bdi结构体,这个初始化调用的是相关的实体初始化函数。而4.02的内核代码只有noop_backing_dev_info一个全局对象了,所以只需要初始化一个。

实体初始化

 

int bdi_init(struct backing_dev_info *bdi)

void bdi_wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi)

         实体初始化函数主要是设置backing_dev_info的参数,bdi_wb_init初始化wb域对应的bdi_writeback结构体。bdi_init会调用bdi_wb_init来完成它的初始化工作。

bdi设备的注册

         一般的,一个bdi设备就是一个分区。由文件系统调用注册函数bdi_setup_and_register初始化和注册bdi。而设备调用bdi_register_dev注册bdi设备。而bdi_register_dev简单的是bdi_register的封装。

bdi_register

         //有问题

这个函数通用块层发现添加分区时调用的。这个函数首先创建一个device结构体,然后将这个创建device赋值给bdi->dev域。而调用这个的时候gendisk这个存储设备的bdi就已经存在了。也就是说,这一步其实本质是发现了某个分区,将该分区

内核从磁盘读取数据流程分析

l  Buffer.c:: block_read_full_page,这个函数使用一个page请求数据

n  这个page如果有关联的headbuffer就使用,没有就生成新的

n  更新buffer和page

n  锁定buffer

n  调用submit_bh启动读请求

l  Buffer.c::submit_bh根据给定的buffer head生成bio结构体,调用submit_bio将其提交

l  Blk-core::submit_bio做一个读写记录,然后调用generic_make_request将请求提交给通用块层

 

当然,请求不止一个入口,我需要的是找到buffer的位置。

我发现真正的页高速缓存位于mm/Filemap.c中,组织成了一个radix树,可以根据adree_space和偏移来检索(find_get_page),或者是添加(add_to_page_cache)、删除(remove_from_page_cache),更新页(read_cache_pag)。

对我而言,重要的是要知道其何时会释放页。所以我需要找到相关的函数,并且添加打印信息。

首先是mm/Filemap.c中直接操作函数:try_to_release_page。该函数释放本页上的所有缓存(只是尝试,不一定能释放,所以我需要添加打印信息,观察什么时候可以成功释放)

我还要找到调用它的上级代码:

l  mm/Vmscan.c中,有shrink_page_list函数,会尝试回收page

l  mm/trancate.c中,有invalidate_complete_page,会回收page(暂时不需要跟踪)

l  mm/swap.c中,交换时会回收page(由于我没有交换分区,所以这个无需跟踪)

l  fs/splice.c中,会尝试偷走一个page,我们暂时没有使用splice,所以暂时不需要跟踪(以后换成sendfile就需要了)

l  fs/buffer.c中,block_invalidatepage会释放page

 

所以,在以上的2处添加打印信息,并且在try_to_release_page添加。

编译,查看结果。

在开机的时候,发现了比较多的回收buffer的操作。但是没有看到回收内存的,而且可以看出,其是有定义releasepage函数的。

在Block_dev.c中有相关的定义:

static const struct address_space_operations def_blk_aops = {

         .readpage         = blkdev_readpage,

         .writepage       = blkdev_writepage,

         .sync_page       = block_sync_page,

         .write_begin    = blkdev_write_begin,

         .write_end       = blkdev_write_end,

         .writepages     = generic_writepages,

         .releasepage   = blkdev_releasepage,

         .direct_IO         = blkdev_direct_IO,

};

理论上,每个文件系统都会定义的。所以,这个函数在ntfs和fat中有可能不同。

但是,我觉得最有可能触发我的bug的不是这里,而是shrink那里。我需要触发它。

我发现在拷贝的过程中:

shrink函数进入的次数比较频繁,而且进入一次会多次进入releasepage函数,原因很容易理解,shrink_inactive_list函数是shrink的调用者,会遍历所有的inactive的页面来shrink。

我可以发现,page的release和复制在同时进行,也就是说,是两者的速度问题。我需要加快page的release的速度。

再上层的调用函数是shrink_list,其既会回收活动的page也会回收不活动的。再向上是shrink_zone和shrink_all_zones函数。

这里我就需要知道是哪个函数在复制时起作用。所以添加了打印信息编译。

所以,其都是由shrink_zone调用的。但是调用了这么多,我也没发现inactive的少了多少,有一个跟踪有多少inactive的page的变量吗?

我发现调用shrink_zone的函数有3个:

l  __zone_reclaim(该函数又由zone_reclaim唯一调用),而这个函数只在Page_alloc.c文件:: get_page_from_freelist中调用,是申请一个页,没有的话就触发收缩内存,也有可能是我们的想要找到的。

l  balance_pgdat

l  shrink_zones(此函数又由do_try_to_free_pages唯一调用,其又由try_to_free_pages唯一调用(没有cgroup),而该函数也是在Alloc_Page.c中需要内存时被触发,还有就是在buffer.c中触发pdflush之后,调用来回收一些内存),可见这可能不是我们最多看见的那个

 

意外的,我还发现了变量:

这个明显可以控制触发reclaim的比例。

我又发现了第二个调用此函数的重要函数:balace_pgdat,这个函数据说是由内核线程kswapd执行的,这个内存线程定期执行,回收一部分的buffer和cache的内存,默认是5%。所以,很自然的,我可以想到,通过设置这个线程执行的时间,或者提高其回收内存的比例就可以解决我现在的问题。

所以,我首先在这三个地方分别添加打印信息,确认平时是由谁在执行。

所以,这就很明显了,在复制时,实际回收内存的kswapd,我要让其回收的比例提高或者是执行的频率加快。

缓存内存回收算法

通过查阅资料,我得到一个重要知识:

kswapd(mm/Page_alloc.c)根据/proc下的min_free_kbytes计算每个zone的3个watermark(min、low、high),当系统可用内存低于watermark[low]的时候,就会叫醒kswapd,如果swapd不给力(回收的内存不如上层申请内存的速度快,使可用内存降至watermark[min])就会触发直接内存回收机制,就是上文的第三种情况。而这种方式会阻塞应用程序。

所以,我们的问题的本质原因就是kswapd回收内存的速度慢于使用内存内存的速度,从而触发了直接内存回收,导致samba阻塞。

而现在的值是864KB,其计算函数是init_per_zone_pages_min,其根据可用的总内存大小计算的。

我将其直接在内部硬编码为1600,如此,其会更早的开始kswapd回收,并且每次回收更多的内存。这样不一定有用,我其实可以让其硬编码,提高让其一次回收更多的内存。

修改之前,其可用的内存数为1700左右,现在是

表明,系统确实以较高的阀值在运作。

zone里面的也是一样提高了阈值。

缓存机制

         内核中所有的页都会用来做文件缓存,当内存不够的时候再回收该部分缓存。因此页分为匿名页和文件缓存页。匿名页是进程使用的,文件缓存页就用于文件的缓存。普通的内核读写都是要使用缓存层提供的缓存,但可以指定DIRECT IO来绕过缓存机制,一般数据库系统都是直接IO,但直接IO有对齐要求,操作麻烦,所以个人编程较少用到。

         至于Linux提供的异步IO,则一定是使用DirectIO,因为异步IO要求立即返回,而经过缓存的IO则是要阻塞等待文件更新到缓存后才会返回,所以不能被异步IO采用。所以,我们可以知道异步IO完成的时候就是数据已经实际的写入硬盘了(而同步IO不一定)。异步IO即使原则上不允许阻塞,但是由于其在实现时使用了锁,阻塞还是有可能的。

         既然要在内存中缓存文件,有的文件很大不可能完整的缓存到内存中。所以就有了数据组织的问题。组织的方法是使用一个叫做buffer head的结构体作为一个文件的总体缓存情况的描述,该结构体是个list。每个buffer head描述的文件缓存是一个缓存页的基树。基树的组织方式使得查询缓存时快。

缓存页的状态

         由于缓存页也是页,其没有针对每个页的缓存用途专门定义一个结构体,而是与page结构体共享。所以在本层查询page结构体的状态就知道当前的页的缓存状态,与page cache相关的页状态主要有:

l  PG_uptodate:页缓存的数据时最新的。如果读命中此页,此页的数据将直接返回给读者,不需要从磁盘读。

l  PG_dirty:页缓存的数据时被写过,但是还没有写入磁盘的。俗称脏的。当kswapd启动写入周期时,其会搜索到拥有该状态的页进行写入

l  PG_private:表示的是页的从属是用来放buffer head。并且该page结构体的private指针指向本页内存储的buffer head链表的头指针(一个页可以放很多个buffer head)

l  PG_mappedtodisk:表示该页中缓存的所有数据都对应在磁盘上有对应的块。这是由于有的写入会创建新的内存部分,此时该块内存在磁盘中没有对应,状态就是PG_dirty,但PG_dirty时不一定是创新的新的部分,还可能是修改。

 

所有的状态都分别占用一个内存位,因此同时存在是可能的。

文件锁

         由于如果多个进程对同一个文件进程读写操作可能导致数据不一致,所以linux定义了两种文件锁:建议锁和强制锁。强制锁只要上锁了,内核就保证没有并发访问,但是建议锁即使上锁,有其他进程不顾锁的存在仍可以继续对文件进行修改。

0 0
原创粉丝点击