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

1.3 struct rq 結(jié)構(gòu)體
目前的x86_64都有多個處理器,那么對于所有處于TASK_RUNNING狀態(tài)的任務(wù)是應(yīng)該位于一個隊列還有每個處理器都有自己的隊列? Linux采用的是每個CPU都有自己的運行隊列,這樣做的好處: (1)每個CPU在自己的運行隊列上選擇任務(wù)降低了競爭 (2)某個任務(wù)位于一個CPU的運行隊列上,經(jīng)過多次調(diào)度后,內(nèi)核趨于選擇相同的CPU執(zhí)行該任務(wù),那么上次任務(wù)運行的變量很可能仍然在這個CPU緩存上,提高運行效率。
在這里我只討論普通任務(wù)的調(diào)度,因為Linux大部分情況下都是在運行普通任務(wù),普通任務(wù)選擇的調(diào)度器是CFS完全調(diào)度。
在調(diào)度時,調(diào)度器去 CFS 運行隊列找是否有任務(wù)需要運行。
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
DEFINE_PER_CPU_SHAred_ALIGNED(struct rq, runqueues);
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
struct rq { .......unsigned int nr_running; //運行隊列上可運行任務(wù)的個數(shù)struct cfs_rq cfs; // CFS 運行隊列struct task_struct *curr, //當(dāng)前正在運行任務(wù)的 task_struct 實例struct task_struct *idle, //指向idle任務(wù)的實例,在沒有其它可運行任務(wù)的時候執(zhí)行......}
二、schedule函數(shù)詳解2.1 schedule函數(shù)簡介
上文說到任務(wù)調(diào)度器是對于可運行狀態(tài)(TASK_RUNNING)的任務(wù)進(jìn)行調(diào)度,如果任務(wù)的狀態(tài)不是TASK_RUNNING,那么該任務(wù)是不會被調(diào)度的。
調(diào)度的入口點是schedule()函數(shù),定義在 kernel/sched/core.c 文件中,這里刪掉了很多代碼,只留了重要的代碼:
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
asmlinkage __visible void __sched schedule(void){......preempt_disable();//禁止內(nèi)核搶占__schedule(false);//任務(wù)開始調(diào)度,調(diào)度的過程中禁止其它任務(wù)搶占sched_preempt_enable_no_resched();//開啟內(nèi)核搶占......}
這里注意schedule調(diào)用__schedule函數(shù)是傳入的是false參數(shù),表示schedule函數(shù)主調(diào)度器。 調(diào)度分為主動調(diào)度和搶占調(diào)度。
__schedule的參數(shù)preempt是bool類型,表示本次調(diào)度是否為搶占調(diào)度:
__schedule的參數(shù)preempt等于0表示不是搶占調(diào)度,即主動調(diào)度,代表此次調(diào)度是該進(jìn)程主動請求調(diào)度,主動調(diào)用了schedule函數(shù)(任務(wù)主動讓出處理器),比如該進(jìn)程進(jìn)入了阻塞態(tài)。 而schedule函數(shù)參數(shù)固定傳入的參數(shù)是false,也就是0,就是調(diào)用schedule函數(shù)就是主動發(fā)起調(diào)度,不是搶占調(diào)度,因此schedule函數(shù)稱為主調(diào)度器。
直接調(diào)用主調(diào)度器schdule函數(shù)的場景有3種: (1)當(dāng)前進(jìn)程需要等待某個條件滿足才能繼續(xù)運行時,調(diào)用一個wait_event()類函數(shù)將自己的狀態(tài)設(shè)為TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE,掛接到某個等待隊列,然后根據(jù)情況設(shè)置一個合適的喚醒定時器,最后調(diào)用schedule()發(fā)起調(diào)度;
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
/* * 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)當(dāng)前進(jìn)程需要睡眠一段特定的時間(不等待任何事件)時,調(diào)用一個sleep()類函數(shù)將自己的狀態(tài)設(shè)為TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE但不進(jìn)入任何等待隊列,然后設(shè)置一個合適的喚醒定時器,最后調(diào)用schedule()發(fā)起調(diào)度;
(3)當(dāng)前進(jìn)程單純地想要讓出CPU控制權(quán)時,調(diào)用yield()函數(shù)將自己的狀態(tài)設(shè)為TASK_RUNNING并依舊處于運行隊列,然后執(zhí)行特定調(diào)度類的yield_task()操作,最后調(diào)用schedule()發(fā)起自愿調(diào)度。
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
/** * 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的參數(shù)preempt等于1表示是搶占調(diào)度,有處于運行態(tài)的任務(wù)發(fā)起的搶占調(diào)度。 例舉幾處發(fā)起搶占調(diào)度的地方:
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
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);//搶占調(diào)度__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();//搶占調(diào)度__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();//搶占調(diào)度__schedule(true);local_irq_disable();sched_preempt_enable_no_resched();} while (need_resched());exception_exit(prev_state);}
__schedule()是主調(diào)度器的主要函數(shù),__schedule在內(nèi)核源碼中有很多注釋,如下所示: 驅(qū)使調(diào)度器并因此進(jìn)入此函數(shù)的主要方法有: 1.顯式阻塞:互斥、信號量、等待隊列等。 2.中斷和用戶空間返回路徑上檢查TIF_NEED_RESCHED標(biāo)志。例如,請參考arch/x86/entry_64.S。 為了驅(qū)動任務(wù)之間的搶占,調(diào)度程序在定時器中斷處理程序scheduler_tick()中設(shè)置標(biāo)志。 3.喚醒不會真正馬上調(diào)用schedule(),只是將一個任務(wù)添加到運行隊列中,設(shè)置任務(wù)標(biāo)志位為TIF_NED_RESCHED,也就是將喚醒的進(jìn)程加入的CFS就緒隊列中(將喚醒的進(jìn)程調(diào)度實體加入到紅黑樹中),僅此而已。
現(xiàn)在,如果添加到運行隊列的新任務(wù)搶占了當(dāng)前任務(wù),則設(shè)置TIF_NED_RESCHED,并在以下的可能情況下調(diào)用schedule(),也就是喚醒的進(jìn)程什么時候調(diào)用schedule()函數(shù),分為以下兩種情況:
(1)如果內(nèi)核可搶占(CONFIG_PREMPT=y): 在系統(tǒng)調(diào)用或異常上下文中,在下一次調(diào)用preempt_enable()時檢查是否需要搶占調(diào)度。 在IRQ上下文中,從中斷處理程序返回到可搶占上下文。硬件中斷處理返回前會檢查是否要搶占當(dāng)前進(jìn)程。
(2)如果內(nèi)核不可搶占(未設(shè)置CONFIG_PREMPT) 調(diào)用cond_resched()。 顯式調(diào)用schedule()。 從syscall或異常返回到用戶空間。 從中斷處理程序返回到用戶空間。
2.2 __schedule 代碼解圖代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
//__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局部變量表示要切換出去的任務(wù),next局部變量表示要切換進(jìn)來的任務(wù)。
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
struct task_struct *prev, *next;
(2)找到當(dāng)前CPU的運行隊列 struct rq,把當(dāng)前正在運行的任務(wù)curr 賦值給 prev。
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
int cpu = smp_processor_id();struct rq = cpu_rq(cpu);struct task_struct *prev = rq->curr;
(3) preempt 用于判斷本次調(diào)度是否為搶占調(diào)度,如果發(fā)生了調(diào)度搶占(preempt =1),那么直接跳過,不參與判斷,直接調(diào)用pick_next_task。搶占調(diào)度通常都是處于運行態(tài)的任務(wù)發(fā)起的搶占調(diào)度。
如果本次調(diào)度不是搶占調(diào)度(preempt = 0),并且該進(jìn)程的state不等于 TASK_RUNNING (0),也就是不是運行態(tài),處于其他狀態(tài)。代表此次調(diào)度是該進(jìn)程主動請求調(diào)度,主動調(diào)用了schedule函數(shù),比如該進(jìn)程進(jìn)入了阻塞態(tài)。
進(jìn)程在操作外部設(shè)備的時候(網(wǎng)絡(luò)和存儲則多是和外部設(shè)備的合作),往往需要讓出 CPU,發(fā)起主動調(diào)度。
由于進(jìn)程不是運行態(tài):TASK_RUNNING了,那么就不能在CFS就緒隊列中了,那么就調(diào)用 deactivate_task 將陷入阻塞態(tài)的進(jìn)程移除CFS就緒隊列,并將進(jìn)程調(diào)度實體的 on_rq 成員置0,表示不在CFS就緒隊列中了。 通常主動請求調(diào)用之前會提前設(shè)置當(dāng)前進(jìn)程的運行狀態(tài)為 TASK_INTERRUPTIBLE 或者 TASK_UNINTERRUPTIBLE。
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
#define TASK_RUNNING0#define TASK_INTERRUPTIBLE1#define TASK_UNINTERRUPTIBLE2......
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
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)選擇下一個將要執(zhí)行的任務(wù),通過CFS調(diào)度算法選擇優(yōu)先級最高的一個任務(wù)。 clear_tsk_need_resched將被搶占的任務(wù)prev(也就是當(dāng)前的任務(wù))需要被調(diào)度的標(biāo)志位(TIF_NEED_RESCHED)給清除掉,表示 prev 接下來不會被調(diào)度。 clear_preempt_need_resched 被搶占的任務(wù)prev(也就是當(dāng)前的任務(wù))的 PREEMPT_NEED_RESCHED 標(biāo)志位給清除掉。
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
if (likely(prev != next)) { //大概率事件,進(jìn)行任務(wù)切換rq->nr_switches++; //可運行隊列切換次數(shù)更新rq->curr = next; //將當(dāng)前任務(wù)curr設(shè)置為將要運行的下一個任務(wù) next ++*switch_count; //任務(wù)切換次數(shù)更新//任務(wù)上下文切換rq = context_switch(rq, prev, next, cookie); /* unlocks the rq */} else { //小概率事件,不進(jìn)行任務(wù)切換lockdep_unpin_lock(&rq->lock, cookie);raw_spin_unlock_irq(&rq->lock);}
(5)如果選擇的任務(wù)next和 原任務(wù)prev不是同一個任務(wù),則進(jìn)行任務(wù)上下文切換 如果是同一個任務(wù),則不進(jìn)行上下文切換。 注意這里是 用 likely()修飾(這是gcc內(nèi)建的一條指令用于優(yōu)化,編譯器可以根據(jù)這條指令對分支選擇進(jìn)行優(yōu)化),表示有很大的概率 選擇的任務(wù)next 和 原任務(wù)prev不是同一個任務(wù)。 由我們程序員來指明指令最可能的分支走向,以達(dá)到優(yōu)化性能的效果。
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
if (likely(prev != next)) { //大概率事件,進(jìn)行任務(wù)切換rq->nr_switches++; //可運行隊列切換次數(shù)更新rq->curr = next; //將當(dāng)前任務(wù)curr設(shè)置為將要運行的下一個任務(wù) next ++*switch_count; //任務(wù)切換次數(shù)更新//任務(wù)上下文切換rq = context_switch(rq, prev, next, cookie); /* unlocks the rq */} else { //小概率事件,不進(jìn)行任務(wù)切換lockdep_unpin_lock(&rq->lock, cookie);raw_spin_unlock_irq(&rq->lock);}
2.3 context_switch 代碼解讀
任務(wù)切換主要是任務(wù)空間即虛擬內(nèi)存(用戶態(tài)的虛擬地址空間,包括了用戶態(tài)的堆棧)、CPU寄存器、內(nèi)核態(tài)堆棧。
后面context_switch 還會專門一篇進(jìn)行描述,這里限于篇幅,只是簡單描述一下。
用偽代碼表示:
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
switch_mm();switch_to(){switch_register();switch_stack();}
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
/* * 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)開始任務(wù)切換前,需要做的準(zhǔn)備工作,這里主要是提供了2個接口給我們內(nèi)核開發(fā)者,當(dāng)任務(wù)切換時我們可以自己添加一些操作進(jìn)去,任務(wù)被重新調(diào)度時我們也可以自己添加一些操作進(jìn)去。 同時通知我們?nèi)蝿?wù)被搶占(sched_out)。
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
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()函數(shù)和 sched_out函數(shù)是內(nèi)核提供給我們開發(fā)者的接口。我們可以通過在這兩個接口里面添加一些操作。 sched_in :任務(wù)重新調(diào)度時會通知我們。 sched_out:任務(wù)被搶占時會通知我們。
備注:調(diào)度器運行調(diào)度相關(guān)的代碼,但其自身并不作為一個單獨的 process 存在,在進(jìn)程切換時,執(zhí)行 switch out 的代碼就是在被換出的 process 中,執(zhí)行 switch in 的代碼就是在被換入的 process 中,因此 scheduler 沒有一個對應(yīng)的 PID。
具體請參考:Linux 進(jìn)程調(diào)度通知機(jī)制
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
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) 切換任務(wù)的用戶虛擬態(tài)地址(不切換內(nèi)核態(tài)的虛擬地址),也包括了用戶態(tài)的棧,主要就是切換了任務(wù)的CR3, CR3寄存器放的是 頁目錄表物理內(nèi)存基地址。
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
mm = next->mm;oldmm = prev->active_mm;if (!mm) { //mm == NULL,代表任務(wù)是內(nèi)核線程// 直接用 被切換進(jìn)程prev的active_mmnext->active_mm = oldmm;atomic_inc(&oldmm->mm_count);//通知處理器不需要切換虛擬地址空間的用戶空間部分,用來加速上下文切換enter_lazy_tlb(oldmm, next);} else//不是內(nèi)核線程,那么就要切換用戶態(tài)的虛擬地址空間,也就是切換任務(wù)的CR3switch_mm_irqs_off(oldmm, mm, next);-->load_cr3(next->pgd); //加載下一個任務(wù)的CR3

(3)切換任務(wù)的寄存器和內(nèi)核態(tài)堆棧,保存原任務(wù)(prev)的所有寄存器信息,恢復(fù)新任務(wù)(next)的所有寄存器信息,當(dāng)切換完之后,并執(zhí)行新的任務(wù)。
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
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) 完成一些清理工作,使其能夠正確的釋放鎖。這個清理工作的完成是第三個任務(wù),系統(tǒng)中隨機(jī)的某個其它任務(wù)。同時通知我們?nèi)蝿?wù)被重新調(diào)度(sched_in)。 這里其實也有點復(fù)雜,我們后面在 context_switch 篇重點描述。
代碼語言:javascript代碼運行次數(shù):0運行復(fù)制
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());
總結(jié) 這篇文章主要講了schdule主調(diào)度器的工作流程,即一個運行中的進(jìn)程主動調(diào)用 _schedule 讓出 CPU,總結(jié)來說就是: (1)通過調(diào)度算法選擇一個優(yōu)先級最高的任務(wù)。 (2)進(jìn)行任務(wù)上下文切換,上下文切換又分用戶態(tài)進(jìn)程空間的切換和內(nèi)核態(tài)的切換。 保存原任務(wù)的所有寄存器信息,恢復(fù)新任務(wù)的所有寄存器信息,并執(zhí)行新的任務(wù)。 (3)上下文切換完后,新的任務(wù)投入運行。 如下圖所示:
