zoukankan      html  css  js  c++  java
  • Linux中多CPU的runqueue及抢占

    一、引出

    在在嵌入式操作系统中,很多线程都可以为实时任务,因为毕竟这些线程很少和人接触,而是面向任务的。所有就有一个抢占的时机问题。特别是2.6内核中引入了新的内核态抢占任务,所以就可以说一下这个内核态抢占的实现。

    内核态抢占主要发生在两个时机,一个是主动的检测是否需要抢占,另一个就是在异常处理完之后的异常判断

    #define preempt_enable()
    do {
     preempt_enable_no_resched();
     barrier();
     preempt_check_resched();
    } while (0)

    这一点和用户态的pthread_setcanceltype中也有使用,也就是如果禁止线程的异步取消,在使能之后的第一时间判断线程是不是已经被取消,包括内核态对信号的处理也是如此,例如,当sigprocmask开启一个信号屏蔽之后,也需要在第一时间来判断系统中是否有未处理的信号,如果有则需要及时处理,这个操作是在sys_sigprocmask--->>>recalc_sigpending--->>>set_tsk_thread_flag(t, TIF_SIGPENDING)中完成。

    另一个就是内核线程无法预测的中断或者异常处理结束之后的判断。

    linux-2.6.21archi386kernelentry.S:  ret_from_exception(ret_from_intr)--->>>>

    #ifdef CONFIG_PREEMPT
    ENTRY(resume_kernel)
     DISABLE_INTERRUPTS(CLBR_ANY)
     cmpl $0,TI_preempt_count(%ebp) # non-zero preempt_count ?首先判断线程是否禁止了抢占,如果禁止抢占,则不检测是否重新调度标志。
     jnz restore_nocheck
    need_resched:
     movl TI_flags(%ebp), %ecx # need_resched set ?
     testb $_TIF_NEED_RESCHED, %cl检测是否需要重新调度
     jz restore_all
     testl $IF_MASK,PT_EFLAGS(%esp) # interrupts off (exception path) ?
     jz restore_all
     call preempt_schedule_irq 这里就是第二个抢占发生的时机,就是内核线程不可预测的时候发生的
     jmp need_resched
    END(resume_kernel)

    在preempt_schedule_irq中引入了一个比较常见的概念,就是这个PREEMPT_ACTIVE,

    add_preempt_count(PREEMPT_ACTIVE);

    /*
     * We use bit 30 of the preempt_count to indicate that kernel
     * preemption is occurring.  See include/asm-arm/hardirq.h.
     */
    #define PREEMPT_ACTIVE 0x40000000

    这个标志位在内核中的线程抢占统计中将会用到,在schedule函数中

    switch_count = &prev->nivcsw;
     if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {这里判断的是抢占标志是否被置位,如果没有置位,也就是如果不是被抢占,则认为是自愿放弃CPU,也就是Voluntary释放CPU,否则认为是被抢占
      switch_count = &prev->nvcsw;
      if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&
        unlikely(signal_pending(prev))))
       prev->state = TASK_RUNNING;
      else {
       if (prev->state == TASK_UNINTERRUPTIBLE)
        rq->nr_uninterruptible++;
       deactivate_task(prev, rq);
      }
     }

    通过/proc/$PID/status可以看到这个切换次数记录。

    static inline void task_context_switch_counts(struct seq_file *m,
          struct task_struct *p)
    {
     seq_printf(m, "voluntary_ctxt_switches: %lu "
       "nonvoluntary_ctxt_switches: %lu ",
       p->nvcsw,
       p->nivcsw);
    }

    从调度的代码中可以看到,如果线程禁止了抢占,那么线程是不能执行调度的,这样可以推出线程在关掉抢占之后不能睡眠,如果需要等待,应该应该用spinlock,在asmlinkage void __sched schedule(void)的开始有这个判断

    if (unlikely(in_atomic() && !current->exit_state)) {
      printk(KERN_ERR "BUG: scheduling while atomic: "
       "%s/0x%08x/%d ",
       current->comm, preempt_count(), current->pid);
      debug_show_held_locks(current);
      if (irqs_disabled())
       print_irqtrace_events(current);
      dump_stack();
     }

    也就是,如果 线程是禁止抢占之后进行调度,后果是很严重的,直接内核就dump_stack了(但是没有panic,至少对386是如此)。这一点也容易理解,因为在自愿和非自愿的两个抢占判断中,都判断了线程的preempt_count的值,如果非零就退出,所以应该是不能走到这一步的。

    二、多核中的运行队列

    这个在大型服务器中是比较有用的一个概念,就是线程在CPU之间的均匀分配或者非均匀分配问题。目的就是让各个CPU尽量负载平衡,不要忙的忙死,闲的闲死。按照计算机的原始概念,CPU可以作为一个资源,然后等待使用这个资源的线程就需要排队。如果要排队,就需要有一个约定的地点让大家在这里排队,这样便于管理,比如说先来先服务,然后优先级的判断等。

    在内核里,这个队列就是每个CPU都定义的一个为struct rq 结构的runqueue变量,这个是每个CPU的一个排队区,可以认为是CPU的一个私有资源,并且是静态分配,每个CPU有天生拥有这么一个队列,拿人权的角度看,这个也就是CPU的一个基本权利,并且是一个内置权利。当CPU存在之后,它的runqueue就存在了。注意:这是一个容器,它是用来存放它的客户线程的,所以的线程在这里进行汇集和等待;对每个CPU来说,它的这个结构本身是不会变化的,变化的只是这个队列中的线程,一个线程可以在这个CPU队列里等待并运行,也可以在另一个CPU中运行,当然不能同时运行。这个变量的定义为

    static DEFINE_PER_CPU(struct rq, runqueues);

    现在,一个CPU需要服务的所有的线程都在这个结构里,所以也就包含了实时线程组和非实时线程组,它们在rq的体现为两个成员。

     struct cfs_rq cfs;
     struct rt_rq rt;

    同一个CPU上的两个运行队列采用不同的调度策略,实时策略也就是内核中希望实现的O(1)调度器,所以它的内容中包含了100个实时队列结构。这个结构也和信号相同,首先有一个位图,表示这个优先级是否有可运行线程,然后有一个指针数组,指向各个优先级的就绪线程,前者用于快速判断最高优先级队列下表,后者用于真正取出该优先级的线程。

    对于cfs调度,它一般是为了保证系统中线程对用户的及时响应,也就是说这个线程和用户交互,不能让用户感觉到某个任务有“卡”的感觉。保证这个流畅的方法就是快速切换,从而在某个时间段内所有的cfs任务都可以被运行一次。也就是不会出现某个任务跑的很欢乐,另外某个跑的很苦逼。

    这个的实现就是大家经常说的内核红黑树结构,很多地方都有说明。这里注意红黑树是一个有序树,有序就需要有键值,并且有键值的比较方法。在内核中这个键值就是每个线程的一个调度实体的vruntime成员,在linux-2.6.37.1kernelsched_fair.c中我们看到的键值比较为put_prev_task_fair--->>>put_prev_entity--->>>__enqueue_entity--->>>entity_key

    static inline s64 entity_key(struct cfs_rq *cfs_rq, struct sched_entity *se)
    {
     return se->vruntime - cfs_rq->min_vruntime;
    }
    而这个调度实体是一个抽象的概念,它可能考虑到了任务组的调度吧。实时任务和cfs任务的调度实体结构并不相同,并且这个两个在task_struct结构中两个并不是一个union,而是实实在在两个独立的实体,在task_struct结构中可以看到:

     struct sched_entity se;
     struct sched_rt_entity rt;

    这个可能是为了保证线程的优先级可以在运行时通过sys_sched_setscheduler来动态修改而设置的吧。

    对于一个runqueue,它对应一个CPU,由于一个CPU上只能同时运行一个线程,所以一个runqueue只有一个curr,因为我们可以看到一个rq有一个curr结构

     struct task_struct *curr, *idle, *stop;
    注意的是,同一时间真正使用CPU的线程只有一个,但是一个CPU上可以有多个线程都是处于就绪状态,也就是running状态,我们可以看到这个running在rq、rt_rq、cfs_rq中都有相应的成员(nr_running)。这里说的running并不是他们在运行,而是可运行,他们是用来进行CPU之间负载均衡的,和是否正在CPU上运行没有直接关系。反过来,一个线程是否处于可运行状态,是通过p->se.on_rq 来判断的

    我们看一下系统唤醒一个线程时的操作:

    wake_up_new_task--->>>activate_task--->>enqueue_task

    p->se.on_rq = 1; 这里可以看到,实时任务也是用了task_struct中的struct sched_entity se;成员,所以可以认为这是一个线程固有的成员,而struct sched_rt_entity rt;是为rt线程专门另外设置的一个附加成员,它们不是互斥或者说可替代的,而是基础和附加属性的关系。

    而对于某个CPU上正在运行的线程的判断则使用的是

    static inline int task_current(struct rq *rq, struct task_struct *p)
    {
     return rq->curr == p;
    }

    而对于nr_running的设置为

    wake_up_new_task--->>>activate_task--->>inc_nr_running(rq);

    static void inc_nr_running(struct rq *rq)
    {
     rq->nr_running++;
    }

  • 相关阅读:
    关闭页面的Js方法
    正则
    css3颜色渐变
    HTML learning
    jquery dwn 开发学习
    前人栽树后人乘凉
    剑指offer-面试题53_2-0~n-1中缺失的数字-二分查找
    剑指offer-面试题53_1-在排序数组中查找数字-二分查找
    堆排序
    剑指offer-面试题52-两个链表的第一个公共节点-链表
  • 原文地址:https://www.cnblogs.com/tsecer/p/10485824.html
Copyright © 2011-2022 走看看