在Linux内核代码中,经常可以看到读取一个变量时,不是直接读取的,而是需要借助一个叫做READ_ONCE的宏;同样,在写入一个变量的时候,也不是直接赋值的,而是需要借助一个叫做WRITE_ONCE的宏。

代码分析

READ_ONCE宏定义如下(代码位于include/linux/compiler.h中):

#define __READ_ONCE(x, check)                        \
({                                  \union { typeof(x) __val; char __c[1]; } __u;           \if (check)                         \__read_once_size(&(x), __u.__c, sizeof(x));        \else                               \__read_once_size_nocheck(&(x), __u.__c, sizeof(x));    \smp_read_barrier_depends();                                     \__u.__val;                            \
})
#define READ_ONCE(x) __READ_ONCE(x, 1)

READ_ONCE宏直接调用了另一个内部定义的宏__READ_ONCE,第二个参数check传的是1。

在__READ_ONCE宏中其实定义了一个大的表达式,表达式的值是最后一个语句的值。这个表达式中先定义了一个联合体,联合体的第一个组成部分是__val,它的类型就是要读取变量的类型;联合体的第二个组成部分是一个只包含一个元素的字符数组__c,这样定义的话,__c就可以当做这个联合体的指针来使用了。然后,还用这个联合体定义了一个变量__u,这个变量是一个局部变量,因此是定义在栈上的。由于check传的是1,接着将调用__read_once_size函数:

#define __READ_ONCE_SIZE                     \
({                                  \switch (size) {                            \case 1: *(__u8 *)res = *(volatile __u8 *)p; break;        \case 2: *(__u16 *)res = *(volatile __u16 *)p; break;      \case 4: *(__u32 *)res = *(volatile __u32 *)p; break;      \case 8: *(__u64 *)res = *(volatile __u64 *)p; break;      \default:                           \barrier();                     \__builtin_memcpy((void *)res, (const void *)p, size);  \barrier();                     \}                              \
})static __always_inline
void __read_once_size(const volatile void *p, void *res, int size)
{__READ_ONCE_SIZE;
}

这个函数的函数体是在宏__READ_ONCE_SIZE中定义的,传入的参数是要读取变量的指针,定义的联合体变量的指针,以及要读取变量的大小。在调用__read_once_size函数时,就将要读取变量的指针转换成了指向volatile变量的指针,告诉编译器要读取的这个变量是volatile的。在C语言中,volatile关键字的作用是:

  1. 声明这个变量易变,不要把它当成一个普通的变量,做出错误的优化。
  2. 保证CPU每次都从内存重新读取变量的值,而不是用寄存器中暂存的值。注意,这里说的是寄存器中缓存的值,而不是CPU缓存中存的值。很多英文文档里面都说了Cache,容易让人产生误解。

__read_once_size函数要完成的操作是将要读取的变量的值拷贝到临时定义的局部联合体变量__u中。如果要读取变量的长度是1、2、4、8字节的时候,直接使用取指针赋值就行了,由于要读取变量的指针已经被转成了volatile的,编译器保证这个操作不会被优化。如果要读取的变量不是上面说的整字节,那么就要用__builtin_memcpy操作进行拷贝了,但前后都需要加上编译器屏障barrier(),这样就可以保证__builtin_memcpy函数调用本身不会被编译器优化掉。

接下来__READ_ONCE宏调用了smp_read_barrier_depends函数,这个函数是为了解决某些特殊CPU架构下的缓存一致性问题的(主要是Alpha),也就是所谓的数据依赖内存屏障,在绝大多数CPU架构下都没什么用处。

__READ_ONCE宏中定义的最后一条语句,就是直接返回局部联合体变量__u中的__val部分,也就是返回要读取变量被拷贝好了的值。由于它是这个表达式的最后一个语句,所以__READ_ONCE宏中定义的表达式的值就是这个值,从而保证了要读取值的变量在使用了READ_ONCE宏后能读取到正确的值。

分析完READ_ONCE宏,那WRITE_ONCE宏就很简单了,基本上就是把READ_ONCE宏要做的事情反过来:

#define WRITE_ONCE(x, val) \
({                          \union { typeof(x) __val; char __c[1]; } __u = \{ .__val = (__force typeof(x)) (val) }; \__write_once_size(&(x), __u.__c, sizeof(x)); \__u.__val;                 \
})

还是定义了一个联合体变量__u,然后直接将要赋值的值读进来。接着调用了__write_once_size函数:

