linux实时进程优先级rt,Linux实时性- PREEMPT_RT实时抢占实现

作者:Paul E. McKenney

翻译整理:土豆丝624

原文链接:

概述:

本篇文章主要讲Linux的实时包PREEMPT_RT 是如何实现的。

PREEMPT_RT 的原理

PREEMPT_RT包的关键点是要使非抢占式的内核代码量尽可能的少,同时为了提供抢占性而必须修改的代码量也要尽可能的少。特别是临界区,中断处理程序和中断禁用的代码序列通常是可抢占式的。PREEMPT_RT包充分利用Linux内核的SMP能力来增加额外的抢占能力,而不是重写Linux内核。某种程度上,可以认为是抢占是给系统新加了一个CPU,然后使用常规锁定原语与抢占任务采取的任何操作进行同步。

注意:这里说的一些原理不要从字面意思上去理解。比如PREEMPT_RT包对每个抢占并不是一个热拔插事件。关键点是使用抢占必须提供SMP机制。后面的章节会详细介绍如何应用这些原理。

PREEMPT_RT的功能

PREEMPT_RT包有如下特性

抢占式临界区

抢占式中断处理

抢占式中断禁止代码序列

内核自旋锁和信号量的优先级继承

递延操作

降低延迟的措施

抢占式临界区

