x86 PerCPU变量基址(gs寄存器)的原理
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寄存器)的原理相关推荐
- linux内核percpu变量声明,Linux kernel percpu变量解析
Linux 2.6 kernel 中的 percpu 变量是经常用到的东西,因为现在很多计算机都已经支持多处理器了,而且 kernel 默认都会被编译成 SMP 的,相对于原来多个处理器共享数据并进行 ...
- linux内核之Per-CPU变量
前言 通过Per-cpu变量除了可以分配内存,还有一个最大的好处就是不需要考虑同步.最好的同步技术就是把不需要同步的内核放在首位,因为每种显示的同步原语都有不容忽视的开销. 本质:Per-cpu变量主 ...
- 详解JavaScript变量类型判断及domReady原理 写得很好
原文:详解JavaScript变量类型判断及domReady原理 我们知道,在开发JavaScript时候,经常要判断JavaScript变量类型,此 JavaScript教程 详细介绍JS变量的判断 ...
- STM32F103系列芯片的地址和寄存器映射原理、LED轮流闪烁实现
STM32F103系列芯片的地址和寄存器映射原理.LED轮流闪烁实现 文章目录 STM32F103系列芯片的地址和寄存器映射原理.LED轮流闪烁实现 1 51单片机和STM32的不同点 2 寄存器 2 ...
- 【i.MX6ULL】驱动开发3——GPIO寄存器配置原理
前面的两篇Linux驱动文章,介绍了字符设备驱动的两种新旧开发方式,并使用一个虚拟的字符驱动来学习字符设备的开发的流程. 本篇起,就要来操作Linux开发板的硬件,首先当然是通过经典的点亮LED灯程序 ...
- 【嵌入式07】寄存器映射原理详解,GPIO端口的初始化设置步骤
本文主要介绍STM32F103系列芯片的地址映射和寄存器映射原理,GPIO端口的初始化设置步骤. 一.STM32F103系列芯片的地址映射和寄存器映射原理 1.什么是寄存器? 2.地址映射和寄存器映射 ...
- 计算机组成原理寄存器的实验原理,计算机组成原理实验报告_寄存器的原理及操作课案.docx...
<计算机组成原理实验报告_寄存器的原理及操作课案.docx>由会员分享,提供在线免费全文阅读可下载,此文档格式为docx,更多相关<计算机组成原理实验报告_寄存器的原理及操作课案.d ...
- 计算机组成原理实验写入怎么,计算机组成原理实验报告_寄存器的原理及操作...
<计算机组成原理实验报告_寄存器的原理及操作>由会员分享,可在线阅读,更多相关<计算机组成原理实验报告_寄存器的原理及操作(10页珍藏版)>请在人人文库网上搜索. 1.成绩:实 ...
- 计算机组成原理r3寄存器,计算机组成原理实验报告-寄存器的原理及操作
<计算机组成原理实验报告-寄存器的原理及操作>由会员分享,可在线阅读,更多相关<计算机组成原理实验报告-寄存器的原理及操作(10页珍藏版)>请在装配图网上搜索. 1.成绩:实 ...
最新文章
- 浙大吴飞教授:尽管AlphaGo Zero已强大到从经验中学习模型,我也绝不赞同马斯克和霍金的威胁论,人才是智能的最终主宰
- 基于JSP实现人力资源管理系统
- 扪心自问!一百多道难搞的面试题,你能答对了多少?
- 关于B.M.W的最原始的说明
- 子窗口_不同线程下主窗口与子窗口的信息交互(一)
- centos7设置mongodb远程连接(亲测)
- HDU 3652 B-number (数位DP)
- How to Use File Choosers
- 方法冲突_化解冲突,要避免用这 2 种方法
- linux下删除编译安装的软件,Linux 中卸载编译安装的软件
- 杰里之 2M 的 SDK 开蓝牙一拖二出现奇怪的问题【篇】
- Oracle表添加时间字段(执行insert、update时,时间字段自动插入当前系统时间)
- 企业数据防泄漏解决方案的介绍!
- 程序员的副业:我的第一本书出版啦!
- 卡塔尔称攻击卡塔尔通讯社黑客来自断交国
- 算法实践:数独的基本解法
- java joda range,Java:joda time
- 下载pyboard的flash中的驱动程序_驱动人生下载-驱动人生绿色最新下载正式版
- 小知识:随机生成26个字母中(一个或多个)的字母
- 假如,我来做一款产品?
热门文章
- TOJ3216 我要4444
- 「Upwork高手攻略」月入1-4万,一个低门槛的大机会,入门指南
- s12xep100 bootloader设计要点总结
- 实现扑克牌的洗牌功能
- 内网渗透神器CobaltStrike之DNS Beacon(四)
- 基于CloudSim 的云资源调度系统分析设计与实现——合肥工业大学云计算课程作业
- 专访比特安:区块链安全领域的“守门人”
- 想体验.NET7又不想安装体验版,Windows沙盒了解一下
- Pandas的MultiIndex多层索引使用
- Xcode7.3.1中通过最新的CocoaPod安装pop动画引擎