時間系統、程式的排程與切換

s1mba發表於2013-09-16

注:本分類下文章大多整理自《深入分析linux核心原始碼》一書,另有參考其他一些資料如《linux核心完全剖析》、《linux c 程式設計一站式學習》等,只是為了更好地理清系統程式設計和網路程式設計中的一些概念性問題,並沒有深入地閱讀分析原始碼,我也是草草翻過這本書,請有興趣的朋友自己參考相關資料。此書出版較早,分析的版本為2.4.16,故出現的一些概念可能跟最新版本核心不同。

此書已經開源,閱讀地址 http://www.kerneltravel.net


一、時間系統

大部分PC 機中有兩個時鐘源,他們分別叫做RTC 和OS(作業系統)時鐘。RTC(Real Time Clock,實時時鐘)也叫做CMOS 時鐘,它是PC 主機板上的一塊晶片(或者叫做時鐘電路),它靠電池供電,即使系統斷電,也可以維持日期和時間。由於它獨立於作業系統,所以也被稱為硬體時鐘,它為整個計算機提供一個計時標準,是最原始最底層的時鐘資料。

OS 時鐘產生於PC 主機板上的定時/計數晶片(8253/8254),由作業系統控制這個晶片的工作,OS 時鐘的基本單位就是該晶片的計數週期。在開機時作業系統取得RTC 中的時間資料來初始化OS時鐘,然後通過計數晶片的向下計數形成了OS 時鐘,所以OS 時鐘並不是本質意義上的時鐘,它更應該被稱為一個計數器。OS 時鐘只在開機時才有效,而且完全由作業系統控制,所以也被稱為軟時鐘或系統時鐘。

Linux 的OS 時鐘的物理產生原因是可程式設計定時/計數器產生的輸出脈衝,這個脈衝送入CPU,就可以引發一箇中斷請求訊號,我們就把它叫做時鐘中斷Linux 中用全域性變數jiffies 表示系統自啟動以來的時鐘滴答數目。每個時鐘滴答,時鐘中斷得到執行。時鐘中斷執行的頻率很高:100 次/秒Linux 設計者將一個“時鐘滴答”定義為10ms,時鐘中斷的主要工作是處理和時間有關的所有資訊、決定是否執行排程程式。和時間有關的所有資訊包括系統時間、程式的時間片、延時、使用CPU 的時間、各種定時器,程式更新後的時間片為程式排程提供依據,然後在時鐘中斷返回時決定是否要執行排程程式

每個時鐘中斷(timer interrupt)發生時,由3 個函式協同工作,共同完成程式的選擇和切換,它們是:schedule()、do_timer()及ret_form_sys_call()。我們先來解釋一下這3 個函式。

• schedule():程式排程函式,由它來完成程式的選擇(排程)。

• do_timer():暫且稱之為時鐘函式,該函式在時鐘中斷服務程式中被呼叫,是時鐘中斷服務程式的主要組成部分,該函式被呼叫的頻率就是時鐘中斷的頻率即每秒鐘100 次(簡稱100 赫茲或100Hz);由這個函式完成系統時間的更新、程式時間片的更新等工作,更新後的程式時間片counter 作為排程的主要依據。

• ret_from_sys_call():系統呼叫、異常及中斷返回函式。當一個系統呼叫或中斷完成時,該函式被呼叫,用於處理一些收尾工作,例如訊號處理、核心任務等。函檢測need_resched 標誌,如果此標誌為非0,那麼就呼叫排程程式schedule()進行程式的選擇。排程程式schedule()會根據具體的標準在執行佇列中選擇下一個應該執行的程式。當從排程程式返回時,如果發現又有排程標誌被設定,則又呼叫排程程式,直到排程標誌為0,這時,從排程程式返回時由RESTORE_ALL恢復被選定程式的環境,返回到被選定程式的使用者空間,使之得到執行。

