简介:这份PDF文献面向嵌入式Linux系统开发人员与内核移植学习者,聚焦多核处理器(SMP)环境下系统移植的关键技术难题。内容围绕SMP硬件结构、启动流程与设备树机制展开,系统梳理了从硬件分析、设备树构建、内核配置到多核同步、调度器优化及测试验证的完整移植路径,并以Linux-3.17.2内核与ARM平台为例进行剖析,适合具备一定内核基础、希望深入理解对称多处理架构的开发者参考。资源包内含1个PDF文件,大小约3.21MB,为正式期刊论文格式,含摘要、体系结构框图与启动流程图示,便于对照研读。目前已有195人学习下载,可作为嵌入式系统移植方向的专业参考文献与工程实践指导。
1. 从单核到双核:为什么嵌入式 Linux 移植绕不开 SMP
手上有一块 Cortex-A9 双核板子,跑的是 Linux 3.17.2,串口打印里只有 CPU0 在干活,cat /proc/cpuinfo只列出一个 processor,top里第二个核永远是 0.0% idle——这是很多嵌入式 Linux 系统移植项目里最常见的开局。问题不在驱动,也不在文件系统,而是内核根本没把第二个核拉起来。SMP(Symmetric Multi-Processing,对称多处理)要解决的就是这件事:让多个对等的 CPU 核共享同一片内存和 I/O 空间,由内核统一调度,而不是各跑各的。
单核时代靠拉主频提性能,工艺到顶之后这条路走不通了,多核并行成了主流。但嵌入式领域能直接吃下多核架构的操作系统并不多,Linux 因为开源、内核从 2.0 起就持续为 SMP 做改造,成了最现实的选择。这篇围绕 Linux-3.17.2、ARM 平台,把 SMP 移植里真正要动手的部分——设备树怎么描述多核、BSP 里smp_operations怎么填、内核怎么配、镜像怎么下——拆开讲清楚,适合正在做板级移植、或者被"第二个核起不来"卡住的嵌入式 Linux 开发者。
2. SMP 硬件结构与内核启动流程拆解
2.1 SMP 体系结构:共享内存与对称访问
SMP 的逻辑框图里,所有 CPU 核和 I/O 设备挂在同一条总线上,共享一个全局存储空间,任意核访问内存的代价是对称的。这一点和 NUMA 有本质区别:NUMA 里每个核到不同内存区域的延迟不一样,而 SMP 里不存在这种"本地内存"概念。代价是总线带宽会成为瓶颈,核越多,缓存一致性维护的开销越大。
Cortex-A9 MPCore 这类多核 SoC 里,维护数据 cache 一致性的关键部件是 SCU(Snoop Control Unit)。它监听各核的 cache 访问,保证一个核改了数据,其他核的 cache 里对应行会失效或更新。移植时如果 SCU 没使能,多核跑起来会出现数据错乱——这种 bug 极难定位,因为单核测试完全正常。所以smp_prepare_cpus里第一件事往往就是scu_enable。
2.2 三阶段启动:从自解压到 start_kernel
Bootloader 完成寄存器、外设、存储器映射和堆栈初始化后,通过函数指针携带三个参数跳进内核:r0=0,r1=机器 ID,r2=标签列表或设备树在内存中的首地址。内核启动分三个阶段:
第一阶段是自解压,执行arch/arm/boot/compressed/head.S的start标号,调用decompress_kernel()把压缩镜像解开。因为解压后的内核可能覆盖当前运行代码,所以要解压到zImage + 堆栈 0x10000这段空间的末尾。
第二阶段是处理器相关代码,入口在arch/arm/kernel/head.S的stext段。此时 MMU 和 D-cache 必须关闭,代码负责获取处理器类型、建页表、开 MMU,然后跳到start_kernel()。
第三阶段是处理器无关代码,从 C 语言的start_kernel开始,初始化所有内核部件。SMP 的关键在于:CPU0 作为主核先启动,初始化全部资源、配好启动页表映射;次核从指定区域启动,只初始化与自身相关的资源,然后进 IDLE 进程,绝不重复初始化 CPU0 已经搞定的东西。
2.3 主从核的唤醒时序
次核的启动不是自动的,得由 CPU0 主动唤醒。流程是:CPU0 在smp_init阶段通过核间中断或写寄存器的方式,向次核发送唤醒信号;次核收到后从secondary_startup进入,先v7_invalidate_l1清掉自己的 L1 cache,再走secondary_startup完成自身 MMU、cache 初始化,最后进 IDLE。这个时序如果错位——比如次核在 CPU0 还没建好页表时就启动——会直接跑飞。所以smp_boot_secondary里传的启动地址必须是物理地址,因为那时次核的 MMU 还没开。
3. 用设备树描述 SMP:cpus 节点与 dtb 编译
3.1 为什么 ARM 要引入设备树
早期 ARM Linux 的arch/arm/plat-xxx和arch/arm/mach-xxx目录里塞满了板级细节代码,大量内容只是在描述"这块板子有几个核、中断怎么连",属于硬编码。扁平设备树 FDT 从 PowerPC 体系引入 ARM 后,这些硬件细节可以直接通过设备树传给内核,板级代码大幅精简。对 SMP 移植来说,设备树让"描述多核"变成写几行 DTS,而不是改一堆 C 代码。
3.2 cpus 节点的写法与参数含义
在arch/arm/boot/dts/下新建xxx.dts,根节点/下加cpus子节点:
cpus { #address-cells = <1>; #size-cells = <0>; cpu@0 { device_type = "cpu"; compatible = "arm,cortex-a9"; reg = <0>; clocks = <&clkc 0>; }; cpu@1 { device_type = "cpu"; compatible = "arm,cortex-a9"; reg = <1>; clocks = <&clkc 0>; }; };#address-cells = <1>表示cpus子节点的reg是一个 32 位无符号整数,#size-cells = <0>表示只分配单独地址、不带大小字段。所以cpu@0和cpu@1的reg分别是 0 和 1,对应两个核的编号。compatible = "arm,cortex-a9"让设备和 cortex-a9 相关驱动绑定。核数要和实际 SoC 一致,写多了内核会去唤醒不存在的核,卡在超时。
3.3 编译 dtb 并挂进 Makefile
DTS 要编译成 dtb 二进制才能被内核解析。在arch/arm/boot/dts/Makefile里加一行:
dtb-$(CONFIG_ARCH_XXX) += xxx.dtb选中ARCH_XXX目标板后,在内核根目录执行:
make dtbs ARCH=arm CROSS_COMPILE=arm-linux-生成物在arch/arm/boot/dts/xxx.dtb。这里ARCH指定目标架构,CROSS_COMPILE是交叉工具链前缀,两个参数漏一个都会报找不到编译器或架构不匹配。
提示:dtb 里的核数和内核配置里的
CONFIG_NR_CPUS要对得上,前者描述硬件实际有几个核,后者是内核支持的最大核数,配小了多出来的核不会被枚举。
4. BSP 移植:smp_operations 与 platsmp.c 实现
4.1 DT_MACHINE_START 与设备树匹配
引入设备树后,每块板子用DT_MACHINE_START和MACHINE_END包一组回调。在arch/arm/mach-xxx/board.c里:
DT_MACHINE_START(XXX, "XXX Board") .smp = smp_ops(xxx_smp_ops), .map_io = xxx_map_io, .init_irq = xxx_irq_init, .init_machine = xxx_init_machine, .init_time = xxx_timer_init, .dt_compat = xxx_dt_match, .reserve = xxx_memory_init, .restart = xxx_system_reset, MACHINE_END.dt_compat是关键:它列出本机器兼容的 root 节点compatible字符串。Bootloader 传进来的设备树 root 节点compatible只要命中这个列表,这组初始化函数就会被执行。.smp把xxx_smp_ops挂上去,SMP 的启动逻辑就接进来了。
4.2 smp_operations 四个成员函数
struct smp_operations定义在arch/arm/include/asm/smp.h,四个成员各管一段:
| 成员函数 | 作用 |
|---|---|
| smp_init_cpus | 设置 SoC 中 CPU 核的个数 |
| smp_prepare_cpus | 设置次核的启动地址映射 |
| smp_secondary_init | 初始化指定核的运行环境 |
| smp_boot_secondary | 完成指定核的启动 |
在arch/arm/mach-xxx/platsmp.c里实现:
static int xxx_boot_secondary(unsigned int cpu, struct task_struct *idle) { /* 次核 MMU 未开,必须传物理地址 */ return xxx_cpu_start(virt_to_phys(xxx_secondary_startup), cpu); } static void __init xxx_smp_init_cpus(void) { int i, ncores = scu_get_core_count(xxx_scu_base); for (i = 0; i < ncores && i < CONFIG_NR_CPUS; i++) set_cpu_possible(i, true); } static void __init xxx_smp_prepare_cpus(unsigned int max_cpus) { scu_enable(xxx_scu_base); /* 使能 SCU,维护 cache 一致性 */ }xxx_smp_init_cpus用scu_get_core_count从 SCU 寄存器读出实际核数,逐个set_cpu_possible标记为可用。xxx_smp_prepare_cpus使能 SCU,这是多核 cache 一致性的前提。xxx_boot_secondary从xxx_secondary_startup唤醒次核,注意virt_to_phys转换——次核此刻 MMU 没开,只能认物理地址。
4.3 headsmp.S 里的次核入口
xxx_secondary_startup定义在arch/arm/mach-xxx/headsmp.S:
ENTRY(xxx_secondary_startup) bl v7_invalidate_l1 b secondary_startup ENDPROC(xxx_secondary_startup)先清 L1 cache,再跳到通用的secondary_startup。清 cache 这步不能省,否则次核可能带着脏数据进内核,导致随机崩溃。
4.4 Makefile、Kconfig 与 Makefile.boot
在arch/arm/mach-xxx/下补三个文件:Makefile决定哪些文件编进内核,Kconfig让这块板子出现在配置菜单里,Makefile.boot指定内核启动的物理起始地址。三个文件缺一个,make menuconfig里就找不到目标板,或者编译时链接不到 BSP 代码。
5. 内核配置、镜像下载与多核验证
5.1 menuconfig 里的 SMP 选项
进内核根目录执行make menuconfig,在 "Kernel Features" 下:
- 选中
Symmetric Multi-Processing Maximum number of CPUs设为 2(和 dtb 里的核数一致)- 按需选
Multi-core scheduler support、Support cpu topology definition
在 "Boot options" 下选中Flattened Device Tree support。配置完保存退出。
5.2 编译 uImage
make uImage ARCH=arm CROSS_COMPILE=arm-linux- LOADADDR=0x80008000LOADADDR是内核加载地址,由具体 SoC 决定,填错会导致解压后跳转地址不对。生成物在arch/arm/boot/uImage。
5.3 下载与启动
把xxx.dtb和uImage通过 Bootloader 下到 RAM:
tftpboot 0x80000100 xxx.dtb fdt addr 0x80000100 tftpboot 0x80008000 uImage bootm 0x80008000 - 0x80000100fdt addr告诉 Bootloader 设备树在内存的位置,bootm的第三个参数把 dtb 地址传给内核。串口终端能看到内核启动打印。
5.4 验证多核是否真的起来了
启动后执行:
cat /proc/cpuinfo | grep processor正常应列出 processor 0 和 1。再看dmesg | grep -i smp,会打印 "SMP: Total of 2 processors activated" 之类的信息。如果只有一个核,先查 dtb 里 cpus 节点核数、再查CONFIG_NR_CPUS、最后查smp_init_cpus里scu_get_core_count读出来的值对不对。
注意:
Allow booting SMP kernel on uniprocessor systems这个选项在调试阶段可以开,方便单核先跑通再上多核,但量产配置里一般关掉。
6. 多核跑起来之后的排错与调优技巧
多核能启动只是第一步,真正难的是跑稳。几个我踩过的点值得单独说。
次核卡在 IDLE 不动。现象是cat /proc/cpuinfo有两个核,但top里第二个核长期 0%。先确认smp_boot_secondary传的是物理地址,再确认xxx_secondary_startup里v7_invalidate_l1没被优化掉。如果次核起来了但没进调度,查Multi-core scheduler support是否选中。
随机崩溃、数据错乱。八成是 SCU 没使能或使能时机不对。scu_enable必须在次核启动前调用,放在smp_prepare_cpus里是标准做法。另外确认xxx_scu_base的地址映射正确,映射错会读到垃圾值。
核间中断收不到。SMP 靠 IPI(核间中断)做调度和同步,中断控制器配置不对,次核就收不到唤醒信号。查 GIC 的配置,确认每个核的 CPU interface 都使能了。
性能不升反降。核多了但负载没均衡,或者锁竞争太激烈。用perf top看热点,如果大量时间耗在 spinlock 上,说明临界区太大。常见做法是缩小锁粒度,或者把频繁访问的共享数据改成 per-cpu 变量。
调试手段。echo l > /proc/sysrq-trigger能打印所有核的调用栈,卡死时特别有用。/sys/devices/system/cpu/下能看到每个核的在线状态,echo 0 > cpu1/online可以热下线一个核做对比测试,快速判断问题是不是多核引入的。
一个具体技巧:怀疑是 cache 一致性问题时,临时把内核配成单核跑同样的负载。单核正常、多核出错,基本可以锁定在 SCU、cache 或锁上,比盲目加打印高效得多。
本文还有配套的精品资源,点击获取