上一節主要梳理了下調度器的流程,其中對於blk_init_allocated_queue有一個elevator_init的初始化函數,下面從elevator_init為突破口,來看看內核中有哪些的調度算法。對於elevator_init其主要用來為請求隊列分配一個I/O調度器
int elevator_init(struct request_queue *q, char *name) { struct elevator_type *e = NULL; int err; /* * q->sysfs_lock must be held to provide mutual exclusion between * elevator_switch() and here. */ lockdep_assert_held(&q->sysfs_lock); //檢查當前進程是否已經設置了調度標志 if (unlikely(q->elevator)) return 0; INIT_LIST_HEAD(&q->queue_head); //初始化請求隊列的相關元素 q->last_merge = NULL; q->end_sector = 0; q->boundary_rq = NULL;
/*下面根據情況在elevator全局鏈表中來尋找適合的調度器分配給請求隊列*/ if (name) { e = elevator_get(name, true); //如果指定了name,則尋找與name匹配的調度器 if (!e) return -EINVAL; }
/*如果沒有指定io調度器,並且chosen_elevator存在,則尋找其指定的調度器*/ if (!e && *chosen_elevator) { e = elevator_get(chosen_elevator, false); if (!e) printk(KERN_ERR "I/O scheduler %s not found\n", chosen_elevator); } /*依然沒獲取到調度器的話則使用默認配置的調度器*/ if (!e) { e = elevator_get(CONFIG_DEFAULT_IOSCHED, false); if (!e) { printk(KERN_ERR "Default I/O scheduler not found. " \ "Using noop.\n"); e = elevator_get("noop", false); //獲取失敗則使用最簡單的noop調度器 } } err = e->ops.elevator_init_fn(q, e); if (err) elevator_put(e); return err; }
所有的I/O調度器類型都會通過鏈表鏈接起來(通過struct elevator_type中的list元素),elevator_get()函數便是通過給定的name,在鏈表中尋找與name匹配的調度器類型。當確定了I/O調度器的類型后,就會調用對應的elevator_init_fn,由於每個調度器根據自身算法的不同,都會擁有不同的隊列結構,在elevator_init_fn()中會調用特定於調度器的初始化函數針對這些隊列進行初始化,並且返回特定於調度器的數據結構,現在內核提供有三種調度的算法
1). noop-iosched
2). cfq-iosched.c
3). deadline-iosched.c
那么我們針對這三種調度算法來看看其實現的基本原理
1. noop調度算法
看看其初始化是通過elv_register注冊了一個elevator_noop結構,下面來看看noop的初始化都做了一些什么?
static int noop_init_queue(struct request_queue *q, struct elevator_type *e) { struct noop_data *nd; struct elevator_queue *eq; eq = elevator_alloc(q, e); //為等待隊列分配一個調度器的實例 if (!eq) return -ENOMEM; nd = kmalloc_node(sizeof(*nd), GFP_KERNEL, q->node); //從通用緩沖區中分配一個屬於指定 NUMA 節點的對象
if (!nd) { kobject_put(&eq->kobj); return -ENOMEM; } eq->elevator_data = nd; INIT_LIST_HEAD(&nd->queue); //加入鏈表 spin_lock_irq(q->queue_lock); q->elevator = eq; //將調度器賦值給等待隊列,以方便后續使用 spin_unlock_irq(q->queue_lock); return 0; }
這個只是主要是將分配的調度器賦予等待隊列,那么看看這個調度方法最核心的數據結構
struct noop_data { struct list_head queue; };
從結構來看只有一個成員queue,其實就noop中維護的一個fifo(先進先出)鏈表的鏈表頭,猜想noop對於調度的處理一個對於基本鏈表的處理方式,就是一個鏈表的當io請求過來了,就會被加入到這個鏈表的后面,在鏈表前面的就會被移到系統的請求隊列(request_queue)中。下面結合代碼看看,整個處理流程。
static void noop_add_request(struct request_queue *q, struct request *rq)
{
struct noop_data *nd = q->elevator->elevator_data;
list_add_tail(&rq->queuelist, &nd->queue);
}
當調度器需要發送request時,會調用noop_dispatch。該函數會直接從調度器所管理的request queue中獲取一個request,然后調用elv_dispatch_sort函數將請求加入到設備所在的request queue中,最后
static int noop_dispatch(struct request_queue *q, int force) { struct noop_data *nd = q->elevator->elevator_data; if (!list_empty(&nd->queue)) { struct request *rq; rq = list_entry(nd->queue.next, struct request, queuelist); //從調度器的隊列頭中獲取一個request list_del_init(&rq->queuelist); //將獲取到的節點(node)從鏈表中刪掉
elv_dispatch_sort(q, rq); //剛取出的rq放入到系統的請求隊列 return 1; } return 0; }
由此可見,noop調度器的實現是很簡單的,僅僅實現了一個調度器的框架,用一條鏈表把所有輸入的request管理起來,簡單方便,不會陷入極端,並且也不會損失多少性能,還能帶來一定額實時性,但是缺點也非常明顯,沒有對io進行排序,沒有參與調度,對於一些機械式的訪問有明顯的不足之處。
2. deadline調度算法
從noop的調度來看,缺少優化和調度,那么deadline是如何來處理調度算法呢?從noop的分析過程來看,其數據結構決定了其方法,那么首先來看看deadline數據結構
struct deadline_data { struct rb_root sort_list[2]; //采用紅黑樹管理所有的request,請求地址作為索引值 struct list_head fifo_list[2]; //采用FIFO隊列管理所有的request,所有請求按照時間先后次序排列 struct request *next_rq[2]; //批量處理請求過程中,需要處理的下一個request unsigned int batching; //統計當前已經批量處理完成的request sector_t last_sector; /* head position */ unsigned int starved; /* times reads have starved writes */ int fifo_expire[2]; //讀寫請求的超時時間值 int fifo_batch; //批量處理的request數量 int writes_starved; //寫飢餓值 int front_merges; };
從其內容來看,這個比noop的調度復雜好幾倍,還引入了紅黑樹縮短查找時間,通過noop的elevator_init_fn我們大致可以看出會做些什么操作?基本都類似,對於deadline會多一些數據結構初始化的操作,所以沒有分析的必要,我們主要關注其差異,那么deadline是如何處理請求,加入到隊列中呢?
static void deadline_add_request(struct request_queue *q, struct request *rq) { struct deadline_data *dd = q->elevator->elevator_data; const int data_dir = rq_data_dir(rq); deadline_add_rq_rb(dd, rq); //請求加入到deadline調度器的sort_list紅黑樹中 /* * set expire time and add to fifo list */ rq->fifo_time = jiffies + dd->fifo_expire[data_dir]; //設置請求超時的時間,這個請求在這個時間到了必須得到響應 list_add_tail(&rq->queuelist, &dd->fifo_list[data_dir]); //將請求加入deadline調度器的list_fifo鏈表中 }
一種是采用紅黑樹(RB tree)的方式將所有request組織起來,通過request的訪問地址作為索引;另一種方式是采用隊列的方式將request管理起來,所有的request采用先來后到的方式進行排序,即FIFO隊列。各個請求被放入到隊列后,那么該輪到合並出場了。
static int deadline_merge(struct request_queue *q, struct request **req, struct bio *bio) { struct deadline_data *dd = q->elevator->elevator_data; struct request *__rq; int ret; /* * check for front merge */ if (dd->front_merges) { sector_t sector = bio_end_sector(bio); //取bio的最后一個扇區 __rq = elv_rb_find(&dd->sort_list[bio_data_dir(bio)], sector); //從紅黑樹中查找起始扇區號與sector相同的request if (__rq) { BUG_ON(sector != blk_rq_pos(__rq)); if (elv_rq_merge_ok(__rq, bio)) { //各項屬性的檢查,確定bio可以插入 ret = ELEVATOR_FRONT_MERGE; goto out; } } } return ELEVATOR_NO_MERGE; out: *req = __rq; return ret; }
那么通過上面的函數可能改變紅黑樹的結構,deadline_merged_request進行bio插入的善后工作,所以要將節點刪除再重新進行插入
static void deadline_merged_request(struct request_queue *q, struct request *req, int type) { struct deadline_data *dd = q->elevator->elevator_data; /* * if the merge was a front merge, we need to reposition request */ if (type == ELEVATOR_FRONT_MERGE) { elv_rb_del(deadline_rb_root(dd, req), req); //將request從紅黑樹中刪除 deadline_add_rq_rb(dd, req); //重新添加至紅黑樹 } }
上面完成隊列的合並后,該輪到調度登場了,deadline_dispatch_requests完成這份工作。
static int deadline_dispatch_requests(struct request_queue *q, int force) { struct deadline_data *dd = q->elevator->elevator_data; const int reads = !list_empty(&dd->fifo_list[READ]); //確定讀fifo的狀態 const int writes = !list_empty(&dd->fifo_list[WRITE]); //確定讀fifo的狀態 struct request *rq; int data_dir; /* * batches are currently reads XOR writes */
/* 如果批量請求處理存在,並且還沒有達到批量請求處理的上限值,那么繼續請求的批量處理 */
if (dd->next_rq[WRITE]) rq = dd->next_rq[WRITE]; else rq = dd->next_rq[READ]; if (rq && dd->batching < dd->fifo_batch) /* we have a next request are still entitled to batch */ goto dispatch_request; /* * at this point we are not running a batch. select the appropriate * data direction (read / write) */ /* 優先處理讀請求隊列 */ if (reads) { //讀請求fifo不為空 BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[READ])); if (writes && (dd->starved++ >= dd->writes_starved)) //如果寫請求隊列存在餓死的現象,那么優先處理寫請求隊列 goto dispatch_writes; data_dir = READ; goto dispatch_find_request; } /* * there are either no reads or writes have been starved */ if (writes) {
/* 沒有讀請求需要處理,或者寫請求隊列存在餓死現象 */ dispatch_writes: BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[WRITE])); dd->starved = 0; data_dir = WRITE; goto dispatch_find_request; } return 0; dispatch_find_request: /* * we are not running a batch, find best request for selected data_dir */ if (deadline_check_fifo(dd, data_dir) || !dd->next_rq[data_dir]) { /* 如果請求隊列中存在即將餓死的request,或者不存在需要批量處理的請求,那么從FIFO隊列頭獲取一個request */
/* * A deadline has expired, the last request was in the other * direction, or we have run out of higher-sectored requests. * Start again from the request with the earliest expiry time. */ rq = rq_entry_fifo(dd->fifo_list[data_dir].next); } else { /* 繼續批量處理,獲取需要批量處理的下一個request */
/* * The last req was the same dir and we have a next request in * sort order. No expired requests so continue on from here. */ rq = dd->next_rq[data_dir]; } dd->batching = 0; /* 將request從調度器中移出,發送至設備 */ dispatch_request: /* * rq is the selected appropriate request. */ dd->batching++; deadline_move_request(dd, rq); return 1; }
deadline調度算法相對noop要復雜一點,其設計目標是,在保證請求按照設備扇區的順序進行訪問的同時,兼顧其它請求不被餓死,要在一個最終期限前被調度到,同時增加了讀操作的具有有限度。
3. CFQ調度算法
暫不分析,待續....