本文简要描述zephyr在cortex-m下如何启动第一个thread和进行上下文切换。
原本没有打算分析zephyr的OS内核,但上周笔记本坏了,只能在台式机上看代码,因此大致看了一下上下文切换和调度的内容。现在买了新笔记本为了不辜负几个小时的读码时间,把看代码的心得也写了下来,果然是写文档的时间远远超过理解代码的时间。
本文基于nrf52832(arm cortex-m4)进行分析。上下文切换和芯片架构强相关,不同的芯片架构,寄存器,堆栈方式,中断使用都不一样,因此本文仅仅对cortex-m系列有较大参考价值。
启动第一个thread
在Zephyr如何运行到main一文中已经有提到zephyr的main函数是在main thread中被调用的,这里再详细分析zephyr如何切到main thread执行的。
编译配置项
相关代码基于下面定义进行分析
1 | #define CONFIG_MULTITHREADING 1 |
流程
kernel/init.c 的_Cstart函数主要完成thread环境准备,并且到第一个thread执行
1 | static struct k_thread _main_thread_s; |
创建Main thread
通过_setup_new_thread将bg_thread_main作为thread进行创建
1 | static void prepare_multithreading(struct k_thread *dummy_thread) |
_setup_new_thread调用_new_thread创建main thread,主要就是对thread堆栈进行初始化,为上下文切换做准备
1 | void _new_thread(struct k_thread *thread, k_thread_stack_t *stack, |
经过上述步骤thread&stack如下:
说明: 对于Cortex-M内核,中断发生时硬件会自动保存r0~r3,ip,lr,pc,xpsr因此_caller_saved不使用也无需初始化。一般情況下对于刚创建的thread来说这些值需要软件到堆栈也就是struct __esf,当Swap第一次调度时会从这里pop出开始执行。但对于一些架构(本文分析的cortex-m)无法直接通过swap进行第一个thread首次切换,就需要配置CONFIG_ARCH_HAS_CUSTOM_SWAP_TO_MAIN,进行第一个thread首次切换。
切到Main thread运行
switch_to_main_thread->_arch_switch_to_main_thread
配置有CONFIG_ARCH_HAS_CUSTOM_SWAP_TO_MAIN的首次切换如下,可以看到是直接将堆栈地址放到PSP,然后bx跳到_thread_entry执行
1 | static ALWAYS_INLINE void |
上下文切换
第一个thread启动后,又可以继续创建其它thread,然后通过上下文切换让其它thread被调度运行
上下文切换方式
上下文切换方式有两种
- thread中主动让CPU PendSV,发生调度
- IRQ退出时,重新调度
下面分别说明
PendSV
PendSV上下文切换大体也分两类:
- 所有内核组件启动,等待,释放时会发生上下文切换,可以搜索_reschedule查看具体在那些地方。
- Thread被sleep,yield,suspend,abort时会发生上下文切换。可搜索_Swap查看具体在那些地方。
_reschedule其实也是调用_Swap来完成, 最终是通过写ICSR进入PendSV来完成上下文切换
1 | static inline int _Swap(unsigned int key) |
__swap触发pendsv后进入pendsv执行代码在arch/arm/core/swap_helper.S,简化如下
1 | SECTION_FUNC(TEXT, __pendsv) |
以上流程如下图:
- 中断pendsv发生,硬件入栈r0~r3,ip,lr,pc,xpsr
- 软件保存r4~r11,psp到当前thread的callee_saved
- 改变当前thread指针指向将要执行的thread
- 软件从将要执行的thread弹出callee_saved到r4~r11,psp,堆栈指针指向将要执行的thread
- 退出中断pendsv,硬件自动从将要执行的thread stack中弹出r0~r3,ip,lr,pc
IRQ
在IRQ响应时执行_isr_wrapper,退出时呼叫_IntExit,该函数会进行上下文切换arch/arm/core/exc_exit.S
如果配置了CONFIG_TIMESLICING,会在每个时间片进行调度检查
1 | SECTION_SUBSEC_FUNC(TEXT, _HandlerModeExit, _IntExit) |
调度简述
本文不做调度分析,就目前用到的调度进行简单说明
优先级调度
zephyr os优先级调度支持下面三种调度方式
- CONFIG_SCHED_DUMB: 使用thread比较少的情况,不使用红黑树,可以节约2k的code size
- CONFIG_SCHED_SCALABLE:适用于thread比较多(>20),将适用红黑树
- CONFIG_SCHED_MULTIQ:1.12版本前默认的调度方式
调度算法主要体现在:
_priq_run_add/_priq_run_remove/_priq_run_best
时间片
相同优先级之间如果不支持时间片,就需要主动释放资源才能让其它thread运行,配置为启动时间片,在nrf52832下是使用的rtc1做为时间片定时中断
初始化中断
drivers/timer/sys_clock_init.c
1 | SYS_DEVICE_DEFINE("sys_clock", z_clock_driver_init, z_clock_device_ctrl, |
drivers/timer/nrf_rtc_timer.c
1 | int z_clock_driver_init(struct device *device) |
NVIC_ClearPendingIRQ(NRF5_IRQ_RTC1_IRQn);
IRQ_CONNECT(NRF5_IRQ_RTC1_IRQn, 1, rtc1_nrf5_isr, 0, 0);
irq_enable(NRF5_IRQ_RTC1_IRQn);
1
}
中断向量表
软件中断向量表,会在IRQ发生时被IRQ中断软件查询使用
tests/kernel/arm_irq_vector_table/src/arm_irq_vector_table.c
1 | void rtc1_nrf5_isr(void); |
上面的_irq_vector_table会被arch/common/gen_isr_tables.py转换为_sw_isr_table
中断函数
arch/arm/core/isr_wrapper.S 响应中断访问软件中断向量表
1 | SECTION_FUNC(TEXT, _isr_wrapper) |
当发生RTC中断时会查询出rtc1_nrf5_isr进行执行,按照下面顺序调用
rtc1_nrf5_isr->rtc_announce_set_next->z_clock_announce->z_time_slice
在z_time_slice进行时间片的计算,并出下一个要执行的thread
1 | void z_time_slice(int ticks) |
当时间片用完后还是通过_priq_run_remove/_priq_run_remove来计算出将要调度的thread
1 | void _move_thread_to_end_of_prio_q(struct k_thread *thread) |
最后IRQ退出,使用_IntExit来进行上下文切换,切换到新调度的thread中运行。
参考
https://docs.zephyrproject.org/latest/kernel/threads/scheduling.html