RT - thread学习(三) 内核解析
文章目录
一、RT - thread线程源码解析
在说明RT - thread的线程调度器的原理之前,我想在这里先讲一下链表的原理,方便后面的代码解析。
1、链表
链表介绍
链表是一种物理存储上非连续,数据元素的逻辑顺序通过链表中的指针链接次序,实现的一种线性存储结构。
简单的说就是
ps.在每个数据元素中放入下一个存储数据元素的地址即可。这样即构成了最简单的链表-单向链表结构。
这里拿我们熟悉的数组和链表做个比较,方便理解链表是怎么存区数据的。
数组 是一段连续的地址空间且数组中的每个元素大小是相等的。编译器只要知道数组中其中一个元素地址就能算出其他元素的地址。所以写程序时访问数组中每个元素特别方便,通过下标迅速就可以访问数组中任何元素。但是因为数组要提前先申请一段连续的地址空间存放数据,所以数组的总大小是固定的。不能变大和变小。对于数组的扩展会比较麻烦。同时因为数组元素地址连续,如果要在数组中插入/删除一个元素,需要移动大量元素,在内存中增加/删除元素的空间。
链表 是在原数据中(即节点中)加入链接元素的地址,构成一个链表节点的数据结构。通过链接元素地址串联起来的线性表。如表述中表述的每个元素节点在物理地址中不是顺序连续的,是通过元素的地址连接了的。所以要访问链表中一个元素,需要从第一个元素开始遍历整个链表,从而找到需要的链表元素数据。对应读取效率大大不如数组的,但是因为每个元素节点都有自己的地址。所以方便把内存中零散内存空间加以利用,提高了内存使用率。同时。增加和删除一个元素节点对于链表就比较方便,只要修改链接元素中的元素地址就可以了。
链表分类
链表一般可分为单向链表,双向链表,循环链表,双向循环链表和静态链表四大类。
因为RT-thread只用到双向循环链表,这边只对双向循环链表做具体的解析。
双向链表的应用
双向循环链表就是在单向链表的基础上加上了前向节点的地址。这样可以增加了用户遍历链表的灵活性。同时头尾节点的地址连起来,构成循环的链表结构。
链表节点初始化
rt_inline void rt_list_init(rt_list_t *l) //初始化节点链表的地址,将前面节点的地址
{
l->next = l->prev = l; //将下一地址和上一地址都指向自己
}
解析: 将 l 节点的地址 赋值给 l 节点中保存的前向节点的地址,同时也赋值给节点中保存的后向节点的地址。
就是前向地址指向自己,后向地址也指向自己。
在链表中L节点后面插入一个节点N
rt_inline void rt_list_insert_after(rt_list_t *l, rt_list_t *n)
{
l->next->prev = n;
n->next = l->next;
l->next = n;
n->prev = l;
}
解析: 在此之前我想我想先说明一下两个形参分别是什么。这个函数的目的是在链表中L节点后面插入一个节点n,所以rt_list_t *l只是链表中其中一个节点的地址。 不一定是链表中的第一个节点地址。也不一定是最后一个节点地址。 rt_list_t *n是需要插入链表的节点参数。
我这里以rt_list_t *l为链表中最后一个节点地址地址为例。
- l->next->prev = n // l->next是表示节点一 ,l->next->prev表示节点一的前向地址赋值为n的地址
- n->next = l->next; // n->next是表示节点n的后向地址赋值为节点一的地址,
- l->next = n; // l->next是表示节点三的后向地址赋值为节点n的地址
- n->prev = l; // n->prev表示节点n的前向地址赋值为节点节点三的地址。
在链表中L节点前面插入一个节点N
rt_inline void rt_list_insert_before(rt_list_t *l, rt_list_t *n)
{
l->prev->next = n;
n->prev = l->prev;
l->prev = n;
n->next = l;
}
解析:和上面同理。
我这里以rt_list_t *l为链表中最后一个节点地址地址为例。
- l->prev->next = n // l->next是表示节点二 ,l->prev->next表示节点二的后向地址赋值为n的地址
- n->prev =l->prev; // n->next是表示节点n的前向地址赋值为节点二的地址,
- l->prev = n; // l->prev 是表示节点三的前向地址赋值为节点n的地址
- n->next = l; // n->next表示节点n的后向地址赋值为节点节点三的地址。
在链表中删除节点n
rt_inline void rt_list_remove(rt_list_t *n)
{
n->next->prev = n->prev;
n->prev->next = n->next;
n->next = n->prev = n;
}
解析: n->next是图中的节点3地址,n->prev是图中的节点2地址。
- n->next->prev = n->prev; //n->next->prev 节点3地址前向地址赋值为节点2地址。
- n->prev->next = n->next; //n->prev->next 节点2地址后向地址赋值为节点3地址。
- n->next = n->prev = n; //初始化节点n
2、Object对象
RT-thread操作系统的实现其实用了面向对象的编程方式,其设计方式和Linuxd的内核实现有点相似。什么是面向对象的编程方式呢,其实就是把一个完整的操作系统抽象成若干个功能模块。我们把一个汽车构成比喻是一个操作系统构成。当我们拿到汽车的时候,不会从头到尾都去了解,比如不会去从车的螺丝,车的材料,车的弹簧数量等等去了解车。而是会从车中的发动机系统、冷却系统、悬挂系统、转向系统、变速箱系统、空调系统、燃油系统、驱动系统、制动系统、润滑系统等等功能系统去了解车。所以我们写操作系统也一样会将操作系统抽象成各个功能模块构成,再进行编写各功能模块。
我们先来了解下RT-thread有的功能模块有哪些。
这些功能模块在Object.c、rtdef.h 中有定义。Object对象(内核对象)则是对所有的功能模块进行管理的对象。
enum rt_object_class_type
{
RT_Object_Class_Null = 0x00, /**< The object is not used. */
RT_Object_Class_Thread = 0x01, /**< The object is a thread. */
RT_Object_Class_Semaphore = 0x02, /**< The object is a semaphore. */
RT_Object_Class_Mutex = 0x03, /**< The object is a mutex. */
RT_Object_Class_Event = 0x04, /**< The object is a event. */
RT_Object_Class_MailBox = 0x05, /**< The object is a mail box. */
RT_Object_Class_MessageQueue = 0x06, /**< The object is a message queue. */
RT_Object_Class_MemHeap = 0x07, /**< The object is a memory heap. */
RT_Object_Class_MemPool = 0x08, /**< The object is a memory pool. */
RT_Object_Class_Device = 0x09, /**< The object is a device. */
RT_Object_Class_Timer = 0x0a, /**< The object is a timer. */
RT_Object_Class_Unknown = 0x0c, /**< The object is unknown. */
RT_Object_Class_Static = 0x80 /**< The object is a static object. */
};
内核对象管理系统 负责访问/管理所有内核对象,包括线程、信号量、互斥量、事件、邮箱、消息队列、定时器、内存池、设备驱动等
对象容器 包含了每类对象的信息,包括对象类型、大小等。对象容器给每类内核对象分配一个链表,所有内核对象都被链接到该链表上
对于每一种具体内核对象和对象控制块,除了基本结构外,还有自己的扩展属性(私有属性),可认为每一种具体对象是抽象对象的派生,继承了基本对象的属性并在此基础上扩展了与自己相关的属性
对象管理模块 中定义了通用的数据结构来保存各种对象的共同属性,具体对象只要在此基础上加上自己某些特有属性就可以表示自己的特征。
而rt_object_information则抽象了对象类型,加入了一个双向链表指针数据域rt_list_node,从而将同类别的内核对象利用该双链指针链接起来,这些同类别的内核对象具有如下可能的特点:
可能在软件运行时生成,也可能在os初始化创建。
其存储类型可能为静态类型,也可能为动态类型(所谓动态类型这里是确指在内核堆上动态申请的内存区域用于存储相应的内核对象)。
在内存空间中,其位置并不连续。
如此以来,将这些内核对象在空间上不连续的变量,利用链表形成了可统一管理、可增可删、可检索的逻辑结构。
而rt_object_container内核容器,其本质是一个内核对象索引表,主要集中管理了下面的信息:
enum rt_object_class_type type:内核对象类别,每项表记录条目的类别
rt_list_t object_list:每类对象链表的头结点的链表指针数据域
rt_size_t object_size:该类个体的大小
利用宏将相应的链表进行选编译,在内核关键数据进行了裁剪管理。而对于内核本身的扩展性而言,如果需要增加新的内核功能,可以方便的增加新的内核对象类,并能方便的加入到这个内核对象容器中,利用公共的对外接口,实现统一管理,而不必对数据管理层进行额外的接口设计。
3、线程解析
RT-threadz中的线程是什么呢,他的对应功能是怎么样的呢。在我们的mcu/soc处理器中是怎么实现的呢。这个线程对我们有什么用的。接下来让我们来一一了解下。
线程介绍
我认为RT-thread中的线程可以理解成是一个能在芯片中并行运行的一个任务,而RT-thread中可以建立多个任务。即,多个任务能够并行运行。
举个例子。
void event1_thread_entry(void* parameter) //线程1函数
{
LED1_ON(); //打开LED1灯
delay(1000); //延时1s
LED1_OFF(); //关闭LED1灯
delay(1000); //延时1s
}
void event2_thread_entry(void* parameter) //线程2函数
{
LED2_ON(); //打开LED2灯
delay(1000); //延时1s
LED2_OFF(); //关闭LED2灯
delay(1000); //延时1s
}
int main(void)
{
unsigned char parameter = 0;
while(1){
event1_thread_entry(¶meter);
event1_thread_entry(¶meter);
}
return 0;
}
以上程序的亮灯流程如下图:
这时任务2是等任务1执行完后才能执行。而任务1也是等任务2执行完后才能执行。这样的话,CPU的执行的效率大大降低,怎么说呢。比如在执行LED1亮灯的1s中,MCU的什么都没有做,在空跑。所以为了提高MCU的执行效率,同时为了让程序任务的分工更加明确我们运用RT-thread的线程操作。
void event1_thread_entry(void* parameter) //线程1函数
{
LED1_ON(); //打开LED1灯
delay(1000); //延时1s
LED1_OFF(); //关闭LED1灯
delay(1000); //延时1s
}
void event2_thread_entry(void* parameter) //线程2函数
{
LED2_ON(); //打开LED2灯
delay(1000); //延时1s
LED2_OFF(); //关闭LED2灯
delay(1000); //延时1s
}
rt_thread_t led1_thread,led2_thread;
int main(void)
{
led1_thread = /* 线程控制块指针 */
rt_thread_create( "led1", /* 线程名字 */
led1_thread_entry, /* 线程入口函数 */
RT_NULL, /* 线程入口函数参数 */
512, /* 线程栈大小 */
1, /* 线程的优先级 */
5); /* 线程时间片 */
rt_thread_startup(led1_thread);
led2_thread = /* 线程控制块指针 */
rt_thread_create( "led2", /* 线程名字 */
led2_thread_entry, /* 线程入口函数 */
RT_NULL, /* 线程入口函数参数 */
512, /* 线程栈大小 */
1, /* 线程的优先级 */
5); /* 线程时间片 */
rt_thread_startup(led2_thread);
}
以上程序的亮灯流程如下图:
可以看到任务1和任务2是同时执行的。那mcu的是怎么实现这个功能的呢,难道有2个PC指针了吗。其实不是的,接下来我们来分析下mcu/soc处理器中是怎么实现的。使用源码来解析。
线程实现原理
抢占式实时多线程操作系统,首先它是分时的。因为单核的处理器IC在某一时刻只能执行一条语句,但是现在有两个任务,能同时吗,其实是不能的,那为什么我们说多任务“同时”进行呢,其实这个同时并不是真的同一时刻,只是让人们感觉起来时同时而已,什么意思呢,因为现在的单核的处理器IC执行速度比较快。所以我们可以这样设置,先让任务1执行1ms, 再让任务2执行1ms,人一般感知不了。所以这就是分时的原理了。时序的逻辑如下图:
可以看到这两个任务几乎是同时执行的。
几乎是同时执行,但也会有延时不是,这时不满足操作系统的实时性。这时,抢占式就出现了。什么是抢占式的呢,比如,我的任务1比较重要,我可以把这个的优先级设置的比较高。这样就能优先做这个事情。即,通过优先级参数,把优先级高的任务先完成在完成优先级低的任务。
线程实现和管理都是通过 线程结构体控制 该结构体称为线程控制块 struct rt_thread 操作系统中每一个线程都是通过各自的线程控制块进行实现和管理的。我们可以说线程控制块就是线程对象,每个线程都是线程对象抽象出来的实例。
线程源码讲解
线程的创建
如上面所说,每一个线程都是通过线程控制块进行实现和管理。所以创建一个线程首先要做的就是要创建一个线程控制块。
/**
* Thread structure
*/
struct rt_thread
{
/* rt object */
char name[RT_NAME_MAX]; /**< 线程的名字 */
rt_uint8_t type; /**< object对象中的类型 */
rt_uint8_t flags; /**< thread's flags */
rt_list_t list; /**< object对象线程链表的节点 */
rt_list_t tlist; /**< 在线程对象中线程链表的节点 */
/* stack point and entry */
void *sp; /**< 线程栈psp指针 */
void *entry; /**< 线程函数入口地址 */
void *parameter; /**< 线程函数入口参数 */
void *stack_addr; /**< 线程栈地址 */
rt_uint32_t stack_size; /**< 线程栈大小 */
/* error code */
rt_err_t error; /**< 错误码 */
rt_uint8_t stat; /**< 线程状态 */
/* priority */
rt_uint8_t current_priority; /**< 当前优先级 */
rt_uint8_t init_priority; /**< 初始化优先级 */
#if RT_THREAD_PRIORITY_MAX > 32
rt_uint8_t number;
rt_uint8_t high_mask;
#endif
rt_uint32_t number_mask;
#if defined(RT_USING_EVENT)
/* thread event */
rt_uint32_t event_set;
rt_uint8_t event_info;
#endif
rt_ubase_t init_tick; /**< 线程初始化时的时间 */
rt_ubase_t remaining_tick; /**< remaining tick */
struct rt_timer thread_timer; /**< built-in thread timer */
void (*cleanup)(struct rt_thread *tid); /**< cleanup function when thread exit */
rt_uint32_t user_data; /**< private user data beyond this thread */
};
typedef struct rt_thread *rt_thread_t;
线程的初始化
1.我们要先创建一个线程控制块。
2.可以调用rt_thread_init/rt_thread_create函数对线程控制块中的值进行赋值。这两个函数的区别是线程栈是自动生成还是有程序员自己指定。
3.将定义的线程控制块放入调度器。
//创建一个线程控制块
rt_thread_t thread1
static void event_thread_entry(void* parameter); //定义一个线程函数
#define RT_EVENT_THREAD_STACK_SIZE 512
static rt_uint8_t event_stack[RT_EVENT_THREAD_STACK_SIZE]; //定义一个线程栈
void event_thread_entry(void* parameter)
{
/*跑线程代码*/
}
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)
{
/* thread check */
RT_ASSERT(thread != RT_NULL);
RT_ASSERT(stack_start != RT_NULL);
//初始化线程控制块中的object对象参数,并在object对象线程链表中插入该线程链表节点。
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_uint8_t *rt_hw_stack_init(void *tentry,
void *parameter,
rt_uint8_t *stack_addr,
void *texit)
{
struct stack_frame *stack_frame;
rt_uint8_t *stk;
unsigned long i;
stk = stack_addr + sizeof(rt_uint32_t);
stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
stk -= sizeof(struct stack_frame);
stack_frame = (struct stack_frame *)stk;
/* init all register */
for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
{
((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
}
stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; /* r0 : argument */
stack_frame->exception_stack_frame.r1 = 0; /* r1 */
stack_frame->exception_stack_frame.r2 = 0; /* r2 */
stack_frame->exception_stack_frame.r3 = 0; /* r3 */
stack_frame->exception_stack_frame.r12 = 0; /* r12 */
stack_frame->exception_stack_frame.lr = (unsigned long)texit; /* lr */
stack_frame->exception_stack_frame.pc = (unsigned long)tentry; /* entry point, pc */
stack_frame->exception_stack_frame.psr = 0x01000000L; /* PSR */
/* return task's current stack address */
return stk;
}
//进一步初始化线程控制块中的各个参数
static 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) //线程执行时间(最小是一个系统时间)
{
/* init thread list */
rt_list_init(&(thread->tlist)); //链表节点地址初始化
thread->entry = (void *)entry; //线程函数地址初始化
thread->parameter = parameter; //线程函数参数初始化
/* stack init */
thread->stack_addr = stack_start; //线程栈地址赋值
thread->stack_size = stack_size; //线程大小赋值
/* init thread stack */
rt_memset(thread->stack_addr, '#', thread->stack_size); //线程栈内容初始化
#ifdef ARCH_CPU_STACK_GROWS_UPWARD
thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
(void *)((char *)thread->stack_addr),
(void *)rt_thread_exit);
#else
//初始化栈内容,且栈是后进先出的,所以需要将栈指针倒序。
thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
(rt_uint8_t *)((char *)thread->stack_addr + thread->stack_size sizeof(rt_ubase_t)),
(void *)rt_thread_exit);
#endif
/* priority init */
RT_ASSERT(priority < RT_THREAD_PRIORITY_MAX);
thread->init_priority = priority;
thread->current_priority = priority;
thread->number_mask = 0;
#if RT_THREAD_PRIORITY_MAX > 32
thread->number = 0;
thread->high_mask = 0;
#endif
/* tick init */
thread->init_tick = tick;
thread->remaining_tick = tick;
/* error and flags */
thread->error = RT_EOK;
thread->stat = RT_THREAD_INIT;
/* initialize cleanup function and user data */
thread->cleanup = 0;
thread->user_data = 0;
/* initialize thread timer */
rt_timer_init(&(thread->thread_timer),
thread->name,
rt_thread_timeout,
thread,
0,
RT_TIMER_FLAG_ONE_SHOT);
RT_OBJECT_HOOK_CALL(rt_thread_inited_hook, (thread));
return RT_EOK;
}
线程状态切换
在说线程调度前我先讲下线程有以下5个状态。
- 初始状态:RT_THREAD_INIT 这个状态是创建时的状态,线程控制块中的值是空的或者是不对的,不进行调度。
- 就绪状态:RT_THREAD_READY 这个状态是这个线程已经具备运行条件,在按照优先级和时间片顺序等 待被执行的状态。
- 运行状态:RT_THREAD_RUNNING 这个状态是说明这个线程正在被MCU执行中,当时间片执行完或者由线程释放执行权,将运行状态将变成就绪状态,重新进行调度。
- 挂起状态:RT_THREAD_SUSPEND 这个状态是这个任务没有满足执行条件,或者阻塞,暂停中,等待条件满足后MCU将这个线程设置成就绪态。这个状态也叫阻塞状态。
- 关闭状态:RT_THREAD_CLOSE 当前线程处于关闭状态,但是还没有释放内存的过程。当设置成这个状态后,当执行要idle线程时会释放这个线程的内存,这时线程才真正关闭了。
从上面的状态可以知道。初始状态,挂起状态,关闭状态,运行状态。说明线程没有准备好,或者条件不满足,或者删掉线程。或者这个线程正在运行,这4个状态都不在线程调度的范畴。
故RT-thread中的线程调度,就是在执行完上一个线程节点的时间片后。需要MCU在就绪态链表中找出满足时间片顺序和优先级的就绪状态线程。
RT-Thread 提供一系列的操作系统调用接口,使得线程的状态在这五个状态之间来回切换。几种状态间的转换关系如下图所示:
线程调度
上面我们说了很多线程的内容,现在来复习一下,
首先,线程其实就是(近似)一个并行运行的任务。它主要是通过线程控制块进行管理和实现的。
其次,线程的创建就是新建一个线程控制块,
接着,线程的初始化就是对线程控制块中的内容进行赋值。
最后,说的就是线程的执行。
那么多个线程我们什么时间执行什么线程,这个是怎么决定的呢,在rt-thread中线程的调度遵循时间片轮询,优先级抢占的规则。
什么是时间片轮训呢,mcu的执行速度是很快的,例如,12MHz的MCU执行一条赋值语句用时约1us。即执行完1000条赋值语句也才1ms。所以,时间片轮训就是给每个任务固定的执行时间。如果有4个线程,则线程1执行10ms后再给到线程2,线程2执行完10ms再给到线程3,线程3执行完10ms后再执行线程4,线程4执行完10ms后再给到线程1以此类推。
如上所说40ms就完成了4个线程的一轮执行,对于人的视角来说就是同时执行的。
那程序是怎么执行切换的呢,
- 要有一个记录每个线程执行的时间变量。thread->remaining_tick这个就是时间变量。
- 到时间后要能切换成其他的已就绪(已准备好执行)的线程。
以上说了时间片顺序轮询,那为什么还要优先级抢占呢。
原因很简单。假如有100个线程。50号线程是最重要的。是按键响应线程。我们怎么能等500ms后再执行呢,这样按键就会很迟钝。所以对于对优先级比较高的线程,rt-thread提供了优先级排序。可以供程序员操作。为 的就是把线程的优先级排个序,先执行高优先级的线程。再执行低优先级的线程。如果优先级相同,则按顺序 执行。
说了那么多线程调度的原理,我们来看下代码中是怎么实现线程调度的。主要来看schedular.c文件.
这里的函数主要对已准备好(就绪状态:RT_THREAD_READY )的线程进行筛选并将满足条件的线程切换执行.
调度器初始化函数
rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];
rt_uint32_t rt_thread_ready_priority_group;
void rt_system_scheduler_init(void)
{
register rt_base_t offset;
rt_scheduler_lock_nest = 0;
//打印最大优先级数
RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, ("start scheduler: max priority 0x%02x\n",
RT_THREAD_PRIORITY_MAX));
//初始化优先级链表
for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++)
{
rt_list_init(&rt_thread_priority_table[offset]);
}
//rt_current_priority初始值是最低优先级
rt_current_priority = RT_THREAD_PRIORITY_MAX - 1;
rt_current_thread = RT_NULL;
/* initialize ready priority group */
rt_thread_ready_priority_group = 0;
#if RT_THREAD_PRIORITY_MAX > 32
/* initialize ready table */
rt_memset(rt_thread_ready_table, 0, sizeof(rt_thread_ready_table));
#endif
//初始化线程失效链表
/* initialize thread defunct */
rt_list_init(&rt_thread_defunct);
}
开启系统调度器函数
void rt_system_scheduler_start(void)
{
register struct rt_thread *to_thread;
register rt_ubase_t highest_ready_priority;
#if RT_THREAD_PRIORITY_MAX > 32
register rt_ubase_t number;
number = __rt_ffs(rt_thread_ready_priority_group) - 1;
highest_ready_priority = (number << 3) + __rt_ffs(rt_thread_ready_table[number]) - 1;
#else
//这里我要说下,rt_thread_ready_priority_group是集成了所有ready线程的优先级,构成的变量
// __rt_ffs(rt_thread_ready_priority_group)则是在集成了所有ready线程的优先级的变量找出最高优先级
highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group) - 1;
#endif
//在最高优先级数组中找出需要运行的线程
to_thread = rt_list_entry(rt_thread_priority_table[highest_ready_priority].next,
struct rt_thread,
tlist);
//赋值当前运行的线程到rt_current_thread, 之后运行rt_current_thread,即是运行当前的线程控制块
rt_current_thread = to_thread;
//切换当前线程
rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp);
}
//切换当前线程
rt_hw_context_switch_to PROC
EXPORT rt_hw_context_switch_to //向外声明函数
; set to thread
LDR r1, =rt_interrupt_to_thread
STR r0, [r1] //将r0的值保存在rt_interrupt_to_thread变量中
//rt_interrupt_to_thread变量中保存的是需要切换的栈地址
; set from thread to 0
LDR r1, =rt_interrupt_from_thread
MOVS r0, #0x0
STR r0, [r1] //将rt_interrupt_from_thread变量清0
; set interrupt flag to 1
LDR r1, =rt_thread_switch_interrupt_flag
MOVS r0, #1
STR r0, [r1] //将rt_thread_switch_interrupt_flag变量置1
; set the PendSV and SysTick exception priority
LDR r0, =NVIC_SHPR3 //NVIC_SYSPRI2(system priority register)的地址给到r0
LDR r1, =NVIC_PENDSV_PRI //NVIC_PENDSV_PRI的地址给到r1
LDR r2, [r0,#0x00] ; read
ORRS r1,r1,r2 ; modify
STR r1, [r0] ; write-back
; trigger the PendSV exception (causes context switch)
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0] //触发PendSV exception PENDSV异常,切换线程上下文
; restore MSP
LDR r0, =SCB_VTOR
LDR r0, [r0]
LDR r0, [r0]
MSR msp, r0
; enable interrupts at processor level
CPSIE I
; never reach here!
ENDP
; r0 --> switch from thread stack
; r1 --> switch to thread stack
; psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
PendSV_Handler PROC
EXPORT PendSV_Handler
;根据cortext-m3内核的编程模型,进行pendsv中断前,内核已经自动的psr, pc, lr, r12, r3, r2, r1, r0把这些寄存器压入到发生切换的线程的堆栈psp中的去了,跳到中断程序,使用的堆栈自动切换成msp
; disable interrupt to protect context switch
MRS r2, PRIMASK
CPSID I //禁止中断
; get rt_thread_switch_interrupt_flag
LDR r0, =rt_thread_switch_interrupt_flag
LDR r1, [r0]
//如果rt_thread_switch_interrupt_flag==0 说明pendsv已经处理过了退出中断
CMP r1, #0x00
BEQ pendsv_exit ; pendsv already handled
MOVS r1, #0x00
STR r1, [r0] //清除rt_thread_switch_interrupt_flag标志
LDR r0, =rt_interrupt_from_thread
LDR r1, [r0]
//如果rt_interrupt_from_thread==0 说明需要对线程进行切换
CMP r1, #0x00
;意思是把线程堆栈中保存的R4-R11赋值到PSP堆栈中
BEQ switch_to_thread ; skip register save at the first time
;获取当前的线程栈指针到R1中
MRS r1, psp ; get from thread stack pointer
SUBS r1, r1, #0x20 ; space for {r4 - r7} and {r8 - r11}
LDR r0, [r0]
STR r1, [r0] ; update from thread stack pointer
;将r4 - r7压入到当前的线程栈中
STMIA r1!, {r4 - r7} ; push thread {r4 - r7} register to thread stack
MOV r4, r8 ; mov thread {r8 - r11} to {r4 - r7}
MOV r5, r9
MOV r6, r10
MOV r7, r11
;将r8 - r11压入到当前的线程栈中
STMIA r1!, {r4 - r7} ; push thread {r8 - r11} high register to thread stack
switch_to_thread ;意思是把线程堆栈中保存的R4-R11赋值到PSP堆栈中
LDR r1, =rt_interrupt_to_thread //导入需要切换的线程堆栈地址
LDR r1, [r1]
LDR r1, [r1] ; 导入需要切换的线程堆栈地址
LDMIA r1!, {r4 - r7} ; pop thread {r4 - r7} register from thread stack
PUSH {r4 - r7} ; push {r4 - r7} to MSP for copy {r8 - r11}
;从将要执行的栈中弹出这个线程中的寄存器r8-r11
LDMIA r1!, {r4 - r7} ; pop thread {r8 - r11} high register from thread stack to {r4 - r7}
MOV r8, r4 ; mov {r4 - r7} to {r8 - r11}
MOV r9, r5
MOV r10, r6
MOV r11, r7
;从将要执行的栈中弹出这个线程中的寄存器r4-r7
POP {r4 - r7} ; pop {r4 - r7} from MSP
MSR psp, r1 ; update stack pointer
pendsv_exit
; restore interrupt
MSR PRIMASK, r2
MOVS r0, #0x04
RSBS r0, r0, #0x00
BX r0
ENDP
这里总结下. 线程调度就是rt_schedular, 在执行完当前线程后, 会从就绪列表中__rt_ffs(rt_thread_ready_priority_group)找到优先级最高的线程, 然后执行PendSV的异常中断, 将当前线程的通用寄存器R4-R11压栈, 并导出将要执行的线程的通用寄存器R4-R11. 并切换线程栈.
参考资料
几种链表的比较 CSDN : https://blog.csdn.net/kobe0818/article/details/108479843
【RT-Thread 内核对象模型】:https://blog.csdn.net/sinat_32960911/article/details/127785062