Linux: 为什么不应该在内核代码中使用 volatile ?

文章目录

  • 1. 前言
  • 2. 背景
  • 3. 为什么不应该在内核代码中使用 volatile ?
  • 4. 参考资料

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 背景

本文基于 Linux 内核文档 Why the “volatile” type class should not be used 进行翻译,加上了笔者的理解后整理而成。本文并非对原文一对一的翻译,这一点提请读者注意。

3. 为什么不应该在内核代码中使用 volatile ?

C 程序员通常认为 volatile 意味着变量可以在当前执行线程之外进行更改,因此,当使用共享数据结构时,他们有时会试图在内核代码中使用它。换句话说,C 程序员通把 volatile 类型变量视为一种原子变量,但事实并非如此。在内核代码中使用 volatile 几乎从来都不是正确的,本文将介绍原因。
关于 volatile,需要理解的关键点是,它的目的是抑制优化,而这几乎从来都不是人们真正想要做的事情。在内核中,必须保护共享数据结构免受不必要的并发访问,防止不必要的并发过程还将以更有效的方式避免几乎所有与优化相关的问题。
volatile 一样,使并发访问数据安全的内核原语自旋锁互斥锁内存屏障等)旨在防止不必要的优化。如果使用得当,也无需使用 volatile 。如果仍然需要 volatile,那么几乎可以肯定代码中的某个地方存在错误。在正确编写的内核代码中,volatile 只能减慢速度。
考虑如下内核代码片段:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

如果所有对共享数据 shared_data 访问的代码都进行上锁操作,则在持有 the_lock 锁时,shared_data 的值不会出现意外更改。在 the_lock 锁持有期间,任何其他想要使用 shared_data 数据的代码都要等待锁 the_lock 的释放。自旋锁原语充当内存屏障的角色 - 它们被显式的编写成这样 - 这意味着数据访问不会在自旋锁覆盖的代码段之间进行优化。因此,编译器可能认为“记住”了变量 shared_data 中的内容(如将数据内容缓存到寄存器中),但 spin_lock() 调用,它会起到内存屏障的作用,所以将迫使编译器“忘记”它所知道的任何内容,因此访问 shared_data 数据时不会出现优化问题。我们来看一下 spinlock 的实现(这里只看特定于 ARMv7 架构的“叫号(tickets)”实现版本,spinlock 经历很多代的变化,实现各有不同,但不管怎么实现,都需保持 Linux 设定的相同语义),理解它为什么可以充当内存屏障的角色:

/* include/linux/spinlock.h */
#define raw_spin_lock(lock) _raw_spin_lock(lock)static __always_inline void spin_lock(spinlock_t *lock)
{raw_spin_lock(&lock->rlock);
}/* kernel/spinlock.c */
#ifndef CONFIG_INLINE_SPIN_LOCK
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{__raw_spin_lock(lock);
}
EXPORT_SYMBOL(_raw_spin_lock);
#endif/* include/linux/spinlock_api_smp.h */
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{preempt_disable(); /* 禁用当前 CPU 上的抢占 */spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); // 不用关注LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); // do_raw_spin_lock()
}/* include/linux/spinlock.h */
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{__acquire(lock); // 不用关注arch_spin_lock(&lock->raw_lock);
}/* arch/arm/include/asm/spinlock.h */
static inline void arch_spin_lock(arch_spinlock_t *lock)
{unsigned long tmp;u32 newval;arch_spinlock_t lockval;prefetchw(&lock->slock);/* lock->next += 1 */__asm__ __volatile__(
"1: ldrex %0, [%3]\n" /* lockval = { .slock = lock->slock } */
" add %1, %0, %4\n" /* newval = lockval.slock + (1 << TICKET_SHIFT) */
" strex %2, %1, [%3]\n" /* lock->slock = newval ==> lock->slock += 1 */
" teq %2, #0\n" /* if (tmp != 0) // tmp != 0 表示strex写没有成功,需继续尝试 */
" bne 1b"    /*  goto 1b; */: "=&r" (lockval), "=&r" (newval), "=&r" (tmp): "r" (&lock->slock), "I" (1 << TICKET_SHIFT) /* I: 立即数 */: "cc");/** 条件* lockval.tickets.next != lockval.tickets.owner* 表示锁已被占,所以此次上锁请求需等待锁占有者释放,* 直到轮到自己的请求号牌ID @next。** 从这里我们可以理解到,为什么要使用临时变量 @lockval* 来复制锁 @lock 的内容,主要是保存的请求号牌ID @next ,* 因为每个上锁请求都会更新 @lock 的 @next ,所以每个请* 求者得使用临时变量记录自己的号牌ID (@next)。*/while (lockval.tickets.next != lockval.tickets.owner) {wfe();/* 读取当前的叫号ID: * arch_spin_unlock() 会更新它,相当于叫号机器 */lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);}/** 插入一个内存屏障,使得之前对 spinlock 的读写操作立马对系统中其它 CPU 可见:* 宣告 spinlock 已被持有,同时保证临界区的存储操作不会跨越到锁前,锁前的存储* 操作也不能跨过锁进入临界区内,也即防止了锁定前后存储操作的乱序。*/smp_mb();
}

从上面看到,spin_lock() 调用的最后插入了内存屏障 smp_mb()宣告了 spinlock 已被持有,同时保证临界区的存储操作不会跨越到锁前,锁前的存储操作也不能跨过锁进入临界区内,也即防止了锁定前后存储操作的乱序。对应的,spin_unlock() 调用的最后,也会有内存屏障操作,宣告了 spinlock 已经释放,其它用户可以来竞争 spinlock 了,同时保证让临界区内的存储操作对系统中其它 CPU 核可见:

spin_unlock()...arch_spin_unlock()/* arch/arm/include/asm/spinlock.h */
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{/** 保证在 spinlock 锁释放前,让 spinlock 锁定的* 临界区内的读写操作对系统中其它 CPU 可见。 */smp_mb();lock->tickets.owner++; /* 叫号下一位: 轮到下一个排队的了 *//** 所有因发起了占锁请求而等待的CPU,都会因此需要* 刷新 @lock->tickets.owner 所在的 cache line ,* 而事实上,只有轮到叫号的请求者,才能获取到锁,* 所以其他没轮到的叫号的CPU完全是没必要刷新自己* 的 cache ,这会造成不必要的开销。* 对此的改进,就是 spin lock 的下一代: MCS Lock 。*/dsb_sev();
}

如果 shared_data 被声明为 volatile,锁仍然是必须的。如果所有对 shared_data 的访问都遵循持锁操作的规则,那么当处于被锁保护的临界代码段中对数据 shared_data 进行访问时,不可能出现有其它地方并发访问 shared_data 的情形,这个时候,编译器可以优化对 shared_data 的访问,但是因为shared_data 被声明为 volatile,这会阻止编译器优化对临界代码段shared_data 的访问,这会造成不必要性能损失。在处理共享数据时,适当的锁定会使 volatile 变得不必要,并且可能有害。
volatile 最初是为内存映射的 I/O 寄存器设计的。在内核中,寄存器访问也应该受到锁的保护,但也不希望编译器在临界代码段优化寄存器访问。但是,在内核中,I/O 内存访问始终通过特定访问函数接口完成;直接通过指针访问 I/O 内存是不恰当的,并且不适用于所有架构。编写这些 I/O 内存访问接口函数是为了防止不必要的优化,因此,再一次,不需要 volatile。常见的 I/O 内存访问接口函数 有(以 ARM 架构为例):

