RCU机制

~~ cite from《Linux Device Driver》

RCU是一种高级的互斥机制,在正确的条件下,也可获得高性能。RCU对它保护的数据结构做了一些限定。它针对经常发生读取而很少写入的情形做了优化。被保护的资源应该通过指针访问,而对这些资源的引用必须仅有原子代码拥有。在需要修改该结构时,写入线程首先复制,然后修改副本,之后用新的版本替代相关指针,这也是算法名称的由来。当内核确信老的版本没有其他引用时,就可释放老的版本。

使用RCU的代码应包含<linux/rcupdate.h>

在读取端,代码时候受RCU保护的数据结构时,必须将引用数据结构的代码包括在rcu_read_lock和rcu_read_unlock调用之间。RCU代码可能如下所示:

   1:  struct my_stuff *stuff;
   2:   
   3:  rcu_read_lock();
   4:  stuff = find_the_stuff(args, ...);
   5:  do_something_with_stuff(stuff);
   6:  rcu_read_unlock();

rcu_read_lock调用非常快,它会禁止内核抢占,但不会等待任何东西。用来检验读取“锁”的代码必须是原子的。在调用rcu_read_unlock之后,就不应该存在对受保护结构的任何引用。

用来修改受保护结构的代码必须在一个步骤中完成。第一步很简单,只需要分配一个新的结构,如果必要则从老的结构中复制数据,然后将读取代码能看到的指针替换掉。这时,读取端会假定修改已经完成,任何进入临界区的代码将看到数据的新版本。

剩下的工作就是释放老的数据结构。当然,问题在于,在其他处理器上运行的代码可能仍在引用老的数据,因此不能立即释放老的结构。相反,写入代码必须等待直到能够确信不存在这样的引用。因为拥有对该数据结构的引用的代码必须(规则规定)是原子的,因此我们可以知道,一旦系统中的每个处理器都至少调度一次之后,所有的引用都会消失。因此,RCU所做的工作就是,设置一个回调函数并等待所有的处理器被调度,之后由回调函数执行清除工作。

修改受RCU保护的数据结构的代码必须通过分配一个struct rcu_head数据结构来获得清除用的回调函数,但并不需要用什么方式来初始化这个结构。通常,这个结构内嵌在由RCU保护的大资源当中。在修改完资源之后,应该做如下调用:

   1:  void call_rcu(struct rcu_head *head, void (*func)(void *arg), void *arg);
在可安全释放该资源时,给定的func会调用,传递到call_rcu的相同参数也会传递给这个函数。通常,func要做的唯一工作就是调用free。