在PREEMPT_RT中,普通的自旋锁(spinlock_t and rwlock_t)是抢占式的,RCU读取侧临界区((rcu_read_lock() 和rcu_read_unlock())也是一样的。信号量临界区是可抢占的,他们已经存在于可抢占和非抢占内核中。这种可抢占性意思是可以阻止获取自旋锁,也就是在可抢占或中断禁用的情况下获取自旋锁是非法的(这个原则的一个例外就是变体_trylock,只要不是在密集信号中重复调用)。这也意味着当使用spinlock_t的时候spin_lock_irqsave()不会禁用硬件中断。

问题1:如何在非抢占内核中实现抢占式信号量临界区?

在中断或抢占禁用的情况下要获取锁要做什么?用raw_spinlock_t而不是spinlock_t,调用spin_lock()的时候使用raw_spinlock_t。PREEMPT_RT包含一个宏集合,这样会让spin_lock()调用的时候就像c++中的函数重载。当使用raw_spinlock_t的时候,就是传统的自旋锁。但是当使用spinlock_t,临界区就是可抢占的。当使用raw_spinlock_t时,各种_irq原语(例如spin_lock_irqsave())会禁用硬件中断,而在使用spinlock_t时不会禁用硬件中断。但是,使用raw_spinlock_t(及其对应的rwlock_t,raw_rwlock_t)应该是例外,而不是常规使用。在一些底层区域比如调度,特定的架构代码和RCU,是不需要这些原始锁的。

自从临界区可以被抢占,就不能依赖单个CPU上给定的临界区。因为是可抢占的,所以可能会移到其他的CPU上。所以,当你在临界区使用per-CPU变量时,必须单独处理抢占的可能性。因为spinlock_t和rwlock_t不再具有这个功能。

可以通过以下两种方式实现。

1. 显示禁用中断,或者通过调用get_cpu_var(), preempt_disable(),或者禁掉硬件中断

2. 使用per-CPU锁来保护per-CPU变量,可以通过使用新的DEFINE_PER_CPU_LOCKED()原语。

由于spin_lock可以睡眠,所以会增加一个额外的任务状态。思考一下下面的代码序列

spin_lock(&mylock1);

current->state = TASK_UNINTERRUPTIBLE;

spin_lock(&mylock2); // [*]

blah();

spin_unlock(&mylock2);

spin_unlock(&mylock1);

由于第二个spin_lock()调用可以睡眠,所以有可能会改变current-state的值,有可能使函数blah()产生令人惊讶的结果。在这种情况下,调度程序可以使用新的TASK_RUNNING_MUTEX位来保留current-state之前的值。尽管生成的环境有点陌生,但是通过少量的代码改动就实现了临界区抢占,并且PREEMPT_RT, PREEMPT和 non-PREEMPT三个配置项都是用相同的代码。

抢占式中断处理

在Preempt_RT环境中几乎所有的进程上下文都有中断处理。虽然任何标为SA_NODELAY的中断都可以在其上下文中运行,但是仅在fpu_irq, irq0, irq2和lpptest指定了SA_NODELAY。其中,只有irq0(per-CPU计时器中断)可以正常使用。fpu-irq是用于浮点协处理器中断,而lpptest是用于中断等待时间基准测试。注意软件计时器(add_timer())不在硬件上下文中运行。它是运行在进程上下文中,并且是完全抢占式的。

注意不要轻易使用SA_NODELAY,因为它会大大降低中断和调度延迟。Per-CPU计时器中断之所以符合条件,是因为它与调度程序和其他核心内核组件紧密相关。此外,在写SA_NODELAY中断处理代码的时候必须要非常谨慎,否则很容易出现崩溃和死锁。

由于per-CPU计时器中断运行在硬件中断上下文中,因此任何和进程上下文代码共享的锁必须是原始自旋锁(raw_spinlock_t 或 raw_rwlock_t)。并且,从进程上下文获取时,必须使用_irq变体,比如spin_lock_irqsave()。另外,当进程上下文代码访问每个和SA_NODELAY中断处理程序共享的per-CPU变量的时候,一般上要禁用硬件中断。

抢占式“中断禁用”代码序列

抢占式中断禁用代码序列的概念从术语上理解似乎是矛盾的,但是牢记PREEMPT_RT原理很重要。原理就是要依靠Linux内核的SMP功能来处理和中断处理程序的竞争。大多数中断处理程序都运行在进程上下文中。任何与中断处理程序有交互的代码都要准备处理在其他CPU上同时运行的该中断处理程序。

因此,spin_lock_irqsave()和相关的原语不需要禁用抢占。之所以安全的原因是,即使中断处理程序运行,即使它抢占了拥有spinlock_t的代码,但是在试图获取spinlock_t的时候会立即阻塞。临界区依旧会被保留。

但是,local_irq_save()依旧禁用抢占,因为没有任何锁依赖它。因此使用锁而不是local_irq_save()可以降低调度延迟,但是以这种方式替换锁会降低SMP性能,因此要小心。

需要和SA_NODELAY中断交互的代码不能使用local_irq_save(),因为它没用禁用硬件中断。相反,应该使用raw_local_irq_save(),类似的,当需要和SA_NODELAY中断处理程序交互的时候,需要使用原始自旋锁(raw_spinlock_t, raw_rwlock_t 和raw_seqlock_t)。但是原始自旋锁和原始中断禁用不应该在一些底层区域,如调度程序,架构依赖代码和RCU之外使用。

内核自旋锁和信号量的优先级继承

实时程序员会经常担心优先级倒置,这可能会发生一下几种情况:

低优先级任务A获取资源,比如获取锁

中优先级任务B开始执行CPU绑定,抢占低优先级任务A

高优先级任务C试图获取低优先任务A持有的锁,但是被阻塞了。因为中优先级任务B已经抢占了低优先级任务A

这种优先级倒置可以无限期地延迟高优先级任务。有两种方式可以解决这个问题:(1)抑制抢占;(2)优先级继承。第一种情况,由于没有抢占,所以任务B不能抢占任务A,从而阻止优先级反转的发生。这种方式在PREEMPT内核中用于自旋锁,但不用于信号量。抑制抢占对于信号量来说是没有意义的。因为持有一个信号量的时候阻塞是合法的,即使没有抢占也会导致优先级反转。对于某些实时工作负载,自旋锁也不能抑制抢占,因为会对调度延迟造成影响。

优先级继承可以用在抢占抑制没有意义的场合。就是高优先级任务临时把优先级赠与持有关键锁的低优先级任务。优先级继承是可以传递的:在上面的例子中,如果更高优先级任务D试图获取高优先级任务C已经持有的第二把锁,任务C和A都将暂时提升为任务D的优先级。优先级提升的持续时间也受到严重限制:一旦低优先级任务A释放了锁,它会立刻失去临时提升的优先级,把锁交给任务C。

但是,任务C运行需要时间,很可能同时另一个更高优先级任务E来试图获取锁。如果发生这种情况,任务E会从任务C那里偷到锁。这样是合法的,因为任务C还没有运行,因此实际上它并没有获取锁。另一方面,如果任务C在任务E试图获取锁之前已经运行,那么任务E是无法偷锁的,必须等待任务C释放锁,可能会提高任务C的优先级以加快处理速度。

另外,在某些情况下会长时间保持锁定。其中一些增加了“抢占点”,以便锁持有者在某些其他任务需要时丢弃该锁。

事实证明,读写优先级继承特别成问题。因此,尽管任务可以递归获取,但Preempt_RT可以通过一次只允许一个任务获取读写锁或信号量来简化这个为题。尽管限制了可扩展性,但这让优先级继承实现成为可能。

问题2:实现读写优先级继承的简单快捷的方法是什么

此外,在某些情况下,信号量不需要优先级继承,比如当信号量用于事件机制而不是锁的时候。compat_semaphore 和compat_rw_semaphore变体可以用于这种情况。很多信号量原语(up(), down()等)可用于compat_semaphore 和compat_rw_semaphore。相同的,读写信号量原语(up_read(), down_write()等)可用于compat_rw_semaphore 和rw_semaphore。

总结一下,优先级继承可以防止优先级反转,允许高优先级任务及时获取锁和信号量,即使这些锁和信号量被低优先级任务持有。PREEMPT_RT的优先级继承具有传递性且能够及时移除,并且具有当高优先级任务突然需要低优先任务持有的锁时,处理这种情况的灵活性。当信号量用于事件机制的时候,compat_semaphore 和compat_rw_semaphore可以避免优先级继承。

递延操作

由于spin_lock()现在可以休眠,所以当抢占或中断被禁用的时候,调用它就不再合法了。在一些情况下,可以通过递延操作要求spin_lock()等到抢占被重新启用的时候来解决这个问题。

当合法获取task_struct中的spinlock_t alloc_lock是,可以将put_task_struct()放到put_task_struct_delayed()队列中,以便延迟运行。

把mmdrop()放到mmdrop_delayed()队列中,延迟运行。

TIF_NEED_RESCHED_DELAYED重新调度,不过需要等到进程返回到用户空间,或者等到下一个preempt_check_resched_delayed()。无论哪种方式,关键点在于避免在唤醒高优先级任务直到当前任务未锁定之前无法取得进展的情况下进行不必要的抢占。没有TIF_NEED_RESCHED_DELAYED,高优先级任务会立刻抢占低优先级任务,只能被快速阻塞等待低优先级任务持有的锁。

解决方案是在spin_unlock()之后增加wake_up()去替代wake_up_process_sync()。如果唤醒的进程抢占当前进程,通过TIF_NEED_RESCHED_DELAYED,唤醒操作会被延迟。

在所有这些情况下,解决方案是将操作推迟到可以更安全或更方便地执行该操作。

降低延迟的操作

在PREEMPT_RT中的一些改变,主要目的是降低调度或中断延迟。

第一种改变是引入x86 MMX/SSE硬件。这个硬件在内核中处理中断禁用。某些情况下意味着等待直到MMX/SSE指令完成。一些MMX/SSE指令没有问题,但是有些指令要花很长时间,所以PREEMPT_RT拒绝使用这些很慢的指令。

第二个改变是使用per-CPU变量用于板坯分配器,以代替之前随意的中断禁用。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值