0. 引子

我们在调试的时候发现,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的基地址的呢?

1. 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.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);}
}

2. percpu宏的实现

有了上一节的基本原理了解后,理解相关的操作宏就比较容易了。percpu常用的有以下宏:

2.1 per_cpu()

这个宏获取某个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基地址。

2.2 percpu_read()

上面的获取指定某个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()的不同

3. x86_64 gs寄存器的初始化

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 中

3.1 cpu0 boot阶段

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,%ecxmovl   initial_gs(%rip),%eaxmovl   initial_gs+4(%rip),%edxwrmsr    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")

3.2 cpu0 运行阶段

在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_32loadsegment(fs, __KERNEL_PERCPU);
#elseloadsegment(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));
#endifload_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)); })

3.3 cpuN 运行阶段

除了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

x86 PerCPU变量基址(gs寄存器)的原理相关推荐

  1. linux内核percpu变量声明,Linux kernel percpu变量解析

    Linux 2.6 kernel 中的 percpu 变量是经常用到的东西,因为现在很多计算机都已经支持多处理器了,而且 kernel 默认都会被编译成 SMP 的,相对于原来多个处理器共享数据并进行 ...

  2. linux内核之Per-CPU变量

    前言 通过Per-cpu变量除了可以分配内存,还有一个最大的好处就是不需要考虑同步.最好的同步技术就是把不需要同步的内核放在首位,因为每种显示的同步原语都有不容忽视的开销. 本质:Per-cpu变量主 ...

  3. 详解JavaScript变量类型判断及domReady原理 写得很好

    原文:详解JavaScript变量类型判断及domReady原理 我们知道,在开发JavaScript时候,经常要判断JavaScript变量类型,此 JavaScript教程 详细介绍JS变量的判断 ...

  4. STM32F103系列芯片的地址和寄存器映射原理、LED轮流闪烁实现

    STM32F103系列芯片的地址和寄存器映射原理.LED轮流闪烁实现 文章目录 STM32F103系列芯片的地址和寄存器映射原理.LED轮流闪烁实现 1 51单片机和STM32的不同点 2 寄存器 2 ...

  5. 【i.MX6ULL】驱动开发3——GPIO寄存器配置原理

    前面的两篇Linux驱动文章,介绍了字符设备驱动的两种新旧开发方式,并使用一个虚拟的字符驱动来学习字符设备的开发的流程. 本篇起,就要来操作Linux开发板的硬件,首先当然是通过经典的点亮LED灯程序 ...

  6. 【嵌入式07】寄存器映射原理详解,GPIO端口的初始化设置步骤

    本文主要介绍STM32F103系列芯片的地址映射和寄存器映射原理,GPIO端口的初始化设置步骤. 一.STM32F103系列芯片的地址映射和寄存器映射原理 1.什么是寄存器? 2.地址映射和寄存器映射 ...

  7. 计算机组成原理寄存器的实验原理,计算机组成原理实验报告_寄存器的原理及操作课案.docx...

    <计算机组成原理实验报告_寄存器的原理及操作课案.docx>由会员分享,提供在线免费全文阅读可下载,此文档格式为docx,更多相关<计算机组成原理实验报告_寄存器的原理及操作课案.d ...

  8. 计算机组成原理实验写入怎么,计算机组成原理实验报告_寄存器的原理及操作...

    <计算机组成原理实验报告_寄存器的原理及操作>由会员分享,可在线阅读,更多相关<计算机组成原理实验报告_寄存器的原理及操作(10页珍藏版)>请在人人文库网上搜索. 1.成绩:实 ...

  9. 计算机组成原理r3寄存器,计算机组成原理实验报告-寄存器的原理及操作

    <计算机组成原理实验报告-寄存器的原理及操作>由会员分享,可在线阅读,更多相关<计算机组成原理实验报告-寄存器的原理及操作(10页珍藏版)>请在装配图网上搜索. 1.成绩:实 ...

最新文章

  1. 浙大吴飞教授:尽管AlphaGo Zero已强大到从经验中学习模型,我也绝不赞同马斯克和霍金的威胁论,人才是智能的最终主宰
  2. 基于JSP实现人力资源管理系统
  3. 扪心自问!一百多道难搞的面试题,你能答对了多少?
  4. 关于B.M.W的最原始的说明
  5. 子窗口_不同线程下主窗口与子窗口的信息交互(一)
  6. centos7设置mongodb远程连接(亲测)
  7. HDU 3652 B-number (数位DP)
  8. How to Use File Choosers
  9. 方法冲突_化解冲突,要避免用这 2 种方法
  10. linux下删除编译安装的软件,Linux 中卸载编译安装的软件
  11. 杰里之 2M 的 SDK 开蓝牙一拖二出现奇怪的问题【篇】
  12. Oracle表添加时间字段(执行insert、update时,时间字段自动插入当前系统时间)
  13. 企业数据防泄漏解决方案的介绍!
  14. 程序员的副业:我的第一本书出版啦!
  15. 卡塔尔称攻击卡塔尔通讯社黑客来自断交国
  16. 算法实践:数独的基本解法
  17. java joda range,Java:joda time
  18. 下载pyboard的flash中的驱动程序_驱动人生下载-驱动人生绿色最新下载正式版
  19. 小知识:随机生成26个字母中(一个或多个)的字母
  20. 假如,我来做一款产品?

热门文章

  1. TOJ3216 我要4444
  2. 「Upwork高手攻略」月入1-4万,一个低门槛的大机会,入门指南
  3. s12xep100 bootloader设计要点总结
  4. 实现扑克牌的洗牌功能
  5. 内网渗透神器CobaltStrike之DNS Beacon(四)
  6. 基于CloudSim 的云资源调度系统分析设计与实现——合肥工业大学云计算课程作业
  7. 专访比特安:区块链安全领域的“守门人”
  8. 想体验.NET7又不想安装体验版,Windows沙盒了解一下
  9. Pandas的MultiIndex多层索引使用
  10. Xcode7.3.1中通过最新的CocoaPod安装pop动画引擎