《Linux內核分析》第八週學習筆記

《Linux內核分析》第八週學習筆記 進程的切換和系統的通常執行過程

郭垚 原創做品轉載請註明出處 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000算法

【學習視頻時間:1小時40分鐘 實驗時間:1小時35分鐘 撰寫博客時間:2小時40分鐘】shell

【學習內容:進程切換、Linux系統的通常執行過程、Linux系統架構】架構

1、進程切換的關鍵代碼switch_to分析

1.1 進程調度與進程調度的時機分析

  操做系統原理中介紹了大量進程調度算法,這些算法從實現的角度看僅僅是從運行隊列中選擇一個新進程,選擇的過程當中運用了不一樣的策略而已。 對於理解操做系統的工做機制,反而是進程的調度時機與進程的切換機制更爲關鍵。函數

1. 進程調度的時機學習

schedule()函數實現調度:this

  • 中斷處理過程(包括時鐘中斷、I/O中斷、系統調用和異常)中,直接調用schedule(),或者返回用戶態時根據need_resched標記調用schedule()
  • 內核線程能夠直接調用schedule()進行進程切換,也能夠在中斷處理過程當中進行調度,也就是說內核線程做爲一類的特殊的進程能夠主動調度,也能夠被動調度
  • 用戶態進程沒法實現主動調度,僅能經過陷入內核態後的某個時機點進行調度,即在中斷處理過程當中進行調度

2. 不一樣類型的進程有不一樣的調度需求atom

  • 第一種分類:spa

    • I/O-bound 頻繁的進行I/O;一般會花費不少時間等待I/O操做的完成
    • CPU-bound 計算密集型;須要大量的CPU時間進行運算
  • 第二種分類:操作系統

    • 批處理進程
    • 實時進程
    • 交互式進程shell

【用戶態進程只能被動調度,內核線程是隻有內核態沒有用戶態的特殊進程。】線程

1.2 進程上下文切換相關代碼分析

1. 爲了控制進程的執行,內核必須有能力掛起正在CPU上執行的進程,並恢復之前掛起的某個進程的執行,這叫作進程切換、任務切換、上下文切換。

2. 掛起正在CPU上執行的進程,與中斷時保存現場是不一樣的,中斷先後是在同一個進程上下文中,只是由用戶態轉向內核態執行。

3. 進程上下文包含了進程執行須要的全部信息

  • 用戶地址空間:包括程序代碼,數據,用戶堆棧等
  • 控制信息:進程描述符,內核堆棧等
  • 硬件上下文(注意中斷也要保存硬件上下文只是保存的方法不一樣)

4. schedule()函數選擇一個新的進程來運行,並調用context_ switch進行上下文的切換,這個宏調用switch_ to來進行關鍵上下文切換

  • next = pick_ next_ task(rq, prev);//進程調度算法都封裝這個函數內部
  • context_ switch(rq, prev, next);//進程上下文切換
  • 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函數代碼分析

  • 注意:

    • 下圖42行保存當前進程的flags
    • 下圖43行把當前進程的堆棧基址壓棧
    • switch_ to完成寄存器的切換:先保存當前進程的寄存器,再進行堆棧切換(下圖第4四、45行)自此後全部的壓棧都是在新進程的堆棧中了,再切換eip(下圖4六、56行),這樣當前進程能夠重新進程中恢復,還有其餘必要的切換
    • next_ ip通常是$1f(對於新建立的進程來講就是ret_ from_ fork)
    • 下圖47行代表下一進程棧頂是起點。next_ ip通常是$1f,對於新建立的子進程是ret_ from_fork
    • 下圖49行jmp _ switch_ to是函數調用,經過寄存器傳遞參數;函數執行結束return的時候從下一條指令開始(便是新進程的開始)
    • 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)

 

2、Linux系統的通常執行過程

2.1 Linux系統的通常執行過程分析

最通常的狀況:正在運行的用戶態進程X切換到運行用戶態進程Y的過程

  1. 正在運行的用戶態進程X
  2. 發生中斷——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).
  3. SAVE_ALL //保存現場
  4. 中斷處理過程當中或中斷返回前調用了schedule(),其中的switch_to作了關鍵的進程上下文切換
  5. 標號1以後開始運行用戶態進程Y(這裏Y曾經經過以上步驟被切換出去過所以能夠從標號1繼續執行)
  6. restore_all //恢復現場
  7. iret - pop cs:eip/ss:esp/eflags from kernel stack
  8. 繼續運行用戶態進程Y

2.2 Linux系統的通常執行過程當中的幾個特殊狀況

幾種特殊狀況:

  1. 經過中斷處理過程當中的調度時機,用戶態進程與內核線程之間互相切換和內核線程之間互相切換,與最通常的狀況很是相似,只是內核線程運行過程當中發生中斷沒有進程用戶態和內核態的轉換
  2. 內核線程主動調用schedule(),只有進程上下文的切換,沒有發生中斷上下文的切換,與最通常的狀況略簡略
  3. 建立子進程的系統調用在子進程中的執行起點及返回用戶態,如fork
  4. 加載一個新的可執行程序後返回到用戶態的狀況,如execve

2.3 內核與舞女

  1. 進程的地址空間一共有4G,其中0——3G是用戶態能夠訪問,3G以上只有內核態能夠訪問
  2. 內核至關於出租車,能夠爲每個「招手」的進程提供內核態到用戶態的轉換。
  3. 沒有進程須要「承載」的時候,內核進入idle0號進程進行「空轉」。當用戶進程有需求時,內核發生中斷,幫助用戶進程完成請求,而後再返回到用戶進程。就好像Taxi將用戶載了一圈以後又把用戶放下來。
  4. 3G以上的部分就是這樣的「出租車」,是全部進程共享的,在內核態部分切換的時候就比較容易
  5. 內核是各類中斷處理程序和內核線程的集合。

3、Linux系統架構和執行過程概覽

3.1 Linux操做系統架構概覽

1. 操做系統的基本概念及目的

2. 典型的Linux操做系統架構

3.2 最簡單、最複雜的操做——執行ls命令

3.3 從CPU和內存的角度看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以上的地址空間。由於這部分對全部進程來講都是共享的:

4、實驗

使用gdb跟蹤分析一個schedule()函數 ,驗證對Linux系統進程調度與進程切換過程的理解

跟蹤調試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()進行進程切換,也能夠在中斷處理過程當中進行調度,也就是說內核線程做爲一類的特殊的進程能夠主動調度,也能夠被動調度。而用戶態進程沒法實現主動調度,僅能經過陷入內核態後的某個時機點進行調度,即在中斷處理過程當中進行調度。

相關文章
相關標籤/搜索