缓冲技术之三:Linux下I/O操作buffer缓冲块使用流程
Linux文件系統(tǒng)中,存在著著名的三大緩沖技術(shù)用以提升讀寫操作效率: inode緩沖區(qū)、dentry緩沖區(qū)、塊緩沖。其中所謂的塊緩沖便是我們前面一直在討論的緩沖池技術(shù),常用來(lái)配備IO操作,用來(lái)減少IO讀取次數(shù),以提升系統(tǒng)效率。
本文便是此前《換成技術(shù)》系列的兩篇文章的基礎(chǔ)上繼續(xù)討論緩沖池技術(shù)。對(duì)于塊緩沖體系而言,需要提及的兩個(gè)概念分別是page cache和buffer cache,每個(gè)page cache包含若干buffer cache,即兩者的粒度不同,page是在buffer緩沖塊基礎(chǔ)上封裝出的更粗粒度的對(duì)象。
內(nèi)存管理系統(tǒng)和VFS(虛擬文件系統(tǒng))只與page cache這一粗粒度級(jí)別的緩沖對(duì)象進(jìn)行交互,內(nèi)存管理系統(tǒng)負(fù)責(zé)維護(hù)page cache的分配和回收(如按照LRU策略進(jìn)行淘汰)。在內(nèi)核需要讀寫數(shù)據(jù)時(shí),使用“內(nèi)存映射”等復(fù)雜機(jī)制進(jìn)可以和物理內(nèi)存塊進(jìn)行正確的映射。而具體文件系統(tǒng)一般只與buffer cache這一更小粒度級(jí)別的緩沖對(duì)象交互,它們負(fù)責(zé)在存儲(chǔ)設(shè)備和buffer cache之間交換數(shù)據(jù),具體的文件系統(tǒng)直接操作的是磁盤等disk部分,而VFS則負(fù)責(zé)將數(shù)個(gè)buffer cache包裝成page cache提供給用戶。
對(duì)于具體的Linux文件系統(tǒng),磁盤等外設(shè)存儲(chǔ)設(shè)備會(huì)以磁盤塊為單位分配給文件用以存儲(chǔ),所以buffer cache大小正好對(duì)應(yīng)著磁盤塊block的大小。引入緩沖區(qū)的目的主要還是為了降低對(duì)文件存儲(chǔ)的外部設(shè)備的IO操作次數(shù)。每個(gè)緩沖區(qū)由兩個(gè)部分組成,第一部分稱為緩沖區(qū)首部,用數(shù)據(jù)結(jié)果buffer_head表示,而第二部分是真正的存儲(chǔ)的數(shù)據(jù)。(這里可以參考我的關(guān)于MiniCRT自定義簡(jiǎn)化版運(yùn)行庫(kù)中提供的堆管理)
typedef struct _heap_header {enum{HEAP_BLOCK_FREE = 0xABABABAB, //空閑塊的魔數(shù)HEAP_BLOCK_USED = 0xCDCDCDCD, //占用塊的魔數(shù)}type;unsigned size; //當(dāng)前塊的尺寸,該size包括塊的信息頭的尺寸struct _heap_header* next;struct _heap_header* prev; }heap_header; 1. Linux系統(tǒng)下IO操作使用Buffer緩沖塊的過(guò)程首先給出page cache的定義 typedef struct page {struct list_head list; //mapping has some page liststruct address_space *mapping; //the inode we belong to unsigned long index; //our offset within mappingstruct page *next_hash; //Next page sharing our hash bucket in the pagecache hash table//和為了快速管理buffer采用hash table一樣,管理page同樣擦用了hash table,這個(gè)next_hash表示和該//page的hash-key一樣的下一個(gè)page的指針atomic_t count; //線程或進(jìn)程使用計(jì)數(shù),在該計(jì)數(shù)為0時(shí)意味著該page已經(jīng)可以被清除了unsigned long flags; //原子鎖標(biāo)志,有可能存在更新不同步的情況struct list_head lru; //pageout list, eg. active_list; protected by pagemap_lru_lock !struct page *pprev_hash; //complement to next_hash和next_hash相對(duì)應(yīng)的指向前面page的指針struct buffer_head *buffers; //buffer maps us to a disk block;/*******On machines where all RAM is mapped into kernel address space, we can simply calculate the virtual address. On machines with highmem some memory is mapped into kernel virtual memory dynamically.So we need a place to store that address. Note that this field could be 16 bits on x86...Architectures with slow multiplication can define WANT_PAGE_VIRTUAL in asm/page.h*********/ #if defined(CONFIG_HIGHMEM) || defined(WANT_PAGE_VIRTUAL)void *virtual; #endif } mem_map_t;
再給出buffer cache的定義形式,其中首先給出buffer_head結(jié)構(gòu)體的定義內(nèi)容
struct buffer_head {struct buffer_head *b_next; //有效buffer是通過(guò)哈希表進(jìn)行管理的,但哈希表中可能因?yàn)?block, dev)組合被映射到同一個(gè)key下,//所以提供這個(gè)同一key下的buffer_head*鏈表指針,用以二次遍歷鎖定準(zhǔn)確的bufferunsigned long b_blocknr; //block number 該buffer映射的磁盤塊塊號(hào)unsigned short b_size; //block size 該buffer映射的磁盤塊內(nèi)容大小unsigned short b_list; //kdev_t b_dev; //該buffer映射的磁盤塊隸屬的虛擬設(shè)備標(biāo)示號(hào)atomic_t b_count; //緩沖區(qū)讀寫使用計(jì)數(shù),如果為0,意味著該緩沖區(qū)內(nèi)的內(nèi)容已經(jīng)沒(méi)有線程聲明要使用了,意味著該Buffer是可以被釋放進(jìn)入空閑隊(duì)列了kdev_t b_rdev; //真實(shí)設(shè)備標(biāo)識(shí)unsigned long b_state; //buffer狀態(tài)標(biāo)記位,各位對(duì)應(yīng)不同的含義unsigned long b_flushtime; //延遲寫的上限時(shí)間struct buffer_head *b_next_free; // lru.free list linkage 指向lru空閑鏈表中next元素struct buffer_head *b_prev_free; //doubly linked list of buffers 指向lru空閑鏈表中prev元素struct buffer_head *b_this_page; //circular list of buffers in one page若該buffer被使用,則該參數(shù)指向同一個(gè)page的buffer鏈表struct buffer_head *b_reqnext; //request queuestruct buffer_head **b_pprev; //doubly linked list of hash-queue hash隊(duì)列雙向鏈表char *b_data; //pointer to data block 指向數(shù)據(jù)塊的指針struct page *b_page; //the page this bh is mapped to 這個(gè)buffer映射的頁(yè)面void (*b_end_io)(struct buffer_head *bh, int uptodate); //IO completion IO結(jié)束時(shí)的執(zhí)行函數(shù)_endvoid *b_private; //reserved for b_end_io 為IO結(jié)尾函數(shù)_end保留位unsigned long b_rsector; //real buffer location on disk 緩沖區(qū)在磁盤上的實(shí)際位置wait_queue_head_t b_wait; struct list_head b_inode_buffers; //doubly linked list of inode dirty buffers//iNode臟緩沖區(qū)循環(huán)鏈表};在buffer_head結(jié)構(gòu)體中提到其狀態(tài)標(biāo)識(shí)位是由unsigned long b_state;來(lái)表示的,下面來(lái)進(jìn)一步分析該狀態(tài)參數(shù)各位的意義。
enum bh_state_bits {BH_Uptodate, //譬如0x0001,如果緩沖區(qū)存在有效數(shù)據(jù)則置為1,否則為0x0000BH_Dirty, //0x0010,如果buffer臟了即數(shù)據(jù)被修改了,則置該位BH_Lock, //如果該緩沖區(qū)被鎖定了,即存在某一進(jìn)程或線程正在使用該buffer,則置該位BH_Req, //如果緩沖區(qū)無(wú)效了,則置為該位BH_Mapped, //如果緩沖區(qū)有一個(gè)磁盤映射置該位BH_New, //如果該緩沖區(qū)是fresh,新分配加入緩沖池的,并且還沒(méi)有被使用,則置該位;BH_Async, //如果緩沖區(qū)是進(jìn)行end_buffer_io_async IO同步則置該位BH_Wait_IO, //如果要將這個(gè)buffer寫回到映射的磁盤中,則置該位BH_Launder, //需要重置該buffer,置該位BH_Attached, //if b_indoe_buffers is linked into a list則置該位BH_JBD, //如果和journal_head 關(guān)聯(lián)置1BH_Sync, //如果buffer是同步讀取置該位BH_Delay, //如果buffer空間是延遲分配置該位BH_PrivateStart, //not a state bit, but the first bit available for private allocation by other entities };從buffer_head和page結(jié)構(gòu)體可以看出,操作系統(tǒng)為了快速定位具體的緩沖塊,采用了hash-table進(jìn)行管理,故而這里介紹下Linux操作系統(tǒng)中關(guān)于Buffer緩沖塊的hash方式
/*關(guān)于VFS如何管理這幾個(gè)buffer cache的鏈表 *1.其中關(guān)于存儲(chǔ)著有效數(shù)據(jù)的buffer,是通過(guò)hash表管理的,key值是由數(shù)據(jù)塊號(hào)+所在設(shè)備標(biāo)識(shí)號(hào)計(jì)算得到 */#define _hashfn (dev, block) \( ( ((dev) << (bh_hash_shift - 6)) ^ ((dev) << (bh_hash_shift - 9)) ) ^ \( ((block) << (bh_hash_shift - 6)) ^ ((block) >> 13) ^ \((block) << (bh_hash_shift - 12)) \) \)介紹了諸多基礎(chǔ)的東西,下面看下Linux下如何具體定位一個(gè)Buffer緩沖塊的函數(shù)bread()。其根據(jù)虛擬設(shè)備號(hào)、緩沖塊號(hào)以及容量參數(shù)進(jìn)行具體定位。
//在具體的文件系統(tǒng)中讀取具體一塊數(shù)據(jù)時(shí),調(diào)用bread函數(shù) struct buffer_head * bread(kdev_t dev, int block, int size) {struct buffer_head * bh;bh = getblk(dev, block, size); //根據(jù)設(shè)備號(hào)、塊號(hào)、和要讀取的字節(jié)數(shù)目返回相應(yīng)的bufferif ( buffer_uptodate(bh) ) //判斷是否存在有效數(shù)據(jù),如果存在那么直接返回即可return bh;set_bit(BH_Sync, &bh->b_state); //如果不存在有效數(shù)據(jù),將這個(gè)buffer設(shè)置為同步狀態(tài)ll_rw_block(READ, l, &bh); //如果沒(méi)有有效數(shù)據(jù),則需要現(xiàn)場(chǎng)從磁盤中將相應(yīng)塊號(hào)的內(nèi)容讀取到buffer中,這個(gè)是一個(gè)操作系統(tǒng)底層的操作wait_on_buffer(bh); //等待buffer的鎖打開(kāi)if ( buffer_uptodate(bh) )return bh;brelse(bh);return NULL; };getblk()函數(shù)的具體實(shí)現(xiàn),其根據(jù)相應(yīng)參數(shù)返回具體的緩沖塊首地址
struct buffer_head * getblk(kdev_t dev, int block, int size) {for (;;){struct buffer_head * bh;bh = get_hash_table(dev, block, size); //關(guān)鍵函數(shù),得到hash表中的bufferif (bh) {touch_buffer(bh);return bh; //返回這個(gè)buffer}//如果沒(méi)有找到對(duì)應(yīng)的buffer,那么試著去增加一個(gè)buffer,就是使用下面的grow_buffer函數(shù)if (!grow_buffers(dev, block, size)) //即調(diào)用該函數(shù)返回一個(gè)足夠空間的fresh buffer,用以提供給后面從磁盤讀取目標(biāo)塊的內(nèi)容的緩沖區(qū)free_more_memory();//如果空間不足,則只能從LRU隊(duì)列中選出buffer,先看是否已“臟”,若是,則寫回磁盤,并清空內(nèi)容,分配給新的數(shù)據(jù)塊} }; #define hash(dev, block) hash_table[ ( _hashfn(HASHDEV(dev), block) & bh_hash_mask ) ] #define get_bh(bh) atomic_inc( &(bh)->b_count )struct buffer_head * get_hash_table( kdev_t dev, int block, int size) {struct buffer_head *bh;struct buffer_head **p = &hash(dev, block); //通過(guò)hash表查找到對(duì)應(yīng)的 bufferread_lock (&hash_table_lock); //判斷得到的buffer數(shù)組中有沒(méi)有我們需要的bufferfor(;;) { bh = *p;if (!bh)break;p = &bh->b_next;if (bh->b_blocknr != block)continue;if (bh->b_size != size)continue;if (bh->b_dev != dev)continue;get_bh(bh); //如果有那么直接執(zhí)行這個(gè)函數(shù),這個(gè)函數(shù)其實(shí)已經(jīng)通過(guò)宏給出break;}read_unlock(&hash_table_lock);return bh; };如果緩沖池不夠用,則試著增加新的緩沖塊,該操作便是通過(guò)grow_buffers()函數(shù)實(shí)現(xiàn)的。
//如果沒(méi)找到對(duì)應(yīng)的buffer,那么使用grow_buffer函數(shù)增加一個(gè)新的buffer,該buffer的狀態(tài)標(biāo)記為BH_New // try to increase the number of buffers available: the size argument is used to determine //what kind of buffers we want stastic int grow_buffers (kdev_t dev, unsigned long block, int size) {struct page* page;struct block_device *bdev;unsigned long index;int sizebits;/**size must be multiple of hard sectorsize 給出的size必須是硬件扇區(qū)的整數(shù)倍*/if (size & (get_hardsect_size(dev) - 1) )BUG();if (size < 512 || size > PAGE_SIZE )BUG();//新加入的緩沖塊大小必須在512到PAGE_SIZEsizebits = -1;do {sizebits++;} while ((size << sizebits) < PAGE_SIZE);index = block >> sizebits;block = index << sizebits;bdev = bdget( kdev_t_to_nr(dev) );if (!bdev){printfk("No block device for %s\n", kdevname(dev));BUG();}/*即根據(jù)需求的size新開(kāi)辟一個(gè)緩沖頁(yè)P(yáng)age*/page = grow_dev_page( bdev, index, size );atominc_dec( &bdev->bd_count);if (!page)return 0;/*Hash in the buffers on the hash list*/hash_page_buffers( page, dev, block, size);UnlockPage( page );page_cache_release( page );/* we hashed up this page, so increment buffermem*/atomic_inc( &buffermem_pages );return 1; }前面說(shuō)到我們是通過(guò)hash-table來(lái)管理已經(jīng)被填入有效數(shù)據(jù)的緩沖區(qū)buffer的,但是其實(shí)緩沖區(qū)類型是由多種的
#define BUF_CLEAN 0 #define BUF_LOCKED 1 //正在等待被寫回的uffer: Buffers scheduled for write #define BUF_DIRTY 2 //臟buffer,但還沒(méi)有被安排寫回,即延遲寫策略不滿足 #define NR_DIRTY 3而事實(shí)上,緩沖池中更新策略一個(gè)重要的概念便是LRU(least recently used最近最少使用)。而緩沖池的具體實(shí)現(xiàn)中除了empty\input\output三種隊(duì)列,還有便是LRU隊(duì)列控制的淘汰緩沖隊(duì)列以及hash-table提供的快速索引。
綜合來(lái)說(shuō),Linux系統(tǒng)為IO讀取操作配備的buffer緩沖池是這樣起作用的:
首先在Hash-table中尋找目標(biāo)buffer,如果找到了該buffer,則直接返回該buffer的buffer_head指針,如果沒(méi)有,那么意味著要讀取的內(nèi)容并不在buffer緩沖池中,故而要為這份新的數(shù)據(jù)內(nèi)容分配一塊新的匹配size的buffer;
先從內(nèi)存中再直接劃分出一塊區(qū)域作為新添加的緩沖塊,加入緩沖池體系中,供應(yīng)本次使用;
如果內(nèi)存空間不足或者已經(jīng)達(dá)到緩沖池規(guī)模上限,則開(kāi)始從LRU隊(duì)列中取出鏈?zhǔn)自?#xff0c;先看是否臟了,如果臟了,則先回寫,然后清空內(nèi)容,將它分配給新的數(shù)據(jù)塊。
總結(jié)
以上是生活随笔為你收集整理的缓冲技术之三:Linux下I/O操作buffer缓冲块使用流程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【iOS开发】——MRC(手动内存管理)
- 下一篇: Vectorcast 2021 sp4