考慮到文章篇幅,在這里我只討論普通進程,其調度算法采用的是cfs(完全公平)調度算法。 至于cfs調度算法的實現后面后專門寫一篇文章,這里只要記住調度時選擇一個優先級最高的任務執行
對于Linux內核來說,調度的基本單位是任務,用 struct task_Struct 表示,定義在include/linux/sched.h文件中,這個結構體包含一個任務的所有信息,結構體成員很多,在這里我只列出與本章內容有關的成員:
代碼語言:JavaScript代碼運行次數:0運行復制
struct task_struct {......(1)volatile long state;(2)const struct sched_class *sched_class;(3)void *stack;struct Thread_struct thread;struct mm_struct *mm, *active_mm;......}
(1)state :表示任務當前的狀態,當state為TASK_RUNNING時,表示任務處于可運行的狀態,并不一定表示目前正在占有CPU,也許在等待調度,調度器只會選擇在該狀態下的任務進行調度。該狀態確保任務可以立即運行,而不需要等待外部事件。
簡單來說就是:任務調度的對象是處于TASK_RUNNING狀態的任務。
處于TASK_RUNNING狀態的任務,可能正在執行用戶態代碼,也可能正在執行內核態的代碼。
(2)sched_class :表示任務所屬的調度器類,我們這里只講CFS調度類。
代碼語言:javascript代碼運行次數:0運行復制
// kernel/sched/sched.hstruct sched_class {......//將任務加入可運行的隊列中void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);//將任務移除可運行的隊列中void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);//選擇一下將要運行的任務struct task_struct * (*pick_next_task) (struct rq *rq,struct task_struct *prev,struct pin_cookie cookie);......}extern const struct sched_class fair_sched_class;
代碼語言:javascript代碼運行次數:0運行復制
// kernel/sched/fair.cconst struct sched_class fair_sched_class;/* * All the scheduling class methods: */const struct sched_class fair_sched_class = {.......enqueue_task= enqueue_task_fair, //CFS 的 enqueue_task 實例.dequeue_task= dequeue_task_fair,//CFS 的 dequeue_task 實例.pick_next_task= pick_next_task_fair,//CFS 的 pick_next_task 實例......}
(3)任務上下文:表示任務調度時要切換的任務上下文(任務切換只發生在內核態)。 stack:當前任務的內核態堆棧(用戶態的sp,用戶態的ip,在內核棧頂部的 pt_regs 結構里面) thread :也叫任務的硬件上下文,主要包含了大部分CPU寄存器(如:內核棧sp)。 struct mm_struct *mm :切換每個任務用戶態的虛擬地址空間(每個任務的用戶棧都是獨立的,都在內存空間里面,切換任務的虛擬地址空間,也就切換了任務的用戶棧)。
1.2 task_struct 結構體的產生
任務(task_struct) 的來源有三處: (1) fork():該函數是一個系統調用,可以復制一個現有的進程來創建一個全新的進程,產生一個 task_struct,然后調用wake_up_new_task()喚醒新的進程,使其進入TASK_RUNNING狀態。 (2) pthread_create():該函數是Glibc中的函數,然后調用clone()系統調用創建一個線程(又叫輕量級進程),產生一個 task_struct,然后調用wake_up_new_task()喚醒新的線程,使其進入TASK_RUNNING狀態。 (3)kthread_create():創建一個新的內核線程,產生一個 task_struct,然后wake_up_new_task(),喚醒新的內核線程,使其進入TASK_RUNNING狀態。
其實這三個API最后都會調用 _do_fork(),不同之處是傳入給 _do_fork() 的參數不同(clone_flags),最終結果就是進程有獨立的地址空間和棧,而用戶線程可以自己指定用戶棧,地址空間和父進程共享,內核線程則只有和內核共享的同一個棧,同一個地址空間。因此上述三個API最終都會創建一個task_struct結構。
備注:這里沒有討論vfok()。
總結:上述三個方式都產生了一個任務 task_struct,然后喚醒該任務使其處于TASK_RUNNING狀態,然后這樣調度器就可以調度 任務(task_struct)了。
還有一個來源就是0號進程(又叫 idle 進程),每個邏輯處理器上都有一個,屬于內核態線程,只有在沒有其他的任務處于TASK_RUNNING狀態時(系統此時處于空閑狀態),任務調度器才會選擇0號進程,然后重復執行 HLT 指令。 HLT 指令 :停止指令執行,并將處理器置于HALT狀態。簡單來說讓該CPU進入休眠狀態,低功耗狀態。 (該指令只能在 privilege level 0執行,且CPU指的是邏輯CPU而不是物理CPU,每個邏輯CPU都有一個idle進程)。

