Linux內核設計與實現 總結筆記(第四章)進程調度

進程調度

調度程序負責決定將哪一個進程投入運行,什麼時候運行以及運行多長時間。node

調度程序沒有太複雜的原理,最大限度地利用處理器時間的原則是,只要有能夠執行的進程,那麼就總會有進程正在執行。linux

 

1、多任務

多任務系統能夠劃分爲兩類:非搶佔式多任務和搶佔式多任務。算法

Linux提供了搶佔式的多任務模式,在此模式下,有調度程序來決定何時中止一個進程的運行,以便其餘進程可以獲得執行機會。緩存

這個強制掛起的動做就叫搶佔。進程在被搶佔以前,可以運行的時間是預先設置好的,叫進程的時間片。ide

 

2、Linux 的進程調度

最終在2.6.23中肯定了Linux的調度算法,稱爲「徹底公平調度算法」,簡稱CFS。模塊化

 

3、策略

I/O消耗型和處理器消耗型進程函數

進程能夠被分爲I/O消耗型和處理器消耗型。優化

調度策略一般要在兩個矛盾的目標中間尋找平衡:進程響應速度(響應時間短)和最大系統利用率(高吞吐量)。this

 

3.1 進程優先級

調度算法中最基本的一類就是基於優先級的調度。一般作法是優先級高的進程先運行,低的後運行,相同優先級的進程按輪轉方式進行調度。spa

Linux採用了兩種不一樣優先級範圍,第一種是用nice值,從-20到+19,默認爲0。nice值越大,優先級越低

第二種範圍是實時優先級,其值是可配置的。範圍是0到99(包括0和99),實時優先級越高,數值越大。

nice和實時優先級是兩個互不相交的範疇。

 

3.2 時間片

時間片是一個數值,它代表進程再被搶佔前所能持續運行的時間。過長或太短的設置時間片都不合適,會增長系統開銷。

不少操做系統默認10ms,但Linux的CFS調度系統不是直接分配時間片給進程,而是按nice值比例分配進程時間片。

而Linux的搶佔也是須要根據時間片的,若是消耗的使用比比當前進程小,則新進程馬上投入運行,搶佔當前進程。不然,退出其運行。

 

4、Linux調度算法

Linux調度器是以模塊方式提供的,目的是能夠容許不一樣類型的進程能夠針對性的選擇調度算法。這種模塊化結構被稱爲調度器類。

任何進程所得到的處理器時間是由它本身和其餘全部可運行進程nice值的相對差值決定的。

 

5、Linux調度的實現

CFS相關代碼位於文件kernel/sched_fair.c中,特別的有四個組成部分:

時間記帳、進程選擇、調度器入口、睡眠和喚醒

 

5.1 時間記帳

全部調度器都必須對進程運行時間作記帳,當一個進程時間片減小到0時,就會進行搶佔。

5.1.1調度器實時結構

CFS必須維護每一個進程運行的時間記帳,由於它須要確保公平分配處理器運行時間。在文件<linux/sched.h>的struct_sched_entity中,來追蹤進程運行記帳:

struct sched_entity {
    struct load_weight  load;       /* for load-balancing 負荷權重,決定進程在CPU上運行時間和被調度次數*/
    struct rb_node      run_node;   /* 樹節點,用於在紅黑樹上組織排序 */
    struct list_head    group_node; /* 實體所在的進程組 */
    unsigned int        on_rq;      /* 是否在就緒隊列上 */

    u64         exec_start;         /* 調度器上次更新這個實例的時間(實際時間) */
    u64         sum_exec_runtime;   /* 自進程啓動起來,運行的總時間(實際時間) */
    u64         vruntime;           /* 該調度實體運行的虛擬時間 */
    u64         prev_sum_exec_runtime;  /* 進程在上次被撤銷cpu時,運行的總時間(實際時間) */

