内核抢占和低延迟相关工作

http://book.51cto.com/art/201005/200919.htm

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

我们现在把注意力转向内核抢占,该特性用来为系统提供更平滑的体验,特别是在多媒体环境下。与此密切相关的是内核进行的低延迟方面的工作,我会稍后讨论。

  1. 内核抢占

如上所述,在系统调用后返回用户状态之前,或者是内核中某些指定的点上,都会调用调度器。这确保除了一些明确指定的情况之外,内核是无法中断的,这不同于用户进程。 如果内核处于相对耗时较长的操作中,比如文件系统或内存管理相关的任务,这种行为可能会带来问题。内核代表特定的进程执行相当长的时间,而其他进程则无法运行。这可能导致系统延迟增加,用户体验到"缓慢的"响应。如果多媒体应用长时间无法得到CPU,则可能发生视频和音频漏失现象。

在编译内核时启用对内核抢占的支持,则可以解决这些问题。如果高优先级进程有事情需要完成,那么在启用内核抢占的情况下,不仅用户空间应用程序可以被中断,内核也可以被中断。切记,内核抢占和用户层进程被其他进程抢占是两个不同的概念!

内核抢占是在内核版本2.5开发期间增加的。尽管使内核可抢占所需的改动非常少,但该机制不像抢占用户空间进程那样容易实现。如果内核无法一次性完成某些操作(例如,对数据结构的操作),那么可能出现竞态条件而使得系统不一致。在多处理器系统上出现的同样的问题会在第5章论述。

因此内核不能在任意点上被中断。幸运的是,大多数不能中断的点已经被SMP实现标识出来了,并且在实现内核抢占时可以重用这些信息。内核的某些易于出现问题的部分每次只能由一个处理器访问,这些部分使用所谓的自旋锁保护:到达危险区域(亦称之为临界区)的第一个处理器会获得锁,在离开该区域时释放该锁。另一个想要访问该区域的处理器在此期间必须等待,直到第一个处理器释放锁为止。只有此时它才能获得锁并进入临界区。

如果内核可以被抢占,即使单处理器系统也会像是SMP系统。考虑正在临界区内部工作的内核被抢占的情形。下一个进程也在核心态操作,凑巧也想要访问同一个临界区。这实际上等价于两个处理器在临界区中工作,我们必须防止这种情形。每次内核进入临界区时,我们必须停用内核抢占。

内核如何跟踪它是否能够被抢占?回想一下,可知系统中的每个进程都有一个特定于体系结构的struct thread_info实例。该结构也包含了一个抢占计数器(preemption counter):

<asm-arch/thread_info.h>   
struct thread_info {   
...   
        int preempt_count; /* 0 => 可抢占, <00 => BUG */  
...  
} 

该成员的值确定了内核当前是否处于一个可以被中断的位置。如果preempt_count为零,则内核可以被中断,否则不行。该值不能直接操作,只能通过辅助函数dec_preempt_count和inc_preempt_ count,这两个函数分别对计数器减1和加1。每次内核进入重要区域,需要禁止抢占时,都会调用inc_preempt_count。在退出该区域时,则调用dec_preempt_count将抢占计数器的值减1。由于内核可能通过不同路线进入某些重要的区域,特别是嵌套的路线,因此preempt_count使用简单的布尔变量是不够的。在陆续进入多个临界区时,在内核再次启用抢占之前,必须确认已经离开所有的临界区。

dec_preempt_count和inc_preempt_count调用会集成到SMP系统的同步操作中(参见第5章)。无论如何,对这两个函数的调用都已经出现在内核的所有相关点上,因此抢占机制只需重用现存的基础设施即可。

还有更多的例程可用于抢占处理。

preempt_disable通过调用inc_preempt_count停用抢占。此外,会指示编译器避免某些内存优化,以免导致某些与抢占机制相关的问题。

preempt_check_resched会检测是否有必要进行调度,如有必要则进行。

preempt_enable启用内核抢占,然后用preempt_check_resched检测是否有必要重调度。

