RT-Thread 专栏总结

1、对RTOS/RT-Thread优先级反转的理解

参考链接 1: https://blog.csdn.net/m0_74712453/article/details/134001652

参考链接 2:https://blog.csdn.net/weixin_45590051/article/details/118330634

优先级反转是实时操作系统最常见的问题,解决办法是互斥量使用优先级继承方法。

1、优先级翻转

1.1 简单定义

  • 简单言之:高优先级线程等待资源的过程中,被其他低优先级线程抢占了 CPU,从而造成高优先级线程被其他低优先级线程阻塞,实时性难以得到保证。

1.2 详细定义

  • 详细言之:最高优先级线程 A 和最低优先级线程 C 通过信号机制共享资源。目前最低优先级线程 C 占有资源,锁定了信号量,最高优先级线程 A 陷入阻塞。若在最低优先级线程 C 释放信号量前,不访问共享资源的中等优先级线程 B 抢占了线程 C 的 CPU,此时就出现线程 B 优先级比线程 A 低,却导致线程 A 阻塞。
    在这里插入图片描述

产生原因:当一个高优先级的任务(如任务A)需要访问一个由低优先级任务(如任务C)持有的共享资源(如信号量)时,高优先级任务会被阻塞,直到低优先级任务释放该资源。
在这段时间内,如果有另一个中等优先级的任务(如任务B)开始执行,并且它不依赖于这个共享资源,它可能会得到CPU并执行完它的工作。
这会导致高优先级任务(A)的实时性得不到保证,因为它需要等待低优先级任务(C)释放资源,同时还可能被中等优先级任务(B)抢占CPU时间。

解决办法1:优先级继承。即当一个高优先级等待低优先级持有的资源,则低优先级暂时获得高优先级任务的优先级,释放资源后,优先级才变成原先的优先级。
解决办法2:优先级天花板。给每个共享资源都分配优先级天花板,进入共享资源临界区的任务优先级会被提升至天花板优先级。

2、例子

2.1 定义

若互斥信号量正在被低优先级 C 的线程持有时,若此时高优先级线程 A 尝试获取这个互斥信号量,则高优先级线程 A 阻塞之时,会将低优先级 C 的优先级提高至与自己相同的优先级。

2.2 获取互斥量函数

RT-Thread 中,获取互斥量函数 rt_mutex_take 内部已经实现了优先级继承

// 高优先级线程 tid2 试图拥有低优先级线程 tid3 的互斥锁,
// RT-Thread 会将 tid3 优先级提高到 tid2 同等优先级
result = rt_mutex_take(mutex, RT_WAITING_FOREVER);

2.3 验证优先级继承 Demo

#include <rtthread.h>

/* tid1 优先级最高,tid2 优先级中等,tid1 优先级最低 */
static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;
static rt_thread_t tid3 = RT_NULL;
static rt_mutex_t mutex = RT_NULL;

#define THREAD_PRIORITY 10
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5

static void thread1_entry(void *parameter)
{
    rt_kprintf("thread1_entry: the priority of thread1 is: %d\n", tid1->current_priority);
    // 延迟让低优先级线程运行
    rt_thread_mdelay(100);

    // 此时 tid3 持有 mutex,tid2 等待持有 mutex
    // 若此时 tid2 与 tid3 优先级不等,则说明测试失败
    if (tid2->current_priority != tid3->current_priority)
    {
        rt_kprintf("thread1_entry: the priority of thread2 is: %d\n", tid2->current_priority);
        rt_kprintf("thread1_entry: the priority of thread3 is: %d\n", tid3->current_priority);
        rt_kprintf("thread1_entry: test failed.\n");
        return;
    }
    else
    {
        rt_kprintf("thread1_entry: the priority of thread2 is: %d\n", tid2->current_priority);
        rt_kprintf("thread1_entry: the priority of thread3 is: %d\n", tid3->current_priority);
        rt_kprintf("thread1_entry: test OK.\n");
    }
}


/* 线程 2 入口 */
static void thread2_entry(void *parameter)
{
    rt_err_t result;
    rt_kprintf("thread2_entry: the priority of thread2 is: %d\n", tid2->current_priority);

    /* 延迟让低优先级线程运行 */
    rt_thread_mdelay(50);

    // 高优先级线程 tid2 试图拥有低优先级线程 tid3 的互斥锁,
    // RT-Thread 会将 tid3 优先级提高到 tid2 同等优先级
    result = rt_mutex_take(mutex, RT_WAITING_FOREVER);
    if (result = RT_EOK)
    {
        rt_mutex_release(mutex);
    }
}

/* 线程 3 入口 */
static void thread3_entry(void *parameter)
{
    rt_tick_t tick;
    rt_err_t result;

    rt_kprintf("thread3_entry: the priority of thread3 is: %d\n", tid3->current_priority);
    
    result = rt_mutex_take(mutex, RT_WAITING_FOREVER);
    if (result != RT_EOK)
    {
        rt_kprintf("thread3_entry: thread3 take a mutex, failed.\n");
    }

    // 做长时间的循环,以便 tid2 可以试图抢夺互斥锁 mutex
    tick = rt_tick_get();
    while (rt_tick_get() - tick < (RT_TICK_PER_SECOND / 2));
    rt_mutex_release(mutex);
}

int pri_inversion(void)
{
    // 创建互斥锁
    mutex = rt_mutex_create("mutex", RT_IPC_FLAG_FIFO);

    if (mutex == RT_NULL)
    {
        rt_kprintf("create dynamic mutex failed.\n");
        return -1;
    }

    tid1 = rt_thread_create( "thread1", thread1_entry, RT_NULL,
        THREAD_STACK_SIZE, THREAD_PRIORITY - 1, THREAD_TIMESLICE);

    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);

    tid2 = rt_thread_create( "thread2", thread2_entry, RT_NULL,
        THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);

    if (tid2 != RT_NULL)
        rt_thread_startup(tid2);

    tid3 = rt_thread_create( "thread3", thread3_entry, RT_NULL,
    THREAD_STACK_SIZE, THREAD_PRIORITY + 1, THREAD_TIMESLICE);

    if (tid3 != RT_NULL)
        rt_thread_startup(tid3);

    return 0;
}

2、操作系统为什么会使应用程序所需的内存暴增?

使用操作系统所增加的内存需求,主要来源于:

  • 1)运行操作系统本身所需的内存

  • 2)创建线程块、消息控制块等操作系统组件需要的内存;

    裸机使用标志位或全局变量来为不同模块之间传递信息,操作系统模式下,会涉及同步、互斥等,需要使用信号量、消息队列等操作系统组件。

    • 扩展:裸机跟操作系统传递消息方式的区别?(最核心的是操作系统可以有多线程,线程栈由堆分配)

3、RT-Thread 内核

实时内核的实现包括:对象管理、线程管理及调度、时钟管理、线程间通信管理、及内存管理等。

3.1 线程调度

  • 1)线程是 RTT 最小的调度单位,RTT 的线程调度算法是基于优先级的全抢占式多线程调度算法。
  • 2)除了中断处理函数、临界区代码和禁止中断的代码时不可抢占之外,其余都可抢占,包括线程调度器本身。
  • 3)支持 256 个线程优先级,0 优先级最高,255 优先级最低,最低优先级留给空闲线程使用。

3.2 时钟管理

  • RTT 的时钟管理以时钟节拍为基础,基于时钟节拍处理所有和时间相关的事件,如线程延迟、线程时间片轮转调度及定时器超时等。
  • 通常使用定时器定时回调函数完成定时服务。
  • 时钟节拍的实现和获取(见3.2.1)

3.2.1 RTT 时钟节拍的实现和获取

参考链接:https://blog.csdn.net/qq_52251819/article/details/134982751

  • 时钟节拍由配置为中断模式的系统滴答定时器产生,在系统滴答定时器初始化函数(rt_hw_systick_init(void))里会使用到 RT_TICK_PER_SECOND这个宏。时钟节拍的值等于1/RT_TICK_PER_SECOND (位于 rtconfig.h),如若 RT_TICK_PER_SECOND - 1000,则滴答定时器中断会 1ms 进行一次。

截图

  • 滴答定时器每次中断会进入滴答定时器中断函数(void SysTick_Handler(void)),每次进入中断,全局变量滴答数 rt_tick 变量就会自增。因此系统是根据全局变量滴答数来获取时间的。

截图


4 线程同步

Q:RTT 如何实现线程同步?

A:通过信号量、互斥量与事件集实现线程间同步。

  • 1)线程通过对信号量、互斥量的获取与释放进行同步。互斥量通过优先级继承的方式解决了实时操作系统常见的优先级反转问题。
  • 2)线程通过对事件的发送与接收进行同步。事件集支持多事件的“或触发”和“与触发”,适合于线程等待多个事件的情况。

4.1 事件集详解与应用

参考:https://blog.csdn.net/kouxi1/article/details/122817106

