天天看點

linux排程器源碼分析 - 新程序加入(三)

  之前的文章已經介紹了排程器已經初始化完成,現在隻需要加入一個周期定時器tick驅動它進行周期排程即可,而加入定時器tick在下一篇文章進行簡單說明(主要這部分涉及排程器比較少,更多的是時鐘、定時器相關知識)。這篇文章主要說明系統如何把一個程序加入到隊列中。

  之前的文章也有提到過,隻有處于TASK_RUNNING狀态下的程序才能夠加入到排程器,其他狀态都不行,也就說明了,當一個程序處于睡眠、挂起狀态的時候是不存在于排程器中的,而程序加入排程器的時機如下:

當程序建立完成時,程序剛建立完成時,即使它運作起來立即調用sleep()程序睡眠,它也必定先會加入到排程器,因為實際上它加入排程器後自己還需要進行一定的初始化和操作,才會調用到我們的“立即”sleep()。

當程序被喚醒時,也使用sleep的例子說明,我們平常寫程式使用的sleep()函數實作原理就是通過系統調用将程序狀态改為TASK_INTERRUPTIBLE,然後移出運作隊列,并且啟動一個定時器,在定時器到期後喚醒程序,再重新放入運作隊列。

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

{

    unsigned long flags;

    /* 擷取目前CPU,并且禁止搶占 */

    int cpu = get_cpu();

    /* 初始化跟排程相關的值,比如排程實體,運作時間等 */

    __sched_fork(clone_flags, p);

    /*

     * 标記為運作狀态,表明此程序正在運作或準備好運作,實際上沒有真正在CPU上運作,這裡隻是導緻了外部信号和事件不能夠喚醒此程序,之後将它插入到運作隊列中

     */

    p->state = TASK_RUNNING;

     * 根據父程序的運作優先級設定設定程序的優先級

    p->prio = current->normal_prio;

     * 更新該程序優先級

    /* 如果需要重新設定優先級 */

    if (unlikely(p->sched_reset_on_fork)) {

        /* 如果是dl排程或者實時排程 */

        if (task_has_dl_policy(p) || task_has_rt_policy(p)) {

            /* 排程政策為SCHED_NORMAL,這個選項将使用CFS排程 */

            p->policy = SCHED_NORMAL;

            /* 根據預設nice值設定靜态優先級 */

            p->static_prio = NICE_TO_PRIO(0);

            /* 實時優先級為0 */

            p->rt_priority = 0;

        } else if (PRIO_TO_NICE(p->static_prio) 0)

        /* p->prio = p->normal_prio = p->static_prio */

        p->prio = p->normal_prio = __normal_prio(p);

        /* 設定程序權重 */

        set_load_weight(p);

         /* sched_reset_on_fork成員在之後已經不需要使用了,直接設為0 */

        p->sched_reset_on_fork = 0;

    }

    if (dl_prio(p->prio)) {

        /* 使能搶占 */

        put_cpu();

        /* 傳回錯誤 */

        return -EAGAIN;

    } else if (rt_prio(p->prio)) {

        /* 根據優先級判斷,如果是實時程序,設定其排程類為rt_sched_class */

        p->sched_class = &rt_sched_class;

    } else {

        /* 如果是普通程序,設定其排程類為fair_sched_class */

        p->sched_class = &fair_sched_class;

    /* 調用調用類的task_fork函數 */

    if (p->sched_class->task_fork)

        p->sched_class->task_fork(p);

     * The child is not yet in the pid-hash so no cgroup attach races,

     * and the cgroup is pinned to this child due to cgroup_fork()

     * is ran before sched_fork().

     *

     * Silence PROVE_RCU.

    raw_spin_lock_irqsave(&p->pi_lock, flags);

    /* 設定新程序的CPU為目前CPU */

    set_task_cpu(p, cpu);

    raw_spin_unlock_irqrestore(&p->pi_lock, flags);

#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)

    if (likely(sched_info_on()))

        memset(&p->sched_info, 0, sizeof(p->sched_info));

#endif

#if defined(CONFIG_SMP)

    p->on_cpu = 0;

    /* task_thread_info(p)->preempt_count = PREEMPT_DISABLED; */

    /* 初始化該程序為核心禁止搶占 */

    init_task_preempt_count(p);

#ifdef CONFIG_SMP

    plist_node_init(&p->pushable_tasks, MAX_PRIO);

    RB_CLEAR_NODE(&p->pushable_dl_tasks);

    /* 使能搶占 */

    put_cpu();

    return 0;

}

   在sched_fork()函數中,主要工作如下:

擷取目前CPU号

禁止核心搶占(這裡基本就是關閉了搶占,因為執行到這裡已經是核心态,又禁止了被搶占)

初始化程序p的一些變量(實時程序和普通程序通用的那些變量)

設定程序p的狀态為TASK_RUNNING(這一步很關鍵,因為隻有處于TASK_RUNNING狀态下的程序才會被排程器放入隊列中)

根據父程序和clone_flags參數設定程序p的優先級和權重。

根據程序p的優先級設定其排程類(實時程序優先級:0~99  普通程序優先級:100~139)

根據排程類進行程序p類型相關的初始化(這裡就實作了實時程序和普通程序獨有的變量進行初始化)

設定程序p的目前CPU為此CPU。

初始化程序p禁止核心搶占(因為當CPU執行到程序p時,程序p還需要進行一些初始化)

使能核心搶占

  可以看出sched_fork()進行的初始化也比較簡單,需要注意的是不同類型的程序會使用不同的排程類,并且也會調用排程類中的初始化函數。在實時程序的排程類中是沒有特定的task_fork()函數的,而普通程序使用cfs政策時會調用到task_fork_fair()函數,我們具體看看實作:

static void task_fork_fair(struct task_struct *p)

    struct cfs_rq *cfs_rq;

    /* 程序p的排程實體se */

    struct sched_entity *se = &p->se, *curr;

    /* 擷取目前CPU */

    int this_cpu = smp_processor_id();

    /* 擷取此CPU的運作隊列 */

    struct rq *rq = this_rq();

    /* 上鎖并儲存中斷記錄 */

    raw_spin_lock_irqsave(&rq->lock, flags);

    /* 更新rq運作時間 */

    update_rq_clock(rq);

    /* cfs_rq = current->se.cfs_rq; */

    cfs_rq = task_cfs_rq(current);

    /* 設定目前程序所在隊列為父程序所在隊列 */

    curr = cfs_rq->curr;

     * Not only the cpu but also the task_group of the parent might have

     * been changed after parent->se.parent,cfs_rq were copied to

     * child->se.parent,cfs_rq. So call __set_task_cpu() to make those

     * of child point to valid ones.

    rcu_read_lock();

    /* 設定此程序所屬CPU */

    __set_task_cpu(p, this_cpu);

    rcu_read_unlock();

    /* 更新目前程序運作時間 */

    update_curr(cfs_rq);

    if (curr)

        /* 将父程序的虛拟運作時間賦給了新程序的虛拟運作時間 */

        se->vruntime = curr->vruntime;

    /* 調整了se的虛拟運作時間 */

    place_entity(cfs_rq, se, 1);

    if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {

        /*

         * Upon rescheduling, sched_class::put_prev_task() will place

         * 'current' within the tree based on its new key value.

         */

        swap(curr->vruntime, se->vruntime);

        resched_curr(rq);

    /* 保證了程序p的vruntime是運作隊列中最小的(這裡占時不确定是不是這個用法,不過确實是最小的了) */

    se->vruntime -= cfs_rq->min_vruntime;

    /* 解鎖,還原中斷記錄 */

    raw_spin_unlock_irqrestore(&rq->lock, flags);

  在task_fork_fair()函數中主要就是設定程序p的虛拟運作時間和所處的cfs隊列,值得我們注意的是 cfs_rq = task_cfs_rq(current); 這一行,在注釋中已經表明task_cfs_rq(current)傳回的是current的se.cfs_rq,注意se.cfs_rq儲存的并不是根cfs隊列,而是所處的cfs_rq,也就是如果父程序處于一個程序組的cfs_rq中,新建立的程序也會處于這個程序組的cfs_rq中。

  到這裡新程序關于排程的初始化已經完成,但是還沒有被排程器加入到隊列中,其是在do_fork()中的wake_up_new_task(p);中加入到隊列中的,我們具體看看wake_up_new_task()的實作:

void wake_up_new_task(struct task_struct *p)

    struct rq *rq;

     * Fork balancing, do it here and not earlier because:

     * - cpus_allowed can change in the fork path

     * - any previously selected cpu might disappear through hotplug

     /* 為程序選擇一個合适的CPU */

    set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));

    /* Initialize new task's runnable average */

    /* 這裡是跟多核負載均衡有關 */

    init_task_runnable_average(p);

    /* 上鎖 */

    rq = __task_rq_lock(p);

    /* 将程序加入到CPU的運作隊列 */

    activate_task(rq, p, 0);

    /* 标記程序p處于隊列中 */

    p->on_rq = TASK_ON_RQ_QUEUED;

    /* 跟調試有關 */

    trace_sched_wakeup_new(p, true);

    /* 檢查是否需要切換目前程序 */

    check_preempt_curr(rq, p, WF_FORK);

    if (p->sched_class->task_woken)

        p->sched_class->task_woken(rq, p);

    task_rq_unlock(rq, p, &flags);

  在wake_up_new_task()函數中,将程序加入到運作隊列的函數為activate_task(),而activate_task()函數最後會調用到新程序排程類中的enqueue_task指針所指函數,這裡我們具體看一下cfs排程類的enqueue_task指針所指函數enqueue_task_fair():

static void

enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)

    struct sched_entity *se = &p->se;

    /* 這裡是一個疊代,我們知道,程序有可能是處于一個程序組中的,是以當這個處于程序組中的程序加入到該程序組的隊列中時,要對此隊列向上疊代 */

    for_each_sched_entity(se) {

        if (se->on_rq)

            break;

        /* 如果不是CONFIG_FAIR_GROUP_SCHED,擷取其所在CPU的rq運作隊列的cfs_rq運作隊列

         * 如果是CONFIG_FAIR_GROUP_SCHED,擷取其所在的cfs_rq運作隊列

        cfs_rq = cfs_rq_of(se);

        /* 加入到隊列中 */

        enqueue_entity(cfs_rq, se, flags);

         * end evaluation on encountering a throttled cfs_rq

         *

         * note: in the case of encountering a throttled cfs_rq we will

         * post the final h_nr_running increment below.

        */

        if (cfs_rq_throttled(cfs_rq))

        cfs_rq->h_nr_running++;

        flags = ENQUEUE_WAKEUP;

    /* 隻有se不處于隊列中或者cfs_rq_throttled(cfs_rq)傳回真才會運作這個循環 */

        update_cfs_shares(cfs_rq);

        update_entity_load_avg(se, 1);

    if (!se) {

        update_rq_runnable_avg(rq, rq->nr_running);

        /* 目前CPU運作隊列活動程序數 + 1 */

        add_nr_running(rq, 1);

    /* 設定下次排程中斷發生時間 */

    hrtick_update(rq);

  在enqueue_task_fair()函數中又使用了enqueue_entity()函數進行操作,如下:

enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)

     * Update the normalized vruntime before updating min_vruntime

     * through calling update_curr().

    if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))

        se->vruntime += cfs_rq->min_vruntime;

     * Update run-time statistics of the 'current'.

    /* 更新目前程序運作時間和虛拟運作時間 */

    enqueue_entity_load_avg(cfs_rq, se, flags & ENQUEUE_WAKEUP);

    /* 更新cfs_rq隊列總權重(就是在原有基礎上加上se的權重) */

    account_entity_enqueue(cfs_rq, se);

    update_cfs_shares(cfs_rq);

    /* 建立的程序flags為0,不會執行這裡 */

    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);

    /* 将se插入到運作隊列cfs_rq的紅黑樹中 */

    if (se != cfs_rq->curr)

        __enqueue_entity(cfs_rq, se);

    /* 将se的on_rq标記為1 */

    se->on_rq = 1;

    /* 如果cfs_rq的隊列中隻有一個程序,這裡做處理 */

    if (cfs_rq->nr_running == 1) {

        list_add_leaf_cfs_rq(cfs_rq);

        check_enqueue_throttle(cfs_rq);

  需要注意的幾點:

新建立的程序先會進行排程相關的結構體和變量初始化,其中會根據不同的類型進行不同的排程類操作,此時并沒有加入到隊列中。

當新程序建立完畢後,它的父程序會将其運作狀态置為TASK_RUNNING,并加入到運作隊列中。

加入運作隊列時系統會根據CPU的負載情況放入不同的CPU隊列中。

繼續閱讀