[TOC]
include/linux/preempt.h 内核抢占(Kernel Preemption) 控制内核代码的可抢占性与延迟 历史与背景 这项技术是为了解决什么特定问题而诞生的? include/linux/preempt.h
是定义**内核抢占(Kernel Preemption)相关API和配置的头文件。这项技术的诞生是为了解决早期Linux内核的一个核心设计局限性:内核态执行的不可抢占性(non-preemptibility) ,以及由此导致的 系统响应延迟(latency)**问题。
在Linux 2.6版本之前,内核是非抢占式 的。这意味着,当一个进程通过系统调用进入内核态执行时,它会一直占有CPU,直到它自愿放弃(例如,因等待I/O而睡眠)或执行完毕返回用户空间。这种模型的缺点是:
高延迟 :如果一个低优先级的进程执行了一个非常耗时的系统调用(例如,对一个大文件进行复杂的读写),那么一个刚刚被唤醒的、需要立即响应用户输入的高优先级进程(如桌面窗口管理器或文本编辑器)将不得不一直等待,直到那个系统调用完成。这会导致系统UI卡顿,响应迟钝。
实时性差 :对于实时系统,这种不可预测的、长时间的延迟是致命的。
内核抢占机制的引入,就是为了改变这种状况。它允许在一个进程正在内核态执行时,如果一个更高优先级的进程变为可运行状态,调度器可以立即中断 当前进程,抢占CPU并将其分配给那个更高优先级的进程。preempt.h
提供了实现这一机制所需的底层构建块。
它的发展经历了哪些重要的里程碑或版本迭代? Linux的可抢占性是一个分阶段演进的、不断增强的过程。
用户抢占(User Preemption) :这是Linux一直具备的能力。当内核从一个中断或系统调用返回用户空间 时,它会检查是否有更高优先级的任务需要运行,并进行抢占。
内核抢占(Kernel Preemption, CONFIG_PREEMPT
) :这是在2.5/2.6开发周期中的一个革命性里程碑 。它使得内核在从中断处理程序返回内核空间 时,如果发现有更高优先级的任务就绪,也可以进行抢占。但是,这种抢占是有条件的:它只在内核代码**没有显式持有锁(如自旋锁)**时才发生。
完全实时抢占(PREEMPT_RT
) :这是内核抢占的终极形态。由实时Linux补丁集(PREEMPT_RT
patchset)引入,现在绝大部分已合入主线内核。它通过将内核中大部分的自旋锁替换为可睡眠的、支持优先级继承的互斥锁,使得内核在几乎任何地方 (除了极少数真正的临界区)都是可抢占的。这极大地降低了内核内部的调度延迟,是构建硬实时Linux系统的基础。
目前该技术的社区活跃度和主流应用情况如何? 内核抢占是现代Linux内核的一个基础特性 。
主流应用 :
桌面系统 :几乎所有桌面发行版都会开启CONFIG_PREEMPT_DESKTOP
,以获得流畅的用户体验。
服务器 :通常会选择CONFIG_PREEMPT_VOLUNTARY
或完全不抢占(CONFIG_PREEMPT_NONE
),以追求更高的吞吐量。
实时系统 :工业控制、电信、金融等领域广泛使用开启了PREEMPT_RT
的Linux内核。preempt.h
中定义的API是所有这些配置下,内核代码正确同步的基础。
核心原理与设计 它的核心工作原理是什么? 内核抢占的核心是围绕一个名为**preempt_count
的 per-thread(每个线程独立)计数器**来工作的。这个计数器像一个“请勿打扰”的标志。
preempt_count
计数器 :
这是一个32位的整数,其不同的位段被用于跟踪不同的状态(是否在中断中、是否持有自旋锁等),但其核心是抢占计数值。
当一个线程进入一个不可被抢占的区域时,它会调用preempt_disable()
,这个函数会增加 preempt_count
的值。
当它离开这个区域时,会调用preempt_enable()
,减少 preempt_count
的值。
抢占的条件 :
一个在内核态运行的线程是可抢占的,当且仅当其preempt_count
的值为0 。
抢占的触发点 :
抢占并不是随时随地发生的。调度器只在特定的检查点 检查是否需要抢占。最主要的检查点是:
从一个硬件中断处理程序返回内核空间时。
当代码显式地调用preempt_enable()
,并且在调用后preempt_count
恰好变为0时。
在这些检查点,如果内核发现当前任务的TIF_NEED_RESCHED
标志被设置了(意味着有更高优先级的任务在等待),并且 preempt_count
为0,那么抢占就会发生。
preempt.h
提供的API :
preempt_disable()
/ preempt_enable()
:控制抢占计数的核心API。
preempt_count()
:读取当前preempt_count
的值。
in_atomic()
/ in_interrupt()
/ in_serving_softirq()
:这些是极其常用的宏,它们通过检查preempt_count
的不同位段来判断当前代码是否处于一个原子上下文(即不可睡眠的上下文)。这是开发者用来判断自己是否可以调用睡眠函数的标准方式。
它的主要优势体现在哪些方面?
降低系统延迟 :极大地提高了交互式应用的响应速度。
支持实时性 :是构建实时Linux系统的基础。
提供了精细的控制 :允许内核开发者通过preempt_disable/enable
来精确地界定那些绝对不能被打断的、极短的临界区。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
性能开销 :抢占检查和preempt_count
的维护会给内核带来微小的、但不可忽略的性能开销。对于纯粹追求计算吞吐量的HPC场景,关闭内核抢占可以获得轻微的性能提升。
增加了并发复杂性 :内核抢占意味着内核代码中可能出现更多的并发路径。一段原本被认为是“原子”执行的代码路径,现在可能会被另一个任务中断,因此需要开发者使用更审慎的锁策略。
使用场景 在哪些具体的业务或技术场景下,它是首选解决方案? preempt.h
中的API主要由内核开发者 在编写底层代码时使用,而不是由用户空间程序直接调用。
保护Per-CPU数据 :当一段代码需要访问一个per-CPU 变量,并且不希望在访问过程中被调度到另一个CPU上(这会导致访问了错误的per-CPU变量)时,一种常见的做法是使用preempt_disable()
和preempt_enable()
将这段代码包裹起来。因为它只在当前CPU执行,所以不需要昂贵的自旋锁。
实现同步原语 :几乎所有的自旋锁实现,在获取锁时都会隐式地调用preempt_disable()
,在释放锁时调用preempt_enable()
。这是为了防止在持有锁时被抢占,从而导致其他试图获取该锁的CPU长时间自旋,造成死锁。
上下文检查 :内核代码在调用一个可能睡眠的函数(如kmalloc(GFP_KERNEL)
)之前,必须 检查自己是否处于原子上下文。BUG_ON(in_atomic());
是一种常见的防御性编程。
是否有不推荐使用该技术的场景?为什么?
保护跨CPU共享数据 :preempt_disable()
绝对不能 被用来保护被多个CPU共享的数据。它只阻止了本地CPU 上的任务切换,无法阻止另一个CPU上的任务同时访问该数据。这种场景必须 使用自旋锁或互斥锁 。
长时间的临界区 :preempt_disable()
应该只用于保护非常短的代码路径。长时间地禁用抢占会严重损害系统延迟,使其效果适得其反。
对比分析 请将其 与 其他相似技术 进行详细对比。 preempt_disable
是内核中用于控制并发的多种底层工具之一。
特性
preempt_disable()
/ enable()
spin_lock()
/ unlock()
local_irq_disable()
/ enable()
保护对象
本地CPU 上的调度 。
多个CPU 对共享数据的互斥访问 。
本地CPU 上的中断处理 。
主要作用
防止当前任务被其他任务抢占 。
防止其他CPU 同时进入临界区。
防止中断处理程序 打断当前代码的执行。
并发模型
阻止任务并发 (在本地CPU)。
阻止CPU并发 (在所有CPU)。
阻止代码与中断 的并发 (在本地CPU)。
开销
低 (通常是原子增减和内存屏障)。
中等 (涉及锁总线和缓存一致性协议)。
低 (通常是一条CPU指令)。
副作用
允许中断继续发生和处理。
隐式地禁用本地CPU抢占 。
隐式地禁用本地CPU抢占 。
适用场景
保护per-CPU 数据,防止在访问过程中被迁移。
保护全局或跨CPU共享 的、在原子上下文中访问的数据。
保护需要与中断处理程序同步的、极其短暂的临界区。
include/asm-generic/preempt.h preempt_count 抢占计数 1 2 3 4 static __always_inline int preempt_count (void ) { return READ_ONCE(current_thread_info()->preempt_count); }
__preempt_count_add __preempt_count_sub 增加 减少 cpu的抢占计数
preempt_count_ptr() 的目的是返回 preempt_count 的地址,而不是直接读取其值。
返回指针本身不涉及数据读取,因此不需要使用 READ_ONCE。
指针操作的目的:
调用者通过返回的指针可以直接操作 preempt_count 的值,例如读取或修改。 数据一致性和原子性需要在使用指针时由调用者负责,而不是在返回指针时处理。
避免额外开销:
READ_ONCE 是为防止编译器优化和重排序而设计的,但在返回指针的场景中,这种保护是多余的,因为指针本身不会被并发修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 static __always_inline volatile int *preempt_count_ptr (void ) { return ¤t_thread_info()->preempt_count; } static __always_inline void __preempt_count_add(int val){ *preempt_count_ptr() += val; } static __always_inline void __preempt_count_sub(int val){ *preempt_count_ptr() -= val; } void preempt_count_add (int val) { #ifdef CONFIG_DEBUG_PREEMPT if (DEBUG_LOCKS_WARN_ON((preempt_count() < 0 ))) return ; #endif __preempt_count_add(val); #ifdef CONFIG_DEBUG_PREEMPT DEBUG_LOCKS_WARN_ON((preempt_count() & PREEMPT_MASK) >= PREEMPT_MASK - 10 ); #endif preempt_latency_start(val); } EXPORT_SYMBOL(preempt_count_add); NOKPROBE_SYMBOL(preempt_count_add); void preempt_count_sub (int val) { #ifdef CONFIG_DEBUG_PREEMPT if (DEBUG_LOCKS_WARN_ON(val > preempt_count())) return ; if (DEBUG_LOCKS_WARN_ON((val < PREEMPT_MASK) && !(preempt_count() & PREEMPT_MASK))) return ; #endif preempt_latency_stop(val); __preempt_count_sub(val); } EXPORT_SYMBOL(preempt_count_sub); NOKPROBE_SYMBOL(preempt_count_sub);
__preempt_count_dec_and_test 抢占计数减少和测试
1 2 3 4 5 6 7 static __always_inline bool __preempt_count_dec_and_test(void ){ return !--*preempt_count_ptr() && tif_need_resched(); }
preempt_count_ptr 获取抢占计数指针 1 2 3 4 static __always_inline volatile int *preempt_count_ptr (void ) { return ¤t_thread_info()->preempt_count; }
preempt_count_dec_and_test 抢占计数减少并测试
1 2 3 4 5 6 7 #define preempt_count_dec_and_test() __preempt_count_dec_and_test() static __always_inline bool __preempt_count_dec_and_test(void ){ return !--*preempt_count_ptr() && tif_need_resched(); }
include/linux/preempt.h preemption counter 抢占计数标识 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #define PREEMPT_BITS 8 #define SOFTIRQ_BITS 8 #define HARDIRQ_BITS 4 #define NMI_BITS 4 #define PREEMPT_SHIFT 0 #define SOFTIRQ_SHIFT (PREEMPT_SHIFT + PREEMPT_BITS) #define HARDIRQ_SHIFT (SOFTIRQ_SHIFT + SOFTIRQ_BITS) #define NMI_SHIFT (HARDIRQ_SHIFT + HARDIRQ_BITS) #define __IRQ_MASK(x) ((1UL << (x))-1) #define PREEMPT_MASK (__IRQ_MASK(PREEMPT_BITS) << PREEMPT_SHIFT) #define SOFTIRQ_MASK (__IRQ_MASK(SOFTIRQ_BITS) << SOFTIRQ_SHIFT) #define HARDIRQ_MASK (__IRQ_MASK(HARDIRQ_BITS) << HARDIRQ_SHIFT) #define NMI_MASK (__IRQ_MASK(NMI_BITS) << NMI_SHIFT) #define PREEMPT_OFFSET (1UL << PREEMPT_SHIFT) #define SOFTIRQ_OFFSET (1UL << SOFTIRQ_SHIFT) #define HARDIRQ_OFFSET (1UL << HARDIRQ_SHIFT) #define NMI_OFFSET (1UL << NMI_SHIFT) #define SOFTIRQ_DISABLE_OFFSET (2 * SOFTIRQ_OFFSET) #define PREEMPT_DISABLED (PREEMPT_DISABLE_OFFSET + PREEMPT_ENABLED)
DEFINE_LOCK_GUARD_0(preempt) 1 DEFINE_LOCK_GUARD_0(preempt, preempt_disable(), preempt_enable())
PERCPU_PTR 获取percpu指针
PERCPU_PTR
是一个宏,用于将传入的指针转换为 percpu 指针类型。它使用了 __force
属性来强制转换类型,以确保编译器不会对类型进行不必要的检查。
typeof(*(__p))
是一个 GCC 扩展,用于获取指针 __p
指向的类型。这个表达式的目的是获取指针所指向的数据类型。
__kernel
是一个宏,通常用于指示内核空间的类型。它可能是一个特定于架构的宏,用于标识内核空间的数据类型。
1 2 #define PERCPU_PTR(__p) \ (typeof(*(__p)) __force __kernel *)((__force unsigned long)(__p))
per_cpu_ptr raw_cpu_ptr this_cpu_ptr 获取percpu指针 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #ifdef CONFIG_SMP #define SHIFT_PERCPU_PTR(__p, __offset) \ RELOC_HIDE(PERCPU_PTR(__p), (__offset)) #define per_cpu_ptr(ptr, cpu) \ ({ \ __verify_pcpu_ptr(ptr); \ SHIFT_PERCPU_PTR((ptr), per_cpu_offset((cpu))); \ }) #define raw_cpu_ptr(ptr) \ ({ \ __verify_pcpu_ptr(ptr); \ arch_raw_cpu_ptr(ptr); \ }) #ifdef CONFIG_DEBUG_PREEMPT #define this_cpu_ptr(ptr) \ ({ \ __verify_pcpu_ptr(ptr); \ SHIFT_PERCPU_PTR(ptr, my_cpu_offset); \ }) #else #define this_cpu_ptr(ptr) raw_cpu_ptr(ptr) #endif #else #define per_cpu_ptr(ptr, cpu) \ ({ \ (void)(cpu); \ __verify_pcpu_ptr(ptr); \ PERCPU_PTR(ptr); \ }) #define raw_cpu_ptr(ptr) per_cpu_ptr(ptr, 0) #define this_cpu_ptr(ptr) raw_cpu_ptr(ptr) #endif
nmi_count hardirq_count softirq_count irq_count 获取中断计数 1 2 3 4 5 6 7 8 9 10 11 12 #define nmi_count() (preempt_count() & NMI_MASK) #define hardirq_count() (preempt_count() & HARDIRQ_MASK) #ifdef CONFIG_PREEMPT_RT # define softirq_count() (current->softirq_disable_cnt & SOFTIRQ_MASK) # define irq_count() ((preempt_count() & (NMI_MASK | HARDIRQ_MASK)) | softirq_count()) #else # define softirq_count() (preempt_count() & SOFTIRQ_MASK) # define irq_count() (preempt_count() & (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_MASK)) #endif
in_irq in_softirq in_interrupt 检查中断状态 1 2 3 4 5 6 7 8 9 #define in_irq() (hardirq_count()) #define in_softirq() (softirq_count()) #define in_interrupt() (irq_count())
preempt_disable 禁用抢占 1 2 3 4 5 #define preempt_disable() \ do { \ preempt_count_inc(); \ barrier(); \ } while (0)
preempt_enable 启用抢占
在启用抢占时,检查是否需要进行调度
如果需要调度,则调用 __preempt_schedule()
函数
如果不需要调度,则仅仅减少抢占计数
当计数为 0 时,表示可以进行抢占,从而执行调度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #ifdef CONFIG_PREEMPTION #define preempt_enable() \ do { \ barrier(); \ if (unlikely(preempt_count_dec_and_test())) \ __preempt_schedule(); \ } while (0) #else #define preempt_enable() \ do { \ barrier(); \ preempt_count_dec(); \ } while (0) #endif
preemptible 检查是否可抢占
检查当前的抢占计数是否为 0,并且中断是否未被禁用
如果满足这两个条件,则表示当前任务是可抢占的
1 #define preemptible() (preempt_count() == 0 && !irqs_disabled())
preempt_disable_notrace 禁用抢占并停止追踪 1 2 3 4 5 #define preempt_disable_notrace() \ do { \ __preempt_count_inc(); \ barrier(); \ } while (0)
preempt_enable_no_resched_notrace 启用调度且不需要调度 1 2 3 4 5 #define preempt_enable_no_resched_notrace() \ do { \ barrier(); \ __preempt_count_dec(); \ } while (0)
kernel/sched/core.c schedule_preempt_disabled 在禁用抢占的情况下调用 1 2 3 4 5 6 7 8 9 10 11 void __sched schedule_preempt_disabled (void ) { sched_preempt_enable_no_resched(); schedule(); preempt_disable(); }
__preempt_schedule 执行调度 1 2 3 4 5 6 7 8 9 10 11 12 13 14 asmlinkage __visible void __sched notrace preempt_schedule (void ) { if (likely(!preemptible())) return ; preempt_schedule_common(); } NOKPROBE_SYMBOL(preempt_schedule); EXPORT_SYMBOL(preempt_schedule);
preempt_schedule_common 通用的抢占调度 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 static void __sched notrace preempt_schedule_common (void ) { do { preempt_disable_notrace(); preempt_latency_start(1 ); __schedule(SM_PREEMPT); preempt_latency_stop(1 ); preempt_enable_no_resched_notrace(); } while (need_resched()); }