• 调度器39—idle调度类 Hello


    一、简介

    idle调度类实现文件是idle.c,最低优先级。每个cpu绑定一个idle线程到 iq->idle 上。

    二、相关结构

    1. sched_class idle_sched_class

    const struct sched_class idle_sched_class     __section("__idle_sched_class") = {
        /* no enqueue/yield_task for idle tasks */
    
        /*
         * dequeue is not valid, we print a debug message there: 
         * 不应dequeue idle线程,若是调用dequeue了,直接dump_stack()
         */
        .dequeue_task    = dequeue_task_idle,
    
        /* 只是执行 resched_curr(rq); */
        .check_preempt_curr    = check_preempt_curr_idle,
    
        /* 直接返回 rq->idle */
        .pick_next_task    = pick_next_task_idle,
        /* 空函数 */
        .put_prev_task    = put_prev_task_idle,
        /* 只是增加 rq->sched_goidle 计数 */
        .set_next_task          = set_next_task_idle,
    
    #ifdef CONFIG_SMP
        /* 只是 WARN_ON_ONCE(1) */
        .balance    = balance_idle,
        /* 选核,直接返回task_cpu(p),IDLE tasks as never migrated */
        .select_task_rq        = select_task_rq_idle,
        /* 设置idle线程的p->cpus_mask竟然是支持的 */
        .set_cpus_allowed    = set_cpus_allowed_common,
    #endif
        /* 空函数 */
        .task_tick        = task_tick_idle,
    
        /* 直接是执行 BUG(); */
        .prio_changed        = prio_changed_idle,
        /* 直接是执行 BUG(); 执行路径只有 rt_mutex_setprio/__sched_setscheduler */
        .switched_to        = switched_to_idle,
        /* 空函数 */
        .update_curr        = update_curr_idle,
    };

    2. idle_threads 

    static DEFINE_PER_CPU(struct task_struct *, idle_threads); //smpboot.c

    里面保存了各个cpu上的idle任务,静态的,且相关函数没有EXPORT_SYMBOL导出,相当于一个模块内部的变量。

    三、实现概述

    1. rq->idle 任务初始化

    在 init_idle() 中进行初始化

    /**
     * init_idle - set up an idle thread for a given CPU
     * @idle: task in question
     * @cpu: CPU the idle task belongs to
     *
     * 注意:此函数不会设置空闲线程的 NEED_RESCHED 标志,以使启动更加稳健。
     */
    void __init init_idle(struct task_struct *idle, int cpu)
    {
        struct rq *rq = cpu_rq(cpu);
        unsigned long flags;
    
        __sched_fork(0, idle);
    
        raw_spin_lock_irqsave(&idle->pi_lock, flags);
        raw_spin_lock(&rq->lock);
    
        idle->state = TASK_RUNNING;
        idle->se.exec_start = sched_clock();
        
        /* idle线程特有标识 */
        idle->flags |= PF_IDLE;
        /* 设置此idle线程只能运行在这一个cpu上 */
        set_cpus_allowed_common(idle, cpumask_of(cpu));
        __set_task_cpu(idle, cpu);
    
        /* 将参数idle task设置为rq的idle线程 */
        rq->idle = idle;
        rcu_assign_pointer(rq->curr, idle);
        idle->on_rq = TASK_ON_RQ_QUEUED;
        idle->on_cpu = 1;
    
        /* 初始化为不可被抢占 */
        init_idle_preempt_count(idle, cpu);
    
        /* idle调度类 */
        idle->sched_class = &idle_sched_class;
    
        /* idle线程的名字是固定的,都是"swapper/X",但是"ps -AT | grep swapper"是不会显示的 */
        sprintf(idle->comm, "%s/%d", INIT_TASK_COMM, cpu);
    }

    init_idle()的调用路径:

    start_kernel //init/main.c
        sched_init //sched/core.c
            init_idle(current, smp_processor_id()); //sched/core.c 只有boot cpu执行,此时PID还是0
            idle_thread_set_boot_cpu(); //per_cpu(idle_threads, smp_processor_id()) = current;
        arch_call_rest_init    //init/main.c 在 sched_init 之后,函数末尾位置才调用
            rest_init //init/main.c kernel_thread(kernel_init, NULL, CLONE_FS) 首先生成 init 以便它获得 pid 1
                kernel_init //init/main.c 此时PID就是1了
                    kernel_init_freeable //init/main.c
                        smp_init //kernel/smp.c
                            idle_threads_init //smpboot.c for_each_possible_cpu(cpu) if (cpu != boot_cpu) 对于非boot的每个CPU都执行
                                idle_init //smpboot.c
                                    fork_idle //fork.c
                                        init_idle(task, cpu); //fork.c
                                    per_cpu(idle_threads, cpu) = tsk; //将fork出来的idle线程赋值给per-cpu的idle_threads指针

    可见,idle线程的名字为"swapper/X",只绑定了一个CPU,相当于是per-cpu的idle线程。idle线程用 PF_IDLE 进行标识,由rq->idle成员直接指向。

    2. idle线程运行内容

    (1) boot cpu 执行初始化流程后

    /*
    start_kernel
        arch_call_rest_init
            rest_init
                cpu_startup_entry(CPUHP_ONLINE);
    */
    void cpu_startup_entry(enum cpuhp_state state)
    {
        arch_cpu_idle_prepare(); //空函数
        cpuhp_online_idle(state);
        /* 进入idle状态 */
        while (1)
            do_idle();
    }

    do_idle的实现:

    static void do_idle(void)
    {
        int cpu = smp_processor_id();
    
        tick_nohz_idle_enter();
    
        /* 若不需要调度,就在这死循环 */
        while (!need_resched()) {
            rmb();
    
            local_irq_disable();
    
            if (cpu_is_offline(cpu)) {
                tick_nohz_idle_stop_tick();
                cpuhp_report_idle_dead();
                arch_cpu_idle_dead();
            }
    
            /* 这里有执行wakeup操作,有 trace_rcu_nocb_wake 可以跟踪 */
            rcu_nocb_flush_deferred_wakeup();
    
            /* 判断是否要进入更深层的休眠 */
            if (cpu_idle_force_poll || tick_check_broadcast_expired()) {
                tick_nohz_idle_restart_tick();
                /* 若是从这里退出有 trace_cpu_idle 可以跟踪 */
                cpu_idle_poll();
            } else {
                /* 进入更深层的idle */
                cpuidle_idle_call();
            }
            arch_cpu_idle_exit();
        }
    
        preempt_set_need_resched();
        tick_nohz_idle_exit();
        __current_clr_polling();
    
        smp_mb__after_atomic();
    
        /* 会处理其它ipi中断发过来的请求和触发软中断 */
        flush_smp_call_function_from_idle();
        /* 调用 __schedule(false) 触发任务切换 */
        schedule_idle();
    }

    pid=0的内核boot线程最终归宿是idle线程,也就是boot cpu的idle线程的pid=0。

    (2) non-boot cpu

    是在 pid=1 的 kernel_init() 的执行路径中通过 fork_idle(int cpu) 依次为每一个非boot cpu创建的

    struct task_struct * __init fork_idle(int cpu)
    {
        struct task_struct *task;
        struct kernel_clone_args args = {
            .flags = CLONE_VM,
        };
    
        task = copy_process(&init_struct_pid, 0, cpu_to_node(cpu), &args);
        if (!IS_ERR(task)) {
            init_idle_pids(task);
            init_idle(task, cpu);
        }
    
        return task;
    }

    copy_process 函数参数1传参是全局的 struct pid init_struct_pid,其 .numbers.nr 是初始化为0的。在 copy_process 中对传参 arg1=init_struct_pid 的新创建的任务的 pid 赋值为 p->pid = pid_nr(pid) 也即是 numbers.nr,也就是0。也就是说非boot cpu的idle线程的pid也是0。

    TODO: fork_idle 得到的idle线程执行的代码是什么呢? 是如何执行到的?

    3. 选 idle 线程时机

    static inline struct task_struct *pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
    {
        const struct sched_class *class;
        struct task_struct *p;
    
        /* 针对CFS的优化选核 */
        if (likely(prev->sched_class <= &fair_sched_class && rq->nr_running == rq->cfs.h_nr_running)) {
            p = pick_next_task_fair(rq, prev, rf);
            if (unlikely(p == RETRY_TASK))
                goto restart;
    
            /* Assumes fair_sched_class->next == idle_sched_class */
            if (!p) {
                put_prev_task(rq, prev);
                /* 选不到任务就让idle任务运行 */
                p = pick_next_task_idle(rq);
            }
    
            return p;
        }
    
    restart:
        /* 这里面有 put_prev_task,虽然CFS选核中有put,但是上面没有执行 */
        put_prev_task_balance(rq, prev, rf);
    
        for_each_class(class) {
            /* 这里按调度类优先级从高到底遍历,若选不到高优先级任务,就选中最低优先级的idle线程来运行 */
            p = class->pick_next_task(rq);
            if (p)
                return p;
        }
    
        /* The idle class should always have a runnable task: */
        BUG();
    }

    调度类是通过链接脚本的方式排列在一起,低优先级调度类低地址,高优先级调度类高地址。for_each_class 进行遍历的时候,先遍历高地址,再遍历低地址。

    //include/asm-generic/vmlinux.lds.h
    #define SCHED_DATA                \
    STRUCT_ALIGN();                 \
    __begin_sched_classes = .;        \
    *(__idle_sched_class)            \
    *(__fair_sched_class)            \
    *(__rt_sched_class)             \
    *(__dl_sched_class)             \
    *(__stop_sched_class)            \
    __end_sched_classes = .;
    
    
    /* defined in include/asm-generic/vmlinux.lds.h */
    extern struct sched_class __begin_sched_classes[];
    extern struct sched_class __end_sched_classes[];
    
    #define sched_class_highest (__end_sched_classes - 1)
    #define sched_class_lowest  (__begin_sched_classes - 1)
    
    #define for_class_range(class, _from, _to) \
        for (class = (_from); class != (_to); class--)
    
    #define for_each_class(class) \
        for_class_range(class, sched_class_highest, sched_class_lowest)

    四、相关实验

    1. idle线程唤醒任务

    let i=0; while true; do if [ i -lt 10 ]; then let i=i+1; else let i=0; sleep 0.1; fi; done &
    [1] 8241

    使用这个脚本验证,运行后pid=8241,从trace上看,sh[8241] 一直是被 sleep [XX] 唤醒,而 sleep [XX] 是被idle线程 swapper [0] 唤醒。sleep线程写为pid=XX是为了表示 sleep 线程其PID一直在变,看起来是每次都是新建sleep进程后进行唤醒的。

    2. 内核中执行 msleep() 函数后的唤醒,trace上看,有时是被idle线程唤醒的。用户空间执行usleep()休眠的线程,从trace上看也有一部分是被idle线程唤醒的,而且在唤醒CPU上此时并没有中断或软中断!

    五、经验总结

    1. 在 init_idle() 函数中将 per-cpu 的 idle 线程的名字固定为"swapper/X"。用 PF_IDLE 标识idle线程。per-cpu的idle线程由各cpu的rq->idle成员直接指向。所有cpu上的idle线程的pid都等于0,且使用"ps -AT"查看不到idle线程。

    2. 内核初始化线程最终会成为boot cpu上的idle线程,并且在idle线程中会执行一些唤醒操作。

    3. trace上idle线程显示为"<idle>-0".

  • 相关阅读:
    set, unordered_set模板类
    C/C++ Bug记录
    win10远程连接
    C/C++缓冲区刷新问题
    hihocoder1711 评论框排版[并查集+set]
    makefile
    Virtual Table
    粤语
    xilinx SDK开发 GPIO使用API总结
    基于zynq 7020的串口UART中断实验
  • 原文地址:https://www.cnblogs.com/hellokitty2/p/16703115.html
Copyright © 2020-2023  润新知