高速数据缓存

---

高速缓存模块的位于文件系统和块设备驱动程序之间.缓存的结构是文件系统中的逻辑块.

这篇文章主要回答3个问题

1.高速缓存中的主要结构和如何分配这些主要的结构

2.高速缓存中循环双向列表和哈希队列的形成

3.根据1和2可以了解高速缓存从构建和查询以及维护缓存的全过程

本文章是我自己学习高速缓存时的参考文章,特此记录以备回顾

原文链接http://blog.csdn.net/yuzhihui_no1/article/details/43677663

---

高速缓冲区其实就是一段内存地址,这段内存地址是访问块设备上数据的一个桥梁。当然如果没有这个高速缓冲区也是可以的,每次要访问块设备上数据时就直接调用函数去读取块设备就可以。


高速缓存区简介:

        其实这个就好比如,一个很远的地方放着一排的东西(假设有:木匠工具1,木匠工具2,木匠工具3    还有水泥匠工具1,水泥匠工具2,水泥匠工具3。。。等等分类排着)。如果你需要木匠工具1,那么你需要跑很远的地方拿到木匠工具1;当你需要木匠工具2时,你又要跑很远的地方去拿;而这个时候你是没办法工作的,因为你手上没有工作需要的工具,所以你得浪费时间来等待工具被送来。如果工作8小时,其中有4个多小时在等工具送来,那么效率是多低下啊;所以聪明的人类就想了个办法,当你需要木匠工具1时,跑很远的地方去拿,但这次不仅仅是拿木匠工具1了,因为你能想到用木匠工具1,说明很可能你是在做木匠的工作,那其他有关木匠的工具等下也很可能会用到。所以干脆把有关木匠的工具全部都拿来,省的等下跑来跑去。如果需要木匠工具2了,就可以直接从身边拿来用就可以了。这样效率一下子就提高很多了。

        上面的假设有个小问题,如果你把所有的木匠工具拿来时,干完需要木匠工具1的工作后;你突然发现接下来要做水泥匠工作,需要水泥匠工具1;那么你又得屁颠屁颠的去很远的地方拿所有有关水泥匠的工具,然后放到工作的地方,再选择水泥匠工具1拿来工作;当干完需要水泥匠工具1的工作后,发现接下来又要干需要木匠工具2的工作;那你又得去很远的地方拿所有有关木匠工具(假设工作地方很小,只能放一类工具)。。。如此循环,发现效率还更低下(因为从很远的地方拿来很多工具,放在工作地方,然后选择需要的工作再拿来工作;而不是从很远的地方拿来工作;所以这样会比开始更浪费时间),但放心出现这样的概率是非常小的;

        上面的比喻(算比喻吗?假设算吧)其实就是高速缓存区的存在原因;块设备数据就好比那很远的一排工具,高速缓存区就好比身边工作的地方,工作当然就是CPU的运行了;从块设备中读取数据是很慢的(I/O操作相对于内存操作来说是比较慢的),所以为了不让CPU浪费宝贵的时间来等待读取块设备上的数据,就在内存中开辟了一段内存地址用来预获取最近使用过的多块设备块上的数据。当CPU需要访问块设备上的数据时,首先会在缓冲区中查找,如果有幸能找到,那么就直接拿过来使用;如果找不到就没办法,需要从块设备上把查找的数据块读取到高速缓存中(一般会多读取几个存放起来),然后CPU再从高速缓存区中读取需要的数据块;所以总结来说CPU只读内存上的数据,如果要读块设备上的数据,必须先放到高速缓存中,然后CPU才会读;


