自学内容网 自学内容网

揭秘RT-Thread线程调度:CPU是如何优雅“分身“的?

揭秘RT-Thread线程调度:CPU是如何优雅"分身"的?

你有没有想过:一个CPU明明只有一个核心,却能同时"干好几件事"——一会儿处理传感器数据,一会儿响应按键输入,还能兼顾串口通信。这背后的秘诀,就藏在RT-Thread的线程调度机制里。今天咱们就扒开这层神秘面纱,看看CPU是如何像"超级打工人"一样,有条不紊地切换任务的。

先搞懂:线程调度到底是啥?

想象一下:你是公司唯一的员工(CPU),老板(RTOS)给你安排了一堆活(线程)——写报告、接电话、复印文件。你不可能同时做三件事,但可以快速切换:写两行报告,接个电话,复印完再回来写报告。只要切换够快,外人看起来就像你"同时"在做所有事。

RT-Thread的线程调度,本质上就是帮CPU做这个"快速切换"的管家。它负责决定:现在该让哪个线程干活?怎么保存上一个线程的工作状态?怎么让下一个线程无缝接手?而咱们今天要聊的,就是RT-Thread启动后第一个线程的调度全过程——相当于"员工第一天上班,老板怎么教他开始干活"。

核心流程:从"开机"到"第一个线程启动"

RT-Thread启动第一个线程的过程,就像一场精心设计的"接力赛",每个函数各司其职,环环相扣。咱们一步一步看这场"比赛"的关键棒次。

第一棒:rtthread_startup——系统启动的"发令枪"

// 伪代码示意:系统启动入口
void rtthread_startup() {
    // 1. 板级初始化(硬件准备,比如时钟、引脚)
    rt_hw_board_init();
    // 2. 调度器初始化(准备好"任务安排表")
    rt_system_scheduler_init();
    // 3. 创建第一个线程(比如main线程)
    rt_thread_create(...)
    // 4. 启动调度器(喊出"各就各位,开始!")
    rt_system_scheduler_start();
}

这一步就像公司开业:先装修办公室(硬件初始化),制定工作制度(调度器初始化),招第一个员工(创建线程),最后宣布"开始营业"(启动调度器)。

第二棒:rt_system_scheduler_start——挑选"第一个干活的人"

调度器启动后,第一件事是找"优先级最高的线程"——就像老板第一天上班,先挑最紧急的活让你干。

void rt_system_scheduler_start() {
    // 找到就绪队列里优先级最高的线程
    struct rt_thread *to_thread = _scheduler_get_highest_priority_thread();
    // 标记这个线程为"运行中"
    rt_cpu_self()->current_thread = to_thread;
    // 关键:切换到这个线程
    rt_hw_context_switch_to((rt_uintptr_t)&to_thread->sp);
}

这里的to_thread就是"第一个幸运儿"(通常是main线程)。而最后一行的rt_hw_context_switch_to,就是"交接工作"的关键——它要把CPU的"工作状态"完全交给新线程。

第三棒:rt_hw_context_switch_to——"交接工作"的准备

这一步是用汇编实现的"细致活",相当于"老员工给新员工写交接单"。主要干三件事:

1. 记录新线程的"工作记录本"(堆栈指针)

每个线程都有自己的"工作记录本"(堆栈),里面记着它上次干活时的状态(寄存器值、执行位置等)。rt_hw_context_switch_to首先把新线程的堆栈指针地址记下来:

; 汇编代码:保存新线程的堆栈指针地址
LDR r1, =rt_interrupt_to_thread  ; 找到"新线程地址本"
STR r0, [r1]                     ; 把新线程的堆栈指针地址写进去(r0是传入的&to_thread->sp)

你可以理解为:老板在白板上写下"新员工的笔记本放在抽屉A",方便后面查找。

2. 标记"这是第一次切换"

第一次切换比较特殊——之前没有正在运行的线程,所以不需要保存上一个线程的状态。这里会把"来源线程"设为0,相当于告诉系统:“这次是第一次干活,不用交接上一个人的活”:

; 汇编代码:标记来源线程为0
LDR r1, =rt_interrupt_from_thread  ; 找到"来源线程地址本"
MOV r0, #0x0                       ; 来源线程设为0
STR r0, [r1]                       ; 写进地址本
3. 设个"低优先级的换班信号"(PendSV异常)

线程切换不能太"霸道"——如果正在处理紧急任务(比如中断),必须等紧急任务完成才能换班。RT-Thread用PendSV异常做切换,还特意把它的优先级设为最低:

; 汇编代码:设置PendSV优先级为最低
LDR r0, =NVIC_SYSPRI2              ; 找到"优先级配置寄存器"
LDR r1, =NVIC_PENDSV_PRI           ; 最低优先级值(0xFFFF0000)
STR r1, [r0]                       ; 写进寄存器

这就像规定"换班只能在所有紧急工作结束后进行",确保不会打断重要操作。

4. 触发切换:“该新线程上场了!”

最后一步是手动触发PendSV异常,相当于老板喊一声:“新员工,该你干活了!”

; 汇编代码:触发PendSV异常
LDR r0, =NVIC_INT_CTRL             ; 找到"中断控制寄存器"
LDR r1, =NVIC_PENDSVSET            ; 触发PendSV的信号
STR r1, [r0]                       ; 发送信号

第四棒:PendSV_Handler——真正的"换班执行"

PendSV异常被触发后,CPU会暂停当前操作,进入PendSV_Handler函数——这才是"换班"的实际操作,相当于新老员工当面交接。

1. 先"锁门":确保切换不被打扰
; 汇编代码:禁用中断,防止切换被打断
MRS r2, PRIMASK  ; 保存当前中断状态(相当于"记下现在门是开还是关")
CPSID I          ; 锁门:禁用所有可屏蔽中断

就像交接工作时,先关上门不让外人打扰,确保资料不会拿错。

2. 检查"是否需要保存上一个线程的状态"

因为这是第一次切换,rt_interrupt_from_thread的值是0,所以不需要保存任何状态,直接跳去加载新线程:

; 汇编代码:检查来源线程是否为0
LDR r0, =rt_interrupt_from_thread
LDR r1, [r0]
CBZ r1, switch_to_thread  ; 如果r1=0,直接跳去加载新线程

翻译成人话:“老板说之前没人干活,不用交接,直接让新员工开始”。

3. 加载新线程的"工作状态"

终于到了最关键的一步:把新线程的"工作记录本"(堆栈)里的内容恢复到CPU寄存器中。

; 汇编代码:从堆栈恢复寄存器
LDR r1, =rt_interrupt_to_thread  ; 找到"新线程地址本"
LDR r1, [r1]                     ; 取出新线程的堆栈指针地址
LDR r1, [r1]                     ; 取出实际的堆栈指针(相当于拿到"笔记本")

; 恢复通用寄存器(r4-r11)
LDMFD r1!, {r4 - r11}            ; 从堆栈弹出数据到寄存器

; 如果有FPU(浮点单元),还要恢复浮点寄存器
IF {FPU} != "SoftVFP"
VLDMFDNE r1!, {d8 - d15}         ; 恢复浮点寄存器
ENDIF

; 更新堆栈指针到线程专用堆栈(PSP)
MSR psp, r1

这就像新员工打开"笔记本",把之前写到一半的内容、用到的工具(寄存器)都摆出来,瞬间回到上次停下的状态。

4. 开门放行:新线程正式开始工作

最后一步是恢复中断,跳转到新线程的入口函数:

; 汇编代码:恢复中断,开始执行新线程
MSR PRIMASK, r2  ; 恢复之前的中断状态(开门)
ORR lr, lr, #0x04 ; 告诉CPU:用线程堆栈(PSP)运行
BX lr            ; 跳转到新线程的入口地址

此时,CPU的程序计数器(PC)会指向新线程的入口函数(比如main_thread_entry),就像新员工翻开笔记本第一页,正式开始干活。

实际效果:第一个线程跑起来了!

当上述步骤全部完成后,CPU会乖乖执行新线程的代码。比如main线程的入口函数会被调用,接着初始化组件、进入用户的main函数——至此,RT-Thread的第一个线程调度完美收官。

你可以理解为:CPU从"系统初始化"这个"准备阶段",正式切换到"处理业务任务"的"工作阶段",整个过程流畅得像无缝衔接的魔术。

总结:线程调度的核心智慧

RT-Thread的线程调度看似复杂,本质上就干了三件事:

  1. 选谁干:通过调度器找到优先级最高的就绪线程;
  2. 怎么交:用PendSV异常在合适的时机(不打扰紧急任务)进行切换;
  3. 状态存哪:用堆栈保存/恢复线程的寄存器状态,确保切换无缝。

正是这套机制,让单个CPU能"同时"处理多个任务,让嵌入式设备反应灵敏又有条不紊。下次当你看到单片机一边采集数据一边响应按键时,就知道——它不是真的会分身,只是切换得足够聪明而已~


原文地址:https://blog.csdn.net/u010665511/article/details/149707840

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!