這一節講述raid5模塊中處理讀寫流程。這個過程很複雜,最關鍵的函數就是handle_stripe,處理一次讀或寫都會多次調用這個函數才能完成。當然,這個函數也是raid5模塊的一個核心函數,他還負責同步,重建,以及擴展的實現。在分析之前,我們需要準備一些預備知識:
一、條帶:我們知道,raid5是以條帶爲基本單位來存取數據的。如下圖所示:
raid5還有其它中數據分佈方式,這裏只列出一種。圖中的block0,block1,block2等這些數據塊在邏輯上是連續的。
值得注意的是,MD中raid5處理數據的最小單位是一個由大小4KB組成的小條帶,即一個頁大小,並不是一次處理一個block大小的條帶。以後出現的條帶均指的是這個由4KB組成的小條帶。這個數據結構如下:
- struct stripe_head {
- struct hlist_node hash;
- struct list_head lru; /* inactive_list or handle_list */
- struct raid5_private_data *raid_conf;
- sector_t sector; /* sector of this row */
- int pd_idx; /* parity disk index */
- unsigned long state; /* state flags */
- atomic_t count; /* nr of active thread/requests */
- spinlock_t lock;
- int bm_seq; /* sequence number for bitmap flushes */
- int disks; /* disks in stripe */
- struct r5dev {
- struct bio req;
- struct bio_vec vec;
- struct page *page;
- struct bio *toread, *towrite, *written;
- sector_t sector; /* sector of this page */
- unsigned long flags;
- } dev[1]; /* allocated with extra space depending of RAID geometry */
- };
hash:條帶在緩衝中hash表項。
lru:條帶所處在那個鏈表中
sector:條帶所處的扇區號,這個扇區號是從單個磁盤的起始地址算起的一個偏移量。
state:條帶的狀態位
r5dev:條帶中描述每個設備的緩衝區,這個結構體是處理io的最小單位,命令經計分解算之後,會被加入相應條帶中相應r5dev中的鏈表中,也就是結構體中toread,towrite鏈表中(通過bio->bi_next連接起來)。在這個結構體中字段req代表請求bio,vec代表bio中的段,sector的意義說該r5dev在陣列中的邏輯扇區號。flags字段代表設備緩衝區的狀態。這些狀態可以在raid5.h中找到.
二、條帶中設備緩衝區的狀態
前面所說的r5dev中的flags字段,其中兩位很重要,即
#define R5_UPTODATE 0 /* page contains current data */
#define R5_LOCKED 1 /* IO has been submitted on "req" */
這兩位可以代表緩衝區的4中狀態,依次是:empty (!R5_UPTODATE !R5_LOCKED )表明緩衝區爲空
want (R5_LOCKED !R5_UPTODATE )表明緩衝區要請求數據
clean (!R5_LOCKED R5_UPTODATE )表明緩衝區中的數據與磁盤上的一致。
dirty(R5_LOCKED R5_UPTODATE )表明緩衝區有有新的數據要寫入磁盤中。
在數據的讀寫過程中,緩衝區的狀態會伴隨着改變,這些將會在以後中體現。
接下來我們看看make_request函數,它的功能是實現了請求的重新分發,確定了bio會加入到那個條帶的那個設備讀寫鏈表中。具體過程如下:
(先略過代碼段
- if (rw == READ &&mddev->reshape_position ==
- MaxSector &&chunk_aligned_read(q,bi))
- return 0;
這段意義以後在說)
a、調用md_write_start函數,該函數判斷是否需要更新元數據。由於一些raid算法帶有冗餘特性,比如raid1,raid5,那麼開始寫數據時,就要進行更新元數據,這防止寫數據不成功導致數據不正確,在陣列再次啓動時就會發起同步操作。寫完之後還要更新元數據。MD是通過in_sync字段來判斷的。如果in_sync=1,說明陣列是同步的。這時就有一個疑問,不能來一次寫請求就更新二次元數據吧?確實,md爲了防止這樣事情發生,引入了一個定時器,即200ms內沒有連續的寫請求發過來就更新元數據。這個功能通過safemode值的重載來完成。
b、計算bio的起始邏輯扇區號logical_sector和最後扇區號last_sector,這裏的logical_sector計算方式爲logical_sector = bi->bi_sector & ~((sector_t)STRIPE_SECTORS-1);其意義是將bio的起始扇區號對齊到條帶,即如果bi_sector=6的話,那麼logical_sector=0.
c、進入循環,對於每個logical_sector,首先判斷其是否正在擴容(conf->expand_progress!=MaxSector)目的是支持在線擴容,即陣列擴容的時候可以訪問陣列.如果正在擴容,還要判斷logical_sector是否在正在擴容的區間內,如果是的話則休眠,否則根據情況進行處理。這裏不做過多描述,在擴容的時候在說。
d、通過函數raid5_compute_sector計算logical_sector所在磁盤相對於開始位置的偏移量new_sector, 同時這個函數還確定了logical_sector所在條帶的設備號dd_index和校驗盤號pd_index.
e、根據第三步計算的new_sector的值來獲取條帶,函數get_active_stripe首先判斷該條帶是否在條帶緩衝區中,如果在的話直接返回。否則試圖從inactive_list中尋找不活動的條帶,如果找到則調用init_stripe來初始化,否則休眠。這個函數的第三個參數代表是否通過非阻塞方式來獲取活動條帶。對於讀寫,我們發現第3個參數爲(bi->bi_rw&RWA_MASK),即處理讀寫時使用的阻塞的方式來獲取條帶,即保證了在讀寫的時一定要能獲取到條帶。如果找到了符合條件的條帶,如果在陣列在擴展中會判斷是否需要重試這個條帶。因爲在獲取條帶的過程中可能引起休眠,就會導致原來logical_sector>expand_prograss變成了logical_sector<expand_prograss,。如果還按之前計算的new_sector進行處理的話,那麼會造成錯誤。所以這裏要retry.
f、之後通過add_stripe_bio將這個bio插入到條帶中,注意,bio會被插入到多個條帶中,當每個條帶均處理完成時,這個bio也就處理完了,這個計數器是由bi_phys_segments來維護的。當bio插入到一個條帶中bi_phys_segments++,當處理完一個條帶bi_phys_segments--。這個函數還會判斷要插入的bio是否覆蓋整個r5dev,即是否爲滿寫,這個判斷在處理寫請求(以rcw方式)時有用。
g、此時,我們已經把bio加入到了該加入的條帶中,之後就要處理這個條帶。這個功能由handle-stripe函數來完成。由於這個函數很複雜,我會在下一節中單獨做分析。
h、handle_stripe函數結束之後,會release_stripe函數,這個函數的功能就是根據條帶的狀態將調到加入到不同的鏈表中以便之後繼續使用。
下一節我會以一次簡單的讀寫來分析handle_stripe函數。讀比較簡單,寫過程較麻煩,涉及到了延遲寫。