1.3 struct rq 結構體
目前的x86_64都有多個處理器,那么對于所有處于TASK_RUNNING狀態的任務是應該位于一個隊列還有每個處理器都有自己的隊列? Linux采用的是每個CPU都有自己的運行隊列,這樣做的好處: (1)每個CPU在自己的運行隊列上選擇任務降低了競爭 (2)某個任務位于一個CPU的運行隊列上,經過多次調度后,內核趨于選擇相同的CPU執行該任務,那么上次任務運行的變量很可能仍然在這個CPU緩存上,提高運行效率。
在這里我只討論普通任務的調度,因為linux大部分情況下都是在運行普通任務,普通任務選擇的調度器是CFS完全調度。
在調度時,調度器去 CFS 運行隊列找是否有任務需要運行。
代碼語言:javascript代碼運行次數:0運行復制
DEFINE_PER_CPU_SHAred_ALIGNED(struct rq, runqueues);
代碼語言:javascript代碼運行次數:0運行復制
struct rq { .......unsigned int nr_running; //運行隊列上可運行任務的個數struct cfs_rq cfs; // CFS 運行隊列struct task_struct *curr, //當前正在運行任務的 task_struct 實例struct task_struct *idle, //指向idle任務的實例,在沒有其它可運行任務的時候執行......}
二、schedule函數詳解2.1 schedule函數簡介
上文說到任務調度器是對于可運行狀態(TASK_RUNNING)的任務進行調度,如果任務的狀態不是TASK_RUNNING,那么該任務是不會被調度的。
調度的入口點是schedule()函數,定義在 kernel/sched/core.c 文件中,這里刪掉了很多代碼,只留了重要的代碼:
代碼語言:javascript代碼運行次數:0運行復制
asmlinkage __visible void __sched schedule(void){......preempt_disable();//禁止內核搶占__schedule(false);//任務開始調度,調度的過程中禁止其它任務搶占sched_preempt_enable_no_resched();//開啟內核搶占......}
這里注意schedule調用__schedule函數是傳入的是false參數,表示schedule函數主調度器。 調度分為主動調度和搶占調度。
__schedule的參數preempt是bool類型,表示本次調度是否為搶占調度:
__schedule的參數preempt等于0表示不是搶占調度,即主動調度,代表此次調度是該進程主動請求調度,主動調用了schedule函數(任務主動讓出處理器),比如該進程進入了阻塞態。 而schedule函數參數固定傳入的參數是false,也就是0,就是調用schedule函數就是主動發起調度,不是搶占調度,因此schedule函數稱為主調度器。
直接調用主調度器schdule函數的場景有3種: (1)當前進程需要等待某個條件滿足才能繼續運行時,調用一個wait_event()類函數將自己的狀態設為TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE,掛接到某個等待隊列,然后根據情況設置一個合適的喚醒定時器,最后調用schedule()發起調度;
代碼語言:javascript代碼運行次數:0運行復制
/* * The below macro ___wait_event() has an explicit shadow of the __ret * variable when used from the wait_event_*() macros. * * This is so that both can use the ___wait_cond_timeout() construct * to wrap the condition. * * The type inconsistency of the wait_event_*() __ret variable is also * on purpose; we use long where we can return timeout values and int * otherwise. */#define ___wait_event(wq, condition, state, exclusive, ret, cmd)({__label__ __out;wait_queue_t __wait;long __ret = ret;/* explicit shadow */init_wait_entry(&__wait, exclusive ? WQ_FLAG_EXCLUSIVE : 0);or (;;) {long __int = prepare_to_wait_event(&wq, &__wait, state);if (condition)reak;if (___wait_is_interruptible(state) && __int) {__ret = __int;goto __out;}cmd;}inish_wait(&wq, &__wait);__out:__ret;})#define __wait_event(wq, condition)(void)___wait_event(wq, condition, TASK_UNINTERRUPTIBLE, 0, 0, schedule())/** * wait_event - sleep until a condition gets true * @wq: the waitqueue to wait on * @condition: a C expression for the event to wait for * * The process is put to sleep (TASK_UNINTERRUPTIBLE) until the * @condition evaluates to true. The @condition is checked each time * the waitqueue @wq is woken up. * * wake_up() has to be called after changing any variable that could * change the result of the wait condition. */#define wait_event(wq, condition)do {might_sleep();if (condition)reak;__wait_event(wq, condition);} while (0)
(2)當前進程需要睡眠一段特定的時間(不等待任何事件)時,調用一個sleep()類函數將自己的狀態設為TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE但不進入任何等待隊列,然后設置一個合適的喚醒定時器,最后調用schedule()發起調度;
(3)當前進程單純地想要讓出CPU控制權時,調用yield()函數將自己的狀態設為TASK_RUNNING并依舊處于運行隊列,然后執行特定調度類的yield_task()操作,最后調用schedule()發起自愿調度。
代碼語言:javascript代碼運行次數:0運行復制
/** * sys_sched_yield - yield the current processor to other threads. * * This function yields the current CPU to other tasks. If there are no * other threads running on this CPU then this function will return. * * Return: 0. */SYSCALL_DEFINE0(sched_yield){struct rq *rq = this_rq_lock();schedstat_inc(rq->yld_count);current->sched_class->yield_task(rq);/* * Since we are going to call schedule() anyway, there's * no need to preempt or enable interrupts: */__release(rq->lock);spin_release(&rq->lock.dep_map, 1, _THIS_IP_);do_raw_spin_unlock(&rq->lock);sched_preempt_enable_no_resched();schedule();return 0;}
__schedule的參數preempt等于1表示是搶占調度,有處于運行態的任務發起的搶占調度。 例舉幾處發起搶占調度的地方:
代碼語言:javascript代碼運行次數:0運行復制
static void __sched notrace preempt_schedule_common(void){do {/* * Because the function tracer can trace preempt_count_sub() * and it also uses preempt_enable/disable_notrace(), if * NEED_RESCHED is set, the preempt_enable_notrace() called * by the function tracer will call this function again and * cause infinite recursion. * * Preemption must be disabled here before the function * tracer can trace. Break up preempt_disable() into two * calls. One to disable preemption without fear of being * traced. The other to still record the preemption latency, * which can also be traced by the function tracer. */preempt_disable_notrace();preempt_latency_start(1);//搶占調度__schedule(true);preempt_latency_stop(1);preempt_enable_no_resched_notrace();/* * Check again in case we missed a preemption opportunity * between schedule and now. */} while (need_resched());}#ifdef CONFIG_PREEMPT/* * this is the entry point to schedule() from in-kernel preemption * off of preempt_enable. Kernel preemptions off return from interrupt * occur there and call schedule directly. */asmlinkage __visible void __sched notrace preempt_schedule(void){/* * If there is a non-zero preempt_count or interrupts are disabled, * we do not want to preempt the current task. Just return.. */if (likely(!preemptible()))return;preempt_schedule_common();}NOKPROBE_SYMBOL(preempt_schedule);EXPORT_SYMBOL(preempt_schedule);/** * preempt_schedule_notrace - preempt_schedule called by tracing * * The tracing infrastructure uses preempt_enable_notrace to prevent * recursion and tracing preempt enabling caused by the tracing * infrastructure itself. But as tracing can happen in areas coming * from userspace or just about to enter userspace, a preempt enable * can occur before user_exit() is called. This will cause the scheduler * to be called when the system is still in usermode. * * To prevent this, the preempt_enable_notrace will use this function * instead of preempt_schedule() to exit user context if needed before * calling the scheduler. */asmlinkage __visible void __sched notrace preempt_schedule_notrace(void){enum ctx_state prev_ctx;if (likely(!preemptible()))return;do {/* * Because the function tracer can trace preempt_count_sub() * and it also uses preempt_enable/disable_notrace(), if * NEED_RESCHED is set, the preempt_enable_notrace() called * by the function tracer will call this function again and * cause infinite recursion. * * Preemption must be disabled here before the function * tracer can trace. Break up preempt_disable() into two * calls. One to disable preemption without fear of being * traced. The other to still record the preemption latency, * which can also be traced by the function tracer. */preempt_disable_notrace();preempt_latency_start(1);/* * Needs preempt disabled in case user_exit() is traced * and the tracer calls preempt_enable_notrace() causing * an infinite recursion. */prev_ctx = exception_enter();//搶占調度__schedule(true);exception_exit(prev_ctx);preempt_latency_stop(1);preempt_enable_no_resched_notrace();} while (need_resched());}EXPORT_SYMBOL_GPL(preempt_schedule_notrace);#endif /* CONFIG_PREEMPT *//* * this is the entry point to schedule() from kernel preemption * off of irq context. * Note, that this is called and return with irqs disabled. This will * protect us against recursive calling from irq. */asmlinkage __visible void __sched preempt_schedule_irq(void){enum ctx_state prev_state;/* Catch callers which need to be fixed */BUG_ON(preempt_count() || !irqs_disabled());prev_state = exception_enter();do {preempt_disable();local_irq_enable();//搶占調度__schedule(true);local_irq_disable();sched_preempt_enable_no_resched();} while (need_resched());exception_exit(prev_state);}
__schedule()是主調度器的主要函數,__schedule在內核源碼中有很多注釋,如下所示: 驅使調度器并因此進入此函數的主要方法有: 1.顯式阻塞:互斥、信號量、等待隊列等。 2.中斷和用戶空間返回路徑上檢查TIF_NEED_RESCHED標志。例如,請參考arch/x86/entry_64.S。 為了驅動任務之間的搶占,調度程序在定時器中斷處理程序scheduler_tick()中設置標志。 3.喚醒不會真正馬上調用schedule(),只是將一個任務添加到運行隊列中,設置任務標志位為TIF_NED_RESCHED,也就是將喚醒的進程加入的CFS就緒隊列中(將喚醒的進程調度實體加入到紅黑樹中),僅此而已。
現在,如果添加到運行隊列的新任務搶占了當前任務,則設置TIF_NED_RESCHED,并在以下的可能情況下調用schedule(),也就是喚醒的進程什么時候調用schedule()函數,分為以下兩種情況:
(1)如果內核可搶占(CONFIG_PREMPT=y): 在系統調用或異常上下文中,在下一次調用preempt_enable()時檢查是否需要搶占調度。 在IRQ上下文中,從中斷處理程序返回到可搶占上下文。硬件中斷處理返回前會檢查是否要搶占當前進程。
(2)如果內核不可搶占(未設置CONFIG_PREMPT) 調用cond_resched()。 顯式調用schedule()。 從syscall或異常返回到用戶空間。 從中斷處理程序返回到用戶空間。
2.2 __schedule 代碼解圖代碼語言:javascript代碼運行次數:0運行復制
//__schedule() is the main scheduler function.static void __sched notrace __schedule(bool preempt){ (1)struct task_struct *prev, *next;unsigned long *switch_count;struct pin_cookie cookie;struct rq *rq;int cpu; (2)cpu = smp_processor_id();rq = cpu_rq(cpu);prev = rq->curr;(3)if (!preempt && prev->state) {if (unlikely(signal_pending_state(prev->state, prev))) {prev->state = TASK_RUNNING;} else {deactivate_task(rq, prev, DEQUEUE_SLEEP);prev->on_rq = 0;}}(4)next = pick_next_task(rq, prev, cookie);clear_tsk_need_resched(prev);clear_preempt_need_resched();(5)if (likely(prev != next)) {rq->nr_switches++;rq->curr = next;++*switch_count;rq = context_switch(rq, prev, next, cookie); /* unlocks the rq */} else {lockdep_unpin_lock(&rq->lock, cookie);raw_spin_unlock_irq(&rq->lock);}}
(1)prev局部變量表示要切換出去的任務,next局部變量表示要切換進來的任務。
代碼語言:javascript代碼運行次數:0運行復制
struct task_struct *prev, *next;
(2)找到當前CPU的運行隊列 struct rq,把當前正在運行的任務curr 賦值給 prev。
代碼語言:javascript代碼運行次數:0運行復制
int cpu = smp_processor_id();struct rq = cpu_rq(cpu);struct task_struct *prev = rq->curr;
(3) preempt 用于判斷本次調度是否為搶占調度,如果發生了調度搶占(preempt =1),那么直接跳過,不參與判斷,直接調用pick_next_task。搶占調度通常都是處于運行態的任務發起的搶占調度。
如果本次調度不是搶占調度(preempt = 0),并且該進程的state不等于 TASK_RUNNING (0),也就是不是運行態,處于其他狀態。代表此次調度是該進程主動請求調度,主動調用了schedule函數,比如該進程進入了阻塞態。
進程在操作外部設備的時候(網絡和存儲則多是和外部設備的合作),往往需要讓出 CPU,發起主動調度。
由于進程不是運行態:TASK_RUNNING了,那么就不能在CFS就緒隊列中了,那么就調用 deactivate_task 將陷入阻塞態的進程移除CFS就緒隊列,并將進程調度實體的 on_rq 成員置0,表示不在CFS就緒隊列中了。 通常主動請求調用之前會提前設置當前進程的運行狀態為 TASK_INTERRUPTIBLE 或者 TASK_UNINTERRUPTIBLE。
代碼語言:javascript代碼運行次數:0運行復制
#define TASK_RUNNING0#define TASK_INTERRUPTIBLE1#define TASK_UNINTERRUPTIBLE2......
代碼語言:javascript代碼運行次數:0運行復制
if (!preempt && prev->state) {if (unlikely(signal_pending_state(prev->state, prev))) {prev->state = TASK_RUNNING;} else {deactivate_task(rq, prev, DEQUEUE_SLEEP);prev->on_rq = 0;}}
(4)選擇下一個將要執行的任務,通過CFS調度算法選擇優先級最高的一個任務。 clear_tsk_need_resched將被搶占的任務prev(也就是當前的任務)需要被調度的標志位(TIF_NEED_RESCHED)給清除掉,表示 prev 接下來不會被調度。 clear_preempt_need_resched 被搶占的任務prev(也就是當前的任務)的 PREEMPT_NEED_RESCHED 標志位給清除掉。
代碼語言:javascript代碼運行次數:0運行復制
if (likely(prev != next)) { //大概率事件,進行任務切換rq->nr_switches++; //可運行隊列切換次數更新rq->curr = next; //將當前任務curr設置為將要運行的下一個任務 next ++*switch_count; //任務切換次數更新//任務上下文切換rq = context_switch(rq, prev, next, cookie); /* unlocks the rq */} else { //小概率事件,不進行任務切換lockdep_unpin_lock(&rq->lock, cookie);raw_spin_unlock_irq(&rq->lock);}
(5)如果選擇的任務next和 原任務prev不是同一個任務,則進行任務上下文切換 如果是同一個任務,則不進行上下文切換。 注意這里是 用 likely()修飾(這是gcc內建的一條指令用于優化,編譯器可以根據這條指令對分支選擇進行優化),表示有很大的概率 選擇的任務next 和 原任務prev不是同一個任務。 由我們程序員來指明指令最可能的分支走向,以達到優化性能的效果。
代碼語言:javascript代碼運行次數:0運行復制
if (likely(prev != next)) { //大概率事件,進行任務切換rq->nr_switches++; //可運行隊列切換次數更新rq->curr = next; //將當前任務curr設置為將要運行的下一個任務 next ++*switch_count; //任務切換次數更新//任務上下文切換rq = context_switch(rq, prev, next, cookie); /* unlocks the rq */} else { //小概率事件,不進行任務切換lockdep_unpin_lock(&rq->lock, cookie);raw_spin_unlock_irq(&rq->lock);}
2.3 context_switch 代碼解讀
任務切換主要是任務空間即虛擬內存(用戶態的虛擬地址空間,包括了用戶態的堆棧)、CPU寄存器、內核態堆棧。
后面context_switch 還會專門一篇進行描述,這里限于篇幅,只是簡單描述一下。
用偽代碼表示:
代碼語言:javascript代碼運行次數:0運行復制
switch_mm();switch_to(){switch_register();switch_stack();}
代碼語言:javascript代碼運行次數:0運行復制
/* * 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 pin_cookie cookie){struct mm_struct *mm, *oldmm;(1)prepare_task_switch(rq, prev, next);(2)mm = next->mm;oldmm = prev->active_mm;if (!mm) {next->active_mm = oldmm;atomic_inc(&oldmm->mm_count);enter_lazy_tlb(oldmm, next);} elseswitch_mm_irqs_off(oldmm, mm, next); (3)/* Here we just switch the register state and the stack. */switch_to(prev, next, prev);(4)return finish_task_switch(prev);}
(1)開始任務切換前,需要做的準備工作,這里主要是提供了2個接口給我們內核開發者,當任務切換時我們可以自己添加一些操作進去,任務被重新調度時我們也可以自己添加一些操作進去。 同時通知我們任務被搶占(sched_out)。
代碼語言:javascript代碼運行次數:0運行復制
prepare_task_switch(rq, prev, next);-->fire_sched_out_preempt_notifiers(prev, next); -->__fire_sched_out_preempt_notifiers(curr, next); -->hlist_for_each_entry(notifier, &curr->preempt_notifiers, link)notifier->ops->sched_out(notifier, next);
這里的 sched_in()函數和 sched_out函數是內核提供給我們開發者的接口。我們可以通過在這兩個接口里面添加一些操作。 sched_in :任務重新調度時會通知我們。 sched_out:任務被搶占時會通知我們。
備注:調度器運行調度相關的代碼,但其自身并不作為一個單獨的 process 存在,在進程切換時,執行 switch out 的代碼就是在被換出的 process 中,執行 switch in 的代碼就是在被換入的 process 中,因此 scheduler 沒有一個對應的 PID。
具體請參考:Linux 進程調度通知機制
代碼語言:javascript代碼運行次數:0運行復制
struct preempt_ops {void (*sched_in)(struct preempt_notifier *notifier, int cpu);void (*sched_out)(struct preempt_notifier *notifier, struct task_struct *next);};struct preempt_notifier {struct hlist_node link;struct preempt_ops *ops;};
(2) 切換任務的用戶虛擬態地址(不切換內核態的虛擬地址),也包括了用戶態的棧,主要就是切換了任務的CR3, CR3寄存器放的是 頁目錄表物理內存基地址。
代碼語言:javascript代碼運行次數:0運行復制
mm = next->mm;oldmm = prev->active_mm;if (!mm) { //mm == NULL,代表任務是內核線程// 直接用 被切換進程prev的active_mmnext->active_mm = oldmm;atomic_inc(&oldmm->mm_count);//通知處理器不需要切換虛擬地址空間的用戶空間部分,用來加速上下文切換enter_lazy_tlb(oldmm, next);} else//不是內核線程,那么就要切換用戶態的虛擬地址空間,也就是切換任務的CR3switch_mm_irqs_off(oldmm, mm, next);-->load_cr3(next->pgd); //加載下一個任務的CR3

(3)切換任務的寄存器和內核態堆棧,保存原任務(prev)的所有寄存器信息,恢復新任務(next)的所有寄存器信息,當切換完之后,并執行新的任務。
代碼語言:javascript代碼運行次數:0運行復制
switch_to(prev, next, prev);-->__switch_to_asm((prev), (next)))-->ENTRY(__switch_to_asm)/* switch stack */movq%rsp, TASK_threadsp(%rdi)movqTASK_threadsp(%rsi), %rspjmp__switch_toEND(__switch_to_asm)-->__switch_to()
(4) 完成一些清理工作,使其能夠正確的釋放鎖。這個清理工作的完成是第三個任務,系統中隨機的某個其它任務。同時通知我們任務被重新調度(sched_in)。 這里其實也有點復雜,我們后面在 context_switch 篇重點描述。
代碼語言:javascript代碼運行次數:0運行復制
finish_task_switch(prev)-->fire_sched_in_preempt_notifiers(current);-->__fire_sched_in_preempt_notifiers(curr)-->hlist_for_each_entry(notifier, &curr->preempt_notifiers, link)notifier->ops->sched_in(notifier, raw_smp_processor_id());
總結 這篇文章主要講了schdule主調度器的工作流程,即一個運行中的進程主動調用 _schedule 讓出 CPU,總結來說就是: (1)通過調度算法選擇一個優先級最高的任務。 (2)進行任務上下文切換,上下文切換又分用戶態進程空間的切換和內核態的切換。 保存原任務的所有寄存器信息,恢復新任務的所有寄存器信息,并執行新的任務。 (3)上下文切換完后,新的任務投入運行。 如下圖所示:
