第二章 RT-Thread内核基础:线程管理与调度

好,咱们直接进入正题。RT-Thread 的内核,说白了就是整个系统的「心脏」。你想想看,一个汽车电子系统里,有几十个任务在同时跑——采集传感器数据、处理控制算法、响应 CAN 总线消息、刷新显示屏……这些任务怎么协调?谁先跑谁后跑?这就是内核要干的事。

我个人习惯把内核理解成一个「调度中心」。它手里有一堆任务(我们叫线程),每个线程都有自己的优先级和状态。内核负责决定:此时此刻,哪个线程该获得 CPU 的使用权。

2.1 线程的状态与生命周期

RT-Thread 的线程有几种状态?我直接给你列出来:

  • 初始状态:线程刚创建,还没开始跑
  • 就绪状态:万事俱备,只等 CPU
  • 运行状态:正在占用 CPU
  • 挂起状态:因为等待某个资源(信号量、消息队列等)而暂停
  • 休眠状态:主动让出 CPU,比如调用了 rt_thread_delay()
  • 终止状态:线程运行结束或被强制删除

嗯,这里要注意:线程不是一直在跑的。它跑一会儿,可能就被更高优先级的线程抢走了。这就是 RT-Thread 的抢占式调度机制。

核心要点:RT-Thread 是抢占式实时内核。高优先级线程可以打断低优先级线程的执行。这一点在汽车电子中至关重要——比如安全相关的任务必须能立刻响应。

2.2 线程的创建与删除

创建线程有两种方式:静态和动态。我一般推荐用静态方式,尤其是在汽车电子这种对可靠性要求极高的场景。

// 静态创建线程
static struct rt_thread led_thread;
static char led_thread_stack[1024];

void led_thread_entry(void *parameter)
{
    while(1)
    {
        rt_pin_write(LED_PIN, PIN_HIGH);
        rt_thread_mdelay(500);
        rt_pin_write(LED_PIN, PIN_LOW);
        rt_thread_mdelay(500);
    }
}

void application_init(void)
{
    rt_thread_init(&led_thread,
                   "led",
                   led_thread_entry,
                   RT_NULL,
                   &led_thread_stack[0],
                   sizeof(led_thread_stack),
                   10,  // 优先级
                   20); // 时间片
    rt_thread_startup(&led_thread);
}

动态创建用 rt_thread_create(),好处是灵活,坏处是——我在项目里吃过亏——动态分配内存可能失败,尤其是在内存碎片化严重的时候。所以,汽车电子里我建议你多用静态方式。

我的经验:在量产项目中,我习惯把所有线程的栈空间都定义成全局数组。这样内存占用一目了然,调试时也方便查看栈使用情况。

线程间同步:信号量、互斥量、事件集

线程之间怎么配合?这就涉及到同步机制了。说白了,就是防止多个线程同时访问同一个资源,或者协调它们的执行顺序。

2.3 信号量

信号量是最基础的同步工具。它像一个计数器,控制着对某个资源的访问数量。

// 创建信号量
rt_sem_t sem = rt_sem_create("sem", 1, RT_IPC_FLAG_FIFO);

// 线程A:等待信号量
void thread_a_entry(void *parameter)
{
    while(1)
    {
        rt_sem_take(sem, RT_WAITING_FOREVER);
        // 访问共享资源
        rt_kprintf("Thread A got semaphore\n");
        rt_thread_mdelay(100);
        rt_sem_release(sem);
    }
}

// 线程B:释放信号量
void thread_b_entry(void *parameter)
{
    while(1)
    {
        rt_thread_mdelay(200);
        rt_sem_release(sem);
        rt_kprintf("Thread B released semaphore\n");
    }
}

信号量分两种:二值信号量(0或1)和计数信号量(可以大于1)。二值信号量常用于互斥访问,计数信号量常用于生产者-消费者模式。

避坑指南:我曾经在一个项目中,信号量超时时间设成了 RT_WAITING_FOREVER,结果某个线程因为 bug 一直没释放信号量,导致其他所有等待该信号量的线程全部卡死。后来我改成带超时的 rt_sem_take(sem, 1000),至少能检测到异常。

2.4 互斥量

互斥量是信号量的「升级版」。它解决了优先级反转的问题——你想想看,如果低优先级线程拿着锁,高优先级线程却要等它,这合理吗?

RT-Thread 的互斥量支持优先级继承协议。什么意思?就是当高优先级线程等待互斥量时,持有互斥量的低优先级线程会临时「继承」高优先级,等释放锁后再恢复原优先级。

// 创建互斥量
rt_mutex_t mutex = rt_mutex_create("mutex", RT_IPC_FLAG_FIFO);

void critical_task(void *parameter)
{
    while(1)
    {
        rt_mutex_take(mutex, RT_WAITING_FOREVER);
        // 临界区代码
        rt_kprintf("Entering critical section\n");
        rt_thread_mdelay(50);
        rt_mutex_release(mutex);
    }
}

