- 论坛徽章:
- 0
|
本帖最后由 l4rmbr 于 2014-04-23 00:53 编辑
jiufei19 发表于 2014-04-22 20:58
回复 8# l4rmbr
但是,我还是有点疑问,我喜欢刨根问底哈
Hi, jiufei19,
rcu_dereference()其实跟rcu机制没关系,这个名字其实起了文档的作用,表示它要在rcu保护的临界区内使用。
至于它本身的真正作用,如前面答案所提,它其实起到防止乱序的作用。
细究其代码,最终真正生效的是这两行:- r = rcu_dereference(p)
- 等价于
- r = ACCESS_ONCE(p);
- smp_read_barrier_depends();
复制代码 它的作用是防止编译器和处理器两个层次的乱序。
为什么要防止。看例子:- 读者端:
- 1 p = gp;
- 2 if (p != NULL) {
- 3 do_something_with(p->a, p->b, p->c);
- 4 }
复制代码- 写者端:
- p->a = 1;
- p->b = 2;
- p->c = 3;
- gp = p;
复制代码 这里大家会有个假设:
读者端:先读p = gp, 然后再依次访问p->a, p->b, p->c,
写者端:先依次写p->a, p->b, p->c, 然后再赋值给gp.
这是很合理的,也直观的假设,逻辑正确,很多处理器都保持这样的基本的假定。
但是DEC Alpha架构的处理器+指令预测的编译器,可以观察到如此激进的优化:- 它先猜测p的值,然后预取p->a, p->b, p->b;然后再取p, 来判断p是否猜测正确。
复制代码 那么,就会出现这种情况:
读者端先预测到p会被加载进gp的地址, 所以它高兴地先预读进来(p = gp),然后读了p->a, p->b, p->c的值。
注意,此时写端的赋值可能还没发生,所以读端预读到的可能是原来旧的值,假设依次为9, 8, 7
然后,等下p真的被加载进gp的地址了,处理器检查一下,发现:呀,我真聪明,预取对了,所以它就理所当然地
认为p-a, p->b, p->c是它之前预取的值。
但是, 这中间,写端可能已经写了新值,即p->a=1, p->b=2, p->c=3
这样就出了大问题了, 读者端自作聪明的预读,结果读到了旧值,它还以为是正确的。
所以, 必要的次序要保证,这里其实有一个数据依赖的关系, 即p的值应该先读,再读p->a, p->b, p->c,
因此,要加一个屏障,也就是可以严格限定存取顺序的指令。
比如- 读者端:
- 1 p = gp;
- ### smp_read_barrier_depends ###
- 2 if (p != NULL) {
- 3 do_something_with(p->a, p->b, p->c);
- 4 }
复制代码- 写者端:
- p->a = 1;
- p->b = 2;
- p->c = 3;
- ### smp_mb ###
- gp = p;
复制代码 加上这两个屏障后,就可以保证,只要在读者端看到p是gp, 那么p->a, p->b ,p->c一定是最新的;因为,写者端也有一个屏障,它保证了先更新p->a, p->b, p->c后再更新gp = p
因此, rcu_dereference() 不过就是加载指针 , 再加一个屏障的封装
而rcu_assign_pointer()不过就是一个屏障,再加赋值指针的封装
可以注意到,屏障指令在读写两端都需要的,而且使用的顺序相反。只有读,写两端配合,才能保证想要的顺序。
关于这个内存屏障,可以参考我之前写过的文章:Linux内核中的内存屏障(1) |
|