Skip to content

第五十一章:抢占

结丹圆满

涉及内核源码:

林小源在调度竞技场中,目睹了一场"突袭"。

一个进程正在 CPU 上运行,处理着一个计算密集的任务。突然,一个更高优先级的进程从沉睡中醒来——它需要立即获得 CPU。但当前进程还在运行,它没有调用 schedule(),也没有睡眠。怎么办?

答案是:抢占

一个身穿黑色铠甲的战士从阴影中走出。他的铠甲上刻着一个标志:TIF_NEED_RESCHED。他的眼神冷酷而果断,手中的长剑闪烁着寒光。

"我是抢占,"他说,"调度器的强制执行者。当调度器决定需要重新调度时,我负责让当前进程让出 CPU——不管它愿不愿意。"

林小源看着战士的动作。只见他举起长剑,在空中划出一道弧线。剑光落在当前进程身上,进程瞬间凝固——它的寄存器被保存,CPU 被释放。

"这是怎么做到的?"林小源问。

"时机,"战士说,"我不能在任意时刻打断进程——那样会导致数据不一致。我只在'安全点'检查是否需要抢占。安全点有两个:从内核态返回用户态时,以及从中断返回时。"

林小源跟着战士走过一条狭窄的通道。通道的墙壁上刻满了代码——那是 preempt_count 的逻辑。

"内核态抢占是什么?"林小源问。

战士停下脚步,指着墙壁上的代码:"在没有内核抢占的系统中,进程在内核态运行时不会被抢占——它必须主动让出 CPU。这保证了内核代码的简单性,但可能导致高延迟。"

他用手指在代码上划过:"内核抢占允许进程在内核态运行时被抢占——只要它不在临界区中。preempt_count 是一个计数器,记录进程在内核态中'不可抢占'的深度。"

林小源看着代码,问道:"preempt_count 怎么变化?"

"每次进入临界区——获取锁、关闭中断——preempt_count 加一,"战士说,"每次退出临界区,preempt_count 减一。只有当 preempt_count 等于零时,进程才能被抢占。"

战士在空中画了一幅图:一个进程在内核态运行,preempt_count 为零。时钟中断发生,scheduler_tick() 设置 TIF_NEED_RESCHED。中断返回时,检查 TIF_NEED_RESCHED 和 preempt_count——两者都满足,调用 schedule(),进程被抢占。

"如果 preempt_count 不为零呢?"林小源问。

"那就不能抢占,"战士说,"进程在临界区中,抢占它可能导致死锁、数据不一致。安全第一。"

林小源坐在通道的尽头,看着战士擦拭他的长剑。

"前辈,内核抢占有几种配置?"他问。

战士竖起三根手指:"三种。CONFIG_PREEMPT_NONE——不支持内核态抢占,进程在内核态运行直到主动让出,吞吐量优先,适合服务器。CONFIG_PREEMPT_VOLUNTARY——支持自愿内核态抢占,在显式的抢占点检查,平衡吞吐量和延迟,适合桌面系统。CONFIG_PREEMPT——支持完全内核态抢占,在任何可抢占的点检查,延迟优先,适合实时系统。"

"所以抢占和简单性是矛盾的?"

"没错,"战士说,"内核抢占降低了延迟,但增加了内核代码的复杂性。每个临界区都必须正确管理 preempt_count,每个锁都必须考虑抢占的影响。这就是为什么 CONFIG_PREEMPT_NONE 仍然存在——有些场景不需要低延迟,但需要简单性。"

林小源看着战士的长剑,心中感慨。抢占是调度器的强制执行机制——它让调度器的决定不只停留在纸面上,而是真正被执行。但这种强制力需要精确的控制,否则会带来灾难。

"抢占是延迟和吞吐量的权衡,"林小源说,"选择哪种配置,取决于你更看重什么。"

战士露出一丝赞许的微笑:"你理解了。"


