合在页高速缓存里面的缓冲区高速缓存

时间:2020-12-30 07:40:01

一直以来,缓存和缓冲的概念十分容易引起混淆,其实如果用英文来表示的话可能会好一些,用英文表示,缓冲就是buffer,缓存就是cache,buffer有减轻,减震的作用,其实就是为了减少抖动而采取的平滑化方案,而后者cache是真实事物的代替或者是为了更低价的取得一些数据而采取的暂存方案,这是它们的区别,那么它们的联系是什么呢?最简单的,缓冲区可以被缓存吗,或者相反,缓存需要缓冲一下子吗?缓存不必缓冲,但是缓冲有必要缓存,因为缓冲直接对应容易引起抖动的操作,而且只要相同的操作在不久的将来很容易再来一次,那么就很有必要将缓冲作一下缓存,将来再操作相同的缓冲区的时候就可以直接从缓存中取得了缓冲区来操作,这就是要点。

对于linux来说,2.4之前的内核中有两种缓存,一种是vfs的页高速缓存,另外一种是缓冲区高速缓存,实际上缓冲区对应于磁盘块,就是磁盘块在内存中的表示罢了,其中的数据也还是文件中的数据,只不过页高速缓存是按照页面管理的,而缓冲区高速缓存是按照块来管理的,两个缓存在数据本身上有一定的重合,这就造成了冗余,在内核中是很不好的,比如123456是页高速缓存的数据,到了缓冲区高速缓存,其实还是123456,只不过不再这么排列了,而是分成了一块一块的,比如1,2,3,...如果能将这些1,2,3指向页高速缓存,那么就可以不再需要为缓冲区高速缓存分配大量的内存空间了。2.4往后的内核就是这么做的,到了2.6内核,有了bio,那么buffer_head再也不用作为IO容器了,一般来说,涉及到io的时候,如果能用bio的话就尽量用bio,实在不行再用老式的buffer_head进行io,即使用了buffer_head,最终也还是要归结为bio的,因为2.6内核中只有bio结构体可以进行io操作。这个怎么理解呢?linux在读写文件的时候会先将文件逻辑映射到页面逻辑,然后会在页高速缓存中寻找文件的数据,如果找到,那么对于读操作,那么就将数据直接返回给用户,如果对于缓冲写操作,那么会将数据写到这个页高速缓存中的页面,如果没有在页高速缓存找到页面,那么会分配一个页面加入到页高速缓存,然后着手IO操作,即使不是没有在页高速缓存找到页面而是找到的页面不是uptodate的页面,那么也要着手IO,怎么IO呢,这就是涉及到2.6内核的一个新特性的新实现,这就是mpage,其实就是页面的io操作,具体实现就要用到bio结构了,vfs子系统会在页高速缓存的需要io的页面上循环进行操作,怎么操作呢,就是将一个页面分割为多个“逻辑块”,然后构造一个bio,将一个页面加入到这个bio,这样循环中的每一个页面都会加入到这个bio,最后,统一用mpage_bio_submit来提交io操作,我们来看看代码片段:

int mpage_readpages(struct address_space *mapping, struct list_head *pages, unsigned nr_pages, get_block_t get_block)

{

...

for (page_idx = 0; page_idx < nr_pages; page_idx++) {

struct page *page = list_entry(pages->prev, struct page, lru);

... //下面的一行循环将一个页面插入到一个bio

bio = do_mpage_readpage(bio, page, nr_pages - page_idx, &last_block_in_bio, get_block);

}

...

if (bio) //最后,如果这个bio存在,那么提交io请求,实际上百分之九十九的可能这个bio是存在并且初始化过的

mpage_bio_submit(READ, bio);

return 0;

}

需要注意的是,mpage机制中,每个页面只能属于一个bio而不能属于多个bio,这是因为如果属于多个bio,那么这些bio完成的时间将不确定,如此一来就不能通过bio的状态来了解page页面的状态,bio中最本质的字段就是bi_io_vec,这是一个bio_vec的链表

struct bio_vec {

struct page *bv_page; //该bio_vec所用的page

unsigned int bv_len; //该bio_vec中数据的长度

unsigned int bv_offset; //该bio_vec的数据在page中的偏移

};

