1. 程式人生 > >第一次作業:基於Linux內核源碼進程模型分析

第一次作業:基於Linux內核源碼進程模型分析

動態性 進程控制 可能 資源分配 自己 解鎖 develop 時間片輪轉調度 因此

一、關於進程

1.1 什麽是進程?

進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。程序是指令、數據及其組織形式的描述,進程是程序的實體。

簡單的說,一個進程就是一個正在運行的程序。

1.2 進程的生命周期

創建: 每個進程都是由其父進程創建進程可以創建子進程,子進程又可以創建子進程的子進程

運行: 多個進程可以同時存在進程間可以通信

撤銷: 進程可以被撤銷,從而結束一個進程的運行

(1)進程的創建:進程是通過調用::fork(),::vfork()和::clone()系統調用創建新進程。在內核中,它們都是調用do_fork實現的。傳統的fork函數直接把父進程的所有資源復制給子進程。而Linux的::fork()使用寫時拷貝頁實現,也就是說,父進程和子進程共享同一個資源拷貝,只有當數據發生改變時,數據才會發生復制。通常的情況,子進程創建後會立即調用exec(),這樣就避免復制父進程的全部資源。

(2)進程的撤銷:進程通過調用exit()退出執行,這個函數會終結進程並釋放所有的資源。父進程可以通過wait4()查詢子進程是否終結。進程退出執行後處於僵死狀態,直到它的父進程調用wait()或者waitpid()為止。父進程退出時,內核會指定線程組的其他進程或者init進程作為其子進程的新父進程。當進程接收到一個不能處理或忽視的信號時,或當在內核態產生一個不可恢復的CPU異常而內核此時正代表該進程在運行,內核可以強迫進程終止。

(3)在Linux系統中,shell命令ps -lA可以查看當前系統的進程,例如:

技術分享圖片

1.3進程的特性

動態性、獨立性、並發性是進程的三大特性。

(1) 動態性

在程序運行的過程中,它的狀態是在不斷變化的。例如一個程序在運行過程中,它是一條指令接著一條指令執行,而每執行一條指令,CPU中那些通用寄存器的值也會發生變化,程序計數器(Program Counter)的值也在變化,每次都指向下一條即將執行的指令。另外堆和棧的內容也在不斷變化,數據在不斷進棧出棧,堆空間在不斷分配和釋放。總之變化無時無刻不在進行。

(2) 獨立性

一個進程是一個獨立的實體,是計算機系統資源的使用單位。每個進程都有"自己"的寄存器和內部狀態,在它運行的時候獨立於其他的進程。當然這個"自己"是帶引號的,也就是說:在物理上,CPU中只存在一套寄存器,如PC寄存器只有一個,但是沒有進程都有屬於自己的邏輯上的PC。物理上的寄存器是真正的硬件寄存器。

(3) 並發性

對於單CPU的情況,從宏觀上來看,每個進程是同時在系統中運行的,而實際上從微觀上來看,在某一特定時刻,只有一個程序運行,換言之各個進程之間實際上是一個接一個順序運行的。因為CPU是有一個,那麽某一個時刻只能有一個進程去使用它。

二、進程的組織

在Linux中,每個進程在創建時都會被分配一個數據結構,稱為進程控制塊(Process Control Block,簡稱PCB),在Linux中叫做任務結構體(task struct)(見下圖),在linux/sched.h中定義。這之中包含了很多重要的信息,供系統調度和進程本身執行使用。所有進程的PCB都存放在內核空間中。PCB中最重要的信息就是進程PID,內核通過這個PID來唯一標識一個進程。PID可以循環使用,最大值是32768。init進程的pid為1,其他進程都是init進程的後代。除了進程控制塊(PCB)以外,每個進程都有獨立的內核堆棧(8k),一個進程描述符結構,這些數據都作為進程的控制信息儲存在內核空間中;而進程的用戶空間主要存儲代碼和數據

技術分享圖片

三、進程的狀態

3.1 進程的狀態

(1)運行狀態(TASK_RUNNING):指正在被CPU運行或者就緒的狀態。這樣的進程被成為runnning進程。運行態的進程可以分為3種情況:內核運行態、用戶運行態、就緒態。

(2)可中斷睡眠狀態(TASK_INTERRUPTIBLE):處於等待狀態中的進程,一旦被該進程等待的資源被釋放,那麽該進程就會進入運行狀態。

