《Linux內核設計與實現》讀書筆記(十四)- 塊I/O層

最近太忙,竟然過了2個月才更新第十四章。。。。linux

主要內容:ios

  • 塊設備簡介
  • 內核訪問塊設備的方法
  • 內核I/O調度程序

 

1. 塊設備簡介

I/O設備主要有2類:算法

  • 字符設備:只能順序讀寫設備中的內容,好比 串口設備,鍵盤
  • 塊設備:可以隨機讀寫設備中的內容,好比 硬盤,U盤

字符設備因爲只能順序訪問,因此應用場景也很少,這篇文章主要討論塊設備。併發

塊設備是隨機訪問的,因此塊設備在不一樣的應用場景中存在很大的優化空間。app

 

塊設備中最重要的一個概念就是塊設備的最小尋址單元。異步

塊設備的最小尋址單元就是扇區,扇區的大小是2的整數倍,通常是 512字節。async

扇區是物理上的最小尋址單元,而邏輯上的最小尋址單元是塊。ide

爲了便於文件系統管理,塊的大小通常是扇區的整數倍,而且小於等於頁的大小。oop

 

查看扇區和I/O塊的方法:大數據

[wangyubin@localhost]$ sudo fdisk -l

WARNING: GPT (GUID Partition Table) detected on '/dev/sda'! The util fdisk doesn't support GPT. Use GNU Parted.


Disk /dev/sda: 500.1 GB, 500107862016 bytes, 976773168 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disk identifier: 0x00000000

上面的 Sector size 就是扇區的值,I/O size就是 塊的值

從上面顯示的結果,咱們發現有個奇怪的地方,扇區的大小有2個值,邏輯大小是 512字節,而物理大小倒是 4096字節。

其實邏輯大小 512字節是爲了兼容之前的軟件應用,而實際物理大小 4096字節是因爲硬盤空間愈來愈大致使的。

具體的前因後果請參考:4KB扇區的緣由

 

2. 內核訪問塊設備的方法

內核經過文件系統訪問塊設備時,須要先把塊讀入到內存中。因此文件系統爲了管理塊設備,必須管理[塊]和內存頁之間的映射。

內核中有2種方法來管理 [] 和內存頁之間的映射。

  • 緩衝區和緩衝區頭
  • bio

 

2.1 緩衝區和緩衝區頭

每一個 [] 都是一個緩衝區,同時對每一個 [] 都定義一個緩衝區頭來描述它。

因爲 [] 的大小是小於內存頁的大小的,因此每一個內存頁會包含一個或者多個 []

 

緩衝區頭定義在 <linux/buffer_head.h>: include/linux/buffer_head.h

struct buffer_head {
    unsigned long b_state;            /* 表示緩衝區狀態 */
    struct buffer_head *b_this_page;/* 當前頁中緩衝區 */
    struct page *b_page;            /* 當前緩衝區所在內存頁 */

    sector_t b_blocknr;        /* 起始塊號 */
    size_t b_size;            /* buffer在內存中的大小 */
    char *b_data;            /* 塊映射在內存頁中的數據 */

    struct block_device *b_bdev; /* 關聯的塊設備 */
    bh_end_io_t *b_end_io;        /* I/O完成方法 */
     void *b_private;             /* 保留的 I/O 完成方法 */
    struct list_head b_assoc_buffers;   /* 關聯的其餘緩衝區 */
    struct address_space *b_assoc_map;    /* 相關的地址空間 */
    atomic_t b_count;                    /* 引用計數 */
};

 

整個 buffer_head 結構體中的字段是減小過的,之前的內核中字段更多。

各個字段的含義經過註釋都很明瞭,只有 b_state 字段比較複雜,它涵蓋了緩衝區可能的各類狀態。

enum bh_state_bits {
    BH_Uptodate,    /* 包含可用數據 */
    BH_Dirty,    /* 該緩衝區是髒的(說明緩衝的內容比磁盤中的內容新,須要回寫磁盤) */
    BH_Lock,    /* 該緩衝區正在被I/O使用,鎖住以防止併發訪問 */
    BH_Req,        /* 該緩衝區有I/O請求操做 */
    BH_Uptodate_Lock,/* 由內存頁中的第一個緩衝區使用,使得該頁中的其餘緩衝區 */

    BH_Mapped,    /* 該緩衝區是映射到磁盤塊的可用緩衝區 */
    BH_New,        /* 緩衝區是經過 get_block() 剛剛映射的,尚且不能訪問 */
    BH_Async_Read,    /* 該緩衝區正經過 end_buffer_async_read() 被異步I/O讀操做使用 */
    BH_Async_Write,    /* 該緩衝區正經過 end_buffer_async_read() 被異步I/O寫操做使用 */
    BH_Delay,    /* 緩衝區還未和磁盤關聯 */
    BH_Boundary,    /* 該緩衝區處於連續塊區的邊界,下一個塊不在連續 */
    BH_Write_EIO,    /* 該緩衝區在寫的時候遇到 I/O 錯誤 */
    BH_Ordered,    /* 順序寫 */
    BH_Eopnotsupp,    /* 該緩衝區發生 「不被支持」 錯誤 */
    BH_Unwritten,    /* 該緩衝區在磁盤上的位置已經被申請,但還有實際寫入數據 */
    BH_Quiet,    /* 該緩衝區禁止錯誤 */

    BH_PrivateStart,/* 不是表示狀態,分配給其餘實體的私有數據區的第一個bit */
};

 

在2.6以前的內核中,主要就是經過緩衝區頭來管理 [塊] 和內存之間的映射的。

用緩衝區頭來管理內核的 I/O 操做主要存在如下2個弊端,因此在2.6開始的內核中,緩衝區頭的做用大大下降了。

- 弊端 1

對內核而言,操做內存頁是最爲簡便和高效的,因此若是經過緩衝區頭來操做的話(緩衝區 即[塊]在內存中映射,可能比頁面要小),效率低下。

並且每一個 [塊] 對應一個緩衝區頭的話,致使內存的利用率下降(緩衝區頭包含的字段很是多)

- 弊端 2

每一個緩衝區頭只能表示一個 [塊],因此內核在處理大數據時,會分解爲對一個個小的 [塊] 的操做,形成沒必要要的負擔和空間浪費。

 

2.2 bio

bio結構體的出現就是爲了改善上面緩衝區頭的2個弊端,它表示了一次 I/O 操做所涉及到的全部內存頁。

/*
 * I/O 操做的主要單元,針對 I/O塊和更低級的層 (ie drivers and
 * stacking drivers)
 */
struct bio {
    sector_t        bi_sector;    /* 磁盤上相關扇區 */
    struct bio        *bi_next;    /* 請求列表 */
    struct block_device    *bi_bdev; /* 相關的塊設備 */
    unsigned long        bi_flags;    /* 狀態和命令標誌 */
    unsigned long        bi_rw;        /* 讀仍是寫 */

    unsigned short        bi_vcnt;    /* bio_vecs的數目 */
    unsigned short        bi_idx;        /* bio_io_vect的當前索引 */

    /* Number of segments in this BIO after
     * physical address coalescing is performed.
     * 結合後的片斷數目
     */
    unsigned int        bi_phys_segments;

    unsigned int        bi_size;    /* 剩餘 I/O 計數 */

    /*
     * To keep track of the max segment size, we account for the
     * sizes of the first and last mergeable segments in this bio.
     * 第一個和最後一個可合併的段的大小
     */
    unsigned int        bi_seg_front_size;
    unsigned int        bi_seg_back_size;

    unsigned int        bi_max_vecs;    /* bio_vecs數目上限 */
    unsigned int        bi_comp_cpu;    /* 結束CPU */

    atomic_t        bi_cnt;        /* 使用計數 */
    struct bio_vec        *bi_io_vec;    /* bio_vec 鏈表 */
    bio_end_io_t        *bi_end_io; /* I/O 完成方法 */
    void            *bi_private;    /* bio結構體建立者的私有方法 */
#if defined(CONFIG_BLK_DEV_INTEGRITY)
    struct bio_integrity_payload *bi_integrity;  /* data integrity */
#endif
    bio_destructor_t    *bi_destructor;    /* bio撤銷方法 */
    /*
     * We can inline a number of vecs at the end of the bio, to avoid
     * double allocations for a small number of bio_vecs. This member
     * MUST obviously be kept at the very end of the bio.
     * 內嵌在結構體末尾的 bio 向量,主要爲了防止出現二次申請少許的 bio_vecs
     */
    struct bio_vec        bi_inline_vecs[0];
};

幾個重要字段說明:

  • bio 結構體表示正在執行的 I/O 操做相關的信息。
  • bio_io_vec 鏈表表示當前 I/O 操做涉及到的內存頁
  • bio_vec 結構體表示 I/O 操做使用的片斷
  • bi_vcnt bi_io_vec鏈表中bi_vec的個數
  • bi_idx 當前的 bi_vec片斷,經過 bi_vcnt(總數)和 bi_idx(當前數),就能夠跟蹤當前 I/O 操做的進度

 

bio_vec 結構體很簡單,定義以下:

struct bio_vec {
    struct page    *bv_page;       /* 對應的物理頁 */
    unsigned int    bv_len;     /* 緩衝區大小 */
    unsigned int    bv_offset;  /* 緩衝區開始的位置 */
};

每一個 bio_vec 都是對應一個頁面,從而保證內核可以方便高效的完成 I/O 操做

bio, bio_vec和page之間的關係

 

2.3 2種方法的對比

緩衝區頭和bio並非相互矛盾的,bio只是緩衝區頭的一種改善,將之前緩衝區頭完成的一部分工做移到bio中來完成。

bio中對應的是內存中的一個個頁,而緩衝區頭對應的是磁盤中的一個塊。

對內核來講,配合使用bio和緩衝區頭 比 只使用緩衝區頭更加的方便高效。

bio至關於在緩衝區上又封裝了一層,使得內核在 I/O操做時只要針對一個或多個內存頁便可,不用再去管理磁盤塊的部分。

 

使用bio結構體還有如下好處:

  • bio結構體很容易處理高端內存,由於它處理的是內存頁而不是直接指針
  • bio結構體既能夠表明普通頁I/O,也能夠表明直接I/O
  • bio結構體便於執行分散-集中(矢量化的)塊I/O操做,操做中的數據能夠取自多個物理頁面

 

3. 內核I/O調度程序

緩衝區頭和bio都是內核處理一個具體I/O操做時涉及的概念。

可是內核除了要完成I/O操做之外,還要調度好全部I/O操做請求,儘可能確保每一個請求能有個合理的響應時間。

 

下面就是目前內核中已有的一些 I/O 調度算法。

3.1 linus電梯

爲了保證磁盤尋址的效率,通常會盡可能讓磁頭向一個方向移動,等到頭了再反過來移動,這樣能夠縮短全部請求的磁盤尋址總時間。

磁頭的移動有點相似於電梯,全部這個 I/O 調度算法也叫電梯調度。

linux中的第一個電梯調度算法就是 linus本人所寫的,全部也叫作 linus 電梯。

 

linus電梯調度主要是對I/O請求進行合併和排序。

當一個新請求加入I/O請求隊列時,可能會發生如下4種操做:

  1. 若是隊列中已存在一個對相鄰磁盤扇區操做的請求,那麼新請求將和這個已存在的請求合併成一個請求
  2. 若是隊列中存在一個駐留時間過長的請求,那麼新請求之間查到隊列尾部,防止舊的請求發生飢餓
  3. 若是隊列中已扇區方向爲序存在合適的插入位置,那麼新請求將被插入該位置,保證隊列中的請求是以被訪問磁盤物理位置爲序進行排列的
  4. 若是隊列中不存在合適的請求插入位置,請求將被插入到隊列尾部

 

linus電梯調度程序在2.6版的內核中被其餘調度程序所取代了。

 

3.2 最終期限I/O調度

linus電梯調度主要考慮了系統的全局吞吐量,對於個別的I/O請求,仍是有可能形成飢餓現象。

並且讀寫請求的響應時間要求也是不同的,通常來講,寫請求的響應時間要求不高,寫請求能夠和提交它的應用程序異步執行,

可是讀請求通常和提交它的應用程序時同步執行,應用程序等獲取到讀的數據後纔會接着往下執行。

所以在 linus 電梯調度程序中,還可能形成 寫-飢餓-讀(wirtes-starving-reads)這種特殊問題。

 

爲了儘可能公平的對待全部請求,同時儘可能保證讀請求的響應時間,提出了最終期限I/O調度算法。

最終期限I/O調度 算法給每一個請求設置了超時時間,默認狀況下,讀請求的超時時間500ms,寫請求的超時時間是5s

但一個新請求加入到I/O請求隊列時,最終期限I/O調度和linus電梯調度相比,多出瞭如下操做:

  1. 新請求加入到 排序隊列(order-FIFO),加入的方法相似 linus電梯新請求加入的方法
  2. 根據新請求的類型,將其加入 讀隊列(read-FIFO) 或者寫隊列(wirte-FIFO) 的尾部(讀寫隊列是按加入時間排序的,因此新請求都是加到尾部)
  3. 調度程序首先判斷 讀,寫隊列頭的請求是否超時,若是超時,從讀,寫隊列頭取出請求,加入到派發隊列(dispatch-FIFO)
  4. 若是沒有超時請求,從 排序隊列(order-FIFO)頭取出一個請求加入到 派發隊列(dispatch-FIFO)
  5. 派發隊列(dispatch-FIFO)按順序將請求提交到磁盤驅動,完成I/O操做

 

最終期限I/O調度 算法也不能嚴格保證響應時間,可是它能夠保證不會發生請求在明顯超時的狀況下仍得不到執行。

最終期限I/O調度 的實現參見: block/deadline-iosched.c

 

3.3 預測I/O調度

最終期限I/O調度算法優先考慮讀請求的響應時間,但系統處於寫操做繁重的狀態時,會大大下降系統的吞吐量。

由於讀請求的超時時間比較短,因此每次有讀請求時,都會打斷寫請求,讓磁盤尋址到讀的位置,完成讀操做後再回來繼續寫。

這種作法保證讀請求的響應速度,卻損害了系統的全局吞吐量(磁頭先去讀再回來寫,發生了2次尋址操做)

 

預測I/O調度算法是爲了解決上述問題而提出的,它是基於最終期限I/O調度算法的。

但有一個新請求加入到I/O請求隊列時,預測I/O調度與最終期限I/O調度相比,多瞭如下操做:

  1. 新的讀請求提交後,並不當即進行請求處理,而是有意等待片刻(默認是6ms)
  2. 等待期間若是有其餘對磁盤相鄰位置進行讀操做的讀請求加入,會馬上處理這些讀請求
  3. 等待期間若是沒有其餘讀請求加入,那麼等待時間至關於浪費掉
  4. 等待時間結束後,繼續執行之前剩下的請求

 

預測I/O調度算法中最重要的是保證等待期間不要浪費,也就是提升預測的準確性,

目前這種預測是依靠一系列的啓發和統計工做,預測I/O調度程序會跟蹤並統計每一個應用程序的I/O操做習慣,以便正確預測應用程序的讀寫行爲。

 

若是預測的準確率足夠高,那麼預測I/O調度和最終期限I/O調度相比,既能提升讀請求的響應時間,又能提升系統吞吐量。

預測I/O調度的實現參見: block/as-iosched.c

 

:預測I/O調度是linux內核中缺省的調度程序。

 

3.4 徹底公正的排隊I/O調度

徹底公正的排隊(Complete Fair Queuing, CFQ)I/O調度 是爲專有工做負荷設計的,它和以前提到的I/O調度有根本的不一樣。

CFQ I/O調度 算法中,每一個進程都有本身的I/O隊列,

CFQ I/O調度程序以時間片輪轉調度隊列,從每一個隊列中選取必定的請求數(默認4個),而後進行下一輪調度。

 

CFQ I/O調度在進程級提供了公平,它的實現位於: block/cfq-iosched.c

 

3.5 空操做的I/O調度

空操做(noop)I/O調度幾乎不作什麼事情,這也是它這樣命名的緣由。

空操做I/O調度只作一件事情,當有新的請求到來時,把它與任一相鄰的請求合併。

 

空操做I/O調度主要用於閃存卡之類的塊設備,這類設備沒有磁頭,沒有尋址的負擔。

空操做I/O調度的實現位於: block/noop-iosched.c

 

3.6 I/O調度程序的選擇

2.6內核中內置了上面4種I/O調度,能夠在啓動時經過命令行選項 elevator=xxx 來啓用任何一種。

elevator選項參數以下:

參數

I/O調度程序

as 預測
cfq 徹底公正排隊
deadline 最終期限
noop 空操做

若是啓動預測I/O調度,啓動的命令行參數中加上 elevator=as

相關文章
相關標籤/搜索