個人腳註OS不是一直執行著的程式碼,而是一堆躺在記憶體裡等著被呼叫的程式碼,中斷處理在核心態,核心就是一個由 interrupt 驅動的程式。
可以是一個系統呼叫,x86 下很多OS的系統呼叫是靠 software interrupt 實現的,比如int 0x80,進入核心後就呼叫特定的核心函式執行。
也可以是一個使用者程式產生的異常。比如執行cpu 指令違法,segment fault 什麼的,作業系統一般會傳送訊號到程式,終止程式。
也可以是一個硬體產生的事件中斷。比如由IO裝置引起的可遮蔽中斷,作業系統會呼叫特定的裝置驅動程式進行服務。
一個使用者程式執行的時候,Linux 程式就在記憶體裡呆著,等著一箇中斷的到來。
一般的時分系統裡,都會有個timer interrupt 每隔一段時間到來,也就是上面說的時鐘中斷了。

二、linux 的排程程式 schedule()

程式的狀態(簡略版):

執行狀態(Running):程式佔用處理器資源;處於此狀態的程式的數目小於等於處理器的數目。在沒有其他程式可以執行時(如所有程式都在阻塞狀態),通常會自動執行系統的空閒程式。

就緒狀態(Ready):程式已獲得除處理器外的所需資源,等待分配處理器資源;只要分配了處理器程式就可執行。就緒程式可以按多個優先順序來劃分佇列。例如,當一個程式由於時間片用完而進入就緒狀態時,排人低優先順序佇列;當程式由I/O操作完成而進入就緒狀態時,排入高優先順序佇列。

阻塞狀態(Blocked):當程式由於等待I/O操作或程式同步等條件而暫停執行時,它處於阻塞狀態。


(一)、下面來了解一下主要的排程演算法及其基本原理。


1.時間片輪轉排程演算法

時間片(Time Slice)就是分配給程式執行的一段時間。
在分時系統中,為了保證人機互動的及時性,系統使每個程式依次地按時間片輪流的方式執行,此時即應採用時間片輪轉法進行排程。在通常的輪轉法中,系統將所有的可執行(即就緒)程式按先來先服務的原則,排成一個佇列,每次排程時把CPU 分配給隊首程式,並令其執行一個時間片。時間片的大小從幾ms 到幾百ms 不等。當執行的時間片用完時,系統發出訊號,通知排程程式,排程程式便據此訊號來停止該程式的執行,並將它送到執行佇列的末尾,等待下一次執行。然後,把處理機分配給就緒佇列中新的隊首程式,同時也讓它執行一個時間片。這樣就可以保證執行佇列中的所有程式,在一個給定的時間(人所能接受的等待時間)內,均能獲得一時間片的處理機執行時間。

2.優先權排程演算法

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

