根據Linux2.6.26源碼分析進程模型
1.關於進程
1.1進程的概念
進程是正在運行的程序實體,並且包括這個運行的程序中占據的所有系統資源,比如說CPU(寄存器),IO,內存,網絡資源等。很多人在回答進程的概念的時候,往往只會說它是一個運行的實體,而會忽略掉進程所占據的資源。比如說,同樣一個程序,同一時刻被兩次運行了,那麽他們就是兩個獨立的進程。linux下查看系統進程的命令是ps。
1.2進程在內核中的描述
在linux中,有一個結構體task_struct,被專門用來描述進程的信息。在2.4版本及其以前,task_struct與內核堆棧是放在同一個4K的頁面中,如下: union task_union{ struct task_struct task; unsigned long stack [INIT_TASK_SIZE/sizeof(log)]; }; 這樣有一個好處,就是在內核中運行的時候,任何時候都可以通過指針得到當前運行進程的task_struct。盡管這給進程管理帶來了好處,不過也有很大的隱患,如果task_struct越來越大,或者內核堆棧壓的太多(函數調用層次太多),就不行了。所以從linux2.6開始,就有所改變了。首先是將這個4K的頁面增加到8K:#defineTHREAD_SIZE (8192),然後又這樣一句代碼:#define alloc_thread_info(tsk) kmalloc(THREAD_SIZE,GFP_KERNEL)。然後還有一個很大的改變,那就是把task_struct從這部分空間中移走,然後抽象出一個thread_info的結構(裏面是經常被訪問的變量): struct thread_info{ struct task_struct *task; //main task structure ...... //還有幾個其他的選項 } 這樣,thread_info就代替了原來task_struct的位置,和內核堆棧共享8K的空間。從上面的描述中也可以看到thread_info裏面有一個指針,指向了task_struct。通過這兩個方式,就可以解決上面提到的隱患問題。
1.3進程的狀態轉換
在linux中,有兩個變量用來表示內核的狀態,volatile long state 用來表示進程的可運行性,long exit_state用來表示進程退出時的狀態。此外,關於進程的詳細狀態,參見下面的宏定義: #define TASK_RUNNING 0 //正在運行的進程; #define TASK_INTERRUPTIBLE 1 //等待資源的進程,當等待的資源有效時被喚醒,也可由其他進程或內核用信號中斷、喚醒後進入就緒狀態 #define TASK_UNINTERRUPTIBLE 2 //同上,但是不能被其他進程或者內核中斷 #define TASK_STOPPED 4 //進程被暫停,一般是收到了SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU信號,通過其他進程的信號才能被喚醒 #define TASK_TRACED 8 //進程被跟蹤,一般在調試的時候會用的 //in tsk->state again #define EXIT_ZOMBIE 16 //僵死狀態,雖然釋放了內存文件等資源,但內核中仍然保存task_struct,等待父進程調用wait4()或者waitpid()函數來回收 #define EXIT_DEAD 32 //進程消亡前最後一個狀態,父進程已調用了wait4()或waitpid() //in tsk->state again #define TASK_NONINTERACTIVE 64 //不是交互式進程,調度的時候會考慮到 至於進程間的轉換,這裏就不描述了。
1.4進程標誌位
為了對每個進程進行更細粒度的控制,在task_struct中還有一些變量flags: unsigned long flags ;//per process flags,defined blow 這個flags可以是下面的一些標誌的組合:
#define PF_ALIGNWARN 0x00000001 /* Print alignment warningmsgs*/
#define PF_STARTING 0x00000002 /* being created */
#define PF_EXITING 0x00000004 /* getting shut down */
#define PF_DEAD 0x00000008 /* Dead */
#define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */
#define PF_SUPERPRIV 0x00000100 /* used super-user privileges */
#define PF_DUMPCORE 0x00000200 /* dumped core */
#define PF_SIGNALED 0x00000400 /* killed by a signal */
#define PF_MEMALLOC 0x00000800 /* Allocating memory */
#define PF_FLUSHER 0x00001000 /* responsible for disk writeback */
#define PF_USED_MATH 0x00002000 /* if unset the fpu must be initialized before use */
#define PF_FREEZE 0x00004000 /* this task is being frozen for suspend now */
#define PF_NOFREEZE 0x00008000 /* this thread should not be frozen */
#define PF_FROZEN 0x00010000 /* frozen for system suspend */
#define PF_FSTRANS 0x00020000 /* inside a filesystem transaction */
#define PF_KSWAPD 0x00040000 /* I am kswapd */
#define PF_SWAPOFF 0x00080000 /* I am in swapoff */
#define PF_LESS_THROTTLE 0x00100000 /* Throttle me less: I clean memory */
#define PF_SYNCWRITE 0x00200000 /* I am doing a sync write */
#define PF_BORROWED_MM 0x00400000 /* I am a kthread doing use_mm */
#define PF_RANDOMIZE 0x00800000 /* randomize virtual address space */
1.5進程的調度策略
在task_struct中與進程調度相關的變量是:unsigned long policy,有三種調度策略: #define SCHED_NORMAL 0 #define SCHED_FIFO 1 #define SCHED_RR 2 不過這裏有點問題,我下的2.6源碼中就這三種調度策略,但是我看到有的書上介紹2.6內核的時候說到此外還有一種調度策略:SCHED_BATCH,說該調度策略一般是用於後臺處理進程,沒有交互性。FIFO與RR屬於實時調度,所以優先級高於另外兩種。 進程的調度優先級: int prio,static_prio; unsigned long rt_priority; prio是進程的動態優先級,隨著進程的運行而改變,調度器有時候還會根據進程的交互性、平均睡眠時間而進行獎懲。默認情況下,實時進程(FIFO,RR)的動態優先級為0到99,另外兩種是100到139,0最高,139最低。static_prio是普通進程的靜態優先級,默認是120,rt_priority是實時進程的靜態優先級。
2.進程的特征
動態性:進程的實質是程序在多道程序系統中的一次執行過程,進程是動態產生,動態消亡的。
並發性:任何進程都可以同其他進程一起並發執行
獨立性:進程是一個能獨立運行的基本單位,同時也是系統分配資源和調度的獨立單位;
異步性:由於進程間的相互制約,使進程具有執行的間斷性,即進程按各自獨立的、不可預知的速度向前推 進
結構特征:進程由程序、數據和進程控制塊三部分組成;
多個不同的進程可以包含相同的程序:一個程序在不同的數據集裏就構成不同的進程,能得到不同的結果; 但是執行過程中,程序不能發生改變。
3.進程的三種基本狀態
a,就緒狀態:進程已獲得除處理器外的所需資源,等待分配處理器資源;只要分配了處理器進程就可執行。就緒進程可以按多個優先級來劃分隊列。例如,當一個進程由於時間片用完而進入就緒狀態 時,排入低優先級隊列;當進程由I/O操作完成而進入就緒狀態時,排入高優先級隊列。
b,運行狀態:進程占用處理器資源;處於此狀態的進程的數目小於等於處理器的數目。在沒有其他進程可以 執行時(如所有進程都在阻塞狀態),通常會自動執行系統的空閑進程。
c,阻塞狀態:由於進程等待某種條件(如I/O操作或進程同步),在條件滿足之前無法繼續執行。該事件發生前即使把處理機分配給該進程,也無法運行。
進程狀態轉換圖:
4.進程控制
創建進程
引起創建進程的事件:
1) 用戶登錄
2) 作業調度
3) 提供服務
4) 應用請求
進程的創建過程
一旦操作系統發現了要求創建新進程的事件後,便調用進程創建原語Creat()按下述步驟創建一個新 進程。
1) 申請空白PCB。為新進程申請獲得唯一的數字標識符,並從PCB集合中索取一個空白PCB。
2) 為新進程分配資源。
3) 初始化進程控制塊。PCB的初始化包括:
①初始化標識信息,將系統分配的標識符和父進程標識符,填入新的PCB中。
②初始化處理機狀態信息,使程序計數器指向程序的入口地址,使棧指針指向棧頂。
③初始化處理機控制信息,將進程的狀態設置為就緒狀態或靜止就緒狀態,對於優先級,通常是將它設置為最低優先級,除非用戶以顯式的方式提出高優先級要求。
4) 將新進程插入就緒隊列,如果進程就緒隊列能夠接納新進程,便將新進程插入到就緒隊列中。
進程終止
引起進程終止的事件
1)正常結束
2)異常結束
3)外界幹預
進程的終止過程
如果系統發生了上述要求終止進程的某事件後,OS便調用進程終止原語,按下述過程去終止指定的進程。
1)根據被終止進程的標識符,從PCB集合中檢索出該進程的PCB,從中讀出該進程狀態。
2)若被終止進程正處於執行狀態,應立即終止該進程的執行,並置調度標誌為真。用於指示該進程被終止後應重新進行調度。
3)若該進程還有子孫進程,還應將其所有子孫進程予以終止,以防他們成為不可控的進程。
4)將被終止的進程所擁有的全部資源,或者歸還給其父進程,或者歸還給系統。
5)將被終止進程(它的PCB)從所在隊列(或鏈表)中移出,等待其它程序來搜集信息。
阻塞喚醒
1.引起進程阻塞和喚醒的事件
1)請求系統服務
2)啟動某種操作
3)新數據尚未到達
4)無新工作可做
2.進程阻塞過程
正在執行的進程,當發現上述某事件後,由於無法繼續執行,於是進程便通過調用阻塞原語block把自己阻塞。可見,進程的阻塞是進程自身的一種主動行為。進入block過程後,由於此時該進程還處於執 行狀態,所以應先立即停止執行,把進程控制塊中的現行狀態由執行改為阻塞,並將PCB插入阻塞隊列。如果系統中設置了因不同事件而阻塞的多個阻塞隊列,則應將本進程插入到具有相同事件的阻塞(等待)隊列。最後,轉調度程序進行重新調度,將處理機分配給另一就緒進程,並進行切換,亦即,保留被阻塞進程的處理機狀態(在PCB中),再按新進程的PCB中的處理機狀態設置CPU環境。
3.進程喚醒過程
當被阻塞的進程所期待的事件出現時,如I/O完成或者其所期待的數據已經到達,則由有關進程(比如, 用完並釋放了該I/O設備的進程)調用喚醒原語wakeup(),將等待該事件的進程喚醒。喚醒原語執行 的過程是:首先把被阻塞的進程從等待該事件的阻塞隊列中移出,將其PCB中的現行狀態由阻塞改為就 緒,然後再將該PCB插入到就緒隊列中。
5.進程調度的算法及思想
5.1先來先服務調度算法
先來先服務(FCFS)調度算法是一種最簡單的調度算法,該算法既可用於作業調度,也可用於進程調度。當在作業調度中采用該算法時,
每次調度都是從後備作業隊列中選擇一個或多個最先進入該隊列的作業,將它們調入內存,為它們分配資源、創建進程,然後放入就緒
隊列。在進程調度中采用FCFS算法時,則每次調度是從就緒隊列中選擇一個最先進入該隊列的進程,為之分配處理機,使之投入運行。
該進程一直運行到完成或發生某事件而阻塞後才放棄處理機。
來看一個例子,假設有三個進程和它們各自執行時間(以毫秒為單位)如下表: 那麽如果三個進程按照P1, P2, P3的順序啟動的話,按照先到先服務的調度算法,執行過程如下: 平均等待時間就是(0 + 24 + 27) / 3 = 17毫秒。FCFS算法是非搶占式的,一旦內核將CPU分配給一個進程就不會被釋放 了,除非進程結束或者請求I/O阻塞。這也是我們之前學習的多任務系統的特點。
5.2基於優先級調度 (Priority Scheduling)
在優先級調度算法中,每個進程都關聯一個優先級,內核將CPU分配給最高優先級的進程。具有相同優先級的進程,按照 先來先服務的原則進行調度。假設進程的執行時間和優先級如下,並且這些進程同時存在於內存中時,內核基於優先級的 調度過程如下: 采取基於優先級調度算法要考慮進程餓死的問題,因為高優先級的進程總是會被優先調度,具有低優先級的進程可能永遠 都不會被內核調度執行。Aging是對於這個問題的一個解決方案,所謂Aging就是指逐漸提高系統中長時間等待的進程的 優先級。比如如果優先級的範圍從127到0(127表示最低優先級),那麽我們可以每15分鐘將等待進程的優先級加1。最終 經過一段時間,即便是擁有最低優先級127的進程也會變成系統中最高優先級的進程,從而被執行。 優先級調度可以搶占式或者非搶占式的。當一個進程在Ready隊列中時,內核將它的優先級與正在CPU上執行的進程的優先級 進行比較。當發現這個新進程的優先級比正在執行的進程高時:對於搶占式內核,新進程會搶占CPU,之前正在執行的進程 轉入Ready隊列;對於非搶占式內核,新進程只會被放置在Ready隊列的頭部,不會搶占正在執行的進程。5.3短進程優先(SCBF--Shortest CPU Burst First)
最短CPU運行期優先調度算法(SCBF--Shortest CPU Burst First) 該算法從就緒隊列中選出下一個“CPU執行期最短”的進程,為之分配處理機。 最短作業優先調度是優先級調度的特例。在優先級調度中我們根據進程的優先級來進行調度,在最短作業優先調度中我們 根據作業的執行時間長短來調度。通過下面的例子來看看SJF是怎樣調度的。 進程1首先執行了1毫秒,當執行時間更短的進程2進入Ready隊列時發生搶占。進程3在進程2執行1毫秒後到來,但是進程3的 執行時間比進程2長。同理進程4的到來也沒法搶占進程2,所以進程2可以一直執行到結束。之後是執行時間第二短的進程4 執行,最後是進程1(要看剩余執行時間,還剩7毫秒)和進程3。 SJF調度是最優的調度,但難點在於如何預測進程的執行時間(Burst Time)。對於批處理系統中的長期調度來說,可以將用戶 提交進程時輸入的執行時間上限作為依據。但對於短期調度來說,沒有辦法能夠提前得知下一個要被分配CPU的進程的執行 時間長短。我們只能通過歷史數據來進行預測,公式如下: α可以取0.5,公式前半部分表示最近一次Burst Time,而後半部分表示過去歷史平均的Burst Time。 該算法雖可獲得較好的調度性能,但難以準確地知道下一個CPU執行期,而只能根據每一個進程的執行歷史來預測。5.4輪轉法 (Round-Robin Scheduling) (RR)
前幾種算法主要用於批處理系統中,不能作為分時系統中的主調度算法,在分時系統中,都采用時間片輪轉法。 簡單輪轉法:系統將所有就緒進程按FIFO規則排隊,按一定的時間間隔把處理機分配給隊列中的進程。這樣,就緒 隊列中所有進程均可獲得一個時間片的處理機而運行。多級隊列方法:將系統中所有進程分成若幹類,每類為一級。 RR調度算法轉為分時系統設計,它與FCFS很像,但是加入了搶占。具體調度過程是:內核從Ready隊列中選取第一個進程, 將CPU資源分配給它,並且設置一個定時器在一個時間片後中斷該進程,調度Ready隊列中的下一進程。很明顯,RR調度 算法是搶占式的,並且在該算法的調度下,沒有一個進程能夠連續占用CPU超過一個時間片,從而達到了分時的目的。 來看下面的例子,假設一個時間片的長度為4毫秒:5.5高響應比優先調度算法
(1) 如果作業的等待時間相同,則要求服務的時間愈短,其優先權愈高,因而該算法有利於短作業. (2) 當要求服務的時間相同時,作業的優先權決定於其等待時間,等待時間愈長,其優先權愈高,因而它實現的是先來先服務. (3) 對於長作業,作業的優先級可以隨等待時間的增加而提高,當其等待時間足夠長時,其優先級便可升到很高, 從而也可獲得處理機. 該算法照顧了短作業,且不會使長作業長期得不到服務
6,linux內核分析之調度算法
inux上主要有兩大類調度算法,CFS(完全公平調度算法)和實時調度算法。宏SCHED_NOMAL主要用於CFS調度,而SCHED_FIFO和SCHED_RR主要用於實時調度。如下面的宏定義:
/* * Scheduling policies */ /*支援Real-Time Task的排程,包括有SCHED_FIFO與SCHED_RR. */ /*(也稱為SCHED_OTHER): 主要用以排程 一般目的的Task.*/ #define SCHED_NORMAL 0 #define SCHED_FIFO 1 /*task預設的 Time Slice長度為100 msecs*/ #define SCHED_RR 2 /*主要用以讓Task可以延長執行的時間 (Time Slice),減少被中斷發生Task Context-Switch 的次數.藉此可以提高 Cache的利用率 (每次Context-Switch都會導致Cache-Flush). 比 較適合用在固定週期執行的Batch Jobs任 務主機上,而不適合用在需要使用者互 動的產品 (會由於Task切換的延遲,而 感覺到系統效能不佳或是反應太慢).*/ #define SCHED_BATCH 3 /* SCHED_ISO: reserved but not implemented yet */ /*為系統中的Idle Task排程.*/ #define SCHED_IDLE 5
linux調度算法實現的高層數據結構主要有運行實體、調度類、運行隊列,下面我們主要看看這幾個數據結構的字段和意義。
運行實體,rq結構體每個cpu有一個,主要存儲一些基本的用於調度的信息,包括實時調度的和CFS調度的調度類,sched_class為對模塊編程的上層支持,對於每個linux新添加進來的調度算法都需要有自己的調度類實例。
/*每個處理器都會配置一個rq*/ struct rq { /* runqueue lock: */ spinlock_t lock; /* * nr_running and cpu_load should be in the same cacheline because * remote CPUs use both these fields when doing load calculation. */ /*用以記錄目前處理器rq中執行task的數量*/ unsigned long nr_running; #define CPU_LOAD_IDX_MAX 5 /*用以表示處理器的負載,在每個處理器的rq中 都會有對應到該處理器的cpu_load參數配置,在每次 處理器觸發scheduler tick時,都會呼叫函數 update_cpu_load_active,進行cpu_load的更新。在系統初始化的時候 會呼叫函數sched_init把rq的cpu_load array初始化為0. 了解他的更新方式最好的方式是通過函數update_cpu_load,公式如下淡? cpu_load[0]會直接等待rq中load.weight的值。 cpu_load[1]=(cpu_load[1]*(2-1)+cpu_load[0])/2 cpu_load[2]=(cpu_load[2]*(4-1)+cpu_load[0])/4 cpu_load[3]=(cpu_load[3]*(8-1)+cpu_load[0])/8 cpu_load[4]=(cpu_load[4]*(16-1)+cpu_load[0]/16 呼叫函數this_cpu_load時,所返回的cpu load值是cpu_load[0] 而在進行cpu blance或migration時,就會呼叫函數 source_load target_load取得對該處理器cpu_load index值, 來進行計算*/ unsigned long cpu_load[CPU_LOAD_IDX_MAX]; #ifdef CONFIG_NO_HZ unsigned long last_tick_seen; unsigned char in_nohz_recently; #endif /* capture load from *all* tasks on this cpu: */ /*load->weight值,會是目前所執行的schedule entity的 load->weight的總和,也就是說rq的load->weight越高, 也表示所負責的排程單元load->weight總和越高 表示處理器所負荷的執行單元也越重*/ struct load_weight load; /*在每次scheduler tick中呼叫update_cpu_load時, 這個值就增加一,可以用來反饋目前cpu load更新的次數*/ unsigned long nr_load_updates; /*用來累加處理器進行context switch的次數,會在 函數schedule呼叫時進行累加,並可以通過函數 nr_context_switches統計目前所有處理器總共的context switch 次數,或是可以透過查看檔案/proc/stat中的ctxt位得知目前 整個系統觸發context switch的次數*/ u64 nr_switches; u64 nr_migrations_in; /*為cfs fair scheduling class 的rq*/ struct cfs_rq cfs; /*為real-time scheduling class 的rq*/ struct rt_rq rt; /*用以支援可以group cfs tasks的機制*/ #ifdef CONFIG_FAIR_GROUP_SCHED /* list of leaf cfs_rq on this cpu: */ /*在有設置fair group scheduling 的環境下, 會基於原本cfs rq中包含有若幹task的group 所成的排程集合,也就是說當有一個group a 就會有自己的cfs rq用來排程自己所屬的tasks, 而屬於這group a的tasks所使用到的處理器時間 就會以這group a總共所分的的時間為上限。 基於cgroup的fair group scheduling 架構,可以創造出 有階層性的task組織,根據不同task的功能群組化 在配置給該群主對應的處理器資源,讓屬於 該群主下的task可以透過rq機制排程。使用屬於 該群主下的資源。 這個變數主要是管理CFS RQ list,操作上可以透過函數 list_add_leaf_cfs_rq把一個group cfs rq加入到list中,或透過 函數list_del_leaf_cfs_rq把一個group cfs rq移除,並可以 透過for_each_leaf_cfs_rq把一個rq上得所有leaf cfs_rq走一遍 */ struct list_head leaf_cfs_rq_list; #endif /*用以支援可以group real-time tasks的機制*/ #ifdef CONFIG_RT_GROUP_SCHED /*類似leaf_cfs_rq_list所扮演的角色,只是這裏 是針對屬於real-time的task,在實際操作上可以 透過函數list_add_leaf_rt_rq,list_del_leaf_rt_rq或 巨集for_each_leaf_rt_rq*/ struct list_head leaf_rt_rq_list; #endif /* * This is part of a global counter where only the total sum * over all CPUs matters. A task can increase this counter on * one CPU and if it got migrated afterwards it may decrease * it on another CPU. Always updated under the runqueue lock: */ /*一般來說,linux kernel 的task狀態可以為TASK_RUNNING TASK_INTERRUPTIBLE(sleep), TASK_UNINTERRUPTIBLE(Deactivate Task,此時Task會從rq中 移除)或TASK_STOPPED. 透過這個變數會統計目前rq中有多少task屬於 TASK_UNINTERRUPTIBLE的狀態。當呼叫函數 active_task時,會把nr_uninterruptible值減一,並透過 該函數 enqueue_task把對應的task依據所在的scheduling class 放在 對應的rq中,並把目前rq中nr_running值加一*/ unsigned long nr_uninterruptible; /*curr:指向目前處理器正在執行的task; idle:指向屬於idle-task scheduling class 的idle task; stop:指向目前最高等級屬於stop-task scheduling class 的task;*/ struct task_struct *curr, *idle; /*基於處理器的jiffies值,用以記錄下次進行處理器 balancing 的時間點*/ unsigned long next_balance; /*用以存儲context-switch發生時,前一個task的memory management 結構並可用在函數finish_task_switch中,透過函數mmdrop釋放前一個 task的記憶體資源*/ struct mm_struct *prev_mm; /*用以記錄目前rq的clock值,基本上該值會等於透過sched_clock_cpu (cpu_of(rq))的回傳值,並會在每次呼叫scheduler_tick時透過 函數update_rq_clock更新目前rq clock值。 在實作部分,函數sched_clock_cpu會透過sched_clock_local或 ched_clock_remote取得對應的sched_clock_data,而處理的sched_clock_data 值,會透過函數sched_clock_tick在每次呼叫scheduler_tick時進行更新; */ u64 clock; /*用以記錄目前rq中有多少task處於等待i/o的sleep狀態 在實際的使用上,例如當driver接受來自task的調用,但處於等待i/o 回復的階段時,為了充分利用處理器的執行資源,這時 就可以在driver中呼叫函數io_schedule,此時 就會把目前rq中的nr_iowait加一,並設定目前task的io_wait為1 然後觸發scheduling 讓其他task有機會可以得到處理器執行時間*/ atomic_t nr_iowait; #ifdef CONFIG_SMP /*root domain是基於多核心架構下的機制, 會由rq結構記住目前采用的root domain,其中包括了 目前的cpu mask(包括span,online rt overload), reference count 跟cpupri 當root domain有被rq參考到時,refcount 就加一,反之就減一。而cpu mask span表示rq可掛上的cpu mask,noline為rq目前已經排程的 cpu mask cpu上執行real-time task.可以參考函數pull_rt_task,當一個rq中屬於 real-time的task已經執行完畢,就會透過函數pull_rt_task從該 rq中屬於rto_mask cpu mask 可以執行的處理器上,找出是否有一個處理器 有大於一個以上的real-time task,若有就會轉到目前這個執行完成 real-time task 的處理器上 而cpupri不同於Task本身有區分140個(0-139) Task Priority (0-99為RT Priority 而 100-139為Nice值 -20-19). CPU Priority本身有102個Priority (包括,-1 為Invalid, 0為Idle,1為Normal,2-101對應到Real-Time Priority 0-99). 參考函式convert_prio, Task Priority如果是 140就會對應到 CPU Idle,如果是大於等於100就會對應到CPU Normal, 若是Task Priority介於0-99之間,就會對應到CPU Real-Time Priority 101-2之間.) 在實際的操作上,例如可以透過函式cpupri_find 帶入一個要插入的Real-Time Task,此時就會依據cpupri中 pri_to_cpu選擇一個目前執行Real-Time Task且該Task 的優先級比目前要插入的Task更低的處理器, 並透過CPU Mask(lowest_mask)返回目前可以選擇的處理器Mask. 實作的部份可以參考檔案kernel/sched_cpupri.c. 在初始化的過程中,會透過函式sched_init呼叫函式init_defrootdomain, 對Root Domain與 CPU Priority機制進行初始化. */ struct root_domain *rd; /*Schedule Domain是基於多核心架構下的機制. 每個處理器都會有一個基礎的Scheduling Domain, Scheduling Domain可以有階層性的架構,透過parent 可以找到上一層的Domain,或是透過child找到 下一層的 Domain (NULL表示結尾.).並可透過span 欄位,表示這個Domain所能涵蓋的處理器範圍. 通常Base Domain會涵蓋系統中所有處理器的個數, 而Child Domain所能涵蓋的處理器個數不超過它的 Parent Domain. 而當在進行Scheduling Domain 中的Task Balance 時,就會以該Domain所能涵蓋的處理器為最大範圍. 同時,每個Schedule Domain都會包括一個或一個以上的 CPU Groups (結構為struct sched_group),並透過next變數把 CPU Groups串連在一起(成為一個單向的Circular linked list), 每個CPU Group都會有變數cpumask來定義這個CPU Group 所涵蓋的處理器範圍.並且CPU Group所包括的處理器 範圍,必需涵蓋在所屬的Schedule Domain處理器範圍中. 當進行Scheduling Domain的Balancing時,會以其下的CPU Groups 為單位,根據cpu_power (會是該Group所涵蓋的處理器 Tasks Loading的總和)來比較不同的CPU Groups的負荷, 以進行Tasks的移動,達到Balancing的目的. 在有支援SMP的架構下,會在函式sched_init中,呼叫open_softirq, 註冊 SCHED_SOFTIRQ Software IRQ與其對應的 Callback函式 run_rebalance_domains. 並會在每次呼叫函式scheduler_tick時, 透過函式trigger_load_balance確認是否目前的jiffies值已經 大於RunQueue下一次要觸發Load Balance的next_balance時間值, 並透過函式raise_softirq觸發SCHED_SOFTIRQ Software IRQ. 在Software IRQ觸發後,就會呼叫函式run_rebalance_domains, 並在函式rebalance_domains中,進行後續處理器上的 Scheduling Domain Load Balance動作. 有關Scheduling Domain進一步的內容,也可以參考 Linux Kernel文件 Documentation/scheduler/sched-domains.txt. */ struct sched_domain *sd; /*這值會等於函式idle_cpu的返回值,如果為1表示 目前CPU RunQueue中執行的為Idle Task. 反之為0, 則表示處理器執行的不是Idle Task (也就是說 處理器正在忙碌中.).*/ unsigned char idle_at_tick; /* For active balancing */ /*若這值不為0,表示會有在Schedule排程動作 結束前,要呼叫的收尾函式. (實作為inline函式 post_schedule in kernel/sched.c),目前只有Real-Time Scheduling Class有支援這個機制(會呼叫函式has_pushable_tasks in kernel/sched_rt.c).*/ int post_schedule; /*當RunQueue中此值為1,表示這個RunQueue正在進行 Fair Scheduling的Load Balance,此時會呼叫stop_one_cpu_nowait 暫停該RunQueue所屬處理器的排程,並透過函式 active_load_balance_cpu_stop,把Tasks從最忙碌的處理器, 移到Idle的處理器上執行.*/ int active_balance; /*用以儲存目前進入Idle且負責進行 Load Balance 流程的處理器ID. 呼叫的流程為,在呼叫函式schedule時, 若該處理器RunQueue的nr_running為0 (也就是目前沒有 正在執行的Task),就會呼叫idle_balance,並觸發後續Load Balance流程.*/ int push_cpu; /* cpu of this runqueue: */ /*用以儲存目前運作這個RunQueue的處理器ID*/ int cpu; /*為1表示目前此RunQueue有在對應的處理器掛上 並執行.*/ int online; /*如果RunQueue中目前有Task正在執行,這個值會 等於目前該RunQueue的Load Weight除以目前RunQueue 中Task數目的均值. (rq->avg_load_per_task = rq->load.weight / nr_running;).*/ unsigned long avg_load_per_task; struct task_struct *migration_thread; struct list_head migration_queue; /*這個值會由Real-Time Scheduling Class呼叫函式 update_curr_rt,用以統計目前Real-Time Task執行時間的 均值,在這函式中會以目前RunQueue的clock_task 減去目前Task執行的起始時間,取得執行時間的 Delta值. (delta_exec = rq->clock_task – curr->se.exec_start; ). 在透過函式sched_rt_avg_update把這Delta值跟原本 RunQueue中的rt_avg值取平均值. 以運作的週期來看, 這個值可反應目前系統中Real-Time Task平均被 分配到的執行時間值.*/ u64 rt_avg; /*這個值主要在函式sched_avg_update更新,以筆者手中 的Linux Kernel 2.6.38.6的實作來說,當RunQueue Clock 減去age_stamp大於 0.5秒 (=sched_avg_period),就會把這值 累加0.5秒 (單位都是nanoseconds). 從函式scale_rt_power 的實作來說,age_stamp值離RunQueue Clock越遠,表示total 值越大,available值也越大,而函式scale_rt_power返回的 div_u64計算結果也越大,最終 RunQueue的cpu_power 與Scheduling Domain中的Scheduling Group的cpu_power 值也就越大. (可參考函式update_cpu_power的實作).*/ u64 age_stamp; /*這值會在觸發Scheduling時,若判斷目前處理器 RunQueue沒有正在運作的Task,就會透過函式 idle_balance更新這值為為目前RunQueue的clock值. 可用以表示這個處理器是何時進入到Idle的 狀態*/ u64 idle_stamp; /*會在有Task運作且idle_stamp不為0 (表示前一個 狀態是在Idle)時以目前RunQueue的clock減去 idle_stmp所計算出的Delta值為依據,更新這個值 . 可反應目前處理器進入Idle狀態的時間長短*/ u64 avg_idle; #endif /* calc_load related fields */ /*用以記錄下一次計算CPU Load的時間,初始值 為目前的jiffies加上五秒與1次的Scheduling Tick的 間隔 (=jiffies + LOAD_FREQ,且LOAD_FREQ=(5*HZ+1))*/ unsigned long calc_load_update; /*會等於RunQueue中nr_running與nr_uninterruptible的 總和.(可參考函式calc_load_fold_active).*/ long calc_load_active; #ifdef CONFIG_SCHED_HRTICK #ifdef CONFIG_SMP /*在函式init_rq_hrtick初始化RunQueue High-Resolution Tick時,此值預設為0. 在函式hrtick_start中,會判斷目前觸發的RunQueue 跟目前處理器所使用的RunQueue是否一致, 若是,就直接呼叫函式hrtimer_restart,反之就會 依據RunQueue中hrtick_csd_pending的值,如果 hrtick_csd_pending為0,就會透過函式 __smp_call_function_single讓RunQueue所在的另一個 處理器執行rq->hrtick_csd.func 所指到的函式 __hrtick_start. 並等待該處理器執行完畢後, 才重新把hrtick_csd_pending設定為1. 也就是說, RunQueue的hrtick_csd_pending是用來作為 SMP架構下,由處理器A觸發處理器B執行 _hrtick_start函式的一個保護機制.而有關在 SMP下如何由一個處理器觸發另一個處理器 執行函式的機制,可以參考kernel/smp.c中 相關smp_call_function_xxxxxxx的實作.s*/ int hrtick_csd_pending; /*用以儲存hrtick機制中,要跨處理器執行的 函式結構.*/ struct call_single_data hrtick_csd; #endif /*為High-Resolution Tick的結構,會透過函式 hrtimer_init初始化.*/ struct hrtimer hrtick_timer; #endif #ifdef CONFIG_SCHEDSTATS /* latency stats */ /*為Scheduling Info.的統計結構,可以參考 include/linux/sched.h中的宣告. 例如在每次觸發 Schedule時,呼叫函式schedule_debug對上一個Task 的lock_depth進行確認(Fork一個新的Process 時, 會把此值預設為-1就是No-Lock,當呼叫 Kernel Lock時, 就會把Current Task的lock_depth加一.), 若lock_depth>=0,就會累加Scheduling Info.的bkl_count值, 用以代表Task Blocking的次數.*/ struct sched_info rq_sched_info; /*可用以表示RunQueue中的Task所得到CPU執行 時間的累加值. 在發生Task Switch時,會透過sched_info_switch呼叫 sched_info_arrive並以目前RunQueue Clock值更新 Task 的sched_info.last_arrival時間,而在Task所分配時間 結束後,會在函式sched_info_depart中以現在的 RunQueue Clock值減去Task的sched_info.last_arrival 時間值,得到的 Delta作為變數rq_cpu_time的累 加值.*/ unsigned long long rq_cpu_time; /* could above be rq->cfs_rq.exec_clock + rq->rt_rq.rt_runtime ? */ /* sys_sched_yield() stats */ /*用以統計呼叫System Call sys_sched_yield的次數.*/ unsigned int yld_count; /* schedule() stats */ unsigned int sched_switch; /*可用以統計觸發Scheduling的次數. 在每次觸發 Scheduling時,會透過函式schedule呼叫schedule_debug, 呼叫schedstat_inc對這變數進行累加.*/ unsigned int sched_count; /*可用以統計進入到Idle Task的次數. 會在函式 pick_next_task_idle中,呼叫schedstat_inc對這變數進行 累加.*/ unsigned int sched_goidle; /* try_to_wake_up() stats */ /*用以統計Wake Up Task的次數.*/ unsigned int ttwu_count; /*用以統計Wake Up 同一個處理器Task的次數.*/ unsigned int ttwu_local; /* BKL stats */ unsigned int bkl_count; #endif };
參考文獻:https://blog.csdn.net/sangliu/article/details/8476770
https://blog.csdn.net/yujingbo1023/article/details/41924419
https://www.cnblogs.com/hanxiaoyu/p/5576277.html
https://blog.csdn.net/bullbat/article/details/7160246
根據Linux2.6.26源碼分析進程模型