详细介绍:[Linux]学习笔记系列 -- [kernel][sched]preempt
title: preempt
categories:
- linux
- kernel
- sched
tags: - linux
- kernel
- sched
abbrlink: 529cc833
date: 2025-10-03 09:01:49
文章目录
- include/linux/preempt.h 内核抢占(Kernel Preemption) 控制内核代码的可抢占性与延迟
- include/asm-generic/preempt.h
- include/linux/preempt.h
- preemption counter 抢占计数标识
- DEFINE_LOCK_GUARD_0(preempt)
- PERCPU_PTR 获取percpu指针
- per_cpu_ptr raw_cpu_ptr this_cpu_ptr 获取percpu指针
- nmi_count hardirq_count softirq_count irq_count 获取中断计数
- in_irq in_softirq in_interrupt 检查中断状态
- preempt_disable 禁用抢占
- preempt_enable 启用抢占
- preemptible 检查是否可抢占
- preempt_disable_notrace 禁用抢占并停止追踪
- preempt_enable_no_resched_notrace 启用调度且不需要调度
- kernel/sched/core.c

https://github.com/wdfk-prog/linux-study
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_RTpatchset)引入,现在绝大部分已合入主线内核。它通过将内核中大部分的自旋锁替换为可睡眠的、支持优先级继承的互斥锁,使得内核在几乎任何地方(除了极少数真正的临界区)都是可抢占的。这极大地降低了内核内部的调度延迟,是构建硬实时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 抢占计数
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 的值,例如读取或修改。
数据一致性和原子性需要在使用指针时由调用者负责,而不是在返回指针时处理。
- 调用者通过返回的指针可以直接操作 preempt_count 的值,例如读取或修改。
- 避免额外开销:
- READ_ONCE 是为防止编译器优化和重排序而设计的,但在返回指针的场景中,这种保护是多余的,因为指针本身不会被并发修改
static __always_inline volatile int *preempt_count_ptr(void)
{
return ¤t_thread_info()->preempt_count;
}
/*
* The various preempt_count add/sub methods
*/
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;
}
//检查当前cpu的抢占计数下溢
//检查抢占计数即将上溢
void preempt_count_add(int val)
{
#ifdef CONFIG_DEBUG_PREEMPT
/*
* Underflow?
*/
if (DEBUG_LOCKS_WARN_ON((preempt_count() < 0)))
return;
#endif
__preempt_count_add(val);
#ifdef CONFIG_DEBUG_PREEMPT
/*
* Spinlock count overflowing soon?
*/
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
/*
* Underflow?
*/
if (DEBUG_LOCKS_WARN_ON(val > preempt_count()))
return;
/*
* Is the spinlock portion underflowing?
*/
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 抢占计数减少和测试
- 返回是否可以调度.计数减少到0且允许调度返回1
static __always_inline bool __preempt_count_dec_and_test(void)
{
/* 由于 load-store 架构无法执行每 cpu 的原子作;我们不能使用 PREEMPT_NEED_RESCHED因为它可能会丢失。
*/
return !--*preempt_count_ptr() //抢占计数减少
&& tif_need_resched(); //设置需要调度bit位
}
preempt_count_ptr 获取抢占计数指针
static __always_inline volatile int *preempt_count_ptr(void)
{
return ¤t_thread_info()->preempt_count;
}
preempt_count_dec_and_test 抢占计数减少并测试
- 函数用于减少抢占计数并测试是否需要进行调度
#define preempt_count_dec_and_test() __preempt_count_dec_and_test()
static __always_inline bool __preempt_count_dec_and_test(void)
{
/* * 由于负载-存储架构无法进行每个 CPU 的原子操作;我们无法使用 PREEMPT_NEED_RESCHED,因为它可能会丢失。 */
return !--*preempt_count_ptr() && tif_need_resched();
}
include/linux/preempt.h
preemption counter 抢占计数标识
/*
* 我们将 hardirq 和 softirq 计数器放入抢占中
*计数器。位掩码具有以下含义:
*
* - 位 0-7 是抢占计数(最大抢占深度:256)
* - 第 8-15 位是软中断计数(软中断的最大 # 值:256)
*
* 理论上 hardirq 计数可以与
* 中断,但我们使用
* interrupts disabled,因此我们不能有嵌套中断。虽然
* 有一些 Palaeontologic 驱动程序可以重新启用
* 处理程序,所以我们在这里需要不止一个位。
*
* PREEMPT_MASK:0x000000ff
* SOFTIRQ_MASK:0x0000ff00
* HARDIRQ_MASK:0x000f0000
* NMI_MASK:0x00f00000
* PREEMPT_NEED_RESCHED:0x80000000
*/
#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)
DEFINE_LOCK_GUARD_0(preempt, preempt_disable(), preempt_enable())
PERCPU_PTR 获取percpu指针
PERCPU_PTR是一个宏,用于将传入的指针转换为 percpu 指针类型。它使用了__force属性来强制转换类型,以确保编译器不会对类型进行不必要的检查。typeof(*(__p))是一个 GCC 扩展,用于获取指针__p指向的类型。这个表达式的目的是获取指针所指向的数据类型。__kernel是一个宏,通常用于指示内核空间的类型。它可能是一个特定于架构的宏,用于标识内核空间的数据类型。
#define PERCPU_PTR(__p) \
(typeof(*(__p)) __force __kernel *)((__force unsigned long)(__p))
per_cpu_ptr raw_cpu_ptr this_cpu_ptr 获取percpu指针
#ifdef CONFIG_SMP
/*
* Add an offset to a pointer. Use RELOC_HIDE() to prevent the compiler
* from making incorrect assumptions about the pointer value.
*/
#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 /* CONFIG_SMP */
#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 /* CONFIG_SMP */
nmi_count hardirq_count softirq_count irq_count 获取中断计数
/*
* 这些宏定义避免了 preempt_count() 的冗余调用,因为鉴于 preempt_count() 通常使用 READ_ONCE() 实现,此类调用会导致冗余加载。
*/
#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 检查中断状态
/*
* 以下宏已弃用,不应在新代码中使用:
* in_irq() - in_hardirq() 的过时版本
* in_softirq() - 我们禁用了 BH,或者正在处理软中断
* in_interrupt() - 我们处于 NMI、IRQ、SoftIRQ 上下文中或禁用了 BH
*/
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())
preempt_disable 禁用抢占
#define preempt_disable() \
do { \
preempt_count_inc(); \
barrier(); \
} while (0)
preempt_enable 启用抢占
- 在启用抢占时,检查是否需要进行调度
- 如果需要调度,则调用
__preempt_schedule()函数 - 如果不需要调度,则仅仅减少抢占计数
- 当计数为 0 时,表示可以进行抢占,从而执行调度
#ifdef CONFIG_PREEMPTION
#define preempt_enable() \
do { \
barrier(); \
if (unlikely(preempt_count_dec_and_test())) \
__preempt_schedule(); \
} while (0)
#else /* !CONFIG_PREEMPTION */
#define preempt_enable() \
do { \
barrier(); \
preempt_count_dec(); \
} while (0)
#endif /* CONFIG_PREEMPTION */
preemptible 检查是否可抢占
- 检查当前的抢占计数是否为 0,并且中断是否未被禁用
- 如果满足这两个条件,则表示当前任务是可抢占的
#define preemptible() (preempt_count() == 0 && !irqs_disabled())
preempt_disable_notrace 禁用抢占并停止追踪
#define preempt_disable_notrace() \
do { \
__preempt_count_inc(); \
barrier(); \
} while (0)
preempt_enable_no_resched_notrace 启用调度且不需要调度
#define preempt_enable_no_resched_notrace() \
do { \
barrier(); \
__preempt_count_dec(); \
} while (0)
kernel/sched/core.c
schedule_preempt_disabled 在禁用抢占的情况下调用
/**
* schedule_preempt_disabled - 在禁用抢占的情况下调用
*
* 禁用抢占的情况下返回。注意:preempt_count 必须为 1
*/
void __sched schedule_preempt_disabled(void)
{
sched_preempt_enable_no_resched();
schedule();
preempt_disable();
}
__preempt_schedule 执行调度
/*
* This is the entry point to schedule() from in-kernel preemption
* off of preempt_enable.
*/
asmlinkage __visible void __sched notrace preempt_schedule(void)
{
/* 如果 preempt_count 不为零或中断被禁用,
* 我们不希望抢占当前任务。就直接返回。 */
if (likely(!preemptible()))
return;
preempt_schedule_common();
}
NOKPROBE_SYMBOL(preempt_schedule);
EXPORT_SYMBOL(preempt_schedule);
preempt_schedule_common 通用的抢占调度
/*
* __sched: 告诉编译器这个函数会调用调度器,可能会导致上下文切换。
* notrace: 告诉内核追踪器(ftrace),不要追踪这个函数的入口和出口。
* 这是为了防止无限递归,因为本函数自身就是抢占路径的一部分。
*/
static void __sched notrace preempt_schedule_common(void)
{
/*
* 使用do-while循环,确保在退出前,所有的抢占请求都被处理完毕。
*/
do {
/*
* 注释解释了一个复杂的问题:
* 因为函数追踪器(ftrace)可能会追踪preempt_count_sub()这类函数,
* 而ftrace本身也需要调用preempt_enable/disable_notrace()来保护自己。
* 如果NEED_RESCHED标志被设置,那么ftrace调用的preempt_enable_notrace()
* 可能会再次调用本函数,从而导致无限递归。
*
* 为了解决这个问题,必须在ftrace可能开始追踪之前,就先禁用抢占。
* 因此,将preempt_disable()拆分为两个调用:
* 1. preempt_disable_notrace(): 先禁用抢占,这个操作本身不会被追踪。
* 2. preempt_latency_start(): 记录抢占延迟,这个操作可以被追踪。
*/
/* 增加抢占计数值,禁用抢占,并且这个操作本身不被ftrace追踪。*/
preempt_disable_notrace();
/* 开始记录抢占延迟,用于实时性分析。参数1表示这是一个抢占延迟。*/
preempt_latency_start(1);
/*
* 调用核心调度函数__schedule()。
* SM_PREEMPT参数告诉调度器,这是一次由内核抢占触发的调度。
* 这个函数执行完毕返回时,CPU可能已经运行了其他任务,然后才切换回来。
*/
__schedule(SM_PREEMPT);
/* 停止记录抢占延迟。*/
preempt_latency_stop(1);
/*
* 减少抢占计数值,但不检查调度请求。
* 因为我们马上就要在while循环中检查了,这里不能再次触发调度。
* 同样,这个操作本身不被ftrace追踪。
*/
preempt_enable_no_resched_notrace();
/*
* 注释:再次检查,以防在schedule返回和现在之间,我们错过了
* 一次抢占机会。
*/
} while (need_resched()); /* 只要need_resched()为真,就一直循环。*/
/* static __always_inline bool need_resched(void)
{
return unlikely(tif_need_resched());
}
*/
}

浙公网安备 33010602011771号