一、引出
在在嵌入式操作系统中,很多线程都可以为实时任务,因为毕竟这些线程很少和人接触,而是面向任务的。所有就有一个抢占的时机问题。特别是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.21\arch\i386\kernel\entry.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:\t%lu\n"
"nonvoluntary_ctxt_switches:\t%lu\n",
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\n",
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.1\kernel\sched_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++;
}