郭垚 原創做品轉載請註明出處 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000算法
【學習視頻時間:1小時40分鐘 實驗時間:1小時35分鐘 撰寫博客時間:2小時40分鐘】shell
【學習內容:進程切換、Linux系統的通常執行過程、Linux系統架構】架構
操做系統原理中介紹了大量進程調度算法,這些算法從實現的角度看僅僅是從運行隊列中選擇一個新進程,選擇的過程當中運用了不一樣的策略而已。 對於理解操做系統的工做機制,反而是進程的調度時機與進程的切換機制更爲關鍵。函數
1. 進程調度的時機學習
schedule()函數實現調度:this
2. 不一樣類型的進程有不一樣的調度需求atom
第一種分類:spa
第二種分類:操作系統
【用戶態進程只能被動調度,內核線程是隻有內核態沒有用戶態的特殊進程。】線程
1. 爲了控制進程的執行,內核必須有能力掛起正在CPU上執行的進程,並恢復之前掛起的某個進程的執行,這叫作進程切換、任務切換、上下文切換。
2. 掛起正在CPU上執行的進程,與中斷時保存現場是不一樣的,中斷先後是在同一個進程上下文中,只是由用戶態轉向內核態執行。
3. 進程上下文包含了進程執行須要的全部信息
4. schedule()函數選擇一個新的進程來運行,並調用context_ switch進行上下文的切換,這個宏調用switch_ to來進行關鍵上下文切換
switch_ to利用了prev和next兩個參數:prev指向當前進程,next指向被調度的進程
31#define switch_to(prev, next, last) 32do { 33 /* 34 * Context-switching clobbers all registers, so we clobber 35 * them explicitly, via unused output variables. 36 * (EAX and EBP is not listed because EBP is saved/restored 37 * explicitly for wchan access and EAX is the return value of 38 * __switch_to()) 39 */ 40 unsigned long ebx, ecx, edx, esi, edi; 41 42 asm volatile("pushfl\n\t" /* save flags */ 43 "pushl %%ebp\n\t" /* save EBP */ 44 "movl %%esp,%[prev_sp]\n\t" /* save ESP */ 45 "movl %[next_sp],%%esp\n\t" /* restore ESP */ 46 "movl $1f,%[prev_ip]\n\t" /* save EIP */ 47 "pushl %[next_ip]\n\t" /* restore EIP */ 48 __switch_canary 49 "jmp __switch_to\n" /* regparm call */ 50 "1:\t" 51 "popl %%ebp\n\t" /* restore EBP */ 52 "popfl\n" /* restore flags */ 53 54 /* output parameters */ 55 : [prev_sp] "=m" (prev->thread.sp), 56 [prev_ip] "=m" (prev->thread.ip), 57 "=a" (last), 58 59 /* clobbered output registers: */ 60 "=b" (ebx), "=c" (ecx), "=d" (edx), 61 "=S" (esi), "=D" (edi) 62 63 __switch_canary_oparam 64 65 /* input parameters: */ 66 : [next_sp] "m" (next->thread.sp), 67 [next_ip] "m" (next->thread.ip), 68 69 /* regparm parameters for __switch_to(): */ 70 [prev] "a" (prev), 71 [next] "d" (next) 72 73 __switch_canary_iparam 74 75 : /* reloaded segment registers */ 76 "memory"); 77} while (0)
5. schedule()函數代碼分析
建立一些局部變量
struct task_struct *prev, *next;//當前進程和一下個進程的進程結構體 unsigned long *switch_count;//進程切換次數 struct rq *rq;//就緒隊列 int cpu;
關閉內核搶佔,初始化一部分變量
need_resched: preempt_disable();//關閉內核搶佔 cpu = smp_processor_id(); rq = cpu_rq(cpu);//與CPU相關的runqueue保存在rq中 rcu_note_context_switch(cpu); prev = rq->curr;//將runqueue當前的值賦給prev
選擇next進程
next = pick_next_task(rq, prev);//挑選一個優先級最高的任務排進隊列 clear_tsk_need_resched(prev);//清除prev的TIF_NEED_RESCHED標誌。 clear_preempt_need_resched();
完成進程的調度
next = pick_next_task(rq, prev);//挑選一個優先級最高的任務排進隊列 clear_tsk_need_resched(prev);//清除prev的TIF_NEED_RESCHED標誌。 clear_preempt_need_resched();
以上代碼中context_switch(rq,prev,next)完成了從prev到next的進程上下文的切換。
6. 進程切換上下文的代碼分析
schedule()函數選擇一個新的進程來運行
next = pick_next_task(rq, prev); clear_tsk_need_resched(prev); clear_preempt_need_resched(); rq->skip_clock_update = 0;
經過context_switch完成進程上下文切換
2336context_switch(struct rq *rq, struct task_struct *prev, 2337 struct task_struct *next) 2338{ 2339 struct mm_struct *mm, *oldmm; 2340 2341 prepare_task_switch(rq, prev, next); 2342 2343 mm = next->mm; 2344 oldmm = prev->active_mm; 2350 arch_start_context_switch(prev); 2351 2352 if (!mm) { 2353 next->active_mm = oldmm; 2354 atomic_inc(&oldmm->mm_count); 2355 enter_lazy_tlb(oldmm, next); 2356 } else 2357 switch_mm(oldmm, mm, next); 2358 2359 if (!prev->mm) { 2360 prev->active_mm = NULL; 2361 rq->prev_mm = oldmm; 2362 } 2369 spin_release(&rq->lock.dep_map, 1, _THIS_IP_); 2370 2371 context_tracking_task_switch(prev, next); 2373 switch_to(prev, next, prev); 2374 2375 barrier(); 2381 finish_task_switch(this_rq(), prev); 2382}
switch_ to函數代碼分析
注意:
next進程曾經是prev進程,nex執行完後執行的「下一個」實際上是剛剛被切換的進程
42 asm volatile("pushfl\n\t" /* save flags */ 43 "pushl %%ebp\n\t" /* save EBP */ 44 "movl %%esp,%[prev_sp]\n\t" /* save ESP */ 45 "movl %[next_sp],%%esp\n\t" /* restore ESP */ 46 "movl $1f,%[prev_ip]\n\t" /* save EIP */ 47 "pushl %[next_ip]\n\t" /* restore EIP */ 48 __switch_canary 49 "jmp __switch_to\n" /* regparm call */ 50 "1:\t" 51 "popl %%ebp\n\t" /* restore EBP */ 52 "popfl\n" /* restore flags */ 53 54 /* output parameters */ 55 : [prev_sp] "=m" (prev->thread.sp), 56 [prev_ip] "=m" (prev->thread.ip), 57 "=a" (last), 58 59 /* clobbered output registers: */ 60 "=b" (ebx), "=c" (ecx), "=d" (edx), 61 "=S" (esi), "=D" (edi) 62 63 __switch_canary_oparam 64 65 /* input parameters: */ 66 : [next_sp] "m" (next->thread.sp), 67 [next_ip] "m" (next->thread.ip), 68 69 /* regparm parameters for __switch_to(): */ 70 [prev] "a" (prev), 71 [next] "d" (next) 72 73 __switch_canary_iparam 74 75 : /* reloaded segment registers */ 76 "memory"); 77} while (0)
最通常的狀況:正在運行的用戶態進程X切換到運行用戶態進程Y的過程
幾種特殊狀況:
1. 操做系統的基本概念及目的
2. 典型的Linux操做系統架構
1. 執行gets()函數
2. 執行系統調用,陷入內核
3. 等待輸入,CPU會調度其餘進程執行,同時wait一個I/O中斷
4. 輸入ls,發I/O中斷給CPU,中斷處理程序進行現場保存、壓棧等等
5. 中斷處理程序發現X進程在等待這個I/O(此時X已經變成阻塞態),處理程序將X設置爲WAKE_UP
6. 進程管理可能會把進程X設置爲next進程,這樣gets系統調用得到數據,再返回用戶態堆棧
7. 從CPU執行指令的角度看:
8. 從內存角度看,全部的物理地址都會被映射到3G以上的地址空間。由於這部分對全部進程來講都是共享的:
跟蹤調試schedule()函數的執行過程以下。由下圖可知進程調度時,首先進入schedule()函數,將一個task_ struct結構體的指針tsk賦值爲當前進程,而後調用sched_ submit_ work(tsk)。
進入sched_ submit_ work(tsk)函數查看它的工做:sched_ submit_ work主要做用是避免死鎖。
由上圖可知該函數時檢測tsk->state是否爲0 (runnable)若爲運行態時則返回, tsk_ is_ pi_ blocked(tsk),檢測tsk的死鎖檢測器是否爲空,若非空的話就return。
進入schedule()函數,schedule()是切換進程的真正代碼:
整個schedule的執行過程能夠用下面的流程圖表示:
本週視頻主要講解了進程切換的關鍵代碼switch_ to分析、Linux系統的通常執行過程、Linux系統架構和執行過程。從中我瞭解到schedule()函數實現進程調度,context_ switch完成進程上下文切換,switch_ to完成寄存器的切換。在調度時機方面,內核線程能夠直接調用schedule()進行進程切換,也能夠在中斷處理過程當中進行調度,也就是說內核線程做爲一類的特殊的進程能夠主動調度,也能夠被動調度。而用戶態進程沒法實現主動調度,僅能經過陷入內核態後的某個時機點進行調度,即在中斷處理過程當中進行調度。