事件用于实现线程之间的同步,一个事件发生即使一个同步。事件集可以实现一对多(一个线程等待多个事件触发)、多对多(多个线程等待多个事件触发)的同步。

其中,一个线程与多个事件的关系可以设为:

  • 1)特定事件触发唤醒线程;
  • 2)其中任意一个事件触发唤醒线程;
  • 3)几个事件都触发后才唤醒线程;

4.1.1 事件集工作机制

事件集可以用 32 bit 无符号整型变量表示,每个 bit 代表一个事件,线程通过逻辑与或者逻辑或将一个或多个事件关联起来,形成事件组合。

  • 逻辑或:即独立性同步,线程与任何事件之一发生同步;
  • 逻辑与:即关联型同步,线程与若干事件都发生同步;
  • 逻辑或和逻辑与只能二选一,不可一起使用。

4.1.2 事件集与信号量的区别

  • 对于事件集,线程与事件集的同步关系可以是一对多,也可以是多对多。即一个线程等待多个事件触发,或多个线程等待多个事件触发;对于信号量,线程与信号量的同步关系只能是一对一,即一个信号量只能同步一个线程。
  • 事件的发送动作在事件未清除之前,是不可累加的(类似边缘触发模式,只触发一次);而信号量的释放动作可以累加(类似水平触发模式)

4.1.3 事件集本质是做标志,为什么不用全局变量?

  • 在操作系统中,涉及多线程同时对全局变量进行访问,需要通过同步与互斥的方式保护全局变量(临界资源);
  • 使用全局变量,线程对事件的获取是主动轮询方式,浪费 CPU;
  • 事件集具有等待超时机制,而全局变量只能由用户自己实现;

4.1.4 事件集控制块

事件集控制块含有 IPC 成员变量,IPC 成员变量含有线程挂起等待链表。

struct rt_object
{
  const char *name; 
  rt_uint8_t  type;   /**< type of kernel object */
  rt_uint8_t  flag;   /**< flag of kernel object */
  rt_list_t   list; 
}
struct rt_ipc_object
{
  struct rt_object parent;
  // 线程挂起等待链表
  rt_list_t suspend_thread;
}
struct rt_event
{
  struct rt_ipc_object parent;
  rt_uint32_t set;
}
typedef struct rt_event* rt_event_t;

4.1.5 事件集函数接口

4.1.5.1 创建/删除
  • 创建事件集:核心操作是1)设置阻塞唤醒模式;2)初始化一个链表用于因记录访问此事件而被阻塞挂起的线程;3)清空事件集。
  • 参数 name:事件集名称;
  • flag:事件集的标志。阻塞线程按优先级等待RT_IPC_FLAG_PRIO还是RT_IPC_FLAG_FIFO,与信号量使用方法一致。(备注:既然有阻塞线程链表,必定要有对阻塞链表排序的方法)
    rt_event_t rt_event_create(const char *name, rt_uint8_t flag)
    {
      ...
      // 设置阻塞唤醒模式
      event->parent.parent.flag = flag;
      // 初始化一个链表用于因记录访问此事件而被阻塞挂起的线程
      _ipc_object_init(&(event->parent));
      /* 事件集合清零 */
      event->set = 0;
      return event;
    }
    
  • 删除事件集:核心操作是1)唤醒所有等待此事件集的线程;2)将事件集从内核对象管理器链表中删除
    rt_err_t rt_event_delete(rt_event_t event)
    {
      ...
      // 唤醒所有等待此事件集的线程
      rt_susp_list_resume_all(&(event->parent.suspend_thread), RT_ERROR);
      // 将事件集从内核对象管理器链表中删除
      rt_object_delete(&(event->parent.parent));
      return RT_EOK;
    }
    
4.1.5.2 发送事件
  • 定义:事件发送 rt_event_send()是指一个线程(任务)向另一个线程(任务)发送事件信号,通知该线程(任务)某事件已经满足条件。如传感器采集事件完成了数据采集工作,就可发送事件通知数据处理线程进行数据处理。
  • 线程 A 调用事件发送函数rt_event_send()依赖事件对象参数 rt_event_t event 和事件集标志参数 rt_uint32_t set。 此函数遍历因等待事件而阻塞挂起的线程,若线程的事件信息与当前事件对象的事件标志相匹配,此线程将被恢复。
    • 备注:rt_thread_t 包含1)用于链接的链表头 rt_list_t 变量;2)记录线程感兴趣的事件集 rt_uint32_t event_set 变量;3)记录事件模式的变量 rt_uint8_t event_info
/*
@param set:要发送的事件标志
*/
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set)
{
  struct rt_list_node *n;
  ...
  /* 关中断 */
  level = rt_hw_interrupt_disable();
  /* 设置事件 */
  event->set |= set;
  
  // 若线程因等待某个事件而阻塞,会被添加进该事件的挂起线程链表
  if (!rt_list_isempty(&event->parent.suspend_thread))
  {
    n = event->parent.suspend_thread.next;
    while (n != &(event->parent.suspend_thread))
    {
      // 根据已知成员 rt_list_t 的地址,算出结构体 rt_thread_t 的首地址      
      thread = rt_list_entry(n, struct rt_thread, list);
      status = -RT_ERROR;
      
      if (thread->event_info & RT_EVENT_FLAG_AND)
      {
        if ((thread->event_set & event->set) == thread->event_set)
        {
            status = RT_EOK;
        }
      }
      else if (thread->event_info & RT_EVENT_FLAG_OR)
      {
        if (thread->event_set & event->set)
        {
            /* 保存收到的事件集*/
            thread->event_set = thread->event_set & event->set;
            status = RT_EOK;
        }
      }
      else
      {...}
      
      n = n->next;
      
      // 若条件满足,恢复线程
      if (status == RT_EOK)
      {
        // 若在接收中设置了RT_EVENT_FLAG_CLEAR,线程唤醒时会将事件标志位清除, 防止一直响应事件
        if (thread->event_info & RT_EVENT_FLAG_CLEAR)
          event->set &= ~thread->event_set;
        // 恢复阻塞的线程
        rt_thread_resume(thread);
        need_schedule = RT_TRUE;
      }
    }
  }
  
  /* 关中断 */
  rt_hw_interrupt_enable(level);
  
  // 发起一次线程调度
  if (need_schedule == RT_TRUE)
    rt_shedule();
  
  return RT_EOK;
}

4.1.5.3 接收事件
  • 定义:事件接收 是指一个线程等待某个事件发生,一旦发生,此线程被唤醒并得到相应的信息。
  • 调用接收事件函数 rt_event_recv() ,线程从事件对象 event 接收事件。
    • 若事件对象 event 满足线程所感兴趣的事件,则根据接收选项 option 是否设置 RT_EVENT_FLAG_CLEAR 来决定是否清除事件相应标志位,并返回;
    • 若不满足,则把线程感兴趣的事件 set 和接收选项 option 写到线程控制块,然后把线程挂在此事件对象的阻塞队列上,直到事件发生或等待事件超时。
/*
@param  event 指向要接收的事件集对象指针
@param  set 线程感兴趣的事件标志
@param  option 取值为:RT_EVENT_FLAG_OR、RT_EVENT_FLAG_AND、RT_EVENT_FLAG_CLEAR        
@param  recved 指向返回的已接收事件的指针
*/
rt_err_t rt_event_recv(rt_event_t event, rt_uint32_t set, rt_uint8_t option, 
                      rt_uint32_t timeout, rt_uint32_t *recved)
{
  ...
  // 获取当前接收事件的线程
  thread = rt_thread_self();
  
  /* 关中断 */
  level = rt_hw_interrupt_disable();
  
  if (option & RT_EVENT_FLAG_AND)
  {
    if ((event->set & set) == set)
      status = RT_EOK;
  }
  else if (option & RT_EVENT_FLAG_OR)
  {
    if (event->set & set)
      status = RT_EOK;
  }
  else
  {...}
  // 若事件集达到触发要求
  if (status == RT_EOK)
  {
    // 记录接收的事件
    if (recved)
      *recved = (event->set & set);
    
    // 设置线程事件信息
    thread->event_set = (event->set & set);
    thread->event_info = option;
    
    if (option & RT_EVENT_FLAG_CLEAR)
      event->set &= ~set;
  }
  // 若当前线程不等待
  else if (timeout == 0)
  {
    ...
  }
  // 若当前线程等待
  else
  {
    /* 设置线程感兴趣的事件信息 */
    thread->event_set  = set;
    thread->event_info = option;
    
    _ipc_list_suspend(&(event->parent.suspend_thread), thred, event->parent.parent.flag);
  
    // 若有等待超时,则启动线程计时器
    if (timeout > 0)
    {
      // 重置线程超时时间并启动定时器
      rt_timer_control(&(thread->thread_timer), RT_TIMER_CTRL_SET_TIME, &timeout);
      rt_timer_start(&(thread->thread_timer));
    }
    
    /* 开中断 */
    rt_hw_interrupt_enable(level);
    /* 发起一次线程调度 */
    rt_schedule();
    ...
    
    // 返回接收到的事件
    if (recved)
      *recved = thread->event_set;
  }
  ...
}

4 RT-Thread 学习记录之线程

https://blog.csdn.net/m0_46430715/article/details/126319773?spm=1001.2014.3001.5502

RT-Thread 大致是采用定时器、中断和链表来执行线程调度。

RT-Thread 线程的 5 种状态:创建态、就绪态、运行态、挂起态、终止态。线程调度就是使得线程在这 5 种状态中切换。

4.1 线程控制块结构体

线程控制块结构体:包含 1)线程名称;2)用于链接线程之间的链表;3)记录线程栈的相关变量(如栈底地址、栈顶指针、栈大小);4)记录线程入口的相关变量(如入口地址、参数等);5)用于抢占的线程优先级;6)用于时间片调度的相关变量(如初始滴答数、剩余滴答数、定时器)、7)用于同步的记录线程感兴趣的事件变量

struct rt_thread
{
  char name[RT_NAME_MAX];   // 线程名称
  rt_list_t list;   // 对象链表
  
  /* 阻塞线程之间的链表(?) */
  rt_list_t tlist;   // 线程链表,
  
  /* 记录线程栈的相关变量 */
  void *sp;                // 栈指针
  void *stack_addr;        // 栈地址指针
  rt_uint32_t stack_size;  // 栈大小
  
  /* 记录线程入口的相关变量 */
  void *entry;             // 入口地址
  void *parameter;         // 参数
  
  rt_uint8_t stat;        // 线程状态
  
  /* 记录线程优先级的变量 */
  rt_uint8_t current_priority;  // 当前优先级
  rt_uint8_t init_priority;  // 初始优先级
  
  /* 记录线程时间片相关的变量 */
  rt_ubase_t init_tick;   // 初始滴答数
  rt_ubase_t remaining_tick;   // 剩余滴答数
  struct rt_timer thread_timer;  // 内置线程定时器
  
  /* 记录线程感兴趣的事件变量 */
  rt_uint32_t event_set;
  rt_uint8_t event_info;
}

4.1 静态初始化线程过程

初始化线程,首先会调用内核对象初始函数 rt_object_init()将线程对象初始化注册到系统管理器,然后用内置静态初始化线程函数 _rt_thread_init() 将线程属性记录到线程控制块中。

rt_err_t rt_thread_init(struct rt_thread *thread, const char *name, void (*entry)(void *parameter),
                        void *parameter,void *stack_start, rt_uint32_t stack_size, 
                        rt_uint8_t priority, rt_uint32_t tick)
{
  /* 将线程对象注册到系统管理器 */
  rt_object_init((rt_object_t)thread, RT_Object_Class_Thread, name);

  return _rt_thread_init(thread,name, entry, parameter,stack_start,
                         stack_size,priority, tick);
}

内置静态初始化线程函数_rt_thread_init()` 将线程属性记录到线程控制块,包括:

1)初始化线程链表(链表只有自身,头尾指针指向自己);

2)指定线程入口函数和参数;

3)初始化线程栈;

4)初始化线程时间片;

5)初始化线程内置定时器;

6)绑定钩子函数

4.2 线程恢复

线程恢复函数rt_thread_resume()核心操作是将线程从阻塞线程链表移除,并插入就绪链表,设置链表状态为就绪态。

rt_err_t rt_thread_resume(rt_thread_t thread)
{
  ...
 /* 将该线程从链表移除 */
  rt_list_remove(&(thread->tlist));
  /* 将该线程插入就绪链表,线程状态将被设置为就绪并从挂起链表中删除。 */
  rt_schedule_insert_thread(thread);
  ...
  return RT_EOK;
}

4.3 线程启动(核心)

线程启动函数 rt_thread_up()主要动作是改变线程状态字,先从初始态切换为挂起态,然后调用线程恢复函数 rt_resume()将挂起态切换为就绪态。最后做一次调度 rt_schedule()

rt_err_t rt_thread_startup(rt_thread_t thread)
{
  ...
  /* 改变线程状态为挂起状态 */
  thread->stat = RT_THREAD_SUSPEND;
  /* 线程恢复,将线程切换到就绪状态 */
  rt_thread_resume(thread);
  /* 获得当前执行的线程句柄 */
  if (rt_thread_self() != RT_NULL)
  {
      /* 做一次调度 */
      rt_schedule();
  }
  return RT_EOK
}
  • 【总结】线程启动流程如下:1)创建线程/初始化线程;2)调用线程启动函数 rt_thread_startup();3)将线程状态从初始态切换为挂起态;4)调用线程恢复函数rt_thread_resume()将线程从线程挂起链表移除,并插入线程就绪链表,设置线程的状态为就绪态;5)线程启动成功

5 RT-Thread 启动过程

参考链接1:https://blog.csdn.net/weixin_44746581/article/details/108659183?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171370552216800222820810%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=171370552216800222820810&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_ecpm_v1~rank_v31_ecpm-6-108659183-null-null.nonecase&utm_term=RT-Thread&spm=1018.2226.3001.4450

参考链接2:https://doc.embedfire.com/rtos/rtthread/zh/latest/application/rtthread_startup.html

RT-Thread 的启动流程(rtthread_startup 函数):

  • 1)硬件关中断;
  • 2)板级初始化:系统堆栈初始化、硬件初始化;
  • 3)定时器初始化;
  • 4)调度器初始化;
  • 5)创建初始线程:初始线程创建后必须等调度器调度才能进入 main 函数;
  • 6)定时器线程初始化;
  • 7)空闲线程初始化:空闲线程的作用是维护定时器,空闲线程周期性地扫描所有定时器,并检查是否到达触发事件。若到达触发时间,则会触发相应的定时器回调函数;
  • 8)启动调度器
void rtthread_startup(void)
{
  rt_hw_interrupt();  // 关闭全局中断
  rt_hw_board_init();  // 板级初始化
  rt_show_version(); // 系统版本信息
  rt_system_scheduler_init(); // 调度器初始化
  rt_system_timer_init(); // 定时器初始化
  rt_system_timer_thread_init();  // 定时器线程初始化
  rt_application_init();  // 应用 main 线程初始化
  rt_thread_idle_init(); // 空闲线程初始化
  rt_system_scheduler_start();  // 启动调度器
}

7 RT-Thread 学习记录之线程调度

参考链接1:https://blog.csdn.net/m0_46430715/article/details/126558101?spm=1001.2014.3001.5502

【详细】参考链接2:https://blog.csdn.net/weixin_44746581/article/details/109055194

RT-Thrread 采用基于优先级的抢占式线程调度,它通过维护一个队列数组,即优先级表rt_thread_priority_table[RT_THREAD_PRIORITY_MAX]来实现线程优先级管理系统。如下图所示,每个优先级队列采用双向环形链表链接。

截图


7.1 调度器初始化

调度器初始化rt_system_scheduler_init()核心动作就是初始化优先级表里的所有双向队列。

void rt_system_scheduler_init(void)
{
  ...
  /* 所有优先级对应的队列数组初始化 */
  for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++)
  {
      rt_list_init(&rt_thread_priority_table[offset]);
  }
  
  /* 初始化就绪链表 */
  rt_memset(rt_thread_ready_table, 0, sizeof(rt_thread_ready_table));
}

7.2 调度器启动(系统初始化调度器时启动)

调度器启动函数rt_system_scheduler_start() 发生在系统初始化线程调度器之时。调度器启动函数rt_system_scheduler_start()即在就绪线程链表里,按照优先级策略,挑选一个线程启动。

具体步骤如下:1)查找出新的最高优先级线程;2)切换该线程;3)从就绪链表中移除此线程,并修改线程的状态;4)通过上下文切换函数,切换到目标线程。

void rt_system_scheduler_start(void)
{
  ...
  // 查找出新的最高优先级线程
  to_thread = _get_highest_priority_thread(&highest_ready_priority);
  // 切换该线程
  rt_current_thread = to_thread;
  // 从就绪链表中移除此线程,并修改线程的状态
  rt_schedule_remove_thread(to_thread);
  to_thread->stat = RT_THREAD_RUNNING;
  // 切换到目标线程
  rt_hw_context_swith_to((rt_ubase_t)&to_thread->sp);
}

7.3 调度器调度(系统运行时调度)

调度器调度一次 rt_schedule() ,即是选择一个最高优先级的就绪线程,并立即切换到该进程。

具体言之:

  • 1)从就绪链表中选出最高优先级就绪线程,并判断当前线程与最高优先级就绪线程的优先级关系。若当前线程优先级大于就绪线程,或者二者优先级相等,但就绪线程有资源未释放,则不进行调度;否则调度。
  • 2)切换线程的具体做法是:设置当前线程为to_thread,就绪线程为from_thread。将to_thread从就绪链表删除,并设置状态为运行态,将from_thread插入就绪链表。
void rt_shedule(void)
{
  level = rt_hw_interrupt_disable();
  
  // 若调度器未加锁,则调度程序未启动
  if (rt_sheduler_lock_nest == 0)
  {
    // 判断就绪链表是否存在线程
    if (rt_thread_ready_priority_group != 0)
    {
      //从就绪链表中选出最高优先级线程
      to_thread = _get_highest_priority_thread(&highest_ready_priority);
      
      // 若当前线程处于运行态
      if ((rt_current_thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_RUNNING)
      {
        // 且当前线程的优先级高于最高优先级就绪线程,则不进行调度
        if (rt_current_thread->current_priority < highest_ready_priority)
        {
          to_thread = rt_current_thread;
        }
        // 若当前线程与最高优先级就绪线程优先级相同,但后者未释放资源,也不进行调度 
        else if ((rt_current_thread->current_priority == highest_ready_priority) && 
                 (rt_current_thread->stat & RT_THREAD_STAT_YEILD_MASK) == 0)
        {
          to_thread = rt_current_thread;
        }
        // 否则进行调度,需要将 from_thread 插入就绪链表
        else
        {
          need_insert_from_thread = 1;
        }
        // 当前线程让出 cpu 资源
        rt_current_thread->stat &= ~RT_THREAD_STAT_YIELD_MASK;
      }
      
      // 若目标线程与当前线程不同则切换线程
      if (to_thread != rt_current_thread)
      {
        rt_current_priority = (rt_uini8_t)higest_ready_priority;
        from_thread = rt_current_thread;
        rt_current_thread = to_thread;
        
        // 将 from_thread 插入就绪链表 
        if (need_insert_from_thread)
        {
          rt_schedule_insert_thread(from_thread);
        }
        // 将 to_thread 从就绪链表删除 
        rt_schedule_remove_thread(to_thread);
        // 将 to_thread 切换为运行态
        to_thread->stat = RT_THREAD_RUNNING | (to_thread->stat & ~RT_THREAD_STAT_MASK);
      }
    }
  }
  
}

8、定时器初始化

参考链接1:https://blog.csdn.net/kouxi1/article/details/122658776
参考链接2:https://doc.embedfire.com/rtos/rtthread/zh/latest/application/software_timer.html # 软件定时器

8.1 定时器基础知识

定时器分为硬件定时器和软件定时器。硬件定时器由外部晶振提供输入时钟,到达设定时间后产生时钟中断(进入 tick 中断服务例程)。软件定时器将 SysTick 系统节拍周期作为基础时钟,超出指定事件后调用超时函数(超时函数在 timer 线程中回调)。超时函数的执行时间应该尽可能地短。

8.2 定时器控制块

struct rt_timer
{
  struct rt_object parent;
  rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL]; // 定时器链表节点
  void (*timeout_func)(void *parameter);  // 定时器超时调用的函数
  void *parameter;  // 超时函数的参数
  rt_tick_t init_tick;  // 定时器初始超时节拍数
  rt_tick_t timeout_tick;  // 定时器实际超时节拍数
};
typedef struct rt_timer *rt_timer_t;

8.3 定时器工作机制

创建的定时器链表会以超时时间升序排序,RT-Thread 通过跳表算法来加快搜索链表元素的速度,使得查找元素时链表的事件复杂度由 O(n) -> O(logn)

跳表算法采取的是“空间换时间”的算法,类似二叉搜索树,把一些节点提取出来作为索引。

8.4 定时器线程

定时器线程入口函数 rt_thread_timer_entry() 代码如下:
由于 (6) 处线程进入休眠,休眠结束后,线程未必能立刻获取到 CPU,因此软件定时器不精准。

static rt_thread_timer_entry(void *parameter)
{
  rt_tick_t next_timeout;
  while (1)
  {
    // 获取软件定时器列表中下一个定时器的到达时间
    next_timeout = rt_timer_list_next_timeout(rt_soft_timer_list);
    // 若无软件定时器
    if (next_timeout == RT_TICK_MAX) {
      // 挂起线程,让出 CPU
      rt_thread_suspend(rt_thread_self());
      rt_schedule();
    }
    else {
      rt_tick_t current_tick;
      current_tick = rt_tick_get();
      if ((next_timeout - current_tick) < RT_TICK_MAX / 2) {
        next_timeout = next_timeout - current_tick;
        // 通过延时函数减少查找定时器时间,提高 CPU 利用率
        rt_thread_delay(next_timeout);  (6)
      }

    }
  }

}




9 RT-Thread 学习记录之上下文切换分析

参考链接:https://blog.csdn.net/kouxi1/article/details/123742937


9.1 上下文切换相关函数及变量

  • void rt_hw_context_switch_to(rt_uint32_t to):无来源线程的上下文切换,在调度器启动第一个线程时调用;
  • void rt_hw_context_switch_to(rt_uint32_t from, rt_uint32_t to):从 from 线程切换到 to 线程
  • void rt_hw_context_switch_interrupt(rt_uint32_t from, rt_uint32_t to):在中断里面,从 from 线程切换到 to 线程
  • rt_uint32_t rt_thread_switch_interrupt_flag:中断里进行切换的标志
  • rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread:线程上下文切换时,用来保存 from 和 to 线程的栈顶指针的指针

9.1.1 void rt_hw_context_switch_to(rt_uint32_t to)

此函数在调度器启动第一个线程时调用,其具体做法如下:

  • 1)保存目标线程的栈顶指针参数:将目的线程的栈顶指针参数 to 保存到 rt_interrupt_to_thread 变量中;
  • 2)将来源线程的栈顶指针设置为 0:将rt_interrupt_from_thread设置为 0;
  • 3)将线程切换中断标志设置为 1:将rt_thread_switch_interrupt_flag设置为 1;
  • 4)设置 PendSV 异常优先级、触发 PendSV 中断;
  • 5)将 PSR(程序状态寄存器)、 PC(程序计数器)、LR(链接寄存器/返回地址寄存器)、R12 等寄存器压入栈,恢复 MSP 的默认值;
  • 6)使能全局中断

9.1.2 PendSV 异常(线程调度在PendSV异常中触发)

参考链接:https://zhuanlan.zhihu.com/p/554825433

PendSV 异常即可挂起的系统调用,其优先级可被设置。若将其优先级设置为最低的异常优先级,可以让 PendSV 异常处理在其他中断处理完成后执行,这对于上下文切换非常有用,是 OS 设计中的关键。

9.1.3 堆栈操作

Cortex M4 有 2 个堆栈寄存器,主堆栈寄存器(MSP)与进程堆栈寄存器(PSP),任一时刻只能使用其中一个。异常永远用 MSP,线程使用 PSP。PendSV 上下文切换中,需要将 PSR、PC、LR、R12 等寄存器压入栈。

  • PSR:程序状态寄存器包含中断标志、条件标志(溢出标志、零标志等)、处理器模式标志等信息;
  • PC:程序计数器存储下一条要执行的指令的地址,若调用函数时,PC 记录的是函数里的第一条语句;
  • LR 寄存器:链接寄存器/返回地址寄存器存的是函数返回的地址。

10 RT-Thread 学习记录之 IPC (线程间同步)

https://blog.csdn.net/m0_46430715/article/details/126690452?spm=1001.2014.3001.5502

IPC 用于保证多线程之间的同步与互斥。RT-Thread 支持的 IPC 有:信号量、互斥量、事件集、邮箱、消息队列等。

10.1 基础结构 rt_ipc_object

struct rt_ipc_object
{
  struct rt_object parent;
  rt_list_t suspend_thread;  // 挂起线程链表
}
  • IPC 对象初始化,即初始化挂起线程链表
    rt_inline rt_err_t rt_ipc_object_init(struct rt_ipc_object *ipc)
    {
      rt_list_init(&(ipc->suspend_thread));
      return RT_EOK;
    }
    

10.2 信号量

信号量用于 1)线程与线程之间的同步;2)中断与线程之间的同步;3)资源计数

信号量类型结构

struct rt_semaphore
{
  struct rt_ipc_object parent; 
  rt_uint16_t value; // 信号量技术
  rt_uint16_t reserved;
};

静态信号量对象的内存空间在编译时期就被编译器分配出来,放在初始化数据段(.data 段)或未初始化数据段(.bss 段)


10.3 互斥量

互斥量是一种特殊的二值信号量。互斥量支持递归访问且能防止线程优先级翻转。


10.4 补充:生产者消费者模型

生产者消费者模型是一个经典的多线程问题。

该问题的核心是1)要保证不让生产者在缓冲区满时向其中写入数据;2)不让消费者在缓冲区空时从中取数据。本质就是同步和互斥问题。

  • 互斥:把缓冲区看作一个临界资源保护区,在某一时刻只能允许一个访问者访问。解决办法:给予临界资源一个二值的信号量,即互斥量。
  • 同步:只有生产者生产后,消费者才能消费。解决办法:给予临界资源两个信号量,分别是表示缓冲区空闲空间的信号量、表示缓冲区数据的信号量。
posted @   MasterBean  阅读(165)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示