2018-2019-120189224 《庖丁解牛Iinux內核分析》第九周學習總結
進程切換過程中有兩個重要問題:一是進行調度的時機;二是進程切換的過程。本次學習總結將圍繞以上兩部分內容展開。
進程調度的時機
進程切換過程
進程調度由操作系統內核進行,目的是合理分配系統資源,令每個進程都能獲得執行時間。進程調度由schedule函數負責,該函數是操作系統內核函數,並非系統調用,只能在內核態中由內核代碼主動調用。因此,用戶態進程無法主動進行進程調度,只能在中斷發生時被動調度。schedule函數的調用時機在之前系統調用課程中已經提及,位於系統調用返回后,返回用戶態代碼之前,內核可能會調用schedule。除了系統調用之外,軟中斷、硬終端以及異常都有可能進行進程調度。進程調度的具體任務是對上下文進行切換,即保存當前進程的上下文,加載將被調度進程的上下文。類似於系統調用的保護現場和恢復現場,但其中有本質區別:保護恢復現場涉及的是同一個進程的上下文,而進程調度的上下文切換則涉及兩個不同進程的上下文。
使用gdb跟蹤分析schedule
啟動實驗樓虛擬機,運行Linux內核,使用-s -S參數暫停內核運行。然后啟動gdb,讀取符號表、連接內核並設置斷點:schedule(),context_switch()。
schedule函數是進程調度的主體函數,其中的pick_next_task負責根據調度策略和調度算法選擇下一個進程。
2865asmlinkage __visible void __sched schedule(void)
2866{
2867 struct task_struct *tsk = current;
2868
2869 sched_submit_work(tsk);
2870 __schedule();
2871}
2872EXPORT_SYMBOL(schedule);
context_switch函數是schedule函數中實現進程切換的函數,上下文切換的關鍵代碼由宏switch_to給出:
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)
以上是switch_to中的關鍵代碼,可以看到這份代碼與my_schedule中的代碼十分相似。都是先將當前進程的上下文(包括flags、ebp等)壓入棧中保存,然后修改當前進程的eip,使當前進程下次執行時能從標號1的位置執行,最后加載將要被調度的進程的eip,新進程會從標號1處開始執行,按照順序從棧中獲得自己的上下文(新進程的ebp和flags等)。不同之處在於my_schedule利用ret命令執行新進程,而switch_to則通過跳轉到_switch_to來執行新進程。由於使用了jmp而不是call,執行_switch_to前沒有壓棧,而__switch_to執行完畢后要出棧,這就將新進程的下一條指令地址賦給了eip寄存器。
進程調度過程總結
進程調度是為了合理分配計算機資源,並讓每個進程都獲得適當的執行機會。由於進程調度函數schedule是內核態函數,且並非系統調用,故用戶態進程只能在發生中斷時被動地調度。而內核態線程即可以被動調度,也可以主動發起進程調度。被動進程調度的時機位於發生中斷並且系統執行完畢對應的中斷服務程序之后。
操作系統的一般執行狀態可描述如下: