- 請求隊列
塊設備將掛起的塊IO請求保存在請求隊列中,該隊列由request_queue結構體表示:
- 在<Blkdev.h(include/linux)>中
- struct request_queue
- {
- /*
- * Together with queue_head for cacheline sharing
- */
- struct list_head queue_head;
- struct request *last_merge;
- elevator_t *elevator;
- /*
- * the queue request freelist, one for reads and one for writes
- */
- struct request_list rq;
- request_fn_proc *request_fn;
- make_request_fn *make_request_fn;
- prep_rq_fn *prep_rq_fn;
- unplug_fn *unplug_fn;
- merge_bvec_fn *merge_bvec_fn;
- issue_flush_fn *issue_flush_fn;
- prepare_flush_fn *prepare_flush_fn;
- softirq_done_fn *softirq_done_fn;
- /*
- * Dispatch queue sorting
- */
- sector_t end_sector;
- struct request *boundary_rq;
- /*
- * Auto-unplugging state
- */
- struct timer_list unplug_timer;
- int unplug_thresh; /* After this many requests */
- unsigned long unplug_delay; /* After this many jiffies */
- struct work_struct unplug_work;
- struct backing_dev_info backing_dev_info;
- /*
- * The queue owner gets to use this for whatever they like.
- * ll_rw_blk doesn't touch it.
- */
- void *queuedata;
- /*
- * queue needs bounce pages for pages above this limit
- */
- unsigned long bounce_pfn;
- gfp_t bounce_gfp;
- /*
- * various queue flags, see QUEUE_* below
- */
- unsigned long queue_flags;
- /*
- * protects queue structures from reentrancy. ->__queue_lock should
- * _never_ be used directly, it is queue private. always use
- * ->queue_lock.
- */
- spinlock_t __queue_lock;
- spinlock_t *queue_lock;
- /*
- * queue kobject
- */
- struct kobject kobj;
- /*
- * queue settings
- */
- unsigned long nr_requests; /* Max # of requests */
- unsigned int nr_congestion_on;
- unsigned int nr_congestion_off;
- unsigned int nr_batching;
- unsigned int max_sectors;
- unsigned int max_hw_sectors;
- unsigned short max_phys_segments;
- unsigned short max_hw_segments;
- unsigned short hardsect_size;
- unsigned int max_segment_size;
- unsigned long seg_boundary_mask;
- unsigned int dma_alignment;
- struct blk_queue_tag *queue_tags;
- unsigned int nr_sorted;
- unsigned int in_flight;
- /*
- * sg stuff
- */
- unsigned int sg_timeout;
- unsigned int sg_reserved_size;
- int node;
- #ifdef CONFIG_BLK_DEV_IO_TRACE
- struct blk_trace *blk_trace;
- #endif
- /*
- * reserved for flush operations
- */
- unsigned int ordered, next_ordered, ordseq;
- int orderr, ordcolor;
- struct request pre_flush_rq, bar_rq, post_flush_rq;
- struct request *orig_bar_rq;
- unsigned int bi_size;
- struct mutex sysfs_lock;
- };
通過內核中像文件系統這樣高層的代碼將請求加入到隊列中。請求隊列只要不爲空,隊列對應的塊設備驅動程序就會從隊列頭獲取請求,然後將其送入對應的塊設備上去。請求隊列表中的每一項都是一個單獨的請求,有request結構體表示:
- /*
- * try to put the fields that are referenced together in the same cacheline
- */
- struct request {
- struct list_head queuelist;
- struct list_head donelist;
- request_queue_t *q;
- unsigned int cmd_flags;
- enum rq_cmd_type_bits cmd_type;
- /* Maintain bio traversal state for part by part I/O submission.
- * hard_* are block layer internals, no driver should touch them!
- */
- sector_t sector; /* next sector to submit */
- sector_t hard_sector; /* next sector to complete */
- unsigned long nr_sectors; /* no. of sectors left to submit */
- unsigned long hard_nr_sectors; /* no. of sectors left to complete */
- /* no. of sectors left to submit in the current segment */
- unsigned int current_nr_sectors;
- /* no. of sectors left to complete in the current segment */
- unsigned int hard_cur_sectors;
- struct bio *bio;
- struct bio *biotail;
- struct hlist_node hash; /* merge hash */
- /*
- * The rb_node is only used inside the io scheduler, requests
- * are pruned when moved to the dispatch queue. So let the
- * completion_data share space with the rb_node.
- */
- union {
- struct rb_node rb_node; /* sort/lookup */
- void *completion_data;
- };
- /*
- * two pointers are available for the IO schedulers, if they need
- * more they have to dynamically allocate it.
- */
- void *elevator_private;
- void *elevator_private2;
- struct gendisk *rq_disk;
- unsigned long start_time;
- /* Number of scatter-gather DMA addr+len pairs after
- * physical address coalescing is performed.
- */
- unsigned short nr_phys_segments;
- /* Number of scatter-gather addr+len pairs after
- * physical and DMA remapping hardware coalescing is performed.
- * This is the number of scatter-gather entries the driver
- * will actually have to deal with after DMA mapping is done.
- */
- unsigned short nr_hw_segments;
- unsigned short ioprio;
- void *special;
- char *buffer;
- int tag;
- int errors;
- int ref_count;
- /*
- * when request is used as a packet command carrier
- */
- unsigned int cmd_len;
- unsigned char cmd[BLK_MAX_CDB];
- unsigned int data_len;
- unsigned int sense_len;
- void *data;
- void *sense;
- unsigned int timeout;
- int retries;
- /*
- * completion callback.
- */
- rq_end_io_fn *end_io;
- void *end_io_data;
- };
因爲一個請求可能要操作多個連續的磁盤塊,所以每個請求可以由多個bio結構體組成。注意,雖然磁盤上的塊必須連續,但在內存中這些塊並不一定要連續,每個bio結構體都可以描述多個片段,而內閣請求也可以包含多個bio結構體。
- IO調度程序
爲了優化尋址操作,內核不會簡單的按請求接收次序或者立即將請求提交給磁盤,它會在提交前,先執行名爲合併與排序的預操作。這種預操作可以極大第提高系統的整體性能。在內核中負責提交IO請求的子系統被稱爲IO調度程序。
IO調度程序將磁盤IO資源分配給系統中所以掛起的塊IO請求。這種資源分配是通過將請求隊列中掛起的請求合併和排序來完成的。
進程調度程序和IO調度程序都是將一個資源虛擬給多個對象。進程調度程序的作用是將處理器資源分配給系統中的運行進程。處理器被虛擬並被系統中的運行進程共享。這種虛擬提供給用戶的就是多任務和分時操作系統。IO調度程序虛擬塊設備給多個磁盤請求,以便降低磁盤尋址時間,確保磁盤性能的最優化。
- IO調度程序的工作
IO調度程序的工作是管理塊設備的請求隊列。它決定隊列中的請求排列順序以及什麼時候派發請求到塊設備。
IO調度程序通過兩種方法減少磁盤尋址時間:合併和排序。合併指將兩個或多個請求結和成一個新請求。通過合併請求,IO調度程序將多次請求的開銷壓縮成一次請求的開銷。更重要的是,請求合併後只需要傳遞給磁盤一條尋址命令,就可以訪問到請求合併前必須多次尋址才能訪問完的磁盤區域了,因此合併請求顯然可以減少系統開銷和磁盤尋址次數。
IO調度程序將整個請求隊列按扇區增長方向有序排列。使所有請求按硬盤上扇區的排列順序有序排列的目的不僅是爲了縮短單獨一次請求的尋址時間,更重要的優化在於,通過保存磁盤頭以直線方向移動,縮短了所以請求的磁盤尋址的時間。該排序算法類似於電梯調度----電梯不能隨意的從一層跳到另一層,它只向一個方向移動,當抵達了同一方向的最後一層後,再掉頭向另一個方向移動。所以IO調度程序被稱爲電梯調度。
- Linus電梯
Linus電梯能執行合併與排序預處理。當有新的請求加入隊列時,它首先檢查其他每一個掛起的請求是否可以和新請求合併。Linus電梯可以執行向前和向後合併。如果新請求正好連在一個現存的請求前,就是向前合併;如果請求直接連接在一個現存的請求後,就是向後合併。
如果合併失敗,就需要尋找可能的插入點(新請求在隊列中的位置必須符合以扇區方向有序排序的原則)。如果找到,新請求就被插入到該點;如果沒有合適的位置,那麼新請求就被加入到隊列尾部。另外,如果發現隊列中有駐留時間過長的請求,那麼新請求就被加入到隊列尾部,即使插入後還要排序。這樣可以避免訪問相近磁盤的請求太多而造成訪問磁盤其他位置請求難以得到執行。
但是這種檢測方法並不是很有效,因爲它並非是給等待了一段時間的請求提供實質性服務----它僅僅是在經過了一定時間後停止插入----排序請求,雖然改善了等待時間但最終還是會導致請求飢餓現象的發生。
- 最終期限IO調度程序
最終期限IO調度程序是爲了解決Linus電梯所帶來的飢餓問題而提出的。
在最後期限IO調度程序中,每個請求都有一個超時時間。默認情況下,讀請求的超時時間是500毫秒,寫請求的超時時間是5秒。最後期限IO調度程序請求類似與Linus電梯,也以磁盤物理位置爲次序維護請求隊列,該隊列被稱爲排序隊列。當一個新請求遞交給排序隊列時,最後期限IO調度程序合併和插入請求,同時會以請求類型爲依據將它們插入到額外隊列中。讀請求按次序被插入到特定的讀FIFO隊列中,寫請求被插入到特定的寫FIFO隊列中。普通隊列以磁盤扇區爲序進行排列,以FIFO形式組織,新隊列總是被加入到隊列尾部。對普通操作來說,最後期限IO調度程序將請求從排序隊列的頭部取下,再推入到派發隊列中,派發隊列然後將請求提交給磁盤驅動,從而保證了最小化的請求尋址。
如果在寫FIFO隊列頭,或是在讀FIFO隊列頭的請求超時,那麼最後期限IO調度程序便從FIFO隊列中提取請求進行服務。
由於讀請求給定的超時時間要比寫請求短,所以最後期限IO調度程序確保了寫請求不會因爲堵塞讀請求而使讀請求發生飢餓現象。
最後期限IO調度程序的實現在文件drivers/block/deadline-iosched.c中。
- 預測IO調度程序
預測(anticipatory)IO調度程序的目標是在保持良好的讀響應的同時也能提供良好的全局吞吐量。
該調度程序是以最後期限IO調度程序爲基礎的。也實現了三個隊列和一個派發隊列,併爲每個請求設置了超時時間。它最主要的改進是它增加了預測啓發能力。預測IO調度程序試圖減少在進行IO操作期間,處理新到的讀請求所帶來的尋址數量。預測IO調度程序與最後期限IO調度不同之處在於,請求提交後並不直接返回處理其他請求,而是會有意空閒片刻。這幾毫秒對應用程序來說是個提交其他讀請求的好機會,任何對相鄰磁盤位置操作的請求都會立刻得到處理。在等待時間結束後,預測IO調度程序重新返回原來的位置,繼續執行以前剩餘的請求。
預測IO調度程序所能帶來的優勢取決於能否正確的預測應用程序和文件系統的行爲 。這種預測依靠一系列的啓發和統計工作。預測IO調度程序跟蹤並統計每個應用程序塊IO操作的習慣行爲,以便正確預測應用程序的未來行爲。
預測IO調度程序的實現在內核源代碼樹的drivers/block/as-iosched.c中,它是Linux內核中默認的IO調度程序。
- 完全公正的排隊IO調度程序
完全公正的排隊IO調度程序(Complete Fair Queuing,簡稱CFQ)是爲專有工作負荷設計的。
CFQ IO調度程序把進入的IO請求放入特定的隊列中,這種隊列是根據引起IO請求的進程組織的。例如,來自foo進程的IO請求進入foo隊列,來自bar進程的IO請求進入bar隊列。在每個隊列中,剛進來的請求與相鄰請求合併在一起,並進行插入分類。對列由此按扇區方式分類。CFQ IO調度程序與其他調度的差異在於每一個提交IO的進程都有自己的隊列。
CFQ IO調度程序以時間片輪轉調度隊列,從每個隊列中選取請求數,然後進行下一輪調度。這就在進程級提供了公平,確保每個進程接收公平的磁盤帶寬片段。預定的工作負荷是多媒體。
CFQ IO調度程序的實現在文件drivers/block/cfq-iosched.c中。
- 空操作的IO調度程序
最後一種IO調度程序是空操作(Noop)的IO調度程序。
空操作的IO調度程序不進行排序,也不進行預尋道操作,但是要執行請求的合併。當一個新的請求提交到隊列時,就把它與任一相鄰的請求合併,維護請求隊列以近似FIFO的順序排列,塊設備驅動程序可以從這種隊列中摘取請求。
如果塊設備沒有一點尋道的負擔(真正的隨機訪問設備),那麼就沒有必要對進入的請求進行插入排序。
空操作的IO調度程序的實現在文件drivers/block/noop-iosched.c中,它是專門爲隨機訪問設備而設計的。
- IO調度程序的選擇
內核中,塊設備默認使用預測IO調度程序。在啓動時,可以覆蓋默認,通過命令行選項elevator=某個有效的激活的IO調度程序。
給定elevator選項的參數
參數 | IO調度程序 |
as | 預測 |
cfq | 完全公正的排隊 |
deadline | 最終期限 |
noop | 空操作 |