qemu打快照后还能扩容吗 qemu使用_临界区


关于rcu的概念请参考谢宝友老师的深入理解RCU之三:概念

这里简单解释下,这里的例子为一个rcu从链表删除的例子, 在synchronize_rcu之前有一些读者会持有到5,6,7元素节点。synchronize_rcu之后保证没有读者持有5,6,7节点,所以可以进行对该节点释放。 这就是rcu要做的事情。在synchronize_rcu之前都可读者可能看到链表的不同副本,一种是从123,指向11,4,8, 一种是从567指向11,4,8。 对于第二种读者,我们不能在他们持有5,6,7的时候将5,6,7释放,所以synchronize_rcu就是等待这些读者读取完成,再进行节点释放(注意只需要等看得见5,6,7节点的读者完成)。

按照上面的情况我们举个例子

thread_reader1 {
    rcu_read_lock();
    {
         遍历链表执行操作
    }
    rcu_read_unlock();
}

thread_reader2 {
    rcu_read_lock();
    {
         遍历链表执行操作
    }
    rcu_read_unlock();
}

thread_writer {
     reu_delete(rcu_list, node);
     synchronize_rcu();
     free(node);
}

另外rcu读者可以嵌套。
知道了用法和语义,我们来看看qemu的实现

这里先说明下qemu ruc设计的整体思路.
首先rcu有一个全局变量
unsigned long rcu_gp_ctr = 0;
当synchronize_rcu进入的时候更新该值,导致后面进入读临界区的rcu读端都会看到这个新值,而之前进入rcu读临界区的读者拿到的该值与新的新的rcu_gp_ctr值不同。
这样就可以区分新读者还是老读者。

在rcu宽限期内旧读者持有rcu保护的数据,所以我们需要等待旧读者都退出可以完成rcu写端的其余操作(比如释放从链表上摘取的节点)
另外当qemu的synchronize_rcu没有采用自悬的方式去等待,而是通过QemuEvent rcu_gp_event 变量休眠,当旧读者退出临界区的时候通知synchronize_rcu去检查状态,
当所有旧读者都退出临界区,synchronize_rcu返回.

rcu如何 同步rcu_gp_ctr状态?

struct rcu_reader_data {
    /* Data used by both reader and synchronize_rcu() */
    unsigned long ctr;
    bool waiting;

    /* Data used by reader only */
    unsigned depth;

    /* Data used for registry, protected by rcu_registry_lock */
    QLIST_ENTRY(rcu_reader_data) node;
};

使用rcu_reader_data 来同步更新rcu_gp_ctr.
rcu_reader_data为threadlocal类型变量,
另外rcu有一个全局链表registry,所有rcu线程创建的时候都会将自己线程
的rcu_reader_data注册到registry链表上,这样其他线程也可以看到任何rcu线程的rcu_reader_data。

static ThreadList registry = QLIST_HEAD_INITIALIZER(registry);

在rcu线程初始化的时候

void rcu_register_thread(void)
{
    assert(QEMU_THREAD_LOCAL_GET_PTR(rcu_reader)->ctr == 0);
    qemu_mutex_lock(&rcu_registry_lock);
    QLIST_INSERT_HEAD(®istry, QEMU_THREAD_LOCAL_GET_PTR(rcu_reader), node);
    qemu_mutex_unlock(&rcu_registry_lock);
}

所以这里rcu_registry_lock就是保护registry链表的。

好了我们来看看rcu_read_lock的实现

static inline void rcu_read_lock(void)
{
    struct rcu_reader_data *p_rcu_reader = QEMU_THREAD_LOCAL_GET_PTR(rcu_reader);
    unsigned ctr;

    if (p_rcu_reader->depth++ > 0) {
        return;
    }

    ctr = atomic_read(&rcu_gp_ctr);
    atomic_set(&p_rcu_reader->ctr, ctr);

    /* Write p_rcu_reader->ctr before reading RCU-protected pointers.  */
    smp_mb_placeholder();
}

其实就是同步全局变量rcu_gp_ctr的值到thread local变量rcu_reader.ctr值,另外p_rcu_reader->depth用于实现rcu读者嵌套语义。 rcu_reader.ctr只有在rcu读者第一次进入的时候更新。 由于该函数为内联函数,为了防止编译器乱序和cpu乱序 添加一个内存屏障。

解锁过程如下

static inline void rcu_read_unlock(void)
{
    struct rcu_reader_data *p_rcu_reader = QEMU_THREAD_LOCAL_GET_PTR(rcu_reader);

    assert(p_rcu_reader->depth != 0);
    if (--p_rcu_reader->depth > 0) {
        return;
    }

    /* Ensure that the critical section is seen to precede the
     * store to p_rcu_reader->ctr.  Together with the following
     * smp_mb_placeholder(), this ensures writes to p_rcu_reader->ctr
     * are sequentially consistent.
     */
    atomic_store_release(&p_rcu_reader->ctr, 0);

    /* Write p_rcu_reader->ctr before reading p_rcu_reader->waiting.  */
    smp_mb_placeholder();
    if (unlikely(atomic_read(&p_rcu_reader->waiting))) {
        atomic_set(&p_rcu_reader->waiting, false);
        qemu_event_set(&rcu_gp_event);
    }
}

解锁代码就是把 thread local变量rcu_reader->ctr设置为0,表示已经退出读临界区。
如果rcu_reader->waiting为真为当前有rcu写者(synchronize_rcu)在等待该读者退出临界区,所以通过 qemu_event_set(&rcu_gp_event)通知写者(synchronize_rcu)有读者退出,使写者(synchronize_rcu)唤醒,后面我们会看到rcu写者((synchronize_rcu))在waiting上睡眠的情况。
另外 smp_mb_placeholder()这个内存屏障用于保证设置rcu_reader->ctr为0在读取rcu_reader->waiting之前完成,这是防止写者锁死,后面分析synchronize_rcu我们会看到原因。

void synchronize_rcu(void)
{
    qemu_mutex_lock(&rcu_sync_lock);

    /* Write RCU-protected pointers before reading p_rcu_reader->ctr.
     * Pairs with smp_mb_placeholder() in rcu_read_lock().
     */
    smp_mb_global();

    qemu_mutex_lock(&rcu_registry_lock);
    if (!QLIST_EMPTY(®istry)) {
        /* In either case, the atomic_mb_set below blocks stores that free
         * old RCU-protected pointers.
         */
        if (sizeof(rcu_gp_ctr) < 8) {
            /* For architectures with 32-bit longs, a two-subphases algorithm
             * ensures we do not encounter overflow bugs.
             *
             * Switch parity: 0 -> 1, 1 -> 0.
             */
            atomic_mb_set(&rcu_gp_ctr, rcu_gp_ctr ^ RCU_GP_CTR);
            wait_for_readers();
            atomic_mb_set(&rcu_gp_ctr, rcu_gp_ctr ^ RCU_GP_CTR);
        } else {
            /* Increment current grace period.  */
            atomic_mb_set(&rcu_gp_ctr, rcu_gp_ctr + RCU_GP_CTR);
        }

        wait_for_readers();
    }

    qemu_mutex_unlock(&rcu_registry_lock);
    qemu_mutex_unlock(&rcu_sync_lock);
}

这里主要是更新rcu_gp_ctr值, 然后调wait_for_readers()函数等待旧的读者完成.
rcu_sync_lock 用于防止多个写者进入 synchronize_rcu
另外在32位处理器和64位采用不同方式更新rcu_gp_ctr。

static void wait_for_readers(void)
{
    ThreadList qsreaders = QLIST_HEAD_INITIALIZER(qsreaders);
    struct rcu_reader_data *index, *tmp;

    for (;;) {
        /* We want to be notified of changes made to rcu_gp_ongoing
         * while we walk the list.
         */
        qemu_event_reset(&rcu_gp_event);

        /* Instead of using atomic_mb_set for index->waiting, and
         * atomic_mb_read for index->ctr, memory barriers are placed
         * manually since writes to different threads are independent.
         * qemu_event_reset has acquire semantics, so no memory barrier
         * is needed here.
         */
        QLIST_FOREACH(index, ®istry, node) {
            atomic_set(&index->waiting, true);
        }

        /* Here, order the stores to index->waiting before the loads of
         * index->ctr.  Pairs with smp_mb_placeholder() in rcu_read_unlock(),
         * ensuring that the loads of index->ctr are sequentially consistent.
         */
        smp_mb_global();

        QLIST_FOREACH_SAFE(index, ®istry, node, tmp) {
            if (!rcu_gp_ongoing(&index->ctr)) {
                QLIST_REMOVE(index, node);
                QLIST_INSERT_HEAD(&qsreaders, index, node);

                /* No need for mb_set here, worst of all we
                 * get some extra futex wakeups.
                 */
                atomic_set(&index->waiting, false);
            }
        }

        if (QLIST_EMPTY(®istry)) {
            break;
        }

        /* Wait for one thread to report a quiescent state and try again.
         * Release rcu_registry_lock, so rcu_(un)register_thread() doesn't
         * wait too much time.
         *
         * rcu_register_thread() may add nodes to ®istry; it will not
         * wake up synchronize_rcu, but that is okay because at least another
         * thread must exit its RCU read-side critical section before
         * synchronize_rcu is done.  The next iteration of the loop will
         * move the new thread's rcu_reader from ®istry to &qsreaders,
         * because rcu_gp_ongoing() will return false.
         *
         * rcu_unregister_thread() may remove nodes from &qsreaders instead
         * of ®istry if it runs during qemu_event_wait.  That's okay;
         * the node then will not be added back to ®istry by QLIST_SWAP
         * below.  The invariant is that the node is part of one list when
         * rcu_registry_lock is released.
         */
        qemu_mutex_unlock(&rcu_registry_lock);
        qemu_event_wait(&rcu_gp_event);
        qemu_mutex_lock(&rcu_registry_lock);
    }

    /* put back the reader list in the registry */
    QLIST_SWAP(®istry, &qsreaders, node);
}

static inline int rcu_gp_ongoing(unsigned long *ctr)
{
    unsigned long v;

    v = atomic_read(ctr);
    return v && (v != rcu_gp_ctr);
}

smp_mb_global()是为了让其他线程看到rcu_gp_event的值,这样新进入的读者就不被认为是新读者

wait_for_readers中rcu_gp_ongoing函数用于区分是否为旧读者
rcu_gp_ongoing中判断rcu线程的rcu_reader->ctr有三种情况

  1. 值为0 表示该线程不在rcu读者临界区内
  2. 与rcu_gp_ctr相同表示为一个新读者
  3. 是一个rcu_gp_ctr旧值,表示为一个旧的读者
    这里对于1,2情况我们的synchronize_rcu都不需要等待, 所以把他们从registry链表取走, 放入qsreaders 链表, 如果registry为空,
    表示所有旧读者都已经执行完成, 则synchronize_rcu结束
    否则的话把每个旧的读者的rcu_reader->waiting 变量都设置为真.则该读者在退出rcu临界区的时候需要通知synchronize_rcu线程,因为synchronize_rcu线程在
    rcu_gp_event上面.
    所以要保证
    读者线程设置rcu_reader->ctr=0要在 读取rcu_reader->waiting之前完成
    写者线程要先读取读者线程的rcu_reader->ctr再设置rcu_reader->waiting
    如果不这样可能导致rcu_sync永久休眠。

最后qemu的rcu不像内核中的rcu,qemu中的rcu在读者临界区会发生休眠(这在用户空间是不可避免的,内核采用禁止抢占而不被调度出去)。所以qemu的rcu性能远不及内核中的rcu.

所以qemu中的synchronize_rcu只在一个线程中调用,这就是call_rcu_thread,

另外读者有没有注意到rcu_read_lock()和rcu_read_unlock并没有任何参数,也就是说这是一个全局锁,对于内核中旧读者最多为cpu个数-1个,并且读者不允许休眠和调度,所以很快就会结束, 而在qemu中这是不现实的,所以qemu退化成只在一个线程中调用synchronize_rcu.

qemu的rcu提供的两个api

#define call_rcu(head, func, field)                                      \
    call_rcu1(({                                                         \
         char __attribute__((unused))                                    \
            offset_must_be_zero[-offsetof(typeof(*(head)), field)],      \
            func_type_invalid = (func) - (void (*)(typeof(head)))(func); \
         &(head)->field;                                                 \
      }),                                                                \
      (RCUCBFunc *)(func))

#define g_free_rcu(obj, field) \
    call_rcu1(({                                                         \
        char __attribute__((unused))                                     \
            offset_must_be_zero[-offsetof(typeof(*(obj)), field)];       \
        &(obj)->field;                                                   \
      }),                                                                \
      (RCUCBFunc *)g_free);