Linux内核设计与实现——读书笔记(2)进程调度

1、Linux调度算法

  2.6.23内核版本开始采用以“公平调度” 为概念引入的调度程序——“完全公平调度算法”,简称CFS

2、I/O消耗型和处理器消耗型的进程

  I/O消耗型进程 大多用于交互,类似鼠标、键盘、用户界面应用程序之类的。
  处理器消耗型 进程会把大多时间用于执行程序上。
  还有一些程序可以同时处于I/O消耗型和处理器消耗型 ,例如sshkeygen和MATLAB。
  调度策略通常要在两种矛盾的目标中间寻找平衡,Linux为了保证交互式应用和桌面系统的性能,会更倾向于优先调度I/O消耗型进程。

3、进程优先级

  Linux采用了两种不同的优先级范围。一种是nice值,它的范围是从-20到19,默认值为0。数值越大,优先级越低。使用命令 “ps -el” ,在NL项可以查看进程的nice值。
  第二种范围是实时优先级,可配置,默认情况下的变化范围从0~99,越高的实时优先级数值意味着进程的优先级更高。使用 “ps -eo state,uid,pid,ppid,rtprio,time,comm” ,RTPRIO项可以查看进程的实时优先级值,如果有进程显示 - ,则表示该进程不是实时进程。

4、时间片

  时间片表明进程在被抢占前所能持续运行的时间。但是Linux的CFS调度器并没有直接分配时间片到进程,它是将处理器的使用比 划分给了进程。这样一来,进程所获得的处理器时间其实是和系统负载密切相关的。这个比例还会受进程nice值的影响,nice值将作为权重调整进程所使用的处理器时间使用比。具有更高nice的进程将被赋予低权重,从而丧失一小部分的处理器使用比;而具有更低nice值的进程则会被赋予高权重,从而抢得更多的处理器使用比。
  Linux系统是抢占式的,新的CFS调度器,其抢占时机取决于新的可运行程序消耗了多少处理器使用比。如果小号的使用比比当前进程小,则新进程立刻投入运行,抢占当前进程。否则,推迟运行。

5、调度器类

  Linux调度器是以模块方式提供的,这种模块化结构被称为调度器类(scheduler classes),允许多种不同的可动态添加的调度算法并存。每个调度器都有一个优先级,基础的调度器代码定义在kernle/sched.c文件中,它会按照优先级顺序遍历调度类,拥有一个可执行进程的最高优先级的调度器类胜出,然后选择需要执行的那一个程序。
  完全公平调度(CFS)是一个针对普通进程的调度类,Linux中称为SCHED_NORMAL ,CFS算法定义在kernel/sched_fair.c 中。

6、Linux调度的实现

  CFS调度算法的相关代码位于kernel/sched_fair.c 中。特别关注其四个组成部分:

  • 时间记账
  • 进程选择
  • 调度器入口
  • 睡眠和唤醒

6.1、时间记账

  所有调度器都必须对进程运行时间做记账。

6.1.1调度器实体结构

  CFS不再有时间片的概念,但是它必须维护每个进程运行的时间记账,因为需要确保每个进程只在公平分配给它的处理器时间内运行。
  CFS使用调度器实体结构(定义在文件<linux.sched.h>的struct_sched_entity中)来追踪进程运行记账:

struct sched_entity {
    struct load_weight  load;       /* for load-balancing */
    struct rb_node      run_node;
    unsigned int        on_rq;

    u64         exec_start;
    u64         sum_exec_runtime;
    u64         vruntime;
    u64         prev_sum_exec_runtime;

#ifdef CONFIG_SCHEDSTATS
    u64         wait_start;
    u64         wait_max;

    u64         sleep_start;
    u64         sleep_max;
    s64         sum_sleep_runtime;

    u64         block_start;
    u64         block_max;
    u64         exec_max;
    u64         slice_max;

    u64         nr_migrations;
    u64         nr_migrations_cold;
    u64         nr_failed_migrations_affine;
    u64         nr_failed_migrations_running;
    u64         nr_failed_migrations_hot;
    u64         nr_forced_migrations;
    u64         nr_forced2_migrations;

