1. 程式人生 > >(萊昂氏unix原始碼分析導讀-36) 快取管理(下)

(萊昂氏unix原始碼分析導讀-36) 快取管理(下)

                                      by cszhao1980

理解了上述內容,下面的這些程式就不難理解了。

首先是函式brelse(buf bp),該函式將傳入的快取歸還到AV佇列中,函式採用尾插法,

即快取會插到AV佇列的隊尾——這樣做顯然有助於提高“延遲寫”技術的效率。萊昂

特別指出,brelse沒有清理B_DONE標誌,這一點非常重要,讀完本章後大家就會明白。

接下來是binit()函式,完成初始化:

(1)         每個快取都被放入到空閒裝置佇列(bfreelist b佇列)中;(有趣的是,使用頭插法)

(2)         每個快取都通過呼叫brelse

放入到AV佇列中;

(3)         初始化每個裝置的b list——都為空(僅有頭結點)。

還有notavail()函式,其作用是將傳入的快取從AV佇列中取下來,併為快取設定B_BUSY

志,表示該快取已經被某裝置佔用,not available了。

clrbuf()函式就比較簡單了,它將快取區清0

incore()有兩個引數:裝置號(adev)和塊號(blkno),它會Loop該裝置的任務佇列(b佇列),

看是否已經為此塊分配了快取(b_blkno == blkno)。

getblk(dev, block)相對比較複雜,它的作用是為指定裝置的指定塊分配一個快取。

(1)    

它首先檢查該裝置的b佇列,看是否已經為此block分配過快取,如果有,則檢查B_BUSY標記;

                   i.     如置位,則設定B_WANTED標記後睡眠;醒來後,跳回程式開頭;

                  ii.     否則,呼叫notavail佔用此快取(注意,如果B_BUSY未置位,則此快取已經被歸還到AV隊列了);

(2)     如果沒有這樣的快取,則需要直接從(bfreelist的)AV佇列中分配。

                如果該佇列為空,則B_WANTED置位後睡眠;

(3)     否則,呼叫notavail(bfreelist->av_forw)

佔用AV佇列的第一個快取;

(4)     如該快取的B_DELWRI置位(“延遲寫”),則呼叫bwrite進行實際io,然後跳回程式開頭;

            【思考題】:該快取被從AV佇列中摘下來,誰負責歸還呢?

(5)         如否,則將該快取從原裝置b佇列中摘下來,放入新裝置的b佇列中。

getblk還有個簡單的用法,即getblk(NODEV)NODEV定義為-1getblk發現dev為負數時,就直接

AV佇列裡取一個快取下來(經過放後,它仍在bfreelistb佇列中)。由於此種情況下沒有

使用block引數,故呼叫時,可省略第二個引數的輸入。

下面看一下讀寫相關的函式,首先是bread(dev, block),它將指定裝置的指定塊讀入快取。

首先,它呼叫getblk來獲取一個快取;

然後,檢查該快取,如果B_DONE標記已經設定,則表示走了大運,無需再讀磁碟了,直接return即可;

如果沒這麼走運,則需要啟動裝置進行實際的讀操作,然後呼叫iowait等待操作完成。

【注】:記得末,brelse沒有清理B_DONE標誌。

【思考題】:這樣的盤塊從何而來?

令人疑惑的是,b_wcount的值是一個負數:-256Why?因為這個值會被直接設定給這與RK裝置的rkcs

存器,而該暫存器的內容是要傳輸word數的補碼。系統的這種寫法其實破壞了其一直努力實現的device

 independent性,rkcs暫存器的特殊要求應該由更底層的函式來實現。

bwrite()進行真實io,將指定快取的內容寫入裝置,對同步寫,它會等待io結束,然後呼叫brelse()

將快取歸還AV佇列。

bflush()函式大家都比較熟悉了,它會Loop指定裝置的任務(b)佇列,呼叫bwrite()函式將“延遲寫”

的快取寫入物理裝置。

現在,讓我們看一下正常的讀過程。根據前面的描述,其過程大致如下:

(1)         當程序要讀取磁碟盤塊時,會首先獲取一個buffer,啟動呼叫bread啟動裝置操作,

                   bread會呼叫iowait睡眠(以此buffer地址為原因);

(2)         裝置操作完成後,會啟動rkintr中斷處理程式,該程式會喚醒等待的程序;

(3)         程序讀取buffer的內容,然後呼叫brelse釋放該buffer

對於寫操作,也有類似的過程,其根本特點是程序會等待io完成,然後再進行下面的工作。

因此,它們也稱為同步io操作。

除了同步io之外,在讀碼過程中,我們還遇到了有關非同步(B_ASYNC)的程式碼,它們是做什麼的呢?

對於寫操作,比較好理解。非同步寫操作用於“延遲寫”技術——在真正寫時,也會呼叫bwrite()

該函式啟動裝置操作後,直接return即可,無需呼叫iowait等待。

讀操作的情形稍微複雜一些,它用於“預讀”。預讀用以提高效率,如果我們能預測下一次要讀取的盤塊

的話,我們可以預先讀取它,以提高效率。

breada()函式提供了預讀的功能,相比bread它多指定了一個引數:read ahead塊號,即要“提前/額外”讀

入的塊的塊號。它首先會以同步方式啟動“正選”塊的讀入,然後會以非同步方式去啟動裝置讀“提前”

塊——這很容易理解,因為在啟動讀操作時,還沒有程序等待此盤塊,因此,無法使用同步方式。

rkintr在處理非同步讀入的盤塊時,會直接呼叫brelse將其釋放到AV佇列(B_DONE仍設定),但在其被再

次分配之前,如果遇到相同盤塊的讀取請求,會直接返回該buffer,無需再次讀入——參見對bread函式的描述。

在這裡順便談一下unix v6的預讀演算法。在inode結構中有一個變數i_lastr,記錄的是上一次讀的盤塊號,如

果本次讀的盤塊號正好為i_lastr+1,我們有理由相信,下次讀操作有很大機會是讀取i_lastr+2號盤塊,因此,

就可以呼叫breada進行預讀。

我們沒有討論所謂的原始(raw)輸入輸出函式physio——它的解析就留給讀者吧。