Buffer cache簡介
1、結構:Buffer Cache是SGA的一部分,Oracle利用Buffer Cache來管理data block,Buffer Cache的最終目的就是儘可能的減少磁碟I/O。Buffer Cache中主要有3大結構用來管理Buffer Cache。
Hash Bucket & Hash Chain List:Hash Bucket與Hash Chain List用來實現data block的快速定位。
LRU List:掛載有指向具體的free buffer, pinned buffer以及還沒有被移動到write list的dirty buffer等信息。所謂的free buffer就是指沒有包含任何數據的buffer,所謂的pinned buffer,就是指當前正在被訪問的buffer。
Write(Dirty)List:掛載有指向具體的dirty block的信息。所謂的dirty block,就是指在buffer cache中被修改過但是還沒有被寫入到磁碟的block。
2、功能:
1)通過快取數據塊,從而加快對磁碟上數據的訪問,減少I/O。
2)通過構造CR塊,從而提供讀一致性功能。
3)通過提供各種lock、latch機制,從而提供多個進程並發訪問同一個數據塊的功能。
3、大小:buffer cache的內容對應磁碟上一個塊(block),塊通常為1K,都是連續的。在linux下,為了更有效的使用物理記憶體,作業系統自動使用所有空閒記憶體作為Buffer Cache使用。當程式需要更多記憶體時,作業系統會自動減小Cache的大小。
在linux下,可通過命令cat /proc/meminfo和free-m查看buffer cache的記憶體使用情況.
Buffer cache的功能詳解
在從外存的一頁到記憶體的一頁的映射過程中,page cache與buffer cache、swap cache共同實現了高速快取功能,以下是其簡單映射圖:
外存的一頁(分解為幾塊,可能不連續)——物理磁碟的磁碟塊——記憶體的buffer Cache——記憶體的一頁(由一個頁框劃分的幾個連續buffer cache構成)——頁高緩系統
在這個過程中,物理檔案系統與Buffer Cache互動,負責在外圍存儲設備和Buffer Cache 之間交換數據。
由於bcache位於物理檔案系統和塊設備驅動程式之間,因此,當物理檔案系統需要從塊設備上讀取數據時,它首先試圖從bcache中去讀。如果命中,則核心就不必在去訪問慢速的塊設備。否則如果命中失敗,也即數據不在bcache中,則核心從塊設備上讀取相應的數據塊,並將其在bcache中快取起來,以備下次訪問之用。
類似地,但物理檔案系統需要向塊設備上寫數據時,也是先將數據寫到相應的緩衝區中,並將這個緩衝區標記為髒(dirty),然後在將來的某些時候將buffer cache中的數據真正地回寫到塊設備上,或者將該緩衝區直接丟棄。從而實現減少磁碟寫操作的頻率。
Buffer Cache的數據結構
緩衝區頭部對象buffer_head
每一個緩衝區都有一個緩衝區頭部來唯一地標識與描述該緩衝區。Linux通過數據結構buffer_head來定義緩衝區頭部。如下所示(include/linux/fs.h)
struct buffer_head {
/* First cache line: */
struct buffer_head *b_next; /* Hash queue list */
unsigned long b_blocknr; /* block number */
unsigned short b_size; /* block size */
unsigned short b_list; /* List that this buffer appears */
kdev_t b_dev; /* device (B_FREE = free) */
atomic_t b_count; /* users using this block */
kdev_t b_rdev; /* Real device */
unsigned long b_state; /* buffer state bitmap (see above) */
unsigned long b_flushtime; /* Time when (dirty) buffer should be written */
struct buffer_head *b_next_free;/* lru/free list linkage */
struct buffer_head *b_prev_free;/* doubly linked list of buffers */
struct buffer_head *b_this_page;/* circular list of buffers in one page */
struct buffer_head *b_reqnext; /* request queue */
struct buffer_head **b_pprev; /* doubly linked list of hash-queue */
char * b_data; /* pointer to data block (512 byte) */
struct page *b_page; /* the page this bh is mapped to */
void (*b_end_io)(struct buffer_head *bh, int uptodate); /* I/O completion */
void *b_private; /* reserved for b_end_io */
unsigned long b_rsector; /* Real buffer location on disk */
wait_queue_head_t b_wait;
struct inode * b_inode;
struct list_head b_inode_buffers; /* doubly linked list of inode dirty buffers */
};
各欄位的含義如下:
1b_next指針:指向哈希鍊表中的下一個buffer_head對象。
2.b_blocknr:本緩衝區對應的塊號(block number)。
3.b_size:以位元組計掉的塊長度。合法值為:512、1024、2048、4096、8192、16384和32768。
4.b_list:記錄這個緩衝區應該出現在哪個鍊表上。
5.d_dev:緩衝區對應的塊所在的塊設備標識符(對於位於free_list鍊表中的緩衝區,b_dev=B_FREE)。
6.b_count:本緩衝區的引用計數。
7.b_rdev:緩衝區對應的塊所在的塊設備的「真實」標識符。
8.b_state:緩衝區的狀態,共有6種:
/* bh state bits */
#define BH_Uptodate 0 /* 1 if the buffer contains valid data */
#define BH_Dirty 1 /* 1 if the buffer is dirty */
#define BH_Lock 2 /* 1 if the buffer is locked */
#define BH_Req 3 /* 0 if the buffer has been invalidated */
#define BH_Mapped 4 /* 1 if the buffer has a disk mapping */
#define BH_New 5 /* 1 if the buffer is new and not yet written out */
#define BH_Protected 6 /* 1 if the buffer is protected */
9.b_flushtime:髒緩衝區必須被回寫到磁碟的最後期限值。
10.b_next_free指針:指向lru/free/unused鍊表中的下一個緩衝區頭部對象。
11b_prev_free指針:指向lru/free/unused鍊表中的前一個緩衝區頭部對象。
12b_this_page指針:指向同屬一個物理頁幀的下一個緩衝區的相應緩衝區頭部對象。同屬一個物理頁幀的所有緩衝區通過這個指針成員連結成一個單向循環鍊表。
13b_reqnext指針:用於塊設備驅動程式的請求鍊表。
14b_pprev:哈希鍊表的後向指針。
15b_data指針:指向緩衝區數據塊的指針。
16b_page指針:指向緩衝區所在物理頁幀的page結構。
17b_rsector:實際設備中原始扇區的個數。
18b_wait:等待這個緩衝區的等待佇列。
19b_inode指針:如果緩衝區屬於某個索引節點,則這個指針指向所屬的inode對象。
20b_inode_buffers指針:如果緩衝區為髒,且又屬於某個索引節點,那么就通過這個指針鏈入inode的i_dirty_buffers鍊表中。
緩衝區頭部對象buffer_head可以被看作是緩衝區的描述符,因此,對bcache中的緩衝區的管理就集中在如何高效地組織處於各種狀態下的buffer_head對象上。
buffer_head對象的SLAB分配器快取
緩衝區頭部對象buffer_head本身有一個叫做bh__cachep的slab分配器快取。因此對buffer_head對象的分配與銷毀都要通過kmem_cache_alloc()函式和kmem_cache_free()函式來進行。
注意不要把bh_cachep SLAB分配器快取和緩衝區本身相混淆。前者只是buffer_head對象所使用的記憶體高速快取,並不與塊設備打交道,而僅僅是一種有效管理buffer_head對象所占用記憶體的方式。後者則是塊設備中的數據塊所使用的記憶體高速快取。但是這二者又是相互關聯的,也即緩衝區快取的實現是以bh_cachep SLAB分配器快取為基礎的。而我們這裡所說的bcache機制包括緩衝區頭部和緩衝區本身這兩個方面的概念。
bh_cachep定義在fs/dcache.c檔案中,並在函式vfs_caches_init()中被初始化,也即通過調用kmem_cache_create()函式來創建bh_cachep這個SLAB分配器快取。
註:函式vfs_caches_init()的工作就是調用kmem_cache_create()函式來為VFS創建各種SLAB分配器快取,包括:names_cachep、filp_cachep、dquot_cachep和bh_cachep等四個SLAB分配器快取。
bcache中的緩衝區頭部對象鍊表
一個緩衝區頭部對象buffer_head總是處於以下四種狀態之一:
1未使用(unused)狀態:該對象是可用的,但是其b_data指針為NULL,也即這個緩衝區頭部沒有和一個緩衝區相關聯。
2空閒(free)狀態:其b_data指針指向一個空閒狀態下的緩衝區(也即該緩衝區沒有具體對應塊設備中哪個數據塊);而b_dev域值為B_FREE(值為0xffff)。
3正在使用(inuse)狀態:其b_data指針指向一個有效的、正在使用中的緩衝區,而b_dev域則指明了相應的塊設備標識符,b_blocknr域則指明了緩衝區所對應的塊號。
4異步(async)狀態:其b_data域指向一個用來實現page I/O操作的臨時緩衝區。4、bcache機制採用了各種鍊表來組織這些對象 為了有效地管理處於上述這些不同狀態下的緩衝區頭部對象,bcache機制採用了各種鍊表來組織這些對象(這一點,bcache機制與VFS的其它cache機制是相同的):
1.哈希鍊表:所有buffer_head對象都通過其b_next與b_pprev兩個指針域鏈入哈希鍊表中,從而可以加快對buffer_head對象的查找(lookup)。
2.近期最少使用鍊表lru_list:每個處在inuse狀態下的buffer_head對象都通過b_next_free和b_prev_free這兩個指針鏈入某一個lru_list鍊表中。
3.空閒鍊表free_list:每一個處於free狀態下的buffer_head對象都根據它所關聯的空閒緩衝區的大小鏈入某個free_list鍊表中(也是通過b_next_free和b_prev_free這兩個指針)。
4.未使用鍊表unused_list:所有處於unused狀態下的buffer_head對象都通過指針域b_next_free和b_prev_free鏈入unused_list鍊表中。
5.inode對象的髒緩衝區鍊表i_dirty_buffers:如果一個髒緩衝區有相關聯的inode對象的話,那么他就通過其b_inode_buffers指針域鏈入其所屬的inode對象的i_dirty_buffers鍊表中。
(更詳細的介紹請見參考資料二)
buffer cache的內部管理機制
在buffer cache中獲取所需要的數據塊的過程
當前台進程發出SELECT或者其他DML語句時,oracle根據SQL語句的執行計畫所找到的數據塊(根據查詢索引或者全表掃描,找到要查詢的數據條目所在的塊,一般根據ROWID等信息,舉個例子:一個簡單的select語句,條件是empno=7788,根據索引,oracle會知道7788這個條目的ROWID,然後就能知道數據塊的位置,到底是在那個表空間,那個對象,那個數據檔案中),會構造一個名為數據塊描述(buffer descriptor)的記憶體結構。該buffer descriptor位於session的PGA中,所包含的內容主要是數據塊所在的物理地址(根據ROWID信息的第33-64bit構造出rbda)、數據塊的類型、數據塊所屬對象的object id等信息。
隨後,oracle會把對數據塊請求的鎖定模式以及所構造出來的buffer descriptor傳入專門搜尋數據塊的函式中。在該函式中,oracle根據buffer descriptor所記錄的信息,套用hash算法以後,得到要找的數據塊所處的hash bucket,也就是確定該數據塊在哪條hash chain上。然後,oracle進入該hash chain,從上面所掛的第一個buffer header開始搜尋,一直搜尋到最後一個buffer header。
在hash chain上搜尋的邏輯如下:
1) 比較buffer header上所記錄的數據塊的地址(rdba),如果不符合,則跳過該buffer header。
2) 跳過狀態為CR的buffer header。(說明有別的進程正在進行一致性讀,所以才構造了這個cr塊,如果我也要找這個塊的原塊,我需要自己再重新構造一個新的cr塊,不會使用這箇舊的cr塊,如果我不是找這個塊的原塊,那我不需要構造,所以這兩種情況下都是跳過cr塊)
3) 如果遇到狀態為READING(正在從磁碟上讀出的數據塊)的buffer header,則等待,一直等到該buffer header的狀態改變以後再比較所記錄的數據塊的地址是否符合。
4) 如果發現數據塊地址符合的buffer header,則查看該buffer header是否位於正在使用的列表上,如果是位於正在使用的列表上,則判斷已存在的鎖定模式與當前所要求的鎖定模式是否兼容,如果是兼容的,則返回該buffer header所記錄的數據塊地址。
5) 如果發現鎖定模式不兼容,則根據找到的buffer header所指向的數據塊的內容,構建一個新的、內容一樣的、數據塊狀態為XCURRENT(實例以排他方式獲取的當前模式數據塊)的複製數據塊,並且構造一個狀態為CR的buffer header,同時該buffer header指向所新建立的複製數據塊。然後,返回該複製數據塊的地址,並將當前進程號放入該buffer header所處的正在使用的列表上。
6) 如果比較完整個hash chain以後還沒發現所要找的buffer header,則從磁碟上讀取數據檔案。並將讀取到的數據塊所對應的buffer header掛到hash chain上。
LRU和LRUW鍊表結構及其管理機制
1、 LRU和LRUW鍊表結構概述
如果在hash chain上沒有找到所要的buffer header時,oracle會發出I/O調用,到磁碟上的數據檔案中獲取數據塊,並將該數據塊的內容拷貝一份到buffer cache中的記憶體數據塊里。這個時候,如果buffer cache中的記憶體數據塊全都被占用,為了高效的管理buffer cache中的記憶體數據塊,oracle引入了LRU和LRUW等鍊表等結構。
在前面描述buffer cache結構的圖上,也可以看到有兩個鍊表:LRU和LRUW。在介紹LRU和LRUW前,先說明幾個概念。
1) 髒數據塊(dirty buffer):buffer cache中的記憶體數據塊的內容與數據檔案中的數據塊的內容不一致。
2) 可用數據塊(free buffer):buffer cache中的記憶體數據塊為空或者其內容與數據檔案中的一致。注意,可用數據塊不一定是空的。
3) 釘住的數據塊(ping buffer):當前正在更新的記憶體數據塊。
4) 資料庫寫進程(DBWR):這是一個很底層的資料庫後台進程。既然是後台進程,就表示該進程是不能被用戶調用的。由oracle內置的一些事件根據需要啟動該進程,該進程用來將髒數據塊寫入磁碟上的數據檔案。
LRU表示Least Recently Used,也就是指最少使用的buffer header鍊表。LRU鍊表串連起來的buffer header都指向可用數據塊。而LRUW則表示Least Recently Used Write,也叫做dirty list,也就是髒數據塊鍊表,LRUW串起來的都是修改過但是還沒有寫入數據檔案的記憶體數據塊所對應的buffer header。一個buffer header或者掛在LRU上,或者掛在LRUW上,不能同時掛在這兩個鍊表上。
隨著硬體技術的發展,電腦的記憶體越來越大。buffer cache也是越來越大,只用一條LRU和一條LRUW來管理buffer header已經不夠用了。同時oracle還引入了多個DBWR後台進程來幫助將buffer cache中的髒數據塊寫入數據檔案,顯然,多個DBWR後台進程都去掃描相同的LRUW鍊表會引起爭用。為此oracle引入了working set(工作集)的概念。每個working set都具有它自己的一組LRU和LRUW鍊表。每個working set都由一個名為“cache buffers lru chain”的latch(也叫做lru latch)來管理,所以從這個意義上說,每一個lru latch就是一個working set。而每個被載入到buffer cache的buffer header都以輪詢的方式掛到working set上去。也就是說,當buffer cache載入一個新的數據塊時,其對應的buffer header會去找一個可用的lru latch(找這個工作集中的lru列表,將新載入進來的數據塊掛到LRU列表上),如果沒有找到,則再找下一個lru latch,直到找到為止。如果輪詢完所有的lru latch也沒能找到可用的lru latch,該進程只有等待latch free等待事件,同時出現在v$session_wait中,並增加“latch misses”。如果啟用了多個DBWR後台進程的話,每個DBWR進程都會對應一個不同的working set,而且每個DBWR只會處理分配給它的working set,不會處理其他的working set。即working set的數量也就是lru latch的數量。而lru latch的數量是由一個隱藏參數:db_block_lru_latches決定的。該參數預設值為DBWR進程的數量×8。
該參數最小必須為8,如果強行設定比8小的數值,oracle將忽略設定的值,而使用8作為該參數值。
1SQL> alter system set "_db_block_lru_latches"=1 scope=spfile;
2SQL> startup force
3SQL> show parameter _db_block
4NAME TYPE VALUE
5------------------------------------ ----------- ------------------------------
6_db_block_lru_latches integer 8
2、 深入LRU鍊表
在8i之前,我們舉一個例子。假設buffer cache只能容納4個數據塊,同時只有一個hash chain和一個LRU。當資料庫剛剛啟動,buffer cache是空的。這時前台進程發出SELECT語句獲取數據塊時,oracle找一個空的記憶體數據塊,並將其對應的buffer header掛到hash chain上。同時,oracle還會把該buffer header掛到LRU的最尾端。隨後前台進程又發出SELECT語句,這時所找到的buffer header在LRU上會掛到前一個buffer header的後面,也就是說第二次SELECT語句所找到的buffer header現在變成了LRU的最尾端了。假設發出4句SELECT以後找到了4個buffer header,從而用完了所有的buffer cache空間。
這個時候,發來第五句SELECT語句。而buffer cache里已經沒有空的記憶體數據塊。但是既然需要容納下第五個數據塊,就必然需要找一個可以被替換的記憶體數據塊。這個記憶體數據塊會到LRU上去找。按照oracle設定的近期最少使用的原則,位於LRU最尾端的BH1將成為犧牲者,oracle會把該BH1對應的記憶體數據塊的內容清空,並將當前第五句SQL所獲得的數據塊的內容拷貝進去。這個時候,BH1就成了LRU的首端,而BH2則成為了LRU的尾端。在這種方式下,經常被訪問的數據塊可以一直靠近LRU的首端,也就保證了這些數據塊可以儘可能的不被替換掉,從而保證了訪問的效率。
到了8i以後,oracle引入了一種更加複雜的機制來管理LRU上的數據塊。8i以後,LRU和LRUW鍊表都具有兩個子鍊表,分別叫做輔助鍊表和主鍊表。同時還對buffer header增加了一個屬性:touch數量,也就是每個buffer header曾經被訪問過的次數,來對LRU鍊表進行管理。oracle每訪問一次buffer header,就會將該buffer header上的touch數量增加1,因此,touch數量“近似”的體現了某個記憶體數據塊總共被訪問的次數。注意,這只是近似,並不精確。因為touch的增加並沒有使用latch來管理並發性。這只是一個大概值,表示趨勢的,不用百分百的精確。
讀入第一個數據塊時,該數據塊對應的buffer header會掛到LRU輔助鍊表(注意,這裡是輔助鍊表,而不是主鍊表)的最末端,同時touch數量為1。讀取第二個不同的數據塊時,該數據塊對應的buffer header會掛到前一個buffer header的後面,從而位於LRU輔助鍊表的最末端,同樣touch為1。假設4個數據塊全都用完以後的LRU鍊表可以用下圖四描述。每個buffer header的touch數量都為1。此時輔助LRU鍊表都掛滿了,而主LRU鍊表還是空的。前台發出第五句SQL語句,要求返回指定的數據塊。這時,oracle發現buffer cache里已經沒有空的記憶體數據塊了,於是從輔助LRU鍊表的尾部開始掃描,也就是從BH1開始掃描,以查找可以被替代的數據塊。掃描的過程中按照下面的邏輯來選擇被犧牲的(也就是可以被替代的)數據塊:
1) 如果被掃描到的buffer header的touch數量小於隱藏參數_db_aging_hot_criteria(該參數預設為2)的值,則選中該buffer header作為犧牲者,並立即返回該buffer header所含有的數據塊的地址。
2) 如果當前buffer header的touch數量大於_db_aging_hot_criteria的值,則不會使用該buffer header。但是如果當前的_db_aging_stay_count的值小於_db_aging_hot_criteri的值,則會將當前該buffer header的touch值賦值給_db_aging_stay_count;否則將當前buffer header的touch數量減掉一半。
按照上述的邏輯,這時將選出BH1作為犧牲者(因為BH1的touch數量為1,小於_db_aging_hot_criteria的值),並將其對應的記憶體數據塊的內容清空,同時將當前第五個數據塊的內容拷貝進去。但是這裡要注意,這個時候該BH1在LRU鍊表上的位置並不會發生任何的變化(這裡是插入了新的數據塊的內容,所以touc的數量沒有變化,下面是返回已經有的數據塊,所以touch的數量加1了,這樣就保證了touch為1的數據塊即不常用的數據塊一直在輔助鍊表,而不會跑到主lru鍊表上)。而不會像8i之前的那樣,BH1變成LRU鍊表的首端。
接下來,前台發來了第六句和第七句SQL,分別要返回與第五句和第四句SQL一樣的數據塊,也就是要返回當前的BH1和BH4。這個時候,oracle會增加BH1和BH4的touch數量,同時將該BH1和BH4從輔助LRU鍊表上摘下,轉移到主LRU鍊表的中間位置。
如果發來了第八句SQL,要求返回與第三句SQL相同的數據塊,也就是當前的BH3,則這時該BH3會插入主LRU鍊表上的BH1和BH4中間,注意每次向主LRU列表插入buffer header時都是向中間位置插入。如果發來了第九句SQL要求返回BH2,則我們可以知道,BH2會轉移到主LRU鍊表的中間。這個時候,輔助LRU鍊表就空了,沒有buffer header了。
如果又發來第十句SQL,要求返回一個新的、buffer cache中不存在所需內容的數據塊時。oracle會先掃描輔助LRU鍊表,發現上面沒有任何的buffer header時,則必須掃描主LRU鍊表。從尾部開始掃描,採用前面說到的與掃描輔助LRU鍊表相同的規則挑選犧牲者。挑出的可以被替代的buffer header將從主LRU鍊表上摘下,放入輔助LRU鍊表。
從上面所描述的buffer header在輔助LRU鍊表和主LRU鍊表之間交替的過程中,我們可以看出,oracle改進LRU鍊表的管理方式的目的,就是想千方百計的能夠將多次被訪問的數據塊保留在記憶體里,同時又要平衡有限的記憶體資源。這種方式相比較8i之前而言,無疑是進步很多的。在8i之前中,某個數據塊可能只會被訪問一次,但是就這么一次的訪問就將該數據塊放到了LRU的首端,從而可能就擠掉了一個LRU上不是那么經常被訪問,但是也會多次訪問的數據塊。而8i以後,將訪問一次的數據塊和訪問一次以上的數據塊徹底分開,而且查找可用數據塊時,始終都是從輔助LRU鍊表開始掃描。實際上也就使得越傾向於只訪問一次的數據塊越快的從記憶體中清理出去。
3、LRUW鍊表管理
LRUW表示髒數據塊鍊表,該鍊表上的buffer header指向的都是已經從LRU鍊表上摘下來、其對應的記憶體數據塊里的內容已經被修改、但是還沒有被寫入數據檔案的記憶體數據塊。在這些髒數據塊在能夠被重用之前,它們必須要被DBWR寫入磁碟。從8i以後,LRUW鍊表同樣包含兩個子鍊表:輔助LRUW鍊表和主LRUW鍊表。
假設前台用戶發出DML語句,要求修改BH2所指向的記憶體數據塊。這時,按順序發生下面的動作:
1)oracle會將BH2從輔助LRU鍊表上摘下,同時插入主LRU鍊表的中間,也就是插入BH1和BH4中間,同時增加BH2的touch的數量。(與selectBH2的效果一樣, 都會使要查找的塊從輔助鍊表上摘下,放入主LRU鍊表)
2)將該BH2的標記設定為釘住(ping)。
3)更新BH2對應的記憶體數據塊的內容。
4)更新完以後,取消釘住的標記(在主LRU列表上進行第一次更新)。
5)將BH2從主LRU鍊表轉移到主LRUW鍊表上。
6)如果這個時候又有進程發出更新BH2所對應的記憶體數據塊的內容,則BH2再次被釘住,更新,取消釘住(可以在主LRUW列表上繼續更新)。
7)DBWR啟動以後,在掃描主LRUW鍊表時會將BH2轉移到輔助LRUW鍊表上(必須轉移到輔助LURW列表才能寫入到磁碟)。
8)DBWR將輔助LRUW鍊表上的BH2對應的數據塊寫入數據檔案。
9)確認成功寫入數據檔案以後,將BH2從輔助LRUW鍊表上轉移到輔助LRU鍊表上(返回到輔助LUR列表)。
可以看到,主LRUW鍊表上包含的buffer header要么是已經更新完了的數據塊,要么是被釘住正在更新的數據塊。而當DBWR進程啟動以後,它會掃描主LRUW鍊表,並跳過正在被釘住更新的buffer header,而將已經更新完了的buffer header從主LRUW鍊表上摘除,並轉移到輔助LRUW鍊表上去。掃描完主LRUW鍊表,或掃描的buffer header的個數達到一定限度時,DBWR會轉到輔助LRUW上,將輔助LRUW上面的buffer header所對應的數據塊寫入數據檔案。所以說,對於輔助鍊表上的buffer header來說,要么是正在等待被寫入的;要么就是已經發出寫入請求,正在寫入而還沒寫完的。這裡要注意的是,buffer header進入LRUW鍊表,是從尾端進入;而DBWR掃描LRUW鍊表時,則是從首端開始。
這裡將主LRUW鍊表和輔助LRUW鍊表分開,主要就是為了提高DBWR在主LRUW鍊表上掃描的效率。如果只有主LRUW鍊表而沒有輔助LRUW鍊表的話,勢必造成三種類型buffer header交織在LRUW鍊表上:
1)正在被釘住更新的buffer header;
2)已經更新完,而正在等待被寫入數據檔案的buffer header;
3)已經發出寫請求,正在寫而尚未寫完的buffer header。
在這種情況下,必然造成DBWR為了找到第二種類型的buffer header而需要掃描不該掃描的第三種類型的buffer header。(把第三種已經發出寫請求,但是還沒有寫完的BH放到了輔助LRUW列表里,避免了掃描第二種已經更新完成,等待被寫入的的BH)
4、DBWR進程
DBWR進程負責將髒數據塊寫入磁碟。它是一個非常重要的進程,在後台進程中的sid為2,在PMON進程啟動以後隨即啟動。
SQL> select c.sid,a.name,a.description
2 from v$bgprocess a ,v$process b , v$session c
3 where a.paddr=b.addr
4 and b.addr = c.paddr;
SID NAME DESCRIPTION
---------- ----- -------------------------------------------
1 PMON process cleanup
2 DBW0 db writer process 0
3 LGWR Redo etc.
4 CKPT checkpoint
………………………………………………………………………………
隨著記憶體的不斷增加,1個DBWR進程可能不夠用了。所以從8i起,我們可以為系統配置多個DBWR進程。初始化參數:db_writer_processe決定了啟動多少個DBWR進程。每個DBWR進程都會分配一個lru latch,也就是說每個DBWR進程對應一個working set。因此oracle建議配置的DBWR進程的數量應該等於lru latch的數量,同時應該小於CPU的數量。系統啟動時,就確定好了working set與DBWR進程的對應關係,每個DBWR進程只會將分配給自己的working set上的髒數據塊寫入數據檔案。DBWR作為一個後台進程,只有在某些條件滿足了才會觸發。這些條件包括:
1) 當進程在輔助LRU鍊表和主LRU鍊表上掃描以查找可以覆蓋的buffer header時,如果已經掃描的buffer header的數量到達一定的限度(由隱藏參數:_db_block_max_scan_pct決定)時,觸發DBWR進程。_db_block_max_scan_pct表示已經掃描的buffer header的個數占整個LRU鍊表上buffer header總數的百分比。這時,搜尋可用buffer header的進程掛起,在v$session_wait中表現為等待“free buffer wait”事件,同時增加v$sysstat中的“dirty buffers inspected”的值。
2) 當DBWR在主LRUW鍊表上查找已經更新完而正在等待被寫入數據檔案的buffer header時,如果找到的buffer header的數量超過一定限度(由隱藏參數:_db_writer_scan_depth_pct決定)時,DBWR就不再繼續往下掃描了,而轉到輔助LRUW鍊表上將其上的髒數據塊寫入數據檔案。_db_writer_scan_depth_pct表示已經掃描的髒數據塊的個數占整個主LRUW鍊表上buffer header總數的百分比。
3) 如果主LRUW鍊表和輔助LRUW鍊表上的髒數據塊的總數超過一定限度,也將觸發DBWR進程。該限度由隱藏參數:_db_large_dirty_queue決定。
4) 發生增量檢查點(incremental checkpoint)或完全檢查點(complete checkpoint)時觸發DBWR。
5) 每隔三秒鐘啟動一次DBWR。
6) 將表空間設定為離線(offline)狀態時觸發DBWR。
7) 發出命令:alter tablespace … begin backup,從而將表空間設定為熱備份狀態時觸發DBWR。
8) 將表空間設定為唯讀狀態時,觸發DBWR。
9) 刪除對象時(比如刪除某個表)會觸發DBWR。
當DBWR要寫髒數據塊時,並不是說立即將所有的髒數據塊都同時寫入磁碟。為了儘量減少物理的I/O的次數,DBWR會將要寫的髒數據塊所對應的buffer header拷貝到一個名為批量寫(write batch)的結構中。每個working set所對應的DBWR進程都可以向該結構里拷貝buffer header。當write batch的buffer header的個數達到一定限額時,才會發生實際的I/O,從而將髒數據塊寫入磁碟。這個限額為硬體平台所能支持的同時並發的異步I/O的最大數量。8i之前是可以用隱藏參數(_db_block_write_batch)來控制這個限額的。但是8i以後,取消了該參數,而由oracle自己來計算。
5 DBWR、CKPT、LGWR進程之間的合作
將記憶體數據塊寫入數據檔案實在是一個相當複雜的過程,在這個過程中,首先要保證安全。所謂安全,就是在寫的過程中,一旦發生實例崩潰,要有一套完整的機制能夠保證用戶已經提交的數據不會丟失;其次,在保證安全的基礎上,要儘可能的提高效率。眾所周知,I/O操作是最昂貴的操作,所以應該儘可能的將髒數據塊收集到一定程度以後,再批量寫入磁碟中。
直觀上最簡單的解決方法就是,每當用戶提交的時候就將所改變的記憶體數據塊交給DBWR,由其寫入數據檔案。這樣的話,一定能夠保證提交的數據不會丟失。但是這種方式效率最為低下,在高並發環境中,一定會引起I/O方面的爭用。oracle當然不會採用這種沒有擴展性的方式。oracle引入了CKPT和LGWR這兩個後台進程,這兩個進程與DBWR進程互相合作,提供了既安全又高效的寫髒數據塊的解決方法。
用戶進程每次修改記憶體數據塊時,都會在日誌緩衝區(redo buffer)中構造一個相應的重做條目(redo entry),該重做條目描述了被修改的數據塊在修改之前和修改之後的值。而LGWR進程則負責將這些重做條目寫入在線上日誌檔案。只要重做條目進入了在線上日誌檔案,那么數據的安全就有保障了,否則這些數據都是有安全隱患的。LGWR 是一個必須和前台用戶進程通信的進程。LGWR 承擔了維護系統數據完整性的任務,它保證了數據在任何情況下都不會丟失。
LGWR將重做條目寫入在線上日誌檔案的情況分兩種:後台寫(background write)和同步寫(sync write)。觸發後台寫的條件有四個:
1)每隔三秒鐘,LGWR啟動一次;
2)在DBWR啟動時,如果發現髒數據塊所對應的重做條目還沒有寫入在線上日誌檔案,則DBWR觸發LGWR進程並等待LRWR寫完以後才會繼續;
3)重做條目的數量達到整個日誌緩衝區的1/3時,觸發LGWR;
4)重做條目的數量達到1MB時,觸發LGWR。
而觸發同步寫的條件就一個:當用戶提交(commit)時,觸發LGWR。
假如DBWR在寫髒數據塊的過程中,突然發生實例崩潰。我們已經知道,用戶提交時,oracle是不一定會把提交的數據塊寫入數據檔案的。那么實例崩潰時,必然會有一些已經提交但是還沒有被寫入數據檔案的記憶體數據塊丟失了。當實例再次啟動時,oracle需要利用日誌檔案中記錄的重做條目在buffer cache中重新構造出被丟失的數據塊,從而完成前滾和回滾的工作,並將丟失的數據塊找回來。於是這裡就存在一個問題,就是oracle在日誌檔案中找重做條目時,到底應該找哪些重做條目?換句話說,應該在日誌檔案中從哪個起點開始往後套用重做條目?注意,這裡所指的日誌檔案可能不止一個日誌檔案。
因為oracle需要隨時預防可能的實例崩潰現象,所以oracle在資料庫的正常運行過程中,會不斷的定位這個起點,以便在不可預期的實例崩潰中能夠最有效的保護並恢複數據。同時,這個起點的選擇非常有講究。首先,這個起點不能太靠前,太靠前意味著要處理很多的重做條目,這樣會導致實例再次啟動時所進行的恢復的時間太長;其次,這個起點也不能太靠後,太靠後說明只有很少的髒數據塊沒有被寫入數據檔案,也就是說前面已經有很多髒數據塊被寫入了數據檔案,那也就意味著只有在DBWR啟動的很頻繁的情況下,才能使得buffer cache中所殘留的髒數據塊的數量很少。但很明顯,DBWR啟動的越頻繁,那么所占用的寫數據檔案的I/O就越嚴重,那么留給其他操作(比如讀取buffer cache中不存在的數據塊等)的I/O資源就越少。這顯然也是不合理的。
從這裡也可以看出,這個起點實際上說明了,在日誌檔案中位於這個起點之前的重做條目所對應的在buffer cache中的髒數據塊已經被寫入了數據檔案,從而在實例崩潰以後的恢復中不需要去考慮。而這個起點以後的重做條目所對應的髒數據塊實際還沒有被寫入數據檔案,如果在實例崩潰以後的恢復中,需要從這個起點開始往後,依次取出日誌檔案中的重做條目進行恢復。考慮到的記憶體容量越來越大,buffer cache也越來越大,buffer cache中包含幾百萬個記憶體數據塊也是很正常的現象的前提下,為了能夠最佳的確定這個起點,oracle引入了名為CKPT的後台進程,通常也叫作檢查點進程(checkpoint process)。這個進程與DBWR共同合作,從而確定這個起點。同時,這個起點也有一個專門的名字,叫做檢查點位置(checkpoint position)。
oracle為了在檢查點的算法上更加的具有可擴展性(也就是為了能夠在巨大的buffer cache下依然有效工作),引入了檢查點佇列(checkpoint queue),該佇列上串起來的都是髒數據塊所對應的buffer header。
而DBWR每次寫髒數據塊時,也是從檢查點佇列上掃描髒數據塊,並將這些髒數據塊實際寫入數據檔案的。當寫完以後,DBWR會將這些已經寫入數據檔案的髒數據塊從檢查點佇列上摘下來。這樣即便是在巨大的buffer cache下工作,CKPT也能夠快速的確定哪些髒數據塊已經被寫入了數據檔案,而哪些還沒有寫入數據檔案,顯然,只要在檢查點佇列上的數據塊都是還沒有寫入數據檔案的髒數據塊。
而且,為了更加有效的處理單實例和多實例(RAC)環境下的表空間的檢查點處理,比如將表空間設定為離線狀態或者為熱備份狀態等,oracle還專門引入了檔案佇列(file queue)。檔案佇列的原理與檢查點佇列是一樣的,只不過每個數據檔案會有一個檔案佇列,該數據檔案所對應的髒數據塊會被串在同一個檔案佇列上;同時為了能夠儘量減少實例崩潰後恢復的時間,oracle還引入了增量檢查點(incremental checkpoint),從而增加了檢查點啟動的次數。如果每次檢查點啟動的間隔時間過長的話,再加上記憶體很大,可能會使得恢復的時間過長。因為前一次檢查點啟動以後,標識出了這個起點。然後在第二次檢查點啟動的過程中,DBWR可能已經將很多髒數據塊已經寫入了數據檔案,而假如在第二次檢查點啟動之前發生實例崩潰,導致在日誌檔案中,所標識的起點仍然是上一次檢查點啟動時所標識的,導致oracle不知道這個起點以後的很多重做條目所對應的髒數據塊實際上已經寫入了數據檔案,從而使得oracle在實例恢復時再次重複的處理一遍,效率低下,浪費時間。
buffer cache的回寫
有些是直接寫(write-through):數據將被立刻寫入磁碟,當然,數據也被放入快取中。如果寫操作是在以後做的,那么該快取被稱為後台寫(write-back)。後台寫比直接寫更有效,但也容易出錯:如果機器崩潰,或者突然掉電,緩衝中改變過的數據就被丟失了。如果仍未被寫入的數據含有重要的薄記信息,這甚至可能意味著檔案系統(如果有的話)已不完整。
針對以上的原因,出現了很多的日誌檔案系統,數據在緩衝區修改後,同時會被檔案系統記錄修改信息,這樣即使此時系統掉電,系統重啟後會首先從日誌記錄中恢複數據,保證數據不丟失。當然這些問題不在本文的敘述範圍。
由於上述原因,在使用適當的關閉過程之前,絕對不要關掉電源,sync命令可以清空(flushes)緩衝,也即,強迫所有未被寫的數據寫入磁碟,可用以確定所有的寫操作都已完成。在傳統的 UNIX系統中,有一個叫做update(kupdate)的程式運行於後台,每隔30秒做一次sync操作,因此通常無需手工使用sync命令了。Linux另外有一個後台程式,bdflush,這個程式執行更頻繁的但不是全面的同步操作,以避免有時sync的大量磁碟I/O操作所帶來的磁碟的突然凍結。
Buffer Cache和Page Cache及其它
概況
page不會同時存在於buffer cache和page cache。add_page_to_hash_queue將此思想顯露無餘。buffer_head 定義在fs.h,和檔案系統有著更為緊密的關係。從檔案讀寫角度看buffer cache快取檔案系統的管理信息像root entry, inode等,而page cache快取檔案的內容。
注意函式block_read_full_page,雖然位於buffer.c,但並沒有使用buffer cache. 但是確實使用了buffer:只是再指定page上創建buffer提交底層驅動讀取檔案內容.這個流程有兩個值得注意的地方:
注意的地方
一是普通file的read通過page cache進行
二是page cache讀取的時候不和buffer cache進行同步
三是page cache的確使用了buffer,不過注意,buffer 不是buffer cache。
2.4的改進:page cache和buffer cache耦合得更好了。在2.2里,磁碟檔案的讀使用page cache,而寫繞過page cache,直接使用buffer cache,因此帶來了同步的問題:寫完之後必須使用update_vm_cache()更新可能有的page cache。2.4中page cache做了比較大的改進,檔案可以通過page cache直接寫了,page cache優先使用high memory。而且,2.4引入了新的對象:file address space,它包含用來讀寫一整頁數據的方法。這些方法考慮到了inode的更新、page cache處理和臨時buffer的使用。page cache和buffer cache的同步問題就消除了。原來使用inode+offset查找page cache變成通過file address space+offset;原來struct page 中的inode成員被address_space類型的mapping成員取代。這個改進還使得匿名記憶體的共享成為可能(這個在2.2很難實現,許多討論過)。