一直以來,緩存和緩沖的概念十分容易引起混淆,其實如果用英文來表示的話可能會好一些,用英文表示,緩沖就是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的字段
if (page_block && blocks[page_block-1] != bh.b_blocknr-1) //如果該頁面中有一個block和别的不連續,那麼就不能用mpage的方式了,那麼隻能由bh的方式接管,其實就是一個一個的bh的送出,最終就是一個bio中隻有一個頁面的一段資料。
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)
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)
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);
下面這個函數很重要,其實是很有意思,很簡潔,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;
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) {
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回調函數的實作:
别的不用多說什麼,我們發現,老的基于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是需要的呢?下面得循環可以得到答案:
if (bh->b_blocknr == block) {
ret = bh;
get_bh(bh);
} while (bh != head);
本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1273494