我们在调试的时候发现,x86下有一个快捷方法,只需一条简单的汇编指令mov %gs:var
就能取出某个percpu变量在当前cpu的值,非常高效。
unsigned long get_mem_value(unsigned long addr) {
unsigned long value = 0 ;
__asm__ __volatile__ ("mov %0, %%rax\n\t"::"r"(addr)) ;
__asm__ __volatile__ ("mov %gs:(%rax), %rax\n\t") ;
__asm__ __volatile__ ("mov %%rax, %[value]\n\t" :[value]"=r"(value)) ;
return value ;
}
unsigned long get_xxx_var(void) {
unsigned long addr = kallsyms_lookup_name("xxx_var") ;
if(! addr) {
dbg("Can't found xxx_var symbols! \r\n") ;
return 0 ;
}
return get_mem_value(addr) ;
}
这种操作的原理是什么样的呢?gs
寄存器是是什么时候被赋值为percpu的基地址的呢?
percpu在NUMA系统上的内存分配还是比较复杂的,这里就不详细解析了。我们这里只了解最基本percpu静态变量的原理。
静态的percpu变量使用DEFINE_PER_CPU()
宏来定义,目的就是把这种类型的变量都放到section(".data..percpu")
:
#define DEFINE_PER_CPU(type, name) \
DEFINE_PER_CPU_SECTION(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
#define PER_CPU_BASE_SECTION ".data..percpu"
链接脚本中关于section(".data..percpu")
的定义,__per_cpu_start
是section起始地址,__per_cpu_end
是section结束地址,__per_cpu_load
是变量地址和存储地址的offset值:
PERCPU_VADDR(INTERNODE_CACHE_BYTES, 0, :percpu)
#define PERCPU_VADDR(cacheline, vaddr, phdr) \
VMLINUX_SYMBOL(__per_cpu_load) = .; \
.data..percpu vaddr : AT(VMLINUX_SYMBOL(__per_cpu_load) \
- LOAD_OFFSET) { \
PERCPU_INPUT(cacheline) \
} phdr \
. = VMLINUX_SYMBOL(__per_cpu_load) + SIZEOF(.data..percpu);
#define PERCPU_INPUT(cacheline) \
VMLINUX_SYMBOL(__per_cpu_start) = .; \
*(.data..percpu..first) \
. = ALIGN(PAGE_SIZE); \
*(.data..percpu..page_aligned) \
. = ALIGN(cacheline); \
*(.data..percpu..readmostly) \
. = ALIGN(cacheline); \
*(.data..percpu) \
*(.data..percpu..shared_aligned) \
VMLINUX_SYMBOL(__per_cpu_end) = .;
需要注意的是section(".data..percpu")
会被链接到地址0,通过符号可以查看:
~> cat /proc/kallsyms
0000000000000000 V irq_stack_union
0000000000000000 D __per_cpu_start
0000000000004000 V gdt_page
0000000000005000 V exception_stacks
000000000000a000 V espfix_stack
000000000000a008 V espfix_waddr
000000000000a010 V tlb_vector_offset
000000000000a080 V old_rsp
...
在内核启动时,需要给每个cpu分配一块独立的percpu变量空间并且拷贝原始内容到独立空间中,每块空间都是section(".data..percpu")
的副本。原始的section(".data..percpu")
属于init
段,在内核启动完成后会被释放。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aZoOoH2v-1592912701960)(images/percpu/percpu_diagram.png)]
如上图,其实有两个地址的概念,一个是基地址base,一个是offset地址:原变量基地址 + offset[N] = 新percpu基地址base[N]
。因为原变量基地址是0,所以通常情况下offset[N] = base[N]
。
这项工作主要在setup_per_cpu_areas()函数中完成:
linux-3.0.101-63\arch\x86\kernel\setup_percpu.c
start_kernel() -> setup_per_cpu_areas():
#define per_cpu_offset(x) (__per_cpu_offset[x])
void __init setup_per_cpu_areas(void)
{
...
/* (1) 给每个cpu分配一个percpu空间,并拷贝数据内容 */
if (pcpu_chosen_fc != PCPU_FC_PAGE) {
rc = pcpu_embed_first_chunk(PERCPU_FIRST_CHUNK_RESERVE,
dyn_size, atom_size,
pcpu_cpu_distance,
pcpu_fc_alloc, pcpu_fc_free);
}
if (rc < 0)
rc = pcpu_page_first_chunk(PERCPU_FIRST_CHUNK_RESERVE,
pcpu_fc_alloc, pcpu_fc_free,
pcpup_populate_pte);
/* alrighty, percpu areas up and running */
delta = (unsigned long)pcpu_base_addr - (unsigned long)__per_cpu_start;
/* (2) 根据已经分配的空间给控制数据赋值 */
for_each_possible_cpu(cpu) {
/* (2.1) 计算percpu空间offset基地址数组__per_cpu_offset[cpu]的值 */
per_cpu_offset(cpu) = delta + pcpu_unit_offsets[cpu];
/* (2.2) 定义了一个percpu的变量"this_cpu_off",用percpu的方式来保存__per_cpu_offset[cpu]数组 */
per_cpu(this_cpu_off, cpu) = per_cpu_offset(cpu);
/* (2.3) 赋值smp_processor_id */
per_cpu(cpu_number, cpu) = cpu;
setup_percpu_segment(cpu);
setup_stack_canary_segment(cpu);
...
/*
* Up to this point, the boot CPU has been using .init.data
* area. Reload any changed state for the boot CPU.
*/
/* (2.4) 设置boot cpu (cpu 0)的gs寄存器为__per_cpu_offset[0] */
if (!cpu)
switch_to_new_gdt(cpu);
}
}
有了上一节的基本原理了解后,理解相关的操作宏就比较容易了。percpu常用的有以下宏:
这个宏获取某个cpu的percpu变量,原理也特别简单:变量地址(&var) + percpu变量的offset基地址(__per_cpu_offset[cpu])
#define per_cpu(var, cpu) \
(*SHIFT_PERCPU_PTR(&(var), per_cpu_offset(cpu)))
#define per_cpu_offset(x) (__per_cpu_offset[x])
#define SHIFT_PERCPU_PTR(__p, __offset) ({ \
__verify_pcpu_ptr((__p)); \
RELOC_HIDE((typeof(*(__p)) __kernel __force *)(__p), (__offset)); \
})
注意:这个宏计算的时候,使用的是变量地址(前面有&符号),对应加上percpu变量的offset基地址。
上面的获取指定某个cpu的percpu变量没有体现出x86的性能优化,现在我们看看获取当前cpu的percpu变量的宏percpu_read()的实现。
一般架构获取当前cpu的percpu变量的步骤:
1. 获取到当前cpu id, smp_processor_id()。
2. 计算得到当前cpu的percpu变量基地址__per_cpu_offset[cpu]。
3. 使用var地址 + __per_cpu_offset[cpu], 得到var在当前cpu的地址。
而x86架构的实现:
#define percpu_read(var) percpu_from_op("mov", var, "m" (var))
#define percpu_from_op(op, var, constraint) \
({ \
typeof(var) pfo_ret__; \
switch (sizeof(var)) { \
case 1: \
asm(op "b "__percpu_arg(1)",%0" \
: "=q" (pfo_ret__) \
: constraint); \
break; \
case 2: \
asm(op "w "__percpu_arg(1)",%0" \
: "=r" (pfo_ret__) \
: constraint); \
break; \
case 4: \
asm(op "l "__percpu_arg(1)",%0" \
: "=r" (pfo_ret__) \
: constraint); \
break; \
case 8: \
asm(op "q "__percpu_arg(1)",%0" \
: "=r" (pfo_ret__) \
: constraint); \
break; \
default: __bad_percpu_size(); \
} \
pfo_ret__; \
})
#define __percpu_arg(x) __percpu_prefix "%P" #x
#define __percpu_prefix "%%"__stringify(__percpu_seg)":"
#define __percpu_seg gs
展开这些宏,归为一句话:
asm("mov %%gs:%P1,%0" \
: "=r" (pfo_ret__) \
: "m" (var)); \
其中的关键就是当前cpu的gs
寄存器保存了__per_cpu_offset[cpu]
基地址。
更关键的是gs
寄存器被设置成了__per_cpu_offset[cpu]
基地址是在哪个节点干的呢??
注意:这类宏传入的是变量而不是变量地址,在asm指令时才会取地址,这是和per_cpu()的不同
x86使用WRMSR
指令来配置gs
寄存器。
x86_64位长模式下,FS和GS寄存器已经和GDT没有关系,其基址保存在MSR_FS_BASE和MSR_GS_BASE中。
MSR 是CPU 的一组64 位寄存器,可以分别通过RDMSR 和WRMSR 两条指令进行读和写的操作,前提要在ECX 中写入MSR 的地址:
指令 | 作用 | 描述 |
---|---|---|
RDMSR | 读模式定义寄存器。 | 对于RDMSR 指令,将会返回相应的MSR 中64bit 信息到(EDX:EAX)寄存器中 |
WRMSR | 写模式定义寄存器。 | 对于WRMSR 指令,把要写入的信息存入(EDX:EAX)中,执行写指令后,即可将相应的信息存入ECX 指定的MSR 中 |
linux-3.0.101-63\arch\x86\kernel\head_64.S:
/* Set up %gs.
*
* The base of %gs always points to the bottom of the irqstack
* union. If the stack protector canary is enabled, it is
* located at %gs:40. Note that, on SMP, the boot cpu uses
* init data section till per cpu areas are set up.
*/
movl $MSR_GS_BASE,%ecx
movl initial_gs(%rip),%eax
movl initial_gs+4(%rip),%edx
wrmsr
ENTRY(initial_gs)
.quad INIT_PER_CPU_VAR(irq_stack_union)
#define INIT_PER_CPU_VAR(var) init_per_cpu__##var
/*
* Per-cpu symbols which need to be offset from __per_cpu_load
* for the boot processor.
*/
#define INIT_PER_CPU(x) init_per_cpu__##x = x + __per_cpu_load
INIT_PER_CPU(gdt_page);
INIT_PER_CPU(irq_stack_union);
在boot阶段时,给cpu0的gs
寄存器配置了一个初始值__per_cpu_load
,这个是原始的section(".data..percpu")
。
在setup_per_cpu_areas()中分配完实际运行时的per_cpu内存空间后,cpu0的gs
寄存器需要重新配置:
void __init setup_per_cpu_areas(void)
{
...
for_each_possible_cpu(cpu) {
/*
* Up to this point, the boot CPU has been using .init.data
* area. Reload any changed state for the boot CPU.
*/
/* (2.4) 设置boot cpu (cpu 0)的gs寄存器为__per_cpu_offset[0] */
if (!cpu)
switch_to_new_gdt(cpu);
}
}
↓
switch_to_new_gdt()
↓
void load_percpu_segment(int cpu)
{
#ifdef CONFIG_X86_32
loadsegment(fs, __KERNEL_PERCPU);
#else
loadsegment(gs, 0);
/* (2.4.1) 将当前cpu的percpu(irq_stack_union.gs_base)的值配置进当前cpu的`gs`寄存器 */
wrmsrl(MSR_GS_BASE, (unsigned long)per_cpu(irq_stack_union.gs_base, cpu));
#endif
load_stack_canary_segment();
}
这里就来到了全文最关键、最难、最精彩的一个地方,per_cpu(irq_stack_union.gs_base, cpu)怎么就等于__per_cpu_offset[cpu]基地址的值了?这个是什么时候赋值的?
这里是使用一个隐含技巧来实现的:
DEFINE_PER_CPU_FIRST(union irq_stack_union,
irq_stack_union) __aligned(PAGE_SIZE);
union irq_stack_union {
char irq_stack[IRQ_STACK_SIZE];
/*
* GCC hardcodes the stack canary as %gs:40. Since the
* irq_stack is the object at %gs:0, we reserve the bottom
* 48 bytes of the irq stack for the canary.
*/
struct {
char gs_base[40];
unsigned long stack_canary;
};
};
我们可以看到irq_stack_union是使用DEFINE_PER_CPU_FIRST()
宏来进行定义的,这个宏定义的变量会放在section(".data..percpu..first")
,在section(".data..percpu")
的最前面。并且使用DEFINE_PER_CPU_FIRST()
宏来定义的变量只有一个,就是irq_stack_union。
而且irq_stack_union.gs_base[]是一个数组,所以我们获取到的是它的地址
,而不是它保存的数值
。
> cat /proc/kallsyms | grep irq_stack_union
0000000000000000 V irq_stack_union
所以,per_cpu(irq_stack_union.gs_base, cpu)展开来就是:
0 + __per_cpu_offset[cpu]
setup_per_cpu_areas()函数中,在__per_cpu_offset[cpu]被赋值以后,per_cpu(irq_stack_union.gs_base, cpu)就等价于__per_cpu_offset[cpu]了。
per_cpu(irq_stack_union.gs_base, cpu)宏的展开:
per_cpu(irq_stack_union.gs_base, cpu)
↓
#define per_cpu(var, cpu) \
(*SHIFT_PERCPU_PTR(&(var), per_cpu_offset(cpu)))
↓
#define SHIFT_PERCPU_PTR(__p, __offset) ({ \
__verify_pcpu_ptr((__p)); \
RELOC_HIDE((typeof(*(__p)) __kernel __force *)(__p), (__offset)); \
})
↓
#define RELOC_HIDE(ptr, off) \
({ unsigned long __ptr; \
__asm__ ("" : "=r"(__ptr) : "0"(ptr)); \
(typeof(ptr)) (__ptr + (off)); })
除了cpu0,其他cpu在boot阶段也需要配置gs
寄存器:
linux-3.0.101-63\arch\x86\kernel\smp.c:
smp_ops -> native_cpu_up() -> do_boot_cpu() -> start_secondary() -> cpu_init() -> switch_to_new_gdt() -> load_percpu_segment()
原理和cpu0一致。
1.内核基础设施——per cpu变量
2.同步与互斥_percpu变量
3.Per-cpu -1- (Basic)
4.x86 SWAPGS