linux中的块缓冲
把塊存放在頁高速緩存中
一、概述
Linux支持的文件系統大多以塊的形式組織文件,為了減少對物理塊設備的訪問,在文件以塊的形式調入內存后,使用塊高速緩存(buffer_cache)對它們進行管理。每個緩沖區由兩部分組成,第一部分稱為緩沖區首部,用數據結構buffer_head表示,第二部分是真正的緩沖區內容(即所存儲的數據)。由于緩沖區首部不與數據區域相連,數據區域獨立存儲。因而在緩沖區首部中,有一個指向數據的指針和一個緩沖區長度的字段。(當一個塊被調入到內存中,它要被存儲在一個緩沖區中。每個緩沖區與一個塊對應,它相當于磁盤塊在內存中的表示。而文件在內存中由file結構體表示,而磁盤塊在內存中是由緩沖區來進行表示的。由于內核處理塊時需要一些信息,如塊屬于哪個設備與塊對應于哪個緩沖區。所以每個緩沖區都有一個緩沖區描述符,稱為buffer_head. 它包含了內核操作緩沖區所需要的全部信息)注意:每個塊在內存中對應于一個緩存區,緩沖區有緩沖區頭部和數據域組成。注意:當頁的所有者為文件的時候,頁中存放的數據塊在磁盤上不一定是連續的(這主要是由于文件空洞的存在),而如果頁的所有者是塊設備的時候,頁中的塊數據在磁盤上一定是連續的。
二、緩沖區首部的數據結構
};
三、緩沖區頁、緩存區首部以及頁描述符之間的聯系
總結
1、從上圖可以看出一個緩沖區頁包含1–8個塊緩沖區,上圖畫的是一個緩沖區頁包含4個塊緩沖區。且每個塊緩沖區的大小事相同的。
2、同一個緩沖區頁內的各個塊緩沖區是在同一個單向的循環鏈表中的。具體是通過每個塊緩沖區首部的b_this_page字段進行連接的。同時頁描述符中的private字段指向該循環鏈表的頭。通過該字段 可以遍歷整個鏈表。
3、每個緩沖區首部通過字段b_page連接到擁有該塊緩沖區的頁的頁描述符上面。
4、每個緩沖區首部通過字段b_data指向該緩沖區首部對應的數據域。
四、分配塊設備緩沖區頁并將其加入到頁高速緩存
當內核發現指定塊的緩沖區所在的頁不在頁高速緩存中時,就分配一個新的塊設備緩沖區頁。內核調用函數grow_buffers()把塊設備緩沖區頁添加到頁高速緩存中,該函數接收三個標識塊的參數:
- block_device描述符的地址bdev。
- 邏輯塊號block(塊在塊設備中的位置)。
下面我們來看該函數的源碼:
首先給出函數調用的關系圖
點擊(此處)折疊或打開
首先grow_buffers()函數調用grow_dev_page()函數來實現其功能。而grow_dev_page()函數調用如下函數實現其功能:
(1)find_or_create_page()函數:該函數主要是在頁高速緩存中查找相應的緩沖區頁是否存在,如果不存在則建立相應的緩沖區頁。最終返回相應緩沖區頁的頁描述符的地址。
(2)page_has_buffers()函數:該函數主要是檢查page中的PG_private標志,如果該標志位空的說明,該頁不是一個緩沖區頁。那么這個時候接下來就會分配相應的緩沖區首部。
(3)page_buffers()函數:主要是根據頁描述符page的private字段來獲取緩沖區頁的第一個緩沖區首部的地址,即獲取第一緩沖區首部bh,這是在內存中有相應的緩沖區頁的情況下才執行這個函數。
(4)alloc_page_buffers:根據頁中所請求的塊大小為頁分配n個緩沖區首部,并把他們通過緩沖區首部的字段b_this_page連接成單向循環鏈表,同時設置各個緩存區首部的相應的字段的值。該函數主要是調用 allpc_buffer_head() 功能為 分配緩沖區首部并加入鏈表 。set_bh_page()該函數功能為:設置緩沖區首部的一些字段,主要是b_page字段(指向頁描述符)和b_data字段。 init_buffer()功能為:設置b_end_io字段和b_private字段。
(5)link_dev_buffers():該函數主要是把緩沖區頭部連成一個循環鏈表,并在page中的private字段存放第一個緩沖區首部的地址。同時把PG_private字段置位該功能主要是由函數attach_page_buffers()完成的。
(6)init_page_buffers()該函數的功能是初始化緩沖區首部的其他字段的值。
整個把緩沖區頁加入到頁高速緩存的過程需要做的工作如下:
(1)首先在頁高速緩存中尋找相應的頁是否存在,如果不存在就建立相應的緩沖區頁。
(2)建立緩沖區頁之后就為該緩沖區頁分配緩沖區首部,同時設置緩沖區首部的各個字段的值。
(3)把緩沖區首部通過緩沖區首部的b_this_page字段把該頁下的所有的緩沖區首部加入到一個單向的循環鏈表中。
(4)把該緩沖區頁所有的緩沖區首部的b_page字段都指向緩沖區頁的頁描述符page,同時page的private字段指向第一個緩沖區首部的地址。
以上基本上就是把緩沖區頁加入到頁高速緩沖的所有工作。具體的見下面的代碼:
點擊(此處)折疊或打開
1. static int
- Check for a block which wants to lie outside our maximum possible
- pagecache index. (this comparison is done using sector_t types).
點擊(此處)折疊或打開
static struct page *
grow_dev_page(struct block_device *bdev, sector_t block,
- Allocate some buffers for this page
- Link the page to the buffers and initialise them. Take the
- lock to be atomic wrt __find_get_block(), which does not
- run under the page lock.
該函數依次執行以下列子步驟:
a. 調用函數find_or_create_page(),傳遞給它的參數有:塊設備的address_space對象(bdev->bd_inode->i mapping)、頁偏移index以及GFP_NOFS標志。正如在前面“頁高速緩存的處理函數”博文所描述的,find_or_create_page()在頁高速緩存中(基樹中)搜索需要的頁,如果需要,就把新的頁插入高速緩存。如果在頁高速緩存中沒有找到相應的頁那么接下來就會分配相應的頁。
b. 此時,所請求的頁已經在頁高速緩存中,而且函數獲得了它的描述符地址。函數檢查它的PG_private標志;如果為空,說明頁還不是一個緩沖區頁(沒有相關的緩沖區首部),就跳到第e步。
c. 頁已經是緩沖區頁。從頁描述符的private字段獲得第一個緩沖區首部的地址bh,并檢查塊大小bh->size是否等于所請求的塊大小;如果大小相等,在頁高速緩存中找到的頁就是有效的緩沖區頁,因此跳到第g步。
d. 如果頁中塊的大小有錯誤,就調用try_to_free_buffers()釋放緩沖區頁的上一個緩沖區首部,并報錯(goto failed)。
e. 調用函數alloc_page_buffers()根據頁中所請求的塊大小分配緩沖區首部,并把它們插入由b_this_page字段實現的單向循環鏈表(注意那個while循環):
struct buffer_head *alloc_page_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) {
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_private = NULL;
bh->b_size = size;
/* Link the buffer to its page */
set_bh_page(bh, page, offset);
init_buffer(bh, NULL, NULL);
}
return head;
no_grow:
……
}
void set_bh_page(struct buffer_head *bh,
struct page *page, unsigned long offset)
{
bh->b_page = page;
BUG_ON(offset >= PAGE_SIZE);
if (PageHighMem(page))
/*
* This catches illegal uses and preserves the offset:
*/
bh->b_data = (char *)(0 + offset);
else
bh->b_data = page_address(page) + offset;
}
inline void
init_buffer(struct buffer_head *bh, bh_end_io_t *handler, void *private)
{
bh->b_end_io = handler;
bh->b_private = private;
}
此外,函數alloc_page_buffers調用set_bh_page用頁描述符的地址初始化緩沖區首部的b_page字段,用塊緩沖區在頁內的線性地址或偏移量初始化b_data字段。
回到grow_dev_page:
f. 調用link_dev_buffers把頁的緩沖區頭連成一個循環鏈表,在page結構的字段private中存放第一個緩沖區首部的地址,把PG_private字段置位,并遞增頁的使用計數器(頁中的塊緩沖區被算作一個頁用戶):
static inline void
link_dev_buffers(struct page *page, struct buffer_head *head)
{
struct buffer_head *bh, *tail;
bh = head;
do {
tail = bh;
bh = bh->b_this_page;
} while (bh);
tail->b_this_page = head;
attach_page_buffers(page, head);
}
static inline void attach_page_buffers(struct page *page,
struct buffer_head *head)
{
page_cache_get(page); /* 并遞增頁的使用計數器 */
SetPagePrivate(page);
set_page_private(page, (unsigned long)head);
}
define SetPagePrivate(page) set_bit(PG_private, &(page)->flags)
define set_page_private(page, v) ((page)->private = (v))
g. 調用init_page_buffers()函數初始化連接到頁的緩沖區首部的字段b_bdev、b_blocknr和b_bstate。因為所有的塊在磁盤上都是相鄰的,因此邏輯塊號是連續的,而且很容易從塊得出:
static void
init_page_buffers(struct page *page, struct block_device *bdev,
sector_t block, int size)
{
struct buffer_head *head = page_buffers(page);
struct buffer_head *bh = head;
int uptodate = PageUptodate(page);
do {
if (!buffer_mapped(bh)) {
init_buffer(bh, NULL, NULL);
bh->b_bdev = bdev;
bh->b_blocknr = block;
if (uptodate)
set_buffer_uptodate(bh);
set_buffer_mapped(bh);
}
block++;
bh = bh->b_this_page;
} while (bh != head);
}
h. 返回頁描述符地址。
五、釋放緩沖區頁(主要的工作就是釋放相應的緩沖區首部)
當內核試圖獲得更多的空閑內存時,就釋放塊設備緩沖區頁。顯然,不可能釋放有臟緩沖區或上鎖的緩沖區的頁。內核調用函數try_to_release_page()釋放緩沖區頁,該函數接收頁描述符的地址page,并執行下述步驟(還可以對普通文件所擁有的緩沖區頁調用try_to_release_page函數):
函數try_to_free_buffers()依次掃描鏈接到緩沖區頁的緩沖區首部,它本質上執行下列操作:
六、在頁高速緩存中搜索塊
當內核需要讀或寫一個單獨的物理設備塊時(例如一個超級塊),必須檢查所請求的塊緩沖區是否已經在頁高速緩存中。在頁高速緩存中搜索指定的塊緩沖區(由塊設備描述符的地址bdev和邏輯塊號nr表示)的過程分成三個步驟:
1.(首先獲取該塊對應的address_space對象,通過它可以找到塊對應的基樹,然后在基樹上就可以通過index找到相應的頁) 獲取一個指針,讓它指向包含指定塊的塊設備的address_space對象(bdev->bd_inode->i_mapping)。
不過,實現的細節要更為復雜。為了提高系統性能,內核維持一個小磁盤高速緩存數組bh_lrus(每個CPU對應一個數組元素),即所謂的最近最少使用(LRU)塊高速緩存。每個磁盤高速緩存有8個指針,指向被指定CPU最近訪問過的緩沖區首部。對每個CPU數組的元素排序,使指向最后被使用過的那個緩沖區首部的指針索引為0。相同的緩沖區首部可能出現在幾個CPU數組中(但是同一個CPU數組中不會有相同的緩沖區首部)。在LRU塊高速緩存中每出現一次緩沖區首部,該緩沖區首部的使用計數器b_count就加1。
這個過程涉及的函數如下:
1、__find_get_block()函數:該函數返回頁高速緩存中的塊緩沖區對應的緩沖區首部的地址;如果不存在指定的塊,就返回NULL。該函數的詳細代碼如下:
點擊(此處)折疊或打開
1. struct buffer_head *
該函數主要執行的功能如下:
點擊(此處)折疊或打開
static struct buffer_head *
lookup_bh_lru(struct block_device *bdev, sector_t block, int size)
點擊(此處)折疊或打開
static struct buffer_head *
__find_get_block_slow(struct block_device *bdev, sector_t block)
- not mapped. This is due to various races between
- file io on the block device and getblk. It gets dealt with
- elsewhere, don’t buffer_error if we had some unmapped buffers
__find_get_block_slow首先根據塊號和塊大小得到與塊設備相關的頁的索引:
index = block >> (PAGE_SHIFT - bdev->bd_inode->i_blkbits)
(2)、__getblk()函數:
古老的函數__getblk()現在的重要性也跟當年一樣重要,即如果查找不到就分配一個緩沖區頭。__getblk()其與__find_get_block()接收相同的參數,也就是block_device描述符的地址bdev、塊號block和塊大小size,并返回與緩沖區對應的緩沖區首部的地址。即使塊根本不存在,該函數也不會失敗,__getblk()會友好地分配塊設備緩沖區頁并返回將要描述塊的緩沖區首部的指針。注意,__getblk()返回的塊緩沖區不必存有有效數據——緩沖區首部的BH_Uptodate標志可能被清0。
1. 調用__find_get_block()檢查塊是否已經在頁高速緩存中。如果找到塊,則函數返回其緩沖區首部的地址。
2. 否則,調用__getblk_slow,觸發grow_buffers()為所請求的頁分配一個新的緩沖區頁。
點擊(此處)折疊或打開
1. static struct buffer_head *
(3)__bread()函數:
函數__bread()接收與__getblk()相同的參數,即block_device描述符的地址bdev、塊號block和塊大小size,并返回與緩沖區對應的緩沖區首部的地址。與__getblk()相反的是,如果需要的話,在返回緩沖區首部之前函數__bread()從磁盤讀塊,將分配到的一個空的buffer_head填滿:
點擊(此處)折疊或打開
1. struct buffer_head *
函數__bread()執行下述步驟:
總結:__find_get_block()函數、__getblk()函數和__bread()函數之間的區別:
(1)__find_get_block()函數:只是在頁高速緩存中查找塊緩沖區對應的緩沖區首部的地址,如果查找到則返回該緩沖區首部的地址,如果沒有找到則返回NULL。
(2)__getblk()函數:功能和上面的函數是一樣的在頁高速緩存中查找塊緩沖區對應的緩沖區首部的地址,如果查找到則返回該緩沖區首部的地址。但是在沒有找到的情況下該函數會分配該塊的緩沖區頁,并返回分配的緩沖區頁的地址。
(3)__bread()函數完成的功能是在__getblk()函數的基礎上,在返回緩沖區首部的地址之前從磁盤中讀取相應的塊到頁高速緩沖中。
上面的三個函數的功能是一步一步的加強的。
七、總結
(1)緩沖區:磁盤塊在物理內存中的表示形式。
(2)緩沖區描述符:對緩沖區的相關信息的描述,描述了緩沖區與磁盤塊的映射關系。
(3)bio(塊I/O):真正的磁盤塊操作用bio來表示,無論是經過頁面高速緩存的I/O還是直接I/O,都是用bio來操作數據塊的。
文章來源http://www.cnblogs.com/children/p/3420430.html
總結
以上是生活随笔為你收集整理的linux中的块缓冲的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 等精度频率计
- 下一篇: linux 其他常用命令