/* IO barriers */
#ifdef CONFIG_ARM_DMA_MEM_BUFFERABLE
#include <asm/barrier.h>
#define __iormb()  rmb()
#define __iowmb()  wmb()
#else
#define __iormb()  do { } while (0)
#define __iowmb()  do { } while (0)
#endif/**  IO port access primitives*  -------------------------** The ARM doesn't have special IO access instructions; all IO is memory* mapped.  Note that these are defined to perform little endian accesses* only.  Their primary purpose is to access PCI and ISA peripherals.** Note that for a big endian machine, this implies that the following* big endian mode connectivity is in place, as described by numerous* ARM documents:**    PCI:  D0-D7   D8-D15 D16-D23 D24-D31*    ARM: D24-D31 D16-D23  D8-D15  D0-D7** The machine specific io.h include defines __io to translate an "IO"* address to a memory address.** Note that we prevent GCC re-ordering or caching values in expressions* by introducing sequence points into the in*() definitions.  Note that* __raw_* do not guarantee this behaviour.** The {in,out}[bwl] macros are for emulating x86-style PCI/ISA IO space.*/
#ifdef __io
#define outb(v,p) ({ __iowmb(); __raw_writeb(v,__io(p)); })
#define outw(v,p) ({ __iowmb(); __raw_writew((__force __u16) \cpu_to_le16(v),__io(p)); })
#define outl(v,p) ({ __iowmb(); __raw_writel((__force __u32) \cpu_to_le32(v),__io(p)); })#define inb(p) ({ __u8 __v = __raw_readb(__io(p)); __iormb(); __v; })
#define inw(p) ({ __u16 __v = le16_to_cpu((__force __le16) \__raw_readw(__io(p))); __iormb(); __v; })
#define inl(p) ({ __u32 __v = le32_to_cpu((__force __le32) \__raw_readl(__io(p))); __iormb(); __v; })#define outsb(p,d,l)  __raw_writesb(__io(p),d,l)
#define outsw(p,d,l)  __raw_writesw(__io(p),d,l)
#define outsl(p,d,l)  __raw_writesl(__io(p),d,l)#define insb(p,d,l)  __raw_readsb(__io(p),d,l)
#define insw(p,d,l)  __raw_readsw(__io(p),d,l)
#define insl(p,d,l)  __raw_readsl(__io(p),d,l)
#endif/**  Memory access primitives*  ------------------------** These perform PCI memory accesses via an ioremap region.  They don't* take an address as such, but a cookie.** Again, these are defined to perform little endian accesses.  See the* IO port primitives for more information.*/
#ifndef readl
#define readb_relaxed(c) ({ u8  __r = __raw_readb(c); __r; })
#define readw_relaxed(c) ({ u16 __r = le16_to_cpu((__force __le16) \__raw_readw(c)); __r; })
#define readl_relaxed(c) ({ u32 __r = le32_to_cpu((__force __le32) \__raw_readl(c)); __r; })#define writeb_relaxed(v,c) __raw_writeb(v,c)
#define writew_relaxed(v,c) __raw_writew((__force u16) cpu_to_le16(v),c)
#define writel_relaxed(v,c) __raw_writel((__force u32) cpu_to_le32(v),c)#define readb(c)  ({ u8  __v = readb_relaxed(c); __iormb(); __v; })
#define readw(c)  ({ u16 __v = readw_relaxed(c); __iormb(); __v; })
#define readl(c)  ({ u32 __v = readl_relaxed(c); __iormb(); __v; })#define writeb(v,c)  ({ __iowmb(); writeb_relaxed(v,c); })
#define writew(v,c)  ({ __iowmb(); writew_relaxed(v,c); })
#define writel(v,c)  ({ __iowmb(); writel_relaxed(v,c); })#define readsb(p,d,l)  __raw_readsb(p,d,l)
#define readsw(p,d,l)  __raw_readsw(p,d,l)
#define readsl(p,d,l)  __raw_readsl(p,d,l)#define writesb(p,d,l)  __raw_writesb(p,d,l)
#define writesw(p,d,l)  __raw_writesw(p,d,l)
#define writesl(p,d,l)  __raw_writesl(p,d,l)#else...#endif /* readl */

看到了吧,这些 I/O 内存访问接口在必要的时候,添加了 __iormb() (实现为 rmb())__iowmb() (实现为 wmb()) 内存屏障 。这些 I/O 内存访问接口是硬件架构强相关的,每个架构的实现都有不同,编程时应使用内核提供的统一接口,保证代码在任何架构都能正确工作,以及可移植性。
另一种可能倾向于使用 volatile 的情况是,当处理器忙等变量的值时,执行繁忙等待的正确方法是:

while (my_variable != what_i_want)cpu_relax();

cpu_relax() 调用可以降低 CPU 功耗,它也恰好充当编译器屏障,因此,再一次,volatile 是不必要的。当然,忙等通常是一种反人类的行为。
在极少数情况下,volatile 在内核中是有意义的:

. 上述 I/O 接口函数可能在直接 I/O 内存访问确实有效的架构上使用 volatile,及使用 *(volatile int *)io_addr 的形式进行读写。此时,从本质上讲,每个I/O 接口函数调用本身都(因为 volatile)变成了一个微小的临界区,并确保访问按程序员的预期进行。
. 更改内存但没有其他可见副作用的内联汇编代码可能会被 GCC 删除。将 volatile关键字添加到 asm 语句将阻止此类删除。
. jiffies 变量的特殊之处在于,它每次被引用时都可以有不同的值,但它可以在没有任何特殊锁定的情况下读取。因此,jiffies 可能是不稳定的,但添加其他此类变量是强烈反对的。在这方面,jiffies 被认为是一个“愚蠢的遗产”问题(Linus 的原话)。修复它会比它的价值更麻烦。
. 指向一致性内存中数据结构的指针可能会被 I/O 设备修改,此时使用 volatile 修饰它们可能是需要的。如网络适配器使用的环形缓冲区(该适配器更改指针以指示已处理的描述符)是此类情况的一个示例。