    u64         nr_wakeups;
    u64         nr_wakeups_sync;
    u64         nr_wakeups_migrate;
    u64         nr_wakeups_local;
    u64         nr_wakeups_remote;
    u64         nr_wakeups_affine;
    u64         nr_wakeups_affine_attempts;
    u64         nr_wakeups_passive;
    u64         nr_wakeups_idle;
#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;
#endif
};

  调度器实体结构是一个名为se的成员变量,嵌入在进程描述符task_struct 内。
在这里插入图片描述

6.1.2、虚拟实时

  vruntime变量存放进程的虚拟运行时间,该运行时间(花在运行上的时间和)的计算是进过了所有可运行进程总数的标准化(或者说是被加权的)。虚拟时间是以ns为单位的,所以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;
    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 = (unsigned long)(now - curr->exec_start);

    __update_curr(cfs_rq, curr, delta_exec);
    curr->exec_start = now; 

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

        cpuacct_charge(curtask, delta_exec);
 

  update_curr() 中计算了当前进程的执行时间,并且将其存放在变量delta_exec中。然后又将执行时间传递给了 __update_curr() ,后者再根据当前可运行进程中总数对运行时间进行加权计算。最终将上述的权重值与当前运行进程的vruntime相加。
  __update_curr() 函数如下所示:

/*
 * Update the current task's runtime statistics. Skip current tasks that
 * are not in our scheduling class.
 */
static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
          unsigned long delta_exec)
{
    unsigned long delta_exec_weighted;
    u64 vruntime;

    schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));

    curr->sum_exec_runtime += delta_exec;
    schedstat_add(cfs_rq, exec_clock, delta_exec);
    delta_exec_weighted = delta_exec;
    if (unlikely(curr->load.weight != NICE_0_LOAD)) {
        delta_exec_weighted = calc_delta_fair(delta_exec_weighted,
                            &curr->load);
    }
    curr->vruntime += delta_exec_weighted;

    /*
     * maintain cfs_rq->min_vruntime to be a monotonic increasing
     * value tracking the leftmost vruntime in the tree.
     */
    if (first_fair(cfs_rq)) {
        vruntime = min_vruntime(curr->vruntime,
                __pick_next_entity(cfs_rq)->vruntime);
    } else
        vruntime = curr->vruntime;

    cfs_rq->min_vruntime =
        max_vruntime(cfs_rq->min_vruntime, vruntime);
}

  update_curr()是由系统定时器周期性调用的,无论是在进程处于可运行态,还是被阻塞处于不可运行态。vruntime可以准确地测量给定进程的运行时间,而且知道谁应该试下一个被运行的进程。

6.2、进程选择

  CFS模型所追求的是“理想多任务处理器”,其中所有可运行进程的vruntime值将一致。为了更好地做到这一点,CFS试图利用一个简单地规则去均衡进程的虚拟运行时间,这也是CFS调度算法的核心:选择具有最小vruntime的任务
  CFS使用红黑树来组织可运行进程队列,并利用其迅速找到最小vrumtime值的进程。 红黑树是一种以树节点形式存储的数据,这些数据都会对应一个键值。可以通过这些键值来快速检索节点上的数据。(通过键值检索到对应节点的速度与整个树的节点规模成指数比关系

6.2.1、挑选下一个任务

  假设红黑树存储了系统中所有的可运行进程,节点的键值是可运行进程的虚拟运行时间。那么所有进程中vruntime最小的那个就在树的最左侧的叶子结点。因此,CFS的进程选择算法可以简单的理解为:运行rbtree树中最左边叶子结点所代表的那个进程。
  代码中使用 __pick_next_entity() 函数实现,定义在kernel/sched_fair.c中:

static inline struct rb_node *first_fair(struct cfs_rq *cfs_rq)                                                                                                                                                   
{
    return cfs_rq->rb_leftmost;
}

static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
    return rb_entry(first_fair(cfs_rq), struct sched_entity, run_node);
}

