Skip to content

第一百七十七章:读取侧临界区

大乘期

涉及内核源码:

告别RCU老者后,林小源沿着一条幽深的甬道前行。甬道两侧刻满了密密麻麻的符文,散发着微弱的蓝光。

甬道尽头是一扇巨大的石门,门楣上刻着三个古篆——"临界区"。门前站着一位面容严肃的中年修士,身穿青灰色道袍,腰悬一块令牌,上书"rcu_read_lock"。

"止步。"中年修士抬手拦住他,"要进入读取侧临界区,你必须先明白规矩。"

"什么规矩?"

中年修士指了指门内。林小源透过门缝看到,里面是一片宁静的湖泊,湖面上漂浮着无数光球,每个光球中都存储着数据。几位修士正乘着小舟在湖面上自由穿行,打捞光球查看其中的数据。

"在里面,你只能做三件事。"中年修士竖起三根手指,"第一,不能睡眠。临界区内禁止抢占,你若睡着,整个CPU都会被你阻塞。第二,不能调用任何可能睡眠的函数。第三,不能持有任何会导致睡眠的锁。"

"为什么这么严格?"

"因为宽限期在等你。"中年修士目光锐利,"外面的写入者要等所有读取者离开才能释放旧数据。你若在里面赖着不走,整个系统都会被你拖慢。"

林小源深吸一口气,跨过石门。一股清凉的气息包裹住他——他知道,自己已经进入了rcu_read_lock()的领域。在他身后,石门轻轻合上,令牌上的光芒亮了起来。

湖面上,林小源看到几位修士同时在打捞光球,彼此之间毫无干扰。

"这就是RCU读取侧的并发优势。"中年修士的声音从岸边传来,"多个读者可以同时进入临界区,互不阻塞。写入者必须等你们全部离开。"

林小源拿起一个光球,其中的数据清晰可见。他注意到,读取操作没有任何同步开销——不需要自旋,不需要原子操作,只需要在进入和离开时标记一下。

"但你怎么知道我们什么时候全部离开?"林小源问道。

"上下文切换。"中年修士答道,"当一个CPU经历了上下文切换,就意味着它离开了RCU读取侧临界区。当所有CPU都经历了这样的'静止状态',宽限期就结束了。"

林小源将光球放回湖面,心中豁然开朗。读取侧临界区的本质,就是用最轻量的方式告诉内核——我正在读取,请不要释放我正在访问的数据。

林小源离开湖泊时,中年修士递给他一块玉简。

"这是rcu_dereference()的用法。"修士说道,"读取RCU保护的指针时,必须用它。它内部包含读屏障,确保你先读到指针,再读到指针指向的数据。如果你直接解引用,编译器和CPU的重排可能导致你读到脏数据。"

"内存屏障……"林小源喃喃道。他在前面的修炼中已经接触过这个概念。

"没错。rcu_dereference不仅仅是一个指针读取——它是一个带有读屏障的指针读取。记住这个区别。"

林小源接过玉简,郑重收入怀中。读取侧临界区看似简单,实则处处是陷阱。

c
/*
 * 读取侧临界区:
 *
 * 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;
}

道藏笔记

内核启示

读取侧临界区就是 rcu_read_lock() 和 rcu_read_unlock() 之间的区域。进去之后告诉内核"我在读",出来时说"我读完了"。实现上就是禁止抢占,轻量到几乎零开销。多个读者可以同时在临界区里,互不阻塞。

但有三条铁律:不能睡眠、不能调可能睡眠的函数、不能持有会睡眠的锁。原因很简单——临界区内禁止抢占,你要是睡着了,整个 CPU 都被你卡住,外面写入者的 synchronize_rcu() 也等不到头。rcu_dereference() 读指针时带读屏障,确保先读到指针再读到数据,顺序不能反。

临界区是边界——读取在边界内完成。


破关试炼

读取侧临界区之试

RCU 读取侧临界区内,读取受保护指针并保证顺序的接口是什么?

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

以修仙之名,悟内核之道