zoukankan      html  css  js  c++  java
  • Linux 内核:RCU机制与使用

    Linux 内核:RCU机制与使用

    背景

    学习Linux源码的时候,发现很多熟悉的数据结构多了__rcu后缀,因此了解了一下这些内容。

    介绍

    RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用。RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数据的时候不对链表进行耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修改(修改的时候,需要加锁)。RCU适用于需要频繁的读取数据,而相应修改数据并不多的情景,例如在文件系统中,经常需要查找定位目录,而对目录的修改相对来说并不多,这就是RCU发挥作用的最佳场景。

    RCU(Read-Copy Update),是 Linux 中比较重要的一种同步机制。顾名思义就是“读,拷贝更新”,再直白点是“随意读,但更新数据的时候,需要先复制一份副本,在副本上完成修改,再一次性地替换旧数据”。这是 Linux 内核实现的一种针对“读多写少”的共享数据的同步机制。

    Linux内核源码当中,关于RCU的文档比较齐全,你可以在 Documentation/RCU/ 目录下找到这些文件。Paul E. McKenney 是内核中RCU源码的主要实现者,他也写了很多RCU方面的文章。他把这些文章和一些关于RCU的论文的链接整理到了一起:http://www2.rdrop.com/users/paulmck/RCU/

    RCU机制解决了什么

    在RCU的实现过程中,我们主要解决以下问题:

    1、在读取过程中,另外一个线程删除了一个节点。删除线程可以把这个节点从链表中移除,但它不能直接销毁这个节点,必须等到所有的读取线程读取完成以后,才进行销毁操作。RCU中把这个过程称为宽限期(Grace period)。

    2、在读取过程中,另外一个线程插入了一个新节点,而读线程读到了这个节点,那么需要保证读到的这个节点是完整的。这里涉及到了发布-订阅机制(Publish-Subscribe Mechanism)。

    3、保证读取链表的完整性。新增或者删除一个节点,不至于导致遍历一个链表从中间断开。但是RCU并不保证一定能读到新增的节点或者不读到要被删除的节点。

    RCU(Read-Copy Update),顾名思义就是读-拷贝修改,它是基于其原理命名的。对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。那么这个“适当的时机”是怎么确定的呢?这是由内核确定的,也是我们后面讨论的重点。

    RCU原理

    RCU实际上是一种改进的rwlock,读者几乎没有什么同步开销,它不需要锁,不使用原子指令,而且在除alpha的所有架构上也不需要内存栅(Memory Barrier),因此不会导致锁竞争,内存延迟以及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。写者的同步开销比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其它写者的修改操作。

    读者必须提供一个信号给写者以便写者能够确定数据可以被安全地释放或修改的时机。有一个专门的垃圾收集器来探测读者的信号,一旦所有的读者都已经发送信号告知它们都不在使用被RCU保护的数据结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作。

    RCU与rwlock的不同之处是:它既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以有多个写者并行访问取决于写者之间使用的同步机制),读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。但RCU不能替代rwlock,因为如果写比较多时,对读者的性能提高不能弥补写者导致的损失。

    读者在访问被RCU保护的共享数据期间不能被阻塞,这是RCU机制得以实现的一个基本前提,也就说当读者在引用被RCU保护的共享数据期间,读者所在的CPU不能发生上下文切换,spinlock和rwlock都需要这样的前提。写者在访问被RCU保护的共享数据时不需要和读者竞争任何锁,只有在有多于一个写者的情况下需要获得某种锁以与其他写者同步。

    写者修改数据前首先拷贝一个被修改元素的副本,然后在副本上进行修改,修改完毕后它向垃圾回收器注册一个回调函数以便在适当的时机执行真正的修改操作。等待适当时机的这一时期称为grace period,而CPU发生了上下文切换称为经历一个quiescent state,grace period就是所有CPU都经历一次quiescent state所需要的等待的时间。垃圾收集器就是在grace period之后调用写者注册的回调函数来完成真正的数据修改或数据释放操作的。

    要想使用好RCU,就要知道RCU的实现原理。我们拿linux 2.6.21 kernel的实现开始分析,为什么选择这个版本的实现呢?因为这个版本的实现相对较为单纯,也比较简单。当然之后内核做了不少改进,如抢占RCU、可睡眠RCU、分层RCU。但是基本思想都是类似的。所以先从简单入手。

    首先,上一节我们提到,写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。而这个“适当的时机”就是所有CPU经历了一次进程切换(也就是一个grace period)。为什么这么设计?因为RCU读者的实现就是关抢占执行读取,读完了当然就可以进程切换了,也就等于是写者可以操作临界区了。

    那么就自然可以想到,内核会设计两个元素,来分别表示写者被挂起的起始点,以及每cpu变量,来表示该cpu是否经过了一次进程切换(quies state)。

    就是说,当写者被挂起后,

    1)重置每cpu变量,值为0。

    2)当某个cpu经历一次进程切换后,就将自己的变量设为1。

    3)当所有的cpu变量都为1后,就可以唤醒写者了。

    下面我们来分别看linux里是如何完成这三步的。

    从一个例子开始

    我们从一个例子入手,这个例子来源于linux kernel文档中的whatisRCU.txt。这个例子使用RCU的核心API来保护一个指向动态分配内存的全局指针。

    struct foo {  
        int a;  
        char b;  
        long c;  
    };  
    
    DEFINE_SPINLOCK(foo_mutex);  
    
    struct foo *gbl_foo;  
    
    void foo_read (void)  
    {  
        foo *fp = gbl_foo;  
        if ( fp != NULL )  
            dosomething(fp->a, fp->b , fp->c );  
    }  
    
    void foo_update( foo* new_fp )  
    {  
        spin_lock(&foo_mutex);  
        foo *old_fp = gbl_foo;  
        gbl_foo = new_fp;  
        spin_unlock(&foo_mutex);  
        kfee(old_fp);  
    }
    

    如上代码所示,RCU被用来保护全局指针struct foo *gbl_foo

    foo_get_a()用来从RCU保护的结构中取得gbl_foo的值。

    foo_update_a()用来更新被RCU保护的gbl_foo的值(更新其a成员)。

    首先,我们思考一下,为什么要在foo_update_a()中使用自旋锁foo_mutex呢?假设中间没有使用自旋锁.那foo_update_a()的代码如下:

    void foo_read(void)
    {
    	rcu_read_lock();
    	foo *fp = gbl_foo;
    	if ( fp != NULL )
    			dosomething(fp->a,fp->b,fp->c);
    	rcu_read_unlock();
    }
     
    void foo_update( foo* new_fp )
    {
    	spin_lock(&foo_mutex);
    	foo *old_fp = gbl_foo;
    	gbl_foo = new_fp;
    	spin_unlock(&foo_mutex);
    	synchronize_rcu();
    	kfee(old_fp);
    }
    

    假设A进程在上图—-标识处被B进程抢点.B进程也执行了goo_ipdate_a().等B执行完后,再切换回A进程.此时,A进程所持的old_fd实际上已经被B进程给释放掉了.此后A进程对old_fd的操作都是非法的。所以在此我们得到一个重要结论:RCU允许多个读者同时访问被保护的数据,也允许多个读者在有写者时访问被保护的数据(但是注意:是否可以有多个写者并行访问取决于写者之间使用的同步机制)。

    说明:本文中说的进程不是用户态的进程,而是内核的调用路径,也可能是内核线程或软中断等。

    RCU的核心API

    另外,我们在上面也看到了几个有关RCU的核心API。它们为别是:

    rcu_read_lock()
    rcu_read_unlock()
    
    synchronize_rcu()
    
    rcu_assign_pointer()
    rcu_dereference()
    

    其中,rcu_read_lock()rcu_read_unlock()用来保持一个读者的RCU临界区.在该临界区内不允许发生上下文切换。

    为什么不能发生切换呢?因为内核要根据“是否发生过切换”来判断读者是否已结束读操作,我们后面再分析。

    而下列的函数用于实现内存屏障的作用。

    • rcu_dereference():读者调用它来获得一个被RCU保护的指针。
    • rcu_assign_pointer():写者使用该函数来为被RCU保护的指针分配一个新的值。

    注意,synchronize_rcu():这是RCU的核心所在,它挂起写者,等待读者都退出后释放老的数据。

    增加链表项

    Linux kernel 中利用 RCU 往链表增加项的源码如下:

    #define list_next_rcu(list)     (*((struct list_head __rcu **)(&(list)->next)))
    
    static inline void __list_add_rcu(struct list_head *new,
                    struct list_head *prev, struct list_head *next)
    {
            new->next = next;
            new->prev = prev;
            rcu_assign_pointer(list_next_rcu(prev), new);
            next->prev = new;
    }
    

    list_next_rcu() 函数中的 rcu 是一个供代码分析工具 Sparse 使用的编译选项,规定有 rcu 标签的指针不能直接使用,而需要使用 rcu_dereference() 返回一个受 RCU 保护的指针才能使用。rcu_dereference() 接口的相关知识会在后文介绍,这一节重点关注 rcu_assign_pointer() 接口。首先看一下 rcu_assign_pointer() 的源码:

    #define __rcu_assign_pointer(p, v, space) 
        ({ 
            smp_wmb(); 
            (p) = (typeof(*v) __force space *)(v); 
        })
    

    上述代码的最终效果是把 v 的值赋值给 p,关键点在于第 3 行的内存屏障。什么是内存屏障(Memory Barrier)呢?CPU 采用流水线技术执行指令时,只保证有内存依赖关系的指令的执行顺序,例如 p = v; a = *p;,由于第 2 条指令访问的指针 p 所指向的内存依赖于第 1 条指令,因此 CPU 会保证第 1 条指令在第 2 条指令执行前执行完毕。但对于没有内存依赖的指令,例如上述 __list_add_rcu() 接口中,假如把第 8 行写成 prev->next = new;,由于这个赋值操作并没涉及到对 new 指针指向的内存的访问,因此认为不依赖于 6,7 行对 new->next 和 new->prev 的赋值,CPU 有可能实际运行时会先执行 prev->next = new; 再执行 new->prev = prev;,这就会造成 new 指针(也就是新加入的链表项)还没完成初始化就被加入了链表中,假如这时刚好有一个读者刚好遍历访问到了该新的链表项(因为 RCU 的一个重要特点就是可随意执行读操作),就会访问到一个未完成初始化的链表项!通过设置内存屏障就能解决该问题,它保证了在内存屏障前边的指令一定会先于内存屏障后边的指令被执行。这就保证了被加入到链表中的项,一定是已经完成了初始化的。

    最后提醒一下,这里要注意的是,如果可能存在多个线程同时执行添加链表项的操作,添加链表项的操作需要用其他同步机制(如 spin_lock 等)进行保护。

    访问链表项

    Linux kernel 中访问 RCU 链表项常见的代码模式是:

    rcu_read_lock();
    list_for_each_entry_rcu(pos, head, member) {
        // do something with `pos`
    }
    rcu_read_unlock();
    

    这里要讲到的 rcu_read_lock() 和 rcu_read_unlock(),是 RCU “随意读” 的关键,它们的效果是声明了一个读端的临界区(read-side critical sections)。在说读端临界区之前,我们先看看读取链表项的宏函数 list_for_each_entry_rcu。追溯源码,获取一个链表项指针主要调用的是一个名为 rcu_dereference() 的宏函数,而这个宏函数的主要实现如下:

    #define __rcu_dereference_check(p, c, space) 
        ({ 
            typeof(*p) *_________p1 = (typeof(*p)*__force )ACCESS_ONCE(p); 
            rcu_lockdep_assert(c, "suspicious rcu_dereference_check()" 
                          " usage"); 
            rcu_dereference_sparse(p, space); 
            smp_read_barrier_depends(); 
            ((typeof(*p) __force __kernel *)(_________p1)); 
        })
    第 3 行:声明指针 _p1 = p;
    第 7 行:smp_read_barrier_depends();
    第 8 行:返回 _p1;
    

    上述两块代码,实际上可以看作这样一种模式:

    rcu_read_lock();
    p1 = rcu_dereference(p);
    if (p1 != NULL) {
        // do something with p1, such as:
        printk("%d
    ", p1->field);
    }
    rcu_read_unlock();
    

    根据 rcu_dereference() 的实现,最终效果就是把一个指针赋值给另一个,那如果把上述第 2 行的 rcu_dereference() 直接写成 p1 = p 会怎样呢?在一般的处理器架构上是一点问题都没有的。但在 alpha 上,编译器的 value-speculation 优化选项据说可能会“猜测” p1 的值,然后重排指令先取值 p1->field~ 因此 Linux kernel 中,smp_read_barrier_depends() 的实现是架构相关的,arm、x86 等架构上是空实现,alpha 上则加了内存屏障,以保证先获得 p 真正的地址再做解引用。因此上一节 “增加链表项” 中提到的 “__rcu” 编译选项强制检查是否使用 rcu_dereference() 访问受 RCU 保护的数据,实际上是为了让代码拥有更好的可移植性。

    现在回到读端临界区的问题上来。多个读端临界区不互斥,即多个读者可同时处于读端临界区中,但一块内存数据一旦能够在读端临界区内被获取到指针引用,这块内存块数据的释放必须等到读端临界区结束,等待读端临界区结束的 Linux kernel API 是synchronize_rcu()。读端临界区的检查是全局的,系统中有任何的代码处于读端临界区,synchronize_rcu() 都会阻塞,知道所有读端临界区结束才会返回。为了直观理解这个问题,举以下的代码实例:

    /* `p` 指向一块受 RCU 保护的共享数据 */
    
    /* reader */
    rcu_read_lock();
    p1 = rcu_dereference(p);
    if (p1 != NULL) {
        printk("%d
    ", p1->field);
    }
    rcu_read_unlock();
    
    /* free the memory */
    p2 = p;
    if (p2 != NULL) {
        p = NULL;
        synchronize_rcu();
        kfree(p2);
    }
    

    用以下图示来表示多个读者与内存释放线程的时序关系:

    img

    上图中,每个读者的方块表示获得 p 的引用(第5行代码)到读端临界区结束的时间周期;t1 表示 p = NULL 的时间;t2 表示 synchronize_rcu() 调用开始的时间;t3 表示 synchronize_rcu() 返回的时间。我们先看 Reader1,2,3,虽然这 3 个读者的结束时间不一样,但都在 t1 前获得了 p 地址的引用。t2 时调用 synchronize_rcu(),这时 Reader1 的读端临界区已结束,但 Reader2,3 还处于读端临界区,因此必须等到 Reader2,3 的读端临界区都结束,也就是 t3,t3 之后,就可以执行 kfree(p2) 释放内存。synchronize_rcu() 阻塞的这一段时间,有个名字,叫做 Grace period。而 Reader4,5,6,无论与 Grace period 的时间关系如何,由于获取引用的时间在 t1 之后,都无法获得 p 指针的引用,因此不会进入 p1 != NULL 的分支。

    删除链表项

    知道了前边说的 Grace period,理解链表项的删除就很容易了。常见的代码模式是:

    p = seach_the_entry_to_delete();
    list_del_rcu(p->list);
    synchronize_rcu();
    kfree(p);
    其中 list_del_rcu() 的源码如下,把某一项移出链表:
    
    /* list.h */
    static inline void __list_del(struct list_head * prev, struct list_head * next)
    {
        next->prev = prev;
        prev->next = next;
    }
    
    /* rculist.h */
    static inline void list_del_rcu(struct list_head *entry)
    {
        __list_del(entry->prev, entry->next);
        entry->prev = LIST_POISON2;
    }
    

    根据上一节“访问链表项”的实例,假如一个读者能够从链表中获得我们正打算删除的链表项,则肯定在 synchronize_rcu() 之前进入了读端临界区,synchronize_rcu() 就会保证读端临界区结束时才会真正释放链表项的内存,而不会释放读者正在访问的链表项。

    更新链表项

    前文提到,RCU 的更新机制是 “Copy Update”,RCU 链表项的更新也是这种机制,典型代码模式是:

    p = search_the_entry_to_update();
    q = kmalloc(sizeof(*p), GFP_KERNEL);
    *q = *p;
    q->field = new_value;
    list_replace_rcu(&p->list, &q->list);
    synchronize_rcu();
    kfree(p);
    

    其中第 3,4 行就是复制一份副本,并在副本上完成更新,然后调用 list_replace_rcu() 用新节点替换掉旧节点。源码如下:

    其中第 3,4 行就是复制一份副本,并在副本上完成更新,然后调用 list_replace_rcu() 用新节点替换掉旧节点,最后释放旧节点内存。

    list_replace_rcu() 源码如下:

    static inline void list_replace_rcu(struct list_head *old,
                    struct list_head *new)
    {
        new->next = old->next;
        new->prev = old->prev;
        rcu_assign_pointer(list_next_rcu(new->prev), new);
        new->next->prev = new;
        old->prev = LIST_POISON2;
    }
    

    RCU链表API应用示例

    下面看下RCU list API的几个应用示例。

    只有增加和删除的链表操作

    在这种应用情况下,绝大部分是对链表的遍历,即读操作,而很少出现的写操作只有增加或删除链表项,并没有对链表项的修改操作,这种情况使用RCU非常容易,从rwlock转换成RCU非常自然。路由表的维护就是这种情况的典型应用,对路由表的操作,绝大部分是路由表查询,而对路由表的写操作也仅仅是增加或删除,因此使用RCU替换原来的rwlock顺理成章。系统调用审计也是这样的情况。

    这是一段使用rwlock的系统调用审计部分的读端代码:

    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;
    }
    

    这种转换非常直接,使用rcu_read_lock和rcu_read_unlock分别替换read_lock和read_unlock,链表遍历函数使用_rcu版本替换就可以了。

    使用rwlock的写端代码:

    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, e);
                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_del替换为list_del_rcu和call_rcu,这是因为被删除的链表项可能还在被别的读者引用,所以不能立即删除,必须等到所有读者经历一个quiescent state才可以删除。另外,list_for_each_entry并没有被替换为list_for_each_entry_rcu,这是因为,只有一个写者在做链表删除操作,因此没有必要使用_rcu版本。

    通常情况下,write_lock和write_unlock应当分别替换成spin_lock和spin_unlock,但是对于只是对链表进行增加和删除操作而且只有一个写者的写端,在使用了_rcu版本的链表操作API后,rwlock可以完全消除,不需要spinlock来同步读者的访问。对于上面的例子,由于已经有audit_netlink_sem被调用者保持,所以spinlock就没有必要了。

    这种情况允许修改结果延后一定时间才可见,而且写者对链表仅仅做增加和删除操作,所以转换成使用RCU非常容易。

    写端需要对链表条目进行修改操作

    如果写者需要对链表条目进行修改,那么就需要首先拷贝要修改的条目,然后修改条目的拷贝,等修改完毕后,再使用条目拷贝取代要修改的条目,要修改条目将被在经历一个grace period后安全删除。

    对于系统调用审计代码,并没有这种情况。这里假设有修改的情况,那么使用rwlock的修改代码应当如下:

    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, ne);
    
                call_rcu(&e->rcu, audit_free_rule, e);
    
                return 0;
            }
        }
    
        return -EFAULT;     /* No matching rule */
    
    }
    

    修改操作立即可见

    前面两种情况,读者能够容忍修改可以在一段时间后看到,也就说读者在修改后某一时间段内,仍然看到的是原来的数据。在很多情况下,读者不能容忍看到旧的数据,这种情况下,需要使用一些新措施,如System V IPC,它在每一个链表条目中增加了一个deleted字段,标记该字段是否删除,如果删除了,就设置为真,否则设置为假,当代码在遍历链表时,核对每一个条目的deleted字段,如果为真,就认为它是不存在的。

    还是以系统调用审计代码为例,如果它不能容忍旧数据,那么,读端代码应该修改为:

    static enum audit_state audit_filter_task(struct task_struct *tsk)
    {
        struct audit_entry *e;
        enum audit_state  state;
    
        rcu_read_lock();
    
        list_for_each_entry_rcu(e, &audit_tsklist, list) {
    
            if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
                spin_lock(&e->lock);
    
                if (e->deleted) {
                    spin_unlock(&e->lock);
                    rcu_read_unlock();
                    return AUDIT_BUILD_CONTEXT;
                }
    
                rcu_read_unlock();
    
                return state;
            }
        }
    
        rcu_read_unlock();
    
        return AUDIT_BUILD_CONTEXT;
    }
    

    注意,对于这种情况,每一个链表条目都需要一个spinlock保护,因为删除操作将修改条目的deleted标志。此外,该函数如果搜索到条目,返回时应当保持该条目的锁。

    写端的删除操作将变成:

    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)) {
    
                spin_lock(&e->lock);
    
                list_del_rcu(&e->list);
    
                e->deleted = 1;
    
                spin_unlock(&e->lock);
    
                call_rcu(&e->rcu, audit_free_rule, e);
    
                return 0;
    
            }
    
        }
    
        return -EFAULT;     /* No matching rule */
    }
    

    删除条目时,需要标记该条目为已删除。这样读者就可以通过该标志立即得知条目是否已经删除。

    小结

    RCU是2.6内核引入的新的锁机制,在绝大部分为读而只有极少部分为写的情况下,它是非常高效的,因此在路由表维护、系统调用审计、SELinux的AVC、dcache和IPC等代码部分中,使用它来取代rwlock来获得更高的性能。

    但是,它也有缺点,延后的删除或释放将占用一些内存,尤其是对嵌入式系统,这可能是非常昂贵的内存开销。此外,写者的开销比较大,尤其是对于那些无法容忍旧数据的情况以及不只一个写者的情况,写者需要spinlock或其他的锁机制来与其他写者同步。

    如果说我的文章对你有用,只不过是我站在巨人的肩膀上再继续努力罢了。
    若在页首无特别声明,本篇文章由 Schips 经过整理后发布。
    博客地址:https://www.cnblogs.com/schips/
  • 相关阅读:
    Football Foundation (FOFO) TOJ 2556
    JAVA- String类练习
    JAVA- 清除数组重复元素
    Mysql远程登陆错误:ERROR 2003
    Linux学习之路(五)压缩命令
    Linux学习之路(四)帮助命令
    如何识别真Microsoft服务与非Microsoft服务来定位病毒自己的服务
    如何用命令行删除EasyBCD开机选择项?
    JAVA- 成员变量与局部变量的区别
    JAVA- 内部类
  • 原文地址:https://www.cnblogs.com/schips/p/linux_cru.html
Copyright © 2011-2022 走看看