在这里插入图片描述

6.2.2、向树中加入进程

  在进程变为可运行态(被唤醒)或者通过fork()第一次创建进程时,CFS将进程加入rbtree中,以及缓存最左叶子结点。enqueue_entity() 函数实现了这一功能:

static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int wakeup)                                                                                                                                        
{
    /*
     * Update run-time statistics of the 'current'.
     */
    update_curr(cfs_rq);

    if (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);
    account_entity_enqueue(cfs_rq, se);
}

  更新运行时间和其他一些统计数据后,调用 __enqueue_entity() 进行繁重的插入操作:

/*
 * Enqueue an entity into the rb-tree:
 */
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;

    /*
     * Find the right place in the rbtree:
     */
    while (*link) {
        parent = *link;
        entry = rb_entry(parent, struct sched_entity, run_node);
        /*
         * We dont care about collisions. Nodes with
         * the same key stay together.
         */
        if (key < entity_key(cfs_rq, entry)) {
            link = &parent->rb_left;
        } else {
            link = &parent->rb_right;
            leftmost = 0;
        }
    }

    /*
     * Maintain a cache of leftmost tree entries (it is frequently
     * used):
     */
    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);
}

在这里插入图片描述

6.2.3、从树中删除进程

  删除动作发生在进程堵塞(变为不可运行态)或者终止时(结束运行),代码中使用 dequeue_entity() 函数实现:

static void
dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int sleep)
{
    /*
     * Update run-time statistics of the 'current'.
     */
    update_curr(cfs_rq);

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

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

    if (se != cfs_rq->curr)
        __dequeue_entity(cfs_rq, se);
    account_entity_dequeue(cfs_rq, se);
}

  代码中主要调用了 __dequeue_entity() 函数完成删操作:

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

    rb_erase(&se->run_node, &cfs_rq->tasks_timeline);
}


在这里插入图片描述

6.3、 调度器入口

  进程调度的主要入口点是函数 schedule() ,它定义在 kernel/sched.c文件中。schedule()会找到一个最高优先级的调度类,调度类需要有自己的可运行队列,然后schedule()函数才能知道下一个该运行的进程是哪个。
  schedule() 函数唯一重要的事就是调用了 pick_next_task()pick_next_task() 会以优先级为序,从高到低,依次检查每一个调度类,并且从最高优先级的调度类中选择出最高优先级的进程:

asmlinkage void __sched schedule(void)
{
	...
	next = pick_next_task(rq, prev);
	...
}

  asmlinkage 作用是表明 schedule() 函数用堆栈来传递参数,不用寄存器传递。asmlinkage.
  __sched 作用是把带有__sched的函数放到.sched.text段。.sched.text段中的函数貌似不会让函数在waiting channel中显示出来。
  kernel有个waiting channel,如果用户空间的进程睡眠了,可以查到是停在内核空间哪个函数中等待的:

  cat “/proc/<pid>/wchan”

/*
 * Pick up the highest-prio task:
 */
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev)
{
    const struct sched_class *class;
    struct task_struct *p;

    /*   
     * Optimization: we know that if all tasks are in
     * the fair class we can call that function directly:
     */
    if (likely(rq->nr_running == rq->cfs.nr_running)) {
        p = fair_sched_class.pick_next_task(rq);
        if (likely(p))
            return p;
    }    

    class = sched_class_highest;
    for ( ; ; ) {
        p = class->pick_next_task(rq);
        if (p)
            return p;
        /*   
         * Will never be NULL as the idle class always
         * returns a non-NULL p:
         */
        class = class->next;
    }    
}

   pick_next_task() 函数开头的语句rq->nr_running == rq->cfs.nr_running,用来加速选择CFS提供的进程,如果所有可运行进程数量等于CFS类对应的可运行进程数(说明所有的可运行进程都是CFS类的,CFS是普通进程的调度类)。
  该函数的核心是for()循环,它以优先级为序,从最高的优先级类开始,遍历每一个调度类,每一个调度类都实现了 pick_next_task()函数,它会返回指向下一个可运行进程的指针,或者没有时返回NULL。
  我们会从第一个返回非NULL值的类中选择下一个可运行的进程。如果是CFS的类,那么就会调用CFS(CFS的代码在kernel/sched_fair.c 中)的pick_next_task()函数, pick_next_task()会调用 pick_next_entity()pick_next_entity() 会再调用上面讲到的函数 __pick_next_entity 去选择下一个进程。

6.4、睡眠和等待

  休眠时内核的操作如下:进程把自己标记成休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule()选择和执行一个其他进程
  唤醒时的过程:进程被设置为可执行状态,然后再从等待队列中移到可执行红黑树中。
  休眠有两种相关的进程状态:TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE。他们的唯一区别在于TASK_UNINTERRUPTIBLE的进程会忽略信号,而处于TASK_INTERRUPTIBLE的进程如果接收到一个信号,会被提前唤醒并相应该信号。两种状态灯的进程位于同一个等待队列上。

6.4.1、等待队列

  等待队列是由等待某些事件发生的进程组成的简单链表。内核用 wake_queue_head_t 来代表等待队列。等待队列可以通过 DECLARE_WAITQUEUE() 静态创建,也可以由 init_waitqueue_head() 动态创建。
在这里插入图片描述
  进程通过执行下面几个步骤将自己加入到一个等待队列中:

  • 1)调用宏DEFINE_WAIT()创建一个等待队列的项/元素。
  • 2)调用add_wait_queue()把自己加入到队列中。该队列会在进程等待的条件满足时唤醒它。在事件发生时,对等待队列执行wake_up()操作。
  • 3)调用prepare_to_wait()方法将进程的状态更改为TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE。
  • 4)如果状态被设置为TASK_INTERRUPTIBLE,则信号唤醒进程。这列是伪唤醒,因此检查并处理信号。
  • 5)当进程被唤醒的时候,在建检查’condition’是否为真。如果是,退出;如果不是,使用shcedule()进行调度。
  • 6)当条件满足后,进程将自己设置为TASK_RUNNING并调用finish_wait()方法把自己移出等待队列。
6.4.2、唤醒

  通过 wake_up() 可以唤醒指定的等待队列上的所有进程。 wake_up() 调用 try_to_wake_up(),该函数负责将进程设置为TASK_RUNNING状态。调用enqueue_task() 将进程放回红黑树中,如果被唤醒的进程优先级比当前正在执行的进程的优先级高,还需设置need_resched 标志。
在这里插入图片描述
  内核需要知道在什么时候调用schedule() 。内核定义了一个 need_resched 标志来表明是否需要重新执行一次调度。
  当某个进程需要被抢占时,scheduler_tick() 会设置这个标志;当一个优先级高的进程进入一个可执行状态的时候,try_to_wake_up() 也会设置这个标志。内核检查该标志,确认被设置,则会调用schedule() 来切换到一个新的进程。

在这里插入图片描述
  在返回用户空间和中断返回的时候,内核也会检查 need_resched 标志,如果已经被置位,内核也会执行调度程序。
  每个进程都包含一个 need_resched 标志,在2.6内核中,它被存放在 thread_info 结构中,用一个特别的标识变量中的一位来表示。

7、抢占和上下文切换

7.1、上下文切换

  上下文切换由定义在 kernel/sched.c 中的 context_switch() 函数负责处理。每当一个新的进程被选出来准备投入运行的时候,schedule() 函数就会调用该函数。context_switch() 函数主要完成了以下两项工作:

  • 调用声明在 <asm/mmu_context.h>中的switch_mm() 函数,该函数负责把虚拟内存从上一个进程映射切换到新进程中。
  • 调用声明在 <asm/system.h>的switch_to() 函数,该函数负责从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息,还有其他和体系结构相关的状态信息,都必须以每个进程为对象进行管理和保存。