对于大多数代码,上述使用 volatile 的理由都不适用。因此,使用 volatile 通常会被视为一个错误,需要对代码进行额外的审查。想要使用 volatile 的开发人员应该退后一步,想想他们真正想要实现的目标。
删除 volatile 变量的补丁通常是受欢迎的 - 只要它们带有一个理由,表明并发问题已经过适当的考虑。

4. 参考资料

[1] https://lwn.net/Articles/233481/
[2] https://lwn.net/Articles/233482/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/844430.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【YashanDB知识库】自动选举配置错误引发的一系列问题

问题现象 问题出现的步骤/操作&#xff1a; ● 配置自动选举&#xff0c;数据库备库手动发起switch over&#xff0c;命令会报错 ● 主、备库变为只读状态&#xff0c;数据库无法进行读写操作 ● shutdown immediate 停止数据库&#xff0c;此时发现数据库一直没有退出&…

C++ Primer Chapter 2 Variables and Basic Types

C Primer Chapter 2 Variables and Basic Types 2024/05/27 2.3 复合类型 引用 定义 通过将声明符写成&d的形式来定义引用类型&#xff0c;其中d是声明的变量名。 int ival1024; int &refValival; int &refVal2; //报错&#xff1a;引用必须被初始化 引用即别名 …

script 标签中 defer 和 async 属性的区别

script 标签中的 defer Vs. async 在 HTML 中&#xff0c;script 标签可以使用 defer 和 async 属性来控制外部 JavaScript 脚本加载和执行的方式。defer 和 async 都可以提高页面的加载性能&#xff0c;主要区别整理如下。 区别点deferasync加载顺序按顺序加载异步加载&…

论文笔记:Vision GNN: An Image is Worth Graph of Nodes

neurips 2022 首次将图神经网络用于视觉任务&#xff0c;同时能取得很好的效果 1 方法 2 架构 在计算机视觉领域&#xff0c;常用的 transformer 通常是 isotropic 的架构&#xff08;如 ViT&#xff09;&#xff0c;而 CNN 更喜欢使用 pyramid 架构&#xff08;如 ResNet&am…

开源数据库同步工具DBSyncer

前言&#xff1a; 这么实用的工具&#xff0c;竟然今天才发现&#xff0c;相见恨晚呀&#xff01;&#xff01;&#xff01;&#xff01; DBSyncer&#xff08;英[dbsɪŋkɜː]&#xff0c;美[dbsɪŋkɜː 简称dbs&#xff09;是一款开源的数据同步中间件&#xff0c;提供M…

必看项目|多维度揭示心力衰竭患者生存关键因素(生存分析、统计检验、随机森林)

1.项目背景 心力衰竭是一种严重的公共卫生问题,影响着全球数百万人的生活质量和寿命,心力衰竭的病因复杂多样,既有个体生理因素的影响,也受到环境和社会因素的制约,个体的生活方式、饮食结构和医疗状况在很大程度上决定了其心力衰竭的风险。在现代社会,随着生活水平的提…

使用moquette mqtt发布wss服务

文章目录 概要一、制作的ssl证书二、配置wss小结 概要 moquette是一款不错的开源mqtt中间件&#xff0c;github地址&#xff1a;https://github.com/moquette-io/moquette。我们在发布mqtt服务的同时&#xff0c;是可以提供websocket服务器的&#xff0c;有些场景下需要用到&a…

OpenAI新模型开始训练!GPT6?

国内可用潘多拉镜像站GPT-4o、GPT-4&#xff08;更多信息请加Q群865143845&#xff09;: 站点&#xff1a;https://xgpt4.ai0.cn/ OpenAI 官网 28 日发文称&#xff0c;新模型已经开始训练&#xff01; 一、新模型开始训练 原话&#xff1a;OpenAI has recently begun training…

价值飙升30%,AI PC拉动半导体出货潮

