尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux SMP多核启动机制详解:从主核引导到从核唤醒与故障排查

Linux SMP多核启动机制详解:从主核引导到从核唤醒与故障排查 做嵌入式Linux和内核开发这一行SMP多核启动这块迟早要啃。很多人的第一反应是系统上电以后四个核不是应该一起跑起来吗实际情况是你在串口上看到的第一行内核日志往往只有一个CPU在工作直到某一行smp: Bringing up secondary CPUs ...出现剩下的核才陆续醒来。这个唤醒过程就是Linux SMP多核启动机制在做的事。我先说结论Linux的多核启动本质上是先让一个主核Boot CPU通常是CPU0把内存、时钟、中断控制器、调度器这些家底都整理好再通过一套状态机和跨核中断把其他从核逐个拉起来。这篇文章按调用的先后顺序把主核启动、从核唤醒、CPU hotplug状态机、内核日志、故障排查一条链路讲完。做内核移植、嵌入式BSP或者面试前想系统过一遍这块知识的都可以拿这篇当个索引。1. SMP多核启动到底在启动什么1.1 对称多处理的不对称起点SMPSymmetric Multi-Processing的关键词是“对称”多个CPU共享同一块物理内存地位平等谁都能执行内核代码、谁都能响应外部中断。x86服务器、ARM64派系、多数嵌入式多核SoC跑的都是SMP这套逻辑。但真正上电那一刻“对称”是不成立的必须有一个不对称的起点。原因很朴素几个核一上电就同时执行首先抢的就是内存控制器连一个能统一指挥的人都没有。内核需要有人先把页表建立起来把异常向量表、中断控制器、时钟源挨个初始化再告诉其他核“你们可以上了”。这个起跑的人就是Boot CPU。这有点像开公司合伙人再多注册那天也得有一个人先去把营业执照办下来其他人随后入场。Linux设计上没有让所有CPU从复位开始就“齐步走”就是为了避免这种群龙无首的混乱。1.2 谁来决定谁是Boot CPUBoot CPU的选定通常是硬件和固件商量好的。ARM64平台上主核从复位向量开始执行其他核要么进入WFIWait For Interrupt低功耗状态要么在指定的spin-table地址上原地打转等主核发信号。x86则是BSPBootstrap Processor先启动APApplication Processor被INIT信号按住等待SIPI指令唤醒。这里有个容易忽略的细节Boot CPU不保证永远是CPU0。某些平台固件自己就用了CPU1跑了一段代码Linux解析到之后会把CPU编号重新梳理。所以写BSP代码时不要想当然地认为“CPU0一定先跑”更可靠的做法是让内核自己在smp_init_cpus阶段去枚举。1.3 主核要准备哪些“基础设施”从复位到start_kernel主核必须搞定三类基础设施。第一是内存。早期汇编阶段要建立恒等映射和内核镜像映射否则后面的C代码连取指都没法做。Linux内核从汇编跳到start_kernel之后跑的是虚拟地址如果页表没把内核映射好PC一跳就非法访问后面全是空谈。第二是中断。GICGeneric Interrupt Controller的distributor必须提前初始化因为从核被唤醒后要跟主核完成“握手”底层靠的是SGISoftware Generated Interrupt也就是IPI的一种。GIC没初始化这个中断发不出去从核在线流程直接卡死。第三是时钟。tick device、clocksource不初始化调度器的时钟节拍就是乱的。多核调度器要把负载分到各个CPU上每个CPU都要有能用的本地定时器所以这些在唤醒从核之前都得就位。这些基础设施都搞定后主核才会走进smp_init和cpu_up去唤醒从核。记住这个顺序后面排障时你就能明白为什么日志里某一步没出现就说明前面某个子系统已经出了问题。2. 从Boot CPU到start_kernel主核的完整引导链2.1 汇编阶段建页表、设栈、跳C代码主核的引导是从架构相关的汇编代码开始的。以ARM64为例入口是stext它要干的事情非常聚焦建立早期页表、设置临时栈、然后跳转到C语言的入口。这一步有几个容易踩坑的点。第一个是栈指针。汇编阶段设置的临时栈必须足够大因为start_kernel早期就会有比较深的函数调用栈太小会在没有日志的情况下莫名崩溃。第二个是异常等级。ARM64有EL1到EL3内核跑在EL1但早期可能从EL2虚拟化扩展甚至EL3下来需要把异常等级切对同时把相关的系统寄存器配好。x86那边更繁琐一点要经历16位实模式、32位保护模式、64位长模式的切换中间还要解析Zero Page里的内存布局。但逻辑内核主线是一致的先让自己的执行环境可靠再往下走。2.2 start_kernel里谁在管“有多少CPU”进入start_kernel之后一个很容易被忽略但非常关键的节点是到底有多少CPU不是靠某个宏写死的而是由固件、设备树或ACPI表告诉内核的。ARM64的setup_arch里会调用smp_init_cpus把设备树里cpus节点、CPU的enable-method逐一解析出来填充possible和present掩码。x86则是从ACPI的MADT表里读Local APIC信息判断有哪些处理器。这里我补充一点possible掩码不是最终决定实际能用的CPU数量它只代表“系统里最多可能有这些CPU”。present代表“硬件上确认存在的CPU”。online才是真正被拉起来参与调度的。这三个掩码在排障时非常有用——如果possible里有CPU4但present里没有说明枚举阶段就丢了如果present有但online没有说明唤醒阶段挂了。2.3 设备树与ACPI怎么描述多核拓扑设备树里CPU节点长这样cpus { #address-cells 1; #size-cells 0; cpu0 { device_type cpu; compatible arm,cortex-a53; reg 0x0; enable-method psci; cpu-release-addr 0x0 0x8000fff8; }; };其中enable-method决定了内核用哪种机制唤醒这个核心。如果是spin-table内核会往cpu-release-addr写入唤醒地址如果是psci内核通过固件接口的CPU_ON指令唤醒。这个属性如果漏掉或者写错从核就永远起不来。ACPI下发的CPU信息则是解析MADTMultiple APIC Description Table里的GICC结构体逻辑上等价于设备树。这套信息最终汇总成struct cpu数组驱动后面的cpu hotplug流程。3. 从核唤醒的三种主流姿势3.1 spin-table最朴素的握手spin-table是早期ARM64平台很常见的唤醒方式思路非常朴素从核上电后在某个约定的内存地址上死循环轮询主核把从核的入口地址写进去然后触发一个事件从核看到新地址后跳过去执行。这里有个关键点写入地址的操作必须具备release语义而从核轮询必须是acquire语义否则会出现缓存不一致从核读到的是旧值。Linux内核在写release地址时用了smp_store_release从核侧用smp_load_acquire配套这一对屏障缺一不可。spin-table的优点是逻辑简单不依赖安全固件很适合裸机环境和早期调试。缺点是它把“电源管理”这种本该由固件管的事放到了内核里扩展性差。现代平台已经逐渐转向PSCI但理解spin-table仍然有意义因为它代表了最基础的“共享内存加中断”的协作模型。3.2 PSCI把活交给固件PSCIPower State Coordination Interface是ARM定义的电源管理规范内核通过smc指令陷入EL3让可信固件去执行CPU上电、下电等操作。从核唤醒用的是CPU_ON这个接口主核传入目标CPU编号和入口地址固件负责把核心从低功耗状态拉起来。为什么PSCI更受青睐因为它把电源状态管理集中到了固件侧内核不需要关心具体平台怎么操作寄存器也不用担心spin-table里缓存一致性的边界问题。同时PSCI还能处理安全状态切换比如从核在Secure World里的话普通内核代码根本没权限碰。在实际开发中如果设备树里写的是enable-method psci内核会拼接一个cpu_on消息通过smc调用触发。启动日志里如果能看到PSCI: SMC call done或者类似固件信息说明这一路是通的。3.3 x86的INIT-SIPI与通用IPI唤醒x86的唤路径和ARM完全不同。BSP通过Local APIC发送INIT信号让AP复位再发送SIPIStartup IPI让AP从实模式的warm reset vector开始执行。AP在trampoline代码里从16位实模式一路切换到长模式最后跳进secondary_startup_64。SIPI一般发两次因为第一次可能因为总线竞争没收到这是硬件层面的容错惯例。无论哪种架构从核真正进入内核C代码后的路径是大同小异的设置current、初始化栈、通知主核“我已经起来了”然后进入idle循环等待调度。区别只在于“走到内核这一步之前”硬件和固件帮你做了多少。4. CPU hotplug状态机与从核上线全流程4.1 cpuhp状态机里发生了什么直接把从核拉起来是最简单的想法但Linux没有这么干。原因在于一个CPU从离线到在线要走完很多子系统的初始化per-cpu时钟、RCU、workqueue、调度器、热插拔回调等等。这些子系统之间有依赖关系必须按顺序来。所以内核把整个生命周期拆成了状态机从CPUHP_OFFLINE一路走到CPUHP_ONLINE中间有几十个状态。从唤醒从核的角度最关键的几个状态是CPUHP_CREATE_THREADS负责创建per-cpu内核线程CPUHP_BRINGUP_CPU是架构相关的实施阶段它会调用__cpu_up真正去执行spin-table或PSCI的唤醒动作。状态机的好处是每个回调函数只干一件小事出错时可以精确定位到是哪一步失败。坏处是排障的时候不看状态机日志你根本不知道从核卡在哪个环节。所以遇到从核不起来的问题第一步不是改代码而是先确认状态机走到哪一步。4.2 从核自举secondary_start_kernel要干的事从核被唤醒后汇编层先设置好栈指针然后跳进secondary_start_kernel。这个函数是每个从核的“二次人生起点”它主要做几件事设置current指向自己的idle任务初始化per-cpu的寄存器状态配置好本地定时器然后进入CPU hotplug的AP侧回调序列。一个常见的坑是从核执行的代码路径里不能随便调用可能需要主核配合的函数否则可能死锁。因为从核起来的早期调度器对它的感知还不完整有些全局锁确实拿到了但等待方是主核而主核正在等待从核完成上线——这就是经典的ABBA死锁。内核为了让从核上线安全专门设计了cpuhp_thread机制从核在状态机的AP侧回调里由自己创建的cpuhp线程执行而不是直接在中断上下文硬跑。4.3 possible/online/present掩码是怎么维护的前面提到过possible、present、online三个掩码这里把它们的维护时机说清楚。possible在smp_init_cpus阶段逐步设置内核根据设备树或ACPI把所有可能存在的CPU编号加进去。present同样在早期发现硬件时设置它表示“这些CPU确实存在”。online则是cpuhp状态机到达CPUHP_ONLINE后由内核把对应的位图置上。系统运行起来后你随时可以通过sysfs查看这些掩码cat /sys/devices/system/cpu/possible cat /sys/devices/system/cpu/present cat /sys/devices/system/cpu/online用这个能快速判断问题是出在CPU“存在性”上还是“可运行性”上。如果possible和present都是0-3但online只有0那明显是从核唤醒或上线的环节出了问题。5. 实操观察怎么确认你的系统真的“多核启动”了5.1 从内核日志里读启动过程启动完成后先看dmesg里和SMP相关的日志dmesg | grep -i smp dmesg | grep -i cpu一个正常的多核启动过程日志大致长这样smp: Bringing up secondary CPUs ... CPU1: Booted secondary processor 0x0000000001 [0x410fd083] CPU2: Booted secondary processor 0x0000000002 [0x410fd083] CPU3: Booted secondary processor 0x0000000003 [0x410fd083] smp: Brought up 1 node, 4 CPUs看到Brought up 1 node, 4 CPUs说明四个核都已经在线。如果只有Bringing up secondary CPUs ...而没有后续说明某个从核卡在唤醒阶段接下来要按状态机去查。值得一提的是Booted secondary processor后面那串数字是该CPU的MIDR寄存器值不同核心型号不一样。看到两个CPU的MIDR不同说明系统里混了大小核配置调度域时要小心。5.2 用sysfs和命令行验证CPU状态内核起来后用几个命令就能确认当前CPU拓扑。nproc lscpu cat /sys/devices/system/cpu/onlinelscpu能看到CPU(s)、Core(s) per socket、Thread(s) per core这些信息底层读的是sysfs和/proc/cpuinfo。在线状态则直接看/sys/devices/system/cpu/online这个文件输出0-3表示从0到3全部在线。如果想看每个核当前的工作负荷可以用top然后按1或者用mpstat -P ALL。在调试多核负载均衡时这两个命令比看平均负载更直接。5.3 中断亲和性与负载均衡配置多核起来只是第一步能不能把活儿均匀分到各核上是另一件事。中断亲和性是一个关键维度。查看中断分布用cat /proc/interrupts每一列对应一个CPU。如果发现某个网卡中断全落在CPU0上那CPU0很快会成为瓶颈。可以用echo手动设置affinityecho 2 /proc/irq/78/smp_affinity这里的2是二进制位图表示只允许CPU1处理该中断。生产环境通常用irqbalance自动调度但在嵌入式场景里手动绑亲和性往往更可控。进程绑核则用tasksettaskset -c 0,1 ./my_benchmark这表示进程只允许在CPU0和CPU1上运行。多核性能调优时先分清楚是中断不均衡还是任务不均衡再决定动哪个参数这个排查顺序很重要。6. 多核启动的典型故障与排查实录6.1 从核没起来日志和错误码怎么定位最常见的问题是某个从核没有成功online。日志里可能报错CPU1: failed to come online或者干脆卡在Bringing up secondary CPUs ...这里没有任何后续。这时候按这个顺序查。先确认设备树或ACPI里CPU节点有没有配enable-method。很多从核起不来是因为设备树里漏了enable-method psci内核不知道用什么方式唤醒干脆跳过。再看固件侧PSCI的CPU_ON是否实现正确这个可以用固件日志或者尝试手动调用CPU_ON接口验证。如果确认唤醒指令发出去了但从核没反应接下来看中断。从核和主核握手的完成通知依赖IPIGIC初始化有问题的话从核可能已经跑了但主核收不到完成中断只能超时。这时候用内核命令maxcpus1启动对比一下单核是否正常能快速把问题范围缩小。6.2 从核启动后随机死机栈与中断的问题比“完全起不来”更难查的是“从核起来了但系统运行一会儿就死”。这种偶发问题优先怀疑两类原因。一类是从核栈问题。每个CPU的idle任务栈是独立分配的大小一般是16K。如果某个驱动在硬中断里用了过深的栈或者配置了过大的percpu变量很容易溢出。内核里有CONFIG_DEBUG_STACK_USAGE选项配合ftrace的stack_max_size可以抓到峰值栈使用排查时开起来非常有用。另一类是中断亲和性问题。如果外设中断在多个核之间迁移而驱动没有正确处理per-cpu数据就会出现竞争。典型现象是单核跑怎么都正常多核跑就偶发crash而且crash现场看起来毫无规律。遇到这种先把irqaffinity固定到一个核上如果问题消失说明跟中断并发有关再进一步查锁或禁用中断的临界区。6.3 手头调多核启动的三个建议最后分享几个我自己调多核启动时的习惯。第一善用内核cmdline。nr_cpus1和maxcpus1都能限制CPU数量但含义不同。nr_cpus是限制possible掩码maxcpus是限制online数量。排障时先用maxcpus1把系统降级为单核确认基线功能正常再逐步放开。第二打开cpuhp的ftrace事件。把/sys/kernel/debug/tracing/events/cpuhp/enable置1启动阶段就能看到每个CPU在状态机上执行了哪些回调、停在哪一步。这比盲改代码高效得多。状态机回调是分阶段的日志里会有明确提示卡在哪一目了然。第三怀疑缓存一致性时优先检查固件和内核的内存屏障是否匹配。spin-table这种共享内存协作方式如果从核的固件轮询没有做acquire主核的release写得再对也白搭。这不是内核能单方面解决的问题得固件和内核一起改。我实际项目里遇到的坑七成出在设备树和固件三成才轮到内核。先把“系统认为有几个CPU、能唤醒哪些核”搞清楚问题往往就解决了一半。再配合状态机日志和合理的内核cmdline基本上能把问题范围缩到很小。多核启动机制本身不难理解难的是把这条链路上的所有参与者串起来从硬件到固件再到内核每一步都走对了才能看到那行让人安心的smp: Brought up 1 node, 4 CPUs。
返回列表