本文共 9290 字,大约阅读时间需要 30 分钟。
我们在调试的时候发现,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_union0000000000000000 D __per_cpu_start0000000000004000 V gdt_page0000000000005000 V exception_stacks000000000000a000 V espfix_stack000000000000a008 V espfix_waddr000000000000a010 V tlb_vector_offset000000000000a080 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.cstart_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_loadINIT_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_union0000000000000000 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.
2. 3. 4.转载地址:http://ruiun.baihongyu.com/