第五十一章:抢占
结丹圆满涉及内核源码:
一
林小源在调度竞技场中,目睹了一场"突袭"。
一个进程正在 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 仍然存在——有些场景不需要低延迟,但需要简单性。"
林小源看着战士的长剑,心中感慨。抢占是调度器的强制执行机制——它让调度器的决定不只停留在纸面上,而是真正被执行。但这种强制力需要精确的控制,否则会带来灾难。
"抢占是延迟和吞吐量的权衡,"林小源说,"选择哪种配置,取决于你更看重什么。"
战士露出一丝赞许的微笑:"你理解了。"
/*
* 抢占的类型:
*
* 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");#include <stdio.h>
/*
* 抢占的类型:
*
* 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");
}
int main() {
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");
return 0;
}道藏笔记
内核启示
抢占是调度的"强制执行"机制。
抢占的类型:
- 用户态抢占 — 从内核态返回用户态时检查
- 内核态抢占 — 在内核态运行时也可以被抢占
抢占的触发条件:
- 时钟中断 →
scheduler_tick()→ 设置 - 唤醒抢占 → → 设置
- 优先级变化 → 设置
内核抢占的配置:
- — 不支持内核态抢占,吞吐量优先
- — 自愿内核态抢占,平衡
- — 完全内核态抢占,延迟优先
的作用:
- 记录进程在内核态中"不可抢占"的深度
- 临界区中
preempt_count > 0,不能被抢占 - 退出临界区时 减 1,可能触发抢占
抢占是"延迟"和"吞吐量"的权衡。
抢占之试
本章讲抢占标记时,哪个 thread flag 会提示当前任务需要重新调度?