Linux調度器 ——進程優先級

一、前言

本文主要描述的是進程優先級這個概念。從用戶空間來看,進程優先級就是nice value和scheduling priority,對應到內核,有靜態優先級、realtime優先級、歸一化優先級和動態優先級等概念,我們希望能在第二章將這些相關的概念描述清楚。爲了加深理解,在第三章我們給出了幾個典型數據流過程的分析。

二、overview

1、藍圖
在這裏插入圖片描述

2、用戶空間的視角

在用戶空間,進程優先級有兩種含義:nice value和scheduling priority。對於普通進程而言,進程優先級就是nice value,從-20(優先級最高)~19(優先級最低),通過修改nice value可以改變普通進程獲取cpu資源的比例。隨着實時需求的提出,進程又被賦予了另外一種屬性scheduling priority,而這些進程被稱爲實時進程。實時進程的優先級的範圍可以通過sched_get_priority_min和sched_get_priority_max,對於linux而言,實時進程的scheduling priority的範圍是1(優先級最低)~99(優先級最高)。當然,普通進程也有scheduling priority,被設定爲0。

3、內核中的實現

內核中,task struct中有若干和進程優先級有個的成員,如下:

struct task_struct {
......
    int prio, static_prio, normal_prio;
    unsigned int rt_priority;
......
    unsigned int policy;
......
}

policy成員記錄了該線程的調度策略,而其他的成員表示了各種類型的優先級,下面的小節我們會一一描述。

4、靜態優先級

task struct中的static_prio成員。我們稱之靜態優先級,其特點如下:

(1)值越小,進程優先級越高

(2)0 – 99用於real-time processes(沒有實際的意義),100 – 139用於普通進程

(3)缺省值是 120

(4)用戶空間可以通過nice()或者setpriority對該值進行修改。通過getpriority可以獲取該值。

(5)新創建的進程會繼承父進程的static priority。

靜態優先級是所有相關優先級的計算的起點,要麼繼承自父進程,要麼用戶空間自行設定。一旦修改了靜態優先級,那麼normal priority和動態優先級都需要重新計算。

5、實時優先級

task struct中的rt_priority成員表示該線程的實時優先級,也就是從用戶空間的視角來看的scheduling priority。0是普通進程,1~99是實時進程,99的優先級最高。

6、歸一化優先級

task struct中的normal_prio成員。我們稱之歸一化優先級(normalized priority),它是根據靜態優先級、scheduling priority和調度策略來計算得到,代碼如下:

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)這個看起來稍微有點晦澀的術語。如果你做過音視頻定點算法的優化,應該對這個詞不陌生。不同的定點數據有不同的表示,有Q31的,有Q15,這些數據的小數點的位置不同,無法進行比較、加減等操作,因此需要歸一化,全部轉換成某個特定的數據格式(其實就是確定小數點的位置)。在數學上,1米和1mm在進行操作的時候也需要歸一化,全部轉換成同一個量綱就OK了。對於這裏的優先級,調度器需要綜合考慮各種因素,例如調度策略,nice value、scheduling priority等,把這些factor全部考慮進來,歸一化成一個數軸上的number,以此來表示其優先級,這就是normalized priority。對於一個線程,其normalized priority的number越小,其優先級越大。

調度策略是deadline的進程比RT進程和normal進程的優先級還要高,因此它的歸一化優先級是負數:-1。如果採用實時調度策略,那麼該線程的normalized priority和rt_priority相關。task struct中的rt_priority成員是用戶空間視角的實時優先級(scheduling priority),MAX_RT_PRIO-1是99,MAX_RT_PRIO-1 - p->rt_priority則翻轉了實時進程的scheduling priority,最高優先級是0,最低是98。順便說一句,normalized priority是99的情況是沒有意義的。對於普通進程,normalized priority就是其靜態優先級。

7、動態優先級

task struct中的prio成員表示了該線程的動態優先級,也就是調度器在進行調度時候使用的那個優先級。動態優先級在運行時可以被修改,例如在處理優先級翻轉問題的時候,系統可能會臨時調升一個普通進程的優先級。一般設定動態優先級的代碼是這樣的:p->prio = effective_prio§,具體計算動態優先級的代碼如下:

static int effective_prio(struct task_struct *p)
{
p->normal_prio = normal_prio§;
if (!rt_prio(p->prio))
return p->normal_prio;
return p->prio;
}

rt_prio是一個根據當前優先級來確定是否是實時進程的函數,包括兩種情況,一種情況是該進程是實時進程,調度策略是SCHED_FIFO或者SCHED_RR。另外一種情況是人爲的將該進程提升到RT priority的區域(例如在使用優先級繼承的方法解決系統中優先級翻轉問題的時候)。在這兩種情況下,我們都不改變其動態優先級,即effective_prio返回當前動態優先級p->prio。其他情況,進程的動態優先級跟隨歸一化的優先級。

三、典型數據流程分析

1、用戶空間設定nice value

用戶空間設定nice value的操作,在內核中主要是set_user_nice函數實現的,無論是sys_nice或者sys_setpriority,在參數檢查和權限檢查之後都會調用set_user_nice函數,完成具體的設定。代碼如下:

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);-------------------(2if (queued)
        dequeue_task(rq, p, DEQUEUE_SAVE);

    p->static_prio = NICE_TO_PRIO(nice);----------------(3set_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);------------(2if (delta < 0 || (delta > 0 && task_running(rq, p)))------------(4resched_curr(rq);
    }
out_unlock:
    task_rq_unlock(rq, p, &flags);
}

(1)如果是實時進程或者deadline類型的進程,那麼nice value其實是沒有什麼實際意義的,不過我們還是設定其靜態優先級,當然,這樣的設定其實不會起到什麼作用的,也不會實際改變調度器行爲,因此直接返回,沒有dequeue和enqueue的動作。

(2)在step中已經處理了調度策略是RT類和DEADLINE類的進程,因此,執行到這裏,只可能是普通進程了,使用CFS算法。如果該task在run queue上(queued 等於true),那麼由於我們修改了nice value,調度器需要重新審視當前runqueue中的task。因此,我們需要將該task從rq中摘下,在重新計算優先級之後,再次插入該runqueue對應的runable task的紅黑樹中。

(3)最核心的代碼就是p->static_prio = NICE_TO_PRIO(nice);這一句了,其他的都是side effect。比如說load weight。當cpu一刻不停的運算的時候,其load是100%,沒有機會調度到idle進程休息一下。當系統中沒有實時進程或者deadline進程的時候,所有的runnable的進程一起來瓜分cpu資源,以此不同的進程分享一個特定比例的cpu資源,我們稱之load weight。不同的nice value對應不同的cpu load weight,因此,當更改nice value的時候,也必須通過set_load_weight來更新該進程的cpu load weight。除了load weight,該線程的動態優先級也需要更新,這是通過p->prio = effective_prio§;來完成的。

(4)delta 記錄了新舊線程的動態優先級的差值,當調試了該線程的優先級(delta < 0),那麼有可能產生一個調度點,因此,調用resched_curr,給當前正在運行的task做一個標記,以便在返回用戶空間的時候進行調度。此外,如果修改當前running狀態的task的動態優先級,那麼調降(delta > 0)意味着該進程有可能需要讓出cpu,因此也需要resched_curr標記當前running狀態的task需要reschedule。

2、進程缺省的調度策略和調度參數

我們先思考這樣的一個問題:在用戶空間設定調度策略和調度參數之前,一個線程的default scheduling policy是什麼呢?這需要追溯到fork的時候(具體代碼在sched_fork函數中),這個和task struct中sched_reset_on_fork設定相關。如果沒有設定這個flag,那麼說明在fork的時候,子進程跟隨父進程的調度策略,如果設定了這個flag,則說明子進程的調度策略和調度參數不能繼承自父進程,而是需要設定爲default。代碼片段如下:

int sched_fork(unsigned long clone_flags, struct task_struct *p)
{

……
    p->prio = current->normal_prio; -------------------(1if (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); ------------(3set_load_weight(p); 
        p->sched_reset_on_fork = 0;
    }

……

}

(1)sched_fork只是fork過程中的一個片段,在fork一開始,dup_task_struct已經複製了一個和父進程完全一個的進程描述符(task struct),因此,如果沒有步驟2中的重置,那麼子進程是跟隨父進程的調度策略和調度參數(各種優先級),當然,有時候爲了解決PI問題而臨時調升父進程的動態優先級,在fork的時候不宜傳遞到子進程中,因此這裏重置了動態優先級。

(2)缺省的調度策略是SCHED_NORMAL,靜態優先級等於120(也就是說nice value等於0),rt priority等於0(普通進程)。不管父進程如何,即便是deadline的進程,其fork的子進程也需要恢復到缺省參數。

(3)既然調度策略和靜態優先級已經修改了,那麼也需要更新動態優先級和歸一化優先級。此外,load weight也需要更新。一旦子進程中恢復到了缺省的調度策略和優先級,那麼sched_reset_on_fork這個flag已經完成了歷史使命,可以clear掉了。

OK,至此,我們瞭解了在fork過程中對調度策略和調度參數的處理,這裏還是要追加一個問題:爲何不一切繼承父進程的調度策略和參數呢?爲何要在fork的時候reset to default呢?在linux中,對於每一個進程,我們都會進行資源限制。例如對於那些實時進程,如果它持續消耗cpu資源而沒有發起一次可以引起阻塞的系統調用,那麼我們猜測這個realtime進程跑飛了,從而鎖住了系統。對於這種情況,我們要進行干預,因此引入了RLIMIT_RTTIME這個per-process的資源限制項。但是,如果用戶空間的realtime進程通過fork其實也可以繞開RLIMIT_RTTIME這個限制,從而肆意的攫取cpu資源。然而,機智的內核開發人員早已經看穿了這一切,爲了防止實時進程“泄露”到其子進程中,sched_reset_on_fork這個flag被提出來。

3、用戶空間設定調度策略和調度參數

通過sched_setparam接口函數可以修改rt priority的調度參數,而通過sched_setscheduler功能會更強一些,不但可以設定rt priority,還可以設定調度策略。而sched_setattr是一個集大成之接口,可以設定一個線程的調度策略以及該調度策略下的調度參數。當然,對於內核,這些接口都通過__sched_setscheduler這個內核函數來完成對指定線程調度策略和調度參數的修改。

__sched_setscheduler分成兩個部分,首先進行安全性檢查和參數檢查,其次進行具體的設定。

我們先看看安全性檢查。如果用戶空間可以自由的修改調度策略和調度優先級,那麼世界就亂套了,每個進程可能都想把自己的調度策略和優先級提升上去,從而獲取足夠的CPU 資源。因此用戶空間設定調度策略和調度參數要遵守一定的規則:如果沒有CAP_SYS_NICE的能力,那麼基本上該線程能被允許的操作只是降級而已。例如從SCHED_FIFO修改成SCHED_NORMAL,異或不修改scheduling policy,而是降低靜態優先級(nice value)或者實時優先級(scheduling priority)。這裏例外的是SCHED_DEADLINE的設定,按理說如果進程本身的調度策略就是SCHED_DEADLINE,那麼應該允許“優先級”降低的操作(這裏用優先級不是那麼合適,其實就是減小run time,或者加大period,這樣可以放鬆對cpu資源的獲取),但是目前的4.4.6內核不允許(也許以後版本的內核會允許)。此外,如果沒有CAP_SYS_NICE的能力,那麼設定調度策略和調度參數的操作只能是限於屬於同一個登錄用戶的線程。如果擁有CAP_SYS_NICE的能力,那麼就沒有那麼多限制了,可以從普通進程提升成實時進程(修改policy),也可以提升靜態優先級或者實時優先級。

具體的修改比較簡單,是通過__setscheduler_params函數完成,其實也就是是根據sched_attr中的參數設定到task struct相關成員中,大家可以自行閱讀代碼進行理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章