(3)不可中斷睡眠狀態(TASK_UNINTERRUPTIBLE):該狀態的進程只能用wake_up()函數喚醒。

(4)暫停狀態(TASK_STOPPED):當進程收到信號SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU時就會進入暫停狀態。可向其發送SIGCONT信號讓進程轉換到可運行狀態。

(5)僵死狀態(TASK_ZOMBIE):當進程已經終止運行,但是父進程還沒有詢問其狀態的情況。
只有當進程從“內核運行態”轉移到“睡眠狀態”時,內核才會進行進程切換操作。在內核態下運行的進程不能被其它進程搶占,而且一個進程不能改變另一個進程的狀態。為了避免進程切換時造成內核數據錯誤,內核在執行臨界區代碼時會禁止一切中斷。
態,它是進程正在等待某個事件或某個資源時所處的狀態。 等待態進一步分為可中斷的等待態和不可中斷的等待態。處於可中斷等待態的進程可以由信號(signal)解除其等待態。處於不可中斷等待態的進程,一般是直接或間接等待硬件條件。 它只能用特定的方式來解除,例如使用喚醒函數wake_up()等。

3.2進程的狀態轉化圖

技術分享圖片

四、進程的調度

4.1調度的基本原理

調度的實質就是資源的分配。系統通過不同的調度算法(Scheduling Algorithm)來實現這種資源的分配。通常來說,選擇什麽樣的調度算法取決於的資源分配的策略(Scheduling Policy

4.2 Linux進程調度的目標

1.高效性:高效意味著在相同的時間下要完成更多的任務。調度程序會被頻繁的執行,所以調度程序要盡可能的高效;

2.加強交互性能:在系統相當的負載下,也要保證系統的響應時間;

3.保證公平和避免饑渴;

4.SMP調度:調度程序必須支持多處理系統;

5.軟實時調度:系統必須有效的調用實時進程,但不保證一定滿足其要求;

4.3 進程饑餓

進程饑餓,即為Starvation,指當等待時間給進程推進和響應帶來明顯影響稱為進程饑餓。當饑餓到一定程度的進程在等待到即使完成也無實際意義的時候稱為饑餓死亡。

產生饑餓的主要原因:

在一個動態系統中,對於每類系統資源,操作系統需要確定一個分配策略,當多個進程同時申請某類資源時,由分配策略確定資源分配給進程的次序。

有時資源分配策略可能是不公平的,即不能保證等待時間上界的存在。在這種情況下,即使系統沒有發生死鎖,某些進程也可能會長時間等待.當等待時間給進程推進和響應帶來明顯影響時,稱發生了進程饑餓,當饑餓到一定程度的進程所賦予的任務即使完成也不再具有實際意義時稱該進程被餓死。

舉個例子,當有多個進程需要打印文件時,如果系統分配打印機的策略是最短文件優先,那麽長文件的打印任務將由於短文件的源源不斷到來而被無限期推遲,導致最終的饑餓甚至餓死。

4.4 調度算法及其基本原理

1.時間片輪轉調度算法

時間片(Time Slice)就是分配給進程運行的一段時間。在通常的輪轉法中,系統將所有的可運行(即就緒)進程按先來先服務的原則,排成一個隊列,每次調度時把CPU分配給隊首進程,並令其執行一個時間片。當執行的時間片用完時,系統發出信號,通知調度程序,調度程序便據此信號來停止該進程的執行,並將它送到運行隊列的末尾,等待下一次執行;然後,把處理機分配給就緒隊列中新的隊首進程,同時也讓它執行一個時間片。這樣就可以保證運行隊列中的所有進程,在一個給定的時間內,均能獲得一時間片的處理機執行時間。

2.優先權調度算法

為了照顧到緊迫型進程在進入系統後便能獲得優先處理,引入了最高優先權調度算法。當將該算法用於進程調度時,系統將把處理機分配給運行隊列中優先權最高的進程,這時,又可進一步把該算法分成兩種方式:

(1) 非搶占式優先權算法(又稱不可剝奪調度:Nonpreemptive Scheduling

在這種方式下,系統一旦將處理機(CPU)分配給運行隊列中優先權最高的進程後,該進程便一直執行下去,直至完成;或因發生某事件使該進程放棄處理機時,系統方可將處理機分配給另一個優先權高的進程。這種調度算法主要用於批處理系統中,也可用於某些對實時性要求不嚴的實時系統中。

(2) 搶占式優先權調度算法(又稱可剝奪調度:Preemptive Scheduling

該算法的本質就是系統中當前運行的進程永遠是可運行進程中優先權最高的那個。在采用這種調度算法時,每當出現一新的可運行進程,就將它和當前運行進程進行優先權比較,如果高於當前進程,將觸發進程調度。這種方式的優先權調度算法,能更好的滿足緊迫進程的要求,故而常用於要求比較嚴格的實時系統中,以及對性能要求較高的批處理和分時系統中。Linux也采用這種調度算法。

3.多級反饋隊列調度

這是時下最時髦的一種調度算法。其本質是:綜合了時間片輪轉調度和搶占式優先權調度的優點,即:優先權高的進程先運行給定的時間片,相同優先權的進程輪流運行給定的時間片。

4.實時調度

最後我們來看一下實時系統中的調度。什麽叫實時系統,就是系統對外部事件有求必應、盡快響應。在實時系統中,廣泛采用搶占調度方式,特別是對於那些要求嚴格的實時系統。因為這種調度方式既具有較大的靈活性,又能獲得很小的調度延遲;但是這種調度方式也比較復雜。

4.5 Linux進程調度時機

Linux的調度程序是一個叫Schedule()的函數,這個函數被調用的頻率很高,由它來決定是否要進行進程的切換,如果要切換的話,切換到哪個進程等等。我們先來看在什麽情況下要執行調度程序,我們把這種情況叫做調度時機。

Linux調度時機主要有:

1、進程狀態轉換的時刻:進程終止、進程睡眠;

2、當前進程的時間片用完時(current->counter=0);

3、設備驅動程序主動調用schedule

4、進程從中斷、異常及系統調用返回到用戶態時;

時機1,進程要調用sleep()或exit()等函數進行狀態轉換,這些函數會主動調用調度程序進行進程調度;

時機2,由於進程的時間片是由時鐘中斷來更新的,因此,這種情況和時機4是一樣的。

時機3,當設備驅動程序執行長而重復的任務時,直接調用調度程序。在每次反復循環中,驅動程序都檢查need_resched的值,如果必要,則調用調度程序schedule()主動放棄CPU

時機4,如前所述,不管是從中斷、異常還是系統調用返回,最終都調用ret_from_sys_call(),由這個函數進行調度標誌的檢測,如果必要,則調用調度程序。那麽,為什麽從系統調用返回時要調用調度程序呢?這當然是從效率考慮。從系統調用返回意味著要離開內核態而返回到用戶態,而狀態的轉換要花費一定的時間,因此,在返回到用戶態前,系統把在內核態該處理的事全部做完。

每個時鐘中斷(timer interrupt)發生時,由三個函數協同工作,共同完成進程的選擇和切換,它們是:schedule()、do_timer()及ret_form_sys_call()。

schedule():進程調度函數,由它來完成進程的選擇(調度);

do_timer():暫且稱之為時鐘函數,該函數在時鐘中斷服務程序中被調用,被調用的頻率就是時鐘中斷的頻率即每秒鐘100次(簡稱100赫茲或100Hz);

ret_from_sys_call():系統調用返回函數。當一個系統調用或中斷完成時,該函數被調用,用於處理一些收尾工作,例如信號處理、核心任務等等。

4.6 進程調度的實現

調度程序內核函數:

asmlinkage void schedule(void)
{
 
  struct task_struct *prev, *next, *p; /* prev表示調度之前的進程, next表示調度之後的進程 */  
    struct list_head *tmp;
    int this_cpu, c;
 
      if (!current->active_mm) BUG();/*如果當前進程的的active_mm為空,出錯*/
need_resched_back:             
           prev = current;         /*讓prev成為當前進程 */
           this_cpu = prev->processor;
 
if (in_interrupt()) {/*如果schedule是在中斷服務程序內部執行,
就說明發生了錯誤*/
           printk("Scheduling in interrupt/n");
              BUG();
        }
   release_kernel_lock(prev, this_cpu); /*釋放全局內核鎖,並開this_cpu的中斷*/
       spin_lock_irq(&runqueue_lock); /*鎖住運行隊列,並且同時關中斷*/
        if (prev->policy == SCHED_RR) /*將一個時間片用完的SCHED_RR實時
               goto move_rr_last;      進程放到隊列的末尾 */
 move_rr_back:
        switch (prev->state) {     /*根據prev的狀態做相應的處理*/
               case TASK_INTERRUPTIBLE:  /*此狀態表明該進程可以被信號中斷*/
                        if (signal_pending(prev)) { /*如果該進程有未處理的信號,則讓其變為可運行狀態*/
                               prev->state = TASK_RUNNING;
                                break;
                        }
                 default:     /*如果為不可中斷的等待狀態或僵死狀態*/
                        del_from_runqueue(prev); /*從運行隊列中刪除*/
                case TASK_RUNNING:;/*如果為可運行狀態,繼續處理*/
         }
         prev->need_resched = 0;
 
     /*下面是調度程序的正文 */
repeat_schedule:    /*真正開始選擇值得運行的進程*/
        next = idle_task(this_cpu); /*缺省選擇空閑進程*/
   c = -1000;
     if (prev->state == TASK_RUNNING)
          goto still_running;
still_running_back:
    list_for_each(tmp, &runqueue_head) { /*遍歷運行隊列*/
       p = list_entry(tmp, struct task_struct, run_list);
 if (can_schedule(p, this_cpu)) { /*單CPU中,該函數總返回1*/                          int weight = goodness(p, this_cpu, prev->active_mm);
               if (weight > c)
                   c = weight, next = p;
           }
     }          
               
/* 如果c為0,說明運行隊列中所有進程的權值都為0,也就是分配給各個進程的
     時間片都已用完,需重新計算各個進程的時間片 */  
    if  (!c) {
             struct task_struct *p;
             spin_unlock_irq(&runqueue_lock);/*鎖住運行隊列*/
              read_lock(&tasklist_lock);  /* 鎖住進程的雙向鏈表*/
             for_each_task(p)            /* 對系統中的每個進程*/
             p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
             read_unlock(&tasklist_lock);
                 spin_lock_irq(&runqueue_lock);
               goto repeat_schedule;
        }
 
 
     spin_unlock_irq(&runqueue_lock);/*對運行隊列解鎖,並開中斷*/
 
       if (prev == next) {     /*如果選中的進程就是原來的進程*/
            prev->policy &= ~SCHED_YIELD;
               goto same_process;
      }
 
          /* 下面開始進行進程切換*/
       kstat.context_swtch++; /*統計上下文切換的次數*/
   
        {
               struct mm_struct *mm = next->mm;
               struct mm_struct *oldmm = prev->active_mm;
              if (!mm) {  /*如果是內核線程,則借用prev的地址空間*/
                      if (next->active_mm) BUG();
                      next->active_mm = oldmm;
                 
              } else { /*如果是一般進程,則切換到next的用戶空間*/
                       if (next->active_mm != mm) BUG();
                       switch_mm(oldmm, mm, next, this_cpu);
              }
 
            if (!prev->mm) { /*如果切換出去的是內核線程*/
                  prev->active_mm = NULL;/*歸還它所借用的地址空間*/
                    mmdrop(oldmm);     /*mm_struct中的共享計數減1*/
               }
        }
    
        switch_to(prev, next, prev); /*進程的真正切換,即堆棧的切換*/
        __schedule_tail(prev);  /*置prev->policy的SCHED_YIELD為0 */
 
same_process:
       reacquire_kernel_lock(current);/*針對SMP*/
        if (current->need_resched)    /*如果調度標誌被置位*/
               goto need_resched_back; /*重新開始調度*/
        return;
}

五、對Linux系統進程模型的看法

Linux是目前最流行的幾個操作系統之一,通過對Linux進程模型的初步了解,為了管理進程,內核必須對每個進程所做的事情進行清楚的描述。因此,內核不是進程,而是進程的管理者。而調度程序利用task_struct之中的信息決定系統中哪個進程最應該運行,並結合進程的狀態信息保證系統運轉的公平和高效。方便了進程的管理,提高了cpu的效率。

六、參考資料

https://blog.csdn.net/sailor_8318/article/details/2452983

http://www.jb51.net/LINUXjishu/66846.html

https://www.ibm.com/developerworks/cn/linux/l-completely-fair-scheduler/index.html?ca=drs-cn-0125

https://blog.csdn.net/u013291303/article/details/68954599

https://www.cnblogs.com/biyeymyhjob/archive/2012/08/01/2617884.html

第一次作業:基於Linux內核源碼進程模型分析