static __always_inline void __write_once_size(volatile void *p, void *res, int size)
{switch (size) {case 1: *(volatile __u8 *)p = *(__u8 *)res; break;case 2: *(volatile __u16 *)p = *(__u16 *)res; break;case 4: *(volatile __u32 *)p = *(__u32 *)res; break;case 8: *(volatile __u64 *)p = *(__u64 *)res; break;default:barrier();__builtin_memcpy((void *)p, (const void *)res, size);barrier();}
}

这次换成了把要赋值变量的指针转换成了指向volatile变量的指针。

WRITE_ONCE宏的最后一条语句还是会返回要赋值的值的,因此也就是说WRITE_ONCE宏是返回要赋值的值的,只不过一般都没什么用。

为什么要用READ_ONCE和WRITE_ONCE宏

通常编译器是以函数为单位对代码进行优化编译的,而且编译器在优化的时候会假设被执行的程序是以单线程来执行的。基于这个假设优化出来的汇编代码,很有可能会在多线程执行的过程中出现严重的问题。可以举几个例子:

1)编译器可以随意优化不相关的内存访问操作,打乱它们的执行次序。

例如,对于如下代码:

a[0] = x;
a[1] = x;

编译器可能会将其优化成:

a[1] = x;
a[0] = x;

这对单线程的程序来说没有问题,因为变量x的值是不会改变的。但是,对于多线程的程序来说,变量x的值可能会被别的线程改变,如果要保证它们的执行顺序,必须加上READ_ONCE宏:

a[0] = READ_ONCE(x);
a[1] = READ_ONCE(x);

注意,一定要两个语句都用READ_ONCE宏,这样才能保证次序,单独用一个还是没法保证。当然,在两条语句中间插入编译器屏障也可以解决这个问题。

又或者,比如下面的程序:

void process_level(void)
{msg = get_message();flag = true;
}void interrupt_handler(void)
{if (flag)process_message(msg);
}

编译器在编译的时候,有可能会把process_level函数优化成:

void process_level(void)
{flag = true;msg = get_message();
}

因为它发现这两条语句没有任何关系,而且第二条语句比第一条语句执行速度要快,但是它并不知道flag位其实是一个标志位,必须要在获得消息后才能被设置成真。这时只能将process_level函数改成:

void process_level(void)
{WRITE_ONCE(msg, get_message());WRITE_ONCE(flag, true);
}

2)如果在编译的时候就能确定某些代码不会被执行到那可能会完全把代码删除。

例如,对于如下的代码:

while (tmp = a)do_something_with(tmp);

如果编译的时候,编译器发现变量a的值永远都是0,那么这条语句就会被优化成:

do { } while (0);

直接删除,什么都不做。这时候,为了保留一定会按照代码执行,那么必须改写成:

while (tmp = READ_ONCE(a))do_something_with(tmp);

还有,对于如下代码:

a = 0;
/* 中间代码没有对变量a赋值 */
......
a = 0;

编译器发现,变量a的值一直是0,那后面再对变量a赋值0就是没有必要的,会直接删除掉最后一个赋值。但是,在多线程程序中,有可能另一个线程更改了变量a。为了保证一定赋值,可以用下面的代码:

WRITE_ONCE(a, 0);
/* 中间代码没有对变量a赋值 */
......
WRITE_ONCE(a, 0);

还存在许多奇奇怪怪的编译器优化,都可以用READ_ONCE和WRITE_ONCE宏告诉编译器别这么做。

不过,READ_ONCE和WRITE_ONCE宏只能保证读写操作不被编译器优化掉,造成多线程执行中出问题,但是它并不能干预CPU执行编译出来的程序,也就是不能解决CPU重排序的问题和缓存一致性的问题,这类问题还是需要使用内存屏障来解决。

而且,由于READ_ONCE和WRITE_ONCE宏的实现原理本身就是借助了C语言的volatile变量,因此如果要读取或者写入的变量本来就是volatile的就不需要再使用这两个宏了。

READ_ONCE和WRITE_ONCE宏与编译器屏障的关系

编译器屏障在Linux内核中是通过调用barrier()宏来实现的,其定义如下(代码位于include/linux/compiler-gcc.h中):

#define barrier() __asm__ __volatile__("": : :"memory")

所以,其实barrier()宏就是往正常的C语言代码里插入了一条汇编指令。这条指令告诉编译器(上面的汇编指令只对GCC编译器有效,其它编译器有对应的别的方法),不要将这条汇编指令前的内存读写指令优化到这条汇编指令之后,同时也不能将这条汇编指令之后的内存读写指令优化到这条汇编指令之前。但是,对于这条汇编指令之前的内存读写指令,以及之后的内存读写指令,想怎么优化都行,没有任何限制。

而READ_ONCE和WRITE_ONCE针对的是读写操作本身,只会影响使用这两个宏的内存访问操作,不能阻止对其它变量的优化操作。

Linux内核中的READ_ONCE和WRITE_ONCE宏相关推荐

  1. Linux 内核中 likely 与 unlikely 的宏定义解析

    在 2.6 内核中,随处能够见到 likely() 和 unlikely() 的身影,那么为什么要用它们?它们之间有什么差别? 首先要明白: if(likely(value)) 等价于 if(valu ...

  2. Linux内核中_IO,_IOR,_IOW,_IOWR宏的用法与解析

    在驱动程序里, ioctl()函数上传送的变量 cmd是应用程序用于区别设备驱动程序请求处理内容的值.cmd除了可区别数字外,还包含有助于处理的几种相应信息. cmd的大小为 32位,共分 4 个域: ...

  3. (六)linux内核中的offsetof与container_of宏

    参考: offsetof与container_of宏[总结] #define offsetof(type, member) (size_t)&(((type*)0)->member)#d ...

  4. Linux内核中_IO,_IOR,_IOW,_IOWR宏

    在驱动程序里, ioctl() 函数上传送的变量 cmd 是应用程序用于区别设备驱动程序请求处理内容的值. cmd 除了可区别数字外,还包含有助于处理的几种相应信息. cmd 的大小为 32 位,共分 ...

  5. Linux内核中的常用宏container_of其实很简单【转】

    转自:http://blog.csdn.net/npy_lp/article/details/7010752 开发平台:Ubuntu11.04 编 译器:gcc version 4.5.2 (Ubun ...

  6. Linux 中的 READ_ONCE和WRITE_ONCE

    源码基于:Linux 5.4 0. 前言 在Linux 内核代码中,经常会看到读取一个变量时,不是直接读取,而是通过 READ_ONCE 宏.同样的,在写入一个变量的时候,也不是直接赋值,而是通过 W ...

  7. Linux内核中max()宏的奥妙何在?(一)

    Linux内核中max()宏的奥妙何在?(一) 1.max()宏那点事 在Linux内核中,有这样四个比较大小的函数,如下: max(x,y) //两个数求最大值 min(x,y) //两个数求最小值 ...

  8. Linux内核中max()宏的奥妙何在?(二)——大神Linus对这个宏怎么看?

    最新max()宏 上回,我们在<Linux内核中max()宏的奥妙何在?(一)>一文中说到,在3.18.34版Linux内核源码中的max()宏,采用了GCC的扩展特性,可以避免一些错误. ...

  9. Linux 内核中的宏定义

    Linux 内核中的宏定义 rtoax 日期 内核版本:linux-5.10.13 注释版代码:https://github.com/Rtoax/linux-5.10.13 __attribute__ ...

最新文章

  1. 复杂数据权限设计方案
  2. Plant Simulation常用命令
  3. 八、TFTP服务器搭建及应用
  4. html滚动菜单置顶,javascript改变position值实现菜单滚动至顶部后固定
  5. 为什么PCB板通常是绿色的?
  6. 数据结构和算法(03)---栈和队列(c++)
  7. 阿里云服务网格ASM集成SLS告警
  8. 阅读众包文献中一些值得mark 的小收获
  9. energy in transition课文翻译_思迪软件科技 招聘 字幕翻译(远程兼职)
  10. 为什么200M宽带还是会很慢?
  11. flash物理引擎应用:你的第一个Fisix应用程序
  12. 分享40套非常精美的免费 PSD 网页图标素材
  13. 如何高效开发支付接口对接
  14. 华为 ---- ISIS 协议
  15. docker swarm 官方文档
  16. 3DS MAX 导入骨骼动画插件
  17. 應電鍍廠要求把5個ITEM的主單位PRIMARY UOM由L改為KG
  18. 基于拦截器实现防表单重复提交
  19. 关于腾讯云域名访问问题的几个可能解决方案
  20. uniapp调用百度地图导航

热门文章

  1. 【语音处理】基于matlab实现语音基频检测
  2. Struts2拦截器-abstractInterceptor
  3. 走出寒冬:PC市场的创新与复苏
  4. Android权限的介绍
  5. ORACLE 11G 查询DBA_SEGMENTS 慢的问题
  6. 诺信EFD的颠覆性UltimusPlus™点胶机将工艺控制提升至新的水平
  7. Linux文件的压缩和解压命令tar
  8. 谈谈程序员35岁职业危机
  9. python:从零开始的百度图片爬虫
  10. 弃用 Docker kill,事实证明,它更牛逼!