一个bio可以有多个bio_vec,每个bio_vec对应一个页面,通过这个bio_vec结构的bv_len和bv_offset来定位数据,但是在mpage中每个页面只能属于一个bio,这是为了判断页面状态的简单,如果不能存在于一个page的磁盘的连续block,那么显然不能设置到一个bio,这时就需要用buffer_head来进行io,虽然buffer_head在2.6内核中也是存在于page的,也是基于page进行缓存的,但是buffer_head本身就是单个的一个一个的,并且可以判断一个bh的状态,另外就是bh到了底层也是通过bio实现的,只不过这个bio只有一个page,也就是其bi_io_vec只有一个bio_vec对象,mpage机制的好处就是充分利用了bio的多个页面的机制,就是说一个bio代表一个io,在底层驱动程序之上,可以将一个bio作为一次io,但是这个bio不像以往的buffer_head只能进行一个磁盘块的io而是可以进行基于页面的io,而且可以给予多个页面,这些页面可以连续也可以不连续,每一个页面中再通过磁盘块来进行块io,只不过向前推进io是通用块层管理的,也就是说,在当前的bi_io_vec完成后,会自动推进到bi_io_vec中下一个对象,这一切都不用用户关心,具体就是通过request_queue来管理的,我们下面来看一眼do_mpage_readpage:

static struct bio * do_mpage_readpage(struct bio *bio, struct page *page, unsigned nr_pages, sector_t *last_block_in_bio, get_block_t get_block)

{

...

if (page_has_buffers(page)) //如果这个页面有bh,那么就直接进入buffer_head方式的io

goto confused;

block_in_file = page->index << (PAGE_CACHE_SHIFT - blkbits);

last_block = (i_size_read(inode) + blocksize - 1) >> blkbits;

bh.b_page = page;

for (page_block = 0; page_block < blocks_per_page; page_block++, block_in_file++) {

bh.b_state = 0;

if (block_in_file < last_block) {

if (get_block(inode, block_in_file, &bh, 0)) //文件系统相关的get_block,这个函数初始化一些bh的字段

goto confused;

}

...

if (page_block && blocks[page_block-1] != bh.b_blocknr-1) //如果该页面中有一个block和别的不连续,那么就不能用mpage的方式了,那么只能由bh的方式接管,其实就是一个一个的bh的提交,最终就是一个bio中只有一个页面的一段数据。

goto confused;

blocks[page_block] = bh.b_blocknr;

bdev = bh.b_bdev;

}

...

alloc_new:

if (bio == NULL) { //分配一个bio

bio = mpage_alloc(bdev, blocks[0] << (blkbits - 9),

min_t(int, nr_pages, bio_get_nr_vecs(bdev)), GFP_KERNEL);

if (bio == NULL)

goto confused;

}

length = first_hole << blkbits;

if (bio_add_page(bio, page, length, 0) < length) {

bio = mpage_bio_submit(READ, bio);

goto alloc_new;

}

...

return bio;

confused:

if (bio)

bio = mpage_bio_submit(READ, bio);

if (!PageUptodate(page)) //接下来的方式就是bh的方式了

block_read_full_page(page, get_block);

...

}

在bh的方式中,block_read_full_page完成一切,在这个函数中,就是将一个页面划分为n多个block,n为PAGE_SIZE/block_size,然后循环判断这n个bh,如果该bh已经uptodate了,那么就继续判断下一个,只要没有uptodate的bh就加入到一个临时的bh数组中,循环完毕之后再循环提交这个临时bh数组中的buffer_head,进行实际的io。

int block_read_full_page(struct page *page, get_block_t *get_block)

{

struct inode *inode = page->mapping->host;

sector_t iblock, lblock;

struct buffer_head *bh, *head, *arr[MAX_BUF_PER_PAGE];

unsigned int blocksize;

int nr, i;

int fully_mapped = 1;

if (!PageLocked(page))

PAGE_BUG(page);

blocksize = 1 << inode->i_blkbits;

if (!page_has_buffers(page)) //如果还没有创建bh,那么为这个页面创建PAGE_SIZE/block_size个bh并且初始化,这样就相当于缓存了这些bh,当下一个用到的时候就可以直接取到,直接取page_buffers就行了。

create_empty_buffers(page, blocksize, 0);

head = page_buffers(page);

...

do {

...

if (!buffer_mapped(bh)) {

fully_mapped = 0;

if (iblock < lblock) {

if (get_block(inode, iblock, bh, 0))

SetPageError(page);

}

...

if (buffer_uptodate(bh)) //如果这个bh已经更新过了,那么掠过,这里的技巧就是每个bh都可以单独的判断状态

continue;

}

arr[nr++] = bh;

} while (i++, iblock++, (bh = bh->b_this_page) != head);

...

for (i = 0; i < nr; i++) { //循环提交这个临时bh数组的buffer_head

bh = arr[i];

if (buffer_uptodate(bh))

end_buffer_async_read(bh, 1);

else

submit_bh(READ, bh);

}

return 0;

}

下面这个函数很重要,其实是很有意思,很简洁,linux中不乏这种简洁的函数,初看不甚理解,再看则恍然大悟

void create_empty_buffers(struct page *page, unsigned long blocksize, unsigned long b_state)

{

struct buffer_head *bh, *head, *tail;

head = create_buffers(page, blocksize, 1);

bh = head;

do {

bh->b_state |= b_state;

tail = bh;

bh = bh->b_this_page;

} while (bh);

tail->b_this_page = head;

spin_lock(&page->mapping->private_lock);

if (PageUptodate(page) || PageDirty(page)) {

...//循环设置这个page中每个bh的状态,什么状态呢?当然是dirty或者uptodate

}

__set_page_buffers(page, head); //将这个页面设置为缓冲区高速缓存

spin_unlock(&page->mapping->private_lock);

}

static struct buffer_head * create_buffers(struct page * page, unsigned long size, int retry)

{

struct buffer_head *bh, *head;

long offset;

try_again:

head = NULL;

offset = PAGE_SIZE;

while ((offset -= size) >= 0) { //这个循环实际上就是将buffer_head加入到页高速缓存的页面

bh = alloc_buffer_head(GFP_NOFS);

if (!bh)

goto no_grow;

bh->b_bdev = NULL;

bh->b_this_page = head;

bh->b_blocknr = -1;

head = bh;

bh->b_state = 0;

atomic_set(&bh->b_count, 0);

bh->b_size = size;

set_bh_page(bh, page, offset); //见下面

bh->b_end_io = NULL;

}

return head;

no_grow:

if (head) {

do {

bh = head;

head = head->b_this_page;

free_buffer_head(bh);

} while (head);

}

if (!retry)

return NULL;

free_more_memory();

goto try_again;

}

void set_bh_page(struct buffer_head *bh, struct page *page, unsigned long offset)

{

bh->b_page = page;

...

if (!PageHighMem(page))

bh->b_data = page_address(page) + offset;

...

}

实际上,在2.6内核中并存了buffer_head和bio两种提交io的方式,这种说法其实不是很准确,它们毕竟不是一个层次的,bh是用bio实现的,可是真的就是有这两种提交io的方式,为何?bio的方式提供了文件操作的mpage机制,可以使得一次读写多个页面的操作可以只提交一次io请求,本质上说,bio这个新的io操作容器是基于页面的,而不是基于磁盘块的,我们知道,在用户空间的程序库设计中,可以尽量的进行抽象,将用户直接操作的步骤变得更加人性化,可是在内核中这么做可能显得没有意义,因为内核的目的不是为了扩展和可读性,而是为了安全高效的为用户提供服务,只要安全只要高效别的什么也不管,可是当我看到bio的设计时,我发现原来在内核中也存在美妙的设计,原来的buffer_head代表一个磁盘逻辑块,如果读写一个页面或者若干页面的数据,那么就必选首先将这些页面映射到直接对应磁盘块的buffer_head,我们可以看一下2.4.0的ext2_aops中readpages回调函数的实现:

int block_read_full_page(struct page *page, get_block_t *get_block)

{

...

}

别的不用多说什么,我们发现,老的基于buffer_head的实现和新的block_read_full_page实现很相似,几乎是一摸一样,那么难道说2.6的mpage是个多余的层次,其实不然,因为2.6的基于bio的io操作将io向前推进的任务交给了通用块层,不再需要vfs层进行页面和块的映射了,之需要vfs用页面初始化一个bio,不管几个页面,不管页面的数据在哪,然后将这个bio提交,这样这些页面的这些数据就会一个个的被写入磁盘或者从磁盘中读出到页面。

对于块设备文件。比如/dev/sda1,这种文件中存有大量真实文件的元数据,这些块设备文件的数据也存在于基树种,并且其bh也存在于基树中,比如在ext2文件系统需要将磁盘inode读入到内存inode,那么此时就需要一个bh,vfs会在这个块设备的基树中寻找这个bh所在的page,如果找到直接返回,如果找不到那么读盘,并且将结果缓存在这个块设备的页高速缓存当中。总的说来,2.6内核就是将缓冲区高速缓存也存到了页高速缓存中,这样可以更加有利于磁盘调度代码的高效率执行,一次性将请求交给通用块驱动,然后具体如何合并和拆分这些bio的页面们以及其中的block,那就看具体的调度策略了。那么还有一个问题,就是一个页面往往对应很多的缓冲区高速缓存,常规情况下是4个,那么怎么知道哪个bh是需要的呢?下面得循环可以得到答案:

head = page_buffers(page);

bh = head;

do {

if (bh->b_blocknr == block) {

ret = bh;

get_bh(bh);

}

bh = bh->b_this_page;

} while (bh != head);