RCU机制

读-拷贝-更新(RCU)是为了保护在多数情况下被多个CPU读的数据结构而设计的另一种同步技术。RCU允许多个读者和写者并发执行(相对于只允许一个写者执行的顺序锁有了改进)。而且,RCU是不使用锁的,就是说,它不使用被所有CPU共享的锁或计数器,在这一点上与读/写自旋锁和顺序锁(由于高速缓存行窃用和失效而有很高的开销)相比RCU具有更大的优势。

RCU是如何不使用共享数据结构而令人惊讶地实现多个CPU同步呢?其关键的思想包包括限制RCP的范围的两个约束条件,这个是重中之重,如下所述:
1. RCU只保护被动态分配并通过指针引用的数据结构。
2. 在被RCU保护的临界区中,任何内核控制路径都不能睡眠。

当内核控制路径要读取被RCU保护的数据结构时,执行宏rcu_read_lock(),它仅仅是preempt_disable():
#define rcu_read_lock() /
    do { /
        preempt_disable(); /
        __acquire(RCU); /  
    } while(0)

接下来,读者间接引用该数据结构指针所对应的内存单元并开始读这个数据结构。正如在前面所强调的,读者在完成对数据结构的读操作之前,是不能睡眠的。

最后,用等同于preempt_enable()的宏rcu_read_unlock()标记临界区的结束:
#define rcu_read_unlock() /
    do { /
        __release(RCU); /
        preempt_enable(); /
    } while(0)

根据上面所述,读者几乎不做任何事情来防止竞争条件的出现,所以写者的工作不得不做得更多一些。

当写者要更新数据结构时,它间接引用指针并生成整个数据结构的副本。接下来,写者修改这个副本。一但修改完毕,写者改变指向数据结构的指针,以使它指向被修改后的副本。由于修改指针值的操作是一个原子操作,所以旧副本和新副本对每个读者或写者都是可见的,在数据结构中不会出现数据崩溃。尽管如此,还需要内存壁垒来保证:只有在数据结构被修改之后,已更新的指针对其他CPU才是可见的。如果把自旋锁与RCU结合起来以禁止写者的并发执行,就隐含地引入了这样的内存壁垒。

然而,使用RCU技术的真正困难在于:写者修改指针时不能立即释放数据结构的旧副本。因为,写者开始修改时,正在访问数据结构的读者可能还在读旧副本。只有在CPU上的所有(潜在的)读者都执行完宏rcu_read_unlock()之后,才可以释放旧副本,并换成新副本。所以,根据约束,内核要求每个潜在的读者在下面的操作之前执行rcu_read_unlock()宏:
(1)CPU执行进程切换(参见前面的约束条件2)
(2)CPU开始在用户态执行
(2)CPU执行空循环

对上面每种CPU情况,Linux给其下了一个定义,叫做CPU已经经过了静止状态(quiescent state)。这个概念就是这么来的。

写者调用函数call_rcu()来释放数据结构的旧副本(注意,内核没有提供什么rcu_write_lock函数,因为写也是并行的,只是替换一个副本,将新副本与旧副本替换):
void fastcall call_rcu(struct rcu_head *head,
                void (*func)(struct rcu_head *rcu))
{
    unsigned long flags;
    struct rcu_data *rdp;

    head->func = func;
    head->next = NULL;
    local_irq_save(flags);
    rdp = &__get_cpu_var(rcu_data);
    *rdp->nxttail = head;
    rdp->nxttail = &head->next;
    if (unlikely(++rdp->qlen > qhimark)) {
        rdp->blimit = INT_MAX;
        force_quiescent_state(rdp, &rcu_ctrlblk);
    }
    local_irq_restore(flags);
}

当所有的CPU都通过静止状态之后,call_rcu()接受rcu_head描述符(通常嵌在要被释放的数据结构中)的地址和将要调用的回调函数的地址(*func)作为参数。一旦回调函数被执行,它通常释放数据结构的旧副本。
struct rcu_head {
    struct rcu_head *next;
    void (*func)(struct rcu_head *head);
};

函数call_rcu()把回调函数和其参数的地址存放在rcu_head描述符中,然后把描述符通过next字段插入回调函数的每CPU(per-CPU)链表中。内核每经过一个时钟滴答就周期性地检查本地CPU是否经过了一个静止状态,即上述三种情况。如果所有CPU都经过了静止状态,本地tasklet的描述符存放在每CPU变量rcu_tasklet中就执行每CPU链表中的所有回调函数,释放数据结构的旧副本。

RCU是Linux 2.6中新加的功能,主要用在网络层和虚拟文件系统中。

你可能感兴趣的:(RCU机制)