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