免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 2361 | 回复: 9

[内核同步] volatile高效使用讨论 [复制链接]

论坛徽章:
0
发表于 2014-10-28 17:43 |显示全部楼层
volatile的具体使用时机到底是什么?

搜了很多资料,了解到volatile的滥用会导致性能的下降
那么有几个问题希望大牛能帮忙解释下,不胜感激
1. #define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
是为了把非volatile变量拯救。或者说达到“非volatile类型的变量,依然可以直接从内存读取而非寄存器"的目的
那么为什么内核类似源码中。对变量的写操作未加该宏的控制。
是否说对寄存器的写操作 是会立马回写到内存的,也就是说volatile只是控制每次读从内存读,不控制写?

2. 内核加了锁。如spin_lock 是否还需volatile


加锁
读a
解锁
其他操作
加锁
读a
解锁

这里的第二次读a,难道就一定是内存中的,而非寄存器?

按照我的理解是volatile的使用时机
1. 防止代码优化。如驱动中写时序,就可能对一个变量重复赋值
这到不难理解
2.循环。尤其是循环检测一个状态。那么这个状态变量可能一直在寄存器中。那么多核情况下不用volatile就可能导致检测不到其他cpu的修改
3.实时性要求高的情况。
一般的内核编码。应该不用考虑volatile吧。因为
1.除非代码很短且集中或声明了register变量。否者寄存器中的值应该是很快被替换掉的。毕竟寄存器不像cache那么大
2.一般代码即便是读到了register缓存值。问题也不大吧。那有那么多代码逻辑要求那么高的。那么是否可以认为出了问题再解决也可以?

论坛徽章:
6
2015年辞旧岁徽章
日期:2015-03-05 16:13:092015年迎新春徽章
日期:2015-03-05 16:13:092015小元宵徽章
日期:2015-03-06 15:58:1815-16赛季CBA联赛之浙江
日期:2016-11-05 14:38:4115-16赛季CBA联赛之新疆
日期:2016-11-11 18:38:06
发表于 2014-10-29 09:41 |显示全部楼层
1 从宏定义就可以看出它是强制读操作,写操作当然用不上,因为写操作本身就说明你已经有数据的最新版本了,不需要再读。

2 在函数中:

加锁
读a  // 独占访问,无须加关键字
解锁

如果多次加解锁:

加锁
读a
解锁
其他操作  // 这引入了竞争条件
加锁
读a       // 是否数据最新版本,依赖于具体编译器的实现。如果要从语言层面消除此不确定性,加关键字声明
解锁

论坛徽章:
0
发表于 2014-10-29 10:57 |显示全部楼层
回复 2# 爻易
1 从宏定义就可以看出它是强制读操作,写操作当然用不上,因为写操作本身就说明你已经有数据的最新版本了,不需要再读。
可能这个我没太说清楚。可以看下下面我对代码的理解

2 在函数中:
加锁
读a
解锁
其他操作  // 这引入了竞争条件
加锁
读a       // 是否数据最新版本,依赖于具体编译器的实现。如果要从语言层面消除此不确定性,加关键字声明
解锁
您这个理解跟我是一样的。那能否解释下下面代码的理解?


