最近太忙,竟然過了2個月才更新第十四章。。。。linux
主要內容:ios
I/O設備主要有2類:算法
字符設備因爲只能順序訪問,因此應用場景也很少,這篇文章主要討論塊設備。併發
塊設備是隨機訪問的,因此塊設備在不一樣的應用場景中存在很大的優化空間。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種方法來管理 [塊] 和內存頁之間的映射。
每一個 [塊] 都是一個緩衝區,同時對每一個 [塊] 都定義一個緩衝區頭來描述它。
因爲 [塊] 的大小是小於內存頁的大小的,因此每一個內存頁會包含一個或者多個 [塊]
緩衝區頭定義在 <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
每一個緩衝區頭只能表示一個 [塊],因此內核在處理大數據時,會分解爲對一個個小的 [塊] 的操做,形成沒必要要的負擔和空間浪費。
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_vec 結構體很簡單,定義以下:
struct bio_vec { struct page *bv_page; /* 對應的物理頁 */ unsigned int bv_len; /* 緩衝區大小 */ unsigned int bv_offset; /* 緩衝區開始的位置 */ };
每一個 bio_vec 都是對應一個頁面,從而保證內核可以方便高效的完成 I/O 操做
緩衝區頭和bio並非相互矛盾的,bio只是緩衝區頭的一種改善,將之前緩衝區頭完成的一部分工做移到bio中來完成。
bio中對應的是內存中的一個個頁,而緩衝區頭對應的是磁盤中的一個塊。
對內核來講,配合使用bio和緩衝區頭 比 只使用緩衝區頭更加的方便高效。
bio至關於在緩衝區上又封裝了一層,使得內核在 I/O操做時只要針對一個或多個內存頁便可,不用再去管理磁盤塊的部分。
使用bio結構體還有如下好處:
緩衝區頭和bio都是內核處理一個具體I/O操做時涉及的概念。
可是內核除了要完成I/O操做之外,還要調度好全部I/O操做請求,儘可能確保每一個請求能有個合理的響應時間。
下面就是目前內核中已有的一些 I/O 調度算法。
爲了保證磁盤尋址的效率,通常會盡可能讓磁頭向一個方向移動,等到頭了再反過來移動,這樣能夠縮短全部請求的磁盤尋址總時間。
磁頭的移動有點相似於電梯,全部這個 I/O 調度算法也叫電梯調度。
linux中的第一個電梯調度算法就是 linus本人所寫的,全部也叫作 linus 電梯。
linus電梯調度主要是對I/O請求進行合併和排序。
當一個新請求加入I/O請求隊列時,可能會發生如下4種操做:
linus電梯調度程序在2.6版的內核中被其餘調度程序所取代了。
linus電梯調度主要考慮了系統的全局吞吐量,對於個別的I/O請求,仍是有可能形成飢餓現象。
並且讀寫請求的響應時間要求也是不同的,通常來講,寫請求的響應時間要求不高,寫請求能夠和提交它的應用程序異步執行,
可是讀請求通常和提交它的應用程序時同步執行,應用程序等獲取到讀的數據後纔會接着往下執行。
所以在 linus 電梯調度程序中,還可能形成 寫-飢餓-讀(wirtes-starving-reads)這種特殊問題。
爲了儘可能公平的對待全部請求,同時儘可能保證讀請求的響應時間,提出了最終期限I/O調度算法。
最終期限I/O調度 算法給每一個請求設置了超時時間,默認狀況下,讀請求的超時時間500ms,寫請求的超時時間是5s
但一個新請求加入到I/O請求隊列時,最終期限I/O調度和linus電梯調度相比,多出瞭如下操做:
最終期限I/O調度 算法也不能嚴格保證響應時間,可是它能夠保證不會發生請求在明顯超時的狀況下仍得不到執行。
最終期限I/O調度 的實現參見: block/deadline-iosched.c
最終期限I/O調度算法優先考慮讀請求的響應時間,但系統處於寫操做繁重的狀態時,會大大下降系統的吞吐量。
由於讀請求的超時時間比較短,因此每次有讀請求時,都會打斷寫請求,讓磁盤尋址到讀的位置,完成讀操做後再回來繼續寫。
這種作法保證讀請求的響應速度,卻損害了系統的全局吞吐量(磁頭先去讀再回來寫,發生了2次尋址操做)
預測I/O調度算法是爲了解決上述問題而提出的,它是基於最終期限I/O調度算法的。
但有一個新請求加入到I/O請求隊列時,預測I/O調度與最終期限I/O調度相比,多瞭如下操做:
預測I/O調度算法中最重要的是保證等待期間不要浪費,也就是提升預測的準確性,
目前這種預測是依靠一系列的啓發和統計工做,預測I/O調度程序會跟蹤並統計每一個應用程序的I/O操做習慣,以便正確預測應用程序的讀寫行爲。
若是預測的準確率足夠高,那麼預測I/O調度和最終期限I/O調度相比,既能提升讀請求的響應時間,又能提升系統吞吐量。
預測I/O調度的實現參見: block/as-iosched.c
注:預測I/O調度是linux內核中缺省的調度程序。
徹底公正的排隊(Complete Fair Queuing, CFQ)I/O調度 是爲專有工做負荷設計的,它和以前提到的I/O調度有根本的不一樣。
CFQ I/O調度 算法中,每一個進程都有本身的I/O隊列,
CFQ I/O調度程序以時間片輪轉調度隊列,從每一個隊列中選取必定的請求數(默認4個),而後進行下一輪調度。
CFQ I/O調度在進程級提供了公平,它的實現位於: block/cfq-iosched.c
空操做(noop)I/O調度幾乎不作什麼事情,這也是它這樣命名的緣由。
空操做I/O調度只作一件事情,當有新的請求到來時,把它與任一相鄰的請求合併。
空操做I/O調度主要用於閃存卡之類的塊設備,這類設備沒有磁頭,沒有尋址的負擔。
空操做I/O調度的實現位於: block/noop-iosched.c
2.6內核中內置了上面4種I/O調度,能夠在啓動時經過命令行選項 elevator=xxx 來啓用任何一種。
elevator選項參數以下:
參數 |
I/O調度程序 |
as | 預測 |
cfq | 徹底公正排隊 |
deadline | 最終期限 |
noop | 空操做 |
若是啓動預測I/O調度,啓動的命令行參數中加上 elevator=as