    u64         nr_migrations;      /* 此調度實體中進程移到其餘CPU組的數量 */

#ifdef CONFIG_SCHEDSTATS
    struct sched_statistics statistics; /* 用於統計一些數據 */
#endif

#ifdef CONFIG_FAIR_GROUP_SCHED
    struct sched_entity *parent;        /* 父親調度實體指針 */
    /* rq on which this entity is (to be) queued: */
    struct cfs_rq       *cfs_rq;        /* 實體所處紅黑樹運行隊列 */
    /* rq "owned" by this entity/group: */
    struct cfs_rq       *my_q;          /* 實體的紅黑樹運行隊列,若是NULL代表其是一個進程 */
#endif
};
struct sched_entity

調度器實體做爲一個se的成員變量,嵌入在struct task_struct內。

5.1.2 虛擬實時

vruntime存放進程運行的虛擬時間,該運行時間是進過了全部可運行進程總數標準化的。

定義在kernel/sched_fair.c中的update_curr()函數實現了該記帳功能。

static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    u64 now = rq_of(cfs_rq)->clock_task;
    unsigned long delta_exec;

    if (unlikely(!curr))
        return;

    /*
     * Get the amount of time the current task was running
     * since the last time we changed load (this cannot
     * overflow on 32 bits):
     */
    /* 計算當前執行時間,存放在變量delta_exec */
    delta_exec = (unsigned long)(now - curr->exec_start);
    if (!delta_exec)
        return;

    /* 傳遞給__update_curr */
    __update_curr(cfs_rq, curr, delta_exec);
    curr->exec_start = now;

    if (entity_is_task(curr)) {
        struct task_struct *curtask = task_of(curr);

        trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
        cpuacct_charge(curtask, delta_exec);
        account_group_exec_runtime(curtask, delta_exec);
    }
}


static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
          unsigned long delta_exec)
{
    unsigned long delta_exec_weighted;

    /* 根據當前可運行進程總數對運行時間進行加權計算 */
    schedstat_set(curr->statistics.exec_max,
              max((u64)delta_exec, curr->statistics.exec_max));

    curr->sum_exec_runtime += delta_exec;
    schedstat_add(cfs_rq, exec_clock, delta_exec);
    delta_exec_weighted = calc_delta_fair(delta_exec, curr);

    /* 權重值與當前運行進程的vruntime相加 */
    curr->vruntime += delta_exec_weighted;
    update_min_vruntime(cfs_rq);

#if defined CONFIG_SMP && defined CONFIG_FAIR_GROUP_SCHED
    cfs_rq->load_unacc_exec_time += delta_exec;
#endif
}
update_curr

update_curr()由系統定時器週期調用,不管進程處於可運行態,仍是阻塞處於不可運行態。

以此,vruntime能夠準確測量給定進程的運行時間。

 

5.2 進程選擇

當CFS須要選擇下一個運行進程時,它會挑一個具備最小vruntime的進程。

這就是CFS調度算法的核心:選擇具備最小vruntime的任務。

5.2.1 挑選下一個任務

CFS的進程選擇算法可簡單總結爲「運行rbtree樹中最左邊葉子節點所表明的那個進程。

實現這一過程的函數是__pick_next_entity(),定義在文件kernel/sched_fair.c中

static struct sched_entity *__pick_next_entity(struct sched_entity *se)
{
    struct rb_node *next = rb_next(&se->run_node);

    if (!next)
        return NULL;

    return rb_entry(next, struct sched_entity, run_node);
}
/* 函數本省不會遍歷找到最左葉子節點,該值已經被緩存在rb_leftmost字段中
 * 若是返回值是NULL,代表沒有最左葉子節點  */
__pick_next_entity

 

5.2.2 向書中加入進程

CFS將進程加入rbtree,緩存最左葉子節點。發生在進程變爲可運行狀態或者是經過fork()調用第一次建立進程時。

enqueue_entity()函數實現了這一目的,在文件kernel/sched_fair.c中:

