linux 进程的管理和调度 --- __schedule() 函数分析

发布时间 2023-11-05 20:51:32作者: 流水灯

运行队列

Linux采用的是每个CPU都有自己的运行队列,这样做的好处:
(1)每个CPU在自己的运行队列上选择任务降低了竞争;
(2)某个任务位于一个CPU的运行队列上,经过多次调度后,内核趋于选择相同的CPU执行该任务,那么上次任务运行的变量很可能仍然在这个CPU缓存上,提高运行效率。

 __schedule()

 入参

#define SM_NONE            0x0  // 主动调度
#define SM_PREEMPT        0x1
#define SM_RTLOCK_WAIT        0x2

 函数分析

static void __sched notrace __schedule(unsigned int sched_mode)
{
    struct task_struct *prev, *next;
    unsigned long *switch_count;
    unsigned long prev_state;
    struct rq_flags rf;
    struct rq *rq;
    int cpu;

    cpu = smp_processor_id();
    rq = cpu_rq(cpu); // 找到当前CPU的运行队列
    prev = rq->curr; // 记录要切换出去的任务

    schedule_debug(prev, !!sched_mode);

    if (sched_feat(HRTICK) || sched_feat(HRTICK_DL))
        hrtick_clear(rq);

    local_irq_disable();
    rcu_note_context_switch(!!sched_mode);

    rq_lock(rq, &rf);
    smp_mb__after_spinlock();

    /* Promote REQ to ACT */
    rq->clock_update_flags <<= 1;
    update_rq_clock(rq);

    switch_count = &prev->nivcsw; // Context involuntary switch counter,此任务上下文非主动切换计数

    prev_state = READ_ONCE(prev->__state);
    if (!(sched_mode & SM_MASK_PREEMPT) && prev_state) { // 主动调度,并且要切换出去的任务不是运行态(非运行态prev_state是非零,通常主调度之前会提前设置当前进程的
                                  // 运行状态为 TASK_INTERRUPTIBLE 或者 TASK_UNINTERRUPTIBLE)
if (signal_pending_state(prev_state, prev)) { WRITE_ONCE(prev->__state, TASK_RUNNING); } else { prev->sched_contributes_to_load = (prev_state & TASK_UNINTERRUPTIBLE) && !(prev_state & TASK_NOLOAD) && !(prev->flags & PF_FROZEN); if (prev->sched_contributes_to_load) rq->nr_uninterruptible++; deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK); // 设置 prev->on_rq 为0,表示此任务不在运行队列里;并把此任务从运行队列移除 if (prev->in_iowait) { atomic_inc(&rq->nr_iowait); delayacct_blkio_start(); } } switch_count = &prev->nvcsw; // Context voluntary switch counter,此任务上下文主动切换计数 } // 抢占调度,要切换的任务还是在运行队列 next = pick_next_task(rq, prev, &rf); // 根据prev任务的调度类选一个最高优先级的任务返回 clear_tsk_need_resched(prev); // 清楚prev任务的 TIF_NEED_RESCHED 的标记 clear_preempt_need_resched(); #ifdef CONFIG_SCHED_DEBUG rq->last_seen_need_resched_ns = 0; #endif if (likely(prev != next)) { rq->nr_switches++; // 队列内的任务切换次数统计 RCU_INIT_POINTER(rq->curr, next); ++*switch_count; // 任务上下文切换统计 migrate_disable_switch(rq, prev); psi_sched_switch(prev, next, !task_on_rq_queued(prev)); trace_sched_switch(sched_mode & SM_MASK_PREEMPT, prev, next); /* Also unlocks the rq: */ rq = context_switch(rq, prev, next, &rf); } else { rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP); rq_unpin_lock(rq, &rf); __balance_callbacks(rq); raw_spin_rq_unlock_irq(rq); } }

上下文切换函数 context_switch,切换内存描述符和MMU的页表首地址、CPU寄存器。

用户线程有用户态和内存态,内核线程只有内核态。

task_struct 有两个成员,分别是 mm 和 active_mm,对于 用户线程 来说 , active_mm 字段 与 mm 字段 指向同一个 " 内存描述符 " ;但对于 " 内核线程 " 来说 , mm 字段 指向 空指针 , active_mm 字段 指向同一个CPU最近运行的用户线程的 " 内存描述符 " ;

/*
 * context_switch - switch to the new MM and the new thread's register state.
 */
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next, struct rq_flags *rf)
{
    prepare_task_switch(rq, prev, next);

    /*
     * For paravirt, this is coupled with an exit in switch_to to
     * combine the page table reload and the switch backend into
     * one hypercall.
     */
    arch_start_context_switch(prev);

    /*
     * kernel -> kernel   lazy + transfer active
     *   user -> kernel   lazy + mmgrab() active
     *
     * kernel ->   user   switch + mmdrop() active
     *   user ->   user   switch
     */
    if (!next->mm) {                                // 切换到内核线程
        enter_lazy_tlb(prev->active_mm, next);

        next->active_mm = prev->active_mm; // 指向要被切换出去线程的内存描述符
        if (prev->mm)                           // 被切换出去线程属于用户线程
            mmgrab(prev->active_mm); // 由于被切换出去线程的内存描述符被要运行的内核线程使用,内存描述符的引用计数加一
        else
            prev->active_mm = NULL; // 被切换出去线程属于内核线程,其内存描述符指向NULL
    } else {                                        // 切换到用户线程
        membarrier_switch_mm(rq, prev->active_mm, next->mm);
        /*
         * sys_membarrier() requires an smp_mb() between setting
         * rq->curr / membarrier_switch_mm() and returning to userspace.
         *
         * The below provides this either through switch_mm(), or in
         * case 'prev->active_mm == next->mm' through
         * finish_task_switch()'s mmdrop().
         */
        switch_mm_irqs_off(prev->active_mm, next->mm, next); // 设置 MMU 的页表首地址为准备运行的用户线程的页表首地址

        if (!prev->mm) {                        // from kernel
            /* will mmdrop() in finish_task_switch(). */
            rq->prev_mm = prev->active_mm;
            prev->active_mm = NULL;
        }
    }

    rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);

    prepare_lock_switch(rq, next, rf);

    /* Here we just switch the register state and the stack. */
    switch_to(prev, next, prev); // 切换CPU寄存器
    barrier();

    return finish_task_switch(prev);
}