1. 程式人生 > >Linux高速緩沖區原理

Linux高速緩沖區原理

end 占用 src idt 當前 bio class strong ext

文件系統-高速緩沖區:

首先我們為什麽需要高速緩沖區而不是直接訪問塊設備中的數據。這是因為,IO設備和內存之間的讀寫速度不匹配而且有一點數據需要寫入或者讀出磁盤就訪問磁盤,磁盤很快就會損壞,而高速緩沖區就起了一個中間過程的作用,把數據存在高速緩沖區中,需要讀取磁盤上的數據時,嘗試匹配高速緩沖區中的數據,匹配成功了,那就直接從高速緩沖區中取數據,然後內核再來操作,如果要存入數據,也是先經過緩沖區,再存入磁盤。這樣就避免了每次都對IO設備進行操作。

高速緩沖區在整個物理內存中的位置處於內核區和主內存區之間。這裏引用《linux0.11代碼完全註釋》中的圖。
技術分享圖片
在高速緩沖區內部,分成了兩個部分,一個是緩沖頭結構,另一個是緩沖塊

。每個緩沖塊的大小與塊設備上的磁盤邏輯塊的大小相同,而緩沖頭結構用於連接起緩沖塊,以及設置一些屬性。結構如圖:
技術分享圖片

那麽內核要使用緩沖塊時,是怎麽和物理設備對應起來的呢?就比如向某個設備寫了一些數據,存儲在緩沖塊中,這個緩沖塊怎麽才能把數據寫入磁盤中。答案是在緩沖頭結構中存儲了塊設備號和緩沖數據的邏輯塊號,它們一起唯一確認了緩沖塊數據對應的塊設備和數據塊。並且為了快速的在緩沖區中查看數據塊是否在緩沖區中,使用了hash表結構以及空閑緩沖塊隊列來進行操作和管理,在linux0.11中采用的散列函數是:#define _hash(dev, block) ((unsigned)(dev^block))%NR_HASH

。NR_HASH是哈希數組的長度。結構如圖:

技術分享圖片
在圖中,雙向箭頭代表哈希在同一個表項的雙向鏈表指針,對應b_prevb_next字段。虛線表示當前連接在空閑緩沖塊鏈表中空閑緩沖塊之間的鏈表指針,free_list是空閑鏈表的頭指針。
在內核中使用getblk()函數來獲取合適的緩沖塊。該函數調用get_hash_table()函數,在hash表中確認指定設備號和邏輯塊號的緩沖塊是否存在,如果存在的話,直接返回對應的緩沖頭結構的指針。否則,會從空閑鏈表頭開始對整個空閑鏈表進行掃描,尋找可以用的空閑緩沖區。有可能可以用的有多個空閑緩沖區,這時就要根據緩沖頭結構的修改標誌和鎖定標誌組合而成的權值,來判斷哪個空閑塊最合適。如果找到的空閑塊即沒有被修改也沒有被鎖定,那就使用該空閑塊了

。如果沒有找到空閑塊,那就讓當前進程進入睡眠,等到繼續執行時再次尋找。如果該空閑塊被鎖定,那麽當前進程也需要進入睡眠,等待其它進程解鎖。如果在睡眠等待的過程中,該緩沖塊又被其它進程占用,就需要重新開始搜索緩沖塊了。如果沒被其它進程占用,就判斷該緩沖塊是否已被修改(還未寫到盤中),如果已被修改,就將該塊寫盤,並等待該塊解鎖。此時如果該緩沖塊又被其它進程占用,就又只有重新找空閑的緩沖塊了。這裏還有一種意外的情況,那就是在當前進程睡眠時,其它進程把我們需要的緩沖塊已經加入了hash隊列中,所以需要最後搜索一次hash隊列,如果在hash隊列中找到了該緩沖塊,就只好重新執行以上操作了。最後,得到了一塊沒有被進程引用也沒有被上鎖以及沒有被修改的空閑緩沖塊,將該塊的引用計數置1,並復位其它的標誌,把該緩沖頭結構從空閑表中取出。在設置了該緩沖塊所屬的設備號和相應的邏輯號後,再把該緩沖頭結構放入hash表對應表項頭部和空閑隊列隊尾。最後,返回該緩沖塊頭的指針。流程圖如圖:
技術分享圖片

