Percpu variable in Linux
凡是对现代计算机系统有一定了解的人都知道,在不同的物理核之间共享数据的开销是挺大的。一般来说,从register中读取一个字长数据的开销是1个cycle,在L1 cache中读取的开销是3个cycle,而如果这个数据在另外一个核甚至是另外一个node的cache上,这个开销最高可能达到100个cycle。因此,设计高性能程序不到万不得已时不应该在不同核之间共享数据。造成这个现象的原因是现代的cpu需要利用MESI协议来保证不同核之间cache的一致性(不然程序员写程序时还不得时常在想我此时读到的数据对不对?是不是最新的值?),要发消息到target core再等消息回来(会引发流水线stall?),不慢才怪。
以上是采用Percpu variable的一个原因,为了保证每个core对共享数据的互斥访问,还必须得加锁,这又带来了额外的性能开销。
而Percpu variable的诞生则很好地解决了以上的问题,避免了cache line的乒乓问题,也没有了锁之间的contention。下面就来讲一讲linux中percpu variable的实现。
在percpu-defs.h中有如下宏定义:
/*
* Variant on the per-CPU variable declaration/definition theme used for
* ordinary per-CPU variables.
*/
#define DECLARE_PER_CPU(type, name) \
DECLARE_PER_CPU_SECTION(type, name, "")
#define DEFINE_PER_CPU(type, name) \
DEFINE_PER_CPU_SECTION(type, name, "")
于是可以用DECLARE_PER_CPU来申明一个Percpu variable,用DEFINE_PER_CPU来定义一个Percpu variable。
继续往下展开,DEFINE_PER_CPU(type, name)可以变为:
#define DEFINE_PER_CPU_SECTION(type, name, sec) \
__PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES \
__typeof__(type) name
#define __PCPU_ATTRS(sec) \
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
PER_CPU_ATTRIBUTES
#ifndef PER_CPU_BASE_SECTION
#ifdef CONFIG_SMP
#define PER_CPU_BASE_SECTION ".data..percpu"
#else
#define PER_CPU_BASE_SECTION ".data"
#endif
#endif
可以看到其实就是利用gcc的__attribute__关键字将这个变量放到了一个叫.data..percpu的特殊section中去了,在编译好的vmlinuz文件中就可以发现这个section。
21:33:22 mcore-tub2:/usr/src/linux-3.13.3 # objdump -h ./vmlinux|grep percpu
15 .data..percpu 00013b00 0000000000000000 000000000169c000 00a00000 2**12
在linux中,除了要用以上所述特殊方式定义一个Percpu variable,访问一个Percpu variable也要遵循特定的规则,要使用per_cpu宏,这个宏有两个参数,第一个是变量名,第二个是cpu的id。不奇怪,这两个参数唯一确定了一个Percpu variable,避免了二义性。
extern unsigned long __per_cpu_offset[NR_CPUS];
#define per_cpu_offset(x) (__per_cpu_offset[x])
#define RELOC_HIDE(ptr, off) \
({ unsigned long __ptr; \
__asm__ ("" : "=r"(__ptr) : "0"(ptr)); \
(typeof(ptr)) (__ptr + (off)); })
/* Weird cast keeps both GCC and sparse happy. */
#define SHIFT_PERCPU_PTR(__p, __offset) ({ \
__verify_pcpu_ptr((__p)); \
RELOC_HIDE((typeof(*(__p)) __kernel __force *)(__p), (__offset)); \
})
/*
* A percpu variable may point to a discarded regions. The following are
* established ways to produce a usable pointer from the percpu variable
* offset.
*/
#define per_cpu(var, cpu) \
(*SHIFT_PERCPU_PTR(&(var), per_cpu_offset(cpu)))
简单来说就是把var的地址加上了一个percpu的偏移,接下来的问题就明朗了,就是这个__per_cpu_offset是怎么来的。
void __init setup_per_cpu_areas(void) {
......
for_each_possible_cpu(cpu) {
per_cpu_offset(cpu) = delta + pcpu_unit_offsets[cpu];
per_cpu(this_cpu_off, cpu) = per_cpu_offset(cpu);
per_cpu(cpu_number, cpu) = cpu;
setup_percpu_segment(cpu);
setup_stack_canary_segment(cpu);
......
}
}
以上代码可以在arch/x86/kernel/setup_percpu.c中找到,含义不言自明。
由于C语言的语法结构简单,所以不能够做到透明利用Percpu variable,必须要借助宏来做到。后文将介绍sv6中Percpu variable的实现,绝对让你想不到。
blog comments powered by Disqus