内核同步

点击跳转到内核课程所有目录

点击这里查看配套的教学视频

1 简介

内核线程、中断处理程序、陷入内核的用户进程,称为执行线程(threads of execution),多个执行线程并发访问共享资源,会造成数据不一致。

很多年前,只支持单核处理器的时代,避免并发访问数据很简单,只有中断发生时和重新调度执行另一个进程时,才会有并发访问数据的可能。

现在内核支持对称多处理器,数据可能同时被多个处理器同时访问。在支持抢占后,调度器还可能在任何时刻抢占正在运行的代码,重新调度执行其他进程。

避免并发和防止竞争条件称为同步(synchronization)。 这里介绍几个概念:

2 rcu函数接口

/**
 * rcu_assign_pointer() - 分配给RCU保护的指针
 * @p: 要赋值的指针
 * @v: 要赋值的内容 (publish)
 *
 * 将指定的值分配给指定的RCU保护指针,确保任何并发的RCU读操作能够看到
 * 任何先前的初始化。
 *
 * 在需要内存屏障的架构上插入内存屏障(大多数架构都需要),并且还防止编译器重新排序
 * 在指针赋值之后初始化结构的代码。更重要的是,这个调用文档化了哪些指针
 * 会被RCU读操作代码解引用。
 *
 * 在某些特殊情况下,你可以使用RCU_INIT_POINTER()来替代
 * rcu_assign_pointer()。RCU_INIT_POINTER()稍微快一些,因为
 * 它不会对CPU或编译器进行约束。尽管如此,当你应该使用
 * rcu_assign_pointer()时使用RCU_INIT_POINTER()是非常糟糕的事情,
 * 会导致无法诊断的内存损坏。所以请小心使用。
 * 请参见RCU_INIT_POINTER()的注释头部了解更多细节。
 *
 * 注意,rcu_assign_pointer()仅对每个参数进行一次求值,不论其出现次数。
 * 其中一个“额外”的求值在typeof()中,另一个只对sparse (__CHECKER__)可见,
 * 它们实际上并不会执行该参数。像大多数cpp宏一样,这种仅执行一次参数的属性
 * 很重要,因此在修改rcu_assign_pointer()和它调用的其他宏时,请小心。
 */
#define rcu_assign_pointer(p, v)

/**
 * rcu_dereference_protected() - 在"更新被防止时"获取RCU指针
 * @p: 要读取的指针,在解引用之前
 * @c: 解引用发生的条件
 *
 * 返回指定的RCU保护指针的值,但省略了READ_ONCE()。
 * 这在更新端锁防止指针值改变的情况下很有用。请注意,这个原语并不会防止编译器重复引用该指针
 * 或者将其与其他引用合并,因此在没有适当锁保护的情况下不应使用。
 *
 * 该函数仅供更新端使用。仅由rcu_read_lock()保护时使用此函数将导致不频繁但非常糟糕的失败。
 */
#define rcu_dereference_protected(p, c)

/**
 * kfree_rcu() - 在延迟期后释放对象
 * @ptr: 需要释放的指针,对于双参数调用的情况。
 * @rhf:@ptr类型中的struct rcu_head的名称。
 *
 * 许多RCU回调函数只是对基础结构调用kfree()。
 * 这些函数很简单,但它们的大小会逐渐增加,此外,当它们在内核模块中使用时,该模块必须在卸载时调用
 * 高延迟的rcu_barrier()函数。
 *
 * kfree_rcu()函数解决了这个问题。  kfree_rcu()并没有在嵌入的rcu_head结构中编码一个函数地址,而是
 * 编码了rcu_head结构在基础结构中的偏移量。
 * 由于函数不能位于内核虚拟内存的低位4096字节内,因此最大可以支持4095字节的偏移量。
 * 如果偏移量大于4095字节,则在kvfree_rcu_arg_2()中将产生编译时错误。如果此错误被触发,可以
 * 回退到使用call_rcu(),或重新安排结构,将rcu_head结构放置在前4096字节内。
 *
 * 要释放的对象可以通过kmalloc()或
 * kmem_cache_alloc()分配。
 *
 * 请注意,允许的偏移量在未来可能会减少。
 *
 * BUILD_BUG_ON检查不能涉及任何函数调用,因此检查是在宏中完成的。
 */
#define kfree_rcu(ptr, rhf) kvfree_rcu_arg_2(ptr, rhf)

/**
 * rcu_access_pointer() - 获取RCU指针但不进行解引用
 * @p: 要读取的指针
 *
 * 返回指定的RCU保护指针的值,但省略了在RCU读侧临界区内的lockdep检查。
 * 这在仅访问指针值但不解引用指针的情况下非常有用,例如,在测试RCU保护的指针
 * 是否为NULL时。虽然rcu_access_pointer()也可以用于更新端锁防止指针值改变的情况,但
 * 在这种情况下应使用rcu_dereference_protected()。
 * 在RCU读侧临界区内,几乎没有理由使用rcu_access_pointer()。
 *
 * 通常最好直接测试rcu_access_pointer()的返回值
 * 以避免后续不小心的更改引入意外的解引用。换句话说,将rcu_access_pointer()返回值赋值
 * 给一个局部变量会导致潜在的事故。
 *
 * 当读侧访问指针至少在一个延迟期之前被移除时,使用rcu_access_pointer()也是允许的,
 * 例如在RCU回调中释放数据时,或者在synchronize_rcu()返回后。这在延迟期过后拆解
 * 多链结构时非常有用。然而,rcu_dereference_protected()通常更适合这种情况。
 */
