xv6源码分析 016
之前我们是自顶向下进行分析的(从文件描述符层开始,一直到物理存储层),现在看代码的话我们就是自底向上进行的,因为我们上层使用的接口都实现在下面的层次,如果直接从顶层开始讲的话,很容易就一头雾水的。
let us begin.
buffer cache
物理存储层跳过,因为我看不懂那部分的代码,有关MMIO的,兄弟们可以去了解一下,我现在还在学习这一块
OK,buffer cache层对应的文件就是buf.h
和bio.c
我们先看看在buf.h
中定义的struct buf
struct buf {
int valid; // has data been read from disk?
int disk; // does disk "own" buf?
uint dev;
uint blockno;
struct sleeplock lock;
uint refcnt;
struct buf *prev; // LRU cache list
struct buf *next;
uchar data[BSIZE];
};
我们一个一个来看
valid
:表示这个buf是否有效,是否是从磁盘中读出来的,因为在buffer cache初始化的时候,里面是没有数据的,也就是说所有的buf都是无效的disk
:表示当前buf属于那个物理磁盘,对于我们的计算机来说,可能会有多个磁盘,因为要保证存储的稳定性(stable-strode),单个磁盘属于non-volatile strode,但是一个磁盘阵列就属于stable-strode,我们一般成多个磁盘组成的存储系统为冗余磁盘阵列(RAID)。这里不做过多的赘述了,感兴趣的兄弟们可以去了解一下。dev
:设备号blockno
:块号,如果物理存储器是磁盘则代表磁盘上的逻辑扇区号,如果是SSD,就是块号(主流的就这两种)。lock
:维护这个buf不变量的一个自旋锁prev
:LRU链表的上一个块next
:LRU链表中的下一个块data
:数据
我们进入bio.c
bio.c
我们的系统中有很多种io,aio,uio,bio,还有java的nio等等,感兴趣的兄弟们可以去了解一下
先看看核心数据结构
bcache
struct {
struct spinlock lock;
struct buf buf[NBUF];
// Linked list of all buffers, through prev/next.
// Sorted by how recently the buffer was used.
// head.next is most recent, head.prev is least.
struct buf head;
} bcache;
很简单,看看就能理解。next one
bcache的初始化
void
binit(void)
{
struct buf *b;
initlock(&bcache.lock, "bcache");
// Create linked list of buffers
bcache.head.prev = &bcache.head;
bcache.head.next = &bcache.head;
for(b = bcache.buf; b < bcache.buf+NBUF; b++){
b->next = bcache.head.next;
b->prev = &bcache.head;
initsleeplock(&b->lock, "buffer");
bcache.head.next->prev = b;
bcache.head.next = b;
}
}
我们可以看到,xv6中是为整个buffer cache维护了一把大锁,导致每一个只能有一个进程访问buffer cache,在后期的lab中,就有一个lab需要我们涉及一个细粒度锁的buffer cache来优化,并支持并发访问
下面就是初始化LRU链表,这里我们需要注意的是,buffer cache只是用bcache.buf
来从存放全部的struct buf
,但是整个buffer cache是通过一个LRU链表来组织的,这里的数据存储和数据的组织我们需要好好的理解一下。
从buffer cache中获取数据
static struct buf*
bget(uint dev, uint blockno)
{
struct buf *b;
acquire(&bcache.lock);
// Is the block already cached?
// 先查看我们指定的buf是否已经在内存中了
for(b = bcache.head.next; b != &bcache.head; b = b->next){
if(b->dev == dev && b->blockno == blockno){
b->refcnt++;
release(&bcache.lock);
acquiresleep(&b->lock);
return b;
}
}
// 如果不在,我们需要找到一个能够存放
// 我们从物理存储设备中读进来的buf。
// 如果buffer cache中的所有buf都在使用中
// 直接panic,哈哈
// Not cached.
// Recycle the least recently used (LRU) unused buffer.
for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
if(b->refcnt == 0) {
b->dev = dev;
b->blockno = blockno;
b->valid = 0;
b->refcnt = 1;
release(&bcache.lock);
acquiresleep(&b->lock);
return b;
}
}
panic("bget: no buffers");
}
主要的步骤如下,
- 查看指定的数据是否已经被缓存在buffer cache中,我们是怎么指定数据的呢。这依赖于我们上一篇提到的dir层的引导块。后面会将
- 如果没有缓存,我们就需要从物理存储设备中读取,这时候,就执行LRU算法并查看算法每一次找到的buf是否正在使用。如果buf这正在使用中,继续执行算法直到找到一个可用的buf,或者到达链表尾部。
- 找到之后我们就直接返回这个buf
- 如果没有找到我们就panic(死机)
设备io操作
现在我们已经拿到了一个能够存放数据的内存块buf了,接下来自然是从磁盘中读取数据到这个buf上,或者将这个buf上的数据写回磁盘了
看函数
// Return a locked buf with the contents of the indicated block.
struct buf*
bread(uint dev, uint blockno)
{
struct buf *b;
b = bget(dev, blockno);
if(!b->valid) {
virtio_disk_rw(b, 0);
b->valid = 1;
}
return b;
}
// Write b's contents to disk. Must be locked.
void
bwrite(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("bwrite");
virtio_disk_rw(b, 1);
}
释放buf
这个函数将一个已经加锁的buf释放,并从LRU队列中移除。
void
brelse(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("brelse");
releasesleep(&b->lock);
acquire(&bcache.lock);
b->refcnt--;
if (b->refcnt == 0) {
// no one is waiting for it.
b->next->prev = b->prev;
b->prev->next = b->next;
b->next = bcache.head.next;
b->prev = &bcache.head;
bcache.head.next->prev = b;
bcache.head.next = b;
}
release(&bcache.lock);
}
pin和unpin
当我们访问一个buf的时候为了避免另一个进程将这个buf驱逐evit,我们需要将这个bufpin住。
void
bpin(struct buf *b) {
acquire(&bcache.lock);
b->refcnt++;
release(&bcache.lock);
}
void
bunpin(struct buf *b) {
acquire(&bcache.lock);
b->refcnt--;
release(&bcache.lock);
}
也很简单就是引用计数的增减。
OK,buffer cache层就讲完了,总的来说还是比较简单,相比于数据库来说。对于数据库中的buffer cache,我给大家安利一波cmu15-445。