Userspace RCU原理

总览

urcu全称user-space read-copy update即用户态RCU,它提供了与内核RCU相似的机制,使得在多核多线程并发访问共享数据时,reader不用阻塞于writer的操作,从而使reader运行的更快,非常适合于读多写少的场景。

urcu特性

针对不同的应用场景,urcu提供了以下5种不同的flavors
- urcu
- QSBR(quiescent-state-based RCU)
- Memory-barrier-base RCU
- “Bullet-proof” RCU
- Signal-based RCU

qsbr是5种flavor中性能最好的,它也可称为显示静默期(Quiescent)声明模式,这种模式下,rcu_read_lock()和rcu_read_unlock()为空操作,对于reader来说负担为零(这一点时另外4种flavor不具备的),但代价是,每个reader线程也必须周期性的调用rcu_quiescent_state()来声明自己进入了静默期,需要注意的是,并非每次读操作完成后都需要做此声明,考虑到读操作的性能和应用读写操作的不平衡性,通常的做法是读操作每进行一定次数(如1024)之后声明一次静默期。还有一点,每个会进入RCU-read-side critical sections的线程都需要事先通过rcu_register_thread()接口进行注册,相应的在线程推出时调用rcu_unregister_thread()接口进行去注册。

实现

全局计数器

RCU机制是用于多核系统中,保持每个核上的线程所看到的全局数据一致性的一种机制,所以需要一种手段可以判断当writer线程进行数据更新时,reader线程看到的数据是否也更新了,为此urcu维护了一个全局的计数器rcu_gp.ctr,每次writer同步操作(synchronize),都会使其加1,表示数据已经更新了,reader需要更新 

struct rcu_gp rcu_gp;
struct rcu_gp {
    unsigned long ctr;
    ...
} __attribute__((aligned(CAA_CACHE_LINE_SIZE)));

与之对应的,每个reader线程也持有一个线程内部的计数器ctx,如果这个ctxrcu_gp.ctr一致,就表明本线程的数据已经最新(ACTIVE_CURRENT),反之则不是最新(ACTIVE_OLD),

struct rcu_reader {
    unsigned long ctr;
    ...
};
DECLARE_URCU_TLS(struct rcu_reader, rcu_reader)
读者 注册(register)\去注册(unregister)\上线(online)\下线(offline)

qsbr rcu的实现中,reader线程必须进行显示的注册,它的目的是将自己挂接在全局链表registry上,通俗的说就是将自己置于全局管理之下,这样当writer在进行同步(synchronize)时,才能知道哪些线程需要同步。线程的上线状态分为在线(online)和离线(offline),后面会提到,处于offline的线程虽然在registry链表上,但在synchronized时,writer会忽略这些线程。线程注册会默认置于online状态。

void rcu_register_thread(void)
{
    URCU_TLS(rcu_reader).tid = pthread_self();
    mutex_lock(&rcu_registry_lock);
    URCU_TLS(rcu_reader).registered = 1;
    cds_list_add(&URCU_TLS(rcu_reader).node, ®istry);
    _rcu_thread_online();
}

线程上线(online)的本质,就是将rcu_gp.ctr的值存储到本线程的ctr中

static inline void _rcu_thread_online(void)
{
    _CMM_STORE_SHARED(URCU_TLS(rcu_reader).ctr, CMM_LOAD_SHARED(rcu_gp.ctr));
}

而线程下线(offline),则是将本线程的ctr清零

static inline void _rcu_thread_offline(void)
{
    CMM_STORE_SHARED(URCU_TLS(rcu_reader).ctr, 0);
    wake_up_gp();
}
写者 同步(synchronize)

rcu机制的一个典型场景:writer线程创建了一份新的数据后,使全局指针gp_ptr指向这片内存空间,替换原数据内存空间,在多核系统中,writer在更新后并不能确保没有reader正在使用旧的内存数据,所以它需要阻塞等待所有的reader线程已经更新(即reader.ctx等于gp.ctr),这个操作便是同步(synchronize),其简化版实现代码片段如下

void synchronize_rcu(void)
{
    CDS_LIST_HEAD(qsreaders);
    DEFINE_URCU_WAIT_NODE(wait, URCU_WAIT_WAITING);
    ......
    urcu_wait_add(&gp_waiters, &wait)  /* 将writer自身置于wait状态 */
    ......
    wait_for_readers(®istry, &cur_snap_readers, &qsreaders); /* writer阻塞在这里 */
    .....
}

static void wait_for_readers(struct cds_list_head *input_readers,
                 struct cds_list_head *cur_snap_readers,
                 struct cds_list_head *qsreaders)
{
    unsigned int wait_loops = 0;
    struct rcu_reader *index, *tmp;

