2019-2020-1 20199304《Linux內核原理與分析》第九周做業

第八章 進程的切換和系統的通常執行過程

知識點

1.進程調度的時機

1.1硬中斷和軟中斷
中斷是指在計算機執行期間,系統內發生任何非尋常的或非預期的急需處理事件,使得CPU暫時中斷當前正在執行的程序而轉去執行相應的時間處理程序。待處理完畢後又返回原來被中斷處繼續執行或調度新的進程執行的過程。
引發中斷的事件稱爲中斷源。中斷源向CPU提出處理的請求稱爲中斷請求。發生中斷時被打斷程序的暫停點稱爲斷點。CPU暫停現行程序而轉爲響應中斷請求的過程稱爲中斷響應。處理中斷源的程序稱爲中斷處理程序。CPU執行有關的中斷處理程序稱爲中斷處理。而返回斷點的過程稱爲中斷返回。中斷的實現由軟件和硬件綜合完成,硬件部分叫作硬件裝置,軟件部分稱爲軟件處理程序。linux

  • 硬中斷:由與系統相連的外設(好比網卡、硬盤)自動產生的。主要是用來通知操做系統系統外設狀態的變化。好比當網卡收到數據包的時候,就會發出一箇中斷。咱們一般所說的中斷指的是硬中斷(hardirq)。
  • 軟中斷:爲了知足實時系統的要求,中斷處理應該是越快越好。linux爲了實現這個特色,當中斷髮生的時候,硬中斷處理那些短期就能夠完成的工做,而將那些處理事件比較長的工做,放到中斷以後來完成,也就是軟中斷(softirq)來完成。
  • 異常:
    • 故障(Fault):出現問題,能夠恢復到當前指令。
    • 退出(Abort):不可恢復的嚴重故障,致使程序沒法繼續運行,只能退出。
    • 陷阱(Trap):程序主動產生的異常。
      1.2進程調度時機
  • schedule函數
    • Linux內核經過schedule函數實現進程調度,schedule函數在運行隊列中找到一個進程,把CPU分配給它。因此調用schedule函數的時候就是進程調度的時機。
  • 上下文
    通常來講,CPU在任什麼時候刻都處於如下3種狀況之一。算法

    • 運行於用戶空間,執行用戶進程上下文。
    • 運行於內核空間,處於進程(通常是內核線程)上下文。
    • 運行於內核空間,處於中斷上下文。
      運行在進程上下文的內核代碼是能夠被搶佔的(Linux2.6支持搶佔)。可是一箇中斷上下文,一般都會始終佔有CPU(固然中斷能夠嵌套,但咱們通常不這樣作),不能夠被打斷。正由於如此,運行在中斷上下文的代碼就要受一些限制,不能作下面的事情:
    • 睡眠或者放棄CPU:這樣作的後果是災難性的,由於內核在進入中斷以前會關閉進程調度,一旦睡眠或者放棄CPU,這時內核沒法調度別的進程來執行,系統就會死掉。
    • 嘗試得到信號量:若是得到不到信號量,代碼就會睡眠,會產生和上面相同的狀況。
    • 執行耗時的任務:中斷處理應該儘量快,由於內核要響應大量服務和請求,中斷上下文佔用CPU時間太長會嚴重影響系統功能。
    • 訪問用戶空間的虛擬地址:由於中斷上下文是和特定進程無關的,它是內核表明硬件運行在內核空間,因此在終端上下文沒法訪問用戶空間的虛擬地址。
  • 進程調度
    簡單總結進程調度時機以下:
    • 用戶進程經過特定的系統調用主動讓出CPU。
    • 中斷處理程序在內核返回用戶態時進行調度。
    • 內核線程主動調用schedule函數讓出CPU。
    • 中斷處理程序主動調用schedule函數讓出CPU,涵蓋第一和第二種狀況。架構

      2.調度策略和算法

      調度算法就是從就緒隊列中選一個進程。調度策略是尋找知足需求的方法,而調度算法是如何實現這個調度策略。
      2.1 進程的分類
      按CPU佔用率分類:ide

  • I/O消耗型進程。這種進程的特色是CPU負載不高,大量時間都在等待讀寫數據
  • 處理器消耗型進程。這種進程的特色是CPU佔用率爲100%,但沒有太多的硬件進行讀寫操做
  • 按對系統響應時間要求分類
  • 交互式進程。此類進程有大量人機交互,對系統響應時間要求比較高,不然用戶會感受系統反應遲緩。
  • 批處理進程。此類進程不須要人機交互,在後臺運行,須要佔用大量的系統資源,可是可以忍受響應延遲。
  • 實時進程。實時進程對調度延遲的要求最高,這些進程執行很是重要的操做,要求當即執行響應並執行。
    當前Linux系統的解決方案是,對於實時進程,linux採用FIFO(先進先出)或者Round Robin(時間片輪轉)的調度策略。對其它進程,則採用CFS調度器,核心是「徹底公平」。函數

