3. 内核基础概念:线程、调度器、中断、时间管理、同步机制概述
好,咱们进入正题。这一章我打算把Zephyr内核的几个核心概念串起来讲一遍。你可能会觉得这些东西太基础,但说实话,我在带新人时发现,很多人就是栽在这些“基础”上。线程怎么切?中断来了谁管?时间片怎么分?同步搞不好就死锁——这些坑我都踩过。
咱们一个一个来。
3.1 线程——系统里真正干活的人
线程,说白了就是一段独立执行的代码。在Zephyr里,线程是调度的基本单位。每个线程有自己的栈、优先级、状态。
我习惯把线程想象成一个“工人”。每个工人有自己的工具箱(栈),有自己的任务优先级。系统调度器就是工头,决定哪个工人上工。
创建线程的代码很简单:
#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("线程在跑\n");
k_sleep(K_MSEC(1000));
}
}
嗯,这里要注意:K_THREAD_DEFINE 这个宏会在编译时就分配好栈空间。我在项目中遇到过有人用动态分配栈,结果堆碎片化导致系统跑几天就挂了。所以,嵌入式里我建议能用静态就用静态。
线程状态机:就绪→运行→挂起/阻塞→就绪。就这么简单。但很多人搞混了“挂起”和“阻塞”。挂起是你主动叫停,阻塞是等资源。
3.2 调度器——谁先跑,谁后跑
调度器是内核的大脑。Zephyr默认用的是基于优先级的抢占式调度。什么意思?高优先级的线程随时可以抢低优先级的CPU。
你想想看,如果两个线程优先级一样怎么办?那就轮着来——时间片轮转。每个线程跑一个时间片,然后换人。
我个人习惯把优先级分成三档:
- 高优先级(0-5):中断下半部、实时控制任务
- 中优先级(6-10):普通业务逻辑
- 低优先级(11-15):后台任务、日志输出
我曾经在一个项目中,把所有线程都设成同一个优先级。结果一个线程里做了个延时操作,整个系统响应都变慢了。后来改成优先级分层,问题就解决了。
避坑指南:我曾经见过有人把中断处理函数里放了个死循环,结果调度器永远切不出去。记住,中断里不要做耗时操作,否则系统就“死”了。
3.3 中断——优先级最高的“不速之客”
中断,说白了就是硬件在喊:“嘿,有事找你!” 内核必须停下手里的活,先去处理中断。
Zephyr里中断处理分两部分:
- ISR(中断服务例程):快进快出,只做最紧急的事
- 中断下半部(通过工作队列或线程):处理剩下的逻辑
为什么这么分?因为ISR里不能调用会阻塞的API。比如你在ISR里调 k_sleep(),那系统就崩了。
注册中断的代码:
#define MY_IRQ_LINE 42
#define MY_IRQ_PRIORITY 2
void my_isr(const void *arg)
{
/* 快速处理硬件事件 */
ARG_UNUSED(arg);
/* 触发一个信号量,让线程去处理剩下的 */
k_sem_give(&my_sem);
}
void main(void)
{
IRQ_CONNECT(MY_IRQ_LINE, MY_IRQ_PRIORITY,
my_isr, NULL, 0);
irq_enable(MY_IRQ_LINE);
}
我的经验:ISR里尽量只做三件事——读寄存器、清标志、发信号量。其他都交给线程。这样系统响应快,也不容易出问题。
3.4 时间管理——别让系统“睡过头”
时间管理是内核的节拍器。Zephyr用系统滴答(tick)来驱动调度和时间测量。默认的tick频率通常是100Hz或1000Hz。
常用的时间API:
| API | 作用 | 注意 |
|---|---|---|
k_sleep() |
让线程休眠指定时间 | 最小精度是1个tick |
k_msleep() |
以毫秒为单位休眠 | 更直观,推荐使用 |
k_uptime_get() |
获取系统运行时间 | 返回毫秒数 |
k_timer_start() |
启动一个定时器 | 可以设置周期或单次 |
我记得有一次,我在一个低功耗项目里把tick频率设成了10Hz,结果 k_sleep(5) 实际睡了100ms。因为tick周期是100ms,最小精度就是100ms。所以,时间精度和功耗之间要权衡。
核心原则:不要指望 k_sleep() 的精度能到微秒级。它只是让线程让出CPU,具体什么时候回来,看调度器心情(和tick周期)。
3.5 同步机制——别让线程“打架”
多线程一起跑,难免要抢资源。同步机制就是用来管这个的。Zephyr提供了几种常用的同步工具:
- 信号量(Semaphore):资源计数,比如“还有几个缓冲区可用”
- 互斥量(Mutex):排他访问,谁拿到谁用
- 条件变量(Condition Variable):等某个条件成立再跑
- 队列(Queue):线程间传数据
我举个实际例子。假设两个线程要共享一个串口:
K_MUTEX_DEFINE(uart_mutex);
void thread_a(void)
{
k_mutex_lock(&uart_mutex, K_FOREVER);
/* 使用串口发送数据 */
uart_send("Hello from A");
k_mutex_unlock(&uart_mutex);
}
void thread_b(void)
{
k_mutex_lock(&uart_mutex, K_FOREVER);
/* 使用串口发送数据 */
uart_send("Hello from B");
k_mutex_unlock(&uart_mutex);
}
这样就不会出现两个线程同时往串口写数据,导致数据错乱的问题。
避坑指南:我曾经在一个项目里,线程A拿了互斥量,然后调了一个函数,那个函数里又去拿同一个互斥量——死锁了。记住,不要在持有锁的情况下再去拿同一个锁。如果一定要这么做,用 K_MUTEX_DEFINE 的可重入版本。
3.6 小结——这些概念怎么串起来?
好,咱们捋一捋。一个典型的Zephyr系统是这样工作的:
- 系统启动,创建几个线程
- 调度器按优先级和时间片,让线程轮流跑
- 突然,硬件来了个中断。CPU停下当前线程,跑ISR
- ISR发了个信号量,然后退出
- 调度器一看,有个高优先级线程在等这个信号量,好,切过去
- 那个线程拿到信号量,处理完数据,然后
k_sleep()让出CPU - 调度器再切回原来的线程
你看,线程、调度器、中断、时间管理、同步机制,全串起来了。每个环节都缺一不可。
下一章,我会带你手把手创建一个Zephyr工程,把这些概念都用上。到时候你就知道,纸上谈兵和实际动手,差距还是挺大的。