再谈Linux内核中的RCU机制(http://blog.chinaunix.net/uid-23769728-id-3080134.html)

RCU的设计思想比较明确,通过新老指针替换的方式来实现免锁方式的共享保护。但是具体到代码的层面,理解起来还是会有些困难。在《深入Linux设备驱动程序内核机制》第4章中,已经明确地叙述了RCU背后所遵循的规则,这些规则是从一个比较高的视角来看,因为我觉得过多的代码分析反而容易让读者在细节上迷失方向。最近拿到书后,我又重头仔细看了RCU部分的文字,觉得还应该补充一点点内容,因为有些东西不一定适合写在书里。

   1:  /**
   2:   * rcu_read_lock() - mark the beginning of an RCU read-side critical section
   3:   *
   4:   * When synchronize_rcu() is invoked on one CPU while other CPUs
   5:   * are within RCU read-side critical sections, then the
   6:   * synchronize_rcu() is guaranteed to block until after all the other
   7:   * CPUs exit their critical sections.  Similarly, if call_rcu() is invoked
   8:   * on one CPU while other CPUs are within RCU read-side critical
   9:   * sections, invocation of the corresponding RCU callback is deferred
  10:   * until after the all the other CPUs exit their critical sections.
  11:   *
  12:   * Note, however, that RCU callbacks are permitted to run concurrently
  13:   * with new RCU read-side critical sections.  One way that this can happen
  14:   * is via the following sequence of events: (1) CPU 0 enters an RCU
  15:   * read-side critical section, (2) CPU 1 invokes call_rcu() to register
  16:   * an RCU callback, (3) CPU 0 exits the RCU read-side critical section,
  17:   * (4) CPU 2 enters a RCU read-side critical section, (5) the RCU
  18:   * callback is invoked.  This is legal, because the RCU read-side critical
  19:   * section that was running concurrently with the call_rcu() (and which
  20:   * therefore might be referencing something that the corresponding RCU
  21:   * callback would free up) has completed before the corresponding
  22:   * RCU callback is invoked.
  23:   *
  24:   * RCU read-side critical sections may be nested.  Any deferred actions
  25:   * will be deferred until the outermost RCU read-side critical section
  26:   * completes.
  27:   *
  28:   * You can avoid reading and understanding the next paragraph by
  29:   * following this rule: don't put anything in an rcu_read_lock() RCU
  30:   * read-side critical section that would block in a !PREEMPT kernel.
  31:   * But if you want the full story, read on!
  32:   *
  33:   * In non-preemptible RCU implementations (TREE_RCU and TINY_RCU), it
  34:   * is illegal to block while in an RCU read-side critical section.  In
  35:   * preemptible RCU implementations (TREE_PREEMPT_RCU and TINY_PREEMPT_RCU)
  36:   * in CONFIG_PREEMPT kernel builds, RCU read-side critical sections may
  37:   * be preempted, but explicit blocking is illegal.  Finally, in preemptible
  38:   * RCU implementations in real-time (CONFIG_PREEMPT_RT) kernel builds,
  39:   * RCU read-side critical sections may be preempted and they may also
  40:   * block, but only when acquiring spinlocks that are subject to priority
  41:   * inheritance.
  42:   */
  43:  static inline void rcu_read_lock(void)
  44:  {
  45:      __rcu_read_lock();
  46:      __acquire(RCU);
  47:      rcu_lock_acquire(&rcu_lock_map);
  48:  }

该实现里面貌似有三个函数调用,但实质性的工作由第一个函数__rcu_read_lock()完成,__rcu_read_lock()通过调用preempt_disable()关闭内核可抢占性。但是中断是允许的,假设读取者正处于rcu临界区中且刚读取了一个共享数据区的指针p(但是还没有访问p中的数据成员),发生了一个中断,而该中断例程ISR恰好需要修改p所指向的数据区,按照RCU的设计原则,ISR会新分配一个同样大小的数据区new_p,再把老数据区p中的数据拷贝到新数据区,接着在new_p基础上做数据修改的工作(因为是在new_p空间中修改,所以不存在对p的并发访问),因此说RCU是一种免锁机制。ISR在把数据更新的工作完成后,将new_p赋值给p(p=new_p),最后它会再注册一个回调函数用以在适当的时候释放老指针p。因此,只要对老指针p上的所有引用都结束了,释放p就不会有问题。当中断处理例程做完这些工作返回后,被中断的进程将依然访问到p空间上的数据,也就是老数据,这样的结果是RCU机制所允许的。RCU规则对读取者与写入者之间因指针切换所造成的短暂的资源视图不一致问题是允许的。

接下来关于RCU一个有趣的问题是:何时才能释放老指针。我见过很多书中对此的回答是:当系统中所有处理器上都发生了一次进程切换。这种程式化的回答让刚刚接触RCU机制的读者感到一头雾水,为什么非要等所有处理器上都发生一次进程切换才可以调用回调函数释放老指针呢?这其实是RCU的设计规则决定的:所有老指针的引用只可能发生在rcu_read_lock与rcu_read_unlock所包括的临界区中,而在这个临界区中不可能发生进程切换,而一旦出了该临界区就不应该再有任何形式的对老指针p的引用。很明显,这个规则要求读取者在临界区中不能发生进程切换,因为一旦有进程切换,释放老指针的回调函数就有可能被调用,从而导致老指针被释放掉,当被切换的进程被重新调度运行时它就有可能引用到一个被释放掉的内存空间。

现在我们看到为什么rcu_read_lock只需要关闭内核可抢占性就可以了,因为它使得即便在临界区中发生了中断,当前进程也不可能别切换出去。内核开发者,确切的说,RCU的设计者所能做的只能到这个程度。接下来就是使用者的责任了,如果爱RCU的临界区中调用了一个函数,该函数可能睡眠,那么RCU的规则就遭到了破坏,系统将进入到一种不稳定的状态。

这再次说明,如果想使用一个东西,一定要搞清楚其内在的机制,像上面刚提到的那个例子,即便现在程序不出现问题,但是系统中留下的隐患如同一个定时炸弹,随时可能被引爆,尤其是过了很长时间问题才突然爆发出来。绝大数情形下,找到问题所花费的时间可能要远远大于静下心来搞懂RCU的原理要多得多。

RCU中的读取者相对rwlock的读取者而言,自由度更高。因为RCU的读取者在访问一个共享资源时,不需要考虑写入者的感受,这不同于rwlock的写入者,rwlock reader在读取共享资源时需要确保没有写入者在操作资源。两者之间的差异化源自RCU对共享资源在读取者与写入者之间进行分离,而rwlock的读取者和写入者则至始至终只是用资源共享的一份拷贝。这也意味着RCU中写入者需要承担更多的责任,而且对同一资源进行更新的多个写入者之间必须引入某种互斥机制,所以RCU属于一种"免锁机制"的说法仅限于读者与写入者之间。所以我们看到:RCU机制应该在有大量的读取操作,而更新操作相对较少的情形下。此时RCU可以大大提升系统的性能,因为RCU的读取操作相对其他一些有锁机制而言,在锁上的开销几乎没有。

实际使用中,共享的资源常常以链表的形式存在,内核为RCU模式下的链表操作实现了几个接口函数,读取者和使用者应该使用这些内核函数,比如list_add_tail_rcu,list_add_rcu,hlist_replace_rcu等等,具体的使用可以参考某些内核编程或者设备驱动程序方面的资料。

在释放老指针方面,Linux内核提供两种方法供使用者使用,一个是调用call_rcu,另一个是调用synchronize_rcu。前者是一种异步方式,call_rcu会将释放老指针的回调函数放入一个节点中,然后将该节点加入到当前正在运行call_rcu的处理器的本地链表中,在时间中断的softirq部分(RCU_SOFTIRQ),rcu软中断处理函数rcu_process_callbacks会检查当前处理器是否经历了一个休眠期(quiescent,此处设计内核进程调度等方面的内容),rcu的内核代码实现在确定系统中所有的处理器都经历了一个休眠期之后(意味着所有处理器上都发生了一次进程切换,因此老指针此时可以被安全释放掉了),将调用call_rcu提供的回调函数。

synchronize_rc的实现则利用了等待队列,在它的实现过程中也会向call_rcu那样想当前处理器的本地链表中加入一个节点,与call_rcu不同之处在于该节点中的回调函数是wake_after_rcu,然后synchronize_rcu将在一个等待队列中睡眠,知道系统中所有处理器都发生了一次进程切换,因而wakeme_after_rcu被rcu_process_callbacks所调用以唤醒睡眠的synchronize_rcu,被唤醒之后,synchronize_rcu知道它现在可以释放老指针了。所以我们看到,call_rcu返回后其注册的回调函数可能还没有被调用,因而也就意味着老指针还未被释放,而synchronize_rcu返回后老指针肯定被释放了。所以,是调用call_rcu还是synchronize_rcu,要视特定需求与当前上下文而定,比如中断处理的上下文肯定不能使用synchronize_rcu函数了。

例子

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