3、Zephyr多核支持总览:Kconfig配置体系、多核启动流程、CPU掩码(cpu_mask)机制

好,咱们今天来聊聊Zephyr的多核支持。说实话,我第一次接触Zephyr的多核架构时,也被它那套配置体系绕得有点晕。但摸清楚之后你会发现,它其实设计得很巧妙。这一节,我会把Kconfig配置、启动流程、还有那个让人又爱又恨的cpu_mask机制,掰开了讲给你听。

3.1 Kconfig配置体系:多核的“开关总闸”

Zephyr的多核支持,说白了就是靠Kconfig这把“钥匙”来开启的。你想想看,一个RTOS要支持多核,得知道你要跑SMP还是AMP,每个核要跑什么,内存怎么划分。这些信息,全得在编译前就定好。

核心配置项一览:

  • CONFIG_SMP:对称多处理,所有核共享内存、跑同一个镜像。这是最常用的模式。
  • CONFIG_AMP:非对称多处理,每个核可以跑不同的镜像,甚至不同的OS。这个配置在Zephyr里是实验性的,但我个人建议,除非你有特别硬的需求,否则先别碰。
  • CONFIG_MP_NUM_CPUS:告诉系统你有几个核。比如双核就设2,四核就设4。
  • CONFIG_SCHED_CPU_MASK:开启CPU亲和性调度,允许你把任务绑定到特定核上。

我在项目中遇到过一个问题:有个同事把CONFIG_SMP打开了,但忘了设CONFIG_MP_NUM_CPUS,结果系统只认到了一个核。嗯,这种低级错误,排查起来还挺费时间的。所以我的习惯是,每次改完Kconfig,都会用west build -t menuconfig看一眼最终配置,确认所有依赖项都自动勾上了。

这里有个小技巧:Zephyr的Kconfig体系是分层的。你可以在soc/board/app/三个层级分别覆盖配置。我个人习惯把多核相关的配置放在board/层,因为不同板子的核数、内存布局都不一样,这样移植起来更清晰。

3.2 多核启动流程:从复位到多核齐飞

多核启动,是Zephyr里最考验功底的地方。你想想看,一个核启动还好说,多个核同时启动,谁先跑?谁后跑?怎么同步?

Zephyr的启动流程,大致分这么几步:

  1. 主核(Primary Core)先跑:系统复位后,只有主核(通常是CPU0)开始执行。它负责初始化硬件、设置页表、初始化中断控制器。
  2. 从核(Secondary Core)被“按兵不动”:从核在复位后,会进入一个自旋等待状态。它们要么在WFE(Wait For Event)里睡着,要么在轮询一个特定的内存地址。
  3. 主核唤醒从核:主核初始化完必要的外设后,会调用arch_start_cpu()函数。这个函数会写一个特定的寄存器(比如ARM的GIC或RVBAR),或者写一个内存地址,告诉从核:“嘿,你可以开始跑了,入口地址在这里!”
  4. 从核跳转执行:从核被唤醒后,会跳转到主核指定的入口地址。这个入口通常是z_mp_entry()z_arm64_secondary_start()。从核会在这里完成自己的初始化,比如设置栈指针、初始化本地中断控制器,然后进入调度器。

我曾经踩过的坑:在某个ARM Cortex-A53的平台上,从核启动后一直跑飞。查了半天,发现是主核写RVBAR寄存器时,地址没对齐。ARM要求RVBAR必须是4字节对齐的,我写了个未对齐的地址,从核直接跳到了非法指令。从那以后,我每次写启动代码,都会用ALIGN(4)宏强制对齐。

启动完成后,所有核都进入了调度器。这时候,它们会一起参与任务调度。但这里有个关键点:每个核都有自己的调度器实例。也就是说,每个核维护自己的就绪队列、自己的当前任务。它们通过一个全局的锁(z_sched_lock)来同步对共享数据结构的访问。

3.3 CPU掩码(cpu_mask)机制:精准控制任务去哪儿跑

CPU掩码,说白了就是一个位图。每一位代表一个CPU核。比如一个四核系统,cpu_mask = 0b0101就表示任务只能在CPU0和CPU2上运行。

这个机制在Zephyr里无处不在。你创建线程时可以指定掩码,运行时也可以动态修改。我给大家看个例子:

// 创建一个线程,只允许在CPU0和CPU1上运行
k_tid_t my_tid = k_thread_create(&my_thread_data, my_stack,
                                  STACK_SIZE, my_entry,
                                  NULL, NULL, NULL,
                                  K_USER | K_INHERIT_PERMS,
                                  K_ANY, K_NO_WAIT);

// 设置CPU亲和性掩码:只允许CPU0和CPU1
k_thread_cpu_mask_enable(my_tid, 0);
k_thread_cpu_mask_enable(my_tid, 1);
// 或者用位操作
k_thread_cpu_mask_set(my_tid, BIT(0) | BIT(1));

你可能会问:“为什么要搞这么复杂?让调度器自己分配不就行了?”

嗯,这里有个实际场景。我在做一个音频处理项目时,有个实时性要求极高的任务,必须独占一个核,不能被其他任务打断。这时候,我就用cpu_mask把它绑定到CPU1上,然后把其他所有任务都限制在CPU0上。这样,CPU1就变成了一个“专用处理器”,延迟稳稳的。

注意事项:

  • 掩码是“或”的关系。你调用k_thread_cpu_mask_enable(tid, 0)再调用k_thread_cpu_mask_enable(tid, 1),结果是任务可以在CPU0和CPU1上运行。如果你想重置,得用k_thread_cpu_mask_clear()
  • 如果所有核都被禁用了,调度器会报错。我见过有人写代码时忘了给某个任务设置掩码,结果任务永远得不到调度。
  • 中断处理函数不受cpu_mask限制。中断可以在任何核上触发,除非你显式设置了中断亲和性。

另外,Zephyr还提供了一个全局的cpu_mask,叫z_cpu_active_mask。它表示当前哪些核是“活跃”的。比如你在运行时热插拔了一个核,这个掩码就会更新。调度器在分配任务时,会优先考虑活跃的核。

我个人习惯,在系统启动后,会打印一下当前的cpu_mask,确认所有核都正确识别了:

// 打印当前活跃的CPU核
for (int i = 0; i < CONFIG_MP_NUM_CPUS; i++) {
    if (arch_is_cpu_active(i)) {
        printk("CPU%d is active\n", i);
    }
}

好了,这一节的内容就到这里。Kconfig配置是基础,启动流程是骨架,cpu_mask是灵魂。这三样东西搞明白了,Zephyr的多核支持你就掌握了七七八八。下一节,我们会深入SMP调度器的实现,看看任务是怎么在多个核之间“飞来飞去”的。