c
/*
 * 抢占的类型:
 *
 * 1. 用户态抢占
 *    进程从内核态返回用户态时检查
 *    TIF_NEED_RESCHED 标志
 *    如果被设置,调用 schedule()
 *
 * 2. 内核态抢占
 *    进程在内核态运行时也可以被抢占
 *    需要 CONFIG_PREEMPT 配置
 *    在 preempt_count == 0 时可以抢占
 *
 * 抢占的触发条件:
 *   - 时钟中断 → scheduler_tick() → 设置 TIF_NEED_RESCHED
 *   - 唤醒抢占 → try_to_wake_up() → 设置 TIF_NEED_RESCHED
 *   - 优先级变化 → 设置 TIF_NEED_RESCHED
 *
 * 抢占的检查点:
 *   - 从内核态返回用户态
 *   - 从中断返回
 *   - 内核态的 preempt_enable() 点
 */

#define TIF_NEED_RESCHED 0

struct thread_info {
    int flags;
    int preempt_count;
};

int test_tif(struct thread_info *ti, int bit) {
    return (ti->flags >> bit) & 1;
}

void schedule(void) {
    printf("  [schedule] 选择下一个进程,执行上下文切换\n");
}

printf("=== 抢占 — 强制让出 ===\n\n");

struct thread_info ti = { .flags = 0, .preempt_count = 0 };

/* 场景 1: 用户态抢占 */
printf("场景 1: 用户态抢占\n");
ti.flags |= (1 << TIF_NEED_RESCHED);
printf("  TIF_NEED_RESCHED = %d\n", test_tif(&ti, TIF_NEED_RESCHED));
printf("  进程从内核态返回用户态时检查...\n");
if (test_tif(&ti, TIF_NEED_RESCHED)) {
    printf("  需要重新调度!\n");
    schedule();
}
printf("\n");

/* 场景 2: 内核态抢占 */
printf("场景 2: 内核态抢占\n");
ti.flags = (1 << TIF_NEED_RESCHED);
ti.preempt_count = 1;  /* 在临界区中 */
printf("  TIF_NEED_RESCHED = %d\n", test_tif(&ti, TIF_NEED_RESCHED));
printf("  preempt_count = %d\n", ti.preempt_count);
printf("  在临界区中,不能抢占\n\n");

/* 退出临界区 */
ti.preempt_count = 0;
printf("  退出临界区\n");
printf("  preempt_count = %d\n", ti.preempt_count);
if (test_tif(&ti, TIF_NEED_RESCHED) && ti.preempt_count == 0) {
    printf("  可以抢占!\n");
    schedule();
}

printf("\n--- 内核抢占的配置 ---\n");
printf("CONFIG_PREEMPT_NONE:\n");
printf("  不支持内核态抢占\n");
printf("  进程在内核态运行直到主动让出\n");
printf("  吞吐量优先\n\n");
printf("CONFIG_PREEMPT_VOLUNTARY:\n");
printf("  支持自愿内核态抢占\n");
printf("  在显式的抢占点检查\n");
printf("  平衡吞吐量和延迟\n\n");
printf("CONFIG_PREEMPT:\n");
printf("  支持完全内核态抢占\n");
printf("  在任何可抢占的点检查\n");
printf("  延迟优先\n");

道藏笔记

内核启示

抢占是调度的"强制执行"机制。

抢占的类型:

  1. 用户态抢占 — 从内核态返回用户态时检查
  2. 内核态抢占 — 在内核态运行时也可以被抢占

抢占的触发条件:

  • 时钟中断 → scheduler_tick() → 设置
  • 唤醒抢占 → → 设置
  • 优先级变化 → 设置

内核抢占的配置:

  • — 不支持内核态抢占,吞吐量优先
  • — 自愿内核态抢占,平衡
  • — 完全内核态抢占,延迟优先

的作用:

  • 记录进程在内核态中"不可抢占"的深度
  • 临界区中 preempt_count > 0,不能被抢占
  • 退出临界区时 减 1,可能触发抢占

抢占是"延迟"和"吞吐量"的权衡。


破关试炼

抢占之试

本章讲抢占标记时,哪个 thread flag 会提示当前任务需要重新调度?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道