当工作中需要使用percpu变量,才知道自己对percpu变量的理解有多薄弱,所以趁现在把经验心得记录下来。
percpu变量基础(wowotech):http://www.wowotech.net/kernel_synchronization/per-cpu.html
一般来说,对于per-cpu的变量,我们一般需要通过关抢占进行并发保护操作;这其中就有几种方式:
- 通过关中断local_irq_save来达到关抢占的效果(这样还能支持嵌套)
- 通过this_cpu_***这类函数操作,见下图说明
- 在hotplug函数中直接无需加锁
- 通过关抢占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(¤t->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;
}
可以看到,通过检查的条件有:
- 抢占计数不为0
- 中断disable了
- 对应进程只允许在一个核上运行
- 系统很早期的时候
其中,条件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指令把值从内存里读了出来,我们需要记住:
- 在大部分体系结构上,读取一个字本身就是原子操作,即在对一个字进行写入操作期间不可能完成对该字的读取,因而该读操作是原子的。
- 一个字长的 读取 总是原子地发生,绝不可能对同一个字交错地进行写。
- 读总是返回一个完整的字,或者在写操作之前,或者之后,绝不可能发生在写的过程中。
但是,如果当前架构不属于上面说的大部分体系结构呢?其实也是没问题的,这里读变量的值只是为了显示等作用,也就是说,并不需要多准确,这里的读可能会出现“滞后”的问题。
按照我的理解,真正的并发操作,还是需要锁保护才行的。
下面写几个例子验证一下,测试环境是x86-64。
嗯,综合上面的分析,估计读变量小于等于本机处理器字长的指令都是原子的。那如果读超过本机处理器字长的变量呢?
显然,对于这种大小为64bit以上的变量,需要多次读操作,例子中读取6*64bit的变量用了4次读操作;试想,如果刚好另一个线程与当前线程存在并发,另一个线程在同步修改该变量,而且修改的动作恰好在这4次读操作中间,那么可能出现这种情况----变量tmp2的var1、var2成员是旧值(写操作前的值),var3、var4成员是新值(写操作后的值),这样就出现了并发问题。因而,如果在64位机器上,在cpu0上对cpu1的per-cpu变量进行读操作,若变量大小大于64bit,且存在异步写该变量的情况,那就不是原子的,是需要锁保护的。
接下来看看写操作。
可见,对内存的写操作,需要先将值读出来,进行加5处理后,再写回内存中,涉及多条指令,显然也不是原子的。
done