天天看點

__schedule的一些小細節

(代碼主要參考5.10)

1. __schedule的參數preempt

static void __sched notrace __schedule(bool preempt)

preempt

是一個bool的類型的值。

__schedule

中有這樣的一段代碼,(有删減):

switch_count = &prev->nivcsw;
​
prev_state = prev->state;
if (!preempt && prev_state) {
    if (signal_pending_state(prev_state, prev)) {
        prev_state = TASK_RUNNING;
    } else {
        ......;
        deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
        ......;
    }
    switch_count = &prev->nvcsw;
}
​
......;
if (likely(prev != next)) {
    ......;
    ++switch_context;
}      

preempt

代表是否自願上下文切換。如果是自願(非搶占進行排程),則為false;如果是非自願(搶占進行排程),則為true。

struct task_struct

有兩個成員

nvcsw

nivcsw

nvcsw nivcsw
Number of Voluntary Context Switches(自願上下文切換的計數) Number of InVoluntary Context Switches(非自願上下文切換計數)

當一個程序非自願上下文切換的時候,即被搶占的時候,會少判斷一些内容;

而當一個程序自願上下文切換的時候,即主動放棄CPU的時候,要進行一些判斷,會決定prev的狀态,是否出隊,以及負載均衡的一些操作,這裡就不較長的描述了。

至于哪些函數,會觸發排程

__schedule

,它們分别是搶占還是非搶占呢?5.10中如下所示:

function preempt
do_task_dead false
schedule false
schedule_idle false
preempt_schedule_comm true
preempt_schedule_notrace true
preempt_schedule_irq true

這些函數留給後續分析吧。

2. pick_next_task的兩條路徑

pick_next_task

函數在

__schedule

中調用,挑選下一個要執行的程序。

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
    ......;
    if (likely(prev->sched_class <= &fair_sched_class &&
            rq->nr_running == rq->cfs.h_nr_running)) {---短路徑    
        p = pick_next_task_fair(rq, prev, rf);
        ......; 
    }
restart:
    ......;
    for_each_class(class) {---長路徑
        p = class->pick_next_task(rq);
        ......;
    }
}      

是走長路徑、還是短路徑呢?判斷條件為:目前程序的排程類是否為cfs或者idle以及運作隊列的程序數量是否與cfs運作隊列的程序數量相等。

cfs_rq

中除了

h_nr_running

外,還有一個

nr_running

,以及

rq

中也存在一個

nr_running

,它們分别代表什麼?

成員 解釋
rq的nr_running 代表運作隊列的程序個數
cfs_rq的nr_running 開啟組排程的話,代表組排程最上層的group個數
cfs_rq的h_nr_running 代表cfs_rq中的程序個數

3. context_switch的四種情況

挑選出下一個要執行的程序next後,要使用

context_switch

進行位址空間的切換。

static __always_inline stuct rq *
context_switch(struct rq *rq, struct task_struct *prev,
        struct task_struct *next, struct rq_flags *rf)
{
    ......;
    if (!next->mm) {
        enter_lazy_tlb(prev->active);
        
        next->active_mm = prev->active_mm;
        if (prev->mm)
            mmgrab(prev->active_mm);
        else
            prev->active_mm = NULL;
    } else {
        membarrier_switch_mm(rq, prev->active_mm, next->mm);
​
        switch_mm_irqs_off(prev->active_mm, next->mm, next);
​
        if (!prev->mm) {
            rq->prev_mm = prev->active_mm;
            prev->active_mm = NULL;
        }
    }
    ......;
    switch_to(prev, next, prev);
    barrier();
​
    return finish_task_switch(prev);
}      

所謂的四種情況,其實就是prev和next分别是user線程還是kernel線程的組合情況。

prev next 操作
kernel kernel tlb lazy模式,next借用prev的active_mm,prev的active_mm清空
user kernel tlb lazy模式,next借用prev的active_mm,prev的mm_count增加計數
kernel user 位址空間切換,rq記錄prev_mm,将prev->active_mm清空
user user 程序位址空間切換

Q1:什麼是tlb lazy模式?

tlb是什麼?是一個虛拟位址轉換成實體位址的快速轉換表,常用于cache尋址中。

通常CPU都是程序切換一次,進行一次flush(後面有其他不用全部flush的方法,不較長的描述了)。

而核心空間是所有程序通用的,故可以不用flush tlb,這就是tlb lazy模式。

Q2:

mm

active_mm

差別?

mm

的存在與否用于判定該程序是屬于user還是kernel;

active_mm

則為實際使用的位址空間,kernel線程總是借用user線程的位址空間。

可以看到,每次kernel線程被切換出去後,它的

active_mm

就會被清空,因為是借用的;而每次user線程切換kernel的時候,還會增加一個計數值,用于表示該user線程的位址空間被借用了。

4. switch_to的三個參數

switch_to

的工作主要是切換核心棧,它的具體實作就不在這裡分析。

不同的體系架構下也不一樣,例如,X86的實作主要使用将目前寄存器的一些值壓到prev的核心棧,将核心棧頂指針儲存到每個程序相關聯的

thread_info

,然後切換到next的核心棧,并出棧,将其棧中内容填充到寄存器,以恢複現場。

switch_to

三個參數,其中兩個prev的考慮:

設想場景如下:a切換到b,b切換到c,c切換到a。

a壓棧時内容:prev為a,next為b;

b壓棧時内容:prev為b,next為c;

c壓棧時内容:prev為c,next為a。

c切換a,出棧之後呢?

prev為a,next為b

可以看到完全沒有c的事了。我們必須得留下c存在過的痕迹。

故這裡使用三個參數,其中兩個prev,用來留下最新的prev的痕迹。

5. finish_task_switch與context_switch的關聯

為什麼一定要留下最新的prev的痕迹呢?你有沒有想過?

finish_task_switch

的參數就是prev,就是因為它使用到了prev,是以才得留下它。

finsih_task_switch

其實涉及到的東西蠻多,計算vtime,perf追蹤點等等。

但就在程序排程過程中,還有一個細節沒有處理,還記得是什麼嗎,參見

finish_task_switch

的部分代碼:

struct mm_struct *mm = rq->prev_mm;
​
......;
​
if (mm) {
    membarrier_mm_sync_core_before_usermode(mm);
    mmdrop(mm);
​
}      

如果從user到kernel,那麼得給借用的

mm

增加一個計數;

但是什麼時候減去呢?

一旦從kernel到user,

rq

就記錄下prev使用的

active_mm

,在

finish_task_switch

中減去這個計數。

有道詞典

static void __s ...

詳細X

靜态孔隙__sched notrace __schedule (bool搶占)