第一百七十八章:宽限期
大乘期涉及内核源码:
一
离开读取侧临界区后,林小源来到一片广阔的荒原。荒原上空无一人,只有一座巨大的沙漏矗立在中央,沙漏中的细沙正在缓缓流淌。
沙漏旁坐着一位身披金色斗篷的女子,面容宁静,双眼微闭。
"前辈是……"
"宽限期。"女子睁开眼,声音如清泉般平静,"Grace Period。我是RCU最核心的存在——没有我,旧数据就无法安全释放。"
林小源看着沙漏中流淌的细沙。"宽限期……就是等待的时间?"
"不仅仅是等待。"宽限期站起身,走到沙漏旁,"看——当写入者调用rcu_assign_pointer()更新指针的那一刻,沙漏就开始倒计时。在那之前已经进入读取侧临界区的所有读者,必须全部离开。只有当最后一个读者也离开了,沙漏流尽,我才会允许写入者释放旧数据。"
"如果有读者迟迟不离开呢?"
宽限期目光一冷。"那我就一直等。这就是为什么读取侧临界区不能睡眠——你若在里面睡着,整个宽限期都会被你阻塞,后面的写入者全部排队等待。"
二
"等待宽限期有两种方式。"宽限期从袖中取出两枚令牌,一枚银色,一枚铜色。
"银色的是synchronize_rcu()——同步等待。调用它之后,当前线程会阻塞,直到宽限期结束。简单可靠,但你什么都做不了,只能干等。适合在进程上下文中使用。"
"铜色的是call_rcu()——异步等待。你注册一个回调函数,然后继续做自己的事。宽限期结束后,内核会自动调用你的回调。适合在中断上下文中使用,因为中断不能睡眠。"
林小源接过两枚令牌,感受到它们不同的温度——银色的冰凉沉静,铜色的温暖跳动。
"那内核怎么知道宽限期结束了?"林小源追问。
"静止状态。"宽限期指向远方,"每个CPU都有自己的运行状态。当一个CPU经历了上下文切换,或者进入了空闲状态,它就经历了一次'静止状态'——说明它已经不在任何RCU读取侧临界区中了。当所有CPU都经历过静止状态,宽限期就结束了。"
三
林小源站在沙漏旁,看着最后一粒细沙落下。
"宽限期前辈,synchronize_rcu()可能会等多久?"
"毫秒级。"宽限期答道,"通常不会太久,但如果某个读取者的临界区很长,或者系统负载很重,等待时间会增加。这就是为什么我们有call_rcu()——你不想等,就注册回调,让内核替你等。"
她顿了顿,补充道:"但call_rcu()也有代价。回调函数会在软中断上下文中执行,所以回调函数本身也不能睡眠。而且,如果你注册了太多回调,内核需要处理的回调队列会很长,影响性能。"
林小源将两枚令牌收好。宽限期的哲学很简单——耐心等待,直到安全。但这份耐心背后,是对每一个读取者的尊重。
/*
* 宽限期 (Grace Period):
*
* 定义:
* 从 rcu_assign_pointer() 到可以 free() 的时间段
* 等待所有在更新前进入临界区的读者离开
*
* 时间线:
* T1: 写入者更新指针
* T2: 读者 A 进入临界区
* T3: 读者 B 进入临界区
* T4: 读者 A 离开临界区
* T5: 读者 B 离开临界区
* T6: 宽限期结束 (所有在 T1 前进入的读者已离开)
* T7: 写入者可以释放旧数据
*
* synchronize_rcu():
* 阻塞等待宽限期结束
* 不能在中断上下文使用
*
* call_rcu():
* 异步等待宽限期
* 注册回调函数
* 可以在中断上下文使用
*
* 内核实现:
* 检测上下文切换
* 每个 CPU 都经历过静止状态
* 宽限期结束
*/
/* 模拟宽限期 */
struct grace_period {
int started;
int completed;
int readers_waiting; /* 仍在临界区的读者数 */
};
void gp_init(struct grace_period *gp) {
gp->started = 1;
gp->completed = 0;
gp->readers_waiting = 0;
}
void gp_reader_enter(struct grace_period *gp) {
gp->readers_waiting++;
}
void gp_reader_leave(struct grace_period *gp) {
gp->readers_waiting--;
if (gp->readers_waiting == 0 && gp->started) {
gp->completed = 1;
}
}
int gp_is_complete(struct grace_period *gp) {
return gp->completed;
}
printf("=== 宽限期 — RCU 的核心 ===\n\n");
struct grace_period gp;
gp_init(&gp);
printf("--- 时间线 ---\n");
printf("T1: 写入者更新指针\n");
gp_reader_enter(&gp); /* 读者进入 */
printf("T2: 读者进入临界区\n");
printf("T3: 写入者等待宽限期...\n");
printf(" 宽限期完成? %s\n", gp_is_complete(&gp) ? "是" : "否");
gp_reader_leave(&gp); /* 读者离开 */
printf("T4: 读者离开临界区\n");
printf(" 宽限期完成? %s\n", gp_is_complete(&gp) ? "是" : "否");
printf("\n--- synchronize_rcu ---\n");
printf("特点:\n");
printf(" 阻塞等待\n");
printf(" 不能在中断上下文使用\n");
printf(" 简单可靠\n\n");
printf("用法:\n");
printf(" 更新指针\n");
printf(" synchronize_rcu()\n");
printf(" 释放旧数据\n\n");
printf("--- call_rcu ---\n");
printf("特点:\n");
printf(" 异步等待\n");
printf(" 可以在中断上下文使用\n");
printf(" 回调函数\n\n");
printf("用法:\n");
printf(" 更新指针\n");
printf(" call_rcu(&head, callback)\n");
printf(" // 宽限期后自动调用 callback\n\n");
printf("--- 宽限期检测 ---\n");
printf("方法:\n");
printf(" 检测上下文切换\n");
printf(" 每个 CPU 经历静止状态\n\n");
printf("静止状态:\n");
printf(" CPU 不在 RCU 读取临界区\n");
printf(" 上下文切换、空闲等\n\n");
printf("--- 性能影响 ---\n");
printf("synchronize_rcu:\n");
printf(" 可能阻塞毫秒级\n");
printf(" 适合进程上下文\n\n");
printf("call_rcu:\n");
printf(" 不阻塞\n");
printf(" 适合中断上下文\n");#include <stdio.h>
#include <stdlib.h>
/*
* 宽限期 (Grace Period):
*
* 定义:
* 从 rcu_assign_pointer() 到可以 free() 的时间段
* 等待所有在更新前进入临界区的读者离开
*
* 时间线:
* T1: 写入者更新指针
* T2: 读者 A 进入临界区
* T3: 读者 B 进入临界区
* T4: 读者 A 离开临界区
* T5: 读者 B 离开临界区
* T6: 宽限期结束 (所有在 T1 前进入的读者已离开)
* T7: 写入者可以释放旧数据
*
* synchronize_rcu():
* 阻塞等待宽限期结束
* 不能在中断上下文使用
*
* call_rcu():
* 异步等待宽限期
* 注册回调函数
* 可以在中断上下文使用
*
* 内核实现:
* 检测上下文切换
* 每个 CPU 都经历过静止状态
* 宽限期结束
*/
/* 模拟宽限期 */
struct grace_period {
int started;
int completed;
int readers_waiting; /* 仍在临界区的读者数 */
};
void gp_init(struct grace_period *gp) {
gp->started = 1;
gp->completed = 0;
gp->readers_waiting = 0;
}
void gp_reader_enter(struct grace_period *gp) {
gp->readers_waiting++;
}
void gp_reader_leave(struct grace_period *gp) {
gp->readers_waiting--;
if (gp->readers_waiting == 0 && gp->started) {
gp->completed = 1;
}
}
int gp_is_complete(struct grace_period *gp) {
return gp->completed;
}
int main() {
printf("=== 宽限期 — RCU 的核心 ===\n\n");
struct grace_period gp;
gp_init(&gp);
printf("--- 时间线 ---\n");
printf("T1: 写入者更新指针\n");
gp_reader_enter(&gp); /* 读者进入 */
printf("T2: 读者进入临界区\n");
printf("T3: 写入者等待宽限期...\n");
printf(" 宽限期完成? %s\n", gp_is_complete(&gp) ? "是" : "否");
gp_reader_leave(&gp); /* 读者离开 */
printf("T4: 读者离开临界区\n");
printf(" 宽限期完成? %s\n", gp_is_complete(&gp) ? "是" : "否");
printf("\n--- synchronize_rcu ---\n");
printf("特点:\n");
printf(" 阻塞等待\n");
printf(" 不能在中断上下文使用\n");
printf(" 简单可靠\n\n");
printf("用法:\n");
printf(" 更新指针\n");
printf(" synchronize_rcu()\n");
printf(" 释放旧数据\n\n");
printf("--- call_rcu ---\n");
printf("特点:\n");
printf(" 异步等待\n");
printf(" 可以在中断上下文使用\n");
printf(" 回调函数\n\n");
printf("用法:\n");
printf(" 更新指针\n");
printf(" call_rcu(&head, callback)\n");
printf(" // 宽限期后自动调用 callback\n\n");
printf("--- 宽限期检测 ---\n");
printf("方法:\n");
printf(" 检测上下文切换\n");
printf(" 每个 CPU 经历静止状态\n\n");
printf("静止状态:\n");
printf(" CPU 不在 RCU 读取临界区\n");
printf(" 上下文切换、空闲等\n\n");
printf("--- 性能影响 ---\n");
printf("synchronize_rcu:\n");
printf(" 可能阻塞毫秒级\n");
printf(" 适合进程上下文\n\n");
printf("call_rcu:\n");
printf(" 不阻塞\n");
printf(" 适合中断上下文\n");
return 0;
}道藏笔记
内核启示
宽限期就是从写入者更新指针到能安全释放旧数据的那段时间。核心逻辑:在更新指针前就已经进入临界区的所有读者,必须全部出来,宽限期才算结束。内核通过检测"静止状态"来判断——每个 CPU 经历上下文切换或进入空闲,说明它不在任何 RCU 临界区里了,所有 CPU 都经历过,宽限期结束。
两种等待方式:synchronize_rcu() 同步阻塞(简单但你干等着,不能在中断上下文用),call_rcu() 异步注册回调(不阻塞,宽限期后内核自动调你的回调函数,中断上下文也能用)。通常毫秒级,但读者临界区太长或系统负载重时会更久。
宽限期是保障——等待读者离开,安全释放旧数据。
宽限期之试
宽限期一章中,注册回调、等宽限期结束后异步释放旧对象的接口是什么?