前面的博文中詳細講述了xv6的檔案系統,其中使用位元影像塊來進行磁碟block的 管理,但是對於block內容進行讀寫則需要更底層的磁碟驅動程式,同時考慮到磁碟讀寫的速度非常慢(相對於記憶體讀寫),因此我們有必要對磁碟的資料區塊進行緩衝。
整個磁碟的緩衝是採用類似對象池(object Pool)的架構來實現的。
對於每個磁碟block,對應一個對應的資料結構buffer,具體如下:
truct buf {<br /> int flags; //B_BUSY=1, B_VALID=2, B_DIRTY=4<br /> uint dev;<br /> uint sector;<br /> struct buf *prev; // block cache list<br /> struct buf *next;<br /> struct buf *qnext; // ide queue<br /> uchar data[512];<br />};
通過dev和sector就可以唯一確定該block的地址,從而進行讀寫。讀寫的粒度為block(512位元組),相應的資料儲存在data中。每個buffer含有一個標誌flags,如果為B_BUSY說明這個buffer正在被一個進程使用,如果B_VALID則說明buffer中的資料已經準備好了,B_DIRTY說明是讀取磁碟資料還是將資料寫入磁碟。
一、磁碟驅動的實現
類似,xv6維持了一個磁碟請求隊列idequeue。它每次將頭部的buf發送到磁碟裝置,而每次新加入的buffer放在隊列的尾部。
根據buffer中的中繼資料及標誌來讀寫扇區,對應的函數為iderw,它將新添加的buffer添加到請求隊列的末尾,並且這個進程因為這個buffer進入睡眠狀態。
iderw的關鍵代碼如下所示:
void<br />iderw(struct buf *b)<br />{<br /> struct buf **pp;</p><p> acquire(&idelock);</p><p> // 將這個buffer加入請求隊列idequeue的末尾<br /> b->qnext = 0;<br /> for(pp=&idequeue; *pp; pp=&(*pp)->qnext)<br /> ;<br /> *pp = b;</p><p> // 如果原來隊列為空白,則立即處理這個buffer<br /> if(idequeue == b)<br /> idestart(b);</p><p> // 等待操作完成<br /> while((b->flags & (B_VALID|B_DIRTY)) != B_VALID)<br /> {<br /> sleep(b, &idelock);<br /> }</p><p> release(&idelock);<br />}<br />
其中將buffer的資料及標誌發送到磁碟裝置的代碼idestart如下:
static void<br />idestart(struct buf *b)<br />{</p><p> idewait(0);<br /> outb(0x3f6, 0); // generate interrupt<br /> outb(0x1f2, 1); // number of sectors<br /> outb(0x1f3, b->sector & 0xff);<br /> outb(0x1f4, (b->sector >> 8) & 0xff);<br /> outb(0x1f5, (b->sector >> 16) & 0xff);<br /> outb(0x1f6, 0xe0 | ((b->dev&1)<<4) | ((b->sector>>24)&0x0f));<br /> if(b->flags & B_DIRTY)<br /> {<br /> //需要寫資料<br /> outb(0x1f7, IDE_CMD_WRITE);<br /> outsl(0x1f0, b->data, 512/4);<br /> }<br /> else<br /> {<br /> //需要讀資料<br /> outb(0x1f7, IDE_CMD_READ);<br /> }<br />}
如果操作時讀取,則在磁碟控制卡資料準備好時會觸發一個中斷通知中斷處理常式擷取資料;如果操作時寫入,則在資料成功寫入後觸發一個中斷
二、磁碟中斷處理常式
xv6磁碟中斷處理常式調用ideintr來進程處理。它通過查詢請求隊列idequeue的第一個buffer得知已發生的操作。如果是讀取操作,則將磁碟控制卡中已經準備好的資料讀到data中。現在buffer中的資料已經準備好了,則設定為B_VALID並清除B_DIRTY,並且喚醒因為這個buffer而進入睡眠狀態的進程。接著ideintr傳遞請求隊列中的下一個buffer到磁碟裝置進行處理。
void<br />ideintr(void)<br />{<br /> struct buf *b;</p><p> // 擷取idequeue中的第1個buf<br /> acquire(&idelock);<br /> if((b = idequeue) == 0)<br /> {<br /> release(&idelock);<br /> cprintf("Spurious IDE interrupt./n");<br /> return;<br /> }<br /> idequeue = b->qnext;</p><p> // 看看是否需要讀取資料<br /> if(!(b->flags & B_DIRTY) && idewait(1) >= 0)<br /> insl(0x1f0, b->data, 512/4);</p><p> // 喚醒正在等待這個buf的進程<br /> b->flags |= B_VALID;<br /> b->flags &= ~B_DIRTY;<br /> wakeup(b);</p><p> // 如果idequeue非空,則繼續處理下一buf<br /> if(idequeue != 0)<br /> idestart(idequeue);</p><p> release(&idelock);<br />}
三、磁碟緩衝
xv6使用緩衝來同步各個進程之間訪問磁碟block,它通過bread中獨佔訪問來實現:即如果有兩個進程通過bread同時讀取具有相同dev和sector的未使用的磁碟block,那麼其中一個進程會立即返回一個已鎖住的buffer,而另一個進程則需要等待前一個進程訪問完後調用brelse時
發送的解鎖訊號。
bread非常簡單,代碼如下:
struct buf*<br />bread(uint dev, uint sector)<br />{<br /> struct buf *b;</p><p> b = bget(dev, sector);<br /> if(!(b->flags & B_VALID))<br /> iderw(b);<br /> return b;<br />}
bread調用bget來擷取並返回給定dev和sector的一個已鎖住的buffer。任何時候只有一個進程訪問bcache,並且只有成功得到一個已鎖住的buffer才釋放這個自旋鎖,從而保證進程訪問磁碟的同步性。
它採用的演算法非常精妙:
1. 在bcache中使用next指標(見最上面的圖)從前往後掃描緩衝bcache,嘗試找到一個和給定資料相同的buffer。若有並已經被別的進程使用(B_BUSY置位),則因為這個buffer進入睡眠狀態,否則,置位B_BUSY,從而返回一個已鎖住的buffer。若沒有找到,則進入2
2. 在bcache中使用pre指標(見最上面的圖)從後往前掃描緩衝bcache,嘗試找到一個未被使用的buffer(B_BUSY=0),如果找到則修改相應的dev和sector,並且置位B_BUSY,並且置位B_VALID和B_DIRTY位,表明bread需要重新整理buffer中的資料而不是使用先前buffer中的資料。
--->需要特別注意的是如果找到了但是該buffer已使用而進入睡眠的進程,後來被喚醒了,則需要從新來過,因為有可能這個buffer被複用到別的dev或sector了,只好重新來過咯
整個過程如下:
static struct buf*<br />bget(uint dev, uint sector)<br />{<br /> struct buf *b;</p><p> acquire(&bcache.lock);</p><p> loop:<br /> // 從前往後,嘗試尋找一個和給定扇區相同的buffer<br /> for(b = bcache.head.next; b != &bcache.head; b = b->next)<br /> {<br /> if(b->dev == dev && b->sector == sector) /找到這樣的buffer<br /> {<br /> if(!(b->flags & B_BUSY)) //如果未被使用則設定為B_BUSY,並返回<br /> {<br /> b->flags |= B_BUSY;<br /> release(&bcache.lock);<br /> return b;<br /> }<br /> sleep(b, &bcache.lock); //如果已被使用則睡眠,等待別的進程釋放buffer<br /> goto loop;<br /> }<br /> }</p><p> // 如果找不到這樣的buffer,則從後往前找一個不在使用的buffer,並修改相應的資訊<br /> for(b = bcache.head.prev; b != &bcache.head; b = b->prev)<br /> {<br /> if((b->flags & B_BUSY) == 0)<br /> {<br /> b->dev = dev;<br /> b->sector = sector;<br /> b->flags = B_BUSY;<br /> release(&bcache.lock);<br /> return b;<br /> }<br /> }<br /> panic("bget: no buffers");<br />}
一旦bread返回了一個已鎖住的buffer給調用者,那麼調用者就可以獨佔的使用buffer來進行讀寫操作。當調用者處理完這個buffer,必須使用brelse來釋放它,brelse將這個buffer移動來列表的最前面,清除B_BUSY位並喚醒其他因為這個buffer而進入睡眠的進程。這個演算法就是經典的LRU演算法,它的精妙之處在於考慮到程式的局部性原理:因為這樣的移動,使得icache中最前面的buffer是最近使用的,而最後面的是最久未使用,從而從前往後尋找最近使用的buffer,從後往前尋找最久未使用的buffer,效率非常高。
四、思考及小結
如果一個block緩衝,從磁碟中讀取時同步還是非同步?並簡述過程
這是一個非同步過程,其過程如下:
1)首先調用bread,在icache中找到一個未被使用(B_BUSY)的buffer。
2) 修改該buffer的dev和sector,共置位B_BUSY,從而返回一個已鎖住的buffer
3)將該buffer加入到請求列表idequeue的末尾,並因為這個buffer而進入睡眠狀態
4)當輪到改buffer時,idestart將該buffer的資料及標誌發送到磁碟裝置
5)磁碟裝置處理完成後,產生一個中斷
6)磁碟中斷處理常式ideintr從磁碟控制卡讀取已準備好的資料到data中,並設定buffer的標誌位B_VALID,並喚醒因為這個buffer而進入睡眠的進程
7)調用者處理完buffer後,最後調用brelse釋放這個buffer。並喚醒因為這個buffer而進入睡眠的進程。