其实 这个问题是看内核netfilter模块rcu锁的使用时产生的。不知道有人看过这块没
代码如下
int nf_register_hook(struct nf_hook_ops *reg)
{
        struct nf_hook_ops *elem;
        int err;

        err = mutex_lock_interruptible(&nf_hook_mutex);
        if (err < 0)
                return err;
        list_for_each_entry(elem, &nf_hooks[reg->pf][reg->hooknum], list) {
/*这里没有使用ACCESS_ONCE相关的函数.基本上相关的资料显示跟您说的是一致的,因为mutex_lock_interruptible屏蔽掉了其它的写操作。那么当前寄存器的值肯定是最新的。那么我的疑问产生了
1.根据大多数资料的显示。volatile类型的变量。其写和读都是立即响应到内存的。那么一个ACCESS_ONCE如何能达到volatile的写目的?
还是说那些资料中说的写其实是很多人的误解,就好像很多人误认为volatile跟cache有关系一样
2.锁屏蔽掉了其他的写操作好像不能说一定就不会出问题吧。
如 cpu1  
rcu_read_lock()
list_for_each_continue_rcu     //这里使用了 ACCESS_ONCE保证寄存器中为最新的。这里假设链表为空,即表头的next和prev都指向自己
rcu_read_unlock()
//这个间隔cpu2 在表里加了一个list节点。这里就有个问题cpu2的寄存器的值会立马写回内存吗?
cpu1继续执行
mutex_lock
list_for_each_entry  //这里没有使用ACCESS_ONCE访问,那么有没有可能寄存中的值还是旧的如现在读到的表为空。那么继续的写操作可就非常危险了。
mutex_unlock
*/
                if (reg->priority < elem->priority)
                        break;
        }
        list_add_rcu(&reg->list, elem->list.prev);
        mutex_unlock(&nf_hook_mutex);
        return 0;
}
int nf_hook_slow(u_int8_t pf, unsigned int hook, struct sk_buff *skb,
                 struct net_device *indev,
                 struct net_device *outdev,
                 int (*okfn)(struct sk_buff *),
{
……
             rcu_read_lock();

        elem = &nf_hooks[pf][hook];
next_hook:
        verdict = nf_iterate(&nf_hooks[pf][hook], skb, hook, indev,
                             outdev, &elem, okfn, hook_thresh);
……
                }
        rcu_read_unlock();
……
}
unsigned int nf_iterate(struct list_head *head,
                        struct sk_buff *skb,
                        unsigned int hook,
                        const struct net_device *indev,
                        const struct net_device *outdev,
                        struct list_head **i,
                        int (*okfn)(struct sk_buff *),
                        int hook_thresh)
{
……
         list_for_each_continue_rcu(*i, head) {
       }
……

}

#define list_for_each_continue_rcu(pos, head) \
        for ((pos) = rcu_dereference_raw((pos)->next); \
                prefetch((pos)->next), (pos) != (head); \
                (pos) = rcu_dereference_raw((pos)->next))
#define rcu_dereference_raw(p)        ({ \
                                typeof(p) _________p1 = ACCESS_ONCE(p); \
                                smp_read_barrier_depends(); \
                                (_________p1); \
                                })

论坛徽章:
6
2015年辞旧岁徽章
日期:2015-03-05 16:13:092015年迎新春徽章
日期:2015-03-05 16:13:092015小元宵徽章
日期:2015-03-06 15:58:1815-16赛季CBA联赛之浙江
日期:2016-11-05 14:38:4115-16赛季CBA联赛之新疆
日期:2016-11-11 18:38:06
发表于 2014-10-29 12:38 |显示全部楼层
只要有过解锁,就意味着缓存在寄存器中的相应数据应当作废,不应该再使用。

但语言层并无加解锁的语义,编译器不明白此语义,它会尽量重复利用寄存器中的值代表变量。

这就需要额外信息通知编译器,明确告诉它某些数据可能已经失去有效性,需要重新装载。

关于写的问题,如果解锁后仍有访问行为,同样可能有缓存行为导致数据不一致。如果不再访问此变量,应该问题不大,编译器通过分析可知此变量已经使用完毕,可以提交到内存(除非你碰到极度脑残编译器)。

语言有弹性,编译器有自己的实现方式,这也是一些程序员更喜欢汇编+人工优化的原因,减少不确定性。