#define rcu_access_pointer(p) __rcu_access_pointer((p), __UNIQUE_ID(rcu), __rcu)

/**
 * rcu_read_lock() - 标记RCU读侧临界区的开始
 *
 * 当一个CPU调用synchronize_rcu(),而其他CPU正在RCU读侧临界区内时,
 * synchronize_rcu()会被阻塞,直到所有其他CPU退出其临界区。同样地,如果一个CPU调用
 * call_rcu(),而其他CPU正在RCU读侧临界区内,那么相应的RCU回调会延迟,直到所有其他
 * CPU退出其临界区。
 *
 * 在v5.0及更高版本的内核中,synchronize_rcu()和call_rcu()还会等待禁用抢占的代码区域,
 * 包括禁用中断或软中断的代码区域。在v5.0之前的内核(定义了synchronize_sched())中,
 * 只有在rcu_read_lock()和rcu_read_unlock()之间的代码才会被保证等待。
 *
 * 需要注意的是,RCU回调允许与新的RCU读侧临界区并行运行。这可以通过以下事件序列发生:
 * (1) CPU 0进入RCU读侧临界区,(2) CPU 1调用call_rcu()注册RCU回调,(3) CPU 0退出RCU读侧临界区,
 * (4) CPU 2进入RCU读侧临界区,(5) RCU回调被调用。这是合法的,因为与call_rcu()并发运行的RCU读侧
 * 临界区(因此可能引用正在被相应RCU回调释放的内容)在相应的RCU回调被调用之前已完成。
 *
 * RCU读侧临界区可以嵌套。任何延迟的操作都会被推迟,直到最外层的RCU读侧临界区完成。
 *
 * 你可以通过遵循这个规则避免理解下一段内容:不要在!PREEMPTION内核中的rcu_read_lock() RCU
 * 读侧临界区中放入任何可能阻塞的代码。但是如果你想了解完整的情况,请继续阅读!
 *
 * 在非可抢占RCU实现(纯TREE_RCU和TINY_RCU)中,在RCU读侧临界区内阻塞是非法的。
 * 在可抢占RCU实现(PREEMPT_RCU)和CONFIG_PREEMPTION内核构建中,RCU读侧临界区
 * 可以被抢占,但显式阻塞是非法的。最后,在实时内核实现(带有-rt补丁集)中,RCU读侧临界区
 * 既可以被抢占,也可以阻塞,但只有在获取需要优先级继承的自旋锁时才允许阻塞。
 */
static __always_inline void rcu_read_lock(void)

/*
 * 那么,rcu_write_lock在哪里? 它不存在,因为没有办法让写者锁住RCU读者。
 * 这是一个特性,而不是错误——这个属性正是RCU带来性能优势的原因。
 * 当然,写者必须彼此协调。正常的自旋锁原语可以很好地完成此工作,
 * 但也可以使用任何其他技术。RCU并不关心写者如何避免彼此冲突,
 * 只要它们能够做到这一点。
 */

/**
 * rcu_read_unlock() - 标记RCU读侧临界区的结束
 *
 * 在几乎所有情况下,rcu_read_unlock()都不会导致死锁。
 * 在最近的内核版本中,将synchronize_sched()和synchronize_rcu_bh()合并成
 * synchronize_rcu()后,这种免死锁性质也扩展到调度器的运行队列和优先级继承的
 * 自旋锁,这是通过在禁用中断时调用rcu_read_unlock()时进行的平稳状态延迟完成的。
 *
 * 请参见rcu_read_lock()了解更多信息。
 */
static inline void rcu_read_unlock(void)

/**
 * rcu_dereference() - 获取RCU保护的指针以便解引用
 * @p: 要读取的指针,在解引用之前
 *
 * 这是一个对rcu_dereference_check()的简单包装。
 */
#define rcu_dereference(p) rcu_dereference_check(p, 0)

/**
 * rcu_dereference_check() - 带有调试检查的rcu_dereference
 * @p: 要读取的指针,在解引用之前
 * @c: 解引用操作发生时的条件
 *
 * 执行rcu_dereference(),同时检查解引用发生时的条件是否正确。通常,条件
 * 用于表示此时应该保持的各种锁定条件。若条件满足,则检查应返回true。包括隐式检查
 * 是否处于RCU读取侧临界区(rcu_read_lock())。
 *
 * 例如:
 *
 *      bar = rcu_dereference_check(foo->bar, lockdep_is_held(&foo->lock));
 *
 * 可以用来告诉lockdep,只有在rcu_read_lock()被持有,或者持有了替换foo->bar时所需的锁,
 * 才能解引用foo->bar。
 *
 * 注意,条件列表还可能包含某些情况下无需持有锁的指示,例如在目标结构的初始化或销毁期间:
 *
 *      bar = rcu_dereference_check(foo->bar, lockdep_is_held(&foo->lock) ||
 *                                            atomic_read(&foo->usage) == 0);
 *
 * 在需要内存屏障的架构上插入内存屏障(当前仅限Alpha架构),防止编译器重新获取指针值
 *(并避免合并获取),更重要的是,文档化哪些指针受RCU保护,并检查该指针是否已标注为__rcu。
 */
#define rcu_dereference_check(p, c) \
        __rcu_dereference_check((p), __UNIQUE_ID(rcu), \
                                (c) || rcu_read_lock_held(), __rcu)