linux内核同步机制-RCU(1)

Linux内核里的同步机制众多,RCU(read-copy update)可以说是实现上比较复杂的一种了,比较适合用在读操作很多而写操作极少的情况,可以用来替代读写锁。RCU的实现本质上是一种trade-off,为了让读操作的开销尽可能的小,写操作的完成可以被推迟。

我们先通过几个内核中的例子来了解一下。

首先来看RCU在读操作方面是如何替代读锁的,观察下面的代码我们发现在使用上几乎没有什么差别,只是把read_lock()/read_unlock()换成了rcu_read_lock()/rcu_read_unlock()。

/* 读操作的例子 */
​
/* 读锁 */
static enum audit_state audit_filter_task(struct task_struct *tsk)
{
    struct audit_entry *e;
    enum audit_state   state;
​
    read_lock(&auditsc_lock);
    /* Note: audit_netlink_sem held by caller. */
    list_for_each_entry(e, &audit_tsklist, list) {
        if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
            read_unlock(&auditsc_lock);
            return state;
        }
    }
    read_unlock(&auditsc_lock);
    return AUDIT_BUILD_CONTEXT;
}
​
/* RCU */
static enum audit_state audit_filter_task(struct task_struct *tsk)
{
    struct audit_entry *e;
    enum audit_state   state;
​
    rcu_read_lock();
    /* Note: audit_netlink_sem held by caller. */
    list_for_each_entry_rcu(e, &audit_tsklist, list) {
        if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
            rcu_read_unlock();
            return state;
        }
    }
    rcu_read_unlock();
    return AUDIT_BUILD_CONTEXT;
}

在删除list元素的例子里我们看到了一点不同,在RCU的版本里并不需要从循环开始上锁,也就是说上RCU在写操作时是不会阻塞读操作的,当然读操作也不会被写操作阻塞。由于需要释放内存,调用了call_rcu()函数,这个函数的作用是推迟释放内存的时机,当所有已进入临界区的读操作完成之后再释放内存。

在插入list元素的例子中同样也不用从循环开始上锁,但是插入元素的函数换成了list_add_rcu(),如果我们跟踪这个函数的实现的话会发现list_add_rcu里使用了rcu_assign_pointer()这个函数,是用来更新指针指向的内容的,这里我们发现RCU主要是用来同步指针指向的数据,通常是通过kmalloc申请的内存。

/* 写操作的例子,删除和插入list元素 */
​
/* 写锁 */
static inline int audit_del_rule(struct audit_rule *rule,
                     struct list_head *list)
{
    struct audit_entry  *e;
​
    write_lock(&auditsc_lock);
    list_for_each_entry(e, list, list) {
        if (!audit_compare_rule(rule, &e->rule)) {
            list_del(&e->list);
            write_unlock(&auditsc_lock);
            return 0;
        }
    }
    write_unlock(&auditsc_lock);
    return -EFAULT;     /* No matching rule */
}
static inline int audit_add_rule(struct audit_entry *entry,
                                 struct list_head *list)
{
    write_lock(&auditsc_lock);
    if (entry->rule.flags & AUDIT_PREPEND) {
        entry->rule.flags &= ~AUDIT_PREPEND;
        list_add(&entry->list, list);
    } else {
        list_add_tail(&entry->list, list);
    }
    write_unlock(&auditsc_lock);
    return 0;
}
​
/* RCU */
static inline int audit_del_rule(struct audit_rule *rule,
                     struct list_head *list)
{
    struct audit_entry  *e;
​
    /* Do not use the _rcu iterator here, since this is the only
         * deletion routine. */
    list_for_each_entry(e, list, list) {
        if (!audit_compare_rule(rule, &e->rule)) {
            list_del_rcu(&e->list);
            call_rcu(&e->rcu, audit_free_rule);
            return 0;
        }
    }
    return -EFAULT;     /* No matching rule */
}
​
static inline int audit_add_rule(struct audit_entry *entry,
                                 struct list_head *list)
{
    if (entry->rule.flags & AUDIT_PREPEND) {
        entry->rule.flags &= ~AUDIT_PREPEND;
        list_add_rcu(&entry->list, list);
    } else {
        list_add_tail_rcu(&entry->list, list);
    }
    return 0;
}

下面这个更新list元素的例子很好的体现了RCU read-copy update的涵义,在RCU版本的代码里首先为新的元素申请内存,并把旧元素的值copy到新元素,更新新元素的值之后,调用list_replace_rcu(),实际也是调用了rcu_assign_pointer()更新了元素,最后调用call_rcu()函数把旧元素的内存释放。

大家可能注意到,list_replace_rcu()函数之前发生的读操作读取的都是旧元素的值,在list_replace_rcu()函数之后如果有读操作则读到的是新元素的值,也就是说在list_replace_rcu()之前的读操作结束之前,实际上内存中存在新旧两个元素。

/* 写操作的例子,更新list元素的值 */
​
/* 写锁 */
static inline int audit_upd_rule(struct audit_rule *rule,
                     struct list_head *list,
                     __u32 newaction,
                     __u32 newfield_count)
{
    struct audit_entry  *e;
    struct audit_newentry *ne;
​
    write_lock(&auditsc_lock);
    /* Note: audit_netlink_sem held by caller. */
    list_for_each_entry(e, list, list) {
        if (!audit_compare_rule(rule, &e->rule)) {
            e->rule.action = newaction;
            e->rule.file_count = newfield_count;
            write_unlock(&auditsc_lock);
            return 0;
        }
    }
    write_unlock(&auditsc_lock);
    return -EFAULT;     /* No matching rule */
}
​
/* RCU */
static inline int audit_upd_rule(struct audit_rule *rule,
                                 struct list_head *list,
                                 __u32 newaction,
                                 __u32 newfield_count)
{
    struct audit_entry  *e;
    struct audit_newentry *ne;
​
    list_for_each_entry(e, list, list) {
        if (!audit_compare_rule(rule, &e->rule)) {
            ne = kmalloc(sizeof(*entry), GFP_ATOMIC);
            if (ne == NULL)
                return -ENOMEM;
            audit_copy_rule(&ne->rule, &e->rule);
            ne->rule.action = newaction;
            ne->rule.file_count = newfield_count;
            list_replace_rcu(&e->list, &ne->list);
            call_rcu(&e->rcu, audit_free_rule);
            return 0;
        }
    }
    return -EFAULT;     /* No matching rule */
}

通过上面几个例子我们对RCU有了大概的了解:

  1. 在读操作的代码段,RCU和读写锁几乎一样,但不会被写操作阻塞

  2. RCU主要用于保证指针数据的同步

  3. 在写操作时,RCU分为两步,将指针指向新内存,推迟回收旧内存

  4. 由于在某一时间段内会同时存在新数据和旧数据,所以只适用于不严格要求数据同步的场景

进一步的,我们看一下RCU提供了哪些接口,以下是所有核心API:

  • 上锁函数,rcu_read_lock/rcu_read_unlock

  • 指针赋值和解引用,rcu_assign_pointer/rcu_dereference

  • 异步或同步回收内存,call_rcu/synchronize_rcu

只有这么几个,足够的简洁,在代码示例中看到的list相关的接口也都是这几个API封装得到的。

这张图是对RCU的一个总结,里面的updater是写操作的一方,reader是读操作的一方,grace period(宽限期),也就是updater等待已经开始的reader退出临界区的时间,grace period的确定在RCU实现里是很重要的话题,我们下篇文章进一步讨论。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值