linux核心分析筆記—-塊I/O層

來源:互聯網
上載者:User

      如果您記性好的話,應該記得我在linux裝置驅動執行個體帖中說的最多的就是字元裝置驅動程式,那麼今天的塊I/O層是一個和字元裝置驅動相對應的裝置。兩者最根本的區別就是看它們能否被隨機訪問,換句話說就是看它們能否在訪問裝置時從一個位置隨意地調到另外一個位置,如果可以就是塊裝置,否則就字元裝置。

      塊裝置中最小的可定址單元是扇區。扇區的大小一般是2的整數倍,最常見的大小是512個位元組。扇區的大小是裝置的物理屬性,扇區是所有塊裝置的基本單元,塊裝置無法對比它還小的單元進行定址和操作,不過許多塊裝置能夠一次就傳輸多個扇區。從軟體角度來講,最小的邏輯可定址單元卻是塊,塊是檔案系統的一種抽象-----只能基於塊來訪問檔案系統。雖然物理磁碟定址是按照扇區級進行的,但是核心執行的所有磁碟操作都是按照塊進行的。前邊已經說過,扇區是裝置的最小可定址單元,所以塊不能比扇區還小,只能數倍於扇區大小。另外核心還要求塊大小是2的整數倍,=而且不能超過一個頁的長度,所以大小的最終要求是,必須是扇區大小的2的整數倍,並且要小於頁面大小。所以通常塊大小是512位元組,1k或4k。

當一個塊被調入記憶體時,它要儲存在一個緩衝區中,每個緩衝區與一個塊對應,它相當於是磁碟塊在記憶體中的表示。另外,由於核心在處理資料時需要一些相關的控制資訊,所以每個緩衝區都有一個叫做buffer_head的描述符來表示,被稱為緩衝區頭,在linux/buffer_head.h中定義,它包含了核心操作緩衝區所需要的全部資訊,如下:

struct buffer_head {        unsigned long        b_state;          /* buffer state flags */        atomic_t             b_count;          /* buffer usage counter */        struct buffer_head   *b_this_page;     /* buffers using this page */        struct page          *b_page;          /* page storing this buffer */        sector_t             b_blocknr;        /* logical block number */        u32                  b_size;           /* block size (in bytes) */        char                 *b_data;          /* buffer in the page */        struct block_device  *b_bdev;          /* device where block resides */        bh_end_io_t          *b_end_io;        /* I/O completion method */        void                 *b_private;       /* data for completion method */        struct list_head     b_assoc_buffers;  /* list of associated mappings */};

      其中的b_state域表示緩衝區的狀態,下表給出一種標誌或多種標誌的組合,在linux/buffer_head.h中定義了所有合法標誌的bh_state_bite列表,如下所示:

    

      bh_state_bits列表包含了一個特殊標誌----BH_PrivateStart,該標誌不是可用狀態標誌,使用它是為了指明可能其它代碼使用的起始位。塊I/O層不會使用BH_PrivateStart或更高的位,那麼某個驅動程式希望通過b_state域儲存資訊時就可以安全地使用這些位。驅動程式可以在這些位中定義自己的狀態標誌,只要保證自訂的狀態標誌不會與塊IO層的專用位發生衝突就可以了。b_count域表示緩衝區的使用計數,可通過兩個定義在檔案linux/buffer_head.h中的內嵌函式對此域進行增減:

static inline void get_bh(struct buffer_head *bh){        atomic_inc(&bh->b_count);}static inline void put_bh(struct buffer_head *bh){        atomic_dec(&bh->b_count);}

      在操作緩衝區頭之前,應該先使用get_bh()函數增加緩衝區頭的引用計數,確保緩衝區頭不會再被分配出去,當完成對緩衝區頭的操作之後,還必須使用put_bh()函數減少引用計數。與緩衝區對應的磁碟物理塊由b_blocknr域索引,該值是b_bdev域指明的塊裝置中的邏輯塊號。與緩衝區對應的記憶體物理頁由b_page域表示,另外,b_data域直接指向相應的塊(它位於b_page域所指明的頁面的某個位置上),塊的大小由b_size域表示,所以塊在記憶體中的起始位置在b_data處,結束位置在(b_data+b_size)處。緩衝區頭的目的在於描述磁碟塊和實體記憶體緩衝區(在特定頁面上的位元組序列)之間的映射關係。這個結構體在核心中扮演一個描述符的角色,說明從緩衝區到塊的映射關係。使用緩衝區頭作為I/O操作有它的弊端,這裡不細說,你明白就好。我們只需知道現在的核心採用了一種新型,靈活而且輕量級的容器---bio結構體。

      bio結構體定義在linux/bio.h中,該結構體代表了正在現場的(活動)以片斷(segment)鏈表形式組織的塊I/O操作。一個片斷是一小塊連續的記憶體緩衝區。這樣的話,就不需要保證單個緩衝區一定要連續,所有通過片斷來描述緩衝區,即使一個緩衝區分散在記憶體的多個位置上,bio結構體也能保證I/O操作的執行。下面給出bio結構體和各個域的描述,如下:

