第一百七十七章:读取侧临界区
大乘期涉及内核源码:
一
告别RCU老者后,林小源沿着一条幽深的甬道前行。甬道两侧刻满了密密麻麻的符文,散发着微弱的蓝光。
甬道尽头是一扇巨大的石门,门楣上刻着三个古篆——"临界区"。门前站着一位面容严肃的中年修士,身穿青灰色道袍,腰悬一块令牌,上书"rcu_read_lock"。
"止步。"中年修士抬手拦住他,"要进入读取侧临界区,你必须先明白规矩。"
"什么规矩?"
中年修士指了指门内。林小源透过门缝看到,里面是一片宁静的湖泊,湖面上漂浮着无数光球,每个光球中都存储着数据。几位修士正乘着小舟在湖面上自由穿行,打捞光球查看其中的数据。
"在里面,你只能做三件事。"中年修士竖起三根手指,"第一,不能睡眠。临界区内禁止抢占,你若睡着,整个CPU都会被你阻塞。第二,不能调用任何可能睡眠的函数。第三,不能持有任何会导致睡眠的锁。"
"为什么这么严格?"
"因为宽限期在等你。"中年修士目光锐利,"外面的写入者要等所有读取者离开才能释放旧数据。你若在里面赖着不走,整个系统都会被你拖慢。"
林小源深吸一口气,跨过石门。一股清凉的气息包裹住他——他知道,自己已经进入了rcu_read_lock()的领域。在他身后,石门轻轻合上,令牌上的光芒亮了起来。
二
湖面上,林小源看到几位修士同时在打捞光球,彼此之间毫无干扰。
"这就是RCU读取侧的并发优势。"中年修士的声音从岸边传来,"多个读者可以同时进入临界区,互不阻塞。写入者必须等你们全部离开。"
林小源拿起一个光球,其中的数据清晰可见。他注意到,读取操作没有任何同步开销——不需要自旋,不需要原子操作,只需要在进入和离开时标记一下。
"但你怎么知道我们什么时候全部离开?"林小源问道。
"上下文切换。"中年修士答道,"当一个CPU经历了上下文切换,就意味着它离开了RCU读取侧临界区。当所有CPU都经历了这样的'静止状态',宽限期就结束了。"
林小源将光球放回湖面,心中豁然开朗。读取侧临界区的本质,就是用最轻量的方式告诉内核——我正在读取,请不要释放我正在访问的数据。
三
林小源离开湖泊时,中年修士递给他一块玉简。
"这是rcu_dereference()的用法。"修士说道,"读取RCU保护的指针时,必须用它。它内部包含读屏障,确保你先读到指针,再读到指针指向的数据。如果你直接解引用,编译器和CPU的重排可能导致你读到脏数据。"
"内存屏障……"林小源喃喃道。他在前面的修炼中已经接触过这个概念。
"没错。rcu_dereference不仅仅是一个指针读取——它是一个带有读屏障的指针读取。记住这个区别。"
林小源接过玉简,郑重收入怀中。读取侧临界区看似简单,实则处处是陷阱。
/*
* 读取侧临界区:
*
* rcu_read_lock()
* 进入临界区
* 告诉内核: 我在读取
* 实际上是禁止抢占
*
* rcu_read_unlock()
* 离开临界区
* 告诉内核: 我读完了
* 实际上是允许抢占
*
* 临界区内的约束:
* 不能睡眠
* 不能调用可能睡眠的函数
* 不能持有导致睡眠的锁
*
* rcu_dereference():
* 读取 RCU 保护的指针
* 确保读取顺序正确
* 配合内存屏障使用
*
* 线程 1 (读取者): 线程 2 (写入者):
* rcu_read_lock() new = kmalloc()
* p = rcu_dereference() *new = *old
* 使用 p rcu_assign_pointer()
* rcu_read_unlock() synchronize_rcu()
* kfree(old)
*/
/* 模拟 RCU 保护的链表 */
struct node {
int data;
struct node *next;
};
struct node *global_head = NULL;
/* 模拟 rcu_read_lock — 禁止抢占 */
int in_rcu_read = 0;
void rcu_read_lock(void) {
in_rcu_read = 1;
}
void rcu_read_unlock(void) {
in_rcu_read = 0;
}
/* 读取临界区内的操作 */
void rcu_reader(void) {
printf("读取者:\n");
rcu_read_lock();
printf(" 进入临界区 (禁止抢占)\n");
struct node *p = global_head;
while (p) {
printf(" 读取: %d\n", p->data);
p = p->next;
}
printf(" 离开临界区 (允许抢占)\n");
rcu_read_unlock();
}
/* 写入者的操作 */
void rcu_writer(int new_data) {
printf("写入者:\n");
/* 复制 */
struct node *new = malloc(sizeof(struct node));
new->data = new_data;
new->next = global_head;
printf(" 创建新节点: %d\n", new_data);
/* 更新指针 */
struct node *old = global_head;
global_head = new;
printf(" 更新指针\n");
/* 等待宽限期 */
printf(" 等待宽限期...\n");
/* synchronize_rcu() */
/* 释放旧数据 */
if (old) {
printf(" 释放旧节点: %d\n", old->data);
free(old);
}
}
printf("=== 读取侧临界区 — RCU 的基础 ===\n\n");
/* 初始化 */
rcu_writer(1);
rcu_writer(2);
printf("\n--- 读取 ---\n");
rcu_reader();
printf("\n--- 临界区约束 ---\n");
printf("不能:\n");
printf(" 睡眠\n");
printf(" 调用可能睡眠的函数\n");
printf(" 持有导致睡眠的锁\n\n");
printf("原因:\n");
printf(" 临界区内禁止抢占\n");
printf(" 睡眠会导致死锁\n\n");
printf("--- rcu_dereference ---\n");
printf("作用:\n");
printf(" 读取 RCU 保护的指针\n");
printf(" 确保读取顺序正确\n\n");
printf("实现:\n");
printf(" 依赖内存屏障\n");
printf(" 防止编译器优化\n\n");
printf("--- 多读者场景 ---\n");
printf("线程 1: rcu_read_lock() ... rcu_read_unlock()\n");
printf("线程 2: rcu_read_lock() ... rcu_read_unlock()\n");
printf("线程 3: rcu_read_lock() ... rcu_read_unlock()\n");
printf("写入者: 等待所有读者离开\n");
/* 清理 */
struct node *p = global_head;
while (p) {
struct node *next = p->next;
free(p);
p = next;
}#include <stdio.h>
#include <stdlib.h>
/*
* 读取侧临界区:
*
* rcu_read_lock()
* 进入临界区
* 告诉内核: 我在读取
* 实际上是禁止抢占
*
* rcu_read_unlock()
* 离开临界区
* 告诉内核: 我读完了
* 实际上是允许抢占
*
* 临界区内的约束:
* 不能睡眠
* 不能调用可能睡眠的函数
* 不能持有导致睡眠的锁
*
* rcu_dereference():
* 读取 RCU 保护的指针
* 确保读取顺序正确
* 配合内存屏障使用
*
* 线程 1 (读取者): 线程 2 (写入者):
* rcu_read_lock() new = kmalloc()
* p = rcu_dereference() *new = *old
* 使用 p rcu_assign_pointer()
* rcu_read_unlock() synchronize_rcu()
* kfree(old)
*/
/* 模拟 RCU 保护的链表 */
struct node {
int data;
struct node *next;
};
struct node *global_head = NULL;
/* 模拟 rcu_read_lock — 禁止抢占 */
int in_rcu_read = 0;
void rcu_read_lock(void) {
in_rcu_read = 1;
}
void rcu_read_unlock(void) {
in_rcu_read = 0;
}
/* 读取临界区内的操作 */
void rcu_reader(void) {
printf("读取者:\n");
rcu_read_lock();
printf(" 进入临界区 (禁止抢占)\n");
struct node *p = global_head;
while (p) {
printf(" 读取: %d\n", p->data);
p = p->next;
}
printf(" 离开临界区 (允许抢占)\n");
rcu_read_unlock();
}
/* 写入者的操作 */
void rcu_writer(int new_data) {
printf("写入者:\n");
/* 复制 */
struct node *new = malloc(sizeof(struct node));
new->data = new_data;
new->next = global_head;
printf(" 创建新节点: %d\n", new_data);
/* 更新指针 */
struct node *old = global_head;
global_head = new;
printf(" 更新指针\n");
/* 等待宽限期 */
printf(" 等待宽限期...\n");
/* synchronize_rcu() */
/* 释放旧数据 */
if (old) {
printf(" 释放旧节点: %d\n", old->data);
free(old);
}
}
int main() {
printf("=== 读取侧临界区 — RCU 的基础 ===\n\n");
/* 初始化 */
rcu_writer(1);
rcu_writer(2);
printf("\n--- 读取 ---\n");
rcu_reader();
printf("\n--- 临界区约束 ---\n");
printf("不能:\n");
printf(" 睡眠\n");
printf(" 调用可能睡眠的函数\n");
printf(" 持有导致睡眠的锁\n\n");
printf("原因:\n");
printf(" 临界区内禁止抢占\n");
printf(" 睡眠会导致死锁\n\n");
printf("--- rcu_dereference ---\n");
printf("作用:\n");
printf(" 读取 RCU 保护的指针\n");
printf(" 确保读取顺序正确\n\n");
printf("实现:\n");
printf(" 依赖内存屏障\n");
printf(" 防止编译器优化\n\n");
printf("--- 多读者场景 ---\n");
printf("线程 1: rcu_read_lock() ... rcu_read_unlock()\n");
printf("线程 2: rcu_read_lock() ... rcu_read_unlock()\n");
printf("线程 3: rcu_read_lock() ... rcu_read_unlock()\n");
printf("写入者: 等待所有读者离开\n");
/* 清理 */
struct node *p = global_head;
while (p) {
struct node *next = p->next;
free(p);
p = next;
}
return 0;
}道藏笔记
内核启示
读取侧临界区就是 rcu_read_lock() 和 rcu_read_unlock() 之间的区域。进去之后告诉内核"我在读",出来时说"我读完了"。实现上就是禁止抢占,轻量到几乎零开销。多个读者可以同时在临界区里,互不阻塞。
但有三条铁律:不能睡眠、不能调可能睡眠的函数、不能持有会睡眠的锁。原因很简单——临界区内禁止抢占,你要是睡着了,整个 CPU 都被你卡住,外面写入者的 synchronize_rcu() 也等不到头。rcu_dereference() 读指针时带读屏障,确保先读到指针再读到数据,顺序不能反。
临界区是边界——读取在边界内完成。
读取侧临界区之试
RCU 读取侧临界区内,读取受保护指针并保证顺序的接口是什么?