static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    /*
     * 經過調用update_curr(),在更新min_vruntime以前先更新規範化的vruntime
     */
    if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
        se->vruntime += cfs_rq->min_vruntime;

    /*
     * 更新"當前任務"的運行時統計數據
     */
    update_curr(cfs_rq);
    update_cfs_load(cfs_rq, 0);
    account_entity_enqueue(cfs_rq, se);
    update_cfs_shares(cfs_rq);

    if (flags & ENQUEUE_WAKEUP) {
        place_entity(cfs_rq, se, 0);
        enqueue_sleeper(cfs_rq, se);
    }

    update_stats_enqueue(cfs_rq, se);
    check_spread(cfs_rq, se);
    if (se != cfs_rq->curr)
        __enqueue_entity(cfs_rq, se);
    se->on_rq = 1;

    if (cfs_rq->nr_running == 1)
        list_add_leaf_cfs_rq(cfs_rq);
}
enqueue_entity

該函數更新運行時間和其餘一些統計數據,把數據項真正插入到紅黑樹中:

/* 把一個調度實體插入紅黑樹中 */
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;
    struct rb_node *parent = NULL;
    struct sched_entity *entry;
    s64 key = entity_key(cfs_rq, se);
    int leftmost = 1;

    /* 在紅黑樹中查找合適的位置 */
    while (*link) {
        parent = *link;
        entry = rb_entry(parent, struct sched_entity, run_node);
        /* 咱們並不關心衝突,具備相同鍵值的結點呆在一塊兒 */
        if (key < entity_key(cfs_rq, entry)) {
            link = &parent->rb_left;
        } else {
            link = &parent->rb_right;
            leftmost = 0;           /* 右分支說明不是最小 */
        }
    }

    /* 維護一個緩存,其中存放樹最左葉子節點(也就是最常使用的) */
    if (leftmost)
        cfs_rq->rb_leftmost = &se->run_node;

    rb_link_node(&se->run_node, parent, link);      /* 插入新結點 */
    rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);    /* 着色 */
}
__enqueue_entity

 

5.2.3 從樹中刪除進程

CFS刪除動做發生在進程堵塞(變爲不可運行狀態)或者終止(結束運行)時。

static void
dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    /* 更新「當前任務」的運行時統計數據 */
    update_curr(cfs_rq);

    update_stats_dequeue(cfs_rq, se);
    if (flags & DEQUEUE_SLEEP) {
#ifdef CONFIG_SCHEDSTATS
        if (entity_is_task(se)) {
            struct task_struct *tsk = task_of(se);

            if (tsk->state & TASK_INTERRUPTIBLE)
                se->statistics.sleep_start = rq_of(cfs_rq)->clock;
            if (tsk->state & TASK_UNINTERRUPTIBLE)
                se->statistics.block_start = rq_of(cfs_rq)->clock;
        }
#endif
    }

    clear_buddies(cfs_rq, se);

    if (se != cfs_rq->curr)
        __dequeue_entity(cfs_rq, se);       /* 實際的刪除工做 */
    se->on_rq = 0;
    update_cfs_load(cfs_rq, 0);
    account_entity_dequeue(cfs_rq, se);

    /* 在更新min_vruntime以後對調度實體進行規範化,由於更新能夠指向"->curr"項
     * 咱們須要在規範化的位置反映這一變化 */
    if (!(flags & DEQUEUE_SLEEP))
        se->vruntime -= cfs_rq->min_vruntime;

    update_min_vruntime(cfs_rq);
    update_cfs_shares(cfs_rq);
}

static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    if (cfs_rq->rb_leftmost == &se->run_node) {
        struct rb_node *next_node;

        next_node = rb_next(&se->run_node);
        cfs_rq->rb_leftmost = next_node;
    }

    /* rb_erase能夠完成全部工做 */
    rb_erase(&se->run_node, &cfs_rq->tasks_timeline);
}
dequeue_entity

 

5.3 調度器入口

進程調度的入口函數是schedule(),在文件kernel/sched.c中。是內核用於調度進程調度器的入口:

選擇哪一個進程能夠運行,什麼時候將其投入運行。schedule能夠找到最高優先級的調度類,讓它找到下一個該運行的進程。

schedule()最重要 的事情是調用pick_next_task(),它會按優先級從高到低依次檢查後,調用優先級最高的進程:

