为什么需要高速缓冲区而不是直接访问块设备中的数据。
这是因为,IO设备和内存之间的读写速度不匹配而且有一点数据需要写入或者读出磁盘就需要访问磁盘,磁盘很容易损坏。而高速缓冲区就起到了一个中间过程的作用,把数据存在高速缓冲区,需要读取磁盘上的数据时,尝试匹配高速缓冲区中的数据,匹配成功了,那就直接从高速缓冲区中取数据,然后内核再来操作,如果是要存入缓冲区,再存入磁盘,这样就避免了每次都对IO设备进行操作。
在高速缓冲区内部,分成了两个部分,一个是缓冲区头部,另一个是缓冲块。每个缓冲块的大小与块设备上的磁盘逻辑块的大小相同,而缓冲头结构用于连接起缓冲块以及设置一些属性。
内核要使用缓冲块,是怎么和物理设备对应起来的呢
比如向wps中写了一些数据,存储在缓冲块中,这个缓冲块怎么才能把数据写入磁盘呢,答案就是缓冲区头结构中存储了块设备号和缓冲数据的逻辑块号,它们一起唯一确认了缓冲块数据对应的块设备和数据块。对于空闲高速缓冲区采用双向链表管理方法。对于占用高速缓冲区的采用散列表管理方法。在linux0.11中采用的散列函数是:
#define _hash(dev, block) ((unsigned)(dev^block))%NR_HASH
。NR_HASH是哈希数组的长度。结构如图:
在图中,双向箭头代表哈希在同一个表项的双向链表指针,对应b_prev
和b_next
字段。虚线表示当前连接在空闲缓冲块链表中空闲缓冲块之间的链表指针,free_list是空闲链表的头指针。
读写流程:
- 读写某个dev block
- 通过getblk()函数找到块对应的高速缓冲区 buffer_head
- 再进行buffer_head的读写 bread()
详细分析getblk()函数
struct buffer_head * getblk(int dev,int block) { struct buffer_head * tmp, * bh; // 定义一个buffer_head 结构体指针 repeat: //找到对应设备的对应块的高速缓冲区 //如果当前找的缓冲区存在于散列表中那么就直接进行返回 if ((bh = get_hash_table(dev,block))) return bh; //如果当前块还不对应一个高速缓冲区 tmp = free_list; //从空闲缓冲区双向链表中找到一个优势值较大的高速缓冲区 do { if (tmp->b_count) continue; //BADNESS是一个优势值 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); //如果没有找到,对当前进程进行休眠 if (!bh) { sleep_on(&buffer_wait); goto repeat; } //等待被找到的空闲缓冲区不被再次使用 wait_on_buffer(bh); if (bh->b_count) //确保在等待过程中找到的高速缓冲区没有被使用 goto repeat; //如果当前找到的高速缓冲区是脏的,进行高速缓冲区的同步 while (bh->b_dirt) { // 如果该块是有残余数据的则进行回写同步 sync_dev(bh->b_dev); wait_on_buffer(bh); if (bh->b_count) goto repeat; } open(const char * filename, int flag,...) /* NOTE!! While we slept waiting for this block, somebody else might */ /* already have added "this" block to the cache. check it */ //如果当前找到的高速缓冲区存在于管理已占用缓冲的散列表中,就回去重新查找 if (find_buffer(dev,block)) goto repeat; /* OK, FINALLY we know that this buffer is the only one of it's kind, */ /* and that it's unused (b_count=0), unlocked (b_lock=0), and clean */ //OOP第二步 //设置当前找到的高速缓冲区 bh->b_count=1; bh->b_dirt=0; bh->b_uptodate=0; //OOP的第三步 //把从空闲缓冲区中找到的高速缓冲区加入到散列表中 remove_from_queues(bh); //删除节点从HASH和双向链表 bh->b_dev=dev; bh->b_blocknr=block; insert_into_queues(bh); //添加节点从HASH和双向链表 //返回对应的高速缓冲区 return bh; }
流程图如下:
详细分析bread()函数
getblk
()函数返回的可能是一个新的空闲块,也可能是含有我们需要的数据的缓冲块。因此对于读取数据块操作函数bread()
,就需要判断该缓冲块的更新标志,已知道所含数据是否有效,如果有效则直接返回给进程,否则就调用底层块读写函数ll_rw_block()
,并同时睡眠,等待数据从物理设备写入缓冲块,醒了之后再重新判断是否有效,如果还是不行,那就释放该缓冲块,并返回NULL。代码如下:
struct buffer_head * bread(int dev,int block) { //找一个buffer_head struct buffer_head * bh; //缓冲区是内存与外设进行数据交换的媒介 if (!(bh=getblk(dev,block))) panic("bread: getblk returned NULL\n"); //如果对应高速缓冲区已经被更新,直接返回 if (bh->b_uptodate) return bh; //从块设备中读取数据到对应的高速缓冲区 ll_rw_block(READ,bh); wait_on_buffer(bh); if (bh->b_uptodate) //返回读取数据后的bh return bh; brelse(bh); return NULL; }
流程图如图:
整体框架图: