一、引出 在在嵌入式操作系统中,很多线程都可以为实时任务,因为毕竟这些线程很少和人接触,而是面向任务的。所有就有一个抢占的时机问题。特别是2.6内核中引入了新的内核态抢占任务,所以就可以说一下这个内核态抢占的实现。 内核态抢占主要发生在两个时机,一个是主动的检测是否需要抢占,另一个就是在异常处理完之后的异常判断。 #define preempt_enable() \ 这一点和用户态的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 在preempt_schedule_irq中引入了一个比较常见的概念,就是这个PREEMPT_ACTIVE, add_preempt_count(PREEMPT_ACTIVE); /* 这个标志位在内核中的线程抢占统计中将会用到,在schedule函数中 switch_count = &prev->nivcsw; 通过/proc/$PID/status可以看到这个切换次数记录。 static inline void task_context_switch_counts(struct seq_file *m, 从调度的代码中可以看到,如果线程禁止了抢占,那么线程是不能执行调度的,这样可以推出线程在关掉抢占之后不能睡眠,如果需要等待,应该应该用spinlock,在asmlinkage void __sched schedule(void)的开始有这个判断 if (unlikely(in_atomic() && !current->exit_state)) { 也就是,如果 线程是禁止抢占之后进行调度,后果是很严重的,直接内核就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; 同一个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) struct sched_entity se; 这个可能是为了保证线程的优先级可以在运行时通过sys_sched_setscheduler来动态修改而设置的吧。 对于一个runqueue,它对应一个CPU,由于一个CPU上只能同时运行一个线程,所以一个runqueue只有一个curr,因为我们可以看到一个rq有一个curr结构 struct task_struct *curr, *idle, *stop; 我们看一下系统唤醒一个线程时的操作: 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) 而对于nr_running的设置为 wake_up_new_task--->>>activate_task--->>inc_nr_running(rq); static void inc_nr_running(struct rq *rq) |
|