2.2 調度策略
Linux系統中經常使用的幾種調度策略爲SCHED_NORMAL、SCHED_FIFO、SCHED_RR。
其中SCHED_NORMAL是用於普通進程的調度類,而SCHED_FIFO和SCHED_RR是用於實時進程的調度類,優先級高於SCHED_NORMAL。內核根據進程的優先級來區分普通進程與實時進程,Linux內核進程優先級爲0~139,數值越高,優先級越低,0爲最高優先級。實時進程的優先級取值爲0~99,普通進程只具備nice值,nice值映射到優先級爲100~139。學習

2.3 CFS調度算法
CFS即爲徹底公平調度算法,其基本原理是基於權重的動態優先級調度算法。每一個進程使用CPU的順序進程由已使用的CPU虛擬時間(vruntime)決定,已使用的虛擬時間越少,進程排序就越靠前,進程再次被調度執行的機率也就越高。每一個進程每次佔用CPU後可以執行的時間(ideal_runtime)由進程的權重決定,而且保證在某個時間週期(_sched_period)內運行隊列的因此進程都可以至少被調度執行一次。atom

3. 進程上下文切換

3.1 進程執行環境的切換
爲了控制進程的執行,內核必須有能力掛起正在CPU中運行的進程,並恢復執行之前掛起的某個進程。這種行爲被稱爲進程切換,任務切換或進程上下文切換。進程上下文包含了進程執行須要的全部信息。idea

  • 用戶空間地址:包括程序代碼、數據、用戶堆棧等。
  • 控制信息:進程描述符、內核堆棧等。
  • 硬件上下文,相關寄存器的值。
    在實際的代碼中,每一個進程切換基本由兩個步驟組成:操作系統

  • 切換頁全局目錄(CR3)以安裝一個新的地址空間,這樣不一樣進程的虛擬地址就會通過不一樣的頁錶轉換爲不一樣的物理地址。
  • 切換內核態堆棧和硬件上下文,由於硬件上下文提供了內核執行新進程所須要的全部信息。
    線程

4.Linux系統的運行過程

  • Linux系統的通常執行過程:正在運行的用戶態進程X切換到用戶態進程Y的過程:
    • 1.正在運行的用戶態進程X;
    • 2.發生中斷(包括異常,系統調用等),硬件完成如下動做:
      • save cs:eip/esp/eflags:當前CPU上下文壓入用戶態進程X的內核堆棧;
      • 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,從Y進程的內核堆棧中彈出(2)中硬件完成的壓棧內容;
    • 8.繼續運行用戶態進程Y。
      Linux操做系統的總體架構以下圖所示

實驗:進程調度相關源代碼跟綜和分析

實驗要求

  • 理解Linux系統中進程調度的時機,能夠在內核代碼中搜索schedule()函數,看都是哪裏調用了schedule(),判斷咱們課程內容中的總結是否準確;
  • 使用gdb跟蹤分析一個schedule()函數 ,驗證您對Linux系統進程調度與進程切換過程的理解;推薦在實驗樓Linux虛擬機環境下完成實驗;
  • 特別關注並仔細分析switch_to中的彙編代碼,理解進程上下文的切換機制,以及與中斷上下文切換的關係;

實驗過程

1.從新克隆一個menu,而後從新編譯內核。



2.打開調試模式,另打開一個窗口進行gdb遠程調試,配置gdb遠程調試並設置斷點。




關鍵代碼分析

context_switch關鍵代碼部分

static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next)
{
    ...
    arch_start_context_switch(prev);

    if (unlikely(!mm)) {    //若是被切換進來的進程的mm爲空切換,內核線程mm爲空
        next->active_mm = oldmm;  //將共享切換出去的進程的active_mm
        atomic_inc(&oldmm->mm_count);  //有一個進程共享,全部引用計數加一
        enter_lazy_tlb(oldmm, next);  //將per cpu變量cpu_tlbstate狀態設爲LAZY
    } else   //普通mm不爲空,則調用switch_mm切換地址空間
        switch_mm(oldmm, mm, next);
...
    //這裏切換寄存器狀態和棧 
    switch_to(prev, next, prev);

switch_to關鍵代碼部分

#define switch_to(prev, next, last)
do {
    /*
     * Context-switching clobbers all registers, so we clobber
     * them explicitly, via unused output variables.
     * (EAX and EBP is not listed because EBP is saved/restored
     * explicitly for wchan access and EAX is the return value of
     * __switch_to())
     */
    unsigned long ebx, ecx, edx, esi, edi;

    asm volatile(
             "pushfl\n\t"  //保存當前進程flags
             "pushl %%ebp\n\t"  //當前進程堆棧基址壓棧
             "movl %%esp,%[prev_sp]\n\t"  //保存ESP,將當前堆棧棧頂保存起來
             "movl %[next_sp],%%esp\n\t"  //更新ESP,將下一棧頂保存到ESP中
                     // 完成內核堆棧的切換
             "movl $1f,%[prev_ip]\n\t"    //保存當前進程的EIP
             "pushl %[next_ip]\n\t"       //將next進程起點壓入堆棧,即next進程的棧頂爲起點
             __switch_canary              //next_ip通常爲$1f,對於新建立的子進程是ret_from_fork      
             "jmp __switch_to\n"    //prve進程中,設置next進程堆棧,jmp與call不一樣,是經過寄存器傳遞參數(call經過堆棧),因此ret時彈出的是以前壓入棧頂的next進程起點
             //完成EIP的切換
             "1:\t"            //next進程開始執行       
             "popl %%ebp\n\t"  //restore EBP
             "popfl\n"         //restore flags

             //輸出量
             : [prev_sp] "=m" (prev->thread.sp),   //保存當前進程的esp
               [prev_ip] "=m" (prev->thread.ip),     //保存當前進倉的eip
               "=a" (last),

               //要破壞的寄存器
               "=b" (ebx), "=c" (ecx), "=d" (edx),
               "=S" (esi), "=D" (edi)

               __switch_canary_oparam

              //輸入量
             : [next_sp]  "m" (next->thread.sp),   //next進程的內核堆棧棧頂地址,即esp
               [next_ip]  "m" (next->thread.ip),     //next進程的eip

               // regparm parameters for __switch_to(): 
               [prev]     "a" (prev),
               [next]     "d" (next)

               __switch_canary_iparam

             : //從新加載段寄存器
            "memory");
} while (0)

總結

經過本次實驗,我學習到了進程調度的時機和進程切換進行,分析進程的調度時機,調度策略和算法,並跟蹤schedule,pick_next_task和context_switch等函數。 我瞭解到進程調度是爲了合理分配計算機資源,並讓每一個進程都得到適當的執行機會。 而在中斷處理過程(包括時鐘中斷、I/O中斷、系統調用和異常)中,系統直接調用schedule(),或者返回用戶態時根據need_resched標記調用schedule()。 用戶態進程沒法實現主動調度,僅能經過陷入內核態後的某個時機點進行調度,即在中斷處理過程當中進行調度。

相關文章
相關標籤/搜索