内核抢占和低延迟

2.8.3 内核抢占和低延迟相关工作(2)

如果可以抢占,则需要执行下列步骤:

  
  
  
  
  1. kernel/sched.c   
  2. do {  
  3.         add_preempt_count(PREEMPT_ACTIVE);  
  4.  
  5.         schedule();   
  6.  
  7.         sub_preempt_count(PREEMPT_ACTIVE);  
  8.         /*  
  9.         * 再次检查,以免在schedule和当前点之间错过了抢占的时机。  
  10.         */  
  11. } while (unlikely(test_thread_flag(TIF_NEED_RESCHED))); 

在调用调度器之前,抢占计数器的值设置为PREEMPT_ACTIVE。这设置了抢占计数器中的一个标志位,使之有一个很大的值,这样就不受普通的抢占计数器加1操作的影响了,如图2-30所示。它向schedule函数表明,调度不是以普通方式引发的,而是由于内核抢占。在内核重调度之后,代码流程回到当前进程。此时标志位已经再次移除,这可能是在一段时间之后,此间的这段时间供抢先的进程执行。

 
图2-30 进程的抢占计数器
此前我忽略了该标志与schedule的关系,因此必须在这里讨论。我们知道,如果进程目前不处于可运行状态,则调度器会用deactivate_task停止其活动。实际上,如果调度是由抢占机制发起的(查看抢占计数器中是否设置了PREEMPT_ACTIVE),则会跳过该操作:
  
  
  
  
  1. kernel/sched.c   
  2. asmlinkage void __sched schedule(void) {  
  3. ...  
  4.         if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {  
  5.                 if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&  
  6.                                 unlikely(signal_pending(prev)))) {  
  7.                         prev->state = TASK_RUNNING;  
  8.                 } else {  
  9.                         deactivate_task(rq, prev, 1);  
  10.                 }  
  11.         }  
  12. ...  

这确保了尽可能快速地选择下一个进程,而无需停止当前进程的活动。如果一个高优先级进程在等待调度,则调度器类将会选择该进程,使其运行。

该方法只是触发内核抢占的一种方法。另一种激活抢占的可能方法是在处理了一个硬件中断请求之后。如果处理器在处理中断请求后返回核心态(返回用户状态则没有影响),特定于体系结构的汇编例程会检查抢占计数器值是否为0,即是否允许抢占,以及是否设置了重调度标志,类似于preempt_schedule的处理。如果两个条件都满足,则调用调度器,这一次是通过preempt_schedule_ irq,表明抢占请求发自中断上下文。该函数和preempt_schedule之间的本质区别是,preempt_ schedule_irq调用时停用了中断,防止中断造成递归调用。

根据本节讲述的方法可知,启用了抢占特性的内核能够比普通内核更快速地用紧急进程替代当前进程。

2. 低延迟

当然,即使没有启用内核抢占,内核也很关注提供良好的延迟时间。例如,这对于网络服务器是很重要的。尽管此类环境不需要内核抢占引入的开销,但内核仍然应该以合理的速度响应重要的事件。例如,如果一网络请求到达,需要守护进程处理,那么该请求不应该被执行繁重IO操作的数据库过度延迟。我已经讨论了内核提供的一些用于缓解该问题的措施:CFS和内核抢占中的调度延迟。第5章中将讨论的实时互斥量也有助于解决该问题,但还有一个与调度有关的操作能够对此有所帮助。

基本上,内核中耗时长的操作不应该完全占据整个系统。相反,它们应该不时地检测是否有另一个进程变为可运行,并在必要的情况下调用调度器选择相应的进程运行。该机制不依赖于内核抢占,即使内核连编时未指定支持抢占,也能够降低延迟。

发起有条件重调度的函数是cond_resched。其实现如下:

  
  
  
  
  1. kernel/sched.c   
  2. int __sched cond_resched(void)   
  3. {   
  4.         if (need_resched() && !(preempt_count() & PREEMPT_ACTIVE))   
  5.                 __cond_resched();   
  6.                 return 1;   
  7.         }  
  8.         return 0;  

need_resched检查是否设置了TIF_NEED_RESCHED标志,代码另外还保证内核当前没有被抢占 ,因此允许重调度。只要两个条件满足,那么__cond_resched会处理必要的细节并调用调度器。

如何使用cond_resched?举例来说,考虑内核读取与给定内存映射关联的内存页的情况。这可以通过无限循环完成,直至所有需要的数据读取完毕:

  
  
  
  
  1. for (;;)  
  2.         /* 读入数据 */  
  3.         if (exit_condition)  
  4.                 continue;  

如果需要大量的读取操作,可能耗时会很长。由于进程运行在内核空间中,调度器无法象在用户空间那样撤销其CPU,假定也没有启用内核抢占。通过在每个循环迭代中调用cond_resched,即可改进此种情况。

  
  
  
  
  1. for (;;)  
  2.         cond_resched();  
  3.         /* 读入数据 */  
  4.         if (exit_condition)  
  5.                 continue;  

内核代码已经仔细核查过,以找出长时间运行的函数,并在适当之处插入对cond_resched的调用。即使没有显式内核抢占,这也能够保证较高的响应速度。

遵循长期以来的UNIX内核传统,Linux的进程状态也支持可中断的和不可中断的睡眠。但在2.6.25的开发周期中,又添加了另一个状态:TASK_KILLABLE。 处于此状态进程正在睡眠,不响应非致命信号,但可以被致命信号杀死,这刚好与TASK_UNINTERRUPTIBLE相反。在撰写本书时,内核中适用于TASK_KILLABLE睡眠之处,都还没有修改。

在内核2.6.25和2.6.26开发期间,调度器的清理相对而言是比较多的。 在这期间增加的一个新特性是实时组调度。这意味着,通过本章介绍的组调度框架,现在也可以处理实时进程了。

另外,调度器相关的文档移到了一个专用目录Documentation/scheduler/下,旧的O(1)调度器的相关文档都已经过时,因而删除了。有关实时组调度的文档可以参考Documentation/scheduler/ sched-rt-group.txt。

你可能感兴趣的:(内核抢占和低延迟)