1.基于Threadx的基础知识讲解
1.1线程讲解
从之前的讲解中我们明白了关于操作系统就是将一个主函数分成多个主函数,我们称这每一个主函数就是一个任务。在Threadx里面我们将这类的任务称之为线程,在以后我们都会以线程来代表分开的主函数。
在ThreadX操作系统中他将函数映射成线程的一部分【ps:其他操作系统也这么干】。我们可以通过往该函数里面写我们的程序就相当于往线程里面写程序,就像这样子。
1 | /******************************************************************************************************************************** |
我们在上面的代码中主要有两个函数,分别是:
- void TASK_INSPECT_Timing_Light_APP(ULONG thread_input) //我称他为时序灯,专门提示时序是否正常的
- void TASK_INTER_Usart_Inter_APP(ULONG thread_input) //我称他为串口交互,专门打印信息的。
我们所看到程序是两个线程,一个专门时序检查,另一个利用串口发送数据,在tx_thread_sleep(x);这个函数就是threadx专门的延时函数,如果是按照我们上一章的,那是延时的是xms,我们也可以调整tx_initialize_low_level.s中的SYSTICK_CYCLES 来对时间进行更改,经过我所实验和验证在ms级别可以保证时序稳定。所以还是不要轻易更改了。
所以tx_thread_sleep(500); 就是延时500ms。至于为什么不用delay_ms我们在之后的教程里会讲的。
所以我们这一章的主要内容是把这两个函数给建立出来并学会去使用他。
那对于这一部分是关于介绍基本知识的,所以我们先回到这一章的主题。
1.2线程的状态
对于线程的状态我分为以下:
- 就绪状态:是线程排着队要运行的一种状态。线准备执行的时候,线程处于就绪状态。就绪的线程如果处于就绪状态的最高优先级的时候,才会执行,然后线程将其状态更改为执行状态。
- 挂起状态:线程因为特定原因不能执行,而是等待着这个原因满足或者消除的时候才可以从挂起状态到就绪状态,而且在就绪状态等待的时候要处于最高优先级的时候才能执行。
- 执行状态:该线程是彻彻底底的运行,在任何给定的时刻,只有一个线程处于执行状态。这也是因为处于执行状态的线程可以彻底的控制底层处理器。
- 中止状态:是用户让指定的线程直接让其泯灭的一种形式【ps:我感觉泯灭的这个词修饰是最恰当的】,被终止的线程无法再次执行,也没有挂起的资格,也就是说该线程直接消失。
- 完成状态:直接完成从函数退出的一种形式,也就是说代表线程函数直接return出函数的一种状态,这个状态跟终止状态权限是一样的,无法执行、无法挂起,无法就绪。
对于线程相互之间转换的关系,我们可以查看这样一个图。
线程在创建成功的时候可以由用户指定线程的状态,
当用户使用TX_AUTO_START【ps:在调用创建函数的时候用户可以填写这个】这个线程自动进入就绪状态,然后由系统进行调度执行。
当用户使用TX_DONT_START【ps:在调用创建函数的时候用户可以填写这个】这个线程就会进入挂起状态,除非用户调用恢复挂起的函数否则这个线程将处于永远挂起的一种状态。
当处于线程就绪状态的最高优先级的程序会直接运行,如果没有其他挂起的原因,就会因为tx_thread_sleep进行挂起,该函数是将线程挂起指定的时间然后再到指定时间后将其自动解除挂起进入到就绪状态准备执行。
注意:如果一个线程没有任何挂起条件,那在他优先级之下的程序就没有任何运行的机会,优先级之上的程序不影响。至于这个原因我会在本文的最后进行讲解,如果可以的话大家可以先尝试写出来原因呦。
即线程就绪->线程运行->线程挂起,这是一个必要的过程,挂起的线程在由用户或者程序自动去解除挂起去运行。当然,挂起的原因有很多:
- 我提到的一种是tx_thread_sleep();这是一种用户指定挂起而程序自动解除挂起的一种情况。挂起的线程是TX_SLEEP状态。注意也是一种挂起!挂起!挂起的方式。但是是系统能自动解除挂起就像休眠一样。
- 是由于消息队列导致的挂起,消息队列是线程与线程之间一个传递信息的通道,如果把消息比作邮件,你可以像海绵宝宝一样什么都不管就光等着邮箱里有邮件。或者是像正常人先干活,然后再时不时的看邮箱里是否有邮件。海绵宝宝那一种我称之为线程的挂起状态,就是如果消息队列中没有消息,线程挂起直到有消息的时候线程进入解除挂起进入就绪状态。而正常人那一种,线程没有挂起,就是路过看一眼是否有消息,有消息在干有消息的活,没消息就干没有消息该干的活。由于消息队列导致的挂起,是TX_QUEUE_SUSP状态。
- 事件导致的挂起,就比如说你突然腹泻难忍,马上就要出来的那种,然后厕所就在你身旁的们后,你肯定会停下手里活,去厕所爽一番,等待自己状态好了,才能在干之前的活。这从线程角度来说,就是突然满足用户规定的事件,而导致的挂起,其实也是一个样子。有事件导致的挂起是X_EVENT_FLAG状态。
- 信号量导致的挂起,这个可以用大家来看的综艺来比喻,比如说跑男有一个场景就是,如果步数倒计时就淘汰的那个场景。就是每个人有自己初始步数,当步数走完了就会淘汰,步数可以通过完成指定任务来增加。这就是一个完美的信号量的例子,把信号量的值比作步数。程序每执行到这里,则信号量就减少一个,直至到零,如果到零后又走一步,则线程就会挂起,对应的该人就会被淘汰。至于这个值也是可以增加的。由信号量导致的挂起是TX_SEMAPHORE_SUSP状态。
- 还有互斥信号量导致的挂起,互斥的信号量跟上面的信号量其实是一样的,但唯一不一样的地方就是值只有0/1两种值。如果比喻一下的话,我更愿意称之为开关,如果开关打开则对应的值可以是1。开关关闭则对应的信号量的值可以是0。至于这个开关你是要拨开还是关闭则由用户控制。但是只有关闭即信号量值为0的时候,线程才会挂起【ps:更像是线程的执行开关】,这种挂起是TX_MUTEX_SUSP状态。
- 还有一种是无论你是什么理由,我让你挂起你就点挂起的方式。如果强行举例子的话我跟感觉就是古代君于臣之间的关系,就是那种君要臣死,臣不得不死那种。对于我们线程来说,我要你挂,你不得不挂。至于这个引起的挂起,是TX_SUSPENDED状态。
- 还有就是内存块分配错误导致的挂起,就好比你在厕所蹲坑,马桶突然就堵了,那我问看这篇文章的各位,遇到这种情况您还蹲坑吗?当然就不蹲了,先把厕所通了在说对吧。对于内存块分配错误其实极大多数都是内存溢出,即马桶堵了,或者就是有人在用你的马桶【ps:至于干什么事情你们自己脑补】,这时候我们所能做的也就只能等待了对吧。对于线程来说就是挂起,我等你用完马桶我在用或者是通完马桶我在用。至于这种引起的线程挂起是TX_BLOCK_MEMORY状态。
- 还有的是内存分配错误导致的挂起,也就是即上一个场景,就是这么大的马桶你直接干满了。至于这种情况正常人肯定是先冲完马桶在忙乎接着干对吧。内存分配也是一样的道理,就是你一旦使用超过内存池的最大容量则等待分配内存的线程就会挂起。这种挂起的状态为TX_BYTE_MEMORY状态。
以上是全部引起的挂起的方式,我都给生动形象讲了以下。那接着上面图,我们知道了线程的就绪状态、执行状态和挂起状态的相互转换。那接下来就是线程完成和中止。
线程完成状态:就是线程从头到尾执行完毕,这种状态好比是在单片机上主函数没有写死循环,一下子主函数就执行完毕的那种。像这样子:
1
2
3
4
5
6int main(void)
{
int a=0;
a=a+1;
return 0;
}对于线程如果像这样子的情况一下子就执行完毕了,线程就处于完成状态。
线程中止状态:就是人为让线程中止,就是我们在写程序的时候那个线程只有前期有用中期跟猪一样不是玩【执行状态】就是睡【TX_SLEEP状态】,我们可以直接让他中止,即把那头猪给杀了。让他中止活着。这就是中止状态
无论处于完成状态还是中止状态,要想重新执行线程都必须先删除线程的在创建一个新的线程才可以。【ps:删除线程只能线程处于中止状态和完成状态才能删除否则删除失败】
即简单的讲一下,线程从创建开始就有两个路,第一个给我挂起,第二个就是自动运行这是用户能参与决策的。再然后就是挂起和就绪可以相互转换,线程可以由用户指定的方式来挂起。也可以由用户或程序自动的进行解除挂起来到就绪列表中,在位于就绪列表的最高优先级的线程享受执行的权利,其他就绪列表的优先级继续等待着,当该线程执行到满足上诉8条挂起条件中的任意一条则进入到挂起状态中,直到满足解挂条件【解挂条件:由用户或程序自动判别解除挂起状态恢复】进入到就绪状态。线程可以在执行状态将函数执行完毕,就会进入到完成状态【这也是完成状态进入的唯一一条路】或者执行到中止接口函数【中止接口函数:是threadx提供的用户可以在运行到这接口函数的时候中止指定的线程进入到中止的状态】中止指定线程【可以指定任意线程但不能是完成状态和中止状态的】。
1.3线程的抢占
线程的抢占类似于中断的抢占关系,如果单片机学的好的应该对这一块的内容很轻松的能被掌握住。但是如果对中断模棱两可或者根本不知中断的,也可以通过学习这一块的知识来掌握住。
中断,从名字里可以能了解一些,中断就是让程序中间断开执行一些其他的事情。但断开的程序有跑到哪里呢?总点有地方去吧!总点要回家对吧!那在讲解这部分的知识时,我想问看这篇文章的你们一个很简单的问题,就是程序的入口是什么?或者说是程序在哪里开始?在哪里停止呢?
答案是主函数,主函数是程序的入口,是程序的开始,也是程序的最终的归宿。但也有些函数,它不是主函数重要性却比主函数还要重要这个函数就是中断服务函数,但是可惜的是你的程序只能选则一个函数来执行,那你的程序在同时面临主函数和中断服务函数会选择那个呢?其实会选择中断服务函数,即如果程序执行在主函数,那么接下来主函数将不会执行,会跳转到中断服务函数中执行在中断服务函数,当中断服务函数执行完毕才会在回到主函数中继续完成主函数未完成的任务。这就叫做中断。但是通常中断并不是只是只有一个中断服务函数,而是会有很多中断服务函数像定时器中断、串口接收中断、外部中断等等。这些中断都可以打断主函数,让程序执行的权利放到自身身上。但如果程序执行的权利已经在中断服务函数中却又来一个比这更中断服务函数还重要的中断服务函数呢?或者程序在主函数中突然同时来了两个一样重要的中断服务函数呢?究竟程序会在那个函数执行呢?
- 对于第一种情况,我们设立了一个抢占优先级,抢占优先级高的可以让低优先级的程序中断执行,先执行完毕抢占优先级高的函数才能继续执行抢占优先级低的。但如果当前程序执行的抢占优先级已经最高的则低抢占优先级则只能等待高抢占优先级的函数执行完毕才能执行。如图所示:
2.对于第二种情况,我们设立了一个响应优先级,响应优先级高的优先执行,即当两个中断函数同时要执行且抢占优先级相同【ps:即相同重要】时,优先执行响应优先级高的中断,在执行响应优先级低的中断。
注意:如果响应优先级并不是抢占优先级,如果两个中断同时要执行但其中一个中断抢占优先级高,无论另一个抢占优先级高还是低,都是抢占优先级高的先执行,抢占优先拥有绝对地位。
例:有四个中断服务函数函数A、B、C、D,抢占优先级A>B=D>C。响应优先级A=C>D>B,以上所有执行时间10us。首先有以下场景请判断执行顺序:
- 当A、B、C、D同时要执行,问:执行的顺序为?
- 在程序执行1us后B和D中断同时来到,程序在运行13us后A中断来了,程序在运行到16us后C中断来了问执行顺序:
答案是:
- A D B C
- D B A B C
现在对于单片机中断优先级已经讲述完毕,接下来讲解线程与线程之间的关系,即抢占。
ThreadX对于线程与线程之间的关系是这样概括的。抢占是临时中断正在执行的线程以优先使用优先级更高的线程的过程。执行线程看不到该过程。当较高优先级的线程完成时,控制权将转移回发生抢占的确切位置。这是实时系统中非常重要的功能,因为它有助于快速响应重要的应用程序事件。
以实际情况说明,现在共有三个线程,A、B、C。优先级C>A>B
- 现在A、B、C都处于就绪状态中,问那个先执行呢?
- 现在B在运行,A处于就绪列表,C处于挂起列表,问此刻执行顺序是?
- 现在A在运行,B处于就绪列表,C处于挂起列表,问此刻的执行顺序是?
答案是:
- C先执行
- B A B
- A B
首先ThreadX中的优先级比较像是单片机中的抢占优先级。所以,在就绪列表中的线程,抢占优先级最高的先执行,在执行完毕之后会因为tx_thread_sleep而进行挂起,所以刚开始的是C先执行,执行完毕会因为tx_thread_sleep而挂起,现在挂起列表中有C、就绪列表中A B,现在就绪列表的A优先级最高所以现在A执行、执行完毕之后也会因为tx_thread_sleep而进行挂起,然后现在挂起列表中有A C,就绪列表中只有B所以现在B在执行,执行完毕之后由于tx_thread_sleep而挂起,现在挂起列表中有B C A而就绪列表中是没有的。所以第一个答案是C B A。即C优先级最高。
至于第二个和第三个我不做解释。大家可以根据第一个自行解答。
对于线程来说每时每刻都在有线程的挂起和解挂,至于为什么线程不是像中断服务函数一样执行完就完毕了,为什么还要涉及到挂起和解挂呢?大家请看这两个的区别:
这是中断服务函数的:
1 | //定时器3中断服务函数 |
这是线程函数的:
1 | void TASK_INTER_Usart_Inter_APP(ULONG thread_input) |
大家可以对比一下有什么区别。
其实最大的区别是线程函数中存在死循环【没有死循环,则函数会运行完毕,运行完毕的线程会处于完成状态】不会将线程函数全部执行完毕,而中断服务函数没有死循环,一下子就会执行完毕从该函数里退出。所以中断服务函数不会存在自身的挂起和解挂。但线程可不一样,线程不是以下子就执行完毕的而是在挂起->解挂->就绪列表中等待->执行->在挂起中循环。这样子是不是感觉线程更像主函数呀?
但这是单个线程,如果有两个、三个、甚至多个呢?而且每个线程重要程度不一样,这就导致了线程和线程之间可以相互抢占,他们的至高规则是高优先级的线程可以抢占低优先级的线程。即当低优先级线程执行的过程中,高优先级的线程直接中断低优先级的线程【低优先级线程没有执行完毕!!!!】。此时低优先级的线程是处于就绪状态,而高优先级的线程处于执行状态,只有当高优先级执行完毕,低优先级才能在断掉的地方执行。
大家可以想象一个场景,这里共有A、B、C,D四个线程优先级A>B>C>D,此刻A线程是正常的带有tx_thread_sleep运行,也就是自动挂起的那种。B线程是没有死循环的那种,也就是说程序能直接退出函数。C线程出现了很大的问题,就是C线程是光有死循环但没有tx_thread_sleep,也就是C线程永远不会挂起。D线程就是跟A线程一样完全正常的。
好此刻A线程处于最高优先级,并且A是正常的所以A肯定会执行的。
之后是B,但是B没有死循环所有B执行完一遍就直接退出函数了,状态就是完成状态。
对于奇葩的线程C,他有死循环,但是死循环中没有tx_thread_sleep延时函数,也就是说,这个线程C他永远不会挂起。但是C执行吗?虽然C没有延时函数来让他挂起,但他是会执行的。
至于最后一个D,他虽然是和A一样正常的。但是他不会运行的,原因是C线程不正常,C不会挂起,C永远处于就绪列表中对吧,但是D优先级很低,比C还低,C一直都会运行,就会一直压着D让其不会运行。所以D是不会允许的。
但是为什么C不正常会压着D而不会对A有任何干扰呢?原因是抢占。对C会一直执行,但是只要我A来了,你的C无论干什么都点给我让路。这就是A能正常执行的原因。
在上面的例子中,大家是不是对线程的抢占有些了解呢?但我这里有一道题,比较让人恶心,那让我和你一起分析以下。
例:现有三个线程A、B、C。代码如下,优先级A>B
1 | void A (void) //线程A |
1 | void B (void) //线程B |
求A、B线程的while执行一次是多少时间。
在这三个问题中要注意:线程和线程之间存在抢占关系!!!
对于这道题,A线程是最高优先级,享受到优先执行和优先运行即,A线程的运行时序是任何线程都无法打断的,这就保证了线程A运行时序的绝对准确。但是由于这种优先级的顺序,这就导致了B线程的时序不稳,大家看这道题,A、B线程挂起时间不是对称的,这就产生了一个问题,在有些时候,A线程不会打断B线程执行,而有些时候本该B线程运行却因为A线程在运行导致的B线程直接往后推迟运行,这样子线程B的时序即循环里面的时间不是我们想要的。
附:例题解析图【ps:感谢509实验室兰云龙的帮助】:
第一次整体调度:
线程 | 线程while里运行理想时间 | 线程while里运行实际时间 | 是否准确 |
---|---|---|---|
A | 1020ms | 1020ms | 是 |
B | 502ms | 502ms | 是 |
第二次整体调度:
线程 | 线程while里运行理想时间 | 线程while里运行实际时间 | 是否准确 |
---|---|---|---|
A | 1020ms | 1020ms | 是 |
B | 502ms | 518ms | 否 |
大家进行一下分析,线程A的时序随着时间推移是一直保持1020ms的,也就是一直是稳定的
但是到B线程呢?B大家可以根据数据来判断。B系统时序是不稳的。那导致B时序不稳的原因是什么?其实是具有大运行效用的线程A抢占B导致线程B的运行时间需要有部分A的时序来参与。
大家可能说了,我写的程序没有如果大的延时效应。但是是真的如此吗?
对于单片机中有一个大延时效应的外设,即串口发送。如果以下子发送和很多字节那就是大延时效应,即打印一串字符可能需要20ms左右。但是,经过我的分析,对串口其中一部分的时间是被用于等待和舍弃掉的所有在之后的文章中我会分析处理的方法
到这里,线程的抢占也就讲完了。如果有不懂得话可以通过qq私聊我,我在给进行讲解。
1.4 调度算法-时间片轮调度
上述的抢占也是调度算法的一种,是常用的调度算法,即线程调度,threadX对它解释是:
ThreadX根据线程的优先级调度线程。具有最高优先级的就绪线程将首先执行。如果相同优先级的多个线程准备好时,它们在执行先入先出(FIFO)的方式。
其次就是同等优先级调度算法,即循环调度,threadX对它解释是:
ThreadX支持具有相同优先级的多个线程的循环调度。这是通过对tx_thread_relinquish的协作调用来完成的。该服务为所有其他具有相同优先级的就绪线程提供了在tx_thread_relinquish调用程序再次执行之前执行的机会。
对于这个循环调度算法,最常用的时间片轮调度算法,threadX对它的解释是:
时间分片是循环调度的另一种形式。时间片指定线程在不放弃处理器的情况下可以执行的最大计时器滴答(计时器中断)数。在ThreadX中,可以在每个线程的基础上进行时间分片。线程的时间片在创建期间分配,并且可以在运行时进行修改。当时间片到期时,相同优先级的所有其他就绪线程将有机会在时间片线程再次执行之前执行。
在线程挂起,放弃,进行导致抢占的ThreadX服务调用或本身经过时间分片后,会为其分配新的线程时间分片。
如果抢占了时间片线程,它将在剩余时间片之前在其他优先级相同的就绪线程之前恢复。
我在这里进行讲解
- 抢占:针对于不同优先级的一种调度算法,其优先级的分配是用户做的,但是优先级分配的过程中用户既要关注与程序运行的时间,还要关注于程序的重要性。通常对应这种原则:运行时间越大,优先级越小。程序越重要,优先级越大。
- 循环调度:针对于同一优先级不同线程的一种调度算法,其主要的是什么时候放弃执行线程,对于这一种情况用户通过tx_thread_relinquish函数可以让执行先放弃这个线程。即原本正在执行A线程但是线程B正在A的后面排队等候A执行完毕B才能执行,等待B执行完毕之后A在执行,至于A线程和B线程什么时候执行完毕呢?那就是只要到tx_thread_relinquish函数及判断该线程执行完毕。
- 时间片轮调度:即设计一个时间片,在这个时间片内这个线程完毕的时候同优先级的其他线程就不会执行,而到了这个时间片而这个线程还未运行完毕,则这个线程就挂起,放入循环调度的末尾等待下一次的执行机会,这就是时间片轮调度,属于循环调度一种,但是放弃执行和执行是Threadx进行内部进行操作的就不需要用户调用tx_thread_relinquish来判断是否放弃执行本线程。
到这里我们的课程就讲解完毕了。
-------------本文结束感谢您的阅读-------------
本文链接: http://1ywd1.github.io/2021/02/08/Threadx%E7%AC%AC%E4%BA%94%E7%AB%A0%E8%AE%B2%E8%A7%A3/
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!