注:本分類下文章大多整理自《深入分析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/