支持多优先级

本贴最后更新于 593 天前,其中的信息可能已经事过景迁

实现多优先级的线程管理。在初始化线程以及线程调度器后,操作系统会选择优先级高的线程来执行。

多优先级线程管理方式

线程被创建之后会被插入到线程优先级列表,优先级列表的索引决定了线程的优先级别,索引越小,意味着优先级越高。

操作系统会从优先级列表的小索引开始执行线程。

阻塞延时的实现:屏蔽掉优先级列表中的线程,让调度器搜索不到。

RT-Thread 系统设计

  • 调度器
  • 线程
  • 空闲线程
  • 时钟函数

RT-Thread 多优先级设计1

1. 调度器程序设计

调度器程序都存放在 schedule.c 中,主要有:

全局变量:

  • 线程优先级表
  • 线程就绪优先级组 - 方便遍历线程
  • 当前线程优先级
  • 当前线程指针

函数实现:

  • 调度器初始化 - 初始化全局变量 2

  • 调度器启动 - 指定第一个运行的线程3

  • 调度器与线程的交互6

    • 存储线程
    • 删除线程

1.1 调度器如何与线程交互!!!7

线程被初始化时由用户指定优先级,启动的时候操作系统会根据优先级来决定插入到线程优先级表的位置。

为了快速地找到线程在线程优先级表的插入和移除的位置,RT-Thread 专门设计了一个线程就绪优先级表。

线程优先级表:

线程就绪优先级组是一个 32 位整数,每一个位对应一个优先级。一个就绪优先级组

/* 线程就绪优先级组 */ rt_uint32_t rt_thread_ready_priority_group;

那么线程优先级组如何帮助系统快速找到线程在优先级表的插入和移除的位置?

线程就绪优先级组的每一个位对应一个优先级,位 0 对应优先级 0,位 1 对应优先级 1,以此类推。比如,当优先级为 10 的线程已经准备好,那么就将线程就绪优先级组的位 10 置 1,表示线程已经就绪,然后根据 10 这个索引值,在线程优先级表 10(rt_thread_priority_table[10])的这个位置插入线程。

image

如何寻找优先级最高的线程?

通过__rt_ffs 函数 与 下__lowest_bit_bitmap[]数组来实现:

____rt_ffs 函数负责返回优先级组从左到右第一个出现 1 的位置 x,这个 x 代表最高优先级在优先级表中的位置。

__rt_ffs 函数实现如下:

/** * 该函数用于从一个 32 位的数中寻找第一个被置 1 的位(从低位开始), * 然后返回该位的索引(即位号) * * @return 返回第一个置 1 位的索引号。如果全为 0,则返回 0。 */ int __rt_ffs(int value) { if(value == 0) return 0; if(value & 0xff) return __lowest_bit_bitmap[value & 0xff] + 1; /* 检查 bits [15:08] */ if (value & 0xff00) return __lowest_bit_bitmap[(value & 0xff00) >> 8] + 9; /* 检查 bits [23:16] */ if (value & 0xff0000) return __lowest_bit_bitmap[(value & 0xff0000) >> 16] + 17; /* 检查 bits [31:24] */ return __lowest_bit_bitmap[(value & 0xff000000) >> 24] + 25; }

__rt_ffs 函数返回__lowest_bit_bitmap 数组中的值。

以 8 位整型数据的取值范围:0 ~ 255 作为数组__lowest_bit_bitmap 的索引。数组索引下的值是该索引值第一个出现 1 的位号。**举例:**十进制数 10 的二进制为:0000 1010,第一个出现 1 的位号是 bit1,则有__lowest_bit_bitmap[10]=1。

为何会这样设计?

要从一个 8 位整形数中从低位开始找出第一个置 1 的位,常规的方法是从低位开始一位一位的判断,优点是逻辑简单好理解,缺点是耗时,这里采取一种空间换时间的方法。

1.2 调度器的初始化 - 初始化调度器的全局变量

scheduler.c 的全局变量为:

/* ************************************************************************* * 全局变量 ************************************************************************* */ /* 线程控制块指针,用于指向当前线程 */ struct rt_thread *rt_current_thread; /* 线程优先级表 */ rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX]; /* 线程就绪优先级组 */ rt_uint32_t rt_thread_ready_priority_group; /* 表示当前运行线程的优先级 */ rt_uint8_t rt_current_priority;

调度器初始化函数的实现:

当前线程指针指向 RT_NULL

线程优先级表中每个链表节点都自指

线程就绪优先级组置 0

当前运行线程的优先级为最低(空闲线程优先级)

/* 初始化系统调度器 */ void rt_system_scheduler_init(void) { #if 0 register rt_base_t offset; /* 线程就绪列表初始化 */ for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++) { rt_list_init(&rt_thread_priority_table[offset]); } /* 初始化当前线程控制块指针 */ rt_current_thread = RT_NULL; #else register rt_base_t offset; /* 线程优先级表初始化 */ for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++) { rt_list_init(&rt_thread_priority_table[offset]); } /* 初始化当前优先级为空闲线程的优先级 */ rt_current_priority = RT_THREAD_PRIORITY_MAX - 1; /* 初始化当前线程控制块指针 */ rt_current_thread = RT_NULL; /* 初始化线程就绪优先级组 */ rt_thread_ready_priority_group = 0; #endif }

1.3 调度器启动 4

调度器的启动负责指定第一个运行的线程,即就虚表中优先级最高的线程。

  • 首先需要调用__rt__ffs 函数得到就绪列表中的最高优先级线程位置(还需要减 1,防止与 0 重复)

    • rt_thread_priority_table[x](x 为__rt_ffs 函数返回值-1)即最高优先级线程中链表节点的位置
  • 使用 rt_list_entry5得到最高优先级控制块的地址。

  • 将全局变量当前线程控制块指向最高优先级控制块的地址

  • 调用汇编函数,切换到新的线程

启动调度器函数实现:

/* 启动系统调度器 */ void rt_system_scheduler_start(void) { #if 0 register struct rt_thread *to_thread; /* 手动指定第一个运行的线程 */ to_thread = rt_list_entry(rt_thread_priority_table[0].next, struct rt_thread, tlist); rt_current_thread = to_thread; /* 切换到第一个线程,该函数在context_rvds.S中实现,在rthw.h声明, 用于实现第一次线程切换。当一个汇编函数在C文件中调用的时候, 如果有形参,则执行的时候会将形参传人到CPU寄存器r0。*/ rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp); #else register struct rt_thread *to_thread; register rt_ubase_t highest_ready_priority; /* 获取就绪的最高优先级 */ highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group) - 1; /* 获取将要运行线程的线程控制块 */ to_thread = rt_list_entry(rt_thread_priority_table[highest_ready_priority].next, struct rt_thread, tlist); rt_current_thread = to_thread; /* 切换到新的线程 */ rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp); /* 永远不会返回 */ #endif }

2. 线程程序设计

全局变量:

// 线程栈初始化函数 extern rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, rt_uint8_t *stack_addr); // 当前线程指针,定义在scheduler.c extern struct rt_thread *rt_current_thread; // 优先级表,定义在scheduler.c extern rt_uint32_t rt_thread_ready_priority_group;

线程程序都存放在 thread.c 中,主要有:

  • 线程初始化8

  • 启动线程 9- 线程与调度器交互,调用调度器提供的接口,将线程插入就绪列表

    • 恢复线程
  • 阻塞延时函数10​ ​- 与调度器交互

image

2.1 线程控制块

线程控制块设计:

/* 控制块定义 - 添加对象成员 */ struct rt_thread { struct rt_object rt_object; rt_list_t tlist; /* 线程链表节点 */ void *sp; /* 线程栈指针 */ void *entry; /* 线程入口地址 */ void *parameter; /* 线程形参 */ void *stack_addr; /* 线程栈起始地址 */ rt_uint32_t stack_size; /* 线程栈大小,单位为字节 */ rt_ubase_t remaining_tick; /* 用于实现阻塞延时 */ rt_uint8_t current_priority; /* 当前优先级 */ rt_uint8_t init_priority; /* 初始优先级 */ rt_uint32_t number_mask; /* 当前优先级掩码 */ rt_err_t error; /* 错误码 */ rt_uint8_t stat; /* 线程的状态 */ }; typedef struct rt_thread *rt_thread_t;

2.2 线程初始化

线程初始化函数:

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) { /* 线程对象初始化 */ /* 线程结构体开头部分的 4 个成员就是 rt_object_t 成员 */ /* 初始化对象属性后,把对象插入到容器的对应列表中去 */ rt_object_init((rt_object_t)thread, RT_Object_Class_Thread, name); rt_list_init(&(thread->tlist)); thread->entry = (void *)entry; thread->parameter = parameter; thread->stack_addr = stack_start; thread->stack_size = stack_size; /* 初始化线程栈,并返回线程栈 栈顶 指针,即低处指针 */ thread->sp = (void *)rt_hw_stack_init( thread->entry, thread->parameter, (void *)((char *)thread->stack_addr + thread->stack_size - 4)); //栈是由高到低排列的,所以需要加栈的大小,即栈底指针 thread->init_priority = priority; thread->current_priority = priority; thread->number_mask = 0; /* 错误码和状态 */ thread->error = RT_EOK; thread->stat = RT_THREAD_INIT; return RT_EOK; }

2.3 启动线程

启动线程函数 - ==**rt_err_t rt_thread_startup(rt_thread_t thread)**==

  • 设置线程的优先级

  • 设置该线程的掩码(用来操作线程插入或移除调度器)

  • 设置线程的状态

  • 将线程插入到就绪列表

/** * 启动一个线程并将其放到系统的就绪列表中 * * @param thread 待启动的线程 * * @return 操作状态, RT_EOK on OK, -RT_ERROR on error */ rt_err_t rt_thread_startup(rt_thread_t thread) { /* 设置当前优先级为初始优先级 */ thread->current_priority = thread->init_priority; thread->number_mask = 1 << thread->current_priority; /* 改变线程的状态为挂起状态 */ thread->stat = RT_THREAD_SUSPEND; /* 然后恢复线程,将线程插入到就绪列表 */ rt_thread_resume(thread); /* 由于是第一次调用,所以rt_thread_self()肯定是RT_NULL,故不会系统调用 */ if(rt_thread_self() != RT_NULL) { /* 系统调度 */ rt_schedule(); } return RT_EOK; }

rt_thread_resume 函数(将线程插入调度器列表)

/** * 该函数用于恢复一个线程然后将其放到就绪列表 * * @param thread 需要被恢复的线程 * * @return 操作状态, RT_EOK on OK, -RT_ERROR on error */ rt_err_t rt_thread_resume(rt_thread_t thread) { register rt_base_t temp; /* 将被恢复的线程必须在挂起态,否则返回错误码 */ if ((thread->stat & RT_THREAD_STAT_MASK) != RT_THREAD_SUSPEND) { return -RT_ERROR; } /* 关中断 */ temp = rt_hw_interrupt_disable(); /* 从挂起队列移除 */ rt_list_remove(&(thread->tlist)); /* 开中断 */ rt_hw_interrupt_enable(temp); /* 插入就绪列表 */ rt_schedule_insert_thread(thread); return RT_EOK; }

2.4 阻塞延时

阻塞延时,需要与调度器交互。阻塞延时函数会屏蔽掉线程优先级组中的某一位,调度器会看不到对应优先级的函数,所以不会执行相应的函数。

/* 实现阻塞延时 */ void rt_thread_delay(rt_tick_t tick) { register rt_base_t temp; struct rt_thread *thread; /* 失能中断 */ temp = rt_hw_interrupt_disable(); thread = rt_current_thread; thread->remaining_tick = tick; /* 改变线程状态 */ thread->stat = RT_THREAD_SUSPEND; rt_thread_ready_priority_group &= ~thread->number_mask; /* 开启中断 */ rt_hw_interrupt_enable(temp); /* 开启系统调度 */ rt_schedule(); }

3. 空闲线程

全局变量:

  • 空闲线程栈大小
  • 空闲线程栈(thread.c 的全局变量)
  • 线程优先级表(scheduler.c 的全局变量)
  • 空闲线程的线程控制块

函数:

  • 空闲线程函数 - 对一个全局变量计数
  • 空闲线程初始化

#include <rtthread.h> #include <rthw.h> /* 实现空闲线程相关程序 */ /* 空闲线程栈大小 */ #define IDLE_THREAD_STACK_SIZE 512 ALIGN(RT_ALIGN_SIZE) /* 空闲线程栈 */ static rt_uint8_t rt_thread_stack[IDLE_THREAD_STACK_SIZE]; extern rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX]; /* 空闲线程的线程控制块 */ struct rt_thread idle; rt_ubase_t rt_idletask_ctr = 0; /* 空闲线程函数,让一个全局变量+1 */ void rt_thread_idle_entry(void *parameter) { parameter = parameter; while (1) { rt_idletask_ctr ++; } } /** * @ingroup SystemInit * * 初始化空闲线程,启动空闲线程 * * @note 当系统初始化的时候该函数必须被调用 */ void rt_thread_idle_init(void) { /* 初始化线程 */ rt_thread_init(&idle, "idle", rt_thread_idle_entry, RT_NULL, &rt_thread_stack[0], sizeof(rt_thread_stack), RT_THREAD_PRIORITY_MAX - 1); /* 将线程插入到就绪列表 */ //rt_list_insert_before( &(rt_thread_priority_table[RT_THREAD_PRIORITY_MAX-1]),&(idle.tlist) ); rt_thread_startup(&idle); }

4. 时钟函数

时钟中断来临时的处理函数 - rt_tick_increase

扫描就绪列线程,让每个线程的延时计数器 remaining_tick 减 1,remaining_tick 是 0 的时候把线程的优先级组的对应位置 1

#include <rthw.h> #include <rtthread.h> #include <rtservice.h> static rt_tick_t rt_tick = 0; /* 系统时基计数器 */ extern rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX]; extern rt_uint32_t rt_thread_ready_priority_group; void rt_tick_increase(void) { rt_base_t i; struct rt_thread *thread; rt_tick++; /* 扫描就绪列表中所有线程的 remaining_tick,如果不为 0,则减 1 */ for( i = 0; i < RT_THREAD_PRIORITY_MAX; i++) { thread = rt_list_entry( rt_thread_priority_table[i].next, struct rt_thread, tlist); if( thread->remaining_tick > 0) { thread->remaining_tick--; if(thread->remaining_tick == 0) { rt_thread_ready_priority_group |= thread->number_mask; } } } #endif /* 系统调度 */ rt_schedule(); }

实验测试

#include <rtthread.h> #include <rthw.h> #include "ARMCM3.h" #include <rtconfig.h> #include <core_cm3.h> /* ************************************************************************* * 全局变量 ************************************************************************* */ rt_uint8_t flag1; rt_uint8_t flag2; extern rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX]; /* ************************************************************************* * 线程控制块& STACK &线程声明 ************************************************************************* */ /* 定义线程控制块 */ struct rt_thread rt_flag1_thread; struct rt_thread rt_flag2_thread; ALIGN(RT_ALIGN_SIZE) /* 定义线程栈 */ rt_uint8_t rt_flag1_thread_stack[512]; rt_uint8_t rt_flag2_thread_stack[512]; /* 线程声明 */ void flag1_thread_entry(void *p_arg); void flag2_thread_entry(void *p_arg); /* ************************************************************************* * 函数声明 ************************************************************************* */ void delay(rt_uint32_t count); int main(void) { /* 关中断 */ rt_hw_interrupt_disable(); /* SysTick中断频率设置 */ SysTick_Config( SystemCoreClock / RT_TICK_PER_SECOND ); // 25MHz / 100 /* 调度器初始化 */ rt_system_scheduler_init(); /* 初始化空闲线程 */ rt_thread_idle_init(); /* 初始化线程 */ rt_thread_init( &rt_flag1_thread, /* 线程控制块 */ "rt_flag1_thread", /* 线程名字,字符串形式 */ flag1_thread_entry, /* 线程入口地址 */ RT_NULL, /* 线程形参 */ &rt_flag1_thread_stack[0], /* 线程栈起始地址 */ sizeof(rt_flag1_thread_stack), 2); /* 线程栈大小,单位为字节 */ /* 将线程1插入就绪列表 */ //rt_list_insert_before( &(rt_thread_priority_table[0]),&(rt_flag1_thread.tlist) ); rt_thread_startup(&rt_flag1_thread); /* 初始化线程 */ rt_thread_init( &rt_flag2_thread, /* 线程控制块 */ "rt_flag2_thread", /* 线程名字,字符串形式 */ flag2_thread_entry, /* 线程入口地址 */ RT_NULL, /* 线程形参 */ &rt_flag2_thread_stack[0], /* 线程栈起始地址 */ sizeof(rt_flag2_thread_stack), 3); /* 线程栈大小,单位为字节 */ /* 将线程2插入就绪列表 */ //rt_list_insert_before( &(rt_thread_priority_table[1]),&(rt_flag2_thread.tlist) ); rt_thread_startup(&rt_flag2_thread); /* 启动系统调度器 */ rt_system_scheduler_start(); } void delay (rt_uint32_t count) { for(; count!=0; count--); } void flag1_thread_entry( void *p_arg ) { for ( ;; ) { #if 0 flag1 = 1; delay( 100 ); flag1 = 0; delay( 100 ); /* 线程切换,这里是手动切换 */ rt_schedule(); #else flag1 = 1; rt_thread_delay(2); flag1 = 0; rt_thread_delay(2); #endif } } void flag2_thread_entry( void *p_arg ) { for ( ;; ) { #if 0 flag2 = 1; delay( 100 ); flag2 = 0; delay( 100 ); /* 线程切换,这里是手动切换 */ rt_schedule(); #else flag2 = 1; rt_thread_delay(2); flag2 = 0; rt_thread_delay(2); #endif } } void SysTick_Handler(void) { /* 进入中断 */ rt_interrupt_enter(); rt_tick_increase(); /* 离开中断 */ rt_interrupt_leave(); }

实验现象:

两个线程仿佛同时执行

image


  1. RT-Thread 多优先级设计

    总体设计

    程序主要分为两大部分:

    1. 线程对象

    2. 调度器(管理线程对象)

    两部分的代码主要实现在 thread.c 与 scheduler.c 两个源代码中。

    .c 文件中都有专属于自己的属性 - 即全局变量,属性只有本源程序的函数才可以初始化。

    每个源程序中都要提供其他源程序可调用的接口。

    注:线程对象是通过调用调度器源程序本身提供的接口来实现对调度器的控制的。

    一份源程序主要分为两部分:

    1. 初始化属性的函数。
    2. 其他对象调用的接口(其实也是设置属性)。

    1. 调度器设计

    调度器程序都存放在 schedule.c 中,主要有:

    全局变量(调度器自身属性):

    • 线程优先级表 - 存放线程对象内部的链表节点
    • 线程就绪优先级组 - 方便遍历线程
    • 当前线程优先级
    • 当前线程指针

    初始化函数:

    • 调度器初始化函数 - 初始化全局变量

    其他对象调用的接口:

    • 将线程插入就绪列表函数
    • 将线程移出就绪列表函数
    • 调度函数 - 实现线程的调度 - 也可以被 main 函数调用

    main 函数调用的接口:

    • 调度器初始化函数
    • 调度函数

    2. 线程设计

    函数设计:

    • 线程初始化 - 初始化线程控制块的值
    • 线程启动 - 调用调度器提供的接口 - 插入就绪列表
    • 线程延时 - 改变线程状态,将调度器的优先级组的对应位置 0

  2. 1.2 调度器的初始化 - 初始化调度器的全局变量

  3. 1.3 调度器启动 1

  4. 启动调度器函数实现:

  5. /* 已知一个结构体里面的成员的地址,反推出该结构体的首地址 */ #define rt_container_of(ptr, type, member) \ ((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member))) #define rt_list_entry(node, type, member) \ rt_container_of(node, type, member)
  6. 1.1 调度器如何与线程交互!!!1

  7. 调度器与线程的交互1

  8. 2.2 线程初始化

  9. 2.3 启动线程

  10. 2.4 阻塞延时

  • C

    C 语言是一门通用计算机编程语言,应用广泛。C 语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。

    85 引用 • 165 回帖 • 2 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...