struct bio {        sector_t             bi_sector;         /* associated sector on disk */        struct bio           *bi_next;          /* list of requests */        struct block_device  *bi_bdev;          /* associated block device */        unsigned long        bi_flags;          /* status and command flags */        unsigned long        bi_rw;             /* read or write? */        unsigned short       bi_vcnt;           /* number of bio_vecs off */        unsigned short       bi_idx;            /* current index in bi_io_vec */        unsigned short       bi_phys_segments;  /* number of segments after coalescing */        unsigned short       bi_hw_segments;    /* number of segments after remapping */        unsigned int         bi_size;           /* I/O count */        unsigned int         bi_hw_front_size;  /* size of the first mergeable segment */        unsigned int         bi_hw_back_size;   /* size of the last mergeable segment */        unsigned int         bi_max_vecs;       /* maximum bio_vecs possible */        struct bio_vec       *bi_io_vec;        /* bio_vec list */        bio_end_io_t         *bi_end_io;        /* I/O completion method */        atomic_t             bi_cnt;            /* usage counter */        void                 *bi_private;       /* owner-private method */        bio_destructor_t     *bi_destructor;    /* destructor method */};

      使用bio結構體的目的主要是代表正在現場執行的I/O操作,所有該結構體中的主要域都是用來管理相關資訊的。其中最重要的幾個域是bi_io_vecs,bi_vcnt和bi_idx.它們之間的關係如所示:

     

      我在前邊已經給出了struct bio的結構體,下面給出struct bio_vec的描述:

struct bio_vec {        struct page     *bv_page;   /* pointer to the physical page on which this buffer resides */         unsigned int    bv_len;     /* the length in bytes of this buffer */         unsigned int    bv_offset;   /* the byte offset within the page where the buffer resides */ };

      下面來分析以上上面的那個圖,我們說:每一個塊I/O請求都通過一個bio結構體表示。每個請求包含一個或多個塊,這些Block Storage在bio_vec結構體數組中,這些結構體描述了每個片斷在物理頁中的實際位置,並且像向量一樣地組織在一起,IO操作的第一個片斷由b_io_vec結構體所指向,其他的片斷在其後依次放置,共有bi_vcnt個片斷。當塊IO開始執行請求,需要使用各個片段時,bi_idx域會不斷更新,從而總指向當前片斷。bi_idx域指向數組中的當前bio_vec片段,塊IO層通過它跟蹤塊IO操作的完成進度。但該域更重要的作用是分割bio結構體。bi_cnt域記錄bio結構體的使用計數,如果為0,則應該銷毀該bio結構體,並釋放它佔用的記憶體。通過下面兩個函數管理使用計數:

void bio_get(struct bio *bio);void bio_put(struct bio *bio);

      最後一個域是bi_private域,這是一個屬於擁有者的私人域,誰建立了bio結構,誰就可以讀寫該域。

      塊裝置將它們掛起的塊IO請求儲存在請求隊列中,該隊列有request_queue結構體體表示,定義在檔案linux/blkdev.h中,包含一個雙向請求鏈表以及相關控制資訊。通過核心中想檔案系統這樣高層的代碼將請求加入到隊列中。請求隊列只要不為空白,隊列對應的塊裝置驅動程式就會從隊列頭擷取請求,然後將其送入對應的塊裝置上去請求隊列表中的每一項都是一個單獨的請求,有reques結構體體表示。隊列中的請求由結構體request表示,定義在檔案linux/blkdev.h表示。因為一個請求可能要操作多個連續的磁碟塊,所有每個請求可有由多個bio結構體組成,注意,雖然磁碟上的塊必須連續,但是在記憶體中的這些塊並不一定要連續----每個bio結構都可以描述多個片段,而每個請求也可以包含多個bio結構體。

      好了,我們明白了塊IO請求,下面的就是IO調度了。每次定址的操作就是定位磁碟磁頭到特定塊上的某個位置,為了最佳化定址操作,核心既不會簡單地按請求接收次序,也不會立即將其提交給磁碟,相反,它會在提交前,先執行名為合并與排序的預操作,這種預操作可以極大地提高系統的整體效能。

      IO發送器通過兩種方法減少磁碟定址時間:合并與排序。合并指將兩個或多個請求結合成一個一個新請求。關於排序的,最有名的當然就是大名鼎鼎的電梯調度。排序就是整個請求隊列將按扇區增長方向有序排列,使所有請求按磁碟上扇區的排列順序有序排列的目的不僅是為了縮短單獨一次請求的定址時間,更重要的最佳化在於,通過保持磁碟頭以直線方向移動,縮短了所有請求的磁碟定址的時間。關於linux中的電梯發送器,很多作業系統的書上都已經說的很明白,我這裡給出一個大致流程:

1.首先,如果隊列中已存在一個對相鄰磁碟扇區操作的請求,那麼新請求將和這個已經存在的請求合并為一個請求。
2.如果隊列中存在一個駐留時間過長的請求,那麼新請求將被插入到隊列尾部,以防止其他舊的請求發生饑餓。
3.如果隊列中以扇區方向為序存在合適的插入位置,那麼新的請求將被插入到該位置,保證隊列中的請求是以被訪問磁碟物理位置為序進行排序的。
4.如果隊列中不存在合適的請求插入位置,請求將被插入到隊列尾部。

      我前邊提到過電梯調度,但是有一個問題一直沒提,那就是電梯發送器的缺點:饑餓。出於減少磁碟定址時間的考慮,對某個磁碟地區上的繁重操作,無疑會使得磁碟其他位置上的操作得不到運行機會,實際上,一個對磁碟同一位置操作的請求流可以造成較遠位置的其他請求永遠得不到運行機會,這是一種很不公平的饑餓現象。更糟糕的是,普通的請求饑餓還會帶來寫--饑餓--讀這種特殊問題。我們知道寫操作通常發生在核心有空時,而讀操作卻必須阻塞知道讀請求被滿足,這對系統效能影響是非常大的。而且我們知道讀請求往往相互依靠,比如要讀大量的檔案,每次都是針對一塊很小的緩衝區進行讀操作,而應用程式只有將上一個資料區域從磁碟中讀取並返回之後,才能繼續讀取下一個資料區,所以如果每一次請求都發生饑餓現象,那麼對讀取檔案的應用程式來說,全部延遲加起來會造成過長的等待時間。減少饑餓請求必須以降低全域輸送量為代價。為了避免這種問題,提出了期限IO發送器,既要盡量提高全域輸送量,又要使請求得到公平處理。在期限IO發送器中,每個請求都有一個逾時時間。預設情況下,讀請求的逾時時間是500ms,寫請求的逾時時間是5s。期限IO調度請求類似與linux電梯,也以磁碟物理位置為次序維護請求隊列,這個隊列被稱為排序隊列。當一個新請求遞交給排序隊列時,期限IO發送器類似於linux電梯,合并和插入請求,但是期限IO發送器同時也會以請求類型為依據將它們插入到額外隊列中。讀請求按次序被插入到特定的讀FIFO隊列中,寫請求被插入到特定的寫FIFO隊列中。雖然普通隊列以磁碟扇區為序進行排序,但是這些隊列是以FIFO形式組織的,結果新隊列總是被加入到隊列尾部。對於普通操作來說,期限IO調度將請求從排序隊列的頭部去下,再推入到派發隊列中,派發隊列然後將請求提交給磁碟驅動,從而保證了最小化的請求定址。如果在寫FIFO隊列頭,或是在讀FIFO隊列頭的請求逾時,那麼期限IO發送器便從FIFO隊列中提取請求進行服務。依靠這種方法,期限IO發送器試圖保證不會發生有請求在明顯超期的情況下仍不能得到服務的現象,如所示:

    

      期限IO發送器的實現在檔案driver/block/deadline-iosched.c中。

      雖然期限IO發送器為降低讀操作回應時間做了許多工作,但同時也降低了系統輸送量。考慮這樣的情況,假設一個系統正處於很繁重的寫操作期間,每次提交新請求,IO發送器都會迅速處理讀請求,這樣磁碟會首先為讀操作定址,執行讀操作,然後返回再定址進行寫操作,並且對每個讀操作都重複這個過程。這種做法明顯損害了系統全域輸送量。這事就有了預測IO發送器。它的基礎就是期限IO發送器。最主要的改進是它增加了預測啟發能力。它的不同之處在於讀操作提交後並不直接返回處理其他請求,而是會有意空閑片刻。這閒置幾秒鐘,對應用程式來說是個提交其他讀請求的好機會-----任何對相鄰磁碟位置操作的請求都會立刻得到處理。在等待時間結束後,預測IO發送器重新返回原來的位置,繼續執行以前剩下的請求。要注意,如果等待可以減少讀請求所帶來的向後再向前(back-and-forth)定址操作,那麼完全值得花一些時間來等待更多的請求(這裡的時間花在對更多請求的預測上),如果一個相鄰的IO請求在等待期帶來,那麼IO發送器可以節省兩次定址操作。如果存在愈來愈多的訪問同樣地區的讀請求到來,那麼片刻等待無疑會避免大量的定址操作。當然,不得不說,如果沒有IO請求在等待期到來,那麼預測IO發送器會給系統效能帶來輕微的損失,浪費掉幾毫秒。預測發送器所帶來的優勢在於能否正確預測應用程式和檔案系統的行為。這種預測依靠一系列的啟發和統計工作。預測IO發送器的實現在檔案driver/block/as-iosched.c中。塊裝置使用哪個IO發送器是可以選擇的。預設的IO發送器就是預測IO發送器。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.