(1)非搶佔式優先權演算法(又稱不可剝奪排程,Nonpreemptive Scheduling
在這種方式下,系統一旦將處理機(CPU)分配給執行佇列中優先權最高的程式後,該程式便一直執行下去,直至完成;或因發生某事件使該程式放棄處理機時,系統方可將處理機分配給另一個優先權高的程式。這種排程演算法主要用於批處理系統中,也可用於某些對實時性要求不嚴的實時系統中。Linux 2.4 之前 kernel is nonpreemptive

(2)搶佔式優先權排程演算法(又稱可剝奪排程,Preemptive Scheduling
該演算法的本質就是系統中當前執行的程式永遠是可執行程式中優先權最高的那個。在這種方式下,系統同樣是把處理機分配給優先權(weight,goodness()函式求出)最高的程式,使之執行。但是隻要一出現了另一個優先權更高的程式時,排程程式就暫停原最高優先權程式的執行,而將處理機分配給新出現的優先權最高的程式,即剝奪當前程式的執行。因此,在採用這種排程演算法時,每當出現一新的可執行程式,就將它和當前執行程式進行優先權比較,如果高於當前程式,將觸發程式排程。這種方式的優先權排程演算法,能更好的滿足緊迫程式的要求,故而常用於要求比較嚴格的實時系統中,以及對效能要求較高的批處理和分時系統中。Linux 2.6開始也實現了這種排程演算法

3.多級反饋佇列排程

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

4.實時排程

最後我們來看一下實時系統中的排程。什麼叫實時系統,就是系統對外部事件有求必應、儘快響應。在實時系統中存在有若干個實時程式或任務,它們用來反應或控制某個(些)外部事件,往往帶有某種程度的緊迫性,因而對實時系統中的程式排程有某些特殊要求。在實時系統中,廣泛採用搶佔排程方式,特別是對於那些要求嚴格的實時系統。因為這種排程方式既具有較大的靈活性,又能獲得很小的排程延遲;但是這種排程方式也比較複雜。


(二)、程式排程的時機

Linux 排程時機主要有。

(1)程式狀態轉換的時刻:程式終止、程式睡眠;
(2)當前程式的時間片用完時(current->counter=0);
(3)裝置驅動程式;
(4)程式從中斷、異常及系統呼叫返回到使用者態時。

時機1,程式要呼叫sleep()或exit()等函式進行狀態轉換,這些函式會主動呼叫排程程式進行程式排程。

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

時機3,當裝置驅動程式執行長而重複的任務時,直接呼叫排程程式。在每次反覆迴圈中,驅動程式都檢查need_resched 的值,如果必要,則呼叫排程程式schedule()主動放棄CPU。

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

(三)、程式排程的依據

排程程式執行時,要在所有處於可執行狀態的程式之中選擇最值得執行的程式投入運行。選擇程式的依據是什麼呢?在每個程式的task_struct 結構中有如下5 項:
need_resched、nice、counter、policy 及rt_priority

(1)need_resched: 在排程時機到來時,檢測這個域的值,如果為1,則呼叫schedule() 。

(2)counter: 程式處於執行狀態時所剩餘的時鐘滴答數,每次時鐘中斷到來時,這個值就減1。當這個域的值變得越來越小,直至為0 時,就把need_resched 域置1,因此,也把這個域叫做程式的“動態優先順序”。

(3)nice: 程式的“靜態優先順序”,這個域決定counter 的初值。只有通過nice()、POSIX.1b sched_setparam() 或 5.4BSD/SVR4 setpriority()系統呼叫才能改變程式的靜態優先順序。

(4)rt_priority: 實時程式的優先順序

(5)policy: 從整體上區分實時程式和普通程式,因為實時程式和普通程式的排程是不同的,它們兩者之間,實時程式應該先於普通程式而執行, 可以通過系統呼叫sched_setscheduler()來改變排程的策略。對於同一型別的不同程式,採用不同的標準來選擇程式。對於普通程式,選擇程式的主要依據為counter 和nice 。對於實時程式,Linux採用了兩種排程策略,即FIFO(先來先服務排程)和RR(時間片輪轉排程)。因為實時程式具有一定程度的緊迫性,所以衡量一個實時程式是否應該執行,Linux 採用了一個比較固定的標準。實時程式的counter 只是用來表示該程式的剩餘滴答數,並不作為衡量它是否值得執行的標準,這和普通程式是有區別的。

(四)、程式可執行程度的衡量

函式goodness()就是用來衡量一個處於可執行狀態的程式值得執行的程度。該函式綜合使用了上面我們提到的5 項,給每個處於可執行狀態的程式賦予一個權值(weight),排程程式以這個權值作為選擇程式的唯一依據。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 

//其中,在sched.h 中對排程策略定義如下:
#define SCHED_OTHER 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_YIELD 0x10

static inline int goodness(struct task_struct *p, struct mm_struct *this_mm)
{
    int weight;
    /* 權值,作為衡量程式是否執行的唯一依據*/
    weight = -1;
    if (p->policy & SCHED_YIELD)
        goto out; /*如果該程式願意“禮讓(yield)”,則讓其權值為-1 */
    switch (p->policy)
    {
            /* 實時程式*/
        case SCHED_FIFO:
        case SCHED_RR:
            weight = 1000 + p->rt_priority;
            break;
            /* 普通程式 */
        case SCHED_OTHER:
        {
            weight = p->counter;
            if(!weight)
                goto out;
            /* 做細微的調整*/
            if (p->mm = this_mm || !p->mm)
                weight = weight + 1;
            weight += 20 - p->nice;
            break;
        }
        default:
            break;
    }
out:
    return weight; /*返回權值*/
}


這個函式比較很簡單。首先,根據policy 區分實時程式和普通程式。實時程式的權值取決於其實時優先順序,其至少是1000,與conter 和nice 無關。普通程式的權值需特別說明如下兩點。

(1)為什麼進行細微的調整?如果p->mm 為空,則意味著該程式無使用者空間(例如核心執行緒),則無需切換到使用者空間。如果
p->mm=this_mm,則說明該程式的使用者空間就是當前程式的使用者空間,該程式完全有可能再次得到執行。對於以上兩種情況,都給其權值加1,算是對它們小小的“獎勵”。

(2)程式的優先順序nice 是從早期UNIX 沿用下來的負向優先順序,其數值標誌“謙讓”的程度,其值越大,就表示其越“謙讓”,也就是優先順序越低,其取值範圍為-20~+19,因此,(20-p->nice)的取值範圍就是0~40。可以看出,普通程式的權值不僅考慮了其剩餘的時間片,還考慮了其優先順序,優先順序越高,其權值越大。

(五)、程式排程的實現

排程程式在核心中就是一個函式schedule().函式所做的事解釋如下:

 如果當前程式既沒有自己的地址空間,也沒有向別的程式借用地址空間,那肯定出錯。另外,如果schedule()在中斷服務程式內部執行,那也出錯。

 對當前程式做相關處理,為選擇下一個程式做好準備。當前程式就是正在執行著的進程,可是,當進入schedule()時,其狀態卻不一定是TASK_RUNNIG,例如,在exit()系統調用中,當前程式的狀態可能已被改為TASK_ZOMBE;又例如,在wait()系統呼叫中,當前進程的狀態可能被置為TASK_INTERRUPTIBLE。因此,如果當前程式處於這些狀態中的一種,就要把它從執行佇列中刪除。

• 從執行佇列中選擇最值得執行的程式,也就是權值最大的程式。

• 如果已經選擇的程式其權值為0,說明執行佇列中所有程式的時間片都用完了(佇列中肯定沒有實時程式,因為其最小權值為1000),因此,重新計算所有程式的時間片,其中巨集操作NICE_TO_TICKS 就是把優先順序nice 轉換為時鐘滴答。

• 程式地址空間的切換。如果新程式有自己的使用者空間,也就是說,如果next->mm 與next->active_mm 相同,那麼,switch_mm()函式就把該程式從核心空間切換到使用者空間,也就是載入next 的頁目錄。如果新程式無使用者空間(next->mm 為空),也就是說,如果它是一個核心執行緒,那它就要在核心空間執行,因此,需要借用前一個程式(prev)的地址空間,因為所有程式的核心空間都是共享的,因此這種借用是有效的。

• 用巨集switch_to()進行真正的程式切換。

三、程式切換

由於i386 CPU 要求軟體設定TR 及TSS,Linux 核心只不過“走過場”地設定TR 及TSS,以滿足CPU 的要求。但是,核心並不使用任務門,也不使用JMP 或CALL 指令實施任務切換。核心只是在初始化階段設定TR,使之指向一個TSS,從此以後再不改變TR 的內容了。也就是說,每個CPU(如果有多個CPU)在初始化以後的全部執行過程中永遠使用那個初始的TSS。同時,核心也不完全依靠TSS 儲存每個程式切換時的暫存器副本,而是將這些暫存器副本保存在各個程式自己的核心棧中(task_struct中的thread_struct 結構)。

這樣以來,TSS 中的絕大部分內容就失去了原來的意義。那麼,當進行任務切換時,怎樣自動更換堆疊?我們知道,新任務的核心棧指標(SS0 和ESP0)應當取自當前任務的TSS,可是,Linux 中並不是每個任務就有一個TSS,而是每個CPU 只有一個TSS。Intel 原來的意圖是讓TR 的內容隨著任務的切換而走馬燈似地換,而在Linux 核心中卻成了只更換TSS 中的SS0 和ESP0,而不更換TSS 本身,也就是根本不更換TR 的內容。這是因為,改變TSS 中SS0 和ESP0 所化的開銷比通過裝入TR 以更換一個TSS 要小得多。因此,在Linux核心中,TSS 並不是屬於某個程式的資源,而是全域性性的公共資源。在多處理機的情況下,儘管核心中確實有多個TSS,但是每個CPU 仍舊只有一個TSS。


參考:http://www.ibm.com/developerworks/cn/linux/l-cn-timers/

相關文章