由于处理器和DRAM的升级&#xff0c;大摩预测每台AI PC的半导体价值将增长20%-30%&#xff0c;PC平均售价也将提高7%。 台北国际电脑展即将于6月2日隆重开幕。 随着展会的临近&#xff0c;各种现象级的AI PC也蓄势待发。 就在上周&#xff0c;联想在业绩会上&#xff0c;首次…

2-EMMC启动及各分区文件生成过程

EMMC的使用比nand flash还是复杂一些&#xff0c;有其特有的分区和电器性能 1、启动过程介绍 跟普通nand或spi flash不同&#xff0c;uboot前面还有好几级 在vendor某些厂商的设计中&#xff0c;ATF并不是BOOTROM加载后的第一个启动镜像&#xff0c;可能是这样的&#xff1a; …

java的方法重写

重写的概述 重写是基于继承来说的&#xff0c;因为父类的方法需求不满足于子类&#xff0c;所以就要在进行方法重写&#xff0c;如果不知道继承是啥可以看我上一篇笔记 在这里用代码举个栗子 例如&#xff1a;我们定义了一个动物类代码如下&#xff1a; public class Animal…

Leecode热题100---二分查找--4:寻找两个正序数组的中位数

题目&#xff1a; 给定两个大小分别为 m 和 n 的正序&#xff08;从小到大&#xff09;数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。 解法1、暴力解法&#xff08;归并&#xff09; 思路&#xff1a; 合并 nums1&#xff0c;nums2 为第三个数组 排序第三个数…

XXL-JOB分布式任务调度框架详解(全网最详细!!!)

​​​​​​​ 引言 第一部分&#xff1a;XXL-JOB概述 第二部分&#xff1a;架构与组件 第三部分&#xff1a;使用教程 第四部分&#xff1a;源码分析 第五部分&#xff1a;最佳实践 引言 在分布式系统中&#xff0c;任务调度是一项基础而又关键的服务&#xff0c;它涉…

Java设计模式:享元模式实现高效对象共享与内存优化(十一)

码到三十五 &#xff1a; 个人主页 目录 一、引言二、享元设计模式的概念1. 对象状态的划分2. 共享机制 三、享元设计模式的组成四、享元设计模式的工作原理五、享元模式的使用六、享元设计模式的优点和适用场景结语 [参见]&#xff1a; Java设计模式&#xff1a;核心概述&…

解决Spring BeanCreationException的常见问题

解决Spring BeanCreationException的常见问题 在使用Spring框架进行开发时&#xff0c;可能会遇到各种异常&#xff0c;其中之一就是BeanCreationException。本文将介绍如何解决以下特定的异常&#xff1a; org.springframework.beans.factory.BeanCreationException: Error …

拼接字符串

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 使用“”运算符可完成对多个字符串的拼接&#xff0c;“”运算符可以连接多个字符串并产生一个字符串对象。 例如&#xff0c;定义两个字符串&#…

任务3.1:采用面向对象方式求三角形面积

面向对象编程&#xff08;OOP&#xff09;是一种将现实世界中的实体抽象为对象&#xff0c;并通过类和对象来模拟现实世界中的行为和属性的编程范式。在本实战任务中&#xff0c;我们通过创建一个Triangle类来模拟现实世界中的三角形&#xff0c;并使用面向对象的方法来求解三角…

「清新题精讲」CF249D - Donkey and Stars

更好的阅读体验 CF249D - Donkey and Stars Description 给定 n n n 个点 ( x i , y i ) (x_i,y_i) (xi​,yi​) 和 a , b , c , d a,b,c,d a,b,c,d&#xff0c;求出最多有多少个点依次连接而成的折线上线段的斜率在 ( a b , c d ) (\frac{a}{b},\frac{c}{d}) (ba​,dc​…

linux怎么查询远程管理卡型号

在Linux中&#xff0c;要查询远程管理卡&#xff08;通常是服务器主板上的集成芯片&#xff0c;如iDRAC、iLO、BMC等&#xff09;的型号&#xff0c;可以使用一些特定厂商的工具&#xff0c;或者通过IPMI&#xff08;Intelligent Platform Management Interface&#xff09;来实…

【智能算法】波搜索算法(WSA)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献5.代码获取 1.背景 2024年&#xff0c;H Zhang受到雷达技术启发&#xff0c;提出了波搜索算法&#xff08;Wave Search Algorithm, WSA&#xff09;。 2.算法原理 2.1算法思想 WSA模拟雷达工作时的发射、反…