/* 挑選最高優先級的任務 */
static inline struct task_struct *
pick_next_task(struct rq *rq)
{
    const struct sched_class *class;
    struct task_struct *p;

    /* 優化:咱們知道若是全部任務都在公平類中
     * 那麼咱們就能夠直接調用那個函數
     * 全部可運行進程數量等於CFS類對應的可運行進程數
     */
    if (likely(rq->nr_running == rq->cfs.nr_running)) {
        p = fair_sched_class.pick_next_task(rq);
        if (likely(p))
            return p;
    }

    /* 從最高優先級類開始,遍歷每一個調度類 */
    for_each_class(class) {
        p = class->pick_next_task(rq);  /* 返回下一個可運行進程的指針,沒有返回NULL */
        if (p)
            return p;
    }

    BUG(); /* the idle class will always have a runnable task */
}
pick_next_task

 

5.4 睡眠和喚醒

休眠(被阻塞)的進程處於一個特殊的不可執行狀態。

進程把本身標記成休眠狀態,從可執行紅黑樹中移出,放入等待隊列,而後調用schedule()選擇和執行一個其餘進程。

喚醒則恰好相反:進程被設置爲可執行狀態,而後再從等待隊列中移到可執行紅黑樹中。

5.4.1 等待隊列

休眠經過等待隊列處理。等待隊列是某些事件發生的進程組組成的簡單鏈表。

wake_queue_head_t 是等待隊列的結構,DECLARE_WAITQUEUE()靜態建立,init_waitqueue_head()動態建立。

內核中進行休眠的推薦操做相對複雜了一點:

/* 'q'是咱們但願休眠的等待隊列 */
DEFINE_WAIT(wait);

add_wait_queue(q, &wait);
while(!condition) {     /* 'condition' 是咱們在等待的事件 */
    prepare_to_wait(&q, &wait, TASK_INTERRUPTIBLE);
    if(signal_pending(current))
        /* 處理信號 */
    schedule();
}
finish_wait(&q, &wait);
/* 1)調用宏DEFINE_WAIT()建立一個等待隊列的項 
 * 2)調用add_wait_queue()把本身加入到隊列中。該隊列會在進程等待的天驕知足時喚醒它,
 * 固然咱們必須在其餘地方撰寫相關代碼,在事件發生時,對等待隊列執行wake_up()操做。
 * 3)調用prepare_to_wait()方法將進程的狀態變動爲TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。
 * 並且該函數若是有必要的話會將進程加回到等待隊列,這是在接下來的循環遍歷中所須要的
 * 4)若是狀態被設置爲TASK_INTERRUPTIBLE,則信號喚醒進程。這就是所謂的僞喚醒
 * (喚醒不是由於事件的發生),所以檢查並處理信號
 * 5)當進程被喚醒的時候,它會再次檢查條件是否爲真。若是是,它就退出循環,若是不是,它
 * 再次調用schedule()並一直重複這步操做。
 * 6)當條件知足後,進程將本身設置爲TASK_RUNNING並調用finish_wait()方法把本身移出等待隊列
 */
休眠隊列

函數inotify_read(),位於文件fs/notify/inotify/inotify_user.c中。

/* 等待隊列的一個典型用法 */
static ssize_t inotify_read(struct file *file, char __user *buf,
                size_t count, loff_t *pos)
{
    struct fsnotify_group *group;
    struct fsnotify_event *kevent;
    char __user *start;
    int ret;
    DEFINE_WAIT(wait);

    start = buf;
    group = file->private_data;

    while (1) {
        prepare_to_wait(&group->notification_waitq, &wait, TASK_INTERRUPTIBLE);

        mutex_lock(&group->notification_mutex);
        kevent = get_one_event(group, count);
        mutex_unlock(&group->notification_mutex);

        pr_debug("%s: group=%p kevent=%p\n", __func__, group, kevent);

        if (kevent) {
            ret = PTR_ERR(kevent);
            if (IS_ERR(kevent))
                break;
            ret = copy_event_to_user(group, kevent, buf);
            fsnotify_put_event(kevent);
            if (ret < 0)
                break;
            buf += ret;
            count -= ret;
            continue;
        }

        ret = -EAGAIN;
        if (file->f_flags & O_NONBLOCK)
            break;
        ret = -EINTR;
        if (signal_pending(current))
            break;

        if (start != buf)
            break;

        schedule();
    }

    finish_wait(&group->notification_waitq, &wait);
    if (start != buf && ret != -EFAULT)
        ret = buf - start;
    return ret;
}
inotify_read

 

