【第3版emWin教程】第14章 emWin6.x的2D圖形庫之基本繪圖
排程策略
在 Linux 裡面,程序大概可以分成兩種。
一種稱為實時程序,也就是需要儘快執行返回結果的那種。另一種是普通程序,大部分的程序其實都是這種。
優先順序其實就是一個數值,對於實時程序,優先順序的範圍是 0~99;對於普通程序,優先順序的範圍是 100~139。數值越小,優先順序越高。
從這裡可以看出,所有的實時程序都比普通程序優先順序要高。
/****** task_struct 程序排程相關 ******/ // 是否在執行佇列上 int on_rq; // 優先順序 int prio; int static_prio; int normal_prio; unsignedint rt_priority; // 排程器類,封裝了排程策略的執行邏輯 const struct sched_class *sched_class; // 排程實體 struct sched_entity se; // 完全公平演算法排程實體 struct sched_rt_entity rt; // 實時排程實體 struct sched_dl_entity dl; // Deadline 排程實體 // 排程策略 unsigned int policy; // 可以使用哪些CPU int nr_cpus_allowed; cpumask_t cpus_allowed;struct sched_info sched_info; // 排程策略定義 #define SCHED_NORMAL 0 #define SCHED_FIFO 1 // 先來先服務 #define SCHED_RR 2 #define SCHED_BATCH 3 #define SCHED_IDLE 5 #define SCHED_DEADLINE 6 // 完全公平演算法排程實體 struct sched_entity { struct load_weight load; struct rb_node run_node; structlist_head group_node; unsigned int on_rq; u64 exec_start; u64 sum_exec_runtime; u64 vruntime; u64 prev_sum_exec_runtime; u64 nr_migrations; struct sched_statistics statistics; ...... };
實時程序的排程策略:SCHED_FIFO, SCHED_RR, SCHED_DEADLINE。
SCHED_FIFO 先來先服務,高優先順序的程序可以搶佔低優先順序的程序,而相同優先順序的程序,我們遵循先來先得。
SCHED_RR 輪流排程演算法,採用時間片,相同優先順序的任務當用完時間片會被放到佇列尾部,以保證公平性,而高優先順序的任務也是可以搶佔低優先順序的任務。
SCHED_DEADLINE,是按照任務的 deadline 進行排程的。當產生一個排程點的時候,DL 排程器總是選擇其 deadline 距離當前時間點最近的那個任務,並排程它執行。
普通程序的排程策略:SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE。
SCHED_NORMAL 是普通的程序。
SCHED_BATCH 是後臺程序,幾乎不需要和前端進行互動。
SCHED_IDLE 是特別空閒的時候才跑的程序。
sched_class 有幾種實現:
stop_sched_class 優先順序最高的任務會使用這種策略,會中斷所有其他執行緒,且不會被其他任務打斷;
dl_sched_class 對應上面的 deadline 排程策略;
rt_sched_class 對應 RR 演算法或者 FIFO 演算法的排程策略,具體排程策略由程序的 task_struct->policy 指定;
fair_sched_class 普通程序的排程策略;
idle_sched_class 空閒程序的排程策略。
每個 CPU 都有自己的 struct rq 結構,用於描述在此 CPU 上所執行的所有程序,包括一個實時程序佇列 rt_rq 和一個 CFS 執行佇列 cfs_rq在。
排程時,排程器首先會先去實時程序佇列找是否有實時程序需要執行,如果沒有才會去 CFS 執行佇列找是否有程序需要執行。
struct rq { /* runqueue lock: */ raw_spinlock_t lock; unsigned int nr_running; unsigned long cpu_load[CPU_LOAD_IDX_MAX]; ...... struct load_weight load; unsigned long nr_load_updates; u64 nr_switches; struct cfs_rq cfs; struct rt_rq rt; struct dl_rq dl; ...... struct task_struct *curr, *idle, *stop; ...... }; /* CFS-related fields in a runqueue */ struct cfs_rq { struct load_weight load; unsigned int nr_running, h_nr_running; u64 exec_clock; u64 min_vruntime; #ifndef CONFIG_64BIT u64 min_vruntime_copy; #endif struct rb_root tasks_timeline; // 指向紅黑樹的根節點 struct rb_node *rb_leftmost; // 指向最左面的節點 struct sched_entity *curr, *next, *last, *skip; ...... };
排程類的定義如下:
struct sched_class { const struct sched_class *next; // 指向下一個排程類 // 向就緒佇列中新增一個程序,當某個程序進入可執行狀態時,呼叫這個函式 void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags); // 將一個程序從就緒佇列中刪除 void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags); void (*yield_task) (struct rq *rq); bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt); void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags); // 選擇接下來要執行的程序 struct task_struct * (*pick_next_task) (struct rq *rq, struct task_struct *prev, struct rq_flags *rf); // 用另一個程序代替當前執行的程序 void (*put_prev_task) (struct rq *rq, struct task_struct *p); // 用於修改排程策略 void (*set_curr_task) (struct rq *rq); // 每次週期性時鐘到的時候,這個函式被呼叫,可能觸發排程 void (*task_tick) (struct rq *rq, struct task_struct *p, int queued); void (*task_fork) (struct task_struct *p); void (*task_dead) (struct task_struct *p); void (*switched_from) (struct rq *this_rq, struct task_struct *task); void (*switched_to) (struct rq *this_rq, struct task_struct *task); void (*prio_changed) (struct rq *this_rq, struct task_struct *task, int oldprio); unsigned int (*get_rr_interval) (struct rq *rq, struct task_struct *task); void (*update_curr) (struct rq *rq) }
排程類分為下面這幾種:
extern const struct sched_class stop_sched_class; extern const struct sched_class dl_sched_class; extern const struct sched_class rt_sched_class; extern const struct sched_class fair_sched_class; extern const struct sched_class idle_sched_class;
它們其實是放在一個連結串列上的。這裡我們以排程最常見的操作,取下一個任務為例,來解析一下。
可以看到,這裡面有一個 for_each_class 迴圈,沿著上面的順序,依次呼叫每個排程類的方法。
這就說明,排程的時候是從優先順序最高的排程類到優先順序低的排程類,依次執行。而對於每種排程類,有自己的實現,例如,CFS 就有 fair_sched_class。
/* * Pick up the highest-prio task: */ static inline struct task_struct * pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) { const struct sched_class *class; struct task_struct *p; ...... for_each_class(class) { p = class->pick_next_task(rq, prev, rf); if (p) { if (unlikely(p == RETRY_TASK)) goto again; return p; } } } const struct sched_class fair_sched_class = { .next = &idle_sched_class, .enqueue_task = enqueue_task_fair, .dequeue_task = dequeue_task_fair, .yield_task = yield_task_fair, .yield_to_task = yield_to_task_fair, .check_preempt_curr = check_preempt_wakeup, .pick_next_task = pick_next_task_fair, .put_prev_task = put_prev_task_fair, .set_curr_task = set_curr_task_fair, .task_tick = task_tick_fair, .task_fork = task_fork_fair, .prio_changed = prio_changed_fair, .switched_from = switched_from_fair, .switched_to = switched_to_fair, .get_rr_interval = get_rr_interval_fair, .update_curr = update_curr_fair, };
對於同樣的 pick_next_task 選取下一個要執行的任務這個動作,不同的排程類有自己的實現。
fair_sched_class 的實現是 pick_next_task_fair,rt_sched_class 的實現是 pick_next_task_rt。
我們會發現這兩個函式是操作不同的佇列,pick_next_task_rt 操作的是 rt_rq,pick_next_task_fair 操作的是 cfs_rq。
static struct task_struct * pick_next_task_rt(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) { struct task_struct *p; struct rt_rq *rt_rq = &rq->rt; ...... } static struct task_struct * pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) { struct cfs_rq *cfs_rq = &rq->cfs; struct sched_entity *se; struct task_struct *p; ...... }
這樣整個執行的場景就串起來了,在每個 CPU 上都有一個佇列 rq,這個佇列裡面包含多個子佇列,例如 rt_rq 和 cfs_rq,不同的佇列有不同的實現方式,cfs_rq 就是用紅黑樹實現的。當有一天,某個 CPU 需要找下一個任務執行的時候,會按照優先順序依次呼叫排程類,不同的排程類操作不同的佇列。當然 rt_sched_class 先被呼叫,它會在 rt_rq 上找下一個任務,只有找不到的時候,才輪到 fair_sched_class 被呼叫,它會在 cfs_rq 上找下一個任務。這樣保證了實時任務的優先順序永遠大於普通任務。
我們重點看下 fair_sched_class 對於 pick_next_task 的實現 pick_next_task_fair,獲取下一個程序。
呼叫路徑如下:pick_next_task_fair->pick_next_entity->__pick_first_entity。
struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq) { struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline); if (!left) return NULL; return rb_entry(left, struct sched_entity, run_node); }
主動排程
舉例:從 Tap 網路裝置等待一個讀取。Tap 網路裝置是虛擬機器使用的網路裝置。當沒有資料到來的時候,它也需要等待,所以也會選擇把 CPU 讓給其他程序。
static ssize_t tap_do_read(struct tap_queue *q, struct iov_iter *to, int noblock, struct sk_buff *skb) { ...... while (1) { if (!noblock) prepare_to_wait(sk_sleep(&q->sk), &wait, TASK_INTERRUPTIBLE); ...... /* Nothing to read, let's sleep */ schedule(); } ...... }
schedule 函式的呼叫過程
asmlinkage __visible void __sched schedule(void) { struct task_struct *tsk = current; sched_submit_work(tsk); do { preempt_disable(); __schedule(false); sched_preempt_enable_no_resched(); } while (need_resched()); } static void __sched notrace __schedule(bool preempt) { struct task_struct *prev, *next; unsigned long *switch_count; struct rq_flags rf; struct rq *rq; int cpu; // 1. 在當前的 CPU 上取出任務佇列 rq cpu = smp_processor_id(); rq = cpu_rq(cpu); prev = rq->curr; ...... // 2. 獲取下一個任務,即繼任 next = pick_next_task(rq, prev, &rf); clear_tsk_need_resched(prev); clear_preempt_need_resched(); ...... // 3. 當選出的繼任者和前任不同,就要進行上下文切換,繼任者程序正式進入執行 if (likely(prev != next)) { rq->nr_switches++; rq->curr = next; ++*switch_count; ...... rq = context_switch(rq, prev, next, &rf); ...... }
pick_next_task 的實現如下:
static inline struct task_struct * pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) { const struct sched_class *class; struct task_struct *p; /* * Optimization: we know that if all tasks are in the fair class we can call that function directly, * but only if the @prev task wasn't of a higher scheduling class, * because otherwise those loose the opportunity to pull in more work from other CPUs. */ if (likely((prev->sched_class == &idle_sched_class || prev->sched_class == &fair_sched_class) && rq->nr_running == rq->cfs.h_nr_running)) { p = fair_sched_class.pick_next_task(rq, prev, rf); if (unlikely(p == RETRY_TASK)) goto again; /* Assumes fair_sched_class->next == idle_sched_class */ if (unlikely(!p)) p = idle_sched_class.pick_next_task(rq, prev, rf); return p; } again: for_each_class(class) { p = class->pick_next_task(rq, prev, rf); if (p) { if (unlikely(p == RETRY_TASK)) goto again; return p; } } }
again 就是依次呼叫排程類。但是這裡有了一個優化,因為大部分程序是普通程序,所以大部分情況下會呼叫上面的邏輯,呼叫的就是 fair_sched_class.pick_next_task。
根據 fair_sched_class 的定義,它呼叫的是 pick_next_task_fair。
static struct task_struct * pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) { struct cfs_rq *cfs_rq = &rq->cfs; struct sched_entity *se; struct task_struct *p; int new_tasks;
對於 CFS 排程類,取出相應的佇列 cfs_rq,這就是那棵紅黑樹。
struct sched_entity *curr = cfs_rq->curr; if (curr) { if (curr->on_rq) update_curr(cfs_rq); else curr = NULL; ...... } se = pick_next_entity(cfs_rq, curr);
取出當前正在執行的任務 curr,如果依然是可執行的狀態,也即處於程序就緒狀態,則呼叫 update_curr 更新 vruntime。
update_curr 會根據實際執行時間算出 vruntime 來。
接著,pick_next_entity 從紅黑樹裡面,取最左邊的一個節點。
// 得到下一個排程實體對應的 task_struct p = task_of(se); // 如果發現繼任和前任不一樣,這就說明有一個更需要執行的程序了,就需要更新紅黑樹了。 if (prev != p) { struct sched_entity *pse = &prev->se; ...... // 前面前任的 vruntime 更新過了,put_prev_entity 放回紅黑樹,會找到相應的位置 put_prev_entity(cfs_rq, pse); // 將繼任者設為當前任務 set_next_entity(cfs_rq, se); } return p
程序上下文切換
上下文切換主要幹兩件事情,一是切換程序空間,也即虛擬記憶體;二是切換暫存器和 CPU 上下文。
/* * context_switch - switch to the new MM and the new thread's register state. */ static __always_inline struct rq * context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next, struct rq_flags *rf) { struct mm_struct *mm, *oldmm; ...... mm = next->mm; oldmm = prev->active_mm; ...... switch_mm_irqs_off(oldmm, mm, next); ...... /* Here we just switch the register state and the stack. */ switch_to(prev, next, prev); barrier(); return finish_task_switch(prev); }
switch_to 是暫存器和棧的切換,它呼叫到了 __switch_to_asm。這是一段彙編程式碼,主要用於棧的切換。
// 32 位作業系統 /* * %eax: prev task * %edx: next task */ ENTRY(__switch_to_asm) ...... /* switch stack */ movl %esp, TASK_threadsp(%eax) movl TASK_threadsp(%edx), %esp ...... jmp __switch_to END(__switch_to_asm) // 64 位作業系統 /* * %rdi: prev task * %rsi: next task */ ENTRY(__switch_to_asm) ...... /* switch stack */ movq %rsp, TASK_threadsp(%rdi) movq TASK_threadsp(%rsi), %rsp ...... jmp __switch_to END(__switch_to_asm)
最終,都返回了 __switch_to 這個函式。這個函式對於 32 位和 64 位作業系統雖然有不同的實現,但裡面做的事情是差不多的。所以這裡僅僅列出 64 位作業系統做的事情。
__visible __notrace_funcgraph struct task_struct * __switch_to(struct task_struct *prev_p, struct task_struct *next_p) { struct thread_struct *prev = &prev_p->thread; struct thread_struct *next = &next_p->thread; ...... int cpu = smp_processor_id(); struct tss_struct *tss = &per_cpu(cpu_tss, cpu); ...... load_TLS(next, cpu); ...... this_cpu_write(current_task, next_p); /* Reload esp0 and ss1. This changes current_thread_info(). */ load_sp0(tss, next); ...... return prev_p; }
所謂的程序切換,就是將某個程序的 thread_struct 裡面的暫存器的值,寫入到 CPU 的 TR 指向的 tss_struct,對於 CPU 來講,這就算是完成了切換。
例如 __switch_to 中的 load_sp0,就是將下一個程序的 thread_struct 的 sp0 的值載入到 tss_struct 裡面去。