1. 程式人生 > >linux程序排程(2)

linux程序排程(2)

1.程序的排程

作為多程序的系統,Linux系統必須擔負起排程程序的責任,不斷地切換程序,以使CPU得到最大化的利用,提高系統的效率。

1.1 Linux程序排程的策略

程序排程的策略主要考慮以下幾個原則

(1) 高效 — 使處理器的利用率最高,空閒最小;
(2) 公平 — 使每一個申請處理器的程序都得到合理的處理器時間;
(3) 週轉時間短 — 使使用者提交任務後得到結果的時間儘可能短;
(4) 吞吐量大— 使單位時間內處理的任務數量儘可能多;
(5) 響應時間短 — 使對每個使用者響應的時間儘可能短。

我們知道Linux有兩種型別的程序:實時和非實時(普通)。

實時程序比所有其它程序的優先順序高。如果有一個實時的程序準備執行,那麼它總是被執行。

實時程序有兩種策略

時間片輪轉或先進先出(round robin and first in first out)。
時間片輪轉的排程策略下,每一個實時程序依次執行
而在先進先出的策略下,每一個可以執行的程序按照它在排程佇列中的順序執行,這個順序不會改變。

對於非實時程序,Linux永遠選擇優先順序最高的程序來執行。

1.2 Linux程序的排程演算法

1. 時間片輪轉排程演算法
用於實時程序。系統使每個程序依次地按時間片輪流執行的方式。

2. 優先權排程演算法


用於非實時程序。系統選擇執行佇列中優先順序最高的程序執行。
Linux採用搶佔式的優先順序演算法,即系統中當前執行的程序永遠是可執行程序中優先權最高的那個。

3. FIFO(先進先出) 排程演算法
前面已經提到過,實時程序按排程策略分為兩種。
採用FIFO的實時程序必須是執行時間較短的程序,因為這種程序一旦獲得CPU就只有等到它執行完或因等待資源主動放棄CPU時其它程序才能獲得執行機會。

1.3 Linux程序的排程時機

時機如下:

1. 程序狀態轉換時: 如程序終止,睡眠等;
2. 可執行佇列中增加新的程序時;
3. 當前程序時間片耗盡

時;
4. 程序從系統呼叫返回到使用者態時(見第6章系統呼叫返回部分的程式碼註釋);
5. 核心處理完中斷後,程序返回到使用者態

1.4 Linux程序的佇列

存在於系統中的程序並不是孤立的,它們是有聯絡的。依據程序的建立關係,系統中的程序組成了一個龐大的網路(類似於家譜)。
通過這種親緣關係網,我們可以從一個程序遍歷到任何一個程序。

執行佇列:

Linux系統為處於就緒態的程序單獨組建了一個佇列,只有在這個佇列中的程序才有機會獲得CPU。

對執行佇列的操作:

由於linux的task_struct結構有自己的一套結構指標,所以Linux為程序佇列單獨設計了一套用於佇列操作的函式。
(1) 使用函式add_to_runqueue()向執行佇列插入一個新程序task_struct結構。
(2) 使用函式del_from_runqueue()從執行佇列摘除一個程序task_struct結構。
(3) 使用函式move_last_runqueue()當前*task_struct結構移到執行佇列隊尾。*
(4) 使用函式move_first_runqueue()當前*task_struct結構移到執行佇列隊頭。*

等待佇列:

與執行佇列相對應,Linux系統也為處於睡眠態的程序組建了一個佇列。

對佇列的基本操作

對佇列的操作無外乎初始化、新增、刪除等。為了實現方便,Linux核心中採用了一套抽象的、通用的、一般的、可以用到各種不同資料結構的佇列操作函式。上面提到的程序運行佇列和等待佇列實際上也是應用這套抽象函式來完成具體佇列操作的。由於這些函式(位置:include/linux/list.h)都很簡單,我們直接在這裡討論。
(1)Linux單獨設定一個list_head結構,並把它放在各種不同的結構(稱之為”宿主”)中作為將那個結構佇列連線指標
list_head結構定義如下:

struct list_head { struct list_head *next,*prev; }; /* prev指向佇列的前一個元
素,* next指向佇列的後一個元素*/

(2)Linux通過一個巨集宿主資料結構內部的每個list_head資料結構進行初始化

#define INIT_LIST_HEAD(ptr) do { \
    (ptr)->next=(ptr);(ptr)->prev=(ptr);\
}while(0)

引數ptr為需要初始化的list_head結構。初始化以後兩個指標都指向該list_head結構自身
(3)Linux使用list_add()將一個結構通過其“佇列頭”list鏈入一個佇列,該函式定義如下:

static __inline__ void list_add(struct list_head *new,struct list_head *head)
   {
    __list_add(new,head,head->next);  /*呼叫__list_add( )來完成操作*/
  }


static __inline__ void __list_add(struct list_head * new,  /* new指向欲鏈入隊
列的宿主數結構內部的list_head資料結構*/
 struct list_head * prev,     
    /* prev指向鏈入點,它可以是個獨立的、真正意義上
的佇列頭,也可以在另一個宿主資料結構內部*/
 struct list_head * next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    prev->next = new;
}

(5) Linux使用list_del( )完成脫鏈操作,該函式定義如下:

  static __inline__ void list_del(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);            /*呼叫__list_del( )來完成操
作*/
}
static __inline__ void __list_del(struct list_head * prev,
                  struct list_head * next)
{  /* 把被刪除元素的前後元素直接鉤鏈,以達到將當前元素從佇列摘除的目的*/
    next->prev = prev;
    prev->next = next;
}