preempt_disable_no_resched停用抢占,但不进行重调度。

在内核中的某些点,普通SMP同步方法提供的保护是不够的。例如,在修改per-cpu变量时可能会发生这种情况。在真正的SMP系统上,这不需要任何形式的保护,因为根据定义只有一个处理器能够操作该变量,系统中其他的每个CPU都有自身的变量实例,不需要访问当前处理器的实例。但内核抢占的出现,使得同一处理器上的两个不同代码路径可以"准并发"地访问该变量,这与两个独立的处理器操作该值的效果是相同的。因此在这些情况下,必须手动调用preempt_disable显式停用抢占。

但要注意,第1章提到的get_cpu和put_cpu函数会自动停用内核抢占,因此如果使用该机制访问per-cpu变量,则没有必要特别注意。

内核如何知道是否需要抢占?首先,必须设置TIF_NEED_RESCHED标志来通知有进程在等待得到CPU时间。这是通过preempt_check_resched来确认的:

<preempt.h>   
#define preempt_check_resched() \  
do {\  
        if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \  
                preempt_schedule(); \  
} while (0) 

我们知道该函数是在抢占停用后重新启用时调用的,此时检测是否有进程打算抢占当前执行的内核代码,是一个比较好的时机。如果是这样,则应尽快完成,而无需等待下一次对调度器的例行调用。

抢占机制中主要的函数是preempt_schedule。设置了TIF_NEED_RESCHED标志,并不能保证一定可以抢占内核,内核有可能正处于临界区中,不能被干扰。可以通过preempt_reschedule检查:

kernel/sched.c   
asmlinkage void __sched preempt_schedule(void)   
{  
        struct thread_info *ti = current_thread_info();  
        /*  
        * 如果preempt_count非零,或中断停用,  
        * 我们不想要抢占当前进程,返回即可。   
        */   
        if (unlikely(ti->preempt_count || irqs_disabled()))   
                return;  
... 

如果抢占计数器大于0,那么抢占仍然是停用的,因此内核不能被中断,该函数立即结束。如果在某些重要的点上内核停用了硬件中断,以保证一次性完成相关的处理,那么抢占也是不可能的。irqs_disabled会检测是否停用了中断,如果已经停用,则内核不能被抢占。

《深入Linux内核架构》第2章进程管理和调度,在本章中,读者已经了解到进程是Linux的一个非常重要和基本的抽象。用于表示进程的数据结构与内核中几乎每个子系统都有关联。本节为大家介绍内核抢占和低延迟相关工作。

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

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

kernel/sched.c   
do {  
        add_preempt_count(PREEMPT_ACTIVE);  
 
        schedule();   
 
        sub_preempt_count(PREEMPT_ACTIVE);  
        /*  
        * 再次检查,以免在schedule和当前点之间错过了抢占的时机。  
        */  
} while (unlikely(test_thread_flag(TIF_NEED_RESCHED))); 

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

图2-30 进程的抢占计数器
此前我忽略了该标志与schedule的关系,因此必须在这里讨论。我们知道,如果进程目前不处于可运行状态,则调度器会用deactivate_task停止其活动。实际上,如果调度是由抢占机制发起的(查看抢占计数器中是否设置了PREEMPT_ACTIVE),则会跳过该操作:
kernel/sched.c

asmlinkage void __sched schedule(void) {  
...  
        if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {  
                if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&  
                                unlikely(signal_pending(prev)))) {  
                        prev->state = TASK_RUNNING;  
                } else {  
                        deactivate_task(rq, prev, 1);  
                }  
        }  
...  
} 

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

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

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

  1. 低延迟

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

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

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

kernel/sched.c   
int __sched cond_resched(void)   
{   
        if (need_resched() && !(preempt_count() & PREEMPT_ACTIVE))   
                __cond_resched();   
                return 1;   
        }  
        return 0;  
} 

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

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

for (;;)  
        /* 读入数据 */  
        if (exit_condition)  
                continue;  

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

for (;;)  
        cond_resched();  
        /* 读入数据 */  
        if (exit_condition)  
                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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值