linux源碼解讀(二):文件系統——高速緩存區


  用戶的應用程序會經常讀寫磁盤文件的數據到內存,但是內存的速度和磁盤的速度理論上差了好幾個數量級;為了更高效地解決內存和磁盤的速度差,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文件系統之 — 通用塊處理層


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM