一、前言
本文主要描述的是進程優(yōu)先級這個概念。從用戶空間來看,進程優(yōu)先級就是nice value和scheduling priority,對應(yīng)到內(nèi)核,有靜態(tài)優(yōu)先級、realtime優(yōu)先級、歸一化優(yōu)先級和動態(tài)優(yōu)先級等概念,我們希望能在第二章將這些相關(guān)的概念描述清楚。為了加深理解,在第三章我們給出了幾個典型數(shù)據(jù)流過程的分析。
二、overview
1、藍圖
2、用戶空間的視角
在用戶空間,進程優(yōu)先級有兩種含義:nice value和scheduling priority。對于普通進程而言,進程優(yōu)先級就是nice value,從-20(優(yōu)先級最高)~19(優(yōu)先級最低),通過修改nice value可以改變普通進程獲取cpu資源的比例。隨著實時需求的提出,進程又被賦予了另外一種屬性scheduling priority,而這些進程被稱為實時進程。實時進程的優(yōu)先級的范圍可以通過sched_get_priority_min和sched_get_priority_max,對于linux而言,實時進程的scheduling priority的范圍是1(優(yōu)先級最低)~99(優(yōu)先級最高)。當(dāng)然,普通進程也有scheduling priority,被設(shè)定為0。
3、內(nèi)核中的實現(xiàn)
內(nèi)核中,task struct中有若干和進程優(yōu)先級有個的成員,如下:
struct task_struct {?
......?
??? int prio, static_prio, normal_prio;?
??? unsigned int rt_priority;?
......?
??? unsigned int policy;?
......?
}
policy成員記錄了該線程的調(diào)度策略,而其他的成員表示了各種類型的優(yōu)先級,下面的小節(jié)我們會一一描述。
4、靜態(tài)優(yōu)先級
task struct中的static_prio成員。我們稱之靜態(tài)優(yōu)先級,其特點如下:
(1)值越小,進程優(yōu)先級越高
(2)0 – 99用于real-time processes(沒有實際的意義),100 – 139用于普通進程
(3)缺省值是 120
(4)用戶空間可以通過nice()或者setpriority對該值進行修改。通過getpriority可以獲取該值。
(5)新創(chuàng)建的進程會繼承父進程的static priority。
靜態(tài)優(yōu)先級是所有相關(guān)優(yōu)先級的計算的起點,要么繼承自父進程,要么用戶空間自行設(shè)定。一旦修改了靜態(tài)優(yōu)先級,那么normal priority和動態(tài)優(yōu)先級都需要重新計算。
5、實時優(yōu)先級
task struct中的rt_priority成員表示該線程的實時優(yōu)先級,也就是從用戶空間的視角來看的scheduling priority。0是普通進程,1~99是實時進程,99的優(yōu)先級最高。
6、歸一化優(yōu)先級
task struct中的normal_prio成員。我們稱之歸一化優(yōu)先級(normalized priority),它是根據(jù)靜態(tài)優(yōu)先級、scheduling priority和調(diào)度策略來計算得到,代碼如下:
static inline int normal_prio(struct task_struct *p)?
{?
??? int prio;
if (task_has_dl_policy(p))?
??????? prio = MAX_DL_PRIO-1;?
??? else if (task_has_rt_policy(p))?
??????? prio = MAX_RT_PRIO-1 - p->rt_priority;?
??? else?
??????? prio = __normal_prio(p);?
??? return prio;?
}
這里我們先聊聊歸一化(Normalization)這個看起來稍微有點晦澀的術(shù)語。如果你做過音視頻定點算法的優(yōu)化,應(yīng)該對這個詞不陌生。不同的定點數(shù)據(jù)有不同的表示,有Q31的,有Q15,這些數(shù)據(jù)的小數(shù)點的位置不同,無法進行比較、加減等操作,因此需要歸一化,全部轉(zhuǎn)換成某個特定的數(shù)據(jù)格式(其實就是確定小數(shù)點的位置)。在數(shù)學(xué)上,1米和1mm在進行操作的時候也需要歸一化,全部轉(zhuǎn)換成同一個量綱就OK了。對于這里的優(yōu)先級,調(diào)度器需要綜合考慮各種因素,例如調(diào)度策略,nice value、scheduling priority等,把這些factor全部考慮進來,歸一化成一個數(shù)軸上的number,以此來表示其優(yōu)先級,這就是normalized priority。對于一個線程,其normalized priority的number越小,其優(yōu)先級越大。
調(diào)度策略是deadline的進程比RT進程和normal進程的優(yōu)先級還要高,因此它的歸一化優(yōu)先級是負(fù)數(shù):-1。如果采用實時調(diào)度策略,那么該線程的normalized priority和rt_priority相關(guān)。task struct中的rt_priority成員是用戶空間視角的實時優(yōu)先級(scheduling priority),MAX_RT_PRIO-1是99,MAX_RT_PRIO-1 - p->rt_priority則翻轉(zhuǎn)了實時進程的scheduling priority,最高優(yōu)先級是0,最低是98。順便說一句,normalized priority是99的情況是沒有意義的。對于普通進程,normalized priority就是其靜態(tài)優(yōu)先級。
7、動態(tài)優(yōu)先級
task struct中的prio成員表示了該線程的動態(tài)優(yōu)先級,也就是調(diào)度器在進行調(diào)度時候使用的那個優(yōu)先級。動態(tài)優(yōu)先級在運行時可以被修改,例如在處理優(yōu)先級翻轉(zhuǎn)問題的時候,系統(tǒng)可能會臨時調(diào)升一個普通進程的優(yōu)先級。一般設(shè)定動態(tài)優(yōu)先級的代碼是這樣的:p->prio = effective_prio(p),具體計算動態(tài)優(yōu)先級的代碼如下:
static int effective_prio(struct task_struct *p)?
{?
??? p->normal_prio = normal_prio(p);?
??? if (!rt_prio(p->prio))?
??????? return p->normal_prio;?
??? return p->prio;?
}
rt_prio是一個根據(jù)當(dāng)前優(yōu)先級來確定是否是實時進程的函數(shù),包括兩種情況,一種情況是該進程是實時進程,調(diào)度策略是SCHED_FIFO或者SCHED_RR。另外一種情況是人為的將該進程提升到RT priority的區(qū)域(例如在使用優(yōu)先級繼承的方法解決系統(tǒng)中優(yōu)先級翻轉(zhuǎn)問題的時候)。在這兩種情況下,我們都不改變其動態(tài)優(yōu)先級,即effective_prio返回當(dāng)前動態(tài)優(yōu)先級p->prio。其他情況,進程的動態(tài)優(yōu)先級跟隨歸一化的優(yōu)先級。
三、典型數(shù)據(jù)流程分析
1、用戶空間設(shè)定nice value
用戶空間設(shè)定nice value的操作,在內(nèi)核中主要是set_user_nice函數(shù)實現(xiàn)的,無論是sys_nice或者sys_setpriority,在參數(shù)檢查和權(quán)限檢查之后都會調(diào)用set_user_nice函數(shù),完成具體的設(shè)定。代碼如下:
void set_user_nice(struct task_struct *p, long nice)?
{?
??? int old_prio, delta, queued;?
??? unsigned long flags;?
??? struct rq *rq;??
??? rq = task_rq_lock(p, &flags);?
??? if (task_has_dl_policy(p) || task_has_rt_policy(p)) {-----------(1)?
??????? p->static_prio = NICE_TO_PRIO(nice);?
??????? goto out_unlock;?
??? }?
??? queued = task_on_rq_queued(p);-------------------(2)?
??? if (queued)?
??????? dequeue_task(rq, p, DEQUEUE_SAVE);
p->static_prio = NICE_TO_PRIO(nice);----------------(3)?
??? set_load_weight(p);?
??? old_prio = p->prio;?
??? p->prio = effective_prio(p);?
??? delta = p->prio - old_prio;
if (queued) {?
??????? enqueue_task(rq, p, ENQUEUE_RESTORE);------------(2)?
??????? if (delta < 0 || (delta > 0 && task_running(rq, p)))------------(4)?
??????????? resched_curr(rq);?
??? }?
out_unlock:?
??? task_rq_unlock(rq, p, &flags);?
}
(1)如果是實時進程或者deadline類型的進程,那么nice value其實是沒有什么實際意義的,不過我們還是設(shè)定其靜態(tài)優(yōu)先級,當(dāng)然,這樣的設(shè)定其實不會起到什么作用的,也不會實際改變調(diào)度器行為,因此直接返回,沒有dequeue和enqueue的動作。
(2)在step中已經(jīng)處理了調(diào)度策略是RT類和DEADLINE類的進程,因此,執(zhí)行到這里,只可能是普通進程了,使用CFS算法。如果該task在run queue上(queued 等于true),那么由于我們修改了nice value,調(diào)度器需要重新審視當(dāng)前runqueue中的task。因此,我們需要將該task從rq中摘下,在重新計算優(yōu)先級之后,再次插入該runqueue對應(yīng)的runable task的紅黑樹中。
(3)最核心的代碼就是p->static_prio = NICE_TO_PRIO(nice);這一句了,其他的都是side effect。比如說load weight。當(dāng)cpu一刻不停的運算的時候,其load是100%,沒有機會調(diào)度到idle進程休息一下。當(dāng)系統(tǒng)中沒有實時進程或者deadline進程的時候,所有的runnable的進程一起來瓜分cpu資源,以此不同的進程分享一個特定比例的cpu資源,我們稱之load weight。不同的nice value對應(yīng)不同的cpu load weight,因此,當(dāng)更改nice value的時候,也必須通過set_load_weight來更新該進程的cpu load weight。除了load weight,該線程的動態(tài)優(yōu)先級也需要更新,這是通過p->prio = effective_prio(p);來完成的。
(4)delta 記錄了新舊線程的動態(tài)優(yōu)先級的差值,當(dāng)調(diào)試了該線程的優(yōu)先級(delta < 0),那么有可能產(chǎn)生一個調(diào)度點,因此,調(diào)用resched_curr,給當(dāng)前正在運行的task做一個標(biāo)記,以便在返回用戶空間的時候進行調(diào)度。此外,如果修改當(dāng)前running狀態(tài)的task的動態(tài)優(yōu)先級,那么調(diào)降(delta > 0)意味著該進程有可能需要讓出cpu,因此也需要resched_curr標(biāo)記當(dāng)前running狀態(tài)的task需要reschedule。
2、進程缺省的調(diào)度策略和調(diào)度參數(shù)
我們先思考這樣的一個問題:在用戶空間設(shè)定調(diào)度策略和調(diào)度參數(shù)之前,一個線程的default scheduling policy是什么呢?這需要追溯到fork的時候(具體代碼在sched_fork函數(shù)中),這個和task struct中sched_reset_on_fork設(shè)定相關(guān)。如果沒有設(shè)定這個flag,那么說明在fork的時候,子進程跟隨父進程的調(diào)度策略,如果設(shè)定了這個flag,則說明子進程的調(diào)度策略和調(diào)度參數(shù)不能繼承自父進程,而是需要設(shè)定為default。代碼片段如下:
int sched_fork(unsigned long clone_flags, struct task_struct *p)?
{
……?
??? p->prio = current->normal_prio; -------------------(1)?
??? if (unlikely(p->sched_reset_on_fork)) {?
??????? if (task_has_dl_policy(p) || task_has_rt_policy(p)) {----------(2)?
??????????? p->policy = SCHED_NORMAL;?
??????????? p->static_prio = NICE_TO_PRIO(0);?
??????????? p->rt_priority = 0;?
??????? } else if (PRIO_TO_NICE(p->static_prio) < 0)?
??????????? p->static_prio = NICE_TO_PRIO(0);
p->prio = p->normal_prio = __normal_prio(p); ------------(3)?
??????? set_load_weight(p);??
??????? p->sched_reset_on_fork = 0;?
??? }
……
}
(1)sched_fork只是fork過程中的一個片段,在fork一開始,dup_task_struct已經(jīng)復(fù)制了一個和父進程完全一個的進程描述符(task struct),因此,如果沒有步驟2中的重置,那么子進程是跟隨父進程的調(diào)度策略和調(diào)度參數(shù)(各種優(yōu)先級),當(dāng)然,有時候為了解決PI問題而臨時調(diào)升父進程的動態(tài)優(yōu)先級,在fork的時候不宜傳遞到子進程中,因此這里重置了動態(tài)優(yōu)先級。
(2)缺省的調(diào)度策略是SCHED_NORMAL,靜態(tài)優(yōu)先級等于120(也就是說nice value等于0),rt priority等于0(普通進程)。不管父進程如何,即便是deadline的進程,其fork的子進程也需要恢復(fù)到缺省參數(shù)。
(3)既然調(diào)度策略和靜態(tài)優(yōu)先級已經(jīng)修改了,那么也需要更新動態(tài)優(yōu)先級和歸一化優(yōu)先級。此外,load weight也需要更新。一旦子進程中恢復(fù)到了缺省的調(diào)度策略和優(yōu)先級,那么sched_reset_on_fork這個flag已經(jīng)完成了歷史使命,可以clear掉了。
OK,至此,我們了解了在fork過程中對調(diào)度策略和調(diào)度參數(shù)的處理,這里還是要追加一個問題:為何不一切繼承父進程的調(diào)度策略和參數(shù)呢?為何要在fork的時候reset to default呢?在linux中,對于每一個進程,我們都會進行資源限制。例如對于那些實時進程,如果它持續(xù)消耗cpu資源而沒有發(fā)起一次可以引起阻塞的系統(tǒng)調(diào)用,那么我們猜測這個realtime進程跑飛了,從而鎖住了系統(tǒng)。對于這種情況,我們要進行干預(yù),因此引入了RLIMIT_RTTIME這個per-process的資源限制項。但是,如果用戶空間的realtime進程通過fork其實也可以繞開RLIMIT_RTTIME這個限制,從而肆意的攫取cpu資源。然而,機智的內(nèi)核開發(fā)人員早已經(jīng)看穿了這一切,為了防止實時進程“泄露”到其子進程中,sched_reset_on_fork這個flag被提出來。
3、用戶空間設(shè)定調(diào)度策略和調(diào)度參數(shù)
通過sched_setparam接口函數(shù)可以修改rt priority的調(diào)度參數(shù),而通過sched_setscheduler功能會更強一些,不但可以設(shè)定rt priority,還可以設(shè)定調(diào)度策略。而sched_setattr是一個集大成之接口,可以設(shè)定一個線程的調(diào)度策略以及該調(diào)度策略下的調(diào)度參數(shù)。當(dāng)然,對于內(nèi)核,這些接口都通過__sched_setscheduler這個內(nèi)核函數(shù)來完成對指定線程調(diào)度策略和調(diào)度參數(shù)的修改。
__sched_setscheduler分成兩個部分,首先進行安全性檢查和參數(shù)檢查,其次進行具體的設(shè)定。
我們先看看安全性檢查。如果用戶空間可以自由的修改調(diào)度策略和調(diào)度優(yōu)先級,那么世界就亂套了,每個進程可能都想把自己的調(diào)度策略和優(yōu)先級提升上去,從而獲取足夠的CPU 資源。因此用戶空間設(shè)定調(diào)度策略和調(diào)度參數(shù)要遵守一定的規(guī)則:如果沒有CAP_SYS_NICE的能力,那么基本上該線程能被允許的操作只是降級而已。例如從SCHED_FIFO修改成SCHED_NORMAL,異或不修改scheduling policy,而是降低靜態(tài)優(yōu)先級(nice value)或者實時優(yōu)先級(scheduling priority)。這里例外的是SCHED_DEADLINE的設(shè)定,按理說如果進程本身的調(diào)度策略就是SCHED_DEADLINE,那么應(yīng)該允許“優(yōu)先級”降低的操作(這里用優(yōu)先級不是那么合適,其實就是減小run time,或者加大period,這樣可以放松對cpu資源的獲?。?,但是目前的4.4.6內(nèi)核不允許(也許以后版本的內(nèi)核會允許)。此外,如果沒有CAP_SYS_NICE的能力,那么設(shè)定調(diào)度策略和調(diào)度參數(shù)的操作只能是限于屬于同一個登錄用戶的線程。如果擁有CAP_SYS_NICE的能力,那么就沒有那么多限制了,可以從普通進程提升成實時進程(修改policy),也可以提升靜態(tài)優(yōu)先級或者實時優(yōu)先級。
具體的修改比較簡單,是通過__setscheduler_params函數(shù)完成,其實也就是是根據(jù)sched_attr中的參數(shù)設(shè)定到task struct相關(guān)成員中,大家可以自行閱讀代碼進行理解。
參考文檔:
1、linux下的各種man page
2、linux 4.4.6內(nèi)核源代碼
評論