smp_setup_processor_id是kernel的第2个函数,其主要作用是:
读取CPU寄存器中唯一标识符MPIDR_EL1
建立逻辑与物理CPU的映射关系
初始化Per-CPU偏移量
输出启动信息
这个函数的具体实例化在/arch/arm64/kernel/setup.h:
void __init smp_setup_processor_id(void)
{
u64 mpidr = read_cpuid_mpidr() & MPIDR_HWID_BITMASK;
set_cpu_logical_map(0, mpidr);
/*
* clear __my_cpu_offset on boot CPU to avoid hang caused by
* using percpu variable early, for example, lockdep will
* access percpu variable inside lock_release
*/
set_my_cpu_offset(0);
pr_info("Booting Linux on physical CPU 0x%010lx [0x%08x]\n",
(unsigned long)mpidr, read_cpuid_id());
}首先是第一个部分,read_cpuid_mpidr,这个函数的本质是进行一次MPIDR_EL1寄存器的read_cpuid。
// 位于/arch/arm64/include/asm/cputype.h
#define __mrs_s(v, r) \
DEFINE_MRS_S \
" mrs_s " v ", " __stringify(r) "\n" \
UNDEFINE_MRS_S
#define read_sysreg_s(r) ({ \
u64 __val; \
asm volatile(__mrs_s("%0", r) : "=r" (__val)); \
__val; \
})
#define read_cpuid(reg) read_sysreg_s(SYS_ ## reg)
static inline u64 __attribute_const__ read_cpuid_mpidr(void)
{
return read_cpuid(MPIDR_EL1);
}这里进行读取的函数是由asm volatile这个函数配合对应的寄存器宏展开,手搓出来的一个arm64架构下的MRS (Move Register from System) 指令。
首先,GCC在进行宏展开的时候,({ ... })块会将最后一个变量作为函数的返回值。
volatile函数是属于asm的一种GCC内联汇编的功能,它直接由GCC内联展开。不如由下面的这个定义,就可以引用:
#define __asm__ asm
#define __volatile__ volatile
这个volatile的函数结构为:
asm volatile (
"汇编指令模板"
: 输出操作数列表 (Output Operands)
: 输入操作数列表 (Input Operands)
: 破坏描述列表 (Clobber List)
);这里,有关具体的指令的展开为:
#define __DEFINE_MRS_MSR_S_REGNUM \
" .irp num,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30\n" \
" .equ .L__reg_num_x\\num, \\num\n" \
" .endr\n" \
" .equ .L__reg_num_xzr, 31\n"
#define DEFINE_MRS_S \
__DEFINE_MRS_MSR_S_REGNUM \
" .macro mrs_s, rt, sreg\n" \
__emit_inst(0xd5200000|(\\sreg)|(.L__reg_num_\\rt)) \
" .endm\n"
#define UNDEFINE_MRS_S \
" .purgem mrs_s\n"
#define __mrs_s(v, r) \
DEFINE_MRS_S \
" mrs_s " v ", " __stringify(r) "\n" \
UNDEFINE_MRS_S
// 这里拼出MPIDR_EL1寄存器
#define sys_reg(op0, op1, crn, crm, op2) \
(((op0) << Op0_shift) | ((op1) << Op1_shift) | \
((crn) << CRn_shift) | ((crm) << CRm_shift) | \
((op2) << Op2_shift))
#define SYS_MPIDR_EL1 sys_reg(3, 0, 0, 0, 5)
#define read_cpuid(reg) read_sysreg_s(SYS_ ## reg)
// 这里最后返回指令执行的读取值
#define read_sysreg_s(r) ({ \
u64 __val; \
asm volatile(__mrs_s("%0", r) : "=r" (__val)); \
__val; \
})这里相当于由read_sysreg_s输入寄存器MPIDR_EL1,拼出操作码MRS所需的指令:
3 << 19=0x60000(二进制:110 0000 0000 0000 0000)0 << 16=0x00 << 12=0x00 << 8=0x05 << 5=160=0xA0(二进制:101 00000)
最终计算出SYS_MPIDR_EL1为:(可以直接对比ARM64指令集找对应指令格式)
SYS_MPIDR_EL1 = 0x60000 | 0xA0 = 0x600A0
同时,还需要拼接一个通用寄存器、一个内存中的u64地址用于保存寄存器的返回值。
mrs本身是一个宏函数:
mrs_s %0, SYS_MPIDR_EL1
它在文件中被宏展开定义为:
.macro mrs_s, rt, sreg
__emit_inst(0xd5200000 | (\sreg) | (.L__reg_num_\rt))
.endm
其中,sreg为:0x600A0(就是SYS_MPIDR_EL1)
其中,L__reg_num_为:%0所找到的某个具体的可用寄存器位置
GCC 使用 %0, %1, %2... 等数字作为占位符,分别对应内联汇编语句中“输出操作数”和“输入操作数”列表中定义的 C 语言变量。
可以看到,在这个指令中,MPIDR_EL1的指令层面地址是确定的,这是属于ARM的一种硬编码地址。同时,MPIDR_EL1寄存器本身又具备一定的格式,可以用其中的Aff0~Aff3寄存器来推断当前物理拓扑的层级信息(核心、集群、插槽)。
ARM硬编码的MPIDR_EL1
旧事重提,这里先贴一个ARM手册链接:A-Profile Architecture
在文档中,有关这个寄存器的描述如下:D24.2.137章节

这玩意儿其实是从32位的版本演进而来的,主要内容包括Aff参数与MT参数,具体如下:
当MT参数为1时,代表启用逻辑线程(超线程);为0时代表启用物理线程
AFF0,最内层亲和性ID,最小的核心或超线程的ID
AFF1,族内核心ID,一般用于同一个Cluster内的核心分组
AFF2,片内核心ID,一般用于区分不同的Cluster,即CPU簇
AFF3,Die/Socket ID,一般用于区分多Die的ID
有关MRS指令,手册里的描述原文如下:C6.2.286章节

这个指令的具体含义是,将一个系统寄存器搬移到一个通用寄存器中去。
这里规定了MRS的格式为:
0b110101010011 <oD> <op1> <CRn> <CRm> <op2> <Rt>其中Xt就是通用寄存器,Systemreg是一个由<op0>_<op1>_<Cn>_<Cm>_<op2>编码的系统寄存器地址,可以在D23章节“AArch64 System Register Emcoding”中找到所有的系统寄存器编码方式。
OP0:为0b10时代表这是系统寄存器地址空间
OP1:用于区分同一组Cn/Cm下的不同寄存器或不同异常级别
Cn:协处理器n,一般标识作为系统寄存器编码的一部分,通常表示寄存器的大类或组
Cm:协处理器m,配合Cn做进一步分细分
OP2:用于在同一组Cn/Cm下做寄存器区分用

这里还提供了一个系统寄存器的大表,用来查询可以基于硬编码+MRS指令访问的各类寄存器:

MRS命令本身的操作码开头是固定的0b110101010011,这意味着刚好为0xd5开头的
同时,有关MPIDR_EL1这个寄存器的Systemreg编码,在其章节内有如下明确定义:

所以确实地址就是上面给定的sys_reg(3, 0, 0, 0, 5)映射出来的0x600A0
asm volatile方法
这个方法的本质其实就是在C文件中写汇编。为了达到上述的MRS的效果(包含两个方面,第一个是要执行MRS,第二个是要找到并放入一个内存中),就可以使用这个方法来跑汇编。
可以使用如下:
uint64_t mpidr; // 内存中定义一个变量
register uint64_t x0 asm("x0"); // 定义
asm volatile(
".inst 0xD5384100\n\t" // 手动插入机器码
"mov %0, x0" // 把结果移出来
: "=r"(mpidr)
:
: "x0"
);来进行MRS并把数据搬到mpidr中去。当然,这里的汇编会绕开编译器的寄存器分配,很容易导致bug。
注意,这里的编码是可以由如下算出来的
__emit_inst(0xd5200000|(\\sreg)|(.L__reg_num_\\rt))
算出来后,其中的L__reg_num_0会直接对应到%0或者x0,进而完成最后的挪出操作。