Linux kernel 启动流程分析4---第一阶段proc info的获取
一、概述
1.1、kernel启动流程第一阶段简单说明
kernel 启动流程第一阶段
+-----------------------------------+
| 入口: stext |
+-----------------------------------+
|
v
+-----------------------------------+
| 设置为 SVC 模式, 关闭所有中断 |
+-----------------------------------+
|
v
+-----------------------------------+
| 获取 CPU ID, 提取相应的 proc_info |
+-----------------------------------+
|
v
+-----------------------------------+
| 验证 tags 或者 dtb |
+-----------------------------------+
|
v
+-----------------------------------+
| 创建页表项 |
+-----------------------------------+
|
v
+-----------------------------------+
| 配置 r13 寄存器, 设置跳转函数 |
+-----------------------------------+
|
v
+-----------------------------------+
| 使能 MMU (Memory Management Unit) |
+-----------------------------------+
|
v
+-----------------------------------+
| 跳转到 start_kernel (第二阶段) |
+-----------------------------------+
解释每一步:
-
入口:
stext
- 这是内核启动的第一个函数,ARM 架构的内核启动代码通常会在汇编文件中定义
ENTRY(stext)
作为入口点,CPU 会从此处开始执行内核的初始化代码。
- 这是内核启动的第一个函数,ARM 架构的内核启动代码通常会在汇编文件中定义
-
设置为 SVC 模式,关闭所有中断
- 在引导阶段,ARM 处理器默认是运行在 User 模式,但是为了执行特权操作,必须将处理器模式切换到 SVC 模式。同时,在此阶段关闭中断,避免在内核初始化期间中断干扰。
-
获取 CPU ID,提取相应的
proc_info
- 通过获取 CPU 的标识符(通常是通过某种机制如
CPUID
指令),可以从设备树或者其他硬件资源中获取到与当前 CPU 相关的配置信息(如架构、特性等)。这些信息将用于后续的处理器配置。
- 通过获取 CPU 的标识符(通常是通过某种机制如
-
验证 tags 或者 dtb
- Tags 或 DTB (Device Tree Blob) 是在引导过程中传递给内核的一些配置信息,内核需要验证这些信息的有效性。通过这些信息,内核可以识别硬件配置及设备信息。
-
创建页表项
- 在内核启动时,需要设置虚拟内存管理(MMU)。此时,内核创建并初始化页表,确保各个虚拟地址正确映射到物理内存。
-
配置 r13 寄存器,设置跳转函数
- 寄存器
r13
通常用于保存栈指针等关键信息。在此步骤中,内核会配置好栈指针,并设置跳转地址,以便在开启 MMU 后能够正确跳转到其他函数。
- 寄存器
-
使能 MMU (Memory Management Unit)
- 启用 MMU 以开启虚拟内存管理。通过 MMU,可以实现内存地址的映射,从而支持虚拟内存、进程隔离等功能。此时,通过设置相应的控制寄存器,开启 MMU。
-
跳转到
start_kernel
(第二阶段)- 在完成上述所有硬件初始化后,内核会跳转到
start_kernel
,这是内核的第二阶段,之后的任务会在此函数中完成,例如内核线程的创建、调度初始化等。
- 在完成上述所有硬件初始化后,内核会跳转到
1.2、疑问
主要带着以下几个问题去理解
为什么要获取CPU ID和proc info?也就是说proc info存放了什么东西以至于有必要在第一阶段、打开MMU之前就去获取?
如何获取对应CPU的proc info?
1.3、对应代码实现
__HEAD
ENTRY(stext)
mrc p15, 0, r9, c0, c0 @ get processor id,用于获取CPU ID,具体参考第二节
bl __lookup_processor_type @ r5=procinfo r9=cpuid,根据cpu id获取proc info,具体参考第一节和第三节。
movs r10, r5 @ invalid processor (r5=0)?,判断proc info是否存在
THUMB( it eq ) @ force fixup-able long branch encoding
beq __error_p @ yes, error 'p'
mrc p15, 0, r9, c0, c0 @ get processor id,用于获取CPU ID,
bl __lookup_processor_type @ r5=procinfo r9=cpuid,根据cpu id获取proc info。
二、procinfo
2.1、说明
获取 CPU ID 和提取相应的 proc_info
(进程信息)通常涉及两个方面:
- 获取 CPU ID:即获取当前系统中 CPU 的标识符或者核编号。
- 提取
proc_info
说明:从 CPU 信息中提取进程相关信息,或者读取系统中与 CPU 相关的详细信息。
procinfo使用proc_info_list结构体,用来说明一个cpu的信息,包括这个cpu的ID号,对应的内核数据映射区的MMU标识等等。
2.2、数据结构定义
arch/arm/include/asm/procinfo.h
/*
* Note! struct processor is always defined if we're
* using MULTI_CPU, otherwise this entry is unused,
* but still exists.
*
* NOTE! The following structure is defined by assembly
* language, NOT C code. For more information, check:
* arch/arm/mm/proc-*.S and arch/arm/kernel/head.S
*/
struct proc_info_list {
unsigned int cpu_val;
unsigned int cpu_mask;
unsigned long __cpu_mm_mmu_flags; /* used by head.S */
unsigned long __cpu_io_mmu_flags; /* used by head.S */
unsigned long __cpu_flush; /* used by head.S */
const char *arch_name;
const char *elf_name;
unsigned int elf_hwcap;
const char *cpu_name;
struct processor *proc;
struct cpu_tlb_fns *tlb;
struct cpu_user_fns *user;
struct cpu_cache_fns *cache;
};
重点关注的成员如下:
- cpu_val:cpu对应的硬件id号
- cpu_mask:cpu硬件id号的掩码
- __cpu_mm_mmu_flags:临时页表映射的内核空间的MMU标识
- __cpu_io_mmu_flags:IO映射区的MMU标识
- __cpu_flush:cpu setup函数的地址,后续在打开MMU过程时候会使用到
注意:
- 这里存在的MMU标识,也就是我们需要在打开MMU之前需要先获取procinfo的原因,因为打开MMU之前需要配置临时内核页表,而配置临时内核页表需要这里的MMU标识来进行设置。在后续创建临时内核页表的文章中会进行说明。
这里回答了“proc info存放了什么东西以至于有必要在第一阶段、打开MMU之前就去获取?”的疑问。
- cpu id和procinfo是一一对应的关系,所以可以通过cpu id来获取到对应的procinfo结构体。
2.3、存放位置
所有CPU的proc info都会被存放到.init.proc.info段中
arch/arm/kernel/vmlinux.lds.S
SECTIONS
{
.init.proc.info : {
ARM_CPU_DISCARD(PROC_INFO)
}
}
#define PROC_INFO \
. = ALIGN(4); \
VMLINUX_SYMBOL(__proc_info_begin) = .; \
*(.proc.info.init) \
VMLINUX_SYMBOL(__proc_info_end) = .;
通过查看Systemp.map可以看到.init.proc.info段里面放了这些cpu的procinfo
8041a800 T __proc_info_begin
8041a800 t __v7_ca5mp_proc_info
8041a834 t __v7_ca9mp_proc_info
8041a868 t __v7_ca8_proc_info
8041a89c t __v7_cr7mp_proc_info
8041a8d0 t __v7_ca7mp_proc_info
8041a904 t __v7_ca12mp_proc_info
8041a938 t __v7_ca15mp_proc_info
8041a96c t __v7_b15mp_proc_info
8041a9a0 t __v7_ca17mp_proc_info
8041a9d4 t __krait_proc_info
8041aa08 t __v7_proc_info
8041aa3c T __proc_info_end
2.4、示例
arm体系是armv7,cortex-A8架构,对应procinfo定义于proc-v7.S中。
arch/arm/mm/proc-v7.S
.section ".proc.info.init", #alloc
/*
* ARM Ltd. Cortex A8 processor.
*/
.type __v7_ca8_proc_info, #object
__v7_ca8_proc_info:
.long 0x410fc080
.long 0xff0ffff0
__v7_proc __v7_ca8_proc_info, __v7_setup, proc_fns = ca8_processor_functions
.size __v7_ca8_proc_info, . - __v7_ca8_proc_info
通过.section “.proc.info.init”将后面的数据结构定义在了.proc.info.init段中,最终在连接过程中被连接到.init.proc.info段中,也就是__proc_info_begin和__proc_info_end之间的位置中。
__v7_proc是一个宏,其定义如下
.macro __v7_proc name, initfunc, mm_mmuflags = 0, io_mmuflags = 0, hwcaps = 0, proc_fns = v7_processor_functions
ALT_SMP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_SMP | \mm_mmuflags)
ALT_UP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_UP | \mm_mmuflags)
.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ | PMD_SECT_AF | \io_mmuflags
initfn \initfunc, \name
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB | HWCAP_FAST_MULT | \
HWCAP_EDSP | HWCAP_TLS | \hwcaps
.long cpu_v7_name
.long \proc_fns
.long v7wbi_tlb_fns
.long v6_user_fns
.long v7_cache_fns
.endm
根据数据结构定义,__v7_ca8_proc_info对应如下结果
- cpu_val:0x410fc080
- cpu_mask:0xff0ffff0
- __cpu_mm_mmu_flags:PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_UP | \mm_mmuflags
- __cpu_io_mmu_flags: PMD_TYPE_SECT | PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ | PMD_SECT_AF | \io_mmuflags
- __cpu_flush : __v7_setup
三、如何获取CPU ID
3.1、原理
arm体系上可支持最多16个协处理器。
arm体系将CPU ID(处理器标识符,主标识符)存放在协处理器cp15的c0寄存器中。
ARM 系统中的协处理器 (Co-Processor)
在 ARM 体系结构中,协处理器用于提供扩展功能,如内存管理、缓存控制、调试、系统控制等。ARM 支持最多 16 个协处理器(协处理器编号 0 到 15)。这些协处理器能够通过特定的指令与主处理器进行交互,通常这些指令以 MRC
(从协处理器读取数据)和 MCR
(向协处理器写入数据)的形式使用。
CP15
协处理器和 C0
寄存器
在 ARM 架构中,CP15
是一个特殊的协处理器,它包含多个寄存器,用于控制和访问与处理器相关的各种状态。特别地,C0
寄存器包含了处理器的标识符信息。
获取 CPU ID
CP15
协处理器的 C0
寄存器通常存储了 CPU 的相关信息,包括处理器的 ID。使用 ARM 汇编语言,可以通过 MRC
指令读取 CP15
协处理器的 C0
寄存器,以获取 CPU 标识符。
例子:读取 CPU ID
在 ARM 中,可以使用以下汇编指令来读取 CP15
协处理器的 C0
寄存器中的 CPU 标识符。
MRC p15, 0, r0, c0, c0, 0 ; 从协处理器 CP15 的 C0 寄存器读取 CPU ID
p15
表示协处理器编号为 15,通常是系统的协处理器。r0
是一个通用寄存器,用于存放C0
寄存器中的值。c0, c0, 0
指示CP15
协处理器的C0
寄存器。
执行这个指令后,CPU 标识符将被加载到 r0
寄存器中,您可以进一步分析该值以获取 CPU 的标识信息。
进一步的 CPU 信息
除了 CPU ID,C0
寄存器还可能存储其他与处理器特性相关的信息(如 CPU 型号、版本等)。在一些 ARM 架构中,还可能存在其他协处理器寄存器,用于存储更多关于处理器的详细信息(如核心数、缓存大小等)。
例如,ARM 的 MIDR
(Main ID Register)寄存器通常用于存储处理器的 ID 信息,您可以通过读取 MIDR
寄存器来获得处理器的 ID。
示例:读取 MIDR(处理器主标识寄存器)
MRC p15, 0, r0, c0, c0, 5 ; 从 CP15 协处理器的 C0 寄存器的第5位置读取 MIDR
3.2、协处理器cp15
3.2.1、CP15
协处理器的功能
CP15
协处理器主要用于以下功能:
- 内存管理:包括页表、MMU(内存管理单元)的配置等。
- 缓存管理:控制缓存的启用与禁用、缓存的清除和刷新等。
- 异常控制:管理中断和异常的处理机制。
- 系统控制:处理器和系统的控制寄存器,包括操作模式、频率等设置。
- 调试与性能监控:提供调试支持和性能计数器等功能。
3.2.2、CP15
协处理器的寄存器和操作
CP15
协处理器包含多个寄存器,允许系统开发人员对 ARM 处理器的多个重要功能进行配置和管理。常见的寄存器包括 C0
到 C15
,它们的功能各不相同,具体功能可以通过 ARM 体系结构文档来查看。下面是一些重要的寄存器和操作。
1. C0 寄存器(ID
寄存器)
C0
寄存器通常存储处理器的标识信息,如 CPU 的型号、版本、核心类型等。它可以帮助开发者识别当前处理器的具体特性。
- 通过
MRC
指令读取:MRC p15, 0, r0, c0, c0, 0
p15
:协处理器 15。c0, c0, 0
:读取C0
寄存器。
2. C1 寄存器(Control Register
)
C1
寄存器是控制寄存器,用于启用或禁用一些系统级别的功能,如 MMU(内存管理单元)和缓存的控制。
- 通过
MRC
指令读取:MRC p15, 0, r0, c1, c0, 0
- 通过
MCR
指令写入:MCR p15, 0, r0, c1, c0, 0
3. C2 寄存器(Memory Management Control Register
)
C2
寄存器控制内存管理的一些功能,如页面表配置。
4. C7 寄存器(Cache Operations Register
)
C7
寄存器用于缓存的控制操作,例如缓存的清除和无效化操作。对于缓存管理,使用 MRC
和 MCR
指令来执行缓存操作。
5. C13 寄存器(Performance Monitor
)
C13
寄存器与性能监控相关,可以用于性能计数器的配置,帮助开发者跟踪和分析处理器的执行效率。
CP15
常用操作
- 启用 MMU(内存管理单元)
MRC p15, 0, r0, c1, c0, 0 ; 读取控制寄存器 C1
ORR r0, r0, #0x1 ; 启用 MMU(设置 C1 寄存器的 bit0)
MCR p15, 0, r0, c1, c0, 0 ; 写回更新后的控制寄存器 C1
- 启用缓存
启用数据缓存:
MRC p15, 0, r0, c1, c0, 0 ; 读取控制寄存器 C1
ORR r0, r0, #0x4 ; 启用数据缓存
MCR p15, 0, r0, c1, c0, 0 ; 写回更新后的控制寄存器 C1
- 清除缓存
MCR p15, 0, r0, c7, c5, 0 ; 清除数据缓存
- 读取处理器 ID
读取 C0
寄存器(通常用于获取处理器 ID 和特性):
MRC p15, 0, r0, c0, c0, 0 ; 读取处理器 ID 信息
3.3、协处理器指令说明
(1)MCR 指令:ARM寄存器到协处理器寄存器的数据传送
MCR{<cond>} <p>,<opcode_1>,<Rd>,<CRn>,<CRm>{,<opcode_2>}
MCR{<cond>} p15,0,<Rd>,<CRn>,<CRm>{,<opcode_2>}
(2)MRC指令: 协处理器寄存器到ARM寄存器的数据传送
MRC{<cond>} <p>,<opcode_1>,<Rd>,<CRn>,<CRm>{,<opcode_2>}
MRC{<cond>} p15,0,<Rd>,<CRn>,<CRm>{,<opcode_2>}
- cond为指令执行的条件码。当忽略时指令为无条件执行。
- opcode_1为协处理器将执行的操作的操作码。对于CP15协处理器来说,< opcode_1>永远为0b000,当< opcode_1>不为0b000时,该指令操作结果不可预知。
- Rd作为源寄存器的ARM寄存器,其值将被传送到协处理器寄存器中。
- CRn作为目标寄存器的协处理器寄存器,其编号可能是C0,C1,…,C15。
- CRm和opcode_2两者组合决定对协处理器寄存器进行所需要的操作,如果没有指定,则将为为C0,opcode_2为0,否则可能导致不可预知的结果。
3.4、获取cpu id的指令
通过上述,通过mrc指令从p15中的c0寄存器中获取CPU ID,并且获取获取CPU ID的参数如下:
p->p15,
opcode_1->0
Rd->r9(我们要存放在r9寄存器其中)
CRn->c0
CRm->c0
最终获取cpu id的指令如下:
mrc p15, 0, r9, c0, c0 @ get processor id
四、如何获取cpu对应的procinfo
4.1、原理
1.内核初始化时的 CPU 探测
内核初始化时会首先探测硬件,特别是 CPU 的数量、型号等信息。Linux 内核会通过 CPUID 指令(在 x86 架构上)或其他架构特有的机制来识别和初始化 CPU。内核会收集每个 CPU 的信息,并将这些信息存储到一个结构体中。
2.proc_info
结构体与 .init.proc.info
段
在内核启动期间,CPU 相关的所有信息通常存储在特定的内存区域,这些区域可以通过 .init.proc.info
来访问。该区域包含了内核在启动时构建的 CPU 信息数据。
proc_info
结构体可能会包含如下信息:
- CPU 类型
- CPU 核数
- 支持的特性(SSE、AVX 等)
- CPU 的频率和缓存信息
- 核心 ID、物理核心数、线程数等
这些信息通常在内核启动时通过静态方式(例如在编译时)或者动态方式(通过 cpuid
和其他机制)收集并存储。
struct proc_info {
int cpu_id;
unsigned long cpu_val;
/* 其他与 CPU 相关的信息 */
};
3.__proc_info_begin
和 __proc_info_end
这两个符号通常定义在内核的某些初始化代码中,表示 proc_info
数据段的起始和结束位置。这段内存区域包含了 CPU 的信息,并且这些信息是在内核初始化阶段静态地设置的。
__proc_info_begin
:表示proc_info
数据段的起始位置。__proc_info_end
:表示proc_info
数据段的结束位置。
这两个符号通常被定义为全局符号,编译器会将它们放置在适当的位置(例如 .init.proc.info
段中)。
4.通过 CPU ID 查找 proc_info
内核通过 CPU ID 来定位与特定 CPU 相关的 proc_info
结构体。在内核启动时,每个 CPU 的信息都会存储在 proc_info_list
中。proc_info_list
是一个链表或数组,存储了每个 CPU 的 proc_info
结构体。内核会根据 CPU 的 ID 从 proc_info_list
中找到对应的 CPU 信息。
在启动过程中,内核会依次遍历 __proc_info_begin
到 __proc_info_end
之间的 proc_info_list
。当内核获取到当前 CPU 的 ID 后,它会遍历这个区间内的所有 proc_info
结构体,找到匹配的 cpu_id
,并返回对应的 proc_info
。
例如,内核可能会执行以下步骤:
void init_cpu_info(void) {
int i;
for (i = 0; i < num_cpus; i++) {
// 获取当前CPU的ID
int cpu_id = get_cpu_id(i);
// 在proc_info_list中查找与CPU ID匹配的proc_info结构体
for (proc_info *info = __proc_info_begin; info < __proc_info_end; info++) {
if (info->cpu_id == cpu_id) {
// 找到匹配的proc_info结构体
process_cpu_info(info);
break;
}
}
}
}
在这个过程中,内核会检查每个 proc_info
结构体的 cpu_id
字段,并与当前的 CPU ID 进行比较。如果匹配,就找到了对应的 proc_info
结构体,从而获得该 CPU 的相关信息。
4.2、代码实现
- 首先将__proc_info_begin的区间信息存放在__lookup_processor_type_data位置上
arch/arm/kernel/head-common.S
/*
* Look in <asm/procinfo.h> for information about the __proc_info structure.
*/
.align 2
.type __lookup_processor_type_data, %object
__lookup_processor_type_data:
.long .
.long __proc_info_begin
.long __proc_info_end
.size __lookup_processor_type_data, . - __lookup_processor_type_data
在stext中调用__lookup_processor_type来获取cpu对应的proc info
其中r9存放的是cpu id,r5存放的是获取到的proc info的地址
__HEAD
ENTRY(stext)
bl __lookup_processor_type @ r5=procinfo r9=cpuid
__lookup_processor_type实现如下
arch/arm/kernel/head-common.S
/*
* Read processor ID register (CP#15, CR0), and look up in the linker-built
* supported processor list. Note that we can't use the absolute addresses
* for the __proc_info lists since we aren't running with the MMU on
* (and therefore, we are not in the correct address space). We have to
* calculate the offset.
*
* r9 = cpuid
* Returns:
* r3, r4, r6 corrupted
* r5 = proc_info pointer in physical address space
* r9 = cpuid (preserved)
*/
__lookup_processor_type:
adr r3, __lookup_processor_type_data
ldmia r3, {r4 - r6}
sub r3, r3, r4 @ get offset between virt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
1: ldmia r5, {r3, r4} @ value, mask
and r4, r4, r9 @ mask wanted bits
teq r3, r4
beq 2f
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
cmp r5, r6
blo 1b
mov r5, #0 @ unknown processor
2: ret lr
ENDPROC(__lookup_processor_type)
解析如下:
(1)获取proc_info区间的连接地址
adr r3, __lookup_processor_type_data
ldmia r3, {r4 - r6}
通过上述
r3存放的是__lookup_processor_type_data的真实的物理地址,也就是在RAM上的位置
r4存放的是__lookup_processor_type_data的连接地址
r5存放的是__proc_info_begin的连接地址
r6存放的是__proc_info_end的连接地址
(2)计算出proc_info区间的内存地址
sub r3, r3, r4 @ get offset between virt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
因为此时MMU是没有打开的,所以并不能直接使用连接地址来访问对应区域,而需要计算出对应区域的内存地址。
首先计算出__lookup_processor_type_data的真实物理地址(r4)和连接地址(r3)的偏移,
然后根据偏移计算出__proc_info_begin(r5)和__proc_info_end(r6)的真实物理地址,也就是内存地址。
通过上述步骤之后
r5存放的是__proc_info_begin的物理地址,也就是内存地址
r6存放的是__proc_info_end的物理地址,也就是内存地址
可以直接r5和r6访问到proc info的区间。
(3)提取结构体信息并进行比较
1: ldmia r5, {r3, r4} @ value, mask
and r4, r4, r9 @ mask wanted bits
teq r3, r4
beq 2f
“ldmia r5, {r3, r4}”获取[__proc_info_begin-__proc_info_end]中第一个proc_info_list结构体的,因为cpu_val和cpu_mask被存放在proc_info_list结构体的前16个字节,所以可以直接这样获取。
r3上就存放了cpu_val,r4上存放了cpu_mask。
将cpu_mask(r4)和获得的cpuid(r9)进行掩码后和cpu_val(r3)进行比较,相等则返回退出,此时的r5上存放了对应的proc_info地址。否则进入下一个循环。
(4)获取下一个proc_info_list结构体,
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
cmp r5, r6
blo 1b
mov r5, #0 @ unknown processor
如果还没有到__proc_info_end(r6),则继续下一个循环。如果已经搜索到结尾,将r5设置为0(也就表示非法值)后直接退出。
通过上述步骤之后,就可以获取到了cpu id对应的proc_info_list结构体,并且其地址存放在r5寄存器中。