重要区别:信号量可以在中断中释放,互斥量不行。因为互斥量涉及优先级继承,中断上下文不能做线程调度。这个坑我踩过,切记。

2.5 事件集

事件集适合「多条件触发」的场景。比如一个线程要等三个传感器都就绪了才能开始工作,用信号量就得等三次,用事件集一次搞定。

#define EVENT_SENSOR1 (1 << 0)
#define EVENT_SENSOR2 (1 << 1)
#define EVENT_SENSOR3 (1 << 2)

// 创建事件集
rt_event_t event = rt_event_create("event", RT_IPC_FLAG_FIFO);

// 等待所有事件
void worker_thread(void *parameter)
{
    rt_uint32_t recv_set;
    while(1)
    {
        // 等待三个事件都发生
        rt_event_recv(event,
                      EVENT_SENSOR1 | EVENT_SENSOR2 | EVENT_SENSOR3,
                      RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
                      RT_WAITING_FOREVER,
                      &recv_set);
        rt_kprintf("All sensors ready, start processing\n");
    }
}

事件集支持「与」和「或」两种模式。与模式就是所有条件都满足才触发,或模式是任意一个满足就触发。这个在汽车电子里很实用——比如安全气囊的触发条件可能是「碰撞信号」与「车速超过阈值」同时满足。

线程间通信:邮箱、消息队列

同步是协调执行顺序,通信是传递数据。RT-Thread 提供了邮箱和消息队列两种方式。

2.6 邮箱

邮箱每次只能传递 4 字节的数据(一个指针或一个整数)。轻量、高效,适合传递控制命令或状态信息。

// 创建邮箱
rt_mailbox_t mb = rt_mb_create("mb", 10, RT_IPC_FLAG_FIFO);

// 发送线程
void sender(void *parameter)
{
    rt_uint32_t cmd = 0x12345678;
    while(1)
    {
        rt_mb_send(mb, cmd);
        rt_thread_mdelay(100);
    }
}

// 接收线程
void receiver(void *parameter)
{
    rt_uint32_t value;
    while(1)
    {
        rt_mb_recv(mb, &value, RT_WAITING_FOREVER);
        rt_kprintf("Received: 0x%08x\n", value);
    }
}

2.7 消息队列

消息队列可以传递任意长度的数据。它内部维护了一个消息缓冲区,支持 FIFO 和优先级两种排队方式。

// 创建消息队列
rt_mq_t mq = rt_mq_create("mq", 64, 10, RT_IPC_FLAG_FIFO);

// 发送结构体数据
typedef struct {
    uint16_t id;
    uint8_t data[8];
} can_message_t;

void can_sender(void *parameter)
{
    can_message_t msg = {0x100, {1,2,3,4,5,6,7,8}};
    while(1)
    {
        rt_mq_send(mq, &msg, sizeof(msg));
        rt_thread_mdelay(50);
    }
}

我的建议:消息队列适合传递结构化的数据包。我在做 CAN 总线解析时,就用消息队列把接收到的 CAN 帧从中断服务函数传递给处理线程。注意,中断里只能用 rt_mq_send() 的变体 rt_mq_send_wait(),而且不能等待。

内存管理

RT-Thread 的内存管理分两种:动态内存堆和静态内存池。

2.8 动态内存堆

动态内存堆就是标准的 malloc/free 机制。RT-Thread 支持多种算法:memheap(多堆)、smallmem(小内存管理)、slab(大内存管理)。

void *ptr = rt_malloc(1024);
if (ptr != RT_NULL)
{
    // 使用内存
    memset(ptr, 0, 1024);
    rt_free(ptr);
}

警告:动态内存分配可能导致碎片化。我曾经在一个长期运行的设备上,发现系统运行一个月后,rt_malloc 频繁失败。后来排查发现是频繁分配释放小内存导致的碎片。解决方案是改用内存池。

2.9 静态内存池

内存池预先分配好固定大小的内存块,分配和释放都是 O(1) 复杂度,没有碎片问题。适合汽车电子这种对实时性和可靠性要求高的场景。

// 创建内存池
rt_mp_t mp = rt_mp_create("mp", 64, 10);  // 10个64字节的块

void *block = rt_mp_alloc(mp, RT_WAITING_FOREVER);
if (block != RT_NULL)
{
    // 使用内存块
    rt_mp_free(block);
}

最佳实践:在汽车电子项目中,我建议对频繁分配释放的数据结构使用内存池。比如 CAN 消息缓冲区、传感器数据缓存等。内存池的大小可以在编译时确定,这样内存使用是确定的,不会出现运行时内存不足的情况。

好了,这一章的内容就这些。线程管理、同步、通信、内存管理——这四个模块是 RT-Thread 内核的基石。下一章我们会深入设备驱动框架,看看怎么把外设和内核优雅地结合起来。