percpu变量的并发操作

当工作中需要使用percpu变量,才知道自己对percpu变量的理解有多薄弱,所以趁现在把经验心得记录下来。

percpu变量基础(wowotech):http://www.wowotech.net/kernel_synchronization/per-cpu.html
一般来说,对于per-cpu的变量,我们一般需要通过关抢占进行并发保护操作;这其中就有几种方式:

  1. 通过关中断local_irq_save来达到关抢占的效果(这样还能支持嵌套)
  2. 通过this_cpu_***这类函数操作,见下图说明
  3. 在hotplug函数中直接无需加锁
  4. 通过关抢占preempt_disable();在这里插入图片描述
    接下来,我们来看看__this_cpu_**开头的函数,这类函数会做抢占检查
#define __this_cpu_add(pcp, val)					\
({									\
	__this_cpu_preempt_check("add");				\
	raw_cpu_add(pcp, val);						\
})

notrace void __this_cpu_preempt_check(const char *op)
{
	check_preemption_disabled("__this_cpu_", op);
}
EXPORT_SYMBOL(__this_cpu_preempt_check);

notrace static unsigned int check_preemption_disabled(const char *what1,
							const char *what2)
{
	int this_cpu = raw_smp_processor_id();

	if (likely(preempt_count()))
		goto out;

	if (irqs_disabled())
		goto out;

	/*
	 * Kernel threads bound to a single CPU can safely use
	 * smp_processor_id():
	 */
	if (cpumask_equal(&current->cpus_allowed, cpumask_of(this_cpu)))
		goto out;

	/*
	 * It is valid to assume CPU-locality during early bootup:
	 */
	if (system_state < SYSTEM_SCHEDULING)
		goto out;

	/*
	 * Avoid recursion:
	 */
	preempt_disable_notrace();

	if (!printk_ratelimit())
		goto out_enable;

	printk(KERN_ERR "BUG: using %s%s() in preemptible [%08x] code: %s/%d\n",
		what1, what2, preempt_count() - 1, current->comm, current->pid);

	printk("caller is %pS\n", __builtin_return_address(0));
	dump_stack();

out_enable:
	preempt_enable_no_resched_notrace();
out:
	return this_cpu;
}

可以看到,通过检查的条件有:

  1. 抢占计数不为0
  2. 中断disable了
  3. 对应进程只允许在一个核上运行
  4. 系统很早期的时候
    其中,条件3属于那类被限制只能在一个cpu上跑的进程,比如每个cpu上的idle(或者叫做swapper)进程,亦或者那些normal worker_pool 中的 kworker线程。

上面通过关抢占来保护percpu变量的说法,有一个前提条件,就是本cpu只访问本cpu的percpu变量;那么,如果cpu0要去访问不属于cpu 0的percpu变量呢?下面拿函数refresh_cpu_vm_stats作为例子

static int refresh_cpu_vm_stats(bool do_pagesets)
{
			***
			struct per_cpu_pageset __percpu *p = zone->pageset;
			v = this_cpu_xchg(p->vm_stat_diff[i], 0);
			***
				__this_cpu_write(p->expire, 3);
			***
			if (!__this_cpu_read(p->expire) ||
			       !__this_cpu_read(p->pcp.count))
				continue;
			***
}

refresh_cpu_vm_stats是个work函数,也就是符合上面条件3,而且经过分析,该函数并没有显示的关抢占
先分析一下这个函数里的percpu变量p的使用。下面分析基于x86架构进行分析。
对于第一个操作,this_cpu_xchg,this_cpu_前缀的,上面分析过,是安全的。
对于第二个操作,__this_cpu_write,这感觉不是安全的,但是,p->expire只有在这个函数里使用,所以没问题
第三个操作和第四个操作一样,__this_cpu_read,在x86上本质就是一条mov指令,引用这里的一句话,“在许多体系结构上,读写本机处理器字长的正确对齐值是原子的”,count是一个int类型的变量,如果内核都直接这样写的话,那应该在目前linux内核支持的架构上,读变量小于等于本机处理器字长的指令都是原子的,那么第三个操作也是没有问题的。

接上面的问题,如果cpu0要去访问不属于cpu 0的percpu变量呢,比如refresh_cpu_vm_stats函数这样写,会有问题吗?下面直接说答案,再讲原理。
当然,首先,得先用per_cpu_ptr获取目标cpu上的变量指针,然后,第一个操作换成xchg,第二个操作直接用等号“=”赋值,第三个操作直接用"->"取值即可,即

static int refresh_cpu_vm_stats(bool do_pagesets)
{
			***
			struct per_cpu_pageset  *p = per_cpu_ptr(zone->pageset, cpu);
			v = xchg(p->vm_stat_diff[i], 0);
			***
				p->expire=3;
			***
			if (!p->expire ||
			       !p->pcp.count)
				continue;
			***
}

为什么这样就可以了呢?
第二个操作,expire只有在这个函数里使用,所以没问题
第一个操作,我们来看看x86的代码。
在这里插入图片描述
根据代码注释,在x86上是没问题的(图中是x86的xchg实现),因为使用的汇编指令是会lock住内存,保证不会有并发问题的。
第三个操作,为什么可以直接取值呢?我们来看看内核里的另外一个函数

static inline unsigned long zone_page_state_snapshot(struct zone *zone,
					enum zone_stat_item item)
{
	long x = atomic_long_read(&zone->vm_stat[item]);

#ifdef CONFIG_SMP
	int cpu;
	for_each_online_cpu(cpu)
		x += per_cpu_ptr(zone->pageset, cpu)->vm_stat_diff[item];

	if (x < 0)
		x = 0;
#endif
	return x;
}

这个函数直接读取了所有cpu上的per_cpu_ptr(zone->pageset, cpu)->vm_stat_diff[item],通过反汇编代码,确实这样写就是一条mov指令把值从内存里读了出来,我们需要记住:

  1. 在大部分体系结构上,读取一个字本身就是原子操作,即在对一个字进行写入操作期间不可能完成对该字的读取,因而该读操作是原子的。
  2. 一个字长的 读取 总是原子地发生,绝不可能对同一个字交错地进行写。
  3. 读总是返回一个完整的字,或者在写操作之前,或者之后,绝不可能发生在写的过程中。

但是,如果当前架构不属于上面说的大部分体系结构呢?其实也是没问题的,这里读变量的值只是为了显示等作用,也就是说,并不需要多准确,这里的读可能会出现“滞后”的问题。
按照我的理解,真正的并发操作,还是需要锁保护才行的。

下面写几个例子验证一下,测试环境是x86-64。

在这里插入图片描述
在这里插入图片描述
嗯,综合上面的分析,估计读变量小于等于本机处理器字长的指令都是原子的。那如果读超过本机处理器字长的变量呢?
在这里插入图片描述
在这里插入图片描述
显然,对于这种大小为64bit以上的变量,需要多次读操作,例子中读取6*64bit的变量用了4次读操作;试想,如果刚好另一个线程与当前线程存在并发,另一个线程在同步修改该变量,而且修改的动作恰好在这4次读操作中间,那么可能出现这种情况----变量tmp2的var1、var2成员是旧值(写操作前的值),var3、var4成员是新值(写操作后的值),这样就出现了并发问题。因而,如果在64位机器上,在cpu0上对cpu1的per-cpu变量进行读操作,若变量大小大于64bit,且存在异步写该变量的情况,那就不是原子的,是需要锁保护的。
接下来看看写操作。
在这里插入图片描述
在这里插入图片描述
可见,对内存的写操作,需要先将值读出来,进行加5处理后,再写回内存中,涉及多条指令,显然也不是原子的。

done

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值