RT-Thread SMP相关问题分析
问题分析:
https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/smp/smp?id=os-tick
OS Tick
在 SMP 系统中,每个 CPU 维护自己独立的 tick 值,用作任务运行计时以及时间片统计。除此之外,CPU0 还通过 tick 计数来更新系统时间,并提供系统定时器的功能,次级 CPU 不需要提供这些功能。
SMP 系统中 CPU0 和次级 CPU 的 OS Tick 管理差异分析
在 RT-Thread SMP (对称多处理) 系统中,尽管每个 CPU 都维护自己独立的 tick 值用于任务运行计时和时间片统计,但只有 CPU0 负责更新系统时间和提供系统定时器功能,次级 CPU 不需要提供这些功能。这种设计有其深层的技术考量。
次级 CPU 不提供系统时间更新和系统定时器功能的原因
1. 避免系统时间不一致性
时间同步问题:
- 如果多个 CPU 同时更新系统时间,会导致时间跳变或回退
- 不同 CPU 的硬件计时器可能存在微小差异,长时间运行会导致偏差
- 由单一 CPU (CPU0) 负责时间维护确保了系统时间的单调递增性
2. 避免资源竞争和提高效率
锁竞争减少:
- 系统时间是全局共享资源,多 CPU 访问需要加锁保护
- 只由 CPU0 负责更新可以避免频繁的锁操作和竞争
- 减少了跨 CPU 的缓存同步开销,提高系统效率
3. 简化系统设计与实现
架构简洁性:
- 集中式时间管理模型设计更简单清晰
- 降低了系统实现的复杂度
- 减少了潜在的时序错误和竞态条件
4. 系统定时器任务的统一调度
定时任务处理:
- 系统定时器通常用于周期性任务调度
- 单一 CPU 处理定时事件避免了重复触发和执行
- 确保定时任务只执行一次,避免重复执行带来的资源浪费
5. 中断处理效率
中断处理优化:
- 时钟中断是高频触发的中断源
- 仅在 CPU0 处理系统级时钟中断可减少整体中断负载
- 次级 CPU 可以专注于处理其他任务和中断
RT-Thread SMP 中的实际实现
在 RT-Thread SMP 实现中,所有 CPU 都有各自的 tick 中断和计数器,用于:
- 记录当前 CPU 上运行的任务的时间片使用情况
- 在需要时触发任务切换
- 维护本 CPU 的调度和时间相关统计
但是只有 CPU0 会:
- 调用
rt_tick_increase()
更新全局系统时钟 - 检查并触发系统定时器列表中到期的定时器
- 执行系统级的周期性任务
这种设计在保证每个 CPU 都能独立进行任务调度的同时,又确保了系统时间和定时器的一致性和准确性。
结论
次级 CPU 不提供系统时间更新和系统定时器功能,是 RT-Thread SMP 系统设计中为了提高性能、简化实现、确保时间一致性而采取的策略。这样的设计既保证了系统正确性,又提高了执行效率,是 SMP 操作系统设计中常见且合理的架构选择。
问题分析:
https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/smp/smp?id=%e5%ae%9e%e7%8e%b0%e6%ac%a1%e7%ba%a7-cpu-%e7%9a%84%e5%90%af%e5%8a%a8%e4%bb%a3%e7%a0%81
按照上面次级 CPU 启动过程的描述,在将 RT-Thread 移植到其它 ARMv7-A SMP 芯片的过程中,内核开发者需要提供以下三个函数:
rt_hw_secondary_cpu_up(), 该函数设置次级 CPU 的启动入口地址为 secondary_cpu_start ,加电启动其它 CPU 核心;
secondary_cpu_c_start(), 该函数用来初始化单个次级 CPU ,主要包括初始化中断控制器接口,设置中断向量表,以及当前 CPU 的 tick 中断。最后获取内核自旋锁 _cpus_lock ,并调用函数 rt_system_scheduler_start() 开启当前 CPU 的任务调度器;
rt_hw_secondary_cpu_idle_exec(), 该函数被次级 CPU 的 idle 线程循环调用,可用来做功耗相关的处理。
次级 CPU 启动代码中获取内核自旋锁 _cpus_lock 的目的分析
在 SMP (对称多处理器) 系统中,次级 CPU 启动过程中的 secondary_cpu_c_start()
函数最后需要获取内核自旋锁 _cpus_lock
,这是 RT-Thread SMP 系统设计中的一个关键环节。下面详细分析这么做的目的和重要性。
_cpus_lock 自旋锁的作用
_cpus_lock
是 RT-Thread SMP 内核中一个核心的自旋锁,它在系统启动和 CPU 同步过程中扮演着关键角色。在次级 CPU 启动过程中获取该锁的主要目的包括:
1. 确保 CPU 初始化的顺序性和同步性
在 SMP 系统启动过程中,CPU0 (主 CPU) 首先初始化系统的基础设施,包括内存管理、线程调度器等核心组件。次级 CPU 必须等待这些初始化完成后才能开始自己的调度。_cpus_lock
锁保证了这种顺序性:
- CPU0 首先持有
_cpus_lock
锁 - 完成系统初始化后释放该锁
- 次级 CPU 必须获取该锁才能继续执行,确保它们在系统完全就绪后才开始任务调度
2. 防止次级 CPU 过早参与系统调度
如果次级 CPU 在系统核心组件初始化完成之前就开始任务调度,可能会导致以下问题:
- 访问未初始化完成的数据结构
- 与 CPU0 的初始化过程产生竞争条件
- 导致系统状态不一致
通过 _cpus_lock
机制,次级 CPU 会在自旋等待直到 CPU0 完成必要的初始化工作并释放锁,从而确保系统状态的一致性。
3. 作为启动信号机制 (Barrier)
_cpus_lock
实际上充当了一个启动信号或同步屏障 (barrier):
- CPU0 释放锁相当于发出"系统已准备就绪"的信号
- 次级 CPU 等待并获取锁相当于接收到该信号,表示"可以开始任务调度"
总结
次级 CPU 在启动过程中获取内核自旋锁 _cpus_lock
的主要目的是:
- 同步启动顺序:确保 CPU0 完成系统核心组件的初始化后,次级 CPU 才开始任务调度
- 保证系统状态一致性:防止次级 CPU 在系统未完全初始化时访问核心数据结构
- 作为启动屏障:提供一个清晰的同步点,表示"系统已准备就绪,次级 CPU 可以开始工作"
处理器间中断(Inter-Processor Interrupt,IPI):https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/smp/smp?id=%e5%a4%84%e7%90%86%e5%99%a8%e9%97%b4%e4%b8%ad%e6%96%ad-ipi
RT-Thread 处理器间中断(IPI)机制分析
处理器间中断(Inter-Processor Interrupt,IPI)是多核系统中一个核心通知其他核心发生特定事件的机制。在RT-Thread操作系统中,当某个CPU上运行的任务改变了系统状态或触发了需要其他CPU感知的事件时,就会使用IPI机制通知其他CPU。
IPI实现逻辑
1. 硬件层实现
从代码中可以看到,RT-Thread的IPI机制基于RISC-V架构的CLINT (Core Local Interruptor)硬件实现:
void clint_ipi_send(uint64_t id)
{CLINT_Type *clint = (CLINT_Type *)CORET_BASE;#if defined(CONFIG_RISCV_SMODE) && CONFIG_RISCV_SMODE// S模式下使用SSIP寄存器switch (id){case C907_CORE0:clint->SSIP0 |= (uint32_t)0x1;break;// 其他核心类似...}
#else// M模式下使用MSIP寄存器switch (id){case C907_CORE0:clint->MSIP0 |= (uint32_t)0x1;break;// 其他核心类似...}
#endif
}
这个函数通过设置CLINT中对应核心的软件中断挂起位(MSIP/SSIP)来触发目标核心的软件中断。
2. 软件层封装
RT-Thread进一步封装了硬件IPI机制,提供了更高级的API:
void rt_hw_ipi_send(int ipi_vector, unsigned int cpu_mask)
{int idx;for (idx = 0; idx < RT_CPUS_NR; idx++){if (cpu_mask & (1 << idx)){clint_ipi_send(idx);}}
}
这个函数通过位掩码cpu_mask
指定要发送IPI的目标CPU,对每个设置了相应位的CPU调用clint_ipi_send
。参数ipi_vector
用于指定IPI的类型,不同类型的IPI会触发不同的处理函数。
IPI使用逻辑
从RT-Thread内核代码中,我们可以看到IPI在调度器中的典型使用场景:
1. 在线程调度中的应用
在_sched_insert_thread_locked
函数中,当一个线程被插入就绪队列时,需要通知可能的目标CPU进行调度:
if (bind_cpu == RT_CPUS_NR) // 线程没有绑定特定CPU
{// 将线程添加到全局就绪队列// ...// 通知当前CPU以外的所有CPU进行调度cpu_mask = RT_CPU_MASK ^ (1 << cpu_id);rt_hw_ipi_send(RT_SCHEDULE_IPI, cpu_mask);
}
else // 线程绑定到特定CPU
{// 将线程添加到绑定CPU的就绪队列// ...// 如果当前CPU不是绑定的CPU,则通知绑定的CPU进行调度if (cpu_id != bind_cpu){cpu_mask = 1 << bind_cpu;rt_hw_ipi_send(RT_SCHEDULE_IPI, cpu_mask);}
}
2. IPI的使用模式
从代码分析,RT-Thread中IPI的使用模式主要包括:
-
调度触发:当线程状态变化时(如从阻塞到就绪),需要通知可能运行该线程的CPU进行重新调度。
- 使用
RT_SCHEDULE_IPI
类型的IPI。
- 使用
-
CPU间同步:当多核共享资源发生变化时,需要通知其他CPU更新其缓存或状态。
-
负载均衡:在负载均衡场景下,可能需要触发其他CPU重新分配任务。
3. IPI处理流程
当CPU接收到IPI后:
- 硬件触发软件中断
- 软件中断处理程序根据IPI类型执行相应的处理函数
- 对于
RT_SCHEDULE_IPI
类型,会调用rt_schedule
进行任务调度重新评估
总结
RT-Thread的IPI机制提供了多核系统中CPU间通信的重要手段:
- 基于硬件CLINT实现低层次的处理器间中断
- 提供软件层次的封装,使IPI使用更加灵活
- 在任务调度、共享资源管理等场景中,使用IPI保证多核系统的状态一致性和及时响应
这种机制确保了当一个CPU上的任务改变了系统状态时,其他相关CPU能够及时得到通知并作出相应处理,是多核系统正常运行的关键。