高速缓存区地址位置:

        高速缓冲区在内存的范围,结束地址在main.c函数中有提到,如果内存大于12MB,高速缓存区末尾地址为4MB(这里选的是16MB的,所以高速缓存区结束地址为4MB);起始地址再buffer.c中有说明,为内核代码的结束位置;当然其中要剔除掉BIOS ROM(可以参考http://blog.csdn.net/yuzhihui_no1/article/details/42744565,BIOS代码一般存放在0xfe000~0xfffff最后几kb中)和显存;所以高速缓存区在内存中的地址范围为如下图:

        


高速缓存区头结构:

        高速缓存区结构:高速缓冲区中分两类结构,第一类是高速缓冲区头部结构主要用来索引高速缓冲块地址,第二类当然就是高速缓存块了。第二类的高速缓冲块和磁盘块大小一样(一般应该是和逻辑块吧,这里逻辑块和磁盘块大小一样)都为1024字节(其他的文件系统好像是512字节,这个minix 1版本的文件系统有点老了)。所以高速缓存块就是存放数据的,没什么好分析的。主要分析下第一类高速缓冲区头部结构的结构体(文件在/linux/include/Fs.h中):

  1. struct buffer_head {  
  2.     char * b_data;          /* pointer to data block (1024 bytes);指向第二类结构高速缓存块的指针*/  
  3.     unsigned long b_blocknr;    /* block number;           磁盘上的数据区的逻辑块号*/  
  4.     unsigned short b_dev;       /* device (0 = free);      映射的数据块所在的设备号,2是软盘,3是硬盘,0是空闲*/  
  5.     unsigned char b_uptodate;       //表示b_data指向的缓冲块是否有效(是否和设备块上的数据一样)  
  6.     unsigned char b_dirt;       /* 0-clean,1-dirty;        是否修改过,是否脏*/  
  7.     unsigned char b_count;      /* users using this block; 有多少个进程在引用这个缓冲块*/  
  8.     unsigned char b_lock;       /* 0 - ok, 1 -locked;      上锁标识*/  
  9.     struct task_struct * b_wait;    //如果上锁了,访问该缓冲块的进程将会在该队列中睡眠等待  
  10.     struct buffer_head * b_prev;    //hash表中使用的前驱指针  
  11.     struct buffer_head * b_next;    //hash表中使用的后驱指针  
  12.     struct buffer_head * b_prev_free;//空闲循环双链表中使用的前驱指针  
  13.     struct buffer_head * b_next_free;//空闲循环双链表中使用的后驱指针  
  14. };  


高速缓存区头结构和缓存块关系

        其实分析当看到上面的高速缓存区头结构后应该就会产生两个疑问,1、高速缓存区块是1024个字节,而高速缓存区头明显不是,那这个内存是怎么分的?2、高速缓存区头结构体中有一套hash链表指针和一套循环双链表指针,那缓存头结构之间是怎么联系的?

        先来解决第一个问题:高速缓存区中是怎么划分缓存头结构和缓存块的?这个划分我是第一次见的,很奇特,就是从缓存区开始地址buffer_start处划分出一块地址定义缓存头结构,同时在缓存区结束地址buffer_end处划分一块地址给缓存块使用,并且让缓存头结构中的b_data指向该缓存块;实现代码是在缓存区初始化函数buffer_init()中:

  1. //缓存区初始化函数  
  2. //参数:buffer_end指向缓存区内存的末端;对于系统有16MB内存,缓存为4MB;8MB--2MB  
  3. void buffer_init(long buffer_end)  
  4. {  
  5.     struct buffer_head * h = start_buffer;  
  6.     void * b;  
  7.     int i;  
  8.   
  9.     if (buffer_end == 1<<20)//640k~1M用于显存和BIOS ROM;高端地址应该设置为b=640kb  
  10.         b = (void *) (640*1024);  
  11.     else  
  12.         b = (void *) buffer_end;//否则高地址还是设置buffer_end  
  13.   
  14.     //下面代码用来初始化缓存区,建立空闲缓存区环链表,并获取系统中缓存块的数目  
  15.     //高端划分1k大小的缓存块,低端设置相应的缓存块头部结果指向高端缓存块  
  16.     while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) {  
  17.         h->b_dev = 0;//使用该缓存区的设备号  
  18.         h->b_dirt = 0;//修改标志  
  19.         h->b_count = 0;//引用计数  
  20.         h->b_lock = 0;//锁标志  
  21.         h->b_uptodate = 0;//有效标志  
  22.         h->b_wait = NULL;  
  23.         h->b_next = NULL;  
  24.         h->b_prev = NULL;  
  25.         h->b_data = (char *) b;//指向末尾的缓存块  
  26.         h->b_prev_free = h-1;  
  27.         h->b_next_free = h+1;  
  28.         h++;    //接着到下一个缓存头  
  29.         NR_BUFFERS++;//缓存块计数  
  30.         if (b == (void *) 0x100000)//如果地址b递减到了1M,则跳到640kb处,因为这段地址被BIOS ROM和显存使用  
  31.             b = (void *) 0xA0000;  
  32.     }  
  33.     h--;//指向最后一个有效缓存头结构  
  34.     free_list = start_buffer;//空闲指针指向第一个头结构  
  35.     free_list->b_prev_free = h;//做个循环链表  
  36.     h->b_next_free = free_list;  
  37.     for (i=0;i<NR_HASH;i++)//初始化hash表  
  38.         hash_table[i]=NULL;  
  39. }     
        最后得到的效果图就是:

        


高速缓存区头结构之间关系

        在上面提到两个问题,第一个问题高速缓存区头结构和缓存块之间的关系已经解决,下面来分析下高速缓存区头结构之间的关系;在高速缓存区头结构中已经看过头结构体了,其中有b_data指针,上面已经解决了,指向了缓存块。但还有两套链表指针,先来看第一套循环双链表指针b_prev_free和b_next_free。
        这个循环双链表指针有free_list作为头指针,指向开始位置。大家可能会发现这个双链表为什么叫free,其实这个也误导了我,我一开始也以为这是个空闲头指针链表(因为是free嘛),后来看了代码才发现这根本就不完全是空闲链表,之所以取空闲为名,大概是因为在初始化时,把所有的缓存头都用双链表链接起来了,而那时候所有的缓存头都是空闲的。但是总的来说这个双链表是表示一条最近最少使用LRU链表。
        至于说free_list指向的链表是LRU链表,可以看下下面代码就明白了,第一、在整个buffer.c程序中对于插入循环双链表只有一个函数:static inline void insert_into_queues(struct buffer_head * bh);看看相关部分的具体实现:
  1. //将指定缓冲区插入空闲链表尾并放入hash队列中  
  2. static inline void insert_into_queues(struct buffer_head * bh)  
  3. {  
  4. /* put at end of free list *///放到空闲链表尾部,free_list是头指针  
  5.     bh->b_next_free = free_list;  
  6.     bh->b_prev_free = free_list->b_prev_free;  
  7.     free_list->b_prev_free->b_next_free = bh;  
  8.     free_list->b_prev_free = bh;  
  9.         ................  
  10.         缺省的是插入hash表的代码  
  11.         .................  
  12. }  
        在插入缓存头结构时,都插入到free_list后面,因为插入的缓存头结构都是刚开始使用过的,所以可以看出从free_list开始越到到后面就表示晚使用过。这中算法可以提供提高查找空闲缓存头结构的效率,下面看下查找缓存头结构的代码,在struct buffer_head * getblk(int dev,int block)函数中:
  1.     //空闲块条件  引用计数为0,干净块,没上锁  
  2.     do {  
  3.         if (tmp->b_count)  
  4.             continue;  
  5.         //其实下面是寻找干净块,没上锁  
  6.         if (!bh || BADNESS(tmp)<BADNESS(bh)) {  
  7.             bh = tmp;  
  8.             if (!BADNESS(tmp))  
  9.                 break;  
  10.         }  
  11. /* and repeat until we find something good */  
  12.     } while ((tmp = tmp->b_next_free) != free_list);  
        上面两段代码解释了缓存头结构双链表是一种LRU链表,下面看下链接图:
        

        分析完头结构中的循环双链表,再来分析下头结构中的hash链表,首先来看下hash函数:
  1. //根据hash函数来  
  2. #define _hashfn(dev,block) (((unsigned)(dev^block))%NR_HASH)//hash函数,dev^block  
  3. #define hash(dev,block) hash_table[_hashfn(dev,block)]//hash表项  
        插入hash表中函数static inline void insert_into_queues(struct buffer_head * bh)中;
  1. //这个hash队列的插入是从头开始插入的;如果要从尾部插入,则要循环判断到位null  
  2. bh->b_next = hash(bh->b_dev,bh->b_blocknr);//插入到得到的hash值队列的第一块,也就是首地址  
  3. hash(bh->b_dev,bh->b_blocknr) = bh;//hash组指针要指向对应hash值队列  
  4. bh->b_next->b_prev = bh;//处理bh第一个hash项的前指针  
        其实hash没有什么好将的,和一般的hash操作一样,下面来看下整体的缓存头结构之间的关系(其中包括循环双链表和hash链表)图:
        
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值