7.2、用户抢占

  用户抢占发生在以下情况:

  • 1)从系统调用返回用户空间时;
  • 2)从中断处理函数返回用户空间时;

  在这两种情况下,如果设置了need_resched 标志,就会发生抢占调度。
  中断处理程序和系统调用返回的返回路径都是和体系结构相关的,在entry.S文件中通过汇编语言实现。此文件包含了内核入口部分代码和内核退出部分代码。

7.3、内核抢占

  在2.6版本的内核中,只要重新调度是安全的,内核就可以在任一时间抢占正在执行的任务。只要没有持有锁,内核就认为是安全的,可以抢占。
  在thread_info结构中引入了preempt_count计数器,计数器初始值为0,每使用一次锁+1,释放一次锁-1。当数值为0的时候,内核就可以抢占。
  如果need_resched标志被设置,并且preempt_count不等于0,内核会继续执行该进程,直到持有的锁被释放,preempt_count 被设置为0。此时释放锁的代码会再次检查need_resched标志,如果标志被设置,则调用调度程序。
  有些内核的代码需要允许或禁用内核抢占。
  如果内核中的进程被阻塞,或者显示地调用了schedule() 函数,内核抢占也会显示执行。
  内核抢占发生在:

  • 1)中断处理程序正在执行,且返回内核空间之前;
  • 2)内核代码再一次具有可抢占性的时候;
  • 3)内核中的任务显示地调用了schedule() 函数;
  • 4)内核中的任务阻塞。

8、实时调度策略

  Linux提供了两种实时调度策略:SCHED_FIFOSCHED_RR,普通调度策略是SCHED_NORMAL。实时调度策略不被“完全公平调度器管理”,而是被一个特殊的实时调度器管理,顶一个在kernel/sched_rt.c 中。
  SCHED_FIFO 是一种简单的先入先出算法,不使用时间片。处于可执行状态的SCHED_FIFO进程会比SCHED_NORMAL进程优先得到调度。.SCHED_FIFO进程在执行时除非阻塞或者显示地释放处理器,不然会一直执行。高优先级的SCHED_FIFO进程或者SCHED_RR进程才可以抢占低优先级的SCHED_FIFO进程。
  SCHED_RR进程是带有时间片的SCHED_FIFO进程。高优先级的SCHED_FIFO进程或者SCHED_RR进程才可以抢占低优先级的SCHED_RR进程。但是都优先级进程不能抢占SCHED_RR进程,及时其时间片已经耗尽。
  两种实时调度算法都是静态优先级。保证高优先级能抢占低优先级。实时优先级范围为0~MAX_RT_PRIO减1,默认情况下MAX_RT_PRIO为100。SCHED_NORMAL 的nice值对应实时优先级的MAX_RT_PRIO~MAX_RT_PRIO+40。也就是nice值 -20 ~ +19对应100 ~ 140 的实时优先级。

9、与调度相关的系统调用

在这里插入图片描述

9.1、调度策略的系统调用

  sched_setscheduler()和sched_getshceduler() 用于设置和获取进程的调度策略。主要工作在于读取和改写task_struct的policy和rt_priority的值

9.2、优先级相关的系统调用

  sched_setparam()和sched_getparam() 函数用户设置和获取实时优先级。主要是获取sched_param结构体(定义在<linux/sched.h>中)中的sched_priority成员。
  nice() 函数可以将给定进程的静态优先级增加一定的量,只有超级用户才能使用负值,从而提高优先级。nice()函数会设置进程的task_struct结构的static_prio值和prio值

9.3、与处理器绑定有关的系统调用

  Linux调度程序提供强制的处理器绑定(processor affinity)机制。这种强制性的绑定保存在进程task_structcpus_allowed 这个位掩码标志中。

9.4、放弃处理器时间

  Linux通过sched_yield() 系统调用,提供了让进程主动放弃处理器时间。对于普通进程,在调用sched_yield() 后,不仅会将进程移动到优先级队列的最后面,还会将进程移动到过期队列中。而对于实时进程,只会讲进程移动到优先级队列的最后面。
  
  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr_zhangsq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值