(6) Linux通過巨集list_entry來獲取通過list_head結構找到的宿主結構指標:

  #define list_entry(ptr, type, member) \
    ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

1.5 Linux程序排程的全過程

1. 轉入schedule處理函式前的過程

根據1.3 Linux程序排程時機,Linux可以以五種方式轉入schedule處理函式
(1) 程序狀態轉換時
當程序要呼叫sleep()或exit()等函式使程序狀態發生改變時,這些函式會主動呼叫schedule()轉入程序排程。

(2) 通過時鐘中斷
Linux初始化時,設定系統定時器的週期為10毫秒。當時鍾中斷髮生時,時鐘中斷服務程式timer_interrupt立即呼叫時鍾處理函式do_timer( )。該函式會呼叫mark_bh(TIMER_BH),將bh_active標誌的TIMER_BH位置1。接著,Linux會在時鐘中斷服務程式中通過程式碼片段

if(bh_active& bh_mask)
  { intr_count=1;
     do_bottom_half();
     intr_count=0;
   }

來判斷此時是否有bottom half 服務要處理,若有則執行do_bottom_half()。該函式會呼叫時鐘響應函式timer_bh(),分別由update_times()、run_old_timers()和run_timer_list()檢查、執行定時服務。*update_times()又呼叫update_process_times()函式調整程序的時間片*,當時間片小於0時,need_resched(需要重排程)標誌會被置位。
當時鍾中斷處理完畢後,系統會返回到入口ret_from_intr,再跳轉到
ret_with_reschedule,判斷need_resched標誌是否置位,若是則轉入執行schedule()。

(3) 核心處理完中斷服務,返回到使用者態時
同(2)中時鐘中斷處理完畢後的返回處理一致,系統會返回到入口ret_from_intr,再跳轉到ret_with_reschedule,判斷need_resched標誌是否置位,若是則轉入執行schedule()。

(4) 程序從系統呼叫返回到使用者態時
系統呼叫的結束後,系統要通過入口ret_from_sys_call核心態轉回使用者態。在ret_from_sys_call中,系統會先判斷是否有half bottom服務請求,若沒有,則跳轉到ret_with_reschedule,判斷need_resched標誌是否置位,若是則轉入執行schedule();若有,則執行do_bottom_half(),然後在轉回ret_with_reschedule,判斷need
_resched標誌是否置位,若是則轉入執行schedule()。

(5) 執行佇列增加程序時
當需要向執行佇列增加一個程序是,系統會呼叫add_to_runqueue(),在此過程中,比較要加入的程序和正在執行的程序的counter值,若要加入程序counter值大於正在執行的程序的counter值加3將need_resched標誌置一。當處理時鐘中斷服務後返回時,轉入schedule()

2. 執行schedule( ),完成程序排程,切換程序

schedule中,先檢查是否是中斷服務程式呼叫了schedule(這是不允許的),如果是則退出schedule。若不是,則檢查是否有bottom half服務請求,若有則執行do_half_bottom。然後在判斷當前程序是否是採用時間片輪轉排程法的實時程序,若是則重新給它分配時間片並把它移到執行佇列尾部。接著根據當前程序的狀態對當前程序作相關處理。接下來,便是排程正文。通過函式goodness( )遍歷執行佇列中所有的程序,選擇權值最大的程序作為下一個執行的程序。若執行佇列中所有的程序的時間片都耗盡了,則要給系統中所有的程序重新分配時間片。最後通過巨集switch_to()切換堆疊,從而達到從當前程序切換到選中的程序的目的。排程結束。

1.6 schedule( )及其呼叫函式

1. schedule( )

schedule( ) 是Linux系統實現程序排程的函式,有些文章也把它稱為排程器
schedule按照不同的排程策略對實時程序非實時程序進行不同的排程處理。

它的簡單流程如下:

(1) 檢查是否是中斷服務程式呼叫了schedule( )(這是不允許的)。若是,則退出shcedule( )。
(2) 若有half bottom服務請求則處理half bottom。
(3) 儲存當前CPU排程程序的資料區;對執行佇列加鎖。
(4) 如果採用輪轉法進行排程,則要重新檢查一下當前程序的counter是否為零。若是,則重新分配時間片(counter),並將其掛到佇列尾部
(5) 檢測程序狀態:對need_resched 重新置0。
(6) 搜尋執行佇列,計算出每一個程序的goodness 並與當前的goodness 相比較,goodness 值最高的程序將獲取CPU。
(7) 若整個運動佇列中的程序的時間片耗盡,重新分配時間片。
對於非實時程序:

執行佇列中的程序由於時間片已耗盡,重新賦值後p->counter=p->pripority=20;不在執行佇列的程序通過p->counter>>1將原有時間片減半再加p->pripority,以此來提高其優先順序,使之能夠在轉入就緒態時有較高的競爭力。但這種競爭力的提高不是無限制的,通過p->counter>>1使p->counter永遠不會超過兩倍p->pripority 的值,這樣就避免了由於p->counter無限增長導致其優先順序高於實時程序的情況。

對於實時程序:

由於實時程序不採用counter值作為優先順序,所有對實時程序的counter的賦值操作無關緊要。

時間片分配完成後轉向(5)。
(8) 切換至新的程序。其中要呼叫switch_mm過載LDT。

2. goodness( )
goodness( ) 用來計算程序的權值
流程如下:

(1) 根據policy區分實時程序和普通程序
(2) 若為實時程序返回權值1000 + p->rt_priority
(3) 若非實時程序,將程序剩餘時間片加priority作為權值返回。如果程序是核心執行緒或使用者空間與當前程序相同, 因而不必切換使用者空間,則給予權值額外加一的”優惠”。