getblk函數返回的可能是一個新的空閑塊,也可能是含有我們需要的數據的緩沖塊。因此對於讀取數據塊操作函數bread(),就需要判斷該緩沖塊的更新標誌,已知道所含數據是否有效,如果有效則直接返回給進程,否則就調用底層塊讀寫函數ll_rw_block(),並同時睡眠,等待數據從物理設備寫入緩沖塊,醒了之後再重新判斷是否有效,如果還是不行,那就釋放該緩沖塊,並返回NULL。流程圖如圖:
技術分享圖片
當程序想要釋放一個緩沖塊時,調用brelse()函數,釋放緩沖塊並喚醒因為等待該緩沖塊而進入睡眠的進程。

最後,除了驅動程序之外,其它上層程序對塊設備的讀寫操作都需要經過高速緩沖區管理程序來實現數據的讀寫。它們之間的聯系主要是通過bread()函數和ll_rw_block()函數實現。如圖所示:

技術分享圖片

init/main.c部分代碼

  1. memory_end = (1<<20) + (EXT_MEM_K<<10);
  2. memory_end &= 0xfffff000;
  3. if (memory_end > 16*1024*1024)
  4. memory_end = 16*1024*1024;
  5. if (memory_end > 12*1024*1024) //內存>12M 設置高速緩沖區大小4M
  6. buffer_memory_end = 4*1024*1024;
  7. else if (memory_end > 6*1024*1024) // 內存>6M 設置高速緩沖區大小2M
  8. buffer_memory_end = 2*1024*1024;
  9. else
  10. buffer_memory_end = 1*1024*1024; //否則設置高速緩沖大小1M
  11. main_memory_start = buffer_memory_end;
  12. ifdef RAMDISK
  13. main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
  14. endif

/fs/buffer.c中初始化函數buffer_init()

  1. struct buffer_head *h = start_buffer;
  2. void *b;
  3. int i;
  4. if (buffer_end == 1<<20) //如果內存末端為1M,則要減掉顯存和BIOS所占的內存640K--1M之間
  5. b = (void *) (640*1024);
  6. else
  7. b = (void *) buffer_end;
  8. // 這段代碼用於初始化緩沖區,建立空閑緩沖區環鏈表,並獲取系統中緩沖塊的數目。
  9. // 操作的過程是從緩沖區高端開始劃分1K 大小的緩沖塊,與此同時在緩沖區低端建立描述該緩沖塊
  10. // 的結構buffer_head,並將這些buffer_head 組成雙向鏈表。
  11. // h 是指向緩沖頭結構的指針,而h+1 是指向內存地址連續的下一個緩沖頭地址,也可以說是指向h
  12. // 緩沖頭的末端外。為了保證有足夠長度的內存來存儲一個緩沖頭結構,需要b 所指向的內存塊
  13. // 地址 >= h 緩沖頭的末端,也即要>=h+1。
  14. while ((b -= BLOCK_SIZE) >= ((void *) (h + 1)))
  15. {
  16. h->b_dev = 0; // 使用該緩沖區的設備號。
  17. h->b_dirt = 0; // 臟標誌,也即緩沖區修改標誌。
  18. h->b_count = 0; // 該緩沖區引用計數。
  19. h->b_lock = 0; // 緩沖區鎖定標誌。
  20. h->b_uptodate = 0; // 緩沖區更新標誌(或稱數據有效標誌)。
  21. h->b_wait = NULL; // 指向等待該緩沖區解鎖的進程。
  22. h->b_next = NULL; // 指向具有相同hash 值的下一個緩沖頭。
  23. h->b_prev = NULL; // 指向具有相同hash 值的前一個緩沖頭。
  24. h->b_data = (char *) b; // 指向對應緩沖區數據塊(1024 字節)。
  25. h->b_prev_free = h - 1; // 指向鏈表中前一項。
  26. h->b_next_free = h + 1; // 指向鏈表中下一項。
  27. h++; // h 指向下一新緩沖頭位置。
  28. NR_BUFFERS++; // 緩沖區塊數累加。
  29. if (b == (void *) 0x100000) // 如果地址b 遞減到等於1MB,則跳過384KB,
  30. b = (void *) 0xA0000; // 讓b 指向地址0xA0000(640KB)處。
  31. }
  32. h--; // 讓h 指向最後一個有效緩沖頭。
  33. free_list = start_buffer; // 讓空閑鏈表頭指向頭一個緩沖區頭。
  34. free_list->b_prev_free = h; // 鏈表頭的b_prev_free 指向前一項(即最後一項)。
  35. h->b_next_free = free_list; // h 的下一項指針指向第一項,形成一個環鏈。
  36. // 初始化hash 表(哈希表、散列表),置表中所有的指針為NULL。
  37. for (i = 0; i < NR_HASH; i++)
  38. hash_table[i] = NULL;

Linux高速緩沖區原理