5.4.2 喚醒

喚醒操做經過wake_up()進行,它會喚醒指定的等待隊列上的全部進程。

try_wake_up()函數負責將進程設置爲TASK_RUNNING狀態

enqueue_task()將進程放入紅黑樹中,若是被喚醒的進程優先級比當前的高,還要設置need_resched標誌。

 

6、搶佔和上下文切換

上下文切換:從一個可執行進程切換到另外一個可執行進程。在kernel/sched.c中的context_switch()函數負責處理。

每當新進程被選出來投入運行時,schedule()就會調用該函數,它完成兩個工做:

  • 調用switch_mm(),把虛擬內存從上一個進程映射給新進程。在<asm/mmu_context.h>中
  • 調用switch_to(),把上一個進程的處理器狀態切換到新進程中,在<asm/system.h>中

在進程調度中,need_resched標誌和schedule()函數是很是重要的組成。

 

6.1 用戶搶佔

用戶搶佔:內核返回用戶空間時,若是need_resched被置位,會致使schedule()被調用。

用戶搶佔產生的狀況:

  • 從系統調用返回用戶空間時
  • 從中斷處理程序返回用戶空間時

 

6.2 內核搶佔

爲了適應搶佔,每一個進程thread_info引入 preempt_count計數器。計數器初始爲0,每當使用鎖的時候數值加1,釋放減1。當數值爲0時,內核能夠搶佔。

內核搶佔會發生在:

  • 中斷處理程序正在執行,且返回內核空間以前。
  • 內核代碼再一次具備可搶佔性的時候。
  • 若是內核中的任務顯示地調用schedule()
  • 若是內核中的任務阻塞(一樣會調用schedule())

 

7、實時調度策略

Linux提供兩種調度策略:SCHED_FIFO和SCHED_RR。而普通的、非實時的調度使用SCHED_NORMAL策略。

SCHED_FIFO:實現了一種簡單的、先入先出的調度算法。

SCHED_RR:在耗盡事先分配給它的時間後就不能再繼續執行了。

 

8、與調度相關的系統調用

與調度相關的系統調用函數

nice()                            /* 設置進程nice值 */
sched_setscheduler()     /* 設置進程的調度策略 */
sched_getscheduler()     /* 獲取進程的調度策略 */
sched_setparam()          /* 設置進程的實時優先級 */
sched_getparam()          /* 獲取進程的實時優先級 */
sched_get_priority_max()    /* 獲取實時優先級的最小值 */
sched_get_priority_min()    /* 獲取實時優先級的最小值 */
sched_rr_get_interval()    /* 獲取進程的時間片值 */
sched_setaffinity()    /* 設置進程的處理器的親和力 */
sched_getaffinity()    /* 獲取進程的處理器的親和力 */
sched_yield()            /* 暫時讓出處理器 */
相關的系統調用

 

8.1 與調度策略和優先級相關的系統調用

sched_setscheduler()和sched_getscheduler()分別用於設置和獲取進程的調度策略和實時優先級。

最重要的工做在於讀取或改寫進程tast_struct的policy和rt_priority的值。

sched_get_priority_max()和sched_get_priority_min()用於返回給定調度策略的最大和最小優先級。

 

8.2 與處理器幫頂有關的系統調用

Linux調度程序提供強制的處理器綁定機制。

 

8.3 放棄處理器時間

經過sched_yield()系統調用,提供給了一種讓進程顯式地將處理器時間讓給其餘等待執行進程的機制。

相關文章
相關標籤/搜索