    /*
     * Wait for each thread URCU_TLS(rcu_reader).ctr to either
     * indicate quiescence (offline), or for them to observe the
     * current rcu_gp.ctr value.
     */
    for (;;) {          /* 直到所有reader.ctr已经到最新才跳出循环 */
        uatomic_set(&rcu_gp.futex, -1);
        cds_list_for_each_entry(index, input_readers, node) {
        _CMM_STORE_SHARED(index->waiting, 1);

        /* 遍历所有输入的reader */
        cds_list_for_each_entry_safe(index, tmp, input_readers, node) {
            switch (rcu_reader_state(&index->ctr)) {
            case RCU_READER_ACTIVE_CURRENT:   /* reader.ctr已经最新 */
            case RCU_READER_INACTIVE:      /* reader处于offline状态 */
                cds_list_move(&index->node, qsreaders); /* 从遍历列表中移除 */
                break;
            case RCU_READER_ACTIVE_OLD:     /* reader.ctr不是最新 */
                break;
            }
        }

        if (cds_list_empty(input_readers)) {
            uatomic_set(&rcu_gp.futex, 0);   /* 列表空了,表示所有reader已更新 跳出循环 */
            break;
        }
    }
}
读者 静默(quiescent)

从上面writer synchronize的过程可知,要使writer结束阻塞状态,reader必须为offline或者将其ctr更新到最新,这是通过调用rcu_quiescent_state()接口声明静默期完成的.

static inline void _rcu_quiescent_state(void)
{
    unsigned long gp_ctr;
    if ((gp_ctr = CMM_LOAD_SHARED(rcu_gp.ctr)) == URCU_TLS(rcu_reader).ctr)
        return;
    _rcu_quiescent_state_update_and_wakeup(gp_ctr);
}
static inline void _rcu_quiescent_state_update_and_wakeup(unsigned long gp_ctr)
{
    _CMM_STORE_SHARED(URCU_TLS(rcu_reader).ctr, gp_ctr); /* 将本线程ctr更新为gp_ctr */
    wake_up_gp();           /* 唤醒writer */
}

example

这个example节选自urcu官方代码的测试例程test_urcu_qsbr
测试例程根据用户输入创建若干个writerreader,writer不断申请释放内存资源,并用全局指针test_rcu_pointer记录资源,reader不断读取test_rcu_pointer指向资源的值,并且每1024次声明静默期,最后统计readerwriter的次数。

void *thr_writer(void *_count)
{
    unsigned long long *count = _count;
    int *new, *old;
    for (;;) {
        new = malloc(sizeof(int));
        assert(new);
        *new = 8;
        old = rcu_xchg_pointer(&test_rcu_pointer, new);

        synchronize_rcu();
        if (old)
            *old = 0;
        free(old);
        URCU_TLS(nr_writes)++;
    }
    printf_verbose("thread_end %s, tid %lu\n",
            "writer", urcu_get_thread_id());
    *count = URCU_TLS(nr_writes);
    return ((void*)2);
}
void *thr_reader(void *_count)
{
    unsigned long long *count = _count;
    int *local_ptr;

    rcu_register_thread();
    rcu_thread_offline();
    rcu_thread_online();

    for (;;) {
        rcu_read_lock();
        local_ptr = rcu_dereference(test_rcu_pointer);
        if (local_ptr)
            assert(*local_ptr == 8);
        rcu_read_unlock();
        URCU_TLS(nr_reads)++;
        /* QS each 1024 reads */
        if (caa_unlikely((URCU_TLS(nr_reads) & ((1 << 10) - 1)) == 0))
            rcu_quiescent_state();
    }

    rcu_unregister_thread();

    *count = URCU_TLS(nr_reads);
    printf_verbose("thread_end %s, tid %lu\n",
            "reader", urcu_get_thread_id());
    return ((void*)1);
}

perfomance

flavor total read totol write
urcu 7826237468 91
qsbr 10427746859 1746176
memory-barrir 365980233 10333212
bullet-proof 568170476 5226213
signal-based 9522616041 2329

call rcu

前面writer的例子中,当writer进行数据更新后释放旧资源,是要在synchronize_rcu()接触阻塞后才能进行,但一些时候,我们希望这样的‘延迟释放’不要阻塞writer线程,而是当reader进行了同步时,通过callback的方式完成资源释放,urcu提供了call_rcu()接口来完成这一功能

struct rcu_head {
    struct cds_wfcq_node next;
    void (*func)(struct rcu_head *head);
};
void call_rcu(struct rcu_head *head,void (*func)(struct rcu_head *head);

通常将要延迟释放的数据结构内嵌一个rcu_head结构,在需要延迟释放时调用

struct global_foo {
    struct rcu_head rcu_head;
    ......
};
struct global_foo g_foo;

writer需要释放时,调用call_rcu(),之后当所有reader都更新完成后,设置的回调函数free_func被调用

call_rcu(&g_foo.rcu_head, free_func);

urcu是如何实现这个功能的呢?
答案是 既然不能阻塞将writer阻塞在synchronize_rcu(),那总得有一个线程阻塞在synchronize_rcu()等待所有reader更新,于是urcu内部创建一个线程,称之为call_rcu_thread,这个线程专门用于writer call_rcu(),注意,这个线程只会在第一次call_rcu()被创建,之后的call_rcu()均使用这个线程,以下时简化版的代码片段

/* 第一次call_rcu()会调用到 call_rcu_data_init() */
static void call_rcu_data_init(struct call_rcu_data **crdpp,unsigned long flags,int cpu_affinity)
{
    struct call_rcu_data *crdp;
    int ret;

    crdp = malloc(sizeof(*crdp));
    if (crdp == NULL)
        urcu_die(errno);
    memset(crdp, '\0', sizeof(*crdp));
    cds_wfcq_init(&crdp->cbs_head, &crdp->cbs_tail);
    ......
    ret = pthread_create(&crdp->tid, NULL, call_rcu_thread, crdp); /* 创建call_rcu_thread */
    if (ret)
        urcu_die(ret);
}

static void *call_rcu_thread(void *arg)
{
    struct call_rcu_data *crdp = (struct call_rcu_data *) arg;
    rcu_register_thread();
    URCU_TLS(thread_call_rcu_data) = crdp;
    for (;;) {
            ......
            synchronize_rcu();  /* 在这里完成同步 */
            rhp->func(rhp);    /* 执行回调 */
            ......
    }
    rcu_unregister_thread();
    return NULL;
}

猜你喜欢

转载自blog.csdn.net/chenmo187j3x1/article/details/80992945
RCU