论坛徽章:
6
2015年辞旧岁徽章
日期:2015-03-05 16:13:092015年迎新春徽章
日期:2015-03-05 16:13:092015小元宵徽章
日期:2015-03-06 15:58:1815-16赛季CBA联赛之浙江
日期:2016-11-05 14:38:4115-16赛季CBA联赛之新疆
日期:2016-11-11 18:38:06
发表于 2014-10-29 13:27 |显示全部楼层
yimeng4a309 发表于 2014-10-29 10:57
回复 2# 爻易
根据大多数资料的显示。volatile类型的变量。其写和读都是立即响应到内存的。那么一个ACCESS_ONCE如何能达到volatile的写目的?
还是说那些资料中说的写其实是很多人的误解,就好像很多人误认为volatile跟cache有关系一样 ...


非volatile变量,如果编译器决定用某个寄存器来缓存它,那么读写变量生成的是读写寄存器指令,编译器再决定在适当的时间提交到内存。如果编译器决定不缓存它,编译器生成的是读写内存的指令。

volatile变量,就是通知编译器不要用寄存器来缓存变量,编译器生成的是读写内存的指令。

论坛徽章:
17
水瓶座
日期:2013-08-29 12:09:27白羊座
日期:2014-08-07 12:36:42丑牛
日期:2014-07-24 12:44:41寅虎
日期:2014-04-16 16:15:33寅虎
日期:2014-03-12 09:28:43摩羯座
日期:2014-03-06 13:22:04技术图书徽章
日期:2014-03-06 11:34:50天蝎座
日期:2014-01-09 11:31:44寅虎
日期:2013-12-27 17:01:44双子座
日期:2013-12-27 12:32:29双子座
日期:2013-12-25 09:03:33丑牛
日期:2013-12-24 16:18:44
发表于 2014-10-29 17:41 |显示全部楼层
Documentation/volatile-considered-harmful.txt

ACCESS_ONCE()
http://lwn.net/Articles/508991/

论坛徽章:
6
2015年辞旧岁徽章
日期:2015-03-05 16:13:092015年迎新春徽章
日期:2015-03-05 16:13:092015小元宵徽章
日期:2015-03-06 15:58:1815-16赛季CBA联赛之浙江
日期:2016-11-05 14:38:4115-16赛季CBA联赛之新疆
日期:2016-11-11 18:38:06
发表于 2014-10-29 22:59 |显示全部楼层
外文多了头晕,中文资料已经足够深入明白,剩下的就是去尝试。

论坛徽章:
0
发表于 2014-10-30 10:59 |显示全部楼层
回复 6# asuka2001
外文确实头晕,不过小有收获
结合其他资料大概总结出来一个结论

加锁后不需要volatile
因为一般锁都会有Barrie防止乱序。而cpu跨过barrier后,会自动刷新寄存器缓存。
当然汇编搞的太少。这个也不知道对不对

   

论坛徽章:
0
发表于 2014-10-30 11:09 |显示全部楼层
回复 7# 爻易
说的对。实测出来的才是最正确的结果。不管是不同版本内核 或 lib库,总会有微小区别,导致代码在临界点出现完全不同的结果
可有时候硬件和编译环境有限。没法测试所有情况,就只能先做到理论上的无错,来防止后期环境的扩展

   

论坛徽章:
6
2015年辞旧岁徽章
日期:2015-03-05 16:13:092015年迎新春徽章
日期:2015-03-05 16:13:092015小元宵徽章
日期:2015-03-06 15:58:1815-16赛季CBA联赛之浙江
日期:2016-11-05 14:38:4115-16赛季CBA联赛之新疆
日期:2016-11-11 18:38:06
发表于 2014-10-30 18:09 |显示全部楼层
回复 8# yimeng4a309

Barrie针对的是处理器,让它按期望的序列执行。

volatile针对的是编译器,让它生成期望的内存访问指令。

它们功能不同,所以不能相互替代,正如<<程序员的自我修养...>>里面描述的那样,两方面都要考虑。

对volatile来说,只有在能确定不会发生数据不一致的情况下,可以不加volatile以提高性能,但如果没有把握或无法确定的话还是得加。
   
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP