---
高速缓存模块的位于文件系统和块设备驱动程序之间.缓存的结构是文件系统中的逻辑块.
这篇文章主要回答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中):
- struct buffer_head {
- char * b_data; /* pointer to data block (1024 bytes);指向第二类结构高速缓存块的指针*/
- unsigned long b_blocknr; /* block number; 磁盘上的数据区的逻辑块号*/
- unsigned short b_dev; /* device (0 = free); 映射的数据块所在的设备号,2是软盘,3是硬盘,0是空闲*/
- unsigned char b_uptodate; //表示b_data指向的缓冲块是否有效(是否和设备块上的数据一样)
- unsigned char b_dirt; /* 0-clean,1-dirty; 是否修改过,是否脏*/
- unsigned char b_count; /* users using this block; 有多少个进程在引用这个缓冲块*/
- unsigned char b_lock; /* 0 - ok, 1 -locked; 上锁标识*/
- struct task_struct * b_wait; //如果上锁了,访问该缓冲块的进程将会在该队列中睡眠等待
- struct buffer_head * b_prev; //hash表中使用的前驱指针
- struct buffer_head * b_next; //hash表中使用的后驱指针
- struct buffer_head * b_prev_free;//空闲循环双链表中使用的前驱指针
- struct buffer_head * b_next_free;//空闲循环双链表中使用的后驱指针
- };
高速缓存区头结构和缓存块关系
其实分析当看到上面的高速缓存区头结构后应该就会产生两个疑问,1、高速缓存区块是1024个字节,而高速缓存区头明显不是,那这个内存是怎么分的?2、高速缓存区头结构体中有一套hash链表指针和一套循环双链表指针,那缓存头结构之间是怎么联系的?
先来解决第一个问题:高速缓存区中是怎么划分缓存头结构和缓存块的?这个划分我是第一次见的,很奇特,就是从缓存区开始地址buffer_start处划分出一块地址定义缓存头结构,同时在缓存区结束地址buffer_end处划分一块地址给缓存块使用,并且让缓存头结构中的b_data指向该缓存块;实现代码是在缓存区初始化函数buffer_init()中:
- //缓存区初始化函数
- //参数:buffer_end指向缓存区内存的末端;对于系统有16MB内存,缓存为4MB;8MB--2MB
- void buffer_init(long buffer_end)
- {
- struct buffer_head * h = start_buffer;
- void * b;
- int i;
- if (buffer_end == 1<<20)//640k~1M用于显存和BIOS ROM;高端地址应该设置为b=640kb
- b = (void *) (640*1024);
- else
- b = (void *) buffer_end;//否则高地址还是设置buffer_end
- //下面代码用来初始化缓存区,建立空闲缓存区环链表,并获取系统中缓存块的数目
- //高端划分1k大小的缓存块,低端设置相应的缓存块头部结果指向高端缓存块
- while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) {
- h->b_dev = 0;//使用该缓存区的设备号
- h->b_dirt = 0;//修改标志
- h->b_count = 0;//引用计数
- h->b_lock = 0;//锁标志
- h->b_uptodate = 0;//有效标志
- h->b_wait = NULL;
- h->b_next = NULL;
- h->b_prev = NULL;
- h->b_data = (char *) b;//指向末尾的缓存块
- h->b_prev_free = h-1;
- h->b_next_free = h+1;
- h++; //接着到下一个缓存头
- NR_BUFFERS++;//缓存块计数
- if (b == (void *) 0x100000)//如果地址b递减到了1M,则跳到640kb处,因为这段地址被BIOS ROM和显存使用
- b = (void *) 0xA0000;
- }
- h--;//指向最后一个有效缓存头结构
- free_list = start_buffer;//空闲指针指向第一个头结构
- free_list->b_prev_free = h;//做个循环链表
- h->b_next_free = free_list;
- for (i=0;i<NR_HASH;i++)//初始化hash表
- hash_table[i]=NULL;
- }
高速缓存区头结构之间关系
- //将指定缓冲区插入空闲链表尾并放入hash队列中
- static inline void insert_into_queues(struct buffer_head * bh)
- {
- /* put at end of free list *///放到空闲链表尾部,free_list是头指针
- bh->b_next_free = free_list;
- bh->b_prev_free = free_list->b_prev_free;
- free_list->b_prev_free->b_next_free = bh;
- free_list->b_prev_free = bh;
- ................
- 缺省的是插入hash表的代码
- .................
- }
- //空闲块条件 引用计数为0,干净块,没上锁
- do {
- if (tmp->b_count)
- continue;
- //其实下面是寻找干净块,没上锁
- if (!bh || BADNESS(tmp)<BADNESS(bh)) {
- bh = tmp;
- if (!BADNESS(tmp))
- break;
- }
- /* and repeat until we find something good */
- } while ((tmp = tmp->b_next_free) != free_list);
分析完头结构中的循环双链表,再来分析下头结构中的hash链表,首先来看下hash函数:
- //根据hash函数来
- #define _hashfn(dev,block) (((unsigned)(dev^block))%NR_HASH)//hash函数,dev^block
- #define hash(dev,block) hash_table[_hashfn(dev,block)]//hash表项
- //这个hash队列的插入是从头开始插入的;如果要从尾部插入,则要循环判断到位null
- bh->b_next = hash(bh->b_dev,bh->b_blocknr);//插入到得到的hash值队列的第一块,也就是首地址
- hash(bh->b_dev,bh->b_blocknr) = bh;//hash组指针要指向对应hash值队列
- bh->b_next->b_prev = bh;//处理bh第一个hash项的前指针