第三章:Zephyr内核基础——线程、调度器、时间管理、内存管理核心概念
好,咱们今天聊聊Zephyr内核的四大基石。线程、调度器、时间管理、内存管理。这四个东西,说白了就是操作系统的“四梁八柱”。你把这四个搞明白了,Zephyr在你手里就不再是个黑盒子。
我个人习惯,学内核先看线程。为什么?因为线程是系统里最活跃的实体,调度器围着它转,时间管理为它计时,内存管理给它喂饭。线程搞不懂,后面全白搭。
3.1 线程:系统里的“活人”
线程是什么?你可以把它想象成一个“执行流”。它有自己的栈、自己的优先级、自己的状态。Zephyr里线程分两种:任务线程和协程。任务线程是抢占式的,协程是非抢占的。我建议你刚开始只用任务线程,协程那玩意儿坑多,后面再碰。
创建线程很简单,用K_THREAD_DEFINE宏。看个例子:
#define MY_STACK_SIZE 1024
#define MY_PRIORITY 5
K_THREAD_DEFINE(my_tid, MY_STACK_SIZE,
my_thread_entry, NULL, NULL, NULL,
MY_PRIORITY, 0, 0);
void my_thread_entry(void *arg1, void *arg2, void *arg3)
{
while (1) {
printk("Hello from my thread!\n");
k_sleep(K_SECONDS(1));
}
}
注意看,K_THREAD_DEFINE第一个参数是线程ID,后面是栈大小、入口函数、参数、优先级。优先级数字越小,优先级越高。0是最高,一般留给系统用。我建议你应用线程优先级设在5到10之间,别跟系统抢。
核心要点:线程栈大小要算准。我见过太多人栈溢出导致系统随机崩溃。1024字节是起步,如果你有局部数组或递归调用,至少给2048。
避坑指南:我曾经在一个项目中,线程栈给了512字节,结果跑着跑着就死机。查了两天才发现是栈溢出,把相邻的全局变量给踩了。从那以后,我每个线程栈至少给1024,关键线程给2048。
3.2 调度器:谁先跑,谁后跑
调度器就是那个决定“下一个该谁跑”的家伙。Zephyr用的是基于优先级的抢占式调度。什么意思?高优先级的线程随时可以打断低优先级的线程。同优先级的线程,按时间片轮转。
调度器的工作流程其实很简单:
- 当前线程主动让出CPU(比如调用
k_yield或k_sleep) - 或者被更高优先级的线程抢占
- 调度器从就绪队列里挑一个优先级最高的线程
- 切换上下文,让那个线程跑
你想想看,如果两个线程优先级一样,调度器怎么处理?它会给他们每人分一个时间片,默认是50毫秒。时间片到了,就换下一个。这叫轮转调度。
个人经验:我建议你尽量少用k_yield。这玩意儿会让当前线程让出CPU,但如果就绪队列里没有同优先级的线程,它马上又会被调度回来,等于白让。用k_sleep(0)效果一样,但更清晰。
调度器还有一个重要概念:中断上下文。中断处理程序里不能调用会阻塞的API,比如k_sem_take。为什么?因为中断没有线程上下文,调度器没法切换。我曾经在中断里调了k_sem_take,结果系统直接挂掉。嗯,这个坑我替你们踩过了。
3.3 时间管理:别让线程“饿死”
时间管理是Zephyr的“心跳”。系统靠一个硬件定时器产生周期性中断,每次中断叫一个tick。默认tick频率是100Hz,也就是10毫秒一次。你可以通过CONFIG_SYS_CLOCK_TICKS_PER_SEC配置。
常用的时间API就这几个:
| API | 功能 | 注意事项 |
|---|---|---|
k_sleep() |
让当前线程休眠指定时间 | 单位是毫秒,最小精度是1个tick |
k_usleep() |
微秒级休眠 | 实际精度受tick限制,别指望太准 |
k_uptime_get() |
获取系统启动后的毫秒数 | 64位,不用担心溢出 |
k_cycle_get_32() |
获取CPU周期数 | 用于精确计时,但注意32位会溢出 |
这里有个细节:k_sleep(1)真的只睡1毫秒吗?不一定。如果tick是10毫秒,那k_sleep(1)实际会睡10毫秒。为什么?因为调度器只在tick中断时才检查有没有线程该醒了。所以,时间精度受tick频率限制。
核心要点:如果你需要高精度延时,比如1毫秒以内,别用k_sleep。用k_busy_wait(),它靠CPU空转来实现微秒级延时。代价是CPU被占满,其他线程跑不了。
3.4 内存管理:别让系统“饿死”
内存管理是嵌入式系统的老大难。Zephyr提供了两种方式:静态分配和动态分配。我个人的建议是:能用静态就别用动态。为什么?动态分配有碎片问题,而且分配失败时处理起来很麻烦。
静态分配就是编译时就定好大小。比如线程栈、信号量、消息队列,都用宏定义好。Zephyr的K_HEAP_DEFINE可以定义一个堆,但我不建议在中断里用它。
动态分配用k_malloc和k_free,跟标准C的malloc类似。但注意,Zephyr的动态内存分配器默认是不线程安全的。如果你在多线程里同时调k_malloc,得加锁。
/* 静态分配示例 */
K_SEM_DEFINE(my_sem, 0, 1); // 信号量,初始0,最大值1
/* 动态分配示例 */
void *ptr = k_malloc(256);
if (ptr != NULL) {
memset(ptr, 0, 256);
/* 使用内存 */
k_free(ptr);
} else {
printk("内存分配失败!\n");
}
避坑指南:我曾经在一个项目中,用k_malloc分配了一个大缓冲区,结果系统跑了两天后突然死机。查了半天,发现是内存碎片导致分配失败。从那以后,我所有关键路径都用静态分配,只在初始化时用一次动态分配。
还有一个概念叫内存池。Zephyr的k_mem_pool可以预分配固定大小的内存块,分配和释放速度很快,而且没有碎片。我建议你在需要频繁分配释放小内存的场景下用内存池。
3.5 四个概念如何协同工作
好了,四个概念都讲完了。它们怎么配合?我给你画个场景:
- 一个高优先级线程A在跑,突然来了个中断
- 中断处理程序里发了个信号量,唤醒了一个中等优先级的线程B
- 中断退出后,调度器发现线程B优先级比A高,于是抢占A,让B跑
- 线程B跑了一会儿,调了
k_sleep(100),进入休眠 - 调度器又把A调度回来继续跑
你看,线程、调度器、时间管理、内存管理,四个东西环环相扣。线程是演员,调度器是导演,时间管理是时钟,内存管理是道具。缺一个,戏就演不下去。
个人经验:我刚开始学Zephyr时,总想把所有功能都用上。后来发现,保持简单才是王道。一个系统里线程别超过10个,优先级别超过3级,内存全静态分配。这样出问题的概率小很多。
嗯,这一章就到这儿。下一章咱们聊中断管理,那才是Zephyr的看家本领。记住,线程是基础,调度器是核心,时间管理是节奏,内存管理是保障。把这四个搞透,你就能在Zephyr的世界里横着走了。