A realtime preemption overview(2005-08-10/Paul McKenney)
实时抢占补丁概观
Yang Honggang<eagle.rtlinux@gmail.com>
ref: http://lwn.net/Articles/146861/
----------------------------------------
PREEMPT_RT的思想
PREEMPT_RT补丁的核心是最小化(Linux)内核中不可抢占部分的代码,同时又将
为支持抢占性必须要修改的代码量最小化。
临界区、中断处理函数、关中断等代码序列通常是进行抢占改进的。
PREEMPT_RT补丁利用Linux内核的SMP特性来进行可抢占改进,这样就避免了对
重写整个Linux代码。
从某种程度上讲,我们可以简单地认为抢占是在系统中添加了一个新的CPU,
然后利用通常的锁机制来对抢占任务进行同步。
注意不要对上面的叙述死扣,比如在每次抢占时PREEMPT_RT并没有产生
CPU热插拔事件。关键是底层SMP环境必须提供容忍自由抢占的机制。
下面的章节给出来PREEMPT_RT的思想是怎样实施的。
PREEMPT_RT特性
1. 临界区可抢占
2. 中断处理函数可抢占
3. "关中断"代码序列可抢占
4. 内核中的spinlock和semaphore支持优先级继承
5. 延迟操作
6. 降低延迟的措施
下面分别介绍:
/// 1. 临界区可抢占
在PREEMPT-RT中通常的spinlock(spinlock_t和rwlock_t)、RCU“读部分”的临界区
(rcu_read_lock()和rcu_read_unlock())都是可抢占的。
Semaphore临界区也是可以抢占的(没有打PREEMPT_RT补丁普通内核也是这样的)。
该可抢占性意味着当获取 spinlock时也可以阻塞,反过来,当关闭中断或者抢占时,
不应该去申请spinlock。这也意味着spinlock_t中使用的spin_lock_irqsave()没有
关闭硬件中断。
测试#1: 在普通内核中怎样支持semaphore临界区抢占?
那么在中断或者抢占关闭的条件下需要申请锁时该如何去做?可以使用raw_spinlock_t
而不是spinlock_t。在raw_spinlock_t中会调用spin_lock()。
PREEMPT_RT中引入了一系列的宏让spin_lock()表现的像C++中的重载一样。当在raw_spinlock_t
中调用时,它表现为传统的spinlock,当在spinlock_t中调用时,它的临界区又可被抢占。
例如,多种_irq原语(如spin_lock_irqsave())用在raw_spinlock_t中时,会关闭硬件中断。
但是,用在spinlock_t中时却不会关闭硬件中断。然而,对于raw_spinlock_t(以及相应的rwlock_t
、raw_rwlock_t)的使用不遵循该规则。在调度器、平台相关的代码和RCU等很少的底层部分才
需要这些raw lock,其他地方用不到。
因为临界区可以被抢占,那么不能指望给定的临界区仅在一个固定的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()可以睡眠,那么需要增加额外的任务状态。看下Ingo Molnar提供的
代码片段:
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 timer
使用SA_NODELAY是因为它和调度和其他内核核心组建联系很紧密。另外,必须非常谨慎地处理
标记为SA_NODELAY的中断处理函数,否则将可能导致oops和死锁。后面的章节中会有介绍。
因为per-CPU时钟中断(比如scheduler_tick())运行在硬件中断上下文,任何与进程上下文共享
的锁必须是raw spinlock(raw_spinlock_t/raw_rwlock_t)。当需要在进程上下中申请spinlock时
必须使用_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的中断交互的代码应该使用raw_local_irq_save(),而不是local_irq_save()。
因为使用local_irq_save()并不会关闭硬件中断。类似地当需要和标记为SA_NODELAY的中断的代码交互时,
应该使用raw spinlock(raw_spinlock_t、raw_rwlock_t和raw_seqlock_t)。但是,不应该在低层次的
领域(如调度器、体系结构相关代码和RCU)之外使用raw spinlock。
内核中的spinlocks和semaphores支持优先级继承
设计实时程序的程序员通常很关心优先级的翻转问题。在如下的情况下会发生优先级翻转:
* 低优先级的任务A获取到一个资源,比如一个锁(L)。
* 中优先级任务B开始执行,抢占了任务A。
* 高优先级任务C尝试获取资源L。因为中优先级的任务B抢占了任务A,(任务A无法释放锁L)那么
高优先级的任务就会阻塞。
优先级翻转可能导致一个高优先级任务被无限期推迟执行。通常有两种方法来解决该问题:
(1)禁止抢占
(2)优先级继承
由于方法1中没有抢占,所以任务B就无法抢占任务A,这样就避免了优先级翻转的发生。
该方法在PREEMPT内核的spinlock中,而在PREEMPT内核的semaphore中没有使用。
因为在持有semaphore时阻塞是合法的,这时即便是没有抢占也会发生优先级逆转,所以
在这种情况下禁止抢占没有意义。对于某些实时任务,禁止抢占会引入显著的调度延迟,
所以就连spinlock中也不能禁止抢占。
优先级继承用在禁用抢占不适用的场合。核心思想是:高优先级任务暂时将其优先级赠与拥有临界
资源(锁)的低优先级任务。此处优先级继承是变化的:比如,又有一个优先级更高的任务D也尝试
获取锁L,那么任务C和A的优先级都会暂时提升为任务D的优先级。优先级继承的持续时间是非常
短暂的。因为一旦低优先级任务A释放了锁,它马上就会失去短暂提升的优先级,然后将锁交给
任务C。
然而,任务C要想运行或者需要一段时间。因为很有可能另一个更高优先级的任务E在同一时间尝试
获取锁L。那么,任务E就会从任务C手中将锁L“偷去”。这是合法的,因为任务C根本没有运行,也没有
正真的获取到锁L。另一种情况是,任务C在任务E尝试获取锁L之前已经开始运行,那么任务E将无法再
“偷去”锁L。任务E必须等待任务C释放锁L,这可能会暂时使得任务C的优先级提高,从而快速运行加快
对锁的释放。
另外,在很多时候任务会长时间持有锁。如果其他任务需要该锁,可以通过增加“抢占点”来使得锁持有者主动
放弃锁。JBD(Journal Block Device)层包括大量的这类例子。
PREEMPT_RT将问题简单化为:在一段时间内仅仅允许一个任务读持有读者-写者锁/semaphore。允许
该任务递归获取该锁。尽管丧失一些灵活性,却使得优先级继承变得切实可行。
快速测试#2: 从写者到多个读者的情况下,怎样简单快速的实现优先级继承?
对于semaphore在有些情形下,不需要优先级继承,比如:
当semaphore被用作事件机制而不是锁的时候(在事件发生之前,我们不知道谁会发出该事件,所以
无法提高其优先级)。在这些情形下,可以使用compat_semaphore和compat_rw_semaphore变种。
多种semaphore原语(up(),down()等)既可以用于compat_semaphore也可以用于semaphore。
类似读者-写者semaphore原语(up_read(),down_write()等)既可以用于compat_rw_semaphore也可以
用于rw_semaphore。然而,通常completion机制是解决此类问题的好选择。
总结一下:优先级继承使得高优先级任务可以及时地获取锁和semaphore,即便是锁或semaphore
已经被低优先级的任务获取。PREEMPT_RT的优先级继承提供短暂的继承,这是高优先级任务突然
要获取低优先级任务的锁所需要的。compat_semaphore和compat_rw_semaphore可以用于不需要semaphore优先级
继承的事件类的使用场合。
/// 延迟操作
因为现在spin_lock()可以睡眠,所以在抢占/中断禁止时调用它是不合法的。在某些情况下的解决办法是,
延迟对spin_lock()的申请直到抢占又被重新开启为止:
* put_task_struct_delayed()将put_task_struct()用队列管理起来,直到对task_struct 中的
spinlock_t alloc_lock申请合法为止。
* 和put_task_struct_delayed()类似, mmdrop_delayed()将mmdrop()用队列管理起来。
* 使用TIF_NEED_RESCHED_DELAYED标志可以进行重新调度,但是调度会延迟到进程准备好返回用户空间,
或者下一个preempt_check_resched_delayed()。两者的关键点是避免了无谓的抢占(将被唤醒的高优先级任务
会等待当前任务释放一个锁)。如果不指定TIF_NEED_RESCHED_DELAYED标志,高优先级的任务将
立即抢占低优先级任务,却很快又会阻塞在对低优先级占有的锁的申请上。
解决的办法是:将后面紧跟着spin_unlock()的wake_up()替换为wake_up_process_sync()。
这样,如果将要被唤醒的进程会抢占当前进程,那么唤醒操作将通过指定TIF_NEED_RESCHED_DELAYED标记被延迟。
对于所有上述的情形,解决方案是:延迟一个动作,直到它可以被更安全和方便地执行为止。
降低延迟的措施
有些PREEMTP_RT的修改的主要原因是降低调度/中断延迟。
x86 MMX/SSE硬件就是一个列子。该硬件在内核空间抢占关闭的情况下进行操作。这意味着,
直到MMX/SSE指令运行完毕,抢占才能开启。有些MMX/SSE指令没有问题,但是有些指令的
执行需要很长时间。对此PREEMPT_RT的解决方案是不使用慢的MMX/SSE指令。
另一些修改是:向slab分配器申请per-CPU变量也是对肆意关闭中断的一种解决方案。
/ PREEMPT_RT基本工具概述
本节对PREEMPT_RT中增加的或者被PREEMT_RT改变很多的内核设施进行简要介绍。
///锁
* spinlock_t
临界区可抢占。_irq 操作(比如,spin_lock_irqsave())不会禁止硬件中断。优先权继承被用来
解决优先级翻转。在PREEMPT_RT中spinlock_t是利用rt_mutex来实现的(同样,rwlock_t, struct semaphore,
和struct rw_semaphore也是如此)。
* raw_spinlock_t
spinlock_t的特殊变种,提供传统的spin锁的功能。使用时,临界区将不可抢占,并且_irq操作将
禁止硬件中断。需要注意的是,通常你应该使用通常的锁(比如,spin_lock())而不是raw_spinlock_t。
除了体系结构相关的代码或者底层调度和同步设施,不应该使用raw_spinlock_t。
* rwlock_t
临界区可抢占。_irq操作(比如,write_lock_irqsave())不会关闭硬件中断。使用优先级继承来解决
优先级翻转问题。为了简化优先级继承的实现,一次只能有一个任务可以读持有一个给定的rwlock_t,
该任务可以递归地读持有该锁。
* RW_LOCK_UNLOCKED(mylock)
该宏只有一个mylock参数,这是优先级继承操作所需要的。不幸的是,这在PREEMPT_RT和非PREEMP_RT
内核之间是不兼容的。因此应该使用DEFINE_RWLOCK(),而不是本宏。
* raw_rwlock_t
rwlock_t的特殊变种,提供传统的行为,临界区非抢占_irq操作将真正关闭硬件中断。类似于
类似于raw_spinlock_t。除了体系结构相关的代码或者底层调度和同步设施,不应该使用raw_rwlock_t.
* seqlock_t
临界区可抢占。更新端使用了优先级继承(读端不能参与优先级继承,因为seqlock_t的读者不能阻塞写者)。
* SEQLOCK_UNLOCKED(name)
应该使用DECLARE_SEQLOCK()。
* struct semaphore
现在转向支持优先级继承。
* down_trylock()
可调度,不能在硬件中断禁止或者抢占禁止时调用。然而,由于几乎所有的中断都运行在进程上下文,并且
允许抢占和中断,所以影响不大。
* struct compat_semaphore
struct semaphore的变体,不支持优先权继承。这对于当你需要一个事件机制而不是一个睡眠锁时很有用处。
* struct rw_semaphore
支持优先权继承。一次只能有一个任务可以读持该rw_semaphore,该任务可以递归地读持有该锁。
* struct compat_rw_semaphore
struct rw_semaphore的变体,不支持优先权继承。这对于当你需要一个事件机制而不是一个睡眠锁时很有用处。
测试#3:为什么事件机制不使用优先权继承?
///per-CPU变量
DEFINE_PER_CPU_LOCKED(type, name)
DECLARE_PER_CPU_LOCKED(type, name)
定义/声明一个指定类型和名称的per-CPU变量,同时也定义/声明一个与之关联的spinlock_t变量。
如果有一组per-CPU变量需要spinlock保护,那么可以将它们组织到一个struct中。
get_per_cpu_locked(var, cpu)
先获取对应的spinlock后再返回指定的CPU相关的per-CPU变量。
put_per_cpu_locked(var, cpu)
释放指定CPU相关的per-CPU变量关联的spinlock。
per_cpu_lock(var, cpu)
将指定CPU相关的per-CPU变量以左值(lvalue)形式返回。这在调用一个函数而这个函数将一个spinlock
作为参数并且它将释放该spinlock时很有用。
per_cpu_locked(var, cpu)
将指定CPU相关的per-CPU变量关联的spinlock以左值形式返回,但是并不获取该锁。这在已经持有了
该锁,但是需要再获取一个对该变量的索引时很有用。或者,你要获取一个对该变量的RCU-read-side
引用,因而不需要获取该锁。
///中断处理函数
SA_NODELAY
用在struct irqaction中,指定相关的中断处理函数应该在硬件中断上下文中直接调用,而不是
交给irq线程处理。redirect_hardirq()会唤醒中断处理函数,中断处理过程或许可以在
do_irqd()找到。
不应该在通常的设备中断处理函数中使用SA_NODELAY,原因如下:
(1)这将增加中断和调度的延迟
(2) SA_NODELAY中断处理函数的编码和维护比通常的中断处理函数难。只应该在低层中断(比如时钟中断)
或者需要极短延迟的硬件中断中使用SA_NODELAY。
local_irq_enable()
local_irq_disable()
local_irq_save(flags)
local_irq_restore(flags)
irqs_disabled()
irqs_disabled_flags()
local_save_flags(flags)
local_irq*()系列函数并没有真正关闭硬件中断,而是仅仅关闭了抢占。这适用于普通中断,但是
不适用于SA_NODELAY类型的中断。
然而,通常在PREEMPT_RT环境下使用锁比使用这些函数更好。但是,也需要考虑在SMP上运行
非PREEMPT_RT内核时的影响。
raw_local_irq_enable()
raw_local_irq_disable()
raw_local_irq_save(flags)
raw_local_irq_restore(flags)
raw_irqs_disabled()
raw_irqs_disabled_flags()
raw_local_save_flags(flags)
因为这些函数会真正将硬件中断关闭,所以适用于SA_NODELAY类型的中断,比如调度器时钟中断(其他调用了scheduler_tick()的函数)。
这些函数专用于调度其和同步机制等底层代码中。记住:不要在raw_local_irq*()的影响域中获取通常的spinlock_t。
//其他
wait_for_timer()
等待指定的定时器到期。 在PREEMPT_RT环境里,定时器运行在进程上下文中,可以被抢占、阻塞(比如,在获取spinlock_t期间)。
smp_send_reschedule_allbutself()
向所有其他CPU发送重新调度IPI。新唤醒的高优先级实时任务在本CPU上的优先级不够高,因此需要调度器快速地找到
其他CPU来运行该任务。这是确保实时任务能被高效的全局调度所需要的。非实时任务继续以传统的per-CPU的方式被调度。
这牺牲了一些优先级精度来确保高效和可伸缩性。
INIT_FS(name)
以变量名为参数,这样内部的rwlock_t就能被正确地初始化(假定需要考虑优先级继承)。
假定需要考虑优先级继承local_irq_disable_nort()
local_irq_enable_nort()
local_irq_save_nort(flags)
local_irq_restore_nort(flags)
spin_lock_nort(lock)
spin_unlock_nort(lock)
spin_lock_bh_nort(lock)
spin_unlock_bh_nort(lock)
BUG_ON_NONRT()
WARN_ON_NONRT()
在PREEMPT_RT环境下,这些函数几乎没有作用。但是在其他环境中,这些函数有作用。这些函数不应该在
如调度器、同步工具或者体系结果相关的代码等底层代码之外使用。
spin_lock_rt(lock)
spin_unlock_rt(lock)
in_atomic_rt()
BUG_ON_RT()
WARN_ON_RT()
相反,这些函数在PREEMPT_RT环境下起作用,但是在其他环境中几乎不起作用。这些函数也不应该
在如调度器、同步工具或者体系结构相关的代码等底层代码之外使用。
smp_processor_id_rt(cpu)
该函数在PREEMPT_RT环境下返回"cpu",在其他环境下和smp_processor_id()作用相同。该函数专用于
slab分配器中。
PREEMPT_RT配置选项
高层抢占选项
PREEMPT_NONE 传统的非抢占模式,适用于服务器配置
PREEMPT_VOLUNTARY 支持自愿抢占点,但是不支持大规模内核抢占。适用于桌面配置。
PREEMPT_DESKTOP 支持资源抢占点,也支持非临界区抢占(PREEMPT)。适用于要求低延迟的桌面环境配置。
PREEMPT_RT 支持包括临界区在内的完全抢占。
特性选择配置选项
PREEMPT 使能非临界区内核抢占
PREEMPT_BKL 使能大内核锁抢占
PREEMPT_HARDIRQS 使硬件中断运行在进程上下文,因此使得硬件中断可以被抢占。然而,对于标记为
SA_NODELAY类型的硬件中断,仍然运行在硬件上下文。
PREEMPT_RCU 使得RCU 读端临界区可以被抢占。
PREEMPT_SOFTIRQS 使得softirq运行在进程上下文,因此可以被抢占。
调试配置选项
这些选项变化较快,但是仍然可以给我们一个PREEMPT_RT环境中的一些调试特性的大概印象。
CRITICAL_PREEMPT_TIMING 计算内核关闭抢占的最大时间。
CRITICAL_IRQSOFT_TIMING 计算内核关闭硬件中断的最大时间。
DEBUG_IRQ_FLAGS 使能内核对spin_unlock_irqestore()等函数的"flags"参数的检查。
DEBUG_RT_LOCKING_MODE 使能运行过程中在可抢占和不可抢占模式之间切换。这对于想评估
PREEMPT_RT机制开销的内核开发者很有用。
DETECT_SOFTLOCKUP 使得内核将任何超过10s没有重新调度的程序的当前栈信息进行转储。
LATENCY_TRACE 记录表示long-latency事件trace的函数调用。可以通过/proc/latency_trace
读出内核中的这些trace。可以通过/proc/sys/kernel/preempt_thresh过滤出低延迟trace。
该选项在跟踪多中延迟时非常有用。
LPPTEST 使能基于并口的延迟测试驱动,比如 Kristian Benoit在2005年的LKML上提出的。可以使用scripts/testlpp.c
来运行该测试。
PRINTK_IGNORE_LOGLEVEL 将会使所有printk()信息打印控制台。通常这不是个好主意,但是在其他调试工具失败时,
这还是很有用的。
RT_DEADLOCK_DETECT 找到死锁循环。
RTC_HISTOGRAM 对使用/dev/rtc的应用程序产生延迟直方图。
WAKEUP_TIMING 测量从高优先级线程被唤醒到它实际开始运行的最大时间间隔(以ms为单位)。测量结果可以通过
/proc/sys/kernel/wakeup_timing获取。测试重启:
echo 0 > /proc/sys/kernel/preempt_max_latency
开发PREEMPT_RT过程中的一些额外收获
因为PREEMPT_RT环境对Linux的SMP-safe编码依赖很大,所以在开发PREEMP_RT的过程中,发现一些Linux内核中的
SMP bug。这些bug包括时钟死锁、ns83820_tx_timeout()等的对锁忽略、一个ACPI-idle调度延迟bug、网络核心锁和
块IO中的一系列preempt-off-needed bug。
测试答案
#1: 在普通内核中怎样支持semaphore临界区抢占?
严格的讲,在non-preemptible内核中,抢占不会发生。但是,由于访问用户数据而产生的页故障或者显式调用
调度器产生了和抢占类似的效果。
#2: 从写者到多个读者的情况下,怎样简单快速的实现优先级继承?
如果你直到如何实现,那么Ingo Molnar会对此很感兴趣。但是在你兴奋前,请首先阅读一下LKML,这个问题
非常复杂,并没有找到合适的解决方案。考虑一种特殊情形:writer-to-reader优先级提升。
一个reader-writer锁被多个读者读持有,这些读者又想写持有其他reader-writer锁,...,如此下去。
这样,对这些读者的优先级提升/恢复的时间和调度延迟相当。
#3: 为什么事件机制不使用优先权继承?
因为Linux不知道需要提升哪个新任务的优先级。以睡眠锁为例,获取semaphore的任务也将是
释放该semaphore的任务,所以内核应该提升该任务的优先级。然而,对于事件,任何一个
任务都可能调用down()唤醒高优先级任务。
实时抢占补丁概观
Yang Honggang<eagle.rtlinux@gmail.com>
ref: http://lwn.net/Articles/146861/
----------------------------------------
PREEMPT_RT的思想
PREEMPT_RT补丁的核心是最小化(Linux)内核中不可抢占部分的代码,同时又将
为支持抢占性必须要修改的代码量最小化。
临界区、中断处理函数、关中断等代码序列通常是进行抢占改进的。
PREEMPT_RT补丁利用Linux内核的SMP特性来进行可抢占改进,这样就避免了对
重写整个Linux代码。
从某种程度上讲,我们可以简单地认为抢占是在系统中添加了一个新的CPU,
然后利用通常的锁机制来对抢占任务进行同步。
注意不要对上面的叙述死扣,比如在每次抢占时PREEMPT_RT并没有产生
CPU热插拔事件。关键是底层SMP环境必须提供容忍自由抢占的机制。
下面的章节给出来PREEMPT_RT的思想是怎样实施的。
PREEMPT_RT特性
1. 临界区可抢占
2. 中断处理函数可抢占
3. "关中断"代码序列可抢占
4. 内核中的spinlock和semaphore支持优先级继承
5. 延迟操作
6. 降低延迟的措施
下面分别介绍:
/// 1. 临界区可抢占
在PREEMPT-RT中通常的spinlock(spinlock_t和rwlock_t)、RCU“读部分”的临界区
(rcu_read_lock()和rcu_read_unlock())都是可抢占的。
Semaphore临界区也是可以抢占的(没有打PREEMPT_RT补丁普通内核也是这样的)。
该可抢占性意味着当获取 spinlock时也可以阻塞,反过来,当关闭中断或者抢占时,
不应该去申请spinlock。这也意味着spinlock_t中使用的spin_lock_irqsave()没有
关闭硬件中断。
测试#1: 在普通内核中怎样支持semaphore临界区抢占?
那么在中断或者抢占关闭的条件下需要申请锁时该如何去做?可以使用raw_spinlock_t
而不是spinlock_t。在raw_spinlock_t中会调用spin_lock()。
PREEMPT_RT中引入了一系列的宏让spin_lock()表现的像C++中的重载一样。当在raw_spinlock_t
中调用时,它表现为传统的spinlock,当在spinlock_t中调用时,它的临界区又可被抢占。
例如,多种_irq原语(如spin_lock_irqsave())用在raw_spinlock_t中时,会关闭硬件中断。
但是,用在spinlock_t中时却不会关闭硬件中断。然而,对于raw_spinlock_t(以及相应的rwlock_t
、raw_rwlock_t)的使用不遵循该规则。在调度器、平台相关的代码和RCU等很少的底层部分才
需要这些raw lock,其他地方用不到。
因为临界区可以被抢占,那么不能指望给定的临界区仅在一个固定的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()可以睡眠,那么需要增加额外的任务状态。看下Ingo Molnar提供的
代码片段:
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 timer
使用SA_NODELAY是因为它和调度和其他内核核心组建联系很紧密。另外,必须非常谨慎地处理
标记为SA_NODELAY的中断处理函数,否则将可能导致oops和死锁。后面的章节中会有介绍。
因为per-CPU时钟中断(比如scheduler_tick())运行在硬件中断上下文,任何与进程上下文共享
的锁必须是raw spinlock(raw_spinlock_t/raw_rwlock_t)。当需要在进程上下中申请spinlock时
必须使用_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的中断交互的代码应该使用raw_local_irq_save(),而不是local_irq_save()。
因为使用local_irq_save()并不会关闭硬件中断。类似地当需要和标记为SA_NODELAY的中断的代码交互时,
应该使用raw spinlock(raw_spinlock_t、raw_rwlock_t和raw_seqlock_t)。但是,不应该在低层次的
领域(如调度器、体系结构相关代码和RCU)之外使用raw spinlock。
内核中的spinlocks和semaphores支持优先级继承
设计实时程序的程序员通常很关心优先级的翻转问题。在如下的情况下会发生优先级翻转:
* 低优先级的任务A获取到一个资源,比如一个锁(L)。
* 中优先级任务B开始执行,抢占了任务A。
* 高优先级任务C尝试获取资源L。因为中优先级的任务B抢占了任务A,(任务A无法释放锁L)那么
高优先级的任务就会阻塞。
优先级翻转可能导致一个高优先级任务被无限期推迟执行。通常有两种方法来解决该问题:
(1)禁止抢占
(2)优先级继承
由于方法1中没有抢占,所以任务B就无法抢占任务A,这样就避免了优先级翻转的发生。
该方法在PREEMPT内核的spinlock中,而在PREEMPT内核的semaphore中没有使用。
因为在持有semaphore时阻塞是合法的,这时即便是没有抢占也会发生优先级逆转,所以
在这种情况下禁止抢占没有意义。对于某些实时任务,禁止抢占会引入显著的调度延迟,
所以就连spinlock中也不能禁止抢占。
优先级继承用在禁用抢占不适用的场合。核心思想是:高优先级任务暂时将其优先级赠与拥有临界
资源(锁)的低优先级任务。此处优先级继承是变化的:比如,又有一个优先级更高的任务D也尝试
获取锁L,那么任务C和A的优先级都会暂时提升为任务D的优先级。优先级继承的持续时间是非常
短暂的。因为一旦低优先级任务A释放了锁,它马上就会失去短暂提升的优先级,然后将锁交给
任务C。
然而,任务C要想运行或者需要一段时间。因为很有可能另一个更高优先级的任务E在同一时间尝试
获取锁L。那么,任务E就会从任务C手中将锁L“偷去”。这是合法的,因为任务C根本没有运行,也没有
正真的获取到锁L。另一种情况是,任务C在任务E尝试获取锁L之前已经开始运行,那么任务E将无法再
“偷去”锁L。任务E必须等待任务C释放锁L,这可能会暂时使得任务C的优先级提高,从而快速运行加快
对锁的释放。
另外,在很多时候任务会长时间持有锁。如果其他任务需要该锁,可以通过增加“抢占点”来使得锁持有者主动
放弃锁。JBD(Journal Block Device)层包括大量的这类例子。
PREEMPT_RT将问题简单化为:在一段时间内仅仅允许一个任务读持有读者-写者锁/semaphore。允许
该任务递归获取该锁。尽管丧失一些灵活性,却使得优先级继承变得切实可行。
快速测试#2: 从写者到多个读者的情况下,怎样简单快速的实现优先级继承?
对于semaphore在有些情形下,不需要优先级继承,比如:
当semaphore被用作事件机制而不是锁的时候(在事件发生之前,我们不知道谁会发出该事件,所以
无法提高其优先级)。在这些情形下,可以使用compat_semaphore和compat_rw_semaphore变种。
多种semaphore原语(up(),down()等)既可以用于compat_semaphore也可以用于semaphore。
类似读者-写者semaphore原语(up_read(),down_write()等)既可以用于compat_rw_semaphore也可以
用于rw_semaphore。然而,通常completion机制是解决此类问题的好选择。
总结一下:优先级继承使得高优先级任务可以及时地获取锁和semaphore,即便是锁或semaphore
已经被低优先级的任务获取。PREEMPT_RT的优先级继承提供短暂的继承,这是高优先级任务突然
要获取低优先级任务的锁所需要的。compat_semaphore和compat_rw_semaphore可以用于不需要semaphore优先级
继承的事件类的使用场合。
/// 延迟操作
因为现在spin_lock()可以睡眠,所以在抢占/中断禁止时调用它是不合法的。在某些情况下的解决办法是,
延迟对spin_lock()的申请直到抢占又被重新开启为止:
* put_task_struct_delayed()将put_task_struct()用队列管理起来,直到对task_struct 中的
spinlock_t alloc_lock申请合法为止。
* 和put_task_struct_delayed()类似, mmdrop_delayed()将mmdrop()用队列管理起来。
* 使用TIF_NEED_RESCHED_DELAYED标志可以进行重新调度,但是调度会延迟到进程准备好返回用户空间,
或者下一个preempt_check_resched_delayed()。两者的关键点是避免了无谓的抢占(将被唤醒的高优先级任务
会等待当前任务释放一个锁)。如果不指定TIF_NEED_RESCHED_DELAYED标志,高优先级的任务将
立即抢占低优先级任务,却很快又会阻塞在对低优先级占有的锁的申请上。
解决的办法是:将后面紧跟着spin_unlock()的wake_up()替换为wake_up_process_sync()。
这样,如果将要被唤醒的进程会抢占当前进程,那么唤醒操作将通过指定TIF_NEED_RESCHED_DELAYED标记被延迟。
对于所有上述的情形,解决方案是:延迟一个动作,直到它可以被更安全和方便地执行为止。
降低延迟的措施
有些PREEMTP_RT的修改的主要原因是降低调度/中断延迟。
x86 MMX/SSE硬件就是一个列子。该硬件在内核空间抢占关闭的情况下进行操作。这意味着,
直到MMX/SSE指令运行完毕,抢占才能开启。有些MMX/SSE指令没有问题,但是有些指令的
执行需要很长时间。对此PREEMPT_RT的解决方案是不使用慢的MMX/SSE指令。
另一些修改是:向slab分配器申请per-CPU变量也是对肆意关闭中断的一种解决方案。
/ PREEMPT_RT基本工具概述
本节对PREEMPT_RT中增加的或者被PREEMT_RT改变很多的内核设施进行简要介绍。
///锁
* spinlock_t
临界区可抢占。_irq 操作(比如,spin_lock_irqsave())不会禁止硬件中断。优先权继承被用来
解决优先级翻转。在PREEMPT_RT中spinlock_t是利用rt_mutex来实现的(同样,rwlock_t, struct semaphore,
和struct rw_semaphore也是如此)。
* raw_spinlock_t
spinlock_t的特殊变种,提供传统的spin锁的功能。使用时,临界区将不可抢占,并且_irq操作将
禁止硬件中断。需要注意的是,通常你应该使用通常的锁(比如,spin_lock())而不是raw_spinlock_t。
除了体系结构相关的代码或者底层调度和同步设施,不应该使用raw_spinlock_t。
* rwlock_t
临界区可抢占。_irq操作(比如,write_lock_irqsave())不会关闭硬件中断。使用优先级继承来解决
优先级翻转问题。为了简化优先级继承的实现,一次只能有一个任务可以读持有一个给定的rwlock_t,
该任务可以递归地读持有该锁。
* RW_LOCK_UNLOCKED(mylock)
该宏只有一个mylock参数,这是优先级继承操作所需要的。不幸的是,这在PREEMPT_RT和非PREEMP_RT
内核之间是不兼容的。因此应该使用DEFINE_RWLOCK(),而不是本宏。
* raw_rwlock_t
rwlock_t的特殊变种,提供传统的行为,临界区非抢占_irq操作将真正关闭硬件中断。类似于
类似于raw_spinlock_t。除了体系结构相关的代码或者底层调度和同步设施,不应该使用raw_rwlock_t.
* seqlock_t
临界区可抢占。更新端使用了优先级继承(读端不能参与优先级继承,因为seqlock_t的读者不能阻塞写者)。
* SEQLOCK_UNLOCKED(name)
应该使用DECLARE_SEQLOCK()。
* struct semaphore
现在转向支持优先级继承。
* down_trylock()
可调度,不能在硬件中断禁止或者抢占禁止时调用。然而,由于几乎所有的中断都运行在进程上下文,并且
允许抢占和中断,所以影响不大。
* struct compat_semaphore
struct semaphore的变体,不支持优先权继承。这对于当你需要一个事件机制而不是一个睡眠锁时很有用处。
* struct rw_semaphore
支持优先权继承。一次只能有一个任务可以读持该rw_semaphore,该任务可以递归地读持有该锁。
* struct compat_rw_semaphore
struct rw_semaphore的变体,不支持优先权继承。这对于当你需要一个事件机制而不是一个睡眠锁时很有用处。
测试#3:为什么事件机制不使用优先权继承?
///per-CPU变量
DEFINE_PER_CPU_LOCKED(type, name)
DECLARE_PER_CPU_LOCKED(type, name)
定义/声明一个指定类型和名称的per-CPU变量,同时也定义/声明一个与之关联的spinlock_t变量。
如果有一组per-CPU变量需要spinlock保护,那么可以将它们组织到一个struct中。
get_per_cpu_locked(var, cpu)
先获取对应的spinlock后再返回指定的CPU相关的per-CPU变量。
put_per_cpu_locked(var, cpu)
释放指定CPU相关的per-CPU变量关联的spinlock。
per_cpu_lock(var, cpu)
将指定CPU相关的per-CPU变量以左值(lvalue)形式返回。这在调用一个函数而这个函数将一个spinlock
作为参数并且它将释放该spinlock时很有用。
per_cpu_locked(var, cpu)
将指定CPU相关的per-CPU变量关联的spinlock以左值形式返回,但是并不获取该锁。这在已经持有了
该锁,但是需要再获取一个对该变量的索引时很有用。或者,你要获取一个对该变量的RCU-read-side
引用,因而不需要获取该锁。
///中断处理函数
SA_NODELAY
用在struct irqaction中,指定相关的中断处理函数应该在硬件中断上下文中直接调用,而不是
交给irq线程处理。redirect_hardirq()会唤醒中断处理函数,中断处理过程或许可以在
do_irqd()找到。
不应该在通常的设备中断处理函数中使用SA_NODELAY,原因如下:
(1)这将增加中断和调度的延迟
(2) SA_NODELAY中断处理函数的编码和维护比通常的中断处理函数难。只应该在低层中断(比如时钟中断)
或者需要极短延迟的硬件中断中使用SA_NODELAY。
local_irq_enable()
local_irq_disable()
local_irq_save(flags)
local_irq_restore(flags)
irqs_disabled()
irqs_disabled_flags()
local_save_flags(flags)
local_irq*()系列函数并没有真正关闭硬件中断,而是仅仅关闭了抢占。这适用于普通中断,但是
不适用于SA_NODELAY类型的中断。
然而,通常在PREEMPT_RT环境下使用锁比使用这些函数更好。但是,也需要考虑在SMP上运行
非PREEMPT_RT内核时的影响。
raw_local_irq_enable()
raw_local_irq_disable()
raw_local_irq_save(flags)
raw_local_irq_restore(flags)
raw_irqs_disabled()
raw_irqs_disabled_flags()
raw_local_save_flags(flags)
因为这些函数会真正将硬件中断关闭,所以适用于SA_NODELAY类型的中断,比如调度器时钟中断(其他调用了scheduler_tick()的函数)。
这些函数专用于调度其和同步机制等底层代码中。记住:不要在raw_local_irq*()的影响域中获取通常的spinlock_t。
//其他
wait_for_timer()
等待指定的定时器到期。 在PREEMPT_RT环境里,定时器运行在进程上下文中,可以被抢占、阻塞(比如,在获取spinlock_t期间)。
smp_send_reschedule_allbutself()
向所有其他CPU发送重新调度IPI。新唤醒的高优先级实时任务在本CPU上的优先级不够高,因此需要调度器快速地找到
其他CPU来运行该任务。这是确保实时任务能被高效的全局调度所需要的。非实时任务继续以传统的per-CPU的方式被调度。
这牺牲了一些优先级精度来确保高效和可伸缩性。
INIT_FS(name)
以变量名为参数,这样内部的rwlock_t就能被正确地初始化(假定需要考虑优先级继承)。
假定需要考虑优先级继承local_irq_disable_nort()
local_irq_enable_nort()
local_irq_save_nort(flags)
local_irq_restore_nort(flags)
spin_lock_nort(lock)
spin_unlock_nort(lock)
spin_lock_bh_nort(lock)
spin_unlock_bh_nort(lock)
BUG_ON_NONRT()
WARN_ON_NONRT()
在PREEMPT_RT环境下,这些函数几乎没有作用。但是在其他环境中,这些函数有作用。这些函数不应该在
如调度器、同步工具或者体系结果相关的代码等底层代码之外使用。
spin_lock_rt(lock)
spin_unlock_rt(lock)
in_atomic_rt()
BUG_ON_RT()
WARN_ON_RT()
相反,这些函数在PREEMPT_RT环境下起作用,但是在其他环境中几乎不起作用。这些函数也不应该
在如调度器、同步工具或者体系结构相关的代码等底层代码之外使用。
smp_processor_id_rt(cpu)
该函数在PREEMPT_RT环境下返回"cpu",在其他环境下和smp_processor_id()作用相同。该函数专用于
slab分配器中。
PREEMPT_RT配置选项
高层抢占选项
PREEMPT_NONE 传统的非抢占模式,适用于服务器配置
PREEMPT_VOLUNTARY 支持自愿抢占点,但是不支持大规模内核抢占。适用于桌面配置。
PREEMPT_DESKTOP 支持资源抢占点,也支持非临界区抢占(PREEMPT)。适用于要求低延迟的桌面环境配置。
PREEMPT_RT 支持包括临界区在内的完全抢占。
特性选择配置选项
PREEMPT 使能非临界区内核抢占
PREEMPT_BKL 使能大内核锁抢占
PREEMPT_HARDIRQS 使硬件中断运行在进程上下文,因此使得硬件中断可以被抢占。然而,对于标记为
SA_NODELAY类型的硬件中断,仍然运行在硬件上下文。
PREEMPT_RCU 使得RCU 读端临界区可以被抢占。
PREEMPT_SOFTIRQS 使得softirq运行在进程上下文,因此可以被抢占。
调试配置选项
这些选项变化较快,但是仍然可以给我们一个PREEMPT_RT环境中的一些调试特性的大概印象。
CRITICAL_PREEMPT_TIMING 计算内核关闭抢占的最大时间。
CRITICAL_IRQSOFT_TIMING 计算内核关闭硬件中断的最大时间。
DEBUG_IRQ_FLAGS 使能内核对spin_unlock_irqestore()等函数的"flags"参数的检查。
DEBUG_RT_LOCKING_MODE 使能运行过程中在可抢占和不可抢占模式之间切换。这对于想评估
PREEMPT_RT机制开销的内核开发者很有用。
DETECT_SOFTLOCKUP 使得内核将任何超过10s没有重新调度的程序的当前栈信息进行转储。
LATENCY_TRACE 记录表示long-latency事件trace的函数调用。可以通过/proc/latency_trace
读出内核中的这些trace。可以通过/proc/sys/kernel/preempt_thresh过滤出低延迟trace。
该选项在跟踪多中延迟时非常有用。
LPPTEST 使能基于并口的延迟测试驱动,比如 Kristian Benoit在2005年的LKML上提出的。可以使用scripts/testlpp.c
来运行该测试。
PRINTK_IGNORE_LOGLEVEL 将会使所有printk()信息打印控制台。通常这不是个好主意,但是在其他调试工具失败时,
这还是很有用的。
RT_DEADLOCK_DETECT 找到死锁循环。
RTC_HISTOGRAM 对使用/dev/rtc的应用程序产生延迟直方图。
WAKEUP_TIMING 测量从高优先级线程被唤醒到它实际开始运行的最大时间间隔(以ms为单位)。测量结果可以通过
/proc/sys/kernel/wakeup_timing获取。测试重启:
echo 0 > /proc/sys/kernel/preempt_max_latency
开发PREEMPT_RT过程中的一些额外收获
因为PREEMPT_RT环境对Linux的SMP-safe编码依赖很大,所以在开发PREEMP_RT的过程中,发现一些Linux内核中的
SMP bug。这些bug包括时钟死锁、ns83820_tx_timeout()等的对锁忽略、一个ACPI-idle调度延迟bug、网络核心锁和
块IO中的一系列preempt-off-needed bug。
测试答案
#1: 在普通内核中怎样支持semaphore临界区抢占?
严格的讲,在non-preemptible内核中,抢占不会发生。但是,由于访问用户数据而产生的页故障或者显式调用
调度器产生了和抢占类似的效果。
#2: 从写者到多个读者的情况下,怎样简单快速的实现优先级继承?
如果你直到如何实现,那么Ingo Molnar会对此很感兴趣。但是在你兴奋前,请首先阅读一下LKML,这个问题
非常复杂,并没有找到合适的解决方案。考虑一种特殊情形:writer-to-reader优先级提升。
一个reader-writer锁被多个读者读持有,这些读者又想写持有其他reader-writer锁,...,如此下去。
这样,对这些读者的优先级提升/恢复的时间和调度延迟相当。
#3: 为什么事件机制不使用优先权继承?
因为Linux不知道需要提升哪个新任务的优先级。以睡眠锁为例,获取semaphore的任务也将是
释放该semaphore的任务,所以内核应该提升该任务的优先级。然而,对于事件,任何一个
任务都可能调用down()唤醒高优先级任务。