rcu_assign_pointer、rcu_dereference、ACCESS_ONCE

题记看代码不要死死的一条一条往后看,要适当的联系上下文,看出整体逻辑性,流程性。事实上,编译器和处理器也不是呆板的一条一条往后执行的,它也会有预取和逻辑判断。

 

由内存屏障到RCU的发布订阅

内存屏障主要解决的问题是编译器的优化和CPU的乱序执行。

 

编译器在优化的时候,生成的汇编指令可能和c语言程序的执行顺序不一样,在需要程序严格按照c语言顺序执行时,需要显式的告诉编译不需要优化,这在linux下是通过barrier()宏完成的,它依靠volidate关键字和memory关键字,前者告诉编译barrier()周围的指令不要被优化,后者作用是告诉编译器汇编代码会使内存里面的值更改,编译器应使用内存里的新值而非寄存器里保存的老值。

 

同样,CPU执行会通过乱序以提高性能。汇编里的指令不一定是按照我们看到的顺序执行的。linux中通过mb()系列宏来保证执行的顺序。简单的说,如果在程序某处插入了mb()/rmb()/wmb()宏,则宏之前的程序保证比宏之后的程序先执行,从而实现串行化。

 

即使是编译器生成的汇编码有序,处理器也不一定能保证有序。就算编译器生成了有序的汇编码,到了处理器那里也拿不准是不是会按照代码顺序执行。所以就算编译器保证有序了,程序员也还是要往代码里面加内存屏障才能保证绝对访存有序,这倒不如编译器干脆不管算了,因为内存屏障本身就是一个sequence point,加入后已经能够保证编译器也有序。

 

处理器虽然乱序执行,但最终会得出正确的结果,所以逻辑上讲程序员本不需要关心处理器乱序的问题。但是在SMP并发执行的情况下,处理器无法知道并发程序之间的逻辑,比如,在不同core上的读者和写者之间的逻辑。简单讲,处理器只保证在单个core上按照code中的顺序给出最终结果。这就要求程序员通过mb()/rmb()/wmb()/read_barrier_depends来告知处理器,从而得到正确的并发结果。内存屏障、数据依赖屏障都是为了处理SMP环境下的数据同步问题,UP根本不存在这个问题。

 

下面分析下内存屏障在RCU上的应用:

#define rcu_assign_pointer(p,v)        ({ \

                                                                       smp_wmb();\

                                                                       (p)= (v); \

                                               })

 

#define rcu_dereference(p)     ({ \

                                               typeof(p)_________p1 =ACCESS_ONCE(p); \

                                               smp_read_barrier_depends();\

                                               (_________p1);\

                                               }) 

        

rcu_assign_pointer()通常用于写者的发布,rcu_dereference()通常用于读者的订阅。

 

写者:

1 p->a = 1;
2 p->b = 2;
3 p->c = 3;
4 rcu_assign_pointer(gp, p);

 

读者:

1 rcu_read_lock();
2 p = rcu_dereference(gp);
3 if (p != NULL) {
4 do_something_with(p->a, p->b, p->c);
5 }
6 rcu_read_unlock();

rcu_assign_pointer()是说,先把那块内存写好,再把指针指过去。这里使用的内存写屏障是为了保证并发的读者读到数据一致性。在这条语句之前的读者读到旧的指针和旧的内存,这条语句之后的读者读到新的指针和新的内存。如果没有这条语句,很有可能出现读者读到新的指针和旧的内存。也就是说,这里通过内存屏障刷新了p所指向的内存的值,至于gp本身的值有没有更新还不确定。实际上,gp本身值的真正更新要等到并发的读者来促发。

rcu_dereference()原语用的是数据依赖屏障,smp_read_barrier_dependence,它要求后面的读操作如果依赖前面的读操作,则前面的读操作需要首先完成。根据数据之间的依赖,要读p->a, p->b, p->c,就必须先读p,要先读p,就必须先读p1,要先读p1,就必须先读gp。也就是说读者所在的core在进行后续的操作之前,gp必须是同步过的当前时刻的最新值。如果没有这个数据依赖屏障,有可能读者所在的core很长一段时间内一直用的是旧的gp值。所以,这里使用数据依赖屏障是为了督促写者将gp值准备好,是为了呼应写者,这个呼应的诉求是通过数据之间的依赖关系来促发的,也就是说到了非呼应不可的地步了。


下面看看kernel中常用的链表操作是如何使用这样的发布、订阅机制的:

写者:

static inline void list_add_rcu(struct list_head *new,struct list_head *head)
{
__list_add_rcu(new, head, head->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;
smp_wmb();
next->prev = new;
prev->next = new;
}

 

读者:

#define list_for_each_entry_rcu(pos, head, member) \

           for(pos = list_entry((head)->next, typeof(*pos), member); \

                       prefetch(rcu_dereference(pos)->member.next),\

                                   &pos->member!= (head); \

                       pos= list_entry(pos->member.next, typeof(*pos), member))

 

写者通过调用list_add_rcu来发布新的节点,其实是发布next->prev, prev->next这两个指针。读者通过list_for_each_entry_rcu来订阅这连个指针,我们将list_for_each_entry_rcu订阅部分简化如下:

pos = prev->next;

prefetch(rcu_dereference(pos)->next);

 

读者通过rcu_dereference订阅的是pos,而由于数据依赖关系,又间接订阅了prev->next指针,或者说是促发prev->next的更新。

 

RCU引出的ACCESS_ONCE

定义

它的定义很简单,在 include/linux/compiler.h的底部:

PLAIN TEXT

C:

1.  #define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))

仅从语法上讲,这似乎毫无意义,先取其地址,在通过指针取其值。而实际上不然,多了一个关键词 volatile,所以它的含义就是强制编译器每次使用 x都从内存中获取。

原因
仅仅从定义来看基本上看不大出来为什么要引入这么一个东西。可以通过几个例子(均来自 Paul,我做了小的修改)看一下。

1. 循环中有每次都要读取的全局变量:

PLAIN TEXT

C:

1.  ...

2.  static int should_continue;

3.  static void do_something(void);

4.  ...

5.                 while (should_continue)

6.                        do_something();

假设 do_something()函数中并没有对变量 should_continue做任何修改,那么,编译器完全有可能把它优化成:

PLAIN TEXT

C:

1.  ...

2.                 if (should_continue)

3.                        for (;;)

4.                                do_something();

这很好理解,不是吗?对于单线程的程序,这么做完全没问题,可是对于多线程,问题就出来了:如果这个线程在执行do_something()的期间,另外一个线程改变了 should_continue的值,那么上面的优化就是完全错误的了!更严重的问题是,编译器根本就没有办法知道这段代码是不是并发的,也就无从决定进行的优化是不是正确的!

这里有两种解决办法:1) should_continue加锁,毕竟多个进程访问和修改全局变量需要锁是很自然的;2)禁止编译器做此优化。加锁的方法有些过了,毕竟 should_continue只是一个布尔,而且退一步讲,就算每次读到的值不是最新的 should_continue的值也可能是无所谓的,大不了多循环几次,所以禁止编译器做优化是一个更简单也更容易的解决办法。我们使用 ACCESS_ONCE()来访问 should_continue

PLAIN TEXT

C:

1.  ...

2.       while (ACCESS_ONCE(should_continue))

3.                        do_something();

2. 指针读取一次,但要dereference多次:

PLAIN TEXT

C:

1.  ...

2.      p = global_ptr;

3.      if (&& p->s && p->s->func)

4.          p->s->func();

那么编译器也有可能把它编译成:

PLAIN TEXT

C:

1.  ...

2.      if (global_ptr && global_ptr->s && global_ptr->s->func)

3.          global_ptr->s->func();

你可以谴责编译器有些笨了,但事实上这是C标准允许的。这种情况下,另外的进程做了 global_ptr = NULL;就会导致后一段代码 segfault,而前一段代码没问题。同上,所以这时候也要用 ACCESS_ONCE()

PLAIN TEXT

C:

1.  ...

2.      p = ACCESS_ONCE(global_ptr);

3.      if (&& p->s && p->s->func)

4.          p->s->func();

3. watchdog中的变量:

PLAIN TEXT

C:

1.  for (;;) {

2.                        still_working = 1;

3.                        do_something();

4.                 }

假设 do_something()定义是可见的,而且没有修改 still_working的值,那么,编译器可能会把它优化成:

PLAIN TEXT

C:

1.  still_working = 1;

2.                 for (;;) {

3.                        do_something();

4.                 }

如果其它进程同时执行了:

PLAIN TEXT

C:

1.  for (;;) {

2.                        still_working = 0;

3.                        sleep(10);

4.                        if (!still_working)

5.                                panic();

6.                 }

通过 still_working变量来检测 wathcdog是否停止了,并且等待10秒后,它确实停止了,panic()!经过编译器优化后,就算它没有停止也会 panic!!所以也应该加上 ACCESS_ONCE()

PLAIN TEXT

C:

1.  for (;;) {

2.                        ACCESS_ONCE(still_working) = 1;

3.                        do_something();

4.                 }

综上,我们不难看出,需要使用 ACCESS_ONCE()的两个条件是:

1. 无锁的情况下访问全局变量;
2.
对该变量的访问可能被编译器优化成合并成一次(上面第13个例子)或者拆分成多次(上面第2个例子)。

例子

Linus 在邮件中给出的另外一个例子是:

编译器有可能把下面的代码:

PLAIN TEXT

C:

1.  if (a> MEMORY) {

2.          do1;

3.          do2;

4.          do3;

5.      } else {

6.          do2;

7.      }

优化成:

PLAIN TEXT

C:

1.  if (a> MEMORY)

2.          do1;

3.      do2;

4.      if (a> MEMORY)

5.          do3;

这里完全符合上面我总结出来的两个条件,所以也应该使用 ACCESS_ONCE()。正如 Linus 所说,不是编译器一定会这么优化,而是你无法证明它不会做这样的优化。

 

原文参考链接

http://blog.csdn.net/jianchaolv/article/details/7527647
http://labs.chinamobile.com/mblog/225/2830
http://wangcong.org/blog/archives/1941


  • 0
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值