用戶的應用程序會經常讀寫磁盤文件的數據到內存,但是內存的速度和磁盤的速度理論上差了好幾個數量級;為了更高效地解決內存和磁盤的速度差,linux也在內存使用了緩存區(作用類似於cpu內部為了解決寄存器和內存速度差異的的L1、L2、L3 cache):如果數據要寫入磁盤文件,先放在緩存區,等湊夠了一定數量后再批量寫入磁盤文件,借此減少磁盤尋址的次數,來提升寫入效率(這里多說幾句:比如U盤插上電腦后,如果要拔出,建議先卸載再拔出,而不是直接拔出,為啥了?U盤的數據也是先放入緩沖區的,緩沖區有自己的管理機制,很久沒有使用的塊可以給其他進程使用,如果是臟塊則要進行寫盤。緩沖在某些情況下才會有寫盤操作,所以要拔出U盤時,應該先進行卸載,這樣才會寫盤,否則數據可能丟失,文件系統可能損壞。);如果從磁盤讀數據,也會先放入緩存區暫存,一旦有其他進程或線程讀取同樣的磁盤文件,這是就可以先從內存的緩存區取數據了,沒必要重新從磁盤讀取,也提升了效率!linux 0.11的緩沖區是怎么工作的了?
在main.c的main函數中,有設置緩存區的大小,代碼如下:內存不同,緩存區的大小也不同,linux是怎么管理和使用這些緩存區了的?
void main(void) /* This really IS void, no error here. */ { /* The startup routine assumes (well, ...) this */ /* * Interrupts are still disabled. Do necessary setups, then * enable them */ //前面這里做的所有事情都是在對內存進行拷貝 ROOT_DEV = ORIG_ROOT_DEV;//設置操作系統的根文件 drive_info = DRIVE_INFO;//設置操作系統驅動參數 //解析setup.s代碼后獲取系統內存參數 memory_end = (1<<20) + (EXT_MEM_K<<10); //取整4k的內存大小 memory_end &= 0xfffff000; if (memory_end > 16*1024*1024)//控制操作系統的最大內存為16M memory_end = 16*1024*1024; if (memory_end > 12*1024*1024) buffer_memory_end = 4*1024*1024;//設置高速緩沖區的大小,跟塊設備有關,跟設備交互的時候,充當緩沖區,寫入到塊設備中的數據先放在緩沖區里,只有執行sync時才真正寫入;這也是為什么要區分塊設備驅動和字符設備驅動;塊設備寫入需要緩沖區,字符設備不需要是直接寫入的 else if (memory_end > 6*1024*1024) buffer_memory_end = 2*1024*1024; else buffer_memory_end = 1*1024*1024; main_memory_start = buffer_memory_end;
1、cpu有分頁機制,硬件上以4KB為單位把內存分割成小塊供程序使用;這個顆粒度是比較大的,有些時候可能會浪費比較多的內存,所以linux緩存區采用了1KB的大小來分割整個緩存區;假設緩存區有2MB,那么一共被分割成了2000個小塊,這么多的緩存區該怎么管理了?
每個緩存都有各自的屬性,比如是否使用、數據是否更新、緩存數據在磁盤的位置、緩存的起始地址等,要想統一管理這么多的屬性,最好的辦法自然是構建結構體了;一個結構體管理1塊(也就是1KB)的緩存區;假設這里有2000個緩存區,就需要2000個結構體,那么問題又來了:這個多的結構體,又該怎么去管理了?
參考前面的進程task結構體管理方式:用task數組來管理所有的進程task結構體,最大限制為64個進程,但是放在這里顯然不適用:不同機器的物理內存大小是不同的,導致緩存區的block數量是不同的,但數組最大的缺點就是定長,無法適應不同的物理內存,那么這里最適合的只剩鏈表了,所以linux 0.11版本使用的結構體如下:
struct buffer_head { char * b_data; /* pointer to data block (1024 bytes):單個數據塊大小1KB */ unsigned long b_blocknr; /* block number */ unsigned short b_dev; /* device (0 = free) */ unsigned char b_uptodate; /*數據是否更新*/ unsigned char b_dirt; /* 0-clean空現,1-dirty已被占用*/ unsigned char b_count; /* users using this block */ /*如果緩沖區的某個block被鎖,上層應用是沒法從這個block對應的磁盤空間讀數據的,這里有個漏洞: A進程鎖定了某block,B進程想辦法解鎖,然后就能監聽A進程從磁盤讀寫了哪些數據 */ unsigned char b_lock; /* 0 - ok, 1 -locked:鎖用於多進程/多線程之間同步,避免數據出錯*/ struct task_struct * b_wait;/*A正在使用這個緩存,並已經鎖定;B也想用,就用這個字段記錄;等A用完后從這里找到B再給B用*/ struct buffer_head * b_prev; struct buffer_head * b_next; struct buffer_head * b_prev_free; struct buffer_head * b_next_free; };
每個字段的含義都在注釋了,這里不再贅述;既然采用了鏈表,解決了數組只能定長的缺點,但是鏈表本身也有缺點:無法直接找到目標實例,需要挨個遍歷鏈表上的每個節點;還是假設有2000個塊,好巧的不巧的是程序所需的block剛好在最后一個節點,那么需要遍歷1999個節點才能到達,效率非常低,這又該怎么解決了?剛好這種快速尋址(時間復雜度O(1))是數組的優勢,怎么解決數組和鏈表各自的優勢了?-----hash表!
linux 0.11版本采用hash表的方式快速尋址,hash映射算法如下:
// hash表的主要作用是減少查找比較元素所花費的時間。通過在元素的存儲位置與關 // 鍵字之間建立一個對應關系(hash函數),我們就可以直接通過函數計算立刻查詢到指定 // 的元素。建立hash函數的指導條件主要是盡量確保散列在任何數組項的概率基本相等。 // 建立函數的方法有多種,這里Linux-0.11主要采用了關鍵字除留余數法。因為我們 // 尋找的緩沖塊有兩個條件,即設備號dev和緩沖塊號block,因此設計的hash函數肯定 // 需要包含這兩個關鍵值。這兩個關鍵字的異或操作只是計算關鍵值的一種方法。再對 // 關鍵值進行MOD運算就可以保證函數所計算得到的值都處於函數數組項范圍內。 #define _hashfn(dev,block) (((unsigned)(dev^block))%NR_HASH) #define hash(dev,block) hash_table[_hashfn(dev,block)]
映射的算法也很簡單:每個buffer_head結構體都有dev和block兩個字段,這兩個字段組合起來本身是不會重復的,所以把這兩個字段異或后模上hash表的長度,就得到了hash數組的偏移;現在問題又來了:這個版本的hash_table數組長度設定為NR_HASH=307,遠不如buffer_head的實例個數,肯定會發生hash沖突,這個該怎么解決了?--這里就要用上鏈表變長的優點了:把發生hash沖突的bufer_head實例首位相接不久得了么?最終的hash_table示意圖如下:hash表本身用數組,存儲buffer_head實例的地址;如果發生hash沖突,相同hash偏移的實例通過b_next和b_prev鏈表首尾連接!

當這個一整套存儲機制建立后,怎么檢索了?linux的檢索方式如下:先通過dev和block號定位到hash表的偏移,再遍歷該偏移處的所有節點,通過比對dev和block號找到目標buffer_head實例!
//// 利用hash表在高速緩沖區中尋找給定設備和指定塊號的緩沖區塊。 // 如果找到則返回緩沖區塊的指針,否則返回NULL。 static struct buffer_head * find_buffer(int dev, int block) { struct buffer_head * tmp; // 搜索hash表,尋找指定設備號和塊號的緩沖塊。 for (tmp = hash(dev,block) ; tmp != NULL ; tmp = tmp->b_next) if (tmp->b_dev==dev && tmp->b_blocknr==block) return tmp; return NULL; }
根據dev和block號找到緩存區的buffer_head並不代表萬事大吉,因為該緩存區可能已經被其他進程/線程占用,當前線程如果一定要用這個緩存區,只能等了,所以最終查找緩存區的代碼如下:這里增加了wait_on_buffer函數:
//// 利用hash表在高速緩沖區中尋找指定的緩沖塊。若找到則對該緩沖塊上鎖 // 返回塊頭指針。 struct buffer_head * get_hash_table(int dev, int block) { struct buffer_head * bh; for (;;) { // 在高速緩沖中尋找給定設備和指定塊的緩沖區塊,如果沒有找到則返回NULL。 if (!(bh=find_buffer(dev,block))) return NULL; // 對該緩沖塊增加引用計數,並等待該緩沖塊解鎖。由於經過了睡眠狀態,其他任務可能會更改這個緩存區對應的dev和block號 // 因此有必要在驗證該緩沖塊的正確性,並返回緩沖塊頭指針。 bh->b_count++; wait_on_buffer(bh); if (bh->b_dev == dev && bh->b_blocknr == block) return bh; // 如果在睡眠時該緩沖塊所屬的設備號或塊設備號發生了改變,則撤消對它的 // 引用計數,重新尋找。 bh->b_count--; } }
wait_on_buffer函數實現:如果發現該緩存區已經上鎖,那么調用sleep_on函數讓出cpu,阻塞在這里等待;這個sleep_on函數傳入的參數是二級指針,並且內部用了tmp變量保存臨時變量;由於二級指針是全局的,所以如果有多個task等待同一個緩存區,sleep_on函數是通過先進后出的棧的形式喚醒等待任務的;參考1有詳細的說明,感興趣的小伙伴建議好好看看!
//// 等待指定緩沖塊解鎖 // 如果指定的緩沖塊bh已經上鎖就讓進程不可中斷地睡眠在該緩沖塊的等待隊列b_wait中。 // 在緩沖塊解鎖時,其等待隊列上的所有進程將被喚醒。雖然是在關閉中斷(cli)之后 // 去睡眠的,但這樣做並不會影響在其他進程上下文中影響中斷。因為每個進程都在自己的 // TSS段中保存了標志寄存器EFLAGS的值,所以在進程切換時CPU中當前EFLAGS的值也隨之 // 改變。使用sleep_on進入睡眠狀態的進程需要用wake_up明確地喚醒。 static inline void wait_on_buffer(struct buffer_head * bh) { cli(); // 關中斷 while (bh->b_lock) // 如果已被上鎖則進程進入睡眠,等待其解鎖 sleep_on(&bh->b_wait); sti(); // 開中斷 }
先進后出的棧形式喚醒等待任務:

接下來可能就是buffer.c中最重要的函數之一了:struct buffer_head * getblk(int dev,int block),根據設備號和塊號得到buffer_head的實例,便於后續使用對應的緩存區;
//// 取高速緩沖中指定的緩沖塊 // 檢查指定(設備號和塊號)的緩沖區是否已經在高速緩沖中。如果指定塊已經在 // 高速緩沖中,則返回對應緩沖區頭指針退出;如果不在,就需要在高速緩沖中設置一個 // 對應設備號和塊好的新項。返回相應的緩沖區頭指針。 struct buffer_head * getblk(int dev,int block) { struct buffer_head * tmp, * bh; repeat: // 搜索hash表,如果指定塊已經在高速緩沖中,則返回對應緩沖區頭指針,退出。 if ((bh = get_hash_table(dev,block))) return bh; // 掃描空閑數據塊鏈表,尋找空閑緩沖區。 // 首先讓tmp指向空閑鏈表的第一個空閑緩沖區頭 tmp = free_list; do { // 如果該緩沖區正被使用(引用計數不等於0),則繼續掃描下一項。對於 // b_count = 0的塊,即高速緩沖中當前沒有引用的塊不一定就是干凈的 // (b_dirt=0)或沒有鎖定的(b_lock=0)。因此,我們還是需要繼續下面的判斷 // 和選擇。例如當一個任務該寫過一塊內容后就釋放了,於是該塊b_count()=0 // 但b_lock不等於0;當一個任務執行breada()預讀幾個塊時,只要ll_rw_block() // 命令發出后,它就會遞減b_count; 但此時實際上硬盤訪問操作可能還在進行, // 因此此時b_lock=1, 但b_count=0. if (tmp->b_count) continue; // 如果緩沖頭指針bh為空,或者tmp所指緩沖頭的標志(修改、鎖定)權重小於bh // 頭標志的權重,則讓bh指向tmp緩沖塊頭。如果該tmp緩沖塊頭表明緩沖塊既 // 沒有修改也沒有鎖定標志置位,則說明已為指定設備上的塊取得對應的高速 // 緩沖塊,則退出循環。否則我們就繼續執行本循環,看看能否找到一個BANDNESS() // 最小的緩沖塊。BADNESS等於0意味着b_block和b_dirt都是0,這塊緩存區還沒被使用,目標緩存區已經找到,可以跳出循環了 if (!bh || BADNESS(tmp)<BADNESS(bh)) { bh = tmp; if (!BADNESS(tmp)) break; } /* and repeat until we find something good */ } while ((tmp = tmp->b_next_free) != free_list); // 如果循環檢查發現所有緩沖塊都正在被使用(所有緩沖塊的頭部引用計數都>0)中, // 則睡眠等待有空閑緩沖塊可用。當有空閑緩沖塊可用時本進程會唄明確的喚醒。 // 然后我們跳轉到函數開始處重新查找空閑緩沖塊。 if (!bh) { sleep_on(&buffer_wait); goto repeat; } // 執行到這里,說明我們已經找到了一個比較合適的空閑緩沖塊了。於是先等待該緩沖區 // 解鎖(多任務同時運行,剛找到的緩存塊可能已經被其他任務搶先一步找到並使用了,所以要再次檢查)。如果在我們睡眠階段該緩沖區又被其他任務使用的話,只好重復上述尋找過程。 wait_on_buffer(bh); if (bh->b_count) goto repeat; // 如果該緩沖區已被修改,則將數據寫盤,並再次等待緩沖區解鎖。同樣地,若該緩沖區 // 又被其他任務使用的話,只好再重復上述尋找過程。 while (bh->b_dirt) { sync_dev(bh->b_dev); wait_on_buffer(bh); if (bh->b_count) goto repeat; } /* NOTE!! While we slept waiting for this block, somebody else might */ /* already have added "this" block to the cache. check it */ // 在高速緩沖hash表中檢查指定設備和塊的緩沖塊是否乘我們睡眠之際已經被加入 // 進去(畢竟是多任務系統,有可能被其他任務搶先使用並放入has表)。如果是的話,就再次重復上述尋找過程。 if (find_buffer(dev,block)) goto repeat; /* OK, FINALLY we know that this buffer is the only one of it's kind, */ /* and that it's unused (b_count=0), unlocked (b_lock=0), and clean */ // 於是讓我們占用此緩沖塊。置引用計數為1,復位修改標志和有效(更新)標志。 bh->b_count=1; bh->b_dirt=0; bh->b_uptodate=0; // 從hash隊列和空閑隊列塊鏈表中移出該緩沖區頭,讓該緩沖區用於指定設備和 // 其上的指定塊。然后根據此新的設備號和塊號重新插入空閑鏈表和hash隊列新 // 位置處。並最終返回緩沖頭指針。 remove_from_queues(bh); bh->b_dev=dev; bh->b_blocknr=block; insert_into_queues(bh); return bh; }
代碼的整體邏輯並不復雜,但是有些細節想展開說說:
- BADNESS(bh):從表達式看,b_dirt左移1位后再和b_lock相加,明顯b_dirt的權重乘以了2,說明作者認為緩存區是否被使用的權重應該大於是否被鎖!但是實際使用的時候,會一直循環查找BADNESS小的緩存區,說明作者認為b_block比b_dirt更重要,也就是緩存區是否上鎖比是否被使用了更重要,這個也符合業務邏輯!
// 下面宏用於同時判斷緩沖區的修改標志和鎖定標志,並且定義修改標志的權重要比鎖定標志大。 // b_dirt左移1位,權重比b_block高 #define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock)
- 循環停止的條件如下:tmp初始值就是free_list,這里的停止的條件也是tmp == free_list,說明free_list是個環形循環鏈表;所以整個do while循環本質上就是在free_list中找BADNESS值最小的buffer_head;如果找到BADNESS等於0(意味着b_block和b_dirt都為0,該緩存區還沒被使用)的buffer_head,直接跳出循環!
while ((tmp = tmp->b_next_free) != free_list);
- 函數結尾處: 再次檢查dev+block是否已經在緩存區了,如果在,說明其他任務捷足先登,已經使用了該緩存區,本任務只能重新走查找的流程;如果該緩存塊還沒被使用,先設置一些標志/屬性位,再把該buffer_head節點從舊hash表和free_list隊列溢出,再重新加入hash_table和free_list隊列,作者是咋想的?為啥要重復干這種事了?
/* already have added "this" block to the cache. check it */ // 在高速緩沖hash表中檢查指定設備和塊的緩沖塊是否乘我們睡眠之際已經被加入 // 進去(畢竟是多任務,期間可能會被其他任務搶先使用並放入hash表)。如果是的話,就再次重復上述尋找過程。 if (find_buffer(dev,block)) goto repeat; /* OK, FINALLY we know that this buffer is the only one of it's kind, */ /* and that it's unused (b_count=0), unlocked (b_lock=0), and clean */ // 於是讓我們占用此緩沖塊。置引用計數為1,復位修改標志和有效(更新)標志。 bh->b_count=1; bh->b_dirt=0; bh->b_uptodate=0; // 從hash隊列和空閑隊列塊鏈表中移出該緩沖區頭,讓該緩沖區用於指定設備和 // 其上的指定塊。然后根據此新的設備號和塊號重新插入空閑鏈表和hash隊列新 // 位置處。並最終返回緩沖頭指針。 /*將緩沖塊從舊的隊列移出,添加到新的隊列中,即哈希表的頭,空閑表的尾,這樣能夠迅速找到該存在的塊,而該緩沖塊存在的時間最長*/ remove_from_queues(bh); bh->b_dev=dev; bh->b_blocknr=block; insert_into_queues(bh); return bh;
先來看看remove_from_queues和insert_into_queu函數代碼:remove_from_queues沒啥好說的,就是簡單粗暴的從hash表和free_list刪除,也是常規的鏈表操作,重點在insert_into_queu函數:
- bh節點加入了free_list鏈表的末尾,直接減少了后續查詢遍歷鏈表的時間,這不就直接提升了查詢效率么?
- bh節點加入hash表某個偏移的表頭,后續通過hash偏移不就能第一個找到該節點了么?又省了遍歷鏈表的操作!
//// 從hash隊列和空閑緩沖區隊列中移走緩沖塊。 // hash隊列是雙向鏈表結構,空閑緩沖塊隊列是雙向循環鏈表結構。 static inline void remove_from_queues(struct buffer_head * bh) { /* remove from hash-queue */ if (bh->b_next) bh->b_next->b_prev = bh->b_prev; if (bh->b_prev) bh->b_prev->b_next = bh->b_next; // 如果該緩沖區是該隊列的頭一個塊(每個hash偏移的頭),則讓hash表的對應項指向本隊列中的下一個 // 緩沖區。 if (hash(bh->b_dev,bh->b_blocknr) == bh) hash(bh->b_dev,bh->b_blocknr) = bh->b_next; /* remove from free list */ if (!(bh->b_prev_free) || !(bh->b_next_free)) panic("Free block list corrupted"); bh->b_prev_free->b_next_free = bh->b_next_free; bh->b_next_free->b_prev_free = bh->b_prev_free; // 如果空閑鏈表頭指向本緩沖區,則讓其指向下一緩沖區。 if (free_list == bh) free_list = bh->b_next_free; } //// 將緩沖塊插入空閑鏈表尾部,同時放入hash隊列中。 static inline void insert_into_queues(struct buffer_head * bh) { /* put at end of free list */ bh->b_next_free = free_list; bh->b_prev_free = free_list->b_prev_free; free_list->b_prev_free->b_next_free = bh; free_list->b_prev_free = bh; /* put the buffer in new hash-queue if it has a device */ // 請注意當hash表某項第1次插入項時,hash()計算值肯定為Null,因此此時得到 // 的bh->b_next肯定是NULL,所以應該在bh->b_next不為NULL時才能給b_prev賦 // bh值。 bh->b_prev = NULL; bh->b_next = NULL; if (!bh->b_dev) return; bh->b_next = hash(bh->b_dev,bh->b_blocknr); hash(bh->b_dev,bh->b_blocknr) = bh; bh->b_next->b_prev = bh; // 此句前應添加"if (bh->b_next)"判斷 }
當一個block使用完后就要釋放了,避免“占着茅坑不拉屎”;釋放的邏輯也簡單,如下:引用計數count--,並且喚醒正在等待該緩存區的其他任務;
// 釋放指定緩沖塊。 // 等待該緩沖塊解鎖。然后引用計數遞減1,並明確地喚醒等待空閑緩沖塊的進程。 void brelse(struct buffer_head * buf) { if (!buf) return; wait_on_buffer(buf); if (!(buf->b_count--)) panic("Trying to free free buffer"); wake_up(&buffer_wait); }
前面很多的操作,尤其是節點的增刪改查都涉及到了hash表和鏈表,那么hash表和鏈表都是怎么建立的了?這里用的是buffer_init函數:hash表初始化時所有的偏移都指向null;
// 緩沖區初始化函數 // 參數buffer_end是緩沖區內存末端。對於具有16MB內存的系統,緩沖區末端被設置為4MB. // 對於有8MB內存的系統,緩沖區末端被設置為2MB。該函數從緩沖區開始位置start_buffer // 處和緩沖區末端buffer_end處分別同時設置(初始化)緩沖塊頭結構和對應的數據塊。直到 // 緩沖區中所有內存被分配完畢。 void buffer_init(long buffer_end) { struct buffer_head * h = start_buffer; void * b; int i; // 首先根據參數提供的緩沖區高端位置確定實際緩沖區高端位置b。如果緩沖區高端等於1Mb, // 則因為從640KB - 1MB被顯示內存和BIOS占用,所以實際可用緩沖區內存高端位置應該是 // 640KB。否則緩沖區內存高端一定大於1MB。 if (buffer_end == 1<<20) b = (void *) (640*1024); else b = (void *) buffer_end; // 這段代碼用於初始化緩沖區,建立空閑緩沖區塊循環鏈表,並獲取系統中緩沖塊數目。 // 操作的過程是從緩沖區高端開始划分1KB大小的緩沖塊,與此同時在緩沖區低端建立 // 描述該緩沖區塊的結構buffer_head,並將這些buffer_head組成雙向鏈表。 // h是指向緩沖頭結構的指針,而h+1是指向內存地址連續的下一個緩沖頭地址,也可以說 // 是指向h緩沖頭的末端外。為了保證有足夠長度的內存來存儲一個緩沖頭結構,需要b所 // 指向的內存塊地址 >= h 緩沖頭的末端,即要求 >= h+1. while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) { h->b_dev = 0; // 使用該緩沖塊的設備號 h->b_dirt = 0; // 臟標志,即緩沖塊修改標志 h->b_count = 0; // 緩沖塊引用計數 h->b_lock = 0; // 緩沖塊鎖定標志 h->b_uptodate = 0; // 緩沖塊更新標志(或稱數據有效標志) h->b_wait = NULL; // 指向等待該緩沖塊解鎖的進程 h->b_next = NULL; // 指向具有相同hash值的下一個緩沖頭 h->b_prev = NULL; // 指向具有相同hash值的前一個緩沖頭 h->b_data = (char *) b; // 指向對應緩沖塊數據塊(1024字節) h->b_prev_free = h-1; // 指向鏈表中前一項 h->b_next_free = h+1; // 指向連表中后一項 h++; // h指向下一新緩沖頭位置 NR_BUFFERS++; // 緩沖區塊數累加 if (b == (void *) 0x100000) // 若b遞減到等於1MB,則跳過384KB b = (void *) 0xA0000; // 讓b指向地址0xA0000(640KB)處 } h--; // 讓h指向最后一個有效緩沖塊頭 free_list = start_buffer; // 讓空閑鏈表頭指向頭一個緩沖快 free_list->b_prev_free = h; // 鏈表頭的b_prev_free指向前一項(即最后一項)。 h->b_next_free = free_list; // h的下一項指針指向第一項,形成一個環鏈 // 最后初始化hash表,置表中所有指針為NULL。 for (i=0;i<NR_HASH;i++) hash_table[i]=NULL; }
截至目前,前面圍繞緩存區做了大量的鋪墊,最終的目的就是和磁盤之間讀寫數據,那么linux又是怎么利用緩存區從磁盤讀數據的了?bread函數代碼如下:整個邏輯也很簡單,先申請緩存區,如果已經更新就直接返回;否則調用ll_rw_block讀磁盤數據;讀數據是要花時間的,這段時間cpu沒必要閑着,可以跳轉到其他進程繼續執行;等數據讀完后喚醒當前進程,檢查buffer是否被鎖、是否被更新;如果都沒有,就可以安心釋放了!
//// 從設備上讀取數據塊。 // 該函數根據指定的設備號 dev 和數據塊號 block,首先在高速緩沖區中申請一塊 // 緩沖塊。如果該緩沖塊中已經包含有有效的數據就直接返回該緩沖塊指針,否則 // 就從設備中讀取指定的數據塊到該緩沖塊中並返回緩沖塊指針。 struct buffer_head * bread(int dev,int block) { struct buffer_head * bh; // 在高速緩沖區中申請一塊緩沖塊。如果返回值是NULL,則表示內核出錯,停機。 // 然后我們判斷其中說是否已有可用數據。如果該緩沖塊中數據是有效的(已更新) // 可以直接使用,則返回。 if (!(bh=getblk(dev,block))) panic("bread: getblk returned NULL\n"); if (bh->b_uptodate) return bh; // 否則我們就調用底層快設備讀寫ll_rw_block函數,產生讀設備塊請求。然后 // 等待指定數據塊被讀入,並等待緩沖區解鎖。在睡眠醒來之后,如果該緩沖區已 // 更新,則返回緩沖區頭指針,退出。否則表明讀設備操作失敗,於是釋放該緩 // 沖區,返回NULL,退出。 ll_rw_block(READ,bh); wait_on_buffer(bh); if (bh->b_uptodate) return bh; brelse(bh); return NULL; }
ll_rw_block:ll全稱應該是lowlevel的意思;rw表示讀或者寫請求,bh用來傳遞數據或保存數據。先通過主設備號判斷是否為有效的設備,同時請求函數是否存在。如果是有效的設備且函數存在,即有驅動,則添加請求到相關鏈表中;
對於一個當前空閑的塊設備,當 ll_rw_block()函數為其建立第一個請求項時,會讓該設備的當前請求項指針current_request直接指向剛建立的請求項,並且立刻調用對應設備的請求項操作函數開始執行塊設備讀寫操作。當一個塊設備已經有幾個請求項組成的鏈表存在,ll_rw_block()就會利用電梯算法,根據磁頭移動距離最小原則,把新建的請求項插入到鏈表適當的位置處;
void ll_rw_block(int rw, struct buffer_head * bh) { unsigned int major; if ((major=MAJOR(bh->b_dev)) >= NR_BLK_DEV || !(blk_dev[major].request_fn)) { printk("Trying to read nonexistent block-device\n\r"); return; } make_request(major,rw,bh); }
該函數內部繼續調用make_request生成request:函數首先判斷是否為提前讀或者提前寫,如果是則要看bh是否上了鎖。上了鎖則直接返回,因為提前操作是不必要的,否則轉化為可以識別的讀或者寫,然后鎖住緩沖區;數據處理結束后在中斷處理函數中解鎖;如果是寫操作但是緩沖區不臟,或者讀操作但是緩沖區已經更新,則直接返回;最后構造request實例,調用add_request函數把實例添加到鏈表!
add_request函數用了電梯調度算法,主要是考慮到早期機械磁盤的移臂的時間消耗較大,要么從里到外,要么從外到里,順着某個方向多處理請求。如果req剛好在磁頭移動的方向上,那么可以先處理,這樣能節省IO(本質是尋址)的時間;
/* * add-request adds a request to the linked list. * It disables interrupts so that it can muck with the * request-lists in peace. */ static void add_request(struct blk_dev_struct * dev, struct request * req) { struct request * tmp; req->next = NULL; cli(); if (req->bh) req->bh->b_dirt = 0; if (!(tmp = dev->current_request)) { dev->current_request = req; sti(); (dev->request_fn)(); return; } for ( ; tmp->next ; tmp=tmp->next) if ((IN_ORDER(tmp,req) || !IN_ORDER(tmp,tmp->next)) && IN_ORDER(req,tmp->next)) break; req->next=tmp->next; tmp->next=req; sti(); } static void make_request(int major,int rw, struct buffer_head * bh) { struct request * req; int rw_ahead; /* WRITEA/READA is special case - it is not really needed, so if the */ /* buffer is locked, we just forget about it, else it's a normal read */ if ((rw_ahead = (rw == READA || rw == WRITEA))) { if (bh->b_lock) return; if (rw == READA) rw = READ; else rw = WRITE; } if (rw!=READ && rw!=WRITE) panic("Bad block dev command, must be R/W/RA/WA"); lock_buffer(bh); if ((rw == WRITE && !bh->b_dirt) || (rw == READ && bh->b_uptodate)) { unlock_buffer(bh); return; } repeat: /* we don't allow the write-requests to fill up the queue completely: * we want some room for reads: they take precedence. The last third * of the requests are only for reads. */ if (rw == READ) req = request+NR_REQUEST; else req = request+((NR_REQUEST*2)/3); /* find an empty request */ while (--req >= request) if (req->dev<0) break; /* if none found, sleep on new requests: check for rw_ahead */ if (req < request) { if (rw_ahead) { unlock_buffer(bh); return; } sleep_on(&wait_for_request); goto repeat; } /* fill up the request-info, and add it to the queue */ req->dev = bh->b_dev; req->cmd = rw; req->errors=0; req->sector = bh->b_blocknr<<1; req->nr_sectors = 2; req->buffer = bh->b_data; req->waiting = NULL; req->bh = bh; req->next = NULL; add_request(major+blk_dev,req); }
add_request中定義了宏IN_ORDER,真正的電梯調度算法體現在這里了:read請求排在寫請求前面,先處理讀請求,再處理寫請求;同一讀或寫請求先處理設備號小的設備請求,再處理設備號大的設備請求;同一讀或寫請求,同一設備,按先里面的扇區再到外面的扇區的順序處理。
/* * This is used in the elevator algorithm: Note that * reads always go before writes. This is natural: reads * are much more time-critical than writes. */ #define IN_ORDER(s1,s2) \ ((s1)->cmd<(s2)->cmd || ((s1)->cmd==(s2)->cmd && \ ((s1)->dev < (s2)->dev || ((s1)->dev == (s2)->dev && \ (s1)->sector < (s2)->sector))))
參考:
1、https://blog.csdn.net/jmh1996/article/details/90139485 linux-0.12源碼分析——緩沖區等待隊列(棧)sleep_on+wake_up分析2
2、https://blog.csdn.net/ac_dao_di/article/details/54615951 linux 0.11 塊設備文件的使用
3、https://cloud.tencent.com/developer/article/1749826 Linux文件系統之 — 通用塊處理層
