Linux内核中的READ_ONCE和WRITE_ONCE宏

在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针对的是读写操作本身,只会影响使用这两个宏的内存访问操作,不能阻止对其它变量的优化操作。

<think>好的,我现在要分析用户提供的代码`static void avic_vcpu_load(struct kvm_vcpu *vcpu, int cpu)`。首先,我需要理解这段代码的功能上下文。看起来这是Linux内核中与KVM虚拟化相关的代码,特别是处理AMD的AVIC(虚拟中断控制器)的部分。 首先,函数名是`avic_vcpu_load`,通常在KVM中,当VCPU(虚拟CPU)被调度到某个物理CPU上运行时,会调用这样的函数来加载相关状态。所以这个函数可能负责在VCPU加载到物理CPU时,配置AVIC相关的硬件设置。 代码开头声明了几个变量,其中`h_physical_id`通过`kvm_cpu_get_apicid(cpu)`获取物理CPU的APIC ID。APIC ID用于在系统中唯一标识每个CPU。接着将传入的`vcpu`转换为`svm`结构体,这里可能是指SVM(Secure Virtual Machine),AMD的虚拟化技术。 接下来有一个条件判断,如果`kvm_vcpu_apicv_active(vcpu)`为假,则直接返回。`apicv_active`可能表示该VCPU的APIC虚拟化是否启用,如果未启用,自然不需要配置AVIC,所以直接返回。 然后有一个WARN_ON检查,确保`h_physical_id`不超过255,因为注释提到主机物理APIC ID是8位,所以最大值是255。如果超过,会触发警告并返回,避免无效的ID导致问题。 接下来的部分操作`entry`变量,它从`svm->avic_physical_id_cache`读取。这里使用`READ_ONCE`可能是为了防止编译器优化导致读取顺序问题,确保原子性。然后检查`entry`是否已经设置了`IS_RUNNING`标志,如果有,则发出警告,因为此时不应该处于运行状态。 然后,代码清除`entry`中的`HOST_PHYSICAL_ID`部分,并将新的`h_physical_id`设置进去。接着处理`IS_RUNNING`标志位,根据`svm->avic_is_running`的状态来设置该位,表示VCPU是否正在运行。 最后,用`WRITE_ONCE`将更新后的`entry`写回到缓存中,确保写入操作的原子性。然后调用`avic_update_iommu_vcpu_affinity`,这个函数可能负责更新IOMMU的映射,确保中断能正确路由到当前运行的物理CPU。 现在需要验证这些步骤是否正确。例如,检查APIC ID是否合理,确保位操作正确,特别是掩码的使用是否正确。另外,`avic_physical_id_cache`的作用是否与硬件寄存器相关,是否需要特定的内存屏障或同步机制? 考虑到AVIC的作用是加速中断处理,正确的物理ID运行状态设置对中断路由至关重要。因此,这段代码在VCPU调度时正确配置了物理CPU的APIC ID运行状态,确保中断能有效传递到正确的物理CPU上处理。</think>这是一个与虚拟化技术中 **AMD AVIC(虚拟高级可编程中断控制器)** 相关的内核函数,用于在 **KVM 虚拟化环境**中处理虚拟 CPU(vCPU)与物理 CPU 的绑定关系。以下是代码的逐步解析: --- ### 函数作用 在 vCPU 被调度到某个物理 CPU 上运行时,更新 AVIC 硬件中与该 vCPU 关联的物理 CPU ID 运行状态,确保中断正确路由。 --- ### 关键步骤解析 1. **参数与变量初始化** - `struct kvm_vcpu *vcpu`:操作的虚拟 CPU。 - `int cpu`:目标物理 CPU 的逻辑编号。 - `h_physical_id`:通过 `kvm_cpu_get_apicid(cpu)` 获取物理 CPU 的 **APIC ID**(硬件唯一标识)。 - `svm = to_svm(vcpu)`:将通用 `kvm_vcpu` 转换为 AMD SVM 结构体(用于 AVIC 相关操作)。 2. **检查 APIC 虚拟化是否激活** ```c if (!kvm_vcpu_apicv_active(vcpu)) return; ``` - 若该 vCPU 的 APIC 虚拟化未激活,直接返回(无需配置 AVIC)。 3. **验证物理 APIC ID 有效性** ```c if (WARN_ON(h_physical_id > AVIC_PHYSICAL_ID_ENTRY_HOST_PHYSICAL_ID_MASK)) return; ``` - APIC ID 是 8 位值(范围 0~255),若超限触发内核警告并退出。 4. **读取当前 AVIC 物理 ID 缓存条目** ```c entry = READ_ONCE(*(svm->avic_physical_id_cache)); WARN_ON(entry & AVIC_PHYSICAL_ID_ENTRY_IS_RUNNING_MASK); ``` - `READ_ONCE`:确保原子读取,防止编译器/CPU 乱序优化。 - 检查当前条目是否已标记为“运行中”(理论上不应发生,故触发警告)。 5. **更新物理 APIC ID 字段** ```c entry &= ~AVIC_PHYSICAL_ID_ENTRY_HOST_PHYSICAL_ID_MASK; entry |= (h_physical_id & AVIC_PHYSICAL_ID_ENTRY_HOST_PHYSICAL_ID_MASK); ``` - 清除旧物理 ID,写入新 ID。 6. **更新运行状态标志位** ```c entry &= ~AVIC_PHYSICAL_ID_ENTRY_IS_RUNNING_MASK; if (svm->avic_is_running) entry |= AVIC_PHYSICAL_ID_ENTRY_IS_RUNNING_MASK; ``` - 根据 `avic_is_running` 状态设置 `IS_RUNNING` 标志,表示 vCPU 是否正在运行。 7. **写回缓存并同步 IOMMU** ```c WRITE_ONCE(*(svm->avic_physical_id_cache), entry); avic_update_iommu_vcpu_affinity(vcpu, h_physical_id, svm->avic_is_running); ``` - `WRITE_ONCE`:原子写入更新后的条目。 - `avic_update_iommu_vcpu_affinity`:更新 IOMMU 中断亲性,确保中断路由到正确的物理 CPU。 --- ### 技术背景 - **AVIC**:AMD 的硬件虚拟化技术,用于加速中断传递,减少 VM-Exit。 - **APIC ID**:x86 CPU 的唯一标识符,用于中断路由。 - **IOMMU 关联性**:确保设备发起的中断能正确映射到正在运行 vCPU 的物理 CPU。 --- ### 总结 此函数在 **vCPU 调度到物理 CPU 时**,通过更新 AVIC 硬件中的物理 ID 状态标志,保证: 1. 中断能正确路由到当前运行 vCPU 的物理 CPU。 2. IOMMU 中断重映射表同步更新,避免中断丢失或误投递。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值